@alchemy.run/fly 0.0.0 → 2.0.0-beta.85
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/LICENSE +201 -0
- package/README.md +57 -4
- package/lib/App.d.ts +230 -0
- package/lib/App.d.ts.map +1 -0
- package/lib/App.js +338 -0
- package/lib/App.js.map +1 -0
- package/lib/AuthProvider.d.ts +22 -0
- package/lib/AuthProvider.d.ts.map +1 -0
- package/lib/AuthProvider.js +48 -0
- package/lib/AuthProvider.js.map +1 -0
- package/lib/BindService.d.ts +159 -0
- package/lib/BindService.d.ts.map +1 -0
- package/lib/BindService.js +222 -0
- package/lib/BindService.js.map +1 -0
- package/lib/Bootstrap.d.ts +8 -0
- package/lib/Bootstrap.d.ts.map +1 -0
- package/lib/Bootstrap.js +41 -0
- package/lib/Bootstrap.js.map +1 -0
- package/lib/Bucket.d.ts +278 -0
- package/lib/Bucket.d.ts.map +1 -0
- package/lib/Bucket.js +658 -0
- package/lib/Bucket.js.map +1 -0
- package/lib/BucketBinding.d.ts +33 -0
- package/lib/BucketBinding.d.ts.map +1 -0
- package/lib/BucketBinding.js +70 -0
- package/lib/BucketBinding.js.map +1 -0
- package/lib/Catalog.d.ts +28 -0
- package/lib/Catalog.d.ts.map +1 -0
- package/lib/Catalog.js +31 -0
- package/lib/Catalog.js.map +1 -0
- package/lib/Certificate.d.ts +189 -0
- package/lib/Certificate.d.ts.map +1 -0
- package/lib/Certificate.js +380 -0
- package/lib/Certificate.js.map +1 -0
- package/lib/Checkpoint.d.ts +55 -0
- package/lib/Checkpoint.d.ts.map +1 -0
- package/lib/Checkpoint.js +3 -0
- package/lib/Checkpoint.js.map +1 -0
- package/lib/CheckpointHttp.d.ts +22 -0
- package/lib/CheckpointHttp.d.ts.map +1 -0
- package/lib/CheckpointHttp.js +53 -0
- package/lib/CheckpointHttp.js.map +1 -0
- package/lib/ConnectPostgres.d.ts +58 -0
- package/lib/ConnectPostgres.d.ts.map +1 -0
- package/lib/ConnectPostgres.js +13 -0
- package/lib/ConnectPostgres.js.map +1 -0
- package/lib/ConnectPostgresHttp.d.ts +26 -0
- package/lib/ConnectPostgresHttp.d.ts.map +1 -0
- package/lib/ConnectPostgresHttp.js +57 -0
- package/lib/ConnectPostgresHttp.js.map +1 -0
- package/lib/Contributions.d.ts +7 -0
- package/lib/Contributions.d.ts.map +1 -0
- package/lib/Contributions.js +6 -0
- package/lib/Contributions.js.map +1 -0
- package/lib/Credentials.d.ts +33 -0
- package/lib/Credentials.d.ts.map +1 -0
- package/lib/Credentials.js +55 -0
- package/lib/Credentials.js.map +1 -0
- package/lib/Decrypt.d.ts +52 -0
- package/lib/Decrypt.d.ts.map +1 -0
- package/lib/Decrypt.js +3 -0
- package/lib/Decrypt.js.map +1 -0
- package/lib/DecryptHttp.d.ts +22 -0
- package/lib/DecryptHttp.d.ts.map +1 -0
- package/lib/DecryptHttp.js +43 -0
- package/lib/DecryptHttp.js.map +1 -0
- package/lib/DeleteObject.d.ts +29 -0
- package/lib/DeleteObject.d.ts.map +1 -0
- package/lib/DeleteObject.js +3 -0
- package/lib/DeleteObject.js.map +1 -0
- package/lib/DeleteObjectHttp.d.ts +12 -0
- package/lib/DeleteObjectHttp.d.ts.map +1 -0
- package/lib/DeleteObjectHttp.js +17 -0
- package/lib/DeleteObjectHttp.js.map +1 -0
- package/lib/Deployment.d.ts +51 -0
- package/lib/Deployment.d.ts.map +1 -0
- package/lib/Deployment.js +62 -0
- package/lib/Deployment.js.map +1 -0
- package/lib/DeploymentImages.d.ts +18 -0
- package/lib/DeploymentImages.d.ts.map +1 -0
- package/lib/DeploymentImages.js +61 -0
- package/lib/DeploymentImages.js.map +1 -0
- package/lib/DeploymentState.d.ts +17 -0
- package/lib/DeploymentState.d.ts.map +1 -0
- package/lib/DeploymentState.js +87 -0
- package/lib/DeploymentState.js.map +1 -0
- package/lib/Encrypt.d.ts +50 -0
- package/lib/Encrypt.d.ts.map +1 -0
- package/lib/Encrypt.js +3 -0
- package/lib/Encrypt.js.map +1 -0
- package/lib/EncryptHttp.d.ts +22 -0
- package/lib/EncryptHttp.d.ts.map +1 -0
- package/lib/EncryptHttp.js +39 -0
- package/lib/EncryptHttp.js.map +1 -0
- package/lib/Environment.d.ts +45 -0
- package/lib/Environment.d.ts.map +1 -0
- package/lib/Environment.js +43 -0
- package/lib/Environment.js.map +1 -0
- package/lib/Errors.d.ts +18 -0
- package/lib/Errors.d.ts.map +1 -0
- package/lib/Errors.js +13 -0
- package/lib/Errors.js.map +1 -0
- package/lib/Exec.d.ts +41 -0
- package/lib/Exec.d.ts.map +1 -0
- package/lib/Exec.js +3 -0
- package/lib/Exec.js.map +1 -0
- package/lib/ExecHttp.d.ts +22 -0
- package/lib/ExecHttp.d.ts.map +1 -0
- package/lib/ExecHttp.js +39 -0
- package/lib/ExecHttp.js.map +1 -0
- package/lib/GetObject.d.ts +34 -0
- package/lib/GetObject.d.ts.map +1 -0
- package/lib/GetObject.js +3 -0
- package/lib/GetObject.js.map +1 -0
- package/lib/GetObjectHttp.d.ts +12 -0
- package/lib/GetObjectHttp.d.ts.map +1 -0
- package/lib/GetObjectHttp.js +17 -0
- package/lib/GetObjectHttp.js.map +1 -0
- package/lib/GetSecret.d.ts +44 -0
- package/lib/GetSecret.d.ts.map +1 -0
- package/lib/GetSecret.js +3 -0
- package/lib/GetSecret.js.map +1 -0
- package/lib/GetSecretHttp.d.ts +22 -0
- package/lib/GetSecretHttp.d.ts.map +1 -0
- package/lib/GetSecretHttp.js +50 -0
- package/lib/GetSecretHttp.js.map +1 -0
- package/lib/HeadObject.d.ts +29 -0
- package/lib/HeadObject.d.ts.map +1 -0
- package/lib/HeadObject.js +3 -0
- package/lib/HeadObject.js.map +1 -0
- package/lib/HeadObjectHttp.d.ts +12 -0
- package/lib/HeadObjectHttp.d.ts.map +1 -0
- package/lib/HeadObjectHttp.js +17 -0
- package/lib/HeadObjectHttp.js.map +1 -0
- package/lib/IpAssignment.d.ts +258 -0
- package/lib/IpAssignment.d.ts.map +1 -0
- package/lib/IpAssignment.js +376 -0
- package/lib/IpAssignment.js.map +1 -0
- package/lib/ListObjectsV2.d.ts +29 -0
- package/lib/ListObjectsV2.d.ts.map +1 -0
- package/lib/ListObjectsV2.js +3 -0
- package/lib/ListObjectsV2.js.map +1 -0
- package/lib/ListObjectsV2Http.d.ts +12 -0
- package/lib/ListObjectsV2Http.d.ts.map +1 -0
- package/lib/ListObjectsV2Http.js +17 -0
- package/lib/ListObjectsV2Http.js.map +1 -0
- package/lib/ListSecrets.d.ts +53 -0
- package/lib/ListSecrets.d.ts.map +1 -0
- package/lib/ListSecrets.js +3 -0
- package/lib/ListSecrets.js.map +1 -0
- package/lib/ListSecretsHttp.d.ts +22 -0
- package/lib/ListSecretsHttp.d.ts.map +1 -0
- package/lib/ListSecretsHttp.js +35 -0
- package/lib/ListSecretsHttp.js.map +1 -0
- package/lib/Machine.d.ts +802 -0
- package/lib/Machine.d.ts.map +1 -0
- package/lib/Machine.js +773 -0
- package/lib/Machine.js.map +1 -0
- package/lib/MachineContainers.d.ts +44 -0
- package/lib/MachineContainers.d.ts.map +1 -0
- package/lib/MachineContainers.js +115 -0
- package/lib/MachineContainers.js.map +1 -0
- package/lib/Metadata.d.ts +109 -0
- package/lib/Metadata.d.ts.map +1 -0
- package/lib/Metadata.js +159 -0
- package/lib/Metadata.js.map +1 -0
- package/lib/MountVolume.d.ts +295 -0
- package/lib/MountVolume.d.ts.map +1 -0
- package/lib/MountVolume.js +20 -0
- package/lib/MountVolume.js.map +1 -0
- package/lib/Postgres.d.ts +298 -0
- package/lib/Postgres.d.ts.map +1 -0
- package/lib/Postgres.js +508 -0
- package/lib/Postgres.js.map +1 -0
- package/lib/PostgresMigrations.d.ts +20 -0
- package/lib/PostgresMigrations.d.ts.map +1 -0
- package/lib/PostgresMigrations.js +58 -0
- package/lib/PostgresMigrations.js.map +1 -0
- package/lib/Providers.d.ts +38 -0
- package/lib/Providers.d.ts.map +1 -0
- package/lib/Providers.js +95 -0
- package/lib/Providers.js.map +1 -0
- package/lib/PutObject.d.ts +35 -0
- package/lib/PutObject.d.ts.map +1 -0
- package/lib/PutObject.js +3 -0
- package/lib/PutObject.js.map +1 -0
- package/lib/PutObjectHttp.d.ts +12 -0
- package/lib/PutObjectHttp.d.ts.map +1 -0
- package/lib/PutObjectHttp.js +17 -0
- package/lib/PutObjectHttp.js.map +1 -0
- package/lib/ReadRedis.d.ts +26 -0
- package/lib/ReadRedis.d.ts.map +1 -0
- package/lib/ReadRedis.js +3 -0
- package/lib/ReadRedis.js.map +1 -0
- package/lib/ReadRedisHttp.d.ts +11 -0
- package/lib/ReadRedisHttp.d.ts.map +1 -0
- package/lib/ReadRedisHttp.js +15 -0
- package/lib/ReadRedisHttp.js.map +1 -0
- package/lib/ReadWriteRedis.d.ts +29 -0
- package/lib/ReadWriteRedis.d.ts.map +1 -0
- package/lib/ReadWriteRedis.js +3 -0
- package/lib/ReadWriteRedis.js.map +1 -0
- package/lib/ReadWriteRedisHttp.d.ts +11 -0
- package/lib/ReadWriteRedisHttp.d.ts.map +1 -0
- package/lib/ReadWriteRedisHttp.js +15 -0
- package/lib/ReadWriteRedisHttp.js.map +1 -0
- package/lib/Redis.d.ts +252 -0
- package/lib/Redis.d.ts.map +1 -0
- package/lib/Redis.js +611 -0
- package/lib/Redis.js.map +1 -0
- package/lib/RedisBinding.d.ts +7 -0
- package/lib/RedisBinding.d.ts.map +1 -0
- package/lib/RedisBinding.js +53 -0
- package/lib/RedisBinding.js.map +1 -0
- package/lib/RedisHttp.d.ts +8 -0
- package/lib/RedisHttp.d.ts.map +1 -0
- package/lib/RedisHttp.js +11 -0
- package/lib/RedisHttp.js.map +1 -0
- package/lib/Region.d.ts +16 -0
- package/lib/Region.d.ts.map +1 -0
- package/lib/Region.js +13 -0
- package/lib/Region.js.map +1 -0
- package/lib/Secret.d.ts +190 -0
- package/lib/Secret.d.ts.map +1 -0
- package/lib/Secret.js +310 -0
- package/lib/Secret.js.map +1 -0
- package/lib/SecretHttp.d.ts +54 -0
- package/lib/SecretHttp.d.ts.map +1 -0
- package/lib/SecretHttp.js +66 -0
- package/lib/SecretHttp.js.map +1 -0
- package/lib/SecretKey.d.ts +164 -0
- package/lib/SecretKey.d.ts.map +1 -0
- package/lib/SecretKey.js +277 -0
- package/lib/SecretKey.js.map +1 -0
- package/lib/SecretKeyHttp.d.ts +14 -0
- package/lib/SecretKeyHttp.d.ts.map +1 -0
- package/lib/SecretKeyHttp.js +5 -0
- package/lib/SecretKeyHttp.js.map +1 -0
- package/lib/Service.d.ts +1017 -0
- package/lib/Service.d.ts.map +1 -0
- package/lib/Service.js +1430 -0
- package/lib/Service.js.map +1 -0
- package/lib/Sign.d.ts +36 -0
- package/lib/Sign.d.ts.map +1 -0
- package/lib/Sign.js +3 -0
- package/lib/Sign.js.map +1 -0
- package/lib/SignHttp.d.ts +22 -0
- package/lib/SignHttp.d.ts.map +1 -0
- package/lib/SignHttp.js +36 -0
- package/lib/SignHttp.js.map +1 -0
- package/lib/Sprite.d.ts +283 -0
- package/lib/Sprite.d.ts.map +1 -0
- package/lib/Sprite.js +471 -0
- package/lib/Sprite.js.map +1 -0
- package/lib/SpriteHttp.d.ts +25 -0
- package/lib/SpriteHttp.d.ts.map +1 -0
- package/lib/SpriteHttp.js +33 -0
- package/lib/SpriteHttp.js.map +1 -0
- package/lib/Verify.d.ts +37 -0
- package/lib/Verify.d.ts.map +1 -0
- package/lib/Verify.js +3 -0
- package/lib/Verify.js.map +1 -0
- package/lib/VerifyHttp.d.ts +22 -0
- package/lib/VerifyHttp.d.ts.map +1 -0
- package/lib/VerifyHttp.js +38 -0
- package/lib/VerifyHttp.js.map +1 -0
- package/lib/Volume.d.ts +53 -0
- package/lib/Volume.d.ts.map +1 -0
- package/lib/Volume.js +188 -0
- package/lib/Volume.js.map +1 -0
- package/lib/VolumeSnapshot.d.ts +135 -0
- package/lib/VolumeSnapshot.d.ts.map +1 -0
- package/lib/VolumeSnapshot.js +213 -0
- package/lib/VolumeSnapshot.js.map +1 -0
- package/lib/Website/AssetDeployment.d.ts +43 -0
- package/lib/Website/AssetDeployment.d.ts.map +1 -0
- package/lib/Website/AssetDeployment.js +162 -0
- package/lib/Website/AssetDeployment.js.map +1 -0
- package/lib/Website/Astro.d.ts +89 -0
- package/lib/Website/Astro.d.ts.map +1 -0
- package/lib/Website/Astro.js +54 -0
- package/lib/Website/Astro.js.map +1 -0
- package/lib/Website/Foldkit.d.ts +52 -0
- package/lib/Website/Foldkit.d.ts.map +1 -0
- package/lib/Website/Foldkit.js +45 -0
- package/lib/Website/Foldkit.js.map +1 -0
- package/lib/Website/FrameworkSite.d.ts +212 -0
- package/lib/Website/FrameworkSite.d.ts.map +1 -0
- package/lib/Website/FrameworkSite.js +205 -0
- package/lib/Website/FrameworkSite.js.map +1 -0
- package/lib/Website/Nextjs.d.ts +54 -0
- package/lib/Website/Nextjs.d.ts.map +1 -0
- package/lib/Website/Nextjs.js +46 -0
- package/lib/Website/Nextjs.js.map +1 -0
- package/lib/Website/Nuxt.d.ts +51 -0
- package/lib/Website/Nuxt.d.ts.map +1 -0
- package/lib/Website/Nuxt.js +36 -0
- package/lib/Website/Nuxt.js.map +1 -0
- package/lib/Website/Octane.d.ts +49 -0
- package/lib/Website/Octane.d.ts.map +1 -0
- package/lib/Website/Octane.js +39 -0
- package/lib/Website/Octane.js.map +1 -0
- package/lib/Website/ReactRouter.d.ts +80 -0
- package/lib/Website/ReactRouter.d.ts.map +1 -0
- package/lib/Website/ReactRouter.js +70 -0
- package/lib/Website/ReactRouter.js.map +1 -0
- package/lib/Website/SolidStart.d.ts +93 -0
- package/lib/Website/SolidStart.d.ts.map +1 -0
- package/lib/Website/SolidStart.js +71 -0
- package/lib/Website/SolidStart.js.map +1 -0
- package/lib/Website/StaticSite.d.ts +162 -0
- package/lib/Website/StaticSite.d.ts.map +1 -0
- package/lib/Website/StaticSite.js +180 -0
- package/lib/Website/StaticSite.js.map +1 -0
- package/lib/Website/SvelteKit.d.ts +52 -0
- package/lib/Website/SvelteKit.d.ts.map +1 -0
- package/lib/Website/SvelteKit.js +37 -0
- package/lib/Website/SvelteKit.js.map +1 -0
- package/lib/Website/TanStackStart.d.ts +76 -0
- package/lib/Website/TanStackStart.d.ts.map +1 -0
- package/lib/Website/TanStackStart.js +66 -0
- package/lib/Website/TanStackStart.js.map +1 -0
- package/lib/Website/Vinext.d.ts +73 -0
- package/lib/Website/Vinext.d.ts.map +1 -0
- package/lib/Website/Vinext.js +67 -0
- package/lib/Website/Vinext.js.map +1 -0
- package/lib/Website/Vite.d.ts +112 -0
- package/lib/Website/Vite.d.ts.map +1 -0
- package/lib/Website/Vite.js +100 -0
- package/lib/Website/Vite.js.map +1 -0
- package/lib/Website/Vocs.d.ts +52 -0
- package/lib/Website/Vocs.d.ts.map +1 -0
- package/lib/Website/Vocs.js +38 -0
- package/lib/Website/Vocs.js.map +1 -0
- package/lib/Website/Waku.d.ts +64 -0
- package/lib/Website/Waku.d.ts.map +1 -0
- package/lib/Website/Waku.js +39 -0
- package/lib/Website/Waku.js.map +1 -0
- package/lib/Website/index.d.ts +17 -0
- package/lib/Website/index.d.ts.map +1 -0
- package/lib/Website/index.js +17 -0
- package/lib/Website/index.js.map +1 -0
- package/lib/WriteCertificates.d.ts +63 -0
- package/lib/WriteCertificates.d.ts.map +1 -0
- package/lib/WriteCertificates.js +3 -0
- package/lib/WriteCertificates.js.map +1 -0
- package/lib/WriteCertificatesHttp.d.ts +25 -0
- package/lib/WriteCertificatesHttp.d.ts.map +1 -0
- package/lib/WriteCertificatesHttp.js +67 -0
- package/lib/WriteCertificatesHttp.js.map +1 -0
- package/lib/WriteRedis.d.ts +26 -0
- package/lib/WriteRedis.d.ts.map +1 -0
- package/lib/WriteRedis.js +3 -0
- package/lib/WriteRedis.js.map +1 -0
- package/lib/WriteRedisHttp.d.ts +11 -0
- package/lib/WriteRedisHttp.d.ts.map +1 -0
- package/lib/WriteRedisHttp.js +15 -0
- package/lib/WriteRedisHttp.js.map +1 -0
- package/lib/WriteSecret.d.ts +68 -0
- package/lib/WriteSecret.d.ts.map +1 -0
- package/lib/WriteSecret.js +4 -0
- package/lib/WriteSecret.js.map +1 -0
- package/lib/WriteSecretHttp.d.ts +26 -0
- package/lib/WriteSecretHttp.d.ts.map +1 -0
- package/lib/WriteSecretHttp.js +54 -0
- package/lib/WriteSecretHttp.js.map +1 -0
- package/lib/bluegreen.d.ts +36 -0
- package/lib/bluegreen.d.ts.map +1 -0
- package/lib/bluegreen.js +460 -0
- package/lib/bluegreen.js.map +1 -0
- package/lib/hosted.d.ts +171 -0
- package/lib/hosted.d.ts.map +1 -0
- package/lib/hosted.js +458 -0
- package/lib/hosted.js.map +1 -0
- package/lib/index.d.ts +65 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +63 -0
- package/lib/index.js.map +1 -0
- package/lib/internal/RemoteBuilder.d.ts +28 -0
- package/lib/internal/RemoteBuilder.d.ts.map +1 -0
- package/lib/internal/RemoteBuilder.js +158 -0
- package/lib/internal/RemoteBuilder.js.map +1 -0
- package/lib/leases.d.ts +53 -0
- package/lib/leases.d.ts.map +1 -0
- package/lib/leases.js +279 -0
- package/lib/leases.js.map +1 -0
- package/lib/ports.d.ts +51 -0
- package/lib/ports.d.ts.map +1 -0
- package/lib/ports.js +123 -0
- package/lib/ports.js.map +1 -0
- package/lib/replicas.d.ts +270 -0
- package/lib/replicas.d.ts.map +1 -0
- package/lib/replicas.js +969 -0
- package/lib/replicas.js.map +1 -0
- package/lib/rpc.d.ts +35 -0
- package/lib/rpc.d.ts.map +1 -0
- package/lib/rpc.js +113 -0
- package/lib/rpc.js.map +1 -0
- package/package.json +83 -3
- package/src/App.ts +514 -0
- package/src/AuthProvider.ts +68 -0
- package/src/BindService.ts +303 -0
- package/src/Bootstrap.ts +66 -0
- package/src/Bucket.ts +918 -0
- package/src/BucketBinding.ts +128 -0
- package/src/Catalog.ts +43 -0
- package/src/Certificate.ts +548 -0
- package/src/Checkpoint.ts +69 -0
- package/src/CheckpointHttp.ts +66 -0
- package/src/ConnectPostgres.ts +68 -0
- package/src/ConnectPostgresHttp.ts +77 -0
- package/src/Contributions.ts +9 -0
- package/src/Credentials.ts +75 -0
- package/src/Decrypt.ts +62 -0
- package/src/DecryptHttp.ts +52 -0
- package/src/DeleteObject.ts +43 -0
- package/src/DeleteObjectHttp.ts +20 -0
- package/src/Deployment.ts +121 -0
- package/src/DeploymentImages.ts +84 -0
- package/src/DeploymentState.ts +104 -0
- package/src/Encrypt.ts +60 -0
- package/src/EncryptHttp.ts +48 -0
- package/src/Environment.ts +81 -0
- package/src/Errors.ts +14 -0
- package/src/Exec.ts +50 -0
- package/src/ExecHttp.ts +47 -0
- package/src/GetObject.ts +48 -0
- package/src/GetObjectHttp.ts +20 -0
- package/src/GetSecret.ts +48 -0
- package/src/GetSecretHttp.ts +60 -0
- package/src/HeadObject.ts +43 -0
- package/src/HeadObjectHttp.ts +20 -0
- package/src/IpAssignment.ts +522 -0
- package/src/ListObjectsV2.ts +43 -0
- package/src/ListObjectsV2Http.ts +20 -0
- package/src/ListSecrets.ts +57 -0
- package/src/ListSecretsHttp.ts +43 -0
- package/src/Machine.ts +1270 -0
- package/src/MachineContainers.ts +152 -0
- package/src/Metadata.ts +195 -0
- package/src/MountVolume.ts +315 -0
- package/src/Postgres.ts +777 -0
- package/src/PostgresMigrations.ts +93 -0
- package/src/Providers.ts +155 -0
- package/src/PutObject.ts +49 -0
- package/src/PutObjectHttp.ts +20 -0
- package/src/ReadRedis.ts +31 -0
- package/src/ReadRedisHttp.ts +18 -0
- package/src/ReadWriteRedis.ts +33 -0
- package/src/ReadWriteRedisHttp.ts +18 -0
- package/src/Redis.ts +823 -0
- package/src/RedisBinding.ts +64 -0
- package/src/RedisHttp.ts +14 -0
- package/src/Region.ts +44 -0
- package/src/Secret.ts +405 -0
- package/src/SecretHttp.ts +149 -0
- package/src/SecretKey.ts +389 -0
- package/src/SecretKeyHttp.ts +23 -0
- package/src/Service.ts +1968 -0
- package/src/Sign.ts +46 -0
- package/src/SignHttp.ts +44 -0
- package/src/Sprite.ts +673 -0
- package/src/SpriteHttp.ts +50 -0
- package/src/Verify.ts +47 -0
- package/src/VerifyHttp.ts +46 -0
- package/src/Volume.ts +264 -0
- package/src/VolumeSnapshot.ts +321 -0
- package/src/Website/AssetDeployment.ts +292 -0
- package/src/Website/Astro.ts +92 -0
- package/src/Website/Foldkit.ts +54 -0
- package/src/Website/FrameworkSite.ts +449 -0
- package/src/Website/Nextjs.ts +50 -0
- package/src/Website/Nuxt.ts +48 -0
- package/src/Website/Octane.ts +44 -0
- package/src/Website/ReactRouter.ts +76 -0
- package/src/Website/SolidStart.ts +90 -0
- package/src/Website/StaticSite.ts +295 -0
- package/src/Website/SvelteKit.ts +49 -0
- package/src/Website/TanStackStart.ts +72 -0
- package/src/Website/Vinext.ts +79 -0
- package/src/Website/Vite.ts +121 -0
- package/src/Website/Vocs.ts +50 -0
- package/src/Website/Waku.ts +65 -0
- package/src/Website/index.ts +16 -0
- package/src/WriteCertificates.ts +90 -0
- package/src/WriteCertificatesHttp.ts +93 -0
- package/src/WriteRedis.ts +31 -0
- package/src/WriteRedisHttp.ts +18 -0
- package/src/WriteSecret.ts +86 -0
- package/src/WriteSecretHttp.ts +71 -0
- package/src/bluegreen.ts +711 -0
- package/src/globals.d.ts +2 -0
- package/src/hosted.ts +667 -0
- package/src/index.ts +64 -0
- package/src/internal/RemoteBuilder.ts +242 -0
- package/src/leases.ts +437 -0
- package/src/ports.ts +177 -0
- package/src/replicas.ts +1480 -0
- package/src/rpc.ts +136 -0
package/lib/Service.js
ADDED
|
@@ -0,0 +1,1430 @@
|
|
|
1
|
+
import { DockerLive, Docker } from "@alchemy.run/docker/Docker";
|
|
2
|
+
import * as machines from "@distilled.cloud/fly-io/machines";
|
|
3
|
+
import { AlchemyContext } from "alchemy/AlchemyContext";
|
|
4
|
+
import * as Bundle from "alchemy/Bundle/Bundle";
|
|
5
|
+
import { deepEqual, isResolved } from "alchemy/Diff";
|
|
6
|
+
import { Platform } from "alchemy/Platform";
|
|
7
|
+
import * as Provider from "alchemy/Provider";
|
|
8
|
+
import { makeRandom } from "alchemy/Random";
|
|
9
|
+
import { packEnvValue } from "alchemy/RuntimeContext";
|
|
10
|
+
import { Stack } from "alchemy/Stack";
|
|
11
|
+
import * as Data from "effect/Data";
|
|
12
|
+
import * as Effect from "effect/Effect";
|
|
13
|
+
import * as Layer from "effect/Layer";
|
|
14
|
+
import * as Redacted from "effect/Redacted";
|
|
15
|
+
import { App, deleteApp, ensureApp } from "./App.js";
|
|
16
|
+
import { boundTargetEnvKeys } from "./BindService.js";
|
|
17
|
+
import { attachBucketSecrets } from "./Bucket.js";
|
|
18
|
+
import { deploymentPolicy, validateDeployment, } from "./Deployment.js";
|
|
19
|
+
import { resolveOrgSlug } from "./Environment.js";
|
|
20
|
+
import { collectBindingState, createFlyHostedSupport, createFlyHostRuntimeContext, defaultHttpServices, DEFAULT_PORT, toEnvRecord, } from "./hosted.js";
|
|
21
|
+
import { ensureFlycastAddress, hasPublicAddress, syncOwnedAppAddresses } from "./IpAssignment.js";
|
|
22
|
+
import { alchemyMetadataKeys, createFlyAppName, createFlyResourceName, diffMachineMetadata, matchesAlchemyPhysicalName, sanitizeFlyAppName, } from "./Metadata.js";
|
|
23
|
+
import { bindingPortOf, findPortConflict, portsOfFly } from "./ports.js";
|
|
24
|
+
import { attachPostgresSecrets } from "./Postgres.js";
|
|
25
|
+
import { attachRedisSecrets } from "./Redis.js";
|
|
26
|
+
import { regionList } from "./Region.js";
|
|
27
|
+
import { deleteReplicaSet, hasPublishedService, listReplicaSets, observeReplicaSet, reconcileReplicas, resolveCount, sameServices, toFlyService, volumeIdsOf, } from "./replicas.js";
|
|
28
|
+
import { BINDING_PORT_ENV, DEFAULT_BINDING_PORT, RPC_ORG_ENV, RPC_TOKEN_ENV } from "./rpc.js";
|
|
29
|
+
const DEFAULT_REGION = "iad";
|
|
30
|
+
const DEFAULT_CPU_KIND = "shared";
|
|
31
|
+
const DEFAULT_CPUS = 1;
|
|
32
|
+
const DEFAULT_MEMORY_MB = 256;
|
|
33
|
+
export const isService = (value) => typeof value === "object" &&
|
|
34
|
+
value !== null &&
|
|
35
|
+
value.Type === "Fly.Service";
|
|
36
|
+
/**
|
|
37
|
+
* A Service is an Effect program running on Fly.io Machines in its own
|
|
38
|
+
* Fly App. It gets its own `{name}.fly.dev` hostname, addresses, logs,
|
|
39
|
+
* and metrics, the way each Cloudflare Worker is its own endpoint. Set
|
|
40
|
+
* `count` to scale it up or down.
|
|
41
|
+
*
|
|
42
|
+
* @see https://fly.io/docs/machines/api/machines-resource/
|
|
43
|
+
*
|
|
44
|
+
* ### Declare a Service
|
|
45
|
+
* A Service is a class. Props describe the Machine. The Effect is the
|
|
46
|
+
* program that runs on it.
|
|
47
|
+
*
|
|
48
|
+
* The Service creates its Fly App, and deleting the Service deletes it.
|
|
49
|
+
* `main: import.meta.url` is the bundle entrypoint. Alchemy bundles this
|
|
50
|
+
* file with Rolldown, builds a Docker image (default `node:26-slim`), and
|
|
51
|
+
* pushes it to `registry.fly.io/{app}:{id}-{hash}`.
|
|
52
|
+
*
|
|
53
|
+
* **Example:** Class + main
|
|
54
|
+
* ```typescript
|
|
55
|
+
* // src/api.ts
|
|
56
|
+
* import * as Fly from "@alchemy.run/fly";
|
|
57
|
+
* import * as Effect from "effect/Effect";
|
|
58
|
+
*
|
|
59
|
+
* export default class Api extends Fly.Service<Api>()(
|
|
60
|
+
* "Api",
|
|
61
|
+
* { main: import.meta.url },
|
|
62
|
+
* Effect.gen(function* () {
|
|
63
|
+
* return {};
|
|
64
|
+
* }),
|
|
65
|
+
* ) {}
|
|
66
|
+
* ```
|
|
67
|
+
*
|
|
68
|
+
* ### Serve HTTP with fetch
|
|
69
|
+
* Return `fetch` from the init Effect to boot an HTTP server. Omit
|
|
70
|
+
* `fetch` for a background service.
|
|
71
|
+
*
|
|
72
|
+
* **Example:** Hello
|
|
73
|
+
* ```typescript
|
|
74
|
+
* export default class Api extends Fly.Service<Api>()(
|
|
75
|
+
* "Api",
|
|
76
|
+
* { main: import.meta.url },
|
|
77
|
+
* Effect.gen(function* () {
|
|
78
|
+
* return {
|
|
79
|
+
* fetch: Effect.succeed(HttpServerResponse.text("hello")),
|
|
80
|
+
* };
|
|
81
|
+
* }),
|
|
82
|
+
* ) {}
|
|
83
|
+
* ```
|
|
84
|
+
*
|
|
85
|
+
* ### Pin a region
|
|
86
|
+
* Fly Machines live in a region. Default is `iad`. See
|
|
87
|
+
* [Regions](/fly/compute/regions) for the list of codes.
|
|
88
|
+
*
|
|
89
|
+
* **Example:** Region
|
|
90
|
+
* ```typescript
|
|
91
|
+
* export default class Api extends Fly.Service<Api>()(
|
|
92
|
+
* "Api",
|
|
93
|
+
* { main: import.meta.url, region: "iad" },
|
|
94
|
+
* Effect.gen(function* () {
|
|
95
|
+
* return {
|
|
96
|
+
* fetch: Effect.succeed(HttpServerResponse.text("hello")),
|
|
97
|
+
* };
|
|
98
|
+
* }),
|
|
99
|
+
* ) {}
|
|
100
|
+
* ```
|
|
101
|
+
*
|
|
102
|
+
* ### Run in several regions
|
|
103
|
+
* Pass a list to run the same program in several regions behind one
|
|
104
|
+
* hostname. `count` Machines run in each region, and Fly's proxy sends
|
|
105
|
+
* each request to the nearest healthy one. `regions` on the Service
|
|
106
|
+
* lists where it runs.
|
|
107
|
+
*
|
|
108
|
+
* **Example:** Two regions, two Machines each
|
|
109
|
+
* ```typescript
|
|
110
|
+
* export default class Api extends Fly.Service<Api>()(
|
|
111
|
+
* "Api",
|
|
112
|
+
* { main: import.meta.url, region: ["iad", "lhr"], count: 2 },
|
|
113
|
+
* Effect.gen(function* () {
|
|
114
|
+
* return {
|
|
115
|
+
* fetch: Effect.succeed(HttpServerResponse.text("hello")),
|
|
116
|
+
* };
|
|
117
|
+
* }),
|
|
118
|
+
* ) {}
|
|
119
|
+
* ```
|
|
120
|
+
*
|
|
121
|
+
* Adding or removing a region updates the Service in place and keeps
|
|
122
|
+
* its App and hostname. Machines already in a kept region stay; a
|
|
123
|
+
* removed region's Machines are deleted. Each Machine's Volume from
|
|
124
|
+
* `MountVolume` is created in that Machine's region.
|
|
125
|
+
*
|
|
126
|
+
* ### Set the port
|
|
127
|
+
* `port` is the port the process listens on inside the Machine.
|
|
128
|
+
* Alchemy writes it to `PORT`. Default is `3000`.
|
|
129
|
+
*
|
|
130
|
+
* **Example:** Port 3000
|
|
131
|
+
* ```typescript
|
|
132
|
+
* export default class Api extends Fly.Service<Api>()(
|
|
133
|
+
* "Api",
|
|
134
|
+
* { main: import.meta.url, region: "iad", port: 3000 },
|
|
135
|
+
* Effect.gen(function* () {
|
|
136
|
+
* return {
|
|
137
|
+
* fetch: Effect.succeed(HttpServerResponse.text("hello")),
|
|
138
|
+
* };
|
|
139
|
+
* }),
|
|
140
|
+
* ) {}
|
|
141
|
+
* ```
|
|
142
|
+
*
|
|
143
|
+
* ### The public URL
|
|
144
|
+
* A Service is public by default. Alchemy allocates a shared IPv4 and
|
|
145
|
+
* an IPv6 on its App (both free) and `api.url` is
|
|
146
|
+
* `https://{appName}.fly.dev`.
|
|
147
|
+
*
|
|
148
|
+
* **Example:** Stack output
|
|
149
|
+
* ```typescript
|
|
150
|
+
* export default Alchemy.Stack(
|
|
151
|
+
* "MyApp",
|
|
152
|
+
* { providers: Fly.providers(), state: Alchemy.localState() },
|
|
153
|
+
* Effect.gen(function* () {
|
|
154
|
+
* const api = yield* Api;
|
|
155
|
+
* return { url: api.url };
|
|
156
|
+
* }),
|
|
157
|
+
* );
|
|
158
|
+
* ```
|
|
159
|
+
*
|
|
160
|
+
* `url` is `undefined` when you pass `services: []` (nothing is
|
|
161
|
+
* published).
|
|
162
|
+
*
|
|
163
|
+
* ### Every published port
|
|
164
|
+
* `url` is one endpoint. A Service that publishes several ports has one
|
|
165
|
+
* endpoint per port at the same hostname, listed in `endpoints` with
|
|
166
|
+
* its port, handlers, and a URL when the port speaks HTTP. Ports that
|
|
167
|
+
* only redirect to HTTPS are left out.
|
|
168
|
+
*
|
|
169
|
+
* **Example:** An admin UI on 8443 and raw TCP on 7000
|
|
170
|
+
* ```typescript
|
|
171
|
+
* export default class Api extends Fly.Service<Api>()(
|
|
172
|
+
* "Api",
|
|
173
|
+
* {
|
|
174
|
+
* main: import.meta.url,
|
|
175
|
+
* services: [
|
|
176
|
+
* {
|
|
177
|
+
* internalPort: 3000,
|
|
178
|
+
* ports: [
|
|
179
|
+
* { port: 80, handlers: ["http"], forceHttps: true },
|
|
180
|
+
* { port: 443, handlers: ["tls", "http"] },
|
|
181
|
+
* ],
|
|
182
|
+
* },
|
|
183
|
+
* { internalPort: 9000, ports: [{ port: 8443, handlers: ["tls", "http"] }] },
|
|
184
|
+
* { internalPort: 7000, ports: [{ port: 7000 }] },
|
|
185
|
+
* ],
|
|
186
|
+
* },
|
|
187
|
+
* Effect.gen(function* () {
|
|
188
|
+
* return {};
|
|
189
|
+
* }),
|
|
190
|
+
* ) {}
|
|
191
|
+
*
|
|
192
|
+
* api.url; // "https://{appName}.fly.dev"
|
|
193
|
+
* api.endpoints.map((endpoint) => endpoint.url);
|
|
194
|
+
* // ["https://{appName}.fly.dev", "https://{appName}.fly.dev:8443", undefined]
|
|
195
|
+
* ```
|
|
196
|
+
*
|
|
197
|
+
* :::note[Plain HTTP on other ports needs IPv6]
|
|
198
|
+
* The free shared IPv4 serves TLS on any port but plain HTTP only on
|
|
199
|
+
* port 80. A plain-HTTP port such as 8080 answers over IPv6 only.
|
|
200
|
+
* :::
|
|
201
|
+
*
|
|
202
|
+
* ### Private Services
|
|
203
|
+
* `public: false` keeps a Service off the internet. It gets only a
|
|
204
|
+
* Flycast address and publishes plain HTTP on port 80 (Fly issues no
|
|
205
|
+
* certificate for `.flycast`). `url` is `undefined`, and `privateUrl` is
|
|
206
|
+
* `http://{appName}.flycast`. Calls go through Fly's proxy, so service
|
|
207
|
+
* checks, `autostart`, and blue/green cutover still apply.
|
|
208
|
+
*
|
|
209
|
+
* Turning `public` on or off updates the Service in place. Its App and
|
|
210
|
+
* hostname stay the same.
|
|
211
|
+
*
|
|
212
|
+
* ### Call another Service
|
|
213
|
+
* A Service can return methods next to `fetch`. Another Service calls
|
|
214
|
+
* them by binding it with {@link bindService}, which returns a typed
|
|
215
|
+
* client. Binding makes the caller deploy after the Service, and only
|
|
216
|
+
* callers that bind a Service receive its caller token, so only they
|
|
217
|
+
* can call its methods.
|
|
218
|
+
*
|
|
219
|
+
* **Example:** A private Service with methods
|
|
220
|
+
* ```typescript
|
|
221
|
+
* export default class Users extends Fly.Service<Users>()(
|
|
222
|
+
* "Users",
|
|
223
|
+
* { main: import.meta.url, public: false },
|
|
224
|
+
* Effect.gen(function* () {
|
|
225
|
+
* return {
|
|
226
|
+
* list: () => Effect.succeed(USERS),
|
|
227
|
+
* get: (id: string) =>
|
|
228
|
+
* Effect.succeed(USERS.find((user) => user.id === id)),
|
|
229
|
+
* };
|
|
230
|
+
* }),
|
|
231
|
+
* ) {}
|
|
232
|
+
* ```
|
|
233
|
+
*
|
|
234
|
+
* **Example:** Bind it from another Service
|
|
235
|
+
* ```typescript
|
|
236
|
+
* export default class Gateway extends Fly.Service<Gateway>()(
|
|
237
|
+
* "Gateway",
|
|
238
|
+
* { main: import.meta.url },
|
|
239
|
+
* Effect.gen(function* () {
|
|
240
|
+
* const users = yield* Fly.bindService(Users);
|
|
241
|
+
* return {
|
|
242
|
+
* fetch: Effect.gen(function* () {
|
|
243
|
+
* return yield* HttpServerResponse.json(yield* users.list());
|
|
244
|
+
* }).pipe(Effect.orDie),
|
|
245
|
+
* };
|
|
246
|
+
* }),
|
|
247
|
+
* ) {}
|
|
248
|
+
* ```
|
|
249
|
+
*
|
|
250
|
+
* Calls use plain HTTP to `privateUrl` inside Fly's WireGuard-encrypted
|
|
251
|
+
* private network. The Service accepts a method call only with its
|
|
252
|
+
* caller token and a `Fly-Src` signature from Fly's proxy for the same
|
|
253
|
+
* organization, so the public internet can never call it. A public
|
|
254
|
+
* Service publishes an extra plain-HTTP port for bound callers,
|
|
255
|
+
* `bindingPort` (default 7780), since its own ports are an HTTPS
|
|
256
|
+
* redirect and HTTPS. `client.fetch` sends a request to the Service's
|
|
257
|
+
* `fetch` routes, and {@link bindEndpoint} targets one published port.
|
|
258
|
+
* Two Services can bind each other; declare them as tag classes and
|
|
259
|
+
* provide their `.make` layers (see the
|
|
260
|
+
* [Connect Services guide](/fly/compute/connecting-services)).
|
|
261
|
+
*
|
|
262
|
+
* ### Isolate Services on a private network
|
|
263
|
+
* Fly's default private network spans the whole organization, so any
|
|
264
|
+
* App in it can reach a private Service's address. Put a stack's
|
|
265
|
+
* Services on their own network with `network`. {@link stackNetwork}
|
|
266
|
+
* names one per stack and stage. Apps on other networks, including the
|
|
267
|
+
* default one, cannot resolve them, and binding a Service on another
|
|
268
|
+
* network fails the caller's deploy with `Fly.ServiceUnreachable`
|
|
269
|
+
* before anything is created. A public Service on the network still
|
|
270
|
+
* serves `url`, which makes it the stack's single entry point.
|
|
271
|
+
*
|
|
272
|
+
* **Example:** Private backend behind a public gateway
|
|
273
|
+
* ```typescript
|
|
274
|
+
* export class Users extends Fly.Service<Users>()(
|
|
275
|
+
* "Users",
|
|
276
|
+
* Effect.gen(function* () {
|
|
277
|
+
* return {
|
|
278
|
+
* main: import.meta.url,
|
|
279
|
+
* public: false,
|
|
280
|
+
* network: yield* Fly.stackNetwork,
|
|
281
|
+
* };
|
|
282
|
+
* }),
|
|
283
|
+
* Effect.gen(function* () {
|
|
284
|
+
* return { list: () => Effect.succeed(USERS) };
|
|
285
|
+
* }),
|
|
286
|
+
* ) {}
|
|
287
|
+
*
|
|
288
|
+
* export class Gateway extends Fly.Service<Gateway>()(
|
|
289
|
+
* "Gateway",
|
|
290
|
+
* Effect.gen(function* () {
|
|
291
|
+
* return { main: import.meta.url, network: yield* Fly.stackNetwork };
|
|
292
|
+
* }),
|
|
293
|
+
* Effect.gen(function* () {
|
|
294
|
+
* const users = yield* Fly.bindService(Users);
|
|
295
|
+
* return {
|
|
296
|
+
* fetch: Effect.gen(function* () {
|
|
297
|
+
* return yield* HttpServerResponse.json(yield* users.list());
|
|
298
|
+
* }).pipe(Effect.orDie),
|
|
299
|
+
* };
|
|
300
|
+
* }),
|
|
301
|
+
* ) {}
|
|
302
|
+
* ```
|
|
303
|
+
*
|
|
304
|
+
* :::caution[Changing `network` replaces the Service]
|
|
305
|
+
* Fly cannot move an App between networks. Alchemy creates the Service
|
|
306
|
+
* on the new network, then deletes the old App.
|
|
307
|
+
* :::
|
|
308
|
+
*
|
|
309
|
+
* ### Fly's proxy is the load balancer
|
|
310
|
+
* There is no LoadBalancer resource. Fly runs an Anycast proxy at
|
|
311
|
+
* the edge.
|
|
312
|
+
*
|
|
313
|
+
* **Example:** Published ports
|
|
314
|
+
* ```typescript
|
|
315
|
+
* export default class Api extends Fly.Service<Api>()(
|
|
316
|
+
* "Api",
|
|
317
|
+
* { main: import.meta.url, region: "iad", port: 3000 },
|
|
318
|
+
* Effect.gen(function* () {
|
|
319
|
+
* return {
|
|
320
|
+
* fetch: Effect.succeed(HttpServerResponse.text("hello")),
|
|
321
|
+
* };
|
|
322
|
+
* }),
|
|
323
|
+
* ) {}
|
|
324
|
+
* ```
|
|
325
|
+
*
|
|
326
|
+
* Unless you override `services`, Alchemy publishes HTTP 80 and
|
|
327
|
+
* HTTPS 443 on that proxy and points them at `port` inside each
|
|
328
|
+
* Machine (`internal_port`). A request to
|
|
329
|
+
* `https://{appName}.fly.dev` lands on Fly's edge. Fly terminates
|
|
330
|
+
* TLS on 443, picks one started Machine that published this service,
|
|
331
|
+
* and forwards to `port` where `fetch` runs.
|
|
332
|
+
*
|
|
333
|
+
* ### Configure routing health checks
|
|
334
|
+
* The generated service includes a TCP check on `port`. To customize
|
|
335
|
+
* it, provide `services` and configure each service's `checks` property.
|
|
336
|
+
* With rolling updates, reconcile waits for each started replica's checks
|
|
337
|
+
* before updating the next replica. Missing or non-passing results are
|
|
338
|
+
* polled within `deploy.healthTimeout` (60 seconds by default), then fail
|
|
339
|
+
* deployment with `Fly.ReplicaChecksNotPassing`. Later replicas remain
|
|
340
|
+
* unchanged; earlier updates are not rolled back. A single rolling replica
|
|
341
|
+
* can be unavailable. Blue/green checks replacements before retiring the
|
|
342
|
+
* old set, with representative/floor readiness for idle capacity.
|
|
343
|
+
*
|
|
344
|
+
* **Example:** HTTP readiness check
|
|
345
|
+
* ```typescript
|
|
346
|
+
* export default class Api extends Fly.Service<Api>()(
|
|
347
|
+
* "Api",
|
|
348
|
+
* {
|
|
349
|
+
* main: import.meta.url,
|
|
350
|
+
* port: 3000,
|
|
351
|
+
* services: [
|
|
352
|
+
* {
|
|
353
|
+
* protocol: "tcp",
|
|
354
|
+
* internalPort: 3000,
|
|
355
|
+
* ports: [
|
|
356
|
+
* { port: 80, handlers: ["http"], forceHttps: true },
|
|
357
|
+
* { port: 443, handlers: ["tls", "http"] },
|
|
358
|
+
* ],
|
|
359
|
+
* checks: [
|
|
360
|
+
* {
|
|
361
|
+
* type: "http",
|
|
362
|
+
* port: 3000,
|
|
363
|
+
* method: "GET",
|
|
364
|
+
* path: "/health",
|
|
365
|
+
* protocol: "http",
|
|
366
|
+
* interval: "15s",
|
|
367
|
+
* timeout: "2s",
|
|
368
|
+
* gracePeriod: "30s",
|
|
369
|
+
* },
|
|
370
|
+
* ],
|
|
371
|
+
* },
|
|
372
|
+
* ],
|
|
373
|
+
* },
|
|
374
|
+
* Effect.gen(function* () {
|
|
375
|
+
* return {
|
|
376
|
+
* fetch: Effect.succeed(HttpServerResponse.text("hello")),
|
|
377
|
+
* };
|
|
378
|
+
* }),
|
|
379
|
+
* ) {}
|
|
380
|
+
* ```
|
|
381
|
+
*
|
|
382
|
+
* ### Scale with count
|
|
383
|
+
* `count` is how many Machines to provision, including idle capacity.
|
|
384
|
+
* Default is `1`. Replicas publish the same proxy service behind
|
|
385
|
+
* `{appName}.fly.dev`; Fly's proxy picks an available Machine per request.
|
|
386
|
+
* Each replica gets its own Volume from every {@link MountVolume} binding;
|
|
387
|
+
* attached volumes require rolling updates.
|
|
388
|
+
*
|
|
389
|
+
* **Example:** Three replicas
|
|
390
|
+
* ```typescript
|
|
391
|
+
* export default class Api extends Fly.Service<Api>()(
|
|
392
|
+
* "Api",
|
|
393
|
+
* { main: import.meta.url, region: "iad", count: 3, port: 3000 },
|
|
394
|
+
* Effect.gen(function* () {
|
|
395
|
+
* return {
|
|
396
|
+
* fetch: Effect.succeed(HttpServerResponse.text("hello")),
|
|
397
|
+
* };
|
|
398
|
+
* }),
|
|
399
|
+
* ) {}
|
|
400
|
+
* ```
|
|
401
|
+
*
|
|
402
|
+
* ### Config
|
|
403
|
+
* Yield `Config` in init. Alchemy reads the value from the env of
|
|
404
|
+
* whoever deploys and writes it onto the Machine, so there is no need
|
|
405
|
+
* to copy it into `env`.
|
|
406
|
+
*
|
|
407
|
+
* `Config.Redacted("API_KEY")` is `Redacted<string>`. Unwrap with
|
|
408
|
+
* `Redacted.value` only where you need the raw string.
|
|
409
|
+
*
|
|
410
|
+
* Alchemy also injects `PORT` (when `port` is set) and stack metadata.
|
|
411
|
+
* For a secret Fly should own and inject into every Machine on an App,
|
|
412
|
+
* run the Service in an {@link App} (`app`) and use {@link Secret}.
|
|
413
|
+
*
|
|
414
|
+
* **Example:** Config.Redacted
|
|
415
|
+
* ```typescript
|
|
416
|
+
* import * as Config from "effect/Config";
|
|
417
|
+
* import * as Redacted from "effect/Redacted";
|
|
418
|
+
*
|
|
419
|
+
* export default class Api extends Fly.Service<Api>()(
|
|
420
|
+
* "Api",
|
|
421
|
+
* { main: import.meta.url, port: 3000 },
|
|
422
|
+
* Effect.gen(function* () {
|
|
423
|
+
* const apiKey = yield* Config.Redacted("API_KEY");
|
|
424
|
+
*
|
|
425
|
+
* return {
|
|
426
|
+
* fetch: Effect.gen(function* () {
|
|
427
|
+
* const token = Redacted.value(apiKey);
|
|
428
|
+
* return HttpServerResponse.text("ok");
|
|
429
|
+
* }),
|
|
430
|
+
* };
|
|
431
|
+
* }),
|
|
432
|
+
* ) {}
|
|
433
|
+
* ```
|
|
434
|
+
*
|
|
435
|
+
* ### Mount a disk
|
|
436
|
+
* Bind {@link MountVolume} inside init. App and region come from the
|
|
437
|
+
* Service. `count: 3` creates three Volumes, one per replica. Provide
|
|
438
|
+
* {@link MountVolumeLive}.
|
|
439
|
+
*
|
|
440
|
+
* **Example:** Per-replica disk
|
|
441
|
+
* ```typescript
|
|
442
|
+
* export default class Api extends Fly.Service<Api>()(
|
|
443
|
+
* "Api",
|
|
444
|
+
* { main: import.meta.url, region: "iad", count: 3, port: 3000 },
|
|
445
|
+
* Effect.gen(function* () {
|
|
446
|
+
* const disk = yield* Fly.MountVolume({ path: "/data", sizeGb: 1 });
|
|
447
|
+
* const fs = yield* FileSystem.FileSystem;
|
|
448
|
+
* return {
|
|
449
|
+
* fetch: Effect.gen(function* () {
|
|
450
|
+
* const text = yield* fs.readFileString(`${disk.path}/hello.txt`);
|
|
451
|
+
* return HttpServerResponse.text(text);
|
|
452
|
+
* }),
|
|
453
|
+
* };
|
|
454
|
+
* }).pipe(Effect.provide(Fly.MountVolumeLive)),
|
|
455
|
+
* ) {}
|
|
456
|
+
* ```
|
|
457
|
+
*
|
|
458
|
+
* ### Guest size
|
|
459
|
+
* `guest` is CPU kind, CPU count, and memory. Default is shared-cpu,
|
|
460
|
+
* 1 CPU, 256 MB. Set `gpuKind` and `gpus` for a GPU. Guest updates in
|
|
461
|
+
* place.
|
|
462
|
+
*
|
|
463
|
+
* **Example:** Bigger guest
|
|
464
|
+
* ```typescript
|
|
465
|
+
* export default class Api extends Fly.Service<Api>()(
|
|
466
|
+
* "Api",
|
|
467
|
+
* {
|
|
468
|
+
* main: import.meta.url,
|
|
469
|
+
* region: "iad",
|
|
470
|
+
* port: 3000,
|
|
471
|
+
* guest: { cpuKind: "shared", cpus: 2, memoryMb: 512 },
|
|
472
|
+
* },
|
|
473
|
+
* Effect.gen(function* () {
|
|
474
|
+
* return {
|
|
475
|
+
* fetch: Effect.succeed(HttpServerResponse.text("hello")),
|
|
476
|
+
* };
|
|
477
|
+
* }),
|
|
478
|
+
* ) {}
|
|
479
|
+
* ```
|
|
480
|
+
*
|
|
481
|
+
* ### A stable hostname
|
|
482
|
+
* `name` names the Service's App, so it is the `{name}.fly.dev`
|
|
483
|
+
* hostname. App names are globally unique across Fly. Omit `name` and
|
|
484
|
+
* Alchemy generates one from the stack, stage, and logical ID.
|
|
485
|
+
*
|
|
486
|
+
* **Example:** Explicit name
|
|
487
|
+
* ```typescript
|
|
488
|
+
* export default class Api extends Fly.Service<Api>()(
|
|
489
|
+
* "Api",
|
|
490
|
+
* { main: import.meta.url, name: "api", port: 3000 },
|
|
491
|
+
* Effect.gen(function* () {
|
|
492
|
+
* return {
|
|
493
|
+
* fetch: Effect.succeed(HttpServerResponse.text("hello")),
|
|
494
|
+
* };
|
|
495
|
+
* }),
|
|
496
|
+
* ) {}
|
|
497
|
+
* ```
|
|
498
|
+
*
|
|
499
|
+
* :::caution[Changing `name` replaces the Service]
|
|
500
|
+
* Fly cannot rename an App. Alchemy creates the Service under the new
|
|
501
|
+
* name, then deletes the old App.
|
|
502
|
+
* :::
|
|
503
|
+
*
|
|
504
|
+
* ### Named export
|
|
505
|
+
* `handler` is the named export to load from `main`. Default is
|
|
506
|
+
* `"default"`.
|
|
507
|
+
*
|
|
508
|
+
* **Example:** Custom handler
|
|
509
|
+
* ```typescript
|
|
510
|
+
* export default class Api extends Fly.Service<Api>()(
|
|
511
|
+
* "Api",
|
|
512
|
+
* { main: import.meta.url, handler: "api", port: 3000 },
|
|
513
|
+
* Effect.gen(function* () {
|
|
514
|
+
* return {
|
|
515
|
+
* fetch: Effect.succeed(HttpServerResponse.text("hello")),
|
|
516
|
+
* };
|
|
517
|
+
* }),
|
|
518
|
+
* ) {}
|
|
519
|
+
* ```
|
|
520
|
+
*
|
|
521
|
+
* ### Base image
|
|
522
|
+
* `image` is the generated Dockerfile's `FROM`. Default is
|
|
523
|
+
* `node:26-slim`. A content-hash change of `main` updates the
|
|
524
|
+
* Machine in place.
|
|
525
|
+
*
|
|
526
|
+
* **Example:** Override FROM
|
|
527
|
+
* ```typescript
|
|
528
|
+
* export default class Api extends Fly.Service<Api>()(
|
|
529
|
+
* "Api",
|
|
530
|
+
* {
|
|
531
|
+
* main: import.meta.url,
|
|
532
|
+
* image: "node:26",
|
|
533
|
+
* port: 3000,
|
|
534
|
+
* },
|
|
535
|
+
* Effect.gen(function* () {
|
|
536
|
+
* return {
|
|
537
|
+
* fetch: Effect.succeed(HttpServerResponse.text("hello")),
|
|
538
|
+
* };
|
|
539
|
+
* }),
|
|
540
|
+
* ) {}
|
|
541
|
+
* ```
|
|
542
|
+
*
|
|
543
|
+
* ### Custom proxy services
|
|
544
|
+
* `services` defaults to HTTP 80 (redirecting to HTTPS) + HTTPS 443
|
|
545
|
+
* toward `port`, or plain HTTP 80 for a private Service. Pass a custom
|
|
546
|
+
* list to change handlers or autostop. Pass `[]` so Fly does not
|
|
547
|
+
* publish a proxy.
|
|
548
|
+
*
|
|
549
|
+
* **Example:** Unpublished process
|
|
550
|
+
* ```typescript
|
|
551
|
+
* export default class Worker extends Fly.Service<Worker>()(
|
|
552
|
+
* "Worker",
|
|
553
|
+
* { main: import.meta.url, region: "iad", services: [] },
|
|
554
|
+
* Effect.gen(function* () {
|
|
555
|
+
* return {};
|
|
556
|
+
* }),
|
|
557
|
+
* ) {}
|
|
558
|
+
* ```
|
|
559
|
+
*
|
|
560
|
+
* ### Background services
|
|
561
|
+
* Omit `port` and `fetch`. Pass `services: []`. Use `ServerHost.run`
|
|
562
|
+
* for a long-running loop. If the process exits, Fly restarts it.
|
|
563
|
+
*
|
|
564
|
+
* **Example:** ServerHost.run
|
|
565
|
+
* ```typescript
|
|
566
|
+
* import { ServerHost } from "alchemy/Runtime/Process";
|
|
567
|
+
*
|
|
568
|
+
* export default class Worker extends Fly.Service<Worker>()(
|
|
569
|
+
* "Worker",
|
|
570
|
+
* { main: import.meta.url, region: "iad", services: [] },
|
|
571
|
+
* Effect.gen(function* () {
|
|
572
|
+
* const host = yield* ServerHost;
|
|
573
|
+
*
|
|
574
|
+
* yield* host.run(
|
|
575
|
+
* Effect.gen(function* () {
|
|
576
|
+
* return yield* Effect.never;
|
|
577
|
+
* }).pipe(Effect.orDie),
|
|
578
|
+
* );
|
|
579
|
+
* }),
|
|
580
|
+
* ) {}
|
|
581
|
+
* ```
|
|
582
|
+
*
|
|
583
|
+
* ### Bundle config
|
|
584
|
+
* `build` is Rolldown `input` / `output` overrides plus
|
|
585
|
+
* pure-annotation options. Use it when `main` needs extra entry
|
|
586
|
+
* points or externals.
|
|
587
|
+
*
|
|
588
|
+
* **Example:** Externals
|
|
589
|
+
* ```typescript
|
|
590
|
+
* export default class Api extends Fly.Service<Api>()(
|
|
591
|
+
* "Api",
|
|
592
|
+
* {
|
|
593
|
+
* main: import.meta.url,
|
|
594
|
+
* port: 3000,
|
|
595
|
+
* build: { input: { external: ["sharp"] } },
|
|
596
|
+
* },
|
|
597
|
+
* Effect.gen(function* () {
|
|
598
|
+
* return {
|
|
599
|
+
* fetch: Effect.succeed(HttpServerResponse.text("hello")),
|
|
600
|
+
* };
|
|
601
|
+
* }),
|
|
602
|
+
* ) {}
|
|
603
|
+
* ```
|
|
604
|
+
*
|
|
605
|
+
* **Example:** Install a native package unbundled
|
|
606
|
+
* `build.install` runs `npm install` in the image for packages that
|
|
607
|
+
* cannot be bundled, such as native addons — same `build.install` as
|
|
608
|
+
* Lambda.
|
|
609
|
+
* ```typescript
|
|
610
|
+
* export default class Api extends Fly.Service<Api>()(
|
|
611
|
+
* "Api",
|
|
612
|
+
* {
|
|
613
|
+
* main: import.meta.url,
|
|
614
|
+
* port: 3000,
|
|
615
|
+
* build: { install: ["sharp"] },
|
|
616
|
+
* },
|
|
617
|
+
* Effect.gen(function* () {
|
|
618
|
+
* return {
|
|
619
|
+
* fetch: Effect.succeed(HttpServerResponse.text("hello")),
|
|
620
|
+
* };
|
|
621
|
+
* }),
|
|
622
|
+
* ) {}
|
|
623
|
+
* ```
|
|
624
|
+
*
|
|
625
|
+
* ### Group Services in one App
|
|
626
|
+
* Pass `app` to run a Service inside an existing {@link App} instead of
|
|
627
|
+
* its own. The Services share the App's hostname, addresses, and
|
|
628
|
+
* {@link Secret}s, and a {@link Certificate} on the App covers them.
|
|
629
|
+
* Alchemy manages no addresses for a Service in a shared App: allocate
|
|
630
|
+
* an {@link IpAssignment} on the App. `url` and `endpoints` follow the
|
|
631
|
+
* Service's own ports, so Services in one App tell themselves apart by
|
|
632
|
+
* port (`https://{appName}.fly.dev` and `https://{appName}.fly.dev:8443`).
|
|
633
|
+
* `public` does not apply.
|
|
634
|
+
*
|
|
635
|
+
* **Example:** API and worker sharing a secret
|
|
636
|
+
* ```typescript
|
|
637
|
+
* export const Site = Fly.App("Site");
|
|
638
|
+
*
|
|
639
|
+
* class Api extends Fly.Service<Api>()(
|
|
640
|
+
* "Api",
|
|
641
|
+
* { app: Site, main: import.meta.url, port: 3000 },
|
|
642
|
+
* Effect.gen(function* () {
|
|
643
|
+
* return {
|
|
644
|
+
* fetch: Effect.succeed(HttpServerResponse.text("hello")),
|
|
645
|
+
* };
|
|
646
|
+
* }),
|
|
647
|
+
* ) {}
|
|
648
|
+
*
|
|
649
|
+
* class Worker extends Fly.Service<Worker>()(
|
|
650
|
+
* "Worker",
|
|
651
|
+
* { app: Site, main: import.meta.url, services: [] },
|
|
652
|
+
* Effect.gen(function* () {
|
|
653
|
+
* return {};
|
|
654
|
+
* }),
|
|
655
|
+
* ) {}
|
|
656
|
+
* ```
|
|
657
|
+
*
|
|
658
|
+
* Fly's proxy routes an App's traffic by port only, so each Service in
|
|
659
|
+
* the App needs its own ports, including its own `bindingPort` when
|
|
660
|
+
* Alchemy adds one. Two Services on the same port and protocol fail with
|
|
661
|
+
* `Fly.ServicePortConflict` before any Machine is created: at plan time
|
|
662
|
+
* when the App already exists, otherwise when the App deploys. Grouped
|
|
663
|
+
* Services can be bound like any other; they cannot bind each other in
|
|
664
|
+
* a cycle.
|
|
665
|
+
*
|
|
666
|
+
* ### Blue/green deployments
|
|
667
|
+
* Opt into healthy replacement Machines instead of in-place updates.
|
|
668
|
+
* The default TCP service check proves the server is listening; supply
|
|
669
|
+
* service HTTP checks when readiness also depends on application state.
|
|
670
|
+
*
|
|
671
|
+
* **Example:** Single-replica HTTP replacement
|
|
672
|
+
* ```typescript
|
|
673
|
+
* export default class Api extends Fly.Service<Api>()(
|
|
674
|
+
* "Api",
|
|
675
|
+
* {
|
|
676
|
+
* main: import.meta.url,
|
|
677
|
+
* deploy: { strategy: "bluegreen" },
|
|
678
|
+
* shutdown: { timeout: "30 seconds" },
|
|
679
|
+
* },
|
|
680
|
+
* Effect.succeed({ fetch: Effect.succeed(HttpServerResponse.text("ready")) }),
|
|
681
|
+
* ) {}
|
|
682
|
+
* ```
|
|
683
|
+
*
|
|
684
|
+
* Keep one Service declaration. The old process retains its own shutdown
|
|
685
|
+
* signal and deadline when the replacement's policy changes. Managed
|
|
686
|
+
* SIGTERM/SIGINT shutdown drains HTTP while runtime resource finalizers run;
|
|
687
|
+
* shared dependencies remain alive until both settle or the deadline expires.
|
|
688
|
+
* Applications own stop-acquisition barriers, separately scoped jobs, and
|
|
689
|
+
* bounded drain or checkpoint logic using ordinary finalizers, not a new
|
|
690
|
+
* shutdown hook. External servers own their signal handling. An old bootstrap
|
|
691
|
+
* cannot gain handlers retroactively. Volumes are incompatible with blue/green;
|
|
692
|
+
* physical IDs and names change. Service-bound secret versions are floors,
|
|
693
|
+
* not vault snapshots, and native Machine leases are not deployment-wide locks.
|
|
694
|
+
* Leases do not serialize vault writers or every simultaneous first deployment;
|
|
695
|
+
* serialize CI invocations for the same resource.
|
|
696
|
+
*
|
|
697
|
+
* Stop and suspend autostop policies preserve idle nonrepresentatives while
|
|
698
|
+
* a representative and the required running floor pass readiness. Requested
|
|
699
|
+
* idle policy is restored before old retirement; a new instance needs fresh
|
|
700
|
+
* checks. Suspension is not SIGTERM shutdown and does not run ordinary
|
|
701
|
+
* shutdown finalizers. Replacements do not inherit suspended process memory.
|
|
702
|
+
* See the [deployment guide](/fly/compute/deployments) for recovery,
|
|
703
|
+
* idle capacity, application responsibilities, and verification limits.
|
|
704
|
+
*
|
|
705
|
+
* @resource
|
|
706
|
+
* @product Service
|
|
707
|
+
*/
|
|
708
|
+
export const Service = Platform("Fly.Service", {
|
|
709
|
+
createRuntimeContext: createFlyHostRuntimeContext("Fly.Service"),
|
|
710
|
+
// `{ app: Site }` at module scope is an Effect. Yield it here so the
|
|
711
|
+
// App is registered and `news.app` is resolved attributes at
|
|
712
|
+
// reconcile (same DX as `yield* App(...)` inside Effect.gen).
|
|
713
|
+
transformProps: (id, props) => Effect.gen(function* () {
|
|
714
|
+
if (globalThis.__ALCHEMY_RUNTIME__)
|
|
715
|
+
return props;
|
|
716
|
+
const app = Effect.isEffect(props.app)
|
|
717
|
+
? yield* props.app
|
|
718
|
+
: props.app;
|
|
719
|
+
// Only callers that bind this Service receive the token.
|
|
720
|
+
const rpcToken = yield* makeRandom(`${id}RpcToken`);
|
|
721
|
+
if (app !== undefined) {
|
|
722
|
+
// Report the ports this Service publishes so the App can reject
|
|
723
|
+
// two Services on the same port before either one is deployed.
|
|
724
|
+
yield* app.bind(id, {
|
|
725
|
+
service: id,
|
|
726
|
+
services: props.services,
|
|
727
|
+
bindingPort: props.bindingPort,
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
return { ...props, app, rpcToken };
|
|
731
|
+
}),
|
|
732
|
+
});
|
|
733
|
+
export class ServiceNotCreated extends Data.TaggedError("Fly.ServiceNotCreated") {
|
|
734
|
+
}
|
|
735
|
+
export class InvalidServiceProps extends Data.TaggedError("Fly.InvalidServiceProps") {
|
|
736
|
+
}
|
|
737
|
+
export class ServiceAppNotResolved extends Data.TaggedError("Fly.ServiceAppNotResolved") {
|
|
738
|
+
}
|
|
739
|
+
const appNameOf = (value) => {
|
|
740
|
+
if (value == null || typeof value !== "object")
|
|
741
|
+
return undefined;
|
|
742
|
+
const name = value.appName;
|
|
743
|
+
return typeof name === "string" && name.length > 0 ? name : undefined;
|
|
744
|
+
};
|
|
745
|
+
const compactRecord = (record) => Object.fromEntries(Object.entries(record ?? {}).flatMap(([key, value]) => value === undefined ? [] : [[key, value]]));
|
|
746
|
+
const toEnv = toEnvRecord;
|
|
747
|
+
const resolveMachineName = (id, name, existing) => Effect.gen(function* () {
|
|
748
|
+
if (name !== undefined)
|
|
749
|
+
return sanitizeFlyAppName(name);
|
|
750
|
+
if (existing !== undefined)
|
|
751
|
+
return existing;
|
|
752
|
+
return yield* createFlyResourceName(id);
|
|
753
|
+
});
|
|
754
|
+
const toFlyGuest = (guest) => {
|
|
755
|
+
const fly = {
|
|
756
|
+
cpu_kind: guest?.cpuKind ?? DEFAULT_CPU_KIND,
|
|
757
|
+
cpus: guest?.cpus ?? DEFAULT_CPUS,
|
|
758
|
+
memory_mb: guest?.memoryMb ?? DEFAULT_MEMORY_MB,
|
|
759
|
+
};
|
|
760
|
+
if (guest?.gpuKind !== undefined)
|
|
761
|
+
fly.gpu_kind = guest.gpuKind;
|
|
762
|
+
if (guest?.gpus !== undefined)
|
|
763
|
+
fly.gpus = guest.gpus;
|
|
764
|
+
return fly;
|
|
765
|
+
};
|
|
766
|
+
const desiredEnv = (props, bindingEnv, alchemyEnv, port) => ({
|
|
767
|
+
...toEnv(bindingEnv),
|
|
768
|
+
...alchemyEnv,
|
|
769
|
+
PORT: String(port),
|
|
770
|
+
...toEnv(props.env),
|
|
771
|
+
});
|
|
772
|
+
const toFlyStatics = (statics) => {
|
|
773
|
+
if (statics === undefined || statics.length === 0)
|
|
774
|
+
return undefined;
|
|
775
|
+
return statics.map((entry) => ({
|
|
776
|
+
guest_path: entry.guestPath,
|
|
777
|
+
url_prefix: entry.urlPrefix,
|
|
778
|
+
...(entry.tigrisBucket !== undefined ? { tigris_bucket: entry.tigrisBucket } : {}),
|
|
779
|
+
...(entry.indexDocument !== undefined ? { index_document: entry.indexDocument } : {}),
|
|
780
|
+
}));
|
|
781
|
+
};
|
|
782
|
+
const buildConfig = (input) => ({
|
|
783
|
+
image: input.image,
|
|
784
|
+
guest: input.guest,
|
|
785
|
+
env: Object.keys(input.env).length > 0 ? input.env : undefined,
|
|
786
|
+
services: input.services.length > 0 ? input.services : undefined,
|
|
787
|
+
mounts: input.mounts.length > 0 ? input.mounts : undefined,
|
|
788
|
+
metadata: input.metadata,
|
|
789
|
+
statics: input.statics !== undefined && input.statics.length > 0 ? input.statics : undefined,
|
|
790
|
+
});
|
|
791
|
+
const sameImage = (machine, image) => {
|
|
792
|
+
const configImage = machine.config?.image;
|
|
793
|
+
if (configImage === image)
|
|
794
|
+
return true;
|
|
795
|
+
const ref = machine.image_ref;
|
|
796
|
+
const colon = image.lastIndexOf(":");
|
|
797
|
+
const slash = image.lastIndexOf("/");
|
|
798
|
+
const split = colon > slash ? colon : -1;
|
|
799
|
+
const repo = split === -1 ? image : image.slice(0, split);
|
|
800
|
+
const tag = split === -1 ? "latest" : image.slice(split + 1);
|
|
801
|
+
const observedRepo = ref?.repository;
|
|
802
|
+
if (observedRepo === undefined || ref?.tag !== tag)
|
|
803
|
+
return false;
|
|
804
|
+
return observedRepo === repo || observedRepo.endsWith(`/${repo}`);
|
|
805
|
+
};
|
|
806
|
+
const sameGuest = (observed, desired) => (observed?.cpu_kind ?? DEFAULT_CPU_KIND) === desired.cpu_kind &&
|
|
807
|
+
(observed?.cpus ?? DEFAULT_CPUS) === desired.cpus &&
|
|
808
|
+
(observed?.memory_mb ?? DEFAULT_MEMORY_MB) === desired.memory_mb &&
|
|
809
|
+
observed?.gpu_kind === desired.gpu_kind &&
|
|
810
|
+
observed?.gpus === desired.gpus;
|
|
811
|
+
const sameEnv = (observed, desired) => deepEqual(compactRecord(observed), desired);
|
|
812
|
+
const sameMounts = (observed, desired) => {
|
|
813
|
+
const key = (mount) => `${mount.volume ?? ""}:${mount.path ?? ""}`;
|
|
814
|
+
const left = [...(observed ?? [])].map(key).sort();
|
|
815
|
+
const right = desired.map(key).sort();
|
|
816
|
+
return deepEqual(left, right);
|
|
817
|
+
};
|
|
818
|
+
const metadataChanged = (observed, desired) => {
|
|
819
|
+
const { removed, added, updated } = diffMachineMetadata(compactRecord(observed), desired);
|
|
820
|
+
return removed.length > 0 || Object.keys(added).length > 0 || Object.keys(updated).length > 0;
|
|
821
|
+
};
|
|
822
|
+
const sameStatics = (observed, desired) => {
|
|
823
|
+
const normalize = (statics) => (statics ?? []).map((entry) => ({
|
|
824
|
+
...entry,
|
|
825
|
+
// Empty and omitted index documents both disable directory indexes.
|
|
826
|
+
index_document: entry.index_document === "" ? undefined : entry.index_document,
|
|
827
|
+
}));
|
|
828
|
+
return deepEqual(normalize(observed), normalize(desired), { stripNullish: true });
|
|
829
|
+
};
|
|
830
|
+
const configDrifted = (machine, desired) => {
|
|
831
|
+
const config = machine.config;
|
|
832
|
+
return (!sameImage(machine, desired.image) ||
|
|
833
|
+
!sameGuest(config?.guest, desired.guest) ||
|
|
834
|
+
!sameEnv(config?.env, desired.env) ||
|
|
835
|
+
!sameServices(config?.services, desired.services) ||
|
|
836
|
+
!sameMounts(config?.mounts, desired.mounts) ||
|
|
837
|
+
metadataChanged(config?.metadata, desired.metadata) ||
|
|
838
|
+
!sameStatics(config?.statics, desired.statics));
|
|
839
|
+
};
|
|
840
|
+
const withPort = (base, port, standard) => port === standard ? base : `${base}:${port}`;
|
|
841
|
+
const isRedirect = (handlers, forceHttps) => forceHttps === true && handlers.includes("http") && !handlers.includes("tls");
|
|
842
|
+
/** Every published port on `host`, excluding HTTPS redirects and ranges. */
|
|
843
|
+
const endpointListOf = (host, services) => {
|
|
844
|
+
const flycast = host.endsWith(".flycast");
|
|
845
|
+
return (services ?? []).flatMap((service) => (service.ports ?? []).flatMap((entry) => {
|
|
846
|
+
const handlers = entry.handlers ?? [];
|
|
847
|
+
if (entry.port === undefined || isRedirect(handlers, entry.force_https))
|
|
848
|
+
return [];
|
|
849
|
+
const http = handlers.includes("http");
|
|
850
|
+
const tls = handlers.includes("tls");
|
|
851
|
+
const url = !http
|
|
852
|
+
? undefined
|
|
853
|
+
: tls
|
|
854
|
+
? flycast
|
|
855
|
+
? undefined
|
|
856
|
+
: withPort(`https://${host}`, entry.port, 443)
|
|
857
|
+
: withPort(`http://${host}`, entry.port, 80);
|
|
858
|
+
return [
|
|
859
|
+
{
|
|
860
|
+
host,
|
|
861
|
+
port: entry.port,
|
|
862
|
+
internalPort: service.internal_port,
|
|
863
|
+
protocol: service.protocol ?? "tcp",
|
|
864
|
+
handlers,
|
|
865
|
+
url,
|
|
866
|
+
},
|
|
867
|
+
];
|
|
868
|
+
}));
|
|
869
|
+
};
|
|
870
|
+
/** HTTPS on the first TLS port, else HTTP on the first plain HTTP port. */
|
|
871
|
+
const mainUrlOf = (endpoints) => endpoints.find((endpoint) => endpoint.url?.startsWith("https://"))?.url ??
|
|
872
|
+
endpoints.find((endpoint) => endpoint.url !== undefined)?.url;
|
|
873
|
+
/** `public` and `network` configure the Service's own App. */
|
|
874
|
+
const validateOwnership = (props) => {
|
|
875
|
+
if (props.app === undefined)
|
|
876
|
+
return Effect.void;
|
|
877
|
+
const conflicting = ["public", "network"].filter((key) => props[key] !== undefined);
|
|
878
|
+
return conflicting.length === 0
|
|
879
|
+
? Effect.void
|
|
880
|
+
: Effect.fail(new InvalidServiceProps({
|
|
881
|
+
message: `Fly.Service ${conflicting.map((key) => `\`${key}\``).join(" and ")} only apply when the Service owns its App; remove \`app\` or ${conflicting.map((key) => `\`${key}\``).join(" and ")}.`,
|
|
882
|
+
}));
|
|
883
|
+
};
|
|
884
|
+
/**
|
|
885
|
+
* A private network name unique to the current stack and stage, for the
|
|
886
|
+
* Service `network` prop. Services in one stage reach each other; other
|
|
887
|
+
* stages and every other App in the organization cannot.
|
|
888
|
+
*/
|
|
889
|
+
export const stackNetwork = Effect.gen(function* () {
|
|
890
|
+
const stack = yield* Stack;
|
|
891
|
+
const name = `${stack.name}-${stack.stage}`
|
|
892
|
+
.toLowerCase()
|
|
893
|
+
.replace(/[^a-z0-9-]+/g, "-")
|
|
894
|
+
.replace(/-+/g, "-")
|
|
895
|
+
.replace(/^-|-$/g, "")
|
|
896
|
+
.slice(0, 63)
|
|
897
|
+
.replace(/-$/, "");
|
|
898
|
+
return /^[a-z]/.test(name) ? name : `n-${name}`.slice(0, 63);
|
|
899
|
+
});
|
|
900
|
+
/** Drop the port Alchemy added for bindings from a services list. */
|
|
901
|
+
const withoutBindingPort = (services, bindingPort) => (services ?? []).flatMap((service) => {
|
|
902
|
+
const ports = (service.ports ?? []).filter((entry) => !(entry.port === bindingPort &&
|
|
903
|
+
(entry.handlers ?? []).includes("http") &&
|
|
904
|
+
!(entry.handlers ?? []).includes("tls")));
|
|
905
|
+
return ports.length === 0 ? [] : [{ ...service, ports }];
|
|
906
|
+
});
|
|
907
|
+
const endpointsOf = (set, access) => {
|
|
908
|
+
if (!hasPublishedService(set.services))
|
|
909
|
+
return { url: undefined, privateUrl: undefined, endpoints: [] };
|
|
910
|
+
const isPublic = !access.ownsApp || access.isPublic;
|
|
911
|
+
const binding = bindingPortOf(portsOfFly(set.services), access.bindingPort);
|
|
912
|
+
const visible = binding?.added ? withoutBindingPort(set.services, binding.port) : set.services;
|
|
913
|
+
const endpoints = endpointListOf(`${set.appName}.${isPublic ? "fly.dev" : "flycast"}`, visible);
|
|
914
|
+
return {
|
|
915
|
+
url: isPublic ? mainUrlOf(endpoints) : undefined,
|
|
916
|
+
privateUrl: binding === undefined
|
|
917
|
+
? undefined
|
|
918
|
+
: withPort(`http://${set.appName}.flycast`, binding.port, 80),
|
|
919
|
+
endpoints,
|
|
920
|
+
};
|
|
921
|
+
};
|
|
922
|
+
/**
|
|
923
|
+
* The Service's Fly services plus the plain-HTTP port for bound callers,
|
|
924
|
+
* when none of its published ports serves plain HTTP.
|
|
925
|
+
*/
|
|
926
|
+
const withBindingService = (services, internalPort, bindingPort) => {
|
|
927
|
+
const binding = bindingPortOf(portsOfFly(services), bindingPort);
|
|
928
|
+
if (binding?.added !== true)
|
|
929
|
+
return { services, added: false };
|
|
930
|
+
if (portsOfFly(services).some((entry) => entry.start === bindingPort))
|
|
931
|
+
return { services, added: true };
|
|
932
|
+
// Add the port to the first TCP service so it shares that service's
|
|
933
|
+
// internal port and health checks (blue/green requires checks on every
|
|
934
|
+
// published service).
|
|
935
|
+
const index = services.findIndex((service) => (service.protocol ?? "tcp") === "tcp");
|
|
936
|
+
if (index === -1) {
|
|
937
|
+
return {
|
|
938
|
+
added: true,
|
|
939
|
+
services: [
|
|
940
|
+
...services,
|
|
941
|
+
{
|
|
942
|
+
protocol: "tcp",
|
|
943
|
+
internal_port: internalPort,
|
|
944
|
+
ports: [{ port: bindingPort, handlers: ["http"] }],
|
|
945
|
+
},
|
|
946
|
+
],
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
return {
|
|
950
|
+
added: true,
|
|
951
|
+
services: services.map((service, i) => i === index
|
|
952
|
+
? {
|
|
953
|
+
...service,
|
|
954
|
+
ports: [...(service.ports ?? []), { port: bindingPort, handlers: ["http"] }],
|
|
955
|
+
}
|
|
956
|
+
: service),
|
|
957
|
+
};
|
|
958
|
+
};
|
|
959
|
+
/** Fly reports the organization's default network as `default`. */
|
|
960
|
+
const normalizeNetwork = (network) => network === undefined || network === null || network === "" || network === "default"
|
|
961
|
+
? undefined
|
|
962
|
+
: network;
|
|
963
|
+
export class ServiceUnreachable extends Data.TaggedError("Fly.ServiceUnreachable") {
|
|
964
|
+
get message() {
|
|
965
|
+
return `Fly Service ${this.service} binds ${this.target}, but ${this.service} is on the ${this.network ?? "default"} network and ${this.target} is on the ${this.targetNetwork ?? "default"} network. Put both on the same \`network\`.`;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
export class ServiceNotBindable extends Data.TaggedError("Fly.ServiceNotBindable") {
|
|
969
|
+
get message() {
|
|
970
|
+
return `Fly Service ${this.service} binds ${this.target}, which publishes no ports (\`services: []\`), so it has no private address.`;
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
export class EndpointNotPublished extends Data.TaggedError("Fly.EndpointNotPublished") {
|
|
974
|
+
get message() {
|
|
975
|
+
return `Fly Service ${this.service} binds port ${this.port} of ${this.target}, which does not publish it.`;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
/** Check every Service this one binds is reachable from its network. */
|
|
979
|
+
const validateTargets = (service, network, targets) => Effect.gen(function* () {
|
|
980
|
+
for (const target of targets) {
|
|
981
|
+
const targetNetwork = normalizeNetwork(target.network);
|
|
982
|
+
if (targetNetwork !== normalizeNetwork(network)) {
|
|
983
|
+
return yield* new ServiceUnreachable({
|
|
984
|
+
service,
|
|
985
|
+
target: target.service,
|
|
986
|
+
network: normalizeNetwork(network),
|
|
987
|
+
targetNetwork,
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
if (target.privateUrl === undefined) {
|
|
991
|
+
return yield* new ServiceNotBindable({ service, target: target.service });
|
|
992
|
+
}
|
|
993
|
+
if (target.port !== undefined &&
|
|
994
|
+
!(target.endpoints ?? []).some((entry) => entry.port === target.port)) {
|
|
995
|
+
return yield* new EndpointNotPublished({
|
|
996
|
+
service,
|
|
997
|
+
target: target.service,
|
|
998
|
+
port: target.port,
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
});
|
|
1003
|
+
/**
|
|
1004
|
+
* Fail when a Machine outside this Service already publishes one of its
|
|
1005
|
+
* ports in a shared App. Catches Machines the App's bindings do not list:
|
|
1006
|
+
* other stacks, the `fly` CLI, or plain `Fly.Machine`s.
|
|
1007
|
+
*/
|
|
1008
|
+
const validateObservedPorts = (appName, fqn, id, services) => Effect.gen(function* () {
|
|
1009
|
+
const listed = yield* machines
|
|
1010
|
+
.listMachines({ app_name: appName })
|
|
1011
|
+
.pipe(Effect.catchTag(["NotFound"], () => Effect.succeed([])));
|
|
1012
|
+
const others = new Map();
|
|
1013
|
+
for (const machine of listed) {
|
|
1014
|
+
if (machine.state === "destroyed")
|
|
1015
|
+
continue;
|
|
1016
|
+
const owner = machine.config?.metadata?.[alchemyMetadataKeys.fqn] ?? machine.id ?? "";
|
|
1017
|
+
if (owner === fqn || others.has(owner))
|
|
1018
|
+
continue;
|
|
1019
|
+
others.set(owner, machine.config?.services);
|
|
1020
|
+
}
|
|
1021
|
+
const mine = portsOfFly(services);
|
|
1022
|
+
for (const [owner, theirs] of others) {
|
|
1023
|
+
const conflict = findPortConflict(appName, [
|
|
1024
|
+
{ id, ports: mine },
|
|
1025
|
+
{ id: owner, ports: portsOfFly(theirs) },
|
|
1026
|
+
]);
|
|
1027
|
+
if (conflict !== undefined && conflict.publishers[0] !== conflict.publishers[1])
|
|
1028
|
+
return yield* conflict;
|
|
1029
|
+
}
|
|
1030
|
+
});
|
|
1031
|
+
const toAttrs = (set, codeHash, access) => ({
|
|
1032
|
+
appName: set.appName,
|
|
1033
|
+
ownsApp: access.ownsApp,
|
|
1034
|
+
network: normalizeNetwork(access.network),
|
|
1035
|
+
rpcToken: access.rpcToken,
|
|
1036
|
+
...endpointsOf(set, access),
|
|
1037
|
+
rolloutPending: set.rolloutPending,
|
|
1038
|
+
machineId: set.machineId,
|
|
1039
|
+
machineIds: set.machineIds,
|
|
1040
|
+
name: set.name,
|
|
1041
|
+
baseName: set.baseName,
|
|
1042
|
+
region: set.region,
|
|
1043
|
+
regions: set.regions,
|
|
1044
|
+
state: set.state,
|
|
1045
|
+
imageRef: set.imageRef,
|
|
1046
|
+
count: Math.round(set.count / Math.max(1, set.regions.length)),
|
|
1047
|
+
mounts: set.mounts,
|
|
1048
|
+
replicas: set.replicas,
|
|
1049
|
+
code: { hash: codeHash },
|
|
1050
|
+
});
|
|
1051
|
+
/** The replica set of a Service whose App exists but runs no Machines yet. */
|
|
1052
|
+
const emptyReplicaSet = (appName, name) => ({
|
|
1053
|
+
appName,
|
|
1054
|
+
machineId: "",
|
|
1055
|
+
machineIds: [],
|
|
1056
|
+
name,
|
|
1057
|
+
baseName: name,
|
|
1058
|
+
region: "",
|
|
1059
|
+
regions: [],
|
|
1060
|
+
state: "created",
|
|
1061
|
+
instanceId: undefined,
|
|
1062
|
+
privateIp: undefined,
|
|
1063
|
+
imageRef: undefined,
|
|
1064
|
+
guest: undefined,
|
|
1065
|
+
services: undefined,
|
|
1066
|
+
count: 0,
|
|
1067
|
+
mounts: [],
|
|
1068
|
+
replicas: [],
|
|
1069
|
+
});
|
|
1070
|
+
const machineIdsOf = (output) => output?.machineIds ??
|
|
1071
|
+
(output?.machineId !== undefined && output.machineId.length > 0 ? [output.machineId] : []);
|
|
1072
|
+
/** The private network the Service's Machines join. */
|
|
1073
|
+
const networkOf = (props) => normalizeNetwork(props.app === undefined ? props.network : props.app.network);
|
|
1074
|
+
/**
|
|
1075
|
+
* Resolve the App the Service runs in. A Service that owns its App creates
|
|
1076
|
+
* it with its addresses; one placed in an App gets a Flycast address on it
|
|
1077
|
+
* so bound callers can reach it.
|
|
1078
|
+
*/
|
|
1079
|
+
const ensureServiceApp = Effect.fn(function* (id, props, output) {
|
|
1080
|
+
yield* validateOwnership(props);
|
|
1081
|
+
const ownsApp = props.app === undefined;
|
|
1082
|
+
const isPublic = !ownsApp || props.public !== false;
|
|
1083
|
+
if (ownsApp) {
|
|
1084
|
+
const desired = props.name !== undefined
|
|
1085
|
+
? sanitizeFlyAppName(props.name)
|
|
1086
|
+
: output?.ownsApp === true
|
|
1087
|
+
? output.appName
|
|
1088
|
+
: yield* createFlyAppName(id);
|
|
1089
|
+
const app = yield* ensureApp({
|
|
1090
|
+
name: desired,
|
|
1091
|
+
network: props.network,
|
|
1092
|
+
previousName: output?.ownsApp === true ? output.appName : undefined,
|
|
1093
|
+
});
|
|
1094
|
+
const appName = app.name ?? desired;
|
|
1095
|
+
yield* syncOwnedAppAddresses(appName, isPublic, props.network);
|
|
1096
|
+
return { appName, ownsApp, isPublic, network: props.network };
|
|
1097
|
+
}
|
|
1098
|
+
const appName = appNameOf(props.app) ?? output?.appName;
|
|
1099
|
+
if (appName === undefined) {
|
|
1100
|
+
return yield* new ServiceAppNotResolved({
|
|
1101
|
+
message: "Fly.Service requires a resolved App with appName.",
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
const network = networkOf(props);
|
|
1105
|
+
yield* ensureFlycastAddress(appName, network);
|
|
1106
|
+
return { appName, ownsApp, isPublic, network };
|
|
1107
|
+
});
|
|
1108
|
+
export const ServiceProvider = () => Provider.effect(Service, Effect.gen(function* () {
|
|
1109
|
+
const stack = yield* Stack;
|
|
1110
|
+
const docker = yield* Docker;
|
|
1111
|
+
const { dotAlchemy } = yield* AlchemyContext;
|
|
1112
|
+
const virtualEntryPlugin = yield* Bundle.virtualEntryPlugin;
|
|
1113
|
+
const hosted = createFlyHostedSupport({
|
|
1114
|
+
stackName: stack.name,
|
|
1115
|
+
stage: stack.stage,
|
|
1116
|
+
virtualEntryPlugin,
|
|
1117
|
+
docker,
|
|
1118
|
+
dotAlchemy,
|
|
1119
|
+
});
|
|
1120
|
+
return Service.Provider.of({
|
|
1121
|
+
stables: ["appName"],
|
|
1122
|
+
nuke: { dependsOn: ["Fly.App"] },
|
|
1123
|
+
diff: Effect.fn(function* ({ news, output }) {
|
|
1124
|
+
if (news === undefined)
|
|
1125
|
+
return;
|
|
1126
|
+
const shape = news;
|
|
1127
|
+
const ownsApp = shape.app === undefined;
|
|
1128
|
+
yield* validateOwnership(shape);
|
|
1129
|
+
if ("main" in news) {
|
|
1130
|
+
const settings = {
|
|
1131
|
+
deploy: news.deploy,
|
|
1132
|
+
shutdown: news.shutdown,
|
|
1133
|
+
services: news.services,
|
|
1134
|
+
checks: news.checks,
|
|
1135
|
+
port: news.port,
|
|
1136
|
+
isExternal: news.isExternal,
|
|
1137
|
+
public: news.public,
|
|
1138
|
+
};
|
|
1139
|
+
if (isResolved(settings)) {
|
|
1140
|
+
yield* validateDeployment(yield* deploymentPolicy(settings.deploy, settings.shutdown, !settings.isExternal), {
|
|
1141
|
+
services: settings.services?.map(toFlyService) ??
|
|
1142
|
+
defaultHttpServices(settings.port ?? DEFAULT_PORT, 1, !ownsApp || settings.public !== false),
|
|
1143
|
+
checks: settings.checks,
|
|
1144
|
+
}, false);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
if (output === undefined)
|
|
1148
|
+
return;
|
|
1149
|
+
// Moving into or out of a shared App changes the hostname.
|
|
1150
|
+
if (ownsApp !== (output.ownsApp === true)) {
|
|
1151
|
+
return { action: "replace", deleteFirst: false };
|
|
1152
|
+
}
|
|
1153
|
+
if (ownsApp && isResolved(news)) {
|
|
1154
|
+
const desiredAppName = news.name !== undefined ? sanitizeFlyAppName(news.name) : output.appName;
|
|
1155
|
+
const nameChanged = desiredAppName !== output.appName;
|
|
1156
|
+
// Fly cannot move an App to another network.
|
|
1157
|
+
const networkChanged = normalizeNetwork(news.network) !== normalizeNetwork(output.network);
|
|
1158
|
+
if (nameChanged || networkChanged) {
|
|
1159
|
+
return {
|
|
1160
|
+
action: "replace",
|
|
1161
|
+
// A pinned App name cannot exist twice.
|
|
1162
|
+
deleteFirst: !nameChanged && news.name !== undefined,
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
if (!ownsApp && isResolved(news)) {
|
|
1167
|
+
const desiredAppName = appNameOf(news.app);
|
|
1168
|
+
const appChanged = desiredAppName !== undefined && desiredAppName !== output.appName;
|
|
1169
|
+
const desiredName = news.name !== undefined
|
|
1170
|
+
? sanitizeFlyAppName(news.name)
|
|
1171
|
+
: (output.baseName ?? output.name);
|
|
1172
|
+
const nameChanged = desiredName !== (output.baseName ?? output.name);
|
|
1173
|
+
if (appChanged || nameChanged) {
|
|
1174
|
+
return {
|
|
1175
|
+
action: "replace",
|
|
1176
|
+
deleteFirst: nameChanged === false && appChanged === false,
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
// The code hash depends only on statically-known props (`main`,
|
|
1181
|
+
// `build`, `image`, `port`, `extraFiles`, `isExternal`). Never
|
|
1182
|
+
// gate it on the WHOLE props being resolved: `app` is a resource
|
|
1183
|
+
// reference that stays unresolved at diff time, so a whole-props
|
|
1184
|
+
// guard makes code-only changes silently noop. By diff time the
|
|
1185
|
+
// effect-config form has been evaluated, so the object view is
|
|
1186
|
+
// safe to read.
|
|
1187
|
+
const statics = news;
|
|
1188
|
+
if (isResolved({
|
|
1189
|
+
main: statics.main,
|
|
1190
|
+
build: statics.build,
|
|
1191
|
+
image: statics.image,
|
|
1192
|
+
port: statics.port,
|
|
1193
|
+
extraFiles: statics.extraFiles,
|
|
1194
|
+
isExternal: statics.isExternal,
|
|
1195
|
+
}) &&
|
|
1196
|
+
statics.main !== undefined) {
|
|
1197
|
+
const hash = yield* hosted.hash(statics);
|
|
1198
|
+
if (hash !== output.code.hash) {
|
|
1199
|
+
return { action: "update" };
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
return output.rolloutPending ? { action: "update" } : undefined;
|
|
1203
|
+
}),
|
|
1204
|
+
read: Effect.fn(function* ({ id, fqn, instanceId, olds, output }) {
|
|
1205
|
+
const ownsApp = output?.ownsApp ?? (olds !== undefined && olds.app === undefined);
|
|
1206
|
+
const appName = ownsApp
|
|
1207
|
+
? (output?.appName ??
|
|
1208
|
+
(olds?.name !== undefined
|
|
1209
|
+
? sanitizeFlyAppName(olds.name)
|
|
1210
|
+
: yield* createFlyAppName(id)))
|
|
1211
|
+
: (appNameOf(olds?.app) ?? output?.appName);
|
|
1212
|
+
const name = yield* resolveMachineName(id, ownsApp ? undefined : olds?.name, output?.baseName ?? output?.name);
|
|
1213
|
+
const found = yield* observeReplicaSet({
|
|
1214
|
+
appName,
|
|
1215
|
+
id,
|
|
1216
|
+
type: "Fly.Service",
|
|
1217
|
+
fqn,
|
|
1218
|
+
resourceInstanceId: instanceId,
|
|
1219
|
+
machineIds: machineIdsOf(output),
|
|
1220
|
+
baseName: name,
|
|
1221
|
+
});
|
|
1222
|
+
if (found === undefined) {
|
|
1223
|
+
// A create interrupted between creating the Service's own App and
|
|
1224
|
+
// starting its Machines (an image build in between can take
|
|
1225
|
+
// minutes) leaves the App with nothing in it. Report it, so the
|
|
1226
|
+
// engine's recovery of the interrupted create deletes it instead
|
|
1227
|
+
// of leaking it. The App's name is derived from this resource's
|
|
1228
|
+
// instance, so an App with that name is this resource's.
|
|
1229
|
+
if (!ownsApp || appName === undefined)
|
|
1230
|
+
return undefined;
|
|
1231
|
+
const app = yield* machines
|
|
1232
|
+
.getApp({ app_name: appName })
|
|
1233
|
+
.pipe(Effect.catchTag("NotFound", () => Effect.undefined));
|
|
1234
|
+
if (app === undefined || !matchesAlchemyPhysicalName(app.name))
|
|
1235
|
+
return undefined;
|
|
1236
|
+
return toAttrs(emptyReplicaSet(appName, name), output?.code.hash ?? "", {
|
|
1237
|
+
ownsApp,
|
|
1238
|
+
isPublic: false,
|
|
1239
|
+
network: output?.network ?? olds?.network,
|
|
1240
|
+
bindingPort: olds?.bindingPort ?? DEFAULT_BINDING_PORT,
|
|
1241
|
+
rpcToken: output?.rpcToken,
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
return toAttrs(found, output?.code.hash ?? "", {
|
|
1245
|
+
ownsApp,
|
|
1246
|
+
isPublic: ownsApp && (yield* hasPublicAddress(found.appName)),
|
|
1247
|
+
network: output?.network ?? (ownsApp ? olds?.network : undefined),
|
|
1248
|
+
bindingPort: olds?.bindingPort ?? DEFAULT_BINDING_PORT,
|
|
1249
|
+
rpcToken: output?.rpcToken,
|
|
1250
|
+
});
|
|
1251
|
+
}),
|
|
1252
|
+
list: Effect.fn(function* () {
|
|
1253
|
+
const sets = yield* listReplicaSets("Fly.Service");
|
|
1254
|
+
// Owned Apps are also listed (and nuked) as Fly.App rows.
|
|
1255
|
+
return sets.map((set) => toAttrs(set, "", {
|
|
1256
|
+
ownsApp: false,
|
|
1257
|
+
isPublic: true,
|
|
1258
|
+
network: undefined,
|
|
1259
|
+
bindingPort: DEFAULT_BINDING_PORT,
|
|
1260
|
+
rpcToken: undefined,
|
|
1261
|
+
}));
|
|
1262
|
+
}),
|
|
1263
|
+
reconcile: Effect.fn(function* ({ id, fqn, instanceId, news, output, bindings, session }) {
|
|
1264
|
+
const props = news;
|
|
1265
|
+
const policy = yield* deploymentPolicy(props.deploy, props.shutdown, !props.isExternal);
|
|
1266
|
+
yield* validateOwnership(props);
|
|
1267
|
+
const bound = collectBindingState(bindings ?? []);
|
|
1268
|
+
// Before creating anything: every bound Service must be reachable.
|
|
1269
|
+
yield* validateTargets(id, networkOf(props), bound.targets);
|
|
1270
|
+
const { appName, ownsApp, isPublic, network } = yield* ensureServiceApp(id, props, output);
|
|
1271
|
+
const name = yield* resolveMachineName(id, ownsApp ? undefined : props.name, output?.baseName ?? output?.name);
|
|
1272
|
+
const regions = regionList(props.region, DEFAULT_REGION);
|
|
1273
|
+
const count = resolveCount(props.count);
|
|
1274
|
+
const port = props.port ?? DEFAULT_PORT;
|
|
1275
|
+
const bindingPort = props.bindingPort ?? DEFAULT_BINDING_PORT;
|
|
1276
|
+
const secretVersions = [];
|
|
1277
|
+
const redisVersion = yield* attachRedisSecrets(appName, bound.redis);
|
|
1278
|
+
if (redisVersion !== undefined)
|
|
1279
|
+
secretVersions.push(redisVersion);
|
|
1280
|
+
const bucketVersion = yield* attachBucketSecrets(appName, bound.buckets, {
|
|
1281
|
+
...bound.env,
|
|
1282
|
+
...toEnv(props.env),
|
|
1283
|
+
});
|
|
1284
|
+
if (bucketVersion !== undefined)
|
|
1285
|
+
secretVersions.push(bucketVersion);
|
|
1286
|
+
for (const pg of bound.postgres) {
|
|
1287
|
+
const version = yield* attachPostgresSecrets(appName, pg.clusterId, pg.variableName);
|
|
1288
|
+
if (version !== undefined)
|
|
1289
|
+
secretVersions.push(version);
|
|
1290
|
+
}
|
|
1291
|
+
const minSecretsVersion = secretVersions.length > 0 ? Math.max(...secretVersions) : undefined;
|
|
1292
|
+
const env = desiredEnv(props, bound.env, hosted.alchemyEnv, port);
|
|
1293
|
+
if (policy.shutdown && !props.isExternal) {
|
|
1294
|
+
env.ALCHEMY_FLY_SHUTDOWN_TIMEOUT_MS = String(policy.shutdown.timeoutMs);
|
|
1295
|
+
}
|
|
1296
|
+
const guest = toFlyGuest(props.guest);
|
|
1297
|
+
const published = withBindingService(props.services !== undefined
|
|
1298
|
+
? props.services.map(toFlyService)
|
|
1299
|
+
: defaultHttpServices(port, count, isPublic), port, bindingPort);
|
|
1300
|
+
const services = published.services;
|
|
1301
|
+
if (!ownsApp)
|
|
1302
|
+
yield* validateObservedPorts(appName, fqn, id, services);
|
|
1303
|
+
if (props.rpcToken !== undefined)
|
|
1304
|
+
env[RPC_TOKEN_ENV] = Redacted.value(props.rpcToken);
|
|
1305
|
+
for (const target of bound.targets) {
|
|
1306
|
+
const keys = boundTargetEnvKeys(target.service);
|
|
1307
|
+
env[keys.appName] = packEnvValue(target.appName);
|
|
1308
|
+
if (target.privateUrl !== undefined)
|
|
1309
|
+
env[keys.url] = packEnvValue(target.privateUrl);
|
|
1310
|
+
if (target.rpcToken !== undefined)
|
|
1311
|
+
env[keys.token] = packEnvValue(target.rpcToken);
|
|
1312
|
+
}
|
|
1313
|
+
env[RPC_ORG_ENV] = yield* resolveOrgSlug();
|
|
1314
|
+
if (published.added)
|
|
1315
|
+
env[BINDING_PORT_ENV] = String(bindingPort);
|
|
1316
|
+
const statics = toFlyStatics(props.statics);
|
|
1317
|
+
const { imageRef, codeHash } = yield* hosted.resolveImage({
|
|
1318
|
+
id,
|
|
1319
|
+
appName,
|
|
1320
|
+
props,
|
|
1321
|
+
previousHash: output?.code.hash,
|
|
1322
|
+
session,
|
|
1323
|
+
});
|
|
1324
|
+
const set = yield* reconcileReplicas({
|
|
1325
|
+
id,
|
|
1326
|
+
type: "Fly.Service",
|
|
1327
|
+
fqn,
|
|
1328
|
+
resourceInstanceId: instanceId,
|
|
1329
|
+
policy,
|
|
1330
|
+
checks: props.checks,
|
|
1331
|
+
appName,
|
|
1332
|
+
baseName: name,
|
|
1333
|
+
regions,
|
|
1334
|
+
// `count` is per region.
|
|
1335
|
+
count: count * regions.length,
|
|
1336
|
+
disks: bound.mounts,
|
|
1337
|
+
minSecretsVersion,
|
|
1338
|
+
outputMachineIds: machineIdsOf(output),
|
|
1339
|
+
preferVolumeIds: (output?.replicas ?? []).map((replica) => replica.mounts.map((mount) => mount.volumeId)),
|
|
1340
|
+
configDrifted: (machine, desired) => configDrifted(machine, {
|
|
1341
|
+
image: imageRef,
|
|
1342
|
+
guest,
|
|
1343
|
+
env,
|
|
1344
|
+
services,
|
|
1345
|
+
mounts: desired.mounts,
|
|
1346
|
+
metadata: desired.metadata,
|
|
1347
|
+
statics,
|
|
1348
|
+
}),
|
|
1349
|
+
buildConfig: ({ mounts, metadata }) => buildConfig({ image: imageRef, guest, env, services, mounts, metadata, statics }),
|
|
1350
|
+
}).pipe(Effect.catchTag("Fly.ReplicaNotCreated", (error) => Effect.fail(new ServiceNotCreated({ resourceName: error.resourceName, appName: error.appName }))));
|
|
1351
|
+
return toAttrs(set, codeHash, {
|
|
1352
|
+
ownsApp,
|
|
1353
|
+
isPublic,
|
|
1354
|
+
network,
|
|
1355
|
+
bindingPort,
|
|
1356
|
+
rpcToken: props.rpcToken,
|
|
1357
|
+
});
|
|
1358
|
+
}),
|
|
1359
|
+
// Create the App, its addresses, and the caller token before any
|
|
1360
|
+
// Machine, so two Services that bind each other can both resolve
|
|
1361
|
+
// the other's private address and token.
|
|
1362
|
+
precreate: Effect.fn(function* ({ id, news }) {
|
|
1363
|
+
const props = news;
|
|
1364
|
+
// Precreate runs before dependencies resolve. A Service placed in
|
|
1365
|
+
// an App cannot see the App yet, so it has no stub to offer;
|
|
1366
|
+
// cycles are supported between Services that own their App.
|
|
1367
|
+
const inert = props.app !== undefined ||
|
|
1368
|
+
!isResolved(props.name) ||
|
|
1369
|
+
!isResolved(props.network) ||
|
|
1370
|
+
!isResolved(props.services) ||
|
|
1371
|
+
!isResolved(props.bindingPort);
|
|
1372
|
+
// No stub: a failed create then recovers through `read`, which
|
|
1373
|
+
// finds the App from the persisted props.
|
|
1374
|
+
if (inert)
|
|
1375
|
+
return undefined;
|
|
1376
|
+
const { appName, ownsApp, isPublic, network } = yield* ensureServiceApp(id, props, undefined);
|
|
1377
|
+
const port = props.port ?? DEFAULT_PORT;
|
|
1378
|
+
const count = resolveCount(props.count);
|
|
1379
|
+
const bindingPort = props.bindingPort ?? DEFAULT_BINDING_PORT;
|
|
1380
|
+
const { services } = withBindingService(props.services !== undefined
|
|
1381
|
+
? props.services.map(toFlyService)
|
|
1382
|
+
: defaultHttpServices(port, count, isPublic), port, bindingPort);
|
|
1383
|
+
const name = yield* resolveMachineName(id, ownsApp ? undefined : props.name);
|
|
1384
|
+
const stub = {
|
|
1385
|
+
appName,
|
|
1386
|
+
machineId: "",
|
|
1387
|
+
machineIds: [],
|
|
1388
|
+
name,
|
|
1389
|
+
baseName: name,
|
|
1390
|
+
region: regionList(props.region, DEFAULT_REGION)[0],
|
|
1391
|
+
regions: regionList(props.region, DEFAULT_REGION),
|
|
1392
|
+
state: "created",
|
|
1393
|
+
instanceId: undefined,
|
|
1394
|
+
privateIp: undefined,
|
|
1395
|
+
imageRef: undefined,
|
|
1396
|
+
guest: undefined,
|
|
1397
|
+
services,
|
|
1398
|
+
count: 0,
|
|
1399
|
+
mounts: [],
|
|
1400
|
+
replicas: [],
|
|
1401
|
+
};
|
|
1402
|
+
return toAttrs(stub, "", {
|
|
1403
|
+
ownsApp,
|
|
1404
|
+
isPublic,
|
|
1405
|
+
network,
|
|
1406
|
+
bindingPort,
|
|
1407
|
+
rpcToken: isResolved(props.rpcToken) ? props.rpcToken : undefined,
|
|
1408
|
+
});
|
|
1409
|
+
}),
|
|
1410
|
+
delete: Effect.fn(function* ({ id, fqn, instanceId, olds, output, force }) {
|
|
1411
|
+
const appName = output.appName || appNameOf(olds.app);
|
|
1412
|
+
// A precreate stub that never reached its App has nothing to delete.
|
|
1413
|
+
if (appName === undefined || appName.length === 0)
|
|
1414
|
+
return;
|
|
1415
|
+
yield* deleteReplicaSet({
|
|
1416
|
+
appName,
|
|
1417
|
+
id,
|
|
1418
|
+
type: "Fly.Service",
|
|
1419
|
+
fqn,
|
|
1420
|
+
resourceInstanceId: instanceId,
|
|
1421
|
+
machineIds: machineIdsOf(output),
|
|
1422
|
+
volumeIds: volumeIdsOf(output),
|
|
1423
|
+
force,
|
|
1424
|
+
});
|
|
1425
|
+
if (output.ownsApp === true)
|
|
1426
|
+
yield* deleteApp(appName);
|
|
1427
|
+
}),
|
|
1428
|
+
});
|
|
1429
|
+
})).pipe(Layer.provide(DockerLive));
|
|
1430
|
+
//# sourceMappingURL=Service.js.map
|