Skip to content

Commit

Permalink
Merge branch 'main' into joshspicer/enforce-Feature-dir-name-match
Browse files Browse the repository at this point in the history
  • Loading branch information
joshspicer authored Aug 1, 2024
2 parents 49f8178 + 6c6aebf commit 521cf3b
Show file tree
Hide file tree
Showing 40 changed files with 516 additions and 109 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ output
src/test/container-features/configs/temp_lifecycle-hooks-alternative-order
test-secrets-temp.json
src/test/container-*/**/src/**/README.md
!src/test/container-features/assets/*.tgz
48 changes: 48 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,54 @@

Notable changes.

## August 2024

### [0.67.0]
- Fix containerEnv substitution. (https://github.com/microsoft/vscode-remote-release/issues/10033)

## July 2024

### [0.66.0]
- Wait for result to be written to stdout. (https://github.com/microsoft/vscode-remote-release/issues/10029)

## June 2024

### [0.65.0]
- Fix confusing error message with local feature. (https://github.com/devcontainers/cli/issues/834)
- Add `--label` parameter to `devcontainer build` command. (https://github.com/devcontainers/cli/issues/837)
- Prefer Docker Compose v2 over v1. (https://github.com/devcontainers/cli/issues/826)

### [0.64.0]
- Fix project name with env variable. (https://github.com/devcontainers/cli/issues/839)

### [0.63.0]
- Surface additional information in `devcontainer up`. (https://github.com/devcontainers/cli/pull/836)
- Changes the config layer of the Feature manifest to a empty descriptor (https://github.com/devcontainers/cli/pull/815)

## May 2024

### [0.62.0]
- Fix support for project name attribute. (https://github.com/devcontainers/cli/issues/831)

### [0.61.0]
- Use --depth 1 to make dotfiles install process faster (https://github.com/devcontainers/cli/pull/830)
- Enable --cache-to and --cache-from in devcontainer up (https://github.com/devcontainers/cli/pull/813)
- Omit generated image name when `--image-name` is given (https://github.com/devcontainers/cli/pull/812)

### [0.60.0]
- Support project name attribute. (https://github.com/microsoft/vscode-remote-release/issues/512)

## April 2024

### [0.59.1]
- Check if image name has registry host. (https://github.com/microsoft/vscode-remote-release/issues/9748)

### [0.59.0]
- Propagate --cache-from to buildx build. (https://github.com/devcontainers/cli/pull/638)
- Disable cache on feature build when `--build-no-cache` is passed. (https://github.com/devcontainers/cli/pull/790)
- Qualify local image for Podman. (https://github.com/microsoft/vscode-remote-release/issues/9748)
- Stop races docker-compose.devcontainer.containerFeatures file. (https://github.com/devcontainers/cli/issues/801)

## March 2024

### [0.58.0]
Expand Down
11 changes: 6 additions & 5 deletions ThirdPartyNotices.txt
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,7 @@ IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

---------------------------------------------------------

tar 6.2.0 - ISC
tar 6.2.1 - ISC
https://github.com/isaacs/node-tar#readme

Copyright (c) Isaac Z. Schlueter and Contributors
Expand Down Expand Up @@ -2405,15 +2405,16 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

---------------------------------------------------------

word-wrap 1.2.3 - MIT
word-wrap 1.2.5 - MIT
https://github.com/jonschlinkert/word-wrap

Copyright (c) 2014-2017, Jon Schlinkert
Copyright (c) 2017, Jon Schlinkert (https://github.com/jonschlinkert)
Copyright (c) 2014-2016, Jon Schlinkert
Copyright (c) 2014-2023, Jon Schlinkert
Copyright (c) 2023, Jon Schlinkert (https://github.com/jonschlinkert)

The MIT License (MIT)

Copyright (c) 2014-2017, Jon Schlinkert
Copyright (c) 2014-2016, Jon Schlinkert

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@devcontainers/cli",
"description": "Dev Containers CLI",
"version": "0.58.0",
"version": "0.67.0",
"bin": {
"devcontainer": "devcontainer.js"
},
Expand Down
4 changes: 2 additions & 2 deletions src/spec-common/dotfiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export async function installDotfiles(params: ResolverParameters, properties: Co
await shellServer.exec(`# Clone & install dotfiles via '${installCommand}'
${createFileCommand(markerFile)} || (echo dotfiles marker found && exit 1) || exit 0
command -v git >/dev/null 2>&1 || (echo git not found && exit 1) || exit 0
[ -e ${targetPath} ] || ${allEnv}git clone ${repository} ${targetPath} || exit $?
[ -e ${targetPath} ] || ${allEnv}git clone --depth 1 ${repository} ${targetPath} || exit $?
echo Setting current directory to '${targetPath}'
cd ${targetPath}
Expand Down Expand Up @@ -74,7 +74,7 @@ fi
await shellServer.exec(`# Clone & install dotfiles
${createFileCommand(markerFile)} || (echo dotfiles marker found && exit 1) || exit 0
command -v git >/dev/null 2>&1 || (echo git not found && exit 1) || exit 0
[ -e ${targetPath} ] || ${allEnv}git clone ${repository} ${targetPath} || exit $?
[ -e ${targetPath} ] || ${allEnv}git clone --depth 1 ${repository} ${targetPath} || exit $?
echo Setting current directory to ${targetPath}
cd ${targetPath}
for f in ${installCommands.join(' ')}
Expand Down
2 changes: 2 additions & 0 deletions src/spec-common/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ interface ContainerErrorData {
start?: boolean;
attach?: boolean;
fileWithError?: string;
disallowedFeatureId?: string;
didStopContainer?: boolean;
learnMoreUrl?: string;
}

Expand Down
9 changes: 6 additions & 3 deletions src/spec-common/injectHeadless.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,18 +323,21 @@ export function getSystemVarFolder(params: ResolverParameters): string {
return params.containerSystemDataFolder || '/var/devcontainer';
}

export async function setupInContainer(params: ResolverParameters, containerProperties: ContainerProperties, config: CommonMergedDevContainerConfig, lifecycleCommandOriginMap: LifecycleHooksInstallMap) {
export async function setupInContainer(params: ResolverParameters, containerProperties: ContainerProperties, config: CommonDevContainerConfig, mergedConfig: CommonMergedDevContainerConfig, lifecycleCommandOriginMap: LifecycleHooksInstallMap) {
await patchEtcEnvironment(params, containerProperties);
await patchEtcProfile(params, containerProperties);
const computeRemoteEnv = params.computeExtensionHostEnv || params.lifecycleHook.enabled;
const updatedConfig = containerSubstitute(params.cliHost.platform, config.configFilePath, containerProperties.env, config);
const remoteEnv = computeRemoteEnv ? probeRemoteEnv(params, containerProperties, updatedConfig) : Promise.resolve({});
const updatedMergedConfig = containerSubstitute(params.cliHost.platform, mergedConfig.configFilePath, containerProperties.env, mergedConfig);
const remoteEnv = computeRemoteEnv ? probeRemoteEnv(params, containerProperties, updatedMergedConfig) : Promise.resolve({});
const secretsP = params.secretsP || Promise.resolve({});
if (params.lifecycleHook.enabled) {
await runLifecycleHooks(params, lifecycleCommandOriginMap, containerProperties, updatedConfig, remoteEnv, secretsP, false);
await runLifecycleHooks(params, lifecycleCommandOriginMap, containerProperties, updatedMergedConfig, remoteEnv, secretsP, false);
}
return {
remoteEnv: params.computeExtensionHostEnv ? await remoteEnv : {},
updatedConfig,
updatedMergedConfig,
};
}

Expand Down
19 changes: 9 additions & 10 deletions src/spec-configuration/containerCollectionsOCIPush.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { requestEnsureAuthenticated } from './httpOCIRegistry';
// Devcontainer Spec (features) : https://containers.dev/implementors/features-distribution/#oci-registry
// Devcontainer Spec (templates): https://github.com/devcontainers/spec/blob/main/proposals/devcontainer-templates-distribution.md#oci-registry
// OCI Spec : https://github.com/opencontainers/distribution-spec/blob/main/spec.md#push
export async function pushOCIFeatureOrTemplate(params: CommonParams, ociRef: OCIRef, pathToTgz: string, tags: string[], collectionType: string, featureAnnotations = {}): Promise<string | undefined> {
export async function pushOCIFeatureOrTemplate(params: CommonParams, ociRef: OCIRef, pathToTgz: string, tags: string[], collectionType: string, annotations: { [key: string]: string } = {}): Promise<string | undefined> {
const { output } = params;

output.write(`-- Starting push of ${collectionType} '${ociRef.id}' to '${ociRef.resource}' with tags '${tags.join(', ')}'`);
Expand All @@ -25,7 +25,7 @@ export async function pushOCIFeatureOrTemplate(params: CommonParams, ociRef: OCI
const dataBytes = fs.readFileSync(pathToTgz);

// Generate Manifest for given feature/template artifact.
const manifest = await generateCompleteManifestForIndividualFeatureOrTemplate(output, dataBytes, pathToTgz, ociRef, collectionType, featureAnnotations);
const manifest = await generateCompleteManifestForIndividualFeatureOrTemplate(output, dataBytes, pathToTgz, ociRef, collectionType, annotations);
if (!manifest) {
output.write(`Failed to generate manifest for ${ociRef.id}`, LogLevel.Error);
return;
Expand All @@ -44,8 +44,8 @@ export async function pushOCIFeatureOrTemplate(params: CommonParams, ociRef: OCI
{
name: 'configLayer',
digest: manifest.manifestObj.config.digest,
contents: Buffer.alloc(0),
size: manifest.manifestObj.config.size,
contents: Buffer.from('{}'),
},
{
name: 'tgzLayer',
Expand Down Expand Up @@ -119,7 +119,7 @@ export async function pushCollectionMetadata(params: CommonParams, collectionRef
name: 'configLayer',
digest: manifest.manifestObj.config.digest,
size: manifest.manifestObj.config.size,
contents: Buffer.alloc(0),
contents: Buffer.from('{}'),
},
{
name: 'collectionLayer',
Expand Down Expand Up @@ -268,14 +268,13 @@ async function putBlob(params: CommonParams, blobPutLocationUriPath: string, oci
// Generate a layer that follows the `application/vnd.devcontainers.layer.v1+tar` mediaType as defined in
// Devcontainer Spec (features) : https://containers.dev/implementors/features-distribution/#oci-registry
// Devcontainer Spec (templates): https://github.com/devcontainers/spec/blob/main/proposals/devcontainer-templates-distribution.md#oci-registry
async function generateCompleteManifestForIndividualFeatureOrTemplate(output: Log, dataBytes: Buffer, pathToTgz: string, ociRef: OCIRef, collectionType: string, featureAnnotations = {}): Promise<ManifestContainer | undefined> {
async function generateCompleteManifestForIndividualFeatureOrTemplate(output: Log, dataBytes: Buffer, pathToTgz: string, ociRef: OCIRef, collectionType: string, annotations: { [key: string]: string } = {}): Promise<ManifestContainer | undefined> {
const tgzLayer = await calculateDataLayer(output, dataBytes, path.basename(pathToTgz), DEVCONTAINER_TAR_LAYER_MEDIATYPE);
if (!tgzLayer) {
output.write(`Failed to calculate tgz layer.`, LogLevel.Error);
return undefined;
}

let annotations: { [key: string]: string } = featureAnnotations;
// Specific registries look for certain optional metadata
// in the manifest, in this case for UI presentation.
if (ociRef.registry === 'ghcr.io') {
Expand Down Expand Up @@ -382,16 +381,16 @@ async function postUploadSessionId(params: CommonParams, ociRef: OCIRef | OCICol
export async function calculateManifestAndContentDigest(output: Log, ociRef: OCIRef | OCICollectionRef, dataLayer: OCILayer, annotations: { [key: string]: string } | undefined): Promise<ManifestContainer> {
// A canonical manifest digest is the sha256 hash of the JSON representation of the manifest, without the signature content.
// See: https://docs.docker.com/registry/spec/api/#content-digests
// Below is an example of a serialized manifest that should resolve to '9726054859c13377c4c3c3c73d15065de59d0c25d61d5652576c0125f2ea8ed3'
// {"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.devcontainers","digest":"sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","size":0},"layers":[{"mediaType":"application/vnd.devcontainers.layer.v1+tar","digest":"sha256:b2006e7647191f7b47222ae48df049c6e21a4c5a04acfad0c4ef614d819de4c5","size":15872,"annotations":{"org.opencontainers.image.title":"go.tgz"}}]}
// Below is an example of a serialized manifest that should resolve to 'dd328c25cc7382aaf4e9ee10104425d9a2561b47fe238407f6c0f77b3f8409fc'
// {"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.devcontainers","digest":"sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","size":2},"layers":[{"mediaType":"application/vnd.devcontainers.layer.v1+tar","digest":"sha256:0bb92d2da46d760c599d0a41ed88d52521209408b529761417090b62ee16dfd1","size":3584,"annotations":{"org.opencontainers.image.title":"devcontainer-feature-color.tgz"}}],"annotations":{"dev.containers.metadata":"{\"id\":\"color\",\"version\":\"1.0.0\",\"name\":\"A feature to remind you of your favorite color\",\"options\":{\"favorite\":{\"type\":\"string\",\"enum\":[\"red\",\"gold\",\"green\"],\"default\":\"red\",\"description\":\"Choose your favorite color.\"}}}","com.github.package.type":"devcontainer_feature"}}

let manifest: OCIManifest = {
schemaVersion: 2,
mediaType: 'application/vnd.oci.image.manifest.v1+json',
config: {
mediaType: 'application/vnd.devcontainers',
digest: 'sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', // A zero byte digest for the devcontainer mediaType.
size: 0
digest: 'sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a', // A empty json byte digest for the devcontainer mediaType.
size: 2
},
layers: [
dataLayer
Expand Down
8 changes: 6 additions & 2 deletions src/spec-configuration/containerFeaturesConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -698,8 +698,12 @@ export async function getFeatureIdType(params: CommonParams, userFeatureId: stri

// Legacy feature-set ID
if (!userFeatureId.includes('/') && !userFeatureId.includes('\\')) {
output.write(`Legacy feature '${userFeatureId}' not supported. Please check https://containers.dev/features for replacements.`, LogLevel.Error);
throw new ContainerError({ description: `Legacy feature '${userFeatureId}' not supported. Please check https://containers.dev/features for replacements.` });
const errorMessage = `Legacy feature '${userFeatureId}' not supported. Please check https://containers.dev/features for replacements.
If you were hoping to use local Features, remember to prepend your Feature name with "./". Please check https://containers.dev/implementors/features-distribution/#addendum-locally-referenced for more information.`;
output.write(errorMessage, LogLevel.Error);
throw new ContainerError({
description: errorMessage
});
}

// Direct tarball reference
Expand Down
4 changes: 2 additions & 2 deletions src/spec-node/collectionCommonUtils/publishCommandImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export function getSemanticTags(version: string, tags: string[], output: Log) {
return semanticVersions;
}

export async function doPublishCommand(params: CommonParams, version: string, ociRef: OCIRef, outputDir: string, collectionType: string, archiveName: string, featureAnnotations = {}) {
export async function doPublishCommand(params: CommonParams, version: string, ociRef: OCIRef, outputDir: string, collectionType: string, archiveName: string, annotations: { [key: string]: string } = {}) {
const { output } = params;

output.write(`Fetching published versions...`, LogLevel.Info);
Expand All @@ -54,7 +54,7 @@ export async function doPublishCommand(params: CommonParams, version: string, oc
if (!!semanticTags) {
output.write(`Publishing tags: ${semanticTags.toString()}...`, LogLevel.Info);
const pathToTgz = path.join(outputDir, archiveName);
const digest = await pushOCIFeatureOrTemplate(params, ociRef, pathToTgz, semanticTags, collectionType, featureAnnotations);
const digest = await pushOCIFeatureOrTemplate(params, ociRef, pathToTgz, semanticTags, collectionType, annotations);
if (!digest) {
output.write(`(!) ERR: Failed to publish ${collectionType}: '${ociRef.resource}'`, LogLevel.Error);
return;
Expand Down
23 changes: 19 additions & 4 deletions src/spec-node/containerFeatures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ export async function extendImage(params: DockerResolverParameters, config: Subs
if (params.buildxCacheTo) {
args.push('--cache-to', params.buildxCacheTo);
}
if (!params.buildNoCache) {
params.additionalCacheFroms.forEach(cacheFrom => args.push('--cache-from', cacheFrom));
}

for (const buildContext in featureBuildInfo.buildKitContexts) {
args.push('--build-context', `${buildContext}=${featureBuildInfo.buildKitContexts[buildContext]}`);
Expand All @@ -95,6 +98,9 @@ export async function extendImage(params: DockerResolverParameters, config: Subs
'build',
);
}
if (params.buildNoCache) {
args.push('--no-cache');
}
for (const buildArg in featureBuildInfo.buildArgs) {
args.push('--build-arg', `${buildArg}=${featureBuildInfo.buildArgs[buildArg]}`);
}
Expand All @@ -105,9 +111,9 @@ export async function extendImage(params: DockerResolverParameters, config: Subs
cliHost.mkdirp(emptyTempDir);
args.push(
'--target', featureBuildInfo.overrideTarget,
'-t', updatedImageName,
...additionalImageNames.map(name => ['-t', name]).flat(),
'-f', dockerfilePath,
...additionalImageNames.length > 0 ? additionalImageNames.map(name => ['-t', name]).flat() : ['-t', updatedImageName],
...params.additionalLabels.length > 0 ? params.additionalLabels.map(label => ['--label', label]).flat() : [],
emptyTempDir
);

Expand All @@ -119,7 +125,7 @@ export async function extendImage(params: DockerResolverParameters, config: Subs
await dockerCLI(infoParams, ...args);
}
return {
updatedImageName: [ updatedImageName ],
updatedImageName: additionalImageNames.length > 0 ? additionalImageNames : [updatedImageName],
imageMetadata: getDevcontainerMetadata(imageBuildInfo.metadata, config, featuresConfig),
imageDetails: async () => imageBuildInfo.imageDetails,
};
Expand Down Expand Up @@ -425,7 +431,7 @@ export async function updateRemoteUserUID(params: DockerResolverParameters, merg
'-f', destDockerfile,
'-t', fixedImageName,
...(platform ? ['--platform', platform] : []),
'--build-arg', `BASE_IMAGE=${imageName}`,
'--build-arg', `BASE_IMAGE=${params.isPodman && !hasRegistryHostname(imageName) ? 'localhost/' : ''}${imageName}`, // Podman: https://github.com/microsoft/vscode-remote-release/issues/9748
'--build-arg', `REMOTE_USER=${remoteUser}`,
'--build-arg', `NEW_UID=${await cliHost.getuid!()}`,
'--build-arg', `NEW_GID=${await cliHost.getgid!()}`,
Expand All @@ -439,3 +445,12 @@ export async function updateRemoteUserUID(params: DockerResolverParameters, merg
}
return fixedImageName;
}

function hasRegistryHostname(imageName: string) {
if (imageName.startsWith('localhost/')) {
return true;
}
const dot = imageName.indexOf('.');
const slash = imageName.indexOf('/');
return dot !== -1 && slash !== -1 && dot < slash;
}
Loading

0 comments on commit 521cf3b

Please sign in to comment.