@launchfile/macos-dev 0.8.0 → 0.10.0
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/dist/bootstrap.d.ts +7 -0
- package/dist/bootstrap.js +25 -30
- package/dist/env-writer.d.ts +82 -9
- package/dist/env-writer.js +147 -22
- package/dist/https-origin.d.ts +48 -0
- package/dist/https-origin.js +81 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -0
- package/dist/port-allocator.d.ts +1 -0
- package/dist/port-allocator.js +16 -2
- package/dist/provider.d.ts +69 -7
- package/dist/provider.js +231 -55
- package/dist/resources/index.d.ts +2 -1
- package/dist/resources/index.js +1 -0
- package/dist/resources/mysql.d.ts +2 -2
- package/dist/resources/mysql.js +23 -3
- package/dist/resources/postgres.d.ts +2 -2
- package/dist/resources/postgres.js +35 -9
- package/dist/resources/redis.d.ts +2 -2
- package/dist/resources/redis.js +1 -1
- package/dist/resources/sqlite.d.ts +2 -2
- package/dist/resources/sqlite.js +26 -5
- package/dist/resources/types.d.ts +18 -1
- package/dist/resources/uses.d.ts +107 -0
- package/dist/resources/uses.js +217 -0
- package/dist/state.d.ts +36 -0
- package/dist/state.js +36 -3
- package/package.json +2 -2
package/dist/provider.d.ts
CHANGED
|
@@ -32,6 +32,24 @@ export interface LaunchUpOpts {
|
|
|
32
32
|
* name is ambiguous. Relative paths resolve against the current directory.
|
|
33
33
|
*/
|
|
34
34
|
storage?: Record<string, string>;
|
|
35
|
+
/**
|
|
36
|
+
* Orchestrator-supplied publication context (D-58): the public URL of the
|
|
37
|
+
* app's PRIMARY endpoint when routing is owned outside this host — a reverse
|
|
38
|
+
* proxy, tunnel, or edge in front of the processes this provider starts.
|
|
39
|
+
* `$app.*` then resolves from it instead of from `http://localhost:<port>`,
|
|
40
|
+
* this provider's own routing answer, which upstream routing has made no
|
|
41
|
+
* longer the address anyone reaches the app at.
|
|
42
|
+
*
|
|
43
|
+
* Absolute `http`/`https` WHATWG URL, no userinfo, query, or fragment; a
|
|
44
|
+
* malformed value is refused (`InvalidAppUrlError`), never degraded into a
|
|
45
|
+
* guessed or localhost `$app.*` (D-58 rule 3). One URL asserts the primary
|
|
46
|
+
* endpoint only (rule 4) — per-component addresses stay `$components.*`.
|
|
47
|
+
* Persisted in `.launchfile/state.json` so `env` and `bootstrap` resolve
|
|
48
|
+
* what the `up` that set it resolved; a later run that omits it keeps the
|
|
49
|
+
* recorded value, and one that supplies a different value replaces it
|
|
50
|
+
* (D-49), matching `@launchfile/docker`.
|
|
51
|
+
*/
|
|
52
|
+
appUrl?: string;
|
|
35
53
|
}
|
|
36
54
|
/**
|
|
37
55
|
* Components this provider must refuse, mapped to the capabilities it cannot
|
|
@@ -69,14 +87,20 @@ export declare function scheduleWarning(component: string, schedule: string): st
|
|
|
69
87
|
export declare function applyHostCapabilityRefusals(launch: NormalizedLaunch): "ok" | "none-left";
|
|
70
88
|
/**
|
|
71
89
|
* Components this provider must refuse because they require a public HTTPS
|
|
72
|
-
* origin (D-60 rule 5), mapped to the entries
|
|
90
|
+
* origin it cannot supply (D-60 rule 5), mapped to the entries and the reason.
|
|
73
91
|
*
|
|
74
|
-
* This provider has no
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
92
|
+
* This provider has no edge of its own, so it cannot provision the origin. It
|
|
93
|
+
* can accept one through the publication-context channel (`appUrl`, D-58) —
|
|
94
|
+
* which for this type IS the supplied-resource channel (PROVIDERS.md §7) —
|
|
95
|
+
* when the URL's scheme is `https`; then the entry is satisfied and nothing is
|
|
96
|
+
* refused. With no URL, or one whose scheme is not `https`, the entry cannot
|
|
97
|
+
* be satisfied and refusing is what PROVIDERS.md §10 item 5 makes conformant.
|
|
98
|
+
* `supports:` entries are never refused: the component runs, degraded.
|
|
99
|
+
*
|
|
100
|
+
* `appUrl` is the effective publication URL — supplied on this run or recorded
|
|
101
|
+
* by an earlier one — normalized.
|
|
78
102
|
*/
|
|
79
|
-
export declare function refusedHttpsOrigins(launch: NormalizedLaunch): Map<string, string[]>;
|
|
103
|
+
export declare function refusedHttpsOrigins(launch: NormalizedLaunch, appUrl?: string): Map<string, string[]>;
|
|
80
104
|
/**
|
|
81
105
|
* Remove every component whose required `https-origin` this provider cannot
|
|
82
106
|
* satisfy, and say so on stderr. Same shape and same reason as
|
|
@@ -85,7 +109,7 @@ export declare function refusedHttpsOrigins(launch: NormalizedLaunch): Map<strin
|
|
|
85
109
|
* an app that declared it needs HTTPS would run over plain HTTP behind a
|
|
86
110
|
* message nobody acted on.
|
|
87
111
|
*/
|
|
88
|
-
export declare function applyHttpsOriginRefusals(launch: NormalizedLaunch): "ok" | "none-left";
|
|
112
|
+
export declare function applyHttpsOriginRefusals(launch: NormalizedLaunch, appUrl?: string): "ok" | "none-left";
|
|
89
113
|
/**
|
|
90
114
|
* Components this provider must refuse because native TLS was selected on a
|
|
91
115
|
* listener it cannot activate (D-61 rule 5), mapped to the entries.
|
|
@@ -109,6 +133,44 @@ export declare function refusedCertificates(launch: NormalizedLaunch, withOption
|
|
|
109
133
|
* {@link applyHostCapabilityRefusals}: the removal IS the refusal.
|
|
110
134
|
*/
|
|
111
135
|
export declare function applyCertificateRefusals(launch: NormalizedLaunch, withOptional: boolean): "ok" | "none-left";
|
|
136
|
+
/**
|
|
137
|
+
* Components this provider must refuse because a `requires` entry names a
|
|
138
|
+
* resource type it has no provisioner for (PROVIDERS.md §10 item 5, D-64),
|
|
139
|
+
* mapped to the entries. The ordinary case of {@link refusedHttpsOrigins}: a
|
|
140
|
+
* `kafka` this provider cannot stand up is exactly a `postgres` it cannot
|
|
141
|
+
* stand up.
|
|
142
|
+
*
|
|
143
|
+
* This provider has no supplied-resource channel, so nothing can satisfy such
|
|
144
|
+
* an entry from outside — provision or refuse are its only conformant
|
|
145
|
+
* outcomes. The vocabulary is open (L-4): a type with no provisioner is a
|
|
146
|
+
* normal, permanent state, and the defect is starting the component without
|
|
147
|
+
* the resource its file says it needs. Host-capability entries take rule 9's
|
|
148
|
+
* path and `supports:` entries are optional (D-8); neither is graded here.
|
|
149
|
+
*/
|
|
150
|
+
export declare function refusedResourceTypes(launch: NormalizedLaunch): Map<string, string[]>;
|
|
151
|
+
/**
|
|
152
|
+
* Remove every component with a required resource this provider cannot
|
|
153
|
+
* provision, and say so on stderr. Same shape and same reason as
|
|
154
|
+
* {@link applyHostCapabilityRefusals}: the removal IS the refusal.
|
|
155
|
+
*/
|
|
156
|
+
export declare function applyResourceTypeRefusals(launch: NormalizedLaunch): "ok" | "none-left";
|
|
157
|
+
/**
|
|
158
|
+
* Components this provider must refuse because a `requires` entry declares a
|
|
159
|
+
* use its provisioner cannot cover (SPEC.md § Resource uses, D-56 rule 1,
|
|
160
|
+
* D-64), mapped to `<entry>: <use>` lines. Graded after
|
|
161
|
+
* {@link refusedResourceTypes}: a type with no provisioner is refused on the
|
|
162
|
+
* type, so only entries whose type this provider stands up reach here. A
|
|
163
|
+
* token this provider does not recognise is uncovered — no provider can
|
|
164
|
+
* claim to cover a use it does not know. `supports:` entries are optional
|
|
165
|
+
* (D-8) and are not graded here.
|
|
166
|
+
*/
|
|
167
|
+
export declare function refusedResourceUses(launch: NormalizedLaunch): Map<string, string[]>;
|
|
168
|
+
/**
|
|
169
|
+
* Remove every component with a required use this provider cannot cover, and
|
|
170
|
+
* say so on stderr. Same shape as {@link applyResourceTypeRefusals}: the
|
|
171
|
+
* removal IS the refusal.
|
|
172
|
+
*/
|
|
173
|
+
export declare function applyResourceUseRefusals(launch: NormalizedLaunch): "ok" | "none-left";
|
|
112
174
|
export declare function launchUp(opts?: LaunchUpOpts): Promise<void>;
|
|
113
175
|
export declare function launchDown(opts?: {
|
|
114
176
|
destroy?: boolean;
|
package/dist/provider.js
CHANGED
|
@@ -7,11 +7,12 @@
|
|
|
7
7
|
import { accessSync, constants as fsConstants } from "node:fs";
|
|
8
8
|
import { readFile } from "node:fs/promises";
|
|
9
9
|
import { join, resolve as resolvePath } from "node:path";
|
|
10
|
-
import { CERTIFICATE, certificateBindings, indexOperatorStoragePaths, MissingOperatorStoragePathError, readLaunch, resolveSourcePrepareCommand, resolveSourceRunCommand, selectionClosure, UnboundOperatorStorageError, unsuppliedRequiredEnv, } from "@launchfile/sdk";
|
|
10
|
+
import { CERTIFICATE, certificateBindings, indexOperatorStoragePaths, MissingOperatorStoragePathError, normalizeAppUrl, readLaunch, resolveSourcePrepareCommand, resolveSourceRunCommand, selectionClosure, UnboundOperatorStorageError, unsuppliedRequiredEnv, appEndpointReferences, useKeys, } from "@launchfile/sdk";
|
|
11
11
|
import { checkPrereqs } from "./prereqs.js";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
12
|
+
import { HTTPS_ORIGIN, httpsOriginSatisfied, httpsOriginShortfall, } from "./https-origin.js";
|
|
13
|
+
import { loadState, initState, saveState, ensureDirs, withRecordedDbIndexes } from "./state.js";
|
|
14
|
+
import { declaredUses, registerResource, resolveComponentEnv, resolverContextFor, resourceMapFromState, generateSecrets, resolveGenerators, writeEnvFile, } from "./env-writer.js";
|
|
15
|
+
import { allocateDbIndexes, getProvisioner, namedDatabases, uncoveredUses, } from "./resources/index.js";
|
|
15
16
|
import { allocatePorts } from "./port-allocator.js";
|
|
16
17
|
import { getRuntimeInstaller } from "./runtimes/index.js";
|
|
17
18
|
import { detectPackageManager } from "./lockfile-detect.js";
|
|
@@ -121,23 +122,29 @@ export function applyHostCapabilityRefusals(launch) {
|
|
|
121
122
|
launch.components = Object.fromEntries(Object.entries(launch.components).filter(([n]) => !refused.has(n)));
|
|
122
123
|
return Object.keys(launch.components).length === 0 ? "none-left" : "ok";
|
|
123
124
|
}
|
|
124
|
-
/** The backing-service type that declares the app's public HTTPS origin (D-60). */
|
|
125
|
-
const HTTPS_ORIGIN = "https-origin";
|
|
126
125
|
/**
|
|
127
126
|
* Components this provider must refuse because they require a public HTTPS
|
|
128
|
-
* origin (D-60 rule 5), mapped to the entries
|
|
127
|
+
* origin it cannot supply (D-60 rule 5), mapped to the entries and the reason.
|
|
129
128
|
*
|
|
130
|
-
* This provider has no
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
129
|
+
* This provider has no edge of its own, so it cannot provision the origin. It
|
|
130
|
+
* can accept one through the publication-context channel (`appUrl`, D-58) —
|
|
131
|
+
* which for this type IS the supplied-resource channel (PROVIDERS.md §7) —
|
|
132
|
+
* when the URL's scheme is `https`; then the entry is satisfied and nothing is
|
|
133
|
+
* refused. With no URL, or one whose scheme is not `https`, the entry cannot
|
|
134
|
+
* be satisfied and refusing is what PROVIDERS.md §10 item 5 makes conformant.
|
|
135
|
+
* `supports:` entries are never refused: the component runs, degraded.
|
|
136
|
+
*
|
|
137
|
+
* `appUrl` is the effective publication URL — supplied on this run or recorded
|
|
138
|
+
* by an earlier one — normalized.
|
|
134
139
|
*/
|
|
135
|
-
export function refusedHttpsOrigins(launch) {
|
|
140
|
+
export function refusedHttpsOrigins(launch, appUrl) {
|
|
136
141
|
const refused = new Map();
|
|
142
|
+
if (httpsOriginSatisfied(appUrl))
|
|
143
|
+
return refused;
|
|
137
144
|
for (const [name, c] of Object.entries(launch.components)) {
|
|
138
145
|
const entries = (c.requires ?? [])
|
|
139
146
|
.filter((r) => r.type === HTTPS_ORIGIN)
|
|
140
|
-
.map((r) =>
|
|
147
|
+
.map((r) => httpsOriginShortfall(r, appUrl));
|
|
141
148
|
if (entries.length > 0)
|
|
142
149
|
refused.set(name, entries);
|
|
143
150
|
}
|
|
@@ -151,11 +158,11 @@ export function refusedHttpsOrigins(launch) {
|
|
|
151
158
|
* an app that declared it needs HTTPS would run over plain HTTP behind a
|
|
152
159
|
* message nobody acted on.
|
|
153
160
|
*/
|
|
154
|
-
export function applyHttpsOriginRefusals(launch) {
|
|
155
|
-
const refused = refusedHttpsOrigins(launch);
|
|
161
|
+
export function applyHttpsOriginRefusals(launch, appUrl) {
|
|
162
|
+
const refused = refusedHttpsOrigins(launch, appUrl);
|
|
156
163
|
for (const [name, entries] of refused) {
|
|
157
164
|
console.error(` Refused: ${name} requires a public HTTPS origin this provider cannot supply ` +
|
|
158
|
-
`(${entries.join("; ")}) —
|
|
165
|
+
`(${entries.join("; ")}) — component not started`);
|
|
159
166
|
}
|
|
160
167
|
if (refused.size === 0)
|
|
161
168
|
return "ok";
|
|
@@ -211,8 +218,101 @@ export function applyCertificateRefusals(launch, withOptional) {
|
|
|
211
218
|
launch.components = Object.fromEntries(Object.entries(launch.components).filter(([n]) => !refused.has(n)));
|
|
212
219
|
return Object.keys(launch.components).length === 0 ? "none-left" : "ok";
|
|
213
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* Components this provider must refuse because a `requires` entry names a
|
|
223
|
+
* resource type it has no provisioner for (PROVIDERS.md §10 item 5, D-64),
|
|
224
|
+
* mapped to the entries. The ordinary case of {@link refusedHttpsOrigins}: a
|
|
225
|
+
* `kafka` this provider cannot stand up is exactly a `postgres` it cannot
|
|
226
|
+
* stand up.
|
|
227
|
+
*
|
|
228
|
+
* This provider has no supplied-resource channel, so nothing can satisfy such
|
|
229
|
+
* an entry from outside — provision or refuse are its only conformant
|
|
230
|
+
* outcomes. The vocabulary is open (L-4): a type with no provisioner is a
|
|
231
|
+
* normal, permanent state, and the defect is starting the component without
|
|
232
|
+
* the resource its file says it needs. Host-capability entries take rule 9's
|
|
233
|
+
* path and `supports:` entries are optional (D-8); neither is graded here.
|
|
234
|
+
*/
|
|
235
|
+
export function refusedResourceTypes(launch) {
|
|
236
|
+
const refused = new Map();
|
|
237
|
+
for (const [name, component] of Object.entries(launch.components)) {
|
|
238
|
+
const entries = (component.requires ?? [])
|
|
239
|
+
.filter((r) => !r.host && r.type !== HTTPS_ORIGIN && !getProvisioner(r.type))
|
|
240
|
+
.map((r) => (r.name === undefined ? r.type : `${r.name} (type "${r.type}")`));
|
|
241
|
+
if (entries.length > 0)
|
|
242
|
+
refused.set(name, entries);
|
|
243
|
+
}
|
|
244
|
+
return refused;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Remove every component with a required resource this provider cannot
|
|
248
|
+
* provision, and say so on stderr. Same shape and same reason as
|
|
249
|
+
* {@link applyHostCapabilityRefusals}: the removal IS the refusal.
|
|
250
|
+
*/
|
|
251
|
+
export function applyResourceTypeRefusals(launch) {
|
|
252
|
+
const refused = refusedResourceTypes(launch);
|
|
253
|
+
for (const [name, entries] of refused) {
|
|
254
|
+
const noun = entries.length === 1 ? "a resource" : "resources";
|
|
255
|
+
console.error(` Refused: ${name} requires ${noun} this provider cannot provision ` +
|
|
256
|
+
`(${entries.join("; ")}) — it has no provisioner for the type and no channel to ` +
|
|
257
|
+
"receive one through; use a provider that provisions it — component not started");
|
|
258
|
+
}
|
|
259
|
+
if (refused.size === 0)
|
|
260
|
+
return "ok";
|
|
261
|
+
launch.components = Object.fromEntries(Object.entries(launch.components).filter(([n]) => !refused.has(n)));
|
|
262
|
+
return Object.keys(launch.components).length === 0 ? "none-left" : "ok";
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Components this provider must refuse because a `requires` entry declares a
|
|
266
|
+
* use its provisioner cannot cover (SPEC.md § Resource uses, D-56 rule 1,
|
|
267
|
+
* D-64), mapped to `<entry>: <use>` lines. Graded after
|
|
268
|
+
* {@link refusedResourceTypes}: a type with no provisioner is refused on the
|
|
269
|
+
* type, so only entries whose type this provider stands up reach here. A
|
|
270
|
+
* token this provider does not recognise is uncovered — no provider can
|
|
271
|
+
* claim to cover a use it does not know. `supports:` entries are optional
|
|
272
|
+
* (D-8) and are not graded here.
|
|
273
|
+
*/
|
|
274
|
+
export function refusedResourceUses(launch) {
|
|
275
|
+
const refused = new Map();
|
|
276
|
+
for (const [name, component] of Object.entries(launch.components)) {
|
|
277
|
+
const entries = [];
|
|
278
|
+
for (const req of component.requires ?? []) {
|
|
279
|
+
if (req.host || req.type === HTTPS_ORIGIN || !req.uses || !getProvisioner(req.type))
|
|
280
|
+
continue;
|
|
281
|
+
const resourceName = req.name ?? req.type;
|
|
282
|
+
for (const use of uncoveredUses(req.type, useKeys(req.uses))) {
|
|
283
|
+
entries.push(`${resourceName}: ${use}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (entries.length > 0)
|
|
287
|
+
refused.set(name, entries);
|
|
288
|
+
}
|
|
289
|
+
return refused;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Remove every component with a required use this provider cannot cover, and
|
|
293
|
+
* say so on stderr. Same shape as {@link applyResourceTypeRefusals}: the
|
|
294
|
+
* removal IS the refusal.
|
|
295
|
+
*/
|
|
296
|
+
export function applyResourceUseRefusals(launch) {
|
|
297
|
+
const refused = refusedResourceUses(launch);
|
|
298
|
+
for (const [name, entries] of refused) {
|
|
299
|
+
const noun = entries.length === 1 ? "a use" : "uses";
|
|
300
|
+
console.error(` Refused: ${name} requires ${noun} of a resource this provider cannot cover ` +
|
|
301
|
+
`(${entries.join("; ")}) — a provider covers every declared use or refuses; declare only ` +
|
|
302
|
+
"what the app uses, or use a provider that covers it — component not started");
|
|
303
|
+
}
|
|
304
|
+
if (refused.size === 0)
|
|
305
|
+
return "ok";
|
|
306
|
+
launch.components = Object.fromEntries(Object.entries(launch.components).filter(([n]) => !refused.has(n)));
|
|
307
|
+
return Object.keys(launch.components).length === 0 ? "none-left" : "ok";
|
|
308
|
+
}
|
|
214
309
|
export async function launchUp(opts = {}) {
|
|
215
310
|
const projectDir = opts.projectDir ?? process.cwd();
|
|
311
|
+
// Publication context (D-58): validated and normalized before anything is
|
|
312
|
+
// checked, provisioned, or started — a malformed URL is an expected refusal
|
|
313
|
+
// (InvalidAppUrlError), never a degraded $app.* or a silent fallback to this
|
|
314
|
+
// provider's own localhost answer.
|
|
315
|
+
const suppliedAppUrl = opts.appUrl === undefined ? undefined : normalizeAppUrl(opts.appUrl);
|
|
216
316
|
// 1. Check prerequisites
|
|
217
317
|
const prereqs = await checkPrereqs();
|
|
218
318
|
if (!prereqs.ok) {
|
|
@@ -267,11 +367,25 @@ export async function launchUp(opts = {}) {
|
|
|
267
367
|
console.error("Every selected component requires a host capability this provider cannot grant.");
|
|
268
368
|
process.exit(1);
|
|
269
369
|
}
|
|
270
|
-
// 2a-bis. A required `https-origin`
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
|
|
370
|
+
// 2a-bis. A required `https-origin` (D-60 rule 5, PROVIDERS.md §10 item 5)
|
|
371
|
+
// is satisfied by the publication context when its scheme is https —
|
|
372
|
+
// supplied on this run, or recorded by an earlier one and preserved on
|
|
373
|
+
// omission, which is why state is loaded here rather than after the
|
|
374
|
+
// refusals (loading writes nothing; the first write is `ensureDirs`).
|
|
375
|
+
// Otherwise it is refused in the same way as a host capability: starting
|
|
376
|
+
// the component anyway is the silent success the type removes.
|
|
377
|
+
let state = await loadState(projectDir);
|
|
378
|
+
if (!state) {
|
|
379
|
+
state = initState(launch.name, launchfileContent);
|
|
380
|
+
}
|
|
381
|
+
// Publication context (D-58), the same preservation rule the docker provider
|
|
382
|
+
// applies: a supplied value replaces the recorded one — the derived $app.*
|
|
383
|
+
// env recomputes below from it (D-49) — while omission keeps what is
|
|
384
|
+
// recorded, so a later plain `up` cannot silently flip a proxied deployment
|
|
385
|
+
// back to localhost.
|
|
386
|
+
if (suppliedAppUrl !== undefined)
|
|
387
|
+
state.appUrl = suppliedAppUrl;
|
|
388
|
+
if (applyHttpsOriginRefusals(launch, state.appUrl) === "none-left") {
|
|
275
389
|
console.error("Every selected component requires a public HTTPS origin this provider cannot supply.");
|
|
276
390
|
process.exit(1);
|
|
277
391
|
}
|
|
@@ -283,6 +397,24 @@ export async function launchUp(opts = {}) {
|
|
|
283
397
|
console.error("Every selected component selected native TLS this provider cannot activate.");
|
|
284
398
|
process.exit(1);
|
|
285
399
|
}
|
|
400
|
+
// 2a-quater. A required resource type this provider has no provisioner for
|
|
401
|
+
// is refused the same way (D-64, PROVIDERS.md §10 item 5). Decided after
|
|
402
|
+
// the selector narrowed `launch.components`, so an unsatisfiable entry on a
|
|
403
|
+
// component outside the start-set blocks nothing (§10 item 5's selector
|
|
404
|
+
// rule), and before anything is provisioned, installed, wired or started.
|
|
405
|
+
if (applyResourceTypeRefusals(launch) === "none-left") {
|
|
406
|
+
console.error("Every selected component requires a resource this provider cannot provision.");
|
|
407
|
+
process.exit(1);
|
|
408
|
+
}
|
|
409
|
+
// 2a-quinquies. A required use this provider's provisioner cannot cover is
|
|
410
|
+
// refused the same way (SPEC.md § Resource uses, D-56 rule 1): a provider
|
|
411
|
+
// covers every declared use or refuses, and never hands over less than the
|
|
412
|
+
// entry declares. Graded here, after the type refusal, for the same
|
|
413
|
+
// selector and before-anything-happens reasons.
|
|
414
|
+
if (applyResourceUseRefusals(launch) === "none-left") {
|
|
415
|
+
console.error("Every selected component requires a use of a resource this provider cannot cover.");
|
|
416
|
+
process.exit(1);
|
|
417
|
+
}
|
|
286
418
|
// An optional capability is not refused — the component runs, degraded.
|
|
287
419
|
for (const [name, c] of Object.entries(launch.components)) {
|
|
288
420
|
for (const sup of c.supports ?? []) {
|
|
@@ -291,11 +423,16 @@ export async function launchUp(opts = {}) {
|
|
|
291
423
|
`${capability}=${String(value)} not granted — running degraded`);
|
|
292
424
|
}
|
|
293
425
|
}
|
|
426
|
+
// The optional mood of `https-origin` (D-60 rule 6): satisfied, it is
|
|
427
|
+
// wired like any other resource at step 8; unsatisfied, the component
|
|
428
|
+
// still runs, its set_env is absent, and the shortfall is named.
|
|
429
|
+
if (httpsOriginSatisfied(state.appUrl))
|
|
430
|
+
continue;
|
|
294
431
|
for (const sup of c.supports ?? []) {
|
|
295
432
|
if (sup.type !== HTTPS_ORIGIN)
|
|
296
433
|
continue;
|
|
297
|
-
console.warn(` Warning: ${name}: optional public HTTPS origin ` +
|
|
298
|
-
|
|
434
|
+
console.warn(` Warning: ${name}: optional public HTTPS origin not satisfied ` +
|
|
435
|
+
`(${httpsOriginShortfall(sup, state.appUrl)}) — running degraded`);
|
|
299
436
|
}
|
|
300
437
|
}
|
|
301
438
|
const componentNames = Object.keys(launch.components);
|
|
@@ -431,39 +568,59 @@ export async function launchUp(opts = {}) {
|
|
|
431
568
|
for (const key of storageIndex.unusedKeys(usedStorageKeys)) {
|
|
432
569
|
console.warn(` Warning: --storage ${key} matches no \`content: operator\` volume — ignored`);
|
|
433
570
|
}
|
|
434
|
-
// 3.
|
|
435
|
-
let state = await loadState(projectDir);
|
|
436
|
-
if (!state) {
|
|
437
|
-
state = initState(launch.name, launchfileContent);
|
|
438
|
-
}
|
|
571
|
+
// 3. State: loaded at step 2a-bis, where the https-origin refusal reads it.
|
|
439
572
|
// 4. Ensure directories
|
|
440
573
|
await ensureDirs(projectDir);
|
|
441
574
|
// 5. Generate secrets
|
|
442
575
|
state.secrets = await generateSecrets(launch.secrets, state.secrets);
|
|
443
|
-
// 6. Provision required resources
|
|
576
|
+
// 6. Provision required resources. Each declared use's `<use>.<property>`
|
|
577
|
+
// keys join the provisioner's instance vocabulary in the map; each redis
|
|
578
|
+
// `db` use key gets one numbered database from one app-wide allocation
|
|
579
|
+
// that is stable between runs and identical on both reference providers
|
|
580
|
+
// (`allocateDbIndexes`), and each named `database` use is one more
|
|
581
|
+
// database the SQL provisioner creates. A resource is provisioned once per
|
|
582
|
+
// name and registered from the pooled uses of every same-name entry (D-24:
|
|
583
|
+
// they describe one resource) — the set the resolver context below treats
|
|
584
|
+
// strictly — so the first entry seen cannot hide keys a later one declares.
|
|
444
585
|
const resourceMap = {};
|
|
586
|
+
const uses = declaredUses(launch);
|
|
587
|
+
const dbIndexes = allocateDbIndexes(launch);
|
|
588
|
+
const provisionOpts = (resourceName) => ({
|
|
589
|
+
appName: launch.name,
|
|
590
|
+
projectDir,
|
|
591
|
+
databases: namedDatabases(uses[resourceName] ?? []),
|
|
592
|
+
});
|
|
445
593
|
for (const [_compName, component] of Object.entries(launch.components)) {
|
|
446
594
|
for (const req of component.requires ?? []) {
|
|
447
595
|
if (req.host)
|
|
448
596
|
continue; // capability, not a backing service (D-44)
|
|
597
|
+
// Satisfied through the publication context, not provisioned; wired
|
|
598
|
+
// at step 8 once $app.* is known. A refused one is already gone.
|
|
599
|
+
if (req.type === HTTPS_ORIGIN)
|
|
600
|
+
continue;
|
|
449
601
|
const resourceName = req.name ?? req.type;
|
|
450
602
|
if (resourceMap[resourceName])
|
|
451
603
|
continue; // Already provisioned
|
|
452
604
|
const provisioner = getProvisioner(req.type);
|
|
453
605
|
if (!provisioner) {
|
|
454
|
-
|
|
455
|
-
|
|
606
|
+
// Step 2a-quater removed every component with such an entry, so
|
|
607
|
+
// reaching here is a bug in this file, not a launch outcome.
|
|
608
|
+
throw new Error(`no provisioner for resource type "${req.type}" on ${_compName} — the component must be refused first`);
|
|
456
609
|
}
|
|
610
|
+
const indexes = dbIndexes[resourceName] ?? {};
|
|
457
611
|
if (opts.dryRun) {
|
|
458
612
|
console.log(` [dry-run] Would provision ${req.type} as "${resourceName}"`);
|
|
459
|
-
resourceMap[resourceName] = { url: "", host: "localhost", port: 0 }
|
|
613
|
+
resourceMap[resourceName] = registerResource(req.type, resourceName, uses, { url: "", host: "localhost", port: 0 }, // placeholder for dedup
|
|
614
|
+
indexes);
|
|
460
615
|
continue;
|
|
461
616
|
}
|
|
462
617
|
process.stdout.write(` \u2193 Provisioning ${req.type}...`);
|
|
463
618
|
const existing = state.resources[resourceName];
|
|
464
|
-
const result = await provisioner.provision(req,
|
|
465
|
-
resourceMap[resourceName] = result.properties;
|
|
466
|
-
state
|
|
619
|
+
const result = await provisioner.provision(req, provisionOpts(resourceName), existing);
|
|
620
|
+
resourceMap[resourceName] = registerResource(req.type, resourceName, uses, result.properties, indexes);
|
|
621
|
+
// The indexes ride along in state so `env` answers `db.*` with the
|
|
622
|
+
// databases the running app was given.
|
|
623
|
+
state.resources[resourceName] = withRecordedDbIndexes(result.state, indexes);
|
|
467
624
|
console.log(" done");
|
|
468
625
|
}
|
|
469
626
|
// Optional supports resources
|
|
@@ -471,23 +628,37 @@ export async function launchUp(opts = {}) {
|
|
|
471
628
|
for (const sup of component.supports ?? []) {
|
|
472
629
|
if (sup.host)
|
|
473
630
|
continue; // capability, not a backing service (D-44)
|
|
631
|
+
if (sup.type === HTTPS_ORIGIN)
|
|
632
|
+
continue; // publication context, step 8
|
|
474
633
|
const resourceName = sup.name ?? sup.type;
|
|
475
634
|
if (resourceMap[resourceName])
|
|
476
635
|
continue;
|
|
477
636
|
const provisioner = getProvisioner(sup.type);
|
|
478
637
|
if (!provisioner)
|
|
479
638
|
continue;
|
|
639
|
+
// An optional entry declaring a use this provider cannot cover is
|
|
640
|
+
// left unfulfilled — bindings absent, said out loud — never refused
|
|
641
|
+
// (D-8). The same shortfall on `requires:` refused the component
|
|
642
|
+
// at step 2a-quinquies.
|
|
643
|
+
const uncovered = sup.uses ? uncoveredUses(sup.type, useKeys(sup.uses)) : [];
|
|
644
|
+
if (uncovered.length > 0) {
|
|
645
|
+
console.warn(` Warning: ${_compName}: optional resource ${resourceName} not satisfied — this provider ` +
|
|
646
|
+
`does not cover ${uncovered.length === 1 ? "a use" : "uses"} it declares ` +
|
|
647
|
+
`(${uncovered.join(", ")}); running degraded`);
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
const indexes = dbIndexes[resourceName] ?? {};
|
|
480
651
|
if (opts.dryRun) {
|
|
481
652
|
console.log(` [dry-run] Would provision optional ${sup.type} as "${resourceName}"`);
|
|
482
|
-
resourceMap[resourceName] = { url: "", host: "localhost", port: 0 };
|
|
653
|
+
resourceMap[resourceName] = registerResource(sup.type, resourceName, uses, { url: "", host: "localhost", port: 0 }, indexes);
|
|
483
654
|
continue;
|
|
484
655
|
}
|
|
485
656
|
try {
|
|
486
657
|
process.stdout.write(` \u2193 Provisioning ${sup.type} (optional)...`);
|
|
487
658
|
const existing = state.resources[resourceName];
|
|
488
|
-
const result = await provisioner.provision(sup,
|
|
489
|
-
resourceMap[resourceName] = result.properties;
|
|
490
|
-
state.resources[resourceName] = result.state;
|
|
659
|
+
const result = await provisioner.provision(sup, provisionOpts(resourceName), existing);
|
|
660
|
+
resourceMap[resourceName] = registerResource(sup.type, resourceName, uses, result.properties, indexes);
|
|
661
|
+
state.resources[resourceName] = withRecordedDbIndexes(result.state, indexes);
|
|
491
662
|
console.log(" done");
|
|
492
663
|
}
|
|
493
664
|
catch {
|
|
@@ -499,9 +670,19 @@ export async function launchUp(opts = {}) {
|
|
|
499
670
|
// 7. Allocate ports
|
|
500
671
|
const componentPorts = await allocatePorts(launch.components, launch.name, state.ports);
|
|
501
672
|
state.ports = componentPorts;
|
|
502
|
-
// 8. Build resolver context (including $app.* properties from D-33)
|
|
503
|
-
|
|
504
|
-
|
|
673
|
+
// 8. Build resolver context (including $app.* properties from D-33). A
|
|
674
|
+
// satisfied `https-origin` registers `url` — the same string as `$app.url`
|
|
675
|
+
// (D-60 rule 4) — so its set_env resolves like any provisioned resource's.
|
|
676
|
+
const context = resolverContextFor(launch, resourceMap, state);
|
|
677
|
+
// `$app.endpoints.<name>.*` resolves "" here (D-63 rule 4, #294). Said
|
|
678
|
+
// once per `up`, and only when the file asks, so the empty value is not a
|
|
679
|
+
// silent one (PROVIDERS.md §10 item 8).
|
|
680
|
+
const endpointRefs = appEndpointReferences(launch);
|
|
681
|
+
if (endpointRefs.length > 0) {
|
|
682
|
+
const names = [...new Set(endpointRefs.map((ref) => ref.path[2] ?? "<none>"))];
|
|
683
|
+
console.warn(` ! $app.endpoints.* resolves "" on this provider (${names.map((n) => `$app.endpoints.${n}.*`).join(", ")}) — ` +
|
|
684
|
+
"it publishes one port per component, not one per endpoint (#294)");
|
|
685
|
+
}
|
|
505
686
|
// 9. Install runtimes
|
|
506
687
|
for (const [name, component] of Object.entries(launch.components)) {
|
|
507
688
|
if (!component.runtime) {
|
|
@@ -568,7 +749,7 @@ export async function launchUp(opts = {}) {
|
|
|
568
749
|
}
|
|
569
750
|
else {
|
|
570
751
|
const { mkdir } = await import("node:fs/promises");
|
|
571
|
-
await mkdir(join(projectDir, ".launchfile", "env"), { recursive: true });
|
|
752
|
+
await mkdir(join(projectDir, ".launchfile", "env"), { recursive: true, mode: 0o700 });
|
|
572
753
|
await writeEnvFile(join(projectDir, ".launchfile", "env", `${name}.env`), env);
|
|
573
754
|
console.log(` \u2193 Wiring ${name} environment... done (${Object.keys(env).length} vars)`);
|
|
574
755
|
}
|
|
@@ -704,7 +885,7 @@ export async function launchDown(opts = {}) {
|
|
|
704
885
|
const provisioner = getProvisioner(resourceState.type);
|
|
705
886
|
if (provisioner) {
|
|
706
887
|
console.log(` Destroying ${resourceState.type} (${name})...`);
|
|
707
|
-
await provisioner.destroy(resourceState);
|
|
888
|
+
await provisioner.destroy(resourceState, { projectDir });
|
|
708
889
|
}
|
|
709
890
|
}
|
|
710
891
|
// Clean up state
|
|
@@ -755,17 +936,12 @@ export async function launchEnv(opts = {}) {
|
|
|
755
936
|
console.log("No active launch state. Run `launch up` first.");
|
|
756
937
|
return;
|
|
757
938
|
}
|
|
758
|
-
//
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
resourceMap[name] = result.properties;
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
const appProperties = computeAppProperties(launch, state.ports);
|
|
768
|
-
const context = buildResolverContext(resourceMap, state.ports, state.secrets, appProperties);
|
|
939
|
+
// `env` reports what the running app has: the resources registered as `up`
|
|
940
|
+
// registered them — declared uses included, a redis `db` use reading back
|
|
941
|
+
// the index `up` recorded — and `$app.*` from the publication context the
|
|
942
|
+
// last `up` recorded (D-58), not this provider's localhost answer.
|
|
943
|
+
const resourceMap = await resourceMapFromState(launch, state, projectDir);
|
|
944
|
+
const context = resolverContextFor(launch, resourceMap, state);
|
|
769
945
|
// `env` reports what the running app has, so it reads minted generator
|
|
770
946
|
// values from the same store `up` persists to (D-49). A value can still be
|
|
771
947
|
// minted here — a generator declared after the last `up` — and then it is
|
|
@@ -6,5 +6,6 @@
|
|
|
6
6
|
import type { ResourceProvisioner, ShellRunner } from "./types.js";
|
|
7
7
|
export declare function getProvisioner(type: string, deps?: Partial<ShellRunner>): ResourceProvisioner | undefined;
|
|
8
8
|
export declare function supportedResourceTypes(): string[];
|
|
9
|
-
export type
|
|
9
|
+
export { allocateDbIndexes, coverUse, coveredUses, type DbIndexes, namedDatabase, namedDatabases, uncoveredUses, withCoveredUses, withDatabasePath, } from "./uses.js";
|
|
10
|
+
export type { ResourceProvisioner, ResourceProperties, ProvisionOpts, DestroyOpts, ShellRunner, } from "./types.js";
|
|
10
11
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/resources/index.js
CHANGED
|
@@ -31,4 +31,5 @@ export function getProvisioner(type, deps) {
|
|
|
31
31
|
export function supportedResourceTypes() {
|
|
32
32
|
return Object.keys(factories);
|
|
33
33
|
}
|
|
34
|
+
export { allocateDbIndexes, coverUse, coveredUses, namedDatabase, namedDatabases, uncoveredUses, withCoveredUses, withDatabasePath, } from "./uses.js";
|
|
34
35
|
//# sourceMappingURL=index.js.map
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type { NormalizedRequirement } from "@launchfile/sdk";
|
|
5
5
|
import type { ResourceState } from "../state.js";
|
|
6
|
-
import type { ProvisionOpts, ResourceProperties, ResourceProvisioner, ShellRunner } from "./types.js";
|
|
6
|
+
import type { DestroyOpts, ProvisionOpts, ResourceProperties, ResourceProvisioner, ShellRunner } from "./types.js";
|
|
7
7
|
export declare class MysqlProvisioner implements ResourceProvisioner {
|
|
8
8
|
#private;
|
|
9
9
|
readonly type = "mysql";
|
|
@@ -13,6 +13,6 @@ export declare class MysqlProvisioner implements ResourceProvisioner {
|
|
|
13
13
|
properties: ResourceProperties;
|
|
14
14
|
state: ResourceState;
|
|
15
15
|
}>;
|
|
16
|
-
destroy(state: ResourceState): Promise<void>;
|
|
16
|
+
destroy(state: ResourceState, _opts: DestroyOpts): Promise<void>;
|
|
17
17
|
}
|
|
18
18
|
//# sourceMappingURL=mysql.d.ts.map
|
package/dist/resources/mysql.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { shell, shellOk } from "../shell.js";
|
|
5
5
|
import { generatePassword } from "../secret-generator.js";
|
|
6
6
|
import { assertSafeIdentifier, assertSafePassword, SAFE_IDENTIFIER, } from "./identifiers.js";
|
|
7
|
+
import { namedDatabase } from "./uses.js";
|
|
7
8
|
const DEFAULT_PORT = 3306;
|
|
8
9
|
const DEFAULT_HOST = "localhost";
|
|
9
10
|
/** Connection flags shared by every mysql invocation, as argv elements. */
|
|
@@ -65,6 +66,22 @@ export class MysqlProvisioner {
|
|
|
65
66
|
"-e",
|
|
66
67
|
`GRANT ALL PRIVILEGES ON \`${dbName}\`.* TO '${user}'@'${DEFAULT_HOST}';`,
|
|
67
68
|
], { allowFailure: true });
|
|
69
|
+
// Named `database` uses (SPEC.md § Resource uses): one more database per
|
|
70
|
+
// name, `<instance>_<name>`, created and granted the same way as the
|
|
71
|
+
// app's own. Security: a use name is schema-validated
|
|
72
|
+
// (^[a-z][a-z0-9-]*$) and the hyphens become underscores, so the
|
|
73
|
+
// identifier check cannot fail on a name that reached here through the
|
|
74
|
+
// parser; it guards the SQL below all the same.
|
|
75
|
+
const databases = (opts.databases ?? []).map((name) => namedDatabase(dbName, name));
|
|
76
|
+
for (const database of databases) {
|
|
77
|
+
assertSafeIdentifier(database, "database name");
|
|
78
|
+
await this.#shell("mysql", [...mysqlArgs(), "-e", `CREATE DATABASE IF NOT EXISTS \`${database}\`;`], { allowFailure: true });
|
|
79
|
+
await this.#shell("mysql", [
|
|
80
|
+
...mysqlArgs(),
|
|
81
|
+
"-e",
|
|
82
|
+
`GRANT ALL PRIVILEGES ON \`${database}\`.* TO '${user}'@'${DEFAULT_HOST}';`,
|
|
83
|
+
], { allowFailure: true });
|
|
84
|
+
}
|
|
68
85
|
const url = `mysql://${user}:${password}@${DEFAULT_HOST}:${port}/${dbName}`;
|
|
69
86
|
const properties = {
|
|
70
87
|
url,
|
|
@@ -82,13 +99,16 @@ export class MysqlProvisioner {
|
|
|
82
99
|
dbName,
|
|
83
100
|
user,
|
|
84
101
|
password,
|
|
102
|
+
...(databases.length > 0 ? { databases } : {}),
|
|
85
103
|
};
|
|
86
104
|
return { properties, state };
|
|
87
105
|
}
|
|
88
|
-
async destroy(state) {
|
|
106
|
+
async destroy(state, _opts) {
|
|
89
107
|
// Security: state values come from disk (state.json) — validate before SQL interpolation
|
|
90
|
-
|
|
91
|
-
|
|
108
|
+
for (const database of [state.dbName, ...(state.databases ?? [])]) {
|
|
109
|
+
if (!database || !SAFE_IDENTIFIER.test(database))
|
|
110
|
+
continue;
|
|
111
|
+
await this.#shell("mysql", [...mysqlArgs(), "-e", `DROP DATABASE IF EXISTS \`${database}\`;`], { allowFailure: true });
|
|
92
112
|
}
|
|
93
113
|
if (state.user && SAFE_IDENTIFIER.test(state.user)) {
|
|
94
114
|
await this.#shell("mysql", [
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import type { NormalizedRequirement } from "@launchfile/sdk";
|
|
11
11
|
import type { ResourceState } from "../state.js";
|
|
12
|
-
import type { ProvisionOpts, ResourceProperties, ResourceProvisioner, ShellRunner } from "./types.js";
|
|
12
|
+
import type { DestroyOpts, ProvisionOpts, ResourceProperties, ResourceProvisioner, ShellRunner } from "./types.js";
|
|
13
13
|
export type { ShellRunner };
|
|
14
14
|
export declare class PostgresProvisioner implements ResourceProvisioner {
|
|
15
15
|
#private;
|
|
@@ -20,6 +20,6 @@ export declare class PostgresProvisioner implements ResourceProvisioner {
|
|
|
20
20
|
properties: ResourceProperties;
|
|
21
21
|
state: ResourceState;
|
|
22
22
|
}>;
|
|
23
|
-
destroy(state: ResourceState): Promise<void>;
|
|
23
|
+
destroy(state: ResourceState, _opts: DestroyOpts): Promise<void>;
|
|
24
24
|
}
|
|
25
25
|
//# sourceMappingURL=postgres.d.ts.map
|