@fabrika/provider-zerops 0.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/package.json +40 -0
- package/src/api.ts +825 -0
- package/src/authoring.ts +33 -0
- package/src/cli-args.ts +101 -0
- package/src/cli.ts +82 -0
- package/src/codec.ts +50 -0
- package/src/collaborators.ts +47 -0
- package/src/compile.ts +382 -0
- package/src/control.ts +390 -0
- package/src/index.ts +98 -0
- package/src/manifest.ts +442 -0
- package/src/namespace-command.ts +209 -0
- package/src/namespace.ts +1028 -0
- package/src/plan.ts +51 -0
- package/src/provider.ts +311 -0
- package/src/schema.generated.ts +607 -0
- package/src/service-names.ts +38 -0
- package/src/types.ts +125 -0
package/src/compile.ts
ADDED
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
// Compiles an app's Zerops declaration into the `zerops-import.yaml` document the platform's
|
|
2
|
+
// service-import endpoint takes. PURE — no network, no filesystem, no clock.
|
|
3
|
+
//
|
|
4
|
+
// THIS FILE IS WHERE ADR-0004's TWO INVARIANTS ARE ENFORCED, and they are enforced STRUCTURALLY, at three
|
|
5
|
+
// layers, so that "someone forgot" is not a reachable state:
|
|
6
|
+
//
|
|
7
|
+
// 1. TYPE. `ZeropsServiceSpec` / `ZeropsProjectSpec` are the platform's contract MINUS
|
|
8
|
+
// the fields fabrika owns — an app literally cannot type `envIsolation`, `envSecrets`,
|
|
9
|
+
// `dotEnvSecrets`, `override`, or a project-level `envVariables`.
|
|
10
|
+
// 2. CONSTRUCTION. The compiler does not COPY an authored object and patch it; it BUILDS each entry and
|
|
11
|
+
// writes `envIsolation: 'service'` (and `override: true`) itself. There is no code path that emits a
|
|
12
|
+
// service or a project without them, because there is only one construction site.
|
|
13
|
+
// 3. ASSERTION. Every document passes `assertZeropsInvariants` before it is serialized. That catches the
|
|
14
|
+
// case the types cannot: a config that arrived from JSON, a hand-built document, a future edit here.
|
|
15
|
+
//
|
|
16
|
+
// The failure this prevents is concrete: with `envIsolation: none` — a legal setting, and NOT reliably the
|
|
17
|
+
// default — every service in the project sees every other service's SERVICE-level variables, so app A's
|
|
18
|
+
// credentials leak to app B. "Never write to project-level env" alone does not prevent that; the isolation
|
|
19
|
+
// setting is the other half.
|
|
20
|
+
|
|
21
|
+
import type { ZeropsImportProject, ZeropsImportService } from './schema.generated'
|
|
22
|
+
import type { ZeropsProjectSpec, ZeropsResourceContext, ZeropsServiceSpec, ZeropsSourceTarget } from './types'
|
|
23
|
+
|
|
24
|
+
/** The ONLY value fabrika ever writes for `envIsolation`, at either level. Not a default — an assertion. */
|
|
25
|
+
export const ENV_ISOLATION: 'service' = 'service'
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The ONLY value fabrika ever writes for `override`, on EVERY service class including managed ones.
|
|
29
|
+
* Live-verified: without it a re-apply is `400 serviceStackNameUnavailable` — see `compileService`.
|
|
30
|
+
*/
|
|
31
|
+
export const OVERRIDE: true = true
|
|
32
|
+
|
|
33
|
+
/** A compiled import document: exactly the two top-level keys the platform's schema defines. */
|
|
34
|
+
export interface ZeropsImportDocument {
|
|
35
|
+
/** Present only when fabrika is creating/reconciling the project itself. */
|
|
36
|
+
project?: ZeropsImportProject
|
|
37
|
+
services: ZeropsImportService[]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** What the compiler needs beyond the app's declaration. */
|
|
41
|
+
export interface CompileInput {
|
|
42
|
+
target: ZeropsSourceTarget
|
|
43
|
+
ctx: ZeropsResourceContext
|
|
44
|
+
/**
|
|
45
|
+
* Provision every service WITHOUT code — the answer to ADR-0004's "no secret can be set before the
|
|
46
|
+
* service exists" gap. Import the services, write their secrets through the env API, deploy later.
|
|
47
|
+
* Applied on top of whatever a service declared, because it is a property of THIS import, not of the app.
|
|
48
|
+
*/
|
|
49
|
+
startWithoutCode?: boolean
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Build one service entry. `envIsolation` and `override` are written HERE, by the compiler, from
|
|
54
|
+
* constants — never copied from the app and never conditional. Widening the spread to `...spec` would be
|
|
55
|
+
* the bug: it would let an untyped config smuggle an `envIsolation` in, which is why the fields are listed.
|
|
56
|
+
*
|
|
57
|
+
* ── What `override: true` actually does ───────────────────────────────────────────────────────────
|
|
58
|
+
*
|
|
59
|
+
* Settled against a live account, and it is neither what the published documentation says nor what this
|
|
60
|
+
* file used to claim. It means **"tolerate a hostname that already exists"** — nothing more:
|
|
61
|
+
*
|
|
62
|
+
* • WITHOUT it, re-applying a document that names an existing service is rejected outright,
|
|
63
|
+
* `400 serviceStackNameUnavailable`, and the whole document fails with it.
|
|
64
|
+
* • WITH it, an existing service is left EXACTLY as it is. A changed `profile`, `maxContainers` or
|
|
65
|
+
* `objectStorageSize` in the document is silently ignored; no process runs; `lastUpdate` does not
|
|
66
|
+
* move. It is not an update, and it is not the destructive replace upstream describes.
|
|
67
|
+
* • Upstream says the field is runtime-services-only. It is not: a managed `postgresql` or
|
|
68
|
+
* `object-storage` service accepts it, and NEEDS it — without it a re-apply fails on the database
|
|
69
|
+
* before it ever reaches a runtime service.
|
|
70
|
+
*
|
|
71
|
+
* So it is written on every service, and the reason is re-appliability, not reconciliation. Nothing in
|
|
72
|
+
* this compiler can converge an existing service; a field that must change is changed through the API
|
|
73
|
+
* that owns it (`PUT /service-stack/{id}/autoscaling`, `UpdateObjectStorageSize`, …).
|
|
74
|
+
*/
|
|
75
|
+
const compileService = (spec: ZeropsServiceSpec, startWithoutCode: boolean | undefined): ZeropsImportService => {
|
|
76
|
+
const service: ZeropsImportService = {
|
|
77
|
+
hostname: spec.hostname,
|
|
78
|
+
type: spec.type,
|
|
79
|
+
// ── compiler-owned, unconditional ────────────────────────────────────────
|
|
80
|
+
// Each service sees ONLY its own variables. Set at service level too (not just on the project)
|
|
81
|
+
// because the schema says a service-level value overrides the project's — so this holds even for a
|
|
82
|
+
// project fabrika did not create and cannot correct (`RequestPutProject` has no `envIsolation`).
|
|
83
|
+
envIsolation: ENV_ISOLATION,
|
|
84
|
+
// Re-applying the document does not COLLIDE with the existing services (see above — it also does
|
|
85
|
+
// not update them). That is the whole idempotency claim of the `apply-import` step (ADR-0003).
|
|
86
|
+
override: OVERRIDE,
|
|
87
|
+
}
|
|
88
|
+
if (spec.nginxConfig !== undefined) {
|
|
89
|
+
service.nginxConfig = spec.nginxConfig
|
|
90
|
+
}
|
|
91
|
+
if (spec.objectStorageSize !== undefined) {
|
|
92
|
+
service.objectStorageSize = spec.objectStorageSize
|
|
93
|
+
}
|
|
94
|
+
if (spec.objectStoragePolicy !== undefined) {
|
|
95
|
+
service.objectStoragePolicy = spec.objectStoragePolicy
|
|
96
|
+
}
|
|
97
|
+
if (spec.objectStorageRawPolicy !== undefined) {
|
|
98
|
+
service.objectStorageRawPolicy = spec.objectStorageRawPolicy
|
|
99
|
+
}
|
|
100
|
+
if (spec.buildFromGit !== undefined) {
|
|
101
|
+
service.buildFromGit = spec.buildFromGit
|
|
102
|
+
}
|
|
103
|
+
if (spec.enableSubdomainAccess !== undefined) {
|
|
104
|
+
service.enableSubdomainAccess = spec.enableSubdomainAccess
|
|
105
|
+
}
|
|
106
|
+
if (spec.priority !== undefined) {
|
|
107
|
+
service.priority = spec.priority
|
|
108
|
+
}
|
|
109
|
+
if (spec.verticalAutoscaling !== undefined) {
|
|
110
|
+
service.verticalAutoscaling = spec.verticalAutoscaling
|
|
111
|
+
}
|
|
112
|
+
if (spec.minContainers !== undefined) {
|
|
113
|
+
service.minContainers = spec.minContainers
|
|
114
|
+
}
|
|
115
|
+
if (spec.maxContainers !== undefined) {
|
|
116
|
+
service.maxContainers = spec.maxContainers
|
|
117
|
+
}
|
|
118
|
+
if (spec.profile !== undefined) {
|
|
119
|
+
service.profile = spec.profile
|
|
120
|
+
}
|
|
121
|
+
if (spec.profileOverrides !== undefined) {
|
|
122
|
+
service.profileOverrides = spec.profileOverrides
|
|
123
|
+
}
|
|
124
|
+
if (spec.mount !== undefined) {
|
|
125
|
+
service.mount = spec.mount
|
|
126
|
+
}
|
|
127
|
+
if (spec.enableCdn !== undefined) {
|
|
128
|
+
service.enableCdn = spec.enableCdn
|
|
129
|
+
}
|
|
130
|
+
if (spec.sshIsolation !== undefined) {
|
|
131
|
+
service.sshIsolation = spec.sshIsolation
|
|
132
|
+
}
|
|
133
|
+
if (spec.zeropsSetup !== undefined) {
|
|
134
|
+
service.zeropsSetup = spec.zeropsSetup
|
|
135
|
+
}
|
|
136
|
+
if (spec.zeropsYaml !== undefined) {
|
|
137
|
+
service.zeropsYaml = spec.zeropsYaml
|
|
138
|
+
}
|
|
139
|
+
// `startWithoutCode` on the IMPORT wins over the app's own value: a provisioning import must be
|
|
140
|
+
// code-free even for a service that normally builds from git.
|
|
141
|
+
const codeless = startWithoutCode ?? spec.startWithoutCode
|
|
142
|
+
if (codeless !== undefined) {
|
|
143
|
+
service.startWithoutCode = codeless
|
|
144
|
+
}
|
|
145
|
+
return service
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Build the project entry. Note what is NOT here: `envVariables`. A project-level variable is injected
|
|
150
|
+
* into EVERY service in the project and one project holds many apps, so writing one would hand app A's
|
|
151
|
+
* credentials to app B (ADR-0004). There is no field to set and no branch that could set it.
|
|
152
|
+
*/
|
|
153
|
+
const compileProject = (spec: ZeropsProjectSpec): ZeropsImportProject => {
|
|
154
|
+
const project: ZeropsImportProject = {
|
|
155
|
+
name: spec.name,
|
|
156
|
+
// Compiler-owned, unconditional. Settable at project CREATION only — `RequestPutProject` has no
|
|
157
|
+
// `envIsolation` — so getting it wrong here is not correctable through the project API afterwards.
|
|
158
|
+
envIsolation: ENV_ISOLATION,
|
|
159
|
+
}
|
|
160
|
+
if (spec.description !== undefined) {
|
|
161
|
+
project.description = spec.description
|
|
162
|
+
}
|
|
163
|
+
if (spec.corePackage !== undefined) {
|
|
164
|
+
project.corePackage = spec.corePackage
|
|
165
|
+
}
|
|
166
|
+
if (spec.tags !== undefined) {
|
|
167
|
+
project.tags = spec.tags
|
|
168
|
+
}
|
|
169
|
+
if (spec.sshIsolation !== undefined) {
|
|
170
|
+
project.sshIsolation = spec.sshIsolation
|
|
171
|
+
}
|
|
172
|
+
if (spec.sharedIpv4 !== undefined) {
|
|
173
|
+
project.sharedIpv4 = spec.sharedIpv4
|
|
174
|
+
}
|
|
175
|
+
if (spec.location !== undefined) {
|
|
176
|
+
project.location = spec.location
|
|
177
|
+
}
|
|
178
|
+
return project
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* The last gate before serialization: re-check ADR-0004's invariants on the FINISHED document. The types
|
|
183
|
+
* make a violation unauthorable and the construction makes it unemittable; this catches everything else —
|
|
184
|
+
* a config deserialized from JSON, a document assembled by hand, a future edit to this file.
|
|
185
|
+
*
|
|
186
|
+
* Deliberately checks the emitted document rather than the input: it is the document that reaches Zerops.
|
|
187
|
+
*/
|
|
188
|
+
export const assertZeropsInvariants = (document: ZeropsImportDocument): void => {
|
|
189
|
+
const project = document.project
|
|
190
|
+
if (project !== undefined) {
|
|
191
|
+
if (project.envVariables !== undefined) {
|
|
192
|
+
throw new Error('zerops: refusing to write PROJECT-level env variables — they reach every service in the project (ADR-0004)')
|
|
193
|
+
}
|
|
194
|
+
if (project.envIsolation !== ENV_ISOLATION) {
|
|
195
|
+
throw new Error(`zerops: project envIsolation must be \`${ENV_ISOLATION}\`, got \`${String(project.envIsolation)}\` (ADR-0004)`)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (document.services.length === 0) {
|
|
199
|
+
throw new Error('zerops: import document declares no services')
|
|
200
|
+
}
|
|
201
|
+
const seen = new Set<string>()
|
|
202
|
+
for (const service of document.services) {
|
|
203
|
+
if (service.envIsolation !== ENV_ISOLATION) {
|
|
204
|
+
throw new Error(
|
|
205
|
+
`zerops: service \`${service.hostname}\` envIsolation must be \`${ENV_ISOLATION}\`, got \`${String(service.envIsolation)}\` (ADR-0004)`,
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
// Every service class, managed included. A document missing it anywhere cannot be re-applied at
|
|
209
|
+
// all: the platform answers `400 serviceStackNameUnavailable` on the first hostname that exists
|
|
210
|
+
// and rejects the whole import, so one omission breaks every later apply, not just that service.
|
|
211
|
+
if (service.override !== OVERRIDE) {
|
|
212
|
+
throw new Error(
|
|
213
|
+
`zerops: service \`${service.hostname}\` must carry \`override: true\`; without it a re-apply is rejected as a name collision`,
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
if (service.envSecrets !== undefined || service.dotEnvSecrets !== undefined) {
|
|
217
|
+
// Never assert on the VALUE — only that the field is present.
|
|
218
|
+
throw new Error(
|
|
219
|
+
`zerops: service \`${service.hostname}\` carries secrets in the import document; secrets are written through the env API (ADR-0004)`,
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
// `zeropsSetup` is part of the import's pipeline configuration, and the platform rejects the WHOLE
|
|
223
|
+
// document when a service names a setup without also naming the repository to build it from:
|
|
224
|
+
//
|
|
225
|
+
// 400 projectImportInvalidParameter
|
|
226
|
+
// {"iam.buildFromGit": ["parameter is required for use of pipelineConfig"]}
|
|
227
|
+
//
|
|
228
|
+
// Verified against a real account, and it is not a corner case — it failed the very first step of
|
|
229
|
+
// the documented bring-up (import → write secrets → deploy) for every runtime service at once.
|
|
230
|
+
// Nothing is lost by dropping the setup name here: it is only redundant restatement of Zerops' own
|
|
231
|
+
// "setup name defaults to the hostname" rule, and a deploy that needs to say otherwise passes
|
|
232
|
+
// `zeropsSetup` at TRIGGER time, where no repository is required.
|
|
233
|
+
if (service.zeropsSetup !== undefined && service.buildFromGit === undefined) {
|
|
234
|
+
throw new Error(
|
|
235
|
+
`zerops: service \`${service.hostname}\` names \`zeropsSetup\` without \`buildFromGit\`; the platform rejects the import (pass the setup when triggering the pipeline instead)`,
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
if (seen.has(service.hostname)) {
|
|
239
|
+
throw new Error(`zerops: duplicate service hostname \`${service.hostname}\` in the import document`)
|
|
240
|
+
}
|
|
241
|
+
seen.add(service.hostname)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Compile an app's Zerops declaration into the import document, invariants already checked. */
|
|
246
|
+
export const compileImport = (input: CompileInput): ZeropsImportDocument => {
|
|
247
|
+
const services = input.target.services(input.ctx).map((spec) => compileService(spec, input.startWithoutCode))
|
|
248
|
+
const document: ZeropsImportDocument = input.target.project === undefined
|
|
249
|
+
? { services }
|
|
250
|
+
: { project: compileProject(input.target.project), services }
|
|
251
|
+
assertZeropsInvariants(document)
|
|
252
|
+
return document
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ── YAML serialization ──────────────────────────────────────────────────────────
|
|
256
|
+
|
|
257
|
+
/** Keys/scalars that are safe unquoted in YAML. Everything else gets a double-quoted, JSON-escaped form. */
|
|
258
|
+
const PLAIN = /^[A-Za-z_][A-Za-z0-9_./@+-]*$/
|
|
259
|
+
|
|
260
|
+
const scalar = (value: string): string => {
|
|
261
|
+
// Reserved plain scalars that YAML would read as booleans/null, plus anything with structure.
|
|
262
|
+
const reserved = new Set(['true', 'false', 'null', 'yes', 'no', 'on', 'off', '~'])
|
|
263
|
+
if (value !== '' && PLAIN.test(value) && !reserved.has(value.toLowerCase())) {
|
|
264
|
+
return value
|
|
265
|
+
}
|
|
266
|
+
return JSON.stringify(value)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const isArray = (value: unknown): value is unknown[] => Array.isArray(value)
|
|
270
|
+
|
|
271
|
+
const isPlainObject = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null && !isArray(value)
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Serialize a compiled document to YAML. Deliberately hand-rolled and tiny: the value space is closed
|
|
275
|
+
* (strings, numbers, booleans, arrays, plain objects — no dates, no cycles, no anchors), a YAML dependency
|
|
276
|
+
* for that would be all risk and no benefit, and the emitter is exercised by every compiler test.
|
|
277
|
+
*
|
|
278
|
+
* Multi-line strings (an inline `nginxConfig`, a `zerops.yaml` command) are emitted as a literal block so
|
|
279
|
+
* their newlines survive intact.
|
|
280
|
+
*/
|
|
281
|
+
const emit = (value: unknown, indent: number): string[] => {
|
|
282
|
+
const pad = ' '.repeat(indent)
|
|
283
|
+
if (isArray(value)) {
|
|
284
|
+
if (value.length === 0) {
|
|
285
|
+
return [`${pad}[]`]
|
|
286
|
+
}
|
|
287
|
+
const lines: string[] = []
|
|
288
|
+
for (const item of value) {
|
|
289
|
+
const rendered = emit(item, indent + 1)
|
|
290
|
+
const first = rendered[0] ?? ''
|
|
291
|
+
lines.push(`${pad}- ${first.trimStart()}`)
|
|
292
|
+
lines.push(...rendered.slice(1))
|
|
293
|
+
}
|
|
294
|
+
return lines
|
|
295
|
+
}
|
|
296
|
+
if (isPlainObject(value)) {
|
|
297
|
+
const entries = Object.entries(value).filter(([, entry]) => entry !== undefined)
|
|
298
|
+
if (entries.length === 0) {
|
|
299
|
+
return [`${pad}{}`]
|
|
300
|
+
}
|
|
301
|
+
const lines: string[] = []
|
|
302
|
+
for (const [key, entry] of entries) {
|
|
303
|
+
if (isPlainObject(entry) || isArray(entry)) {
|
|
304
|
+
const rendered = emit(entry, indent + 1)
|
|
305
|
+
// An empty collection renders inline so the key never dangles.
|
|
306
|
+
if (rendered.length === 1 && (rendered[0]?.trim() === '{}' || rendered[0]?.trim() === '[]')) {
|
|
307
|
+
lines.push(`${pad}${scalar(key)}: ${rendered[0]?.trim() ?? ''}`)
|
|
308
|
+
continue
|
|
309
|
+
}
|
|
310
|
+
lines.push(`${pad}${scalar(key)}:`)
|
|
311
|
+
lines.push(...rendered)
|
|
312
|
+
continue
|
|
313
|
+
}
|
|
314
|
+
lines.push(...emit(entry, indent).map((line, index) => (index === 0 ? `${pad}${scalar(key)}: ${line.trimStart()}` : line)))
|
|
315
|
+
}
|
|
316
|
+
return lines
|
|
317
|
+
}
|
|
318
|
+
if (typeof value === 'string') {
|
|
319
|
+
if (value.includes('\n')) {
|
|
320
|
+
const body = value.replace(/\n$/, '').split('\n').map((line) => `${pad} ${line}`)
|
|
321
|
+
return [`${pad}|-`, ...body]
|
|
322
|
+
}
|
|
323
|
+
return [`${pad}${scalar(value)}`]
|
|
324
|
+
}
|
|
325
|
+
if (value === null) {
|
|
326
|
+
return [`${pad}null`]
|
|
327
|
+
}
|
|
328
|
+
return [`${pad}${String(value)}`]
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Render an arbitrary Zerops document (a `zerops.yaml`, a fragment) with the SAME emitter the import
|
|
333
|
+
* document uses. Exported so nothing outside this file has to grow a second YAML writer: two emitters
|
|
334
|
+
* disagreeing about quoting or block scalars is a class of bug worth spending one export to avoid.
|
|
335
|
+
*
|
|
336
|
+
* Deliberately does NOT assert the import invariants — it does not know it is looking at an import
|
|
337
|
+
* document. `renderImportYaml` is the entry point that does, and it is the one the driver calls.
|
|
338
|
+
*/
|
|
339
|
+
export const renderYaml = (value: unknown): string => `${emit(value, 0).join('\n')}\n`
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Render a compiled document as the `zerops-import.yaml` text the API takes. Re-asserts the invariants:
|
|
343
|
+
* this is the last place the document is still inspectable, and the only place it becomes a string.
|
|
344
|
+
*/
|
|
345
|
+
export const renderImportYaml = (document: ZeropsImportDocument): string => {
|
|
346
|
+
assertZeropsInvariants(document)
|
|
347
|
+
const lines: string[] = []
|
|
348
|
+
if (document.project !== undefined) {
|
|
349
|
+
lines.push('project:')
|
|
350
|
+
lines.push(...emit(document.project, 1))
|
|
351
|
+
}
|
|
352
|
+
lines.push('services:')
|
|
353
|
+
lines.push(...emit(document.services, 1))
|
|
354
|
+
return `${lines.join('\n')}\n`
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** Compile + render in one step — what the driver's `apply-import` step actually calls. */
|
|
358
|
+
export const compileImportYaml = (input: CompileInput): { document: ZeropsImportDocument; yaml: string } => {
|
|
359
|
+
const document = compileImport(input)
|
|
360
|
+
return { document, yaml: renderImportYaml(document) }
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Compile the PROVISIONING import: the same document with every service forced `startWithoutCode: true`.
|
|
365
|
+
*
|
|
366
|
+
* This is ADR-0004's recorded gap closed. The ADR accepted a cost — "no secret can be set before the
|
|
367
|
+
* Zerops service exists, so registration must provision" — and `startWithoutCode` makes that provisioning
|
|
368
|
+
* cheap and code-free. The order becomes: register the app → import its services with no code → write
|
|
369
|
+
* their secrets through the SERVICE-level env API → deploy later. No local secret buffer, no secret in
|
|
370
|
+
* this document, and no secret held anywhere fabrika could leak it.
|
|
371
|
+
*
|
|
372
|
+
* Called by the control plane at EDIT time, not by the driver: on Zerops, pushing secrets is not a deploy
|
|
373
|
+
* step at all.
|
|
374
|
+
*
|
|
375
|
+
* **This document is the one that is NOT safe to re-apply, and `override` does not make it safe.**
|
|
376
|
+
* Verified live: re-applying it at a service that already carries code starts a `stack.deploy` and
|
|
377
|
+
* activates a new, EMPTY app version — the running code becomes a `BACKUP`. The steady-state document
|
|
378
|
+
* (no `startWithoutCode`) is a complete no-op on the same services. So this form belongs to first
|
|
379
|
+
* bring-up only; anything that reconciles an existing project applies `compileImportYaml`.
|
|
380
|
+
*/
|
|
381
|
+
export const compileProvisioningYaml = (input: Omit<CompileInput, 'startWithoutCode'>): { document: ZeropsImportDocument; yaml: string } =>
|
|
382
|
+
compileImportYaml({ ...input, startWithoutCode: true })
|