@caelo-cms/provisioning 0.10.65 → 0.10.67

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.
Files changed (67) hide show
  1. package/dist/agent-db.js +37 -6
  2. package/dist/converge-job-gcp.d.ts.map +1 -1
  3. package/dist/converge-job-gcp.js +1 -1
  4. package/dist/converge-job-gcp.js.map +1 -1
  5. package/dist/install-state.d.ts +17 -2
  6. package/dist/install-state.d.ts.map +1 -1
  7. package/dist/install-state.js +11 -3
  8. package/dist/install-state.js.map +1 -1
  9. package/dist/lifecycle.d.ts +4 -1
  10. package/dist/lifecycle.d.ts.map +1 -1
  11. package/dist/lifecycle.js +12 -2
  12. package/dist/lifecycle.js.map +1 -1
  13. package/dist/release-manifest.d.ts +10 -4
  14. package/dist/release-manifest.d.ts.map +1 -1
  15. package/dist/release-manifest.js +17 -8
  16. package/dist/release-manifest.js.map +1 -1
  17. package/dist/upgrade/__tests__/live-indexes.d.ts +23 -0
  18. package/dist/upgrade/__tests__/live-indexes.d.ts.map +1 -0
  19. package/dist/upgrade/__tests__/live-indexes.js +41 -0
  20. package/dist/upgrade/__tests__/live-indexes.js.map +1 -0
  21. package/dist/upgrade/gcp-lookup.d.ts +22 -13
  22. package/dist/upgrade/gcp-lookup.d.ts.map +1 -1
  23. package/dist/upgrade/gcp-lookup.js +42 -3
  24. package/dist/upgrade/gcp-lookup.js.map +1 -1
  25. package/dist/upgrade/image-forms.d.ts +55 -0
  26. package/dist/upgrade/image-forms.d.ts.map +1 -0
  27. package/dist/upgrade/image-forms.js +164 -0
  28. package/dist/upgrade/image-forms.js.map +1 -0
  29. package/dist/upgrade/release-images.d.ts +24 -0
  30. package/dist/upgrade/release-images.d.ts.map +1 -1
  31. package/dist/upgrade/release-images.js +30 -0
  32. package/dist/upgrade/release-images.js.map +1 -1
  33. package/dist/upgrader-gcp.d.ts.map +1 -1
  34. package/dist/upgrader-gcp.js +3 -0
  35. package/dist/upgrader-gcp.js.map +1 -1
  36. package/dist/upgrader-job.d.ts +8 -0
  37. package/dist/upgrader-job.d.ts.map +1 -1
  38. package/dist/upgrader-job.js +51 -10
  39. package/dist/upgrader-job.js.map +1 -1
  40. package/dist/upgrader-records.d.ts +27 -2
  41. package/dist/upgrader-records.d.ts.map +1 -1
  42. package/dist/upgrader-records.js +77 -19
  43. package/dist/upgrader-records.js.map +1 -1
  44. package/dist/wizards/gcp.d.ts.map +1 -1
  45. package/dist/wizards/gcp.js +8 -1
  46. package/dist/wizards/gcp.js.map +1 -1
  47. package/package.json +2 -2
  48. package/src/converge-job-gcp.ts +1 -0
  49. package/src/install-state.test.ts +32 -0
  50. package/src/install-state.ts +21 -2
  51. package/src/lifecycle.ts +19 -2
  52. package/src/release-manifest.test.ts +27 -2
  53. package/src/release-manifest.ts +20 -7
  54. package/src/upgrade/__tests__/fixtures/admin-0.10.64-index.json +28 -0
  55. package/src/upgrade/__tests__/fixtures/gateway-0.10.64-index.json +28 -0
  56. package/src/upgrade/__tests__/live-indexes.ts +61 -0
  57. package/src/upgrade/gcp-lookup.test.ts +53 -9
  58. package/src/upgrade/gcp-lookup.ts +60 -15
  59. package/src/upgrade/image-forms.test.ts +192 -0
  60. package/src/upgrade/image-forms.ts +226 -0
  61. package/src/upgrade/release-images.ts +49 -0
  62. package/src/upgrader-gcp.ts +3 -0
  63. package/src/upgrader-job.test.ts +153 -0
  64. package/src/upgrader-job.ts +73 -12
  65. package/src/upgrader-records.test.ts +202 -0
  66. package/src/upgrader-records.ts +115 -19
  67. package/src/wizards/gcp.ts +17 -1
