@alchemy.run/fly 2.0.0-beta.85 → 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 +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/BindService.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as Binding from "alchemy/Binding";
|
|
2
2
|
import type { Resource } from "alchemy/Resource";
|
|
3
|
-
import { makeFetchRpcStub,
|
|
3
|
+
import { makeFetchRpcStub, type Rpc } from "alchemy/Rpc";
|
|
4
4
|
import { CurrentRuntimeContext, type RuntimeContext, sanitizeKey } from "alchemy/RuntimeContext";
|
|
5
5
|
import { isYieldableEffectLike } from "alchemy/Util/effect";
|
|
6
6
|
import * as Data from "effect/Data";
|
|
@@ -9,9 +9,7 @@ import * as HttpClient from "effect/http/HttpClient";
|
|
|
9
9
|
import type * as HttpClientError from "effect/http/HttpClientError";
|
|
10
10
|
import * as HttpClientRequest from "effect/http/HttpClientRequest";
|
|
11
11
|
import type * as HttpClientResponse from "effect/http/HttpClientResponse";
|
|
12
|
-
import * as Redacted from "effect/Redacted";
|
|
13
12
|
import type { BoundTarget, ServiceBinding } from "./MountVolume.ts";
|
|
14
|
-
import { RPC_TOKEN_HEADER } from "./rpc.ts";
|
|
15
13
|
import type { Service } from "./Service.ts";
|
|
16
14
|
|
|
17
15
|
/** The deployed caller has no address for the bound Service. */
|
|
@@ -75,12 +73,13 @@ const logicalIdOf = (target: unknown): string => {
|
|
|
75
73
|
export const boundTargetEnvKeys = (id: string) => ({
|
|
76
74
|
appName: sanitizeKey(`FLY_BIND_${id}_APP`),
|
|
77
75
|
url: sanitizeKey(`FLY_BIND_${id}_URL`),
|
|
78
|
-
token: sanitizeKey(`FLY_BIND_${id}_TOKEN`),
|
|
79
76
|
});
|
|
80
77
|
|
|
81
78
|
/**
|
|
82
79
|
* Record the binding on the caller and transport the target's private
|
|
83
|
-
* address
|
|
80
|
+
* address as Outputs. For {@link bindService}, also record the caller's
|
|
81
|
+
* App on the target, which serves its methods only to the Apps recorded
|
|
82
|
+
* there. Returns runtime accessors for the address.
|
|
84
83
|
*/
|
|
85
84
|
const bindTarget = Effect.fn(function* <Shape, Req>(
|
|
86
85
|
target: Target<Shape> | Effect.Effect<Target<Shape>, never, Req>,
|
|
@@ -105,11 +104,22 @@ const bindTarget = Effect.fn(function* <Shape, Req>(
|
|
|
105
104
|
appName: service.appName,
|
|
106
105
|
network: service.network,
|
|
107
106
|
privateUrl: service.privateUrl,
|
|
108
|
-
rpcToken: service.rpcToken,
|
|
109
107
|
port,
|
|
110
108
|
endpoints: service.endpoints,
|
|
111
109
|
} as unknown as BoundTarget,
|
|
112
110
|
});
|
|
111
|
+
if (port === undefined) {
|
|
112
|
+
// Fly signs each request's calling App (`Fly-Src`), so the target
|
|
113
|
+
// authorizes callers by App: no key to hand out.
|
|
114
|
+
yield* (
|
|
115
|
+
service as unknown as {
|
|
116
|
+
bind: (
|
|
117
|
+
label: TemplateStringsArray,
|
|
118
|
+
...args: unknown[]
|
|
119
|
+
) => (data: ServiceBinding) => Effect.Effect<void>;
|
|
120
|
+
}
|
|
121
|
+
).bind`Caller(${host})`({ caller: { appName: host.appName as unknown as string } });
|
|
122
|
+
}
|
|
113
123
|
}
|
|
114
124
|
}
|
|
115
125
|
// Captured at bind time: the host's runtime context reads the values
|
|
@@ -126,15 +136,6 @@ const bindTarget = Effect.fn(function* <Shape, Req>(
|
|
|
126
136
|
return {
|
|
127
137
|
appName: required(keys.appName),
|
|
128
138
|
url: required(keys.url),
|
|
129
|
-
token: read(keys.token).pipe(
|
|
130
|
-
Effect.map((value) =>
|
|
131
|
-
Redacted.isRedacted(value)
|
|
132
|
-
? String(Redacted.value(value))
|
|
133
|
-
: typeof value === "string"
|
|
134
|
-
? value
|
|
135
|
-
: "",
|
|
136
|
-
),
|
|
137
|
-
),
|
|
138
139
|
};
|
|
139
140
|
});
|
|
140
141
|
|
|
@@ -150,12 +151,13 @@ const withBase = (base: string, url: string) => {
|
|
|
150
151
|
* `fetch` and `run`) and forwards `fetch` requests, over Fly's private
|
|
151
152
|
* network.
|
|
152
153
|
*
|
|
153
|
-
* Binding makes the caller deploy after the Service
|
|
154
|
-
* Service's private address and
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
154
|
+
* Binding makes the caller deploy after the Service, hands it the
|
|
155
|
+
* Service's private address, and records the caller's App on the Service.
|
|
156
|
+
* Fly's proxy signs every call with the calling App (`Fly-Src`), and the
|
|
157
|
+
* Service answers methods only for the Apps recorded on it, so neither
|
|
158
|
+
* other Services on the network nor the public internet can call them,
|
|
159
|
+
* and no key is involved. Calls are plain HTTP inside Fly's
|
|
160
|
+
* WireGuard-encrypted network.
|
|
159
161
|
*
|
|
160
162
|
* ### Call a private Service
|
|
161
163
|
* **Example:** Methods on the target
|
|
@@ -250,24 +252,15 @@ export const bindService = <Shape, Req = never>(
|
|
|
250
252
|
Effect.gen(function* () {
|
|
251
253
|
const http = yield* HttpClient.HttpClient;
|
|
252
254
|
const bound = yield* bindTarget(target, undefined);
|
|
253
|
-
const send = (request: HttpClientRequest.HttpClientRequest
|
|
255
|
+
const send = (request: HttpClientRequest.HttpClientRequest) =>
|
|
254
256
|
Effect.gen(function* () {
|
|
255
257
|
const base = yield* bound.url;
|
|
256
|
-
|
|
257
|
-
if (isRpc) {
|
|
258
|
-
next = HttpClientRequest.setHeader(next, RPC_TOKEN_HEADER, yield* bound.token);
|
|
259
|
-
}
|
|
260
|
-
return yield* http.execute(next);
|
|
258
|
+
return yield* http.execute(HttpClientRequest.setUrl(request, withBase(base, request.url)));
|
|
261
259
|
});
|
|
262
260
|
return makeFetchRpcStub<ServiceClient<Shape>>({
|
|
263
261
|
fetch: (request) =>
|
|
264
|
-
send(request
|
|
265
|
-
|
|
266
|
-
unknown
|
|
267
|
-
>,
|
|
268
|
-
base: {
|
|
269
|
-
fetch: (request: HttpClientRequest.HttpClientRequest) => send(request, false),
|
|
270
|
-
},
|
|
262
|
+
send(request) as Effect.Effect<HttpClientResponse.HttpClientResponse, unknown>,
|
|
263
|
+
base: { fetch: send },
|
|
271
264
|
});
|
|
272
265
|
});
|
|
273
266
|
|
package/src/Bootstrap.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from "alchemy/Runtime/Bootstrap";
|
|
15
15
|
import { ManagedHttpShutdown } from "alchemy/Runtime/ManagedHttpShutdown";
|
|
16
16
|
import * as ConfigProvider from "effect/ConfigProvider";
|
|
17
|
+
import * as SecureRandom from "effect/crypto/SecureRandom";
|
|
17
18
|
import * as Effect from "effect/Effect";
|
|
18
19
|
import * as FetchHttpClient from "effect/http/FetchHttpClient";
|
|
19
20
|
import * as Layer from "effect/Layer";
|
|
@@ -31,6 +32,7 @@ export const bootstrap = (entrypoint: unknown): Promise<void> => {
|
|
|
31
32
|
const platform = Layer.mergeAll(
|
|
32
33
|
NodeServices.layer,
|
|
33
34
|
FetchHttpClient.layer,
|
|
35
|
+
SecureRandom.layerWebCrypto,
|
|
34
36
|
Logger.layer([Logger.consolePretty()]),
|
|
35
37
|
);
|
|
36
38
|
|
package/src/Bucket.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { fromCredentials, type Credentials } from "@distilled.cloud/aws/Credentials";
|
|
2
|
+
import * as AwsEndpoint from "@distilled.cloud/aws/Endpoint";
|
|
3
|
+
import type { RegionName } from "@distilled.cloud/aws/Region";
|
|
4
|
+
import * as S3 from "@distilled.cloud/aws/s3";
|
|
1
5
|
import type { AddOnsResponseEdgesItemNode } from "@distilled.cloud/fly-io/addons";
|
|
2
6
|
import * as addons from "@distilled.cloud/fly-io/addons";
|
|
3
7
|
import * as machines from "@distilled.cloud/fly-io/machines";
|
|
@@ -9,6 +13,9 @@ import { createInternalTags } from "alchemy/Tags";
|
|
|
9
13
|
import * as Data from "effect/Data";
|
|
10
14
|
import * as Duration from "effect/Duration";
|
|
11
15
|
import * as Effect from "effect/Effect";
|
|
16
|
+
import * as FetchHttpClient from "effect/http/FetchHttpClient";
|
|
17
|
+
import type * as HttpClient from "effect/http/HttpClient";
|
|
18
|
+
import * as Layer from "effect/Layer";
|
|
12
19
|
import * as Redacted from "effect/Redacted";
|
|
13
20
|
import * as Result from "effect/Result";
|
|
14
21
|
import * as Schedule from "effect/Schedule";
|
|
@@ -133,9 +140,19 @@ export type Bucket = Resource<
|
|
|
133
140
|
bucketName: Redacted.Redacted<string> | undefined;
|
|
134
141
|
},
|
|
135
142
|
never,
|
|
136
|
-
Providers
|
|
143
|
+
Providers,
|
|
144
|
+
BucketLookup
|
|
137
145
|
>;
|
|
138
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Props for {@link Bucket.lookup}: find a Tigris bucket this stack
|
|
149
|
+
* doesn't manage by its name.
|
|
150
|
+
*/
|
|
151
|
+
export interface BucketLookup {
|
|
152
|
+
/** Tigris bucket / add-on name. */
|
|
153
|
+
name: string;
|
|
154
|
+
}
|
|
155
|
+
|
|
139
156
|
/**
|
|
140
157
|
* A Fly.Bucket is Tigris S3-compatible object storage, billed through
|
|
141
158
|
* Fly. It is an org-scoped GraphQL add-on (`AddOnType` `tigris`).
|
|
@@ -245,6 +262,18 @@ export type Bucket = Resource<
|
|
|
245
262
|
* });
|
|
246
263
|
* ```
|
|
247
264
|
*
|
|
265
|
+
* ### Looking up an existing bucket
|
|
266
|
+
* `Bucket.lookup` references a bucket Alchemy doesn't manage (created with
|
|
267
|
+
* `fly storage create` or by another stack). It is read during plan/deploy,
|
|
268
|
+
* never created, updated or deleted. Tigris returns a bucket's access keys
|
|
269
|
+
* only when it is created, so a looked-up bucket's credential attributes
|
|
270
|
+
* are `undefined` unless Fly still reports them.
|
|
271
|
+
*
|
|
272
|
+
* **Example:** Reference an existing bucket
|
|
273
|
+
* ```typescript
|
|
274
|
+
* const assets = Fly.Bucket.lookup({ name: "my-assets" });
|
|
275
|
+
* ```
|
|
276
|
+
*
|
|
248
277
|
* @resource
|
|
249
278
|
* @product Bucket
|
|
250
279
|
*/
|
|
@@ -267,10 +296,29 @@ export class BucketProvisionFailed extends Data.TaggedError("Fly.BucketProvision
|
|
|
267
296
|
class BucketPending extends Data.TaggedError("Fly.BucketPending")<{
|
|
268
297
|
resourceName: string;
|
|
269
298
|
status: string;
|
|
270
|
-
}> {
|
|
299
|
+
}> {
|
|
300
|
+
override get message() {
|
|
301
|
+
return `Tigris bucket ${this.resourceName} is still ${this.status}`;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Fly lists an add-on whose Tigris bucket is gone, and Fly's delete
|
|
307
|
+
* fails on the missing bucket.
|
|
308
|
+
*/
|
|
309
|
+
export class BucketRecordStale extends Data.TaggedError("Fly.BucketRecordStale")<{
|
|
310
|
+
resourceName: string;
|
|
311
|
+
reason: string;
|
|
312
|
+
}> {
|
|
313
|
+
override get message() {
|
|
314
|
+
return `Fly keeps a stale add-on record for Tigris bucket ${this.resourceName}: ${this.reason}`;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
271
317
|
|
|
272
318
|
const TIGRIS = "tigris" as const;
|
|
273
319
|
|
|
320
|
+
const TIGRIS_ENDPOINT = "https://fly.storage.tigris.dev";
|
|
321
|
+
|
|
274
322
|
const backoff = Schedule.min([
|
|
275
323
|
Schedule.exponential(Duration.millis(500), 1.5),
|
|
276
324
|
Schedule.spaced(Duration.seconds(5)),
|
|
@@ -493,32 +541,213 @@ const waitUntilReady = (name: string, addOnId: string) =>
|
|
|
493
541
|
Effect.catchTag("Fly.BucketPending", () => findById(addOnId)),
|
|
494
542
|
);
|
|
495
543
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
544
|
+
interface TigrisKeys {
|
|
545
|
+
accessKeyId: Redacted.Redacted<string>;
|
|
546
|
+
secretAccessKey: Redacted.Redacted<string>;
|
|
547
|
+
endpoint: string;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/** The bucket's own S3 keys. Fly returns them only from createAddOn. */
|
|
551
|
+
const tigrisKeysOf = (
|
|
552
|
+
attrs: Pick<Bucket["Attributes"], "accessKeyId" | "secretAccessKey" | "endpoint">,
|
|
553
|
+
): TigrisKeys | undefined =>
|
|
554
|
+
attrs.accessKeyId !== undefined && attrs.secretAccessKey !== undefined
|
|
555
|
+
? {
|
|
556
|
+
accessKeyId: attrs.accessKeyId,
|
|
557
|
+
secretAccessKey: attrs.secretAccessKey,
|
|
558
|
+
endpoint: asString(attrs.endpoint) ?? TIGRIS_ENDPOINT,
|
|
559
|
+
}
|
|
560
|
+
: undefined;
|
|
561
|
+
|
|
562
|
+
const withTigrisKeys = <A, E>(
|
|
563
|
+
keys: TigrisKeys,
|
|
564
|
+
operation: Effect.Effect<A, E, Credentials | HttpClient.HttpClient>,
|
|
565
|
+
) =>
|
|
566
|
+
operation.pipe(
|
|
567
|
+
Effect.provide(
|
|
568
|
+
Layer.mergeAll(
|
|
569
|
+
fromCredentials(
|
|
570
|
+
{ accessKeyId: keys.accessKeyId, secretAccessKey: keys.secretAccessKey },
|
|
571
|
+
"auto" as RegionName,
|
|
572
|
+
),
|
|
573
|
+
AwsEndpoint.of(keys.endpoint),
|
|
574
|
+
FetchHttpClient.layer,
|
|
575
|
+
),
|
|
576
|
+
),
|
|
577
|
+
);
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Signed HeadBucket. Tigris answers 404 for a name it has no bucket
|
|
581
|
+
* under and 403 for a bucket the keys don't own. After a delete it can
|
|
582
|
+
* keep answering 200 to the deleted bucket's own keys for minutes.
|
|
583
|
+
*/
|
|
584
|
+
const tigrisHasBucket = (keys: TigrisKeys, bucketName: string) =>
|
|
585
|
+
withTigrisKeys(keys, S3.headBucket({ Bucket: bucketName })).pipe(
|
|
586
|
+
Effect.as(true),
|
|
587
|
+
Effect.catchTag(["NotFound", "NoSuchBucket"], () => Effect.succeed(false)),
|
|
588
|
+
);
|
|
589
|
+
|
|
590
|
+
const requestDelete = (addOnId: string, name: string) =>
|
|
591
|
+
addons
|
|
592
|
+
.deleteAddOn({
|
|
593
|
+
input: addOnId.length > 0 ? { addOnId } : { name, provider: TIGRIS },
|
|
594
|
+
})
|
|
595
|
+
.pipe(
|
|
596
|
+
Effect.as("deleted" as const),
|
|
597
|
+
Effect.catchTag("TigrisBucketNotFound", () => Effect.succeed("missing" as const)),
|
|
598
|
+
);
|
|
599
|
+
|
|
600
|
+
interface DeleteTarget {
|
|
601
|
+
addOnId: string;
|
|
602
|
+
/** Fly add-on name. */
|
|
603
|
+
name: string;
|
|
604
|
+
/** Tigris bucket name (the add-on name unless Fly said otherwise). */
|
|
605
|
+
bucketName: string;
|
|
606
|
+
keys: TigrisKeys | undefined;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const deleteTargetOf = (attrs: Bucket["Attributes"]): DeleteTarget => ({
|
|
610
|
+
addOnId: attrs.addOnId,
|
|
611
|
+
name: attrs.name,
|
|
612
|
+
bucketName: asString(attrs.bucketName) ?? attrs.name,
|
|
613
|
+
keys: tigrisKeysOf(attrs),
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Delete through Fly (which also empties the bucket), then confirm.
|
|
618
|
+
* With the bucket's keys, Tigris decides (its missing-bucket answer to
|
|
619
|
+
* the delete, else HeadBucket): Fly's list lags it and can keep a stale
|
|
620
|
+
* record. Without keys, the add-on must leave Fly's list.
|
|
621
|
+
* Fails with BucketPending once the confirm runs out; `missing` reports
|
|
622
|
+
* whether Fly's last delete said Tigris has no such bucket while the
|
|
623
|
+
* record stayed listed (only without keys).
|
|
624
|
+
*/
|
|
625
|
+
const deleteVerified = (target: DeleteTarget) =>
|
|
626
|
+
Effect.suspend(() => {
|
|
627
|
+
const state = { missing: false };
|
|
628
|
+
return Effect.gen(function* () {
|
|
629
|
+
// Other delete failures (a bucket still attached to an App, a Fly
|
|
630
|
+
// 5xx) are transient; the confirm below decides.
|
|
631
|
+
const requested = yield* Effect.result(requestDelete(target.addOnId, target.name));
|
|
632
|
+
state.missing = Result.isSuccess(requested) && requested.success === "missing";
|
|
633
|
+
if (target.keys !== undefined) {
|
|
634
|
+
// Fly forwards Tigris's own "bucket does not exist" for the delete:
|
|
635
|
+
// the bucket is gone. HeadBucket can keep answering 200 to the
|
|
636
|
+
// deleted bucket's keys for minutes after that.
|
|
637
|
+
if (state.missing) return;
|
|
638
|
+
if (!(yield* tigrisHasBucket(target.keys, target.bucketName))) return;
|
|
639
|
+
return yield* new BucketPending({ resourceName: target.name, status: "deleting" });
|
|
640
|
+
}
|
|
641
|
+
const still =
|
|
642
|
+
(target.addOnId.length > 0 ? yield* findById(target.addOnId) : undefined) ??
|
|
643
|
+
(target.name.length > 0 ? yield* findByName(target.name) : undefined);
|
|
644
|
+
if (still !== undefined) {
|
|
645
|
+
return yield* new BucketPending({
|
|
646
|
+
resourceName: target.name,
|
|
647
|
+
status: still.status ?? "deleting",
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
}).pipe(
|
|
651
|
+
Effect.retry({
|
|
652
|
+
while: (e) => e._tag === "Fly.BucketPending",
|
|
653
|
+
schedule: Schedule.spaced("2 seconds"),
|
|
654
|
+
times: 20,
|
|
505
655
|
}),
|
|
656
|
+
Effect.as({ missing: false }),
|
|
657
|
+
Effect.catchTag("Fly.BucketPending", (pending) =>
|
|
658
|
+
state.missing && target.keys === undefined
|
|
659
|
+
? Effect.succeed({ missing: true })
|
|
660
|
+
: Effect.fail(pending),
|
|
661
|
+
),
|
|
506
662
|
);
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
})
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
const randomBase32 = (length: number) =>
|
|
666
|
+
Effect.sync(() => {
|
|
667
|
+
const alphabet = "abcdefghijklmnopqrstuvwxyz234567";
|
|
668
|
+
return Array.from(crypto.getRandomValues(new Uint8Array(length)), (b) => alphabet[b % 32]).join(
|
|
669
|
+
"",
|
|
670
|
+
);
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* A throwaway bucket, only for its keys. Its name matches the physical
|
|
675
|
+
* name shape, so a leaked one is listed and nuked like any other.
|
|
676
|
+
*/
|
|
677
|
+
const helperBucket = Effect.fn(function* (orgSlug: string) {
|
|
678
|
+
const org = yield* resolveOrganization(orgSlug);
|
|
679
|
+
if (org === undefined) {
|
|
680
|
+
return yield* new BucketOrgMissing({ orgSlug });
|
|
681
|
+
}
|
|
682
|
+
yield* ensureTos(orgSlug, org.id);
|
|
683
|
+
const name = `alchemy-fix-${yield* randomBase32(16)}`;
|
|
684
|
+
const created = yield* addons.createAddOn({
|
|
685
|
+
input: {
|
|
686
|
+
type: TIGRIS,
|
|
687
|
+
name,
|
|
688
|
+
organizationId: org.id,
|
|
689
|
+
options: desiredOptions({}),
|
|
690
|
+
},
|
|
691
|
+
});
|
|
692
|
+
return toAttrs(created.addOn, undefined, { name, orgSlug });
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Fly keeps an add-on record whose Tigris bucket is gone, and its
|
|
697
|
+
* delete fails on the missing bucket. Recreate the name in Tigris with a
|
|
698
|
+
* throwaway bucket's keys so Fly's delete has a bucket to remove.
|
|
699
|
+
* CreateBucket fails on a name Tigris still holds, so this never touches
|
|
700
|
+
* a live bucket. A name deleted in the last several minutes stays
|
|
701
|
+
* blocked (`BucketInaccessible`); the delete fails and a later one
|
|
702
|
+
* repairs it.
|
|
703
|
+
*/
|
|
704
|
+
const repairStaleRecord = Effect.fn(function* (target: DeleteTarget, orgSlug: string) {
|
|
705
|
+
yield* Effect.logWarning(
|
|
706
|
+
`Fly still lists Tigris bucket ${target.name} after Tigris reported it gone; repairing the stale record`,
|
|
707
|
+
);
|
|
708
|
+
yield* Effect.scoped(
|
|
709
|
+
Effect.gen(function* () {
|
|
710
|
+
const helper = yield* Effect.acquireRelease(helperBucket(orgSlug), (attrs) =>
|
|
711
|
+
deleteVerified(deleteTargetOf(attrs)).pipe(Effect.ignore),
|
|
712
|
+
);
|
|
713
|
+
const keys = tigrisKeysOf(helper);
|
|
714
|
+
if (keys === undefined) {
|
|
715
|
+
return yield* new BucketRecordStale({
|
|
716
|
+
resourceName: target.name,
|
|
717
|
+
reason: "Fly returned no keys for the temporary repair bucket",
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
const recreated = yield* Effect.result(
|
|
721
|
+
withTigrisKeys(keys, S3.createBucket({ Bucket: target.bucketName })),
|
|
722
|
+
);
|
|
723
|
+
if (Result.isFailure(recreated)) {
|
|
724
|
+
return yield* new BucketRecordStale({
|
|
725
|
+
resourceName: target.name,
|
|
726
|
+
reason: `Tigris would not recreate the name (${recreated.failure._tag}); a recently deleted name stays blocked for several minutes, so retry the delete later`,
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
const result = yield* deleteVerified({ ...target, keys: undefined });
|
|
730
|
+
if (result.missing) {
|
|
731
|
+
return yield* new BucketRecordStale({
|
|
732
|
+
resourceName: target.name,
|
|
733
|
+
reason: "Fly's delete still reported a missing bucket after the name was recreated",
|
|
734
|
+
});
|
|
735
|
+
}
|
|
520
736
|
}),
|
|
521
737
|
);
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Delete and confirm. A stale Fly record is repaired only when there are
|
|
742
|
+
* no keys to ask Tigris with (nuke, adopted buckets): with keys, Tigris's
|
|
743
|
+
* 404 already proves the bucket is gone.
|
|
744
|
+
*/
|
|
745
|
+
const waitUntilGone = Effect.fn(function* (attrs: Bucket["Attributes"]) {
|
|
746
|
+
const target = deleteTargetOf(attrs);
|
|
747
|
+
const result = yield* deleteVerified(target);
|
|
748
|
+
if (!result.missing) return;
|
|
749
|
+
yield* repairStaleRecord(target, attrs.orgSlug ?? (yield* resolveOrgSlug()));
|
|
750
|
+
});
|
|
522
751
|
|
|
523
752
|
const resolveOrganization = (orgSlug: string) =>
|
|
524
753
|
addons
|
|
@@ -582,6 +811,15 @@ export const BucketProvider = () =>
|
|
|
582
811
|
return undefined;
|
|
583
812
|
}),
|
|
584
813
|
|
|
814
|
+
lookup: Effect.fn(function* ({ name }) {
|
|
815
|
+
const found = yield* findByName(name);
|
|
816
|
+
if (found === undefined) return undefined;
|
|
817
|
+
const detail = yield* addons
|
|
818
|
+
.addOn({ id: found.id, name: found.name ?? name, provider: TIGRIS })
|
|
819
|
+
.pipe(Effect.catchTag("FlyIoParseError", () => Effect.undefined));
|
|
820
|
+
return toAttrs(detail ?? found, undefined, { name });
|
|
821
|
+
}),
|
|
822
|
+
|
|
585
823
|
read: Effect.fn(function* ({ id, olds, output }) {
|
|
586
824
|
const name = yield* resolveBucketName(id, olds?.name, output?.name);
|
|
587
825
|
const found =
|
|
@@ -716,15 +954,8 @@ export const BucketProvider = () =>
|
|
|
716
954
|
}),
|
|
717
955
|
|
|
718
956
|
delete: Effect.fn(function* ({ output }) {
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
if (addOnId.length === 0 && name.length === 0) return;
|
|
722
|
-
// Always wait for the add-on to leave the list. A failed
|
|
723
|
-
// deleteAddOn plus a single list miss is not gone — Tigris
|
|
724
|
-
// list lags, and a bucket still attached to an App is a
|
|
725
|
-
// transient error. waitUntilGone retries delete and only
|
|
726
|
-
// succeeds once list (id and name) is empty.
|
|
727
|
-
yield* waitUntilGone(addOnId, name);
|
|
957
|
+
if (output.addOnId.length === 0 && output.name.length === 0) return;
|
|
958
|
+
yield* waitUntilGone(output);
|
|
728
959
|
}),
|
|
729
960
|
});
|
|
730
961
|
|
package/src/Certificate.ts
CHANGED
|
@@ -97,9 +97,21 @@ export type Certificate = Resource<
|
|
|
97
97
|
source: CertificateSource;
|
|
98
98
|
},
|
|
99
99
|
never,
|
|
100
|
-
Providers
|
|
100
|
+
Providers,
|
|
101
|
+
CertificateLookup
|
|
101
102
|
>;
|
|
102
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Props for {@link Certificate.lookup}: find a certificate this stack
|
|
106
|
+
* doesn't manage by its App and hostname.
|
|
107
|
+
*/
|
|
108
|
+
export interface CertificateLookup {
|
|
109
|
+
/** Fly App name the certificate is attached to. */
|
|
110
|
+
appName: string;
|
|
111
|
+
/** Hostname the certificate covers. */
|
|
112
|
+
hostname: string;
|
|
113
|
+
}
|
|
114
|
+
|
|
103
115
|
/**
|
|
104
116
|
* A Fly.Certificate covers a hostname on an {@link App}. Default
|
|
105
117
|
* `kind` is `"acme"` (Let's Encrypt). `"custom"` uploads a PEM.
|
|
@@ -176,6 +188,20 @@ export type Certificate = Resource<
|
|
|
176
188
|
* });
|
|
177
189
|
* ```
|
|
178
190
|
*
|
|
191
|
+
* ### Looking up an existing certificate
|
|
192
|
+
* `Certificate.lookup` references a certificate Alchemy doesn't manage
|
|
193
|
+
* (added with `fly certs add` or by another stack). It is read during
|
|
194
|
+
* plan/deploy, never created, updated or deleted.
|
|
195
|
+
*
|
|
196
|
+
* **Example:** Read the DNS records Fly expects
|
|
197
|
+
* ```typescript
|
|
198
|
+
* const cert = Fly.Certificate.lookup({
|
|
199
|
+
* appName: "my-site",
|
|
200
|
+
* hostname: "www.example.com",
|
|
201
|
+
* });
|
|
202
|
+
* const target = cert.dnsRequirements;
|
|
203
|
+
* ```
|
|
204
|
+
*
|
|
179
205
|
* @resource
|
|
180
206
|
* @product Certificate
|
|
181
207
|
*/
|
|
@@ -423,6 +449,11 @@ export const CertificateProvider = () =>
|
|
|
423
449
|
return undefined;
|
|
424
450
|
}),
|
|
425
451
|
|
|
452
|
+
lookup: Effect.fn(function* ({ appName, hostname }) {
|
|
453
|
+
const found = yield* getByHostname(appName, hostname);
|
|
454
|
+
return found === undefined ? undefined : toAttrs(appName, found, hostname);
|
|
455
|
+
}),
|
|
456
|
+
|
|
426
457
|
read: Effect.fn(function* ({ olds, output }) {
|
|
427
458
|
const appName = output?.appName ?? appNameOf(olds?.app);
|
|
428
459
|
const hostname = output?.hostname ?? olds?.hostname;
|
package/src/Credentials.ts
CHANGED
|
@@ -24,11 +24,14 @@ export {
|
|
|
24
24
|
*/
|
|
25
25
|
export const bindFlyApiToken = (): Effect.Effect<void, never, Credentials> =>
|
|
26
26
|
Effect.gen(function* () {
|
|
27
|
+
// Bound as Redacted so a Service that owns its App stores the token
|
|
28
|
+
// as an App secret, not plain Machine env. The runtime reads it back
|
|
29
|
+
// with `CredentialsFromEnv`.
|
|
27
30
|
const token = globalThis.__ALCHEMY_RUNTIME__
|
|
28
|
-
? ""
|
|
31
|
+
? Redacted.make("")
|
|
29
32
|
: yield* Credentials.pipe(
|
|
30
33
|
Effect.flatMap((resolve) => resolve),
|
|
31
|
-
Effect.map((cfg) =>
|
|
34
|
+
Effect.map((cfg) => cfg.apiKey),
|
|
32
35
|
// Binding initialization cannot fail; the engine reports this
|
|
33
36
|
// defect as `CredentialsRequired`.
|
|
34
37
|
orDieCredentialsUnavailable("Fly"),
|
|
@@ -44,9 +47,10 @@ export const bindFlyApiToken = (): Effect.Effect<void, never, Credentials> =>
|
|
|
44
47
|
* `Credentials`, and the binding must use them — a laptop deploy has no
|
|
45
48
|
* `FLY_API_TOKEN` in its env once the token lives in the Alchemy profile.
|
|
46
49
|
* Inside a deployed Machine there is no profile; `bindFlyApiToken`
|
|
47
|
-
* has already `RuntimeContext.set` the org token, which
|
|
48
|
-
*
|
|
49
|
-
* `Credentials` when present, otherwise read
|
|
50
|
+
* has already `RuntimeContext.set` the org token, which reaches the
|
|
51
|
+
* Machine as `FLY_API_TOKEN` (an App secret, or Machine env in a shared
|
|
52
|
+
* App). So: reuse the ambient `Credentials` when present, otherwise read
|
|
53
|
+
* it with {@link CredentialsFromEnv}.
|
|
50
54
|
*/
|
|
51
55
|
export const CredentialsFromAmbientOrEnv: Layer.Layer<Credentials> = Layer.effect(
|
|
52
56
|
Credentials,
|
package/src/IpAssignment.ts
CHANGED
|
@@ -85,9 +85,21 @@ export type IpAssignment = Resource<
|
|
|
85
85
|
createdAt: string | undefined;
|
|
86
86
|
},
|
|
87
87
|
never,
|
|
88
|
-
Providers
|
|
88
|
+
Providers,
|
|
89
|
+
IpAssignmentLookup
|
|
89
90
|
>;
|
|
90
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Props for {@link IpAssignment.lookup}: find an address this stack
|
|
94
|
+
* doesn't manage by its App and IP.
|
|
95
|
+
*/
|
|
96
|
+
export interface IpAssignmentLookup {
|
|
97
|
+
/** Fly App name the address is assigned to. */
|
|
98
|
+
appName: string;
|
|
99
|
+
/** The allocated address. */
|
|
100
|
+
ip: string;
|
|
101
|
+
}
|
|
102
|
+
|
|
91
103
|
const resolveIpAssignmentProps = (
|
|
92
104
|
props: IpAssignmentProps | Effect.Effect<IpAssignmentProps, never, Providers>,
|
|
93
105
|
): Effect.Effect<IpAssignmentProps, never, Providers> =>
|
|
@@ -247,6 +259,19 @@ const IpAssignmentResource = Resource<IpAssignment>("Fly.IpAssignment");
|
|
|
247
259
|
* );
|
|
248
260
|
* ```
|
|
249
261
|
*
|
|
262
|
+
* ### Looking up an existing address
|
|
263
|
+
* `IpAssignment.lookup` references an address Alchemy doesn't manage
|
|
264
|
+
* (allocated with `fly ips allocate-v4` or by another stack). It is read
|
|
265
|
+
* during plan/deploy, never allocated or released.
|
|
266
|
+
*
|
|
267
|
+
* **Example:** Reference an existing address
|
|
268
|
+
* ```typescript
|
|
269
|
+
* const ip = Fly.IpAssignment.lookup({
|
|
270
|
+
* appName: "my-site",
|
|
271
|
+
* ip: "2a09:8280:1::1",
|
|
272
|
+
* });
|
|
273
|
+
* ```
|
|
274
|
+
*
|
|
250
275
|
* @resource
|
|
251
276
|
* @product App
|
|
252
277
|
*/
|
|
@@ -439,6 +464,11 @@ export const IpAssignmentProvider = () =>
|
|
|
439
464
|
return undefined;
|
|
440
465
|
}),
|
|
441
466
|
|
|
467
|
+
lookup: Effect.fn(function* ({ appName, ip }) {
|
|
468
|
+
const found = yield* findByIp(appName, ip);
|
|
469
|
+
return found?.ip === undefined ? undefined : toAttrs(appName, found);
|
|
470
|
+
}),
|
|
471
|
+
|
|
442
472
|
read: Effect.fn(function* ({ olds, output }) {
|
|
443
473
|
const appName = output?.appName ?? appNameOf(olds?.app);
|
|
444
474
|
if (appName === undefined) return undefined;
|