@fabrika/provider-zerops 0.3.1 → 0.3.3
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 +6 -6
- package/src/cli-args.ts +2 -1
- package/src/descriptor.ts +45 -1
- package/src/installation/admin-options.ts +4 -32
- package/src/installation/deploy-options.ts +6 -34
- package/src/installation/index.ts +41 -17
- package/src/installation/init-options.ts +148 -0
- package/src/installation/init.ts +94 -221
- package/src/installation/install-collaborators.ts +2 -3
- package/src/installation/install-options.ts +6 -43
- package/src/installation/install.ts +7 -31
- package/src/installation/sidecar.ts +4 -1
- package/src/installation/upgrade-options.ts +3 -12
- package/src/managed-env.ts +11 -3
- package/src/manifest.ts +6 -3
- package/src/provider.ts +1 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fabrika/provider-zerops",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -43,11 +43,11 @@
|
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"zod": "^3.25.76",
|
|
46
|
-
"@fabrika/auth": "^0.3.
|
|
47
|
-
"@fabrika/auth-core": "^0.3.
|
|
48
|
-
"@fabrika/installation-init": "^0.3.
|
|
49
|
-
"@fabrika/provider-contract": "^0.3.
|
|
50
|
-
"@fabrika/proxy-core": "^0.3.
|
|
46
|
+
"@fabrika/auth": "^0.3.3",
|
|
47
|
+
"@fabrika/auth-core": "^0.3.3",
|
|
48
|
+
"@fabrika/installation-init": "^0.3.3",
|
|
49
|
+
"@fabrika/provider-contract": "^0.3.3",
|
|
50
|
+
"@fabrika/proxy-core": "^0.3.3",
|
|
51
51
|
"yaml": "^2.9.1"
|
|
52
52
|
}
|
|
53
53
|
}
|
package/src/cli-args.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { describeArgument } from '@fabrika/installation-init'
|
|
1
2
|
export interface ZeropsCliArgs {
|
|
2
3
|
command: string | undefined
|
|
3
4
|
subcommand: string | undefined
|
|
@@ -71,7 +72,7 @@ export const parseZeropsCliArgs = (argv: readonly string[]): ZeropsCliArgs => {
|
|
|
71
72
|
} else if (subcommand === undefined && command === 'namespace') {
|
|
72
73
|
subcommand = arg
|
|
73
74
|
} else {
|
|
74
|
-
throw new Error(`Unexpected argument: ${arg}`)
|
|
75
|
+
throw new Error(`Unexpected argument: ${describeArgument(arg)}`)
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
78
|
|
package/src/descriptor.ts
CHANGED
|
@@ -128,8 +128,52 @@ export const selectZeropsDescriptorSetup = (parsed: ZeropsDescriptor, setup?: st
|
|
|
128
128
|
return selected
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
const inheritanceSchema = z.object({
|
|
132
|
+
zerops: z.array(z.intersection(
|
|
133
|
+
descriptorMappingSchema,
|
|
134
|
+
z.object({ setup: z.string(), extends: z.union([z.string(), z.array(z.string())]).optional() }),
|
|
135
|
+
)),
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The setup entries a deploy of `selected` applies: the selected one and, transitively, every setup it
|
|
140
|
+
* `extends`. Only these reach the running service; the other setups in the file never do.
|
|
141
|
+
*/
|
|
142
|
+
export const appliedZeropsSetups = (parsed: ZeropsDescriptor, selected: string): JsonValue[] => {
|
|
143
|
+
const document = inheritanceSchema.safeParse(parsed.document)
|
|
144
|
+
if (!document.success) return invalid('requires extends to name setups')
|
|
145
|
+
const applied: JsonValue[] = []
|
|
146
|
+
const visit = (name: string): void => {
|
|
147
|
+
const entry = document.data.zerops.find((candidate) => candidate.setup === name)
|
|
148
|
+
if (entry === undefined) return invalid(`extends unknown setup \`${name}\``)
|
|
149
|
+
if (applied.includes(entry)) return
|
|
150
|
+
applied.push(entry)
|
|
151
|
+
for (const parent of typeof entry.extends === 'string' ? [entry.extends] : entry.extends ?? []) visit(parent)
|
|
152
|
+
}
|
|
153
|
+
visit(selected)
|
|
154
|
+
return applied
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Refuse a selected setup that `extends` another without its own `run`: Zerops merges `extends` per
|
|
159
|
+
* whole section, and the deploy writes `FABRIKA_RELEASE` into `run`, which would replace the inherited one.
|
|
160
|
+
*/
|
|
161
|
+
export const assertOwnRunWhenExtending = (parsed: ZeropsDescriptor, selected: string): void => {
|
|
162
|
+
const [entry] = appliedZeropsSetups(parsed, selected)
|
|
163
|
+
if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) return
|
|
164
|
+
if (entry['extends'] !== undefined && entry['run'] === undefined) {
|
|
165
|
+
invalid(`setup \`${selected}\` extends another setup, so it must declare its own run section — the deploy writes FABRIKA_RELEASE into it`)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** The document Zerops executes: the selected setup and what it extends, which is exactly what the ownership check read. */
|
|
170
|
+
export const executionZeropsDocument = (parsed: ZeropsDescriptor, selected: string): { readonly [key: string]: JsonValue } => ({
|
|
171
|
+
...parsed.document,
|
|
172
|
+
zerops: appliedZeropsSetups(parsed, selected),
|
|
173
|
+
})
|
|
174
|
+
|
|
131
175
|
export const prepareZeropsDescriptor = (contents: string, setup?: string): PreparedZeropsDescriptor => {
|
|
132
176
|
const parsed = parseZeropsDescriptor(contents)
|
|
133
177
|
const selected = selectZeropsDescriptorSetup(parsed, setup)
|
|
134
|
-
return { contents: renderYaml(parsed
|
|
178
|
+
return { contents: renderYaml(executionZeropsDocument(parsed, selected)), setup: selected }
|
|
135
179
|
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// and prompts for nothing — and a missing required value is an error naming both the flag and the
|
|
9
9
|
// variable that would supply it.
|
|
10
10
|
|
|
11
|
+
import { describeArgument, readEnv, requiredSetting, setting } from '@fabrika/installation-init'
|
|
11
12
|
import { parsePlatformHostname, parsePlatformScheme } from './placement-options'
|
|
12
13
|
|
|
13
14
|
/** Every input `runPlatformAdmin` takes from the operator. */
|
|
@@ -36,35 +37,6 @@ const FLAGS = ['--email', '--iam-host', '--scheme'] as const
|
|
|
36
37
|
*/
|
|
37
38
|
export const REISSUE_FLAG = '--reissue'
|
|
38
39
|
|
|
39
|
-
const readFlag = (argv: readonly string[], name: string): string | undefined => {
|
|
40
|
-
const prefix = `${name}=`
|
|
41
|
-
const matches = argv.filter((arg) => arg.startsWith(prefix))
|
|
42
|
-
if (matches.length > 1) {
|
|
43
|
-
throw new Error(`${name} was given more than once`)
|
|
44
|
-
}
|
|
45
|
-
const value = matches[0]?.slice(prefix.length)
|
|
46
|
-
return value === undefined || value.trim() === '' ? undefined : value.trim()
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const readEnv = (env: Record<string, string | undefined>, name: string): string | undefined => {
|
|
50
|
-
const value = env[name]
|
|
51
|
-
return value === undefined || value.trim() === '' ? undefined : value.trim()
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const setting = (
|
|
55
|
-
argv: readonly string[],
|
|
56
|
-
env: Record<string, string | undefined>,
|
|
57
|
-
flag: string,
|
|
58
|
-
variable: string,
|
|
59
|
-
): string | undefined => readFlag(argv, flag) ?? readEnv(env, variable)
|
|
60
|
-
|
|
61
|
-
const required = (value: string | undefined, flag: string, variable: string): string => {
|
|
62
|
-
if (value === undefined) {
|
|
63
|
-
throw new Error(`${flag}=<value> or ${variable} is required`)
|
|
64
|
-
}
|
|
65
|
-
return value
|
|
66
|
-
}
|
|
67
|
-
|
|
68
40
|
const assertKnownArguments = (argv: readonly string[]): void => {
|
|
69
41
|
for (const arg of argv) {
|
|
70
42
|
if (arg === REISSUE_FLAG) {
|
|
@@ -73,7 +45,7 @@ const assertKnownArguments = (argv: readonly string[]): void => {
|
|
|
73
45
|
const name = arg.split('=')[0] ?? arg
|
|
74
46
|
if (!FLAGS.some((flag) => flag === name) || !arg.includes('=')) {
|
|
75
47
|
throw new Error(
|
|
76
|
-
`unexpected argument
|
|
48
|
+
`unexpected argument ${describeArgument(arg)}. Credentials are read from the environment and have no flag; run `
|
|
77
49
|
+ '`fabrika platform admin --provider=zerops --help` for the surface',
|
|
78
50
|
)
|
|
79
51
|
}
|
|
@@ -92,7 +64,7 @@ const readScheme = (argv: readonly string[], env: Record<string, string | undefi
|
|
|
92
64
|
* composed from it and `--scheme`, so a value carrying either would compose a second one.
|
|
93
65
|
*/
|
|
94
66
|
const readIamHost = (argv: readonly string[], env: Record<string, string | undefined>): string => {
|
|
95
|
-
const host =
|
|
67
|
+
const host = requiredSetting(setting(argv, env, '--iam-host', 'FABRIKA_PLATFORM_IAM_HOST'), '--iam-host', 'FABRIKA_PLATFORM_IAM_HOST')
|
|
96
68
|
return parsePlatformHostname(host)
|
|
97
69
|
}
|
|
98
70
|
|
|
@@ -119,7 +91,7 @@ export const parsePlatformAdminArgs = (
|
|
|
119
91
|
): PlatformAdminInput => {
|
|
120
92
|
assertKnownArguments(argv)
|
|
121
93
|
return {
|
|
122
|
-
email:
|
|
94
|
+
email: requiredSetting(setting(argv, env, '--email', 'FABRIKA_PLATFORM_ADMIN_EMAIL'), '--email', 'FABRIKA_PLATFORM_ADMIN_EMAIL'),
|
|
123
95
|
iamHost: readIamHost(argv, env),
|
|
124
96
|
scheme: readScheme(argv, env),
|
|
125
97
|
provisioningKey: provisioningKey(env),
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// The command is unattended by construction: nothing here prompts, and a missing required value is an
|
|
13
13
|
// error naming both the flag and the variable that would supply it.
|
|
14
14
|
|
|
15
|
+
import { describeArgument, readEnv, requiredSetting, setting } from '@fabrika/installation-init'
|
|
15
16
|
import { parsePlatformHosts, parsePlatformScheme } from './placement-options'
|
|
16
17
|
import { readInstallationSourceDir } from './source-options'
|
|
17
18
|
|
|
@@ -52,35 +53,6 @@ const FLAGS = [
|
|
|
52
53
|
'--source-dir',
|
|
53
54
|
] as const
|
|
54
55
|
|
|
55
|
-
const readFlag = (argv: readonly string[], name: string): string | undefined => {
|
|
56
|
-
const prefix = `${name}=`
|
|
57
|
-
const matches = argv.filter((arg) => arg.startsWith(prefix))
|
|
58
|
-
if (matches.length > 1) {
|
|
59
|
-
throw new Error(`${name} was given more than once`)
|
|
60
|
-
}
|
|
61
|
-
const value = matches[0]?.slice(prefix.length)
|
|
62
|
-
return value === undefined || value.trim() === '' ? undefined : value.trim()
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const readEnv = (env: Record<string, string | undefined>, name: string): string | undefined => {
|
|
66
|
-
const value = env[name]
|
|
67
|
-
return value === undefined || value.trim() === '' ? undefined : value.trim()
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const setting = (
|
|
71
|
-
argv: readonly string[],
|
|
72
|
-
env: Record<string, string | undefined>,
|
|
73
|
-
flag: string,
|
|
74
|
-
variable: string,
|
|
75
|
-
): string | undefined => readFlag(argv, flag) ?? readEnv(env, variable)
|
|
76
|
-
|
|
77
|
-
const required = (value: string | undefined, flag: string, variable: string): string => {
|
|
78
|
-
if (value === undefined) {
|
|
79
|
-
throw new Error(`${flag}=<value> or ${variable} is required`)
|
|
80
|
-
}
|
|
81
|
-
return value
|
|
82
|
-
}
|
|
83
|
-
|
|
84
56
|
const assertKnownArguments = (argv: readonly string[]): void => {
|
|
85
57
|
for (const arg of argv) {
|
|
86
58
|
if (arg === '--dry-run') {
|
|
@@ -89,7 +61,7 @@ const assertKnownArguments = (argv: readonly string[]): void => {
|
|
|
89
61
|
const name = arg.split('=')[0] ?? arg
|
|
90
62
|
if (!FLAGS.some((flag) => flag === name) || !arg.includes('=')) {
|
|
91
63
|
throw new Error(
|
|
92
|
-
`unexpected argument
|
|
64
|
+
`unexpected argument ${describeArgument(arg)}. Credentials are read from the environment and have no flag; run `
|
|
93
65
|
+ '`fabrika platform deploy --provider=zerops --help` for the surface',
|
|
94
66
|
)
|
|
95
67
|
}
|
|
@@ -123,7 +95,7 @@ const readProject = (argv: readonly string[], env: Record<string, string | undef
|
|
|
123
95
|
if (name === undefined) {
|
|
124
96
|
throw new Error('--project-id=<id> or FABRIKA_ZEROPS_PROJECT_ID is required (or --project-name with --client-id)')
|
|
125
97
|
}
|
|
126
|
-
const clientId =
|
|
98
|
+
const clientId = requiredSetting(setting(argv, env, '--client-id', 'FABRIKA_ZEROPS_CLIENT_ID'), '--client-id', 'FABRIKA_ZEROPS_CLIENT_ID')
|
|
127
99
|
return { kind: 'name', clientId, name }
|
|
128
100
|
}
|
|
129
101
|
|
|
@@ -140,12 +112,12 @@ export const parsePlatformDeployArgs = (
|
|
|
140
112
|
return {
|
|
141
113
|
project: readProject(argv, env),
|
|
142
114
|
// A dry run reads, and reading still needs a token; it is the one credential it cannot do without.
|
|
143
|
-
accessToken:
|
|
144
|
-
environment:
|
|
115
|
+
accessToken: requiredSetting(readEnv(env, 'FABRIKA_ZEROPS_ACCESS_TOKEN'), '(no flag)', 'FABRIKA_ZEROPS_ACCESS_TOKEN'),
|
|
116
|
+
environment: requiredSetting(setting(argv, env, '--env', 'FABRIKA_PLATFORM_ENVIRONMENT'), '--env', 'FABRIKA_PLATFORM_ENVIRONMENT'),
|
|
145
117
|
// Not required for a dry run: it authenticates a write this mode never makes.
|
|
146
118
|
iamAdminKey: dryRun
|
|
147
119
|
? readEnv(env, 'FABRIKA_IAM_PROVISIONING_KEY') ?? ''
|
|
148
|
-
:
|
|
120
|
+
: requiredSetting(readEnv(env, 'FABRIKA_IAM_PROVISIONING_KEY'), '(no flag)', 'FABRIKA_IAM_PROVISIONING_KEY'),
|
|
149
121
|
scheme: readScheme(argv, env),
|
|
150
122
|
dryRun,
|
|
151
123
|
...(hosts === undefined ? {} : { hosts }),
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { reconcileSchema } from '@fabrika/auth'
|
|
2
|
-
import {
|
|
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'
|
|
3
10
|
import type { SchemaReconciler } from '@fabrika/provider-contract'
|
|
4
11
|
import type { InstallationCli, InstallationCommand } from '@fabrika/provider-contract/installation'
|
|
5
12
|
import { createZeropsApi, defaultSleep } from '../index'
|
|
@@ -7,9 +14,10 @@ import { consoleAdminCollaborators, runPlatformAdmin } from './admin'
|
|
|
7
14
|
import { parsePlatformAdminArgs, REISSUE_FLAG } from './admin-options'
|
|
8
15
|
import { deployPlatform, PLATFORM_CONCURRENT_DEPLOY, PLATFORM_DEPLOY_ORDER, PLATFORM_SEQUENTIAL_DEPLOY } from './deploy'
|
|
9
16
|
import { parsePlatformDeployArgs } from './deploy-options'
|
|
10
|
-
import { consoleInitCollaborators,
|
|
17
|
+
import { consoleInitCollaborators, runInit } from './init'
|
|
18
|
+
import { parseInitArgs } from './init-options'
|
|
11
19
|
import { consoleInstallCollaborators, runInstall } from './install'
|
|
12
|
-
import { parsePlatformInstallArgs
|
|
20
|
+
import { parsePlatformInstallArgs } from './install-options'
|
|
13
21
|
import { prepareInstallationSource } from './installation-source'
|
|
14
22
|
import { consoleDeployLog } from './log'
|
|
15
23
|
import { generatedArtifacts } from './topology/artifacts'
|
|
@@ -34,7 +42,7 @@ const USAGE = `Zerops installation
|
|
|
34
42
|
|
|
35
43
|
Commands:
|
|
36
44
|
fabrika platform install --provider=zerops [options]
|
|
37
|
-
fabrika platform init --provider=zerops <installation>
|
|
45
|
+
fabrika platform init --provider=zerops <installation> --tag=<tag> --project-id=<id> [options]
|
|
38
46
|
fabrika platform plan --provider=zerops
|
|
39
47
|
fabrika platform deploy --provider=zerops [options]
|
|
40
48
|
fabrika platform admin --provider=zerops --email=<address> --iam-host=<host> [--scheme=<http|https>] [${REISSUE_FLAG}]
|
|
@@ -109,11 +117,12 @@ Creates and maintains the operator's SIDECAR REPOSITORY: the GitHub pipeline tha
|
|
|
109
117
|
credentials. ADR-0025 puts that pipeline in a repository the operator owns — fabrika ships the
|
|
110
118
|
generator, never the pipeline, because one public repository cannot hold every account's credentials.
|
|
111
119
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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.
|
|
117
126
|
|
|
118
127
|
Init writes nothing to Zerops. It reads control and proxy only to print the console and IAM origins
|
|
119
128
|
the installation records, and prints none it cannot verify. Public repositories deploy without a
|
|
@@ -129,16 +138,30 @@ file afterwards.
|
|
|
129
138
|
<installation> names this installation: the GitHub Environment, the default
|
|
130
139
|
repository \`contember/fabrika-zerops-<installation>\`, and the
|
|
131
140
|
local checkout directory
|
|
132
|
-
|
|
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
|
|
133
156
|
|
|
134
157
|
It writes TWO secrets into that Environment and GENERATES NEITHER — both already belong to the
|
|
135
|
-
installation, so a value invented here would not be one it accepts:
|
|
158
|
+
installation, so a value invented here would not be one it accepts. Both are environment only:
|
|
136
159
|
|
|
137
|
-
|
|
138
|
-
|
|
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
|
|
139
163
|
|
|
140
|
-
Each is
|
|
141
|
-
over \`gh\` stdin, and never printed and never written to disk.
|
|
164
|
+
Each is sent to GitHub over \`gh\` stdin, and never printed and never written to disk.
|
|
142
165
|
|
|
143
166
|
The generated pipeline pins a published TAG of contember/fabrika-platform and refuses a branch, at
|
|
144
167
|
init and again in the workflow. It uploads that checkout's exact committed archive to every installation service.
|
|
@@ -273,7 +296,7 @@ const neverAborted = (): AbortSignal => new AbortController().signal
|
|
|
273
296
|
|
|
274
297
|
const runPlan = (argv: readonly string[]): void => {
|
|
275
298
|
if (argv.length > 0) {
|
|
276
|
-
throw new Error(`Unexpected Zerops plan arguments: ${argv.join(' ')}`)
|
|
299
|
+
throw new Error(`Unexpected Zerops plan arguments: ${argv.map(describeArgument).join(' ')}`)
|
|
277
300
|
}
|
|
278
301
|
const artifacts = generatedArtifacts()
|
|
279
302
|
for (const artifact of artifacts) {
|
|
@@ -342,7 +365,8 @@ export const installationCli: InstallationCli = {
|
|
|
342
365
|
return
|
|
343
366
|
}
|
|
344
367
|
if (command === 'init') {
|
|
345
|
-
|
|
368
|
+
const input = parseInitArgs(argv, process.env)
|
|
369
|
+
await runInit(input, consoleInitCollaborators(input))
|
|
346
370
|
return
|
|
347
371
|
}
|
|
348
372
|
if (command === 'plan') {
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// The CLI surface of `fabrika platform init --provider=zerops`, parsed once.
|
|
2
|
+
//
|
|
3
|
+
// The same two rules `deploy-options.ts` states: every CREDENTIAL comes from the environment and only
|
|
4
|
+
// from the environment, and everything else may come from a flag OR the environment, flag first.
|
|
5
|
+
// Nothing here prompts — a missing required value is an error naming both the flag and the variable —
|
|
6
|
+
// so a pipeline or an agent drives init end to end. Its confirmations stay, answered by `--yes`.
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
CHECKOUT_FLAG,
|
|
10
|
+
type CommandEnv,
|
|
11
|
+
describeArgument,
|
|
12
|
+
readEnv,
|
|
13
|
+
readFlag,
|
|
14
|
+
requiredSetting,
|
|
15
|
+
setting,
|
|
16
|
+
sidecarCheckoutDir,
|
|
17
|
+
UNATTENDED_FLAG,
|
|
18
|
+
} from '@fabrika/installation-init'
|
|
19
|
+
import { parsePlatformHosts } from './placement-options'
|
|
20
|
+
import { assertPinnedTag, defaultCheckoutName, defaultSidecarRepo } from './sidecar'
|
|
21
|
+
|
|
22
|
+
/** The name every bypass in this platform is gated on, and therefore the one name an installation cannot take. */
|
|
23
|
+
const LOCAL_ENVIRONMENT = 'local'
|
|
24
|
+
|
|
25
|
+
/** Every input `runInit` takes from the operator. */
|
|
26
|
+
export interface InitInput {
|
|
27
|
+
/** Names the GitHub Environment, the default repository and the local checkout directory. */
|
|
28
|
+
readonly installation: string
|
|
29
|
+
readonly repo: string
|
|
30
|
+
/** The sidecar's local checkout: `--checkout`, or `fabrika-zerops-<installation>` in the working directory. */
|
|
31
|
+
readonly checkoutDir: string
|
|
32
|
+
/** The published fabrika-platform tag the sidecar pins in `fabrika.ref`. */
|
|
33
|
+
readonly fabrikaRef: string
|
|
34
|
+
readonly projectId: string
|
|
35
|
+
/** The name this installation calls itself — written to every service as `ENVIRONMENT`. */
|
|
36
|
+
readonly environmentName: string
|
|
37
|
+
/** All three or none: none derives them from the proxy's Zerops subdomains. */
|
|
38
|
+
readonly hosts?: { readonly iam: string; readonly control: string; readonly operations: string }
|
|
39
|
+
/** Region API base, when the installation is not on the default one. */
|
|
40
|
+
readonly apiBaseUrl?: string
|
|
41
|
+
/** The Zerops INTEGRATION token `platform install` minted. Environment only. */
|
|
42
|
+
readonly accessToken: string
|
|
43
|
+
/** The `px_` key this installation's IAM was seeded with. Environment only. */
|
|
44
|
+
readonly provisioningKey: string
|
|
45
|
+
/** `--yes`: answer every confirmation yes. */
|
|
46
|
+
readonly unattended: boolean
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const FLAGS = ['--repo', CHECKOUT_FLAG, '--tag', '--project-id', '--env', '--iam-host', '--console-host', '--operations-host'] as const
|
|
50
|
+
|
|
51
|
+
const REPO_PATTERN = /^[^/\s]+\/[^/\s]+$/
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The installation's environment name, refused when it claims to be local.
|
|
55
|
+
*
|
|
56
|
+
* The same refusal the services enforce at boot: `local` is the value `localDevLogin`, the ephemeral
|
|
57
|
+
* signing key and the credential-less caller path are all gated on, and a deployed installation is
|
|
58
|
+
* never a loopback address.
|
|
59
|
+
*/
|
|
60
|
+
export const checkedEnvironmentName = (declared: string): string => {
|
|
61
|
+
const trimmed = declared.trim()
|
|
62
|
+
if (trimmed === '') {
|
|
63
|
+
throw new Error('an installation environment name is required — it is written to every service as ENVIRONMENT')
|
|
64
|
+
}
|
|
65
|
+
if (trimmed === LOCAL_ENVIRONMENT) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`\`${LOCAL_ENVIRONMENT}\` is refused: it is the name every development bypass is gated on, and a deployed `
|
|
68
|
+
+ 'installation is not a loopback address. Name it after the installation instead',
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
return trimmed
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const assertKnownArguments = (argv: readonly string[]): void => {
|
|
75
|
+
for (const arg of argv) {
|
|
76
|
+
if (arg === UNATTENDED_FLAG || !arg.startsWith('-')) {
|
|
77
|
+
continue
|
|
78
|
+
}
|
|
79
|
+
const name = arg.split('=')[0] ?? arg
|
|
80
|
+
if (!FLAGS.some((flag) => flag === name) || !arg.includes('=')) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`unexpected argument ${describeArgument(arg)}. Credentials are read from the environment and have no flag; run `
|
|
83
|
+
+ '`fabrika platform init --provider=zerops --help` for the surface',
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const readInstallation = (argv: readonly string[]): string => {
|
|
90
|
+
const positional = argv.filter((arg) => !arg.startsWith('-')).map((arg) => arg.trim())
|
|
91
|
+
if (positional.length > 1) {
|
|
92
|
+
throw new Error('unexpected second positional argument: name ONE installation')
|
|
93
|
+
}
|
|
94
|
+
const installation = positional[0]
|
|
95
|
+
if (installation === undefined || installation === '') {
|
|
96
|
+
throw new Error('Zerops installation init requires an installation name, for example `fabrika platform init --provider=zerops test`')
|
|
97
|
+
}
|
|
98
|
+
return installation
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const readRepo = (argv: readonly string[], installation: string): string => {
|
|
102
|
+
const repo = readFlag(argv, '--repo')
|
|
103
|
+
if (repo === undefined) {
|
|
104
|
+
return defaultSidecarRepo(installation)
|
|
105
|
+
}
|
|
106
|
+
if (!REPO_PATTERN.test(repo)) {
|
|
107
|
+
throw new Error(`\`${repo}\` is not a <owner>/<name> repository`)
|
|
108
|
+
}
|
|
109
|
+
return repo
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** A credential, with the one message that says why there is no flag for it. */
|
|
113
|
+
const credential = (env: CommandEnv, variable: string, what: string): string => {
|
|
114
|
+
const value = readEnv(env, variable)
|
|
115
|
+
if (value === undefined) {
|
|
116
|
+
throw new Error(`${variable} is required: ${what}. It has no flag — a credential on a command line lands in logs and shell history`)
|
|
117
|
+
}
|
|
118
|
+
return value
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Parse the command's arguments and environment into the one input `runInit` takes. */
|
|
122
|
+
export const parseInitArgs = (argv: readonly string[], env: CommandEnv, cwd: string = process.cwd()): InitInput => {
|
|
123
|
+
assertKnownArguments(argv)
|
|
124
|
+
const installation = readInstallation(argv)
|
|
125
|
+
const hosts = parsePlatformHosts({
|
|
126
|
+
iam: setting(argv, env, '--iam-host', 'FABRIKA_PLATFORM_IAM_HOST'),
|
|
127
|
+
control: setting(argv, env, '--console-host', 'FABRIKA_PLATFORM_CONSOLE_HOST'),
|
|
128
|
+
operations: setting(argv, env, '--operations-host', 'FABRIKA_PLATFORM_OPERATIONS_HOST'),
|
|
129
|
+
})
|
|
130
|
+
const apiBaseUrl = readEnv(env, 'FABRIKA_ZEROPS_API_URL')
|
|
131
|
+
return {
|
|
132
|
+
installation,
|
|
133
|
+
repo: readRepo(argv, installation),
|
|
134
|
+
checkoutDir: sidecarCheckoutDir(readFlag(argv, CHECKOUT_FLAG), defaultCheckoutName(installation), cwd),
|
|
135
|
+
fabrikaRef: assertPinnedTag(requiredSetting(setting(argv, env, '--tag', 'FABRIKA_PLATFORM_TAG'), '--tag', 'FABRIKA_PLATFORM_TAG')),
|
|
136
|
+
projectId: requiredSetting(setting(argv, env, '--project-id', 'FABRIKA_ZEROPS_PROJECT_ID'), '--project-id', 'FABRIKA_ZEROPS_PROJECT_ID'),
|
|
137
|
+
environmentName: checkedEnvironmentName(setting(argv, env, '--env', 'FABRIKA_PLATFORM_ENVIRONMENT') ?? installation),
|
|
138
|
+
accessToken: credential(
|
|
139
|
+
env,
|
|
140
|
+
'FABRIKA_ZEROPS_ACCESS_TOKEN',
|
|
141
|
+
"the Zerops INTEGRATION token `platform install` minted, read from the `control` service's environment",
|
|
142
|
+
),
|
|
143
|
+
provisioningKey: credential(env, 'FABRIKA_IAM_PROVISIONING_KEY', 'the px_ key `platform install` printed'),
|
|
144
|
+
unattended: argv.includes(UNATTENDED_FLAG),
|
|
145
|
+
...(hosts === undefined ? {} : { hosts }),
|
|
146
|
+
...(apiBaseUrl === undefined ? {} : { apiBaseUrl }),
|
|
147
|
+
}
|
|
148
|
+
}
|