Skip to content

Commit f2a9b10

Browse files
authored
Skip addition of #syntax directive when docker engine version >= 23.0.0 (#1118)
* Adding check to skip addition of `#syntax` directive when docker engine version < `23.0.0` * Updated workflow runner-image
1 parent 4ae9f7c commit f2a9b10

File tree

5 files changed

+92
-82
lines changed

5 files changed

+92
-82
lines changed
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
name: Docker v20 Tests for dockerfile frontend test
2+
3+
on:
4+
push:
5+
branches: ['main', 'directive-syntax-further-changes']
6+
pull_request:
7+
branches: ['main']
8+
9+
jobs:
10+
test-docker-v20:
11+
name: Docker v20.10 Compatibility
12+
runs-on: ubuntu-22.04
13+
14+
steps:
15+
- uses: actions/checkout@v6
16+
17+
- uses: actions/setup-node@v5
18+
with:
19+
node-version: '18.x'
20+
21+
- name: Install Docker v20.10
22+
run: |
23+
sudo apt-get remove -y docker-ce docker-ce-cli containerd.io || true
24+
sudo apt-get update
25+
sudo apt-get install -y \
26+
ca-certificates \
27+
curl \
28+
gnupg \
29+
lsb-release
30+
sudo mkdir -p /etc/apt/keyrings
31+
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
32+
echo \
33+
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
34+
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
35+
sudo apt-get update
36+
sudo apt-get install -y docker-ce=5:20.10.* docker-ce-cli=5:20.10.* containerd.io
37+
sudo systemctl restart docker
38+
39+
- name: Verify Docker version, Install and Test
40+
run: |
41+
# Verify
42+
docker version
43+
DOCKER_VERSION=$(docker version --format '{{.Server.Version}}')
44+
if [[ ! "$DOCKER_VERSION" =~ ^20\.10\. ]]; then
45+
echo "ERROR: Expected Docker v20.10.x but got $DOCKER_VERSION"
46+
exit 1
47+
fi
48+
yarn install --frozen-lockfile
49+
yarn type-check
50+
yarn package
51+
yarn test-matrix --forbid-only src/test/cli.up.test.ts
52+
env:
53+
CI: true

