Skip to content

Commit 7716a88

Browse files
committed
Adding precautionary check for dockerhub registry availability in devcontainer cli
1 parent f61c25f commit 7716a88

File tree

2 files changed

+85
-22
lines changed

2 files changed

+85
-22
lines changed

src/spec-node/containerFeatures.ts

Lines changed: 80 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@ 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, retry } 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';
1818
import { ContainerError } from '../spec-common/errors';
19+
import { requestResolveHeaders } from '../spec-utils/httpRequest';
1920

2021
// Escapes environment variable keys.
2122
//
@@ -154,7 +155,7 @@ export async function getExtendImageBuildInfo(params: DockerResolverParameters,
154155
}
155156
};
156157
}
157-
return { featureBuildInfo: getImageBuildOptions(params, config, dstFolder, baseName, imageBuildInfo) };
158+
return { featureBuildInfo: await getImageBuildOptions(params, config, dstFolder, baseName, imageBuildInfo) };
158159
}
159160

160161
// Generates the end configuration.
@@ -193,24 +194,25 @@ export interface ImageBuildOptions {
193194
securityOpts: string[];
194195
}
195196

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: `
197+
async function getImageBuildOptions(params: DockerResolverParameters, config: SubstitutedConfig<DevContainerConfig>, dstFolder: string, baseName: string, imageBuildInfo: ImageBuildInfo): Promise<ImageBuildOptions> {
198+
const syntax = imageBuildInfo.dockerfile?.preamble.directives.syntax;
199+
const dockerHubAccessible = syntax ? await ensureDockerfileFrontendAccessible(params) : false;
200+
return {
201+
dstFolder,
202+
dockerfileContent: `
201203
FROM $_DEV_CONTAINERS_BASE_IMAGE AS dev_containers_target_stage
202204
${getDevcontainerMetadataLabel(getDevcontainerMetadata(imageBuildInfo.metadata, config, { featureSets: [] }, [], getOmitDevcontainerPropertyOverride(params.common)))}
203205
`,
204-
overrideTarget: 'dev_containers_target_stage',
205-
dockerfilePrefixContent: `${syntax ? `# syntax=${syntax}` : ''}
206-
ARG _DEV_CONTAINERS_BASE_IMAGE=placeholder
206+
overrideTarget: 'dev_containers_target_stage',
207+
dockerfilePrefixContent: `${dockerHubAccessible && syntax ? `# syntax=${syntax}` : ''}
208+
ARG _DEV_CONTAINERS_BASE_IMAGE=placeholder
207209
`,
208-
buildArgs: {
209-
_DEV_CONTAINERS_BASE_IMAGE: baseName,
210-
} as Record<string, string>,
211-
buildKitContexts: {} as Record<string, string>,
212-
securityOpts: [],
213-
};
210+
buildArgs: {
211+
_DEV_CONTAINERS_BASE_IMAGE: baseName,
212+
} as Record<string, string>,
213+
buildKitContexts: {} as Record<string, string>,
214+
securityOpts: [],
215+
};
214216
}
215217

216218
function getOmitDevcontainerPropertyOverride(resolverParams: { omitConfigRemotEnvFromMetadata?: boolean }): (keyof DevContainerConfig & keyof ImageMetadataEntry)[] {
@@ -221,6 +223,62 @@ function getOmitDevcontainerPropertyOverride(resolverParams: { omitConfigRemotEn
221223
return [];
222224
}
223225

226+
async function checkDockerfileFrontendAccessibleOrThrow(params: DockerResolverParameters): Promise<void> {
227+
const { output } = params.common;
228+
229+
const tokenRes = await requestResolveHeaders({
230+
type: 'GET',
231+
url: 'https://auth.docker.io/token?service=registry.docker.io&scope=repository:docker/dockerfile:pull&tag=1.4',
232+
headers: { 'user-agent': 'devcontainer' }
233+
}, output);
234+
if (!tokenRes || tokenRes.statusCode !== 200) {
235+
throw new Error('Token fetch failed: status ' + (tokenRes?.statusCode ?? 'unknown'));
236+
}
237+
238+
let body: any;
239+
try {
240+
body = JSON.parse(tokenRes.resBody.toString());
241+
} catch (e) {
242+
throw new Error('Token parse failed: ' + (e instanceof Error ? e.message : String(e)));
243+
}
244+
const token: string | undefined = body?.token || body?.access_token;
245+
if (!token) {
246+
throw new Error('Token missing in auth response');
247+
}
248+
249+
const manifestRes = await requestResolveHeaders({
250+
type: 'GET',
251+
url: 'https://registry-1.docker.io/v2/docker/dockerfile/manifests/1.4',
252+
headers: {
253+
'user-agent': 'devcontainer',
254+
'authorization': `Bearer ${token}`,
255+
'accept': 'application/vnd.docker.distribution.manifest.v2+json'
256+
}
257+
}, output);
258+
if (!manifestRes || manifestRes.statusCode !== 200) {
259+
throw new Error('Manifest fetch failed: status ' + (manifestRes?.statusCode ?? 'unknown'));
260+
}
261+
}
262+
263+
async function ensureDockerfileFrontendAccessible(params: DockerResolverParameters): Promise<boolean> {
264+
const { output } = params.common;
265+
try {
266+
await retry(
267+
async () => { await checkDockerfileFrontendAccessibleOrThrow(params); },
268+
{ maxRetries: 5, retryIntervalMilliseconds: 2000, output }
269+
);
270+
output.write('Dockerfile frontend is accessible in DockerHub registry.', LogLevel.Info);
271+
return true;
272+
} catch (err) {
273+
output.write(
274+
'Dockerfile frontend check failed after retries: ' +
275+
(err instanceof Error ? err.message : String(err)),
276+
LogLevel.Warning
277+
);
278+
return false;
279+
}
280+
}
281+
224282
async function getFeaturesBuildOptions(params: DockerResolverParameters, devContainerConfig: SubstitutedConfig<DevContainerConfig>, featuresConfig: FeaturesConfig, baseName: string, imageBuildInfo: ImageBuildInfo, composeServiceUser: string | undefined): Promise<ImageBuildOptions | undefined> {
225283
const { common } = params;
226284
const { cliHost, output } = common;
@@ -262,11 +320,12 @@ async function getFeaturesBuildOptions(params: DockerResolverParameters, devCont
262320
.replace('#{devcontainerMetadata}', getDevcontainerMetadataLabel(imageMetadata))
263321
.replace('#{containerEnvMetadata}', generateContainerEnvs(devContainerConfig.config.containerEnv, true))
264322
;
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}` : ''}
323+
const syntax = imageBuildInfo.dockerfile?.preamble.directives.syntax;
324+
const omitSyntaxDirective = common.omitSyntaxDirective; // Can be removed when https://github.com/moby/buildkit/issues/4556 is fixed
325+
const dockerHubAccessible = !omitSyntaxDirective ? await ensureDockerfileFrontendAccessible(params) : false;
326+
const dockerfilePrefixContent = `${omitSyntaxDirective ? '' :
327+
useBuildKitBuildContexts && dockerHubAccessible && !(imageBuildInfo.dockerfile && supportsBuildContexts(imageBuildInfo.dockerfile)) ? '# syntax=docker/dockerfile:1.4' :
328+
syntax ? `# syntax=${syntax}` : ''}
270329
ARG _DEV_CONTAINERS_BASE_IMAGE=placeholder
271330
`;
272331

src/spec-node/utils.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,11 @@ export async function retry<T>(fn: () => Promise<T>, options: { retryIntervalMil
4646
return await fn();
4747
} catch (err) {
4848
lastError = err;
49-
output.write(`Retrying (Attempt ${i}) with error '${toErrorText(err)}'`, LogLevel.Warning);
49+
output.write(
50+
`Retrying (Attempt ${i}) with error
51+
'${toErrorText(String(err && (err.stack || err.message) || err))}'`,
52+
LogLevel.Warning
53+
);
5054
await new Promise(resolve => setTimeout(resolve, retryIntervalMilliseconds));
5155
}
5256
}

0 commit comments

Comments
 (0)