@continuous-excellence/ze-great-dashboard-aws 0.5.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -6
- package/bootstrap/core-v1.yml +1 -0
- package/bootstrap/github-oidc-v2.yml +27 -1
- package/dist/bootstrap-check.d.ts +45 -0
- package/dist/bootstrap.d.ts +29 -0
- package/dist/cli.js +448 -37
- package/dist/guided.d.ts +1 -1
- package/dist/handoff.d.ts +2 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +401 -12
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -862,6 +862,125 @@ function bootstrapTemplateRevision(template) {
|
|
|
862
862
|
if (!revision) throw new Error("Bootstrap template has no BootstrapTemplateRevision output");
|
|
863
863
|
return revision;
|
|
864
864
|
}
|
|
865
|
+
function templateRevisionIfPresent(template) {
|
|
866
|
+
return template.match(/BootstrapTemplateRevision:\s*\{\s*Value:\s*'([^']+)'\s*}/)?.[1];
|
|
867
|
+
}
|
|
868
|
+
function inspectTemplate(kind, path, template) {
|
|
869
|
+
const resourcesBlock = template.match(/^Resources:\n([\s\S]*?)(?=^[A-Za-z][A-Za-z0-9]*:\s*$)/m)?.[1] ?? "";
|
|
870
|
+
const resources = [
|
|
871
|
+
...resourcesBlock.matchAll(/^ {2}([A-Za-z][A-Za-z0-9]+):\n {4}Type: ([^\n]+)$/gm)
|
|
872
|
+
].map((match) => ({ logicalId: match[1], type: match[2] }));
|
|
873
|
+
const iamActions = [...template.matchAll(/^\s+Action: (?:\[([^\]]+)\]|([^\n]+))$/gm)].flatMap(([, list, scalar]) => (list ?? scalar ?? "").split(",").map((action) => action.trim())).filter(Boolean);
|
|
874
|
+
return {
|
|
875
|
+
kind,
|
|
876
|
+
path,
|
|
877
|
+
contractVersion: bootstrapContractVersion(template),
|
|
878
|
+
templateRevision: templateRevisionIfPresent(template),
|
|
879
|
+
sha256: sha256(template),
|
|
880
|
+
resources,
|
|
881
|
+
iamActions: [...new Set(iamActions)]
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
async function bootstrapPlan(config) {
|
|
885
|
+
const packageManifest = JSON.parse(
|
|
886
|
+
await readFile2(new URL("../package.json", import.meta.url), "utf8")
|
|
887
|
+
);
|
|
888
|
+
const packageTemplates = await Promise.all(
|
|
889
|
+
["core", "github-oidc"].map(async (kind) => {
|
|
890
|
+
const [path, template] = await Promise.all([
|
|
891
|
+
bootstrapTemplatePath(kind),
|
|
892
|
+
bootstrapTemplate(kind)
|
|
893
|
+
]);
|
|
894
|
+
return inspectTemplate(kind, path, template);
|
|
895
|
+
})
|
|
896
|
+
);
|
|
897
|
+
return {
|
|
898
|
+
packageVersion: typeof packageManifest.version === "string" ? packageManifest.version : "unknown",
|
|
899
|
+
packageTemplates,
|
|
900
|
+
configuration: config,
|
|
901
|
+
notes: [
|
|
902
|
+
"Templates are owned by the installed npm package; package.json and package-lock.json pin the source version.",
|
|
903
|
+
"Generated CloudFormation parameter files and describe-stacks captures are deployment artifacts, not source configuration.",
|
|
904
|
+
"This plan performs no AWS or GitHub mutations."
|
|
905
|
+
]
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
function capturedStack(input) {
|
|
909
|
+
const stack = input && typeof input === "object" && "Stacks" in input ? input.Stacks?.[0] : input;
|
|
910
|
+
return stack && typeof stack === "object" ? stack : {};
|
|
911
|
+
}
|
|
912
|
+
function outputValues(stack) {
|
|
913
|
+
return Object.fromEntries(
|
|
914
|
+
(stack.Outputs ?? []).map(({ OutputKey, OutputValue }) => [OutputKey, OutputValue])
|
|
915
|
+
);
|
|
916
|
+
}
|
|
917
|
+
function bootstrapConsistency(kind, config, stackInput, expectedContract, expectedRevision, coreOutputs = {}) {
|
|
918
|
+
const stack = capturedStack(stackInput);
|
|
919
|
+
const mismatches = [];
|
|
920
|
+
const expectedStackName = kind === "core" ? config.core?.stackName : config.githubOidc?.stackName;
|
|
921
|
+
if (stack.StackName !== expectedStackName)
|
|
922
|
+
mismatches.push(
|
|
923
|
+
`stack name is ${stack.StackName ?? "missing"}; expected ${expectedStackName ?? "missing"}`
|
|
924
|
+
);
|
|
925
|
+
const observedRegion = stack.StackId?.match(/^arn:[^:]+:cloudformation:([^:]+):/)?.[1];
|
|
926
|
+
if (observedRegion !== config.region)
|
|
927
|
+
mismatches.push(
|
|
928
|
+
`Region is ${observedRegion ?? "missing"}; expected ${config.region ?? "missing"}`
|
|
929
|
+
);
|
|
930
|
+
if (!["CREATE_COMPLETE", "UPDATE_COMPLETE"].includes(stack.StackStatus ?? ""))
|
|
931
|
+
mismatches.push(
|
|
932
|
+
`stack status is ${stack.StackStatus ?? "missing"}; expected a successful stable status`
|
|
933
|
+
);
|
|
934
|
+
const outputs2 = outputValues(stack);
|
|
935
|
+
if (outputs2.BootstrapContractVersion !== expectedContract)
|
|
936
|
+
mismatches.push(
|
|
937
|
+
`contract is ${outputs2.BootstrapContractVersion ?? "missing"}; expected ${expectedContract}`
|
|
938
|
+
);
|
|
939
|
+
if (expectedRevision && outputs2.BootstrapTemplateRevision !== expectedRevision)
|
|
940
|
+
mismatches.push(
|
|
941
|
+
`template revision is ${outputs2.BootstrapTemplateRevision ?? "missing"}; expected ${expectedRevision}`
|
|
942
|
+
);
|
|
943
|
+
const requiredOutputs = kind === "core" ? ["ArtifactBucketName", "ApplicationStackName", "CloudFormationExecutionRoleArn"] : ["GitHubDeployRoleArn"];
|
|
944
|
+
for (const key of requiredOutputs) if (!outputs2[key]) mismatches.push(`${key} output is missing`);
|
|
945
|
+
if (kind === "core") {
|
|
946
|
+
if (outputs2.ArtifactBucketName !== config.core?.artifactBucketName)
|
|
947
|
+
mismatches.push(
|
|
948
|
+
`ArtifactBucketName output is ${outputs2.ArtifactBucketName ?? "missing"}; expected ${config.core?.artifactBucketName ?? "missing"}`
|
|
949
|
+
);
|
|
950
|
+
if (outputs2.ApplicationStackName !== config.core?.applicationStackName)
|
|
951
|
+
mismatches.push(
|
|
952
|
+
`ApplicationStackName output is ${outputs2.ApplicationStackName ?? "missing"}; expected ${config.core?.applicationStackName ?? "missing"}`
|
|
953
|
+
);
|
|
954
|
+
}
|
|
955
|
+
const parameters = Object.fromEntries(
|
|
956
|
+
(stack.Parameters ?? []).map(({ ParameterKey, ParameterValue }) => [
|
|
957
|
+
ParameterKey,
|
|
958
|
+
ParameterValue
|
|
959
|
+
])
|
|
960
|
+
);
|
|
961
|
+
const expectedParameters = kind === "core" ? {
|
|
962
|
+
ArtifactBucketName: config.core?.artifactBucketName,
|
|
963
|
+
ApplicationStackName: config.core?.applicationStackName,
|
|
964
|
+
DashboardFunctionName: config.core?.dashboardFunctionName,
|
|
965
|
+
RuntimeSecretArn: config.core?.runtimeSecretArn ?? "",
|
|
966
|
+
ArtifactKmsKeyArn: config.core?.artifactKmsKeyArn ?? ""
|
|
967
|
+
} : {
|
|
968
|
+
GitHubOidcProviderArn: config.githubOidc?.providerArn,
|
|
969
|
+
GitHubRepository: config.githubOidc?.repository,
|
|
970
|
+
GitHubOwnerId: config.githubOidc?.ownerId,
|
|
971
|
+
GitHubRepositoryId: config.githubOidc?.repositoryId,
|
|
972
|
+
GitHubEnvironment: config.githubOidc?.environment,
|
|
973
|
+
CoreBootstrapStackName: config.core?.stackName,
|
|
974
|
+
ApplicationStackName: config.core?.applicationStackName,
|
|
975
|
+
ArtifactBucketName: config.core?.artifactBucketName,
|
|
976
|
+
CloudFormationExecutionRoleArn: coreOutputs.CloudFormationExecutionRoleArn,
|
|
977
|
+
ConsumerGatewayStackName: config.githubOidc?.consumerGatewayStackName ?? ""
|
|
978
|
+
};
|
|
979
|
+
for (const [key, expected] of Object.entries(expectedParameters))
|
|
980
|
+
if (expected !== void 0 && parameters[key] !== expected)
|
|
981
|
+
mismatches.push(`${key} is ${parameters[key] ?? "missing"}; expected ${expected}`);
|
|
982
|
+
return { ok: mismatches.length === 0, mismatches };
|
|
983
|
+
}
|
|
865
984
|
function mergeBootstrapParameters(requested, deployed) {
|
|
866
985
|
const supplied = new Map(requested.map((entry) => [entry.ParameterKey, entry.ParameterValue]));
|
|
867
986
|
const duplicates = requested.map((entry) => entry.ParameterKey);
|
|
@@ -909,6 +1028,7 @@ function requiredBootstrapParameters(kind, values) {
|
|
|
909
1028
|
"GitHubOwnerId",
|
|
910
1029
|
"GitHubRepositoryId",
|
|
911
1030
|
"GitHubEnvironment",
|
|
1031
|
+
"CoreBootstrapStackName",
|
|
912
1032
|
"ApplicationStackName",
|
|
913
1033
|
"ArtifactBucketName",
|
|
914
1034
|
"CloudFormationExecutionRoleArn"
|
|
@@ -919,6 +1039,256 @@ function requiredBootstrapParameters(kind, values) {
|
|
|
919
1039
|
return [...keys, ...optional2].filter((key) => values[key] !== void 0).map((ParameterKey) => ({ ParameterKey, ParameterValue: values[ParameterKey] ?? "" }));
|
|
920
1040
|
}
|
|
921
1041
|
|
|
1042
|
+
// packages/aws/src/bootstrap-check.ts
|
|
1043
|
+
function formatBootstrapCheckText(result) {
|
|
1044
|
+
const lines = [`AWS bootstrap consistency (${result.packageVersion})`];
|
|
1045
|
+
for (const stack of result.stacks) {
|
|
1046
|
+
lines.push(
|
|
1047
|
+
`${stack.consistency.ok && (!stack.resourceDrift || stack.resourceDrift.ok) ? "PASS" : "FAIL"} ${stack.kind}: ${stack.stackName} (${stack.stackStatus ?? "unavailable"})`,
|
|
1048
|
+
` template: contract ${stack.contractVersion}, revision ${stack.templateRevision ?? "none"}, sha256 ${stack.templateSha256}`
|
|
1049
|
+
);
|
|
1050
|
+
for (const mismatch of stack.consistency.mismatches) lines.push(` mismatch: ${mismatch}`);
|
|
1051
|
+
if (!stack.resourceDrift) continue;
|
|
1052
|
+
lines.push(` resource drift: ${stack.resourceDrift.status}`);
|
|
1053
|
+
if (stack.resourceDrift.reason) lines.push(` drift error: ${stack.resourceDrift.reason}`);
|
|
1054
|
+
for (const resource of stack.resourceDrift.resources) {
|
|
1055
|
+
lines.push(
|
|
1056
|
+
` drifted resource: ${resource.logicalId} (${resource.resourceType ?? "unknown"}, ${resource.status ?? "unknown"})`
|
|
1057
|
+
);
|
|
1058
|
+
for (const difference of resource.differences)
|
|
1059
|
+
lines.push(
|
|
1060
|
+
` ${difference.path ?? "unknown property"}: ${difference.differenceType ?? "changed"}; expected ${difference.expectedValue ?? "unknown"}, actual ${difference.actualValue ?? "unknown"}`
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
return `${lines.join("\n")}
|
|
1065
|
+
`;
|
|
1066
|
+
}
|
|
1067
|
+
function message(error) {
|
|
1068
|
+
return error instanceof Error ? error.message : String(error);
|
|
1069
|
+
}
|
|
1070
|
+
function parseJson(raw, operation) {
|
|
1071
|
+
try {
|
|
1072
|
+
const value2 = JSON.parse(raw);
|
|
1073
|
+
if (!value2 || typeof value2 !== "object" || Array.isArray(value2))
|
|
1074
|
+
throw new Error("response is not an object");
|
|
1075
|
+
return value2;
|
|
1076
|
+
} catch (error) {
|
|
1077
|
+
throw new Error(`${operation} returned invalid JSON: ${message(error)}`);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
function stackFrom(response) {
|
|
1081
|
+
const stacks = response.Stacks;
|
|
1082
|
+
return Array.isArray(stacks) && stacks[0] && typeof stacks[0] === "object" ? stacks[0] : void 0;
|
|
1083
|
+
}
|
|
1084
|
+
function outputs(stack) {
|
|
1085
|
+
const entries = stack?.Outputs;
|
|
1086
|
+
if (!Array.isArray(entries)) return {};
|
|
1087
|
+
return Object.fromEntries(
|
|
1088
|
+
entries.filter(
|
|
1089
|
+
(entry) => Boolean(entry && typeof entry === "object" && typeof entry.OutputKey === "string")
|
|
1090
|
+
).map(({ OutputKey, OutputValue }) => [OutputKey, OutputValue])
|
|
1091
|
+
);
|
|
1092
|
+
}
|
|
1093
|
+
async function resourceDrift(stackName2, region2, dependencies) {
|
|
1094
|
+
try {
|
|
1095
|
+
const detected = parseJson(
|
|
1096
|
+
await dependencies.execute("aws", [
|
|
1097
|
+
"cloudformation",
|
|
1098
|
+
"detect-stack-drift",
|
|
1099
|
+
"--stack-name",
|
|
1100
|
+
stackName2,
|
|
1101
|
+
"--region",
|
|
1102
|
+
region2,
|
|
1103
|
+
"--output",
|
|
1104
|
+
"json",
|
|
1105
|
+
"--no-cli-pager"
|
|
1106
|
+
]),
|
|
1107
|
+
"detect-stack-drift"
|
|
1108
|
+
);
|
|
1109
|
+
const detectionId = detected.StackDriftDetectionId;
|
|
1110
|
+
if (typeof detectionId !== "string")
|
|
1111
|
+
throw new Error("detect-stack-drift returned no StackDriftDetectionId");
|
|
1112
|
+
const now = dependencies.now ?? Date.now;
|
|
1113
|
+
const sleep = dependencies.sleep ?? ((milliseconds) => new Promise((resolve3) => setTimeout(resolve3, milliseconds)));
|
|
1114
|
+
const deadline = now() + 10 * 60 * 1e3;
|
|
1115
|
+
let status;
|
|
1116
|
+
while (true) {
|
|
1117
|
+
status = parseJson(
|
|
1118
|
+
await dependencies.execute("aws", [
|
|
1119
|
+
"cloudformation",
|
|
1120
|
+
"describe-stack-drift-detection-status",
|
|
1121
|
+
"--stack-drift-detection-id",
|
|
1122
|
+
detectionId,
|
|
1123
|
+
"--region",
|
|
1124
|
+
region2,
|
|
1125
|
+
"--output",
|
|
1126
|
+
"json",
|
|
1127
|
+
"--no-cli-pager"
|
|
1128
|
+
]),
|
|
1129
|
+
"describe-stack-drift-detection-status"
|
|
1130
|
+
);
|
|
1131
|
+
if (status.DetectionStatus !== "DETECTION_IN_PROGRESS") break;
|
|
1132
|
+
if (now() >= deadline)
|
|
1133
|
+
return {
|
|
1134
|
+
ok: false,
|
|
1135
|
+
status: "TIMED_OUT",
|
|
1136
|
+
detectionId,
|
|
1137
|
+
reason: "CloudFormation drift detection did not complete within 10 minutes",
|
|
1138
|
+
resources: []
|
|
1139
|
+
};
|
|
1140
|
+
await sleep(5e3);
|
|
1141
|
+
}
|
|
1142
|
+
if (status.DetectionStatus !== "DETECTION_COMPLETE")
|
|
1143
|
+
return {
|
|
1144
|
+
ok: false,
|
|
1145
|
+
status: String(status.DetectionStatus ?? "UNKNOWN"),
|
|
1146
|
+
detectionId,
|
|
1147
|
+
reason: typeof status.DetectionStatusReason === "string" ? status.DetectionStatusReason : "CloudFormation drift detection failed",
|
|
1148
|
+
resources: []
|
|
1149
|
+
};
|
|
1150
|
+
const stackDriftStatus = String(status.StackDriftStatus ?? "UNKNOWN");
|
|
1151
|
+
if (stackDriftStatus !== "DRIFTED")
|
|
1152
|
+
return {
|
|
1153
|
+
ok: stackDriftStatus === "IN_SYNC",
|
|
1154
|
+
status: stackDriftStatus,
|
|
1155
|
+
detectionId,
|
|
1156
|
+
resources: []
|
|
1157
|
+
};
|
|
1158
|
+
const described = parseJson(
|
|
1159
|
+
await dependencies.execute("aws", [
|
|
1160
|
+
"cloudformation",
|
|
1161
|
+
"describe-stack-resource-drifts",
|
|
1162
|
+
"--stack-name",
|
|
1163
|
+
stackName2,
|
|
1164
|
+
"--stack-resource-drift-status-filters",
|
|
1165
|
+
"MODIFIED",
|
|
1166
|
+
"DELETED",
|
|
1167
|
+
"--region",
|
|
1168
|
+
region2,
|
|
1169
|
+
"--output",
|
|
1170
|
+
"json",
|
|
1171
|
+
"--no-cli-pager"
|
|
1172
|
+
]),
|
|
1173
|
+
"describe-stack-resource-drifts"
|
|
1174
|
+
);
|
|
1175
|
+
const resources = Array.isArray(described.StackResourceDrifts) ? described.StackResourceDrifts.flatMap((entry) => {
|
|
1176
|
+
if (!entry || typeof entry !== "object") return [];
|
|
1177
|
+
const drift = entry;
|
|
1178
|
+
if (typeof drift.LogicalResourceId !== "string") return [];
|
|
1179
|
+
const differences = Array.isArray(drift.PropertyDifferences) ? drift.PropertyDifferences.flatMap((item) => {
|
|
1180
|
+
if (!item || typeof item !== "object") return [];
|
|
1181
|
+
const difference = item;
|
|
1182
|
+
return [
|
|
1183
|
+
{
|
|
1184
|
+
...typeof difference.PropertyPath === "string" ? { path: difference.PropertyPath } : {},
|
|
1185
|
+
...typeof difference.DifferenceType === "string" ? { differenceType: difference.DifferenceType } : {},
|
|
1186
|
+
...typeof difference.ExpectedValue === "string" ? { expectedValue: difference.ExpectedValue } : {},
|
|
1187
|
+
...typeof difference.ActualValue === "string" ? { actualValue: difference.ActualValue } : {}
|
|
1188
|
+
}
|
|
1189
|
+
];
|
|
1190
|
+
}) : [];
|
|
1191
|
+
return [
|
|
1192
|
+
{
|
|
1193
|
+
logicalId: drift.LogicalResourceId,
|
|
1194
|
+
...typeof drift.ResourceType === "string" ? { resourceType: drift.ResourceType } : {},
|
|
1195
|
+
...typeof drift.StackResourceDriftStatus === "string" ? { status: drift.StackResourceDriftStatus } : {},
|
|
1196
|
+
differences
|
|
1197
|
+
}
|
|
1198
|
+
];
|
|
1199
|
+
}) : [];
|
|
1200
|
+
return { ok: false, status: stackDriftStatus, detectionId, resources };
|
|
1201
|
+
} catch (error) {
|
|
1202
|
+
return { ok: false, status: "ERROR", reason: message(error), resources: [] };
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
async function checkBootstrap(config, options, dependencies) {
|
|
1206
|
+
const requiredManifestValues = [
|
|
1207
|
+
["region", config.region],
|
|
1208
|
+
["core.stackName", config.core?.stackName],
|
|
1209
|
+
["core.artifactBucketName", config.core?.artifactBucketName],
|
|
1210
|
+
["core.applicationStackName", config.core?.applicationStackName],
|
|
1211
|
+
["core.dashboardFunctionName", config.core?.dashboardFunctionName],
|
|
1212
|
+
["githubOidc.stackName", config.githubOidc?.stackName],
|
|
1213
|
+
["githubOidc.providerArn", config.githubOidc?.providerArn],
|
|
1214
|
+
["githubOidc.repository", config.githubOidc?.repository],
|
|
1215
|
+
["githubOidc.ownerId", config.githubOidc?.ownerId],
|
|
1216
|
+
["githubOidc.repositoryId", config.githubOidc?.repositoryId],
|
|
1217
|
+
["githubOidc.environment", config.githubOidc?.environment]
|
|
1218
|
+
];
|
|
1219
|
+
const missing = requiredManifestValues.filter(([, value2]) => !value2).map(([name]) => name);
|
|
1220
|
+
if (missing.length) throw new Error(`Bootstrap manifest is missing: ${missing.join(", ")}`);
|
|
1221
|
+
const stackNames = {
|
|
1222
|
+
core: config.core?.stackName,
|
|
1223
|
+
"github-oidc": config.githubOidc?.stackName
|
|
1224
|
+
};
|
|
1225
|
+
const plan = await bootstrapPlan(config);
|
|
1226
|
+
const responses = await Promise.all(
|
|
1227
|
+
["core", "github-oidc"].map(async (kind) => {
|
|
1228
|
+
const stackName2 = stackNames[kind];
|
|
1229
|
+
try {
|
|
1230
|
+
const response = parseJson(
|
|
1231
|
+
await dependencies.execute("aws", [
|
|
1232
|
+
"cloudformation",
|
|
1233
|
+
"describe-stacks",
|
|
1234
|
+
"--stack-name",
|
|
1235
|
+
stackName2,
|
|
1236
|
+
"--region",
|
|
1237
|
+
config.region,
|
|
1238
|
+
"--output",
|
|
1239
|
+
"json",
|
|
1240
|
+
"--no-cli-pager"
|
|
1241
|
+
]),
|
|
1242
|
+
"describe-stacks"
|
|
1243
|
+
);
|
|
1244
|
+
return { kind, stackName: stackName2, response };
|
|
1245
|
+
} catch (error) {
|
|
1246
|
+
return { kind, stackName: stackName2, error: message(error) };
|
|
1247
|
+
}
|
|
1248
|
+
})
|
|
1249
|
+
);
|
|
1250
|
+
const coreResponse = responses.find(({ kind }) => kind === "core")?.response;
|
|
1251
|
+
const coreOutputValues = outputs(stackFrom(coreResponse ?? {}));
|
|
1252
|
+
const stacks = await Promise.all(
|
|
1253
|
+
responses.map(async ({ kind, stackName: stackName2, response, error }) => {
|
|
1254
|
+
const template = plan.packageTemplates.find((candidate) => candidate.kind === kind);
|
|
1255
|
+
if (!template) throw new Error(`Installed package has no ${kind} bootstrap template`);
|
|
1256
|
+
const stack = stackFrom(response ?? {});
|
|
1257
|
+
const consistency = error ? { ok: false, mismatches: [`unable to describe stack: ${error}`] } : bootstrapConsistency(
|
|
1258
|
+
kind,
|
|
1259
|
+
config,
|
|
1260
|
+
response,
|
|
1261
|
+
template.contractVersion,
|
|
1262
|
+
template.templateRevision,
|
|
1263
|
+
coreOutputValues
|
|
1264
|
+
);
|
|
1265
|
+
const checked = {
|
|
1266
|
+
kind,
|
|
1267
|
+
stackName: stackName2,
|
|
1268
|
+
...typeof stack?.StackStatus === "string" ? { stackStatus: stack.StackStatus } : {},
|
|
1269
|
+
contractVersion: template.contractVersion,
|
|
1270
|
+
templateRevision: template.templateRevision,
|
|
1271
|
+
templateSha256: template.sha256,
|
|
1272
|
+
consistency
|
|
1273
|
+
};
|
|
1274
|
+
if (options.resourceDrift && !error)
|
|
1275
|
+
checked.resourceDrift = await resourceDrift(
|
|
1276
|
+
stackName2,
|
|
1277
|
+
config.region,
|
|
1278
|
+
dependencies
|
|
1279
|
+
);
|
|
1280
|
+
return checked;
|
|
1281
|
+
})
|
|
1282
|
+
);
|
|
1283
|
+
return {
|
|
1284
|
+
ok: stacks.every(
|
|
1285
|
+
({ consistency, resourceDrift: drift }) => consistency.ok && (!drift || drift.ok)
|
|
1286
|
+
),
|
|
1287
|
+
packageVersion: plan.packageVersion,
|
|
1288
|
+
stacks
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
|
|
922
1292
|
// packages/aws/src/handoff.ts
|
|
923
1293
|
var githubSubjectKeys = [
|
|
924
1294
|
"repository_owner",
|
|
@@ -927,7 +1297,7 @@ var githubSubjectKeys = [
|
|
|
927
1297
|
"repository_id",
|
|
928
1298
|
"context"
|
|
929
1299
|
];
|
|
930
|
-
function
|
|
1300
|
+
function outputValues2(stack) {
|
|
931
1301
|
return Object.fromEntries(
|
|
932
1302
|
(stack.Outputs ?? []).filter(
|
|
933
1303
|
(entry) => typeof entry.OutputKey === "string" && typeof entry.OutputValue === "string"
|
|
@@ -963,6 +1333,9 @@ function stackName(config, kind) {
|
|
|
963
1333
|
`${kind}.stackName`
|
|
964
1334
|
);
|
|
965
1335
|
}
|
|
1336
|
+
function workFile(workDir, name) {
|
|
1337
|
+
return workDir ? `${workDir.replace(/\/+$/, "")}/${name}` : name;
|
|
1338
|
+
}
|
|
966
1339
|
function changeSetCommands(input) {
|
|
967
1340
|
const name = `${input.kind}-initial-review`;
|
|
968
1341
|
const stack = stackName(input.config, input.kind);
|
|
@@ -1036,7 +1409,7 @@ function changeSetCommands(input) {
|
|
|
1036
1409
|
},
|
|
1037
1410
|
{
|
|
1038
1411
|
name: "capture-stack",
|
|
1039
|
-
captureFile: `${input.kind}-deployed-stack.json
|
|
1412
|
+
captureFile: workFile(input.workDir, `${input.kind}-deployed-stack.json`),
|
|
1040
1413
|
args: ["aws", "cloudformation", "describe-stacks", ...common, "--no-cli-pager"]
|
|
1041
1414
|
}
|
|
1042
1415
|
];
|
|
@@ -1090,7 +1463,7 @@ async function contracts() {
|
|
|
1090
1463
|
}
|
|
1091
1464
|
async function bootstrapHandoff(input) {
|
|
1092
1465
|
const expectedContracts = await contracts();
|
|
1093
|
-
const coreCapturePath = input.coreStackPath ?? "core-deployed-stack.json";
|
|
1466
|
+
const coreCapturePath = input.coreStackPath ?? workFile(input.workDir, "core-deployed-stack.json");
|
|
1094
1467
|
const core = input.coreStack === void 0 ? void 0 : deployedBootstrapStack(input.coreStack, expectedContracts.core);
|
|
1095
1468
|
const oidc = input.githubOidcStack === void 0 ? void 0 : deployedBootstrapStack(input.githubOidcStack, expectedContracts.githubOidc);
|
|
1096
1469
|
if (!core) {
|
|
@@ -1104,9 +1477,10 @@ async function bootstrapHandoff(input) {
|
|
|
1104
1477
|
phase: "core",
|
|
1105
1478
|
expectedContracts,
|
|
1106
1479
|
templatePath,
|
|
1107
|
-
parameterPath: "core-bootstrap.json",
|
|
1480
|
+
parameterPath: workFile(input.workDir, "core-bootstrap.json"),
|
|
1108
1481
|
expectedOutputs: [
|
|
1109
1482
|
"BootstrapContractVersion",
|
|
1483
|
+
"BootstrapTemplateRevision",
|
|
1110
1484
|
"ArtifactBucketName",
|
|
1111
1485
|
"ApplicationStackName",
|
|
1112
1486
|
"CloudFormationExecutionRoleArn"
|
|
@@ -1121,7 +1495,8 @@ async function bootstrapHandoff(input) {
|
|
|
1121
1495
|
config: input.config,
|
|
1122
1496
|
configPath: input.configPath,
|
|
1123
1497
|
templatePath,
|
|
1124
|
-
parameterPath: "core-bootstrap.json"
|
|
1498
|
+
parameterPath: workFile(input.workDir, "core-bootstrap.json"),
|
|
1499
|
+
workDir: input.workDir
|
|
1125
1500
|
})
|
|
1126
1501
|
};
|
|
1127
1502
|
}
|
|
@@ -1133,6 +1508,7 @@ async function bootstrapHandoff(input) {
|
|
|
1133
1508
|
GitHubOwnerId: input.config.githubOidc?.ownerId,
|
|
1134
1509
|
GitHubRepositoryId: input.config.githubOidc?.repositoryId,
|
|
1135
1510
|
GitHubEnvironment: input.config.githubOidc?.environment,
|
|
1511
|
+
CoreBootstrapStackName: input.config.core?.stackName,
|
|
1136
1512
|
ConsumerGatewayStackName: input.config.githubOidc?.consumerGatewayStackName,
|
|
1137
1513
|
...coreBootstrapOutputs(core)
|
|
1138
1514
|
});
|
|
@@ -1141,7 +1517,7 @@ async function bootstrapHandoff(input) {
|
|
|
1141
1517
|
phase: "github-oidc",
|
|
1142
1518
|
expectedContracts,
|
|
1143
1519
|
templatePath,
|
|
1144
|
-
parameterPath: "github-oidc-bootstrap.json",
|
|
1520
|
+
parameterPath: workFile(input.workDir, "github-oidc-bootstrap.json"),
|
|
1145
1521
|
expectedOutputs: [
|
|
1146
1522
|
"BootstrapContractVersion",
|
|
1147
1523
|
"BootstrapTemplateRevision",
|
|
@@ -1157,8 +1533,9 @@ async function bootstrapHandoff(input) {
|
|
|
1157
1533
|
config: input.config,
|
|
1158
1534
|
configPath: input.configPath,
|
|
1159
1535
|
templatePath,
|
|
1160
|
-
parameterPath: "github-oidc-bootstrap.json",
|
|
1161
|
-
coreCapturePath
|
|
1536
|
+
parameterPath: workFile(input.workDir, "github-oidc-bootstrap.json"),
|
|
1537
|
+
coreCapturePath,
|
|
1538
|
+
workDir: input.workDir
|
|
1162
1539
|
}),
|
|
1163
1540
|
prerequisite: await (input.provider ?? githubOidcProvider).prerequisite(
|
|
1164
1541
|
input.config,
|
|
@@ -1170,12 +1547,13 @@ async function bootstrapHandoff(input) {
|
|
|
1170
1547
|
input.config,
|
|
1171
1548
|
input.runner
|
|
1172
1549
|
);
|
|
1550
|
+
const githubOidcCapturePath = input.githubOidcStackPath ?? workFile(input.workDir, "github-oidc-deployed-stack.json");
|
|
1173
1551
|
if (prerequisite.status !== "ready")
|
|
1174
1552
|
return {
|
|
1175
1553
|
phase: "github-environment",
|
|
1176
1554
|
expectedContracts,
|
|
1177
1555
|
expectedOutputs: [],
|
|
1178
|
-
requiredCapturedFiles: [coreCapturePath,
|
|
1556
|
+
requiredCapturedFiles: [coreCapturePath, githubOidcCapturePath],
|
|
1179
1557
|
reviewCheckpoints: [
|
|
1180
1558
|
"A GitHub administrator must complete and verify the immutable-subject migration before deployments."
|
|
1181
1559
|
],
|
|
@@ -1186,7 +1564,7 @@ async function bootstrapHandoff(input) {
|
|
|
1186
1564
|
phase: "application-gateway",
|
|
1187
1565
|
expectedContracts,
|
|
1188
1566
|
expectedOutputs: [],
|
|
1189
|
-
requiredCapturedFiles: [coreCapturePath,
|
|
1567
|
+
requiredCapturedFiles: [coreCapturePath, githubOidcCapturePath],
|
|
1190
1568
|
reviewCheckpoints: [
|
|
1191
1569
|
"Consumer owns gateway selection, private Lambda permission, authentication, and smoke tests."
|
|
1192
1570
|
],
|
|
@@ -1267,7 +1645,7 @@ async function verifyBootstrap(input) {
|
|
|
1267
1645
|
const [owner, repository, ...extra] = (github?.repository ?? "").split("/");
|
|
1268
1646
|
if (!owner || !repository || extra.length)
|
|
1269
1647
|
throw new Error("GitHub repository must be owner/repository");
|
|
1270
|
-
const deployRole =
|
|
1648
|
+
const deployRole = outputValues2(oidc).GitHubDeployRoleArn;
|
|
1271
1649
|
if (!deployRole) throw new Error("GitHub OIDC stack JSON is missing outputs: GitHubDeployRoleArn");
|
|
1272
1650
|
const providerArn = github?.providerArn ?? "";
|
|
1273
1651
|
const account = providerArn.match(/^arn:[^:]+:iam::(\d+):oidc-provider\//)?.[1];
|
|
@@ -1544,8 +1922,13 @@ function quote(args2) {
|
|
|
1544
1922
|
}
|
|
1545
1923
|
async function bootstrapGuide(input) {
|
|
1546
1924
|
const handoff = await bootstrapHandoff(input);
|
|
1925
|
+
const plan = await bootstrapPlan(input.config);
|
|
1547
1926
|
const lines = [
|
|
1548
1927
|
`Phase: ${handoff.phase}`,
|
|
1928
|
+
`Package version: ${plan.packageVersion}`,
|
|
1929
|
+
...plan.packageTemplates.map(
|
|
1930
|
+
(template) => `Template ${template.kind}: contract ${template.contractVersion}${template.templateRevision ? `, revision ${template.templateRevision}` : ""}, sha256 ${template.sha256}`
|
|
1931
|
+
),
|
|
1549
1932
|
`Required captures: ${handoff.requiredCapturedFiles.join(", ") || "none"}`
|
|
1550
1933
|
];
|
|
1551
1934
|
if (handoff.expectedOutputs.length)
|
|
@@ -1558,7 +1941,9 @@ async function bootstrapGuide(input) {
|
|
|
1558
1941
|
lines.push("PAUSE: review the change set before approval.");
|
|
1559
1942
|
if (command.name === "execute-reviewed-change-set")
|
|
1560
1943
|
lines.push("PAUSE: execute only after explicit approval.");
|
|
1561
|
-
lines.push(
|
|
1944
|
+
lines.push(
|
|
1945
|
+
command.captureFile ? `${quote(command.args)} > ${quote([command.captureFile])}` : quote(command.args)
|
|
1946
|
+
);
|
|
1562
1947
|
}
|
|
1563
1948
|
if (handoff.reviewCheckpoints.length)
|
|
1564
1949
|
lines.push("", "Review pauses:", ...handoff.reviewCheckpoints.map((item) => `- ${item}`));
|
|
@@ -1939,16 +2324,17 @@ function bootstrapValues(config, coreOutputs = {}) {
|
|
|
1939
2324
|
coreOutputs.ApplicationStackName ?? config.core?.applicationStackName
|
|
1940
2325
|
),
|
|
1941
2326
|
DashboardFunctionName: option("--function-name", config.core?.dashboardFunctionName),
|
|
1942
|
-
RuntimeSecretArn: option("--runtime-secret-arn", config.core?.runtimeSecretArn),
|
|
1943
|
-
ArtifactKmsKeyArn: option("--artifact-kms-key-arn", config.core?.artifactKmsKeyArn),
|
|
2327
|
+
RuntimeSecretArn: option("--runtime-secret-arn", config.core?.runtimeSecretArn ?? ""),
|
|
2328
|
+
ArtifactKmsKeyArn: option("--artifact-kms-key-arn", config.core?.artifactKmsKeyArn ?? ""),
|
|
1944
2329
|
GitHubOidcProviderArn: option("--github-oidc-provider-arn", config.githubOidc?.providerArn),
|
|
1945
2330
|
GitHubRepository: option("--github-repository", config.githubOidc?.repository),
|
|
1946
2331
|
GitHubOwnerId: option("--github-owner-id", config.githubOidc?.ownerId),
|
|
1947
2332
|
GitHubRepositoryId: option("--github-repository-id", config.githubOidc?.repositoryId),
|
|
1948
2333
|
GitHubEnvironment: option("--github-environment", config.githubOidc?.environment),
|
|
2334
|
+
CoreBootstrapStackName: config.core?.stackName,
|
|
1949
2335
|
ConsumerGatewayStackName: option(
|
|
1950
2336
|
"--consumer-gateway-stack",
|
|
1951
|
-
config.githubOidc?.consumerGatewayStackName
|
|
2337
|
+
config.githubOidc?.consumerGatewayStackName ?? ""
|
|
1952
2338
|
),
|
|
1953
2339
|
CloudFormationExecutionRoleArn: option(
|
|
1954
2340
|
"--execution-role-arn",
|
|
@@ -2149,9 +2535,11 @@ try {
|
|
|
2149
2535
|
await bootstrapGuide({
|
|
2150
2536
|
config,
|
|
2151
2537
|
configPath,
|
|
2538
|
+
workDir: option("--work-dir"),
|
|
2152
2539
|
coreStack: coreStackPath ? JSON.parse(await readFile5(coreStackPath, "utf8")) : void 0,
|
|
2153
2540
|
coreStackPath,
|
|
2154
2541
|
githubOidcStack: githubStackPath ? JSON.parse(await readFile5(githubStackPath, "utf8")) : void 0,
|
|
2542
|
+
githubOidcStackPath: githubStackPath,
|
|
2155
2543
|
runner
|
|
2156
2544
|
})
|
|
2157
2545
|
);
|
|
@@ -2171,9 +2559,11 @@ try {
|
|
|
2171
2559
|
await bootstrapHandoff({
|
|
2172
2560
|
config,
|
|
2173
2561
|
configPath,
|
|
2562
|
+
workDir: option("--work-dir"),
|
|
2174
2563
|
coreStack: coreStackPath ? JSON.parse(await readFile5(coreStackPath, "utf8")) : void 0,
|
|
2175
2564
|
coreStackPath,
|
|
2176
2565
|
githubOidcStack: githubStackPath ? JSON.parse(await readFile5(githubStackPath, "utf8")) : void 0,
|
|
2566
|
+
githubOidcStackPath: githubStackPath,
|
|
2177
2567
|
runner
|
|
2178
2568
|
})
|
|
2179
2569
|
)
|
|
@@ -2200,6 +2590,42 @@ try {
|
|
|
2200
2590
|
contractVersion: bootstrapContractVersion(await bootstrapTemplate(kind))
|
|
2201
2591
|
})
|
|
2202
2592
|
);
|
|
2593
|
+
} else if (action === "plan") {
|
|
2594
|
+
requiredOption("--config");
|
|
2595
|
+
const plan = await bootstrapPlan(config);
|
|
2596
|
+
if (option("--format") === "text") {
|
|
2597
|
+
console.log("AWS bootstrap plan (read-only)");
|
|
2598
|
+
console.log(`Package version: ${plan.packageVersion}`);
|
|
2599
|
+
for (const template of plan.packageTemplates) {
|
|
2600
|
+
console.log(`
|
|
2601
|
+
${template.kind}: ${template.path}`);
|
|
2602
|
+
console.log(` contract: ${template.contractVersion}`);
|
|
2603
|
+
if (template.templateRevision) console.log(` revision: ${template.templateRevision}`);
|
|
2604
|
+
console.log(` sha256: ${template.sha256}`);
|
|
2605
|
+
console.log(
|
|
2606
|
+
` resources: ${template.resources.map(({ logicalId, type }) => `${logicalId} (${type})`).join(", ")}`
|
|
2607
|
+
);
|
|
2608
|
+
console.log(` declared IAM actions: ${template.iamActions.join(", ") || "none"}`);
|
|
2609
|
+
}
|
|
2610
|
+
console.log(`
|
|
2611
|
+
${plan.notes.join("\n")}`);
|
|
2612
|
+
} else console.log(JSON.stringify(plan));
|
|
2613
|
+
} else if (action === "check") {
|
|
2614
|
+
requiredOption("--config");
|
|
2615
|
+
const { execFile: execFile3 } = await import("node:child_process");
|
|
2616
|
+
const { promisify: promisify3 } = await import("node:util");
|
|
2617
|
+
const result = await checkBootstrap(
|
|
2618
|
+
config,
|
|
2619
|
+
{ resourceDrift: args.includes("--resource-drift") },
|
|
2620
|
+
{
|
|
2621
|
+
async execute(command, commandArgs) {
|
|
2622
|
+
return (await promisify3(execFile3)(command, commandArgs)).stdout.trim();
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
);
|
|
2626
|
+
if (option("--format") === "text") process.stdout.write(formatBootstrapCheckText(result));
|
|
2627
|
+
else console.log(JSON.stringify(result));
|
|
2628
|
+
if (!result.ok) process.exitCode = 1;
|
|
2203
2629
|
} else if (action === "parameters") {
|
|
2204
2630
|
const kind = bootstrapKind();
|
|
2205
2631
|
const output = option("--output", `aws-dashboard-bootstrap-${kind}.json`) ?? `aws-dashboard-bootstrap-${kind}.json`;
|
|
@@ -2234,33 +2660,14 @@ try {
|
|
|
2234
2660
|
console.log(
|
|
2235
2661
|
JSON.stringify({ output, kind, preservedDeployedValues: Boolean(deployedStackPath) })
|
|
2236
2662
|
);
|
|
2237
|
-
} else if (action === "status") {
|
|
2238
|
-
const kind = bootstrapKind();
|
|
2239
|
-
const stackName2 = requiredOption("--stack-name", bootstrapStackName(kind, config));
|
|
2240
|
-
const region2 = option("--region", config.region);
|
|
2241
|
-
const awsCommand = [
|
|
2242
|
-
"aws",
|
|
2243
|
-
"cloudformation",
|
|
2244
|
-
"describe-stacks",
|
|
2245
|
-
"--stack-name",
|
|
2246
|
-
stackName2,
|
|
2247
|
-
...region2 ? ["--region", region2] : [],
|
|
2248
|
-
"--no-cli-pager"
|
|
2249
|
-
];
|
|
2250
|
-
console.log(
|
|
2251
|
-
JSON.stringify({
|
|
2252
|
-
kind,
|
|
2253
|
-
contractVersion: bootstrapContractVersion(await bootstrapTemplate(kind)),
|
|
2254
|
-
awsCommand,
|
|
2255
|
-
shellCommand: args.includes("--format-shell") ? shellCommand(awsCommand) : void 0
|
|
2256
|
-
})
|
|
2257
|
-
);
|
|
2258
2663
|
} else if (action === "change-set") {
|
|
2259
2664
|
const kind = bootstrapKind();
|
|
2260
2665
|
const stackName2 = requiredOption("--stack-name", bootstrapStackName(kind, config));
|
|
2261
2666
|
const changeSetName = requiredOption("--change-set-name");
|
|
2262
2667
|
const parametersPath = requiredOption("--parameters");
|
|
2263
2668
|
const region2 = option("--region", config.region);
|
|
2669
|
+
const plan = await bootstrapPlan(config);
|
|
2670
|
+
const template = plan.packageTemplates.find((candidate) => candidate.kind === kind);
|
|
2264
2671
|
await existingParameters(parametersPath);
|
|
2265
2672
|
const awsCommand = [
|
|
2266
2673
|
"aws",
|
|
@@ -2284,6 +2691,10 @@ try {
|
|
|
2284
2691
|
console.log(
|
|
2285
2692
|
JSON.stringify({
|
|
2286
2693
|
kind,
|
|
2694
|
+
packageVersion: plan.packageVersion,
|
|
2695
|
+
contractVersion: template?.contractVersion,
|
|
2696
|
+
templateRevision: template?.templateRevision,
|
|
2697
|
+
templateSha256: template?.sha256,
|
|
2287
2698
|
reviewRequired: true,
|
|
2288
2699
|
awsCommand,
|
|
2289
2700
|
shellCommand: args.includes("--format-shell") ? shellCommand(awsCommand) : void 0
|
|
@@ -2291,7 +2702,7 @@ try {
|
|
|
2291
2702
|
);
|
|
2292
2703
|
} else {
|
|
2293
2704
|
throw new Error(
|
|
2294
|
-
"Usage: ze-great-dashboard-aws bootstrap init|preflight|guide|handoff|verify --config manifest.json [options], or bootstrap template|parameters|
|
|
2705
|
+
"Usage: ze-great-dashboard-aws bootstrap init|preflight|plan|check|guide|handoff|verify --config manifest.json [options], or bootstrap template|parameters|change-set --kind core|github-oidc [options]"
|
|
2295
2706
|
);
|
|
2296
2707
|
}
|
|
2297
2708
|
}
|
package/dist/guided.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type BootstrapConfig } from './bootstrap.js';
|
|
2
2
|
import { type BootstrapProvider, bootstrapHandoff, type CommandRunner } from './handoff.js';
|
|
3
3
|
export type BootstrapCheckStatus = 'ready' | 'missing' | 'mismatch' | 'unverified';
|
|
4
4
|
export type BootstrapPreflightCheck = {
|