@intentius/chant-lexicon-pulumi 0.124.54 → 0.124.55
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/README.md +42 -3
- package/dist/adopt.d.ts +68 -0
- package/dist/adopt.d.ts.map +1 -0
- package/dist/adopt.js +162 -0
- package/dist/capability-plugin.d.ts +3 -0
- package/dist/capability-plugin.d.ts.map +1 -1
- package/dist/capability-plugin.js +8 -1
- package/dist/change-set.d.ts +27 -4
- package/dist/change-set.d.ts.map +1 -1
- package/dist/change-set.js +15 -5
- package/dist/ci-setup.d.ts +53 -0
- package/dist/ci-setup.d.ts.map +1 -0
- package/dist/ci-setup.js +146 -0
- package/dist/composites/pulumi-adopt-op.d.ts +106 -0
- package/dist/composites/pulumi-adopt-op.d.ts.map +1 -0
- package/dist/composites/pulumi-adopt-op.js +132 -0
- package/dist/engine-events.d.ts +222 -0
- package/dist/engine-events.d.ts.map +1 -0
- package/dist/engine-events.js +280 -0
- package/dist/exit-codes.d.ts +53 -0
- package/dist/exit-codes.d.ts.map +1 -0
- package/dist/exit-codes.js +67 -0
- package/dist/index.d.ts +12 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -2
- package/dist/init-templates.d.ts.map +1 -1
- package/dist/init-templates.js +5 -3
- package/dist/integrity.json +3 -3
- package/dist/lint/post-synth/pul015.d.ts +2 -1
- package/dist/lint/post-synth/pul015.d.ts.map +1 -1
- package/dist/lock-rows.d.ts +32 -0
- package/dist/lock-rows.d.ts.map +1 -0
- package/dist/lock-rows.js +36 -0
- package/dist/manifest.json +1 -1
- package/dist/member-engine.d.ts +27 -2
- package/dist/member-engine.d.ts.map +1 -1
- package/dist/member-engine.js +58 -7
- package/dist/member-outputs.d.ts.map +1 -1
- package/dist/member-outputs.js +4 -4
- package/dist/op/activities/index.d.ts +9 -1
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/index.js +8 -1
- package/dist/op/activities/pulumi-adopt.d.ts +104 -0
- package/dist/op/activities/pulumi-adopt.d.ts.map +1 -0
- package/dist/op/activities/pulumi-adopt.js +216 -0
- package/dist/op/activities/pulumi.d.ts.map +1 -1
- package/dist/op/activities/pulumi.js +2 -1
- package/dist/op/activity-contracts.d.ts +73 -0
- package/dist/op/activity-contracts.d.ts.map +1 -1
- package/dist/op/activity-contracts.js +49 -0
- package/dist/op/builders.d.ts +9 -0
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/builders.js +20 -0
- package/dist/plan-shape.d.ts +47 -0
- package/dist/plan-shape.d.ts.map +1 -0
- package/dist/plan-shape.js +94 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +25 -4
- package/dist/plunge-timings.d.ts +43 -0
- package/dist/plunge-timings.d.ts.map +1 -0
- package/dist/plunge-timings.js +106 -0
- package/dist/pulumi-up.d.ts +376 -0
- package/dist/pulumi-up.d.ts.map +1 -0
- package/dist/pulumi-up.js +823 -0
- package/dist/release-copy.d.ts +47 -0
- package/dist/release-copy.d.ts.map +1 -0
- package/dist/release-copy.js +131 -0
- package/dist/review-stack.d.ts +92 -0
- package/dist/review-stack.d.ts.map +1 -0
- package/dist/review-stack.js +222 -0
- package/dist/rules/pul015.ts +2 -1
- package/dist/run.d.ts +250 -7
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +432 -26
- package/dist/stack-export-snapshot.d.ts +125 -0
- package/dist/stack-export-snapshot.d.ts.map +1 -0
- package/dist/stack-export-snapshot.js +233 -0
- package/dist/stack-ops.d.ts +175 -0
- package/dist/stack-ops.d.ts.map +1 -0
- package/dist/stack-ops.js +400 -0
- package/dist/stack-references.d.ts +46 -0
- package/dist/stack-references.d.ts.map +1 -0
- package/dist/stack-references.js +88 -0
- package/dist/trace-facts.d.ts +34 -0
- package/dist/trace-facts.d.ts.map +1 -0
- package/dist/trace-facts.js +81 -0
- package/dist/workspace-member.d.ts +13 -2
- package/dist/workspace-member.d.ts.map +1 -1
- package/dist/workspace-member.js +14 -9
- package/package.json +2 -2
- package/src/adopt.ts +202 -0
- package/src/capability-plugin.ts +15 -6
- package/src/change-set.ts +29 -5
- package/src/ci-setup.test.ts +123 -0
- package/src/ci-setup.ts +176 -0
- package/src/composites/pulumi-adopt-op.test.ts +223 -0
- package/src/composites/pulumi-adopt-op.ts +191 -0
- package/src/composites/pulumi-watch-op.test.ts +2 -2
- package/src/engine-events.test.ts +134 -0
- package/src/engine-events.ts +390 -0
- package/src/engine-graph.test.ts +1 -1
- package/src/exit-codes.test.ts +21 -0
- package/src/exit-codes.ts +71 -0
- package/src/index.ts +75 -1
- package/src/init-templates.ts +5 -3
- package/src/lint/post-synth/pul015.ts +2 -1
- package/src/lock-rows.test.ts +60 -0
- package/src/lock-rows.ts +56 -0
- package/src/member-engine.test.ts +112 -0
- package/src/member-engine.ts +73 -6
- package/src/member-outputs.test.ts +2 -2
- package/src/member-outputs.ts +9 -8
- package/src/op/activities/index.ts +19 -1
- package/src/op/activities/pulumi-adopt.ts +334 -0
- package/src/op/activities/pulumi.ts +2 -1
- package/src/op/activity-contracts.ts +75 -0
- package/src/op/builders.ts +37 -0
- package/src/plan-shape.test.ts +91 -0
- package/src/plan-shape.ts +105 -0
- package/src/plugin.test.ts +1 -1
- package/src/plugin.ts +28 -4
- package/src/plunge-timings.test.ts +241 -0
- package/src/plunge-timings.ts +115 -0
- package/src/plunge-up-failed.test.ts +54 -0
- package/src/pr-note.test.ts +87 -0
- package/src/pulumi-up.test.ts +470 -0
- package/src/pulumi-up.ts +1032 -0
- package/src/release-copy.test.ts +74 -0
- package/src/release-copy.ts +142 -0
- package/src/review-stack.test.ts +254 -0
- package/src/review-stack.ts +266 -0
- package/src/run.test.ts +176 -3
- package/src/run.ts +501 -30
- package/src/stack-export-snapshot.test.ts +190 -0
- package/src/stack-export-snapshot.ts +312 -0
- package/src/stack-ops.test.ts +286 -0
- package/src/stack-ops.ts +552 -0
- package/src/stack-references.test.ts +96 -0
- package/src/stack-references.ts +110 -0
- package/src/testdata/pulumi-stream/README.md +16 -0
- package/src/testdata/pulumi-stream/preview-stream.ndjson +42 -0
- package/src/testdata/pulumi-stream/up-failed-plunge.ndjson +5 -0
- package/src/trace-facts.test.ts +38 -0
- package/src/trace-facts.ts +83 -0
- package/src/workspace-member.test.ts +2 -1
- package/src/workspace-member.ts +30 -12
|
@@ -0,0 +1,823 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `pulumi-up`, the pulumi lexicon's apply capability (#4541): a Pulumi or
|
|
3
|
+
* plunge stack as a component step that plans and applies, the way
|
|
4
|
+
* `terraform-apply` does a root. Ruling 1 on INTENTIUS/plywood#1 widened
|
|
5
|
+
* #4450 and #4476 so chant runs `up`, from an apply job holding the apply
|
|
6
|
+
* identity only; the argv go through `runApplyPulumi` and `runApplyPlunge`
|
|
7
|
+
* (`./run.ts`), each one fixed form.
|
|
8
|
+
*
|
|
9
|
+
* The step is `{ kind: "pulumi-up", dir, stack }`, with optional `member`,
|
|
10
|
+
* `config`, `engine`, `cwd`, `lockGrain` and `policyPacks`. One kind covers
|
|
11
|
+
* both engines; `pulumiMemberEngine` (`./member-engine.ts`, #4559) picks
|
|
12
|
+
* the binary from the step's `engine`, `plywood.yml`, `PULUMI_BACKEND_URL`
|
|
13
|
+
* or `Pulumi.yaml`, and the artifact records which rule answered.
|
|
14
|
+
*
|
|
15
|
+
* ## plan
|
|
16
|
+
*
|
|
17
|
+
* The preview runs with `PULUMI_ENABLE_STREAMING_JSON_PREVIEW=true` (ruling
|
|
18
|
+
* C), so one engine run gives the change set and CrossGuard's verdict
|
|
19
|
+
* (`./engine-events.ts`). On plunge it also writes `--save-plan` into a
|
|
20
|
+
* scratch directory under the OS temp dir, never the working tree, since the
|
|
21
|
+
* file holds secret ciphertext. The plan answers:
|
|
22
|
+
*
|
|
23
|
+
* - `member`: `<member>/<stack>`, the name in the wave's set digest;
|
|
24
|
+
* - `planDigest` and `changeSet` from `pulumiPreviewPart`; for a member on
|
|
25
|
+
* the resource lock grain (the step's `lockGrain`, else `ci.lockGrain`
|
|
26
|
+
* through `DeployContext.lockGrain`, else `resource` on plunge and
|
|
27
|
+
* `target` on stock Pulumi, #4552) the digest leaves `same` and `read`
|
|
28
|
+
* steps out (ruling B);
|
|
29
|
+
* - `outputs` from the stack step's planned outputs;
|
|
30
|
+
* - `shape` from `pulumiPlanShape` (`./plan-shape.ts`);
|
|
31
|
+
* - `artifact`: the engine, the shape, the digest, CrossGuard's findings,
|
|
32
|
+
* and on plunge the plan file.
|
|
33
|
+
*
|
|
34
|
+
* A mandatory CrossGuard violation, or a preview Pulumi fails with its
|
|
35
|
+
* policy code, throws {@link PulumiPolicyDeniedError}: no gate opens. Any
|
|
36
|
+
* other failed preview throws, naming the engine's error.
|
|
37
|
+
*
|
|
38
|
+
* With `DeployContext.unresolvedInputs` set the member is a hole naming the
|
|
39
|
+
* upstreams it waits on, as `terraform-apply`'s `waitingRoot` is. A stand-in
|
|
40
|
+
* preview is later work.
|
|
41
|
+
*
|
|
42
|
+
* ## run
|
|
43
|
+
*
|
|
44
|
+
* - plunge, with the plan's artifact: `plunge up --plan <file> --yes --json
|
|
45
|
+
* --stack <s>`. plunge checks the plan's stamp and refuses a plan whose
|
|
46
|
+
* records moved, or one with no stamp, before anything changes (exit 40),
|
|
47
|
+
* which throws {@link PulumiPlanMovedError}: the table's moved code, 4.
|
|
48
|
+
* - stock Pulumi, with the artifact: preview again and throw core's
|
|
49
|
+
* `PlanShapeMovedError` when the fresh shape goes beyond the approved one;
|
|
50
|
+
* then `pulumi up --yes --json --stack <s>`, and compare the steps in its
|
|
51
|
+
* event stream with the approved shape (ruling A). A difference fails the
|
|
52
|
+
* step with {@link PulumiAppliedShapeError}, whose message the apply
|
|
53
|
+
* record keeps. It is failed, never moved: resources changed.
|
|
54
|
+
* - no approved plan (`chant run --components`, a release promote, a fan-out
|
|
55
|
+
* with no wave gate): preview and `up` in one go (ruling D), as
|
|
56
|
+
* `terraform-apply` does with no plan.
|
|
57
|
+
*
|
|
58
|
+
* Every engine exit goes through `pulumiExitOf` (`./exit-codes.ts`, #4553):
|
|
59
|
+
* plunge's 40 is moved, Pulumi's policy code is denied, anything else is
|
|
60
|
+
* failed with the stream's error and the URNs that landed.
|
|
61
|
+
*
|
|
62
|
+
* After `up`, the stack's outputs come from `stack output --json
|
|
63
|
+
* --show-secrets=false`, secrets as `[secret]`. Then `stack export --stack
|
|
64
|
+
* <s>` runs, its secrets are stripped, and it goes to the index store as
|
|
65
|
+
* the member's snapshot (#4546, `./stack-export-snapshot.ts`), at
|
|
66
|
+
* `<env>/<stack>__pulumi-export` on `refs/chant/snapshots` by default. The
|
|
67
|
+
* graph and the member's reads take it from there, so the member needs no
|
|
68
|
+
* checked-in `stack-export.json`. A failed export or write leaves the apply
|
|
69
|
+
* standing and says why in `exportSnapshot`.
|
|
70
|
+
*
|
|
71
|
+
* ## policy packs at base (#4613)
|
|
72
|
+
*
|
|
73
|
+
* Under a pull request or an op wave the caller sets
|
|
74
|
+
* `DeployContext.policyBase`, and the plan copies its CrossGuard packs out
|
|
75
|
+
* of that commit into the step's scratch directory before the preview
|
|
76
|
+
* (`@intentius/chant/policy-packs`): every `ci.policy.packs` entry of the
|
|
77
|
+
* declaration at base (or of `ci.policy.source`), then each step pack that
|
|
78
|
+
* is in the repository. A step pack the base lacks, or one that differs on
|
|
79
|
+
* the head, is a note on stderr and in the artifact; the base's copy is the
|
|
80
|
+
* one that runs. `run` hands `up` the same copies. A base or source that
|
|
81
|
+
* cannot be read throws, so no gate opens on a plan no policy judged.
|
|
82
|
+
*
|
|
83
|
+
* ## config
|
|
84
|
+
*
|
|
85
|
+
* `config` (a dependent's `stackOutput()` wiring) is written into a scratch
|
|
86
|
+
* copy of `Pulumi.<stack>.yaml` and passed with `--config-file`; `--config`
|
|
87
|
+
* is never used, since it saves into the working tree's file. A key with no
|
|
88
|
+
* `:` is the project's own (`<project>:<key>`).
|
|
89
|
+
*
|
|
90
|
+
* ## StackReference (#4547)
|
|
91
|
+
*
|
|
92
|
+
* `declaredReads` reads each step's export for its `StackReference`s
|
|
93
|
+
* (`./stack-references.ts`) and reports each as a read of the other
|
|
94
|
+
* `pulumi-up` step that applies the referenced stack, so core orders the
|
|
95
|
+
* reader after it, holds the reader when it fails, and leaves the reader a
|
|
96
|
+
* hole while the upstream's plan cannot say an output it reads. A reference
|
|
97
|
+
* to a stack no step applies adds no edge and is named on the plan's
|
|
98
|
+
* artifact under `stackReferences.outside`.
|
|
99
|
+
*
|
|
100
|
+
* ## releases (#4542)
|
|
101
|
+
*
|
|
102
|
+
* `releaseInputs` answers the stack's directory, so a release cut records it
|
|
103
|
+
* and a promote or rollback under the release previews and applies the
|
|
104
|
+
* recorded copy, under the release gate with that copy's plan. A stack whose
|
|
105
|
+
* backend is a file path relative to its directory is unbound: a copy
|
|
106
|
+
* elsewhere would read no state.
|
|
107
|
+
*
|
|
108
|
+
* The cut records tracked files, so a Node program's copy has no
|
|
109
|
+
* `node_modules`. Before the copy's first preview, `plan` and `run` call
|
|
110
|
+
* `prepareReleaseCopy` (`./release-copy.ts`), which runs `pulumi install`
|
|
111
|
+
* (`plunge install`) in the copy when its runtime needs one, and refuses a
|
|
112
|
+
* Node copy with no lockfile by name, since an install would take today's
|
|
113
|
+
* versions rather than the release's.
|
|
114
|
+
*
|
|
115
|
+
* ## review stacks (#4609)
|
|
116
|
+
*
|
|
117
|
+
* When `DeployContext.env` is a pull request's environment `pr-<n>` (`chant
|
|
118
|
+
* components pr-env up`), the step applies its review stack instead of its
|
|
119
|
+
* own: the step's stack with the last segment replaced by `pr-<n>` (or
|
|
120
|
+
* `pr-<n>-<reviewSuffix>`), so `acme/shop/dev` becomes `acme/shop/pr-12`.
|
|
121
|
+
* Before the first preview, `plan` runs `stack init <pr-stack> --no-select
|
|
122
|
+
* --copy-config-from <template>` (`initReviewStack`, `./review-stack.ts`),
|
|
123
|
+
* where the template is `CHANT_ENV_TEMPLATE`'s stack when it names a stack of
|
|
124
|
+
* this project (`<project>/<stack>`, plywood's `review_stacks.template`),
|
|
125
|
+
* else the step's own stack. A stack that exists already is updated. The
|
|
126
|
+
* plan gives core the stack's `location`, `<engine>:<dir>#<stack>`, which
|
|
127
|
+
* `pr-env up` keeps on the environment's record and `pr-env down` and the
|
|
128
|
+
* TTL sweep read back to destroy and remove it (`reviewStackTeardown`).
|
|
129
|
+
*/
|
|
130
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
131
|
+
import { tmpdir } from "node:os";
|
|
132
|
+
import { basename, isAbsolute, join, relative, resolve } from "node:path";
|
|
133
|
+
import { PlanShapeMovedError } from "@intentius/chant/components/covered-round";
|
|
134
|
+
import { computePlanDigest } from "@intentius/chant/lifecycle/plan-digest";
|
|
135
|
+
import { parseYAML } from "@intentius/chant/yaml";
|
|
136
|
+
import { pulumiPreviewPart } from "./change-set.js";
|
|
137
|
+
import { PULUMI_STREAMING_PREVIEW_ENV, pulumiAppliedSteps, pulumiFailedUrns, pulumiLandedUrns, pulumiPolicyEvents, pulumiPolicyFindings, pulumiStreamError, pulumiStreamPreview, pulumiStreamSummary, pulumiStreamTimings, } from "./engine-events.js";
|
|
138
|
+
import { CHANT_EXIT } from "@intentius/chant/exit-codes";
|
|
139
|
+
import { pulumiExitOf } from "./exit-codes.js";
|
|
140
|
+
import { pulumiMemberEngine } from "./member-engine.js";
|
|
141
|
+
import { ciEngineDir, ciRelease, CI_ENGINE_ROOT } from "./ci-setup.js";
|
|
142
|
+
import { pulumiMemberPlannedOutputs } from "./member-outputs.js";
|
|
143
|
+
import { pulumiPlanShape, pulumiShapeDifferences } from "./plan-shape.js";
|
|
144
|
+
import { pulumiLockRows } from "./lock-rows.js";
|
|
145
|
+
import { pulumiTraceFacts } from "./trace-facts.js";
|
|
146
|
+
import { defaultPlungeRunner, defaultPulumiRunner, engineRunnerAt, plungeUpArgs, pulumiMemberStackOutputs, pulumiRunFailure, pulumiUpArgs, pulumiUpPreviewArgs, memberExportOf, runAllowedPlunge, runAllowedPulumi, runApplyPlunge, runApplyPulumi, } from "./run.js";
|
|
147
|
+
import { writePulumiExportSnapshot } from "./stack-export-snapshot.js";
|
|
148
|
+
import { pointerSegment, pulumiStackReferences, stackReferenceOwner } from "./stack-references.js";
|
|
149
|
+
import { isObject } from "./values.js";
|
|
150
|
+
import { prepareReleaseCopy } from "./release-copy.js";
|
|
151
|
+
import { initReviewStack, reviewConfigSource, reviewStackFor, reviewStackLocation } from "./review-stack.js";
|
|
152
|
+
/**
|
|
153
|
+
* The packs a preview runs (#4613). With `ctx.policyBase` each step pack in
|
|
154
|
+
* the repository and every `ci.policy.packs` entry is copied from base into
|
|
155
|
+
* `scratch`, and the notes say which step packs did not run as the head has
|
|
156
|
+
* them. Without it, the step's packs as they stand. Throws when the base or
|
|
157
|
+
* the policy source cannot be read: the preview then does not run.
|
|
158
|
+
*/
|
|
159
|
+
async function policyPacksFor(ctx, r, input, scratch) {
|
|
160
|
+
const own = input.policyPacks ?? [];
|
|
161
|
+
if (!ctx.policyBase)
|
|
162
|
+
return { packs: own };
|
|
163
|
+
let mod;
|
|
164
|
+
try {
|
|
165
|
+
mod = await import("@intentius/chant/policy-packs");
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
mod = undefined;
|
|
169
|
+
}
|
|
170
|
+
if (!mod)
|
|
171
|
+
throw new Error(`pulumi-up: "${r.member}": the policy packs are read at base ${ctx.policyBase.slice(0, 12)}, and this chant has no reader for them; update @intentius/chant`);
|
|
172
|
+
const got = await mod.policyPacksAtBase({ cwd: r.dir, base: ctx.policyBase, stepPacks: own, tmp: scratch });
|
|
173
|
+
if ("error" in got)
|
|
174
|
+
throw new Error(`pulumi-up: "${r.member}": ${got.error}, so the preview did not run`);
|
|
175
|
+
for (const note of got.notes)
|
|
176
|
+
console.error(`pulumi-up: "${r.member}": ${note}`);
|
|
177
|
+
if (got.source)
|
|
178
|
+
console.error(`pulumi-up: "${r.member}": ci.policy.packs read from ${got.source.repo}@${got.source.ref} at commit ${got.source.commit}`);
|
|
179
|
+
const read = {
|
|
180
|
+
base: got.base,
|
|
181
|
+
packs: got.packs.map((p) => ({ dir: p.dir, ...(p.config ? { config: p.config } : {}), from: p.from, name: p.name })),
|
|
182
|
+
notes: got.notes,
|
|
183
|
+
...(got.source ? { source: { ...got.source } } : {}),
|
|
184
|
+
};
|
|
185
|
+
return { packs: got.packs.map((p) => ({ dir: p.dir, ...(p.config ? { config: p.config } : {}) })), read };
|
|
186
|
+
}
|
|
187
|
+
/** plunge refused a saved plan whose records moved, or one with no stamp (exit 40). `planMoved` is core's moved mark. */
|
|
188
|
+
export class PulumiPlanMovedError extends Error {
|
|
189
|
+
constructor(member, message) {
|
|
190
|
+
super(message);
|
|
191
|
+
this.member = member;
|
|
192
|
+
this.planMoved = true;
|
|
193
|
+
this.name = "PulumiPlanMovedError";
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/** CrossGuard denied the plan: a mandatory violation, or the engine's policy exit. No gate opens. */
|
|
197
|
+
export class PulumiPolicyDeniedError extends Error {
|
|
198
|
+
constructor(member, findings, detail) {
|
|
199
|
+
const denied = findings.denials.map((d) => `${d.rule}: ${d.message}`).join("; ");
|
|
200
|
+
super(`"${member}": CrossGuard denied the plan${denied ? `: ${denied}` : ""}${detail ? ` (${detail})` : ""}`);
|
|
201
|
+
this.member = member;
|
|
202
|
+
this.findings = findings;
|
|
203
|
+
this.policyDenied = true;
|
|
204
|
+
/** The table's code for it (#4553, ruling B: 7 everywhere). */
|
|
205
|
+
this.exitCode = CHANT_EXIT.denied;
|
|
206
|
+
this.name = "PulumiPolicyDeniedError";
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* A stock `pulumi up` ran steps the approved shape does not hold (ruling A).
|
|
211
|
+
* The update has run, so this is a failure, not a moved plan: the step fails
|
|
212
|
+
* and the differences land on the apply record through the message.
|
|
213
|
+
*/
|
|
214
|
+
export class PulumiAppliedShapeError extends Error {
|
|
215
|
+
constructor(member, differences, summary) {
|
|
216
|
+
super(`"${member}": pulumi up ran steps the approved plan did not hold: ${differences.join("; ")}. The update ran; check the stack and plan it again.`);
|
|
217
|
+
this.member = member;
|
|
218
|
+
this.differences = differences;
|
|
219
|
+
this.summary = summary;
|
|
220
|
+
this.name = "PulumiAppliedShapeError";
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* An `up` that failed (#4553's failed outcome). It carries the stream's
|
|
225
|
+
* summary, plunge's per-resource timings included (#4579), so the run record
|
|
226
|
+
* and the trace (`./trace-facts.ts` reads `member` and `summary` off the
|
|
227
|
+
* error) keep what ran before it stopped.
|
|
228
|
+
*/
|
|
229
|
+
export class PulumiUpFailedError extends Error {
|
|
230
|
+
constructor(member, message, summary) {
|
|
231
|
+
super(message);
|
|
232
|
+
this.member = member;
|
|
233
|
+
this.summary = summary;
|
|
234
|
+
this.name = "PulumiUpFailedError";
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/** A stock Pulumi member set to the resource grain (#4552): the backend's stack lock already orders its updates. */
|
|
238
|
+
export class PulumiResourceGrainRefusedError extends Error {
|
|
239
|
+
constructor(member) {
|
|
240
|
+
super(`"${member}": the resource lock grain needs plunge; a stock Pulumi stack keeps the stack grain, since its backend's stack lock orders updates (ci.lockGrain: target)`);
|
|
241
|
+
this.member = member;
|
|
242
|
+
this.name = "PulumiResourceGrainRefusedError";
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
/** The default lock grain for an engine (#4552's design): resource on plunge, the stack (target) on stock Pulumi. */
|
|
246
|
+
export function pulumiDefaultLockGrain(engine) {
|
|
247
|
+
return engine === "plunge" ? "resource" : "target";
|
|
248
|
+
}
|
|
249
|
+
const STREAMING = { [PULUMI_STREAMING_PREVIEW_ENV]: "true" };
|
|
250
|
+
function stackDir(input) {
|
|
251
|
+
return isAbsolute(input.dir) ? input.dir : resolve(input.cwd ?? process.cwd(), input.dir);
|
|
252
|
+
}
|
|
253
|
+
function readProject(dir) {
|
|
254
|
+
for (const name of ["Pulumi.yaml", "Pulumi.yml"]) {
|
|
255
|
+
const file = join(dir, name);
|
|
256
|
+
if (!existsSync(file))
|
|
257
|
+
continue;
|
|
258
|
+
try {
|
|
259
|
+
return parseYAML(readFileSync(file, "utf-8"));
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
return undefined;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return undefined;
|
|
266
|
+
}
|
|
267
|
+
function requireInput(input) {
|
|
268
|
+
if (typeof input?.dir !== "string" || input.dir === "")
|
|
269
|
+
throw new Error('pulumi-up: the step needs "dir", the directory holding Pulumi.yaml');
|
|
270
|
+
if (typeof input.stack !== "string" || input.stack === "" || input.stack.startsWith("-"))
|
|
271
|
+
throw new Error('pulumi-up: the step needs "stack", the stack to apply');
|
|
272
|
+
}
|
|
273
|
+
/** `<member>/<stack>`: the member's name in the wave's set digest. */
|
|
274
|
+
export function pulumiUpMember(input) {
|
|
275
|
+
const dir = stackDir(input);
|
|
276
|
+
const named = input.member ?? readProject(dir)?.name ?? basename(dir);
|
|
277
|
+
return `${named}/${input.stack}`;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* The stack's config with `config` written over it, as the text of a
|
|
281
|
+
* `Pulumi.<stack>.yaml` (JSON, which YAML reads). A key with no `:` is the
|
|
282
|
+
* project's own. A value left `undefined` or `null` is not written.
|
|
283
|
+
*/
|
|
284
|
+
export function pulumiScratchConfig(dir, stack, config) {
|
|
285
|
+
const project = readProject(dir)?.name;
|
|
286
|
+
const file = join(dir, `Pulumi.${stack}.yaml`);
|
|
287
|
+
let doc = {};
|
|
288
|
+
if (existsSync(file)) {
|
|
289
|
+
try {
|
|
290
|
+
doc = parseYAML(readFileSync(file, "utf-8")) ?? {};
|
|
291
|
+
}
|
|
292
|
+
catch (err) {
|
|
293
|
+
throw new Error(`pulumi-up: ${file} does not read as YAML, so its config cannot be copied: ${err instanceof Error ? err.message : String(err)}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const merged = { ...(doc.config ?? {}) };
|
|
297
|
+
for (const [key, value] of Object.entries(config)) {
|
|
298
|
+
if (value === undefined || value === null)
|
|
299
|
+
continue;
|
|
300
|
+
const full = key.includes(":") || typeof project !== "string" ? key : `${project}:${key}`;
|
|
301
|
+
merged[full] = value;
|
|
302
|
+
}
|
|
303
|
+
return `${JSON.stringify({ ...doc, config: merged }, null, 2)}\n`;
|
|
304
|
+
}
|
|
305
|
+
/** The variable `pr-env up` sets to `ci.prEnvironment.template` (core's `PR_ENV_TEMPLATE_VAR`). */
|
|
306
|
+
export const PR_ENV_TEMPLATE_VARIABLE = "CHANT_ENV_TEMPLATE";
|
|
307
|
+
/**
|
|
308
|
+
* The step as it applies in `env` (#4609): with its review stack in place of
|
|
309
|
+
* its own stack in a pull request's environment, and the stack it copies its
|
|
310
|
+
* config from. The step unchanged, with no review, anywhere else.
|
|
311
|
+
*/
|
|
312
|
+
export function pulumiReviewStep(input, env, template) {
|
|
313
|
+
requireInput(input);
|
|
314
|
+
const stack = reviewStackFor(input.stack, env, input.reviewSuffix);
|
|
315
|
+
if (!stack)
|
|
316
|
+
return { step: input };
|
|
317
|
+
const project = readProject(stackDir(input))?.name;
|
|
318
|
+
const copyConfigFrom = reviewConfigSource({ stack: input.stack, ...(typeof project === "string" ? { project } : {}) }, template, stack);
|
|
319
|
+
return { step: { ...input, stack }, review: { stack, ...(copyConfigFrom ? { copyConfigFrom } : {}) } };
|
|
320
|
+
}
|
|
321
|
+
function templateOf(opts) {
|
|
322
|
+
const v = (opts.env ?? process.env)[PR_ENV_TEMPLATE_VARIABLE];
|
|
323
|
+
return typeof v === "string" && v !== "" ? v : undefined;
|
|
324
|
+
}
|
|
325
|
+
/** The review stack's location on the environment's record: `<engine>:<dir>#<stack>`, `dir` relative to the working directory core runs in. */
|
|
326
|
+
function locationOf(r, stack) {
|
|
327
|
+
return reviewStackLocation(r.engine, relative(process.cwd(), r.dir) || ".", stack);
|
|
328
|
+
}
|
|
329
|
+
/** A plan file name with nothing a path could read into. */
|
|
330
|
+
const fileSafe = (s) => s.replace(/[^A-Za-z0-9._-]+/g, "_");
|
|
331
|
+
function resolveStep(input, opts, declared) {
|
|
332
|
+
requireInput(input);
|
|
333
|
+
const dir = stackDir(input);
|
|
334
|
+
const member = pulumiUpMember(input);
|
|
335
|
+
const picked = pulumiMemberEngine(dir, input.engine, opts.env ? { env: opts.env } : {});
|
|
336
|
+
// #4552: the step's own grain, else ci.lockGrain as the caller read it at base, else the engine's default.
|
|
337
|
+
const grain = input.lockGrain ?? declared ?? pulumiDefaultLockGrain(picked.engine);
|
|
338
|
+
if (grain === "resource" && picked.engine === "pulumi")
|
|
339
|
+
throw new PulumiResourceGrainRefusedError(member);
|
|
340
|
+
const engineBin = pulumiUpEngineBin(input, picked.engine);
|
|
341
|
+
return { dir, member, engine: picked.engine, ...(engineBin ? { engineBin } : {}), engineSource: picked.source, engineBecause: picked.because, grain };
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* The binary a step pinned with `engineVersion` runs (#4592): the versioned
|
|
345
|
+
* path the install lines write. Undefined without `engineVersion`, so the
|
|
346
|
+
* binary on PATH runs. A version or root a shell or path would read as
|
|
347
|
+
* more is refused, as the install lines refuse it.
|
|
348
|
+
*/
|
|
349
|
+
export function pulumiUpEngineBin(input, engine) {
|
|
350
|
+
if (input.engineVersion === undefined) {
|
|
351
|
+
if (input.engineRoot !== undefined)
|
|
352
|
+
throw new Error('pulumi-up: "engineRoot" needs "engineVersion", the release installed under it');
|
|
353
|
+
return undefined;
|
|
354
|
+
}
|
|
355
|
+
try {
|
|
356
|
+
ciRelease(engine, input.engineVersion);
|
|
357
|
+
return `${ciEngineDir(engine, input.engineVersion, input.engineRoot ?? CI_ENGINE_ROOT)}/${engine}`;
|
|
358
|
+
}
|
|
359
|
+
catch (err) {
|
|
360
|
+
throw new Error(`pulumi-up: ${err instanceof Error ? err.message : String(err)}`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
/** The `run` and `plunge` options for the stack output and export reads: the pinned binary's for both when the step names one (#4592), else the caller's. */
|
|
364
|
+
function runnerOptions(r, opts) {
|
|
365
|
+
if (r.engineBin) {
|
|
366
|
+
const { pulumi, plunge } = runnersFor(r, opts);
|
|
367
|
+
return { run: pulumi, plunge };
|
|
368
|
+
}
|
|
369
|
+
return { ...(opts.run ? { run: opts.run } : {}), ...(opts.plunge ? { plunge: opts.plunge } : {}) };
|
|
370
|
+
}
|
|
371
|
+
/** The runners a resolved step uses: the pinned binary's when it names one, else the options' or the PATH default. */
|
|
372
|
+
function runnersFor(r, opts) {
|
|
373
|
+
if (r.engineBin) {
|
|
374
|
+
const pinned = opts.runAt ? opts.runAt(r.engineBin) : engineRunnerAt(r.engineBin);
|
|
375
|
+
return { pulumi: pinned, plunge: pinned };
|
|
376
|
+
}
|
|
377
|
+
return { pulumi: opts.run ?? defaultPulumiRunner, plunge: opts.plunge ?? defaultPlungeRunner };
|
|
378
|
+
}
|
|
379
|
+
/** The scratch directory, the config copy in it and plunge's plan file path. */
|
|
380
|
+
function scratchFor(r, input, opts) {
|
|
381
|
+
const scratch = mkdtempSync(join(opts.tmp ?? tmpdir(), "chant-pulumi-up-"));
|
|
382
|
+
let configFile;
|
|
383
|
+
if (input.config && Object.keys(input.config).length > 0) {
|
|
384
|
+
configFile = join(scratch, `Pulumi.${input.stack}.yaml`);
|
|
385
|
+
writeFileSync(configFile, pulumiScratchConfig(r.dir, input.stack, input.config), { mode: 0o600 });
|
|
386
|
+
}
|
|
387
|
+
const planFile = r.engine === "plunge" ? join(scratch, `${fileSafe(r.member)}.plan.json`) : undefined;
|
|
388
|
+
return { scratch, ...(configFile ? { configFile } : {}), ...(planFile ? { planFile } : {}) };
|
|
389
|
+
}
|
|
390
|
+
function dropScratch(scratch) {
|
|
391
|
+
if (!scratch)
|
|
392
|
+
return;
|
|
393
|
+
try {
|
|
394
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
395
|
+
}
|
|
396
|
+
catch {
|
|
397
|
+
// A scratch dir that stays behind holds nothing a later run reads.
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
/** The streamed preview, as part, shape and CrossGuard findings. Throws for a failed preview or a denial. */
|
|
401
|
+
async function previewStack(ctx, r, input, opts, files) {
|
|
402
|
+
const targets = ctx.targets && ctx.targets.length > 0 ? ctx.targets : undefined;
|
|
403
|
+
// #4613: the packs read at base when the caller gave them, else the step's as they stand.
|
|
404
|
+
const packs = files.policyPacks ?? input.policyPacks ?? [];
|
|
405
|
+
const args = pulumiUpPreviewArgs({
|
|
406
|
+
stack: input.stack,
|
|
407
|
+
...(files.planFile ? { savePlan: files.planFile } : {}),
|
|
408
|
+
...(files.configFile ? { configFile: files.configFile } : {}),
|
|
409
|
+
...(targets ? { targets } : {}),
|
|
410
|
+
...(packs.length ? { policyPacks: packs } : {}),
|
|
411
|
+
});
|
|
412
|
+
let stdout;
|
|
413
|
+
try {
|
|
414
|
+
({ stdout } =
|
|
415
|
+
r.engine === "plunge"
|
|
416
|
+
? await runAllowedPlunge(runnersFor(r, opts).plunge, args, r.dir, STREAMING)
|
|
417
|
+
: await runAllowedPulumi(runnersFor(r, opts).pulumi, args, r.dir, STREAMING));
|
|
418
|
+
}
|
|
419
|
+
catch (err) {
|
|
420
|
+
const f = pulumiRunFailure(err);
|
|
421
|
+
if (f.missingBinary)
|
|
422
|
+
throw new Error(`pulumi-up: "${r.member}": the ${r.engine} binary was not found on PATH; install ${r.engine} to plan this stack`);
|
|
423
|
+
const stream = f.stdout ?? "";
|
|
424
|
+
const policy = pulumiPolicyFindings(pulumiPolicyEvents(stream));
|
|
425
|
+
if (policy.denials.length > 0 || pulumiExitOf(f.exitCode) === CHANT_EXIT.denied)
|
|
426
|
+
throw new PulumiPolicyDeniedError(r.member, policy, pulumiStreamError(stream));
|
|
427
|
+
const why = pulumiStreamError(stream) ?? (f.stderr?.trim() || f.message);
|
|
428
|
+
throw new Error(`pulumi-up: "${r.member}": ${r.engine} preview failed: ${why}`);
|
|
429
|
+
}
|
|
430
|
+
const preview = pulumiStreamPreview(stdout);
|
|
431
|
+
const policy = pulumiPolicyFindings(pulumiPolicyEvents(stdout));
|
|
432
|
+
// A mandatory violation that left the exit at 0 still denies: the gate reads the events, never the code alone.
|
|
433
|
+
if (policy.denials.length > 0)
|
|
434
|
+
throw new PulumiPolicyDeniedError(r.member, policy);
|
|
435
|
+
const prior = await memberExportOf(r.dir, { stack: input.stack, ...(opts.snapshots !== undefined ? { store: opts.snapshots } : {}) });
|
|
436
|
+
const part = pulumiPreviewPart({
|
|
437
|
+
member: r.member,
|
|
438
|
+
preview,
|
|
439
|
+
stack: input.stack,
|
|
440
|
+
...(prior !== undefined ? { prior } : {}),
|
|
441
|
+
...(targets ? { targeted: true } : {}),
|
|
442
|
+
...(r.grain === "resource" ? { digest: { changedOnly: true } } : {}),
|
|
443
|
+
});
|
|
444
|
+
const shape = pulumiPlanShape(part, r.member);
|
|
445
|
+
return { part, preview, shape, policy, prior };
|
|
446
|
+
}
|
|
447
|
+
/** The member of a stack whose `config` or `StackReference`s read outputs not known yet: a hole naming them, no plan. */
|
|
448
|
+
function waitingStack(ctx, r, input, unresolved) {
|
|
449
|
+
const config = Object.fromEntries(Object.entries(input.config ?? {}).map(([k, v]) => [k, v === undefined ? null : v]));
|
|
450
|
+
const planDigest = computePlanDigest("pulumi-up-unresolved", { env: ctx.env, member: r.member, stack: input.stack, config, unresolved });
|
|
451
|
+
const changeSet = {
|
|
452
|
+
member: {
|
|
453
|
+
member: r.member,
|
|
454
|
+
lexicon: "pulumi",
|
|
455
|
+
planner: "pulumi",
|
|
456
|
+
scope: input.stack,
|
|
457
|
+
status: "planned",
|
|
458
|
+
planDigest,
|
|
459
|
+
holes: [
|
|
460
|
+
{
|
|
461
|
+
address: r.member,
|
|
462
|
+
type: "pulumi:pulumi:Stack",
|
|
463
|
+
reason: `the stack reads ${unresolved.join(", ")}, known only once ${unresolved.length === 1 ? "that applies" : "those apply"}; it is planned again then`,
|
|
464
|
+
},
|
|
465
|
+
],
|
|
466
|
+
},
|
|
467
|
+
entries: [],
|
|
468
|
+
};
|
|
469
|
+
return { member: r.member, planDigest, changeSet, ...(ctx.targets?.length ? { targets: ctx.targets } : {}) };
|
|
470
|
+
}
|
|
471
|
+
function isArtifact(value) {
|
|
472
|
+
const v = value;
|
|
473
|
+
return v?.kind === "pulumi-up" && typeof v.planDigest === "string" && typeof v.shape === "object" && v.shape !== null;
|
|
474
|
+
}
|
|
475
|
+
/** A release's copy of the program, made runnable before its first engine run (#4542). */
|
|
476
|
+
async function readyCopy(r, opts) {
|
|
477
|
+
const runners = runnersFor(r, opts);
|
|
478
|
+
await prepareReleaseCopy({ dir: r.dir, member: r.member, engine: r.engine, run: r.engine === "plunge" ? runners.plunge : runners.pulumi });
|
|
479
|
+
}
|
|
480
|
+
async function planStack(ctx, original, opts) {
|
|
481
|
+
// #4609: in pr-<n>, the review stack, made from the template before its first preview.
|
|
482
|
+
const { step: input, review } = pulumiReviewStep(original, ctx.env, templateOf(opts));
|
|
483
|
+
const r = resolveStep(input, opts, ctx.lockGrain);
|
|
484
|
+
const location = review ? { location: locationOf(r, review.stack) } : {};
|
|
485
|
+
const unresolved = [...(ctx.unresolvedInputs ?? [])].sort();
|
|
486
|
+
if (unresolved.length > 0)
|
|
487
|
+
return { ...waitingStack(ctx, r, input, unresolved), ...location };
|
|
488
|
+
await readyCopy(r, opts);
|
|
489
|
+
if (review) {
|
|
490
|
+
const runners = runnersFor(r, opts);
|
|
491
|
+
await initReviewStack({
|
|
492
|
+
dir: r.dir,
|
|
493
|
+
stack: review.stack,
|
|
494
|
+
engine: r.engine,
|
|
495
|
+
run: r.engine === "plunge" ? runners.plunge : runners.pulumi,
|
|
496
|
+
...(review.copyConfigFrom ? { template: review.copyConfigFrom } : {}),
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
const files = scratchFor(r, input, opts);
|
|
500
|
+
let read;
|
|
501
|
+
let packs;
|
|
502
|
+
try {
|
|
503
|
+
packs = await policyPacksFor(ctx, r, input, files.scratch);
|
|
504
|
+
read = await previewStack(ctx, r, input, opts, { ...files, policyPacks: packs.packs });
|
|
505
|
+
}
|
|
506
|
+
catch (err) {
|
|
507
|
+
dropScratch(files.scratch);
|
|
508
|
+
throw err;
|
|
509
|
+
}
|
|
510
|
+
const planDigest = read.part.member.planDigest;
|
|
511
|
+
const references = stackReferencesOf(read.prior, input);
|
|
512
|
+
const planned = pulumiMemberPlannedOutputs(r.member, { part: read.part, preview: read.preview, ...(read.prior !== undefined ? { exported: read.prior } : {}) });
|
|
513
|
+
const artifact = {
|
|
514
|
+
kind: "pulumi-up",
|
|
515
|
+
member: r.member,
|
|
516
|
+
stack: input.stack,
|
|
517
|
+
engine: r.engine,
|
|
518
|
+
engineSource: r.engineSource,
|
|
519
|
+
engineBecause: r.engineBecause,
|
|
520
|
+
planDigest,
|
|
521
|
+
shape: read.shape,
|
|
522
|
+
scratch: files.scratch,
|
|
523
|
+
...(files.planFile ? { planFile: files.planFile } : {}),
|
|
524
|
+
...(files.configFile ? { configFile: files.configFile } : {}),
|
|
525
|
+
policy: read.policy,
|
|
526
|
+
...(packs.read ? { policyPacks: packs.read } : {}),
|
|
527
|
+
...(references ? { stackReferences: references } : {}),
|
|
528
|
+
};
|
|
529
|
+
return {
|
|
530
|
+
member: r.member,
|
|
531
|
+
planDigest,
|
|
532
|
+
artifact,
|
|
533
|
+
changeSet: read.part,
|
|
534
|
+
outputs: { known: planned.known, unknown: planned.unknown },
|
|
535
|
+
shape: read.shape,
|
|
536
|
+
...(ctx.targets?.length ? { targets: ctx.targets } : {}),
|
|
537
|
+
...location,
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
/** The export's `StackReference`s, split into the ones core wired into `stackReferences` and the rest; undefined when it holds none. */
|
|
541
|
+
function stackReferencesOf(exported, input) {
|
|
542
|
+
const refs = pulumiStackReferences(exported);
|
|
543
|
+
if (refs.length === 0)
|
|
544
|
+
return undefined;
|
|
545
|
+
const wired = new Set(Object.keys(input.stackReferences ?? {}));
|
|
546
|
+
const read = refs.filter((r) => wired.has(r.name)).map((r) => r.name);
|
|
547
|
+
const outside = refs
|
|
548
|
+
.filter((r) => !wired.has(r.name))
|
|
549
|
+
.map((r) => ({
|
|
550
|
+
name: r.name,
|
|
551
|
+
why: r.outputs.length === 0 && r.secretOutputs.length > 0
|
|
552
|
+
? "every output the export recorded for it is secret, and chant reads outputs blind"
|
|
553
|
+
: r.outputs.length === 0
|
|
554
|
+
? "the export recorded no output for it"
|
|
555
|
+
: "no other pulumi-up step in this project applies that stack",
|
|
556
|
+
}));
|
|
557
|
+
const lastUpdate = exportUpdatedAt(exported);
|
|
558
|
+
return { read: [...new Set(read)].sort(), outside, ...(lastUpdate ? { lastUpdate } : {}) };
|
|
559
|
+
}
|
|
560
|
+
/** When the exported stack last updated (#4617): `deployment.manifest.time`, when it is a time. */
|
|
561
|
+
export function exportUpdatedAt(exported) {
|
|
562
|
+
const deployment = isObject(exported) && isObject(exported.deployment) ? exported.deployment : undefined;
|
|
563
|
+
const time = deployment && isObject(deployment.manifest) ? deployment.manifest.time : undefined;
|
|
564
|
+
return typeof time === "string" && !Number.isNaN(Date.parse(time)) ? time : undefined;
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* `declaredReads` (#4547): each step's `StackReference`s, read from its
|
|
568
|
+
* export (the index store's, else the checked-in one), as reads of the one
|
|
569
|
+
* other `pulumi-up` step whose project and stack the reference names, one
|
|
570
|
+
* per output the export recorded that is not secret, placed at
|
|
571
|
+
* `/stackReferences/<reference name>/<output>`.
|
|
572
|
+
*/
|
|
573
|
+
export async function pulumiDeclaredReads(ctx, members, opts = {}) {
|
|
574
|
+
const steps = members
|
|
575
|
+
.map((m) => {
|
|
576
|
+
const step = m.step;
|
|
577
|
+
if (typeof step?.dir !== "string" || typeof step.stack !== "string")
|
|
578
|
+
return undefined;
|
|
579
|
+
const dir = isAbsolute(step.dir) ? step.dir : resolve(ctx.root, step.cwd ?? ".", step.dir);
|
|
580
|
+
const project = readProject(dir)?.name;
|
|
581
|
+
return { m, step, dir, project: typeof project === "string" ? project : basename(dir) };
|
|
582
|
+
})
|
|
583
|
+
.filter((x) => x !== undefined);
|
|
584
|
+
const owners = steps.map((s) => ({ component: s.m.component, project: s.project, stack: s.step.stack }));
|
|
585
|
+
const out = [];
|
|
586
|
+
for (const s of steps) {
|
|
587
|
+
const exported = await memberExportOf(s.dir, { stack: s.step.stack, ...(opts.exportStore !== undefined ? { store: opts.exportStore } : {}) });
|
|
588
|
+
for (const ref of pulumiStackReferences(exported)) {
|
|
589
|
+
const found = stackReferenceOwner(ref, { component: s.m.component, project: s.project }, owners);
|
|
590
|
+
if (!("owner" in found))
|
|
591
|
+
continue;
|
|
592
|
+
for (const output of ref.outputs) {
|
|
593
|
+
out.push({ reader: s.m.component, index: s.m.index, upstream: found.owner.component, output, at: `/stackReferences/${pointerSegment(ref.name)}/${pointerSegment(output)}` });
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
return out;
|
|
598
|
+
}
|
|
599
|
+
/** An `up` that exited non-zero, read through the exit-code table (#4553). */
|
|
600
|
+
function upFailure(r, err, policy) {
|
|
601
|
+
const f = pulumiRunFailure(err);
|
|
602
|
+
if (f.missingBinary)
|
|
603
|
+
return new Error(`pulumi-up: "${r.member}": the ${r.engine} binary was not found on PATH; nothing was applied`);
|
|
604
|
+
const stream = f.stdout ?? "";
|
|
605
|
+
const why = pulumiStreamError(stream) ?? (f.stderr?.trim() || f.message).split("\n").slice(-3).join("\n");
|
|
606
|
+
const outcome = pulumiExitOf(f.exitCode);
|
|
607
|
+
if (outcome === CHANT_EXIT.moved) {
|
|
608
|
+
return new PulumiPlanMovedError(r.member, `"${r.member}": refusing to apply. plunge refused the saved plan, since a record it planned against moved or the plan has no stamp: ${why}. Plan it again.`);
|
|
609
|
+
}
|
|
610
|
+
const events = pulumiPolicyFindings(pulumiPolicyEvents(stream));
|
|
611
|
+
if (outcome === CHANT_EXIT.denied || events.denials.length > 0) {
|
|
612
|
+
return new PulumiPolicyDeniedError(r.member, events.denials.length > 0 ? events : policy, why);
|
|
613
|
+
}
|
|
614
|
+
const landed = pulumiLandedUrns(stream);
|
|
615
|
+
const failed = pulumiFailedUrns(stream);
|
|
616
|
+
const detail = [
|
|
617
|
+
...(failed.length > 0 ? [`failed: ${failed.join(", ")}`] : []),
|
|
618
|
+
...(landed.length > 0 ? [`landed before it stopped: ${landed.join(", ")}`] : ["nothing landed"]),
|
|
619
|
+
].join("; ");
|
|
620
|
+
return new PulumiUpFailedError(r.member, `pulumi-up: "${r.member}": ${r.engine} up failed (exit ${f.exitCode ?? "unknown"}): ${why}. ${detail}`, failedUpSummary(stream));
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* What a failed `up`'s stream still says (#4579): its `summaryEvent`, or,
|
|
624
|
+
* when the engine stopped before printing one, the per-resource timings
|
|
625
|
+
* plunge put on the steps that ran (`resOutputsEvent`, `resOpFailedEvent`),
|
|
626
|
+
* under `result: "failed"`. Undefined when the stream holds neither.
|
|
627
|
+
*/
|
|
628
|
+
export function failedUpSummary(stream) {
|
|
629
|
+
const summary = pulumiStreamSummary(stream);
|
|
630
|
+
if (summary)
|
|
631
|
+
return summary;
|
|
632
|
+
const timings = pulumiStreamTimings(stream);
|
|
633
|
+
return Object.keys(timings).length > 0 ? { resourceChanges: {}, result: "failed", timings } : undefined;
|
|
634
|
+
}
|
|
635
|
+
async function runStack(ctx, original, opts) {
|
|
636
|
+
// #4609: in pr-<n>, the review stack the plan made and previewed.
|
|
637
|
+
const { step: input } = pulumiReviewStep(original, ctx.env, templateOf(opts));
|
|
638
|
+
const r = resolveStep(input, opts, ctx.lockGrain);
|
|
639
|
+
const handed = ctx.plans?.[r.member];
|
|
640
|
+
const fromWave = isArtifact(handed);
|
|
641
|
+
const targets = ctx.targets && ctx.targets.length > 0 ? ctx.targets : undefined;
|
|
642
|
+
await readyCopy(r, opts);
|
|
643
|
+
// Ruling D: no approved plan handed in, so plan here and apply that.
|
|
644
|
+
let approved;
|
|
645
|
+
if (fromWave) {
|
|
646
|
+
approved = handed;
|
|
647
|
+
}
|
|
648
|
+
else {
|
|
649
|
+
const planned = await planStack(ctx, original, opts);
|
|
650
|
+
if (!isArtifact(planned.artifact)) {
|
|
651
|
+
throw new Error(`pulumi-up: "${r.member}": the stack's config reads outputs not known yet (${(ctx.unresolvedInputs ?? []).join(", ")}), so it cannot apply`);
|
|
652
|
+
}
|
|
653
|
+
approved = planned.artifact;
|
|
654
|
+
}
|
|
655
|
+
// The config copy: the plan's when it is still there, else written again (its text is the same).
|
|
656
|
+
const own = existsSync(approved.scratch) ? undefined : scratchFor(r, input, opts);
|
|
657
|
+
const configFile = approved.configFile && existsSync(approved.configFile) ? approved.configFile : own?.configFile;
|
|
658
|
+
// #4613: the packs the plan read at base, the same copies, else read again at that base; a plan that read none runs the step's own.
|
|
659
|
+
let policyPacks = input.policyPacks;
|
|
660
|
+
if (approved.policyPacks) {
|
|
661
|
+
const copies = approved.policyPacks.packs;
|
|
662
|
+
if (copies.every((p) => existsSync(p.dir) && (!p.config || existsSync(p.config)))) {
|
|
663
|
+
policyPacks = copies.map((p) => ({ dir: p.dir, ...(p.config ? { config: p.config } : {}) }));
|
|
664
|
+
}
|
|
665
|
+
else {
|
|
666
|
+
try {
|
|
667
|
+
policyPacks = (await policyPacksFor({ ...ctx, policyBase: approved.policyPacks.base }, r, input, own?.scratch ?? approved.scratch)).packs;
|
|
668
|
+
}
|
|
669
|
+
catch (err) {
|
|
670
|
+
dropScratch(approved.scratch);
|
|
671
|
+
dropScratch(own?.scratch);
|
|
672
|
+
throw err;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
const tail = {
|
|
677
|
+
...(configFile ? { configFile } : {}),
|
|
678
|
+
...(targets ? { targets } : {}),
|
|
679
|
+
...(policyPacks?.length ? { policyPacks } : {}),
|
|
680
|
+
};
|
|
681
|
+
try {
|
|
682
|
+
let stdout;
|
|
683
|
+
if (r.engine === "plunge") {
|
|
684
|
+
if (!approved.planFile || !existsSync(approved.planFile)) {
|
|
685
|
+
throw new Error(`pulumi-up: "${r.member}": the saved plan ${approved.planFile ?? "(none)"} is not on this machine; plan the stack in the apply job, then apply`);
|
|
686
|
+
}
|
|
687
|
+
try {
|
|
688
|
+
({ stdout } = await runApplyPlunge(runnersFor(r, opts).plunge, plungeUpArgs({ plan: approved.planFile, stack: input.stack, ...tail }), r.dir));
|
|
689
|
+
}
|
|
690
|
+
catch (err) {
|
|
691
|
+
throw upFailure(r, err, approved.policy);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
else {
|
|
695
|
+
if (fromWave) {
|
|
696
|
+
// Preview again, and hold it to the approved shape before anything changes.
|
|
697
|
+
const fresh = await previewStack(ctx, r, input, opts, { ...(configFile ? { configFile } : {}), ...(policyPacks ? { policyPacks } : {}) });
|
|
698
|
+
const differences = pulumiShapeDifferences(approved.shape, fresh.shape, fresh.preview.steps);
|
|
699
|
+
if (differences.length > 0)
|
|
700
|
+
throw new PlanShapeMovedError(r.member, differences);
|
|
701
|
+
}
|
|
702
|
+
try {
|
|
703
|
+
({ stdout } = await runApplyPulumi(runnersFor(r, opts).pulumi, pulumiUpArgs({ stack: input.stack, ...tail }), r.dir));
|
|
704
|
+
}
|
|
705
|
+
catch (err) {
|
|
706
|
+
throw upFailure(r, err, approved.policy);
|
|
707
|
+
}
|
|
708
|
+
// Ruling A: the steps the update ran, held to the approved shape.
|
|
709
|
+
const steps = pulumiAppliedSteps(stdout);
|
|
710
|
+
const ran = pulumiPreviewPart({ member: r.member, preview: { steps }, stack: input.stack });
|
|
711
|
+
const differences = pulumiShapeDifferences(approved.shape, pulumiPlanShape(ran, r.member), steps);
|
|
712
|
+
if (differences.length > 0)
|
|
713
|
+
throw new PulumiAppliedShapeError(r.member, differences, pulumiStreamSummary(stdout));
|
|
714
|
+
}
|
|
715
|
+
const outputs = await pulumiMemberStackOutputs({
|
|
716
|
+
dir: r.dir,
|
|
717
|
+
stack: input.stack,
|
|
718
|
+
engine: r.engine,
|
|
719
|
+
...runnerOptions(r, opts),
|
|
720
|
+
});
|
|
721
|
+
const summary = pulumiStreamSummary(stdout);
|
|
722
|
+
const exportSnapshot = await writePulumiExportSnapshot({
|
|
723
|
+
dir: r.dir,
|
|
724
|
+
stack: input.stack,
|
|
725
|
+
environment: ctx.env,
|
|
726
|
+
engine: r.engine,
|
|
727
|
+
...runnerOptions(r, opts),
|
|
728
|
+
...(opts.snapshots !== undefined ? { store: opts.snapshots } : {}),
|
|
729
|
+
});
|
|
730
|
+
const hasPolicy = approved.policy.warnings.length > 0 || approved.policy.denials.length > 0;
|
|
731
|
+
return {
|
|
732
|
+
member: r.member,
|
|
733
|
+
stack: input.stack,
|
|
734
|
+
engine: r.engine,
|
|
735
|
+
planDigest: approved.planDigest,
|
|
736
|
+
fromWavePlan: fromWave,
|
|
737
|
+
outputs,
|
|
738
|
+
...(summary ? { summary } : {}),
|
|
739
|
+
...(hasPolicy ? { policy: approved.policy } : {}),
|
|
740
|
+
exportSnapshot,
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
finally {
|
|
744
|
+
dropScratch(approved.scratch);
|
|
745
|
+
dropScratch(own?.scratch);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
/** `releaseInputs` (#4542): the stack's directory, or unbound when its state is a file relative to it. */
|
|
749
|
+
export function pulumiReleaseInputs(input, ctx) {
|
|
750
|
+
if (typeof input?.dir !== "string" || input.dir === "" || typeof input.stack !== "string")
|
|
751
|
+
return null;
|
|
752
|
+
const base = resolve(ctx.memberDir, input.cwd ?? ".");
|
|
753
|
+
const dir = isAbsolute(input.dir) ? input.dir : resolve(base, input.dir);
|
|
754
|
+
const project = readProject(dir);
|
|
755
|
+
if (!project)
|
|
756
|
+
return null;
|
|
757
|
+
const toPosix = (p) => p.split("\\").join("/") || ".";
|
|
758
|
+
const fromMember = (abs) => toPosix(relative(ctx.memberDir, abs));
|
|
759
|
+
const backend = project.backend?.url;
|
|
760
|
+
if (typeof backend === "string") {
|
|
761
|
+
const m = /^(?:plunge\+)?file:\/\/(.*)$/.exec(backend);
|
|
762
|
+
if (m && !m[1].startsWith("/") && !m[1].startsWith("~")) {
|
|
763
|
+
return {
|
|
764
|
+
unbound: `stack "${input.stack}" keeps its state at ${backend}, a path relative to ${fromMember(dir)}, so a preview of a recorded copy would read no state; roll it back with chant lifecycle rollback --to <ref>`,
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
const paths = [fromMember(dir)];
|
|
769
|
+
const main = typeof project.main === "string" ? resolve(dir, project.main) : undefined;
|
|
770
|
+
if (main && relative(dir, main).startsWith(".."))
|
|
771
|
+
paths.push(fromMember(main));
|
|
772
|
+
return { field: "dir", dir: fromMember(dir), paths: [...new Set(paths)].sort() };
|
|
773
|
+
}
|
|
774
|
+
/** The `pulumi-up` capability, with injectable runners for tests. */
|
|
775
|
+
export function createPulumiUpCapability(opts = {}) {
|
|
776
|
+
return {
|
|
777
|
+
kind: "pulumi-up",
|
|
778
|
+
// A stack's update has no undo short of applying the previous program, which is a change of its own.
|
|
779
|
+
rollbackPolicy: "needs-opt-out",
|
|
780
|
+
// Each target passes as `--target <urn>` to the preview and to `up`.
|
|
781
|
+
targets: "accept",
|
|
782
|
+
plan: (ctx, input) => planStack(ctx, input, opts),
|
|
783
|
+
run: (ctx, input) => runStack(ctx, input, opts),
|
|
784
|
+
// The outputs standing now, for a dependent planned before this stack applies.
|
|
785
|
+
async outputs(ctx, original) {
|
|
786
|
+
const { step: input } = pulumiReviewStep(original, ctx.env, templateOf(opts));
|
|
787
|
+
const r = resolveStep(input, opts);
|
|
788
|
+
return pulumiMemberStackOutputs({
|
|
789
|
+
dir: r.dir,
|
|
790
|
+
stack: input.stack,
|
|
791
|
+
engine: r.engine,
|
|
792
|
+
...runnerOptions(r, opts),
|
|
793
|
+
});
|
|
794
|
+
},
|
|
795
|
+
releaseInputs: (input, ctx) => pulumiReleaseInputs(input, ctx),
|
|
796
|
+
// #4552: resource on plunge, the stack (target) on stock Pulumi.
|
|
797
|
+
defaultLockGrain(input) {
|
|
798
|
+
requireInput(input);
|
|
799
|
+
return pulumiDefaultLockGrain(pulumiMemberEngine(stackDir(input), input.engine, opts.env ? { env: opts.env } : {}).engine);
|
|
800
|
+
},
|
|
801
|
+
// Stock Pulumi keeps the stack grain: its backend's stack lock already orders updates, and nothing in it is per resource.
|
|
802
|
+
lockGrainRefusal(input, grain) {
|
|
803
|
+
if (grain !== "resource")
|
|
804
|
+
return undefined;
|
|
805
|
+
requireInput(input);
|
|
806
|
+
const engine = pulumiMemberEngine(stackDir(input), input.engine, opts.env ? { env: opts.env } : {}).engine;
|
|
807
|
+
return engine === "pulumi" ? new PulumiResourceGrainRefusedError(pulumiUpMember(input)).message : undefined;
|
|
808
|
+
},
|
|
809
|
+
// URN rows, object rows, and the stack row when something may appear only at apply (./lock-rows.ts).
|
|
810
|
+
lockRows(input, plan) {
|
|
811
|
+
if (!plan.changeSet)
|
|
812
|
+
return [];
|
|
813
|
+
const project = readProject(stackDir(input))?.name;
|
|
814
|
+
// #4609: the stack the plan previewed, the review stack in pr-<n>.
|
|
815
|
+
const stack = isArtifact(plan.artifact) ? plan.artifact.stack : input.stack;
|
|
816
|
+
return pulumiLockRows(plan.changeSet, plan.member, { ...(typeof project === "string" ? { project } : {}), stack });
|
|
817
|
+
},
|
|
818
|
+
// #4555: the stack, the engine and the summary's counts, for the step's span and run record.
|
|
819
|
+
traceFacts: (input, outcome) => pulumiTraceFacts(input, outcome),
|
|
820
|
+
declaredReads: (ctx, members) => pulumiDeclaredReads(ctx, members, opts.snapshots !== undefined ? { exportStore: opts.snapshots } : {}),
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
export const pulumiUpCapability = createPulumiUpCapability();
|