@fabrika/provider-zerops 0.0.26 → 0.3.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 +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 +0 -5
- package/src/cli.ts +3 -28
- package/src/collaborators.ts +2 -4
- package/src/compile.ts +75 -20
- package/src/control.ts +217 -447
- package/src/descriptor.ts +135 -0
- package/src/index.ts +24 -128
- package/src/installation/admin-options.ts +128 -0
- package/src/installation/admin.ts +123 -0
- package/src/installation/deploy-options.ts +155 -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 +366 -0
- package/src/installation/init.ts +521 -0
- package/src/installation/install-collaborators.ts +28 -0
- package/src/installation/install-environment.ts +193 -0
- package/src/installation/install-options.ts +193 -0
- package/src/installation/install-project.ts +201 -0
- package/src/installation/install-provisioning.ts +185 -0
- package/src/installation/install.ts +286 -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 +122 -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 +108 -0
- package/src/installation/upgrade-output.ts +31 -0
- package/src/installation/upgrade.ts +373 -0
- package/src/managed-env.ts +105 -0
- package/src/manifest.ts +208 -315
- 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 -1260
- package/src/plan.ts +21 -23
- package/src/provider.ts +399 -254
- package/src/service-references.ts +28 -0
- 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,135 @@
|
|
|
1
|
+
import type { JsonValue } from '@fabrika/provider-contract'
|
|
2
|
+
import { isAlias, isMap, isNode, isScalar, isSeq, parseAllDocuments } from 'yaml'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
import { renderYaml } from './compile'
|
|
5
|
+
|
|
6
|
+
export interface ZeropsDescriptor {
|
|
7
|
+
document: { readonly [key: string]: JsonValue }
|
|
8
|
+
setups: readonly string[]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface PreparedZeropsDescriptor {
|
|
12
|
+
contents: string
|
|
13
|
+
setup: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const invalid = (reason: string): never => {
|
|
17
|
+
throw new Error(`invalid fabrika manifest: zerops.yaml ${reason}`)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const descriptorJsonSchema: z.ZodType<JsonValue, z.ZodTypeDef, unknown> = z.lazy(() =>
|
|
21
|
+
z.union([
|
|
22
|
+
z.string(),
|
|
23
|
+
z.number(),
|
|
24
|
+
z.boolean(),
|
|
25
|
+
z.null(),
|
|
26
|
+
z.array(descriptorJsonSchema),
|
|
27
|
+
descriptorMappingSchema,
|
|
28
|
+
])
|
|
29
|
+
)
|
|
30
|
+
// Zod records drop __proto__; descriptor mappings must preserve every decoded key and its order.
|
|
31
|
+
const descriptorMappingSchema: z.ZodType<{ [key: string]: JsonValue }, z.ZodTypeDef, unknown> = z.preprocess(
|
|
32
|
+
(value) => typeof value === 'object' && value !== null && !Array.isArray(value) ? Object.entries(value) : null,
|
|
33
|
+
z.array(z.tuple([z.string(), descriptorJsonSchema])).transform((entries) => Object.fromEntries(entries)),
|
|
34
|
+
)
|
|
35
|
+
const phaseSchema = z.intersection(descriptorMappingSchema, z.object({ envVariables: z.record(z.string()).optional() }))
|
|
36
|
+
const setupSchema = z.intersection(
|
|
37
|
+
descriptorMappingSchema,
|
|
38
|
+
z.object({
|
|
39
|
+
setup: z.string().refine((value) => value.trim() !== ''),
|
|
40
|
+
build: phaseSchema.optional(),
|
|
41
|
+
run: phaseSchema.optional(),
|
|
42
|
+
}),
|
|
43
|
+
)
|
|
44
|
+
const descriptorDocumentSchema = z.intersection(descriptorMappingSchema, z.object({ zerops: z.array(setupSchema).min(1) }))
|
|
45
|
+
|
|
46
|
+
// Compare decimal significands, not binary floats: parsing must not silently round source digits.
|
|
47
|
+
const decimalIdentity = (source: string): string => {
|
|
48
|
+
const match = /^([+-]?)(\d*)(?:\.(\d*))?(?:[eE]([+-]?\d+))?$/.exec(source)
|
|
49
|
+
if (match === null) return invalid('contains an unsupported numeric representation')
|
|
50
|
+
const fraction = match[3] ?? ''
|
|
51
|
+
const digits = `${match[2] ?? ''}${fraction}`.replace(/^0+/, '')
|
|
52
|
+
if (digits === '') return '0'
|
|
53
|
+
const significant = digits.replace(/0+$/, '')
|
|
54
|
+
const exponent = BigInt(match[4] ?? '0') - BigInt(fraction.length) + BigInt(digits.length - significant.length)
|
|
55
|
+
return `${match[1] === '-' ? '-' : ''}${significant}e${exponent}`
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const decodeNode = (node: unknown): JsonValue => {
|
|
59
|
+
if (isAlias(node)) return invalid('must not contain aliases')
|
|
60
|
+
if (!isNode(node)) return invalid('contains an unsupported node')
|
|
61
|
+
if (node.tag !== undefined) return invalid('must not contain explicit tags')
|
|
62
|
+
if ('anchor' in node && node.anchor !== undefined) return invalid('must not contain anchors')
|
|
63
|
+
if (isMap(node)) {
|
|
64
|
+
const entries: Array<[string, JsonValue]> = []
|
|
65
|
+
const keys = new Set<string>()
|
|
66
|
+
for (const pair of node.items) {
|
|
67
|
+
const key = decodeNode(pair.key)
|
|
68
|
+
if (typeof key !== 'string') return invalid('requires string mapping keys')
|
|
69
|
+
if (key === '<<') return invalid('must not contain merge keys')
|
|
70
|
+
if (keys.has(key)) return invalid('must not contain duplicate keys')
|
|
71
|
+
keys.add(key)
|
|
72
|
+
entries.push([key, decodeNode(pair.value)])
|
|
73
|
+
}
|
|
74
|
+
return Object.fromEntries(entries)
|
|
75
|
+
}
|
|
76
|
+
if (isSeq(node)) return node.items.map(decodeNode)
|
|
77
|
+
if (!isScalar(node)) return invalid('contains an unsupported node')
|
|
78
|
+
const value: unknown = node.value
|
|
79
|
+
if (typeof value === 'bigint') {
|
|
80
|
+
const number = Number(value)
|
|
81
|
+
if (!Number.isSafeInteger(number)) return invalid('contains an inexact number')
|
|
82
|
+
return number
|
|
83
|
+
}
|
|
84
|
+
if (typeof value === 'number') {
|
|
85
|
+
if (!Number.isFinite(value) || (Number.isInteger(value) && !Number.isSafeInteger(value))) return invalid('contains an inexact number')
|
|
86
|
+
if (node.source === undefined || decimalIdentity(node.source) !== decimalIdentity(String(value))) return invalid('contains an inexact number')
|
|
87
|
+
return value
|
|
88
|
+
}
|
|
89
|
+
if (typeof value === 'string' && /\p{Surrogate}/u.test(value)) return invalid('contains an invalid Unicode scalar')
|
|
90
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean') return value
|
|
91
|
+
return invalid('contains an unsupported scalar')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const parseDocument = (contents: string, schema: 'core' | 'yaml-1.1'): JsonValue => {
|
|
95
|
+
const documents = parseAllDocuments(contents, { schema, intAsBigInt: true, merge: false, resolveKnownTags: false, uniqueKeys: true })
|
|
96
|
+
const document = documents[0]
|
|
97
|
+
if (documents.length !== 1 || document === undefined) return invalid('requires exactly one mapping document')
|
|
98
|
+
if (document.errors.length > 0 || document.warnings.length > 0) return invalid('is not parseable YAML without errors or warnings')
|
|
99
|
+
return decodeNode(document.contents)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export const parseZeropsDescriptor = (contents: string): ZeropsDescriptor => {
|
|
103
|
+
// Go readers fold or reject these raw characters; escaped string data remains supported.
|
|
104
|
+
if (/[\u007f-\u009f\u2028\u2029\ufffe\uffff]/u.test(contents)) return invalid('contains raw characters unsupported by YAML-1.1 readers')
|
|
105
|
+
const document = parseDocument(contents, 'core')
|
|
106
|
+
const legacy = parseDocument(contents, 'yaml-1.1')
|
|
107
|
+
if (JSON.stringify(document) !== JSON.stringify(legacy)) return invalid('contains ambiguous plain scalars; quote them explicitly')
|
|
108
|
+
const result = descriptorDocumentSchema.safeParse(document)
|
|
109
|
+
if (!result.success) {
|
|
110
|
+
const path = result.error.issues[0]?.path ?? []
|
|
111
|
+
if (path.includes('envVariables')) return invalid('requires envVariables string mappings')
|
|
112
|
+
if (path.includes('build') || path.includes('run')) return invalid('requires build and run mappings')
|
|
113
|
+
if (path.length > 1) return invalid('requires string setup names')
|
|
114
|
+
return invalid(path.length === 0 ? 'requires a mapping document' : 'requires a nonempty zerops sequence')
|
|
115
|
+
}
|
|
116
|
+
const setups: string[] = []
|
|
117
|
+
for (const entry of result.data.zerops) {
|
|
118
|
+
if (setups.includes(entry.setup)) return invalid('must not contain duplicate setup names')
|
|
119
|
+
setups.push(entry.setup)
|
|
120
|
+
}
|
|
121
|
+
return { document: result.data, setups }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export const selectZeropsDescriptorSetup = (parsed: ZeropsDescriptor, setup?: string): string => {
|
|
125
|
+
const selected = setup ?? (parsed.setups.length === 1 ? parsed.setups[0] : undefined)
|
|
126
|
+
if (selected === undefined) return invalid('requires an explicit setup selector for multiple setups')
|
|
127
|
+
if (!parsed.setups.includes(selected)) return invalid('does not contain the selected setup')
|
|
128
|
+
return selected
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export const prepareZeropsDescriptor = (contents: string, setup?: string): PreparedZeropsDescriptor => {
|
|
132
|
+
const parsed = parseZeropsDescriptor(contents)
|
|
133
|
+
const selected = selectZeropsDescriptorSetup(parsed, setup)
|
|
134
|
+
return { contents: renderYaml(parsed.document), setup: selected }
|
|
135
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
export {
|
|
2
|
-
asAppVersionStatus,
|
|
3
2
|
createZeropsApi,
|
|
4
3
|
waitForProcess,
|
|
5
4
|
ZEROPS_ACTIVE,
|
|
@@ -36,7 +35,6 @@ export { defineApp, isZeropsAppConfig, useSharedPostgres, zeropsSharedServiceHos
|
|
|
36
35
|
export type { ZeropsAuthoredAppConfig } from './authoring'
|
|
37
36
|
export { parseZeropsCliArgs } from './cli-args'
|
|
38
37
|
export type { ZeropsCliArgs } from './cli-args'
|
|
39
|
-
export { zeropsTargetCodec } from './codec'
|
|
40
38
|
export { defaultSleep, defaultZeropsCollaborators } from './collaborators'
|
|
41
39
|
export type { Sleeper, ZeropsCollaboratorFactory, ZeropsCollaborators } from './collaborators'
|
|
42
40
|
export {
|
|
@@ -49,10 +47,12 @@ export {
|
|
|
49
47
|
renderImportYaml,
|
|
50
48
|
renderYaml,
|
|
51
49
|
} from './compile'
|
|
52
|
-
export type { CompileInput, ZeropsImportDocument } from './compile'
|
|
53
|
-
export { createZeropsControlProvider, zeropsStoredTargetCodec } from './control'
|
|
54
|
-
export type {
|
|
50
|
+
export type { CompileInput, ZeropsImportDocument, ZeropsJsonImportDocument } from './compile'
|
|
51
|
+
export { createZeropsControlProvider, parseZeropsCheckpoint, zeropsStoredTargetCodec } from './control'
|
|
52
|
+
export type { ZeropsControlProviderOptions, ZeropsStoredTarget } from './control'
|
|
53
|
+
export { type PreparedZeropsDescriptor, prepareZeropsDescriptor } from './descriptor'
|
|
55
54
|
export {
|
|
55
|
+
assertFabrikaManifestCoordinates,
|
|
56
56
|
compileFabrikaManifest,
|
|
57
57
|
createZeropsArtifactSourceDescriptor,
|
|
58
58
|
FABRIKA_MANIFEST_VERSION,
|
|
@@ -69,7 +69,9 @@ export {
|
|
|
69
69
|
compileZeropsNamespaceTopology,
|
|
70
70
|
createZeropsNamespaceCapabilities,
|
|
71
71
|
createZeropsNamespaceOperator,
|
|
72
|
-
|
|
72
|
+
decodeZeropsNamespaceProxySource,
|
|
73
|
+
republishProxy,
|
|
74
|
+
verifyZeropsNamespaceProxySource,
|
|
73
75
|
ZEROPS_NAMESPACE_IAM_ISSUER_VARIABLE,
|
|
74
76
|
ZEROPS_NAMESPACE_IAM_KEY_VARIABLE,
|
|
75
77
|
ZEROPS_NAMESPACE_POSTGRES_HOSTNAME,
|
|
@@ -79,143 +81,37 @@ export {
|
|
|
79
81
|
zeropsNamespaceTargetCodec,
|
|
80
82
|
} from './namespace'
|
|
81
83
|
export type {
|
|
82
|
-
NamespaceProxyIamInput,
|
|
83
84
|
ZeropsNamespaceOperatorOptions,
|
|
84
85
|
ZeropsNamespaceOptions,
|
|
85
86
|
ZeropsNamespacePlanOptions,
|
|
86
87
|
ZeropsNamespacePostgres,
|
|
87
88
|
ZeropsNamespacePresetInput,
|
|
89
|
+
ZeropsNamespaceProxyBuild,
|
|
90
|
+
ZeropsNamespaceProxyDeployment,
|
|
91
|
+
ZeropsNamespaceProxySource,
|
|
88
92
|
ZeropsNamespacePublicAccess,
|
|
89
93
|
ZeropsNamespaceTarget,
|
|
90
94
|
ZeropsNamespaceTopology,
|
|
95
|
+
ZeropsProxyApi,
|
|
96
|
+
ZeropsProxyRepublishInput,
|
|
91
97
|
} from './namespace'
|
|
92
|
-
export {
|
|
93
|
-
export type { ZeropsNamespaceCommandDependencies } from './namespace-command'
|
|
98
|
+
export { postgresSchema as zeropsNamespacePostgresSchema } from './namespace/codec'
|
|
94
99
|
export { buildZeropsPlan, resolveDeployHostname } from './plan'
|
|
95
|
-
export type {
|
|
96
|
-
export { CANCELLED,
|
|
97
|
-
export type {
|
|
100
|
+
export type { ZeropsStep, ZeropsStepKind } from './plan'
|
|
101
|
+
export { CANCELLED, createZeropsDeployer, interpolateManifest } from './provider'
|
|
102
|
+
export type { ZeropsDeployResult, ZeropsDeployRun } from './provider'
|
|
98
103
|
export type * from './schema.generated'
|
|
99
|
-
export {
|
|
100
|
-
|
|
101
|
-
buildZeropsSourceCancelResponse,
|
|
102
|
-
buildZeropsSourceCredentialActivateRequestV2,
|
|
103
|
-
buildZeropsSourceCredentialActivateResponseV2,
|
|
104
|
-
buildZeropsSourceCredentialBundleV2,
|
|
105
|
-
buildZeropsSourceCredentialStatusRequestV2,
|
|
106
|
-
buildZeropsSourceCredentialStatusResponseV2,
|
|
107
|
-
buildZeropsSourceErrorEnvelope,
|
|
108
|
-
buildZeropsSourceInstallationsVerifyRequest,
|
|
109
|
-
buildZeropsSourceInstallationsVerifyResponse,
|
|
110
|
-
buildZeropsSourceResolveRequest,
|
|
111
|
-
buildZeropsSourceResolveRequestV2,
|
|
112
|
-
buildZeropsSourceResolveResponse,
|
|
113
|
-
buildZeropsSourceResolveResponseV2,
|
|
114
|
-
buildZeropsSourceUploadRequest,
|
|
115
|
-
buildZeropsSourceUploadRequestV2,
|
|
116
|
-
buildZeropsSourceUploadResponse,
|
|
117
|
-
buildZeropsSourceUploadResponseV2,
|
|
118
|
-
buildZeropsSourceWebhookConfigureRequest,
|
|
119
|
-
buildZeropsSourceWebhookConfigureResponse,
|
|
120
|
-
decodeZeropsSourceCancelRequest,
|
|
121
|
-
decodeZeropsSourceCancelResponse,
|
|
122
|
-
decodeZeropsSourceCredentialActivateRequestV2,
|
|
123
|
-
decodeZeropsSourceCredentialActivateResponseV2,
|
|
124
|
-
decodeZeropsSourceCredentialBundleV2,
|
|
125
|
-
decodeZeropsSourceCredentialStatusRequestV2,
|
|
126
|
-
decodeZeropsSourceCredentialStatusResponseV2,
|
|
127
|
-
decodeZeropsSourceErrorEnvelope,
|
|
128
|
-
decodeZeropsSourceInstallationsVerifyRequest,
|
|
129
|
-
decodeZeropsSourceInstallationsVerifyResponse,
|
|
130
|
-
decodeZeropsSourceResolveRequest,
|
|
131
|
-
decodeZeropsSourceResolveRequestV2,
|
|
132
|
-
decodeZeropsSourceResolveResponse,
|
|
133
|
-
decodeZeropsSourceResolveResponseV2,
|
|
134
|
-
decodeZeropsSourceUploadRequest,
|
|
135
|
-
decodeZeropsSourceUploadRequestV2,
|
|
136
|
-
decodeZeropsSourceUploadResponse,
|
|
137
|
-
decodeZeropsSourceUploadResponseV2,
|
|
138
|
-
decodeZeropsSourceWebhookConfigureRequest,
|
|
139
|
-
decodeZeropsSourceWebhookConfigureResponse,
|
|
140
|
-
normalizeZeropsSourceRepository,
|
|
141
|
-
serializeZeropsSourceCredentialBundleV2,
|
|
142
|
-
sha256ZeropsSourceCredentialBundleV2,
|
|
143
|
-
ZEROPS_SOURCE_CANCEL_PATH,
|
|
144
|
-
ZEROPS_SOURCE_CREDENTIAL_ACTIVATE_PATH_V2,
|
|
145
|
-
ZEROPS_SOURCE_CREDENTIAL_BUNDLE_VERSION_V2,
|
|
146
|
-
ZEROPS_SOURCE_CREDENTIAL_ENV_V2_PREFIX,
|
|
147
|
-
ZEROPS_SOURCE_CREDENTIAL_STATUS_PATH_V2,
|
|
148
|
-
ZEROPS_SOURCE_ERROR_CODES,
|
|
149
|
-
ZEROPS_SOURCE_ERROR_STAGES,
|
|
150
|
-
ZEROPS_SOURCE_INSTALLATIONS_VERIFY_PATH,
|
|
151
|
-
ZEROPS_SOURCE_PROTOCOL_VERSION,
|
|
152
|
-
ZEROPS_SOURCE_PROTOCOL_VERSION_V2,
|
|
153
|
-
ZEROPS_SOURCE_RESOLVE_PATH,
|
|
154
|
-
ZEROPS_SOURCE_RESOLVE_PATH_V2,
|
|
155
|
-
ZEROPS_SOURCE_UPLOAD_PATH,
|
|
156
|
-
ZEROPS_SOURCE_UPLOAD_PATH_V2,
|
|
157
|
-
ZEROPS_SOURCE_WEBHOOK_CONFIGURE_PATH,
|
|
158
|
-
zeropsSourceCredentialEnvV2,
|
|
159
|
-
} from './source'
|
|
104
|
+
export { isZeropsUploadDestination, normalizeZeropsSourceRepository } from './source'
|
|
105
|
+
export type { ZeropsSourceRepository } from './source'
|
|
160
106
|
export type {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
ZeropsSourceClient,
|
|
166
|
-
ZeropsSourceClientV2,
|
|
167
|
-
ZeropsSourceCredentialActivateInput,
|
|
168
|
-
ZeropsSourceCredentialActivateRequestV2,
|
|
169
|
-
ZeropsSourceCredentialActivateResponseV2,
|
|
170
|
-
ZeropsSourceCredentialActiveStatusResponseV2,
|
|
171
|
-
ZeropsSourceCredentialAnonymousStatusResponseV2,
|
|
172
|
-
ZeropsSourceCredentialBundleV2,
|
|
173
|
-
ZeropsSourceCredentialManager,
|
|
174
|
-
ZeropsSourceCredentialManagerV2,
|
|
175
|
-
ZeropsSourceCredentialStatusInput,
|
|
176
|
-
ZeropsSourceCredentialStatusRequestV2,
|
|
177
|
-
ZeropsSourceCredentialStatusResponseV2,
|
|
178
|
-
ZeropsSourceDescriptor,
|
|
179
|
-
ZeropsSourceErrorCode,
|
|
180
|
-
ZeropsSourceErrorEnvelope,
|
|
181
|
-
ZeropsSourceErrorStage,
|
|
182
|
-
ZeropsSourceGitHubAppIdentityV1,
|
|
183
|
-
ZeropsSourceInstallationScopeV1,
|
|
184
|
-
ZeropsSourceInstallationsVerifyInput,
|
|
185
|
-
ZeropsSourceInstallationsVerifyRequestV1,
|
|
186
|
-
ZeropsSourceInstallationsVerifyResponseV1,
|
|
187
|
-
ZeropsSourceInstallationVerificationV1,
|
|
188
|
-
ZeropsSourcePrivateBindingV2,
|
|
189
|
-
ZeropsSourceRepository,
|
|
107
|
+
ZeropsSourceArchive,
|
|
108
|
+
ZeropsSourcePort,
|
|
109
|
+
ZeropsSourcePrivateBinding,
|
|
110
|
+
ZeropsSourceResolution,
|
|
190
111
|
ZeropsSourceResolveInput,
|
|
191
|
-
ZeropsSourceResolveInputV2,
|
|
192
|
-
ZeropsSourceResolveRequestV1,
|
|
193
|
-
ZeropsSourceResolveRequestV2,
|
|
194
|
-
ZeropsSourceResolveResponseV1,
|
|
195
|
-
ZeropsSourceResolveResponseV2,
|
|
196
|
-
ZeropsSourceResolveResult,
|
|
197
112
|
ZeropsSourceUploadInput,
|
|
198
|
-
ZeropsSourceUploadInputV2,
|
|
199
|
-
ZeropsSourceUploadRequestV1,
|
|
200
|
-
ZeropsSourceUploadRequestV2,
|
|
201
|
-
ZeropsSourceUploadResponseV1,
|
|
202
|
-
ZeropsSourceUploadResponseV2,
|
|
203
113
|
ZeropsSourceUploadResult,
|
|
204
|
-
|
|
205
|
-
ZeropsSourceWebhookConfigureRequestV1,
|
|
206
|
-
ZeropsSourceWebhookConfigureResponseV1,
|
|
207
|
-
} from './source'
|
|
208
|
-
export { createZeropsSourceConnectionAdmin, SourceConnectionAdminError } from './source-connection'
|
|
209
|
-
export type {
|
|
210
|
-
SourceConnectionActivateInput,
|
|
211
|
-
SourceConnectionAdmin,
|
|
212
|
-
SourceConnectionAdminErrorCode,
|
|
213
|
-
SourceConnectionInspection,
|
|
214
|
-
SourceConnectionStatus,
|
|
215
|
-
SourceConnectionStatusInput,
|
|
216
|
-
SourceConnectionZeropsApi,
|
|
217
|
-
ZeropsSourceConnectionAdminOptions,
|
|
218
|
-
} from './source-connection'
|
|
114
|
+
} from './source/port'
|
|
219
115
|
export type {
|
|
220
116
|
ZeropsAppConfig,
|
|
221
117
|
ZeropsCompilerOwnedProjectField,
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// The CLI surface of `fabrika platform admin --provider=zerops`, parsed once.
|
|
2
|
+
//
|
|
3
|
+
// The same two rules `install-options.ts` and `deploy-options.ts` state: the CREDENTIAL comes from the
|
|
4
|
+
// environment and only from the environment (there is no `--key`, and an unknown flag is an error, so
|
|
5
|
+
// one cannot arrive on a command line), and everything else may come from a flag OR the environment.
|
|
6
|
+
//
|
|
7
|
+
// The command is unattended by construction — it is additive and idempotent, so it confirms nothing
|
|
8
|
+
// and prompts for nothing — and a missing required value is an error naming both the flag and the
|
|
9
|
+
// variable that would supply it.
|
|
10
|
+
|
|
11
|
+
import { parsePlatformHostname, parsePlatformScheme } from './placement-options'
|
|
12
|
+
|
|
13
|
+
/** Every input `runPlatformAdmin` takes from the operator. */
|
|
14
|
+
export interface PlatformAdminInput {
|
|
15
|
+
/** The mailbox that identifies the first administrator. */
|
|
16
|
+
readonly email: string
|
|
17
|
+
/** IAM's public hostname — the address the proxy fronts, as `platform deploy` names it. */
|
|
18
|
+
readonly iamHost: string
|
|
19
|
+
/** The scheme a browser speaks to this installation. With `iamHost` it composes IAM's public origin. */
|
|
20
|
+
readonly scheme: 'http' | 'https'
|
|
21
|
+
/** The `px_` provisioning key `platform install` printed. Environment only. */
|
|
22
|
+
readonly provisioningKey: string
|
|
23
|
+
/** Replace an outstanding enrollment instead of reporting it. */
|
|
24
|
+
readonly reissue: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const FLAGS = ['--email', '--iam-host', '--scheme'] as const
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The one way a second enrollment URL is ever issued.
|
|
31
|
+
*
|
|
32
|
+
* Printing the URL once is the property a re-run has to keep, so an outstanding enrollment is
|
|
33
|
+
* REPORTED rather than replaced. An enrollment that expired unopened would otherwise be a dead end —
|
|
34
|
+
* the only other way to issue one is a console nobody can sign in to yet — so the way out is a flag,
|
|
35
|
+
* which makes replacing the outstanding one an explicit choice.
|
|
36
|
+
*/
|
|
37
|
+
export const REISSUE_FLAG = '--reissue'
|
|
38
|
+
|
|
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
|
+
const assertKnownArguments = (argv: readonly string[]): void => {
|
|
69
|
+
for (const arg of argv) {
|
|
70
|
+
if (arg === REISSUE_FLAG) {
|
|
71
|
+
continue
|
|
72
|
+
}
|
|
73
|
+
const name = arg.split('=')[0] ?? arg
|
|
74
|
+
if (!FLAGS.some((flag) => flag === name) || !arg.includes('=')) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`unexpected argument \`${arg}\`. Credentials are read from the environment and have no flag; run `
|
|
77
|
+
+ '`fabrika platform admin --provider=zerops --help` for the surface',
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const readScheme = (argv: readonly string[], env: Record<string, string | undefined>): 'http' | 'https' => {
|
|
84
|
+
const raw = setting(argv, env, '--scheme', 'FABRIKA_PLATFORM_SCHEME')
|
|
85
|
+
return parsePlatformScheme(raw)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* IAM's public hostname, checked to be one.
|
|
90
|
+
*
|
|
91
|
+
* A BARE hostname, the same coordinate `platform deploy` takes under the same name: the origin is
|
|
92
|
+
* composed from it and `--scheme`, so a value carrying either would compose a second one.
|
|
93
|
+
*/
|
|
94
|
+
const readIamHost = (argv: readonly string[], env: Record<string, string | undefined>): string => {
|
|
95
|
+
const host = required(setting(argv, env, '--iam-host', 'FABRIKA_PLATFORM_IAM_HOST'), '--iam-host', 'FABRIKA_PLATFORM_IAM_HOST')
|
|
96
|
+
return parsePlatformHostname(host)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The credential, and the one message that says why there is no flag for it.
|
|
101
|
+
*
|
|
102
|
+
* `required` would name a flag beside the variable, and there is none — a key on a command line lands
|
|
103
|
+
* in a CI log, a process listing and the shell history of whoever reproduced the run.
|
|
104
|
+
*/
|
|
105
|
+
const provisioningKey = (env: Record<string, string | undefined>): string => {
|
|
106
|
+
const value = readEnv(env, 'FABRIKA_IAM_PROVISIONING_KEY')
|
|
107
|
+
if (value === undefined) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
'FABRIKA_IAM_PROVISIONING_KEY is required. It has no flag: the key platform install printed comes from the environment only.',
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
return value
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Parse the command's arguments and environment into the one input `runPlatformAdmin` takes. */
|
|
116
|
+
export const parsePlatformAdminArgs = (
|
|
117
|
+
argv: readonly string[],
|
|
118
|
+
env: Record<string, string | undefined>,
|
|
119
|
+
): PlatformAdminInput => {
|
|
120
|
+
assertKnownArguments(argv)
|
|
121
|
+
return {
|
|
122
|
+
email: required(setting(argv, env, '--email', 'FABRIKA_PLATFORM_ADMIN_EMAIL'), '--email', 'FABRIKA_PLATFORM_ADMIN_EMAIL'),
|
|
123
|
+
iamHost: readIamHost(argv, env),
|
|
124
|
+
scheme: readScheme(argv, env),
|
|
125
|
+
provisioningKey: provisioningKey(env),
|
|
126
|
+
reissue: argv.includes(REISSUE_FLAG),
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// `fabrika platform admin --provider=zerops` — admit the first administrator to a running installation.
|
|
2
|
+
//
|
|
3
|
+
// `platform install` ends with a provisioning key and nobody who can sign in, and until this command
|
|
4
|
+
// existed the four `/admin/rpc` calls that fix that lived in a throwaway script. The mechanics are
|
|
5
|
+
// provider-neutral and live in `@fabrika/installation-init`; what belongs HERE is the Zerops-shaped
|
|
6
|
+
// half — where this installation's IAM answers, and which credential reaches it.
|
|
7
|
+
//
|
|
8
|
+
// It confirms nothing and prompts for nothing. Every step is additive and idempotent, so there is
|
|
9
|
+
// nothing for an operator to agree to, and `install`'s stdin rule (a non-terminal stdin is refused
|
|
10
|
+
// without `--yes`) has nothing to guard here.
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
action as consoleAction,
|
|
14
|
+
createIamAdminClient,
|
|
15
|
+
ensureFirstAdministrator,
|
|
16
|
+
type FirstAdministratorApi,
|
|
17
|
+
type FirstAdministratorResult,
|
|
18
|
+
} from '@fabrika/installation-init'
|
|
19
|
+
import type { PlatformAdminInput } from './admin-options'
|
|
20
|
+
import { REISSUE_FLAG } from './admin-options'
|
|
21
|
+
import { platformOrigin } from './hosts'
|
|
22
|
+
import { MACHINE_KEY_NEXT_STEP_TITLE, machineKeyNextStepLines } from './machine-key'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Where this command's progress goes.
|
|
26
|
+
*
|
|
27
|
+
* The same rule the rest of this package's logging states: no helper here takes a secret value. The
|
|
28
|
+
* enrollment URL is a credential in transit and never travels through the log — it goes to `print`,
|
|
29
|
+
* which is stdout and nothing else.
|
|
30
|
+
*/
|
|
31
|
+
export interface AdminLog {
|
|
32
|
+
step(title: string): void
|
|
33
|
+
info(message: string): void
|
|
34
|
+
warn(message: string): void
|
|
35
|
+
ok(message: string): void
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** `AdminLog` plus the boxed hand-off the closing block uses — composed in below, not asked of the caller. */
|
|
39
|
+
export interface AdminReportLog extends AdminLog {
|
|
40
|
+
action(title: string, lines: readonly string[]): void
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface PlatformAdminCollaborators {
|
|
44
|
+
readonly client: FirstAdministratorApi
|
|
45
|
+
readonly log: AdminReportLog
|
|
46
|
+
/** stdout, for the command's one piece of DATA: the enrollment URL, on a line of its own. */
|
|
47
|
+
readonly print: (line: string) => void
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The real collaborators: the typed admin client for this installation's public IAM, and the console. */
|
|
51
|
+
export const consoleAdminCollaborators = (input: PlatformAdminInput, log: AdminLog): PlatformAdminCollaborators => ({
|
|
52
|
+
client: createIamAdminClient({ origin: platformOrigin(input.scheme, input.iamHost), provisioningKey: input.provisioningKey }),
|
|
53
|
+
log: { ...log, action: (title, lines) => consoleAction(title, [...lines]) },
|
|
54
|
+
print: (line) => console.info(line),
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Admit the first administrator, and report exactly what was found rather than what was assumed.
|
|
59
|
+
*
|
|
60
|
+
* Nothing is printed before the whole sequence has succeeded except its progress: a failure — an
|
|
61
|
+
* unreachable IAM, a refused key, a 4xx — throws, and the CLI exits non-zero with that one message.
|
|
62
|
+
* No partial result and, above all, no URL.
|
|
63
|
+
*/
|
|
64
|
+
export const runPlatformAdmin = async (input: PlatformAdminInput, collaborators: PlatformAdminCollaborators): Promise<void> => {
|
|
65
|
+
const { client, log, print } = collaborators
|
|
66
|
+
const origin = platformOrigin(input.scheme, input.iamHost)
|
|
67
|
+
|
|
68
|
+
log.step('Admit the first administrator')
|
|
69
|
+
log.info(`${origin} · ${input.email}`)
|
|
70
|
+
if (input.scheme === 'http' && !isLoopback(input.iamHost)) {
|
|
71
|
+
// Two credentials cross this connection: the provisioning key going out, the enrollment URL
|
|
72
|
+
// coming back. On a loopback address that is a local composition; anywhere else it is a mistake.
|
|
73
|
+
log.warn('this speaks plain http to a host that is not loopback — the provisioning key and the enrollment URL both cross it in the clear')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const result = await ensureFirstAdministrator(client, { email: input.email, reissueEnrollment: input.reissue })
|
|
77
|
+
report(result, log)
|
|
78
|
+
announceEnrollment(result, log, print)
|
|
79
|
+
// The next command an operator runs needs an origin this one already holds and two keys it must not
|
|
80
|
+
// print. Naming both places is the difference between one `key issue` and a guessed hostname.
|
|
81
|
+
log.action(MACHINE_KEY_NEXT_STEP_TITLE, machineKeyNextStepLines({ iamOrigin: origin }))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const report = (result: FirstAdministratorResult, log: AdminLog): void => {
|
|
85
|
+
log.ok(
|
|
86
|
+
result.principal === 'invited'
|
|
87
|
+
? `invited ${result.email} as principal ${result.principalId}`
|
|
88
|
+
: `${result.email} already exists as principal ${result.principalId}`,
|
|
89
|
+
)
|
|
90
|
+
// `app: null` is the whole point: an `admin` grant scoped to the console's own app id leaves
|
|
91
|
+
// Delivery and Operations working while the Access plane refuses.
|
|
92
|
+
log.ok(result.grant === 'created' ? 'granted the cross-app `admin` role' : 'the cross-app `admin` role was already granted')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The one place the enrollment URL is ever written, and it is written once.
|
|
97
|
+
*
|
|
98
|
+
* A URL that sets a password is a credential: it goes to stdout on a line of its own, is stored
|
|
99
|
+
* nowhere, and is not repeated. The three other outcomes carry no URL at all and say what was found.
|
|
100
|
+
*/
|
|
101
|
+
const announceEnrollment = (result: FirstAdministratorResult, log: AdminLog, print: (line: string) => void): void => {
|
|
102
|
+
if (result.enrollment.state === 'already-set') {
|
|
103
|
+
log.ok('a password is already set for this principal — no enrollment was issued')
|
|
104
|
+
log.info('Sign in at the console. A forgotten password is a reset, which the Access plane issues to a signed-in administrator.')
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
if (result.enrollment.state === 'outstanding') {
|
|
108
|
+
log.warn(`an enrollment is already outstanding — a second one was NOT issued. Pass ${REISSUE_FLAG} to replace it if the first expired unopened`)
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
if (result.enrollment.state === 'emailed') {
|
|
112
|
+
log.ok(`the enrollment was emailed to ${result.enrollment.email}, valid until ${expiry(result.enrollment.expiresAt)}`)
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
log.ok(`enrollment issued, valid until ${expiry(result.enrollment.expiresAt)}`)
|
|
116
|
+
log.info('The URL below sets the first password. It is a credential: it is printed once, stored nowhere, and it expires.')
|
|
117
|
+
print(result.enrollment.url)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const expiry = (unixSeconds: number): string => new Date(unixSeconds * 1000).toISOString()
|
|
121
|
+
|
|
122
|
+
/** Where `http` is a local composition rather than a credential crossing the open network. */
|
|
123
|
+
const isLoopback = (host: string): boolean => host === 'localhost' || host.endsWith('.localhost') || host === '127.0.0.1' || host === '::1'
|