Skip to content
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

feat(DockerImage): allow mounting existing or volume copy volumes #32832

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion packages/@aws-cdk/aws-lambda-go-alpha/lib/bundling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export class Bundling implements cdk.BundlingOptions {
public readonly environment?: { [key: string]: string };
public readonly local?: cdk.ILocalBundling;
public readonly entrypoint?: string[];
public readonly volumes?: cdk.DockerVolume[];
public readonly volumes?: (cdk.DockerVolume | cdk.VolumeCopyDockerVolume | cdk.ExistingDockerVolume)[];
public readonly volumesFrom?: string[];
public readonly workingDirectory?: string;
public readonly user?: string;
Expand Down
4 changes: 2 additions & 2 deletions packages/@aws-cdk/aws-lambda-python-alpha/lib/bundling.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as path from 'path';
import { Architecture, AssetCode, Code, Runtime } from 'aws-cdk-lib/aws-lambda';
import { AssetStaging, BundlingFileAccess, BundlingOptions as CdkBundlingOptions, DockerImage, DockerVolume } from 'aws-cdk-lib/core';
import { AssetStaging, BundlingFileAccess, BundlingOptions as CdkBundlingOptions, DockerImage, DockerVolume, type ExistingDockerVolume, type VolumeCopyDockerVolume } from 'aws-cdk-lib/core';
import { Packaging, DependenciesFile } from './packaging';
import { BundlingOptions, ICommandHooks } from './types';

Expand Down Expand Up @@ -65,7 +65,7 @@ export class Bundling implements CdkBundlingOptions {
public readonly image: DockerImage;
public readonly entrypoint?: string[];
public readonly command: string[];
public readonly volumes?: DockerVolume[];
public readonly volumes?: (DockerVolume | VolumeCopyDockerVolume | ExistingDockerVolume)[];
public readonly volumesFrom?: string[];
public readonly environment?: { [key: string]: string };
public readonly workingDirectory?: string;
Expand Down
6 changes: 4 additions & 2 deletions packages/aws-cdk-lib/aws-lambda-nodejs/lib/bundling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import { IConstruct } from 'constructs';
import { PackageInstallation } from './package-installation';
import { LockFile, PackageManager } from './package-manager';
import { BundlingOptions, OutputFormat, SourceMapMode } from './types';
import { exec, extractDependencies, findUp, getTsconfigCompilerOptions, isSdkV2Runtime } from './util';
import {
exec, extractDependencies, findUp, getTsconfigCompilerOptions, isSdkV2Runtime,
} from './util';
import { Architecture, AssetCode, Code, Runtime } from '../../aws-lambda';
import * as cdk from '../../core';
import { LAMBDA_NODEJS_SDK_V3_EXCLUDE_SMITHY_PACKAGES } from '../../cx-api';
Expand Down Expand Up @@ -83,7 +85,7 @@ export class Bundling implements cdk.BundlingOptions {
public readonly image: cdk.DockerImage;
public readonly entrypoint?: string[];
public readonly command: string[];
public readonly volumes?: cdk.DockerVolume[];
public readonly volumes?: (cdk.DockerVolume | cdk.VolumeCopyDockerVolume | cdk.ExistingDockerVolume)[];
public readonly volumesFrom?: string[];
public readonly environment?: { [key: string]: string };
public readonly workingDirectory: string;
Expand Down
45 changes: 41 additions & 4 deletions packages/aws-cdk-lib/core/lib/asset-staging.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import * as crypto from 'crypto';
import * as os from 'os';
import * as path from 'path';
import { Construct } from 'constructs';
import * as fs from 'fs-extra';
import { AssetHashType, AssetOptions, FileAssetPackaging } from './assets';
import { BundlingFileAccess, BundlingOptions, BundlingOutput } from './bundling';
import { BundlingFileAccess, BundlingOptions, BundlingOutput, DockerVolumeType } from './bundling';
import { FileSystem, FingerprintOptions } from './fs';
import { clearLargeFileFingerprintCache } from './fs/fingerprint';
import { Names } from './names';
import { AssetBundlingVolumeCopy, AssetBundlingBindMount } from './private/asset-staging';
import { Cache } from './private/cache';
import { Stack } from './stack';
import { Stage } from './stage';
Expand Down Expand Up @@ -452,17 +452,43 @@ export class AssetStaging extends Construct {
sourcePath: this.sourcePath,
bundleDir,
...options,
securityOpt: options.securityOpt ?? '',
user: options.user ?? this.determineUser(),
volumes: [...(options.volumes ?? [])],
workingDirectory:
options.workingDirectory ?? AssetStaging.BUNDLING_INPUT_DIR,
};

// Add the asset input and output volumes based on BundlingFileAccess setting
switch (options.bundlingFileAccess) {
case BundlingFileAccess.VOLUME_COPY:
new AssetBundlingVolumeCopy(assetStagingOptions).run();
assetStagingOptions.volumes.push({
dockerVolumeType: DockerVolumeType.VOLUME_COPY,
hostInputPath: assetStagingOptions.sourcePath,
containerPath: AssetStaging.BUNDLING_INPUT_DIR,
},
{
dockerVolumeType: DockerVolumeType.VOLUME_COPY,
hostOutputPath: assetStagingOptions.bundleDir,
containerPath: AssetStaging.BUNDLING_OUTPUT_DIR,
});
break;
case BundlingFileAccess.BIND_MOUNT:
default:
new AssetBundlingBindMount(assetStagingOptions).run();
assetStagingOptions.volumes.push({
dockerVolumeType: DockerVolumeType.BIND_MOUNT,
hostPath: assetStagingOptions.sourcePath,
containerPath: AssetStaging.BUNDLING_INPUT_DIR,
},
{
dockerVolumeType: DockerVolumeType.BIND_MOUNT,
hostPath: assetStagingOptions.bundleDir,
containerPath: AssetStaging.BUNDLING_OUTPUT_DIR,
});
break;
}

assetStagingOptions.image.run(assetStagingOptions);
}
} catch (err) {
// When bundling fails, keep the bundle output for diagnosability, but
Expand Down Expand Up @@ -526,6 +552,17 @@ export class AssetStaging extends Construct {
}
return targetPath;
}

/**
* Determines a useful default user if not given otherwise
*/
private determineUser(): string {
// Default to current user
const userInfo = os.userInfo();
return userInfo.uid !== -1 // uid is -1 on Windows
? `${userInfo.uid}:${userInfo.gid}`
: '1000:1000';
}
}