src/spec-node/containerFeatures.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { LogLevel, makeLog } from '../spec-utils/log';
1111
import { FeaturesConfig, getContainerFeaturesBaseDockerFile, getFeatureInstallWrapperScript, getFeatureLayers, getFeatureMainValue, getFeatureValueObject, generateFeaturesConfig, Feature, generateContainerEnvs } from '../spec-configuration/containerFeaturesConfiguration';
1212
import { readLocalFile } from '../spec-utils/pfs';
1313
import { includeAllConfiguredFeatures } from '../spec-utils/product';
14-
import { createFeaturesTempFolder, DockerResolverParameters, getCacheFolder, getFolderImageName, getEmptyContextFolder, SubstitutedConfig, ensureDockerHubImageAccessible } from './utils';
14+
import { createFeaturesTempFolder, DockerResolverParameters, getCacheFolder, getFolderImageName, getEmptyContextFolder, SubstitutedConfig } from './utils';
1515
import { isEarlierVersion, parseVersion, runCommandNoPty } from '../spec-common/commonUtils';
1616
import { getDevcontainerMetadata, getDevcontainerMetadataLabel, getImageBuildInfoFromImage, ImageBuildInfo, ImageMetadataEntry, imageMetadataLabel, MergedDevContainerConfig } from './imageMetadata';
1717
import { supportsBuildContexts } from './dockerfileUtils';
@@ -195,15 +195,14 @@ export interface ImageBuildOptions {
195195

196196
async function getImageBuildOptions(params: DockerResolverParameters, config: SubstitutedConfig<DevContainerConfig>, dstFolder: string, baseName: string, imageBuildInfo: ImageBuildInfo): Promise<ImageBuildOptions> {
197197
const syntax = imageBuildInfo.dockerfile?.preamble.directives.syntax;
198-
const dockerHubAccessible = syntax ? await ensureDockerHubImageAccessible(params, 'docker/dockerfile', '1.4') : false;
199198
return {
200199
dstFolder,
201200
dockerfileContent: `
202201
FROM $_DEV_CONTAINERS_BASE_IMAGE AS dev_containers_target_stage
203202
${getDevcontainerMetadataLabel(getDevcontainerMetadata(imageBuildInfo.metadata, config, { featureSets: [] }, [], getOmitDevcontainerPropertyOverride(params.common)))}
204203
`,
205204
overrideTarget: 'dev_containers_target_stage',
206-
dockerfilePrefixContent: `${dockerHubAccessible && syntax ? `# syntax=${syntax}` : ''}
205+
dockerfilePrefixContent: `${syntax ? `# syntax=${syntax}` : ''}
207206
ARG _DEV_CONTAINERS_BASE_IMAGE=placeholder
208207
`,
209208
buildArgs: {
@@ -242,7 +241,10 @@ async function getFeaturesBuildOptions(params: DockerResolverParameters, devCont
242241
const useBuildKitBuildContexts = buildKitVersionParsed ? !isEarlierVersion(buildKitVersionParsed, minRequiredVersion) : false;
243242
const buildContentImageName = 'dev_container_feature_content_temp';
244243
const disableSELinuxLabels = useBuildKitBuildContexts && await isUsingSELinuxLabels(params);
245-
244+
// Access Docker engine version
245+
const dockerEngineVersionParsed = params.dockerEngineVersion?.versionMatch ? parseVersion(params.dockerEngineVersion.versionMatch) : undefined;
246+
const minDockerEngineVersion = [23, 0, 0];
247+
const skipDefaultSyntax = dockerEngineVersionParsed ? !isEarlierVersion(dockerEngineVersionParsed, minDockerEngineVersion) : false;
246248
const omitPropertyOverride = params.common.skipPersistingCustomizationsFromFeatures ? ['customizations'] : [];
247249
const imageMetadata = getDevcontainerMetadata(imageBuildInfo.metadata, devContainerConfig, featuresConfig, omitPropertyOverride, getOmitDevcontainerPropertyOverride(params.common));
248250
const { containerUser, remoteUser } = findContainerUsers(imageMetadata, composeServiceUser, imageBuildInfo.user);
@@ -265,9 +267,9 @@ async function getFeaturesBuildOptions(params: DockerResolverParameters, devCont
265267
;
266268
const syntax = imageBuildInfo.dockerfile?.preamble.directives.syntax;
267269
const omitSyntaxDirective = common.omitSyntaxDirective; // Can be removed when https://github.com/moby/buildkit/issues/4556 is fixed
268-
const dockerHubAccessible = !omitSyntaxDirective ? await ensureDockerHubImageAccessible(params, 'docker/dockerfile', '1.4') : false;
269270
const dockerfilePrefixContent = `${omitSyntaxDirective ? '' :
270-
useBuildKitBuildContexts && dockerHubAccessible && !(imageBuildInfo.dockerfile && supportsBuildContexts(imageBuildInfo.dockerfile)) ? '# syntax=docker/dockerfile:1.4' :
271+
skipDefaultSyntax ? (syntax ? `# syntax=${syntax}` : '') :
272+
useBuildKitBuildContexts && !(imageBuildInfo.dockerfile && supportsBuildContexts(imageBuildInfo.dockerfile)) ? '# syntax=docker/dockerfile:1.4' :
271273
syntax ? `# syntax=${syntax}` : ''}
272274
ARG _DEV_CONTAINERS_BASE_IMAGE=placeholder
273275
`;

src/spec-node/devContainers.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { LogLevel, LogDimensions, toErrorText, createCombinedLog, createTerminal
1717
import { dockerComposeCLIConfig } from './dockerCompose';
1818
import { Mount } from '../spec-configuration/containerFeaturesConfiguration';
1919
import { getPackageConfig, PackageConfiguration } from '../spec-utils/product';
20-
import { dockerBuildKitVersion, isPodman } from '../spec-shutdown/dockerUtils';
20+
import { dockerBuildKitVersion, dockerEngineVersion, isPodman } from '../spec-shutdown/dockerUtils';
2121
import { Event } from '../spec-utils/event';
2222

2323

@@ -205,6 +205,16 @@ export async function createDockerParams(options: ProvisionOptions, disposables:
205205
output,
206206
platformInfo
207207
}));
208+
209+
const dockerEngineVer = await dockerEngineVersion({
210+
cliHost,
211+
dockerCLI: dockerPath,
212+
dockerComposeCLI,
213+
env: cliHost.env,
214+
output,
215+
platformInfo
216+
});
217+
208218
return {
209219
common,
210220
parsedAuthority,
@@ -225,6 +235,7 @@ export async function createDockerParams(options: ProvisionOptions, disposables:
225235
updateRemoteUserUIDDefault,
226236
additionalCacheFroms: options.additionalCacheFroms,
227237
buildKitVersion,
238+
dockerEngineVersion: dockerEngineVer,
228239
isTTY: process.stdout.isTTY || options.logFormat === 'json',
229240
experimentalLockfile,
230241
experimentalFrozenLockfile,

src/spec-node/utils.ts

Lines changed: 1 addition & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ import { ImageMetadataEntry, MergedDevContainerConfig } from './imageMetadata';
2828
import { getImageIndexEntryForPlatform, getManifest, getRef } from '../spec-configuration/containerCollectionsOCI';
2929
import { requestEnsureAuthenticated } from '../spec-configuration/httpOCIRegistry';
3030
import { configFileLabel, findDevContainer, hostFolderLabel } from './singleContainer';
31-
import { requestResolveHeaders } from '../spec-utils/httpRequest';
3231
export { getConfigFilePath, getDockerfilePath, isDockerFileConfig } from '../spec-configuration/configuration';
3332
export { uriToFsPath, parentURI } from '../spec-configuration/configurationCommonUtils';
3433

@@ -37,12 +36,6 @@ export type BindMountConsistency = 'consistent' | 'cached' | 'delegated' | undef
3736

3837
export type GPUAvailability = 'all' | 'detect' | 'none';
3938

40-
// Constants for DockerHub registry + image access check
41-
const DEVCONTAINER_USER_AGENT = 'devcontainer';
42-
const DOCKER_MANIFEST_ACCEPT_HEADER = 'application/vnd.docker.distribution.manifest.v2+json';
43-
const DOCKERFILE_FRONTEND_CHECK_MAX_RETRIES = 5;
44-
const DOCKERFILE_FRONTEND_CHECK_RETRY_INTERVAL_MS = 2000;
45-
4639
// Generic retry function
4740
export async function retry<T>(fn: () => Promise<T>, options: { retryIntervalMilliseconds: number; maxRetries: number; output: Log }): Promise<T> {
4841
const { retryIntervalMilliseconds, maxRetries, output } = options;
@@ -124,6 +117,7 @@ export interface DockerResolverParameters {
124117
updateRemoteUserUIDDefault: UpdateRemoteUserUIDDefault;
125118
additionalCacheFroms: string[];
126119
buildKitVersion: { versionString: string; versionMatch?: string } | undefined;
120+
dockerEngineVersion: { versionString: string; versionMatch?: string } | undefined;
127121
isTTY: boolean;
128122
experimentalLockfile?: boolean;
129123
experimentalFrozenLockfile?: boolean;
@@ -609,71 +603,3 @@ export function runAsyncHandler(handler: () => Promise<void>) {
609603
}
610604
})();
611605
}
612-
613-
// Helper functions to construct DockerHub URLs
614-
function getDockerHubAuthUrl(imageName: string, version: string): string {
615-
return `https://auth.docker.io/token?service=registry.docker.io&scope=repository:${imageName}:pull&tag=${version}`;
616-
}
617-
618-
function getDockerHubRegistryUrl(imageName: string, version: string): string {
619-
return `https://registry-1.docker.io/v2/${imageName}/manifests/${version}`;
620-
}
621-
622-
async function checkDockerHubImageAccessible(params: DockerResolverParameters, imageName: string, version: string): Promise<void> {
623-
const { output } = params.common;
624-
625-
const authUrl = getDockerHubAuthUrl(imageName, version);
626-
const registryUrl = getDockerHubRegistryUrl(imageName, version);
627-
628-
const tokenRes = await requestResolveHeaders({
629-
type: 'GET',
630-
url: authUrl,
631-
headers: { 'user-agent': DEVCONTAINER_USER_AGENT }
632-
}, output);
633-
if (!tokenRes || tokenRes.statusCode !== 200) {
634-
throw new Error('Token fetch failed: status ' + (tokenRes?.statusCode ?? 'unknown'));
635-
}
636-
637-
let body: any;
638-
try {
639-
body = JSON.parse(tokenRes.resBody.toString());
640-
} catch (e) {
641-
throw new Error('Token parse failed: ' + (e instanceof Error ? e.message : String(e)));
642-
}
643-
const token: string | undefined = body?.token || body?.access_token;
644-
if (!token) {
645-
throw new Error('Token missing in auth response');
646-
}
647-
648-
const manifestRes = await requestResolveHeaders({
649-
type: 'GET',
650-
url: registryUrl,
651-
headers: {
652-
'user-agent': DEVCONTAINER_USER_AGENT,
653-
'authorization': `Bearer ${token}`,
654-
'accept': DOCKER_MANIFEST_ACCEPT_HEADER
655-
}
656-
}, output);
657-
if (!manifestRes || manifestRes.statusCode !== 200) {
658-
throw new Error('Manifest fetch failed: status ' + (manifestRes?.statusCode ?? 'unknown'));
659-
}
660-
}
661-
662-
export async function ensureDockerHubImageAccessible(params: DockerResolverParameters, imageName: string, version: string): Promise<boolean> {
663-
const { output } = params.common;
664-
try {
665-
await retry(
666-
async () => { await checkDockerHubImageAccessible(params, imageName, version); },
667-
{ maxRetries: DOCKERFILE_FRONTEND_CHECK_MAX_RETRIES, retryIntervalMilliseconds: DOCKERFILE_FRONTEND_CHECK_RETRY_INTERVAL_MS, output }
668-
);
669-
output.write('Dockerfile frontend is accessible in DockerHub registry.', LogLevel.Info);
670-
return true;
671-
} catch (err) {
672-
output.write(
673-
'Dockerfile frontend check failed after retries: ' +
674-
(err instanceof Error ? err.message : String(err)),
675-
LogLevel.Warning
676-
);
677-
return false;
678-
}
679-
}

src/spec-shutdown/dockerUtils.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,24 @@ export async function dockerBuildKitVersion(params: DockerCLIParameters | Partia
259259
}
260260
}
261261

262+
export async function dockerEngineVersion(params: DockerCLIParameters | PartialExecParameters | DockerResolverParameters): Promise<{ versionString: string; versionMatch?: string } | undefined> {
263+
try {
264+
const execParams = {
265+
...toExecParameters(params),
266+
print: true,
267+
};
268+
const result = await dockerCLI(execParams, 'version', '--format', '{{.Server.Version}}');
269+
const versionString = result.stdout.toString().trim();
270+
const versionMatch = versionString.match(/(?<major>[0-9]+)\.(?<minor>[0-9]+)\.(?<patch>[0-9]+)/);
271+
if (!versionMatch) {
272+
return { versionString };
273+
}
274+
return { versionString, versionMatch: versionMatch[0] };
275+
} catch {
276+
return undefined;
277+
}
278+
}
279+
262280
export async function dockerCLI(params: DockerCLIParameters | PartialExecParameters | DockerResolverParameters, ...args: string[]) {
263281
const partial = toExecParameters(params);
264282
return runCommandNoPty({

0 commit comments

Comments
 (0)