From e197371683e98af3e40b7b338e914f053d384755 Mon Sep 17 00:00:00 2001 From: Cristobal Espinosa <4583012+cresvi@users.noreply.github.com> Date: Fri, 16 Feb 2024 14:03:19 -0600 Subject: [PATCH] feat(ecs): credentialSpecs in ContainerDefinitionOptions (#29085) ### Issue # (if applicable) Closes #N/A ### Reason for this change v2.127.0 updated the L1 construct for AWS::ECS::TaskDefinition, adding support for the property ContainerDefinitions.CredentialSpecs, [see](https://github.com/aws/aws-cdk/pull/29053). This PR adds support for CredentialSpecs property in the L2 construct used by `Ec2TaskDefinition.addContainer` method. ### Description of changes Added property in L2 construct, updated unit test and added integration test. ### Description of how you validated changes - [x] Unit test updated and validated - [x] Integration test added and validated ### Checklist - [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- ...efaultTestDeployAssertF6677424.assets.json | 19 + ...aultTestDeployAssertF6677424.template.json | 36 ++ ...tion-container-credentialspecs.assets.json | 19 + ...on-container-credentialspecs.template.json | 255 ++++++++++ .../cdk.out | 1 + .../integ.json | 12 + .../manifest.json | 164 +++++++ .../tree.json | 456 ++++++++++++++++++ ...sk-definition-container-credentialspecs.ts | 51 ++ packages/aws-cdk-lib/aws-ecs/README.md | 38 ++ .../aws-ecs/lib/container-definition.ts | 38 ++ .../aws-ecs/lib/credential-spec.ts | 129 +++++ packages/aws-cdk-lib/aws-ecs/lib/index.ts | 1 + .../aws-ecs/test/container-definition.test.ts | 32 +- .../aws-ecs/test/credential-spec.test.ts | 140 ++++++ 15 files changed, 1386 insertions(+), 5 deletions(-) create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/integ.task-definition-container-credentialspecs.js.snapshot/TaskDefinitionContainerCredSpecsDefaultTestDeployAssertF6677424.assets.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/integ.task-definition-container-credentialspecs.js.snapshot/TaskDefinitionContainerCredSpecsDefaultTestDeployAssertF6677424.template.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/integ.task-definition-container-credentialspecs.js.snapshot/aws-ecs-task-definition-container-credentialspecs.assets.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/integ.task-definition-container-credentialspecs.js.snapshot/aws-ecs-task-definition-container-credentialspecs.template.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/integ.task-definition-container-credentialspecs.js.snapshot/cdk.out create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/integ.task-definition-container-credentialspecs.js.snapshot/integ.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/integ.task-definition-container-credentialspecs.js.snapshot/manifest.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/integ.task-definition-container-credentialspecs.js.snapshot/tree.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/integ.task-definition-container-credentialspecs.ts create mode 100644 packages/aws-cdk-lib/aws-ecs/lib/credential-spec.ts create mode 100644 packages/aws-cdk-lib/aws-ecs/test/credential-spec.test.ts diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/integ.task-definition-container-credentialspecs.js.snapshot/TaskDefinitionContainerCredSpecsDefaultTestDeployAssertF6677424.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/integ.task-definition-container-credentialspecs.js.snapshot/TaskDefinitionContainerCredSpecsDefaultTestDeployAssertF6677424.assets.json new file mode 100644 index 0000000000000..05f160b14fd82 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/integ.task-definition-container-credentialspecs.js.snapshot/TaskDefinitionContainerCredSpecsDefaultTestDeployAssertF6677424.assets.json @@ -0,0 +1,19 @@ +{ + "version": "36.0.0", + "files": { + "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { + "source": { + "path": "TaskDefinitionContainerCredSpecsDefaultTestDeployAssertF6677424.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/integ.task-definition-container-credentialspecs.js.snapshot/TaskDefinitionContainerCredSpecsDefaultTestDeployAssertF6677424.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/integ.task-definition-container-credentialspecs.js.snapshot/TaskDefinitionContainerCredSpecsDefaultTestDeployAssertF6677424.template.json new file mode 100644 index 0000000000000..ad9d0fb73d1dd --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/integ.task-definition-container-credentialspecs.js.snapshot/TaskDefinitionContainerCredSpecsDefaultTestDeployAssertF6677424.template.json @@ -0,0 +1,36 @@ +{ + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/integ.task-definition-container-credentialspecs.js.snapshot/aws-ecs-task-definition-container-credentialspecs.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/integ.task-definition-container-credentialspecs.js.snapshot/aws-ecs-task-definition-container-credentialspecs.assets.json new file mode 100644 index 0000000000000..b924c8a6de6c2 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/integ.task-definition-container-credentialspecs.js.snapshot/aws-ecs-task-definition-container-credentialspecs.assets.json @@ -0,0 +1,19 @@ +{ + "version": "36.0.0", + "files": { + "15b792b5a8db7ad07b3634fb52b565ba733387c368ab58c412b388fcaad71f2a": { + "source": { + "path": "aws-ecs-task-definition-container-credentialspecs.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "15b792b5a8db7ad07b3634fb52b565ba733387c368ab58c412b388fcaad71f2a.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/integ.task-definition-container-credentialspecs.js.snapshot/aws-ecs-task-definition-container-credentialspecs.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/integ.task-definition-container-credentialspecs.js.snapshot/aws-ecs-task-definition-container-credentialspecs.template.json new file mode 100644 index 0000000000000..2805ad765fd87 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/integ.task-definition-container-credentialspecs.js.snapshot/aws-ecs-task-definition-container-credentialspecs.template.json @@ -0,0 +1,255 @@ +{ + "Resources": { + "bucket43879C71": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "AES256" + } + } + ] + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "bucketPolicy638F945D": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "bucket43879C71" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "bucket43879C71", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "bucket43879C71", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, + "parameter76C24FC7": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": "Sample CredSpec" + } + }, + "taskexecutionrole7BB27090": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/AmazonS3ReadOnlyAccess" + ] + ] + } + ], + "RoleName": "aws-ecs-task-definition-container-credentialspecs-task-exec-role" + } + }, + "TaskDefTaskRole1EDB4A67": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TaskDef54694570": { + "Type": "AWS::ECS::TaskDefinition", + "Properties": { + "ContainerDefinitions": [ + { + "CredentialSpecs": [ + { + "Fn::Join": [ + "", + [ + "credentialspec:arn:", + { + "Ref": "AWS::Partition" + }, + ":ssm:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":parameter/", + { + "Ref": "parameter76C24FC7" + } + ] + ] + } + ], + "Essential": true, + "Image": "public.ecr.aws/ecs-sample-image/amazon-ecs-sample:latest", + "Memory": 512, + "MemoryReservation": 32, + "Name": "DomainJoinedContainer" + }, + { + "CredentialSpecs": [ + { + "Fn::Join": [ + "", + [ + "credentialspecdomainless:", + { + "Fn::GetAtt": [ + "bucket43879C71", + "Arn" + ] + }, + "/credSpecDomainless" + ] + ] + } + ], + "Essential": true, + "Image": "public.ecr.aws/ecs-sample-image/amazon-ecs-sample:latest", + "Memory": 512, + "MemoryReservation": 32, + "Name": "DomainlessContainer" + } + ], + "ExecutionRoleArn": { + "Fn::GetAtt": [ + "taskexecutionrole7BB27090", + "Arn" + ] + }, + "Family": "awsecstaskdefinitioncontainercredentialspecsTaskDefE15276BC", + "NetworkMode": "bridge", + "RequiresCompatibilities": [ + "EC2" + ], + "TaskRoleArn": { + "Fn::GetAtt": [ + "TaskDefTaskRole1EDB4A67", + "Arn" + ] + } + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/integ.task-definition-container-credentialspecs.js.snapshot/cdk.out b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/integ.task-definition-container-credentialspecs.js.snapshot/cdk.out new file mode 100644 index 0000000000000..1f0068d32659a --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/integ.task-definition-container-credentialspecs.js.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"36.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/integ.task-definition-container-credentialspecs.js.snapshot/integ.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/integ.task-definition-container-credentialspecs.js.snapshot/integ.json new file mode 100644 index 0000000000000..250fe9811366a --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/integ.task-definition-container-credentialspecs.js.snapshot/integ.json @@ -0,0 +1,12 @@ +{ + "version": "36.0.0", + "testCases": { + "TaskDefinitionContainerCredSpecs/DefaultTest": { + "stacks": [ + "aws-ecs-task-definition-container-credentialspecs" + ], + "assertionStack": "TaskDefinitionContainerCredSpecs/DefaultTest/DeployAssert", + "assertionStackName": "TaskDefinitionContainerCredSpecsDefaultTestDeployAssertF6677424" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/integ.task-definition-container-credentialspecs.js.snapshot/manifest.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/integ.task-definition-container-credentialspecs.js.snapshot/manifest.json new file mode 100644 index 0000000000000..b73e5baec0629 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/integ.task-definition-container-credentialspecs.js.snapshot/manifest.json @@ -0,0 +1,164 @@ +{ + "version": "36.0.0", + "artifacts": { + "aws-ecs-task-definition-container-credentialspecs.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "aws-ecs-task-definition-container-credentialspecs.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "aws-ecs-task-definition-container-credentialspecs": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "aws-ecs-task-definition-container-credentialspecs.template.json", + "terminationProtection": false, + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/15b792b5a8db7ad07b3634fb52b565ba733387c368ab58c412b388fcaad71f2a.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "aws-ecs-task-definition-container-credentialspecs.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "aws-ecs-task-definition-container-credentialspecs.assets" + ], + "metadata": { + "/aws-ecs-task-definition-container-credentialspecs/bucket/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "bucket43879C71" + } + ], + "/aws-ecs-task-definition-container-credentialspecs/bucket/Policy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "bucketPolicy638F945D" + } + ], + "/aws-ecs-task-definition-container-credentialspecs/parameter/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "parameter76C24FC7" + } + ], + "/aws-ecs-task-definition-container-credentialspecs/task-execution-role/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "taskexecutionrole7BB27090" + } + ], + "/aws-ecs-task-definition-container-credentialspecs/TaskDef/TaskRole/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "TaskDefTaskRole1EDB4A67" + } + ], + "/aws-ecs-task-definition-container-credentialspecs/TaskDef/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "TaskDef54694570", + "trace": [ + "!!DESTRUCTIVE_CHANGES: WILL_REPLACE" + ] + } + ], + "/aws-ecs-task-definition-container-credentialspecs/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/aws-ecs-task-definition-container-credentialspecs/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ], + "s3bucket64CB25AF": [ + { + "type": "aws:cdk:logicalId", + "data": "s3bucket64CB25AF", + "trace": [ + "!!DESTRUCTIVE_CHANGES: WILL_DESTROY" + ] + } + ], + "s3bucketPolicyF7E91061": [ + { + "type": "aws:cdk:logicalId", + "data": "s3bucketPolicyF7E91061", + "trace": [ + "!!DESTRUCTIVE_CHANGES: WILL_DESTROY" + ] + } + ] + }, + "displayName": "aws-ecs-task-definition-container-credentialspecs" + }, + "TaskDefinitionContainerCredSpecsDefaultTestDeployAssertF6677424.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "TaskDefinitionContainerCredSpecsDefaultTestDeployAssertF6677424.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "TaskDefinitionContainerCredSpecsDefaultTestDeployAssertF6677424": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "TaskDefinitionContainerCredSpecsDefaultTestDeployAssertF6677424.template.json", + "terminationProtection": false, + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "TaskDefinitionContainerCredSpecsDefaultTestDeployAssertF6677424.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "TaskDefinitionContainerCredSpecsDefaultTestDeployAssertF6677424.assets" + ], + "metadata": { + "/TaskDefinitionContainerCredSpecs/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/TaskDefinitionContainerCredSpecs/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "TaskDefinitionContainerCredSpecs/DefaultTest/DeployAssert" + }, + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/integ.task-definition-container-credentialspecs.js.snapshot/tree.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/integ.task-definition-container-credentialspecs.js.snapshot/tree.json new file mode 100644 index 0000000000000..206bf29b36a21 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/integ.task-definition-container-credentialspecs.js.snapshot/tree.json @@ -0,0 +1,456 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "aws-ecs-task-definition-container-credentialspecs": { + "id": "aws-ecs-task-definition-container-credentialspecs", + "path": "aws-ecs-task-definition-container-credentialspecs", + "children": { + "bucket": { + "id": "bucket", + "path": "aws-ecs-task-definition-container-credentialspecs/bucket", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-ecs-task-definition-container-credentialspecs/bucket/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::S3::Bucket", + "aws:cdk:cloudformation:props": { + "bucketEncryption": { + "serverSideEncryptionConfiguration": [ + { + "serverSideEncryptionByDefault": { + "sseAlgorithm": "AES256" + } + } + ] + }, + "publicAccessBlockConfiguration": { + "blockPublicAcls": true, + "blockPublicPolicy": true, + "ignorePublicAcls": true, + "restrictPublicBuckets": true + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_s3.CfnBucket", + "version": "0.0.0" + } + }, + "Policy": { + "id": "Policy", + "path": "aws-ecs-task-definition-container-credentialspecs/bucket/Policy", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-ecs-task-definition-container-credentialspecs/bucket/Policy/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::S3::BucketPolicy", + "aws:cdk:cloudformation:props": { + "bucket": { + "Ref": "bucket43879C71" + }, + "policyDocument": { + "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "bucket43879C71", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "bucket43879C71", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_s3.CfnBucketPolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_s3.BucketPolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_s3.Bucket", + "version": "0.0.0" + } + }, + "parameter": { + "id": "parameter", + "path": "aws-ecs-task-definition-container-credentialspecs/parameter", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-ecs-task-definition-container-credentialspecs/parameter/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::SSM::Parameter", + "aws:cdk:cloudformation:props": { + "type": "String", + "value": "Sample CredSpec" + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_ssm.CfnParameter", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_ssm.StringParameter", + "version": "0.0.0" + } + }, + "task-execution-role": { + "id": "task-execution-role", + "path": "aws-ecs-task-definition-container-credentialspecs/task-execution-role", + "children": { + "Importtask-execution-role": { + "id": "Importtask-execution-role", + "path": "aws-ecs-task-definition-container-credentialspecs/task-execution-role/Importtask-execution-role", + "constructInfo": { + "fqn": "aws-cdk-lib.Resource", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "aws-ecs-task-definition-container-credentialspecs/task-execution-role/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Role", + "aws:cdk:cloudformation:props": { + "assumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "managedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/AmazonS3ReadOnlyAccess" + ] + ] + } + ], + "roleName": "aws-ecs-task-definition-container-credentialspecs-task-exec-role" + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.CfnRole", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.Role", + "version": "0.0.0" + } + }, + "TaskDef": { + "id": "TaskDef", + "path": "aws-ecs-task-definition-container-credentialspecs/TaskDef", + "children": { + "TaskRole": { + "id": "TaskRole", + "path": "aws-ecs-task-definition-container-credentialspecs/TaskDef/TaskRole", + "children": { + "ImportTaskRole": { + "id": "ImportTaskRole", + "path": "aws-ecs-task-definition-container-credentialspecs/TaskDef/TaskRole/ImportTaskRole", + "constructInfo": { + "fqn": "aws-cdk-lib.Resource", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "aws-ecs-task-definition-container-credentialspecs/TaskDef/TaskRole/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Role", + "aws:cdk:cloudformation:props": { + "assumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.CfnRole", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.Role", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "aws-ecs-task-definition-container-credentialspecs/TaskDef/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::ECS::TaskDefinition", + "aws:cdk:cloudformation:props": { + "containerDefinitions": [ + { + "credentialSpecs": [ + { + "Fn::Join": [ + "", + [ + "credentialspec:arn:", + { + "Ref": "AWS::Partition" + }, + ":ssm:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":parameter/", + { + "Ref": "parameter76C24FC7" + } + ] + ] + } + ], + "essential": true, + "image": "public.ecr.aws/ecs-sample-image/amazon-ecs-sample:latest", + "memory": 512, + "memoryReservation": 32, + "name": "DomainJoinedContainer" + }, + { + "credentialSpecs": [ + { + "Fn::Join": [ + "", + [ + "credentialspecdomainless:", + { + "Fn::GetAtt": [ + "bucket43879C71", + "Arn" + ] + }, + "/credSpecDomainless" + ] + ] + } + ], + "essential": true, + "image": "public.ecr.aws/ecs-sample-image/amazon-ecs-sample:latest", + "memory": 512, + "memoryReservation": 32, + "name": "DomainlessContainer" + } + ], + "executionRoleArn": { + "Fn::GetAtt": [ + "taskexecutionrole7BB27090", + "Arn" + ] + }, + "family": "awsecstaskdefinitioncontainercredentialspecsTaskDefE15276BC", + "networkMode": "bridge", + "requiresCompatibilities": [ + "EC2" + ], + "taskRoleArn": { + "Fn::GetAtt": [ + "TaskDefTaskRole1EDB4A67", + "Arn" + ] + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_ecs.CfnTaskDefinition", + "version": "0.0.0" + } + }, + "DomainJoinedContainer": { + "id": "DomainJoinedContainer", + "path": "aws-ecs-task-definition-container-credentialspecs/TaskDef/DomainJoinedContainer", + "constructInfo": { + "fqn": "aws-cdk-lib.aws_ecs.ContainerDefinition", + "version": "0.0.0" + } + }, + "DomainlessContainer": { + "id": "DomainlessContainer", + "path": "aws-ecs-task-definition-container-credentialspecs/TaskDef/DomainlessContainer", + "constructInfo": { + "fqn": "aws-cdk-lib.aws_ecs.ContainerDefinition", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_ecs.Ec2TaskDefinition", + "version": "0.0.0" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "aws-ecs-task-definition-container-credentialspecs/BootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "aws-ecs-task-definition-container-credentialspecs/CheckBootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.Stack", + "version": "0.0.0" + } + }, + "TaskDefinitionContainerCredSpecs": { + "id": "TaskDefinitionContainerCredSpecs", + "path": "TaskDefinitionContainerCredSpecs", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "TaskDefinitionContainerCredSpecs/DefaultTest", + "children": { + "Default": { + "id": "Default", + "path": "TaskDefinitionContainerCredSpecs/DefaultTest/Default", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "DeployAssert": { + "id": "DeployAssert", + "path": "TaskDefinitionContainerCredSpecs/DefaultTest/DeployAssert", + "children": { + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "TaskDefinitionContainerCredSpecs/DefaultTest/DeployAssert/BootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "TaskDefinitionContainerCredSpecs/DefaultTest/DeployAssert/CheckBootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.Stack", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests-alpha.IntegTestCase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests-alpha.IntegTest", + "version": "0.0.0" + } + }, + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.App", + "version": "0.0.0" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/integ.task-definition-container-credentialspecs.ts b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/integ.task-definition-container-credentialspecs.ts new file mode 100644 index 0000000000000..551c89c574171 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/integ.task-definition-container-credentialspecs.ts @@ -0,0 +1,51 @@ +import * as cdk from 'aws-cdk-lib'; +import * as ecs from 'aws-cdk-lib/aws-ecs'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import * as s3 from 'aws-cdk-lib/aws-s3'; +import * as ssm from 'aws-cdk-lib/aws-ssm'; +import { IntegTest } from '@aws-cdk/integ-tests-alpha'; + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-ecs-task-definition-container-credentialspecs'); + +const bucket = new s3.Bucket(stack, 'bucket', { + encryption: s3.BucketEncryption.S3_MANAGED, + blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, + removalPolicy: cdk.RemovalPolicy.DESTROY, + enforceSSL: true, +}); + +const parameter = new ssm.StringParameter(stack, 'parameter', { + stringValue: 'Sample CredSpec', +}); + +const taskExecutionRole = new iam.Role(stack, 'task-execution-role', { + roleName: 'aws-ecs-task-definition-container-credentialspecs-task-exec-role', + assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), +}); +taskExecutionRole.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonECSTaskExecutionRolePolicy')); +taskExecutionRole.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonS3ReadOnlyAccess')); + +const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef', { + executionRole: taskExecutionRole, +}); + +taskDefinition.addContainer('DomainJoinedContainer', { + image: ecs.ContainerImage.fromRegistry('public.ecr.aws/ecs-sample-image/amazon-ecs-sample:latest'), + memoryReservationMiB: 32, + memoryLimitMiB: 512, + credentialSpecs: [ecs.DomainJoinedCredentialSpec.fromSsmParameter(parameter)], +}); + +taskDefinition.addContainer('DomainlessContainer', { + image: ecs.ContainerImage.fromRegistry('public.ecr.aws/ecs-sample-image/amazon-ecs-sample:latest'), + memoryReservationMiB: 32, + memoryLimitMiB: 512, + credentialSpecs: [ecs.DomainlessCredentialSpec.fromS3Bucket(bucket, 'credSpecDomainless')], +}); + +new IntegTest(app, 'TaskDefinitionContainerCredSpecs', { + testCases: [stack], +}); + +app.synth(); \ No newline at end of file diff --git a/packages/aws-cdk-lib/aws-ecs/README.md b/packages/aws-cdk-lib/aws-ecs/README.md index 322e5b0635f03..43ac276abe91c 100644 --- a/packages/aws-cdk-lib/aws-ecs/README.md +++ b/packages/aws-cdk-lib/aws-ecs/README.md @@ -623,6 +623,44 @@ taskDefinition.addContainer('windowsservercore', { }); ``` +### Using Windows authentication with gMSA + +Amazon ECS supports Active Directory authentication for Linux containers through a special kind of service account called a group Managed Service Account (gMSA). For more details, please see the [product documentation on how to implement on Windows containers](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/windows-gmsa.html), or this [blog post on how to implement on Linux containers](https://aws.amazon.com/blogs/containers/using-windows-authentication-with-gmsa-on-linux-containers-on-amazon-ecs/). + +There are two types of CredentialSpecs, domained-join or domainless. Both types support creation from a S3 bucket, a SSM parameter, or by directly specifying a location for the file in the constructor. + +A domian-joined gMSA container looks like: + +```ts +// Make sure the task definition's execution role has permissions to read from the S3 bucket or SSM parameter where the CredSpec file is stored. +declare const parameter: ssm.IParameter; +declare const taskDefinition: ecs.TaskDefinition; + +// Domain-joined gMSA container from a SSM parameter +taskDefinition.addContainer('gmsa-domain-joined-container', { + image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"), + cpu: 128, + memoryLimitMiB: 256, + credentialSpecs: [ecs.DomainJoinedCredentialSpec.fromSsmParameter(parameter)], +}); +``` + +A domianless gMSA container looks like: + +```ts +// Make sure the task definition's execution role has permissions to read from the S3 bucket or SSM parameter where the CredSpec file is stored. +declare const bucket: s3.Bucket; +declare const taskDefinition: ecs.TaskDefinition; + +// Domainless gMSA container from a S3 bucket object. +taskDefinition.addContainer('gmsa-domainless-container', { + image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"), + cpu: 128, + memoryLimitMiB: 256, + credentialSpecs: [ecs.DomainlessCredentialSpec.fromS3Bucket(bucket, 'credSpec')], +}); +``` + ### Using Graviton2 with Fargate AWS Graviton2 supports AWS Fargate. For more details, please see this [blog post](https://aws.amazon.com/blogs/aws/announcing-aws-graviton2-support-for-aws-fargate-get-up-to-40-better-price-performance-for-your-serverless-containers/) diff --git a/packages/aws-cdk-lib/aws-ecs/lib/container-definition.ts b/packages/aws-cdk-lib/aws-ecs/lib/container-definition.ts index aecbc52b59fa4..3c7d3597aeb41 100644 --- a/packages/aws-cdk-lib/aws-ecs/lib/container-definition.ts +++ b/packages/aws-cdk-lib/aws-ecs/lib/container-definition.ts @@ -1,6 +1,7 @@ import { Construct } from 'constructs'; import { NetworkMode, TaskDefinition } from './base/task-definition'; import { ContainerImage, ContainerImageConfig } from './container-image'; +import { CredentialSpec, CredentialSpecConfig } from './credential-spec'; import { CfnTaskDefinition } from './ecs.generated'; import { EnvironmentFile, EnvironmentFileConfig } from './environment-file'; import { LinuxParameters } from './linux-parameters'; @@ -126,6 +127,17 @@ export interface ContainerDefinitionOptions { */ readonly command?: string[]; + /** + * A list of ARNs in SSM or Amazon S3 to a credential spec (`CredSpec`) file that configures the container for Active Directory authentication. + * + * We recommend that you use this parameter instead of the `dockerSecurityOptions`. + * + * Currently, only one credential spec is allowed per container definition. + * + * @default - No credential specs. + */ + readonly credentialSpecs?: CredentialSpec[]; + /** * The minimum number of CPU units to reserve for the container. * @@ -460,6 +472,11 @@ export class ContainerDefinition extends Construct { */ public readonly logDriverConfig?: LogDriverConfig; + /** + * The crdential specifications for this container. + */ + public readonly credentialSpecs?: CredentialSpecConfig[]; + /** * The name of the image referenced by this container. */ @@ -538,6 +555,18 @@ export class ContainerDefinition extends Construct { } } + if (props.credentialSpecs) { + this.credentialSpecs = []; + + if (props.credentialSpecs.length > 1) { + throw new Error('Only one credential spec is allowed per container definition.'); + } + + for (const credSpec of props.credentialSpecs) { + this.credentialSpecs.push(credSpec.bind()); + } + } + if (props.cpu) { this.cpu = props.cpu; } @@ -794,6 +823,7 @@ export class ContainerDefinition extends Construct { public renderContainerDefinition(_taskDefinition?: TaskDefinition): CfnTaskDefinition.ContainerDefinitionProperty { return { command: this.props.command, + credentialSpecs: this.credentialSpecs && this.credentialSpecs.map(renderCredentialSpec), cpu: this.props.cpu, disableNetworking: this.props.disableNetworking, dependsOn: cdk.Lazy.any({ produce: () => this.containerDependencies.map(renderContainerDependency) }, { omitEmptyArray: true }), @@ -912,6 +942,14 @@ function renderEnvironmentFiles(partition: string, environmentFiles: Environment return ret; } +function renderCredentialSpec(credSpec: CredentialSpecConfig): string { + if (!credSpec.location) { + throw Error('CredentialSpec must specify a valid location or ARN'); + } + + return `${credSpec.typePrefix}:${credSpec.location}`; +} + function renderHealthCheck(hc: HealthCheck): CfnTaskDefinition.HealthCheckProperty { if (hc.interval?.toSeconds() !== undefined) { if (5 > hc.interval?.toSeconds() || hc.interval?.toSeconds() > 300) { diff --git a/packages/aws-cdk-lib/aws-ecs/lib/credential-spec.ts b/packages/aws-cdk-lib/aws-ecs/lib/credential-spec.ts new file mode 100644 index 0000000000000..b92fecc0c18aa --- /dev/null +++ b/packages/aws-cdk-lib/aws-ecs/lib/credential-spec.ts @@ -0,0 +1,129 @@ +import { IBucket } from '../../aws-s3'; +import { IParameter } from '../../aws-ssm'; + +/** + * Base construct for a credential specification (CredSpec). + */ +export class CredentialSpec { + /** + * Helper method to generate the ARN for a S3 object. Used to avoid duplication of logic in derived classes. + */ + protected static arnForS3Object(bucket: IBucket, key: string) { + if (!key) { + throw new Error('key is undefined'); + } + + return bucket.arnForObjects(key); + } + + /** + * Helper method to generate the ARN for a SSM parameter. Used to avoid duplication of logic in derived classes. + */ + protected static arnForSsmParameter(parameter: IParameter) { + return parameter.parameterArn; + } + + /** + * Prefix string based on the type of CredSpec. + */ + public readonly prefixId: string; + + /** + * Location or ARN from where to retrieve the CredSpec file. + */ + public readonly fileLocation: string; + + /** + * @param fileLocation Location or ARN from where to retrieve the CredSpec file + */ + public constructor(prefixId: string, fileLocation: string) { + this.prefixId = prefixId; + this.fileLocation = fileLocation; + } + + /** + * Called when the container is initialized to allow this object to bind + * to the stack. + */ + public bind(): CredentialSpecConfig { + return { + typePrefix: this.prefixId, + location: this.fileLocation, + }; + } +} + +/** + * Credential specification (CredSpec) file. + */ +export class DomainJoinedCredentialSpec extends CredentialSpec { + /** + * Loads the CredSpec from a S3 bucket object. + * + * @param bucket The S3 bucket + * @param key The object key + * @returns CredSpec with it's locations set to the S3 object's ARN. + */ + public static fromS3Bucket(bucket: IBucket, key: string) { + return new DomainJoinedCredentialSpec(CredentialSpec.arnForS3Object(bucket, key)); + } + + /** + * Loads the CredSpec from a SSM parameter. + * + * @param parameter The SSM parameter + * @returns CredSpec with it's locations set to the SSM parameter's ARN. + */ + public static fromSsmParameter(parameter: IParameter) { + return new DomainJoinedCredentialSpec(CredentialSpec.arnForSsmParameter(parameter)); + } + + public constructor(fileLocation: string) { + super('credentialspec', fileLocation); + } +} + +/** + * Credential specification for domainless gMSA. + */ +export class DomainlessCredentialSpec extends CredentialSpec { + /** + * Loads the CredSpec from a S3 bucket object. + * + * @param bucket The S3 bucket + * @param key The object key + * @returns CredSpec with it's locations set to the S3 object's ARN. + */ + public static fromS3Bucket(bucket: IBucket, key: string) { + return new DomainlessCredentialSpec(CredentialSpec.arnForS3Object(bucket, key)); + } + + /** + * Loads the CredSpec from a SSM parameter. + * + * @param parameter The SSM parameter + * @returns CredSpec with it's locations set to the SSM parameter's ARN. + */ + public static fromSsmParameter(parameter: IParameter) { + return new DomainlessCredentialSpec(CredentialSpec.arnForSsmParameter(parameter)); + } + + public constructor(fileLocation: string) { + super('credentialspecdomainless', fileLocation); + } +} + +/** + * Configuration for a credential specification (CredSpec) used for a ECS container. + */ +export interface CredentialSpecConfig { + /** + * Prefix used for the CredSpec string. + */ + readonly typePrefix: string; + + /** + * Location of the CredSpec file. + */ + readonly location: string; +} \ No newline at end of file diff --git a/packages/aws-cdk-lib/aws-ecs/lib/index.ts b/packages/aws-cdk-lib/aws-ecs/lib/index.ts index e3e34f236dd4d..13931980ba3e5 100644 --- a/packages/aws-cdk-lib/aws-ecs/lib/index.ts +++ b/packages/aws-cdk-lib/aws-ecs/lib/index.ts @@ -8,6 +8,7 @@ export * from './container-image'; export * from './amis'; export * from './cluster'; export * from './environment-file'; +export * from './credential-spec'; export * from './firelens-log-router'; export * from './placement'; diff --git a/packages/aws-cdk-lib/aws-ecs/test/container-definition.test.ts b/packages/aws-cdk-lib/aws-ecs/test/container-definition.test.ts index 8a5c5ea165249..0e61cc3341af5 100644 --- a/packages/aws-cdk-lib/aws-ecs/test/container-definition.test.ts +++ b/packages/aws-cdk-lib/aws-ecs/test/container-definition.test.ts @@ -404,7 +404,7 @@ describe('container definition', () => { }).toThrow(/Service connect-related port mapping field 'appProtocol' cannot be set without 'name'/); }); - test('multiple port mappings of the same name error out', () =>{ + test('multiple port mappings of the same name error out', () => { // GIVEN const stack = new cdk.Stack(); const taskDefinition = new ecs.FargateTaskDefinition(stack, 'TaskDef'); @@ -465,6 +465,7 @@ describe('container definition', () => { memoryReservationMiB: 512, containerName: 'Example Container', command: ['CMD-SHELL'], + credentialSpecs: [new ecs.DomainlessCredentialSpec('arn:aws:s3:::bucket_name/key_name')], cpu: 128, disableNetworking: true, dnsSearchDomains: ['example.com'], @@ -514,6 +515,9 @@ describe('container definition', () => { 'CMD-SHELL', ], Cpu: 128, + CredentialSpecs: [ + 'credentialspecdomainless:arn:aws:s3:::bucket_name/key_name', + ], DisableNetworking: true, DnsSearchDomains: [ 'example.com', @@ -1099,7 +1103,7 @@ describe('container definition', () => { // THEN const expected = 8080; - expect(actual).toEqual( expected); + expect(actual).toEqual(expected); }); }); @@ -1125,7 +1129,7 @@ describe('container definition', () => { // THEN const expected = 8081; - expect(actual).toEqual( expected); + expect(actual).toEqual(expected); }); test('Ingress port should be 0 if not supplied', () => { @@ -2401,7 +2405,7 @@ describe('container definition', () => { }); // THEN - expect(taskDefinition.defaultContainer).toEqual( container); + expect(taskDefinition.defaultContainer).toEqual(container); }); @@ -2418,7 +2422,7 @@ describe('container definition', () => { }); // THEN - expect(taskDefinition.defaultContainer).toEqual( undefined); + expect(taskDefinition.defaultContainer).toEqual(undefined); }); }); @@ -2662,4 +2666,22 @@ describe('container definition', () => { ], }); }); + + test('fails if more than one credentialSpec is provided', () => { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef'); + const containerDefinitionProps = { + image: ecs.ContainerImage.fromRegistry('/aws/aws-example-app'), + taskDefinition, + memoryLimitMiB: 2048, + credentialSpecs: [ + new ecs.DomainlessCredentialSpec('arn:aws:s3:::bucket_name/key_name'), + new ecs.DomainlessCredentialSpec('arn:aws:s3:::bucket_name/key_name_2'), + ], + }; + + // THEN + expect(() => new ecs.ContainerDefinition(stack, 'Container', containerDefinitionProps)).toThrow(/Only one credential spec is allowed per container definition/); + }); }); diff --git a/packages/aws-cdk-lib/aws-ecs/test/credential-spec.test.ts b/packages/aws-cdk-lib/aws-ecs/test/credential-spec.test.ts new file mode 100644 index 0000000000000..158e08a2f06d5 --- /dev/null +++ b/packages/aws-cdk-lib/aws-ecs/test/credential-spec.test.ts @@ -0,0 +1,140 @@ +import * as s3 from '../../aws-s3'; +import * as ssm from '../../aws-ssm'; +import * as cdk from '../../core'; +import * as ecs from '../lib'; + +/* eslint-disable dot-notation */ + +describe('credential spec', () => { + describe('ecs.DomainJoinedCredentialSpec', () => { + test('returns the correct prefixId and location', () => { + // GIVEN + const stack = new cdk.Stack(); + const credSpecLocation = 'credSpecLocation'; + const credSpec = new ecs.DomainJoinedCredentialSpec(credSpecLocation); + const containerDefinition = defineContainerDefinition(stack, credSpec); + + // THEN + expect(containerDefinition.credentialSpecs?.length == 1); + expect(containerDefinition.credentialSpecs?.at(0)?.typePrefix).toEqual('credentialspec'); + expect(containerDefinition.credentialSpecs?.at(0)?.location).toEqual(credSpecLocation); + }); + + describe('fromS3Bucket', () => { + test('returns a valid version-less S3 object ARN as location', () => { + // GIVEN + const stack = new cdk.Stack(); + const objectKey = 'credSpec'; + const bucket = new s3.Bucket(stack, 'bucket'); + const credSpec = ecs.DomainJoinedCredentialSpec.fromS3Bucket(bucket, objectKey); + const containerDefinition = defineContainerDefinition(stack, credSpec); + + // THEN + expect(containerDefinition.credentialSpecs?.at(0)?.location).toEqual(bucket.arnForObjects(objectKey)); + }); + + test('returns a valid versioned S3 object ARN as location', () => { + // GIVEN + const stack = new cdk.Stack(); + const objectKey = 'credSpec'; + const bucket = new s3.Bucket(stack, 'bucket'); + const credSpec = ecs.DomainJoinedCredentialSpec.fromS3Bucket(bucket, objectKey); + const containerDefinition = defineContainerDefinition(stack, credSpec); + + // THEN + expect(containerDefinition.credentialSpecs?.at(0)?.location).toEqual(bucket.arnForObjects(objectKey)); + }); + }); + + describe('fromSsmParameter', () => { + test('returns a valid SSM parameter ARN as location', () => { + // GIVEN + const stack = new cdk.Stack(); + const parameter = new ssm.StringParameter(stack, 'parameter', { + stringValue: 'value', + }); + const credSpec = ecs.DomainJoinedCredentialSpec.fromSsmParameter(parameter); + const containerDefinition = defineContainerDefinition(stack, credSpec); + + // THEN + expect(containerDefinition.credentialSpecs?.at(0)?.location).toEqual(parameter.parameterArn); + }); + }); + }); + + describe('ecs.DomainlessCredentialSpec', () => { + test('returns the correct prefixId and location', () => { + // GIVEN + const stack = new cdk.Stack(); + const credSpecLocation = 'credSpecLocation'; + const credSpec = new ecs.DomainlessCredentialSpec(credSpecLocation); + const containerDefinition = defineContainerDefinition(stack, credSpec); + + // THEN + expect(containerDefinition.credentialSpecs?.length == 1); + expect(containerDefinition.credentialSpecs?.at(0)?.typePrefix).toEqual('credentialspecdomainless'); + expect(containerDefinition.credentialSpecs?.at(0)?.location).toEqual(credSpecLocation); + }); + + describe('fromS3Bucket', () => { + test('fails if key name is empty', () => { + // GIVEN + const stack = new cdk.Stack(); + const bucket = new s3.Bucket(stack, 'bucket'); + + // THEN + expect(() => ecs.DomainlessCredentialSpec.fromS3Bucket(bucket, '')).toThrow(/key is undefined/); + }); + + test('returns a valid version-less S3 object ARN as location', () => { + // GIVEN + const stack = new cdk.Stack(); + const objectKey = 'credSpec'; + const bucket = new s3.Bucket(stack, 'bucket'); + const credSpec = ecs.DomainlessCredentialSpec.fromS3Bucket(bucket, objectKey); + const containerDefinition = defineContainerDefinition(stack, credSpec); + + // THEN + expect(containerDefinition.credentialSpecs?.at(0)?.location).toEqual(bucket.arnForObjects(objectKey)); + }); + + test('returns a valid versioned S3 object ARN as location', () => { + // GIVEN + const stack = new cdk.Stack(); + const objectKey = 'credSpec'; + const bucket = new s3.Bucket(stack, 'bucket'); + const credSpec = ecs.DomainlessCredentialSpec.fromS3Bucket(bucket, objectKey); + const containerDefinition = defineContainerDefinition(stack, credSpec); + + // THEN + expect(containerDefinition.credentialSpecs?.at(0)?.location).toEqual(bucket.arnForObjects(objectKey)); + }); + }); + + describe('fromSsmParameter', () => { + test('returns a valid SSM parameter ARN as location', () => { + // GIVEN + const stack = new cdk.Stack(); + const parameter = new ssm.StringParameter(stack, 'parameter', { + stringValue: 'value', + }); + const credSpec = ecs.DomainlessCredentialSpec.fromSsmParameter(parameter); + const containerDefinition = defineContainerDefinition(stack, credSpec); + + // THEN + expect(containerDefinition.credentialSpecs?.at(0)?.location).toEqual(parameter.parameterArn); + }); + }); + }); +}); + +function defineContainerDefinition(stack: cdk.Stack, credentialSpec: ecs.CredentialSpec) { + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef'); + + return new ecs.ContainerDefinition(stack, 'Container', { + credentialSpecs: [credentialSpec], + image: ecs.ContainerImage.fromRegistry('/aws/aws-example-app'), + memoryLimitMiB: 512, + taskDefinition, + }); +}