@go-to-k/cdkd 0.120.0 → 0.122.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 +8 -4
- package/dist/cli.js +522 -27
- package/dist/cli.js.map +1 -1
- package/dist/{deploy-engine-Chzg_hDE.js → deploy-engine-B2RZT3ai.js} +46 -11
- package/dist/deploy-engine-B2RZT3ai.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/deploy-engine-Chzg_hDE.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { a as setAwsClients, i as resetAwsClients, r as getAwsClients, t as AwsClients } from "./aws-clients-CuHRHcyW.js";
|
|
3
|
-
import { $ as
|
|
3
|
+
import { $ as LocalInvokeBuildError, A as stringifyValue, B as resolveApp, C as DiffCalculator, Ct as withSkipPrefix, D as S3StateBackend, E as LockManager, F as runDockerForeground, G as warnDeprecatedNoPrefixCliFlag, H as resolveSkipPrefix, I as runDockerStreaming, J as resolveBucketRegion, K as AssemblyReader, L as Synthesizer, M as buildDockerImage, N as formatDockerLoginError, O as shouldRetainResource, P as getDockerCmd, R as getDefaultStateBucketName, S as IntrinsicFunctionResolver, St as generateResourceNameWithFallback, T as TemplateParser, U as resolveStateBucketWithDefault, V as resolveCaptureObservedState, W as resolveStateBucketWithDefaultAndSource, X as CdkdError, _ as normalizeAwsTagsToCfn, _t as runStackBuffered, a as withRetry, at as RouteDiscoveryError, b as CloudControlProvider, bt as PATTERN_B_RESOURCE_TYPES, c as cyan, d as red, f as yellow, ft as normalizeAwsError, g as matchesCdkPath, h as CDK_PATH_TAG, ht as getLogger, i as withResourceDeadline, it as ResourceUpdateNotSupportedError, j as WorkGraph, k as AssetPublisher, l as gray, m as collectInlinePolicyNamesManagedBySiblings, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nt as ProvisioningError, o as IMPLICIT_DELETE_DEPENDENCIES, ot as StackHasActiveImportsError, p as IAMRoleProvider, pt as withErrorHandling, r as DeployEngine, rt as ResourceTimeoutError, s as bold, st as StackTerminationProtectionError, t as DEFAULT_RESOURCE_TIMEOUT_MS, tt as PartialFailureError, u as green, v as resolveExplicitPhysicalId, vt as getLiveRenderer, w as DagBuilder, wt as withStackName, x as assertRegionMatch, xt as generateResourceName, y as ProviderRegistry, yt as PATTERN_B_NAME_PROPERTIES, z as getLegacyStateBucketName } from "./deploy-engine-B2RZT3ai.js";
|
|
4
4
|
import { createHash, createHmac, createPublicKey, createVerify, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
5
5
|
import { CopyObjectCommand, CreateBucketCommand, DeleteBucketAnalyticsConfigurationCommand, DeleteBucketCommand, DeleteBucketCorsCommand, DeleteBucketIntelligentTieringConfigurationCommand, DeleteBucketInventoryConfigurationCommand, DeleteBucketLifecycleCommand, DeleteBucketMetricsConfigurationCommand, DeleteBucketPolicyCommand, DeleteBucketReplicationCommand, DeleteBucketTaggingCommand, DeleteBucketWebsiteCommand, DeleteObjectCommand, DeleteObjectsCommand, GetBucketAccelerateConfigurationCommand, GetBucketCorsCommand, GetBucketEncryptionCommand, GetBucketLifecycleConfigurationCommand, GetBucketLocationCommand, GetBucketLoggingCommand, GetBucketNotificationConfigurationCommand, GetBucketPolicyCommand, GetBucketReplicationCommand, GetBucketTaggingCommand, GetBucketVersioningCommand, GetBucketWebsiteCommand, GetObjectCommand, GetObjectLockConfigurationCommand, GetPublicAccessBlockCommand, HeadBucketCommand, ListBucketAnalyticsConfigurationsCommand, ListBucketIntelligentTieringConfigurationsCommand, ListBucketInventoryConfigurationsCommand, ListBucketMetricsConfigurationsCommand, ListBucketsCommand, ListDirectoryBucketsCommand, ListObjectVersionsCommand, ListObjectsV2Command, NoSuchBucket, PutBucketAccelerateConfigurationCommand, PutBucketAnalyticsConfigurationCommand, PutBucketCorsCommand, PutBucketEncryptionCommand, PutBucketIntelligentTieringConfigurationCommand, PutBucketInventoryConfigurationCommand, PutBucketLifecycleConfigurationCommand, PutBucketLoggingCommand, PutBucketMetricsConfigurationCommand, PutBucketNotificationConfigurationCommand, PutBucketOwnershipControlsCommand, PutBucketPolicyCommand, PutBucketReplicationCommand, PutBucketTaggingCommand, PutBucketVersioningCommand, PutBucketWebsiteCommand, PutObjectCommand, PutObjectLockConfigurationCommand, PutPublicAccessBlockCommand, S3Client, S3ServiceException } from "@aws-sdk/client-s3";
|
|
6
6
|
import { AddRoleToInstanceProfileCommand, AddUserToGroupCommand, AttachGroupPolicyCommand, AttachUserPolicyCommand, CreateGroupCommand, CreateInstanceProfileCommand, CreateLoginProfileCommand, CreateUserCommand, DeleteAccessKeyCommand, DeleteGroupCommand, DeleteGroupPolicyCommand, DeleteInstanceProfileCommand, DeleteLoginProfileCommand, DeleteRolePolicyCommand, DeleteUserCommand, DeleteUserPermissionsBoundaryCommand, DeleteUserPolicyCommand, DetachGroupPolicyCommand, DetachUserPolicyCommand, GetGroupCommand, GetGroupPolicyCommand, GetInstanceProfileCommand, GetRolePolicyCommand, GetUserCommand, GetUserPolicyCommand, IAMClient, ListAccessKeysCommand, ListAttachedGroupPoliciesCommand, ListAttachedUserPoliciesCommand, ListGroupPoliciesCommand, ListGroupsForUserCommand, ListInstanceProfilesCommand, ListUserPoliciesCommand, ListUserTagsCommand, ListUsersCommand, NoSuchEntityException, PutGroupPolicyCommand, PutRolePolicyCommand, PutUserPermissionsBoundaryCommand, PutUserPolicyCommand, RemoveRoleFromInstanceProfileCommand, RemoveUserFromGroupCommand, TagUserCommand, UntagUserCommand, UpdateLoginProfileCommand } from "@aws-sdk/client-iam";
|
|
@@ -696,7 +696,7 @@ async function synthCommand(options) {
|
|
|
696
696
|
const template = stacks[0].template;
|
|
697
697
|
process.stdout.write(toYaml(template));
|
|
698
698
|
}
|
|
699
|
-
logger.info(`\n
|
|
699
|
+
logger.info(`\n${green("✓")} ${bold("Synthesis complete!")} Found ${stacks.length} stack(s):`);
|
|
700
700
|
for (const stack of stacks) {
|
|
701
701
|
const resourceCount = countDeployableResources(stack.template);
|
|
702
702
|
const outputCount = Object.keys(stack.template.Outputs ?? {}).length;
|
|
@@ -30909,7 +30909,7 @@ async function deployCommand(stacks, options) {
|
|
|
30909
30909
|
};
|
|
30910
30910
|
process.on("SIGINT", topLevelSigintHandler);
|
|
30911
30911
|
try {
|
|
30912
|
-
logger.info("Synthesizing CDK app...");
|
|
30912
|
+
logger.info(cyan("Synthesizing CDK app..."));
|
|
30913
30913
|
const synthesizer = new Synthesizer();
|
|
30914
30914
|
const context = parseContextOptions(options.context);
|
|
30915
30915
|
const { stacks: allStacks } = await synthesizer.synthesize({
|
|
@@ -30995,7 +30995,7 @@ async function deployCommand(stacks, options) {
|
|
|
30995
30995
|
};
|
|
30996
30996
|
const runStackInner = async (stackInfo) => {
|
|
30997
30997
|
const stackRegion = stackInfo.region || baseRegion;
|
|
30998
|
-
logger.info(`\
|
|
30998
|
+
logger.info(`\n${cyan("Deploying stack:")} ${bold(cyan(stackInfo.stackName))}${stackRegion !== baseRegion ? gray(` (region: ${stackRegion})`) : ""}`);
|
|
30999
30999
|
switchRegion(stackRegion);
|
|
31000
31000
|
const stackAwsClients = new AwsClients({
|
|
31001
31001
|
region: stackRegion,
|
|
@@ -31032,19 +31032,19 @@ async function deployCommand(stacks, options) {
|
|
|
31032
31032
|
...options.resourceWarnAfter?.perTypeMs && { resourceWarnAfterByType: options.resourceWarnAfter.perTypeMs },
|
|
31033
31033
|
...options.resourceTimeout?.perTypeMs && { resourceTimeoutByType: options.resourceTimeout.perTypeMs }
|
|
31034
31034
|
}, stackRegion, exportIndexStore).deploy(stackInfo.stackName, stackInfo.template);
|
|
31035
|
-
logger.info("
|
|
31036
|
-
logger.info(` Stack: ${deployResult.stackName}`);
|
|
31037
|
-
logger.info(` Created: ${deployResult.created}`);
|
|
31038
|
-
logger.info(` Updated: ${deployResult.updated}`);
|
|
31039
|
-
logger.info(` Deleted: ${deployResult.deleted}`);
|
|
31040
|
-
logger.info(` Unchanged: ${deployResult.unchanged}`);
|
|
31041
|
-
logger.info(` Duration: ${(deployResult.durationMs / 1e3).toFixed(2)}
|
|
31035
|
+
logger.info(`\n${bold("Deployment Summary:")}`);
|
|
31036
|
+
logger.info(` Stack: ${bold(cyan(deployResult.stackName))}`);
|
|
31037
|
+
logger.info(` Created: ${deployResult.created > 0 ? green(deployResult.created) : gray(deployResult.created)}`);
|
|
31038
|
+
logger.info(` Updated: ${deployResult.updated > 0 ? yellow(deployResult.updated) : gray(deployResult.updated)}`);
|
|
31039
|
+
logger.info(` Deleted: ${deployResult.deleted > 0 ? red(deployResult.deleted) : gray(deployResult.deleted)}`);
|
|
31040
|
+
logger.info(` Unchanged: ${gray(deployResult.unchanged)}`);
|
|
31041
|
+
logger.info(` Duration: ${cyan((deployResult.durationMs / 1e3).toFixed(2) + "s")}`);
|
|
31042
31042
|
if (deployResult.outputs && Object.keys(deployResult.outputs).length > 0) {
|
|
31043
31043
|
logger.info("\nOutputs:");
|
|
31044
31044
|
for (const [key, value] of Object.entries(deployResult.outputs)) logger.info(` ${deployResult.stackName}.${key} = ${String(value)}`);
|
|
31045
31045
|
}
|
|
31046
|
-
if (options.dryRun) logger.info("
|
|
31047
|
-
else logger.info("
|
|
31046
|
+
if (options.dryRun) logger.info(`\n${green("✓")} ${bold("Dry run completed")} - no actual changes made`);
|
|
31047
|
+
else logger.info(`\n${green("✓")} ${bold("Deployment completed successfully")}`);
|
|
31048
31048
|
} finally {
|
|
31049
31049
|
stackAwsClients.destroy();
|
|
31050
31050
|
stateS3Client.destroy();
|
|
@@ -32239,7 +32239,7 @@ async function runDestroyForStack(stackName, state, ctx) {
|
|
|
32239
32239
|
if (resourceCount === 0) {
|
|
32240
32240
|
logger.info(`Stack ${stackName} has no resources, cleaning up state...`);
|
|
32241
32241
|
await ctx.stateBackend.deleteState(stackName, regionForState);
|
|
32242
|
-
logger.info("✓ State deleted
|
|
32242
|
+
logger.info(`${green("✓")} State deleted`);
|
|
32243
32243
|
result.skippedEmpty = true;
|
|
32244
32244
|
return result;
|
|
32245
32245
|
}
|
|
@@ -32399,7 +32399,7 @@ async function runDestroyForStack(stackName, state, ctx) {
|
|
|
32399
32399
|
onTimeout: (elapsedMs) => new ResourceTimeoutError(logicalId, resource.resourceType, stackRegion, elapsedMs, "DELETE", timeoutMs)
|
|
32400
32400
|
});
|
|
32401
32401
|
renderer.removeTask(logicalId);
|
|
32402
|
-
logger.info(`
|
|
32402
|
+
logger.info(` ${red("✗")} ${bold(logicalId)} ${gray(`(${resource.resourceType})`)} ${red("deleted")}`);
|
|
32403
32403
|
result.deletedCount++;
|
|
32404
32404
|
} catch (error) {
|
|
32405
32405
|
renderer.removeTask(logicalId);
|
|
@@ -32427,8 +32427,8 @@ async function runDestroyForStack(stackName, state, ctx) {
|
|
|
32427
32427
|
if (ctx.exportIndexStore) await ctx.exportIndexStore.removeStack(stackName, regionForState);
|
|
32428
32428
|
} else logger.warn(`${result.errorCount} resource(s) failed to delete. State preserved.`);
|
|
32429
32429
|
const retainedSuffix = result.retainedCount > 0 ? `, ${result.retainedCount} retained` : "";
|
|
32430
|
-
if (result.errorCount === 0) logger.info(`\n✓ Stack ${stackName} destroyed (${result.deletedCount} deleted${retainedSuffix}, ${result.errorCount} errors)`);
|
|
32431
|
-
else logger.warn(`\n⚠ Stack ${stackName} partially destroyed (${result.deletedCount} deleted${retainedSuffix}, ${result.errorCount} errors). State preserved — re-run 'cdkd destroy' / 'cdkd state destroy' to clean up.`);
|
|
32430
|
+
if (result.errorCount === 0) logger.info(`\n${green("✓")} ${bold(`Stack ${stackName} destroyed`)} (${green(result.deletedCount)} deleted${retainedSuffix}, ${result.errorCount} errors)`);
|
|
32431
|
+
else logger.warn(`\n${yellow("⚠")} ${bold(`Stack ${stackName} partially destroyed`)} (${green(result.deletedCount)} deleted${retainedSuffix}, ${red(result.errorCount)} errors). State preserved — re-run 'cdkd destroy' / 'cdkd state destroy' to clean up.`);
|
|
32432
32432
|
} finally {
|
|
32433
32433
|
renderer.stop();
|
|
32434
32434
|
logger.debug("Releasing lock...");
|
|
@@ -33466,7 +33466,7 @@ async function publishAssetsCommand(stacks, options) {
|
|
|
33466
33466
|
logger.info(` ${tag} ${id} — ${detail}`);
|
|
33467
33467
|
}
|
|
33468
33468
|
if (failed.length > 0) throw new PartialFailureError(`Asset publishing completed with ${failed.length} stack failure(s) (${totalAssets} asset(s) published successfully across the rest).`);
|
|
33469
|
-
logger.info(`\n
|
|
33469
|
+
logger.info(`\n${green("✓")} ${bold("Asset publishing complete")} (${totalAssets} asset(s))`);
|
|
33470
33470
|
}
|
|
33471
33471
|
/**
|
|
33472
33472
|
* Create publish-assets command.
|
|
@@ -35768,6 +35768,119 @@ async function loadStateForStack(stackName, synthRegion, opts) {
|
|
|
35768
35768
|
resetAwsClients();
|
|
35769
35769
|
}
|
|
35770
35770
|
}
|
|
35771
|
+
/**
|
|
35772
|
+
* Build a {@link CrossStackResolver} that walks cdkd's S3 state to look
|
|
35773
|
+
* up `Fn::ImportValue` / `Fn::GetStackOutput` references the same way
|
|
35774
|
+
* `cdkd deploy`'s `IntrinsicFunctionResolver` does. Returns `undefined`
|
|
35775
|
+
* when the state bucket cannot be resolved (warn + fall back; matches
|
|
35776
|
+
* `loadStateForStack`'s policy).
|
|
35777
|
+
*
|
|
35778
|
+
* The returned `dispose` closes the AWS clients owned by the resolver
|
|
35779
|
+
* when the caller is done — callers MUST call it (typically in a
|
|
35780
|
+
* `try / finally`) so the per-request S3 client isn't leaked across the
|
|
35781
|
+
* CLI's lifetime.
|
|
35782
|
+
*
|
|
35783
|
+
* Why a separate AwsClients instance from `loadStateForStack`: the
|
|
35784
|
+
* existing helper destroys its clients in a `finally` immediately after
|
|
35785
|
+
* loading the consumer stack's state. The cross-stack resolver lives
|
|
35786
|
+
* longer — every env-var that references a cross-stack output triggers a
|
|
35787
|
+
* new state read. Owning a fresh `AwsClients` here gives the resolver
|
|
35788
|
+
* an independent lifetime managed by the caller.
|
|
35789
|
+
*
|
|
35790
|
+
* Same-account / same-region only in v1 (the resolver's `producerRegion`
|
|
35791
|
+
* arg is honored, but only for state lookups within the same cdkd state
|
|
35792
|
+
* bucket). Cross-region `Fn::ImportValue` is tracked under #451;
|
|
35793
|
+
* cross-account `Fn::GetStackOutput.RoleArn` is tracked under #449.
|
|
35794
|
+
*/
|
|
35795
|
+
async function buildCrossStackResolver(consumerRegion, opts) {
|
|
35796
|
+
const logger = getLogger();
|
|
35797
|
+
const prefix = opts.logPrefix ?? "--from-state";
|
|
35798
|
+
let stateBucket;
|
|
35799
|
+
try {
|
|
35800
|
+
stateBucket = await resolveStateBucketWithDefault(opts.stateBucket, consumerRegion);
|
|
35801
|
+
} catch (err) {
|
|
35802
|
+
logger.warn(`${prefix}: cross-stack resolver could not resolve state bucket: ${err instanceof Error ? err.message : String(err)}. Fn::ImportValue / Fn::GetStackOutput env entries will warn-and-drop.`);
|
|
35803
|
+
return;
|
|
35804
|
+
}
|
|
35805
|
+
const awsClients = new AwsClients({
|
|
35806
|
+
...opts.region !== void 0 && { region: opts.region },
|
|
35807
|
+
...opts.profile !== void 0 && { profile: opts.profile }
|
|
35808
|
+
});
|
|
35809
|
+
const stateConfig = {
|
|
35810
|
+
bucket: stateBucket,
|
|
35811
|
+
prefix: opts.statePrefix
|
|
35812
|
+
};
|
|
35813
|
+
const stateBackend = new S3StateBackend(awsClients.s3, stateConfig, {
|
|
35814
|
+
...opts.region !== void 0 && { region: opts.region },
|
|
35815
|
+
...opts.profile !== void 0 && { profile: opts.profile }
|
|
35816
|
+
});
|
|
35817
|
+
try {
|
|
35818
|
+
await stateBackend.verifyBucketExists();
|
|
35819
|
+
} catch (err) {
|
|
35820
|
+
awsClients.destroy();
|
|
35821
|
+
logger.warn(`${prefix}: cross-stack resolver could not access state bucket '${stateBucket}': ${err instanceof Error ? err.message : String(err)}. Fn::ImportValue / Fn::GetStackOutput env entries will warn-and-drop.`);
|
|
35822
|
+
return;
|
|
35823
|
+
}
|
|
35824
|
+
const exportIndex = new ExportIndexStore(awsClients.s3, stateBucket, opts.statePrefix, consumerRegion, stateBackend);
|
|
35825
|
+
return {
|
|
35826
|
+
resolver: {
|
|
35827
|
+
async resolveImport(exportName) {
|
|
35828
|
+
try {
|
|
35829
|
+
const entry = await exportIndex.lookup(exportName);
|
|
35830
|
+
if (entry) {
|
|
35831
|
+
const value = entry.value;
|
|
35832
|
+
if (typeof value === "string") return value;
|
|
35833
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
35834
|
+
return JSON.stringify(value);
|
|
35835
|
+
}
|
|
35836
|
+
} catch (err) {
|
|
35837
|
+
logger.debug(`${prefix}: exports index lookup failed for '${exportName}': ${err instanceof Error ? err.message : String(err)}; falling back to per-stack state scan`);
|
|
35838
|
+
}
|
|
35839
|
+
let refs;
|
|
35840
|
+
try {
|
|
35841
|
+
refs = await stateBackend.listStacks();
|
|
35842
|
+
} catch (err) {
|
|
35843
|
+
logger.debug(`${prefix}: failed to list stacks during Fn::ImportValue fallback for '${exportName}': ${err instanceof Error ? err.message : String(err)}`);
|
|
35844
|
+
return;
|
|
35845
|
+
}
|
|
35846
|
+
for (const ref of refs) {
|
|
35847
|
+
const region = ref.region ?? consumerRegion;
|
|
35848
|
+
if (region !== consumerRegion) continue;
|
|
35849
|
+
try {
|
|
35850
|
+
const got = await stateBackend.getState(ref.stackName, region);
|
|
35851
|
+
if (!got || !got.state.outputs) continue;
|
|
35852
|
+
if (exportName in got.state.outputs) {
|
|
35853
|
+
const value = got.state.outputs[exportName];
|
|
35854
|
+
if (typeof value === "string") return value;
|
|
35855
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
35856
|
+
return JSON.stringify(value);
|
|
35857
|
+
}
|
|
35858
|
+
} catch (err) {
|
|
35859
|
+
logger.debug(`${prefix}: state read failed for ${ref.stackName} (${region}) during Fn::ImportValue fallback: ${err instanceof Error ? err.message : String(err)}`);
|
|
35860
|
+
continue;
|
|
35861
|
+
}
|
|
35862
|
+
}
|
|
35863
|
+
},
|
|
35864
|
+
async resolveGetStackOutput(producerStack, producerRegion, outputName) {
|
|
35865
|
+
try {
|
|
35866
|
+
const got = await stateBackend.getState(producerStack, producerRegion);
|
|
35867
|
+
if (!got || !got.state.outputs) return void 0;
|
|
35868
|
+
if (!(outputName in got.state.outputs)) return void 0;
|
|
35869
|
+
const value = got.state.outputs[outputName];
|
|
35870
|
+
if (typeof value === "string") return value;
|
|
35871
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
35872
|
+
return JSON.stringify(value);
|
|
35873
|
+
} catch (err) {
|
|
35874
|
+
logger.debug(`${prefix}: state read failed for Fn::GetStackOutput '${producerStack}.${outputName}' (${producerRegion}): ${err instanceof Error ? err.message : String(err)}`);
|
|
35875
|
+
return;
|
|
35876
|
+
}
|
|
35877
|
+
}
|
|
35878
|
+
},
|
|
35879
|
+
dispose: () => {
|
|
35880
|
+
awsClients.destroy();
|
|
35881
|
+
}
|
|
35882
|
+
};
|
|
35883
|
+
}
|
|
35771
35884
|
|
|
35772
35885
|
//#endregion
|
|
35773
35886
|
//#region src/local/intrinsic-image.ts
|
|
@@ -36733,6 +36846,159 @@ function resolveJoin(arg, context) {
|
|
|
36733
36846
|
};
|
|
36734
36847
|
}
|
|
36735
36848
|
/**
|
|
36849
|
+
* Async sibling of {@link substituteAgainstState}. Same semantics for every
|
|
36850
|
+
* intrinsic the sync path supports; additionally consults the
|
|
36851
|
+
* `crossStackResolver` (when supplied on the context) for `Fn::ImportValue`
|
|
36852
|
+
* and `Fn::GetStackOutput`.
|
|
36853
|
+
*
|
|
36854
|
+
* Callers that don't need cross-stack support should keep using the sync
|
|
36855
|
+
* helper. Code paths that wire `--from-state` env / secret substitution
|
|
36856
|
+
* (e.g. `cdkd local invoke --from-state`, `cdkd local run-task --from-state`)
|
|
36857
|
+
* route through this async version so a single env-var referencing a
|
|
36858
|
+
* cross-stack output is no longer warn-and-dropped.
|
|
36859
|
+
*/
|
|
36860
|
+
async function substituteAgainstStateAsync(value, contextOrResources) {
|
|
36861
|
+
const context = isContext(contextOrResources) ? contextOrResources : { resources: contextOrResources };
|
|
36862
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return {
|
|
36863
|
+
kind: "literal",
|
|
36864
|
+
value
|
|
36865
|
+
};
|
|
36866
|
+
if (value === null || typeof value !== "object") return {
|
|
36867
|
+
kind: "unresolved",
|
|
36868
|
+
reason: `unsupported value type: ${value === null ? "null" : typeof value}`
|
|
36869
|
+
};
|
|
36870
|
+
const obj = value;
|
|
36871
|
+
const keys = Object.keys(obj);
|
|
36872
|
+
if (keys.length !== 1) return {
|
|
36873
|
+
kind: "unresolved",
|
|
36874
|
+
reason: `expected an intrinsic with one key, got ${keys.length} keys`
|
|
36875
|
+
};
|
|
36876
|
+
const intrinsic = keys[0];
|
|
36877
|
+
const arg = obj[intrinsic];
|
|
36878
|
+
if (intrinsic === "Ref" || intrinsic === "Fn::GetAtt" || intrinsic === "Fn::Sub" || intrinsic === "Fn::Join") return substituteAgainstState(value, context);
|
|
36879
|
+
if (intrinsic === "Fn::ImportValue") return resolveImportValueAsync(arg, context);
|
|
36880
|
+
if (intrinsic === "Fn::GetStackOutput") return resolveGetStackOutputAsync(arg, context);
|
|
36881
|
+
return {
|
|
36882
|
+
kind: "unresolved",
|
|
36883
|
+
reason: `unsupported intrinsic '${intrinsic}' (supported: Ref, Fn::GetAtt, Fn::Sub, Fn::Join, Fn::ImportValue, Fn::GetStackOutput)`
|
|
36884
|
+
};
|
|
36885
|
+
}
|
|
36886
|
+
/**
|
|
36887
|
+
* `Fn::ImportValue: <exportName>` — the argument may itself be an
|
|
36888
|
+
* intrinsic that resolves to a string (e.g.
|
|
36889
|
+
* `{Fn::ImportValue: {Fn::Sub: 'MyStack-${AWS::Region}-Bucket'}}` — the
|
|
36890
|
+
* inner `Fn::Sub` is resolved against `pseudoParameters` first, then the
|
|
36891
|
+
* resulting string is looked up via the cross-stack resolver).
|
|
36892
|
+
*/
|
|
36893
|
+
async function resolveImportValueAsync(arg, context) {
|
|
36894
|
+
const inner = substituteAgainstState(arg, context);
|
|
36895
|
+
if (inner.kind !== "literal") return {
|
|
36896
|
+
kind: "unresolved",
|
|
36897
|
+
reason: `Fn::ImportValue argument: ${inner.reason}`
|
|
36898
|
+
};
|
|
36899
|
+
if (typeof inner.value !== "string" || inner.value.length === 0) return {
|
|
36900
|
+
kind: "unresolved",
|
|
36901
|
+
reason: `Fn::ImportValue argument must resolve to a non-empty string, got ${typeof inner.value}`
|
|
36902
|
+
};
|
|
36903
|
+
const exportName = inner.value;
|
|
36904
|
+
if (!context.crossStackResolver) return {
|
|
36905
|
+
kind: "unresolved",
|
|
36906
|
+
reason: `Fn::ImportValue '${exportName}': no cross-stack resolver supplied (pass --from-state and ensure the producer stack was deployed via cdkd deploy)`
|
|
36907
|
+
};
|
|
36908
|
+
let resolved;
|
|
36909
|
+
try {
|
|
36910
|
+
resolved = await context.crossStackResolver.resolveImport(exportName);
|
|
36911
|
+
} catch (err) {
|
|
36912
|
+
return {
|
|
36913
|
+
kind: "unresolved",
|
|
36914
|
+
reason: `Fn::ImportValue '${exportName}': lookup failed: ${err instanceof Error ? err.message : String(err)}`
|
|
36915
|
+
};
|
|
36916
|
+
}
|
|
36917
|
+
if (resolved === void 0) return {
|
|
36918
|
+
kind: "unresolved",
|
|
36919
|
+
reason: `Fn::ImportValue '${exportName}': export not found in any cdkd-managed stack in this region`
|
|
36920
|
+
};
|
|
36921
|
+
return {
|
|
36922
|
+
kind: "literal",
|
|
36923
|
+
value: resolved
|
|
36924
|
+
};
|
|
36925
|
+
}
|
|
36926
|
+
/**
|
|
36927
|
+
* `Fn::GetStackOutput: { StackName, OutputName, Region? }`. Same shape as
|
|
36928
|
+
* the deploy-engine resolver. `RoleArn` (cross-account) is intentionally
|
|
36929
|
+
* NOT supported in this path — the user-visible error message points at
|
|
36930
|
+
* the followup issue tracking it.
|
|
36931
|
+
*/
|
|
36932
|
+
async function resolveGetStackOutputAsync(arg, context) {
|
|
36933
|
+
if (!arg || typeof arg !== "object" || Array.isArray(arg)) return {
|
|
36934
|
+
kind: "unresolved",
|
|
36935
|
+
reason: `Fn::GetStackOutput argument must be an object with StackName / OutputName / Region, got ${arg === null ? "null" : Array.isArray(arg) ? "array" : typeof arg}`
|
|
36936
|
+
};
|
|
36937
|
+
const args = arg;
|
|
36938
|
+
const stackNameSub = substituteAgainstState(args["StackName"], context);
|
|
36939
|
+
if (stackNameSub.kind !== "literal") return {
|
|
36940
|
+
kind: "unresolved",
|
|
36941
|
+
reason: `Fn::GetStackOutput.StackName: ${stackNameSub.reason}`
|
|
36942
|
+
};
|
|
36943
|
+
if (typeof stackNameSub.value !== "string" || stackNameSub.value.length === 0) return {
|
|
36944
|
+
kind: "unresolved",
|
|
36945
|
+
reason: `Fn::GetStackOutput.StackName must resolve to a non-empty string, got ${typeof stackNameSub.value}`
|
|
36946
|
+
};
|
|
36947
|
+
const stackName = stackNameSub.value;
|
|
36948
|
+
const outputNameSub = substituteAgainstState(args["OutputName"], context);
|
|
36949
|
+
if (outputNameSub.kind !== "literal") return {
|
|
36950
|
+
kind: "unresolved",
|
|
36951
|
+
reason: `Fn::GetStackOutput.OutputName: ${outputNameSub.reason}`
|
|
36952
|
+
};
|
|
36953
|
+
if (typeof outputNameSub.value !== "string" || outputNameSub.value.length === 0) return {
|
|
36954
|
+
kind: "unresolved",
|
|
36955
|
+
reason: `Fn::GetStackOutput.OutputName must resolve to a non-empty string, got ${typeof outputNameSub.value}`
|
|
36956
|
+
};
|
|
36957
|
+
const outputName = outputNameSub.value;
|
|
36958
|
+
let region;
|
|
36959
|
+
if (args["Region"] !== void 0 && args["Region"] !== null) {
|
|
36960
|
+
const regionSub = substituteAgainstState(args["Region"], context);
|
|
36961
|
+
if (regionSub.kind !== "literal") return {
|
|
36962
|
+
kind: "unresolved",
|
|
36963
|
+
reason: `Fn::GetStackOutput.Region: ${regionSub.reason}`
|
|
36964
|
+
};
|
|
36965
|
+
if (typeof regionSub.value !== "string" || regionSub.value.length === 0) return {
|
|
36966
|
+
kind: "unresolved",
|
|
36967
|
+
reason: `Fn::GetStackOutput.Region must resolve to a non-empty string, got ${typeof regionSub.value}`
|
|
36968
|
+
};
|
|
36969
|
+
region = regionSub.value;
|
|
36970
|
+
} else region = context.consumerRegion ?? context.pseudoParameters?.region;
|
|
36971
|
+
if (!region) return {
|
|
36972
|
+
kind: "unresolved",
|
|
36973
|
+
reason: `Fn::GetStackOutput '${stackName}.${outputName}': no Region supplied and consumer region is unknown (set --region, AWS_REGION, or env.region on the CDK stack)`
|
|
36974
|
+
};
|
|
36975
|
+
if (args["RoleArn"] !== void 0 && args["RoleArn"] !== null) return {
|
|
36976
|
+
kind: "unresolved",
|
|
36977
|
+
reason: `Fn::GetStackOutput '${stackName}.${outputName}': RoleArn (cross-account) is not yet supported by --from-state — tracked under issue #449`
|
|
36978
|
+
};
|
|
36979
|
+
if (!context.crossStackResolver) return {
|
|
36980
|
+
kind: "unresolved",
|
|
36981
|
+
reason: `Fn::GetStackOutput '${stackName}.${outputName}': no cross-stack resolver supplied (pass --from-state and ensure the producer stack was deployed via cdkd deploy)`
|
|
36982
|
+
};
|
|
36983
|
+
let resolved;
|
|
36984
|
+
try {
|
|
36985
|
+
resolved = await context.crossStackResolver.resolveGetStackOutput(stackName, region, outputName);
|
|
36986
|
+
} catch (err) {
|
|
36987
|
+
return {
|
|
36988
|
+
kind: "unresolved",
|
|
36989
|
+
reason: `Fn::GetStackOutput '${stackName}.${outputName}' (${region}): lookup failed: ${err instanceof Error ? err.message : String(err)}`
|
|
36990
|
+
};
|
|
36991
|
+
}
|
|
36992
|
+
if (resolved === void 0) return {
|
|
36993
|
+
kind: "unresolved",
|
|
36994
|
+
reason: `Fn::GetStackOutput '${stackName}.${outputName}' (${region}): output not found in producer stack state`
|
|
36995
|
+
};
|
|
36996
|
+
return {
|
|
36997
|
+
kind: "literal",
|
|
36998
|
+
value: resolved
|
|
36999
|
+
};
|
|
37000
|
+
}
|
|
37001
|
+
/**
|
|
36736
37002
|
* Build a pre-substituted env map from the template entry by feeding each
|
|
36737
37003
|
* intrinsic value through `substituteAgainstState`. Literal entries pass
|
|
36738
37004
|
* through untouched (the env-resolver handles them).
|
|
@@ -36773,6 +37039,50 @@ function substituteEnvVarsFromState(templateEnv, contextOrResources) {
|
|
|
36773
37039
|
audit
|
|
36774
37040
|
};
|
|
36775
37041
|
}
|
|
37042
|
+
/**
|
|
37043
|
+
* Async sibling of {@link substituteEnvVarsFromState}. Routes every
|
|
37044
|
+
* intrinsic-valued entry through {@link substituteAgainstStateAsync} so
|
|
37045
|
+
* `Fn::ImportValue` / `Fn::GetStackOutput` resolve via the context's
|
|
37046
|
+
* `crossStackResolver` (when supplied). Mirrors the sync version in every
|
|
37047
|
+
* other respect: literal entries pass through unchanged, unresolved
|
|
37048
|
+
* entries are dropped with a per-key audit reason, and the env-resolver
|
|
37049
|
+
* sees the same "no template value" shape so the warn-and-drop path
|
|
37050
|
+
* fires consistently.
|
|
37051
|
+
*
|
|
37052
|
+
* Closes issue #454 — `cdkd local invoke --from-state` and
|
|
37053
|
+
* `cdkd local run-task --from-state` can now resolve cross-stack output
|
|
37054
|
+
* references in env vars / secrets instead of warn-and-dropping them.
|
|
37055
|
+
*/
|
|
37056
|
+
async function substituteEnvVarsFromStateAsync(templateEnv, contextOrResources) {
|
|
37057
|
+
const env = {};
|
|
37058
|
+
const audit = {
|
|
37059
|
+
resolvedKeys: [],
|
|
37060
|
+
unresolved: []
|
|
37061
|
+
};
|
|
37062
|
+
if (!templateEnv) return {
|
|
37063
|
+
env,
|
|
37064
|
+
audit
|
|
37065
|
+
};
|
|
37066
|
+
const context = isContext(contextOrResources) ? contextOrResources : { resources: contextOrResources };
|
|
37067
|
+
for (const [key, value] of Object.entries(templateEnv)) {
|
|
37068
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
37069
|
+
env[key] = value;
|
|
37070
|
+
continue;
|
|
37071
|
+
}
|
|
37072
|
+
const result = await substituteAgainstStateAsync(value, context);
|
|
37073
|
+
if (result.kind === "literal") {
|
|
37074
|
+
env[key] = result.value;
|
|
37075
|
+
audit.resolvedKeys.push(key);
|
|
37076
|
+
} else audit.unresolved.push({
|
|
37077
|
+
key,
|
|
37078
|
+
reason: result.reason
|
|
37079
|
+
});
|
|
37080
|
+
}
|
|
37081
|
+
return {
|
|
37082
|
+
env,
|
|
37083
|
+
audit
|
|
37084
|
+
};
|
|
37085
|
+
}
|
|
36776
37086
|
|
|
36777
37087
|
//#endregion
|
|
36778
37088
|
//#region src/local/ecs-task-resolver.ts
|
|
@@ -36816,6 +37126,7 @@ function detectEcsImageResolutionNeeds(stack) {
|
|
|
36816
37126
|
let needsPseudoParameters = false;
|
|
36817
37127
|
let needsStateResources = false;
|
|
36818
37128
|
let needsEnvOrSecretSubstitution = false;
|
|
37129
|
+
let needsCrossStackResolver = false;
|
|
36819
37130
|
for (const res of Object.values(resources)) {
|
|
36820
37131
|
if (res.Type !== "AWS::ECS::TaskDefinition") continue;
|
|
36821
37132
|
const props = res.Properties ?? {};
|
|
@@ -36828,6 +37139,10 @@ function detectEcsImageResolutionNeeds(stack) {
|
|
|
36828
37139
|
if (need.pseudo) needsPseudoParameters = true;
|
|
36829
37140
|
if (need.state) needsStateResources = true;
|
|
36830
37141
|
if (containerHasIntrinsicEnvOrSecret(co)) needsEnvOrSecretSubstitution = true;
|
|
37142
|
+
if (containerHasCrossStackEnvOrSecret(co)) {
|
|
37143
|
+
needsEnvOrSecretSubstitution = true;
|
|
37144
|
+
needsCrossStackResolver = true;
|
|
37145
|
+
}
|
|
36831
37146
|
}
|
|
36832
37147
|
const rawVolumes = props["Volumes"];
|
|
36833
37148
|
if (Array.isArray(rawVolumes)) {
|
|
@@ -36841,10 +37156,37 @@ function detectEcsImageResolutionNeeds(stack) {
|
|
|
36841
37156
|
return {
|
|
36842
37157
|
needsPseudoParameters,
|
|
36843
37158
|
needsStateResources,
|
|
36844
|
-
needsEnvOrSecretSubstitution
|
|
37159
|
+
needsEnvOrSecretSubstitution,
|
|
37160
|
+
needsCrossStackResolver
|
|
36845
37161
|
};
|
|
36846
37162
|
}
|
|
36847
37163
|
/**
|
|
37164
|
+
* Returns true when any `Environment[].Value` or `Secrets[].ValueFrom`
|
|
37165
|
+
* is a top-level `Fn::ImportValue` / `Fn::GetStackOutput` intrinsic.
|
|
37166
|
+
* Issue #454 — gates cross-stack resolver construction so literal +
|
|
37167
|
+
* same-stack-intrinsic env / secret maps don't pay the extra cost.
|
|
37168
|
+
*/
|
|
37169
|
+
function containerHasCrossStackEnvOrSecret(c) {
|
|
37170
|
+
const env = c["Environment"];
|
|
37171
|
+
if (Array.isArray(env)) for (const entry of env) {
|
|
37172
|
+
if (!entry || typeof entry !== "object") continue;
|
|
37173
|
+
const v = entry["Value"];
|
|
37174
|
+
if (isCrossStackIntrinsic(v)) return true;
|
|
37175
|
+
}
|
|
37176
|
+
const secrets = c["Secrets"];
|
|
37177
|
+
if (Array.isArray(secrets)) for (const entry of secrets) {
|
|
37178
|
+
if (!entry || typeof entry !== "object") continue;
|
|
37179
|
+
const v = entry["ValueFrom"];
|
|
37180
|
+
if (isCrossStackIntrinsic(v)) return true;
|
|
37181
|
+
}
|
|
37182
|
+
return false;
|
|
37183
|
+
}
|
|
37184
|
+
function isCrossStackIntrinsic(value) {
|
|
37185
|
+
if (!value || typeof value !== "object") return false;
|
|
37186
|
+
const obj = value;
|
|
37187
|
+
return "Fn::ImportValue" in obj || "Fn::GetStackOutput" in obj;
|
|
37188
|
+
}
|
|
37189
|
+
/**
|
|
36848
37190
|
* Detect whether a Volume entry has an intrinsic-valued `Host.SourcePath`.
|
|
36849
37191
|
* Used by `detectEcsImageResolutionNeeds` (Gap 6 of #286) to trigger
|
|
36850
37192
|
* state-load + pseudo-parameter resolution under `--from-state` when the
|
|
@@ -37589,6 +37931,87 @@ function checkVolumeHostPath(hostPath) {
|
|
|
37589
37931
|
return false;
|
|
37590
37932
|
}
|
|
37591
37933
|
}
|
|
37934
|
+
/**
|
|
37935
|
+
* Async post-pass that walks the resolved task's container Environment +
|
|
37936
|
+
* Secrets entries from the raw template (preserved in `task.resource`)
|
|
37937
|
+
* and re-attempts substitution via {@link substituteAgainstStateAsync}
|
|
37938
|
+
* against the supplied context. The sync `parseContainerDefinition`
|
|
37939
|
+
* pass already substituted everything the legacy resolver handles; this
|
|
37940
|
+
* pass picks up the additional shapes the async resolver supports —
|
|
37941
|
+
* specifically `Fn::ImportValue` / `Fn::GetStackOutput` (issue #454).
|
|
37942
|
+
*
|
|
37943
|
+
* Resolved entries are patched onto the container's `environment` /
|
|
37944
|
+
* `secrets` map AND the corresponding `warnings` entries are filtered
|
|
37945
|
+
* out so the CLI doesn't print a stale per-container warn for an entry
|
|
37946
|
+
* the post-pass successfully resolved. Entries that STILL can't resolve
|
|
37947
|
+
* (e.g. producer stack not deployed) keep their original warning so the
|
|
37948
|
+
* CLI's UX matches the sync path.
|
|
37949
|
+
*
|
|
37950
|
+
* Pure-functional on the task structure outside of in-place mutation of
|
|
37951
|
+
* the container's `environment` / `secrets` / `warnings` arrays. The
|
|
37952
|
+
* task is expected to be the same `ResolvedEcsTask` instance returned
|
|
37953
|
+
* by `resolveEcsTaskTarget` — the runner downstream reads from these
|
|
37954
|
+
* fields directly.
|
|
37955
|
+
*/
|
|
37956
|
+
async function applyCrossStackResolverToTask(task, context) {
|
|
37957
|
+
if (!context.crossStackResolver) return;
|
|
37958
|
+
const rawContainers = (task.resource.Properties ?? {})["ContainerDefinitions"];
|
|
37959
|
+
if (!Array.isArray(rawContainers)) return;
|
|
37960
|
+
for (let idx = 0; idx < task.containers.length; idx += 1) {
|
|
37961
|
+
const container = task.containers[idx];
|
|
37962
|
+
const raw = rawContainers[idx];
|
|
37963
|
+
if (!raw || typeof raw !== "object") continue;
|
|
37964
|
+
const c = raw;
|
|
37965
|
+
const containerName = pickString(c["Name"]) ?? container.name;
|
|
37966
|
+
const resolvedEnvKeys = /* @__PURE__ */ new Set();
|
|
37967
|
+
const resolvedSecretNames = /* @__PURE__ */ new Set();
|
|
37968
|
+
if (Array.isArray(c["Environment"])) for (const entry of c["Environment"]) {
|
|
37969
|
+
if (!entry || typeof entry !== "object") continue;
|
|
37970
|
+
const e = entry;
|
|
37971
|
+
const key = pickString(e["Name"]);
|
|
37972
|
+
const value = e["Value"];
|
|
37973
|
+
if (!key) continue;
|
|
37974
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") continue;
|
|
37975
|
+
if (key in container.environment) continue;
|
|
37976
|
+
if (!isCrossStackIntrinsic(value)) continue;
|
|
37977
|
+
const sub = await substituteAgainstStateAsync(value, context);
|
|
37978
|
+
if (sub.kind === "literal") {
|
|
37979
|
+
container.environment[key] = String(sub.value);
|
|
37980
|
+
resolvedEnvKeys.add(key);
|
|
37981
|
+
}
|
|
37982
|
+
}
|
|
37983
|
+
if (Array.isArray(c["Secrets"])) for (const entry of c["Secrets"]) {
|
|
37984
|
+
if (!entry || typeof entry !== "object") continue;
|
|
37985
|
+
const e = entry;
|
|
37986
|
+
const sName = pickString(e["Name"]);
|
|
37987
|
+
const valueFromRaw = e["ValueFrom"];
|
|
37988
|
+
if (!sName) continue;
|
|
37989
|
+
if (typeof valueFromRaw === "string" && valueFromRaw.length > 0) continue;
|
|
37990
|
+
if (container.secrets.some((s) => s.name === sName)) continue;
|
|
37991
|
+
if (!isCrossStackIntrinsic(valueFromRaw)) continue;
|
|
37992
|
+
const sub = await substituteAgainstStateAsync(valueFromRaw, context);
|
|
37993
|
+
if (sub.kind === "literal" && typeof sub.value === "string" && sub.value.length > 0) {
|
|
37994
|
+
container.secrets.push({
|
|
37995
|
+
name: sName,
|
|
37996
|
+
valueFrom: sub.value
|
|
37997
|
+
});
|
|
37998
|
+
resolvedSecretNames.add(sName);
|
|
37999
|
+
}
|
|
38000
|
+
}
|
|
38001
|
+
if (resolvedEnvKeys.size > 0 || resolvedSecretNames.size > 0) {
|
|
38002
|
+
container.warnings = container.warnings.filter((w) => {
|
|
38003
|
+
for (const k of resolvedEnvKeys) if (w.startsWith(`Environment '${k}' dropped:`)) return false;
|
|
38004
|
+
for (const k of resolvedSecretNames) if (w.startsWith(`Secret '${k}' dropped:`)) return false;
|
|
38005
|
+
return true;
|
|
38006
|
+
});
|
|
38007
|
+
task.warnings = task.warnings.filter((w) => {
|
|
38008
|
+
for (const k of resolvedEnvKeys) if (w.startsWith(`Container '${containerName}': Environment '${k}' dropped:`)) return false;
|
|
38009
|
+
for (const k of resolvedSecretNames) if (w.startsWith(`Container '${containerName}': Secret '${k}' dropped:`)) return false;
|
|
38010
|
+
return true;
|
|
38011
|
+
});
|
|
38012
|
+
}
|
|
38013
|
+
}
|
|
38014
|
+
}
|
|
37592
38015
|
|
|
37593
38016
|
//#endregion
|
|
37594
38017
|
//#region src/local/runtime-image.ts
|
|
@@ -44605,8 +45028,34 @@ async function localRunTaskCommand(target, options) {
|
|
|
44605
45028
|
...Object.keys(context).length > 0 && { context }
|
|
44606
45029
|
};
|
|
44607
45030
|
const { stacks } = await synthesizer.synthesize(synthOpts);
|
|
44608
|
-
const
|
|
45031
|
+
const imageContext = await buildEcsImageResolutionContext(target, stacks, options);
|
|
45032
|
+
const task = resolveEcsTaskTarget(target, stacks, imageContext);
|
|
44609
45033
|
logger.info(`Target: ${task.stack.stackName}/${task.taskDefinitionLogicalId} (family=${task.family}, containers=${task.containers.length})`);
|
|
45034
|
+
const taskNeeds = detectEcsImageResolutionNeeds(stacks.find((s) => s.stackName === task.stack.stackName) ?? task.stack);
|
|
45035
|
+
let taskCrossStackDispose;
|
|
45036
|
+
if (options.fromState && taskNeeds.needsCrossStackResolver) {
|
|
45037
|
+
const consumerRegion = options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? task.stack.region ?? "us-east-1";
|
|
45038
|
+
const built = await buildCrossStackResolver(consumerRegion, {
|
|
45039
|
+
...options.stateBucket !== void 0 && { stateBucket: options.stateBucket },
|
|
45040
|
+
statePrefix: options.statePrefix,
|
|
45041
|
+
...options.region !== void 0 && { region: options.region },
|
|
45042
|
+
...options.profile !== void 0 && { profile: options.profile }
|
|
45043
|
+
});
|
|
45044
|
+
if (built) {
|
|
45045
|
+
taskCrossStackDispose = built.dispose;
|
|
45046
|
+
try {
|
|
45047
|
+
await applyCrossStackResolverToTask(task, {
|
|
45048
|
+
resources: imageContext?.stateResources ?? {},
|
|
45049
|
+
...imageContext?.pseudoParameters && { pseudoParameters: imageContext.pseudoParameters },
|
|
45050
|
+
consumerRegion,
|
|
45051
|
+
crossStackResolver: built.resolver
|
|
45052
|
+
});
|
|
45053
|
+
} finally {
|
|
45054
|
+
taskCrossStackDispose();
|
|
45055
|
+
taskCrossStackDispose = void 0;
|
|
45056
|
+
}
|
|
45057
|
+
}
|
|
45058
|
+
} else if (!options.fromState && taskNeeds.needsCrossStackResolver) logger.warn("Container Environment / Secrets entries contain Fn::ImportValue / Fn::GetStackOutput intrinsics. Pass --from-state to substitute them against deployed cdkd state.");
|
|
44610
45059
|
sigintHandler = () => {
|
|
44611
45060
|
sigintCount += 1;
|
|
44612
45061
|
if (sigintCount >= 2) {
|
|
@@ -44894,6 +45343,7 @@ async function localInvokeCommand(target, options) {
|
|
|
44894
45343
|
let stateAudit;
|
|
44895
45344
|
let templateEnv = getTemplateEnv(lambda.resource);
|
|
44896
45345
|
let stateForRoleHint;
|
|
45346
|
+
let crossStackDispose;
|
|
44897
45347
|
if (options.fromState) {
|
|
44898
45348
|
const loaded = await loadStateForStack(lambda.stack.stackName, lambda.stack.region, {
|
|
44899
45349
|
...options.stackRegion !== void 0 && { stackRegion: options.stackRegion },
|
|
@@ -44904,16 +45354,38 @@ async function localInvokeCommand(target, options) {
|
|
|
44904
45354
|
});
|
|
44905
45355
|
if (loaded) {
|
|
44906
45356
|
stateForRoleHint = loaded.state;
|
|
44907
|
-
const subContext = {
|
|
45357
|
+
const subContext = {
|
|
45358
|
+
resources: loaded.state.resources,
|
|
45359
|
+
consumerRegion: loaded.region
|
|
45360
|
+
};
|
|
44908
45361
|
if (envHasIntrinsicValue(templateEnv)) {
|
|
44909
45362
|
const pseudo = await resolvePseudoParametersForInvoke(lambda.stack.region, options);
|
|
44910
45363
|
if (pseudo) subContext.pseudoParameters = pseudo;
|
|
44911
45364
|
}
|
|
44912
|
-
|
|
44913
|
-
|
|
44914
|
-
|
|
44915
|
-
|
|
44916
|
-
|
|
45365
|
+
if (envHasCrossStackIntrinsic(templateEnv)) {
|
|
45366
|
+
const built = await buildCrossStackResolver(loaded.region, {
|
|
45367
|
+
...options.stateBucket !== void 0 && { stateBucket: options.stateBucket },
|
|
45368
|
+
statePrefix: options.statePrefix,
|
|
45369
|
+
...options.region !== void 0 && { region: options.region },
|
|
45370
|
+
...options.profile !== void 0 && { profile: options.profile }
|
|
45371
|
+
});
|
|
45372
|
+
if (built) {
|
|
45373
|
+
subContext.crossStackResolver = built.resolver;
|
|
45374
|
+
crossStackDispose = built.dispose;
|
|
45375
|
+
}
|
|
45376
|
+
}
|
|
45377
|
+
try {
|
|
45378
|
+
const { env, audit } = await substituteEnvVarsFromStateAsync(templateEnv, subContext);
|
|
45379
|
+
templateEnv = env;
|
|
45380
|
+
stateAudit = audit;
|
|
45381
|
+
for (const key of audit.resolvedKeys) logger.debug(`--from-state: substituted env var ${key} from cdkd state`);
|
|
45382
|
+
for (const { key, reason } of audit.unresolved) logger.warn(`--from-state: could not substitute env var ${key} (${reason}). Override it via --env-vars or it will be dropped.`);
|
|
45383
|
+
} finally {
|
|
45384
|
+
if (crossStackDispose) {
|
|
45385
|
+
crossStackDispose();
|
|
45386
|
+
crossStackDispose = void 0;
|
|
45387
|
+
}
|
|
45388
|
+
}
|
|
44917
45389
|
}
|
|
44918
45390
|
}
|
|
44919
45391
|
const overrides = readEnvOverridesFile(options.envVars);
|
|
@@ -45186,6 +45658,29 @@ function envHasIntrinsicValue(templateEnv) {
|
|
|
45186
45658
|
return false;
|
|
45187
45659
|
}
|
|
45188
45660
|
/**
|
|
45661
|
+
* Returns true when any value in the function's template env map carries
|
|
45662
|
+
* a top-level `Fn::ImportValue` / `Fn::GetStackOutput` intrinsic. Used to
|
|
45663
|
+
* gate the cross-stack resolver construction inside the `--from-state`
|
|
45664
|
+
* flow: literal + same-stack-intrinsic env maps shouldn't pay for the
|
|
45665
|
+
* extra S3 client / index-load cost (issue #454).
|
|
45666
|
+
*
|
|
45667
|
+
* Detection is one level deep — same heuristic CDK 2.x uses for these
|
|
45668
|
+
* intrinsics in practice. Nested cross-stack intrinsics (e.g. an
|
|
45669
|
+
* `Fn::ImportValue` buried inside a `Fn::Join`) are not detected here;
|
|
45670
|
+
* those won't resolve in v1 anyway because the async resolver path
|
|
45671
|
+
* defers to the sync helper for `Fn::Join` / `Fn::Sub` bodies (see
|
|
45672
|
+
* `substituteAgainstStateAsync` docstring).
|
|
45673
|
+
*/
|
|
45674
|
+
function envHasCrossStackIntrinsic(templateEnv) {
|
|
45675
|
+
if (!templateEnv) return false;
|
|
45676
|
+
for (const v of Object.values(templateEnv)) {
|
|
45677
|
+
if (!v || typeof v !== "object") continue;
|
|
45678
|
+
const obj = v;
|
|
45679
|
+
if ("Fn::ImportValue" in obj || "Fn::GetStackOutput" in obj) return true;
|
|
45680
|
+
}
|
|
45681
|
+
return false;
|
|
45682
|
+
}
|
|
45683
|
+
/**
|
|
45189
45684
|
* Build the AWS pseudo-parameter bag for `--from-state` env-var
|
|
45190
45685
|
* substitution. Issues a single `sts:GetCallerIdentity` for the account
|
|
45191
45686
|
* id and derives `partition` / `urlSuffix` from the resolved region. Any
|
|
@@ -46717,7 +47212,7 @@ function reorderArgs(argv) {
|
|
|
46717
47212
|
*/
|
|
46718
47213
|
async function main() {
|
|
46719
47214
|
const program = new Command();
|
|
46720
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.
|
|
47215
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.122.0");
|
|
46721
47216
|
program.addCommand(createBootstrapCommand());
|
|
46722
47217
|
program.addCommand(createSynthCommand());
|
|
46723
47218
|
program.addCommand(createListCommand());
|