@deployfoundation/foundation-deploy 0.2.5 → 0.2.7
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 +1 -1
- package/dist/bin/app.js +1 -1
- package/dist/bin/foundation-deploy.js +98 -8
- package/dist/{chunk-my0awr1g.js → chunk-revzxtba.js} +11 -1
- package/dist/src/index.js +1 -1
- package/package.json +1 -1
- package/src/deploy/deploy.ts +94 -9
- package/src/deploy/setup.ts +17 -2
- package/src/stacks/agent-stack.ts +20 -0
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ bun run packages/infra/bin/foundation-deploy.ts <command> --instance <path>
|
|
|
47
47
|
|
|
48
48
|
| Command | What it does |
|
|
49
49
|
| --- | --- |
|
|
50
|
-
| `deploy` | Build and push the agent image, `cdk deploy --all`, refresh the runtime secret, sync config. |
|
|
50
|
+
| `deploy` | Build and push the agent image, `cdk deploy --all`, refresh the runtime secret (and `cdk deploy` once more when that changed it, so no container boots on the old one), sync config. |
|
|
51
51
|
| `post-deploy` | Smoke DEFAULT, probe the persistent mount, promote `live`, smoke `live`. |
|
|
52
52
|
| `config:sync` | Upload the runtime config and Foundation's product skills to S3. |
|
|
53
53
|
| `setup` | First-deploy orchestration, idempotent. |
|
package/dist/bin/app.js
CHANGED
|
@@ -87,6 +87,7 @@ async function extractSkillsTarball(tarball, dir, opts = {}) {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
// src/deploy/deploy.ts
|
|
90
|
+
import { createHash } from "node:crypto";
|
|
90
91
|
import { existsSync as existsSync2 } from "node:fs";
|
|
91
92
|
import { createRequire } from "node:module";
|
|
92
93
|
import { join as join3 } from "node:path";
|
|
@@ -273,8 +274,48 @@ var UPWORK_GRAPHQL_DOCUMENTS = Object.freeze({
|
|
|
273
274
|
marketplaceJobPosting(id: $id) {
|
|
274
275
|
id
|
|
275
276
|
content { title description }
|
|
276
|
-
contractTerms {
|
|
277
|
-
|
|
277
|
+
contractTerms {
|
|
278
|
+
contractType
|
|
279
|
+
experienceLevel
|
|
280
|
+
personsToHire
|
|
281
|
+
fixedPriceContractTerms {
|
|
282
|
+
amount { rawValue currency displayValue }
|
|
283
|
+
maxAmount { rawValue currency displayValue }
|
|
284
|
+
engagementDuration { label weeks }
|
|
285
|
+
}
|
|
286
|
+
hourlyContractTerms {
|
|
287
|
+
engagementDuration { label weeks }
|
|
288
|
+
engagementType
|
|
289
|
+
hourlyBudgetType
|
|
290
|
+
hourlyBudgetMin
|
|
291
|
+
hourlyBudgetMax
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
contractorSelection {
|
|
295
|
+
proposalRequirement {
|
|
296
|
+
coverLetterRequired
|
|
297
|
+
freelancerMilestonesAllowed
|
|
298
|
+
screeningQuestions { question sequenceNumber }
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
clientCompanyPublic {
|
|
302
|
+
id
|
|
303
|
+
country { name twoLetterAbbreviation }
|
|
304
|
+
paymentVerification { status paymentVerified }
|
|
305
|
+
}
|
|
306
|
+
activityStat {
|
|
307
|
+
applicationsBidStats {
|
|
308
|
+
avgRateBid { rawValue currency displayValue }
|
|
309
|
+
minRateBid { rawValue currency displayValue }
|
|
310
|
+
maxRateBid { rawValue currency displayValue }
|
|
311
|
+
}
|
|
312
|
+
jobActivity {
|
|
313
|
+
lastClientActivity
|
|
314
|
+
invitesSent
|
|
315
|
+
totalInvitedToInterview
|
|
316
|
+
totalHired
|
|
317
|
+
}
|
|
318
|
+
}
|
|
278
319
|
}
|
|
279
320
|
}`
|
|
280
321
|
}),
|
|
@@ -313,6 +354,14 @@ var UPWORK_GRAPHQL_DOCUMENTS = Object.freeze({
|
|
|
313
354
|
connectsBalanceFree
|
|
314
355
|
connectsBalancePaid
|
|
315
356
|
}
|
|
357
|
+
}`
|
|
358
|
+
}),
|
|
359
|
+
account_context: Object.freeze({
|
|
360
|
+
operationName: "AccountContext",
|
|
361
|
+
query: `query AccountContext {
|
|
362
|
+
user { id nid rid name }
|
|
363
|
+
organization { id rid name }
|
|
364
|
+
companySelector { items { title organizationId organizationRid typeTitle } }
|
|
316
365
|
}`
|
|
317
366
|
}),
|
|
318
367
|
submit_proposal: Object.freeze({
|
|
@@ -982,7 +1031,8 @@ async function cdkDeploy(ctx, opts) {
|
|
|
982
1031
|
...opts.alarmEmail === "" ? [] : ["-c", `alarmEmail=${opts.alarmEmail}`],
|
|
983
1032
|
...opts.phase1 === true ? ["-c", "deployRuntime=false"] : [],
|
|
984
1033
|
...opts.tag === undefined || opts.release !== undefined ? [] : ["-c", `agentImageTag=${opts.tag}`],
|
|
985
|
-
...opts.release === undefined ? [] : releaseContext(opts.release)
|
|
1034
|
+
...opts.release === undefined ? [] : releaseContext(opts.release),
|
|
1035
|
+
...opts.runtimeSecretDigest === undefined ? [] : ["-c", `runtimeSecretDigest=${opts.runtimeSecretDigest}`]
|
|
986
1036
|
];
|
|
987
1037
|
await run([
|
|
988
1038
|
...cdkCommand(),
|
|
@@ -1082,6 +1132,28 @@ async function syncRuntimeSecret(ctx) {
|
|
|
1082
1132
|
await putSecretJson(ctx, secretRuntime, value);
|
|
1083
1133
|
return Object.keys(value);
|
|
1084
1134
|
}
|
|
1135
|
+
function runtimeSecretDigest(value) {
|
|
1136
|
+
return createHash("sha256").update(value).digest("hex").slice(0, 16);
|
|
1137
|
+
}
|
|
1138
|
+
async function currentRuntimeSecretDigest(ctx) {
|
|
1139
|
+
if (ctx.dryRun === true)
|
|
1140
|
+
return;
|
|
1141
|
+
try {
|
|
1142
|
+
return runtimeSecretDigest(await readSecretString(ctx, ctx.names.secretRuntime));
|
|
1143
|
+
} catch {
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
async function refreshRuntimeSecret(before, refresh) {
|
|
1148
|
+
const log = refresh.log ?? ((line) => console.log(line));
|
|
1149
|
+
log(` keys: ${(await refresh.sync()).join(", ")}`);
|
|
1150
|
+
const after = await refresh.digest();
|
|
1151
|
+
if (after === undefined || after === before)
|
|
1152
|
+
return "unchanged";
|
|
1153
|
+
log("▶ the runtime secret changed: cdk deploy once more, so no container boots on the old one");
|
|
1154
|
+
await refresh.redeploy(after);
|
|
1155
|
+
return "rolled";
|
|
1156
|
+
}
|
|
1085
1157
|
async function promoteLive(ctx) {
|
|
1086
1158
|
const dry = ctx.dryRun === true;
|
|
1087
1159
|
const runner = cliRunner(ctx);
|
|
@@ -1136,19 +1208,26 @@ async function deploy(ctx, opts) {
|
|
|
1136
1208
|
console.log(` ${await buildAndPushImage(ctx, { tag })}`);
|
|
1137
1209
|
}
|
|
1138
1210
|
console.log(`▶ cdk deploy --all${phase1 ? " (phase 1: no runtime)" : release !== undefined ? ` (release ${release.version})` : ` (image ${tag})`}`);
|
|
1139
|
-
await
|
|
1211
|
+
const deployed = await currentRuntimeSecretDigest(ctx);
|
|
1212
|
+
const deployStacks = (runtimeSecretDigest2) => cdkDeploy(ctx, {
|
|
1140
1213
|
admins,
|
|
1141
1214
|
alarmEmail,
|
|
1142
1215
|
tag,
|
|
1143
1216
|
phase1,
|
|
1144
|
-
...release === undefined ? {} : { release }
|
|
1217
|
+
...release === undefined ? {} : { release },
|
|
1218
|
+
...runtimeSecretDigest2 === undefined ? {} : { runtimeSecretDigest: runtimeSecretDigest2 }
|
|
1145
1219
|
});
|
|
1220
|
+
await deployStacks(deployed);
|
|
1146
1221
|
if (ctx.instance.integrations.knock) {
|
|
1147
1222
|
console.log(`▶ Knock OAuth public-client registration (${ctx.names.secretKnockOauthClient})`);
|
|
1148
1223
|
console.log(` ${await syncKnockOAuthClient(ctx)}`);
|
|
1149
1224
|
}
|
|
1150
1225
|
console.log(`▶ runtime secret refresh (${ctx.names.secretRuntime})`);
|
|
1151
|
-
|
|
1226
|
+
await refreshRuntimeSecret(phase1 ? undefined : deployed, {
|
|
1227
|
+
sync: () => syncRuntimeSecret(ctx),
|
|
1228
|
+
digest: async () => phase1 ? undefined : currentRuntimeSecretDigest(ctx),
|
|
1229
|
+
redeploy: (digest) => deployStacks(digest)
|
|
1230
|
+
});
|
|
1152
1231
|
console.log("▶ config sync");
|
|
1153
1232
|
const bucket = await configSync(ctx, {
|
|
1154
1233
|
runtimeConfigPath: customization.runtimeConfigPath,
|
|
@@ -1771,9 +1850,20 @@ async function setup(ctx, opts = {}) {
|
|
|
1771
1850
|
console.log(` ${repositoryUri}:${tag}`);
|
|
1772
1851
|
await buildAndPushImage(ctx, { tag, repositoryUri });
|
|
1773
1852
|
step(8, "phase 2: deploy the AgentCore runtime");
|
|
1774
|
-
|
|
1853
|
+
const phase2 = (runtimeSecretDigest2) => cdkDeploy(ctx, {
|
|
1854
|
+
admins,
|
|
1855
|
+
alarmEmail,
|
|
1856
|
+
tag,
|
|
1857
|
+
...runtimeSecretDigest2 === undefined ? {} : { runtimeSecretDigest: runtimeSecretDigest2 }
|
|
1858
|
+
});
|
|
1859
|
+
const written = await currentRuntimeSecretDigest(ctx);
|
|
1860
|
+
await phase2(written);
|
|
1775
1861
|
step(9, `refresh ${ctx.names.secretRuntime} with the runtime arn`);
|
|
1776
|
-
await
|
|
1862
|
+
await refreshRuntimeSecret(written, {
|
|
1863
|
+
sync: () => syncRuntimeSecret(ctx),
|
|
1864
|
+
digest: () => currentRuntimeSecretDigest(ctx),
|
|
1865
|
+
redeploy: phase2
|
|
1866
|
+
});
|
|
1777
1867
|
step(10, "create the pinned `live` endpoint");
|
|
1778
1868
|
await ensureLiveEndpoint(ctx);
|
|
1779
1869
|
step(11, `seed ${ctx.names.secretCodex}`);
|
|
@@ -41,6 +41,7 @@ class FoundationAgent extends cdk.Stack {
|
|
|
41
41
|
const display = props.instance.displayName;
|
|
42
42
|
const deployRuntime = String(this.node.tryGetContext("deployRuntime") ?? "true") !== "false";
|
|
43
43
|
const imageTag = this.node.tryGetContext("agentImageTag");
|
|
44
|
+
const secretDigest = runtimeSecretDigestContext(this.node.tryGetContext("runtimeSecretDigest"));
|
|
44
45
|
this.repository = new ecr.Repository(this, "AgentRepository", {
|
|
45
46
|
repositoryName: names.ecrRepo,
|
|
46
47
|
imageScanOnPush: true,
|
|
@@ -124,7 +125,8 @@ class FoundationAgent extends cdk.Stack {
|
|
|
124
125
|
EnvironmentVariables: {
|
|
125
126
|
RUNTIME_SECRET_ID: props.secrets.runtime.secretName,
|
|
126
127
|
AWS_REGION: this.region,
|
|
127
|
-
FOUNDATION_WORKSPACE_ROOT: "/workspace"
|
|
128
|
+
FOUNDATION_WORKSPACE_ROOT: "/workspace",
|
|
129
|
+
...secretDigest === undefined ? {} : { FOUNDATION_RUNTIME_SECRET_DIGEST: secretDigest }
|
|
128
130
|
},
|
|
129
131
|
Tags: { project: props.instance.naming.secretPrefix }
|
|
130
132
|
}
|
|
@@ -661,6 +663,14 @@ class FoundationAgent extends cdk.Stack {
|
|
|
661
663
|
function secretArnPattern(secret) {
|
|
662
664
|
return secret.secretFullArn ?? `${secret.secretArn}-??????`;
|
|
663
665
|
}
|
|
666
|
+
function runtimeSecretDigestContext(value) {
|
|
667
|
+
if (value === undefined || value === "")
|
|
668
|
+
return;
|
|
669
|
+
const digest = String(value);
|
|
670
|
+
if (!/^[0-9a-f]{16}$/.test(digest))
|
|
671
|
+
throw new Error(`runtimeSecretDigest must be 16 lowercase hex characters, got "${digest}"`);
|
|
672
|
+
return digest;
|
|
673
|
+
}
|
|
664
674
|
|
|
665
675
|
// src/stacks/api-stack.ts
|
|
666
676
|
import { readFileSync } from "node:fs";
|
package/dist/src/index.js
CHANGED
package/package.json
CHANGED
package/src/deploy/deploy.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* `--phase1` deploys everything except the runtime (`-c deployRuntime=false`);
|
|
12
12
|
* a plain `deploy` afterwards builds, pushes and creates it.
|
|
13
13
|
*/
|
|
14
|
+
import { createHash } from "node:crypto";
|
|
14
15
|
import { existsSync } from "node:fs";
|
|
15
16
|
import { createRequire } from "node:module";
|
|
16
17
|
import { join } from "node:path";
|
|
@@ -18,7 +19,15 @@ import {
|
|
|
18
19
|
parseKnockOAuthClient,
|
|
19
20
|
registerKnockPublicClient,
|
|
20
21
|
} from "@deployfoundation/foundation-connectors";
|
|
21
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
type AwsContext,
|
|
24
|
+
aws,
|
|
25
|
+
cdkEnv,
|
|
26
|
+
putSecretJson,
|
|
27
|
+
readSecretJson,
|
|
28
|
+
readSecretString,
|
|
29
|
+
stackOutput,
|
|
30
|
+
} from "./aws.ts";
|
|
22
31
|
import { configSync } from "./config-sync.ts";
|
|
23
32
|
import { adminsCsvFromFile } from "./config.ts";
|
|
24
33
|
import {
|
|
@@ -206,6 +215,8 @@ export interface CdkDeployOptions {
|
|
|
206
215
|
phase1?: boolean;
|
|
207
216
|
/** A verified release; its manifest is what the app reads its artifacts from. */
|
|
208
217
|
release?: ResolvedRelease;
|
|
218
|
+
/** {@link runtimeSecretDigest} of the runtime secret the new version should boot with. */
|
|
219
|
+
runtimeSecretDigest?: string;
|
|
209
220
|
}
|
|
210
221
|
|
|
211
222
|
export async function cdkDeploy(ctx: AwsContext, opts: CdkDeployOptions): Promise<void> {
|
|
@@ -225,6 +236,9 @@ export async function cdkDeploy(ctx: AwsContext, opts: CdkDeployOptions): Promis
|
|
|
225
236
|
? []
|
|
226
237
|
: ["-c", `agentImageTag=${opts.tag}`]),
|
|
227
238
|
...(opts.release === undefined ? [] : releaseContext(opts.release)),
|
|
239
|
+
...(opts.runtimeSecretDigest === undefined
|
|
240
|
+
? []
|
|
241
|
+
: ["-c", `runtimeSecretDigest=${opts.runtimeSecretDigest}`]),
|
|
228
242
|
];
|
|
229
243
|
await run(
|
|
230
244
|
[
|
|
@@ -410,6 +424,68 @@ export async function syncRuntimeSecret(ctx: AwsContext): Promise<string[]> {
|
|
|
410
424
|
return Object.keys(value);
|
|
411
425
|
}
|
|
412
426
|
|
|
427
|
+
/**
|
|
428
|
+
* A short, one-way digest of the runtime secret as stored. It ends up in the
|
|
429
|
+
* runtime's environment (see the Agent stack), so it is truncated SHA-256 of
|
|
430
|
+
* a document full of high-entropy tokens, never anything reversible.
|
|
431
|
+
*/
|
|
432
|
+
export function runtimeSecretDigest(value: string): string {
|
|
433
|
+
return createHash("sha256").update(value).digest("hex").slice(0, 16);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** The digest of the runtime secret now, or undefined when there is none yet (or in a dry run). */
|
|
437
|
+
export async function currentRuntimeSecretDigest(ctx: AwsContext): Promise<string | undefined> {
|
|
438
|
+
if (ctx.dryRun === true) return undefined;
|
|
439
|
+
try {
|
|
440
|
+
return runtimeSecretDigest(await readSecretString(ctx, ctx.names.secretRuntime));
|
|
441
|
+
} catch {
|
|
442
|
+
return undefined;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export interface RuntimeSecretRefresh {
|
|
447
|
+
/** Rewrite the secret from the stack outputs; returns the key names. */
|
|
448
|
+
sync: () => Promise<string[]>;
|
|
449
|
+
/** The secret's digest as it stands now. */
|
|
450
|
+
digest: () => Promise<string | undefined>;
|
|
451
|
+
/** `cdk deploy` again with this digest, which cuts a new runtime version. */
|
|
452
|
+
redeploy: (digest: string) => Promise<void>;
|
|
453
|
+
log?: (line: string) => void;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Refresh the runtime secret, and when that changed it, roll the runtime.
|
|
458
|
+
*
|
|
459
|
+
* A container reads the secret once, at boot, and AgentCore boots a pool of
|
|
460
|
+
* them ahead of demand as soon as a version exists. The secret is composed from
|
|
461
|
+
* stack outputs (the runtime's own ARN among them), so it can only be written
|
|
462
|
+
* AFTER `cdk deploy` created the version, by which time that pool has already
|
|
463
|
+
* booted on the old value: a first deploy's placeholder, or an upgrade's stale
|
|
464
|
+
* outputs. Those containers answer with the fallback config until they
|
|
465
|
+
* recycle, and every session they serve in the meantime is broken. Found by
|
|
466
|
+
* migrating Sky, where a dozen of them failed smoke after smoke.
|
|
467
|
+
*
|
|
468
|
+
* Writing first is impossible, and `update-agent-runtime` from the CLI would
|
|
469
|
+
* resend an incomplete definition (the CLI does not carry the S3 Files mount),
|
|
470
|
+
* so the new version comes from CloudFormation: `cdk deploy` once more with the
|
|
471
|
+
* new digest in the runtime's environment. An upgrade whose outputs did not
|
|
472
|
+
* change produces the same digest and costs nothing.
|
|
473
|
+
*
|
|
474
|
+
* `before` is the digest the version just deployed was given.
|
|
475
|
+
*/
|
|
476
|
+
export async function refreshRuntimeSecret(
|
|
477
|
+
before: string | undefined,
|
|
478
|
+
refresh: RuntimeSecretRefresh,
|
|
479
|
+
): Promise<"unchanged" | "rolled"> {
|
|
480
|
+
const log = refresh.log ?? ((line: string) => console.log(line));
|
|
481
|
+
log(` keys: ${(await refresh.sync()).join(", ")}`);
|
|
482
|
+
const after = await refresh.digest();
|
|
483
|
+
if (after === undefined || after === before) return "unchanged";
|
|
484
|
+
log("▶ the runtime secret changed: cdk deploy once more, so no container boots on the old one");
|
|
485
|
+
await refresh.redeploy(after);
|
|
486
|
+
return "rolled";
|
|
487
|
+
}
|
|
488
|
+
|
|
413
489
|
/**
|
|
414
490
|
* What the workflow does after a deploy, for a deploy run from a laptop: smoke
|
|
415
491
|
* the version just created on DEFAULT, move the pinned `live` endpoint onto
|
|
@@ -543,20 +619,29 @@ export async function deploy(ctx: AwsContext, opts: DeployOptions): Promise<void
|
|
|
543
619
|
: ` (image ${tag})`
|
|
544
620
|
}`,
|
|
545
621
|
);
|
|
546
|
-
await
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
622
|
+
const deployed = await currentRuntimeSecretDigest(ctx);
|
|
623
|
+
const deployStacks = (runtimeSecretDigest: string | undefined) =>
|
|
624
|
+
cdkDeploy(ctx, {
|
|
625
|
+
admins,
|
|
626
|
+
alarmEmail,
|
|
627
|
+
tag,
|
|
628
|
+
phase1,
|
|
629
|
+
...(release === undefined ? {} : { release }),
|
|
630
|
+
...(runtimeSecretDigest === undefined ? {} : { runtimeSecretDigest }),
|
|
631
|
+
});
|
|
632
|
+
await deployStacks(deployed);
|
|
553
633
|
if (ctx.instance.integrations.knock) {
|
|
554
634
|
console.log(`▶ Knock OAuth public-client registration (${ctx.names.secretKnockOauthClient})`);
|
|
555
635
|
console.log(` ${await syncKnockOAuthClient(ctx)}`);
|
|
556
636
|
}
|
|
557
637
|
|
|
558
638
|
console.log(`▶ runtime secret refresh (${ctx.names.secretRuntime})`);
|
|
559
|
-
|
|
639
|
+
await refreshRuntimeSecret(phase1 ? undefined : deployed, {
|
|
640
|
+
sync: () => syncRuntimeSecret(ctx),
|
|
641
|
+
// Phase 1 has no runtime to roll.
|
|
642
|
+
digest: async () => (phase1 ? undefined : currentRuntimeSecretDigest(ctx)),
|
|
643
|
+
redeploy: (digest) => deployStacks(digest),
|
|
644
|
+
});
|
|
560
645
|
|
|
561
646
|
console.log("▶ config sync");
|
|
562
647
|
const bucket = await configSync(ctx, {
|
package/src/deploy/setup.ts
CHANGED
|
@@ -31,6 +31,8 @@ import {
|
|
|
31
31
|
buildAndPushImage,
|
|
32
32
|
cdkCommand,
|
|
33
33
|
cdkDeploy,
|
|
34
|
+
currentRuntimeSecretDigest,
|
|
35
|
+
refreshRuntimeSecret,
|
|
34
36
|
syncKnockOAuthClient,
|
|
35
37
|
syncRuntimeSecret,
|
|
36
38
|
} from "./deploy.ts";
|
|
@@ -375,12 +377,25 @@ export async function setup(
|
|
|
375
377
|
await buildAndPushImage(ctx, { tag, repositoryUri });
|
|
376
378
|
|
|
377
379
|
step(8, "phase 2: deploy the AgentCore runtime");
|
|
378
|
-
|
|
380
|
+
const phase2 = (runtimeSecretDigest: string | undefined) =>
|
|
381
|
+
cdkDeploy(ctx, {
|
|
382
|
+
admins,
|
|
383
|
+
alarmEmail,
|
|
384
|
+
tag,
|
|
385
|
+
...(runtimeSecretDigest === undefined ? {} : { runtimeSecretDigest }),
|
|
386
|
+
});
|
|
387
|
+
const written = await currentRuntimeSecretDigest(ctx);
|
|
388
|
+
await phase2(written);
|
|
379
389
|
|
|
380
390
|
// Only FOUNDATION_AGENT_RUNTIME_ARN needs phase 2, but it is what a routine's
|
|
381
391
|
// schedule targets, so the secret is composed once more now that it exists.
|
|
392
|
+
// That changes it, and phase 2's containers booted without it: roll them.
|
|
382
393
|
step(9, `refresh ${ctx.names.secretRuntime} with the runtime arn`);
|
|
383
|
-
await
|
|
394
|
+
await refreshRuntimeSecret(written, {
|
|
395
|
+
sync: () => syncRuntimeSecret(ctx),
|
|
396
|
+
digest: () => currentRuntimeSecretDigest(ctx),
|
|
397
|
+
redeploy: phase2,
|
|
398
|
+
});
|
|
384
399
|
|
|
385
400
|
step(10, "create the pinned `live` endpoint");
|
|
386
401
|
await ensureLiveEndpoint(ctx);
|
|
@@ -108,6 +108,7 @@ export class FoundationAgent extends cdk.Stack {
|
|
|
108
108
|
|
|
109
109
|
const deployRuntime = String(this.node.tryGetContext("deployRuntime") ?? "true") !== "false";
|
|
110
110
|
const imageTag: string | undefined = this.node.tryGetContext("agentImageTag");
|
|
111
|
+
const secretDigest = runtimeSecretDigestContext(this.node.tryGetContext("runtimeSecretDigest"));
|
|
111
112
|
|
|
112
113
|
this.repository = new ecr.Repository(this, "AgentRepository", {
|
|
113
114
|
repositoryName: names.ecrRepo,
|
|
@@ -237,6 +238,12 @@ export class FoundationAgent extends cdk.Stack {
|
|
|
237
238
|
RUNTIME_SECRET_ID: props.secrets.runtime.secretName,
|
|
238
239
|
AWS_REGION: this.region,
|
|
239
240
|
FOUNDATION_WORKSPACE_ROOT: "/workspace",
|
|
241
|
+
// Which runtime secret the containers of this version boot with.
|
|
242
|
+
// Nothing reads it: it is here so that a deploy whose secret refresh
|
|
243
|
+
// changed the secret can cut a new version through CloudFormation
|
|
244
|
+
// (the only path that carries every property, the mount included),
|
|
245
|
+
// and no pre-warmed container keeps serving the old value.
|
|
246
|
+
...(secretDigest === undefined ? {} : { FOUNDATION_RUNTIME_SECRET_DIGEST: secretDigest }),
|
|
240
247
|
},
|
|
241
248
|
// The App-level tag aspect cannot reach a raw CfnResource.
|
|
242
249
|
Tags: { project: props.instance.naming.secretPrefix },
|
|
@@ -1004,3 +1011,16 @@ export class FoundationAgent extends cdk.Stack {
|
|
|
1004
1011
|
function secretArnPattern(secret: secretsmanager.ISecret): string {
|
|
1005
1012
|
return secret.secretFullArn ?? `${secret.secretArn}-??????`;
|
|
1006
1013
|
}
|
|
1014
|
+
|
|
1015
|
+
/**
|
|
1016
|
+
* `-c runtimeSecretDigest=<16 hex>`, from `runtimeSecretDigest()` in the
|
|
1017
|
+
* deploy tool. Absent means "not known", which a first `cdk deploy` of a
|
|
1018
|
+
* brand-new instance is: its secret does not exist yet.
|
|
1019
|
+
*/
|
|
1020
|
+
function runtimeSecretDigestContext(value: unknown): string | undefined {
|
|
1021
|
+
if (value === undefined || value === "") return undefined;
|
|
1022
|
+
const digest = String(value);
|
|
1023
|
+
if (!/^[0-9a-f]{16}$/.test(digest))
|
|
1024
|
+
throw new Error(`runtimeSecretDigest must be 16 lowercase hex characters, got "${digest}"`);
|
|
1025
|
+
return digest;
|
|
1026
|
+
}
|