1import * as GCP from "alchemy/GCP";
2import * as Kubernetes from "alchemy/Kubernetes";
3import * as Effect from "effect/Effect";
4import {
5 EntriesDatabase,
6 entryPath,
7 GuestbookCluster,
8 GuestbookNamespace,
9} from "./infra.ts";
10
11export const seedEntries = [
12 { id: "ada", message: "First computers, now clusters." },
13 {
14 id: "grace",
15 message:
16 "A cluster in port is safe, but that is not what clusters are for.",
17 },
18 { id: "linus", message: "Talk is cheap. Show me the manifest." },
19];
20
21/**
22 * A one-shot `Kubernetes.Job` in the INLINE EFFECT form: props + an init Effect
23 * whose impl returns `{ run }` — a one-shot entry that executes to
24 * completion inside the pod, after which the process exits (the Kubernetes
25 * analog of `GCP.Run.Job`).
26 *
27 * The Effect program is bundled into a generated image
28 * (`main: import.meta.url` names this module as the entrypoint). The
29 * `GCP.Firestore.WriteDatabase` binding injects the database name into the
30 * pod and grants `roles/datastore.user` (project-level, conditioned to the
31 * database) to the Job's Kubernetes ServiceAccount principal.
32 *
33 * Applying the batch/v1 Job runs it: the seed executes once on deploy.
34 * Adding `schedule: "0 3 * * *"` to the props would synthesize a CronJob
35 * instead.
36 */
37export default Kubernetes.Job(
38 "SeedJob",
39 // Props are themselves an Effect so they can reference shared resources.
40 Effect.gen(function* () {
41 const cluster = yield* GuestbookCluster;
42 const ns = yield* GuestbookNamespace;
43 return {
44 cluster,
45 main: import.meta.url,
46 // Deploy after the namespace exists (see src/infra.ts).
47 namespace: ns.name,
48 // A fresh deploy's Firestore grant (a project binding under an IAM
49 // Condition) can take a few minutes to reach Firestore; Kubernetes
50 // retries the pod with exponential backoff (10s, 20s, 40s, …) until
51 // then.
52 backoffLimit: 6,
53 resources: {
54 requests: { cpu: "250m", memory: "512Mi" },
55 limits: { cpu: "250m", memory: "512Mi" },
56 },
57 };
58 }),
59 Effect.gen(function* () {
60 const db = yield* GCP.Firestore.WriteDatabase(EntriesDatabase);
61
62 return {
63 // One-shot entry: seed the database, log, exit 0.
64 run: Effect.gen(function* () {
65 yield* Effect.forEach(
66 seedEntries,
67 (entry) =>
68 db.set(entryPath(entry.id), {
69 author: entry.id,
70 message: entry.message,
71 }),
72 { discard: true },
73 );
74 yield* Effect.log(`seeded ${seedEntries.length} guestbook entries`);
75 }).pipe(Effect.orDie),
76 };
77 }).pipe(Effect.provide(GCP.Firestore.WriteDatabaseHttp)),
78);