From 7716a885bb2e850cc222d7499c4bbdd662adb84e Mon Sep 17 00:00:00 2001 From: Kaniska Date: Fri, 28 Nov 2025 16:53:14 +0000 Subject: [PATCH 1/5] Adding precautionary check for dockerhub registry availability in devcontainer cli --- src/spec-node/containerFeatures.ts | 101 +++++++++++++++++++++++------ src/spec-node/utils.ts | 6 +- 2 files changed, 85 insertions(+), 22 deletions(-) diff --git a/src/spec-node/containerFeatures.ts b/src/spec-node/containerFeatures.ts index c7e42a56a..d7774d762 100644 --- a/src/spec-node/containerFeatures.ts +++ b/src/spec-node/containerFeatures.ts @@ -11,11 +11,12 @@ import { LogLevel, makeLog } from '../spec-utils/log'; import { FeaturesConfig, getContainerFeaturesBaseDockerFile, getFeatureInstallWrapperScript, getFeatureLayers, getFeatureMainValue, getFeatureValueObject, generateFeaturesConfig, Feature, generateContainerEnvs } from '../spec-configuration/containerFeaturesConfiguration'; import { readLocalFile } from '../spec-utils/pfs'; import { includeAllConfiguredFeatures } from '../spec-utils/product'; -import { createFeaturesTempFolder, DockerResolverParameters, getCacheFolder, getFolderImageName, getEmptyContextFolder, SubstitutedConfig } from './utils'; +import { createFeaturesTempFolder, DockerResolverParameters, getCacheFolder, getFolderImageName, getEmptyContextFolder, SubstitutedConfig, retry } from './utils'; import { isEarlierVersion, parseVersion, runCommandNoPty } from '../spec-common/commonUtils'; import { getDevcontainerMetadata, getDevcontainerMetadataLabel, getImageBuildInfoFromImage, ImageBuildInfo, ImageMetadataEntry, imageMetadataLabel, MergedDevContainerConfig } from './imageMetadata'; import { supportsBuildContexts } from './dockerfileUtils'; import { ContainerError } from '../spec-common/errors'; +import { requestResolveHeaders } from '../spec-utils/httpRequest'; // Escapes environment variable keys. // @@ -154,7 +155,7 @@ export async function getExtendImageBuildInfo(params: DockerResolverParameters, } }; } - return { featureBuildInfo: getImageBuildOptions(params, config, dstFolder, baseName, imageBuildInfo) }; + return { featureBuildInfo: await getImageBuildOptions(params, config, dstFolder, baseName, imageBuildInfo) }; } // Generates the end configuration. @@ -193,24 +194,25 @@ export interface ImageBuildOptions { securityOpts: string[]; } -function getImageBuildOptions(params: DockerResolverParameters, config: SubstitutedConfig, dstFolder: string, baseName: string, imageBuildInfo: ImageBuildInfo): ImageBuildOptions { - const syntax = imageBuildInfo.dockerfile?.preamble.directives.syntax; - return { - dstFolder, - dockerfileContent: ` +async function getImageBuildOptions(params: DockerResolverParameters, config: SubstitutedConfig, dstFolder: string, baseName: string, imageBuildInfo: ImageBuildInfo): Promise { + const syntax = imageBuildInfo.dockerfile?.preamble.directives.syntax; + const dockerHubAccessible = syntax ? await ensureDockerfileFrontendAccessible(params) : false; + return { + dstFolder, + dockerfileContent: ` FROM $_DEV_CONTAINERS_BASE_IMAGE AS dev_containers_target_stage ${getDevcontainerMetadataLabel(getDevcontainerMetadata(imageBuildInfo.metadata, config, { featureSets: [] }, [], getOmitDevcontainerPropertyOverride(params.common)))} `, - overrideTarget: 'dev_containers_target_stage', - dockerfilePrefixContent: `${syntax ? `# syntax=${syntax}` : ''} - ARG _DEV_CONTAINERS_BASE_IMAGE=placeholder + overrideTarget: 'dev_containers_target_stage', + dockerfilePrefixContent: `${dockerHubAccessible && syntax ? `# syntax=${syntax}` : ''} + ARG _DEV_CONTAINERS_BASE_IMAGE=placeholder `, - buildArgs: { - _DEV_CONTAINERS_BASE_IMAGE: baseName, - } as Record, - buildKitContexts: {} as Record, - securityOpts: [], - }; + buildArgs: { + _DEV_CONTAINERS_BASE_IMAGE: baseName, + } as Record, + buildKitContexts: {} as Record, + securityOpts: [], + }; } function getOmitDevcontainerPropertyOverride(resolverParams: { omitConfigRemotEnvFromMetadata?: boolean }): (keyof DevContainerConfig & keyof ImageMetadataEntry)[] { @@ -221,6 +223,62 @@ function getOmitDevcontainerPropertyOverride(resolverParams: { omitConfigRemotEn return []; } +async function checkDockerfileFrontendAccessibleOrThrow(params: DockerResolverParameters): Promise { + const { output } = params.common; + + const tokenRes = await requestResolveHeaders({ + type: 'GET', + url: 'https://auth.docker.io/token?service=registry.docker.io&scope=repository:docker/dockerfile:pull&tag=1.4', + headers: { 'user-agent': 'devcontainer' } + }, output); + if (!tokenRes || tokenRes.statusCode !== 200) { + throw new Error('Token fetch failed: status ' + (tokenRes?.statusCode ?? 'unknown')); + } + + let body: any; + try { + body = JSON.parse(tokenRes.resBody.toString()); + } catch (e) { + throw new Error('Token parse failed: ' + (e instanceof Error ? e.message : String(e))); + } + const token: string | undefined = body?.token || body?.access_token; + if (!token) { + throw new Error('Token missing in auth response'); + } + + const manifestRes = await requestResolveHeaders({ + type: 'GET', + url: 'https://registry-1.docker.io/v2/docker/dockerfile/manifests/1.4', + headers: { + 'user-agent': 'devcontainer', + 'authorization': `Bearer ${token}`, + 'accept': 'application/vnd.docker.distribution.manifest.v2+json' + } + }, output); + if (!manifestRes || manifestRes.statusCode !== 200) { + throw new Error('Manifest fetch failed: status ' + (manifestRes?.statusCode ?? 'unknown')); + } +} + +async function ensureDockerfileFrontendAccessible(params: DockerResolverParameters): Promise { + const { output } = params.common; + try { + await retry( + async () => { await checkDockerfileFrontendAccessibleOrThrow(params); }, + { maxRetries: 5, retryIntervalMilliseconds: 2000, output } + ); + output.write('Dockerfile frontend is accessible in DockerHub registry.', LogLevel.Info); + return true; + } catch (err) { + output.write( + 'Dockerfile frontend check failed after retries: ' + + (err instanceof Error ? err.message : String(err)), + LogLevel.Warning + ); + return false; + } +} + async function getFeaturesBuildOptions(params: DockerResolverParameters, devContainerConfig: SubstitutedConfig, featuresConfig: FeaturesConfig, baseName: string, imageBuildInfo: ImageBuildInfo, composeServiceUser: string | undefined): Promise { const { common } = params; const { cliHost, output } = common; @@ -262,11 +320,12 @@ async function getFeaturesBuildOptions(params: DockerResolverParameters, devCont .replace('#{devcontainerMetadata}', getDevcontainerMetadataLabel(imageMetadata)) .replace('#{containerEnvMetadata}', generateContainerEnvs(devContainerConfig.config.containerEnv, true)) ; - const syntax = imageBuildInfo.dockerfile?.preamble.directives.syntax; - const omitSyntaxDirective = common.omitSyntaxDirective; // Can be removed when https://github.com/moby/buildkit/issues/4556 is fixed - const dockerfilePrefixContent = `${omitSyntaxDirective ? '' : - useBuildKitBuildContexts && !(imageBuildInfo.dockerfile && supportsBuildContexts(imageBuildInfo.dockerfile)) ? '# syntax=docker/dockerfile:1.4' : - syntax ? `# syntax=${syntax}` : ''} + const syntax = imageBuildInfo.dockerfile?.preamble.directives.syntax; + const omitSyntaxDirective = common.omitSyntaxDirective; // Can be removed when https://github.com/moby/buildkit/issues/4556 is fixed + const dockerHubAccessible = !omitSyntaxDirective ? await ensureDockerfileFrontendAccessible(params) : false; + const dockerfilePrefixContent = `${omitSyntaxDirective ? '' : + useBuildKitBuildContexts && dockerHubAccessible && !(imageBuildInfo.dockerfile && supportsBuildContexts(imageBuildInfo.dockerfile)) ? '# syntax=docker/dockerfile:1.4' : + syntax ? `# syntax=${syntax}` : ''} ARG _DEV_CONTAINERS_BASE_IMAGE=placeholder `; diff --git a/src/spec-node/utils.ts b/src/spec-node/utils.ts index 81f6b6b17..49e17e96e 100644 --- a/src/spec-node/utils.ts +++ b/src/spec-node/utils.ts @@ -46,7 +46,11 @@ export async function retry(fn: () => Promise, options: { retryIntervalMil return await fn(); } catch (err) { lastError = err; - output.write(`Retrying (Attempt ${i}) with error '${toErrorText(err)}'`, LogLevel.Warning); + output.write( + `Retrying (Attempt ${i}) with error + '${toErrorText(String(err && (err.stack || err.message) || err))}'`, + LogLevel.Warning + ); await new Promise(resolve => setTimeout(resolve, retryIntervalMilliseconds)); } } From 8ad5a7591d94b6c6e03aae812b5e51c9e65ff43b Mon Sep 17 00:00:00 2001 From: Kaniska Date: Mon, 1 Dec 2025 12:44:17 +0000 Subject: [PATCH 2/5] Using constants --- src/spec-node/containerFeatures.ts | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/spec-node/containerFeatures.ts b/src/spec-node/containerFeatures.ts index d7774d762..453642f21 100644 --- a/src/spec-node/containerFeatures.ts +++ b/src/spec-node/containerFeatures.ts @@ -18,6 +18,14 @@ import { supportsBuildContexts } from './dockerfileUtils'; import { ContainerError } from '../spec-common/errors'; import { requestResolveHeaders } from '../spec-utils/httpRequest'; +// Constants for DockerHub registry access check +const DOCKERHUB_AUTH_URL = 'https://auth.docker.io/token?service=registry.docker.io&scope=repository:docker/dockerfile:pull&tag=1.4'; +const DOCKERHUB_REGISTRY_URL = 'https://registry-1.docker.io/v2/docker/dockerfile/manifests/1.4'; +const DEVCONTAINER_USER_AGENT = 'devcontainer'; +const DOCKER_MANIFEST_ACCEPT_HEADER = 'application/vnd.docker.distribution.manifest.v2+json'; +const DOCKERFILE_FRONTEND_CHECK_MAX_RETRIES = 5; +const DOCKERFILE_FRONTEND_CHECK_RETRY_INTERVAL_MS = 2000; + // Escapes environment variable keys. // // Environment variables must contain: @@ -223,13 +231,13 @@ function getOmitDevcontainerPropertyOverride(resolverParams: { omitConfigRemotEn return []; } -async function checkDockerfileFrontendAccessibleOrThrow(params: DockerResolverParameters): Promise { +async function checkDockerfileFrontendAccessible(params: DockerResolverParameters): Promise { const { output } = params.common; const tokenRes = await requestResolveHeaders({ type: 'GET', - url: 'https://auth.docker.io/token?service=registry.docker.io&scope=repository:docker/dockerfile:pull&tag=1.4', - headers: { 'user-agent': 'devcontainer' } + url: DOCKERHUB_AUTH_URL, + headers: { 'user-agent': DEVCONTAINER_USER_AGENT } }, output); if (!tokenRes || tokenRes.statusCode !== 200) { throw new Error('Token fetch failed: status ' + (tokenRes?.statusCode ?? 'unknown')); @@ -248,11 +256,11 @@ async function checkDockerfileFrontendAccessibleOrThrow(params: DockerResolverPa const manifestRes = await requestResolveHeaders({ type: 'GET', - url: 'https://registry-1.docker.io/v2/docker/dockerfile/manifests/1.4', + url: DOCKERHUB_REGISTRY_URL, headers: { - 'user-agent': 'devcontainer', + 'user-agent': DEVCONTAINER_USER_AGENT, 'authorization': `Bearer ${token}`, - 'accept': 'application/vnd.docker.distribution.manifest.v2+json' + 'accept': DOCKER_MANIFEST_ACCEPT_HEADER } }, output); if (!manifestRes || manifestRes.statusCode !== 200) { @@ -264,10 +272,10 @@ async function ensureDockerfileFrontendAccessible(params: DockerResolverParamete const { output } = params.common; try { await retry( - async () => { await checkDockerfileFrontendAccessibleOrThrow(params); }, - { maxRetries: 5, retryIntervalMilliseconds: 2000, output } + async () => { await checkDockerfileFrontendAccessible(params); }, + { maxRetries: DOCKERFILE_FRONTEND_CHECK_MAX_RETRIES, retryIntervalMilliseconds: DOCKERFILE_FRONTEND_CHECK_RETRY_INTERVAL_MS, output } ); - output.write('Dockerfile frontend is accessible in DockerHub registry.', LogLevel.Info); + output.write('Dockerfile frontend is accessible in DockerHub registry.', LogLevel.Info); return true; } catch (err) { output.write( From a30599041539ab1704e03a388c02a1b4e2eeb1f0 Mon Sep 17 00:00:00 2001 From: Kaniska Date: Mon, 1 Dec 2025 20:39:10 +0530 Subject: [PATCH 3/5] Update src/spec-node/containerFeatures.ts Co-authored-by: Sam Byng --- src/spec-node/containerFeatures.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spec-node/containerFeatures.ts b/src/spec-node/containerFeatures.ts index 453642f21..c4cc7b36f 100644 --- a/src/spec-node/containerFeatures.ts +++ b/src/spec-node/containerFeatures.ts @@ -18,7 +18,7 @@ import { supportsBuildContexts } from './dockerfileUtils'; import { ContainerError } from '../spec-common/errors'; import { requestResolveHeaders } from '../spec-utils/httpRequest'; -// Constants for DockerHub registry access check +// Constants for DockerHub registry + Dockerfile v1.4 image access check const DOCKERHUB_AUTH_URL = 'https://auth.docker.io/token?service=registry.docker.io&scope=repository:docker/dockerfile:pull&tag=1.4'; const DOCKERHUB_REGISTRY_URL = 'https://registry-1.docker.io/v2/docker/dockerfile/manifests/1.4'; const DEVCONTAINER_USER_AGENT = 'devcontainer'; From 4eb05274d51f218f11e4c32501829bc2b3aa5e88 Mon Sep 17 00:00:00 2001 From: Kaniska Date: Mon, 1 Dec 2025 16:26:15 +0000 Subject: [PATCH 4/5] Making the functions generic --- src/spec-node/containerFeatures.ts | 71 ++-------------------------- src/spec-node/utils.ts | 76 +++++++++++++++++++++++++++++- 2 files changed, 78 insertions(+), 69 deletions(-) diff --git a/src/spec-node/containerFeatures.ts b/src/spec-node/containerFeatures.ts index c4cc7b36f..124e2415f 100644 --- a/src/spec-node/containerFeatures.ts +++ b/src/spec-node/containerFeatures.ts @@ -11,20 +11,11 @@ import { LogLevel, makeLog } from '../spec-utils/log'; import { FeaturesConfig, getContainerFeaturesBaseDockerFile, getFeatureInstallWrapperScript, getFeatureLayers, getFeatureMainValue, getFeatureValueObject, generateFeaturesConfig, Feature, generateContainerEnvs } from '../spec-configuration/containerFeaturesConfiguration'; import { readLocalFile } from '../spec-utils/pfs'; import { includeAllConfiguredFeatures } from '../spec-utils/product'; -import { createFeaturesTempFolder, DockerResolverParameters, getCacheFolder, getFolderImageName, getEmptyContextFolder, SubstitutedConfig, retry } from './utils'; +import { createFeaturesTempFolder, DockerResolverParameters, getCacheFolder, getFolderImageName, getEmptyContextFolder, SubstitutedConfig, ensureDockerfileFrontendAccessible } from './utils'; import { isEarlierVersion, parseVersion, runCommandNoPty } from '../spec-common/commonUtils'; import { getDevcontainerMetadata, getDevcontainerMetadataLabel, getImageBuildInfoFromImage, ImageBuildInfo, ImageMetadataEntry, imageMetadataLabel, MergedDevContainerConfig } from './imageMetadata'; import { supportsBuildContexts } from './dockerfileUtils'; import { ContainerError } from '../spec-common/errors'; -import { requestResolveHeaders } from '../spec-utils/httpRequest'; - -// Constants for DockerHub registry + Dockerfile v1.4 image access check -const DOCKERHUB_AUTH_URL = 'https://auth.docker.io/token?service=registry.docker.io&scope=repository:docker/dockerfile:pull&tag=1.4'; -const DOCKERHUB_REGISTRY_URL = 'https://registry-1.docker.io/v2/docker/dockerfile/manifests/1.4'; -const DEVCONTAINER_USER_AGENT = 'devcontainer'; -const DOCKER_MANIFEST_ACCEPT_HEADER = 'application/vnd.docker.distribution.manifest.v2+json'; -const DOCKERFILE_FRONTEND_CHECK_MAX_RETRIES = 5; -const DOCKERFILE_FRONTEND_CHECK_RETRY_INTERVAL_MS = 2000; // Escapes environment variable keys. // @@ -204,7 +195,7 @@ export interface ImageBuildOptions { async function getImageBuildOptions(params: DockerResolverParameters, config: SubstitutedConfig, dstFolder: string, baseName: string, imageBuildInfo: ImageBuildInfo): Promise { const syntax = imageBuildInfo.dockerfile?.preamble.directives.syntax; - const dockerHubAccessible = syntax ? await ensureDockerfileFrontendAccessible(params) : false; + const dockerHubAccessible = syntax ? await ensureDockerfileFrontendAccessible(params, 'docker/dockerfile', '1.4') : false; return { dstFolder, dockerfileContent: ` @@ -231,62 +222,6 @@ function getOmitDevcontainerPropertyOverride(resolverParams: { omitConfigRemotEn return []; } -async function checkDockerfileFrontendAccessible(params: DockerResolverParameters): Promise { - const { output } = params.common; - - const tokenRes = await requestResolveHeaders({ - type: 'GET', - url: DOCKERHUB_AUTH_URL, - headers: { 'user-agent': DEVCONTAINER_USER_AGENT } - }, output); - if (!tokenRes || tokenRes.statusCode !== 200) { - throw new Error('Token fetch failed: status ' + (tokenRes?.statusCode ?? 'unknown')); - } - - let body: any; - try { - body = JSON.parse(tokenRes.resBody.toString()); - } catch (e) { - throw new Error('Token parse failed: ' + (e instanceof Error ? e.message : String(e))); - } - const token: string | undefined = body?.token || body?.access_token; - if (!token) { - throw new Error('Token missing in auth response'); - } - - const manifestRes = await requestResolveHeaders({ - type: 'GET', - url: DOCKERHUB_REGISTRY_URL, - headers: { - 'user-agent': DEVCONTAINER_USER_AGENT, - 'authorization': `Bearer ${token}`, - 'accept': DOCKER_MANIFEST_ACCEPT_HEADER - } - }, output); - if (!manifestRes || manifestRes.statusCode !== 200) { - throw new Error('Manifest fetch failed: status ' + (manifestRes?.statusCode ?? 'unknown')); - } -} - -async function ensureDockerfileFrontendAccessible(params: DockerResolverParameters): Promise { - const { output } = params.common; - try { - await retry( - async () => { await checkDockerfileFrontendAccessible(params); }, - { maxRetries: DOCKERFILE_FRONTEND_CHECK_MAX_RETRIES, retryIntervalMilliseconds: DOCKERFILE_FRONTEND_CHECK_RETRY_INTERVAL_MS, output } - ); - output.write('Dockerfile frontend is accessible in DockerHub registry.', LogLevel.Info); - return true; - } catch (err) { - output.write( - 'Dockerfile frontend check failed after retries: ' + - (err instanceof Error ? err.message : String(err)), - LogLevel.Warning - ); - return false; - } -} - async function getFeaturesBuildOptions(params: DockerResolverParameters, devContainerConfig: SubstitutedConfig, featuresConfig: FeaturesConfig, baseName: string, imageBuildInfo: ImageBuildInfo, composeServiceUser: string | undefined): Promise { const { common } = params; const { cliHost, output } = common; @@ -330,7 +265,7 @@ async function getFeaturesBuildOptions(params: DockerResolverParameters, devCont ; const syntax = imageBuildInfo.dockerfile?.preamble.directives.syntax; const omitSyntaxDirective = common.omitSyntaxDirective; // Can be removed when https://github.com/moby/buildkit/issues/4556 is fixed - const dockerHubAccessible = !omitSyntaxDirective ? await ensureDockerfileFrontendAccessible(params) : false; + const dockerHubAccessible = !omitSyntaxDirective ? await ensureDockerfileFrontendAccessible(params, 'docker/dockerfile', '1.4') : false; const dockerfilePrefixContent = `${omitSyntaxDirective ? '' : useBuildKitBuildContexts && dockerHubAccessible && !(imageBuildInfo.dockerfile && supportsBuildContexts(imageBuildInfo.dockerfile)) ? '# syntax=docker/dockerfile:1.4' : syntax ? `# syntax=${syntax}` : ''} diff --git a/src/spec-node/utils.ts b/src/spec-node/utils.ts index 49e17e96e..018cf63b7 100644 --- a/src/spec-node/utils.ts +++ b/src/spec-node/utils.ts @@ -28,7 +28,7 @@ import { ImageMetadataEntry, MergedDevContainerConfig } from './imageMetadata'; import { getImageIndexEntryForPlatform, getManifest, getRef } from '../spec-configuration/containerCollectionsOCI'; import { requestEnsureAuthenticated } from '../spec-configuration/httpOCIRegistry'; import { configFileLabel, findDevContainer, hostFolderLabel } from './singleContainer'; - +import { requestResolveHeaders } from '../spec-utils/httpRequest'; export { getConfigFilePath, getDockerfilePath, isDockerFileConfig } from '../spec-configuration/configuration'; export { uriToFsPath, parentURI } from '../spec-configuration/configurationCommonUtils'; @@ -37,6 +37,12 @@ export type BindMountConsistency = 'consistent' | 'cached' | 'delegated' | undef export type GPUAvailability = 'all' | 'detect' | 'none'; +// Constants for DockerHub registry + image access check +const DEVCONTAINER_USER_AGENT = 'devcontainer'; +const DOCKER_MANIFEST_ACCEPT_HEADER = 'application/vnd.docker.distribution.manifest.v2+json'; +const DOCKERFILE_FRONTEND_CHECK_MAX_RETRIES = 5; +const DOCKERFILE_FRONTEND_CHECK_RETRY_INTERVAL_MS = 2000; + // Generic retry function export async function retry(fn: () => Promise, options: { retryIntervalMilliseconds: number; maxRetries: number; output: Log }): Promise { const { retryIntervalMilliseconds, maxRetries, output } = options; @@ -603,3 +609,71 @@ export function runAsyncHandler(handler: () => Promise) { } })(); } + +// Helper functions to construct DockerHub URLs +function getDockerHubAuthUrl(imageName: string, version: string): string { + return `https://auth.docker.io/token?service=registry.docker.io&scope=repository:${imageName}:pull&tag=${version}`; +} + +function getDockerHubRegistryUrl(imageName: string, version: string): string { + return `https://registry-1.docker.io/v2/${imageName}/manifests/${version}`; +} + +async function checkDockerfileFrontendAccessible(params: DockerResolverParameters, imageName: string, version: string): Promise { + const { output } = params.common; + + const authUrl = getDockerHubAuthUrl(imageName, version); + const registryUrl = getDockerHubRegistryUrl(imageName, version); + + const tokenRes = await requestResolveHeaders({ + type: 'GET', + url: authUrl, + headers: { 'user-agent': DEVCONTAINER_USER_AGENT } + }, output); + if (!tokenRes || tokenRes.statusCode !== 200) { + throw new Error('Token fetch failed: status ' + (tokenRes?.statusCode ?? 'unknown')); + } + + let body: any; + try { + body = JSON.parse(tokenRes.resBody.toString()); + } catch (e) { + throw new Error('Token parse failed: ' + (e instanceof Error ? e.message : String(e))); + } + const token: string | undefined = body?.token || body?.access_token; + if (!token) { + throw new Error('Token missing in auth response'); + } + + const manifestRes = await requestResolveHeaders({ + type: 'GET', + url: registryUrl, + headers: { + 'user-agent': DEVCONTAINER_USER_AGENT, + 'authorization': `Bearer ${token}`, + 'accept': DOCKER_MANIFEST_ACCEPT_HEADER + } + }, output); + if (!manifestRes || manifestRes.statusCode !== 200) { + throw new Error('Manifest fetch failed: status ' + (manifestRes?.statusCode ?? 'unknown')); + } +} + +export async function ensureDockerfileFrontendAccessible(params: DockerResolverParameters, imageName: string, version: string): Promise { + const { output } = params.common; + try { + await retry( + async () => { await checkDockerfileFrontendAccessible(params, imageName, version); }, + { maxRetries: DOCKERFILE_FRONTEND_CHECK_MAX_RETRIES, retryIntervalMilliseconds: DOCKERFILE_FRONTEND_CHECK_RETRY_INTERVAL_MS, output } + ); + output.write('Dockerfile frontend is accessible in DockerHub registry.', LogLevel.Info); + return true; + } catch (err) { + output.write( + 'Dockerfile frontend check failed after retries: ' + + (err instanceof Error ? err.message : String(err)), + LogLevel.Warning + ); + return false; + } +} From 30f0b341b47eede2e0e58966c404969bfca7f61e Mon Sep 17 00:00:00 2001 From: Kaniska Date: Mon, 1 Dec 2025 16:40:52 +0000 Subject: [PATCH 5/5] Make function names generic --- src/spec-node/containerFeatures.ts | 6 +++--- src/spec-node/utils.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/spec-node/containerFeatures.ts b/src/spec-node/containerFeatures.ts index 124e2415f..f8912254a 100644 --- a/src/spec-node/containerFeatures.ts +++ b/src/spec-node/containerFeatures.ts @@ -11,7 +11,7 @@ import { LogLevel, makeLog } from '../spec-utils/log'; import { FeaturesConfig, getContainerFeaturesBaseDockerFile, getFeatureInstallWrapperScript, getFeatureLayers, getFeatureMainValue, getFeatureValueObject, generateFeaturesConfig, Feature, generateContainerEnvs } from '../spec-configuration/containerFeaturesConfiguration'; import { readLocalFile } from '../spec-utils/pfs'; import { includeAllConfiguredFeatures } from '../spec-utils/product'; -import { createFeaturesTempFolder, DockerResolverParameters, getCacheFolder, getFolderImageName, getEmptyContextFolder, SubstitutedConfig, ensureDockerfileFrontendAccessible } from './utils'; +import { createFeaturesTempFolder, DockerResolverParameters, getCacheFolder, getFolderImageName, getEmptyContextFolder, SubstitutedConfig, ensureDockerHubImageAccessible } from './utils'; import { isEarlierVersion, parseVersion, runCommandNoPty } from '../spec-common/commonUtils'; import { getDevcontainerMetadata, getDevcontainerMetadataLabel, getImageBuildInfoFromImage, ImageBuildInfo, ImageMetadataEntry, imageMetadataLabel, MergedDevContainerConfig } from './imageMetadata'; import { supportsBuildContexts } from './dockerfileUtils'; @@ -195,7 +195,7 @@ export interface ImageBuildOptions { async function getImageBuildOptions(params: DockerResolverParameters, config: SubstitutedConfig, dstFolder: string, baseName: string, imageBuildInfo: ImageBuildInfo): Promise { const syntax = imageBuildInfo.dockerfile?.preamble.directives.syntax; - const dockerHubAccessible = syntax ? await ensureDockerfileFrontendAccessible(params, 'docker/dockerfile', '1.4') : false; + const dockerHubAccessible = syntax ? await ensureDockerHubImageAccessible(params, 'docker/dockerfile', '1.4') : false; return { dstFolder, dockerfileContent: ` @@ -265,7 +265,7 @@ async function getFeaturesBuildOptions(params: DockerResolverParameters, devCont ; const syntax = imageBuildInfo.dockerfile?.preamble.directives.syntax; const omitSyntaxDirective = common.omitSyntaxDirective; // Can be removed when https://github.com/moby/buildkit/issues/4556 is fixed - const dockerHubAccessible = !omitSyntaxDirective ? await ensureDockerfileFrontendAccessible(params, 'docker/dockerfile', '1.4') : false; + const dockerHubAccessible = !omitSyntaxDirective ? await ensureDockerHubImageAccessible(params, 'docker/dockerfile', '1.4') : false; const dockerfilePrefixContent = `${omitSyntaxDirective ? '' : useBuildKitBuildContexts && dockerHubAccessible && !(imageBuildInfo.dockerfile && supportsBuildContexts(imageBuildInfo.dockerfile)) ? '# syntax=docker/dockerfile:1.4' : syntax ? `# syntax=${syntax}` : ''} diff --git a/src/spec-node/utils.ts b/src/spec-node/utils.ts index 018cf63b7..0dbed5171 100644 --- a/src/spec-node/utils.ts +++ b/src/spec-node/utils.ts @@ -619,7 +619,7 @@ function getDockerHubRegistryUrl(imageName: string, version: string): string { return `https://registry-1.docker.io/v2/${imageName}/manifests/${version}`; } -async function checkDockerfileFrontendAccessible(params: DockerResolverParameters, imageName: string, version: string): Promise { +async function checkDockerHubImageAccessible(params: DockerResolverParameters, imageName: string, version: string): Promise { const { output } = params.common; const authUrl = getDockerHubAuthUrl(imageName, version); @@ -659,11 +659,11 @@ async function checkDockerfileFrontendAccessible(params: DockerResolverParameter } } -export async function ensureDockerfileFrontendAccessible(params: DockerResolverParameters, imageName: string, version: string): Promise { +export async function ensureDockerHubImageAccessible(params: DockerResolverParameters, imageName: string, version: string): Promise { const { output } = params.common; try { await retry( - async () => { await checkDockerfileFrontendAccessible(params, imageName, version); }, + async () => { await checkDockerHubImageAccessible(params, imageName, version); }, { maxRetries: DOCKERFILE_FRONTEND_CHECK_MAX_RETRIES, retryIntervalMilliseconds: DOCKERFILE_FRONTEND_CHECK_RETRY_INTERVAL_MS, output } ); output.write('Dockerfile frontend is accessible in DockerHub registry.', LogLevel.Info);