From 4e02566fab0f6c6708c9ee766e2805adbb329f18 Mon Sep 17 00:00:00 2001 From: Richard Simpson Date: Wed, 8 Mar 2023 13:59:56 -0600 Subject: [PATCH 1/3] feat(ecr-assets): Support cache-from and cache-to flags (#24024) This adds the `--cache-from` and `--cache-to` flag options. --- ### All Submissions: * [x] Have you followed the guidelines in our [Contributing guide?](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md?rgh-link-date=2022-12-09T23%3A48%3A14Z) ### Adding new Construct Runtime Dependencies: * [ ] This PR adds new construct runtime dependencies following the process described [here](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md/?rgh-link-date=2022-12-09T23%3A48%3A14Z#adding-construct-runtime-dependencies) ### New Features * [x] Have you added the new feature to an [integration test](https://github.com/aws/aws-cdk/blob/main/INTEGRATION_TESTS.md?rgh-link-date=2022-12-09T23%3A48%3A14Z)? * [x] Did you use `yarn integ` to deploy the infrastructure and generate the snapshot (i.e. `yarn integ` without `--dry-run`)? _By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license_ --- packages/@aws-cdk/aws-ecr-assets/README.md | 12 +++ .../aws-ecr-assets/lib/image-asset.ts | 54 ++++++++++++ .../test/build-image-cache.test.ts | 88 +++++++++++++++++++ .../integ-assets-docker.template.json | 5 ++ .../integ.assets-docker.js.snapshot/tree.json | 4 +- .../test/integ.assets-docker.ts | 7 ++ .../lib/assets/docker-image-asset.ts | 38 ++++++++ .../lib/cloud-assembly/metadata-schema.ts | 38 ++++++++ .../schema/assets.schema.json | 31 +++++++ .../schema/cloud-assembly.schema.json | 31 +++++++ .../schema/cloud-assembly.version.json | 4 +- packages/@aws-cdk/core/lib/assets.ts | 34 +++++++ .../asset-manifest-builder.ts | 2 + .../core/lib/stack-synthesizers/legacy.ts | 2 + packages/@aws-cdk/cx-api/lib/assets.ts | 2 + packages/cdk-assets/lib/private/docker.ts | 17 ++++ .../lib/private/handlers/container-images.ts | 2 + 17 files changed, 366 insertions(+), 5 deletions(-) create mode 100644 packages/@aws-cdk/aws-ecr-assets/test/build-image-cache.test.ts diff --git a/packages/@aws-cdk/aws-ecr-assets/README.md b/packages/@aws-cdk/aws-ecr-assets/README.md index 32059529f1ee1..26c3963afbe05 100644 --- a/packages/@aws-cdk/aws-ecr-assets/README.md +++ b/packages/@aws-cdk/aws-ecr-assets/README.md @@ -121,6 +121,18 @@ const asset = new DockerImageAsset(this, 'MyBuildImage', { }) ``` +You can optionally pass cache from and cache to options to cache images: + +```ts +import { DockerImageAsset, Platform } from '@aws-cdk/aws-ecr-assets'; + +const asset = new DockerImageAsset(this, 'MyBuildImage', { + directory: path.join(__dirname, 'my-image'), + cacheFrom: [{ type: 'registry', params: { ref: 'ghcr.io/myorg/myimage:cache' }}], + cacheTo: { type: 'registry', params: { ref: 'ghcr.io/myorg/myimage:cache', mode: 'max', compression: 'zstd' }} +}) +``` + ## Images from Tarball Images are loaded from a local tarball, uploaded to ECR by the CDK toolkit and/or your app's CI-CD pipeline, and can be diff --git a/packages/@aws-cdk/aws-ecr-assets/lib/image-asset.ts b/packages/@aws-cdk/aws-ecr-assets/lib/image-asset.ts index da03e18f80046..49e93ff47bbfb 100644 --- a/packages/@aws-cdk/aws-ecr-assets/lib/image-asset.ts +++ b/packages/@aws-cdk/aws-ecr-assets/lib/image-asset.ts @@ -148,6 +148,28 @@ export interface DockerImageAssetInvalidationOptions { readonly outputs?: boolean; } +/** + * Options for configuring the Docker cache backend + */ +export interface DockerCacheOption { + /** + * The type of cache to use. + * Refer to https://docs.docker.com/build/cache/backends/ for full list of backends. + * @default - unspecified + * + * @example 'registry' + */ + readonly type: string; + /** + * Any parameters to pass into the docker cache backend configuration. + * Refer to https://docs.docker.com/build/cache/backends/ for cache backend configuration. + * @default {} No options provided + * + * @example { ref: `12345678.dkr.ecr.us-west-2.amazonaws.com/cache:${branch}`, mode: "max" } + */ + readonly params?: { [key: string]: string }; +} + /** * Options for DockerImageAsset */ @@ -236,6 +258,22 @@ export interface DockerImageAssetOptions extends FingerprintOptions, FileFingerp * @see https://docs.docker.com/engine/reference/commandline/build/#custom-build-outputs */ readonly outputs?: string[]; + + /** + * Cache from options to pass to the `docker build` command. + * + * @default - no cache from options are passed to the build command + * @see https://docs.docker.com/build/cache/backends/ + */ + readonly cacheFrom?: DockerCacheOption[]; + + /** + * Cache to options to pass to the `docker build` command. + * + * @default - no cache to options are passed to the build command + * @see https://docs.docker.com/build/cache/backends/ + */ + readonly cacheTo?: DockerCacheOption; } /** @@ -316,6 +354,16 @@ export class DockerImageAsset extends Construct implements IAsset { */ private readonly dockerOutputs?: string[]; + /** + * Cache from options to pass to the `docker build` command. + */ + private readonly dockerCacheFrom?: DockerCacheOption[]; + + /** + * Cache to options to pass to the `docker build` command. + */ + private readonly dockerCacheTo?: DockerCacheOption; + /** * Docker target to build to */ @@ -407,6 +455,8 @@ export class DockerImageAsset extends Construct implements IAsset { this.dockerBuildSecrets = props.buildSecrets; this.dockerBuildTarget = props.target; this.dockerOutputs = props.outputs; + this.dockerCacheFrom = props.cacheFrom; + this.dockerCacheTo = props.cacheTo; const location = stack.synthesizer.addDockerImageAsset({ directoryName: this.assetPath, @@ -418,6 +468,8 @@ export class DockerImageAsset extends Construct implements IAsset { networkMode: props.networkMode?.mode, platform: props.platform?.platform, dockerOutputs: this.dockerOutputs, + dockerCacheFrom: this.dockerCacheFrom, + dockerCacheTo: this.dockerCacheTo, }); this.repository = ecr.Repository.fromRepositoryName(this, 'Repository', location.repositoryName); @@ -456,6 +508,8 @@ export class DockerImageAsset extends Construct implements IAsset { resource.cfnOptions.metadata[cxapi.ASSET_RESOURCE_METADATA_DOCKER_BUILD_TARGET_KEY] = this.dockerBuildTarget; resource.cfnOptions.metadata[cxapi.ASSET_RESOURCE_METADATA_PROPERTY_KEY] = resourceProperty; resource.cfnOptions.metadata[cxapi.ASSET_RESOURCE_METADATA_DOCKER_OUTPUTS_KEY] = this.dockerOutputs; + resource.cfnOptions.metadata[cxapi.ASSET_RESOURCE_METADATA_DOCKER_CACHE_FROM_KEY] = this.dockerCacheFrom; + resource.cfnOptions.metadata[cxapi.ASSET_RESOURCE_METADATA_DOCKER_CACHE_TO_KEY] = this.dockerCacheTo; } } diff --git a/packages/@aws-cdk/aws-ecr-assets/test/build-image-cache.test.ts b/packages/@aws-cdk/aws-ecr-assets/test/build-image-cache.test.ts new file mode 100644 index 0000000000000..e50c41c4c6f95 --- /dev/null +++ b/packages/@aws-cdk/aws-ecr-assets/test/build-image-cache.test.ts @@ -0,0 +1,88 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { AssetManifest } from '@aws-cdk/cloud-assembly-schema'; +import { App, Stack } from '@aws-cdk/core'; +import { AssetManifestArtifact, CloudArtifact, CloudAssembly } from '@aws-cdk/cx-api'; +import { DockerImageAsset } from '../lib'; + +describe('build cache', () => { + test('manifest contains cache from options ', () => { + // GIVEN + const app = new App(); + const stack = new Stack(app); + const asset = new DockerImageAsset(stack, 'DockerImage6', { + directory: path.join(__dirname, 'demo-image'), + cacheFrom: [{ type: 'registry', params: { image: 'foo' } }], + }); + + // WHEN + const asm = app.synth(); + + // THEN + const manifestArtifact = getAssetManifest(asm); + const manifest = readAssetManifest(manifestArtifact); + + expect(Object.keys(manifest.dockerImages ?? {}).length).toBe(1); + expect(manifest.dockerImages?.[asset.assetHash]?.source.cacheFrom?.length).toBe(1); + expect(manifest.dockerImages?.[asset.assetHash]?.source.cacheFrom?.[0]).toStrictEqual({ + type: 'registry', + params: { image: 'foo' }, + }); + }); + test('manifest contains cache to options ', () => { + // GIVEN + const app = new App(); + const stack = new Stack(app); + const asset = new DockerImageAsset(stack, 'DockerImage6', { + directory: path.join(__dirname, 'demo-image'), + cacheTo: { type: 'inline' }, + }); + + // WHEN + const asm = app.synth(); + + // THEN + const manifestArtifact = getAssetManifest(asm); + const manifest = readAssetManifest(manifestArtifact); + + expect(Object.keys(manifest.dockerImages ?? {}).length).toBe(1); + expect(manifest.dockerImages?.[asset.assetHash]?.source.cacheTo).toStrictEqual({ + type: 'inline', + }); + }); + + test('manifest does not contain options when not specified', () => { + // GIVEN + const app = new App(); + const stack = new Stack(app); + const asset = new DockerImageAsset(stack, 'DockerImage6', { + directory: path.join(__dirname, 'demo-image'), + }); + + // WHEN + const asm = app.synth(); + + // THEN + const manifestArtifact = getAssetManifest(asm); + const manifest = readAssetManifest(manifestArtifact); + expect(Object.keys(manifest.dockerImages ?? {}).length).toBe(1); + expect(manifest.dockerImages?.[asset.assetHash]?.source.cacheFrom).toBeUndefined(); + expect(manifest.dockerImages?.[asset.assetHash]?.source.cacheTo).toBeUndefined(); + }); +}); + +function isAssetManifest(x: CloudArtifact): x is AssetManifestArtifact { + return x instanceof AssetManifestArtifact; +} + +function getAssetManifest(asm: CloudAssembly): AssetManifestArtifact { + const manifestArtifact = asm.artifacts.filter(isAssetManifest)[0]; + if (!manifestArtifact) { + throw new Error('no asset manifest in assembly'); + } + return manifestArtifact; +} + +function readAssetManifest(manifestArtifact: AssetManifestArtifact): AssetManifest { + return JSON.parse(fs.readFileSync(manifestArtifact.file, { encoding: 'utf-8' })); +} diff --git a/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.js.snapshot/integ-assets-docker.template.json b/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.js.snapshot/integ-assets-docker.template.json index 8c7c033450117..ac306d4d02d70 100644 --- a/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.js.snapshot/integ-assets-docker.template.json +++ b/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.js.snapshot/integ-assets-docker.template.json @@ -81,6 +81,11 @@ "Value": { "Fn::Sub": "${AWS::AccountId}.dkr.ecr.${AWS::Region}.${AWS::URLSuffix}/cdk-hnb659fds-container-assets-${AWS::AccountId}-${AWS::Region}:60dea2e16e94d1977b92fe03fa7085fea446233f1fe499702b69593438baa59f" } + }, + "ImageUri6": { + "Value": { + "Fn::Sub": "${AWS::AccountId}.dkr.ecr.${AWS::Region}.${AWS::URLSuffix}/cdk-hnb659fds-container-assets-${AWS::AccountId}-${AWS::Region}:0a3355be12051c9984bf2b0b2bba4e6ea535968e5b6e7396449701732fe5ed14" + } } }, "Parameters": { diff --git a/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.js.snapshot/tree.json b/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.js.snapshot/tree.json index 32988bdc52723..06c8f34bf353e 100644 --- a/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.js.snapshot/tree.json +++ b/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.js.snapshot/tree.json @@ -262,8 +262,8 @@ "version": "0.0.0" } }, - "ImageUri5": { - "id": "ImageUri5", + "ImageUri4": { + "id": "ImageUri4", "path": "integ-assets-docker/ImageUri5", "constructInfo": { "fqn": "@aws-cdk/core.CfnOutput", diff --git a/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.ts b/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.ts index 702b83fe011a5..65a6f266d5a35 100644 --- a/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.ts +++ b/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.ts @@ -31,17 +31,24 @@ const asset5 = new assets.DockerImageAsset(stack, 'DockerImage5', { }, }); +const asset6 = new assets.DockerImageAsset(stack, 'DockerImage6', { + directory: path.join(__dirname, 'demo-image'), + cacheTo: { type: 'inline' }, +}); + const user = new iam.User(stack, 'MyUser'); asset.repository.grantPull(user); asset2.repository.grantPull(user); asset3.repository.grantPull(user); asset4.repository.grantPull(user); asset5.repository.grantPull(user); +asset6.repository.grantPull(user); new cdk.CfnOutput(stack, 'ImageUri', { value: asset.imageUri }); new cdk.CfnOutput(stack, 'ImageUri2', { value: asset2.imageUri }); new cdk.CfnOutput(stack, 'ImageUri3', { value: asset3.imageUri }); new cdk.CfnOutput(stack, 'ImageUri4', { value: asset4.imageUri }); new cdk.CfnOutput(stack, 'ImageUri5', { value: asset5.imageUri }); +new cdk.CfnOutput(stack, 'ImageUri6', { value: asset6.imageUri }); app.synth(); diff --git a/packages/@aws-cdk/cloud-assembly-schema/lib/assets/docker-image-asset.ts b/packages/@aws-cdk/cloud-assembly-schema/lib/assets/docker-image-asset.ts index 809ef1c9f91f4..6224af4fb6c77 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/lib/assets/docker-image-asset.ts +++ b/packages/@aws-cdk/cloud-assembly-schema/lib/assets/docker-image-asset.ts @@ -97,6 +97,22 @@ export interface DockerImageSource { * @see https://docs.docker.com/engine/reference/commandline/build/#custom-build-outputs */ readonly dockerOutputs?: string[]; + + /** + * Cache from options to pass to the `docker build` command. + * + * @default - no cache from options are passed to the build command + * @see https://docs.docker.com/build/cache/backends/ + */ + readonly cacheFrom?: DockerCacheOption[]; + + /** + * Cache to options to pass to the `docker build` command. + * + * @default - no cache to options are passed to the build command + * @see https://docs.docker.com/build/cache/backends/ + */ + readonly cacheTo?: DockerCacheOption; } /** @@ -113,3 +129,25 @@ export interface DockerImageDestination extends AwsDestination { */ readonly imageTag: string; } + +/** + * Options for configuring the Docker cache backend + */ +export interface DockerCacheOption { + /** + * The type of cache to use. + * Refer to https://docs.docker.com/build/cache/backends/ for full list of backends. + * @default - unspecified + * + * @example 'registry' + */ + readonly type: string; + /** + * Any parameters to pass into the docker cache backend configuration. + * Refer to https://docs.docker.com/build/cache/backends/ for cache backend configuration. + * @default {} No options provided + * + * @example { ref: `12345678.dkr.ecr.us-west-2.amazonaws.com/cache:${branch}`, mode: "max" } + */ + readonly params?: { [key: string]: string }; +} \ No newline at end of file diff --git a/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/metadata-schema.ts b/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/metadata-schema.ts index c3d4ac127a46f..bcc67298ff308 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/metadata-schema.ts +++ b/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/metadata-schema.ts @@ -71,6 +71,28 @@ export interface Tag { readonly value: string } +/** + * Options for configuring the Docker cache backend + */ +export interface ContainerImageAssetCacheOption { + /** + * The type of cache to use. + * Refer to https://docs.docker.com/build/cache/backends/ for full list of backends. + * @default - unspecified + * + * @example 'registry' + */ + readonly type: string; + /** + * Any parameters to pass into the docker cache backend configuration. + * Refer to https://docs.docker.com/build/cache/backends/ for cache backend configuration. + * @default {} No options provided + * + * @example { ref: `12345678.dkr.ecr.us-west-2.amazonaws.com/cache:${branch}`, mode: "max" } + */ + readonly params?: { [key: string]: string }; +} + /** * Metadata Entry spec for container images. */ @@ -160,6 +182,22 @@ export interface ContainerImageAssetMetadataEntry extends BaseAssetMetadataEntry * @see https://docs.docker.com/engine/reference/commandline/build/#custom-build-outputs */ readonly outputs?: string[]; + + /** + * Cache from options to pass to the `docker build` command. + * + * @default - no cache from options are passed to the build command + * @see https://docs.docker.com/build/cache/backends/ + */ + readonly cacheFrom?: ContainerImageAssetCacheOption[]; + + /** + * Cache to options to pass to the `docker build` command. + * + * @default - no cache to options are passed to the build command + * @see https://docs.docker.com/build/cache/backends/ + */ + readonly cacheTo?: ContainerImageAssetCacheOption; } /** diff --git a/packages/@aws-cdk/cloud-assembly-schema/schema/assets.schema.json b/packages/@aws-cdk/cloud-assembly-schema/schema/assets.schema.json index 28dea7efbf357..6e7f02037f744 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/schema/assets.schema.json +++ b/packages/@aws-cdk/cloud-assembly-schema/schema/assets.schema.json @@ -176,9 +176,40 @@ "items": { "type": "string" } + }, + "cacheFrom": { + "description": "Cache from options to pass to the `docker build` command. (Default - no cache from options are passed to the build command)", + "type": "array", + "items": { + "$ref": "#/definitions/DockerCacheOption" + } + }, + "cacheTo": { + "description": "Cache to options to pass to the `docker build` command. (Default - no cache to options are passed to the build command)", + "$ref": "#/definitions/DockerCacheOption" } } }, + "DockerCacheOption": { + "description": "Options for configuring the Docker cache backend", + "type": "object", + "properties": { + "type": { + "description": "The type of cache to use.\nRefer to https://docs.docker.com/build/cache/backends/ for full list of backends. (Default - unspecified)", + "type": "string" + }, + "params": { + "description": "Any parameters to pass into the docker cache backend configuration.\nRefer to https://docs.docker.com/build/cache/backends/ for cache backend configuration. (Default {} No options provided)", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "required": [ + "type" + ] + }, "DockerImageDestination": { "description": "Where to publish docker images", "type": "object", diff --git a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json index 92eb946bc9149..12d263bfdea2f 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json +++ b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json @@ -248,6 +248,17 @@ "type": "string" } }, + "cacheFrom": { + "description": "Cache from options to pass to the `docker build` command. (Default - no cache from options are passed to the build command)", + "type": "array", + "items": { + "$ref": "#/definitions/ContainerImageAssetCacheOption" + } + }, + "cacheTo": { + "description": "Cache to options to pass to the `docker build` command. (Default - no cache to options are passed to the build command)", + "$ref": "#/definitions/ContainerImageAssetCacheOption" + }, "id": { "description": "Logical identifier for the asset", "type": "string" @@ -268,6 +279,26 @@ "sourceHash" ] }, + "ContainerImageAssetCacheOption": { + "description": "Options for configuring the Docker cache backend", + "type": "object", + "properties": { + "type": { + "description": "The type of cache to use.\nRefer to https://docs.docker.com/build/cache/backends/ for full list of backends. (Default - unspecified)", + "type": "string" + }, + "params": { + "description": "Any parameters to pass into the docker cache backend configuration.\nRefer to https://docs.docker.com/build/cache/backends/ for cache backend configuration. (Default {} No options provided)", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "required": [ + "type" + ] + }, "Tag": { "description": "Metadata Entry spec for stack tag.", "type": "object", diff --git a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.version.json b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.version.json index 7233abc4c3527..0d5aff521d3a2 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.version.json +++ b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.version.json @@ -1,3 +1 @@ -{ - "version": "30.1.0" -} +{"version":"31.0.0"} diff --git a/packages/@aws-cdk/core/lib/assets.ts b/packages/@aws-cdk/core/lib/assets.ts index 10e1a61e7e5d0..72f9429f2afdb 100644 --- a/packages/@aws-cdk/core/lib/assets.ts +++ b/packages/@aws-cdk/core/lib/assets.ts @@ -242,6 +242,18 @@ export interface DockerImageAssetSource { */ readonly dockerOutputs?: string[]; + /** + * Cache from options to pass to the `docker build` command. + * @default - no cache from args are passed + */ + readonly dockerCacheFrom?: DockerCacheOption[]; + + /** + * Cache to options to pass to the `docker build` command. + * @default - no cache to args are passed + */ + readonly dockerCacheTo?: DockerCacheOption; + } /** @@ -347,3 +359,25 @@ export interface DockerImageAssetLocation { */ readonly imageTag?: string; } + +/** + * Options for configuring the Docker cache backend + */ +export interface DockerCacheOption { + /** + * The type of cache to use. + * Refer to https://docs.docker.com/build/cache/backends/ for full list of backends. + * @default - unspecified + * + * @example 'registry' + */ + readonly type: string; + /** + * Any parameters to pass into the docker cache backend configuration. + * Refer to https://docs.docker.com/build/cache/backends/ for cache backend configuration. + * @default {} No options provided + * + * @example { ref: `12345678.dkr.ecr.us-west-2.amazonaws.com/cache:${branch}`, mode: "max" } + */ + readonly params?: { [key: string]: string }; +} \ No newline at end of file diff --git a/packages/@aws-cdk/core/lib/stack-synthesizers/asset-manifest-builder.ts b/packages/@aws-cdk/core/lib/stack-synthesizers/asset-manifest-builder.ts index 0f923dfaef659..109ddc4c00c1f 100644 --- a/packages/@aws-cdk/core/lib/stack-synthesizers/asset-manifest-builder.ts +++ b/packages/@aws-cdk/core/lib/stack-synthesizers/asset-manifest-builder.ts @@ -71,6 +71,8 @@ export class AssetManifestBuilder { networkMode: asset.networkMode, platform: asset.platform, dockerOutputs: asset.dockerOutputs, + cacheFrom: asset.dockerCacheFrom, + cacheTo: asset.dockerCacheTo, }, { repositoryName: target.repositoryName, imageTag, diff --git a/packages/@aws-cdk/core/lib/stack-synthesizers/legacy.ts b/packages/@aws-cdk/core/lib/stack-synthesizers/legacy.ts index bce9dac8ced65..70dc22333bf84 100644 --- a/packages/@aws-cdk/core/lib/stack-synthesizers/legacy.ts +++ b/packages/@aws-cdk/core/lib/stack-synthesizers/legacy.ts @@ -148,6 +148,8 @@ export class LegacyStackSynthesizer extends StackSynthesizer implements IReusabl networkMode: asset.networkMode, platform: asset.platform, outputs: asset.dockerOutputs, + cacheFrom: asset.dockerCacheFrom, + cacheTo: asset.dockerCacheTo, }; this.boundStack.node.addMetadata(cxschema.ArtifactMetadataEntryType.ASSET, metadata); diff --git a/packages/@aws-cdk/cx-api/lib/assets.ts b/packages/@aws-cdk/cx-api/lib/assets.ts index 3cc8312f646d4..42461895828be 100644 --- a/packages/@aws-cdk/cx-api/lib/assets.ts +++ b/packages/@aws-cdk/cx-api/lib/assets.ts @@ -17,6 +17,8 @@ export const ASSET_RESOURCE_METADATA_DOCKER_BUILD_TARGET_KEY = 'aws:asset:docker export const ASSET_RESOURCE_METADATA_PROPERTY_KEY = 'aws:asset:property'; export const ASSET_RESOURCE_METADATA_IS_BUNDLED_KEY = 'aws:asset:is-bundled'; export const ASSET_RESOURCE_METADATA_DOCKER_OUTPUTS_KEY = 'aws:asset:docker-outputs'; +export const ASSET_RESOURCE_METADATA_DOCKER_CACHE_FROM_KEY = 'aws:asset:docker-cache-from'; +export const ASSET_RESOURCE_METADATA_DOCKER_CACHE_TO_KEY = 'aws:asset:docker-cache-to'; /** * Separator string that separates the prefix separator from the object key separator. diff --git a/packages/cdk-assets/lib/private/docker.ts b/packages/cdk-assets/lib/private/docker.ts index 509e15cbed22f..f5a6d675aa634 100644 --- a/packages/cdk-assets/lib/private/docker.ts +++ b/packages/cdk-assets/lib/private/docker.ts @@ -19,6 +19,8 @@ interface BuildOptions { readonly networkMode?: string; readonly platform?: string; readonly outputs?: string[]; + readonly cacheFrom?: DockerCacheOption[]; + readonly cacheTo?: DockerCacheOption; } export interface DockerCredentialsConfig { @@ -36,6 +38,11 @@ enum InspectImageErrorCode { Podman = 125 } +export interface DockerCacheOption { + readonly type: string; + readonly params?: { [key: string]: string }; +} + export class Docker { private configDir: string | undefined = undefined; @@ -90,6 +97,8 @@ export class Docker { ...options.networkMode ? ['--network', options.networkMode] : [], ...options.platform ? ['--platform', options.platform] : [], ...options.outputs ? options.outputs.map(output => [`--output=${output}`]) : [], + ...options.cacheFrom ? options.cacheFrom.map(cacheFrom => this.cacheOptionToFlag(cacheFrom)) : [], + ...options.cacheTo ? [this.cacheOptionToFlag(options.cacheTo)] : [], '.', ]; await this.execute(buildCommand, { cwd: options.directory }); @@ -179,6 +188,14 @@ export class Docker { throw e; } } + + private cacheOptionToFlag(option: DockerCacheOption): string { + let flag = `type=${option.type}`; + if (option.params) { + flag += ',' + Object.entries(option.params).map(([k, v]) => `${k}=${v}`).join(','); + } + return flag; + } } export interface DockerFactoryOptions { diff --git a/packages/cdk-assets/lib/private/handlers/container-images.ts b/packages/cdk-assets/lib/private/handlers/container-images.ts index 811d02e1a8d01..e25b12d368a01 100644 --- a/packages/cdk-assets/lib/private/handlers/container-images.ts +++ b/packages/cdk-assets/lib/private/handlers/container-images.ts @@ -173,6 +173,8 @@ class ContainerImageBuilder { networkMode: source.networkMode, platform: source.platform, outputs: source.dockerOutputs, + cacheFrom: source.cacheFrom, + cacheTo: source.cacheTo, }); } From d451b3014a1d39e0a6ea18c2ec79a547b187adc5 Mon Sep 17 00:00:00 2001 From: Richard Simpson Date: Wed, 8 Mar 2023 15:04:04 -0600 Subject: [PATCH 2/3] fix(ecr-assets): prefix cache arguments correctly (#24524) Follow up to #24024, fixes an issue where cache args were not correctly prefixed and adds additional testing. Apologies for the second PR, I realized there was an issue literally as the auto-merge happened! ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/cdk-assets/lib/private/docker.ts | 4 +- .../cdk-assets/test/docker-images.test.ts | 139 ++++++++++++++++++ 2 files changed, 141 insertions(+), 2 deletions(-) diff --git a/packages/cdk-assets/lib/private/docker.ts b/packages/cdk-assets/lib/private/docker.ts index f5a6d675aa634..eea1bb7b3a093 100644 --- a/packages/cdk-assets/lib/private/docker.ts +++ b/packages/cdk-assets/lib/private/docker.ts @@ -97,8 +97,8 @@ export class Docker { ...options.networkMode ? ['--network', options.networkMode] : [], ...options.platform ? ['--platform', options.platform] : [], ...options.outputs ? options.outputs.map(output => [`--output=${output}`]) : [], - ...options.cacheFrom ? options.cacheFrom.map(cacheFrom => this.cacheOptionToFlag(cacheFrom)) : [], - ...options.cacheTo ? [this.cacheOptionToFlag(options.cacheTo)] : [], + ...options.cacheFrom ? [...options.cacheFrom.map(cacheFrom => ['--cache-from', this.cacheOptionToFlag(cacheFrom)]).flat()] : [], + ...options.cacheTo ? ['--cache-to', this.cacheOptionToFlag(options.cacheTo)] : [], '.', ]; await this.execute(buildCommand, { cwd: options.directory }); diff --git a/packages/cdk-assets/test/docker-images.test.ts b/packages/cdk-assets/test/docker-images.test.ts index 7b7d94b54362f..688828a68c021 100644 --- a/packages/cdk-assets/test/docker-images.test.ts +++ b/packages/cdk-assets/test/docker-images.test.ts @@ -144,6 +144,68 @@ beforeEach(() => { }, }, }), + '/cache/cdk.out/assets.json': JSON.stringify({ + version: Manifest.version(), + dockerImages: { + theAsset: { + source: { + directory: 'dockerdir', + cacheFrom: [{ type: 'registry', params: { ref: 'abcdef' } }], + cacheTo: { type: 'inline' }, + }, + destinations: { + theDestination: { + region: 'us-north-50', + assumeRoleArn: 'arn:aws:role', + repositoryName: 'repo', + imageTag: 'nopqr', + }, + }, + }, + }, + }), + '/cache-from-multiple/cdk.out/assets.json': JSON.stringify({ + version: Manifest.version(), + dockerImages: { + theAsset: { + source: { + directory: 'dockerdir', + cacheFrom: [ + { type: 'registry', params: { ref: 'cache:ref' } }, + { type: 'registry', params: { ref: 'cache:main' } }, + { type: 'gha' }, + ], + }, + destinations: { + theDestination: { + region: 'us-north-50', + assumeRoleArn: 'arn:aws:role', + repositoryName: 'repo', + imageTag: 'nopqr', + }, + }, + }, + }, + }), + '/cache-to-complex/cdk.out/assets.json': JSON.stringify({ + version: Manifest.version(), + dockerImages: { + theAsset: { + source: { + directory: 'dockerdir', + cacheTo: { type: 'registry', params: { ref: 'cache:main', mode: 'max', compression: 'zstd' } }, + }, + destinations: { + theDestination: { + region: 'us-north-50', + assumeRoleArn: 'arn:aws:role', + repositoryName: 'repo', + imageTag: 'nopqr', + }, + }, + }, + }, + }), '/platform-arm64/cdk.out/dockerdir/Dockerfile': 'FROM scratch', }); @@ -284,6 +346,83 @@ describe('with a complete manifest', () => { expectAllSpawns(); expect(true).toBeTruthy(); // Expect no exception, satisfy linter }); + + test('build with cache option', async () => { + pub = new AssetPublishing(AssetManifest.fromPath('/cache/cdk.out'), { aws }); + const defaultNetworkDockerpath = '/cache/cdk.out/dockerdir'; + aws.mockEcr.describeImages = mockedApiFailure('ImageNotFoundException', 'File does not exist'); + aws.mockEcr.getAuthorizationToken = mockedApiResult({ + authorizationData: [ + { authorizationToken: 'dXNlcjpwYXNz', proxyEndpoint: 'https://proxy.com/' }, + ], + }); + + const expectAllSpawns = mockSpawn( + { commandLine: ['docker', 'login', '--username', 'user', '--password-stdin', 'https://proxy.com/'] }, + { commandLine: ['docker', 'inspect', 'cdkasset-theasset'], exitCode: 1 }, + { commandLine: ['docker', 'build', '--tag', 'cdkasset-theasset', '--cache-from', 'type=registry,ref=abcdef', '--cache-to', 'type=inline', '.'], cwd: defaultNetworkDockerpath }, + { commandLine: ['docker', 'tag', 'cdkasset-theasset', '12345.amazonaws.com/repo:nopqr'] }, + { commandLine: ['docker', 'push', '12345.amazonaws.com/repo:nopqr'] }, + ); + + await pub.publish(); + + expectAllSpawns(); + expect(true).toBeTruthy(); // Expect no exception, satisfy linter + }); + + test('build with multiple cache from option', async () => { + pub = new AssetPublishing(AssetManifest.fromPath('/cache-from-multiple/cdk.out'), { aws }); + const defaultNetworkDockerpath = '/cache-from-multiple/cdk.out/dockerdir'; + aws.mockEcr.describeImages = mockedApiFailure('ImageNotFoundException', 'File does not exist'); + aws.mockEcr.getAuthorizationToken = mockedApiResult({ + authorizationData: [ + { authorizationToken: 'dXNlcjpwYXNz', proxyEndpoint: 'https://proxy.com/' }, + ], + }); + + const expectAllSpawns = mockSpawn( + { commandLine: ['docker', 'login', '--username', 'user', '--password-stdin', 'https://proxy.com/'] }, + { commandLine: ['docker', 'inspect', 'cdkasset-theasset'], exitCode: 1 }, + { + commandLine: [ + 'docker', 'build', '--tag', 'cdkasset-theasset', '--cache-from', 'type=registry,ref=cache:ref', '--cache-from', 'type=registry,ref=cache:main', '--cache-from', 'type=gha', '.', + ], + cwd: defaultNetworkDockerpath, + }, + { commandLine: ['docker', 'tag', 'cdkasset-theasset', '12345.amazonaws.com/repo:nopqr'] }, + { commandLine: ['docker', 'push', '12345.amazonaws.com/repo:nopqr'] }, + ); + + await pub.publish(); + + expectAllSpawns(); + expect(true).toBeTruthy(); // Expect no exception, satisfy linter + }); + + test('build with cache to complex option', async () => { + pub = new AssetPublishing(AssetManifest.fromPath('/cache-to-complex/cdk.out'), { aws }); + const defaultNetworkDockerpath = '/cache-to-complex/cdk.out/dockerdir'; + aws.mockEcr.describeImages = mockedApiFailure('ImageNotFoundException', 'File does not exist'); + aws.mockEcr.getAuthorizationToken = mockedApiResult({ + authorizationData: [ + { authorizationToken: 'dXNlcjpwYXNz', proxyEndpoint: 'https://proxy.com/' }, + ], + }); + + const expectAllSpawns = mockSpawn( + { commandLine: ['docker', 'login', '--username', 'user', '--password-stdin', 'https://proxy.com/'] }, + { commandLine: ['docker', 'inspect', 'cdkasset-theasset'], exitCode: 1 }, + { commandLine: ['docker', 'build', '--tag', 'cdkasset-theasset', '--cache-to', 'type=registry,ref=cache:main,mode=max,compression=zstd', '.'], cwd: defaultNetworkDockerpath }, + { commandLine: ['docker', 'tag', 'cdkasset-theasset', '12345.amazonaws.com/repo:nopqr'] }, + { commandLine: ['docker', 'push', '12345.amazonaws.com/repo:nopqr'] }, + ); + + await pub.publish(); + + expectAllSpawns(); + expect(true).toBeTruthy(); // Expect no exception, satisfy linter + }); }); describe('external assets', () => { From 7c7ad6d18bd0d48a30858c1964d27d8a02b274ae Mon Sep 17 00:00:00 2001 From: Mitch Lloyd Date: Wed, 8 Mar 2023 17:02:50 -0500 Subject: [PATCH 3/3] feat(kinesisanalytics-flink): VPC support for Flink applications (#24442) The Kinesis Data Analytics team added support for [deploying Flink applications in a VPC](https://docs.aws.amazon.com/kinesisanalytics/latest/java/vpc.html). This feature is also available in CloudFormation. Deploying Flink in a VPC allows the application to reach services like Redis and other databases. This PR adds support for configuring `VpcConfigurations` with `vpcSubets` (subnetSelection) and securityGroups following similar patterns for resources like `lambda.Function` that support optional deployment in a VPC. Some design decisions: - Name the subnet selection prop `vpcSubnets`. Some resources call the subnet selection property `subnetSelection` but `vpcSubnets` seemed more popular and is used by the Lambda and ECS modules. - Only support passing an array of security groups. Some resources support adding a single SecurityGroup or SecurityGroupId properties but it appears this [usage is deprecated](https://github.com/aws/aws-cdk/blob/main/packages/%40aws-cdk/aws-lambda/lib/function.ts#L170) in favor of always passing an array of SecurityGroups. - I added a `fromApplicationAttributes` factory that includes `securityGroups`. This seemed like an appropriate time to add this method given there was another property to pass besides ARN and name. However I didn't go down the path of including a role in `fromApplicationAttributes` yet in order to keep this PR focused. - ~~I thought about adding a section to the readme about using VPCs, but I didn't notice a section like that in the [Lambda readme](https://github.com/aws/aws-cdk/blob/main/packages/%40aws-cdk/aws-lambda/README.md) for instance. My current thinking is that the conventions for VPC-bound resources are so consistent it probably doesn't warrant more documentation~~ @aws-cdk-automation did not buy this rational. I'd like to follow-up with a PR to move code into more files as the > 1K lines of code in `application.ts` is getting a little unweildy. I wanted to avoid moving code around in this PR to make it easier to review. Closes #21104. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../aws-kinesisanalytics-flink/README.md | 16 +- .../lib/application.ts | 231 +++- .../lib/private/validation.ts | 17 + .../aws-kinesisanalytics-flink/package.json | 3 + .../rosetta/default.ts-fixture | 3 +- .../test/application.test.ts | 220 ++- .../FlinkAppTest.assets.json | 32 + .../FlinkAppTest.template.json | 720 ++++++++++ ...efaultTestDeployAssert06A9965C.assets.json | 19 + ...aultTestDeployAssert06A9965C.template.json | 36 + .../WordCount.jar | Bin 0 -> 15192 bytes .../integ.vpc-application.js.snapshot/cdk.out | 1 + .../integ.json | 12 + .../manifest.json | 285 ++++ .../tree.json | 1175 +++++++++++++++++ .../test/integ.vpc-application.ts | 19 + 16 files changed, 2722 insertions(+), 67 deletions(-) create mode 100644 packages/@aws-cdk/aws-kinesisanalytics-flink/test/integ.vpc-application.js.snapshot/FlinkAppTest.assets.json create mode 100644 packages/@aws-cdk/aws-kinesisanalytics-flink/test/integ.vpc-application.js.snapshot/FlinkAppTest.template.json create mode 100644 packages/@aws-cdk/aws-kinesisanalytics-flink/test/integ.vpc-application.js.snapshot/VpcTestDefaultTestDeployAssert06A9965C.assets.json create mode 100644 packages/@aws-cdk/aws-kinesisanalytics-flink/test/integ.vpc-application.js.snapshot/VpcTestDefaultTestDeployAssert06A9965C.template.json create mode 100644 packages/@aws-cdk/aws-kinesisanalytics-flink/test/integ.vpc-application.js.snapshot/asset.8be9e0b5f53d41e9a3b1d51c9572c65f24f8170a7188d0ed57fb7d571de4d577/WordCount.jar create mode 100644 packages/@aws-cdk/aws-kinesisanalytics-flink/test/integ.vpc-application.js.snapshot/cdk.out create mode 100644 packages/@aws-cdk/aws-kinesisanalytics-flink/test/integ.vpc-application.js.snapshot/integ.json create mode 100644 packages/@aws-cdk/aws-kinesisanalytics-flink/test/integ.vpc-application.js.snapshot/manifest.json create mode 100644 packages/@aws-cdk/aws-kinesisanalytics-flink/test/integ.vpc-application.js.snapshot/tree.json create mode 100644 packages/@aws-cdk/aws-kinesisanalytics-flink/test/integ.vpc-application.ts diff --git a/packages/@aws-cdk/aws-kinesisanalytics-flink/README.md b/packages/@aws-cdk/aws-kinesisanalytics-flink/README.md index 8e91fcd78b6ac..2e99aeb3ba0d0 100644 --- a/packages/@aws-cdk/aws-kinesisanalytics-flink/README.md +++ b/packages/@aws-cdk/aws-kinesisanalytics-flink/README.md @@ -46,7 +46,7 @@ const flinkApp = new flink.Application(this, 'Application', { }, }, // ... - runtime: flink.Runtime.FLINK_1_13, + runtime: flink.Runtime.FLINK_1_15, code: flink.ApplicationCode.fromBucket(bucket, 'my-app.jar'), }); ``` @@ -59,7 +59,7 @@ snapshotting, monitoring, and parallelism. declare const bucket: s3.Bucket; const flinkApp = new flink.Application(this, 'Application', { code: flink.ApplicationCode.fromBucket(bucket, 'my-app.jar'), - runtime: flink.Runtime.FLINK_1_13, + runtime: flink.Runtime.FLINK_1_15, checkpointingEnabled: true, // default is true checkpointInterval: Duration.seconds(30), // default is 1 minute minPauseBetweenCheckpoints: Duration.seconds(10), // default is 5 seconds @@ -72,3 +72,15 @@ const flinkApp = new flink.Application(this, 'Application', { logGroup: new logs.LogGroup(this, 'LogGroup'), // by default, a new LogGroup will be created }); ``` + +Flink applications can optionally be deployed in a VPC: + +```ts +declare const bucket: s3.Bucket; +declare const vpc: ec2.Vpc; +const flinkApp = new flink.Application(this, 'Application', { + code: flink.ApplicationCode.fromBucket(bucket, 'my-app.jar'), + runtime: flink.Runtime.FLINK_1_15, + vpc, +}); +``` diff --git a/packages/@aws-cdk/aws-kinesisanalytics-flink/lib/application.ts b/packages/@aws-cdk/aws-kinesisanalytics-flink/lib/application.ts index 312c55eb9ac22..a7cee60ffa496 100644 --- a/packages/@aws-cdk/aws-kinesisanalytics-flink/lib/application.ts +++ b/packages/@aws-cdk/aws-kinesisanalytics-flink/lib/application.ts @@ -1,4 +1,5 @@ import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; +import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import { CfnApplicationCloudWatchLoggingOptionV2, CfnApplicationV2 } from '@aws-cdk/aws-kinesisanalytics'; import * as logs from '@aws-cdk/aws-logs'; @@ -14,7 +15,7 @@ import { LogLevel, MetricsLevel, PropertyGroups, Runtime } from './types'; * An interface expressing the public properties on both an imported and * CDK-created Flink application. */ -export interface IApplication extends core.IResource, iam.IGrantable { +export interface IApplication extends core.IResource, ec2.IConnectable, iam.IGrantable { /** * The application ARN. * @@ -56,7 +57,7 @@ export interface IApplication extends core.IResource, iam.IGrantable { * * Reporting Level: Application * - * @default average over 5 minutes + * @default - average over 5 minutes */ metricKpus(props?: cloudwatch.MetricOptions): cloudwatch.Metric; @@ -67,7 +68,7 @@ export interface IApplication extends core.IResource, iam.IGrantable { * * Reporting Level: Application * - * @default average over 5 minutes + * @default - average over 5 minutes */ metricDowntime(props?: cloudwatch.MetricOptions): cloudwatch.Metric; @@ -78,7 +79,7 @@ export interface IApplication extends core.IResource, iam.IGrantable { * * Reporting Level: Application * - * @default sample count over 5 minutes + * @default - sample count over 5 minutes */ metricUptime(props?: cloudwatch.MetricOptions): cloudwatch.Metric; @@ -90,7 +91,7 @@ export interface IApplication extends core.IResource, iam.IGrantable { * * Reporting Level: Application * - * @default sum over 5 minutes + * @default - sum over 5 minutes */ metricFullRestarts(props?: cloudwatch.MetricOptions): cloudwatch.Metric; @@ -101,7 +102,7 @@ export interface IApplication extends core.IResource, iam.IGrantable { * * Reporting Level: Application * - * @default sum over 5 minutes + * @default - sum over 5 minutes */ metricNumberOfFailedCheckpoints(props?: cloudwatch.MetricOptions): cloudwatch.Metric; @@ -112,7 +113,7 @@ export interface IApplication extends core.IResource, iam.IGrantable { * * Reporting Level: Application * - * @default maximum over 5 minutes + * @default - maximum over 5 minutes */ metricLastCheckpointDuration(props?: cloudwatch.MetricOptions): cloudwatch.Metric; @@ -123,7 +124,7 @@ export interface IApplication extends core.IResource, iam.IGrantable { * * Reporting Level: Application * - * @default maximum over 5 minutes + * @default - maximum over 5 minutes */ metricLastCheckpointSize(props?: cloudwatch.MetricOptions): cloudwatch.Metric; @@ -136,7 +137,7 @@ export interface IApplication extends core.IResource, iam.IGrantable { * * Reporting Level: Application * - * @default average over 5 minutes + * @default - average over 5 minutes */ metricCpuUtilization(props?: cloudwatch.MetricOptions): cloudwatch.Metric; @@ -149,7 +150,7 @@ export interface IApplication extends core.IResource, iam.IGrantable { * * Reporting Level: Application * - * @default average over 5 minutes + * @default - average over 5 minutes */ metricHeapMemoryUtilization(props?: cloudwatch.MetricOptions): cloudwatch.Metric; @@ -160,7 +161,7 @@ export interface IApplication extends core.IResource, iam.IGrantable { * * Reporting Level: Application * - * @default sum over 5 minutes + * @default - sum over 5 minutes */ metricOldGenerationGCTime(props?: cloudwatch.MetricOptions): cloudwatch.Metric; @@ -172,7 +173,7 @@ export interface IApplication extends core.IResource, iam.IGrantable { * * Reporting Level: Application * - * @default sum over 5 minutes + * @default - sum over 5 minutes */ metricOldGenerationGCCount(props?: cloudwatch.MetricOptions): cloudwatch.Metric; @@ -183,7 +184,7 @@ export interface IApplication extends core.IResource, iam.IGrantable { * * Reporting Level: Application * - * @default average over 5 minutes + * @default - average over 5 minutes */ metricThreadsCount(props?: cloudwatch.MetricOptions): cloudwatch.Metric; @@ -195,7 +196,7 @@ export interface IApplication extends core.IResource, iam.IGrantable { * * Reporting Level: Application, Operator, Task, Parallelism * - * @default average over 5 minutes + * @default - average over 5 minutes */ metricNumRecordsIn(props?: cloudwatch.MetricOptions): cloudwatch.Metric; @@ -207,7 +208,7 @@ export interface IApplication extends core.IResource, iam.IGrantable { * * Reporting Level: Application, Operator, Task, Parallelism * - * @default average over 5 minutes + * @default - average over 5 minutes */ metricNumRecordsInPerSecond(props?: cloudwatch.MetricOptions): cloudwatch.Metric; @@ -218,7 +219,7 @@ export interface IApplication extends core.IResource, iam.IGrantable { * * Reporting Level: Application, Operator, Task, Parallelism * - * @default average over 5 minutes + * @default - average over 5 minutes */ metricNumRecordsOut(props?: cloudwatch.MetricOptions): cloudwatch.Metric; @@ -230,7 +231,7 @@ export interface IApplication extends core.IResource, iam.IGrantable { * * Reporting Level: Application, Operator, Task, Parallelism * - * @default average over 5 minutes + * @default - average over 5 minutes */ metricNumRecordsOutPerSecond(props?: cloudwatch.MetricOptions): cloudwatch.Metric; @@ -241,7 +242,7 @@ export interface IApplication extends core.IResource, iam.IGrantable { * * Reporting Level: Application, Operator, Task, Parallelism * - * @default sum over 5 minutes + * @default - sum over 5 minutes */ metricNumLateRecordsDropped(props?: cloudwatch.MetricOptions): cloudwatch.Metric; @@ -252,7 +253,7 @@ export interface IApplication extends core.IResource, iam.IGrantable { * * Reporting Level: Application, Operator, Task, Parallelism * - * @default maximum over 5 minutes + * @default - maximum over 5 minutes */ metricCurrentInputWatermark(props?: cloudwatch.MetricOptions): cloudwatch.Metric; @@ -263,7 +264,7 @@ export interface IApplication extends core.IResource, iam.IGrantable { * * Reporting Level: Application, Operator, Task, Parallelism * - * @default maximum over 5 minutes + * @default - maximum over 5 minutes */ metricCurrentOutputWatermark(props?: cloudwatch.MetricOptions): cloudwatch.Metric; @@ -274,7 +275,7 @@ export interface IApplication extends core.IResource, iam.IGrantable { * * Reporting Level: Application, Operator, Task, Parallelism * - * @default average over 5 minutes + * @default - average over 5 minutes */ metricManagedMemoryUsed(props?: cloudwatch.MetricOptions): cloudwatch.Metric; @@ -285,7 +286,7 @@ export interface IApplication extends core.IResource, iam.IGrantable { * * Reporting Level: Application, Operator, Task, Parallelism * - * @default average over 5 minutes + * @default - average over 5 minutes */ metricManagedMemoryTotal(props?: cloudwatch.MetricOptions): cloudwatch.Metric; @@ -296,7 +297,7 @@ export interface IApplication extends core.IResource, iam.IGrantable { * * Reporting Level: Application, Operator, Task, Parallelism * - * @default average over 5 minutes + * @default - average over 5 minutes */ metricManagedMemoryUtilization(props?: cloudwatch.MetricOptions): cloudwatch.Metric; @@ -309,7 +310,7 @@ export interface IApplication extends core.IResource, iam.IGrantable { * * Reporting Level: Operator, Task, Parallelism * - * @default average over 5 minutes + * @default - average over 5 minutes */ metricIdleTimeMsPerSecond(props?: cloudwatch.MetricOptions): cloudwatch.Metric; @@ -321,7 +322,7 @@ export interface IApplication extends core.IResource, iam.IGrantable { * * Reporting Level: Operator, Task, Parallelism * - * @default average over 5 minutes + * @default - average over 5 minutes */ metricBackPressuredTimeMsPerSecond(props?: cloudwatch.MetricOptions): cloudwatch.Metric; @@ -334,7 +335,7 @@ export interface IApplication extends core.IResource, iam.IGrantable { * * Reporting Level: Operator, Task, Parallelism * - * @default average over 5 minutes + * @default - average over 5 minutes */ metricBusyTimePerMsPerSecond(props?: cloudwatch.MetricOptions): cloudwatch.Metric; } @@ -351,6 +352,13 @@ abstract class ApplicationBase extends core.Resource implements IApplication { // Implement iam.IGrantable interface public abstract readonly grantPrincipal: iam.IPrincipal; + /** + * The underlying connections object for the connections getter. + * + * @internal + */ + protected _connections?: ec2.Connections; + /** Implement the convenience `IApplication.addToPrincipalPolicy` method. */ public addToRolePolicy(policyStatement: iam.PolicyStatement): boolean { if (this.role) { @@ -361,6 +369,13 @@ abstract class ApplicationBase extends core.Resource implements IApplication { return false; } + public get connections() { + if (!this._connections) { + throw new Error('This Application isn\'t associated with a VPC. Provide a "vpc" prop when creating the Application or "securityGroups" when importing it.'); + } + return this._connections; + } + /** * Return a CloudWatch metric associated with this Flink application. * @@ -385,7 +400,7 @@ abstract class ApplicationBase extends core.Resource implements IApplication { * * Reporting Level: Application * - * @default average over 5 minutes + * @default - average over 5 minutes */ metricKpus(props?: cloudwatch.MetricOptions) { return this.metric('KPUs', { statistic: 'Average', ...props }); @@ -399,7 +414,7 @@ abstract class ApplicationBase extends core.Resource implements IApplication { * * Reporting Level: Application * - * @default average over 5 minutes + * @default - average over 5 minutes */ metricDowntime(props?: cloudwatch.MetricOptions) { return this.metric('downtime', { statistic: 'Average', ...props }); @@ -412,7 +427,7 @@ abstract class ApplicationBase extends core.Resource implements IApplication { * * Reporting Level: Application * - * @default average over 5 minutes + * @default - average over 5 minutes */ metricUptime(props?: cloudwatch.MetricOptions) { return this.metric('uptime', { statistic: 'Average', ...props }); @@ -426,7 +441,7 @@ abstract class ApplicationBase extends core.Resource implements IApplication { * * Reporting Level: Application * - * @default sum over 5 minutes + * @default - sum over 5 minutes */ metricFullRestarts(props?: cloudwatch.MetricOptions) { return this.metric('fullRestarts', { statistic: 'Sum', ...props }); @@ -439,7 +454,7 @@ abstract class ApplicationBase extends core.Resource implements IApplication { * * Reporting Level: Application * - * @default sum over 5 minutes + * @default - sum over 5 minutes */ metricNumberOfFailedCheckpoints(props?: cloudwatch.MetricOptions) { return this.metric('numberOfFailedCheckpoints', { statistic: 'Sum', ...props }); @@ -452,7 +467,7 @@ abstract class ApplicationBase extends core.Resource implements IApplication { * * Reporting Level: Application * - * @default maximum over 5 minutes + * @default - maximum over 5 minutes */ metricLastCheckpointDuration(props?: cloudwatch.MetricOptions) { return this.metric('lastCheckpointDuration', { statistic: 'Maximum', ...props }); @@ -465,7 +480,7 @@ abstract class ApplicationBase extends core.Resource implements IApplication { * * Reporting Level: Application * - * @default maximum over 5 minutes + * @default - maximum over 5 minutes */ metricLastCheckpointSize(props?: cloudwatch.MetricOptions) { return this.metric('lastCheckpointSize', { statistic: 'Maximum', ...props }); @@ -480,7 +495,7 @@ abstract class ApplicationBase extends core.Resource implements IApplication { * * Reporting Level: Application * - * @default average over 5 minutes + * @default - average over 5 minutes */ metricCpuUtilization(props?: cloudwatch.MetricOptions) { return this.metric('cpuUtilization', { statistic: 'Average', ...props }); @@ -495,7 +510,7 @@ abstract class ApplicationBase extends core.Resource implements IApplication { * * Reporting Level: Application * - * @default average over 5 minutes + * @default - average over 5 minutes */ metricHeapMemoryUtilization(props?: cloudwatch.MetricOptions) { return this.metric('heapMemoryUtilization', { statistic: 'Average', ...props }); @@ -508,7 +523,7 @@ abstract class ApplicationBase extends core.Resource implements IApplication { * * Reporting Level: Application * - * @default sum over 5 minutes + * @default - sum over 5 minutes */ metricOldGenerationGCTime(props?: cloudwatch.MetricOptions) { return this.metric('oldGenerationGCTime', { statistic: 'Sum', ...props }); @@ -522,7 +537,7 @@ abstract class ApplicationBase extends core.Resource implements IApplication { * * Reporting Level: Application * - * @default sum over 5 minutes + * @default - sum over 5 minutes */ metricOldGenerationGCCount(props?: cloudwatch.MetricOptions) { return this.metric('oldGenerationGCCount', { statistic: 'Sum', ...props }); @@ -535,7 +550,7 @@ abstract class ApplicationBase extends core.Resource implements IApplication { * * Reporting Level: Application * - * @default average over 5 minutes + * @default - average over 5 minutes */ metricThreadsCount(props?: cloudwatch.MetricOptions) { return this.metric('threadsCount', { statistic: 'Average', ...props }); @@ -549,7 +564,7 @@ abstract class ApplicationBase extends core.Resource implements IApplication { * * Reporting Level: Application, Operator, Task, Parallelism * - * @default average over 5 minutes + * @default - average over 5 minutes */ metricNumRecordsIn(props?: cloudwatch.MetricOptions) { return this.metric('numRecordsIn', { statistic: 'Average', ...props }); @@ -563,7 +578,7 @@ abstract class ApplicationBase extends core.Resource implements IApplication { * * Reporting Level: Application, Operator, Task, Parallelism * - * @default average over 5 minutes + * @default - average over 5 minutes */ metricNumRecordsInPerSecond(props?: cloudwatch.MetricOptions) { return this.metric('numRecordsInPerSecond', { statistic: 'Average', ...props }); @@ -576,7 +591,7 @@ abstract class ApplicationBase extends core.Resource implements IApplication { * * Reporting Level: Application, Operator, Task, Parallelism * - * @default average over 5 minutes + * @default - average over 5 minutes */ metricNumRecordsOut(props?: cloudwatch.MetricOptions) { return this.metric('numRecordsOut', { statistic: 'Average', ...props }); @@ -590,7 +605,7 @@ abstract class ApplicationBase extends core.Resource implements IApplication { * * Reporting Level: Application, Operator, Task, Parallelism * - * @default average over 5 minutes + * @default - average over 5 minutes */ metricNumRecordsOutPerSecond(props?: cloudwatch.MetricOptions) { return this.metric('numRecordsOutPerSecond', { statistic: 'Average', ...props }); @@ -604,7 +619,7 @@ abstract class ApplicationBase extends core.Resource implements IApplication { * * Reporting Level: Application, Operator, Task, Parallelism * - * @default sum over 5 minutes + * @default - sum over 5 minutes */ metricNumLateRecordsDropped(props?: cloudwatch.MetricOptions) { return this.metric('numLateRecordsDropped', { statistic: 'Sum', ...props }); @@ -617,7 +632,7 @@ abstract class ApplicationBase extends core.Resource implements IApplication { * * Reporting Level: Application, Operator, Task, Parallelism * - * @default maximum over 5 minutes + * @default - maximum over 5 minutes */ metricCurrentInputWatermark(props?: cloudwatch.MetricOptions) { return this.metric('currentInputWatermark', { statistic: 'Maximum', ...props }); @@ -630,7 +645,7 @@ abstract class ApplicationBase extends core.Resource implements IApplication { * * Reporting Level: Application, Operator, Task, Parallelism * - * @default maximum over 5 minutes + * @default - maximum over 5 minutes */ metricCurrentOutputWatermark(props?: cloudwatch.MetricOptions) { return this.metric('currentOutputWatermark', { statistic: 'Maximum', ...props }); @@ -643,7 +658,7 @@ abstract class ApplicationBase extends core.Resource implements IApplication { * * Reporting Level: Application, Operator, Task, Parallelism * - * @default average over 5 minutes + * @default - average over 5 minutes */ metricManagedMemoryUsed(props?: cloudwatch.MetricOptions) { return this.metric('managedMemoryUsed', { statistic: 'Average', ...props }); @@ -656,7 +671,7 @@ abstract class ApplicationBase extends core.Resource implements IApplication { * * Reporting Level: Application, Operator, Task, Parallelism * - * @default average over 5 minutes + * @default - average over 5 minutes */ metricManagedMemoryTotal(props?: cloudwatch.MetricOptions) { return this.metric('managedMemoryTotal', { statistic: 'Average', ...props }); @@ -669,7 +684,7 @@ abstract class ApplicationBase extends core.Resource implements IApplication { * * Reporting Level: Application, Operator, Task, Parallelism * - * @default average over 5 minutes + * @default - average over 5 minutes */ metricManagedMemoryUtilization(props?: cloudwatch.MetricOptions) { return this.metric('managedMemoryUtilization', { statistic: 'Average', ...props }); @@ -684,7 +699,7 @@ abstract class ApplicationBase extends core.Resource implements IApplication { * * Reporting Level: Operator, Task, Parallelism * - * @default average over 5 minutes + * @default - average over 5 minutes */ metricIdleTimeMsPerSecond(props?: cloudwatch.MetricOptions) { return this.metric('idleTimeMsPerSecond', { statistic: 'Average', ...props }); @@ -698,7 +713,7 @@ abstract class ApplicationBase extends core.Resource implements IApplication { * * Reporting Level: Operator, Task, Parallelism * - * @default average over 5 minutes + * @default - average over 5 minutes */ metricBackPressuredTimeMsPerSecond(props?: cloudwatch.MetricOptions) { return this.metric('backPressuredTimeMsPerSecond', { statistic: 'Average', ...props }); @@ -713,13 +728,32 @@ abstract class ApplicationBase extends core.Resource implements IApplication { * * Reporting Level: Operator, Task, Parallelism * - * @default average over 5 minutes + * @default - average over 5 minutes */ metricBusyTimePerMsPerSecond(props?: cloudwatch.MetricOptions) { return this.metric('busyTimePerMsPerSecond', { statistic: 'Average', ...props }); } } +/** + * Attributes used for importing an Application with Application.fromApplicationAttributes. + */ +export interface ApplicationAttributes { + /** + * The ARN of the Flink application. + * + * Format: arn::kinesisanalytics:::application/ + */ + readonly applicationArn: string; + + /** + * The security groups for this Flink application if deployed in a VPC. + * + * @default - no security groups + */ + readonly securityGroups?: ec2.ISecurityGroup[]; +} + /** * Props for creating an Application construct. */ @@ -751,7 +785,7 @@ export interface ApplicationProps { /** * The interval between checkpoints. * - * @default 1 minute + * @default - 1 minute */ readonly checkpointInterval?: core.Duration; @@ -759,7 +793,7 @@ export interface ApplicationProps { * The minimum amount of time in to wait after a checkpoint finishes to start * a new checkpoint. * - * @default 5 seconds + * @default - 5 seconds */ readonly minPauseBetweenCheckpoints?: core.Duration; @@ -815,7 +849,7 @@ export interface ApplicationProps { * Configuration PropertyGroups. You can use these property groups to pass * arbitrary runtime configuration values to your Flink app. * - * @default No property group configuration provided to the Flink app + * @default - No property group configuration provided to the Flink app */ readonly propertyGroups?: PropertyGroups; @@ -837,9 +871,30 @@ export interface ApplicationProps { /** * The log group to send log entries to. * - * @default CDK's default LogGroup + * @default - CDK's default LogGroup */ readonly logGroup?: logs.ILogGroup; + + /** + * Deploy the Flink application in a VPC. + * + * @default - no VPC + */ + readonly vpc?: ec2.IVpc; + + /** + * Choose which VPC subnets to use. + * + * @default - SubnetType.PRIVATE_WITH_EGRESS subnets + */ + readonly vpcSubnets?: ec2.SubnetSelection; + + /** + * Security groups to use with a provided VPC. + * + * @default - a new security group is created for this application. + */ + readonly securityGroups?: ec2.ISecurityGroup[]; } /** @@ -851,7 +906,7 @@ class Import extends ApplicationBase { public readonly applicationName: string; public readonly applicationArn: string; - constructor(scope: Construct, id: string, attrs: { applicationArn: string, applicationName: string }) { + constructor(scope: Construct, id: string, attrs: { applicationArn: string, securityGroups?: ec2.ISecurityGroup[] }) { super(scope, id); // Imported applications have no associated role or grantPrincipal @@ -859,7 +914,16 @@ class Import extends ApplicationBase { this.role = undefined; this.applicationArn = attrs.applicationArn; - this.applicationName = attrs.applicationName; + const applicationName = core.Stack.of(scope).splitArn(attrs.applicationArn, core.ArnFormat.SLASH_RESOURCE_NAME).resourceName; + if (!applicationName) { + throw new Error(`applicationArn for fromApplicationArn (${attrs.applicationArn}) must include resource name`); + } + this.applicationName = applicationName; + + const securityGroups = attrs.securityGroups ?? []; + if (securityGroups.length > 0) { + this._connections = new ec2.Connections({ securityGroups }); + } } } @@ -877,7 +941,7 @@ export class Application extends ApplicationBase { public static fromApplicationName(scope: Construct, id: string, applicationName: string): IApplication { const applicationArn = core.Stack.of(scope).formatArn(applicationArnComponents(applicationName)); - return new Import(scope, id, { applicationArn, applicationName }); + return new Import(scope, id, { applicationArn }); } /** @@ -885,12 +949,17 @@ export class Application extends ApplicationBase { * applicationArn. */ public static fromApplicationArn(scope: Construct, id: string, applicationArn: string): IApplication { - const applicationName = core.Stack.of(scope).splitArn(applicationArn, core.ArnFormat.SLASH_RESOURCE_NAME).resourceName; - if (!applicationName) { - throw new Error(`applicationArn for fromApplicationArn (${applicationArn}) must include resource name`); - } + return new Import(scope, id, { applicationArn }); + } - return new Import(scope, id, { applicationArn, applicationName }); + /** + * Import an existing application defined outside of CDK code. + */ + public static fromApplicationAttributes(scope: Construct, id: string, attrs: ApplicationAttributes): IApplication { + return new Import(scope, id, { + applicationArn: attrs.applicationArn, + securityGroups: attrs.securityGroups, + }); } public readonly applicationArn: string; @@ -919,6 +988,23 @@ export class Application extends ApplicationBase { const code = props.code.bind(this); code.bucket.grantRead(this); + let vpcConfigurations; + if (props.vpc) { + const securityGroups = props.securityGroups ?? [ + new ec2.SecurityGroup(this, 'SecurityGroup', { + vpc: props.vpc, + }), + ]; + this._connections = new ec2.Connections({ securityGroups }); + const subnetSelection = props.vpcSubnets ?? { + subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS, + }; + vpcConfigurations = [{ + securityGroupIds: securityGroups.map(sg => sg.securityGroupId), + subnetIds: props.vpc.selectSubnets(subnetSelection).subnetIds, + }]; + } + const resource = new CfnApplicationV2(this, 'Resource', { applicationName: props.applicationName, runtimeEnvironment: props.runtime.value, @@ -939,6 +1025,7 @@ export class Application extends ApplicationBase { applicationSnapshotConfiguration: { snapshotsEnabled: props.snapshotsEnabled ?? true, }, + vpcConfigurations, }, }); resource.node.addDependency(this.role); @@ -978,6 +1065,24 @@ export class Application extends ApplicationBase { }, }); + // Permissions required for VPC usage per: + // https://docs.aws.amazon.com/kinesisanalytics/latest/java/vpc-permissions.html + if (props.vpc) { + this.role.addToPrincipalPolicy(new iam.PolicyStatement({ + actions: [ + 'ec2:DescribeVpcs', + 'ec2:DescribeSubnets', + 'ec2:DescribeSecurityGroups', + 'ec2:DescribeDhcpOptions', + 'ec2:CreateNetworkInterface', + 'ec2:CreateNetworkInterfacePermission', + 'ec2:DescribeNetworkInterfaces', + 'ec2:DeleteNetworkInterface', + ], + resources: ['*'], + })); + } + this.applicationName = this.getResourceNameAttribute(resource.ref); this.applicationArn = this.getResourceArnAttribute( core.Stack.of(this).formatArn(applicationArnComponents(resource.ref)), diff --git a/packages/@aws-cdk/aws-kinesisanalytics-flink/lib/private/validation.ts b/packages/@aws-cdk/aws-kinesisanalytics-flink/lib/private/validation.ts index b0f94f56daf77..739956e926f3f 100644 --- a/packages/@aws-cdk/aws-kinesisanalytics-flink/lib/private/validation.ts +++ b/packages/@aws-cdk/aws-kinesisanalytics-flink/lib/private/validation.ts @@ -1,9 +1,13 @@ +import * as ec2 from '@aws-cdk/aws-ec2'; import * as core from '@aws-cdk/core'; interface ValidatedProps { applicationName?: string; parallelism?: number; parallelismPerKpu?: number; + vpc?: ec2.IVpc; + vpcSubnets?: ec2.SubnetSelection; + securityGroups?: ec2.ISecurityGroup[]; } /** @@ -13,6 +17,7 @@ export function validateFlinkApplicationProps(props: ValidatedProps) { validateApplicationName(props.applicationName); validateParallelism(props.parallelism); validateParallelismPerKpu(props.parallelismPerKpu); + validateVpcProps(props); } function validateApplicationName(applicationName?: string) { @@ -52,3 +57,15 @@ function validateParallelismPerKpu(parallelismPerKpu?: number) { throw new Error('parallelismPerKpu must be at least 1'); } } + +function validateVpcProps({ vpc, securityGroups = [], vpcSubnets }: ValidatedProps) { + if (!vpc) { + if (vpcSubnets) { + throw new Error('vpc prop required when passing vpcSubnets'); + } + + if (securityGroups.length > 0) { + throw new Error('vpc prop required when passing securityGroups'); + } + } +} diff --git a/packages/@aws-cdk/aws-kinesisanalytics-flink/package.json b/packages/@aws-cdk/aws-kinesisanalytics-flink/package.json index 103eb37da9387..b0aeedbafabfe 100644 --- a/packages/@aws-cdk/aws-kinesisanalytics-flink/package.json +++ b/packages/@aws-cdk/aws-kinesisanalytics-flink/package.json @@ -76,6 +76,7 @@ "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/integ-runner": "0.0.0", + "@aws-cdk/integ-tests": "0.0.0", "@aws-cdk/pkglint": "0.0.0", "@types/jest": "^27.5.2", "jest": "^27.5.1", @@ -84,6 +85,7 @@ "dependencies": { "@aws-cdk/assets": "0.0.0", "@aws-cdk/aws-cloudwatch": "0.0.0", + "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-kinesisanalytics": "0.0.0", "@aws-cdk/aws-kms": "0.0.0", @@ -98,6 +100,7 @@ "peerDependencies": { "@aws-cdk/assets": "0.0.0", "@aws-cdk/aws-cloudwatch": "0.0.0", + "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-kinesisanalytics": "0.0.0", "@aws-cdk/aws-kms": "0.0.0", diff --git a/packages/@aws-cdk/aws-kinesisanalytics-flink/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-kinesisanalytics-flink/rosetta/default.ts-fixture index a9f46e29f793b..69cf40a794c9a 100644 --- a/packages/@aws-cdk/aws-kinesisanalytics-flink/rosetta/default.ts-fixture +++ b/packages/@aws-cdk/aws-kinesisanalytics-flink/rosetta/default.ts-fixture @@ -1,6 +1,7 @@ // Fixture with packages imported, but nothing else import { Construct } from 'constructs'; import { Duration, Stack } from '@aws-cdk/core'; +import * as ec2 from '@aws-cdk/aws-ec2'; import * as flink from '@aws-cdk/aws-kinesisanalytics-flink'; import * as logs from '@aws-cdk/aws-logs'; import * as s3 from '@aws-cdk/aws-s3'; @@ -11,4 +12,4 @@ class Fixture extends Stack { /// here } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-kinesisanalytics-flink/test/application.test.ts b/packages/@aws-cdk/aws-kinesisanalytics-flink/test/application.test.ts index f663c2b4cc0e7..3b83c039452b4 100644 --- a/packages/@aws-cdk/aws-kinesisanalytics-flink/test/application.test.ts +++ b/packages/@aws-cdk/aws-kinesisanalytics-flink/test/application.test.ts @@ -1,6 +1,7 @@ import * as path from 'path'; import { Match, Template } from '@aws-cdk/assertions'; import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; +import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import * as logs from '@aws-cdk/aws-logs'; import * as s3 from '@aws-cdk/aws-s3'; @@ -76,8 +77,14 @@ describe('Application', () => { Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { - Statement: Match.arrayWith([ + Statement: Match.arrayEquals([ { Action: 'cloudwatch:PutMetricData', Effect: 'Allow', Resource: '*' }, + // Access to read from the code bucket + { + Action: ['s3:GetObject*', 's3:GetBucket*', 's3:List*'], + Effect: 'Allow', + Resource: Match.anyValue(), + }, { Action: 'logs:DescribeLogGroups', Effect: 'Allow', @@ -504,6 +511,206 @@ describe('Application', () => { }); }); + test('using a VPC with default vpcSubnets and securityGroups', () => { + new flink.Application(stack, 'FlinkApplication', { + ...requiredProps, + vpc: new ec2.Vpc(stack, 'VPC'), + }); + + const template = Template.fromStack(stack); + template.hasResourceProperties( + 'AWS::KinesisAnalyticsV2::Application', + { + ApplicationConfiguration: { + VpcConfigurations: [ + { + SecurityGroupIds: [ + { + 'Fn::GetAtt': ['FlinkApplicationSecurityGroup1FD816EE', 'GroupId'], + }, + ], + SubnetIds: [ + { + Ref: 'VPCPrivateSubnet1Subnet8BCA10E0', + }, + { + Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A', + }, + ], + }, + ], + }, + }, + ); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: Match.arrayWith([ + { + Action: [ + 'ec2:DescribeVpcs', + 'ec2:DescribeSubnets', + 'ec2:DescribeSecurityGroups', + 'ec2:DescribeDhcpOptions', + 'ec2:CreateNetworkInterface', + 'ec2:CreateNetworkInterfacePermission', + 'ec2:DescribeNetworkInterfaces', + 'ec2:DeleteNetworkInterface', + ], + Effect: 'Allow', + Resource: '*', + }, + ]), + }, + }); + }); + + test('providing securityGroups', () => { + const vpc = new ec2.Vpc(stack, 'VPC'); + new flink.Application(stack, 'FlinkApplication', { + ...requiredProps, + vpc, + securityGroups: [ + new ec2.SecurityGroup(stack, 'ProvidedSecurityGroup', { vpc }), + ], + }); + + Template.fromStack(stack).hasResourceProperties( + 'AWS::KinesisAnalyticsV2::Application', + { + ApplicationConfiguration: { + VpcConfigurations: [ + { + SecurityGroupIds: [ + { + 'Fn::GetAtt': ['ProvidedSecurityGroup3C7655DD', 'GroupId'], + }, + ], + }, + ], + }, + }, + ); + }); + + test('providing a subnetSelection', () => { + new flink.Application(stack, 'FlinkApplication', { + ...requiredProps, + vpc: new ec2.Vpc(stack, 'VPC'), + vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC }, + }); + + Template.fromStack(stack).hasResourceProperties( + 'AWS::KinesisAnalyticsV2::Application', + { + ApplicationConfiguration: { + VpcConfigurations: [ + { + SubnetIds: [ + { + Ref: 'VPCPublicSubnet1SubnetB4246D30', + }, + { + Ref: 'VPCPublicSubnet2Subnet74179F39', + }, + ], + }, + ], + }, + }, + ); + }); + + test('using connections on a created Application', () => { + const app = new flink.Application(stack, 'FlinkApplication', { + ...requiredProps, + vpc: new ec2.Vpc(stack, 'VPC'), + }); + + app.connections.allowFromAnyIpv4(ec2.Port.tcp(443)); + + Template.fromStack(stack).hasResourceProperties( + 'AWS::EC2::SecurityGroup', + { + SecurityGroupEgress: [{ + Description: 'Allow all outbound traffic by default', + IpProtocol: '-1', + }], + SecurityGroupIngress: [{ + Description: 'from 0.0.0.0/0:443', + FromPort: 443, + IpProtocol: 'tcp', + ToPort: 443, + }], + }, + ); + }); + + test('using connections on an imported Application', () => { + const app = flink.Application.fromApplicationAttributes(stack, 'FlinkApplication', { + applicationArn: 'arn:aws:kinesisanalytics:us-west-2:012345678901:application/my-app', + securityGroups: [ec2.SecurityGroup.fromSecurityGroupId(stack, 'ImportedSG', 'sg-123456789')], + }); + + app.connections.allowFromAnyIpv4(ec2.Port.tcp(443)); + + Template.fromStack(stack).hasResourceProperties( + 'AWS::EC2::SecurityGroupIngress', + { + FromPort: 443, + GroupId: 'sg-123456789', + IpProtocol: 'tcp', + ToPort: 443, + }, + ); + }); + + test('validating vpnSubnets prop requires vpc prop', () => { + expect(() => { + new flink.Application(stack, 'FlinkApplication', { + ...requiredProps, + vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC }, + }); + }).toThrow(/vpc prop required when passing vpcSubnets/); + }); + + test('validating securityGroups prop requires vpc prop', () => { + expect(() => { + const vpc = new ec2.Vpc(stack, 'VPC'); + const securityGroup = new ec2.SecurityGroup(stack, 'SecurityGroup', { + vpc, + }); + new flink.Application(stack, 'Error', { + ...requiredProps, + securityGroups: [securityGroup], + }); + }).toThrow(/vpc prop required when passing securityGroups/); + + // empty array for securityGroups is treated the same as undefined + expect(() => { + new flink.Application(stack, 'OK', { + ...requiredProps, + securityGroups: [], + }); + }).not.toThrow(); + }); + + test('validating vpc provided when using connections for created App', () => { + let app = new flink.Application(stack, 'FlinkApplication', { + ...requiredProps, + }); + expect(() => { + app.connections; + }).toThrow(/This Application isn\'t associated with a VPC/); + }); + + test('validating vpc provided when using connections for imported App', () => { + let app = flink.Application.fromApplicationName(stack, 'FlinkApplication', 'Name'); + expect(() => { + app.connections; + }).toThrow(/This Application isn\'t associated with a VPC/); + }); + test('validating applicationName', () => { // Expect no error with valid name new flink.Application(stack, 'ValidString', { @@ -612,6 +819,17 @@ describe('Application', () => { expect(flinkApp.addToRolePolicy(new iam.PolicyStatement())).toBe(false); }); + test('fromFlinkApplicationAttributes', () => { + const arn = 'arn:aws:kinesisanalytics:us-west-2:012345678901:application/my-app'; + const flinkApp = flink.Application.fromApplicationAttributes(stack, 'Imported', { + applicationArn: arn, + }); + + expect(flinkApp.applicationName).toEqual('my-app'); + expect(flinkApp.applicationArn).toEqual(arn); + expect(flinkApp.addToRolePolicy(new iam.PolicyStatement())).toBe(false); + }); + test('get metric', () => { const flinkApp = new flink.Application(stack, 'Application', { ...requiredProps }); expect(flinkApp.metric('KPUs', { statistic: 'Sum' })) diff --git a/packages/@aws-cdk/aws-kinesisanalytics-flink/test/integ.vpc-application.js.snapshot/FlinkAppTest.assets.json b/packages/@aws-cdk/aws-kinesisanalytics-flink/test/integ.vpc-application.js.snapshot/FlinkAppTest.assets.json new file mode 100644 index 0000000000000..705074e025672 --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisanalytics-flink/test/integ.vpc-application.js.snapshot/FlinkAppTest.assets.json @@ -0,0 +1,32 @@ +{ + "version": "30.1.0", + "files": { + "8be9e0b5f53d41e9a3b1d51c9572c65f24f8170a7188d0ed57fb7d571de4d577": { + "source": { + "path": "asset.8be9e0b5f53d41e9a3b1d51c9572c65f24f8170a7188d0ed57fb7d571de4d577", + "packaging": "zip" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "8be9e0b5f53d41e9a3b1d51c9572c65f24f8170a7188d0ed57fb7d571de4d577.zip", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + }, + "e6269b086e65eaed552c57d90811a297037300cdaf9403468e748cc1d22dc668": { + "source": { + "path": "FlinkAppTest.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "e6269b086e65eaed552c57d90811a297037300cdaf9403468e748cc1d22dc668.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/aws-kinesisanalytics-flink/test/integ.vpc-application.js.snapshot/FlinkAppTest.template.json b/packages/@aws-cdk/aws-kinesisanalytics-flink/test/integ.vpc-application.js.snapshot/FlinkAppTest.template.json new file mode 100644 index 0000000000000..b2ab7859cf42a --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisanalytics-flink/test/integ.vpc-application.js.snapshot/FlinkAppTest.template.json @@ -0,0 +1,720 @@ +{ + "Resources": { + "VPCB9E5F0B4": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "FlinkAppTest/VPC" + } + ] + } + }, + "VPCPublicSubnet1SubnetB4246D30": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": { + "Fn::Select": [ + 0, + { + "Fn::GetAZs": "" + } + ] + }, + "CidrBlock": "10.0.0.0/18", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "FlinkAppTest/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1RouteTableFEE4B781": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "FlinkAppTest/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1RouteTableAssociation0B0896DC": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet1RouteTableFEE4B781" + }, + "SubnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + } + } + }, + "VPCPublicSubnet1DefaultRoute91CEF279": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet1RouteTableFEE4B781" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VPCIGWB7E252D3" + } + }, + "DependsOn": [ + "VPCVPCGW99B986DC" + ] + }, + "VPCPublicSubnet1EIP6AD938E8": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "FlinkAppTest/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1NATGatewayE0556630": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + }, + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet1EIP6AD938E8", + "AllocationId" + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "FlinkAppTest/VPC/PublicSubnet1" + } + ] + }, + "DependsOn": [ + "VPCPublicSubnet1DefaultRoute91CEF279", + "VPCPublicSubnet1RouteTableAssociation0B0896DC" + ] + }, + "VPCPublicSubnet2Subnet74179F39": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": { + "Fn::Select": [ + 1, + { + "Fn::GetAZs": "" + } + ] + }, + "CidrBlock": "10.0.64.0/18", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "FlinkAppTest/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2RouteTable6F1A15F1": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "FlinkAppTest/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2RouteTableAssociation5A808732": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet2RouteTable6F1A15F1" + }, + "SubnetId": { + "Ref": "VPCPublicSubnet2Subnet74179F39" + } + } + }, + "VPCPublicSubnet2DefaultRouteB7481BBA": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet2RouteTable6F1A15F1" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VPCIGWB7E252D3" + } + }, + "DependsOn": [ + "VPCVPCGW99B986DC" + ] + }, + "VPCPublicSubnet2EIP4947BC00": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "FlinkAppTest/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2NATGateway3C070193": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "VPCPublicSubnet2Subnet74179F39" + }, + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet2EIP4947BC00", + "AllocationId" + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "FlinkAppTest/VPC/PublicSubnet2" + } + ] + }, + "DependsOn": [ + "VPCPublicSubnet2DefaultRouteB7481BBA", + "VPCPublicSubnet2RouteTableAssociation5A808732" + ] + }, + "VPCPrivateSubnet1Subnet8BCA10E0": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": { + "Fn::Select": [ + 0, + { + "Fn::GetAZs": "" + } + ] + }, + "CidrBlock": "10.0.128.0/18", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "FlinkAppTest/VPC/PrivateSubnet1" + } + ] + } + }, + "VPCPrivateSubnet1RouteTableBE8A6027": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "FlinkAppTest/VPC/PrivateSubnet1" + } + ] + } + }, + "VPCPrivateSubnet1RouteTableAssociation347902D1": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet1RouteTableBE8A6027" + }, + "SubnetId": { + "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + } + } + }, + "VPCPrivateSubnet1DefaultRouteAE1D6490": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet1RouteTableBE8A6027" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VPCPublicSubnet1NATGatewayE0556630" + } + } + }, + "VPCPrivateSubnet2SubnetCFCDAA7A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": { + "Fn::Select": [ + 1, + { + "Fn::GetAZs": "" + } + ] + }, + "CidrBlock": "10.0.192.0/18", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "FlinkAppTest/VPC/PrivateSubnet2" + } + ] + } + }, + "VPCPrivateSubnet2RouteTable0A19E10E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "FlinkAppTest/VPC/PrivateSubnet2" + } + ] + } + }, + "VPCPrivateSubnet2RouteTableAssociation0C73D413": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet2RouteTable0A19E10E" + }, + "SubnetId": { + "Ref": "VPCPrivateSubnet2SubnetCFCDAA7A" + } + } + }, + "VPCPrivateSubnet2DefaultRouteF4F5CFD2": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet2RouteTable0A19E10E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VPCPublicSubnet2NATGateway3C070193" + } + } + }, + "VPCIGWB7E252D3": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "FlinkAppTest/VPC" + } + ] + } + }, + "VPCVPCGW99B986DC": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "InternetGatewayId": { + "Ref": "VPCIGWB7E252D3" + } + } + }, + "AppRole1AF9B530": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "kinesisanalytics.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "AppRoleDefaultPolicy9CADBAA1": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "cloudwatch:PutMetricData", + "ec2:CreateNetworkInterface", + "ec2:CreateNetworkInterfacePermission", + "ec2:DeleteNetworkInterface", + "ec2:DescribeDhcpOptions", + "ec2:DescribeNetworkInterfaces", + "ec2:DescribeSecurityGroups", + "ec2:DescribeSubnets", + "ec2:DescribeVpcs" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "s3:GetBucket*", + "s3:GetObject*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "/*" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + } + ] + ] + } + ] + }, + { + "Action": "logs:DescribeLogGroups", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:*" + ] + ] + } + }, + { + "Action": "logs:DescribeLogStreams", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "AppLogGroupC72EEC8C", + "Arn" + ] + } + }, + { + "Action": "logs:PutLogEvents", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:", + { + "Ref": "AppLogGroupC72EEC8C" + }, + ":log-stream:", + { + "Ref": "AppLogStream3CAF66A7" + } + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "AppRoleDefaultPolicy9CADBAA1", + "Roles": [ + { + "Ref": "AppRole1AF9B530" + } + ] + } + }, + "AppSecurityGroupC292657D": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "FlinkAppTest/App/SecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Ref": "VPCB9E5F0B4" + } + } + }, + "AppF1B96344": { + "Type": "AWS::KinesisAnalyticsV2::Application", + "Properties": { + "RuntimeEnvironment": "FLINK-1_15", + "ServiceExecutionRole": { + "Fn::GetAtt": [ + "AppRole1AF9B530", + "Arn" + ] + }, + "ApplicationConfiguration": { + "ApplicationCodeConfiguration": { + "CodeContent": { + "S3ContentLocation": { + "BucketARN": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + } + ] + ] + }, + "FileKey": "8be9e0b5f53d41e9a3b1d51c9572c65f24f8170a7188d0ed57fb7d571de4d577.zip" + } + }, + "CodeContentType": "ZIPFILE" + }, + "ApplicationSnapshotConfiguration": { + "SnapshotsEnabled": true + }, + "VpcConfigurations": [ + { + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "AppSecurityGroupC292657D", + "GroupId" + ] + } + ], + "SubnetIds": [ + { + "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + }, + { + "Ref": "VPCPrivateSubnet2SubnetCFCDAA7A" + } + ] + } + ] + } + }, + "DependsOn": [ + "AppRoleDefaultPolicy9CADBAA1", + "AppRole1AF9B530" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "AppLogGroupC72EEC8C": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "RetentionInDays": 731 + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "AppLogStream3CAF66A7": { + "Type": "AWS::Logs::LogStream", + "Properties": { + "LogGroupName": { + "Ref": "AppLogGroupC72EEC8C" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "AppLoggingOption75BE995E": { + "Type": "AWS::KinesisAnalyticsV2::ApplicationCloudWatchLoggingOption", + "Properties": { + "ApplicationName": { + "Ref": "AppF1B96344" + }, + "CloudWatchLoggingOption": { + "LogStreamARN": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:", + { + "Ref": "AppLogGroupC72EEC8C" + }, + ":log-stream:", + { + "Ref": "AppLogStream3CAF66A7" + } + ] + ] + } + } + } + } + }, + "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/aws-kinesisanalytics-flink/test/integ.vpc-application.js.snapshot/VpcTestDefaultTestDeployAssert06A9965C.assets.json b/packages/@aws-cdk/aws-kinesisanalytics-flink/test/integ.vpc-application.js.snapshot/VpcTestDefaultTestDeployAssert06A9965C.assets.json new file mode 100644 index 0000000000000..e03abd70970a9 --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisanalytics-flink/test/integ.vpc-application.js.snapshot/VpcTestDefaultTestDeployAssert06A9965C.assets.json @@ -0,0 +1,19 @@ +{ + "version": "30.1.0", + "files": { + "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { + "source": { + "path": "VpcTestDefaultTestDeployAssert06A9965C.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/aws-kinesisanalytics-flink/test/integ.vpc-application.js.snapshot/VpcTestDefaultTestDeployAssert06A9965C.template.json b/packages/@aws-cdk/aws-kinesisanalytics-flink/test/integ.vpc-application.js.snapshot/VpcTestDefaultTestDeployAssert06A9965C.template.json new file mode 100644 index 0000000000000..ad9d0fb73d1dd --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisanalytics-flink/test/integ.vpc-application.js.snapshot/VpcTestDefaultTestDeployAssert06A9965C.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/aws-kinesisanalytics-flink/test/integ.vpc-application.js.snapshot/asset.8be9e0b5f53d41e9a3b1d51c9572c65f24f8170a7188d0ed57fb7d571de4d577/WordCount.jar b/packages/@aws-cdk/aws-kinesisanalytics-flink/test/integ.vpc-application.js.snapshot/asset.8be9e0b5f53d41e9a3b1d51c9572c65f24f8170a7188d0ed57fb7d571de4d577/WordCount.jar new file mode 100644 index 0000000000000000000000000000000000000000..9c533e6fea60771f319c0f4923cdbf728bdc72e0 GIT binary patch literal 15192 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