1import * as GCP from "@/GCP";
2import * as Kubernetes from "@/Kubernetes";
3import * as Effect from "effect/Effect";
4import { HttpServerRequest } from "effect/http/HttpServerRequest";
5import * as HttpServerResponse from "effect/http/HttpServerResponse";
6import {
7 SmokeBucket,
8 SmokeCluster,
9 SmokeNamespace,
10 smokeResources,
11} from "./smoke-resources.ts";
12
13/**
14 * Effect-native `Kubernetes.Deployment` behind a GKE LoadBalancer. The
15 * `GCP.Storage.ReadWriteBucket` binding grants `roles/storage.objectUser`
16 * on the bucket to the Deployment's KSA Workload Identity principal; the
17 * pod authenticates through the GKE metadata server.
18 *
19 * - `GET /health` — readiness.
20 * - `PUT /objects/<key>` — write the request body, read it back.
21 * - `GET /objects/<key>` — read an object (404 when missing).
22 */
23export class GkeSmokeApi extends Kubernetes.Deployment<GkeSmokeApi>()(
24 "GkeSmokeApi",
25) {}
26
27export default GkeSmokeApi.make(
28 Effect.gen(function* () {
29 const cluster = yield* SmokeCluster;
30 const ns = yield* SmokeNamespace;
31 return {
32 cluster,
33 main: import.meta.url,
34 namespace: ns.name,
35 port: 3000,
36 replicas: 1,
37 serviceType: "LoadBalancer" as const,
38 resources: smokeResources,
39 };
40 }),
41 Effect.gen(function* () {
42 const bucket = yield* GCP.Storage.ReadWriteBucket(SmokeBucket);
43
44 return {
45 fetch: Effect.gen(function* () {
46 const request = yield* HttpServerRequest;
47 const url = new URL(request.url, "http://localhost");
48 const key = url.pathname.startsWith("/objects/")
49 ? decodeURIComponent(url.pathname.slice("/objects/".length))
50 : undefined;
51
52 if (key !== undefined && request.method === "PUT") {
53 const body = yield* request.text;
54 yield* bucket.put(key, body, { contentType: "text/plain" });
55 const read = yield* bucket.get(key);
56 return yield* HttpServerResponse.json({
57 key,
58 read: read && new TextDecoder().decode(read.body),
59 });
60 }
61
62 if (key !== undefined && request.method === "GET") {
63 const read = yield* bucket.get(key);
64 return read === undefined
65 ? HttpServerResponse.text("not found", { status: 404 })
66 : HttpServerResponse.text(new TextDecoder().decode(read.body));
67 }
68
69 return yield* HttpServerResponse.json({ ok: true });
70 }).pipe(Effect.orDie),
71 };
72 }).pipe(Effect.provide(GCP.Storage.ReadWriteBucketHttp)),
73);