@mettlecast/domain-cdk-packer 0.2.95 → 0.2.97
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/DomainStack.d.ts +17 -0
- package/dist/DomainStack.js +37 -26
- package/dist/__tests__/domain-stack.test.js +46 -0
- package/dist/pack-domain.d.ts +6 -0
- package/dist/pack-domain.js +1 -0
- package/package.json +1 -1
package/dist/DomainStack.d.ts
CHANGED
|
@@ -98,6 +98,23 @@ export interface DomainStackProps extends cdk.StackProps {
|
|
|
98
98
|
* Existing dashboards will be removed on the next CDK deploy.
|
|
99
99
|
*/
|
|
100
100
|
disableCloudWatchDashboards?: boolean;
|
|
101
|
+
/**
|
|
102
|
+
* When true, the per-domain DynamoDB table and S3 bucket are imported
|
|
103
|
+
* (adopted) instead of created. This is the migration-safe path for
|
|
104
|
+
* environments where those resources already exist OUTSIDE the CloudFormation
|
|
105
|
+
* stack (e.g. created manually or by an earlier deployment generation) —
|
|
106
|
+
* CloudFormation would otherwise fail the change-set with "already exists".
|
|
107
|
+
*
|
|
108
|
+
* Imported resources are not owned by the stack, so:
|
|
109
|
+
* - their data is preserved untouched;
|
|
110
|
+
* - CloudFormation can never accidentally delete them on stack deletion;
|
|
111
|
+
* - IAM grants, env vars (DOMAIN_TABLE_NAME / DOMAIN_BUCKET_NAME) and
|
|
112
|
+
* CfnOutputs keep referencing the same physical names.
|
|
113
|
+
*
|
|
114
|
+
* The deploy workflow detects pre-existing resources and passes the CDK
|
|
115
|
+
* context `adopt-existing-resources=true` (see workflowGenerator.ts).
|
|
116
|
+
*/
|
|
117
|
+
adoptExistingResources?: boolean;
|
|
101
118
|
}
|
|
102
119
|
export declare class DomainStack extends cdk.Stack {
|
|
103
120
|
/** Shared HTTP API for routing API and webhook requests. */
|
package/dist/DomainStack.js
CHANGED
|
@@ -75,7 +75,7 @@ export class DomainStack extends cdk.Stack {
|
|
|
75
75
|
throw new Error(`Registry validation failed for domain '${props.registry.domain?.id ?? '<unknown>'}':\n${lines.join('\n')}\n\n` +
|
|
76
76
|
`Fix registry data before deploying.`);
|
|
77
77
|
}
|
|
78
|
-
const { registry, eventBusArn, eventBusName, databaseUrl, dbSecretArn, appSecretArn, userPoolArn, userPoolId, userPoolClientId, internalSubnetSelection, internetSubnetSelection, enableCmk, enableWaf, enableAlarms, alarmSnsTopicArn, enableCanaryDeploy, reservedConcurrency, logRetentionDays, corsAllowedOrigins, allowCredentials, disableCloudWatchDashboards } = props;
|
|
78
|
+
const { registry, eventBusArn, eventBusName, databaseUrl, dbSecretArn, appSecretArn, userPoolArn, userPoolId, userPoolClientId, internalSubnetSelection, internetSubnetSelection, enableCmk, enableWaf, enableAlarms, alarmSnsTopicArn, enableCanaryDeploy, reservedConcurrency, logRetentionDays, corsAllowedOrigins, allowCredentials, disableCloudWatchDashboards, adoptExistingResources } = props;
|
|
79
79
|
const vpc = props.vpc ?? (props.vpcId
|
|
80
80
|
? ec2.Vpc.fromVpcAttributes(this, 'SharedVpc', {
|
|
81
81
|
vpcId: props.vpcId,
|
|
@@ -328,34 +328,45 @@ export class DomainStack extends cdk.Stack {
|
|
|
328
328
|
}
|
|
329
329
|
return { lambdas, byId };
|
|
330
330
|
};
|
|
331
|
-
// Per-domain DynamoDB table — PK=tenantId (tenant is primary partition), SK=entity key
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
331
|
+
// Per-domain DynamoDB table — PK=tenantId (tenant is primary partition), SK=entity key.
|
|
332
|
+
//
|
|
333
|
+
// Issue #5234: when `adoptExistingResources` is set (detected by the deploy
|
|
334
|
+
// workflow), the table already exists OUTSIDE this CloudFormation stack.
|
|
335
|
+
// Import it instead of creating it so the change-set does not fail with
|
|
336
|
+
// "resource already exists"; the imported table is not owned by the stack,
|
|
337
|
+
// so its data is preserved and CloudFormation can never delete it.
|
|
338
|
+
const domainTable = adoptExistingResources
|
|
339
|
+
? dynamodb.Table.fromTableName(this, 'DomainTable', resourceBaseName)
|
|
340
|
+
: new dynamodb.Table(this, 'DomainTable', {
|
|
341
|
+
tableName: resourceBaseName,
|
|
342
|
+
partitionKey: { name: 'tenantId', type: dynamodb.AttributeType.STRING },
|
|
343
|
+
sortKey: { name: 'sk', type: dynamodb.AttributeType.STRING },
|
|
344
|
+
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
|
|
345
|
+
encryption: enableCmk ? dynamodb.TableEncryption.CUSTOMER_MANAGED : dynamodb.TableEncryption.AWS_MANAGED,
|
|
346
|
+
encryptionKey: cmkKey,
|
|
347
|
+
timeToLiveAttribute: 'expiresAt',
|
|
348
|
+
removalPolicy: cdk.RemovalPolicy.RETAIN,
|
|
349
|
+
pointInTimeRecovery: true,
|
|
350
|
+
});
|
|
351
|
+
// Per-domain S3 bucket — all objects prefixed {tenantId}/ enforced in runtime.
|
|
344
352
|
const bucketName = resourceBaseName.length <= 63
|
|
345
353
|
? resourceBaseName
|
|
346
354
|
: `${resourceBaseName.slice(0, 55)}-${cdk.Fn.select(0, cdk.Fn.split('-', cdk.Names.uniqueId(this))).toLowerCase()}`;
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
355
|
+
// Issue #5234: same adoption path for the bucket as the table above.
|
|
356
|
+
const domainBucket = adoptExistingResources
|
|
357
|
+
? s3.Bucket.fromBucketName(this, 'DomainBucket', bucketName)
|
|
358
|
+
: new s3.Bucket(this, 'DomainBucket', {
|
|
359
|
+
bucketName,
|
|
360
|
+
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
|
|
361
|
+
encryption: enableCmk ? s3.BucketEncryption.KMS : s3.BucketEncryption.S3_MANAGED,
|
|
362
|
+
encryptionKey: cmkKey,
|
|
363
|
+
versioned: true,
|
|
364
|
+
enforceSSL: true,
|
|
365
|
+
removalPolicy: cdk.RemovalPolicy.RETAIN,
|
|
366
|
+
lifecycleRules: [{
|
|
367
|
+
noncurrentVersionExpiration: cdk.Duration.days(90),
|
|
368
|
+
}],
|
|
369
|
+
});
|
|
359
370
|
environment['DOMAIN_TABLE_NAME'] = domainTable.tableName;
|
|
360
371
|
environment['DOMAIN_BUCKET_NAME'] = domainBucket.bucketName;
|
|
361
372
|
// Per-domain tenant-scoped IAM role for defence-in-depth storage segregation
|
|
@@ -191,6 +191,52 @@ describe('DomainStack', () => {
|
|
|
191
191
|
template.hasOutput('DomainBucketName', {});
|
|
192
192
|
});
|
|
193
193
|
});
|
|
194
|
+
describe('adopt existing resources (#5234)', () => {
|
|
195
|
+
const app = new cdk.App();
|
|
196
|
+
const stack = new DomainStack(app, 'TestDomainStackAdopt', {
|
|
197
|
+
registry: minimalRegistry,
|
|
198
|
+
eventBusArn,
|
|
199
|
+
projectId: 'Test',
|
|
200
|
+
envCode: 'Dev',
|
|
201
|
+
adoptExistingResources: true,
|
|
202
|
+
});
|
|
203
|
+
const template = Template.fromStack(stack);
|
|
204
|
+
it('does NOT create a DynamoDB table — the pre-existing table is imported', () => {
|
|
205
|
+
template.resourceCountIs('AWS::DynamoDB::Table', 0);
|
|
206
|
+
});
|
|
207
|
+
it('does NOT create an S3 bucket — the pre-existing bucket is imported', () => {
|
|
208
|
+
template.resourceCountIs('AWS::S3::Bucket', 0);
|
|
209
|
+
});
|
|
210
|
+
it('keeps the CfnOutputs for the adopted table and bucket names', () => {
|
|
211
|
+
template.hasOutput('DomainTableName', {});
|
|
212
|
+
template.hasOutput('DomainBucketName', {});
|
|
213
|
+
});
|
|
214
|
+
it('sets DOMAIN_TABLE_NAME and DOMAIN_BUCKET_NAME env vars to the adopted names', () => {
|
|
215
|
+
const lambdas = template.findResources('AWS::Lambda::Function');
|
|
216
|
+
const domainLambdas = Object.entries(lambdas).filter(([id]) => !id.includes('Health') && !id.includes('Ready') && !id.includes('LogRetention'));
|
|
217
|
+
for (const [, res] of domainLambdas) {
|
|
218
|
+
expect(res.Properties.Environment?.Variables?.DOMAIN_TABLE_NAME).toBe('test-dev-test-domain');
|
|
219
|
+
expect(res.Properties.Environment?.Variables?.DOMAIN_BUCKET_NAME).toBe('test-dev-test-domain');
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
it('still grants Lambdas read/write on the adopted table and bucket', () => {
|
|
223
|
+
const policies = Object.values(template.findResources('AWS::IAM::Policy'));
|
|
224
|
+
const statementResources = (action) => policies.flatMap(p => p.Properties.PolicyDocument.Statement
|
|
225
|
+
.filter(s => s.Action?.includes(action))
|
|
226
|
+
.flatMap(s => {
|
|
227
|
+
const res = s.Resource;
|
|
228
|
+
if (Array.isArray(res))
|
|
229
|
+
return res.map(r => JSON.stringify(r));
|
|
230
|
+
if (res === undefined)
|
|
231
|
+
return [];
|
|
232
|
+
return [JSON.stringify(res)];
|
|
233
|
+
}));
|
|
234
|
+
// DynamoDB grant targets the imported table ARN (synthesized as Fn::Join).
|
|
235
|
+
expect(statementResources('dynamodb:PutItem').some(r => r.includes(':table/test-dev-test-domain'))).toBe(true);
|
|
236
|
+
// S3 grant targets the imported bucket ARN.
|
|
237
|
+
expect(statementResources('s3:PutObject').some(r => r.includes(':s3:::test-dev-test-domain/*'))).toBe(true);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
194
240
|
describe('tenant-scoped IAM role (defence in depth)', () => {
|
|
195
241
|
const app = new cdk.App();
|
|
196
242
|
const stack = new DomainStack(app, 'TestDomainStackTenant', { registry: minimalRegistry, eventBusArn, projectId: 'Test', envCode: 'Dev' });
|
package/dist/pack-domain.d.ts
CHANGED
|
@@ -57,6 +57,12 @@ export interface PackDomainOptions {
|
|
|
57
57
|
crossDomainActionArns?: Record<string, string>;
|
|
58
58
|
/** Disable auto-generated per-domain CloudWatch dashboard. */
|
|
59
59
|
disableCloudWatchDashboards?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* When true, the per-domain DynamoDB table and S3 bucket are imported
|
|
62
|
+
* (adopted) instead of created — the migration-safe path when those
|
|
63
|
+
* resources already exist outside the CloudFormation stack (#5234).
|
|
64
|
+
*/
|
|
65
|
+
adoptExistingResources?: boolean;
|
|
60
66
|
}
|
|
61
67
|
/**
|
|
62
68
|
* Convenience entry-point: constructs a DomainStack from a compiled registry.
|
package/dist/pack-domain.js
CHANGED
|
@@ -61,5 +61,6 @@ export function packDomain(registry, app, stackId, eventBusArn, options) {
|
|
|
61
61
|
envCode: opts.envCode,
|
|
62
62
|
crossDomainActionArns: opts.crossDomainActionArns,
|
|
63
63
|
disableCloudWatchDashboards: opts.disableCloudWatchDashboards,
|
|
64
|
+
adoptExistingResources: opts.adoptExistingResources,
|
|
64
65
|
});
|
|
65
66
|
}
|