Skip to content

Commit

Permalink
Recreate lockfile with upgrade command (#645)
Browse files Browse the repository at this point in the history
* add --regenerate flag to recreate lockfile

* move to its own upgrade command

* linting

* promote to top-level command
  • Loading branch information
joshspicer authored Sep 29, 2023
1 parent efb3ffc commit 722548e
Show file tree
Hide file tree
Showing 8 changed files with 208 additions and 12 deletions.
19 changes: 7 additions & 12 deletions src/spec-node/devContainersSpecCLI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,26 @@ import textTable from 'text-table';
import * as jsonc from 'jsonc-parser';

import { createDockerParams, createLog, launch, ProvisionOptions } from './devContainers';
import { SubstitutedConfig, createContainerProperties, createFeaturesTempFolder, envListToObj, inspectDockerImage, isDockerFileConfig, SubstituteConfig, addSubstitution, findContainerAndIdLabels, getCacheFolder } from './utils';
import { SubstitutedConfig, createContainerProperties, envListToObj, inspectDockerImage, isDockerFileConfig, SubstituteConfig, addSubstitution, findContainerAndIdLabels, getCacheFolder } from './utils';
import { URI } from 'vscode-uri';
import { ContainerError } from '../spec-common/errors';
import { Log, LogDimensions, LogLevel, makeLog, mapLogLevel } from '../spec-utils/log';
import { probeRemoteEnv, runLifecycleHooks, runRemoteCommand, UserEnvProbe, setupInContainer } from '../spec-common/injectHeadless';
import { extendImage } from './containerFeatures';
import { DockerCLIParameters, dockerPtyCLI, inspectContainer } from '../spec-shutdown/dockerUtils';
import { buildAndExtendDockerCompose, dockerComposeCLIConfig, getDefaultImageName, getProjectName, readDockerComposeConfig, readVersionPrefix } from './dockerCompose';
import { DevContainerConfig, DevContainerFromDockerComposeConfig, DevContainerFromDockerfileConfig, getDockerComposeFilePaths } from '../spec-configuration/configuration';
import { DevContainerFromDockerComposeConfig, DevContainerFromDockerfileConfig, getDockerComposeFilePaths } from '../spec-configuration/configuration';
import { workspaceFromPath } from '../spec-utils/workspaces';
import { readDevContainerConfigFile } from './configContainer';
import { getDefaultDevContainerConfigPath, getDevContainerConfigPathIn, uriToFsPath } from '../spec-configuration/configurationCommonUtils';
import { CLIHost, getCLIHost } from '../spec-common/cliHost';
import { loadNativeModule, processSignals } from '../spec-common/commonUtils';
import { FeaturesConfig, generateFeaturesConfig, getContainerFeaturesFolder, loadVersionInfo } from '../spec-configuration/containerFeaturesConfiguration';
import { loadVersionInfo } from '../spec-configuration/containerFeaturesConfiguration';
import { featuresTestOptions, featuresTestHandler } from './featuresCLI/test';
import { featuresPackageHandler, featuresPackageOptions } from './featuresCLI/package';
import { featuresPublishHandler, featuresPublishOptions } from './featuresCLI/publish';
import { beforeContainerSubstitute, containerSubstitute, substitute } from '../spec-common/variableSubstitution';
import { getPackageConfig, PackageConfiguration } from '../spec-utils/product';
import { getPackageConfig, } from '../spec-utils/product';
import { getDevcontainerMetadata, getImageBuildInfo, getImageMetadataFromContainer, ImageMetadataEntry, lifecycleCommandOriginMapFromMetadata, mergeConfiguration, MergedDevContainerConfig } from './imageMetadata';
import { templatesPublishHandler, templatesPublishOptions } from './templatesCLI/publish';
import { templateApplyHandler, templateApplyOptions } from './templatesCLI/apply';
Expand All @@ -39,6 +39,8 @@ import { Event, NodeEventEmitter } from '../spec-utils/event';
import { ensureNoDisallowedFeatures } from './disallowedFeatures';
import { featuresResolveDependenciesHandler, featuresResolveDependenciesOptions } from './featuresCLI/resolveDependencies';
import { getFeatureIdWithoutVersion } from '../spec-configuration/containerFeaturesOCI';
import { featuresUpgradeHandler, featuresUpgradeOptions } from './upgradeCommand';
import { readFeaturesConfig } from './featureUtils';

const defaultDefaultUserEnvProbe: UserEnvProbe = 'loginInteractiveShell';

Expand Down Expand Up @@ -68,6 +70,7 @@ const mountRegex = /^type=(bind|volume),source=([^,]+),target=([^,]+)(?:,externa
y.command('run-user-commands', 'Run user commands', runUserCommandsOptions, runUserCommandsHandler);
y.command('read-configuration', 'Read configuration', readConfigurationOptions, readConfigurationHandler);
y.command('outdated', 'Show current and available versions', outdatedOptions, outdatedHandler);
y.command('upgrade', 'Upgrade lockfile', featuresUpgradeOptions, featuresUpgradeHandler);
y.command('features', 'Features commands', (y: Argv) => {
y.command('test [target]', 'Test Features', featuresTestOptions, featuresTestHandler);
y.command('package <target>', 'Package Features', featuresPackageOptions, featuresPackageHandler);
Expand Down Expand Up @@ -1047,14 +1050,6 @@ async function readConfiguration({
process.exit(0);
}

async function readFeaturesConfig(params: DockerCLIParameters, pkg: PackageConfiguration, config: DevContainerConfig, extensionPath: string, skipFeatureAutoMapping: boolean, additionalFeatures: Record<string, string | boolean | Record<string, string | boolean>>): Promise<FeaturesConfig | undefined> {
const { cliHost, output } = params;
const { cwd, env, platform } = cliHost;
const featuresTmpFolder = await createFeaturesTempFolder({ cliHost, package: pkg });
const cacheFolder = await getCacheFolder(cliHost);
return generateFeaturesConfig({ extensionPath, cacheFolder, cwd, output, env, skipFeatureAutoMapping, platform }, featuresTmpFolder, config, getContainerFeaturesFolder, additionalFeatures);
}

function outdatedOptions(y: Argv) {
return y.options({
'user-data-folder': { type: 'string', description: 'Host path to a directory that is intended to be persisted and share state between sessions.' },
Expand Down
13 changes: 13 additions & 0 deletions src/spec-node/featureUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { DevContainerConfig } from '../spec-configuration/configuration';
import { FeaturesConfig, generateFeaturesConfig, getContainerFeaturesFolder } from '../spec-configuration/containerFeaturesConfiguration';
import { DockerCLIParameters } from '../spec-shutdown/dockerUtils';
import { PackageConfiguration } from '../spec-utils/product';
import { createFeaturesTempFolder, getCacheFolder } from './utils';

export async function readFeaturesConfig(params: DockerCLIParameters, pkg: PackageConfiguration, config: DevContainerConfig, extensionPath: string, skipFeatureAutoMapping: boolean, additionalFeatures: Record<string, string | boolean | Record<string, string | boolean>>): Promise<FeaturesConfig | undefined> {
const { cliHost, output } = params;
const { cwd, env, platform } = cliHost;
const featuresTmpFolder = await createFeaturesTempFolder({ cliHost, package: pkg });
const cacheFolder = await getCacheFolder(cliHost);
return generateFeaturesConfig({ extensionPath, cacheFolder, cwd, output, env, skipFeatureAutoMapping, platform }, featuresTmpFolder, config, getContainerFeaturesFolder, additionalFeatures);
}
118 changes: 118 additions & 0 deletions src/spec-node/upgradeCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { Argv } from 'yargs';
import { UnpackArgv } from './devContainersSpecCLI';
import { dockerComposeCLIConfig } from './dockerCompose';
import { Log, LogLevel, mapLogLevel } from '../spec-utils/log';
import { createLog } from './devContainers';
import { getPackageConfig } from '../spec-utils/product';
import { DockerCLIParameters } from '../spec-shutdown/dockerUtils';
import path from 'path';
import { getCLIHost } from '../spec-common/cliHost';
import { loadNativeModule } from '../spec-common/commonUtils';
import { URI } from 'vscode-uri';
import { workspaceFromPath } from '../spec-utils/workspaces';
import { getDefaultDevContainerConfigPath, getDevContainerConfigPathIn, uriToFsPath } from '../spec-configuration/configurationCommonUtils';
import { readDevContainerConfigFile } from './configContainer';
import { ContainerError } from '../spec-common/errors';
import { getCacheFolder } from './utils';
import { getLockfilePath, writeLockfile } from '../spec-configuration/lockfile';
import { writeLocalFile } from '../spec-utils/pfs';
import { readFeaturesConfig } from './featureUtils';

export function featuresUpgradeOptions(y: Argv) {
return y
.options({
'workspace-folder': { type: 'string', description: 'Workspace folder.', demandOption: true },
'docker-path': { type: 'string', description: 'Path to docker executable.', default: 'docker' },
'docker-compose-path': { type: 'string', description: 'Path to docker-compose executable.', default: 'docker-compose' },
'config': { type: 'string', description: 'devcontainer.json path. The default is to use .devcontainer/devcontainer.json or, if that does not exist, .devcontainer.json in the workspace folder.' },
'log-level': { choices: ['error' as 'error', 'info' as 'info', 'debug' as 'debug', 'trace' as 'trace'], default: 'info' as 'info', description: 'Log level.' },
});
}

export type FeaturesUpgradeArgs = UnpackArgv<ReturnType<typeof featuresUpgradeOptions>>;

export function featuresUpgradeHandler(args: FeaturesUpgradeArgs) {
(async () => await featuresUpgrade(args))().catch(console.error);
}

async function featuresUpgrade({
'workspace-folder': workspaceFolderArg,
'docker-path': dockerPath,
config: configArg,
'docker-compose-path': dockerComposePath,
'log-level': inputLogLevel,
}: FeaturesUpgradeArgs) {
const disposables: (() => Promise<unknown> | undefined)[] = [];
const dispose = async () => {
await Promise.all(disposables.map(d => d()));
};
let output: Log | undefined;
try {
const workspaceFolder = path.resolve(process.cwd(), workspaceFolderArg);
const configFile = configArg ? URI.file(path.resolve(process.cwd(), configArg)) : undefined;
const cliHost = await getCLIHost(workspaceFolder, loadNativeModule, true);
const extensionPath = path.join(__dirname, '..', '..');
const sessionStart = new Date();
const pkg = getPackageConfig();
const output = createLog({
logLevel: mapLogLevel(inputLogLevel),
logFormat: 'text',
log: text => process.stderr.write(text),
terminalDimensions: undefined,
}, pkg, sessionStart, disposables);
const dockerComposeCLI = dockerComposeCLIConfig({
exec: cliHost.exec,
env: cliHost.env,
output,
}, dockerPath, dockerComposePath);
const dockerParams: DockerCLIParameters = {
cliHost,
dockerCLI: dockerPath,
dockerComposeCLI,
env: cliHost.env,
output,
};

const workspace = workspaceFromPath(cliHost.path, workspaceFolder);
const configPath = configFile ? configFile : await getDevContainerConfigPathIn(cliHost, workspace.configFolderPath);
const configs = configPath && await readDevContainerConfigFile(cliHost, workspace, configPath, true, output) || undefined;
if (!configs) {
throw new ContainerError({ description: `Dev container config (${uriToFsPath(configFile || getDefaultDevContainerConfigPath(cliHost, workspace!.configFolderPath), cliHost.platform)}) not found.` });
}
const config = configs.config.config;
const cacheFolder = await getCacheFolder(cliHost);
const params = {
extensionPath,
cacheFolder,
cwd: cliHost.cwd,
output,
env: cliHost.env,
skipFeatureAutoMapping: false,
platform: cliHost.platform,
};

const bold = process.stdout.isTTY ? '\x1b[1m' : '';
const clear = process.stdout.isTTY ? '\x1b[0m' : '';
output.raw(`${bold}Upgrading lockfile...\n${clear}\n`, LogLevel.Info);

// Truncate existing lockfile
const lockfilePath = getLockfilePath(config);
await writeLocalFile(lockfilePath, '');
// Update lockfile
const featuresConfig = await readFeaturesConfig(dockerParams, pkg, config, extensionPath, false, {});
if (!featuresConfig) {
throw new ContainerError({ description: `Failed to update lockfile` });
}
await writeLockfile(params, config, featuresConfig, true);
} catch (err) {
if (output) {
output.write(err && (err.stack || err.message) || String(err));
} else {
console.error(err);
}
await dispose();
process.exit(1);
}
await dispose();
process.exit(0);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
"features": {
"ghcr.io/devcontainers/features/git:1.1.5": "latest",
"ghcr.io/devcontainers/features/git-lfs@sha256:24d5802c837b2519b666a8403a9514c7296d769c9607048e9f1e040e7d7e331c": "latest",
"ghcr.io/devcontainers/features/github-cli:1.0.9": "latest",
"ghcr.io/devcontainers/features/azure-cli:1.2.1": "latest"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.devcontainer-lock.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"features": {
"ghcr.io/devcontainers/features/azure-cli:1.2.0": {
"version": "1.2.0",
"resolved": "ghcr.io/devcontainers/features/azure-cli@sha256:cb2832052c03202e321c84389116a3981b5b24b8c6d0532841c46b03500e1415",
"integrity": "sha256:cb2832052c03202e321c84389116a3981b5b24b8c6d0532841c46b03500e1415"
},
"ghcr.io/devcontainers/features/git-lfs@sha256:24d5802c837b2519b666a8403a9514c7296d769c9607048e9f1e040e7d7e331c": {
"version": "1.0.6",
"resolved": "ghcr.io/devcontainers/features/git-lfs@sha256:24d5802c837b2519b666a8403a9514c7296d769c9607048e9f1e040e7d7e331c",
"integrity": "sha256:24d5802c837b2519b666a8403a9514c7296d769c9607048e9f1e040e7d7e331c"
},
"ghcr.io/devcontainers/features/git:1.1.0": {
"version": "1.1.0",
"resolved": "ghcr.io/devcontainers/features/git@sha256:bc4b9ae3f843a35edfea7b9295a0e89958d2ddfe8b2bf327ec1a5f7cf3c5a2fa",
"integrity": "sha256:bc4b9ae3f843a35edfea7b9295a0e89958d2ddfe8b2bf327ec1a5f7cf3c5a2fa"
},
"ghcr.io/devcontainers/features/github-cli:1.0.2": {
"version": "1.0.2",
"resolved": "ghcr.io/devcontainers/features/github-cli@sha256:0b52d28bcaf2054bf70fd932161f93aae34830031d29747680acdd500e02cc09",
"integrity": "sha256:0b52d28bcaf2054bf70fd932161f93aae34830031d29747680acdd500e02cc09"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"features": {
"ghcr.io/devcontainers/features/azure-cli:1.2.1": {
"version": "1.2.1",
"resolved": "ghcr.io/devcontainers/features/azure-cli@sha256:a00aa292592a8df58a940d6f6dfcf2bfd3efab145f62a17ccb12656528793134",
"integrity": "sha256:a00aa292592a8df58a940d6f6dfcf2bfd3efab145f62a17ccb12656528793134"
},
"ghcr.io/devcontainers/features/git-lfs@sha256:24d5802c837b2519b666a8403a9514c7296d769c9607048e9f1e040e7d7e331c": {
"version": "1.0.6",
"resolved": "ghcr.io/devcontainers/features/git-lfs@sha256:24d5802c837b2519b666a8403a9514c7296d769c9607048e9f1e040e7d7e331c",
"integrity": "sha256:24d5802c837b2519b666a8403a9514c7296d769c9607048e9f1e040e7d7e331c"
},
"ghcr.io/devcontainers/features/git:1.1.5": {
"version": "1.1.5",
"resolved": "ghcr.io/devcontainers/features/git@sha256:2ab83ca71d55d5c00a1255b07f3a83a53cd2de77ce8b9637abad38095d672a5b",
"integrity": "sha256:2ab83ca71d55d5c00a1255b07f3a83a53cd2de77ce8b9637abad38095d672a5b"
},
"ghcr.io/devcontainers/features/github-cli:1.0.9": {
"version": "1.0.9",
"resolved": "ghcr.io/devcontainers/features/github-cli@sha256:9024deeca80347dea7603a3bb5b4951988f0bf5894ba036a6ee3f29c025692c6",
"integrity": "sha256:9024deeca80347dea7603a3bb5b4951988f0bf5894ba036a6ee3f29c025692c6"
}
}
}
12 changes: 12 additions & 0 deletions src/test/container-features/lockfile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,18 @@ describe('Lockfile', function () {
assert.ok(azure.latest);
});

it('upgrade command', async () => {
const workspaceFolder = path.join(__dirname, 'configs/lockfile-upgrade-command');

const lockfilePath = path.join(workspaceFolder, '.devcontainer-lock.json');
await cpLocal(path.join(workspaceFolder, 'outdated.devcontainer-lock.json'), lockfilePath);

await shellExec(`${cli} upgrade --workspace-folder ${workspaceFolder}`);
const actual = await readLocalFile(lockfilePath);
const expected = await readLocalFile(path.join(workspaceFolder, 'upgraded.devcontainer-lock.json'));
assert.equal(actual.toString(), expected.toString());
});

it('OCI feature integrity', async () => {
const workspaceFolder = path.join(__dirname, 'configs/lockfile-oci-integrity');

Expand Down

0 comments on commit 722548e

Please sign in to comment.