@empire-builder-kit/nx 1.0.0-rc.2 → 1.0.0-rc.21
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/executors.json +10 -0
- package/package.json +4 -4
- package/src/cli/output.js +1 -1
- package/src/developer-agent/model.js +4 -4
- package/src/developer-agent/model.js.map +1 -1
- package/src/developer-agent/operation-registry.js +67 -10
- package/src/developer-agent/operation-registry.js.map +1 -1
- package/src/developer-agent/planning.js +7 -9
- package/src/developer-agent/planning.js.map +1 -1
- package/src/developer-agent/workspace-inspection.js +12 -0
- package/src/developer-agent/workspace-inspection.js.map +1 -1
- package/src/executors/blueprint-trust/blueprint-trust.js +6 -0
- package/src/executors/blueprint-trust/blueprint-trust.js.map +1 -1
- package/src/executors/deployment-plan/deployment-plan.js +5 -0
- package/src/executors/deployment-plan/deployment-plan.js.map +1 -1
- package/src/executors/foundation-preflight/foundation-preflight.d.ts +2 -0
- package/src/executors/foundation-preflight/foundation-preflight.js +6 -2
- package/src/executors/foundation-preflight/foundation-preflight.js.map +1 -1
- package/src/executors/foundation-preflight/schema.json +10 -0
- package/src/executors/foundation-profile-preflight/foundation-profile-preflight.d.ts +2 -0
- package/src/executors/foundation-profile-preflight/foundation-profile-preflight.js +2 -1
- package/src/executors/foundation-profile-preflight/foundation-profile-preflight.js.map +1 -1
- package/src/executors/foundation-profile-preflight/schema.json +19 -3
- package/src/executors/github-bootstrap/github-bootstrap.js +1 -0
- package/src/executors/github-bootstrap/github-bootstrap.js.map +1 -1
- package/src/executors/infrastructure-plan/infrastructure-plan.d.ts +229 -5
- package/src/executors/infrastructure-plan/infrastructure-plan.js +2298 -175
- package/src/executors/infrastructure-plan/infrastructure-plan.js.map +1 -1
- package/src/executors/infrastructure-plan/schema.json +16 -0
- package/src/executors/local-deploy/fixtures/local-deploy-state-machine.mjs +53 -0
- package/src/executors/local-deploy/local-approval.d.ts +23 -0
- package/src/executors/local-deploy/local-approval.js +43 -0
- package/src/executors/local-deploy/local-approval.js.map +1 -0
- package/src/executors/local-deploy/local-deploy.d.ts +69 -0
- package/src/executors/local-deploy/local-deploy.js +911 -0
- package/src/executors/local-deploy/local-deploy.js.map +1 -0
- package/src/executors/local-deploy/schema.json +42 -0
- package/src/executors/router-waf-rehearsal/router-waf-rehearsal.d.ts +3 -0
- package/src/executors/router-waf-rehearsal/router-waf-rehearsal.js +94 -0
- package/src/executors/router-waf-rehearsal/router-waf-rehearsal.js.map +1 -0
- package/src/executors/router-waf-rehearsal/schema.json +9 -0
- package/src/executors/secret-metadata/schema.json +5 -0
- package/src/executors/secret-metadata/secret-metadata.d.ts +12 -1
- package/src/executors/secret-metadata/secret-metadata.js +68 -16
- package/src/executors/secret-metadata/secret-metadata.js.map +1 -1
- package/src/executors/sst-lifecycle/logging-preflight.d.ts +5 -0
- package/src/executors/sst-lifecycle/logging-preflight.js +23 -13
- package/src/executors/sst-lifecycle/logging-preflight.js.map +1 -1
- package/src/executors/sst-lifecycle/run-sst-command.d.ts +8 -1
- package/src/executors/sst-lifecycle/run-sst-command.js +144 -12
- package/src/executors/sst-lifecycle/run-sst-command.js.map +1 -1
- package/src/executors/sst-lifecycle/schema.json +13 -1
- package/src/executors/sst-lifecycle/sst-environment-source.d.ts +25 -0
- package/src/executors/sst-lifecycle/sst-environment-source.js +213 -0
- package/src/executors/sst-lifecycle/sst-environment-source.js.map +1 -0
- package/src/foundation/index.d.ts +2 -0
- package/src/foundation/index.js +2 -0
- package/src/foundation/index.js.map +1 -1
- package/src/foundation/logging-preflight.js +1 -1
- package/src/foundation/logging-preflight.js.map +1 -1
- package/src/foundation/logging.d.ts +3 -1
- package/src/foundation/logging.js +1 -1
- package/src/foundation/logging.js.map +1 -1
- package/src/foundation/partial-state.d.ts +10 -0
- package/src/foundation/partial-state.js +21 -0
- package/src/foundation/partial-state.js.map +1 -0
- package/src/foundation/router-waf.d.ts +98 -0
- package/src/foundation/router-waf.js +401 -0
- package/src/foundation/router-waf.js.map +1 -0
- package/src/generators/function/function.js +2 -1
- package/src/generators/function/function.js.map +1 -1
- package/src/generators/post-generation-install.d.ts +1 -0
- package/src/generators/post-generation-install.js +134 -67
- package/src/generators/post-generation-install.js.map +1 -1
- package/src/generators/preset/generator.js +23 -3
- package/src/generators/preset/generator.js.map +1 -1
- package/src/generators/preset/schema.d.ts +7 -0
- package/src/generators/preset/schema.json +47 -0
- package/src/generators/preset/tool-versions.d.ts +5 -5
- package/src/generators/preset/tool-versions.js +4 -4
- package/src/generators/preset/workspace-files-ci-acceptance.d.ts +1 -0
- package/src/generators/preset/workspace-files-ci-acceptance.js +791 -0
- package/src/generators/preset/workspace-files-ci-acceptance.js.map +1 -0
- package/src/generators/preset/workspace-files-developer-agent.d.ts +2 -0
- package/src/generators/preset/workspace-files-developer-agent.js +25 -11
- package/src/generators/preset/workspace-files-developer-agent.js.map +1 -1
- package/src/generators/preset/workspace-files-foundation-workload-roles.js +5 -2
- package/src/generators/preset/workspace-files-foundation-workload-roles.js.map +1 -1
- package/src/generators/preset/workspace-files-performance-load.js +2 -1
- package/src/generators/preset/workspace-files-performance-load.js.map +1 -1
- package/src/generators/preset/workspace-files-workload-role-baseline.js +254 -42
- package/src/generators/preset/workspace-files-workload-role-baseline.js.map +1 -1
- package/src/generators/preset/workspace-files.d.ts +8 -4
- package/src/generators/preset/workspace-files.js +1039 -68
- package/src/generators/preset/workspace-files.js.map +1 -1
- package/src/generators/repository-policy/repository-policy.js +65 -3
- package/src/generators/repository-policy/repository-policy.js.map +1 -1
- package/src/generators/repository-policy/schema.d.ts +6 -0
- package/src/generators/repository-policy/schema.json +46 -0
- package/src/generators/slice/built-in-plan-api-files.js +2 -1
- package/src/generators/slice/built-in-plan-api-files.js.map +1 -1
- package/src/generators/slice/built-in-plan-authorization-infrastructure-files.js +19 -1
- package/src/generators/slice/built-in-plan-authorization-infrastructure-files.js.map +1 -1
- package/src/generators/slice/built-in-plan-data-files.js +16 -5
- package/src/generators/slice/built-in-plan-data-files.js.map +1 -1
- package/src/generators/slice/built-in-plan-data-retention-job-files.js +10 -6
- package/src/generators/slice/built-in-plan-data-retention-job-files.js.map +1 -1
- package/src/generators/slice/built-in-plan-database-lifecycle-files.js +25 -10
- package/src/generators/slice/built-in-plan-database-lifecycle-files.js.map +1 -1
- package/src/generators/slice/built-in-plan-foundation-files.js +2 -3
- package/src/generators/slice/built-in-plan-foundation-files.js.map +1 -1
- package/src/generators/slice/built-in-plan-infrastructure-files.js +364 -29
- package/src/generators/slice/built-in-plan-infrastructure-files.js.map +1 -1
- package/src/generators/slice/built-in-plan-infrastructure-safety-files.js +67 -11
- package/src/generators/slice/built-in-plan-infrastructure-safety-files.js.map +1 -1
- package/src/generators/slice/built-in-plan-metadata-files.js +1 -0
- package/src/generators/slice/built-in-plan-metadata-files.js.map +1 -1
- package/src/generators/slice/built-in-plan-operations-files.js +442 -5
- package/src/generators/slice/built-in-plan-operations-files.js.map +1 -1
- package/src/generators/slice/built-in-plan-performance-files.d.ts +2 -1
- package/src/generators/slice/built-in-plan-performance-files.js +41 -47
- package/src/generators/slice/built-in-plan-performance-files.js.map +1 -1
- package/src/generators/slice/built-in-plan-project-files.js +14 -1
- package/src/generators/slice/built-in-plan-project-files.js.map +1 -1
- package/src/generators/slice/built-in-plan-release-files.js +235 -56
- package/src/generators/slice/built-in-plan-release-files.js.map +1 -1
- package/src/generators/slice/built-in-plan-testing-files.js +92 -5
- package/src/generators/slice/built-in-plan-testing-files.js.map +1 -1
- package/src/generators/slice/built-in-plan-web-files.js +13 -0
- package/src/generators/slice/built-in-plan-web-files.js.map +1 -1
- package/src/generators/slice/built-in-plan-workload-role-files.js +5 -2
- package/src/generators/slice/built-in-plan-workload-role-files.js.map +1 -1
- package/src/generators/slice/built-in-plan.d.ts +2 -1
- package/src/generators/slice/built-in-plan.js +8 -5
- package/src/generators/slice/built-in-plan.js.map +1 -1
- package/src/generators/slice/slice.js +11 -3
- package/src/generators/slice/slice.js.map +1 -1
- package/src/generators/upgrade/upgrade.js +35 -0
- package/src/generators/upgrade/upgrade.js.map +1 -1
- package/src/generators/workload/frozen-container.d.ts +3 -2
- package/src/generators/workload/frozen-container.js +6 -4
- package/src/generators/workload/frozen-container.js.map +1 -1
- package/src/generators/workload/workload.js +2 -2
- package/src/generators/workload/workload.js.map +1 -1
- package/src/git-policy/github-bootstrap.d.ts +3 -1
- package/src/git-policy/github-bootstrap.js +22 -5
- package/src/git-policy/github-bootstrap.js.map +1 -1
- package/src/git-policy/routing.d.ts +2 -0
- package/src/git-policy/routing.js +12 -1
- package/src/git-policy/routing.js.map +1 -1
- package/src/infrastructure-change/local-capability.d.ts +73 -0
- package/src/infrastructure-change/local-capability.js +114 -0
- package/src/infrastructure-change/local-capability.js.map +1 -0
- package/src/infrastructure-change/model.d.ts +25 -4
- package/src/infrastructure-change/model.js +1 -0
- package/src/infrastructure-change/model.js.map +1 -1
- package/src/infrastructure-change/normalize.d.ts +4 -0
- package/src/infrastructure-change/normalize.js +47 -7
- package/src/infrastructure-change/normalize.js.map +1 -1
- package/src/infrastructure-change/plan.d.ts +30 -3
- package/src/infrastructure-change/plan.js +323 -13
- package/src/infrastructure-change/plan.js.map +1 -1
- package/src/infrastructure-change/risk.d.ts +3 -1
- package/src/infrastructure-change/risk.js +96 -16
- package/src/infrastructure-change/risk.js.map +1 -1
- package/src/local-dev/environment-inspector.d.ts +1 -0
- package/src/local-dev/environment-inspector.js +21 -9
- package/src/local-dev/environment-inspector.js.map +1 -1
- package/src/local-dev/private-state.d.ts +2 -0
- package/src/local-dev/private-state.js +3 -1
- package/src/local-dev/private-state.js.map +1 -1
- package/src/performance/cost-controls.d.ts +5 -3
- package/src/performance/cost-controls.js +30 -24
- package/src/performance/cost-controls.js.map +1 -1
- package/src/performance/model.d.ts +1 -1
- package/src/performance/model.js +0 -2
- package/src/performance/model.js.map +1 -1
- package/src/performance/test-fixtures.js +0 -2
- package/src/performance/test-fixtures.js.map +1 -1
- package/src/release/source-authority.d.ts +1 -0
- package/src/release/source-authority.js +136 -9
- package/src/release/source-authority.js.map +1 -1
- package/src/repository-policy/deployment-plan.d.ts +4 -1
- package/src/repository-policy/deployment-plan.js +8 -0
- package/src/repository-policy/deployment-plan.js.map +1 -1
- package/src/repository-policy/foundation-profile.js +1 -2
- package/src/repository-policy/foundation-profile.js.map +1 -1
- package/src/repository-policy/policy.d.ts +25 -1
- package/src/repository-policy/policy.js +145 -4
- package/src/repository-policy/policy.js.map +1 -1
- package/src/schemas/performance-policy-v1.json +0 -2
- package/src/schemas/repository-policy-v1.json +41 -0
- package/src/secrets/operator.js +22 -4
- package/src/secrets/operator.js.map +1 -1
- package/src/secrets/verification.js +9 -1
- package/src/secrets/verification.js.map +1 -1
- package/src/sst-environment-source-format.d.ts +3 -0
- package/src/sst-environment-source-format.js +179 -0
- package/src/sst-environment-source-format.js.map +1 -0
- package/src/supply-chain/blueprint-trust-git.js +72 -21
- package/src/supply-chain/blueprint-trust-git.js.map +1 -1
- package/src/supply-chain/blueprint-verification-authority.js +55 -3
- package/src/supply-chain/blueprint-verification-authority.js.map +1 -1
- package/src/supply-chain/blueprint-verification-execution.js +17 -2
- package/src/supply-chain/blueprint-verification-execution.js.map +1 -1
- package/src/supply-chain/build-output.d.ts +7 -1
- package/src/supply-chain/build-output.js +160 -20
- package/src/supply-chain/build-output.js.map +1 -1
- package/src/supply-chain/generated-command.js +131 -43
- package/src/supply-chain/generated-command.js.map +1 -1
- package/src/supply-chain/generated-container-release-command.js +187 -38
- package/src/supply-chain/generated-container-release-command.js.map +1 -1
- package/src/supply-chain/generated-ecr-collector-command.js +12 -2
- package/src/supply-chain/generated-ecr-collector-command.js.map +1 -1
- package/src/supply-chain/generated-scanner-command.js +114 -54
- package/src/supply-chain/generated-scanner-command.js.map +1 -1
- package/src/supply-chain/inventory.d.ts +17 -0
- package/src/supply-chain/inventory.js +114 -3
- package/src/supply-chain/inventory.js.map +1 -1
- package/src/supply-chain/pnpm-toolchain-authority.d.ts +4 -3
- package/src/supply-chain/pnpm-toolchain-authority.js +20 -34
- package/src/supply-chain/pnpm-toolchain-authority.js.map +1 -1
- package/src/supply-chain/policy.js +4 -1
- package/src/supply-chain/policy.js.map +1 -1
- package/src/supply-chain/sbom.d.ts +1 -1
- package/src/supply-chain/sbom.js +30 -5
- package/src/supply-chain/sbom.js.map +1 -1
- package/src/supply-chain/scanner.js +10 -1
- package/src/supply-chain/scanner.js.map +1 -1
- package/src/workload-roles/account-baseline.d.ts +11 -0
- package/src/workload-roles/account-baseline.js +253 -16
- package/src/workload-roles/account-baseline.js.map +1 -1
- package/src/workload-roles/authority.d.ts +3 -1
- package/src/workload-roles/authority.js +48 -2
- package/src/workload-roles/authority.js.map +1 -1
- package/src/workload-roles/aws-s3-state.js +27 -0
- package/src/workload-roles/aws-s3-state.js.map +1 -1
- package/src/workload-roles/boundary.d.ts +2 -1
- package/src/workload-roles/boundary.js +9 -2
- package/src/workload-roles/boundary.js.map +1 -1
- package/src/workload-roles/handler.js +52 -24
- package/src/workload-roles/handler.js.map +1 -1
- package/src/workload-roles/injection.js +1 -1
- package/src/workload-roles/injection.js.map +1 -1
- package/src/workload-roles/provisioner.js +12 -4
- package/src/workload-roles/provisioner.js.map +1 -1
- package/src/workload-roles/registration.d.ts +1 -0
- package/src/workload-roles/registration.js +5 -0
- package/src/workload-roles/registration.js.map +1 -1
- package/src/workload-roles/stage-boundary.js +1 -1
- package/src/workload-roles/stage-boundary.js.map +1 -1
- package/src/workload-roles/standard-slice.js +1 -0
- package/src/workload-roles/standard-slice.js.map +1 -1
|
@@ -1,13 +1,32 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MAX_SST_DIFF_JSON_BYTES = void 0;
|
|
4
|
+
exports.collectInfrastructureBuildArtifacts = collectInfrastructureBuildArtifacts;
|
|
5
|
+
exports.infrastructureConfigurationMetadataDigest = infrastructureConfigurationMetadataDigest;
|
|
6
|
+
exports.matchesExpectedAssumedRoleArn = matchesExpectedAssumedRoleArn;
|
|
7
|
+
exports.localProductionConfirmation = localProductionConfirmation;
|
|
8
|
+
exports.revalidateLocalUnlockCallerAuthority = revalidateLocalUnlockCallerAuthority;
|
|
9
|
+
exports.parseRefreshPreviewJson = parseRefreshPreviewJson;
|
|
3
10
|
exports.extractPrivateImportIdentifierDigests = extractPrivateImportIdentifierDigests;
|
|
4
11
|
exports.approvalCandidateSourceCommit = approvalCandidateSourceCommit;
|
|
5
12
|
exports.createCandidateBoundSstChildEnvironment = createCandidateBoundSstChildEnvironment;
|
|
13
|
+
exports.normalizeDesiredSstDiffV4_17_1 = normalizeDesiredSstDiffV4_17_1;
|
|
6
14
|
exports.assessPulumiRefreshPreview = assessPulumiRefreshPreview;
|
|
15
|
+
exports.requiresIamMutationAnalysis = requiresIamMutationAnalysis;
|
|
7
16
|
exports.standardRemoveProbeForResource = standardRemoveProbeForResource;
|
|
8
17
|
exports.probeStandardRemoveResource = probeStandardRemoveResource;
|
|
9
18
|
exports.observeInfrastructureState = observeInfrastructureState;
|
|
19
|
+
exports.actionRisk = actionRisk;
|
|
10
20
|
exports.validateGovernanceEvidence = validateGovernanceEvidence;
|
|
21
|
+
exports.createLocalPlanApprovalRecord = createLocalPlanApprovalRecord;
|
|
22
|
+
exports.hasUnreviewableLocalPlanBlock = hasUnreviewableLocalPlanBlock;
|
|
23
|
+
exports.createLocalRefreshGovernanceReview = createLocalRefreshGovernanceReview;
|
|
24
|
+
exports.createLocalDeployGovernanceReview = createLocalDeployGovernanceReview;
|
|
25
|
+
exports.authorizeRetainedLocalInfrastructurePlan = authorizeRetainedLocalInfrastructurePlan;
|
|
26
|
+
exports.requiresGovernanceValidation = requiresGovernanceValidation;
|
|
27
|
+
exports.isLocalPlanReviewPending = isLocalPlanReviewPending;
|
|
28
|
+
exports.observeLocalMutationStateIdentity = observeLocalMutationStateIdentity;
|
|
29
|
+
exports.retainLocalRefreshStateBackup = retainLocalRefreshStateBackup;
|
|
11
30
|
exports.runInfrastructurePlanExecutor = runInfrastructurePlanExecutor;
|
|
12
31
|
exports.revalidateInfrastructureMutationAuthorization = revalidateInfrastructureMutationAuthorization;
|
|
13
32
|
exports.recordInfrastructureMutationConvergence = recordInfrastructureMutationConvergence;
|
|
@@ -21,8 +40,16 @@ const node_os_1 = require("node:os");
|
|
|
21
40
|
const devkit_1 = require("@nx/devkit");
|
|
22
41
|
const canonical_json_1 = require("@empire-builder-kit/runtime/canonical-json");
|
|
23
42
|
const index_js_1 = require("../../infrastructure-change/index.js");
|
|
43
|
+
const local_capability_js_1 = require("../../infrastructure-change/local-capability.js");
|
|
44
|
+
const private_state_js_1 = require("../../local-dev/private-state.js");
|
|
24
45
|
const index_js_2 = require("../../repository-policy/index.js");
|
|
46
|
+
const source_authority_js_1 = require("../../release/source-authority.js");
|
|
25
47
|
const run_sst_command_js_1 = require("../sst-lifecycle/run-sst-command.js");
|
|
48
|
+
function isCanonicalDeploymentStage(value) {
|
|
49
|
+
return (value === 'development' || value === 'staging' || value === 'production');
|
|
50
|
+
}
|
|
51
|
+
const refreshPreviewDiagnosticsByObservation = new WeakMap();
|
|
52
|
+
const refreshMutationPlanMaterialByObservation = new WeakMap();
|
|
26
53
|
function repositoryInfrastructureApprovalMode(context, stage) {
|
|
27
54
|
return stage === 'production'
|
|
28
55
|
? (0, index_js_2.readRepositoryOperatingPolicy)(context.root).productionApproval.mode
|
|
@@ -47,6 +74,99 @@ function projectEvidence(context, projects) {
|
|
|
47
74
|
const SHA256 = /^[a-f0-9]{64}$/;
|
|
48
75
|
const SOURCE_COMMIT = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/;
|
|
49
76
|
const MAX_JSON_BYTES = 5 * 1024 * 1024;
|
|
77
|
+
exports.MAX_SST_DIFF_JSON_BYTES = 64 * 1024 * 1024;
|
|
78
|
+
const MAX_BUILD_ARTIFACTS = 512;
|
|
79
|
+
const MAX_BUILD_ARTIFACT_BYTES = 256 * 1024 * 1024;
|
|
80
|
+
const MAX_BUILD_ARTIFACT_TOTAL_BYTES = 4 * 1024 * 1024 * 1024;
|
|
81
|
+
function sameBuildArtifactIdentity(left, right) {
|
|
82
|
+
return (left.dev === right.dev &&
|
|
83
|
+
left.ino === right.ino &&
|
|
84
|
+
left.mode === right.mode &&
|
|
85
|
+
left.nlink === right.nlink &&
|
|
86
|
+
left.size === right.size &&
|
|
87
|
+
left.mtimeMs === right.mtimeMs &&
|
|
88
|
+
left.ctimeMs === right.ctimeMs);
|
|
89
|
+
}
|
|
90
|
+
/** @internal */
|
|
91
|
+
function collectInfrastructureBuildArtifacts(projectRoot) {
|
|
92
|
+
const artifactsRoot = (0, node_path_1.join)(projectRoot, '.sst', 'artifacts');
|
|
93
|
+
const entries = [];
|
|
94
|
+
let totalBytes = 0;
|
|
95
|
+
if ((0, node_fs_1.existsSync)(artifactsRoot)) {
|
|
96
|
+
const rootIdentity = (0, node_fs_1.lstatSync)(artifactsRoot);
|
|
97
|
+
if (!rootIdentity.isDirectory() || rootIdentity.isSymbolicLink()) {
|
|
98
|
+
throw new Error('The SST artifact root must be one real directory.');
|
|
99
|
+
}
|
|
100
|
+
const children = (0, node_fs_1.readdirSync)(artifactsRoot, { withFileTypes: true }).sort((left, right) => (0, deterministic_order_js_1.compareUtf16)(left.name, right.name));
|
|
101
|
+
if (children.length > 4_096) {
|
|
102
|
+
throw new Error('The SST artifact root exceeds its directory bound.');
|
|
103
|
+
}
|
|
104
|
+
for (const child of children) {
|
|
105
|
+
if (!child.isDirectory())
|
|
106
|
+
continue;
|
|
107
|
+
const artifactPath = (0, node_path_1.join)(artifactsRoot, child.name, 'code.zip');
|
|
108
|
+
if (!(0, node_fs_1.existsSync)(artifactPath))
|
|
109
|
+
continue;
|
|
110
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,255}$/u.test(child.name)) {
|
|
111
|
+
throw new Error('An SST artifact directory name is invalid.');
|
|
112
|
+
}
|
|
113
|
+
if (entries.length === MAX_BUILD_ARTIFACTS) {
|
|
114
|
+
throw new Error('The SST build artifact count exceeds its bound.');
|
|
115
|
+
}
|
|
116
|
+
const lexical = (0, node_fs_1.lstatSync)(artifactPath);
|
|
117
|
+
if (!lexical.isFile() ||
|
|
118
|
+
lexical.isSymbolicLink() ||
|
|
119
|
+
lexical.nlink !== 1) {
|
|
120
|
+
throw new Error(`SST build artifact ${child.name} has an invalid identity.`);
|
|
121
|
+
}
|
|
122
|
+
const descriptor = (0, node_fs_1.openSync)(artifactPath, 'r');
|
|
123
|
+
try {
|
|
124
|
+
const before = (0, node_fs_1.fstatSync)(descriptor);
|
|
125
|
+
if (!before.isFile() ||
|
|
126
|
+
before.nlink !== 1 ||
|
|
127
|
+
lexical.dev !== before.dev ||
|
|
128
|
+
lexical.ino !== before.ino ||
|
|
129
|
+
lexical.mode !== before.mode ||
|
|
130
|
+
lexical.size !== before.size ||
|
|
131
|
+
before.size < 1 ||
|
|
132
|
+
before.size > MAX_BUILD_ARTIFACT_BYTES) {
|
|
133
|
+
throw new Error(`SST build artifact ${child.name} has an invalid identity.`);
|
|
134
|
+
}
|
|
135
|
+
const bytes = (0, node_fs_1.readFileSync)(descriptor);
|
|
136
|
+
const after = (0, node_fs_1.fstatSync)(descriptor);
|
|
137
|
+
if (bytes.byteLength !== before.size ||
|
|
138
|
+
!sameBuildArtifactIdentity(before, after)) {
|
|
139
|
+
throw new Error(`SST build artifact ${child.name} changed while reading.`);
|
|
140
|
+
}
|
|
141
|
+
totalBytes += bytes.byteLength;
|
|
142
|
+
if (totalBytes > MAX_BUILD_ARTIFACT_TOTAL_BYTES) {
|
|
143
|
+
throw new Error('The SST build artifact bytes exceed their aggregate bound.');
|
|
144
|
+
}
|
|
145
|
+
entries.push({
|
|
146
|
+
digest: (0, node_crypto_1.createHash)('sha256').update(bytes).digest('hex'),
|
|
147
|
+
path: `.sst/artifacts/${child.name}/code.zip`,
|
|
148
|
+
size: bytes.byteLength,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
(0, node_fs_1.closeSync)(descriptor);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const unsigned = {
|
|
157
|
+
entries,
|
|
158
|
+
kind: 'ebk-infrastructure-build-artifacts',
|
|
159
|
+
schemaVersion: 1,
|
|
160
|
+
};
|
|
161
|
+
return { ...unsigned, digest: digest(canonicalJson(unsigned)) };
|
|
162
|
+
}
|
|
163
|
+
/** @internal */
|
|
164
|
+
function infrastructureConfigurationMetadataDigest(input) {
|
|
165
|
+
if (!SHA256.test(input.buildArtifactDigest)) {
|
|
166
|
+
throw new Error('The infrastructure build artifact digest is invalid.');
|
|
167
|
+
}
|
|
168
|
+
return digest(canonicalJson(input));
|
|
169
|
+
}
|
|
50
170
|
const STANDARD_REMOVE_CLOUD_CONTROL_TYPES = {
|
|
51
171
|
'aws:acm/certificate:Certificate': 'AWS::CertificateManager::Certificate',
|
|
52
172
|
'aws:apigatewayv2/api:Api': 'AWS::ApiGatewayV2::Api',
|
|
@@ -106,6 +226,9 @@ const STANDARD_REMOVE_CLOUD_CONTROL_TYPES = {
|
|
|
106
226
|
'aws:sfn/stateMachine:StateMachine': 'AWS::StepFunctions::StateMachine',
|
|
107
227
|
'aws:sns/topic:Topic': 'AWS::SNS::Topic',
|
|
108
228
|
'aws:sqs/queue:Queue': 'AWS::SQS::Queue',
|
|
229
|
+
'aws:wafv2/ipSet:IpSet': 'AWS::WAFv2::IPSet',
|
|
230
|
+
'aws:wafv2/webAcl:WebAcl': 'AWS::WAFv2::WebACL',
|
|
231
|
+
'aws:wafv2/webAclLoggingConfiguration:WebAclLoggingConfiguration': 'AWS::WAFv2::LoggingConfiguration',
|
|
109
232
|
};
|
|
110
233
|
function digest(value) {
|
|
111
234
|
return (0, node_crypto_1.createHash)('sha256').update(value, 'utf8').digest('hex');
|
|
@@ -117,6 +240,66 @@ function requiredEnvironment(environment, name, pattern) {
|
|
|
117
240
|
}
|
|
118
241
|
return value;
|
|
119
242
|
}
|
|
243
|
+
function expectedDeploymentAccount(environment, invocation) {
|
|
244
|
+
const retained = requiredEnvironment(invocation.environment, 'AWS_EXPECTED_ACCOUNT_ID', /^\d{12}$/);
|
|
245
|
+
const ambient = environment.AWS_EXPECTED_ACCOUNT_ID?.trim();
|
|
246
|
+
if (ambient !== undefined && ambient !== '' && ambient !== retained) {
|
|
247
|
+
throw new Error('Ambient AWS_EXPECTED_ACCOUNT_ID does not match committed deployment topology.');
|
|
248
|
+
}
|
|
249
|
+
return retained;
|
|
250
|
+
}
|
|
251
|
+
function matchesExpectedAssumedRoleArn(account, callerArn, expectedRoleArn) {
|
|
252
|
+
const expected = /^arn:([^:]+):iam::(\d{12}):role\/(.+)$/.exec(expectedRoleArn);
|
|
253
|
+
const observed = /^arn:([^:]+):sts::(\d{12}):assumed-role\/(.+)\/([^/]+)$/.exec(callerArn);
|
|
254
|
+
const expectedRoleName = expected?.[3]?.split('/').at(-1);
|
|
255
|
+
return Boolean(expected &&
|
|
256
|
+
observed &&
|
|
257
|
+
expectedRoleName &&
|
|
258
|
+
expected[1] === observed[1] &&
|
|
259
|
+
expected[2] === account &&
|
|
260
|
+
observed[2] === account &&
|
|
261
|
+
expectedRoleName === observed[3]);
|
|
262
|
+
}
|
|
263
|
+
function localProductionConfirmation(input) {
|
|
264
|
+
if (!['deploy', 'refresh', 'remove'].includes(input.action) ||
|
|
265
|
+
!/^\d{12}$/.test(input.accountId) ||
|
|
266
|
+
!/^[a-z]{2}(?:-gov)?-[a-z]+(?:-[a-z]+)*-\d$/.test(input.region) ||
|
|
267
|
+
!SOURCE_COMMIT.test(input.sourceCommit)) {
|
|
268
|
+
throw new Error('Local production confirmation requires one exact action, account, Region, and source commit.');
|
|
269
|
+
}
|
|
270
|
+
return [
|
|
271
|
+
input.action,
|
|
272
|
+
'production',
|
|
273
|
+
input.accountId,
|
|
274
|
+
input.region,
|
|
275
|
+
input.sourceCommit,
|
|
276
|
+
].join(':');
|
|
277
|
+
}
|
|
278
|
+
function revalidateLocalUnlockCallerAuthority(input) {
|
|
279
|
+
const environment = input.environment ?? process.env;
|
|
280
|
+
const expectedAccount = expectedDeploymentAccount(environment, input.invocation);
|
|
281
|
+
const expectedRoleArn = input.invocation.environment.EBK_EXPECTED_DEPLOYMENT_ROLE_ARN?.trim();
|
|
282
|
+
if (!expectedRoleArn) {
|
|
283
|
+
throw new Error('Local unlock requires one exact expected temporary assumed role.');
|
|
284
|
+
}
|
|
285
|
+
const caller = record(runJsonCommand(input.spawn ??
|
|
286
|
+
node_child_process_1.spawnSync, 'aws', [
|
|
287
|
+
'sts',
|
|
288
|
+
'get-caller-identity',
|
|
289
|
+
'--output',
|
|
290
|
+
'json',
|
|
291
|
+
...(input.profile ? ['--profile', input.profile] : []),
|
|
292
|
+
], {
|
|
293
|
+
cwd: input.invocation.cwd,
|
|
294
|
+
env: (0, run_sst_command_js_1.createSstChildEnvironment)(environment, input.invocation.environment),
|
|
295
|
+
label: 'Local unlock AWS caller identity',
|
|
296
|
+
}), 'Local unlock AWS caller identity');
|
|
297
|
+
const callerArn = typeof caller['Arn'] === 'string' ? caller['Arn'] : '';
|
|
298
|
+
if (caller['Account'] !== expectedAccount ||
|
|
299
|
+
!matchesExpectedAssumedRoleArn(expectedAccount, callerArn, expectedRoleArn)) {
|
|
300
|
+
throw new Error('Local unlock AWS caller does not match the committed account and exact temporary assumed role.');
|
|
301
|
+
}
|
|
302
|
+
}
|
|
120
303
|
function parseJson(value, label) {
|
|
121
304
|
if (Buffer.byteLength(value, 'utf8') > MAX_JSON_BYTES) {
|
|
122
305
|
throw new Error(`${label} exceeds the 5 MiB metadata limit.`);
|
|
@@ -128,6 +311,17 @@ function parseJson(value, label) {
|
|
|
128
311
|
throw new Error(`${label} is not valid JSON.`);
|
|
129
312
|
}
|
|
130
313
|
}
|
|
314
|
+
function parseRefreshPreviewJson(value) {
|
|
315
|
+
if (Buffer.byteLength(value, 'utf8') > exports.MAX_SST_DIFF_JSON_BYTES) {
|
|
316
|
+
throw new Error('SST refresh preview exceeds the 64 MiB metadata limit.');
|
|
317
|
+
}
|
|
318
|
+
try {
|
|
319
|
+
return JSON.parse(value);
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
throw new Error('SST refresh preview is not valid JSON.');
|
|
323
|
+
}
|
|
324
|
+
}
|
|
131
325
|
function record(value, label) {
|
|
132
326
|
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
133
327
|
throw new Error(`${label} must be one JSON object.`);
|
|
@@ -184,6 +378,56 @@ function stringArray(value, label) {
|
|
|
184
378
|
}
|
|
185
379
|
return [...new Set(value)].sort();
|
|
186
380
|
}
|
|
381
|
+
function adoptedImports(value) {
|
|
382
|
+
if (!Array.isArray(value) || value.length > 64) {
|
|
383
|
+
throw new Error('policy.adoptedImports must be one bounded array.');
|
|
384
|
+
}
|
|
385
|
+
const imports = value.map((entry, index) => {
|
|
386
|
+
const candidate = record(entry, `policy.adoptedImports[${String(index)}]`);
|
|
387
|
+
requireExactKeys(candidate, [
|
|
388
|
+
'authority',
|
|
389
|
+
'logicalId',
|
|
390
|
+
'privateIdentifierTemplate',
|
|
391
|
+
'recoveryRunbookRef',
|
|
392
|
+
'removalBehavior',
|
|
393
|
+
'resourceType',
|
|
394
|
+
], `policy.adoptedImports[${String(index)}]`);
|
|
395
|
+
const logicalId = candidate['logicalId'];
|
|
396
|
+
const privateIdentifierTemplate = candidate['privateIdentifierTemplate'];
|
|
397
|
+
const recoveryRunbookRef = candidate['recoveryRunbookRef'];
|
|
398
|
+
const resourceType = candidate['resourceType'];
|
|
399
|
+
if (candidate['authority'] !== 'workload-role-account-baseline' ||
|
|
400
|
+
candidate['removalBehavior'] !== 'retain' ||
|
|
401
|
+
typeof logicalId !== 'string' ||
|
|
402
|
+
!/^[A-Za-z][A-Za-z0-9._-]{0,255}$/.test(logicalId) ||
|
|
403
|
+
typeof resourceType !== 'string' ||
|
|
404
|
+
!/^[A-Za-z0-9][A-Za-z0-9:._/-]{0,255}$/.test(resourceType) ||
|
|
405
|
+
typeof privateIdentifierTemplate !== 'string' ||
|
|
406
|
+
privateIdentifierTemplate.length > 512 ||
|
|
407
|
+
privateIdentifierTemplate.split('{stage}').length !== 2 ||
|
|
408
|
+
/[{}]/.test(privateIdentifierTemplate.replace('{stage}', '')) ||
|
|
409
|
+
typeof recoveryRunbookRef !== 'string' ||
|
|
410
|
+
recoveryRunbookRef.length > 256 ||
|
|
411
|
+
recoveryRunbookRef.startsWith('/') ||
|
|
412
|
+
recoveryRunbookRef.split('/').some((part) => !part || part === '..')) {
|
|
413
|
+
throw new Error(`policy.adoptedImports[${String(index)}] must bind one exact retained workload-role import and recovery runbook.`);
|
|
414
|
+
}
|
|
415
|
+
return {
|
|
416
|
+
authority: 'workload-role-account-baseline',
|
|
417
|
+
logicalId,
|
|
418
|
+
privateIdentifierTemplate,
|
|
419
|
+
recoveryRunbookRef,
|
|
420
|
+
removalBehavior: 'retain',
|
|
421
|
+
resourceType,
|
|
422
|
+
};
|
|
423
|
+
});
|
|
424
|
+
const keys = imports.map(({ logicalId, resourceType }) => `${logicalId}\u0000${resourceType}`);
|
|
425
|
+
if (new Set(keys).size !== keys.length) {
|
|
426
|
+
throw new Error('policy.adoptedImports contains a duplicate resource.');
|
|
427
|
+
}
|
|
428
|
+
return imports.sort((left, right) => (0, deterministic_order_js_1.compareUtf16)(left.logicalId, right.logicalId) ||
|
|
429
|
+
(0, deterministic_order_js_1.compareUtf16)(left.resourceType, right.resourceType));
|
|
430
|
+
}
|
|
187
431
|
function readJsonFile(path, label) {
|
|
188
432
|
return parseJson((0, node_fs_1.readFileSync)(path, 'utf8'), label);
|
|
189
433
|
}
|
|
@@ -224,7 +468,9 @@ function readDescriptor(path, project) {
|
|
|
224
468
|
}
|
|
225
469
|
return {
|
|
226
470
|
policy: {
|
|
471
|
+
adoptedImports: adoptedImports(policy.adoptedImports),
|
|
227
472
|
additiveLowRiskTypes: stringArray(policy.additiveLowRiskTypes, 'policy.additiveLowRiskTypes'),
|
|
473
|
+
additiveReviewedTypes: stringArray(policy.additiveReviewedTypes, 'policy.additiveReviewedTypes'),
|
|
228
474
|
costOrQuotaTypes: stringArray(policy.costOrQuotaTypes, 'policy.costOrQuotaTypes'),
|
|
229
475
|
statefulTypes: stringArray(policy.statefulTypes, 'policy.statefulTypes'),
|
|
230
476
|
},
|
|
@@ -242,7 +488,7 @@ function readOptionalArray(environment, name) {
|
|
|
242
488
|
}
|
|
243
489
|
return value;
|
|
244
490
|
}
|
|
245
|
-
function planAuthorityPolicyDigest(environment) {
|
|
491
|
+
function planAuthorityPolicyDigest(environment, invocation) {
|
|
246
492
|
const parseArray = (name) => {
|
|
247
493
|
const raw = requiredEnvironment(environment, name);
|
|
248
494
|
if (Buffer.byteLength(raw, 'utf8') > 128 * 1024) {
|
|
@@ -253,6 +499,38 @@ function planAuthorityPolicyDigest(environment) {
|
|
|
253
499
|
return value;
|
|
254
500
|
throw new Error(`${name} must contain one non-empty exact JSON array.`);
|
|
255
501
|
};
|
|
502
|
+
if (environment.EBK_LOCAL_DEPLOYMENT_MODE !== undefined) {
|
|
503
|
+
if (environment.EBK_LOCAL_DEPLOYMENT_MODE !== 'federated' ||
|
|
504
|
+
environment.GITHUB_ACTIONS === 'true') {
|
|
505
|
+
throw new Error('Local infrastructure plan authority requires an admitted federated profile outside GitHub Actions.');
|
|
506
|
+
}
|
|
507
|
+
for (const name of [
|
|
508
|
+
'AWS_PLAN_GENERATED_OBJECT_ARNS_JSON',
|
|
509
|
+
'AWS_PLAN_GENERATED_PARAMETER_ARNS_JSON',
|
|
510
|
+
'AWS_PLAN_GENERATED_SECRET_ARNS_JSON',
|
|
511
|
+
'AWS_PLAN_PROVIDER_REFRESH_POLICY_JSON',
|
|
512
|
+
'AWS_PLAN_STATE_KMS_KEY_ARN',
|
|
513
|
+
]) {
|
|
514
|
+
if (environment[name] !== undefined) {
|
|
515
|
+
throw new Error(`Local infrastructure plan authority rejects protected-only input ${name}.`);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
const region = requiredEnvironment(environment, 'AWS_REGION', /^[a-z]{2}(?:-gov)?-[a-z]+-\d$/);
|
|
519
|
+
const roleArn = requiredEnvironment(environment, 'AWS_PLAN_ROLE_ARN', /^arn:aws:iam::\d{12}:role\/[A-Za-z0-9+=,.@_/-]{1,512}$/);
|
|
520
|
+
if (invocation.environment.AWS_REGION !== region ||
|
|
521
|
+
invocation.environment.EBK_EXPECTED_DEPLOYMENT_ROLE_ARN !== roleArn) {
|
|
522
|
+
throw new Error('Local plan authority differs from the exact lifecycle Region or assumed role.');
|
|
523
|
+
}
|
|
524
|
+
return digest(canonicalJson({
|
|
525
|
+
account: requiredEnvironment(environment, 'AWS_EXPECTED_ACCOUNT_ID', /^\d{12}$/),
|
|
526
|
+
applications: parseArray('AWS_PLAN_SST_APPS_JSON'),
|
|
527
|
+
mode: 'local-federated',
|
|
528
|
+
region,
|
|
529
|
+
roleArn,
|
|
530
|
+
sourceTree: requiredEnvironment(environment, 'EBK_LOCAL_SOURCE_TREE', /^[a-f0-9]{40}$/),
|
|
531
|
+
stateBucketArn: requiredEnvironment(environment, 'AWS_PLAN_STATE_BUCKET_ARN', /^arn:aws:s3:::[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/),
|
|
532
|
+
}));
|
|
533
|
+
}
|
|
256
534
|
const providerPolicy = parseArray('AWS_PLAN_PROVIDER_REFRESH_POLICY_JSON');
|
|
257
535
|
if (providerPolicy.some((statement) => typeof statement !== 'object' ||
|
|
258
536
|
statement === null ||
|
|
@@ -297,24 +575,70 @@ function releaseCandidateBinding(input) {
|
|
|
297
575
|
}
|
|
298
576
|
return { digest: digest(raw), path };
|
|
299
577
|
}
|
|
578
|
+
function localSourceCandidateBinding(input) {
|
|
579
|
+
const policy = (0, index_js_2.readRepositoryOperatingPolicy)(input.context.root);
|
|
580
|
+
const stage = requiredEnvironment(input.environment, 'EBK_STAGE');
|
|
581
|
+
if (input.environment.EBK_LOCAL_DEPLOYMENT_MODE !== 'federated' ||
|
|
582
|
+
input.environment.GITHUB_ACTIONS === 'true' ||
|
|
583
|
+
!['development', 'staging', 'production'].includes(stage) ||
|
|
584
|
+
(0, index_js_2.requiresGithubDeployment)(policy.deploymentProfile, stage)) {
|
|
585
|
+
throw new Error('Local source candidates require a stage admitted by the committed deployment profile.');
|
|
586
|
+
}
|
|
587
|
+
const expectedTree = requiredEnvironment(input.environment, 'EBK_LOCAL_SOURCE_TREE', /^[a-f0-9]{40}$/);
|
|
588
|
+
const status = input.spawn('git', ['status', '--porcelain=v1', '--untracked-files=all'], {
|
|
589
|
+
cwd: input.context.root,
|
|
590
|
+
encoding: 'utf8',
|
|
591
|
+
env: input.environment,
|
|
592
|
+
maxBuffer: MAX_JSON_BYTES,
|
|
593
|
+
shell: false,
|
|
594
|
+
});
|
|
595
|
+
const tree = input.spawn('git', ['rev-parse', 'HEAD^{tree}'], {
|
|
596
|
+
cwd: input.context.root,
|
|
597
|
+
encoding: 'utf8',
|
|
598
|
+
env: input.environment,
|
|
599
|
+
maxBuffer: MAX_JSON_BYTES,
|
|
600
|
+
shell: false,
|
|
601
|
+
});
|
|
602
|
+
if (status.status !== 0 ||
|
|
603
|
+
status.stdout !== '' ||
|
|
604
|
+
status.stderr !== '' ||
|
|
605
|
+
tree.status !== 0 ||
|
|
606
|
+
tree.stdout.trim() !== expectedTree) {
|
|
607
|
+
throw new Error('Local source candidate differs from the exact clean Git tree.');
|
|
608
|
+
}
|
|
609
|
+
const materialSourceTree = input.materialSourceTree ?? expectedTree;
|
|
610
|
+
if (!SOURCE_COMMIT.test(materialSourceTree)) {
|
|
611
|
+
throw new Error('Local source candidate material tree must be one exact Git object ID.');
|
|
612
|
+
}
|
|
613
|
+
return {
|
|
614
|
+
digest: digest(canonicalJson({
|
|
615
|
+
kind: 'ebk-local-source-candidate',
|
|
616
|
+
project: input.project,
|
|
617
|
+
schemaVersion: 1,
|
|
618
|
+
sourceCommit: input.sourceCommit,
|
|
619
|
+
sourceTree: materialSourceTree,
|
|
620
|
+
})),
|
|
621
|
+
};
|
|
622
|
+
}
|
|
300
623
|
/** @internal */
|
|
301
624
|
function approvalCandidateSourceCommit(context, options, environment) {
|
|
302
625
|
const project = context.projectName;
|
|
303
|
-
const
|
|
304
|
-
if (
|
|
626
|
+
const workflowSelectedSource = environment['EBK_RELEASE_SOURCE_COMMIT']?.trim();
|
|
627
|
+
if (workflowSelectedSource !== undefined &&
|
|
628
|
+
!SOURCE_COMMIT.test(workflowSelectedSource)) {
|
|
305
629
|
throw new Error('EBK_RELEASE_SOURCE_COMMIT must be one exact Git object ID when set.');
|
|
306
630
|
}
|
|
307
631
|
if (!project)
|
|
308
|
-
return
|
|
632
|
+
return workflowSelectedSource;
|
|
309
633
|
const approvalPath = relativeFile(context.root, options.approvalPath ?? 'operations/infrastructure-approval.json');
|
|
310
634
|
if (!(0, node_fs_1.existsSync)(approvalPath))
|
|
311
|
-
return
|
|
635
|
+
return workflowSelectedSource;
|
|
312
636
|
let batch;
|
|
313
637
|
try {
|
|
314
638
|
batch = record(readJsonFile(approvalPath, 'Infrastructure approval candidate binding'), 'Infrastructure approval candidate binding');
|
|
315
639
|
}
|
|
316
640
|
catch {
|
|
317
|
-
return
|
|
641
|
+
return workflowSelectedSource;
|
|
318
642
|
}
|
|
319
643
|
const action = options.action ?? 'deploy';
|
|
320
644
|
const stage = options.stage?.trim();
|
|
@@ -323,7 +647,7 @@ function approvalCandidateSourceCommit(context, options, environment) {
|
|
|
323
647
|
batch['action'] !== action ||
|
|
324
648
|
stage === undefined ||
|
|
325
649
|
batch['stage'] !== stage) {
|
|
326
|
-
return
|
|
650
|
+
return workflowSelectedSource;
|
|
327
651
|
}
|
|
328
652
|
let approvedSource;
|
|
329
653
|
try {
|
|
@@ -333,16 +657,40 @@ function approvalCandidateSourceCommit(context, options, environment) {
|
|
|
333
657
|
approvedSource = planned['sourceCommit'];
|
|
334
658
|
}
|
|
335
659
|
catch {
|
|
336
|
-
return
|
|
660
|
+
return workflowSelectedSource;
|
|
337
661
|
}
|
|
338
662
|
if (typeof approvedSource !== 'string' ||
|
|
339
663
|
!SOURCE_COMMIT.test(approvedSource)) {
|
|
340
|
-
return
|
|
664
|
+
return workflowSelectedSource;
|
|
341
665
|
}
|
|
342
|
-
|
|
666
|
+
const localAuthority = environment['EBK_LOCAL_DEPLOYMENT_MODE'] === 'federated';
|
|
667
|
+
const localSelectedSource = localAuthority
|
|
668
|
+
? environment['EBK_SOURCE_COMMIT']?.trim()
|
|
669
|
+
: undefined;
|
|
670
|
+
if (localSelectedSource !== undefined &&
|
|
671
|
+
!SOURCE_COMMIT.test(localSelectedSource)) {
|
|
672
|
+
throw new Error('EBK_SOURCE_COMMIT must be one exact Git object ID for local approval resume.');
|
|
673
|
+
}
|
|
674
|
+
if (localAuthority && localSelectedSource === undefined) {
|
|
675
|
+
throw new Error('Local approval-resume infrastructure planning requires the exact clean local source.');
|
|
676
|
+
}
|
|
677
|
+
if (!localAuthority && workflowSelectedSource === undefined) {
|
|
343
678
|
throw new Error('Approval-resume infrastructure planning requires a workflow-selected release source.');
|
|
344
679
|
}
|
|
345
|
-
if (
|
|
680
|
+
if (localAuthority) {
|
|
681
|
+
try {
|
|
682
|
+
(0, source_authority_js_1.validateReviewedReleaseCheckout)({
|
|
683
|
+
deploymentSourceCommit: localSelectedSource,
|
|
684
|
+
releaseSourceCommit: approvedSource,
|
|
685
|
+
repositoryRoot: context.root,
|
|
686
|
+
reviewedSourceCommit: localSelectedSource,
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
catch {
|
|
690
|
+
throw new Error('Infrastructure approval candidate is not the exact clean local approval-only descendant.');
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
else if (workflowSelectedSource !== approvedSource) {
|
|
346
694
|
throw new Error('Infrastructure approval candidate differs from the workflow-selected release source.');
|
|
347
695
|
}
|
|
348
696
|
return approvedSource;
|
|
@@ -352,10 +700,23 @@ function createCandidateBoundSstChildEnvironment(environment, invocationEnvironm
|
|
|
352
700
|
if (!SOURCE_COMMIT.test(candidateSourceCommit)) {
|
|
353
701
|
throw new Error('Infrastructure lifecycle candidate must be one exact Git object ID.');
|
|
354
702
|
}
|
|
355
|
-
|
|
703
|
+
const childEnvironment = {
|
|
356
704
|
...(0, run_sst_command_js_1.createSstChildEnvironment)(environment, invocationEnvironment),
|
|
357
705
|
EBK_RELEASE_SOURCE_COMMIT: candidateSourceCommit,
|
|
358
706
|
};
|
|
707
|
+
// These values authorize the retained plan after SST has produced it. They
|
|
708
|
+
// are not desired infrastructure inputs and must never enter SST's program
|
|
709
|
+
// environment, where components such as Nextjs can persist the ambient
|
|
710
|
+
// process environment in a command resource. Keeping them outside the SST
|
|
711
|
+
// child makes the unapproved and approved previews materially identical.
|
|
712
|
+
for (const name of [
|
|
713
|
+
'EBK_LOCAL_PLAN_APPROVER_ARN',
|
|
714
|
+
'EBK_LOCAL_PLAN_MATERIAL_DIGEST',
|
|
715
|
+
'EBK_LOCAL_PLAN_SUMMARY_DIGEST',
|
|
716
|
+
]) {
|
|
717
|
+
delete childEnvironment[name];
|
|
718
|
+
}
|
|
719
|
+
return childEnvironment;
|
|
359
720
|
}
|
|
360
721
|
function frameworkAndBlueprintVersions(workspaceRoot, projectRoot) {
|
|
361
722
|
const workspacePackage = record(readJsonFile((0, node_path_1.join)(workspaceRoot, 'package.json'), 'Workspace package'), 'Workspace package');
|
|
@@ -461,15 +822,26 @@ function defaultSpawn(executable, args, options) {
|
|
|
461
822
|
function canonicalJson(value) {
|
|
462
823
|
return (0, canonical_json_1.canonicalJson)(value, { omitUndefinedObjectValues: true });
|
|
463
824
|
}
|
|
464
|
-
function
|
|
825
|
+
function stateExport(value) {
|
|
465
826
|
const root = record(value, 'SST state export');
|
|
827
|
+
const sstCheckpoint = typeof root['latest'] === 'object' && root['latest'] !== null;
|
|
466
828
|
const deployment = typeof root['deployment'] === 'object' && root['deployment'] !== null
|
|
467
829
|
? record(root['deployment'], 'SST state deployment')
|
|
468
|
-
:
|
|
830
|
+
: sstCheckpoint
|
|
831
|
+
? record(root['latest'], 'SST state checkpoint')
|
|
832
|
+
: root;
|
|
469
833
|
if (!Array.isArray(deployment['resources'])) {
|
|
470
834
|
throw new Error('SST state export does not contain a Pulumi deployment resource list.');
|
|
471
835
|
}
|
|
472
|
-
|
|
836
|
+
if (sstCheckpoint &&
|
|
837
|
+
(typeof root['stack'] !== 'string' ||
|
|
838
|
+
!/^[^/]+\/[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?\/(development|staging|production)$/.test(root['stack']))) {
|
|
839
|
+
throw new Error('SST state checkpoint stack identity is malformed.');
|
|
840
|
+
}
|
|
841
|
+
return {
|
|
842
|
+
resources: deployment['resources'].map((resource, index) => record(resource, `SST state resource ${index}`)),
|
|
843
|
+
...(sstCheckpoint ? { sstStack: root['stack'] } : {}),
|
|
844
|
+
};
|
|
473
845
|
}
|
|
474
846
|
function stringRecord(value) {
|
|
475
847
|
if (typeof value !== 'object' ||
|
|
@@ -619,7 +991,21 @@ function removePhysicalSelector(resource) {
|
|
|
619
991
|
return undefined;
|
|
620
992
|
}
|
|
621
993
|
function expectedTags(resource) {
|
|
622
|
-
|
|
994
|
+
// The AWS provider records the complete effective tag authority in tagsAll.
|
|
995
|
+
// Resource-local tags omit provider default tags, so comparing those bytes to
|
|
996
|
+
// AWS's complete observed tag set would permanently classify every
|
|
997
|
+
// default-tagged resource as drifted.
|
|
998
|
+
const materialAuthority = resource.external === true
|
|
999
|
+
? [resource.outputs, resource.inputs]
|
|
1000
|
+
: [resource.inputs, resource.outputs];
|
|
1001
|
+
for (const material of materialAuthority) {
|
|
1002
|
+
if (typeof material !== 'object' || material === null)
|
|
1003
|
+
continue;
|
|
1004
|
+
const tags = stringRecord(material['tagsAll']);
|
|
1005
|
+
if (tags)
|
|
1006
|
+
return tags;
|
|
1007
|
+
}
|
|
1008
|
+
for (const material of materialAuthority) {
|
|
623
1009
|
if (typeof material !== 'object' || material === null)
|
|
624
1010
|
continue;
|
|
625
1011
|
const tags = stringRecord(material['tags']);
|
|
@@ -647,8 +1033,473 @@ const PULUMI_REFRESH_PREVIEW_VERSION = '3.215.0';
|
|
|
647
1033
|
const REFRESH_PREVIEW_PROPERTY_OVERLAYS = {
|
|
648
1034
|
'aws:secretsmanager/secretVersion:SecretVersion': ['inputs.secretString'],
|
|
649
1035
|
};
|
|
1036
|
+
function observeSecretsManagerVersion(input) {
|
|
1037
|
+
const urn = typeof input.resource.urn === 'string' ? input.resource.urn : 'unknown';
|
|
1038
|
+
const inputs = record(input.resource.inputs, `${urn} inputs`);
|
|
1039
|
+
const outputs = record(input.resource.outputs, `${urn} outputs`);
|
|
1040
|
+
const secretId = outputs['secretId'];
|
|
1041
|
+
const versionId = outputs['versionId'];
|
|
1042
|
+
const expectedStages = outputs['versionStages'];
|
|
1043
|
+
const resourceId = input.resource.id;
|
|
1044
|
+
const expectedArn = new RegExp(`^arn:(?:aws|aws-us-gov|aws-cn):secretsmanager:${input.region}:${input.account}:secret:[^\\s]+$`);
|
|
1045
|
+
if (typeof secretId !== 'string' ||
|
|
1046
|
+
secretId.length > 2_048 ||
|
|
1047
|
+
hasControlCharacters(secretId) ||
|
|
1048
|
+
!expectedArn.test(secretId) ||
|
|
1049
|
+
typeof versionId !== 'string' ||
|
|
1050
|
+
versionId.length > 256 ||
|
|
1051
|
+
hasControlCharacters(versionId) ||
|
|
1052
|
+
inputs['secretId'] !== secretId ||
|
|
1053
|
+
resourceId !== `${secretId}|${versionId}` ||
|
|
1054
|
+
!Array.isArray(expectedStages) ||
|
|
1055
|
+
expectedStages.length === 0 ||
|
|
1056
|
+
expectedStages.length > 20 ||
|
|
1057
|
+
expectedStages.some((stage) => typeof stage !== 'string' ||
|
|
1058
|
+
stage.length === 0 ||
|
|
1059
|
+
stage.length > 256 ||
|
|
1060
|
+
hasControlCharacters(stage)) ||
|
|
1061
|
+
new Set(expectedStages).size !== expectedStages.length) {
|
|
1062
|
+
throw new Error('Secrets Manager version state does not expose one exact account-, Region-, and stage-bound identity.');
|
|
1063
|
+
}
|
|
1064
|
+
const normalizedExpectedStages = [...expectedStages].sort(deterministic_order_js_1.compareUtf16);
|
|
1065
|
+
const listed = record(runJsonCommand(input.spawn, 'aws', [
|
|
1066
|
+
'secretsmanager',
|
|
1067
|
+
'list-secret-version-ids',
|
|
1068
|
+
'--secret-id',
|
|
1069
|
+
secretId,
|
|
1070
|
+
'--include-deprecated',
|
|
1071
|
+
'--max-results',
|
|
1072
|
+
'100',
|
|
1073
|
+
'--output',
|
|
1074
|
+
'json',
|
|
1075
|
+
], {
|
|
1076
|
+
cwd: input.invocation.cwd,
|
|
1077
|
+
env: input.childEnvironment,
|
|
1078
|
+
label: 'AWS Secrets Manager version-stage observation',
|
|
1079
|
+
}), 'AWS Secrets Manager version-stage observation');
|
|
1080
|
+
if (listed['NextToken'] !== undefined) {
|
|
1081
|
+
throw new Error('AWS Secrets Manager version-stage observation exceeds its bounded inventory.');
|
|
1082
|
+
}
|
|
1083
|
+
const versions = listed['Versions'];
|
|
1084
|
+
if (!Array.isArray(versions) || versions.length > 100) {
|
|
1085
|
+
throw new Error('AWS Secrets Manager version-stage observation is malformed or unbounded.');
|
|
1086
|
+
}
|
|
1087
|
+
const observed = new Map();
|
|
1088
|
+
for (const [index, value] of versions.entries()) {
|
|
1089
|
+
const version = record(value, `AWS Secrets Manager version-stage observation Versions[${String(index)}]`);
|
|
1090
|
+
const observedVersionId = version['VersionId'];
|
|
1091
|
+
const observedStages = version['VersionStages'] ?? [];
|
|
1092
|
+
if (typeof observedVersionId !== 'string' ||
|
|
1093
|
+
observedVersionId.length === 0 ||
|
|
1094
|
+
observedVersionId.length > 256 ||
|
|
1095
|
+
hasControlCharacters(observedVersionId) ||
|
|
1096
|
+
!Array.isArray(observedStages) ||
|
|
1097
|
+
observedStages.length > 20 ||
|
|
1098
|
+
observedStages.some((stage) => typeof stage !== 'string' ||
|
|
1099
|
+
stage.length === 0 ||
|
|
1100
|
+
stage.length > 256 ||
|
|
1101
|
+
hasControlCharacters(stage)) ||
|
|
1102
|
+
new Set(observedStages).size !== observedStages.length ||
|
|
1103
|
+
observed.has(observedVersionId)) {
|
|
1104
|
+
throw new Error('AWS Secrets Manager version-stage observation contains an ambiguous version identity.');
|
|
1105
|
+
}
|
|
1106
|
+
observed.set(observedVersionId, [...observedStages].sort(deterministic_order_js_1.compareUtf16));
|
|
1107
|
+
}
|
|
1108
|
+
const observedStages = observed.get(versionId);
|
|
1109
|
+
return {
|
|
1110
|
+
arnDigest: digest(secretId),
|
|
1111
|
+
drifted: observedStages === undefined ||
|
|
1112
|
+
canonicalJson(observedStages) !== canonicalJson(normalizedExpectedStages),
|
|
1113
|
+
observedIdentityDigest: observedStages === undefined
|
|
1114
|
+
? null
|
|
1115
|
+
: digest(canonicalJson({
|
|
1116
|
+
secretId,
|
|
1117
|
+
versionId,
|
|
1118
|
+
versionStages: observedStages,
|
|
1119
|
+
})),
|
|
1120
|
+
urn,
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
650
1123
|
function normalizedDiffMaterial(diff) {
|
|
651
|
-
return
|
|
1124
|
+
return (0, index_js_1.materialNormalizedSstDiffResources)(diff);
|
|
1125
|
+
}
|
|
1126
|
+
const REFRESH_STATE_PROVIDER_OPERATIONS = new Set([
|
|
1127
|
+
'discard',
|
|
1128
|
+
'discard-replaced',
|
|
1129
|
+
'read',
|
|
1130
|
+
'read-replacement',
|
|
1131
|
+
]);
|
|
1132
|
+
function withoutRefreshStateResources(diff) {
|
|
1133
|
+
return {
|
|
1134
|
+
...diff,
|
|
1135
|
+
resources: diff.resources.filter(({ providerOperation }) => !REFRESH_STATE_PROVIDER_OPERATIONS.has(providerOperation)),
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
const MAX_REFRESH_DIAGNOSTIC_RESOURCES = 512;
|
|
1139
|
+
const MAX_REFRESH_DIAGNOSTIC_PATHS_PER_RESOURCE = 128;
|
|
1140
|
+
const MAX_REFRESH_DIAGNOSTIC_DEPTH = 32;
|
|
1141
|
+
function isExactInertSameEntry(recordEntry) {
|
|
1142
|
+
if (recordEntry['op'] !== 'same')
|
|
1143
|
+
return false;
|
|
1144
|
+
const urn = recordEntry['urn'];
|
|
1145
|
+
const resourceType = recordEntry['type'];
|
|
1146
|
+
const detailedDiff = recordEntry['detailedDiff'];
|
|
1147
|
+
const diffs = recordEntry['diffs'];
|
|
1148
|
+
const keys = recordEntry['keys'];
|
|
1149
|
+
const hasOld = Object.prototype.hasOwnProperty.call(recordEntry, 'old');
|
|
1150
|
+
const hasNew = Object.prototype.hasOwnProperty.call(recordEntry, 'new');
|
|
1151
|
+
const isEmptyRecord = typeof detailedDiff === 'object' &&
|
|
1152
|
+
detailedDiff !== null &&
|
|
1153
|
+
!Array.isArray(detailedDiff) &&
|
|
1154
|
+
Object.keys(detailedDiff).length === 0;
|
|
1155
|
+
return (typeof urn === 'string' &&
|
|
1156
|
+
urn.length > 0 &&
|
|
1157
|
+
typeof resourceType === 'string' &&
|
|
1158
|
+
resourceType.length > 0 &&
|
|
1159
|
+
(detailedDiff === undefined || detailedDiff === null || isEmptyRecord) &&
|
|
1160
|
+
(diffs === undefined || (Array.isArray(diffs) && diffs.length === 0)) &&
|
|
1161
|
+
(keys === undefined || (Array.isArray(keys) && keys.length === 0)) &&
|
|
1162
|
+
hasOld === hasNew &&
|
|
1163
|
+
(!hasOld ||
|
|
1164
|
+
canonicalJson(recordEntry['old']) === canonicalJson(recordEntry['new'])));
|
|
1165
|
+
}
|
|
1166
|
+
/** @internal */
|
|
1167
|
+
function normalizeDesiredSstDiffV4_17_1(input) {
|
|
1168
|
+
let raw = input.stdout;
|
|
1169
|
+
if (typeof raw === 'string') {
|
|
1170
|
+
try {
|
|
1171
|
+
raw = JSON.parse(raw);
|
|
1172
|
+
}
|
|
1173
|
+
catch {
|
|
1174
|
+
return (0, index_js_1.normalizeSstDiffV4_17_1)(input);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
const desired = Array.isArray(raw)
|
|
1178
|
+
? raw.filter((entry) => !(typeof entry === 'object' &&
|
|
1179
|
+
entry !== null &&
|
|
1180
|
+
!Array.isArray(entry) &&
|
|
1181
|
+
isExactInertSameEntry(entry)))
|
|
1182
|
+
: raw;
|
|
1183
|
+
return (0, index_js_1.normalizeSstDiffV4_17_1)({ ...input, stdout: desired });
|
|
1184
|
+
}
|
|
1185
|
+
function jsonPointerSegment(value) {
|
|
1186
|
+
return value.replace(/~/g, '~0').replace(/\//g, '~1');
|
|
1187
|
+
}
|
|
1188
|
+
function changedJsonPointerPaths(left, right) {
|
|
1189
|
+
const paths = [];
|
|
1190
|
+
const visit = (previous, current, path, depth) => {
|
|
1191
|
+
if (paths.length >= MAX_REFRESH_DIAGNOSTIC_PATHS_PER_RESOURCE)
|
|
1192
|
+
return;
|
|
1193
|
+
if (previous === undefined || current === undefined) {
|
|
1194
|
+
if (previous !== current)
|
|
1195
|
+
paths.push(path || '/');
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
if (canonicalJson(previous) === canonicalJson(current)) {
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
const previousRecord = typeof previous === 'object' &&
|
|
1202
|
+
previous !== null &&
|
|
1203
|
+
!Array.isArray(previous)
|
|
1204
|
+
? previous
|
|
1205
|
+
: undefined;
|
|
1206
|
+
const currentRecord = typeof current === 'object' && current !== null && !Array.isArray(current)
|
|
1207
|
+
? current
|
|
1208
|
+
: undefined;
|
|
1209
|
+
if (depth >= MAX_REFRESH_DIAGNOSTIC_DEPTH ||
|
|
1210
|
+
previousRecord === undefined ||
|
|
1211
|
+
currentRecord === undefined) {
|
|
1212
|
+
paths.push(path || '/');
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
for (const key of [
|
|
1216
|
+
...new Set([
|
|
1217
|
+
...Object.keys(previousRecord),
|
|
1218
|
+
...Object.keys(currentRecord),
|
|
1219
|
+
]),
|
|
1220
|
+
].sort(deterministic_order_js_1.compareUtf16)) {
|
|
1221
|
+
if (paths.length >= MAX_REFRESH_DIAGNOSTIC_PATHS_PER_RESOURCE)
|
|
1222
|
+
break;
|
|
1223
|
+
visit(previousRecord[key], currentRecord[key], `${path}/${jsonPointerSegment(key)}`, depth + 1);
|
|
1224
|
+
}
|
|
1225
|
+
};
|
|
1226
|
+
visit(left, right, '', 0);
|
|
1227
|
+
return paths;
|
|
1228
|
+
}
|
|
1229
|
+
const EXTERNAL_LOOKUP_NAME_TYPES = new Set([
|
|
1230
|
+
'aws:cloudwatch/eventBus:EventBus',
|
|
1231
|
+
'aws:ecs/cluster:Cluster',
|
|
1232
|
+
'aws:iam/role:Role',
|
|
1233
|
+
]);
|
|
1234
|
+
const EXTERNAL_LOOKUP_TAG_NAME_TYPES = new Set([
|
|
1235
|
+
'aws:ec2/eip:Eip',
|
|
1236
|
+
'aws:ec2/instance:Instance',
|
|
1237
|
+
'aws:ec2/internetGateway:InternetGateway',
|
|
1238
|
+
'aws:ec2/natGateway:NatGateway',
|
|
1239
|
+
'aws:ec2/routeTable:RouteTable',
|
|
1240
|
+
'aws:ec2/securityGroup:SecurityGroup',
|
|
1241
|
+
'aws:ec2/subnet:Subnet',
|
|
1242
|
+
'aws:ec2/vpc:Vpc',
|
|
1243
|
+
]);
|
|
1244
|
+
function exactRecord(value) {
|
|
1245
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
1246
|
+
? value
|
|
1247
|
+
: undefined;
|
|
1248
|
+
}
|
|
1249
|
+
function exactStringAt(value, ...path) {
|
|
1250
|
+
let current = value;
|
|
1251
|
+
for (const segment of path) {
|
|
1252
|
+
const currentRecord = exactRecord(current);
|
|
1253
|
+
if (!currentRecord)
|
|
1254
|
+
return undefined;
|
|
1255
|
+
current = currentRecord[segment];
|
|
1256
|
+
}
|
|
1257
|
+
return typeof current === 'string' && current.length > 0
|
|
1258
|
+
? current
|
|
1259
|
+
: undefined;
|
|
1260
|
+
}
|
|
1261
|
+
function escapedRegularExpression(value) {
|
|
1262
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1263
|
+
}
|
|
1264
|
+
const SST_PRETTY_NAME_SUFFIX = /^[abcdefhkmnorstuvwxz]{8}$/u;
|
|
1265
|
+
function exactSstPhysicalName(maxLength, project, stack, logicalId, candidate) {
|
|
1266
|
+
// Pinned SST 4.17.1's physicalName(max, name) reserves nine characters for
|
|
1267
|
+
// `-<pretty suffix>`, strips non-alphanumeric logical-name characters, and
|
|
1268
|
+
// selects one of name, stage+name, or app+stage+name before truncation. Long
|
|
1269
|
+
// external IAM role reads therefore do not use the simple full prefix below.
|
|
1270
|
+
const mainLength = maxLength - 9;
|
|
1271
|
+
const name = logicalId.replace(/[^a-zA-Z0-9]/gu, '');
|
|
1272
|
+
if (mainLength <= 0 || name.length === 0 || candidate.length > maxLength) {
|
|
1273
|
+
return false;
|
|
1274
|
+
}
|
|
1275
|
+
let expectedMain;
|
|
1276
|
+
if (name.length + 1 >= mainLength) {
|
|
1277
|
+
expectedMain = name.substring(0, mainLength);
|
|
1278
|
+
}
|
|
1279
|
+
else if (name.length + stack.length + 2 >= mainLength) {
|
|
1280
|
+
expectedMain = `${stack.substring(0, mainLength - name.length - 1)}-${name}`;
|
|
1281
|
+
}
|
|
1282
|
+
else {
|
|
1283
|
+
expectedMain = `${project.substring(0, mainLength - stack.length - name.length - 2)}-${stack}-${name}`;
|
|
1284
|
+
}
|
|
1285
|
+
return (candidate.length === expectedMain.length + 9 &&
|
|
1286
|
+
candidate.startsWith(`${expectedMain}-`) &&
|
|
1287
|
+
SST_PRETTY_NAME_SUFFIX.test(candidate.slice(-8)));
|
|
1288
|
+
}
|
|
1289
|
+
function exactOwnPropertyAbsent(value, ...path) {
|
|
1290
|
+
const property = path.at(-1);
|
|
1291
|
+
if (!property)
|
|
1292
|
+
return false;
|
|
1293
|
+
let current = value;
|
|
1294
|
+
for (const segment of path.slice(0, -1)) {
|
|
1295
|
+
const currentRecord = exactRecord(current);
|
|
1296
|
+
if (!currentRecord)
|
|
1297
|
+
return false;
|
|
1298
|
+
current = currentRecord[segment];
|
|
1299
|
+
}
|
|
1300
|
+
const parent = exactRecord(current);
|
|
1301
|
+
return Boolean(parent && !Object.prototype.hasOwnProperty.call(parent, property));
|
|
1302
|
+
}
|
|
1303
|
+
function exactExternalLookupNormalization(input) {
|
|
1304
|
+
// Pulumi `Resource.get` records stack-local auto-names in input state even
|
|
1305
|
+
// though the provider read is bound to a different exact physical ID. The
|
|
1306
|
+
// provider hydrates that one input from the observed resource on every
|
|
1307
|
+
// refresh, so treating it as owned-resource drift permanently wedges a
|
|
1308
|
+
// consumer of pre-provisioned roles or Foundation discovery resources. This
|
|
1309
|
+
// exception is intentionally limited to external resources, exact Pulumi
|
|
1310
|
+
// auto-name shapes, one known input path, and an unchanged output/identity
|
|
1311
|
+
// whose observed value corroborates the hydrated input.
|
|
1312
|
+
const urn = input.entry['urn'];
|
|
1313
|
+
const resourceType = input.entry['type'];
|
|
1314
|
+
if (typeof urn !== 'string' ||
|
|
1315
|
+
typeof resourceType !== 'string' ||
|
|
1316
|
+
input.resource?.external !== true ||
|
|
1317
|
+
input.resource.urn !== urn ||
|
|
1318
|
+
input.resource.type !== resourceType) {
|
|
1319
|
+
return undefined;
|
|
1320
|
+
}
|
|
1321
|
+
const urnParts = urn.split('::');
|
|
1322
|
+
const stack = urnParts[0]?.replace(/^urn:pulumi:/u, '');
|
|
1323
|
+
const project = urnParts[1];
|
|
1324
|
+
const logicalId = urnParts[3];
|
|
1325
|
+
if (urnParts.length !== 4 ||
|
|
1326
|
+
!stack ||
|
|
1327
|
+
!project ||
|
|
1328
|
+
!logicalId ||
|
|
1329
|
+
!/^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/u.test(stack) ||
|
|
1330
|
+
!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(project) ||
|
|
1331
|
+
!/^[A-Za-z0-9][A-Za-z0-9._-]{0,255}$/u.test(logicalId)) {
|
|
1332
|
+
return undefined;
|
|
1333
|
+
}
|
|
1334
|
+
const oldValue = input.entry['old'];
|
|
1335
|
+
const newValue = input.entry['new'];
|
|
1336
|
+
const oldRecord = exactRecord(oldValue);
|
|
1337
|
+
const newRecord = exactRecord(newValue);
|
|
1338
|
+
const oldInputs = exactRecord(oldRecord?.['inputs']);
|
|
1339
|
+
const oldOutputs = exactRecord(oldRecord?.['outputs']);
|
|
1340
|
+
const retainedInputs = exactRecord(input.resource.inputs);
|
|
1341
|
+
const retainedOutputs = exactRecord(input.resource.outputs);
|
|
1342
|
+
if (!oldRecord ||
|
|
1343
|
+
!newRecord ||
|
|
1344
|
+
!oldInputs ||
|
|
1345
|
+
!oldOutputs ||
|
|
1346
|
+
!retainedInputs ||
|
|
1347
|
+
!retainedOutputs ||
|
|
1348
|
+
canonicalJson(oldInputs) !== canonicalJson(retainedInputs) ||
|
|
1349
|
+
canonicalJson(oldOutputs) !== canonicalJson(retainedOutputs) ||
|
|
1350
|
+
(oldRecord['id'] !== undefined && oldRecord['id'] !== input.resource.id) ||
|
|
1351
|
+
(newRecord['id'] !== undefined &&
|
|
1352
|
+
oldRecord['id'] !== undefined &&
|
|
1353
|
+
newRecord['id'] !== oldRecord['id'])) {
|
|
1354
|
+
return undefined;
|
|
1355
|
+
}
|
|
1356
|
+
const onlyPath = input.changedPropertyPaths.length === 1
|
|
1357
|
+
? input.changedPropertyPaths[0]
|
|
1358
|
+
: undefined;
|
|
1359
|
+
const expectedPrefix = `${project}-${stack}-${logicalId}`;
|
|
1360
|
+
if (onlyPath === '/inputs/name' &&
|
|
1361
|
+
EXTERNAL_LOOKUP_NAME_TYPES.has(resourceType)) {
|
|
1362
|
+
const oldName = exactStringAt(oldRecord, 'inputs', 'name');
|
|
1363
|
+
const newName = exactStringAt(newRecord, 'inputs', 'name');
|
|
1364
|
+
const observedName = exactStringAt(newRecord, 'outputs', 'name');
|
|
1365
|
+
const retainedObservedName = exactStringAt(input.resource.outputs, 'name');
|
|
1366
|
+
if (oldName &&
|
|
1367
|
+
newName &&
|
|
1368
|
+
observedName &&
|
|
1369
|
+
retainedObservedName === observedName &&
|
|
1370
|
+
newName === observedName &&
|
|
1371
|
+
oldName !== newName &&
|
|
1372
|
+
(resourceType === 'aws:iam/role:Role'
|
|
1373
|
+
? exactSstPhysicalName(64, project, stack, logicalId, oldName)
|
|
1374
|
+
: new RegExp(`^${escapedRegularExpression(expectedPrefix)}-[abcdefhkmnorstuvwxz]{8}$`, 'u').test(oldName))) {
|
|
1375
|
+
return { changedPropertyPath: onlyPath, logicalId, resourceType };
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
if (onlyPath === '/inputs/tags/Name' &&
|
|
1379
|
+
EXTERNAL_LOOKUP_TAG_NAME_TYPES.has(resourceType)) {
|
|
1380
|
+
const oldName = exactStringAt(oldRecord, 'inputs', 'tags', 'Name');
|
|
1381
|
+
const newName = exactStringAt(newRecord, 'inputs', 'tags', 'Name');
|
|
1382
|
+
const observedName = exactStringAt(newRecord, 'outputs', 'tagsAll', 'Name') ??
|
|
1383
|
+
exactStringAt(newRecord, 'outputs', 'tags', 'Name');
|
|
1384
|
+
const retainedObservedName = exactStringAt(input.resource.outputs, 'tagsAll', 'Name') ??
|
|
1385
|
+
exactStringAt(input.resource.outputs, 'tags', 'Name');
|
|
1386
|
+
if (oldName === expectedPrefix &&
|
|
1387
|
+
newName &&
|
|
1388
|
+
observedName &&
|
|
1389
|
+
retainedObservedName === observedName &&
|
|
1390
|
+
newName === observedName &&
|
|
1391
|
+
oldName !== newName) {
|
|
1392
|
+
return { changedPropertyPath: onlyPath, logicalId, resourceType };
|
|
1393
|
+
}
|
|
1394
|
+
if (resourceType === 'aws:ec2/securityGroup:SecurityGroup' &&
|
|
1395
|
+
oldName === expectedPrefix &&
|
|
1396
|
+
exactOwnPropertyAbsent(newRecord, 'inputs', 'tags', 'Name') &&
|
|
1397
|
+
exactOwnPropertyAbsent(newRecord, 'outputs', 'tags', 'Name') &&
|
|
1398
|
+
exactOwnPropertyAbsent(newRecord, 'outputs', 'tagsAll', 'Name') &&
|
|
1399
|
+
exactOwnPropertyAbsent(input.resource.outputs, 'tags', 'Name') &&
|
|
1400
|
+
exactOwnPropertyAbsent(input.resource.outputs, 'tagsAll', 'Name')) {
|
|
1401
|
+
return { changedPropertyPath: onlyPath, logicalId, resourceType };
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
return undefined;
|
|
1405
|
+
}
|
|
1406
|
+
const EXTERNAL_FRAMEWORK_TAG_OBSERVATION_PATHS = new Set([
|
|
1407
|
+
'/inputs/name',
|
|
1408
|
+
'/inputs/tags/Name',
|
|
1409
|
+
'/outputs/tags/ebk:framework-version',
|
|
1410
|
+
'/outputs/tagsAll/ebk:framework-version',
|
|
1411
|
+
]);
|
|
1412
|
+
function externalProviderReadLogicalId(resourceType, physicalId) {
|
|
1413
|
+
// This is the secret-free identity projection used by
|
|
1414
|
+
// infrastructure-change/normalize.ts for Pulumi provider reads. Keeping the
|
|
1415
|
+
// correlation on that projection lets the refresh observer prove that an
|
|
1416
|
+
// external-state observation is also present in the ordinary desired plan
|
|
1417
|
+
// without retaining its physical ID.
|
|
1418
|
+
return `external-${(0, node_crypto_1.createHash)('sha256')
|
|
1419
|
+
.update(resourceType)
|
|
1420
|
+
.update('\0')
|
|
1421
|
+
.update(physicalId)
|
|
1422
|
+
.digest('hex')
|
|
1423
|
+
.slice(0, 24)}`;
|
|
1424
|
+
}
|
|
1425
|
+
function exactExternalFrameworkTagObservation(input) {
|
|
1426
|
+
// A downstream stack records Resource.get state for Foundation resources.
|
|
1427
|
+
// After Foundation advances its framework-version tag, Pulumi reports that
|
|
1428
|
+
// external observation during Billing's refresh phase even though Billing's
|
|
1429
|
+
// ordinary desired phase already classifies the same physical identity as a
|
|
1430
|
+
// non-mutating provider read. Exclude only that exact metadata transition:
|
|
1431
|
+
// retained external identity, one matching ordinary read, both framework
|
|
1432
|
+
// tag projections changing together, and at most SST's already-bounded
|
|
1433
|
+
// name hydration. Any other provider observation remains drift.
|
|
1434
|
+
const urn = input.entry['urn'];
|
|
1435
|
+
const resourceType = input.entry['type'];
|
|
1436
|
+
if (typeof urn !== 'string' ||
|
|
1437
|
+
typeof resourceType !== 'string' ||
|
|
1438
|
+
input.resource?.external !== true ||
|
|
1439
|
+
input.resource.urn !== urn ||
|
|
1440
|
+
input.resource.type !== resourceType ||
|
|
1441
|
+
input.changedPropertyPaths.length === 0 ||
|
|
1442
|
+
input.changedPropertyPaths.length >=
|
|
1443
|
+
MAX_REFRESH_DIAGNOSTIC_PATHS_PER_RESOURCE ||
|
|
1444
|
+
input.changedPropertyPaths.some((path) => !EXTERNAL_FRAMEWORK_TAG_OBSERVATION_PATHS.has(path)) ||
|
|
1445
|
+
!input.changedPropertyPaths.includes('/outputs/tags/ebk:framework-version') ||
|
|
1446
|
+
!input.changedPropertyPaths.includes('/outputs/tagsAll/ebk:framework-version')) {
|
|
1447
|
+
return undefined;
|
|
1448
|
+
}
|
|
1449
|
+
const inputPaths = input.changedPropertyPaths.filter((path) => path.startsWith('/inputs/'));
|
|
1450
|
+
if (inputPaths.length > 1)
|
|
1451
|
+
return undefined;
|
|
1452
|
+
const oldRecord = exactRecord(input.entry['old']);
|
|
1453
|
+
const newRecord = exactRecord(input.entry['new']);
|
|
1454
|
+
const oldInputs = exactRecord(oldRecord?.['inputs']);
|
|
1455
|
+
const oldOutputs = exactRecord(oldRecord?.['outputs']);
|
|
1456
|
+
const retainedInputs = exactRecord(input.resource.inputs);
|
|
1457
|
+
const retainedOutputs = exactRecord(input.resource.outputs);
|
|
1458
|
+
const physicalId = input.resource.id;
|
|
1459
|
+
if (!oldRecord ||
|
|
1460
|
+
!newRecord ||
|
|
1461
|
+
!oldInputs ||
|
|
1462
|
+
!oldOutputs ||
|
|
1463
|
+
!retainedInputs ||
|
|
1464
|
+
!retainedOutputs ||
|
|
1465
|
+
typeof physicalId !== 'string' ||
|
|
1466
|
+
physicalId.length === 0 ||
|
|
1467
|
+
canonicalJson(oldInputs) !== canonicalJson(retainedInputs) ||
|
|
1468
|
+
canonicalJson(oldOutputs) !== canonicalJson(retainedOutputs) ||
|
|
1469
|
+
(oldRecord['id'] !== undefined && oldRecord['id'] !== physicalId) ||
|
|
1470
|
+
(newRecord['id'] !== undefined && newRecord['id'] !== physicalId)) {
|
|
1471
|
+
return undefined;
|
|
1472
|
+
}
|
|
1473
|
+
const oldFrameworkVersion = exactStringAt(oldRecord, 'outputs', 'tagsAll', 'ebk:framework-version');
|
|
1474
|
+
const oldFrameworkVersionTag = exactStringAt(oldRecord, 'outputs', 'tags', 'ebk:framework-version');
|
|
1475
|
+
const newFrameworkVersion = exactStringAt(newRecord, 'outputs', 'tagsAll', 'ebk:framework-version');
|
|
1476
|
+
const newFrameworkVersionTag = exactStringAt(newRecord, 'outputs', 'tags', 'ebk:framework-version');
|
|
1477
|
+
if (!oldFrameworkVersion ||
|
|
1478
|
+
oldFrameworkVersionTag !== oldFrameworkVersion ||
|
|
1479
|
+
!newFrameworkVersion ||
|
|
1480
|
+
newFrameworkVersionTag !== newFrameworkVersion ||
|
|
1481
|
+
oldFrameworkVersion === newFrameworkVersion) {
|
|
1482
|
+
return undefined;
|
|
1483
|
+
}
|
|
1484
|
+
if (inputPaths.length === 1 &&
|
|
1485
|
+
!exactExternalLookupNormalization({
|
|
1486
|
+
changedPropertyPaths: inputPaths,
|
|
1487
|
+
entry: input.entry,
|
|
1488
|
+
resource: input.resource,
|
|
1489
|
+
})) {
|
|
1490
|
+
return undefined;
|
|
1491
|
+
}
|
|
1492
|
+
const providerReadLogicalId = externalProviderReadLogicalId(resourceType, physicalId);
|
|
1493
|
+
const providerReadKey = `${resourceType}\0${providerReadLogicalId}`;
|
|
1494
|
+
if ((input.ordinaryProviderReadCounts.get(providerReadKey) ?? 0) < 1) {
|
|
1495
|
+
return undefined;
|
|
1496
|
+
}
|
|
1497
|
+
return {
|
|
1498
|
+
changedPropertyPaths: [...input.changedPropertyPaths],
|
|
1499
|
+
logicalId: urn.split('::').at(-1) ?? '',
|
|
1500
|
+
providerReadLogicalId,
|
|
1501
|
+
resourceType,
|
|
1502
|
+
};
|
|
652
1503
|
}
|
|
653
1504
|
function assessPulumiRefreshPreview(input) {
|
|
654
1505
|
const sstPath = input.invocation.args[0];
|
|
@@ -660,14 +1511,14 @@ function assessPulumiRefreshPreview(input) {
|
|
|
660
1511
|
env: {
|
|
661
1512
|
...input.childEnvironment,
|
|
662
1513
|
PULUMI_OPTION_REFRESH: 'true',
|
|
663
|
-
PULUMI_OPTION_RUN_PROGRAM: '
|
|
1514
|
+
PULUMI_OPTION_RUN_PROGRAM: 'false',
|
|
664
1515
|
},
|
|
665
|
-
maxBuffer:
|
|
1516
|
+
maxBuffer: exports.MAX_SST_DIFF_JSON_BYTES,
|
|
666
1517
|
shell: false,
|
|
667
1518
|
});
|
|
668
1519
|
let raw;
|
|
669
1520
|
try {
|
|
670
|
-
raw =
|
|
1521
|
+
raw = parseRefreshPreviewJson(result.stdout);
|
|
671
1522
|
}
|
|
672
1523
|
catch {
|
|
673
1524
|
raw = null;
|
|
@@ -675,7 +1526,26 @@ function assessPulumiRefreshPreview(input) {
|
|
|
675
1526
|
const entries = Array.isArray(raw) ? raw : [];
|
|
676
1527
|
const refreshEntries = [];
|
|
677
1528
|
const desiredEntries = [];
|
|
678
|
-
|
|
1529
|
+
const diagnosticResources = [];
|
|
1530
|
+
const externalFrameworkTagObservations = [];
|
|
1531
|
+
const externalLookupNormalizations = [];
|
|
1532
|
+
const ordinaryProviderReadCounts = new Map();
|
|
1533
|
+
for (const resource of input.ordinaryDiff.resources) {
|
|
1534
|
+
if (resource.operation !== 'read' ||
|
|
1535
|
+
resource.providerOperation !== 'read' ||
|
|
1536
|
+
resource.replacement ||
|
|
1537
|
+
resource.changedProperties.length > 0) {
|
|
1538
|
+
continue;
|
|
1539
|
+
}
|
|
1540
|
+
const key = `${resource.resourceType}\0${resource.logicalId}`;
|
|
1541
|
+
ordinaryProviderReadCounts.set(key, (ordinaryProviderReadCounts.get(key) ?? 0) + 1);
|
|
1542
|
+
}
|
|
1543
|
+
const externalResources = (input.stateResources ?? []).filter((resource) => resource.external === true && typeof resource.urn === 'string');
|
|
1544
|
+
const externalResourcesByUrn = new Map(externalResources.map((resource) => [resource.urn, resource]));
|
|
1545
|
+
let shapeComplete = Array.isArray(raw) &&
|
|
1546
|
+
externalResourcesByUrn.size === externalResources.length;
|
|
1547
|
+
let inertSameCount = 0;
|
|
1548
|
+
let rejectedSameCount = 0;
|
|
679
1549
|
const observedRefreshUrns = new Set();
|
|
680
1550
|
for (const entry of entries) {
|
|
681
1551
|
if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
|
|
@@ -700,72 +1570,334 @@ function assessPulumiRefreshPreview(input) {
|
|
|
700
1570
|
!Array.isArray(detailedDiff) &&
|
|
701
1571
|
Object.keys(detailedDiff).length > 0;
|
|
702
1572
|
const hasDiffs = Array.isArray(diffs) && diffs.length > 0;
|
|
1573
|
+
const changedPropertyPaths = [
|
|
1574
|
+
...(hasOld && hasNew
|
|
1575
|
+
? changedJsonPointerPaths(recordEntry['old'], recordEntry['new'])
|
|
1576
|
+
: []),
|
|
1577
|
+
...(hasDetailedDiff
|
|
1578
|
+
? Object.keys(detailedDiff).map((path) => `/providerDetailedDiff/${jsonPointerSegment(path)}`)
|
|
1579
|
+
: []),
|
|
1580
|
+
...(hasDiffs
|
|
1581
|
+
? diffs
|
|
1582
|
+
.filter((path) => typeof path === 'string')
|
|
1583
|
+
.map((path) => `/providerDiffs/${jsonPointerSegment(path)}`)
|
|
1584
|
+
: []),
|
|
1585
|
+
];
|
|
1586
|
+
const boundedChangedPropertyPaths = [...new Set(changedPropertyPaths)]
|
|
1587
|
+
.sort(deterministic_order_js_1.compareUtf16)
|
|
1588
|
+
.slice(0, MAX_REFRESH_DIAGNOSTIC_PATHS_PER_RESOURCE);
|
|
1589
|
+
const externalLookupNormalization = !hasDetailedDiff && !hasDiffs
|
|
1590
|
+
? exactExternalLookupNormalization({
|
|
1591
|
+
changedPropertyPaths: [...new Set(changedPropertyPaths)].sort(deterministic_order_js_1.compareUtf16),
|
|
1592
|
+
entry: recordEntry,
|
|
1593
|
+
resource: typeof urn === 'string'
|
|
1594
|
+
? externalResourcesByUrn.get(urn)
|
|
1595
|
+
: undefined,
|
|
1596
|
+
})
|
|
1597
|
+
: undefined;
|
|
1598
|
+
const externalFrameworkTagObservation = input.action === 'deploy' && !hasDetailedDiff && !hasDiffs
|
|
1599
|
+
? exactExternalFrameworkTagObservation({
|
|
1600
|
+
changedPropertyPaths: [...new Set(changedPropertyPaths)].sort(deterministic_order_js_1.compareUtf16),
|
|
1601
|
+
entry: recordEntry,
|
|
1602
|
+
ordinaryProviderReadCounts,
|
|
1603
|
+
resource: typeof urn === 'string'
|
|
1604
|
+
? externalResourcesByUrn.get(urn)
|
|
1605
|
+
: undefined,
|
|
1606
|
+
})
|
|
1607
|
+
: undefined;
|
|
703
1608
|
if (hasNew && recordEntry['new'] === null) {
|
|
704
1609
|
refreshEntries.push({ ...recordEntry, op: 'delete' });
|
|
705
1610
|
}
|
|
1611
|
+
else if (hasOld &&
|
|
1612
|
+
hasNew &&
|
|
1613
|
+
canonicalJson(recordEntry['old']) === canonicalJson(recordEntry['new'])) {
|
|
1614
|
+
// The refresh old/new pair is Pulumi's recorded-versus-observed cloud
|
|
1615
|
+
// authority. A refresh-preview event can also inherit detailedDiff and
|
|
1616
|
+
// diffs metadata from the ordinary desired-source phase (for example,
|
|
1617
|
+
// a candidate tag update). Identical canonical old/new material proves
|
|
1618
|
+
// the provider read itself observed no change; desired changes remain
|
|
1619
|
+
// independently bound by the preceding ordinary preview.
|
|
1620
|
+
continue;
|
|
1621
|
+
}
|
|
1622
|
+
else if (externalLookupNormalization) {
|
|
1623
|
+
if (externalLookupNormalizations.length >=
|
|
1624
|
+
MAX_REFRESH_DIAGNOSTIC_RESOURCES) {
|
|
1625
|
+
shapeComplete = false;
|
|
1626
|
+
}
|
|
1627
|
+
else {
|
|
1628
|
+
externalLookupNormalizations.push(externalLookupNormalization);
|
|
1629
|
+
}
|
|
1630
|
+
continue;
|
|
1631
|
+
}
|
|
1632
|
+
else if (externalFrameworkTagObservation) {
|
|
1633
|
+
if (externalFrameworkTagObservations.length >=
|
|
1634
|
+
MAX_REFRESH_DIAGNOSTIC_RESOURCES) {
|
|
1635
|
+
shapeComplete = false;
|
|
1636
|
+
}
|
|
1637
|
+
else {
|
|
1638
|
+
externalFrameworkTagObservations.push(externalFrameworkTagObservation);
|
|
1639
|
+
const key = `${externalFrameworkTagObservation.resourceType}\0${externalFrameworkTagObservation.providerReadLogicalId}`;
|
|
1640
|
+
ordinaryProviderReadCounts.set(key, (ordinaryProviderReadCounts.get(key) ?? 1) - 1);
|
|
1641
|
+
}
|
|
1642
|
+
continue;
|
|
1643
|
+
}
|
|
706
1644
|
else if (hasDetailedDiff ||
|
|
707
1645
|
hasDiffs ||
|
|
708
1646
|
(hasOld &&
|
|
709
1647
|
hasNew &&
|
|
710
1648
|
canonicalJson(recordEntry['old']) !==
|
|
711
1649
|
canonicalJson(recordEntry['new']))) {
|
|
712
|
-
refreshEntries.push({
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
1650
|
+
refreshEntries.push({
|
|
1651
|
+
...recordEntry,
|
|
1652
|
+
detailedDiff: Object.fromEntries(boundedChangedPropertyPaths.map((path) => [
|
|
1653
|
+
path,
|
|
1654
|
+
{
|
|
1655
|
+
diffKind: 'update',
|
|
1656
|
+
inputDiff: path.startsWith('/inputs/'),
|
|
1657
|
+
},
|
|
1658
|
+
])),
|
|
1659
|
+
op: 'update',
|
|
1660
|
+
});
|
|
721
1661
|
}
|
|
722
1662
|
else {
|
|
723
1663
|
shapeComplete = false;
|
|
724
1664
|
}
|
|
1665
|
+
if (refreshEntries.length > diagnosticResources.length &&
|
|
1666
|
+
diagnosticResources.length < MAX_REFRESH_DIAGNOSTIC_RESOURCES) {
|
|
1667
|
+
diagnosticResources.push({
|
|
1668
|
+
changeBasis: [
|
|
1669
|
+
...(hasDetailedDiff ? ['detailedDiff'] : []),
|
|
1670
|
+
...(hasDiffs ? ['diffs'] : []),
|
|
1671
|
+
...(hasOld &&
|
|
1672
|
+
hasNew &&
|
|
1673
|
+
canonicalJson(recordEntry['old']) !==
|
|
1674
|
+
canonicalJson(recordEntry['new'])
|
|
1675
|
+
? ['old-new']
|
|
1676
|
+
: []),
|
|
1677
|
+
],
|
|
1678
|
+
changedPropertyPaths: boundedChangedPropertyPaths,
|
|
1679
|
+
logicalId: typeof urn === 'string' ? (urn.split('::').at(-1) ?? '') : '',
|
|
1680
|
+
resourceType: typeof recordEntry['type'] === 'string'
|
|
1681
|
+
? recordEntry['type']
|
|
1682
|
+
: 'unknown',
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
725
1685
|
}
|
|
726
|
-
else if (operation
|
|
727
|
-
|
|
1686
|
+
else if (operation === 'same') {
|
|
1687
|
+
const isExactNoChangeMarker = isExactInertSameEntry(recordEntry);
|
|
1688
|
+
if (isExactNoChangeMarker)
|
|
1689
|
+
inertSameCount += 1;
|
|
1690
|
+
else {
|
|
1691
|
+
rejectedSameCount += 1;
|
|
1692
|
+
shapeComplete = false;
|
|
1693
|
+
}
|
|
1694
|
+
// Pinned Pulumi can emit an inert `same` step when the provider refresh
|
|
1695
|
+
// observed no drift and the following desired preview has no changes.
|
|
1696
|
+
// It carries no desired authority. Accept only the exact no-change shape
|
|
1697
|
+
// above and omit it from both the drift and desired projections.
|
|
728
1698
|
}
|
|
729
1699
|
else {
|
|
730
|
-
|
|
1700
|
+
desiredEntries.push(entry);
|
|
731
1701
|
}
|
|
732
1702
|
}
|
|
733
|
-
const
|
|
734
|
-
exitCode:
|
|
1703
|
+
const candidateDrift = (0, index_js_1.normalizeSstDiffV4_17_1)({
|
|
1704
|
+
exitCode: 0,
|
|
735
1705
|
stdout: refreshEntries,
|
|
736
1706
|
toolVersion: input.toolVersion,
|
|
737
1707
|
});
|
|
738
|
-
const
|
|
739
|
-
exitCode:
|
|
1708
|
+
const candidatePostRefreshDesired = normalizeDesiredSstDiffV4_17_1({
|
|
1709
|
+
exitCode: 0,
|
|
740
1710
|
stdout: desiredEntries,
|
|
741
1711
|
toolVersion: input.toolVersion,
|
|
742
1712
|
});
|
|
1713
|
+
const knownDiagnosticOperations = new Set([
|
|
1714
|
+
'create',
|
|
1715
|
+
'create-replacement',
|
|
1716
|
+
'delete',
|
|
1717
|
+
'delete-replaced',
|
|
1718
|
+
'discard',
|
|
1719
|
+
'discard-replaced',
|
|
1720
|
+
'import',
|
|
1721
|
+
'import-replacement',
|
|
1722
|
+
'read',
|
|
1723
|
+
'read-replacement',
|
|
1724
|
+
'remove-pending-replace',
|
|
1725
|
+
'replace',
|
|
1726
|
+
'update',
|
|
1727
|
+
]);
|
|
1728
|
+
const postRefreshDesiredOperationCounts = {};
|
|
1729
|
+
for (const entry of desiredEntries) {
|
|
1730
|
+
const operation = typeof entry === 'object' &&
|
|
1731
|
+
entry !== null &&
|
|
1732
|
+
!Array.isArray(entry) &&
|
|
1733
|
+
typeof entry['op'] === 'string' &&
|
|
1734
|
+
knownDiagnosticOperations.has(entry['op'])
|
|
1735
|
+
? entry['op']
|
|
1736
|
+
: 'unsupported';
|
|
1737
|
+
postRefreshDesiredOperationCounts[operation] =
|
|
1738
|
+
(postRefreshDesiredOperationCounts[operation] ?? 0) + 1;
|
|
1739
|
+
}
|
|
1740
|
+
const spawnErrorCode = result.error?.code;
|
|
1741
|
+
const errorCode = typeof spawnErrorCode === 'string' &&
|
|
1742
|
+
/^E[A-Z0-9_]{1,31}$/.test(spawnErrorCode)
|
|
1743
|
+
? spawnErrorCode
|
|
1744
|
+
: null;
|
|
1745
|
+
const command = {
|
|
1746
|
+
errorCode,
|
|
1747
|
+
signal: typeof result.signal === 'string' &&
|
|
1748
|
+
/^SIG[A-Z0-9]{1,15}$/.test(result.signal)
|
|
1749
|
+
? result.signal
|
|
1750
|
+
: null,
|
|
1751
|
+
status: Number.isInteger(result.status) ? result.status : null,
|
|
1752
|
+
stderrBytes: Buffer.byteLength(result.stderr ?? '', 'utf8'),
|
|
1753
|
+
stdoutBytes: Buffer.byteLength(result.stdout ?? '', 'utf8'),
|
|
1754
|
+
};
|
|
1755
|
+
// SST/Pulumi emits no machine-readable completion marker for an exit-one
|
|
1756
|
+
// preview. A well-formed prefix is therefore not mutation authority.
|
|
743
1757
|
const commandSucceeded = result.status === 0;
|
|
744
|
-
const
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
1758
|
+
const drift = commandSucceeded
|
|
1759
|
+
? candidateDrift
|
|
1760
|
+
: (0, index_js_1.normalizeSstDiffV4_17_1)({
|
|
1761
|
+
exitCode: result.status ?? -1,
|
|
1762
|
+
stdout: refreshEntries,
|
|
1763
|
+
toolVersion: input.toolVersion,
|
|
1764
|
+
});
|
|
1765
|
+
const postRefreshDesired = commandSucceeded
|
|
1766
|
+
? candidatePostRefreshDesired
|
|
1767
|
+
: normalizeDesiredSstDiffV4_17_1({
|
|
1768
|
+
exitCode: result.status ?? -1,
|
|
1769
|
+
stdout: desiredEntries,
|
|
1770
|
+
toolVersion: input.toolVersion,
|
|
1771
|
+
});
|
|
1772
|
+
const postRefreshDesiredFailureCounts = {};
|
|
1773
|
+
for (const failure of postRefreshDesired.failures) {
|
|
1774
|
+
postRefreshDesiredFailureCounts[failure.code] =
|
|
1775
|
+
(postRefreshDesiredFailureCounts[failure.code] ?? 0) + 1;
|
|
1776
|
+
}
|
|
1777
|
+
const ordinaryDesired = withoutRefreshStateResources(input.ordinaryDiff);
|
|
1778
|
+
const ordinaryDesiredMaterial = normalizedDiffMaterial(ordinaryDesired);
|
|
1779
|
+
const refreshReconciliation = input.action === 'refresh';
|
|
1780
|
+
const providerRefreshSeparated = (ordinaryDesired.complete || refreshReconciliation) &&
|
|
1781
|
+
postRefreshDesired.complete;
|
|
748
1782
|
const complete = commandSucceeded &&
|
|
749
1783
|
shapeComplete &&
|
|
750
1784
|
drift.complete &&
|
|
751
|
-
|
|
1785
|
+
(ordinaryDesired.complete || refreshReconciliation) &&
|
|
1786
|
+
postRefreshDesired.complete &&
|
|
752
1787
|
input.toolVersion === index_js_1.SST_DIFF_TOOL_VERSION &&
|
|
753
1788
|
PULUMI_REFRESH_PREVIEW_VERSION === '3.215.0';
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
1789
|
+
const entryCounts = {
|
|
1790
|
+
desired: desiredEntries.length,
|
|
1791
|
+
externalFrameworkTagObserved: externalFrameworkTagObservations.length,
|
|
1792
|
+
externalLookupNormalized: externalLookupNormalizations.length,
|
|
1793
|
+
inertSame: inertSameCount,
|
|
1794
|
+
refresh: observedRefreshUrns.size,
|
|
1795
|
+
rejectedSame: rejectedSameCount,
|
|
1796
|
+
};
|
|
1797
|
+
const ordinaryDesiredDiagnostics = {
|
|
1798
|
+
complete: ordinaryDesired.complete,
|
|
1799
|
+
failureCounts: Object.fromEntries(ordinaryDesired.failures
|
|
1800
|
+
.map(({ code }) => code)
|
|
1801
|
+
.sort(deterministic_order_js_1.compareUtf16)
|
|
1802
|
+
.reduce((counts, code) => {
|
|
1803
|
+
counts.set(code, (counts.get(code) ?? 0) + 1);
|
|
1804
|
+
return counts;
|
|
1805
|
+
}, new Map())),
|
|
1806
|
+
resourceCount: ordinaryDesired.resources.length,
|
|
1807
|
+
};
|
|
1808
|
+
const postRefreshDesiredDiagnostics = {
|
|
1809
|
+
complete: postRefreshDesired.complete,
|
|
1810
|
+
failureCounts: Object.fromEntries(Object.entries(postRefreshDesiredFailureCounts).sort(([left], [right]) => (0, deterministic_order_js_1.compareUtf16)(left, right))),
|
|
1811
|
+
operationCounts: Object.fromEntries(Object.entries(postRefreshDesiredOperationCounts).sort(([left], [right]) => (0, deterministic_order_js_1.compareUtf16)(left, right))),
|
|
1812
|
+
resourceCount: postRefreshDesired.resources.length,
|
|
1813
|
+
};
|
|
1814
|
+
const sortedDiagnosticResources = diagnosticResources.sort((left, right) => (0, deterministic_order_js_1.compareUtf16)(left.logicalId, right.logicalId) ||
|
|
1815
|
+
(0, deterministic_order_js_1.compareUtf16)(left.resourceType, right.resourceType));
|
|
1816
|
+
const sortedExternalLookupNormalizations = externalLookupNormalizations.sort((left, right) => (0, deterministic_order_js_1.compareUtf16)(left.logicalId, right.logicalId) ||
|
|
1817
|
+
(0, deterministic_order_js_1.compareUtf16)(left.resourceType, right.resourceType));
|
|
1818
|
+
const sortedExternalFrameworkTagObservations = externalFrameworkTagObservations.sort((left, right) => (0, deterministic_order_js_1.compareUtf16)(left.logicalId, right.logicalId) ||
|
|
1819
|
+
(0, deterministic_order_js_1.compareUtf16)(left.resourceType, right.resourceType));
|
|
1820
|
+
const observedRefreshInventory = [...observedRefreshUrns].sort();
|
|
1821
|
+
const previewDigest = digest(canonicalJson(
|
|
1822
|
+
// Refresh applies only the observed provider-state reconciliation. The
|
|
1823
|
+
// following desired preview is a completeness/operation-shape guard and
|
|
1824
|
+
// must be planned again before any deploy; binding its ephemeral build
|
|
1825
|
+
// identifiers here would make an otherwise identical refresh unreviewable.
|
|
1826
|
+
refreshReconciliation
|
|
1827
|
+
? {
|
|
1828
|
+
action: input.action,
|
|
1829
|
+
commandSucceeded,
|
|
1830
|
+
complete,
|
|
1831
|
+
desiredAuthority: 'refresh-reconciliation-plus-post-refresh-desired-validation',
|
|
761
1832
|
drift: normalizedDiffMaterial(drift),
|
|
1833
|
+
entryCounts,
|
|
1834
|
+
externalFrameworkTagObservations: sortedExternalFrameworkTagObservations,
|
|
1835
|
+
externalLookupNormalizations: sortedExternalLookupNormalizations,
|
|
1836
|
+
observedRefreshInventory,
|
|
1837
|
+
ordinaryDesired: ordinaryDesiredDiagnostics,
|
|
1838
|
+
postRefreshDesired: postRefreshDesiredDiagnostics,
|
|
1839
|
+
providerRefreshSeparated,
|
|
762
1840
|
pulumiVersion: PULUMI_REFRESH_PREVIEW_VERSION,
|
|
1841
|
+
resources: sortedDiagnosticResources,
|
|
763
1842
|
sstVersion: input.toolVersion,
|
|
764
|
-
}
|
|
1843
|
+
}
|
|
1844
|
+
: {
|
|
1845
|
+
action: input.action,
|
|
1846
|
+
desired: ordinaryDesiredMaterial,
|
|
1847
|
+
desiredAuthority: 'ordinary-preview-plus-separated-refresh-preview',
|
|
1848
|
+
ordinaryDesiredComplete: ordinaryDesired.complete,
|
|
1849
|
+
ordinaryDesiredFailures: ordinaryDesired.failures,
|
|
1850
|
+
drift: normalizedDiffMaterial(drift),
|
|
1851
|
+
externalFrameworkTagObservations: sortedExternalFrameworkTagObservations,
|
|
1852
|
+
externalLookupNormalizations: sortedExternalLookupNormalizations,
|
|
1853
|
+
// The ordinary preview above is the exact desired-source
|
|
1854
|
+
// authority retained by the material infrastructure plan. The
|
|
1855
|
+
// second program execution exists only to prove the provider
|
|
1856
|
+
// refresh stayed separated and that the desired program still
|
|
1857
|
+
// completed. Binding its full resource projection makes harmless
|
|
1858
|
+
// build/linker scheduling differences create a new drift digest
|
|
1859
|
+
// even when the ordinary material plan and provider observation
|
|
1860
|
+
// are exact. Retain its bounded completeness and operation shape;
|
|
1861
|
+
// never substitute it for the ordinary desired authority.
|
|
1862
|
+
postRefreshDesired: postRefreshDesiredDiagnostics,
|
|
1863
|
+
providerRefreshSeparated,
|
|
1864
|
+
pulumiVersion: PULUMI_REFRESH_PREVIEW_VERSION,
|
|
1865
|
+
sstVersion: input.toolVersion,
|
|
1866
|
+
}));
|
|
1867
|
+
const diagnostics = {
|
|
1868
|
+
authorityMode: refreshReconciliation
|
|
1869
|
+
? 'refresh-reconciliation'
|
|
1870
|
+
: 'ordinary-and-refresh-separated',
|
|
1871
|
+
command,
|
|
1872
|
+
commandSucceeded,
|
|
1873
|
+
complete,
|
|
1874
|
+
entryCounts,
|
|
1875
|
+
providerRefreshSeparated,
|
|
1876
|
+
ordinaryDesired: ordinaryDesiredDiagnostics,
|
|
1877
|
+
postRefreshDesired: postRefreshDesiredDiagnostics,
|
|
1878
|
+
digest: previewDigest,
|
|
1879
|
+
kind: 'ebk-infrastructure-refresh-preview-diagnostics',
|
|
1880
|
+
externalFrameworkTagObservations: sortedExternalFrameworkTagObservations,
|
|
1881
|
+
externalLookupNormalizations: sortedExternalLookupNormalizations,
|
|
1882
|
+
resources: sortedDiagnosticResources,
|
|
1883
|
+
schemaVersion: 7,
|
|
1884
|
+
};
|
|
1885
|
+
return {
|
|
1886
|
+
commandSucceeded,
|
|
1887
|
+
complete,
|
|
1888
|
+
diagnostics,
|
|
1889
|
+
providerRefreshSeparated,
|
|
1890
|
+
digest: previewDigest,
|
|
765
1891
|
drift,
|
|
766
|
-
observedRefreshUrns:
|
|
1892
|
+
observedRefreshUrns: observedRefreshInventory,
|
|
1893
|
+
rawDrift: JSON.stringify(refreshEntries),
|
|
1894
|
+
stderr: result.stderr,
|
|
767
1895
|
};
|
|
768
1896
|
}
|
|
1897
|
+
/** @internal */
|
|
1898
|
+
function requiresIamMutationAnalysis(_action, resource) {
|
|
1899
|
+
return (0, index_js_1.requiresIamPolicyAnalysis)(resource);
|
|
1900
|
+
}
|
|
769
1901
|
function costQuotaEvidence(input) {
|
|
770
1902
|
const quotaByType = {
|
|
771
1903
|
'aws:lambda/function:Function': {
|
|
@@ -869,8 +2001,10 @@ function iamPolicyDocuments(input) {
|
|
|
869
2001
|
: undefined;
|
|
870
2002
|
if (!policyProperty)
|
|
871
2003
|
continue;
|
|
872
|
-
if (resource.operation === 'delete'
|
|
2004
|
+
if (resource.operation === 'delete' ||
|
|
2005
|
+
REFRESH_STATE_PROVIDER_OPERATIONS.has(resource.providerOperation)) {
|
|
873
2006
|
continue;
|
|
2007
|
+
}
|
|
874
2008
|
const rawEntry = entries[resource.sourceIndex];
|
|
875
2009
|
if (typeof rawEntry !== 'object' ||
|
|
876
2010
|
rawEntry === null ||
|
|
@@ -1753,16 +2887,19 @@ function probeStandardRemoveResource(input) {
|
|
|
1753
2887
|
function observeInfrastructureState(input) {
|
|
1754
2888
|
const stage = requiredEnvironment(input.childEnvironment, 'EBK_CONSUMER_STAGE');
|
|
1755
2889
|
const region = requiredEnvironment(input.childEnvironment, 'AWS_REGION');
|
|
1756
|
-
const expectedAccount =
|
|
2890
|
+
const expectedAccount = expectedDeploymentAccount(input.environment, input.invocation);
|
|
1757
2891
|
const caller = record(runJsonCommand(input.spawn, 'aws', ['sts', 'get-caller-identity', '--output', 'json'], {
|
|
1758
2892
|
cwd: input.invocation.cwd,
|
|
1759
2893
|
env: input.childEnvironment,
|
|
1760
2894
|
label: 'AWS caller identity',
|
|
1761
2895
|
}), 'AWS caller identity');
|
|
2896
|
+
const callerArn = typeof caller['Arn'] === 'string' ? caller['Arn'] : '';
|
|
2897
|
+
const expectedRoleArn = input.childEnvironment.EBK_EXPECTED_DEPLOYMENT_ROLE_ARN?.trim();
|
|
1762
2898
|
if (caller['Account'] !== expectedAccount ||
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
2899
|
+
!callerArn ||
|
|
2900
|
+
(expectedRoleArn &&
|
|
2901
|
+
!matchesExpectedAssumedRoleArn(expectedAccount, callerArn, expectedRoleArn))) {
|
|
2902
|
+
throw new Error('Read-only infrastructure observer does not match the expected AWS account and temporary assumed role.');
|
|
1766
2903
|
}
|
|
1767
2904
|
const stateLocation = exactSstStateLocation(input.environment);
|
|
1768
2905
|
const stateKey = `app/${input.project}/${stage}.json`;
|
|
@@ -1794,7 +2931,10 @@ function observeInfrastructureState(input) {
|
|
|
1794
2931
|
maxBuffer: 32 * 1024 * 1024,
|
|
1795
2932
|
})
|
|
1796
2933
|
: null;
|
|
1797
|
-
const
|
|
2934
|
+
const exportedState = stateExists
|
|
2935
|
+
? stateExport(exported)
|
|
2936
|
+
: { resources: [] };
|
|
2937
|
+
const resources = exportedState.resources;
|
|
1798
2938
|
const stackIdentities = new Set();
|
|
1799
2939
|
for (const resource of resources) {
|
|
1800
2940
|
const urn = typeof resource.urn === 'string' ? resource.urn : '';
|
|
@@ -1808,6 +2948,10 @@ function observeInfrastructureState(input) {
|
|
|
1808
2948
|
const stackIdentity = stateExists
|
|
1809
2949
|
? [...stackIdentities][0]
|
|
1810
2950
|
: `${input.project}/${stage}`;
|
|
2951
|
+
if (exportedState.sstStack &&
|
|
2952
|
+
exportedState.sstStack.split('/').slice(1).join('/') !== stackIdentity) {
|
|
2953
|
+
throw new Error('SST state checkpoint identity does not match its Pulumi resource inventory.');
|
|
2954
|
+
}
|
|
1811
2955
|
const [application, observedStage] = stackIdentity.split('/');
|
|
1812
2956
|
if (!application || observedStage !== stage) {
|
|
1813
2957
|
throw new Error('SST state export stack identity does not match the requested stage.');
|
|
@@ -1833,14 +2977,14 @@ function observeInfrastructureState(input) {
|
|
|
1833
2977
|
urn: typeof resource.urn === 'string' ? resource.urn : 'unknown',
|
|
1834
2978
|
}));
|
|
1835
2979
|
const refreshPreview = assessPulumiRefreshPreview({
|
|
2980
|
+
action: input.action,
|
|
1836
2981
|
childEnvironment: input.childEnvironment,
|
|
1837
2982
|
invocation: input.invocation,
|
|
1838
2983
|
ordinaryDiff: input.ordinaryDiff,
|
|
1839
2984
|
spawn: input.spawn,
|
|
2985
|
+
stateResources: deployableResources,
|
|
1840
2986
|
toolVersion: input.toolVersion,
|
|
1841
2987
|
});
|
|
1842
|
-
const refreshInventoryComplete = deployableResources.every((resource) => typeof resource.urn === 'string' &&
|
|
1843
|
-
refreshPreview.observedRefreshUrns.includes(resource.urn));
|
|
1844
2988
|
const structuralResources = deployableResources
|
|
1845
2989
|
.map((resource) => ({
|
|
1846
2990
|
dependencies: Array.isArray(resource.dependencies)
|
|
@@ -1862,9 +3006,15 @@ function observeInfrastructureState(input) {
|
|
|
1862
3006
|
urn: typeof resource.urn === 'string' ? resource.urn : 'unknown',
|
|
1863
3007
|
}))
|
|
1864
3008
|
.filter((value) => value.arn !== undefined && value.tags !== undefined);
|
|
3009
|
+
// Multiple Pulumi resources can deliberately project the same physical AWS
|
|
3010
|
+
// resource (for example, a workload role and its registration component).
|
|
3011
|
+
// The Resource Groups Tagging API rejects duplicate ResourceARNList values,
|
|
3012
|
+
// while the evidence model must still compare the one observed tag set with
|
|
3013
|
+
// every distinct Pulumi URN below.
|
|
3014
|
+
const taggedArns = [...new Set(tagged.map(({ arn }) => arn))].sort(deterministic_order_js_1.compareUtf16);
|
|
1865
3015
|
const observedTags = new Map();
|
|
1866
|
-
for (let index = 0; index <
|
|
1867
|
-
const arns =
|
|
3016
|
+
for (let index = 0; index < taggedArns.length; index += 100) {
|
|
3017
|
+
const arns = taggedArns.slice(index, index + 100);
|
|
1868
3018
|
const result = record(runJsonCommand(input.spawn, 'aws', [
|
|
1869
3019
|
'resourcegroupstaggingapi',
|
|
1870
3020
|
'get-resources',
|
|
@@ -1907,6 +3057,27 @@ function observeInfrastructureState(input) {
|
|
|
1907
3057
|
typeof resource.urn === 'string' ? resource.urn : 'unknown',
|
|
1908
3058
|
resource,
|
|
1909
3059
|
]));
|
|
3060
|
+
const secretVersionObservations = new Map(deployableResources
|
|
3061
|
+
.filter((resource) => resource.type === 'aws:secretsmanager/secretVersion:SecretVersion')
|
|
3062
|
+
.map((resource) => {
|
|
3063
|
+
const observation = observeSecretsManagerVersion({
|
|
3064
|
+
account: expectedAccount,
|
|
3065
|
+
childEnvironment: input.childEnvironment,
|
|
3066
|
+
invocation: input.invocation,
|
|
3067
|
+
region,
|
|
3068
|
+
resource,
|
|
3069
|
+
spawn: input.spawn,
|
|
3070
|
+
});
|
|
3071
|
+
return [observation.urn, observation];
|
|
3072
|
+
}));
|
|
3073
|
+
// A successful pinned Pulumi refresh preview reads the complete stack but
|
|
3074
|
+
// emits refresh events only for recorded-versus-observed changes. Unchanged
|
|
3075
|
+
// resources are intentionally absent from the JSON event stream; requiring
|
|
3076
|
+
// one event per state resource would turn a clean read into partial coverage.
|
|
3077
|
+
// Every emitted refresh event is still parsed fail-closed above. Pulumi's
|
|
3078
|
+
// later desired-preview events are independently normalized but never used
|
|
3079
|
+
// as drift authority; the preceding ordinary preview remains the sole
|
|
3080
|
+
// desired-update authority.
|
|
1910
3081
|
const coverage = structuralResources.map(({ type, urn }) => {
|
|
1911
3082
|
const taggedResource = taggedByUrn.get(urn);
|
|
1912
3083
|
const observed = taggedResource !== undefined && observedTags.has(taggedResource.arn);
|
|
@@ -1919,27 +3090,33 @@ function observeInfrastructureState(input) {
|
|
|
1919
3090
|
: []),
|
|
1920
3091
|
].filter((path) => materialProperties.includes(path));
|
|
1921
3092
|
const refreshCoversResource = refreshPreview.complete &&
|
|
1922
|
-
refreshPreview.
|
|
1923
|
-
incompleteInventory.length === 0
|
|
1924
|
-
|
|
3093
|
+
refreshPreview.providerRefreshSeparated &&
|
|
3094
|
+
incompleteInventory.length === 0;
|
|
3095
|
+
const secretVersionObservation = secretVersionObservations.get(urn);
|
|
1925
3096
|
return {
|
|
1926
3097
|
adapter: refreshCoversResource
|
|
1927
|
-
?
|
|
3098
|
+
? secretVersionObservation
|
|
3099
|
+
? 'sst-pulumi-refresh-preview+aws-secrets-manager-version-stages'
|
|
3100
|
+
: 'sst-pulumi-refresh-preview'
|
|
1928
3101
|
: taggedResource === undefined
|
|
1929
3102
|
? 'none'
|
|
1930
3103
|
: 'aws-resource-groups-tagging-api',
|
|
1931
|
-
arnDigest:
|
|
3104
|
+
arnDigest: secretVersionObservation?.arnDigest ??
|
|
3105
|
+
(taggedResource === undefined ? null : digest(taggedResource.arn)),
|
|
1932
3106
|
coveredProperties: refreshCoversResource
|
|
1933
3107
|
? [
|
|
1934
3108
|
'existence',
|
|
1935
|
-
...materialProperties.filter((path) => !overlayProperties.includes(path)
|
|
3109
|
+
...materialProperties.filter((path) => !overlayProperties.includes(path) ||
|
|
3110
|
+
secretVersionObservation !== undefined),
|
|
1936
3111
|
]
|
|
1937
3112
|
: observed
|
|
1938
3113
|
? ['existence', 'tags']
|
|
1939
3114
|
: [],
|
|
1940
3115
|
type,
|
|
1941
3116
|
unobservedMaterialProperties: refreshCoversResource
|
|
1942
|
-
?
|
|
3117
|
+
? secretVersionObservation
|
|
3118
|
+
? []
|
|
3119
|
+
: overlayProperties
|
|
1943
3120
|
: materialProperties.length > 0
|
|
1944
3121
|
? materialProperties
|
|
1945
3122
|
: ['service-native-configuration'],
|
|
@@ -1947,7 +3124,8 @@ function observeInfrastructureState(input) {
|
|
|
1947
3124
|
};
|
|
1948
3125
|
});
|
|
1949
3126
|
const unsupported = coverage
|
|
1950
|
-
.filter(({ coveredProperties, unobservedMaterialProperties }) => coveredProperties.length
|
|
3127
|
+
.filter(({ coveredProperties, unobservedMaterialProperties }) => coveredProperties.length === 0 ||
|
|
3128
|
+
unobservedMaterialProperties.length > 0)
|
|
1951
3129
|
.map(({ adapter, coveredProperties, type, unobservedMaterialProperties, urn, }) => ({
|
|
1952
3130
|
adapter,
|
|
1953
3131
|
coveredProperties,
|
|
@@ -1956,8 +3134,7 @@ function observeInfrastructureState(input) {
|
|
|
1956
3134
|
urn,
|
|
1957
3135
|
}));
|
|
1958
3136
|
if (!refreshPreview.complete ||
|
|
1959
|
-
!refreshPreview.
|
|
1960
|
-
!refreshInventoryComplete ||
|
|
3137
|
+
!refreshPreview.providerRefreshSeparated ||
|
|
1961
3138
|
incompleteInventory.length > 0) {
|
|
1962
3139
|
unsupported.push({
|
|
1963
3140
|
adapter: 'sst-pulumi-refresh-preview',
|
|
@@ -1965,10 +3142,9 @@ function observeInfrastructureState(input) {
|
|
|
1965
3142
|
type: 'full-stack-refresh-preview',
|
|
1966
3143
|
unobservedMaterialProperties: [
|
|
1967
3144
|
...(!refreshPreview.complete ? ['preview.complete'] : []),
|
|
1968
|
-
...(!refreshPreview.
|
|
1969
|
-
? ['preview.
|
|
3145
|
+
...(!refreshPreview.providerRefreshSeparated
|
|
3146
|
+
? ['preview.provider-refresh-separated']
|
|
1970
3147
|
: []),
|
|
1971
|
-
...(!refreshInventoryComplete ? ['preview.refresh-inventory'] : []),
|
|
1972
3148
|
...(incompleteInventory.length > 0 ? ['state.custom-inventory'] : []),
|
|
1973
3149
|
],
|
|
1974
3150
|
urn: 'full-stack',
|
|
@@ -1989,12 +3165,18 @@ function observeInfrastructureState(input) {
|
|
|
1989
3165
|
stateKey: `app/${application}/${stage}.json`,
|
|
1990
3166
|
})),
|
|
1991
3167
|
};
|
|
3168
|
+
const allDrifted = [
|
|
3169
|
+
...drifted,
|
|
3170
|
+
...[...secretVersionObservations.values()]
|
|
3171
|
+
.filter((observation) => observation.drifted)
|
|
3172
|
+
.map(({ arnDigest, urn }) => ({ arnDigest, urn })),
|
|
3173
|
+
].sort((left, right) => (0, deterministic_order_js_1.compareUtf16)(left.urn, right.urn));
|
|
1992
3174
|
const driftStatus = !refreshPreview.commandSucceeded
|
|
1993
3175
|
? 'failed'
|
|
1994
|
-
:
|
|
1995
|
-
? '
|
|
1996
|
-
:
|
|
1997
|
-
? '
|
|
3176
|
+
: unsupported.length > 0
|
|
3177
|
+
? 'partial-coverage'
|
|
3178
|
+
: refreshPreview.drift.resources.length > 0 || allDrifted.length > 0
|
|
3179
|
+
? 'drift-detected'
|
|
1998
3180
|
: 'no-drift';
|
|
1999
3181
|
// The full export is canonicalized and hashed in memory. Only the digest is
|
|
2000
3182
|
// retained, so changes to IDs, inputs, outputs, protection, or provider
|
|
@@ -2038,11 +3220,14 @@ function observeInfrastructureState(input) {
|
|
|
2038
3220
|
})
|
|
2039
3221
|
.sort((left, right) => (0, deterministic_order_js_1.compareUtf16)(left.logicalId, right.logicalId) ||
|
|
2040
3222
|
(0, deterministic_order_js_1.compareUtf16)(left.resourceType, right.resourceType));
|
|
2041
|
-
|
|
3223
|
+
const observation = {
|
|
2042
3224
|
callInventory: [
|
|
2043
3225
|
'resourcegroupstaggingapi:GetResources',
|
|
2044
3226
|
's3:GetObject',
|
|
2045
3227
|
's3:ListBucket',
|
|
3228
|
+
...(secretVersionObservations.size > 0
|
|
3229
|
+
? ['secretsmanager:ListSecretVersionIds']
|
|
3230
|
+
: []),
|
|
2046
3231
|
'sst:DiffRefreshPreview',
|
|
2047
3232
|
'ssm:GetParameter',
|
|
2048
3233
|
'sts:GetCallerIdentity',
|
|
@@ -2051,19 +3236,28 @@ function observeInfrastructureState(input) {
|
|
|
2051
3236
|
coverage,
|
|
2052
3237
|
digest: digest(canonicalJson({
|
|
2053
3238
|
coverage,
|
|
2054
|
-
drifted,
|
|
3239
|
+
drifted: allDrifted,
|
|
2055
3240
|
incompleteInventory,
|
|
2056
3241
|
observerBinding,
|
|
2057
3242
|
refreshPreviewDigest: refreshPreview.digest,
|
|
2058
3243
|
refreshPreviewDrift: normalizedDiffMaterial(refreshPreview.drift),
|
|
2059
3244
|
refreshPreviewInventory: refreshPreview.observedRefreshUrns,
|
|
3245
|
+
secretsManagerVersionIdentities: [
|
|
3246
|
+
...secretVersionObservations.values(),
|
|
3247
|
+
]
|
|
3248
|
+
.map(({ arnDigest, observedIdentityDigest, urn }) => ({
|
|
3249
|
+
arnDigest,
|
|
3250
|
+
observedIdentityDigest,
|
|
3251
|
+
urn,
|
|
3252
|
+
}))
|
|
3253
|
+
.sort((left, right) => (0, deterministic_order_js_1.compareUtf16)(left.urn, right.urn)),
|
|
2060
3254
|
unsupported,
|
|
2061
3255
|
})),
|
|
2062
3256
|
expiresAt,
|
|
2063
3257
|
generatedAt,
|
|
2064
3258
|
status: driftStatus,
|
|
2065
3259
|
},
|
|
2066
|
-
observerArnDigest: digest(
|
|
3260
|
+
observerArnDigest: digest(callerArn),
|
|
2067
3261
|
resources: resourceInventory,
|
|
2068
3262
|
state: {
|
|
2069
3263
|
backend: stateExists
|
|
@@ -2077,6 +3271,13 @@ function observeInfrastructureState(input) {
|
|
|
2077
3271
|
version: digest(canonicalJson({ lock: stateLock.digest, stateDigest })),
|
|
2078
3272
|
},
|
|
2079
3273
|
};
|
|
3274
|
+
refreshPreviewDiagnosticsByObservation.set(observation, refreshPreview.diagnostics);
|
|
3275
|
+
refreshMutationPlanMaterialByObservation.set(observation, {
|
|
3276
|
+
diff: refreshPreview.drift,
|
|
3277
|
+
stderr: refreshPreview.stderr,
|
|
3278
|
+
stdout: refreshPreview.rawDrift,
|
|
3279
|
+
});
|
|
3280
|
+
return observation;
|
|
2080
3281
|
}
|
|
2081
3282
|
function writeGithubOutputs(environment, values) {
|
|
2082
3283
|
const path = environment.GITHUB_OUTPUT?.trim();
|
|
@@ -2090,7 +3291,8 @@ function writePrivateEvidence(path, contents) {
|
|
|
2090
3291
|
(0, node_fs_1.writeFileSync)(path, contents, { encoding: 'utf8', mode: 0o600 });
|
|
2091
3292
|
(0, node_fs_1.chmodSync)(path, 0o600);
|
|
2092
3293
|
}
|
|
2093
|
-
|
|
3294
|
+
/** @internal */
|
|
3295
|
+
function actionRisk(risk, action, options = {}) {
|
|
2094
3296
|
if (action === 'deploy')
|
|
2095
3297
|
return risk;
|
|
2096
3298
|
const code = action === 'refresh'
|
|
@@ -2107,17 +3309,27 @@ function actionRisk(risk, action) {
|
|
|
2107
3309
|
else {
|
|
2108
3310
|
requiredApprovals.add('data-owner-and-recovery');
|
|
2109
3311
|
}
|
|
3312
|
+
const findings = action === 'refresh'
|
|
3313
|
+
? risk.findings.map((finding) => finding.disposition === 'block'
|
|
3314
|
+
? {
|
|
3315
|
+
...finding,
|
|
3316
|
+
disposition: 'review',
|
|
3317
|
+
message: 'Provider refresh records observed cloud state without mutating the cloud; the exact drift decision must review this finding and a later desired-state plan must still converge. ' +
|
|
3318
|
+
finding.message,
|
|
3319
|
+
}
|
|
3320
|
+
: finding)
|
|
3321
|
+
: risk.findings;
|
|
2110
3322
|
return {
|
|
2111
|
-
aggregate:
|
|
2112
|
-
'unknown'
|
|
2113
|
-
'
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
3323
|
+
aggregate: options.diffComplete === false
|
|
3324
|
+
? 'unknown'
|
|
3325
|
+
: action === 'refresh'
|
|
3326
|
+
? 'stateful-or-destructive'
|
|
3327
|
+
: ['unknown', 'protection-reduction', 'security-expansion'].includes(risk.aggregate)
|
|
3328
|
+
? risk.aggregate
|
|
3329
|
+
: 'stateful-or-destructive',
|
|
2118
3330
|
applyAllowed: false,
|
|
2119
3331
|
findings: [
|
|
2120
|
-
...
|
|
3332
|
+
...findings,
|
|
2121
3333
|
{
|
|
2122
3334
|
class: 'stateful-or-destructive',
|
|
2123
3335
|
code,
|
|
@@ -2128,17 +3340,62 @@ function actionRisk(risk, action) {
|
|
|
2128
3340
|
requiredApprovals: [...requiredApprovals].sort(),
|
|
2129
3341
|
};
|
|
2130
3342
|
}
|
|
2131
|
-
function validateGovernanceEvidence(reviewed, plan, observedResources = [], importIdentifierDigests = {}) {
|
|
3343
|
+
function validateGovernanceEvidence(reviewed, plan, observedResources = [], importIdentifierDigests = {}, adoptedImportPolicy = []) {
|
|
2132
3344
|
const imports = plan.diff.resources.filter(({ operation }) => operation === 'import');
|
|
2133
|
-
const
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
3345
|
+
const adoptedByResource = new Map(adoptedImportPolicy.map((entry) => [
|
|
3346
|
+
`${entry.logicalId}\u0000${entry.resourceType}`,
|
|
3347
|
+
entry,
|
|
3348
|
+
]));
|
|
3349
|
+
const governedImports = imports.filter((resource) => !adoptedByResource.has(infrastructureResourceKey(resource)));
|
|
3350
|
+
const replacements = governedReplacementResources(plan);
|
|
3351
|
+
const digests = {};
|
|
3352
|
+
if (imports.length > 0) {
|
|
3353
|
+
if (Object.keys(importIdentifierDigests).length !== imports.length) {
|
|
3354
|
+
throw new Error('Infrastructure import private identity is ambiguous.');
|
|
3355
|
+
}
|
|
3356
|
+
const adoptedResources = imports.flatMap((resource) => {
|
|
3357
|
+
const key = infrastructureResourceKey(resource);
|
|
3358
|
+
const adopted = adoptedByResource.get(key);
|
|
3359
|
+
if (!adopted)
|
|
3360
|
+
return [];
|
|
3361
|
+
const expectedIdentifierDigest = digest(adopted.privateIdentifierTemplate.replace('{stage}', plan.target.stage));
|
|
3362
|
+
if (importIdentifierDigests[key] !== expectedIdentifierDigest) {
|
|
3363
|
+
throw new Error(`Adopted infrastructure import ${resource.logicalId} does not match its exact stage-bound private identity.`);
|
|
3364
|
+
}
|
|
3365
|
+
return [
|
|
3366
|
+
{
|
|
3367
|
+
authority: adopted.authority,
|
|
3368
|
+
logicalId: adopted.logicalId,
|
|
3369
|
+
privateIdentifierDigest: expectedIdentifierDigest,
|
|
3370
|
+
recoveryRunbookRef: adopted.recoveryRunbookRef,
|
|
3371
|
+
removalBehavior: adopted.removalBehavior,
|
|
3372
|
+
resourceType: adopted.resourceType,
|
|
3373
|
+
},
|
|
3374
|
+
];
|
|
3375
|
+
});
|
|
3376
|
+
if (adoptedResources.length > 0) {
|
|
3377
|
+
digests['adopted-import'] = digest(canonicalJson({
|
|
3378
|
+
kind: 'ebk-adopted-infrastructure-import-authority',
|
|
3379
|
+
materialDigest: plan.materialDigest,
|
|
3380
|
+
project: plan.projects[0],
|
|
3381
|
+
resources: adoptedResources,
|
|
3382
|
+
schemaVersion: 1,
|
|
3383
|
+
sourceCommit: plan.sourceCommit,
|
|
3384
|
+
stage: plan.target.stage,
|
|
3385
|
+
stateDigest: plan.state.digest,
|
|
3386
|
+
}));
|
|
3387
|
+
}
|
|
3388
|
+
}
|
|
3389
|
+
const requiresGovernance = plan.action !== 'deploy' ||
|
|
3390
|
+
governedImports.length > 0 ||
|
|
3391
|
+
replacements.length > 0;
|
|
3392
|
+
if (!requiresGovernance) {
|
|
3393
|
+
return Object.fromEntries(Object.entries(digests).sort(([left], [right]) => (0, deterministic_order_js_1.compareUtf16)(left, right)));
|
|
3394
|
+
}
|
|
2137
3395
|
if (!reviewed) {
|
|
2138
3396
|
throw new Error('Governed infrastructure mutation evidence is absent from the exact approval record.');
|
|
2139
3397
|
}
|
|
2140
3398
|
const governance = record(reviewed.governance, 'Infrastructure approval governance evidence');
|
|
2141
|
-
const digests = {};
|
|
2142
3399
|
const common = (value, kind, label, extraKeys) => {
|
|
2143
3400
|
requireExactKeys(value, [
|
|
2144
3401
|
'kind',
|
|
@@ -2246,11 +3503,12 @@ function validateGovernanceEvidence(reviewed, plan, observedResources = [], impo
|
|
|
2246
3503
|
}
|
|
2247
3504
|
digests.removal = digest(canonicalJson(removal));
|
|
2248
3505
|
}
|
|
2249
|
-
if (
|
|
3506
|
+
if (governedImports.length > 0) {
|
|
2250
3507
|
const importEvidence = record(governance['import'], 'Infrastructure import governance');
|
|
2251
3508
|
common(importEvidence, 'ebk-infrastructure-import-governance', 'Infrastructure import governance', ['resources']);
|
|
2252
3509
|
const resources = importEvidence['resources'];
|
|
2253
|
-
if (!Array.isArray(resources) ||
|
|
3510
|
+
if (!Array.isArray(resources) ||
|
|
3511
|
+
resources.length !== governedImports.length) {
|
|
2254
3512
|
throw new Error('Infrastructure import governance must cover every exact imported resource.');
|
|
2255
3513
|
}
|
|
2256
3514
|
const byResource = new Map(resources.map((entry, index) => {
|
|
@@ -2269,7 +3527,7 @@ function validateGovernanceEvidence(reviewed, plan, observedResources = [], impo
|
|
|
2269
3527
|
value,
|
|
2270
3528
|
];
|
|
2271
3529
|
}));
|
|
2272
|
-
for (const resource of
|
|
3530
|
+
for (const resource of governedImports) {
|
|
2273
3531
|
const key = infrastructureResourceKey(resource);
|
|
2274
3532
|
const evidence = byResource.get(key);
|
|
2275
3533
|
if (!evidence ||
|
|
@@ -2282,20 +3540,20 @@ function validateGovernanceEvidence(reviewed, plan, observedResources = [], impo
|
|
|
2282
3540
|
throw new Error(`Infrastructure import ${resource.logicalId} lacks exact source-bound private identity, ownership, controller, and tested removal evidence.`);
|
|
2283
3541
|
}
|
|
2284
3542
|
}
|
|
2285
|
-
if (byResource.size !==
|
|
2286
|
-
Object.keys(importIdentifierDigests).length !== imports.length) {
|
|
3543
|
+
if (byResource.size !== governedImports.length) {
|
|
2287
3544
|
throw new Error('Infrastructure import governance is ambiguous.');
|
|
2288
3545
|
}
|
|
2289
3546
|
digests.import = digest(canonicalJson(importEvidence));
|
|
2290
3547
|
}
|
|
2291
3548
|
if (replacements.length > 0) {
|
|
3549
|
+
const activeOperatorAcknowledgement = reviewed?.approval?.mode === 'operator-acknowledgement';
|
|
2292
3550
|
const replacementEvidence = record(governance['replacement'], 'Infrastructure replacement governance');
|
|
2293
3551
|
common(replacementEvidence, 'ebk-infrastructure-replacement-governance', 'Infrastructure replacement governance', ['resources']);
|
|
2294
3552
|
const resources = replacementEvidence['resources'];
|
|
2295
3553
|
if (!Array.isArray(resources) || resources.length !== replacements.length) {
|
|
2296
3554
|
throw new Error('Infrastructure replacement governance must cover every exact replacement.');
|
|
2297
3555
|
}
|
|
2298
|
-
const
|
|
3556
|
+
const byResource = new Map(resources.map((entry, index) => {
|
|
2299
3557
|
const value = record(entry, `Infrastructure replacement resource ${index}`);
|
|
2300
3558
|
requireExactKeys(value, [
|
|
2301
3559
|
'dataCopyEvidenceRef',
|
|
@@ -2307,14 +3565,20 @@ function validateGovernanceEvidence(reviewed, plan, observedResources = [], impo
|
|
|
2307
3565
|
'rollbackLimit',
|
|
2308
3566
|
'strategy',
|
|
2309
3567
|
], `Infrastructure replacement resource ${index}`);
|
|
2310
|
-
return [
|
|
3568
|
+
return [
|
|
3569
|
+
`${value['logicalId']}\u0000${value['resourceType']}`,
|
|
3570
|
+
value,
|
|
3571
|
+
];
|
|
2311
3572
|
}));
|
|
2312
3573
|
for (const resource of replacements) {
|
|
2313
|
-
const evidence =
|
|
3574
|
+
const evidence = byResource.get(`${resource.logicalId}\u0000${resource.resourceType}`);
|
|
2314
3575
|
if (!evidence ||
|
|
2315
3576
|
evidence['resourceType'] !== resource.resourceType ||
|
|
2316
3577
|
evidence['strategy'] !== resource.replacementStrategy ||
|
|
2317
|
-
evidence['nameCollisionCheck'] !==
|
|
3578
|
+
evidence['nameCollisionCheck'] !==
|
|
3579
|
+
(activeOperatorAcknowledgement
|
|
3580
|
+
? 'not-independently-verified'
|
|
3581
|
+
: 'passed') ||
|
|
2318
3582
|
![
|
|
2319
3583
|
'dataCopyEvidenceRef',
|
|
2320
3584
|
'dependencyCutoverRef',
|
|
@@ -2322,16 +3586,32 @@ function validateGovernanceEvidence(reviewed, plan, observedResources = [], impo
|
|
|
2322
3586
|
'rollbackLimit',
|
|
2323
3587
|
].every((key) => typeof evidence[key] === 'string' &&
|
|
2324
3588
|
evidence[key].trim().length > 0)) {
|
|
2325
|
-
throw new Error(`Infrastructure replacement ${resource.logicalId} lacks exact cutover, data, rollback, collision, or recovery evidence.`);
|
|
3589
|
+
throw new Error(`Infrastructure replacement ${resource.logicalId} lacks exact cutover, data, rollback, honestly classified collision review, or recovery evidence.`);
|
|
2326
3590
|
}
|
|
2327
3591
|
}
|
|
2328
|
-
if (
|
|
3592
|
+
if (byResource.size !== replacements.length) {
|
|
2329
3593
|
throw new Error('Infrastructure replacement governance is ambiguous.');
|
|
2330
3594
|
}
|
|
2331
3595
|
digests.replacement = digest(canonicalJson(replacementEvidence));
|
|
2332
3596
|
}
|
|
2333
3597
|
return Object.fromEntries(Object.entries(digests).sort(([left], [right]) => (0, deterministic_order_js_1.compareUtf16)(left, right)));
|
|
2334
3598
|
}
|
|
3599
|
+
function governedReplacementResources(plan) {
|
|
3600
|
+
const replacements = new Map();
|
|
3601
|
+
for (const resource of plan.diff.resources) {
|
|
3602
|
+
if (resource.operation !== 'replace' && !resource.replacement)
|
|
3603
|
+
continue;
|
|
3604
|
+
const key = `${resource.logicalId}\u0000${resource.resourceType}`;
|
|
3605
|
+
const retained = replacements.get(key);
|
|
3606
|
+
if (retained &&
|
|
3607
|
+
retained.replacementStrategy !== resource.replacementStrategy) {
|
|
3608
|
+
throw new Error(`Infrastructure replacement ${resource.logicalId} has ambiguous provider strategies.`);
|
|
3609
|
+
}
|
|
3610
|
+
replacements.set(key, retained ?? resource);
|
|
3611
|
+
}
|
|
3612
|
+
return [...replacements.values()].sort((left, right) => (0, deterministic_order_js_1.compareUtf16)(left.logicalId, right.logicalId) ||
|
|
3613
|
+
(0, deterministic_order_js_1.compareUtf16)(left.resourceType, right.resourceType));
|
|
3614
|
+
}
|
|
2335
3615
|
function computeCurrentInfrastructurePlan(input) {
|
|
2336
3616
|
const { action, context, dependencies, environment, invocation, options } = input;
|
|
2337
3617
|
const project = context.projectName;
|
|
@@ -2357,17 +3637,66 @@ function computeCurrentInfrastructurePlan(input) {
|
|
|
2357
3637
|
}));
|
|
2358
3638
|
const spawn = dependencies.spawn ?? defaultSpawn;
|
|
2359
3639
|
const sourceCommit = assertCheckedOutSourceCommit(dependencies.spawn, context.root, environment);
|
|
2360
|
-
const repository = requiredEnvironment(environment, 'GITHUB_REPOSITORY');
|
|
2361
3640
|
const candidateSourceCommit = input.candidateSourceCommit ?? sourceCommit;
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
sourceCommit
|
|
2366
|
-
|
|
2367
|
-
|
|
3641
|
+
let materialEnvironment = environment;
|
|
3642
|
+
let materialSourceTree;
|
|
3643
|
+
if (environment.EBK_LOCAL_DEPLOYMENT_MODE === 'federated' &&
|
|
3644
|
+
candidateSourceCommit !== sourceCommit) {
|
|
3645
|
+
try {
|
|
3646
|
+
(0, source_authority_js_1.validateReviewedReleaseCheckout)({
|
|
3647
|
+
deploymentSourceCommit: sourceCommit,
|
|
3648
|
+
releaseSourceCommit: candidateSourceCommit,
|
|
3649
|
+
repositoryRoot: context.root,
|
|
3650
|
+
reviewedSourceCommit: sourceCommit,
|
|
3651
|
+
});
|
|
3652
|
+
}
|
|
3653
|
+
catch {
|
|
3654
|
+
throw new Error('Local deployment cannot substitute a different release source commit.');
|
|
3655
|
+
}
|
|
3656
|
+
const releaseTree = spawn('git', ['rev-parse', `${candidateSourceCommit}^{tree}`], {
|
|
3657
|
+
cwd: context.root,
|
|
3658
|
+
encoding: 'utf8',
|
|
3659
|
+
env: environment,
|
|
3660
|
+
maxBuffer: MAX_JSON_BYTES,
|
|
3661
|
+
shell: false,
|
|
3662
|
+
});
|
|
3663
|
+
materialSourceTree = releaseTree.stdout.trim();
|
|
3664
|
+
if (releaseTree.status !== 0 ||
|
|
3665
|
+
releaseTree.stderr !== '' ||
|
|
3666
|
+
!SOURCE_COMMIT.test(materialSourceTree)) {
|
|
3667
|
+
throw new Error('Local deployment cannot resolve the exact reviewed release source tree.');
|
|
3668
|
+
}
|
|
3669
|
+
materialEnvironment = {
|
|
3670
|
+
...environment,
|
|
3671
|
+
EBK_LOCAL_SOURCE_TREE: materialSourceTree,
|
|
3672
|
+
};
|
|
3673
|
+
}
|
|
3674
|
+
const repository = environment.EBK_LOCAL_DEPLOYMENT_MODE === 'federated'
|
|
3675
|
+
? `local-source/${requiredEnvironment(materialEnvironment, 'EBK_LOCAL_SOURCE_TREE', /^[a-f0-9]{40}$/)}`
|
|
3676
|
+
: requiredEnvironment(environment, 'GITHUB_REPOSITORY');
|
|
3677
|
+
const candidate = environment.EBK_LOCAL_DEPLOYMENT_MODE === 'federated'
|
|
3678
|
+
? localSourceCandidateBinding({
|
|
3679
|
+
context,
|
|
3680
|
+
environment,
|
|
3681
|
+
...(materialSourceTree === undefined ? {} : { materialSourceTree }),
|
|
3682
|
+
project,
|
|
3683
|
+
sourceCommit: candidateSourceCommit,
|
|
3684
|
+
spawn,
|
|
3685
|
+
})
|
|
3686
|
+
: releaseCandidateBinding({
|
|
3687
|
+
project,
|
|
3688
|
+
repository,
|
|
3689
|
+
sourceCommit: candidateSourceCommit,
|
|
3690
|
+
workspaceRoot: context.root,
|
|
3691
|
+
});
|
|
2368
3692
|
const childEnvironment = {
|
|
2369
3693
|
...createCandidateBoundSstChildEnvironment(environment, invocation.environment, candidateSourceCommit),
|
|
2370
|
-
|
|
3694
|
+
// Workload-role publication is one stage-wide boundary spanning the
|
|
3695
|
+
// Foundation and every Slice. The reviewed source commit is the only
|
|
3696
|
+
// candidate identity common to that complete project set; the
|
|
3697
|
+
// project-specific sealed candidate digest remains independently bound in
|
|
3698
|
+
// the material infrastructure plan below.
|
|
3699
|
+
EBK_CANDIDATE_IDENTITY: candidateSourceCommit,
|
|
2371
3700
|
};
|
|
2372
3701
|
let backendBootstrapDigest = digest('dependency-supplied-observer');
|
|
2373
3702
|
if (dependencies.observe === undefined) {
|
|
@@ -2377,7 +3706,7 @@ function computeCurrentInfrastructurePlan(input) {
|
|
|
2377
3706
|
}
|
|
2378
3707
|
const stage = requiredEnvironment(childEnvironment, 'EBK_CONSUMER_STAGE');
|
|
2379
3708
|
backendBootstrapDigest = preflightSstBootstrap({
|
|
2380
|
-
accountId:
|
|
3709
|
+
accountId: expectedDeploymentAccount(environment, invocation),
|
|
2381
3710
|
childEnvironment,
|
|
2382
3711
|
invocation,
|
|
2383
3712
|
region: requiredEnvironment(childEnvironment, 'AWS_REGION'),
|
|
@@ -2415,29 +3744,53 @@ function computeCurrentInfrastructurePlan(input) {
|
|
|
2415
3744
|
cwd: invocation.cwd,
|
|
2416
3745
|
encoding: 'utf8',
|
|
2417
3746
|
env: childEnvironment,
|
|
2418
|
-
maxBuffer:
|
|
3747
|
+
maxBuffer: exports.MAX_SST_DIFF_JSON_BYTES,
|
|
2419
3748
|
shell: false,
|
|
2420
3749
|
});
|
|
2421
|
-
const diff = (
|
|
2422
|
-
|
|
3750
|
+
const diff = normalizeDesiredSstDiffV4_17_1({
|
|
3751
|
+
errorCode: typeof diffResult.error?.code === 'string' ? diffResult.error.code : null,
|
|
3752
|
+
exitCode: diffResult.status ?? -1,
|
|
3753
|
+
signal: typeof diffResult.signal === 'string' ? diffResult.signal : null,
|
|
3754
|
+
spawnError: diffResult.error != null,
|
|
3755
|
+
stderr: diffResult.stderr,
|
|
2423
3756
|
stdout: diffResult.stdout,
|
|
2424
3757
|
toolVersion: version,
|
|
2425
3758
|
});
|
|
2426
|
-
const
|
|
3759
|
+
const now = (dependencies.now ?? (() => new Date()))();
|
|
3760
|
+
const observation = (dependencies.observe ?? observeInfrastructureState)({
|
|
3761
|
+
action,
|
|
3762
|
+
backendBootstrapDigest,
|
|
3763
|
+
childEnvironment,
|
|
3764
|
+
environment,
|
|
3765
|
+
invocation,
|
|
3766
|
+
now,
|
|
3767
|
+
ordinaryDiff: diff,
|
|
3768
|
+
project,
|
|
3769
|
+
sourceCommit: candidateSourceCommit,
|
|
3770
|
+
spawn,
|
|
3771
|
+
toolVersion: version,
|
|
3772
|
+
});
|
|
3773
|
+
const refreshMutationPlanMaterial = action === 'refresh'
|
|
3774
|
+
? refreshMutationPlanMaterialByObservation.get(observation)
|
|
3775
|
+
: undefined;
|
|
3776
|
+
const plannedDiff = refreshMutationPlanMaterial?.diff ?? diff;
|
|
3777
|
+
const plannedDiffStderr = refreshMutationPlanMaterial?.stderr ?? diffResult.stderr;
|
|
3778
|
+
const plannedDiffStdout = refreshMutationPlanMaterial?.stdout ?? diffResult.stdout;
|
|
3779
|
+
const importIdentifierDigests = extractPrivateImportIdentifierDigests(plannedDiffStdout, plannedDiff);
|
|
2427
3780
|
const iamValidations = iamPolicyDocuments({
|
|
2428
|
-
accountId:
|
|
3781
|
+
accountId: expectedDeploymentAccount(environment, invocation),
|
|
2429
3782
|
childEnvironment,
|
|
2430
|
-
diff,
|
|
2431
|
-
diffStdout:
|
|
3783
|
+
diff: plannedDiff,
|
|
3784
|
+
diffStdout: plannedDiffStdout,
|
|
2432
3785
|
invocation,
|
|
2433
3786
|
region: requiredEnvironment(childEnvironment, 'AWS_REGION'),
|
|
2434
3787
|
sourceCommit: candidateSourceCommit,
|
|
2435
3788
|
spawn,
|
|
2436
3789
|
});
|
|
2437
3790
|
const boundedCostQuotaEvidence = costQuotaEvidence({
|
|
2438
|
-
accountId:
|
|
3791
|
+
accountId: expectedDeploymentAccount(environment, invocation),
|
|
2439
3792
|
childEnvironment,
|
|
2440
|
-
diff,
|
|
3793
|
+
diff: plannedDiff,
|
|
2441
3794
|
invocation,
|
|
2442
3795
|
policy: descriptor.policy,
|
|
2443
3796
|
project,
|
|
@@ -2446,12 +3799,7 @@ function computeCurrentInfrastructurePlan(input) {
|
|
|
2446
3799
|
spawn,
|
|
2447
3800
|
});
|
|
2448
3801
|
const policySignals = readOptionalArray(environment, 'EBK_INFRA_POLICY_SIGNALS_PATH');
|
|
2449
|
-
const policyBearingIamResources =
|
|
2450
|
-
[
|
|
2451
|
-
'aws:iam/policy:Policy',
|
|
2452
|
-
'aws:iam/role:Role',
|
|
2453
|
-
'aws:iam/rolePolicy:RolePolicy',
|
|
2454
|
-
].includes(resourceType));
|
|
3802
|
+
const policyBearingIamResources = plannedDiff.resources.filter((resource) => requiresIamMutationAnalysis(action, resource));
|
|
2455
3803
|
if (policyBearingIamResources.some(({ logicalId }) => !iamValidations.some((validation) => validation.logicalId === logicalId &&
|
|
2456
3804
|
validation.analyzer === 'aws-iam-access-analyzer-validate-policy' &&
|
|
2457
3805
|
validation.status === 'complete'))) {
|
|
@@ -2462,45 +3810,35 @@ function computeCurrentInfrastructurePlan(input) {
|
|
|
2462
3810
|
});
|
|
2463
3811
|
}
|
|
2464
3812
|
const risk = actionRisk((0, index_js_1.classifyInfrastructureRisk)({
|
|
2465
|
-
diff,
|
|
3813
|
+
diff: plannedDiff,
|
|
2466
3814
|
iamValidations,
|
|
2467
3815
|
policy: descriptor.policy,
|
|
2468
3816
|
policySignals,
|
|
2469
|
-
}), action);
|
|
2470
|
-
const now = (dependencies.now ?? (() => new Date()))();
|
|
2471
|
-
const observation = (dependencies.observe ?? observeInfrastructureState)({
|
|
2472
|
-
backendBootstrapDigest,
|
|
2473
|
-
childEnvironment,
|
|
2474
|
-
environment,
|
|
2475
|
-
invocation,
|
|
2476
|
-
now,
|
|
2477
|
-
ordinaryDiff: diff,
|
|
2478
|
-
project,
|
|
2479
|
-
sourceCommit,
|
|
2480
|
-
spawn,
|
|
2481
|
-
toolVersion: version,
|
|
2482
|
-
});
|
|
3817
|
+
}), action, { diffComplete: plannedDiff.complete });
|
|
2483
3818
|
const drift = observation.drift;
|
|
2484
3819
|
const state = observation.state;
|
|
2485
3820
|
const expiresAt = new Date(Math.min(now.getTime() + 60 * 60_000, Date.parse(drift.expiresAt), Date.parse(state.expiresAt)));
|
|
2486
|
-
const account =
|
|
3821
|
+
const account = expectedDeploymentAccount(environment, invocation);
|
|
2487
3822
|
const framework = frameworkVersion(context.root);
|
|
2488
|
-
const planAuthorityDigest = planAuthorityPolicyDigest(
|
|
3823
|
+
const planAuthorityDigest = planAuthorityPolicyDigest(materialEnvironment, invocation);
|
|
3824
|
+
const buildArtifacts = collectInfrastructureBuildArtifacts(root);
|
|
2489
3825
|
return {
|
|
3826
|
+
buildArtifacts,
|
|
2490
3827
|
costQuotaEvidence: boundedCostQuotaEvidence,
|
|
2491
3828
|
descriptor,
|
|
2492
|
-
diffStderr:
|
|
2493
|
-
diffStdout:
|
|
3829
|
+
diffStderr: plannedDiffStderr,
|
|
3830
|
+
diffStdout: plannedDiffStdout,
|
|
2494
3831
|
iamValidations,
|
|
2495
3832
|
importIdentifierDigests,
|
|
2496
3833
|
observation,
|
|
2497
3834
|
plan: (0, index_js_1.createInfrastructurePlan)({
|
|
2498
3835
|
action,
|
|
2499
|
-
configurationMetadataDigest:
|
|
3836
|
+
configurationMetadataDigest: infrastructureConfigurationMetadataDigest({
|
|
3837
|
+
buildArtifactDigest: buildArtifacts.digest,
|
|
2500
3838
|
configurationInputs,
|
|
2501
3839
|
releaseCandidateDigest: candidate.digest,
|
|
2502
|
-
})
|
|
2503
|
-
diff,
|
|
3840
|
+
}),
|
|
3841
|
+
diff: plannedDiff,
|
|
2504
3842
|
driftEvidenceDigest: drift.digest,
|
|
2505
3843
|
driftEvidenceExpiresAt: drift.expiresAt,
|
|
2506
3844
|
driftEvidenceGeneratedAt: drift.generatedAt,
|
|
@@ -2546,6 +3884,591 @@ function computeCurrentInfrastructurePlan(input) {
|
|
|
2546
3884
|
toolVersion: version,
|
|
2547
3885
|
};
|
|
2548
3886
|
}
|
|
3887
|
+
/** @internal */
|
|
3888
|
+
function createLocalPlanApprovalRecord(input) {
|
|
3889
|
+
const expectedMaterialDigest = input.environment['EBK_LOCAL_PLAN_MATERIAL_DIGEST']?.trim();
|
|
3890
|
+
if (!expectedMaterialDigest)
|
|
3891
|
+
return undefined;
|
|
3892
|
+
const summaryDigest = input.environment['EBK_LOCAL_PLAN_SUMMARY_DIGEST']?.trim() ?? '';
|
|
3893
|
+
const approverArn = input.environment['EBK_LOCAL_PLAN_APPROVER_ARN']?.trim() ?? '';
|
|
3894
|
+
const sourceCommit = input.environment['EBK_SOURCE_COMMIT']?.trim() ?? '';
|
|
3895
|
+
const policy = (0, index_js_2.readRepositoryOperatingPolicy)(input.context.root);
|
|
3896
|
+
if (input.environment['GITHUB_ACTIONS'] === 'true' ||
|
|
3897
|
+
input.environment['EBK_LOCAL_DEPLOYMENT_MODE'] !== 'federated' ||
|
|
3898
|
+
(input.plan.action !== 'deploy' && input.plan.action !== 'refresh') ||
|
|
3899
|
+
!isCanonicalDeploymentStage(input.plan.target.stage) ||
|
|
3900
|
+
(0, index_js_2.requiresGithubDeployment)(policy.deploymentProfile, input.plan.target.stage) ||
|
|
3901
|
+
expectedMaterialDigest !== input.plan.materialDigest ||
|
|
3902
|
+
!SHA256.test(summaryDigest) ||
|
|
3903
|
+
sourceCommit !== input.plan.sourceCommit ||
|
|
3904
|
+
!/^arn:(aws|aws-us-gov|aws-cn):sts::\d{12}:assumed-role\/.+\/.+$/.test(approverArn) ||
|
|
3905
|
+
input.plan.requiredApprovals.length === 0 ||
|
|
3906
|
+
hasUnreviewableLocalPlanBlock(input.plan)) {
|
|
3907
|
+
throw new Error('Local infrastructure plan approval does not match the exact federated actor, source, stage, and reviewed plan.');
|
|
3908
|
+
}
|
|
3909
|
+
const evidenceRef = `local-plan-summary:${summaryDigest}`;
|
|
3910
|
+
return {
|
|
3911
|
+
...(input.plan.action === 'refresh' &&
|
|
3912
|
+
input.plan.requiredApprovals.includes('point-of-no-return')
|
|
3913
|
+
? { acknowledgement: `refresh-state-adoption:${summaryDigest}` }
|
|
3914
|
+
: {}),
|
|
3915
|
+
approvedAt: input.now.toISOString(),
|
|
3916
|
+
approver: `local-federated:${digest(approverArn)}`,
|
|
3917
|
+
author: 'ebk-infrastructure-plan',
|
|
3918
|
+
codeownerReviewed: false,
|
|
3919
|
+
evidenceRefs: Object.fromEntries(input.plan.requiredApprovals.map((requirement) => [
|
|
3920
|
+
requirement,
|
|
3921
|
+
evidenceRef,
|
|
3922
|
+
])),
|
|
3923
|
+
expiresAt: input.plan.expiresAt,
|
|
3924
|
+
materialDigest: input.plan.materialDigest,
|
|
3925
|
+
mode: 'operator-acknowledgement',
|
|
3926
|
+
stage: input.plan.target.stage,
|
|
3927
|
+
};
|
|
3928
|
+
}
|
|
3929
|
+
/** @internal */
|
|
3930
|
+
function hasUnreviewableLocalPlanBlock(plan) {
|
|
3931
|
+
const blocks = plan.resourceFindings.filter(({ disposition }) => disposition === 'block');
|
|
3932
|
+
if (blocks.length === 0)
|
|
3933
|
+
return false;
|
|
3934
|
+
return !(plan.action === 'refresh' &&
|
|
3935
|
+
blocks.length === 1 &&
|
|
3936
|
+
blocks[0]?.class === 'stateful-or-destructive' &&
|
|
3937
|
+
blocks[0]?.code === 'protected-state-refresh' &&
|
|
3938
|
+
blocks[0]?.resourceId === undefined);
|
|
3939
|
+
}
|
|
3940
|
+
/** @internal */
|
|
3941
|
+
function createLocalRefreshGovernanceReview(input) {
|
|
3942
|
+
if (input.plan.action !== 'refresh')
|
|
3943
|
+
return undefined;
|
|
3944
|
+
const summaryDigest = input.environment['EBK_LOCAL_PLAN_SUMMARY_DIGEST']?.trim() ?? '';
|
|
3945
|
+
const evidenceRef = `local-plan-summary:${summaryDigest}`;
|
|
3946
|
+
const project = input.plan.projects[0];
|
|
3947
|
+
if (!SHA256.test(summaryDigest) ||
|
|
3948
|
+
input.plan.projects.length !== 1 ||
|
|
3949
|
+
!project ||
|
|
3950
|
+
input.approval.materialDigest !== input.plan.materialDigest ||
|
|
3951
|
+
input.approval.mode !== 'operator-acknowledgement' ||
|
|
3952
|
+
input.approval.evidenceRefs['deployment-review'] !== evidenceRef ||
|
|
3953
|
+
!SHA256.test(input.plan.driftEvidenceDigest) ||
|
|
3954
|
+
!SHA256.test(input.plan.state.digest)) {
|
|
3955
|
+
throw new Error('Local refresh governance does not match the exact reviewed plan summary.');
|
|
3956
|
+
}
|
|
3957
|
+
return {
|
|
3958
|
+
approval: input.approval,
|
|
3959
|
+
governance: {
|
|
3960
|
+
driftDecision: {
|
|
3961
|
+
actor: input.approval.approver,
|
|
3962
|
+
approvalRef: evidenceRef,
|
|
3963
|
+
decision: 'adopt',
|
|
3964
|
+
driftEvidenceDigest: input.plan.driftEvidenceDigest,
|
|
3965
|
+
kind: 'ebk-infrastructure-drift-decision',
|
|
3966
|
+
materialDigest: input.plan.materialDigest,
|
|
3967
|
+
project,
|
|
3968
|
+
reviewedSourceChangeRef: input.plan.sourceCommit,
|
|
3969
|
+
runbookRef: 'docs/repository-operating-policy.md',
|
|
3970
|
+
schemaVersion: 1,
|
|
3971
|
+
sourceCommit: input.plan.sourceCommit,
|
|
3972
|
+
stage: input.plan.target.stage,
|
|
3973
|
+
stateDigest: input.plan.state.digest,
|
|
3974
|
+
},
|
|
3975
|
+
},
|
|
3976
|
+
planned: input.plan,
|
|
3977
|
+
};
|
|
3978
|
+
}
|
|
3979
|
+
/** @internal */
|
|
3980
|
+
function createLocalDeployGovernanceReview(input) {
|
|
3981
|
+
if (input.plan.action !== 'deploy')
|
|
3982
|
+
return undefined;
|
|
3983
|
+
const replacements = governedReplacementResources(input.plan);
|
|
3984
|
+
if (replacements.length === 0)
|
|
3985
|
+
return undefined;
|
|
3986
|
+
const summaryDigest = input.environment['EBK_LOCAL_PLAN_SUMMARY_DIGEST']?.trim() ?? '';
|
|
3987
|
+
const evidenceRef = `local-plan-summary:${summaryDigest}`;
|
|
3988
|
+
const project = input.plan.projects[0];
|
|
3989
|
+
if (!SHA256.test(summaryDigest) ||
|
|
3990
|
+
input.plan.projects.length !== 1 ||
|
|
3991
|
+
!project ||
|
|
3992
|
+
input.approval.materialDigest !== input.plan.materialDigest ||
|
|
3993
|
+
input.approval.mode !== 'operator-acknowledgement' ||
|
|
3994
|
+
input.approval.evidenceRefs['deployment-review'] !== evidenceRef ||
|
|
3995
|
+
Object.values(input.approval.evidenceRefs).some((reference) => reference !== evidenceRef) ||
|
|
3996
|
+
replacements.some(({ replacementStrategy }) => replacementStrategy !== 'create-before-delete') ||
|
|
3997
|
+
!SHA256.test(input.plan.state.digest)) {
|
|
3998
|
+
throw new Error('Local deploy governance does not match the exact reviewed plan summary and create-before-delete inventory.');
|
|
3999
|
+
}
|
|
4000
|
+
return {
|
|
4001
|
+
approval: input.approval,
|
|
4002
|
+
governance: {
|
|
4003
|
+
replacement: {
|
|
4004
|
+
kind: 'ebk-infrastructure-replacement-governance',
|
|
4005
|
+
materialDigest: input.plan.materialDigest,
|
|
4006
|
+
project,
|
|
4007
|
+
resources: replacements.map((resource) => ({
|
|
4008
|
+
dataCopyEvidenceRef: evidenceRef,
|
|
4009
|
+
dependencyCutoverRef: evidenceRef,
|
|
4010
|
+
logicalId: resource.logicalId,
|
|
4011
|
+
nameCollisionCheck: 'not-independently-verified',
|
|
4012
|
+
recoveryRunbookRef: 'docs/repository-operating-policy.md',
|
|
4013
|
+
resourceType: resource.resourceType,
|
|
4014
|
+
rollbackLimit: 'before-dependent-cutover',
|
|
4015
|
+
strategy: resource.replacementStrategy,
|
|
4016
|
+
})),
|
|
4017
|
+
schemaVersion: 1,
|
|
4018
|
+
sourceCommit: input.plan.sourceCommit,
|
|
4019
|
+
stage: input.plan.target.stage,
|
|
4020
|
+
stateDigest: input.plan.state.digest,
|
|
4021
|
+
},
|
|
4022
|
+
},
|
|
4023
|
+
planned: input.plan,
|
|
4024
|
+
};
|
|
4025
|
+
}
|
|
4026
|
+
/**
|
|
4027
|
+
* Retains exact local approval for a plan that the stage-wide local executor
|
|
4028
|
+
* has just recomputed and reviewed. This deliberately does not run SST again:
|
|
4029
|
+
* the project mutation executor consumes one invocation-scoped capability and
|
|
4030
|
+
* rechecks source, artifacts, credentials, policy, state identity, and lock
|
|
4031
|
+
* immediately before spawn without claiming provider-preview replay.
|
|
4032
|
+
*
|
|
4033
|
+
* @internal
|
|
4034
|
+
*/
|
|
4035
|
+
function authorizeRetainedLocalInfrastructurePlan(input) {
|
|
4036
|
+
if (input.plan.diff.resources.some(({ operation }) => operation === 'import')) {
|
|
4037
|
+
throw new Error('Local retained-plan authorization cannot reconstruct private import identity evidence.');
|
|
4038
|
+
}
|
|
4039
|
+
const approval = createLocalPlanApprovalRecord(input) ??
|
|
4040
|
+
(input.plan.requiredApprovals.length === 0
|
|
4041
|
+
? {
|
|
4042
|
+
approvedAt: input.now.toISOString(),
|
|
4043
|
+
approver: 'committed-standing-policy',
|
|
4044
|
+
author: 'committed-standing-policy',
|
|
4045
|
+
codeownerReviewed: true,
|
|
4046
|
+
evidenceRefs: {},
|
|
4047
|
+
expiresAt: input.plan.expiresAt,
|
|
4048
|
+
materialDigest: input.plan.materialDigest,
|
|
4049
|
+
mode: 'standing-policy',
|
|
4050
|
+
stage: input.plan.target.stage,
|
|
4051
|
+
}
|
|
4052
|
+
: undefined);
|
|
4053
|
+
if (!approval) {
|
|
4054
|
+
throw new Error('Local retained-plan authorization requires current standing policy or an exact operator acknowledgement.');
|
|
4055
|
+
}
|
|
4056
|
+
const reviewedApproval = createLocalDeployGovernanceReview({
|
|
4057
|
+
approval,
|
|
4058
|
+
environment: input.environment,
|
|
4059
|
+
plan: input.plan,
|
|
4060
|
+
}) ??
|
|
4061
|
+
createLocalRefreshGovernanceReview({
|
|
4062
|
+
approval,
|
|
4063
|
+
environment: input.environment,
|
|
4064
|
+
plan: input.plan,
|
|
4065
|
+
});
|
|
4066
|
+
const validation = (0, index_js_1.validateInfrastructurePlanForApply)({
|
|
4067
|
+
approval,
|
|
4068
|
+
now: input.now.toISOString(),
|
|
4069
|
+
planned: input.plan,
|
|
4070
|
+
recomputed: input.plan,
|
|
4071
|
+
sourceChangesSincePlan: [],
|
|
4072
|
+
});
|
|
4073
|
+
if (!validation.allowed) {
|
|
4074
|
+
throw new Error('Local retained-plan authorization did not admit the exact current plan.');
|
|
4075
|
+
}
|
|
4076
|
+
const governanceEvidenceDigests = validateGovernanceEvidence(reviewedApproval, input.plan);
|
|
4077
|
+
const authorization = {
|
|
4078
|
+
action: input.plan.action,
|
|
4079
|
+
approval,
|
|
4080
|
+
...(Object.keys(governanceEvidenceDigests).length > 0
|
|
4081
|
+
? { governanceEvidenceDigests }
|
|
4082
|
+
: {}),
|
|
4083
|
+
kind: 'ebk-infrastructure-authorization',
|
|
4084
|
+
...(reviewedApproval?.governance !== undefined
|
|
4085
|
+
? { localGovernance: reviewedApproval.governance }
|
|
4086
|
+
: {}),
|
|
4087
|
+
plannedMaterialDigest: input.plan.materialDigest,
|
|
4088
|
+
recomputedMaterialDigest: input.plan.materialDigest,
|
|
4089
|
+
schemaVersion: 1,
|
|
4090
|
+
sourceChangesSincePlan: [],
|
|
4091
|
+
validation,
|
|
4092
|
+
};
|
|
4093
|
+
const root = projectRoot(input.context, {});
|
|
4094
|
+
const evidenceDirectory = (0, node_path_1.join)(root, '.ebk', 'evidence');
|
|
4095
|
+
(0, node_fs_1.mkdirSync)(evidenceDirectory, { recursive: true, mode: 0o700 });
|
|
4096
|
+
(0, node_fs_1.chmodSync)(evidenceDirectory, 0o700);
|
|
4097
|
+
writePrivateEvidence((0, node_path_1.join)(evidenceDirectory, 'infrastructure-plan-authorization.json'), `${JSON.stringify(authorization, null, 2)}\n`);
|
|
4098
|
+
return authorization;
|
|
4099
|
+
}
|
|
4100
|
+
/** @internal */
|
|
4101
|
+
function requiresGovernanceValidation(input) {
|
|
4102
|
+
return (input.hasApproval ||
|
|
4103
|
+
(input.adoptedImportCount > 0 && input.action !== 'refresh'));
|
|
4104
|
+
}
|
|
4105
|
+
/** @internal */
|
|
4106
|
+
function isLocalPlanReviewPending(input) {
|
|
4107
|
+
const findingCodes = input.validation.findings.map(({ code }) => code).sort();
|
|
4108
|
+
const hasExactPendingReviewFindings = findingCodes.length === 1 && findingCodes[0] === 'approval-missing';
|
|
4109
|
+
if (input.environment['GITHUB_ACTIONS'] === 'true' ||
|
|
4110
|
+
input.environment['EBK_LOCAL_DEPLOYMENT_MODE'] !== 'federated' ||
|
|
4111
|
+
(input.plan.action !== 'deploy' && input.plan.action !== 'refresh') ||
|
|
4112
|
+
input.plan.requiredApprovals.length === 0 ||
|
|
4113
|
+
hasUnreviewableLocalPlanBlock(input.plan) ||
|
|
4114
|
+
input.validation.allowed ||
|
|
4115
|
+
!hasExactPendingReviewFindings) {
|
|
4116
|
+
return false;
|
|
4117
|
+
}
|
|
4118
|
+
const policy = (0, index_js_2.readRepositoryOperatingPolicy)(input.context.root);
|
|
4119
|
+
if (!isCanonicalDeploymentStage(input.plan.target.stage))
|
|
4120
|
+
return false;
|
|
4121
|
+
return !(0, index_js_2.requiresGithubDeployment)(policy.deploymentProfile, input.plan.target.stage);
|
|
4122
|
+
}
|
|
4123
|
+
function runLocalGitAuthorityCommand(input) {
|
|
4124
|
+
const result = input.spawn('git', input.args, {
|
|
4125
|
+
cwd: input.context.root,
|
|
4126
|
+
encoding: 'utf8',
|
|
4127
|
+
env: input.environment,
|
|
4128
|
+
maxBuffer: MAX_JSON_BYTES,
|
|
4129
|
+
shell: false,
|
|
4130
|
+
});
|
|
4131
|
+
if (result.status !== 0 || result.stderr !== '') {
|
|
4132
|
+
throw new Error('Local mutation source identity could not be revalidated.');
|
|
4133
|
+
}
|
|
4134
|
+
return result.stdout.trim();
|
|
4135
|
+
}
|
|
4136
|
+
/** @internal */
|
|
4137
|
+
function observeLocalMutationStateIdentity(input) {
|
|
4138
|
+
const stage = requiredEnvironment(input.childEnvironment, 'EBK_CONSUMER_STAGE');
|
|
4139
|
+
const region = requiredEnvironment(input.childEnvironment, 'AWS_REGION');
|
|
4140
|
+
const expectedAccount = expectedDeploymentAccount(input.environment, input.invocation);
|
|
4141
|
+
const caller = record(runJsonCommand(input.spawn, 'aws', ['sts', 'get-caller-identity', '--output', 'json'], {
|
|
4142
|
+
cwd: input.invocation.cwd,
|
|
4143
|
+
env: input.childEnvironment,
|
|
4144
|
+
label: 'Local mutation AWS caller identity',
|
|
4145
|
+
}), 'Local mutation AWS caller identity');
|
|
4146
|
+
const callerArn = typeof caller['Arn'] === 'string' ? caller['Arn'] : '';
|
|
4147
|
+
const expectedRoleArn = input.childEnvironment.EBK_EXPECTED_DEPLOYMENT_ROLE_ARN?.trim();
|
|
4148
|
+
if (caller['Account'] !== expectedAccount ||
|
|
4149
|
+
!callerArn ||
|
|
4150
|
+
!expectedRoleArn ||
|
|
4151
|
+
!matchesExpectedAssumedRoleArn(expectedAccount, callerArn, expectedRoleArn)) {
|
|
4152
|
+
throw new Error('Local mutation caller no longer matches the expected account and temporary assumed role.');
|
|
4153
|
+
}
|
|
4154
|
+
const stateLocation = exactSstStateLocation(input.environment);
|
|
4155
|
+
const stateKey = `app/${input.project}/${stage}.json`;
|
|
4156
|
+
const stateExists = exactS3KeyExists({
|
|
4157
|
+
childEnvironment: input.childEnvironment,
|
|
4158
|
+
invocation: input.invocation,
|
|
4159
|
+
key: stateKey,
|
|
4160
|
+
label: `Local mutation SST state identity ${stateKey}`,
|
|
4161
|
+
spawn: input.spawn,
|
|
4162
|
+
stateBucketName: stateLocation.bucketName,
|
|
4163
|
+
});
|
|
4164
|
+
const sstPath = input.invocation.args[0];
|
|
4165
|
+
if (!sstPath)
|
|
4166
|
+
throw new Error('Workspace SST path is missing.');
|
|
4167
|
+
const exported = stateExists
|
|
4168
|
+
? runJsonCommand(input.spawn, input.invocation.executable, [
|
|
4169
|
+
sstPath,
|
|
4170
|
+
'state',
|
|
4171
|
+
'export',
|
|
4172
|
+
'--stage',
|
|
4173
|
+
stage,
|
|
4174
|
+
...(input.invocation.args.includes('--profile')
|
|
4175
|
+
? input.invocation.args.slice(input.invocation.args.indexOf('--profile'), input.invocation.args.indexOf('--profile') + 2)
|
|
4176
|
+
: []),
|
|
4177
|
+
], {
|
|
4178
|
+
cwd: input.invocation.cwd,
|
|
4179
|
+
env: input.childEnvironment,
|
|
4180
|
+
label: 'Local mutation SST state export',
|
|
4181
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
4182
|
+
})
|
|
4183
|
+
: null;
|
|
4184
|
+
const exportedState = stateExists
|
|
4185
|
+
? stateExport(exported)
|
|
4186
|
+
: { resources: [] };
|
|
4187
|
+
const stackIdentities = new Set();
|
|
4188
|
+
for (const resource of exportedState.resources) {
|
|
4189
|
+
const urn = typeof resource.urn === 'string' ? resource.urn : '';
|
|
4190
|
+
const match = /^urn:pulumi:([^:]+)::([^:]+)::/.exec(urn);
|
|
4191
|
+
if (match)
|
|
4192
|
+
stackIdentities.add(`${match[2]}/${match[1]}`);
|
|
4193
|
+
}
|
|
4194
|
+
if (stateExists && stackIdentities.size !== 1) {
|
|
4195
|
+
throw new Error('Local mutation SST state does not identify exactly one application and stack.');
|
|
4196
|
+
}
|
|
4197
|
+
const stack = stateExists
|
|
4198
|
+
? [...stackIdentities][0]
|
|
4199
|
+
: `${input.project}/${stage}`;
|
|
4200
|
+
if (exportedState.sstStack &&
|
|
4201
|
+
exportedState.sstStack.split('/').slice(1).join('/') !== stack) {
|
|
4202
|
+
throw new Error('Local mutation SST checkpoint identity does not match its resource inventory.');
|
|
4203
|
+
}
|
|
4204
|
+
const [application, observedStage] = stack.split('/');
|
|
4205
|
+
if (application !== input.project ||
|
|
4206
|
+
observedStage !== stage ||
|
|
4207
|
+
!stateLocation.applications.includes(application)) {
|
|
4208
|
+
throw new Error('Local mutation SST state is outside the exact project and stage authority.');
|
|
4209
|
+
}
|
|
4210
|
+
const stateLock = observeSstStateLock({
|
|
4211
|
+
app: application,
|
|
4212
|
+
childEnvironment: input.childEnvironment,
|
|
4213
|
+
invocation: input.invocation,
|
|
4214
|
+
spawn: input.spawn,
|
|
4215
|
+
stage,
|
|
4216
|
+
stateBucketName: stateLocation.bucketName,
|
|
4217
|
+
});
|
|
4218
|
+
const observerBinding = {
|
|
4219
|
+
accountFingerprint: digest(expectedAccount),
|
|
4220
|
+
backendBootstrapDigest: input.backendBootstrapDigest,
|
|
4221
|
+
project: input.project,
|
|
4222
|
+
region,
|
|
4223
|
+
sourceCommit: input.sourceCommit,
|
|
4224
|
+
stage,
|
|
4225
|
+
stateLocationDigest: digest(canonicalJson({
|
|
4226
|
+
bucketArn: stateLocation.bucketArn,
|
|
4227
|
+
lockKey: stateLock.key,
|
|
4228
|
+
stateKey,
|
|
4229
|
+
})),
|
|
4230
|
+
};
|
|
4231
|
+
const stateDigest = digest(canonicalJson(stateExists
|
|
4232
|
+
? { exportedState: exported, observerBinding }
|
|
4233
|
+
: { observerBinding, stateKey, status: 'absent' }));
|
|
4234
|
+
const generatedAt = input.now.toISOString();
|
|
4235
|
+
const expiresAt = new Date(input.now.getTime() + 60 * 60_000).toISOString();
|
|
4236
|
+
return {
|
|
4237
|
+
actorArn: callerArn,
|
|
4238
|
+
exportedState: exported,
|
|
4239
|
+
identity: {
|
|
4240
|
+
backend: stateExists
|
|
4241
|
+
? 'sst-pulumi-state-export'
|
|
4242
|
+
: 'sst-pulumi-state-absent',
|
|
4243
|
+
digest: digest(canonicalJson({ lock: stateLock.digest, stateDigest })),
|
|
4244
|
+
expiresAt,
|
|
4245
|
+
locked: stateLock.locked,
|
|
4246
|
+
observedAt: generatedAt,
|
|
4247
|
+
stack,
|
|
4248
|
+
version: digest(canonicalJson({ lock: stateLock.digest, stateDigest })),
|
|
4249
|
+
},
|
|
4250
|
+
resourceCount: exportedState.resources.length,
|
|
4251
|
+
};
|
|
4252
|
+
}
|
|
4253
|
+
/** @internal */
|
|
4254
|
+
function retainLocalRefreshStateBackup(input) {
|
|
4255
|
+
if (input.plan.action !== 'refresh' ||
|
|
4256
|
+
input.observation.identity.backend !== 'sst-pulumi-state-export' ||
|
|
4257
|
+
input.observation.exportedState === null) {
|
|
4258
|
+
throw new Error('Local refresh requires an exportable current state recovery point.');
|
|
4259
|
+
}
|
|
4260
|
+
const evidenceDirectory = (0, node_path_1.join)(projectRoot(input.context, {}), '.ebk', 'evidence');
|
|
4261
|
+
(0, node_fs_1.mkdirSync)(evidenceDirectory, { recursive: true, mode: 0o700 });
|
|
4262
|
+
(0, node_fs_1.chmodSync)(evidenceDirectory, 0o700);
|
|
4263
|
+
const backupContents = `${JSON.stringify(input.observation.exportedState, null, 2)}\n`;
|
|
4264
|
+
const backupPath = (0, node_path_1.join)(evidenceDirectory, 'infrastructure-state-backup.private.json');
|
|
4265
|
+
writePrivateEvidence(backupPath, backupContents);
|
|
4266
|
+
(0, private_state_js_1.hardenPrivatePath)(backupPath, 'file');
|
|
4267
|
+
(0, private_state_js_1.assertPrivatePath)(backupPath, 'file');
|
|
4268
|
+
const retainedBackup = (0, node_fs_1.readFileSync)(backupPath, 'utf8');
|
|
4269
|
+
const retainedBackupStat = (0, node_fs_1.lstatSync)(backupPath, { bigint: true });
|
|
4270
|
+
if (!retainedBackupStat.isFile() ||
|
|
4271
|
+
retainedBackupStat.nlink !== 1n ||
|
|
4272
|
+
retainedBackup !== backupContents) {
|
|
4273
|
+
throw new Error('Local refresh state recovery point could not be verified after write.');
|
|
4274
|
+
}
|
|
4275
|
+
writePrivateEvidence((0, node_path_1.join)(evidenceDirectory, 'infrastructure-state-backup.json'), `${JSON.stringify({
|
|
4276
|
+
backupDigest: digest(backupContents),
|
|
4277
|
+
capturedAt: input.now.toISOString(),
|
|
4278
|
+
kind: 'ebk-infrastructure-state-backup',
|
|
4279
|
+
project: input.project,
|
|
4280
|
+
resourceCount: input.observation.resourceCount,
|
|
4281
|
+
schemaVersion: 1,
|
|
4282
|
+
sourceCommit: input.plan.sourceCommit,
|
|
4283
|
+
stage: input.plan.target.stage,
|
|
4284
|
+
stateDigest: input.observation.identity.digest,
|
|
4285
|
+
stateVersion: input.observation.identity.version,
|
|
4286
|
+
}, null, 2)}\n`);
|
|
4287
|
+
}
|
|
4288
|
+
function consumeLocalMutationCapability(input) {
|
|
4289
|
+
const capabilityPath = input.environment.EBK_LOCAL_MUTATION_CAPABILITY_PATH?.trim() ?? '';
|
|
4290
|
+
const nonce = input.environment.EBK_LOCAL_MUTATION_CAPABILITY_NONCE?.trim() ?? '';
|
|
4291
|
+
if (!(0, node_path_1.isAbsolute)(capabilityPath) || !nonce) {
|
|
4292
|
+
throw new Error('Local mutation requires one invocation-scoped capability path and nonce.');
|
|
4293
|
+
}
|
|
4294
|
+
const parent = (0, node_fs_1.realpathSync)((0, node_path_1.dirname)(capabilityPath));
|
|
4295
|
+
const temporaryRoot = (0, node_fs_1.realpathSync)((0, node_os_1.tmpdir)());
|
|
4296
|
+
const parentRelative = (0, node_path_1.relative)(temporaryRoot, parent);
|
|
4297
|
+
const parentStat = (0, node_fs_1.lstatSync)(parent, { bigint: true });
|
|
4298
|
+
let parentIsPrivate = true;
|
|
4299
|
+
try {
|
|
4300
|
+
(0, private_state_js_1.assertPrivatePath)(parent, 'directory');
|
|
4301
|
+
}
|
|
4302
|
+
catch {
|
|
4303
|
+
parentIsPrivate = false;
|
|
4304
|
+
}
|
|
4305
|
+
if (parentRelative === '..' ||
|
|
4306
|
+
parentRelative.startsWith(`..${process.platform === 'win32' ? '\\' : '/'}`) ||
|
|
4307
|
+
(0, node_path_1.isAbsolute)(parentRelative) ||
|
|
4308
|
+
!(0, node_path_1.basename)(parent).startsWith('ebk-local-mutation-') ||
|
|
4309
|
+
!parentStat.isDirectory() ||
|
|
4310
|
+
!parentIsPrivate) {
|
|
4311
|
+
throw new Error('Local mutation capability directory is outside the private temporary authority boundary.');
|
|
4312
|
+
}
|
|
4313
|
+
let before;
|
|
4314
|
+
try {
|
|
4315
|
+
before = (0, node_fs_1.lstatSync)(capabilityPath, { bigint: true });
|
|
4316
|
+
}
|
|
4317
|
+
catch {
|
|
4318
|
+
throw new Error('Local mutation capability is malformed, expired, replayed, or absent.');
|
|
4319
|
+
}
|
|
4320
|
+
const expectedUid = process.getuid?.();
|
|
4321
|
+
let capabilityIsPrivate = true;
|
|
4322
|
+
try {
|
|
4323
|
+
(0, private_state_js_1.assertPrivatePath)(capabilityPath, 'file');
|
|
4324
|
+
}
|
|
4325
|
+
catch {
|
|
4326
|
+
capabilityIsPrivate = false;
|
|
4327
|
+
}
|
|
4328
|
+
if (!before.isFile() ||
|
|
4329
|
+
before.nlink !== 1n ||
|
|
4330
|
+
before.size > 64n * 1024n ||
|
|
4331
|
+
!capabilityIsPrivate ||
|
|
4332
|
+
(expectedUid !== undefined && before.uid !== BigInt(expectedUid))) {
|
|
4333
|
+
throw new Error('Local mutation capability is not one bounded private regular file.');
|
|
4334
|
+
}
|
|
4335
|
+
const contents = (0, node_fs_1.readFileSync)(capabilityPath, 'utf8');
|
|
4336
|
+
const after = (0, node_fs_1.lstatSync)(capabilityPath, { bigint: true });
|
|
4337
|
+
if (before.dev !== after.dev ||
|
|
4338
|
+
before.ino !== after.ino ||
|
|
4339
|
+
before.mode !== after.mode ||
|
|
4340
|
+
before.nlink !== after.nlink ||
|
|
4341
|
+
before.size !== after.size ||
|
|
4342
|
+
before.mtimeNs !== after.mtimeNs ||
|
|
4343
|
+
before.ctimeNs !== after.ctimeNs) {
|
|
4344
|
+
throw new Error('Local mutation capability changed while it was read.');
|
|
4345
|
+
}
|
|
4346
|
+
const capability = record(parseJson(contents, 'Local mutation capability'), 'Local mutation capability');
|
|
4347
|
+
(0, local_capability_js_1.validateLocalMutationCapability)({
|
|
4348
|
+
authorizationContents: input.authorizationContents,
|
|
4349
|
+
capability,
|
|
4350
|
+
expectedAuthority: capability.authority,
|
|
4351
|
+
nonce,
|
|
4352
|
+
now: input.now,
|
|
4353
|
+
planContents: input.planContents,
|
|
4354
|
+
});
|
|
4355
|
+
const spawn = input.dependencies.spawn ?? defaultSpawn;
|
|
4356
|
+
const deploymentSourceCommit = assertCheckedOutSourceCommit(input.dependencies.spawn, input.context.root, input.environment);
|
|
4357
|
+
const sourceStatus = runLocalGitAuthorityCommand({
|
|
4358
|
+
args: ['status', '--porcelain=v1', '--untracked-files=all'],
|
|
4359
|
+
context: input.context,
|
|
4360
|
+
environment: input.environment,
|
|
4361
|
+
spawn,
|
|
4362
|
+
});
|
|
4363
|
+
const deploymentSourceTree = runLocalGitAuthorityCommand({
|
|
4364
|
+
args: ['rev-parse', 'HEAD^{tree}'],
|
|
4365
|
+
context: input.context,
|
|
4366
|
+
environment: input.environment,
|
|
4367
|
+
spawn,
|
|
4368
|
+
});
|
|
4369
|
+
const releaseSourceTree = runLocalGitAuthorityCommand({
|
|
4370
|
+
args: ['rev-parse', `${input.plan.sourceCommit}^{tree}`],
|
|
4371
|
+
context: input.context,
|
|
4372
|
+
environment: input.environment,
|
|
4373
|
+
spawn,
|
|
4374
|
+
});
|
|
4375
|
+
if (sourceStatus !== '') {
|
|
4376
|
+
throw new Error('Local mutation source tree changed after its current plan was acknowledged.');
|
|
4377
|
+
}
|
|
4378
|
+
if (deploymentSourceCommit !== input.plan.sourceCommit) {
|
|
4379
|
+
(0, source_authority_js_1.validateReviewedReleaseCheckout)({
|
|
4380
|
+
deploymentSourceCommit,
|
|
4381
|
+
releaseSourceCommit: input.plan.sourceCommit,
|
|
4382
|
+
repositoryRoot: input.context.root,
|
|
4383
|
+
reviewedSourceCommit: deploymentSourceCommit,
|
|
4384
|
+
});
|
|
4385
|
+
}
|
|
4386
|
+
const root = projectRoot(input.context, input.options);
|
|
4387
|
+
const descriptorPath = relativeFile(root, input.options.descriptorPath ?? 'operations/infrastructure-change.json');
|
|
4388
|
+
const descriptor = readDescriptor(descriptorPath, input.context.projectName ?? '');
|
|
4389
|
+
const buildArtifacts = collectInfrastructureBuildArtifacts(root);
|
|
4390
|
+
const retainedBuildArtifacts = record(readJsonFile((0, node_path_1.join)(root, '.ebk', 'evidence', 'infrastructure-build-artifacts.json'), 'Retained infrastructure build artifacts'), 'Retained infrastructure build artifacts');
|
|
4391
|
+
if (retainedBuildArtifacts['digest'] !== buildArtifacts.digest) {
|
|
4392
|
+
throw new Error('Local mutation build artifacts changed after the current plan was acknowledged.');
|
|
4393
|
+
}
|
|
4394
|
+
const policy = (0, index_js_2.readRepositoryOperatingPolicy)(input.context.root);
|
|
4395
|
+
const expectedRoleArn = input.invocation.environment.EBK_EXPECTED_DEPLOYMENT_ROLE_ARN?.trim() ?? '';
|
|
4396
|
+
if (digest(descriptor.raw) !== capability.authority.policyProfileDigest ||
|
|
4397
|
+
frameworkVersion(input.context.root) !==
|
|
4398
|
+
capability.authority.frameworkVersion ||
|
|
4399
|
+
planAuthorityPolicyDigest(input.environment, input.invocation) !==
|
|
4400
|
+
capability.authority.planAuthorityPolicyDigest) {
|
|
4401
|
+
throw new Error('Local mutation source, policy, role, artifact, or environment authority changed after acknowledgement.');
|
|
4402
|
+
}
|
|
4403
|
+
const childEnvironment = createCandidateBoundSstChildEnvironment({
|
|
4404
|
+
...input.environment,
|
|
4405
|
+
EBK_LOCAL_SOURCE_TREE: releaseSourceTree,
|
|
4406
|
+
}, input.invocation.environment, input.plan.sourceCommit);
|
|
4407
|
+
const stateLocation = exactSstStateLocation(input.environment);
|
|
4408
|
+
const backendBootstrapDigest = preflightSstBootstrap({
|
|
4409
|
+
accountId: expectedDeploymentAccount(input.environment, input.invocation),
|
|
4410
|
+
childEnvironment,
|
|
4411
|
+
invocation: input.invocation,
|
|
4412
|
+
region: requiredEnvironment(childEnvironment, 'AWS_REGION'),
|
|
4413
|
+
spawn,
|
|
4414
|
+
stateBucketName: stateLocation.bucketName,
|
|
4415
|
+
});
|
|
4416
|
+
preflightSstBackendParameter({
|
|
4417
|
+
childEnvironment,
|
|
4418
|
+
expectedType: 'SecureString',
|
|
4419
|
+
invocation: input.invocation,
|
|
4420
|
+
name: `/sst/passphrase/${input.context.projectName}/${input.plan.target.stage}`,
|
|
4421
|
+
spawn,
|
|
4422
|
+
});
|
|
4423
|
+
preflightProtectedSstSecrets({
|
|
4424
|
+
application: input.context.projectName ?? '',
|
|
4425
|
+
childEnvironment,
|
|
4426
|
+
invocation: input.invocation,
|
|
4427
|
+
spawn,
|
|
4428
|
+
stage: input.plan.target.stage,
|
|
4429
|
+
stateBucketName: stateLocation.bucketName,
|
|
4430
|
+
});
|
|
4431
|
+
const stateObservation = observeLocalMutationStateIdentity({
|
|
4432
|
+
backendBootstrapDigest,
|
|
4433
|
+
childEnvironment,
|
|
4434
|
+
environment: input.environment,
|
|
4435
|
+
invocation: input.invocation,
|
|
4436
|
+
now: input.now,
|
|
4437
|
+
project: input.context.projectName ?? '',
|
|
4438
|
+
sourceCommit: input.plan.sourceCommit,
|
|
4439
|
+
spawn,
|
|
4440
|
+
});
|
|
4441
|
+
const state = stateObservation.identity;
|
|
4442
|
+
const expectedAuthority = (0, local_capability_js_1.localMutationAuthorityEnvelope)({
|
|
4443
|
+
actorArn: stateObservation.actorArn,
|
|
4444
|
+
deploymentProfile: policy.deploymentProfile,
|
|
4445
|
+
deploymentSourceCommit,
|
|
4446
|
+
deploymentSourceTree,
|
|
4447
|
+
expectedRoleArn,
|
|
4448
|
+
plan: input.plan,
|
|
4449
|
+
project: input.context.projectName ?? '',
|
|
4450
|
+
releaseSourceTree,
|
|
4451
|
+
});
|
|
4452
|
+
if ((0, local_capability_js_1.computeLocalMutationAuthorityDigest)(expectedAuthority) !==
|
|
4453
|
+
capability.authorityDigest ||
|
|
4454
|
+
state.locked !== false ||
|
|
4455
|
+
state.backend !== capability.authority.state.backend ||
|
|
4456
|
+
state.digest !== capability.authority.state.digest ||
|
|
4457
|
+
state.stack !== capability.authority.state.stack ||
|
|
4458
|
+
state.version !== capability.authority.state.version) {
|
|
4459
|
+
throw new Error('Local mutation state identity or lock changed after the current plan was acknowledged.');
|
|
4460
|
+
}
|
|
4461
|
+
if (input.action === 'refresh') {
|
|
4462
|
+
retainLocalRefreshStateBackup({
|
|
4463
|
+
context: input.context,
|
|
4464
|
+
now: input.now,
|
|
4465
|
+
observation: stateObservation,
|
|
4466
|
+
plan: input.plan,
|
|
4467
|
+
project: input.context.projectName ?? '',
|
|
4468
|
+
});
|
|
4469
|
+
}
|
|
4470
|
+
(0, node_fs_1.rmSync)(capabilityPath);
|
|
4471
|
+
}
|
|
2549
4472
|
async function runInfrastructurePlanExecutor(options, context, environment = process.env, dependencies = {}) {
|
|
2550
4473
|
const action = options.action ?? 'deploy';
|
|
2551
4474
|
const root = projectRoot(context, options);
|
|
@@ -2572,7 +4495,27 @@ async function runInfrastructurePlanExecutor(options, context, environment = pro
|
|
|
2572
4495
|
let approval;
|
|
2573
4496
|
let reviewedApproval;
|
|
2574
4497
|
let changes = [];
|
|
2575
|
-
|
|
4498
|
+
const localApproval = createLocalPlanApprovalRecord({
|
|
4499
|
+
context,
|
|
4500
|
+
environment,
|
|
4501
|
+
now,
|
|
4502
|
+
plan,
|
|
4503
|
+
});
|
|
4504
|
+
if (localApproval) {
|
|
4505
|
+
approval = localApproval;
|
|
4506
|
+
reviewedApproval =
|
|
4507
|
+
createLocalDeployGovernanceReview({
|
|
4508
|
+
approval: localApproval,
|
|
4509
|
+
environment,
|
|
4510
|
+
plan,
|
|
4511
|
+
}) ??
|
|
4512
|
+
createLocalRefreshGovernanceReview({
|
|
4513
|
+
approval: localApproval,
|
|
4514
|
+
environment,
|
|
4515
|
+
plan,
|
|
4516
|
+
});
|
|
4517
|
+
}
|
|
4518
|
+
else if (descriptor.standingPolicyRiskClasses.includes(plan.aggregateRisk) &&
|
|
2576
4519
|
plan.requiredApprovals.length === 0) {
|
|
2577
4520
|
approval = {
|
|
2578
4521
|
approvedAt: now.toISOString(),
|
|
@@ -2628,8 +4571,12 @@ async function runInfrastructurePlanExecutor(options, context, environment = pro
|
|
|
2628
4571
|
recomputed: plan,
|
|
2629
4572
|
sourceChangesSincePlan: changes,
|
|
2630
4573
|
});
|
|
2631
|
-
const governanceEvidenceDigests =
|
|
2632
|
-
|
|
4574
|
+
const governanceEvidenceDigests = requiresGovernanceValidation({
|
|
4575
|
+
action: planned.action,
|
|
4576
|
+
adoptedImportCount: descriptor.policy.adoptedImports.length,
|
|
4577
|
+
hasApproval: approval !== undefined,
|
|
4578
|
+
})
|
|
4579
|
+
? validateGovernanceEvidence(reviewedApproval, planned, current.observation.resources, current.importIdentifierDigests, descriptor.policy.adoptedImports)
|
|
2633
4580
|
: {};
|
|
2634
4581
|
const authorization = {
|
|
2635
4582
|
action,
|
|
@@ -2638,6 +4585,9 @@ async function runInfrastructurePlanExecutor(options, context, environment = pro
|
|
|
2638
4585
|
? { governanceEvidenceDigests }
|
|
2639
4586
|
: {}),
|
|
2640
4587
|
kind: 'ebk-infrastructure-authorization',
|
|
4588
|
+
...(localApproval && reviewedApproval?.governance !== undefined
|
|
4589
|
+
? { localGovernance: reviewedApproval.governance }
|
|
4590
|
+
: {}),
|
|
2641
4591
|
plannedMaterialDigest: planned.materialDigest,
|
|
2642
4592
|
recomputedMaterialDigest: plan.materialDigest,
|
|
2643
4593
|
schemaVersion: 1,
|
|
@@ -2647,9 +4597,14 @@ async function runInfrastructurePlanExecutor(options, context, environment = pro
|
|
|
2647
4597
|
const evidenceDirectory = (0, node_path_1.join)(root, '.ebk', 'evidence');
|
|
2648
4598
|
(0, node_fs_1.mkdirSync)(evidenceDirectory, { recursive: true, mode: 0o700 });
|
|
2649
4599
|
(0, node_fs_1.chmodSync)(evidenceDirectory, 0o700);
|
|
4600
|
+
const refreshPreviewDiagnostics = refreshPreviewDiagnosticsByObservation.get(current.observation);
|
|
4601
|
+
if (refreshPreviewDiagnostics) {
|
|
4602
|
+
writePrivateEvidence((0, node_path_1.join)(evidenceDirectory, 'infrastructure-drift-diagnostics.json'), `${JSON.stringify(refreshPreviewDiagnostics, null, 2)}\n`);
|
|
4603
|
+
}
|
|
2650
4604
|
writePrivateEvidence((0, node_path_1.join)(evidenceDirectory, 'infrastructure-plan.raw.json'), current.diffStdout);
|
|
2651
4605
|
writePrivateEvidence((0, node_path_1.join)(evidenceDirectory, 'infrastructure-plan.private-stderr.txt'), current.diffStderr);
|
|
2652
4606
|
writePrivateEvidence((0, node_path_1.join)(evidenceDirectory, 'infrastructure-plan.json'), `${JSON.stringify(plan, null, 2)}\n`);
|
|
4607
|
+
writePrivateEvidence((0, node_path_1.join)(evidenceDirectory, 'infrastructure-build-artifacts.json'), `${JSON.stringify(current.buildArtifacts, null, 2)}\n`);
|
|
2653
4608
|
writePrivateEvidence((0, node_path_1.join)(evidenceDirectory, 'infrastructure-plan-authorization.json'), `${JSON.stringify(authorization, null, 2)}\n`);
|
|
2654
4609
|
writePrivateEvidence((0, node_path_1.join)(evidenceDirectory, 'infrastructure-observer-authority.json'), `${JSON.stringify({
|
|
2655
4610
|
kind: 'ebk-infrastructure-observer-authority',
|
|
@@ -2686,15 +4641,32 @@ async function runInfrastructurePlanExecutor(options, context, environment = pro
|
|
|
2686
4641
|
const waitsForEnvironmentReviewer = approvalMode === 'environment-reviewer' &&
|
|
2687
4642
|
plan.target.stage === 'production' &&
|
|
2688
4643
|
(0, index_js_1.isEnvironmentReviewerPendingAuthorization)(authorization);
|
|
2689
|
-
|
|
4644
|
+
const waitsForLocalPlanReview = isLocalPlanReviewPending({
|
|
4645
|
+
context,
|
|
4646
|
+
environment,
|
|
4647
|
+
plan,
|
|
4648
|
+
validation,
|
|
4649
|
+
});
|
|
4650
|
+
return {
|
|
4651
|
+
success: validation.allowed ||
|
|
4652
|
+
waitsForEnvironmentReviewer ||
|
|
4653
|
+
waitsForLocalPlanReview,
|
|
4654
|
+
};
|
|
2690
4655
|
}
|
|
2691
4656
|
async function revalidateInfrastructureMutationAuthorization(action, options, context, invocation, environment = process.env, dependencies = {}) {
|
|
2692
4657
|
const root = projectRoot(context, options);
|
|
2693
4658
|
const evidenceDirectory = (0, node_path_1.join)(root, '.ebk', 'evidence');
|
|
2694
|
-
const
|
|
4659
|
+
const retainedPlanContents = (0, node_fs_1.readFileSync)((0, node_path_1.join)(evidenceDirectory, 'infrastructure-plan.json'), 'utf8');
|
|
4660
|
+
const retainedPlan = record(parseJson(retainedPlanContents, 'Retained infrastructure plan'), 'Retained infrastructure plan');
|
|
2695
4661
|
const authorizationRaw = (0, node_fs_1.readFileSync)((0, node_path_1.join)(evidenceDirectory, 'infrastructure-plan-authorization.json'), 'utf8');
|
|
2696
4662
|
const retainedAuthorization = record(parseJson(authorizationRaw, 'Retained infrastructure authorization'), 'Retained infrastructure authorization');
|
|
2697
4663
|
const stage = invocation.environment.EBK_CONSUMER_STAGE;
|
|
4664
|
+
const operatingPolicy = (0, index_js_2.readRepositoryOperatingPolicy)(context.root);
|
|
4665
|
+
const localGovernedAuthority = (stage === 'development' ||
|
|
4666
|
+
stage === 'staging' ||
|
|
4667
|
+
stage === 'production') &&
|
|
4668
|
+
!(0, index_js_2.requiresGithubDeployment)(operatingPolicy.deploymentProfile, stage) &&
|
|
4669
|
+
environment.GITHUB_ACTIONS !== 'true';
|
|
2698
4670
|
const approvalMode = repositoryInfrastructureApprovalMode(context, stage);
|
|
2699
4671
|
const reviewedSummaryDigest = environment.EBK_ENVIRONMENT_REVIEWED_PLAN_DIGEST?.trim();
|
|
2700
4672
|
if (reviewedSummaryDigest && approvalMode !== 'environment-reviewer') {
|
|
@@ -2740,10 +4712,77 @@ async function revalidateInfrastructureMutationAuthorization(action, options, co
|
|
|
2740
4712
|
!retainedPlan.projects.includes(context.projectName ?? '')) {
|
|
2741
4713
|
throw new Error(`Protected ${action} requires the exact retained plan and plan-clean authorization for the current project and stage.`);
|
|
2742
4714
|
}
|
|
4715
|
+
if (localGovernedAuthority && stage === 'production') {
|
|
4716
|
+
const expectedAccount = expectedDeploymentAccount(environment, invocation);
|
|
4717
|
+
const expectedConfirmation = localProductionConfirmation({
|
|
4718
|
+
action,
|
|
4719
|
+
accountId: expectedAccount,
|
|
4720
|
+
region: retainedPlan.target.region,
|
|
4721
|
+
sourceCommit: retainedPlan.sourceCommit,
|
|
4722
|
+
});
|
|
4723
|
+
if (options.localProductionConfirmation !== expectedConfirmation) {
|
|
4724
|
+
throw new Error(`Local production mutation requires --localProductionConfirmation=${expectedConfirmation}.`);
|
|
4725
|
+
}
|
|
4726
|
+
}
|
|
2743
4727
|
const planOptions = {
|
|
2744
4728
|
...options,
|
|
2745
4729
|
action,
|
|
2746
4730
|
};
|
|
4731
|
+
if (localGovernedAuthority && (action === 'deploy' || action === 'refresh')) {
|
|
4732
|
+
const now = (dependencies.now ?? (() => new Date()))();
|
|
4733
|
+
(dependencies.localCapability ?? consumeLocalMutationCapability)({
|
|
4734
|
+
action,
|
|
4735
|
+
authorizationContents: authorizationRaw,
|
|
4736
|
+
context,
|
|
4737
|
+
dependencies,
|
|
4738
|
+
environment,
|
|
4739
|
+
invocation,
|
|
4740
|
+
now,
|
|
4741
|
+
options: planOptions,
|
|
4742
|
+
plan: retainedPlan,
|
|
4743
|
+
planContents: retainedPlanContents,
|
|
4744
|
+
});
|
|
4745
|
+
const reviewedApproval = retainedAuthorization.approval &&
|
|
4746
|
+
retainedAuthorization.localGovernance !== undefined
|
|
4747
|
+
? {
|
|
4748
|
+
approval: retainedAuthorization.approval,
|
|
4749
|
+
governance: retainedAuthorization.localGovernance,
|
|
4750
|
+
planned: retainedPlan,
|
|
4751
|
+
}
|
|
4752
|
+
: undefined;
|
|
4753
|
+
const governanceEvidenceDigests = validateGovernanceEvidence(reviewedApproval, retainedPlan);
|
|
4754
|
+
if (canonicalJson(governanceEvidenceDigests) !==
|
|
4755
|
+
canonicalJson(retainedAuthorization.governanceEvidenceDigests ?? {})) {
|
|
4756
|
+
throw new Error('Local governed mutation evidence changed after operator acknowledgement.');
|
|
4757
|
+
}
|
|
4758
|
+
const verifiedAt = now.toISOString();
|
|
4759
|
+
const binding = {
|
|
4760
|
+
action,
|
|
4761
|
+
kind: 'ebk-infrastructure-mutation-binding',
|
|
4762
|
+
localAuthority: {
|
|
4763
|
+
confirmationDigest: stage === 'production'
|
|
4764
|
+
? digest(options.localProductionConfirmation ?? '')
|
|
4765
|
+
: null,
|
|
4766
|
+
expectedRoleArn: invocation.environment.EBK_EXPECTED_DEPLOYMENT_ROLE_ARN,
|
|
4767
|
+
mode: 'local-invocation-capability',
|
|
4768
|
+
},
|
|
4769
|
+
recomputedMaterialDigest: retainedPlan.materialDigest,
|
|
4770
|
+
retainedAuthorizationDigest: digest(authorizationRaw),
|
|
4771
|
+
retainedMaterialDigest: retainedPlan.materialDigest,
|
|
4772
|
+
schemaVersion: 1,
|
|
4773
|
+
sourceCommit: retainedPlan.sourceCommit,
|
|
4774
|
+
stage: retainedPlan.target.stage,
|
|
4775
|
+
...(Object.keys(governanceEvidenceDigests).length > 0
|
|
4776
|
+
? { governanceEvidenceDigests }
|
|
4777
|
+
: {}),
|
|
4778
|
+
validation: retainedAuthorization.validation,
|
|
4779
|
+
verifiedAt,
|
|
4780
|
+
};
|
|
4781
|
+
(0, node_fs_1.mkdirSync)(evidenceDirectory, { recursive: true, mode: 0o700 });
|
|
4782
|
+
(0, node_fs_1.chmodSync)(evidenceDirectory, 0o700);
|
|
4783
|
+
writePrivateEvidence((0, node_path_1.join)(evidenceDirectory, 'infrastructure-mutation-binding.json'), `${JSON.stringify(binding, null, 2)}\n`);
|
|
4784
|
+
return;
|
|
4785
|
+
}
|
|
2747
4786
|
const candidateSourceCommit = approvalCandidateSourceCommit(context, planOptions, environment) ??
|
|
2748
4787
|
retainedPlan.sourceCommit;
|
|
2749
4788
|
const recomputedCurrent = computeCurrentInfrastructurePlan({
|
|
@@ -2758,18 +4797,34 @@ async function revalidateInfrastructureMutationAuthorization(action, options, co
|
|
|
2758
4797
|
const recomputed = recomputedCurrent.plan;
|
|
2759
4798
|
let reviewedApproval;
|
|
2760
4799
|
const approvalRecordPath = retainedAuthorization.approval?.recordPath;
|
|
4800
|
+
if (retainedAuthorization.localGovernance !== undefined &&
|
|
4801
|
+
(!localGovernedAuthority ||
|
|
4802
|
+
(action !== 'deploy' && action !== 'refresh') ||
|
|
4803
|
+
approvalRecordPath !== undefined)) {
|
|
4804
|
+
throw new Error('Local infrastructure governance is invalid outside one exact local refresh authorization.');
|
|
4805
|
+
}
|
|
2761
4806
|
if (approvalRecordPath) {
|
|
2762
4807
|
const batch = record(readJsonFile(relativeFile(context.root, approvalRecordPath), 'Retained infrastructure approval governance'), 'Retained infrastructure approval governance');
|
|
2763
4808
|
reviewedApproval = batch.projects[context.projectName ?? ''];
|
|
2764
4809
|
}
|
|
2765
|
-
|
|
4810
|
+
else if (localGovernedAuthority &&
|
|
4811
|
+
retainedAuthorization.approval &&
|
|
4812
|
+
retainedAuthorization.localGovernance !== undefined) {
|
|
4813
|
+
reviewedApproval = {
|
|
4814
|
+
approval: retainedAuthorization.approval,
|
|
4815
|
+
governance: retainedAuthorization.localGovernance,
|
|
4816
|
+
planned: retainedPlan,
|
|
4817
|
+
};
|
|
4818
|
+
}
|
|
4819
|
+
const governanceEvidenceDigests = validateGovernanceEvidence(reviewedApproval, reviewedApproval?.planned ?? retainedPlan, recomputedCurrent.observation.resources, recomputedCurrent.importIdentifierDigests, recomputedCurrent.descriptor.policy.adoptedImports);
|
|
2766
4820
|
if (canonicalJson(governanceEvidenceDigests) !==
|
|
2767
4821
|
canonicalJson(retainedAuthorization.governanceEvidenceDigests ?? {})) {
|
|
2768
4822
|
throw new Error('Governed infrastructure mutation evidence changed after plan authorization.');
|
|
2769
4823
|
}
|
|
2770
4824
|
const changes = sourceChanges(dependencies.spawn, context.root, environment, retainedPlan.sourceCommit);
|
|
2771
4825
|
const now = (dependencies.now ?? (() => new Date()))().toISOString();
|
|
2772
|
-
const validationPlan = retainedAuthorization.approval?.mode === 'human-record'
|
|
4826
|
+
const validationPlan = retainedAuthorization.approval?.mode === 'human-record' ||
|
|
4827
|
+
retainedAuthorization.approval?.mode === 'operator-acknowledgement'
|
|
2773
4828
|
? {
|
|
2774
4829
|
...retainedPlan,
|
|
2775
4830
|
// The retained authorization already proved the reviewer acted after
|
|
@@ -2778,15 +4833,28 @@ async function revalidateInfrastructureMutationAuthorization(action, options, co
|
|
|
2778
4833
|
generatedAt: retainedAuthorization.approval.approvedAt,
|
|
2779
4834
|
}
|
|
2780
4835
|
: retainedPlan;
|
|
2781
|
-
const
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
4836
|
+
const protectedSourceAuthority = environment.GITHUB_ACTIONS === 'true' &&
|
|
4837
|
+
(action === 'deploy' || action === 'refresh');
|
|
4838
|
+
const validation = protectedSourceAuthority
|
|
4839
|
+
? (0, index_js_1.validateInfrastructurePlanForProtectedApply)({
|
|
4840
|
+
...(retainedAuthorization.approval
|
|
4841
|
+
? { approval: retainedAuthorization.approval }
|
|
4842
|
+
: {}),
|
|
4843
|
+
...(environmentAdmission ? { environmentReviewerApproved: true } : {}),
|
|
4844
|
+
now,
|
|
4845
|
+
planned: validationPlan,
|
|
4846
|
+
recomputed,
|
|
4847
|
+
sourceChangesSincePlan: changes,
|
|
4848
|
+
})
|
|
4849
|
+
: (0, index_js_1.validateInfrastructurePlanForApply)({
|
|
4850
|
+
...(retainedAuthorization.approval
|
|
4851
|
+
? { approval: retainedAuthorization.approval }
|
|
4852
|
+
: {}),
|
|
4853
|
+
now,
|
|
4854
|
+
planned: validationPlan,
|
|
4855
|
+
recomputed,
|
|
4856
|
+
sourceChangesSincePlan: changes,
|
|
4857
|
+
});
|
|
2790
4858
|
const environmentReviewerAllowed = environmentAdmission !== undefined &&
|
|
2791
4859
|
(0, index_js_1.isEnvironmentReviewerPendingAuthorization)({
|
|
2792
4860
|
...retainedAuthorization,
|
|
@@ -2797,6 +4865,17 @@ async function revalidateInfrastructureMutationAuthorization(action, options, co
|
|
|
2797
4865
|
const binding = {
|
|
2798
4866
|
action,
|
|
2799
4867
|
kind: 'ebk-infrastructure-mutation-binding',
|
|
4868
|
+
...(localGovernedAuthority
|
|
4869
|
+
? {
|
|
4870
|
+
localAuthority: {
|
|
4871
|
+
confirmationDigest: stage === 'production'
|
|
4872
|
+
? digest(options.localProductionConfirmation ?? '')
|
|
4873
|
+
: null,
|
|
4874
|
+
expectedRoleArn: invocation.environment.EBK_EXPECTED_DEPLOYMENT_ROLE_ARN,
|
|
4875
|
+
mode: 'local-federated',
|
|
4876
|
+
},
|
|
4877
|
+
}
|
|
4878
|
+
: {}),
|
|
2800
4879
|
recomputedMaterialDigest: recomputed.materialDigest,
|
|
2801
4880
|
retainedAuthorizationDigest: digest(authorizationRaw),
|
|
2802
4881
|
retainedMaterialDigest: retainedPlan.materialDigest,
|
|
@@ -2904,6 +4983,7 @@ async function recordInfrastructureMutationConvergence(action, options, context,
|
|
|
2904
4983
|
};
|
|
2905
4984
|
let result;
|
|
2906
4985
|
let converged = false;
|
|
4986
|
+
let refreshStateChangedAfterNonzeroExit = false;
|
|
2907
4987
|
try {
|
|
2908
4988
|
const planOptions = {
|
|
2909
4989
|
...options,
|
|
@@ -3063,6 +5143,25 @@ async function recordInfrastructureMutationConvergence(action, options, context,
|
|
|
3063
5143
|
}
|
|
3064
5144
|
else {
|
|
3065
5145
|
const postPlanEmpty = current.plan.diff.complete && current.plan.diff.resources.length === 0;
|
|
5146
|
+
refreshStateChangedAfterNonzeroExit =
|
|
5147
|
+
action === 'refresh' &&
|
|
5148
|
+
!applySucceeded &&
|
|
5149
|
+
retainedPlan.state.backend === current.plan.state.backend &&
|
|
5150
|
+
retainedPlan.state.stack === current.plan.state.stack &&
|
|
5151
|
+
retainedPlan.state.digest !== current.plan.state.digest &&
|
|
5152
|
+
retainedPlan.state.version !== current.plan.state.version &&
|
|
5153
|
+
current.plan.state.locked === false &&
|
|
5154
|
+
current.plan.diff.complete &&
|
|
5155
|
+
current.plan.diff.failures.length === 0 &&
|
|
5156
|
+
(current.plan.driftStatus === 'no-drift' ||
|
|
5157
|
+
current.plan.driftStatus === 'drift-detected');
|
|
5158
|
+
const mutationCompleted = applySucceeded;
|
|
5159
|
+
const refreshHandoffRetained = action === 'refresh' &&
|
|
5160
|
+
mutationCompleted &&
|
|
5161
|
+
current.plan.diff.complete &&
|
|
5162
|
+
current.plan.diff.failures.length === 0 &&
|
|
5163
|
+
(current.plan.driftStatus === 'no-drift' ||
|
|
5164
|
+
current.plan.driftStatus === 'drift-detected');
|
|
3066
5165
|
const requiredCheckIds = action === 'deploy'
|
|
3067
5166
|
? ['deployed-smoke', 'alarm-health', 'data-integrity']
|
|
3068
5167
|
: [];
|
|
@@ -3072,7 +5171,7 @@ async function recordInfrastructureMutationConvergence(action, options, context,
|
|
|
3072
5171
|
// resource in the authorized plan. The empty recomputed post-plan
|
|
3073
5172
|
// cannot prove that no provider mutation occurred.
|
|
3074
5173
|
changedResourceCount: retainedPlan.diff.resources.length,
|
|
3075
|
-
status:
|
|
5174
|
+
status: mutationCompleted ? 'succeeded' : 'failed',
|
|
3076
5175
|
},
|
|
3077
5176
|
checks: [],
|
|
3078
5177
|
cleanupDebt: [],
|
|
@@ -3089,6 +5188,14 @@ async function recordInfrastructureMutationConvergence(action, options, context,
|
|
|
3089
5188
|
action,
|
|
3090
5189
|
...evidenceBinding,
|
|
3091
5190
|
...convergence,
|
|
5191
|
+
...(refreshHandoffRetained && !convergence.converged
|
|
5192
|
+
? {
|
|
5193
|
+
findings: [
|
|
5194
|
+
'Refresh completed with an exact remaining desired-state plan; run a new governed deployment plan before applying it.',
|
|
5195
|
+
],
|
|
5196
|
+
outcome: 'refresh-complete-replan-required',
|
|
5197
|
+
}
|
|
5198
|
+
: {}),
|
|
3092
5199
|
kind: 'ebk-infrastructure-convergence',
|
|
3093
5200
|
postPlan: current.plan.diff,
|
|
3094
5201
|
postApplyDrift: {
|
|
@@ -3103,6 +5210,19 @@ async function recordInfrastructureMutationConvergence(action, options, context,
|
|
|
3103
5210
|
driftBasis: postPlanEmpty && current.plan.driftStatus === 'no-drift'
|
|
3104
5211
|
? 'live-observer-no-drift-and-empty-source-state-plan'
|
|
3105
5212
|
: 'live-observer-not-converged',
|
|
5213
|
+
processOutcome: refreshStateChangedAfterNonzeroExit
|
|
5214
|
+
? {
|
|
5215
|
+
childExit: 'nonzero',
|
|
5216
|
+
disposition: 'state-changed-recovery-required',
|
|
5217
|
+
postStateDigest: current.plan.state.digest,
|
|
5218
|
+
retainedStateDigest: retainedPlan.state.digest,
|
|
5219
|
+
}
|
|
5220
|
+
: {
|
|
5221
|
+
childExit: applySucceeded ? 'zero' : 'nonzero',
|
|
5222
|
+
disposition: applySucceeded
|
|
5223
|
+
? 'completed'
|
|
5224
|
+
: 'not-reconciled',
|
|
5225
|
+
},
|
|
3106
5226
|
}
|
|
3107
5227
|
: {}),
|
|
3108
5228
|
schemaVersion: 1,
|
|
@@ -3123,14 +5243,17 @@ async function recordInfrastructureMutationConvergence(action, options, context,
|
|
|
3123
5243
|
(0, node_fs_1.mkdirSync)(evidenceDirectory, { recursive: true, mode: 0o700 });
|
|
3124
5244
|
(0, node_fs_1.chmodSync)(evidenceDirectory, 0o700);
|
|
3125
5245
|
writePrivateEvidence((0, node_path_1.join)(evidenceDirectory, 'infrastructure-convergence.json'), `${JSON.stringify(result, null, 2)}\n`);
|
|
3126
|
-
//
|
|
3127
|
-
//
|
|
3128
|
-
//
|
|
3129
|
-
//
|
|
5246
|
+
// Successful deploy and refresh commands may intentionally retain
|
|
5247
|
+
// nonterminal handoffs. Deploy waits for deployed smoke; refresh may leave
|
|
5248
|
+
// an exact desired-state plan that must be separately reviewed and applied.
|
|
5249
|
+
// Neither handoff is terminal convergence.
|
|
3130
5250
|
return (converged ||
|
|
3131
5251
|
(action === 'deploy' &&
|
|
3132
5252
|
applySucceeded &&
|
|
3133
|
-
result['outcome'] === 'apply-complete-unverified')
|
|
5253
|
+
result['outcome'] === 'apply-complete-unverified') ||
|
|
5254
|
+
(action === 'refresh' &&
|
|
5255
|
+
applySucceeded &&
|
|
5256
|
+
result['outcome'] === 'refresh-complete-replan-required'));
|
|
3134
5257
|
}
|
|
3135
5258
|
async function finalizeInfrastructureMutationConvergence(options, context, environment = process.env, dependencies = {}) {
|
|
3136
5259
|
const root = projectRoot(context, options);
|