Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 88 additions & 21 deletions src/spec-node/containerFeatures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,20 @@ 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';

// 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.
//
Expand Down Expand Up @@ -154,7 +163,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.
Expand Down Expand Up @@ -193,24 +202,25 @@ export interface ImageBuildOptions {
securityOpts: string[];
}

function getImageBuildOptions(params: DockerResolverParameters, config: SubstitutedConfig<DevContainerConfig>, dstFolder: string, baseName: string, imageBuildInfo: ImageBuildInfo): ImageBuildOptions {
const syntax = imageBuildInfo.dockerfile?.preamble.directives.syntax;
return {
dstFolder,
dockerfileContent: `
async function getImageBuildOptions(params: DockerResolverParameters, config: SubstitutedConfig<DevContainerConfig>, dstFolder: string, baseName: string, imageBuildInfo: ImageBuildInfo): Promise<ImageBuildOptions> {
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<string, string>,
buildKitContexts: {} as Record<string, string>,
securityOpts: [],
};
buildArgs: {
_DEV_CONTAINERS_BASE_IMAGE: baseName,
} as Record<string, string>,
buildKitContexts: {} as Record<string, string>,
securityOpts: [],
};
}

function getOmitDevcontainerPropertyOverride(resolverParams: { omitConfigRemotEnvFromMetadata?: boolean }): (keyof DevContainerConfig & keyof ImageMetadataEntry)[] {
Expand All @@ -221,6 +231,62 @@ function getOmitDevcontainerPropertyOverride(resolverParams: { omitConfigRemotEn
return [];
}

async function checkDockerfileFrontendAccessible(params: DockerResolverParameters): Promise<void> {
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<boolean> {
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<DevContainerConfig>, featuresConfig: FeaturesConfig, baseName: string, imageBuildInfo: ImageBuildInfo, composeServiceUser: string | undefined): Promise<ImageBuildOptions | undefined> {
const { common } = params;
const { cliHost, output } = common;
Expand Down Expand Up @@ -262,11 +328,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
`;

Expand Down
6 changes: 5 additions & 1 deletion src/spec-node/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,11 @@ export async function retry<T>(fn: () => Promise<T>, 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));
}
}
Expand Down