integ/lib/testing-tier.ts (184 lines of code) (raw):
/**
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
import * as path from 'path';
import { CfnOutput, Duration, Stack, StackProps } from 'aws-cdk-lib';
import {
BastionHostLinux,
IMachineImage,
InstanceType,
MachineImage,
Port,
Vpc,
} from 'aws-cdk-lib/aws-ec2';
import { Asset } from 'aws-cdk-lib/aws-s3-assets';
import {
SessionManagerHelper,
X509CertificatePem,
} from 'aws-rfdk';
import { RenderQueue, Version } from 'aws-rfdk/deadline';
import { Construct } from 'constructs';
import { NetworkTier } from '../components/_infrastructure/lib/network-tier';
import { IRenderFarmDb } from './storage-struct';
/**
* Interface for configuring UserData to add to Bastion
*/
interface UserDataConfigProps {
/**
* Local path to the framework directory containing the testing script to be copied to the Bastion
*/
readonly testingScriptPath: string;
}
/**
* Base interface for Testing Tier
*/
export interface TestingTierProps extends StackProps {
/**
* The unique suffix given to all stacks in the testing app
*/
readonly integStackTag: string;
/**
* The machine image to use for the Bastion instance.
* Defaults to an image that's suitable for running Deadline.
*/
readonly bastionMachineImageOverride?: IMachineImage;
}
/**
* Base class for Testing Tier stacks
*/
export abstract class TestingTier extends Stack {
/**
* The Bastion instance used for communicating with the farm and executing test cases
*/
public readonly testInstance: BastionHostLinux;
/**
* The shared infrastructure VPC.
*/
protected readonly vpc: Vpc;
/**
* The version of Deadline used for installing DeadlineClient. Must be set by env variable before test execution.
*/
private deadlineVersion: string = process.env.DEADLINE_VERSION!.toString();
/**
* Full path to locally staged Deadline assets. Must be set by env variable before test execution.
*/
private stagePath: string = process.env.DEADLINE_STAGING_PATH!.toString();
constructor(scope: Construct, id: string, props: TestingTierProps) {
super(scope, id, props);
const infrastructureStackName = 'RFDKIntegInfrastructure' + props.integStackTag;
// Vpc.fromLookup acquires vpc deployed to the _infrastructure stack
this.vpc = Vpc.fromLookup(this, 'Vpc', { tags: { StackName: infrastructureStackName }}) as Vpc;
// Create an instance that can be used for testing; SSM commands are communicated to the
// host instance to run test scripts installed during setup of the instance
this.testInstance = new BastionHostLinux(this, 'Bastion', {
vpc: this.vpc,
subnetSelection: { subnetGroupName: NetworkTier.subnetConfig.testRunner.name },
instanceType: new InstanceType('t3.small'),
machineImage: props.bastionMachineImageOverride ?? this.getMachineImageForDeadlineVersion(this.deadlineVersion),
});
if (process.env.DEV_MODE?.toLowerCase() === 'true') {
SessionManagerHelper.grantPermissionsTo(this.testInstance);
}
// Output bastion id for use in tests
new CfnOutput(this, 'bastionId', {
value: this.testInstance.instanceId,
});
}
/**
* Grants the bastion permissions to read a resource's cert and creates a stack output for its secretARN.
*
* @param testSuiteId Test case to configure the cert for
* @param cert Certificate for authenticating to the database/render queue used for this test case
* @param suffix The suffix to apply to the construct ID. This should be used when this method is called
* more than once in the same stack to distinguish between to CfnOutputs for a certificate.
*/
public configureCert(testSuiteId: string, cert?: X509CertificatePem, suffix?: string) {
if(cert) {
cert.cert.grantRead(this.testInstance);
new CfnOutput(this, 'CertSecretARN' + (suffix ?? '') + testSuiteId, {
value: cert.cert.secretArn,
});
};
}
/**
* Allows the bastion to connect to the docDB/mongoDB instance and creates a stack output for the secretARN for the database
*
* @param testSuiteId Test case to configure the database for
* @param database Database object to connect to the test Bastion
*/
public configureDatabase(testSuiteId: string, database: IRenderFarmDb) {
const db = database.db;
const dbSecret = database.secret!;
this.testInstance.connections.allowTo(db, Port.tcp(27017));
dbSecret.grantRead(this.testInstance);
new CfnOutput(this, 'DatabaseSecretARN' + testSuiteId, {
value: dbSecret.secretArn,
});
}
/**
* Configures connections on the farm's render queue to allow the bastion access
*
* @param testSuiteId Test case to configure the render queue for
* @param renderQueue Render queue object to connect to the test Bastion
*/
public configureRenderQueue(testSuiteId: string, renderQueue: RenderQueue) {
const port = renderQueue.endpoint.portAsString();
// We are matching the name given to the render queue host in render-struct.ts
const host = 'renderqueue';
const suffix = '.local';
const maxLength = 64 - host.length - '.'.length - suffix.length - 1;
let address;
switch(port) {
case '8080':
address = renderQueue.endpoint.hostname;
break;
case '4433':
address = host + '.' + Stack.of(renderQueue).stackName.slice(0, maxLength) + suffix;
break;
default:
break;
}
const renderQueueEndpoint = `${address}:${port}`;
this.testInstance.connections.allowToDefaultPort(renderQueue);
this.testInstance.connections.allowTo(renderQueue, Port.tcp(22));
new CfnOutput(this, 'renderQueueEndpoint' + testSuiteId, {
value: renderQueueEndpoint,
});
}
/**
* Adds userData commands to the test instance to install DeadlineClient
*/
protected installDeadlineClient() {
const clientInstaller = new Asset(this, 'ClientInstaller', {
path: path.join(this.stagePath, 'bin', 'DeadlineClient-' + this.deadlineVersion + '-linux-x64-installer.run'),
});
clientInstaller.grantRead(this.testInstance);
const installerPath: string = this.testInstance.instance.userData.addS3DownloadCommand({
bucket: clientInstaller.bucket,
bucketKey: clientInstaller.s3ObjectKey,
});
this.testInstance.instance.userData.addCommands(
'cd ~ec2-user',
`cp ${installerPath} ./deadline-client-installer.run`,
'chmod +x *.run',
'sudo ./deadline-client-installer.run --mode unattended',
`rm -f ${installerPath}`,
'rm -f ./deadline-client-installer.run',
'export DEADLINE_PATH=/opt/Thinkbox/Deadline10/bin',
);
}
/**
* Configures assets to install on the bastion via userData
*
* @param props Options for configuring Bastion userData
*/
public configureBastionUserData(props: UserDataConfigProps) {
this.testInstance.instance.instance.cfnOptions.creationPolicy = {
...this.testInstance.instance.instance.cfnOptions.creationPolicy,
resourceSignal: {
timeout: Duration.minutes(5).toIsoString(),
count: 1,
},
};
const userDataCommands = [];
userDataCommands.push(
'set -xeou pipefail',
);
const instanceSetupScripts = new Asset(this, 'SetupScripts', {
path: path.join(__dirname, '..', 'components', 'deadline', 'common', 'scripts', 'bastion', 'setup'),
});
instanceSetupScripts.grantRead(this.testInstance);
const setupZipPath: string = this.testInstance.instance.userData.addS3DownloadCommand({
bucket: instanceSetupScripts.bucket,
bucketKey: instanceSetupScripts.s3ObjectKey,
});
userDataCommands.push(
// Unzip the utility scripts to: ~ec2-user/setupScripts/
'cd ~ec2-user',
'mkdir -p setupScripts',
'cd setupScripts',
`unzip ${setupZipPath}`,
`rm -f ${setupZipPath}`,
'chmod +x *.sh',
'./install_jq.sh',
);
const instanceUtilScripts = new Asset(this, 'UtilScripts', {
path: path.join(__dirname, '..', 'components', 'deadline', 'common', 'scripts', 'bastion', 'utils'),
});
instanceUtilScripts.grantRead(this.testInstance);
const utilZipPath: string = this.testInstance.instance.userData.addS3DownloadCommand({
bucket: instanceUtilScripts.bucket,
bucketKey: instanceUtilScripts.s3ObjectKey,
});
userDataCommands.push(
// Unzip the utility scripts to: ~ec2-user/utilScripts/
'cd ~ec2-user',
'mkdir -p utilScripts',
'cd utilScripts',
`unzip ${utilZipPath}`,
`rm -f ${utilZipPath}`,
'chmod +x *.sh',
);
const testingScripts = new Asset(this, 'TestingScripts', {
path: props.testingScriptPath,
});
testingScripts.grantRead(this.testInstance);
const testsZipPath: string = this.testInstance.instance.userData.addS3DownloadCommand({
bucket: testingScripts.bucket,
bucketKey: testingScripts.s3ObjectKey,
});
userDataCommands.push(
// Unzip the testing scripts to: ~ec2-user/testScripts/
'cd ~ec2-user',
'mkdir -p testScripts',
'cd testScripts',
`unzip ${testsZipPath}`,
`rm -f ${testsZipPath}`,
'chmod +x *.sh',
);
userDataCommands.push(
// Everything will be owned by root, by default (UserData runs as root)
'cd ~ec2-user',
'chown ec2-user.ec2-user -R *',
);
this.testInstance.instance.userData.addCommands( ...userDataCommands );
this.testInstance.instance.userData.addSignalOnExitCommand( this.testInstance.instance );
}
/**
* Return an Amazon Linux machine image that can run the specified version of Deadline.
*/
private getMachineImageForDeadlineVersion(deadlineVersion: string): IMachineImage {
const REMOVED_SUPPORT_FOR_AMAZON_LINUX_2 = new Version([10, 4, 0, 0]);
if (Version.parse(deadlineVersion).isLessThan(REMOVED_SUPPORT_FOR_AMAZON_LINUX_2)) {
return MachineImage.latestAmazonLinux2();
} else {
return MachineImage.latestAmazonLinux2023();
}
}
}