@kungfu-tech/buildchain 3.0.5 → 3.0.6-alpha.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 +31 -0
- package/actions/macos-credential-island/README.md +8 -0
- package/bin/buildchain.mjs +1 -1
- package/contracts/auditable-demo-media-profiles-v1.json +61 -0
- package/contracts/auditable-demo-scenario-v1.schema.json +164 -0
- package/contracts/evidence/auditable-demo-responsive-web-delivery-v1.json +3 -3
- package/contracts/evidence/auditable-demo-web-delivery-v1.json +3 -3
- package/dist/site/buildchain-contract.json +107 -26
- package/dist/site/buildchain-site.json +121 -49
- package/dist/site/capability-registry.json +3 -3
- package/dist/site/cli-registry.json +9 -3
- package/dist/site/controller-registry.json +27 -3
- package/dist/site/kfd-claims.json +101 -15
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +10 -9
- package/dist/site/node-api-registry.json +540 -31
- package/dist/site/page-registry.json +105 -29
- package/dist/site/public-surface-audit.json +184 -11
- package/dist/site/publication-authority-registry.json +62 -1
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +13 -13
- package/dist/site/workflow-registry.json +118 -4
- package/docs/MAP.md +4 -1
- package/docs/auditable-demo.md +90 -7
- package/docs/aws-us-elastic-runner-burst-plane.md +151 -16
- package/docs/cli-reference.md +35 -2
- package/docs/dev-alpha-candidate-patrol.md +14 -5
- package/docs/dev-qualification-patrol.md +108 -0
- package/docs/node-api-reference.md +123 -72
- package/docs/publish-transaction.md +7 -5
- package/docs/release-flow.md +4 -0
- package/docs/release-governance.md +39 -0
- package/docs/release-propagation.md +93 -0
- package/docs/reusable-build-surface.md +53 -12
- package/docs/stable-candidate-patrol.md +5 -2
- package/package.json +1 -1
- package/packages/core/buildchain-contract.js +36 -1
- package/packages/core/buildchain-publication-authority.js +3 -0
- package/packages/core/public-surface-audit.js +4 -35
- package/packages/core/release-propagation-agent-entry.js +185 -0
- package/packages/core/release-propagation-pickup.js +387 -0
- package/packages/core/release-propagation-push.js +293 -0
- package/packages/core/release-propagation-release.js +61 -18
- package/packages/core/release-propagation-stage-evidence.js +36 -0
- package/packages/core/release-propagation-work.js +8 -4
- package/packages/core/release-propagation.js +27 -2
- package/packages/core/workflow-call-contract.js +308 -0
- package/packages/core/workflow-yaml-contract.js +272 -0
- package/scripts/artifact-signing-controller-core.mjs +525 -0
- package/scripts/artifact-signing-controller.mjs +302 -0
- package/scripts/artifact-signing-delegation.mjs +51 -3
- package/scripts/auditable-demo-platform.mjs +564 -0
- package/scripts/auditable-demo-renditions.mjs +7 -2
- package/scripts/auditable-demo-transport-smoke.mjs +176 -0
- package/scripts/auditable-demo.mjs +27 -21
- package/scripts/aws-macos-jit-controller-core.mjs +389 -0
- package/scripts/aws-macos-jit-controller-runtime.mjs +82 -0
- package/scripts/aws-macos-jit-controller.mjs +558 -0
- package/scripts/aws-macos-jit-job-controller.mjs +401 -0
- package/scripts/aws-windows-jit-campaign-core.mjs +296 -0
- package/scripts/aws-windows-jit-campaign.mjs +202 -0
- package/scripts/aws-windows-jit-controller-core.mjs +20 -2
- package/scripts/aws-windows-jit-controller.mjs +184 -109
- package/scripts/aws-windows-jit-core.mjs +21 -3
- package/scripts/aws-windows-jit.mjs +2 -0
- package/scripts/build-standalone-binary.mjs +6 -0
- package/scripts/buildchain-cli-help.mjs +2 -1
- package/scripts/capture-package-release-propagation.mjs +24 -1
- package/scripts/check-inventory.mjs +6 -0
- package/scripts/compiler-cache-evidence.mjs +90 -7
- package/scripts/dev-alpha-candidate-patrol.mjs +80 -5
- package/scripts/dev-pr-auto-merge.mjs +4 -4
- package/scripts/dev-qualification-patrol.mjs +594 -0
- package/scripts/dispatch-artifact-signing-authority.mjs +312 -61
- package/scripts/finalize-native-artifact-signing-result.mjs +37 -15
- package/scripts/generate-site-bundle.mjs +5 -0
- package/scripts/inspect-artifact-signing-requests.mjs +12 -0
- package/scripts/locked-source-checkout.mjs +17 -3
- package/scripts/release-propagation.mjs +150 -2
- package/scripts/run-lifecycle-core.mjs +25 -0
- package/scripts/site-capability-metadata.mjs +1 -1
- package/scripts/stable-candidate-patrol.mjs +30 -4
- package/scripts/workflow-call-contract.mjs +133 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
import {
|
|
7
|
+
createWindowsJitCampaignArmPlan,
|
|
8
|
+
windowsCampaignArmItems,
|
|
9
|
+
windowsCampaignKillArgs,
|
|
10
|
+
} from "./aws-windows-jit-campaign-core.mjs";
|
|
11
|
+
|
|
12
|
+
function arg(name, fallback = "") {
|
|
13
|
+
const index = process.argv.indexOf(`--${name}`);
|
|
14
|
+
return index === -1 ? fallback : process.argv[index + 1] || "";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function aws(plan, serviceArgs) {
|
|
18
|
+
const profile = arg("aws-profile");
|
|
19
|
+
const result = spawnSync(
|
|
20
|
+
"aws",
|
|
21
|
+
[
|
|
22
|
+
...(profile ? ["--profile", profile] : []),
|
|
23
|
+
"--region",
|
|
24
|
+
plan.aws.region,
|
|
25
|
+
...serviceArgs,
|
|
26
|
+
],
|
|
27
|
+
{ encoding: "utf8", maxBuffer: 16 * 1024 * 1024 },
|
|
28
|
+
);
|
|
29
|
+
if (result.status !== 0) {
|
|
30
|
+
const detail = String(result.stderr || result.stdout || "")
|
|
31
|
+
.trim()
|
|
32
|
+
.slice(0, 2000);
|
|
33
|
+
throw new Error(
|
|
34
|
+
`AWS campaign mutation failed${detail ? `: ${detail}` : ""}`,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
return result.stdout ? JSON.parse(result.stdout) : {};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function armPlan() {
|
|
41
|
+
return createWindowsJitCampaignArmPlan({
|
|
42
|
+
campaignId: arg("campaign-id"),
|
|
43
|
+
sourceSha: arg("source-sha"),
|
|
44
|
+
stateTable: arg("state-table"),
|
|
45
|
+
region: arg("region", "us-east-1"),
|
|
46
|
+
armedAt: arg("armed-at", new Date().toISOString()),
|
|
47
|
+
expiresAt: arg("expires-at"),
|
|
48
|
+
phaseSpendBaselineUsd: arg("phase-spend-baseline-usd"),
|
|
49
|
+
maxAcceptedInstances: arg("max-accepted-instances"),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function confirm(
|
|
54
|
+
plan,
|
|
55
|
+
{ phaseSpendBaseline = true, maxAcceptedInstances = true } = {},
|
|
56
|
+
) {
|
|
57
|
+
if (arg("confirm-campaign-id") !== plan.campaign.id) {
|
|
58
|
+
throw new Error("--confirm-campaign-id must equal the campaign id");
|
|
59
|
+
}
|
|
60
|
+
if (arg("confirm-source-sha") !== plan.source.sha) {
|
|
61
|
+
throw new Error("--confirm-source-sha must equal the exact source SHA");
|
|
62
|
+
}
|
|
63
|
+
if (arg("confirm-state-table") !== plan.aws.stateTable) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
"--confirm-state-table must equal the campaign state table",
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
if (
|
|
69
|
+
phaseSpendBaseline &&
|
|
70
|
+
(!arg("confirm-phase-spend-baseline-usd").trim() ||
|
|
71
|
+
Number(arg("confirm-phase-spend-baseline-usd")) !==
|
|
72
|
+
plan.limits.phaseSpendBaselineUsd)
|
|
73
|
+
) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
"--confirm-phase-spend-baseline-usd must equal the phase spend baseline",
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
if (
|
|
79
|
+
maxAcceptedInstances &&
|
|
80
|
+
(!arg("confirm-max-accepted-instances").trim() ||
|
|
81
|
+
Number(arg("confirm-max-accepted-instances")) !==
|
|
82
|
+
plan.limits.maxAcceptedInstances)
|
|
83
|
+
) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
"--confirm-max-accepted-instances must equal the campaign slot ceiling",
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function killSwitchTopic() {
|
|
91
|
+
const topic = arg("kill-switch-topic");
|
|
92
|
+
if (
|
|
93
|
+
!/^arn:aws:sns:us-east-1:\d{12}:kungfu-buildchain-windows-jit-[A-Za-z0-9_-]+$/.test(
|
|
94
|
+
topic,
|
|
95
|
+
)
|
|
96
|
+
) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
"--kill-switch-topic must be the dedicated Windows JIT SNS ARN",
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
if (arg("confirm-kill-switch-topic") !== topic) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
"--confirm-kill-switch-topic must equal the dedicated kill-switch topic",
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
return topic;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function main() {
|
|
110
|
+
const mode = process.argv[2] || "plan-arm";
|
|
111
|
+
if (["plan-arm", "arm-campaign"].includes(mode)) {
|
|
112
|
+
const plan = armPlan();
|
|
113
|
+
if (mode === "plan-arm") {
|
|
114
|
+
process.stdout.write(`${JSON.stringify(plan, null, 2)}\n`);
|
|
115
|
+
return plan;
|
|
116
|
+
}
|
|
117
|
+
confirm(plan);
|
|
118
|
+
aws(plan, [
|
|
119
|
+
"dynamodb",
|
|
120
|
+
"transact-write-items",
|
|
121
|
+
"--transact-items",
|
|
122
|
+
JSON.stringify(windowsCampaignArmItems(plan)),
|
|
123
|
+
"--output",
|
|
124
|
+
"json",
|
|
125
|
+
]);
|
|
126
|
+
const result = {
|
|
127
|
+
contract: plan.contract,
|
|
128
|
+
kind: "campaign-arm-result",
|
|
129
|
+
status: "armed",
|
|
130
|
+
campaign: plan.campaign,
|
|
131
|
+
source: plan.source,
|
|
132
|
+
aws: plan.aws,
|
|
133
|
+
limits: plan.limits,
|
|
134
|
+
};
|
|
135
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
if (mode === "kill-campaign") {
|
|
139
|
+
const now = new Date();
|
|
140
|
+
const plan = createWindowsJitCampaignArmPlan({
|
|
141
|
+
campaignId: arg("campaign-id"),
|
|
142
|
+
sourceSha: arg("source-sha"),
|
|
143
|
+
stateTable: arg("state-table"),
|
|
144
|
+
region: arg("region", "us-east-1"),
|
|
145
|
+
armedAt: now.toISOString(),
|
|
146
|
+
expiresAt: new Date(now.getTime() + 1000).toISOString(),
|
|
147
|
+
phaseSpendBaselineUsd: 0,
|
|
148
|
+
});
|
|
149
|
+
confirm(plan, {
|
|
150
|
+
phaseSpendBaseline: false,
|
|
151
|
+
maxAcceptedInstances: false,
|
|
152
|
+
});
|
|
153
|
+
const topic = killSwitchTopic();
|
|
154
|
+
aws(
|
|
155
|
+
plan,
|
|
156
|
+
windowsCampaignKillArgs(
|
|
157
|
+
plan.aws.stateTable,
|
|
158
|
+
arg("reason", "operator-kill"),
|
|
159
|
+
new Date().toISOString(),
|
|
160
|
+
),
|
|
161
|
+
);
|
|
162
|
+
const notification = aws(plan, [
|
|
163
|
+
"sns",
|
|
164
|
+
"publish",
|
|
165
|
+
"--topic-arn",
|
|
166
|
+
topic,
|
|
167
|
+
"--message",
|
|
168
|
+
JSON.stringify({
|
|
169
|
+
contract: plan.contract,
|
|
170
|
+
action: "kill-campaign",
|
|
171
|
+
campaignId: plan.campaign.id,
|
|
172
|
+
sourceSha: plan.source.sha,
|
|
173
|
+
}),
|
|
174
|
+
"--output",
|
|
175
|
+
"json",
|
|
176
|
+
]);
|
|
177
|
+
const result = {
|
|
178
|
+
contract: plan.contract,
|
|
179
|
+
kind: "campaign-kill-result",
|
|
180
|
+
status: "killed",
|
|
181
|
+
campaign: plan.campaign,
|
|
182
|
+
source: plan.source,
|
|
183
|
+
killSwitchTopic: topic,
|
|
184
|
+
notificationMessageId: notification.MessageId || "",
|
|
185
|
+
};
|
|
186
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
throw new Error(`unsupported Windows JIT campaign mode: ${mode}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (
|
|
193
|
+
process.argv[1] &&
|
|
194
|
+
import.meta.url === pathToFileURL(process.argv[1]).href
|
|
195
|
+
) {
|
|
196
|
+
try {
|
|
197
|
+
main();
|
|
198
|
+
} catch (error) {
|
|
199
|
+
console.error(`::error::${error.message || error}`);
|
|
200
|
+
process.exitCode = 1;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
import { digest } from "./aws-runner-burst-core.mjs";
|
|
4
4
|
import {
|
|
5
5
|
WINDOWS_EC2_JIT,
|
|
6
|
+
windowsJitCampaignId,
|
|
6
7
|
windowsJitRunnerLabel,
|
|
7
8
|
windowsJitRunnerLabels,
|
|
8
9
|
} from "./aws-windows-jit-core.mjs";
|
|
9
10
|
|
|
10
11
|
export const AWS_WINDOWS_JIT_CONTROLLER_CONTRACT =
|
|
11
|
-
"kungfu-buildchain-aws-windows-jit-controller/
|
|
12
|
+
"kungfu-buildchain-aws-windows-jit-controller/v2";
|
|
12
13
|
|
|
13
14
|
function exact(value, pattern, label) {
|
|
14
15
|
const normalized = String(value || "").trim();
|
|
@@ -59,8 +60,9 @@ export function createWindowsJitLaunchPlan(values = {}) {
|
|
|
59
60
|
);
|
|
60
61
|
const jobId = exact(values.jobId, /^\d+$/, "jobId");
|
|
61
62
|
const qualification = qualificationId(values.qualificationId);
|
|
63
|
+
const campaign = windowsJitCampaignId(values.campaignId);
|
|
62
64
|
const runnerLabel = windowsJitRunnerLabel(values.runnerLabel);
|
|
63
|
-
const expectedLabel = `${WINDOWS_EC2_JIT.labelPrefix}${qualification}`;
|
|
65
|
+
const expectedLabel = `${WINDOWS_EC2_JIT.labelPrefix}${campaign}-${qualification}`;
|
|
64
66
|
if (runnerLabel !== expectedLabel) {
|
|
65
67
|
throw new Error(`runnerLabel must be ${expectedLabel}`);
|
|
66
68
|
}
|
|
@@ -109,6 +111,11 @@ export function createWindowsJitLaunchPlan(values = {}) {
|
|
|
109
111
|
/^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/,
|
|
110
112
|
"evidenceBucket",
|
|
111
113
|
);
|
|
114
|
+
const stateTable = exact(
|
|
115
|
+
values.stateTable,
|
|
116
|
+
/^kungfu-buildchain-windows-jit(?:-[A-Za-z0-9_.-]+)?$/,
|
|
117
|
+
"stateTable",
|
|
118
|
+
);
|
|
112
119
|
const launchedAt = iso(values.launchedAt, "launchedAt");
|
|
113
120
|
const clientToken = `kungfu-${runId}-${runAttempt}-${qualification}`;
|
|
114
121
|
const jitParameterName =
|
|
@@ -125,6 +132,7 @@ export function createWindowsJitLaunchPlan(values = {}) {
|
|
|
125
132
|
tag("kungfu:owner", "buildchain"),
|
|
126
133
|
tag("kungfu:plane", "aws-us-elastic-runner-burst"),
|
|
127
134
|
tag("kungfu:provider", "windows-ec2-jit"),
|
|
135
|
+
tag("kungfu:campaign-id", campaign),
|
|
128
136
|
tag("kungfu:github-run-id", runId),
|
|
129
137
|
tag("kungfu:github-run-attempt", runAttempt),
|
|
130
138
|
tag("kungfu:qualification-id", qualification),
|
|
@@ -141,6 +149,7 @@ export function createWindowsJitLaunchPlan(values = {}) {
|
|
|
141
149
|
contract: AWS_WINDOWS_JIT_CONTROLLER_CONTRACT,
|
|
142
150
|
kind: "launch-plan",
|
|
143
151
|
repository,
|
|
152
|
+
campaign: { id: campaign },
|
|
144
153
|
source: { sha: sourceSha, ref: sourceRef },
|
|
145
154
|
github: {
|
|
146
155
|
runId,
|
|
@@ -148,6 +157,7 @@ export function createWindowsJitLaunchPlan(values = {}) {
|
|
|
148
157
|
jobId,
|
|
149
158
|
qualificationId: qualification,
|
|
150
159
|
event: "workflow_dispatch",
|
|
160
|
+
displayTitle: `AWS Windows JIT ${campaign} ${qualification}`,
|
|
151
161
|
},
|
|
152
162
|
runner: {
|
|
153
163
|
name: runnerName,
|
|
@@ -164,6 +174,7 @@ export function createWindowsJitLaunchPlan(values = {}) {
|
|
|
164
174
|
securityGroupId,
|
|
165
175
|
instanceProfileName,
|
|
166
176
|
evidenceBucket,
|
|
177
|
+
stateTable,
|
|
167
178
|
jitParameterName,
|
|
168
179
|
launchedAt,
|
|
169
180
|
clientToken,
|
|
@@ -188,6 +199,13 @@ export function createWindowsJitLaunchPlan(values = {}) {
|
|
|
188
199
|
exactSourceRequired: true,
|
|
189
200
|
queuedJobRequired: true,
|
|
190
201
|
activeInstanceCeiling: WINDOWS_EC2_JIT.maxConcurrentInstances,
|
|
202
|
+
campaignAcceptedInstanceCeiling: WINDOWS_EC2_JIT.maxAcceptedInstances,
|
|
203
|
+
campaignReservationUsd:
|
|
204
|
+
(WINDOWS_EC2_JIT.pricePerHourUsd *
|
|
205
|
+
WINDOWS_EC2_JIT.maximumInstanceLifetimeMinutes) /
|
|
206
|
+
60,
|
|
207
|
+
campaignBudgetLimitUsd: WINDOWS_EC2_JIT.budgetLimitUsd,
|
|
208
|
+
persistentCampaignLedgerRequired: true,
|
|
191
209
|
awsDryRunRequiredBeforeLaunch: true,
|
|
192
210
|
userDataTransport: "fileb://rendered-bootstrap",
|
|
193
211
|
jitConfigTransport: "0600-temporary-file-to-ssm-secure-string",
|
|
@@ -11,6 +11,10 @@ import {
|
|
|
11
11
|
createWindowsJitLaunchPlan,
|
|
12
12
|
windowsRunInstancesArgs,
|
|
13
13
|
} from "./aws-windows-jit-controller-core.mjs";
|
|
14
|
+
import {
|
|
15
|
+
windowsCampaignMarkLaunchedArgs,
|
|
16
|
+
windowsCampaignReservationItems,
|
|
17
|
+
} from "./aws-windows-jit-campaign-core.mjs";
|
|
14
18
|
import { renderWindowsJitBootstrap } from "./aws-windows-jit-core.mjs";
|
|
15
19
|
|
|
16
20
|
function arg(name, fallback = "") {
|
|
@@ -75,6 +79,7 @@ function assertLivePreflight(plan, profile) {
|
|
|
75
79
|
);
|
|
76
80
|
if (
|
|
77
81
|
run.event !== plan.github.event ||
|
|
82
|
+
run.display_title !== plan.github.displayTitle ||
|
|
78
83
|
run.head_sha !== plan.source.sha ||
|
|
79
84
|
run.head_repository?.full_name !== plan.repository ||
|
|
80
85
|
!["queued", "in_progress"].includes(run.status)
|
|
@@ -267,6 +272,169 @@ function cleanupFailedLaunch(
|
|
|
267
272
|
return failures;
|
|
268
273
|
}
|
|
269
274
|
|
|
275
|
+
function generateJitConfiguration(plan) {
|
|
276
|
+
const jit = ghJson(
|
|
277
|
+
[
|
|
278
|
+
"api",
|
|
279
|
+
"--method",
|
|
280
|
+
"POST",
|
|
281
|
+
`repos/${plan.repository}/actions/runners/generate-jitconfig`,
|
|
282
|
+
"--input",
|
|
283
|
+
"-",
|
|
284
|
+
],
|
|
285
|
+
JSON.stringify({
|
|
286
|
+
name: plan.runner.name,
|
|
287
|
+
runner_group_id: 1,
|
|
288
|
+
labels: plan.runner.labels,
|
|
289
|
+
work_folder: "_work",
|
|
290
|
+
}),
|
|
291
|
+
"GitHub JIT configuration",
|
|
292
|
+
).encoded_jit_config;
|
|
293
|
+
if (!jit || typeof jit !== "string") {
|
|
294
|
+
throw new Error("GitHub JIT configuration was empty");
|
|
295
|
+
}
|
|
296
|
+
return jit;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function createJitParameter(plan, profile, parameterInputPath, jit) {
|
|
300
|
+
fs.writeFileSync(
|
|
301
|
+
parameterInputPath,
|
|
302
|
+
JSON.stringify({
|
|
303
|
+
Name: plan.aws.jitParameterName,
|
|
304
|
+
Description: `One-shot GitHub Actions JIT config for ${plan.repository} run ${plan.github.runId} attempt ${plan.github.runAttempt}`,
|
|
305
|
+
Type: "SecureString",
|
|
306
|
+
Tier: "Advanced",
|
|
307
|
+
Value: jit,
|
|
308
|
+
Tags: [
|
|
309
|
+
{ Key: "kungfu:owner", Value: "buildchain" },
|
|
310
|
+
{ Key: "kungfu:plane", Value: "aws-us-elastic-runner-burst" },
|
|
311
|
+
{ Key: "kungfu:provider", Value: "windows-ec2-jit" },
|
|
312
|
+
{ Key: "kungfu:campaign-id", Value: plan.campaign.id },
|
|
313
|
+
{ Key: "kungfu:github-run-id", Value: plan.github.runId },
|
|
314
|
+
{
|
|
315
|
+
Key: "kungfu:qualification-id",
|
|
316
|
+
Value: plan.github.qualificationId,
|
|
317
|
+
},
|
|
318
|
+
],
|
|
319
|
+
}),
|
|
320
|
+
{ mode: 0o600 },
|
|
321
|
+
);
|
|
322
|
+
requireSuccess(
|
|
323
|
+
commandResult(
|
|
324
|
+
"aws",
|
|
325
|
+
awsArgs(plan, profile, [
|
|
326
|
+
"ssm",
|
|
327
|
+
"put-parameter",
|
|
328
|
+
"--cli-input-json",
|
|
329
|
+
`file://${parameterInputPath}`,
|
|
330
|
+
"--output",
|
|
331
|
+
"json",
|
|
332
|
+
]),
|
|
333
|
+
),
|
|
334
|
+
"SSM JIT parameter creation",
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function writeBootstrap(plan, bootstrapPath) {
|
|
339
|
+
const template = fs.readFileSync(
|
|
340
|
+
path.resolve(
|
|
341
|
+
"infra/aws-us-elastic-runner-burst-plane/windows-jit-bootstrap.ps1",
|
|
342
|
+
),
|
|
343
|
+
"utf8",
|
|
344
|
+
);
|
|
345
|
+
fs.writeFileSync(
|
|
346
|
+
bootstrapPath,
|
|
347
|
+
renderWindowsJitBootstrap(template, {
|
|
348
|
+
region: plan.aws.region,
|
|
349
|
+
campaignId: plan.campaign.id,
|
|
350
|
+
jitParameterName: plan.aws.jitParameterName,
|
|
351
|
+
evidenceBucket: plan.aws.evidenceBucket,
|
|
352
|
+
runnerLabel: plan.runner.label,
|
|
353
|
+
sourceSha: plan.source.sha,
|
|
354
|
+
githubRunId: plan.github.runId,
|
|
355
|
+
githubRunAttempt: plan.github.runAttempt,
|
|
356
|
+
amiId: plan.aws.amiId,
|
|
357
|
+
amiName: plan.aws.amiName,
|
|
358
|
+
instanceType: plan.aws.instanceType,
|
|
359
|
+
launchedAt: plan.aws.launchedAt,
|
|
360
|
+
}),
|
|
361
|
+
{ mode: 0o600 },
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function assertLaunchDryRun(plan, profile, bootstrapPath) {
|
|
366
|
+
const dryRun = commandResult(
|
|
367
|
+
"aws",
|
|
368
|
+
awsArgs(
|
|
369
|
+
plan,
|
|
370
|
+
profile,
|
|
371
|
+
windowsRunInstancesArgs(plan, { bootstrapPath, dryRun: true }),
|
|
372
|
+
),
|
|
373
|
+
);
|
|
374
|
+
if (
|
|
375
|
+
dryRun.status === 0 ||
|
|
376
|
+
!/DryRunOperation/.test(`${dryRun.stdout}\n${dryRun.stderr}`)
|
|
377
|
+
) {
|
|
378
|
+
throw new Error("EC2 RunInstances DryRun did not return DryRunOperation");
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function reserveCampaign(plan, profile) {
|
|
383
|
+
requireSuccess(
|
|
384
|
+
commandResult(
|
|
385
|
+
"aws",
|
|
386
|
+
awsArgs(plan, profile, [
|
|
387
|
+
"dynamodb",
|
|
388
|
+
"transact-write-items",
|
|
389
|
+
"--transact-items",
|
|
390
|
+
JSON.stringify(
|
|
391
|
+
windowsCampaignReservationItems(plan, new Date().toISOString()),
|
|
392
|
+
),
|
|
393
|
+
"--output",
|
|
394
|
+
"json",
|
|
395
|
+
]),
|
|
396
|
+
),
|
|
397
|
+
"Windows campaign atomic reservation",
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function launchInstance(plan, profile, bootstrapPath) {
|
|
402
|
+
const launched = jsonResult(
|
|
403
|
+
commandResult(
|
|
404
|
+
"aws",
|
|
405
|
+
awsArgs(
|
|
406
|
+
plan,
|
|
407
|
+
profile,
|
|
408
|
+
windowsRunInstancesArgs(plan, { bootstrapPath, dryRun: false }),
|
|
409
|
+
),
|
|
410
|
+
),
|
|
411
|
+
"EC2 RunInstances",
|
|
412
|
+
);
|
|
413
|
+
const instance = launched.Instances?.[0];
|
|
414
|
+
if (!/^i-[0-9a-f]+$/.test(String(instance?.InstanceId || ""))) {
|
|
415
|
+
throw new Error("EC2 RunInstances returned no instance identity");
|
|
416
|
+
}
|
|
417
|
+
return instance;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function markCampaignLaunched(plan, profile, instanceId) {
|
|
421
|
+
requireSuccess(
|
|
422
|
+
commandResult(
|
|
423
|
+
"aws",
|
|
424
|
+
awsArgs(
|
|
425
|
+
plan,
|
|
426
|
+
profile,
|
|
427
|
+
windowsCampaignMarkLaunchedArgs(
|
|
428
|
+
plan,
|
|
429
|
+
instanceId,
|
|
430
|
+
new Date().toISOString(),
|
|
431
|
+
),
|
|
432
|
+
),
|
|
433
|
+
),
|
|
434
|
+
"Windows campaign launch ledger update",
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
|
|
270
438
|
export function executeWindowsJitLaunch(plan, { profile = "" } = {}) {
|
|
271
439
|
if (plan?.contract !== AWS_WINDOWS_JIT_CONTROLLER_CONTRACT) {
|
|
272
440
|
throw new Error("Windows JIT launch plan contract is invalid");
|
|
@@ -285,124 +453,23 @@ export function executeWindowsJitLaunch(plan, { profile = "" } = {}) {
|
|
|
285
453
|
let failure;
|
|
286
454
|
const cleanupFailures = [];
|
|
287
455
|
try {
|
|
288
|
-
const jit =
|
|
289
|
-
|
|
290
|
-
"api",
|
|
291
|
-
"--method",
|
|
292
|
-
"POST",
|
|
293
|
-
`repos/${plan.repository}/actions/runners/generate-jitconfig`,
|
|
294
|
-
"--input",
|
|
295
|
-
"-",
|
|
296
|
-
],
|
|
297
|
-
JSON.stringify({
|
|
298
|
-
name: plan.runner.name,
|
|
299
|
-
runner_group_id: 1,
|
|
300
|
-
labels: plan.runner.labels,
|
|
301
|
-
work_folder: "_work",
|
|
302
|
-
}),
|
|
303
|
-
"GitHub JIT configuration",
|
|
304
|
-
).encoded_jit_config;
|
|
305
|
-
if (!jit || typeof jit !== "string") {
|
|
306
|
-
throw new Error("GitHub JIT configuration was empty");
|
|
307
|
-
}
|
|
308
|
-
fs.writeFileSync(
|
|
309
|
-
parameterInputPath,
|
|
310
|
-
JSON.stringify({
|
|
311
|
-
Name: plan.aws.jitParameterName,
|
|
312
|
-
Description: `One-shot GitHub Actions JIT config for ${plan.repository} run ${plan.github.runId} attempt ${plan.github.runAttempt}`,
|
|
313
|
-
Type: "SecureString",
|
|
314
|
-
Tier: "Advanced",
|
|
315
|
-
Value: jit,
|
|
316
|
-
Tags: [
|
|
317
|
-
{ Key: "kungfu:owner", Value: "buildchain" },
|
|
318
|
-
{ Key: "kungfu:plane", Value: "aws-us-elastic-runner-burst" },
|
|
319
|
-
{ Key: "kungfu:provider", Value: "windows-ec2-jit" },
|
|
320
|
-
{ Key: "kungfu:github-run-id", Value: plan.github.runId },
|
|
321
|
-
{
|
|
322
|
-
Key: "kungfu:qualification-id",
|
|
323
|
-
Value: plan.github.qualificationId,
|
|
324
|
-
},
|
|
325
|
-
],
|
|
326
|
-
}),
|
|
327
|
-
{ mode: 0o600 },
|
|
328
|
-
);
|
|
329
|
-
requireSuccess(
|
|
330
|
-
commandResult(
|
|
331
|
-
"aws",
|
|
332
|
-
awsArgs(plan, profile, [
|
|
333
|
-
"ssm",
|
|
334
|
-
"put-parameter",
|
|
335
|
-
"--cli-input-json",
|
|
336
|
-
`file://${parameterInputPath}`,
|
|
337
|
-
"--output",
|
|
338
|
-
"json",
|
|
339
|
-
]),
|
|
340
|
-
),
|
|
341
|
-
"SSM JIT parameter creation",
|
|
342
|
-
);
|
|
456
|
+
const jit = generateJitConfiguration(plan);
|
|
457
|
+
createJitParameter(plan, profile, parameterInputPath, jit);
|
|
343
458
|
parameterCreated = true;
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
fs.readFileSync(
|
|
348
|
-
path.resolve(
|
|
349
|
-
"infra/aws-us-elastic-runner-burst-plane/windows-jit-bootstrap.ps1",
|
|
350
|
-
),
|
|
351
|
-
"utf8",
|
|
352
|
-
),
|
|
353
|
-
{
|
|
354
|
-
region: plan.aws.region,
|
|
355
|
-
jitParameterName: plan.aws.jitParameterName,
|
|
356
|
-
evidenceBucket: plan.aws.evidenceBucket,
|
|
357
|
-
runnerLabel: plan.runner.label,
|
|
358
|
-
sourceSha: plan.source.sha,
|
|
359
|
-
githubRunId: plan.github.runId,
|
|
360
|
-
githubRunAttempt: plan.github.runAttempt,
|
|
361
|
-
amiId: plan.aws.amiId,
|
|
362
|
-
amiName: plan.aws.amiName,
|
|
363
|
-
instanceType: plan.aws.instanceType,
|
|
364
|
-
launchedAt: plan.aws.launchedAt,
|
|
365
|
-
},
|
|
366
|
-
),
|
|
367
|
-
{ mode: 0o600 },
|
|
368
|
-
);
|
|
369
|
-
const dryRun = commandResult(
|
|
370
|
-
"aws",
|
|
371
|
-
awsArgs(
|
|
372
|
-
plan,
|
|
373
|
-
profile,
|
|
374
|
-
windowsRunInstancesArgs(plan, { bootstrapPath, dryRun: true }),
|
|
375
|
-
),
|
|
376
|
-
);
|
|
377
|
-
if (
|
|
378
|
-
dryRun.status === 0 ||
|
|
379
|
-
!/DryRunOperation/.test(`${dryRun.stdout}\n${dryRun.stderr}`)
|
|
380
|
-
) {
|
|
381
|
-
throw new Error("EC2 RunInstances DryRun did not return DryRunOperation");
|
|
382
|
-
}
|
|
459
|
+
writeBootstrap(plan, bootstrapPath);
|
|
460
|
+
assertLaunchDryRun(plan, profile, bootstrapPath);
|
|
461
|
+
reserveCampaign(plan, profile);
|
|
383
462
|
launchAttempted = true;
|
|
384
|
-
const
|
|
385
|
-
commandResult(
|
|
386
|
-
"aws",
|
|
387
|
-
awsArgs(
|
|
388
|
-
plan,
|
|
389
|
-
profile,
|
|
390
|
-
windowsRunInstancesArgs(plan, { bootstrapPath, dryRun: false }),
|
|
391
|
-
),
|
|
392
|
-
),
|
|
393
|
-
"EC2 RunInstances",
|
|
394
|
-
);
|
|
395
|
-
const instance = launched.Instances?.[0];
|
|
396
|
-
if (!/^i-[0-9a-f]+$/.test(String(instance?.InstanceId || ""))) {
|
|
397
|
-
throw new Error("EC2 RunInstances returned no instance identity");
|
|
398
|
-
}
|
|
463
|
+
const instance = launchInstance(plan, profile, bootstrapPath);
|
|
399
464
|
launchSucceeded = true;
|
|
465
|
+
markCampaignLaunched(plan, profile, instance.InstanceId);
|
|
400
466
|
result = {
|
|
401
467
|
schemaVersion: 1,
|
|
402
468
|
contract: AWS_WINDOWS_JIT_CONTROLLER_CONTRACT,
|
|
403
469
|
kind: "launch-result",
|
|
404
470
|
status: "launched",
|
|
405
471
|
source: plan.source,
|
|
472
|
+
campaign: plan.campaign,
|
|
406
473
|
github: plan.github,
|
|
407
474
|
runner: plan.runner,
|
|
408
475
|
aws: {
|
|
@@ -452,6 +519,7 @@ function planFromArgs(execute) {
|
|
|
452
519
|
runAttempt: arg("run-attempt", "1"),
|
|
453
520
|
jobId: arg("job-id"),
|
|
454
521
|
qualificationId: arg("qualification-id"),
|
|
522
|
+
campaignId: arg("campaign-id"),
|
|
455
523
|
runnerLabel: arg("runner-label"),
|
|
456
524
|
runnerName: arg("runner-name"),
|
|
457
525
|
sourceSha: arg("source-sha"),
|
|
@@ -464,6 +532,7 @@ function planFromArgs(execute) {
|
|
|
464
532
|
securityGroupId: arg("security-group-id"),
|
|
465
533
|
instanceProfileName: arg("instance-profile-name"),
|
|
466
534
|
evidenceBucket: arg("evidence-bucket"),
|
|
535
|
+
stateTable: arg("state-table"),
|
|
467
536
|
jitParameterName: arg("jit-parameter"),
|
|
468
537
|
launchedAt: arg("launched-at", new Date().toISOString()),
|
|
469
538
|
});
|
|
@@ -482,6 +551,12 @@ export function main() {
|
|
|
482
551
|
if (arg("confirm-run-id") !== plan.github.runId) {
|
|
483
552
|
throw new Error("--confirm-run-id must equal the exact GitHub run id");
|
|
484
553
|
}
|
|
554
|
+
if (arg("confirm-campaign-id") !== plan.campaign.id) {
|
|
555
|
+
throw new Error("--confirm-campaign-id must equal the campaign id");
|
|
556
|
+
}
|
|
557
|
+
if (arg("confirm-state-table") !== plan.aws.stateTable) {
|
|
558
|
+
throw new Error("--confirm-state-table must equal the campaign state table");
|
|
559
|
+
}
|
|
485
560
|
const result = executeWindowsJitLaunch(plan, {
|
|
486
561
|
profile: arg("aws-profile"),
|
|
487
562
|
});
|