@fabrika/provider-zerops 0.4.2 → 0.4.4
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 +8 -7
- package/src/api/client.ts +11 -14
- package/src/api/polling.ts +42 -0
- package/src/api/readers.ts +9 -2
- package/src/api/service-env-limit.ts +29 -0
- package/src/api/service-env-response.ts +34 -2
- package/src/api/types.ts +39 -9
- package/src/api.ts +10 -1
- package/src/control.ts +23 -8
- package/src/execution.ts +44 -5
- package/src/index.ts +5 -0
- package/src/installation/admin.ts +4 -2
- package/src/installation/checker-release.ts +44 -0
- package/src/installation/deploy.ts +95 -37
- package/src/installation/generated/platform-console-schema.ts +18 -2
- package/src/installation/generated/platform-proxy-manifest.ts +20 -0
- package/src/installation/guide.md +169 -52
- package/src/installation/index.ts +22 -15
- package/src/installation/init-options.ts +2 -2
- package/src/installation/init.ts +8 -2
- package/src/installation/install-collaborators.ts +4 -0
- package/src/installation/install-environment.ts +52 -30
- package/src/installation/install-options.ts +20 -19
- package/src/installation/install-project.ts +23 -15
- package/src/installation/install-provisioning.ts +12 -12
- package/src/installation/install-secrets.ts +89 -0
- package/src/installation/install-token.ts +37 -0
- package/src/installation/install.ts +34 -21
- package/src/installation/machine-key.ts +3 -2
- package/src/installation/platform-archive.ts +87 -0
- package/src/installation/platform-upload.ts +2 -2
- package/src/installation/secrets.ts +3 -4
- package/src/installation/sidecar.ts +38 -2
- package/src/installation/templates/README.md +1 -1
- package/src/installation/templates/platform.yml +23 -2
- package/src/installation/topology/setups.ts +15 -27
- package/src/installation/topology/topology.ts +105 -104
- package/src/installation/upgrade.ts +24 -7
- package/src/managed-env.ts +11 -0
- package/src/manifest.ts +25 -3
- package/src/namespace/codec.ts +6 -1
- package/src/namespace/errors.ts +3 -1
- package/src/namespace/provisioning.ts +18 -5
- package/src/namespace/proxy-attempt.ts +56 -14
- package/src/namespace/update.ts +50 -8
- package/src/provider.ts +57 -13
- package/src/proxy-manifest-carrier.ts +32 -0
- package/src/source/port.ts +9 -1
- package/src/installation/manifest.ts +0 -95
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
//
|
|
20
20
|
// ── What this command writes, and what it deliberately does not ───────────────────────────────────
|
|
21
21
|
//
|
|
22
|
-
// It
|
|
23
|
-
// per-installation configuration derived from the resolved public hosts
|
|
24
|
-
// the origins it must know about, and the installation's environment name). **It writes no credential at all** — every
|
|
22
|
+
// It composes the proxy manifest and ships it in the proxy's build archive. It writes the exact namespace
|
|
23
|
+
// planning source default and the per-installation configuration derived from the resolved public hosts
|
|
24
|
+
// (each service's own origin, the origins it must know about, and the installation's environment name). **It writes no credential at all** — every
|
|
25
25
|
// secret an installation holds is `platform init`'s to place, and a deploy that cannot write one cannot
|
|
26
26
|
// leak one. The two credentials this command HOLDS are the Zerops access token and the IAM admin key,
|
|
27
27
|
// both from the environment, neither ever logged.
|
|
@@ -35,10 +35,14 @@
|
|
|
35
35
|
// readiness gate.
|
|
36
36
|
|
|
37
37
|
import { FABRIKA_SERVICE_TABLE_DIGEST, readEnvironmentName } from '@fabrika/auth-core'
|
|
38
|
+
import { IAM_APP_ID, IAM_APP_SCHEMA } from '@fabrika/iam-contract'
|
|
38
39
|
import type { SchemaReconciler } from '@fabrika/provider-contract'
|
|
39
40
|
import { encodeProxyManifestJson, FABRIKA_PROXY_MANIFEST_JSON } from '@fabrika/proxy-core/manifest'
|
|
41
|
+
import { assertServiceEnvValueFits } from '../api'
|
|
40
42
|
import {
|
|
43
|
+
enableSubdomainAccessWhenHttp,
|
|
41
44
|
prepareZeropsDescriptor,
|
|
45
|
+
readableContent,
|
|
42
46
|
type Sleeper,
|
|
43
47
|
ZEROPS_ACTIVE,
|
|
44
48
|
ZEROPS_SERVICE_NOT_HTTP,
|
|
@@ -47,7 +51,10 @@ import {
|
|
|
47
51
|
ZeropsApiError,
|
|
48
52
|
type ZeropsNamespaceProxySource,
|
|
49
53
|
type ZeropsService,
|
|
54
|
+
type ZeropsServiceEnv,
|
|
50
55
|
} from '../index'
|
|
56
|
+
import { ZEROPS_PROXY_MANIFEST_PATH } from '../proxy-manifest-carrier'
|
|
57
|
+
import { assertCheckerPublished, type CheckerPublication } from './checker-release'
|
|
51
58
|
import type { PlatformDeployInput } from './deploy-options'
|
|
52
59
|
import { PLATFORM_CONSOLE_APP_SCHEMA } from './generated/platform-console-schema'
|
|
53
60
|
import { PLATFORM_PROXY_MANIFEST_TEMPLATE } from './generated/platform-proxy-manifest'
|
|
@@ -61,8 +68,8 @@ import {
|
|
|
61
68
|
validateInstallationSetups,
|
|
62
69
|
} from './installation-source'
|
|
63
70
|
import type { DeployLog } from './log'
|
|
64
|
-
import { mergePlatformProxyManifest, readLiveProxyManifest } from './manifest'
|
|
65
71
|
import { type IamOAuthConfig, platformIamOAuthConfig } from './mcp'
|
|
72
|
+
import { archiveWithDataFile } from './platform-archive'
|
|
66
73
|
import type { PreparedPlatformSource } from './platform-source'
|
|
67
74
|
import { type PlatformUploadFetch, uploadPlatformArchive } from './platform-upload'
|
|
68
75
|
import { platformProxyAppFor, type PlatformProxyComposition, platformServiceTableDigest, resolvePlatformProxyManifest } from './proxy-manifest'
|
|
@@ -139,6 +146,7 @@ export interface PlatformServiceDeployCollaborators {
|
|
|
139
146
|
/** Everything `deployPlatform` reaches the outside world through. */
|
|
140
147
|
export interface PlatformDeployCollaborators extends PlatformServiceDeployCollaborators {
|
|
141
148
|
readonly prepareSource: PlatformSourcePreparer
|
|
149
|
+
readonly checkerPublished: CheckerPublication
|
|
142
150
|
readonly reconcileSchema: SchemaReconciler
|
|
143
151
|
}
|
|
144
152
|
|
|
@@ -146,6 +154,16 @@ export interface PlatformDeployCollaborators extends PlatformServiceDeployCollab
|
|
|
146
154
|
export interface PlatformServiceDeployInput {
|
|
147
155
|
readonly source: PreparedPlatformSource
|
|
148
156
|
readonly dryRun: boolean
|
|
157
|
+
/** The proxy's manifest, which its archive carries as the one data file (ADR-0045); the proxy is refused without it. */
|
|
158
|
+
readonly proxyManifest?: string
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** The archive one service builds from: the snapshot, and for the proxy the snapshot plus its manifest. */
|
|
162
|
+
const serviceArchive = async (service: ResolvedService, input: PlatformServiceDeployInput): Promise<Blob> => {
|
|
163
|
+
const snapshot = Bun.file(input.source.archivePath)
|
|
164
|
+
if (service.hostname !== PLATFORM_PROXY_SERVICE) return snapshot
|
|
165
|
+
if (input.proxyManifest === undefined) throw new Error('the proxy is built only with the manifest its archive carries')
|
|
166
|
+
return new Blob([archiveWithDataFile(await snapshot.bytes(), { path: ZEROPS_PROXY_MANIFEST_PATH, contents: input.proxyManifest })])
|
|
149
167
|
}
|
|
150
168
|
|
|
151
169
|
/** One resolved platform service. */
|
|
@@ -237,13 +255,16 @@ export const serviceOf = (services: ReadonlyMap<PlatformDeployService, ResolvedS
|
|
|
237
255
|
return service
|
|
238
256
|
}
|
|
239
257
|
|
|
240
|
-
/**
|
|
258
|
+
/** One service's environment by key. A sensitive record carries no value. Values are never logged. */
|
|
259
|
+
export type LiveServiceEnv = ReadonlyMap<string, ZeropsServiceEnv>
|
|
260
|
+
|
|
261
|
+
/** Read one service's environment by key. */
|
|
241
262
|
export const readServiceEnv = async (
|
|
242
263
|
serviceId: string,
|
|
243
264
|
{ api, signal }: Pick<PlatformServiceDeployCollaborators, 'api' | 'signal'>,
|
|
244
|
-
): Promise<
|
|
265
|
+
): Promise<LiveServiceEnv> => {
|
|
245
266
|
const variables = await api.listServiceEnv({ serviceId, signal })
|
|
246
|
-
return new Map(variables.map((variable) => [variable.key, variable
|
|
267
|
+
return new Map(variables.map((variable) => [variable.key, variable]))
|
|
247
268
|
}
|
|
248
269
|
|
|
249
270
|
/**
|
|
@@ -254,11 +275,11 @@ export const readServiceEnv = async (
|
|
|
254
275
|
* `custom-domain` installation, where the domains are bound to the project balancer out of band and
|
|
255
276
|
* publishing a subdomain would add a public address nobody asked for.
|
|
256
277
|
*/
|
|
257
|
-
const resolvePlacement = (input: PlatformDeployInput, proxyEnv:
|
|
278
|
+
const resolvePlacement = (input: PlatformDeployInput, proxyEnv: LiveServiceEnv): PlatformPlacement => {
|
|
258
279
|
if (input.hosts !== undefined) {
|
|
259
280
|
return { hosts: input.hosts, scheme: input.scheme, zeropsSubdomain: false }
|
|
260
281
|
}
|
|
261
|
-
const subdomains = proxyEnv.get(ZEROPS_SUBDOMAIN_VARIABLE)
|
|
282
|
+
const subdomains = readableContent(proxyEnv.get(ZEROPS_SUBDOMAIN_VARIABLE))
|
|
262
283
|
if (subdomains === undefined || subdomains.trim() === '') {
|
|
263
284
|
throw new Error(
|
|
264
285
|
'the proxy service publishes no Zerops subdomain — deploy it once so it has HTTP ports, or name every host '
|
|
@@ -281,7 +302,6 @@ const IAM_OAUTH_CONFIG = 'FABRIKA_IAM_OAUTH_CONFIG'
|
|
|
281
302
|
const desiredServiceEnv = (
|
|
282
303
|
environment: string,
|
|
283
304
|
placement: PlatformPlacement,
|
|
284
|
-
manifestJson: string,
|
|
285
305
|
proxySource: ZeropsNamespaceProxySource,
|
|
286
306
|
oauthConfig: IamOAuthConfig | undefined,
|
|
287
307
|
controlTableDigest: string,
|
|
@@ -314,7 +334,6 @@ const desiredServiceEnv = (
|
|
|
314
334
|
// Compared byte-for-byte against a token's `iss`, so this is the PUBLIC issuer and not the
|
|
315
335
|
// private `iam:3000` the manifest dials — `readProxyEnv` canonicalizes it to an origin.
|
|
316
336
|
['FABRIKA_IAM_ISSUER', iamOrigin],
|
|
317
|
-
[FABRIKA_PROXY_MANIFEST_JSON, manifestJson],
|
|
318
337
|
['FABRIKA_PROXY_SESSION_OWNER_JSON', JSON.stringify({ issuer: iamOrigin, upstream: PRIVATE_IAM_UPSTREAM })],
|
|
319
338
|
]),
|
|
320
339
|
],
|
|
@@ -338,29 +357,56 @@ const desiredServiceEnv = (
|
|
|
338
357
|
])
|
|
339
358
|
}
|
|
340
359
|
|
|
341
|
-
/**
|
|
360
|
+
/**
|
|
361
|
+
* Write only what differs. Returns the KEYS written — never a value, not even in a debug line.
|
|
362
|
+
*
|
|
363
|
+
* Nothing here is a credential, so everything is written readable. A record an older release wrote
|
|
364
|
+
* sensitive reads as different and is rewritten readable, which is what lets the next deploy compare it.
|
|
365
|
+
*/
|
|
342
366
|
const applyServiceEnv = async (
|
|
343
367
|
service: ResolvedService,
|
|
344
368
|
desired: ReadonlyMap<string, string>,
|
|
345
|
-
live:
|
|
369
|
+
live: LiveServiceEnv,
|
|
346
370
|
input: PlatformDeployInput,
|
|
347
371
|
collaborators: PlatformDeployCollaborators,
|
|
348
372
|
): Promise<string[]> => {
|
|
349
373
|
const written: string[] = []
|
|
350
374
|
for (const [key, value] of desired) {
|
|
351
|
-
if (live.get(key) === value) {
|
|
375
|
+
if (readableContent(live.get(key)) === value) {
|
|
352
376
|
continue
|
|
353
377
|
}
|
|
354
378
|
written.push(key)
|
|
379
|
+
// The client refuses it too; checking here lets a dry run report what the real write would refuse.
|
|
380
|
+
assertServiceEnvValueFits(key, value)
|
|
355
381
|
if (input.dryRun) {
|
|
356
382
|
continue
|
|
357
383
|
}
|
|
358
384
|
assertRunning(collaborators.signal)
|
|
359
|
-
await collaborators.api.putServiceEnv({ serviceId: service.id, key, value, signal: collaborators.signal })
|
|
385
|
+
await collaborators.api.putServiceEnv({ serviceId: service.id, key, value, sensitive: false, signal: collaborators.signal })
|
|
360
386
|
}
|
|
361
387
|
return written
|
|
362
388
|
}
|
|
363
389
|
|
|
390
|
+
/**
|
|
391
|
+
* Delete, unread, the manifest copy older releases wrote sensitive: nothing reads it, as each proxy version serves
|
|
392
|
+
* the file its own build wrote, and the platform proxy fronts only the platform's apps (ADR-0034). True if one existed.
|
|
393
|
+
*/
|
|
394
|
+
const removeLegacyProxyManifest = async (
|
|
395
|
+
proxyEnv: LiveServiceEnv,
|
|
396
|
+
input: PlatformDeployInput,
|
|
397
|
+
collaborators: PlatformDeployCollaborators,
|
|
398
|
+
): Promise<boolean> => {
|
|
399
|
+
const legacy = proxyEnv.get(FABRIKA_PROXY_MANIFEST_JSON)
|
|
400
|
+
if (legacy === undefined) {
|
|
401
|
+
return false
|
|
402
|
+
}
|
|
403
|
+
if (!input.dryRun) {
|
|
404
|
+
assertRunning(collaborators.signal)
|
|
405
|
+
await collaborators.api.deleteServiceEnv({ envId: legacy.id, signal: collaborators.signal })
|
|
406
|
+
}
|
|
407
|
+
return true
|
|
408
|
+
}
|
|
409
|
+
|
|
364
410
|
/** Only a known rejection permits another request against the same reservation. */
|
|
365
411
|
const triggerPipeline = async (
|
|
366
412
|
service: ResolvedService,
|
|
@@ -434,11 +480,12 @@ export const deployPlatformService = async (
|
|
|
434
480
|
return
|
|
435
481
|
}
|
|
436
482
|
prepareZeropsDescriptor(input.source.descriptor.contents, service.hostname)
|
|
483
|
+
const archive = await serviceArchive(service, input)
|
|
437
484
|
const reserved = await api.createAppVersion({ serviceId: service.id, name: input.source.commitSha, signal })
|
|
438
485
|
const appVersionId = reserved.id
|
|
439
486
|
log.info(`${service.hostname}: app-version ${appVersionId}, commit ${input.source.commitSha}, setup ${service.hostname}`)
|
|
440
487
|
try {
|
|
441
|
-
await uploadPlatformArchive(reserved.uploadUrl,
|
|
488
|
+
await uploadPlatformArchive(reserved.uploadUrl, archive, signal, collaborators.uploadFetch)
|
|
442
489
|
assertRunning(signal)
|
|
443
490
|
} catch {
|
|
444
491
|
await cleanupReservation(service, appVersionId, collaborators)
|
|
@@ -502,7 +549,8 @@ const deployConcurrently = async (
|
|
|
502
549
|
* The decision is the READ-BACK and never the call returning: on an already-published service the
|
|
503
550
|
* platform answers 2xx and then fails the process it hands back, and a freshly-enabled one can still
|
|
504
551
|
* read `false` on the next read. Both live-verified. Runs on every deploy, so a subdomain someone
|
|
505
|
-
* turned off in the GUI comes back, and throws rather than leaving the installation unreachable.
|
|
552
|
+
* turned off in the GUI comes back, and throws rather than leaving the installation unreachable. The
|
|
553
|
+
* HTTP ports lag a fresh ACTIVE version, so `serviceStackIsNotHttp` is retried for a bounded window first.
|
|
506
554
|
*/
|
|
507
555
|
const ensureSubdomainAccess = async (
|
|
508
556
|
proxy: ResolvedService,
|
|
@@ -519,11 +567,11 @@ const ensureSubdomainAccess = async (
|
|
|
519
567
|
return
|
|
520
568
|
}
|
|
521
569
|
try {
|
|
522
|
-
await
|
|
570
|
+
await enableSubdomainAccessWhenHttp({ api, serviceId: proxy.id, sleep, signal })
|
|
523
571
|
} catch (error) {
|
|
524
572
|
if (error instanceof ZeropsApiError && error.code === ZEROPS_SERVICE_NOT_HTTP) {
|
|
525
573
|
throw new Error(
|
|
526
|
-
`the proxy cannot be published on a zerops.app subdomain: it exposes no deployed HTTP port (${ZEROPS_SERVICE_NOT_HTTP})`,
|
|
574
|
+
`the proxy cannot be published on a zerops.app subdomain: it still exposes no deployed HTTP port (${ZEROPS_SERVICE_NOT_HTTP}) minutes after its deploy`,
|
|
527
575
|
)
|
|
528
576
|
}
|
|
529
577
|
throw error
|
|
@@ -569,6 +617,7 @@ export const deployPlatform = async (
|
|
|
569
617
|
const source = await collaborators.prepareSource(input.sourceDir, collaborators.signal)
|
|
570
618
|
try {
|
|
571
619
|
validateInstallationSetups(source, PLATFORM_DEPLOY_ORDER)
|
|
620
|
+
await assertCheckerPublished(collaborators.checkerPublished, collaborators.signal)
|
|
572
621
|
await deployPreparedPlatform(input, collaborators, source)
|
|
573
622
|
} finally {
|
|
574
623
|
await source.dispose()
|
|
@@ -617,26 +666,18 @@ export const deployPreparedPlatform = async (
|
|
|
617
666
|
? 'MCP authoring: off'
|
|
618
667
|
: `MCP authoring: on, ${oauthConfig.clients.length} client(s), resource(s) ${oauthConfig.resources.map((entry) => entry.resource).join(', ')}`,
|
|
619
668
|
)
|
|
620
|
-
|
|
621
|
-
log.info(`${merged.manifest.apps.length} app(s): ${merged.manifest.apps.map((app) => app.id).join(', ')}`)
|
|
622
|
-
if (merged.carried.length > 0) {
|
|
623
|
-
log.info(`carried through from the live manifest: ${merged.carried.join(', ')}`)
|
|
624
|
-
}
|
|
625
|
-
for (const displaced of merged.superseded) {
|
|
626
|
-
log.warn(
|
|
627
|
-
`the live manifest entry \`${displaced.id}\` stood on the platform host ${displaced.host} and was replaced — `
|
|
628
|
-
+ 'that host now answers for the platform app that owns it',
|
|
629
|
-
)
|
|
630
|
-
}
|
|
669
|
+
log.info(`${platformManifest.apps.length} app(s): ${platformManifest.apps.map((app) => app.id).join(', ')}`)
|
|
631
670
|
|
|
632
671
|
log.step('Write the service environment')
|
|
633
672
|
const controlTableDigest = platformServiceTableDigest(platformManifest, platformProxyAppFor(PLATFORM_PROXY_MANIFEST_TEMPLATE, 'control').id)
|
|
634
|
-
const desired = desiredServiceEnv(environment, placement,
|
|
673
|
+
const desired = desiredServiceEnv(environment, placement, proxySource, oauthConfig, controlTableDigest)
|
|
635
674
|
for (const hostname of PLATFORM_DEPLOY_ORDER) {
|
|
636
675
|
const service = serviceOf(services, hostname)
|
|
637
676
|
const live = hostname === PLATFORM_PROXY_SERVICE ? proxyEnv : await readServiceEnv(service.id, collaborators)
|
|
638
677
|
const keys = desired.get(hostname) ?? new Map<string, string>()
|
|
639
|
-
|
|
678
|
+
const liveOAuthConfig = live.get(IAM_OAUTH_CONFIG)
|
|
679
|
+
const oauthStillSet = liveOAuthConfig !== undefined && (liveOAuthConfig.sensitive || liveOAuthConfig.content.trim() !== '')
|
|
680
|
+
if (hostname === 'iam' && oauthConfig === undefined && oauthStillSet) {
|
|
640
681
|
log.warn(`iam: MCP authoring is off but ${IAM_OAUTH_CONFIG} is still set; IAM keeps issuing MCP tokens until it is removed`)
|
|
641
682
|
}
|
|
642
683
|
const written = await applyServiceEnv(service, keys, live, input, collaborators)
|
|
@@ -646,12 +687,19 @@ export const deployPreparedPlatform = async (
|
|
|
646
687
|
: `${hostname}: ${input.dryRun ? 'would write' : 'wrote'} ${written.join(', ')}`,
|
|
647
688
|
)
|
|
648
689
|
}
|
|
690
|
+
if (await removeLegacyProxyManifest(proxyEnv, input, collaborators)) {
|
|
691
|
+
log.info(
|
|
692
|
+
`${PLATFORM_PROXY_SERVICE}: ${input.dryRun ? 'would remove' : 'removed'} ${FABRIKA_PROXY_MANIFEST_JSON}, the copy of the manifest a `
|
|
693
|
+
+ 'release before this one kept there',
|
|
694
|
+
)
|
|
695
|
+
}
|
|
649
696
|
|
|
650
|
-
// EVERY deploy is after EVERY write
|
|
651
|
-
//
|
|
652
|
-
//
|
|
697
|
+
// EVERY deploy is after EVERY write, and the manifest rides in the proxy's own archive. That is what makes
|
|
698
|
+
// the sequence fail closed: a proxy build that does not go ACTIVE leaves the previous manifest in front of
|
|
699
|
+
// the previous code, because control builds only after it — and `ENVIRONMENT` is in place before any
|
|
700
|
+
// service that refuses `local` restarts.
|
|
653
701
|
log.step(`Deploy ${PLATFORM_CONCURRENT_DEPLOY.join(' + ')} together, then ${PLATFORM_SEQUENTIAL_DEPLOY.join(' → ')}`)
|
|
654
|
-
const buildInput = { source, dryRun: input.dryRun }
|
|
702
|
+
const buildInput = { source, dryRun: input.dryRun, proxyManifest: encodeProxyManifestJson(platformManifest) }
|
|
655
703
|
await deployConcurrently(services, buildInput, collaborators)
|
|
656
704
|
for (const hostname of PLATFORM_SEQUENTIAL_DEPLOY) {
|
|
657
705
|
await deployPlatformService(serviceOf(services, hostname), buildInput, collaborators)
|
|
@@ -669,11 +717,12 @@ export const deployPreparedPlatform = async (
|
|
|
669
717
|
log.info('custom-domain installation: the operator binds each domain to the project balancer; no subdomain is published')
|
|
670
718
|
}
|
|
671
719
|
|
|
672
|
-
log.step('Register the console with IAM')
|
|
720
|
+
log.step('Register the console and IAM with IAM')
|
|
673
721
|
const consoleApp = platformProxyAppFor(PLATFORM_PROXY_MANIFEST_TEMPLATE, 'control')
|
|
674
722
|
const consoleOrigin = platformOrigin(placement.scheme, placement.hosts.control)
|
|
675
723
|
if (input.dryRun) {
|
|
676
724
|
log.info(`would reconcile \`${consoleApp.id}\` and register its return origin ${consoleOrigin}`)
|
|
725
|
+
log.info(`would reconcile \`${IAM_APP_ID}\`'s own vocabulary`)
|
|
677
726
|
} else {
|
|
678
727
|
// The schema PUT is what REGISTERS the app, and `apps.setReturnOrigins` 404s for an app IAM has
|
|
679
728
|
// never heard of — so both travel in this one call, in that order (ADR-0022/ADR-0023).
|
|
@@ -686,6 +735,15 @@ export const deployPreparedPlatform = async (
|
|
|
686
735
|
signal,
|
|
687
736
|
})
|
|
688
737
|
log.info(`\`${consoleApp.id}\` reconciled; return origin ${consoleOrigin}`)
|
|
738
|
+
// IAM's own vocabulary, which `user-admin` is granted from (ADR-0058). It has no return origin.
|
|
739
|
+
await collaborators.reconcileSchema({
|
|
740
|
+
url: platformOrigin(placement.scheme, placement.hosts.iam),
|
|
741
|
+
app: IAM_APP_ID,
|
|
742
|
+
schema: IAM_APP_SCHEMA,
|
|
743
|
+
adminKey: input.iamAdminKey,
|
|
744
|
+
signal,
|
|
745
|
+
})
|
|
746
|
+
log.info(`\`${IAM_APP_ID}\` reconciled`)
|
|
689
747
|
}
|
|
690
748
|
|
|
691
749
|
log.step(input.dryRun ? 'Dry run complete — nothing was written' : 'Installation deployed')
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import type { AppSchema } from '@fabrika/auth-core'
|
|
12
12
|
|
|
13
13
|
export const PLATFORM_CONSOLE_APP_SCHEMA: AppSchema = {
|
|
14
|
+
displayName: 'Console',
|
|
14
15
|
scopes: [
|
|
15
16
|
{ type: 'app', label: 'App' },
|
|
16
17
|
{ type: 'environment', label: 'Environment' },
|
|
@@ -31,14 +32,29 @@ export const PLATFORM_CONSOLE_APP_SCHEMA: AppSchema = {
|
|
|
31
32
|
{ action: 'operations.manage', description: 'Manage Operations sources, alerts, and retention' },
|
|
32
33
|
],
|
|
33
34
|
roles: {
|
|
35
|
+
observer: {
|
|
36
|
+
name: 'Observer',
|
|
37
|
+
description: 'Read Operations errors and activity.',
|
|
38
|
+
permissions: ['operations.read'],
|
|
39
|
+
},
|
|
40
|
+
triager: {
|
|
41
|
+
name: 'Triager',
|
|
42
|
+
description: 'Read and triage Operations errors.',
|
|
43
|
+
permissions: ['operations.read', 'operations.triage'],
|
|
44
|
+
},
|
|
45
|
+
deployer: {
|
|
46
|
+
name: 'Deployer',
|
|
47
|
+
description: 'Trigger, read and cancel deploys (no registry, namespace or secret management).',
|
|
48
|
+
permissions: ['deploy.*'],
|
|
49
|
+
},
|
|
34
50
|
operator: {
|
|
35
51
|
name: 'Operator',
|
|
36
|
-
description: '
|
|
52
|
+
description: 'Deploy, and read, triage and manage Operations (no registry or secret management).',
|
|
37
53
|
permissions: ['deploy.*', 'operations.*'],
|
|
38
54
|
},
|
|
39
55
|
admin: {
|
|
40
56
|
name: 'Admin',
|
|
41
|
-
description: '
|
|
57
|
+
description: 'Every console action in every scope.',
|
|
42
58
|
permissions: ['*'],
|
|
43
59
|
},
|
|
44
60
|
},
|
|
@@ -278,6 +278,16 @@ export const PLATFORM_PROXY_MANIFEST_TEMPLATE: PlatformProxyManifestTemplate = {
|
|
|
278
278
|
admission: null,
|
|
279
279
|
oauth: null,
|
|
280
280
|
},
|
|
281
|
+
{
|
|
282
|
+
id: 'github-setup',
|
|
283
|
+
kind: 'service',
|
|
284
|
+
method: 'GET',
|
|
285
|
+
path: '/api/source/github/setup/:connectionId',
|
|
286
|
+
callers: ['human'],
|
|
287
|
+
requirement: { kind: 'authenticated' },
|
|
288
|
+
admission: null,
|
|
289
|
+
oauth: null,
|
|
290
|
+
},
|
|
281
291
|
{
|
|
282
292
|
id: 'rpc',
|
|
283
293
|
kind: 'service',
|
|
@@ -453,6 +463,16 @@ export const PLATFORM_PROXY_MANIFEST_TEMPLATE: PlatformProxyManifestTemplate = {
|
|
|
453
463
|
admission: null,
|
|
454
464
|
oauth: null,
|
|
455
465
|
},
|
|
466
|
+
{
|
|
467
|
+
id: 'github-setup',
|
|
468
|
+
kind: 'service',
|
|
469
|
+
method: 'GET',
|
|
470
|
+
path: '/api/source/github/setup/:connectionId',
|
|
471
|
+
callers: ['human'],
|
|
472
|
+
requirement: { kind: 'authenticated' },
|
|
473
|
+
admission: null,
|
|
474
|
+
oauth: null,
|
|
475
|
+
},
|
|
456
476
|
{
|
|
457
477
|
id: 'rpc',
|
|
458
478
|
kind: 'service',
|