@caelo-cms/provisioning 0.10.46 → 0.10.48

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 (342) hide show
  1. package/dist/adc-impersonation.d.ts.map +1 -1
  2. package/dist/adc-impersonation.js +23 -1
  3. package/dist/adc-impersonation.js.map +1 -1
  4. package/dist/agent/apply.d.ts +45 -0
  5. package/dist/agent/apply.d.ts.map +1 -0
  6. package/dist/agent/apply.js +83 -0
  7. package/dist/agent/apply.js.map +1 -0
  8. package/dist/agent/cli-provenance.d.ts +42 -0
  9. package/dist/agent/cli-provenance.d.ts.map +1 -0
  10. package/dist/agent/cli-provenance.js +138 -0
  11. package/dist/agent/cli-provenance.js.map +1 -0
  12. package/dist/agent/command.d.ts +79 -0
  13. package/dist/agent/command.d.ts.map +1 -0
  14. package/dist/agent/command.js +454 -0
  15. package/dist/agent/command.js.map +1 -0
  16. package/dist/agent/config.d.ts +30 -0
  17. package/dist/agent/config.d.ts.map +1 -0
  18. package/dist/agent/config.js +60 -0
  19. package/dist/agent/config.js.map +1 -0
  20. package/dist/agent/lock.d.ts +13 -0
  21. package/dist/agent/lock.d.ts.map +1 -0
  22. package/dist/agent/lock.js +100 -0
  23. package/dist/agent/lock.js.map +1 -0
  24. package/dist/agent/queue.d.ts +35 -0
  25. package/dist/agent/queue.d.ts.map +1 -0
  26. package/dist/agent/queue.js +50 -0
  27. package/dist/agent/queue.js.map +1 -0
  28. package/dist/agent/run.d.ts +97 -0
  29. package/dist/agent/run.d.ts.map +1 -0
  30. package/dist/agent/run.js +198 -0
  31. package/dist/agent/run.js.map +1 -0
  32. package/dist/agent/systemd.d.ts +55 -0
  33. package/dist/agent/systemd.d.ts.map +1 -0
  34. package/dist/agent/systemd.js +171 -0
  35. package/dist/agent/systemd.js.map +1 -0
  36. package/dist/agent/units.d.ts +44 -0
  37. package/dist/agent/units.d.ts.map +1 -0
  38. package/dist/agent/units.js +127 -0
  39. package/dist/agent/units.js.map +1 -0
  40. package/dist/agent-db.js +18362 -0
  41. package/dist/cli.js +122 -16
  42. package/dist/cli.js.map +1 -1
  43. package/dist/cloudsql-iam-user.d.ts +39 -0
  44. package/dist/cloudsql-iam-user.d.ts.map +1 -0
  45. package/dist/cloudsql-iam-user.js +119 -0
  46. package/dist/cloudsql-iam-user.js.map +1 -0
  47. package/dist/compose.d.ts +16 -0
  48. package/dist/compose.d.ts.map +1 -1
  49. package/dist/compose.js +26 -1
  50. package/dist/compose.js.map +1 -1
  51. package/dist/dns/index.d.ts +2 -0
  52. package/dist/dns/index.d.ts.map +1 -1
  53. package/dist/dns/index.js +3 -2
  54. package/dist/dns/index.js.map +1 -1
  55. package/dist/dns/manual.d.ts +5 -0
  56. package/dist/dns/manual.d.ts.map +1 -1
  57. package/dist/dns/manual.js +28 -12
  58. package/dist/dns/manual.js.map +1 -1
  59. package/dist/gcloud.d.ts +2 -0
  60. package/dist/gcloud.d.ts.map +1 -1
  61. package/dist/gcloud.js +1 -1
  62. package/dist/gcloud.js.map +1 -1
  63. package/dist/gcp-names.d.ts +15 -0
  64. package/dist/gcp-names.d.ts.map +1 -1
  65. package/dist/gcp-names.js +23 -0
  66. package/dist/gcp-names.js.map +1 -1
  67. package/dist/index.d.ts +6 -0
  68. package/dist/index.d.ts.map +1 -1
  69. package/dist/index.js +4 -0
  70. package/dist/index.js.map +1 -1
  71. package/dist/infra-contract.d.ts +1 -1
  72. package/dist/infra-contract.js +7 -1
  73. package/dist/infra-contract.js.map +1 -1
  74. package/dist/install-job-runner.d.ts +13 -0
  75. package/dist/install-job-runner.d.ts.map +1 -0
  76. package/dist/install-job-runner.js +155 -0
  77. package/dist/install-job-runner.js.map +1 -0
  78. package/dist/install-job.d.ts +210 -0
  79. package/dist/install-job.d.ts.map +1 -0
  80. package/dist/install-job.js +449 -0
  81. package/dist/install-job.js.map +1 -0
  82. package/dist/install-state.d.ts +19 -1
  83. package/dist/install-state.d.ts.map +1 -1
  84. package/dist/install-state.js +31 -3
  85. package/dist/install-state.js.map +1 -1
  86. package/dist/lifecycle.d.ts.map +1 -1
  87. package/dist/lifecycle.js +24 -5
  88. package/dist/lifecycle.js.map +1 -1
  89. package/dist/migration-runner.d.ts +5 -0
  90. package/dist/migration-runner.d.ts.map +1 -1
  91. package/dist/migration-runner.js +1 -1
  92. package/dist/migration-runner.js.map +1 -1
  93. package/dist/operator-access.d.ts +14 -8
  94. package/dist/operator-access.d.ts.map +1 -1
  95. package/dist/operator-access.js +16 -102
  96. package/dist/operator-access.js.map +1 -1
  97. package/dist/run-mode.d.ts +27 -0
  98. package/dist/run-mode.d.ts.map +1 -0
  99. package/dist/run-mode.js +22 -0
  100. package/dist/run-mode.js.map +1 -0
  101. package/dist/runtime-secrets.d.ts +19 -0
  102. package/dist/runtime-secrets.d.ts.map +1 -1
  103. package/dist/runtime-secrets.js +44 -0
  104. package/dist/runtime-secrets.js.map +1 -1
  105. package/dist/self-hosted-roles.d.ts +22 -1
  106. package/dist/self-hosted-roles.d.ts.map +1 -1
  107. package/dist/self-hosted-roles.js +81 -12
  108. package/dist/self-hosted-roles.js.map +1 -1
  109. package/dist/self-hosted-upgrade.d.ts +17 -0
  110. package/dist/self-hosted-upgrade.d.ts.map +1 -1
  111. package/dist/self-hosted-upgrade.js +16 -1
  112. package/dist/self-hosted-upgrade.js.map +1 -1
  113. package/dist/stack-contract.d.ts +6 -0
  114. package/dist/stack-contract.d.ts.map +1 -1
  115. package/dist/stack-contract.js +10 -1
  116. package/dist/stack-contract.js.map +1 -1
  117. package/dist/state-attach.d.ts +23 -0
  118. package/dist/state-attach.d.ts.map +1 -0
  119. package/dist/state-attach.js +112 -0
  120. package/dist/state-attach.js.map +1 -0
  121. package/dist/state-backend.d.ts +11 -0
  122. package/dist/state-backend.d.ts.map +1 -1
  123. package/dist/state-backend.js +20 -0
  124. package/dist/state-backend.js.map +1 -1
  125. package/dist/state-command.d.ts +20 -0
  126. package/dist/state-command.d.ts.map +1 -0
  127. package/dist/state-command.js +237 -0
  128. package/dist/state-command.js.map +1 -0
  129. package/dist/state-migrate.d.ts +121 -0
  130. package/dist/state-migrate.d.ts.map +1 -0
  131. package/dist/state-migrate.js +290 -0
  132. package/dist/state-migrate.js.map +1 -0
  133. package/dist/state-pulumi.d.ts +17 -0
  134. package/dist/state-pulumi.d.ts.map +1 -0
  135. package/dist/state-pulumi.js +218 -0
  136. package/dist/state-pulumi.js.map +1 -0
  137. package/dist/state-store.d.ts +27 -0
  138. package/dist/state-store.d.ts.map +1 -1
  139. package/dist/state-store.js +65 -0
  140. package/dist/state-store.js.map +1 -1
  141. package/dist/upgrade/converge.d.ts +6 -0
  142. package/dist/upgrade/converge.d.ts.map +1 -1
  143. package/dist/upgrade/converge.js +59 -2
  144. package/dist/upgrade/converge.js.map +1 -1
  145. package/dist/upgrade/gcp-lookup.d.ts +6 -0
  146. package/dist/upgrade/gcp-lookup.d.ts.map +1 -1
  147. package/dist/upgrade/gcp-lookup.js +25 -0
  148. package/dist/upgrade/gcp-lookup.js.map +1 -1
  149. package/dist/upgrade/release-images.d.ts +12 -0
  150. package/dist/upgrade/release-images.d.ts.map +1 -1
  151. package/dist/upgrade/release-images.js +31 -0
  152. package/dist/upgrade/release-images.js.map +1 -1
  153. package/dist/upgrade/upgrade.d.ts +5 -0
  154. package/dist/upgrade/upgrade.d.ts.map +1 -1
  155. package/dist/upgrade/upgrade.js +28 -1
  156. package/dist/upgrade/upgrade.js.map +1 -1
  157. package/dist/upgrader-assess.d.ts +82 -0
  158. package/dist/upgrader-assess.d.ts.map +1 -0
  159. package/dist/upgrader-assess.js +116 -0
  160. package/dist/upgrader-assess.js.map +1 -0
  161. package/dist/upgrader-gcp.d.ts +17 -0
  162. package/dist/upgrader-gcp.d.ts.map +1 -0
  163. package/dist/upgrader-gcp.js +87 -0
  164. package/dist/upgrader-gcp.js.map +1 -0
  165. package/dist/upgrader-job.d.ts +131 -0
  166. package/dist/upgrader-job.d.ts.map +1 -0
  167. package/dist/upgrader-job.js +259 -0
  168. package/dist/upgrader-job.js.map +1 -0
  169. package/dist/upgrader-reporter.d.ts +35 -0
  170. package/dist/upgrader-reporter.d.ts.map +1 -0
  171. package/dist/upgrader-reporter.js +79 -0
  172. package/dist/upgrader-reporter.js.map +1 -0
  173. package/dist/upgrader-setup.d.ts +59 -0
  174. package/dist/upgrader-setup.d.ts.map +1 -0
  175. package/dist/upgrader-setup.js +308 -0
  176. package/dist/upgrader-setup.js.map +1 -0
  177. package/dist/wizard.d.ts +6 -0
  178. package/dist/wizard.d.ts.map +1 -1
  179. package/dist/wizard.js +32 -4
  180. package/dist/wizard.js.map +1 -1
  181. package/dist/wizards/gcp-firebase.d.ts +3 -0
  182. package/dist/wizards/gcp-firebase.d.ts.map +1 -1
  183. package/dist/wizards/gcp-firebase.js +1 -0
  184. package/dist/wizards/gcp-firebase.js.map +1 -1
  185. package/dist/wizards/gcp-install-job.d.ts +74 -0
  186. package/dist/wizards/gcp-install-job.d.ts.map +1 -0
  187. package/dist/wizards/gcp-install-job.js +398 -0
  188. package/dist/wizards/gcp-install-job.js.map +1 -0
  189. package/dist/wizards/gcp-prompts.d.ts +3 -0
  190. package/dist/wizards/gcp-prompts.d.ts.map +1 -0
  191. package/dist/wizards/gcp-prompts.js +25 -0
  192. package/dist/wizards/gcp-prompts.js.map +1 -0
  193. package/dist/wizards/gcp-pulumi.d.ts +9 -2
  194. package/dist/wizards/gcp-pulumi.d.ts.map +1 -1
  195. package/dist/wizards/gcp-pulumi.js +8 -6
  196. package/dist/wizards/gcp-pulumi.js.map +1 -1
  197. package/dist/wizards/gcp.d.ts +40 -0
  198. package/dist/wizards/gcp.d.ts.map +1 -1
  199. package/dist/wizards/gcp.js +291 -83
  200. package/dist/wizards/gcp.js.map +1 -1
  201. package/package.json +6 -4
  202. package/src/adapter.ts +97 -0
  203. package/src/adc-impersonation.ts +176 -0
  204. package/src/admin-domain.test.ts +254 -0
  205. package/src/admin-domain.ts +245 -0
  206. package/src/admin-memory.test.ts +151 -0
  207. package/src/agent/apply.test.ts +185 -0
  208. package/src/agent/apply.ts +124 -0
  209. package/src/agent/cli-provenance.test.ts +159 -0
  210. package/src/agent/cli-provenance.ts +191 -0
  211. package/src/agent/command.test.ts +77 -0
  212. package/src/agent/command.ts +564 -0
  213. package/src/agent/config.ts +86 -0
  214. package/src/agent/lock-config.test.ts +97 -0
  215. package/src/agent/lock.ts +110 -0
  216. package/src/agent/queue.test.ts +10 -0
  217. package/src/agent/queue.ts +91 -0
  218. package/src/agent/run.test.ts +382 -0
  219. package/src/agent/run.ts +295 -0
  220. package/src/agent/systemd.ts +209 -0
  221. package/src/agent/units.test.ts +223 -0
  222. package/src/agent/units.ts +163 -0
  223. package/src/azure-front-door.test.ts +119 -0
  224. package/src/azure-front-door.ts +56 -0
  225. package/src/bootstrap-token.ts +20 -0
  226. package/src/caddy.ts +112 -0
  227. package/src/cdn-copy.ts +91 -0
  228. package/src/cert-wait.test.ts +194 -0
  229. package/src/cert-wait.ts +163 -0
  230. package/src/checkpoint-secrets.test.ts +380 -0
  231. package/src/checkpoint-secrets.ts +283 -0
  232. package/src/cli-routing.test.ts +45 -0
  233. package/src/cli-routing.ts +43 -0
  234. package/src/cli.ts +1491 -0
  235. package/src/cloudsql-iam-user.ts +147 -0
  236. package/src/compose.ts +334 -0
  237. package/src/default-firewall.test.ts +411 -0
  238. package/src/default-firewall.ts +342 -0
  239. package/src/deployed-release.test.ts +231 -0
  240. package/src/deployed-release.ts +220 -0
  241. package/src/dns/cloudflare.ts +182 -0
  242. package/src/dns/groups.test.ts +76 -0
  243. package/src/dns/groups.ts +86 -0
  244. package/src/dns/index.ts +56 -0
  245. package/src/dns/manual.ts +130 -0
  246. package/src/dns/types.ts +27 -0
  247. package/src/gateway-credentials.test.ts +180 -0
  248. package/src/gateway-credentials.ts +144 -0
  249. package/src/gcloud-components.test.ts +76 -0
  250. package/src/gcloud-components.ts +85 -0
  251. package/src/gcloud-retry.ts +41 -0
  252. package/src/gcloud-runner.test.ts +61 -0
  253. package/src/gcloud.test.ts +99 -0
  254. package/src/gcloud.ts +322 -0
  255. package/src/gcp-names.ts +135 -0
  256. package/src/index.test.ts +332 -0
  257. package/src/index.ts +114 -0
  258. package/src/infra-contract.ts +32 -0
  259. package/src/install-job-runner.ts +166 -0
  260. package/src/install-job.test.ts +566 -0
  261. package/src/install-job.ts +542 -0
  262. package/src/install-state.test.ts +239 -0
  263. package/src/install-state.ts +583 -0
  264. package/src/internal-token.ts +35 -0
  265. package/src/lifecycle.ts +900 -0
  266. package/src/mcp-iap-stacks.test.ts +58 -0
  267. package/src/mcp-iap.test.ts +149 -0
  268. package/src/mcp-iap.ts +131 -0
  269. package/src/media-volume.test.ts +296 -0
  270. package/src/migration-runner.ts +365 -0
  271. package/src/operator-access-stacks.test.ts +55 -0
  272. package/src/operator-access.test.ts +332 -0
  273. package/src/operator-access.ts +554 -0
  274. package/src/ops-backend.test.ts +320 -0
  275. package/src/ops-backend.ts +414 -0
  276. package/src/provisioner-identity.test.ts +482 -0
  277. package/src/provisioner-identity.ts +407 -0
  278. package/src/redirects-emit.ts +175 -0
  279. package/src/region-data.ts +428 -0
  280. package/src/regions.test.ts +277 -0
  281. package/src/regions.ts +370 -0
  282. package/src/release-contract.ts +57 -0
  283. package/src/release-manifest.test.ts +293 -0
  284. package/src/release-manifest.ts +430 -0
  285. package/src/run-mode.ts +40 -0
  286. package/src/runtime-secrets.test.ts +627 -0
  287. package/src/runtime-secrets.ts +754 -0
  288. package/src/self-command.test.ts +14 -0
  289. package/src/self-command.ts +21 -0
  290. package/src/self-hosted-compose.test.ts +177 -0
  291. package/src/self-hosted-release.ts +240 -0
  292. package/src/self-hosted-roles.integration.test.ts +161 -0
  293. package/src/self-hosted-roles.test.ts +124 -0
  294. package/src/self-hosted-roles.ts +203 -0
  295. package/src/self-hosted-upgrade.test.ts +394 -0
  296. package/src/self-hosted-upgrade.ts +548 -0
  297. package/src/stack-contract-parity.test.ts +199 -0
  298. package/src/stack-contract.test.ts +391 -0
  299. package/src/stack-contract.ts +888 -0
  300. package/src/stack-converge-secrets.test.ts +421 -0
  301. package/src/stack-converge.test.ts +833 -0
  302. package/src/stack-converge.ts +909 -0
  303. package/src/staging-admin.test.ts +246 -0
  304. package/src/staging-admin.ts +324 -0
  305. package/src/state-attach.test.ts +211 -0
  306. package/src/state-attach.ts +132 -0
  307. package/src/state-backend.test.ts +145 -0
  308. package/src/state-backend.ts +262 -0
  309. package/src/state-command.ts +293 -0
  310. package/src/state-migrate.test.ts +479 -0
  311. package/src/state-migrate.ts +434 -0
  312. package/src/state-pulumi.ts +289 -0
  313. package/src/state-store.test.ts +212 -0
  314. package/src/state-store.ts +243 -0
  315. package/src/upgrade/app-roll.ts +486 -0
  316. package/src/upgrade/backup.ts +109 -0
  317. package/src/upgrade/converge.ts +437 -0
  318. package/src/upgrade/gcp-lookup.ts +167 -0
  319. package/src/upgrade/health.test.ts +279 -0
  320. package/src/upgrade/health.ts +305 -0
  321. package/src/upgrade/release-images.test.ts +119 -0
  322. package/src/upgrade/release-images.ts +391 -0
  323. package/src/upgrade/reporter.ts +46 -0
  324. package/src/upgrade/upgrade.test.ts +626 -0
  325. package/src/upgrade/upgrade.ts +249 -0
  326. package/src/upgrader-assess.test.ts +265 -0
  327. package/src/upgrader-assess.ts +233 -0
  328. package/src/upgrader-gcp.ts +123 -0
  329. package/src/upgrader-job.test.ts +408 -0
  330. package/src/upgrader-job.ts +499 -0
  331. package/src/upgrader-reporter.test.ts +46 -0
  332. package/src/upgrader-reporter.ts +105 -0
  333. package/src/upgrader-setup.test.ts +169 -0
  334. package/src/upgrader-setup.ts +355 -0
  335. package/src/wizard.ts +425 -0
  336. package/src/wizards/gcp-cost.ts +188 -0
  337. package/src/wizards/gcp-firebase.ts +72 -0
  338. package/src/wizards/gcp-install-job.ts +506 -0
  339. package/src/wizards/gcp-prompts.ts +26 -0
  340. package/src/wizards/gcp-pulumi.test.ts +246 -0
  341. package/src/wizards/gcp-pulumi.ts +516 -0
  342. package/src/wizards/gcp.ts +1935 -0
