@go-to-k/cdkd 0.232.0 → 0.234.0
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/README.md +1 -1
- package/dist/cli.js +223 -395
- package/dist/cli.js.map +1 -1
- package/dist/{deploy-engine-BkPZar1R.js → deploy-engine-Dj9mXUQa.js} +743 -7
- package/dist/deploy-engine-Dj9mXUQa.js.map +1 -0
- package/dist/index.d.ts +65 -46
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/deploy-engine-BkPZar1R.js.map +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { B as generateResourceName, E as SynthesisError, F as getLiveRenderer, M as getLogger, S as ResourceUpdateNotSupportedError, T as StateError, U as withStackName, V as generateResourceNameWithFallback, a as normalizeAwsTagsToCfn, b as ProvisioningError, c as AssetError, d as DependencyError, g as MacroExpansionError, h as LockError, i as matchesCdkPath, k as normalizeAwsError, l as CdkdError, n as isTerminationProtectionPropagationError, o as resolveExplicitPhysicalId, s as assertRegionMatch, t as disableInstanceApiTermination, x as ResourceTimeoutError, z as applyDefaultNameForFallback } from "./ec2-termination-protection-uvVnteGl.js";
|
|
2
2
|
import { r as getAwsClients } from "./aws-clients-pjPwZz1r.js";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
|
-
import { DeleteObjectCommand, DeleteObjectsCommand, GetBucketLocationCommand, GetObjectCommand, HeadBucketCommand, HeadObjectCommand, ListObjectsV2Command, NoSuchKey, PutObjectCommand, S3Client, S3ServiceException } from "@aws-sdk/client-s3";
|
|
4
|
+
import { CreateBucketCommand, DeleteObjectCommand, DeleteObjectsCommand, GetBucketLocationCommand, GetObjectCommand, HeadBucketCommand, HeadObjectCommand, ListObjectsV2Command, NoSuchKey, PutBucketEncryptionCommand, PutBucketPolicyCommand, PutObjectCommand, PutPublicAccessBlockCommand, S3Client, S3ServiceException } from "@aws-sdk/client-s3";
|
|
5
5
|
import { CloudControlClient, CreateResourceCommand, DeleteResourceCommand, GetResourceCommand, GetResourceRequestStatusCommand, ListResourcesCommand, UpdateResourceCommand } from "@aws-sdk/client-cloudcontrol";
|
|
6
6
|
import { AttachRolePolicyCommand, CreateRoleCommand, DeleteRoleCommand, DeleteRolePermissionsBoundaryCommand, DeleteRolePolicyCommand, DetachRolePolicyCommand, GetRoleCommand, GetRolePolicyCommand, IAMClient, ListAttachedRolePoliciesCommand, ListInstanceProfilesForRoleCommand, ListRolePoliciesCommand, ListRoleTagsCommand, ListRolesCommand, NoSuchEntityException, PutRolePermissionsBoundaryCommand, PutRolePolicyCommand, RemoveRoleFromInstanceProfileCommand, TagRoleCommand, UntagRoleCommand, UpdateAssumeRolePolicyCommand, UpdateRoleCommand } from "@aws-sdk/client-iam";
|
|
7
7
|
import { PublishCommand, SNSClient } from "@aws-sdk/client-sns";
|
|
@@ -24,7 +24,7 @@ import { DescribeListenersCommand, DescribeLoadBalancersCommand, ElasticLoadBala
|
|
|
24
24
|
import { KMSClient, ListAliasesCommand } from "@aws-sdk/client-kms";
|
|
25
25
|
import { readFile } from "fs/promises";
|
|
26
26
|
import { join as join$1 } from "path";
|
|
27
|
-
import { DescribeImagesCommand as DescribeImagesCommand$1, ECRClient, GetAuthorizationTokenCommand } from "@aws-sdk/client-ecr";
|
|
27
|
+
import { CreateRepositoryCommand, DescribeImagesCommand as DescribeImagesCommand$1, DescribeRepositoriesCommand, ECRClient, GetAuthorizationTokenCommand, PutImageTagMutabilityCommand } from "@aws-sdk/client-ecr";
|
|
28
28
|
import { hostname } from "os";
|
|
29
29
|
import graphlib from "graphlib";
|
|
30
30
|
import { DescribeDBClustersCommand, DescribeDBInstancesCommand, RDSClient } from "@aws-sdk/client-rds";
|
|
@@ -1774,6 +1774,22 @@ function resolveCaptureObservedState(cliValue) {
|
|
|
1774
1774
|
return true;
|
|
1775
1775
|
}
|
|
1776
1776
|
/**
|
|
1777
|
+
* Resolve the effective `--use-cdk-bootstrap-assets` value (issue #1002
|
|
1778
|
+
* PR 2, design §4.2): pin legacy asset destinations for one app even when
|
|
1779
|
+
* the region's bootstrap marker exists — for apps deployed via both
|
|
1780
|
+
* CloudFormation and cdkd during a migration window.
|
|
1781
|
+
*
|
|
1782
|
+
* Priority: CLI flag (`true` wins) > `cdk.json context.cdkd.useCdkBootstrapAssets`
|
|
1783
|
+
* (boolean) > default `false`. The CLI flag has no `--no-` negation form, so
|
|
1784
|
+
* a cdk.json `true` can only be overruled by removing the context entry —
|
|
1785
|
+
* matching the "per-app pin" semantics (the app is CFn-co-deployed or it
|
|
1786
|
+
* isn't; per-invocation flip-flop would churn stack properties).
|
|
1787
|
+
*/
|
|
1788
|
+
function resolveUseCdkBootstrapAssets(cliValue) {
|
|
1789
|
+
if (cliValue === true) return true;
|
|
1790
|
+
return (loadCdkJson()?.context?.["cdkd"])?.["useCdkBootstrapAssets"] === true;
|
|
1791
|
+
}
|
|
1792
|
+
/**
|
|
1777
1793
|
* Pre-parse argv walk that surfaces the deprecation warning when the
|
|
1778
1794
|
* user explicitly passes the legacy `--no-prefix-user-supplied-names`
|
|
1779
1795
|
* flag. Commander's auto-negation of `--prefix-user-supplied-names`
|
|
@@ -2896,6 +2912,705 @@ var DockerAssetPublisher = class {
|
|
|
2896
2912
|
}
|
|
2897
2913
|
};
|
|
2898
2914
|
|
|
2915
|
+
//#endregion
|
|
2916
|
+
//#region src/assets/asset-storage.ts
|
|
2917
|
+
/**
|
|
2918
|
+
* cdkd-owned asset storage — naming, bootstrap marker, and deploy-time
|
|
2919
|
+
* asset-mode detection (issue #1002, design at
|
|
2920
|
+
* docs/design/1002-cdkd-asset-storage.md).
|
|
2921
|
+
*
|
|
2922
|
+
* Why this exists: cdkd publishes assets to the CDK bootstrap bucket / ECR
|
|
2923
|
+
* repo, but `cdk gc` decides "in use" by scanning CloudFormation stack
|
|
2924
|
+
* templates — cdkd-deployed stacks have no CFn stack, so every cdkd-published
|
|
2925
|
+
* asset looks isolated and gets deleted. The fix is cdkd-owned asset storage
|
|
2926
|
+
* created by `cdkd bootstrap` (structurally out of `cdk gc`'s reach because
|
|
2927
|
+
* gc only discovers storage from the CDK bootstrap stack's outputs).
|
|
2928
|
+
*
|
|
2929
|
+
* The upgrade is strictly transparent: deploys behave byte-identically to
|
|
2930
|
+
* before until the user re-runs `cdkd bootstrap` for a region, which writes a
|
|
2931
|
+
* per-region marker object into the state bucket. Deploy reads the marker to
|
|
2932
|
+
* pick the mode; PR 2 of the phasing wires the actual publish redirection +
|
|
2933
|
+
* template rewrite onto the `cdkd-assets` mode resolved here.
|
|
2934
|
+
*/
|
|
2935
|
+
/** Marker schema version, bumped if the marker shape ever changes. */
|
|
2936
|
+
const ASSET_SUPPORT_VERSION = 1;
|
|
2937
|
+
/**
|
|
2938
|
+
* S3 key prefix for bootstrap markers in the state bucket. Deliberately
|
|
2939
|
+
* outside the `cdkd/` state prefix so `state list` key-listing never mistakes
|
|
2940
|
+
* a marker for a stack, and concurrent bootstraps in two regions cannot race
|
|
2941
|
+
* last-writer-wins on a shared object (design §12.1).
|
|
2942
|
+
*/
|
|
2943
|
+
const BOOTSTRAP_MARKER_PREFIX = "cdkd-bootstrap/";
|
|
2944
|
+
/**
|
|
2945
|
+
* Name of the cdkd-owned asset bucket for an (account, region) pair.
|
|
2946
|
+
* Per-region because Lambda requires the code bucket to be in the function's
|
|
2947
|
+
* region — the same reason CDK's asset bucket is per-region (design §3).
|
|
2948
|
+
*/
|
|
2949
|
+
function getCdkdAssetBucketName(accountId, region) {
|
|
2950
|
+
return `cdkd-assets-${accountId}-${region}`;
|
|
2951
|
+
}
|
|
2952
|
+
/**
|
|
2953
|
+
* Name of the cdkd-owned container-asset ECR repository for an
|
|
2954
|
+
* (account, region) pair. ECR repos are account+region scoped by ARN, so the
|
|
2955
|
+
* suffix is not strictly needed — the CDK-parallel shape keeps the future
|
|
2956
|
+
* template rewrite uniform and the names self-describing (design §3).
|
|
2957
|
+
*/
|
|
2958
|
+
function getCdkdContainerRepoName(accountId, region) {
|
|
2959
|
+
return `cdkd-container-assets-${accountId}-${region}`;
|
|
2960
|
+
}
|
|
2961
|
+
/**
|
|
2962
|
+
* State-bucket key of the bootstrap marker for a region.
|
|
2963
|
+
*/
|
|
2964
|
+
function getBootstrapMarkerKey(region) {
|
|
2965
|
+
return `${BOOTSTRAP_MARKER_PREFIX}${region}.json`;
|
|
2966
|
+
}
|
|
2967
|
+
/**
|
|
2968
|
+
* Parse and validate a bootstrap marker body.
|
|
2969
|
+
*
|
|
2970
|
+
* Throws on malformed JSON or missing required fields — a corrupt marker is
|
|
2971
|
+
* treated like the marker-present-but-resources-missing case (hard error,
|
|
2972
|
+
* never a silent legacy fallback that would flip-flop stack properties
|
|
2973
|
+
* between deploys — design §4.1).
|
|
2974
|
+
*/
|
|
2975
|
+
function parseBootstrapMarker(body, markerKey) {
|
|
2976
|
+
let parsed;
|
|
2977
|
+
try {
|
|
2978
|
+
parsed = JSON.parse(body);
|
|
2979
|
+
} catch (error) {
|
|
2980
|
+
throw new CdkdError(`Bootstrap marker '${markerKey}' in the state bucket is not valid JSON. Re-run 'cdkd bootstrap' for this region to rewrite it.`, "INVALID_BOOTSTRAP_MARKER", error);
|
|
2981
|
+
}
|
|
2982
|
+
const marker = parsed;
|
|
2983
|
+
if (typeof marker.assetBucket !== "string" || marker.assetBucket.length === 0 || typeof marker.containerRepo !== "string" || marker.containerRepo.length === 0 || typeof marker.assetSupportVersion !== "number") throw new CdkdError(`Bootstrap marker '${markerKey}' in the state bucket is malformed (missing assetBucket / containerRepo / assetSupportVersion). Re-run 'cdkd bootstrap' for this region to rewrite it.`, "INVALID_BOOTSTRAP_MARKER");
|
|
2984
|
+
if (marker.assetSupportVersion > 1) throw new CdkdError(`Bootstrap marker '${markerKey}' has assetSupportVersion ${marker.assetSupportVersion}, but this cdkd only understands up to ${1}. Upgrade cdkd to deploy in this region.`, "UNSUPPORTED_BOOTSTRAP_MARKER_VERSION");
|
|
2985
|
+
return {
|
|
2986
|
+
assetBucket: marker.assetBucket,
|
|
2987
|
+
containerRepo: marker.containerRepo,
|
|
2988
|
+
assetSupportVersion: marker.assetSupportVersion,
|
|
2989
|
+
createdAt: typeof marker.createdAt === "string" ? marker.createdAt : ""
|
|
2990
|
+
};
|
|
2991
|
+
}
|
|
2992
|
+
/**
|
|
2993
|
+
* Verify that the asset bucket + ECR repo named by a marker actually exist.
|
|
2994
|
+
*
|
|
2995
|
+
* Called when a marker is present: the user opted the region in, so missing
|
|
2996
|
+
* resources (user deleted them) are a hard error naming the missing resource
|
|
2997
|
+
* and the `cdkd bootstrap` fix — never a silent fallback to CDK bootstrap
|
|
2998
|
+
* storage (design §4.1).
|
|
2999
|
+
*
|
|
3000
|
+
* Every S3 call passes `ExpectedBucketOwner` so a marker pointing at a
|
|
3001
|
+
* since-recreated foreign bucket can never leak assets cross-account
|
|
3002
|
+
* (bucket-squatting defense, design §5).
|
|
3003
|
+
*/
|
|
3004
|
+
async function verifyAssetStorageExists(marker, accountId, region, opts = {}) {
|
|
3005
|
+
const rebootstrapHint = `Run 'cdkd bootstrap --region ${region}' to recreate it. cdkd never silently falls back to CDK bootstrap asset storage once a region is opted in.`;
|
|
3006
|
+
const clientOpts = {
|
|
3007
|
+
region,
|
|
3008
|
+
...opts.profile && { profile: opts.profile }
|
|
3009
|
+
};
|
|
3010
|
+
const s3Client = new S3Client(clientOpts);
|
|
3011
|
+
const ecrClient = new ECRClient(clientOpts);
|
|
3012
|
+
try {
|
|
3013
|
+
try {
|
|
3014
|
+
await s3Client.send(new HeadBucketCommand({
|
|
3015
|
+
Bucket: marker.assetBucket,
|
|
3016
|
+
ExpectedBucketOwner: accountId
|
|
3017
|
+
}));
|
|
3018
|
+
} catch (error) {
|
|
3019
|
+
const err = error;
|
|
3020
|
+
if (err.name === "NotFound" || err.name === "NoSuchBucket") throw new CdkdError(`cdkd asset storage is bootstrapped for region '${region}' but the asset bucket '${marker.assetBucket}' is missing. ${rebootstrapHint}`, "ASSET_STORAGE_MISSING");
|
|
3021
|
+
if (err.$metadata?.httpStatusCode === 403) throw new CdkdError(`Asset bucket '${marker.assetBucket}' exists but is not owned by account ${accountId} (or access is denied). Refusing to use it. ${rebootstrapHint}`, "ASSET_STORAGE_FOREIGN_BUCKET", error);
|
|
3022
|
+
throw error;
|
|
3023
|
+
}
|
|
3024
|
+
try {
|
|
3025
|
+
await ecrClient.send(new DescribeRepositoriesCommand({ repositoryNames: [marker.containerRepo] }));
|
|
3026
|
+
} catch (error) {
|
|
3027
|
+
if (error.name === "RepositoryNotFoundException") throw new CdkdError(`cdkd asset storage is bootstrapped for region '${region}' but the container-asset ECR repository '${marker.containerRepo}' is missing. ${rebootstrapHint}`, "ASSET_STORAGE_MISSING");
|
|
3028
|
+
throw error;
|
|
3029
|
+
}
|
|
3030
|
+
} finally {
|
|
3031
|
+
s3Client.destroy();
|
|
3032
|
+
ecrClient.destroy();
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
/**
|
|
3036
|
+
* Create (or adopt, when already owned by this account) the cdkd asset
|
|
3037
|
+
* bucket + container-asset ECR repo for a region, then write the bootstrap
|
|
3038
|
+
* marker. Called by `cdkd bootstrap` unless `--no-assets` is passed.
|
|
3039
|
+
*
|
|
3040
|
+
* Idempotent like the state-bucket path: existing resources are left as-is
|
|
3041
|
+
* unless `force` re-applies their configuration. The marker is written LAST,
|
|
3042
|
+
* only after both resources exist (design §5) — a crash mid-bootstrap leaves
|
|
3043
|
+
* no marker, so deploys stay in legacy mode.
|
|
3044
|
+
*
|
|
3045
|
+
* Bucket-squatting defense: the bucket name is predictable (same weakness as
|
|
3046
|
+
* CDK's bootstrap bucket), so this refuses to adopt a bucket this account
|
|
3047
|
+
* does not own, and the `HeadBucket` probe passes `ExpectedBucketOwner`.
|
|
3048
|
+
*/
|
|
3049
|
+
async function ensureAssetStorage(options) {
|
|
3050
|
+
const logger = getLogger();
|
|
3051
|
+
const { s3Client, ecrClient, stateBackend, accountId, region, force } = options;
|
|
3052
|
+
const assetBucket = getCdkdAssetBucketName(accountId, region);
|
|
3053
|
+
const containerRepo = getCdkdContainerRepoName(accountId, region);
|
|
3054
|
+
let bucketExists = false;
|
|
3055
|
+
try {
|
|
3056
|
+
await s3Client.send(new HeadBucketCommand({
|
|
3057
|
+
Bucket: assetBucket,
|
|
3058
|
+
ExpectedBucketOwner: accountId
|
|
3059
|
+
}));
|
|
3060
|
+
bucketExists = true;
|
|
3061
|
+
logger.info(`Asset bucket ${assetBucket} already exists`);
|
|
3062
|
+
} catch (error) {
|
|
3063
|
+
const err = error;
|
|
3064
|
+
if (err.name === "NotFound" || err.name === "NoSuchBucket") {} else if (err.$metadata?.httpStatusCode === 403) throw new CdkdError(`Asset bucket name '${assetBucket}' is already taken by a bucket this account does not own (or access is denied). Refusing to adopt it — resolve the naming conflict before re-running 'cdkd bootstrap'.`, "ASSET_STORAGE_FOREIGN_BUCKET", error);
|
|
3065
|
+
else throw normalizeAwsError(error, {
|
|
3066
|
+
bucket: assetBucket,
|
|
3067
|
+
operation: "HeadBucket"
|
|
3068
|
+
});
|
|
3069
|
+
}
|
|
3070
|
+
if (!bucketExists) {
|
|
3071
|
+
logger.info(`Creating asset bucket: ${assetBucket} in region ${region}`);
|
|
3072
|
+
try {
|
|
3073
|
+
await s3Client.send(new CreateBucketCommand({
|
|
3074
|
+
Bucket: assetBucket,
|
|
3075
|
+
...region !== "us-east-1" && { CreateBucketConfiguration: { LocationConstraint: region } }
|
|
3076
|
+
}));
|
|
3077
|
+
logger.info(`✓ Created asset bucket: ${assetBucket}`);
|
|
3078
|
+
} catch (error) {
|
|
3079
|
+
const err = error;
|
|
3080
|
+
if (err.name === "BucketAlreadyOwnedByYou") logger.info(`Asset bucket ${assetBucket} already exists`);
|
|
3081
|
+
else if (err.name === "BucketAlreadyExists") throw new CdkdError(`Asset bucket name '${assetBucket}' is already taken by another AWS account. Refusing to adopt it — resolve the naming conflict before re-running 'cdkd bootstrap'.`, "ASSET_STORAGE_FOREIGN_BUCKET", error);
|
|
3082
|
+
else throw normalizeAwsError(error, {
|
|
3083
|
+
bucket: assetBucket,
|
|
3084
|
+
operation: "CreateBucket"
|
|
3085
|
+
});
|
|
3086
|
+
}
|
|
3087
|
+
}
|
|
3088
|
+
if (!bucketExists || force) {
|
|
3089
|
+
await s3Client.send(new PutBucketEncryptionCommand({
|
|
3090
|
+
Bucket: assetBucket,
|
|
3091
|
+
ExpectedBucketOwner: accountId,
|
|
3092
|
+
ServerSideEncryptionConfiguration: { Rules: [{
|
|
3093
|
+
ApplyServerSideEncryptionByDefault: { SSEAlgorithm: "AES256" },
|
|
3094
|
+
BucketKeyEnabled: true
|
|
3095
|
+
}] }
|
|
3096
|
+
}));
|
|
3097
|
+
await s3Client.send(new PutPublicAccessBlockCommand({
|
|
3098
|
+
Bucket: assetBucket,
|
|
3099
|
+
ExpectedBucketOwner: accountId,
|
|
3100
|
+
PublicAccessBlockConfiguration: {
|
|
3101
|
+
BlockPublicAcls: true,
|
|
3102
|
+
BlockPublicPolicy: true,
|
|
3103
|
+
IgnorePublicAcls: true,
|
|
3104
|
+
RestrictPublicBuckets: true
|
|
3105
|
+
}
|
|
3106
|
+
}));
|
|
3107
|
+
await s3Client.send(new PutBucketPolicyCommand({
|
|
3108
|
+
Bucket: assetBucket,
|
|
3109
|
+
ExpectedBucketOwner: accountId,
|
|
3110
|
+
Policy: JSON.stringify({
|
|
3111
|
+
Version: "2012-10-17",
|
|
3112
|
+
Statement: [{
|
|
3113
|
+
Sid: "DenyExternalAccess",
|
|
3114
|
+
Effect: "Deny",
|
|
3115
|
+
Principal: "*",
|
|
3116
|
+
Action: "s3:*",
|
|
3117
|
+
Resource: [`arn:aws:s3:::${assetBucket}`, `arn:aws:s3:::${assetBucket}/*`],
|
|
3118
|
+
Condition: { StringNotEquals: { "aws:PrincipalAccount": accountId } }
|
|
3119
|
+
}]
|
|
3120
|
+
})
|
|
3121
|
+
}));
|
|
3122
|
+
logger.info("✓ Configured asset bucket (AES-256 encryption, public access block, deny external access)");
|
|
3123
|
+
}
|
|
3124
|
+
let repoExists = false;
|
|
3125
|
+
try {
|
|
3126
|
+
await ecrClient.send(new DescribeRepositoriesCommand({ repositoryNames: [containerRepo] }));
|
|
3127
|
+
repoExists = true;
|
|
3128
|
+
logger.info(`Container-asset repository ${containerRepo} already exists`);
|
|
3129
|
+
} catch (error) {
|
|
3130
|
+
if (error.name !== "RepositoryNotFoundException") throw error;
|
|
3131
|
+
}
|
|
3132
|
+
if (!repoExists) {
|
|
3133
|
+
logger.info(`Creating container-asset ECR repository: ${containerRepo}`);
|
|
3134
|
+
try {
|
|
3135
|
+
await ecrClient.send(new CreateRepositoryCommand({
|
|
3136
|
+
repositoryName: containerRepo,
|
|
3137
|
+
imageTagMutability: "IMMUTABLE"
|
|
3138
|
+
}));
|
|
3139
|
+
logger.info(`✓ Created container-asset ECR repository: ${containerRepo}`);
|
|
3140
|
+
} catch (error) {
|
|
3141
|
+
if (error.name === "RepositoryAlreadyExistsException") logger.info(`Container-asset repository ${containerRepo} already exists`);
|
|
3142
|
+
else throw error;
|
|
3143
|
+
}
|
|
3144
|
+
} else if (force) {
|
|
3145
|
+
await ecrClient.send(new PutImageTagMutabilityCommand({
|
|
3146
|
+
repositoryName: containerRepo,
|
|
3147
|
+
imageTagMutability: "IMMUTABLE"
|
|
3148
|
+
}));
|
|
3149
|
+
logger.info("✓ Configured container-asset repository (immutable tags)");
|
|
3150
|
+
}
|
|
3151
|
+
const marker = {
|
|
3152
|
+
assetBucket,
|
|
3153
|
+
containerRepo,
|
|
3154
|
+
assetSupportVersion: 1,
|
|
3155
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3156
|
+
};
|
|
3157
|
+
await stateBackend.putRawObject(getBootstrapMarkerKey(region), JSON.stringify(marker, null, 2));
|
|
3158
|
+
logger.info(`✓ Wrote bootstrap marker (${getBootstrapMarkerKey(region)})`);
|
|
3159
|
+
return {
|
|
3160
|
+
assetBucket,
|
|
3161
|
+
containerRepo
|
|
3162
|
+
};
|
|
3163
|
+
}
|
|
3164
|
+
/**
|
|
3165
|
+
* Deploy-time asset-mode resolver. One instance per CLI invocation; results
|
|
3166
|
+
* are cached per region for the process lifetime (the marker read is one
|
|
3167
|
+
* GetObject against a bucket every deploy already reads — design §4.1).
|
|
3168
|
+
*
|
|
3169
|
+
* In legacy mode, one `logger.info` line per invocation mentions the
|
|
3170
|
+
* `cdk gc` hazard and the `cdkd bootstrap` opt-in (info, not warn — existing
|
|
3171
|
+
* users are not doing anything wrong; design §12.2).
|
|
3172
|
+
*/
|
|
3173
|
+
var AssetModeResolver = class {
|
|
3174
|
+
logger = getLogger().child("AssetMode");
|
|
3175
|
+
cache = /* @__PURE__ */ new Map();
|
|
3176
|
+
legacyNoticeShown = false;
|
|
3177
|
+
stateBackend;
|
|
3178
|
+
accountId;
|
|
3179
|
+
profile;
|
|
3180
|
+
useCdkBootstrapAssets;
|
|
3181
|
+
suppressLegacyNotice;
|
|
3182
|
+
constructor(stateBackend, accountId, opts = {}) {
|
|
3183
|
+
this.stateBackend = stateBackend;
|
|
3184
|
+
this.accountId = accountId;
|
|
3185
|
+
this.profile = opts.profile;
|
|
3186
|
+
this.useCdkBootstrapAssets = opts.useCdkBootstrapAssets ?? false;
|
|
3187
|
+
this.suppressLegacyNotice = opts.suppressLegacyNotice ?? false;
|
|
3188
|
+
}
|
|
3189
|
+
/**
|
|
3190
|
+
* Resolve the asset mode for a deploy region. Concurrent callers for the
|
|
3191
|
+
* same region share one in-flight resolution.
|
|
3192
|
+
*/
|
|
3193
|
+
resolve(region) {
|
|
3194
|
+
if (this.useCdkBootstrapAssets) return Promise.resolve({ mode: "legacy" });
|
|
3195
|
+
const cached = this.cache.get(region);
|
|
3196
|
+
if (cached) return cached;
|
|
3197
|
+
const inFlight = this.doResolve(region).catch((error) => {
|
|
3198
|
+
this.cache.delete(region);
|
|
3199
|
+
throw error;
|
|
3200
|
+
});
|
|
3201
|
+
this.cache.set(region, inFlight);
|
|
3202
|
+
return inFlight;
|
|
3203
|
+
}
|
|
3204
|
+
async doResolve(region) {
|
|
3205
|
+
const markerKey = getBootstrapMarkerKey(region);
|
|
3206
|
+
const body = await this.stateBackend.getRawObject(markerKey);
|
|
3207
|
+
if (body === null) {
|
|
3208
|
+
if (!this.legacyNoticeShown && !this.suppressLegacyNotice) {
|
|
3209
|
+
this.legacyNoticeShown = true;
|
|
3210
|
+
this.logger.info("Assets are published to the CDK bootstrap bucket/repo, which 'cdk gc' may garbage-collect (cdkd-deployed stacks have no CloudFormation stack for gc to scan). Run 'cdkd bootstrap' to create cdkd-owned asset storage that 'cdk gc' never touches.");
|
|
3211
|
+
}
|
|
3212
|
+
return { mode: "legacy" };
|
|
3213
|
+
}
|
|
3214
|
+
const marker = parseBootstrapMarker(body, markerKey);
|
|
3215
|
+
await verifyAssetStorageExists(marker, this.accountId, region, { ...this.profile && { profile: this.profile } });
|
|
3216
|
+
this.logger.debug(`cdkd asset storage active for region '${region}': ${marker.assetBucket} / ${marker.containerRepo}`);
|
|
3217
|
+
return {
|
|
3218
|
+
mode: "cdkd-assets",
|
|
3219
|
+
marker
|
|
3220
|
+
};
|
|
3221
|
+
}
|
|
3222
|
+
};
|
|
3223
|
+
|
|
3224
|
+
//#endregion
|
|
3225
|
+
//#region src/assets/asset-redirect.ts
|
|
3226
|
+
/**
|
|
3227
|
+
* Resolve `${AWS::AccountId}` / `${AWS::Region}` / `${AWS::Partition}`
|
|
3228
|
+
* placeholders in an asset-manifest destination value. Module-level twin of
|
|
3229
|
+
* `AssetManifestLoader.resolveAssetDestinationValue` (same substitution set,
|
|
3230
|
+
* same `aws` default partition) so the mapping table, the publishers, and
|
|
3231
|
+
* the rewrite flatten identically.
|
|
3232
|
+
*/
|
|
3233
|
+
function flattenAssetPlaceholders(value, accountId, region, partition = "aws") {
|
|
3234
|
+
return value.replace(/\$\{AWS::AccountId\}/g, accountId).replace(/\$\{AWS::Region\}/g, region).replace(/\$\{AWS::Partition\}/g, partition);
|
|
3235
|
+
}
|
|
3236
|
+
/**
|
|
3237
|
+
* §8 scope rule, file-asset leg: `true` when a FLATTENED bucket name is
|
|
3238
|
+
* default-bootstrap-shaped for this (account, region) — any qualifier
|
|
3239
|
+
* (`cdk-hnb659fds-assets-…` default or `cdk-myqual-assets-…` custom; gc can
|
|
3240
|
+
* target any bootstrap stack). Everything else (user-chosen names,
|
|
3241
|
+
* AppStagingSynthesizer staging buckets, other accounts/regions) is out of
|
|
3242
|
+
* scope and left verbatim.
|
|
3243
|
+
*/
|
|
3244
|
+
function isDefaultBootstrapBucketName(name, accountId, region) {
|
|
3245
|
+
return new RegExp(`^cdk-[a-z0-9]+-assets-${accountId}-${escapeRegExp(region)}$`).test(name);
|
|
3246
|
+
}
|
|
3247
|
+
/** §8 scope rule, container-image leg (`cdk-<qualifier>-container-assets-…`). */
|
|
3248
|
+
function isDefaultBootstrapRepoName(name, accountId, region) {
|
|
3249
|
+
return new RegExp(`^cdk-[a-z0-9]+-container-assets-${accountId}-${escapeRegExp(region)}$`).test(name);
|
|
3250
|
+
}
|
|
3251
|
+
function escapeRegExp(s) {
|
|
3252
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3253
|
+
}
|
|
3254
|
+
/**
|
|
3255
|
+
* Build the §6 asset-location mapping table from a stack's asset manifest.
|
|
3256
|
+
*
|
|
3257
|
+
* For every file / docker destination that (a) targets the deploy region and
|
|
3258
|
+
* (b) is default-bootstrap-shaped after flattening, the flattened source name
|
|
3259
|
+
* maps to the marker's cdkd-owned bucket / repo. `objectKey` / `imageTag`
|
|
3260
|
+
* (content hashes, including any `bucketPrefix` baked into `objectKey`) are
|
|
3261
|
+
* never part of the table — keys and tags flow through unchanged so the
|
|
3262
|
+
* existence-check/skip logic works as-is per storage.
|
|
3263
|
+
*
|
|
3264
|
+
* Template assets (`*.template.json` sources) contribute their destinations
|
|
3265
|
+
* too even though cdkd never publishes them: the parent's `TemplateURL`
|
|
3266
|
+
* property references the same bootstrap bucket and must rewrite with it
|
|
3267
|
+
* (harmlessly — `NestedStackProvider` never dereferences the URL).
|
|
3268
|
+
*
|
|
3269
|
+
* Cross-region destinations (`dest.region` ≠ deploy region) are skipped
|
|
3270
|
+
* entirely — cdkd asset storage and its marker are per-region (§8).
|
|
3271
|
+
*/
|
|
3272
|
+
function buildAssetRedirectMap(manifest, marker, accountId, region, partition = "aws") {
|
|
3273
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
3274
|
+
const repos = /* @__PURE__ */ new Map();
|
|
3275
|
+
const sources = /* @__PURE__ */ new Map();
|
|
3276
|
+
const addForms = (rawName, flattened, target) => {
|
|
3277
|
+
sources.set(flattened, target);
|
|
3278
|
+
if (rawName !== flattened) sources.set(rawName, target);
|
|
3279
|
+
const suffixRe = new RegExp(`-${accountId}-${escapeRegExp(region)}$`);
|
|
3280
|
+
for (const suffix of [
|
|
3281
|
+
"-${AWS::AccountId}-${AWS::Region}",
|
|
3282
|
+
`-\${AWS::AccountId}-${region}`,
|
|
3283
|
+
`-${accountId}-\${AWS::Region}`
|
|
3284
|
+
]) {
|
|
3285
|
+
const form = flattened.replace(suffixRe, suffix);
|
|
3286
|
+
if (form !== flattened) sources.set(form, target);
|
|
3287
|
+
}
|
|
3288
|
+
};
|
|
3289
|
+
const destTargetsDeployRegion = (dest) => {
|
|
3290
|
+
if (!dest.region) return true;
|
|
3291
|
+
return flattenAssetPlaceholders(dest.region, accountId, region, partition) === region;
|
|
3292
|
+
};
|
|
3293
|
+
for (const asset of Object.values(manifest.files ?? {})) for (const dest of Object.values(asset.destinations ?? {})) {
|
|
3294
|
+
if (!destTargetsDeployRegion(dest)) continue;
|
|
3295
|
+
const flattened = flattenAssetPlaceholders(dest.bucketName, accountId, region, partition);
|
|
3296
|
+
if (!isDefaultBootstrapBucketName(flattened, accountId, region)) continue;
|
|
3297
|
+
buckets.set(flattened, marker.assetBucket);
|
|
3298
|
+
addForms(dest.bucketName, flattened, marker.assetBucket);
|
|
3299
|
+
}
|
|
3300
|
+
for (const asset of Object.values(manifest.dockerImages ?? {})) for (const dest of Object.values(asset.destinations ?? {})) {
|
|
3301
|
+
if (!destTargetsDeployRegion(dest)) continue;
|
|
3302
|
+
const flattened = flattenAssetPlaceholders(dest.repositoryName, accountId, region, partition);
|
|
3303
|
+
if (!isDefaultBootstrapRepoName(flattened, accountId, region)) continue;
|
|
3304
|
+
repos.set(flattened, marker.containerRepo);
|
|
3305
|
+
addForms(dest.repositoryName, flattened, marker.containerRepo);
|
|
3306
|
+
}
|
|
3307
|
+
return {
|
|
3308
|
+
buckets,
|
|
3309
|
+
repos,
|
|
3310
|
+
entries: [...sources.entries()].map(([source, target]) => ({
|
|
3311
|
+
source,
|
|
3312
|
+
target
|
|
3313
|
+
})).sort((a, b) => b.source.length - a.source.length),
|
|
3314
|
+
accountId,
|
|
3315
|
+
region,
|
|
3316
|
+
partition
|
|
3317
|
+
};
|
|
3318
|
+
}
|
|
3319
|
+
/**
|
|
3320
|
+
* Boundary-aware replacement (§7 step 2): a source name only matches when it
|
|
3321
|
+
* stands alone as a name token — not preceded by a name character (so a user
|
|
3322
|
+
* bucket `my-cdk-hnb659fds-assets-…` is never corrupted) and not followed by
|
|
3323
|
+
* one (so `cdk-hnb659fds-assets-<acct>-<region>-backup` is never corrupted).
|
|
3324
|
+
* URI delimiters (`/`, `:`, `.`, quotes, whitespace) and end-of-string are
|
|
3325
|
+
* boundaries.
|
|
3326
|
+
*
|
|
3327
|
+
* Known trade-off: a trailing `.` MUST be a boundary for virtual-host-style
|
|
3328
|
+
* URLs (`<bucket>.s3.<region>.amazonaws.com`), so a user bucket literally
|
|
3329
|
+
* named `cdk-<qualifier>-assets-<acct>-<region>.backup` (dot-suffixed
|
|
3330
|
+
* lookalike) would have its prefix rewritten. S3 discourages dots in bucket
|
|
3331
|
+
* names (breaks virtual-host TLS) and the name would ALSO have to collide
|
|
3332
|
+
* with the deploy account+region bootstrap shape — accepted as pathological.
|
|
3333
|
+
*/
|
|
3334
|
+
function buildBoundaryRegex(source) {
|
|
3335
|
+
return new RegExp(`(?<![A-Za-z0-9_.-])${escapeRegExp(source)}(?![A-Za-z0-9_-])`, "g");
|
|
3336
|
+
}
|
|
3337
|
+
function rewriteString(value, map, counter) {
|
|
3338
|
+
let result = value;
|
|
3339
|
+
for (const { source, target } of map.entries) {
|
|
3340
|
+
if (!result.includes(source)) continue;
|
|
3341
|
+
const re = buildBoundaryRegex(source);
|
|
3342
|
+
result = result.replace(re, () => {
|
|
3343
|
+
counter.n++;
|
|
3344
|
+
return target;
|
|
3345
|
+
});
|
|
3346
|
+
}
|
|
3347
|
+
return result;
|
|
3348
|
+
}
|
|
3349
|
+
/** True when the string contains any mapped source name at a token boundary. */
|
|
3350
|
+
function containsAnySource(value, map) {
|
|
3351
|
+
for (const { source } of map.entries) {
|
|
3352
|
+
if (!value.includes(source)) continue;
|
|
3353
|
+
if (buildBoundaryRegex(source).test(value)) return true;
|
|
3354
|
+
}
|
|
3355
|
+
return false;
|
|
3356
|
+
}
|
|
3357
|
+
const FOLDABLE_PSEUDO_PARAMS = new Set([
|
|
3358
|
+
"AWS::AccountId",
|
|
3359
|
+
"AWS::Region",
|
|
3360
|
+
"AWS::Partition",
|
|
3361
|
+
"AWS::URLSuffix"
|
|
3362
|
+
]);
|
|
3363
|
+
function evaluatePseudoParam(name, map) {
|
|
3364
|
+
switch (name) {
|
|
3365
|
+
case "AWS::AccountId": return map.accountId;
|
|
3366
|
+
case "AWS::Region": return map.region;
|
|
3367
|
+
case "AWS::Partition": return map.partition;
|
|
3368
|
+
case "AWS::URLSuffix": return "amazonaws.com";
|
|
3369
|
+
default: throw new Error(`Not a foldable pseudo parameter: ${name}`);
|
|
3370
|
+
}
|
|
3371
|
+
}
|
|
3372
|
+
/** A `Fn::Join` part that partial evaluation can fold to a literal. */
|
|
3373
|
+
function foldablePartValue(part, map) {
|
|
3374
|
+
if (typeof part === "string") return part;
|
|
3375
|
+
if (part !== null && typeof part === "object" && !Array.isArray(part)) {
|
|
3376
|
+
const keys = Object.keys(part);
|
|
3377
|
+
if (keys.length === 1 && keys[0] === "Ref") {
|
|
3378
|
+
const ref = part["Ref"];
|
|
3379
|
+
if (typeof ref === "string" && FOLDABLE_PSEUDO_PARAMS.has(ref)) return evaluatePseudoParam(ref, map);
|
|
3380
|
+
}
|
|
3381
|
+
}
|
|
3382
|
+
}
|
|
3383
|
+
/**
|
|
3384
|
+
* §7 `Fn::Join` handling: fold maximal runs of pseudo-parameter-only parts
|
|
3385
|
+
* (string literals + `{Ref: AWS::AccountId|Region|Partition|URLSuffix}` —
|
|
3386
|
+
* all deploy-time constants) into a literal, and KEEP the folded literal only
|
|
3387
|
+
* when a source name actually matched inside it — otherwise the original
|
|
3388
|
+
* parts are preserved so templates without asset references stay
|
|
3389
|
+
* byte-identical. Joins whose relevant runs contain real resource refs are
|
|
3390
|
+
* left alone (synthesizer output never splits an asset location across a
|
|
3391
|
+
* resource ref).
|
|
3392
|
+
*
|
|
3393
|
+
* Returns the new parts array (or the original when nothing changed).
|
|
3394
|
+
*/
|
|
3395
|
+
function foldAndRewriteJoinParts(delimiter, parts, map, counter) {
|
|
3396
|
+
const out = [];
|
|
3397
|
+
let changed = false;
|
|
3398
|
+
let i = 0;
|
|
3399
|
+
while (i < parts.length) {
|
|
3400
|
+
const folded = foldablePartValue(parts[i], map);
|
|
3401
|
+
if (folded === void 0) {
|
|
3402
|
+
out.push(parts[i]);
|
|
3403
|
+
i++;
|
|
3404
|
+
continue;
|
|
3405
|
+
}
|
|
3406
|
+
const runValues = [folded];
|
|
3407
|
+
let j = i + 1;
|
|
3408
|
+
for (; j < parts.length; j++) {
|
|
3409
|
+
const v = foldablePartValue(parts[j], map);
|
|
3410
|
+
if (v === void 0) break;
|
|
3411
|
+
runValues.push(v);
|
|
3412
|
+
}
|
|
3413
|
+
const literal = runValues.join(delimiter);
|
|
3414
|
+
if (j - i > 1 && containsAnySource(literal, map)) {
|
|
3415
|
+
out.push(rewriteString(literal, map, counter));
|
|
3416
|
+
changed = true;
|
|
3417
|
+
} else for (let k = i; k < j; k++) out.push(parts[k]);
|
|
3418
|
+
i = j;
|
|
3419
|
+
}
|
|
3420
|
+
return changed ? out : parts;
|
|
3421
|
+
}
|
|
3422
|
+
function rewriteNode(node, map, counter) {
|
|
3423
|
+
if (typeof node === "string") return rewriteString(node, map, counter);
|
|
3424
|
+
if (Array.isArray(node)) {
|
|
3425
|
+
for (let i = 0; i < node.length; i++) node[i] = rewriteNode(node[i], map, counter);
|
|
3426
|
+
return node;
|
|
3427
|
+
}
|
|
3428
|
+
if (node !== null && typeof node === "object") {
|
|
3429
|
+
const obj = node;
|
|
3430
|
+
const joinArgs = obj["Fn::Join"];
|
|
3431
|
+
if (Object.keys(obj).length === 1 && Array.isArray(joinArgs) && joinArgs.length === 2 && typeof joinArgs[0] === "string" && Array.isArray(joinArgs[1])) {
|
|
3432
|
+
const foldedParts = foldAndRewriteJoinParts(joinArgs[0], joinArgs[1], map, counter);
|
|
3433
|
+
obj["Fn::Join"] = [joinArgs[0], rewriteNode(foldedParts, map, counter)];
|
|
3434
|
+
return obj;
|
|
3435
|
+
}
|
|
3436
|
+
for (const key of Object.keys(obj)) obj[key] = rewriteNode(obj[key], map, counter);
|
|
3437
|
+
return obj;
|
|
3438
|
+
}
|
|
3439
|
+
return node;
|
|
3440
|
+
}
|
|
3441
|
+
/**
|
|
3442
|
+
* §7 template reference rewrite: deep-walk the parsed template IN PLACE and
|
|
3443
|
+
* replace every boundary-matched source name (both placeholder and flattened
|
|
3444
|
+
* forms, incl. inside `Fn::Sub` template strings and across folded `Fn::Join`
|
|
3445
|
+
* pseudo-parameter runs) with the cdkd-owned storage name. Returns the number
|
|
3446
|
+
* of replacements for logging.
|
|
3447
|
+
*
|
|
3448
|
+
* Applied per stack before DAG build (deploy), before diff computation
|
|
3449
|
+
* (diff, incl. every recursive child template), and before state write
|
|
3450
|
+
* (import, incl. the recursive CFn-migration child walk). `cdkd synth` and
|
|
3451
|
+
* `cdkd export` output stays unrewritten by design (§7.1).
|
|
3452
|
+
*/
|
|
3453
|
+
function rewriteTemplateAssetReferences(template, map) {
|
|
3454
|
+
if (map.entries.length === 0) return 0;
|
|
3455
|
+
const counter = { n: 0 };
|
|
3456
|
+
rewriteNode(template, map, counter);
|
|
3457
|
+
return counter.n;
|
|
3458
|
+
}
|
|
3459
|
+
function auditNode(node, map, path, findings) {
|
|
3460
|
+
if (typeof node === "string") {
|
|
3461
|
+
for (const { source } of map.entries) {
|
|
3462
|
+
if (!node.includes(source)) continue;
|
|
3463
|
+
if (buildBoundaryRegex(source).test(node)) {
|
|
3464
|
+
findings.push({
|
|
3465
|
+
path,
|
|
3466
|
+
source
|
|
3467
|
+
});
|
|
3468
|
+
return;
|
|
3469
|
+
}
|
|
3470
|
+
}
|
|
3471
|
+
return;
|
|
3472
|
+
}
|
|
3473
|
+
if (Array.isArray(node)) {
|
|
3474
|
+
node.forEach((item, i) => auditNode(item, map, `${path}[${i}]`, findings));
|
|
3475
|
+
return;
|
|
3476
|
+
}
|
|
3477
|
+
if (node !== null && typeof node === "object") for (const [key, value] of Object.entries(node)) auditNode(value, map, path ? `${path}.${key}` : key, findings);
|
|
3478
|
+
}
|
|
3479
|
+
/**
|
|
3480
|
+
* §7 step 3 post-resolution audit (defense in depth): after the intrinsic
|
|
3481
|
+
* resolver produces final literals, scan the resolved properties for any
|
|
3482
|
+
* remaining mapped SOURCE name. A hit means a template shape the rewrite
|
|
3483
|
+
* missed — the deploy engine fails the resource loudly instead of deploying
|
|
3484
|
+
* a split-brain reference (assets in cdkd storage, property pointing at the
|
|
3485
|
+
* CDK bootstrap bucket that `cdk gc` may have emptied).
|
|
3486
|
+
*/
|
|
3487
|
+
function findUnrewrittenAssetReferences(resolvedProperties, map) {
|
|
3488
|
+
const findings = [];
|
|
3489
|
+
auditNode(resolvedProperties, map, "", findings);
|
|
3490
|
+
return findings;
|
|
3491
|
+
}
|
|
3492
|
+
/**
|
|
3493
|
+
* Apply the mapping table to one file asset's destinations (§6 publish-time
|
|
3494
|
+
* redirection). Returns a shallow-cloned asset when any destination is
|
|
3495
|
+
* redirected, or the original object otherwise. `objectKey` is untouched.
|
|
3496
|
+
*/
|
|
3497
|
+
function redirectFileAsset(asset, map) {
|
|
3498
|
+
let changed = false;
|
|
3499
|
+
const destinations = {};
|
|
3500
|
+
for (const [id, dest] of Object.entries(asset.destinations ?? {})) {
|
|
3501
|
+
const flattened = flattenAssetPlaceholders(dest.bucketName, map.accountId, map.region, map.partition);
|
|
3502
|
+
const target = map.buckets.get(flattened);
|
|
3503
|
+
if (target && destRegionMatches(dest, map)) {
|
|
3504
|
+
destinations[id] = {
|
|
3505
|
+
...dest,
|
|
3506
|
+
bucketName: target
|
|
3507
|
+
};
|
|
3508
|
+
changed = true;
|
|
3509
|
+
} else destinations[id] = dest;
|
|
3510
|
+
}
|
|
3511
|
+
return changed ? {
|
|
3512
|
+
...asset,
|
|
3513
|
+
destinations
|
|
3514
|
+
} : asset;
|
|
3515
|
+
}
|
|
3516
|
+
/**
|
|
3517
|
+
* Apply the mapping table to one docker-image asset's destinations.
|
|
3518
|
+
* Returns a shallow-cloned asset when any destination is redirected.
|
|
3519
|
+
* `imageTag` is untouched.
|
|
3520
|
+
*/
|
|
3521
|
+
function redirectDockerAsset(asset, map) {
|
|
3522
|
+
let changed = false;
|
|
3523
|
+
const destinations = {};
|
|
3524
|
+
for (const [id, dest] of Object.entries(asset.destinations ?? {})) {
|
|
3525
|
+
const flattened = flattenAssetPlaceholders(dest.repositoryName, map.accountId, map.region, map.partition);
|
|
3526
|
+
const target = map.repos.get(flattened);
|
|
3527
|
+
if (target && destRegionMatches(dest, map)) {
|
|
3528
|
+
destinations[id] = {
|
|
3529
|
+
...dest,
|
|
3530
|
+
repositoryName: target
|
|
3531
|
+
};
|
|
3532
|
+
changed = true;
|
|
3533
|
+
} else destinations[id] = dest;
|
|
3534
|
+
}
|
|
3535
|
+
return changed ? {
|
|
3536
|
+
...asset,
|
|
3537
|
+
destinations
|
|
3538
|
+
} : asset;
|
|
3539
|
+
}
|
|
3540
|
+
function destRegionMatches(dest, map) {
|
|
3541
|
+
if (!dest.region) return true;
|
|
3542
|
+
return flattenAssetPlaceholders(dest.region, map.accountId, map.region, map.partition) === map.region;
|
|
3543
|
+
}
|
|
3544
|
+
/**
|
|
3545
|
+
* Load a stack's asset manifest and report whether it has anything cdkd
|
|
3546
|
+
* would actually publish (file assets excluding CFn template assets, plus
|
|
3547
|
+
* docker images). Returns `null` when the manifest file does not exist or
|
|
3548
|
+
* has nothing publishable — the caller then skips asset-mode resolution
|
|
3549
|
+
* entirely so asset-less deploys stay byte-identical to pre-#1002 behavior
|
|
3550
|
+
* (no marker read, no legacy-mode notice).
|
|
3551
|
+
*
|
|
3552
|
+
* Non-ENOENT read/parse failures propagate — a corrupt manifest must not
|
|
3553
|
+
* be mistaken for "no assets".
|
|
3554
|
+
*/
|
|
3555
|
+
function loadPublishableAssetManifest(manifestPath) {
|
|
3556
|
+
let raw;
|
|
3557
|
+
try {
|
|
3558
|
+
raw = readFileSync(manifestPath, "utf-8");
|
|
3559
|
+
} catch (error) {
|
|
3560
|
+
if (error.code === "ENOENT") return null;
|
|
3561
|
+
throw error;
|
|
3562
|
+
}
|
|
3563
|
+
const manifest = JSON.parse(raw);
|
|
3564
|
+
if (Object.values(manifest.files ?? {}).filter((asset) => !isCfnTemplateAssetPath(asset.source.path)).length + Object.keys(manifest.dockerImages ?? {}).length === 0) return null;
|
|
3565
|
+
return manifest;
|
|
3566
|
+
}
|
|
3567
|
+
/**
|
|
3568
|
+
* Lazily-initializing per-stack redirect resolver for commands that do NOT
|
|
3569
|
+
* already resolve the caller account id up front (`diff`, `import`). Returns
|
|
3570
|
+
* a function that maps `(manifestPath, region)` to the stack's
|
|
3571
|
+
* {@link AssetRedirectMap} — or `undefined` when the stack has nothing
|
|
3572
|
+
* publishable, the region is in legacy mode, or the map has no
|
|
3573
|
+
* in-scope destinations.
|
|
3574
|
+
*
|
|
3575
|
+
* The STS `GetCallerIdentity` call and the {@link AssetModeResolver} are
|
|
3576
|
+
* created on first need only, so invocations that never touch an
|
|
3577
|
+
* asset-bearing stack make no extra AWS calls (byte-identical to pre-#1002
|
|
3578
|
+
* behavior). `useCdkBootstrapAssets` short-circuits everything to legacy.
|
|
3579
|
+
*/
|
|
3580
|
+
function createAssetRedirectResolver(opts) {
|
|
3581
|
+
let accountIdPromise;
|
|
3582
|
+
let modeResolver;
|
|
3583
|
+
const getAccountId = () => {
|
|
3584
|
+
accountIdPromise ??= (async () => {
|
|
3585
|
+
const { STSClient, GetCallerIdentityCommand } = await import("@aws-sdk/client-sts");
|
|
3586
|
+
const stsClient = new STSClient({
|
|
3587
|
+
region: opts.stsRegion,
|
|
3588
|
+
...opts.profile && { profile: opts.profile }
|
|
3589
|
+
});
|
|
3590
|
+
try {
|
|
3591
|
+
return (await stsClient.send(new GetCallerIdentityCommand({}))).Account;
|
|
3592
|
+
} finally {
|
|
3593
|
+
stsClient.destroy();
|
|
3594
|
+
}
|
|
3595
|
+
})();
|
|
3596
|
+
return accountIdPromise;
|
|
3597
|
+
};
|
|
3598
|
+
return async (manifestPath, region) => {
|
|
3599
|
+
if (opts.useCdkBootstrapAssets || !manifestPath) return void 0;
|
|
3600
|
+
const manifest = loadPublishableAssetManifest(manifestPath);
|
|
3601
|
+
if (!manifest) return void 0;
|
|
3602
|
+
const accountId = await getAccountId();
|
|
3603
|
+
modeResolver ??= new AssetModeResolver(opts.stateBackend, accountId, {
|
|
3604
|
+
...opts.profile && { profile: opts.profile },
|
|
3605
|
+
...opts.suppressLegacyNotice && { suppressLegacyNotice: true }
|
|
3606
|
+
});
|
|
3607
|
+
const mode = await modeResolver.resolve(region);
|
|
3608
|
+
if (mode.mode !== "cdkd-assets") return void 0;
|
|
3609
|
+
const map = buildAssetRedirectMap(manifest, mode.marker, accountId, region);
|
|
3610
|
+
return map.entries.length > 0 ? map : void 0;
|
|
3611
|
+
};
|
|
3612
|
+
}
|
|
3613
|
+
|
|
2899
3614
|
//#endregion
|
|
2900
3615
|
//#region src/deployment/work-graph.ts
|
|
2901
3616
|
/**
|
|
@@ -3044,8 +3759,9 @@ var AssetPublisher = class {
|
|
|
3044
3759
|
const manifest = JSON.parse(content);
|
|
3045
3760
|
const cdkOutputDir = manifestPath.replace(/\/[^/]+$/, "");
|
|
3046
3761
|
const prefix = options.nodePrefix || "";
|
|
3762
|
+
const redirect = options.redirect;
|
|
3047
3763
|
const nodeIds = [];
|
|
3048
|
-
const fileAssets = Object.entries(manifest.files || {}).filter(([, asset]) => !isCfnTemplateAssetPath(asset.source.path));
|
|
3764
|
+
const fileAssets = Object.entries(manifest.files || {}).filter(([, asset]) => !isCfnTemplateAssetPath(asset.source.path)).map(([hash, asset]) => [hash, redirect ? redirectFileAsset(asset, redirect) : asset]);
|
|
3049
3765
|
for (const [hash, asset] of fileAssets) {
|
|
3050
3766
|
const nodeId = `asset-publish:${prefix}file:${hash}`;
|
|
3051
3767
|
graph.addNode({
|
|
@@ -3065,7 +3781,8 @@ var AssetPublisher = class {
|
|
|
3065
3781
|
});
|
|
3066
3782
|
nodeIds.push(nodeId);
|
|
3067
3783
|
}
|
|
3068
|
-
for (const [hash,
|
|
3784
|
+
for (const [hash, rawAsset] of Object.entries(manifest.dockerImages || {})) {
|
|
3785
|
+
const asset = redirect ? redirectDockerAsset(rawAsset, redirect) : rawAsset;
|
|
3069
3786
|
const localTag = `cdkd-asset-${hash}`;
|
|
3070
3787
|
const buildNodeId = `asset-build:${prefix}docker:${hash}`;
|
|
3071
3788
|
const publishNodeId = `asset-publish:${prefix}docker:${hash}`;
|
|
@@ -3128,7 +3845,8 @@ var AssetPublisher = class {
|
|
|
3128
3845
|
if (this.addAssetsToGraph(graph, manifestPath, {
|
|
3129
3846
|
accountId,
|
|
3130
3847
|
region,
|
|
3131
|
-
...options.profile && { profile: options.profile }
|
|
3848
|
+
...options.profile && { profile: options.profile },
|
|
3849
|
+
...options.redirect && { redirect: options.redirect }
|
|
3132
3850
|
}).length === 0) {
|
|
3133
3851
|
this.logger.debug("No assets to publish");
|
|
3134
3852
|
return;
|
|
@@ -14520,6 +15238,22 @@ var DeployEngine = class {
|
|
|
14520
15238
|
} catch {}
|
|
14521
15239
|
}
|
|
14522
15240
|
/**
|
|
15241
|
+
* Issue #1002 PR 2 — §7 step 3 post-resolution audit (defense in depth).
|
|
15242
|
+
* No-op in legacy mode (`options.assetRedirect` unset). In cdkd-assets
|
|
15243
|
+
* mode, a resolved property still naming a mapped SOURCE (CDK bootstrap)
|
|
15244
|
+
* bucket / repo means a template shape the §7 rewrite missed — fail the
|
|
15245
|
+
* resource loudly BEFORE provisioning instead of deploying a split-brain
|
|
15246
|
+
* reference (assets live in cdkd storage, the property points at the CDK
|
|
15247
|
+
* bootstrap bucket that `cdk gc` may have emptied).
|
|
15248
|
+
*/
|
|
15249
|
+
auditResolvedAssetReferences(logicalId, resourceType, resolvedProps) {
|
|
15250
|
+
const redirect = this.options.assetRedirect;
|
|
15251
|
+
if (!redirect) return;
|
|
15252
|
+
const findings = findUnrewrittenAssetReferences(resolvedProps, redirect);
|
|
15253
|
+
if (findings.length === 0) return;
|
|
15254
|
+
throw new ProvisioningError(`Unrewritten asset reference on '${logicalId}' (${resourceType}): this region uses cdkd-owned asset storage, but the following resolved properties still point at the CDK bootstrap storage that 'cdk gc' may garbage-collect:\n${findings.map((f) => ` - ${f.path}: still references '${f.source}'`).join("\n")}\nThis is a template shape cdkd's asset-reference rewrite did not cover — deploying it would split-brain the stack (assets in cdkd storage, properties reading the CDK bucket). Please report this at https://github.com/go-to-k/cdkd/issues with the property shape. Workaround: deploy with --use-cdk-bootstrap-assets to pin the legacy destinations for this app.`, resourceType, logicalId);
|
|
15255
|
+
}
|
|
15256
|
+
/**
|
|
14523
15257
|
* Inner body of provisionResource, extracted so the outer wrapper can
|
|
14524
15258
|
* apply the per-resource deadline (`withResourceDeadline`) without
|
|
14525
15259
|
* having the timeout / warn timer code dwarf the real provisioning
|
|
@@ -14539,6 +15273,7 @@ var DeployEngine = class {
|
|
|
14539
15273
|
...conditions && { conditions }
|
|
14540
15274
|
}, stackName);
|
|
14541
15275
|
const resolvedProps = await this.resolver.resolve(desiredProps, context);
|
|
15276
|
+
this.auditResolvedAssetReferences(logicalId, resourceType, resolvedProps);
|
|
14542
15277
|
const createDecision = this.providerRegistry.getProviderFor({
|
|
14543
15278
|
resourceType,
|
|
14544
15279
|
properties: resolvedProps
|
|
@@ -14578,6 +15313,7 @@ var DeployEngine = class {
|
|
|
14578
15313
|
...conditions && { conditions }
|
|
14579
15314
|
}, stackName);
|
|
14580
15315
|
const resolvedProps = await this.resolver.resolve(desiredProps, context);
|
|
15316
|
+
this.auditResolvedAssetReferences(logicalId, resourceType, resolvedProps);
|
|
14581
15317
|
if (JSON.stringify(resolvedProps) === JSON.stringify(currentProps)) {
|
|
14582
15318
|
if (change.attributeChanges && change.attributeChanges.length > 0) {
|
|
14583
15319
|
const attrSummary = change.attributeChanges.map((a) => `${a.attribute}: ${a.oldValue ?? "(unset)"} → ${a.newValue ?? "(unset)"}`).join(", ");
|
|
@@ -15000,5 +15736,5 @@ var DeployEngine = class {
|
|
|
15000
15736
|
};
|
|
15001
15737
|
|
|
15002
15738
|
//#endregion
|
|
15003
|
-
export {
|
|
15004
|
-
//# sourceMappingURL=deploy-engine-
|
|
15739
|
+
export { AssetManifestLoader as $, DiffCalculator as A, buildAssetRedirectMap as B, findActionableSilentDrops as C, refStateLookupFromResource as D, cfnRefValueFromPhysicalId as E, rebuildClientForBucketRegion as F, BOOTSTRAP_MARKER_PREFIX as G, loadPublishableAssetManifest as H, shouldRetainResource as I, buildDockerImage as J, ensureAssetStorage as K, AssetPublisher as L, TemplateParser as M, LockManager as N, WAFv2WebACLProvider as O, S3StateBackend as P, runDockerStreaming as Q, stringifyValue as R, ProviderRegistry as S, IntrinsicFunctionResolver as T, rewriteTemplateAssetReferences as U, createAssetRedirectResolver as V, AssetModeResolver as W, getDockerCmd as X, formatDockerLoginError as Y, runDockerForeground as Z, green as _, AssemblyReader as _t, withRetry as a, resolveApp as at, IAMRoleProvider as b, computeImplicitDeleteEdges as c, resolveStateBucketWithDefault as ct, isStatefulRecreateTargetSync as d, warnDeprecatedNoPrefixCliFlag as dt, getDockerImageBySourceHash as et, renderStatefulReason as f, CFN_TEMPLATE_BODY_LIMIT as ft, gray as g, uploadCfnTemplate as gt, cyan as h, findLargeInlineResources as ht, withResourceDeadline as i, getLegacyStateBucketName as it, DagBuilder as j, applyRoleArnIfSet as k, extractDeploymentEventError as l, resolveStateBucketWithDefaultAndSource as lt, bold as m, MIGRATE_TMP_PREFIX as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, synthesisStatusMessage as nt, isRetryableTransientError as o, resolveCaptureObservedState as ot, formatResourceLine as p, CFN_TEMPLATE_URL_LIMIT as pt, parseBootstrapMarker as q, DeployEngine as r, getDefaultStateBucketName as rt, IMPLICIT_DELETE_DEPENDENCIES as s, resolveSkipPrefix as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, Synthesizer as tt, MULTI_REGION_RECREATE_BLOCKED_TYPES as u, resolveUseCdkBootstrapAssets as ut, red as v, clearBucketRegionCache as vt, CloudControlProvider as w, collectInlinePolicyNamesManagedBySiblings as x, yellow as y, resolveBucketRegion as yt, WorkGraph as z };
|
|
15740
|
+
//# sourceMappingURL=deploy-engine-Dj9mXUQa.js.map
|