function renderAssetFilename(assetHash: string, extension = '') {
Expand Down
189 changes: 125 additions & 64 deletions packages/aws-cdk-lib/core/lib/bundling.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { spawnSync } from 'child_process';
import * as crypto from 'crypto';
import { isAbsolute, join } from 'path';
import { DockerCacheOption } from './assets';
import { FileSystem } from './fs';
import { dockerExec } from './private/asset-staging';
import { dockerExec, DockerVolumeHelper } from './private/bundling';
import { quiet, reset } from './private/jsii-deprecated';

/**
Expand Down Expand Up @@ -61,7 +60,7 @@ export interface BundlingOptions {
*
* @default - no additional volumes are mounted
*/
readonly volumes?: DockerVolume[];
readonly volumes?: (DockerVolume | VolumeCopyDockerVolume | ExistingDockerVolume)[];

/**
* Where to mount the specified volumes from
Expand Down Expand Up @@ -255,7 +254,6 @@ export class BundlingDockerImage {
* Runs a Docker image
*/
public run(options: DockerRunOptions = {}) {
const volumes = options.volumes || [];
const environment = options.environment || {};
const entrypoint = options.entrypoint?.[0] || null;
const command = [
Expand All @@ -267,36 +265,39 @@ export class BundlingDockerImage {
: [],
];

const dockerArgs: string[] = [
'run', '--rm',
...options.securityOpt
? ['--security-opt', options.securityOpt]
: [],
...options.network
? ['--network', options.network]
: [],
...options.platform
? ['--platform', options.platform]
: [],
...options.user
? ['-u', options.user]
: [],
...options.volumesFrom
? flatten(options.volumesFrom.map(v => ['--volumes-from', v]))
: [],
...flatten(volumes.map(v => ['-v', `${v.hostPath}:${v.containerPath}:${isSeLinux() ? 'z,' : ''}${v.consistency ?? DockerVolumeConsistency.DELEGATED}`])),
...flatten(Object.entries(environment).map(([k, v]) => ['--env', `${k}=${v}`])),
...options.workingDirectory
? ['-w', options.workingDirectory]
: [],
...entrypoint
? ['--entrypoint', entrypoint]
: [],
this.image,
...command,
];
const volumeHelper = new DockerVolumeHelper(options);

dockerExec(dockerArgs);
try {
const dockerArgs: string[] = [
'run', '--rm',
...options.securityOpt
? ['--security-opt', options.securityOpt]
: [],
...options.network
? ['--network', options.network]
: [],
...options.platform
? ['--platform', options.platform]
: [],
...options.user
? ['-u', options.user]
: [],
...volumeHelper.volumeCommands,
...flatten(Object.entries(environment).map(([k, v]) => ['--env', `${k}=${v}`])),
...options.workingDirectory
? ['-w', options.workingDirectory]
: [],
...entrypoint
? ['--entrypoint', entrypoint]
: [],
this.image,
...command,
];

dockerExec(dockerArgs);
} finally {
volumeHelper.cleanup();
}
}

/**
Expand Down Expand Up @@ -461,19 +462,62 @@ export class DockerImage extends BundlingDockerImage {
}

/**
* A Docker volume
* The access mechanism used to make this volume available to the bundling container
*/
export interface DockerVolume {
export enum DockerVolumeType {
/**
* The path to the file or directory on the host machine
* Creates temporary volumes and containers to copy files from the host to the bundling container and back.
* This is slower, but works also in more complex situations with remote or shared docker sockets.
*/
readonly hostPath: string;
VOLUME_COPY = 'VOLUME_COPY',

/**
* The source and output folders will be mounted as bind mount from the host system
* This is faster and simpler, but less portable than `VOLUME_COPY`.
*/
BIND_MOUNT = 'BIND_MOUNT',

/**
* The volume already exists and will not be created or destroyed by this class
*/
EXISTING = 'EXISTING',
}

/**
* Common properties for all Docker volume types.
*/
export interface DockerVolumeBase {
/**
* The path where the file or directory is mounted in the container
* The path inside the container where the volume is mounted.
* This property is required for all volume types.
*/
readonly containerPath: string;

/**
* `--volume` options to use when mounting this volume to the docker container.
*
* @default - 'z' option is used for selinux bind mounts
*/
readonly opts?: string[];
}

/**
* Configuration for a `BIND_MOUNT` type Docker volume.
*/
export interface DockerVolume extends DockerVolumeBase {
/**
* The type of the Docker volume.
*
* @default DockerVolumeType.BIND_MOUNT
*/
readonly dockerVolumeType?: DockerVolumeType.BIND_MOUNT;

/**
* The path on the host machine to be mounted as a bind mount.
* This property is required for `BIND_MOUNT` volumes.
*/
readonly hostPath: string;

/**
* Mount consistency. Only applicable for macOS
*
Expand All @@ -483,6 +527,48 @@ export interface DockerVolume {
readonly consistency?: DockerVolumeConsistency;
}

/**
* Configuration for a `VOLUME_COPY` type Docker volume.
*/
export interface VolumeCopyDockerVolume extends DockerVolumeBase {
/**
* The type of the Docker volume.
*
* @default DockerVolumeType.BIND_MOUNT
*/
readonly dockerVolumeType: DockerVolumeType.VOLUME_COPY;

/**
* The path on the host machine to be used as the input for the volume.
* @default - Does not copy from the host machine
*/
readonly hostInputPath?: string;

/**
* The path on the host machine where the output from the volume will be written.
* @default - Does not copy to the host machine
*/
readonly hostOutputPath?: string;
}

/**
* Configuration for an `EXISTING` type Docker volume.
*/
export interface ExistingDockerVolume extends DockerVolumeBase {
/**
* The type of the Docker volume.
*
* @default DockerVolumeType.BIND_MOUNT
*/
readonly dockerVolumeType: DockerVolumeType.EXISTING;

/**
* The name of the existing volume.
* This property is required for `EXISTING` volumes.
*/
readonly volumeName: string;
}

/**
* Supported Docker volume consistency types. Only valid on macOS due to the way file storage works on Mac
*/
Expand Down Expand Up @@ -524,7 +610,7 @@ export interface DockerRunOptions {
*
* @default - no volumes are mounted
*/
readonly volumes?: DockerVolume[];
readonly volumes?: (DockerVolume | VolumeCopyDockerVolume | ExistingDockerVolume)[];

/**
* Where to mount the specified volumes from
Expand Down Expand Up @@ -640,28 +726,3 @@ export interface DockerBuildOptions {
function flatten(x: string[][]) {
return Array.prototype.concat([], ...x);
}

function isSeLinux(): boolean {
if (process.platform != 'linux') {
return false;
}
const prog = 'selinuxenabled';
const proc = spawnSync(prog, [], {
stdio: [ // show selinux status output
'pipe', // get value of stdio
process.stderr, // redirect stdout to stderr
'inherit', // inherit stderr
],
});
if (proc.error) {
// selinuxenabled not a valid command, therefore not enabled
return false;
}
if (proc.status == 0) {
// selinux enabled
return true;
} else {
// selinux not enabled
return false;
}
}
Loading
Loading