@go-to-k/cdkd 0.162.2 → 0.163.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/dist/cli.js +201 -22
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1004,9 +1004,12 @@ const MULTI_REGION_RECREATE_BLOCKED_TYPES = new Set(["AWS::DynamoDB::GlobalTable
|
|
|
1004
1004
|
* Cheap, synchronous read of the resource's recorded properties only.
|
|
1005
1005
|
* For `AWS::S3::Bucket` this returns `null` — the live `ListObjectsV2`
|
|
1006
1006
|
* probe to distinguish empty buckets (safe to recreate) from
|
|
1007
|
-
* non-empty (data loss)
|
|
1008
|
-
*
|
|
1009
|
-
*
|
|
1007
|
+
* non-empty (data loss) lives in
|
|
1008
|
+
* `src/deployment/recreate-targets.ts#probeStatefulRecreateTargetsAsync`
|
|
1009
|
+
* (issue [#648]) and runs after this sync first-cut. Sync callers can
|
|
1010
|
+
* still treat `null` as "not stateful" — the deploy command does both
|
|
1011
|
+
* passes back-to-back; only callers that explicitly opt out of the
|
|
1012
|
+
* async probe need to assume conservative "stateful" semantics.
|
|
1010
1013
|
*
|
|
1011
1014
|
* Returns the {@link StatefulReason} when the type is stateful (or
|
|
1012
1015
|
* `null` for non-stateful types).
|
|
@@ -1038,11 +1041,47 @@ function renderStatefulReason(reason) {
|
|
|
1038
1041
|
//#endregion
|
|
1039
1042
|
//#region src/deployment/recreate-targets.ts
|
|
1040
1043
|
/**
|
|
1044
|
+
* Pre-flight validation for `--recreate-via-cc-api <LogicalId>` deploy
|
|
1045
|
+
* flag (issue [#615]).
|
|
1046
|
+
*
|
|
1047
|
+
* Three things to validate before the deploy engine acts on the user's
|
|
1048
|
+
* recreate list:
|
|
1049
|
+
*
|
|
1050
|
+
* 1. Every named logical id MUST exist in the synth template. A typo
|
|
1051
|
+
* should fail fast, not silently skip.
|
|
1052
|
+
* 2. Every named logical id MUST exist in cdkd state (the recreate
|
|
1053
|
+
* operation requires an existing physical resource to destroy +
|
|
1054
|
+
* recreate). A logical id in the template but absent from state
|
|
1055
|
+
* is a CREATE on the next deploy regardless — recreate is a
|
|
1056
|
+
* no-op for fresh deploys and should error out with a clear
|
|
1057
|
+
* message rather than silently apply.
|
|
1058
|
+
* 3. Stateful-resource guard: every named target whose resource type
|
|
1059
|
+
* is in {@link STATEFUL_TYPES} (or conditionally stateful — S3
|
|
1060
|
+
* bucket with objects, LogGroup with retention) MUST be matched
|
|
1061
|
+
* by an explicit `--force-stateful-recreation` flag. The sync
|
|
1062
|
+
* first-cut runs from the recorded properties alone; the live
|
|
1063
|
+
* `s3:ListObjectsV2` probe (issue [#648]) promotes a `null`
|
|
1064
|
+
* reason to `'has-objects'` when a bucket actually contains data.
|
|
1065
|
+
* 4. Multi-region refusal: every named target whose resource type
|
|
1066
|
+
* is in {@link MULTI_REGION_RECREATE_BLOCKED_TYPES} (e.g.
|
|
1067
|
+
* `AWS::DynamoDB::GlobalTable`) is refused outright. Out of
|
|
1068
|
+
* scope for v1; no `--force-stateful-recreation` bypass since
|
|
1069
|
+
* this is a structural limitation, not a data-loss footgun.
|
|
1070
|
+
*
|
|
1071
|
+
* Plus one cross-flag invariant: `--recreate-via-cc-api MyLambda`
|
|
1072
|
+
* combined with `--allow-unsupported-properties AWS::Lambda::Function:LoggingConfig`
|
|
1073
|
+
* on a resource whose template carries `LoggingConfig` is **ambiguous
|
|
1074
|
+
* intent** — does the user want SDK + silent drop, or CC migration?
|
|
1075
|
+
* Fail fast and let the user pick one strategy per resource.
|
|
1076
|
+
*/
|
|
1077
|
+
/**
|
|
1041
1078
|
* Plan-time validation of the user's recreate-via-cc-api list.
|
|
1042
1079
|
*
|
|
1043
|
-
* Pure with respect to AWS — does NOT probe S3 bucket emptiness.
|
|
1044
|
-
*
|
|
1045
|
-
* `
|
|
1080
|
+
* Pure with respect to AWS — does NOT probe S3 bucket emptiness. Wrap
|
|
1081
|
+
* the result with {@link probeAndRevalidateStateful} to promote S3
|
|
1082
|
+
* targets' `statefulReason` via a live `s3:ListObjectsV2` round-trip
|
|
1083
|
+
* before rendering errors. The deploy command does this; the validator
|
|
1084
|
+
* itself stays sync so unit tests don't need an S3 mock.
|
|
1046
1085
|
*
|
|
1047
1086
|
* Input order is preserved; duplicate logical ids in the user's input
|
|
1048
1087
|
* are deduplicated.
|
|
@@ -1137,6 +1176,84 @@ function renderRecreateTargetsErrors(validation) {
|
|
|
1137
1176
|
}
|
|
1138
1177
|
return lines.length > 0 ? lines.join("\n") : null;
|
|
1139
1178
|
}
|
|
1179
|
+
/**
|
|
1180
|
+
* Async S3 object probe (issue [#648]).
|
|
1181
|
+
*
|
|
1182
|
+
* For every `AWS::S3::Bucket` target whose sync {@link StatefulReason}
|
|
1183
|
+
* is `null` (the sync map defers — see {@link isStatefulRecreateTargetSync}),
|
|
1184
|
+
* issues a single-page `ListObjectVersions(MaxKeys=1)` against the
|
|
1185
|
+
* bucket's recorded physical id. When the bucket has at least one
|
|
1186
|
+
* current object, prior version, OR delete-marker, promotes the
|
|
1187
|
+
* target's `statefulReason` to `'has-objects'`.
|
|
1188
|
+
*
|
|
1189
|
+
* Uses `ListObjectVersions` rather than `ListObjectsV2` so the probe
|
|
1190
|
+
* mirrors the s3-bucket-provider's `emptyBucket` view: a versioned
|
|
1191
|
+
* bucket whose current keys have all been soft-deleted (so
|
|
1192
|
+
* `ListObjectsV2.KeyCount === 0`) still holds prior versions +
|
|
1193
|
+
* delete-markers that the destroy + recreate cycle would lose. Using
|
|
1194
|
+
* the same listing API as the provider ensures the probe and the
|
|
1195
|
+
* destroy path agree on "empty".
|
|
1196
|
+
*
|
|
1197
|
+
* **Soft-fail on probe errors**: if `ListObjectVersions` throws
|
|
1198
|
+
* (permission denied, bucket-not-found mid-flight, transient network
|
|
1199
|
+
* error), logs a warn and leaves the target's `statefulReason` at the
|
|
1200
|
+
* sync result (`null`). The user can decide to proceed without the
|
|
1201
|
+
* probe by passing `--force-stateful-recreation`.
|
|
1202
|
+
*
|
|
1203
|
+
* Returns a NEW array of targets; the input is not mutated. Non-S3
|
|
1204
|
+
* targets and S3 targets whose sync reason is already non-null are
|
|
1205
|
+
* passed through unchanged.
|
|
1206
|
+
*/
|
|
1207
|
+
async function probeStatefulRecreateTargetsAsync(targets, s3Client, logger = getLogger().child("recreate-targets")) {
|
|
1208
|
+
const promoted = [];
|
|
1209
|
+
for (const target of targets) {
|
|
1210
|
+
if (target.resourceType !== "AWS::S3::Bucket" || target.statefulReason !== null) {
|
|
1211
|
+
promoted.push({ ...target });
|
|
1212
|
+
continue;
|
|
1213
|
+
}
|
|
1214
|
+
try {
|
|
1215
|
+
const result = await s3Client.send(new ListObjectVersionsCommand({
|
|
1216
|
+
Bucket: target.physicalId,
|
|
1217
|
+
MaxKeys: 1
|
|
1218
|
+
}));
|
|
1219
|
+
const hasVersions = (result.Versions?.length ?? 0) > 0;
|
|
1220
|
+
const hasDeleteMarkers = (result.DeleteMarkers?.length ?? 0) > 0;
|
|
1221
|
+
if (hasVersions || hasDeleteMarkers) promoted.push({
|
|
1222
|
+
...target,
|
|
1223
|
+
statefulReason: "has-objects"
|
|
1224
|
+
});
|
|
1225
|
+
else promoted.push({ ...target });
|
|
1226
|
+
} catch (e) {
|
|
1227
|
+
logger.warn(`--recreate-via-cc-api: live S3 probe failed for ${target.logicalId} (bucket ${target.physicalId}); leaving stateful guard at the sync result. If the bucket might be non-empty, re-run with --force-stateful-recreation. Underlying error: ${e instanceof Error ? e.message : String(e)}`);
|
|
1228
|
+
promoted.push({ ...target });
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
return promoted;
|
|
1232
|
+
}
|
|
1233
|
+
/**
|
|
1234
|
+
* Async re-validation of the stateful-guard slice of a
|
|
1235
|
+
* {@link RecreateTargetsValidation}, after promoting S3 bucket reasons
|
|
1236
|
+
* via {@link probeStatefulRecreateTargetsAsync}.
|
|
1237
|
+
*
|
|
1238
|
+
* Skips the probe entirely when `forceStatefulRecreation: true` — the
|
|
1239
|
+
* sync validation already omits the blocked list in that case, and
|
|
1240
|
+
* skipping avoids an unnecessary AWS round-trip (plus permission-denied
|
|
1241
|
+
* warn-and-skip cycle on low-privilege CI roles).
|
|
1242
|
+
*
|
|
1243
|
+
* Returns a NEW validation; the input is not mutated. Non-stateful
|
|
1244
|
+
* categories (`unknownLogicalIds` / `missingFromState` /
|
|
1245
|
+
* `ambiguousIntent` / `blockedMultiRegionTargets`) are preserved verbatim.
|
|
1246
|
+
*/
|
|
1247
|
+
async function probeAndRevalidateStateful(input) {
|
|
1248
|
+
if (input.forceStatefulRecreation) return input.validation;
|
|
1249
|
+
const promoted = await probeStatefulRecreateTargetsAsync(input.validation.targets, input.s3Client);
|
|
1250
|
+
const blockedStatefulTargets = promoted.filter((t) => t.statefulReason !== null);
|
|
1251
|
+
return {
|
|
1252
|
+
...input.validation,
|
|
1253
|
+
targets: promoted,
|
|
1254
|
+
blockedStatefulTargets
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1140
1257
|
|
|
1141
1258
|
//#endregion
|
|
1142
1259
|
//#region src/state/export-index-store.ts
|
|
@@ -33597,18 +33714,22 @@ async function deployCommand(stacks, options) {
|
|
|
33597
33714
|
let recreateViaCcApiTargets;
|
|
33598
33715
|
if (options.recreateViaCcApi?.length) {
|
|
33599
33716
|
const stateForRecreateCheck = await stackStateBackend.getState(stackInfo.stackName, stackRegion);
|
|
33600
|
-
const validation =
|
|
33601
|
-
|
|
33602
|
-
|
|
33603
|
-
|
|
33604
|
-
|
|
33605
|
-
|
|
33606
|
-
|
|
33607
|
-
|
|
33608
|
-
|
|
33609
|
-
|
|
33610
|
-
|
|
33611
|
-
|
|
33717
|
+
const validation = await probeAndRevalidateStateful({
|
|
33718
|
+
validation: validateRecreateTargets({
|
|
33719
|
+
template: stackInfo.template,
|
|
33720
|
+
state: stateForRecreateCheck?.state ?? {
|
|
33721
|
+
version: 7,
|
|
33722
|
+
stackName: stackInfo.stackName,
|
|
33723
|
+
region: stackRegion,
|
|
33724
|
+
resources: {},
|
|
33725
|
+
outputs: {},
|
|
33726
|
+
lastModified: Date.now()
|
|
33727
|
+
},
|
|
33728
|
+
recreateViaCcApi: options.recreateViaCcApi,
|
|
33729
|
+
allowUnsupportedProperties: new Set(options.allowUnsupportedProperties ?? []),
|
|
33730
|
+
forceStatefulRecreation: options.forceStatefulRecreation ?? false
|
|
33731
|
+
}),
|
|
33732
|
+
s3Client: stackAwsClients.s3,
|
|
33612
33733
|
forceStatefulRecreation: options.forceStatefulRecreation ?? false
|
|
33613
33734
|
});
|
|
33614
33735
|
const errorBlock = renderRecreateTargetsErrors(validation);
|
|
@@ -54737,6 +54858,7 @@ async function localStartApiCommand(target, options) {
|
|
|
54737
54858
|
for (const [k, v] of direct) corsConfigByApiId.set(k, v);
|
|
54738
54859
|
}
|
|
54739
54860
|
const stateByStack = options.fromState || isCfnFlagPresent(options) ? await loadStateForRoutedStacks(targetStacks, routes, routesWithAuth, options) : /* @__PURE__ */ new Map();
|
|
54861
|
+
const profileCredentials = options.profile ? await resolveProfileCredentials(options.profile) : void 0;
|
|
54740
54862
|
const lambdaIds = uniqueLambdaIds(routes, routesWithAuth, webSocketApis);
|
|
54741
54863
|
const specs = /* @__PURE__ */ new Map();
|
|
54742
54864
|
for (let i = 0; i < lambdaIds.length; i++) {
|
|
@@ -54753,7 +54875,8 @@ async function localStartApiCommand(target, options) {
|
|
|
54753
54875
|
layerTmpDirs,
|
|
54754
54876
|
stateByStack,
|
|
54755
54877
|
skipPull: options.pull === false,
|
|
54756
|
-
...options.layerRoleArn !== void 0 && { layerRoleArn: options.layerRoleArn }
|
|
54878
|
+
...options.layerRoleArn !== void 0 && { layerRoleArn: options.layerRoleArn },
|
|
54879
|
+
...profileCredentials && { profileCredentials }
|
|
54757
54880
|
});
|
|
54758
54881
|
specs.set(logicalId, spec);
|
|
54759
54882
|
}
|
|
@@ -55187,7 +55310,7 @@ function warnIamRoutes(routesWithAuth) {
|
|
|
55187
55310
|
* missing, runtime not supported).
|
|
55188
55311
|
*/
|
|
55189
55312
|
async function buildContainerSpec(args) {
|
|
55190
|
-
const { logicalId, stacks, overrides, assumeRole, containerHost, debugPort, stsRegion, inlineTmpDirs, layerTmpDirs, stateByStack, skipPull, layerRoleArn } = args;
|
|
55313
|
+
const { logicalId, stacks, overrides, assumeRole, containerHost, debugPort, stsRegion, inlineTmpDirs, layerTmpDirs, stateByStack, skipPull, layerRoleArn, profileCredentials } = args;
|
|
55191
55314
|
const lambda = resolveLambdaByLogicalId(logicalId, stacks);
|
|
55192
55315
|
let codeDir;
|
|
55193
55316
|
let optDir;
|
|
@@ -55233,7 +55356,15 @@ async function buildContainerSpec(args) {
|
|
|
55233
55356
|
dockerEnv["AWS_SECRET_ACCESS_KEY"] = creds.secretAccessKey;
|
|
55234
55357
|
dockerEnv["AWS_SESSION_TOKEN"] = creds.sessionToken;
|
|
55235
55358
|
if (stsRegion) dockerEnv["AWS_REGION"] = stsRegion;
|
|
55236
|
-
} else
|
|
55359
|
+
} else {
|
|
55360
|
+
forwardAwsEnv$1(dockerEnv);
|
|
55361
|
+
if (profileCredentials) {
|
|
55362
|
+
dockerEnv["AWS_ACCESS_KEY_ID"] = profileCredentials.accessKeyId;
|
|
55363
|
+
dockerEnv["AWS_SECRET_ACCESS_KEY"] = profileCredentials.secretAccessKey;
|
|
55364
|
+
if (profileCredentials.sessionToken) dockerEnv["AWS_SESSION_TOKEN"] = profileCredentials.sessionToken;
|
|
55365
|
+
else delete dockerEnv["AWS_SESSION_TOKEN"];
|
|
55366
|
+
}
|
|
55367
|
+
}
|
|
55237
55368
|
if (debugPort !== void 0) dockerEnv["NODE_OPTIONS"] = `--inspect-brk=0.0.0.0:${debugPort}`;
|
|
55238
55369
|
const tmpfs = lambda.ephemeralStorageMb !== void 0 ? {
|
|
55239
55370
|
target: "/tmp",
|
|
@@ -55596,6 +55727,54 @@ function forwardAwsEnv$1(env) {
|
|
|
55596
55727
|
}
|
|
55597
55728
|
}
|
|
55598
55729
|
/**
|
|
55730
|
+
* Issue #654: resolve `--profile <p>` to a concrete credential set
|
|
55731
|
+
* for forwarding to Lambda containers.
|
|
55732
|
+
*
|
|
55733
|
+
* The dev's AWS credentials may live in any of:
|
|
55734
|
+
* - `~/.aws/sso/cache/*.json` (AWS IAM Identity Center / legacy SSO)
|
|
55735
|
+
* - `~/.aws/credentials` (regular long-lived access keys)
|
|
55736
|
+
* - `~/.aws/config` profiles with `role_arn` + `source_profile` (chained AssumeRole)
|
|
55737
|
+
* - `credential_process` external resolvers
|
|
55738
|
+
*
|
|
55739
|
+
* `forwardAwsEnv` only reads `process.env.AWS_*`, which is empty for
|
|
55740
|
+
* every shape except "user manually exported the env vars". The
|
|
55741
|
+
* Lambda container therefore boots without creds and the handler's
|
|
55742
|
+
* AWS SDK call fails with `Could not load credentials from any providers`.
|
|
55743
|
+
*
|
|
55744
|
+
* This helper constructs a transient `STSClient({ profile })` to drive
|
|
55745
|
+
* the SDK's default credential provider chain — same code path cdkd's
|
|
55746
|
+
* own CFn / CC API clients use when `--profile` is set, so SSO / IAM
|
|
55747
|
+
* Identity Center / role-assumption profiles all resolve the same way
|
|
55748
|
+
* they already do for cdkd's outbound calls. We then extract the
|
|
55749
|
+
* resolved `AwsCredentialIdentity` via `sts.config.credentials()` and
|
|
55750
|
+
* return the underlying `{ accessKeyId, secretAccessKey, sessionToken? }`
|
|
55751
|
+
* for env-var injection.
|
|
55752
|
+
*
|
|
55753
|
+
* Called ONCE at server boot; the resolved creds are reused for every
|
|
55754
|
+
* Lambda container's env overlay (when `--assume-role` is not set for
|
|
55755
|
+
* that Lambda — assume-role wins per the existing precedence). SSO
|
|
55756
|
+
* temp creds typically last 1h+, so a single resolve is fine for the
|
|
55757
|
+
* common dev session; long-running `--watch` sessions that outlive
|
|
55758
|
+
* the creds need a cdkd restart (deferred refresh out of scope for
|
|
55759
|
+
* v1, see issue #654).
|
|
55760
|
+
*/
|
|
55761
|
+
async function resolveProfileCredentials(profile) {
|
|
55762
|
+
const { STSClient } = await import("@aws-sdk/client-sts");
|
|
55763
|
+
const sts = new STSClient({ profile });
|
|
55764
|
+
try {
|
|
55765
|
+
const credsProvider = sts.config.credentials;
|
|
55766
|
+
const creds = typeof credsProvider === "function" ? await credsProvider() : credsProvider;
|
|
55767
|
+
if (!creds || !creds.accessKeyId || !creds.secretAccessKey) throw new Error(`--profile '${profile}': credential provider chain resolved without usable credentials. Check \`aws sso login --profile ` + profile + "` for SSO profiles, or `~/.aws/credentials` / `~/.aws/config` for regular profiles.");
|
|
55768
|
+
return {
|
|
55769
|
+
accessKeyId: creds.accessKeyId,
|
|
55770
|
+
secretAccessKey: creds.secretAccessKey,
|
|
55771
|
+
...creds.sessionToken && { sessionToken: creds.sessionToken }
|
|
55772
|
+
};
|
|
55773
|
+
} finally {
|
|
55774
|
+
sts.destroy();
|
|
55775
|
+
}
|
|
55776
|
+
}
|
|
55777
|
+
/**
|
|
55599
55778
|
* Issue an STS AssumeRole and return temporary credentials. Mirrors
|
|
55600
55779
|
* `cdkd local invoke`'s helper byte-for-byte; lifted here so the
|
|
55601
55780
|
* start-api command stays self-contained.
|
|
@@ -60270,7 +60449,7 @@ function reorderArgs(argv) {
|
|
|
60270
60449
|
*/
|
|
60271
60450
|
async function main() {
|
|
60272
60451
|
const program = new Command();
|
|
60273
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.
|
|
60452
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.163.0");
|
|
60274
60453
|
program.addCommand(createBootstrapCommand());
|
|
60275
60454
|
program.addCommand(createSynthCommand());
|
|
60276
60455
|
program.addCommand(createListCommand());
|