@fabrika/provider-zerops 0.8.1 → 0.9.0-cloudflare.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 +16 -14
- package/src/api/client.ts +18 -0
- package/src/api/readers.ts +29 -0
- package/src/api/types.ts +64 -0
- package/src/api.ts +2 -0
- package/src/cli.ts +11 -0
- package/src/collaborators.ts +0 -14
- package/src/control.ts +35 -57
- package/src/execution.ts +113 -102
- package/src/index.ts +2 -0
- package/src/installation/admin.ts +1 -1
- package/src/installation/deploy-options.ts +1 -2
- package/src/installation/deploy.ts +225 -80
- 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 +48 -18
- package/src/installation/hosts.ts +1 -7
- package/src/installation/index.ts +13 -16
- package/src/installation/init-options.ts +2 -1
- package/src/installation/install-environment.ts +47 -108
- package/src/installation/install-options.ts +3 -1
- package/src/installation/install-secrets.ts +24 -68
- package/src/installation/install.ts +9 -4
- package/src/installation/sidecar.ts +5 -26
- package/src/installation/topology/setups.ts +15 -4
- package/src/installation/topology/topology.ts +31 -21
- 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 +24 -4
- package/src/namespace/proxy-attempt.ts +43 -2
- package/src/namespace/proxy-reads.ts +12 -22
- package/src/namespace/types.ts +2 -0
- 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
|
@@ -82,22 +82,27 @@ export type PublicAccess = 'custom-domain' | 'zerops-subdomain'
|
|
|
82
82
|
* `standard` is the production shape: HA Postgres, a separate database and bucket for Operations, and
|
|
83
83
|
* enough containers that no single one is a point of failure.
|
|
84
84
|
*
|
|
85
|
-
* `light` is one project
|
|
86
|
-
*
|
|
85
|
+
* `light` is one project with two `postgresql:single@18` services — `db` for IAM and control, `operationsdb`
|
|
86
|
+
* for Operations — and one bucket, at one container per service. This tier exists because the standard
|
|
87
87
|
* shape costs ~13 containers before a single application is deployed, which is more platform than a
|
|
88
|
-
* small fleet — or an evaluation — can justify.
|
|
88
|
+
* small fleet — or an evaluation — can justify. Applications run in their own namespace projects
|
|
89
|
+
* (ADR-0034); a production namespace that must keep its data through a node loss asks for HA there.
|
|
89
90
|
*
|
|
90
|
-
*
|
|
91
|
+
* Operations gets its own database, not a schema beside IAM's, because Zerops backs up a whole service on
|
|
92
|
+
* one schedule: identity and deploy state is worth an hourly dump, error history is not, and only two
|
|
93
|
+
* services can carry two schedules. It also keeps Operations' high-volume writes off the identity path.
|
|
91
94
|
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
+
* Sharing the rest is safe rather than merely cheap, and that is a property of the code, not of this comment:
|
|
96
|
+
*
|
|
97
|
+
* • IAM's and control's table names do not intersect, and ADR-0017 gives each its own migration ledger
|
|
98
|
+
* and advisory lock, so one database holds both without coordination. A light installation created
|
|
99
|
+
* before Operations moved out still holds all three in `db`, which the same property covers.
|
|
95
100
|
* • Their blob keys are prefix-disjoint — control writes `runs/`, Operations writes `events/`,
|
|
96
101
|
* `dead/` and `source-maps/` — so one bucket cannot collide.
|
|
97
102
|
*
|
|
98
|
-
* What `light` genuinely gives up is
|
|
99
|
-
*
|
|
100
|
-
* That is the trade, and it is why `standard` remains the default rather than the special case.
|
|
103
|
+
* What `light` genuinely gives up is AVAILABILITY and DURABILITY: one container per service, and a
|
|
104
|
+
* single-node database loses everything since its last backup when its node fails (Zerops has no
|
|
105
|
+
* point-in-time recovery). That is the trade, and it is why `standard` remains the default rather than the special case.
|
|
101
106
|
*/
|
|
102
107
|
export type PlatformTier = 'standard' | 'light'
|
|
103
108
|
|
|
@@ -188,9 +193,9 @@ export const platformTopology = (options: TopologyOptions): ProjectTopology => {
|
|
|
188
193
|
hostname: 'db',
|
|
189
194
|
// HA is encoded in the TYPE. The deprecated `mode` field is not merely avoided here — it
|
|
190
195
|
// is absent from `ZeropsServiceSpec`, so it cannot be written. One database serves both
|
|
191
|
-
// IAM and the control plane;
|
|
192
|
-
// because a second HA Postgres triples
|
|
193
|
-
// already gives us.
|
|
196
|
+
// IAM and the control plane; both use its default schema with disjoint tables and their own
|
|
197
|
+
// migration ledgers (ADR-0017), not separate services, because a second HA Postgres triples
|
|
198
|
+
// the cost of the isolation the private network already gives us.
|
|
194
199
|
type: 'postgresql:ha@18',
|
|
195
200
|
// The identity and control-plane database: every login, every token mint, every run's
|
|
196
201
|
// state. `oltp-production` is also what an HA service gets by DEFAULT — stated here so
|
|
@@ -315,10 +320,9 @@ const lightPlatformTopology = (options: TopologyOptions, publicAccess: PublicAcc
|
|
|
315
320
|
// that creates the project. Same shape as `compileZeropsNamespaceTopology` in `src/namespace/topology.ts`.
|
|
316
321
|
const services = (): ZeropsServiceSpec[] => [
|
|
317
322
|
{
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
-
// a `${operationsdb_connectionString}` baked into it would name a service `light` does not have.
|
|
323
|
+
// IAM and control. Which schema each one gets is not expressed here — it is `FABRIKA_*_DATABASE_URL`,
|
|
324
|
+
// written per installation through the env API, because this same repository-root `zerops.yaml`
|
|
325
|
+
// serves both tiers.
|
|
322
326
|
hostname: 'db',
|
|
323
327
|
type: 'postgresql:single@18',
|
|
324
328
|
// `oltp-hobby`, written out because a default is not a choice — this type's default is
|
|
@@ -326,15 +330,21 @@ const lightPlatformTopology = (options: TopologyOptions, publicAccess: PublicAcc
|
|
|
326
330
|
//
|
|
327
331
|
// A floor is not headroom. Every PostgreSQL profile shares the same 8-core / 48 GB / 250 GB
|
|
328
332
|
// ceiling and the same vertical autoscaler, so `oltp-staging` does not let this service grow
|
|
329
|
-
// any further than `oltp-hobby` does — it only pre-pays for memory the tier is idle on.
|
|
330
|
-
// this single service holds IAM, control, Operations and the apps beside them is an argument
|
|
331
|
-
// about the ceiling, which is unchanged.
|
|
333
|
+
// any further than `oltp-hobby` does — it only pre-pays for memory the tier is idle on.
|
|
332
334
|
//
|
|
333
335
|
// An installation that wants a specific floor sets one on the service — `minRam` accepts a
|
|
334
336
|
// float — which is where a per-installation value belongs (ADR-0004), not in this document.
|
|
335
337
|
profile: 'oltp-hobby',
|
|
336
338
|
priority: 100,
|
|
337
339
|
},
|
|
340
|
+
{
|
|
341
|
+
// Operations' own database: its own backup schedule (the Zerops default, daily) and its own
|
|
342
|
+
// capacity, off the identity path. Same type and profile as `db`, for the same reasons.
|
|
343
|
+
hostname: 'operationsdb',
|
|
344
|
+
type: 'postgresql:single@18',
|
|
345
|
+
profile: 'oltp-hobby',
|
|
346
|
+
priority: 100,
|
|
347
|
+
},
|
|
338
348
|
{
|
|
339
349
|
// ONE bucket. Prefix-disjoint by construction: `runs/` (control) vs `events/`, `dead/` and
|
|
340
350
|
// `source-maps/` (Operations).
|
|
@@ -367,7 +377,7 @@ const lightPlatformTopology = (options: TopologyOptions, publicAccess: PublicAcc
|
|
|
367
377
|
platform: 'zerops',
|
|
368
378
|
project: {
|
|
369
379
|
name: 'platform',
|
|
370
|
-
description: 'fabrika light tier: control plane, Operations, IAM
|
|
380
|
+
description: 'fabrika light tier: control plane, Operations, IAM and the auth proxy, on single-node data services.',
|
|
371
381
|
corePackage: 'LIGHT',
|
|
372
382
|
tags: ['fabrika', 'platform', 'light', options.env],
|
|
373
383
|
},
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
// The pin is checked BEFORE anything leaves this machine: `assertPinnedTag` refuses a branch or a SHA
|
|
9
9
|
// (ADR-0025) without a network call, so `--to=main` fails on the spot rather than after a clone.
|
|
10
10
|
|
|
11
|
-
import { describeArgument, readFlag } from '@fabrika/installation-init'
|
|
12
|
-
import {
|
|
11
|
+
import { assertPinnedTag, describeArgument, readFlag } from '@fabrika/installation-init'
|
|
12
|
+
import { defaultCheckoutDir } from './sidecar'
|
|
13
13
|
|
|
14
14
|
/** Where the sidecar repository this command edits is found. */
|
|
15
15
|
export type UpgradeSidecar =
|
package/src/managed-env.ts
CHANGED
|
@@ -1,18 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
FRAMEWORK_VARIABLE_PREFIXES,
|
|
3
|
-
type FrameworkVariableName,
|
|
4
|
-
frameworkVariableReference,
|
|
5
|
-
isConnectKeyVariable,
|
|
6
|
-
isConnectUrlVariable,
|
|
7
|
-
isEmailTransportVariable,
|
|
8
|
-
isFrameworkVariableName,
|
|
9
|
-
isPlatformTransportVariable,
|
|
10
|
-
isWorkloadKeyVariable,
|
|
11
|
-
PLATFORM_RECEIVERS,
|
|
12
|
-
platformTransportVariables,
|
|
13
|
-
} from '@fabrika/auth-core'
|
|
1
|
+
import { frameworkVariableReference, isFrameworkVariableName } from '@fabrika/auth-core'
|
|
14
2
|
import type { JsonValue } from '@fabrika/provider-contract'
|
|
15
|
-
import { z } from 'zod'
|
|
16
3
|
import { renderYaml } from './compile'
|
|
17
4
|
import {
|
|
18
5
|
assertOwnRunWhenExtending,
|
|
@@ -22,166 +9,6 @@ import {
|
|
|
22
9
|
selectZeropsDescriptorSetup,
|
|
23
10
|
} from './descriptor'
|
|
24
11
|
|
|
25
|
-
/** The framework-only names Control hands every Zerops deploy; the rest of the list is the provider's own. */
|
|
26
|
-
export type ManagedKey = Extract<
|
|
27
|
-
FrameworkVariableName,
|
|
28
|
-
| 'FABRIKA_RELEASE'
|
|
29
|
-
| 'FABRIKA_OPERATIONS_DSN'
|
|
30
|
-
| 'FABRIKA_IAM_ISSUER'
|
|
31
|
-
| 'FABRIKA_IAM_APP_ID'
|
|
32
|
-
| 'FABRIKA_APP_ID'
|
|
33
|
-
| 'FABRIKA_ENVIRONMENT'
|
|
34
|
-
| 'FABRIKA_SERVICE_KEY'
|
|
35
|
-
| 'FABRIKA_PROXY_SECRET'
|
|
36
|
-
| 'FABRIKA_IAM_RPC_URL'
|
|
37
|
-
| 'FABRIKA_IAM_RPC_KEY'
|
|
38
|
-
| 'FABRIKA_APP_ORIGIN'
|
|
39
|
-
| 'FABRIKA_TOKEN_KEYS'
|
|
40
|
-
>
|
|
41
|
-
|
|
42
|
-
export const MANAGED_KEYS: readonly ManagedKey[] = [
|
|
43
|
-
'FABRIKA_RELEASE',
|
|
44
|
-
'FABRIKA_OPERATIONS_DSN',
|
|
45
|
-
'FABRIKA_IAM_ISSUER',
|
|
46
|
-
'FABRIKA_IAM_APP_ID',
|
|
47
|
-
'FABRIKA_APP_ID',
|
|
48
|
-
'FABRIKA_ENVIRONMENT',
|
|
49
|
-
'FABRIKA_SERVICE_KEY',
|
|
50
|
-
'FABRIKA_PROXY_SECRET',
|
|
51
|
-
'FABRIKA_IAM_RPC_URL',
|
|
52
|
-
'FABRIKA_IAM_RPC_KEY',
|
|
53
|
-
'FABRIKA_APP_ORIGIN',
|
|
54
|
-
'FABRIKA_TOKEN_KEYS',
|
|
55
|
-
]
|
|
56
|
-
|
|
57
|
-
/** The managed keys written as service variables; `FABRIKA_RELEASE` travels only in the execution descriptor (ADR-0044). */
|
|
58
|
-
export const SERVICE_MANAGED_KEYS: readonly Exclude<ManagedKey, 'FABRIKA_RELEASE'>[] = [
|
|
59
|
-
'FABRIKA_OPERATIONS_DSN',
|
|
60
|
-
'FABRIKA_IAM_ISSUER',
|
|
61
|
-
'FABRIKA_IAM_APP_ID',
|
|
62
|
-
'FABRIKA_APP_ID',
|
|
63
|
-
'FABRIKA_ENVIRONMENT',
|
|
64
|
-
'FABRIKA_SERVICE_KEY',
|
|
65
|
-
'FABRIKA_PROXY_SECRET',
|
|
66
|
-
'FABRIKA_IAM_RPC_URL',
|
|
67
|
-
'FABRIKA_IAM_RPC_KEY',
|
|
68
|
-
'FABRIKA_APP_ORIGIN',
|
|
69
|
-
'FABRIKA_TOKEN_KEYS',
|
|
70
|
-
]
|
|
71
|
-
|
|
72
|
-
/** The managed keys that carry a credential, written sensitive and therefore never compared. */
|
|
73
|
-
export const SENSITIVE_MANAGED_KEYS: ReadonlySet<ManagedKey> = new Set<ManagedKey>([
|
|
74
|
-
'FABRIKA_OPERATIONS_DSN',
|
|
75
|
-
'FABRIKA_SERVICE_KEY',
|
|
76
|
-
'FABRIKA_PROXY_SECRET',
|
|
77
|
-
'FABRIKA_IAM_RPC_KEY',
|
|
78
|
-
'FABRIKA_TOKEN_KEYS',
|
|
79
|
-
])
|
|
80
|
-
|
|
81
|
-
const PLATFORM_URL_VARIABLES: ReadonlySet<string> = new Set(PLATFORM_RECEIVERS.map((receiver) => platformTransportVariables(receiver).url))
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* A workload, connect or platform transport key and every mail transport variable are written sensitive; a connect or
|
|
85
|
-
* platform transport URL is an origin.
|
|
86
|
-
*/
|
|
87
|
-
export const isSensitiveFamilyVariable = (name: string): boolean => !isConnectUrlVariable(name) && !PLATFORM_URL_VARIABLES.has(name)
|
|
88
|
-
|
|
89
|
-
export type ManagedValues = Record<ManagedKey, string | null>
|
|
90
|
-
|
|
91
|
-
const managedValuesSchema = z.object({
|
|
92
|
-
FABRIKA_RELEASE: z.string().nullable(),
|
|
93
|
-
FABRIKA_OPERATIONS_DSN: z.string().nullable(),
|
|
94
|
-
FABRIKA_IAM_ISSUER: z.string().nullable(),
|
|
95
|
-
FABRIKA_IAM_APP_ID: z.string().nullable(),
|
|
96
|
-
FABRIKA_APP_ID: z.string().nullable(),
|
|
97
|
-
FABRIKA_ENVIRONMENT: z.string().nullable(),
|
|
98
|
-
FABRIKA_SERVICE_KEY: z.string().nullable(),
|
|
99
|
-
FABRIKA_PROXY_SECRET: z.string().nullable(),
|
|
100
|
-
// IAM's membership transport (ADR-0022 rule 6): its public origin and this environment's key, or both null.
|
|
101
|
-
FABRIKA_IAM_RPC_URL: z.string().nullable(),
|
|
102
|
-
FABRIKA_IAM_RPC_KEY: z.string().nullable(),
|
|
103
|
-
// The environment's canonical origin (`ctx.url`), or null when it has no public origin.
|
|
104
|
-
FABRIKA_APP_ORIGIN: z.string().nullable(),
|
|
105
|
-
// The keys of the application's signed tokens, or null when its contract declares no token purpose.
|
|
106
|
-
FABRIKA_TOKEN_KEYS: z.string().nullable(),
|
|
107
|
-
}).strict()
|
|
108
|
-
|
|
109
|
-
export function parseManagedValues(value: unknown): ManagedValues {
|
|
110
|
-
const result = managedValuesSchema.safeParse(value)
|
|
111
|
-
if (!result.success) throw new Error('zerops: managed environment must name exactly the twelve managed keys')
|
|
112
|
-
return result.data
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* What Control hands a deploy: the twelve managed keys, the key of each job and webhook of the approved contract, the
|
|
117
|
-
* key and owner origin of each connector import, and the installation's mail transport when a job sends through it.
|
|
118
|
-
*/
|
|
119
|
-
export interface ManagedEnvironment {
|
|
120
|
-
readonly values: ManagedValues
|
|
121
|
-
/**
|
|
122
|
-
* `FABRIKA_<KIND>_<NAME>_KEY` → the workload's `px_` key, `FABRIKA_CONNECT_<IMPORT>_KEY` / `_URL`, each
|
|
123
|
-
* `FABRIKA_EMAIL_*` of the installation's mail transport, and `FABRIKA_PLATFORM_<RECEIVER>_URL` / `_KEY`.
|
|
124
|
-
*/
|
|
125
|
-
readonly familyVariables: Readonly<Record<string, string>>
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const keySchema = z.string().startsWith('px_').min(4)
|
|
129
|
-
const mailValueSchema = z.string().min(1)
|
|
130
|
-
const originSchema = (schemes: readonly string[]) =>
|
|
131
|
-
z.string().refine((value) => {
|
|
132
|
-
try {
|
|
133
|
-
const url = new URL(value)
|
|
134
|
-
return url.origin === value && schemes.includes(url.protocol)
|
|
135
|
-
} catch {
|
|
136
|
-
return false
|
|
137
|
-
}
|
|
138
|
-
})
|
|
139
|
-
const connectOriginSchema = originSchema(['https:'])
|
|
140
|
-
// Control's public origin is http in the local stack (`http://control.fabrika.localhost:18080`).
|
|
141
|
-
const platformOriginSchema = originSchema(['http:', 'https:'])
|
|
142
|
-
|
|
143
|
-
export function parseManagedEnvironment(value: Readonly<Record<string, string | null>>): ManagedEnvironment {
|
|
144
|
-
const managed: Record<string, string | null> = {}
|
|
145
|
-
const familyVariables: Record<string, string> = {}
|
|
146
|
-
for (const [name, entry] of Object.entries(value)) {
|
|
147
|
-
if (!isFrameworkFamilyVariable(name)) {
|
|
148
|
-
managed[name] = entry
|
|
149
|
-
continue
|
|
150
|
-
}
|
|
151
|
-
if (isEmailTransportVariable(name)) {
|
|
152
|
-
const parsed = mailValueSchema.safeParse(entry)
|
|
153
|
-
if (!parsed.success) throw new Error(`zerops: ${name} must not be empty`)
|
|
154
|
-
familyVariables[name] = parsed.data
|
|
155
|
-
continue
|
|
156
|
-
}
|
|
157
|
-
if (isPlatformTransportVariable(name)) {
|
|
158
|
-
const address = PLATFORM_URL_VARIABLES.has(name)
|
|
159
|
-
const parsed = (address ? platformOriginSchema : keySchema).safeParse(entry)
|
|
160
|
-
if (!parsed.success) throw new Error(`zerops: ${name} must be ${address ? 'an http(s) origin' : 'a px_ key'}`)
|
|
161
|
-
familyVariables[name] = parsed.data
|
|
162
|
-
continue
|
|
163
|
-
}
|
|
164
|
-
const origin = isConnectUrlVariable(name)
|
|
165
|
-
if (!origin && !isWorkloadKeyVariable(name) && !isConnectKeyVariable(name)) {
|
|
166
|
-
throw new Error(`zerops: ${name} is not a variable a deploy writes`)
|
|
167
|
-
}
|
|
168
|
-
const parsed = (origin ? connectOriginSchema : keySchema).safeParse(entry)
|
|
169
|
-
if (!parsed.success) {
|
|
170
|
-
throw new Error(`zerops: ${name} must be ${origin ? 'an https origin' : 'a px_ key'}`)
|
|
171
|
-
}
|
|
172
|
-
familyVariables[name] = parsed.data
|
|
173
|
-
}
|
|
174
|
-
return { values: parseManagedValues(managed), familyVariables }
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* A service variable only the platform writes per workload, import, platform receiver or the installation's mail
|
|
179
|
-
* transport: a stale one is removed when the deploy no longer hands it.
|
|
180
|
-
*/
|
|
181
|
-
export function isFrameworkFamilyVariable(name: string): boolean {
|
|
182
|
-
return FRAMEWORK_VARIABLE_PREFIXES.some((prefix) => name.startsWith(prefix))
|
|
183
|
-
}
|
|
184
|
-
|
|
185
12
|
const refuse = (): never => {
|
|
186
13
|
throw new Error('zerops: application configuration uses a Fabrika-managed environment name')
|
|
187
14
|
}
|
package/src/manifest.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CONTRACT_JOB_NAME_PATTERN, CRON_SCHEDULE_PATTERN } from '@fabrika/app/build'
|
|
1
2
|
import { appSchemaSchema } from '@fabrika/auth'
|
|
2
3
|
import { type AppSchema, type ContractArtifact, contractDigest, isDigest } from '@fabrika/auth-core'
|
|
3
4
|
import type { OperationsSourceMapBuildUpload } from '@fabrika/operations-contract/releases'
|
|
@@ -12,10 +13,12 @@ import {
|
|
|
12
13
|
FABRIKA_CHECK_VERSION,
|
|
13
14
|
LOCKED_APP_ID_PATTERN,
|
|
14
15
|
lockedAuthoredBuild,
|
|
16
|
+
READINESS_PATH_PATTERN,
|
|
15
17
|
ZEROPS_RUNTIME_PACKAGES,
|
|
16
18
|
zeropsAssetRoots,
|
|
17
19
|
type ZeropsContractRun,
|
|
18
20
|
zeropsContractRun,
|
|
21
|
+
type ZeropsDevExecution,
|
|
19
22
|
type ZeropsLockedExecution,
|
|
20
23
|
type ZeropsRuntimePackage,
|
|
21
24
|
} from './execution'
|
|
@@ -310,15 +313,10 @@ const importDocumentSchema = z.object({ project: importProjectSchema.optional(),
|
|
|
310
313
|
/** The import document a manifest carries: every service already holds the ADR-0004 invariants. */
|
|
311
314
|
export type FabrikaManifestImportDocument = z.infer<typeof importDocumentSchema>
|
|
312
315
|
|
|
313
|
-
/** A literal path of unreserved characters: it is written into the generated probes unquoted by any shell. */
|
|
314
|
-
const READINESS_PATH = /^(?:\/[A-Za-z0-9_~-][A-Za-z0-9._~-]*)+$/
|
|
315
|
-
const JOB_NAME = /^[A-Za-z][A-Za-z0-9_]*$/
|
|
316
|
-
const CRON_SCHEDULE = /^[0-9*/,-]+(?: [0-9*/,-]+){4}$/
|
|
317
|
-
|
|
318
316
|
const contractRunSchema = z.object({
|
|
319
317
|
digest: z.string().regex(CONTRACT_DIGEST_PATTERN),
|
|
320
|
-
readinessPath: z.string().regex(
|
|
321
|
-
jobs: z.array(z.object({ name: z.string().regex(
|
|
318
|
+
readinessPath: z.string().regex(READINESS_PATH_PATTERN),
|
|
319
|
+
jobs: z.array(z.object({ name: z.string().regex(CONTRACT_JOB_NAME_PATTERN), schedule: z.string().regex(CRON_SCHEDULE_PATTERN) }).strict()),
|
|
322
320
|
files: z.literal(true).optional(),
|
|
323
321
|
scan: z.literal(true).optional(),
|
|
324
322
|
}).strict()
|
|
@@ -476,8 +474,11 @@ export const lockedExecutionOf = (manifest: FabrikaManifest): ZeropsLockedExecut
|
|
|
476
474
|
}
|
|
477
475
|
}
|
|
478
476
|
|
|
479
|
-
/**
|
|
480
|
-
|
|
477
|
+
/**
|
|
478
|
+
* What Control lets a provider publish for one environment (`ProviderEnvironment.access`), and, for an environment of
|
|
479
|
+
* kind `dev`, the commit its bootstrap builds.
|
|
480
|
+
*/
|
|
481
|
+
export type ZeropsAccess = Pick<ProviderEnvironment, 'access'> & { readonly dev?: ZeropsDevExecution }
|
|
481
482
|
|
|
482
483
|
/**
|
|
483
484
|
* The execution descriptor's lock-down at deploy (ADR-0050): Control's approved contract decides it, not the artifact,
|
|
@@ -506,6 +507,7 @@ export function lockedExecutionFor(
|
|
|
506
507
|
storageHostname: applicationStorageHostname(manifest.target.importDocument.services),
|
|
507
508
|
runtimePackages: manifest.target.runtimePackages ?? [],
|
|
508
509
|
sourceMaps: artifactUpload === undefined ? null : sourceMapUpload(artifactUpload, access.contract),
|
|
510
|
+
...(granted.dev === undefined ? {} : { dev: granted.dev }),
|
|
509
511
|
}
|
|
510
512
|
}
|
|
511
513
|
|
|
@@ -522,11 +524,6 @@ const sourceMapUpload = (upload: NonNullable<ProviderDeployInput['artifactUpload
|
|
|
522
524
|
assetRoots: zeropsAssetRoots(contract),
|
|
523
525
|
})
|
|
524
526
|
|
|
525
|
-
/** The IAM schema a deploy, cancel or reconcile applies: the approved contract's. `undefined` reconciles nothing. */
|
|
526
|
-
export function approvedSchemaOf(granted: ZeropsAccess): AppSchema | undefined {
|
|
527
|
-
return granted.access?.schema
|
|
528
|
-
}
|
|
529
|
-
|
|
530
527
|
export const manifestServiceHostnames = (manifest: FabrikaManifest): string[] =>
|
|
531
528
|
manifest.target.importDocument.services.map((service) => service.hostname)
|
|
532
529
|
|
package/src/namespace/claims.ts
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ProviderClaimedResource,
|
|
3
|
+
ProviderClaimedResourcesInput,
|
|
4
|
+
ProviderDeploymentNamespace,
|
|
5
|
+
ProviderRegistration,
|
|
6
|
+
} from '@fabrika/provider-contract'
|
|
2
7
|
import { isProxyHost } from '@fabrika/proxy-core/manifest'
|
|
8
|
+
import { type ZeropsApi, ZeropsApiError } from '../api'
|
|
3
9
|
import { assertFabrikaManifestCoordinates, manifestServiceHostnames, zeropsArtifactCodec } from '../manifest'
|
|
4
10
|
import { zeropsSharedServicePrefix } from '../service-names'
|
|
5
11
|
import { decodeEnvelope } from './codec'
|
|
6
12
|
import { ZEROPS_NAMESPACE_POSTGRES_HOSTNAME, ZEROPS_NAMESPACE_PROXY_HOSTNAME } from './constants'
|
|
13
|
+
import { asNamespaceError } from './errors'
|
|
7
14
|
|
|
8
15
|
export const namespaceResourceClaims = (namespace: ProviderDeploymentNamespace): string[] => {
|
|
9
16
|
const target = decodeEnvelope(namespace.target)
|
|
@@ -75,3 +82,51 @@ export const registrationResourceClaims = (registration: ProviderRegistration):
|
|
|
75
82
|
}
|
|
76
83
|
return hostnames.map((hostname) => `service:${hostname}`).sort()
|
|
77
84
|
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The platform's answer for a project that does not exist, a deleted one included. A project the token holds no grant
|
|
88
|
+
* on answers `403 insufficientPermissions` instead (zerops-platform.md), so this never reads an unreachable one as gone.
|
|
89
|
+
*/
|
|
90
|
+
export const ZEROPS_PROJECT_NOT_FOUND = 'projectNotFound'
|
|
91
|
+
|
|
92
|
+
const SERVICE_CLAIM_PREFIX = 'service:'
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Whether each claimed service still exists in the namespace's project. Gone means the project reads `projectNotFound`,
|
|
96
|
+
* or it exists and has no service under the claimed hostname. Any other answer, a key that names no service, or a
|
|
97
|
+
* namespace that never recorded its project throws: Control then releases nothing.
|
|
98
|
+
*/
|
|
99
|
+
export const claimedResources = async (
|
|
100
|
+
api: Pick<ZeropsApi, 'getProject' | 'findService'>,
|
|
101
|
+
input: ProviderClaimedResourcesInput,
|
|
102
|
+
): Promise<ProviderClaimedResource[]> => {
|
|
103
|
+
try {
|
|
104
|
+
const services = input.resourceKeys.map((resourceKey) => {
|
|
105
|
+
const hostname = resourceKey.startsWith(SERVICE_CLAIM_PREFIX) ? resourceKey.slice(SERVICE_CLAIM_PREFIX.length) : ''
|
|
106
|
+
if (hostname === '') throw new Error(`Zerops cannot tell whether claimed resource \`${resourceKey}\` exists`)
|
|
107
|
+
return { resourceKey, hostname }
|
|
108
|
+
})
|
|
109
|
+
const projectId = decodeEnvelope(input.namespace.target).projectId
|
|
110
|
+
// An interrupted first provision can leave a project its marker names but the target does not.
|
|
111
|
+
if (projectId === undefined) {
|
|
112
|
+
throw new Error(`Zerops namespace \`${input.namespace.id}\` records no project, so its services cannot be read back`)
|
|
113
|
+
}
|
|
114
|
+
if (!(await projectExists(api, projectId, input.signal))) return services.map(({ resourceKey }) => ({ resourceKey, exists: false }))
|
|
115
|
+
return await Promise.all(services.map(async ({ resourceKey, hostname }) => ({
|
|
116
|
+
resourceKey,
|
|
117
|
+
exists: (await api.findService({ projectId, hostname, signal: input.signal })) !== null,
|
|
118
|
+
})))
|
|
119
|
+
} catch (cause) {
|
|
120
|
+
throw asNamespaceError(cause, 'claim check')
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export const projectExists = async (api: Pick<ZeropsApi, 'getProject'>, projectId: string, signal: AbortSignal): Promise<boolean> => {
|
|
125
|
+
try {
|
|
126
|
+
await api.getProject({ projectId, signal })
|
|
127
|
+
return true
|
|
128
|
+
} catch (error) {
|
|
129
|
+
if (error instanceof ZeropsApiError && error.code === ZEROPS_PROJECT_NOT_FOUND) return false
|
|
130
|
+
throw error
|
|
131
|
+
}
|
|
132
|
+
}
|
package/src/namespace/errors.ts
CHANGED
|
@@ -42,7 +42,7 @@ const splitApiMessage = (message: string, code: string): { summary: string; deta
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
/** Every namespace lifecycle failure leaves this provider as one typed, coded error. */
|
|
45
|
-
export const asNamespaceError = (cause: unknown, mode: 'provision' | 'reconcile' | 'update'): ProviderNamespaceError => {
|
|
45
|
+
export const asNamespaceError = (cause: unknown, mode: 'provision' | 'reconcile' | 'update' | 'claim check'): ProviderNamespaceError => {
|
|
46
46
|
if (cause instanceof ProviderNamespaceError) return cause
|
|
47
47
|
// The same value is refused again on every retry; only a smaller manifest or a new carrier fixes it.
|
|
48
48
|
if (cause instanceof ZeropsServiceEnvValueTooLargeError) return namespaceError(cause.code, cause.message, false)
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { appEnvironmentId } from '@fabrika/auth-core'
|
|
2
2
|
import { decodeEnvelope, type ProviderNamespaceMember } from '@fabrika/provider-contract'
|
|
3
|
-
import {
|
|
4
|
-
import { contractProxyTable, type ProxyManifest } from '@fabrika/proxy-core/manifest'
|
|
3
|
+
import { contractProxyEntry, contractProxyTable, type ProxyManifest } from '@fabrika/proxy-core/manifest'
|
|
5
4
|
import { assertFabrikaManifestCoordinates, zeropsArtifactCodec } from '../manifest'
|
|
6
5
|
|
|
7
6
|
/** Compile every public member of one deployment namespace into its strict proxy manifest. */
|
|
@@ -28,29 +27,17 @@ export function compileNamespaceProxyManifest(namespaceId: string, members: read
|
|
|
28
27
|
if (hosts.has(host)) throw new Error(`duplicate proxy host \`${host}\` in deployment namespace ${namespaceId}`)
|
|
29
28
|
ids.add(id)
|
|
30
29
|
hosts.add(host)
|
|
31
|
-
|
|
32
|
-
if (operationsOrigin !== undefined && !isPolicyOrigin(operationsOrigin, ['https:', 'http:'])) {
|
|
33
|
-
throw new Error(`Zerops proxy target ${member.appId}/${member.env} has an Operations ingest origin that is not an http(s) origin`)
|
|
34
|
-
}
|
|
35
|
-
const crossOrigins = member.crossOrigins ?? []
|
|
36
|
-
if (
|
|
37
|
-
crossOrigins.length > MAX_CROSS_ORIGINS
|
|
38
|
-
|| new Set(crossOrigins).size !== crossOrigins.length
|
|
39
|
-
|| crossOrigins.some((origin) => !isCrossOrigin(origin) || new URL(origin).hostname === host)
|
|
40
|
-
) {
|
|
41
|
-
throw new Error(`Zerops proxy target ${member.appId}/${member.env} has a cross-origin list the proxy manifest refuses`)
|
|
42
|
-
}
|
|
43
|
-
apps.push({
|
|
30
|
+
apps.push(contractProxyEntry({
|
|
44
31
|
id,
|
|
45
32
|
hosts: [host],
|
|
46
33
|
upstream: proxy.upstream,
|
|
47
34
|
// A Zerops public domain is always served over the project's TLS-terminating L7 balancer, so the
|
|
48
35
|
// browser speaks https even though the proxy itself is reached over plain HTTP (ADR-0022).
|
|
49
36
|
scheme: 'https',
|
|
50
|
-
|
|
51
|
-
...(operationsOrigin === undefined ? {} : { operationsOrigin }),
|
|
52
|
-
...(crossOrigins
|
|
53
|
-
})
|
|
37
|
+
table: contractProxyTable(access.contract, access.contractDigest),
|
|
38
|
+
...(member.operationsOrigin === undefined ? {} : { operationsOrigin: member.operationsOrigin }),
|
|
39
|
+
...(member.crossOrigins === undefined ? {} : { crossOrigins: member.crossOrigins }),
|
|
40
|
+
}))
|
|
54
41
|
}
|
|
55
42
|
return { apps }
|
|
56
43
|
}
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
type ZeropsService,
|
|
18
18
|
} from '../api'
|
|
19
19
|
import { defaultSleep } from '../collaborators'
|
|
20
|
-
import { namespaceResourceClaims, registrationResourceClaims } from './claims'
|
|
20
|
+
import { claimedResources, namespaceResourceClaims, projectExists, registrationResourceClaims, ZEROPS_PROJECT_NOT_FOUND } from './claims'
|
|
21
21
|
import { checkpoint, decodeEnvelope, normalizedNamespace, normalizeTarget } from './codec'
|
|
22
22
|
import { ZEROPS_NAMESPACE_POSTGRES_HOSTNAME, ZEROPS_NAMESPACE_PROXY_HOSTNAME, ZEROPS_NAMESPACE_SUBDOMAIN_VARIABLE } from './constants'
|
|
23
23
|
import { asNamespaceError, namespaceError, ZEROPS_NAMESPACE_CANCELLED, ZEROPS_SUBDOMAIN_NOT_PUBLISHED } from './errors'
|
|
@@ -67,10 +67,18 @@ const validateProject = (
|
|
|
67
67
|
target: NormalizedZeropsNamespaceTarget,
|
|
68
68
|
project: ZeropsProject,
|
|
69
69
|
adopting: boolean,
|
|
70
|
+
installationProjectId: string | undefined,
|
|
70
71
|
): void => {
|
|
71
72
|
if (project.id === '') {
|
|
72
73
|
throw new Error('Zerops namespace project returned an empty id')
|
|
73
74
|
}
|
|
75
|
+
// Checked on every reconcile, before the import and the proxy build, so a namespace that already adopted it stops
|
|
76
|
+
// here instead of rebuilding the platform's enforcement point with an empty manifest (ADR-0034).
|
|
77
|
+
if (project.id === installationProjectId) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`Zerops project ${project.id} is the installation's own project; a deployment namespace needs a project of its own`,
|
|
80
|
+
)
|
|
81
|
+
}
|
|
74
82
|
if (target.projectName !== project.name) {
|
|
75
83
|
throw new Error(`Zerops namespace project name mismatch: expected \`${target.projectName}\`, got \`${project.name}\``)
|
|
76
84
|
}
|
|
@@ -222,6 +230,7 @@ const deployProxy = async (
|
|
|
222
230
|
target: NormalizedZeropsNamespaceTarget,
|
|
223
231
|
options: ZeropsNamespaceOptions,
|
|
224
232
|
wait: ProviderNamespaceRepublishInput['wait'],
|
|
233
|
+
reuseActive: boolean,
|
|
225
234
|
): Promise<{ namespace: ProviderDeploymentNamespace; target: NormalizedZeropsNamespaceTarget }> => {
|
|
226
235
|
let currentNamespace = namespace
|
|
227
236
|
const manifest = compileNamespaceProxyManifest(namespace.id, input.members)
|
|
@@ -229,6 +238,7 @@ const deployProxy = async (
|
|
|
229
238
|
namespaceId: namespace.id,
|
|
230
239
|
target,
|
|
231
240
|
manifest,
|
|
241
|
+
brand: input.brand ?? null,
|
|
232
242
|
proxySecrets: namespaceProxySecrets(manifest, input.members),
|
|
233
243
|
...(options.source === undefined ? {} : { source: options.source }),
|
|
234
244
|
api: options.api,
|
|
@@ -236,6 +246,7 @@ const deployProxy = async (
|
|
|
236
246
|
iamKey: options.iamKey,
|
|
237
247
|
signal: input.signal,
|
|
238
248
|
wait,
|
|
249
|
+
reuseActive,
|
|
239
250
|
sleep: options.sleep ?? defaultSleep,
|
|
240
251
|
boundary: () => input.events.boundary(),
|
|
241
252
|
checkpoint: async next => {
|
|
@@ -255,7 +266,7 @@ const republishNamespace = async (
|
|
|
255
266
|
if (target.proxyServiceId === undefined || (target.proxyBuild === undefined && !target.ready)) {
|
|
256
267
|
throw new Error(`Namespace ${namespace.id} is not ready`)
|
|
257
268
|
}
|
|
258
|
-
const deployed = await deployProxy(input, namespace, target, options, input.wait)
|
|
269
|
+
const deployed = await deployProxy(input, namespace, target, options, input.wait, true)
|
|
259
270
|
return { namespace: deployed.namespace, settled: deployed.target.proxyBuild === undefined && deployed.target.ready }
|
|
260
271
|
}
|
|
261
272
|
|
|
@@ -358,6 +369,14 @@ const runNamespaceMutation = async (
|
|
|
358
369
|
} catch {
|
|
359
370
|
input.signal.throwIfAborted()
|
|
360
371
|
await checkpoint(input, namespace, { ...target, ready: false })
|
|
372
|
+
// A deleted project never resolves the version: retrying would hold the namespace `provisioning`, unremovable.
|
|
373
|
+
if (target.projectId !== undefined && !(await projectExists(options.api, target.projectId, input.signal))) {
|
|
374
|
+
throw namespaceError(
|
|
375
|
+
ZEROPS_PROJECT_NOT_FOUND,
|
|
376
|
+
`Zerops project ${target.projectId} of namespace \`${namespace.id}\` no longer exists; remove the namespace`,
|
|
377
|
+
false,
|
|
378
|
+
)
|
|
379
|
+
}
|
|
361
380
|
throw namespaceError('proxyAttemptUnresolved', `Proxy version ${target.proxyDeployment.appVersionId} remains unresolved`, true)
|
|
362
381
|
}
|
|
363
382
|
}
|
|
@@ -398,7 +417,7 @@ const runNamespaceMutation = async (
|
|
|
398
417
|
target = { ...target, projectName: project.name }
|
|
399
418
|
namespace = await checkpoint(input, namespace, target)
|
|
400
419
|
}
|
|
401
|
-
validateProject(namespace, target, project, initiallyAdopted)
|
|
420
|
+
validateProject(namespace, target, project, initiallyAdopted, options.installationProjectId)
|
|
402
421
|
|
|
403
422
|
if (initiallyAdopted) {
|
|
404
423
|
// Validate before the first mutation; the import below then enforces service-level envIsolation.
|
|
@@ -433,7 +452,7 @@ const runNamespaceMutation = async (
|
|
|
433
452
|
}
|
|
434
453
|
namespace = await checkpoint(input, namespace, target)
|
|
435
454
|
|
|
436
|
-
const deployed = await deployProxy(input, namespace, target, options, 'active')
|
|
455
|
+
const deployed = await deployProxy(input, namespace, target, options, 'active', false)
|
|
437
456
|
namespace = deployed.namespace
|
|
438
457
|
target = { ...deployed.target, proxyConfigured: true }
|
|
439
458
|
// After the deploy, because the platform refuses a service with no HTTP port — and never before `ready`
|
|
@@ -470,6 +489,7 @@ export const createZeropsNamespaceCapabilities = (options: ZeropsNamespaceOption
|
|
|
470
489
|
provision: (input) => mutateNamespace(input, options, 'provision'),
|
|
471
490
|
reconcile: (input) => mutateNamespace(input, options, 'reconcile'),
|
|
472
491
|
republish: (input) => republishNamespace(input, options),
|
|
492
|
+
claimedResources: (input) => claimedResources(options.api, input),
|
|
473
493
|
updates: createZeropsNamespaceUpdates(options),
|
|
474
494
|
operator: createZeropsNamespaceOperator({ proxySource: options.proxySource }),
|
|
475
495
|
}
|