diff --git a/examples/deadline/All-In-AWS-Infrastructure-Basic/python/package/lib/service_tier.py b/examples/deadline/All-In-AWS-Infrastructure-Basic/python/package/lib/service_tier.py index fa4886638..c71679495 100644 --- a/examples/deadline/All-In-AWS-Infrastructure-Basic/python/package/lib/service_tier.py +++ b/examples/deadline/All-In-AWS-Infrastructure-Basic/python/package/lib/service_tier.py @@ -44,6 +44,7 @@ ThinkboxDockerRecipes, UsageBasedLicense, UsageBasedLicensing, + VersionQuery, ) @@ -121,10 +122,10 @@ def __init__(self, scope: Construct, stack_id: str, *, props: ServiceTierProps, self, 'Repository', vpc=props.vpc, - version=recipes.version, database=props.database, file_system=props.file_system, - repository_installation_timeout=Duration.minutes(20) + repository_installation_timeout=Duration.minutes(20), + version=recipes.version, ) server_cert = X509CertificatePem( @@ -142,7 +143,6 @@ def __init__(self, scope: Construct, stack_id: str, *, props: ServiceTierProps, self, 'RenderQueue', vpc=props.vpc, - version=recipes.version, images=recipes.render_queue_images, repository=repository, hostname=RenderQueueHostNameProps( @@ -155,6 +155,7 @@ def __init__(self, scope: Construct, stack_id: str, *, props: ServiceTierProps, ), internal_protocol=ApplicationProtocol.HTTPS ), + version=recipes.version, # TODO - Evaluate deletion protection for your own needs. This is set to false to # cleanly remove everything when this stack is destroyed. If you would like to ensure # that this resource is not accidentally deleted, you should set this to true. diff --git a/examples/deadline/All-In-AWS-Infrastructure-Basic/ts/lib/service-tier.ts b/examples/deadline/All-In-AWS-Infrastructure-Basic/ts/lib/service-tier.ts index e2c0cb605..d8694de7a 100644 --- a/examples/deadline/All-In-AWS-Infrastructure-Basic/ts/lib/service-tier.ts +++ b/examples/deadline/All-In-AWS-Infrastructure-Basic/ts/lib/service-tier.ts @@ -16,7 +16,7 @@ import { IPrivateHostedZone, } from '@aws-cdk/aws-route53'; import * as cdk from '@aws-cdk/core'; -import { +import { IMountableLinuxFilesystem, X509CertificatePem, } from 'aws-rfdk'; @@ -30,8 +30,8 @@ import { UsageBasedLicense, UsageBasedLicensing, } from 'aws-rfdk/deadline'; -import { - Secret +import { + Secret, } from '@aws-cdk/aws-secretsmanager'; import { Duration } from '@aws-cdk/core'; @@ -150,7 +150,6 @@ export class ServiceTier extends cdk.Stack { }); this.renderQueue = new RenderQueue(this, 'RenderQueue', { vpc: props.vpc, - version: recipes.version, images: recipes.renderQueueImages, repository: repository, hostname: { @@ -163,6 +162,7 @@ export class ServiceTier extends cdk.Stack { }, internalProtocol: ApplicationProtocol.HTTPS, }, + version: recipes.version, // TODO - Evaluate deletion protection for your own needs. This is set to false to // cleanly remove everything when this stack is destroyed. If you would like to ensure // that this resource is not accidentally deleted, you should set this to true. @@ -170,7 +170,7 @@ export class ServiceTier extends cdk.Stack { }); this.renderQueue.connections.allowDefaultPortFrom(this.bastion); - const ublCertSecret = Secret.fromSecretArn(this, 'UBLCertsSecret', props.ublCertsSecretArn); + const ublCertSecret = Secret.fromSecretArn(this, 'UBLCertsSecret', props.ublCertsSecretArn); this.ublLicensing = new UsageBasedLicensing(this, 'UBLLicensing', { vpc: props.vpc, images: recipes.ublImages, diff --git a/integ/components/deadline/deadline_01_repository/bin/deadline_01_repository.ts b/integ/components/deadline/deadline_01_repository/bin/deadline_01_repository.ts index 02880ca37..a5a926ae3 100644 --- a/integ/components/deadline/deadline_01_repository/bin/deadline_01_repository.ts +++ b/integ/components/deadline/deadline_01_repository/bin/deadline_01_repository.ts @@ -4,6 +4,8 @@ */ import { App, Stack } from '@aws-cdk/core'; +import { VersionQuery } from 'aws-rfdk/deadline'; + import { DatabaseType, StorageStruct } from '../../../../lib/storage-struct'; import { RepositoryTestingTier } from '../lib/repository-testing-tier'; @@ -17,17 +19,23 @@ const integStackTag = process.env.INTEG_STACK_TAG!.toString(); const componentTier = new Stack(app, 'RFDKInteg-DL-ComponentTier' + integStackTag, {env}); +// This will get the installers for the latest version of Deadline +const version = new VersionQuery(componentTier, 'VersionQuery'); + const structs: Array = [ new StorageStruct(componentTier, 'StorageStruct1', { integStackTag, + version, }), new StorageStruct(componentTier, 'StorageStruct2', { integStackTag, databaseType: DatabaseType.DocDB, + version, }), new StorageStruct(componentTier, 'StorageStruct3', { integStackTag, databaseType: DatabaseType.MongoDB, + version, }), ]; diff --git a/integ/components/deadline/deadline_02_renderQueue/bin/deadline_02_renderQueue.ts b/integ/components/deadline/deadline_02_renderQueue/bin/deadline_02_renderQueue.ts index 423fdd948..48b4d76b1 100644 --- a/integ/components/deadline/deadline_02_renderQueue/bin/deadline_02_renderQueue.ts +++ b/integ/components/deadline/deadline_02_renderQueue/bin/deadline_02_renderQueue.ts @@ -4,6 +4,8 @@ */ import { App, Stack } from '@aws-cdk/core'; +import { VersionQuery } from 'aws-rfdk/deadline'; + import { RenderStruct } from '../../../../lib/render-struct'; import { DatabaseType, StorageStruct } from '../../../../lib/storage-struct'; import { RenderQueueTestingTier } from '../lib/renderQueue-testing-tier'; @@ -20,10 +22,14 @@ const integStackTag = process.env.INTEG_STACK_TAG!.toString(); // Create component stack const componentTier = new Stack(app, 'RFDKInteg-RQ-ComponentTier' + integStackTag, {env}); +// This will get the installers for the latest version of Deadline +const version = new VersionQuery(componentTier, 'VersionQuery'); + // Add struct containing Deadline repository (the same repo is used for all test configurations) const storage = new StorageStruct(componentTier, 'StorageStruct', { integStackTag, databaseType: DatabaseType.DocDB, + version, }); const structs: Array = [ @@ -32,12 +38,14 @@ const structs: Array = [ integStackTag, repository: storage.repo, protocol: 'http', + version, }), //Create test struct for Render Queue in https mode new RenderStruct(componentTier, 'RenderStructRQ2', { integStackTag, repository: storage.repo, protocol: 'https', + version, }), ]; diff --git a/integ/components/deadline/deadline_03_workerFleet/bin/deadline_03_workerFleet.ts b/integ/components/deadline/deadline_03_workerFleet/bin/deadline_03_workerFleet.ts index a597ea7ea..82c15b96c 100644 --- a/integ/components/deadline/deadline_03_workerFleet/bin/deadline_03_workerFleet.ts +++ b/integ/components/deadline/deadline_03_workerFleet/bin/deadline_03_workerFleet.ts @@ -4,6 +4,8 @@ */ import { App, Stack } from '@aws-cdk/core'; +import { VersionQuery } from 'aws-rfdk/deadline'; + import { RenderStruct } from '../../../../lib/render-struct'; import { DatabaseType, StorageStruct } from '../../../../lib/storage-struct'; import { WorkerStruct } from '../../../../lib/worker-struct'; @@ -29,16 +31,22 @@ oss.forEach( os => { const testId = 'WF' + i.toString(); // Create component stack for structs const componentTier = new Stack(app, 'RFDKInteg-' + testId + '-ComponentTier' + integStackTag, {env}); + + // This will get the installers for the latest version of Deadline + const version = new VersionQuery(componentTier, 'VersionQuery'); + // Create StorageStruct with repository const storage = new StorageStruct(componentTier, 'StorageStruct' + testId, { integStackTag, databaseType: DatabaseType.DocDB, + version, }); // Create render queue with either HTTP or HTTPS protocol const render = new RenderStruct(componentTier, 'RenderStruct' + testId, { integStackTag, repository: storage.repo, protocol, + version, }); // Create worker struct containing three nodes using either Linux or Windows structs.push(new WorkerStruct(componentTier, 'WorkerStruct' + testId, { diff --git a/integ/lib/render-struct.ts b/integ/lib/render-struct.ts index 6d306f008..f5b944b9a 100644 --- a/integ/lib/render-struct.ts +++ b/integ/lib/render-struct.ts @@ -8,12 +8,19 @@ import { ApplicationProtocol } from '@aws-cdk/aws-elasticloadbalancingv2'; import { PrivateHostedZone } from '@aws-cdk/aws-route53'; import { Construct, Stack } from '@aws-cdk/core'; import { X509CertificatePem } from 'aws-rfdk'; -import { IRepository, RenderQueue, Stage, ThinkboxDockerRecipes } from 'aws-rfdk/deadline'; +import { + IRepository, + IVersion, + RenderQueue, + Stage, + ThinkboxDockerRecipes, +} from 'aws-rfdk/deadline'; export interface RenderStructProps { readonly integStackTag: string; readonly repository: IRepository; readonly protocol: string; + readonly version: IVersion } export class RenderStruct extends Construct { @@ -79,11 +86,11 @@ export class RenderStruct extends Construct { vpc, repository: props.repository, images: recipes.renderQueueImages, - version: recipes.version, logGroupProps: { logGroupPrefix: Stack.of(this).stackName + '-' + id, }, hostname, + version: props.version, trafficEncryption, deletionProtection: false, }; diff --git a/integ/lib/storage-struct.ts b/integ/lib/storage-struct.ts index 710c875c0..ca7b79dd0 100644 --- a/integ/lib/storage-struct.ts +++ b/integ/lib/storage-struct.ts @@ -4,13 +4,36 @@ */ import { DatabaseCluster } from '@aws-cdk/aws-docdb'; -import { InstanceClass, InstanceSize, InstanceType, Vpc, SubnetType } from '@aws-cdk/aws-ec2'; +import { + InstanceClass, + InstanceSize, + InstanceType, + Vpc, + SubnetType, +} from '@aws-cdk/aws-ec2'; import { FileSystem } from '@aws-cdk/aws-efs'; import { PrivateHostedZone } from '@aws-cdk/aws-route53'; import { ISecret } from '@aws-cdk/aws-secretsmanager'; -import { Construct, Duration, RemovalPolicy, Stack } from '@aws-cdk/core'; -import { MongoDbInstance, MongoDbPostInstallSetup, MongoDbSsplLicenseAcceptance, MongoDbVersion, MountableEfs, X509CertificatePem, X509CertificatePkcs12 } from 'aws-rfdk'; -import { DatabaseConnection, Repository, Stage, ThinkboxDockerRecipes } from 'aws-rfdk/deadline'; +import { + Construct, + Duration, + RemovalPolicy, + Stack, +} from '@aws-cdk/core'; +import { + MongoDbInstance, + MongoDbPostInstallSetup, + MongoDbSsplLicenseAcceptance, + MongoDbVersion, + MountableEfs, + X509CertificatePem, + X509CertificatePkcs12, +} from 'aws-rfdk'; +import { + DatabaseConnection, + IVersion, + Repository, +} from 'aws-rfdk/deadline'; // Interface for supplying database connection and accompanying secret for credentials @@ -27,6 +50,7 @@ export enum DatabaseType { export interface StorageStructProps { readonly integStackTag: string; + readonly version: IVersion; readonly databaseType?: DatabaseType; } @@ -44,17 +68,10 @@ export class StorageStruct extends Construct { userAcceptsSSPL === 'true' ? MongoDbSsplLicenseAcceptance.USER_ACCEPTS_SSPL : MongoDbSsplLicenseAcceptance.USER_REJECTS_SSPL; const infrastructureStackName = 'RFDKIntegInfrastructure' + props.integStackTag; - const stagePath = process.env.DEADLINE_STAGING_PATH!.toString(); // Get farm VPC from lookup const vpc = Vpc.fromLookup(this, 'Vpc', { tags: { StackName: infrastructureStackName }}) as Vpc; - // Create recipes object used to prepare the Deadline installers - const recipes = new ThinkboxDockerRecipes(this, 'DockerRecipes', { - stage: Stage.fromDirectory(stagePath), - }); - const version = recipes.version; - let cacert; let database; let databaseConnection; @@ -188,7 +205,7 @@ export class StorageStruct extends Construct { vpc, database: databaseConnection, fileSystem: deadlineMountableEfs, - version: version, + version: props.version, repositoryInstallationTimeout: Duration.minutes(20), logGroupProps: { logGroupPrefix: Stack.of(this).stackName + '-' + id, diff --git a/packages/aws-rfdk/lib/core/lambdas/nodejs/lib/version-provider/index.ts b/packages/aws-rfdk/lib/core/lambdas/nodejs/lib/version-provider/index.ts index 180ad2437..2e5e872ea 100644 --- a/packages/aws-rfdk/lib/core/lambdas/nodejs/lib/version-provider/index.ts +++ b/packages/aws-rfdk/lib/core/lambdas/nodejs/lib/version-provider/index.ts @@ -3,4 +3,5 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './version-provider'; +export * from './version'; +export * from './version-provider'; \ No newline at end of file diff --git a/packages/aws-rfdk/bin/index-test.json b/packages/aws-rfdk/lib/core/lambdas/nodejs/lib/version-provider/test/index-test.json similarity index 100% rename from packages/aws-rfdk/bin/index-test.json rename to packages/aws-rfdk/lib/core/lambdas/nodejs/lib/version-provider/test/index-test.json diff --git a/packages/aws-rfdk/lib/core/lambdas/nodejs/lib/version-provider/test/version-provider.test.ts b/packages/aws-rfdk/lib/core/lambdas/nodejs/lib/version-provider/test/version-provider.test.ts index 6ecebe3b6..026463d96 100644 --- a/packages/aws-rfdk/lib/core/lambdas/nodejs/lib/version-provider/test/version-provider.test.ts +++ b/packages/aws-rfdk/lib/core/lambdas/nodejs/lib/version-provider/test/version-provider.test.ts @@ -3,47 +3,22 @@ * SPDX-License-Identifier: Apache-2.0 */ -/* eslint-disable no-console */ /* eslint-disable dot-notation */ -import { Platform, Product, VersionProvider } from '../version-provider'; +import * as path from 'path'; -const versionProvider = new VersionProvider('bin/index-test.json'); +import { Version } from '../version'; +import { + Platform, + Product, + VersionProvider, +} from '../version-provider'; + +const versionProvider = new VersionProvider(path.join(__dirname, 'index-test.json')); const indexTest = versionProvider['readInstallersIndex'](); const productSection = indexTest[Product.deadline]; -test('version parsing', () => { - const result = versionProvider['parseVersionString']('10.1.10.6'); - - expect(result).not.toBeNull(); - - if (result === null) { return; } - expect(result[0]).toEqual('10.1.10.6'); - expect(result[1]).toEqual('10'); - expect(result[2]).toEqual('1'); - expect(result[3]).toEqual('10'); - expect(result[4]).toEqual('6'); -}); - -test('partial version parsing', () => { - const result = versionProvider['parseVersionString']('10.1'); - - expect(result).not.toBeNull(); - - if (result === null) { return; } - expect(result[0]).toEqual('10.1'); - expect(result[1]).toEqual('10'); - expect(result[2]).toEqual('1'); - expect(result[3]).toBeUndefined(); - expect(result[4]).toBeUndefined(); -}); - -test.each(['10.1.9.2.1', '10.', '10.1.', '10.-1', 'a.b.c'])('incorrect version %s parsing', (versionString: string) => { - const result = versionProvider['parseVersionString'](versionString); - expect(result).toBeNull(); -}); - test.each([[Platform.linux, '10.1.9.2'], [Platform.mac, '10.1.9.2'], [Platform.windows, '10.1.8.5'], @@ -67,21 +42,26 @@ test.each([ [Platform.mac, { bundle: 's3://thinkbox-installers/Deadline/10.1.9.2/Mac/Deadline-10.1.9.2-osx-installers.dmg', } ], -])('get Uri for platform', (platform: Platform, versionedUris: any) => { - versionProvider['getUrisForPlatform']( +])('get Uri for platform', async (platform: Platform, versionedUris: any) => { + const result = versionProvider['getUrisForPlatform']( Product.deadline, productSection, platform, '10.1.9.2', - ).then(result => { - expect(result).not.toBeNull(); - - expect(result?.Uris).toEqual(versionedUris); - }, - ).catch(error => { - process.stderr.write(`${error.toString()}\n`); - process.exit(1); - }); + ); + + expect(result).not.toBeNull(); + expect(result?.Uris).toEqual(versionedUris); +}); + +test('get Uri for platform - bad version', async () => { + const badVersion = 'badVersionString'; + expect(() => versionProvider['getUrisForPlatform']( + Product.deadline, + productSection, + Platform.linux, + badVersion, + )).toThrowError(`Couldn't parse version from ${badVersion}`); }); test('get deadline version', async () => { @@ -160,38 +140,6 @@ test('get deadline version for all platforms', async () => { expect(windowsInstallerVersion?.PatchVersion).toEqual('5'); }); -test('validate correct input', async () => { - expect(versionProvider.implementsIVersionProviderProperties({ - product: Product.deadline, - versionString: '10.1.9.2', - platform: 'linux', - })).toBeTruthy(); -}); - -test('validate non-object input', async () => { - expect(versionProvider['implementsIVersionProviderProperties']('test')).toEqual(false); -}); - -test('validate input without product', async () => { - expect(versionProvider.implementsIVersionProviderProperties({ - versionString: 'version', - })).toEqual(false); -}); - -test('validate input with invalid versionString', async () => { - expect(versionProvider.implementsIVersionProviderProperties({ - product: Product.deadline, - versionString: 'version', - })).toEqual(false); -}); - -test('validate input with invalid platform', async () => { - expect(versionProvider['implementsIVersionProviderProperties']({ - product: Product.deadline, - platform: 'test', - })).toEqual(false); -}); - test('not defined file path', () => { expect(() => (new VersionProvider())['readInstallersIndex']()).toThrowError(/File path should be defined./); }); @@ -205,39 +153,63 @@ test('get latest version without latest section', () => { }); test('get latest version without informtion for platform', () => { - expect(() => versionProvider['getLatestVersion']('linux',{latest: {}})).toThrowError(/Information about latest version for platform linux can not be found/); + expect(() => versionProvider['getLatestVersion']('linux',{ latest: {} })).toThrowError(/Information about latest version for platform linux can not be found/); }); test('get requested Uri version for existing product.', () => { - const requestedVersion = versionProvider['parseVersionString']('10.1.9.2'); - expect(versionProvider['getRequestedUriVersion'](requestedVersion, { - 10: { - 1: { - 9: { - 2: { - linux: 's3://thinkbox-installers/DeadlineDocker/10.1.9.2/DeadlineDocker-10.1.9.2.tar.gz', + const requestedVersion = Version.parseFromVersionString('10.1.9.2'); + + expect(requestedVersion).not.toBeNull(); + if (requestedVersion === null) { + return; + } + + expect(versionProvider['getRequestedUriVersion']( + requestedVersion, + { + 10: { + 1: { + 9: { + 2: { + linux: 's3://thinkbox-installers/DeadlineDocker/10.1.9.2/DeadlineDocker-10.1.9.2.tar.gz', + }, }, }, }, - }}, Platform.linux, Product.deadlineDocker )).toEqual({ + }, + Platform.linux, + Product.deadlineDocker, + )).toEqual({ MajorVersion: '10', MinorVersion: '1', ReleaseVersion: '9', PatchVersion: '2', - Uris: {bundle: 's3://thinkbox-installers/DeadlineDocker/10.1.9.2/DeadlineDocker-10.1.9.2.tar.gz'}, + Uris: { bundle: 's3://thinkbox-installers/DeadlineDocker/10.1.9.2/DeadlineDocker-10.1.9.2.tar.gz' }, }); }); test('get requested Uri version for not existing product.', () => { - const requestedVersion = versionProvider['parseVersionString']('10.1.9.2'); - expect(versionProvider['getRequestedUriVersion'](requestedVersion, { - 10: { - 1: { - 9: { - 2: { - linux: 's3://thinkbox-installers/DeadlineDocker/10.1.9.2/DeadlineDocker-10.1.9.2.tar.gz', + const requestedVersion = Version.parseFromVersionString('10.1.9.2'); + + expect(requestedVersion).not.toBeNull(); + if (requestedVersion === null) { + return; + } + + expect(versionProvider['getRequestedUriVersion']( + requestedVersion, + { + 10: { + 1: { + 9: { + 2: { + linux: 's3://thinkbox-installers/DeadlineDocker/10.1.9.2/DeadlineDocker-10.1.9.2.tar.gz', + }, }, }, }, - }}, Platform.windows, Product.deadlineDocker )).toEqual(undefined); + }, + Platform.windows, + Product.deadlineDocker, + )).toEqual(undefined); }); diff --git a/packages/aws-rfdk/lib/core/lambdas/nodejs/lib/version-provider/version-provider.ts b/packages/aws-rfdk/lib/core/lambdas/nodejs/lib/version-provider/version-provider.ts index 7f9b76a8a..1e6717f13 100644 --- a/packages/aws-rfdk/lib/core/lambdas/nodejs/lib/version-provider/version-provider.ts +++ b/packages/aws-rfdk/lib/core/lambdas/nodejs/lib/version-provider/version-provider.ts @@ -3,13 +3,13 @@ * SPDX-License-Identifier: Apache-2.0 */ -/* eslint-disable no-console */ - import * as fs from 'fs'; -import * as http from 'http'; +import { IncomingMessage } from 'http'; import * as https from 'https'; import * as url from 'url'; +import { Version } from './version'; + export enum Platform { linux = 'linux', @@ -70,25 +70,24 @@ export interface IVersionedUris { } /** - * The version provider parse index JSON which can be downloaded or loaded from local file - * and returns URIs for specific product. + * The version provider parses a JSON file containing version information for the Deadline and DockerDeadline products. + * It can be downloaded or loaded from local file and returns URIs for the specific products. * By default returns the last version of URIs or specified full or partial version. * If platform is not defined returns URIs for each platform. */ export class VersionProvider { + private static readonly VERSION_INDEX_URL = 'https://downloads.thinkboxsoftware.com/version_info.json'; + private readonly indexFilePath: string|undefined; - private readonly VALID_VERSION_REGEX = /^(0|[1-9]\d*)(?:\.(0|[1-9]\d*))?(?:\.(0|[1-9]\d*))?(?:\.(0|[1-9]\d*))?$/; + constructor(indexFilePath?: string) { this.indexFilePath = indexFilePath; } /** * Returns URIs for specified product - * - * @param resourceProperties */ public async getVersionUris(resourceProperties: IVersionProviderProperties): Promise> { - /* istanbul ignore next */ const indexJson = this.indexFilePath ? this.readInstallersIndex() : await this.downloadInstallerIndex(); const productSection = indexJson[resourceProperties.product]; @@ -99,7 +98,7 @@ export class VersionProvider { let installers = new Map(); if (resourceProperties.platform) { - const versionedUris = await this.getUrisForPlatform( + const versionedUris = this.getUrisForPlatform( resourceProperties.product, productSection, resourceProperties.platform, @@ -108,9 +107,10 @@ export class VersionProvider { if (versionedUris) { installers.set(resourceProperties.platform, versionedUris); } + } else { Object.values(Platform).forEach(async p => { - const versionedUris = await this.getUrisForPlatform( + const versionedUris = this.getUrisForPlatform( resourceProperties.product, productSection, p, @@ -125,29 +125,8 @@ export class VersionProvider { return installers; } - public implementsIVersionProviderProperties(value: any): boolean { - if (!value || typeof(value) !== 'object') { return false; } - - if (!value.product || !Object.values(Product).includes(value.product)) { - return false; - } - - if (value.versionString) { - if (null === this.parseVersionString(value.versionString)) { return false; } - } - - if (value.platform) { - if (!Object.values(Platform).includes(value.platform.toLowerCase())) { return false; } - } - - return true; - } - - /* istanbul ignore next */ // @ts-ignore private async downloadInstallerIndex() { - const productionInfoURL = 'https://downloads.thinkboxsoftware.com/version_info.json'; - - const parsedUrl = url.parse(productionInfoURL); + const parsedUrl = url.parse(VersionProvider.VERSION_INDEX_URL); const options = { host: parsedUrl.hostname, @@ -155,7 +134,7 @@ export class VersionProvider { }; return new Promise((resolve, reject) => { - https.get(options, (res: http.IncomingMessage) => { + https.get(options, (res: IncomingMessage) => { let json = ''; res.on('data', (chunk: any) => { @@ -200,29 +179,22 @@ export class VersionProvider { return json; } - private parseVersionString(versionString: string): RegExpExecArray | null { - return this.VALID_VERSION_REGEX.exec(versionString); - } - /** - * This method returns IVersionedUris for specific platform - * - * @param product - * @param productSection - * @param platform - * @param version + * This method returns IVersionedUris (the patch version plus installer URI's) for a specific platform. */ - private async getUrisForPlatform( + private getUrisForPlatform( product: Product, productSection: any, platform: Platform, version?: string, - ): Promise { + ): IVersionedUris | undefined { const versionString: string = version ? version : this.getLatestVersion(platform, productSection); + const requestedVersion = Version.parseFromVersionString(versionString); - const requestedVersion = this.parseVersionString( versionString ); + if (!requestedVersion) { + throw new Error(`Couldn't parse version from ${versionString}`); + } - // Based on the requested version, fetches the latest patch and its installer file paths. return this.getRequestedUriVersion( requestedVersion, productSection.versions, @@ -233,9 +205,6 @@ export class VersionProvider { /** * This method returns the latest version for specified platform. - * - * @param platform - * @param indexedVersionInfo */ private getLatestVersion(platform: string, indexedVersionInfo: any): string { const latestSection = indexedVersionInfo.latest; @@ -258,17 +227,13 @@ export class VersionProvider { * with the indexed info. * If any of the requested version number is missing, it fetches the latest * (highest) available version for it. - * - * @param requestedVersion - * @param indexedVersionInfo */ private getRequestedUriVersion( - requestedVersion: RegExpExecArray | null, + requestedVersion: string[], indexedVersionInfo: any, platform: Platform, product: Product, ): IVersionedUris | undefined { - let versionMap = indexedVersionInfo; const versionArray: string[] = []; @@ -276,7 +241,7 @@ export class VersionProvider { // and get the matching version from the indexed version map. for (let versionIndex = 0; versionIndex < 4; versionIndex++) { let version: string; - if (requestedVersion?.[versionIndex + 1] == null) { + if (requestedVersion[versionIndex + 1] == null) { // version is not provided, get the max version. const numberValues: number[] = (Object.keys(versionMap)).map((val: string) => { @@ -293,18 +258,18 @@ export class VersionProvider { let uriIndex: IUris | undefined; if ((platform in versionMap)) { + const platformVersionMap = versionMap[platform]; if (product == Product.deadline) { - const platformVersion = versionMap[platform]; uriIndex = { - bundle: platformVersion.bundle, - clientInstaller: versionMap[platform].clientInstaller, - repositoryInstaller: versionMap[platform].repositoryInstaller, - certificateInstaller: versionMap[platform].certificateInstaller, + bundle: platformVersionMap.bundle, + clientInstaller: platformVersionMap.clientInstaller, + repositoryInstaller: platformVersionMap.repositoryInstaller, + certificateInstaller: platformVersionMap.certificateInstaller, }; } else { // Product.deadlineDocker uriIndex = { - bundle: versionMap[platform], + bundle: platformVersionMap, }; } } diff --git a/packages/aws-rfdk/lib/core/lambdas/nodejs/lib/version-provider/version.ts b/packages/aws-rfdk/lib/core/lambdas/nodejs/lib/version-provider/version.ts new file mode 100644 index 000000000..833740c0d --- /dev/null +++ b/packages/aws-rfdk/lib/core/lambdas/nodejs/lib/version-provider/version.ts @@ -0,0 +1,35 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Utility class for validating and converting the version number to or from a version string + */ +export class Version { + public static validateVersionString(versionString: string): boolean { + if (null === this.parseFromVersionString(versionString)) { return false; } + return true; + } + + public static parseFromVersionString(versionString: string): RegExpExecArray | null { + return Version.VALID_VERSION_REGEX.exec(versionString); + } + + public static convertToFullVersionString(major: string, minor: string, release: string, patch: string): string { + const majorNumber = Number(major); + const minorNumber = Number(minor); + const releaseNumber = Number(release); + const patchNumber = Number(patch); + + if (isNaN(majorNumber) || majorNumber < 0 + || isNaN(minorNumber) || minorNumber < 0 + || isNaN(releaseNumber) || releaseNumber < 0 + || isNaN(patchNumber) || patchNumber < 0) { + throw new Error(`A component of the version was not in the correct format: ${major}.${minor}.${release}.${patch}`); + } + return `${major}.${minor}.${release}.${patch}`; + } + + private static readonly VALID_VERSION_REGEX = /^(0|[1-9]\d*)(?:\.(0|[1-9]\d*))?(?:\.(0|[1-9]\d*))?(?:\.(0|[1-9]\d*))?$/; +} diff --git a/packages/aws-rfdk/lib/core/lambdas/nodejs/version-provider/handler.ts b/packages/aws-rfdk/lib/core/lambdas/nodejs/version-provider/handler.ts index dccb8482f..69a6e31a6 100644 --- a/packages/aws-rfdk/lib/core/lambdas/nodejs/version-provider/handler.ts +++ b/packages/aws-rfdk/lib/core/lambdas/nodejs/version-provider/handler.ts @@ -8,51 +8,175 @@ import { LambdaContext } from '../lib/aws-lambda'; import { CfnRequestEvent, SimpleCustomResource } from '../lib/custom-resource'; import { - VersionProvider, - IVersionProviderProperties, - IVersionedUris, Platform, + Product, + Version, + VersionProvider, } from '../lib/version-provider'; +/** + * The input to this Custom Resource + */ +export interface IVersionProviderResourceProperties { + /** + * The version of Deadline to look up. + */ + readonly versionString?: string, +} + +/** + * Output of this Custom Resource, it is all made up of a flat structure of strings to avoid issues with how the + * results are tokenized by CDK to be used in constructs. + */ +export interface FlatVersionedUriOutput { + /** + * The S3 bucket holding the installers. This makes the assumption they are all in the same bucket. + */ + readonly S3Bucket: string; + + /** + * The major version of the Linux installer. For example, "a" in "a.b.c.d". + */ + readonly MajorVersion: string; + + /** + * The minor version of the Linux installer. For example, "b" in "a.b.c.d". + */ + readonly MinorVersion: string; + + /** + * The release version of the Linux installer. For example, "c" in "a.b.c.d". + */ + readonly ReleaseVersion: string; + + /** + * The patch version of the Linux installer. For example, "d" in "a.b.c.d". + */ + readonly LinuxPatchVersion: string; + + /** + * The patch version of the Linux installer. For example, "d" in "a.b.c.d". + */ + readonly LinuxFullVersionString: string; + /** + * The object key of the Deadline repository installer for Linux. + */ + readonly LinuxRepositoryInstaller: string; +} + +/** + * This custom resource will parse and return the S3 URI's of the Deadline installers and Docker recipes for use in + * any constructs that need to install Deadline. + */ export class VersionProviderResource extends SimpleCustomResource { readonly versionProvider: VersionProvider; - constructor(indexFilePath?: string) { + constructor() { super(); - this.versionProvider = new VersionProvider(indexFilePath); + this.versionProvider = new VersionProvider(); } /** * @inheritdoc */ - /* istanbul ignore next */ // @ts-ignore public validateInput(data: object): boolean { - return this.versionProvider.implementsIVersionProviderProperties(data); + return this.implementsIVersionProviderResourceProperties(data); } /** * @inheritdoc */ // @ts-ignore -- we do not use the physicalId - public async doCreate(physicalId: string, resourceProperties: IVersionProviderProperties): Promise> { - return await this.versionProvider.getVersionUris(resourceProperties); + public async doCreate(physicalId: string, resourceProperties: IVersionProviderResourceProperties): Promise { + const deadlinePlatFormVersionedUris = await this.versionProvider.getVersionUris({ + versionString: resourceProperties.versionString, + platform: Platform.linux, + product: Product.deadline, + }); + + const deadlineLinux = deadlinePlatFormVersionedUris.get(Platform.linux)!; + const deadlineLinuxUris = deadlineLinux.Uris; + + const s3Bucket = this.parseS3BucketName(deadlineLinuxUris.bundle); + const linuxRepoObjectKey = this.parseS3ObjectKey(deadlineLinuxUris.repositoryInstaller!); + + return { + S3Bucket: s3Bucket, + MajorVersion: deadlineLinux.MajorVersion, + MinorVersion: deadlineLinux.MinorVersion, + ReleaseVersion: deadlineLinux.ReleaseVersion, + LinuxPatchVersion: deadlineLinux.PatchVersion, + LinuxFullVersionString: Version.convertToFullVersionString( + deadlineLinux.MajorVersion, + deadlineLinux.MinorVersion, + deadlineLinux.ReleaseVersion, + deadlineLinux.PatchVersion, + ), + LinuxRepositoryInstaller: linuxRepoObjectKey, + }; } /** * @inheritdoc */ /* istanbul ignore next */ // @ts-ignore - public async doDelete(physicalId: string, resourceProperties: IVersionProviderProperties): Promise { + public async doDelete(physicalId: string, resourceProperties: IVersionProviderResourceProperties): Promise { // Nothing to do -- we don't modify anything. return; } + + private implementsIVersionProviderResourceProperties(value: any): boolean { + if (!value || typeof(value) !== 'object') { return false; } + + if (value.versionString) { + if (!Version.validateVersionString(value.versionString)) { return false; } + } + + return true; + } + + /** + * Parses the S3 bucket name from an S3 URI. + */ + private parseS3BucketName(uri: string): string { + let bucketName; + try { + bucketName = this.findRegex(uri, /^s3:\/\/([A-Za-z0-9\-]+)\//)[1]; + } catch (e) { + throw new Error(`Could not parse S3 bucket name from ${uri}`); + } + return bucketName; + } + + /** + * Parses the S3 object key from an S3 URI. + */ + private parseS3ObjectKey(uri: string): string { + let objectKey; + try { + objectKey = this.findRegex(uri, /^s3:\/\/[A-Za-z0-9\-]+\/([A-Za-z0-9\-\/\.]+)$/)[1]; + } catch (e) { + throw new Error(`Could not parse S3 object key from ${uri}`); + } + return objectKey; + } + + // Assumes a single capture is in the regex + private findRegex(str: string, re: RegExp): RegExpMatchArray { + const found = str.match(re); + + if (found === null) { + throw new Error(`Couldn't find regular expression ${re} in ${str}`); + } + + return found; + } } /** * The handler used to provide the installer links for the requested version */ -/* istanbul ignore next */ export async function handler(event: CfnRequestEvent, context: LambdaContext): Promise { const versionProvider = new VersionProviderResource(); return await versionProvider.handler(event, context); diff --git a/packages/aws-rfdk/lib/core/lambdas/nodejs/version-provider/test/handler.test.ts b/packages/aws-rfdk/lib/core/lambdas/nodejs/version-provider/test/handler.test.ts new file mode 100644 index 000000000..c4e5861a0 --- /dev/null +++ b/packages/aws-rfdk/lib/core/lambdas/nodejs/version-provider/test/handler.test.ts @@ -0,0 +1,72 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* eslint-disable dot-notation */ + +import { VersionProviderResource } from '../handler'; + +describe('VersionProviderResource', () => { + let versionProviderResource: VersionProviderResource; + beforeEach(() => { + versionProviderResource = new VersionProviderResource(); + }); + + describe('implementsIVersionProviderResourceProperties', () => { + test('correct input', async () => { + expect(versionProviderResource['implementsIVersionProviderResourceProperties']({ + versionString: '10.1.9.2', + })).toBeTruthy(); + }); + + test('correct input with no versionString', async () => { + expect(versionProviderResource['implementsIVersionProviderResourceProperties']({})).toBeTruthy(); + }); + + test('non-object input', async () => { + expect(versionProviderResource['implementsIVersionProviderResourceProperties']('test')).toBeFalsy(); + }); + + test('input with invalid versionString', async () => { + expect(versionProviderResource['implementsIVersionProviderResourceProperties']({ + versionString: 'version', + })).toBeFalsy(); + }); + }); + + describe('parseS3BucketName', () => { + test('correct input', () => { + expect(versionProviderResource['parseS3BucketName']('s3://bucketName/objectKey')).toEqual('bucketName'); + }); + + test.each([ + ':/bucketName/objectKey', + 's3:/bucketName/objectKey', + 's3://bucketName', + 'bucketName', + ])('malformed input: %p', (s3Uri: string) => { + expect(() => versionProviderResource['parseS3BucketName'](s3Uri)).toThrowError(/Could not parse S3 bucket name/); + }); + }); + + describe('parseS3ObjectKey', () => { + test.each([ + ['s3://bucketName/objectKey', 'objectKey'], + ['s3://bucketName/objectDirectory/objectName', 'objectDirectory/objectName'], + ['s3://bucketName/objectDirectory/objectName.run', 'objectDirectory/objectName.run'], + ['s3://bucketName/objectDirectory/10.1.9.2/objectName.run', 'objectDirectory/10.1.9.2/objectName.run'], + ])('correct input: %p', (s3Uri: string, objectKey: string) => { + expect(versionProviderResource['parseS3ObjectKey'](s3Uri)).toEqual(objectKey); + }); + + test.each([ + ':/bucketName/objectKey', + 's3:/bucketName/objectKey', + 's3://bucketName', + 'bucketName', + ])('malformed input: %p', (s3Uri: string) => { + expect(() => versionProviderResource['parseS3ObjectKey'](s3Uri)).toThrowError(/Could not parse S3 object key/); + }); + }); +}); diff --git a/packages/aws-rfdk/lib/core/lambdas/nodejs/version-provider/test/version.test.ts b/packages/aws-rfdk/lib/core/lambdas/nodejs/version-provider/test/version.test.ts new file mode 100644 index 000000000..ea8cfc185 --- /dev/null +++ b/packages/aws-rfdk/lib/core/lambdas/nodejs/version-provider/test/version.test.ts @@ -0,0 +1,91 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Version } from '../../lib/version-provider/version'; + +describe('validateVersionString', () => { + test('correct input', () => { + expect(Version.validateVersionString('10.1.9.2')).toBeTruthy(); + }); + test('malformed input', () => { + expect(Version.validateVersionString('10.1.9.2.1')).toBeFalsy(); + }); +}); + +describe('parseVersionString', () => { + test.each([ + [ + '10.1.10.6', + [ '10', '1', '10', '6' ], + ], + [ + '10.1.9.2', + [ '10', '1', '9', '2' ], + ], + [ + '10.1.9', + [ '10', '1', '9' ], + ], + [ + '10.1', + [ '10', '1' ], + ], + [ + '10', + [ '10' ], + ], + ])('correct input: %p', (value: string, resultArray: string[]) => { + const regexResult = Version.parseFromVersionString(value); + + expect(regexResult).not.toBeNull(); + if (regexResult === null) { return; } + + expect(regexResult[0]).toEqual(value); + + for (let x = 0; x < resultArray.length; x++) { + expect(regexResult[x+1]).toEqual(resultArray[x]); + } + }); + + test.each([ + '10.1.9.2.1', + '10.', + '10.1.', + '10.-1', + 'a.b.c', + ])('incorrect version %s parsing', (versionString: string) => { + const result = Version.parseFromVersionString(versionString); + expect(result).toBeNull(); + }); +}); + +describe('convertToFullVersionString', () => { + test('correct input', () => { + expect(Version.convertToFullVersionString( + '10', + '1', + '9', + '2', + )).toEqual('10.1.9.2'); + }); + + test('negative value', () => { + expect(() => Version.convertToFullVersionString( + '10', + '-1', + '9', + '2', + )).toThrowError(/A component of the version was not in the correct format/); + }); + + test('non-numeric value', () => { + expect(() => Version.convertToFullVersionString( + '10', + 'test', + '9', + '2', + )).toThrowError(/A component of the version was not in the correct format/); + }); +}); diff --git a/packages/aws-rfdk/lib/deadline/lib/index.ts b/packages/aws-rfdk/lib/deadline/lib/index.ts index d4e42225f..7be2ac796 100644 --- a/packages/aws-rfdk/lib/deadline/lib/index.ts +++ b/packages/aws-rfdk/lib/deadline/lib/index.ts @@ -13,4 +13,5 @@ export * from './render-queue-ref'; export * from './stage'; export * from './thinkbox-docker-recipes'; export * from './version'; +export * from './version-query'; export * from './version-ref'; diff --git a/packages/aws-rfdk/lib/deadline/lib/repository.ts b/packages/aws-rfdk/lib/deadline/lib/repository.ts index 7f646cc93..23211fb41 100644 --- a/packages/aws-rfdk/lib/deadline/lib/repository.ts +++ b/packages/aws-rfdk/lib/deadline/lib/repository.ts @@ -467,8 +467,9 @@ export class Repository extends Construct implements IRepository { if (props.database) { this.databaseConnection = props.database; if (props.databaseAuditLogging !== undefined){ - this.node.addWarning(`The parameter databaseAuditLogging only has an effect when the Repository is creating its own database. - Please ensure that the Database provided is configured correctly.`); + const warningMsg = 'The parameter databaseAuditLogging only has an effect when the Repository is creating its own database.\n' + + 'Please ensure that the Database provided is configured correctly.'; + this.node.addWarning(warningMsg); } } else { const databaseAuditLogging = props.databaseAuditLogging ?? true; @@ -794,7 +795,7 @@ export class Repository extends Construct implements IRepository { if (!version.linuxInstallers?.repository) { throw new Error('Version given to Repository must provide a Linux Repository installer.'); } - const linuxVersionString = version.linuxFullVersionString(); + const linuxVersionString = version.linuxFullVersionString; if (!linuxVersionString) { throw new Error('Version given to Repository must provide a full Linux version string.'); } diff --git a/packages/aws-rfdk/lib/deadline/lib/stage.ts b/packages/aws-rfdk/lib/deadline/lib/stage.ts index cc8e6529a..16ad9e22e 100644 --- a/packages/aws-rfdk/lib/deadline/lib/stage.ts +++ b/packages/aws-rfdk/lib/deadline/lib/stage.ts @@ -10,9 +10,10 @@ import { DockerImageAsset } from '@aws-cdk/aws-ecr-assets'; import { Construct } from '@aws-cdk/core'; import { + IVersion, Version, VersionQuery, -} from './version'; +} from './'; /** * Build arguments to supply to a Docker image build @@ -197,8 +198,8 @@ export class Stage { * @param scope The parent scope * @param id The construct ID */ - public getVersion(scope: Construct, id: string) { - return VersionQuery.exactString(scope, id, this.manifest.version); + public getVersion(scope: Construct, id: string): IVersion { + return new VersionQuery(scope, id, { version: this.manifest.version }); } /** diff --git a/packages/aws-rfdk/lib/deadline/lib/thinkbox-docker-recipes.ts b/packages/aws-rfdk/lib/deadline/lib/thinkbox-docker-recipes.ts index d16b57460..dabfa940c 100644 --- a/packages/aws-rfdk/lib/deadline/lib/thinkbox-docker-recipes.ts +++ b/packages/aws-rfdk/lib/deadline/lib/thinkbox-docker-recipes.ts @@ -106,6 +106,8 @@ export class ThinkboxDockerRecipes extends Construct { this.version = props.stage.getVersion(this, 'Version'); + // this.versionQuery = props.stage.getVersion(this, 'Version'); + for (const recipe of [ThinkboxManagedDeadlineDockerRecipes.RemoteConnectionServer, ThinkboxManagedDeadlineDockerRecipes.LicenseForwarder]) { if (!props.stage.manifest.recipes[recipe]) { throw new Error(`Could not find ${recipe} recipe`); diff --git a/packages/aws-rfdk/lib/deadline/lib/version-query.ts b/packages/aws-rfdk/lib/deadline/lib/version-query.ts new file mode 100644 index 000000000..eec4271ba --- /dev/null +++ b/packages/aws-rfdk/lib/deadline/lib/version-query.ts @@ -0,0 +1,156 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { join } from 'path'; + +import { + Code, + SingletonFunction, + Runtime, +} from '@aws-cdk/aws-lambda'; +import { RetentionDays } from '@aws-cdk/aws-logs'; +import { + Bucket, +} from '@aws-cdk/aws-s3'; +import { + Construct, + CustomResource, + Duration, + Token, +} from '@aws-cdk/core'; + +import { + IVersionProviderResourceProperties, +} from '../../core/lambdas/nodejs/version-provider'; + +import { + IVersion, + PlatformInstallers, +} from './version-ref'; + +/** + * Properties for the Deadline Version + */ +export interface VersionQueryProps { + /** + * String containing the complete or partial deadline version. + * + * @default - the latest available version of deadline installer. + */ + readonly version?: string; +} + +/** + * The abstract class for new or imported(custom) Deadline Version. + */ +abstract class VersionQueryBase extends Construct implements IVersion { + /** + * @inheritdoc + */ + readonly abstract majorVersion: number; + + /** + * @inheritdoc + */ + readonly abstract minorVersion: number; + + /** + * @inheritdoc + */ + readonly abstract releaseVersion: number; + + /** + * @inheritdoc + */ + readonly abstract linuxInstallers: PlatformInstallers; + + /** + * @inheritdoc + */ + readonly abstract linuxFullVersionString: string; +} + +/** + * This class encapsulates information about a particular version of Thinkbox's Deadline software. + * Information such as the version number, and where to get installers for that version from Amazon S3. + * + * The version of an official release of Deadline is always four numeric version components separated by dots. + * ex: 10.1.8.5. We refer to the components in this version, in order from left-to-right, as the + * major, minor, release, and patch versions. For example, Deadline version 10.1.8.5 is majorVersion 10, minorVersion 1, + * releaseVersion 8, and patchVersion 5. + * + * All of the installers provided by an instance of this class must be for the same Deadline release (ex: 10.1.8), + * but the patch versions may differ between operating systems depending on the particulars of that release of Deadline. + * This class provides a simple way to query a version of Deadline prior to or during deployment of a + * CDK app. + * + * You pass an instance of this class to various Deadline constructs in this library to tell those + * constructs which version of Deadline you want them to use, and be configured for. + */ +export class VersionQuery extends VersionQueryBase { + /** + * @inheritdoc + */ + readonly majorVersion: number; + + /** + * @inheritdoc + */ + readonly minorVersion: number; + + /** + * @inheritdoc + */ + readonly releaseVersion: number; + + /** + * @inheritdoc + */ + readonly linuxFullVersionString: string; + + /** + * @inheritdoc + */ + readonly linuxInstallers: PlatformInstallers; + + constructor(scope: Construct, id: string, props?: VersionQueryProps) { + super(scope, id); + + const lambdaCode = Code.fromAsset(join(__dirname, '../..', 'core', 'lambdas', 'nodejs')); + + const lambdaFunc = new SingletonFunction(this, 'VersionProviderFunction', { + uuid: '2e19e243-16ee-4d1a-a3c9-18d35eddd446', + description: 'Used by the Version construct to get installer locations for a specific Deadline version.', + code: lambdaCode, + runtime: Runtime.NODEJS_12_X, + handler: 'version-provider.handler', + timeout: Duration.seconds(30), + logRetention: RetentionDays.ONE_WEEK, + }); + + const deadlineProperties: IVersionProviderResourceProperties = { + versionString: props?.version, + }; + + const deadlineResource = new CustomResource(this, 'DeadlineResource', { + serviceToken: lambdaFunc.functionArn, + properties: deadlineProperties, + resourceType: 'Custom::RFDK_DEADLINE_INSTALLERS', + }); + + this.majorVersion = Token.asNumber(deadlineResource.getAtt('MajorVersion')); + this.minorVersion = Token.asNumber(deadlineResource.getAtt('MinorVersion')); + this.releaseVersion = Token.asNumber(deadlineResource.getAtt('ReleaseVersion')); + + this.linuxFullVersionString = Token.asString(deadlineResource.getAtt('LinuxFullVersionString')); + this.linuxInstallers = { + patchVersion: Token.asNumber(deadlineResource.getAtt('LinuxPatchVersion')), + repository: { + objectKey: Token.asString(deadlineResource.getAtt('LinuxRepositoryInstaller')), + s3Bucket: Bucket.fromBucketName(scope, 'InstallerBucket', Token.asString(deadlineResource.getAtt('S3Bucket'))), + }, + }; + } +} diff --git a/packages/aws-rfdk/lib/deadline/lib/version-ref.ts b/packages/aws-rfdk/lib/deadline/lib/version-ref.ts index 3cedbfabe..5c24d609a 100644 --- a/packages/aws-rfdk/lib/deadline/lib/version-ref.ts +++ b/packages/aws-rfdk/lib/deadline/lib/version-ref.ts @@ -37,10 +37,8 @@ export interface PlatformInstallers { * * - DeadlineRepository-10.1.8.5-linux-x64-installer.run * - DeadlineRepository-10.1.8.5-windows-installer.exe - * - * @default No repository installer is provided. */ - readonly repository?: Installer; + readonly repository: Installer; } /** @@ -92,14 +90,12 @@ export interface IVersion extends IReleaseVersion { * * @default No installers for Linux are provided. */ - readonly linuxInstallers?: PlatformInstallers; + readonly linuxInstallers: PlatformInstallers; /** * Construct the full version string for the linux patch release referenced in * this version object. This is constructed by joining the major, minor, * release, and patch versions by dots. - * - * Will return undefined if the linuxInstallers property is undefined. */ - linuxFullVersionString(): string | undefined; + readonly linuxFullVersionString: string; } diff --git a/packages/aws-rfdk/lib/deadline/lib/version.ts b/packages/aws-rfdk/lib/deadline/lib/version.ts index cf97ad064..dfab0d18e 100644 --- a/packages/aws-rfdk/lib/deadline/lib/version.ts +++ b/packages/aws-rfdk/lib/deadline/lib/version.ts @@ -3,75 +3,10 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Bucket } from '@aws-cdk/aws-s3'; -import { Construct } from '@aws-cdk/core'; - import { IPatchVersion, - IVersion, - PlatformInstallers, } from './version-ref'; -/** - * Properties for the Deadline Version - */ -export interface VersionQueryProps { - /** - * String containing the complete or partial deadline version. - * - * @default - the latest available version of deadline installer. - */ - readonly version?: string; -} - -/** - * The abstract class for new or imported(custom) Deadline Version. - */ -abstract class VersionQueryBase extends Construct implements IVersion { - protected static readonly INSTALLER_BUCKET = 'thinkbox-installers'; - - /** - * @inheritdoc - */ - public abstract readonly majorVersion: number; - - /** - * @inheritdoc - */ - public abstract readonly minorVersion: number; - - /** - * @inheritdoc - */ - public abstract readonly releaseVersion: number; - - /** - * @inheritdoc - */ - public abstract readonly linuxInstallers?: PlatformInstallers; - - /** - * @inheritdoc - */ - public linuxFullVersionString(): string | undefined { - if (!this.linuxInstallers) { - return undefined; - } - return this.fullVersionString(this.linuxInstallers.patchVersion); - } - - protected fullVersionString(patchVersion: number): string { - return `${this.releaseVersionString}.${patchVersion}`; - } - - /** - * Helper to concatenate the major, minor, and release version values into a release version string. - */ - protected get releaseVersionString(): string { - return `${this.majorVersion}.${this.minorVersion}.${this.releaseVersion}`; - } -} - /** * This class is reposonsible to do basic operations on version format. */ @@ -152,7 +87,6 @@ export class Version implements IPatchVersion { } constructor(components: number[]) { - // validations if(components.length != 4) { throw new Error('Invalid version format. Version should contain exactly 4 components.'); } @@ -160,6 +94,9 @@ export class Version implements IPatchVersion { if (component < 0) { throw new RangeError('Invalid version format. None of the version components can be negative.'); } + if (!Number.isInteger(component)) { + throw new RangeError('Invalid version format. None of the version components can contain decimal values.'); + } }); this.components = components; @@ -231,125 +168,3 @@ export class Version implements IPatchVersion { return 0; } } - -/** - * This class encapsulates information about a particular version of Thinkbox's Deadline software. - * Information such as the version number, and where to get installers for that version from Amazon S3. - * - * The version of an official release of Deadline is always four numeric version components separated by dots. - * ex: 10.1.8.5. We refer to the components in this version, in order from left-to-right, as the - * major, minor, release, and patch versions. For example, Deadline version 10.1.8.5 is majorVersion 10, minorVersion 1, - * releaseVersion 8, and patchVersion 5. - * - * All of the installers provided by an instance of this class must be for the same Deadline release (ex: 10.1.8), - * but the patch versions may differ between operating systems depending on the particulars of that release of Deadline. - * This class provides a simple way to query a version of Deadline prior to or during deployment of a - * CDK app. - * - * You pass an instance of this class to various Deadline constructs in this library to tell those - * constructs which version of Deadline you want them to use, and be configured for. - */ -export class VersionQuery extends VersionQueryBase { - /** - * Specify a Deadline version from a fully-qualified Deadline patch version. - * - * This only provides the Linux repository installer based on its conventional - * S3 object path. - * - * @remark Thinkbox reserves the right to revoke patch versions of Deadline and suceeed them with a new patch version. - * For this reason, using this method may fail if Thinkbox revokes the specific patch version of the Deadline - * installer in the event of a critical issue such as a security vulnerability. - * - * Use at your own risk. - * - * @param scope The parent scope - * @param id The construct ID - * @param versionComponents The individual components of the Deadline release version - */ - public static exact(scope: Construct, id: string, versionComponents: IPatchVersion): IVersion { - class ExactVersion extends VersionQueryBase { - /** - * @inheritdoc - */ - public readonly majorVersion: number; - - /** - * @inheritdoc - */ - public readonly minorVersion: number; - - /** - * @inheritdoc - */ - public readonly releaseVersion: number; - - /** - * @inheritdoc - */ - public readonly linuxInstallers?: PlatformInstallers; - - constructor() { - super(scope, id); - const installerBucket = Bucket.fromBucketName(this, 'ThinkboxInstallers', ExactVersion.INSTALLER_BUCKET); - - const { majorVersion, minorVersion, releaseVersion, patchVersion } = versionComponents; - - this.majorVersion = majorVersion; - this.minorVersion = minorVersion; - this.releaseVersion = releaseVersion; - - const fullVersionString = this.fullVersionString(patchVersion); - const objectKey = `Deadline/${fullVersionString}/Linux/DeadlineRepository-${fullVersionString}-linux-x64-installer.run`; - - this.linuxInstallers = { - patchVersion, - repository: { - s3Bucket: installerBucket, - objectKey, - }, - }; - } - } - - return new ExactVersion(); - } - - /** - * Specify Version from a fully-qualified Deadline release version string. - * - * This only provides the Linux repository installer based on its conventional - * S3 object path. - * - * @param scope The parent scope - * @param id The construct ID - * @param versionString A fully qualified version string (e.g. 10.1.9.2) - */ - public static exactString(scope: Construct, id: string, versionString: string) { - return VersionQuery.exact(scope, id, Version.parse(versionString)); - } - - /** - * @inheritdoc - */ - public readonly majorVersion: number; - - /** - * @inheritdoc - */ - public readonly minorVersion: number; - - /** - * @inheritdoc - */ - public readonly releaseVersion: number; - - /** - * @inheritdoc - */ - public readonly linuxInstallers?: PlatformInstallers; - - constructor(scope: Construct, id: string, props?: VersionQueryProps) { - super(scope, id); - throw new Error(`MethodNotSupportedException: This method is currently not implemented. Input: ${JSON.stringify(props)}`); - } -} diff --git a/packages/aws-rfdk/lib/deadline/test/asset-constants.ts b/packages/aws-rfdk/lib/deadline/test/asset-constants.ts index 17e865690..187fe0704 100644 --- a/packages/aws-rfdk/lib/deadline/test/asset-constants.ts +++ b/packages/aws-rfdk/lib/deadline/test/asset-constants.ts @@ -41,3 +41,8 @@ export const RQ_CONNECTION_ASSET = { Bucket: 'AssetParameters89a29e05a2a88ec4d4a02e847e3c3c9461d0154b326492f4cad655d4ca0bda98S3BucketC22E185C', Key: 'AssetParameters89a29e05a2a88ec4d4a02e847e3c3c9461d0154b326492f4cad655d4ca0bda98S3VersionKey0833D670', }; + +export const VERSION_QUERY_ASSET = { + Bucket: stringLike('AssetParameters*S3Bucket75763B6A'), + Key: stringLike('AssetParameters*S3VersionKeyCF2C67CB'), +}; diff --git a/packages/aws-rfdk/lib/deadline/test/render-queue.test.ts b/packages/aws-rfdk/lib/deadline/test/render-queue.test.ts index cf249172d..4e2d25be4 100644 --- a/packages/aws-rfdk/lib/deadline/test/render-queue.test.ts +++ b/packages/aws-rfdk/lib/deadline/test/render-queue.test.ts @@ -89,12 +89,7 @@ describe('RenderQueue', () => { app = new App(); dependencyStack = new Stack(app, 'DepStack'); vpc = new Vpc(dependencyStack, 'Vpc'); - version = VersionQuery.exact(dependencyStack, 'Version', { - majorVersion: 10, - minorVersion: 1, - releaseVersion: 9, - patchVersion: 1, - }); + version = new VersionQuery(dependencyStack, 'Version'); repository = new Repository(dependencyStack, 'Repo', { version, vpc, @@ -1910,12 +1905,7 @@ describe('RenderQueue', () => { }, }); isolatedVpc = new Vpc(isolatedStack, 'Vpc'); - isolatedVersion = VersionQuery.exact(isolatedStack, 'Version', { - majorVersion: 10, - minorVersion: 1, - releaseVersion: 9, - patchVersion: 1, - }); + isolatedVersion = new VersionQuery(isolatedStack, 'Version'); isolatedRepository = new Repository(isolatedStack, 'Repo', { version: isolatedVersion, diff --git a/packages/aws-rfdk/lib/deadline/test/repository.test.ts b/packages/aws-rfdk/lib/deadline/test/repository.test.ts index b6c151d5a..8c09e891d 100644 --- a/packages/aws-rfdk/lib/deadline/test/repository.test.ts +++ b/packages/aws-rfdk/lib/deadline/test/repository.test.ts @@ -10,6 +10,7 @@ import { haveResource, haveResourceLike, ResourcePart, + stringLike, } from '@aws-cdk/assert'; import {AutoScalingGroup} from '@aws-cdk/aws-autoscaling'; import {DatabaseCluster} from '@aws-cdk/aws-docdb'; @@ -29,9 +30,7 @@ import { import { FileSystem as EfsFileSystem, } from '@aws-cdk/aws-efs'; -import { - Bucket, -} from '@aws-cdk/aws-s3'; +import { Bucket } from '@aws-cdk/aws-s3'; import { App, CfnElement, @@ -50,7 +49,6 @@ import { DatabaseConnection, IVersion, Repository, - VersionQuery, } from '../lib'; import { REPO_DC_ASSET, @@ -59,7 +57,7 @@ import { let app: App; let stack: Stack; let vpc: IVpc; -let deadlineVersion: IVersion; +let version: IVersion; function escapeTokenRegex(s: string): string { // A CDK Token looks like: ${Token[TOKEN.12]} @@ -72,26 +70,33 @@ beforeEach(() => { app = new App(); stack = new Stack(app, 'Stack'); vpc = new Vpc(stack, 'VPC'); - deadlineVersion = VersionQuery.exact(stack, 'Version', { + version = { majorVersion: 10, minorVersion: 1, releaseVersion: 9, - patchVersion: 2, - }); + linuxInstallers: { + patchVersion: 2, + repository: { + objectKey: 'testInstaller', + s3Bucket: new Bucket(stack, 'InstallerBucket'), + }, + }, + linuxFullVersionString: '10.1.9.2', + }; }); test('can create two repositories', () => { // GIVEN new Repository(stack, 'Repo1', { vpc, - version: deadlineVersion, + version, }); // THEN expect(() => { new Repository(stack, 'Repo2', { vpc, - version: deadlineVersion, + version, }); }).not.toThrow(); }); @@ -100,7 +105,7 @@ test('repository installer instance is created correctly', () => { // WHEN new Repository(stack, 'repositoryInstaller', { vpc, - version: deadlineVersion, + version, }); // THEN @@ -173,7 +178,7 @@ test('repository installer honors vpcSubnet', () => { }); new Repository(stack, 'repositoryInstaller', { vpc: attrVpc, - version: deadlineVersion, + version, vpcSubnets: { subnetType: SubnetType.PUBLIC }, }); @@ -187,7 +192,7 @@ test('repository installer security groups created correctly', () => { // WHEN new Repository(stack, 'repositoryInstaller', { vpc, - version: deadlineVersion, + version, }); // THEN @@ -270,7 +275,7 @@ test('repository installer iam permissions: db secret access', () => { // WHEN new Repository(stack, 'repositoryInstaller', { vpc, - version: deadlineVersion, + version, }); // THEN @@ -305,7 +310,7 @@ test('repository installer iam permissions: installer get', () => { // WHEN new Repository(stack, 'repositoryInstaller', { vpc, - version: deadlineVersion, + version, }); // THEN @@ -328,7 +333,10 @@ test('repository installer iam permissions: installer get', () => { { Ref: 'AWS::Partition', }, - ':s3:::thinkbox-installers', + ':s3:::', + { + Ref: stringLike('AssetParameters*S3Bucket352E624B'), + }, ], ], }, @@ -340,7 +348,11 @@ test('repository installer iam permissions: installer get', () => { { Ref: 'AWS::Partition', }, - ':s3:::thinkbox-installers/Deadline/10.1.9.2/Linux/DeadlineRepository-10.1.9.2-linux-x64-installer.run', + ':s3:::', + { + Ref: stringLike('AssetParameters*S3Bucket352E624B'), + }, + '/*', ], ], }, @@ -355,7 +367,7 @@ test('default repository installer log group created correctly', () => { // WHEN new Repository(stack, 'repositoryInstaller', { vpc, - version: deadlineVersion, + version, }); // THEN @@ -369,7 +381,7 @@ test('repository installer logs all required files', () => { // WHEN new Repository(stack, 'repositoryInstaller', { vpc, - version: deadlineVersion, + version, }); // THEN @@ -397,7 +409,7 @@ test('repository mounts repository filesystem', () => { // GIVEN const repo = new Repository(stack, 'repositoryInstaller', { vpc, - version: deadlineVersion, + version, }); // WHEN @@ -415,7 +427,7 @@ test.each([ // WHEN new Repository(stack, 'repositoryInstaller', { vpc, - version: deadlineVersion, + version, removalPolicy: { database: policy, }, @@ -435,7 +447,7 @@ test.each([ // WHEN new Repository(stack, 'repositoryInstaller', { vpc, - version: deadlineVersion, + version, removalPolicy: { filesystem: policy, }, @@ -460,7 +472,7 @@ test('repository warns if removal policy for filesystem when filesystem provided const repo = new Repository(stack, 'repositoryInstaller', { vpc, fileSystem: testFS, - version: deadlineVersion, + version, removalPolicy: { filesystem: RemovalPolicy.DESTROY, }, @@ -500,7 +512,7 @@ test('repository warns if removal policy for database when database provided', ( const repo = new Repository(stack, 'repositoryInstaller', { vpc, database: DatabaseConnection.forDocDB({ database: fsDatabase, login: fsDatabase.secret! }), - version: deadlineVersion, + version, removalPolicy: { database: RemovalPolicy.DESTROY, }, @@ -529,7 +541,7 @@ test('repository creates deadlineDatabase if none provided', () => { new Repository(stack, 'repositoryInstaller', { vpc, fileSystem: testFS, - version: deadlineVersion, + version, }); // THEN @@ -560,7 +572,7 @@ test('disabling Audit logging does not enable Cloudwatch audit logs', () => { new Repository(stack, 'repositoryInstaller', { vpc, fileSystem: testFS, - version: deadlineVersion, + version, databaseAuditLogging: false, }); @@ -598,7 +610,7 @@ test('repository warns if databaseAuditLogging defined and database is specified // WHEN const repo = new Repository(stack, 'repositoryInstaller', { vpc, - version: deadlineVersion, + version, removalPolicy: { filesystem: RemovalPolicy.DESTROY, }, @@ -606,13 +618,15 @@ test('repository warns if databaseAuditLogging defined and database is specified databaseAuditLogging: true, }); + const warningMsg = 'The parameter databaseAuditLogging only has an effect when the Repository is creating its own database.\n' + + 'Please ensure that the Database provided is configured correctly.'; + // THEN expect(repo.node.metadata).toEqual( expect.arrayContaining([ expect.objectContaining({ type: 'aws:cdk:warning', - data: `The parameter databaseAuditLogging only has an effect when the Repository is creating its own database. - Please ensure that the Database provided is configured correctly.`, + data: warningMsg, }), ]), ); @@ -638,7 +652,7 @@ test('honors subnet specification', () => { // WHEN new Repository(isolatedStack, 'repositoryInstaller', { vpc: dependencyVpc, - version: deadlineVersion, + version, vpcSubnets: { subnets, }, @@ -662,7 +676,7 @@ test('repository honors database instance count', () => { // WHEN new Repository(stack, 'repositoryInstaller', { vpc, - version: deadlineVersion, + version, documentDbInstanceCount: instanceCount, }); @@ -679,7 +693,7 @@ test('repository honors database retention period', () => { // WHEN new Repository(stack, 'repositoryInstaller', { vpc, - version: deadlineVersion, + version, backupOptions: { databaseRetention: Duration.days(period), }, @@ -714,7 +728,7 @@ test('warns if both retention period and database provided', () => { const repo = new Repository(stack, 'repositoryInstaller', { vpc, database: DatabaseConnection.forDocDB({ database: fsDatabase, login: fsDatabase.secret! }), - version: deadlineVersion, + version, backupOptions: { databaseRetention: Duration.days(20), }, @@ -761,7 +775,7 @@ test('repository creates filesystem if none provided', () => { new Repository(stack, 'repositoryInstaller', { vpc, database: DatabaseConnection.forDocDB({ database: fsDatabase, login: fsDatabase.secret }), - version: deadlineVersion, + version, }); // THEN @@ -772,7 +786,7 @@ test('repository creates filesystem if none provided', () => { test('default repository instance is created using user defined installation path prefix', () => { const repo = new Repository(stack, 'repositoryInstaller', { vpc, - version: deadlineVersion, + version, repositoryInstallationPrefix: 'xyz', }); @@ -784,7 +798,7 @@ test('default repository instance is created using user defined installation pat test('default repository instance is created using user defined installation path prefix with extra slashes in path', () => { const repo = new Repository(stack, 'repositoryInstaller', { vpc, - version: deadlineVersion, + version, repositoryInstallationPrefix: '/xyz//', }); @@ -797,7 +811,7 @@ test('repository instance is created with user defined timeout', () => { new Repository(stack, 'repositoryInstaller', { vpc, repositoryInstallationTimeout: Duration.minutes(30), - version: deadlineVersion, + version, }); // THEN @@ -817,7 +831,7 @@ test('repository instance is created with correct installer path version', () => const repo = new Repository(stack, 'repositoryInstaller', { vpc, repositoryInstallationTimeout: Duration.minutes(30), - version: deadlineVersion, + version, }); // THEN @@ -835,7 +849,7 @@ test.each([ // WHEN new Repository(stack, id, { vpc, - version: deadlineVersion, + version, logGroupProps: { logGroupPrefix: testPrefix, }, @@ -852,7 +866,7 @@ test('validate instance self-termination', () => { const repo = new Repository(stack, 'repositoryInstaller', { vpc, repositoryInstallationTimeout: Duration.minutes(30), - version: deadlineVersion, + version, }); const asgLogicalId = stack.getLogicalId(repo.node.defaultChild!.node.defaultChild as CfnElement); @@ -887,7 +901,7 @@ test('repository configure client instance', () => { const repo = new Repository(stack, 'repositoryInstaller', { vpc, repositoryInstallationTimeout: Duration.minutes(30), - version: deadlineVersion, + version, }); const instance = new Instance(stack, 'Instance', { vpc, @@ -921,7 +935,7 @@ test('configureClientInstance uses singleton for repo config script', () => { const repo = new Repository(stack, 'repositoryInstaller', { vpc, repositoryInstallationTimeout: Duration.minutes(30), - version: deadlineVersion, + version, }); const instance1 = new Instance(stack, 'Instance1', { vpc, @@ -997,56 +1011,12 @@ test('configureClientInstance uses singleton for repo config script', () => { })); }); -test('must provide linux repository installer', () => { - // GIVEN - const version: IVersion = { - majorVersion: 10, - minorVersion: 1, - releaseVersion: 0, - linuxFullVersionString: () => '10.1.0.3', - }; - - // THEN - expect(() => { - new Repository(stack, 'repositoryInstaller', { - vpc, - version, - }); - }).toThrowError('Version given to Repository must provide a Linux Repository installer.'); -}); - -test('must provide linux repository full version string', () => { - // GIVEN - const s3Bucket = Bucket.fromBucketName(stack, 'Bucket', 'someBucket'); - const version: IVersion = { - majorVersion: 10, - minorVersion: 1, - releaseVersion: 0, - linuxFullVersionString: () => undefined, - linuxInstallers: { - patchVersion: 1, - repository: { - s3Bucket, - objectKey: 'somekey', - }, - }, - }; - - // THEN - expect(() => { - new Repository(stack, 'repositoryInstaller', { - vpc, - version, - }); - }).toThrowError('Version given to Repository must provide a full Linux version string.'); -}); - test('windows client cannot direct connect to repository', () => { // GIVEN const repo = new Repository(stack, 'repositoryInstaller', { vpc, repositoryInstallationTimeout: Duration.minutes(30), - version: deadlineVersion, + version, }); const instance = new Instance(stack, 'Instance', { vpc, @@ -1071,7 +1041,7 @@ describe('tagging', () => { const isolatedStack = new Stack(app, 'IsolatedStack'); new Repository(isolatedStack, 'Repository', { vpc, - version: deadlineVersion, + version, }); return isolatedStack; }, diff --git a/packages/aws-rfdk/lib/deadline/test/stage.test.ts b/packages/aws-rfdk/lib/deadline/test/stage.test.ts index ff7818cfc..831f8ff5e 100644 --- a/packages/aws-rfdk/lib/deadline/test/stage.test.ts +++ b/packages/aws-rfdk/lib/deadline/test/stage.test.ts @@ -6,6 +6,10 @@ import * as fs from 'fs'; import * as path from 'path'; +import { + expect as expectCDK, + haveResource, +} from '@aws-cdk/assert'; import { App, Stack, @@ -395,19 +399,8 @@ describe('Stage', () => { }); // WHEN - const version = stage.getVersion(stack, 'Version'); - const linuxFullVersionString = version.linuxFullVersionString(); - - // THEN - expect(version.majorVersion).toEqual(10); - expect(version.minorVersion).toEqual(1); - expect(version.releaseVersion).toEqual(9); - - expect(version.linuxInstallers).toBeDefined(); - expect(version.linuxInstallers?.patchVersion).toEqual(2); - - expect(linuxFullVersionString).toBeDefined(); - expect(linuxFullVersionString).toEqual('10.1.9.2'); + stage.getVersion(stack, 'Version'); + expectCDK(stack).to(haveResource('Custom::RFDK_DEADLINE_INSTALLERS')); }); }); }); diff --git a/packages/aws-rfdk/lib/deadline/test/thinkbox-docker-recipes.test.ts b/packages/aws-rfdk/lib/deadline/test/thinkbox-docker-recipes.test.ts index 590053bbd..45484994d 100644 --- a/packages/aws-rfdk/lib/deadline/test/thinkbox-docker-recipes.test.ts +++ b/packages/aws-rfdk/lib/deadline/test/thinkbox-docker-recipes.test.ts @@ -5,6 +5,10 @@ import * as path from 'path'; +import { + expect as expectCDK, + haveResource, +} from '@aws-cdk/assert'; import { DockerImageAsset } from '@aws-cdk/aws-ecr-assets'; import { App, @@ -13,7 +17,6 @@ import { import { DeadlineDockerRecipes, - IVersion, Manifest, Recipe, Stage, @@ -129,40 +132,13 @@ describe('ThinkboxDockerRecipes', () => { expect(actualImage.sourceHash).toEqual(expectedImage.sourceHash); }); - describe('provides the Deadline version with', () => { - let version: IVersion; - beforeEach(() => { - // WHEN - const recipes = new ThinkboxDockerRecipes(stack, 'Recipes', { - stage, - }); - version = recipes.version; + test('provides the Deadline version', () => { + // WHEN + new ThinkboxDockerRecipes(stack, 'Recipes', { + stage, }); - test('majorVersion attribute', () => { - // THEN - expect(version.majorVersion).toEqual(MAJOR_VERSION); - }); - test('minorVersion attribute', () => { - // THEN - expect(version.minorVersion).toEqual(MINOR_VERSION); - }); - test('releaseVersion component', () => { - // THEN - expect(version.releaseVersion).toEqual(RELEASE_VERSION); - }); - test('linuxFullVersionString() result', () => { - // THEN - expect(version.linuxFullVersionString()).toEqual(FULL_VERSION_STRING); - }); - test('linuxInstallers', () => { - // THEN - expect(version.linuxInstallers).toBeDefined(); - expect(version.linuxInstallers?.patchVersion).toEqual(PATCH_VERSION); - expect(version.linuxInstallers?.repository).toBeDefined(); - expect(version.linuxInstallers?.repository?.s3Bucket.bucketName).toEqual('thinkbox-installers'); - expect(version.linuxInstallers?.repository?.objectKey).toEqual(`Deadline/${FULL_VERSION_STRING}/Linux/DeadlineRepository-${FULL_VERSION_STRING}-linux-x64-installer.run`); - }); + expectCDK(stack).to(haveResource('Custom::RFDK_DEADLINE_INSTALLERS')); }); test.each([ 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 228090417..e8ffaab33 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 @@ -57,7 +57,7 @@ const env = { }; let app: App; let certificateSecret: ISecret; -let deadlineVersion: IVersion; +let versionedInstallers: IVersion; let dependencyStack: Stack; let dockerContainer: DockerImageAsset; let images: UsageBasedLicensingImages; @@ -77,27 +77,20 @@ describe('UsageBasedLicensing', () => { dependencyStack = new Stack(app, 'DependencyStack', { env }); - deadlineVersion = VersionQuery.exact(dependencyStack, 'Version', { - majorVersion: 10, - minorVersion: 1, - releaseVersion: 9, - patchVersion: 1, - }); - - expect(deadlineVersion.linuxFullVersionString).toBeDefined(); + versionedInstallers = new VersionQuery(dependencyStack, 'VersionQuery'); 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, + version: versionedInstallers, }), + version: versionedInstallers, }); lfCluster = new Cluster(dependencyStack, 'licenseForwarderCluster', { diff --git a/packages/aws-rfdk/lib/deadline/test/version-query.test.ts b/packages/aws-rfdk/lib/deadline/test/version-query.test.ts new file mode 100644 index 000000000..015f300ef --- /dev/null +++ b/packages/aws-rfdk/lib/deadline/test/version-query.test.ts @@ -0,0 +1,116 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + ABSENT, + expect as expectCDK, + haveResourceLike, + stringLike, +} from '@aws-cdk/assert'; +import { + App, + Stack, +} from '@aws-cdk/core'; + +import { + VersionQuery, +} from '../lib'; + +import { VERSION_QUERY_ASSET } from './asset-constants'; + +test('VersionQuery constructor full', () => { + const app = new App(); + const stack = new Stack(app, 'Stack'); + new VersionQuery(stack, 'VersionQuery', { version: '10.1.9'}); + + expectCDK(stack).to(haveResourceLike('Custom::RFDK_DEADLINE_INSTALLERS', { + versionString: '10.1.9', + })); + expectCDK(stack).to(haveResourceLike('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + }, + ManagedPolicyArns: [ + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + ], + ], + }, + ], + })); + expectCDK(stack).to(haveResourceLike('AWS::Lambda::Function', { + Code: { + S3Bucket: { + Ref: VERSION_QUERY_ASSET.Bucket, + }, + S3Key: { + 'Fn::Join': [ + '', + [ + { + 'Fn::Select': [ + 0, + { + 'Fn::Split': [ + '||', + { + Ref: VERSION_QUERY_ASSET.Key, + }, + ], + }, + ], + }, + { + 'Fn::Select': [ + 1, + { + 'Fn::Split': [ + '||', + { + Ref: VERSION_QUERY_ASSET.Key, + }, + ], + }, + ], + }, + ], + ], + }, + }, + Handler: 'version-provider.handler', + Role: { + 'Fn::GetAtt': [ + stringLike('SingletonLambda*ServiceRole*'), + 'Arn', + ], + }, + Runtime: 'nodejs12.x', + })); +}); + +test('VersionQuery constructor no versionString', () => { + const app = new App(); + const stack = new Stack(app, 'Stack'); + new VersionQuery(stack, 'VersionQuery'); + + expectCDK(stack).to(haveResourceLike('Custom::RFDK_DEADLINE_INSTALLERS', { + versionString: ABSENT, + })); +}); diff --git a/packages/aws-rfdk/lib/deadline/test/version.test.ts b/packages/aws-rfdk/lib/deadline/test/version.test.ts index 3c8c0eeec..6fda7c171 100644 --- a/packages/aws-rfdk/lib/deadline/test/version.test.ts +++ b/packages/aws-rfdk/lib/deadline/test/version.test.ts @@ -3,22 +3,20 @@ * SPDX-License-Identifier: Apache-2.0 */ +/* eslint-disable dot-notation */ + +// import { +// expect as expectCDK, +// haveResource, +// haveResourceLike, +// } from '@aws-cdk/assert'; +// import { +// Stack, +// } from '@aws-cdk/core'; import { - expect as expectCDK, - haveResource, - haveResourceLike, -} from '@aws-cdk/assert'; -import { - Stack, -} from '@aws-cdk/core'; -import { - IVersion, Version, - VersionQuery, } from '../lib'; -let stack: Stack; - describe('Version', () => { describe('.isGreaterThan', () => { test.each<[string, { firstVersion: string, secondVersion: string, expectedValue: boolean }]>([ @@ -164,19 +162,19 @@ describe('Version', () => { 'incorrect component count', { version: [10, 1, 9], - expectedException: /Invalid version format/, + expectedException: /Invalid version format. Version should contain exactly 4 components./, }, ], [ 'negative value', { - version: [10, -1, 9], - expectedException: /Invalid version format/, + version: [10, -1, 9, 2], + expectedException: /Invalid version format. None of the version components can be negative./, }, ], [ 'decimal value', { - version: [10, 1, 9.2], - expectedException: /Invalid version format/, + version: [10, 1, 9.2, 2], + expectedException: /Invalid version format. None of the version components can contain decimal values./, }, ], [ 'correct value', @@ -191,7 +189,11 @@ describe('Version', () => { if (expectedException) { expect(() => new Version(version)).toThrow(expectedException); } else { - expect(() => new Version(version)).not.toThrow(); + const versionObj = new Version(version); + expect(versionObj.majorVersion).toEqual(version[0]); + expect(versionObj.minorVersion).toEqual(version[1]); + expect(versionObj.releaseVersion).toEqual(version[2]); + expect(versionObj.patchVersion).toEqual(version[3]); } }); }); @@ -234,141 +236,3 @@ describe('Version', () => { }); }); }); - -describe('VersionQuery', () => { - beforeEach(() => { - stack = new Stack(undefined, undefined); - }); - - describe('constructor', () => { - test('throws not implemented error for empty version string', () => { - // WHEN - expect(() => { - new VersionQuery(stack, 'version', { - version: '', - }); - }).toThrowError(/MethodNotSupportedException: This method is currently not implemented./); - - // THEN - expectCDK(stack).notTo(haveResource('AWS::Lambda::Function')); - expectCDK(stack).notTo(haveResourceLike('AWS::CloudFormation::CustomResource', { - DeadlineVersion: '', - })); - }); - - test('throws not implemented error for valid version string', () => { - // WHEN - expect(() => { - new VersionQuery(stack, 'version', { - version: '1.2', - }); - }).toThrowError(/MethodNotSupportedException: This method is currently not implemented./); - - // THEN - expectCDK(stack).notTo(haveResource('AWS::Lambda::Function')); - expectCDK(stack).notTo(haveResourceLike('AWS::CloudFormation::CustomResource', { - DeadlineVersion: '1.2', - })); - }); - - test('throws not implemented error without props', () => { - // WHEN - expect(() => { - new VersionQuery(stack, 'version'); - }).toThrowError(/MethodNotSupportedException: This method is currently not implemented./); - - // THEN - expectCDK(stack).notTo(haveResource('AWS::Lambda::Function')); - expectCDK(stack).notTo(haveResourceLike('AWS::CloudFormation::CustomResource')); - expectCDK(stack).notTo(haveResourceLike('AWS::CloudFormation::CustomResource', { - DeadlineVersion: '', - })); - }); - }); - - // GIVEN - const majorVersion = 1; - const minorVersion = 2; - const releaseVersion = 3; - const patchVersion = 4; - const expectedVersionString = `${majorVersion}.${minorVersion}.${releaseVersion}.${patchVersion}`; - - let version: IVersion; - - function exactVersionTests() { - test('does not create a custom resource', () => { - // THEN - expectCDK(stack).notTo(haveResourceLike('AWS::CloudFormation::CustomResource')); - }); - - test('does not create a lambda', () => { - // THEN - expectCDK(stack).notTo(haveResource('AWS::Lambda::Function')); - }); - - test('populates version components', () => { - // THEN - expect(version.majorVersion).toEqual(1); - expect(version.minorVersion).toEqual(2); - expect(version.releaseVersion).toEqual(3); - }); - - test('provides linux installers', () => { - // GIVEN - const linuxFullVersionString = version.linuxFullVersionString(); - - // THEN - expect(version.linuxInstallers).toBeDefined(); - expect(linuxFullVersionString).toBeDefined(); - expect(linuxFullVersionString).toMatch(expectedVersionString); - - expect(version.linuxInstallers!.repository).toBeDefined(); - expect(version.linuxInstallers!.repository!.s3Bucket.bucketName).toMatch('thinkbox-installers'); - expect(version.linuxInstallers!.repository!.objectKey).toMatch(`DeadlineRepository-${expectedVersionString}-linux-x64-installer.run`); - }); - } - - describe('.exact()', () => { - beforeEach(() => { - version = VersionQuery.exact(stack, 'version', { - majorVersion, - minorVersion, - releaseVersion, - patchVersion, - }); - }); - - exactVersionTests(); - }); - - describe('.exactString()', () => { - beforeEach(() => { - version = VersionQuery.exactString(stack, 'version', expectedVersionString); - }); - - exactVersionTests(); - - test.each([ - [''], - ['abc'], - ['1'], - ['1.2'], - ['1.2.3'], - ['1.2.3.4a'], - ['a1.2.3.4a'], - ['a1.2.3.4'], - [' 1.2.3.4 '], - ['a.b.c.d'], - ['-1.0.2.3'], - ['.1.0.2.3'], - ])('throws an error on invalid versions %p', (versionStr: string) => { - // WHEN - function when() { - VersionQuery.exactString(stack, 'version', versionStr); - } - - // THEN - expect(when).toThrowError(new RegExp(`^Invalid version format. Expected format 'a.b.c.d', found '${versionStr}'$`)); - }); - }); -}); diff --git a/packages/aws-rfdk/lib/deadline/test/worker-fleet.test.ts b/packages/aws-rfdk/lib/deadline/test/worker-fleet.test.ts index a538ec83d..f74e65d93 100644 --- a/packages/aws-rfdk/lib/deadline/test/worker-fleet.test.ts +++ b/packages/aws-rfdk/lib/deadline/test/worker-fleet.test.ts @@ -27,7 +27,7 @@ import { AssetImage, ContainerImage, } from '@aws-cdk/aws-ecs'; -import {ArtifactMetadataEntryType} from '@aws-cdk/cloud-assembly-schema'; +import { ArtifactMetadataEntryType } from '@aws-cdk/cloud-assembly-schema'; import { App, CfnElement, @@ -70,20 +70,15 @@ beforeEach(() => { }); vpc = new Vpc(stack, 'VPC'); rcsImage = ContainerImage.fromAsset(__dirname); - const version = VersionQuery.exact(stack, 'Version', { - majorVersion: 10, - minorVersion: 0, - releaseVersion: 0, - patchVersion: 0, - }); + const version = new VersionQuery(stack, 'VersionQuery'); renderQueue = new RenderQueue(stack, 'RQ', { - version, vpc, images: { remoteConnectionServer: rcsImage }, repository: new Repository(stack, 'Repository', { vpc, version, }), + version, }); wfstack = new Stack(app, 'workerFleetStack', { env: {