@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
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'
|
|
@@ -230,6 +230,7 @@ const deployProxy = async (
|
|
|
230
230
|
target: NormalizedZeropsNamespaceTarget,
|
|
231
231
|
options: ZeropsNamespaceOptions,
|
|
232
232
|
wait: ProviderNamespaceRepublishInput['wait'],
|
|
233
|
+
reuseActive: boolean,
|
|
233
234
|
): Promise<{ namespace: ProviderDeploymentNamespace; target: NormalizedZeropsNamespaceTarget }> => {
|
|
234
235
|
let currentNamespace = namespace
|
|
235
236
|
const manifest = compileNamespaceProxyManifest(namespace.id, input.members)
|
|
@@ -237,6 +238,7 @@ const deployProxy = async (
|
|
|
237
238
|
namespaceId: namespace.id,
|
|
238
239
|
target,
|
|
239
240
|
manifest,
|
|
241
|
+
brand: input.brand ?? null,
|
|
240
242
|
proxySecrets: namespaceProxySecrets(manifest, input.members),
|
|
241
243
|
...(options.source === undefined ? {} : { source: options.source }),
|
|
242
244
|
api: options.api,
|
|
@@ -244,6 +246,7 @@ const deployProxy = async (
|
|
|
244
246
|
iamKey: options.iamKey,
|
|
245
247
|
signal: input.signal,
|
|
246
248
|
wait,
|
|
249
|
+
reuseActive,
|
|
247
250
|
sleep: options.sleep ?? defaultSleep,
|
|
248
251
|
boundary: () => input.events.boundary(),
|
|
249
252
|
checkpoint: async next => {
|
|
@@ -263,7 +266,7 @@ const republishNamespace = async (
|
|
|
263
266
|
if (target.proxyServiceId === undefined || (target.proxyBuild === undefined && !target.ready)) {
|
|
264
267
|
throw new Error(`Namespace ${namespace.id} is not ready`)
|
|
265
268
|
}
|
|
266
|
-
const deployed = await deployProxy(input, namespace, target, options, input.wait)
|
|
269
|
+
const deployed = await deployProxy(input, namespace, target, options, input.wait, true)
|
|
267
270
|
return { namespace: deployed.namespace, settled: deployed.target.proxyBuild === undefined && deployed.target.ready }
|
|
268
271
|
}
|
|
269
272
|
|
|
@@ -366,6 +369,14 @@ const runNamespaceMutation = async (
|
|
|
366
369
|
} catch {
|
|
367
370
|
input.signal.throwIfAborted()
|
|
368
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
|
+
}
|
|
369
380
|
throw namespaceError('proxyAttemptUnresolved', `Proxy version ${target.proxyDeployment.appVersionId} remains unresolved`, true)
|
|
370
381
|
}
|
|
371
382
|
}
|
|
@@ -441,7 +452,7 @@ const runNamespaceMutation = async (
|
|
|
441
452
|
}
|
|
442
453
|
namespace = await checkpoint(input, namespace, target)
|
|
443
454
|
|
|
444
|
-
const deployed = await deployProxy(input, namespace, target, options, 'active')
|
|
455
|
+
const deployed = await deployProxy(input, namespace, target, options, 'active', false)
|
|
445
456
|
namespace = deployed.namespace
|
|
446
457
|
target = { ...deployed.target, proxyConfigured: true }
|
|
447
458
|
// After the deploy, because the platform refuses a service with no HTTP port — and never before `ready`
|
|
@@ -478,6 +489,7 @@ export const createZeropsNamespaceCapabilities = (options: ZeropsNamespaceOption
|
|
|
478
489
|
provision: (input) => mutateNamespace(input, options, 'provision'),
|
|
479
490
|
reconcile: (input) => mutateNamespace(input, options, 'reconcile'),
|
|
480
491
|
republish: (input) => republishNamespace(input, options),
|
|
492
|
+
claimedResources: (input) => claimedResources(options.api, input),
|
|
481
493
|
updates: createZeropsNamespaceUpdates(options),
|
|
482
494
|
operator: createZeropsNamespaceOperator({ proxySource: options.proxySource }),
|
|
483
495
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { followLeaseLoss, isLeaseLoss, ProviderNamespaceError } from '@fabrika/provider-contract'
|
|
2
|
+
import type { InstallationBrand } from '@fabrika/proxy-core/brand'
|
|
2
3
|
import { encodeProxyManifestJson, FABRIKA_PROXY_MANIFEST_JSON, type ProxyManifest } from '@fabrika/proxy-core/manifest'
|
|
3
4
|
import {
|
|
4
5
|
readableContent,
|
|
@@ -11,7 +12,12 @@ import {
|
|
|
11
12
|
} from '../api'
|
|
12
13
|
import type { Sleeper } from '../collaborators'
|
|
13
14
|
import { prepareZeropsDescriptor } from '../descriptor'
|
|
14
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
ZEROPS_PROXY_MANIFEST_PATH,
|
|
17
|
+
zeropsProxyAcceptsBrand,
|
|
18
|
+
type ZeropsProxyManifestCarrier,
|
|
19
|
+
zeropsProxyManifestCarrier,
|
|
20
|
+
} from '../proxy-manifest-carrier'
|
|
15
21
|
import { isZeropsUploadDestination } from '../source'
|
|
16
22
|
import type { ZeropsSourcePort } from '../source/port'
|
|
17
23
|
import { deleteVersion } from '../version-attempt'
|
|
@@ -35,6 +41,8 @@ export interface ZeropsProxyRepublishInput {
|
|
|
35
41
|
readonly namespaceId: string
|
|
36
42
|
readonly target: ZeropsNamespaceTarget
|
|
37
43
|
readonly manifest: ProxyManifest
|
|
44
|
+
/** The installation's client logo (ADR-0066); added to the manifest only when the pinned proxy can parse it. */
|
|
45
|
+
readonly brand?: InstallationBrand | null
|
|
38
46
|
readonly source?: ZeropsSourcePort
|
|
39
47
|
readonly api: ZeropsProxyApi
|
|
40
48
|
readonly iamUrl: string
|
|
@@ -44,6 +52,11 @@ export interface ZeropsProxyRepublishInput {
|
|
|
44
52
|
/** Aborts with `lease_lost` when the namespace lease goes, and with `cancel_requested` when a deploy is cancelled. */
|
|
45
53
|
readonly signal: AbortSignal
|
|
46
54
|
readonly wait: 'started' | 'active'
|
|
55
|
+
/**
|
|
56
|
+
* Reuse the proxy's ACTIVE version when it was built from this pin with this manifest identity. A deploy and a
|
|
57
|
+
* registry change pass true; provisioning and reconcile rebuild, so an operator's repair always reaches the platform.
|
|
58
|
+
*/
|
|
59
|
+
readonly reuseActive: boolean
|
|
47
60
|
/** Persist the target; the caller fences the write on the namespace lease. */
|
|
48
61
|
readonly checkpoint: (target: ZeropsNamespaceTarget) => Promise<void>
|
|
49
62
|
readonly boundary: () => Promise<void>
|
|
@@ -88,6 +101,28 @@ const writeProxyIam = async (input: ZeropsProxyRepublishInput, serviceId: string
|
|
|
88
101
|
const proxySecretsDigest = async (secrets: ReadonlyMap<string, string>): Promise<string> =>
|
|
89
102
|
digest([...secrets].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)).map(([key, value]) => `${key}=${value}`).join('\n'))
|
|
90
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Whether the proxy already runs `manifestSha256` on this pin: the recorded deployment names both, and its version is
|
|
106
|
+
* still the service's ACTIVE one. Any doubt, a failed read included, answers false, and the proxy is rebuilt.
|
|
107
|
+
*/
|
|
108
|
+
const servesManifest = async (input: ZeropsProxyRepublishInput, target: ZeropsNamespaceTarget, manifestSha256: string): Promise<boolean> => {
|
|
109
|
+
const deployed = target.proxyDeployment
|
|
110
|
+
const pin = target.proxySource
|
|
111
|
+
if (
|
|
112
|
+
target.ready !== true || deployed === undefined || deployed.manifestSha256 !== manifestSha256
|
|
113
|
+
|| deployed.commitSha !== pin.commitSha || deployed.descriptorSha256 !== pin.descriptor.sha256
|
|
114
|
+
|| deployed.repository.owner !== pin.repository.owner || deployed.repository.name !== pin.repository.name
|
|
115
|
+
) return false
|
|
116
|
+
await input.boundary()
|
|
117
|
+
try {
|
|
118
|
+
const version = await input.api.getAppVersion({ appVersionId: deployed.appVersionId, signal: input.signal })
|
|
119
|
+
return version.id === deployed.appVersionId && version.status === ZEROPS_ACTIVE && version.serviceStackId === target.proxyServiceId
|
|
120
|
+
} catch {
|
|
121
|
+
input.signal.throwIfAborted()
|
|
122
|
+
return false
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
91
126
|
/**
|
|
92
127
|
* Rebuild the namespace proxy with `manifest` (ADR-0044). The attempt is recorded as `proxyBuild` and never
|
|
93
128
|
* resumed: an inherited attempt not yet `triggered` is deleted, a triggered one is polled to a terminal state.
|
|
@@ -100,7 +135,11 @@ export async function republishProxy(input: ZeropsProxyRepublishInput): Promise<
|
|
|
100
135
|
await verifyZeropsNamespaceProxySource(pin)
|
|
101
136
|
// The pinned descriptor decides: a namespace kept on an older proxy revision still lifts the variable (ADR-0045).
|
|
102
137
|
const carrier = zeropsProxyManifestCarrier(pin.descriptor.contents)
|
|
103
|
-
|
|
138
|
+
// So does it for the logo: an older parser refuses the key, and the namespace keeps the mark until it is re-pinned.
|
|
139
|
+
const brand = input.brand ?? null
|
|
140
|
+
const manifest = encodeProxyManifestJson(
|
|
141
|
+
brand !== null && zeropsProxyAcceptsBrand(pin.descriptor.contents) ? { ...input.manifest, brand } : input.manifest,
|
|
142
|
+
)
|
|
104
143
|
// The attempt's identity covers the IAM configuration and the proxy secrets too, so rotating any forces a rebuild.
|
|
105
144
|
const proxySecrets = input.proxySecrets ?? new Map<string, string>()
|
|
106
145
|
const secrets = proxySecrets.size === 0 ? '' : `\n${await proxySecretsDigest(proxySecrets)}`
|
|
@@ -173,6 +212,8 @@ export async function republishProxy(input: ZeropsProxyRepublishInput): Promise<
|
|
|
173
212
|
if (inherited.manifestSha256 === manifestSha256) return target
|
|
174
213
|
}
|
|
175
214
|
|
|
215
|
+
if (input.reuseActive && target.proxyBuild === undefined && await servesManifest(input, target, manifestSha256)) return target
|
|
216
|
+
|
|
176
217
|
if (input.source === undefined) throw new Error('Zerops namespace source client is required')
|
|
177
218
|
const runId = crypto.randomUUID()
|
|
178
219
|
const resolved = await input.source.resolve({
|
|
@@ -1,36 +1,26 @@
|
|
|
1
|
-
// What a namespace proxy pinned to an older source cannot read. Its manifest parser is strict: one row of a form
|
|
2
|
-
// not know, or a CSP directive it does not know, refuses the whole manifest and blocks the namespace's next
|
|
3
|
-
// So a deploy that would publish either is refused before
|
|
4
|
-
// this control plane's own.
|
|
1
|
+
// What a namespace proxy pinned to an older source cannot read or serve. Its manifest parser is strict: one row of a form
|
|
2
|
+
// it does not know, or a CSP directive it does not know, refuses the whole manifest and blocks the namespace's next
|
|
3
|
+
// republish; and it has no terminal sign-out. So a deploy that would publish either, or a terminal, is refused before
|
|
4
|
+
// publication while the namespace's proxy source is not this control plane's own.
|
|
5
5
|
|
|
6
6
|
import type { ProviderApprovedAccess } from '@fabrika/provider-contract'
|
|
7
|
-
import type
|
|
8
|
-
import { contractProxyTable, type ProxyEndpointRow } from '@fabrika/proxy-core/manifest'
|
|
7
|
+
import { contractProxyTable, type OlderProxyReaders } from '@fabrika/proxy-core/manifest'
|
|
9
8
|
import type { ZeropsNamespaceProxySource } from './types'
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
* What the namespace proxy of one older platform release parses of an application's manifest entry: a row, and the
|
|
13
|
-
* entry's CSP directives, each by that release's own parser.
|
|
14
|
-
*/
|
|
15
|
-
export interface OlderProxyReader {
|
|
16
|
-
readonly row: (row: ProxyEndpointRow) => boolean
|
|
17
|
-
readonly csp: (csp: CspOrigins) => boolean
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* The namespace proxies of the older releases a pin may run whose parsers Control holds (ADR-0062). A pin's release is not
|
|
22
|
-
* recorded, so what any of them reads is what the pin may read.
|
|
23
|
-
*/
|
|
24
|
-
export type OlderProxyReaders = readonly [OlderProxyReader, ...OlderProxyReader[]]
|
|
10
|
+
export type { OlderProxyReader, OlderProxyReaders } from '@fabrika/proxy-core/manifest'
|
|
25
11
|
|
|
26
12
|
/**
|
|
27
13
|
* What of the approved contract's manifest entry no older release's proxy parses, as the namespace publishes it: each
|
|
28
14
|
* such row by its id, and `csp` when no older proxy parses the entry's CSP (`workerSrc`, which only this release knows).
|
|
15
|
+
* A contract that declares a terminal adds `terminal` when no older proxy serves the sign-out its sessions end at
|
|
16
|
+
* (ADR-0067 §5).
|
|
29
17
|
*/
|
|
30
18
|
export function entriesNeedingCurrentProxy(access: ProviderApprovedAccess, olderProxies: OlderProxyReaders): string[] {
|
|
31
19
|
const table = contractProxyTable(access.contract, access.contractDigest)
|
|
32
20
|
const rows = table.endpoints.filter((row) => !olderProxies.some((proxy) => proxy.row(row))).map((row) => row.id)
|
|
33
|
-
|
|
21
|
+
const csp = olderProxies.some((proxy) => proxy.csp(table.csp)) ? [] : ['csp']
|
|
22
|
+
const terminal = access.contract.terminal === undefined || olderProxies.some((proxy) => proxy.signsOut) ? [] : ['terminal']
|
|
23
|
+
return [...rows, ...csp, ...terminal]
|
|
34
24
|
}
|
|
35
25
|
|
|
36
26
|
/** This control plane's own proxy source, and the parsers of the older releases a pinned proxy may run. */
|
|
@@ -58,7 +48,7 @@ export function assertNamespaceProxyReads(
|
|
|
58
48
|
return
|
|
59
49
|
}
|
|
60
50
|
throw new Error(
|
|
61
|
-
`the proxy of deployment namespace \`${namespace}\` is pinned to an older source than this platform's, and no older release's proxy reads ${
|
|
51
|
+
`the proxy of deployment namespace \`${namespace}\` is pinned to an older source than this platform's, and no older release's proxy reads or serves ${
|
|
62
52
|
rows.map((row) => `\`${row}\``).join(', ')
|
|
63
53
|
}; re-pin it with \`fabrika control namespaces update --namespace=${namespace} --proxy-source=current\`, then deploy again`,
|
|
64
54
|
)
|