packages/aws-cdk-lib/aws-ec2/lib/cfn-init-elements.ts (520 lines of code) (raw):
import * as fs from 'fs';
import { InitBindOptions, InitElementConfig, InitElementType, InitPlatform } from './private/cfn-init-internal';
import * as iam from '../../aws-iam';
import * as s3 from '../../aws-s3';
import * as s3_assets from '../../aws-s3-assets';
import { Duration, UnscopedValidationError, ValidationError } from '../../core';
import { md5hash } from '../../core/lib/helpers-internal';
/**
* An object that represents reasons to restart an InitService
*
* Pass an instance of this object to the `InitFile`, `InitCommand`,
* `InitSource` and `InitPackage` objects, and finally to an `InitService`
* itself to cause the actions (files, commands, sources, and packages)
* to trigger a restart of the service.
*
* For example, the following will run a custom command to install Nginx,
* and trigger the nginx service to be restarted after the command has run.
*
* ```ts
* const handle = new ec2.InitServiceRestartHandle();
* ec2.CloudFormationInit.fromElements(
* ec2.InitCommand.shellCommand('/usr/bin/custom-nginx-install.sh', { serviceRestartHandles: [handle] }),
* ec2.InitService.enable('nginx', { serviceRestartHandle: handle }),
* );
* ```
*/
export class InitServiceRestartHandle {
private readonly commands = new Array<string>();
private readonly files = new Array<string>();
private readonly sources = new Array<string>();
private readonly packages: Record<string, string[]> = {};
/**
* Add a command key to the restart set
* @internal
*/
public _addCommand(key: string) {
return this.commands.push(key);
}
/**
* Add a file key to the restart set
* @internal
*/
public _addFile(key: string) {
return this.files.push(key);
}
/**
* Add a source key to the restart set
* @internal
*/
public _addSource(key: string) {
return this.sources.push(key);
}
/**
* Add a package key to the restart set
* @internal
*/
public _addPackage(packageType: string, key: string) {
if (!this.packages[packageType]) {
this.packages[packageType] = [];
}
this.packages[packageType].push(key);
}
/**
* Render the restart handles for use in an InitService declaration
* @internal
*/
public _renderRestartHandles(): any {
const nonEmpty = <A>(x: A[]) => x.length > 0 ? x : undefined;
return {
commands: nonEmpty(this.commands),
files: nonEmpty(this.files),
packages: Object.keys(this.packages).length > 0 ? this.packages : undefined,
sources: nonEmpty(this.sources),
};
}
}
/**
* Base class for all CloudFormation Init elements
*/
export abstract class InitElement {
/**
* Returns the init element type for this element.
*/
public abstract readonly elementType: string;
/**
* Called when the Init config is being consumed. Renders the CloudFormation
* representation of this init element, and calculates any authentication
* properties needed, if any.
*
* @param options bind options for the element.
* @internal
*/
public abstract _bind(options: InitBindOptions): InitElementConfig;
}
/**
* Options for InitCommand
*/
export interface InitCommandOptions {
/**
* Identifier key for this command
*
* Commands are executed in lexicographical order of their key names.
*
* @default - Automatically generated based on index
*/
readonly key?: string;
/**
* Sets environment variables for the command.
*
* This property overwrites, rather than appends, the existing environment.
*
* @default - Use current environment
*/
readonly env?: Record<string, string>;
/**
* The working directory
*
* @default - Use default working directory
*/
readonly cwd?: string;
/**
* Command to determine whether this command should be run
*
* If the test passes (exits with error code of 0), the command is run.
*
* @default - Always run the command
*/
readonly testCmd?: string;
/**
* Continue running if this command fails
*
* @default false
*/
readonly ignoreErrors?: boolean;
/**
* The duration to wait after a command has finished in case the command causes a reboot.
*
* Set this value to `InitCommandWaitDuration.none()` if you do not want to wait for every command;
* `InitCommandWaitDuration.forever()` directs cfn-init to exit and resume only after the reboot is complete.
*
* For Windows systems only.
*
* @default - 60 seconds
*/
readonly waitAfterCompletion?: InitCommandWaitDuration;
/**
* Restart the given service(s) after this command has run
*
* @default - Do not restart any service
*/
readonly serviceRestartHandles?: InitServiceRestartHandle[];
}
/**
* Represents a duration to wait after a command has finished, in case of a reboot (Windows only).
*/
export abstract class InitCommandWaitDuration {
/** Wait for a specified duration after a command. */
public static of(duration: Duration): InitCommandWaitDuration {
return new class extends InitCommandWaitDuration {
/** @internal */
public _render() { return duration.toSeconds(); }
}();
}
/** Do not wait for this command. */
public static none(): InitCommandWaitDuration {
return InitCommandWaitDuration.of(Duration.seconds(0));
}
/** cfn-init will exit and resume only after a reboot. */
public static forever(): InitCommandWaitDuration {
return new class extends InitCommandWaitDuration {
/** @internal */
public _render() { return 'forever'; }
}();
}
/**
* Render to a CloudFormation value.
* @internal
*/
public abstract _render(): any;
}
/**
* Command to execute on the instance
*/
export class InitCommand extends InitElement {
/**
* Run a shell command
*
* Remember that some characters like `&`, `|`, `;`, `>` etc. have special meaning in a shell and
* need to be preceded by a `\` if you want to treat them as part of a filename.
*/
public static shellCommand(shellCommand: string, options: InitCommandOptions = {}): InitCommand {
return new InitCommand(shellCommand, options);
}
/**
* Run a command from an argv array
*
* You do not need to escape space characters or enclose command parameters in quotes.
*/
public static argvCommand(argv: string[], options: InitCommandOptions = {}): InitCommand {
if (argv.length === 0) {
throw new UnscopedValidationError('Cannot define argvCommand with an empty arguments');
}
return new InitCommand(argv, options);
}
public readonly elementType = InitElementType.COMMAND.toString();
private constructor(private readonly command: string[] | string, private readonly options: InitCommandOptions) {
super();
}
/** @internal */
public _bind(options: InitBindOptions): InitElementConfig {
const commandKey = this.options.key || `${options.index}`.padStart(3, '0'); // 001, 005, etc.
if (options.platform !== InitPlatform.WINDOWS && this.options.waitAfterCompletion !== undefined) {
throw new ValidationError(`Command '${this.command}': 'waitAfterCompletion' is only valid for Windows systems.`, options.scope);
}
for (const handle of this.options.serviceRestartHandles ?? []) {
handle._addCommand(commandKey);
}
return {
config: {
[commandKey]: {
command: this.command,
env: this.options.env,
cwd: this.options.cwd,
test: this.options.testCmd,
ignoreErrors: this.options.ignoreErrors,
waitAfterCompletion: this.options.waitAfterCompletion?._render(),
},
},
};
}
}
/**
* Options for InitFile
*/
export interface InitFileOptions {
/**
* The name of the owning group for this file.
*
* Not supported for Windows systems.
*
* @default 'root'
*/
readonly group?: string;
/**
* The name of the owning user for this file.
*
* Not supported for Windows systems.
*
* @default 'root'
*/
readonly owner?: string;
/**
* A six-digit octal value representing the mode for this file.
*
* Use the first three digits for symlinks and the last three digits for
* setting permissions. To create a symlink, specify 120xxx, where xxx
* defines the permissions of the target file. To specify permissions for a
* file, use the last three digits, such as 000644.
*
* Not supported for Windows systems.
*
* @default '000644'
*/
readonly mode?: string;
/**
* True if the inlined content (from a string or file) should be treated as base64 encoded.
* Only applicable for inlined string and file content.
*
* @default false
*/
readonly base64Encoded?: boolean;
/**
* Restart the given service after this file has been written
*
* @default - Do not restart any service
*/
readonly serviceRestartHandles?: InitServiceRestartHandle[];
}
/**
* Additional options for creating an InitFile from an asset.
*/
export interface InitFileAssetOptions extends InitFileOptions, s3_assets.AssetOptions {
}
/**
* Create files on the EC2 instance.
*/
export abstract class InitFile extends InitElement {
/**
* Use a literal string as the file content
*/
public static fromString(fileName: string, content: string, options: InitFileOptions = {}): InitFile {
if (!content) {
throw new UnscopedValidationError(`InitFile ${fileName}: cannot create empty file. Please supply at least one character of content.`);
}
return new class extends InitFile {
protected _doBind(bindOptions: InitBindOptions) {
return {
config: this._standardConfig(options, bindOptions.platform, {
content,
encoding: this.options.base64Encoded ? 'base64' : 'plain',
}),
};
}
}(fileName, options);
}
/**
* Write a symlink with the given symlink target
*/
public static symlink(fileName: string, target: string, options: InitFileOptions = {}): InitFile {
const { mode, ...otherOptions } = options;
if (mode && mode.slice(0, 3) !== '120') {
throw new UnscopedValidationError('File mode for symlinks must begin with 120XXX');
}
return InitFile.fromString(fileName, target, { mode: (mode || '120644'), ...otherOptions });
}
/**
* Use a JSON-compatible object as the file content, write it to a JSON file.
*
* May contain tokens.
*/
public static fromObject(fileName: string, obj: Record<string, any>, options: InitFileOptions = {}): InitFile {
return new class extends InitFile {
protected _doBind(bindOptions: InitBindOptions) {
return {
config: this._standardConfig(options, bindOptions.platform, {
content: obj,
}),
};
}
}(fileName, options);
}
/**
* Read a file from disk and use its contents
*
* The file will be embedded in the template, so care should be taken to not
* exceed the template size.
*
* If options.base64encoded is set to true, this will base64-encode the file's contents.
*/
public static fromFileInline(targetFileName: string, sourceFileName: string, options: InitFileOptions = {}): InitFile {
const encoding = options.base64Encoded ? 'base64' : 'utf8';
const fileContents = fs.readFileSync(sourceFileName).toString(encoding);
return InitFile.fromString(targetFileName, fileContents, options);
}
/**
* Download from a URL at instance startup time
*/
public static fromUrl(fileName: string, url: string, options: InitFileOptions = {}): InitFile {
return new class extends InitFile {
protected _doBind(bindOptions: InitBindOptions) {
return {
config: this._standardConfig(options, bindOptions.platform, {
source: url,
}),
};
}
}(fileName, options);
}
/**
* Download a file from an S3 bucket at instance startup time
*/
public static fromS3Object(fileName: string, bucket: s3.IBucket, key: string, options: InitFileOptions = {}): InitFile {
return new class extends InitFile {
protected _doBind(bindOptions: InitBindOptions) {
bucket.grantRead(bindOptions.instanceRole, key);
return {
config: this._standardConfig(options, bindOptions.platform, {
source: bucket.urlForObject(key),
}),
authentication: standardS3Auth(bindOptions.instanceRole, bucket.bucketName),
};
}
}(fileName, options);
}
/**
* Create an asset from the given file
*
* This is appropriate for files that are too large to embed into the template.
*/
public static fromAsset(targetFileName: string, path: string, options: InitFileAssetOptions = {}): InitFile {
return new class extends InitFile {
protected _doBind(bindOptions: InitBindOptions) {
// md5 hash uses bindOptions.scope.node.children to get a unique value for each InitFile
// using bindOptions.scope.node.id would cause naming collisions if multiple InitFiles were created in the same stack, as the id would be the same stack id
const asset = new s3_assets.Asset(bindOptions.scope, `${md5hash(bindOptions.scope.node.children.toString())}${targetFileName}Asset`, {
path,
...options,
});
asset.grantRead(bindOptions.instanceRole);
return {
config: this._standardConfig(options, bindOptions.platform, {
source: asset.httpUrl,
}),
authentication: standardS3Auth(bindOptions.instanceRole, asset.s3BucketName),
assetHash: asset.assetHash,
};
}
}(targetFileName, options);
}
/**
* Use a file from an asset at instance startup time
*/
public static fromExistingAsset(targetFileName: string, asset: s3_assets.Asset, options: InitFileOptions = {}): InitFile {
return new class extends InitFile {
protected _doBind(bindOptions: InitBindOptions) {
asset.grantRead(bindOptions.instanceRole);
return {
config: this._standardConfig(options, bindOptions.platform, {
source: asset.httpUrl,
}),
authentication: standardS3Auth(bindOptions.instanceRole, asset.s3BucketName),
assetHash: asset.assetHash,
};
}
}(targetFileName, options);
}
public readonly elementType = InitElementType.FILE.toString();
protected constructor(private readonly fileName: string, private readonly options: InitFileOptions) {
super();
}
/** @internal */
public _bind(bindOptions: InitBindOptions): InitElementConfig {
for (const handle of this.options.serviceRestartHandles ?? []) {
handle._addFile(this.fileName);
}
return this._doBind(bindOptions);
}
/**
* Perform the actual bind and render
*
* This is in a second method so the superclass can guarantee that
* the common work of registering into serviceHandles cannot be forgotten.
* @internal
*/
protected abstract _doBind(options: InitBindOptions): InitElementConfig;
/**
* Render the standard config block, given content vars
* @internal
*/
protected _standardConfig(fileOptions: InitFileOptions, platform: InitPlatform, contentVars: Record<string, any>): Record<string, any> {
if (platform === InitPlatform.WINDOWS) {
if (fileOptions.group || fileOptions.owner || fileOptions.mode) {
throw new UnscopedValidationError('Owner, group, and mode options not supported for Windows.');
}
return {
[this.fileName]: { ...contentVars },
};
}
return {
[this.fileName]: {
...contentVars,
mode: fileOptions.mode || '000644',
owner: fileOptions.owner || 'root',
group: fileOptions.group || 'root',
},
};
}
}
/**
* Create Linux/UNIX groups and assign group IDs.
*
* Not supported for Windows systems.
*/
export class InitGroup extends InitElement {
/**
* Create a group from its name, and optionally, group id
*/
public static fromName(groupName: string, groupId?: number): InitGroup {
return new InitGroup(groupName, groupId);
}
public readonly elementType = InitElementType.GROUP.toString();
protected constructor(private groupName: string, private groupId?: number) {
super();
}
/** @internal */
public _bind(options: InitBindOptions): InitElementConfig {
if (options.platform === InitPlatform.WINDOWS) {
throw new UnscopedValidationError('Init groups are not supported on Windows');
}
return {
config: {
[this.groupName]: this.groupId !== undefined ? { gid: this.groupId } : {},
},
};
}
}
/**
* Optional parameters used when creating a user
*/
export interface InitUserOptions {
/**
* The user's home directory.
*
* @default assigned by the OS
*/
readonly homeDir?: string;
/**
* A user ID. The creation process fails if the user name exists with a different user ID.
* If the user ID is already assigned to an existing user the operating system may
* reject the creation request.
*
* @default assigned by the OS
*/
readonly userId?: number;
/**
* A list of group names. The user will be added to each group in the list.
*
* @default the user is not associated with any groups.
*/
readonly groups?: string[];
}
/**
* Create Linux/UNIX users and to assign user IDs.
*
* Users are created as non-interactive system users with a shell of
* /sbin/nologin. This is by design and cannot be modified.
*
* Not supported for Windows systems.
*/
export class InitUser extends InitElement {
/**
* Create a user from user name.
*/
public static fromName(userName: string, options: InitUserOptions = {}): InitUser {
return new InitUser(userName, options);
}
public readonly elementType = InitElementType.USER.toString();
protected constructor(private readonly userName: string, private readonly userOptions: InitUserOptions) {
super();
}
/** @internal */
public _bind(options: InitBindOptions): InitElementConfig {
if (options.platform === InitPlatform.WINDOWS) {
throw new UnscopedValidationError('Init users are not supported on Windows');
}
return {
config: {
[this.userName]: {
uid: this.userOptions.userId,
groups: this.userOptions.groups,
homeDir: this.userOptions.homeDir,
},
},
};
}
}
/**
* Options for InitPackage.rpm/InitPackage.msi
*/
export interface LocationPackageOptions {
/**
* Identifier key for this package
*
* You can use this to order package installs.
*
* @default - Automatically generated
*/
readonly key?: string;
/**
* Restart the given service after this command has run
*
* @default - Do not restart any service
*/
readonly serviceRestartHandles?: InitServiceRestartHandle[];
}
/**
* Options for InitPackage.yum/apt/rubyGem/python
*/
export interface NamedPackageOptions {
/**
* Specify the versions to install
*
* @default - Install the latest version
*/
readonly version?: string[];
/**
* Restart the given services after this command has run
*
* @default - Do not restart any service
*/
readonly serviceRestartHandles?: InitServiceRestartHandle[];
}
/**
* A package to be installed during cfn-init time
*/
export class InitPackage extends InitElement {
/**
* Install an RPM from an HTTP URL or a location on disk
*/
public static rpm(this: void, location: string, options: LocationPackageOptions = {}): InitPackage {
return new InitPackage('rpm', [location], options.key, options.serviceRestartHandles);
}
/**
* Install a package using Yum
*/
public static yum(this: void, packageName: string, options: NamedPackageOptions = {}): InitPackage {
return new InitPackage('yum', options.version ?? [], packageName, options.serviceRestartHandles);
}
/**
* Install a package from RubyGems
*/
public static rubyGem(this: void, gemName: string, options: NamedPackageOptions = {}): InitPackage {
return new InitPackage('rubygems', options.version ?? [], gemName, options.serviceRestartHandles);
}
/**
* Install a package from PyPI
*/
public static python(this: void, packageName: string, options: NamedPackageOptions = {}): InitPackage {
return new InitPackage('python', options.version ?? [], packageName, options.serviceRestartHandles);
}
/**
* Install a package using APT
*/
public static apt(this: void, packageName: string, options: NamedPackageOptions = {}): InitPackage {
return new InitPackage('apt', options.version ?? [], packageName, options.serviceRestartHandles);
}
/**
* Install an MSI package from an HTTP URL or a location on disk
*/
public static msi(this: void, location: string, options: LocationPackageOptions = {}): InitPackage {
// The MSI package version must be a string, not an array.
return new class extends InitPackage {
protected renderPackageVersions() { return location; }
}('msi', [location], options.key, options.serviceRestartHandles);
}
public readonly elementType = InitElementType.PACKAGE.toString();
protected constructor(
private readonly type: string,
private readonly versions: string[],
private readonly packageName?: string,
private readonly serviceHandles?: InitServiceRestartHandle[],
) {
super();
}
/** @internal */
public _bind(options: InitBindOptions): InitElementConfig {
if ((this.type === 'msi') !== (options.platform === InitPlatform.WINDOWS)) {
if (this.type === 'msi') {
throw new UnscopedValidationError('MSI installers are only supported on Windows systems.');
} else {
throw new UnscopedValidationError('Windows only supports the MSI package type');
}
}
if (!this.packageName && !['rpm', 'msi'].includes(this.type)) {
throw new UnscopedValidationError('Package name must be specified for all package types besides RPM and MSI.');
}
const packageName = this.packageName || `${options.index}`.padStart(3, '0');
for (const handle of this.serviceHandles ?? []) {
handle._addPackage(this.type, packageName);
}
return {
config: {
[this.type]: {
[packageName]: this.renderPackageVersions(),
},
},
};
}
protected renderPackageVersions(): any {
return this.versions;
}
}
/**
* Options for an InitService
*/
export interface InitServiceOptions {
/**
* Enable or disable this service
*
* Set to true to ensure that the service will be started automatically upon boot.
*
* Set to false to ensure that the service will not be started automatically upon boot.
*
* @default - true if used in `InitService.enable()`, no change to service
* state if used in `InitService.fromOptions()`.
*/
readonly enabled?: boolean;
/**
* Make sure this service is running or not running after cfn-init finishes.
*
* Set to true to ensure that the service is running after cfn-init finishes.
*
* Set to false to ensure that the service is not running after cfn-init finishes.
*
* @default - same value as `enabled`.
*/
readonly ensureRunning?: boolean;
/**
* Restart service when the actions registered into the restartHandle have been performed
*
* Register actions into the restartHandle by passing it to `InitFile`, `InitCommand`,
* `InitPackage` and `InitSource` objects.
*
* @default - No files trigger restart
*/
readonly serviceRestartHandle?: InitServiceRestartHandle;
/**
* What service manager to use
*
* This needs to match the actual service manager on your Operating System.
* For example, Amazon Linux 1 uses SysVinit, but Amazon Linux 2 uses Systemd.
*
* @default ServiceManager.SYSVINIT for Linux images, ServiceManager.WINDOWS for Windows images
*/
readonly serviceManager?: ServiceManager;
}
/**
* A services that be enabled, disabled or restarted when the instance is launched.
*/
export class InitService extends InitElement {
/**
* Enable and start the given service, optionally restarting it
*/
public static enable(serviceName: string, options: InitServiceOptions = {}): InitService {
const { enabled, ensureRunning, ...otherOptions } = options;
return new InitService(serviceName, {
enabled: enabled ?? true,
ensureRunning: ensureRunning ?? enabled ?? true,
...otherOptions,
});
}
/**
* Disable and stop the given service
*/
public static disable(serviceName: string): InitService {
return new InitService(serviceName, { enabled: false, ensureRunning: false });
}
/**
* Install a systemd-compatible config file for the given service
*
* This is a helper function to create a simple systemd configuration
* file that will allow running a service on the machine using `InitService.enable()`.
*
* Systemd allows many configuration options; this function does not pretend
* to expose all of them. If you need advanced configuration options, you
* can use `InitFile` to create exactly the configuration file you need
* at `/etc/systemd/system/${serviceName}.service`.
*/
public static systemdConfigFile(serviceName: string, options: SystemdConfigFileOptions): InitFile {
if (!options.command.startsWith('/')) {
throw new UnscopedValidationError(`SystemD executables must use an absolute path, got '${options.command}'`);
}
if (options.environmentFiles?.some(file => !file.startsWith('/'))) {
throw new UnscopedValidationError('SystemD environment files must use absolute paths');
}
const lines = [
'[Unit]',
...(options.description ? [`Description=${options.description}`] : []),
...(options.afterNetwork ?? true ? ['After=network.target'] : []),
'[Service]',
`ExecStart=${options.command}`,
...(options.cwd ? [`WorkingDirectory=${options.cwd}`] : []),
...(options.user ? [`User=${options.user}`] : []),
...(options.group ? [`Group=${options.user}`] : []),
...(options.keepRunning ?? true ? ['Restart=always'] : []),
...(
options.environmentFiles
? options.environmentFiles.map(file => `EnvironmentFile=${file}`)
: []
),
...(
options.environmentVariables
? Object.entries(options.environmentVariables)
.map(([key, value]) => `Environment="${key}=${value}"`)
: []
),
'[Install]',
'WantedBy=multi-user.target',
];
return InitFile.fromString(`/etc/systemd/system/${serviceName}.service`, lines.join('\n'));
}
public readonly elementType = InitElementType.SERVICE.toString();
private constructor(private readonly serviceName: string, private readonly serviceOptions: InitServiceOptions) {
super();
}
/** @internal */
public _bind(options: InitBindOptions): InitElementConfig {
const serviceManager = this.serviceOptions.serviceManager
?? (options.platform === InitPlatform.LINUX ? ServiceManager.SYSVINIT : ServiceManager.WINDOWS);
return {
config: {
[serviceManagerToString(serviceManager)]: {
[this.serviceName]: {
enabled: this.serviceOptions.enabled,
ensureRunning: this.serviceOptions.ensureRunning,
...this.serviceOptions.serviceRestartHandle?._renderRestartHandles(),
},
},
},
};
}
}
/**
* Additional options for an InitSource
*/
export interface InitSourceOptions {
/**
* Restart the given services after this archive has been extracted
*
* @default - Do not restart any service
*/
readonly serviceRestartHandles?: InitServiceRestartHandle[];
}
/**
* Additional options for an InitSource that builds an asset from local files.
*/
export interface InitSourceAssetOptions extends InitSourceOptions, s3_assets.AssetOptions {
}
/**
* Extract an archive into a directory
*/
export abstract class InitSource extends InitElement {
/**
* Retrieve a URL and extract it into the given directory
*/
public static fromUrl(targetDirectory: string, url: string, options: InitSourceOptions = {}): InitSource {
return new class extends InitSource {
protected _doBind() {
return {
config: { [this.targetDirectory]: url },
};
}
}(targetDirectory, options.serviceRestartHandles);
}
/**
* Extract a GitHub branch into a given directory
*/
public static fromGitHub(targetDirectory: string, owner: string, repo: string, refSpec?: string, options: InitSourceOptions = {}): InitSource {
return InitSource.fromUrl(targetDirectory, `https://github.com/${owner}/${repo}/tarball/${refSpec ?? 'master'}`, options);
}
/**
* Extract an archive stored in an S3 bucket into the given directory
*/
public static fromS3Object(targetDirectory: string, bucket: s3.IBucket, key: string, options: InitSourceOptions = {}): InitSource {
return new class extends InitSource {
protected _doBind(bindOptions: InitBindOptions) {
bucket.grantRead(bindOptions.instanceRole, key);
return {
config: { [this.targetDirectory]: bucket.urlForObject(key) },
authentication: standardS3Auth(bindOptions.instanceRole, bucket.bucketName),
};
}
}(targetDirectory, options.serviceRestartHandles);
}
/**
* Create an InitSource from an asset created from the given path.
*/
public static fromAsset(targetDirectory: string, path: string, options: InitSourceAssetOptions = {}): InitSource {
return new class extends InitSource {
protected _doBind(bindOptions: InitBindOptions) {
// md5 hash uses bindOptions.scope.node.children to get a unique value for each InitFile
// using bindOptions.scope.node.id would cause naming collisions if multiple InitFiles were created in the same stack, as the id would be the same stack id
const asset = new s3_assets.Asset(bindOptions.scope, `${md5hash(bindOptions.scope.node.children.toString())}${targetDirectory}Asset`, {
path,
...bindOptions,
});
asset.grantRead(bindOptions.instanceRole);
return {
config: { [this.targetDirectory]: asset.httpUrl },
authentication: standardS3Auth(bindOptions.instanceRole, asset.s3BucketName),
assetHash: asset.assetHash,
};
}
}(targetDirectory, options.serviceRestartHandles);
}
/**
* Extract a directory from an existing directory asset.
*/
public static fromExistingAsset(targetDirectory: string, asset: s3_assets.Asset, options: InitSourceOptions = {}): InitSource {
return new class extends InitSource {
protected _doBind(bindOptions: InitBindOptions) {
asset.grantRead(bindOptions.instanceRole);
return {
config: { [this.targetDirectory]: asset.httpUrl },
authentication: standardS3Auth(bindOptions.instanceRole, asset.s3BucketName),
assetHash: asset.assetHash,
};
}
}(targetDirectory, options.serviceRestartHandles);
}
public readonly elementType = InitElementType.SOURCE.toString();
protected constructor(private readonly targetDirectory: string, private readonly serviceHandles?: InitServiceRestartHandle[]) {
super();
}
/** @internal */
public _bind(options: InitBindOptions): InitElementConfig {
for (const handle of this.serviceHandles ?? []) {
handle._addSource(this.targetDirectory);
}
// Delegate actual bind to subclasses
return this._doBind(options);
}
/**
* Perform the actual bind and render
*
* This is in a second method so the superclass can guarantee that
* the common work of registering into serviceHandles cannot be forgotten.
* @internal
*/
protected abstract _doBind(options: InitBindOptions): InitElementConfig;
}
/**
* Render a standard S3 auth block for use in AWS::CloudFormation::Authentication
*
* This block is the same every time (modulo bucket name), so it has the same
* key every time so the blocks are merged into one in the final render.
*/
function standardS3Auth(role: iam.IRole, bucketName: string) {
return {
S3AccessCreds: {
type: 'S3',
roleName: role.roleName,
buckets: [bucketName],
},
};
}
/**
* The service manager that will be used by InitServices
*
* The value needs to match the service manager used by your operating
* system.
*/
export enum ServiceManager {
/**
* Use SysVinit
*
* This is the default for Linux systems.
*/
SYSVINIT,
/**
* Use Windows
*
* This is the default for Windows systems.
*/
WINDOWS,
/**
* Use systemd
*/
SYSTEMD,
}
function serviceManagerToString(x: ServiceManager): string {
switch (x) {
case ServiceManager.SYSTEMD: return 'systemd';
case ServiceManager.SYSVINIT: return 'sysvinit';
case ServiceManager.WINDOWS: return 'windows';
}
}
/**
* Options for creating a SystemD configuration file
*/
export interface SystemdConfigFileOptions {
/**
* The command to run to start this service
*/
readonly command: string;
/**
* The working directory for the command
*
* @default Root directory or home directory of specified user
*/
readonly cwd?: string;
/**
* A description of this service
*
* @default - No description
*/
readonly description?: string;
/**
* The user to execute the process under
*
* @default root
*/
readonly user?: string;
/**
* The group to execute the process under
*
* @default root
*/
readonly group?: string;
/**
* Keep the process running all the time
*
* Restarts the process when it exits for any reason other
* than the machine shutting down.
*
* @default true
*/
readonly keepRunning?: boolean;
/**
* Start the service after the networking part of the OS comes up
*
* @default true
*/
readonly afterNetwork?: boolean;
/**
* Environment variables to load when the process is running.
*
* @default - No environment variables set
*/
readonly environmentVariables?: Record<string, string>;
/**
* Loads environment variables from files when the process is running.
* Must use absolute paths.
*
* @default - No environment files
*/
readonly environmentFiles?: string[];
}