@@ -15,6 +15,12 @@
15
15
  * Images that belong to no signed release are a deployment nobody can
16
16
  * vouch for, so the run stops and says so.
17
17
  *
18
+ * A serving image is a release's image when its digest is one of that
19
+ * image's forms (#1025, upgrade/image-forms.ts): the index the manifest
20
+ * signs, or a platform manifest inside it. Cloud Run records the
21
+ * linux/amd64 platform manifest it resolved, so comparing with the index
22
+ * alone never matched on a live install.
23
+ *
18
24
  * Which releases to try comes from untrusted hints — the version
19
25
  * install.json names, and the release tags pointing at the serving digests
20
26
  * in the fixed mirror (releaseVersionForDigest). A wrong hint costs one
@@ -25,6 +31,7 @@ import type { GcloudRunner } from "./gcloud-retry.js";
25
31
  import type { ImageDigests, InstallMetadata } from "./install-state.js";
26
32
  import type { ReleaseManifest, ReleaseManifestResult } from "./release-manifest.js";
27
33
  import type { RollSlug } from "./upgrade/app-roll.js";
34
+ import type { ReleaseImageForms } from "./upgrade/image-forms.js";
28
35
  import { releaseVersionForDigest } from "./upgrade/release-images.js";
29
36
 
30
37
  /** Every digest each serving image is known by (gcp-lookup.ts servingImageDigests). */
@@ -32,18 +39,35 @@ export type ServingDigests = Readonly<Record<RollSlug, readonly string[]>>;
32
39
 
33
40
  const SLUGS: readonly RollSlug[] = ["admin", "gateway"];
34
41
 
35
- /** Whether `recorded` names the images that serve, in any form they are known by. */
42
+ /**
43
+ * Whether install.json names the images that serve: per service, the
44
+ * recorded index digest or its recorded linux/amd64 form (#1025) is one of
45
+ * the digests the serving revision is known by.
46
+ */
36
47
  export function recordMatchesServing(
37
- recorded: ImageDigests | undefined,
48
+ recorded: Pick<InstallMetadata, "imageDigests" | "platformDigests">,
38
49
  serving: ServingDigests,
39
50
  ): boolean {
40
- if (!recorded) return false;
41
- return SLUGS.every((slug) => serving[slug].includes(recorded[slug]));
51
+ const { imageDigests, platformDigests } = recorded;
52
+ if (!imageDigests) return false;
53
+ return SLUGS.every(
54
+ (slug) =>
55
+ serving[slug].includes(imageDigests[slug]) ||
56
+ (platformDigests !== undefined && serving[slug].includes(platformDigests[slug])),
57
+ );
42
58
  }
43
59
 
44
60
  /** What {@link findServingRelease} learned. */
45
61
  export type ServingRelease =
46
- | { readonly ok: true; readonly manifest: ReleaseManifest }
62
+ | {
63
+ readonly ok: true;
64
+ readonly manifest: ReleaseManifest;
65
+ /**
66
+ * The linux/amd64 form of each of its images (#1025), to record next
67
+ * to the index digests; null when it is not known for both.
68
+ */
69
+ readonly platformDigests: ImageDigests | null;
70
+ }
47
71
  | {
48
72
  readonly ok: false;
49
73
  /**
@@ -62,6 +86,15 @@ export interface ServingReleaseDeps {
62
86
  readonly run: GcloudRunner;
63
87
  /** fetchVerifiedReleaseManifest against the fixed registry. */
64
88
  readonly fetchManifest: (version: string) => Promise<ReleaseManifestResult>;
89
+ /** Every digest a release image is known by (#1025; createReleaseImageForms). */
90
+ readonly imageForms: ReleaseImageForms;
91
+ /**
92
+ * The image digests the services' templates name (the index a deploy
93
+ * pinned; after a rollback, possibly another release's). Untrusted hints
94
+ * for which release tags to look up, asked only when nothing else found
95
+ * the serving release.
96
+ */
97
+ readonly hintDigests?: () => Promise<ServingDigests>;
65
98
  }
