@fabrika/provider-zerops 0.2.0 → 0.3.2
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 +18 -5
- package/src/api/client.ts +522 -0
- package/src/api/errors.ts +50 -0
- package/src/api/polling.ts +59 -0
- package/src/api/readers.ts +185 -0
- package/src/api/service-env-response.ts +34 -0
- package/src/api/types.ts +602 -0
- package/src/api.ts +29 -1378
- package/src/authoring.ts +7 -10
- package/src/cli-args.ts +2 -6
- package/src/cli.ts +3 -28
- package/src/collaborators.ts +2 -4
- package/src/compile.ts +3 -1
- package/src/control.ts +203 -450
- package/src/descriptor.ts +179 -0
- package/src/index.ts +23 -127
- package/src/installation/admin-options.ts +100 -0
- package/src/installation/admin.ts +123 -0
- package/src/installation/deploy-options.ts +127 -0
- package/src/installation/deploy.ts +668 -0
- package/src/installation/generated/platform-console-schema.ts +41 -0
- package/src/installation/generated/platform-proxy-manifest.ts +72 -0
- package/src/installation/hosts.ts +117 -0
- package/src/installation/index.ts +390 -0
- package/src/installation/init-options.ts +148 -0
- package/src/installation/init.ts +394 -0
- package/src/installation/install-collaborators.ts +27 -0
- package/src/installation/install-environment.ts +193 -0
- package/src/installation/install-options.ts +156 -0
- package/src/installation/install-project.ts +201 -0
- package/src/installation/install-provisioning.ts +185 -0
- package/src/installation/install.ts +262 -0
- package/src/installation/installation-source.ts +42 -0
- package/src/installation/log.ts +77 -0
- package/src/installation/machine-key.ts +65 -0
- package/src/installation/manifest.ts +95 -0
- package/src/installation/placement-options.ts +33 -0
- package/src/installation/platform-source.ts +106 -0
- package/src/installation/platform-upload.ts +34 -0
- package/src/installation/proxy-manifest.ts +113 -0
- package/src/installation/secrets.ts +79 -0
- package/src/installation/sidecar.ts +125 -0
- package/src/installation/source-options.ts +12 -0
- package/src/installation/templates/README.md +116 -0
- package/src/installation/templates/fabrika.ref +1 -0
- package/src/installation/templates/gitignore +3 -0
- package/src/installation/templates/platform.yml +144 -0
- package/src/installation/topology/artifacts.ts +141 -0
- package/src/installation/topology/index.ts +13 -0
- package/src/installation/topology/installation-schemas.ts +225 -0
- package/src/installation/topology/invariants.ts +86 -0
- package/src/installation/topology/setups.ts +442 -0
- package/src/installation/topology/topology.ts +501 -0
- package/src/installation/topology/validate.ts +26 -0
- package/src/installation/upgrade-options.ts +99 -0
- package/src/installation/upgrade-output.ts +31 -0
- package/src/installation/upgrade.ts +373 -0
- package/src/managed-env.ts +113 -0
- package/src/manifest.ts +194 -344
- package/src/namespace/claims.ts +77 -0
- package/src/namespace/codec.ts +179 -0
- package/src/namespace/constants.ts +30 -0
- package/src/namespace/errors.ts +60 -0
- package/src/namespace/manifest.ts +28 -0
- package/src/namespace/operator.ts +151 -0
- package/src/namespace/provisioning.ts +482 -0
- package/src/namespace/proxy-attempt.ts +220 -0
- package/src/namespace/proxy-source.ts +22 -0
- package/src/namespace/topology.ts +115 -0
- package/src/namespace/types.ts +114 -0
- package/src/namespace.ts +34 -1271
- package/src/plan.ts +21 -23
- package/src/provider.ts +266 -241
- package/src/source/constants.ts +11 -0
- package/src/source/port.ts +49 -0
- package/src/source/types.ts +4 -0
- package/src/source/validation.ts +72 -0
- package/src/source.ts +2 -1220
- package/src/types.ts +8 -7
- package/src/version-attempt.ts +18 -0
- package/src/codec.ts +0 -108
- package/src/namespace-command.ts +0 -209
- package/src/source-connection.ts +0 -344
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// GENERATED FILE — DO NOT EDIT BY HAND.
|
|
2
|
+
//
|
|
3
|
+
// Source: packages/platform-artifacts/src/console-schema.ts
|
|
4
|
+
// Regenerate: bun run --filter @fabrika/platform-artifacts gen
|
|
5
|
+
//
|
|
6
|
+
// The console's authorization vocabulary — `controlSchema` in `@fabrika/control` — as DATA. A
|
|
7
|
+
// PUBLISHED deploy command may not import the private package that declares it, and `platform deploy`
|
|
8
|
+
// has to send this document to IAM: the schema PUT is what REGISTERS the app, and
|
|
9
|
+
// `apps.setReturnOrigins` 404s for an app IAM has never heard of. `gen:check` is the drift witness.
|
|
10
|
+
|
|
11
|
+
import type { AppSchema } from '@fabrika/auth-core'
|
|
12
|
+
|
|
13
|
+
export const PLATFORM_CONSOLE_APP_SCHEMA: AppSchema = {
|
|
14
|
+
scopes: [
|
|
15
|
+
{ type: 'app', label: 'App' },
|
|
16
|
+
{ type: 'environment', label: 'Environment' },
|
|
17
|
+
],
|
|
18
|
+
actions: [
|
|
19
|
+
{ action: 'deploy.trigger', description: 'Trigger a deploy run' },
|
|
20
|
+
{ action: 'deploy.read', description: 'Read deploy runs + their logs' },
|
|
21
|
+
{ action: 'app.manage', description: 'Manage the app registry (apps + app_envs)' },
|
|
22
|
+
{ action: 'namespace.manage', description: 'Manage deployment namespaces' },
|
|
23
|
+
{ action: 'secret.manage', description: 'Manage secret values + their references' },
|
|
24
|
+
{ action: 'source.connection.manage', description: 'Manage the platform GitHub source connection' },
|
|
25
|
+
{ action: 'operations.read', description: 'Read Operations errors and activity' },
|
|
26
|
+
{ action: 'operations.triage', description: 'Triage Operations errors' },
|
|
27
|
+
{ action: 'operations.manage', description: 'Manage Operations sources, alerts, and retention' },
|
|
28
|
+
],
|
|
29
|
+
roles: {
|
|
30
|
+
operator: {
|
|
31
|
+
name: 'Operator',
|
|
32
|
+
description: 'Trigger and read any deploy (no registry or secret management).',
|
|
33
|
+
permissions: ['deploy.*', 'operations.*'],
|
|
34
|
+
},
|
|
35
|
+
admin: {
|
|
36
|
+
name: 'Admin',
|
|
37
|
+
description: 'Full access to every vozka action in every scope.',
|
|
38
|
+
permissions: ['*'],
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// GENERATED FILE — DO NOT EDIT BY HAND.
|
|
2
|
+
//
|
|
3
|
+
// Source: packages/platform-artifacts/src/proxy-manifest.ts
|
|
4
|
+
// Regenerate: bun run --filter @fabrika/platform-artifacts gen
|
|
5
|
+
//
|
|
6
|
+
// The installation-independent half of `FABRIKA_PROXY_MANIFEST_JSON`: which apps the platform proxy
|
|
7
|
+
// fronts, at which private upstream, on which public listener, under which gates. The hosts and the
|
|
8
|
+
// browser-facing scheme belong to ONE installation and are supplied at deploy time —
|
|
9
|
+
// `resolvePlatformProxyManifest` binds them.
|
|
10
|
+
//
|
|
11
|
+
// The gate rules below are `CONTROL_PROXY_GATES` and `OPERATIONS_PROXY_GATES` as of the last
|
|
12
|
+
// regeneration, copied on purpose: a PUBLISHED deploy command may not import the private package that
|
|
13
|
+
// owns them. `gen:check` in CI is what makes the copy a fact rather than a hope.
|
|
14
|
+
|
|
15
|
+
import type { PlatformProxyManifestTemplate } from '../proxy-manifest'
|
|
16
|
+
|
|
17
|
+
export const PLATFORM_PROXY_MANIFEST_TEMPLATE: PlatformProxyManifestTemplate = {
|
|
18
|
+
apps: [
|
|
19
|
+
{
|
|
20
|
+
id: 'iam',
|
|
21
|
+
service: 'iam',
|
|
22
|
+
port: 8080,
|
|
23
|
+
upstream: 'iam:3000',
|
|
24
|
+
gates: {
|
|
25
|
+
rules: [
|
|
26
|
+
{ path: '/*', kind: 'public' },
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: 'vozka',
|
|
32
|
+
service: 'control',
|
|
33
|
+
port: 8082,
|
|
34
|
+
upstream: 'control:3000',
|
|
35
|
+
gates: {
|
|
36
|
+
rules: [
|
|
37
|
+
{ path: '/healthz', kind: 'public' },
|
|
38
|
+
{ path: '/.well-known/fabrika-builder', kind: 'public' },
|
|
39
|
+
{ path: '/builder/connect', kind: 'public' },
|
|
40
|
+
{ path: '/api/health', kind: 'public' },
|
|
41
|
+
{ path: '/webhooks/github', kind: 'public' },
|
|
42
|
+
{ path: '/webhooks/github/*', kind: 'public' },
|
|
43
|
+
{ path: '/iam/admin', kind: 'service' },
|
|
44
|
+
{ path: '/iam/admin', kind: 'human' },
|
|
45
|
+
{ path: '/iam/admin/*', kind: 'service' },
|
|
46
|
+
{ path: '/iam/admin/*', kind: 'human' },
|
|
47
|
+
{ path: '/operations/api', kind: 'service' },
|
|
48
|
+
{ path: '/operations/api', kind: 'human' },
|
|
49
|
+
{ path: '/operations/api/*', kind: 'service' },
|
|
50
|
+
{ path: '/operations/api/*', kind: 'human' },
|
|
51
|
+
{ path: '/api/source/github/manifest/*', kind: 'human' },
|
|
52
|
+
{ path: '/api/source/github/callback', kind: 'human' },
|
|
53
|
+
{ path: '/api/*', kind: 'service' },
|
|
54
|
+
{ path: '/api/*', kind: 'human' },
|
|
55
|
+
{ path: '/*', kind: 'human' },
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: 'operations',
|
|
61
|
+
service: 'operations',
|
|
62
|
+
port: 8083,
|
|
63
|
+
upstream: 'operations:3000',
|
|
64
|
+
gates: {
|
|
65
|
+
rules: [
|
|
66
|
+
{ path: '/api/*/envelope/', kind: 'public' },
|
|
67
|
+
{ path: '/api/artifacts/source-maps/', kind: 'public' },
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// Where one installation's platform services answer, and the two ways a deploy can know.
|
|
2
|
+
//
|
|
3
|
+
// ── Why this is not a constant ────────────────────────────────────────────────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// `resolvePlatformProxyManifest` takes the hosts as an argument precisely because they belong to ONE
|
|
6
|
+
// installation. There are exactly two sources for them, and which one is used decides something else
|
|
7
|
+
// too — whether the proxy should be published on a Zerops subdomain at all:
|
|
8
|
+
//
|
|
9
|
+
// derived the operator names none, and each platform app's host is read off the PROXY service's
|
|
10
|
+
// `zeropsSubdomain` variable, keyed by the listener port the committed template assigns.
|
|
11
|
+
// This is the `zerops-subdomain` installation, and the deploy ensures the subdomain.
|
|
12
|
+
// explicit the operator names all three. This is the `custom-domain` installation: the domains are
|
|
13
|
+
// bound to the project's L7 balancer out of band, and the deploy must NOT publish a
|
|
14
|
+
// subdomain — doing so would add a second public address nobody asked for.
|
|
15
|
+
//
|
|
16
|
+
// All three or none. A mix would leave the deploy unable to say which installation it is looking at.
|
|
17
|
+
//
|
|
18
|
+
// ── `zeropsSubdomain` is a NAME, not a state ──────────────────────────────────────────────────────
|
|
19
|
+
//
|
|
20
|
+
// Live-verified (`docs/reference/zerops-platform.md`): the variable is present and non-empty on a
|
|
21
|
+
// service whose subdomain was never enabled, and unchanged after one is disabled. That is exactly what
|
|
22
|
+
// makes it usable HERE, one step before `enableSubdomainAccess` — the manifest has to carry the hosts
|
|
23
|
+
// before the proxy that serves them is built. What it needs is a DEPLOYED HTTP port: a proxy that has
|
|
24
|
+
// never been deployed publishes none, so the variable names nothing and this refuses rather than
|
|
25
|
+
// guessing at a hostname.
|
|
26
|
+
|
|
27
|
+
import type { PlatformProxyService } from './proxy-manifest'
|
|
28
|
+
|
|
29
|
+
/** The public hostname of each platform service, without a scheme and without a port. */
|
|
30
|
+
export type PlatformHosts = Readonly<Record<PlatformProxyService, string>>
|
|
31
|
+
|
|
32
|
+
/** Everything the deploy needs to know about where this installation answers. */
|
|
33
|
+
export interface PlatformPlacement {
|
|
34
|
+
readonly hosts: PlatformHosts
|
|
35
|
+
/** The scheme the BROWSER speaks. Configuration, never a header — see `ProxyApp.scheme`. */
|
|
36
|
+
readonly scheme: 'http' | 'https'
|
|
37
|
+
/**
|
|
38
|
+
* Whether the proxy's public entry point is Zerops' generated subdomain. True only on the derived
|
|
39
|
+
* path: a custom-domain installation must not additionally publish a `.zerops.app` address.
|
|
40
|
+
*/
|
|
41
|
+
readonly zeropsSubdomain: boolean
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The Zerops variable naming one URL per published HTTP port. READ_ONLY, written by the platform. */
|
|
45
|
+
export const ZEROPS_SUBDOMAIN_VARIABLE = 'zeropsSubdomain'
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Map the proxy's generated subdomains by the listener port each one fronts.
|
|
49
|
+
*
|
|
50
|
+
* The host form is `<hostname>-<hash>-<port>.<region>.zerops.app`, one line per HTTP port. Read as
|
|
51
|
+
* URLs rather than by splitting on `-`, so a hostname that itself contains a digit-suffixed segment
|
|
52
|
+
* cannot be misread: the port is the trailing `-<digits>` of the FIRST label only.
|
|
53
|
+
*/
|
|
54
|
+
export const parseZeropsSubdomains = (value: string): Map<number, string> => {
|
|
55
|
+
const byPort = new Map<number, string>()
|
|
56
|
+
for (const line of value.split('\n')) {
|
|
57
|
+
const trimmed = line.trim()
|
|
58
|
+
if (trimmed === '') {
|
|
59
|
+
continue
|
|
60
|
+
}
|
|
61
|
+
let hostname: string
|
|
62
|
+
try {
|
|
63
|
+
hostname = new URL(trimmed).hostname
|
|
64
|
+
} catch {
|
|
65
|
+
continue
|
|
66
|
+
}
|
|
67
|
+
const label = hostname.split('.')[0] ?? ''
|
|
68
|
+
const match = /-(\d+)$/.exec(label)
|
|
69
|
+
if (match?.[1] === undefined) {
|
|
70
|
+
continue
|
|
71
|
+
}
|
|
72
|
+
byPort.set(Number.parseInt(match[1], 10), hostname)
|
|
73
|
+
}
|
|
74
|
+
return byPort
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** One platform app's listener, as the committed template assigns it. */
|
|
78
|
+
export interface PlatformListener {
|
|
79
|
+
readonly service: PlatformProxyService
|
|
80
|
+
readonly port: number
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Bind every platform service to the generated subdomain of its listener.
|
|
85
|
+
*
|
|
86
|
+
* Throws naming every port it could not find, because the alternative — writing a manifest with a
|
|
87
|
+
* guessed or missing host — is a proxy that fronts nothing at an address the console then bounces to.
|
|
88
|
+
*/
|
|
89
|
+
export const derivePlatformHosts = (subdomains: string, listeners: readonly PlatformListener[]): PlatformHosts => {
|
|
90
|
+
const byPort = parseZeropsSubdomains(subdomains)
|
|
91
|
+
const resolved = new Map<PlatformProxyService, string>()
|
|
92
|
+
const missing: string[] = []
|
|
93
|
+
for (const listener of listeners) {
|
|
94
|
+
const host = byPort.get(listener.port)
|
|
95
|
+
if (host === undefined) {
|
|
96
|
+
missing.push(`${listener.service} (:${listener.port})`)
|
|
97
|
+
continue
|
|
98
|
+
}
|
|
99
|
+
resolved.set(listener.service, host)
|
|
100
|
+
}
|
|
101
|
+
if (missing.length > 0) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
`the proxy publishes no Zerops subdomain for ${missing.join(', ')} — deploy the proxy once so it has HTTP ports, `
|
|
104
|
+
+ 'or name every host explicitly with --iam-host / --console-host / --operations-host',
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
const iam = resolved.get('iam')
|
|
108
|
+
const control = resolved.get('control')
|
|
109
|
+
const operations = resolved.get('operations')
|
|
110
|
+
if (iam === undefined || control === undefined || operations === undefined) {
|
|
111
|
+
throw new Error('the platform proxy manifest template does not assign a listener to every platform service')
|
|
112
|
+
}
|
|
113
|
+
return { iam, control, operations }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** `https://<host>` — the origin a browser, a token's `iss` and a return-origin registry all use. */
|
|
117
|
+
export const platformOrigin = (scheme: 'http' | 'https', host: string): string => `${scheme}://${host}`
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import { reconcileSchema } from '@fabrika/auth'
|
|
2
|
+
import {
|
|
3
|
+
describeArgument,
|
|
4
|
+
info as consoleInfo,
|
|
5
|
+
ok as consoleOk,
|
|
6
|
+
step as consoleStep,
|
|
7
|
+
UNATTENDED_FLAG,
|
|
8
|
+
warn as consoleWarn,
|
|
9
|
+
} from '@fabrika/installation-init'
|
|
10
|
+
import type { SchemaReconciler } from '@fabrika/provider-contract'
|
|
11
|
+
import type { InstallationCli, InstallationCommand } from '@fabrika/provider-contract/installation'
|
|
12
|
+
import { createZeropsApi, defaultSleep } from '../index'
|
|
13
|
+
import { consoleAdminCollaborators, runPlatformAdmin } from './admin'
|
|
14
|
+
import { parsePlatformAdminArgs, REISSUE_FLAG } from './admin-options'
|
|
15
|
+
import { deployPlatform, PLATFORM_CONCURRENT_DEPLOY, PLATFORM_DEPLOY_ORDER, PLATFORM_SEQUENTIAL_DEPLOY } from './deploy'
|
|
16
|
+
import { parsePlatformDeployArgs } from './deploy-options'
|
|
17
|
+
import { consoleInitCollaborators, runInit } from './init'
|
|
18
|
+
import { parseInitArgs } from './init-options'
|
|
19
|
+
import { consoleInstallCollaborators, runInstall } from './install'
|
|
20
|
+
import { parsePlatformInstallArgs } from './install-options'
|
|
21
|
+
import { prepareInstallationSource } from './installation-source'
|
|
22
|
+
import { consoleDeployLog } from './log'
|
|
23
|
+
import { generatedArtifacts } from './topology/artifacts'
|
|
24
|
+
import { assertArtifactMatchesSchema } from './topology/validate'
|
|
25
|
+
import { consoleUpgradeCollaborators, FABRIKA_REPOSITORY, runUpgrade, SIDECAR_BRANCH } from './upgrade'
|
|
26
|
+
import { parsePlatformUpgradeArgs, UPGRADE_DRY_RUN_FLAG } from './upgrade-options'
|
|
27
|
+
|
|
28
|
+
export { PLATFORM_CONCURRENT_DEPLOY, PLATFORM_DEPLOY_ORDER, PLATFORM_PROXY_SERVICE, PLATFORM_SEQUENTIAL_DEPLOY } from './deploy'
|
|
29
|
+
export { PLATFORM_CONSOLE_APP_SCHEMA } from './generated/platform-console-schema'
|
|
30
|
+
export { PLATFORM_PROXY_MANIFEST_TEMPLATE } from './generated/platform-proxy-manifest'
|
|
31
|
+
export {
|
|
32
|
+
type PlatformProxyApp,
|
|
33
|
+
platformProxyAppFor,
|
|
34
|
+
type PlatformProxyComposition,
|
|
35
|
+
type PlatformProxyManifestTemplate,
|
|
36
|
+
type PlatformProxyPlacement,
|
|
37
|
+
type PlatformProxyService,
|
|
38
|
+
resolvePlatformProxyManifest,
|
|
39
|
+
} from './proxy-manifest'
|
|
40
|
+
|
|
41
|
+
const USAGE = `Zerops installation
|
|
42
|
+
|
|
43
|
+
Commands:
|
|
44
|
+
fabrika platform install --provider=zerops [options]
|
|
45
|
+
fabrika platform init --provider=zerops <installation> --tag=<tag> --project-id=<id> [options]
|
|
46
|
+
fabrika platform plan --provider=zerops
|
|
47
|
+
fabrika platform deploy --provider=zerops [options]
|
|
48
|
+
fabrika platform admin --provider=zerops --email=<address> --iam-host=<host> [--scheme=<http|https>] [${REISSUE_FLAG}]
|
|
49
|
+
fabrika platform upgrade --provider=zerops --to=<tag> [<installation>] [--sidecar=<path>|<owner>/<name>] [${UPGRADE_DRY_RUN_FLAG}]
|
|
50
|
+
|
|
51
|
+
\`install\` CREATES an installation in a project you created empty; \`init\` and \`deploy\` UPDATE AN
|
|
52
|
+
INSTALLATION THAT ALREADY EXISTS. Run them in that order: install generates the provisioning key that
|
|
53
|
+
init then writes into the operator's GitHub Environment. \`admin\` comes LAST, once the installation is
|
|
54
|
+
serving: it admits the first human, and nothing before it can sign in. \`upgrade\` is the one you run
|
|
55
|
+
again and again afterwards: it moves the sidecar's pin to a newer published release and follows the
|
|
56
|
+
deploy that push triggers.
|
|
57
|
+
|
|
58
|
+
── platform install ──────────────────────────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
The from-scratch bring-up. With \`--create-project\` it creates the project too, with core package
|
|
61
|
+
LIGHT and \`envIsolation: service\` — the two settings Zerops accepts at project CREATION only and never
|
|
62
|
+
afterwards. \`--project-id=<id>\` installs into an empty project you created yourself instead, with both
|
|
63
|
+
already right; nothing here can read \`envIsolation\` back or correct it.
|
|
64
|
+
|
|
65
|
+
Interactive and laptop-side. It CONFIRMS before every step that leaves your machine: reading the
|
|
66
|
+
project, importing the services, minting the token, building the proxy, writing the variables, and
|
|
67
|
+
deploying. Declining any of them stops there; nothing before the import is a mutation. A stdin that
|
|
68
|
+
is not a terminal is refused unless \`${UNATTENDED_FLAG}\` is given — with nobody there to answer, a default yes
|
|
69
|
+
is not consent.
|
|
70
|
+
|
|
71
|
+
1. create the project and wait for it to be ACTIVE (\`--create-project\`; its id is printed before
|
|
72
|
+
the wait), or read the one you named — and refuse a core package the tier does not match
|
|
73
|
+
2. import ${PLATFORM_DEPLOY_ORDER.length + 2} services without code, and wait for the import's processes
|
|
74
|
+
3. generate six secrets and mint the control plane's Zerops INTEGRATION token
|
|
75
|
+
(client role NO_ACCESS, ADMIN on this project only — never your personal token)
|
|
76
|
+
4. pass 1: give the proxy an EMPTY manifest and build it, so it publishes one public hostname per
|
|
77
|
+
HTTP port; the three platform hosts are then READ off those, never composed
|
|
78
|
+
5. write every remaining variable on all four runtime services
|
|
79
|
+
6. pass 2: hand the whole ordered sequence to \`platform deploy\`
|
|
80
|
+
7. print the provisioning key — the CLI keeps no copy; it is held by iam and control in Zerops
|
|
81
|
+
|
|
82
|
+
It REFUSES a project that already holds an installation's generated secrets: a second bring-up would
|
|
83
|
+
roll a new vault key over the old one, which is unrecoverable, and new signing keys, which logs
|
|
84
|
+
everyone out. Once every bootstrap variable is placed, \`platform deploy\` can update the installation
|
|
85
|
+
or finish a failed pass 2. Read FABRIKA_IAM_PROVISIONING_KEY from iam or control and
|
|
86
|
+
FABRIKA_ZEROPS_ACCESS_TOKEN from control in Zerops if the handoff was interrupted.
|
|
87
|
+
Neither deploy nor init completes a partial bootstrap. If interrupted after a generated secret was
|
|
88
|
+
placed but before every bootstrap variable was placed, preserve the existing keys and arrange
|
|
89
|
+
operator-owned recovery, or start a fresh installation in a separate empty project.
|
|
90
|
+
Re-running install after any generated secret was placed is refused.
|
|
91
|
+
|
|
92
|
+
Only the LIGHT tier can be installed this way — it is the one topology that emits a services-only
|
|
93
|
+
import document. Authentication is PASSWORD ONLY; no administrator exists yet when this finishes.
|
|
94
|
+
|
|
95
|
+
Options (a flag beats the environment variable beside it):
|
|
96
|
+
|
|
97
|
+
--project-id=<id> FABRIKA_ZEROPS_PROJECT_ID the empty project you created
|
|
98
|
+
--create-project create the project on --client-id instead of
|
|
99
|
+
naming one with --project-id
|
|
100
|
+
--project-name=<name> the created project's name, default
|
|
101
|
+
fabrika-<env>
|
|
102
|
+
--client-id=<id> FABRIKA_ZEROPS_CLIENT_ID the client the token is minted on
|
|
103
|
+
--env=<name> FABRIKA_PLATFORM_ENVIRONMENT written to every service as ENVIRONMENT
|
|
104
|
+
--scheme=<http|https> FABRIKA_PLATFORM_SCHEME default https
|
|
105
|
+
--source-dir=<checkout> required clean full checkout running this CLI
|
|
106
|
+
--tier=light FABRIKA_PLATFORM_TIER default light, and the only value
|
|
107
|
+
${UNATTENDED_FLAG} answer every confirmation yes —
|
|
108
|
+
required when stdin is not a terminal
|
|
109
|
+
|
|
110
|
+
FABRIKA_ZEROPS_ACCESS_TOKEN required Zerops access token, environment only and no flag
|
|
111
|
+
FABRIKA_ZEROPS_API_URL optional region API base, when not the default
|
|
112
|
+
|
|
113
|
+
── platform init ─────────────────────────────────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
Creates and maintains the operator's SIDECAR REPOSITORY: the GitHub pipeline that calls
|
|
116
|
+
\`platform deploy\`, the tag it is pinned to, and the GitHub Environment holding this installation's
|
|
117
|
+
credentials. ADR-0025 puts that pipeline in a repository the operator owns — fabrika ships the
|
|
118
|
+
generator, never the pipeline, because one public repository cannot hold every account's credentials.
|
|
119
|
+
|
|
120
|
+
Laptop-side and prompt-free: every value comes from a flag or the environment, so a pipeline or an
|
|
121
|
+
agent runs it end to end. It CONFIRMS before every step that leaves the operator's disk: reading the
|
|
122
|
+
project, reading its control and proxy services, creating or pushing the repository, writing the
|
|
123
|
+
Environment, and triggering the run. A stdin that is not a terminal is refused unless \`${UNATTENDED_FLAG}\` is given.
|
|
124
|
+
Declining a write stops there and prints the command that repeats the run; a re-run is safe. Declining
|
|
125
|
+
the read of control and proxy only leaves the console and IAM origins out of what init prints.
|
|
126
|
+
|
|
127
|
+
Init writes nothing to Zerops. It reads control and proxy only to print the console and IAM origins
|
|
128
|
+
the installation records, and prints none it cannot verify. Public repositories deploy without a
|
|
129
|
+
GitHub App. GitHub App creation, webhook configuration, and installation verification belong to the
|
|
130
|
+
authenticated Control UI at \`Settings → Source\`; Control keeps the App keys in its vault (ADR-0029).
|
|
131
|
+
Init never prompts for, writes, recovers, or prints GitHub App credentials, and no source credential
|
|
132
|
+
reaches the sidecar or GitHub Environment.
|
|
133
|
+
|
|
134
|
+
If an older init left an owner-only local recovery file, this release does not open or delete it. Since
|
|
135
|
+
ADR-0029 there is nothing left to adopt it into: connect a new source in Control and delete the old
|
|
136
|
+
file afterwards.
|
|
137
|
+
|
|
138
|
+
<installation> names this installation: the GitHub Environment, the default
|
|
139
|
+
repository \`contember/fabrika-zerops-<installation>\`, and the
|
|
140
|
+
local checkout directory
|
|
141
|
+
|
|
142
|
+
Options (a flag beats the environment variable beside it):
|
|
143
|
+
|
|
144
|
+
--repo=<owner>/<name> a different sidecar repository
|
|
145
|
+
--checkout=<dir> the sidecar's local checkout, default
|
|
146
|
+
./fabrika-zerops-<installation>; refused
|
|
147
|
+
inside another git repository
|
|
148
|
+
--tag=<tag> FABRIKA_PLATFORM_TAG required; the published fabrika tag to pin
|
|
149
|
+
--project-id=<id> FABRIKA_ZEROPS_PROJECT_ID required; the project holding the platform
|
|
150
|
+
--env=<name> FABRIKA_PLATFORM_ENVIRONMENT default <installation>
|
|
151
|
+
--iam-host=<host> FABRIKA_PLATFORM_IAM_HOST custom domains: all three hosts or none;
|
|
152
|
+
--console-host=<host> FABRIKA_PLATFORM_CONSOLE_HOST none derives them from the proxy's Zerops
|
|
153
|
+
--operations-host=<host> FABRIKA_PLATFORM_OPERATIONS_HOST subdomains
|
|
154
|
+
${UNATTENDED_FLAG} answer every confirmation yes —
|
|
155
|
+
required when stdin is not a terminal
|
|
156
|
+
|
|
157
|
+
It writes TWO secrets into that Environment and GENERATES NEITHER — both already belong to the
|
|
158
|
+
installation, so a value invented here would not be one it accepts. Both are environment only:
|
|
159
|
+
|
|
160
|
+
FABRIKA_ZEROPS_ACCESS_TOKEN required the INTEGRATION token install minted (the \`control\` service's env)
|
|
161
|
+
FABRIKA_IAM_PROVISIONING_KEY required the px_ key install printed
|
|
162
|
+
FABRIKA_ZEROPS_API_URL optional region API base, when not the default
|
|
163
|
+
|
|
164
|
+
Each is sent to GitHub over \`gh\` stdin, and never printed and never written to disk.
|
|
165
|
+
|
|
166
|
+
The generated pipeline pins a published TAG of contember/fabrika-platform and refuses a branch, at
|
|
167
|
+
init and again in the workflow. It uploads that checkout's exact committed archive to every installation service.
|
|
168
|
+
|
|
169
|
+
── platform deploy ───────────────────────────────────────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
Unattended and idempotent. It owns the WHOLE ordered sequence, so an operator's pipeline calls this
|
|
172
|
+
ONE step — on Cloudflare the order lives in the scaffolded workflow instead, deliberately
|
|
173
|
+
(ADR-0027). In order:
|
|
174
|
+
|
|
175
|
+
1. resolve the project and its ${PLATFORM_DEPLOY_ORDER.join(', ')} services by hostname
|
|
176
|
+
2. write each service's environment: the composed proxy manifest, the installation's environment
|
|
177
|
+
name, and the origins derived from the resolved public hosts
|
|
178
|
+
3. deploy ${PLATFORM_CONCURRENT_DEPLOY.join(' + ')} together, then ${PLATFORM_SEQUENTIAL_DEPLOY.join(' → ')}, waiting for each to become ACTIVE
|
|
179
|
+
4. ensure the proxy's public entry point
|
|
180
|
+
5. reconcile the console's app schema and register its return origin with IAM
|
|
181
|
+
|
|
182
|
+
The order is a security property, not just a dependency: the application enforces nothing
|
|
183
|
+
(ADR-0022), so the proxy is deployed BEFORE control, and every environment write happens before any
|
|
184
|
+
deploy. A run that cannot apply the manifest never reaches step 3. ${PLATFORM_CONCURRENT_DEPLOY.join(', ')} order
|
|
185
|
+
NOTHING against each other — none calls a sibling at boot and none reads a variable another's build
|
|
186
|
+
needs — so they build at once; a failure of one is reported with that service named, and the others
|
|
187
|
+
are still followed to their end.
|
|
188
|
+
|
|
189
|
+
The proxy manifest is MERGED, never replaced. On a shared project an application's entry lives in the
|
|
190
|
+
same document; entries the platform does not own are carried through unchanged. An entry standing on
|
|
191
|
+
one of the platform's own public hosts is replaced, and the run says so.
|
|
192
|
+
|
|
193
|
+
Options (a flag beats the environment variable beside it):
|
|
194
|
+
|
|
195
|
+
--project-id=<id> FABRIKA_ZEROPS_PROJECT_ID the installation's project
|
|
196
|
+
--project-name=<name> FABRIKA_ZEROPS_PROJECT_NAME with --client-id, instead of the id
|
|
197
|
+
--client-id=<id> FABRIKA_ZEROPS_CLIENT_ID
|
|
198
|
+
--env=<name> FABRIKA_PLATFORM_ENVIRONMENT written to every service as ENVIRONMENT
|
|
199
|
+
--iam-host=<host> FABRIKA_PLATFORM_IAM_HOST all three, or none
|
|
200
|
+
--console-host=<host> FABRIKA_PLATFORM_CONSOLE_HOST
|
|
201
|
+
--operations-host=<host> FABRIKA_PLATFORM_OPERATIONS_HOST
|
|
202
|
+
--scheme=<http|https> FABRIKA_PLATFORM_SCHEME default https
|
|
203
|
+
--source-dir=<checkout> required clean full checkout running this CLI
|
|
204
|
+
--dry-run read everything, change nothing
|
|
205
|
+
|
|
206
|
+
Naming NO host derives all three from the proxy service's Zerops subdomains and makes this a
|
|
207
|
+
\`zerops-subdomain\` installation, whose public entry point step 4 ensures. Naming all three makes it a
|
|
208
|
+
\`custom-domain\` installation: the domains are bound to the project balancer out of band and no
|
|
209
|
+
subdomain is published.
|
|
210
|
+
|
|
211
|
+
Credentials are read from the ENVIRONMENT ONLY and have no flag, so they cannot reach a CI log or a
|
|
212
|
+
process listing:
|
|
213
|
+
|
|
214
|
+
FABRIKA_ZEROPS_ACCESS_TOKEN required Zerops access token (an INTEGRATION token scoped to
|
|
215
|
+
this installation's projects, never a personal one)
|
|
216
|
+
FABRIKA_IAM_PROVISIONING_KEY required the IAM-issued px_ admin key step 4 authenticates with
|
|
217
|
+
(a --dry-run does not need it)
|
|
218
|
+
FABRIKA_ZEROPS_API_URL optional region API base, when not the default
|
|
219
|
+
|
|
220
|
+
This command writes NO credential. Every secret an installation holds is placed at bring-up.
|
|
221
|
+
|
|
222
|
+
Install and deploy require --source-dir explicitly. Run the CLI from that same clean full checkout;
|
|
223
|
+
a packaged CLI cannot supply another revision's manifest or schema. --from-git and
|
|
224
|
+
FABRIKA_ZEROPS_BUILD_FROM_GIT are refused; remove them. Every build uses one exact source archive.
|
|
225
|
+
Install and deploy write FABRIKA_ZEROPS_PROXY_SOURCE_JSON on Control from the same verified snapshot,
|
|
226
|
+
using the public contember/fabrika-platform repository. This default is for NEW namespace planning;
|
|
227
|
+
upgrading it does not replace persisted namespace pins. Namespace source resolution checks public
|
|
228
|
+
commit availability before its effects; a local commit is not thereby published to GitHub.
|
|
229
|
+
|
|
230
|
+
── platform admin ────────────────────────────────────────────────────────────────────────────────
|
|
231
|
+
|
|
232
|
+
The first human. A fresh installation has none — nothing seeds an admission list, and this command is
|
|
233
|
+
why it does not have to. It is unattended, prompts for nothing, and changes nothing on a re-run:
|
|
234
|
+
|
|
235
|
+
1. find the principal holding that mailbox, or invite one
|
|
236
|
+
2. grant it the CROSS-APP \`admin\` role, unless it already holds one
|
|
237
|
+
3. issue ONE password enrollment and print its URL
|
|
238
|
+
|
|
239
|
+
The grant is cross-app (\`app: null\`) and that is load-bearing: grants filter to the calling app, so an
|
|
240
|
+
\`admin\` grant scoped to the console's own app id leaves Delivery and Operations working while the
|
|
241
|
+
Access plane refuses. A re-run invites nobody twice, grants nothing twice and issues no second
|
|
242
|
+
enrollment — a password that is already set, or an enrollment already outstanding, is REPORTED. Pass
|
|
243
|
+
\`${REISSUE_FLAG}\` to replace an outstanding one, which is the way out when the first expired unopened.
|
|
244
|
+
|
|
245
|
+
The enrollment URL is a credential: it is printed once, on a line of its own, and stored nowhere.
|
|
246
|
+
|
|
247
|
+
\`install\` reports the host this needs as \`✓ iam <host>\` and ends with this command ready to run.
|
|
248
|
+
|
|
249
|
+
Options (a flag beats the environment variable beside it):
|
|
250
|
+
|
|
251
|
+
--email=<address> FABRIKA_PLATFORM_ADMIN_EMAIL the administrator's mailbox
|
|
252
|
+
--iam-host=<host> FABRIKA_PLATFORM_IAM_HOST IAM's public hostname, as \`deploy\` names it
|
|
253
|
+
--scheme=<http|https> FABRIKA_PLATFORM_SCHEME default https
|
|
254
|
+
${REISSUE_FLAG} replace an outstanding enrollment
|
|
255
|
+
|
|
256
|
+
FABRIKA_IAM_PROVISIONING_KEY required the px_ admin key \`install\` printed, environment only
|
|
257
|
+
|
|
258
|
+
── platform upgrade ──────────────────────────────────────────────────────────────────────────────
|
|
259
|
+
|
|
260
|
+
Roll a RUNNING installation onto a newer published fabrika release. Five steps in the sidecar
|
|
261
|
+
repository \`platform init\` created, in one command: check that ${FABRIKA_REPOSITORY} carries the tag,
|
|
262
|
+
write it to \`fabrika.ref\`, commit it as \`chore: roll <installation> forward to fabrika <tag>\`, push —
|
|
263
|
+
and the push is what triggers the pipeline — then print the run's URL and watch it to its conclusion.
|
|
264
|
+
|
|
265
|
+
The run URL is printed BEFORE the watch begins, so an operator who closes the terminal can come back
|
|
266
|
+
to it. The generated workflow QUEUES behind an earlier roll rather than cancelling it, so a run may sit
|
|
267
|
+
\`queued\` for a while; that is reported, not waited on silently.
|
|
268
|
+
|
|
269
|
+
A published TAG stays the only acceptable pin (ADR-0025): a branch or a commit SHA is refused here,
|
|
270
|
+
before anything is contacted, and again by the workflow at run time. It also refuses a directory that is
|
|
271
|
+
not a checkout, a checkout not on \`${SIDECAR_BRANCH}\` or tracking no upstream (the workflow triggers on
|
|
272
|
+
\`push: branches: [${SIDECAR_BRANCH}]\`, so a push from anywhere else deploys nothing), a sidecar with uncommitted
|
|
273
|
+
changes to tracked files (the commit stages \`fabrika.ref\` alone), and a pin already equal to \`--to\` —
|
|
274
|
+
though a pin that is committed and NEVER PUSHED says so instead, because that is what a failed push
|
|
275
|
+
leaves behind and no deploy happened.
|
|
276
|
+
|
|
277
|
+
A tag lookup that FAILS is a third answer and never "no such tag": a rate limit or an expired \`gh\`
|
|
278
|
+
login reported as a missing tag sends an operator to check the wrong repository.
|
|
279
|
+
|
|
280
|
+
--to=<tag> required the published fabrika release to roll onto, e.g. v0.1.0
|
|
281
|
+
<installation> names the sidecar \`platform init\` created: the checkout
|
|
282
|
+
./fabrika-zerops-<installation> under the current directory
|
|
283
|
+
--sidecar=<path> a checkout somewhere else — anything that is not shaped
|
|
284
|
+
\`<owner>/<name>\`, so ./owner/name names a directory
|
|
285
|
+
--sidecar=<owner>/<name> a GitHub repository, cloned into a temporary directory
|
|
286
|
+
whose path is printed
|
|
287
|
+
${UPGRADE_DRY_RUN_FLAG} report the roll it would commit and stop
|
|
288
|
+
|
|
289
|
+
Name the installation or the sidecar; naming both is fine and the sidecar wins. This command holds no
|
|
290
|
+
credential of its own: \`gh\` and \`git\` carry the operator's own login, which is also why it runs on a
|
|
291
|
+
laptop and not in the pipeline it triggers.
|
|
292
|
+
`
|
|
293
|
+
|
|
294
|
+
/** No cancellation source exists behind a CLI invocation; the ports all take a signal regardless. */
|
|
295
|
+
const neverAborted = (): AbortSignal => new AbortController().signal
|
|
296
|
+
|
|
297
|
+
const runPlan = (argv: readonly string[]): void => {
|
|
298
|
+
if (argv.length > 0) {
|
|
299
|
+
throw new Error(`Unexpected Zerops plan arguments: ${argv.map(describeArgument).join(' ')}`)
|
|
300
|
+
}
|
|
301
|
+
const artifacts = generatedArtifacts()
|
|
302
|
+
for (const artifact of artifacts) {
|
|
303
|
+
assertArtifactMatchesSchema(artifact)
|
|
304
|
+
console.info(artifact.path)
|
|
305
|
+
}
|
|
306
|
+
console.info(`${artifacts.length} Zerops installation artifact(s) validated`)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** The one collaborator both the deploy and the install reach IAM's provisioning surface through. */
|
|
310
|
+
const consoleSchemaReconciler: SchemaReconciler = (call) =>
|
|
311
|
+
reconcileSchema({
|
|
312
|
+
url: call.url,
|
|
313
|
+
app: call.app,
|
|
314
|
+
schema: call.schema,
|
|
315
|
+
...(call.returnOrigins === undefined ? {} : { returnOrigins: call.returnOrigins }),
|
|
316
|
+
...(call.adminKey === undefined ? {} : { adminKey: call.adminKey }),
|
|
317
|
+
signal: call.signal,
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
const runDeploy = async (argv: readonly string[]): Promise<void> => {
|
|
321
|
+
const input = parsePlatformDeployArgs(argv, process.env)
|
|
322
|
+
await deployPlatform(input, {
|
|
323
|
+
prepareSource: prepareInstallationSource,
|
|
324
|
+
uploadFetch: (url, init) => fetch(url, init),
|
|
325
|
+
api: createZeropsApi({
|
|
326
|
+
token: input.accessToken,
|
|
327
|
+
...(input.apiBaseUrl === undefined ? {} : { baseUrl: input.apiBaseUrl }),
|
|
328
|
+
}),
|
|
329
|
+
reconcileSchema: consoleSchemaReconciler,
|
|
330
|
+
sleep: defaultSleep,
|
|
331
|
+
log: consoleDeployLog(),
|
|
332
|
+
signal: neverAborted(),
|
|
333
|
+
})
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const runAdmin = async (argv: readonly string[]): Promise<void> => {
|
|
337
|
+
const input = parsePlatformAdminArgs(argv, process.env)
|
|
338
|
+
await runPlatformAdmin(
|
|
339
|
+
input,
|
|
340
|
+
consoleAdminCollaborators(input, {
|
|
341
|
+
step: (title) => consoleStep(title),
|
|
342
|
+
info: (message) => consoleInfo(message),
|
|
343
|
+
warn: (message) => consoleWarn(message),
|
|
344
|
+
ok: (message) => consoleOk(message),
|
|
345
|
+
}),
|
|
346
|
+
)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const runPlatformInstall = async (argv: readonly string[]): Promise<void> => {
|
|
350
|
+
const input = parsePlatformInstallArgs(argv, process.env)
|
|
351
|
+
await runInstall(input, consoleInstallCollaborators(input, consoleSchemaReconciler))
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const runPlatformUpgrade = async (argv: readonly string[]): Promise<void> => {
|
|
355
|
+
await runUpgrade(parsePlatformUpgradeArgs(argv), consoleUpgradeCollaborators())
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export const installationCli: InstallationCli = {
|
|
359
|
+
provider: 'zerops',
|
|
360
|
+
commands: ['install', 'init', 'plan', 'deploy', 'admin', 'upgrade'],
|
|
361
|
+
usage: USAGE,
|
|
362
|
+
run: async (command: InstallationCommand, argv: readonly string[]) => {
|
|
363
|
+
if (command === 'install') {
|
|
364
|
+
await runPlatformInstall(argv)
|
|
365
|
+
return
|
|
366
|
+
}
|
|
367
|
+
if (command === 'init') {
|
|
368
|
+
const input = parseInitArgs(argv, process.env)
|
|
369
|
+
await runInit(input, consoleInitCollaborators(input))
|
|
370
|
+
return
|
|
371
|
+
}
|
|
372
|
+
if (command === 'plan') {
|
|
373
|
+
runPlan(argv)
|
|
374
|
+
return
|
|
375
|
+
}
|
|
376
|
+
if (command === 'deploy') {
|
|
377
|
+
await runDeploy(argv)
|
|
378
|
+
return
|
|
379
|
+
}
|
|
380
|
+
if (command === 'admin') {
|
|
381
|
+
await runAdmin(argv)
|
|
382
|
+
return
|
|
383
|
+
}
|
|
384
|
+
if (command === 'upgrade') {
|
|
385
|
+
await runPlatformUpgrade(argv)
|
|
386
|
+
return
|
|
387
|
+
}
|
|
388
|
+
throw new Error(`Zerops installation does not support \`platform ${command}\``)
|
|
389
|
+
},
|
|
390
|
+
}
|