@continuous-excellence/ze-great-dashboard-aws 0.5.1 → 0.6.1
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 +52 -129
- 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 +451 -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 +404 -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,11 +1333,17 @@ 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);
|
|
969
1342
|
const common = ["--stack-name", stack, "--region", region(input.config)];
|
|
970
1343
|
const parameters = [
|
|
1344
|
+
"npm",
|
|
1345
|
+
"exec",
|
|
1346
|
+
"--",
|
|
971
1347
|
"ze-great-dashboard-aws",
|
|
972
1348
|
"bootstrap",
|
|
973
1349
|
"parameters",
|
|
@@ -1036,7 +1412,7 @@ function changeSetCommands(input) {
|
|
|
1036
1412
|
},
|
|
1037
1413
|
{
|
|
1038
1414
|
name: "capture-stack",
|
|
1039
|
-
captureFile: `${input.kind}-deployed-stack.json
|
|
1415
|
+
captureFile: workFile(input.workDir, `${input.kind}-deployed-stack.json`),
|
|
1040
1416
|
args: ["aws", "cloudformation", "describe-stacks", ...common, "--no-cli-pager"]
|
|
1041
1417
|
}
|
|
1042
1418
|
];
|
|
@@ -1090,7 +1466,7 @@ async function contracts() {
|
|
|
1090
1466
|
}
|
|
1091
1467
|
async function bootstrapHandoff(input) {
|
|
1092
1468
|
const expectedContracts = await contracts();
|
|
1093
|
-
const coreCapturePath = input.coreStackPath ?? "core-deployed-stack.json";
|
|
1469
|
+
const coreCapturePath = input.coreStackPath ?? workFile(input.workDir, "core-deployed-stack.json");
|
|
1094
1470
|
const core = input.coreStack === void 0 ? void 0 : deployedBootstrapStack(input.coreStack, expectedContracts.core);
|
|
1095
1471
|
const oidc = input.githubOidcStack === void 0 ? void 0 : deployedBootstrapStack(input.githubOidcStack, expectedContracts.githubOidc);
|
|
1096
1472
|
if (!core) {
|
|
@@ -1104,9 +1480,10 @@ async function bootstrapHandoff(input) {
|
|
|
1104
1480
|
phase: "core",
|
|
1105
1481
|
expectedContracts,
|
|
1106
1482
|
templatePath,
|
|
1107
|
-
parameterPath: "core-bootstrap.json",
|
|
1483
|
+
parameterPath: workFile(input.workDir, "core-bootstrap.json"),
|
|
1108
1484
|
expectedOutputs: [
|
|
1109
1485
|
"BootstrapContractVersion",
|
|
1486
|
+
"BootstrapTemplateRevision",
|
|
1110
1487
|
"ArtifactBucketName",
|
|
1111
1488
|
"ApplicationStackName",
|
|
1112
1489
|
"CloudFormationExecutionRoleArn"
|
|
@@ -1121,7 +1498,8 @@ async function bootstrapHandoff(input) {
|
|
|
1121
1498
|
config: input.config,
|
|
1122
1499
|
configPath: input.configPath,
|
|
1123
1500
|
templatePath,
|
|
1124
|
-
parameterPath: "core-bootstrap.json"
|
|
1501
|
+
parameterPath: workFile(input.workDir, "core-bootstrap.json"),
|
|
1502
|
+
workDir: input.workDir
|
|
1125
1503
|
})
|
|
1126
1504
|
};
|
|
1127
1505
|
}
|
|
@@ -1133,6 +1511,7 @@ async function bootstrapHandoff(input) {
|
|
|
1133
1511
|
GitHubOwnerId: input.config.githubOidc?.ownerId,
|
|
1134
1512
|
GitHubRepositoryId: input.config.githubOidc?.repositoryId,
|
|
1135
1513
|
GitHubEnvironment: input.config.githubOidc?.environment,
|
|
1514
|
+
CoreBootstrapStackName: input.config.core?.stackName,
|
|
1136
1515
|
ConsumerGatewayStackName: input.config.githubOidc?.consumerGatewayStackName,
|
|
1137
1516
|
...coreBootstrapOutputs(core)
|
|
1138
1517
|
});
|
|
@@ -1141,7 +1520,7 @@ async function bootstrapHandoff(input) {
|
|
|
1141
1520
|
phase: "github-oidc",
|
|
1142
1521
|
expectedContracts,
|
|
1143
1522
|
templatePath,
|
|
1144
|
-
parameterPath: "github-oidc-bootstrap.json",
|
|
1523
|
+
parameterPath: workFile(input.workDir, "github-oidc-bootstrap.json"),
|
|
1145
1524
|
expectedOutputs: [
|
|
1146
1525
|
"BootstrapContractVersion",
|
|
1147
1526
|
"BootstrapTemplateRevision",
|
|
@@ -1157,8 +1536,9 @@ async function bootstrapHandoff(input) {
|
|
|
1157
1536
|
config: input.config,
|
|
1158
1537
|
configPath: input.configPath,
|
|
1159
1538
|
templatePath,
|
|
1160
|
-
parameterPath: "github-oidc-bootstrap.json",
|
|
1161
|
-
coreCapturePath
|
|
1539
|
+
parameterPath: workFile(input.workDir, "github-oidc-bootstrap.json"),
|
|
1540
|
+
coreCapturePath,
|
|
1541
|
+
workDir: input.workDir
|
|
1162
1542
|
}),
|
|
1163
1543
|
prerequisite: await (input.provider ?? githubOidcProvider).prerequisite(
|
|
1164
1544
|
input.config,
|
|
@@ -1170,12 +1550,13 @@ async function bootstrapHandoff(input) {
|
|
|
1170
1550
|
input.config,
|
|
1171
1551
|
input.runner
|
|
1172
1552
|
);
|
|
1553
|
+
const githubOidcCapturePath = input.githubOidcStackPath ?? workFile(input.workDir, "github-oidc-deployed-stack.json");
|
|
1173
1554
|
if (prerequisite.status !== "ready")
|
|
1174
1555
|
return {
|
|
1175
1556
|
phase: "github-environment",
|
|
1176
1557
|
expectedContracts,
|
|
1177
1558
|
expectedOutputs: [],
|
|
1178
|
-
requiredCapturedFiles: [coreCapturePath,
|
|
1559
|
+
requiredCapturedFiles: [coreCapturePath, githubOidcCapturePath],
|
|
1179
1560
|
reviewCheckpoints: [
|
|
1180
1561
|
"A GitHub administrator must complete and verify the immutable-subject migration before deployments."
|
|
1181
1562
|
],
|
|
@@ -1186,7 +1567,7 @@ async function bootstrapHandoff(input) {
|
|
|
1186
1567
|
phase: "application-gateway",
|
|
1187
1568
|
expectedContracts,
|
|
1188
1569
|
expectedOutputs: [],
|
|
1189
|
-
requiredCapturedFiles: [coreCapturePath,
|
|
1570
|
+
requiredCapturedFiles: [coreCapturePath, githubOidcCapturePath],
|
|
1190
1571
|
reviewCheckpoints: [
|
|
1191
1572
|
"Consumer owns gateway selection, private Lambda permission, authentication, and smoke tests."
|
|
1192
1573
|
],
|
|
@@ -1267,7 +1648,7 @@ async function verifyBootstrap(input) {
|
|
|
1267
1648
|
const [owner, repository, ...extra] = (github?.repository ?? "").split("/");
|
|
1268
1649
|
if (!owner || !repository || extra.length)
|
|
1269
1650
|
throw new Error("GitHub repository must be owner/repository");
|
|
1270
|
-
const deployRole =
|
|
1651
|
+
const deployRole = outputValues2(oidc).GitHubDeployRoleArn;
|
|
1271
1652
|
if (!deployRole) throw new Error("GitHub OIDC stack JSON is missing outputs: GitHubDeployRoleArn");
|
|
1272
1653
|
const providerArn = github?.providerArn ?? "";
|
|
1273
1654
|
const account = providerArn.match(/^arn:[^:]+:iam::(\d+):oidc-provider\//)?.[1];
|
|
@@ -1544,8 +1925,13 @@ function quote(args2) {
|
|
|
1544
1925
|
}
|
|
1545
1926
|
async function bootstrapGuide(input) {
|
|
1546
1927
|
const handoff = await bootstrapHandoff(input);
|
|
1928
|
+
const plan = await bootstrapPlan(input.config);
|
|
1547
1929
|
const lines = [
|
|
1548
1930
|
`Phase: ${handoff.phase}`,
|
|
1931
|
+
`Package version: ${plan.packageVersion}`,
|
|
1932
|
+
...plan.packageTemplates.map(
|
|
1933
|
+
(template) => `Template ${template.kind}: contract ${template.contractVersion}${template.templateRevision ? `, revision ${template.templateRevision}` : ""}, sha256 ${template.sha256}`
|
|
1934
|
+
),
|
|
1549
1935
|
`Required captures: ${handoff.requiredCapturedFiles.join(", ") || "none"}`
|
|
1550
1936
|
];
|
|
1551
1937
|
if (handoff.expectedOutputs.length)
|
|
@@ -1558,7 +1944,9 @@ async function bootstrapGuide(input) {
|
|
|
1558
1944
|
lines.push("PAUSE: review the change set before approval.");
|
|
1559
1945
|
if (command.name === "execute-reviewed-change-set")
|
|
1560
1946
|
lines.push("PAUSE: execute only after explicit approval.");
|
|
1561
|
-
lines.push(
|
|
1947
|
+
lines.push(
|
|
1948
|
+
command.captureFile ? `${quote(command.args)} > ${quote([command.captureFile])}` : quote(command.args)
|
|
1949
|
+
);
|
|
1562
1950
|
}
|
|
1563
1951
|
if (handoff.reviewCheckpoints.length)
|
|
1564
1952
|
lines.push("", "Review pauses:", ...handoff.reviewCheckpoints.map((item) => `- ${item}`));
|
|
@@ -1939,16 +2327,17 @@ function bootstrapValues(config, coreOutputs = {}) {
|
|
|
1939
2327
|
coreOutputs.ApplicationStackName ?? config.core?.applicationStackName
|
|
1940
2328
|
),
|
|
1941
2329
|
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),
|
|
2330
|
+
RuntimeSecretArn: option("--runtime-secret-arn", config.core?.runtimeSecretArn ?? ""),
|
|
2331
|
+
ArtifactKmsKeyArn: option("--artifact-kms-key-arn", config.core?.artifactKmsKeyArn ?? ""),
|
|
1944
2332
|
GitHubOidcProviderArn: option("--github-oidc-provider-arn", config.githubOidc?.providerArn),
|
|
1945
2333
|
GitHubRepository: option("--github-repository", config.githubOidc?.repository),
|
|
1946
2334
|
GitHubOwnerId: option("--github-owner-id", config.githubOidc?.ownerId),
|
|
1947
2335
|
GitHubRepositoryId: option("--github-repository-id", config.githubOidc?.repositoryId),
|
|
1948
2336
|
GitHubEnvironment: option("--github-environment", config.githubOidc?.environment),
|
|
2337
|
+
CoreBootstrapStackName: config.core?.stackName,
|
|
1949
2338
|
ConsumerGatewayStackName: option(
|
|
1950
2339
|
"--consumer-gateway-stack",
|
|
1951
|
-
config.githubOidc?.consumerGatewayStackName
|
|
2340
|
+
config.githubOidc?.consumerGatewayStackName ?? ""
|
|
1952
2341
|
),
|
|
1953
2342
|
CloudFormationExecutionRoleArn: option(
|
|
1954
2343
|
"--execution-role-arn",
|
|
@@ -2149,9 +2538,11 @@ try {
|
|
|
2149
2538
|
await bootstrapGuide({
|
|
2150
2539
|
config,
|
|
2151
2540
|
configPath,
|
|
2541
|
+
workDir: option("--work-dir"),
|
|
2152
2542
|
coreStack: coreStackPath ? JSON.parse(await readFile5(coreStackPath, "utf8")) : void 0,
|
|
2153
2543
|
coreStackPath,
|
|
2154
2544
|
githubOidcStack: githubStackPath ? JSON.parse(await readFile5(githubStackPath, "utf8")) : void 0,
|
|
2545
|
+
githubOidcStackPath: githubStackPath,
|
|
2155
2546
|
runner
|
|
2156
2547
|
})
|
|
2157
2548
|
);
|
|
@@ -2171,9 +2562,11 @@ try {
|
|
|
2171
2562
|
await bootstrapHandoff({
|
|
2172
2563
|
config,
|
|
2173
2564
|
configPath,
|
|
2565
|
+
workDir: option("--work-dir"),
|
|
2174
2566
|
coreStack: coreStackPath ? JSON.parse(await readFile5(coreStackPath, "utf8")) : void 0,
|
|
2175
2567
|
coreStackPath,
|
|
2176
2568
|
githubOidcStack: githubStackPath ? JSON.parse(await readFile5(githubStackPath, "utf8")) : void 0,
|
|
2569
|
+
githubOidcStackPath: githubStackPath,
|
|
2177
2570
|
runner
|
|
2178
2571
|
})
|
|
2179
2572
|
)
|
|
@@ -2200,6 +2593,42 @@ try {
|
|
|
2200
2593
|
contractVersion: bootstrapContractVersion(await bootstrapTemplate(kind))
|
|
2201
2594
|
})
|
|
2202
2595
|
);
|
|
2596
|
+
} else if (action === "plan") {
|
|
2597
|
+
requiredOption("--config");
|
|
2598
|
+
const plan = await bootstrapPlan(config);
|
|
2599
|
+
if (option("--format") === "text") {
|
|
2600
|
+
console.log("AWS bootstrap plan (read-only)");
|
|
2601
|
+
console.log(`Package version: ${plan.packageVersion}`);
|
|
2602
|
+
for (const template of plan.packageTemplates) {
|
|
2603
|
+
console.log(`
|
|
2604
|
+
${template.kind}: ${template.path}`);
|
|
2605
|
+
console.log(` contract: ${template.contractVersion}`);
|
|
2606
|
+
if (template.templateRevision) console.log(` revision: ${template.templateRevision}`);
|
|
2607
|
+
console.log(` sha256: ${template.sha256}`);
|
|
2608
|
+
console.log(
|
|
2609
|
+
` resources: ${template.resources.map(({ logicalId, type }) => `${logicalId} (${type})`).join(", ")}`
|
|
2610
|
+
);
|
|
2611
|
+
console.log(` declared IAM actions: ${template.iamActions.join(", ") || "none"}`);
|
|
2612
|
+
}
|
|
2613
|
+
console.log(`
|
|
2614
|
+
${plan.notes.join("\n")}`);
|
|
2615
|
+
} else console.log(JSON.stringify(plan));
|
|
2616
|
+
} else if (action === "check") {
|
|
2617
|
+
requiredOption("--config");
|
|
2618
|
+
const { execFile: execFile3 } = await import("node:child_process");
|
|
2619
|
+
const { promisify: promisify3 } = await import("node:util");
|
|
2620
|
+
const result = await checkBootstrap(
|
|
2621
|
+
config,
|
|
2622
|
+
{ resourceDrift: args.includes("--resource-drift") },
|
|
2623
|
+
{
|
|
2624
|
+
async execute(command, commandArgs) {
|
|
2625
|
+
return (await promisify3(execFile3)(command, commandArgs)).stdout.trim();
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
);
|
|
2629
|
+
if (option("--format") === "text") process.stdout.write(formatBootstrapCheckText(result));
|
|
2630
|
+
else console.log(JSON.stringify(result));
|
|
2631
|
+
if (!result.ok) process.exitCode = 1;
|
|
2203
2632
|
} else if (action === "parameters") {
|
|
2204
2633
|
const kind = bootstrapKind();
|
|
2205
2634
|
const output = option("--output", `aws-dashboard-bootstrap-${kind}.json`) ?? `aws-dashboard-bootstrap-${kind}.json`;
|
|
@@ -2234,33 +2663,14 @@ try {
|
|
|
2234
2663
|
console.log(
|
|
2235
2664
|
JSON.stringify({ output, kind, preservedDeployedValues: Boolean(deployedStackPath) })
|
|
2236
2665
|
);
|
|
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
2666
|
} else if (action === "change-set") {
|
|
2259
2667
|
const kind = bootstrapKind();
|
|
2260
2668
|
const stackName2 = requiredOption("--stack-name", bootstrapStackName(kind, config));
|
|
2261
2669
|
const changeSetName = requiredOption("--change-set-name");
|
|
2262
2670
|
const parametersPath = requiredOption("--parameters");
|
|
2263
2671
|
const region2 = option("--region", config.region);
|
|
2672
|
+
const plan = await bootstrapPlan(config);
|
|
2673
|
+
const template = plan.packageTemplates.find((candidate) => candidate.kind === kind);
|
|
2264
2674
|
await existingParameters(parametersPath);
|
|
2265
2675
|
const awsCommand = [
|
|
2266
2676
|
"aws",
|
|
@@ -2284,6 +2694,10 @@ try {
|
|
|
2284
2694
|
console.log(
|
|
2285
2695
|
JSON.stringify({
|
|
2286
2696
|
kind,
|
|
2697
|
+
packageVersion: plan.packageVersion,
|
|
2698
|
+
contractVersion: template?.contractVersion,
|
|
2699
|
+
templateRevision: template?.templateRevision,
|
|
2700
|
+
templateSha256: template?.sha256,
|
|
2287
2701
|
reviewRequired: true,
|
|
2288
2702
|
awsCommand,
|
|
2289
2703
|
shellCommand: args.includes("--format-shell") ? shellCommand(awsCommand) : void 0
|
|
@@ -2291,7 +2705,7 @@ try {
|
|
|
2291
2705
|
);
|
|
2292
2706
|
} else {
|
|
2293
2707
|
throw new Error(
|
|
2294
|
-
"Usage: ze-great-dashboard-aws bootstrap init|preflight|guide|handoff|verify --config manifest.json [options], or bootstrap template|parameters|
|
|
2708
|
+
"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
2709
|
);
|
|
2296
2710
|
}
|
|
2297
2711
|
}
|
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 = {
|