66
99
 
67
100
  /**
@@ -79,7 +112,7 @@ export async function findServingRelease(
79
112
  const unverified: string[] = [];
80
113
  const unreachable: string[] = [];
81
114
  const tried: string[] = [];
82
- for await (const version of candidateVersions(meta, serving, deps.run)) {
115
+ for await (const version of candidateVersions(meta, serving, deps)) {
83
116
  if (tried.includes(version)) continue;
84
117
  tried.push(version);
85
118
  const fetched = await deps.fetchManifest(version);
@@ -92,12 +125,21 @@ export async function findServingRelease(
92
125
  continue;
93
126
  }
94
127
  const m = fetched.manifest;
95
- const matching = SLUGS.filter((slug) => serving[slug].includes(m.digests[slug]));
96
- if (matching.length === SLUGS.length) return { ok: true, manifest: m };
97
- if (matching.length > 0) {
98
- const other = SLUGS.filter((s) => !matching.includes(s));
128
+ const judged = await judgeRelease(m, serving, deps.imageForms);
129
+ if (judged.kind === "unreadable") {
130
+ // The index could not be read, so this release is neither confirmed
131
+ // nor ruled out: an outage is retried, anything else is a release
132
+ // record that could not be verified.
133
+ (judged.transient ? unreachable : unverified).push(`v${version} (${judged.message})`);
134
+ continue;
135
+ }
136
+ if (judged.matching.length === SLUGS.length) {
137
+ return { ok: true, manifest: m, platformDigests: judged.platformDigests };
138
+ }
139
+ if (judged.matching.length > 0) {
140
+ const other = SLUGS.filter((s) => !judged.matching.includes(s));
99
141
  partial.push(
100
- `the ${matching.join(", ")} serves v${m.version}'s image, the ${other.join(", ")} does not`,
142
+ `the ${judged.matching.join(", ")} serves v${m.version}'s image, the ${other.join(", ")} does not`,
101
143
  );
102
144
  }
103
145
  }
@@ -125,6 +167,51 @@ export async function findServingRelease(
125
167
  };
126
168
  }
127
169
 
170
+ /** Which of release `m`'s images serve, by {@link judgeRelease}. */
171
+ type ReleaseJudgement =
172
+ | {
173
+ readonly kind: "judged";
174
+ readonly matching: readonly RollSlug[];
175
+ readonly platformDigests: ImageDigests | null;
176
+ }
177
+ | { readonly kind: "unreadable"; readonly transient: boolean; readonly message: string };
178
+
179
+ /**
180
+ * Which of `m`'s images serve. A serving digest equal to the signed index
181
+ * matches without a lookup; any other is compared with the image's
182
+ * platform forms (signed, or read from the content-checked index).
183
+ */
184
+ async function judgeRelease(
185
+ m: ReleaseManifest,
186
+ serving: ServingDigests,
187
+ imageForms: ReleaseImageForms,
188
+ ): Promise<ReleaseJudgement> {
189
+ const matching: RollSlug[] = [];
190
+ const platform: Partial<Record<RollSlug, string>> = {};
191
+ for (const slug of SLUGS) {
192
+ if (serving[slug].includes(m.digests[slug])) {
193
+ matching.push(slug);
194
+ const signed = m.platformDigests?.[slug];
195
+ if (signed) platform[slug] = signed;
196
+ continue;
197
+ }
198
+ const forms = await imageForms.of(m, slug);
199
+ if (!forms.ok)
200
+ return { kind: "unreadable", transient: forms.transient, message: forms.message };
201
+ const match = serving[slug].find((d) => forms.digests.includes(d));
202
+ if (match) {
203
+ matching.push(slug);
204
+ if (forms.amd64) platform[slug] = forms.amd64;
205
+ }
206
+ }
207
+ const { admin, gateway } = platform;
208
+ return {
209
+ kind: "judged",
210
+ matching,
211
+ platformDigests: admin && gateway ? { admin, gateway } : null,
212
+ };
213
+ }
214
+
128
215
  /**
129
216
  * Release versions worth checking, most likely first. Lazy: the version
130
217
  * install.json names usually is the serving one, and then no tag listing
@@ -133,17 +220,26 @@ export async function findServingRelease(
133
220
  async function* candidateVersions(
134
221
  meta: InstallMetadata,
135
222
  serving: ServingDigests,
136
- run: GcloudRunner,
223
+ deps: ServingReleaseDeps,
137
224
  ): AsyncGenerator<string> {
138
225
  if (meta.releaseVersion) yield meta.releaseVersion;
139
- // Release tags point at the index digest a deploy pins (the deployed form,
140
- // listed first); the platform digest Cloud Run resolved has no tag.
141
- for (const slug of SLUGS) {
142
- for (const digest of serving[slug]) {
143
- const version = await releaseVersionForDigest(run, slug, digest);
144
- if (version) yield version;
226
+ // Release tags point at the index digest a deploy pins. The platform
227
+ // digest Cloud Run records for a revision (#1025) has no tag, so the
228
+ // indexes the service templates name are asked too: only hints, the
229
+ // signed manifest still decides.
230
+ const asked: string[] = [];
231
+ const tagged = async function* (digests: ServingDigests): AsyncGenerator<string> {
232
+ for (const slug of SLUGS) {
233
+ for (const digest of digests[slug]) {
234
+ if (asked.includes(`${slug}@${digest}`)) continue;
235
+ asked.push(`${slug}@${digest}`);
236
+ const version = await releaseVersionForDigest(deps.run, slug, digest);
237
+ if (version) yield version;
238
+ }
145
239
  }
146
- }
240
+ };
241
+ yield* tagged(serving);
242
+ if (deps.hintDigests) yield* tagged(await deps.hintDigests());
147
243
  }
148
244
 
149
245
  function capitalize(s: string): string {
@@ -120,6 +120,7 @@ import {
120
120
  } from "../state-backend.js";
121
121
  import { acquireStateLock, GcsStateStore } from "../state-store.js";
122
122
  import { lookupFailure, resolveGcpResourceName } from "../upgrade/gcp-lookup.js";
123
+ import { createReleaseImageForms, platformDigestsOf } from "../upgrade/image-forms.js";
123
124
  import {
124
125
  releaseVersionForDigest,
125
126
  resolveOpsImageRef,
@@ -500,7 +501,22 @@ export async function runGcpConverge(opts: GcpConvergeOpts): Promise<void> {
500
501
  // the admin can install the next update itself.
501
502
  if (upgraderReady && convergeReady && remoteMcpReady) {
502
503
  const version = releaseVersion;
503
- recordImageDigests(installId, imageDigests, version);
504
+ // #1025 — the linux/amd64 forms Cloud Run reports, next to the indexes,
505
+ // so the upgrader recognises what serves without a registry lookup.
506
+ const platform = await platformDigestsOf(imageDigests, createReleaseImageForms());
507
+ if (!platform.ok) {
508
+ log.warn(
509
+ yellow(
510
+ `install.json records the release by its image indexes only; their linux/amd64 forms could not be read (${platform.message}). The first in-product update reads them from the release registry.`,
511
+ ),
512
+ );
513
+ }
514
+ recordImageDigests(
515
+ installId,
516
+ imageDigests,
517
+ version,
518
+ platform.ok ? platform.platformDigests : null,
519
+ );
504
520
  recordInfraContractVersion(installId, INFRA_CONTRACT_VERSION);
505
521
  if (!version) {
506
522
  log.warn(