From dadc86f4f792357511820fcb39525a3dcd11c57d Mon Sep 17 00:00:00 2001 From: Roman Yakobenchuk Date: Sat, 30 Jan 2021 01:52:04 +0000 Subject: [PATCH] feat(deadline): add SEPConfigurationSetup construct --- .../All-In-AWS-Infrastructure-SEP/README.md | 8 +- .../python/README.md | 49 +- .../python/package/app.py | 18 + .../python/package/config.py | 26 + .../python/package/lib/sep_stack.py | 150 ++++- .../ts/README.md | 56 +- .../ts/bin/app.ts | 16 + .../ts/bin/config.ts | 26 + .../ts/lib/sep-stack.ts | 153 ++--- .../aws-rfdk/lib/deadline/lib/render-queue.ts | 6 - .../lib/deadline/lib/sep-configuration.ts | 61 +- .../lib/deadline/lib/sep-spotfleet.ts | 46 +- .../deadline/test/sep-configuration.test.ts | 2 +- .../lib/deadline-client/deadline-client.ts | 1 - .../test/deadline-client.test.ts | 36 ++ .../nodejs/lib/sep-configuration/index.ts | 1 - .../lib/sep-configuration/sep-requests.ts | 18 +- .../nodejs/sep-configuration/handler.ts | 120 +++- .../sep-configuration/test/handler.test.ts | 588 +++++++++++++++--- 19 files changed, 1036 insertions(+), 345 deletions(-) create mode 100644 examples/deadline/All-In-AWS-Infrastructure-SEP/python/package/config.py create mode 100644 examples/deadline/All-In-AWS-Infrastructure-SEP/ts/bin/config.ts diff --git a/examples/deadline/All-In-AWS-Infrastructure-SEP/README.md b/examples/deadline/All-In-AWS-Infrastructure-SEP/README.md index 95d36f397..729f7773a 100644 --- a/examples/deadline/All-In-AWS-Infrastructure-SEP/README.md +++ b/examples/deadline/All-In-AWS-Infrastructure-SEP/README.md @@ -10,7 +10,7 @@ _**Note:** This application is an illustrative example to showcase some of the c ## Architecture -This sample application deploys a basic Deadline Render farm that is configured to use Deadlines [Spot Event Plugin](https://docs.thinkboxsoftware.com/products/deadline/10.1/1_User%20Manual/manual/event-spot.html). +This sample application deploys a basic Deadline Render farm that is configured to use Deadlines [Spot Event Plugin](https://docs.thinkboxsoftware.com/products/deadline/10.1/1_User%20Manual/manual/event-spot.html). ### Components @@ -22,15 +22,15 @@ The Repository component contains the database and file system that store persis #### Render Queue -The Render Queue component contains the fleet of [Deadline Remote Connection Server](https://docs.thinkboxsoftware.com/products/deadline/10.1/1_User%20Manual/manual/remote-connection-server.html) instances behind an [Application Load Balancer](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/introduction.html). This acts as the central service for Deadline applications and is the only component that interacts with the Repository. When comparing this component to the "All in AWS Infrastructure - Basic" example it has been granted additional permissions in order to use the Spot Event Plugin. +The Render Queue component contains the fleet of [Deadline Remote Connection Server](https://docs.thinkboxsoftware.com/products/deadline/10.1/1_User%20Manual/manual/remote-connection-server.html) instances behind an [Application Load Balancer](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/introduction.html). This acts as the central service for Deadline applications and is the only component that interacts with the Repository. When comparing this component to the "All in AWS Infrastructure - Basic" example it has been granted additional permissions in order to use the Spot Event Plugin. #### Spot Event Plugin Configurations -The Spot Event plugin requires additional Roles for both Deadline's Resource Tracker and the Spot Workers that are created and a Security Group to allow your Spot workers the ability to access the Render Queue. +Spot Event plugin Configuration Setup component generates and saves the [Spot Fleet Request Configurations](https://docs.thinkboxsoftware.com/products/deadline/10.1/1_User%20Manual/manual/event-spot.html#spot-fleet-request-configurations). The Spot Workers that are created will be configured to connect to the Render Queue. The Spot Event plugin requires additional Role for Deadline's Resource Tracker. ## Prerequisites -- The Spot Fleet Configuration requires an [Amazon Machine Image (AMI)](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AMIs.html) with the Deadline Worker application installed. This AMI must have Deadline Installed and should be configured to connect to your repository. For additional information on setting up your AMI please see the [Spot Event Plugin Documentation](https://docs.thinkboxsoftware.com/products/deadline/10.1/1_User%20Manual/manual/event-spot.html). +- The Spot Fleet Configuration requires an [Amazon Machine Image (AMI)](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AMIs.html) with the Deadline Worker application installed. This AMI must have Deadline Installed and should be configured to connect to your repository. For additional information on setting up your AMI please see the [Spot Event Plugin Documentation](https://docs.thinkboxsoftware.com/products/deadline/10.1/1_User%20Manual/manual/event-spot.html). - You have setup and configured the AWS CLI - Your AWS account already has CDK bootstrapped in the desired region by running `cdk bootstrap` - You must have NodeJS installed on your system diff --git a/examples/deadline/All-In-AWS-Infrastructure-SEP/python/README.md b/examples/deadline/All-In-AWS-Infrastructure-SEP/python/README.md index 33b893d03..34b9a49a7 100644 --- a/examples/deadline/All-In-AWS-Infrastructure-SEP/python/README.md +++ b/examples/deadline/All-In-AWS-Infrastructure-SEP/python/README.md @@ -32,7 +32,41 @@ These instructions assume that your working directory is `examples/deadline/All- popd pip install ../../../../dist/python/aws-rfdk-.tar.gz ``` -3. Stage the Docker recipes for `RenderQueue`: +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: + ```bash + aws --region ec2 describe-images \ + --owners 357466774442 \ + --filters "Name=name,Values=*Worker*" "Name=name,Values=**" \ + --query 'Images[*].[ImageId, Name]' \ + --output text + ``` + + And enter it into this section of `package/config.py`: + ```python + # For example, in the us-west-2 region + self.deadline_client_linux_ami_map: Mapping[str, str] = { + 'us-west-2': '' + } + ``` + + --- + + **Note:** The next two steps are optional. You may skip these if you do not need SSH access into your render farm. + + --- +5. Create an EC2 key pair to give you SSH access to the render farm: + + ```bash + aws ec2 create-key-pair --key-name + ``` +6. 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] = '' + ``` +7. Stage the Docker recipes for `RenderQueue`: ```bash # Set this value to the version of RFDK your application targets @@ -43,22 +77,13 @@ These instructions assume that your working directory is `examples/deadline/All- npx --package=aws-rfdk@${RFDK_VERSION} stage-deadline --output stage ${RFDK_DEADLINE_VERSION} ``` -4. Deploy all the stacks in the sample app: +8. Deploy all the stacks in the sample app: ```bash cdk deploy "*" ``` -5. Connect to your Render Farm and open up the Deadline Monitor. - -6. Configure the Spot event plugin by following the directions in the [Spot Event Plugin documentation](https://docs.thinkboxsoftware.com/products/deadline/10.1/1_User%20Manual/manual/event-spot.html) with the following considerations: - - Use the default security credentials by using turning "Use Local Credentials" to False and leaving both "Access Key ID" and "Secret Access Key" blank. - Ensure that the Region your Spot workers will be launched in is the same region as your CDK application. - When Creating your Spot Fleet Requests, set the IAM instance profile to "DeadlineSpotWorkerRole" and set the security group to "DeadlineSpotSecurityGroup". - Configure your instances to connect to the Render Queue by either creating your AMI after launching your app and preconfiguring the AMI or by setting up a userdata in the Spot Fleet Request. (see the Spot Event Plugin documentation for additional information on configuring this connection.) - -7. Once you are finished with the sample app, you can tear it down by running: +9. 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-SEP/python/package/app.py b/examples/deadline/All-In-AWS-Infrastructure-SEP/python/package/app.py index d0f6eb611..103b984af 100644 --- a/examples/deadline/All-In-AWS-Infrastructure-SEP/python/package/app.py +++ b/examples/deadline/All-In-AWS-Infrastructure-SEP/python/package/app.py @@ -10,11 +10,27 @@ Environment ) +from aws_cdk.aws_ec2 import ( + MachineImage +) + from .lib import ( sep_stack, ) +from .config import config + def main(): + # ------------------------------ + # Validate Config Values + # ------------------------------ + + if not config.key_pair_name: + print('EC2 key pair name not specified. You will not have SSH access to the render farm.') + + if 'region' in config.deadline_client_linux_ami_map: + raise ValueError('Deadline Client Linux AMI map is required but was not specified.') + # ------------------------------ # Application # ------------------------------ @@ -33,6 +49,8 @@ def main(): # ------------------------------ sep_props = sep_stack.SEPStackProps( docker_recipes_stage_path=os.path.join(os.path.dirname(os.path.realpath(__file__)), os.pardir, 'stage'), + key_pair_name=config.key_pair_name, + worker_machine_image=MachineImage.generic_linux(config.deadline_client_linux_ami_map) ) service = sep_stack.SEPStack(app, 'SEPStack', props=sep_props, env=env) diff --git a/examples/deadline/All-In-AWS-Infrastructure-SEP/python/package/config.py b/examples/deadline/All-In-AWS-Infrastructure-SEP/python/package/config.py new file mode 100644 index 000000000..8d6dadabb --- /dev/null +++ b/examples/deadline/All-In-AWS-Infrastructure-SEP/python/package/config.py @@ -0,0 +1,26 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +from typing import ( + List, + Mapping, + Optional +) + +class AppConfig: + """ + Configuration values for the sample app. + + TODO: Fill these in with your own values. + """ + def __init__(self): + # 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. + self.deadline_client_linux_ami_map: Mapping[str, str] = {'us-east-1': 'ami-0f5650d87270255ae'} + + # (Optional) The name of the EC2 keypair to associate with the instances. + self.key_pair_name: Optional[str] = None + + +config: AppConfig = AppConfig() diff --git a/examples/deadline/All-In-AWS-Infrastructure-SEP/python/package/lib/sep_stack.py b/examples/deadline/All-In-AWS-Infrastructure-SEP/python/package/lib/sep_stack.py index 3c9ed816b..94121f060 100644 --- a/examples/deadline/All-In-AWS-Infrastructure-SEP/python/package/lib/sep_stack.py +++ b/examples/deadline/All-In-AWS-Infrastructure-SEP/python/package/lib/sep_stack.py @@ -1,7 +1,9 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -import typing +from typing import ( + Optional +) from dataclasses import dataclass from aws_cdk.core import ( @@ -11,19 +13,39 @@ StackProps ) from aws_cdk.aws_ec2 import ( + IMachineImage, + InstanceClass, + InstanceSize, + InstanceType, SecurityGroup, - Vpc, + Vpc ) from aws_cdk.aws_iam import ( ManagedPolicy, Role, - ServicePrincipal + ServicePrincipal +) +from aws_cdk.aws_elasticloadbalancingv2 import ( + ApplicationProtocol +) +from aws_cdk.aws_route53 import ( + PrivateHostedZone ) from aws_rfdk.deadline import ( + SEPConfigurationProperties, RenderQueue, + RenderQueueExternalTLSProps, + RenderQueueHostNameProps, + RenderQueueTrafficEncryptionProps, Repository, + SEPConfigurationSetup, + SEPSpotFleet, Stage, - ThinkboxDockerRecipes, + ThinkboxDockerRecipes +) +from aws_rfdk import ( + DistinguishedName, + X509CertificatePem ) @@ -34,6 +56,10 @@ class SEPStackProps(StackProps): """ # The path to the directory where the staged Deadline Docker recipes are. docker_recipes_stage_path: str + # The IMachineImage to use for Workers (needs Deadline Client installed). + worker_machine_image: IMachineImage + # The name of the EC2 keypair to associate with Worker nodes. + key_pair_name: Optional[str] class SEPStack(Stack): @@ -73,46 +99,82 @@ def __init__(self, scope: Construct, stack_id: str, *, props: SEPStackProps, **k repository_installation_timeout=Duration.minutes(20) ) + # The following code is used to demonstrate how to use the SEPConfigurationSetup if TLS is enabled. + host = 'renderqueue' + suffix = '.local' + # We are calculating the max length we can add to the common name to keep it under the maximum allowed 64 + # characters and then taking a slice of the stack name so we don't get an error when creating the certificate + # with openssl + max_length = 64 - len(host) - len('.') - len(suffix) - 1 + zone_name = Stack.of(self).stack_name[:max_length] + suffix + + # Internal DNS zone for the VPC. + dns_zone = PrivateHostedZone( + self, + 'DnsZone', + vpc=vpc, + zone_name=zone_name + ) + + # NOTE: this certificate is used by SEPConfigurationSetup construct below. + ca_cert = X509CertificatePem( + self, + 'CaCert' + '_SEPConfigurationTest', + subject=DistinguishedName( + cn='ca.renderfarm' + suffix + ) + ) + + server_cert = X509CertificatePem( + self, + 'RenderQueueCertPEM' + '_SEPConfigurationTest', + subject=DistinguishedName( + cn=host + '.' + zone_name + ), + signing_certificate=ca_cert + ) + render_queue = RenderQueue( self, 'RenderQueue', - vpc=props.vpc, + vpc=vpc, version=recipes.version, images=recipes.render_queue_images, repository=repository, # 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. - deletion_protection=False + deletion_protection=False, + hostname=RenderQueueHostNameProps( + hostname=host, + zone=dns_zone + ), + traffic_encryption=RenderQueueTrafficEncryptionProps( + external_tls=RenderQueueExternalTLSProps( + rfdk_certificate=server_cert + ), + internal_protocol=ApplicationProtocol.HTTPS + ) + ) + + # Create the IAM Role for the spot fleet. + # Note if you already have a worker IAM role in your account you can use it instead. + fleet_role = Role( + self, + 'FleetRole', + assumed_by=ServicePrincipal('spotfleet.amazonaws.com'), + managed_policies=[ManagedPolicy.from_managed_policy_arn( + self, + 'AmazonEC2SpotFleetTaggingRole', + 'arn:aws:iam::aws:policy/service-role/AmazonEC2SpotFleetTaggingRole' + )] ) # Adds the following IAM managed Policies to the Render Queue so it has the necessary permissions # to run the Spot Event Plugin and launch a Resource Tracker: # * AWSThinkboxDeadlineSpotEventPluginAdminPolicy # * AWSThinkboxDeadlineResourceTrackerAdminPolicy - render_queue.add_sep_policies() - - # Create the security group that you will assign to your workers - worker_security_group = SecurityGroup( - self, - 'SpotSecurityGroup', - vpc=props.vpc, - allow_all_outbound=True, - security_group_name='DeadlineSpotSecurityGroup', - ) - worker_security_group.connections.allow_to_default_port( - render_queue.endpoint - ) - - # Create the IAM Role for the Spot Event Plugins workers. - # Note: This Role MUST have a roleName that begins with "DeadlineSpot" - # Note: If you already have a worker IAM role in your account you can remove this code. - worker_iam_role = Role( - self, - 'SpotWorkerRole', - assumed_by=ServicePrincipal('ec2.amazonaws.com'), - managed_policies= [ManagedPolicy.from_aws_managed_policy_name('AWSThinkboxDeadlineSpotEventPluginWorkerPolicy')], - role_name= 'DeadlineSpotWorkerRole', - ) + # Also, adds policies that allow the Render Queue to tag spot fleet requests and to pass the spot fleet role. + render_queue.add_sep_policies(True, [fleet_role.role_arn]) # Creates the Resource Tracker Access role. This role is required to exist in your account so the resource tracker will work properly # Note: If you already have a Resource Tracker IAM role in your account you can remove this code. @@ -121,6 +183,32 @@ def __init__(self, scope: Construct, stack_id: str, *, props: SEPStackProps, **k 'ResourceTrackerRole', assumed_by=ServicePrincipal('lambda.amazonaws.com'), managed_policies= [ManagedPolicy.from_aws_managed_policy_name('AWSThinkboxDeadlineResourceTrackerAccessPolicy')], - role_name= 'DeadlineResourceTrackerAccessRole', + role_name= 'DeadlineResourceTrackerAccessRole' + ) + + fleet = SEPSpotFleet( + self, + 'SEPSpotFleet', + vpc=vpc, + render_queue=render_queue, + fleet_role=fleet_role, + deadline_groups=['group_name'], + instance_types=[InstanceType.of(InstanceClass.BURSTABLE3, InstanceSize.LARGE)], + worker_machine_image=props.worker_machine_image, + target_capacity=1, + key_name=props.key_pair_name + ) + + SEPConfigurationSetup( + self, + 'SEPConfigurationSetup', + vpc=vpc, + render_queue=render_queue, + ca_cert=ca_cert.cert, + spot_fleet_options=SEPConfigurationProperties( + spot_fleets=[fleet], + enable_resource_tracker=True, + region=self.region + ) ) diff --git a/examples/deadline/All-In-AWS-Infrastructure-SEP/ts/README.md b/examples/deadline/All-In-AWS-Infrastructure-SEP/ts/README.md index 8c3427be9..f632f6def 100644 --- a/examples/deadline/All-In-AWS-Infrastructure-SEP/ts/README.md +++ b/examples/deadline/All-In-AWS-Infrastructure-SEP/ts/README.md @@ -17,12 +17,50 @@ These instructions assume that your working directory is `examples/deadline/All- ``` yarn install ``` -3. 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`: +3. Modify the `deadline_ver` field in the `config` block of `package.json` as desired (Deadline 10.1.12 and up are supported), then stage the Docker recipes for `RenderQueue`: ``` yarn stage ``` -4. 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: + +4. 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 \ + --filters "Name=name,Values=*Worker*" "Name=name,Values=**" \ + --query 'Images[*].[ImageId, Name]' \ + --output text + ``` + + And enter it into this section of `bin/config.ts`: + ```ts + // For example, in the us-west-2 region + public readonly deadlineClientLinuxAmiMap: Record = { + ['us-west-2']: '', + // ... + }; + ``` + + --- + + **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. + + --- +5. Create an EC2 key pair to give you SSH access to the render farm: + + ``` + aws ec2 create-key-pair --key-name + ``` +6. 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 = ''; + ``` + +7. 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 ../../../.. @@ -35,22 +73,14 @@ These instructions assume that your working directory is `examples/deadline/All- # Run the example's build yarn build ``` -5. Deploy all the stacks in the sample app: + +8. Deploy all the stacks in the sample app: ``` cdk deploy ``` -6. Connect to your Render Farm and open up the Deadline Monitor. - -7. Configure the Spot event plugin by following the directions in the [Spot Event Plugin documentation](https://docs.thinkboxsoftware.com/products/deadline/10.1/1_User%20Manual/manual/event-spot.html) with the following considerations: - - Use the default security credentials by using turning "Use Local Credentials" to False and leaving both "Access Key ID" and "Secret Access Key" blank. - Ensure that the Region your Spot workers will be launched in is the same region as your CDK application. - When Creating your Spot Fleet Requests, set the IAM instance profile to "DeadlineSpotWorkerRole" and set the security group to "DeadlineSpotSecurityGroup". - Configure your instances to connect to the Render Queue by either creating your AMI after launching your app and preconfiguring the AMI or by setting up a userdata in the Spot Fleet Request. (see the Spot Event Plugin documentation for additional information on configuring this connection.) - -8. Once you are finished with the sample app, you can tear it down by running: +9. 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-SEP/ts/bin/app.ts b/examples/deadline/All-In-AWS-Infrastructure-SEP/ts/bin/app.ts index 682d5da6b..5e25d8c6a 100644 --- a/examples/deadline/All-In-AWS-Infrastructure-SEP/ts/bin/app.ts +++ b/examples/deadline/All-In-AWS-Infrastructure-SEP/ts/bin/app.ts @@ -7,8 +7,22 @@ import 'source-map-support/register'; import * as path from 'path'; import * as pkg from '../package.json'; +import { MachineImage } from '@aws-cdk/aws-ec2'; import * as cdk from '@aws-cdk/core'; import { SEPStack } from '../lib/sep-stack'; +import { config } from './config'; + +// ------------------------------ // +// --- Validate Config Values --- // +// ------------------------------ // + +if (!config.keyPairName) { + console.log('EC2 key pair name not specified. You will not have SSH access to the render farm.'); +} + +if (config.deadlineClientLinuxAmiMap === {['region']: 'ami-id'}) { + throw new Error('Deadline Client Linux AMI map is required but was not specified.'); +} // ------------------- // // --- Application --- // @@ -25,4 +39,6 @@ const app = new cdk.App(); new SEPStack(app, 'SEPStack', { env, dockerRecipesStagePath: path.join(__dirname, '..', pkg.config.stage_path), // Stage directory in config is relative, make it absolute + workerMachineImage: MachineImage.genericLinux(config.deadlineClientLinuxAmiMap), + keyPairName: config.keyPairName ?? undefined, }); diff --git a/examples/deadline/All-In-AWS-Infrastructure-SEP/ts/bin/config.ts b/examples/deadline/All-In-AWS-Infrastructure-SEP/ts/bin/config.ts new file mode 100644 index 000000000..59d68034e --- /dev/null +++ b/examples/deadline/All-In-AWS-Infrastructure-SEP/ts/bin/config.ts @@ -0,0 +1,26 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import 'source-map-support/register'; + +/** + * Configuration values for the sample app. + * + * TODO: Fill these in with your own values. + */ +class AppConfig { + /** + * 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. + */ + public readonly deadlineClientLinuxAmiMap: Record = {['us-west-2']: 'ami-039f0c1faba28b015'}; + + /** + * (Optional) The name of the EC2 keypair to associate with instances. + */ + public readonly keyPairName?: string; +} + +export const config = new AppConfig(); \ No newline at end of file diff --git a/examples/deadline/All-In-AWS-Infrastructure-SEP/ts/lib/sep-stack.ts b/examples/deadline/All-In-AWS-Infrastructure-SEP/ts/lib/sep-stack.ts index 619cdba08..fb5885dad 100644 --- a/examples/deadline/All-In-AWS-Infrastructure-SEP/ts/lib/sep-stack.ts +++ b/examples/deadline/All-In-AWS-Infrastructure-SEP/ts/lib/sep-stack.ts @@ -4,14 +4,10 @@ */ import { - CfnClientVpnAuthorizationRule, - CfnClientVpnEndpoint, - CfnClientVpnTargetNetworkAssociation, + IMachineImage, InstanceClass, InstanceSize, InstanceType, - GenericLinuxImage, - SecurityGroup, Vpc, } from '@aws-cdk/aws-ec2'; import { @@ -20,13 +16,17 @@ import { Stack, StackProps } from '@aws-cdk/core'; -import { ApplicationProtocol } from '@aws-cdk/aws-elasticloadbalancingv2'; +import { + ApplicationProtocol +} from '@aws-cdk/aws-elasticloadbalancingv2'; import { ManagedPolicy, Role, ServicePrincipal, } from '@aws-cdk/aws-iam'; -import { PrivateHostedZone } from '@aws-cdk/aws-route53'; +import { + PrivateHostedZone +} from '@aws-cdk/aws-route53'; import { RenderQueue, Repository, @@ -34,10 +34,10 @@ import { ThinkboxDockerRecipes, SEPConfigurationSetup, SEPSpotFleet, - SEPSpotFleetAllocationStrategy, - SpotEventPluginState, } from 'aws-rfdk/deadline'; -import { X509CertificatePem } from 'aws-rfdk'; +import { + X509CertificatePem +} from 'aws-rfdk'; /** * Properties for {@link SEPStack}. @@ -48,6 +48,16 @@ export interface SEPStackProps extends StackProps { * The path to the directory where the staged Deadline Docker recipes are. */ readonly dockerRecipesStagePath: string; + + /** + * The {@link IMachineImage} to use for Workers (needs Deadline Client installed). + */ + readonly workerMachineImage: IMachineImage; + + /** + * The name of the EC2 keypair to associate with Worker nodes. + */ + readonly keyPairName?: string; } export class SEPStack extends Stack { @@ -61,9 +71,7 @@ export class SEPStack extends Stack { constructor(scope: Construct, id: string, props: SEPStackProps) { super(scope, id, props); - const vpc = new Vpc(this, 'Vpc', { - cidr: '10.100.0.0/16', - }); + const vpc = new Vpc(this, 'Vpc', { maxAzs: 2 }); const recipes = new ThinkboxDockerRecipes(this, 'Image', { stage: Stage.fromDirectory(props.dockerRecipesStagePath), @@ -75,8 +83,7 @@ export class SEPStack extends Stack { repositoryInstallationTimeout: Duration.minutes(20), }); - // TODO: remove this. Testing TLS - + // The following code is used to demonstrate how to use the SEPConfigurationSetup if TLS is enabled. const host = 'renderqueue'; const suffix = '.local'; // We are calculating the max length we can add to the common name to keep it under the maximum allowed 64 @@ -85,7 +92,8 @@ export class SEPStack extends Stack { const maxLength = 64 - host.length - '.'.length - suffix.length - 1; const zoneName = Stack.of(this).stackName.slice(0, maxLength) + suffix; - const cacert = new X509CertificatePem(this, 'CaCert' + '_SEP_configuration_test', { + // NOTE: this certificate is used by SEPConfigurationSetup construct below. + const cacert = new X509CertificatePem(this, 'CaCert' + '_SEPConfigurationTest', { subject: { cn: 'ca.renderfarm' + suffix, }, @@ -93,7 +101,7 @@ export class SEPStack extends Stack { const trafficEncryption = { externalTLS: { - rfdkCertificate: new X509CertificatePem(this, 'RenderQueueCertPEM' + '_SEP_configuration_test', { + rfdkCertificate: new X509CertificatePem(this, 'RenderQueueCertPEM' + '_SEPConfigurationTest', { subject: { cn: host + '.' + zoneName, }, @@ -119,41 +127,22 @@ export class SEPStack extends Stack { // 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. deletionProtection: false, - // TODO: delete this. Testing TLS hostname, trafficEncryption, }); - // Create the security group that you will assign to your workers - const workerSecurityGroup = new SecurityGroup(this, 'SpotSecurityGroup', { - vpc, - allowAllOutbound: true, - securityGroupName: 'DeadlineSpotSecurityGroup', + // Creates the Resource Tracker Access role. This role is required to exist in your account so the resource tracker will work properly + // Note: If you already have a Resource Tracker IAM role in your account you can remove this code. + new Role( this, 'ResourceTrackerRole', { + assumedBy: new ServicePrincipal('lambda.amazonaws.com'), + managedPolicies: [ + ManagedPolicy.fromAwsManagedPolicyName('AWSThinkboxDeadlineResourceTrackerAccessPolicy'), + ], + roleName: 'DeadlineResourceTrackerAccessRole', }); - workerSecurityGroup.connections.allowToDefaultPort(renderQueue.endpoint); - - // // Create the IAM Role for the Spot Event Plugins workers. - // // Note: This Role MUST have a roleName that begins with "DeadlineSpot" - // // Note if you already have a worker IAM role in your account you can remove this code. - // const role = new Role( this, 'SpotWorkerRole', { - // assumedBy: new ServicePrincipal('ec2.amazonaws.com'), - // managedPolicies: [ - // ManagedPolicy.fromAwsManagedPolicyName('AWSThinkboxDeadlineSpotEventPluginWorkerPolicy'), - // ], - // roleName: 'DeadlineSpotWorkerRole55667', - // }); - - // // Creates the Resource Tracker Access role. This role is required to exist in your account so the resource tracker will work properly - // // Note: If you already have a Resource Tracker IAM role in your account you can remove this code. - // new Role( this, 'ResourceTrackerRole', { - // assumedBy: new ServicePrincipal('lambda.amazonaws.com'), - // managedPolicies: [ - // ManagedPolicy.fromAwsManagedPolicyName('AWSThinkboxDeadlineResourceTrackerAccessPolicy'), - // ], - // roleName: 'DeadlineResourceTrackerAccessRole', - // }); - // TODO: would be better to create this role inside of spotFleet, but it creates a circular dependency + // Create the IAM Role for the spot fleet. + // Note if you already have a worker IAM role in your account you can use it instead. const fleetRole = new Role(this, 'FleetRole', { assumedBy: new ServicePrincipal('spotfleet.amazonaws.com'), managedPolicies: [ @@ -165,36 +154,24 @@ export class SEPStack extends Stack { // to run the Spot Event Plugin and launch a Resource Tracker: // * AWSThinkboxDeadlineSpotEventPluginAdminPolicy // * AWSThinkboxDeadlineResourceTrackerAdminPolicy - // and allows to pass a spot fleet role + // Also, adds policies that allow the Render Queue to tag spot fleet requests and to pass the spot fleet role. renderQueue.addSEPPolicies(true, [fleetRole.roleArn]); - const fleet = new SEPSpotFleet(this, 'TestSpotFleet1', { + const fleet = new SEPSpotFleet(this, 'SEPSpotFleet', { vpc, renderQueue, fleetRole, - securityGroups: [ - workerSecurityGroup, - ], deadlineGroups: [ - 'group_name1', - 'group_name2', - ], - deadlinePools: [ - 'pool1', - 'pool2', + 'group_name', ], instanceTypes: [ InstanceType.of(InstanceClass.T3, InstanceSize.LARGE), ], - workerMachineImage: new GenericLinuxImage({ - [this.region]: 'ami-0f5650d87270255ae', - }), + workerMachineImage: props.workerMachineImage, targetCapacity: 1, - allocationStrategy: SEPSpotFleetAllocationStrategy.CAPACITY_OPTIMIZED, - keyName: 'VPC-B-keypair', + keyName: props.keyPairName, }); - // WHEN new SEPConfigurationSetup(this, 'SEPConfigurationSetup', { vpc, renderQueue: renderQueue, @@ -203,57 +180,9 @@ export class SEPStack extends Stack { spotFleets: [ fleet, ], - groupPools: { - 'group_name1': ['pool1', 'pool2'], - }, - enableResourceTracker: false, - deleteEC2SpotInterruptedWorkers: true, - deleteSEPTerminatedWorkers: true, + enableResourceTracker: true, region: this.region, - state: SpotEventPluginState.GLOBAL_ENABLED, }, }); - - // TODO: remove this. Only for testing - const securityGroup = new SecurityGroup(this, 'SG-VPN-RFDK', { - vpc, - }); - - const endpoint = new CfnClientVpnEndpoint(this, 'ClientVpnEndpointRFDK', { - description: "VPN", - vpcId: vpc.vpcId, - securityGroupIds: [ - securityGroup.securityGroupId, - ], - authenticationOptions: [{ - type: "certificate-authentication", - mutualAuthentication: { - clientRootCertificateChainArn: "arn:aws:acm:us-east-1:693238537026:certificate/5ce1c76e-c2e1-4da1-b47a-8273af60a766", - }, - }], - clientCidrBlock: '10.200.0.0/16', - connectionLogOptions: { - enabled: false, - }, - serverCertificateArn: "arn:aws:acm:us-east-1:693238537026:certificate/acc475c0-eaf1-4d6a-9367-d294927565d6", - }); - - let i = 0; - vpc.privateSubnets.map(subnet => { - new CfnClientVpnTargetNetworkAssociation(this, `ClientVpnNetworkAssociation${i}`, { - clientVpnEndpointId: endpoint.ref, - subnetId: subnet.subnetId, - }); - i++; - }); - - new CfnClientVpnAuthorizationRule(this, 'ClientVpnAuthRule', { - clientVpnEndpointId: endpoint.ref, - targetNetworkCidr: '10.100.0.0/16', - authorizeAllGroups: true, - description: "Allow access to whole VPC CIDR range" - }); - - renderQueue.connections.allowDefaultPortFrom(securityGroup); } } diff --git a/packages/aws-rfdk/lib/deadline/lib/render-queue.ts b/packages/aws-rfdk/lib/deadline/lib/render-queue.ts index 9762d327a..2e83b9754 100644 --- a/packages/aws-rfdk/lib/deadline/lib/render-queue.ts +++ b/packages/aws-rfdk/lib/deadline/lib/render-queue.ts @@ -496,12 +496,6 @@ export class RenderQueue extends RenderQueueBase implements IGrantable { }, }, })); - this.taskDefinition.taskRole.addToPrincipalPolicy(new PolicyStatement({ - actions: [ - 'iam:GetRole', - ], - resources: iamFleetRoleArns, - })); } } diff --git a/packages/aws-rfdk/lib/deadline/lib/sep-configuration.ts b/packages/aws-rfdk/lib/deadline/lib/sep-configuration.ts index 71753d47a..99b000241 100644 --- a/packages/aws-rfdk/lib/deadline/lib/sep-configuration.ts +++ b/packages/aws-rfdk/lib/deadline/lib/sep-configuration.ts @@ -123,7 +123,7 @@ export enum SpotEventPluginAwsInstanceStatus { /** * Spot Event Plugin configuration options */ -export interface ISEPConfigurationProperties { +export interface SEPConfigurationProperties { /** * The array of Spot Event Plugin spot fleets used to generate the mapping between your groups and spot fleet requests. * @@ -235,8 +235,8 @@ interface SEPGeneralOptions { readonly Logging?: string; readonly Region?: string; readonly IdleShutdown?: number; - readonly DeleteInterruptedSlaves?: boolean; // TODO: should we rename slaves here? - readonly DeleteTerminatedSlaves?: boolean; // TODO: should we rename slaves here? + readonly DeleteInterruptedSlaves?: boolean; + readonly DeleteTerminatedSlaves?: boolean; readonly StrictHardCap?: boolean; readonly StaggerInstances?: number; readonly PreJobTaskMode?: string; @@ -274,7 +274,7 @@ export interface SEPConfigurationSetupProps { * The Spot Event Plugin settings. * See https://docs.thinkboxsoftware.com/products/deadline/10.1/1_User%20Manual/manual/event-spot.html?highlight=spot%20even%20plugin#event-plugin-configuration-options */ - readonly spotFleetOptions: ISEPConfigurationProperties; + readonly spotFleetOptions: SEPConfigurationProperties; } /** @@ -331,7 +331,22 @@ export class SEPConfigurationSetup extends Construct { lamdbaFunc.connections.allowToDefaultPort(props.renderQueue); props.caCert?.grantRead(lamdbaFunc.grantPrincipal); - const combinedPluginConfigs = this.combinedSpotPluginConfigs(props.spotFleetOptions); + const combinedPluginConfigs: SEPGeneralOptions = { + AWSInstanceStatus: props.spotFleetOptions.awsInstanceStatus, + DeleteInterruptedSlaves: props.spotFleetOptions.deleteEC2SpotInterruptedWorkers, + DeleteTerminatedSlaves: props.spotFleetOptions.deleteSEPTerminatedWorkers, + GroupPools: props.spotFleetOptions.groupPools ? Stack.of(this).toJsonString(props.spotFleetOptions.groupPools) : undefined, + IdleShutdown: props.spotFleetOptions.idleShutdown, + Logging: props.spotFleetOptions.loggingLevel, + PreJobTaskMode: props.spotFleetOptions.preJobTaskMode, + Region: props.spotFleetOptions.region ?? Stack.of(this).region, + ResourceTracker: props.spotFleetOptions.enableResourceTracker, + StaggerInstances: props.spotFleetOptions.maximumInstancesStartedPerCycle, + State: props.spotFleetOptions.state, + StrictHardCap: props.spotFleetOptions.strictHardCap, + UseLocalCredentials: true, + NamedProfile: '', + }; const combinedSpotFleetConfigs = this.combinedSpotFleetConfigs(props.spotFleetOptions.spotFleets); const properties: ISEPConfiguratorResourceProperties = { @@ -361,13 +376,12 @@ export class SEPConfigurationSetup extends Construct { this.node.defaultChild = resource; } - private combinedSpotFleetConfigs(spotFleets?: SEPSpotFleet[]): string | undefined { + private combinedSpotFleetConfigs(spotFleets?: SEPSpotFleet[]): object | undefined { if (!spotFleets || spotFleets.length === 0) { return undefined; } let fullSpotFleetRequestConfiguration: any = {}; - spotFleets.map(fleet => { fleet.sepSpotFleetRequestConfigurations.map(configuration => { for (const [key, value] of Object.entries(configuration)) { @@ -379,37 +393,6 @@ export class SEPConfigurationSetup extends Construct { }); }); - return JSON.stringify(fullSpotFleetRequestConfiguration); // TODO: Stack.of(this).toJsonString(fullSpotFleetRequestConfiguration); - } - - private combinedSpotPluginConfigs(spotFleetOptions: ISEPConfigurationProperties) { - const pluginOptions: SEPGeneralOptions = { - AWSInstanceStatus: spotFleetOptions.awsInstanceStatus, - DeleteInterruptedSlaves: spotFleetOptions.deleteEC2SpotInterruptedWorkers, - DeleteTerminatedSlaves: spotFleetOptions.deleteSEPTerminatedWorkers, - GroupPools: spotFleetOptions.groupPools ? Stack.of(this).toJsonString(spotFleetOptions.groupPools) : undefined, - IdleShutdown: spotFleetOptions.idleShutdown, - Logging: spotFleetOptions.loggingLevel, - PreJobTaskMode: spotFleetOptions.preJobTaskMode, - Region: spotFleetOptions.region ?? Stack.of(this).region, - ResourceTracker: spotFleetOptions.enableResourceTracker, - StaggerInstances: spotFleetOptions.maximumInstancesStartedPerCycle, - State: spotFleetOptions.state, - StrictHardCap: spotFleetOptions.strictHardCap, - UseLocalCredentials: true, - NamedProfile: '', - }; - - let configs = []; - - for (const [key, value] of Object.entries(pluginOptions)) { - if (value !== undefined) { - configs.push({ - Key: key, - Value: value, - }); - } - } - return configs; + return fullSpotFleetRequestConfiguration; } } diff --git a/packages/aws-rfdk/lib/deadline/lib/sep-spotfleet.ts b/packages/aws-rfdk/lib/deadline/lib/sep-spotfleet.ts index 1c78c5694..b4a0d7e92 100644 --- a/packages/aws-rfdk/lib/deadline/lib/sep-spotfleet.ts +++ b/packages/aws-rfdk/lib/deadline/lib/sep-spotfleet.ts @@ -39,7 +39,7 @@ import { Fn, Expiration, IResource, - // Lazy, + Lazy, Names, ResourceEnvironment, Stack, @@ -559,11 +559,11 @@ export class SEPSpotFleet extends SEPSpotFleetBase { roles: [this.role.roleName], }); - const securityGroups = this.securityGroups.map(sg => { - return { GroupId: sg.securityGroupId }; - }); + const securityGroupsToken = Lazy.any({ produce: () => { + return this.securityGroups.map(sg => { return { GroupId: sg.securityGroupId }; }); + } }); - const userData = Fn.base64(this.userData.render()); + const userDataToken = Lazy.string({ produce: () => Fn.base64(this.userData.render()) }); const blockDeviceMappings = (props.blockDevices !== undefined ? synthesizeBlockDeviceMappings(this, props.blockDevices) : undefined); @@ -574,19 +574,23 @@ export class SEPSpotFleet extends SEPSpotFleetBase { }); const subnetId = subnetIds.length != 0 ? subnetIds.join(',') : undefined; - const instanceTags = this.instanceTags.map(tag => { - return { - Key: tag.key, - Value: tag.value, - }; - }); - - const spotFleetRequestTags = this.spotFleetRequestTags.map(tag => { - return { - Key: tag.key, - Value: tag.value, - }; - }); + const instanceTagsToken = Lazy.any({ produce: () => { + return this.instanceTags.map(tag => { + return { + Key: tag.key, + Value: tag.value, + }; + }); + } }); + + const spotFleetRequestTags = Lazy.any({ produce: () => { + return this.spotFleetRequestTags.map(tag => { + return { + Key: tag.key, + Value: tag.value, + }; + }); + } }); let launchSpecifications: any[] = []; @@ -598,15 +602,15 @@ export class SEPSpotFleet extends SEPSpotFleetBase { }, ImageId: this.imageId, KeyName: props.keyName, - SecurityGroups: securityGroups, + SecurityGroups: securityGroupsToken, SubnetId: subnetId, TagSpecifications: [ { ResourceType: ISpotFleetResourceType.INSTANCE, - Tags: instanceTags, + Tags: instanceTagsToken, }, ], - UserData: userData, + UserData: userDataToken, InstanceType: instanceType.toString(), }; launchSpecifications.push(launchSpecification); diff --git a/packages/aws-rfdk/lib/deadline/test/sep-configuration.test.ts b/packages/aws-rfdk/lib/deadline/test/sep-configuration.test.ts index e22c8ec2d..db37ca92a 100644 --- a/packages/aws-rfdk/lib/deadline/test/sep-configuration.test.ts +++ b/packages/aws-rfdk/lib/deadline/test/sep-configuration.test.ts @@ -104,7 +104,7 @@ describe('SEPConfigurationSetup', () => { renderQueue: renderQueue, spotFleetOptions: { spotFleets: [ - fleet, // TODO: Typescript is complaining + fleet, ], groupPools, }, diff --git a/packages/aws-rfdk/lib/lambdas/nodejs/lib/deadline-client/deadline-client.ts b/packages/aws-rfdk/lib/lambdas/nodejs/lib/deadline-client/deadline-client.ts index a9af5a10a..1476e669b 100644 --- a/packages/aws-rfdk/lib/lambdas/nodejs/lib/deadline-client/deadline-client.ts +++ b/packages/aws-rfdk/lib/lambdas/nodejs/lib/deadline-client/deadline-client.ts @@ -91,7 +91,6 @@ export class DeadlineClient { if (props.protocol === 'HTTPS') { this.protocol = https; - // TODO: maybe add here a check that at least something needs to be provided const httpsAgent = new https.Agent({ pfx: props.tls?.pfx, passphrase: props.tls?.passphrase, diff --git a/packages/aws-rfdk/lib/lambdas/nodejs/lib/deadline-client/test/deadline-client.test.ts b/packages/aws-rfdk/lib/lambdas/nodejs/lib/deadline-client/test/deadline-client.test.ts index d9b2a8cc1..2ccf43458 100644 --- a/packages/aws-rfdk/lib/lambdas/nodejs/lib/deadline-client/test/deadline-client.test.ts +++ b/packages/aws-rfdk/lib/lambdas/nodejs/lib/deadline-client/test/deadline-client.test.ts @@ -71,6 +71,42 @@ describe('ThinkboxEcrProvider', () => { ); }); + test('successful http get request with options', async () => { + // GIVEN + jest.requireMock('http').request.mockImplementation(httpRequestMock); + response = new MockResponse(); + + // WHEN + deadlineClient = new DeadlineClient({ + host: 'hostname', + port: 8080, + protocol: 'HTTP', + }); + + const promise = deadlineClient.GetRequest('/get/version/test', { + headers: { + 'Content-Type': 'application/json', + }, + }); + response.emit('data', Buffer.from(JSON.stringify(''), 'utf8')); + response.emit('end'); + promise.then(resp => resp).catch(err => err); + + // THEN + // should make an HTTP request + expect(jest.requireMock('http').request) + .toBeCalledWith( + { + agent: undefined, + method: 'GET', + port: 8080, + host: 'hostname', + path: '/get/version/test', + }, + expect.any(Function), + ); + }); + test('failed http get request', async () => { // GIVEN response = new MockResponse(); diff --git a/packages/aws-rfdk/lib/lambdas/nodejs/lib/sep-configuration/index.ts b/packages/aws-rfdk/lib/lambdas/nodejs/lib/sep-configuration/index.ts index 223c2f0e8..fe86a22db 100644 --- a/packages/aws-rfdk/lib/lambdas/nodejs/lib/sep-configuration/index.ts +++ b/packages/aws-rfdk/lib/lambdas/nodejs/lib/sep-configuration/index.ts @@ -3,5 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -// TODO: do we even need this? export * from './sep-requests'; \ No newline at end of file diff --git a/packages/aws-rfdk/lib/lambdas/nodejs/lib/sep-configuration/sep-requests.ts b/packages/aws-rfdk/lib/lambdas/nodejs/lib/sep-configuration/sep-requests.ts index 406714056..b29cfbed3 100644 --- a/packages/aws-rfdk/lib/lambdas/nodejs/lib/sep-configuration/sep-requests.ts +++ b/packages/aws-rfdk/lib/lambdas/nodejs/lib/sep-configuration/sep-requests.ts @@ -18,7 +18,6 @@ export class EventPluginRequests { } public async describeServerData(): Promise { - console.log('Sending request to describe server data.'); return await this.deadlineClient.PostRequest('/rcs/v1/describeServerData', { ServerDataIds: [ 'event.plugin.spot', @@ -27,9 +26,7 @@ export class EventPluginRequests { } private async concurrencyToken(): Promise { - console.log('Getting concurrency token.'); const response = await this.describeServerData(); - console.log('Received concurrency token.'); const describedData: { ServerData: { @@ -39,18 +36,14 @@ export class EventPluginRequests { } = response.data; const found = describedData.ServerData.find(element => element.ID === 'event.plugin.spot'); - console.log('Concurrency token is:'); // TODO: remove this console - console.log(found?.ConcurrencyToken); return found?.ConcurrencyToken ?? ''; } public async saveServerData(config: string): Promise { - console.log('Getting concurrency token to save server data.'); - const concurrencyToken = await this.concurrencyToken(); - console.log(`Received concurrency token: ${concurrencyToken}`); // TODO: remove this console - - console.log('Sending put server data request with config:'); + console.log('Saving server data configuration:'); console.log(config); + + const concurrencyToken = await this.concurrencyToken(); await this.deadlineClient.PostRequest('/rcs/v1/putServerData', { ServerData: [ { @@ -62,12 +55,12 @@ export class EventPluginRequests { }, ], }); - console.log('Server data was put successfully put.'); + console.log('Server data successfully saved.'); return true; } public async configureSpotEventPlugin(configs: { Key: string, Value: any }[]): Promise { - console.log('Sending save config request with configs:'); + console.log('Saving plugin configuration:'); console.log(configs); await this.deadlineClient.PostRequest('/db/plugins/event/config/save', { ID: 'spot', @@ -79,6 +72,7 @@ export class EventPluginRequests { Name: 'Spot', PluginEnabled: 1, }); + console.log('Plugin configuration successfully saved.'); return true; } } \ No newline at end of file diff --git a/packages/aws-rfdk/lib/lambdas/nodejs/sep-configuration/handler.ts b/packages/aws-rfdk/lib/lambdas/nodejs/sep-configuration/handler.ts index e5b4a2059..60da2f921 100644 --- a/packages/aws-rfdk/lib/lambdas/nodejs/sep-configuration/handler.ts +++ b/packages/aws-rfdk/lib/lambdas/nodejs/sep-configuration/handler.ts @@ -61,13 +61,13 @@ export interface ISEPConfiguratorResourceProperties { * A JSON string containing the Spot Fleet Request Configurations. * See https://docs.thinkboxsoftware.com/products/deadline/10.1/1_User%20Manual/manual/event-spot.html?highlight=spot%20even%20plugin#example-spot-fleet-request-configurations */ - readonly spotFleetRequestConfigurations?: string; + readonly spotFleetRequestConfigurations?: object; /** * The Spot Event Plugin settings. * See https://docs.thinkboxsoftware.com/products/deadline/10.1/1_User%20Manual/manual/event-spot.html?highlight=spot%20even%20plugin#event-plugin-configuration-options */ - readonly spotPluginConfigurations?: { Key: string, Value: any }[]; + readonly spotPluginConfigurations?: object; } /** @@ -96,13 +96,15 @@ export class SEPConfiguratorResource extends SimpleCustomResource { const eventPluginRequests = await this.spotEventPluginRequests(resourceProperties.connection); if (resourceProperties.spotFleetRequestConfigurations) { - const response = await eventPluginRequests.saveServerData(resourceProperties.spotFleetRequestConfigurations); + const stringConfigs = this.spotFleetRequestToString(resourceProperties.spotFleetRequestConfigurations); + const response = await eventPluginRequests.saveServerData(stringConfigs); if (!response) { - console.log(`Failed to save spot fleet request with configuration: ${resourceProperties.spotFleetRequestConfigurations}`); + console.log(`Failed to save spot fleet request with configuration: ${stringConfigs}`); } } if (resourceProperties.spotPluginConfigurations) { - const response = await eventPluginRequests.configureSpotEventPlugin(resourceProperties.spotPluginConfigurations); + const spotEventFleetPluginConfigs = this.spotFleetPluginConfigsToArray(resourceProperties.spotPluginConfigurations); + const response = await eventPluginRequests.configureSpotEventPlugin(spotEventFleetPluginConfigs); if (!response) { console.log(`Failed to save Spot Event Plugin Configurations: ${resourceProperties.spotPluginConfigurations}`); } @@ -150,27 +152,16 @@ export class SEPConfiguratorResource extends SimpleCustomResource { private isValidSFRConfig(value: any): boolean { if (!value) { return true; } - - if (typeof(value) !== 'string') { return false; } - try { - JSON.parse(value); - } catch (e) { - return false; - } + if (typeof(value) !== 'object') { return false; } + if (Array.isArray(value)) { return false; } return true; } private isValidSpotPluginConfig(value: any): boolean { if (!value) { return true; } + if (typeof(value) !== 'object') { return false; } + if (Array.isArray(value)) { return false; } - if (!Array.isArray(value)) { return false; } - - for (const config of value) { - if (Array.isArray(config)) { return false; } - if (typeof(config) !== 'object') { return false; } - if (!config.Key || typeof(config.Key) !== 'string') { return false; } - if (config.Value === undefined) { return false; } - } return true; } @@ -185,6 +176,95 @@ export class SEPConfiguratorResource extends SimpleCustomResource { })); } + private convertToBoolean(input: string): boolean | undefined { + try { + return JSON.parse(input); + } catch(e) { + return undefined; + } + } + + /** + * Passing spot fleet request configs into lambda converts all numbers and booleans into strings. + * This functions converts these values back to their original type. + */ + private spotFleetRequestToString(spotFleetRequestConfigs: object): string { + let convertedSpotFleetRequestConfigs = spotFleetRequestConfigs; + for (const [_, sfrConfigs] of Object.entries(convertedSpotFleetRequestConfigs)) { + if ('TargetCapacity' in sfrConfigs) { + sfrConfigs.TargetCapacity = Number.parseInt(sfrConfigs.TargetCapacity, 10); + } + if ('ReplaceUnhealthyInstances' in sfrConfigs) { + sfrConfigs.ReplaceUnhealthyInstances = this.convertToBoolean(sfrConfigs.ReplaceUnhealthyInstances); + } + if ('TerminateInstancesWithExpiration' in sfrConfigs) { + sfrConfigs.TerminateInstancesWithExpiration = this.convertToBoolean(sfrConfigs.TerminateInstancesWithExpiration); + } + if ('LaunchSpecifications' in sfrConfigs) { + sfrConfigs.LaunchSpecifications.map((launchSpecification: any) => { + if ('BlockDeviceMappings' in launchSpecification) { + launchSpecification.BlockDeviceMappings.map((blockDeviceMapping: any) => { + if ('noDevice' in blockDeviceMapping) { + blockDeviceMapping.noDevice = this.convertToBoolean(blockDeviceMapping.noDevice); + } + if ('ebs' in blockDeviceMapping) { + if ('deleteOnTermination' in blockDeviceMapping.ebs) { + blockDeviceMapping.ebs.deleteOnTermination = this.convertToBoolean(blockDeviceMapping.ebs.deleteOnTermination); + } + if ('encrypted' in blockDeviceMapping.ebs) { + blockDeviceMapping.ebs.encrypted = this.convertToBoolean(blockDeviceMapping.ebs.encrypted); + } + if ('iops' in blockDeviceMapping.ebs) { + blockDeviceMapping.ebs.iops = Number.parseInt(blockDeviceMapping.ebs.iops, 10); + } + if ('volumeSize' in blockDeviceMapping.ebs) { + blockDeviceMapping.ebs.volumeSize = Number.parseInt(blockDeviceMapping.ebs.volumeSize, 10); + } + } + }); + } + }); + } + } + return JSON.stringify(convertedSpotFleetRequestConfigs); + } + + private spotFleetPluginConfigsToArray(pluginOptions: any): any { + let convertedConfigs = pluginOptions; + if ('DeleteInterruptedSlaves' in convertedConfigs) { + convertedConfigs.DeleteInterruptedSlaves = this.convertToBoolean(convertedConfigs.DeleteInterruptedSlaves); + } + if ('DeleteTerminatedSlaves' in convertedConfigs) { + convertedConfigs.DeleteTerminatedSlaves = this.convertToBoolean(convertedConfigs.DeleteTerminatedSlaves); + } + if ('IdleShutdown' in convertedConfigs) { + convertedConfigs.IdleShutdown = Number.parseInt(convertedConfigs.IdleShutdown, 10); + } + if ('ResourceTracker' in convertedConfigs) { + convertedConfigs.ResourceTracker = this.convertToBoolean(convertedConfigs.ResourceTracker); + } + if ('StaggerInstances' in convertedConfigs) { + convertedConfigs.StaggerInstances = this.convertToBoolean(convertedConfigs.StaggerInstances); + } + if ('StrictHardCap' in convertedConfigs) { + convertedConfigs.StrictHardCap = this.convertToBoolean(convertedConfigs.StrictHardCap); + } + if ('UseLocalCredentials' in convertedConfigs) { + convertedConfigs.UseLocalCredentials = this.convertToBoolean(convertedConfigs.UseLocalCredentials); + } + + let configs = []; + for (const [key, value] of Object.entries(pluginOptions)) { + if (value !== undefined) { + configs.push({ + Key: key, + Value: value, + }); + } + } + return configs; + } + /** * Retrieve CA certificate data from the Secret with the given ARN. * @param certificateArn diff --git a/packages/aws-rfdk/lib/lambdas/nodejs/sep-configuration/test/handler.test.ts b/packages/aws-rfdk/lib/lambdas/nodejs/sep-configuration/test/handler.test.ts index acdcabf68..07b49950f 100644 --- a/packages/aws-rfdk/lib/lambdas/nodejs/sep-configuration/test/handler.test.ts +++ b/packages/aws-rfdk/lib/lambdas/nodejs/sep-configuration/test/handler.test.ts @@ -4,7 +4,6 @@ */ /* eslint-disable dot-notation */ -/* eslint-disable no-console */ // TODO: remove import * as AWS from 'aws-sdk'; import { mock, restore, setSDKInstance } from 'aws-sdk-mock'; @@ -22,22 +21,9 @@ async function successRequestMock(request: { [key: string]: string}, returnValue describe('SEPConfiguratorResource', () => { // Valid configurations - const validSpotPluginConfig = [ - { - Key: 'ResourceTracker', - Value: true, - }, - { - Key: 'GroupPools', - Value: '{\"group_name1\":[\"pool1\",\"pool2\"]}', - }, - ]; - const validSpotPluginConfig2 = [ - { - Key: 'ResourceTracker', - Value: '', - }, - ]; + const validSpotPluginConfig = { + ResourceTracker: true, + }; const validConnection: IConnectionOptions = { hostname: 'internal-hostname.com', protocol: 'HTTPS', @@ -46,7 +32,11 @@ describe('SEPConfiguratorResource', () => { passphrase: secretArn, pfxCertificate: secretArn, }; - const validSpotFleetConfig = '{\"group_name1\":{\"AllocationStrategy\":\"capacityOptimized\"}}'; + const validSpotFleetConfig = { + group_name1: { + AllocationStrategy: 'capacityOptimized', + }, + }; const allConfigs = { spotPluginConfigurations: validSpotPluginConfig, @@ -66,12 +56,12 @@ describe('SEPConfiguratorResource', () => { }; describe('doCreate', () => { - // let consoleLogMock: jest.SpyInstance; + let consoleLogMock: jest.SpyInstance; let mockedHandler: SEPConfiguratorResource; let mockEventPluginRequests: { saveServerData: jest.Mock; configureSpotEventPlugin: jest.Mock; }; beforeEach(() => { - // consoleLogMock = jest.spyOn(console, 'log').mockReturnValue(undefined); + consoleLogMock = jest.spyOn(console, 'log').mockReturnValue(undefined); mockEventPluginRequests = { saveServerData: jest.fn(), @@ -99,7 +89,6 @@ describe('SEPConfiguratorResource', () => { return true; } const mockSaveServerData = jest.fn( (a) => returnTrue(a) ); - // tslint:disable-next-line: no-string-literal mockEventPluginRequests.saveServerData = mockSaveServerData; // WHEN @@ -108,7 +97,7 @@ describe('SEPConfiguratorResource', () => { // THEN expect(result).toBeUndefined(); expect(mockSaveServerData.mock.calls.length).toBe(1); - expect(mockSaveServerData.mock.calls[0][0]).toEqual(noPluginConfigs.spotFleetRequestConfigurations); + expect(mockSaveServerData.mock.calls[0][0]).toEqual(JSON.stringify(noPluginConfigs.spotFleetRequestConfigurations)); }); test('save spot event plugin configs', async () => { @@ -116,17 +105,16 @@ describe('SEPConfiguratorResource', () => { async function returnTrue(_v1: any): Promise { return true; } - const mockconfigureSpotEventPlugin = jest.fn( (a) => returnTrue(a) ); - // tslint:disable-next-line: no-string-literal - mockEventPluginRequests.configureSpotEventPlugin = mockconfigureSpotEventPlugin; + const mockConfigureSpotEventPlugin = jest.fn( (a) => returnTrue(a) ); + mockEventPluginRequests.configureSpotEventPlugin = mockConfigureSpotEventPlugin; // WHEN const result = await mockedHandler.doCreate('physicalId', noFleetConfigs); // THEN expect(result).toBeUndefined(); - expect(mockconfigureSpotEventPlugin.mock.calls.length).toBe(1); - expect(mockconfigureSpotEventPlugin.mock.calls[0][0]).toEqual(noFleetConfigs.spotPluginConfigurations); + expect(mockConfigureSpotEventPlugin.mock.calls.length).toBe(1); + expect(mockConfigureSpotEventPlugin.mock.calls[0][0]).toEqual([{ Key: 'ResourceTracker', Value: true }]); }); test('save both configs', async () => { @@ -135,12 +123,10 @@ describe('SEPConfiguratorResource', () => { return true; } const mockSaveServerData = jest.fn( (a) => returnTrue(a) ); - // tslint:disable-next-line: no-string-literal mockEventPluginRequests.saveServerData = mockSaveServerData; - const mockconfigureSpotEventPlugin = jest.fn( (a) => returnTrue(a) ); - // tslint:disable-next-line: no-string-literal - mockEventPluginRequests.configureSpotEventPlugin = mockconfigureSpotEventPlugin; + const mockConfigureSpotEventPlugin = jest.fn( (a) => returnTrue(a) ); + mockEventPluginRequests.configureSpotEventPlugin = mockConfigureSpotEventPlugin; // WHEN const result = await mockedHandler.doCreate('physicalId', allConfigs); @@ -148,10 +134,42 @@ describe('SEPConfiguratorResource', () => { // THEN expect(result).toBeUndefined(); expect(mockSaveServerData.mock.calls.length).toBe(1); - expect(mockSaveServerData.mock.calls[0][0]).toEqual(allConfigs.spotFleetRequestConfigurations); + expect(mockSaveServerData.mock.calls[0][0]).toEqual(JSON.stringify(allConfigs.spotFleetRequestConfigurations)); + + expect(mockConfigureSpotEventPlugin.mock.calls.length).toBe(1); + expect(mockConfigureSpotEventPlugin.mock.calls[0][0]).toEqual([{ Key: 'ResourceTracker', Value: true }]); + }); + + test('log when cannot save spot fleet request configs', async () => { + // GIVEN + async function returnFalse(_v1: any): Promise { + return false; + } + const mockSaveServerData = jest.fn( (a) => returnFalse(a) ); + mockEventPluginRequests.saveServerData = mockSaveServerData; + + // WHEN + await mockedHandler.doCreate('physicalId', noPluginConfigs); - expect(mockconfigureSpotEventPlugin.mock.calls.length).toBe(1); - expect(mockconfigureSpotEventPlugin.mock.calls[0][0]).toEqual(allConfigs.spotPluginConfigurations); + // THEN + expect(consoleLogMock.mock.calls.length).toBe(1); + expect(consoleLogMock.mock.calls[0][0]).toMatch(/Failed to save spot fleet request with configuration/); + }); + + test('log when cannot save spot event plugin configs', async () => { + // GIVEN + async function returnFalse(_v1: any): Promise { + return false; + } + const mockConfigureSpotEventPlugin = jest.fn( (a) => returnFalse(a) ); + mockEventPluginRequests.configureSpotEventPlugin = mockConfigureSpotEventPlugin; + + // WHEN + await mockedHandler.doCreate('physicalId', noFleetConfigs); + + // THEN + expect(consoleLogMock.mock.calls.length).toBe(1); + expect(consoleLogMock.mock.calls[0][0]).toMatch(/Failed to save Spot Event Plugin Configurations/); }); }); @@ -231,28 +249,7 @@ describe('SEPConfiguratorResource', () => { }); // Invalid configurations - const invalidSpotPluginConfig = [ - { - key: 'ResourceTracker', - value: true, - }, - ]; - const invalidSpotPluginConfigNoValue = [ - { - Key: 'ResourceTracker', - }, - ]; - const invalidSpotPluginConfigNoKey = [ - { - Value: true, - }, - ]; - const invalidSpotPluginConfigWrongKey = [ - { - Key: true, - Value: true, - }, - ]; + const invalidSpotPluginConfig: any = []; const noProtocolConnection = { hostname: 'internal-hostname.us-east-1.elb.amazonaws.com', port: '4433', @@ -321,20 +318,25 @@ describe('SEPConfiguratorResource', () => { port: '4433', pfxCertificate: 'notArn', }; - const invalidSpotFleetConfig = { - inValid: 'invalid', - }; + const invalidSpotFleetConfig = '{ inValid: 10 }'; describe('should return false if', () => { test.each([ - invalidSpotPluginConfig, - invalidSpotPluginConfigNoKey, - invalidSpotPluginConfigNoValue, - invalidSpotPluginConfigWrongKey, - ])('given invalid spot plugin config', (invalidConfig: any) => { + undefined, + [], + ])('{input=%s}', (input) => { + // WHEN + const handler = new SEPConfiguratorResource(new AWS.SecretsManager()); + const returnValue = handler.validateInput(input); + + // THEN + expect(returnValue).toBeFalsy(); + }); + + test('given invalid spot plugin config', () => { // GIVEN const input = { - spotPluginConfigurations: invalidConfig, + spotPluginConfigurations: invalidSpotPluginConfig, connection: validConnection, spotFleetRequestConfigurations: validSpotFleetConfig, }; @@ -453,7 +455,7 @@ describe('SEPConfiguratorResource', () => { describe('should return true if', () => { test.each([ - '{ "JSON": "string" }', + { json: 'object' }, undefined, ])('{input=%s}', (input: any) => { // WHEN @@ -470,12 +472,9 @@ describe('SEPConfiguratorResource', () => { describe('should return false if', () => { test.each([ 10, - [[]], + [], 'string', invalidSpotPluginConfig, - invalidSpotPluginConfigNoKey, - invalidSpotPluginConfigNoValue, - invalidSpotPluginConfigWrongKey, ])('{input=%s}', (input: any) => { // WHEN const handler = new SEPConfiguratorResource(new AWS.SecretsManager()); @@ -489,8 +488,6 @@ describe('SEPConfiguratorResource', () => { describe('should return true if', () => { test.each([ validSpotPluginConfig, - validSpotPluginConfig2, - [], undefined, ])('{input=%s}', (input: any) => { // WHEN @@ -532,4 +529,451 @@ describe('SEPConfiguratorResource', () => { }); }); }); + + describe('.spotEventPluginRequests()', () => { + test('creates a valid object with http', async () => { + // GIVEN + const validHTTPConnection: IConnectionOptions = { + hostname: 'internal-hostname.com', + protocol: 'HTTP', + port: '8080', + }; + + // WHEN + const handler = new SEPConfiguratorResource(new AWS.SecretsManager()); + const result = await handler['spotEventPluginRequests'](validHTTPConnection); + + expect(result).toBeDefined(); + }); + + test('creates a valid object with https', async () => { + // GIVEN + const validHTTPSConnection: IConnectionOptions = { + hostname: 'internal-hostname.com', + protocol: 'HTTP', + port: '8080', + caCertificate: secretArn, + }; + + // WHEN + const handler = new SEPConfiguratorResource(new AWS.SecretsManager()); + async function returnCerificateContent(_v1: any): Promise { + return 'BEGIN CERTIFICATE'; + } + // tslint:disable-next-line: no-string-literal + handler['readCertificateData'] = jest.fn( (a) => returnCerificateContent(a) ); + const result = await handler['spotEventPluginRequests'](validHTTPSConnection); + + expect(result).toBeDefined(); + }); + }); + + describe('.spotFleetPluginConfigsToArray()', () => { + test('converts DeleteInterruptedSlaves', () => { + // GIVEN + let pluginConfig = { + DeleteInterruptedSlaves: 'true', + }; + const expectedResult = [{ + Key: 'DeleteInterruptedSlaves', + Value: true, + }]; + + // WHEN + const handler = new SEPConfiguratorResource(new AWS.SecretsManager()); + const returnValue = handler['spotFleetPluginConfigsToArray'](pluginConfig); + + // THEN + expect(returnValue).toEqual(expectedResult); + }); + + test('converts DeleteTerminatedSlaves', () => { + // GIVEN + let pluginConfig = { + DeleteTerminatedSlaves: 'true', + }; + const expectedResult = [{ + Key: 'DeleteTerminatedSlaves', + Value: true, + }]; + + // WHEN + const handler = new SEPConfiguratorResource(new AWS.SecretsManager()); + const returnValue = handler['spotFleetPluginConfigsToArray'](pluginConfig); + + // THEN + expect(returnValue).toEqual(expectedResult); + }); + + test('converts IdleShutdown', () => { + // GIVEN + let pluginConfig = { + IdleShutdown: '10', + }; + const expectedResult = [{ + Key: 'IdleShutdown', + Value: 10, + }]; + + // WHEN + const handler = new SEPConfiguratorResource(new AWS.SecretsManager()); + const returnValue = handler['spotFleetPluginConfigsToArray'](pluginConfig); + + // THEN + expect(returnValue).toEqual(expectedResult); + }); + + test('converts ResourceTracker', () => { + // GIVEN + let pluginConfig = { + ResourceTracker: 'true', + }; + const expectedResult = [{ + Key: 'ResourceTracker', + Value: true, + }]; + + // WHEN + const handler = new SEPConfiguratorResource(new AWS.SecretsManager()); + const returnValue = handler['spotFleetPluginConfigsToArray'](pluginConfig); + + // THEN + expect(returnValue).toEqual(expectedResult); + }); + + test('converts StaggerInstances', () => { + // GIVEN + let pluginConfig = { + StaggerInstances: 'true', + }; + const expectedResult = [{ + Key: 'StaggerInstances', + Value: true, + }]; + + // WHEN + const handler = new SEPConfiguratorResource(new AWS.SecretsManager()); + const returnValue = handler['spotFleetPluginConfigsToArray'](pluginConfig); + + // THEN + expect(returnValue).toEqual(expectedResult); + }); + + test('converts StrictHardCap', () => { + // GIVEN + let pluginConfig = { + StrictHardCap: 'true', + }; + const expectedResult = [{ + Key: 'StrictHardCap', + Value: true, + }]; + + // WHEN + const handler = new SEPConfiguratorResource(new AWS.SecretsManager()); + const returnValue = handler['spotFleetPluginConfigsToArray'](pluginConfig); + + // THEN + expect(returnValue).toEqual(expectedResult); + }); + + test('converts UseLocalCredentials', () => { + // GIVEN + let pluginConfig = { + UseLocalCredentials: 'true', + }; + const expectedResult = [{ + Key: 'UseLocalCredentials', + Value: true, + }]; + + // WHEN + const handler = new SEPConfiguratorResource(new AWS.SecretsManager()); + const returnValue = handler['spotFleetPluginConfigsToArray'](pluginConfig); + + // THEN + expect(returnValue).toEqual(expectedResult); + }); + + test('skips undefined values', () => { + // GIVEN + let pluginConfig = { + DeleteInterruptedSlaves: undefined, + }; + + // WHEN + const handler = new SEPConfiguratorResource(new AWS.SecretsManager()); + const returnValue = handler['spotFleetPluginConfigsToArray'](pluginConfig); + + // THEN + expect(returnValue).toEqual([]); + }); + }); + + describe('.spotFleetRequestToString()', () => { + test('converts TargetCapacity', () => { + // GIVEN + const sfrConfig = { + groupname: { + TargetCapacity: '1', + }, + }; + const expectedResult = { + groupname: { + TargetCapacity: 1, + }, + }; + + // WHEN + const handler = new SEPConfiguratorResource(new AWS.SecretsManager()); + const returnValue = handler['spotFleetRequestToString'](sfrConfig); + + // THEN + expect(JSON.parse(returnValue)).toEqual(expectedResult); + }); + + test('converts ReplaceUnhealthyInstances', () => { + // GIVEN + const sfrConfig = { + groupname: { + ReplaceUnhealthyInstances: 'true', + }, + }; + const expectedResult = { + groupname: { + ReplaceUnhealthyInstances: true, + }, + }; + + // WHEN + const handler = new SEPConfiguratorResource(new AWS.SecretsManager()); + const returnValue = handler['spotFleetRequestToString'](sfrConfig); + + // THEN + expect(JSON.parse(returnValue)).toEqual(expectedResult); + }); + + test('converts TerminateInstancesWithExpiration', () => { + // GIVEN + const sfrConfig = { + groupname: { + TerminateInstancesWithExpiration: 'true', + }, + }; + const expectedResult = { + groupname: { + TerminateInstancesWithExpiration: true, + }, + }; + + // WHEN + const handler = new SEPConfiguratorResource(new AWS.SecretsManager()); + const returnValue = handler['spotFleetRequestToString'](sfrConfig); + + // THEN + expect(JSON.parse(returnValue)).toEqual(expectedResult); + }); + + test('converts whole BlockDeviceMappings', () => { + // GIVEN + const sfrConfig = { + groupname: { + LaunchSpecifications: [{ + BlockDeviceMappings: [{ + noDevice: 'true', + ebs: { + deleteOnTermination: 'true', + encrypted: 'true', + iops: '10', + volumeSize: '10', + }, + }], + }], + }, + }; + const expectedResult = { + groupname: { + LaunchSpecifications: [{ + BlockDeviceMappings: [{ + noDevice: true, + ebs: { + deleteOnTermination: true, + encrypted: true, + iops: 10, + volumeSize: 10, + }, + }], + }], + }, + }; + + // WHEN + const handler = new SEPConfiguratorResource(new AWS.SecretsManager()); + const returnValue = handler['spotFleetRequestToString'](sfrConfig); + + // THEN + expect(JSON.parse(returnValue)).toEqual(expectedResult); + }); + + test('converts noDevice', () => { + // GIVEN + const sfrConfig = { + groupname: { + LaunchSpecifications: [{ + BlockDeviceMappings: [{ + noDevice: 'true', + }], + }], + }, + }; + const expectedResult = { + groupname: { + LaunchSpecifications: [{ + BlockDeviceMappings: [{ + noDevice: true, + }], + }], + }, + }; + + // WHEN + const handler = new SEPConfiguratorResource(new AWS.SecretsManager()); + const returnValue = handler['spotFleetRequestToString'](sfrConfig); + + // THEN + expect(JSON.parse(returnValue)).toEqual(expectedResult); + }); + + test('converts deleteOnTermination', () => { + // GIVEN + const sfrConfig = { + groupname: { + LaunchSpecifications: [{ + BlockDeviceMappings: [{ + ebs: { + deleteOnTermination: 'true', + }, + }], + }], + }, + }; + const expectedResult = { + groupname: { + LaunchSpecifications: [{ + BlockDeviceMappings: [{ + ebs: { + deleteOnTermination: true, + }, + }], + }], + }, + }; + + // WHEN + const handler = new SEPConfiguratorResource(new AWS.SecretsManager()); + const returnValue = handler['spotFleetRequestToString'](sfrConfig); + + // THEN + expect(JSON.parse(returnValue)).toEqual(expectedResult); + }); + + test('converts encrypted', () => { + // GIVEN + const sfrConfig = { + groupname: { + LaunchSpecifications: [{ + BlockDeviceMappings: [{ + ebs: { + encrypted: 'true', + }, + }], + }], + }, + }; + const expectedResult = { + groupname: { + LaunchSpecifications: [{ + BlockDeviceMappings: [{ + ebs: { + encrypted: true, + }, + }], + }], + }, + }; + + // WHEN + const handler = new SEPConfiguratorResource(new AWS.SecretsManager()); + const returnValue = handler['spotFleetRequestToString'](sfrConfig); + + // THEN + expect(JSON.parse(returnValue)).toEqual(expectedResult); + }); + + test('converts volumeSize', () => { + // GIVEN + const sfrConfig = { + groupname: { + LaunchSpecifications: [{ + BlockDeviceMappings: [{ + ebs: { + volumeSize: '10', + }, + }], + }], + }, + }; + const expectedResult = { + groupname: { + LaunchSpecifications: [{ + BlockDeviceMappings: [{ + ebs: { + volumeSize: 10, + }, + }], + }], + }, + }; + + // WHEN + const handler = new SEPConfiguratorResource(new AWS.SecretsManager()); + const returnValue = handler['spotFleetRequestToString'](sfrConfig); + + // THEN + expect(JSON.parse(returnValue)).toEqual(expectedResult); + }); + + test('converts iops', () => { + // GIVEN + const sfrConfig = { + groupname: { + LaunchSpecifications: [{ + BlockDeviceMappings: [{ + ebs: { + iops: '10', + }, + }], + }], + }, + }; + const expectedResult = { + groupname: { + LaunchSpecifications: [{ + BlockDeviceMappings: [{ + ebs: { + iops: 10, + }, + }], + }], + }, + }; + + // WHEN + const handler = new SEPConfiguratorResource(new AWS.SecretsManager()); + const returnValue = handler['spotFleetRequestToString'](sfrConfig); + + // THEN + expect(JSON.parse(returnValue)).toEqual(expectedResult); + }); + }); });