diff --git a/packages/@aws-cdk/mixins-preview/NOTICE b/packages/@aws-cdk/mixins-preview/NOTICE index 289f76aa621ed..14e7543089795 100644 --- a/packages/@aws-cdk/mixins-preview/NOTICE +++ b/packages/@aws-cdk/mixins-preview/NOTICE @@ -6,111 +6,3 @@ Copyright 2018-2026 Amazon.com, Inc. or its affiliates. All Rights Reserved. The AWS CDK includes the following third-party software/licensing: ---------------- - -** minimatch - https://www.npmjs.com/package/minimatch - -Blue Oak Model License - -Version 1.0.0 - -Purpose - -This license gives everyone as much permission to work with -this software as possible, while protecting contributors -from liability. - -Acceptance - -In order to receive this license, you must agree to its -rules. The rules of this license are both obligations -under that agreement and conditions to your license. -You must not do anything with this software that triggers -a rule that you cannot or will not follow. - -Copyright - -Each contributor licenses you to do everything with this -software that would otherwise infringe that contributor's -copyright in it. - -Notices - -You must ensure that everyone who gets a copy of -any part of this software from you, with or without -changes, also gets the text of this license or a link to -https://blueoakcouncil.org/license/1.0.0. - -Excuse - -If anyone notifies you in writing that you have not -complied with Notices, you can keep your -license by taking all practical steps to comply within 30 -days after the notice. If you do not do so, your license -ends immediately. - -Patent - -Each contributor licenses you to do everything with this -software that would otherwise infringe any patent claims -they can license or become able to license. - -Reliability - -No contributor can revoke this license. - -No Liability - -As far as the law allows, this software comes as is, -without any warranty or condition, and no contributor -will be liable to anyone for any damages related to this -software or this license, under any kind of legal claim. - ----------------- - -** brace-expansion - https://www.npmjs.com/package/brace-expansion -Copyright Julian Gruber -TypeScript port Copyright Isaac Z. Schlueter - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ----------------- - -** balanced-match - https://www.npmjs.com/package/balanced-match -Original code Copyright Julian Gruber -Port to TypeScript Copyright Isaac Z. Schlueter - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ----------------- diff --git a/packages/@aws-cdk/mixins-preview/README.md b/packages/@aws-cdk/mixins-preview/README.md index f85fff9078327..89358edb96328 100644 --- a/packages/@aws-cdk/mixins-preview/README.md +++ b/packages/@aws-cdk/mixins-preview/README.md @@ -15,6 +15,9 @@ +> **Note**: The core Mixins mechanism (`Mixins`, `Mixin`, `IMixin`, `MixinApplicator`, `ConstructSelector`) is now available in `constructs` and `aws-cdk-lib/core`. Please update your imports. +> This package continues to provide additional preview features until they move to their final destinations. + This package provides two main features: 1. **Mixins** - Composable abstractions for adding functionality to constructs @@ -22,85 +25,25 @@ This package provides two main features: --- -## CDK Mixins - CDK Mixins provide a new, advanced way to add functionality through composable abstractions. Unlike traditional L2 constructs that bundle all features together, Mixins allow you to pick and choose exactly the capabilities you need for constructs. -### Key Benefits +## Key Benefits + +CDK Mixins offer a well-defined way to build self-contained constructs features. +Mixins are applied during or after construct construction. * **Universal Compatibility**: Apply the same abstractions to L1 constructs, L2 constructs, or custom constructs -* **Composable Design**: Mix and match features without being locked into specific implementations +* **Composable Design**: Mix and match features without being locked into specific implementations * **Cross-Service Abstractions**: Use common patterns like encryption across different AWS services * **Escape Hatch Freedom**: Customize resources in a safe, typed way while keeping the abstractions you want -### Basic Usage - -Mixins use `Mixins.of()` as the fundamental API for applying abstractions to constructs: - -```typescript -// Base form: apply mixins to any construct -const bucket = new s3.CfnBucket(scope, "MyBucket"); -Mixins.of(bucket) - .apply(new EncryptionAtRest()) - .apply(new AutoDeleteObjects()); -``` - -#### Fluent Syntax with `.with()` - -For convenience, you can use the `.with()` method for a more fluent syntax: - -```typescript -import '@aws-cdk/mixins-preview/with'; - -const bucket = new s3.CfnBucket(scope, "MyBucket") - .with(new BucketVersioning()) - .with(new AutoDeleteObjects()); -``` - -The `.with()` method is available after importing `@aws-cdk/mixins-preview/with`, which augments all constructs with this method. It provides the same functionality as `Mixins.of().apply()` but with a more chainable API. - -> **Note**: The `.with()` fluent syntax is only available in JavaScript and TypeScript. Other jsii languages (Python, Java, C#, and Go) should use the `Mixins.of(...).requireAll()` syntax instead. The import requirement is temporary during the preview phase. Once the API is stable, the `.with()` method will be available by default on all constructs and in all languages. - -### Creating Custom Mixins - -Mixins are simple classes that implement the `IMixin` interface (usually by extending the abstract `Mixin` class: +Mixins are an _addition_, _not_ a replacement for construct properties. +By itself, they cannot change optionality of properties or change defaults. -```typescript -// Simple mixin that enables versioning -class CustomVersioningMixin extends Mixin implements IMixin { - supports(construct: any): boolean { - return construct instanceof s3.CfnBucket; - } - - applyTo(bucket: any): void { - bucket.versioningConfiguration = { - status: "Enabled" - }; - } -} - -// Usage -const bucket = new s3.CfnBucket(scope, "MyBucket"); -Mixins.of(bucket).apply(new CustomVersioningMixin()); -``` - -### Construct Selection - -Mixins operate on construct trees and can be applied selectively: - -```typescript -// Apply to all constructs in a scope -Mixins.of(scope).apply(new EncryptionAtRest()); +### Usage and documentation -// Apply to specific resource types -Mixins.of(scope, ConstructSelector.resourcesOfType(s3.CfnBucket.CFN_RESOURCE_TYPE_NAME)) - .apply(new EncryptionAtRest()); - -// Apply to constructs matching a path pattern -Mixins.of(scope, ConstructSelector.byPath("**/*-prod-*/**")) - .apply(new ProductionSecurityMixin()); -``` +See the [documentation for `aws-cdk-lib`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib-readme.html#mixins). ### Built-in Mixins @@ -110,11 +53,11 @@ Mixins.of(scope, ConstructSelector.byPath("**/*-prod-*/**")) ```typescript // Works across different resource types -const bucket = new s3.CfnBucket(scope, "Bucket"); -Mixins.of(bucket).apply(new EncryptionAtRest()); +const myBucket = new s3.CfnBucket(scope, "Bucket"); +Mixins.of(myBucket).apply(new EncryptionAtRest()); -const logGroup = new logs.CfnLogGroup(scope, "LogGroup"); -Mixins.of(logGroup).apply(new EncryptionAtRest()); +const myLogGroup = new logs.CfnLogGroup(scope, "LogGroup"); +Mixins.of(myLogGroup).apply(new EncryptionAtRest()); ``` #### S3-Specific Mixins @@ -122,29 +65,27 @@ Mixins.of(logGroup).apply(new EncryptionAtRest()); **AutoDeleteObjects**: Configures automatic object deletion for S3 buckets ```typescript -const bucket = new s3.CfnBucket(scope, "Bucket"); -Mixins.of(bucket).apply(new AutoDeleteObjects()); +const myBucket = new s3.CfnBucket(scope, "Bucket"); +Mixins.of(myBucket).apply(new AutoDeleteObjects()); ``` **BucketVersioning**: Enables versioning on S3 buckets ```typescript -const bucket = new s3.CfnBucket(scope, "Bucket"); -Mixins.of(bucket).apply(new BucketVersioning()); +const myBucket = new s3.CfnBucket(scope, "Bucket"); +Mixins.of(myBucket).apply(new BucketVersioning()); ``` **BucketBlockPublicAccess**: Enables blocking public-access on S3 buckets ```typescript -const bucket = new s3.CfnBucket(scope, "Bucket"); -Mixins.of(bucket).apply(new BucketBlockPublicAccess()); +const myBucket = new s3.CfnBucket(scope, "Bucket"); +Mixins.of(myBucket).apply(new BucketBlockPublicAccess()); ``` **BucketPolicyStatementsMixin**: Adds IAM policy statements to a bucket policy ```typescript -declare const bucket: s3.IBucketRef; - const bucketPolicy = new s3.CfnBucketPolicy(scope, "BucketPolicy", { bucket: bucket, policyDocument: new iam.PolicyDocument(), @@ -178,14 +119,13 @@ Mixins.of(cluster).apply(new ClusterSettings([{ Configures vended logs delivery for supported resources to various destinations: ```typescript -import '@aws-cdk/mixins-preview/with'; import * as cloudfrontMixins from '@aws-cdk/mixins-preview/aws-cloudfront/mixins'; // Create CloudFront distribution -declare const bucket: s3.Bucket; +declare const origin: s3.IBucket; const distribution = new cloudfront.Distribution(scope, 'Distribution', { defaultBehavior: { - origin: origins.S3BucketOrigin.withOriginAccessControl(bucket), + origin: origins.S3BucketOrigin.withOriginAccessControl(origin), }, }); @@ -212,10 +152,10 @@ import '@aws-cdk/mixins-preview/with'; import * as cloudfrontMixins from '@aws-cdk/mixins-preview/aws-cloudfront/mixins'; // Create CloudFront distribution -declare const bucket: s3.Bucket; +declare const origin: s3.IBucket; const distribution = new cloudfront.Distribution(scope, 'Distribution', { defaultBehavior: { - origin: origins.S3BucketOrigin.withOriginAccessControl(bucket), + origin: origins.S3BucketOrigin.withOriginAccessControl(origin), }, }); @@ -272,10 +212,10 @@ const sourceStack = new Stack(app, 'source-stack', { }); // Create CloudFront distribution -declare const bucket: s3.Bucket; +declare const origin: s3.IBucket; const distribution = new cloudfront.Distribution(sourceStack, 'Distribution', { defaultBehavior: { - origin: origins.S3BucketOrigin.withOriginAccessControl(bucket), + origin: origins.S3BucketOrigin.withOriginAccessControl(origin), }, }); @@ -292,8 +232,7 @@ For every CloudFormation resource, CDK Mixins automatically generates type-safe ```typescript import '@aws-cdk/mixins-preview/with'; - -const bucket = new s3.Bucket(scope, "Bucket") +new s3.Bucket(scope, "Bucket") .with(new CfnBucketPropsMixin({ versioningConfiguration: { status: "Enabled" }, publicAccessBlockConfiguration: { @@ -306,8 +245,6 @@ const bucket = new s3.Bucket(scope, "Bucket") Property mixins support two merge strategies: ```typescript -declare const bucket: s3.CfnBucket; - // MERGE (default): Deep merges properties with existing values Mixins.of(bucket).apply(new CfnBucketPropsMixin( { versioningConfiguration: { status: "Enabled" } }, @@ -358,8 +295,8 @@ import * as events from 'aws-cdk-lib/aws-events'; import * as targets from 'aws-cdk-lib/aws-events-targets'; // Works with L2 constructs -const bucket = new s3.Bucket(scope, 'Bucket'); -const bucketEvents = BucketEvents.fromBucket(bucket); +const myBucket = new s3.Bucket(scope, 'Bucket'); +const bucketEvents = BucketEvents.fromBucket(myBucket); declare const fn: lambda.Function; new events.Rule(scope, 'Rule', { @@ -389,7 +326,6 @@ new events.CfnRule(scope, 'CfnRule', { ```typescript import { BucketEvents } from '@aws-cdk/mixins-preview/aws-s3/events'; -declare const bucket: s3.Bucket; const bucketEvents = BucketEvents.fromBucket(bucket); // Bucket name is automatically injected from the bucket reference @@ -403,7 +339,6 @@ const pattern = bucketEvents.objectCreatedPattern(); import { BucketEvents } from '@aws-cdk/mixins-preview/aws-s3/events'; import * as events from 'aws-cdk-lib/aws-events'; -declare const bucket: s3.Bucket; const bucketEvents = BucketEvents.fromBucket(bucket); const pattern = bucketEvents.objectCreatedPattern({ diff --git a/packages/@aws-cdk/mixins-preview/lib/core/private/metadata.ts b/packages/@aws-cdk/mixins-preview/lib/core/private/metadata.ts deleted file mode 100644 index 17e2745d60314..0000000000000 --- a/packages/@aws-cdk/mixins-preview/lib/core/private/metadata.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { IConstruct } from 'constructs'; -import type { IMixin } from '../mixins'; - -const MIXIN_METADATA_KEY = 'aws:cdk:analytics:mixin'; -const JSII_RUNTIME_SYMBOL = Symbol.for('jsii.rtti'); -const ALLOWED_FQN_PREFIXES: ReadonlyArray = [ - // SCOPES - '@aws-cdk/', '@aws-cdk-containers/', '@aws-solutions-konstruk/', '@aws-solutions-constructs/', '@amzn/', '@cdklabs/', - // PACKAGES - 'aws-rfdk.', 'aws-cdk-lib.', 'cdk8s.', -]; - -export function addMetadata(construct: IConstruct, mixin: IMixin) { - let reportedValue = '*'; - const fqn = Object.getPrototypeOf(mixin).constructor[JSII_RUNTIME_SYMBOL]?.fqn; - if (fqn && ALLOWED_FQN_PREFIXES.find(prefix => fqn.startsWith(prefix))) { - reportedValue = fqn; - } - construct.node.addMetadata(MIXIN_METADATA_KEY, { mixin: reportedValue }); -} - -export function applyMixin(construct: IConstruct, mixin: IMixin) { - addMetadata(construct, mixin); - mixin.applyTo(construct); -} diff --git a/packages/@aws-cdk/mixins-preview/lib/index.ts b/packages/@aws-cdk/mixins-preview/lib/index.ts index e73b069f125c5..83368ac172401 100644 --- a/packages/@aws-cdk/mixins-preview/lib/index.ts +++ b/packages/@aws-cdk/mixins-preview/lib/index.ts @@ -1,3 +1,2 @@ -export * as core from './core'; export * as mixins from './mixins'; export * from './services'; diff --git a/packages/@aws-cdk/mixins-preview/lib/services/aws-ecs/cluster.ts b/packages/@aws-cdk/mixins-preview/lib/services/aws-ecs/cluster.ts index 37e69cf8c1637..dd5b17120c2fc 100644 --- a/packages/@aws-cdk/mixins-preview/lib/services/aws-ecs/cluster.ts +++ b/packages/@aws-cdk/mixins-preview/lib/services/aws-ecs/cluster.ts @@ -1,4 +1,4 @@ -import { Mixin } from '../../core'; +import { Mixin } from 'aws-cdk-lib/core'; import { CfnCluster } from 'aws-cdk-lib/aws-ecs'; import type { IConstruct } from 'constructs/lib/construct'; diff --git a/packages/@aws-cdk/mixins-preview/lib/services/aws-logs/logs-destination.ts b/packages/@aws-cdk/mixins-preview/lib/services/aws-logs/logs-destination.ts index 658feb39ca0fb..fbe11269f77c0 100644 --- a/packages/@aws-cdk/mixins-preview/lib/services/aws-logs/logs-destination.ts +++ b/packages/@aws-cdk/mixins-preview/lib/services/aws-logs/logs-destination.ts @@ -1,9 +1,9 @@ -import { Aws, type IEnvironmentAware, Names, Stack, Tags } from 'aws-cdk-lib/core'; +import type { IEnvironmentAware } from 'aws-cdk-lib/core'; +import { Aws, ConstructSelector, Mixins, Names, Stack, Tags } from 'aws-cdk-lib/core'; import * as logs from 'aws-cdk-lib/aws-logs'; import * as s3 from 'aws-cdk-lib/aws-s3'; import type { Construct, IConstruct } from 'constructs'; import { tryFindBucketPolicyForBucket, tryFindKmsKeyConstruct, tryFindKmsKeyforBucket } from '../../mixins/private/reflections'; -import { ConstructSelector, Mixins } from '../../core'; import { BucketPolicyStatementsMixin } from '../aws-s3/bucket-policy'; import { AccountPrincipal, Effect, PolicyDocument, PolicyStatement, ServicePrincipal } from 'aws-cdk-lib/aws-iam'; import type { CfnKey, IKeyRef } from 'aws-cdk-lib/aws-kms'; diff --git a/packages/@aws-cdk/mixins-preview/lib/services/aws-s3/bucket-policy.ts b/packages/@aws-cdk/mixins-preview/lib/services/aws-s3/bucket-policy.ts index fc7c1b8234845..c980da37c17dd 100644 --- a/packages/@aws-cdk/mixins-preview/lib/services/aws-s3/bucket-policy.ts +++ b/packages/@aws-cdk/mixins-preview/lib/services/aws-s3/bucket-policy.ts @@ -1,5 +1,5 @@ import type { IConstruct } from 'constructs/lib/construct'; -import { Mixin } from '../../core'; +import { Mixin } from 'aws-cdk-lib/core'; import type { PolicyStatement } from 'aws-cdk-lib/aws-iam'; import { PolicyDocument } from 'aws-cdk-lib/aws-iam'; import { CfnBucketPolicy } from 'aws-cdk-lib/aws-s3'; diff --git a/packages/@aws-cdk/mixins-preview/lib/services/aws-s3/bucket.ts b/packages/@aws-cdk/mixins-preview/lib/services/aws-s3/bucket.ts index 738ee396d7267..546afde059a4b 100644 --- a/packages/@aws-cdk/mixins-preview/lib/services/aws-s3/bucket.ts +++ b/packages/@aws-cdk/mixins-preview/lib/services/aws-s3/bucket.ts @@ -2,7 +2,7 @@ import type { IConstruct } from 'constructs'; import * as s3 from 'aws-cdk-lib/aws-s3'; import { CfnResource, CustomResource, Tags } from 'aws-cdk-lib/core'; import { AutoDeleteObjectsProvider } from '../../custom-resource-handlers/aws-s3/auto-delete-objects-provider'; -import type { IMixin } from '../../core'; +import type { IMixin } from 'constructs'; import { tryFindBucketPolicyForBucket } from '../../mixins/private/reflections'; const AUTO_DELETE_OBJECTS_RESOURCE_TYPE = 'Custom::S3AutoDeleteObjects'; diff --git a/packages/@aws-cdk/mixins-preview/lib/with.ts b/packages/@aws-cdk/mixins-preview/lib/with.ts index df9db070f7054..964f169ab8037 100644 --- a/packages/@aws-cdk/mixins-preview/lib/with.ts +++ b/packages/@aws-cdk/mixins-preview/lib/with.ts @@ -1,33 +1,3 @@ -import type { IConstruct } from 'constructs'; -import { Construct } from 'constructs'; -import type { IMixin } from './core'; -import { applyMixin } from './core/private/metadata'; - -declare module 'constructs' { - interface IConstruct { - with(...mixin: IMixin[]): this; - } - - interface Construct { - with(...mixin: IMixin[]): this; - } -} - -// Hack the prototype to add .with() method -// Changed the loop order so that mixins are applied in order. -// The list of constructs is now captured at the start of the call, -// ensuring that constructs added by a mixin will not be visited by subsequent mixins. -// -// This code is a hard copy of code from this PR https://github.com/aws/constructs/pull/2843 -// and is intended to be removed once the code is available in upstream. -(Construct.prototype as any).with = function(this: IConstruct, ...mixins: IMixin[]): IConstruct { - const allConstructs = this.node.findAll(); - for (const mixin of mixins) { - for (const construct of allConstructs) { - if (mixin.supports(construct)) { - applyMixin(construct, mixin); - } - } - } - return this; -}; +// empty on purpose +// this is now native to the constructs package +// we keep this file so that existing imports of it do nothing but don't fail diff --git a/packages/@aws-cdk/mixins-preview/package.json b/packages/@aws-cdk/mixins-preview/package.json index 8dedc749c581e..eed1626c13a5c 100644 --- a/packages/@aws-cdk/mixins-preview/package.json +++ b/packages/@aws-cdk/mixins-preview/package.json @@ -673,12 +673,7 @@ "jest": "^29.7.0", "tsx": "^4.21.0" }, - "dependencies": { - "minimatch": "^10.2.2" - }, - "bundleDependencies": [ - "minimatch" - ], + "dependencies": {}, "peerDependencies": { "aws-cdk-lib": "^0.0.0", "constructs": "^10.5.0" diff --git a/packages/@aws-cdk/mixins-preview/rosetta/default.ts-fixture b/packages/@aws-cdk/mixins-preview/rosetta/default.ts-fixture index abae491f33222..c4d57a4d7a979 100644 --- a/packages/@aws-cdk/mixins-preview/rosetta/default.ts-fixture +++ b/packages/@aws-cdk/mixins-preview/rosetta/default.ts-fixture @@ -1,4 +1,4 @@ -import { Construct } from 'constructs'; +import { Construct, IConstruct } from 'constructs'; import { Stack, App } from 'aws-cdk-lib/core'; import * as s3 from 'aws-cdk-lib/aws-s3'; import * as logs from 'aws-cdk-lib/aws-logs'; @@ -8,7 +8,8 @@ import * as cloudfront from 'aws-cdk-lib/aws-cloudfront'; import * as origins from 'aws-cdk-lib/aws-cloudfront-origins'; import * as iam from 'aws-cdk-lib/aws-iam'; import '@aws-cdk/mixins-preview/with'; -import { ConstructSelector, Mixins, Mixin, IMixin } from '@aws-cdk/mixins-preview/core'; +import { Mixins, Mixin, IConstructSelector } from 'aws-cdk-lib/core'; +import { IMixin } from 'constructs'; import { PropertyMergeStrategy } from '@aws-cdk/mixins-preview/mixins'; import { AutoDeleteObjects, BucketVersioning, CfnBucketPropsMixin, BucketPolicyStatementsMixin, BucketBlockPublicAccess } from '@aws-cdk/mixins-preview/aws-s3/mixins'; @@ -17,20 +18,38 @@ import { mixins as alexa_mixins } from '@aws-cdk/mixins-preview/alexa-ask'; import { aws_logs } from '@aws-cdk/mixins-preview'; declare const scope: Construct; +declare const stack: Stack; +declare const selector: IConstructSelector; -class MyMixin extends Mixins { +class MyMixin extends Mixin { supports(_construct: any): boolean { return true; } applyTo(_construct: any): void { } } -class ProductionSecurityMixin extends Mixins { +class ProductionSecurityMixin extends Mixin { supports(_construct: any): boolean { return true; } applyTo(_construct: any): void { } } -class EncryptionAtRest extends Mixins { +class EncryptionAtRest extends Mixin { supports(_construct: any): boolean { return true; } applyTo(_construct: any): void { } } +class CustomBucket extends Construct {} + + +declare const bucketAccessLogsMixin: IMixin; +declare const bucket: s3.CfnBucket; +declare const queue: Construct; + +interface MyEncryptionAtRestProps { + bucketKey?: string; + algorithm?: string; +} + +function isKmsEncrypted(x: any): boolean { + return true; +} + /// here diff --git a/packages/@aws-cdk/mixins-preview/scripts/spec2logs/builder.ts b/packages/@aws-cdk/mixins-preview/scripts/spec2logs/builder.ts index d88bf5ba4126a..f4180591f4844 100644 --- a/packages/@aws-cdk/mixins-preview/scripts/spec2logs/builder.ts +++ b/packages/@aws-cdk/mixins-preview/scripts/spec2logs/builder.ts @@ -5,10 +5,9 @@ import type { Method } from '@cdklabs/typewriter'; import { Module, ExternalModule, ClassType, Stability, Type, expr, stmt, ThingSymbol, $this, CallableProxy, NewExpression, $E, $T, EnumType, InterfaceType } from '@cdklabs/typewriter'; import { MIXINS_LOGS_DELIVERY } from './helpers'; import type { ServiceSubmoduleProps, LocatedModule } from '@aws-cdk/spec2cdk/lib/cdk/service-submodule'; -import { BaseServiceSubmodule, relativeImportPath } from '@aws-cdk/spec2cdk/lib/cdk/service-submodule'; +import { BaseServiceSubmodule } from '@aws-cdk/spec2cdk/lib/cdk/service-submodule'; import type { AddServiceProps, LibraryBuilderProps } from '@aws-cdk/spec2cdk/lib/cdk/library-builder'; import { LibraryBuilder } from '@aws-cdk/spec2cdk/lib/cdk/library-builder'; -import { MIXINS_CORE } from '../spec2mixins/helpers'; import { ResourceReference } from '@aws-cdk/spec2cdk/lib/cdk/reference-props'; class LogsDeliveryBuilderServiceModule extends BaseServiceSubmodule { @@ -72,7 +71,6 @@ export class LogsDeliveryBuilder extends LibraryBuilder { CDK_CORE.import(module, 'cdk'); CONSTRUCTS.import(module, 'constructs'); - MIXINS_CORE.import(module, 'core', { fromLocation: relativeImportPath(filePath, '../core') }); MIXINS_COMMON.import(module, 'mixins', { fromLocation: '../../mixins' }); MIXINS_UTILS.import(module, 'helpers', { fromLocation: '../../util/property-mixins' }); submodule.constructLibModule.import(module, 'service'); @@ -94,8 +93,8 @@ class L1PropsMixin extends ClassType { super(scope, { export: true, name: `${naming.classNameFromResource(resource)}PropsMixin`, - implements: [MIXINS_CORE.IMixin], - extends: MIXINS_CORE.Mixin, + implements: [CONSTRUCTS.IMixin], + extends: CDK_CORE.Mixin, docs: { summary: `L1 property mixin for ${resource.cloudFormationType}`, ...util.splitDocumentation(resource.documentation), diff --git a/packages/@aws-cdk/mixins-preview/scripts/spec2mixins/helpers.ts b/packages/@aws-cdk/mixins-preview/scripts/spec2mixins/helpers.ts index 41c52f2109e59..7ba4f5ddae605 100644 --- a/packages/@aws-cdk/mixins-preview/scripts/spec2mixins/helpers.ts +++ b/packages/@aws-cdk/mixins-preview/scripts/spec2mixins/helpers.ts @@ -1,10 +1,5 @@ import { Type, ExternalModule, $T, $E, expr, ThingSymbol } from '@cdklabs/typewriter'; -class MixinsCore extends ExternalModule { - public readonly IMixin = Type.fromName(this, 'IMixin'); - public readonly Mixin = Type.fromName(this, 'Mixin'); -} - class MixinsCommon extends ExternalModule { public readonly PropertyMergeStrategy = $T(Type.fromName(this, 'PropertyMergeStrategy')); public readonly CfnPropertyMixinOptions = Type.fromName(this, 'CfnPropertyMixinOptions'); @@ -15,6 +10,5 @@ class MixinsUtils extends ExternalModule { public readonly shallowAssign = $E(expr.sym(new ThingSymbol('shallowAssign', this))); } -export const MIXINS_CORE = new MixinsCore('@aws-cdk/mixins-preview/core'); export const MIXINS_COMMON = new MixinsCommon('@aws-cdk/mixins-preview/mixins'); export const MIXINS_UTILS = new MixinsUtils('@aws-cdk/mixins-preview/util/property-mixins'); diff --git a/packages/@aws-cdk/mixins-preview/test/codegen/__snapshots__/logs-delivery.test.ts.snap b/packages/@aws-cdk/mixins-preview/test/codegen/__snapshots__/logs-delivery.test.ts.snap index 2079474c8e44e..40ca3777c1163 100644 --- a/packages/@aws-cdk/mixins-preview/test/codegen/__snapshots__/logs-delivery.test.ts.snap +++ b/packages/@aws-cdk/mixins-preview/test/codegen/__snapshots__/logs-delivery.test.ts.snap @@ -5,7 +5,6 @@ exports[`Logs Delivery Mixin for a resource 1`] = ` import * as cdk from "aws-cdk-lib/core"; import * as interfaces from "aws-cdk-lib/interfaces"; import * as constructs from "constructs"; -import * as core from "../../core"; import * as logsDelivery from "../aws-logs/logs-delivery"; import * as service from "aws-cdk-lib/aws-some"; @@ -163,7 +162,7 @@ export class CfnThingAccessLogs { * @stability external * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-some-resource.html */ -export class CfnThingLogsMixin extends core.Mixin implements core.IMixin { +export class CfnThingLogsMixin extends cdk.Mixin implements constructs.IMixin { public static readonly APPLICATION_LOGS: CfnThingApplicationLogs = new CfnThingApplicationLogs(); public static readonly ACCESS_LOGS: CfnThingAccessLogs = new CfnThingAccessLogs(); diff --git a/packages/@aws-cdk/mixins-preview/test/codegen/__snapshots__/resources.test.ts.snap b/packages/@aws-cdk/mixins-preview/test/codegen/__snapshots__/resources.test.ts.snap index 450af8e044575..93c76f967d023 100644 --- a/packages/@aws-cdk/mixins-preview/test/codegen/__snapshots__/resources.test.ts.snap +++ b/packages/@aws-cdk/mixins-preview/test/codegen/__snapshots__/resources.test.ts.snap @@ -4,7 +4,6 @@ exports[`L1 property mixin for a standard-issue resource 1`] = ` "/* eslint-disable prettier/prettier, @stylistic/max-len */ import * as cdk from "aws-cdk-lib/core"; import * as constructs from "constructs"; -import * as core from "../../core"; import * as mixins from "../../mixins"; import * as helpers from "../../util/property-mixins"; import * as service from "aws-cdk-lib/aws-some"; @@ -15,7 +14,7 @@ import * as service from "aws-cdk-lib/aws-some"; * @stability external * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-some-resource.html */ -export class CfnThingPropsMixin extends core.Mixin implements core.IMixin { +export class CfnThingPropsMixin extends cdk.Mixin implements constructs.IMixin { protected static readonly CFN_PROPERTY_KEYS: Array = ["config", "externalId", "id"]; protected readonly props: CfnThingMixinProps; diff --git a/packages/@aws-cdk/mixins-preview/test/mixins/full-example.ts b/packages/@aws-cdk/mixins-preview/test/mixins/full-example.ts index 1dbff2cea49d1..6a837537cfb95 100644 --- a/packages/@aws-cdk/mixins-preview/test/mixins/full-example.ts +++ b/packages/@aws-cdk/mixins-preview/test/mixins/full-example.ts @@ -5,7 +5,7 @@ import * as logs from 'aws-cdk-lib/aws-logs'; import { Mixins, ConstructSelector, -} from '../../lib/core'; +} from 'aws-cdk-lib/core'; import * as s3Mixins from '../../lib/services/aws-s3/mixins'; describe('Integration Tests', () => { diff --git a/packages/@aws-cdk/mixins-preview/test/with.test.ts b/packages/@aws-cdk/mixins-preview/test/with.test.ts deleted file mode 100644 index 87d87db277ed5..0000000000000 --- a/packages/@aws-cdk/mixins-preview/test/with.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Stack, App } from 'aws-cdk-lib/core'; -import { Template } from 'aws-cdk-lib/assertions'; -import * as s3 from 'aws-cdk-lib/aws-s3'; -import '../lib/with'; -import * as s3Mixins from '../lib/services/aws-s3/mixins'; - -describe('Mixin Extensions', () => { - let app: App; - let stack: Stack; - - beforeEach(() => { - app = new App(); - stack = new Stack(app, 'TestStack'); - }); - - describe('.with() method', () => { - test('can chain multiple mixins', () => { - const bucket = new s3.CfnBucket(stack, 'Bucket') - .with(new s3Mixins.BucketVersioning()) - .with(new s3Mixins.AutoDeleteObjects()); - - const versionConfig = bucket.versioningConfiguration as any; - expect(versionConfig?.status).toBe('Enabled'); - - const template = Template.fromStack(stack); - template.hasResourceProperties('Custom::S3AutoDeleteObjects', { - BucketName: { Ref: 'Bucket' }, - }); - }); - - test('can apply multiple mixins', () => { - const bucket = new s3.CfnBucket(stack, 'Bucket') - .with(new s3Mixins.BucketVersioning(), new s3Mixins.AutoDeleteObjects()); - - const versionConfig = bucket.versioningConfiguration as any; - expect(versionConfig?.status).toBe('Enabled'); - - const template = Template.fromStack(stack); - template.hasResourceProperties('Custom::S3AutoDeleteObjects', { - BucketName: { Ref: 'Bucket' }, - }); - }); - - test('returns the same construct', () => { - const bucket = new s3.CfnBucket(stack, 'Bucket'); - const result = bucket.with(new s3Mixins.BucketVersioning()); - - expect(result).toBe(bucket); - }); - }); -}); diff --git a/packages/aws-cdk-lib/README.md b/packages/aws-cdk-lib/README.md index 6b06af44feace..ea26d46c36364 100644 --- a/packages/aws-cdk-lib/README.md +++ b/packages/aws-cdk-lib/README.md @@ -525,7 +525,7 @@ Namespaces](https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespace in the AWS General Reference. Some L1 constructs also have an auto-generated static `arnFor()` -method that can be used to generate ARNs for resources of that type. For example, +method that can be used to generate ARNs for resources of that type. For example, `sns.Topic.arnForTopic(topic)` can be used to generate an ARN for a given topic. Note that the parameter to this method is of type `ITopicRef`, which means that it can be used with both `Topic` (L2) and `CfnTopic` (L1) constructs. @@ -1764,6 +1764,238 @@ Annotations.of(this).addInfoV2('my-lib:Construct.someInfo', 'Some message explai Annotations.of(this).acknowledgeInfo('my-lib:Construct.someInfo', 'This info can be ignored'); ``` +## Mixins + +CDK Mixins provide a new, advanced way to add functionality through composable abstractions. +Unlike traditional L2 constructs that bundle all features together, Mixins allow you to pick and choose exactly the capabilities you need for constructs. + +Mixins are an *addition*, not a replacement for construct properties. +They are applied during or after construct construction using the `.with()` method: + +```ts fixture=README-mixins +// Apply mixins fluently with .with() +new s3.CfnBucket(scope, "MyL1Bucket") + .with(new EncryptionAtRest()) + .with(new AutoDeleteObjects()); + +// Apply multiple mixins to the same construct +new s3.CfnBucket(scope, "MyL1Bucket") + .with(new EncryptionAtRest(), new AutoDeleteObjects()); + +// Mixins work with all types of constructs: +// L1, L2 and even custom constructs +new s3.Bucket(stack, 'MyL2Bucket').with(new EncryptionAtRest()); +new CustomBucket(stack, 'MyCustomBucket').with(new EncryptionAtRest()); +``` + +There is an alternative form available that allows additional, advanced configuration of Mixin application: `Mixins.of()`. + +```ts fixture=README-mixins +import { ConstructSelector } from "aws-cdk-lib/core"; + +// Basic: Apply mixins to any construct, calls can be chained +const myBucket = new s3.CfnBucket(scope, "MyBucket"); +Mixins.of(myBucket) + .apply(new EncryptionAtRest()) + .apply(new AutoDeleteObjects()); + +// Basic: Or multiple Mixins passed to apply +Mixins.of(myBucket) + .apply(new EncryptionAtRest(), new AutoDeleteObjects()); + +// Advanced: Apply to constructs matching a selector, e.g. match by ID +Mixins.of( + scope, + ConstructSelector.byId("prod/**") +).apply(new ProductionSecurityMixin()); + +// Advanced: Require a mixin to be applied to every node in the construct tree +Mixins.of(stack) + .apply(new ProductionSecurityMixin()) + .requireAll(); +``` + +### How Mixins are applied + +Each construct has a `with()` method and Mixins will be applied to all nodes of the construct. +Sometimes more control is needed. +Especially when authoring construct libraries, it may be desirable to have full control over the Mixin application process. +Think of the L3 pattern again: How can you encode the rules to which Mixins may or may not be applied in your L3? +This is where `Mixins.of()` and the `MixinApplicator` class come in. +They provide more complex ways to select targets, apply Mixins and set expectations. + +#### Mixin application on construct trees + +When working with construct trees like Stacks (as opposed to single resources), +`Mixins.of()` offers a more comprehensive API to configure how Mixins are applied. +By default, Mixins are applied to all supported constructs in the tree: + +```ts fixture=README-mixins +// Apply to all constructs in a scope +Mixins.of(scope).apply(new EncryptionAtRest()); +``` + +Optionally, you may select specific constructs: + +```ts fixture=README-mixins +import { ConstructSelector } from "aws-cdk-lib/core"; + +// Apply to a given L1 resource or L2 resource construct +Mixins.of( + bucket, + ConstructSelector.cfnResource() // provided CfnResource or a CfnResource default child +).apply(new EncryptionAtRest()); + +// Apply to all resources of a specific type +Mixins.of( + scope, + ConstructSelector.resourcesOfType(s3.CfnBucket.CFN_RESOURCE_TYPE_NAME) +).apply(new EncryptionAtRest()); + +// Alternative: select by CloudFormation resource type name +Mixins.of( + scope, + ConstructSelector.resourcesOfType("AWS::S3::Bucket") +).apply(new EncryptionAtRest()); + +// Apply to constructs matching a pattern +Mixins.of( + scope, + ConstructSelector.byId("prod/**") +).apply(new ProductionSecurityMixin()); + +// The default is to apply to all constructs in the scope +Mixins.of( + scope, + ConstructSelector.all() // pass through to IConstruct.findAll() +).apply(new ProductionSecurityMixin()); +``` + +#### Mixins that must be used + +Sometimes you need assertions that a Mixin has been applied to certain set of constructs. +`Mixins.of(...)` keeps track of Mixin applications and this report can be used to create assertions. + +It comes with two convenience helpers: +Use `requireAll()` to assert the Mixin will be applied to all selected constructs. +If a construct is in the selection that is not supported by the Mixin, this will throw an error. +The `requireAny()` helper will assert the Mixin was applied to at least one construct from the selection. +If the Mixin wasn't applied to any construct at all, this will throw an error. + +Both helpers will only check future calls of `apply()`. +Set them before calling `apply()` to take effect. + +```ts fixture=README-mixins +Mixins.of(scope, selector) + // Assert Mixin was applied to all constructs in the selection + .requireAll() + // Or assert Mixin was applied to at least one construct in the selection + // .requireAny() + .apply(new EncryptionAtRest()); + +// Get an application report for manual assertions +const report = Mixins.of(scope).apply(new EncryptionAtRest()).report; +``` + +### Creating Custom Mixins + +Mixins are simple classes that implement the `IMixin` interface (usually by extending the abstract `Mixin` class): + +```ts fixture=README-mixins +class EnableVersioning extends Mixin implements IMixin { + supports(construct: any): construct is s3.CfnBucket { + return s3.CfnBucket.isCfnBucket(construct); + } + + applyTo(bucket: IConstruct): void { + (bucket as s3.CfnBucket).versioningConfiguration = { + status: "Enabled" + }; + } +} + +// Usage +new s3.CfnBucket(scope, "MyBucket") + .with(new EnableVersioning()); +``` + +We recommend to implement Mixins at the L1 level and to have them target a specific resource construct. +This way, the same Mixin can be applied to constructs from all levels. + +When applied, the `.supports()` method is used to decided if a Mixin can be applied to a given construct. +Depending on the application method (see below), the Mixin is then applied, skipped or an error is thrown. + +```ts fixture=README-mixins +bucketAccessLogsMixin.supports(bucket); // returns `true` +bucketAccessLogsMixin.supports(queue); // returns `false` +``` + +#### Validation with Mixins + +Mixins have two distinct phases: Initialization and application. +During initialization only the Mixin's input properties are available, but during application we also have access the target construct. + +Mixins should validate their properties and targets as early as possible. +During initialization validate all input properties. +Then during application validate any target dependent pre-conditions or interactions with Mixin properties. + +Like with constructs, Mixins should *throw an error* in case of unrecoverable failures and use *annotations* for recoverable ones. +It is best practices to collect errors and throw as a group whenever possible. +Mixins can attach *[lazy validators](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md#attaching-lazy-validators)* to the target construct. +Use this to ensure a certain property is met at end of an app's execution. + +```ts fixture=README-mixins +class MyEncryptionAtRest extends Mixin { + constructor(props: MyEncryptionAtRestProps = {}) { + super(); + // Validate Mixin props at construction time + if (props.bucketKey && props.algorithm === 'aws:kms:dsse') { + throw new Error("Cannot use S3 Bucket Key and DSSE together"); + } + } + + supports(construct: any): construct is s3.CfnBucket { + return s3.CfnBucket.isCfnBucket(construct); + } + + applyTo(target: s3.CfnBucket): s3.CfnBucket { + // Validate pre-conditions on the target, throw if error is unrecoverable + if (!target.bucketEncryption) { + throw new Error("Bucket encryption not configured"); + } + + // Validate properties are met after app execution + target.node.addValidation({ + validate: () => isKmsEncrypted(target) + ? ['This bucket must use aws:kms encryption.'] + : [] + }); + + target.bucketEncryption = { + serverSideEncryptionConfiguration: [{ + bucketKeyEnabled: true, + serverSideEncryptionByDefault: { + sseAlgorithm: "aws:kms" + } + }] + }; + return target; + } +} +``` + +#### Mixins and Aspects + +Mixins and Aspects are similar concepts and both are implementations of the [visitor pattern](https://en.wikipedia.org/wiki/Visitor_pattern). +They crucially differ in their time of application: + +- Mixins are always applied *immediately*, they are a tool of *imperative* programming. +- Aspects are applied *after* all other code during the synthesis phase, this makes them *declarative*. + +Both Mixins and Aspects have valid use cases and complement each other. +We recommend to use Mixins to *make changes*, and to use Aspects to *validate behaviors*. +Aspects may also be used when changes need to apply to *future additions*, for examples in custom libraries. + ## Aspects [Aspects](https://docs.aws.amazon.com/cdk/v2/guide/aspects.html) is a feature in CDK that allows you to apply operations or transformations across all diff --git a/packages/aws-cdk-lib/core/lib/cfn-element.ts b/packages/aws-cdk-lib/core/lib/cfn-element.ts index 1756f957fb533..55e01dbf763ee 100644 --- a/packages/aws-cdk-lib/core/lib/cfn-element.ts +++ b/packages/aws-cdk-lib/core/lib/cfn-element.ts @@ -78,6 +78,10 @@ export abstract class CfnElement extends Construct { } } + public with(...mixins: IMixin[]): IConstruct { + return withMixins(this, ...mixins); + } + /** * Overrides the auto-generated logical ID with a specific ID. * @param newLogicalId The new logical ID to use for this stack element. @@ -207,4 +211,6 @@ function notTooLong(x: string) { import { CfnReference } from './private/cfn-reference'; import { Stack } from './stack'; import { Token } from './token';import { ValidationError } from './errors'; +import type { IConstruct, IMixin } from 'constructs'; +import { withMixins } from './mixins/private/mixin-metadata'; diff --git a/packages/aws-cdk-lib/core/lib/index.ts b/packages/aws-cdk-lib/core/lib/index.ts index fb8325d22d51c..b20a126ac733c 100644 --- a/packages/aws-cdk-lib/core/lib/index.ts +++ b/packages/aws-cdk-lib/core/lib/index.ts @@ -1,6 +1,8 @@ export * from './aspect'; export * from './tag-aspect'; +export * from './mixins'; + export * from './token'; export * from './resolvable'; export * from './type-hints'; diff --git a/packages/@aws-cdk/mixins-preview/lib/core/applicator.ts b/packages/aws-cdk-lib/core/lib/mixins/applicator.ts similarity index 91% rename from packages/@aws-cdk/mixins-preview/lib/core/applicator.ts rename to packages/aws-cdk-lib/core/lib/mixins/applicator.ts index 716cce05dbd69..ac54b08f183d0 100644 --- a/packages/@aws-cdk/mixins-preview/lib/core/applicator.ts +++ b/packages/aws-cdk-lib/core/lib/mixins/applicator.ts @@ -1,9 +1,8 @@ -import type { IConstruct } from 'constructs'; -import { ValidationError } from 'aws-cdk-lib/core'; -import type { IMixin } from './mixins'; +import type { IConstruct, IMixin } from 'constructs'; +import { ValidationError } from '../errors'; +import { applyMixin } from './private/mixin-metadata'; import { ConstructSelector, type IConstructSelector } from './selectors'; -import { applyMixin } from './private/metadata'; -import { memoizedGetter } from 'aws-cdk-lib/core/lib/helpers-internal'; +import { memoizedGetter } from '../helpers-internal'; /** * Represents a successful mixin application. diff --git a/packages/@aws-cdk/mixins-preview/lib/core/index.ts b/packages/aws-cdk-lib/core/lib/mixins/index.ts similarity index 59% rename from packages/@aws-cdk/mixins-preview/lib/core/index.ts rename to packages/aws-cdk-lib/core/lib/mixins/index.ts index 4d35b575e4910..87f30ed5b3e7b 100644 --- a/packages/@aws-cdk/mixins-preview/lib/core/index.ts +++ b/packages/aws-cdk-lib/core/lib/mixins/index.ts @@ -1,4 +1,3 @@ -// Re-export all core functionality from separate modules export * from './mixins'; export * from './selectors'; export * from './applicator'; diff --git a/packages/@aws-cdk/mixins-preview/lib/core/mixins.ts b/packages/aws-cdk-lib/core/lib/mixins/mixins.ts similarity index 59% rename from packages/@aws-cdk/mixins-preview/lib/core/mixins.ts rename to packages/aws-cdk-lib/core/lib/mixins/mixins.ts index 098aaab02f5c4..bd69a4c16d0ff 100644 --- a/packages/@aws-cdk/mixins-preview/lib/core/mixins.ts +++ b/packages/aws-cdk-lib/core/lib/mixins/mixins.ts @@ -1,9 +1,8 @@ -import type { IConstruct } from 'constructs'; -import type { IConstructSelector } from './selectors'; +import type { IConstruct, IMixin } from 'constructs'; import { MixinApplicator } from './applicator'; +import type { IConstructSelector } from './selectors'; -// this will change when we update the interface to deliberately break compatibility checks -const MIXIN_SYMBOL = Symbol.for('@aws-cdk/mixins-preview.Mixin.pre1'); +const MIXIN_SYMBOL = Symbol.for('@aws-cdk/core.Mixin'); /** * Main entry point for applying mixins. @@ -17,22 +16,6 @@ export class Mixins { } } -/** - * A mixin is a reusable piece of functionality that can be applied to constructs - * to add behavior, properties, or modify existing functionality without inheritance. - */ -export interface IMixin { - /** - * Determines whether this mixin can be applied to the given construct. - */ - supports(construct: IConstruct): boolean; - - /** - * Applies the mixin functionality to the target construct. - */ - applyTo(construct: IConstruct): void; -} - /** * Abstract base class for mixins that provides default implementations. */ diff --git a/packages/aws-cdk-lib/core/lib/mixins/private/mixin-metadata.ts b/packages/aws-cdk-lib/core/lib/mixins/private/mixin-metadata.ts new file mode 100644 index 0000000000000..38a5ddd1910c7 --- /dev/null +++ b/packages/aws-cdk-lib/core/lib/mixins/private/mixin-metadata.ts @@ -0,0 +1,40 @@ +// THIS FILE CANNOT HAVE IMPORTS EXCEPT TYPES AND CONSTANTS +// OTHERWISE WE WILL GET CIRCULAR DEPENDENCY ISSUES +import type { IConstruct, IMixin } from 'constructs'; +import { JSII_RUNTIME_SYMBOL } from '../../constants'; +import { ALLOWED_FQN_PREFIXES } from '../../private/constants'; + +const MIXIN_METADATA_KEY = 'aws:cdk:analytics:mixin'; + +function addMetadata(construct: IConstruct, mixin: IMixin) { + let reportedValue = '*'; + const fqn = Object.getPrototypeOf(mixin).constructor[JSII_RUNTIME_SYMBOL]?.fqn; + if (fqn && ALLOWED_FQN_PREFIXES.find(prefix => fqn.startsWith(prefix))) { + reportedValue = fqn; + } + construct.node.addMetadata(MIXIN_METADATA_KEY, { mixin: reportedValue }); +} + +/** + * Apply a mixin with metadata + */ +export function applyMixin(construct: IConstruct, mixin: IMixin) { + addMetadata(construct, mixin); + mixin.applyTo(construct); +} + +/** + * Canonical internal implementation of `.with()` that also emits metadata + * Must be separate from MixinApplicator to prevent circular dependency issues + */ +export function withMixins(target: IConstruct, ...mixins: IMixin[]) { + const allConstructs = target.node.findAll(); + for (const mixin of mixins) { + for (const construct of allConstructs) { + if (mixin.supports(construct)) { + applyMixin(construct, mixin); + } + } + } + return target; +} diff --git a/packages/@aws-cdk/mixins-preview/lib/core/selectors.ts b/packages/aws-cdk-lib/core/lib/mixins/selectors.ts similarity index 98% rename from packages/@aws-cdk/mixins-preview/lib/core/selectors.ts rename to packages/aws-cdk-lib/core/lib/mixins/selectors.ts index 004266f5ff237..8fa8f5c2cd8e4 100644 --- a/packages/@aws-cdk/mixins-preview/lib/core/selectors.ts +++ b/packages/aws-cdk-lib/core/lib/mixins/selectors.ts @@ -1,5 +1,5 @@ import type { IConstruct, Node } from 'constructs'; -import { CfnResource } from 'aws-cdk-lib/core'; +import { CfnResource } from '../cfn-resource'; /** * Selects constructs from a construct tree. diff --git a/packages/aws-cdk-lib/core/lib/private/constants.ts b/packages/aws-cdk-lib/core/lib/private/constants.ts new file mode 100644 index 0000000000000..89238f8f36875 --- /dev/null +++ b/packages/aws-cdk-lib/core/lib/private/constants.ts @@ -0,0 +1,9 @@ +/** + * We filter to only ever report on these constructs + */ +export const ALLOWED_FQN_PREFIXES: ReadonlyArray = [ + // SCOPES + '@aws-cdk/', '@aws-cdk-containers/', '@aws-solutions-konstruk/', '@aws-solutions-constructs/', '@amzn/', '@cdklabs/', + // PACKAGES + 'aws-rfdk.', 'aws-cdk-lib.', 'cdk8s.', +]; diff --git a/packages/aws-cdk-lib/core/lib/private/stack-metadata.ts b/packages/aws-cdk-lib/core/lib/private/stack-metadata.ts index a7f6c7a843147..f005d8ee1116a 100644 --- a/packages/aws-cdk-lib/core/lib/private/stack-metadata.ts +++ b/packages/aws-cdk-lib/core/lib/private/stack-metadata.ts @@ -8,14 +8,7 @@ import { MetadataType } from '../metadata-type'; import type { Resource } from '../resource'; import { Stage } from '../stage'; import type { IPolicyValidationPluginBeta1 } from '../validation'; - -// We filter to only ever report on these constructs -const ALLOWED_FQN_PREFIXES: ReadonlyArray = [ - // SCOPES - '@aws-cdk/', '@aws-cdk-containers/', '@aws-solutions-konstruk/', '@aws-solutions-constructs/', '@amzn/', '@cdklabs/', - // PACKAGES - 'aws-rfdk.', 'aws-cdk-lib.', 'cdk8s.', -]; +import { ALLOWED_FQN_PREFIXES } from './constants'; // These metadata types are always included const ALLOWED_METADATA_TYPES: ReadonlySet = new Set([ diff --git a/packages/aws-cdk-lib/core/lib/resource.ts b/packages/aws-cdk-lib/core/lib/resource.ts index 745af1c59d086..29c963d23576b 100644 --- a/packages/aws-cdk-lib/core/lib/resource.ts +++ b/packages/aws-cdk-lib/core/lib/resource.ts @@ -1,4 +1,5 @@ import { Construct } from 'constructs'; +import type { IConstruct, IMixin } from 'constructs'; import type { ArnComponents } from './arn'; import { Arn, ArnFormat } from './arn'; import { CfnResource } from './cfn-resource'; @@ -14,10 +15,7 @@ import type { IResolveContext } from './resolvable'; import { Stack } from './stack'; import { Token, Tokenization } from './token'; import type { IEnvironmentAware, ResourceEnvironment } from '../../interfaces/environment-aware'; - -// v2 - leave this as a separate section so it reduces merge conflicts when compat is removed -// eslint-disable-next-line import/order -import type { IConstruct } from 'constructs'; +import { withMixins } from './mixins/private/mixin-metadata'; /** * Interface for L2 Resource constructs. @@ -171,6 +169,10 @@ export abstract class Resource extends Construct implements IResource { }; } + public with(...mixins: IMixin[]): IConstruct { + return withMixins(this, ...mixins); + } + /** * Returns a string-encoded token that resolves to the physical name that * should be passed to the CloudFormation resource. diff --git a/packages/@aws-cdk/mixins-preview/test/core/metadata.test.ts b/packages/aws-cdk-lib/core/test/mixins/mixin-metadata.test.ts similarity index 95% rename from packages/@aws-cdk/mixins-preview/test/core/metadata.test.ts rename to packages/aws-cdk-lib/core/test/mixins/mixin-metadata.test.ts index f2fc2f8ad5710..6c55d0536f5bf 100644 --- a/packages/@aws-cdk/mixins-preview/test/core/metadata.test.ts +++ b/packages/aws-cdk-lib/core/test/mixins/mixin-metadata.test.ts @@ -1,6 +1,6 @@ import { Construct } from 'constructs'; -import { App, Stack } from 'aws-cdk-lib/core'; -import { Mixin, Mixins } from '../../lib/core'; +import { App, Stack } from '../../lib'; +import { Mixin, Mixins } from '../../lib/mixins'; const JSII_RUNTIME_SYMBOL = Symbol.for('jsii.rtti'); const MIXIN_METADATA_KEY = 'aws:cdk:analytics:mixin'; diff --git a/packages/@aws-cdk/mixins-preview/test/core/mixin.test.ts b/packages/aws-cdk-lib/core/test/mixins/mixin.test.ts similarity index 96% rename from packages/@aws-cdk/mixins-preview/test/core/mixin.test.ts rename to packages/aws-cdk-lib/core/test/mixins/mixin.test.ts index ca8350e4ea1f5..01b3557460123 100644 --- a/packages/@aws-cdk/mixins-preview/test/core/mixin.test.ts +++ b/packages/aws-cdk-lib/core/test/mixins/mixin.test.ts @@ -1,5 +1,5 @@ import { Construct } from 'constructs'; -import { Mixin } from '../../lib/core'; +import { Mixin } from '../../lib/mixins'; class TestConstruct extends Construct { constructor(scope: Construct, id: string) { diff --git a/packages/@aws-cdk/mixins-preview/test/core/mixins.test.ts b/packages/aws-cdk-lib/core/test/mixins/mixins.test.ts similarity index 86% rename from packages/@aws-cdk/mixins-preview/test/core/mixins.test.ts rename to packages/aws-cdk-lib/core/test/mixins/mixins.test.ts index 98d69d8b86730..b2d2e69e1f2ff 100644 --- a/packages/@aws-cdk/mixins-preview/test/core/mixins.test.ts +++ b/packages/aws-cdk-lib/core/test/mixins/mixins.test.ts @@ -1,12 +1,8 @@ -import { Construct, type IConstruct } from 'constructs'; -import { Stack, App } from 'aws-cdk-lib/core'; -import * as s3 from 'aws-cdk-lib/aws-s3'; -import * as logs from 'aws-cdk-lib/aws-logs'; -import type { IMixin } from '../../lib/core'; -import { - Mixin, - Mixins, -} from '../../lib/core'; +import { Construct, type IMixin, type IConstruct } from 'constructs'; +import { CfnLogGroup } from '../../../aws-logs'; +import { CfnBucket } from '../../../aws-s3'; +import { Stack, App } from '../../lib'; +import { Mixin, Mixins } from '../../lib/mixins'; class Root extends Construct { constructor() { @@ -29,7 +25,7 @@ class TestMixin extends Mixin { class SelectiveMixin implements IMixin { supports(_construct: any): boolean { - return _construct instanceof s3.CfnBucket; + return _construct instanceof CfnBucket; } applyTo(construct: any): any { @@ -76,8 +72,8 @@ describe('Core Mixins Framework', () => { }); test('selective mixin only applies to supported constructs', () => { - const bucket = new s3.CfnBucket(stack, 'Bucket'); - const logGroup = new logs.CfnLogGroup(stack, 'LogGroup'); + const bucket = new CfnBucket(stack, 'Bucket'); + const logGroup = new CfnLogGroup(stack, 'LogGroup'); const mixin = new SelectiveMixin(); expect(mixin.supports(bucket)).toBe(true); @@ -115,8 +111,8 @@ describe('Core Mixins Framework', () => { }); test('skips unsupported constructs', () => { - const bucket = new s3.CfnBucket(stack, 'Bucket'); - const logGroup = new logs.CfnLogGroup(stack, 'LogGroup'); + const bucket = new CfnBucket(stack, 'Bucket'); + const logGroup = new CfnLogGroup(stack, 'LogGroup'); const mixin = new SelectiveMixin(); Mixins.of(stack).apply(mixin); @@ -125,7 +121,7 @@ describe('Core Mixins Framework', () => { }); test('requireAny throws when no constructs match', () => { - new logs.CfnLogGroup(stack, 'LogGroup'); + new CfnLogGroup(stack, 'LogGroup'); const mixin = new SelectiveMixin(); expect(() => { @@ -134,8 +130,8 @@ describe('Core Mixins Framework', () => { }); test('requireAll throws when some constructs do not support mixin', () => { - new s3.CfnBucket(stack, 'Bucket'); - new logs.CfnLogGroup(stack, 'LogGroup'); + new CfnBucket(stack, 'Bucket'); + new CfnLogGroup(stack, 'LogGroup'); const mixin = new SelectiveMixin(); expect(() => { @@ -144,8 +140,8 @@ describe('Core Mixins Framework', () => { }); test('report returns successful mixin applications', () => { - const bucket = new s3.CfnBucket(stack, 'Bucket'); - new logs.CfnLogGroup(stack, 'LogGroup'); + const bucket = new CfnBucket(stack, 'Bucket'); + new CfnLogGroup(stack, 'LogGroup'); const mixin = new SelectiveMixin(); const applicator = Mixins.of(stack).apply(mixin); diff --git a/packages/@aws-cdk/mixins-preview/test/core/selectors.test.ts b/packages/aws-cdk-lib/core/test/mixins/selectors.test.ts similarity index 71% rename from packages/@aws-cdk/mixins-preview/test/core/selectors.test.ts rename to packages/aws-cdk-lib/core/test/mixins/selectors.test.ts index 723f4550d8925..5eb8b73b19579 100644 --- a/packages/@aws-cdk/mixins-preview/test/core/selectors.test.ts +++ b/packages/aws-cdk-lib/core/test/mixins/selectors.test.ts @@ -1,8 +1,8 @@ import { Construct } from 'constructs'; -import { Stack, App } from 'aws-cdk-lib/core'; -import * as s3 from 'aws-cdk-lib/aws-s3'; -import * as logs from 'aws-cdk-lib/aws-logs'; -import { ConstructSelector } from '../../lib/core'; +import { CfnLogGroup } from '../../../aws-logs'; +import { Bucket, CfnBucket } from '../../../aws-s3'; +import { Stack, App } from '../../lib'; +import { ConstructSelector } from '../../lib/mixins/selectors'; class TestConstruct extends Construct { constructor(scope: Construct, id: string) { @@ -30,17 +30,17 @@ describe('ConstructSelector', () => { }); test('resourcesOfType() selects by type', () => { - const bucket = new s3.CfnBucket(stack, 'Bucket'); - const logGroup = new logs.CfnLogGroup(stack, 'LogGroup'); + const bucket = new CfnBucket(stack, 'Bucket'); + const logGroup = new CfnLogGroup(stack, 'LogGroup'); - const selected = ConstructSelector.resourcesOfType(s3.CfnBucket.CFN_RESOURCE_TYPE_NAME).select(stack); + const selected = ConstructSelector.resourcesOfType(CfnBucket.CFN_RESOURCE_TYPE_NAME).select(stack); expect(selected).toContain(bucket); expect(selected).not.toContain(logGroup); }); test('resourcesOfType() selects by CloudFormation type', () => { - const bucket = new s3.CfnBucket(stack, 'Bucket'); - const logGroup = new logs.CfnLogGroup(stack, 'LogGroup'); + const bucket = new CfnBucket(stack, 'Bucket'); + const logGroup = new CfnLogGroup(stack, 'LogGroup'); const selected = ConstructSelector.resourcesOfType('AWS::S3::Bucket').select(stack); expect(selected).toContain(bucket); @@ -48,8 +48,8 @@ describe('ConstructSelector', () => { }); test('byId() selects by ID pattern', () => { - const prodBucket = new s3.CfnBucket(stack, 'prod-bucket'); - const devBucket = new s3.CfnBucket(stack, 'dev-bucket'); + const prodBucket = new CfnBucket(stack, 'prod-bucket'); + const devBucket = new CfnBucket(stack, 'dev-bucket'); const selected = ConstructSelector.byId('*prod*').select(stack); expect(selected).toContain(prodBucket); @@ -58,8 +58,8 @@ describe('ConstructSelector', () => { test('byPath() selects by construct path pattern', () => { const scope = new Construct(stack, 'Prefix'); - const prodBucket = new s3.CfnBucket(scope, 'prod-bucket'); - const devBucket = new s3.CfnBucket(stack, 'dev-bucket'); + const prodBucket = new CfnBucket(scope, 'prod-bucket'); + const devBucket = new CfnBucket(stack, 'dev-bucket'); const selected = ConstructSelector.byPath('*/Prefix/**').select(stack); expect(selected).toContain(prodBucket); @@ -67,8 +67,8 @@ describe('ConstructSelector', () => { }); test('cfnResource() selects CfnResource or default child', () => { - const bucket = new s3.CfnBucket(stack, 'Bucket'); - const l2Bucket = new s3.Bucket(stack, 'L2Bucket'); + const bucket = new CfnBucket(stack, 'Bucket'); + const l2Bucket = new Bucket(stack, 'L2Bucket'); const selectedFromCfn = ConstructSelector.cfnResource().select(bucket); expect(selectedFromCfn).toContain(bucket); diff --git a/packages/aws-cdk-lib/rosetta/README-mixins.ts-fixture b/packages/aws-cdk-lib/rosetta/README-mixins.ts-fixture new file mode 100644 index 0000000000000..6d32745df599c --- /dev/null +++ b/packages/aws-cdk-lib/rosetta/README-mixins.ts-fixture @@ -0,0 +1,54 @@ +import { Construct, IConstruct } from 'constructs'; +import { Stack, App } from 'aws-cdk-lib/core'; +import * as s3 from 'aws-cdk-lib/aws-s3'; +import * as logs from 'aws-cdk-lib/aws-logs'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; +import * as cloudfront from 'aws-cdk-lib/aws-cloudfront'; +import * as origins from 'aws-cdk-lib/aws-cloudfront-origins'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import '@aws-cdk/mixins-preview/with'; +import { Mixins, Mixin, IConstructSelector } from 'aws-cdk-lib/core'; +import { IMixin } from 'constructs'; + +declare const scope: Construct; +declare const stack: Stack; +declare const selector: IConstructSelector; + +class MyMixin extends Mixin { + supports(_construct: any): boolean { return true; } + applyTo(_construct: any): void { } +} + +class ProductionSecurityMixin extends Mixin { + supports(_construct: any): boolean { return true; } + applyTo(_construct: any): void { } +} + +class EncryptionAtRest extends Mixin { + supports(_construct: any): boolean { return true; } + applyTo(_construct: any): void { } +} + +class AutoDeleteObjects extends Mixin { + supports(_construct: any): boolean { return true; } + applyTo(_construct: any): void { } +} + +class CustomBucket extends Construct {} + + +declare const bucketAccessLogsMixin: IMixin; +declare const bucket: s3.CfnBucket; +declare const queue: Construct; + +interface MyEncryptionAtRestProps { + bucketKey?: string; + algorithm?: string; +} + +function isKmsEncrypted(x: any): boolean { + return true; +} + +/// here diff --git a/packages/aws-cdk-lib/rosetta/default.ts-fixture b/packages/aws-cdk-lib/rosetta/default.ts-fixture index 520442e0e82bd..2c8b400edc6af 100644 --- a/packages/aws-cdk-lib/rosetta/default.ts-fixture +++ b/packages/aws-cdk-lib/rosetta/default.ts-fixture @@ -12,6 +12,7 @@ import * as sqs from 'aws-cdk-lib/aws-sqs'; import * as s3 from 'aws-cdk-lib/aws-s3'; import * as api from 'aws-cdk-lib/aws-apigateway'; import * as rds from 'aws-cdk-lib/aws-rds'; +import * as logs from 'aws-cdk-lib/aws-logs'; import { propertyInjectable } from 'aws-cdk-lib/core/lib/prop-injectable'; import { InjectionContext, IPropertyInjector, PropertyInjectors } from 'aws-cdk-lib/core'; import { @@ -32,6 +33,7 @@ import { CfnParameter, CfnResource, CfnResourceProps, + ConstructSelector, CustomResource, CustomResourceProvider, CustomResourceProviderRuntime, @@ -43,6 +45,8 @@ import { PermissionsBoundary, RemovalPolicy, RemovalPolicies, + Mixin, + Mixins, MissingRemovalPolicies, Resource, SecretValue, @@ -58,6 +62,7 @@ import { } from 'aws-cdk-lib'; import { IConstruct, + IMixin, Construct, DependencyGroup, } from 'constructs'; @@ -98,6 +103,21 @@ class MyAspect implements IAspect { visit(node: IConstruct) {} } +class MyMixin extends Mixin { + supports(_construct: IConstruct): boolean { return true; } + applyTo(_construct: IConstruct): void { } +} + +class EncryptionAtRest extends Mixin { + supports(_construct: IConstruct): boolean { return true; } + applyTo(_construct: IConstruct): void { } +} + +class AutoDeleteObjects extends Mixin { + supports(_construct: IConstruct): boolean { return true; } + applyTo(_construct: IConstruct): void { } +} + class fixture$construct extends Construct { public constructor(scope: Construct, id: string) { super(scope, id); diff --git a/tools/@aws-cdk/spec2cdk/lib/cdk/cdk.ts b/tools/@aws-cdk/spec2cdk/lib/cdk/cdk.ts index db4e1f13e082c..e61416926cdcd 100644 --- a/tools/@aws-cdk/spec2cdk/lib/cdk/cdk.ts +++ b/tools/@aws-cdk/spec2cdk/lib/cdk/cdk.ts @@ -57,6 +57,8 @@ export class CdkCore extends ExternalModule { public readonly AWSEventMetadata = Type.fromName(this, 'AWSEventMetadata'); public readonly AWSEventMetadataProps = Type.fromName(this, 'AWSEventMetadataProps'); + public readonly Mixin = Type.fromName(this, 'Mixin'); + constructor(fqn: string) { super(fqn); } @@ -119,6 +121,7 @@ export class CdkErrors extends ExternalModule { export class Constructs extends ExternalModule { public readonly Construct = Type.fromName(this, 'Construct'); public readonly IConstruct = Type.fromName(this, 'IConstruct'); + public readonly IMixin = Type.fromName(this, 'IMixin'); constructor() { super('constructs');