UNPKG

aws-rfdk

Version:

Package for core render farm constructs

1,084 lines 154 kB
"use strict"; /** * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ Object.defineProperty(exports, "__esModule", { value: true }); /* eslint-disable dot-notation */ const aws_cdk_lib_1 = require("aws-cdk-lib"); const assertions_1 = require("aws-cdk-lib/assertions"); const aws_autoscaling_1 = require("aws-cdk-lib/aws-autoscaling"); const aws_ec2_1 = require("aws-cdk-lib/aws-ec2"); const aws_ecs_1 = require("aws-cdk-lib/aws-ecs"); const lib_1 = require("../../core/lib"); const asset_constants_1 = require("../../core/test/asset-constants"); const tag_helpers_1 = require("../../core/test/tag-helpers"); const lib_2 = require("../lib"); const asset_constants_2 = require("./asset-constants"); const test_helper_1 = require("./test-helper"); let app; let stack; let wfstack; let vpc; let renderQueue; let rcsImage; beforeEach(() => { app = new aws_cdk_lib_1.App(); stack = new aws_cdk_lib_1.Stack(app, 'infraStack', { env: { region: 'us-east-1', }, }); vpc = new aws_ec2_1.Vpc(stack, 'VPC'); rcsImage = aws_ecs_1.ContainerImage.fromAsset(__dirname); const version = new lib_2.VersionQuery(stack, 'VersionQuery'); renderQueue = new lib_2.RenderQueue(stack, 'RQ', { vpc, images: { remoteConnectionServer: rcsImage }, repository: new lib_2.Repository(stack, 'Repository', { vpc, version, secretsManagementSettings: { enabled: false }, }), trafficEncryption: { externalTLS: { enabled: false } }, version, }); wfstack = new aws_cdk_lib_1.Stack(app, 'workerFleetStack', { env: { region: 'us-east-1', }, }); }); test('default worker fleet is created correctly', () => { // WHEN const fleet = new lib_2.WorkerInstanceFleet(wfstack, 'workerFleet', { vpc, workerMachineImage: new aws_ec2_1.GenericWindowsImage({ 'us-east-1': 'ami-any', }), renderQueue, }); // THEN assertions_1.Template.fromStack(wfstack).resourceCountIs('AWS::AutoScaling::AutoScalingGroup', 1); assertions_1.Template.fromStack(wfstack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { InstanceType: 't3.large', IamInstanceProfile: { Ref: assertions_1.Match.stringLikeRegexp('^workerFleetInstanceProfile.*'), }, ImageId: 'ami-any', SecurityGroups: [ { 'Fn::GetAtt': [ assertions_1.Match.stringLikeRegexp('^workerFleetInstanceSecurityGroup.*'), 'GroupId', ], }, ], spotPrice: assertions_1.Match.absent(), }); assertions_1.Template.fromStack(wfstack).hasResourceProperties('AWS::EC2::SecurityGroupIngress', { IpProtocol: 'tcp', ToPort: parseInt(renderQueue.endpoint.portAsString(), 10), SourceSecurityGroupId: { 'Fn::GetAtt': [ stack.getLogicalId(fleet.fleet.connections.securityGroups[0].node.defaultChild), 'GroupId', ], }, GroupId: { 'Fn::ImportValue': 'infraStack:ExportsOutputFnGetAttRQLBSecurityGroupAC643AEDGroupId8F9F7830', }, }); assertions_1.Template.fromStack(wfstack).hasResourceProperties('Custom::LogRetention', { RetentionInDays: 3, LogGroupName: '/renderfarm/workerFleet', }); assertions_1.Annotations.fromStack(wfstack).hasWarning(`/${fleet.node.path}`, assertions_1.Match.stringLikeRegexp('.*being created without being provided any block devices so the Source AMI\'s devices will be used. Workers can have access to sensitive data so it is recommended to either explicitly encrypt the devices on the worker fleet or to ensure the source AMI\'s Drives are encrypted.')); assertions_1.Annotations.fromStack(wfstack).hasWarning(`/${fleet.node.path}`, assertions_1.Match.stringLikeRegexp('.*being created without a health monitor attached to it. This means that the fleet will not automatically scale-in to 0 if the workers are unhealthy')); }); test('security group is added to fleet after its creation', () => { // WHEN const fleet = new lib_2.WorkerInstanceFleet(stack, 'workerFleet', { vpc, workerMachineImage: new aws_ec2_1.GenericWindowsImage({ 'us-east-1': 'ami-any', }), renderQueue, }); fleet.addSecurityGroup(aws_ec2_1.SecurityGroup.fromSecurityGroupId(stack, 'SG', 'sg-123456789', { allowAllOutbound: false, })); // THEN assertions_1.Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { SecurityGroups: [ { 'Fn::GetAtt': [ stack.getLogicalId(fleet.fleet.connections.securityGroups[0].node.defaultChild), 'GroupId', ], }, 'sg-123456789', ], }); }); test('WorkerFleet uses given security group', () => { // WHEN new lib_2.WorkerInstanceFleet(stack, 'workerFleet', { vpc, workerMachineImage: new aws_ec2_1.GenericWindowsImage({ 'us-east-1': 'ami-any', }), renderQueue, securityGroup: aws_ec2_1.SecurityGroup.fromSecurityGroupId(stack, 'SG', 'sg-123456789', { allowAllOutbound: false, }), }); // THEN assertions_1.Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { SecurityGroups: [ 'sg-123456789', ], }); }); describe('allowing log listener port', () => { test('from CIDR', () => { // WHEN const fleet = new lib_2.WorkerInstanceFleet(stack, 'workerFleet', { vpc, workerMachineImage: new aws_ec2_1.GenericWindowsImage({ 'us-east-1': 'ami-any', }), renderQueue, }); fleet.allowListenerPortFrom(aws_ec2_1.Peer.ipv4('127.0.0.1/24').connections); // THEN assertions_1.Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { SecurityGroupEgress: [{ CidrIp: '0.0.0.0/0' }], SecurityGroupIngress: [ { CidrIp: '127.0.0.1/24', Description: 'Worker remote command listening port', FromPort: lib_2.WorkerInstanceConfiguration['DEFAULT_LISTENER_PORT'], IpProtocol: 'tcp', ToPort: lib_2.WorkerInstanceConfiguration['DEFAULT_LISTENER_PORT'] + lib_2.WorkerInstanceFleet['MAX_WORKERS_PER_HOST'], }, ], }); }); test('to CIDR', () => { // WHEN const fleet = new lib_2.WorkerInstanceFleet(stack, 'workerFleet', { vpc, workerMachineImage: new aws_ec2_1.GenericWindowsImage({ 'us-east-1': 'ami-any', }), renderQueue, }); fleet.allowListenerPortTo(aws_ec2_1.Peer.ipv4('127.0.0.1/24').connections); // THEN assertions_1.Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { SecurityGroupEgress: [{ CidrIp: '0.0.0.0/0' }], SecurityGroupIngress: [ { CidrIp: '127.0.0.1/24', Description: 'Worker remote command listening port', FromPort: lib_2.WorkerInstanceConfiguration['DEFAULT_LISTENER_PORT'], IpProtocol: 'tcp', ToPort: lib_2.WorkerInstanceConfiguration['DEFAULT_LISTENER_PORT'] + lib_2.WorkerInstanceFleet['MAX_WORKERS_PER_HOST'], }, ], }); }); test('from SecurityGroup', () => { // WHEN const fleet = new lib_2.WorkerInstanceFleet(stack, 'workerFleet', { vpc, workerMachineImage: new aws_ec2_1.GenericWindowsImage({ 'us-east-1': 'ami-any', }), renderQueue, }); const securityGroup = aws_ec2_1.SecurityGroup.fromSecurityGroupId(stack, 'SG', 'sg-123456789'); fleet.allowListenerPortFrom(securityGroup); // THEN assertions_1.Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupIngress', { FromPort: lib_2.WorkerInstanceConfiguration['DEFAULT_LISTENER_PORT'], IpProtocol: 'tcp', SourceSecurityGroupId: 'sg-123456789', ToPort: lib_2.WorkerInstanceConfiguration['DEFAULT_LISTENER_PORT'] + lib_2.WorkerInstanceFleet['MAX_WORKERS_PER_HOST'], }); }); test('to SecurityGroup', () => { // WHEN const fleet = new lib_2.WorkerInstanceFleet(stack, 'workerFleet', { vpc, workerMachineImage: new aws_ec2_1.GenericWindowsImage({ 'us-east-1': 'ami-any', }), renderQueue, }); const securityGroup = aws_ec2_1.SecurityGroup.fromSecurityGroupId(stack, 'SG', 'sg-123456789'); fleet.allowListenerPortTo(securityGroup); // THEN assertions_1.Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupIngress', { FromPort: lib_2.WorkerInstanceConfiguration['DEFAULT_LISTENER_PORT'], IpProtocol: 'tcp', SourceSecurityGroupId: 'sg-123456789', ToPort: lib_2.WorkerInstanceConfiguration['DEFAULT_LISTENER_PORT'] + lib_2.WorkerInstanceFleet['MAX_WORKERS_PER_HOST'], }); }); test('from other stack', () => { const otherStack = new aws_cdk_lib_1.Stack(app, 'otherStack', { env: { region: 'us-east-1' }, }); // WHEN const fleet = new lib_2.WorkerInstanceFleet(stack, 'workerFleet', { vpc, workerMachineImage: new aws_ec2_1.GenericWindowsImage({ 'us-east-1': 'ami-any', }), renderQueue, }); const securityGroup = aws_ec2_1.SecurityGroup.fromSecurityGroupId(otherStack, 'SG', 'sg-123456789'); fleet.allowListenerPortFrom(securityGroup); // THEN assertions_1.Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupIngress', { FromPort: lib_2.WorkerInstanceConfiguration['DEFAULT_LISTENER_PORT'], IpProtocol: 'tcp', SourceSecurityGroupId: 'sg-123456789', ToPort: lib_2.WorkerInstanceConfiguration['DEFAULT_LISTENER_PORT'] + lib_2.WorkerInstanceFleet['MAX_WORKERS_PER_HOST'], }); }); test('to other stack', () => { const otherStack = new aws_cdk_lib_1.Stack(app, 'otherStack', { env: { region: 'us-east-1' }, }); // WHEN const fleet = new lib_2.WorkerInstanceFleet(stack, 'workerFleet', { vpc, workerMachineImage: new aws_ec2_1.GenericWindowsImage({ 'us-east-1': 'ami-any', }), renderQueue, }); const securityGroup = aws_ec2_1.SecurityGroup.fromSecurityGroupId(otherStack, 'SG', 'sg-123456789'); fleet.allowListenerPortTo(securityGroup); // THEN assertions_1.Template.fromStack(otherStack).hasResourceProperties('AWS::EC2::SecurityGroupIngress', { FromPort: lib_2.WorkerInstanceConfiguration['DEFAULT_LISTENER_PORT'], IpProtocol: 'tcp', SourceSecurityGroupId: 'sg-123456789', ToPort: lib_2.WorkerInstanceConfiguration['DEFAULT_LISTENER_PORT'] + lib_2.WorkerInstanceFleet['MAX_WORKERS_PER_HOST'], }); }); }); test('default worker fleet is created correctly with linux image', () => { // WHEN new lib_2.WorkerInstanceFleet(stack, 'workerFleet', { vpc, workerMachineImage: new aws_ec2_1.GenericLinuxImage({ 'us-east-1': 'ami-any', }), renderQueue, }); // THEN // 3 = repository + renderqueue + worker fleet assertions_1.Template.fromStack(stack).resourceCountIs('AWS::AutoScaling::AutoScalingGroup', 3); assertions_1.Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { InstanceType: 't3.large', IamInstanceProfile: { Ref: assertions_1.Match.stringLikeRegexp('^workerFleetInstanceProfile.*'), }, ImageId: 'ami-any', SecurityGroups: [ { 'Fn::GetAtt': [ assertions_1.Match.stringLikeRegexp('^workerFleetInstanceSecurityGroup.*'), 'GroupId', ], }, ], spotPrice: assertions_1.Match.absent(), }); }); test('default worker fleet is created correctly with spot config', () => { // WHEN new lib_2.WorkerInstanceFleet(wfstack, 'workerFleet', { vpc, workerMachineImage: new aws_ec2_1.GenericLinuxImage({ 'us-east-1': '123', }), renderQueue, spotPrice: 2.5, }); // THEN assertions_1.Template.fromStack(wfstack).resourceCountIs('AWS::AutoScaling::AutoScalingGroup', 1); assertions_1.Template.fromStack(wfstack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { SpotPrice: '2.5', }); }); test('default worker fleet is not created with incorrect spot config', () => { // WHEN expect(() => { new lib_2.WorkerInstanceFleet(wfstack, 'workerFleet', { vpc, workerMachineImage: new aws_ec2_1.GenericLinuxImage({ 'us-east-1': '123', }), renderQueue, spotPrice: lib_2.WorkerInstanceFleet.SPOT_PRICE_MAX_LIMIT + 1, }); }).toThrow(/Invalid value: 256 for property 'spotPrice'. Valid values can be any decimal between 0.001 and 255./); // WHEN expect(() => { new lib_2.WorkerInstanceFleet(wfstack, 'workerFleet2', { vpc, workerMachineImage: new aws_ec2_1.GenericLinuxImage({ 'us-east-1': '123', }), renderQueue, spotPrice: lib_2.WorkerInstanceFleet.SPOT_PRICE_MIN_LIMIT / 2, }); }).toThrow(/Invalid value: 0.0005 for property 'spotPrice'. Valid values can be any decimal between 0.001 and 255./); }); test('default worker fleet is created correctly custom Instance type', () => { // WHEN new lib_2.WorkerInstanceFleet(stack, 'workerFleet', { vpc, workerMachineImage: new aws_ec2_1.GenericLinuxImage({ 'us-east-1': '123', }), renderQueue, instanceType: aws_ec2_1.InstanceType.of(aws_ec2_1.InstanceClass.T2, aws_ec2_1.InstanceSize.MEDIUM), }); // THEN assertions_1.Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { InstanceType: 't2.medium', }); }); test.each([ 'test-prefix/', '', ])('default worker fleet is created correctly with custom LogGroup prefix %s', (testPrefix) => { // GIVEN const id = 'workerFleet'; // WHEN new lib_2.WorkerInstanceFleet(stack, id, { vpc, workerMachineImage: new aws_ec2_1.GenericLinuxImage({ 'us-east-1': '123', }), renderQueue, logGroupProps: { logGroupPrefix: testPrefix, }, }); assertions_1.Template.fromStack(stack).hasResourceProperties('Custom::LogRetention', { RetentionInDays: 3, LogGroupName: testPrefix + id, }); }); test('worker fleet uses given UserData', () => { // GIVEN const id = 'workerFleet'; const userData = aws_ec2_1.UserData.forLinux(); // WHEN const workerFleet = new lib_2.WorkerInstanceFleet(stack, id, { vpc, workerMachineImage: new aws_ec2_1.GenericLinuxImage({ 'us-east-1': '123', }), renderQueue, userData, }); // THEN expect(workerFleet.fleet.userData).toBe(userData); }); test('default linux worker fleet is created correctly custom subnet values', () => { vpc = new aws_ec2_1.Vpc(stack, 'VPC1Az', { maxAzs: 1, }); // WHEN new lib_2.WorkerInstanceFleet(stack, 'workerFleet', { vpc, workerMachineImage: new aws_ec2_1.GenericLinuxImage({ 'us-east-1': '123', }), renderQueue, instanceType: aws_ec2_1.InstanceType.of(aws_ec2_1.InstanceClass.T2, aws_ec2_1.InstanceSize.MEDIUM), vpcSubnets: { subnetType: aws_ec2_1.SubnetType.PUBLIC, }, healthCheckConfig: { port: 6161, }, }); // THEN assertions_1.Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { VPCZoneIdentifier: [{ Ref: 'VPC1AzPublicSubnet1Subnet9649CC17', }], }); assertions_1.Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { InstanceType: 't2.medium', IamInstanceProfile: { Ref: assertions_1.Match.stringLikeRegexp('workerFleetInstanceProfile.*'), }, UserData: { 'Fn::Base64': { 'Fn::Join': [ '', [ '#!/bin/bash\n' + 'function exitTrap(){\nexitCode=$?\n/opt/aws/bin/cfn-signal --stack infraStack --resource workerFleetASG25520D69 --region us-east-1 -e $exitCode || echo \'Failed to send Cloudformation Signal\'\n}\n' + 'trap exitTrap EXIT\n' + `mkdir -p $(dirname '/tmp/${asset_constants_1.CWA_ASSET_LINUX.Key}.sh')\naws s3 cp 's3://`, { 'Fn::Sub': asset_constants_1.CWA_ASSET_LINUX.Bucket.replace('${AWS::Region}', 'us-east-1'), }, `/${asset_constants_1.CWA_ASSET_LINUX.Key}.sh' '/tmp/${asset_constants_1.CWA_ASSET_LINUX.Key}.sh'\n` + `set -e\nchmod +x '/tmp/${asset_constants_1.CWA_ASSET_LINUX.Key}.sh'\n'/tmp/${asset_constants_1.CWA_ASSET_LINUX.Key}.sh' -i us-east-1 `, { Ref: assertions_1.Match.stringLikeRegexp('^workerFleetStringParameter.*'), }, `\nmkdir -p $(dirname '/tmp/${asset_constants_2.RQ_CONNECTION_ASSET.Key}.py')\naws s3 cp 's3://`, { 'Fn::Sub': asset_constants_2.RQ_CONNECTION_ASSET.Bucket.replace('${AWS::Region}', 'us-east-1'), }, `/${asset_constants_2.RQ_CONNECTION_ASSET.Key}.py' '/tmp/${asset_constants_2.RQ_CONNECTION_ASSET.Key}.py'\n` + 'if [ -f "/etc/profile.d/deadlineclient.sh" ]; then\n source "/etc/profile.d/deadlineclient.sh"\nfi\n' + `"\${DEADLINE_PATH}/deadlinecommand" -executeScriptNoGui "/tmp/${asset_constants_2.RQ_CONNECTION_ASSET.Key}.py" --render-queue "http://`, { 'Fn::GetAtt': [ 'RQLB3B7B1CBC', 'DNSName', ], }, `:8080" \nrm -f "/tmp/${asset_constants_2.RQ_CONNECTION_ASSET.Key}.py"` + `\nmkdir -p $(dirname '/tmp/${asset_constants_2.CONFIG_WORKER_PORT_ASSET_LINUX.Key}.py')\naws s3 cp 's3://`, { 'Fn::Sub': asset_constants_2.CONFIG_WORKER_PORT_ASSET_LINUX.Bucket.replace('${AWS::Region}', 'us-east-1'), }, `/${asset_constants_2.CONFIG_WORKER_PORT_ASSET_LINUX.Key}.py' '/tmp/${asset_constants_2.CONFIG_WORKER_PORT_ASSET_LINUX.Key}.py'\n` + `mkdir -p $(dirname '/tmp/${asset_constants_2.CONFIG_WORKER_ASSET_LINUX.Key}.sh')\naws s3 cp 's3://`, { 'Fn::Sub': asset_constants_2.CONFIG_WORKER_ASSET_LINUX.Bucket.replace('${AWS::Region}', 'us-east-1'), }, `/${asset_constants_2.CONFIG_WORKER_ASSET_LINUX.Key}.sh' '/tmp/${asset_constants_2.CONFIG_WORKER_ASSET_LINUX.Key}.sh'\n` + 'set -e\n' + `chmod +x '/tmp/${asset_constants_2.CONFIG_WORKER_ASSET_LINUX.Key}.sh'\n` + `'/tmp/${asset_constants_2.CONFIG_WORKER_ASSET_LINUX.Key}.sh' '' '' '' '${lib_2.Version.MINIMUM_SUPPORTED_DEADLINE_VERSION.toString()}' ${lib_2.WorkerInstanceConfiguration['DEFAULT_LISTENER_PORT']} /tmp/${asset_constants_2.CONFIG_WORKER_PORT_ASSET_LINUX.Key}.py`, ], ], }, }, }); }); test('default windows worker fleet is created correctly custom subnet values', () => { vpc = new aws_ec2_1.Vpc(stack, 'VPC1Az', { maxAzs: 1, }); // WHEN new lib_2.WorkerInstanceFleet(stack, 'workerFleet', { vpc, workerMachineImage: new aws_ec2_1.GenericWindowsImage({ 'us-east-1': '123', }), renderQueue, instanceType: aws_ec2_1.InstanceType.of(aws_ec2_1.InstanceClass.T2, aws_ec2_1.InstanceSize.MEDIUM), vpcSubnets: { subnetType: aws_ec2_1.SubnetType.PUBLIC, }, healthCheckConfig: { port: 6161, }, }); // THEN assertions_1.Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { VPCZoneIdentifier: [{ Ref: 'VPC1AzPublicSubnet1Subnet9649CC17', }], }); assertions_1.Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { InstanceType: 't2.medium', IamInstanceProfile: { Ref: assertions_1.Match.stringLikeRegexp('workerFleetInstanceProfile.*'), }, UserData: { 'Fn::Base64': { 'Fn::Join': [ '', [ '<powershell>trap {\n$success=($PSItem.Exception.Message -eq "Success")\n' + 'cfn-signal --stack infraStack --resource workerFleetASG25520D69 --region us-east-1 --success ($success.ToString().ToLower())\nbreak\n}\n' + `mkdir (Split-Path -Path 'C:/temp/${asset_constants_1.CWA_ASSET_WINDOWS.Key}.ps1' ) -ea 0\nRead-S3Object -BucketName '`, { 'Fn::Sub': asset_constants_1.CWA_ASSET_WINDOWS.Bucket.replace('${AWS::Region}', 'us-east-1'), }, `' -key '${asset_constants_1.CWA_ASSET_WINDOWS.Key}.ps1' -file 'C:/temp/${asset_constants_1.CWA_ASSET_WINDOWS.Key}.ps1' -ErrorAction Stop\n&'C:/temp/${asset_constants_1.CWA_ASSET_WINDOWS.Key}.ps1' -i us-east-1 `, { Ref: assertions_1.Match.stringLikeRegexp('^workerFleetStringParameter.*'), }, `\nif (!$?) { Write-Error 'Failed to execute the file \"C:/temp/${asset_constants_1.CWA_ASSET_WINDOWS.Key}.ps1\"' -ErrorAction Stop }\n` + `mkdir (Split-Path -Path 'C:/temp/${asset_constants_2.RQ_CONNECTION_ASSET.Key}.py' ) -ea 0\nRead-S3Object -BucketName '`, { 'Fn::Sub': asset_constants_2.RQ_CONNECTION_ASSET.Bucket.replace('${AWS::Region}', 'us-east-1'), }, `' -key '${asset_constants_2.RQ_CONNECTION_ASSET.Key}.py' -file 'C:/temp/${asset_constants_2.RQ_CONNECTION_ASSET.Key}.py' -ErrorAction Stop\n` + '$ErrorActionPreference = "Stop"\n' + '$DEADLINE_PATH = (get-item env:"DEADLINE_PATH").Value\n' + `& "$DEADLINE_PATH/deadlinecommand.exe" -executeScriptNoGui "C:/temp/${asset_constants_2.RQ_CONNECTION_ASSET.Key}.py" --render-queue "http://`, { 'Fn::GetAtt': [ 'RQLB3B7B1CBC', 'DNSName', ], }, ':8080" 2>&1\n' + `Remove-Item -Path "C:/temp/${asset_constants_2.RQ_CONNECTION_ASSET.Key}.py"\n` + `mkdir (Split-Path -Path 'C:/temp/${asset_constants_2.CONFIG_WORKER_ASSET_WINDOWS.Key}.py' ) -ea 0\nRead-S3Object -BucketName '`, { 'Fn::Sub': asset_constants_2.CONFIG_WORKER_ASSET_WINDOWS.Bucket.replace('${AWS::Region}', 'us-east-1'), }, `' -key '${asset_constants_2.CONFIG_WORKER_ASSET_WINDOWS.Key}.py' -file 'C:/temp/${asset_constants_2.CONFIG_WORKER_ASSET_WINDOWS.Key}.py' -ErrorAction Stop\n` + `mkdir (Split-Path -Path 'C:/temp/${asset_constants_2.CONFIG_WORKER_PORT_ASSET_WINDOWS.Key}.ps1' ) -ea 0\nRead-S3Object -BucketName '`, { 'Fn::Sub': asset_constants_2.CONFIG_WORKER_PORT_ASSET_WINDOWS.Bucket.replace('${AWS::Region}', 'us-east-1'), }, `' -key '${asset_constants_2.CONFIG_WORKER_PORT_ASSET_WINDOWS.Key}.ps1' -file 'C:/temp/${asset_constants_2.CONFIG_WORKER_PORT_ASSET_WINDOWS.Key}.ps1' -ErrorAction Stop\n` + `&'C:/temp/${asset_constants_2.CONFIG_WORKER_PORT_ASSET_WINDOWS.Key}.ps1' '' '' '' '${lib_2.Version.MINIMUM_SUPPORTED_DEADLINE_VERSION.toString()}' ${lib_2.WorkerInstanceConfiguration['DEFAULT_LISTENER_PORT']} C:/temp/${asset_constants_2.CONFIG_WORKER_ASSET_WINDOWS.Key}.py\n` + `if (!$?) { Write-Error 'Failed to execute the file \"C:/temp/${asset_constants_2.CONFIG_WORKER_PORT_ASSET_WINDOWS.Key}.ps1\"' -ErrorAction Stop }\n` + 'throw \"Success\"</powershell>', ], ], }, }, }); }); test('default worker fleet is created correctly with groups, pools and region', () => { vpc = new aws_ec2_1.Vpc(stack, 'VPC1Az', { maxAzs: 1, }); // WHEN new lib_2.WorkerInstanceFleet(stack, 'workerFleet', { vpc, workerMachineImage: new aws_ec2_1.GenericLinuxImage({ 'us-east-1': '123', }), renderQueue, instanceType: aws_ec2_1.InstanceType.of(aws_ec2_1.InstanceClass.T2, aws_ec2_1.InstanceSize.MEDIUM), vpcSubnets: { subnetType: aws_ec2_1.SubnetType.PUBLIC, }, groups: ['A', 'B'], // We want to make sure that these are converted to lowercase pools: ['C', 'D'], // We want to make sure that these are converted to lowercase region: 'E', }); // THEN assertions_1.Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { InstanceType: 't2.medium', IamInstanceProfile: { Ref: assertions_1.Match.stringLikeRegexp('workerFleetInstanceProfile.*'), }, UserData: { 'Fn::Base64': { 'Fn::Join': [ '', [ '#!/bin/bash\n' + 'function exitTrap(){\nexitCode=$?\n/opt/aws/bin/cfn-signal --stack infraStack --resource workerFleetASG25520D69 --region us-east-1 -e $exitCode || echo \'Failed to send Cloudformation Signal\'\n}\n' + 'trap exitTrap EXIT\n' + `mkdir -p $(dirname '/tmp/${asset_constants_1.CWA_ASSET_LINUX.Key}.sh')\naws s3 cp 's3://`, { 'Fn::Sub': asset_constants_1.CWA_ASSET_LINUX.Bucket.replace('${AWS::Region}', 'us-east-1'), }, `/${asset_constants_1.CWA_ASSET_LINUX.Key}.sh' '/tmp/${asset_constants_1.CWA_ASSET_LINUX.Key}.sh'\n` + `set -e\nchmod +x '/tmp/${asset_constants_1.CWA_ASSET_LINUX.Key}.sh'\n'/tmp/${asset_constants_1.CWA_ASSET_LINUX.Key}.sh' -i us-east-1 `, { Ref: assertions_1.Match.stringLikeRegexp('^workerFleetStringParameter.*'), }, `\nmkdir -p $(dirname '/tmp/${asset_constants_2.RQ_CONNECTION_ASSET.Key}.py')\naws s3 cp 's3://`, { 'Fn::Sub': asset_constants_2.RQ_CONNECTION_ASSET.Bucket.replace('${AWS::Region}', 'us-east-1'), }, `/${asset_constants_2.RQ_CONNECTION_ASSET.Key}.py' '/tmp/${asset_constants_2.RQ_CONNECTION_ASSET.Key}.py'\n` + 'if [ -f "/etc/profile.d/deadlineclient.sh" ]; then\n source "/etc/profile.d/deadlineclient.sh"\nfi\n' + `"\${DEADLINE_PATH}/deadlinecommand" -executeScriptNoGui "/tmp/${asset_constants_2.RQ_CONNECTION_ASSET.Key}.py" --render-queue "http://`, { 'Fn::GetAtt': [ 'RQLB3B7B1CBC', 'DNSName', ], }, `:8080" \nrm -f "/tmp/${asset_constants_2.RQ_CONNECTION_ASSET.Key}.py"` + `\nmkdir -p $(dirname '/tmp/${asset_constants_2.CONFIG_WORKER_PORT_ASSET_LINUX.Key}.py')\naws s3 cp 's3://`, { 'Fn::Sub': asset_constants_2.CONFIG_WORKER_PORT_ASSET_LINUX.Bucket.replace('${AWS::Region}', 'us-east-1'), }, `/${asset_constants_2.CONFIG_WORKER_PORT_ASSET_LINUX.Key}.py' '/tmp/${asset_constants_2.CONFIG_WORKER_PORT_ASSET_LINUX.Key}.py'\n` + `mkdir -p $(dirname '/tmp/${asset_constants_2.CONFIG_WORKER_ASSET_LINUX.Key}.sh')\naws s3 cp 's3://`, { 'Fn::Sub': asset_constants_2.CONFIG_WORKER_ASSET_LINUX.Bucket.replace('${AWS::Region}', 'us-east-1'), }, `/${asset_constants_2.CONFIG_WORKER_ASSET_LINUX.Key}.sh' '/tmp/${asset_constants_2.CONFIG_WORKER_ASSET_LINUX.Key}.sh'\n` + 'set -e\n' + `chmod +x '/tmp/${asset_constants_2.CONFIG_WORKER_ASSET_LINUX.Key}.sh'\n` + `'/tmp/${asset_constants_2.CONFIG_WORKER_ASSET_LINUX.Key}.sh' 'a,b' 'c,d' 'E' '${lib_2.Version.MINIMUM_SUPPORTED_DEADLINE_VERSION.toString()}' ${lib_2.WorkerInstanceConfiguration['DEFAULT_LISTENER_PORT']} /tmp/${asset_constants_2.CONFIG_WORKER_PORT_ASSET_LINUX.Key}.py`, ], ], }, }, }); }); test('worker fleet does validation correctly with groups, pools and region', () => { vpc = new aws_ec2_1.Vpc(stack, 'VPC1Az', { maxAzs: 1, }); // group name as 'none' expect(() => { new lib_2.WorkerInstanceFleet(stack, 'workerFleet', { vpc, workerMachineImage: new aws_ec2_1.GenericLinuxImage({ 'us-east-1': '123', }), renderQueue, groups: ['A', 'none'], }); }).toThrow(); // group name with whitespace expect(() => { new lib_2.WorkerInstanceFleet(stack, 'workerFleet1', { vpc, workerMachineImage: new aws_ec2_1.GenericLinuxImage({ 'us-east-1': '123', }), renderQueue, groups: ['A', 'no ne'], }); }).toThrow(/Invalid value: no ne for property 'groups'/); // pool name with whitespace expect(() => { new lib_2.WorkerInstanceFleet(stack, 'workerFleet2', { vpc, workerMachineImage: new aws_ec2_1.GenericLinuxImage({ 'us-east-1': '123', }), renderQueue, pools: ['A', 'none'], }); }).toThrow(/Invalid value: none for property 'pools'/); // pool name as 'none' expect(() => { new lib_2.WorkerInstanceFleet(stack, 'workerFleet3', { vpc, workerMachineImage: new aws_ec2_1.GenericLinuxImage({ 'us-east-1': '123', }), renderQueue, pools: ['A', 'none'], }); }).toThrow(/Invalid value: none for property 'pools'/); // region as 'none' expect(() => { new lib_2.WorkerInstanceFleet(stack, 'workerFleet4', { vpc, workerMachineImage: new aws_ec2_1.GenericLinuxImage({ 'us-east-1': '123', }), renderQueue, region: 'none', }); }).toThrow(/Invalid value: none for property 'region'/); // region as 'all' expect(() => { new lib_2.WorkerInstanceFleet(stack, 'workerFleet5', { vpc, workerMachineImage: new aws_ec2_1.GenericLinuxImage({ 'us-east-1': '123', }), renderQueue, region: 'all', }); }).toThrow(/Invalid value: all for property 'region'/); // region as 'unrecognized' expect(() => { new lib_2.WorkerInstanceFleet(stack, 'workerFleet6', { vpc, workerMachineImage: new aws_ec2_1.GenericLinuxImage({ 'us-east-1': '123', }), renderQueue, region: 'unrecognized', }); }).toThrow(/Invalid value: unrecognized for property 'region'/); // region with invalid characters expect(() => { new lib_2.WorkerInstanceFleet(stack, 'workerFleet7', { vpc, workerMachineImage: new aws_ec2_1.GenericLinuxImage({ 'us-east-1': '123', }), renderQueue, region: 'none@123', }); }).toThrow(/Invalid value: none@123 for property 'region'/); // region with reserved name as substring expect(() => { new lib_2.WorkerInstanceFleet(stack, 'workerFleet8', { vpc, workerMachineImage: new aws_ec2_1.GenericLinuxImage({ 'us-east-1': '123', }), renderQueue, region: 'none123', }); }).not.toThrow(); // region with case-insensitive name expect(() => { new lib_2.WorkerInstanceFleet(stack, 'workerFleet9', { vpc, workerMachineImage: new aws_ec2_1.GenericLinuxImage({ 'us-east-1': '123', }), renderQueue, region: 'None', }); }).toThrow(/Invalid value: None for property 'region'/); }); describe('Block Device Tests', () => { let healthMonitor; beforeEach(() => { // create a health monitor so it does not trigger warnings healthMonitor = new lib_1.HealthMonitor(wfstack, 'healthMonitor', { vpc, }); }); test('Warning if no BlockDevices provided', () => { const fleet = new lib_2.WorkerInstanceFleet(wfstack, 'workerFleet', { vpc, workerMachineImage: new aws_ec2_1.GenericWindowsImage({ 'us-east-1': 'ami-any', }), renderQueue, healthMonitor, }); assertions_1.Annotations.fromStack(wfstack).hasWarning(`/${fleet.node.path}`, assertions_1.Match.stringLikeRegexp('.*being created without being provided any block devices so the Source AMI\'s devices will be used. Workers can have access to sensitive data so it is recommended to either explicitly encrypt the devices on the worker fleet or to ensure the source AMI\'s Drives are encrypted.')); }); test('No Warnings if Encrypted BlockDevices Provided', () => { const VOLUME_SIZE = 50; // WHEN const fleet = new lib_2.WorkerInstanceFleet(wfstack, 'workerFleet', { vpc, workerMachineImage: new aws_ec2_1.GenericWindowsImage({ 'us-east-1': 'ami-any', }), renderQueue, healthMonitor, blockDevices: [{ deviceName: '/dev/xvda', volume: aws_autoscaling_1.BlockDeviceVolume.ebs(VOLUME_SIZE, { encrypted: true }), }], }); //THEN assertions_1.Template.fromStack(wfstack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { BlockDeviceMappings: [ { Ebs: { Encrypted: true, VolumeSize: VOLUME_SIZE, }, }, ], }); assertions_1.Annotations.fromStack(wfstack).hasNoInfo(`/${fleet.node.path}`, assertions_1.Match.anyValue()); assertions_1.Annotations.fromStack(wfstack).hasNoWarning(`/${fleet.node.path}`, assertions_1.Match.anyValue()); assertions_1.Annotations.fromStack(wfstack).hasNoError(`/${fleet.node.path}`, assertions_1.Match.anyValue()); }); test('Warnings if non-Encrypted BlockDevices Provided', () => { const VOLUME_SIZE = 50; const DEVICE_NAME = '/dev/xvda'; // WHEN const fleet = new lib_2.WorkerInstanceFleet(wfstack, 'workerFleet', { vpc, workerMachineImage: new aws_ec2_1.GenericWindowsImage({ 'us-east-1': 'ami-any', }), renderQueue, healthMonitor, blockDevices: [{ deviceName: DEVICE_NAME, volume: aws_autoscaling_1.BlockDeviceVolume.ebs(VOLUME_SIZE, { encrypted: false }), }], }); //THEN assertions_1.Template.fromStack(wfstack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { BlockDeviceMappings: [ { Ebs: { Encrypted: false, VolumeSize: VOLUME_SIZE, }, }, ], }); assertions_1.Annotations.fromStack(wfstack).hasWarning(`/${fleet.node.path}`, assertions_1.Match.stringLikeRegexp(`The BlockDevice \"${DEVICE_NAME}\" on the worker-fleet workerFleet is not encrypted. Workers can have access to sensitive data so it is recommended to encrypt the devices on the worker fleet.`)); }); test('Warnings for BlockDevices without encryption specified', () => { const VOLUME_SIZE = 50; const DEVICE_NAME = '/dev/xvda'; // WHEN const fleet = new lib_2.WorkerInstanceFleet(wfstack, 'workerFleet', { vpc, workerMachineImage: new aws_ec2_1.GenericWindowsImage({ 'us-east-1': 'ami-any', }), renderQueue, healthMonitor, blockDevices: [{ deviceName: DEVICE_NAME, volume: aws_autoscaling_1.BlockDeviceVolume.ebs(VOLUME_SIZE), }], }); //THEN assertions_1.Template.fromStack(wfstack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { BlockDeviceMappings: [ { Ebs: { VolumeSize: VOLUME_SIZE, }, }, ], }); assertions_1.Annotations.fromStack(wfstack).hasWarning(`/${fleet.node.path}`, assertions_1.Match.stringLikeRegexp(`The BlockDevice \"${DEVICE_NAME}\" on the worker-fleet workerFleet is not encrypted. Workers can have access to sensitive data so it is recommended to encrypt the devices on the worker fleet.`)); }); test('No warnings for Ephemeral blockDeviceVolumes', () => { const DEVICE_NAME = '/dev/xvda'; // WHEN const fleet = new lib_2.WorkerInstanceFleet(wfstack, 'workerFleet', { vpc, workerMachineImage: new aws_ec2_1.GenericWindowsImage({ 'us-east-1': 'ami-any', }), renderQueue, healthMonitor, blockDevices: [{ deviceName: DEVICE_NAME, volume: aws_autoscaling_1.BlockDeviceVolume.ephemeral(0), }], }); //THEN assertions_1.Template.fromStack(wfstack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { BlockDeviceMappings: [ { DeviceName: DEVICE_NAME, VirtualName: 'ephemeral0', }, ], }); assertions_1.Annotations.fromStack(wfstack).hasNoInfo(`/${fleet.node.path}`, assertions_1.Match.anyValue()); assertions_1.Annotations.fromStack(wfstack).hasNoWarning(`/${fleet.node.path}`, assertions_1.Match.anyValue()); assertions_1.Annotations.fromStack(wfstack).hasNoError(`/${fleet.node.path}`, assertions_1.Match.anyValue()); }); test('No warnings for Suppressed blockDeviceVolumes', () => { const DEVICE_NAME = '/dev/xvda'; // WHEN const fleet = new lib_2.WorkerInstanceFleet(wfstack, 'workerFleet', { vpc, workerMachineImage: new aws_ec2_1.GenericWindowsImage({ 'us-east-1': 'ami-any', }), renderQueue, healthMonitor, blockDevices: [{ deviceName: DEVICE_NAME, volume: aws_autoscaling_1.BlockDeviceVolume.noDevice(), }], }); //THEN assertions_1.Template.fromStack(wfstack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { BlockDeviceMappings: [ { DeviceName: DEVICE_NAME, }, ], }); assertions_1.Annotations.fromStack(wfstack).hasNoInfo(`/${fleet.node.path}`, assertions_1.Match.anyValue()); assertions_1.Annotations.fromStack(wfstack).hasNoWarning(`/${fleet.node.path}`, assertions_1.Match.anyValue()); assertions_1.Annotations.fromStack(wfstack).hasNoError(`/${fleet.node.path}`, assertions_1.Match.anyValue()); }); }); describe('HealthMonitor Tests', () => { let healthMonitor; beforeEach(() => { // create a health monitor so it does not trigger warnings healthMonitor = new lib_1.HealthMonitor(wfstack, 'healthMonitor', { vpc, }); }); test('Monitor is configured for Windows', () => { // WHEN const fleet = new lib_2.WorkerInstanceFleet(wfstack, 'workerFleet', { vpc, workerMachineImage: new aws_ec2_1.GenericWindowsImage({ 'us-east-1': 'ami-any', }), renderQueue, healthMonitor, }); const userData = fleet.fleet.userData.render(); // THEN // Ensure the configuration script is executed with the expected arguments. expect(userData).toContain(`&'C:/temp/${asset_constants_2.CONFIG_WORKER_HEALTHCHECK_WINDOWS.Key}.ps1' '63415' '${lib_2.Version.MINIMUM_SUPPORTED_DEADLINE_VERSION.toString()}'`); // Ensure that the health monitor target group has been set up. // Note: It's sufficient to just check for any resource created by the HealthMonitor registration. // The HealthMonitor tests cover ensuring that all of the resources are set up. assertions_1.Template.fromStack(wfstack).hasResourceProperties('AWS::ElasticLoadBalancingV2::TargetGroup', { HealthCheckIntervalSeconds: 300, HealthCheckPort: '63415', HealthCheckProtocol: 'HTTP', Port: 8081, Protocol: 'HTTP', TargetType: 'instance', }); }); test('Monitor is configured for Linux', () => { // WHEN const fleet = new lib_2.WorkerInstanceFleet(wfstack, 'workerFleet', { vpc, workerMachineImage: new aws_ec2_1.GenericLinuxImage({ 'us-east-1': 'ami-any', }), renderQueue, healthMonitor, }); const userData = fleet.fleet.userData.render(); // THEN // Ensure the configuration script is executed with the expected arguments. expect(userData).toContain(`'/tmp/${asset_constants_2.CONFIG_WORKER_HEALTHCHECK_LINUX.Key}.sh' '63415' '${lib_2.Version.MINIMUM_SUPPORTED_DEADLINE_VERSION.toString()}'`); // Ensure that the health monitor target group has been set up. // Note: It's sufficient to just check for any resource created by the HealthMonitor registration. // The HealthMonitor tests cover ensuring that all of the resources are set up. assertions_1.Template.fromStack(wfstack).hasResourceProperties('AWS::ElasticLoadBalancingV2::TargetGroup', { HealthCheckIntervalSeconds: 300, HealthCheckPort: '63415', HealthCheckProtocol: 'HTTP', Port: 8081, Protocol: 'HTTP', TargetType: 'instance', }); }); test('UserData is added', () => { // WHEN class UserDataProvider extends lib_2.InstanceUserDataProvider { preCloudWatchAgent(host) { host.userData.addCommands('echo preCloudWatchAgent'); } preRenderQueueConfiguration(host) { host.userData.addCommands('echo preRenderQueueConfiguration'); } preWorkerConfiguration(host) { host.userData.addCommands('echo preWorkerConfiguration'); } postWorkerLaunch(host) { host.userData.addCommands('echo postWorkerLaunch'); } } const fleet = new lib_2.WorkerInstanceFleet(wfstack, 'workerFleet', { vpc, workerMachineImage: new aws_ec2_1.GenericLinuxImage({ 'us-east-1': 'ami-any', }), renderQueue, healthMonitor, userDataProvider: new UserDataProvider(wfstack, 'UserDataProvider'), }); const userData = fleet.fleet.userData.render(); // THEN expect(userData).toContain('echo preCloudWatchAgent'); expect(userData).toContain('echo preRenderQueueConfiguration'); expect(userData).toContain('echo preWorkerConfiguration'); expect(userData).toContain('echo postWorkerLaunch'); }); }); describe('tagging', () => { (0, tag_helpers_1.testConstructTags)({ constructName: 'WorkerInstanceFleet', createConstruct: () => { // GIVEN const healthMonitorStack = new aws_cdk_lib_1.Stack(app, 'HealthMonitorStack', { env: { region: 'us-east-1', }, }); const healthMonitor = new lib_1.HealthMonitor(healthMonitorStack, 'healthMonitor', { vpc, }); const deviceName = '/dev/xvda'; // WHEN new lib_2.WorkerInstanceFleet(wfstack, 'WorkerFleet', { vpc, workerMachineImage: new aws_ec2_1.GenericLinuxImage({ 'us-east-1': 'ami-any', }), renderQueue, healthMonitor, blockDevices: [{ deviceName, volume: aws_autoscaling_1.BlockDeviceVolume.noDevice(), }], }); return wfstack; }, resourceTypeCounts: { 'AWS::EC2::SecurityGroup': 1, 'AWS::IAM::Role': 1, 'AWS::AutoScaling::AutoScalingGroup': 1, 'AWS::ElasticLoadBalancingV2::TargetGroup': 1, 'AWS::SSM::Parameter': 1, }, }); }); test('worker fleet signals when non-zero minCapacity', () => { // WHEN const fleet = new lib_2.WorkerInstanceFleet(wfstack, 'workerFleet', { vpc, workerMachineImage: new aws_ec2_1.GenericWindowsImage({ 'us-east-1': 'ami-any', }), renderQueue, minCapacity: 1, }); // WHEN const userData = fleet.fleet.userData.render(); // THEN expect(userData).toContain('cfn-signal'); assertions_1.Template.fromStack(wfstack).hasResource('AWS::AutoScaling::AutoScalingGroup', { CreationPolicy: { ResourceSignal: { Count: 1, }, }, }); assertions_1.Annotations.fromStack(wfstack).hasWarning(`/${fleet.node.path}`, assertions_1.Match.stringLikeRegexp('.*being created without being provided any block devices so the Source AMI\'s devices will be used. Workers can have access to sensitive data so it is recommended to either explicitly encrypt the devices on the worker fleet or to ensure the source AMI\'s Drives are encrypted.')); assertions_1.Annotations.fromStack(wfstack).hasWarning(`/${fleet.node.path}`, assertions_1.Match.stringLikeRegexp('.*being created without a health monitor attached to it. This means that the fleet will not automatically scale-in to 0 if the workers are unhealthy')); }); test('worker fleet does not signal when zero minCapacity', () => { // WHEN const fleet = new lib_2.WorkerInstanceFleet(wfstack, 'workerFleet', { vpc, workerMachineImage: new aws_ec2_1.GenericWindowsImage({ 'us-east-1': 'ami-any', }), renderQueue, minCapacity: 0, }); // WHEN const userData = fleet.fleet.userData.render(); // THEN // There should be no cfn-signal call in the UserData. expect(userData).not.toContain('cfn-signal'); // Make sure we don't have a CreationPolicy (0, test_helper_1.resourcePropertiesCountIs)(wfstack, 'AWS::AutoScaling::AutoScalingGroup', { CreationPolicy: assertions_1.Match.anyValue(), }, 0); assertions_1.Annotations.fromStack(wfstack).hasWarning(`/${fleet.node.path}`, assertions_1.Match.stringLikeRegexp('.*Depl