| 1 | /** |
| 2 | * `AWS.Lambda.MicrovmImage` under `alchemy dev`: the dev provider builds the |
| 3 | * image with a plain HOST-side `docker build` (BuildKit layer cache against |
| 4 | * the user's real sources) and hands the emulator a pre-built |
| 5 | * `docker://<tag>` artifact, which the alchemy floci fork runs as-is — |
| 6 | * nothing is zipped, uploaded, or built inside the emulator. |
| 7 | * |
| 8 | * Proof structure: |
| 9 | * - deploy an effectful image: attrs carry the dummy account AND a |
| 10 | * `docker://alchemy-dev/microvm-…` artifact uri — the live cloud can |
| 11 | * never mint either, and the uri is proof the host-build path ran; |
| 12 | * - `RunMicrovm` out-of-band reaches RUNNING — the emulator actually |
| 13 | * BOOTED a VM from the host-built reference (an invalid or unbuilt ref |
| 14 | * fails the run); |
| 15 | * - re-deploy with NO change: a true noop (dev diff policy — content is |
| 16 | * the watch loop's job, and nothing changed); |
| 17 | * - rewrite the program's marker in the clone WITHOUT a deploy: the |
| 18 | * sidecar watch loop rebuilds (docker layer cache — only the COPY layer |
| 19 | * re-runs) and re-reconciles; the emulator's `latestActiveImageVersion` |
| 20 | * advances within seconds, and a fresh `RunMicrovm` boots the new |
| 21 | * version; |
| 22 | * - destroy removes the image from the emulator. |
| 23 | * |
| 24 | * Requires Docker (floci runs as a container, MicroVMs run as containers); |
| 25 | * skipped when the daemon is unavailable. |
| 26 | */ |
| 27 | import * as AWS from "@/AWS"; |
| 28 | import * as Endpoint from "@/AWS/Endpoint.ts"; |
| 29 | import * as Region from "@/AWS/Region.ts"; |
| 30 | import * as Test from "@/Test/Alchemy"; |
| 31 | import { Credentials } from "@distilled.cloud/aws/Credentials"; |
| 32 | import type { RegionName } from "@distilled.cloud/aws/Region"; |
| 33 | import * as microvms from "@distilled.cloud/aws/lambda-microvms"; |
| 34 | import { expect } from "alchemy-test"; |
| 35 | import * as Effect from "effect/Effect"; |
| 36 | import * as FileSystem from "effect/FileSystem"; |
| 37 | import * as Layer from "effect/Layer"; |
| 38 | import * as Path from "effect/Path"; |
| 39 | import * as Redacted from "effect/Redacted"; |
| 40 | import * as Schedule from "effect/Schedule"; |
| 41 | import { fileURLToPath } from "node:url"; |
| 42 | import { cloneFixture } from "../../Cloudflare/Utils/Fixture.ts"; |
| 43 | import { dockerAvailable, FLOCI_ENDPOINT } from "../Local/fixtures/raw.ts"; |
| 44 | |
| 45 | const { test } = Test.make({ providers: AWS.providers(), dev: true }); |
| 46 | |
| 47 | const fixtureDir = fileURLToPath( |
| 48 | new URL("./fixtures/microvm-dev", import.meta.url), |
| 49 | ); |
| 50 | |
| 51 | /** Floci-scoped context for the raw distilled calls the test makes itself. */ |
| 52 | const flociContext = Layer.mergeAll( |
| 53 | Endpoint.of(FLOCI_ENDPOINT), |
| 54 | Region.of("us-east-1"), |
| 55 | Layer.succeed( |
| 56 | Credentials, |
| 57 | Effect.succeed({ |
| 58 | accessKeyId: Redacted.make("test"), |
| 59 | secretAccessKey: Redacted.make("test"), |
| 60 | sessionToken: undefined, |
| 61 | region: "us-east-1" as RegionName, |
| 62 | }), |
| 63 | ), |
| 64 | ); |
| 65 | |
| 66 | /** Boot a VM from the image and prove it reaches RUNNING, then clean up. */ |
| 67 | const bootsToRunning = (imageArn: string, imageVersion?: string) => |
| 68 | Effect.gen(function* () { |
| 69 | const vm = yield* microvms.runMicrovm({ |
| 70 | imageIdentifier: imageArn, |
| 71 | ...(imageVersion !== undefined ? { imageVersion } : {}), |
| 72 | }); |
| 73 | const state = yield* microvms |
| 74 | .getMicrovm({ microvmIdentifier: vm.microvmId }) |
| 75 | .pipe( |
| 76 | Effect.map((m) => m.state), |
| 77 | Effect.repeat({ |
| 78 | schedule: Schedule.spaced("1 second"), |
| 79 | until: (s): boolean => s !== "PENDING", |
| 80 | times: 60, |
| 81 | }), |
| 82 | Effect.ensuring( |
| 83 | microvms |
| 84 | .terminateMicrovm({ microvmIdentifier: vm.microvmId }) |
| 85 | .pipe(Effect.ignore), |
| 86 | ), |
| 87 | ); |
| 88 | return { state, imageVersion: vm.imageVersion }; |
| 89 | }).pipe(Effect.provide(flociContext)); |
| 90 | |
| 91 | test.provider.skipIf(!dockerAvailable)( |
| 92 | "dev builds the image host-side (docker://), boots it, and a content edit hot-rebuilds it within seconds", |
| 93 | (stack) => |
| 94 | Effect.gen(function* () { |
| 95 | const fs = yield* FileSystem.FileSystem; |
| 96 | const path = yield* Path.Path; |
| 97 | |
| 98 | yield* stack.destroy(); |
| 99 | |
| 100 | const clone = yield* cloneFixture(fixtureDir, { |
| 101 | prefix: "floci-dev-microvm-", |
| 102 | }); |
| 103 | const mainPath = path.join(clone, "program.ts"); |
| 104 | |
| 105 | const declaration = Effect.gen(function* () { |
| 106 | const buildRole = yield* AWS.IAM.Role("DevMicrovmBuildRole"); |
| 107 | return yield* AWS.Lambda.MicrovmImage("DevMicrovm", { |
| 108 | main: mainPath, |
| 109 | buildRole, |
| 110 | runtime: "bun", |
| 111 | resources: [{ minimumMemoryInMiB: 512 }], |
| 112 | cpuConfigurations: [{ architecture: "ARM_64" }], |
| 113 | }); |
| 114 | }); |
| 115 | |
| 116 | const first = yield* stack.deploy(declaration); |
| 117 | // Emulator identity: the live cloud never mints the dummy account. |
| 118 | expect(first.imageArn).toContain(":000000000000:"); |
| 119 | // The host-build path: a pre-built local docker reference, not an |
| 120 | // uploaded-zip S3 uri. |
| 121 | expect(first.codeArtifact?.uri).toMatch( |
| 122 | /^docker:\/\/alchemy-dev\/microvm-/, |
| 123 | ); |
| 124 | expect(first.codeArtifact?.hash).toBeDefined(); |
| 125 | const v1 = first.latestActiveImageVersion; |
| 126 | expect(v1).toBeDefined(); |
| 127 | |
| 128 | // The emulator can BOOT the host-built reference: RunMicrovm reaches |
| 129 | // RUNNING (a missing or broken image ref fails the run). |
| 130 | const booted = yield* bootsToRunning(first.imageArn); |
| 131 | expect(booted.state).toBe("RUNNING"); |
| 132 | expect(booted.imageVersion).toBe(v1); |
| 133 | |
| 134 | // Same declaration, same source — a true noop: nothing is rebuilt. |
| 135 | const unchanged = yield* stack.deploy(declaration); |
| 136 | expect(unchanged.codeArtifact?.hash).toBe(first.codeArtifact!.hash); |
| 137 | expect(unchanged.latestActiveImageVersion).toBe(v1); |
| 138 | |
| 139 | // Content-only edit, NO deploy: the sidecar watch loop rebuilds |
| 140 | // (cached docker build) and re-reconciles; the emulator's active |
| 141 | // version advances. This is the latency the whole design exists for. |
| 142 | const source = yield* fs.readFileString(mainPath); |
| 143 | const editStartedAt = Date.now(); |
| 144 | yield* fs.writeFileString( |
| 145 | mainPath, |
| 146 | source.replace(`"microvm-marker-v1"`, `"microvm-marker-v2"`), |
| 147 | ); |
| 148 | const rebuilt = yield* microvms |
| 149 | .getMicrovmImage({ imageIdentifier: first.imageArn }) |
| 150 | .pipe( |
| 151 | Effect.provide(flociContext), |
| 152 | Effect.repeat({ |
| 153 | schedule: Schedule.spaced("500 millis"), |
| 154 | until: (image): boolean => |
| 155 | image.latestActiveImageVersion !== undefined && |
| 156 | image.latestActiveImageVersion !== v1, |
| 157 | times: 120, |
| 158 | }), |
| 159 | ); |
| 160 | const rebuildMs = Date.now() - editStartedAt; |
| 161 | // eslint-disable-next-line no-console |
| 162 | console.log( |
| 163 | `microvm content edit -> new active version in ${rebuildMs}ms`, |
| 164 | ); |
| 165 | expect(rebuilt.latestActiveImageVersion).not.toBe(v1); |
| 166 | // The point of the docker:// path: a content edit is a cached docker |
| 167 | // build, not a zip-upload-extract-rebuild. Well under a minute even |
| 168 | // on a cold-ish daemon; typically a few seconds. |
| 169 | expect(rebuildMs).toBeLessThan(30_000); |
| 170 | |
| 171 | // A fresh VM boots the NEW active version. |
| 172 | const rebooted = yield* bootsToRunning(first.imageArn); |
| 173 | expect(rebooted.state).toBe("RUNNING"); |
| 174 | expect(rebooted.imageVersion).toBe(rebuilt.latestActiveImageVersion); |
| 175 | |
| 176 | // Destroy: the image must be gone from the emulator. |
| 177 | yield* stack.destroy(); |
| 178 | const gone = yield* microvms |
| 179 | .getMicrovmImage({ imageIdentifier: first.imageArn }) |
| 180 | .pipe( |
| 181 | Effect.map(() => false), |
| 182 | Effect.catchTag("ResourceNotFoundException", () => |
| 183 | Effect.succeed(true), |
| 184 | ), |
| 185 | Effect.provide(flociContext), |
| 186 | Effect.repeat({ |
| 187 | schedule: Schedule.spaced("1 second"), |
| 188 | until: (isGone): boolean => isGone, |
| 189 | times: 20, |
| 190 | }), |
| 191 | ); |
| 192 | expect(gone).toBe(true); |
| 193 | }), |
| 194 | { |
| 195 | tags: ["provider:aws", "provider:aws:iam", "provider:aws:lambda", "local"], |
| 196 | timeout: 600_000, |
| 197 | }, |
| 198 | ); |