@@ -0,0 +1,199 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ /**
4
+ * Parity between the Pulumi stacks and `cms-provision upgrade`: everything a
5
+ * stack declares for the admin/gateway env, IAM and the CDN must either come
6
+ * from the shared contract (stack-contract.ts) that upgrade also applies, or
7
+ * be explicitly exempted with a reason. A stack change that adds an env var,
8
+ * an IAM binding or a CDN setting without deciding how existing installs get
9
+ * it fails here.
10
+ *
11
+ * Pure-string checks on the stack programs, like the other stack tests.
12
+ */
13
+
14
+ import { describe, expect, it } from "bun:test";
15
+ import { readFileSync } from "node:fs";
16
+ import { resolve } from "node:path";
17
+ import {
18
+ type GcpProvider,
19
+ type IamInvariant,
20
+ runtimeSecretBindings,
21
+ STACK_IAM_NOT_ENSURED,
22
+ stackIamInvariants,
23
+ } from "./stack-contract.js";
24
+ import { imageDigestConfig } from "./wizards/gcp-pulumi.js";
25
+
26
+ const stack = (provider: GcpProvider) =>
27
+ readFileSync(resolve(import.meta.dir, `../stacks/${provider}/index.ts`), "utf8");
28
+
29
+ interface DeclaredBinding {
30
+ /** Logical name after `${namePrefix}-`, placeholders written as `*`. */
31
+ readonly name: string;
32
+ readonly type: string;
33
+ readonly body: string;
34
+ }
35
+
36
+ /** Every IAM member/binding/policy resource a stack program declares. */
37
+ function declaredBindings(src: string): DeclaredBinding[] {
38
+ const re =
39
+ /new gcp\.([\w.]+(?:IAMMember|IamMember|IAMBinding|IamBinding|IAMPolicy|IamPolicy))\(\s*`\$\{namePrefix\}-([^`]+)`,([\s\S]*?)\n\s*\);/g;
40
+ const out: DeclaredBinding[] = [];
41
+ for (const m of src.matchAll(re)) {
42
+ const [, type = "", rawName = "", body = ""] = m;
43
+ out.push({ type, name: rawName.replace(/\$\{[^}]*\}/g, "*"), body });
44
+ }
45
+ return out;
46
+ }
47
+
48
+ /**
49
+ * The stacks declare the secret accessors in one loop over
50
+ * `runtimeSecretBindings()` — the same list upgrade's invariants come from —
51
+ * named `${namePrefix}-${binding.stackResource}`. The parser sees that as `*`.
52
+ */
53
+ const SECRET_LOOP_NAME = "*";
54
+ const secretBindingNames = () => runtimeSecretBindings().map((b) => b.stackResource);
55
+
56
+ const expand = (d: DeclaredBinding) =>
57
+ d.name === SECRET_LOOP_NAME ? secretBindingNames() : [d.name];
58
+
59
+ const MEMBER_EXPR: Record<IamInvariant["member"], string> = {
60
+ "run-sa": "runSa.email",
61
+ // The secret loop resolves the SA per service through `serviceSa`.
62
+ "gateway-sa": "gatewaySa.email",
63
+ "static-publisher-sa": "staticPublisherSa.email",
64
+ "iap-service-agent": "iapServiceIdentity.email",
65
+ allUsers: 'member: "allUsers"',
66
+ };
67
+
68
+ function targetExpr(inv: IamInvariant): { type: RegExp; ref?: string } {
69
+ switch (inv.target.kind) {
70
+ case "project":
71
+ return { type: /^projects\.IAMMember$/ };
72
+ case "secret":
73
+ return { type: /^secretmanager\.SecretIamMember$/ };
74
+ case "bucket":
75
+ return { type: /^storage\.BucketIAMMember$/, ref: `bucket: ${inv.target.bucket}Bucket.name` };
76
+ case "run-service":
77
+ return {
78
+ type: /^cloudrunv2\.ServiceIamMember$/,
79
+ ref: `name: ${inv.target.service}Svc.name`,
80
+ };
81
+ }
82
+ }
83
+
84
+ describe.each(["gcp", "gcp-firebase"] as const)("%s stack ↔ upgrade parity", (provider) => {
85
+ const src = stack(provider);
86
+ const declared = declaredBindings(src);
87
+ const invariants = stackIamInvariants(provider);
88
+ const exempt = STACK_IAM_NOT_ENSURED[provider];
89
+
90
+ it("finds the stack's IAM resources (guards the parser)", () => {
91
+ expect(declared.length).toBeGreaterThan(8);
92
+ });
93
+
94
+ it("declares the secret accessors from runtimeSecretBindings(), per service SA", () => {
95
+ const loop = declared.filter((d) => d.name === SECRET_LOOP_NAME);
96
+ expect(loop).toHaveLength(1);
97
+ expect(loop[0]?.type).toBe("secretmanager.SecretIamMember");
98
+ expect(src).toContain("for (const binding of runtimeSecretBindings())");
99
+ expect(loop[0]?.body).toContain("serviceSa[binding.service].email");
100
+ expect(src).toMatch(
101
+ /const serviceSa: Record<CloudRunSlug, [^>]+> = \{\s*admin: runSa,\s*gateway: gatewaySa,/,
102
+ );
103
+ expect(src).toContain("serviceAccount: serviceSa[args.serviceName].email");
104
+ });
105
+
106
+ it("every IAM binding the stack declares is ensured by upgrade or exempted with a reason", () => {
107
+ const unhandled: string[] = [];
108
+ for (const d of declared) {
109
+ for (const name of expand(d)) {
110
+ if (exempt[name]) continue;
111
+ const inv = invariants.find((i) => i.stackResource === name);
112
+ if (!inv) {
113
+ unhandled.push(`${name} (${d.type})`);
114
+ continue;
115
+ }
116
+ const role = d.body.match(/role: "([^"]+)"/)?.[1];
117
+ expect(`${name}: ${role}`).toBe(`${name}: ${inv.role}`);
118
+ if (d.name !== SECRET_LOOP_NAME) expect(d.body).toContain(MEMBER_EXPR[inv.member]);
119
+ const target = targetExpr(inv);
120
+ expect(d.type).toMatch(target.type);
121
+ if (target.ref) expect(d.body).toContain(target.ref);
122
+ }
123
+ }
124
+ expect(unhandled).toEqual([]);
125
+ });
126
+
127
+ it("every invariant and exemption names a binding the stack still declares", () => {
128
+ const names = new Set(declared.flatMap(expand));
129
+ for (const inv of invariants) expect(names).toContain(inv.stackResource);
130
+ for (const key of Object.keys(exempt)) expect(names).toContain(key);
131
+ });
132
+
133
+ it("builds the admin + gateway env from the shared contracts", () => {
134
+ expect(src).toMatch(/serviceName: "admin",[\s\S]*?contractEnv: adminEnvContract\(/);
135
+ expect(src).toMatch(/serviceName: "gateway",[\s\S]*?contractEnv: gatewayEnvContract\(/);
136
+ expect(src).toContain("envs: [...args.contractEnv],");
137
+ });
138
+
139
+ it("mounts the media bucket on the admin, and only there, from the contract upgrade applies", () => {
140
+ // The stack's admin gets adminMediaVolume() — the same volume
141
+ // stack-converge.ts planMediaVolume adds to installs that lack it.
142
+ expect(src).toMatch(
143
+ /serviceName: "admin",[\s\S]*?mediaVolume: adminMediaVolumeTemplate\(adminMediaVolume\(project, env\)\),\n\}\);/,
144
+ );
145
+ expect(src.match(/mediaVolume: adminMediaVolumeTemplate\(/g)).toHaveLength(1);
146
+ expect(src).not.toMatch(/serviceName: "gateway",[^}]*mediaVolume:/);
147
+ // No volume, mount or execution environment besides the contract's.
148
+ expect(src.match(/executionEnvironment:/g)).toHaveLength(1);
149
+ expect(src).toContain("executionEnvironment: args.mediaVolume.executionEnvironment,");
150
+ expect(src.match(/volumes:/g)).toHaveLength(1);
151
+ expect(src).toContain("volumes: [...args.mediaVolume.volumes],");
152
+ expect(src.match(/volumeMounts:/g)).toHaveLength(1);
153
+ expect(src).toContain("volumeMounts: [...args.mediaVolume.volumeMounts]");
154
+ });
155
+
156
+ it("sets no env var outside the contract (C1: no secret value in a plain var)", () => {
157
+ const envNames = [...src.matchAll(/\{\s*name: "([A-Z][A-Z0-9_]+)"/g)].map((m) => m[1]);
158
+ expect(envNames).toEqual([]);
159
+ expect(src).not.toMatch(/valueSource:/);
160
+ });
161
+
162
+ it("builds no database URL with a password in it", () => {
163
+ expect(src).not.toMatch(/:\$\{postgresPassword\}@|\$\{pw\}@/);
164
+ expect(src).toContain("sqlInstance.privateIpAddress.apply(databaseUrls)");
165
+ });
166
+
167
+ it("references the CLI-generated secrets by id instead of creating them", () => {
168
+ for (const secret of ["internal-secret", "tool-approval-secret"]) {
169
+ expect(src).toContain(`"${secret}": gcpSecretId(env, "${secret}")`);
170
+ expect(src).not.toContain(`makeSecret("${secret}"`);
171
+ }
172
+ });
173
+
174
+ it("names buckets and the run SA with the helpers upgrade uses", () => {
175
+ expect(src).not.toMatch(/name: `\$\{project\}-\$\{namePrefix\}-/);
176
+ expect(src).toContain("accountId: runServiceAccountId(env)");
177
+ expect(src).toContain('name: gcpBucketName(project, env, "media")');
178
+ });
179
+
180
+ it("pins images by the config keys upgrade writes back", () => {
181
+ const keys = Object.keys(
182
+ imageDigestConfig(provider, { admin: "sha256:a", gateway: "sha256:b" }),
183
+ );
184
+ expect(keys).toEqual([
185
+ `caelo-${provider}:image-digest-admin`,
186
+ `caelo-${provider}:image-digest-gateway`,
187
+ ]);
188
+ // The stack reads `<ns>:image-digest-<service>` through pulumi.Config.
189
+ expect(src).toMatch(/cfg\.get\(`image-digest-\$\{service\}`\)/);
190
+ });
191
+ });
192
+
193
+ describe("gcp stack CDN policy", () => {
194
+ it("comes from the shared constant upgrade ensures", () => {
195
+ const src = stack("gcp");
196
+ expect(src).toContain("cdnPolicy: { ...STATIC_CDN_POLICY }");
197
+ expect(src).not.toMatch(/(clientTtl|maxTtl|defaultTtl):\s*\d/);
198
+ });
199
+ });
@@ -0,0 +1,391 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ import { describe, expect, it } from "bun:test";
4
+ import { databasePasswordVar } from "@caelo-cms/shared";
5
+ import { generateDockerCompose } from "./compose.js";
6
+ import {
7
+ adminEnvContract,
8
+ CLI_GENERATED_SECRETS,
9
+ type CloudRunEnvVar,
10
+ DATABASE_ROLE_SECRET,
11
+ databaseUrls,
12
+ defaultStagingHost,
13
+ gatewayEnvContract,
14
+ iamMember,
15
+ RETIRED_IAM_BINDINGS,
16
+ RETIRED_SERVICE_ENV,
17
+ runtimeSecretBindings,
18
+ SERVICE_SECRET_ENV,
19
+ STACK_IAM_NOT_ENSURED,
20
+ serviceSecrets,
21
+ stackIamInvariants,
22
+ } from "./stack-contract.js";
23
+
24
+ const base = {
25
+ projectId: "acme",
26
+ env: "production",
27
+ domain: "acme.com",
28
+ stagingHost: "staging.acme.com",
29
+ region: "europe-west1",
30
+ databaseUrls: databaseUrls("10.20.0.3"),
31
+ };
32
+ const asRecord = (vars: CloudRunEnvVar[]) =>
33
+ Object.fromEntries(
34
+ vars.map((v) => [
35
+ v.name,
36
+ "value" in v
37
+ ? v.value
38
+ : `secret:${v.valueSource.secretKeyRef.secret}:${v.valueSource.secretKeyRef.version}`,
39
+ ]),
40
+ );
41
+
42
+ const ADMIN_SECRETS = {
43
+ ADMIN_DATABASE_PASSWORD: "secret:caelo-production-postgres-password:latest",
44
+ PUBLIC_ADMIN_DATABASE_PASSWORD: "secret:caelo-production-postgres-password:latest",
45
+ CAELO_SECRET_KEK: "secret:caelo-production-secret-kek:latest",
46
+ CAELO_INTERNAL_SECRET: "secret:caelo-production-internal-secret:latest",
47
+ CAELO_TOOL_APPROVAL_SECRET: "secret:caelo-production-tool-approval-secret:latest",
48
+ CAELO_STAGING_GATE_SECRET: "secret:caelo-production-staging-gate-secret:latest",
49
+ };
50
+
51
+ describe("databaseUrls", () => {
52
+ it("builds password-less URLs for the four role/database pairs", () => {
53
+ expect(databaseUrls("10.20.0.3")).toEqual({
54
+ admin: "postgresql://admin_role@10.20.0.3:5432/cms_admin?sslmode=require",
55
+ publicAdmin: "postgresql://admin_role@10.20.0.3:5432/cms_public?sslmode=require",
56
+ public: "postgresql://public_role@10.20.0.3:5432/cms_public?sslmode=require",
57
+ gateway: "postgresql://gateway_role@10.20.0.3:5432/cms_admin?sslmode=require",
58
+ });
59
+ });
60
+ });
61
+
62
+ describe("adminEnvContract", () => {
63
+ it("gcp: carries the database URLs, site URL, buckets, generator path, MCP SA and secrets", () => {
64
+ expect(asRecord(adminEnvContract({ ...base, provider: "gcp" }))).toEqual({
65
+ CAELO_PROVIDER: "gcp",
66
+ CAELO_ENV: "production",
67
+ MEDIA_STORAGE_URL: "gs://acme-caelo-production-media",
68
+ ADMIN_DATABASE_URL: "postgresql://admin_role@10.20.0.3:5432/cms_admin?sslmode=require",
69
+ PUBLIC_ADMIN_DATABASE_URL:
70
+ "postgresql://admin_role@10.20.0.3:5432/cms_public?sslmode=require",
71
+ CAELO_SITE_URL: "https://acme.com",
72
+ CAELO_GENERATOR_CLI: "/app/apps/static-generator/src/cli.ts",
73
+ MEDIA_ROOT_DIR: "/app/apps/admin/data/media",
74
+ CAELO_MCP_IAP_SERVICE_ACCOUNT: "caelo-mcp@acme.iam.gserviceaccount.com",
75
+ CAELO_OPERATOR_ACCESS_JOB:
76
+ "projects/acme/locations/europe-west1/jobs/caelo-production-operator-access-sync",
77
+ CAELO_UPGRADER_JOB: "projects/acme/locations/europe-west1/jobs/caelo-production-upgrader",
78
+ CAELO_STATIC_BUCKET: "acme-caelo-production-static",
79
+ CAELO_STAGING_BUCKET: "acme-caelo-production-staging",
80
+ ...ADMIN_SECRETS,
81
+ });
82
+ });
83
+
84
+ it("gcp-firebase: adds the Pulumi-generated Firebase inputs", () => {
85
+ expect(
86
+ asRecord(
87
+ adminEnvContract({
88
+ ...base,
89
+ provider: "gcp-firebase",
90
+ firebaseSiteId: "caelo-production-site-abc123",
91
+ gatewayService: "caelo-production-gateway-1a2b3c4",
92
+ }),
93
+ ),
94
+ ).toEqual({
95
+ CAELO_PROVIDER: "gcp-firebase",
96
+ CAELO_ENV: "production",
97
+ MEDIA_STORAGE_URL: "gs://acme-caelo-production-media",
98
+ ADMIN_DATABASE_URL: "postgresql://admin_role@10.20.0.3:5432/cms_admin?sslmode=require",
99
+ PUBLIC_ADMIN_DATABASE_URL:
100
+ "postgresql://admin_role@10.20.0.3:5432/cms_public?sslmode=require",
101
+ CAELO_SITE_URL: "https://acme.com",
102
+ CAELO_GENERATOR_CLI: "/app/apps/static-generator/src/cli.ts",
103
+ MEDIA_ROOT_DIR: "/app/apps/admin/data/media",
104
+ CAELO_MCP_IAP_SERVICE_ACCOUNT: "caelo-mcp@acme.iam.gserviceaccount.com",
105
+ CAELO_OPERATOR_ACCESS_JOB:
106
+ "projects/acme/locations/europe-west1/jobs/caelo-production-operator-access-sync",
107
+ CAELO_UPGRADER_JOB: "projects/acme/locations/europe-west1/jobs/caelo-production-upgrader",
108
+ CAELO_FIREBASE_SITE: "caelo-production-site-abc123",
109
+ CAELO_GATEWAY_SERVICE: "caelo-production-gateway-1a2b3c4",
110
+ CAELO_GATEWAY_REGION: "europe-west1",
111
+ CAELO_STAGING_HOST: "staging.acme.com",
112
+ ...ADMIN_SECRETS,
113
+ });
114
+ });
115
+ });
116
+
117
+ describe.each([
118
+ ["admin", adminEnvContract({ ...base, provider: "gcp" })],
119
+ ["gateway", gatewayEnvContract({ ...base, provider: "gcp" })],
120
+ ] as const)("%s env contract", (service, vars) => {
121
+ it("carries no secret value in a plain var (C1: run.services.get exposes plain vars)", () => {
122
+ for (const v of vars) {
123
+ if (!("value" in v)) continue;
124
+ expect(v.name).not.toMatch(/PASSWORD|SECRET|KEK|API_KEY|TOKEN/);
125
+ // A URL var never carries a password inline.
126
+ if (v.value.includes("://")) expect(new URL(v.value).password).toBe("");
127
+ }
128
+ });
129
+
130
+ it("references exactly the secrets SERVICE_SECRET_ENV gives the service", () => {
131
+ const refs = Object.fromEntries(
132
+ vars.flatMap((v) =>
133
+ "valueSource" in v ? [[v.name, v.valueSource.secretKeyRef.secret] as const] : [],
134
+ ),
135
+ );
136
+ expect(refs).toEqual(
137
+ Object.fromEntries(
138
+ Object.entries(SERVICE_SECRET_ENV[service]).map(([name, secret]) => [
139
+ name,
140
+ `caelo-production-${secret}`,
141
+ ]),
142
+ ),
143
+ );
144
+ });
145
+
146
+ it("pairs every password-less database URL with a password var the apps compose it from", () => {
147
+ const names = vars.map((v) => v.name);
148
+ for (const url of names.filter((n) => n.endsWith("DATABASE_URL"))) {
149
+ expect(names).toContain(databasePasswordVar(url));
150
+ }
151
+ });
152
+ });
153
+
154
+ describe("gatewayEnvContract", () => {
155
+ const gateway = gatewayEnvContract({ ...base, provider: "gcp" });
156
+
157
+ it("carries its two own logins and none of the admin's database or app secrets", () => {
158
+ expect(asRecord(gateway)).toEqual({
159
+ CAELO_PROVIDER: "gcp",
160
+ CAELO_ENV: "production",
161
+ MEDIA_STORAGE_URL: "gs://acme-caelo-production-media",
162
+ PUBLIC_DATABASE_URL: "postgresql://public_role@10.20.0.3:5432/cms_public?sslmode=require",
163
+ GATEWAY_DATABASE_URL: "postgresql://gateway_role@10.20.0.3:5432/cms_admin?sslmode=require",
164
+ PUBLIC_DATABASE_PASSWORD: "secret:caelo-production-public-role-password:latest",
165
+ GATEWAY_DATABASE_PASSWORD: "secret:caelo-production-gateway-role-password:latest",
166
+ CAELO_STAGING_GATE_SECRET: "secret:caelo-production-staging-gate-secret:latest",
167
+ });
168
+ });
169
+
170
+ it("#650 — reads the staging host from the database, not its env", () => {
171
+ const fb = asRecord(gatewayEnvContract({ ...base, provider: "gcp-firebase" }));
172
+ expect(fb.CAELO_STAGING_HOST).toBeUndefined();
173
+ expect(fb.CAELO_STAGING_GATE_SECRET).toBe("secret:caelo-production-staging-gate-secret:latest");
174
+ });
175
+
176
+ it("#650 — the admin's staging host seed is the install setting, any host", () => {
177
+ const fb = asRecord(
178
+ adminEnvContract({
179
+ ...base,
180
+ stagingHost: "preview.other.de",
181
+ provider: "gcp-firebase",
182
+ firebaseSiteId: "s",
183
+ gatewayService: "g",
184
+ }),
185
+ );
186
+ expect(fb.CAELO_STAGING_HOST).toBe("preview.other.de");
187
+ expect(defaultStagingHost("Acme.com.")).toBe("staging.acme.com");
188
+ });
189
+
190
+ it("the gateway's run SA reads no admin-only secret (admin password, KEK, internal, tool approval)", () => {
191
+ expect(serviceSecrets("gateway")).toEqual([
192
+ "gateway-role-password",
193
+ "public-role-password",
194
+ "staging-gate-secret",
195
+ ]);
196
+ for (const v of gateway) {
197
+ expect(v.name).not.toMatch(/KEK|INTERNAL|TOOL_APPROVAL|PUBLIC_ADMIN/);
198
+ }
199
+ });
200
+
201
+ it("retires the KEK the older stacks gave it", () => {
202
+ expect(RETIRED_SERVICE_ENV.gateway).toContain("CAELO_SECRET_KEK");
203
+ expect(gateway.map((v) => v.name)).not.toContain("CAELO_SECRET_KEK");
204
+ });
205
+
206
+ // Issue #613 closed the CLAUDE.md §2 gap #579 documented here ("still
207
+ // carries the admin_role credential the gateway boots with"): the gateway
208
+ // gets no admin_role URL, password or secret, and upgrade removes the
209
+ // ones older installs carry.
210
+ it("holds no admin_role credential, and upgrade retires the old ones (CLAUDE.md §2, #613)", () => {
211
+ const names = gateway.map((v) => v.name);
212
+ expect(names).not.toContain("ADMIN_DATABASE_URL");
213
+ expect(names).not.toContain("ADMIN_DATABASE_PASSWORD");
214
+ for (const v of gateway) {
215
+ if ("value" in v && v.value.includes("://")) {
216
+ expect(new URL(v.value).username).not.toBe("admin_role");
217
+ }
218
+ }
219
+ expect(serviceSecrets("gateway")).not.toContain(DATABASE_ROLE_SECRET.admin_role);
220
+ expect(RETIRED_SERVICE_ENV.gateway).toEqual(
221
+ expect.arrayContaining(["ADMIN_DATABASE_URL", "ADMIN_DATABASE_PASSWORD"]),
222
+ );
223
+ });
224
+ });
225
+
226
+ describe("database role secrets (#613)", () => {
227
+ it("gives every role its own password secret", () => {
228
+ const secrets = Object.values(DATABASE_ROLE_SECRET);
229
+ expect(new Set(secrets).size).toBe(secrets.length);
230
+ expect(DATABASE_ROLE_SECRET).toEqual({
231
+ admin_role: "postgres-password",
232
+ public_role: "public-role-password",
233
+ gateway_role: "gateway-role-password",
234
+ });
235
+ });
236
+
237
+ it("the CLI generates the gateway's role passwords (never seeded from admin_role's)", () => {
238
+ expect(CLI_GENERATED_SECRETS).toEqual(
239
+ expect.arrayContaining(["public-role-password", "gateway-role-password"]),
240
+ );
241
+ });
242
+
243
+ it("retires the gateway SA's read access to admin_role's password", () => {
244
+ expect(RETIRED_IAM_BINDINGS).toContainEqual(
245
+ expect.objectContaining({
246
+ role: "roles/secretmanager.secretAccessor",
247
+ member: "gateway-sa",
248
+ target: { kind: "secret", name: "postgres-password" },
249
+ }),
250
+ );
251
+ for (const provider of ["gcp", "gcp-firebase"] as const) {
252
+ // Never both ensured and retired.
253
+ for (const r of RETIRED_IAM_BINDINGS) {
254
+ expect(
255
+ stackIamInvariants(provider).some(
256
+ (i) =>
257
+ i.role === r.role &&
258
+ i.member === r.member &&
259
+ JSON.stringify(i.target) === JSON.stringify(r.target),
260
+ ),
261
+ ).toBe(false);
262
+ }
263
+ }
264
+ });
265
+ });
266
+
267
+ describe("runtime secrets", () => {
268
+ it("binds each service's SA to exactly the secrets its env reads", () => {
269
+ expect(runtimeSecretBindings()).toEqual([
270
+ { service: "admin", secret: "postgres-password", stackResource: "postgres-password-binding" },
271
+ { service: "admin", secret: "secret-kek", stackResource: "secret-kek-binding" },
272
+ { service: "admin", secret: "internal-secret", stackResource: "internal-secret-binding" },
273
+ {
274
+ service: "admin",
275
+ secret: "tool-approval-secret",
276
+ stackResource: "tool-approval-secret-binding",
277
+ },
278
+ {
279
+ service: "admin",
280
+ secret: "staging-gate-secret",
281
+ stackResource: "staging-gate-secret-binding",
282
+ },
283
+ {
284
+ service: "gateway",
285
+ secret: "gateway-role-password",
286
+ stackResource: "gateway-gateway-role-password-binding",
287
+ },
288
+ {
289
+ service: "gateway",
290
+ secret: "public-role-password",
291
+ stackResource: "gateway-public-role-password-binding",
292
+ },
293
+ {
294
+ service: "gateway",
295
+ secret: "staging-gate-secret",
296
+ stackResource: "gateway-staging-gate-secret-binding",
297
+ },
298
+ ]);
299
+ });
300
+
301
+ it("every CLI-generated secret is read by some service", () => {
302
+ for (const s of CLI_GENERATED_SECRETS) {
303
+ expect([...serviceSecrets("admin"), ...serviceSecrets("gateway")]).toContain(s);
304
+ }
305
+ });
306
+ });
307
+
308
+ describe("self-hosted compose", () => {
309
+ it("sets CAELO_SITE_URL from the same helper", () => {
310
+ const yaml = generateDockerCompose({
311
+ domain: "acme.com",
312
+ postgresPassword: "p",
313
+ rolePasswords: { admin: "a", public: "b", gateway: "g" },
314
+ minioRootUser: "u",
315
+ minioRootPassword: "m",
316
+ caeloSecretKek: "k",
317
+ diskSize: "10G",
318
+ images: {
319
+ mode: "release" as const,
320
+ admin: `ghcr.io/caelo-cms/admin@sha256:${"a".repeat(64)}`,
321
+ gateway: `ghcr.io/caelo-cms/gateway@sha256:${"b".repeat(64)}`,
322
+ },
323
+ });
324
+ expect(yaml).toContain('CAELO_SITE_URL: "https://acme.com"');
325
+ });
326
+ });
327
+
328
+ describe("stackIamInvariants", () => {
329
+ for (const provider of ["gcp", "gcp-firebase"] as const) {
330
+ it(`${provider}: one invariant per stack resource, none also exempted`, () => {
331
+ const names = stackIamInvariants(provider).map((i) => i.stackResource);
332
+ expect(new Set(names).size).toBe(names.length);
333
+ for (const n of names) expect(STACK_IAM_NOT_ENSURED[provider][n]).toBeUndefined();
334
+ });
335
+ }
336
+
337
+ it("gcp-firebase ensures the A5 bindings: run.viewer on the gateway + telemetry roles", () => {
338
+ const inv = stackIamInvariants("gcp-firebase");
339
+ expect(inv).toContainEqual(
340
+ expect.objectContaining({
341
+ role: "roles/run.viewer",
342
+ member: "run-sa",
343
+ target: { kind: "run-service", service: "gateway" },
344
+ onFailure: "abort",
345
+ }),
346
+ );
347
+ for (const role of ["roles/logging.logWriter", "roles/monitoring.metricWriter"]) {
348
+ expect(inv).toContainEqual(
349
+ expect.objectContaining({ role, member: "run-sa", target: { kind: "project" } }),
350
+ );
351
+ }
352
+ });
353
+
354
+ it("grants the gateway SA only its two role passwords, the staging gate key + telemetry roles", () => {
355
+ for (const provider of ["gcp", "gcp-firebase"] as const) {
356
+ const gateway = stackIamInvariants(provider).filter((i) => i.member === "gateway-sa");
357
+ expect(gateway.map((i) => `${i.role} ${JSON.stringify(i.target)}`).sort()).toEqual([
358
+ 'roles/logging.logWriter {"kind":"project"}',
359
+ 'roles/monitoring.metricWriter {"kind":"project"}',
360
+ 'roles/secretmanager.secretAccessor {"kind":"secret","name":"gateway-role-password"}',
361
+ 'roles/secretmanager.secretAccessor {"kind":"secret","name":"public-role-password"}',
362
+ 'roles/secretmanager.secretAccessor {"kind":"secret","name":"staging-gate-secret"}',
363
+ ]);
364
+ }
365
+ });
366
+
367
+ it("gcp ensures the telemetry roles too", () => {
368
+ const roles = stackIamInvariants("gcp").map((i) => i.role);
369
+ expect(roles).toContain("roles/logging.logWriter");
370
+ expect(roles).toContain("roles/monitoring.metricWriter");
371
+ });
372
+ });
373
+
374
+ describe("iamMember", () => {
375
+ const install = { projectId: "acme", projectNumber: "123", env: "production" };
376
+ it("resolves every principal to the name the stack gives it", () => {
377
+ expect(iamMember("run-sa", install)).toBe(
378
+ "serviceAccount:caelo-production-run-sa@acme.iam.gserviceaccount.com",
379
+ );
380
+ expect(iamMember("gateway-sa", install)).toBe(
381
+ "serviceAccount:caelo-production-gateway-sa@acme.iam.gserviceaccount.com",
382
+ );
383
+ expect(iamMember("static-publisher-sa", install)).toBe(
384
+ "serviceAccount:caelo-prod-publisher@acme.iam.gserviceaccount.com",
385
+ );
386
+ expect(iamMember("iap-service-agent", install)).toBe(
387
+ "serviceAccount:service-123@gcp-sa-iap.iam.gserviceaccount.com",
388
+ );
389
+ expect(iamMember("allUsers", install)).toBe("allUsers");
390
+ });
391
+ });