| 1 | import * as machines from "@distilled.cloud/fly-io/machines"; |
| 2 | import * as Fly from "@/Fly"; |
| 3 | import * as Provider from "@/Provider"; |
| 4 | import * as Test from "@/Test/Alchemy"; |
| 5 | import { expect } from "alchemy-test"; |
| 6 | import * as Effect from "effect/Effect"; |
| 7 | import { MinimumLogLevel } from "effect/References"; |
| 8 | import * as Result from "effect/Result"; |
| 9 | import * as Schedule from "effect/Schedule"; |
| 10 | |
| 11 | const { test } = Test.make({ providers: Fly.providers() }); |
| 12 | |
| 13 | const logLevel = Effect.provideService( |
| 14 | MinimumLogLevel, |
| 15 | process.env.DEBUG ? "Debug" : "Info", |
| 16 | ); |
| 17 | |
| 18 | const CUSTOM_HOSTNAME = "alchemy-fly-cert-1.example.com"; |
| 19 | const REPLACE_HOSTNAME = "alchemy-fly-cert-2.example.com"; |
| 20 | |
| 21 | /** |
| 22 | * Self-signed RSA-2048 certificates generated once with openssl and checked |
| 23 | * in so every test run uploads identical PEMs (never generated at test time): |
| 24 | * |
| 25 | * ```sh |
| 26 | * openssl req -x509 -newkey rsa:2048 -nodes -keyout key.pem -out cert.pem \ |
| 27 | * -days 3650 -subj "/CN=alchemy-fly-cert-1.example.com/O=Alchemy/C=US" \ |
| 28 | * -addext "subjectAltName=DNS:alchemy-fly-cert-1.example.com" |
| 29 | * ``` |
| 30 | */ |
| 31 | const CERT_1A = `-----BEGIN CERTIFICATE----- |
| 32 | MIIDQDCCAiigAwIBAgIJALfOoslKUT5HMA0GCSqGSIb3DQEBCwUAMEgxJzAlBgNV |
| 33 | BAMMHmFsY2hlbXktZmx5LWNlcnQtMS5leGFtcGxlLmNvbTEQMA4GA1UECgwHQWxj |
| 34 | aGVteTELMAkGA1UEBhMCVVMwHhcNMjYwODE5MDczMzA4WhcNMzYwODE2MDczMzA4 |
| 35 | WjBIMScwJQYDVQQDDB5hbGNoZW15LWZseS1jZXJ0LTEuZXhhbXBsZS5jb20xEDAO |
| 36 | BgNVBAoMB0FsY2hlbXkxCzAJBgNVBAYTAlVTMIIBIjANBgkqhkiG9w0BAQEFAAOC |
| 37 | AQ8AMIIBCgKCAQEAw94Wjab6qnzkDDoZgnzKUSoszHvkO4oTRio+dLHWaN6T1nCe |
| 38 | YaoWlzjBVzSnpyHlKmCjzyYlLFbWdNxuyeqr8js0/pfQSCDAJetvWJCwHqcDxYV9 |
| 39 | fOxjNcbRyK22HcQW/iaqDFBmTb9HPp2zyH7smuFDgS6zEFWintoRtkizqAhdkl3c |
| 40 | hI9soUK5jVoOK9f9xRbGMGf8JBuBevmzdZ2kosp346FuV3XCxzsvVcx3ub+QN0Gg |
| 41 | K2MGW+c5gwGmltOmycdMpATIwrv7RtVH51cEjLHVGnySlMnmITDdtMnd2dlMjShe |
| 42 | hBcR/Wn49iGz5y6mMz1s5NN7TjtzmmYJcx3sCQIDAQABoy0wKzApBgNVHREEIjAg |
| 43 | gh5hbGNoZW15LWZseS1jZXJ0LTEuZXhhbXBsZS5jb20wDQYJKoZIhvcNAQELBQAD |
| 44 | ggEBAFMH+KMuoyc4z8TXpir+qptIrnqNb1klioxg8zpyvslXw5vmOjP+u6WXN3rK |
| 45 | 9f1YqONz77+h0QO2vx/gBYmx1Ax0e/xfIw0b1T+HOXCjk5Z9EL4zxx4oiotZltza |
| 46 | meWmuIDNuIkEbmQJZ55ET3UFtjQOkJCPXFTye9imB/jAQplLpFEL7JXP4jbqD64Y |
| 47 | Cr+R+xfzKk1vlOLj4a36atZeLrvqz176jUC/KFHjvUaa+vW5FByoWtOb2FKbdBC7 |
| 48 | uebgS7SNRiqw2w7FbvQW9VsviOA7s7ZPsI6yB2O926zm7lrxXMQUXpaL+O6bXx0m |
| 49 | uU0bCHvkCBRzA+ocsBEH7sxAgI8= |
| 50 | -----END CERTIFICATE----- |
| 51 | `; |
| 52 | |
| 53 | const KEY_1A = `-----BEGIN PRIVATE KEY----- |
| 54 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDD3haNpvqqfOQM |
| 55 | OhmCfMpRKizMe+Q7ihNGKj50sdZo3pPWcJ5hqhaXOMFXNKenIeUqYKPPJiUsVtZ0 |
| 56 | 3G7J6qvyOzT+l9BIIMAl629YkLAepwPFhX187GM1xtHIrbYdxBb+JqoMUGZNv0c+ |
| 57 | nbPIfuya4UOBLrMQVaKe2hG2SLOoCF2SXdyEj2yhQrmNWg4r1/3FFsYwZ/wkG4F6 |
| 58 | +bN1naSiynfjoW5XdcLHOy9VzHe5v5A3QaArYwZb5zmDAaaW06bJx0ykBMjCu/tG |
| 59 | 1UfnVwSMsdUafJKUyeYhMN20yd3Z2UyNKF6EFxH9afj2IbPnLqYzPWzk03tOO3Oa |
| 60 | ZglzHewJAgMBAAECggEACmEtjsonhHtj1mYJzglw60Yx45A5MxKJHPHGJ4b5Fsuc |
| 61 | yANl0UUjN1ZRoJ5wuAGq1EkUxTh/Rc9ARCceU+L0w7xxfYBsEDZ0GE5WsznPq4As |
| 62 | Sf5d/Q3F4CauHVVfTkqC8Wr5Hffww/P8AYx354saXMbNPf5MjPQMzyA9SgymQKRB |
| 63 | Ca88cZRTSKAO1exTCLas8Lc5d22uJADKhCivjZzRt2eKf3bX2Kr0N89M76QDVWIV |
| 64 | jcCDHxZ2yd2nkS8N9S8kHJQatmc5/YRhcCqfuJSrRNq/vUXKAL7od/PHiB/s+fe8 |
| 65 | mboPovuElV6Qhwea8wuLDKpmZpPz9vxyNkDeVyhVhQKBgQDyvumG5lrn9iuDTFaf |
| 66 | MrvQGLzoIhu6CnRJQhMjOM8zfa+h74Ej9bZFwzIAv1abs0rkdn7/Lw7Y4+14y4V0 |
| 67 | V8JZ09RMStLCCMa4se30P7+H6P4sVRvgIo7ACSNyJ5AyotgCXGUQw+rU4Ew0B92o |
| 68 | oKuyPzzjiSV1MB2t0LehnHzPywKBgQDOj+odrz/hQOvDC1bzpqX4f5iVG2hfwWzz |
| 69 | AsgRbojCp0KlrBW6Fz2LHLDk9EyCXON2xDhIHnzgHvkhleaNMFFoZu7aIOJjjE2k |
| 70 | uZt8ZusDWHDoFmvhgijHYo50CRm+iP5MqZnZ330h2UpyLsDpsFMuUTbIh86QeoUs |
| 71 | 287xPBSQ+wKBgQCYM+Qw1VbCgrOdy8u0Xhcsz5YC5wADknJ/TJK7Tu98FZ5+JrIO |
| 72 | Xg4/h7heh5pCXhTjUvkl/9eLXr6TMukmnbAaqps+itvDFcWkIMxWjXIGQay9F1A3 |
| 73 | JPPkrNYwyWW8miZetJgZ/v3LJCgjp5rwFG4TgVsoP+HgrJ6vUWMSThBkKQKBgCUP |
| 74 | /3LKEg0pp+O8MiLPoPIAevEuFMExpInJ0voFujYq6rNtOAzGxL0kLb029E9juVCD |
| 75 | DpIqHj/cbtkO22oz5Dd2WJ78zYINF5VZ1EMy+DIGeWO7OiohP43e6i55v4vHatF4 |
| 76 | kOldx0b+hPQN3YFQqOwjmE/Mxkx4H1MYMX5pSEtnAoGAVh/vrfIAm6BlnHN8bOEy |
| 77 | guJG67NWTGleZd/z3dsyjqZr/TKsUR/qnsvN+uIJF6xB8CAGrwGuPcPk2p+91CKP |
| 78 | zGSvSBL56mSeLh7EWOVPkGBWxavEcZagZ6ehppK/8BjVPnzR4B2d+vh+WBY9uJMH |
| 79 | XBcpU0H7jtbDMicqxDGkEo4= |
| 80 | -----END PRIVATE KEY----- |
| 81 | `; |
| 82 | |
| 83 | const CERT_1B = `-----BEGIN CERTIFICATE----- |
| 84 | MIIDQDCCAiigAwIBAgIJAKGWJJtQyhFkMA0GCSqGSIb3DQEBCwUAMEgxJzAlBgNV |
| 85 | BAMMHmFsY2hlbXktZmx5LWNlcnQtMS5leGFtcGxlLmNvbTEQMA4GA1UECgwHQWxj |
| 86 | aGVteTELMAkGA1UEBhMCVVMwHhcNMjYwODE5MDczMzA4WhcNMzYwODE2MDczMzA4 |
| 87 | WjBIMScwJQYDVQQDDB5hbGNoZW15LWZseS1jZXJ0LTEuZXhhbXBsZS5jb20xEDAO |
| 88 | BgNVBAoMB0FsY2hlbXkxCzAJBgNVBAYTAlVTMIIBIjANBgkqhkiG9w0BAQEFAAOC |
| 89 | AQ8AMIIBCgKCAQEA1H+Zm+wZgrZXb6yMUBCtLXUFxAiQwtrW3CS+pFiRKqUAF/NW |
| 90 | bUzu1ksugXhsFFiLh0fCjsk5DEUh9Slix18pI/OvJM6Ae5SONl5QAwzirAwTzQLq |
| 91 | TFI2d2HTktF/RSuvM5gSI2xS0Fcpgh277AIuT4+UYQY7lt2sxpMt8z/77ir0kJEC |
| 92 | +Kdn7h+YeFJwS7fQM4vy6onilQR1hwQIITh8oAvabygorRfKirw23DFZv6/OM5Dq |
| 93 | L8oR9G1N1C07cBCEbyWvJXcSB1hPnpIMSW3DrB4zvUNukuxcyYPbN2jTbYCm2vzU |
| 94 | UP+yYCSxE/MetpkJsBcf9pQa29/PF9Sx1+R2HQIDAQABoy0wKzApBgNVHREEIjAg |
| 95 | gh5hbGNoZW15LWZseS1jZXJ0LTEuZXhhbXBsZS5jb20wDQYJKoZIhvcNAQELBQAD |
| 96 | ggEBAJvY/S7A1fI+zuvzIgmYOnku9f9VEu/fVXyNI6ZJHj1g7sF69H30R3VwRJGe |
| 97 | 0OaVRxrjcBf/WikVXMrevkb14xmWVGPyJY86lUE5ofF/H/FL/zHQ2KBGJqZdffug |
| 98 | LZydgkFHL7RX1Bo7kbQbIG0SithWnV2Gibx0QnW/HHh2OjOuYP9jSGFL4zYiVjRq |
| 99 | qIPeyOUwvVUDZcYYKTIVt2EMj5jHZFH6yHnVbJnKYjlJIs7ywzK9/X24uGI49NpX |
| 100 | knNTol8YVudJSO/V0DGsADj7kINIxs5GgwtQ0AUJvb+6P0iDVFxWVjTsF2JOf0zW |
| 101 | TmEwSgr3XQioUtj36eJ8Puux8w4= |
| 102 | -----END CERTIFICATE----- |
| 103 | `; |
| 104 | |
| 105 | const KEY_1B = `-----BEGIN PRIVATE KEY----- |
| 106 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDUf5mb7BmCtldv |
| 107 | rIxQEK0tdQXECJDC2tbcJL6kWJEqpQAX81ZtTO7WSy6BeGwUWIuHR8KOyTkMRSH1 |
| 108 | KWLHXykj868kzoB7lI42XlADDOKsDBPNAupMUjZ3YdOS0X9FK68zmBIjbFLQVymC |
| 109 | HbvsAi5Pj5RhBjuW3azGky3zP/vuKvSQkQL4p2fuH5h4UnBLt9Azi/LqieKVBHWH |
| 110 | BAghOHygC9pvKCitF8qKvDbcMVm/r84zkOovyhH0bU3ULTtwEIRvJa8ldxIHWE+e |
| 111 | kgxJbcOsHjO9Q26S7FzJg9s3aNNtgKba/NRQ/7JgJLET8x62mQmwFx/2lBrb388X |
| 112 | 1LHX5HYdAgMBAAECggEBAK0ApaLX+Xz9zvnQVPvqV9OmbmdHQfQiDsNz9vH+WVGK |
| 113 | vCTDrB2YgOpekyI9VGDMJeyhc7ikApoGvrrGUvFWfDwU7QbeSKRUMeP5rNKO7wx7 |
| 114 | WLOUKASy+jz/nCzv+tnp0t4CdOH6EwIjtFuGsbRXojSMBJVpaaJOH5gbw1FDSmx/ |
| 115 | eI1TRkwU7kHKOUkEoOHIrl1JFF5MiwtDIxYkQd6LmS1hL1oLBl4hNuUzlWVz6Utz |
| 116 | 2dP3iMNXHQ+brEw5C+v6YUk+t5goV3SIW/EKREpAIyw6zhXtGYTsPLzd6S0VrA7j |
| 117 | NI6kS6YNVRWoZHxmZEJBhgvzGN2N4UtgIAg2DCHBjoECgYEA6eAfGT5zBG5IVemr |
| 118 | x08u45dlf32ZNmKazr+LY/6+81x6ep5jc+DOUpe3muBg0gfxSCCZoMZpbIdr6STW |
| 119 | Tl/b8JCfgJ5ZeJRAiFYNfpKkfxfCjJ0df02dsXxh4bHJ5kfPfoC5ndgpMeg0oR+c |
| 120 | 0TvtdIspoTrdqNBib5qtnc+BCTkCgYEA6JnHvaCp/C03uP5lXD7zSOTj0/uRzQj8 |
| 121 | eh0W1ucgZDMlwxk+nGFj8sjhSKq5BrGskr3X9lD8O4gGJ0CYnnfA5v2sKIubWKJw |
| 122 | p1J3vwjZNaCXyfl2GgU8i0i0tKqb6GKNikyEYJY8LzImLEwHsJBFjqtmQ/INAZgG |
| 123 | WRisYhZBiAUCgYAmWg1fe2ErGdac2AvGTFLZGYuYY4VLaNIQE1MNW8n+aGzhTLXs |
| 124 | W7IZ0y1VpXPbHVhQxp/KwJ6rIG3uto66HXgYs+6kDdOINW97q/jch7mX6dKKcn1S |
| 125 | 0eyJHWeDtj7wl1gMW5mUq8zdlUGEJoX1vckLsoJqIRgWRc3OlWPhHBUB6QKBgQC3 |
| 126 | aNMsfqyrEYi30blhNK4nk3zmoZ8FOwnCzH8+878B46bqIVTSgKUMQ0QJRb1iPuWi |
| 127 | TBonuxI5avfXKcNuaWVtfyeqTPMwv93uwkN0GmkUNU6bT57Fw81K0wjS0Rjg9B1B |
| 128 | qTZU5wIMARJCqa0Cl7CRYCviddG1qQeQZ7k+GuiPdQKBgHlmMzKipMwFinvDyhNw |
| 129 | 6G8UKF+GRx1I4CnuEm6F8l+gguNQ4ufjeCpqepJQ0vQJ92UJtRdK2y5K3Wg7wl6l |
| 130 | 5/txHtxiDrOQtXHD6fZGOP9FeyUCzM15t+dskhLsnxBLbnSAbJ1+UG1rMXCGFt87 |
| 131 | FgKFMKpXLAf0maBRv4RowQtS |
| 132 | -----END PRIVATE KEY----- |
| 133 | `; |
| 134 | |
| 135 | const CERT_2 = `-----BEGIN CERTIFICATE----- |
| 136 | MIIDQDCCAiigAwIBAgIJAKXks8DaxmKRMA0GCSqGSIb3DQEBCwUAMEgxJzAlBgNV |
| 137 | BAMMHmFsY2hlbXktZmx5LWNlcnQtMi5leGFtcGxlLmNvbTEQMA4GA1UECgwHQWxj |
| 138 | aGVteTELMAkGA1UEBhMCVVMwHhcNMjYwODE5MDczMzA5WhcNMzYwODE2MDczMzA5 |
| 139 | WjBIMScwJQYDVQQDDB5hbGNoZW15LWZseS1jZXJ0LTIuZXhhbXBsZS5jb20xEDAO |
| 140 | BgNVBAoMB0FsY2hlbXkxCzAJBgNVBAYTAlVTMIIBIjANBgkqhkiG9w0BAQEFAAOC |
| 141 | AQ8AMIIBCgKCAQEA7QHJUNwTdk3V5JlofytnMAubCB+iAclI6FLkCPS8tWekXPKD |
| 142 | jx8Xp3pnl8dkMhGxEpNpZSAdH7GoWZFhcOLLnT3ZojkeN8xCnVfXqvjZplLPN/ie |
| 143 | 973shD2/cREVxRR/Bh6yc9NL528wSDEyNJ/lP5hdzNGSWiliE8jFaH3ymfeqhqYd |
| 144 | V3ghVBtyzP6l+2bjksiP2Km7ks+u0Qe9Mme4/u3wnY9dcWSCtpj8DcfKF+fUtMQh |
| 145 | RzD5TxWAV1ZZC8/bXqRzYWLbfHHVVDUE1wQI7fdEU4pwPiq70a9LyGKMlo1Upo+M |
| 146 | 4EwR2QKtAFjNz8wvMsCODoM+Buv8QtDlPKV4WQIDAQABoy0wKzApBgNVHREEIjAg |
| 147 | gh5hbGNoZW15LWZseS1jZXJ0LTIuZXhhbXBsZS5jb20wDQYJKoZIhvcNAQELBQAD |
| 148 | ggEBAKIa9MMRCDU+W+HX+S04NWax2ixA0tO4tfQeeRJwbSxxCLaeUWK/J7Sb1avw |
| 149 | iLHSxBcpMeSvN42rL0aoQtZzLuxKg+COg+frLM+1lCRf5lRZilknpFV8BUb5wWmb |
| 150 | mGbNlU5z9hmkZ2cX/50IYLaQSEXrCVofq9KBev/ohA6VtT4WEd+to1aIP0h45MmT |
| 151 | qT0+MGBT2OZVIC15tvm4K2Aw0psIkG1R+WxZDLnIIyGCN/3eHL6kAm3njXxGj+81 |
| 152 | K3xJ/mfJ6UgPvnuAbqeC+rHmWtFRbJpSI85HuFkvi60O+P1u77EjuSFmWFsLJrnp |
| 153 | /qOFL9VsXMBN3vNu7u/0ZBUBUVI= |
| 154 | -----END CERTIFICATE----- |
| 155 | `; |
| 156 | |
| 157 | const KEY_2 = `-----BEGIN PRIVATE KEY----- |
| 158 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDtAclQ3BN2TdXk |
| 159 | mWh/K2cwC5sIH6IByUjoUuQI9Ly1Z6Rc8oOPHxenemeXx2QyEbESk2llIB0fsahZ |
| 160 | kWFw4sudPdmiOR43zEKdV9eq+NmmUs83+J73veyEPb9xERXFFH8GHrJz00vnbzBI |
| 161 | MTI0n+U/mF3M0ZJaKWITyMVoffKZ96qGph1XeCFUG3LM/qX7ZuOSyI/YqbuSz67R |
| 162 | B70yZ7j+7fCdj11xZIK2mPwNx8oX59S0xCFHMPlPFYBXVlkLz9tepHNhYtt8cdVU |
| 163 | NQTXBAjt90RTinA+KrvRr0vIYoyWjVSmj4zgTBHZAq0AWM3PzC8ywI4Ogz4G6/xC |
| 164 | 0OU8pXhZAgMBAAECggEBAI++eZ71G0ixRHz1Hg+i/16Aa3Kt3NBAiFt/ipZz+M/9 |
| 165 | IA7Pd0MilHIbJyFC6V4EpOSjS1Tt+TvzwCsypWqV4vBnRsvA4Lbux6guUrt7WmYp |
| 166 | 60wDGkcfhYde+/FTb2SsXa4UiB7/lBW1tg7S56RLJftAKgPDR4QvfTCKoFATs0P1 |
| 167 | az0u0cDhDQXSLoCIs+FXswMa/o1uvPyv2SR1yj3z6G5qth55u5mOXylDu1Se7r3y |
| 168 | oM5WWV1zoSBbHNsHRu3UbOkm0stPyj2/01KU3LTuGDwlT9hfv+x4zsnTgo+CGOck |
| 169 | vLNvAywiD8td7XxOEwEVaksSAJQ80ge+9Uv7nDWlCdECgYEA+rldCBZ+2E/BKRsV |
| 170 | R9UkIi1iqwJxSR03hrU3SGbAW9S85JnIkqOcgJRtlhzMtlQ4Cym4tC/hEwcJtoZS |
| 171 | YQyFCrrxwLwwGuKIReqVwoQgqpJrhKs3+eCT/ssHTnnufQ68ObZUTDe3/yd5W0MR |
| 172 | 45sw5ucdXasNsrJ5VIwgEZ0J6bcCgYEA8f6HnW9uT6UqhcFSlA6JUOzyQxKSwRBX |
| 173 | 15l2wqAGOytbPfxo0t88RNw0zDCMLLJW/h4nH14TIqsZvmPVgbci3aOl8iK4p6Cs |
| 174 | rU6fPObzqyqWahB0uWxlvkV5xPh69lpPXgW4FHS2AJ/7LuT0K7l1xskFS0uygk82 |
| 175 | NcYXOwZ37m8CgYEAjzAVhWzzfC3wLr2c5a1AOZSZ6Pg4In1hHLmZnOuxp8BTMGrz |
| 176 | NWvjETuzaiuAbhCjAR0Oszk03V0Dmw4TNgGkaYBiWKQhBPmiwes7JB+5WDDO3rG6 |
| 177 | AibT3ous3JCeNyWXMY6DjhO0LTAqCoi/SrFyuCv9hWUPKaLrz/FGz0hURPkCgYB/ |
| 178 | wZeJFzYOmCz91lUpUw9NiPpRTLHreRxW928KSv9fJ4GRFPumMDKaTAMXtZ2bTz+8 |
| 179 | AuE2nyzNBbWenQgv5iZjCq1mSsxfuuTOzg4lUexcdixYrH1jEwYX0If75A7OllkG |
| 180 | U1CehAs+AzfRxw/dMZkrLB8+ZCVGxFjjRMQo5j5UwwKBgAL8u/8C5ZDlK9/h85NJ |
| 181 | jQGt3JmtR2FDjlamV6wAese7LgmVt7PPz/Ou42cOFDBN/p9fheLhNOA0Vo4RX6Y0 |
| 182 | ikCU8TnSPOzNvJPwSZQnP0A4fbMGUlXa5q/cmbK1VeiD2f5FdJ+G4oIlRYuCxCgd |
| 183 | onolH/5YrvTrrhSOZeUzS72j |
| 184 | -----END PRIVATE KEY----- |
| 185 | `; |
| 186 | |
| 187 | const waitUntilAppGone = (appName: string) => |
| 188 | machines.getApp({ app_name: appName }).pipe( |
| 189 | Effect.as("found" as const), |
| 190 | Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)), |
| 191 | Effect.repeat({ |
| 192 | schedule: Schedule.spaced("1 second"), |
| 193 | until: (status) => status === "gone", |
| 194 | times: 10, |
| 195 | }), |
| 196 | ); |
| 197 | |
| 198 | const waitUntilCertGone = (appName: string, hostname: string) => |
| 199 | machines.getAppCertificate({ app_name: appName, hostname }).pipe( |
| 200 | Effect.as("found" as const), |
| 201 | Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)), |
| 202 | Effect.repeat({ |
| 203 | schedule: Schedule.spaced("1 second"), |
| 204 | until: (status) => status === "gone", |
| 205 | times: 10, |
| 206 | }), |
| 207 | ); |
| 208 | |
| 209 | const deleteIfPresent = (appName: string, hostname: string) => |
| 210 | machines |
| 211 | .deleteAppCertificate({ app_name: appName, hostname }) |
| 212 | .pipe(Effect.catchTag("NotFound", () => Effect.void)); |
| 213 | |
| 214 | test.provider( |
| 215 | "create, update, and delete a custom certificate", |
| 216 | (stack) => |
| 217 | Effect.gen(function* () { |
| 218 | yield* stack.destroy(); |
| 219 | |
| 220 | const created = yield* stack.deploy( |
| 221 | Effect.gen(function* () { |
| 222 | const app = yield* Fly.App("CertApp"); |
| 223 | const cert = yield* Fly.Certificate("Www", { |
| 224 | app, |
| 225 | hostname: CUSTOM_HOSTNAME, |
| 226 | kind: "custom", |
| 227 | fullchain: CERT_1A, |
| 228 | privateKey: KEY_1A, |
| 229 | }); |
| 230 | return { app, cert }; |
| 231 | }), |
| 232 | ); |
| 233 | |
| 234 | expect(created.cert.appName).toEqual(created.app.appName); |
| 235 | expect(created.cert.hostname).toEqual(CUSTOM_HOSTNAME); |
| 236 | expect(created.cert.source).toEqual("custom"); |
| 237 | expect(created.cert.status).toEqual(expect.any(String)); |
| 238 | |
| 239 | const fetched = yield* machines.getAppCertificate({ |
| 240 | app_name: created.app.appName, |
| 241 | hostname: CUSTOM_HOSTNAME, |
| 242 | }); |
| 243 | expect(fetched.hostname).toEqual(CUSTOM_HOSTNAME); |
| 244 | expect( |
| 245 | fetched.certificates?.some((entry) => entry.source === "custom"), |
| 246 | ).toBe(true); |
| 247 | |
| 248 | const updated = yield* stack.deploy( |
| 249 | Effect.gen(function* () { |
| 250 | const app = yield* Fly.App("CertApp"); |
| 251 | const cert = yield* Fly.Certificate("Www", { |
| 252 | app, |
| 253 | hostname: CUSTOM_HOSTNAME, |
| 254 | kind: "custom", |
| 255 | fullchain: CERT_1B, |
| 256 | privateKey: KEY_1B, |
| 257 | }); |
| 258 | return { app, cert }; |
| 259 | }), |
| 260 | ); |
| 261 | |
| 262 | expect(updated.cert.hostname).toEqual(CUSTOM_HOSTNAME); |
| 263 | expect(updated.cert.source).toEqual("custom"); |
| 264 | expect(updated.cert.appName).toEqual(created.app.appName); |
| 265 | expect(updated.app.appId).toEqual(created.app.appId); |
| 266 | |
| 267 | const refetched = yield* machines.getAppCertificate({ |
| 268 | app_name: updated.app.appName, |
| 269 | hostname: CUSTOM_HOSTNAME, |
| 270 | }); |
| 271 | expect(refetched.hostname).toEqual(CUSTOM_HOSTNAME); |
| 272 | expect( |
| 273 | refetched.certificates?.some((entry) => entry.source === "custom"), |
| 274 | ).toBe(true); |
| 275 | |
| 276 | yield* stack.destroy(); |
| 277 | |
| 278 | const certGone = yield* waitUntilCertGone( |
| 279 | created.app.appName, |
| 280 | CUSTOM_HOSTNAME, |
| 281 | ); |
| 282 | expect(certGone).toEqual("gone"); |
| 283 | const appGone = yield* waitUntilAppGone(created.app.appName); |
| 284 | expect(appGone).toEqual("gone"); |
| 285 | }).pipe(logLevel), |
| 286 | { |
| 287 | tags: [ |
| 288 | "provider:fly", |
| 289 | "provider:fly:app", |
| 290 | "provider:fly:certificate", |
| 291 | "provider:fly:machine", |
| 292 | "live", |
| 293 | ], |
| 294 | timeout: 120_000, |
| 295 | }, |
| 296 | ); |
| 297 | |
| 298 | test.provider( |
| 299 | "replace when hostname changes", |
| 300 | (stack) => |
| 301 | Effect.gen(function* () { |
| 302 | yield* stack.destroy(); |
| 303 | |
| 304 | const created = yield* stack.deploy( |
| 305 | Effect.gen(function* () { |
| 306 | const app = yield* Fly.App("CertReplaceApp"); |
| 307 | const cert = yield* Fly.Certificate("Www", { |
| 308 | app, |
| 309 | hostname: CUSTOM_HOSTNAME, |
| 310 | kind: "custom", |
| 311 | fullchain: CERT_1A, |
| 312 | privateKey: KEY_1A, |
| 313 | }); |
| 314 | return { app, cert }; |
| 315 | }), |
| 316 | ); |
| 317 | |
| 318 | expect(created.cert.hostname).toEqual(CUSTOM_HOSTNAME); |
| 319 | |
| 320 | const replaced = yield* stack.deploy( |
| 321 | Effect.gen(function* () { |
| 322 | const app = yield* Fly.App("CertReplaceApp"); |
| 323 | const cert = yield* Fly.Certificate("Www", { |
| 324 | app, |
| 325 | hostname: REPLACE_HOSTNAME, |
| 326 | kind: "custom", |
| 327 | fullchain: CERT_2, |
| 328 | privateKey: KEY_2, |
| 329 | }); |
| 330 | return { app, cert }; |
| 331 | }), |
| 332 | ); |
| 333 | |
| 334 | expect(replaced.cert.hostname).toEqual(REPLACE_HOSTNAME); |
| 335 | expect(replaced.cert.hostname).not.toEqual(created.cert.hostname); |
| 336 | expect(replaced.cert.source).toEqual("custom"); |
| 337 | expect(replaced.cert.appName).toEqual(created.app.appName); |
| 338 | |
| 339 | const fetched = yield* machines.getAppCertificate({ |
| 340 | app_name: replaced.app.appName, |
| 341 | hostname: REPLACE_HOSTNAME, |
| 342 | }); |
| 343 | expect(fetched.hostname).toEqual(REPLACE_HOSTNAME); |
| 344 | |
| 345 | const oldGone = yield* waitUntilCertGone( |
| 346 | created.app.appName, |
| 347 | CUSTOM_HOSTNAME, |
| 348 | ); |
| 349 | expect(oldGone).toEqual("gone"); |
| 350 | |
| 351 | yield* stack.destroy(); |
| 352 | |
| 353 | const certGone = yield* waitUntilCertGone( |
| 354 | replaced.app.appName, |
| 355 | REPLACE_HOSTNAME, |
| 356 | ); |
| 357 | expect(certGone).toEqual("gone"); |
| 358 | const appGone = yield* waitUntilAppGone(replaced.app.appName); |
| 359 | expect(appGone).toEqual("gone"); |
| 360 | }).pipe(logLevel), |
| 361 | { |
| 362 | tags: [ |
| 363 | "provider:fly", |
| 364 | "provider:fly:app", |
| 365 | "provider:fly:certificate", |
| 366 | "provider:fly:machine", |
| 367 | "live", |
| 368 | ], |
| 369 | timeout: 120_000, |
| 370 | }, |
| 371 | ); |
| 372 | |
| 373 | test.provider( |
| 374 | "list enumerates the deployed certificate", |
| 375 | (stack) => |
| 376 | Effect.gen(function* () { |
| 377 | yield* stack.destroy(); |
| 378 | |
| 379 | const deployed = yield* stack.deploy( |
| 380 | Effect.gen(function* () { |
| 381 | const app = yield* Fly.App("CertListApp"); |
| 382 | const cert = yield* Fly.Certificate("Www", { |
| 383 | app, |
| 384 | hostname: CUSTOM_HOSTNAME, |
| 385 | kind: "custom", |
| 386 | fullchain: CERT_1A, |
| 387 | privateKey: KEY_1A, |
| 388 | }); |
| 389 | return { app, cert }; |
| 390 | }), |
| 391 | ); |
| 392 | |
| 393 | const provider = yield* Provider.findProvider(Fly.Certificate); |
| 394 | const all = yield* provider.list(); |
| 395 | const found = all.find( |
| 396 | (row) => |
| 397 | row.appName === deployed.app.appName && |
| 398 | row.hostname === CUSTOM_HOSTNAME, |
| 399 | ); |
| 400 | expect(found).toBeDefined(); |
| 401 | expect(found?.source).toEqual("custom"); |
| 402 | |
| 403 | yield* stack.destroy(); |
| 404 | |
| 405 | const certGone = yield* waitUntilCertGone( |
| 406 | deployed.app.appName, |
| 407 | CUSTOM_HOSTNAME, |
| 408 | ); |
| 409 | expect(certGone).toEqual("gone"); |
| 410 | const appGone = yield* waitUntilAppGone(deployed.app.appName); |
| 411 | expect(appGone).toEqual("gone"); |
| 412 | }).pipe(logLevel), |
| 413 | { |
| 414 | tags: [ |
| 415 | "provider:fly", |
| 416 | "provider:fly:app", |
| 417 | "provider:fly:certificate", |
| 418 | "provider:fly:machine", |
| 419 | "live", |
| 420 | ], |
| 421 | timeout: 120_000, |
| 422 | }, |
| 423 | ); |
| 424 | |
| 425 | test.provider( |
| 426 | "ACME create is rejected without a hostname", |
| 427 | (stack) => |
| 428 | Effect.gen(function* () { |
| 429 | yield* stack.destroy(); |
| 430 | |
| 431 | const app = yield* stack.deploy( |
| 432 | Effect.gen(function* () { |
| 433 | return yield* Fly.App("AcmeProbeApp"); |
| 434 | }), |
| 435 | ); |
| 436 | |
| 437 | const result = yield* Effect.result( |
| 438 | machines.createAppAcmeCertificate({ |
| 439 | app_name: app.appName, |
| 440 | }), |
| 441 | ); |
| 442 | |
| 443 | if (Result.isFailure(result)) { |
| 444 | expect(["BadRequest", "UnprocessableEntity"]).toContain( |
| 445 | result.failure._tag, |
| 446 | ); |
| 447 | } else { |
| 448 | const hostname = result.success.hostname; |
| 449 | if (hostname !== undefined && hostname.length > 0) { |
| 450 | yield* deleteIfPresent(app.appName, hostname); |
| 451 | } |
| 452 | expect(result.success.hostname).toBeUndefined(); |
| 453 | } |
| 454 | |
| 455 | yield* stack.destroy(); |
| 456 | |
| 457 | const gone = yield* waitUntilAppGone(app.appName); |
| 458 | expect(gone).toEqual("gone"); |
| 459 | }).pipe(logLevel), |
| 460 | { |
| 461 | tags: [ |
| 462 | "provider:fly", |
| 463 | "provider:fly:app", |
| 464 | "provider:fly:certificate", |
| 465 | "provider:fly:machine", |
| 466 | "live", |
| 467 | ], |
| 468 | timeout: 120_000, |
| 469 | }, |
| 470 | ); |