From a5596d23a50554b3fa85d83e45b26904d7bec301 Mon Sep 17 00:00:00 2001 From: Josh Usiskin <56369778+jusiskin@users.noreply.github.com> Date: Tue, 11 Aug 2020 12:27:29 -0500 Subject: [PATCH] chore(deadline): clean up UsageBasedLicensing construct and tests (#20) BREAKING CHANGE: construct IDs renamed in UsageBasedLicensing. - Previously deployed resources will be terminated when updating - Default log stream prefix changed from 'docker' to 'LicenseForwarder' - Memory properties are no longer specified when constructing UsageBasedLicensing instances --- .../lib/deadline/lib/usage-based-licensing.ts | 179 +++-- .../test/usage-based-licensing.test.ts | 753 ++++++++++++------ 2 files changed, 613 insertions(+), 319 deletions(-) diff --git a/packages/aws-rfdk/lib/deadline/lib/usage-based-licensing.ts b/packages/aws-rfdk/lib/deadline/lib/usage-based-licensing.ts index 8545d4e4b..17da7c0d4 100644 --- a/packages/aws-rfdk/lib/deadline/lib/usage-based-licensing.ts +++ b/packages/aws-rfdk/lib/deadline/lib/usage-based-licensing.ts @@ -42,6 +42,26 @@ import { import {IRenderQueue} from './render-queue'; import {IWorkerFleet} from './worker-fleet'; +/** + * Properties for constructing a {@link UsageBasedLicense} instance. + */ +export interface UsageBasedLicenseProps { + /** + * The name of the product that the usage-based license applies to. + */ + readonly licenseName: string; + + /** + * The set of ports that are used for licensing traffic + */ + readonly ports: Port[]; + + /** + * The maximum number of usage-based licenses that can be used concurrently. + */ + readonly limit?: number; +} + /** * Instances of this class represent a usage-based license for a particular product. * It encapsulates all of the information specific to a product that the UsageBasedLicensing @@ -65,7 +85,11 @@ export class UsageBasedLicense { * @default - limit will be set to unlimited */ public static for3dsMax(limit?: number): UsageBasedLicense { - return new UsageBasedLicense('max', [Port.tcp(27002)], limit); + return new UsageBasedLicense({ + licenseName: 'max', + ports: [Port.tcp(27002)], + limit, + }); } /** @@ -79,7 +103,11 @@ export class UsageBasedLicense { * @default - limit will be set to unlimited */ public static forArnold(limit?: number): UsageBasedLicense { - return new UsageBasedLicense('arnold', [Port.tcp(5056), Port.tcp(7056)], limit); + return new UsageBasedLicense({ + licenseName: 'arnold', + ports: [Port.tcp(5056), Port.tcp(7056)], + limit, + }); } /** @@ -90,7 +118,11 @@ export class UsageBasedLicense { * @default - limit will be set to unlimited */ public static forCinema4D(limit?: number): UsageBasedLicense { - return new UsageBasedLicense('cinema4d', [Port.tcp(5057), Port.tcp(7057)], limit); + return new UsageBasedLicense({ + licenseName: 'cinema4d', + ports: [Port.tcp(5057), Port.tcp(7057)], + limit, + }); } /** @@ -101,7 +133,11 @@ export class UsageBasedLicense { * @default - limit will be set to unlimited */ public static forClarisse(limit?: number): UsageBasedLicense { - return new UsageBasedLicense('clarisse', [Port.tcp(40500)], limit); + return new UsageBasedLicense({ + licenseName: 'clarisse', + ports: [Port.tcp(40500)], + limit, + }); } /** @@ -112,7 +148,11 @@ export class UsageBasedLicense { * @default - limit will be set to unlimited */ public static forHoudini(limit?: number): UsageBasedLicense { - return new UsageBasedLicense('houdini', [Port.tcp(1715)], limit); + return new UsageBasedLicense({ + licenseName: 'houdini', + ports: [Port.tcp(1715)], + limit, + }); } /** @@ -123,7 +163,11 @@ export class UsageBasedLicense { * @default - limit will be set to unlimited */ public static forKatana(limit?: number): UsageBasedLicense { - return new UsageBasedLicense('katana', [Port.tcp(4101), Port.tcp(6101)], limit); + return new UsageBasedLicense({ + licenseName: 'katana', + ports: [Port.tcp(4101), Port.tcp(6101)], + limit, + }); } /** @@ -134,7 +178,11 @@ export class UsageBasedLicense { * @default - limit will be set to unlimited */ public static forKeyShot(limit?: number): UsageBasedLicense { - return new UsageBasedLicense('keyshot', [Port.tcp(27003), Port.tcp(2703)], limit); + return new UsageBasedLicense({ + licenseName: 'keyshot', + ports: [Port.tcp(27003), Port.tcp(2703)], + limit, + }); } /** @@ -145,7 +193,11 @@ export class UsageBasedLicense { * @default - limit will be set to unlimited */ public static forKrakatoa(limit?: number): UsageBasedLicense { - return new UsageBasedLicense('krakatoa', [Port.tcp(27000), Port.tcp(2700)], limit); + return new UsageBasedLicense({ + licenseName: 'krakatoa', + ports: [Port.tcp(27000), Port.tcp(2700)], + limit, + }); } /** @@ -156,7 +208,11 @@ export class UsageBasedLicense { * @default - limit will be set to unlimited */ public static forMantra(limit?: number): UsageBasedLicense { - return new UsageBasedLicense('mantra', [Port.tcp(1716)], limit); + return new UsageBasedLicense({ + licenseName: 'mantra', + ports: [Port.tcp(1716)], + limit, + }); } /** @@ -167,7 +223,11 @@ export class UsageBasedLicense { * @default - limit will be set to unlimited */ public static forMaxwell(limit?: number): UsageBasedLicense { - return new UsageBasedLicense('maxwell', [Port.tcp(5055), Port.tcp(7055)], limit); + return new UsageBasedLicense({ + licenseName: 'maxwell', + ports: [Port.tcp(5055), Port.tcp(7055)], + limit, + }); } /** @@ -181,7 +241,11 @@ export class UsageBasedLicense { * @default - limit will be set to unlimited */ public static forMaya(limit?: number): UsageBasedLicense { - return new UsageBasedLicense('maya', [Port.tcp(27002), Port.tcp(2702)], limit); + return new UsageBasedLicense({ + licenseName: 'maya', + ports: [Port.tcp(27002), Port.tcp(2702)], + limit, + }); } /** @@ -192,7 +256,11 @@ export class UsageBasedLicense { * @default - limit will be set to unlimited */ public static forNuke(limit?: number): UsageBasedLicense { - return new UsageBasedLicense('nuke', [Port.tcp(4101), Port.tcp(6101)], limit); + return new UsageBasedLicense({ + licenseName: 'nuke', + ports: [Port.tcp(4101), Port.tcp(6101)], + limit, + }); } /** @@ -203,7 +271,11 @@ export class UsageBasedLicense { * @default - limit will be set to unlimited */ public static forRealFlow(limit?: number): UsageBasedLicense { - return new UsageBasedLicense('realflow', [Port.tcp(5055), Port.tcp(7055)], limit); + return new UsageBasedLicense({ + licenseName: 'realflow', + ports: [Port.tcp(5055), Port.tcp(7055)], + limit, + }); } /** @@ -214,7 +286,11 @@ export class UsageBasedLicense { * @default - limit will be set to unlimited */ public static forRedShift(limit?: number): UsageBasedLicense { - return new UsageBasedLicense('redshift', [Port.tcp(5054), Port.tcp(7054)], limit); + return new UsageBasedLicense({ + licenseName: 'redshift', + ports: [Port.tcp(5054), Port.tcp(7054)], + limit, + }); } /** @@ -225,7 +301,11 @@ export class UsageBasedLicense { * @default - limit will be set to unlimited */ public static forVray(limit?: number): UsageBasedLicense { - return new UsageBasedLicense('vray', [Port.tcp(30306)], limit); + return new UsageBasedLicense({ + licenseName: 'vray', + ports: [Port.tcp(30306)], + limit, + }); } /** @@ -236,7 +316,11 @@ export class UsageBasedLicense { * @default - limit will be set to unlimited */ public static forYeti(limit?: number): UsageBasedLicense { - return new UsageBasedLicense('yeti', [Port.tcp(5053), Port.tcp(7053)], limit); + return new UsageBasedLicense({ + licenseName: 'yeti', + ports: [Port.tcp(5053), Port.tcp(7053)], + limit, + }); } /** @@ -254,10 +338,10 @@ export class UsageBasedLicense { */ public readonly limit?: number; - constructor(licenseName: string, ports: Port[], limit?: number) { - this.licenseName = licenseName; - this.ports = ports; - this.limit = limit; + constructor(props: UsageBasedLicenseProps) { + this.licenseName = props.licenseName; + this.ports = props.ports; + this.limit = props.limit; } } @@ -325,33 +409,6 @@ export interface UsageBasedLicensingProps { */ readonly licenses: UsageBasedLicense[]; - /** - * The amount (in MiB) of memory to present to the License Forwarder container. - * - * If your container attempts to exceed the allocated memory, the container - * is terminated. - * - * At least one of memoryLimitMiB and memoryReservationMiB is required for non-Fargate services. - * - * @default - No memory limit. - */ - readonly memoryLimitMiB?: number; - - /** - * The soft limit (in MiB) of memory to reserve for the License Forwarder container. - * - * When system memory is under heavy contention, Docker attempts to keep the - * container memory to this soft limit. However, your container can consume more - * memory when it needs to, up to either the hard limit specified with the memory - * parameter (if applicable), or all of the available memory on the container - * instance, whichever comes first. - * - * At least one of memoryLimitMiB and memoryReservationMiB is required for non-Fargate services. - * - * @default - No memory reserved. - */ - readonly memoryReservationMiB?: number; - /** * Properties for setting up the Deadline License Forwarder's LogGroup in CloudWatch * @default - LogGroup will be created with all properties' default values to the LogGroup: /renderfarm/ @@ -408,19 +465,22 @@ export class UsageBasedLicensing extends Construct implements IGrantable { /** * The Amazon ECS cluster that is hosting the Deadline License Forwarder for UBL. */ - public cluster: Cluster; + public readonly cluster: Cluster; /** * Autoscaling group for license forwarder instances */ - public asg: AutoScalingGroup; + public readonly asg: AutoScalingGroup; /** * The principal to grant permissions to. */ public readonly grantPrincipal: IPrincipal; - private readonly service: Ec2Service; + /** + * The ECS service that serves usage based licensing. + */ + public readonly service: Ec2Service; constructor(scope: Construct, id: string, props: UsageBasedLicensingProps) { super(scope, id); @@ -437,7 +497,7 @@ export class UsageBasedLicensing extends Construct implements IGrantable { this.cluster = new Cluster(this, 'Cluster', { vpc: props.vpc }); - this.asg = this.cluster.addCapacity('ClusterCapacity', { + this.asg = this.cluster.addCapacity('ASG', { vpcSubnets: props.vpcSubnets ?? { subnetType: SubnetType.PRIVATE }, instanceType: props.instanceType ? props.instanceType : InstanceType.of(InstanceClass.C5, InstanceSize.LARGE), minCapacity: props.desiredCount ?? 1, @@ -448,7 +508,7 @@ export class UsageBasedLicensing extends Construct implements IGrantable { }], }); - const taskDefinition = new TaskDefinition(this, 'TaskDef', { + const taskDefinition = new TaskDefinition(this, 'TaskDefinition', { compatibility: Compatibility.EC2, networkMode: NetworkMode.HOST, }); @@ -472,17 +532,16 @@ export class UsageBasedLicensing extends Construct implements IGrantable { ...props.logGroupProps, logGroupPrefix: prefix, }; - const logGroup = LogGroupFactory.createOrFetch(this, 'LogGroupWrapper', `${id}`, defaultedLogGroupProps); + const logGroup = LogGroupFactory.createOrFetch(this, 'LogGroupWrapper', id, defaultedLogGroupProps); logGroup.grantWrite(this.asg); - const container = taskDefinition.addContainer('Container', { + const container = taskDefinition.addContainer('LicenseForwarderContainer', { image: props.images.licenseForwarder, environment: containerEnv, - memoryLimitMiB: props.memoryLimitMiB, - memoryReservationMiB: props.memoryReservationMiB, + memoryReservationMiB: 1024, logging: LogDriver.awsLogs({ logGroup, - streamPrefix: 'docker', + streamPrefix: 'LicenseForwarder', }), }); @@ -510,6 +569,10 @@ export class UsageBasedLicensing extends Construct implements IGrantable { maxHealthyPercent: 100, }); + // An explicit dependency is required from the service to the ASG providing its capacity. + // See: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-attribute-dependson.html + this.service.node.addDependency(this.asg); + this.node.defaultChild = this.service; this.connections.allowToDefaultPort(props.renderQueue); } @@ -530,7 +593,7 @@ export class UsageBasedLicensing extends Construct implements IGrantable { } /** - * The connections object that allows you to control network egress/ingress to the Licence Forwarder. + * The connections object that allows you to control network egress/ingress to the License Forwarder. */ public get connections() { return this.service.connections; diff --git a/packages/aws-rfdk/lib/deadline/test/usage-based-licensing.test.ts b/packages/aws-rfdk/lib/deadline/test/usage-based-licensing.test.ts index a37d2c33a..d7b6abe6d 100644 --- a/packages/aws-rfdk/lib/deadline/test/usage-based-licensing.test.ts +++ b/packages/aws-rfdk/lib/deadline/test/usage-based-licensing.test.ts @@ -6,14 +6,12 @@ import { arrayWith, expect as expectCDK, - haveResource, haveResourceLike, + stringLike, } from '@aws-cdk/assert'; import { GenericWindowsImage, - InstanceClass, - InstanceSize, - InstanceType, IVpc, + IVpc, SecurityGroup, SubnetType, Vpc, @@ -25,15 +23,19 @@ import { Cluster, ContainerImage, } from '@aws-cdk/aws-ecs'; +import { + ILogGroup, +} from '@aws-cdk/aws-logs'; import { ISecret, Secret, } from '@aws-cdk/aws-secretsmanager'; import { + App, + CfnElement, Stack, } from '@aws-cdk/core'; import { - IRenderQueue, IVersion, IWorkerFleet, RenderQueue, @@ -45,296 +47,525 @@ import { WorkerInstanceFleet, } from '../lib'; +const env = { + region: 'us-east-1', +}; +let app: App; +let certificateSecret: ISecret; +let deadlineVersion: IVersion; +let dependencyStack: Stack; +let dockerContainer: DockerImageAsset; +let images: UsageBasedLicensingImages; +let lfCluster: Cluster; +let licenses: UsageBasedLicense[]; +let rcsImage: ContainerImage; +let renderQueue: RenderQueue; let stack: Stack; +let ubl: UsageBasedLicensing; let vpc: IVpc; -let rcsImage: ContainerImage; -let renderQueue: IRenderQueue; -let lfCluster: Cluster; -let certSecret: ISecret; let workerFleet: IWorkerFleet; -let dockerContainer: DockerImageAsset; -let deadlineVersion: IVersion; -let images: UsageBasedLicensingImages; -beforeEach(() => { - stack = new Stack(undefined, undefined, { - env: { - region: 'us-east-1', - }, - }); +describe('UsageBasedLicensing', () => { + beforeEach(() => { + // GIVEN + app = new App(); - deadlineVersion = VersionQuery.exact(stack, 'Version', { - majorVersion: 10, - minorVersion: 1, - releaseVersion: 9, - patchVersion: 1, - }); + dependencyStack = new Stack(app, 'DependencyStack', { env }); - expect(deadlineVersion.linuxFullVersionString).toBeDefined(); - - vpc = new Vpc(stack, 'VPC'); - rcsImage = ContainerImage.fromDockerImageAsset(new DockerImageAsset(stack, 'Image', { - directory: __dirname, - })); - renderQueue = new RenderQueue(stack, 'RQ-NonDefaultPort', { - version: deadlineVersion, - vpc, - images: { remoteConnectionServer: rcsImage }, - repository: new Repository(stack, 'RepositoryNonDefault', { - vpc, + deadlineVersion = VersionQuery.exact(dependencyStack, 'Version', { + majorVersion: 10, + minorVersion: 1, + releaseVersion: 9, + patchVersion: 1, + }); + + expect(deadlineVersion.linuxFullVersionString).toBeDefined(); + + vpc = new Vpc(dependencyStack, 'VPC'); + rcsImage = ContainerImage.fromDockerImageAsset(new DockerImageAsset(dependencyStack, 'Image', { + directory: __dirname, + })); + renderQueue = new RenderQueue(dependencyStack, 'RQ-NonDefaultPort', { version: deadlineVersion, - }), - }); + vpc, + images: { remoteConnectionServer: rcsImage }, + repository: new Repository(dependencyStack, 'RepositoryNonDefault', { + vpc, + version: deadlineVersion, + }), + }); - lfCluster = new Cluster(stack, 'licenseForwarderCluster', { - vpc, - }); - certSecret = Secret.fromSecretArn(lfCluster, 'CertSecret', 'arn:aws:secretsmanager:us-west-2:675872700355:secret:CertSecret-j1kiFz'); + lfCluster = new Cluster(dependencyStack, 'licenseForwarderCluster', { + vpc, + }); + certificateSecret = Secret.fromSecretArn(lfCluster, 'CertSecret', 'arn:aws:secretsmanager:us-west-2:675872700355:secret:CertSecret-j1kiFz'); + + dockerContainer = new DockerImageAsset(lfCluster, 'license-forwarder', { + directory: __dirname, + }); + images = { + licenseForwarder: ContainerImage.fromDockerImageAsset(dockerContainer), + }; + + const workerStack = new Stack(app, 'WorkerStack', { env }); + workerFleet = new WorkerInstanceFleet(workerStack, 'workerFleet', { + vpc, + workerMachineImage: new GenericWindowsImage({ + 'us-east-1': 'ami-any', + }), + renderQueue, + securityGroup: SecurityGroup.fromSecurityGroupId(dependencyStack, 'SG', 'sg-123456789', { + allowAllOutbound: false, + }), + }); + licenses = [UsageBasedLicense.forMaya()]; + + stack = new Stack(app, 'Stack', { env }); - dockerContainer = new DockerImageAsset(lfCluster, 'license-forwarder', { - directory: __dirname, + // WHEN + ubl = new UsageBasedLicensing(stack, 'UBL', { + certificateSecret, + images, + licenses, + renderQueue, + vpc, + }); }); - images = { - licenseForwarder: ContainerImage.fromDockerImageAsset(dockerContainer), - }; - - workerFleet = new WorkerInstanceFleet(stack, 'workerFleet', { - vpc, - workerMachineImage: new GenericWindowsImage({ - 'us-east-1': 'ami-any', - }), - renderQueue, - securityGroup: SecurityGroup.fromSecurityGroupId(stack, 'SG', 'sg-123456789', { - allowAllOutbound: false, - }), + + test('creates an ECS cluster', () => { + // THEN + expectCDK(stack).to(haveResourceLike('AWS::ECS::Cluster')); }); -}); - -test('configures deployment configuration', () => { - // WHEN - new UsageBasedLicensing(stack, 'licenseForwarder', { - vpc, - images, - certificateSecret: certSecret, - memoryLimitMiB: 1024, - licenses: [UsageBasedLicense.forVray()], - renderQueue, + + describe('creates an ASG', () => { + test('defaults', () => { + // THEN + expectCDK(stack).to(haveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + MinSize: '1', + MaxSize: '1', + VPCZoneIdentifier: [ + { + 'Fn::ImportValue': stringLike(`${dependencyStack.stackName}:ExportsOutputRefVPCPrivateSubnet1Subnet*`), + }, + { + 'Fn::ImportValue': stringLike(`${dependencyStack.stackName}:ExportsOutputRefVPCPrivateSubnet2Subnet*`), + }, + ], + })); + }); + + test('capacity can be specified', () => { + // WHEN + const isolatedStack = new Stack(app, 'MyStack', { env }); + new UsageBasedLicensing(isolatedStack, 'licenseForwarder', { + certificateSecret, + desiredCount: 2, + images, + licenses, + renderQueue, + vpc, + }); + + // THEN + expectCDK(isolatedStack).to(haveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + MinSize: '2', + MaxSize: '2', + })); + }); + + test('gives write access to log group', () => { + // GIVEN + const logGroup = ubl.node.findChild('UBLLogGroup') as ILogGroup; + const asgRoleLogicalId = Stack.of(ubl).getLogicalId(ubl.asg.role.node.defaultChild as CfnElement); + + // THEN + expectCDK(stack).to(haveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: arrayWith( + { + Action: arrayWith( + 'logs:CreateLogStream', + 'logs:PutLogEvents', + ), + Effect: 'Allow', + Resource: stack.resolve(logGroup.logGroupArn), + }, + ), + Version: '2012-10-17', + }, + Roles: arrayWith( + { Ref: asgRoleLogicalId }, + ), + })); + }); }); - // THEN - expectCDK(stack).to(haveResourceLike('AWS::ECS::Service', { - DeploymentConfiguration: { - MaximumPercent: 100, - MinimumHealthyPercent: 0, - }, - })); -}); - -test('default ECS stack for License Forwarder is created correctly', () => { - // WHEN - new UsageBasedLicensing(stack, 'licenseForwarder', { - vpc, - images, - certificateSecret: certSecret, - memoryLimitMiB: 3 * 1024, - licenses: [UsageBasedLicense.forVray()], - renderQueue, + describe('creates an ECS service', () => { + test('associated with the cluster', () => { + // THEN + expectCDK(stack).to(haveResourceLike('AWS::ECS::Service', { + Cluster: { Ref: stack.getLogicalId(ubl.cluster.node.defaultChild as CfnElement) }, + })); + }); + + describe('DesiredCount', () => { + test('defaults to 1', () => { + // THEN + expectCDK(stack).to(haveResourceLike('AWS::ECS::Service', { + DesiredCount: 1, + })); + }); + + test('can be specified', () => { + // GIVEN + const desiredCount = 2; + const isolatedStack = new Stack(app, 'IsolatedStack', { env }); + + // WHEN + new UsageBasedLicensing(isolatedStack, 'UBL', { + certificateSecret, + images, + licenses, + renderQueue, + vpc, + desiredCount, + }); + + // THEN + expectCDK(isolatedStack).to(haveResourceLike('AWS::ECS::Service', { + DesiredCount: desiredCount, + })); + }); + }); + + test('sets launch type to EC2', () => { + // THEN + expectCDK(stack).to(haveResourceLike('AWS::ECS::Service', { + LaunchType: 'EC2', + })); + }); + + test('sets distinct instance placement constraint', () => { + // THEN + expectCDK(stack).to(haveResourceLike('AWS::ECS::Service', { + PlacementConstraints: arrayWith( + { Type: 'distinctInstance' }, + ), + })); + }); + + test('uses the task definition', () => { + // THEN + expectCDK(stack).to(haveResourceLike('AWS::ECS::Service', { + TaskDefinition: { Ref: stack.getLogicalId(ubl.service.taskDefinition.node.defaultChild as CfnElement) }, + })); + }); + + test('with the correct deployment configuration', () => { + // THEN + expectCDK(stack).to(haveResourceLike('AWS::ECS::Service', { + DeploymentConfiguration: { + MaximumPercent: 100, + MinimumHealthyPercent: 0, + }, + })); + }); }); - // THEN - expectCDK(stack).to(haveResource('AWS::ECS::Cluster')); - expectCDK(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { - ContainerDefinitions: [ - { - Environment: arrayWith( + describe('creates a task definition', () => { + test('container name is LicenseForwarderContainer', () => { + // THEN + expectCDK(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ { - Name: 'UBL_CERTIFICATES_URI', - Value: 'arn:aws:secretsmanager:us-west-2:675872700355:secret:CertSecret-j1kiFz', + Name: 'LicenseForwarderContainer', }, + ], + })); + }); + + test('container is marked essential', () => { + // THEN + expectCDK(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ { - Name: 'UBL_LIMITS', - Value: 'vray:2147483647', + Essential: true, }, - ), - Essential: true, - Image: {}, - LogConfiguration: { - LogDriver: 'awslogs', - Options: { - 'awslogs-group': {}, - 'awslogs-stream-prefix': 'docker', - 'awslogs-region': 'us-east-1', + ], + })); + }); + + test('with increased ulimits', () => { + // THEN + expectCDK(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ + { + Ulimits: [ + { + HardLimit: 200000, + Name: 'nofile', + SoftLimit: 200000, + }, + { + HardLimit: 64000, + Name: 'nproc', + SoftLimit: 64000, + }, + ], }, - }, - Memory: 3072, - Name: 'Container', - Ulimits: [ + ], + })); + }); + + test('with awslogs log driver', () => { + // THEN + expectCDK(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ { - HardLimit: 200000, - Name: 'nofile', - SoftLimit: 200000, + LogConfiguration: { + LogDriver: 'awslogs', + Options: { + 'awslogs-group': {}, + 'awslogs-stream-prefix': 'LicenseForwarder', + 'awslogs-region': env.region, + }, + }, }, + ], + })); + }); + + test('configures UBL certificates', () => { + // GIVEN + const taskRoleLogicalId = Stack.of(ubl).getLogicalId(ubl.service.taskDefinition.taskRole.node.defaultChild as CfnElement); + + // THEN + expectCDK(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ { - HardLimit: 64000, - Name: 'nproc', - SoftLimit: 64000, + Environment: arrayWith( + { + Name: 'UBL_CERTIFICATES_URI', + Value: certificateSecret.secretArn, + }, + ), }, ], - }, - ], - ExecutionRoleArn: {}, - NetworkMode: 'host', - RequiresCompatibilities: [ 'EC2' ], - TaskRoleArn: {}, - })); - expectCDK(stack).to(haveResourceLike('AWS::AutoScaling::AutoScalingGroup', { - MinSize: '1', - MaxSize: '1', - VPCZoneIdentifier: [ - { - Ref: 'VPCPrivateSubnet1Subnet8BCA10E0', - }, - { - Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A', - }, - ], - })); -}); - -test('License Forwarder capacity is set correctly', () => { - // WHEN - new UsageBasedLicensing(stack, 'licenseForwarder', { - vpc, - images, - certificateSecret: certSecret, - memoryLimitMiB: 3 * 1024, - licenses: [UsageBasedLicense.forVray()], - desiredCount: 2, - renderQueue, - }); + TaskRoleArn: { + 'Fn::GetAtt': [ + taskRoleLogicalId, + 'Arn', + ], + }, + })); - // THEN - expectCDK(stack).to(haveResourceLike('AWS::AutoScaling::AutoScalingGroup', { - MinSize: '2', - MaxSize: '2', - })); -}); - -test('License Forwarder subnet selection', () => { - // WHEN - new UsageBasedLicensing(stack, 'licenseForwarder', { - vpc, - images, - certificateSecret: certSecret, - memoryLimitMiB: 3 * 1024, - licenses: [UsageBasedLicense.forVray()], - vpcSubnets: { subnetType: SubnetType.PUBLIC }, - renderQueue, + expectCDK(stack).to(haveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ + 'secretsmanager:GetSecretValue', + 'secretsmanager:DescribeSecret', + ], + Effect: 'Allow', + Resource: certificateSecret.secretArn, + }, + ], + Version: '2012-10-17', + }, + Roles: [ + { Ref: Stack.of(ubl).getLogicalId(ubl.service.taskDefinition.taskRole.node.defaultChild as CfnElement) }, + ], + })); + }); + + test('uses host networking', () => { + // THEN + expectCDK(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + NetworkMode: 'host', + })); + }); + + test('is marked EC2 compatible only', () => { + // THEN + expectCDK(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + RequiresCompatibilities: [ 'EC2' ], + })); + }); }); - // THEN - expectCDK(stack).to(haveResourceLike('AWS::AutoScaling::AutoScalingGroup', { - VPCZoneIdentifier: [ - { - Ref: 'VPCPublicSubnet1SubnetB4246D30', - }, - { - Ref: 'VPCPublicSubnet2Subnet74179F39', - }, - ], - })); -}); - -test('test license limits', () => { - // WHEN - new UsageBasedLicensing(stack, 'licenseForwarder', { - vpc, - images, - memoryLimitMiB: 2 * 1024, - certificateSecret: certSecret, - instanceType: InstanceType.of(InstanceClass.C4, InstanceSize.LARGE), - logGroupProps: {logGroupPrefix: 'licenseForwarderTest', bucketName: 'logS3Bucket'}, - renderQueue, - licenses: [ - UsageBasedLicense.forMaya(10), - UsageBasedLicense.forVray(10), - ], + test('License Forwarder subnet selection', () => { + // GIVEN + const publicSubnetIds = ['PublicSubnet1', 'PublicSubnet2']; + const vpcFromAttributes = Vpc.fromVpcAttributes(dependencyStack, 'AttrVpc', { + availabilityZones: ['us-east-1a', 'us-east-1b'], + vpcId: 'vpcid', + publicSubnetIds, + }); + stack = new Stack(app, 'IsolatedStack', { env }); + + // WHEN + new UsageBasedLicensing(stack, 'licenseForwarder', { + certificateSecret, + images, + licenses, + renderQueue, + vpc: vpcFromAttributes, + vpcSubnets: { subnetType: SubnetType.PUBLIC }, + }); + + // THEN + expectCDK(stack).to(haveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + VPCZoneIdentifier: publicSubnetIds, + })); }); - // THEN - expectCDK(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { - ContainerDefinitions: [ - { - Environment: arrayWith( + describe('license limits', () => { + test('multiple licenses with limits', () => { + // GIVEN + const isolatedStack = new Stack(app, 'IsolatedStack', { env }); + + // WHEN + new UsageBasedLicensing(isolatedStack, 'licenseForwarder', { + vpc, + images, + certificateSecret, + renderQueue, + licenses: [ + UsageBasedLicense.forMaya(10), + UsageBasedLicense.forVray(10), + ], + }); + + // THEN + expectCDK(isolatedStack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ { - Name: 'UBL_CERTIFICATES_URI', - Value: 'arn:aws:secretsmanager:us-west-2:675872700355:secret:CertSecret-j1kiFz', + Environment: arrayWith( + { + Name: 'UBL_LIMITS', + Value: 'maya:10;vray:10', + }, + ), }, - { - Name: 'UBL_LIMITS', - Value: 'maya:10;vray:10', + ], + })); + }); + + test.each([ + ['3dsMax', UsageBasedLicense.for3dsMax(10), [27002]], + ['Arnold', UsageBasedLicense.forArnold(10), [5056, 7056]], + ['Cinema4D', UsageBasedLicense.forCinema4D(10), [5057, 7057]], + ['Clarisse', UsageBasedLicense.forClarisse(10), [40500]], + ['Houdini', UsageBasedLicense.forHoudini(10), [1715]], + ['Katana', UsageBasedLicense.forKatana(10), [4101, 6101]], + ['KeyShot', UsageBasedLicense.forKeyShot(10), [27003, 2703]], + ['Krakatoa', UsageBasedLicense.forKrakatoa(10), [27000, 2700]], + ['Mantra', UsageBasedLicense.forMantra(10), [1716]], + ['Maxwell', UsageBasedLicense.forMaxwell(10), [5055, 7055]], + ['Maya', UsageBasedLicense.forMaya(10), [27002, 2702]], + ['Nuke', UsageBasedLicense.forNuke(10), [4101, 6101]], + ['RealFlow', UsageBasedLicense.forRealFlow(10), [5055, 7055]], + ['RedShift', UsageBasedLicense.forRedShift(10), [5054, 7054]], + ['Vray', UsageBasedLicense.forVray(10), [30306]], + ['Yeti', UsageBasedLicense.forYeti(10), [5053, 7053]], + ])('Test open port for license type %s', (_licenseName: string, license: UsageBasedLicense, ports: number[]) => { + // GIVEN + const isolatedStack = new Stack(app, 'IsolatedStack', { env }); + + // WHEN + ubl = new UsageBasedLicensing(isolatedStack, 'licenseForwarder', { + vpc, + certificateSecret, + licenses: [ + license, + ], + renderQueue, + images, + }); + + ubl.grantPortAccess(workerFleet, [license]); + + // THEN + ports.forEach( port => { + const ublAsgSecurityGroup = ubl.asg.connections.securityGroups[0].node.defaultChild; + const ublAsgSecurityGroupLogicalId = isolatedStack.getLogicalId(ublAsgSecurityGroup as CfnElement); + + expectCDK(isolatedStack).to(haveResourceLike('AWS::EC2::SecurityGroupIngress', { + IpProtocol: 'tcp', + ToPort: port, + GroupId: { + 'Fn::GetAtt': [ + ublAsgSecurityGroupLogicalId, + 'GroupId', + ], }, - ), - Memory: 2048, - }, - ], - })); - expectCDK(stack).to(haveResourceLike('AWS::EC2::SecurityGroupIngress', { - IpProtocol: 'tcp', - ToPort: 8080, - })); -}); - -test.each([ - [UsageBasedLicense.for3dsMax(10), [27002]], - [UsageBasedLicense.forArnold(10), [5056, 7056]], - [UsageBasedLicense.forCinema4D(10), [5057, 7057]], - [UsageBasedLicense.forClarisse(10), [40500]], - [UsageBasedLicense.forHoudini(10), [1715]], - [UsageBasedLicense.forKatana(10), [4101, 6101]], - [UsageBasedLicense.forKeyShot(10), [27003, 2703]], - [UsageBasedLicense.forKrakatoa(10), [27000, 2700]], - [UsageBasedLicense.forMantra(10), [1716]], - [UsageBasedLicense.forMaxwell(10), [5055, 7055]], - [UsageBasedLicense.forMaya(10), [27002, 2702]], - [UsageBasedLicense.forNuke(10), [4101, 6101]], - [UsageBasedLicense.forRealFlow(10), [5055, 7055]], - [UsageBasedLicense.forRedShift(10), [5054, 7054]], - [UsageBasedLicense.forVray(10), [30306]], - [UsageBasedLicense.forYeti(10), [5053, 7053]], -])('Test open port for license type', ( license: UsageBasedLicense, ports: number[]) => { - // WHEN - const licenseForwarder = new UsageBasedLicensing(stack, 'licenseForwarder', { - vpc, - certificateSecret: certSecret, - instanceType: InstanceType.of(InstanceClass.C5, InstanceSize.LARGE), - licenses: [ - license, - ], - memoryLimitMiB: 2 * 1024, - renderQueue, - images, + SourceSecurityGroupId: { + 'Fn::ImportValue': stringLike(`${Stack.of(workerFleet)}:ExportsOutputFnGetAttworkerFleetInstanceSecurityGroupB00C2885GroupId60416F0A`), + }, + })); + }); + }); + + test('requires one usage based license', () => { + // Without any licenses + expect(() => { + new UsageBasedLicensing(dependencyStack, 'licenseForwarder', { + vpc, + images, + certificateSecret: certificateSecret, + licenses: [], + renderQueue, + }); + }).toThrowError('Should be specified at least one license with defined limit.'); + }); }); - licenseForwarder.grantPortAccess(workerFleet, [license]); + describe('configures render queue', () => { + test('adds ingress rule for asg', () => { + const ublAsgSg = ubl.asg.connections.securityGroups[0].node.defaultChild as CfnElement; - // THEN - ports.forEach( port => { - expectCDK(stack).to(haveResourceLike('AWS::EC2::SecurityGroupIngress', { - IpProtocol: 'tcp', - ToPort: port, - })); - }); -}); - -// Without any licenses -expect(() => { - new UsageBasedLicensing(stack, 'licenseForwarder', { - vpc, - images, - memoryLimitMiB: 2 * 1024, - certificateSecret: certSecret, - licenses: [], - renderQueue, + expectCDK(stack).to(haveResourceLike('AWS::EC2::SecurityGroupIngress', { + IpProtocol: 'tcp', + FromPort: 8080, + ToPort: 8080, + GroupId: { + 'Fn::ImportValue': stringLike(`${Stack.of(renderQueue).stackName}:ExportsOutputFnGetAttRQNonDefaultPortLBSecurityGroup*`), + }, + SourceSecurityGroupId: { + 'Fn::GetAtt': [ + Stack.of(ubl).getLogicalId(ublAsgSg), + 'GroupId', + ], + }, + })); + }); + + test('sets RENDER_QUEUE_URI environment variable', () => { + // THEN + expectCDK(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ + { + Environment: arrayWith( + { + Name: 'RENDER_QUEUE_URI', + Value: { + 'Fn::Join': [ + '', + [ + 'http://', + { + 'Fn::ImportValue': stringLike(`${Stack.of(renderQueue).stackName}:ExportsOutputFnGetAttRQNonDefaultPortLB*`), + }, + ':8080', + ], + ], + }, + }, + ), + }, + ], + })); + }); }); -}).toThrowError('Should be specified at least one license with defined limit.'); +}); \ No newline at end of file