1import * as GCP from "@/GCP";
2import * as Test from "@/Test/Alchemy";
3import * as gkehub from "@distilled.cloud/gcp/gkehub_v2";
4import { expect } from "alchemy-test";
5import * as Effect from "effect/Effect";
6import { MinimumLogLevel } from "effect/References";
7import * as Schedule from "effect/Schedule";
8import { GcpEnvironment } from "@/GCP/Environment";
9
10const { test } = Test.make({ providers: GCP.providers() });
11
12const logLevel = Effect.provideService(
13 MinimumLogLevel,
14 process.env.DEBUG ? "Debug" : "Info",
15);
16
17const membership = process.env.GCP_TEST_GKE_MEMBERSHIP;
18// The full lifecycle needs a registered Fleet membership; set
19// GCP_TEST_GKE_MEMBERSHIP to run it.
20const runLifecycle = !!membership;
21
22const missingMembershipOf = (project: string) =>
23 `projects/${project}/locations/global/memberships/alchemy-missing-membership`;
24const missingFeatureOf = (project: string) =>
25 `${missingMembershipOf(project)}/features/configmanagement`;
26
27const waitUntilGone = (name: string) =>
28 gkehub.getProjectsLocationsMembershipsFeatures({ name }).pipe(
29 Effect.as("found" as const),
30 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
31 Effect.repeat({
32 schedule: Schedule.spaced("2 seconds"),
33 until: (status) => status === "gone",
34 times: 10,
35 }),
36 );
37
38test.provider(
39 "getProjectsLocationsMembershipsFeatures on a missing feature fails with NotFound",
40 (stack) =>
41 Effect.gen(function* () {
42 const { project } = yield* GcpEnvironment.current;
43 const missingFeature = missingFeatureOf(project);
44 yield* stack.destroy();
45
46 const error = yield* Effect.flip(
47 gkehub.getProjectsLocationsMembershipsFeatures({
48 name: missingFeature,
49 }),
50 );
51 expect(error._tag).toEqual("NotFound");
52
53 const page = yield* gkehub.listProjectsLocationsMembershipsFeatures({
54 parent: `projects/${project}/locations/global/memberships/-`,
55 pageSize: 10,
56 });
57 expect(
58 (page.membershipFeatures ?? []).map((feature) => feature.name),
59 ).not.toContain(missingFeature);
60
61 yield* stack.destroy();
62 }).pipe(logLevel),
63 { tags: ["provider:gcp", "provider:gcp:gkehub", "live"], timeout: 90_000 },
64);
65
66test.provider.skipIf(runLifecycle)(
67 "create against a missing membership fails with NotFound",
68 (stack) =>
69 Effect.gen(function* () {
70 const { project } = yield* GcpEnvironment.current;
71 const missingMembership = missingMembershipOf(project);
72 yield* stack.destroy();
73
74 const error = yield* Effect.flip(
75 stack.deploy(
76 Effect.gen(function* () {
77 return yield* GCP.GKEHub.MembershipsFeature("ConfigSync", {
78 membership: missingMembership,
79 featureId: "configmanagement",
80 spec: {
81 configmanagement: {
82 configSync: { enabled: false },
83 },
84 },
85 labels: { env: "test" },
86 });
87 }),
88 ),
89 );
90 expect(error._tag).toEqual("NotFound");
91
92 yield* stack.destroy();
93 }).pipe(logLevel),
94 { tags: ["provider:gcp", "provider:gcp:gkehub", "live"], timeout: 90_000 },
95);
96
97test.provider.skipIf(!runLifecycle)(
98 "create, update, and delete a membership feature",
99 (stack) =>
100 Effect.gen(function* () {
101 yield* stack.destroy();
102
103 const created = yield* stack.deploy(
104 Effect.gen(function* () {
105 return yield* GCP.GKEHub.MembershipsFeature("ConfigSync", {
106 membership: membership!,
107 featureId: "configmanagement",
108 spec: {
109 configmanagement: {
110 configSync: { enabled: false },
111 },
112 },
113 labels: { env: "test" },
114 });
115 }),
116 );
117
118 expect(created.name).toContain("/features/");
119 expect(created.featureId).toEqual("configmanagement");
120 expect(created.membership).toEqual(
121 membership!.includes("/")
122 ? membership!.replace(/\/+$/, "")
123 : expect.stringContaining(`/memberships/${membership}`),
124 );
125 expect(created.labels).toMatchObject({ env: "test" });
126
127 const fetched = yield* gkehub.getProjectsLocationsMembershipsFeatures({
128 name: created.name,
129 });
130 expect(fetched.name).toEqual(created.name);
131 expect(fetched.labels?.env).toEqual("test");
132 expect(fetched.labels?.["alchemy-id"]).toEqual(expect.any(String));
133
134 const updated = yield* stack.deploy(
135 Effect.gen(function* () {
136 return yield* GCP.GKEHub.MembershipsFeature("ConfigSync", {
137 membership: created.membership,
138 featureId: created.featureId,
139 location: created.location,
140 spec: {
141 configmanagement: {
142 configSync: { enabled: false, preventDrift: true },
143 },
144 },
145 labels: { env: "prod", role: "hub" },
146 });
147 }),
148 );
149
150 expect(updated.name).toEqual(created.name);
151 expect(updated.labels).toMatchObject({ env: "prod", role: "hub" });
152
153 yield* stack.destroy();
154 const gone = yield* waitUntilGone(created.name);
155 expect(gone).toEqual("gone");
156 }).pipe(logLevel),
157 { tags: ["provider:gcp", "provider:gcp:gkehub", "live"], timeout: 120_000 },
158);