@alchemy.run/fly 2.0.0-beta.85 → 2.0.0-beta.87
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 +26 -4
- package/lib/App.d.ts.map +1 -1
- package/lib/App.js +25 -2
- 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 +20 -27
- 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 +36 -2
- package/lib/Bucket.d.ts.map +1 -1
- package/lib/Bucket.js +181 -28
- package/lib/Bucket.js.map +1 -1
- package/lib/BucketBinding.d.ts.map +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 +18 -0
- 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.map +1 -1
- package/lib/ConnectPostgres.js.map +1 -1
- package/lib/ConnectPostgresHttp.d.ts.map +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 +4 -3
- package/lib/Credentials.d.ts.map +1 -1
- package/lib/Credentials.js +9 -5
- 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.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.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 +17 -0
- 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 +26 -1
- package/lib/Machine.d.ts.map +1 -1
- package/lib/Machine.js +16 -1
- 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 +33 -1
- package/lib/Postgres.d.ts.map +1 -1
- package/lib/Postgres.js +20 -0
- package/lib/Postgres.js.map +1 -1
- package/lib/PostgresMigrations.d.ts.map +1 -1
- 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 +21 -1
- package/lib/Redis.d.ts.map +1 -1
- package/lib/Redis.js +28 -0
- package/lib/Redis.js.map +1 -1
- package/lib/RedisBinding.d.ts.map +1 -1
- package/lib/RedisBinding.js.map +1 -1
- package/lib/RedisHttp.d.ts.map +1 -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 +22 -1
- package/lib/Secret.d.ts.map +1 -1
- package/lib/Secret.js +15 -0
- 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 +22 -1
- package/lib/SecretKey.d.ts.map +1 -1
- package/lib/SecretKey.js +15 -0
- 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 +96 -21
- package/lib/Service.d.ts.map +1 -1
- package/lib/Service.js +251 -37
- 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 +22 -3
- package/lib/Sprite.d.ts.map +1 -1
- package/lib/Sprite.js +15 -0
- 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 +3 -3
- package/lib/Volume.d.ts.map +1 -1
- package/lib/Volume.js +17 -1
- 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 +23 -0
- package/lib/VolumeSnapshot.js.map +1 -1
- package/lib/Website/AssetDeployment.d.ts.map +1 -1
- package/lib/Website/AssetDeployment.js.map +1 -1
- package/lib/Website/Astro.d.ts.map +1 -1
- 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.map +1 -1
- 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.map +1 -1
- 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.map +1 -1
- 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.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 +1 -1
- package/lib/bluegreen.js.map +1 -1
- package/lib/hosted.d.ts +16 -0
- 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.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 +38 -26
- package/lib/replicas.d.ts.map +1 -1
- package/lib/replicas.js +57 -2
- 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 +36 -3
- package/src/BindService.ts +27 -34
- package/src/Bootstrap.ts +2 -0
- package/src/Bucket.ts +264 -33
- package/src/Certificate.ts +32 -1
- package/src/Credentials.ts +9 -5
- package/src/IpAssignment.ts +31 -1
- package/src/Machine.ts +34 -1
- package/src/MountVolume.ts +5 -2
- package/src/Postgres.ts +43 -1
- package/src/Redis.ts +43 -1
- package/src/Secret.ts +29 -1
- package/src/SecretKey.ts +29 -1
- package/src/Service.ts +389 -70
- package/src/Sprite.ts +34 -8
- package/src/Volume.ts +25 -1
- package/src/VolumeSnapshot.ts +39 -1
- package/src/bluegreen.ts +4 -1
- package/src/hosted.ts +118 -13
- package/src/internal/Oidc.ts +99 -0
- package/src/leases.ts +35 -7
- package/src/replicas.ts +63 -2
- 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";
|
|
@@ -65,6 +65,7 @@ import {
|
|
|
65
65
|
deleteReplicaSet,
|
|
66
66
|
hasPublishedService,
|
|
67
67
|
listReplicaSets,
|
|
68
|
+
lookupReplicaSet,
|
|
68
69
|
observeReplicaSet,
|
|
69
70
|
reconcileReplicas,
|
|
70
71
|
resolveCount,
|
|
@@ -74,7 +75,7 @@ import {
|
|
|
74
75
|
type Replica,
|
|
75
76
|
type ReplicaSet,
|
|
76
77
|
} from "./replicas.ts";
|
|
77
|
-
import { BINDING_PORT_ENV, DEFAULT_BINDING_PORT,
|
|
78
|
+
import { BINDING_PORT_ENV, DEFAULT_BINDING_PORT, RPC_CALLERS_ENV, RPC_ORG_ENV } from "./rpc.ts";
|
|
78
79
|
|
|
79
80
|
/**
|
|
80
81
|
* A resource-valued prop: the resource itself, or an Effect that produces
|
|
@@ -140,18 +141,21 @@ export interface ServiceProps extends PlatformProps {
|
|
|
140
141
|
* @default 7780
|
|
141
142
|
*/
|
|
142
143
|
bindingPort?: number;
|
|
143
|
-
/**
|
|
144
|
-
* Caller token for bound callers. Set automatically from an
|
|
145
|
-
* `Alchemy.Random` child resource; do not pass it.
|
|
146
|
-
* @internal
|
|
147
|
-
*/
|
|
148
|
-
rpcToken?: Redacted.Redacted<string>;
|
|
149
144
|
/**
|
|
150
145
|
* Module entrypoint bundled with rolldown and baked into a Docker
|
|
151
146
|
* image pushed to `registry.fly.io`. Typically `import.meta.url`.
|
|
152
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}.
|
|
153
157
|
*/
|
|
154
|
-
|
|
158
|
+
context?: string;
|
|
155
159
|
/**
|
|
156
160
|
* Region or regions to run in (`"iad"`, `["iad", "lhr"]`). The
|
|
157
161
|
* Service runs `count` Machines in each region behind one hostname;
|
|
@@ -272,6 +276,19 @@ export interface ServiceEndpoint {
|
|
|
272
276
|
url: string | undefined;
|
|
273
277
|
}
|
|
274
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
|
+
|
|
275
292
|
export type Service = Resource<
|
|
276
293
|
"Fly.Service",
|
|
277
294
|
ServiceProps,
|
|
@@ -287,8 +304,19 @@ export type Service = Resource<
|
|
|
287
304
|
* shared App's network. `undefined` for the organization's default.
|
|
288
305
|
*/
|
|
289
306
|
network?: string;
|
|
290
|
-
/**
|
|
291
|
-
|
|
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;
|
|
292
320
|
/** Fly Machine id of replica 0. */
|
|
293
321
|
machineId: string;
|
|
294
322
|
/** Fly Machine ids of every replica. */
|
|
@@ -339,9 +367,24 @@ export type Service = Resource<
|
|
|
339
367
|
code: { hash: string };
|
|
340
368
|
},
|
|
341
369
|
ServiceBinding,
|
|
342
|
-
Providers
|
|
370
|
+
Providers,
|
|
371
|
+
ServiceLookup
|
|
343
372
|
>;
|
|
344
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
|
+
|
|
345
388
|
export const isService = (value: unknown): value is Service =>
|
|
346
389
|
typeof value === "object" &&
|
|
347
390
|
value !== null &&
|
|
@@ -539,9 +582,9 @@ export type ServiceRuntimeContext = FlyHostRuntimeContext;
|
|
|
539
582
|
* ### Call another Service
|
|
540
583
|
* A Service can return methods next to `fetch`. Another Service calls
|
|
541
584
|
* them by binding it with {@link bindService}, which returns a typed
|
|
542
|
-
* client. Binding makes the caller deploy after the Service
|
|
543
|
-
* callers that bind a Service
|
|
544
|
-
*
|
|
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.
|
|
545
588
|
*
|
|
546
589
|
* **Example:** A private Service with methods
|
|
547
590
|
* ```typescript
|
|
@@ -575,9 +618,10 @@ export type ServiceRuntimeContext = FlyHostRuntimeContext;
|
|
|
575
618
|
* ```
|
|
576
619
|
*
|
|
577
620
|
* Calls use plain HTTP to `privateUrl` inside Fly's WireGuard-encrypted
|
|
578
|
-
* private network. The Service accepts a method call only with
|
|
579
|
-
*
|
|
580
|
-
*
|
|
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
|
|
581
625
|
* Service publishes an extra plain-HTTP port for bound callers,
|
|
582
626
|
* `bindingPort` (default 7780), since its own ports are an HTTPS
|
|
583
627
|
* redirect and HTTPS. `client.fetch` sends a request to the Service's
|
|
@@ -728,8 +772,18 @@ export type ServiceRuntimeContext = FlyHostRuntimeContext;
|
|
|
728
772
|
*
|
|
729
773
|
* ### Config
|
|
730
774
|
* Yield `Config` in init. Alchemy reads the value from the env of
|
|
731
|
-
* whoever deploys and
|
|
732
|
-
* 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.
|
|
733
787
|
*
|
|
734
788
|
* `Config.Redacted("API_KEY")` is `Redacted<string>`. Unwrap with
|
|
735
789
|
* `Redacted.value` only where you need the raw string.
|
|
@@ -867,6 +921,20 @@ export type ServiceRuntimeContext = FlyHostRuntimeContext;
|
|
|
867
921
|
* ) {}
|
|
868
922
|
* ```
|
|
869
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
|
+
*
|
|
870
938
|
* ### Custom proxy services
|
|
871
939
|
* `services` defaults to HTTP 80 (redirecting to HTTPS) + HTTPS 443
|
|
872
940
|
* toward `port`, or plain HTTP 80 for a private Service. Pass a custom
|
|
@@ -1029,35 +1097,45 @@ export type ServiceRuntimeContext = FlyHostRuntimeContext;
|
|
|
1029
1097
|
* See the [deployment guide](/fly/compute/deployments) for recovery,
|
|
1030
1098
|
* idle capacity, application responsibilities, and verification limits.
|
|
1031
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
|
+
*
|
|
1032
1112
|
* @resource
|
|
1033
1113
|
* @product Service
|
|
1034
1114
|
*/
|
|
1035
|
-
export const Service: Platform<Service, ServiceServices, ServiceShape, ServiceRuntimeContext>
|
|
1036
|
-
Platform("Fly.Service", {
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
}),
|
|
1060
|
-
});
|
|
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
|
+
});
|
|
1061
1139
|
|
|
1062
1140
|
export class ServiceNotCreated extends Data.TaggedError("Fly.ServiceNotCreated")<{
|
|
1063
1141
|
resourceName: string;
|
|
@@ -1107,6 +1185,147 @@ const toFlyGuest = (guest: MachineGuest | undefined): FlyMachineGuest => {
|
|
|
1107
1185
|
return fly;
|
|
1108
1186
|
};
|
|
1109
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
|
+
|
|
1110
1329
|
const desiredEnv = (
|
|
1111
1330
|
props: ServiceProps,
|
|
1112
1331
|
bindingEnv: Record<string, any>,
|
|
@@ -1146,6 +1365,7 @@ const buildConfig = (input: {
|
|
|
1146
1365
|
mounts: FlyMachineMount[];
|
|
1147
1366
|
metadata: Record<string, string>;
|
|
1148
1367
|
statics?: FlyStatic[];
|
|
1368
|
+
processes?: FlyMachineConfig["processes"];
|
|
1149
1369
|
}): FlyMachineConfig => ({
|
|
1150
1370
|
image: input.image,
|
|
1151
1371
|
guest: input.guest,
|
|
@@ -1154,6 +1374,7 @@ const buildConfig = (input: {
|
|
|
1154
1374
|
mounts: input.mounts.length > 0 ? input.mounts : undefined,
|
|
1155
1375
|
metadata: input.metadata,
|
|
1156
1376
|
statics: input.statics !== undefined && input.statics.length > 0 ? input.statics : undefined,
|
|
1377
|
+
processes: input.processes,
|
|
1157
1378
|
});
|
|
1158
1379
|
|
|
1159
1380
|
const sameImage = (machine: FlyMachine, image: string) => {
|
|
@@ -1207,6 +1428,22 @@ const sameStatics = (observed: FlyStatic[] | undefined, desired: FlyStatic[] | u
|
|
|
1207
1428
|
return deepEqual(normalize(observed), normalize(desired), { stripNullish: true });
|
|
1208
1429
|
};
|
|
1209
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
|
+
|
|
1210
1447
|
const configDrifted = (
|
|
1211
1448
|
machine: FlyMachine,
|
|
1212
1449
|
desired: {
|
|
@@ -1217,6 +1454,7 @@ const configDrifted = (
|
|
|
1217
1454
|
mounts: FlyMachineMount[];
|
|
1218
1455
|
metadata: Record<string, string>;
|
|
1219
1456
|
statics?: FlyStatic[];
|
|
1457
|
+
processes?: FlyMachineConfig["processes"];
|
|
1220
1458
|
},
|
|
1221
1459
|
) => {
|
|
1222
1460
|
const config = machine.config;
|
|
@@ -1227,7 +1465,8 @@ const configDrifted = (
|
|
|
1227
1465
|
!sameServices(config?.services, desired.services) ||
|
|
1228
1466
|
!sameMounts(config?.mounts, desired.mounts) ||
|
|
1229
1467
|
metadataChanged(config?.metadata, desired.metadata) ||
|
|
1230
|
-
!sameStatics(config?.statics, desired.statics)
|
|
1468
|
+
!sameStatics(config?.statics, desired.statics) ||
|
|
1469
|
+
!sameProcesses(config?.processes, desired.processes)
|
|
1231
1470
|
);
|
|
1232
1471
|
};
|
|
1233
1472
|
|
|
@@ -1310,7 +1549,9 @@ interface Access {
|
|
|
1310
1549
|
isPublic: boolean;
|
|
1311
1550
|
network: string | undefined;
|
|
1312
1551
|
bindingPort: number;
|
|
1313
|
-
|
|
1552
|
+
secrets?: Record<string, BoundSecret>;
|
|
1553
|
+
secretsVersion?: number;
|
|
1554
|
+
orgSlug: string | undefined;
|
|
1314
1555
|
}
|
|
1315
1556
|
|
|
1316
1557
|
/** Drop the port Alchemy added for bindings from a services list. */
|
|
@@ -1493,7 +1734,9 @@ const toAttrs = (set: ReplicaSet, codeHash: string, access: Access): Service["At
|
|
|
1493
1734
|
appName: set.appName,
|
|
1494
1735
|
ownsApp: access.ownsApp,
|
|
1495
1736
|
network: normalizeNetwork(access.network),
|
|
1496
|
-
|
|
1737
|
+
secrets: access.secrets,
|
|
1738
|
+
secretsVersion: access.secretsVersion,
|
|
1739
|
+
orgSlug: access.orgSlug,
|
|
1497
1740
|
...endpointsOf(set, access),
|
|
1498
1741
|
rolloutPending: set.rolloutPending,
|
|
1499
1742
|
machineId: set.machineId,
|
|
@@ -1687,8 +1930,15 @@ export const ServiceProvider = () =>
|
|
|
1687
1930
|
// effect-config form has been evaluated, so the object view is
|
|
1688
1931
|
// safe to read.
|
|
1689
1932
|
const statics = news as Partial<
|
|
1690
|
-
Pick<
|
|
1933
|
+
Pick<
|
|
1934
|
+
ServiceProps,
|
|
1935
|
+
"main" | "context" | "build" | "image" | "port" | "extraFiles" | "isExternal"
|
|
1936
|
+
>
|
|
1691
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
|
+
}
|
|
1692
1942
|
if (
|
|
1693
1943
|
isResolved({
|
|
1694
1944
|
main: statics.main,
|
|
@@ -1708,6 +1958,24 @@ export const ServiceProvider = () =>
|
|
|
1708
1958
|
return output.rolloutPending ? { action: "update" as const } : undefined;
|
|
1709
1959
|
}),
|
|
1710
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
|
+
|
|
1711
1979
|
read: Effect.fn(function* ({ id, fqn, instanceId, olds, output }) {
|
|
1712
1980
|
const ownsApp = output?.ownsApp ?? (olds !== undefined && olds.app === undefined);
|
|
1713
1981
|
const appName = ownsApp
|
|
@@ -1747,7 +2015,7 @@ export const ServiceProvider = () =>
|
|
|
1747
2015
|
isPublic: false,
|
|
1748
2016
|
network: output?.network ?? olds?.network,
|
|
1749
2017
|
bindingPort: olds?.bindingPort ?? DEFAULT_BINDING_PORT,
|
|
1750
|
-
|
|
2018
|
+
orgSlug: output?.orgSlug ?? (yield* resolveOrgSlug()),
|
|
1751
2019
|
});
|
|
1752
2020
|
}
|
|
1753
2021
|
return toAttrs(found, output?.code.hash ?? "", {
|
|
@@ -1755,7 +2023,9 @@ export const ServiceProvider = () =>
|
|
|
1755
2023
|
isPublic: ownsApp && (yield* hasPublicAddress(found.appName)),
|
|
1756
2024
|
network: output?.network ?? (ownsApp ? olds?.network : undefined),
|
|
1757
2025
|
bindingPort: olds?.bindingPort ?? DEFAULT_BINDING_PORT,
|
|
1758
|
-
|
|
2026
|
+
secrets: output?.secrets,
|
|
2027
|
+
secretsVersion: output?.secretsVersion,
|
|
2028
|
+
orgSlug: output?.orgSlug ?? (yield* resolveOrgSlug()),
|
|
1759
2029
|
});
|
|
1760
2030
|
}),
|
|
1761
2031
|
|
|
@@ -1768,7 +2038,7 @@ export const ServiceProvider = () =>
|
|
|
1768
2038
|
isPublic: true,
|
|
1769
2039
|
network: undefined,
|
|
1770
2040
|
bindingPort: DEFAULT_BINDING_PORT,
|
|
1771
|
-
|
|
2041
|
+
orgSlug: undefined,
|
|
1772
2042
|
}),
|
|
1773
2043
|
);
|
|
1774
2044
|
}),
|
|
@@ -1806,9 +2076,35 @@ export const ServiceProvider = () =>
|
|
|
1806
2076
|
const version = yield* attachPostgresSecrets(appName, pg.clusterId, pg.variableName);
|
|
1807
2077
|
if (version !== undefined) secretVersions.push(version);
|
|
1808
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);
|
|
1809
2105
|
const minSecretsVersion =
|
|
1810
2106
|
secretVersions.length > 0 ? Math.max(...secretVersions) : undefined;
|
|
1811
|
-
const
|
|
2107
|
+
const processes = secretMappings(boundSecrets);
|
|
1812
2108
|
if (policy.shutdown && !props.isExternal) {
|
|
1813
2109
|
env.ALCHEMY_FLY_SHUTDOWN_TIMEOUT_MS = String(policy.shutdown.timeoutMs);
|
|
1814
2110
|
}
|
|
@@ -1822,24 +2118,35 @@ export const ServiceProvider = () =>
|
|
|
1822
2118
|
);
|
|
1823
2119
|
const services = published.services;
|
|
1824
2120
|
if (!ownsApp) yield* validateObservedPorts(appName, fqn, id, services);
|
|
1825
|
-
|
|
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(",");
|
|
1826
2124
|
for (const target of bound.targets) {
|
|
1827
2125
|
const keys = boundTargetEnvKeys(target.service);
|
|
1828
2126
|
env[keys.appName] = packEnvValue(target.appName);
|
|
1829
2127
|
if (target.privateUrl !== undefined) env[keys.url] = packEnvValue(target.privateUrl);
|
|
1830
|
-
if (target.rpcToken !== undefined) env[keys.token] = packEnvValue(target.rpcToken);
|
|
1831
2128
|
}
|
|
1832
2129
|
env[RPC_ORG_ENV] = yield* resolveOrgSlug();
|
|
1833
2130
|
if (published.added) env[BINDING_PORT_ENV] = String(bindingPort);
|
|
1834
2131
|
const statics = toFlyStatics(props.statics);
|
|
1835
2132
|
|
|
1836
|
-
const { imageRef, codeHash } =
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
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
|
+
});
|
|
1843
2150
|
|
|
1844
2151
|
const set = yield* reconcileReplicas({
|
|
1845
2152
|
id,
|
|
@@ -1868,9 +2175,19 @@ export const ServiceProvider = () =>
|
|
|
1868
2175
|
mounts: desired.mounts,
|
|
1869
2176
|
metadata: desired.metadata,
|
|
1870
2177
|
statics,
|
|
2178
|
+
processes,
|
|
1871
2179
|
}),
|
|
1872
2180
|
buildConfig: ({ mounts, metadata }) =>
|
|
1873
|
-
buildConfig({
|
|
2181
|
+
buildConfig({
|
|
2182
|
+
image: imageRef,
|
|
2183
|
+
guest,
|
|
2184
|
+
env,
|
|
2185
|
+
services,
|
|
2186
|
+
mounts,
|
|
2187
|
+
metadata,
|
|
2188
|
+
statics,
|
|
2189
|
+
processes,
|
|
2190
|
+
}),
|
|
1874
2191
|
}).pipe(
|
|
1875
2192
|
Effect.catchTag("Fly.ReplicaNotCreated", (error) =>
|
|
1876
2193
|
Effect.fail(
|
|
@@ -1883,13 +2200,15 @@ export const ServiceProvider = () =>
|
|
|
1883
2200
|
isPublic,
|
|
1884
2201
|
network,
|
|
1885
2202
|
bindingPort,
|
|
1886
|
-
|
|
2203
|
+
secrets: boundSecrets.secrets,
|
|
2204
|
+
secretsVersion: boundSecrets.version,
|
|
2205
|
+
orgSlug: yield* resolveOrgSlug(),
|
|
1887
2206
|
});
|
|
1888
2207
|
}),
|
|
1889
2208
|
|
|
1890
|
-
// Create the App
|
|
1891
|
-
//
|
|
1892
|
-
//
|
|
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.
|
|
1893
2212
|
precreate: Effect.fn(function* ({ id, news }) {
|
|
1894
2213
|
const props = news;
|
|
1895
2214
|
// Precreate runs before dependencies resolve. A Service placed in
|
|
@@ -1943,7 +2262,7 @@ export const ServiceProvider = () =>
|
|
|
1943
2262
|
isPublic,
|
|
1944
2263
|
network,
|
|
1945
2264
|
bindingPort,
|
|
1946
|
-
|
|
2265
|
+
orgSlug: yield* resolveOrgSlug(),
|
|
1947
2266
|
});
|
|
1948
2267
|
}),
|
|
1949
2268
|
|