Skip to content

Image metadata #188

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Sep 26, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
63 changes: 49 additions & 14 deletions src/spec-common/injectHeadless.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export interface ResolverParameters {
buildxPush: boolean;
skipFeatureAutoMapping: boolean;
skipPostAttach: boolean;
experimentalImageMetadata: boolean;
}

export interface PostCreate {
Expand Down Expand Up @@ -125,6 +126,37 @@ export interface CommonDevContainerConfig {
userEnvProbe?: UserEnvProbe;
}

export interface CommonContainerMetadata {
onCreateCommand?: string | string[];
updateContentCommand?: string | string[];
postCreateCommand?: string | string[];
postStartCommand?: string | string[];
postAttachCommand?: string | string[];
waitFor?: DevContainerConfigCommand;
remoteEnv?: Record<string, string | null>;
userEnvProbe?: UserEnvProbe;
}

export type CommonMergedDevContainerConfig = MergedConfig<CommonDevContainerConfig>;

type MergedConfig<T extends CommonDevContainerConfig> = Omit<T, typeof replaceProperties[number]> & UpdatedConfigProperties;

const replaceProperties = [
'onCreateCommand',
'updateContentCommand',
'postCreateCommand',
'postStartCommand',
'postAttachCommand',
] as const;

interface UpdatedConfigProperties {
onCreateCommands?: (string | string[])[];
updateContentCommands?: (string | string[])[];
postCreateCommands?: (string | string[])[];
postStartCommands?: (string | string[])[];
postAttachCommands?: (string | string[])[];
}

export interface OSRelease {
hardware: string;
id: string;
Expand Down Expand Up @@ -262,7 +294,7 @@ export function getSystemVarFolder(params: ResolverParameters): string {
return params.containerSystemDataFolder || '/var/devcontainer';
}

export async function setupInContainer(params: ResolverParameters, containerProperties: ContainerProperties, config: CommonDevContainerConfig) {
export async function setupInContainer(params: ResolverParameters, containerProperties: ContainerProperties, config: CommonMergedDevContainerConfig) {
await patchEtcEnvironment(params, containerProperties);
await patchEtcProfile(params, containerProperties);
const computeRemoteEnv = params.computeExtensionHostEnv || params.postCreate.enabled;
Expand All @@ -276,7 +308,7 @@ export async function setupInContainer(params: ResolverParameters, containerProp
};
}

export function probeRemoteEnv(params: ResolverParameters, containerProperties: ContainerProperties, config: CommonDevContainerConfig) {
export function probeRemoteEnv(params: ResolverParameters, containerProperties: ContainerProperties, config: CommonMergedDevContainerConfig) {
return probeUserEnv(params, containerProperties, config)
.then<Record<string, string>>(shellEnv => ({
...shellEnv,
Expand All @@ -285,7 +317,7 @@ export function probeRemoteEnv(params: ResolverParameters, containerProperties:
} as Record<string, string>));
}

export async function runPostCreateCommands(params: ResolverParameters, containerProperties: ContainerProperties, config: CommonDevContainerConfig, remoteEnv: Promise<Record<string, string>>, stopForPersonalization: boolean): Promise<'skipNonBlocking' | 'prebuild' | 'stopForPersonalization' | 'done'> {
export async function runPostCreateCommands(params: ResolverParameters, containerProperties: ContainerProperties, config: CommonMergedDevContainerConfig, remoteEnv: Promise<Record<string, string>>, stopForPersonalization: boolean): Promise<'skipNonBlocking' | 'prebuild' | 'stopForPersonalization' | 'done'> {
const skipNonBlocking = params.postCreate.skipNonBlocking;
const waitFor = config.waitFor || defaultWaitFor;
if (skipNonBlocking && waitFor === 'initializeCommand') {
Expand Down Expand Up @@ -342,16 +374,16 @@ export async function getOSRelease(shellServer: ShellServer) {
return { hardware, id, version };
}

async function runPostCreateCommand(params: ResolverParameters, containerProperties: ContainerProperties, config: CommonDevContainerConfig, postCommandName: 'onCreateCommand' | 'updateContentCommand' | 'postCreateCommand', remoteEnv: Promise<Record<string, string>>, rerun: boolean) {
async function runPostCreateCommand(params: ResolverParameters, containerProperties: ContainerProperties, config: CommonMergedDevContainerConfig, postCommandName: 'onCreateCommand' | 'updateContentCommand' | 'postCreateCommand', remoteEnv: Promise<Record<string, string>>, rerun: boolean) {
const markerFile = path.posix.join(containerProperties.userDataFolder, `.${postCommandName}Marker`);
const doRun = !!containerProperties.createdAt && await updateMarkerFile(containerProperties.shellServer, markerFile, containerProperties.createdAt) || rerun;
await runPostCommand(params, containerProperties, config, postCommandName, remoteEnv, doRun);
await runPostCommands(params, containerProperties, config, postCommandName, remoteEnv, doRun);
}

async function runPostStartCommand(params: ResolverParameters, containerProperties: ContainerProperties, config: CommonDevContainerConfig, remoteEnv: Promise<Record<string, string>>) {
async function runPostStartCommand(params: ResolverParameters, containerProperties: ContainerProperties, config: CommonMergedDevContainerConfig, remoteEnv: Promise<Record<string, string>>) {
const markerFile = path.posix.join(containerProperties.userDataFolder, '.postStartCommandMarker');
const doRun = !!containerProperties.startedAt && await updateMarkerFile(containerProperties.shellServer, markerFile, containerProperties.startedAt);
await runPostCommand(params, containerProperties, config, 'postStartCommand', remoteEnv, doRun);
await runPostCommands(params, containerProperties, config, 'postStartCommand', remoteEnv, doRun);
}

async function updateMarkerFile(shellServer: ShellServer, location: string, content: string) {
Expand All @@ -363,12 +395,15 @@ async function updateMarkerFile(shellServer: ShellServer, location: string, cont
}
}

async function runPostAttachCommand(params: ResolverParameters, containerProperties: ContainerProperties, config: CommonDevContainerConfig, remoteEnv: Promise<Record<string, string>>) {
await runPostCommand(params, containerProperties, config, 'postAttachCommand', remoteEnv, true);
async function runPostAttachCommand(params: ResolverParameters, containerProperties: ContainerProperties, config: CommonMergedDevContainerConfig, remoteEnv: Promise<Record<string, string>>) {
await runPostCommands(params, containerProperties, config, 'postAttachCommand', remoteEnv, true);
}

async function runPostCommands(params: ResolverParameters, containerProperties: ContainerProperties, config: CommonMergedDevContainerConfig, postCommandName: 'onCreateCommand' | 'updateContentCommand' | 'postCreateCommand' | 'postStartCommand' | 'postAttachCommand', remoteEnv: Promise<Record<string, string>>, doRun: boolean) {
return Promise.all((config[`${postCommandName}s`] || []).map(config => runPostCommand(params, containerProperties, config, postCommandName, remoteEnv, doRun)));
}

async function runPostCommand({ postCreate }: ResolverParameters, containerProperties: ContainerProperties, config: CommonDevContainerConfig, postCommandName: 'onCreateCommand' | 'updateContentCommand' | 'postCreateCommand' | 'postStartCommand' | 'postAttachCommand', remoteEnv: Promise<Record<string, string>>, doRun: boolean) {
const postCommand = config[postCommandName];
async function runPostCommand({ postCreate }: ResolverParameters, containerProperties: ContainerProperties, postCommand: string | string[], postCommandName: 'onCreateCommand' | 'updateContentCommand' | 'postCreateCommand' | 'postStartCommand' | 'postAttachCommand', remoteEnv: Promise<Record<string, string>>, doRun: boolean) {
if (doRun && postCommand && (typeof postCommand === 'string' ? postCommand.trim() : postCommand.length)) {
const progressName = `Running ${postCommandName}...`;
const progressDetail = typeof postCommand === 'string' ? postCommand : postCommand.join(' ');
Expand Down Expand Up @@ -585,7 +620,7 @@ async function patchEtcProfile(params: ResolverParameters, containerProperties:
}
}

async function probeUserEnv(params: { defaultUserEnvProbe: UserEnvProbe; allowSystemConfigChange: boolean; output: Log }, containerProperties: { shell: string; remoteExec: ExecFunction; installFolder?: string; env?: NodeJS.ProcessEnv; shellServer?: ShellServer; launchRootShellServer?: (() => Promise<ShellServer>); user?: string }, config?: { userEnvProbe?: UserEnvProbe }) {
async function probeUserEnv(params: { defaultUserEnvProbe: UserEnvProbe; allowSystemConfigChange: boolean; output: Log }, containerProperties: { shell: string; remoteExec: ExecFunction; installFolder?: string; env?: NodeJS.ProcessEnv; shellServer?: ShellServer; launchRootShellServer?: (() => Promise<ShellServer>); user?: string }, config?: CommonMergedDevContainerConfig) {
const env = await runUserEnvProbe(params, containerProperties, config, 'cat /proc/self/environ', '\0');
if (env) {
return env;
Expand All @@ -595,8 +630,8 @@ async function probeUserEnv(params: { defaultUserEnvProbe: UserEnvProbe; allowSy
return env2 || {};
}

async function runUserEnvProbe(params: { defaultUserEnvProbe: UserEnvProbe; allowSystemConfigChange: boolean; output: Log }, containerProperties: { shell: string; remoteExec: ExecFunction; installFolder?: string; env?: NodeJS.ProcessEnv; shellServer?: ShellServer; launchRootShellServer?: (() => Promise<ShellServer>); user?: string }, config: { userEnvProbe?: UserEnvProbe } | undefined, cmd: string, sep: string) {
let { userEnvProbe } = config || {};
async function runUserEnvProbe(params: { defaultUserEnvProbe: UserEnvProbe; allowSystemConfigChange: boolean; output: Log }, containerProperties: { shell: string; remoteExec: ExecFunction; installFolder?: string; env?: NodeJS.ProcessEnv; shellServer?: ShellServer; launchRootShellServer?: (() => Promise<ShellServer>); user?: string }, config: CommonMergedDevContainerConfig | undefined, cmd: string, sep: string) {
let userEnvProbe = config?.userEnvProbe;
params.output.write(`userEnvProbe: ${userEnvProbe || params.defaultUserEnvProbe}${userEnvProbe ? '' : ' (default)'}`);
if (!userEnvProbe) {
userEnvProbe = params.defaultUserEnvProbe;
Expand Down
27 changes: 23 additions & 4 deletions src/spec-configuration/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import * as path from 'path';
import { URI } from 'vscode-uri';
import { FileHost, parentURI, uriToFsPath } from './configurationCommonUtils';
import { Mount } from './containerFeaturesConfiguration';
import { RemoteDocuments } from './editableFiles';

export type DevContainerConfig = DevContainerFromImageConfig | DevContainerFromDockerfileConfig | DevContainerFromDockerComposeConfig;
Expand Down Expand Up @@ -52,16 +53,21 @@ export interface DevContainerFromImageConfig {
/** remote path to folder or workspace */
workspaceFolder?: string;
workspaceMount?: string;
mounts?: string[];
mounts?: (Mount | string)[];
containerEnv?: Record<string, string>;
remoteEnv?: Record<string, string | null>;
containerUser?: string;
init?: boolean;
privileged?: boolean;
capAdd?: string[];
securityOpt?: string[];
remoteEnv?: Record<string, string | null>;
remoteUser?: string;
updateRemoteUserUID?: boolean;
userEnvProbe?: UserEnvProbe;
features?: Record<string, string | boolean | Record<string, string | boolean>>;
overrideFeatureInstallOrder?: string[];
hostRequirements?: HostRequirements;
customizations?: Record<string, any>;
}

export type DevContainerFromDockerfileConfig = {
Expand All @@ -84,16 +90,21 @@ export type DevContainerFromDockerfileConfig = {
/** remote path to folder or workspace */
workspaceFolder?: string;
workspaceMount?: string;
mounts?: string[];
mounts?: (Mount | string)[];
containerEnv?: Record<string, string>;
remoteEnv?: Record<string, string | null>;
containerUser?: string;
init?: boolean;
privileged?: boolean;
capAdd?: string[];
securityOpt?: string[];
remoteEnv?: Record<string, string | null>;
remoteUser?: string;
updateRemoteUserUID?: boolean;
userEnvProbe?: UserEnvProbe;
features?: Record<string, string | boolean | Record<string, string | boolean>>;
overrideFeatureInstallOrder?: string[];
hostRequirements?: HostRequirements;
customizations?: Record<string, any>;
} & (
{
dockerFile: string;
Expand Down Expand Up @@ -135,13 +146,21 @@ export interface DevContainerFromDockerComposeConfig {
postAttachCommand?: string | string[];
waitFor?: DevContainerConfigCommand;
runServices?: string[];
mounts?: (Mount | string)[];
containerEnv?: Record<string, string>;
containerUser?: string;
init?: boolean;
privileged?: boolean;
capAdd?: string[];
securityOpt?: string[];
remoteEnv?: Record<string, string | null>;
remoteUser?: string;
updateRemoteUserUID?: boolean;
userEnvProbe?: UserEnvProbe;
features?: Record<string, string | boolean | Record<string, string | boolean>>;
overrideFeatureInstallOrder?: string[];
hostRequirements?: HostRequirements;
customizations?: Record<string, any>;
}

interface DevContainerVSCodeConfig {
Expand Down
8 changes: 8 additions & 0 deletions src/spec-configuration/containerFeaturesConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ export interface Mount {
external?: boolean;
}

export function parseMount(str: string): Mount {
return str.split(',')
.map(s => s.split('='))
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}) as Mount;
}

export type SourceInformation = LocalCacheSourceInformation | GithubSourceInformation | DirectTarballSourceInformation | FilePathSourceInformation | OCISourceInformation;

interface BaseSourceInformation {
Expand Down Expand Up @@ -221,6 +227,8 @@ COPY --from=dev_containers_feature_content_source {contentSourceRootPath} /tmp/b

ARG _DEV_CONTAINERS_IMAGE_USER=root
USER $_DEV_CONTAINERS_IMAGE_USER

#{devcontainerMetadata}
`;
}

Expand Down
22 changes: 14 additions & 8 deletions src/spec-node/configContainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ import * as jsonc from 'jsonc-parser';

import { openDockerfileDevContainer } from './singleContainer';
import { openDockerComposeDevContainer } from './dockerCompose';
import { ResolverResult, DockerResolverParameters, isDockerFileConfig, runUserCommand, createDocuments, getWorkspaceConfiguration, BindMountConsistency, uriToFsPath, DevContainerAuthority, isDevContainerAuthority } from './utils';
import { ResolverResult, DockerResolverParameters, isDockerFileConfig, runUserCommand, createDocuments, getWorkspaceConfiguration, BindMountConsistency, uriToFsPath, DevContainerAuthority, isDevContainerAuthority, SubstituteConfig, SubstitutedConfig } from './utils';
import { substitute } from '../spec-common/variableSubstitution';
import { ContainerError } from '../spec-common/errors';
import { Workspace, workspaceFromPath, isWorkspacePath } from '../spec-utils/workspaces';
import { URI } from 'vscode-uri';
import { CLIHost } from '../spec-common/commonUtils';
import { Log } from '../spec-utils/log';
import { getDefaultDevContainerConfigPath, getDevContainerConfigPathIn } from '../spec-configuration/configurationCommonUtils';
import { DevContainerConfig, updateFromOldProperties } from '../spec-configuration/configuration';
import { DevContainerConfig, DevContainerFromDockerComposeConfig, DevContainerFromDockerfileConfig, DevContainerFromImageConfig, updateFromOldProperties } from '../spec-configuration/configuration';

export { getWellKnownDevContainerPaths as getPossibleDevContainerPaths } from '../spec-configuration/configurationCommonUtils';

Expand Down Expand Up @@ -52,18 +52,19 @@ async function resolveWithLocalFolder(params: DockerResolverParameters, parsedAu
throw new ContainerError({ description: `No dev container config and no workspace found.` });
}
}
const config = configs.config;
const configWithRaw = configs.config;
const { config } = configWithRaw;

await runUserCommand({ ...params, common: { ...common, output: common.postCreate.output } }, config.initializeCommand, common.postCreate.onDidInput);

let result: ResolverResult;
if (isDockerFileConfig(config) || 'image' in config) {
result = await openDockerfileDevContainer(params, config, configs.workspaceConfig, idLabels);
result = await openDockerfileDevContainer(params, configWithRaw as SubstitutedConfig<DevContainerFromDockerfileConfig | DevContainerFromImageConfig>, configs.workspaceConfig, idLabels);
} else if ('dockerComposeFile' in config) {
if (!workspace) {
throw new ContainerError({ description: `A Dev Container using Docker Compose requires a workspace folder.` });
}
result = await openDockerComposeDevContainer(params, workspace, config, idLabels);
result = await openDockerComposeDevContainer(params, workspace, configWithRaw as SubstitutedConfig<DevContainerFromDockerComposeConfig>, idLabels);
} else {
throw new ContainerError({ description: `Dev container config (${(config as DevContainerConfig).configFilePath}) is missing one of "image", "dockerFile" or "dockerComposeFile" properties.` });
}
Expand All @@ -82,13 +83,14 @@ export async function readDevContainerConfigFile(cliHost: CLIHost, workspace: Wo
throw new ContainerError({ description: `Dev container config (${uriToFsPath(configFile, cliHost.platform)}) must contain a JSON object literal.` });
}
const workspaceConfig = await getWorkspaceConfiguration(cliHost, workspace, updated, mountWorkspaceGitRoot, output, consistency);
const config: DevContainerConfig = substitute({
const substitute0: SubstituteConfig = value => substitute({
platform: cliHost.platform,
localWorkspaceFolder: workspace?.rootFolderPath,
containerWorkspaceFolder: workspaceConfig.workspaceFolder,
configFile,
env: cliHost.env,
}, updated);
}, value);
const config: DevContainerConfig = substitute0(updated);
if (typeof config.workspaceFolder === 'string') {
workspaceConfig.workspaceFolder = config.workspaceFolder;
}
Expand All @@ -97,7 +99,11 @@ export async function readDevContainerConfigFile(cliHost: CLIHost, workspace: Wo
}
config.configFilePath = configFile;
return {
config,
config: {
config,
raw: updated,
substitute: substitute0,
},
workspaceConfig,
};
}
Loading