1import * as AWS from "@/AWS";
2import { HttpNamespace, InstanceRegistration, Service } from "@/AWS/CloudMap";
3import * as Test from "@/Test/Alchemy";
4import * as sd from "@distilled.cloud/aws/servicediscovery";
5import { expect } from "alchemy-test";
6import * as Data from "effect/Data";
7import * as Effect from "effect/Effect";
8import * as Schedule from "effect/Schedule";
9
10const { test } = Test.make({ providers: AWS.providers() });
11
12const findInstance = (serviceId: string, instanceId: string) =>
13 sd.getInstance({ ServiceId: serviceId, InstanceId: instanceId }).pipe(
14 Effect.map((r) => r.Instance),
15 Effect.catchTag(["InstanceNotFound", "ServiceNotFound"], () =>
16 Effect.succeed(undefined),
17 ),
18 );
19
20class InstanceStillExists extends Data.TaggedError("InstanceStillExists")<{
21 readonly instanceId: string;
22}> {}
23
24const assertInstanceDeleted = (serviceId: string, instanceId: string) =>
25 findInstance(serviceId, instanceId).pipe(
26 Effect.flatMap((instance) =>
27 instance === undefined
28 ? Effect.void
29 : Effect.fail(new InstanceStillExists({ instanceId })),
30 ),
31 Effect.retry({
32 while: (e) => e._tag === "InstanceStillExists",
33 schedule: Schedule.max([
34 Schedule.spaced("3 seconds"),
35 Schedule.recurs(20),
36 ]),
37 }),
38 );
39
40test.provider(
41 "register two instances, update attributes, destroy chain (instances -> service -> namespace)",
42 (stack) =>
43 Effect.gen(function* () {
44 yield* stack.destroy();
45
46 const makeStack = (alphaEndpoint: string) =>
47 Effect.gen(function* () {
48 const namespace = yield* HttpNamespace("InstanceTestNamespace", {
49 name: "alchemy-test-cloudmap-instances",
50 });
51 const service = yield* Service("Workers", {
52 namespaceId: namespace.namespaceId,
53 name: "workers",
54 });
55 const alpha = yield* InstanceRegistration("Alpha", {
56 serviceId: service.serviceId,
57 instanceId: "alpha",
58 attributes: { endpoint: alphaEndpoint, zone: "us-west-2a" },
59 });
60 const beta = yield* InstanceRegistration("Beta", {
61 serviceId: service.serviceId,
62 instanceId: "beta",
63 attributes: { endpoint: "http://beta.internal:8080" },
64 });
65 return { namespace, service, alpha, beta };
66 });
67
68 const { namespace, service, alpha, beta } = yield* stack.deploy(
69 makeStack("http://alpha.internal:8080"),
70 );
71
72 expect(alpha.instanceId).toBe("alpha");
73 expect(beta.instanceId).toBe("beta");
74 expect(alpha.serviceId).toBe(service.serviceId);
75
76 // out-of-band verification via distilled
77 const alphaInstance = yield* findInstance(service.serviceId, "alpha");
78 expect(alphaInstance?.Attributes?.endpoint).toBe(
79 "http://alpha.internal:8080",
80 );
81 expect(alphaInstance?.Attributes?.zone).toBe("us-west-2a");
82 const betaInstance = yield* findInstance(service.serviceId, "beta");
83 expect(betaInstance?.Attributes?.endpoint).toBe(
84 "http://beta.internal:8080",
85 );
86
87 // both instances are discoverable via the data-plane API
88 const discovered = yield* sd.discoverInstances({
89 NamespaceName: "alchemy-test-cloudmap-instances",
90 ServiceName: "workers",
91 HealthStatus: "ALL",
92 });
93 const discoveredIds = (discovered.Instances ?? [])
94 .map((i) => i.InstanceId)
95 .sort();
96 expect(discoveredIds).toEqual(["alpha", "beta"]);
97
98 // attribute update re-registers (upsert) with the same identity
99 const updated = yield* stack.deploy(
100 makeStack("http://alpha.internal:9090"),
101 );
102 expect(updated.alpha.serviceId).toBe(service.serviceId);
103 expect(updated.alpha.instanceId).toBe("alpha");
104
105 const afterUpdate = yield* findInstance(service.serviceId, "alpha");
106 expect(afterUpdate?.Attributes?.endpoint).toBe(
107 "http://alpha.internal:9090",
108 );
109
110 // destroy tears down instances -> service -> namespace in order
111 yield* stack.destroy();
112 yield* assertInstanceDeleted(service.serviceId, "alpha");
113 yield* assertInstanceDeleted(service.serviceId, "beta");
114 const serviceGone = yield* sd.getService({ Id: service.serviceId }).pipe(
115 Effect.map(() => false),
116 Effect.catchTag("ServiceNotFound", () => Effect.succeed(true)),
117 );
118 expect(serviceGone).toBe(true);
119 const namespaceGone = yield* sd
120 .getNamespace({ Id: namespace.namespaceId })
121 .pipe(
122 Effect.map(() => false),
123 Effect.catchTag("NamespaceNotFound", () => Effect.succeed(true)),
124 );
125 expect(namespaceGone).toBe(true);
126 }),
127 { tags: ["provider:aws", "provider:aws:cloudmap", "live"], timeout: 240_000 },
128);