@alchemy.run/fly 2.0.0-beta.84 → 2.0.0-beta.86
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/lib/App.d.ts +27 -5
- package/lib/App.d.ts.map +1 -1
- package/lib/App.js +29 -8
- package/lib/App.js.map +1 -1
- package/lib/AuthProvider.d.ts +1 -1
- package/lib/AuthProvider.d.ts.map +1 -1
- package/lib/AuthProvider.js.map +1 -1
- package/lib/BindService.d.ts +7 -7
- package/lib/BindService.d.ts.map +1 -1
- package/lib/BindService.js +21 -28
- package/lib/BindService.js.map +1 -1
- package/lib/Bootstrap.d.ts.map +1 -1
- package/lib/Bootstrap.js +2 -1
- package/lib/Bootstrap.js.map +1 -1
- package/lib/Bucket.d.ts +38 -4
- package/lib/Bucket.d.ts.map +1 -1
- package/lib/Bucket.js +190 -37
- package/lib/Bucket.js.map +1 -1
- package/lib/BucketBinding.d.ts.map +1 -1
- package/lib/BucketBinding.js +1 -1
- package/lib/BucketBinding.js.map +1 -1
- package/lib/Catalog.d.ts.map +1 -1
- package/lib/Catalog.js.map +1 -1
- package/lib/Certificate.d.ts +25 -1
- package/lib/Certificate.d.ts.map +1 -1
- package/lib/Certificate.js +22 -4
- package/lib/Certificate.js.map +1 -1
- package/lib/Checkpoint.d.ts.map +1 -1
- package/lib/Checkpoint.js.map +1 -1
- package/lib/CheckpointHttp.d.ts.map +1 -1
- package/lib/CheckpointHttp.js.map +1 -1
- package/lib/ConnectPostgres.d.ts +1 -1
- package/lib/ConnectPostgres.d.ts.map +1 -1
- package/lib/ConnectPostgres.js.map +1 -1
- package/lib/ConnectPostgresHttp.d.ts.map +1 -1
- package/lib/ConnectPostgresHttp.js +1 -1
- package/lib/ConnectPostgresHttp.js.map +1 -1
- package/lib/Contributions.d.ts +1 -1
- package/lib/Contributions.d.ts.map +1 -1
- package/lib/Contributions.js.map +1 -1
- package/lib/Credentials.d.ts +5 -4
- package/lib/Credentials.d.ts.map +1 -1
- package/lib/Credentials.js +18 -18
- package/lib/Credentials.js.map +1 -1
- package/lib/Decrypt.d.ts.map +1 -1
- package/lib/Decrypt.js.map +1 -1
- package/lib/DecryptHttp.d.ts.map +1 -1
- package/lib/DecryptHttp.js.map +1 -1
- package/lib/DeleteObject.d.ts.map +1 -1
- package/lib/DeleteObject.js.map +1 -1
- package/lib/DeleteObjectHttp.d.ts.map +1 -1
- package/lib/DeleteObjectHttp.js.map +1 -1
- package/lib/Deployment.d.ts.map +1 -1
- package/lib/Deployment.js.map +1 -1
- package/lib/DeploymentImages.d.ts.map +1 -1
- package/lib/DeploymentImages.js +1 -1
- package/lib/DeploymentImages.js.map +1 -1
- package/lib/DeploymentState.d.ts.map +1 -1
- package/lib/DeploymentState.js.map +1 -1
- package/lib/Encrypt.d.ts.map +1 -1
- package/lib/Encrypt.js.map +1 -1
- package/lib/EncryptHttp.d.ts.map +1 -1
- package/lib/EncryptHttp.js.map +1 -1
- package/lib/Environment.d.ts.map +1 -1
- package/lib/Environment.js.map +1 -1
- package/lib/Errors.d.ts +1 -1
- package/lib/Errors.d.ts.map +1 -1
- package/lib/Errors.js.map +1 -1
- package/lib/Exec.d.ts.map +1 -1
- package/lib/Exec.js.map +1 -1
- package/lib/ExecHttp.d.ts.map +1 -1
- package/lib/ExecHttp.js.map +1 -1
- package/lib/GetObject.d.ts.map +1 -1
- package/lib/GetObject.js.map +1 -1
- package/lib/GetObjectHttp.d.ts.map +1 -1
- package/lib/GetObjectHttp.js.map +1 -1
- package/lib/GetSecret.d.ts.map +1 -1
- package/lib/GetSecret.js.map +1 -1
- package/lib/GetSecretHttp.d.ts.map +1 -1
- package/lib/GetSecretHttp.js.map +1 -1
- package/lib/HeadObject.d.ts.map +1 -1
- package/lib/HeadObject.js.map +1 -1
- package/lib/HeadObjectHttp.d.ts.map +1 -1
- package/lib/HeadObjectHttp.js.map +1 -1
- package/lib/IpAssignment.d.ts +24 -1
- package/lib/IpAssignment.d.ts.map +1 -1
- package/lib/IpAssignment.js +21 -4
- package/lib/IpAssignment.js.map +1 -1
- package/lib/ListObjectsV2.d.ts.map +1 -1
- package/lib/ListObjectsV2.js.map +1 -1
- package/lib/ListObjectsV2Http.d.ts.map +1 -1
- package/lib/ListObjectsV2Http.js.map +1 -1
- package/lib/ListSecrets.d.ts.map +1 -1
- package/lib/ListSecrets.js.map +1 -1
- package/lib/ListSecretsHttp.d.ts.map +1 -1
- package/lib/ListSecretsHttp.js.map +1 -1
- package/lib/Machine.d.ts +27 -2
- package/lib/Machine.d.ts.map +1 -1
- package/lib/Machine.js +18 -3
- package/lib/Machine.js.map +1 -1
- package/lib/MachineContainers.d.ts.map +1 -1
- package/lib/MachineContainers.js.map +1 -1
- package/lib/Metadata.d.ts.map +1 -1
- package/lib/Metadata.js.map +1 -1
- package/lib/MountVolume.d.ts +7 -2
- package/lib/MountVolume.d.ts.map +1 -1
- package/lib/MountVolume.js.map +1 -1
- package/lib/Postgres.d.ts +34 -2
- package/lib/Postgres.d.ts.map +1 -1
- package/lib/Postgres.js +26 -6
- package/lib/Postgres.js.map +1 -1
- package/lib/PostgresMigrations.d.ts +7 -22
- package/lib/PostgresMigrations.d.ts.map +1 -1
- package/lib/PostgresMigrations.js +37 -89
- package/lib/PostgresMigrations.js.map +1 -1
- package/lib/Providers.d.ts +1 -1
- package/lib/Providers.d.ts.map +1 -1
- package/lib/Providers.js.map +1 -1
- package/lib/PutObject.d.ts.map +1 -1
- package/lib/PutObject.js.map +1 -1
- package/lib/PutObjectHttp.d.ts.map +1 -1
- package/lib/PutObjectHttp.js.map +1 -1
- package/lib/ReadRedis.d.ts.map +1 -1
- package/lib/ReadRedis.js.map +1 -1
- package/lib/ReadRedisHttp.d.ts.map +1 -1
- package/lib/ReadRedisHttp.js.map +1 -1
- package/lib/ReadWriteRedis.d.ts.map +1 -1
- package/lib/ReadWriteRedis.js.map +1 -1
- package/lib/ReadWriteRedisHttp.d.ts.map +1 -1
- package/lib/ReadWriteRedisHttp.js.map +1 -1
- package/lib/Redis.d.ts +23 -3
- package/lib/Redis.d.ts.map +1 -1
- package/lib/Redis.js +37 -6
- package/lib/Redis.js.map +1 -1
- package/lib/RedisBinding.d.ts.map +1 -1
- package/lib/RedisBinding.js +1 -1
- package/lib/RedisBinding.js.map +1 -1
- package/lib/RedisHttp.d.ts +1 -1
- package/lib/RedisHttp.d.ts.map +1 -1
- package/lib/RedisHttp.js +4 -1
- package/lib/RedisHttp.js.map +1 -1
- package/lib/Region.d.ts.map +1 -1
- package/lib/Region.js.map +1 -1
- package/lib/Secret.d.ts +23 -2
- package/lib/Secret.d.ts.map +1 -1
- package/lib/Secret.js +17 -2
- package/lib/Secret.js.map +1 -1
- package/lib/SecretHttp.d.ts.map +1 -1
- package/lib/SecretHttp.js.map +1 -1
- package/lib/SecretKey.d.ts +23 -2
- package/lib/SecretKey.d.ts.map +1 -1
- package/lib/SecretKey.js +17 -2
- package/lib/SecretKey.js.map +1 -1
- package/lib/SecretKeyHttp.d.ts.map +1 -1
- package/lib/SecretKeyHttp.js.map +1 -1
- package/lib/Service.d.ts +104 -34
- package/lib/Service.d.ts.map +1 -1
- package/lib/Service.js +301 -54
- package/lib/Service.js.map +1 -1
- package/lib/Sign.d.ts.map +1 -1
- package/lib/Sign.js.map +1 -1
- package/lib/SignHttp.d.ts.map +1 -1
- package/lib/SignHttp.js.map +1 -1
- package/lib/Sprite.d.ts +24 -5
- package/lib/Sprite.d.ts.map +1 -1
- package/lib/Sprite.js +22 -5
- package/lib/Sprite.js.map +1 -1
- package/lib/SpriteHttp.d.ts.map +1 -1
- package/lib/SpriteHttp.js.map +1 -1
- package/lib/Verify.d.ts.map +1 -1
- package/lib/Verify.js.map +1 -1
- package/lib/VerifyHttp.d.ts.map +1 -1
- package/lib/VerifyHttp.js.map +1 -1
- package/lib/Volume.d.ts +4 -4
- package/lib/Volume.d.ts.map +1 -1
- package/lib/Volume.js +21 -5
- package/lib/Volume.js.map +1 -1
- package/lib/VolumeSnapshot.d.ts +32 -1
- package/lib/VolumeSnapshot.d.ts.map +1 -1
- package/lib/VolumeSnapshot.js +24 -1
- package/lib/VolumeSnapshot.js.map +1 -1
- package/lib/Website/AssetDeployment.d.ts.map +1 -1
- package/lib/Website/AssetDeployment.js +9 -9
- package/lib/Website/AssetDeployment.js.map +1 -1
- package/lib/Website/Astro.d.ts +8 -6
- package/lib/Website/Astro.d.ts.map +1 -1
- package/lib/Website/Astro.js +12 -13
- package/lib/Website/Astro.js.map +1 -1
- package/lib/Website/Foldkit.d.ts.map +1 -1
- package/lib/Website/Foldkit.js.map +1 -1
- package/lib/Website/FrameworkSite.d.ts +39 -2
- package/lib/Website/FrameworkSite.d.ts.map +1 -1
- package/lib/Website/FrameworkSite.js +46 -3
- package/lib/Website/FrameworkSite.js.map +1 -1
- package/lib/Website/Nextjs.d.ts.map +1 -1
- package/lib/Website/Nextjs.js.map +1 -1
- package/lib/Website/Nuxt.d.ts.map +1 -1
- package/lib/Website/Nuxt.js.map +1 -1
- package/lib/Website/Octane.d.ts.map +1 -1
- package/lib/Website/Octane.js.map +1 -1
- package/lib/Website/ReactRouter.d.ts.map +1 -1
- package/lib/Website/ReactRouter.js.map +1 -1
- package/lib/Website/SolidStart.d.ts.map +1 -1
- package/lib/Website/SolidStart.js.map +1 -1
- package/lib/Website/StaticSite.d.ts +10 -3
- package/lib/Website/StaticSite.d.ts.map +1 -1
- package/lib/Website/StaticSite.js +16 -4
- package/lib/Website/StaticSite.js.map +1 -1
- package/lib/Website/SvelteKit.d.ts.map +1 -1
- package/lib/Website/SvelteKit.js.map +1 -1
- package/lib/Website/TanStackStart.d.ts.map +1 -1
- package/lib/Website/TanStackStart.js.map +1 -1
- package/lib/Website/Vinext.d.ts.map +1 -1
- package/lib/Website/Vinext.js.map +1 -1
- package/lib/Website/Vite.d.ts +9 -0
- package/lib/Website/Vite.d.ts.map +1 -1
- package/lib/Website/Vite.js +9 -0
- package/lib/Website/Vite.js.map +1 -1
- package/lib/Website/Vocs.d.ts.map +1 -1
- package/lib/Website/Vocs.js.map +1 -1
- package/lib/Website/Waku.d.ts.map +1 -1
- package/lib/Website/Waku.js.map +1 -1
- package/lib/Website/index.d.ts.map +1 -1
- package/lib/Website/index.js.map +1 -1
- package/lib/WriteCertificates.d.ts.map +1 -1
- package/lib/WriteCertificates.js.map +1 -1
- package/lib/WriteCertificatesHttp.d.ts.map +1 -1
- package/lib/WriteCertificatesHttp.js +2 -2
- package/lib/WriteCertificatesHttp.js.map +1 -1
- package/lib/WriteRedis.d.ts.map +1 -1
- package/lib/WriteRedis.js.map +1 -1
- package/lib/WriteRedisHttp.d.ts.map +1 -1
- package/lib/WriteRedisHttp.js.map +1 -1
- package/lib/WriteSecret.d.ts.map +1 -1
- package/lib/WriteSecret.js.map +1 -1
- package/lib/WriteSecretHttp.d.ts.map +1 -1
- package/lib/WriteSecretHttp.js.map +1 -1
- package/lib/bluegreen.d.ts +5 -4
- package/lib/bluegreen.d.ts.map +1 -1
- package/lib/bluegreen.js +7 -5
- package/lib/bluegreen.js.map +1 -1
- package/lib/hosted.d.ts +19 -4
- package/lib/hosted.d.ts.map +1 -1
- package/lib/hosted.js +101 -11
- package/lib/hosted.js.map +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js.map +1 -1
- package/lib/internal/Oidc.d.ts +15 -0
- package/lib/internal/Oidc.d.ts.map +1 -0
- package/lib/internal/Oidc.js +73 -0
- package/lib/internal/Oidc.js.map +1 -0
- package/lib/internal/RemoteBuilder.d.ts.map +1 -1
- package/lib/internal/RemoteBuilder.js +21 -8
- package/lib/internal/RemoteBuilder.js.map +1 -1
- package/lib/leases.d.ts +6 -2
- package/lib/leases.d.ts.map +1 -1
- package/lib/leases.js +25 -5
- package/lib/leases.js.map +1 -1
- package/lib/ports.d.ts.map +1 -1
- package/lib/ports.js.map +1 -1
- package/lib/replicas.d.ts +39 -27
- package/lib/replicas.d.ts.map +1 -1
- package/lib/replicas.js +63 -8
- package/lib/replicas.js.map +1 -1
- package/lib/rpc.d.ts +14 -12
- package/lib/rpc.d.ts.map +1 -1
- package/lib/rpc.js +38 -40
- package/lib/rpc.js.map +1 -1
- package/package.json +15 -15
- package/src/App.ts +41 -10
- package/src/BindService.ts +28 -36
- package/src/Bootstrap.ts +2 -0
- package/src/Bucket.ts +278 -45
- package/src/BucketBinding.ts +1 -1
- package/src/Certificate.ts +36 -5
- package/src/ConnectPostgres.ts +1 -1
- package/src/ConnectPostgresHttp.ts +1 -1
- package/src/Credentials.ts +18 -41
- package/src/DeploymentImages.ts +1 -1
- package/src/Errors.ts +1 -1
- package/src/IpAssignment.ts +35 -5
- package/src/Machine.ts +39 -4
- package/src/MountVolume.ts +5 -2
- package/src/Postgres.ts +50 -8
- package/src/PostgresMigrations.ts +39 -99
- package/src/Redis.ts +54 -9
- package/src/RedisBinding.ts +1 -1
- package/src/RedisHttp.ts +4 -1
- package/src/Secret.ts +32 -4
- package/src/SecretKey.ts +32 -4
- package/src/Service.ts +441 -88
- package/src/Sprite.ts +45 -17
- package/src/Volume.ts +30 -6
- package/src/VolumeSnapshot.ts +40 -2
- package/src/Website/AssetDeployment.ts +13 -15
- package/src/Website/Astro.ts +20 -15
- package/src/Website/FrameworkSite.ts +92 -6
- package/src/Website/StaticSite.ts +27 -7
- package/src/Website/Vite.ts +9 -0
- package/src/WriteCertificatesHttp.ts +2 -2
- package/src/bluegreen.ts +11 -5
- package/src/hosted.ts +121 -17
- package/src/internal/Oidc.ts +99 -0
- package/src/internal/RemoteBuilder.ts +37 -19
- package/src/leases.ts +35 -7
- package/src/replicas.ts +70 -9
- package/src/rpc.ts +37 -40
package/src/Service.ts
CHANGED
|
@@ -14,11 +14,11 @@ import { deepEqual, isResolved } from "alchemy/Diff";
|
|
|
14
14
|
import type { Input } from "alchemy/Input";
|
|
15
15
|
import { Platform, type Main, type MainRpc, type PlatformProps } from "alchemy/Platform";
|
|
16
16
|
import * as Provider from "alchemy/Provider";
|
|
17
|
-
import {
|
|
18
|
-
import type { Resource } from "alchemy/Resource";
|
|
17
|
+
import type { LookupMethod, Resource } from "alchemy/Resource";
|
|
19
18
|
import type { ServerHost } from "alchemy/Runtime/Process";
|
|
20
19
|
import { packEnvValue } from "alchemy/RuntimeContext";
|
|
21
20
|
import { Stack } from "alchemy/Stack";
|
|
21
|
+
import { sha256 } from "alchemy/Util/sha256";
|
|
22
22
|
import * as Data from "effect/Data";
|
|
23
23
|
import * as Effect from "effect/Effect";
|
|
24
24
|
import * as Layer from "effect/Layer";
|
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
createFlyAppName,
|
|
53
53
|
createFlyResourceName,
|
|
54
54
|
diffMachineMetadata,
|
|
55
|
+
matchesAlchemyPhysicalName,
|
|
55
56
|
sanitizeFlyAppName,
|
|
56
57
|
} from "./Metadata.ts";
|
|
57
58
|
import type { BoundTarget, MountedDisk, ServiceBinding } from "./MountVolume.ts";
|
|
@@ -64,6 +65,7 @@ import {
|
|
|
64
65
|
deleteReplicaSet,
|
|
65
66
|
hasPublishedService,
|
|
66
67
|
listReplicaSets,
|
|
68
|
+
lookupReplicaSet,
|
|
67
69
|
observeReplicaSet,
|
|
68
70
|
reconcileReplicas,
|
|
69
71
|
resolveCount,
|
|
@@ -73,7 +75,7 @@ import {
|
|
|
73
75
|
type Replica,
|
|
74
76
|
type ReplicaSet,
|
|
75
77
|
} from "./replicas.ts";
|
|
76
|
-
import { BINDING_PORT_ENV, DEFAULT_BINDING_PORT,
|
|
78
|
+
import { BINDING_PORT_ENV, DEFAULT_BINDING_PORT, RPC_CALLERS_ENV, RPC_ORG_ENV } from "./rpc.ts";
|
|
77
79
|
|
|
78
80
|
/**
|
|
79
81
|
* A resource-valued prop: the resource itself, or an Effect that produces
|
|
@@ -139,18 +141,21 @@ export interface ServiceProps extends PlatformProps {
|
|
|
139
141
|
* @default 7780
|
|
140
142
|
*/
|
|
141
143
|
bindingPort?: number;
|
|
142
|
-
/**
|
|
143
|
-
* Caller token for bound callers. Set automatically from an
|
|
144
|
-
* `Alchemy.Random` child resource; do not pass it.
|
|
145
|
-
* @internal
|
|
146
|
-
*/
|
|
147
|
-
rpcToken?: Redacted.Redacted<string>;
|
|
148
144
|
/**
|
|
149
145
|
* Module entrypoint bundled with rolldown and baked into a Docker
|
|
150
146
|
* image pushed to `registry.fly.io`. Typically `import.meta.url`.
|
|
151
147
|
* A content-hash change updates the workload using the selected deployment strategy.
|
|
148
|
+
* Set this or {@link context}.
|
|
149
|
+
*/
|
|
150
|
+
main?: string;
|
|
151
|
+
/**
|
|
152
|
+
* A Docker build context whose `Dockerfile` builds the Service's image
|
|
153
|
+
* as it is, on a Fly remote builder (or locally with
|
|
154
|
+
* `build: { remote: false }`), for `linux/amd64`. The image's own
|
|
155
|
+
* `CMD` runs. A change to any file in it updates the workload. Set this
|
|
156
|
+
* or {@link main}.
|
|
152
157
|
*/
|
|
153
|
-
|
|
158
|
+
context?: string;
|
|
154
159
|
/**
|
|
155
160
|
* Region or regions to run in (`"iad"`, `["iad", "lhr"]`). The
|
|
156
161
|
* Service runs `count` Machines in each region behind one hostname;
|
|
@@ -271,6 +276,19 @@ export interface ServiceEndpoint {
|
|
|
271
276
|
url: string | undefined;
|
|
272
277
|
}
|
|
273
278
|
|
|
279
|
+
/** An App secret a {@link Service} wrote. Never the plaintext. */
|
|
280
|
+
export interface BoundSecret {
|
|
281
|
+
/**
|
|
282
|
+
* Name of the App secret, scoped to the Service
|
|
283
|
+
* (`ALCHEMY_<hash>_<ENV_VAR>`). The Machine maps it back to its env var.
|
|
284
|
+
*/
|
|
285
|
+
name: string;
|
|
286
|
+
/** Fly's digest of the value after the write. */
|
|
287
|
+
digest: string;
|
|
288
|
+
/** sha256 of `<name>:<value>` for the value written. Never the plaintext. */
|
|
289
|
+
sha256: string;
|
|
290
|
+
}
|
|
291
|
+
|
|
274
292
|
export type Service = Resource<
|
|
275
293
|
"Fly.Service",
|
|
276
294
|
ServiceProps,
|
|
@@ -286,8 +304,19 @@ export type Service = Resource<
|
|
|
286
304
|
* shared App's network. `undefined` for the organization's default.
|
|
287
305
|
*/
|
|
288
306
|
network?: string;
|
|
289
|
-
/**
|
|
290
|
-
|
|
307
|
+
/**
|
|
308
|
+
* App secrets the Service wrote from bound `Config` and Redacted
|
|
309
|
+
* values, by the env var they set: the scoped secret name, Fly's
|
|
310
|
+
* digest after the write and a sha256 of the value.
|
|
311
|
+
*/
|
|
312
|
+
secrets?: Record<string, BoundSecret>;
|
|
313
|
+
/** App-secrets version that holds {@link secrets}. */
|
|
314
|
+
secretsVersion?: number;
|
|
315
|
+
/**
|
|
316
|
+
* Slug of the organization the App is in. Its Machines' OIDC tokens are
|
|
317
|
+
* issued by `https://oidc.fly.io/{orgSlug}`.
|
|
318
|
+
*/
|
|
319
|
+
orgSlug?: string;
|
|
291
320
|
/** Fly Machine id of replica 0. */
|
|
292
321
|
machineId: string;
|
|
293
322
|
/** Fly Machine ids of every replica. */
|
|
@@ -338,9 +367,24 @@ export type Service = Resource<
|
|
|
338
367
|
code: { hash: string };
|
|
339
368
|
},
|
|
340
369
|
ServiceBinding,
|
|
341
|
-
Providers
|
|
370
|
+
Providers,
|
|
371
|
+
ServiceLookup
|
|
342
372
|
>;
|
|
343
373
|
|
|
374
|
+
/**
|
|
375
|
+
* Props for {@link Service.lookup}: find a Service this stack doesn't
|
|
376
|
+
* manage by its App, and its name when the App holds several.
|
|
377
|
+
*/
|
|
378
|
+
export interface ServiceLookup {
|
|
379
|
+
/** Fly App name the Service's Machines run in. */
|
|
380
|
+
appName: string;
|
|
381
|
+
/**
|
|
382
|
+
* Machine name of the Service, for an App that runs several. Omit it to
|
|
383
|
+
* take every Machine in the App.
|
|
384
|
+
*/
|
|
385
|
+
name?: string;
|
|
386
|
+
}
|
|
387
|
+
|
|
344
388
|
export const isService = (value: unknown): value is Service =>
|
|
345
389
|
typeof value === "object" &&
|
|
346
390
|
value !== null &&
|
|
@@ -538,9 +582,9 @@ export type ServiceRuntimeContext = FlyHostRuntimeContext;
|
|
|
538
582
|
* ### Call another Service
|
|
539
583
|
* A Service can return methods next to `fetch`. Another Service calls
|
|
540
584
|
* them by binding it with {@link bindService}, which returns a typed
|
|
541
|
-
* client. Binding makes the caller deploy after the Service
|
|
542
|
-
* callers that bind a Service
|
|
543
|
-
*
|
|
585
|
+
* client. Binding makes the caller deploy after the Service and records
|
|
586
|
+
* the caller's App on it, so only callers that bind a Service can call
|
|
587
|
+
* its methods.
|
|
544
588
|
*
|
|
545
589
|
* **Example:** A private Service with methods
|
|
546
590
|
* ```typescript
|
|
@@ -574,9 +618,10 @@ export type ServiceRuntimeContext = FlyHostRuntimeContext;
|
|
|
574
618
|
* ```
|
|
575
619
|
*
|
|
576
620
|
* Calls use plain HTTP to `privateUrl` inside Fly's WireGuard-encrypted
|
|
577
|
-
* private network. The Service accepts a method call only with
|
|
578
|
-
*
|
|
579
|
-
*
|
|
621
|
+
* private network. The Service accepts a method call only with a
|
|
622
|
+
* `Fly-Src` signature from Fly's proxy naming the App of a Service that
|
|
623
|
+
* binds it, so neither the public internet nor any other App can call
|
|
624
|
+
* it, and no caller holds a key. A public
|
|
580
625
|
* Service publishes an extra plain-HTTP port for bound callers,
|
|
581
626
|
* `bindingPort` (default 7780), since its own ports are an HTTPS
|
|
582
627
|
* redirect and HTTPS. `client.fetch` sends a request to the Service's
|
|
@@ -727,8 +772,18 @@ export type ServiceRuntimeContext = FlyHostRuntimeContext;
|
|
|
727
772
|
*
|
|
728
773
|
* ### Config
|
|
729
774
|
* Yield `Config` in init. Alchemy reads the value from the env of
|
|
730
|
-
* whoever deploys and
|
|
731
|
-
* to copy it into `env
|
|
775
|
+
* whoever deploys and stores it as a secret on the Service's App, so
|
|
776
|
+
* there is no need to copy it into `env` and it never appears in the
|
|
777
|
+
* Machine config. Redacted outputs of bound resources are stored the
|
|
778
|
+
* same way. The secret's name is scoped to the Service
|
|
779
|
+
* (`ALCHEMY_<hash>_API_KEY`) and the Machine maps it back to `API_KEY`,
|
|
780
|
+
* so it never collides with `Fly.Secret`, `fly secrets set` or an
|
|
781
|
+
* attachment. The bound value wins over an App secret already named
|
|
782
|
+
* `API_KEY`, which stays in the App untouched. The Machine sees the
|
|
783
|
+
* App's other secrets as they were at deploy; one set later with
|
|
784
|
+
* `fly secrets set` reaches it on the next deploy. A Service in a shared
|
|
785
|
+
* App (`app`) writes the values into the Machine env instead, because
|
|
786
|
+
* Fly gives every App secret to every Machine in the App.
|
|
732
787
|
*
|
|
733
788
|
* `Config.Redacted("API_KEY")` is `Redacted<string>`. Unwrap with
|
|
734
789
|
* `Redacted.value` only where you need the raw string.
|
|
@@ -866,6 +921,20 @@ export type ServiceRuntimeContext = FlyHostRuntimeContext;
|
|
|
866
921
|
* ) {}
|
|
867
922
|
* ```
|
|
868
923
|
*
|
|
924
|
+
* ### Run your own image
|
|
925
|
+
* `context` builds a directory's `Dockerfile` as it is, on a Fly remote
|
|
926
|
+
* builder, and runs the image's own `CMD`. A change to any file in the
|
|
927
|
+
* directory updates the Machines.
|
|
928
|
+
*
|
|
929
|
+
* **Example:** A Dockerfile build
|
|
930
|
+
* ```typescript
|
|
931
|
+
* const worker = yield* Fly.Service("Worker", {
|
|
932
|
+
* context: "./worker",
|
|
933
|
+
* public: false,
|
|
934
|
+
* services: [],
|
|
935
|
+
* });
|
|
936
|
+
* ```
|
|
937
|
+
*
|
|
869
938
|
* ### Custom proxy services
|
|
870
939
|
* `services` defaults to HTTP 80 (redirecting to HTTPS) + HTTPS 443
|
|
871
940
|
* toward `port`, or plain HTTP 80 for a private Service. Pass a custom
|
|
@@ -928,28 +997,23 @@ export type ServiceRuntimeContext = FlyHostRuntimeContext;
|
|
|
928
997
|
* ) {}
|
|
929
998
|
* ```
|
|
930
999
|
*
|
|
931
|
-
* **Example:** Install
|
|
932
|
-
* `
|
|
933
|
-
*
|
|
934
|
-
*
|
|
1000
|
+
* **Example:** Install a native package unbundled
|
|
1001
|
+
* `build.install` runs `npm install` in the image for packages that
|
|
1002
|
+
* cannot be bundled, such as native addons — same `build.install` as
|
|
1003
|
+
* Lambda.
|
|
935
1004
|
* ```typescript
|
|
936
1005
|
* export default class Api extends Fly.Service<Api>()(
|
|
937
1006
|
* "Api",
|
|
938
1007
|
* {
|
|
939
1008
|
* main: import.meta.url,
|
|
940
1009
|
* port: 3000,
|
|
941
|
-
* build: { install: ["
|
|
1010
|
+
* build: { install: ["sharp"] },
|
|
942
1011
|
* },
|
|
943
1012
|
* Effect.gen(function* () {
|
|
944
|
-
* const conn = yield* Fly.ConnectPostgres(Db);
|
|
945
|
-
* const db = yield* Drizzle.Postgres(conn.connectionString);
|
|
946
1013
|
* return {
|
|
947
|
-
* fetch: Effect.
|
|
948
|
-
* const rows = yield* db.execute("select 1 as ok");
|
|
949
|
-
* return HttpServerResponse.json({ rows });
|
|
950
|
-
* }),
|
|
1014
|
+
* fetch: Effect.succeed(HttpServerResponse.text("hello")),
|
|
951
1015
|
* };
|
|
952
|
-
* })
|
|
1016
|
+
* }),
|
|
953
1017
|
* ) {}
|
|
954
1018
|
* ```
|
|
955
1019
|
*
|
|
@@ -1033,38 +1097,48 @@ export type ServiceRuntimeContext = FlyHostRuntimeContext;
|
|
|
1033
1097
|
* See the [deployment guide](/fly/compute/deployments) for recovery,
|
|
1034
1098
|
* idle capacity, application responsibilities, and verification limits.
|
|
1035
1099
|
*
|
|
1100
|
+
* ### Looking up an existing Service
|
|
1101
|
+
* `Service.lookup` references a Service Alchemy doesn't manage (deployed
|
|
1102
|
+
* with `fly deploy` or by another stack). It is read during plan/deploy,
|
|
1103
|
+
* never created, updated or deleted, and `url`, `endpoints` and
|
|
1104
|
+
* `privateUrl` come from the Machines and addresses Fly reports.
|
|
1105
|
+
*
|
|
1106
|
+
* **Example:** Call a Service another stack deployed
|
|
1107
|
+
* ```typescript
|
|
1108
|
+
* const api = Fly.Service.lookup({ appName: "my-api" });
|
|
1109
|
+
* const url = api.privateUrl;
|
|
1110
|
+
* ```
|
|
1111
|
+
*
|
|
1036
1112
|
* @resource
|
|
1037
1113
|
* @product Service
|
|
1038
1114
|
*/
|
|
1039
|
-
export const Service: Platform<Service, ServiceServices, ServiceShape, ServiceRuntimeContext>
|
|
1040
|
-
Platform("Fly.Service", {
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
}),
|
|
1064
|
-
});
|
|
1115
|
+
export const Service: Platform<Service, ServiceServices, ServiceShape, ServiceRuntimeContext> &
|
|
1116
|
+
LookupMethod<Service> = Platform("Fly.Service", {
|
|
1117
|
+
createRuntimeContext: createFlyHostRuntimeContext("Fly.Service"),
|
|
1118
|
+
// `{ app: Site }` at module scope is an Effect. Yield it here so the
|
|
1119
|
+
// App is registered and `news.app` is resolved attributes at
|
|
1120
|
+
// reconcile (same DX as `yield* App(...)` inside Effect.gen).
|
|
1121
|
+
transformProps: (id, props) =>
|
|
1122
|
+
Effect.gen(function* () {
|
|
1123
|
+
if (globalThis.__ALCHEMY_RUNTIME__) return props;
|
|
1124
|
+
const app = Effect.isEffect(props.app)
|
|
1125
|
+
? yield* props.app as Effect.Effect<App, never, Providers>
|
|
1126
|
+
: props.app;
|
|
1127
|
+
if (app !== undefined) {
|
|
1128
|
+
// Report the ports this Service publishes so the App can reject
|
|
1129
|
+
// two Services on the same port before either one is deployed.
|
|
1130
|
+
yield* app.bind(id, {
|
|
1131
|
+
service: id,
|
|
1132
|
+
services: props.services,
|
|
1133
|
+
bindingPort: props.bindingPort,
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
return { ...props, app } as typeof props;
|
|
1137
|
+
}),
|
|
1138
|
+
});
|
|
1065
1139
|
|
|
1066
1140
|
export class ServiceNotCreated extends Data.TaggedError("Fly.ServiceNotCreated")<{
|
|
1067
|
-
|
|
1141
|
+
resourceName: string;
|
|
1068
1142
|
appName: string;
|
|
1069
1143
|
}> {}
|
|
1070
1144
|
|
|
@@ -1111,6 +1185,147 @@ const toFlyGuest = (guest: MachineGuest | undefined): FlyMachineGuest => {
|
|
|
1111
1185
|
return fly;
|
|
1112
1186
|
};
|
|
1113
1187
|
|
|
1188
|
+
/**
|
|
1189
|
+
* Prefix of the App secrets a Service writes. Scoped to its FQN so they
|
|
1190
|
+
* never share a name with `Fly.Secret`, `fly secrets set`, the
|
|
1191
|
+
* Redis/Postgres/Bucket attachments, or another Service.
|
|
1192
|
+
*/
|
|
1193
|
+
const boundSecretPrefix = Effect.fn(function* (fqn: string) {
|
|
1194
|
+
const hash = yield* sha256(fqn);
|
|
1195
|
+
return `ALCHEMY_${hash.slice(0, 8).toUpperCase()}_`;
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
/** A secret some Service wrote under its {@link boundSecretPrefix}. */
|
|
1199
|
+
const SCOPED_SECRET = /^ALCHEMY_[0-9A-F]{8}_/;
|
|
1200
|
+
|
|
1201
|
+
/**
|
|
1202
|
+
* Store bound `Config` and Redacted values as App secrets under names
|
|
1203
|
+
* scoped to the Service; the Machine config maps each back to its env var
|
|
1204
|
+
* (see {@link secretMappings}). Fly's digest is not a hash Alchemy can
|
|
1205
|
+
* compute, so a value is unchanged only when its sha256 matches the last
|
|
1206
|
+
* write and Fly still reports that write's digest. Deletes secrets under
|
|
1207
|
+
* the Service's prefix that are no longer bound, including ones a crash
|
|
1208
|
+
* left out of state; never touches any other secret. Returns what it now
|
|
1209
|
+
* owns, the App's other secrets the Machine should still see, and the
|
|
1210
|
+
* App-secrets version Machines must reach.
|
|
1211
|
+
*/
|
|
1212
|
+
const syncBoundSecrets = Effect.fn(function* (
|
|
1213
|
+
appName: string,
|
|
1214
|
+
fqn: string,
|
|
1215
|
+
desired: Record<string, Redacted.Redacted<string>>,
|
|
1216
|
+
previous: { secrets: Record<string, BoundSecret>; version: number | undefined },
|
|
1217
|
+
) {
|
|
1218
|
+
if (Object.keys(desired).length === 0 && Object.keys(previous.secrets).length === 0) {
|
|
1219
|
+
return { secrets: {}, others: [], version: previous.version };
|
|
1220
|
+
}
|
|
1221
|
+
const prefix = yield* boundSecretPrefix(fqn);
|
|
1222
|
+
const listDigests = machines
|
|
1223
|
+
.listSecrets({ app_name: appName, show_secrets: false })
|
|
1224
|
+
.pipe(
|
|
1225
|
+
Effect.map(
|
|
1226
|
+
(listed) =>
|
|
1227
|
+
new Map(
|
|
1228
|
+
(listed.secrets ?? []).flatMap((secret) =>
|
|
1229
|
+
secret.name === undefined ? [] : [[secret.name, secret.digest ?? ""] as const],
|
|
1230
|
+
),
|
|
1231
|
+
),
|
|
1232
|
+
),
|
|
1233
|
+
);
|
|
1234
|
+
const observed = yield* listDigests;
|
|
1235
|
+
const planned = yield* Effect.forEach(
|
|
1236
|
+
Object.entries(desired),
|
|
1237
|
+
([envVar, value]) =>
|
|
1238
|
+
// Salted with the scoped name, so equal values on different Services
|
|
1239
|
+
// or Apps never share a hash in state.
|
|
1240
|
+
sha256(`${prefix}${envVar}:${Redacted.value(value)}`).pipe(
|
|
1241
|
+
Effect.map((hash) => ({ envVar, name: `${prefix}${envVar}`, value, hash })),
|
|
1242
|
+
),
|
|
1243
|
+
{ concurrency: "unbounded" },
|
|
1244
|
+
);
|
|
1245
|
+
const isUnchanged = ({ envVar, name, hash }: (typeof planned)[number]) => {
|
|
1246
|
+
const last = previous.secrets[envVar];
|
|
1247
|
+
return (
|
|
1248
|
+
last !== undefined &&
|
|
1249
|
+
last.name === name &&
|
|
1250
|
+
last.sha256 === hash &&
|
|
1251
|
+
observed.get(name) === last.digest
|
|
1252
|
+
);
|
|
1253
|
+
};
|
|
1254
|
+
const kept = planned.filter(isUnchanged);
|
|
1255
|
+
const changed = planned.filter((entry) => !isUnchanged(entry));
|
|
1256
|
+
const unbound = [...observed.keys()].filter(
|
|
1257
|
+
(name) => name.startsWith(prefix) && !(name.slice(prefix.length) in desired),
|
|
1258
|
+
);
|
|
1259
|
+
const write =
|
|
1260
|
+
changed.length === 0
|
|
1261
|
+
? Effect.succeed({ version: undefined, written: [] as [string, BoundSecret][] })
|
|
1262
|
+
: Effect.gen(function* () {
|
|
1263
|
+
const response = yield* machines.updateSecrets({
|
|
1264
|
+
app_name: appName,
|
|
1265
|
+
values: Object.fromEntries(changed.map(({ name, value }) => [name, value])),
|
|
1266
|
+
});
|
|
1267
|
+
const digests = yield* listDigests;
|
|
1268
|
+
return {
|
|
1269
|
+
version: response.version ?? response.Version,
|
|
1270
|
+
written: changed.map(({ envVar, name, hash }): [string, BoundSecret] => [
|
|
1271
|
+
envVar,
|
|
1272
|
+
{ name, digest: digests.get(name) ?? "", sha256: hash },
|
|
1273
|
+
]),
|
|
1274
|
+
};
|
|
1275
|
+
});
|
|
1276
|
+
const remove = Effect.forEach(
|
|
1277
|
+
unbound,
|
|
1278
|
+
(name) =>
|
|
1279
|
+
machines.deleteSecret({ app_name: appName, secret_name: name }).pipe(
|
|
1280
|
+
Effect.map((response) => response.version ?? response.Version),
|
|
1281
|
+
Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
|
|
1282
|
+
),
|
|
1283
|
+
{ concurrency: "unbounded" },
|
|
1284
|
+
);
|
|
1285
|
+
const [{ version: writeVersion, written }, deleteVersions] = yield* Effect.all([write, remove], {
|
|
1286
|
+
concurrency: "unbounded",
|
|
1287
|
+
});
|
|
1288
|
+
const secrets: Record<string, BoundSecret> = Object.fromEntries([
|
|
1289
|
+
...kept.map(({ envVar }): [string, BoundSecret] => [envVar, previous.secrets[envVar]!]),
|
|
1290
|
+
...written,
|
|
1291
|
+
]);
|
|
1292
|
+
const versions = [writeVersion, ...deleteVersions].filter(
|
|
1293
|
+
(version): version is number => version !== undefined && Number.isSafeInteger(version),
|
|
1294
|
+
);
|
|
1295
|
+
// A bound value wins over an App secret with the same env var name, as
|
|
1296
|
+
// a Machine env var did; that secret stays in the App untouched.
|
|
1297
|
+
const others = [...observed.keys()]
|
|
1298
|
+
.filter((name) => !SCOPED_SECRET.test(name) && !(name in desired))
|
|
1299
|
+
.sort();
|
|
1300
|
+
return {
|
|
1301
|
+
secrets,
|
|
1302
|
+
others,
|
|
1303
|
+
version: versions.length > 0 ? Math.max(...versions) : previous.version,
|
|
1304
|
+
};
|
|
1305
|
+
});
|
|
1306
|
+
|
|
1307
|
+
/**
|
|
1308
|
+
* Fly maps a process's `secrets` entries from a scoped App secret to the
|
|
1309
|
+
* env var the program reads, but only with `ignore_app_secrets`, which
|
|
1310
|
+
* hides every App secret the process does not list. So the App's other
|
|
1311
|
+
* secrets (attachments, `fly secrets set`) are listed under their own
|
|
1312
|
+
* names, as they were at deploy.
|
|
1313
|
+
*/
|
|
1314
|
+
const secretMappings = (bound: {
|
|
1315
|
+
secrets: Record<string, BoundSecret>;
|
|
1316
|
+
others: string[];
|
|
1317
|
+
}): FlyMachineConfig["processes"] => {
|
|
1318
|
+
const mapped = Object.entries(bound.secrets).map(([envVar, secret]) => ({
|
|
1319
|
+
env_var: envVar,
|
|
1320
|
+
name: secret.name,
|
|
1321
|
+
}));
|
|
1322
|
+
if (mapped.length === 0) return undefined;
|
|
1323
|
+
const entries = [...mapped, ...bound.others.map((name) => ({ env_var: name, name }))].sort(
|
|
1324
|
+
(left, right) => left.env_var.localeCompare(right.env_var),
|
|
1325
|
+
);
|
|
1326
|
+
return [{ secrets: entries, ignore_app_secrets: true }];
|
|
1327
|
+
};
|
|
1328
|
+
|
|
1114
1329
|
const desiredEnv = (
|
|
1115
1330
|
props: ServiceProps,
|
|
1116
1331
|
bindingEnv: Record<string, any>,
|
|
@@ -1150,6 +1365,7 @@ const buildConfig = (input: {
|
|
|
1150
1365
|
mounts: FlyMachineMount[];
|
|
1151
1366
|
metadata: Record<string, string>;
|
|
1152
1367
|
statics?: FlyStatic[];
|
|
1368
|
+
processes?: FlyMachineConfig["processes"];
|
|
1153
1369
|
}): FlyMachineConfig => ({
|
|
1154
1370
|
image: input.image,
|
|
1155
1371
|
guest: input.guest,
|
|
@@ -1158,6 +1374,7 @@ const buildConfig = (input: {
|
|
|
1158
1374
|
mounts: input.mounts.length > 0 ? input.mounts : undefined,
|
|
1159
1375
|
metadata: input.metadata,
|
|
1160
1376
|
statics: input.statics !== undefined && input.statics.length > 0 ? input.statics : undefined,
|
|
1377
|
+
processes: input.processes,
|
|
1161
1378
|
});
|
|
1162
1379
|
|
|
1163
1380
|
const sameImage = (machine: FlyMachine, image: string) => {
|
|
@@ -1211,6 +1428,22 @@ const sameStatics = (observed: FlyStatic[] | undefined, desired: FlyStatic[] | u
|
|
|
1211
1428
|
return deepEqual(normalize(observed), normalize(desired), { stripNullish: true });
|
|
1212
1429
|
};
|
|
1213
1430
|
|
|
1431
|
+
/** Fly fills an omitted secret `name` with its `env_var`. */
|
|
1432
|
+
const sameProcesses = (
|
|
1433
|
+
observed: FlyMachineConfig["processes"],
|
|
1434
|
+
desired: FlyMachineConfig["processes"],
|
|
1435
|
+
) => {
|
|
1436
|
+
const normalize = (processes: FlyMachineConfig["processes"]) =>
|
|
1437
|
+
(processes ?? []).map((process) => ({
|
|
1438
|
+
...process,
|
|
1439
|
+
secrets: process.secrets?.map((secret) => ({
|
|
1440
|
+
env_var: secret.env_var,
|
|
1441
|
+
name: secret.name || secret.env_var,
|
|
1442
|
+
})),
|
|
1443
|
+
}));
|
|
1444
|
+
return deepEqual(normalize(observed), normalize(desired), { stripNullish: true });
|
|
1445
|
+
};
|
|
1446
|
+
|
|
1214
1447
|
const configDrifted = (
|
|
1215
1448
|
machine: FlyMachine,
|
|
1216
1449
|
desired: {
|
|
@@ -1221,6 +1454,7 @@ const configDrifted = (
|
|
|
1221
1454
|
mounts: FlyMachineMount[];
|
|
1222
1455
|
metadata: Record<string, string>;
|
|
1223
1456
|
statics?: FlyStatic[];
|
|
1457
|
+
processes?: FlyMachineConfig["processes"];
|
|
1224
1458
|
},
|
|
1225
1459
|
) => {
|
|
1226
1460
|
const config = machine.config;
|
|
@@ -1231,13 +1465,11 @@ const configDrifted = (
|
|
|
1231
1465
|
!sameServices(config?.services, desired.services) ||
|
|
1232
1466
|
!sameMounts(config?.mounts, desired.mounts) ||
|
|
1233
1467
|
metadataChanged(config?.metadata, desired.metadata) ||
|
|
1234
|
-
!sameStatics(config?.statics, desired.statics)
|
|
1468
|
+
!sameStatics(config?.statics, desired.statics) ||
|
|
1469
|
+
!sameProcesses(config?.processes, desired.processes)
|
|
1235
1470
|
);
|
|
1236
1471
|
};
|
|
1237
1472
|
|
|
1238
|
-
const portsOf = (services: FlyMachineService[] | undefined) =>
|
|
1239
|
-
(services ?? []).flatMap((service) => service.ports ?? []);
|
|
1240
|
-
|
|
1241
1473
|
const withPort = (base: string, port: number, standard: number) =>
|
|
1242
1474
|
port === standard ? base : `${base}:${port}`;
|
|
1243
1475
|
|
|
@@ -1317,7 +1549,9 @@ interface Access {
|
|
|
1317
1549
|
isPublic: boolean;
|
|
1318
1550
|
network: string | undefined;
|
|
1319
1551
|
bindingPort: number;
|
|
1320
|
-
|
|
1552
|
+
secrets?: Record<string, BoundSecret>;
|
|
1553
|
+
secretsVersion?: number;
|
|
1554
|
+
orgSlug: string | undefined;
|
|
1321
1555
|
}
|
|
1322
1556
|
|
|
1323
1557
|
/** Drop the port Alchemy added for bindings from a services list. */
|
|
@@ -1500,7 +1734,9 @@ const toAttrs = (set: ReplicaSet, codeHash: string, access: Access): Service["At
|
|
|
1500
1734
|
appName: set.appName,
|
|
1501
1735
|
ownsApp: access.ownsApp,
|
|
1502
1736
|
network: normalizeNetwork(access.network),
|
|
1503
|
-
|
|
1737
|
+
secrets: access.secrets,
|
|
1738
|
+
secretsVersion: access.secretsVersion,
|
|
1739
|
+
orgSlug: access.orgSlug,
|
|
1504
1740
|
...endpointsOf(set, access),
|
|
1505
1741
|
rolloutPending: set.rolloutPending,
|
|
1506
1742
|
machineId: set.machineId,
|
|
@@ -1517,6 +1753,26 @@ const toAttrs = (set: ReplicaSet, codeHash: string, access: Access): Service["At
|
|
|
1517
1753
|
code: { hash: codeHash },
|
|
1518
1754
|
});
|
|
1519
1755
|
|
|
1756
|
+
/** The replica set of a Service whose App exists but runs no Machines yet. */
|
|
1757
|
+
const emptyReplicaSet = (appName: string, name: string): ReplicaSet => ({
|
|
1758
|
+
appName,
|
|
1759
|
+
machineId: "",
|
|
1760
|
+
machineIds: [],
|
|
1761
|
+
name,
|
|
1762
|
+
baseName: name,
|
|
1763
|
+
region: "",
|
|
1764
|
+
regions: [],
|
|
1765
|
+
state: "created",
|
|
1766
|
+
instanceId: undefined,
|
|
1767
|
+
privateIp: undefined,
|
|
1768
|
+
imageRef: undefined,
|
|
1769
|
+
guest: undefined,
|
|
1770
|
+
services: undefined,
|
|
1771
|
+
count: 0,
|
|
1772
|
+
mounts: [],
|
|
1773
|
+
replicas: [],
|
|
1774
|
+
});
|
|
1775
|
+
|
|
1520
1776
|
const machineIdsOf = (output: Service["Attributes"] | undefined) =>
|
|
1521
1777
|
output?.machineIds ??
|
|
1522
1778
|
(output?.machineId !== undefined && output.machineId.length > 0 ? [output.machineId] : []);
|
|
@@ -1587,7 +1843,7 @@ export const ServiceProvider = () =>
|
|
|
1587
1843
|
stables: ["appName"],
|
|
1588
1844
|
nuke: { dependsOn: ["Fly.App"] },
|
|
1589
1845
|
|
|
1590
|
-
diff: Effect.fn(function* ({
|
|
1846
|
+
diff: Effect.fn(function* ({ news, output }) {
|
|
1591
1847
|
if (news === undefined) return;
|
|
1592
1848
|
const shape = news as Partial<Pick<ServiceProps, "app" | "public" | "network">>;
|
|
1593
1849
|
const ownsApp = shape.app === undefined;
|
|
@@ -1674,8 +1930,15 @@ export const ServiceProvider = () =>
|
|
|
1674
1930
|
// effect-config form has been evaluated, so the object view is
|
|
1675
1931
|
// safe to read.
|
|
1676
1932
|
const statics = news as Partial<
|
|
1677
|
-
Pick<
|
|
1933
|
+
Pick<
|
|
1934
|
+
ServiceProps,
|
|
1935
|
+
"main" | "context" | "build" | "image" | "port" | "extraFiles" | "isExternal"
|
|
1936
|
+
>
|
|
1678
1937
|
>;
|
|
1938
|
+
if (typeof statics.context === "string") {
|
|
1939
|
+
const hash = yield* hosted.contextHash(statics.context);
|
|
1940
|
+
if (hash !== output.code.hash) return { action: "update" as const };
|
|
1941
|
+
}
|
|
1679
1942
|
if (
|
|
1680
1943
|
isResolved({
|
|
1681
1944
|
main: statics.main,
|
|
@@ -1695,6 +1958,24 @@ export const ServiceProvider = () =>
|
|
|
1695
1958
|
return output.rolloutPending ? { action: "update" as const } : undefined;
|
|
1696
1959
|
}),
|
|
1697
1960
|
|
|
1961
|
+
lookup: Effect.fn(function* ({ appName, name }) {
|
|
1962
|
+
const found = yield* lookupReplicaSet({ appName, name });
|
|
1963
|
+
if (found === undefined) return undefined;
|
|
1964
|
+
const app = yield* machines
|
|
1965
|
+
.getApp({ app_name: appName })
|
|
1966
|
+
.pipe(Effect.catchTag("NotFound", () => Effect.undefined));
|
|
1967
|
+
const attrs = toAttrs(found, "", {
|
|
1968
|
+
// Reported as Alchemy-owned only so the endpoints follow the App's
|
|
1969
|
+
// observed addresses, as a declared Service's own App's do.
|
|
1970
|
+
ownsApp: true,
|
|
1971
|
+
isPublic: yield* hasPublicAddress(appName),
|
|
1972
|
+
network: app?.network,
|
|
1973
|
+
bindingPort: DEFAULT_BINDING_PORT,
|
|
1974
|
+
orgSlug: app?.organization?.slug,
|
|
1975
|
+
});
|
|
1976
|
+
return { ...attrs, ownsApp: false };
|
|
1977
|
+
}),
|
|
1978
|
+
|
|
1698
1979
|
read: Effect.fn(function* ({ id, fqn, instanceId, olds, output }) {
|
|
1699
1980
|
const ownsApp = output?.ownsApp ?? (olds !== undefined && olds.app === undefined);
|
|
1700
1981
|
const appName = ownsApp
|
|
@@ -1717,13 +1998,34 @@ export const ServiceProvider = () =>
|
|
|
1717
1998
|
machineIds: machineIdsOf(output),
|
|
1718
1999
|
baseName: name,
|
|
1719
2000
|
});
|
|
1720
|
-
if (found === undefined)
|
|
2001
|
+
if (found === undefined) {
|
|
2002
|
+
// A create interrupted between creating the Service's own App and
|
|
2003
|
+
// starting its Machines (an image build in between can take
|
|
2004
|
+
// minutes) leaves the App with nothing in it. Report it, so the
|
|
2005
|
+
// engine's recovery of the interrupted create deletes it instead
|
|
2006
|
+
// of leaking it. The App's name is derived from this resource's
|
|
2007
|
+
// instance, so an App with that name is this resource's.
|
|
2008
|
+
if (!ownsApp || appName === undefined) return undefined;
|
|
2009
|
+
const app = yield* machines
|
|
2010
|
+
.getApp({ app_name: appName })
|
|
2011
|
+
.pipe(Effect.catchTag("NotFound", () => Effect.undefined));
|
|
2012
|
+
if (app === undefined || !matchesAlchemyPhysicalName(app.name)) return undefined;
|
|
2013
|
+
return toAttrs(emptyReplicaSet(appName, name), output?.code.hash ?? "", {
|
|
2014
|
+
ownsApp,
|
|
2015
|
+
isPublic: false,
|
|
2016
|
+
network: output?.network ?? olds?.network,
|
|
2017
|
+
bindingPort: olds?.bindingPort ?? DEFAULT_BINDING_PORT,
|
|
2018
|
+
orgSlug: output?.orgSlug ?? (yield* resolveOrgSlug()),
|
|
2019
|
+
});
|
|
2020
|
+
}
|
|
1721
2021
|
return toAttrs(found, output?.code.hash ?? "", {
|
|
1722
2022
|
ownsApp,
|
|
1723
2023
|
isPublic: ownsApp && (yield* hasPublicAddress(found.appName)),
|
|
1724
2024
|
network: output?.network ?? (ownsApp ? olds?.network : undefined),
|
|
1725
2025
|
bindingPort: olds?.bindingPort ?? DEFAULT_BINDING_PORT,
|
|
1726
|
-
|
|
2026
|
+
secrets: output?.secrets,
|
|
2027
|
+
secretsVersion: output?.secretsVersion,
|
|
2028
|
+
orgSlug: output?.orgSlug ?? (yield* resolveOrgSlug()),
|
|
1727
2029
|
});
|
|
1728
2030
|
}),
|
|
1729
2031
|
|
|
@@ -1736,7 +2038,7 @@ export const ServiceProvider = () =>
|
|
|
1736
2038
|
isPublic: true,
|
|
1737
2039
|
network: undefined,
|
|
1738
2040
|
bindingPort: DEFAULT_BINDING_PORT,
|
|
1739
|
-
|
|
2041
|
+
orgSlug: undefined,
|
|
1740
2042
|
}),
|
|
1741
2043
|
);
|
|
1742
2044
|
}),
|
|
@@ -1774,9 +2076,35 @@ export const ServiceProvider = () =>
|
|
|
1774
2076
|
const version = yield* attachPostgresSecrets(appName, pg.clusterId, pg.variableName);
|
|
1775
2077
|
if (version !== undefined) secretVersions.push(version);
|
|
1776
2078
|
}
|
|
2079
|
+
const env = desiredEnv(props, bound.env, hosted.alchemyEnv, port);
|
|
2080
|
+
// Redacted values (every bound `Config`) become App secrets, but
|
|
2081
|
+
// only in an App the Service owns: Fly gives every App secret to
|
|
2082
|
+
// every Machine in the App. A plain `env` entry wins over a binding.
|
|
2083
|
+
const redacted = Object.keys(env).filter((key) =>
|
|
2084
|
+
props.env?.[key] !== undefined
|
|
2085
|
+
? Redacted.isRedacted(props.env[key])
|
|
2086
|
+
: bound.secretNames.includes(key),
|
|
2087
|
+
);
|
|
2088
|
+
// Move them out of the plain env record before anything else reads it.
|
|
2089
|
+
const desiredSecrets: Record<string, Redacted.Redacted<string>> = {};
|
|
2090
|
+
if (ownsApp) {
|
|
2091
|
+
for (const key of redacted) {
|
|
2092
|
+
desiredSecrets[key] = Redacted.make(env[key]!);
|
|
2093
|
+
delete env[key];
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
const boundSecrets = yield* syncBoundSecrets(
|
|
2097
|
+
appName,
|
|
2098
|
+
fqn,
|
|
2099
|
+
desiredSecrets,
|
|
2100
|
+
output?.appName === appName
|
|
2101
|
+
? { secrets: output.secrets ?? {}, version: output.secretsVersion }
|
|
2102
|
+
: { secrets: {}, version: undefined },
|
|
2103
|
+
);
|
|
2104
|
+
if (boundSecrets.version !== undefined) secretVersions.push(boundSecrets.version);
|
|
1777
2105
|
const minSecretsVersion =
|
|
1778
2106
|
secretVersions.length > 0 ? Math.max(...secretVersions) : undefined;
|
|
1779
|
-
const
|
|
2107
|
+
const processes = secretMappings(boundSecrets);
|
|
1780
2108
|
if (policy.shutdown && !props.isExternal) {
|
|
1781
2109
|
env.ALCHEMY_FLY_SHUTDOWN_TIMEOUT_MS = String(policy.shutdown.timeoutMs);
|
|
1782
2110
|
}
|
|
@@ -1790,24 +2118,35 @@ export const ServiceProvider = () =>
|
|
|
1790
2118
|
);
|
|
1791
2119
|
const services = published.services;
|
|
1792
2120
|
if (!ownsApp) yield* validateObservedPorts(appName, fqn, id, services);
|
|
1793
|
-
|
|
2121
|
+
// The Apps of the Services that bind this one; only they may call
|
|
2122
|
+
// its methods (see `serveFlyRpc`).
|
|
2123
|
+
if (bound.callers.length > 0) env[RPC_CALLERS_ENV] = bound.callers.join(",");
|
|
1794
2124
|
for (const target of bound.targets) {
|
|
1795
2125
|
const keys = boundTargetEnvKeys(target.service);
|
|
1796
2126
|
env[keys.appName] = packEnvValue(target.appName);
|
|
1797
2127
|
if (target.privateUrl !== undefined) env[keys.url] = packEnvValue(target.privateUrl);
|
|
1798
|
-
if (target.rpcToken !== undefined) env[keys.token] = packEnvValue(target.rpcToken);
|
|
1799
2128
|
}
|
|
1800
2129
|
env[RPC_ORG_ENV] = yield* resolveOrgSlug();
|
|
1801
2130
|
if (published.added) env[BINDING_PORT_ENV] = String(bindingPort);
|
|
1802
2131
|
const statics = toFlyStatics(props.statics);
|
|
1803
2132
|
|
|
1804
|
-
const { imageRef, codeHash } =
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
2133
|
+
const { imageRef, codeHash } =
|
|
2134
|
+
props.context !== undefined
|
|
2135
|
+
? yield* hosted.resolveContextImage({
|
|
2136
|
+
id,
|
|
2137
|
+
appName,
|
|
2138
|
+
context: props.context,
|
|
2139
|
+
remote: props.build?.remote,
|
|
2140
|
+
previousHash: output?.code.hash,
|
|
2141
|
+
session,
|
|
2142
|
+
})
|
|
2143
|
+
: yield* hosted.resolveImage({
|
|
2144
|
+
id,
|
|
2145
|
+
appName,
|
|
2146
|
+
props: props as HostedProgramProps,
|
|
2147
|
+
previousHash: output?.code.hash,
|
|
2148
|
+
session,
|
|
2149
|
+
});
|
|
1811
2150
|
|
|
1812
2151
|
const set = yield* reconcileReplicas({
|
|
1813
2152
|
id,
|
|
@@ -1836,12 +2175,24 @@ export const ServiceProvider = () =>
|
|
|
1836
2175
|
mounts: desired.mounts,
|
|
1837
2176
|
metadata: desired.metadata,
|
|
1838
2177
|
statics,
|
|
2178
|
+
processes,
|
|
1839
2179
|
}),
|
|
1840
2180
|
buildConfig: ({ mounts, metadata }) =>
|
|
1841
|
-
buildConfig({
|
|
2181
|
+
buildConfig({
|
|
2182
|
+
image: imageRef,
|
|
2183
|
+
guest,
|
|
2184
|
+
env,
|
|
2185
|
+
services,
|
|
2186
|
+
mounts,
|
|
2187
|
+
metadata,
|
|
2188
|
+
statics,
|
|
2189
|
+
processes,
|
|
2190
|
+
}),
|
|
1842
2191
|
}).pipe(
|
|
1843
2192
|
Effect.catchTag("Fly.ReplicaNotCreated", (error) =>
|
|
1844
|
-
Effect.fail(
|
|
2193
|
+
Effect.fail(
|
|
2194
|
+
new ServiceNotCreated({ resourceName: error.resourceName, appName: error.appName }),
|
|
2195
|
+
),
|
|
1845
2196
|
),
|
|
1846
2197
|
);
|
|
1847
2198
|
return toAttrs(set, codeHash, {
|
|
@@ -1849,13 +2200,15 @@ export const ServiceProvider = () =>
|
|
|
1849
2200
|
isPublic,
|
|
1850
2201
|
network,
|
|
1851
2202
|
bindingPort,
|
|
1852
|
-
|
|
2203
|
+
secrets: boundSecrets.secrets,
|
|
2204
|
+
secretsVersion: boundSecrets.version,
|
|
2205
|
+
orgSlug: yield* resolveOrgSlug(),
|
|
1853
2206
|
});
|
|
1854
2207
|
}),
|
|
1855
2208
|
|
|
1856
|
-
// Create the App
|
|
1857
|
-
//
|
|
1858
|
-
//
|
|
2209
|
+
// Create the App and its addresses before any Machine, so two
|
|
2210
|
+
// Services that bind each other can both resolve the other's App
|
|
2211
|
+
// and private address.
|
|
1859
2212
|
precreate: Effect.fn(function* ({ id, news }) {
|
|
1860
2213
|
const props = news;
|
|
1861
2214
|
// Precreate runs before dependencies resolve. A Service placed in
|
|
@@ -1909,7 +2262,7 @@ export const ServiceProvider = () =>
|
|
|
1909
2262
|
isPublic,
|
|
1910
2263
|
network,
|
|
1911
2264
|
bindingPort,
|
|
1912
|
-
|
|
2265
|
+
orgSlug: yield* resolveOrgSlug(),
|
|
1913
2266
|
});
|
|
1914
2267
|
}),
|
|
1915
2268
|
|