From 030db42ad06a471af3833665cb80dceee034aa02 Mon Sep 17 00:00:00 2001 From: Kyle Laker Date: Thu, 28 Dec 2023 23:05:16 -0500 Subject: [PATCH] feat(cloudfront): Key Value Store L2 (#28473) This adds an initial resource to support creating a Key Value Store and specifying an import source. Unfortunately, CloudFormation doesn't seem to support specifying the `KeyValueStoreAssociations` property of a function so there isn't a way to actually associate the store with a function. Closes #28377. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- ...efaultTestDeployAssert87336A31.assets.json | 19 ++ ...aultTestDeployAssert87336A31.template.json | 36 +++ ...0affe8fd26234f67dcf2e93db5142bc2991a1.json | 12 + .../integ.key-value-store.js.snapshot/cdk.out | 1 + .../integ-key-value-store.assets.json | 32 +++ .../integ-key-value-store.template.json | 64 ++++++ .../integ.json | 12 + .../manifest.json | 113 ++++++++++ .../tree.json | 171 ++++++++++++++ .../test/integ.key-value-store.ts | 18 ++ .../test/test-import-source.json | 12 + packages/aws-cdk-lib/aws-cloudfront/README.md | 21 ++ .../aws-cdk-lib/aws-cloudfront/lib/index.ts | 1 + .../aws-cloudfront/lib/key-value-store.ts | 209 ++++++++++++++++++ .../test/key-value-store-source.json | 0 .../test/key-value-store.test.ts | 122 ++++++++++ 16 files changed, 843 insertions(+) create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront/test/integ.key-value-store.js.snapshot/KeyValueStoreDefaultTestDeployAssert87336A31.assets.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront/test/integ.key-value-store.js.snapshot/KeyValueStoreDefaultTestDeployAssert87336A31.template.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront/test/integ.key-value-store.js.snapshot/asset.9c0f502203ec9d802046021b8a20affe8fd26234f67dcf2e93db5142bc2991a1.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront/test/integ.key-value-store.js.snapshot/cdk.out create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront/test/integ.key-value-store.js.snapshot/integ-key-value-store.assets.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront/test/integ.key-value-store.js.snapshot/integ-key-value-store.template.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront/test/integ.key-value-store.js.snapshot/integ.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront/test/integ.key-value-store.js.snapshot/manifest.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront/test/integ.key-value-store.js.snapshot/tree.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront/test/integ.key-value-store.ts create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront/test/test-import-source.json create mode 100644 packages/aws-cdk-lib/aws-cloudfront/lib/key-value-store.ts create mode 100644 packages/aws-cdk-lib/aws-cloudfront/test/key-value-store-source.json create mode 100644 packages/aws-cdk-lib/aws-cloudfront/test/key-value-store.test.ts diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront/test/integ.key-value-store.js.snapshot/KeyValueStoreDefaultTestDeployAssert87336A31.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront/test/integ.key-value-store.js.snapshot/KeyValueStoreDefaultTestDeployAssert87336A31.assets.json new file mode 100644 index 0000000000000..9d8e7b1e39bb7 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront/test/integ.key-value-store.js.snapshot/KeyValueStoreDefaultTestDeployAssert87336A31.assets.json @@ -0,0 +1,19 @@ +{ + "version": "35.0.0", + "files": { + "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { + "source": { + "path": "KeyValueStoreDefaultTestDeployAssert87336A31.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-cloudfront/test/integ.key-value-store.js.snapshot/KeyValueStoreDefaultTestDeployAssert87336A31.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront/test/integ.key-value-store.js.snapshot/KeyValueStoreDefaultTestDeployAssert87336A31.template.json new file mode 100644 index 0000000000000..ad9d0fb73d1dd --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront/test/integ.key-value-store.js.snapshot/KeyValueStoreDefaultTestDeployAssert87336A31.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-cloudfront/test/integ.key-value-store.js.snapshot/asset.9c0f502203ec9d802046021b8a20affe8fd26234f67dcf2e93db5142bc2991a1.json b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront/test/integ.key-value-store.js.snapshot/asset.9c0f502203ec9d802046021b8a20affe8fd26234f67dcf2e93db5142bc2991a1.json new file mode 100644 index 0000000000000..c5999fd9d8723 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront/test/integ.key-value-store.js.snapshot/asset.9c0f502203ec9d802046021b8a20affe8fd26234f67dcf2e93db5142bc2991a1.json @@ -0,0 +1,12 @@ +{ + "data":[ + { + "key":"key1", + "value":"value" + }, + { + "key":"key2", + "value":"value" + } + ] +} diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront/test/integ.key-value-store.js.snapshot/cdk.out b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront/test/integ.key-value-store.js.snapshot/cdk.out new file mode 100644 index 0000000000000..c5cb2e5de6344 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront/test/integ.key-value-store.js.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"35.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront/test/integ.key-value-store.js.snapshot/integ-key-value-store.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront/test/integ.key-value-store.js.snapshot/integ-key-value-store.assets.json new file mode 100644 index 0000000000000..b0da7a914d7ee --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront/test/integ.key-value-store.js.snapshot/integ-key-value-store.assets.json @@ -0,0 +1,32 @@ +{ + "version": "35.0.0", + "files": { + "9c0f502203ec9d802046021b8a20affe8fd26234f67dcf2e93db5142bc2991a1": { + "source": { + "path": "asset.9c0f502203ec9d802046021b8a20affe8fd26234f67dcf2e93db5142bc2991a1.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "9c0f502203ec9d802046021b8a20affe8fd26234f67dcf2e93db5142bc2991a1.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + }, + "67b48bddcb70e3ca5fc03cf0a123eeb6b3616c2e388fb855ab860bd8f3a6c86c": { + "source": { + "path": "integ-key-value-store.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "67b48bddcb70e3ca5fc03cf0a123eeb6b3616c2e388fb855ab860bd8f3a6c86c.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-cloudfront/test/integ.key-value-store.js.snapshot/integ-key-value-store.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront/test/integ.key-value-store.js.snapshot/integ-key-value-store.template.json new file mode 100644 index 0000000000000..05bf7e195dd9c --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront/test/integ.key-value-store.js.snapshot/integ-key-value-store.template.json @@ -0,0 +1,64 @@ +{ + "Resources": { + "TestKeyValueStore8D0C09A2": { + "Type": "AWS::CloudFront::KeyValueStore", + "Properties": { + "Comment": "A test Key Value Store for CloudFront", + "ImportSource": { + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "/9c0f502203ec9d802046021b8a20affe8fd26234f67dcf2e93db5142bc2991a1.json" + ] + ] + }, + "SourceType": "S3" + }, + "Name": "integkeyvaluestoreTestKeyValueStore737B4C4B" + } + } + }, + "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-cloudfront/test/integ.key-value-store.js.snapshot/integ.json b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront/test/integ.key-value-store.js.snapshot/integ.json new file mode 100644 index 0000000000000..c083e81baebff --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront/test/integ.key-value-store.js.snapshot/integ.json @@ -0,0 +1,12 @@ +{ + "version": "35.0.0", + "testCases": { + "KeyValueStore/DefaultTest": { + "stacks": [ + "integ-key-value-store" + ], + "assertionStack": "KeyValueStore/DefaultTest/DeployAssert", + "assertionStackName": "KeyValueStoreDefaultTestDeployAssert87336A31" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront/test/integ.key-value-store.js.snapshot/manifest.json b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront/test/integ.key-value-store.js.snapshot/manifest.json new file mode 100644 index 0000000000000..0f9b9ba23adf1 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront/test/integ.key-value-store.js.snapshot/manifest.json @@ -0,0 +1,113 @@ +{ + "version": "35.0.0", + "artifacts": { + "integ-key-value-store.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "integ-key-value-store.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "integ-key-value-store": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "integ-key-value-store.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}/67b48bddcb70e3ca5fc03cf0a123eeb6b3616c2e388fb855ab860bd8f3a6c86c.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "integ-key-value-store.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": [ + "integ-key-value-store.assets" + ], + "metadata": { + "/integ-key-value-store/TestKeyValueStore/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "TestKeyValueStore8D0C09A2" + } + ], + "/integ-key-value-store/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/integ-key-value-store/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "integ-key-value-store" + }, + "KeyValueStoreDefaultTestDeployAssert87336A31.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "KeyValueStoreDefaultTestDeployAssert87336A31.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "KeyValueStoreDefaultTestDeployAssert87336A31": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "KeyValueStoreDefaultTestDeployAssert87336A31.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": [ + "KeyValueStoreDefaultTestDeployAssert87336A31.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": [ + "KeyValueStoreDefaultTestDeployAssert87336A31.assets" + ], + "metadata": { + "/KeyValueStore/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/KeyValueStore/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "KeyValueStore/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-cloudfront/test/integ.key-value-store.js.snapshot/tree.json b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront/test/integ.key-value-store.js.snapshot/tree.json new file mode 100644 index 0000000000000..06061cbdb9222 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront/test/integ.key-value-store.js.snapshot/tree.json @@ -0,0 +1,171 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "integ-key-value-store": { + "id": "integ-key-value-store", + "path": "integ-key-value-store", + "children": { + "TestKeyValueStore": { + "id": "TestKeyValueStore", + "path": "integ-key-value-store/TestKeyValueStore", + "children": { + "ImportSource": { + "id": "ImportSource", + "path": "integ-key-value-store/TestKeyValueStore/ImportSource", + "children": { + "Stage": { + "id": "Stage", + "path": "integ-key-value-store/TestKeyValueStore/ImportSource/Stage", + "constructInfo": { + "fqn": "aws-cdk-lib.AssetStaging", + "version": "0.0.0" + } + }, + "AssetBucket": { + "id": "AssetBucket", + "path": "integ-key-value-store/TestKeyValueStore/ImportSource/AssetBucket", + "constructInfo": { + "fqn": "aws-cdk-lib.aws_s3.BucketBase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_s3_assets.Asset", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "integ-key-value-store/TestKeyValueStore/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::CloudFront::KeyValueStore", + "aws:cdk:cloudformation:props": { + "comment": "A test Key Value Store for CloudFront", + "importSource": { + "sourceType": "S3", + "sourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "/9c0f502203ec9d802046021b8a20affe8fd26234f67dcf2e93db5142bc2991a1.json" + ] + ] + } + }, + "name": "integkeyvaluestoreTestKeyValueStore737B4C4B" + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_cloudfront.CfnKeyValueStore", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_cloudfront.KeyValueStore", + "version": "0.0.0" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "integ-key-value-store/BootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "integ-key-value-store/CheckBootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.Stack", + "version": "0.0.0" + } + }, + "KeyValueStore": { + "id": "KeyValueStore", + "path": "KeyValueStore", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "KeyValueStore/DefaultTest", + "children": { + "Default": { + "id": "Default", + "path": "KeyValueStore/DefaultTest/Default", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "DeployAssert": { + "id": "DeployAssert", + "path": "KeyValueStore/DefaultTest/DeployAssert", + "children": { + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "KeyValueStore/DefaultTest/DeployAssert/BootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "KeyValueStore/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-cloudfront/test/integ.key-value-store.ts b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront/test/integ.key-value-store.ts new file mode 100644 index 0000000000000..be6c2f8da3155 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront/test/integ.key-value-store.ts @@ -0,0 +1,18 @@ +import * as path from 'node:path'; +import * as cdk from 'aws-cdk-lib'; +import { IntegTest } from '@aws-cdk/integ-tests-alpha'; +import * as cloudfront from 'aws-cdk-lib/aws-cloudfront'; + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'integ-key-value-store'); + +new cloudfront.KeyValueStore(stack, 'TestKeyValueStore', { + comment: 'A test Key Value Store for CloudFront', + source: cloudfront.ImportSource.fromAsset(path.join(__dirname, 'test-import-source.json')), +}); + +new IntegTest(app, 'KeyValueStore', { + testCases: [stack], +}); + +app.synth(); diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront/test/test-import-source.json b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront/test/test-import-source.json new file mode 100644 index 0000000000000..c5999fd9d8723 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront/test/test-import-source.json @@ -0,0 +1,12 @@ +{ + "data":[ + { + "key":"key1", + "value":"value" + }, + { + "key":"key2", + "value":"value" + } + ] +} diff --git a/packages/aws-cdk-lib/aws-cloudfront/README.md b/packages/aws-cdk-lib/aws-cloudfront/README.md index 4766a7801b6b8..18b47bcd6ea51 100644 --- a/packages/aws-cdk-lib/aws-cloudfront/README.md +++ b/packages/aws-cdk-lib/aws-cloudfront/README.md @@ -536,6 +536,27 @@ It will auto-generate the name of the function and deploy it to the `live` stage Additionally, you can load the function's code from a file using the `FunctionCode.fromFile()` method. +### Key Value Store + +A CloudFront Key Value Store can be created and optionally have data imported from a JSON file +by default. + +To create an empty Key Value Store: + +```ts +const store = new cloudfront.KeyValueStore(this, 'KeyValueStore'); +``` + +To also include an initial set of value, the `source` property can be specified. For the +structure of this file, see [Creating a file of key value pairs](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/kvs-with-functions-create-s3-kvp.html). + +```ts +const store = new cloudfront.KeyValueStore(this, 'KeyValueStore', { + keyValueStoreName: 'KeyValueStore', + source: cloudfront.ImportSource.fromAsset('path-to-data.json'), +}); +``` + ### Logging You can configure CloudFront to create log files that contain detailed information about every user request that CloudFront receives. diff --git a/packages/aws-cdk-lib/aws-cloudfront/lib/index.ts b/packages/aws-cdk-lib/aws-cloudfront/lib/index.ts index fec6239124609..5a7e2863c8a4e 100644 --- a/packages/aws-cdk-lib/aws-cloudfront/lib/index.ts +++ b/packages/aws-cdk-lib/aws-cloudfront/lib/index.ts @@ -4,6 +4,7 @@ export * from './endpoint'; export * from './function'; export * from './geo-restriction'; export * from './key-group'; +export * from './key-value-store'; export * from './origin'; export * from './origin-access-identity'; export * from './origin-request-policy'; diff --git a/packages/aws-cdk-lib/aws-cloudfront/lib/key-value-store.ts b/packages/aws-cdk-lib/aws-cloudfront/lib/key-value-store.ts new file mode 100644 index 0000000000000..4dc6c45fd3871 --- /dev/null +++ b/packages/aws-cdk-lib/aws-cloudfront/lib/key-value-store.ts @@ -0,0 +1,209 @@ +import { Construct } from 'constructs'; +import { CfnKeyValueStore } from './cloudfront.generated'; +import * as s3 from '../../aws-s3'; +import * as s3_assets from '../../aws-s3-assets'; +import { Resource, IResource, Lazy, Names, Stack, Arn, ArnFormat } from '../../core'; + +/** + * The data to be imported to the key value store. + */ +export abstract class ImportSource { + /** + * An import source that exists as an object in an S3 bucket. + * + * @param bucket the S3 bucket that contains the data + * @param key the key within the S3 bucket that contains the data + */ + public static fromBucket(bucket: s3.IBucket, key: string): ImportSource { + return new S3ImportSource(bucket, key); + } + + /** + * An import source that exists as a local file. + * + * @param path the path to the local file + * @param options the configuration for the temporarily created S3 file + */ + public static fromAsset(path: string, options?: s3_assets.AssetOptions): ImportSource { + return new AssetImportSource(path, options); + } + + /** + * Called when the key value store is initialized to allow the import source to + * be bound to the stack. + * + * The method is primarily intended for internal use. + * + * @param scope the binding scope + * @internal + */ + public abstract _bind(scope: Construct): CfnKeyValueStore.ImportSourceProperty; +} + +/** + * An import source from an S3 object. + */ +export class S3ImportSource extends ImportSource { + /** + * @param bucket the S3 bucket that contains the data + * @param key the key within the S3 bucket that contains the data + */ + constructor(public readonly bucket: s3.IBucket, public readonly key: string) { + super(); + } + + /** + * @internal + */ + public _bind(_scope: Construct): CfnKeyValueStore.ImportSourceProperty { + return { + sourceType: 'S3', + sourceArn: `${this.bucket.arnForObjects(this.key)}`, + }; + } +} + +/** + * An import source from a local file. + */ +export class AssetImportSource extends ImportSource { + private asset?: s3_assets.Asset; + + /** + * @param path the path to the local file + * @param options the configuration for the temporarily created S3 file + */ + constructor(public readonly path: string, private readonly options: s3_assets.AssetOptions = {}) { + super(); + } + + /** + * @internal + */ + public _bind(scope: Construct): CfnKeyValueStore.ImportSourceProperty { + if (!this.asset) { + this.asset = new s3_assets.Asset(scope, 'ImportSource', { + path: this.path, + deployTime: true, + ...this.options, + }); + } else if (Stack.of(this.asset) !== Stack.of(scope)) { + throw new Error( + `Asset is already associated with another stack '${Stack.of(this.asset).stackName}. ` + + 'Create a new ImportSource instance for every stack.', + ); + } + + return { + sourceType: 'S3', + sourceArn: this.asset.bucket.arnForObjects(this.asset.s3ObjectKey), + }; + } +} + +/** + * The properties to create a Key Value Store. + */ +export interface KeyValueStoreProps { + /** + * The unique name of the Key Value Store. + * + * @default A generated name + */ + readonly keyValueStoreName?: string; + + /** + * A comment for the Key Value Store + * + * @default No comment will be specified + */ + readonly comment?: string; + + /** + * The import source for the Key Value Store. + * + * This will populate the initial items in the Key Value Store. The + * source data must be in a valid JSON format. + * + * @default No data will be imported to the store + */ + readonly source?: ImportSource; +} + +/** + * A CloudFront Key Value Store. + */ +export interface IKeyValueStore extends IResource { + /** + * The ARN of the Key Value Store. + * + * @attribute + */ + readonly keyValueStoreArn: string; + + /** + * The Unique ID of the Key Value Store. + * + * @attribute + */ + readonly keyValueStoreId: string; + + /** + * The status of the Key Value Store. + * + * @attribute + */ + readonly keyValueStoreStatus: string; +} + +/** + * A CloudFront Key Value Store. + * + * @resource AWS::CloudFront::KeyValueStore + */ +export class KeyValueStore extends Resource implements IKeyValueStore { + /** + * Import a Key Value Store using its ARN. + */ + public static fromKeyValueStoreArn(scope: Construct, id: string, keyValueStoreArn: string): IKeyValueStore { + const storeId = Arn.split(keyValueStoreArn, ArnFormat.SLASH_RESOURCE_NAME).resourceName; + if (!storeId) { + throw new Error(`Invalid Key Value Store Arn: '${keyValueStoreArn}'`); + } + return new class Import extends Resource implements IKeyValueStore { + readonly keyValueStoreArn: string = keyValueStoreArn; + readonly keyValueStoreId: string = storeId!; + constructor() { + super(scope, id, { + environmentFromArn: keyValueStoreArn, + }); + } + + public get keyValueStoreStatus(): string { + throw new Error('Status is not available for imported Key Value Store'); + } + }; + } + + readonly keyValueStoreArn: string; + readonly keyValueStoreId: string; + readonly keyValueStoreStatus: string; + + constructor(scope: Construct, id: string, props?: KeyValueStoreProps) { + super(scope, id, { + physicalName: props?.keyValueStoreName ?? Lazy.string({ + produce: () => Names.uniqueResourceName(this, { maxLength: 64 }), + }), + }); + + const resource = new CfnKeyValueStore(this, 'Resource', { + name: this.physicalName, + comment: props?.comment, + importSource: props?.source?._bind(this), + }); + + this.keyValueStoreArn = resource.attrArn; + this.keyValueStoreId = resource.attrId; + this.keyValueStoreStatus = resource.attrStatus; + } +} diff --git a/packages/aws-cdk-lib/aws-cloudfront/test/key-value-store-source.json b/packages/aws-cdk-lib/aws-cloudfront/test/key-value-store-source.json new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/packages/aws-cdk-lib/aws-cloudfront/test/key-value-store.test.ts b/packages/aws-cdk-lib/aws-cloudfront/test/key-value-store.test.ts new file mode 100644 index 0000000000000..f7c7221c23f5a --- /dev/null +++ b/packages/aws-cdk-lib/aws-cloudfront/test/key-value-store.test.ts @@ -0,0 +1,122 @@ +import * as path from 'node:path'; +import { Match, Template } from '../../assertions'; +import * as s3 from '../../aws-s3'; +import { App, Stack } from '../../core'; +import { KeyValueStore, ImportSource } from '../lib'; + +describe('Key Value Store', () => { + test('minimal example', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new KeyValueStore(stack, 'TestStore'); + + // THEN + Template.fromStack(stack).templateMatches({ + Resources: { + TestStore8BB973CF: { + Type: 'AWS::CloudFront::KeyValueStore', + Properties: { + Name: 'TestStore', + }, + }, + }, + }); + }); + + test('with name', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new KeyValueStore(stack, 'TestStore', { + keyValueStoreName: 'TestStoreName', + }); + + // THEN + Template.fromStack(stack).templateMatches({ + Resources: { + TestStore8BB973CF: { + Type: 'AWS::CloudFront::KeyValueStore', + Properties: { + Name: 'TestStoreName', + }, + }, + }, + }); + }); + + test('with comment', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new KeyValueStore(stack, 'TestStore', { + comment: 'Test comment', + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::KeyValueStore', { + Comment: 'Test comment', + }); + }); + + test('with code from S3 bucket', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + const bucket = s3.Bucket.fromBucketArn(stack, 'TestBucket', 'arn:aws:s3:::bucket'); + new KeyValueStore(stack, 'TestStore', { + source: ImportSource.fromBucket(bucket, 'test'), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::KeyValueStore', { + ImportSource: { + SourceArn: 'arn:aws:s3:::bucket/test', + SourceType: 'S3', + }, + }); + }); + + test('with code from local file', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new KeyValueStore(stack, 'TestStore', { + source: ImportSource.fromAsset(path.join(__dirname, 'key-value-store-source.json')), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::KeyValueStore', { + ImportSource: { + SourceArn: Match.anyValue(), + SourceType: 'S3', + }, + }); + }); + + test('imported resource throws error when missing ID', () => { + // GIVEN + const stack = new Stack(); + + // THEN + expect( + () => KeyValueStore.fromKeyValueStoreArn(stack, 'TestStore', 'arn:aws:cloudfront::123456789012:key-value-store'), + ).toThrow(/Invalid Key Value Store Arn:/); + }); + + test('imported resource throws error when accessing status', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + const store = KeyValueStore.fromKeyValueStoreArn(stack, 'TestStore', 'arn:aws:cloudfront::123456789012:key-value-store/id1'); + + // THEN + expect(() => store.keyValueStoreStatus).toThrow('Status is not available for imported Key Value Store'); + }); +});