@fabrika/provider-zerops 0.8.2 → 0.9.0-cloudflare.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 +16 -14
- package/src/api/client.ts +6 -0
- package/src/api/types.ts +12 -0
- package/src/cli.ts +11 -0
- package/src/collaborators.ts +0 -14
- package/src/control.ts +33 -57
- package/src/execution.ts +113 -102
- package/src/installation/admin.ts +1 -1
- package/src/installation/deploy-options.ts +1 -2
- package/src/installation/deploy.ts +41 -75
- package/src/installation/generated/platform-console-schema.ts +3 -0
- package/src/installation/generated/platform-proxy-manifest.ts +21 -1
- package/src/installation/guide.md +9 -6
- package/src/installation/hosts.ts +1 -7
- package/src/installation/index.ts +0 -13
- package/src/installation/init-options.ts +2 -1
- package/src/installation/install-environment.ts +42 -106
- package/src/installation/install-options.ts +3 -1
- package/src/installation/install-secrets.ts +24 -68
- package/src/installation/install.ts +8 -4
- package/src/installation/sidecar.ts +5 -26
- package/src/installation/topology/setups.ts +5 -0
- package/src/installation/upgrade-options.ts +2 -2
- package/src/managed-env.ts +1 -174
- package/src/manifest.ts +11 -14
- package/src/namespace/claims.ts +56 -1
- package/src/namespace/errors.ts +1 -1
- package/src/namespace/manifest.ts +6 -19
- package/src/namespace/provisioning.ts +15 -3
- package/src/namespace/proxy-attempt.ts +43 -2
- package/src/namespace/proxy-reads.ts +12 -22
- package/src/namespace-cli.ts +152 -0
- package/src/plan.ts +2 -7
- package/src/provider.ts +128 -126
- package/src/proxy-manifest-carrier.ts +18 -0
- package/src/types.ts +0 -2
- package/types/src/api/client.d.ts +5 -0
- package/types/src/api/errors.d.ts +24 -0
- package/types/src/api/polling.d.ts +41 -0
- package/types/src/api/readers.d.ts +1115 -0
- package/types/src/api/service-env-limit.d.ts +17 -0
- package/types/src/api/service-env-response.d.ts +23 -0
- package/types/src/api/types.d.ts +687 -0
- package/types/src/api.d.ts +6 -0
- package/types/src/authoring.d.ts +9 -0
- package/types/src/cli-args.d.ts +19 -0
- package/types/src/cli.d.ts +13 -0
- package/types/src/collaborators.d.ts +16 -0
- package/types/src/compile.d.ts +91 -0
- package/types/src/control.d.ts +40 -0
- package/types/src/database-budget.d.ts +38 -0
- package/types/src/descriptor.d.ts +28 -0
- package/types/src/execution.d.ts +125 -0
- package/types/src/exposure.d.ts +47 -0
- package/types/src/index.d.ts +31 -0
- package/types/src/installation/admin-options.d.ts +24 -0
- package/types/src/installation/admin.d.ts +35 -0
- package/types/src/installation/checker-release.d.ts +12 -0
- package/types/src/installation/deploy-options.d.ts +37 -0
- package/types/src/installation/deploy.d.ts +136 -0
- package/types/src/installation/generated/platform-console-schema.d.ts +2 -0
- package/types/src/installation/generated/platform-proxy-manifest.d.ts +2 -0
- package/types/src/installation/hosts.d.ts +34 -0
- package/types/src/installation/index.d.ts +7 -0
- package/types/src/installation/init-options.d.ts +38 -0
- package/types/src/installation/init.d.ts +60 -0
- package/types/src/installation/install-collaborators.d.ts +24 -0
- package/types/src/installation/install-environment.d.ts +63 -0
- package/types/src/installation/install-options.d.ts +49 -0
- package/types/src/installation/install-project.d.ts +13 -0
- package/types/src/installation/install-provisioning.d.ts +58 -0
- package/types/src/installation/install-secrets.d.ts +17 -0
- package/types/src/installation/install-token.d.ts +10 -0
- package/types/src/installation/install.d.ts +23 -0
- package/types/src/installation/installation-source.d.ts +8 -0
- package/types/src/installation/log.d.ts +44 -0
- package/types/src/installation/machine-key.d.ts +19 -0
- package/types/src/installation/placement-options.d.ts +11 -0
- package/types/src/installation/platform-archive.d.ts +7 -0
- package/types/src/installation/platform-source.d.ts +8 -0
- package/types/src/installation/platform-upload.d.ts +3 -0
- package/types/src/installation/sidecar.d.ts +49 -0
- package/types/src/installation/source-options.d.ts +1 -0
- package/types/src/installation/topology/artifacts.d.ts +8 -0
- package/types/src/installation/topology/index.d.ts +5 -0
- package/types/src/installation/topology/installation-schemas.d.ts +2242 -0
- package/types/src/installation/topology/invariants.d.ts +23 -0
- package/types/src/installation/topology/setups.d.ts +59 -0
- package/types/src/installation/topology/topology.d.ts +153 -0
- package/types/src/installation/topology/validate.d.ts +11 -0
- package/types/src/installation/upgrade-options.d.ts +26 -0
- package/types/src/installation/upgrade-output.d.ts +6 -0
- package/types/src/installation/upgrade.d.ts +88 -0
- package/types/src/managed-env.d.ts +24 -0
- package/types/src/manifest.d.ts +204 -0
- package/types/src/namespace/claims.d.ts +16 -0
- package/types/src/namespace/codec.d.ts +334 -0
- package/types/src/namespace/constants.d.ts +25 -0
- package/types/src/namespace/errors.d.ts +15 -0
- package/types/src/namespace/manifest.d.ts +4 -0
- package/types/src/namespace/operator.d.ts +12 -0
- package/types/src/namespace/provisioning.d.ts +4 -0
- package/types/src/namespace/proxy-attempt.d.ts +39 -0
- package/types/src/namespace/proxy-reads.d.ts +22 -0
- package/types/src/namespace/proxy-secrets.d.ts +8 -0
- package/types/src/namespace/proxy-source.d.ts +74 -0
- package/types/src/namespace/topology.d.ts +9 -0
- package/types/src/namespace/types.d.ts +101 -0
- package/types/src/namespace/update.d.ts +6 -0
- package/types/src/namespace/validation.d.ts +12 -0
- package/types/src/namespace-cli.d.ts +2 -0
- package/types/src/namespace.d.ts +9 -0
- package/types/src/plan.d.ts +11 -0
- package/types/src/provider.d.ts +63 -0
- package/types/src/proxy-manifest-carrier.d.ts +24 -0
- package/types/src/schema.generated.d.ts +376 -0
- package/types/src/service-names.d.ts +4 -0
- package/types/src/service-references.d.ts +13 -0
- package/types/src/source/constants.d.ts +6 -0
- package/types/src/source/port.d.ts +60 -0
- package/types/src/source/types.d.ts +4 -0
- package/types/src/source/validation.d.ts +19 -0
- package/types/src/source.d.ts +2 -0
- package/types/src/types.d.ts +119 -0
- package/types/src/version-attempt.d.ts +8 -0
- package/src/installation/mcp.ts +0 -102
- package/src/installation/proxy-manifest.ts +0 -214
- package/src/installation/secrets.ts +0 -78
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// `fabrika namespace` on Zerops: the provider options, the plan, and the namespace each command hands Control. The
|
|
2
|
+
// `fabrika` command holds Control's origin and credential, and prints the result.
|
|
3
|
+
|
|
4
|
+
import type { JsonValue, ProviderDeploymentNamespace, ProviderNamespacePlan } from '@fabrika/provider-contract'
|
|
5
|
+
import type { NamespaceCli, NamespaceCommandContext, NamespaceCommandResult } from '@fabrika/provider-contract/app-cli'
|
|
6
|
+
import { parseZeropsCliArgs, type ZeropsCliArgs } from './cli-args'
|
|
7
|
+
import {
|
|
8
|
+
createZeropsNamespaceOperator,
|
|
9
|
+
decodeZeropsNamespaceProxySource,
|
|
10
|
+
verifyZeropsNamespaceProxySource,
|
|
11
|
+
zeropsNamespaceTargetCodec,
|
|
12
|
+
} from './namespace'
|
|
13
|
+
import type { ZeropsNamespacePostgres } from './namespace'
|
|
14
|
+
import { postgresSchema } from './namespace/codec'
|
|
15
|
+
|
|
16
|
+
const NAMESPACE_USAGE = `fabrika namespace — Zerops deployment namespaces
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
fabrika namespace plan --provider=zerops --id=<id> --env=<env> --preset=<cheap|mid|full> [namespace options]
|
|
20
|
+
fabrika namespace create --provider=zerops --id=<id> --env=<env> --preset=<cheap|mid|full> [namespace options] [--control-url=<url>]
|
|
21
|
+
fabrika namespace adopt --provider=zerops --id=<id> --env=<env> --preset=<cheap|mid|full> --project-id=<id> [namespace options] [--control-url=<url>]
|
|
22
|
+
fabrika namespace reconcile --provider=zerops --id=<id> [--control-url=<url>]
|
|
23
|
+
|
|
24
|
+
Options:
|
|
25
|
+
--id=<id> Namespace id.
|
|
26
|
+
--env=<env> Environment.
|
|
27
|
+
--preset=<cheap|mid|full> Provider-owned namespace preset.
|
|
28
|
+
--exclusive-app=<id> App reserved by a full namespace.
|
|
29
|
+
--project-id=<id> Existing Zerops project id for adoption.
|
|
30
|
+
--project-name=<name> Zerops project name (create: default fabrika-<env>-<id>;
|
|
31
|
+
adopt: the existing project's name, default the namespace id).
|
|
32
|
+
--core-package=<tier> LIGHT or SERIOUS.
|
|
33
|
+
--public-access=<mode> custom-domain or zerops-subdomain.
|
|
34
|
+
--postgres-type=<type> Cheap-tier PostgreSQL type.
|
|
35
|
+
--postgres-profile=<profile> Cheap-tier PostgreSQL profile.
|
|
36
|
+
--control-url=<url> Control origin; or FABRIKA_CONTROL_URL.
|
|
37
|
+
--json Print the result as JSON instead of rows.
|
|
38
|
+
-h, --help Show this help.
|
|
39
|
+
|
|
40
|
+
\`plan\` prints one \`<path>\\t<value>\` row per field of the plan, keyed by its path in --json; a multi-line
|
|
41
|
+
value such as the proxy's descriptor file is shown by size only. The other commands print one
|
|
42
|
+
\`<id>\\t<env>\\t<state>\\t<exclusive app>\` row.
|
|
43
|
+
|
|
44
|
+
Mutating namespace commands read the Bearer credential from FABRIKA_CONTROL_TOKEN.
|
|
45
|
+
Plan/create/adopt require FABRIKA_ZEROPS_PROXY_SOURCE_JSON:
|
|
46
|
+
{"repository":{"owner":"...","name":"..."},"commitSha":"...","descriptor":{"path":"zerops.yaml","contents":"...","sha256":"..."}}
|
|
47
|
+
Reconcile uses the persisted namespace source.
|
|
48
|
+
`
|
|
49
|
+
|
|
50
|
+
const required = (value: string | undefined, label: string): string => {
|
|
51
|
+
if (value === undefined || value.trim() === '') {
|
|
52
|
+
throw new Error(`${label} is required`)
|
|
53
|
+
}
|
|
54
|
+
return value.trim()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const postgres = (args: ZeropsCliArgs): ZeropsNamespacePostgres | undefined => {
|
|
58
|
+
const type = args.postgresType
|
|
59
|
+
const profile = args.postgresProfile
|
|
60
|
+
if (type === undefined && profile === undefined) return undefined
|
|
61
|
+
if (type === undefined) {
|
|
62
|
+
throw new Error('--postgres-type is required with --postgres-profile')
|
|
63
|
+
}
|
|
64
|
+
const result = postgresSchema.safeParse({ type, profile })
|
|
65
|
+
if (result.success) return result.data
|
|
66
|
+
if (result.error.issues[0]?.path[0] === 'type') throw new Error('--postgres-type must be postgresql:ha@18 or postgresql:single@18')
|
|
67
|
+
throw new Error(`--postgres-profile ${profile} is not supported by ${type}`)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const planOptions = (args: ZeropsCliArgs): { [key: string]: JsonValue } | undefined => {
|
|
71
|
+
const values: { [key: string]: JsonValue } = {}
|
|
72
|
+
if (args.projectName !== undefined) values['projectName'] = required(args.projectName, '--project-name')
|
|
73
|
+
if (args.corePackage !== undefined) values['corePackage'] = required(args.corePackage, '--core-package')
|
|
74
|
+
if (args.publicAccess !== undefined) values['publicAccess'] = required(args.publicAccess, '--public-access')
|
|
75
|
+
const database = postgres(args)
|
|
76
|
+
if (database !== undefined) {
|
|
77
|
+
values['postgres'] = database.profile === undefined ? { type: database.type } : { type: database.type, profile: database.profile }
|
|
78
|
+
}
|
|
79
|
+
return Object.keys(values).length === 0 ? undefined : values
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const namespacePlan = async (
|
|
83
|
+
args: ZeropsCliArgs,
|
|
84
|
+
source: Readonly<Record<string, string | undefined>>,
|
|
85
|
+
): Promise<ProviderNamespacePlan> => {
|
|
86
|
+
// `FABRIKA_ZEROPS_*` because Zerops reserves the bare `ZEROPS_` prefix and refuses to store a custom
|
|
87
|
+
// variable using it (`userDataZeropsPrefixForbidden`).
|
|
88
|
+
const serialized = required(source['FABRIKA_ZEROPS_PROXY_SOURCE_JSON'], 'FABRIKA_ZEROPS_PROXY_SOURCE_JSON')
|
|
89
|
+
let value: unknown
|
|
90
|
+
try {
|
|
91
|
+
value = JSON.parse(serialized)
|
|
92
|
+
} catch {
|
|
93
|
+
throw new Error('FABRIKA_ZEROPS_PROXY_SOURCE_JSON must contain valid JSON')
|
|
94
|
+
}
|
|
95
|
+
const proxySource = decodeZeropsNamespaceProxySource(value)
|
|
96
|
+
await verifyZeropsNamespaceProxySource(proxySource)
|
|
97
|
+
const operator = createZeropsNamespaceOperator({ proxySource })
|
|
98
|
+
const options = planOptions(args)
|
|
99
|
+
return operator.plan({
|
|
100
|
+
id: required(args.namespaceId, '--id'),
|
|
101
|
+
env: required(args.env, '--env'),
|
|
102
|
+
preset: required(args.preset, '--preset'),
|
|
103
|
+
...(args.exclusiveAppId === undefined ? {} : { exclusiveAppId: required(args.exclusiveAppId, '--exclusive-app') }),
|
|
104
|
+
...(options === undefined ? {} : { options }),
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** One namespace command. Every input is validated before `context.control()` is read. */
|
|
109
|
+
const runZeropsNamespaceCommand = async (
|
|
110
|
+
subcommand: string,
|
|
111
|
+
argv: readonly string[],
|
|
112
|
+
context: NamespaceCommandContext,
|
|
113
|
+
): Promise<NamespaceCommandResult> => {
|
|
114
|
+
const args = parseZeropsCliArgs(['namespace', subcommand, ...argv])
|
|
115
|
+
if (subcommand === 'plan') {
|
|
116
|
+
return { plan: await namespacePlan(args, context.source) }
|
|
117
|
+
}
|
|
118
|
+
if (subcommand === 'reconcile') {
|
|
119
|
+
const id = required(args.namespaceId, '--id')
|
|
120
|
+
return { namespace: await context.control().reconcile(id) }
|
|
121
|
+
}
|
|
122
|
+
if (subcommand !== 'create' && subcommand !== 'adopt') {
|
|
123
|
+
throw new Error(`Unknown namespace command: ${subcommand}`)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const planned = await namespacePlan(args, context.source)
|
|
127
|
+
const control = context.control()
|
|
128
|
+
if (subcommand === 'create') {
|
|
129
|
+
return { namespace: await control.create(planned.namespace) }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const projectId = required(args.projectId, '--project-id')
|
|
133
|
+
const decoded = zeropsNamespaceTargetCodec.decode(planned.namespace.target.payload)
|
|
134
|
+
const namespace: ProviderDeploymentNamespace = {
|
|
135
|
+
...planned.namespace,
|
|
136
|
+
target: {
|
|
137
|
+
provider: 'zerops',
|
|
138
|
+
version: zeropsNamespaceTargetCodec.version,
|
|
139
|
+
payload: zeropsNamespaceTargetCodec.encode({
|
|
140
|
+
...decoded,
|
|
141
|
+
// An adopted project keeps its own name, so the plan's default for a created one does not apply.
|
|
142
|
+
projectName: args.projectName ?? planned.namespace.id,
|
|
143
|
+
managed: false,
|
|
144
|
+
projectId,
|
|
145
|
+
ready: false,
|
|
146
|
+
}),
|
|
147
|
+
},
|
|
148
|
+
}
|
|
149
|
+
return { namespace: await control.adopt(namespace) }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export const zeropsNamespaceCli: NamespaceCli = { usage: NAMESPACE_USAGE, run: runZeropsNamespaceCommand }
|
package/src/plan.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { type FabrikaManifest, manifestServiceHostnames } from './manifest'
|
|
2
|
-
import type { ZeropsRuntimeTarget } from './types'
|
|
3
2
|
|
|
4
3
|
export type ZeropsStepKind =
|
|
5
4
|
| 'apply-import'
|
|
@@ -11,7 +10,6 @@ export type ZeropsStepKind =
|
|
|
11
10
|
| 'upload-source'
|
|
12
11
|
| 'trigger-build'
|
|
13
12
|
| 'await-version'
|
|
14
|
-
| 'reconcile-schema'
|
|
15
13
|
|
|
16
14
|
export interface ZeropsStep {
|
|
17
15
|
readonly id: string
|
|
@@ -30,7 +28,7 @@ export const resolveDeployHostname = (manifest: FabrikaManifest): string => {
|
|
|
30
28
|
}
|
|
31
29
|
|
|
32
30
|
/** Build the provider-owned deploy order without touching the platform. */
|
|
33
|
-
export const buildZeropsPlan = (manifest: FabrikaManifest
|
|
31
|
+
export const buildZeropsPlan = (manifest: FabrikaManifest): readonly ZeropsStep[] => {
|
|
34
32
|
const serviceHostnames = manifestServiceHostnames(manifest)
|
|
35
33
|
const steps: ZeropsStep[] = []
|
|
36
34
|
|
|
@@ -48,7 +46,7 @@ export const buildZeropsPlan = (manifest: FabrikaManifest, target: ZeropsRuntime
|
|
|
48
46
|
steps.push({
|
|
49
47
|
id: 'raise-start-memory',
|
|
50
48
|
kind: 'raise-start-memory',
|
|
51
|
-
description: "Raise the deploy service's minimum RAM to what its init commands need",
|
|
49
|
+
description: "Raise the deploy service's minimum RAM to what its init commands need, and pin a dev environment to one container",
|
|
52
50
|
})
|
|
53
51
|
steps.push({
|
|
54
52
|
id: 'size-database',
|
|
@@ -59,9 +57,6 @@ export const buildZeropsPlan = (manifest: FabrikaManifest, target: ZeropsRuntime
|
|
|
59
57
|
steps.push({ id: 'upload-source', kind: 'upload-source', description: 'Upload the resolved repository snapshot' })
|
|
60
58
|
steps.push({ id: 'trigger-build', kind: 'trigger-build', description: 'Trigger the Zerops build+deploy pipeline' })
|
|
61
59
|
steps.push({ id: 'await-version', kind: 'await-version', description: 'Await the Zerops pipeline and relay its build log' })
|
|
62
|
-
if (manifest.app.schema !== undefined && target.propustkaUrl !== undefined) {
|
|
63
|
-
steps.push({ id: 'reconcile-schema', kind: 'reconcile-schema', description: 'Reconcile authz schema into IAM' })
|
|
64
|
-
}
|
|
65
60
|
|
|
66
61
|
return steps
|
|
67
62
|
}
|
package/src/provider.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
// The Zerops provider owns its plan and every effect needed to execute it.
|
|
2
2
|
//
|
|
3
3
|
// A Zerops deploy is HTTP and nothing else (ADR-0003) — no container, no shell, no filesystem — so this
|
|
4
|
-
// driver's whole side-effect surface is one REST client
|
|
5
|
-
//
|
|
4
|
+
// driver's whole side-effect surface is one REST client (`./collaborators.ts`). It never spawns a process and has
|
|
5
|
+
// no `runCommand` to be handed.
|
|
6
6
|
//
|
|
7
7
|
// The interesting step is `await-version`: fabrika does not RUN the build, it WATCHES one the platform
|
|
8
8
|
// runs. Recovery never resumes a deploy (ADR-0044): a version reserved but never triggered is deleted, and
|
|
9
9
|
// one whose build may have been requested is settled only by polling it.
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import { FABRIKA_SOURCE_MAP_UPLOAD_KEY, managedValue, staleManagedVariables } from '@fabrika/auth-core'
|
|
12
12
|
import { type OperationsSourceMapUploadState, readSourceMapUploadReport } from '@fabrika/operations-contract/releases'
|
|
13
13
|
import {
|
|
14
14
|
type DeployStepResult,
|
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
import {
|
|
51
51
|
PLATFORM_TYPESCRIPT_VERSION,
|
|
52
52
|
renderLockedExecutionDescriptor,
|
|
53
|
+
type ZeropsDevExecution,
|
|
53
54
|
zeropsJobProcesses,
|
|
54
55
|
type ZeropsLockedExecution,
|
|
55
56
|
zeropsStartMinRam,
|
|
@@ -65,20 +66,9 @@ import {
|
|
|
65
66
|
type StorageProvenanceMarker,
|
|
66
67
|
unprovenBucketReason,
|
|
67
68
|
} from './exposure'
|
|
68
|
-
import {
|
|
69
|
-
assertAuthoredManagedDocument,
|
|
70
|
-
assertAuthoredManagedInputs,
|
|
71
|
-
isFrameworkFamilyVariable,
|
|
72
|
-
isSensitiveFamilyVariable,
|
|
73
|
-
type ManagedValues,
|
|
74
|
-
overlayRelease,
|
|
75
|
-
parseManagedEnvironment,
|
|
76
|
-
SENSITIVE_MANAGED_KEYS,
|
|
77
|
-
SERVICE_MANAGED_KEYS,
|
|
78
|
-
} from './managed-env'
|
|
69
|
+
import { assertAuthoredManagedDocument, assertAuthoredManagedInputs, overlayRelease } from './managed-env'
|
|
79
70
|
import {
|
|
80
71
|
applicationDatabaseOf,
|
|
81
|
-
approvedSchemaOf,
|
|
82
72
|
type FabrikaImportDocument,
|
|
83
73
|
type FabrikaManifest,
|
|
84
74
|
lockedExecutionFor,
|
|
@@ -96,8 +86,6 @@ export const CANCELLED = 'deploy cancelled'
|
|
|
96
86
|
export interface ZeropsDeployRun {
|
|
97
87
|
readonly appId: string
|
|
98
88
|
readonly env: string
|
|
99
|
-
/** Every origin IAM may hand this app a session at; app-wide, because IAM's registry is keyed by app id. */
|
|
100
|
-
readonly returnOrigins?: readonly string[]
|
|
101
89
|
readonly secrets: Readonly<Record<string, string>>
|
|
102
90
|
readonly vars: Readonly<Record<string, string>>
|
|
103
91
|
/** Platform-owned runtime configuration, separate from operator-managed application vars. */
|
|
@@ -108,6 +96,8 @@ export interface ZeropsDeployRun {
|
|
|
108
96
|
readonly access?: ProviderApprovedAccess
|
|
109
97
|
/** The run-scoped source-map destination Control minted; the build uploads to it. */
|
|
110
98
|
readonly artifactUpload?: ProviderDeployInput['artifactUpload']
|
|
99
|
+
/** Present for an environment of kind `dev`: the launcher starts instead of the serving entry, on one container. */
|
|
100
|
+
readonly dev?: ZeropsDevExecution
|
|
111
101
|
readonly dryRun: boolean
|
|
112
102
|
readonly signal: AbortSignal
|
|
113
103
|
readonly events: ProviderRunEvents
|
|
@@ -332,9 +322,8 @@ interface StepEnv {
|
|
|
332
322
|
signal: AbortSignal
|
|
333
323
|
dryRun: boolean
|
|
334
324
|
attempt: Attempt
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
familyVariables: Readonly<Record<string, string>>
|
|
325
|
+
/** What Control handed (`parseManagedEnvironment`). */
|
|
326
|
+
managed: ProviderManagedEnvironment
|
|
338
327
|
/** `await-version` gives up at this instant. */
|
|
339
328
|
deadlineMs: number
|
|
340
329
|
}
|
|
@@ -577,9 +566,9 @@ const verifyExposure = async (env: StepEnv): Promise<void> => {
|
|
|
577
566
|
}
|
|
578
567
|
}
|
|
579
568
|
if (findings.length > 0) {
|
|
580
|
-
const dsn = env.
|
|
569
|
+
const dsn = managedValue(env.managed, 'FABRIKA_OPERATIONS_DSN')
|
|
581
570
|
if (dsn !== null) {
|
|
582
|
-
const alert = { appId: run.appId, env: run.env, release: env.
|
|
571
|
+
const alert = { appId: run.appId, env: run.env, release: managedValue(env.managed, 'FABRIKA_RELEASE'), findings }
|
|
583
572
|
await zerops.alertExposure({ dsn, alert, signal }).then(
|
|
584
573
|
() => log(' raised an Operations alert'),
|
|
585
574
|
() => log(' [warn] the Operations alert could not be sent'),
|
|
@@ -595,86 +584,101 @@ const verifyExposure = async (env: StepEnv): Promise<void> => {
|
|
|
595
584
|
)
|
|
596
585
|
}
|
|
597
586
|
|
|
598
|
-
/** Write the
|
|
587
|
+
/** Write the managed variables Control handed; `FABRIKA_RELEASE` is never a service variable (ADR-0044). */
|
|
599
588
|
const writeServiceEnv = async (env: StepEnv): Promise<void> => {
|
|
600
|
-
const { zerops, log, signal,
|
|
589
|
+
const { zerops, log, signal, managed } = env
|
|
601
590
|
const serviceId = env.run.target.serviceId
|
|
591
|
+
const written = Object.entries(managed).flatMap(([key, { value, credential }]) =>
|
|
592
|
+
key === 'FABRIKA_RELEASE' || value === null ? [] : [{ key, value, credential }]
|
|
593
|
+
)
|
|
594
|
+
const writtenKeys = new Set(written.map(({ key }) => key))
|
|
602
595
|
if (env.dryRun) {
|
|
603
596
|
const upload = env.run.artifactUpload !== undefined
|
|
604
|
-
const
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
log(
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
597
|
+
const removed = Object.keys(managed).filter((key) => key !== 'FABRIKA_RELEASE' && !writtenKeys.has(key))
|
|
598
|
+
log(
|
|
599
|
+
` [dry-run] would write service variables on ${serviceId}: ${
|
|
600
|
+
[...writtenKeys, ...(upload ? [FABRIKA_SOURCE_MAP_UPLOAD_KEY] : [])].join(', ') || '(none)'
|
|
601
|
+
}`,
|
|
602
|
+
)
|
|
603
|
+
log(
|
|
604
|
+
` [dry-run] would remove service variables on ${serviceId} if present: ${
|
|
605
|
+
[...removed, ...(upload ? [] : [FABRIKA_SOURCE_MAP_UPLOAD_KEY])].join(', ') || '(none)'
|
|
606
|
+
}`,
|
|
607
|
+
)
|
|
608
|
+
log(` [dry-run] would remove every other job, webhook, connector, mail and platform variable on ${serviceId}`)
|
|
615
609
|
return
|
|
616
610
|
}
|
|
617
611
|
assertRunning(signal)
|
|
618
612
|
// One read: every write below waits for its own user-data sync, so the build request never meets `userDataSyncRunning`.
|
|
619
613
|
const records = await zerops.api.listServiceEnv({ serviceId, signal })
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
const sensitive = SENSITIVE_MANAGED_KEYS.has(key)
|
|
624
|
-
assertRunning(signal)
|
|
625
|
-
if (value !== null && (sensitive || readableContent(existing) !== value)) {
|
|
626
|
-
await zerops.api.putServiceEnv({ serviceId, key, value, sensitive, signal })
|
|
627
|
-
log(` ${key}: updated`)
|
|
628
|
-
} else if (value === null && existing !== undefined) {
|
|
629
|
-
await zerops.api.deleteServiceEnv({ envId: existing.id, signal })
|
|
630
|
-
log(` ${key}: removed`)
|
|
631
|
-
} else {
|
|
632
|
-
log(` ${key}: unchanged`)
|
|
633
|
-
}
|
|
614
|
+
const changes: ServiceEnvChange[] = []
|
|
615
|
+
const put = (key: string, value: string, sensitive: boolean): void => {
|
|
616
|
+
changes.push({ line: `${key}: updated`, apply: () => zerops.api.putServiceEnv({ serviceId, key, value, sensitive, signal }) })
|
|
634
617
|
}
|
|
635
|
-
const
|
|
636
|
-
|
|
637
|
-
if (typeof provenance === 'string' && readableContent(marker) !== provenance) {
|
|
638
|
-
assertRunning(signal)
|
|
639
|
-
// Not a credential, and the exposure read-back above reads it: it must stay readable.
|
|
640
|
-
await zerops.api.putServiceEnv({ serviceId, key: STORAGE_PROVENANCE_KEY, value: provenance, sensitive: false, signal })
|
|
641
|
-
log(` ${STORAGE_PROVENANCE_KEY}: updated`)
|
|
642
|
-
} else if (provenance === null && marker !== undefined) {
|
|
643
|
-
assertRunning(signal)
|
|
644
|
-
await zerops.api.deleteServiceEnv({ envId: marker.id, signal })
|
|
645
|
-
log(` ${STORAGE_PROVENANCE_KEY}: removed`)
|
|
618
|
+
const remove = (key: string, envId: string): void => {
|
|
619
|
+
changes.push({ line: `${key}: removed`, apply: () => zerops.api.deleteServiceEnv({ envId, signal }) })
|
|
646
620
|
}
|
|
647
|
-
// A
|
|
648
|
-
for (const
|
|
621
|
+
// A credential is written sensitive and never read back, so it is written every time.
|
|
622
|
+
for (const { key, value, credential } of written) {
|
|
649
623
|
const existing = records.find((record) => record.key === key)
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
if (sensitive || readableContent(existing) !== value) {
|
|
653
|
-
await zerops.api.putServiceEnv({ serviceId, key, value, sensitive, signal })
|
|
654
|
-
log(` ${key}: updated`)
|
|
655
|
-
} else {
|
|
656
|
-
log(` ${key}: unchanged`)
|
|
657
|
-
}
|
|
624
|
+
if (credential || readableContent(existing) !== value) put(key, value, credential)
|
|
625
|
+
else changes.push({ line: `${key}: unchanged`, apply: null })
|
|
658
626
|
}
|
|
627
|
+
// A variable Control no longer hands is removed, so no stale key stays readable. `/env` also reports the descriptor's
|
|
628
|
+
// `FABRIKA_RELEASE`, which Zerops refuses to delete.
|
|
629
|
+
const stale = new Set(staleManagedVariables(records.map((record) => record.key), new Set([...writtenKeys, 'FABRIKA_RELEASE'])))
|
|
659
630
|
for (const record of records) {
|
|
660
|
-
if (
|
|
661
|
-
assertRunning(signal)
|
|
662
|
-
await zerops.api.deleteServiceEnv({ envId: record.id, signal })
|
|
663
|
-
log(` ${record.key}: removed`)
|
|
664
|
-
}
|
|
631
|
+
if (stale.has(record.key)) remove(record.key, record.id)
|
|
665
632
|
}
|
|
633
|
+
const provenance = env.attempt.storageProvenance
|
|
634
|
+
const marker = records.find((record) => record.key === STORAGE_PROVENANCE_KEY)
|
|
635
|
+
// Not a credential, and the exposure read-back above reads it: it must stay readable.
|
|
636
|
+
if (typeof provenance === 'string' && readableContent(marker) !== provenance) put(STORAGE_PROVENANCE_KEY, provenance, false)
|
|
637
|
+
else if (provenance === null && marker !== undefined) remove(STORAGE_PROVENANCE_KEY, marker.id)
|
|
666
638
|
// The build lifts the upload's bearer from here (`${RUNTIME_…}`), so the descriptor never holds it.
|
|
667
639
|
const bearer = env.run.artifactUpload?.bearer ?? null
|
|
668
640
|
const uploadKey = records.find((record) => record.key === FABRIKA_SOURCE_MAP_UPLOAD_KEY)
|
|
669
|
-
if (bearer !== null)
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
641
|
+
if (bearer !== null) put(FABRIKA_SOURCE_MAP_UPLOAD_KEY, bearer, true)
|
|
642
|
+
else if (uploadKey !== undefined) remove(FABRIKA_SOURCE_MAP_UPLOAD_KEY, uploadKey.id)
|
|
643
|
+
await applyServiceEnvChanges(changes, log, signal)
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/** One service variable write or removal `write-service-env` decided on, or a key it leaves as it is (`apply: null`). */
|
|
647
|
+
interface ServiceEnvChange {
|
|
648
|
+
readonly line: string
|
|
649
|
+
readonly apply: (() => Promise<void>) | null
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/** Each write waits ~3 s for its own user-data sync; concurrent writes to one service do not conflict (zerops-platform.md). */
|
|
653
|
+
const SERVICE_ENV_WRITE_CONCURRENCY = 6
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Applies the changes `SERVICE_ENV_WRITE_CONCURRENCY` at a time and waits for every started one, so no sync still runs
|
|
657
|
+
* when the step ends. A failure or a cancel starts no further write; lines follow the decision order.
|
|
658
|
+
*/
|
|
659
|
+
const applyServiceEnvChanges = async (changes: readonly ServiceEnvChange[], log: (line: string) => void, signal: AbortSignal): Promise<void> => {
|
|
660
|
+
const applied = new Set<number>()
|
|
661
|
+
const failures: unknown[] = []
|
|
662
|
+
let next = 0
|
|
663
|
+
const worker = async (): Promise<void> => {
|
|
664
|
+
while (next < changes.length && failures.length === 0 && !signal.aborted) {
|
|
665
|
+
const index = next++
|
|
666
|
+
const apply = changes[index]?.apply ?? null
|
|
667
|
+
if (apply === null) continue
|
|
668
|
+
try {
|
|
669
|
+
await apply()
|
|
670
|
+
applied.add(index)
|
|
671
|
+
} catch (error) {
|
|
672
|
+
failures.push(error)
|
|
673
|
+
}
|
|
674
|
+
}
|
|
677
675
|
}
|
|
676
|
+
await Promise.all(Array.from({ length: SERVICE_ENV_WRITE_CONCURRENCY }, worker))
|
|
677
|
+
for (const [index, change] of changes.entries()) {
|
|
678
|
+
if (change.apply === null || applied.has(index)) log(` ${change.line}`)
|
|
679
|
+
}
|
|
680
|
+
if (failures.length > 0) throw failures[0]
|
|
681
|
+
assertRunning(signal)
|
|
678
682
|
}
|
|
679
683
|
|
|
680
684
|
/** The custom autoscaling with its minimum RAM raised to `minRam`; every other field is sent back as it was read. */
|
|
@@ -690,6 +694,7 @@ const withMinRam = (custom: ZeropsServiceAutoscaling, minRam: number): ZeropsSer
|
|
|
690
694
|
*/
|
|
691
695
|
const raiseStartMemory = async (env: StepEnv): Promise<void> => {
|
|
692
696
|
const { run, zerops, log, signal, dryRun } = env
|
|
697
|
+
if (run.dev !== undefined) await pinOneContainer(env)
|
|
693
698
|
const locked = lockedExecutionFor(run.artifact, run, dryRun)
|
|
694
699
|
const minRam = locked === null ? null : zeropsStartMinRam(locked)
|
|
695
700
|
const hostname = run.artifact.target.deployService
|
|
@@ -712,6 +717,42 @@ const raiseStartMemory = async (env: StepEnv): Promise<void> => {
|
|
|
712
717
|
log(` minimum RAM of ${hostname}: ${before ?? 'the platform default'} → ${after} GB`)
|
|
713
718
|
}
|
|
714
719
|
|
|
720
|
+
/**
|
|
721
|
+
* A dev environment runs on exactly one container: the launcher writes the files it syncs into that container, and a
|
|
722
|
+
* second one would serve the bootstrap head. Sends the custom autoscaling whole with only the container counts changed,
|
|
723
|
+
* and decides by reading it back.
|
|
724
|
+
*/
|
|
725
|
+
const pinOneContainer = async (env: StepEnv): Promise<void> => {
|
|
726
|
+
const { run, zerops, log, signal, dryRun } = env
|
|
727
|
+
const hostname = run.artifact.target.deployService
|
|
728
|
+
if (dryRun) {
|
|
729
|
+
log(` [dry-run] would pin ${hostname} to one container`)
|
|
730
|
+
return
|
|
731
|
+
}
|
|
732
|
+
assertRunning(signal)
|
|
733
|
+
const service = await zerops.api.getService({ serviceId: run.target.serviceId, signal })
|
|
734
|
+
const custom = service.customAutoscaling
|
|
735
|
+
const horizontal = service.currentAutoscaling?.horizontal
|
|
736
|
+
if (horizontal?.minContainerCount === 1 && horizontal.maxContainerCount === 1) {
|
|
737
|
+
log(` ${hostname} runs on one container, unchanged`)
|
|
738
|
+
return
|
|
739
|
+
}
|
|
740
|
+
if (custom === undefined) throw new Error(`zerops: ${hostname} did not report its autoscaling whole, so it cannot be pinned to one container`)
|
|
741
|
+
const process = await zerops.api.configureCustomAutoscaling({
|
|
742
|
+
serviceId: service.id,
|
|
743
|
+
autoscaling: { ...custom, horizontal: { minContainerCount: 1, maxContainerCount: 1 } },
|
|
744
|
+
signal,
|
|
745
|
+
})
|
|
746
|
+
if (process !== null) {
|
|
747
|
+
await waitForProcess({ api: zerops.api, processId: process.id, sleep: zerops.sleep, signal, label: `the autoscaling change of ${hostname}` })
|
|
748
|
+
}
|
|
749
|
+
const after = (await zerops.api.getService({ serviceId: service.id, signal })).currentAutoscaling?.horizontal
|
|
750
|
+
if (after?.minContainerCount !== 1 || after.maxContainerCount !== 1) {
|
|
751
|
+
throw new Error(`zerops: ${hostname} reads back ${after?.maxContainerCount ?? 'no'} container(s) after it was pinned to one`)
|
|
752
|
+
}
|
|
753
|
+
log(` ${hostname} pinned to one container`)
|
|
754
|
+
}
|
|
755
|
+
|
|
715
756
|
/** What raising a minimum RAM calls; the deploy's collaborators, and the live platform-facts probe of the same path. */
|
|
716
757
|
export interface MinRamRaise {
|
|
717
758
|
zerops: Pick<ZeropsCollaborators, 'api' | 'sleep'>
|
|
@@ -777,7 +818,7 @@ const sizeDatabase = async (env: StepEnv): Promise<void> => {
|
|
|
777
818
|
const deploy = await zerops.api.getService({ serviceId: run.target.serviceId, signal })
|
|
778
819
|
const demand: ZeropsDatabaseDemand = {
|
|
779
820
|
maxContainers: deploy.currentAutoscaling?.horizontal.maxContainerCount ?? ZEROPS_MAX_CONTAINERS,
|
|
780
|
-
jobs: zeropsJobProcesses(locked
|
|
821
|
+
jobs: zeropsJobProcesses(locked),
|
|
781
822
|
}
|
|
782
823
|
const service = await zerops.api.findService({ projectId: run.target.projectId, hostname: database.hostname, signal })
|
|
783
824
|
if (service === null) throw new Error(`zerops: the database ${database.hostname} does not exist, so its connections cannot be sized`)
|
|
@@ -939,7 +980,7 @@ const runStep = async (spec: ZeropsStep, env: StepEnv): Promise<void> => {
|
|
|
939
980
|
const appVersionId = requireVersion(env, 'trigger-build')
|
|
940
981
|
await verifyZeropsArtifactSourceDescriptor(artifact.target.sourceDescriptor)
|
|
941
982
|
const { contents } = artifact.target.sourceDescriptor
|
|
942
|
-
const release = env.
|
|
983
|
+
const release = managedValue(env.managed, 'FABRIKA_RELEASE')
|
|
943
984
|
const descriptor = locked === null
|
|
944
985
|
? overlayRelease(contents, artifact.target.zeropsSetup, release)
|
|
945
986
|
: renderLockedExecutionDescriptor(contents, artifact.target.zeropsSetup, release, sizedExecution(locked, attempt))
|
|
@@ -989,35 +1030,6 @@ const runStep = async (spec: ZeropsStep, env: StepEnv): Promise<void> => {
|
|
|
989
1030
|
await awaitVersion(env, appVersionId, attempt.processId, locked !== null && locked.databaseUrl !== null)
|
|
990
1031
|
return
|
|
991
1032
|
}
|
|
992
|
-
|
|
993
|
-
case 'reconcile-schema': {
|
|
994
|
-
const schema = approvedSchemaOf(run)
|
|
995
|
-
const propustkaUrl = target.propustkaUrl
|
|
996
|
-
if (schema === undefined || propustkaUrl === undefined) {
|
|
997
|
-
return
|
|
998
|
-
}
|
|
999
|
-
// Return origins ride this step because `apps.setReturnOrigins` 404s for an app IAM has never
|
|
1000
|
-
// heard of, and the schema reconcile is what registers it. Separate call, same moment.
|
|
1001
|
-
const returnOrigins = run.returnOrigins !== undefined && run.returnOrigins.length > 0 ? run.returnOrigins : undefined
|
|
1002
|
-
const iamApp = appEnvironmentId(artifact.app.id, artifact.app.env)
|
|
1003
|
-
if (dryRun) {
|
|
1004
|
-
log(` [dry-run] would reconcile schema for \`${iamApp}\` against ${propustkaUrl}`)
|
|
1005
|
-
if (returnOrigins !== undefined) {
|
|
1006
|
-
log(` [dry-run] would register return origins for \`${iamApp}\`: ${returnOrigins.join(', ')}`)
|
|
1007
|
-
}
|
|
1008
|
-
return
|
|
1009
|
-
}
|
|
1010
|
-
assertRunning(signal)
|
|
1011
|
-
await zerops.reconcileSchema({
|
|
1012
|
-
url: propustkaUrl,
|
|
1013
|
-
app: iamApp,
|
|
1014
|
-
schema,
|
|
1015
|
-
...(returnOrigins === undefined ? {} : { returnOrigins }),
|
|
1016
|
-
adminKey: target.adminKey,
|
|
1017
|
-
signal,
|
|
1018
|
-
})
|
|
1019
|
-
return
|
|
1020
|
-
}
|
|
1021
1033
|
}
|
|
1022
1034
|
}
|
|
1023
1035
|
|
|
@@ -1039,15 +1051,6 @@ async (run: ZeropsDeployRun): Promise<ZeropsDeployResult> => {
|
|
|
1039
1051
|
throw new Error(`zerops: artifact environment drift: expected \`${run.env}\`, got \`${run.artifact.app.env}\``)
|
|
1040
1052
|
}
|
|
1041
1053
|
assertAuthoredManagedInputs({ variableNames: Object.keys(run.vars), secretNames: Object.keys(run.secrets) })
|
|
1042
|
-
// The proxy serves an endpoint table exactly when Control approved a contract, whatever the artifact says.
|
|
1043
|
-
const contractProxy = run.access !== undefined
|
|
1044
|
-
const { values: handed, familyVariables } = parseManagedEnvironment(run.managedEnvironment)
|
|
1045
|
-
// Only a contract application checks the secret; any other would merely hold a credential its own code can print.
|
|
1046
|
-
const managedValues: ManagedValues = contractProxy ? handed : { ...handed, FABRIKA_PROXY_SECRET: null }
|
|
1047
|
-
if (!run.dryRun && contractProxy && managedValues.FABRIKA_PROXY_SECRET === null) {
|
|
1048
|
-
// The application would refuse every request its proxy sends (ADR-0022 rule 5).
|
|
1049
|
-
throw new Error('zerops: a contract application needs its proxy secret, and Control supplied none')
|
|
1050
|
-
}
|
|
1051
1054
|
const hostnames = manifestServiceHostnames(run.artifact)
|
|
1052
1055
|
const document = interpolateManifest(run.artifact.target.importDocument, run.artifact.app.pipeline.vars, run.vars, hostnames)
|
|
1053
1056
|
const compiled = { yaml: renderYaml(document), hostnames }
|
|
@@ -1061,11 +1064,10 @@ async (run: ZeropsDeployRun): Promise<ZeropsDeployResult> => {
|
|
|
1061
1064
|
signal: run.signal,
|
|
1062
1065
|
dryRun: run.dryRun,
|
|
1063
1066
|
attempt: { triggered: false },
|
|
1064
|
-
|
|
1065
|
-
familyVariables,
|
|
1067
|
+
managed: run.managedEnvironment,
|
|
1066
1068
|
deadlineMs: Date.now() + ZEROPS_VERSION_DEADLINE_MS,
|
|
1067
1069
|
}
|
|
1068
|
-
const steps = buildZeropsPlan(run.artifact
|
|
1070
|
+
const steps = buildZeropsPlan(run.artifact)
|
|
1069
1071
|
const options = { appId: run.appId, env: run.env, dryRun: run.dryRun, signal: run.signal, log: run.events.log }
|
|
1070
1072
|
const result = await runSteps(steps, options, (step) => runStep(step, env))
|
|
1071
1073
|
const { appVersionId, triggered } = env.attempt
|
|
@@ -30,3 +30,21 @@ export const zeropsProxyManifestCarrier = (descriptorContents: string, setup = '
|
|
|
30
30
|
}
|
|
31
31
|
return 'archive'
|
|
32
32
|
}
|
|
33
|
+
|
|
34
|
+
/** The build variable that marks a proxy source whose manifest parser knows `brand` (ADR-0066). */
|
|
35
|
+
export const FABRIKA_PROXY_MANIFEST_BRAND = 'FABRIKA_PROXY_MANIFEST_BRAND'
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Whether a pinned descriptor's proxy reads a manifest that carries the installation's client logo. An older parser
|
|
39
|
+
* refuses the unknown key and fails the build, so a namespace kept on such a pin gets no logo and keeps the fabrika
|
|
40
|
+
* mark until it is re-pinned (`--proxy-source=current`). Decided by the build section Zerops applies, as the carrier is.
|
|
41
|
+
*/
|
|
42
|
+
export const zeropsProxyAcceptsBrand = (descriptorContents: string, setup = 'proxy'): boolean => {
|
|
43
|
+
const parsed = parseZeropsDescriptor(descriptorContents)
|
|
44
|
+
for (const entry of appliedZeropsSetups(parsed, selectZeropsDescriptorSetup(parsed, setup))) {
|
|
45
|
+
const read = buildSectionSchema.safeParse(entry)
|
|
46
|
+
if (!read.success) continue
|
|
47
|
+
return read.data.build.envVariables?.[FABRIKA_PROXY_MANIFEST_BRAND] === '1'
|
|
48
|
+
}
|
|
49
|
+
return false
|
|
50
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ZeropsApi, ZeropsApiOptions } from './types';
|
|
2
|
+
/** Base URL of the Zerops public REST API. */
|
|
3
|
+
export declare const ZEROPS_API_BASE = "https://api.app-prg1.zerops.io/api/rest/public";
|
|
4
|
+
/** Build the real client. Every request carries the bearer and the run's signal. */
|
|
5
|
+
export declare const createZeropsApi: (options: ZeropsApiOptions) => ZeropsApi;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export declare const isAppVersionNotFoundResponse: (value: unknown) => boolean;
|
|
2
|
+
/**
|
|
3
|
+
* A non-2xx answer from the Zerops API. `code` is the platform's own error identifier, so a caller can
|
|
4
|
+
* branch on `userDataDuplicateKey` rather than matching a prose message.
|
|
5
|
+
*/
|
|
6
|
+
export declare class ZeropsApiError extends Error {
|
|
7
|
+
readonly status: number;
|
|
8
|
+
readonly code: string;
|
|
9
|
+
constructor(message: string, status: number, code: string);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The platform's code for "this service publishes no HTTP port", the answer `enableSubdomainAccess` gives
|
|
13
|
+
* before a service's first deploy. Exported so a caller can say what is missing instead of relaying prose.
|
|
14
|
+
*/
|
|
15
|
+
export declare const ZEROPS_SERVICE_NOT_HTTP = "serviceStackIsNotHttp";
|
|
16
|
+
/**
|
|
17
|
+
* Turn a non-2xx response into a `ZeropsApiError`. Zerops answers with `{ error: { code, message, meta } }`
|
|
18
|
+
* (the envelope observed live; the OpenAPI `Error` schema shows the inner `{ code, message }`).
|
|
19
|
+
*
|
|
20
|
+
* `redactDetail` drops the server's MESSAGE, which a validation error can fill with the value it
|
|
21
|
+
* rejected. The CODE is a fixed identifier and is kept either way — without it nothing downstream can
|
|
22
|
+
* tell a duplicate key from a missing service.
|
|
23
|
+
*/
|
|
24
|
+
export declare const apiError: (label: string, status: number, body: unknown, redactDetail: boolean) => ZeropsApiError;
|