diff --git a/examples/deadline/All-In-AWS-Infrastructure-Basic/python/README.md b/examples/deadline/All-In-AWS-Infrastructure-Basic/python/README.md index 49fa353ec..0bb80e937 100644 --- a/examples/deadline/All-In-AWS-Infrastructure-Basic/python/README.md +++ b/examples/deadline/All-In-AWS-Infrastructure-Basic/python/README.md @@ -32,7 +32,22 @@ These instructions assume that your working directory is `examples/deadline/All- popd pip install ../../../../dist/python/aws-rfdk-.tar.gz ``` -4. Change the value in the `deadline_client_linux_ami_map` variable in `package/config.py` to include the region + AMI ID mapping of your EC2 AMI(s) with Deadline Worker. You can use the following AWS CLI query to find AMI ID's: +4. You must read and accept the [AWS Thinkbox End-User License Agreement (EULA)](https://www.awsthinkbox.com/end-user-license-agreement) to deploy and run Deadline. To do so, change the value of the `accept_aws_thinkbox_eula` in `package/config.py`: + + ```py + # Change this value to AwsThinkboxEulaAcceptance.USER_ACCEPTS_AWS_THINKBOX_EULA if you wish to accept the EULA + # for Deadline and proceed with Deadline deployment. Users must explicitly accept the AWS Thinkbox EULA before + # using the AWS Thinkbox Deadline container images. + # + # See https://www.awsthinkbox.com/end-user-license-agreement for the terms of the agreement. + self.accept_aws_thinkbox_eula: AwsThinkboxEulaAcceptance = AwsThinkboxEulaAcceptance.USER_REJECTS_AWS_THINKBOX_EULA + ``` +5. Change the value of the `deadline_version` variable in `package/config.py` to specify the desired version of Deadline to be deployed to your render farm. RFDK is compatible with Deadline versions 10.1.9.x and later. To see the available versions of Deadline, consult the [Deadline release notes](https://docs.thinkboxsoftware.com/products/deadline/10.1/1_User%20Manual/manual/release-notes.html). It is recommended to use the latest version of Deadline available when building your farm, but to pin this version when the farm is ready for production use. For example, to pin to the latest `10.1.12.x` release of Deadline, use: + + ```python + self.deadline_version: str = '10.1.12' + ``` +6. Change the value of the `deadline_client_linux_ami_map` variable in `package/config.py` to include the region + AMI ID mapping of your EC2 AMI(s) with Deadline Worker. You can use the following AWS CLI query to find AMI ID's: ```bash aws --region ec2 describe-images \ --owners 357466774442 \ @@ -48,17 +63,17 @@ These instructions assume that your working directory is `examples/deadline/All- 'us-west-2': '' } ``` -5. Create a binary secret in [SecretsManager](https://aws.amazon.com/secrets-manager/) that contains your [Usage-Based Licensing](https://docs.thinkboxsoftware.com/products/deadline/10.1/1_User%20Manual/manual/aws-portal/licensing-setup.html?highlight=usage%20based%20licensing) certificates in a `.zip` file: +7. Create a binary secret in [SecretsManager](https://aws.amazon.com/secrets-manager/) that contains your [Usage-Based Licensing](https://docs.thinkboxsoftware.com/products/deadline/10.1/1_User%20Manual/manual/aws-portal/licensing-setup.html?highlight=usage%20based%20licensing) certificates in a `.zip` file: ```bash aws secretsmanager create-secret --name --secret-binary fileb:// ``` -6. The output from the previous step will contain the secret's ARN. Change the value of the `ubl_certificate_secret_arn` variable in `package/config.py` to the secret's ARN: +8. The output from the previous step will contain the secret's ARN. Change the value of the `ubl_certificate_secret_arn` variable in `package/config.py` to the secret's ARN: ```python self.ubl_certificate_secret_arn: str = '' ``` -7. Choose your UBL limits and change the value of the `ubl_licenses` variable in `package/config.py` accordingly. For example: +9. Choose your UBL limits and change the value of the `ubl_licenses` variable in `package/config.py` accordingly. For example: ```python self.ubl_licenses: List[UsageBasedLicense] = [ @@ -77,19 +92,19 @@ These instructions assume that your working directory is `examples/deadline/All- **Note:** The next two steps are optional. You may skip these if you do not need SSH access into your render farm. --- -8. Create an EC2 key pair to give you SSH access to the render farm: +10. Create an EC2 key pair to give you SSH access to the render farm: ```bash aws ec2 create-key-pair --key-name ``` -9. Change the value of the `key_pair_name` variable in `package/config.py` to your value for `` in the previous step: +11. Change the value of the `key_pair_name` variable in `package/config.py` to your value for `` in the previous step: **Note:** Save the value of the `"KeyMaterial"` field as a file in a secure location. This is your private key that you can use to SSH into the render farm. ```python self.key_pair_name: Optional[str] = '' ``` -10. Choose the type of database you would like to deploy (AWS DocumentDB or MongoDB). +12. Choose the type of database you would like to deploy (AWS DocumentDB or MongoDB). If you would like to use MongoDB, you will need to accept the Mongo SSPL (see next step). Once you've decided on a database type, change the value of the `deploy_mongo_db` variable in `package/config.py` accordingly: @@ -97,29 +112,18 @@ These instructions assume that your working directory is `examples/deadline/All- # True = MongoDB, False = Amazon DocumentDB self.deploy_mongo_db: bool = False ``` -11. If you set `deploy_mongo_db` to `True`, then you must accept the [SSPL license](https://www.mongodb.com/licensing/server-side-public-license) to successfully deploy MongoDB. To do so, change the value of `accept_sspl_license` in `package/config.py`: +13. If you set `deploy_mongo_db` to `True`, then you must accept the [SSPL license](https://www.mongodb.com/licensing/server-side-public-license) to successfully deploy MongoDB. To do so, change the value of `accept_sspl_license` in `package/config.py`: ```python # To accept the MongoDB SSPL, change from USER_REJECTS_SSPL to USER_ACCEPTS_SSPL self.accept_sspl_license: MongoDbSsplLicenseAcceptance = MongoDbSsplLicenseAcceptance.USER_REJECTS_SSPL ``` -12. Stage the Docker recipes for `RenderQueue` and `UBLLicensing`: - - ```bash - # Set this value to the version of RFDK your application targets - RFDK_VERSION= - - # Set this value to the version of AWS Thinkbox Deadline you'd like to deploy to your farm. Deadline 10.1.9 and up are supported. - RFDK_DEADLINE_VERSION= - - npx --package=aws-rfdk@${RFDK_VERSION} stage-deadline ${RFDK_DEADLINE_VERSION} --output stage - ``` -12. Deploy all the stacks in the sample app: +14. Deploy all the stacks in the sample app: ```bash cdk deploy "*" ``` -13. Once you are finished with the sample app, you can tear it down by running: +15. Once you are finished with the sample app, you can tear it down by running: ```bash cdk destroy "*" diff --git a/examples/deadline/All-In-AWS-Infrastructure-Basic/python/package/app.py b/examples/deadline/All-In-AWS-Infrastructure-Basic/python/package/app.py index 904238574..5e848d302 100644 --- a/examples/deadline/All-In-AWS-Infrastructure-Basic/python/package/app.py +++ b/examples/deadline/All-In-AWS-Infrastructure-Basic/python/package/app.py @@ -103,11 +103,12 @@ def main(): database=storage.database, file_system=storage.file_system, vpc=network.vpc, - docker_recipes_stage_path=os.path.join(os.path.dirname(os.path.realpath(__file__)), os.pardir, 'stage'), ubl_certs_secret_arn=config.ubl_certificate_secret_arn, ubl_licenses=config.ubl_licenses, root_ca=security.root_ca, - dns_zone=network.dns_zone + dns_zone=network.dns_zone, + deadline_version=config.deadline_version, + accept_aws_thinkbox_eula=config.accept_aws_thinkbox_eula ) service = service_tier.ServiceTier(app, 'ServiceTier', props=service_props, env=env) diff --git a/examples/deadline/All-In-AWS-Infrastructure-Basic/python/package/config.py b/examples/deadline/All-In-AWS-Infrastructure-Basic/python/package/config.py index 6d8fba738..6b0b7b645 100644 --- a/examples/deadline/All-In-AWS-Infrastructure-Basic/python/package/config.py +++ b/examples/deadline/All-In-AWS-Infrastructure-Basic/python/package/config.py @@ -8,7 +8,10 @@ ) from aws_rfdk import MongoDbSsplLicenseAcceptance -from aws_rfdk.deadline import UsageBasedLicense +from aws_rfdk.deadline import ( + AwsThinkboxEulaAcceptance, + UsageBasedLicense +) class AppConfig: @@ -18,6 +21,17 @@ class AppConfig: TODO: Fill these in with your own values. """ def __init__(self): + # Change this value to AwsThinkboxEulaAcceptance.USER_ACCEPTS_AWS_THINKBOX_EULA if you wish to accept the EULA + # for Deadline and proceed with Deadline deployment. Users must explicitly accept the AWS Thinkbox EULA before + # using the AWS Thinkbox Deadline container images. + # + # See https://www.awsthinkbox.com/end-user-license-agreement for the terms of the agreement. + self.accept_aws_thinkbox_eula: AwsThinkboxEulaAcceptance = AwsThinkboxEulaAcceptance.USER_REJECTS_AWS_THINKBOX_EULA + + # The version of Deadline to use on the render farm. Leave as None for the latest release or specify a version + # to pin to. Some examples of pinned version values are "10", "10.1", or "10.1.12" + self.deadline_version: Optional[str] = None + # A map of regions to Deadline Client Linux AMIs.As an example, the Linux Deadline 10.1.12.1 AMI ID # from us-west-2 is filled in. It can be used as-is, added to, or replaced. Ideally the version here # should match the one used for staging the render queue and usage based licensing recipes. 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 334eddc7c..3622de6f4 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 @@ -35,16 +35,17 @@ X509CertificatePem ) from aws_rfdk.deadline import ( + AwsThinkboxEulaAcceptance, DatabaseConnection, RenderQueue, RenderQueueHostNameProps, RenderQueueTrafficEncryptionProps, RenderQueueExternalTLSProps, Repository, - Stage, - ThinkboxDockerRecipes, + ThinkboxDockerImages, UsageBasedLicense, UsageBasedLicensing, + VersionQuery, ) @@ -59,8 +60,6 @@ class ServiceTierProps(StackProps): database: DatabaseConnection # The file system to install Deadline Repository to. file_system: IMountableLinuxFilesystem - # The path to the directory where the staged Deadline Docker recipes are. - docker_recipes_stage_path: str # The ARN of the secret containing the UBL certificates .zip file (in binary form). ubl_certs_secret_arn: typing.Optional[str] # The UBL licenses to configure @@ -69,6 +68,10 @@ class ServiceTierProps(StackProps): root_ca: X509CertificatePem # Internal DNS zone for the VPC dns_zone: IPrivateHostedZone + # Version of Deadline to use + deadline_version: str + # Whether the AWS Thinkbox End-User License Agreement is accepted or not + accept_aws_thinkbox_eula: AwsThinkboxEulaAcceptance class ServiceTier(Stack): @@ -113,10 +116,10 @@ def __init__(self, scope: Construct, stack_id: str, *, props: ServiceTierProps, location='/mnt/efs' ) - recipes = ThinkboxDockerRecipes( + self.version = VersionQuery( self, - 'Image', - stage=Stage.from_directory(props.docker_recipes_stage_path) + 'Version', + version=props.deadline_version ) repository = Repository( @@ -126,7 +129,14 @@ def __init__(self, scope: Construct, stack_id: str, *, props: ServiceTierProps, database=props.database, file_system=props.file_system, repository_installation_timeout=Duration.minutes(20), - version=recipes.version, + version=self.version + ) + + images = ThinkboxDockerImages( + self, + 'Images', + version=self.version, + user_aws_thinkbox_eula_acceptance=props.accept_aws_thinkbox_eula ) server_cert = X509CertificatePem( @@ -144,7 +154,7 @@ def __init__(self, scope: Construct, stack_id: str, *, props: ServiceTierProps, self, 'RenderQueue', vpc=props.vpc, - images=recipes.render_queue_images, + images=images, repository=repository, hostname=RenderQueueHostNameProps( hostname='renderqueue', @@ -156,7 +166,7 @@ def __init__(self, scope: Construct, stack_id: str, *, props: ServiceTierProps, ), internal_protocol=ApplicationProtocol.HTTPS ), - version=recipes.version, + version=self.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. @@ -178,9 +188,9 @@ def __init__(self, scope: Construct, stack_id: str, *, props: ServiceTierProps, ubl_cert_secret = Secret.from_secret_arn(self, 'ublcertssecret', props.ubl_certs_secret_arn) self.ubl_licensing = UsageBasedLicensing( self, - 'usagebasedlicensing', + 'UsageBasedLicensing', vpc=props.vpc, - images=recipes.ubl_images, + images=images, licenses=props.ubl_licenses, render_queue=self.render_queue, certificate_secret=ubl_cert_secret, diff --git a/examples/deadline/All-In-AWS-Infrastructure-Basic/ts/README.md b/examples/deadline/All-In-AWS-Infrastructure-Basic/ts/README.md index b8147e593..51cb845d9 100644 --- a/examples/deadline/All-In-AWS-Infrastructure-Basic/ts/README.md +++ b/examples/deadline/All-In-AWS-Infrastructure-Basic/ts/README.md @@ -18,7 +18,25 @@ These instructions assume that your working directory is `examples/deadline/All- ``` yarn install ``` -3. Change the value in the `deadlineClientLinuxAmiMap` variable in `bin/config.ts` to include the region + AMI ID mapping of your EC2 AMI(s) with Deadline Worker. You can use the following AWS CLI query to find AMI ID's: +3. You must read and accept the [AWS Thinkbox End-User License Agreement (EULA)](https://www.awsthinkbox.com/end-user-license-agreement) to deploy and run Deadline. To do so, change the value of the `acceptAwsThinkboxEula` in `bin/config.ts`: + + ```ts + /** + * Change this value to AwsThinkboxEulaAcceptance.USER_ACCEPTS_AWS_THINKBOX_EULA if you wish to accept the EULA for + * Deadline and proceed with Deadline deployment. Users must explicitly accept the AWS Thinkbox EULA before using the + * AWS Thinkbox Deadline container images. + * + * See https://www.awsthinkbox.com/end-user-license-agreement for the terms of the agreement. + */ + public readonly acceptAwsThinkboxEula: AwsThinkboxEulaAcceptance = AwsThinkboxEulaAcceptance.USER_REJECTS_AWS_THINKBOX_EULA; + ``` +4. Change the value of the `deadlineVersion` variable in `bin/config.ts` to specify the desired version of Deadline to be deployed to your render farm. RFDK is compatible with Deadline versions 10.1.9.x and later. To see the available versions of Deadline, consult the [Deadline release notes](https://docs.thinkboxsoftware.com/products/deadline/10.1/1_User%20Manual/manual/release-notes.html). It is recommended to use the latest version of Deadline available when building your farm, but to pin this version when the farm is ready for production use. For example, to pin to the latest `10.1.12.x` release of Deadline, use: + + ```ts + public readonly deadlineVersion: string = '10.1.12'; + ``` + +5. Change the value of the `deadlineClientLinuxAmiMap` variable in `bin/config.ts` to include the region + AMI ID mapping of your EC2 AMI(s) with Deadline Worker. You can use the following AWS CLI query to find AMI ID's: ``` aws --region ec2 describe-images \ --owners 357466774442 \ @@ -41,17 +59,17 @@ These instructions assume that your working directory is `examples/deadline/All- **Note:** The next three steps are for setting up usage based licensing and are optional. You may skip these if you do not need to use licenses for rendering. --- -4. Create a binary secret in [SecretsManager](https://aws.amazon.com/secrets-manager/) that contains your [Usage-Based Licensing](https://docs.thinkboxsoftware.com/products/deadline/10.1/1_User%20Manual/manual/aws-portal/licensing-setup.html?highlight=usage%20based%20licensing) certificates in a `.zip` file: +6. Create a binary secret in [SecretsManager](https://aws.amazon.com/secrets-manager/) that contains your [Usage-Based Licensing](https://docs.thinkboxsoftware.com/products/deadline/10.1/1_User%20Manual/manual/aws-portal/licensing-setup.html?highlight=usage%20based%20licensing) certificates in a `.zip` file: ``` aws secretsmanager create-secret --name --secret-binary fileb:// ``` -5. The output from the previous step will contain the secret's ARN. Change the value of the `ublCertificatesSecretArn` variable in `bin/config.ts` to the secret's ARN: +7. The output from the previous step will contain the secret's ARN. Change the value of the `ublCertificatesSecretArn` variable in `bin/config.ts` to the secret's ARN: ```ts public readonly ublCertificatesSecretArn: string = ''; ``` -6. Choose your UBL limits and change the value of the `ublLicenses` variable in `bin/config.ts` accordingly. For example: +8. Choose your UBL limits and change the value of the `ublLicenses` variable in `bin/config.ts` accordingly. For example: ```ts public readonly ublLicenses: UsageBasedLicense[] = [ @@ -70,19 +88,19 @@ These instructions assume that your working directory is `examples/deadline/All- **Note:** The next two steps are for allowing SSH access to your render farm and are optional. You may skip these if you do not need SSH access into your render farm. --- -7. Create an EC2 key pair to give you SSH access to the render farm: +9. Create an EC2 key pair to give you SSH access to the render farm: ``` aws ec2 create-key-pair --key-name ``` -8. Change the value of the `keyPairName` variable in `bin/config.ts` to your value for `` in the previous step: +10. Change the value of the `keyPairName` variable in `bin/config.ts` to your value for `` in the previous step: **Note:** Save the value of the `"KeyMaterial"` field as a file in a secure location. This is your private key that you can use to SSH into the render farm. ```ts public readonly keyPairName: string = ''; ``` -9. Choose the type of database you would like to deploy (AWS DocumentDB or MongoDB). +11. Choose the type of database you would like to deploy (AWS DocumentDB or MongoDB). If you would like to use MongoDB, you will need to accept the Mongo SSPL (see next step). Once you've decided on a database type, change the value of the `deployMongoDB` variable in `bin/config.ts` accordingly: @@ -90,18 +108,13 @@ These instructions assume that your working directory is `examples/deadline/All- // true = MongoDB, false = Amazon DocumentDB public readonly deployMongoDB: boolean = false; ``` -10. If you set `deployMongoDB` to `true`, then you must accept the [SSPL license](https://www.mongodb.com/licensing/server-side-public-license) to successfully deploy MongoDB. To do so, change the value of `acceptSsplLicense` in `bin/config.ts`: +12. If you set `deployMongoDB` to `true`, then you must accept the [SSPL license](https://www.mongodb.com/licensing/server-side-public-license) to successfully deploy MongoDB. To do so, change the value of `acceptSsplLicense` in `bin/config.ts`: ```ts // To accept the MongoDB SSPL, change from USER_REJECTS_SSPL to USER_ACCEPTS_SSPL public readonly acceptSsplLicense: MongoDbSsplLicenseAcceptance = MongoDbSsplLicenseAcceptance.USER_REJECTS_SSPL; ``` -11. Modify the `deadline_ver` field in the `config` block of `package.json` as desired (Deadline 10.1.9 and up are supported), then stage the Docker recipes for `RenderQueue` and `UBLLicensing`: - - ``` - yarn stage - ``` -12. Build the `aws-rfdk` package, and then build the sample app. There is some magic in the way yarn workspaces and lerna packages work that will link the built `aws-rfdk` from the base directory as the dependency to be used in the example's directory: +13. Build the `aws-rfdk` package, and then build the sample app. There is some magic in the way yarn workspaces and lerna packages work that will link the built `aws-rfdk` from the base directory as the dependency to be used in the example's directory: ```bash # Navigate to the root directory of the RFDK repository (assumes you started in the example's directory) pushd ../../../.. @@ -114,12 +127,12 @@ These instructions assume that your working directory is `examples/deadline/All- # Run the example's build yarn build ``` -13. Deploy all the stacks in the sample app: +14. Deploy all the stacks in the sample app: ``` cdk deploy "*" ``` -14. Once you are finished with the sample app, you can tear it down by running: +15. Once you are finished with the sample app, you can tear it down by running: ``` cdk destroy "*" diff --git a/examples/deadline/All-In-AWS-Infrastructure-Basic/ts/bin/app.ts b/examples/deadline/All-In-AWS-Infrastructure-Basic/ts/bin/app.ts index 5e8fb8195..659cbb01b 100644 --- a/examples/deadline/All-In-AWS-Infrastructure-Basic/ts/bin/app.ts +++ b/examples/deadline/All-In-AWS-Infrastructure-Basic/ts/bin/app.ts @@ -5,12 +5,12 @@ */ import 'source-map-support/register'; -import * as path from 'path'; -import * as pkg from '../package.json'; import { config } from './config'; import * as cdk from '@aws-cdk/core'; import { NetworkTier } from '../lib/network-tier'; -import { ServiceTier } from '../lib/service-tier'; +import { + ServiceTier, +} from '../lib/service-tier'; import { StorageTier, StorageTierDocDB, @@ -100,11 +100,12 @@ const service = new ServiceTier(app, 'ServiceTier', { database: storage.database, fileSystem: storage.fileSystem, vpc: network.vpc, - dockerRecipesStagePath: path.join(__dirname, '..', pkg.config.stage_path), // Stage directory in config is relative, make it absolute + deadlineVersion: config.deadlineVersion, ublCertsSecretArn: config.ublCertificatesSecretArn, ublLicenses: config.ublLicenses, rootCa: security.rootCa, dnsZone: network.dnsZone, + acceptAwsThinkboxEula: config.acceptAwsThinkboxEula, }); // -------------------- // diff --git a/examples/deadline/All-In-AWS-Infrastructure-Basic/ts/bin/config.ts b/examples/deadline/All-In-AWS-Infrastructure-Basic/ts/bin/config.ts index 30e939d5c..167753128 100644 --- a/examples/deadline/All-In-AWS-Infrastructure-Basic/ts/bin/config.ts +++ b/examples/deadline/All-In-AWS-Infrastructure-Basic/ts/bin/config.ts @@ -6,6 +6,7 @@ import 'source-map-support/register'; import { UsageBasedLicense } from 'aws-rfdk/deadline'; import { MongoDbSsplLicenseAcceptance } from 'aws-rfdk'; +import { AwsThinkboxEulaAcceptance } from 'aws-rfdk/deadline'; /** * Configuration values for the sample app. @@ -13,6 +14,21 @@ import { MongoDbSsplLicenseAcceptance } from 'aws-rfdk'; * TODO: Fill these in with your own values. */ class AppConfig { + /** + * Change this value to AwsThinkboxEulaAcceptance.USER_ACCEPTS_AWS_THINKBOX_EULA if you wish to accept the EULA for + * Deadline and proceed with Deadline deployment. Users must explicitly accept the AWS Thinkbox EULA before using the + * AWS Thinkbox Deadline container images. + * + * See https://www.awsthinkbox.com/end-user-license-agreement for the terms of the agreement. + */ + public readonly acceptAwsThinkboxEula: AwsThinkboxEulaAcceptance = AwsThinkboxEulaAcceptance.USER_REJECTS_AWS_THINKBOX_EULA; + + /** + * The version of Deadline to use on the render farm. Some examples of pinned version values are "10", "10.1", or + * "10.1.12" + * @default The latest available version of Deadline is used + */ + public readonly deadlineVersion?: string; /** * A map of regions to Deadline Client Linux AMIs. As an example, the Linux Deadline 10.1.12.1 AMI ID from us-west-2 @@ -49,7 +65,6 @@ class AppConfig { * if you wish to accept the SSPL and proceed with MongoDB deployment. */ public readonly acceptSsplLicense: MongoDbSsplLicenseAcceptance = MongoDbSsplLicenseAcceptance.USER_REJECTS_SSPL; - } export const config = new AppConfig(); 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 f004234b2..dbb050413 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 @@ -21,13 +21,14 @@ import { X509CertificatePem, } from 'aws-rfdk'; import { + AwsThinkboxEulaAcceptance, DatabaseConnection, RenderQueue, Repository, - Stage, - ThinkboxDockerRecipes, + ThinkboxDockerImages, UsageBasedLicense, UsageBasedLicensing, + VersionQuery, } from 'aws-rfdk/deadline'; import { Secret, @@ -54,11 +55,6 @@ export interface ServiceTierProps extends cdk.StackProps { */ readonly fileSystem: IMountableLinuxFilesystem; - /** - * The path to the directory where the staged Deadline Docker recipes are. - */ - readonly dockerRecipesStagePath: string; - /** * Our self-signed root CA certificate for the internal endpoints in the farm. */ @@ -81,12 +77,27 @@ export interface ServiceTierProps extends cdk.StackProps { */ readonly ublLicenses?: UsageBasedLicense[]; + /** + * Version of Deadline to use. + * @default The latest available release of Deadline is used + */ + readonly deadlineVersion?: string; + + /** + * Whether the AWS Thinkbox End-User License Agreement is accepted or not + */ + readonly acceptAwsThinkboxEula: AwsThinkboxEulaAcceptance; } /** * The service tier contains all "business-logic" constructs (e.g. Render Queue, UBL Licensing / License Forwarder, etc.). */ export class ServiceTier extends cdk.Stack { + /** + * A bastion host to connect to the render farm with. + */ + public readonly bastion: BastionHostLinux; + /** * The render queue. */ @@ -98,9 +109,9 @@ export class ServiceTier extends cdk.Stack { public readonly ublLicensing?: UsageBasedLicensing; /** - * A bastion host to connect to the render farm with. + * The version of Deadline configured by the app. */ - public readonly bastion: BastionHostLinux; + public readonly version: VersionQuery; /** * Initializes a new instance of {@link ServiceTier}. @@ -133,18 +144,23 @@ export class ServiceTier extends cdk.Stack { location: '/mnt/efs', }); - const recipes = new ThinkboxDockerRecipes(this, 'Image', { - stage: Stage.fromDirectory(props.dockerRecipesStagePath), + this.version = new VersionQuery(this, 'Version', { + version: props.deadlineVersion, }); const repository = new Repository(this, 'Repository', { vpc: props.vpc, - version: recipes.version, + version: this.version, database: props.database, fileSystem: props.fileSystem, repositoryInstallationTimeout: Duration.minutes(20), }); + const images = new ThinkboxDockerImages(this, 'Images', { + version: this.version, + userAwsThinkboxEulaAcceptance: props.acceptAwsThinkboxEula, + }); + const serverCert = new X509CertificatePem(this, 'RQCert', { subject: { cn: `renderqueue.${props.dnsZone.zoneName}`, @@ -153,10 +169,11 @@ export class ServiceTier extends cdk.Stack { }, signingCertificate: props.rootCa, }); + this.renderQueue = new RenderQueue(this, 'RenderQueue', { vpc: props.vpc, - images: recipes.renderQueueImages, - repository: repository, + images: images, + repository, hostname: { hostname: 'renderqueue', zone: props.dnsZone, @@ -167,7 +184,7 @@ export class ServiceTier extends cdk.Stack { }, internalProtocol: ApplicationProtocol.HTTPS, }, - version: recipes.version, + version: this.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. @@ -191,7 +208,7 @@ export class ServiceTier extends cdk.Stack { this.ublLicensing = new UsageBasedLicensing(this, 'UBLLicensing', { vpc: props.vpc, - images: recipes.ublImages, + images: images, licenses: props.ublLicenses, renderQueue: this.renderQueue, certificateSecret: ublCertSecret, diff --git a/examples/deadline/All-In-AWS-Infrastructure-Basic/ts/package.json b/examples/deadline/All-In-AWS-Infrastructure-Basic/ts/package.json index 3c813cf1d..36cad1fc8 100644 --- a/examples/deadline/All-In-AWS-Infrastructure-Basic/ts/package.json +++ b/examples/deadline/All-In-AWS-Infrastructure-Basic/ts/package.json @@ -4,10 +4,6 @@ "bin": { "app": "bin/app.js" }, - "config": { - "deadline_ver": "10.1.12.1", - "stage_path": "stage" - }, "scripts": { "build": "tsc", "build+test": "yarn build && yarn test", diff --git a/packages/aws-rfdk/lib/deadline/README.md b/packages/aws-rfdk/lib/deadline/README.md index 2c67e25a3..03f747e9b 100644 --- a/packages/aws-rfdk/lib/deadline/README.md +++ b/packages/aws-rfdk/lib/deadline/README.md @@ -21,12 +21,14 @@ _**Note:** RFDK constructs currently support Deadline 10.1.9 and later._ - [Configuring Deadline Client Connections](#configuring-deadline-client-connections) - [Stage](#stage) - [Staging Docker Recipes](#staging-docker-recipes) +- [ThinkboxDockerImages](#thinkbox-docker-images) - [Usage Based Licensing](#usage-based-licensing-ubl) - [Docker Container Images](#usage-based-licensing-docker-container-images) - [Uploading Binary Secrets to SecretsManager](#uploading-binary-secrets-to-secretsmanager) - [VersionQuery](#versionquery) - [Worker Fleet](#worker-fleet) - [Health Monitoring](#worker-fleet-health-monitoring) + - [Custom Worker Instance Startup](#custom-worker-instance-startup) ## Render Queue @@ -46,15 +48,18 @@ _**Note:** The number of instances running the Render Queue is currently limited The following example outlines how to construct a `RenderQueue`: ```ts -const recipes = new ThinkboxDockerRecipes(stack, 'Recipes', { - stage: Stage.fromDirectory(/* ... */) -}); const version = VersionQuery.exactString(stack, 'Version', '1.2.3.4'); +const images = new ThinkboxDockerImages(stack, 'Images', { + version: version, + // Change this to AwsThinkboxEulaAcceptance.USER_ACCEPTS_AWS_THINKBOX_EULA to accept the terms + // of the AWS Thinkbox End User License Agreement + userAwsThinkboxEulaAcceptance: AwsThinkboxEulaAcceptance.USER_REJECTS_AWS_THINKBOX_EULA, +}); const repository = new Repository(stack, 'Repository', { /* ...*/}); const renderQueue = new RenderQueue(stack, 'RenderQueue', { vpc: vpc, - images: recipes.renderQueueImages, + images: images, version: version, repository: repository, }); @@ -62,13 +67,11 @@ const renderQueue = new RenderQueue(stack, 'RenderQueue', { ### Render Queue Docker Container Images -The `RenderQueue` currently requires only one Docker container image for the Deadline Remote Connection Server (RCS). An RCS image must satisfy the following criteria to be compatible with RFDK: +The `RenderQueue` currently requires only one Docker container image for the Deadline Remote Connection Server (RCS). -- Deadline Client must be installed -- The port the RCS will be listening on must be exposed -- The default command must launch the RCS +AWS Thinkbox provides Docker recipes and images that set these up for you. These can be accessed with the `ThinkboxDockerRecipes` and `ThinkboxDockerImages` constructs (see [Staging Docker Recipes](#staging-docker-recipes) and [Thinkbox Docker Images](#thinkbox-docker-images) respectively). -AWS Thinkbox provides Docker recipes that set these up for you. These can be accessed with the `ThinkboxDockerRecipes` class (see [Staging Docker Recipes](#staging-docker-recipes)). +If you need to customize the Docker images of your Render Queue, it is recommended that you stage the recipes and modify them as desired. Once staged to a directory, consult the `README.md` file in the root for details on how to extend the recipes. ### Render Queue Encryption @@ -217,6 +220,73 @@ We recommend adding a `script` field in your `package.json` that runs the `stage With this in place, staging the Deadline Docker recipes can be done simply by running `npm run stage`. +## Thinkbox Docker Images + +Thinkbox publishes Docker images for use with RFDK to a public ECR repository. The `ThinkboxDockerImages` construct +simplifies using these images. + +--- + +_**Note:** Deadline is licensed under the terms of the +[AWS Thinkbox End User License Agreement](https://www.awsthinkbox.com/end-user-license-agreement). Users of +`ThinkboxDockerImages` must explicitly signify their acceptance of the terms of the AWS Thinkbox EULA through +`userAwsThinkboxEulaAcceptance` property. By default, `userAwsThinkboxEulaAcceptance` is set to rejection._ + +--- + + +To use it, simply create one: + +```ts +// This will provide Docker container images for the latest Deadline release +const images = new ThinkboxDockerImages(scope, 'Images', { + // Change this to AwsThinkboxEulaAcceptance.USER_ACCEPTS_AWS_THINKBOX_EULA to accept the terms + // of the AWS Thinkbox End User License Agreement + userAwsThinkboxEulaAcceptance: AwsThinkboxEulaAcceptance.USER_REJECTS_AWS_THINKBOX_EULA, +}); +``` + +If you desire a specific version of Deadline, you can supply a version with: + +```ts +// Specify a version of Deadline +const version = new VersionQuery(scope, 'Version', { + version: '10.1.12', +}); + +// This will provide Docker container images for the specified version of Deadline +const images = new ThinkboxDockerImages(scope, 'Images', { + version: version, + // Change this to AwsThinkboxEulaAcceptance.USER_ACCEPTS_AWS_THINKBOX_EULA to accept the terms + // of the AWS Thinkbox End User License Agreement + userAwsThinkboxEulaAcceptance: AwsThinkboxEulaAcceptance.USER_REJECTS_AWS_THINKBOX_EULA, +}); +``` + +To use these images, you can use the expressive methods or provide the instance directly to downstream constructs: + +```ts +const renderQueue = new RenderQueue(scope, 'RenderQueue', { + images: images, + // ... +}); +const ubl = new UsageBasedLicensing(scope, 'RenderQueue', { + images: images, + // ... +}); + +// OR + +const renderQueue = new RenderQueue(scope, 'RenderQueue', { + images: images.forRenderQueue(), + // ... +}); +const ubl = new UsageBasedLicensing(scope, 'RenderQueue', { + images: images.forUsageBasedLicensing(), + // ... +}); +``` + ## Usage-Based Licensing (UBL) Usage-Based Licensing is an on-demand licensing model (see [Deadline Documentation](https://docs.thinkboxsoftware.com/products/deadline/10.1/1_User%20Manual/manual/licensing-usage-based.html)). The RFDK supports this type of licensing with the `UsageBasedLicensing` construct. This construct contains the following components: @@ -236,14 +306,18 @@ _**Note:** This construct is not usable in any China region._ The following example outlines how to construct `UsageBasedLicensing`: ```ts -const recipes = new ThinkboxDockerRecipes(stack, 'Recipes', { - stage: Stage.fromDirectory(/* ... */) +const version = new VersionQuery(stack, 'Version', '1.2.3.4'); +const images = new ThinkboxDockerImages(stack, 'Images', { + version: version, + // Change this to AwsThinkboxEulaAcceptance.USER_ACCEPTS_AWS_THINKBOX_EULA to accept the terms + // of the AWS Thinkbox End User License Agreement + userAwsThinkboxEulaAcceptance: AwsThinkboxEulaAcceptance.USER_REJECTS_AWS_THINKBOX_EULA, }); const ubl = new UsageBasedLicensing(stack, 'UsageBasedLicensing', { vpc: vpc, renderQueue: renderQueue, - images: recipes.ublImages, + images: images, licenses: [ UsageBasedLicense.forKrakatoa(/* ... */), /* ... */ ], certificateSecret: /* ... */, // This must be a binary secret (see below) memoryReservationMiB: /* ... */ @@ -252,12 +326,9 @@ const ubl = new UsageBasedLicensing(stack, 'UsageBasedLicensing', { ### Usage-Based Licensing Docker Container Images -`UsageBasedLicensing` currently requires only one Docker container image for the Deadline License Forwarder. A License Forwarder image must satisfy the following criteria to be compatible with RFDK: +`UsageBasedLicensing` currently requires only one Docker container image for the Deadline License Forwarder. -- Deadline Client must be installed -- The default command must launch the License Forwarder - -AWS Thinkbox provides Docker recipes that sets these up for you. These can be accessed with the `ThinkboxDockerRecipes` class (see [Staging Docker Recipes](#staging-docker-recipes)). +AWS Thinkbox provides Docker recipes that sets these up for you. These can be accessed with the `ThinkboxDockerRecipes` and `ThinkboxDockerImages` constructs (see [Staging Docker Recipes](#staging-docker-recipes) and [Thinkbox Docker Images](#thinkbox-docker-images) respectively). ### Uploading Binary Secrets to SecretsManager @@ -320,11 +391,12 @@ const workerFleet = new WorkerInstanceFleet(stack, 'WorkerFleet', { }); ``` -### User data scripts for the Worker configuration +### Custom Worker Instance Startup You have possibility to run user data scripts at various points during the Worker configuration lifecycle. To do this, subclass `InstanceUserDataProvider` and override desired methods: + ```ts class UserDataProvider extends InstanceUserDataProvider { preCloudWatchAgent(host: IHost): void { @@ -337,5 +409,4 @@ const fleet = new WorkerInstanceFleet(stack, 'WorkerFleet', { workerMachineImage: /* ... */, userDataProvider: new UserDataProvider(stack, 'UserDataProvider'), }); - -``` \ No newline at end of file +``` diff --git a/packages/aws-rfdk/lib/deadline/lib/index.ts b/packages/aws-rfdk/lib/deadline/lib/index.ts index 0b9dea368..8d45c2d40 100644 --- a/packages/aws-rfdk/lib/deadline/lib/index.ts +++ b/packages/aws-rfdk/lib/deadline/lib/index.ts @@ -11,6 +11,7 @@ export * from './worker-fleet'; export * from './render-queue'; export * from './render-queue-ref'; export * from './stage'; +export * from './thinkbox-docker-images'; export * from './thinkbox-docker-recipes'; export * from './version'; export * from './version-query'; diff --git a/packages/aws-rfdk/lib/deadline/lib/thinkbox-docker-images.ts b/packages/aws-rfdk/lib/deadline/lib/thinkbox-docker-images.ts new file mode 100644 index 000000000..568aa142f --- /dev/null +++ b/packages/aws-rfdk/lib/deadline/lib/thinkbox-docker-images.ts @@ -0,0 +1,280 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { randomBytes } from 'crypto'; +import * as path from 'path'; + +import { + ContainerImage, + RepositoryImage, +} from '@aws-cdk/aws-ecs'; +import { + Code, + SingletonFunction, + Runtime, +} from '@aws-cdk/aws-lambda'; +import { RetentionDays } from '@aws-cdk/aws-logs'; +import { + Construct, + CustomResource, + Duration, + Token, +} from '@aws-cdk/core'; + +import { + IVersion, + RenderQueueImages, + ThinkboxManagedDeadlineDockerRecipes, + UsageBasedLicensingImages, +} from '.'; + +/** + * Choices for signifying the user's stance on the terms of the AWS Thinkbox End-User License Agreement (EULA). + * See: https://www.awsthinkbox.com/end-user-license-agreement + */ +export enum AwsThinkboxEulaAcceptance { + /** + * The user signifies their explicit rejection of the tems of the AWS Thinkbox EULA. + * + * See: https://www.awsthinkbox.com/end-user-license-agreement + */ + USER_REJECTS_AWS_THINKBOX_EULA = 0, + + /** + * The user signifies their explicit acceptance of the terms of the AWS Thinkbox EULA. + * + * See: https://www.awsthinkbox.com/end-user-license-agreement + */ + USER_ACCEPTS_AWS_THINKBOX_EULA = 1, +} + +/** + * Interface to specify the properties when instantiating a {@link ThinkboxDockerImages} instnace. + */ +export interface ThinkboxDockerImagesProps { + /** + * The Deadline version to obtain images for. + * @default latest + */ + readonly version?: IVersion; + + /** + * Deadline is licensed under the terms of the AWS Thinkbox End-User License Agreement (see: https://www.awsthinkbox.com/end-user-license-agreement). + * Users of ThinkboxDockerImages must explicitly signify their acceptance of the terms of the AWS Thinkbox EULA through this + * property before the {@link ThinkboxDockerImages} will be allowed to deploy Deadline. + */ + // Developer note: It is a legal requirement that the default be USER_REJECTS_AWS_THINKBOX_EULA. + readonly userAwsThinkboxEulaAcceptance: AwsThinkboxEulaAcceptance; +} + +/** + * An API for interacting with publicly available Deadline container images published by AWS Thinkbox. + * + * This provides container images as required by RFDK's Deadline constructs such as + * + * * {@link @aws-rfdk/deadline#RenderQueue} + * * {@link @aws-rfdk/deadline#UsageBasedLicensing} + * + * Successful usage of the published Deadline container images with this class requires: + * + * 1) Explicit acceptance of the terms of the AWS Thinkbox End User License Agreement, under which Deadline is + * distributed; and + * 2) The lambda on which the custom resource looks up the Thinkbox container images is able to make HTTPS + * requests to the official AWS Thinbox download site: https://downloads.thinkboxsoftware.com + * + * Resources Deployed + * ------------------------ + * - A Lambda function containing a script to look up the AWS Thinkbox container image registry + * + * Security Considerations + * ------------------------ + * - CDK deploys the code for this lambda as an S3 object in the CDK bootstrap bucket. You must limit write access to + * your CDK bootstrap bucket to prevent an attacker from modifying the actions performed by these scripts. We strongly + * recommend that you either enable Amazon S3 server access logging on your CDK bootstrap bucket, or enable AWS + * CloudTrail on your account to assist in post-incident analysis of compromised production environments. + * + * For example, to construct a RenderQueue using the images: + * + * ```ts + * import { App, Stack, Vpc } from '@aws-rfdk/core'; + * import { AwsThinkboxEulaAcceptance, RenderQueue, Repository, ThinkboxDockerImages, VersionQuery } from '@aws-rfdk/deadline'; + * const app = new App(); + * const stack = new Stack(app, 'Stack'); + * const vpc = new Vpc(stack, 'Vpc'); + * const version = new VersionQuery(stack, 'Version', { + * version: '10.1.12', + * }); + * const images = new ThinkboxDockerImages(stack, 'Image', { + * version, + * // Change this to AwsThinkboxEulaAcceptance.USER_ACCEPTS_AWS_THINKBOX_EULA to accept the terms + * // of the AWS Thinkbox End User License Agreement + * userAwsThinkboxEulaAcceptance: AwsThinkboxEulaAcceptance.USER_REJECTS_AWS_THINKBOX_EULA, + * }); + * const repository = new Repository(stack, 'Repository', { + * vpc, + * version, + * }); + * + * const renderQueue = new RenderQueue(stack, 'RenderQueue', { + * images: images.forRenderQueue(), + * // ... + * }); + * ``` + */ +export class ThinkboxDockerImages extends Construct { + /** + * The AWS Thinkbox licensing message that is presented to the user if they create an instance of + * this class without explicitly accepting the AWS Thinkbox EULA. + * + * Note to developers: The text of this string is a legal requirement, and must not be altered + * witout approval. + */ + private static readonly AWS_THINKBOX_EULA_MESSAGE: string = ` +The ThinkboxDockerImages will install Deadline onto one or more EC2 instances. + +Deadline is provided by AWS Thinkbox under the AWS Thinkbox End User License +Agreement (EULA). By installing Deadline, you are agreeing to the terms of this +license. Follow the link below to read the terms of the AWS Thinkbox EULA. + +https://www.awsthinkbox.com/end-user-license-agreement + +By using the ThinkboxDockerImages to install Deadline you agree to the terms of +the AWS Thinkbox EULA. + +Please set the userAwsThinkboxEulaAcceptance property to +USER_ACCEPTS_AWS_THINKBOX_EULA to signify your acceptance of the terms of the +AWS Thinkbox EULA. +`; + + /** + * A {@link DockerImageAsset} that can be used to build Thinkbox's Deadline RCS Docker Recipe into a + * container image that can be deployed in CDK. + * + * @param scope The parent scope + * @param id The construct ID + */ + public readonly remoteConnectionServer: ContainerImage; + + /** + * A {@link DockerImageAsset} that can be used to build Thinkbox's Deadline License Forwarder Docker Recipe into a + * container image that can be deployed in CDK. + * + * @param scope The parent scope + * @param id The construct ID + */ + public readonly licenseForwarder: ContainerImage; + + /** + * The version of Deadline installed in the container images + */ + private readonly version?: IVersion; + + /** + * The base URI for AWS Thinkbox published Deadline ECR images. + */ + private readonly ecrBaseURI: string; + + /** + * Whether the user has accepted the AWS Thinkbox EULA + */ + private readonly userAwsThinkboxEulaAcceptance: AwsThinkboxEulaAcceptance; + + constructor(scope: Construct, id: string, props: ThinkboxDockerImagesProps) { + super(scope, id); + + this.userAwsThinkboxEulaAcceptance = props.userAwsThinkboxEulaAcceptance; + this.version = props?.version; + + const lambdaCode = Code.fromAsset(path.join(__dirname, '..', '..', 'lambdas', 'nodejs')); + + const lambdaFunc = new SingletonFunction(this, 'VersionProviderFunction', { + uuid: '08553416-1fc9-4be9-a818-609a31ae1b5b', + description: 'Used by the ThinkboxDockerImages construct to look up the ECR repositories where AWS Thinkbox publishes Deadline container images.', + code: lambdaCode, + runtime: Runtime.NODEJS_12_X, + handler: 'ecr-provider.handler', + timeout: Duration.seconds(30), + logRetention: RetentionDays.ONE_WEEK, + }); + + const ecrProvider = new CustomResource(this, 'ThinkboxEcrProvider', { + serviceToken: lambdaFunc.functionArn, + properties: { + // create a random string that will force the Lambda to "update" on each deployment. Changes to its output will + // be propagated to any CloudFormation resource providers that reference the output ARN + ForceRun: this.forceRun(), + }, + resourceType: 'Custom::RFDK_EcrProvider', + }); + + this.node.defaultChild = ecrProvider; + + this.ecrBaseURI = ecrProvider.getAtt('EcrURIPrefix').toString(); + + this.remoteConnectionServer = this.ecrImageForRecipe(ThinkboxManagedDeadlineDockerRecipes.REMOTE_CONNECTION_SERVER); + this.licenseForwarder = this.ecrImageForRecipe(ThinkboxManagedDeadlineDockerRecipes.LICENSE_FORWARDER); + } + + protected onValidate() { + const errors: string[] = []; + + // Users must accept the AWS Thinkbox EULA to use the container images + if (this.userAwsThinkboxEulaAcceptance !== AwsThinkboxEulaAcceptance.USER_ACCEPTS_AWS_THINKBOX_EULA) { + errors.push(ThinkboxDockerImages.AWS_THINKBOX_EULA_MESSAGE); + } + + return errors; + } + + private ecrImageForRecipe(recipe: ThinkboxManagedDeadlineDockerRecipes): RepositoryImage { + let registryName = `${this.ecrBaseURI}${recipe}`; + if (this.versionString) { + registryName += `:${this.versionString}`; + } + return ContainerImage.fromRegistry( + registryName, + ); + } + + /** + * Returns container images for use with the {@link RenderQueue} construct + */ + public forRenderQueue(): RenderQueueImages { + return this; + } + + /** + * Returns container images for use with the {@link UsageBasedLicensing} construct + */ + public forUsageBasedLicensing(): UsageBasedLicensingImages { + return this; + } + + /** + * A string representation of the Deadline version to retrieve images for. + * + * This can be undefined - in which case the latest available version of Deadline is used. + */ + private get versionString(): string | undefined { + function numAsString(num: number): string { + return Token.isUnresolved(num) ? Token.asString(num) : num.toString(); + } + + const version = this.version; + if (version) { + const major = numAsString(version.majorVersion); + const minor = numAsString(version.minorVersion); + const release = numAsString(version.releaseVersion); + + return `${major}.${minor}.${release}`; + } + + return undefined; + } + + private forceRun(): string { + return randomBytes(32).toString('base64').slice(0, 32); + } +} diff --git a/packages/aws-rfdk/lib/deadline/test/thinkbox-docker-images.test.ts b/packages/aws-rfdk/lib/deadline/test/thinkbox-docker-images.test.ts new file mode 100644 index 000000000..bec255ac9 --- /dev/null +++ b/packages/aws-rfdk/lib/deadline/test/thinkbox-docker-images.test.ts @@ -0,0 +1,235 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + arrayWith, + expect as expectCDK, + haveResource, + objectLike, + stringLike, + SynthUtils, +} from '@aws-cdk/assert'; +import { + Compatibility, + ContainerDefinition, + ContainerImage, + TaskDefinition, +} from '@aws-cdk/aws-ecs'; +import { + App, + CustomResource, + Stack, + Token, +} from '@aws-cdk/core'; + +import { + AwsThinkboxEulaAcceptance, + IVersion, + RenderQueueImages, + ThinkboxDockerImages, + ThinkboxManagedDeadlineDockerRecipes, + UsageBasedLicensingImages, + VersionQuery, +} from '../lib'; + +describe('ThinkboxDockerRecipes', () => { + let app: App; + let depStack: Stack; + let stack: Stack; + let images: ThinkboxDockerImages; + let userAwsThinkboxEulaAcceptance: AwsThinkboxEulaAcceptance; + + describe('defaults', () => { + beforeEach(() => { + // GIVEN + app = new App(); + stack = new Stack(app, 'Stack'); + userAwsThinkboxEulaAcceptance = AwsThinkboxEulaAcceptance.USER_ACCEPTS_AWS_THINKBOX_EULA; + + // WHEN + images = new ThinkboxDockerImages(stack, 'Images', { + userAwsThinkboxEulaAcceptance, + }); + }); + + test('fails validation when EULA is not accepted', () =>{ + // GIVEN + const newStack = new Stack(app, 'NewStack'); + const expectedError = ` +The ThinkboxDockerImages will install Deadline onto one or more EC2 instances. + +Deadline is provided by AWS Thinkbox under the AWS Thinkbox End User License +Agreement (EULA). By installing Deadline, you are agreeing to the terms of this +license. Follow the link below to read the terms of the AWS Thinkbox EULA. + +https://www.awsthinkbox.com/end-user-license-agreement + +By using the ThinkboxDockerImages to install Deadline you agree to the terms of +the AWS Thinkbox EULA. + +Please set the userAwsThinkboxEulaAcceptance property to +USER_ACCEPTS_AWS_THINKBOX_EULA to signify your acceptance of the terms of the +AWS Thinkbox EULA. +`; + userAwsThinkboxEulaAcceptance = AwsThinkboxEulaAcceptance.USER_REJECTS_AWS_THINKBOX_EULA; + new ThinkboxDockerImages(newStack, 'Images', { + userAwsThinkboxEulaAcceptance, + }); + + // WHEN + function synth() { + SynthUtils.synthesize(newStack); + } + + // THEN + expect(synth).toThrow(expectedError); + }); + + test('creates Custom::RFDK_ECR_PROVIDER', () => { + // THEN + expectCDK(stack).to(haveResource('Custom::RFDK_EcrProvider', { + ForceRun: stringLike('*'), + })); + }); + + describe('provides container images for', () => { + test.each<[string, () => ContainerImage, ThinkboxManagedDeadlineDockerRecipes]>([ + [ + 'RCS', + () => { + return images.remoteConnectionServer; + }, + ThinkboxManagedDeadlineDockerRecipes.REMOTE_CONNECTION_SERVER, + ], + [ + 'License Forwarder', + () => { + return images.licenseForwarder; + }, + ThinkboxManagedDeadlineDockerRecipes.LICENSE_FORWARDER, + ], + ])('%s', (_label, imageGetter, recipe) => { + // GIVEN + const taskDefStack = new Stack(app, 'TaskDefStack'); + const image = imageGetter(); + const taskDefinition = new TaskDefinition(taskDefStack, 'TaskDef', { + compatibility: Compatibility.EC2, + }); + const ecrProvider = images.node.defaultChild as CustomResource; + const expectedImage = `${ecrProvider.getAtt('EcrURIPrefix')}${recipe}`; + + // WHEN + new ContainerDefinition(taskDefStack, 'ContainerDef', { + image, + taskDefinition, + memoryReservationMiB: 1024, + }); + + // THEN + expectCDK(taskDefStack).to(haveResource('AWS::ECS::TaskDefinition', { + ContainerDefinitions: arrayWith(objectLike({ + Image: taskDefStack.resolve(expectedImage), + })), + })); + }); + }); + + describe('.forRenderQueue()', () => { + let rcsImage: ContainerImage; + let rqImages: RenderQueueImages; + + beforeEach(() => { + // GIVEN + rcsImage = images.remoteConnectionServer; + + // WHEN + rqImages = images.forRenderQueue(); + }); + + test('returns correct RCS image', () => { + // THEN + expect(rqImages.remoteConnectionServer).toBe(rcsImage); + }); + }); + + describe('.forUsageBasedLicensing()', () => { + let lfImage: ContainerImage; + let ublImages: UsageBasedLicensingImages; + + beforeEach(() => { + // GIVEN + lfImage = images.licenseForwarder; + + // WHEN + ublImages = images.forUsageBasedLicensing(); + }); + + test('returns correct RCS image', () => { + // THEN + expect(ublImages.licenseForwarder).toBe(lfImage); + }); + }); + }); + + describe('with version', () => { + let version: IVersion; + + beforeEach(() => { + // GIVEN + app = new App(); + depStack = new Stack(app, 'DepStack'); + version = new VersionQuery(depStack, 'Version'); + stack = new Stack(app, 'Stack'); + + // WHEN + images = new ThinkboxDockerImages(stack, 'Images', { + version, + userAwsThinkboxEulaAcceptance, + }); + }); + + describe('provides container images for', () => { + test.each<[string, () => ContainerImage, ThinkboxManagedDeadlineDockerRecipes]>([ + [ + 'RCS', + () => { + return images.remoteConnectionServer; + }, + ThinkboxManagedDeadlineDockerRecipes.REMOTE_CONNECTION_SERVER, + ], + [ + 'License Forwarder', + () => { + return images.licenseForwarder; + }, + ThinkboxManagedDeadlineDockerRecipes.LICENSE_FORWARDER, + ], + ])('%s', (_label, imageGetter, recipe) => { + // GIVEN + const taskDefStack = new Stack(app, 'TaskDefStack'); + const image = imageGetter(); + const taskDefinition = new TaskDefinition(taskDefStack, 'TaskDef', { + compatibility: Compatibility.EC2, + }); + const ecrProvider = images.node.defaultChild as CustomResource; + const expectedImage = `${ecrProvider.getAtt('EcrURIPrefix')}${recipe}:${Token.asString(version.majorVersion)}.${Token.asString(version.minorVersion)}.${Token.asString(version.releaseVersion)}`; + + // WHEN + new ContainerDefinition(taskDefStack, 'ContainerDef', { + image, + taskDefinition, + memoryReservationMiB: 1024, + }); + + // THEN + expectCDK(taskDefStack).to(haveResource('AWS::ECS::TaskDefinition', { + ContainerDefinitions: arrayWith(objectLike({ + Image: taskDefStack.resolve(expectedImage), + })), + })); + }); + }); + }); +}); diff --git a/packages/aws-rfdk/lib/lambdas/nodejs/ecr-provider/handler.ts b/packages/aws-rfdk/lib/lambdas/nodejs/ecr-provider/handler.ts new file mode 100644 index 000000000..b570dd041 --- /dev/null +++ b/packages/aws-rfdk/lib/lambdas/nodejs/ecr-provider/handler.ts @@ -0,0 +1,93 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* eslint-disable no-console */ + +import { LambdaContext } from '../lib/aws-lambda'; +import { CfnRequestEvent, SimpleCustomResource } from '../lib/custom-resource'; +import { + ThinkboxEcrProvider, +} from '../lib/ecr-provider'; + +/** + * The input to this Custom Resource + */ +export interface ThinkboxEcrProviderResourceProperties { + /** + * A random string that forces the Lambda to run again and obtain the latest ECR. + */ + readonly ForceRun?: string; +} + +/** + * Output of this Custom Resource. + */ +export interface ThinkboxEcrProviderResourceOutput { + /** + * The URI prefix of the ECR repositories containing Deadline container images published by AWS Thinkbox. This can be + * suffixed with the recipe name to get a Deadline image's complete ECR repository URI. + */ + readonly EcrURIPrefix: string; +} + +/** + * This custom resource will parse and return the base ECR ARN or URI containing Thinkbox published Docker Images. + * A global ECR base URI is returned. + */ +export class ThinkboxEcrProviderResource extends SimpleCustomResource { + readonly ecrProvider: ThinkboxEcrProvider; + + constructor() { + super(); + this.ecrProvider = new ThinkboxEcrProvider(); + } + + /** + * @inheritdoc + */ + public validateInput(data: object): boolean { + return this.isEcrProviderResourceProperties(data); + } + + /** + * @inheritdoc + */ + public async doCreate(_physicalId: string, _resourceProperties: ThinkboxEcrProviderResourceProperties): Promise { + const result = { + EcrURIPrefix: await this.ecrProvider.getGlobalEcrBaseURI(), + }; + console.log('result = '); + console.log(JSON.stringify(result, null, 4)); + return result; + } + + /** + * @inheritdoc + */ + /* istanbul ignore next */ + public async doDelete(_physicalId: string, _resourceProperties: ThinkboxEcrProviderResourceProperties): Promise { + // Nothing to do -- we don't modify anything. + return; + } + + private isEcrProviderResourceProperties(value: any): value is ThinkboxEcrProviderResourceProperties { + if (!value || typeof(value) !== 'object' || Array.isArray(value)) { return false; } + + function isOptionalString(val: any): val is string | undefined { + return val === undefined || typeof(val) == 'string'; + } + + return isOptionalString(value.ForceRun); + } +} + +/** + * 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 ecrProvider = new ThinkboxEcrProviderResource(); + return await ecrProvider.handler(event, context); +} diff --git a/packages/aws-rfdk/lib/lambdas/nodejs/ecr-provider/index.ts b/packages/aws-rfdk/lib/lambdas/nodejs/ecr-provider/index.ts new file mode 100644 index 000000000..761a8c9dd --- /dev/null +++ b/packages/aws-rfdk/lib/lambdas/nodejs/ecr-provider/index.ts @@ -0,0 +1,6 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './handler'; diff --git a/packages/aws-rfdk/lib/lambdas/nodejs/ecr-provider/test/ecr-provider.test.ts b/packages/aws-rfdk/lib/lambdas/nodejs/ecr-provider/test/ecr-provider.test.ts new file mode 100644 index 000000000..dcb1b7032 --- /dev/null +++ b/packages/aws-rfdk/lib/lambdas/nodejs/ecr-provider/test/ecr-provider.test.ts @@ -0,0 +1,433 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EventEmitter } from 'events'; +import * as fs from 'fs'; + +import { ThinkboxEcrProvider } from '../../lib/ecr-provider'; + +jest.mock('fs'); +jest.mock('https'); + +describe('ThinkboxEcrProvider', () => { + /** + * Suite of parametrized tests for testing the ECR index schema validation. + * + * The suite is an array of tests, where each test should fail validation. Each test is represented as an array of two + * elements: [name, indexObject] + * + * - The first element is the name describing what is contained in the value + * - The second element is the value that should be JSON encoded and supplied to the ThinkboxEcrProvider + */ + const INDEX_SCHEMA_VALIDATION_SUITE: Array<[string, any]> = [ + [ + 'array', + [], + ], + [ + 'number', + 1, + ], + [ + 'string', + 'abc', + ], + [ + 'object missing registry', + { + products: { + deadline: { + namespace: 'a', + }, + }, + }, + ], + [ + 'object with registry with wrong type', + { + registry: 1, + products: { + deadline: { + namespace: 'a', + }, + }, + }, + ], + [ + 'object missing products', + { + registry: { + uri: 'a', + }, + }, + ], + [ + 'object with products with wrong type', + { + registry: { + uri: 'a', + }, + products: 1, + }, + ], + [ + 'object with registry missing uri', + { + registry: {}, + products: { + deadline: { + namespace: 'a', + }, + }, + }, + ], + [ + 'object with registry uri with wrong type', + { + registry: { + uri: 1, + }, + products: { + deadline: { + namespace: 'a', + }, + }, + }, + ], + [ + 'object with missing products.deadline', + { + registry: { + uri: 1, + }, + products: {}, + }, + ], + [ + 'object with products.deadline with wrong type', + { + registry: { + uri: 1, + }, + products: { + deadline: 1, + }, + }, + ], + [ + 'object with missing products.deadline.namespace', + { + registry: { + uri: 1, + }, + products: { + deadline: {}, + }, + }, + ], + [ + 'object with products.deadline.namespace with wrong type', + { + registry: { + uri: 1, + }, + products: { + deadline: { + namespace: 1, + }, + }, + }, + ], + ]; + + let ecrProvider: ThinkboxEcrProvider; + + describe('without indexPath', () => { + class MockResponse extends EventEmitter { + public statusCode: number = 200; + } + + let request: EventEmitter; + let response: MockResponse; + + /** + * Mock implementation of a successful HTTPS GET request + * + * @param _url The URL of the HTTPS request + * @param callback The callback to call when a response is available + */ + function httpGetMockSuccess(_url: string, callback: (_request: any) => void) { + if (callback) { + callback(response); + } + return request; + } + + /** + * Mock implementation of a failed HTTPS GET request + * + * @param _url The URL of the HTTPS request + * @param _callback The callback to call when a response is available + */ + function httpGetMockError(_url: string, _callback: (request: any) => void) { + return request; + } + + beforeEach(() => { + request = new EventEmitter(); + response = new MockResponse(); + jest.requireMock('https').get.mockImplementation(httpGetMockSuccess); + + // GIVEN + ecrProvider = new ThinkboxEcrProvider(); + }); + + const EXPECTED_URL = 'https://downloads.thinkboxsoftware.com/thinkbox_ecr.json'; + test(`gets ${EXPECTED_URL} for global lookup`, async () => { + // GIVEN + const registryUri = 'registryUri'; + const deadlineNamespace = 'namespace'; + const mockData = { + registry: { + uri: registryUri, + }, + products: { + deadline: { + namespace: deadlineNamespace, + }, + }, + }; + + // WHEN + const promise = ecrProvider.getGlobalEcrBaseURI(); + response.emit('data', JSON.stringify(mockData)); + response.emit('end'); + await promise; + + // THEN + // should make an HTTPS request for the ECR index + expect(jest.requireMock('https').get) + .toBeCalledWith( + EXPECTED_URL, + expect.any(Function), + ); + }); + + describe('.getGlobalEcrBaseArn()', () => { + test('obtains global prefix from index', async () => { + // GIVEN + const registryUri = 'registryUri'; + const deadlineNamespace = 'namespace'; + const mockData = { + registry: { + uri: registryUri, + }, + products: { + deadline: { + namespace: deadlineNamespace, + }, + }, + }; + const expectedBaseArn = `${registryUri}/${deadlineNamespace}`; + + // WHEN + const promise = ecrProvider.getGlobalEcrBaseURI(); + response.emit('data', JSON.stringify(mockData)); + response.emit('end'); + + // THEN + await expect(promise) + .resolves + .toEqual(expectedBaseArn); + }); + + test('handles request errors', async () => { + // GIVEN + const error = new Error('test'); + jest.requireMock('https').get.mockImplementation(httpGetMockError); + function simulateRequestError() { + request.emit('error', error); + }; + + // WHEN + const promise = ecrProvider.getGlobalEcrBaseURI(); + simulateRequestError(); + + // THEN + await expect(promise) + .rejects + .toThrowError(error); + }); + + test.each([ + [404], + [401], + [500], + ])('handles %d response errors', async (statusCode: number) => { + // GIVEN + response.statusCode = statusCode; + + // WHEN + const promise = ecrProvider.getGlobalEcrBaseURI(); + response.emit('data', ''); + response.emit('end'); + + // THEN + await expect(promise) + .rejects + .toThrowError(`Expected status code 200, but got ${statusCode}`); + }); + + test('fails on bad JSON', async () => { + // GIVEN + const responseBody = 'this is not json'; + + // WHEN + const promise = ecrProvider.getGlobalEcrBaseURI(); + response.emit('data', responseBody); + response.emit('end'); + + // THEN + await expect(promise) + .rejects + .toThrow(/^ECR index file contains invalid JSON: ".*"$/); + }); + + describe('index schema validation', () => { + test.each(INDEX_SCHEMA_VALIDATION_SUITE)('fails when fetching %s', async (_name: string, value: any) => { + // WHEN + const promise = ecrProvider.getGlobalEcrBaseURI(); + response.emit('data', JSON.stringify(value)); + response.emit('end'); + + // THEN + await expect(promise) + .rejects + .toThrowError(/^expected .+ to be an? .+ but got .+$/); + }); + }); + }); + }); + + describe('with indexPath', () => { + // GIVEN + const registryUri = 'registryUri'; + const deadlineNamespace = 'deadlineNamespace'; + const indexPath = 'somefile'; + const mockData = { + registry: { + uri: registryUri, + }, + products: { + deadline: { + namespace: deadlineNamespace, + }, + }, + }; + const globalURIPrefix = `${registryUri}/${deadlineNamespace}`; + + beforeEach(() => { + // WHEN + const existsSync: jest.Mock = jest.requireMock('fs').existsSync; + const readFileSync: jest.Mock = jest.requireMock('fs').readFileSync; + + // reset tracked calls to mocks + existsSync.mockReset(); + readFileSync.mockReset(); + // set the default mock implementations + existsSync.mockReturnValue(true); + readFileSync.mockReturnValue(JSON.stringify(mockData)); + + ecrProvider = new ThinkboxEcrProvider(indexPath); + }); + + describe('.getGlobalEcrBaseURI', () => { + let baseURIPromise: Promise; + + beforeEach(() => { + // WHEN + baseURIPromise = ecrProvider.getGlobalEcrBaseURI(); + }); + + test('reads file', async () => { + // THEN + await expect(baseURIPromise) + .resolves.toEqual(globalURIPrefix); + expect(fs.existsSync).toBeCalledTimes(1); + expect(fs.readFileSync).toBeCalledWith(indexPath, 'utf8'); + }); + + test('returns correct prefix', async () => { + await expect(baseURIPromise) + .resolves + .toEqual(globalURIPrefix); + }); + + test.each([ + ['existsSync'], + ['readFileSync'], + ])('fails on fs.%s exception', async (methodName: string) => { + // GIVEN + const error = new Error('message'); + jest.requireMock('fs')[methodName].mockImplementation(() => { + throw error; + }); + ecrProvider = new ThinkboxEcrProvider(indexPath); + + // WHEN + baseURIPromise = ecrProvider.getGlobalEcrBaseURI(); + + // THEN + await expect(baseURIPromise) + .rejects + .toThrowError(error); + }); + + describe('index schema validation', () => { + test.each(INDEX_SCHEMA_VALIDATION_SUITE)('fails when fetching %s', async (_name: string, value: any) => { + // GIVEN + jest.requireMock('fs').readFileSync.mockReturnValue(JSON.stringify(value)); + ecrProvider = new ThinkboxEcrProvider(indexPath); + + // WHEN + baseURIPromise = ecrProvider.getGlobalEcrBaseURI(); + + // THEN + await expect(baseURIPromise) + .rejects + .toThrowError(/^expected .+ to be an? .+ but got .+$/); + }); + }); + + test('fails on non-existent file', async () => { + // GIVEN + jest.requireMock('fs').existsSync.mockReturnValue(false); + ecrProvider = new ThinkboxEcrProvider(indexPath); + + // WHEN + baseURIPromise = ecrProvider.getGlobalEcrBaseURI(); + + // THEN + await expect(baseURIPromise) + .rejects + .toThrowError(`File "${indexPath}" was not found`); + }); + + test('fails on bad JSON', async () => { + // GIVEN + jest.requireMock('fs').readFileSync.mockReturnValue('bad json'); + ecrProvider = new ThinkboxEcrProvider(indexPath); + + // WHEN + baseURIPromise = ecrProvider.getGlobalEcrBaseURI(); + + // THEN + await expect(baseURIPromise) + .rejects + .toThrow(/^ECR index file contains invalid JSON: ".*"$/); + }); + }); + }); +}); diff --git a/packages/aws-rfdk/lib/lambdas/nodejs/ecr-provider/test/handler.test.ts b/packages/aws-rfdk/lib/lambdas/nodejs/ecr-provider/test/handler.test.ts new file mode 100644 index 000000000..b65e8997c --- /dev/null +++ b/packages/aws-rfdk/lib/lambdas/nodejs/ecr-provider/test/handler.test.ts @@ -0,0 +1,120 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* eslint-disable dot-notation */ + +import { ThinkboxEcrProviderResource } from '../handler'; + +jest.mock('../../lib/ecr-provider', () => { + class ThinkboxEcrProviderMock { + static readonly mocks = { + constructor: jest.fn(), + getGlobalEcrBaseURI: jest.fn, []>(() => { + return Promise.resolve('public.ecr.aws/deadline/'); + }), + }; + + constructor(indexFilePath?: string) { + ThinkboxEcrProviderMock.mocks.constructor(indexFilePath); + } + + getGlobalEcrBaseURI() { + return ThinkboxEcrProviderMock.mocks.getGlobalEcrBaseURI(); + } + } + + return { + ThinkboxEcrProvider: ThinkboxEcrProviderMock, + }; +}); + +jest.mock('https'); + +describe('ThinkboxEcrProviderResource', () => { + let ecrProviderResource: ThinkboxEcrProviderResource; + + beforeAll(() => { + // Suppress console output during tests + jest.spyOn(console, 'log').mockReturnValue(undefined); + jest.spyOn(console, 'info').mockReturnValue(undefined); + jest.spyOn(console, 'warn').mockReturnValue(undefined); + jest.spyOn(console, 'error').mockReturnValue(undefined); + }); + + beforeEach(() => { + ecrProviderResource = new ThinkboxEcrProviderResource(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + describe('.validateInput()', () => { + // Valid configurations + describe('should return true if', () => { + test.each([ + 'testValue', + undefined, + ])('{ForceRun=%s}', async (forceRun: string | undefined) => { + // GIVEN + const input = { + forceRun, + }; + + // WHEN + const returnValue = ecrProviderResource.validateInput(input); + + // THEN + expect(returnValue).toBeTruthy(); + }); + }); + + // Invalid configurations + const invalidConfigs = [ + { ForceRun: 1 }, + { ForceRun: [1] }, + { ForceRun: { a: 1 } }, + [], + 'abc', + 1, + ]; + describe('should return false if', () => { + test.each<[any, any]>(invalidConfigs.map((config: any) => { + return [ + JSON.stringify(config), + config, + ]; + }))('%s', async (_str: string, config: any) => { + expect(ecrProviderResource.validateInput(config)).toBeFalsy(); + }); + }); + }); + + describe('uses ThinkboxEcrProvider', () => { + test('global', async () => { + // GIVEN + const mockBaseURI = 'baseURI'; + const ThinkboxEcrProvider = jest.requireMock('../../lib/ecr-provider').ThinkboxEcrProvider; + ThinkboxEcrProvider.mocks.getGlobalEcrBaseURI.mockReturnValue(Promise.resolve(mockBaseURI)); + + // WHEN + const promise = ecrProviderResource.doCreate('someID', { + ForceRun: 'forceRun', + }); + const result = await promise; + + // THEN + expect(ThinkboxEcrProvider.mocks.constructor).toHaveBeenCalledTimes(1); + expect(ThinkboxEcrProvider.mocks.getGlobalEcrBaseURI).toHaveBeenCalledTimes(1); + expect(result).toEqual({ + EcrURIPrefix: mockBaseURI, + }); + }); + }); +}); diff --git a/packages/aws-rfdk/lib/lambdas/nodejs/lib/ecr-provider/ecr-provider.ts b/packages/aws-rfdk/lib/lambdas/nodejs/lib/ecr-provider/ecr-provider.ts new file mode 100644 index 000000000..256c11d1c --- /dev/null +++ b/packages/aws-rfdk/lib/lambdas/nodejs/lib/ecr-provider/ecr-provider.ts @@ -0,0 +1,185 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as fs from 'fs'; +import { IncomingMessage } from 'http'; +import * as https from 'https'; + +/** + * The schema of the AWS Thinkbox Docker image index + */ +interface ThinkboxDockerImageIndex { + /** + * Information about the AWS Thinkbox Docker registry + */ + readonly registry: { + /** + * The base URI of the AWS Thinkbox Docker registry + */ + readonly uri: string; + }; + + /** + * Catalog of products for which AWS Thinkbox publishes Docker images + */ + readonly products: { + /** + * Deadline images + */ + readonly deadline: { + /** + * The URI namespace appended to the AWS Thinkbox Docker registry URI where + * Docker images can be pulled. + * + * Image Docker URIs can be computed as: + * + * + */ + readonly namespace: string; + }; + }; +} + +/** + * The version provider parses a JSON file containing references to ECRs that serve Thinkbox's Deadline Docker images. + * It can be downloaded or loaded from local file and returns the ECR ARN prefix. + */ +export class ThinkboxEcrProvider { + /** + * The URL to obtain the ECR index from. + */ + private static readonly ECR_INDEX_URL = 'https://downloads.thinkboxsoftware.com/thinkbox_ecr.json'; + + private indexJsonPromise?: Promise; + + constructor(private readonly indexPath?: string) {} + + private get indexJson() { + if (!this.indexJsonPromise) { + this.indexJsonPromise = new Promise((resolve, reject) => { + try { + if (this.indexPath) { + return resolve(this.readEcrIndex(this.indexPath)); + } + else { + // return resolve(this.getMockEcrIndex()); + resolve(this.getEcrIndex()); + } + } + catch (e) { + return reject(e); + } + }).then((json: string) => { + // convert the response to a json object and return. + let data: any; + try { + data = JSON.parse(json); + } + catch (e) { + throw new Error(`ECR index file contains invalid JSON: "${e}"`); + } + + if (this.verifyThinkboxDockerImageIndex(data)) { + return data; + } + else { + throw new Error('This should be a dead code path'); + } + }); + } + return this.indexJsonPromise; + } + + private verifyThinkboxDockerImageIndex(index: any): index is ThinkboxDockerImageIndex { + function expectObject(key: string, value: any) { + const valueType = typeof value; + if (valueType != 'object') { + throw new Error(`expected ${key} to be an object but got ${valueType}`); + } + + if (Array.isArray(index)) { + throw new Error(`expected ${key} to be an object but got array`); + } + } + + function expectString(key: string, value: any) { + const valueType = typeof value; + if (valueType != 'string') { + throw new Error(`expected ${key} to be a string but got ${valueType}`); + } + } + + expectObject('index', index); + expectObject('index.registry', index.registry); + expectString('index.registry.uri', index.registry.uri); + expectObject('index.products', index.products); + expectObject('index.products.deadline', index.products.deadline); + expectString('index.products.deadline.namespace', index.products.deadline.namespace); + + return true; + } + + /** + * Gets the global ECR base URI for Thinkbox published Deadline Docker images. + */ + public async getGlobalEcrBaseURI(): Promise { + const indexJson = await this.indexJson; + + const globalEcrBaseURI = `${indexJson.registry.uri}/${indexJson.products.deadline.namespace}`; + if (globalEcrBaseURI === undefined) { + throw new Error('No global ECR'); + } + if (typeof(globalEcrBaseURI) != 'string') { + throw new Error(`Unexpected type for global base ECR URI: "${typeof(globalEcrBaseURI)}`); + } + + return globalEcrBaseURI; + } + + /** + * Downloads and parses the ECR index. + * + * Returns a promise that is resolved with a JSON-parsed object containing the index. + */ + private async getEcrIndex(): Promise { + return new Promise((resolve, reject) => { + const request = https.get(ThinkboxEcrProvider.ECR_INDEX_URL, (res: IncomingMessage) => { + let json = ''; + + res.on('data', (chunk: string) => { + // keep appending the response chunks until we get 'end' event. + json += chunk; + }); + + res.on('end', () => { + // complete response is available here: + if (res.statusCode === 200) { + resolve(json); + } else { + reject(new Error(`Expected status code 200, but got ${res.statusCode}`)); + } + }); + }); + + request.on('error', (err: Error) => { + reject(err); + }); + }); + } + + /** + * This method reads the ECR index from a file and returns a parsed JSON object. + * + * @param indexFilePath The path to the ECR index file + */ + private readEcrIndex(indexFilePath: string): string { + if (!fs.existsSync(indexFilePath)) { + throw new Error(`File "${indexFilePath}" was not found`); + } + const json = fs.readFileSync(indexFilePath, 'utf8'); + + return json; + } +} diff --git a/packages/aws-rfdk/lib/lambdas/nodejs/lib/ecr-provider/index.ts b/packages/aws-rfdk/lib/lambdas/nodejs/lib/ecr-provider/index.ts new file mode 100644 index 000000000..652bbf4d1 --- /dev/null +++ b/packages/aws-rfdk/lib/lambdas/nodejs/lib/ecr-provider/index.ts @@ -0,0 +1,6 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './ecr-provider';