@geekmidas/cli 10.0.0-alpha.11 → 10.0.0-alpha.110
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +315 -414
- package/bin/adjacent-tsconfig.mjs +46 -8
- package/bin/gkm.mjs +11 -3
- package/bin/owning-tsconfig-jsx-register.mjs +18 -0
- package/bin/owning-tsconfig-jsx.mjs +108 -0
- package/dist/EndpointGenerator-BwbYTMtB.cjs +1090 -0
- package/dist/EndpointGenerator-BwbYTMtB.cjs.map +1 -0
- package/dist/EndpointGenerator-CvLX8bpx.mjs +1031 -0
- package/dist/EndpointGenerator-CvLX8bpx.mjs.map +1 -0
- package/dist/GoDaddyProvider-CjeOduia.mjs +388 -0
- package/dist/GoDaddyProvider-CjeOduia.mjs.map +1 -0
- package/dist/GoDaddyProvider-qNUzTXI7.cjs +388 -0
- package/dist/GoDaddyProvider-qNUzTXI7.cjs.map +1 -0
- package/dist/{HostingerProvider-BcyFRbqV.mjs → HostingerProvider-BNpS9aRJ.mjs} +29 -25
- package/dist/HostingerProvider-BNpS9aRJ.mjs.map +1 -0
- package/dist/{HostingerProvider-BOvsgATO.cjs → HostingerProvider-DR1iws1P.cjs} +29 -25
- package/dist/HostingerProvider-DR1iws1P.cjs.map +1 -0
- package/dist/LegacyStateStore-MxiSu7KJ.cjs +124 -0
- package/dist/LegacyStateStore-MxiSu7KJ.cjs.map +1 -0
- package/dist/LegacyStateStore-pgscYsiG.mjs +124 -0
- package/dist/LegacyStateStore-pgscYsiG.mjs.map +1 -0
- package/dist/LocalStateStore-BcvyKbAw.mjs +167 -0
- package/dist/LocalStateStore-BcvyKbAw.mjs.map +1 -0
- package/dist/LocalStateStore-Bm8Vmwjb.cjs +178 -0
- package/dist/LocalStateStore-Bm8Vmwjb.cjs.map +1 -0
- package/dist/OpenApiTsGenerator-BD6ZOQ51.cjs +541 -0
- package/dist/OpenApiTsGenerator-BD6ZOQ51.cjs.map +1 -0
- package/dist/OpenApiTsGenerator-DXXFXSbO.mjs +536 -0
- package/dist/OpenApiTsGenerator-DXXFXSbO.mjs.map +1 -0
- package/dist/{Route53Provider-CH9XH7bz.cjs → Route53Provider-Bllus2NR.cjs} +21 -22
- package/dist/Route53Provider-Bllus2NR.cjs.map +1 -0
- package/dist/{Route53Provider-C0xmer8U.mjs → Route53Provider-QXxjkZO1.mjs} +13 -13
- package/dist/Route53Provider-QXxjkZO1.mjs.map +1 -0
- package/dist/S3StateStore-DXHSBVsV.cjs +222 -0
- package/dist/S3StateStore-DXHSBVsV.cjs.map +1 -0
- package/dist/S3StateStore-H2XsAzpJ.mjs +223 -0
- package/dist/S3StateStore-H2XsAzpJ.mjs.map +1 -0
- package/dist/SSMStateStore-BLPSNl-U.cjs +204 -0
- package/dist/SSMStateStore-BLPSNl-U.cjs.map +1 -0
- package/dist/SSMStateStore-CENciQEM.mjs +205 -0
- package/dist/SSMStateStore-CENciQEM.mjs.map +1 -0
- package/dist/SubscriberGenerator-CeH7wbYt.cjs +571 -0
- package/dist/SubscriberGenerator-CeH7wbYt.cjs.map +1 -0
- package/dist/SubscriberGenerator-DHnIYwUU.mjs +518 -0
- package/dist/SubscriberGenerator-DHnIYwUU.mjs.map +1 -0
- package/dist/add-Cdg1kEqN.cjs +719 -0
- package/dist/add-Cdg1kEqN.cjs.map +1 -0
- package/dist/add-snhdVmR-.mjs +717 -0
- package/dist/add-snhdVmR-.mjs.map +1 -0
- package/dist/appPorts-BoXMwolc.mjs +249 -0
- package/dist/appPorts-BoXMwolc.mjs.map +1 -0
- package/dist/appPorts-IW-OZmRi.cjs +296 -0
- package/dist/appPorts-IW-OZmRi.cjs.map +1 -0
- package/dist/aws-8QQ8n-t6.mjs +89 -0
- package/dist/aws-8QQ8n-t6.mjs.map +1 -0
- package/dist/aws-DY9NqQIT.cjs +100 -0
- package/dist/aws-DY9NqQIT.cjs.map +1 -0
- package/dist/awsStore-Bx5nCFnS.mjs +121 -0
- package/dist/awsStore-Bx5nCFnS.mjs.map +1 -0
- package/dist/awsStore-u3ehhMoY.cjs +138 -0
- package/dist/awsStore-u3ehhMoY.cjs.map +1 -0
- package/dist/{backup-provisioner-DXXg6FIL.mjs → backup-provisioner-C8DnCmxK.mjs} +11 -8
- package/dist/backup-provisioner-C8DnCmxK.mjs.map +1 -0
- package/dist/{backup-provisioner-cE2PuCTb.cjs → backup-provisioner-oDEsPj02.cjs} +22 -20
- package/dist/backup-provisioner-oDEsPj02.cjs.map +1 -0
- package/dist/backups/runner.cjs +519 -0
- package/dist/backups/runner.cjs.map +1 -0
- package/dist/backups/runner.d.cts +102 -0
- package/dist/backups/runner.d.cts.map +1 -0
- package/dist/backups/runner.d.mts +102 -0
- package/dist/backups/runner.d.mts.map +1 -0
- package/dist/backups/runner.mjs +506 -0
- package/dist/backups/runner.mjs.map +1 -0
- package/dist/{bundler-CaTGtiaO.cjs → bundler-B4GDxImV.cjs} +38 -35
- package/dist/bundler-B4GDxImV.cjs.map +1 -0
- package/dist/{bundler-S3ek7ea-.mjs → bundler-Wd9H4QLJ.mjs} +36 -32
- package/dist/bundler-Wd9H4QLJ.mjs.map +1 -0
- package/dist/cli-BZYz55tO.cjs +71 -0
- package/dist/cli-BZYz55tO.cjs.map +1 -0
- package/dist/cli-BzrDSNSg.mjs +71 -0
- package/dist/cli-BzrDSNSg.mjs.map +1 -0
- package/dist/compose-DN9zySvQ.cjs +3330 -0
- package/dist/compose-DN9zySvQ.cjs.map +1 -0
- package/dist/compose-Gsd1Nq7R.mjs +3187 -0
- package/dist/compose-Gsd1Nq7R.mjs.map +1 -0
- package/dist/config-D48erO2j.d.mts +269 -0
- package/dist/config-D48erO2j.d.mts.map +1 -0
- package/dist/config-DHDtsQrB.d.cts +269 -0
- package/dist/config-DHDtsQrB.d.cts.map +1 -0
- package/dist/config.cjs +421 -11
- package/dist/config.cjs.map +1 -0
- package/dist/config.d.cts +4 -120
- package/dist/config.d.mts +4 -120
- package/dist/config.mjs +405 -4
- package/dist/config.mjs.map +1 -0
- package/dist/credentialSchemas-Cf5Pf8VE.mjs +237 -0
- package/dist/credentialSchemas-Cf5Pf8VE.mjs.map +1 -0
- package/dist/credentialSchemas-D_VUCDUh.cjs +272 -0
- package/dist/credentialSchemas-D_VUCDUh.cjs.map +1 -0
- package/dist/credentials-B5N5yKk6.mjs +267 -0
- package/dist/credentials-B5N5yKk6.mjs.map +1 -0
- package/dist/credentials-Zqr3QQYB.cjs +350 -0
- package/dist/credentials-Zqr3QQYB.cjs.map +1 -0
- package/dist/databases-BeGP5eDh.mjs +495 -0
- package/dist/databases-BeGP5eDh.mjs.map +1 -0
- package/dist/databases-DGALFDG3.cjs +598 -0
- package/dist/databases-DGALFDG3.cjs.map +1 -0
- package/dist/define-6poNqm5O.mjs +106 -0
- package/dist/define-6poNqm5O.mjs.map +1 -0
- package/dist/define-crmMcXFS.cjs +153 -0
- package/dist/define-crmMcXFS.cjs.map +1 -0
- package/dist/deploy/public.cjs +212 -0
- package/dist/deploy/public.cjs.map +1 -0
- package/dist/deploy/public.d.cts +653 -0
- package/dist/deploy/public.d.cts.map +1 -0
- package/dist/deploy/public.d.mts +653 -0
- package/dist/deploy/public.d.mts.map +1 -0
- package/dist/deploy/public.mjs +145 -0
- package/dist/deploy/public.mjs.map +1 -0
- package/dist/deploy/sniffer-envkit-patch.cjs +18 -10
- package/dist/deploy/sniffer-envkit-patch.cjs.map +1 -1
- package/dist/deploy/sniffer-envkit-patch.d.cts +0 -2
- package/dist/deploy/sniffer-envkit-patch.d.cts.map +1 -1
- package/dist/deploy/sniffer-envkit-patch.d.mts +0 -2
- package/dist/deploy/sniffer-envkit-patch.d.mts.map +1 -1
- package/dist/deploy/sniffer-envkit-patch.mjs +10 -2
- package/dist/deploy/sniffer-envkit-patch.mjs.map +1 -1
- package/dist/deploy/sniffer-envparser-worker.cjs +54 -0
- package/dist/deploy/sniffer-envparser-worker.cjs.map +1 -0
- package/dist/deploy/sniffer-envparser-worker.d.cts +1 -0
- package/dist/deploy/sniffer-envparser-worker.d.mts +1 -0
- package/dist/deploy/sniffer-envparser-worker.mjs +55 -0
- package/dist/deploy/sniffer-envparser-worker.mjs.map +1 -0
- package/dist/deploy/sniffer-hooks.cjs +13 -7
- package/dist/deploy/sniffer-hooks.cjs.map +1 -1
- package/dist/deploy/sniffer-hooks.d.cts +1 -2
- package/dist/deploy/sniffer-hooks.d.cts.map +1 -1
- package/dist/deploy/sniffer-hooks.d.mts +1 -2
- package/dist/deploy/sniffer-hooks.d.mts.map +1 -1
- package/dist/deploy/sniffer-hooks.mjs +10 -3
- package/dist/deploy/sniffer-hooks.mjs.map +1 -1
- package/dist/deploy/sniffer-loader.cjs +16 -7
- package/dist/deploy/sniffer-loader.cjs.map +1 -1
- package/dist/deploy/sniffer-loader.d.cts +1 -1
- package/dist/deploy/sniffer-loader.d.mts +1 -1
- package/dist/deploy/sniffer-loader.mjs +14 -3
- package/dist/deploy/sniffer-loader.mjs.map +1 -1
- package/dist/deploy/sniffer-routes-worker.cjs +11 -11
- package/dist/deploy/sniffer-routes-worker.cjs.map +1 -1
- package/dist/deploy/sniffer-routes-worker.d.cts +1 -1
- package/dist/deploy/sniffer-routes-worker.d.mts +1 -1
- package/dist/deploy/sniffer-routes-worker.mjs +4 -4
- package/dist/deploy/sniffer-routes-worker.mjs.map +1 -1
- package/dist/deploy/sniffer-worker.cjs +15 -6
- package/dist/deploy/sniffer-worker.cjs.map +1 -1
- package/dist/deploy/sniffer-worker.d.cts +1 -3
- package/dist/deploy/sniffer-worker.d.cts.map +1 -1
- package/dist/deploy/sniffer-worker.d.mts +1 -3
- package/dist/deploy/sniffer-worker.d.mts.map +1 -1
- package/dist/deploy/sniffer-worker.mjs +15 -4
- package/dist/deploy/sniffer-worker.mjs.map +1 -1
- package/dist/deploy-CFvq8daZ.cjs +2390 -0
- package/dist/deploy-CFvq8daZ.cjs.map +1 -0
- package/dist/deploy-H2S8_mTx.mjs +2317 -0
- package/dist/deploy-H2S8_mTx.mjs.map +1 -0
- package/dist/derive-B4vDpzgP.mjs +150 -0
- package/dist/derive-B4vDpzgP.mjs.map +1 -0
- package/dist/derive-DAbo49sm.cjs +161 -0
- package/dist/derive-DAbo49sm.cjs.map +1 -0
- package/dist/discover-Bsr7HjqJ.cjs +616 -0
- package/dist/discover-Bsr7HjqJ.cjs.map +1 -0
- package/dist/discover-Ca8U75dl.mjs +537 -0
- package/dist/discover-Ca8U75dl.mjs.map +1 -0
- package/dist/discover-CvTtV5ZD.d.cts +108 -0
- package/dist/discover-CvTtV5ZD.d.cts.map +1 -0
- package/dist/discover-W2kab9Lc.d.mts +108 -0
- package/dist/discover-W2kab9Lc.d.mts.map +1 -0
- package/dist/dns-BdETzP10.cjs +1057 -0
- package/dist/dns-BdETzP10.cjs.map +1 -0
- package/dist/dns-pFMShDqW.mjs +926 -0
- package/dist/dns-pFMShDqW.mjs.map +1 -0
- package/dist/dokploy-HioAjP7K.cjs +38 -0
- package/dist/dokploy-HioAjP7K.cjs.map +1 -0
- package/dist/dokploy-oDJbnvCb.mjs +38 -0
- package/dist/dokploy-oDJbnvCb.mjs.map +1 -0
- package/dist/drivers-D11JHxrD.mjs +177 -0
- package/dist/drivers-D11JHxrD.mjs.map +1 -0
- package/dist/drivers-GKaA6ITM.cjs +200 -0
- package/dist/drivers-GKaA6ITM.cjs.map +1 -0
- package/dist/{emit-CIlvnDvV.mjs → emit-C1e8qD08.mjs} +13 -26
- package/dist/emit-C1e8qD08.mjs.map +1 -0
- package/dist/{emit-CUD1BWls.cjs → emit-Dmc1nvdS.cjs} +31 -57
- package/dist/emit-Dmc1nvdS.cjs.map +1 -0
- package/dist/encryption-D5pDJyp2.mjs +89 -0
- package/dist/encryption-D5pDJyp2.mjs.map +1 -0
- package/dist/encryption-DVCxD-gV.cjs +90 -0
- package/dist/encryption-DVCxD-gV.cjs.map +1 -0
- package/dist/engine-BaJGPzte.cjs +3196 -0
- package/dist/engine-BaJGPzte.cjs.map +1 -0
- package/dist/engine-CfAKgr2_.mjs +3083 -0
- package/dist/engine-CfAKgr2_.mjs.map +1 -0
- package/dist/errors-D8Gwp5kU.mjs +23 -0
- package/dist/errors-D8Gwp5kU.mjs.map +1 -0
- package/dist/errors-DMC4Umsb.cjs +34 -0
- package/dist/errors-DMC4Umsb.cjs.map +1 -0
- package/dist/github-D902Vwr9.cjs +829 -0
- package/dist/github-D902Vwr9.cjs.map +1 -0
- package/dist/github-E-ZxqFPq.mjs +829 -0
- package/dist/github-E-ZxqFPq.mjs.map +1 -0
- package/dist/identity-CBXr4wNi.mjs +129 -0
- package/dist/identity-CBXr4wNi.mjs.map +1 -0
- package/dist/identity-CbLfgQNT.cjs +182 -0
- package/dist/identity-CbLfgQNT.cjs.map +1 -0
- package/dist/{index-Ayxg_DQY.d.mts → index-8OfpOtSI.d.cts} +39 -24
- package/dist/index-8OfpOtSI.d.cts.map +1 -0
- package/dist/{index-CR5urhWX.d.cts → index-Dpji6Bxq.d.mts} +39 -24
- package/dist/index-Dpji6Bxq.d.mts.map +1 -0
- package/dist/index.cjs +13764 -12651
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +13654 -12553
- package/dist/index.mjs.map +1 -1
- package/dist/local-BwC2v2Y8.mjs +224 -0
- package/dist/local-BwC2v2Y8.mjs.map +1 -0
- package/dist/local-DNhnkugT.cjs +241 -0
- package/dist/local-DNhnkugT.cjs.map +1 -0
- package/dist/migrate-BZ0GnSZQ.mjs +181 -0
- package/dist/migrate-BZ0GnSZQ.mjs.map +1 -0
- package/dist/migrate-CTqhWtev.cjs +271 -0
- package/dist/migrate-CTqhWtev.cjs.map +1 -0
- package/dist/migrate-DCXwHcWX.mjs +266 -0
- package/dist/migrate-DCXwHcWX.mjs.map +1 -0
- package/dist/migrate-Df81ew0k.cjs +181 -0
- package/dist/migrate-Df81ew0k.cjs.map +1 -0
- package/dist/openapi.cjs +214 -10
- package/dist/openapi.cjs.map +1 -0
- package/dist/openapi.d.cts +38 -17
- package/dist/openapi.d.cts.map +1 -1
- package/dist/openapi.d.mts +38 -17
- package/dist/openapi.d.mts.map +1 -1
- package/dist/openapi.mjs +208 -5
- package/dist/openapi.mjs.map +1 -0
- package/dist/projectBucket-BBbQ1lHF.cjs +263 -0
- package/dist/projectBucket-BBbQ1lHF.cjs.map +1 -0
- package/dist/projectBucket-C_DN4WzF.mjs +222 -0
- package/dist/projectBucket-C_DN4WzF.mjs.map +1 -0
- package/dist/provider-BqpjG9QM.cjs +57 -0
- package/dist/provider-BqpjG9QM.cjs.map +1 -0
- package/dist/provider-CEOKk8NS.mjs +46 -0
- package/dist/provider-CEOKk8NS.mjs.map +1 -0
- package/dist/ready-BMFjxBYA.cjs +404 -0
- package/dist/ready-BMFjxBYA.cjs.map +1 -0
- package/dist/ready-CIt5SOtg.mjs +375 -0
- package/dist/ready-CIt5SOtg.mjs.map +1 -0
- package/dist/reconcile/public.cjs +3 -5
- package/dist/reconcile/public.d.cts +11 -87
- package/dist/reconcile/public.d.cts.map +1 -1
- package/dist/reconcile/public.d.mts +11 -87
- package/dist/reconcile/public.d.mts.map +1 -1
- package/dist/reconcile/public.mjs +3 -4
- package/dist/{reconcile-C13ERt1U.mjs → reconcile--I7V4Klj.mjs} +4 -7
- package/dist/reconcile--I7V4Klj.mjs.map +1 -0
- package/dist/{reconcile-CYMhFSYy.cjs → reconcile-CuCQBa5-.cjs} +5 -8
- package/dist/reconcile-CuCQBa5-.cjs.map +1 -0
- package/dist/resolve-BMpPd8x3.mjs +263 -0
- package/dist/resolve-BMpPd8x3.mjs.map +1 -0
- package/dist/resolve-D043YmW9.cjs +316 -0
- package/dist/resolve-D043YmW9.cjs.map +1 -0
- package/dist/rollback-CBiYsQ3T.cjs +165 -0
- package/dist/rollback-CBiYsQ3T.cjs.map +1 -0
- package/dist/rollback-CDs_XNb0.mjs +136 -0
- package/dist/rollback-CDs_XNb0.mjs.map +1 -0
- package/dist/rolldown-runtime-BhDjJH2R.mjs +15 -0
- package/dist/{chunk-CUT6urMc.cjs → rolldown-runtime-C0BPl7ul.cjs} +23 -10
- package/dist/run-BJLIerMM.mjs +133 -0
- package/dist/run-BJLIerMM.mjs.map +1 -0
- package/dist/run-fVZ-rLnc.cjs +174 -0
- package/dist/run-fVZ-rLnc.cjs.map +1 -0
- package/dist/runtime-ACdfK59n.d.cts +82 -0
- package/dist/runtime-ACdfK59n.d.cts.map +1 -0
- package/dist/runtime-YyQuekxi.d.mts +82 -0
- package/dist/runtime-YyQuekxi.d.mts.map +1 -0
- package/dist/s3-CrPoA89T.cjs +215 -0
- package/dist/s3-CrPoA89T.cjs.map +1 -0
- package/dist/s3-DQXZdmCM.mjs +215 -0
- package/dist/s3-DQXZdmCM.mjs.map +1 -0
- package/dist/sandbox/config-worker.cjs +43 -0
- package/dist/sandbox/config-worker.cjs.map +1 -0
- package/dist/sandbox/config-worker.d.cts +1 -0
- package/dist/sandbox/config-worker.d.mts +1 -0
- package/dist/sandbox/config-worker.mjs +44 -0
- package/dist/sandbox/config-worker.mjs.map +1 -0
- package/dist/sandbox/credentials-worker.cjs +54 -0
- package/dist/sandbox/credentials-worker.cjs.map +1 -0
- package/dist/sandbox/credentials-worker.d.cts +1 -0
- package/dist/sandbox/credentials-worker.d.mts +1 -0
- package/dist/sandbox/credentials-worker.mjs +55 -0
- package/dist/sandbox/credentials-worker.mjs.map +1 -0
- package/dist/sandbox/discover-worker.cjs +47 -0
- package/dist/sandbox/discover-worker.cjs.map +1 -0
- package/dist/sandbox/discover-worker.d.cts +1 -0
- package/dist/sandbox/discover-worker.d.mts +1 -0
- package/dist/sandbox/discover-worker.mjs +48 -0
- package/dist/sandbox/discover-worker.mjs.map +1 -0
- package/dist/sandbox/migrate-worker.cjs +75 -0
- package/dist/sandbox/migrate-worker.cjs.map +1 -0
- package/dist/sandbox/migrate-worker.d.cts +1 -0
- package/dist/sandbox/migrate-worker.d.mts +1 -0
- package/dist/sandbox/migrate-worker.mjs +76 -0
- package/dist/sandbox/migrate-worker.mjs.map +1 -0
- package/dist/sandbox-C4g1eG31.d.mts +104 -0
- package/dist/sandbox-C4g1eG31.d.mts.map +1 -0
- package/dist/sandbox-CHv3OYXg.cjs +165 -0
- package/dist/sandbox-CHv3OYXg.cjs.map +1 -0
- package/dist/sandbox-CbVB7uOD.mjs +118 -0
- package/dist/sandbox-CbVB7uOD.mjs.map +1 -0
- package/dist/sandbox-CdKgQ3XU.d.cts +104 -0
- package/dist/sandbox-CdKgQ3XU.d.cts.map +1 -0
- package/dist/{workspace-C-a3G_U0.cjs → schemas-BCwKKA42.mjs} +958 -1868
- package/dist/schemas-BCwKKA42.mjs.map +1 -0
- package/dist/{workspace-DaEX7FEg.mjs → schemas-BQ_v0lEX.cjs} +1048 -1678
- package/dist/schemas-BQ_v0lEX.cjs.map +1 -0
- package/dist/secretsManager-CQORubp9.mjs +97 -0
- package/dist/secretsManager-CQORubp9.mjs.map +1 -0
- package/dist/secretsManager-RAGcEDX1.cjs +108 -0
- package/dist/secretsManager-RAGcEDX1.cjs.map +1 -0
- package/dist/seeds-BjcO4DDa.mjs +8194 -0
- package/dist/seeds-BjcO4DDa.mjs.map +1 -0
- package/dist/seeds-DNqRA1aQ.cjs +9173 -0
- package/dist/seeds-DNqRA1aQ.cjs.map +1 -0
- package/dist/sniffer-DipSS8r8.cjs +1998 -0
- package/dist/sniffer-DipSS8r8.cjs.map +1 -0
- package/dist/sniffer-b0XPmSyT.mjs +1825 -0
- package/dist/sniffer-b0XPmSyT.mjs.map +1 -0
- package/dist/sst-B-Uz4cvm.cjs +464 -0
- package/dist/sst-B-Uz4cvm.cjs.map +1 -0
- package/dist/sst-C1pxbFEd.mjs +441 -0
- package/dist/sst-C1pxbFEd.mjs.map +1 -0
- package/dist/stage-C_y2UovO.cjs +266 -0
- package/dist/stage-C_y2UovO.cjs.map +1 -0
- package/dist/stage-DrDmZusd.mjs +266 -0
- package/dist/stage-DrDmZusd.mjs.map +1 -0
- package/dist/stages-DJrOYLZ-.mjs +176 -0
- package/dist/stages-DJrOYLZ-.mjs.map +1 -0
- package/dist/stages-_3RVNG83.cjs +176 -0
- package/dist/stages-_3RVNG83.cjs.map +1 -0
- package/dist/target/public.cjs +15 -0
- package/dist/target/public.d.cts +28 -0
- package/dist/target/public.d.cts.map +1 -0
- package/dist/target/public.d.mts +28 -0
- package/dist/target/public.d.mts.map +1 -0
- package/dist/target/public.mjs +4 -0
- package/dist/terminal-CvRLxz6g.mjs +85 -0
- package/dist/terminal-CvRLxz6g.mjs.map +1 -0
- package/dist/terminal-D_NRPW4W.cjs +85 -0
- package/dist/terminal-D_NRPW4W.cjs.map +1 -0
- package/dist/types-B6UXlEkf.d.cts +7650 -0
- package/dist/types-B6UXlEkf.d.cts.map +1 -0
- package/dist/types-HqVPddRK.d.mts +7649 -0
- package/dist/types-HqVPddRK.d.mts.map +1 -0
- package/dist/utils-D2Nk1Nv2.cjs +205 -0
- package/dist/utils-D2Nk1Nv2.cjs.map +1 -0
- package/dist/utils-D_fxUqvU.mjs +156 -0
- package/dist/utils-D_fxUqvU.mjs.map +1 -0
- package/dist/vitest.cjs +65 -0
- package/dist/vitest.cjs.map +1 -0
- package/dist/vitest.d.cts +29 -0
- package/dist/vitest.d.cts.map +1 -0
- package/dist/vitest.d.mts +29 -0
- package/dist/vitest.d.mts.map +1 -0
- package/dist/vitest.mjs +65 -0
- package/dist/vitest.mjs.map +1 -0
- package/dist/workerRuntime-BXfhbe13.cjs +108 -0
- package/dist/workerRuntime-BXfhbe13.cjs.map +1 -0
- package/dist/workerRuntime-CsOyBRx2.mjs +79 -0
- package/dist/workerRuntime-CsOyBRx2.mjs.map +1 -0
- package/dist/workspace/index.cjs +8 -4
- package/dist/workspace/index.d.cts +3 -3
- package/dist/workspace/index.d.mts +3 -3
- package/dist/workspace/index.mjs +2 -3
- package/dist/workspace-BwqQnool.cjs +2806 -0
- package/dist/workspace-BwqQnool.cjs.map +1 -0
- package/dist/workspace-D1QbNd55.mjs +2394 -0
- package/dist/workspace-D1QbNd55.mjs.map +1 -0
- package/package.json +54 -22
- package/dist/CachedStateProvider-BEBl2wQT.cjs +0 -60
- package/dist/CachedStateProvider-BEBl2wQT.cjs.map +0 -1
- package/dist/CachedStateProvider-C-8wtlto.mjs +0 -3
- package/dist/CachedStateProvider-Co4n5Wvs.mjs +0 -54
- package/dist/CachedStateProvider-Co4n5Wvs.mjs.map +0 -1
- package/dist/CachedStateProvider-DqhGpzsm.cjs +0 -3
- package/dist/HostingerProvider-BOvsgATO.cjs.map +0 -1
- package/dist/HostingerProvider-BcyFRbqV.mjs.map +0 -1
- package/dist/LocalStateProvider-Bq-0C0sg.mjs +0 -42
- package/dist/LocalStateProvider-Bq-0C0sg.mjs.map +0 -1
- package/dist/LocalStateProvider-yjXZXZDM.cjs +0 -43
- package/dist/LocalStateProvider-yjXZXZDM.cjs.map +0 -1
- package/dist/Route53Provider-C0xmer8U.mjs.map +0 -1
- package/dist/Route53Provider-CH9XH7bz.cjs.map +0 -1
- package/dist/SSMStateProvider-CXtNgPfy.cjs +0 -66
- package/dist/SSMStateProvider-CXtNgPfy.cjs.map +0 -1
- package/dist/SSMStateProvider-DOZifZfE.mjs +0 -66
- package/dist/SSMStateProvider-DOZifZfE.mjs.map +0 -1
- package/dist/backup-provisioner-DXXg6FIL.mjs.map +0 -1
- package/dist/backup-provisioner-cE2PuCTb.cjs.map +0 -1
- package/dist/bundler-CaTGtiaO.cjs.map +0 -1
- package/dist/bundler-S3ek7ea-.mjs.map +0 -1
- package/dist/chunk-Duj1WY3L.mjs +0 -7
- package/dist/config-B0L2tvvC.mjs +0 -402
- package/dist/config-B0L2tvvC.mjs.map +0 -1
- package/dist/config-HhDpJmLO.cjs +0 -462
- package/dist/config-HhDpJmLO.cjs.map +0 -1
- package/dist/config.d.cts.map +0 -1
- package/dist/config.d.mts.map +0 -1
- package/dist/credentials-B2Pyh3bJ.cjs +0 -2681
- package/dist/credentials-B2Pyh3bJ.cjs.map +0 -1
- package/dist/credentials-B5zRHdAl.mjs +0 -141
- package/dist/credentials-B5zRHdAl.mjs.map +0 -1
- package/dist/credentials-BapzjyV5.cjs +0 -195
- package/dist/credentials-BapzjyV5.cjs.map +0 -1
- package/dist/credentials-CZrys9C5.mjs +0 -2549
- package/dist/credentials-CZrys9C5.mjs.map +0 -1
- package/dist/discover-CAlFgYec.mjs +0 -229
- package/dist/discover-CAlFgYec.mjs.map +0 -1
- package/dist/discover-DTmFeloL.cjs +0 -271
- package/dist/discover-DTmFeloL.cjs.map +0 -1
- package/dist/dokploy-api-5cK2InBm.cjs +0 -3
- package/dist/dokploy-api-D2Ny4Jrc.mjs +0 -3
- package/dist/dokploy-api-pdaBryoY.mjs +0 -706
- package/dist/dokploy-api-pdaBryoY.mjs.map +0 -1
- package/dist/dokploy-api-zqKe9lHH.cjs +0 -718
- package/dist/dokploy-api-zqKe9lHH.cjs.map +0 -1
- package/dist/emit-CIlvnDvV.mjs.map +0 -1
- package/dist/emit-CUD1BWls.cjs.map +0 -1
- package/dist/encryption-D2pL3tHH.cjs +0 -54
- package/dist/encryption-D2pL3tHH.cjs.map +0 -1
- package/dist/encryption-YDsXXi6w.mjs +0 -3
- package/dist/encryption-_4zwjOAR.mjs +0 -42
- package/dist/encryption-_4zwjOAR.mjs.map +0 -1
- package/dist/encryption-sQ2Oxx0d.cjs +0 -4
- package/dist/fullstack-secrets-D6MyHhQP.cjs +0 -111
- package/dist/fullstack-secrets-D6MyHhQP.cjs.map +0 -1
- package/dist/fullstack-secrets-METaRLS1.mjs +0 -87
- package/dist/fullstack-secrets-METaRLS1.mjs.map +0 -1
- package/dist/generator-CNzBw7g5.cjs +0 -289
- package/dist/generator-CNzBw7g5.cjs.map +0 -1
- package/dist/generator-VE2JrFlY.mjs +0 -253
- package/dist/generator-VE2JrFlY.mjs.map +0 -1
- package/dist/github-BrvaPkFf.cjs +0 -197
- package/dist/github-BrvaPkFf.cjs.map +0 -1
- package/dist/github-CrpsXSfV.mjs +0 -196
- package/dist/github-CrpsXSfV.mjs.map +0 -1
- package/dist/index-Ayxg_DQY.d.mts.map +0 -1
- package/dist/index-CR5urhWX.d.cts.map +0 -1
- package/dist/keystore-C9rint2E.cjs +0 -101
- package/dist/keystore-C9rint2E.cjs.map +0 -1
- package/dist/keystore-TxeYM7an.mjs +0 -83
- package/dist/keystore-TxeYM7an.mjs.map +0 -1
- package/dist/openapi-DaKlTKxz.cjs +0 -2075
- package/dist/openapi-DaKlTKxz.cjs.map +0 -1
- package/dist/openapi-react-query-BKH334Ug.cjs +0 -173
- package/dist/openapi-react-query-BKH334Ug.cjs.map +0 -1
- package/dist/openapi-react-query-DoR5CmzQ.mjs +0 -167
- package/dist/openapi-react-query-DoR5CmzQ.mjs.map +0 -1
- package/dist/openapi-react-query.cjs +0 -4
- package/dist/openapi-react-query.d.cts +0 -11
- package/dist/openapi-react-query.d.cts.map +0 -1
- package/dist/openapi-react-query.d.mts +0 -11
- package/dist/openapi-react-query.d.mts.map +0 -1
- package/dist/openapi-react-query.mjs +0 -4
- package/dist/openapi-y1d7Kalt.mjs +0 -2021
- package/dist/openapi-y1d7Kalt.mjs.map +0 -1
- package/dist/reconcile-C13ERt1U.mjs.map +0 -1
- package/dist/reconcile-CYMhFSYy.cjs.map +0 -1
- package/dist/setup-BWgMs4gg.mjs +0 -12
- package/dist/setup-DOv92pIu.cjs +0 -473
- package/dist/setup-DOv92pIu.cjs.map +0 -1
- package/dist/setup-DQo_h_yK.mjs +0 -443
- package/dist/setup-DQo_h_yK.mjs.map +0 -1
- package/dist/setup-lCx_DcUo.cjs +0 -12
- package/dist/storage-B020InPW.mjs +0 -222
- package/dist/storage-B020InPW.mjs.map +0 -1
- package/dist/storage-CSNplaJn.cjs +0 -282
- package/dist/storage-CSNplaJn.cjs.map +0 -1
- package/dist/storage-DGBzMLZa.mjs +0 -4
- package/dist/storage-DlVol_oj.cjs +0 -8
- package/dist/sync-0ILCb-1S.cjs +0 -93
- package/dist/sync-0ILCb-1S.cjs.map +0 -1
- package/dist/sync-CzHXiR9c.mjs +0 -76
- package/dist/sync-CzHXiR9c.mjs.map +0 -1
- package/dist/sync-DYENfOt4.cjs +0 -4
- package/dist/sync-HAau6h_R.mjs +0 -3
- package/dist/types-CL7jPFM2.d.mts +0 -2220
- package/dist/types-CL7jPFM2.d.mts.map +0 -1
- package/dist/types-Y3fKQSAK.d.cts +0 -2220
- package/dist/types-Y3fKQSAK.d.cts.map +0 -1
- package/dist/workspace-C-a3G_U0.cjs.map +0 -1
- package/dist/workspace-DQxxs7Jk.cjs +0 -10
- package/dist/workspace-DaEX7FEg.mjs.map +0 -1
- package/dist/workspace-DbI580qS.mjs +0 -9
|
@@ -0,0 +1,2390 @@
|
|
|
1
|
+
const require_rolldown_runtime = require("./rolldown-runtime-C0BPl7ul.cjs");
|
|
2
|
+
const require_errors = require("./errors-DMC4Umsb.cjs");
|
|
3
|
+
const require_run = require("./run-fVZ-rLnc.cjs");
|
|
4
|
+
const require_credentials = require("./credentials-Zqr3QQYB.cjs");
|
|
5
|
+
const require_workspace = require("./workspace-BwqQnool.cjs");
|
|
6
|
+
const require_dns = require("./dns-BdETzP10.cjs");
|
|
7
|
+
const require_define = require("./define-crmMcXFS.cjs");
|
|
8
|
+
const require_seeds = require("./seeds-DNqRA1aQ.cjs");
|
|
9
|
+
const require_derive = require("./derive-DAbo49sm.cjs");
|
|
10
|
+
const require_identity = require("./identity-CbLfgQNT.cjs");
|
|
11
|
+
const require_discover = require("./discover-Bsr7HjqJ.cjs");
|
|
12
|
+
const require_sandbox = require("./sandbox-CHv3OYXg.cjs");
|
|
13
|
+
const require_config = require("./config.cjs");
|
|
14
|
+
const require_resolve = require("./resolve-D043YmW9.cjs");
|
|
15
|
+
const require_local = require("./local-DNhnkugT.cjs");
|
|
16
|
+
const require_sniffer = require("./sniffer-DipSS8r8.cjs");
|
|
17
|
+
const require_projectBucket = require("./projectBucket-BBbQ1lHF.cjs");
|
|
18
|
+
const require_credentialSchemas = require("./credentialSchemas-D_VUCDUh.cjs");
|
|
19
|
+
let node_path = require("node:path");
|
|
20
|
+
let node_crypto = require("node:crypto");
|
|
21
|
+
let _geekmidas_manifest = require("@geekmidas/manifest");
|
|
22
|
+
let _geekmidas_storage_s3_url = require("@geekmidas/storage/s3-url");
|
|
23
|
+
_geekmidas_storage_s3_url = require_rolldown_runtime.__toESM(_geekmidas_storage_s3_url, 1);
|
|
24
|
+
//#region src/providers/aws.ts
|
|
25
|
+
/**
|
|
26
|
+
* The AWS account a provider provisions in — the stage's, found the way the
|
|
27
|
+
* SSM and Secrets Manager stores find it.
|
|
28
|
+
*
|
|
29
|
+
* 1. `--profile`, resolved on its own: exported keys left over from another
|
|
30
|
+
* account never win over a profile someone named.
|
|
31
|
+
* 2. `AWS_PROFILE`, else `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY` — the
|
|
32
|
+
* deploy's own `aws` credential kind (`storedCredentials`).
|
|
33
|
+
* 3. The SDK's default chain: `~/.aws` default profile, SSO, a role. In CI,
|
|
34
|
+
* whatever `aws-actions/configure-aws-credentials` exported.
|
|
35
|
+
*
|
|
36
|
+
* None of them: the provider has no provisioning credentials, and the stage
|
|
37
|
+
* is treated as `external`.
|
|
38
|
+
*/
|
|
39
|
+
async function awsProvisioningCredentials(lookup) {
|
|
40
|
+
const region = lookup.env.AWS_REGION ?? lookup.env.AWS_DEFAULT_REGION;
|
|
41
|
+
const where = region ? { region } : {};
|
|
42
|
+
if (lookup.profile) return {
|
|
43
|
+
profile: lookup.profile,
|
|
44
|
+
...where
|
|
45
|
+
};
|
|
46
|
+
const stored = await require_credentials.storedCredentials({
|
|
47
|
+
env: lookup.env,
|
|
48
|
+
...lookup.home ? { home: lookup.home } : {}
|
|
49
|
+
}).get({
|
|
50
|
+
kind: "aws",
|
|
51
|
+
stage: lookup.stage
|
|
52
|
+
});
|
|
53
|
+
if (stored) return stored;
|
|
54
|
+
try {
|
|
55
|
+
const { fromNodeProviderChain } = await import("@aws-sdk/credential-providers");
|
|
56
|
+
await fromNodeProviderChain({
|
|
57
|
+
timeout: 1e3,
|
|
58
|
+
maxRetries: 0
|
|
59
|
+
})();
|
|
60
|
+
return {
|
|
61
|
+
chain: true,
|
|
62
|
+
...where
|
|
63
|
+
};
|
|
64
|
+
} catch {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* An S3-compatible endpoint the environment points the SDK at — the AWS
|
|
70
|
+
* emulator in tests. The SDK reads it itself; it is read here too, because
|
|
71
|
+
* an emulator takes path-style requests, and a key written for one has to
|
|
72
|
+
* say where it is.
|
|
73
|
+
*/
|
|
74
|
+
function awsEndpoint(env, service) {
|
|
75
|
+
return env[`AWS_ENDPOINT_URL_${service}`] ?? env.AWS_ENDPOINT_URL;
|
|
76
|
+
}
|
|
77
|
+
/** An SDK client's config for a provisioning credential. */
|
|
78
|
+
async function awsClientConfig(credential, region) {
|
|
79
|
+
if ("profile" in credential) {
|
|
80
|
+
const { fromIni } = await import("@aws-sdk/credential-providers");
|
|
81
|
+
return {
|
|
82
|
+
region,
|
|
83
|
+
credentials: fromIni({ profile: credential.profile })
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
if ("accessKeyId" in credential) return {
|
|
87
|
+
region,
|
|
88
|
+
credentials: {
|
|
89
|
+
accessKeyId: credential.accessKeyId,
|
|
90
|
+
secretAccessKey: credential.secretAccessKey,
|
|
91
|
+
...credential.sessionToken ? { sessionToken: credential.sessionToken } : {}
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
return { region };
|
|
95
|
+
}
|
|
96
|
+
//#endregion
|
|
97
|
+
//#region src/target/secrets.ts
|
|
98
|
+
/**
|
|
99
|
+
* A stage's secrets as a target sees them: read and written through the
|
|
100
|
+
* stage's own store, and masked in everything the run prints once read.
|
|
101
|
+
*/
|
|
102
|
+
/**
|
|
103
|
+
* Shorter values are left alone: what a progress line legitimately prints —
|
|
104
|
+
* a port, a stage, an app name — would otherwise come out as `***` wherever
|
|
105
|
+
* a secret happened to share it.
|
|
106
|
+
*/
|
|
107
|
+
const SHORTEST_MASKED = 6;
|
|
108
|
+
/** The values a run must never print, and the rewrite that keeps it so. */
|
|
109
|
+
var Redactor = class {
|
|
110
|
+
values = /* @__PURE__ */ new Set();
|
|
111
|
+
add(value) {
|
|
112
|
+
if (value && value.length >= SHORTEST_MASKED) this.values.add(value);
|
|
113
|
+
}
|
|
114
|
+
/** `text` with every masked value replaced by `***`, longest first. */
|
|
115
|
+
redact(text) {
|
|
116
|
+
if (this.values.size === 0) return text;
|
|
117
|
+
let out = text;
|
|
118
|
+
const longestFirst = [...this.values].sort((a, b) => b.length - a.length);
|
|
119
|
+
for (const value of longestFirst) out = out.replaceAll(value, "***");
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
/** Every value in `secrets` that is a secret rather than a label. */
|
|
124
|
+
function secretValues(secrets) {
|
|
125
|
+
return [
|
|
126
|
+
secrets.seed,
|
|
127
|
+
...Object.entries(secrets.custom ?? {}).filter(([key]) => !require_workspace.isReservedStageKey(key)).map(([, value]) => value),
|
|
128
|
+
...Object.values(secrets.urls ?? {}),
|
|
129
|
+
...Object.values(secrets.services ?? {}).map((service) => service?.password)
|
|
130
|
+
].filter((value) => typeof value === "string");
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* The stage's secrets through the store `secrets.store` names for it. Opening
|
|
134
|
+
* one builds a client and makes no request, so a target that never reads them
|
|
135
|
+
* costs nothing.
|
|
136
|
+
*/
|
|
137
|
+
async function deploySecrets(workspace, stage, redactor, options = {}) {
|
|
138
|
+
const store = await require_seeds.secretsStoreFor(workspace, stage, options);
|
|
139
|
+
const mask = (secrets) => {
|
|
140
|
+
for (const value of secretValues(secrets)) redactor.add(value);
|
|
141
|
+
};
|
|
142
|
+
return {
|
|
143
|
+
store: store.name,
|
|
144
|
+
async read() {
|
|
145
|
+
const secrets = await store.read(stage);
|
|
146
|
+
if (secrets) mask(secrets);
|
|
147
|
+
return secrets;
|
|
148
|
+
},
|
|
149
|
+
async write(secrets) {
|
|
150
|
+
mask(secrets);
|
|
151
|
+
await store.write(stage, secrets);
|
|
152
|
+
},
|
|
153
|
+
mask: (value) => redactor.add(value)
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
const SUFFIX_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
157
|
+
/** A name that S3 would refuse — what the name builder never returns. */
|
|
158
|
+
var InvalidBucketName = class extends require_errors.GkmError {
|
|
159
|
+
bucket;
|
|
160
|
+
reason;
|
|
161
|
+
constructor(bucket, reason) {
|
|
162
|
+
super(`'${bucket}' is not a valid S3 bucket name: ${reason}. Rename the construct or set deploy.namespace in gkm.config.ts to something that leaves room for it.`);
|
|
163
|
+
this.bucket = bucket;
|
|
164
|
+
this.reason = reason;
|
|
165
|
+
this.name = "InvalidBucketName";
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
/** Lowercase `[a-z0-9-]`, runs of anything else one `-`, no edge dashes. */
|
|
169
|
+
function part(value) {
|
|
170
|
+
return (0, _geekmidas_manifest.kebabCase)(value).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* `parts` joined by `-`, shortened to `max` characters by trimming the longest
|
|
174
|
+
* part one character at a time — so a long construct id gives way before a
|
|
175
|
+
* short stage name does, and every part keeps its start.
|
|
176
|
+
*/
|
|
177
|
+
function fitParts(parts, max) {
|
|
178
|
+
const kept = parts.map(part).filter(Boolean);
|
|
179
|
+
const length = () => kept.reduce((sum, p) => sum + p.length, 0) + kept.length - 1;
|
|
180
|
+
while (length() > max) {
|
|
181
|
+
let longest = 0;
|
|
182
|
+
for (let i = 1; i < kept.length; i++) if (kept[i].length > kept[longest].length) longest = i;
|
|
183
|
+
if (kept[longest].length <= 1) break;
|
|
184
|
+
kept[longest] = kept[longest].slice(0, -1).replace(/-+$/, "");
|
|
185
|
+
}
|
|
186
|
+
return kept.join("-");
|
|
187
|
+
}
|
|
188
|
+
/** Why `name` is not a valid bucket name, or undefined when it is. */
|
|
189
|
+
function bucketNameProblem(name) {
|
|
190
|
+
if (name.length < 3 || name.length > 63) return `it is ${name.length} characters, and S3 takes 3 to 63`;
|
|
191
|
+
if (!/^[a-z0-9-]+$/.test(name)) return "it may hold only lowercase letters, digits and -";
|
|
192
|
+
if (!/^[a-z0-9].*[a-z0-9]$/.test(name)) return "it must start and end with a letter or a digit";
|
|
193
|
+
if (name.startsWith("xn--") || name.startsWith("sthree-")) return "it starts with a prefix S3 reserves ('xn--', 'sthree-')";
|
|
194
|
+
if (name.endsWith("-s3alias") || name.endsWith("--ol-s3")) return "it ends with a suffix S3 reserves ('-s3alias', '--ol-s3')";
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* The good name: `<namespace>-<project>-<stage>-<bucket id>`, lowercased, no
|
|
198
|
+
* dots, shortened to leave room for a clash's suffix.
|
|
199
|
+
*
|
|
200
|
+
* @throws {InvalidBucketName} when nothing usable is left
|
|
201
|
+
*/
|
|
202
|
+
function bucketName(input) {
|
|
203
|
+
const name = fitParts([
|
|
204
|
+
input.scope,
|
|
205
|
+
input.stage,
|
|
206
|
+
input.id
|
|
207
|
+
], 56);
|
|
208
|
+
const problem = bucketNameProblem(name);
|
|
209
|
+
if (problem) throw new InvalidBucketName(name, problem);
|
|
210
|
+
return name;
|
|
211
|
+
}
|
|
212
|
+
/** Six random `[a-z0-9]` characters. */
|
|
213
|
+
function randomSuffix(random = node_crypto.randomInt) {
|
|
214
|
+
let suffix = "";
|
|
215
|
+
for (let i = 0; i < 6; i++) suffix += SUFFIX_ALPHABET[random(36)];
|
|
216
|
+
return suffix;
|
|
217
|
+
}
|
|
218
|
+
/** The good name with a clash's suffix. */
|
|
219
|
+
function suffixedBucketName(good, suffix) {
|
|
220
|
+
return `${good}-${suffix}`;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* The IAM user a bucket is reached with on a stage:
|
|
224
|
+
* `gkm-<project>-<stage>-<bucket id>`, at most 64 characters.
|
|
225
|
+
*/
|
|
226
|
+
function iamUserName(input) {
|
|
227
|
+
return fitParts([
|
|
228
|
+
"gkm",
|
|
229
|
+
input.scope,
|
|
230
|
+
input.stage,
|
|
231
|
+
input.id
|
|
232
|
+
], 64);
|
|
233
|
+
}
|
|
234
|
+
/** The path every user gkm creates is under, so it finds its own. */
|
|
235
|
+
const IAM_USER_PATH = "/gkm/";
|
|
236
|
+
/** The inline policy each user is given. */
|
|
237
|
+
const IAM_POLICY_NAME = "gkm-bucket";
|
|
238
|
+
//#endregion
|
|
239
|
+
//#region src/providers/s3/policy.ts
|
|
240
|
+
const bucketArn = (bucket) => `arn:aws:s3:::${bucket}`;
|
|
241
|
+
/**
|
|
242
|
+
* What the app's key may do: objects in this bucket, and listing it — and
|
|
243
|
+
* nothing else in the account.
|
|
244
|
+
*
|
|
245
|
+
* Multipart uploads are in it because a presigned upload of a large file is
|
|
246
|
+
* one, and an abandoned one is cleaned up by the app. Reading old versions is
|
|
247
|
+
* added only when the bucket keeps them.
|
|
248
|
+
*/
|
|
249
|
+
function userPolicy(bucket, options = {}) {
|
|
250
|
+
return {
|
|
251
|
+
Version: "2012-10-17",
|
|
252
|
+
Statement: [{
|
|
253
|
+
Sid: "GkmObjects",
|
|
254
|
+
Effect: "Allow",
|
|
255
|
+
Action: [
|
|
256
|
+
"s3:GetObject",
|
|
257
|
+
"s3:PutObject",
|
|
258
|
+
"s3:DeleteObject",
|
|
259
|
+
"s3:AbortMultipartUpload",
|
|
260
|
+
"s3:ListMultipartUploadParts",
|
|
261
|
+
...options.versioning ? ["s3:GetObjectVersion"] : []
|
|
262
|
+
],
|
|
263
|
+
Resource: `${bucketArn(bucket)}/*`
|
|
264
|
+
}, {
|
|
265
|
+
Sid: "GkmBucket",
|
|
266
|
+
Effect: "Allow",
|
|
267
|
+
Action: [
|
|
268
|
+
"s3:ListBucket",
|
|
269
|
+
"s3:GetBucketLocation",
|
|
270
|
+
"s3:ListBucketMultipartUploads"
|
|
271
|
+
],
|
|
272
|
+
Resource: bucketArn(bucket)
|
|
273
|
+
}]
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* One open pattern as the resource it is public on. A `**` is S3's `*`; a
|
|
278
|
+
* single `*` is too, which crosses `/` where the construct's own check does
|
|
279
|
+
* not — the construct is the stricter of the two (see `bucketPolicy` in
|
|
280
|
+
* reconcile, which the local MinIO is given the same way).
|
|
281
|
+
*/
|
|
282
|
+
function openResource(bucket, pattern) {
|
|
283
|
+
const path = pattern.replace(/^\/+/, "").replace(/\*\*/g, "*");
|
|
284
|
+
return `${bucketArn(bucket)}/${path}`;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* The bucket's own policy: every request refused unless it came over TLS,
|
|
288
|
+
* and — only when a file server opens paths on it — anonymous `s3:GetObject`
|
|
289
|
+
* on exactly those prefixes.
|
|
290
|
+
*/
|
|
291
|
+
function bucketPolicy(bucket, open = []) {
|
|
292
|
+
const statements = [{
|
|
293
|
+
Sid: "GkmDenyInsecureTransport",
|
|
294
|
+
Effect: "Deny",
|
|
295
|
+
Principal: "*",
|
|
296
|
+
Action: "s3:*",
|
|
297
|
+
Resource: [bucketArn(bucket), `${bucketArn(bucket)}/*`],
|
|
298
|
+
Condition: { Bool: { "aws:SecureTransport": "false" } }
|
|
299
|
+
}];
|
|
300
|
+
if (open.length > 0) statements.push({
|
|
301
|
+
Sid: "GkmOpenPaths",
|
|
302
|
+
Effect: "Allow",
|
|
303
|
+
Principal: "*",
|
|
304
|
+
Action: "s3:GetObject",
|
|
305
|
+
Resource: [...new Set(open.map((p) => openResource(bucket, p)))].sort()
|
|
306
|
+
});
|
|
307
|
+
return {
|
|
308
|
+
Version: "2012-10-17",
|
|
309
|
+
Statement: statements
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Block Public Access. All four flags on, unless the bucket has open paths:
|
|
314
|
+
* then a *policy* may make it public (`BlockPublicPolicy`,
|
|
315
|
+
* `RestrictPublicBuckets` off), and ACLs still may not.
|
|
316
|
+
*/
|
|
317
|
+
function publicAccessBlock(open) {
|
|
318
|
+
return {
|
|
319
|
+
BlockPublicAcls: true,
|
|
320
|
+
IgnorePublicAcls: true,
|
|
321
|
+
BlockPublicPolicy: !open,
|
|
322
|
+
RestrictPublicBuckets: !open
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
function corsRule(origins) {
|
|
326
|
+
return {
|
|
327
|
+
AllowedMethods: [
|
|
328
|
+
"GET",
|
|
329
|
+
"HEAD",
|
|
330
|
+
"POST",
|
|
331
|
+
"PUT"
|
|
332
|
+
],
|
|
333
|
+
AllowedOrigins: [...new Set(origins)].sort(),
|
|
334
|
+
AllowedHeaders: ["*"],
|
|
335
|
+
ExposeHeaders: ["ETag"],
|
|
336
|
+
MaxAgeSeconds: 3e3
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* A policy document in one canonical form — keys sorted, a lone string a
|
|
341
|
+
* one-element list, lists sorted — so one AWS reformatted reads as the same
|
|
342
|
+
* document rather than as drift.
|
|
343
|
+
*/
|
|
344
|
+
function canonicalPolicy(value) {
|
|
345
|
+
return JSON.stringify(canonical(value));
|
|
346
|
+
}
|
|
347
|
+
function canonical(value, key) {
|
|
348
|
+
if (Array.isArray(value)) {
|
|
349
|
+
const items = value.map((v) => canonical(v));
|
|
350
|
+
return key === "Statement" ? items : items.map((v) => JSON.stringify(v)).sort();
|
|
351
|
+
}
|
|
352
|
+
if (value && typeof value === "object") return Object.fromEntries(Object.keys(value).sort().map((k) => {
|
|
353
|
+
const v = value[k];
|
|
354
|
+
return [k, canonical(k === "Action" || k === "Resource" || k === "AWS" ? Array.isArray(v) ? v : [v] : v, k)];
|
|
355
|
+
}));
|
|
356
|
+
return value;
|
|
357
|
+
}
|
|
358
|
+
//#endregion
|
|
359
|
+
//#region src/providers/iam.ts
|
|
360
|
+
/**
|
|
361
|
+
* An IAM user gkm creates for one thing on a stage, its inline policy, and
|
|
362
|
+
* the access key written into the stage's secrets as an `s3://` URL — what
|
|
363
|
+
* the `s3` provider gives each bucket, and the deploy gives a stage's
|
|
364
|
+
* backups.
|
|
365
|
+
*
|
|
366
|
+
* - **The user** is under `/gkm/`, tagged with the deploy identity, the stage
|
|
367
|
+
* and what it is for. One that exists without those tags is someone
|
|
368
|
+
* else's, and is refused rather than taken over.
|
|
369
|
+
* - **Its policy** is read on every run and put back when it drifted.
|
|
370
|
+
* - **Its key**: one, written into the stage's secrets the moment IAM shows
|
|
371
|
+
* its secret. `--rotate-keys` issues a successor and keeps the old one
|
|
372
|
+
* active until the stage has deployed with the new one; the next run after
|
|
373
|
+
* that — or `--retire-old-keys` — deletes it.
|
|
374
|
+
* - **A new key** takes IAM seconds to become usable: the state records when
|
|
375
|
+
* it was issued, and a check made with it waits that out
|
|
376
|
+
* ({@link whileKeyIsNew}).
|
|
377
|
+
*/
|
|
378
|
+
/** The tags gkm finds its own buckets and users by. */
|
|
379
|
+
const TAG_PROJECT = "gkm:project";
|
|
380
|
+
const TAG_STAGE = "gkm:stage";
|
|
381
|
+
const TAG_CONSTRUCT = "gkm:construct";
|
|
382
|
+
/** The keys gkm tags a bucket and a user with. */
|
|
383
|
+
function identityTags(ctx, id) {
|
|
384
|
+
return [
|
|
385
|
+
{
|
|
386
|
+
Key: TAG_PROJECT,
|
|
387
|
+
Value: ctx.identity.key
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
Key: TAG_STAGE,
|
|
391
|
+
Value: ctx.stage
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
Key: TAG_CONSTRUCT,
|
|
395
|
+
Value: id
|
|
396
|
+
}
|
|
397
|
+
];
|
|
398
|
+
}
|
|
399
|
+
/** A read whose "there is none" answer is one of `missing`. */
|
|
400
|
+
async function readOr$1(read, missing) {
|
|
401
|
+
try {
|
|
402
|
+
return await read();
|
|
403
|
+
} catch (error) {
|
|
404
|
+
const e = error;
|
|
405
|
+
if (missing.includes(e?.Code ?? e?.name ?? "")) return void 0;
|
|
406
|
+
throw error;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
/** IAM returns a policy document URL-encoded; an emulator may not. */
|
|
410
|
+
function decodePolicy(document) {
|
|
411
|
+
try {
|
|
412
|
+
return decodeURIComponent(document);
|
|
413
|
+
} catch {
|
|
414
|
+
return document;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* The user and its inline policy: found — and refused when it is not this
|
|
419
|
+
* stage's — or created, and the policy put back when it drifted.
|
|
420
|
+
*
|
|
421
|
+
* @throws {IamUserNotOwned}
|
|
422
|
+
*/
|
|
423
|
+
async function ensureIamUser(ctx, clients, input) {
|
|
424
|
+
const { IAM, iam } = clients;
|
|
425
|
+
const { id, user, policy } = input;
|
|
426
|
+
const entry = {
|
|
427
|
+
key: `iam-user:${id}`,
|
|
428
|
+
type: "iam-user"
|
|
429
|
+
};
|
|
430
|
+
const found = await readOr$1(() => iam.send(new IAM.GetUserCommand({ UserName: user })), ["NoSuchEntity", "NoSuchEntityException"]);
|
|
431
|
+
if (found?.User) {
|
|
432
|
+
const tags = await iam.send(new IAM.ListUserTagsCommand({ UserName: user }));
|
|
433
|
+
const tag = (key) => tags.Tags?.find((t) => t.Key === key)?.Value;
|
|
434
|
+
if (found.User.Path !== "/gkm/" || tag("gkm:project") !== ctx.identity.key || tag("gkm:stage") !== ctx.stage) throw new require_workspace.IamUserNotOwned(user, ctx.identity.key, ctx.stage);
|
|
435
|
+
await ctx.state.ready(entry, user);
|
|
436
|
+
} else await ctx.change({
|
|
437
|
+
construct: id,
|
|
438
|
+
resource: `IAM user ${user}`,
|
|
439
|
+
change: `create under ${IAM_USER_PATH}`
|
|
440
|
+
}, async () => {
|
|
441
|
+
await ctx.state.pending(entry);
|
|
442
|
+
await iam.send(new IAM.CreateUserCommand({
|
|
443
|
+
UserName: user,
|
|
444
|
+
Path: IAM_USER_PATH,
|
|
445
|
+
Tags: identityTags(ctx, id)
|
|
446
|
+
}));
|
|
447
|
+
await ctx.state.ready(entry, user);
|
|
448
|
+
});
|
|
449
|
+
const exists = Boolean(found?.User) || !ctx.dryRun;
|
|
450
|
+
const current = exists ? await readOr$1(() => iam.send(new IAM.GetUserPolicyCommand({
|
|
451
|
+
UserName: user,
|
|
452
|
+
PolicyName: input.policyName
|
|
453
|
+
})), ["NoSuchEntity", "NoSuchEntityException"]) : void 0;
|
|
454
|
+
const document = current?.PolicyDocument ? JSON.parse(decodePolicy(current.PolicyDocument)) : void 0;
|
|
455
|
+
if (!document || canonicalPolicy(document) !== canonicalPolicy(policy)) await ctx.change({
|
|
456
|
+
construct: id,
|
|
457
|
+
resource: `IAM user ${user}`,
|
|
458
|
+
change: `policy ${input.policyName}: ${input.describe}`
|
|
459
|
+
}, async () => {
|
|
460
|
+
await iam.send(new IAM.PutUserPolicyCommand({
|
|
461
|
+
UserName: user,
|
|
462
|
+
PolicyName: input.policyName,
|
|
463
|
+
PolicyDocument: JSON.stringify(policy)
|
|
464
|
+
}));
|
|
465
|
+
});
|
|
466
|
+
return {
|
|
467
|
+
user,
|
|
468
|
+
exists
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* The user's access key, in the stage's secrets as the bucket's URL.
|
|
473
|
+
*
|
|
474
|
+
* - First run: one key, written into the stage's secrets.
|
|
475
|
+
* - `--rotate-keys`: a second key, written in place of the first; the first
|
|
476
|
+
* stays active until the stage has deployed with the second, and the next
|
|
477
|
+
* run after that deploy — or `--retire-old-keys` — deletes it.
|
|
478
|
+
* - A key the secrets no longer hold (a store emptied by hand): replaced the
|
|
479
|
+
* same way, since its secret cannot be read back.
|
|
480
|
+
*
|
|
481
|
+
* @throws {BucketKeySetElsewhere} when the secret names another bucket
|
|
482
|
+
* @throws {RotationInProgress} on `--rotate-keys` with the last rotation's old key still active
|
|
483
|
+
* @throws {AccessKeyLimit} when the user has two keys gkm did not record
|
|
484
|
+
*/
|
|
485
|
+
async function ensureAccessKey(ctx, clients, input) {
|
|
486
|
+
const { IAM, iam } = clients;
|
|
487
|
+
const { id, bucket, user, urlKey } = input;
|
|
488
|
+
const entryKey = `iam-access-key:${id}`;
|
|
489
|
+
const record = ctx.state.record(entryKey);
|
|
490
|
+
const given = ctx.secrets[urlKey];
|
|
491
|
+
let inUrl;
|
|
492
|
+
let parsed;
|
|
493
|
+
if (given !== void 0) {
|
|
494
|
+
try {
|
|
495
|
+
parsed = _geekmidas_storage_s3_url.parse(given);
|
|
496
|
+
} catch {
|
|
497
|
+
parsed = void 0;
|
|
498
|
+
}
|
|
499
|
+
if (parsed?.bucket !== bucket) throw new require_workspace.BucketKeySetElsewhere(urlKey, bucket, parsed?.bucket ?? given.replace(/\/\/[^@]*@/, "//"), ctx.stage);
|
|
500
|
+
inUrl = parsed.accessKeyId;
|
|
501
|
+
}
|
|
502
|
+
const listed = input.exists ? (await iam.send(new IAM.ListAccessKeysCommand({ UserName: user }))).AccessKeyMetadata?.map((k) => k.AccessKeyId) ?? [] : [];
|
|
503
|
+
let current = record?.status === "ready" ? record.id : void 0;
|
|
504
|
+
let previous = typeof record?.data?.previous === "string" ? record.data.previous : void 0;
|
|
505
|
+
const rotatedAt = typeof record?.data?.rotatedAt === "string" ? record.data.rotatedAt : void 0;
|
|
506
|
+
const entry = (data = {}) => ({
|
|
507
|
+
key: entryKey,
|
|
508
|
+
type: "iam-access-key",
|
|
509
|
+
data: {
|
|
510
|
+
user,
|
|
511
|
+
...data
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
if (previous && current) {
|
|
515
|
+
const deployed = rotatedAt !== void 0 && ctx.state.lastDeployedAt !== void 0 && ctx.state.lastDeployedAt > rotatedAt;
|
|
516
|
+
if (ctx.retireOldKeys || deployed) {
|
|
517
|
+
const old = previous;
|
|
518
|
+
const keep = current;
|
|
519
|
+
await ctx.change({
|
|
520
|
+
construct: id,
|
|
521
|
+
resource: `IAM user ${user}`,
|
|
522
|
+
change: `delete the rotated-out access key ${old}`
|
|
523
|
+
}, async () => {
|
|
524
|
+
if (listed.includes(old)) await iam.send(new IAM.DeleteAccessKeyCommand({
|
|
525
|
+
UserName: user,
|
|
526
|
+
AccessKeyId: old
|
|
527
|
+
}));
|
|
528
|
+
await ctx.state.ready(entry(), keep);
|
|
529
|
+
});
|
|
530
|
+
previous = void 0;
|
|
531
|
+
} else ctx.log(` ${id}: the old access key ${previous} stays active until '${ctx.stage}' is deployed with the new one; the next deploy after that deletes it (or pass --retire-old-keys)`);
|
|
532
|
+
}
|
|
533
|
+
if (!current && inUrl && listed.includes(inUrl)) {
|
|
534
|
+
await ctx.state.ready(entry(), inUrl);
|
|
535
|
+
current = inUrl;
|
|
536
|
+
}
|
|
537
|
+
if (current !== void 0 && listed.includes(current) && inUrl === current && !ctx.rotateKeys) {
|
|
538
|
+
if (parsed && parsed.region !== clients.region) {
|
|
539
|
+
const address = parsed;
|
|
540
|
+
await ctx.change({
|
|
541
|
+
construct: id,
|
|
542
|
+
resource: urlKey,
|
|
543
|
+
change: `name ${clients.region}, the bucket's region (it said ${address.region ?? "none"})`
|
|
544
|
+
}, () => ctx.writeSecrets({ [urlKey]: _geekmidas_storage_s3_url.build({
|
|
545
|
+
...address,
|
|
546
|
+
region: clients.region
|
|
547
|
+
}) }));
|
|
548
|
+
}
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
if (ctx.rotateKeys && previous) throw new require_workspace.RotationInProgress(id, previous, ctx.stage);
|
|
552
|
+
const outgoing = current && listed.includes(current) ? current : void 0;
|
|
553
|
+
if (listed.length >= 2) throw new require_workspace.AccessKeyLimit(user, listed);
|
|
554
|
+
const why = !current ? "create an access key" : ctx.rotateKeys ? "rotate: issue a new access key" : "issue a new access key (the secrets no longer hold the recorded one)";
|
|
555
|
+
await ctx.change({
|
|
556
|
+
construct: id,
|
|
557
|
+
resource: `IAM user ${user}`,
|
|
558
|
+
change: why
|
|
559
|
+
}, async () => {
|
|
560
|
+
await ctx.state.pending(entry(outgoing ? { previous: outgoing } : {}));
|
|
561
|
+
const key = (await iam.send(new IAM.CreateAccessKeyCommand({ UserName: user }))).AccessKey;
|
|
562
|
+
await ctx.writeSecrets({ [urlKey]: _geekmidas_storage_s3_url.build({
|
|
563
|
+
bucket,
|
|
564
|
+
region: clients.region,
|
|
565
|
+
...clients.endpoint ? {
|
|
566
|
+
endpoint: clients.endpoint,
|
|
567
|
+
forcePathStyle: true
|
|
568
|
+
} : {},
|
|
569
|
+
accessKeyId: key.AccessKeyId,
|
|
570
|
+
secretAccessKey: key.SecretAccessKey
|
|
571
|
+
}) });
|
|
572
|
+
const issuedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
573
|
+
await ctx.state.ready(entry(outgoing ? {
|
|
574
|
+
previous: outgoing,
|
|
575
|
+
rotatedAt: issuedAt,
|
|
576
|
+
issuedAt
|
|
577
|
+
} : { issuedAt }), key.AccessKeyId);
|
|
578
|
+
});
|
|
579
|
+
if (outgoing) ctx.log(` ${urlKey} holds the new key. This deploy moves '${ctx.stage}' onto it; the next deploy after it deletes the old key ${outgoing}`);
|
|
580
|
+
}
|
|
581
|
+
/** How long a key issued this recently counts as new to IAM. */
|
|
582
|
+
const NEW_KEY_WINDOW_MS = 6e5;
|
|
583
|
+
/** The waits between asks while a new key becomes usable: about a minute. */
|
|
584
|
+
let newKeyWaits = [
|
|
585
|
+
2e3,
|
|
586
|
+
4e3,
|
|
587
|
+
8e3,
|
|
588
|
+
15e3,
|
|
589
|
+
15e3,
|
|
590
|
+
15e3
|
|
591
|
+
];
|
|
592
|
+
/**
|
|
593
|
+
* When the key in a URL was issued, if the stage's state says it was just
|
|
594
|
+
* now — the record of `id`'s key naming that very key.
|
|
595
|
+
*/
|
|
596
|
+
function issuedJustNow(resources, id, accessKeyId) {
|
|
597
|
+
const record = resources?.[`iam-access-key:${id}`];
|
|
598
|
+
const issuedAt = record?.data?.issuedAt;
|
|
599
|
+
if (record?.id !== accessKeyId || typeof issuedAt !== "string") return;
|
|
600
|
+
const age = Date.now() - Date.parse(issuedAt);
|
|
601
|
+
return age >= 0 && age < NEW_KEY_WINDOW_MS ? issuedAt : void 0;
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* `ask` again, for about a minute, while it is `refused` and the key was
|
|
605
|
+
* issued just now — IAM refuses a key for seconds after making it. Returns
|
|
606
|
+
* the last answer and how long was waited.
|
|
607
|
+
*/
|
|
608
|
+
async function whileKeyIsNew(first, options) {
|
|
609
|
+
let answer = first;
|
|
610
|
+
let waited = 0;
|
|
611
|
+
if (!options.issuedAt || !options.refused(answer)) return {
|
|
612
|
+
answer,
|
|
613
|
+
waited
|
|
614
|
+
};
|
|
615
|
+
options.onWait?.();
|
|
616
|
+
for (const wait of newKeyWaits) {
|
|
617
|
+
await new Promise((resolve) => setTimeout(resolve, wait));
|
|
618
|
+
waited += wait;
|
|
619
|
+
answer = await options.ask();
|
|
620
|
+
if (!options.refused(answer)) break;
|
|
621
|
+
}
|
|
622
|
+
return {
|
|
623
|
+
answer,
|
|
624
|
+
waited
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
//#endregion
|
|
628
|
+
//#region src/backups/provision.ts
|
|
629
|
+
/** What the backups user and its key are recorded and tagged as. */
|
|
630
|
+
const BACKUPS_ID = "backups";
|
|
631
|
+
/** The backups user's inline policy's name. */
|
|
632
|
+
const BACKUPS_POLICY_NAME = "gkm-backups";
|
|
633
|
+
/** Where a deploy's check puts its marker, under the backups prefix. */
|
|
634
|
+
const VERIFY_MARKER = ".gkm-verify";
|
|
635
|
+
/** The backups user's name: `gkm-<project>-<stage>-backups`. */
|
|
636
|
+
function backupsUserName(scope, stage) {
|
|
637
|
+
return iamUserName({
|
|
638
|
+
scope,
|
|
639
|
+
stage,
|
|
640
|
+
id: BACKUPS_ID
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* The backups key's policy: put objects under the stage's backups prefix.
|
|
645
|
+
* Uploading in parts is `s3:PutObject` too; aborting one is not, and the
|
|
646
|
+
* bucket's lifecycle does it instead.
|
|
647
|
+
*/
|
|
648
|
+
function backupsPolicy(bucket, prefix) {
|
|
649
|
+
return {
|
|
650
|
+
Version: "2012-10-17",
|
|
651
|
+
Statement: [{
|
|
652
|
+
Sid: "GkmBackupsPut",
|
|
653
|
+
Effect: "Allow",
|
|
654
|
+
Action: ["s3:PutObject"],
|
|
655
|
+
Resource: `arn:aws:s3:::${bucket}/${prefix}/*`
|
|
656
|
+
}]
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
/** A stage that backs up, and no AWS credentials to create its key with. */
|
|
660
|
+
var BackupsCredentialsMissing = class extends require_errors.GkmError {
|
|
661
|
+
stage;
|
|
662
|
+
constructor(stage) {
|
|
663
|
+
super(`'${stage}' runs Postgres, so the deploy backs it up — and creates the IAM user and key it backs up with in the stage's AWS account — and it found no AWS credentials to do it with. On this machine, supply ${require_workspace.S3_PROVISIONING.describe}. In CI, the step that runs the deploy needs the stage's role (AWS_ROLE_ARN, from gkm deploy:github). To take no backups, set deploy.backups.${stage}: false.`);
|
|
664
|
+
this.stage = stage;
|
|
665
|
+
this.name = "BackupsCredentialsMissing";
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
/** No region to create the project bucket in. */
|
|
669
|
+
var BackupsRegionRequired = class extends require_errors.GkmError {
|
|
670
|
+
stage;
|
|
671
|
+
constructor(stage) {
|
|
672
|
+
super(`'${stage}' is backed up to the project bucket, and there is no region to find it in: set state: { provider: 's3', region } in gkm.config.ts, or AWS_REGION.`);
|
|
673
|
+
this.stage = stage;
|
|
674
|
+
this.name = "BackupsRegionRequired";
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
/** A bucket construct whose IAM user would be the backups user. */
|
|
678
|
+
var BackupsUserNameTaken = class extends require_errors.GkmError {
|
|
679
|
+
stage;
|
|
680
|
+
construct;
|
|
681
|
+
constructor(stage, construct) {
|
|
682
|
+
super(`The bucket '${construct}' is reached on '${stage}' with the IAM user the stage's backups are put with — both are named after 'backups'. Rename the construct, or set deploy.backups.${stage}: false.`);
|
|
683
|
+
this.stage = stage;
|
|
684
|
+
this.construct = construct;
|
|
685
|
+
this.name = "BackupsUserNameTaken";
|
|
686
|
+
}
|
|
687
|
+
};
|
|
688
|
+
/** The backups key could not put its marker: backups would fail too. */
|
|
689
|
+
var BackupsKeyRefused = class extends require_errors.GkmError {
|
|
690
|
+
stage;
|
|
691
|
+
bucket;
|
|
692
|
+
key;
|
|
693
|
+
said;
|
|
694
|
+
constructor(stage, bucket, key, said) {
|
|
695
|
+
super(`The ${require_dns.BACKUPS_URL_KEY} key of '${stage}' could not put s3://${bucket}/${key} (${said}), so its backups would fail the same way. Run the deploy with the stage's AWS credentials to put its user and policy back, or --rotate-keys to issue it a new key.`);
|
|
696
|
+
this.stage = stage;
|
|
697
|
+
this.bucket = bucket;
|
|
698
|
+
this.key = key;
|
|
699
|
+
this.said = said;
|
|
700
|
+
this.name = "BackupsKeyRefused";
|
|
701
|
+
}
|
|
702
|
+
};
|
|
703
|
+
/**
|
|
704
|
+
* The region the project bucket is in: the state's, when it is kept in S3,
|
|
705
|
+
* the secrets store's when that is in AWS, the credential's, `AWS_REGION`.
|
|
706
|
+
*/
|
|
707
|
+
function backupsRegion(workspace, stage, credential, env = process.env) {
|
|
708
|
+
const state = workspace.state;
|
|
709
|
+
const store = workspace.secrets?.store;
|
|
710
|
+
const region = (state && "region" in state ? state.region : void 0) ?? (store && typeof store === "object" && "region" in store ? store.region : void 0) ?? credential?.region ?? env.AWS_REGION ?? env.AWS_DEFAULT_REGION;
|
|
711
|
+
if (!region) throw new BackupsRegionRequired(stage);
|
|
712
|
+
return region;
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* The stage's backups bucket, lifecycle, user and key — or, for a stage set
|
|
716
|
+
* to `false`, a notice and nothing else. Returns what the stage's backups
|
|
717
|
+
* came to.
|
|
718
|
+
*
|
|
719
|
+
* @throws {BackupsCredentialsMissing} when there are no AWS credentials
|
|
720
|
+
*/
|
|
721
|
+
async function ensureStageBackups(input) {
|
|
722
|
+
const { workspace, manifest, stage, ctx } = input;
|
|
723
|
+
const env = input.env ?? process.env;
|
|
724
|
+
const choice = require_dns.stageBackups(workspace, manifest, stage);
|
|
725
|
+
if (choice.mode === "disabled") {
|
|
726
|
+
ctx.log(`💾 backups: off — deploy.backups.${stage} is false. Nothing is backed up, and nothing already backed up is deleted.`);
|
|
727
|
+
return choice;
|
|
728
|
+
}
|
|
729
|
+
if (choice.mode !== "on") return choice;
|
|
730
|
+
if (require_workspace.stageProvider(workspace, "objects", stage).mode === "provider") {
|
|
731
|
+
const clash = Object.entries(manifest).find(([id, d]) => d.kind === "objects" && require_derive.appKey(id) === "backups");
|
|
732
|
+
if (clash) throw new BackupsUserNameTaken(stage, clash[0]);
|
|
733
|
+
}
|
|
734
|
+
const credential = await awsProvisioningCredentials({
|
|
735
|
+
stage,
|
|
736
|
+
env,
|
|
737
|
+
...input.profile ? { profile: input.profile } : {},
|
|
738
|
+
...input.home ? { home: input.home } : {}
|
|
739
|
+
});
|
|
740
|
+
if (!credential) throw new BackupsCredentialsMissing(stage);
|
|
741
|
+
const region = backupsRegion(workspace, stage, credential, env);
|
|
742
|
+
const clients = await backupsClients(credential, region, env);
|
|
743
|
+
const bucket = require_projectBucket.projectBucketName(ctx.identity.scope, await require_projectBucket.callerAccountId(clients.sts));
|
|
744
|
+
const prefix = require_dns.backupsPrefix(workspace, stage);
|
|
745
|
+
const expiries = require_dns.backupExpiries(workspace, manifest);
|
|
746
|
+
ctx.log(`💾 backups: ${choice.backups.describe}, to s3://${bucket}/${prefix}/${ctx.dryRun ? " (dry run)" : ""}`);
|
|
747
|
+
if (await require_projectBucket.findProjectBucket(clients.s3, bucket) === "none") await ctx.change({
|
|
748
|
+
construct: require_dns.BACKUPS_SERVICE,
|
|
749
|
+
resource: `bucket ${bucket}`,
|
|
750
|
+
change: `create in ${region}, expiring each stage's backups`
|
|
751
|
+
}, async () => {
|
|
752
|
+
await require_projectBucket.ensureProjectBucket(clients.s3, {
|
|
753
|
+
bucket,
|
|
754
|
+
region,
|
|
755
|
+
identity: ctx.identity,
|
|
756
|
+
expiries
|
|
757
|
+
});
|
|
758
|
+
});
|
|
759
|
+
else if (await require_projectBucket.reconcileProjectBucketLifecycle(clients.s3, bucket, expiries, { dryRun: true })) await ctx.change({
|
|
760
|
+
construct: require_dns.BACKUPS_SERVICE,
|
|
761
|
+
resource: `bucket ${bucket}`,
|
|
762
|
+
change: `lifecycle: ${expiries.map((e) => `${e.prefix} after ${e.days} days`).join(", ")}`
|
|
763
|
+
}, async () => {
|
|
764
|
+
await require_projectBucket.reconcileProjectBucketLifecycle(clients.s3, bucket, expiries);
|
|
765
|
+
});
|
|
766
|
+
const user = await ensureIamUser(ctx, clients, {
|
|
767
|
+
id: BACKUPS_ID,
|
|
768
|
+
user: backupsUserName(ctx.identity.scope, stage),
|
|
769
|
+
policyName: BACKUPS_POLICY_NAME,
|
|
770
|
+
policy: backupsPolicy(bucket, prefix),
|
|
771
|
+
describe: `put objects under ${prefix}/ only`
|
|
772
|
+
});
|
|
773
|
+
await ensureAccessKey(ctx, clients, {
|
|
774
|
+
id: BACKUPS_ID,
|
|
775
|
+
bucket,
|
|
776
|
+
urlKey: require_dns.BACKUPS_URL_KEY,
|
|
777
|
+
...user
|
|
778
|
+
});
|
|
779
|
+
return choice;
|
|
780
|
+
}
|
|
781
|
+
/** The clients a backups run creates through. */
|
|
782
|
+
async function backupsClients(credential, region, env) {
|
|
783
|
+
const S3 = await import("@aws-sdk/client-s3");
|
|
784
|
+
const IAM = await import("@aws-sdk/client-iam");
|
|
785
|
+
const { STSClient } = await import("@aws-sdk/client-sts");
|
|
786
|
+
const config = await awsClientConfig(credential, region);
|
|
787
|
+
const endpoint = awsEndpoint(env, "S3");
|
|
788
|
+
return {
|
|
789
|
+
IAM,
|
|
790
|
+
iam: new IAM.IAMClient(config),
|
|
791
|
+
s3: new S3.S3Client({
|
|
792
|
+
...config,
|
|
793
|
+
...endpoint ? {
|
|
794
|
+
endpoint,
|
|
795
|
+
forcePathStyle: true
|
|
796
|
+
} : {}
|
|
797
|
+
}),
|
|
798
|
+
sts: new STSClient(config),
|
|
799
|
+
region,
|
|
800
|
+
...endpoint ? { endpoint } : {}
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* The stage's backups key puts its marker under the prefix — the one thing
|
|
805
|
+
* it may do. A key issued by this deploy is waited for, as a bucket's is.
|
|
806
|
+
* Returns whether there was a key to check.
|
|
807
|
+
*
|
|
808
|
+
* @throws {BackupsKeyRefused}
|
|
809
|
+
*/
|
|
810
|
+
async function verifyStageBackups(input) {
|
|
811
|
+
const { stage } = input;
|
|
812
|
+
if (require_dns.stageBackups(input.workspace, input.manifest, stage).mode !== "on") return false;
|
|
813
|
+
const url = input.secrets[require_dns.BACKUPS_URL_KEY];
|
|
814
|
+
if (!url) return false;
|
|
815
|
+
const address = (await import("@geekmidas/storage/s3-url")).parse(url);
|
|
816
|
+
if (!address.accessKeyId || !address.secretAccessKey) return false;
|
|
817
|
+
const S3 = await import("@aws-sdk/client-s3");
|
|
818
|
+
const client = new S3.S3Client({
|
|
819
|
+
region: address.region ?? "us-east-1",
|
|
820
|
+
credentials: {
|
|
821
|
+
accessKeyId: address.accessKeyId,
|
|
822
|
+
secretAccessKey: address.secretAccessKey
|
|
823
|
+
},
|
|
824
|
+
...address.endpoint ? { endpoint: address.endpoint } : {},
|
|
825
|
+
...address.forcePathStyle ? { forcePathStyle: true } : {}
|
|
826
|
+
});
|
|
827
|
+
const Key = `${require_dns.backupsPrefix(input.workspace, stage)}/${VERIFY_MARKER}`;
|
|
828
|
+
const put = async () => {
|
|
829
|
+
try {
|
|
830
|
+
await client.send(new S3.PutObjectCommand({
|
|
831
|
+
Bucket: address.bucket,
|
|
832
|
+
Key,
|
|
833
|
+
Body: (/* @__PURE__ */ new Date()).toISOString(),
|
|
834
|
+
ContentType: "text/plain"
|
|
835
|
+
}));
|
|
836
|
+
return {
|
|
837
|
+
ok: true,
|
|
838
|
+
said: ""
|
|
839
|
+
};
|
|
840
|
+
} catch (error) {
|
|
841
|
+
const e = error;
|
|
842
|
+
const status = e.$metadata?.httpStatusCode;
|
|
843
|
+
return {
|
|
844
|
+
ok: false,
|
|
845
|
+
...status !== void 0 ? { status } : {},
|
|
846
|
+
said: `HTTP ${status ?? "no response"} ${e.name ?? "Error"}: ${e.message ?? ""}`
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
};
|
|
850
|
+
try {
|
|
851
|
+
const { answer } = await whileKeyIsNew(await put(), {
|
|
852
|
+
issuedAt: issuedJustNow(input.resources, BACKUPS_ID, address.accessKeyId),
|
|
853
|
+
refused: (answer) => !answer.ok && answer.status === 403,
|
|
854
|
+
ask: put,
|
|
855
|
+
onWait: () => input.log?.(` backups: waiting for the new key ${address.accessKeyId} to become active…`)
|
|
856
|
+
});
|
|
857
|
+
if (!answer.ok) throw new BackupsKeyRefused(stage, address.bucket, Key, answer.said);
|
|
858
|
+
return true;
|
|
859
|
+
} finally {
|
|
860
|
+
client.destroy();
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
//#endregion
|
|
864
|
+
//#region src/providers/s3/origins.ts
|
|
865
|
+
/**
|
|
866
|
+
* Which browser origins may call a bucket: the stage's sites that depend on
|
|
867
|
+
* an API using it.
|
|
868
|
+
*
|
|
869
|
+
* The same reading of the graph an API's own CORS origins are — its callers —
|
|
870
|
+
* taken one step further: a site that calls an API which hands it a presigned
|
|
871
|
+
* upload URL is a site whose browser PUTs to the bucket. A site that reads the
|
|
872
|
+
* bucket (or a file server over it) itself is one too. Nothing is written
|
|
873
|
+
* down: add a site that calls the API and the next provision run allows it.
|
|
874
|
+
*/
|
|
875
|
+
/** The keys a reader of this bucket holds: its URL, and each server's over it. */
|
|
876
|
+
function bucketKeys(manifest, bucket) {
|
|
877
|
+
return [(0, _geekmidas_manifest.provideKey)(bucket, "url"), ...Object.entries(manifest).filter(([, d]) => d.kind === "file-server" && d.of === bucket).map(([id]) => (0, _geekmidas_manifest.provideKey)(id, "url"))];
|
|
878
|
+
}
|
|
879
|
+
function bucketOrigins(input) {
|
|
880
|
+
const { manifest, workspace, stage } = input;
|
|
881
|
+
const keys = bucketKeys(manifest, input.bucket);
|
|
882
|
+
const readers = Object.entries(manifest).filter(([, d]) => d.kind === "rest-api" || d.kind === "site").filter(([id]) => {
|
|
883
|
+
const env = require_seeds.appEnvKeys(manifest, require_derive.appKey(id), input.runnables);
|
|
884
|
+
return env !== void 0 && keys.some((key) => env.has(key));
|
|
885
|
+
});
|
|
886
|
+
const sites = /* @__PURE__ */ new Set();
|
|
887
|
+
for (const [id, declaration] of readers) {
|
|
888
|
+
if (declaration.kind === "site") {
|
|
889
|
+
sites.add(id);
|
|
890
|
+
continue;
|
|
891
|
+
}
|
|
892
|
+
for (const caller of (0, _geekmidas_manifest.dependentsOf)(manifest, id)) if (manifest[caller]?.kind === "site") sites.add(caller);
|
|
893
|
+
}
|
|
894
|
+
const origins = /* @__PURE__ */ new Set();
|
|
895
|
+
const unresolved = [];
|
|
896
|
+
for (const id of [...sites].sort()) {
|
|
897
|
+
const name = require_derive.appKey(id);
|
|
898
|
+
const app = workspace.apps[name];
|
|
899
|
+
if (!app) {
|
|
900
|
+
unresolved.push(name);
|
|
901
|
+
continue;
|
|
902
|
+
}
|
|
903
|
+
try {
|
|
904
|
+
const host = require_seeds.resolveHost(name, app, stage, workspace.domains, require_seeds.isMainFrontendApp(name, app, workspace.apps));
|
|
905
|
+
origins.add(`https://${host}`);
|
|
906
|
+
} catch (error) {
|
|
907
|
+
if (!(error instanceof require_seeds.NoDomainForStage)) throw error;
|
|
908
|
+
unresolved.push(name);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
return {
|
|
912
|
+
origins: [...origins].sort(),
|
|
913
|
+
unresolved
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
//#endregion
|
|
917
|
+
//#region src/providers/s3/index.ts
|
|
918
|
+
function bucketConstructs(manifest) {
|
|
919
|
+
return Object.entries(manifest).filter(([, d]) => d.kind === "objects").map(([id, d]) => ({
|
|
920
|
+
id,
|
|
921
|
+
versioned: d.versioned === true,
|
|
922
|
+
servers: Object.entries(manifest).filter(([, s]) => s.kind === "file-server" && s.of === id).map(([sid, s]) => ({
|
|
923
|
+
id: sid,
|
|
924
|
+
open: s.open ?? []
|
|
925
|
+
})).sort((a, b) => a.id.localeCompare(b.id))
|
|
926
|
+
})).sort((a, b) => a.id.localeCompare(b.id));
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* The region the stage's buckets are created in: the entry's, the secrets
|
|
930
|
+
* store's when it is in AWS, the credential's, `AWS_REGION`.
|
|
931
|
+
*/
|
|
932
|
+
function s3Region(ctx, env = process.env) {
|
|
933
|
+
const store = ctx.workspace.secrets?.store;
|
|
934
|
+
const storeRegion = store && typeof store === "object" && "region" in store ? store.region : void 0;
|
|
935
|
+
const region = ctx.config.region ?? storeRegion ?? ctx.credential?.region ?? env.AWS_REGION ?? env.AWS_DEFAULT_REGION;
|
|
936
|
+
if (!region) throw new require_workspace.S3RegionRequired(ctx.stage);
|
|
937
|
+
return region;
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* The address a file server over `bucket` serves on: the bucket's regional
|
|
941
|
+
* virtual-host endpoint, which is what `FileServer.url(key)` appends a key to
|
|
942
|
+
* — `https://<bucket>.s3.<region>.amazonaws.com/<key>`. Against an emulator,
|
|
943
|
+
* its path-style address.
|
|
944
|
+
*/
|
|
945
|
+
function bucketPublicUrl(bucket, region, endpoint) {
|
|
946
|
+
if (endpoint) return `${endpoint.replace(/\/+$/, "")}/${bucket}`;
|
|
947
|
+
return `https://${bucket}.s3.${region}.amazonaws.com`;
|
|
948
|
+
}
|
|
949
|
+
/** What an AWS SDK error is called, whichever field carries it. */
|
|
950
|
+
function errorName(error) {
|
|
951
|
+
const e = error;
|
|
952
|
+
return e?.Code ?? e?.name ?? "";
|
|
953
|
+
}
|
|
954
|
+
/** The answer an SDK call failed with. */
|
|
955
|
+
function s3Answer(error) {
|
|
956
|
+
const e = error;
|
|
957
|
+
const headers = Object.fromEntries(Object.entries(e?.$response?.headers ?? {}).map(([k, v]) => [k.toLowerCase(), v]));
|
|
958
|
+
const status = e?.$metadata?.httpStatusCode ?? e?.$response?.statusCode;
|
|
959
|
+
const region = headers["x-amz-bucket-region"];
|
|
960
|
+
const requestId = e?.$metadata?.requestId ?? headers["x-amz-request-id"];
|
|
961
|
+
const id2 = e?.$metadata?.extendedRequestId ?? headers["x-amz-id-2"];
|
|
962
|
+
return {
|
|
963
|
+
...status !== void 0 ? { status } : {},
|
|
964
|
+
name: errorName(error) || "Error",
|
|
965
|
+
message: e?.message ?? String(error),
|
|
966
|
+
...region ? { region } : {},
|
|
967
|
+
...requestId ? { requestId } : {},
|
|
968
|
+
...id2 ? { id2 } : {}
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* `HTTP 403 Unknown: UnknownError [x-amz-bucket-region: eu-west-1,
|
|
973
|
+
* x-amz-request-id: …, x-amz-id-2: …]` — everything S3 said, for a message.
|
|
974
|
+
*/
|
|
975
|
+
function describeAnswer(answer) {
|
|
976
|
+
const said = answer.message && answer.message !== answer.name ? `${answer.name}: ${answer.message}` : answer.name;
|
|
977
|
+
const headers = [
|
|
978
|
+
answer.region && `x-amz-bucket-region: ${answer.region}`,
|
|
979
|
+
answer.requestId && `x-amz-request-id: ${answer.requestId}`,
|
|
980
|
+
answer.id2 && `x-amz-id-2: ${answer.id2}`
|
|
981
|
+
].filter(Boolean);
|
|
982
|
+
return `${answer.status === void 0 ? "no response" : `HTTP ${answer.status}`} ${said}` + (headers.length > 0 ? ` [${headers.join(", ")}]` : "");
|
|
983
|
+
}
|
|
984
|
+
/** `HeadBucket`: the bucket's region when it answers, else what S3 said. */
|
|
985
|
+
async function headBucket(S3, client, bucket) {
|
|
986
|
+
try {
|
|
987
|
+
const out = await client.send(new S3.HeadBucketCommand({ Bucket: bucket }));
|
|
988
|
+
return {
|
|
989
|
+
ok: true,
|
|
990
|
+
...out.BucketRegion ? { region: out.BucketRegion } : {}
|
|
991
|
+
};
|
|
992
|
+
} catch (error) {
|
|
993
|
+
return {
|
|
994
|
+
ok: false,
|
|
995
|
+
...s3Answer(error)
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
/** `GetBucketLocation`'s answer as a region: none is us-east-1, `EU` eu-west-1. */
|
|
1000
|
+
async function bucketLocation(S3, client, bucket) {
|
|
1001
|
+
try {
|
|
1002
|
+
const location = (await client.send(new S3.GetBucketLocationCommand({ Bucket: bucket }))).LocationConstraint;
|
|
1003
|
+
return { region: !location ? "us-east-1" : location === "EU" ? "eu-west-1" : location };
|
|
1004
|
+
} catch (error) {
|
|
1005
|
+
return { failed: s3Answer(error) };
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
/**
|
|
1009
|
+
* Where `bucket` is: `HeadBucket`'s `x-amz-bucket-region` — on a 200, a 301
|
|
1010
|
+
* or a 400 alike — else `GetBucketLocation`.
|
|
1011
|
+
*
|
|
1012
|
+
* @throws {BucketRegionUnknown}
|
|
1013
|
+
*/
|
|
1014
|
+
async function bucketRegion(clients, bucket) {
|
|
1015
|
+
const { S3, s3 } = clients;
|
|
1016
|
+
const head = await headBucket(S3, s3, bucket);
|
|
1017
|
+
if (head.region) return head.region;
|
|
1018
|
+
const location = await bucketLocation(S3, s3, bucket);
|
|
1019
|
+
if ("region" in location) return location.region;
|
|
1020
|
+
throw new require_workspace.BucketRegionUnknown(bucket, head.ok ? "HTTP 200 without x-amz-bucket-region" : describeAnswer(head), describeAnswer(location.failed));
|
|
1021
|
+
}
|
|
1022
|
+
/** A read whose "there is none" answer is one of `missing`. */
|
|
1023
|
+
async function readOr(read, missing) {
|
|
1024
|
+
try {
|
|
1025
|
+
return await read();
|
|
1026
|
+
} catch (error) {
|
|
1027
|
+
if (missing.includes(errorName(error))) return void 0;
|
|
1028
|
+
throw error;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
const defaultClients = async (credential, region) => {
|
|
1032
|
+
const S3 = await import("@aws-sdk/client-s3");
|
|
1033
|
+
const IAM = await import("@aws-sdk/client-iam");
|
|
1034
|
+
const config = await awsClientConfig(credential, region);
|
|
1035
|
+
const endpoint = awsEndpoint(process.env, "S3");
|
|
1036
|
+
return {
|
|
1037
|
+
S3,
|
|
1038
|
+
IAM,
|
|
1039
|
+
s3: new S3.S3Client({
|
|
1040
|
+
...config,
|
|
1041
|
+
...endpoint ? {
|
|
1042
|
+
endpoint,
|
|
1043
|
+
forcePathStyle: true
|
|
1044
|
+
} : {}
|
|
1045
|
+
}),
|
|
1046
|
+
iam: new IAM.IAMClient(config),
|
|
1047
|
+
region,
|
|
1048
|
+
...endpoint ? { endpoint } : {}
|
|
1049
|
+
};
|
|
1050
|
+
};
|
|
1051
|
+
/** For tests: what makes the clients. */
|
|
1052
|
+
let makeClients = defaultClients;
|
|
1053
|
+
async function ensureS3(ctx) {
|
|
1054
|
+
const region = s3Region(ctx);
|
|
1055
|
+
const clients = await makeClients(ctx.credential, region);
|
|
1056
|
+
for (const construct of bucketConstructs(ctx.manifest)) await ensureBucket(ctx, clients, construct);
|
|
1057
|
+
}
|
|
1058
|
+
async function ensureBucket(ctx, stageClients, construct) {
|
|
1059
|
+
const { id } = construct;
|
|
1060
|
+
const nameInput = {
|
|
1061
|
+
scope: ctx.identity.scope,
|
|
1062
|
+
stage: ctx.stage,
|
|
1063
|
+
id: require_derive.appKey(id)
|
|
1064
|
+
};
|
|
1065
|
+
const versioning = ctx.config.versioning ?? construct.versioned;
|
|
1066
|
+
const open = [...new Set(construct.servers.flatMap((s) => s.open))].sort();
|
|
1067
|
+
const located = await ensureBucketName(ctx, stageClients, id, bucketName(nameInput));
|
|
1068
|
+
const { bucket, exists } = located;
|
|
1069
|
+
let clients = stageClients;
|
|
1070
|
+
if (located.region !== stageClients.region) {
|
|
1071
|
+
clients = await makeClients(ctx.credential, located.region);
|
|
1072
|
+
ctx.log(` '${bucket}' is in ${located.region}, not ${stageClients.region}: '${id}' is reached and addressed there`);
|
|
1073
|
+
}
|
|
1074
|
+
await ensureBucketSettings(ctx, clients, {
|
|
1075
|
+
id,
|
|
1076
|
+
bucket,
|
|
1077
|
+
exists,
|
|
1078
|
+
versioning,
|
|
1079
|
+
open
|
|
1080
|
+
});
|
|
1081
|
+
const user = await ensureIamUser(ctx, clients, {
|
|
1082
|
+
id,
|
|
1083
|
+
user: iamUserName(nameInput),
|
|
1084
|
+
policyName: IAM_POLICY_NAME,
|
|
1085
|
+
policy: userPolicy(bucket, { versioning }),
|
|
1086
|
+
describe: "this bucket only"
|
|
1087
|
+
});
|
|
1088
|
+
await ensureAccessKey(ctx, clients, {
|
|
1089
|
+
id,
|
|
1090
|
+
bucket,
|
|
1091
|
+
urlKey: (0, _geekmidas_manifest.provideKey)(id, "url"),
|
|
1092
|
+
...user
|
|
1093
|
+
});
|
|
1094
|
+
const publicUrl = bucketPublicUrl(bucket, clients.region, clients.endpoint);
|
|
1095
|
+
for (const server of construct.servers) {
|
|
1096
|
+
const key = (0, _geekmidas_manifest.provideKey)(server.id, "url");
|
|
1097
|
+
const entry = {
|
|
1098
|
+
key: `file-server:${server.id}`,
|
|
1099
|
+
type: "url"
|
|
1100
|
+
};
|
|
1101
|
+
const recorded = ctx.state.record(entry.key)?.id;
|
|
1102
|
+
const current = ctx.secrets[key];
|
|
1103
|
+
if (current === publicUrl) {
|
|
1104
|
+
await ctx.state.ready(entry, publicUrl);
|
|
1105
|
+
continue;
|
|
1106
|
+
}
|
|
1107
|
+
if (current !== void 0 && current !== recorded) {
|
|
1108
|
+
ctx.log(` ${key} is set to ${current} by you, and kept — the bucket's own address is ${publicUrl}`);
|
|
1109
|
+
continue;
|
|
1110
|
+
}
|
|
1111
|
+
await ctx.change({
|
|
1112
|
+
construct: server.id,
|
|
1113
|
+
resource: key,
|
|
1114
|
+
change: `set to ${publicUrl}`
|
|
1115
|
+
}, async () => {
|
|
1116
|
+
await ctx.writeSecrets({ [key]: publicUrl });
|
|
1117
|
+
await ctx.state.ready(entry, publicUrl);
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
/**
|
|
1122
|
+
* The bucket's name: the recorded one, else the good name, else the good
|
|
1123
|
+
* name with a random suffix — up to {@link SUFFIX_ATTEMPTS} times.
|
|
1124
|
+
*/
|
|
1125
|
+
async function ensureBucketName(ctx, clients, id, good) {
|
|
1126
|
+
const { S3, s3, region } = clients;
|
|
1127
|
+
const key = `s3-bucket:${id}`;
|
|
1128
|
+
const record = ctx.state.record(key);
|
|
1129
|
+
const entry = (name, where) => ({
|
|
1130
|
+
key,
|
|
1131
|
+
type: "s3-bucket",
|
|
1132
|
+
data: {
|
|
1133
|
+
name,
|
|
1134
|
+
region: where
|
|
1135
|
+
}
|
|
1136
|
+
});
|
|
1137
|
+
/**
|
|
1138
|
+
* Whether this account holds `name`: yes, no, or someone else does. Read
|
|
1139
|
+
* by status — a `HEAD` has no body, so its error is named `Unknown` — and,
|
|
1140
|
+
* when S3 says the bucket is in another region, asked again there.
|
|
1141
|
+
*/
|
|
1142
|
+
const probe = async (name) => {
|
|
1143
|
+
let head = await headBucket(S3, s3, name);
|
|
1144
|
+
if (!head.ok && head.region && head.region !== region && (head.status === 301 || head.status === 400)) {
|
|
1145
|
+
const there = await makeClients(ctx.credential, head.region);
|
|
1146
|
+
head = await headBucket(there.S3, there.s3, name);
|
|
1147
|
+
}
|
|
1148
|
+
if (head.ok) return "ours";
|
|
1149
|
+
if (head.status === 404) return "none";
|
|
1150
|
+
if (head.status === 403) return "taken";
|
|
1151
|
+
throw new require_workspace.BucketProbeFailed(name, describeAnswer(head));
|
|
1152
|
+
};
|
|
1153
|
+
/** Where the bucket is now — read, not assumed: it may be adopted. */
|
|
1154
|
+
const located = async (name, made) => ctx.dryRun && made === "created" ? region : bucketRegion(clients, name);
|
|
1155
|
+
const create = async (name) => {
|
|
1156
|
+
if (ctx.dryRun) {
|
|
1157
|
+
const found = await probe(name);
|
|
1158
|
+
if (found === "none") {
|
|
1159
|
+
await ctx.change({
|
|
1160
|
+
construct: id,
|
|
1161
|
+
resource: `bucket ${name}`,
|
|
1162
|
+
change: `create in ${region}`
|
|
1163
|
+
}, async () => {});
|
|
1164
|
+
return "created";
|
|
1165
|
+
}
|
|
1166
|
+
return found;
|
|
1167
|
+
}
|
|
1168
|
+
await ctx.state.pending(entry(name, region));
|
|
1169
|
+
try {
|
|
1170
|
+
await s3.send(new S3.CreateBucketCommand({
|
|
1171
|
+
Bucket: name,
|
|
1172
|
+
...region === "us-east-1" ? {} : { CreateBucketConfiguration: { LocationConstraint: region } }
|
|
1173
|
+
}));
|
|
1174
|
+
} catch (error) {
|
|
1175
|
+
const code = errorName(error);
|
|
1176
|
+
if (code === "BucketAlreadyOwnedByYou") return "ours";
|
|
1177
|
+
if (code === "BucketAlreadyExists") return "taken";
|
|
1178
|
+
throw error;
|
|
1179
|
+
}
|
|
1180
|
+
await ctx.change({
|
|
1181
|
+
construct: id,
|
|
1182
|
+
resource: `bucket ${name}`,
|
|
1183
|
+
change: `create in ${region}`
|
|
1184
|
+
}, async () => {});
|
|
1185
|
+
return "created";
|
|
1186
|
+
};
|
|
1187
|
+
const adopt = async (name, where) => {
|
|
1188
|
+
const there = where === region ? clients : await makeClients(ctx.credential, where);
|
|
1189
|
+
const owner = (await readOr(() => there.s3.send(new there.S3.GetBucketTaggingCommand({ Bucket: name })), ["NoSuchTagSet", "NoSuchTagSetError"]))?.TagSet?.find((t) => t.Key === TAG_PROJECT)?.Value;
|
|
1190
|
+
if (owner && owner !== ctx.identity.key) throw new require_workspace.BucketOwnedByAnotherProject(name, owner, ctx.identity.key);
|
|
1191
|
+
};
|
|
1192
|
+
if (record?.status === "ready" && record.id) {
|
|
1193
|
+
const found = await probe(record.id);
|
|
1194
|
+
if (found === "taken") throw new require_workspace.RecordedBucketUnreachable(id, record.id);
|
|
1195
|
+
const made = found === "ours" ? "ours" : await create(record.id);
|
|
1196
|
+
if (made === "taken") throw new require_workspace.RecordedBucketUnreachable(id, record.id);
|
|
1197
|
+
const where = await located(record.id, made);
|
|
1198
|
+
if (!ctx.dryRun) await ctx.state.ready(entry(record.id, where), record.id);
|
|
1199
|
+
return {
|
|
1200
|
+
bucket: record.id,
|
|
1201
|
+
exists: made === "ours",
|
|
1202
|
+
region: where
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
const pending = record?.data?.name;
|
|
1206
|
+
const names = [
|
|
1207
|
+
...typeof pending === "string" && pending !== good ? [pending] : [],
|
|
1208
|
+
good,
|
|
1209
|
+
...Array.from({ length: 5 }, () => suffixedBucketName(good, randomSuffix()))
|
|
1210
|
+
];
|
|
1211
|
+
const tried = [];
|
|
1212
|
+
for (const name of names) {
|
|
1213
|
+
tried.push(name);
|
|
1214
|
+
const made = await create(name);
|
|
1215
|
+
if (made === "taken") continue;
|
|
1216
|
+
const where = await located(name, made);
|
|
1217
|
+
if (made === "ours") await adopt(name, where);
|
|
1218
|
+
if (!ctx.dryRun) await ctx.state.ready(entry(name, where), name);
|
|
1219
|
+
if (name !== good) ctx.log(` '${good}' is taken by another account; '${id}' is '${name}', recorded in the stage's state`);
|
|
1220
|
+
return {
|
|
1221
|
+
bucket: name,
|
|
1222
|
+
exists: made === "ours",
|
|
1223
|
+
region: where
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
throw new require_workspace.BucketNameUnavailable(id, tried.filter((n) => n !== pending));
|
|
1227
|
+
}
|
|
1228
|
+
/** Tags, Block Public Access, encryption, policy, versioning and CORS. */
|
|
1229
|
+
async function ensureBucketSettings(ctx, clients, input) {
|
|
1230
|
+
const { S3, s3 } = clients;
|
|
1231
|
+
const { id, bucket } = input;
|
|
1232
|
+
const Bucket = bucket;
|
|
1233
|
+
const fresh = ctx.dryRun && !input.exists;
|
|
1234
|
+
const read = async (fn, missing) => fresh ? void 0 : readOr(fn, missing);
|
|
1235
|
+
const set = (change, apply) => ctx.change({
|
|
1236
|
+
construct: id,
|
|
1237
|
+
resource: `bucket ${bucket}`,
|
|
1238
|
+
change
|
|
1239
|
+
}, () => apply().then(() => void 0));
|
|
1240
|
+
const tagged = await read(() => s3.send(new S3.GetBucketTaggingCommand({ Bucket })), ["NoSuchTagSet", "NoSuchTagSetError"]);
|
|
1241
|
+
const ours = identityTags(ctx, id);
|
|
1242
|
+
const others = (tagged?.TagSet ?? []).filter((t) => !ours.some((o) => o.Key === t.Key));
|
|
1243
|
+
if (ours.some((o) => !tagged?.TagSet?.some((t) => t.Key === o.Key && t.Value === o.Value))) await set("tag", () => s3.send(new S3.PutBucketTaggingCommand({
|
|
1244
|
+
Bucket,
|
|
1245
|
+
Tagging: { TagSet: [...others, ...ours].map((t) => ({
|
|
1246
|
+
Key: t.Key,
|
|
1247
|
+
Value: t.Value
|
|
1248
|
+
})) }
|
|
1249
|
+
})));
|
|
1250
|
+
const isOpen = input.open.length > 0;
|
|
1251
|
+
const block = publicAccessBlock(isOpen);
|
|
1252
|
+
const ensureBlock = async () => {
|
|
1253
|
+
const now = (await read(() => s3.send(new S3.GetPublicAccessBlockCommand({ Bucket })), ["NoSuchPublicAccessBlockConfiguration"]))?.PublicAccessBlockConfiguration;
|
|
1254
|
+
if (!(now && Object.keys(block).every((k) => (now[k] ?? false) === block[k]))) await set(isOpen ? "block public access (ACLs; the policy opens the open paths)" : "block all public access", () => s3.send(new S3.PutPublicAccessBlockCommand({
|
|
1255
|
+
Bucket,
|
|
1256
|
+
PublicAccessBlockConfiguration: block
|
|
1257
|
+
})));
|
|
1258
|
+
};
|
|
1259
|
+
const policy = bucketPolicy(bucket, input.open);
|
|
1260
|
+
const ensurePolicy = async () => {
|
|
1261
|
+
const current = await read(() => s3.send(new S3.GetBucketPolicyCommand({ Bucket })), ["NoSuchBucketPolicy"]);
|
|
1262
|
+
if (!(current?.Policy !== void 0 && canonicalPolicy(JSON.parse(current.Policy)) === canonicalPolicy(policy))) await set(isOpen ? `policy: TLS only, public read on ${input.open.join(", ")}` : "policy: TLS only", () => s3.send(new S3.PutBucketPolicyCommand({
|
|
1263
|
+
Bucket,
|
|
1264
|
+
Policy: JSON.stringify(policy)
|
|
1265
|
+
})));
|
|
1266
|
+
};
|
|
1267
|
+
if (isOpen) {
|
|
1268
|
+
await ensureBlock();
|
|
1269
|
+
await ensurePolicy();
|
|
1270
|
+
} else {
|
|
1271
|
+
await ensurePolicy();
|
|
1272
|
+
await ensureBlock();
|
|
1273
|
+
}
|
|
1274
|
+
if ((await read(() => s3.send(new S3.GetBucketEncryptionCommand({ Bucket })), ["ServerSideEncryptionConfigurationNotFoundError"]))?.ServerSideEncryptionConfiguration?.Rules?.[0]?.ApplyServerSideEncryptionByDefault?.SSEAlgorithm !== "AES256" && !fresh) await set("encrypt by default (SSE-S3)", () => s3.send(new S3.PutBucketEncryptionCommand({
|
|
1275
|
+
Bucket,
|
|
1276
|
+
ServerSideEncryptionConfiguration: { Rules: [{ ApplyServerSideEncryptionByDefault: { SSEAlgorithm: "AES256" } }] }
|
|
1277
|
+
})));
|
|
1278
|
+
const status = (await read(() => s3.send(new S3.GetBucketVersioningCommand({ Bucket })), []))?.Status;
|
|
1279
|
+
if (input.versioning && status !== "Enabled") await set("enable versioning", () => s3.send(new S3.PutBucketVersioningCommand({
|
|
1280
|
+
Bucket,
|
|
1281
|
+
VersioningConfiguration: { Status: "Enabled" }
|
|
1282
|
+
})));
|
|
1283
|
+
else if (!input.versioning && status === "Enabled") await set("suspend versioning", () => s3.send(new S3.PutBucketVersioningCommand({
|
|
1284
|
+
Bucket,
|
|
1285
|
+
VersioningConfiguration: { Status: "Suspended" }
|
|
1286
|
+
})));
|
|
1287
|
+
const { origins, unresolved } = bucketOrigins({
|
|
1288
|
+
workspace: ctx.workspace,
|
|
1289
|
+
manifest: ctx.manifest,
|
|
1290
|
+
runnables: ctx.runnables,
|
|
1291
|
+
stage: ctx.stage,
|
|
1292
|
+
bucket: id
|
|
1293
|
+
});
|
|
1294
|
+
if (unresolved.length > 0) ctx.log(` ${unresolved.join(", ")} ${unresolved.length === 1 ? "has" : "have"} no address on '${ctx.stage}' (domains.${ctx.stage}), so ${unresolved.length === 1 ? "is" : "are"} not in '${bucket}''s CORS`);
|
|
1295
|
+
if (origins.length === 0) return;
|
|
1296
|
+
const rule = corsRule(origins);
|
|
1297
|
+
const cors = await read(() => s3.send(new S3.GetBucketCorsCommand({ Bucket })), ["NoSuchCORSConfiguration"]);
|
|
1298
|
+
if (!(cors?.CORSRules?.length === 1 && canonicalCors(cors.CORSRules[0]) === canonicalCors(rule))) await set(`CORS for ${origins.join(", ")}`, () => s3.send(new S3.PutBucketCorsCommand({
|
|
1299
|
+
Bucket,
|
|
1300
|
+
CORSConfiguration: { CORSRules: [rule] }
|
|
1301
|
+
})));
|
|
1302
|
+
}
|
|
1303
|
+
function canonicalCors(rule) {
|
|
1304
|
+
const sorted = (list) => [...list ?? []].sort();
|
|
1305
|
+
return JSON.stringify({
|
|
1306
|
+
AllowedMethods: sorted(rule.AllowedMethods),
|
|
1307
|
+
AllowedOrigins: sorted(rule.AllowedOrigins),
|
|
1308
|
+
AllowedHeaders: sorted(rule.AllowedHeaders),
|
|
1309
|
+
ExposeHeaders: sorted(rule.ExposeHeaders),
|
|
1310
|
+
MaxAgeSeconds: rule.MaxAgeSeconds ?? null
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
/**
|
|
1314
|
+
* Each provisioned bucket answers `HeadBucket` to the key in the stage's
|
|
1315
|
+
* secrets — the one the apps will use. A bucket whose URL carries no key is
|
|
1316
|
+
* not one this provider wrote, and the deploy's own check covers a URL that
|
|
1317
|
+
* is missing.
|
|
1318
|
+
*
|
|
1319
|
+
* A failure is read by its status, never its name — a `HEAD` has no body,
|
|
1320
|
+
* so SDK v3 calls a 301, 400 or 403 `Unknown` — and the message carries
|
|
1321
|
+
* everything S3 said: status, error, region, request ids.
|
|
1322
|
+
*
|
|
1323
|
+
* @throws {BucketRegionMismatch} the bucket is in another region than its URL says
|
|
1324
|
+
* @throws {ProvisionedBucketUnreachable}
|
|
1325
|
+
*/
|
|
1326
|
+
async function verifyS3(ctx) {
|
|
1327
|
+
const S3 = await import("@aws-sdk/client-s3");
|
|
1328
|
+
for (const { id } of bucketConstructs(ctx.manifest)) {
|
|
1329
|
+
const urlKey = (0, _geekmidas_manifest.provideKey)(id, "url");
|
|
1330
|
+
const url = ctx.secrets[urlKey];
|
|
1331
|
+
if (url === void 0) continue;
|
|
1332
|
+
const address = _geekmidas_storage_s3_url.parse(url);
|
|
1333
|
+
if (!address.accessKeyId || !address.secretAccessKey) continue;
|
|
1334
|
+
const asked = address.region ?? ctx.config.region ?? "us-east-1";
|
|
1335
|
+
const client = new S3.S3Client({
|
|
1336
|
+
region: asked,
|
|
1337
|
+
credentials: {
|
|
1338
|
+
accessKeyId: address.accessKeyId,
|
|
1339
|
+
secretAccessKey: address.secretAccessKey
|
|
1340
|
+
},
|
|
1341
|
+
...address.endpoint ? { endpoint: address.endpoint } : {},
|
|
1342
|
+
...address.forcePathStyle ? { forcePathStyle: true } : {}
|
|
1343
|
+
});
|
|
1344
|
+
try {
|
|
1345
|
+
const issuedAt = issuedJustNow(ctx.resources, id, address.accessKeyId);
|
|
1346
|
+
const { answer: head, waited } = await whileKeyIsNew(await headBucket(S3, client, address.bucket), {
|
|
1347
|
+
issuedAt,
|
|
1348
|
+
refused: (answer) => !answer.ok && answer.status === 403,
|
|
1349
|
+
ask: () => headBucket(S3, client, address.bucket),
|
|
1350
|
+
onWait: () => ctx.log?.(` ${id}: waiting for the new key ${address.accessKeyId} to become active…`)
|
|
1351
|
+
});
|
|
1352
|
+
if (head.ok) continue;
|
|
1353
|
+
const fail = (reason) => new require_workspace.ProvisionedBucketUnreachable(id, address.bucket, ctx.stage, reason);
|
|
1354
|
+
const said = describeAnswer(head);
|
|
1355
|
+
if (head.status === 301 || head.status === 400 && head.region && head.region !== asked) {
|
|
1356
|
+
let where = head.region;
|
|
1357
|
+
let located = "";
|
|
1358
|
+
if (!where) {
|
|
1359
|
+
const location = await bucketLocation(S3, client, address.bucket);
|
|
1360
|
+
if ("region" in location) where = location.region;
|
|
1361
|
+
else located = `; GetBucketLocation answered ${describeAnswer(location.failed)}`;
|
|
1362
|
+
}
|
|
1363
|
+
if (where && where !== asked) throw new require_workspace.BucketRegionMismatch(id, address.bucket, ctx.stage, urlKey, where, address.region, asked, said);
|
|
1364
|
+
throw fail(`S3 redirected the request (${said}${located})`);
|
|
1365
|
+
}
|
|
1366
|
+
if (head.status === 404) throw fail(`it does not exist (${said})`);
|
|
1367
|
+
if (head.status === 403) throw fail(`the key is refused — AccessDenied, or an invalid or not-yet-active key (${said})` + (issuedAt ? `. The key was issued at ${issuedAt} by this deploy, and IAM keys take seconds to become usable: it was still refused after waiting ${Math.round(waited / 1e3)}s` : ""));
|
|
1368
|
+
throw fail(said);
|
|
1369
|
+
} finally {
|
|
1370
|
+
client.destroy();
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
//#endregion
|
|
1375
|
+
//#region src/providers/registry.ts
|
|
1376
|
+
/**
|
|
1377
|
+
* The providers gkm ships, by kind and name — registered the way a deploy
|
|
1378
|
+
* target's built-ins are (`target/builtins.ts`). A provider from a package is
|
|
1379
|
+
* one more entry here once there is a loading story for it.
|
|
1380
|
+
*/
|
|
1381
|
+
const BUILTIN_PROVIDERS = { objects: { s3: {
|
|
1382
|
+
kind: "objects",
|
|
1383
|
+
name: "s3",
|
|
1384
|
+
check: require_workspace.checkS3Config,
|
|
1385
|
+
provisioning: require_workspace.S3_PROVISIONING,
|
|
1386
|
+
credentials: awsProvisioningCredentials,
|
|
1387
|
+
runtimeKeys(manifest) {
|
|
1388
|
+
return bucketConstructs(manifest).flatMap((b) => [(0, _geekmidas_manifest.provideKey)(b.id, "url"), ...b.servers.map((s) => (0, _geekmidas_manifest.provideKey)(s.id, "url"))]);
|
|
1389
|
+
},
|
|
1390
|
+
ensure: ensureS3,
|
|
1391
|
+
verify: verifyS3
|
|
1392
|
+
} } };
|
|
1393
|
+
/** The provider `kind` names `name`, if gkm has one. */
|
|
1394
|
+
function builtinProvider(kind, name) {
|
|
1395
|
+
return Object.hasOwn(BUILTIN_PROVIDERS[kind], name) ? BUILTIN_PROVIDERS[kind][name] : void 0;
|
|
1396
|
+
}
|
|
1397
|
+
//#endregion
|
|
1398
|
+
//#region src/providers/index.ts
|
|
1399
|
+
/** The constructs of a kind: `objects` is every bucket. */
|
|
1400
|
+
function constructsOf(manifest, kind) {
|
|
1401
|
+
return Object.entries(manifest).filter(([, d]) => d.kind === kind).map(([id]) => id).sort();
|
|
1402
|
+
}
|
|
1403
|
+
/**
|
|
1404
|
+
* What every deploy runs: each provider's `verify()` — the cheap check that
|
|
1405
|
+
* what it created is still there and the stage's key reaches it. Nothing on
|
|
1406
|
+
* the local stage, and nothing for a kind on `external` or `false` — a kind
|
|
1407
|
+
* the stage has none of is the deploy's readiness check's to refuse.
|
|
1408
|
+
*/
|
|
1409
|
+
async function verifyStageProviders(input) {
|
|
1410
|
+
const verified = [];
|
|
1411
|
+
for (const kind of require_seeds.PROVIDER_KINDS) {
|
|
1412
|
+
if (constructsOf(input.manifest, kind).length === 0) continue;
|
|
1413
|
+
const choice = require_workspace.stageProvider(input.workspace, kind, input.stage);
|
|
1414
|
+
if (choice.mode !== "provider") continue;
|
|
1415
|
+
const provider = builtinProvider(kind, choice.name);
|
|
1416
|
+
await provider.verify({
|
|
1417
|
+
workspace: input.workspace,
|
|
1418
|
+
manifest: input.manifest,
|
|
1419
|
+
stage: input.stage,
|
|
1420
|
+
config: choice.config,
|
|
1421
|
+
secrets: input.secrets,
|
|
1422
|
+
...input.resources ? { resources: input.resources } : {},
|
|
1423
|
+
...input.log ? { log: input.log } : {}
|
|
1424
|
+
});
|
|
1425
|
+
verified.push(`${kind}: ${provider.name}`);
|
|
1426
|
+
}
|
|
1427
|
+
if (await verifyStageBackups(input)) verified.push("backups: s3");
|
|
1428
|
+
return verified;
|
|
1429
|
+
}
|
|
1430
|
+
/**
|
|
1431
|
+
* A stage whose provider creates its resources, and no credentials to create
|
|
1432
|
+
* them with. Raised before anything is created, built or started.
|
|
1433
|
+
*/
|
|
1434
|
+
var ProviderCredentialsMissing = class extends require_errors.GkmError {
|
|
1435
|
+
stage;
|
|
1436
|
+
kind;
|
|
1437
|
+
provider;
|
|
1438
|
+
ids;
|
|
1439
|
+
env;
|
|
1440
|
+
describe;
|
|
1441
|
+
login;
|
|
1442
|
+
constructor(stage, kind, provider, ids, env, describe, login) {
|
|
1443
|
+
super(`deploy.${kind}.${stage} is ${provider}, so the deploy creates what ${ids.join(", ")} ${ids.length === 1 ? "is" : "are"} backed by on '${stage}' and writes ${ids.length === 1 ? "its key" : "their keys"} into the stage's secrets — and it found no ${provider} credentials to do it with. On this machine, supply ${describe}` + (login ? `, or run gkm login --provider ${login}` : "") + `. In CI, the step that runs the deploy needs them: the '${stage}' environment's role (AWS_ROLE_ARN, from gkm deploy:github), assumed before that step. With the keys set by hand instead, set deploy.${kind}.${stage} to 'external'.`);
|
|
1444
|
+
this.stage = stage;
|
|
1445
|
+
this.kind = kind;
|
|
1446
|
+
this.provider = provider;
|
|
1447
|
+
this.ids = ids;
|
|
1448
|
+
this.env = env;
|
|
1449
|
+
this.describe = describe;
|
|
1450
|
+
this.login = login;
|
|
1451
|
+
this.name = "ProviderCredentialsMissing";
|
|
1452
|
+
}
|
|
1453
|
+
};
|
|
1454
|
+
/**
|
|
1455
|
+
* `ensure()` for every provider the stage configures — find or create, fix
|
|
1456
|
+
* drift, record in the stage's state, write the runtime keys into its
|
|
1457
|
+
* secrets. Run by the deploy, which holds the stage's lock. A provider with
|
|
1458
|
+
* no provisioning credentials stops the run with
|
|
1459
|
+
* {@link ProviderCredentialsMissing} before anything is created.
|
|
1460
|
+
*/
|
|
1461
|
+
async function provisionStage(options) {
|
|
1462
|
+
const { workspace, stage, manifest } = options;
|
|
1463
|
+
const dryRun = options.dryRun === true;
|
|
1464
|
+
const env = options.env ?? process.env;
|
|
1465
|
+
const log = options.log ?? ((line) => console.log(line));
|
|
1466
|
+
const runnables = options.runnables ?? {};
|
|
1467
|
+
const reports = [];
|
|
1468
|
+
const configured = [];
|
|
1469
|
+
for (const kind of require_seeds.PROVIDER_KINDS) {
|
|
1470
|
+
const choice = require_workspace.stageProvider(workspace, kind, stage);
|
|
1471
|
+
if (choice.mode !== "provider") {
|
|
1472
|
+
reports.push({
|
|
1473
|
+
kind,
|
|
1474
|
+
mode: choice.mode,
|
|
1475
|
+
actions: [],
|
|
1476
|
+
planned: []
|
|
1477
|
+
});
|
|
1478
|
+
continue;
|
|
1479
|
+
}
|
|
1480
|
+
const provider = builtinProvider(kind, choice.name);
|
|
1481
|
+
const report = {
|
|
1482
|
+
kind,
|
|
1483
|
+
mode: "provider",
|
|
1484
|
+
provider: provider.name,
|
|
1485
|
+
actions: [],
|
|
1486
|
+
planned: []
|
|
1487
|
+
};
|
|
1488
|
+
reports.push(report);
|
|
1489
|
+
const ids = constructsOf(manifest, kind);
|
|
1490
|
+
if (ids.length === 0) continue;
|
|
1491
|
+
const credential = await provider.credentials({
|
|
1492
|
+
stage,
|
|
1493
|
+
env,
|
|
1494
|
+
...options.profile ? { profile: options.profile } : {},
|
|
1495
|
+
...options.home ? { home: options.home } : {}
|
|
1496
|
+
});
|
|
1497
|
+
if (credential === void 0) throw new ProviderCredentialsMissing(stage, kind, provider.name, ids, provider.provisioning.env, provider.provisioning.describe, provider.provisioning.login);
|
|
1498
|
+
configured.push({
|
|
1499
|
+
kind,
|
|
1500
|
+
provider,
|
|
1501
|
+
config: choice.config,
|
|
1502
|
+
credential,
|
|
1503
|
+
report
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
const backups = require_dns.stageBackups(workspace, manifest, stage);
|
|
1507
|
+
if (configured.length === 0 && backups.mode !== "on") {
|
|
1508
|
+
if (backups.mode === "disabled") log(`💾 backups: off — deploy.backups.${stage} is false. Nothing is backed up, and nothing already backed up is deleted.`);
|
|
1509
|
+
return reports;
|
|
1510
|
+
}
|
|
1511
|
+
const identity = require_identity.deployIdentity(workspace, stage);
|
|
1512
|
+
let secrets = (await options.secrets.read())?.custom ?? {};
|
|
1513
|
+
for (const entry of configured) entry.report.planned = entry.provider.runtimeKeys(manifest).filter((key) => secrets[key] === void 0);
|
|
1514
|
+
const state = dryRun ? await readOnlyState(options.state, stage) : journalState(await require_seeds.DeployJournal.open(options.state, stage, () => require_dns.deploysWithCompose(workspace) ? require_seeds.createComposeState(stage) : require_seeds.createEmptyState(stage, "", "")));
|
|
1515
|
+
const writeSecrets = async (values) => {
|
|
1516
|
+
if (dryRun) return;
|
|
1517
|
+
const stored = await options.secrets.read() ?? require_seeds.initStageSecrets(stage);
|
|
1518
|
+
const next = {
|
|
1519
|
+
...stored,
|
|
1520
|
+
custom: {
|
|
1521
|
+
...stored.custom,
|
|
1522
|
+
...values
|
|
1523
|
+
},
|
|
1524
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1525
|
+
};
|
|
1526
|
+
await options.secrets.write(next);
|
|
1527
|
+
secrets = next.custom;
|
|
1528
|
+
};
|
|
1529
|
+
for (const entry of configured) {
|
|
1530
|
+
log(`☁️ ${entry.kind}: ${entry.provider.name}${dryRun ? " (dry run)" : ""}`);
|
|
1531
|
+
await entry.provider.ensure({
|
|
1532
|
+
workspace,
|
|
1533
|
+
manifest,
|
|
1534
|
+
runnables,
|
|
1535
|
+
stage,
|
|
1536
|
+
identity,
|
|
1537
|
+
config: entry.config,
|
|
1538
|
+
credential: entry.credential,
|
|
1539
|
+
get secrets() {
|
|
1540
|
+
return secrets;
|
|
1541
|
+
},
|
|
1542
|
+
state,
|
|
1543
|
+
dryRun,
|
|
1544
|
+
rotateKeys: options.rotateKeys === true,
|
|
1545
|
+
retireOldKeys: options.retireOldKeys === true,
|
|
1546
|
+
async change(action, apply) {
|
|
1547
|
+
if (!dryRun) await apply();
|
|
1548
|
+
entry.report.actions.push(action);
|
|
1549
|
+
log(` ${dryRun ? "would " : ""}${action.change} — ${action.resource}`);
|
|
1550
|
+
},
|
|
1551
|
+
writeSecrets,
|
|
1552
|
+
log
|
|
1553
|
+
});
|
|
1554
|
+
if (entry.report.actions.length === 0) log(" up to date");
|
|
1555
|
+
}
|
|
1556
|
+
if (backups.mode === "on" || backups.mode === "disabled") {
|
|
1557
|
+
const report = {
|
|
1558
|
+
kind: "backups",
|
|
1559
|
+
mode: backups.mode === "on" ? "provider" : "none",
|
|
1560
|
+
actions: [],
|
|
1561
|
+
planned: backups.mode === "on" && secrets["BACKUPS_URL"] === void 0 ? [require_dns.BACKUPS_URL_KEY] : []
|
|
1562
|
+
};
|
|
1563
|
+
reports.push(report);
|
|
1564
|
+
await ensureStageBackups({
|
|
1565
|
+
workspace,
|
|
1566
|
+
manifest,
|
|
1567
|
+
stage,
|
|
1568
|
+
...options.profile ? { profile: options.profile } : {},
|
|
1569
|
+
...options.home ? { home: options.home } : {},
|
|
1570
|
+
env,
|
|
1571
|
+
ctx: {
|
|
1572
|
+
identity,
|
|
1573
|
+
stage,
|
|
1574
|
+
state,
|
|
1575
|
+
dryRun,
|
|
1576
|
+
get secrets() {
|
|
1577
|
+
return secrets;
|
|
1578
|
+
},
|
|
1579
|
+
rotateKeys: options.rotateKeys === true,
|
|
1580
|
+
retireOldKeys: options.retireOldKeys === true,
|
|
1581
|
+
async change(action, apply) {
|
|
1582
|
+
if (!dryRun) await apply();
|
|
1583
|
+
report.actions.push(action);
|
|
1584
|
+
log(` ${dryRun ? "would " : ""}${action.change} — ${action.resource}`);
|
|
1585
|
+
},
|
|
1586
|
+
writeSecrets,
|
|
1587
|
+
log
|
|
1588
|
+
}
|
|
1589
|
+
});
|
|
1590
|
+
if (backups.mode === "on" && report.actions.length === 0) log(" up to date");
|
|
1591
|
+
}
|
|
1592
|
+
return reports;
|
|
1593
|
+
}
|
|
1594
|
+
/** The journal as `ensure()` sees it. */
|
|
1595
|
+
function journalState(journal) {
|
|
1596
|
+
return {
|
|
1597
|
+
record: (key) => journal.record(key),
|
|
1598
|
+
pending: (entry) => journal.pending(entry),
|
|
1599
|
+
ready: (entry, id) => journal.ready(entry, id),
|
|
1600
|
+
lastDeployedAt: journal.existed ? journal.state.lastDeployedAt : void 0
|
|
1601
|
+
};
|
|
1602
|
+
}
|
|
1603
|
+
/** What a dry run reads, and the writes it does not make. */
|
|
1604
|
+
async function readOnlyState(store, stage) {
|
|
1605
|
+
const stored = await store.read(stage);
|
|
1606
|
+
return {
|
|
1607
|
+
record: (key) => stored?.resources[key],
|
|
1608
|
+
pending: async () => {},
|
|
1609
|
+
ready: async () => {},
|
|
1610
|
+
...stored ? { lastDeployedAt: stored.state.lastDeployedAt } : {}
|
|
1611
|
+
};
|
|
1612
|
+
}
|
|
1613
|
+
//#endregion
|
|
1614
|
+
//#region src/secrets/stale.ts
|
|
1615
|
+
/**
|
|
1616
|
+
* Stored secrets an older gkm generated that now shadow a derived value.
|
|
1617
|
+
*
|
|
1618
|
+
* Before constructs, initialising a stage wrote an address for each app into
|
|
1619
|
+
* its custom secrets: `<APP>_DATABASE_URL` as a `postgresql://…@localhost`
|
|
1620
|
+
* URL with a password of its own, and `http://localhost:<port>` for a site or
|
|
1621
|
+
* an auth server. Those keys are now a construct's — `database.schema('AuthDatabase')`
|
|
1622
|
+
* provides `AUTH_DATABASE_URL`, `new BetterAuth('Auth')` provides `AUTH_URL`
|
|
1623
|
+
* — and a deploy derives them. A value the stage holds is a value set by hand,
|
|
1624
|
+
* and set by hand wins: the container is handed `localhost`, where nothing
|
|
1625
|
+
* answers, with a password no role has.
|
|
1626
|
+
*
|
|
1627
|
+
* So a deploy refuses such a stage rather than run it, naming each key and
|
|
1628
|
+
* the command that removes it. Nothing is deleted for the user: the value is
|
|
1629
|
+
* theirs, and only they can say it is stale. A URL that names a real host — a
|
|
1630
|
+
* managed database — is a deliberate choice and still wins.
|
|
1631
|
+
*/
|
|
1632
|
+
/** The kinds whose URL an older gkm wrote into a stage's secrets. */
|
|
1633
|
+
const ADDRESSED = /* @__PURE__ */ new Set([
|
|
1634
|
+
"database",
|
|
1635
|
+
"database-reader",
|
|
1636
|
+
"database-schema",
|
|
1637
|
+
"rest-api",
|
|
1638
|
+
"site"
|
|
1639
|
+
]);
|
|
1640
|
+
/** Hosts that are this machine — never anything, inside a container. */
|
|
1641
|
+
const LOOPBACK = /* @__PURE__ */ new Set([
|
|
1642
|
+
"localhost",
|
|
1643
|
+
"127.0.0.1",
|
|
1644
|
+
"[::1]",
|
|
1645
|
+
"::1"
|
|
1646
|
+
]);
|
|
1647
|
+
/**
|
|
1648
|
+
* A stage holds a construct's address as a custom secret, pointing at
|
|
1649
|
+
* `localhost` — what an older `gkm secrets:init`, `gkm setup` or `gkm init`
|
|
1650
|
+
* generated. Set by hand wins over derived, so the app would be handed it.
|
|
1651
|
+
*/
|
|
1652
|
+
var StaleStageSecrets = class extends require_errors.GkmError {
|
|
1653
|
+
stage;
|
|
1654
|
+
stale;
|
|
1655
|
+
constructor(stage, stale) {
|
|
1656
|
+
const one = stale.length === 1;
|
|
1657
|
+
super(`The stage '${stage}' holds ${one ? "a value" : "values"} an older gkm generated for ${one ? "an address a construct" : "addresses constructs"} now ${one ? "provides" : "provide"}:\n` + stale.map((s) => ` ${s.key}: ${s.construct}'s, stored pointing at ${s.host}`).join("\n") + `
|
|
1658
|
+
A stored value wins over the derived one, so the app would be handed an address where nothing answers inside its container. Remove ${one ? "it" : "each"}, and the construct's own value is used:\n` + stale.map((s) => ` gkm secrets:unset ${s.key} --stage ${stage}`).join("\n") + "\nA managed service set with its real host is not affected.");
|
|
1659
|
+
this.stage = stage;
|
|
1660
|
+
this.stale = stale;
|
|
1661
|
+
this.name = "StaleStageSecrets";
|
|
1662
|
+
}
|
|
1663
|
+
};
|
|
1664
|
+
/** The host a stored value names, if it is a URL. */
|
|
1665
|
+
function hostOf(value) {
|
|
1666
|
+
try {
|
|
1667
|
+
return new URL(value).hostname;
|
|
1668
|
+
} catch {
|
|
1669
|
+
return;
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
/**
|
|
1673
|
+
* Each stored key that a construct provides an address for, holding an
|
|
1674
|
+
* address on this machine.
|
|
1675
|
+
*/
|
|
1676
|
+
function staleStageSecrets(manifest, supplied) {
|
|
1677
|
+
const stale = [];
|
|
1678
|
+
for (const [id, declaration] of Object.entries(manifest)) {
|
|
1679
|
+
if (!ADDRESSED.has(declaration.kind)) continue;
|
|
1680
|
+
const key = (0, _geekmidas_manifest.providedKeyFor)(id, declaration.kind, "url");
|
|
1681
|
+
const value = supplied[key];
|
|
1682
|
+
if (value === void 0) continue;
|
|
1683
|
+
const host = hostOf(value);
|
|
1684
|
+
if (host === void 0 || !LOOPBACK.has(host)) continue;
|
|
1685
|
+
stale.push({
|
|
1686
|
+
key,
|
|
1687
|
+
construct: id,
|
|
1688
|
+
host
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1691
|
+
return stale.sort((a, b) => a.key.localeCompare(b.key));
|
|
1692
|
+
}
|
|
1693
|
+
/**
|
|
1694
|
+
* Refuse a stage whose stored secrets would shadow a construct's address with
|
|
1695
|
+
* `localhost`.
|
|
1696
|
+
*
|
|
1697
|
+
* @throws {StaleStageSecrets} naming each key and the command removing it
|
|
1698
|
+
*/
|
|
1699
|
+
function assertNoStaleSecrets(options) {
|
|
1700
|
+
const stale = staleStageSecrets(options.manifest, options.supplied);
|
|
1701
|
+
if (stale.length > 0) throw new StaleStageSecrets(options.stage, stale);
|
|
1702
|
+
}
|
|
1703
|
+
//#endregion
|
|
1704
|
+
//#region src/deploy/readiness.ts
|
|
1705
|
+
/**
|
|
1706
|
+
* Whether a stage is ready to be deployed: the one check, run once by every
|
|
1707
|
+
* deploy through a server target — after the stage's providers have created
|
|
1708
|
+
* what they create and written its keys, and before the target touches
|
|
1709
|
+
* anything.
|
|
1710
|
+
*
|
|
1711
|
+
* A build is not a deploy. Images carry no stage's keys, so a build-only run
|
|
1712
|
+
* (`gkm compose --build --push`) never calls this: a key the deploy itself
|
|
1713
|
+
* writes — a provider's bucket URL — is not there yet when CI builds, and
|
|
1714
|
+
* must not stop the build.
|
|
1715
|
+
*/
|
|
1716
|
+
/**
|
|
1717
|
+
* The stage has every key it needs, read from its secrets as the providers
|
|
1718
|
+
* left them:
|
|
1719
|
+
*
|
|
1720
|
+
* 1. no kind it declares is one the stage set to `false`;
|
|
1721
|
+
* 2. its mail and storage are its own — or a dev service, where allowed,
|
|
1722
|
+
* reported once, loudly;
|
|
1723
|
+
* 3. what its providers created is still there, and its key reaches it;
|
|
1724
|
+
* 4. no key holds an address an older gkm stored for what a construct now
|
|
1725
|
+
* provides;
|
|
1726
|
+
* 5. each third party's credentials match their construct's schema.
|
|
1727
|
+
*
|
|
1728
|
+
* The local stage derives its mail and storage, so only 4 and 5 apply to it.
|
|
1729
|
+
*
|
|
1730
|
+
* @throws {StageProviderDisabled}
|
|
1731
|
+
* @throws {ExternalServicesNotConfigured} naming every missing key at once
|
|
1732
|
+
* @throws {StaleStageSecrets}
|
|
1733
|
+
* @throws {CredentialsInvalid}
|
|
1734
|
+
*/
|
|
1735
|
+
async function assertStageReady(ctx) {
|
|
1736
|
+
const { workspace, manifest, stage } = ctx;
|
|
1737
|
+
const supplied = (await ctx.secrets.read())?.custom ?? {};
|
|
1738
|
+
if (stage !== workspace.stages.local) {
|
|
1739
|
+
require_seeds.assertStageProvidersEnabled(workspace, manifest, stage);
|
|
1740
|
+
const domain = workspace.domains?.[stage];
|
|
1741
|
+
const services = require_seeds.externalServices({
|
|
1742
|
+
stage,
|
|
1743
|
+
declarations: require_seeds.stageServiceDeclarations({
|
|
1744
|
+
manifest,
|
|
1745
|
+
...ctx.runnables ? { runnables: ctx.runnables } : {}
|
|
1746
|
+
}),
|
|
1747
|
+
supplied,
|
|
1748
|
+
allow: ctx.allowDevServices,
|
|
1749
|
+
...domain ? { domain } : {},
|
|
1750
|
+
providers: require_seeds.stageProviderNotes(workspace, stage)
|
|
1751
|
+
});
|
|
1752
|
+
if (services.missing.length > 0) throw new require_seeds.ExternalServicesNotConfigured(stage, services.missing);
|
|
1753
|
+
require_seeds.reportDevServices(ctx, require_seeds.devServicesUsed(services));
|
|
1754
|
+
const verified = await verifyStageProviders({
|
|
1755
|
+
workspace,
|
|
1756
|
+
manifest,
|
|
1757
|
+
stage,
|
|
1758
|
+
secrets: supplied,
|
|
1759
|
+
resources: (await ctx.state.read(stage))?.resources ?? {},
|
|
1760
|
+
log: (line) => ctx.logger.info(line)
|
|
1761
|
+
});
|
|
1762
|
+
for (const line of verified) ctx.logger.info(`✓ ${line} verified`);
|
|
1763
|
+
}
|
|
1764
|
+
assertNoStaleSecrets({
|
|
1765
|
+
manifest,
|
|
1766
|
+
stage,
|
|
1767
|
+
supplied
|
|
1768
|
+
});
|
|
1769
|
+
await require_credentialSchemas.assertStageCredentials({
|
|
1770
|
+
root: workspace.root,
|
|
1771
|
+
patterns: require_seeds.constructGlobs(workspace),
|
|
1772
|
+
manifest,
|
|
1773
|
+
stage,
|
|
1774
|
+
supplied
|
|
1775
|
+
});
|
|
1776
|
+
}
|
|
1777
|
+
//#endregion
|
|
1778
|
+
//#region src/deploy/orchestrate.ts
|
|
1779
|
+
/**
|
|
1780
|
+
* The run around a target: what every deploy does whatever it deploys
|
|
1781
|
+
* through.
|
|
1782
|
+
*
|
|
1783
|
+
* It loads the workspace, resolves the target, works out the apps and the
|
|
1784
|
+
* identity, takes the stage's lock, and calls the target's phases in order,
|
|
1785
|
+
* reporting each as it starts, finishes or fails. Everything that is about
|
|
1786
|
+
* *where* the stage goes is the target's.
|
|
1787
|
+
*/
|
|
1788
|
+
/** Apps were asked for by name that the workspace does not have. */
|
|
1789
|
+
var UnknownDeployApps = class extends require_errors.GkmError {
|
|
1790
|
+
apps;
|
|
1791
|
+
available;
|
|
1792
|
+
constructor(apps, available) {
|
|
1793
|
+
super(`Unknown apps: ${apps.join(", ")}\nAvailable apps: ${available.join(", ")}`);
|
|
1794
|
+
this.apps = apps;
|
|
1795
|
+
this.available = available;
|
|
1796
|
+
this.name = "UnknownDeployApps";
|
|
1797
|
+
}
|
|
1798
|
+
};
|
|
1799
|
+
/** Every app asked for deploys through some other target. */
|
|
1800
|
+
var NoDeployableApps = class extends require_errors.GkmError {
|
|
1801
|
+
stage;
|
|
1802
|
+
target;
|
|
1803
|
+
constructor(stage, target) {
|
|
1804
|
+
super(`No apps to deploy through "${target}": every selected app deploys somewhere else. Pick the target with --target, or set each app's deploy in gkm.config.ts.`);
|
|
1805
|
+
this.stage = stage;
|
|
1806
|
+
this.target = target;
|
|
1807
|
+
this.name = "NoDeployableApps";
|
|
1808
|
+
}
|
|
1809
|
+
};
|
|
1810
|
+
/** A build-only run through a target with no build phase. */
|
|
1811
|
+
var TargetBuildsNothing = class extends require_errors.GkmError {
|
|
1812
|
+
target;
|
|
1813
|
+
constructor(target) {
|
|
1814
|
+
super(`"${target}" has no build phase, so a build-only run would do nothing. Deploy it in full, or build through a target that builds images (compose).`);
|
|
1815
|
+
this.target = target;
|
|
1816
|
+
this.name = "TargetBuildsNothing";
|
|
1817
|
+
}
|
|
1818
|
+
};
|
|
1819
|
+
/** A failed rollback, after the failure that called for it. */
|
|
1820
|
+
var RollbackFailed = class extends Error {
|
|
1821
|
+
target;
|
|
1822
|
+
failure;
|
|
1823
|
+
constructor(target, failure, cause) {
|
|
1824
|
+
super(`Deploying through "${target}" failed, and so did rolling it back: ${cause instanceof Error ? cause.message : String(cause)}. The stage may be part-released; deploy again, or roll it back by hand. The first failure: ${failure instanceof Error ? failure.message : String(failure)}`, { cause });
|
|
1825
|
+
this.target = target;
|
|
1826
|
+
this.failure = failure;
|
|
1827
|
+
this.name = "RollbackFailed";
|
|
1828
|
+
}
|
|
1829
|
+
};
|
|
1830
|
+
/**
|
|
1831
|
+
* Generate image tag from stage and timestamp
|
|
1832
|
+
*/
|
|
1833
|
+
function generateTag(stage) {
|
|
1834
|
+
return `${stage}-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19)}`;
|
|
1835
|
+
}
|
|
1836
|
+
/** Why an app that deploys through another target is left out. */
|
|
1837
|
+
function elsewhere(target, stage) {
|
|
1838
|
+
const instead = require_define.builtinTarget(target)?.instead?.(stage);
|
|
1839
|
+
if (instead) return `it deploys through "${target}" — ${instead}`;
|
|
1840
|
+
return `it deploys through "${target}" — run \`gkm deploy --target ${target} --stage ${stage}\``;
|
|
1841
|
+
}
|
|
1842
|
+
/**
|
|
1843
|
+
* Deploy a workspace's stage through its target.
|
|
1844
|
+
*
|
|
1845
|
+
* The engine under `deploy()` and `gkm deploy`. It never prompts and never
|
|
1846
|
+
* exits: credentials come from `ctx.credentials`, progress goes to
|
|
1847
|
+
* `ctx.emit` and to `output`, and every failure is thrown.
|
|
1848
|
+
*
|
|
1849
|
+
* @internal
|
|
1850
|
+
*/
|
|
1851
|
+
async function runDeploy(source, request, ctx) {
|
|
1852
|
+
const { stage } = request;
|
|
1853
|
+
const redactor = ctx.redactor ?? new Redactor();
|
|
1854
|
+
const emit = (event) => ctx.emit(event.type === "log" ? {
|
|
1855
|
+
...event,
|
|
1856
|
+
message: redactor.redact(event.message)
|
|
1857
|
+
} : event);
|
|
1858
|
+
/** Run one phase between its events. */
|
|
1859
|
+
const phase = async (name, body) => {
|
|
1860
|
+
ctx.signal?.throwIfAborted();
|
|
1861
|
+
emit({
|
|
1862
|
+
type: "phase.started",
|
|
1863
|
+
phase: name
|
|
1864
|
+
});
|
|
1865
|
+
try {
|
|
1866
|
+
const value = await body();
|
|
1867
|
+
emit({
|
|
1868
|
+
type: "phase.finished",
|
|
1869
|
+
phase: name
|
|
1870
|
+
});
|
|
1871
|
+
return value;
|
|
1872
|
+
} catch (error) {
|
|
1873
|
+
emit({
|
|
1874
|
+
type: "phase.failed",
|
|
1875
|
+
phase: name,
|
|
1876
|
+
error: require_sniffer.eventError(error)
|
|
1877
|
+
});
|
|
1878
|
+
throw error;
|
|
1879
|
+
}
|
|
1880
|
+
};
|
|
1881
|
+
ctx.signal?.throwIfAborted();
|
|
1882
|
+
emit({
|
|
1883
|
+
type: "phase.started",
|
|
1884
|
+
phase: "validate"
|
|
1885
|
+
});
|
|
1886
|
+
let prepared;
|
|
1887
|
+
try {
|
|
1888
|
+
prepared = await prepare(source, request, ctx, emit, redactor);
|
|
1889
|
+
} catch (error) {
|
|
1890
|
+
emit({
|
|
1891
|
+
type: "phase.failed",
|
|
1892
|
+
phase: "validate",
|
|
1893
|
+
error: require_sniffer.eventError(error)
|
|
1894
|
+
});
|
|
1895
|
+
throw error;
|
|
1896
|
+
}
|
|
1897
|
+
const { resolved, phaseCtx, store } = prepared;
|
|
1898
|
+
const { target } = resolved;
|
|
1899
|
+
const resources = !ctx.buildOnly && phaseCtx.stage !== phaseCtx.workspace.stages.local;
|
|
1900
|
+
const provision = async () => {
|
|
1901
|
+
if (!request.skipDns && resolved.target.runtime === "server") try {
|
|
1902
|
+
await require_dns.dnsCredentials(phaseCtx.stage, require_dns.stageDnsDomains(phaseCtx.stage, phaseCtx.workspace.domains, phaseCtx.workspace.dns), ctx.credentials);
|
|
1903
|
+
} catch (error) {
|
|
1904
|
+
emit({
|
|
1905
|
+
type: "phase.failed",
|
|
1906
|
+
phase: "validate",
|
|
1907
|
+
error: require_sniffer.eventError(error)
|
|
1908
|
+
});
|
|
1909
|
+
throw error;
|
|
1910
|
+
}
|
|
1911
|
+
await provisionResources(phaseCtx, request, ctx, store, emit);
|
|
1912
|
+
};
|
|
1913
|
+
if (ctx.dryRun) {
|
|
1914
|
+
if (resources) await provision();
|
|
1915
|
+
const run = await validate(phaseCtx, emit, () => readyToDeploy(target, phaseCtx));
|
|
1916
|
+
await phase("plan", () => target.plan(phaseCtx, run));
|
|
1917
|
+
return target.result(phaseCtx, run);
|
|
1918
|
+
}
|
|
1919
|
+
if (ctx.buildOnly) {
|
|
1920
|
+
if (!target.build) throw new TargetBuildsNothing(resolved.name);
|
|
1921
|
+
const run = await validate(phaseCtx, emit, () => target.validate(phaseCtx));
|
|
1922
|
+
await phase("build", () => target.build(phaseCtx, run));
|
|
1923
|
+
return target.result(phaseCtx, run);
|
|
1924
|
+
}
|
|
1925
|
+
let lock;
|
|
1926
|
+
try {
|
|
1927
|
+
require_sniffer.assertStateOutlivesRun(phaseCtx.workspace, stage, "deploy");
|
|
1928
|
+
lock = await store.lock(stage, { operation: "deploy" });
|
|
1929
|
+
} catch (error) {
|
|
1930
|
+
emit({
|
|
1931
|
+
type: "phase.failed",
|
|
1932
|
+
phase: "validate",
|
|
1933
|
+
error: require_sniffer.eventError(error)
|
|
1934
|
+
});
|
|
1935
|
+
throw error;
|
|
1936
|
+
}
|
|
1937
|
+
try {
|
|
1938
|
+
try {
|
|
1939
|
+
await require_sniffer.assertStateRoom(store, stage);
|
|
1940
|
+
} catch (error) {
|
|
1941
|
+
emit({
|
|
1942
|
+
type: "phase.failed",
|
|
1943
|
+
phase: "validate",
|
|
1944
|
+
error: require_sniffer.eventError(error)
|
|
1945
|
+
});
|
|
1946
|
+
throw error;
|
|
1947
|
+
}
|
|
1948
|
+
if (resources) await provision();
|
|
1949
|
+
const run = await validate(phaseCtx, emit, () => readyToDeploy(target, phaseCtx));
|
|
1950
|
+
if (target.provision) await phase("provision", () => target.provision(phaseCtx, run));
|
|
1951
|
+
if (target.build) await phase("build", () => target.build(phaseCtx, run));
|
|
1952
|
+
let releasing = "release";
|
|
1953
|
+
try {
|
|
1954
|
+
await phase("release", () => target.release(phaseCtx, run));
|
|
1955
|
+
releasing = "verify";
|
|
1956
|
+
if (target.verify) await phase("verify", () => target.verify(phaseCtx, run));
|
|
1957
|
+
} catch (error) {
|
|
1958
|
+
if (ctx.signal?.aborted || !target.capabilities.rollback) throw error;
|
|
1959
|
+
if (!target.rollback) throw error;
|
|
1960
|
+
try {
|
|
1961
|
+
await phase("rollback", () => target.rollback(phaseCtx, run, {
|
|
1962
|
+
phase: releasing,
|
|
1963
|
+
error
|
|
1964
|
+
}));
|
|
1965
|
+
} catch (rollbackError) {
|
|
1966
|
+
throw new RollbackFailed(resolved.name, error, rollbackError);
|
|
1967
|
+
}
|
|
1968
|
+
throw error;
|
|
1969
|
+
}
|
|
1970
|
+
return await target.result(phaseCtx, run);
|
|
1971
|
+
} finally {
|
|
1972
|
+
await lock.release();
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
/**
|
|
1976
|
+
* The stage's providers, run inside the validate phase. A dry run plans them
|
|
1977
|
+
* and, so the checks after it see the stage as the run would leave it, reads
|
|
1978
|
+
* the keys they would write as placeholders.
|
|
1979
|
+
*/
|
|
1980
|
+
async function provisionResources(phaseCtx, request, ctx, store, emit) {
|
|
1981
|
+
try {
|
|
1982
|
+
const reports = await provisionStage({
|
|
1983
|
+
workspace: phaseCtx.workspace,
|
|
1984
|
+
stage: phaseCtx.stage,
|
|
1985
|
+
manifest: phaseCtx.manifest,
|
|
1986
|
+
runnables: phaseCtx.runnables ?? {},
|
|
1987
|
+
secrets: phaseCtx.secrets,
|
|
1988
|
+
state: store,
|
|
1989
|
+
dryRun: ctx.dryRun,
|
|
1990
|
+
...request.rotateKeys ? { rotateKeys: true } : {},
|
|
1991
|
+
...request.retireOldKeys ? { retireOldKeys: true } : {},
|
|
1992
|
+
...ctx.home ? { home: ctx.home } : {},
|
|
1993
|
+
log: (line) => phaseCtx.logger.info(line)
|
|
1994
|
+
});
|
|
1995
|
+
if (!ctx.dryRun) return;
|
|
1996
|
+
const planned = reports.flatMap((r) => r.planned);
|
|
1997
|
+
if (planned.length === 0) return;
|
|
1998
|
+
const secrets = phaseCtx.secrets;
|
|
1999
|
+
phaseCtx.secrets = {
|
|
2000
|
+
...secrets,
|
|
2001
|
+
async read() {
|
|
2002
|
+
const stored = await secrets.read() ?? require_seeds.initStageSecrets(phaseCtx.stage);
|
|
2003
|
+
const custom = { ...stored.custom };
|
|
2004
|
+
for (const key of planned) custom[key] ??= PLANNED_BY_THE_DEPLOY;
|
|
2005
|
+
return {
|
|
2006
|
+
...stored,
|
|
2007
|
+
custom
|
|
2008
|
+
};
|
|
2009
|
+
}
|
|
2010
|
+
};
|
|
2011
|
+
} catch (error) {
|
|
2012
|
+
emit({
|
|
2013
|
+
type: "phase.failed",
|
|
2014
|
+
phase: "validate",
|
|
2015
|
+
error: require_sniffer.eventError(error)
|
|
2016
|
+
});
|
|
2017
|
+
throw error;
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
/** What a dry run reads for a key its providers would write. */
|
|
2021
|
+
const PLANNED_BY_THE_DEPLOY = "s3://planned-by-this-deploy";
|
|
2022
|
+
/**
|
|
2023
|
+
* What a deploy validates: the stage ready to be deployed — every key it
|
|
2024
|
+
* needs, read as its providers left them — then what the target would run,
|
|
2025
|
+
* then whatever only the target checks. A build-only run validates the
|
|
2026
|
+
* target's run alone: it calls neither readiness check.
|
|
2027
|
+
*/
|
|
2028
|
+
async function readyToDeploy(target, phaseCtx) {
|
|
2029
|
+
if (target.runtime === "server") await assertStageReady(phaseCtx);
|
|
2030
|
+
const run = await target.validate(phaseCtx);
|
|
2031
|
+
await target.ready?.(phaseCtx, run);
|
|
2032
|
+
return run;
|
|
2033
|
+
}
|
|
2034
|
+
/** `body`, then the run's `deploy.started`, inside the validate phase. */
|
|
2035
|
+
async function validate(phaseCtx, emit, body) {
|
|
2036
|
+
try {
|
|
2037
|
+
const run = await body();
|
|
2038
|
+
const skipped = new Set(phaseCtx.skipped.map((s) => s.app));
|
|
2039
|
+
emit({
|
|
2040
|
+
type: "deploy.started",
|
|
2041
|
+
stage: phaseCtx.stage,
|
|
2042
|
+
target: phaseCtx.target,
|
|
2043
|
+
identity: phaseCtx.identity.key,
|
|
2044
|
+
tag: phaseCtx.tag,
|
|
2045
|
+
apps: phaseCtx.apps.filter((app) => !skipped.has(app)),
|
|
2046
|
+
dryRun: phaseCtx.dryRun
|
|
2047
|
+
});
|
|
2048
|
+
emit({
|
|
2049
|
+
type: "phase.finished",
|
|
2050
|
+
phase: "validate"
|
|
2051
|
+
});
|
|
2052
|
+
return run;
|
|
2053
|
+
} catch (error) {
|
|
2054
|
+
emit({
|
|
2055
|
+
type: "phase.failed",
|
|
2056
|
+
phase: "validate",
|
|
2057
|
+
error: require_sniffer.eventError(error)
|
|
2058
|
+
});
|
|
2059
|
+
throw error;
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
/** Everything the phases are handed, worked out before the first of them. */
|
|
2063
|
+
async function prepare(source, request, ctx, emit, redactor) {
|
|
2064
|
+
const { stage } = request;
|
|
2065
|
+
const configured = typeof source === "function" ? await source() : source;
|
|
2066
|
+
const local = stage === configured.stages.local;
|
|
2067
|
+
if (!local) require_workspace.assertDeployedStage(configured.stages, stage);
|
|
2068
|
+
const targetName = request.target ?? configured.deploy?.default ?? "dokploy";
|
|
2069
|
+
const resolved = await require_resolve.resolveTarget(targetName, {
|
|
2070
|
+
workspace: configured,
|
|
2071
|
+
stage,
|
|
2072
|
+
...ctx.targets ? { host: ctx.targets } : {}
|
|
2073
|
+
});
|
|
2074
|
+
if (local && !resolved.target.capabilities.localStage) require_workspace.assertDeployedStage(configured.stages, stage);
|
|
2075
|
+
const options = await require_resolve.parseTargetOptions(resolved);
|
|
2076
|
+
const allowDevServices = request.allowDevServices === true;
|
|
2077
|
+
if (allowDevServices && resolved.target.runtime === "aws") throw new require_seeds.DevServicesNeedServerTarget(targetName);
|
|
2078
|
+
const runnables = {};
|
|
2079
|
+
const background = {};
|
|
2080
|
+
const manifest = await require_discover.discover({
|
|
2081
|
+
patterns: require_seeds.constructGlobs(configured),
|
|
2082
|
+
cwd: configured.root,
|
|
2083
|
+
runnables,
|
|
2084
|
+
background
|
|
2085
|
+
});
|
|
2086
|
+
require_workspace.resolveStageTelemetry({
|
|
2087
|
+
...configured.deploy?.telemetry ? { telemetry: configured.deploy.telemetry } : {},
|
|
2088
|
+
stage,
|
|
2089
|
+
local,
|
|
2090
|
+
target: targetName,
|
|
2091
|
+
runtime: resolved.target.runtime,
|
|
2092
|
+
selfHosted: resolved.target.capabilities.selfHostedTelemetry === true,
|
|
2093
|
+
used: require_seeds.usesTelemetry(manifest)
|
|
2094
|
+
});
|
|
2095
|
+
const units = require_derive.derivedApps(manifest, configured);
|
|
2096
|
+
const workspace = Object.keys(units).length > 0 ? {
|
|
2097
|
+
...configured,
|
|
2098
|
+
apps: units
|
|
2099
|
+
} : configured;
|
|
2100
|
+
const identity = require_identity.deployIdentity(workspace, stage);
|
|
2101
|
+
const tag = request.tag ?? (resolved.target.tag ? await resolved.target.tag({
|
|
2102
|
+
cwd: workspace.root,
|
|
2103
|
+
stage
|
|
2104
|
+
}) : generateTag(stage));
|
|
2105
|
+
const buildOrder = require_workspace.getAppBuildOrder(workspace);
|
|
2106
|
+
let selected = buildOrder;
|
|
2107
|
+
if (request.apps && request.apps.length > 0) {
|
|
2108
|
+
const unknown = request.apps.filter((name) => !workspace.apps[name]);
|
|
2109
|
+
if (unknown.length > 0) throw new UnknownDeployApps(unknown, Object.keys(workspace.apps));
|
|
2110
|
+
selected = buildOrder.filter((name) => request.apps.includes(name));
|
|
2111
|
+
}
|
|
2112
|
+
const skipped = [];
|
|
2113
|
+
const skip = (app, reason) => {
|
|
2114
|
+
skipped.push({
|
|
2115
|
+
app,
|
|
2116
|
+
reason
|
|
2117
|
+
});
|
|
2118
|
+
emit({
|
|
2119
|
+
type: "app.skipped",
|
|
2120
|
+
app,
|
|
2121
|
+
reason
|
|
2122
|
+
});
|
|
2123
|
+
};
|
|
2124
|
+
const byDefault = configured.deploy?.default ?? "dokploy";
|
|
2125
|
+
const apps = selected.filter((name) => {
|
|
2126
|
+
const own = workspace.apps[name].resolvedDeployTarget;
|
|
2127
|
+
const deploysThrough = own === byDefault ? targetName : own;
|
|
2128
|
+
if (deploysThrough === targetName) return true;
|
|
2129
|
+
skip(name, elsewhere(deploysThrough, stage));
|
|
2130
|
+
return false;
|
|
2131
|
+
});
|
|
2132
|
+
if (apps.length === 0) throw new NoDeployableApps(stage, targetName);
|
|
2133
|
+
const store = await require_sniffer.createStateStore({
|
|
2134
|
+
config: stage === workspace.stages.local ? void 0 : workspace.state,
|
|
2135
|
+
workspaceRoot: workspace.root,
|
|
2136
|
+
workspaceName: workspace.name,
|
|
2137
|
+
namespace: workspace.deploy?.namespace
|
|
2138
|
+
});
|
|
2139
|
+
const secrets = await deploySecrets(workspace, stage, redactor, { ...ctx.home ? { home: ctx.home } : {} });
|
|
2140
|
+
const line = (level) => (message) => emit({
|
|
2141
|
+
type: "log",
|
|
2142
|
+
level,
|
|
2143
|
+
message
|
|
2144
|
+
});
|
|
2145
|
+
const logger = {
|
|
2146
|
+
info: line("info"),
|
|
2147
|
+
warn: line("warn"),
|
|
2148
|
+
error: line("error")
|
|
2149
|
+
};
|
|
2150
|
+
return {
|
|
2151
|
+
resolved,
|
|
2152
|
+
phaseCtx: {
|
|
2153
|
+
target: targetName,
|
|
2154
|
+
cwd: workspace.root,
|
|
2155
|
+
stage,
|
|
2156
|
+
tag,
|
|
2157
|
+
tagGiven: request.tag !== void 0,
|
|
2158
|
+
apps,
|
|
2159
|
+
skipped,
|
|
2160
|
+
identity,
|
|
2161
|
+
workspace,
|
|
2162
|
+
manifest,
|
|
2163
|
+
runnables,
|
|
2164
|
+
background,
|
|
2165
|
+
options,
|
|
2166
|
+
dryRun: ctx.dryRun,
|
|
2167
|
+
atomic: request.atomic ?? false,
|
|
2168
|
+
allowDevServices,
|
|
2169
|
+
...request.skipDns ? { skipDns: true } : {},
|
|
2170
|
+
credentials: ctx.credentials,
|
|
2171
|
+
state: store,
|
|
2172
|
+
secrets,
|
|
2173
|
+
logger,
|
|
2174
|
+
signal: ctx.signal ?? new AbortController().signal,
|
|
2175
|
+
childOutput: ctx.childOutput ?? "inherit",
|
|
2176
|
+
emit,
|
|
2177
|
+
skip,
|
|
2178
|
+
name: (resource) => require_identity.applicationName(identity, resource)
|
|
2179
|
+
},
|
|
2180
|
+
store
|
|
2181
|
+
};
|
|
2182
|
+
}
|
|
2183
|
+
//#endregion
|
|
2184
|
+
//#region src/deploy/deploy.ts
|
|
2185
|
+
/**
|
|
2186
|
+
* `deploy()`: a deploy as a library call.
|
|
2187
|
+
*
|
|
2188
|
+
* `gkm deploy` could only be driven from an interactive terminal: it prompted
|
|
2189
|
+
* for what it lacked and exited the process on failure, worked out of
|
|
2190
|
+
* `process.cwd()`, and reported progress by printing. Here the caller says
|
|
2191
|
+
* where the project is, hands over the credentials and a signal, and gets back
|
|
2192
|
+
* a run — events to iterate while it goes, and a structured result at the end.
|
|
2193
|
+
* Nothing below this asks a person anything or ends the process; `gkm deploy`
|
|
2194
|
+
* is a thin wrapper that does both.
|
|
2195
|
+
*/
|
|
2196
|
+
/**
|
|
2197
|
+
* Every event a run emits, kept so that each iteration — however late it
|
|
2198
|
+
* starts — sees the run from its first event to its last.
|
|
2199
|
+
*/
|
|
2200
|
+
var EventLog = class {
|
|
2201
|
+
events = [];
|
|
2202
|
+
closed = false;
|
|
2203
|
+
waiting = [];
|
|
2204
|
+
push(event) {
|
|
2205
|
+
this.events.push(event);
|
|
2206
|
+
this.wake();
|
|
2207
|
+
}
|
|
2208
|
+
close() {
|
|
2209
|
+
this.closed = true;
|
|
2210
|
+
this.wake();
|
|
2211
|
+
}
|
|
2212
|
+
wake() {
|
|
2213
|
+
const waiting = this.waiting;
|
|
2214
|
+
this.waiting = [];
|
|
2215
|
+
for (const resume of waiting) resume();
|
|
2216
|
+
}
|
|
2217
|
+
async *iterate() {
|
|
2218
|
+
let next = 0;
|
|
2219
|
+
while (true) if (next < this.events.length) yield this.events[next++];
|
|
2220
|
+
else if (this.closed) return;
|
|
2221
|
+
else await new Promise((resume) => this.waiting.push(resume));
|
|
2222
|
+
}
|
|
2223
|
+
};
|
|
2224
|
+
function forward(logger, level, message) {
|
|
2225
|
+
if (!logger) return;
|
|
2226
|
+
if (level === "info") logger.info(message);
|
|
2227
|
+
else if (level === "error" && logger.error) logger.error(message);
|
|
2228
|
+
else logger.warn(message);
|
|
2229
|
+
}
|
|
2230
|
+
/**
|
|
2231
|
+
* Deploy a project's stage.
|
|
2232
|
+
*
|
|
2233
|
+
* ```ts
|
|
2234
|
+
* import { deploy } from '@geekmidas/cli/deploy';
|
|
2235
|
+
*
|
|
2236
|
+
* const run = deploy({ cwd: '/srv/checkouts/shop', stage: 'production' });
|
|
2237
|
+
* for await (const event of run) {
|
|
2238
|
+
* if (event.type === 'app.deployed') console.log(event.app, event.url);
|
|
2239
|
+
* }
|
|
2240
|
+
* const result = await run.result;
|
|
2241
|
+
* ```
|
|
2242
|
+
*
|
|
2243
|
+
* The run starts at once; iterating it is optional, and an iteration started
|
|
2244
|
+
* late still sees every event from the first.
|
|
2245
|
+
*/
|
|
2246
|
+
function deploy(input) {
|
|
2247
|
+
const log = new EventLog();
|
|
2248
|
+
const redactor = new Redactor();
|
|
2249
|
+
const emit = (event) => {
|
|
2250
|
+
const masked = event.type === "log" ? {
|
|
2251
|
+
...event,
|
|
2252
|
+
message: redactor.redact(event.message)
|
|
2253
|
+
} : event;
|
|
2254
|
+
log.push(masked);
|
|
2255
|
+
if (masked.type === "log") forward(input.logger, masked.level, masked.message);
|
|
2256
|
+
};
|
|
2257
|
+
const ctx = {
|
|
2258
|
+
emit,
|
|
2259
|
+
credentials: input.credentials ?? require_credentials.storedCredentials({ home: input.home ?? require_run.gkmHome() }),
|
|
2260
|
+
dryRun: input.dryRun ?? false,
|
|
2261
|
+
...input.buildOnly ? { buildOnly: true } : {},
|
|
2262
|
+
home: input.home ?? require_run.gkmHome(),
|
|
2263
|
+
redactor,
|
|
2264
|
+
...input.signal ? { signal: input.signal } : {},
|
|
2265
|
+
...input.childOutput ? { childOutput: input.childOutput } : {},
|
|
2266
|
+
...input.targets ? { targets: input.targets } : {}
|
|
2267
|
+
};
|
|
2268
|
+
const result = require_discover.withOutput((level, message) => emit({
|
|
2269
|
+
type: "log",
|
|
2270
|
+
level,
|
|
2271
|
+
message
|
|
2272
|
+
}), async () => {
|
|
2273
|
+
try {
|
|
2274
|
+
input.signal?.throwIfAborted();
|
|
2275
|
+
const sandbox = input.sandbox ?? new require_local.LocalSandbox({ root: require_config.findWorkspaceRoot((0, node_path.resolve)(input.cwd)) });
|
|
2276
|
+
const deployed = await require_sandbox.withSandbox(sandbox, () => runDeploy(() => load(input, sandbox), {
|
|
2277
|
+
stage: input.stage,
|
|
2278
|
+
...input.target ? { target: input.target } : {},
|
|
2279
|
+
...input.tag ? { tag: input.tag } : {},
|
|
2280
|
+
...input.apps ? { apps: input.apps } : {},
|
|
2281
|
+
...input.atomic ? { atomic: true } : {},
|
|
2282
|
+
...input.allowDevServices ? { allowDevServices: input.allowDevServices } : {},
|
|
2283
|
+
...input.skipDns ? { skipDns: true } : {},
|
|
2284
|
+
...input.rotateKeys ? { rotateKeys: true } : {},
|
|
2285
|
+
...input.retireOldKeys ? { retireOldKeys: true } : {}
|
|
2286
|
+
}, ctx));
|
|
2287
|
+
emit({
|
|
2288
|
+
type: "deploy.finished",
|
|
2289
|
+
result: deployed
|
|
2290
|
+
});
|
|
2291
|
+
return deployed;
|
|
2292
|
+
} catch (error) {
|
|
2293
|
+
emit({
|
|
2294
|
+
type: "deploy.failed",
|
|
2295
|
+
error: require_sniffer.eventError(error)
|
|
2296
|
+
});
|
|
2297
|
+
throw error;
|
|
2298
|
+
} finally {
|
|
2299
|
+
log.close();
|
|
2300
|
+
}
|
|
2301
|
+
});
|
|
2302
|
+
result.catch(() => {});
|
|
2303
|
+
return {
|
|
2304
|
+
result,
|
|
2305
|
+
[Symbol.asyncIterator]: () => log.iterate()
|
|
2306
|
+
};
|
|
2307
|
+
}
|
|
2308
|
+
/**
|
|
2309
|
+
* The workspace at `input.cwd`. The stage is checked by the run, once it
|
|
2310
|
+
* knows the target: one that runs on this machine may take the local stage.
|
|
2311
|
+
*/
|
|
2312
|
+
async function load(input, sandbox) {
|
|
2313
|
+
const { workspace } = await require_config.loadWorkspaceConfig((0, node_path.resolve)(input.cwd), { sandbox });
|
|
2314
|
+
return workspace;
|
|
2315
|
+
}
|
|
2316
|
+
//#endregion
|
|
2317
|
+
Object.defineProperty(exports, "IAM_USER_PATH", {
|
|
2318
|
+
enumerable: true,
|
|
2319
|
+
get: function() {
|
|
2320
|
+
return IAM_USER_PATH;
|
|
2321
|
+
}
|
|
2322
|
+
});
|
|
2323
|
+
Object.defineProperty(exports, "NoDeployableApps", {
|
|
2324
|
+
enumerable: true,
|
|
2325
|
+
get: function() {
|
|
2326
|
+
return NoDeployableApps;
|
|
2327
|
+
}
|
|
2328
|
+
});
|
|
2329
|
+
Object.defineProperty(exports, "RollbackFailed", {
|
|
2330
|
+
enumerable: true,
|
|
2331
|
+
get: function() {
|
|
2332
|
+
return RollbackFailed;
|
|
2333
|
+
}
|
|
2334
|
+
});
|
|
2335
|
+
Object.defineProperty(exports, "TargetBuildsNothing", {
|
|
2336
|
+
enumerable: true,
|
|
2337
|
+
get: function() {
|
|
2338
|
+
return TargetBuildsNothing;
|
|
2339
|
+
}
|
|
2340
|
+
});
|
|
2341
|
+
Object.defineProperty(exports, "UnknownDeployApps", {
|
|
2342
|
+
enumerable: true,
|
|
2343
|
+
get: function() {
|
|
2344
|
+
return UnknownDeployApps;
|
|
2345
|
+
}
|
|
2346
|
+
});
|
|
2347
|
+
Object.defineProperty(exports, "awsClientConfig", {
|
|
2348
|
+
enumerable: true,
|
|
2349
|
+
get: function() {
|
|
2350
|
+
return awsClientConfig;
|
|
2351
|
+
}
|
|
2352
|
+
});
|
|
2353
|
+
Object.defineProperty(exports, "awsEndpoint", {
|
|
2354
|
+
enumerable: true,
|
|
2355
|
+
get: function() {
|
|
2356
|
+
return awsEndpoint;
|
|
2357
|
+
}
|
|
2358
|
+
});
|
|
2359
|
+
Object.defineProperty(exports, "awsProvisioningCredentials", {
|
|
2360
|
+
enumerable: true,
|
|
2361
|
+
get: function() {
|
|
2362
|
+
return awsProvisioningCredentials;
|
|
2363
|
+
}
|
|
2364
|
+
});
|
|
2365
|
+
Object.defineProperty(exports, "backupsUserName", {
|
|
2366
|
+
enumerable: true,
|
|
2367
|
+
get: function() {
|
|
2368
|
+
return backupsUserName;
|
|
2369
|
+
}
|
|
2370
|
+
});
|
|
2371
|
+
Object.defineProperty(exports, "bucketName", {
|
|
2372
|
+
enumerable: true,
|
|
2373
|
+
get: function() {
|
|
2374
|
+
return bucketName;
|
|
2375
|
+
}
|
|
2376
|
+
});
|
|
2377
|
+
Object.defineProperty(exports, "deploy", {
|
|
2378
|
+
enumerable: true,
|
|
2379
|
+
get: function() {
|
|
2380
|
+
return deploy;
|
|
2381
|
+
}
|
|
2382
|
+
});
|
|
2383
|
+
Object.defineProperty(exports, "iamUserName", {
|
|
2384
|
+
enumerable: true,
|
|
2385
|
+
get: function() {
|
|
2386
|
+
return iamUserName;
|
|
2387
|
+
}
|
|
2388
|
+
});
|
|
2389
|
+
|
|
2390
|
+
//# sourceMappingURL=deploy-CFvq8daZ.cjs.map
|