Skip to content

Commit 4ae9f7c

Browse files
authored
Merge pull request #1113 from Kaniska244/directive-dockerfile-perser-removal
Adding precautionary check for DockerHub registry availability in dev container cli
2 parents f61c25f + 30f0b34 commit 4ae9f7c

File tree

2 files changed

+103
-23
lines changed

2 files changed

+103
-23
lines changed

src/spec-node/containerFeatures.ts

Lines changed: 23 additions & 21 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 } from './utils';
14+
import { createFeaturesTempFolder, DockerResolverParameters, getCacheFolder, getFolderImageName, getEmptyContextFolder, SubstitutedConfig, ensureDockerHubImageAccessible } 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';
@@ -154,7 +154,7 @@ export async function getExtendImageBuildInfo(params: DockerResolverParameters,
154154
}
155155
};
156156
}
157-
return { featureBuildInfo: getImageBuildOptions(params, config, dstFolder, baseName, imageBuildInfo) };
157+
return { featureBuildInfo: await getImageBuildOptions(params, config, dstFolder, baseName, imageBuildInfo) };
158158
}
159159

160160
// Generates the end configuration.
@@ -193,24 +193,25 @@ export interface ImageBuildOptions {
193193
securityOpts: string[];
194194
}
195195

196-
function getImageBuildOptions(params: DockerResolverParameters, config: SubstitutedConfig<DevContainerConfig>, dstFolder: string, baseName: string, imageBuildInfo: ImageBuildInfo): ImageBuildOptions {
197-
const syntax = imageBuildInfo.dockerfile?.preamble.directives.syntax;
198-
return {
199-
dstFolder,
200-
dockerfileContent: `
196+
async function getImageBuildOptions(params: DockerResolverParameters, config: SubstitutedConfig<DevContainerConfig>, dstFolder: string, baseName: string, imageBuildInfo: ImageBuildInfo): Promise<ImageBuildOptions> {
197+
const syntax = imageBuildInfo.dockerfile?.preamble.directives.syntax;
198+
const dockerHubAccessible = syntax ? await ensureDockerHubImageAccessible(params, 'docker/dockerfile', '1.4') : false;
199+
return {
200+
dstFolder,
201+
dockerfileContent: `
201202
FROM $_DEV_CONTAINERS_BASE_IMAGE AS dev_containers_target_stage
202203
${getDevcontainerMetadataLabel(getDevcontainerMetadata(imageBuildInfo.metadata, config, { featureSets: [] }, [], getOmitDevcontainerPropertyOverride(params.common)))}
203204
`,
204-
overrideTarget: 'dev_containers_target_stage',
205-
dockerfilePrefixContent: `${syntax ? `# syntax=${syntax}` : ''}
206-
ARG _DEV_CONTAINERS_BASE_IMAGE=placeholder
205+
overrideTarget: 'dev_containers_target_stage',
206+
dockerfilePrefixContent: `${dockerHubAccessible && syntax ? `# syntax=${syntax}` : ''}
207+
ARG _DEV_CONTAINERS_BASE_IMAGE=placeholder
207208
`,
208-
buildArgs: {
209-
_DEV_CONTAINERS_BASE_IMAGE: baseName,
210-
} as Record<string, string>,
211-
buildKitContexts: {} as Record<string, string>,
212-
securityOpts: [],
213-
};
209+
buildArgs: {
210+
_DEV_CONTAINERS_BASE_IMAGE: baseName,
211+
} as Record<string, string>,
212+
buildKitContexts: {} as Record<string, string>,
213+
securityOpts: [],
214+
};
214215
}
215216

216217
function getOmitDevcontainerPropertyOverride(resolverParams: { omitConfigRemotEnvFromMetadata?: boolean }): (keyof DevContainerConfig & keyof ImageMetadataEntry)[] {
@@ -262,11 +263,12 @@ async function getFeaturesBuildOptions(params: DockerResolverParameters, devCont
262263
.replace('#{devcontainerMetadata}', getDevcontainerMetadataLabel(imageMetadata))
263264
.replace('#{containerEnvMetadata}', generateContainerEnvs(devContainerConfig.config.containerEnv, true))
264265
;
265-
const syntax = imageBuildInfo.dockerfile?.preamble.directives.syntax;
266-
const omitSyntaxDirective = common.omitSyntaxDirective; // Can be removed when https://github.com/moby/buildkit/issues/4556 is fixed
267-
const dockerfilePrefixContent = `${omitSyntaxDirective ? '' :
268-
useBuildKitBuildContexts && !(imageBuildInfo.dockerfile && supportsBuildContexts(imageBuildInfo.dockerfile)) ? '# syntax=docker/dockerfile:1.4' :
269-
syntax ? `# syntax=${syntax}` : ''}
266+
const syntax = imageBuildInfo.dockerfile?.preamble.directives.syntax;
267+
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;
269+
const dockerfilePrefixContent = `${omitSyntaxDirective ? '' :
270+
useBuildKitBuildContexts && dockerHubAccessible && !(imageBuildInfo.dockerfile && supportsBuildContexts(imageBuildInfo.dockerfile)) ? '# syntax=docker/dockerfile:1.4' :
271+
syntax ? `# syntax=${syntax}` : ''}
270272
ARG _DEV_CONTAINERS_BASE_IMAGE=placeholder
271273
`;
272274

src/spec-node/utils.ts

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ 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-
31+
import { requestResolveHeaders } from '../spec-utils/httpRequest';
3232
export { getConfigFilePath, getDockerfilePath, isDockerFileConfig } from '../spec-configuration/configuration';
3333
export { uriToFsPath, parentURI } from '../spec-configuration/configurationCommonUtils';
3434

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

3838
export type GPUAvailability = 'all' | 'detect' | 'none';
3939

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+
4046
// Generic retry function
4147
export async function retry<T>(fn: () => Promise<T>, options: { retryIntervalMilliseconds: number; maxRetries: number; output: Log }): Promise<T> {
4248
const { retryIntervalMilliseconds, maxRetries, output } = options;
@@ -46,7 +52,11 @@ export async function retry<T>(fn: () => Promise<T>, options: { retryIntervalMil
4652
return await fn();
4753
} catch (err) {
4854
lastError = err;
49-
output.write(`Retrying (Attempt ${i}) with error '${toErrorText(err)}'`, LogLevel.Warning);
55+
output.write(
56+
`Retrying (Attempt ${i}) with error
57+
'${toErrorText(String(err && (err.stack || err.message) || err))}'`,
58+
LogLevel.Warning
59+
);
5060
await new Promise(resolve => setTimeout(resolve, retryIntervalMilliseconds));
5161
}
5262
}
@@ -599,3 +609,71 @@ export function runAsyncHandler(handler: () => Promise<void>) {
599609
}
600610
})();
601611
}
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+
}

0 commit comments

Comments
 (0)