@caelo-cms/provisioning 0.10.69 → 0.10.71
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/dist/adopt-existing.d.ts +119 -0
- package/dist/adopt-existing.d.ts.map +1 -0
- package/dist/adopt-existing.js +118 -0
- package/dist/adopt-existing.js.map +1 -0
- package/dist/preview-plan.d.ts +7 -0
- package/dist/preview-plan.d.ts.map +1 -1
- package/dist/preview-plan.js +1 -0
- package/dist/preview-plan.js.map +1 -1
- package/dist/remote-mcp.d.ts +27 -8
- package/dist/remote-mcp.d.ts.map +1 -1
- package/dist/remote-mcp.js +88 -56
- package/dist/remote-mcp.js.map +1 -1
- package/dist/stack-contract.d.ts +53 -39
- package/dist/stack-contract.d.ts.map +1 -1
- package/dist/stack-contract.js +110 -0
- package/dist/stack-contract.js.map +1 -1
- package/dist/state-command.d.ts.map +1 -1
- package/dist/state-command.js +27 -1
- package/dist/state-command.js.map +1 -1
- package/dist/state-migrate.d.ts +7 -0
- package/dist/state-migrate.d.ts.map +1 -1
- package/dist/state-migrate.js +18 -1
- package/dist/state-migrate.js.map +1 -1
- package/dist/state-pulumi.d.ts +7 -0
- package/dist/state-pulumi.d.ts.map +1 -1
- package/dist/state-pulumi.js +11 -0
- package/dist/state-pulumi.js.map +1 -1
- package/dist/upgrade/converge.d.ts.map +1 -1
- package/dist/upgrade/converge.js +4 -2
- package/dist/upgrade/converge.js.map +1 -1
- package/dist/wizards/gcp-pulumi.d.ts +28 -5
- package/dist/wizards/gcp-pulumi.d.ts.map +1 -1
- package/dist/wizards/gcp-pulumi.js +70 -8
- package/dist/wizards/gcp-pulumi.js.map +1 -1
- package/dist/wizards/gcp.d.ts.map +1 -1
- package/dist/wizards/gcp.js +100 -46
- package/dist/wizards/gcp.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/fixtures/viu-one-plan-state-migrate.txt +98 -0
- package/src/adopt-existing.test.ts +397 -0
- package/src/adopt-existing.ts +228 -0
- package/src/preview-plan.ts +8 -0
- package/src/remote-mcp-stack-config.test.ts +195 -0
- package/src/remote-mcp.test.ts +99 -16
- package/src/remote-mcp.ts +104 -58
- package/src/stack-contract-parity.test.ts +28 -3
- package/src/stack-contract.ts +107 -6
- package/src/state-command.ts +38 -1
- package/src/state-migrate.test.ts +38 -0
- package/src/state-migrate.ts +24 -1
- package/src/state-pulumi.ts +21 -0
- package/src/upgrade/converge.ts +4 -2
- package/src/wizards/gcp-pulumi.ts +105 -9
- package/src/wizards/gcp.ts +126 -55
- package/stacks/gcp/index.ts +13 -2
- package/stacks/gcp-firebase/index.ts +20 -6
|
@@ -14,8 +14,14 @@
|
|
|
14
14
|
import { mkdirSync, readFileSync, realpathSync, renameSync, writeFileSync } from "node:fs";
|
|
15
15
|
import { join, relative, resolve as resolvePath } from "node:path";
|
|
16
16
|
import * as pulumi from "@pulumi/pulumi/automation";
|
|
17
|
+
import {
|
|
18
|
+
type AdoptionCandidate,
|
|
19
|
+
AdoptionError,
|
|
20
|
+
adoptExistingResources,
|
|
21
|
+
} from "../adopt-existing.js";
|
|
17
22
|
import { AUDIT_PARALLELISM_DEFAULT } from "../audit-job-setup.js";
|
|
18
23
|
import { checkpointableOutputs } from "../checkpoint-secrets.js";
|
|
24
|
+
import { gcloud } from "../gcloud.js";
|
|
19
25
|
import { GCP_STACK_ENV } from "../gcp-names.js";
|
|
20
26
|
import { type ImageDigests, stackWorkDir } from "../install-state.js";
|
|
21
27
|
import { collectPreviewSteps, type PlannedStep } from "../preview-plan.js";
|
|
@@ -24,7 +30,12 @@ import {
|
|
|
24
30
|
withoutAmbientGoogleCredentials,
|
|
25
31
|
} from "../provisioner-identity.js";
|
|
26
32
|
import { ensureGeneratedSecrets, stackSecretReplication } from "../runtime-secrets.js";
|
|
27
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
ADMIN_MEMORY_DEFAULT,
|
|
35
|
+
REMOTE_MCP_IDENTITY_CONFIG,
|
|
36
|
+
type RemoteMcpIdentity,
|
|
37
|
+
serializeRemoteMcpIdentity,
|
|
38
|
+
} from "../stack-contract.js";
|
|
28
39
|
import type { ResolvedBackend } from "../state-backend.js";
|
|
29
40
|
|
|
30
41
|
/**
|
|
@@ -90,6 +101,13 @@ export interface PulumiUpInputs {
|
|
|
90
101
|
* Defaults to 'gcp' for backwards compatibility.
|
|
91
102
|
*/
|
|
92
103
|
provider?: "gcp" | "gcp-firebase";
|
|
104
|
+
/**
|
|
105
|
+
* #1082 — the remote-MCP identity the stack declares as env
|
|
106
|
+
* (stack-contract.ts REMOTE_MCP_IDENTITY_CONFIG), resolved from the live
|
|
107
|
+
* install. Null when it could not be resolved (a fresh install before its
|
|
108
|
+
* first `up`): the config keeps what an earlier run wrote.
|
|
109
|
+
*/
|
|
110
|
+
remoteMcpIdentity?: RemoteMcpIdentity | null;
|
|
93
111
|
}
|
|
94
112
|
|
|
95
113
|
export interface PulumiUpResult {
|
|
@@ -395,6 +413,7 @@ export function resolveSecretReplication(
|
|
|
395
413
|
|
|
396
414
|
/** A resource as `pulumi stack export` lists it. */
|
|
397
415
|
export interface StateResource {
|
|
416
|
+
urn?: string;
|
|
398
417
|
type?: string;
|
|
399
418
|
inputs?: Record<string, unknown>;
|
|
400
419
|
outputs?: Record<string, unknown>;
|
|
@@ -449,6 +468,7 @@ export type StackConfigInputs = Pick<
|
|
|
449
468
|
| "wafAdaptiveProtection"
|
|
450
469
|
| "iapAllowlist"
|
|
451
470
|
| "imageDigests"
|
|
471
|
+
| "remoteMcpIdentity"
|
|
452
472
|
> & { provider: "gcp" | "gcp-firebase" };
|
|
453
473
|
|
|
454
474
|
/**
|
|
@@ -487,6 +507,13 @@ export async function applyStackConfig(
|
|
|
487
507
|
// v0.3.2 — anthropicApiKey config dropped. Runtime path is
|
|
488
508
|
// /settings/ai → ai_providers (KEK-encrypted).
|
|
489
509
|
...imageDigestConfig(provider, inputs.imageDigests),
|
|
510
|
+
...(inputs.remoteMcpIdentity
|
|
511
|
+
? {
|
|
512
|
+
[`${ns}:${REMOTE_MCP_IDENTITY_CONFIG}`]: {
|
|
513
|
+
value: serializeRemoteMcpIdentity(inputs.remoteMcpIdentity),
|
|
514
|
+
},
|
|
515
|
+
}
|
|
516
|
+
: {}),
|
|
490
517
|
});
|
|
491
518
|
|
|
492
519
|
// Read back only after the required keys are set: `pulumi config` refuses to
|
|
@@ -534,6 +561,54 @@ export function pulumiGcpEnv(
|
|
|
534
561
|
};
|
|
535
562
|
}
|
|
536
563
|
|
|
564
|
+
/**
|
|
565
|
+
* The URN of the stack's explicit GCP provider (`new gcp.Provider` in both
|
|
566
|
+
* stacks), which every stack resource uses. An adopted resource is imported
|
|
567
|
+
* under it (#1079): under Pulumi's default provider instead, the next `up`
|
|
568
|
+
* would see a provider change on it.
|
|
569
|
+
*
|
|
570
|
+
* @throws AdoptionError unless the state holds exactly one
|
|
571
|
+
*/
|
|
572
|
+
export function stackGcpProviderUrn(resources: ReadonlyArray<StateResource>): string {
|
|
573
|
+
const urns = resources
|
|
574
|
+
.filter((r) => r.type === "pulumi:providers:gcp" && typeof r.urn === "string")
|
|
575
|
+
.map((r) => r.urn as string);
|
|
576
|
+
if (urns.length !== 1) {
|
|
577
|
+
throw new AdoptionError(
|
|
578
|
+
`the stack's state holds ${urns.length} GCP providers, so it is unclear which one resources that already exist would be adopted under. Nothing was imported or applied. Report this; the stack needs a look before the installer can run.`,
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
return urns[0] as string;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/** The name {@link importIntoStack} gives the stack's provider in the import's name table. */
|
|
585
|
+
const IMPORT_PROVIDER_NAME = "gcpProvider";
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* `pulumi import` the candidates into the stack under its explicit provider.
|
|
589
|
+
* State only: nothing in the project changes. Not protected (the program
|
|
590
|
+
* does not protect them either) and no code generated.
|
|
591
|
+
*/
|
|
592
|
+
async function importIntoStack(
|
|
593
|
+
stack: pulumi.Stack,
|
|
594
|
+
providerUrn: string,
|
|
595
|
+
candidates: readonly AdoptionCandidate[],
|
|
596
|
+
onOutput: (msg: string) => void,
|
|
597
|
+
): Promise<void> {
|
|
598
|
+
await stack.import({
|
|
599
|
+
resources: candidates.map((c) => ({
|
|
600
|
+
type: c.step.type,
|
|
601
|
+
name: c.step.name,
|
|
602
|
+
id: c.importId,
|
|
603
|
+
provider: IMPORT_PROVIDER_NAME,
|
|
604
|
+
})),
|
|
605
|
+
nameTable: { [IMPORT_PROVIDER_NAME]: providerUrn },
|
|
606
|
+
protect: false,
|
|
607
|
+
generateCode: false,
|
|
608
|
+
onOutput,
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
|
|
537
612
|
/** Thrown when the review (preview-plan.ts planGate) stops `pulumi up` after the preview. */
|
|
538
613
|
export class PulumiUpDeclinedError extends Error {
|
|
539
614
|
override readonly name = "PulumiUpDeclinedError";
|
|
@@ -544,16 +619,23 @@ export class PulumiUpDeclinedError extends Error {
|
|
|
544
619
|
* Streams resource-create events to the supplied onEvent callback so
|
|
545
620
|
* the wizard can render a live progress bar.
|
|
546
621
|
*
|
|
547
|
-
* On a stack that already holds resources, a preview runs first (#737)
|
|
548
|
-
*
|
|
549
|
-
*
|
|
622
|
+
* On a stack that already holds resources, a preview runs first (#737).
|
|
623
|
+
* Planned creates of resources that exist in the project already (made by
|
|
624
|
+
* `upgrade` through gcloud) are adopted into the stack and the stack is
|
|
625
|
+
* previewed again (adopt-existing.ts, #1079). `review` sees that plan and
|
|
626
|
+
* what was adopted: it lists them and decides (planGate) whether `up` may
|
|
627
|
+
* apply the plan. Nothing is applied when it says no.
|
|
550
628
|
*
|
|
551
629
|
* @throws PulumiUpDeclinedError when `review` declines the plan
|
|
630
|
+
* @throws AdoptionError when adopting fails; nothing was applied
|
|
552
631
|
*/
|
|
553
632
|
export async function pulumiUpGcp(
|
|
554
633
|
inputs: PulumiUpInputs,
|
|
555
634
|
onEvent: (kind: "resource" | "error" | "log", message: string) => void,
|
|
556
|
-
review: (
|
|
635
|
+
review: (
|
|
636
|
+
steps: readonly PlannedStep[],
|
|
637
|
+
adopted: readonly AdoptionCandidate[],
|
|
638
|
+
) => Promise<boolean>,
|
|
557
639
|
): Promise<PulumiUpResult> {
|
|
558
640
|
const env = pulumiGcpEnv(process.env, inputs);
|
|
559
641
|
|
|
@@ -608,11 +690,25 @@ export async function pulumiUpGcp(
|
|
|
608
690
|
|
|
609
691
|
// #737 — never `up` a deployed stack blind: list what it would change
|
|
610
692
|
// (replaces and deletes first) and let the review stop it. A first run
|
|
611
|
-
// on an empty stack only creates, so it skips the extra preview
|
|
693
|
+
// on an empty stack only creates, so it skips the extra preview (and has
|
|
694
|
+
// no provider in its state to adopt anything under).
|
|
612
695
|
if (stateResources.length > 0) {
|
|
613
|
-
const
|
|
614
|
-
|
|
615
|
-
|
|
696
|
+
const preview = async () => {
|
|
697
|
+
const collector = collectPreviewSteps();
|
|
698
|
+
await stack.preview({ onEvent: collector.onEvent });
|
|
699
|
+
return collector.steps();
|
|
700
|
+
};
|
|
701
|
+
// #1079 — what `upgrade` created through gcloud joins the state first.
|
|
702
|
+
const { steps, adopted } = await adoptExistingResources(await preview(), {
|
|
703
|
+
projectId: inputs.projectId,
|
|
704
|
+
gcloud,
|
|
705
|
+
preview,
|
|
706
|
+
importResources: (candidates) =>
|
|
707
|
+
importIntoStack(stack, stackGcpProviderUrn(stateResources), candidates, (msg) =>
|
|
708
|
+
onEvent("log", msg),
|
|
709
|
+
),
|
|
710
|
+
});
|
|
711
|
+
if (!(await review(steps, adopted))) {
|
|
616
712
|
throw new PulumiUpDeclinedError("pulumi up was not run: the planned changes were declined.");
|
|
617
713
|
}
|
|
618
714
|
}
|
package/src/wizards/gcp.ts
CHANGED
|
@@ -33,6 +33,7 @@ import { join } from "node:path";
|
|
|
33
33
|
import { cancel, confirm, isCancel, log, note, select, spinner } from "@clack/prompts";
|
|
34
34
|
import { bold, cyan, dim, green, red, yellow } from "kleur/colors";
|
|
35
35
|
import { checkAdcImpersonation } from "../adc-impersonation.js";
|
|
36
|
+
import { type AdoptionCandidate, AdoptionError, formatAdopted } from "../adopt-existing.js";
|
|
36
37
|
import { ensureAuditJob, resolveAdminImage } from "../audit-job-setup.js";
|
|
37
38
|
import { CERT_WAIT_TIMEOUT_MS, lbCertProbe, waitForCert } from "../cert-wait.js";
|
|
38
39
|
import { redactCredentials } from "../checkpoint-secrets.js";
|
|
@@ -104,10 +105,20 @@ import {
|
|
|
104
105
|
} from "../provisioner-identity.js";
|
|
105
106
|
import { revokePublicRoleMemberships } from "../public-role-isolation.js";
|
|
106
107
|
import { decideRegion, detectCliRegion, installableRegions, suggestRegion } from "../regions.js";
|
|
107
|
-
import {
|
|
108
|
+
import {
|
|
109
|
+
checkRemoteMcpIdentityEnv,
|
|
110
|
+
ensureRemoteMcpEdge,
|
|
111
|
+
findRemoteMcpTarget,
|
|
112
|
+
type RemoteMcpTarget,
|
|
113
|
+
resolveStackRemoteMcpIdentity,
|
|
114
|
+
} from "../remote-mcp.js";
|
|
108
115
|
import type { RunMode } from "../run-mode.js";
|
|
109
116
|
import { selfCommand } from "../self-command.js";
|
|
110
|
-
import {
|
|
117
|
+
import {
|
|
118
|
+
ABSENT_DEFAULT_FIREWALL_RULES,
|
|
119
|
+
type RemoteMcpIdentity,
|
|
120
|
+
sameRemoteMcpIdentity,
|
|
121
|
+
} from "../stack-contract.js";
|
|
111
122
|
import {
|
|
112
123
|
chooseInstallBackend,
|
|
113
124
|
describeBackend,
|
|
@@ -119,7 +130,6 @@ import {
|
|
|
119
130
|
resolveBackend,
|
|
120
131
|
} from "../state-backend.js";
|
|
121
132
|
import { acquireStateLock, GcsStateStore } from "../state-store.js";
|
|
122
|
-
import { lookupFailure, resolveGcpResourceName } from "../upgrade/gcp-lookup.js";
|
|
123
133
|
import { createReleaseImageForms, platformDigestsOf } from "../upgrade/image-forms.js";
|
|
124
134
|
import {
|
|
125
135
|
releaseVersionForDigest,
|
|
@@ -378,12 +388,22 @@ export async function runGcpConverge(opts: GcpConvergeOpts): Promise<void> {
|
|
|
378
388
|
// post-cleanup re-runs both pass through cleanly).
|
|
379
389
|
await stepDrainLegacyCaeloAdminUser(projectId, region);
|
|
380
390
|
|
|
391
|
+
// === 9.8. The remote-MCP identity the stack declares (#1082) ===
|
|
392
|
+
// The stack owns the identity env vars (stack-contract.ts
|
|
393
|
+
// RemoteMcpIdentity) and declares them from config the CLI resolves from
|
|
394
|
+
// the live services, so this `up` keeps what they carry. A fresh install
|
|
395
|
+
// has no services yet: the identity is resolved after the first `up` and
|
|
396
|
+
// applied by a second one below.
|
|
397
|
+
const identityBefore = deployed
|
|
398
|
+
? await stepResolveRemoteMcpIdentity(projectId, region, provider, domain, "before")
|
|
399
|
+
: null;
|
|
400
|
+
|
|
381
401
|
// === 10. Pulumi up via Automation SDK ===
|
|
382
402
|
// Pre-built signed images live at the Caelo-team public AR repo
|
|
383
403
|
// (caelo-website/caelo-cms-images by default). Cloud Run reads them
|
|
384
404
|
// directly with no operator-side IAM binding — anonymous public pull.
|
|
385
405
|
phase("Provisioning the infrastructure (pulumi up, 8-15 min)");
|
|
386
|
-
|
|
406
|
+
const upOpts: PulumiUpOpts = {
|
|
387
407
|
projectId,
|
|
388
408
|
domain,
|
|
389
409
|
// #650 — the wizard recorded it (or the install predates the setting).
|
|
@@ -405,9 +425,24 @@ export async function runGcpConverge(opts: GcpConvergeOpts): Promise<void> {
|
|
|
405
425
|
// matching config namespace.
|
|
406
426
|
provider,
|
|
407
427
|
nonInteractive: opts.nonInteractive,
|
|
408
|
-
}
|
|
428
|
+
};
|
|
429
|
+
await stepPulumiUp(installId, { ...upOpts, remoteMcpIdentity: identityBefore?.identity ?? null });
|
|
409
430
|
recordImageDigests(installId, imageDigests);
|
|
410
431
|
|
|
432
|
+
// === 10.05. The identity once the services exist (#1082) ===
|
|
433
|
+
// Read again after every `up`: on a fresh install (or one whose identity
|
|
434
|
+
// could not be read before) this is the first time it can be, and an `up`
|
|
435
|
+
// that replaced a service changed it. A second `up` then applies it
|
|
436
|
+
// through the stack — never through gcloud, which the next `up` would
|
|
437
|
+
// undo.
|
|
438
|
+
const identity = await stepResolveRemoteMcpIdentity(projectId, region, provider, domain, "after");
|
|
439
|
+
if (identity && !sameRemoteMcpIdentity(identity.identity, identityBefore?.identity ?? null)) {
|
|
440
|
+
log.info(
|
|
441
|
+
"Applying the remote-MCP URLs and IAP audience through the stack (second pulumi up)...",
|
|
442
|
+
);
|
|
443
|
+
await stepPulumiUp(installId, { ...upOpts, remoteMcpIdentity: identity.identity });
|
|
444
|
+
}
|
|
445
|
+
|
|
411
446
|
// === 10.1. Remove the default network's pre-populated firewall rules ===
|
|
412
447
|
// GCP creates them when Compute Engine is enabled (step 5). Run after
|
|
413
448
|
// Pulumi rather than right after enabling the API: the default network
|
|
@@ -488,11 +523,9 @@ export async function runGcpConverge(opts: GcpConvergeOpts): Promise<void> {
|
|
|
488
523
|
keepSigner ? [`serviceAccount:${upgraderServiceAccountEmail(projectId, GCP_STACK_ENV)}`] : [],
|
|
489
524
|
);
|
|
490
525
|
|
|
491
|
-
// === 11.6. Remote MCP (#806): LB rules (gcp) + the identity env ===
|
|
492
|
-
//
|
|
493
|
-
|
|
494
|
-
// service's template dropped it (remote-mcp.ts).
|
|
495
|
-
const remoteMcpReady = await stepRemoteMcp(projectId, region, provider, domain);
|
|
526
|
+
// === 11.6. Remote MCP (#806): LB rules (gcp) + a check of the identity env ===
|
|
527
|
+
// The env itself came with `pulumi up` (step 10.05, #1082).
|
|
528
|
+
const remoteMcpReady = await stepRemoteMcp(projectId, provider, domain, identity);
|
|
496
529
|
|
|
497
530
|
// A fresh install ran every converge step above, so it is at this CLI's
|
|
498
531
|
// infrastructure contract — recorded only when the upgrader and the
|
|
@@ -1165,74 +1198,104 @@ async function stepOperatorAccessSync(
|
|
|
1165
1198
|
s.stop(green(`Operator-access sync job ready (${r.done.length} steps)`));
|
|
1166
1199
|
}
|
|
1167
1200
|
|
|
1201
|
+
/** The install's identity for the stack, with the deployed services it was read from. */
|
|
1202
|
+
interface ResolvedRemoteMcpIdentity {
|
|
1203
|
+
readonly target: RemoteMcpTarget;
|
|
1204
|
+
readonly identity: RemoteMcpIdentity;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1168
1207
|
/**
|
|
1169
|
-
* #
|
|
1170
|
-
*
|
|
1171
|
-
*
|
|
1172
|
-
*
|
|
1173
|
-
*
|
|
1174
|
-
* or `upgrade` retries. Idempotent.
|
|
1208
|
+
* #1082 — resolve the remote-MCP identity (remote-mcp.ts
|
|
1209
|
+
* resolveStackRemoteMcpIdentity) for the stack config: the deployed
|
|
1210
|
+
* services' names, then their URLs / the IAP backend and the organisation.
|
|
1211
|
+
* Read-only. Not fatal: remote MCP stays off until the Owner sets it up, so
|
|
1212
|
+
* a failure warns and the `up` keeps the identity the config last recorded.
|
|
1175
1213
|
*
|
|
1176
|
-
* @
|
|
1214
|
+
* @param when `before` the `up` (the services exist), or `after` it
|
|
1215
|
+
* @returns the identity, or null when it could not be read
|
|
1177
1216
|
*/
|
|
1178
|
-
async function
|
|
1217
|
+
async function stepResolveRemoteMcpIdentity(
|
|
1179
1218
|
projectId: string,
|
|
1180
1219
|
region: string,
|
|
1181
1220
|
provider: "gcp" | "gcp-firebase",
|
|
1182
1221
|
domain: string,
|
|
1183
|
-
|
|
1222
|
+
when: "before" | "after",
|
|
1223
|
+
): Promise<ResolvedRemoteMcpIdentity | null> {
|
|
1184
1224
|
const s = spinner();
|
|
1185
1225
|
s.start(
|
|
1186
|
-
"
|
|
1226
|
+
"Reading the remote-MCP URLs and IAP audience (Google sign-in, ChatGPT / claude.ai connectors)...",
|
|
1187
1227
|
);
|
|
1188
|
-
const name = (slug: "admin" | "gateway") =>
|
|
1189
|
-
resolveGcpResourceName(
|
|
1190
|
-
gcloud,
|
|
1191
|
-
"run-service",
|
|
1192
|
-
`caelo-${GCP_STACK_ENV}-${slug}`,
|
|
1193
|
-
projectId,
|
|
1194
|
-
region,
|
|
1195
|
-
);
|
|
1196
|
-
const [adminLookup, gatewayLookup] = [await name("admin"), await name("gateway")];
|
|
1197
1228
|
const failed = (error: string) => {
|
|
1198
|
-
s.stop(yellow(`Remote
|
|
1229
|
+
s.stop(yellow(`Remote-MCP URLs not read: ${error}`));
|
|
1199
1230
|
log.warn(
|
|
1200
|
-
|
|
1231
|
+
when === "before"
|
|
1232
|
+
? "pulumi up keeps the remote-MCP env the stack recorded last; the installer reads it again after the update."
|
|
1233
|
+
: "The install works; Settings → Google sign-in and Settings → MCP report this install as not prepared until a re-run of the installer reads them.",
|
|
1201
1234
|
);
|
|
1202
|
-
return
|
|
1235
|
+
return null;
|
|
1203
1236
|
};
|
|
1204
|
-
|
|
1205
|
-
return failed(
|
|
1206
|
-
lookupFailure(adminLookup, `caelo-${GCP_STACK_ENV}-admin* Cloud Run service`, "operator"),
|
|
1207
|
-
);
|
|
1208
|
-
}
|
|
1209
|
-
if (gatewayLookup.status !== "found") {
|
|
1210
|
-
return failed(
|
|
1211
|
-
lookupFailure(gatewayLookup, `caelo-${GCP_STACK_ENV}-gateway* Cloud Run service`, "operator"),
|
|
1212
|
-
);
|
|
1213
|
-
}
|
|
1214
|
-
const [admin, gateway] = [adminLookup.name, gatewayLookup.name];
|
|
1215
|
-
const edge = await ensureRemoteMcpEdge({ provider, projectId, env: GCP_STACK_ENV, domain });
|
|
1216
|
-
if (!edge.ok) return failed(edge.error);
|
|
1217
|
-
const env = await ensureRemoteMcpIdentityEnv({
|
|
1237
|
+
const found = await findRemoteMcpTarget({
|
|
1218
1238
|
provider,
|
|
1219
1239
|
projectId,
|
|
1220
1240
|
region,
|
|
1221
1241
|
env: GCP_STACK_ENV,
|
|
1222
1242
|
domain,
|
|
1223
|
-
services: { admin, gateway },
|
|
1224
1243
|
});
|
|
1225
|
-
if (!
|
|
1226
|
-
|
|
1227
|
-
|
|
1244
|
+
if (!found.ok) return failed(found.error);
|
|
1245
|
+
const { target } = found;
|
|
1246
|
+
const resolved = await resolveStackRemoteMcpIdentity(target);
|
|
1247
|
+
if (!resolved.ok) return failed(resolved.error);
|
|
1248
|
+
s.stop(green(`Remote-MCP URLs read (${resolved.identity.mcpPublicUrl})`));
|
|
1249
|
+
if (resolved.orgDomainError && when === "after") {
|
|
1228
1250
|
// #914 — without the organisation's domain the admin cannot tell who
|
|
1229
1251
|
// IAP's built-in sign-in admits, so it refuses new users until the
|
|
1230
1252
|
// Owner sets up Google sign-in. Say so now rather than at the first
|
|
1231
1253
|
// invite.
|
|
1232
1254
|
log.warn(
|
|
1233
|
-
`Could not read this project's Google organisation (${
|
|
1255
|
+
`Could not read this project's Google organisation (${resolved.orgDomainError}). Set up Google sign-in under Settings → Google sign-in before adding users, or re-run ${bold(selfCommand("upgrade"))} as an account with the Organization Viewer role.`,
|
|
1234
1256
|
);
|
|
1235
1257
|
}
|
|
1258
|
+
return { target, identity: resolved.identity };
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
/**
|
|
1262
|
+
* #806 — the CLI's part of remote MCP (remote-mcp.ts): on `gcp` the load
|
|
1263
|
+
* balancer's discovery routes and Cloud Armor rule. The identity env vars
|
|
1264
|
+
* (admin URL, the broker's public URL, the IAP audience) come with `pulumi
|
|
1265
|
+
* up` from the stack config (#1082); this only checks that the services
|
|
1266
|
+
* carry them. Remote MCP stays off until the Owner sets it up under
|
|
1267
|
+
* Settings → MCP, so a failure does not stop the install: it warns, and the
|
|
1268
|
+
* next pass or `upgrade` retries. Idempotent.
|
|
1269
|
+
*
|
|
1270
|
+
* @returns whether every part is in place
|
|
1271
|
+
*/
|
|
1272
|
+
async function stepRemoteMcp(
|
|
1273
|
+
projectId: string,
|
|
1274
|
+
provider: "gcp" | "gcp-firebase",
|
|
1275
|
+
domain: string,
|
|
1276
|
+
identity: ResolvedRemoteMcpIdentity | null,
|
|
1277
|
+
): Promise<boolean> {
|
|
1278
|
+
const s = spinner();
|
|
1279
|
+
s.start(
|
|
1280
|
+
"Preparing remote MCP (ChatGPT / claude.ai connectors; off until the Owner enables it)...",
|
|
1281
|
+
);
|
|
1282
|
+
const failed = (error: string) => {
|
|
1283
|
+
s.stop(yellow(`Remote MCP not prepared: ${error}`));
|
|
1284
|
+
log.warn(
|
|
1285
|
+
`The install works; Settings → Google sign-in and Settings → MCP report this install as not prepared until ${bold(selfCommand("upgrade"))} prepares it.`,
|
|
1286
|
+
);
|
|
1287
|
+
return false;
|
|
1288
|
+
};
|
|
1289
|
+
if (!identity) return failed("its URLs and IAP audience could not be read (see above)");
|
|
1290
|
+
const edge = await ensureRemoteMcpEdge({ provider, projectId, env: GCP_STACK_ENV, domain });
|
|
1291
|
+
if (!edge.ok) return failed(edge.error);
|
|
1292
|
+
const check = await checkRemoteMcpIdentityEnv(identity.target, identity.identity);
|
|
1293
|
+
if (!check.ok) return failed(check.error);
|
|
1294
|
+
const missing = [...check.drift.admin, ...check.drift.gateway].map((c) => c.name);
|
|
1295
|
+
if (missing.length > 0) {
|
|
1296
|
+
return failed(`pulumi up left ${[...new Set(missing)].join(", ")} out of the services' env`);
|
|
1297
|
+
}
|
|
1298
|
+
s.stop(green(`Remote MCP and Google sign-in prepared (${identity.identity.mcpPublicUrl})`));
|
|
1236
1299
|
return true;
|
|
1237
1300
|
}
|
|
1238
1301
|
|
|
@@ -1480,22 +1543,29 @@ interface PulumiUpOpts {
|
|
|
1480
1543
|
provider?: "gcp" | "gcp-firebase";
|
|
1481
1544
|
/** No terminal to ask (the install job, `--non-interactive`): see planGate. */
|
|
1482
1545
|
nonInteractive: boolean;
|
|
1546
|
+
/** #1082 — the remote-MCP identity the stack declares (gcp-pulumi.ts PulumiUpInputs). */
|
|
1547
|
+
remoteMcpIdentity?: RemoteMcpIdentity | null;
|
|
1483
1548
|
}
|
|
1484
1549
|
|
|
1485
1550
|
/**
|
|
1486
1551
|
* The installer's review of a deployed stack's plan before `pulumi up`
|
|
1487
1552
|
* (#737): the grouped list, the full diff in the install dir, and a y/N
|
|
1488
1553
|
* for any replace or delete. A data-bearing one is asked about by name and
|
|
1489
|
-
* defaults to No; without a terminal it is refused (planGate).
|
|
1554
|
+
* defaults to No; without a terminal it is refused (planGate). Resources
|
|
1555
|
+
* adopted into the stack before this preview (adopt-existing.ts, #1079) are
|
|
1556
|
+
* listed first; they are no longer in the plan.
|
|
1490
1557
|
*
|
|
1491
1558
|
* @returns whether `pulumi up` may apply the plan
|
|
1492
1559
|
*/
|
|
1493
1560
|
async function reviewPulumiPlan(
|
|
1494
1561
|
installId: string,
|
|
1495
1562
|
steps: readonly PlannedStep[],
|
|
1563
|
+
adopted: readonly AdoptionCandidate[],
|
|
1496
1564
|
nonInteractive: boolean,
|
|
1497
1565
|
): Promise<boolean> {
|
|
1498
1566
|
const gate = planGate(steps, { interactive: !nonInteractive && process.stdin.isTTY === true });
|
|
1567
|
+
const adoptedLines = formatAdopted(adopted);
|
|
1568
|
+
if (adoptedLines.length > 0) log.info(adoptedLines.join("\n"));
|
|
1499
1569
|
if (steps.length === 0) {
|
|
1500
1570
|
log.info(dim("Preview: no changes planned."));
|
|
1501
1571
|
return true;
|
|
@@ -1601,11 +1671,12 @@ async function stepPulumiUp(installId: string, opts: PulumiUpOpts): Promise<void
|
|
|
1601
1671
|
// logs: silent — too noisy for a live progress UI; pulumi
|
|
1602
1672
|
// diagnostics will print on failure via the SDK's onOutput.
|
|
1603
1673
|
},
|
|
1604
|
-
(steps) => reviewPulumiPlan(installId, steps, opts.nonInteractive),
|
|
1674
|
+
(steps, adopted) => reviewPulumiPlan(installId, steps, adopted, opts.nonInteractive),
|
|
1605
1675
|
);
|
|
1606
1676
|
} catch (e) {
|
|
1607
|
-
if (
|
|
1608
|
-
declined = e.message;
|
|
1677
|
+
if (e instanceof AdoptionError) declined = `pulumi up was not run: ${e.message}`;
|
|
1678
|
+
else if (e instanceof PulumiUpDeclinedError) declined = e.message;
|
|
1679
|
+
else throw e;
|
|
1609
1680
|
} finally {
|
|
1610
1681
|
releaseLock();
|
|
1611
1682
|
}
|
package/stacks/gcp/index.ts
CHANGED
|
@@ -37,13 +37,16 @@ import {
|
|
|
37
37
|
adminEnvContract,
|
|
38
38
|
adminMediaVolume,
|
|
39
39
|
adminMediaVolumeTemplate,
|
|
40
|
+
CLOUD_RUN_IGNORE_CHANGES,
|
|
40
41
|
type CloudRunEnvVar,
|
|
41
42
|
type CloudRunSlug,
|
|
42
43
|
databaseUrls,
|
|
43
44
|
defaultStagingHost,
|
|
44
45
|
GATEWAY_TIMEOUT_SECONDS,
|
|
45
46
|
gatewayEnvContract,
|
|
47
|
+
REMOTE_MCP_IDENTITY_CONFIG,
|
|
46
48
|
type RuntimeSecret,
|
|
49
|
+
remoteMcpIdentityStackEnv,
|
|
47
50
|
runtimeSecretBindings,
|
|
48
51
|
STATIC_CDN_POLICY,
|
|
49
52
|
secretFileVolumesTemplate,
|
|
@@ -685,6 +688,12 @@ const envContractInputs = {
|
|
|
685
688
|
},
|
|
686
689
|
} as const;
|
|
687
690
|
|
|
691
|
+
// #1082 — the remote-MCP identity env (stack-contract.ts RemoteMcpIdentity):
|
|
692
|
+
// names generated identities no resource here can take as input, so the CLI
|
|
693
|
+
// resolves them from the live install and writes them as config before `up`.
|
|
694
|
+
// Declared here so the stack is their one owner and `up` keeps them.
|
|
695
|
+
const remoteMcpEnv = remoteMcpIdentityStackEnv(cfg.get(REMOTE_MCP_IDENTITY_CONFIG));
|
|
696
|
+
|
|
688
697
|
function cloudRunService(args: CloudRunArgs): gcp.cloudrunv2.Service {
|
|
689
698
|
return new gcp.cloudrunv2.Service(
|
|
690
699
|
`${namePrefix}-${args.serviceName}`,
|
|
@@ -740,6 +749,8 @@ function cloudRunService(args: CloudRunArgs): gcp.cloudrunv2.Service {
|
|
|
740
749
|
},
|
|
741
750
|
{
|
|
742
751
|
...opts,
|
|
752
|
+
// #1082 — which tool deployed last; see CLOUD_RUN_IGNORE_CHANGES.
|
|
753
|
+
ignoreChanges: [...CLOUD_RUN_IGNORE_CHANGES],
|
|
743
754
|
dependsOn: [
|
|
744
755
|
pgAdminUser,
|
|
745
756
|
cmsAdminDb,
|
|
@@ -771,7 +782,7 @@ const adminSvc = cloudRunService({
|
|
|
771
782
|
// in Cloud Run logs. 3600s is the platform max; no cost penalty for
|
|
772
783
|
// unused timeout (Cloud Run bills running time, not allocated time).
|
|
773
784
|
timeout: "3600s",
|
|
774
|
-
contractEnv: adminEnvContract(envContractInputs),
|
|
785
|
+
contractEnv: [...adminEnvContract(envContractInputs), ...remoteMcpEnv.admin],
|
|
775
786
|
mediaVolume: adminMediaVolumeTemplate(adminMediaVolume(project, env)),
|
|
776
787
|
});
|
|
777
788
|
const gatewaySvc = cloudRunService({
|
|
@@ -786,7 +797,7 @@ const gatewaySvc = cloudRunService({
|
|
|
786
797
|
timeout: `${GATEWAY_TIMEOUT_SECONDS}s`,
|
|
787
798
|
// public_role on cms_public only: no cms_admin login of any role
|
|
788
799
|
// (#613, #635, CLAUDE.md §2).
|
|
789
|
-
contractEnv: gatewayEnvContract(envContractInputs),
|
|
800
|
+
contractEnv: [...gatewayEnvContract(envContractInputs), ...remoteMcpEnv.gateway],
|
|
790
801
|
});
|
|
791
802
|
|
|
792
803
|
// =========================================================================
|
|
@@ -42,13 +42,16 @@ import {
|
|
|
42
42
|
adminEnvContract,
|
|
43
43
|
adminMediaVolume,
|
|
44
44
|
adminMediaVolumeTemplate,
|
|
45
|
+
CLOUD_RUN_IGNORE_CHANGES,
|
|
45
46
|
type CloudRunEnvVar,
|
|
46
47
|
type CloudRunSlug,
|
|
47
48
|
databaseUrls,
|
|
48
49
|
defaultStagingHost,
|
|
49
50
|
GATEWAY_TIMEOUT_SECONDS,
|
|
50
51
|
gatewayEnvContract,
|
|
52
|
+
REMOTE_MCP_IDENTITY_CONFIG,
|
|
51
53
|
type RuntimeSecret,
|
|
54
|
+
remoteMcpIdentityStackEnv,
|
|
52
55
|
runtimeSecretBindings,
|
|
53
56
|
secretFileVolumesTemplate,
|
|
54
57
|
} from "../../dist/stack-contract.js";
|
|
@@ -552,6 +555,12 @@ function imageRef(service: string): string {
|
|
|
552
555
|
return digest ? `${base}@${digest}` : `${base}:${cfg.get("image-tag") ?? "latest"}`;
|
|
553
556
|
}
|
|
554
557
|
|
|
558
|
+
// #1082 — the remote-MCP identity env (stack-contract.ts RemoteMcpIdentity):
|
|
559
|
+
// names generated identities no resource here can take as input, so the CLI
|
|
560
|
+
// resolves them from the live install and writes them as config before `up`.
|
|
561
|
+
// Declared here so the stack is their one owner and `up` keeps them.
|
|
562
|
+
const remoteMcpEnv = remoteMcpIdentityStackEnv(cfg.get(REMOTE_MCP_IDENTITY_CONFIG));
|
|
563
|
+
|
|
555
564
|
function cloudRunService(args: CloudRunArgs): gcp.cloudrunv2.Service {
|
|
556
565
|
return new gcp.cloudrunv2.Service(
|
|
557
566
|
`${namePrefix}-${args.serviceName}`,
|
|
@@ -600,6 +609,8 @@ function cloudRunService(args: CloudRunArgs): gcp.cloudrunv2.Service {
|
|
|
600
609
|
},
|
|
601
610
|
{
|
|
602
611
|
...opts,
|
|
612
|
+
// #1082 — which tool deployed last; see CLOUD_RUN_IGNORE_CHANGES.
|
|
613
|
+
ignoreChanges: [...CLOUD_RUN_IGNORE_CHANGES],
|
|
603
614
|
dependsOn: [
|
|
604
615
|
pgAdminUser,
|
|
605
616
|
cmsAdminDb,
|
|
@@ -654,7 +665,7 @@ const gatewaySvc = cloudRunService({
|
|
|
654
665
|
// request passes its rate limits, captcha and honeypot; staging requests
|
|
655
666
|
// pass the staging access gate.
|
|
656
667
|
ingress: "INGRESS_TRAFFIC_ALL",
|
|
657
|
-
contractEnv: gatewayEnvContract(envContractInputs),
|
|
668
|
+
contractEnv: [...gatewayEnvContract(envContractInputs), ...remoteMcpEnv.gateway],
|
|
658
669
|
});
|
|
659
670
|
|
|
660
671
|
const adminSvc = cloudRunService({
|
|
@@ -670,11 +681,14 @@ const adminSvc = cloudRunService({
|
|
|
670
681
|
// Use the input string (known at config time) for the site — the
|
|
671
682
|
// resource output is Output<string | undefined>, which Input<string>
|
|
672
683
|
// rejects.
|
|
673
|
-
contractEnv:
|
|
674
|
-
...
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
684
|
+
contractEnv: [
|
|
685
|
+
...adminEnvContract({
|
|
686
|
+
...envContractInputs,
|
|
687
|
+
firebaseSiteId,
|
|
688
|
+
gatewayService: gatewaySvc.name,
|
|
689
|
+
}),
|
|
690
|
+
...remoteMcpEnv.admin,
|
|
691
|
+
],
|
|
678
692
|
mediaVolume: adminMediaVolumeTemplate(adminMediaVolume(project, env)),
|
|
679
693
|
});
|
|
680
694
|
|