@kungfu-tech/buildchain 3.0.5 → 3.0.6-alpha.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 +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 +126 -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 +103 -16
- package/dist/site/kfd-upstream-aggregate.json +9 -9
- package/dist/site/manual-registry.json +10 -9
- package/dist/site/node-api-registry.json +540 -31
- package/dist/site/page-registry.json +110 -29
- package/dist/site/public-surface-audit.json +186 -12
- 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 +120 -5
- package/docs/MAP.md +5 -1
- package/docs/auditable-demo.md +90 -7
- package/docs/aws-us-elastic-runner-burst-plane.md +181 -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 +47 -1
- 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 +3 -2
- 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 +295 -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 +185 -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 +15 -15
- package/scripts/dev-qualification-patrol.mjs +594 -0
- package/scripts/dispatch-artifact-signing-authority.mjs +310 -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)
|
|
@@ -159,6 +164,7 @@ function assertLivePreflight(plan, profile) {
|
|
|
159
164
|
throw new Error("Windows AMI identity or availability mismatch");
|
|
160
165
|
}
|
|
161
166
|
return {
|
|
167
|
+
budgetGuard: jsonResult(commandResult("/bin/bash", ["scripts/aws-windows-jit-operator.sh", "launch-gate", "--region", plan.aws.region, ...(profile ? ["--aws-profile", profile] : [])]), "provider Budget launch gate"),
|
|
162
168
|
runStatus: run.status,
|
|
163
169
|
jobStatus: job.status,
|
|
164
170
|
activeInstances: activeInstances.length,
|
|
@@ -267,6 +273,169 @@ function cleanupFailedLaunch(
|
|
|
267
273
|
return failures;
|
|
268
274
|
}
|
|
269
275
|
|
|
276
|
+
function generateJitConfiguration(plan) {
|
|
277
|
+
const jit = ghJson(
|
|
278
|
+
[
|
|
279
|
+
"api",
|
|
280
|
+
"--method",
|
|
281
|
+
"POST",
|
|
282
|
+
`repos/${plan.repository}/actions/runners/generate-jitconfig`,
|
|
283
|
+
"--input",
|
|
284
|
+
"-",
|
|
285
|
+
],
|
|
286
|
+
JSON.stringify({
|
|
287
|
+
name: plan.runner.name,
|
|
288
|
+
runner_group_id: 1,
|
|
289
|
+
labels: plan.runner.labels,
|
|
290
|
+
work_folder: "_work",
|
|
291
|
+
}),
|
|
292
|
+
"GitHub JIT configuration",
|
|
293
|
+
).encoded_jit_config;
|
|
294
|
+
if (!jit || typeof jit !== "string") {
|
|
295
|
+
throw new Error("GitHub JIT configuration was empty");
|
|
296
|
+
}
|
|
297
|
+
return jit;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function createJitParameter(plan, profile, parameterInputPath, jit) {
|
|
301
|
+
fs.writeFileSync(
|
|
302
|
+
parameterInputPath,
|
|
303
|
+
JSON.stringify({
|
|
304
|
+
Name: plan.aws.jitParameterName,
|
|
305
|
+
Description: `One-shot GitHub Actions JIT config for ${plan.repository} run ${plan.github.runId} attempt ${plan.github.runAttempt}`,
|
|
306
|
+
Type: "SecureString",
|
|
307
|
+
Tier: "Advanced",
|
|
308
|
+
Value: jit,
|
|
309
|
+
Tags: [
|
|
310
|
+
{ Key: "kungfu:owner", Value: "buildchain" },
|
|
311
|
+
{ Key: "kungfu:plane", Value: "aws-us-elastic-runner-burst" },
|
|
312
|
+
{ Key: "kungfu:provider", Value: "windows-ec2-jit" },
|
|
313
|
+
{ Key: "kungfu:campaign-id", Value: plan.campaign.id },
|
|
314
|
+
{ Key: "kungfu:github-run-id", Value: plan.github.runId },
|
|
315
|
+
{
|
|
316
|
+
Key: "kungfu:qualification-id",
|
|
317
|
+
Value: plan.github.qualificationId,
|
|
318
|
+
},
|
|
319
|
+
],
|
|
320
|
+
}),
|
|
321
|
+
{ mode: 0o600 },
|
|
322
|
+
);
|
|
323
|
+
requireSuccess(
|
|
324
|
+
commandResult(
|
|
325
|
+
"aws",
|
|
326
|
+
awsArgs(plan, profile, [
|
|
327
|
+
"ssm",
|
|
328
|
+
"put-parameter",
|
|
329
|
+
"--cli-input-json",
|
|
330
|
+
`file://${parameterInputPath}`,
|
|
331
|
+
"--output",
|
|
332
|
+
"json",
|
|
333
|
+
]),
|
|
334
|
+
),
|
|
335
|
+
"SSM JIT parameter creation",
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function writeBootstrap(plan, bootstrapPath) {
|
|
340
|
+
const template = fs.readFileSync(
|
|
341
|
+
path.resolve(
|
|
342
|
+
"infra/aws-us-elastic-runner-burst-plane/windows-jit-bootstrap.ps1",
|
|
343
|
+
),
|
|
344
|
+
"utf8",
|
|
345
|
+
);
|
|
346
|
+
fs.writeFileSync(
|
|
347
|
+
bootstrapPath,
|
|
348
|
+
renderWindowsJitBootstrap(template, {
|
|
349
|
+
region: plan.aws.region,
|
|
350
|
+
campaignId: plan.campaign.id,
|
|
351
|
+
jitParameterName: plan.aws.jitParameterName,
|
|
352
|
+
evidenceBucket: plan.aws.evidenceBucket,
|
|
353
|
+
runnerLabel: plan.runner.label,
|
|
354
|
+
sourceSha: plan.source.sha,
|
|
355
|
+
githubRunId: plan.github.runId,
|
|
356
|
+
githubRunAttempt: plan.github.runAttempt,
|
|
357
|
+
amiId: plan.aws.amiId,
|
|
358
|
+
amiName: plan.aws.amiName,
|
|
359
|
+
instanceType: plan.aws.instanceType,
|
|
360
|
+
launchedAt: plan.aws.launchedAt,
|
|
361
|
+
}),
|
|
362
|
+
{ mode: 0o600 },
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function assertLaunchDryRun(plan, profile, bootstrapPath) {
|
|
367
|
+
const dryRun = commandResult(
|
|
368
|
+
"aws",
|
|
369
|
+
awsArgs(
|
|
370
|
+
plan,
|
|
371
|
+
profile,
|
|
372
|
+
windowsRunInstancesArgs(plan, { bootstrapPath, dryRun: true }),
|
|
373
|
+
),
|
|
374
|
+
);
|
|
375
|
+
if (
|
|
376
|
+
dryRun.status === 0 ||
|
|
377
|
+
!/DryRunOperation/.test(`${dryRun.stdout}\n${dryRun.stderr}`)
|
|
378
|
+
) {
|
|
379
|
+
throw new Error("EC2 RunInstances DryRun did not return DryRunOperation");
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function reserveCampaign(plan, profile) {
|
|
384
|
+
requireSuccess(
|
|
385
|
+
commandResult(
|
|
386
|
+
"aws",
|
|
387
|
+
awsArgs(plan, profile, [
|
|
388
|
+
"dynamodb",
|
|
389
|
+
"transact-write-items",
|
|
390
|
+
"--transact-items",
|
|
391
|
+
JSON.stringify(
|
|
392
|
+
windowsCampaignReservationItems(plan, new Date().toISOString()),
|
|
393
|
+
),
|
|
394
|
+
"--output",
|
|
395
|
+
"json",
|
|
396
|
+
]),
|
|
397
|
+
),
|
|
398
|
+
"Windows campaign atomic reservation",
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function launchInstance(plan, profile, bootstrapPath) {
|
|
403
|
+
const launched = jsonResult(
|
|
404
|
+
commandResult(
|
|
405
|
+
"aws",
|
|
406
|
+
awsArgs(
|
|
407
|
+
plan,
|
|
408
|
+
profile,
|
|
409
|
+
windowsRunInstancesArgs(plan, { bootstrapPath, dryRun: false }),
|
|
410
|
+
),
|
|
411
|
+
),
|
|
412
|
+
"EC2 RunInstances",
|
|
413
|
+
);
|
|
414
|
+
const instance = launched.Instances?.[0];
|
|
415
|
+
if (!/^i-[0-9a-f]+$/.test(String(instance?.InstanceId || ""))) {
|
|
416
|
+
throw new Error("EC2 RunInstances returned no instance identity");
|
|
417
|
+
}
|
|
418
|
+
return instance;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function markCampaignLaunched(plan, profile, instanceId) {
|
|
422
|
+
requireSuccess(
|
|
423
|
+
commandResult(
|
|
424
|
+
"aws",
|
|
425
|
+
awsArgs(
|
|
426
|
+
plan,
|
|
427
|
+
profile,
|
|
428
|
+
windowsCampaignMarkLaunchedArgs(
|
|
429
|
+
plan,
|
|
430
|
+
instanceId,
|
|
431
|
+
new Date().toISOString(),
|
|
432
|
+
),
|
|
433
|
+
),
|
|
434
|
+
),
|
|
435
|
+
"Windows campaign launch ledger update",
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
|
|
270
439
|
export function executeWindowsJitLaunch(plan, { profile = "" } = {}) {
|
|
271
440
|
if (plan?.contract !== AWS_WINDOWS_JIT_CONTROLLER_CONTRACT) {
|
|
272
441
|
throw new Error("Windows JIT launch plan contract is invalid");
|
|
@@ -285,124 +454,23 @@ export function executeWindowsJitLaunch(plan, { profile = "" } = {}) {
|
|
|
285
454
|
let failure;
|
|
286
455
|
const cleanupFailures = [];
|
|
287
456
|
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
|
-
);
|
|
457
|
+
const jit = generateJitConfiguration(plan);
|
|
458
|
+
createJitParameter(plan, profile, parameterInputPath, jit);
|
|
343
459
|
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
|
-
}
|
|
460
|
+
writeBootstrap(plan, bootstrapPath);
|
|
461
|
+
assertLaunchDryRun(plan, profile, bootstrapPath);
|
|
462
|
+
reserveCampaign(plan, profile);
|
|
383
463
|
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
|
-
}
|
|
464
|
+
const instance = launchInstance(plan, profile, bootstrapPath);
|
|
399
465
|
launchSucceeded = true;
|
|
466
|
+
markCampaignLaunched(plan, profile, instance.InstanceId);
|
|
400
467
|
result = {
|
|
401
468
|
schemaVersion: 1,
|
|
402
469
|
contract: AWS_WINDOWS_JIT_CONTROLLER_CONTRACT,
|
|
403
470
|
kind: "launch-result",
|
|
404
471
|
status: "launched",
|
|
405
472
|
source: plan.source,
|
|
473
|
+
campaign: plan.campaign,
|
|
406
474
|
github: plan.github,
|
|
407
475
|
runner: plan.runner,
|
|
408
476
|
aws: {
|
|
@@ -452,6 +520,7 @@ function planFromArgs(execute) {
|
|
|
452
520
|
runAttempt: arg("run-attempt", "1"),
|
|
453
521
|
jobId: arg("job-id"),
|
|
454
522
|
qualificationId: arg("qualification-id"),
|
|
523
|
+
campaignId: arg("campaign-id"),
|
|
455
524
|
runnerLabel: arg("runner-label"),
|
|
456
525
|
runnerName: arg("runner-name"),
|
|
457
526
|
sourceSha: arg("source-sha"),
|
|
@@ -464,6 +533,7 @@ function planFromArgs(execute) {
|
|
|
464
533
|
securityGroupId: arg("security-group-id"),
|
|
465
534
|
instanceProfileName: arg("instance-profile-name"),
|
|
466
535
|
evidenceBucket: arg("evidence-bucket"),
|
|
536
|
+
stateTable: arg("state-table"),
|
|
467
537
|
jitParameterName: arg("jit-parameter"),
|
|
468
538
|
launchedAt: arg("launched-at", new Date().toISOString()),
|
|
469
539
|
});
|
|
@@ -482,6 +552,12 @@ export function main() {
|
|
|
482
552
|
if (arg("confirm-run-id") !== plan.github.runId) {
|
|
483
553
|
throw new Error("--confirm-run-id must equal the exact GitHub run id");
|
|
484
554
|
}
|
|
555
|
+
if (arg("confirm-campaign-id") !== plan.campaign.id) {
|
|
556
|
+
throw new Error("--confirm-campaign-id must equal the campaign id");
|
|
557
|
+
}
|
|
558
|
+
if (arg("confirm-state-table") !== plan.aws.stateTable) {
|
|
559
|
+
throw new Error("--confirm-state-table must equal the campaign state table");
|
|
560
|
+
}
|
|
485
561
|
const result = executeWindowsJitLaunch(plan, {
|
|
486
562
|
profile: arg("aws-profile"),
|
|
487
563
|
});
|