1import * as GCP from "@/GCP";
2import * as Test from "@/Test/Alchemy";
3import * as compute from "@distilled.cloud/gcp/compute_v1";
4import { expect } from "alchemy-test";
5import * as Effect from "effect/Effect";
6import { MinimumLogLevel } from "effect/References";
7import * as Schedule from "effect/Schedule";
8import {
9 CERT_A_PEM,
10 CERT_B_PEM,
11 KEY_A_PEM,
12 KEY_B_PEM,
13} from "./fixtures/https-proxy-cert.ts";
14import { GcpEnvironment } from "@/GCP/Environment";
15
16const { test } = Test.make({ providers: GCP.providers() });
17
18const logLevel = Effect.provideService(
19 MinimumLogLevel,
20 process.env.DEBUG ? "Debug" : "Info",
21);
22
23const waitUntilGone = (targetSslProxyName: string) =>
24 GcpEnvironment.current.pipe(
25 Effect.flatMap(({ project }) =>
26 compute
27 .getTargetSslProxies({ project, targetSslProxy: targetSslProxyName })
28 .pipe(
29 Effect.as("found" as const),
30 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
31 Effect.repeat({
32 schedule: Schedule.spaced("1 second"),
33 until: (status) => status === "gone",
34 times: 10,
35 }),
36 ),
37 ),
38 );
39
40const resourceTail = (value: string | undefined): string => {
41 if (value === undefined || value.length === 0) return "";
42 const parts = value.split("/").filter((part) => part.length > 0);
43 return parts[parts.length - 1] ?? "";
44};
45
46test.provider(
47 "create, update, and delete a target ssl proxy",
48 (stack) =>
49 Effect.gen(function* () {
50 const { project } = yield* GcpEnvironment.current;
51 yield* stack.destroy();
52
53 const created = yield* stack.deploy(
54 Effect.gen(function* () {
55 const check = yield* GCP.Compute.HealthCheck("Probe", {
56 description: "ssl probe",
57 type: "TCP",
58 tcpHealthCheck: { port: 443 },
59 });
60 const backend = yield* GCP.Compute.BackendService("SslA", {
61 protocol: "SSL",
62 loadBalancingScheme: "EXTERNAL",
63 description: "ssl backend a",
64 healthChecks: [check.selfLink.as<string>()],
65 });
66 const cert = yield* GCP.Compute.SslCertificate("TlsA", {
67 description: "frontend tls a",
68 certificate: CERT_A_PEM,
69 privateKey: KEY_A_PEM,
70 });
71 const proxy = yield* GCP.Compute.TargetSslProxy("Proxy", {
72 description: "ssl frontend",
73 service: backend.name,
74 sslCertificates: [cert.sslCertificateName],
75 });
76 return { check, backend, cert, proxy };
77 }),
78 );
79
80 expect(created.proxy.targetSslProxyName).toEqual(expect.any(String));
81 expect(created.proxy.description).toEqual("ssl frontend");
82 expect(
83 created.proxy.proxyHeader === "NONE" ||
84 created.proxy.proxyHeader === undefined,
85 ).toEqual(true);
86 expect(resourceTail(created.proxy.service)).toEqual(created.backend.name);
87 expect(created.proxy.sslCertificates.map(resourceTail)).toContain(
88 created.cert.sslCertificateName,
89 );
90
91 const fetched = yield* compute.getTargetSslProxies({
92 project,
93 targetSslProxy: created.proxy.targetSslProxyName,
94 });
95 expect(fetched.name).toEqual(created.proxy.targetSslProxyName);
96 expect(resourceTail(fetched.service)).toEqual(
97 resourceTail(created.proxy.service),
98 );
99 expect(fetched.description).toContain("[alchemy ");
100 expect(fetched.description).toContain("ssl frontend");
101 expect((fetched.sslCertificates ?? []).map(resourceTail)).toContain(
102 created.cert.sslCertificateName,
103 );
104
105 const updated = yield* stack.deploy(
106 Effect.gen(function* () {
107 const check = yield* GCP.Compute.HealthCheck("Probe", {
108 healthCheckName: created.check.healthCheckName,
109 description: "ssl probe",
110 type: "TCP",
111 tcpHealthCheck: { port: 443 },
112 });
113 yield* GCP.Compute.BackendService("SslA", {
114 name: created.backend.name,
115 protocol: "SSL",
116 loadBalancingScheme: "EXTERNAL",
117 description: "ssl backend a",
118 healthChecks: [check.selfLink.as<string>()],
119 });
120 const other = yield* GCP.Compute.BackendService("SslB", {
121 protocol: "SSL",
122 loadBalancingScheme: "EXTERNAL",
123 description: "ssl backend b",
124 healthChecks: [check.selfLink.as<string>()],
125 });
126 yield* GCP.Compute.SslCertificate("TlsA", {
127 sslCertificateName: created.cert.sslCertificateName,
128 description: "frontend tls a",
129 certificate: CERT_A_PEM,
130 privateKey: KEY_A_PEM,
131 });
132 const certB = yield* GCP.Compute.SslCertificate("TlsB", {
133 description: "frontend tls b",
134 certificate: CERT_B_PEM,
135 privateKey: KEY_B_PEM,
136 });
137 const proxy = yield* GCP.Compute.TargetSslProxy("Proxy", {
138 targetSslProxyName: created.proxy.targetSslProxyName,
139 description: "ssl frontend",
140 service: other.name,
141 sslCertificates: [certB.sslCertificateName],
142 proxyHeader: "PROXY_V1",
143 });
144 return { other, certB, proxy };
145 }),
146 );
147
148 expect(updated.proxy.targetSslProxyName).toEqual(
149 created.proxy.targetSslProxyName,
150 );
151 expect(updated.proxy.description).toEqual("ssl frontend");
152 expect(updated.proxy.proxyHeader).toEqual("PROXY_V1");
153 expect(resourceTail(updated.proxy.service)).toEqual(updated.other.name);
154 expect(updated.proxy.sslCertificates.map(resourceTail)).toContain(
155 updated.certB.sslCertificateName,
156 );
157 expect(updated.proxy.sslCertificates.map(resourceTail)).not.toContain(
158 created.cert.sslCertificateName,
159 );
160
161 const refetched = yield* compute.getTargetSslProxies({
162 project,
163 targetSslProxy: updated.proxy.targetSslProxyName,
164 });
165 expect(refetched.description).toContain("ssl frontend");
166 expect(refetched.proxyHeader).toEqual("PROXY_V1");
167 expect(resourceTail(refetched.service)).toEqual(
168 resourceTail(updated.proxy.service),
169 );
170 expect(resourceTail(refetched.service)).not.toEqual(
171 resourceTail(created.proxy.service),
172 );
173 expect((refetched.sslCertificates ?? []).map(resourceTail)).toContain(
174 updated.certB.sslCertificateName,
175 );
176
177 yield* stack.destroy();
178
179 const gone = yield* waitUntilGone(created.proxy.targetSslProxyName);
180 expect(gone).toEqual("gone");
181 }).pipe(logLevel),
182 { tags: ["provider:gcp", "provider:gcp:compute", "live"], timeout: 180_000 },
183);