| 1 | import * as GCP from "@/GCP"; |
| 2 | import * as Test from "@/Test/Alchemy"; |
| 3 | import * as certificatemanager from "@distilled.cloud/gcp/certificatemanager_v1"; |
| 4 | import { expect } from "alchemy-test"; |
| 5 | import * as Effect from "effect/Effect"; |
| 6 | import { MinimumLogLevel } from "effect/References"; |
| 7 | import * as Schedule from "effect/Schedule"; |
| 8 | |
| 9 | const { test } = Test.make({ providers: GCP.providers() }); |
| 10 | |
| 11 | const logLevel = Effect.provideService( |
| 12 | MinimumLogLevel, |
| 13 | process.env.DEBUG ? "Debug" : "Info", |
| 14 | ); |
| 15 | |
| 16 | // Self-signed RSA-2048 fixtures generated once with openssl (not at test time). |
| 17 | const CERT_A = `-----BEGIN CERTIFICATE----- |
| 18 | MIIDOjCCAiKgAwIBAgIUY07uz+vKYeBK3NYMhk03eWyTPKkwDQYJKoZIhvcNAQEL |
| 19 | BQAwHTEbMBkGA1UEAwwSYWxjaGVteS1zc2wtYS50ZXN0MB4XDTI2MDgyNDE1NDM0 |
| 20 | NFoXDTM2MDgyMTE1NDM0NFowHTEbMBkGA1UEAwwSYWxjaGVteS1zc2wtYS50ZXN0 |
| 21 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzUAYVVO5iOluteYkxOqA |
| 22 | bBF+EnWrtfsUMoPJPXBEIrmT66G+vx+rArDsRxJqnWr9YstEQKsHtkErGF37avZr |
| 23 | 12jTIiKWk9auxyhN8O6WD2jND13m+IfsdeWKSIFx3j/RJ0McJQYu4YuN8ViZm3t9 |
| 24 | o3f5N+4DczgqzloV7U6/DFeMZ23nwBD9IgrUf6KyGoC/sJ4twTjpxSECekiOHgyn |
| 25 | /M7SwrBTJYJTsMXzqYBPVjk7f17+7LAwNqBNzQx6HsGV8sZX4kDlcMAL53obLV1z |
| 26 | kkJHhP3suaWCUl5uarZ9r5vc7DC2j6vvtghFsDvRWqqram3fCEOJOQmoAVlChbtq |
| 27 | SwIDAQABo3IwcDAdBgNVHQ4EFgQUEicpezwWkZY3JMEwx1D0eraiFCEwHwYDVR0j |
| 28 | BBgwFoAUEicpezwWkZY3JMEwx1D0eraiFCEwDwYDVR0TAQH/BAUwAwEB/zAdBgNV |
| 29 | HREEFjAUghJhbGNoZW15LXNzbC1hLnRlc3QwDQYJKoZIhvcNAQELBQADggEBAMRj |
| 30 | u6+KYAYwX/cuJruXaX7Pa9kX4Es+637dDPWBP9lLedVOGEpOujiDn2JUuNZNl6vo |
| 31 | eqaJrjQuJkSgVVYikz3dGWTEvqlDIJ5nbRIo8etW1d5eoZeWcd95t2VXNRlfULBS |
| 32 | eNOFYOI6iaPW2CQXFvNAgER/Azj8Y4ilky504m0bqkZWTl/2f+x1KaiBqkxqiOdX |
| 33 | n5gPpu+jujVOyJni7KJo4+iIdWtQI1xAazXyOFefTMkPGLkXeQ/+tXrmY9NoVvNO |
| 34 | JBEe8o+wi/LxH/K9/It1rCfWckb5zHU57li4ZxetlwYssVW4Of25sMp9c4JNF3gJ |
| 35 | mkhcNRA0cRkbnom+kn8= |
| 36 | -----END CERTIFICATE-----`; |
| 37 | |
| 38 | const KEY_A = `-----BEGIN PRIVATE KEY----- |
| 39 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDNQBhVU7mI6W61 |
| 40 | 5iTE6oBsEX4Sdau1+xQyg8k9cEQiuZProb6/H6sCsOxHEmqdav1iy0RAqwe2QSsY |
| 41 | Xftq9mvXaNMiIpaT1q7HKE3w7pYPaM0PXeb4h+x15YpIgXHeP9EnQxwlBi7hi43x |
| 42 | WJmbe32jd/k37gNzOCrOWhXtTr8MV4xnbefAEP0iCtR/orIagL+wni3BOOnFIQJ6 |
| 43 | SI4eDKf8ztLCsFMlglOwxfOpgE9WOTt/Xv7ssDA2oE3NDHoewZXyxlfiQOVwwAvn |
| 44 | ehstXXOSQkeE/ey5pYJSXm5qtn2vm9zsMLaPq++2CEWwO9Faqqtqbd8IQ4k5CagB |
| 45 | WUKFu2pLAgMBAAECggEASEceaonZx69gV7jsUNW9lJDSYyDB74sz0RgceUC8Fbhh |
| 46 | MTSbrcUynPd9tQd0uOuQwEYRSm6QACvRx7psy31se4ZD93zTpssOcD6ut73k5RdE |
| 47 | QvmP2QxQhjHncOH4ncm+VwOoeRdE0hMpUIihSyIBG9wnTH1KBLyOQm1x1EgLOgZH |
| 48 | DPtZPUg4j/KeMF5HbI0ZycSeMtWmwhYfsHrgFH/lIm/6TKaEJ0SNlFcW4GzMlxi3 |
| 49 | KEeoBw5RhKwY+Hs+zEy78udGqe+5aahSqk7IaO1UlDEmBFoNksuC6ZyEXITPJKbV |
| 50 | BU8rH6jI/xdHqdOaWBI/rLYxgm2IMrqANR3B0hF78QKBgQDnpFmpyfoj2w7nxhlg |
| 51 | d+9RuXC55Q0UMXPPIZ3b4lmNqmPcgkC55pu64D6snxj3HBeWvZ8a8E7WifW6fo0V |
| 52 | 68wpzBfMIiE7h0YWeNpfhCZnz61xOblshTpKy+QHIl5iBmrQUUfMuMjJKmIynjyI |
| 53 | WRT/d+OzzWm2eDsNoajpT4gFcQKBgQDi1Uy76+xggrLvHYfhEFyz6UYn9xVe7Chl |
| 54 | ru1vHQjTIx+V0pDmlHpKbK0+tyspZTeWYYMwRzqol7u76mmXZXZ7WK3aFA6Q8aYl |
| 55 | 4KO6wS70Lryt5oB5QFvpkiSMFl1bzHGvCxCTX+uieR796Pfms6zHwzK0/ibe8lEJ |
| 56 | KNplRXMdewKBgQC9aHG4l+LldrWVZzJQ40DY/lziZBxxqo4bjE1cApVfdTf6krcC |
| 57 | S0KDZ+FXnS/4vwu6wopaqKyOWHiJaflLN2fVtYCv9iheWJpCvccx2wjcUcBsmNq5 |
| 58 | laa4ikeGXd/3H3AvroabK21isDljUmgExXKaAho6Z3hNL7p5xvoq7FE4wQKBgQC5 |
| 59 | bXCK9nOG+ZDYk6VuQHfnwrxNE1ju/dKQPQ1vlaaPItlBGp7FP38ws+JzsDyiXFGy |
| 60 | pwgdQT0ccN1Q4nFrB9BxSK7l5Rt7NW+C6z4s/psplcM7zYAcnpYEPCmQMwAieOA+ |
| 61 | HadxMipn6OeC3R06BIsryc/70P9ppWDFQhY2Ty2pXQKBgCPjmTUuOwE1J0aegpb3 |
| 62 | eBZPBH701wZufH75uMVB5PApxO1PKMyRqeOMhbLGxws6jgf2YOPqmB9zSjQWEeQ/ |
| 63 | ME7uU5bUhTH24H7jqxo3U7wWVRWgy2TvR4gfcfI9QKVXSzDBauq1DiW86j6edI+2 |
| 64 | fygch3+OZKV+N0l93MQpSWv4 |
| 65 | -----END PRIVATE KEY-----`; |
| 66 | |
| 67 | const certificateIdOf = (name: string) => name.split("/").pop() ?? name; |
| 68 | |
| 69 | const hasCertificate = ( |
| 70 | names: readonly string[] | undefined, |
| 71 | certName: string, |
| 72 | ) => |
| 73 | (names ?? []).some( |
| 74 | (name) => certificateIdOf(name) === certificateIdOf(certName), |
| 75 | ); |
| 76 | |
| 77 | const waitUntilGone = (name: string) => |
| 78 | certificatemanager |
| 79 | .getProjectsLocationsCertificateMapsCertificateMapEntries({ name }) |
| 80 | .pipe( |
| 81 | Effect.as("found" as const), |
| 82 | Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)), |
| 83 | Effect.repeat({ |
| 84 | schedule: Schedule.spaced("1 second"), |
| 85 | until: (status) => status === "gone", |
| 86 | times: 10, |
| 87 | }), |
| 88 | ); |
| 89 | |
| 90 | test.provider( |
| 91 | "create, update, and delete a certificate map entry", |
| 92 | (stack) => |
| 93 | Effect.gen(function* () { |
| 94 | yield* stack.destroy(); |
| 95 | |
| 96 | const created = yield* stack.deploy( |
| 97 | Effect.gen(function* () { |
| 98 | const map = yield* GCP.CertificateManager.CertificateMap( |
| 99 | "FrontendMap", |
| 100 | { |
| 101 | location: "global", |
| 102 | description: "frontend map for entries", |
| 103 | }, |
| 104 | ); |
| 105 | const certA = yield* GCP.CertificateManager.Certificate("TlsA", { |
| 106 | location: "global", |
| 107 | pemCertificate: CERT_A, |
| 108 | pemPrivateKey: KEY_A, |
| 109 | }); |
| 110 | const entry = yield* GCP.CertificateManager.CertificateMapEntry( |
| 111 | "Www", |
| 112 | { |
| 113 | certificateMap: map.name, |
| 114 | description: "entry a", |
| 115 | labels: { env: "test" }, |
| 116 | certificates: [certA.name], |
| 117 | hostname: "alchemy-ssl-a.test", |
| 118 | }, |
| 119 | ); |
| 120 | return { map, certA, entry }; |
| 121 | }), |
| 122 | ); |
| 123 | |
| 124 | expect(created.entry.name).toContain("/certificateMapEntries/"); |
| 125 | expect(created.entry.certificateMapEntryId).toEqual(expect.any(String)); |
| 126 | expect(created.entry.certificateMap).toEqual(created.map.name); |
| 127 | expect(created.entry.location).toEqual("global"); |
| 128 | expect(created.entry.description).toEqual("entry a"); |
| 129 | expect(created.entry.labels).toMatchObject({ env: "test" }); |
| 130 | expect(created.entry.hostname).toEqual("alchemy-ssl-a.test"); |
| 131 | expect(created.entry.certificates).toContain(created.certA.name); |
| 132 | expect(created.entry.createTime).toEqual(expect.any(String)); |
| 133 | |
| 134 | const fetched = |
| 135 | yield* certificatemanager.getProjectsLocationsCertificateMapsCertificateMapEntries( |
| 136 | { |
| 137 | name: created.entry.name, |
| 138 | }, |
| 139 | ); |
| 140 | expect(fetched.name).toEqual(created.entry.name); |
| 141 | expect(fetched.hostname).toEqual("alchemy-ssl-a.test"); |
| 142 | expect(fetched.description).toEqual("entry a"); |
| 143 | expect(fetched.labels?.env).toEqual("test"); |
| 144 | expect(hasCertificate(fetched.certificates, created.certA.name)).toEqual( |
| 145 | true, |
| 146 | ); |
| 147 | expect( |
| 148 | Object.keys(fetched.labels ?? {}).some((key) => |
| 149 | key.startsWith("alchemy-"), |
| 150 | ), |
| 151 | ).toEqual(true); |
| 152 | |
| 153 | const updated = yield* stack.deploy( |
| 154 | Effect.gen(function* () { |
| 155 | const map = yield* GCP.CertificateManager.CertificateMap( |
| 156 | "FrontendMap", |
| 157 | { |
| 158 | certificateMapId: created.map.certificateMapId, |
| 159 | location: "global", |
| 160 | description: "frontend map for entries", |
| 161 | }, |
| 162 | ); |
| 163 | const certA = yield* GCP.CertificateManager.Certificate("TlsA", { |
| 164 | certificateId: created.certA.certificateId, |
| 165 | location: "global", |
| 166 | pemCertificate: CERT_A, |
| 167 | pemPrivateKey: KEY_A, |
| 168 | }); |
| 169 | const entry = yield* GCP.CertificateManager.CertificateMapEntry( |
| 170 | "Www", |
| 171 | { |
| 172 | certificateMap: map.name, |
| 173 | certificateMapEntryId: created.entry.certificateMapEntryId, |
| 174 | description: "entry b", |
| 175 | labels: { env: "prod", role: "sni" }, |
| 176 | certificates: [certA.name], |
| 177 | hostname: "alchemy-ssl-a.test", |
| 178 | }, |
| 179 | ); |
| 180 | return { map, certA, entry }; |
| 181 | }), |
| 182 | ); |
| 183 | |
| 184 | expect(updated.entry.name).toEqual(created.entry.name); |
| 185 | expect(updated.entry.description).toEqual("entry b"); |
| 186 | expect(updated.entry.labels).toMatchObject({ env: "prod", role: "sni" }); |
| 187 | expect(updated.entry.hostname).toEqual("alchemy-ssl-a.test"); |
| 188 | expect(updated.entry.certificates).toContain(created.certA.name); |
| 189 | |
| 190 | const refetched = |
| 191 | yield* certificatemanager.getProjectsLocationsCertificateMapsCertificateMapEntries( |
| 192 | { |
| 193 | name: created.entry.name, |
| 194 | }, |
| 195 | ); |
| 196 | expect(refetched.description).toEqual("entry b"); |
| 197 | expect(refetched.labels?.env).toEqual("prod"); |
| 198 | expect(refetched.labels?.role).toEqual("sni"); |
| 199 | expect(refetched.hostname).toEqual("alchemy-ssl-a.test"); |
| 200 | expect( |
| 201 | hasCertificate(refetched.certificates, created.certA.name), |
| 202 | ).toEqual(true); |
| 203 | |
| 204 | yield* stack.destroy(); |
| 205 | |
| 206 | const gone = yield* waitUntilGone(created.entry.name); |
| 207 | expect(gone).toEqual("gone"); |
| 208 | }).pipe(logLevel), |
| 209 | { |
| 210 | tags: ["provider:gcp", "provider:gcp:certificatemanager", "live"], |
| 211 | timeout: 120_000, |
| 212 | }, |
| 213 | ); |