@molecule/api-code-sandbox-flyio 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +115 -0
- package/README.md +2189 -0
- package/dist/api.d.ts +140 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +226 -0
- package/dist/browser-guard.d.ts +2 -0
- package/dist/browser-guard.d.ts.map +1 -0
- package/dist/browser-guard.js +18 -0
- package/dist/egress.d.ts +180 -0
- package/dist/egress.d.ts.map +1 -0
- package/dist/egress.js +283 -0
- package/dist/exec.d.ts +85 -0
- package/dist/exec.d.ts.map +1 -0
- package/dist/exec.js +215 -0
- package/dist/flycast.d.ts +173 -0
- package/dist/flycast.d.ts.map +1 -0
- package/dist/flycast.js +287 -0
- package/dist/ids.d.ts +79 -0
- package/dist/ids.d.ts.map +1 -0
- package/dist/ids.js +121 -0
- package/dist/index.d.ts +346 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +359 -0
- package/dist/provider.d.ts +24 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +1425 -0
- package/dist/storage.d.ts +119 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +241 -0
- package/dist/templates.d.ts +310 -0
- package/dist/templates.d.ts.map +1 -0
- package/dist/templates.js +655 -0
- package/dist/types.d.ts +519 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +11 -0
- package/dist/utilities.d.ts +29 -0
- package/dist/utilities.d.ts.map +1 -0
- package/dist/utilities.js +44 -0
- package/package.json +63 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flycast private routes — the one mechanism Fly documents for reaching an app
|
|
3
|
+
* ACROSS custom 6PN private networks, and the pure logic behind wiring it per
|
|
4
|
+
* project.
|
|
5
|
+
*
|
|
6
|
+
* **The problem this solves.** This provider puts every project's sandbox app on
|
|
7
|
+
* its own custom 6PN, which is what makes tenants unable to reach each other:
|
|
8
|
+
* "Apps on separate 6PNs can never communicate unless explicitly configured to
|
|
9
|
+
* do so" (https://fly.io/docs/networking/custom-private-networks/). The same
|
|
10
|
+
* sentence severs two paths the product REQUIRES — a sandbox reaching its own
|
|
11
|
+
* `mol_<uuid>` database on the tenant Postgres cluster, and a sandbox reaching
|
|
12
|
+
* the control plane's egress proxy. Neither is optional: a scaffolded app
|
|
13
|
+
* connects to `DATABASE_URL` at startup, and the proxy is where the per-project
|
|
14
|
+
* host allowlist is enforced.
|
|
15
|
+
*
|
|
16
|
+
* **The mechanism.** Fly names exactly three ways to cross a 6PN boundary
|
|
17
|
+
* (https://fly.io/docs/networking/custom-private-networks/): a public service
|
|
18
|
+
* IP, `fly-replay`, and a Flycast private address allocated INTO the calling
|
|
19
|
+
* network. Only the third one grants a single directed edge without exposing
|
|
20
|
+
* anything publicly, so it is what this module implements:
|
|
21
|
+
*
|
|
22
|
+
* > "You can use Flycast to expose an app on one private network to another
|
|
23
|
+
* > private network in your organization; the app won't be accessible via
|
|
24
|
+
* > Flycast from its own network. Use the `--network` option to specify the
|
|
25
|
+
* > network from which requests will originate:
|
|
26
|
+
* > `fly ips allocate-v6 --private --network custom-network-name`"
|
|
27
|
+
* > — https://fly.io/docs/networking/flycast/
|
|
28
|
+
*
|
|
29
|
+
* The REST equivalent is `POST /v1/apps/{app}/ip_assignments`, whose
|
|
30
|
+
* `assignIPRequest` schema carries `network`, `org_slug`, `region`,
|
|
31
|
+
* `service_name` and `type` — read directly from
|
|
32
|
+
* https://docs.machines.dev/openapi.json rather than inferred. `type` is a bare
|
|
33
|
+
* `string` there with no enum and no description, so `private_v6` — the value
|
|
34
|
+
* flyctl passes for `allocate-v6 --private`
|
|
35
|
+
* (https://github.com/superfly/flyctl `internal/command/ips/allocate.go`) — is
|
|
36
|
+
* this provider's default and is configurable via
|
|
37
|
+
* {@link FlyioConfig.privateIpAssignmentType}. Treat the literal as UNVERIFIED
|
|
38
|
+
* against an official Fly *doc page*; the flyctl source is a secondary source.
|
|
39
|
+
*
|
|
40
|
+
* **Three consequences that shape the code, none of which can be designed
|
|
41
|
+
* around:**
|
|
42
|
+
*
|
|
43
|
+
* 1. **The allocation lives on the TARGET app, keyed by the CALLER's network.**
|
|
44
|
+
* So the tenant Postgres app accumulates one private address per project
|
|
45
|
+
* network, and destroying a project must release its own. Fly documents no
|
|
46
|
+
* limit on how many private addresses one app may hold, in either direction —
|
|
47
|
+
* see the module `@remarks` in `index.ts`.
|
|
48
|
+
* 2. **`GET /apps/{app}/ip_assignments` does not report which network an address
|
|
49
|
+
* belongs to** (`IPAssignment` is `{created_at, ip, region, service_name,
|
|
50
|
+
* shared}`). A listing therefore cannot tell you whether THIS project's route
|
|
51
|
+
* already exists, so the addresses are recorded in the sandbox Machine's own
|
|
52
|
+
* metadata and read back from there.
|
|
53
|
+
* 3. **The target app needs a services block.** "Services configured in your
|
|
54
|
+
* app's `fly.toml` with an `[http_service]` or `[services]` section"
|
|
55
|
+
* (https://fly.io/docs/networking/flycast/). That is an operator action on
|
|
56
|
+
* the target app, not something this provider can perform.
|
|
57
|
+
*
|
|
58
|
+
* @module
|
|
59
|
+
*/
|
|
60
|
+
import type { FlyNetworkPolicyPort, FlyPrivateService } from './types.js';
|
|
61
|
+
/**
|
|
62
|
+
* DNS suffix Fly serves a Flycast address at: "If an app has a Flycast address
|
|
63
|
+
* allocated to it, there will be an AAAA record at `my-app-name.flycast`"
|
|
64
|
+
* (https://fly.io/docs/networking/flycast/).
|
|
65
|
+
*/
|
|
66
|
+
export declare const FLYCAST_SUFFIX = ".flycast";
|
|
67
|
+
/**
|
|
68
|
+
* Machine-metadata key suffix (after the configured prefix) recording the
|
|
69
|
+
* private addresses allocated for a project, so `destroy()` can release exactly
|
|
70
|
+
* the ones it created. See consequence 2 in the module description: a listing on
|
|
71
|
+
* the target app cannot tell you which network an address serves.
|
|
72
|
+
*/
|
|
73
|
+
export declare const PRIVATE_ROUTES_METADATA_SUFFIX = "privateRoutes";
|
|
74
|
+
/**
|
|
75
|
+
* `type` sent to `POST /apps/{app}/ip_assignments` for a Flycast address. This
|
|
76
|
+
* is the value flyctl passes for `fly ips allocate-v6 --private`; the field has
|
|
77
|
+
* no enum in Fly's OpenAPI specification, so it is configurable rather than
|
|
78
|
+
* hardcoded.
|
|
79
|
+
*/
|
|
80
|
+
export declare const DEFAULT_PRIVATE_IP_TYPE = "private_v6";
|
|
81
|
+
/**
|
|
82
|
+
* Renders the DNS name a sandbox dials to reach an app over its Flycast address.
|
|
83
|
+
* @param app - The target Fly app name.
|
|
84
|
+
* @returns `<app>.flycast`.
|
|
85
|
+
*/
|
|
86
|
+
export declare function flycastHost(app: string): string;
|
|
87
|
+
/**
|
|
88
|
+
* Parses the declared cross-network services.
|
|
89
|
+
*
|
|
90
|
+
* Each entry is `<app>:<port>` — the Fly app to allocate a Flycast address on,
|
|
91
|
+
* and the TCP port a sandbox dials it at. The port is not cosmetic: it is what
|
|
92
|
+
* {@link mergeEgressPorts} adds to the Fly network policy, and what
|
|
93
|
+
* {@link assertPrivateRoutesForEnv} checks the injected connection URLs against.
|
|
94
|
+
* @param raw - Comma-separated `app:port` pairs, e.g.
|
|
95
|
+
* `molecule-pg-tenant:5432,molecule-api:3129`.
|
|
96
|
+
* @returns The declared services, deduplicated, in order — or `undefined` when
|
|
97
|
+
* the input is absent or empty, meaning "allocate nothing", which is correct
|
|
98
|
+
* for a control plane that shares one 6PN with its sandboxes.
|
|
99
|
+
* @throws {Error} When an entry is present but unparseable. Dropping one
|
|
100
|
+
* silently would leave a sandbox with a `DATABASE_URL` that nothing routes.
|
|
101
|
+
*/
|
|
102
|
+
export declare function parsePrivateServices(raw: string | undefined): FlyPrivateService[] | undefined;
|
|
103
|
+
/**
|
|
104
|
+
* Adds every declared private-service port to the egress network policy.
|
|
105
|
+
*
|
|
106
|
+
* A Fly network policy is deny-by-default once any rule exists for a direction,
|
|
107
|
+
* so a sandbox told to dial `molecule-pg-tenant.flycast:5432` under a policy
|
|
108
|
+
* allowing only `tcp:3128` would fail to connect with no diagnostic beyond a
|
|
109
|
+
* timeout. Deriving the port from the declaration — rather than asking the
|
|
110
|
+
* operator to keep two lists in step — is what makes that class of outage
|
|
111
|
+
* impossible: FLY-OPERATOR-SETUP.md § 9 can keep saying "never widen this list",
|
|
112
|
+
* because the operator never has to.
|
|
113
|
+
*
|
|
114
|
+
* It is deliberately a UNION and never a replacement, so an operator's list is
|
|
115
|
+
* still exactly what they wrote plus the ports their own declarations require,
|
|
116
|
+
* and the applied policy is logged.
|
|
117
|
+
*
|
|
118
|
+
* Whether the addition is even load-bearing is UNVERIFIED: Fly states "Network
|
|
119
|
+
* policies only apply to traffic directly to and from Machines. They do not
|
|
120
|
+
* affect traffic routed through the Fly Proxy"
|
|
121
|
+
* (https://fly.io/docs/machines/guides-examples/network-policies/), and Flycast
|
|
122
|
+
* traffic IS routed through Fly Proxy — but Fly says that about ingress and
|
|
123
|
+
* never addresses a Machine's egress toward a Flycast address. An inert entry
|
|
124
|
+
* costs a port on the allow list; a missing one costs every database in the
|
|
125
|
+
* fleet, so the port is added.
|
|
126
|
+
* @param ports - The operator's configured ports, or `undefined` when no policy
|
|
127
|
+
* is being applied at all.
|
|
128
|
+
* @param services - The declared private services, or `undefined`.
|
|
129
|
+
* @returns The union, deduplicated — or `undefined` when `ports` is `undefined`.
|
|
130
|
+
* "No policy" is never turned INTO a policy here: applying one that Fly then
|
|
131
|
+
* uses to drop everything else is not something to infer from an unrelated
|
|
132
|
+
* setting.
|
|
133
|
+
*/
|
|
134
|
+
export declare function mergeEgressPorts(ports: FlyNetworkPolicyPort[] | undefined, services: FlyPrivateService[] | undefined): FlyNetworkPolicyPort[] | undefined;
|
|
135
|
+
/**
|
|
136
|
+
* Encodes allocated addresses for storage in Machine metadata.
|
|
137
|
+
* @param routes - Target app name → allocated private address.
|
|
138
|
+
* @returns `app=address` pairs joined by commas. An IPv6 literal contains
|
|
139
|
+
* neither `,` nor `=`, so the encoding is unambiguous.
|
|
140
|
+
*/
|
|
141
|
+
export declare function encodePrivateRoutes(routes: Record<string, string>): string;
|
|
142
|
+
/**
|
|
143
|
+
* Decodes the addresses recorded by {@link encodePrivateRoutes}.
|
|
144
|
+
* @param raw - The metadata value, or `undefined` when the Machine carries none.
|
|
145
|
+
* @returns Target app name → allocated private address. Unparseable entries are
|
|
146
|
+
* skipped: this value only drives cleanup and self-healing, and a malformed
|
|
147
|
+
* entry must not stop either.
|
|
148
|
+
*/
|
|
149
|
+
export declare function decodePrivateRoutes(raw: string | undefined): Record<string, string>;
|
|
150
|
+
/**
|
|
151
|
+
* Refuses to boot a sandbox that is told to dial a Flycast host this provider
|
|
152
|
+
* did not allocate a route for.
|
|
153
|
+
*
|
|
154
|
+
* This is the check that turns the product-breaking failure into a startup
|
|
155
|
+
* error. The control plane bakes `DATABASE_URL` (and the proxy environment) into
|
|
156
|
+
* every sandbox; on Fly those must name `<app>.flycast`, and that name resolves
|
|
157
|
+
* ONLY because this provider allocated a private address into the project's
|
|
158
|
+
* network for that app. Get the two out of step — a `SANDBOX_DB_HOST` pointing
|
|
159
|
+
* at an app nobody declared, a port that does not match the one the policy
|
|
160
|
+
* allows — and the sandbox boots healthy, the scaffolded app cannot connect, and
|
|
161
|
+
* the only symptom is a connection timeout inside someone else's project.
|
|
162
|
+
*
|
|
163
|
+
* It is also the guard on the control-plane cluster: reaching
|
|
164
|
+
* `molecule-pg-control.flycast` from a sandbox would require an operator to have
|
|
165
|
+
* declared it as a private service, which is an explicit act with its own name
|
|
166
|
+
* in the configuration — never something this provider arranges on its own.
|
|
167
|
+
* @param env - The environment the caller is baking into the sandbox.
|
|
168
|
+
* @param services - The declared private services, or `undefined`.
|
|
169
|
+
* @throws {Error} When an environment value names a `.flycast` host with no
|
|
170
|
+
* matching declaration, naming the variable and the fix.
|
|
171
|
+
*/
|
|
172
|
+
export declare function assertPrivateRoutesForEnv(env: Record<string, string> | undefined, services: FlyPrivateService[] | undefined): void;
|
|
173
|
+
//# sourceMappingURL=flycast.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flycast.d.ts","sourceRoot":"","sources":["../src/flycast.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0DG;AAIH,OAAO,KAAK,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAEzE;;;;GAIG;AACH,eAAO,MAAM,cAAc,aAAa,CAAA;AAExC;;;;;GAKG;AACH,eAAO,MAAM,8BAA8B,kBAAkB,CAAA;AAE7D;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,eAAe,CAAA;AAKnD;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,iBAAiB,EAAE,GAAG,SAAS,CAqC7F;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,oBAAoB,EAAE,GAAG,SAAS,EACzC,QAAQ,EAAE,iBAAiB,EAAE,GAAG,SAAS,GACxC,oBAAoB,EAAE,GAAG,SAAS,CAUpC;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAI1E;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAUnF;AA2BD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,yBAAyB,CACvC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,EACvC,QAAQ,EAAE,iBAAiB,EAAE,GAAG,SAAS,GACxC,IAAI,CAwCN"}
|
package/dist/flycast.js
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flycast private routes — the one mechanism Fly documents for reaching an app
|
|
3
|
+
* ACROSS custom 6PN private networks, and the pure logic behind wiring it per
|
|
4
|
+
* project.
|
|
5
|
+
*
|
|
6
|
+
* **The problem this solves.** This provider puts every project's sandbox app on
|
|
7
|
+
* its own custom 6PN, which is what makes tenants unable to reach each other:
|
|
8
|
+
* "Apps on separate 6PNs can never communicate unless explicitly configured to
|
|
9
|
+
* do so" (https://fly.io/docs/networking/custom-private-networks/). The same
|
|
10
|
+
* sentence severs two paths the product REQUIRES — a sandbox reaching its own
|
|
11
|
+
* `mol_<uuid>` database on the tenant Postgres cluster, and a sandbox reaching
|
|
12
|
+
* the control plane's egress proxy. Neither is optional: a scaffolded app
|
|
13
|
+
* connects to `DATABASE_URL` at startup, and the proxy is where the per-project
|
|
14
|
+
* host allowlist is enforced.
|
|
15
|
+
*
|
|
16
|
+
* **The mechanism.** Fly names exactly three ways to cross a 6PN boundary
|
|
17
|
+
* (https://fly.io/docs/networking/custom-private-networks/): a public service
|
|
18
|
+
* IP, `fly-replay`, and a Flycast private address allocated INTO the calling
|
|
19
|
+
* network. Only the third one grants a single directed edge without exposing
|
|
20
|
+
* anything publicly, so it is what this module implements:
|
|
21
|
+
*
|
|
22
|
+
* > "You can use Flycast to expose an app on one private network to another
|
|
23
|
+
* > private network in your organization; the app won't be accessible via
|
|
24
|
+
* > Flycast from its own network. Use the `--network` option to specify the
|
|
25
|
+
* > network from which requests will originate:
|
|
26
|
+
* > `fly ips allocate-v6 --private --network custom-network-name`"
|
|
27
|
+
* > — https://fly.io/docs/networking/flycast/
|
|
28
|
+
*
|
|
29
|
+
* The REST equivalent is `POST /v1/apps/{app}/ip_assignments`, whose
|
|
30
|
+
* `assignIPRequest` schema carries `network`, `org_slug`, `region`,
|
|
31
|
+
* `service_name` and `type` — read directly from
|
|
32
|
+
* https://docs.machines.dev/openapi.json rather than inferred. `type` is a bare
|
|
33
|
+
* `string` there with no enum and no description, so `private_v6` — the value
|
|
34
|
+
* flyctl passes for `allocate-v6 --private`
|
|
35
|
+
* (https://github.com/superfly/flyctl `internal/command/ips/allocate.go`) — is
|
|
36
|
+
* this provider's default and is configurable via
|
|
37
|
+
* {@link FlyioConfig.privateIpAssignmentType}. Treat the literal as UNVERIFIED
|
|
38
|
+
* against an official Fly *doc page*; the flyctl source is a secondary source.
|
|
39
|
+
*
|
|
40
|
+
* **Three consequences that shape the code, none of which can be designed
|
|
41
|
+
* around:**
|
|
42
|
+
*
|
|
43
|
+
* 1. **The allocation lives on the TARGET app, keyed by the CALLER's network.**
|
|
44
|
+
* So the tenant Postgres app accumulates one private address per project
|
|
45
|
+
* network, and destroying a project must release its own. Fly documents no
|
|
46
|
+
* limit on how many private addresses one app may hold, in either direction —
|
|
47
|
+
* see the module `@remarks` in `index.ts`.
|
|
48
|
+
* 2. **`GET /apps/{app}/ip_assignments` does not report which network an address
|
|
49
|
+
* belongs to** (`IPAssignment` is `{created_at, ip, region, service_name,
|
|
50
|
+
* shared}`). A listing therefore cannot tell you whether THIS project's route
|
|
51
|
+
* already exists, so the addresses are recorded in the sandbox Machine's own
|
|
52
|
+
* metadata and read back from there.
|
|
53
|
+
* 3. **The target app needs a services block.** "Services configured in your
|
|
54
|
+
* app's `fly.toml` with an `[http_service]` or `[services]` section"
|
|
55
|
+
* (https://fly.io/docs/networking/flycast/). That is an operator action on
|
|
56
|
+
* the target app, not something this provider can perform.
|
|
57
|
+
*
|
|
58
|
+
* @module
|
|
59
|
+
*/
|
|
60
|
+
import { t } from '@molecule/api-i18n';
|
|
61
|
+
/**
|
|
62
|
+
* DNS suffix Fly serves a Flycast address at: "If an app has a Flycast address
|
|
63
|
+
* allocated to it, there will be an AAAA record at `my-app-name.flycast`"
|
|
64
|
+
* (https://fly.io/docs/networking/flycast/).
|
|
65
|
+
*/
|
|
66
|
+
export const FLYCAST_SUFFIX = '.flycast';
|
|
67
|
+
/**
|
|
68
|
+
* Machine-metadata key suffix (after the configured prefix) recording the
|
|
69
|
+
* private addresses allocated for a project, so `destroy()` can release exactly
|
|
70
|
+
* the ones it created. See consequence 2 in the module description: a listing on
|
|
71
|
+
* the target app cannot tell you which network an address serves.
|
|
72
|
+
*/
|
|
73
|
+
export const PRIVATE_ROUTES_METADATA_SUFFIX = 'privateRoutes';
|
|
74
|
+
/**
|
|
75
|
+
* `type` sent to `POST /apps/{app}/ip_assignments` for a Flycast address. This
|
|
76
|
+
* is the value flyctl passes for `fly ips allocate-v6 --private`; the field has
|
|
77
|
+
* no enum in Fly's OpenAPI specification, so it is configurable rather than
|
|
78
|
+
* hardcoded.
|
|
79
|
+
*/
|
|
80
|
+
export const DEFAULT_PRIVATE_IP_TYPE = 'private_v6';
|
|
81
|
+
/** Fly app names are DNS labels: lowercase alphanumerics and hyphens. */
|
|
82
|
+
const APP_NAME = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
83
|
+
/**
|
|
84
|
+
* Renders the DNS name a sandbox dials to reach an app over its Flycast address.
|
|
85
|
+
* @param app - The target Fly app name.
|
|
86
|
+
* @returns `<app>.flycast`.
|
|
87
|
+
*/
|
|
88
|
+
export function flycastHost(app) {
|
|
89
|
+
return `${app}${FLYCAST_SUFFIX}`;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Parses the declared cross-network services.
|
|
93
|
+
*
|
|
94
|
+
* Each entry is `<app>:<port>` — the Fly app to allocate a Flycast address on,
|
|
95
|
+
* and the TCP port a sandbox dials it at. The port is not cosmetic: it is what
|
|
96
|
+
* {@link mergeEgressPorts} adds to the Fly network policy, and what
|
|
97
|
+
* {@link assertPrivateRoutesForEnv} checks the injected connection URLs against.
|
|
98
|
+
* @param raw - Comma-separated `app:port` pairs, e.g.
|
|
99
|
+
* `molecule-pg-tenant:5432,molecule-api:3129`.
|
|
100
|
+
* @returns The declared services, deduplicated, in order — or `undefined` when
|
|
101
|
+
* the input is absent or empty, meaning "allocate nothing", which is correct
|
|
102
|
+
* for a control plane that shares one 6PN with its sandboxes.
|
|
103
|
+
* @throws {Error} When an entry is present but unparseable. Dropping one
|
|
104
|
+
* silently would leave a sandbox with a `DATABASE_URL` that nothing routes.
|
|
105
|
+
*/
|
|
106
|
+
export function parsePrivateServices(raw) {
|
|
107
|
+
const entries = (raw ?? '')
|
|
108
|
+
.split(',')
|
|
109
|
+
.map((entry) => entry.trim())
|
|
110
|
+
.filter(Boolean);
|
|
111
|
+
if (entries.length === 0)
|
|
112
|
+
return undefined;
|
|
113
|
+
const services = [];
|
|
114
|
+
for (const entry of entries) {
|
|
115
|
+
const index = entry.lastIndexOf(':');
|
|
116
|
+
const app = index > 0 ? entry.slice(0, index) : '';
|
|
117
|
+
const port = Number(entry.slice(index + 1));
|
|
118
|
+
if (!APP_NAME.test(app) ||
|
|
119
|
+
app.length > 63 ||
|
|
120
|
+
!Number.isInteger(port) ||
|
|
121
|
+
port < 1 ||
|
|
122
|
+
port > 65535) {
|
|
123
|
+
throw new Error(t('codeSandbox.flyio.error.badPrivateService', { entry }, {
|
|
124
|
+
defaultValue: `Invalid Fly private service "${entry}" — expected "<fly-app>:<port>", e.g. ` +
|
|
125
|
+
'"molecule-pg-tenant:5432". The app is the Fly app a Flycast address is allocated ' +
|
|
126
|
+
'on; the port is the one a sandbox dials it at.',
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
if (!services.some((service) => service.app === app && service.port === port)) {
|
|
130
|
+
services.push({ app, port });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return services;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Adds every declared private-service port to the egress network policy.
|
|
137
|
+
*
|
|
138
|
+
* A Fly network policy is deny-by-default once any rule exists for a direction,
|
|
139
|
+
* so a sandbox told to dial `molecule-pg-tenant.flycast:5432` under a policy
|
|
140
|
+
* allowing only `tcp:3128` would fail to connect with no diagnostic beyond a
|
|
141
|
+
* timeout. Deriving the port from the declaration — rather than asking the
|
|
142
|
+
* operator to keep two lists in step — is what makes that class of outage
|
|
143
|
+
* impossible: FLY-OPERATOR-SETUP.md § 9 can keep saying "never widen this list",
|
|
144
|
+
* because the operator never has to.
|
|
145
|
+
*
|
|
146
|
+
* It is deliberately a UNION and never a replacement, so an operator's list is
|
|
147
|
+
* still exactly what they wrote plus the ports their own declarations require,
|
|
148
|
+
* and the applied policy is logged.
|
|
149
|
+
*
|
|
150
|
+
* Whether the addition is even load-bearing is UNVERIFIED: Fly states "Network
|
|
151
|
+
* policies only apply to traffic directly to and from Machines. They do not
|
|
152
|
+
* affect traffic routed through the Fly Proxy"
|
|
153
|
+
* (https://fly.io/docs/machines/guides-examples/network-policies/), and Flycast
|
|
154
|
+
* traffic IS routed through Fly Proxy — but Fly says that about ingress and
|
|
155
|
+
* never addresses a Machine's egress toward a Flycast address. An inert entry
|
|
156
|
+
* costs a port on the allow list; a missing one costs every database in the
|
|
157
|
+
* fleet, so the port is added.
|
|
158
|
+
* @param ports - The operator's configured ports, or `undefined` when no policy
|
|
159
|
+
* is being applied at all.
|
|
160
|
+
* @param services - The declared private services, or `undefined`.
|
|
161
|
+
* @returns The union, deduplicated — or `undefined` when `ports` is `undefined`.
|
|
162
|
+
* "No policy" is never turned INTO a policy here: applying one that Fly then
|
|
163
|
+
* uses to drop everything else is not something to infer from an unrelated
|
|
164
|
+
* setting.
|
|
165
|
+
*/
|
|
166
|
+
export function mergeEgressPorts(ports, services) {
|
|
167
|
+
if (!ports)
|
|
168
|
+
return undefined;
|
|
169
|
+
if (!services?.length)
|
|
170
|
+
return ports;
|
|
171
|
+
const merged = [...ports];
|
|
172
|
+
for (const service of services) {
|
|
173
|
+
if (!merged.some((port) => port.protocol === 'tcp' && port.port === service.port)) {
|
|
174
|
+
merged.push({ protocol: 'tcp', port: service.port });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return merged;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Encodes allocated addresses for storage in Machine metadata.
|
|
181
|
+
* @param routes - Target app name → allocated private address.
|
|
182
|
+
* @returns `app=address` pairs joined by commas. An IPv6 literal contains
|
|
183
|
+
* neither `,` nor `=`, so the encoding is unambiguous.
|
|
184
|
+
*/
|
|
185
|
+
export function encodePrivateRoutes(routes) {
|
|
186
|
+
return Object.entries(routes)
|
|
187
|
+
.map(([app, ip]) => `${app}=${ip}`)
|
|
188
|
+
.join(',');
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Decodes the addresses recorded by {@link encodePrivateRoutes}.
|
|
192
|
+
* @param raw - The metadata value, or `undefined` when the Machine carries none.
|
|
193
|
+
* @returns Target app name → allocated private address. Unparseable entries are
|
|
194
|
+
* skipped: this value only drives cleanup and self-healing, and a malformed
|
|
195
|
+
* entry must not stop either.
|
|
196
|
+
*/
|
|
197
|
+
export function decodePrivateRoutes(raw) {
|
|
198
|
+
const routes = {};
|
|
199
|
+
for (const entry of (raw ?? '').split(',')) {
|
|
200
|
+
const index = entry.indexOf('=');
|
|
201
|
+
if (index <= 0)
|
|
202
|
+
continue;
|
|
203
|
+
const app = entry.slice(0, index).trim();
|
|
204
|
+
const ip = entry.slice(index + 1).trim();
|
|
205
|
+
if (app && ip)
|
|
206
|
+
routes[app] = ip;
|
|
207
|
+
}
|
|
208
|
+
return routes;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Extracts the `<app>` and port from a URL that names a Flycast host.
|
|
212
|
+
* @param value - A candidate environment-variable value.
|
|
213
|
+
* @returns The target app and the port the URL dials it at (`undefined` when the
|
|
214
|
+
* URL carries no explicit port), or `null` when the value is not a URL naming
|
|
215
|
+
* a `.flycast` host.
|
|
216
|
+
*/
|
|
217
|
+
function flycastTarget(value) {
|
|
218
|
+
let url;
|
|
219
|
+
try {
|
|
220
|
+
url = new URL(value);
|
|
221
|
+
}
|
|
222
|
+
catch (_error) {
|
|
223
|
+
// Intentionally ignored: "not a URL" is the normal case for an environment
|
|
224
|
+
// value, and a non-URL cannot be the connection string this check is
|
|
225
|
+
// responsible for. There is nothing to report.
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
const host = url.hostname.toLowerCase();
|
|
229
|
+
if (!host.endsWith(FLYCAST_SUFFIX))
|
|
230
|
+
return null;
|
|
231
|
+
const app = host.slice(0, -FLYCAST_SUFFIX.length);
|
|
232
|
+
if (!app)
|
|
233
|
+
return null;
|
|
234
|
+
const port = url.port ? Number(url.port) : undefined;
|
|
235
|
+
return Number.isInteger(port) ? { app, port } : { app };
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Refuses to boot a sandbox that is told to dial a Flycast host this provider
|
|
239
|
+
* did not allocate a route for.
|
|
240
|
+
*
|
|
241
|
+
* This is the check that turns the product-breaking failure into a startup
|
|
242
|
+
* error. The control plane bakes `DATABASE_URL` (and the proxy environment) into
|
|
243
|
+
* every sandbox; on Fly those must name `<app>.flycast`, and that name resolves
|
|
244
|
+
* ONLY because this provider allocated a private address into the project's
|
|
245
|
+
* network for that app. Get the two out of step — a `SANDBOX_DB_HOST` pointing
|
|
246
|
+
* at an app nobody declared, a port that does not match the one the policy
|
|
247
|
+
* allows — and the sandbox boots healthy, the scaffolded app cannot connect, and
|
|
248
|
+
* the only symptom is a connection timeout inside someone else's project.
|
|
249
|
+
*
|
|
250
|
+
* It is also the guard on the control-plane cluster: reaching
|
|
251
|
+
* `molecule-pg-control.flycast` from a sandbox would require an operator to have
|
|
252
|
+
* declared it as a private service, which is an explicit act with its own name
|
|
253
|
+
* in the configuration — never something this provider arranges on its own.
|
|
254
|
+
* @param env - The environment the caller is baking into the sandbox.
|
|
255
|
+
* @param services - The declared private services, or `undefined`.
|
|
256
|
+
* @throws {Error} When an environment value names a `.flycast` host with no
|
|
257
|
+
* matching declaration, naming the variable and the fix.
|
|
258
|
+
*/
|
|
259
|
+
export function assertPrivateRoutesForEnv(env, services) {
|
|
260
|
+
if (!env)
|
|
261
|
+
return;
|
|
262
|
+
const declared = services ?? [];
|
|
263
|
+
for (const [key, value] of Object.entries(env)) {
|
|
264
|
+
if (typeof value !== 'string' || !value.toLowerCase().includes(FLYCAST_SUFFIX))
|
|
265
|
+
continue;
|
|
266
|
+
const target = flycastTarget(value);
|
|
267
|
+
if (!target)
|
|
268
|
+
continue;
|
|
269
|
+
const forApp = declared.filter((service) => service.app === target.app);
|
|
270
|
+
if (forApp.length === 0) {
|
|
271
|
+
throw new Error(t('codeSandbox.flyio.error.undeclaredPrivateRoute', { key, app: target.app }, {
|
|
272
|
+
defaultValue: `${key} points at "${flycastHost(target.app)}", but "${target.app}" is not a ` +
|
|
273
|
+
'declared Fly private service, so no Flycast address is allocated into this ' +
|
|
274
|
+
"project's 6PN and the name will not resolve inside the sandbox. Add " +
|
|
275
|
+
`"${target.app}:<port>" to FLY_SANDBOX_PRIVATE_SERVICES (or config.privateServices).`,
|
|
276
|
+
}));
|
|
277
|
+
}
|
|
278
|
+
if (target.port !== undefined && !forApp.some((service) => service.port === target.port)) {
|
|
279
|
+
throw new Error(t('codeSandbox.flyio.error.privateRoutePortMismatch', { key, app: target.app, port: String(target.port) }, {
|
|
280
|
+
defaultValue: `${key} dials "${flycastHost(target.app)}" on port ${target.port}, but the declared ` +
|
|
281
|
+
`Fly private service for "${target.app}" is on port ` +
|
|
282
|
+
`${forApp.map((service) => service.port).join(', ')}. The sandbox egress policy ` +
|
|
283
|
+
'allows the declared port, so the connection would be dropped. Make the two match.',
|
|
284
|
+
}));
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
package/dist/ids.d.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sandbox identifiers, Fly app naming, and Machine-state mapping.
|
|
3
|
+
*
|
|
4
|
+
* A Fly Machine id is only unique WITHIN its app, and every Machines API call is
|
|
5
|
+
* `/v1/apps/{app_name}/machines/{machine_id}` — so a bare Machine id is not
|
|
6
|
+
* enough to address a Machine. The core interface hands `get(id)` and
|
|
7
|
+
* `destroy(id)` a single opaque string, so this provider's sandbox id is the
|
|
8
|
+
* composite `"<app>:<machineId>"`. Fly app names cannot contain `:` and Machine
|
|
9
|
+
* ids are hex, so the split is unambiguous. Callers must keep treating the id as
|
|
10
|
+
* opaque; nothing outside this module should parse it.
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
import type { FlyMachineState } from './types.js';
|
|
15
|
+
/**
|
|
16
|
+
* Builds the opaque sandbox id addressing one Machine inside one app.
|
|
17
|
+
* @param app - Fly app name.
|
|
18
|
+
* @param machineId - Fly Machine id.
|
|
19
|
+
* @returns The composite sandbox id.
|
|
20
|
+
*/
|
|
21
|
+
export declare function toSandboxId(app: string, machineId: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* Splits a composite sandbox id back into its app and Machine id.
|
|
24
|
+
* @param id - A sandbox id previously produced by {@link toSandboxId}.
|
|
25
|
+
* @returns The app name and Machine id.
|
|
26
|
+
* @throws {Error} When the id is not in `<app>:<machineId>` form — a bare Machine id
|
|
27
|
+
* cannot be addressed, and guessing an app would delete or inspect the wrong
|
|
28
|
+
* tenant's Machine.
|
|
29
|
+
*/
|
|
30
|
+
export declare function parseSandboxId(id: string): {
|
|
31
|
+
app: string;
|
|
32
|
+
machineId: string;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Derives a Fly app name for a project.
|
|
36
|
+
*
|
|
37
|
+
* Fly app names are globally unique DNS labels: lowercase alphanumerics and
|
|
38
|
+
* hyphens, no leading/trailing hyphen, at most 63 characters (they become
|
|
39
|
+
* `<app>.fly.dev`). Project ids are uuids, so `<prefix>-<uuid>` fits with room
|
|
40
|
+
* to spare; longer ids are truncated rather than rejected, and the trailing
|
|
41
|
+
* hyphen that truncation can leave is stripped.
|
|
42
|
+
* @param prefix - Configured app-name prefix.
|
|
43
|
+
* @param projectId - The project id from `SandboxConfig`.
|
|
44
|
+
* @returns A syntactically valid Fly app name.
|
|
45
|
+
* @throws {Error} When `projectId` has no characters usable in a DNS label.
|
|
46
|
+
*/
|
|
47
|
+
export declare function appNameForProject(prefix: string, projectId: string): string;
|
|
48
|
+
/**
|
|
49
|
+
* Maps a Fly Machine state onto the core `Sandbox['status']` union.
|
|
50
|
+
*
|
|
51
|
+
* The core has four statuses and Fly documents seventeen states
|
|
52
|
+
* (https://fly.io/docs/machines/machine-states/), so the mapping is lossy by
|
|
53
|
+
* construction:
|
|
54
|
+
*
|
|
55
|
+
* - `started` → `running`.
|
|
56
|
+
* - `suspended`/`suspending` → `sleeping`. This is the mapping this bond exists
|
|
57
|
+
* for: `sleep()` suspends and `wake()` resumes from the memory snapshot.
|
|
58
|
+
* - In-flight transitions (`creating`, `starting`, `restarting`, `updating`,
|
|
59
|
+
* `replacing`, `migrated`) → `creating`, i.e. "not usable yet, will be".
|
|
60
|
+
* - Everything else — including `failed` and `launch_failed` — → `stopped`.
|
|
61
|
+
* The core union has no error status, so a failed Machine is reported as
|
|
62
|
+
* stopped; `failed` is distinguishable only via {@link isFailedState}.
|
|
63
|
+
*
|
|
64
|
+
* An unrecognized future state falls through to `stopped`, which is the safe
|
|
65
|
+
* default: a caller retries a start rather than assuming a usable sandbox.
|
|
66
|
+
* @param state - Raw Fly Machine state string.
|
|
67
|
+
* @returns The core sandbox status.
|
|
68
|
+
*/
|
|
69
|
+
export declare function mapMachineState(state: FlyMachineState): 'creating' | 'running' | 'sleeping' | 'stopped';
|
|
70
|
+
/**
|
|
71
|
+
* Reports whether a Fly Machine state means the Machine errored, rather than
|
|
72
|
+
* having been stopped on purpose. {@link mapMachineState} flattens both to
|
|
73
|
+
* `stopped` because the core union has no failure status, so this is the only
|
|
74
|
+
* way to tell them apart.
|
|
75
|
+
* @param state - Raw Fly Machine state string.
|
|
76
|
+
* @returns `true` for `failed` and `launch_failed`.
|
|
77
|
+
*/
|
|
78
|
+
export declare function isFailedState(state: FlyMachineState): boolean;
|
|
79
|
+
//# sourceMappingURL=ids.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ids.d.ts","sourceRoot":"","sources":["../src/ids.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAKjD;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAElE;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAgB7E;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAqB3E;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,eAAe,GACrB,UAAU,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,CAiBjD;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAE7D"}
|
package/dist/ids.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sandbox identifiers, Fly app naming, and Machine-state mapping.
|
|
3
|
+
*
|
|
4
|
+
* A Fly Machine id is only unique WITHIN its app, and every Machines API call is
|
|
5
|
+
* `/v1/apps/{app_name}/machines/{machine_id}` — so a bare Machine id is not
|
|
6
|
+
* enough to address a Machine. The core interface hands `get(id)` and
|
|
7
|
+
* `destroy(id)` a single opaque string, so this provider's sandbox id is the
|
|
8
|
+
* composite `"<app>:<machineId>"`. Fly app names cannot contain `:` and Machine
|
|
9
|
+
* ids are hex, so the split is unambiguous. Callers must keep treating the id as
|
|
10
|
+
* opaque; nothing outside this module should parse it.
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
import { t } from '@molecule/api-i18n';
|
|
15
|
+
/** Separator between the app name and the Machine id in a composite sandbox id. */
|
|
16
|
+
const ID_SEPARATOR = ':';
|
|
17
|
+
/**
|
|
18
|
+
* Builds the opaque sandbox id addressing one Machine inside one app.
|
|
19
|
+
* @param app - Fly app name.
|
|
20
|
+
* @param machineId - Fly Machine id.
|
|
21
|
+
* @returns The composite sandbox id.
|
|
22
|
+
*/
|
|
23
|
+
export function toSandboxId(app, machineId) {
|
|
24
|
+
return `${app}${ID_SEPARATOR}${machineId}`;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Splits a composite sandbox id back into its app and Machine id.
|
|
28
|
+
* @param id - A sandbox id previously produced by {@link toSandboxId}.
|
|
29
|
+
* @returns The app name and Machine id.
|
|
30
|
+
* @throws {Error} When the id is not in `<app>:<machineId>` form — a bare Machine id
|
|
31
|
+
* cannot be addressed, and guessing an app would delete or inspect the wrong
|
|
32
|
+
* tenant's Machine.
|
|
33
|
+
*/
|
|
34
|
+
export function parseSandboxId(id) {
|
|
35
|
+
const index = id.indexOf(ID_SEPARATOR);
|
|
36
|
+
if (index <= 0 || index === id.length - 1) {
|
|
37
|
+
throw new Error(t('codeSandbox.flyio.error.badSandboxId', { id }, {
|
|
38
|
+
defaultValue: `Invalid Fly sandbox id "${id}" — expected "<app>:<machineId>". ` +
|
|
39
|
+
'Fly Machine ids are only unique within an app, so a bare Machine id cannot be addressed.',
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
return { app: id.slice(0, index), machineId: id.slice(index + 1) };
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Derives a Fly app name for a project.
|
|
46
|
+
*
|
|
47
|
+
* Fly app names are globally unique DNS labels: lowercase alphanumerics and
|
|
48
|
+
* hyphens, no leading/trailing hyphen, at most 63 characters (they become
|
|
49
|
+
* `<app>.fly.dev`). Project ids are uuids, so `<prefix>-<uuid>` fits with room
|
|
50
|
+
* to spare; longer ids are truncated rather than rejected, and the trailing
|
|
51
|
+
* hyphen that truncation can leave is stripped.
|
|
52
|
+
* @param prefix - Configured app-name prefix.
|
|
53
|
+
* @param projectId - The project id from `SandboxConfig`.
|
|
54
|
+
* @returns A syntactically valid Fly app name.
|
|
55
|
+
* @throws {Error} When `projectId` has no characters usable in a DNS label.
|
|
56
|
+
*/
|
|
57
|
+
export function appNameForProject(prefix, projectId) {
|
|
58
|
+
const sanitize = (value) => value
|
|
59
|
+
.toLowerCase()
|
|
60
|
+
.replace(/[^a-z0-9-]+/g, '-')
|
|
61
|
+
.replace(/-+/g, '-')
|
|
62
|
+
.replace(/^-|-$/g, '');
|
|
63
|
+
const safePrefix = sanitize(prefix) || 'mol-sandbox';
|
|
64
|
+
const safeProject = sanitize(projectId);
|
|
65
|
+
if (!safeProject) {
|
|
66
|
+
throw new Error(t('codeSandbox.flyio.error.badProjectId', { projectId }, {
|
|
67
|
+
defaultValue: `Cannot derive a Fly app name from projectId "${projectId}" — no DNS-safe characters.`,
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
return `${safePrefix}-${safeProject}`.slice(0, 63).replace(/-+$/, '');
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Maps a Fly Machine state onto the core `Sandbox['status']` union.
|
|
74
|
+
*
|
|
75
|
+
* The core has four statuses and Fly documents seventeen states
|
|
76
|
+
* (https://fly.io/docs/machines/machine-states/), so the mapping is lossy by
|
|
77
|
+
* construction:
|
|
78
|
+
*
|
|
79
|
+
* - `started` → `running`.
|
|
80
|
+
* - `suspended`/`suspending` → `sleeping`. This is the mapping this bond exists
|
|
81
|
+
* for: `sleep()` suspends and `wake()` resumes from the memory snapshot.
|
|
82
|
+
* - In-flight transitions (`creating`, `starting`, `restarting`, `updating`,
|
|
83
|
+
* `replacing`, `migrated`) → `creating`, i.e. "not usable yet, will be".
|
|
84
|
+
* - Everything else — including `failed` and `launch_failed` — → `stopped`.
|
|
85
|
+
* The core union has no error status, so a failed Machine is reported as
|
|
86
|
+
* stopped; `failed` is distinguishable only via {@link isFailedState}.
|
|
87
|
+
*
|
|
88
|
+
* An unrecognized future state falls through to `stopped`, which is the safe
|
|
89
|
+
* default: a caller retries a start rather than assuming a usable sandbox.
|
|
90
|
+
* @param state - Raw Fly Machine state string.
|
|
91
|
+
* @returns The core sandbox status.
|
|
92
|
+
*/
|
|
93
|
+
export function mapMachineState(state) {
|
|
94
|
+
switch (state) {
|
|
95
|
+
case 'started':
|
|
96
|
+
return 'running';
|
|
97
|
+
case 'suspended':
|
|
98
|
+
case 'suspending':
|
|
99
|
+
return 'sleeping';
|
|
100
|
+
case 'creating':
|
|
101
|
+
case 'starting':
|
|
102
|
+
case 'restarting':
|
|
103
|
+
case 'updating':
|
|
104
|
+
case 'replacing':
|
|
105
|
+
case 'migrated':
|
|
106
|
+
return 'creating';
|
|
107
|
+
default:
|
|
108
|
+
return 'stopped';
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Reports whether a Fly Machine state means the Machine errored, rather than
|
|
113
|
+
* having been stopped on purpose. {@link mapMachineState} flattens both to
|
|
114
|
+
* `stopped` because the core union has no failure status, so this is the only
|
|
115
|
+
* way to tell them apart.
|
|
116
|
+
* @param state - Raw Fly Machine state string.
|
|
117
|
+
* @returns `true` for `failed` and `launch_failed`.
|
|
118
|
+
*/
|
|
119
|
+
export function isFailedState(state) {
|
|
120
|
+
return state === 'failed' || state === 'launch_failed';
|
|
121
|
+
}
|