@alchemy.run/fly 2.0.0-beta.84 → 2.0.0-beta.85
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/App.d.ts +4 -4
- package/lib/App.d.ts.map +1 -1
- package/lib/App.js +4 -6
- package/lib/App.js.map +1 -1
- package/lib/AuthProvider.d.ts +1 -1
- package/lib/AuthProvider.d.ts.map +1 -1
- package/lib/BindService.d.ts.map +1 -1
- package/lib/BindService.js +1 -1
- package/lib/BindService.js.map +1 -1
- package/lib/Bucket.d.ts +2 -2
- package/lib/Bucket.d.ts.map +1 -1
- package/lib/Bucket.js +10 -10
- package/lib/Bucket.js.map +1 -1
- package/lib/BucketBinding.js +1 -1
- package/lib/BucketBinding.js.map +1 -1
- package/lib/Certificate.js +4 -4
- package/lib/Certificate.js.map +1 -1
- package/lib/ConnectPostgres.d.ts +1 -1
- package/lib/ConnectPostgres.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/Credentials.d.ts +1 -1
- package/lib/Credentials.d.ts.map +1 -1
- package/lib/Credentials.js +10 -14
- package/lib/Credentials.js.map +1 -1
- package/lib/DeploymentImages.js +1 -1
- package/lib/DeploymentImages.js.map +1 -1
- package/lib/Errors.d.ts +1 -1
- package/lib/Errors.d.ts.map +1 -1
- package/lib/IpAssignment.js +4 -4
- package/lib/IpAssignment.js.map +1 -1
- package/lib/Machine.d.ts +1 -1
- package/lib/Machine.d.ts.map +1 -1
- package/lib/Machine.js +2 -2
- package/lib/Machine.js.map +1 -1
- package/lib/Postgres.d.ts +1 -1
- package/lib/Postgres.d.ts.map +1 -1
- package/lib/Postgres.js +6 -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/Redis.d.ts +2 -2
- package/lib/Redis.d.ts.map +1 -1
- package/lib/Redis.js +9 -6
- package/lib/Redis.js.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/Secret.d.ts +1 -1
- package/lib/Secret.d.ts.map +1 -1
- package/lib/Secret.js +2 -2
- package/lib/Secret.js.map +1 -1
- package/lib/SecretKey.d.ts +1 -1
- package/lib/SecretKey.d.ts.map +1 -1
- package/lib/SecretKey.js +2 -2
- package/lib/SecretKey.js.map +1 -1
- package/lib/Service.d.ts +8 -13
- package/lib/Service.d.ts.map +1 -1
- package/lib/Service.js +51 -18
- package/lib/Service.js.map +1 -1
- package/lib/Sprite.d.ts +2 -2
- package/lib/Sprite.d.ts.map +1 -1
- package/lib/Sprite.js +7 -5
- package/lib/Sprite.js.map +1 -1
- package/lib/Volume.d.ts +2 -2
- package/lib/Volume.d.ts.map +1 -1
- package/lib/Volume.js +5 -5
- package/lib/Volume.js.map +1 -1
- package/lib/VolumeSnapshot.js +1 -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/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/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/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/WriteCertificatesHttp.js +2 -2
- package/lib/WriteCertificatesHttp.js.map +1 -1
- package/lib/bluegreen.d.ts +4 -4
- package/lib/bluegreen.d.ts.map +1 -1
- package/lib/bluegreen.js +6 -4
- package/lib/bluegreen.js.map +1 -1
- package/lib/hosted.d.ts +3 -4
- package/lib/hosted.d.ts.map +1 -1
- package/lib/hosted.js.map +1 -1
- 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 +2 -2
- package/lib/replicas.d.ts +10 -10
- package/lib/replicas.d.ts.map +1 -1
- package/lib/replicas.js +7 -7
- package/lib/replicas.js.map +1 -1
- package/package.json +13 -13
- package/src/App.ts +5 -7
- package/src/BindService.ts +1 -2
- package/src/Bucket.ts +15 -13
- package/src/BucketBinding.ts +1 -1
- package/src/Certificate.ts +4 -4
- package/src/ConnectPostgres.ts +1 -1
- package/src/ConnectPostgresHttp.ts +1 -1
- package/src/Credentials.ts +9 -36
- package/src/DeploymentImages.ts +1 -1
- package/src/Errors.ts +1 -1
- package/src/IpAssignment.ts +4 -4
- package/src/Machine.ts +5 -3
- package/src/Postgres.ts +7 -7
- package/src/PostgresMigrations.ts +39 -99
- package/src/Redis.ts +11 -8
- package/src/RedisBinding.ts +1 -1
- package/src/RedisHttp.ts +4 -1
- package/src/Secret.ts +3 -3
- package/src/SecretKey.ts +3 -3
- package/src/Service.ts +53 -19
- package/src/Sprite.ts +11 -9
- package/src/Volume.ts +6 -6
- package/src/VolumeSnapshot.ts +1 -1
- 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 +7 -4
- package/src/hosted.ts +3 -4
- package/src/internal/RemoteBuilder.ts +37 -19
- package/src/replicas.ts +8 -8
package/src/Secret.ts
CHANGED
|
@@ -207,7 +207,7 @@ export const Secret: typeof SecretResource = Object.assign(
|
|
|
207
207
|
|
|
208
208
|
export class SecretNotCreated extends Data.TaggedError("Fly.SecretNotCreated")<{
|
|
209
209
|
appName: string;
|
|
210
|
-
|
|
210
|
+
secretName: string;
|
|
211
211
|
}> {}
|
|
212
212
|
|
|
213
213
|
export class SecretAppRequired extends Data.TaggedError("Fly.SecretAppRequired")<{
|
|
@@ -251,7 +251,7 @@ const getByName = (appName: string, secretName: string) =>
|
|
|
251
251
|
secret_name: secretName,
|
|
252
252
|
show_secrets: false,
|
|
253
253
|
})
|
|
254
|
-
.pipe(Effect.catchTag("NotFound", () => Effect.
|
|
254
|
+
.pipe(Effect.catchTag("NotFound", () => Effect.undefined));
|
|
255
255
|
|
|
256
256
|
const listSecrets = (appName: string) =>
|
|
257
257
|
machines
|
|
@@ -360,7 +360,7 @@ export const SecretProvider = () =>
|
|
|
360
360
|
}
|
|
361
361
|
|
|
362
362
|
if (current === undefined) {
|
|
363
|
-
return yield* new SecretNotCreated({ appName, name });
|
|
363
|
+
return yield* new SecretNotCreated({ appName, secretName: name });
|
|
364
364
|
}
|
|
365
365
|
|
|
366
366
|
// Sync — skip if we just created. Prefer observed digest vs hash of
|
package/src/SecretKey.ts
CHANGED
|
@@ -184,7 +184,7 @@ export const SecretKey: typeof SecretKeyResource = Object.assign(
|
|
|
184
184
|
|
|
185
185
|
export class SecretKeyNotCreated extends Data.TaggedError("Fly.SecretKeyNotCreated")<{
|
|
186
186
|
appName: string;
|
|
187
|
-
|
|
187
|
+
secretName: string;
|
|
188
188
|
}> {}
|
|
189
189
|
|
|
190
190
|
export class SecretKeyAppRequired extends Data.TaggedError("Fly.SecretKeyAppRequired")<{
|
|
@@ -223,7 +223,7 @@ const getByName = (appName: string, secretName: string) =>
|
|
|
223
223
|
app_name: appName,
|
|
224
224
|
secret_name: secretName,
|
|
225
225
|
})
|
|
226
|
-
.pipe(Effect.catchTag("NotFound", () => Effect.
|
|
226
|
+
.pipe(Effect.catchTag("NotFound", () => Effect.undefined));
|
|
227
227
|
|
|
228
228
|
const listKeys = (appName: string) =>
|
|
229
229
|
machines.listSecretKeys({ app_name: appName }).pipe(
|
|
@@ -346,7 +346,7 @@ export const SecretKeyProvider = () =>
|
|
|
346
346
|
}
|
|
347
347
|
|
|
348
348
|
if (current === undefined) {
|
|
349
|
-
return yield* new SecretKeyNotCreated({ appName, name });
|
|
349
|
+
return yield* new SecretKeyNotCreated({ appName, secretName: name });
|
|
350
350
|
}
|
|
351
351
|
|
|
352
352
|
// Sync — re-set when the caller supplied new material. Observed
|
package/src/Service.ts
CHANGED
|
@@ -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";
|
|
@@ -928,28 +929,23 @@ export type ServiceRuntimeContext = FlyHostRuntimeContext;
|
|
|
928
929
|
* ) {}
|
|
929
930
|
* ```
|
|
930
931
|
*
|
|
931
|
-
* **Example:** Install
|
|
932
|
-
* `
|
|
933
|
-
*
|
|
934
|
-
*
|
|
932
|
+
* **Example:** Install a native package unbundled
|
|
933
|
+
* `build.install` runs `npm install` in the image for packages that
|
|
934
|
+
* cannot be bundled, such as native addons — same `build.install` as
|
|
935
|
+
* Lambda.
|
|
935
936
|
* ```typescript
|
|
936
937
|
* export default class Api extends Fly.Service<Api>()(
|
|
937
938
|
* "Api",
|
|
938
939
|
* {
|
|
939
940
|
* main: import.meta.url,
|
|
940
941
|
* port: 3000,
|
|
941
|
-
* build: { install: ["
|
|
942
|
+
* build: { install: ["sharp"] },
|
|
942
943
|
* },
|
|
943
944
|
* Effect.gen(function* () {
|
|
944
|
-
* const conn = yield* Fly.ConnectPostgres(Db);
|
|
945
|
-
* const db = yield* Drizzle.Postgres(conn.connectionString);
|
|
946
945
|
* return {
|
|
947
|
-
* fetch: Effect.
|
|
948
|
-
* const rows = yield* db.execute("select 1 as ok");
|
|
949
|
-
* return HttpServerResponse.json({ rows });
|
|
950
|
-
* }),
|
|
946
|
+
* fetch: Effect.succeed(HttpServerResponse.text("hello")),
|
|
951
947
|
* };
|
|
952
|
-
* })
|
|
948
|
+
* }),
|
|
953
949
|
* ) {}
|
|
954
950
|
* ```
|
|
955
951
|
*
|
|
@@ -1064,7 +1060,7 @@ export const Service: Platform<Service, ServiceServices, ServiceShape, ServiceRu
|
|
|
1064
1060
|
});
|
|
1065
1061
|
|
|
1066
1062
|
export class ServiceNotCreated extends Data.TaggedError("Fly.ServiceNotCreated")<{
|
|
1067
|
-
|
|
1063
|
+
resourceName: string;
|
|
1068
1064
|
appName: string;
|
|
1069
1065
|
}> {}
|
|
1070
1066
|
|
|
@@ -1235,9 +1231,6 @@ const configDrifted = (
|
|
|
1235
1231
|
);
|
|
1236
1232
|
};
|
|
1237
1233
|
|
|
1238
|
-
const portsOf = (services: FlyMachineService[] | undefined) =>
|
|
1239
|
-
(services ?? []).flatMap((service) => service.ports ?? []);
|
|
1240
|
-
|
|
1241
1234
|
const withPort = (base: string, port: number, standard: number) =>
|
|
1242
1235
|
port === standard ? base : `${base}:${port}`;
|
|
1243
1236
|
|
|
@@ -1517,6 +1510,26 @@ const toAttrs = (set: ReplicaSet, codeHash: string, access: Access): Service["At
|
|
|
1517
1510
|
code: { hash: codeHash },
|
|
1518
1511
|
});
|
|
1519
1512
|
|
|
1513
|
+
/** The replica set of a Service whose App exists but runs no Machines yet. */
|
|
1514
|
+
const emptyReplicaSet = (appName: string, name: string): ReplicaSet => ({
|
|
1515
|
+
appName,
|
|
1516
|
+
machineId: "",
|
|
1517
|
+
machineIds: [],
|
|
1518
|
+
name,
|
|
1519
|
+
baseName: name,
|
|
1520
|
+
region: "",
|
|
1521
|
+
regions: [],
|
|
1522
|
+
state: "created",
|
|
1523
|
+
instanceId: undefined,
|
|
1524
|
+
privateIp: undefined,
|
|
1525
|
+
imageRef: undefined,
|
|
1526
|
+
guest: undefined,
|
|
1527
|
+
services: undefined,
|
|
1528
|
+
count: 0,
|
|
1529
|
+
mounts: [],
|
|
1530
|
+
replicas: [],
|
|
1531
|
+
});
|
|
1532
|
+
|
|
1520
1533
|
const machineIdsOf = (output: Service["Attributes"] | undefined) =>
|
|
1521
1534
|
output?.machineIds ??
|
|
1522
1535
|
(output?.machineId !== undefined && output.machineId.length > 0 ? [output.machineId] : []);
|
|
@@ -1587,7 +1600,7 @@ export const ServiceProvider = () =>
|
|
|
1587
1600
|
stables: ["appName"],
|
|
1588
1601
|
nuke: { dependsOn: ["Fly.App"] },
|
|
1589
1602
|
|
|
1590
|
-
diff: Effect.fn(function* ({
|
|
1603
|
+
diff: Effect.fn(function* ({ news, output }) {
|
|
1591
1604
|
if (news === undefined) return;
|
|
1592
1605
|
const shape = news as Partial<Pick<ServiceProps, "app" | "public" | "network">>;
|
|
1593
1606
|
const ownsApp = shape.app === undefined;
|
|
@@ -1717,7 +1730,26 @@ export const ServiceProvider = () =>
|
|
|
1717
1730
|
machineIds: machineIdsOf(output),
|
|
1718
1731
|
baseName: name,
|
|
1719
1732
|
});
|
|
1720
|
-
if (found === undefined)
|
|
1733
|
+
if (found === undefined) {
|
|
1734
|
+
// A create interrupted between creating the Service's own App and
|
|
1735
|
+
// starting its Machines (an image build in between can take
|
|
1736
|
+
// minutes) leaves the App with nothing in it. Report it, so the
|
|
1737
|
+
// engine's recovery of the interrupted create deletes it instead
|
|
1738
|
+
// of leaking it. The App's name is derived from this resource's
|
|
1739
|
+
// instance, so an App with that name is this resource's.
|
|
1740
|
+
if (!ownsApp || appName === undefined) return undefined;
|
|
1741
|
+
const app = yield* machines
|
|
1742
|
+
.getApp({ app_name: appName })
|
|
1743
|
+
.pipe(Effect.catchTag("NotFound", () => Effect.undefined));
|
|
1744
|
+
if (app === undefined || !matchesAlchemyPhysicalName(app.name)) return undefined;
|
|
1745
|
+
return toAttrs(emptyReplicaSet(appName, name), output?.code.hash ?? "", {
|
|
1746
|
+
ownsApp,
|
|
1747
|
+
isPublic: false,
|
|
1748
|
+
network: output?.network ?? olds?.network,
|
|
1749
|
+
bindingPort: olds?.bindingPort ?? DEFAULT_BINDING_PORT,
|
|
1750
|
+
rpcToken: output?.rpcToken,
|
|
1751
|
+
});
|
|
1752
|
+
}
|
|
1721
1753
|
return toAttrs(found, output?.code.hash ?? "", {
|
|
1722
1754
|
ownsApp,
|
|
1723
1755
|
isPublic: ownsApp && (yield* hasPublicAddress(found.appName)),
|
|
@@ -1841,7 +1873,9 @@ export const ServiceProvider = () =>
|
|
|
1841
1873
|
buildConfig({ image: imageRef, guest, env, services, mounts, metadata, statics }),
|
|
1842
1874
|
}).pipe(
|
|
1843
1875
|
Effect.catchTag("Fly.ReplicaNotCreated", (error) =>
|
|
1844
|
-
Effect.fail(
|
|
1876
|
+
Effect.fail(
|
|
1877
|
+
new ServiceNotCreated({ resourceName: error.resourceName, appName: error.appName }),
|
|
1878
|
+
),
|
|
1845
1879
|
),
|
|
1846
1880
|
);
|
|
1847
1881
|
return toAttrs(set, codeHash, {
|
package/src/Sprite.ts
CHANGED
|
@@ -318,18 +318,18 @@ export const Sprite: Platform<Sprite, SpriteServices, SpriteShape, SpriteRuntime
|
|
|
318
318
|
);
|
|
319
319
|
|
|
320
320
|
export class SpriteNotCreated extends Data.TaggedError("Fly.SpriteNotCreated")<{
|
|
321
|
-
|
|
321
|
+
resourceName: string;
|
|
322
322
|
}> {}
|
|
323
323
|
|
|
324
324
|
export class SpriteExecFailed extends Data.TaggedError("Fly.SpriteExecFailed")<{
|
|
325
|
-
|
|
325
|
+
resourceName: string;
|
|
326
326
|
cmd: string;
|
|
327
327
|
exitCode: number | undefined;
|
|
328
328
|
stderr: string | undefined;
|
|
329
329
|
}> {}
|
|
330
330
|
|
|
331
331
|
class SpritePending extends Data.TaggedError("Fly.SpritePending")<{
|
|
332
|
-
|
|
332
|
+
resourceName: string;
|
|
333
333
|
}> {}
|
|
334
334
|
|
|
335
335
|
const toStatus = (status: string | undefined): SpriteStatus =>
|
|
@@ -368,12 +368,14 @@ const ownershipLabels = (id: string) =>
|
|
|
368
368
|
});
|
|
369
369
|
|
|
370
370
|
const getByName = (name: string) =>
|
|
371
|
-
sprites.getSprite({ name }).pipe(Effect.catchTag("NotFound", () => Effect.
|
|
371
|
+
sprites.getSprite({ name }).pipe(Effect.catchTag("NotFound", () => Effect.undefined));
|
|
372
372
|
|
|
373
373
|
const waitForSprite = (name: string) =>
|
|
374
374
|
getByName(name).pipe(
|
|
375
375
|
Effect.flatMap((found) =>
|
|
376
|
-
found !== undefined
|
|
376
|
+
found !== undefined
|
|
377
|
+
? Effect.succeed(found)
|
|
378
|
+
: Effect.fail(new SpritePending({ resourceName: name })),
|
|
377
379
|
),
|
|
378
380
|
Effect.retry({
|
|
379
381
|
while: (e) => e._tag === "Fly.SpritePending",
|
|
@@ -458,7 +460,7 @@ const runExec = (input: { name: string; cmd: string[]; env?: string[] }) =>
|
|
|
458
460
|
);
|
|
459
461
|
if ((result.exit_code ?? 0) !== 0) {
|
|
460
462
|
return yield* new SpriteExecFailed({
|
|
461
|
-
|
|
463
|
+
resourceName: input.name,
|
|
462
464
|
cmd: input.cmd.join(" "),
|
|
463
465
|
exitCode: result.exit_code,
|
|
464
466
|
stderr: result.stderr,
|
|
@@ -576,12 +578,12 @@ export const SpriteProvider = () =>
|
|
|
576
578
|
read: Effect.fn(function* ({ id, olds, output }) {
|
|
577
579
|
const name = yield* resolveSpriteName(id, olds?.name, output?.name);
|
|
578
580
|
const found = yield* (
|
|
579
|
-
output?.name !== undefined ? getByName(output.name) : Effect.
|
|
581
|
+
output?.name !== undefined ? getByName(output.name) : Effect.undefined
|
|
580
582
|
).pipe(
|
|
581
583
|
Effect.flatMap((existing) =>
|
|
582
584
|
existing !== undefined ? Effect.succeed(existing) : getByName(name),
|
|
583
585
|
),
|
|
584
|
-
Effect.catchTag("SpritesNotEnabled", () => Effect.
|
|
586
|
+
Effect.catchTag("SpritesNotEnabled", () => Effect.undefined),
|
|
585
587
|
);
|
|
586
588
|
if (found === undefined) return undefined;
|
|
587
589
|
const attrs = toAttrs(found, name, output?.code.hash ?? "");
|
|
@@ -628,7 +630,7 @@ export const SpriteProvider = () =>
|
|
|
628
630
|
}
|
|
629
631
|
|
|
630
632
|
if (current === undefined) {
|
|
631
|
-
return yield* new SpriteNotCreated({ name });
|
|
633
|
+
return yield* new SpriteNotCreated({ resourceName: name });
|
|
632
634
|
}
|
|
633
635
|
// Narrowed once so the `NotFound` fallbacks below keep the
|
|
634
636
|
// non-optional type instead of widening back to `| undefined`.
|
package/src/Volume.ts
CHANGED
|
@@ -16,7 +16,7 @@ export const DEFAULT_VOLUME_REGION = "iad";
|
|
|
16
16
|
export const MIN_SIZE_GB = 1;
|
|
17
17
|
|
|
18
18
|
export class VolumeNotCreated extends Data.TaggedError("Fly.VolumeNotCreated")<{
|
|
19
|
-
|
|
19
|
+
resourceName: string;
|
|
20
20
|
appName: string;
|
|
21
21
|
}> {}
|
|
22
22
|
|
|
@@ -39,7 +39,7 @@ const transientState = (state: string | undefined) =>
|
|
|
39
39
|
export const getVolumeById = (appName: string, volumeId: string) =>
|
|
40
40
|
machines.getVolumeById({ app_name: appName, volume_id: volumeId }).pipe(
|
|
41
41
|
Effect.map((volume) => (destroying(volume.state) ? undefined : volume)),
|
|
42
|
-
Effect.catchTag("NotFound", () => Effect.
|
|
42
|
+
Effect.catchTag("NotFound", () => Effect.undefined),
|
|
43
43
|
);
|
|
44
44
|
|
|
45
45
|
export const listVolumesByApp = (appName: string) =>
|
|
@@ -66,7 +66,7 @@ export const listVolumeGroup = (appName: string, name: string, region: string) =
|
|
|
66
66
|
export const waitUntilVolumeReady = (appName: string, volumeId: string) =>
|
|
67
67
|
getVolumeById(appName, volumeId).pipe(
|
|
68
68
|
Effect.flatMap((volume) => {
|
|
69
|
-
if (volume === undefined) return Effect.
|
|
69
|
+
if (volume === undefined) return Effect.undefined;
|
|
70
70
|
if (transientState(volume.state)) {
|
|
71
71
|
return Effect.fail(
|
|
72
72
|
new VolumePending({
|
|
@@ -130,7 +130,7 @@ export const createVolume = Effect.fn(function* (input: {
|
|
|
130
130
|
source_volume_id: input.disk.sourceVolumeId,
|
|
131
131
|
require_unique_zone: input.disk.requireUniqueZone,
|
|
132
132
|
})
|
|
133
|
-
.pipe(Effect.catchTag("Conflict", () => Effect.
|
|
133
|
+
.pipe(Effect.catchTag("Conflict", () => Effect.undefined));
|
|
134
134
|
if (created?.id !== undefined && created.id.length > 0) {
|
|
135
135
|
return (yield* waitUntilVolumeReady(input.appName, created.id)) ?? created;
|
|
136
136
|
}
|
|
@@ -138,7 +138,7 @@ export const createVolume = Effect.fn(function* (input: {
|
|
|
138
138
|
const hit = group.at(-1);
|
|
139
139
|
if (hit === undefined || hit.id === undefined) {
|
|
140
140
|
return yield* new VolumeNotCreated({
|
|
141
|
-
|
|
141
|
+
resourceName: input.name,
|
|
142
142
|
appName: input.appName,
|
|
143
143
|
});
|
|
144
144
|
}
|
|
@@ -211,7 +211,7 @@ export const ensureVolumeGroup = Effect.fn(function* (input: {
|
|
|
211
211
|
}
|
|
212
212
|
if (group.length < input.count) {
|
|
213
213
|
return yield* new VolumeNotCreated({
|
|
214
|
-
|
|
214
|
+
resourceName: input.name,
|
|
215
215
|
appName: input.appName,
|
|
216
216
|
});
|
|
217
217
|
}
|
package/src/VolumeSnapshot.ts
CHANGED
|
@@ -214,7 +214,7 @@ const waitForNewSnapshot = (appName: string, volumeId: string, knownIds: Readonl
|
|
|
214
214
|
times: 8,
|
|
215
215
|
schedule: backoff,
|
|
216
216
|
}),
|
|
217
|
-
Effect.catchTag("Fly.VolumeSnapshotPending", () => Effect.
|
|
217
|
+
Effect.catchTag("Fly.VolumeSnapshotPending", () => Effect.undefined),
|
|
218
218
|
);
|
|
219
219
|
|
|
220
220
|
export const VolumeSnapshotProvider = () =>
|
|
@@ -95,7 +95,7 @@ const scopeOf = (bucket: Bucket) =>
|
|
|
95
95
|
endpoint === undefined
|
|
96
96
|
) {
|
|
97
97
|
return yield* new TigrisCredentialsMissing({
|
|
98
|
-
|
|
98
|
+
resourceName: bucketName ?? bucket.LogicalId,
|
|
99
99
|
});
|
|
100
100
|
}
|
|
101
101
|
return { bucketName, accessKeyId, secretAccessKey, endpoint, region };
|
|
@@ -131,14 +131,14 @@ const walkFiles = Effect.fn(function* (root: string) {
|
|
|
131
131
|
const path = yield* Path.Path;
|
|
132
132
|
if (!(yield* fs.exists(root))) return [] as string[];
|
|
133
133
|
const names = yield* fs.readDirectory(root, { recursive: true });
|
|
134
|
-
const files = yield* Effect.
|
|
135
|
-
names
|
|
134
|
+
const files = yield* Effect.forEach(
|
|
135
|
+
names,
|
|
136
|
+
(name) =>
|
|
136
137
|
Effect.gen(function* () {
|
|
137
138
|
const full = path.join(root, name);
|
|
138
139
|
const stat = yield* fs.stat(full);
|
|
139
140
|
return stat.type === "File" ? name.replaceAll("\\", "/") : undefined;
|
|
140
141
|
}),
|
|
141
|
-
),
|
|
142
142
|
{ concurrency: "unbounded" },
|
|
143
143
|
);
|
|
144
144
|
return files
|
|
@@ -213,8 +213,9 @@ export const AssetDeploymentProvider = () =>
|
|
|
213
213
|
: path.resolve(initialCwd, news.sourcePath);
|
|
214
214
|
const files = yield* walkFiles(root);
|
|
215
215
|
const observed = yield* listObserved(scope, prefix);
|
|
216
|
-
const prepared = yield* Effect.
|
|
217
|
-
files
|
|
216
|
+
const prepared = yield* Effect.forEach(
|
|
217
|
+
files,
|
|
218
|
+
(relative) =>
|
|
218
219
|
Effect.gen(function* () {
|
|
219
220
|
const body = yield* fs.readFile(path.join(root, relative));
|
|
220
221
|
const key = prefix.length > 0 ? `${prefix}/${relative}` : relative;
|
|
@@ -222,7 +223,6 @@ export const AssetDeploymentProvider = () =>
|
|
|
222
223
|
const cacheControl = cacheControlOf(relative);
|
|
223
224
|
return { relative, key, body, contentType, cacheControl };
|
|
224
225
|
}),
|
|
225
|
-
),
|
|
226
226
|
{ concurrency: "unbounded" },
|
|
227
227
|
);
|
|
228
228
|
const version = yield* Effect.sync(() => {
|
|
@@ -258,10 +258,9 @@ export const AssetDeploymentProvider = () =>
|
|
|
258
258
|
);
|
|
259
259
|
|
|
260
260
|
if (news.purge ?? true) {
|
|
261
|
-
yield* Effect.
|
|
262
|
-
[...observed.keys()]
|
|
263
|
-
|
|
264
|
-
.map((key) => deleteObject(scope, key)),
|
|
261
|
+
yield* Effect.forEach(
|
|
262
|
+
[...observed.keys()].filter((key) => !desired.has(key)),
|
|
263
|
+
(key) => deleteObject(scope, key),
|
|
265
264
|
{ concurrency: s3Concurrency },
|
|
266
265
|
);
|
|
267
266
|
}
|
|
@@ -283,10 +282,9 @@ export const AssetDeploymentProvider = () =>
|
|
|
283
282
|
if (!(olds.purge ?? true)) return;
|
|
284
283
|
const scope = yield* scopeOf(olds.bucket);
|
|
285
284
|
const observed = yield* listObserved(scope, output.prefix);
|
|
286
|
-
yield* Effect.
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
);
|
|
285
|
+
yield* Effect.forEach([...observed.keys()], (key) => deleteObject(scope, key), {
|
|
286
|
+
concurrency: s3Concurrency,
|
|
287
|
+
});
|
|
290
288
|
},
|
|
291
289
|
Effect.catchTag("NoSuchBucket", () => Effect.void),
|
|
292
290
|
),
|
package/src/Website/Astro.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { resolveAstroOutput } from "alchemy/Website/AstroOutput";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
1
3
|
import { frameworkSite, staticConfigFromAssets, type FrameworkSiteProps } from "./FrameworkSite.ts";
|
|
2
4
|
|
|
3
5
|
/** The framework-integration package that drives the Astro build. */
|
|
@@ -21,8 +23,9 @@ export interface AstroProps extends FrameworkSiteProps {
|
|
|
21
23
|
* Astro output target. `"server"` renders pages on demand; individual
|
|
22
24
|
* pages opt into prerendering with `export const prerender = true`.
|
|
23
25
|
* `"static"` prerenders every page at build time and deploys
|
|
24
|
-
* assets-only (no SSR server).
|
|
25
|
-
*
|
|
26
|
+
* assets-only (no SSR server). Overrides the `output` in your
|
|
27
|
+
* `astro.config.*`; leave it unset to use the file's value.
|
|
28
|
+
* @default the config file's `output`, else `"server"`
|
|
26
29
|
*/
|
|
27
30
|
output?: "server" | "static";
|
|
28
31
|
/** Source directory, relative to `rootDir`. @default "./src" */
|
|
@@ -41,9 +44,9 @@ export interface AstroProps extends FrameworkSiteProps {
|
|
|
41
44
|
* running the Node adapter's serve entry (static files first, then the
|
|
42
45
|
* framework handler) on port 3000.
|
|
43
46
|
*
|
|
44
|
-
* Pages render on demand by default (`output: "server"`).
|
|
45
|
-
* `astro: { output: "static" }`
|
|
46
|
-
* is assets-only.
|
|
47
|
+
* Pages render on demand by default (`output: "server"`). When your
|
|
48
|
+
* `astro.config.*` or `astro: { output: "static" }` declares a static
|
|
49
|
+
* site, every page is prerendered and the deploy is assets-only.
|
|
47
50
|
*
|
|
48
51
|
*
|
|
49
52
|
* ### Creating Astro Sites
|
|
@@ -75,13 +78,15 @@ export interface AstroProps extends FrameworkSiteProps {
|
|
|
75
78
|
* @resource
|
|
76
79
|
* @product Website
|
|
77
80
|
*/
|
|
78
|
-
export const Astro = (id: string, props: AstroProps = {}) =>
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
81
|
+
export const Astro = (id: string, props: AstroProps = {}) =>
|
|
82
|
+
resolveAstroOutput({ rootDir: props.rootDir, output: props.astro?.output }).pipe(
|
|
83
|
+
Effect.flatMap((output) =>
|
|
84
|
+
frameworkSite(id, props, {
|
|
85
|
+
name: "Astro",
|
|
86
|
+
framework: ASTRO_FRAMEWORK_SPECIFIER,
|
|
87
|
+
target: ASTRO_NODE_TARGET_SPECIFIER,
|
|
88
|
+
options: { astro: { ...props.astro, output } },
|
|
89
|
+
static: output === "static" ? staticConfigFromAssets(props.assets) : undefined,
|
|
90
|
+
}),
|
|
91
|
+
),
|
|
92
|
+
);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { AlchemyContext } from "alchemy/AlchemyContext";
|
|
2
2
|
import type { MemoOptions } from "alchemy/Command/Memo";
|
|
3
|
+
import * as DNS from "alchemy/DNS/Adapter";
|
|
4
|
+
import type { Input } from "alchemy/Input";
|
|
3
5
|
import * as Namespace from "alchemy/Namespace";
|
|
4
6
|
import * as Output from "alchemy/Output";
|
|
5
7
|
import { ProviderModePolicy } from "alchemy/ProviderMode";
|
|
@@ -18,7 +20,7 @@ import * as Path from "effect/Path";
|
|
|
18
20
|
import * as Redacted from "effect/Redacted";
|
|
19
21
|
import { App } from "../App.ts";
|
|
20
22
|
import { Bucket } from "../Bucket.ts";
|
|
21
|
-
import { Certificate } from "../Certificate.ts";
|
|
23
|
+
import { Certificate, type CertificateDnsRequirements } from "../Certificate.ts";
|
|
22
24
|
import { IpAssignment } from "../IpAssignment.ts";
|
|
23
25
|
import type { Providers } from "../Providers.ts";
|
|
24
26
|
import { Service, type ServiceProps } from "../Service.ts";
|
|
@@ -33,6 +35,80 @@ export type Ref<T> = T | Effect.Effect<T, never, Providers>;
|
|
|
33
35
|
export type { ServerDevProps, WebsiteAssetsProps, WebsiteNotFoundHandling };
|
|
34
36
|
export { staticConfigFromAssets };
|
|
35
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Custom hostname of a Fly website: the hostname itself, or
|
|
40
|
+
* `{ name, dns }` to also publish its DNS records.
|
|
41
|
+
*/
|
|
42
|
+
export type WebsiteDomain =
|
|
43
|
+
| string
|
|
44
|
+
| {
|
|
45
|
+
/** Hostname, e.g. `www.example.com`. */
|
|
46
|
+
name: string;
|
|
47
|
+
/**
|
|
48
|
+
* DNS host that publishes the hostname's records (see
|
|
49
|
+
* [DNS Adapters](/infrastructure-as-code/dns-adapters)), e.g.
|
|
50
|
+
* `Cloudflare.DNS.Adapter()`: the ACME challenge `CNAME` Fly lists in
|
|
51
|
+
* the Certificate's `dnsRequirements`, and `A` / `AAAA` records at
|
|
52
|
+
* the site's addresses. Omitted: publish the records yourself.
|
|
53
|
+
*/
|
|
54
|
+
dns?: DNS.DnsConfig;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** The hostname of a {@link WebsiteDomain}. */
|
|
58
|
+
export const websiteDomainName = (domain: WebsiteDomain | undefined): string | undefined =>
|
|
59
|
+
domain === undefined || typeof domain === "string" ? domain : domain.name;
|
|
60
|
+
|
|
61
|
+
const acmeChallengeRecords = (
|
|
62
|
+
hostname: string,
|
|
63
|
+
requirements: CertificateDnsRequirements | undefined,
|
|
64
|
+
): DNS.DnsRecord[] => {
|
|
65
|
+
const challenge = requirements?.acmeChallenge;
|
|
66
|
+
if (challenge?.target === undefined || challenge.target.length === 0) {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
const expected = `_acme-challenge.${hostname}`;
|
|
70
|
+
const name =
|
|
71
|
+
challenge.name !== undefined &&
|
|
72
|
+
DNS.normalizeDnsName(challenge.name).endsWith(DNS.normalizeDnsName(hostname))
|
|
73
|
+
? challenge.name
|
|
74
|
+
: expected;
|
|
75
|
+
return [{ name, type: "CNAME", value: challenge.target }];
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Publish a Fly website's records through `domain.dns`: the Certificate's
|
|
80
|
+
* ACME challenge `CNAME` (`CertificateValidation`) and `A` / `AAAA`
|
|
81
|
+
* records at the site's shared IPv4 plus any IPv6 Fly lists for the
|
|
82
|
+
* hostname (`Domain`). No-op for a plain hostname or an object without
|
|
83
|
+
* `dns`.
|
|
84
|
+
*/
|
|
85
|
+
export const publishWebsiteDomainDns = Effect.fn("Fly.Website.publishDomainDns")(function* (args: {
|
|
86
|
+
readonly domain: WebsiteDomain;
|
|
87
|
+
readonly certificate: Certificate;
|
|
88
|
+
readonly ip: IpAssignment;
|
|
89
|
+
}) {
|
|
90
|
+
if (typeof args.domain === "string" || args.domain.dns === undefined) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const dns = yield* DNS.resolve(args.domain.dns);
|
|
94
|
+
const name = args.domain.name;
|
|
95
|
+
yield* dns.records("CertificateValidation", {
|
|
96
|
+
records: Output.map(args.certificate.dnsRequirements, (requirements) =>
|
|
97
|
+
acmeChallengeRecords(name, requirements),
|
|
98
|
+
) as unknown as Input<DNS.DnsRecord[]>,
|
|
99
|
+
});
|
|
100
|
+
yield* dns.alias("Domain", {
|
|
101
|
+
name,
|
|
102
|
+
target: {
|
|
103
|
+
ipv4: Output.map(args.ip.ip, (ip) => [ip]) as unknown as Input<string[]>,
|
|
104
|
+
ipv6: Output.map(
|
|
105
|
+
args.certificate.dnsRequirements,
|
|
106
|
+
(requirements) => requirements?.aaaa ?? [],
|
|
107
|
+
) as unknown as Input<string[]>,
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
36
112
|
/**
|
|
37
113
|
* Props shared by every Fly framework website composite.
|
|
38
114
|
*/
|
|
@@ -78,9 +154,15 @@ export interface FrameworkSiteProps {
|
|
|
78
154
|
dev?: ServerDevProps;
|
|
79
155
|
/**
|
|
80
156
|
* Optional custom hostname. Requests ACME (`Fly.Certificate`) on the
|
|
81
|
-
* App
|
|
157
|
+
* App and `url` becomes `https://<domain>`.
|
|
158
|
+
*
|
|
159
|
+
* A string (or `{ name }`) leaves DNS to you. Set `dns` (e.g.
|
|
160
|
+
* `{ name: "www.example.com", dns: Cloudflare.DNS.Adapter() }`) to
|
|
161
|
+
* publish the ACME challenge `CNAME` and the `A` / `AAAA` records
|
|
162
|
+
* through that DNS host. See
|
|
163
|
+
* [DNS Adapters](/infrastructure-as-code/dns-adapters).
|
|
82
164
|
*/
|
|
83
|
-
domain?:
|
|
165
|
+
domain?: WebsiteDomain;
|
|
84
166
|
/**
|
|
85
167
|
* User-defined tags. Accepted for API parity; Fly Services do not
|
|
86
168
|
* surface resource tags.
|
|
@@ -327,16 +409,20 @@ const runFrameworkSite = Effect.fn("Fly.Website.FrameworkSite")(function* (
|
|
|
327
409
|
: undefined,
|
|
328
410
|
});
|
|
329
411
|
|
|
412
|
+
const domainName = websiteDomainName(props.domain);
|
|
330
413
|
const certificate =
|
|
331
|
-
|
|
414
|
+
domainName !== undefined
|
|
332
415
|
? yield* Certificate("Certificate", {
|
|
333
416
|
app,
|
|
334
|
-
hostname:
|
|
417
|
+
hostname: domainName,
|
|
335
418
|
kind: "acme",
|
|
336
419
|
})
|
|
337
420
|
: undefined;
|
|
421
|
+
if (props.domain !== undefined && certificate !== undefined) {
|
|
422
|
+
yield* publishWebsiteDomainDns({ domain: props.domain, certificate, ip });
|
|
423
|
+
}
|
|
338
424
|
|
|
339
|
-
const url =
|
|
425
|
+
const url = domainName !== undefined ? `https://${domainName}` : app.url;
|
|
340
426
|
|
|
341
427
|
return { url, app, service, ip, certificate };
|
|
342
428
|
});
|
|
@@ -15,9 +15,12 @@ import { IpAssignment } from "../IpAssignment.ts";
|
|
|
15
15
|
import { Service, type ServiceProps } from "../Service.ts";
|
|
16
16
|
import {
|
|
17
17
|
type FrameworkSite,
|
|
18
|
+
publishWebsiteDomainDns,
|
|
18
19
|
type Ref,
|
|
19
20
|
type WebsiteAssetsProps,
|
|
21
|
+
type WebsiteDomain,
|
|
20
22
|
staticConfigFromAssets,
|
|
23
|
+
websiteDomainName,
|
|
21
24
|
} from "./FrameworkSite.ts";
|
|
22
25
|
|
|
23
26
|
const DEFAULT_PORT = 3000;
|
|
@@ -69,9 +72,16 @@ export interface StaticSiteProps {
|
|
|
69
72
|
*/
|
|
70
73
|
app?: Ref<App>;
|
|
71
74
|
/**
|
|
72
|
-
* Optional custom hostname. Requests ACME (`Fly.Certificate`) on the App
|
|
75
|
+
* Optional custom hostname. Requests ACME (`Fly.Certificate`) on the App
|
|
76
|
+
* and `url` becomes `https://<domain>`.
|
|
77
|
+
*
|
|
78
|
+
* A string (or `{ name }`) leaves DNS to you. Set `dns` (e.g.
|
|
79
|
+
* `{ name: "www.example.com", dns: Cloudflare.DNS.Adapter() }`) to
|
|
80
|
+
* publish the ACME challenge `CNAME` and the `A` / `AAAA` records
|
|
81
|
+
* through that DNS host. See
|
|
82
|
+
* [DNS Adapters](/infrastructure-as-code/dns-adapters).
|
|
73
83
|
*/
|
|
74
|
-
domain?:
|
|
84
|
+
domain?: WebsiteDomain;
|
|
75
85
|
/**
|
|
76
86
|
* User-defined tags. Accepted for API parity; Fly Services do not
|
|
77
87
|
* surface resource tags.
|
|
@@ -262,14 +272,24 @@ export const StaticSite = (id: string, props: StaticSiteProps) =>
|
|
|
262
272
|
],
|
|
263
273
|
});
|
|
264
274
|
|
|
275
|
+
const domainName = websiteDomainName(props.domain);
|
|
265
276
|
const certificate =
|
|
266
|
-
|
|
267
|
-
? yield* Certificate("Certificate", {
|
|
268
|
-
|
|
269
|
-
|
|
277
|
+
domainName !== undefined
|
|
278
|
+
? yield* Certificate("Certificate", {
|
|
279
|
+
app,
|
|
280
|
+
hostname: domainName,
|
|
281
|
+
kind: "acme",
|
|
282
|
+
}).pipe(Namespace.push(id))
|
|
270
283
|
: undefined;
|
|
284
|
+
if (props.domain !== undefined && certificate !== undefined) {
|
|
285
|
+
yield* publishWebsiteDomainDns({
|
|
286
|
+
domain: props.domain,
|
|
287
|
+
certificate,
|
|
288
|
+
ip,
|
|
289
|
+
}).pipe(Namespace.push(id));
|
|
290
|
+
}
|
|
271
291
|
|
|
272
|
-
const url =
|
|
292
|
+
const url = domainName !== undefined ? `https://${domainName}` : app.url;
|
|
273
293
|
|
|
274
294
|
return { url, app, service, ip, certificate };
|
|
275
295
|
}).pipe(Effect.orDie);
|
package/src/Website/Vite.ts
CHANGED
|
@@ -77,6 +77,15 @@ export interface ViteProps extends FrameworkSiteProps {
|
|
|
77
77
|
* });
|
|
78
78
|
* ```
|
|
79
79
|
*
|
|
80
|
+
* **Example:** Publish DNS in Cloudflare
|
|
81
|
+
* ```typescript
|
|
82
|
+
* // The ACME challenge CNAME and A/AAAA records are published in the
|
|
83
|
+
* // Cloudflare zone. Requires Cloudflare.providers() in the stack.
|
|
84
|
+
* const site = yield* Fly.Website.Vite("Web", {
|
|
85
|
+
* domain: { name: "app.example.com", dns: Cloudflare.DNS.Adapter() },
|
|
86
|
+
* });
|
|
87
|
+
* ```
|
|
88
|
+
*
|
|
80
89
|
* ### Build Configuration
|
|
81
90
|
* **Example:** Custom Output Directory and Base Path
|
|
82
91
|
* ```typescript
|