t3-code-android-nightly/.repos/alchemy-effect/packages/alchemy/test/Kubernetes/Deployment.test.ts
Julius Marminge 6f9cea00ae
chore(refs): sync Effect and Alchemy references to 4.0.1 and beta.80 (#16170)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 13:22:30 -07:00

224 lines
8.7 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import * as AWS from "@/AWS";
import { Network } from "@/AWS/EC2/Network.ts";
import { Cluster } from "@/AWS/EKS/Cluster.ts";
import { makeEksTransport } from "@/AWS/EKS/KubernetesAdapter.ts";
import * as Kubernetes from "@/Kubernetes";
import { readObject } from "@/Kubernetes/internal/client.ts";
import * as Core from "@/Test/Core";
import * as Provider from "@/Provider";
import * as Test from "@/Test/Alchemy";
import * as dynamodb from "@distilled.cloud/aws/dynamodb";
import { describe, expect } from "alchemy-test";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Schedule from "effect/Schedule";
import * as HttpClient from "effect/http/HttpClient";
import EksHostApi from "./fixtures/deployment.ts";
const testOptions = {
providers: Layer.mergeAll(AWS.providers(), Kubernetes.providers()),
};
const { test, beforeAll, afterAll } = Test.make(testOptions);
// Ungated probe: `Deployment` is a composite host (in-cluster
// Deployment/Service plus adapter-owned cloud resources — ECR repo,
// pod-identity role, PodIdentityAssociation on EKS). It has no faithful
// single-API enumeration, so `list()` is intentionally empty. This probe
// proves the provider is registered, its record type-checks (a missing /
// mistyped `list` collapses Provider.of inference), and it runs live —
// without paying the ~15-minute EKS control-plane create.
test.provider(
"list returns an empty array (composite host, not enumerable)",
() =>
Effect.gen(function* () {
const provider = yield* Provider.findProvider(Kubernetes.Deployment);
const all = yield* provider.list();
expect(Array.isArray(all)).toBe(true);
expect(all).toEqual([]);
}),
{
tags: [
"provider:aws",
"provider:kubernetes",
"provider:kubernetes:deployment",
"live",
],
},
);
// Full end-to-end (gated). An EKS Auto Mode cluster takes ~10–15 min to
// provision, plus image build/push, pod scheduling, and NLB provisioning —
// well beyond the routine speed-doctrine ceiling. Gate it behind AWS_TEST_SLOW.
//
// It deploys, in two phases (refs read committed stack state, not the in-flight
// plan): (1) a public network + a `compute: "auto"` cluster (the provider owns
// the cluster/node IAM roles); (2) the same infra + the `Deployment` fixture,
// which binds DynamoDB `PutItem`. The bound policy lands on the generated
// pod-identity role and the table name is injected into the pod; the pod
// resolves credentials via the EKS Pod Identity container-credentials chain.
// The test curls the LoadBalancer `/put` route, then asserts the item was
// written by reading it back out-of-band through the DynamoDB API — proving
// the binding, pod identity, image pipeline, and server-side-apply path in one
// shot.
//
// NOTE: this path has not been run green live in this factory wave (cluster
// create alone exceeds the agent budget). It is gated + skip-clean; run on an
// account with Auto Mode Pod Identity by setting AWS_TEST_SLOW=1.
// Cluster declared at a TOP-LEVEL logical id so the fixture can resolve it
// with `Cluster.ref("EksHostCluster")` (no namespace nesting). `compute:
// "auto"` exercises the AutoCluster fold-in: the provider creates and owns the
// cluster + node IAM roles.
const infra = Effect.gen(function* () {
const network = yield* Network("EksHostNetwork", {
cidrBlock: "10.84.0.0/16",
availabilityZones: 2,
});
const cluster = yield* Cluster("EksHostCluster", {
compute: "auto",
resourcesVpcConfig: {
subnetIds: network.publicSubnetIds,
endpointPublicAccess: true,
endpointPrivateAccess: true,
},
});
return { cluster, network };
});
const sharedStack = Core.scratchStack(testOptions, "EksServerHost");
describe.skipIf(!process.env.AWS_TEST_SLOW)(
"Kubernetes Deployment E2E",
{
tags: [
"provider:aws",
"provider:aws:dynamodb",
"provider:aws:ec2",
"provider:aws:eks",
"provider:kubernetes",
"provider:kubernetes:deployment",
"provider:kubernetes:helmchart",
"live",
],
},
() => {
let baseUrl: string;
let helmRelease: Kubernetes.HelmChart["Attributes"];
let helmCluster: Cluster["Attributes"];
beforeAll(
Effect.gen(function* () {
yield* sharedStack.destroy();
// Phase 1: cluster + network only.
yield* sharedStack.deploy(infra);
// Phase 2: same infra + the Deployment fixture (refs the cluster) +
// a HelmChart rendering the local fixture chart onto the cluster.
const { host, cluster, release } = yield* sharedStack.deploy(
Effect.gen(function* () {
const { cluster } = yield* infra;
const release = yield* Kubernetes.HelmChart("E2EHelmChart", {
cluster,
chart: `${import.meta.dirname}/fixtures/chart`,
values: {
message: "helm-e2e",
secondConfigMap: { enabled: true },
},
});
const host = yield* EksHostApi;
return { host, cluster, release };
}),
);
helmRelease = release;
helmCluster = cluster;
// `url` is a full URL (`http://<nlb-hostname>:<port>` — the NLB
// listener is the Service port, not 80).
expect(host.url).toBeTruthy();
baseUrl = host.url!.replace(/\/+$/, "");
// NLB DNS + pod readiness ramp — retry /health.
yield* HttpClient.get(`${baseUrl}/health`).pipe(
Effect.flatMap((res) =>
res.status === 200
? Effect.succeed(res)
: Effect.fail(new Error(`/health ${res.status}`)),
),
Effect.tapError((e) => Effect.logWarning(String(e))),
Effect.retry({ schedule: Schedule.spaced("10 seconds"), times: 60 }),
);
}),
// Cluster create (~18 min) + image build/push + Auto Mode node launch +
// NLB provisioning/DNS + URL readiness poll (~5–10 min) routinely total
// 35+ min end-to-end.
{ timeout: 2_700_000 },
);
afterAll.skipIf(!!process.env.NO_DESTROY)(sharedStack.destroy(), {
timeout: 600_000,
});
test.provider(
"bound DynamoDB PutItem writes an item from inside the pod",
() =>
Effect.gen(function* () {
// Deterministic id: the table is created fresh by this suite's deploy
// (beforeAll starts with a destroy), so no stale item can pre-exist,
// and a stable id keeps re-runs convergent instead of accreting items.
const itemId = "eks-deployment-put-item";
const res = yield* HttpClient.get(`${baseUrl}/put?id=${itemId}`).pipe(
Effect.retry({ schedule: Schedule.spaced("5 seconds"), times: 12 }),
);
expect(res.status).toBe(200);
const body = (yield* res.json) as { written: string; table: string };
expect(body.written).toBe(itemId);
// Prove the binding actually reached DynamoDB: read the item back
// out-of-band via the control-plane API.
const got = yield* dynamodb
.getItem({ TableName: body.table, Key: { pk: { S: itemId } } })
.pipe(
Effect.retry({
schedule: Schedule.spaced("2 seconds"),
times: 10,
}),
);
expect(got.Item?.pk?.S).toBe(itemId);
}),
{ timeout: 180_000 },
);
test.provider(
"HelmChart renders and applies its objects onto the cluster",
() =>
Effect.gen(function* () {
// The chart rendered both ConfigMaps (the conditional one was
// toggled on via values) into the default namespace.
expect(helmRelease.objects).toHaveLength(2);
expect(helmRelease.namespace).toBe("default");
// The generic attributes persist the cluster connection for
// adapter-based delete.
expect(helmRelease.connection.auth.kind).toBe("aws-eks");
// Read the primary ConfigMap back out-of-band through the
// Kubernetes API and prove the values reached the cluster.
const configRef = helmRelease.objects.find((object) =>
object.name.endsWith("-config"),
)!;
expect(configRef).toBeDefined();
const transport = yield* makeEksTransport({
clusterName: helmCluster.clusterName,
endpoint: helmCluster.endpoint!,
certificateAuthorityData: helmCluster.certificateAuthorityData!,
});
const applied = (yield* readObject({
transport,
object: configRef,
})) as { data?: Record<string, string> } | undefined;
expect(applied?.data?.message).toBe("helm-e2e");
expect(applied?.data?.release).toBe(helmRelease.releaseName);
}),
{ timeout: 120_000 },
);
},
);