| 1 | import * as GCP from "alchemy/GCP"; |
| 2 | import * as Kubernetes from "alchemy/Kubernetes"; |
| 3 | import * as Effect from "effect/Effect"; |
| 4 | import { |
| 5 | EntriesDatabase, |
| 6 | entryPath, |
| 7 | GuestbookCluster, |
| 8 | GuestbookNamespace, |
| 9 | } from "./infra.ts"; |
| 10 | |
| 11 | export 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 | */ |
| 37 | export 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 | ); |