@intentius/chant-lexicon-terraform 0.124.52 → 0.124.54
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 +7 -6
- package/dist/__fixtures__/pr-loop/estate.component.d.ts +7 -0
- package/dist/__fixtures__/pr-loop/estate.component.d.ts.map +1 -0
- package/dist/__fixtures__/pr-loop/estate.component.js +7 -0
- package/dist/__fixtures__/report/generate.d.ts +26 -0
- package/dist/__fixtures__/report/generate.d.ts.map +1 -0
- package/dist/__fixtures__/report/generate.js +58 -0
- package/dist/applied-state.d.ts +27 -0
- package/dist/applied-state.d.ts.map +1 -0
- package/dist/applied-state.js +44 -0
- package/dist/backend-config.d.ts +53 -0
- package/dist/backend-config.d.ts.map +1 -0
- package/dist/backend-config.js +136 -0
- package/dist/behaviour/kinds.d.ts.map +1 -1
- package/dist/behaviour/kinds.js +4 -0
- package/dist/behaviour/predict.d.ts.map +1 -1
- package/dist/behaviour/predict.js +6 -2
- package/dist/behaviour/reference-catalog.d.ts +16 -5
- package/dist/behaviour/reference-catalog.d.ts.map +1 -1
- package/dist/behaviour/reference-catalog.js +16 -5
- package/dist/behaviour/request.d.ts.map +1 -1
- package/dist/behaviour/request.js +3 -2
- package/dist/changed-roots.d.ts +6 -4
- package/dist/changed-roots.d.ts.map +1 -1
- package/dist/changed-roots.js +14 -18
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +4 -3
- package/dist/components/capability-plugin.d.ts +65 -3
- package/dist/components/capability-plugin.d.ts.map +1 -1
- package/dist/components/capability-plugin.js +197 -13
- package/dist/components/compose-group.d.ts +145 -0
- package/dist/components/compose-group.d.ts.map +1 -0
- package/dist/components/compose-group.js +407 -0
- package/dist/components/placeholder-preview.d.ts +78 -0
- package/dist/components/placeholder-preview.d.ts.map +1 -0
- package/dist/components/placeholder-preview.js +254 -0
- package/dist/compose/apply-events.d.ts +46 -0
- package/dist/compose/apply-events.d.ts.map +1 -0
- package/dist/compose/apply-events.js +77 -0
- package/dist/compose/estate.d.ts +194 -0
- package/dist/compose/estate.d.ts.map +1 -0
- package/dist/compose/estate.js +363 -0
- package/dist/compose/hcl.d.ts +99 -0
- package/dist/compose/hcl.d.ts.map +1 -0
- package/dist/compose/hcl.js +302 -0
- package/dist/compose/module-address.d.ts +24 -0
- package/dist/compose/module-address.d.ts.map +1 -0
- package/dist/compose/module-address.js +43 -0
- package/dist/compose/root.d.ts +127 -0
- package/dist/compose/root.d.ts.map +1 -0
- package/dist/compose/root.js +586 -0
- package/dist/compose/run.d.ts +183 -0
- package/dist/compose/run.d.ts.map +1 -0
- package/dist/compose/run.js +447 -0
- package/dist/compose/slice.d.ts +102 -0
- package/dist/compose/slice.d.ts.map +1 -0
- package/dist/compose/slice.js +249 -0
- package/dist/compose/state-address.d.ts +28 -0
- package/dist/compose/state-address.d.ts.map +1 -0
- package/dist/compose/state-address.js +154 -0
- package/dist/compose/state-merge.d.ts +52 -0
- package/dist/compose/state-merge.d.ts.map +1 -0
- package/dist/compose/state-merge.js +135 -0
- package/dist/compose.d.ts +179 -0
- package/dist/compose.d.ts.map +1 -0
- package/dist/compose.js +482 -0
- package/dist/composites/terraform-apply-op.d.ts.map +1 -1
- package/dist/composites/terraform-apply-op.js +7 -3
- package/dist/config.d.ts +59 -13
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +43 -9
- package/dist/describe-resources.d.ts +74 -12
- package/dist/describe-resources.d.ts.map +1 -1
- package/dist/describe-resources.js +374 -66
- package/dist/drift-read.d.ts +33 -0
- package/dist/drift-read.d.ts.map +1 -0
- package/dist/drift-read.js +243 -0
- package/dist/drift-record.d.ts +147 -0
- package/dist/drift-record.d.ts.map +1 -0
- package/dist/drift-record.js +383 -0
- package/dist/hcl/edges.d.ts.map +1 -1
- package/dist/hcl/edges.js +24 -1
- package/dist/hcl/files.d.ts +61 -0
- package/dist/hcl/files.d.ts.map +1 -0
- package/dist/hcl/files.js +221 -0
- package/dist/hcl/instances.d.ts +82 -0
- package/dist/hcl/instances.d.ts.map +1 -0
- package/dist/hcl/instances.js +390 -0
- package/dist/hcl/parse.d.ts +81 -9
- package/dist/hcl/parse.d.ts.map +1 -1
- package/dist/hcl/parse.js +114 -16
- package/dist/hcl/roots.d.ts.map +1 -1
- package/dist/hcl/roots.js +5 -0
- package/dist/hcl/suppressions.d.ts.map +1 -1
- package/dist/hcl/suppressions.js +8 -2
- package/dist/index.d.ts +11 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -2
- package/dist/integrity.json +3 -3
- package/dist/live-plan-reasons.d.ts +21 -0
- package/dist/live-plan-reasons.d.ts.map +1 -0
- package/dist/live-plan-reasons.js +47 -0
- package/dist/lsp/option-keys.d.ts.map +1 -1
- package/dist/lsp/option-keys.js +2 -0
- package/dist/manifest.json +1 -1
- package/dist/op/activities/index.d.ts +6 -2
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/index.js +5 -1
- package/dist/op/activities/live-detect.d.ts +4 -3
- package/dist/op/activities/live-detect.d.ts.map +1 -1
- package/dist/op/activities/live-detect.js +57 -32
- package/dist/op/activities/pin-rollout.d.ts +2 -0
- package/dist/op/activities/pin-rollout.d.ts.map +1 -1
- package/dist/op/activities/pin-rollout.js +1 -0
- package/dist/op/activities/terraform.d.ts +153 -5
- package/dist/op/activities/terraform.d.ts.map +1 -1
- package/dist/op/activities/terraform.js +205 -20
- package/dist/op/activity-contracts.d.ts +27 -0
- package/dist/op/activity-contracts.d.ts.map +1 -1
- package/dist/op/activity-contracts.js +19 -1
- package/dist/op/builders.d.ts +7 -1
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/builders.js +9 -0
- package/dist/pin/edit.d.ts.map +1 -1
- package/dist/pin/edit.js +35 -0
- package/dist/pin/rollout.d.ts +2 -0
- package/dist/pin/rollout.d.ts.map +1 -1
- package/dist/pin/rollout.js +14 -11
- package/dist/plan-shape.d.ts +30 -0
- package/dist/plan-shape.d.ts.map +1 -0
- package/dist/plan-shape.js +125 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +32 -18
- package/dist/report.d.ts +15 -6
- package/dist/report.d.ts.map +1 -1
- package/dist/report.js +28 -55
- package/dist/skills/chant-terraform.md +1 -1
- package/dist/state/azure-blob.d.ts +68 -0
- package/dist/state/azure-blob.d.ts.map +1 -0
- package/dist/state/azure-blob.js +201 -0
- package/dist/state/backend.d.ts +158 -0
- package/dist/state/backend.d.ts.map +1 -0
- package/dist/state/backend.js +406 -0
- package/dist/state/exec.d.ts +39 -0
- package/dist/state/exec.d.ts.map +1 -0
- package/dist/state/exec.js +80 -0
- package/dist/state/gcs.d.ts +62 -0
- package/dist/state/gcs.d.ts.map +1 -0
- package/dist/state/gcs.js +224 -0
- package/dist/state/index.d.ts +15 -0
- package/dist/state/index.d.ts.map +1 -0
- package/dist/state/index.js +14 -0
- package/dist/state/locks.d.ts +105 -0
- package/dist/state/locks.d.ts.map +1 -0
- package/dist/state/locks.js +130 -0
- package/dist/state/object-store.d.ts +37 -0
- package/dist/state/object-store.d.ts.map +1 -0
- package/dist/state/object-store.js +20 -0
- package/dist/state/root-states.d.ts +131 -0
- package/dist/state/root-states.d.ts.map +1 -0
- package/dist/state/root-states.js +241 -0
- package/dist/state/s3.d.ts +68 -0
- package/dist/state/s3.d.ts.map +1 -0
- package/dist/state/s3.js +170 -0
- package/dist/state/store.d.ts +51 -0
- package/dist/state/store.d.ts.map +1 -0
- package/dist/state/store.js +185 -0
- package/dist/terragrunt/affected.d.ts.map +1 -1
- package/dist/terragrunt/affected.js +2 -1
- package/package.json +7 -3
- package/src/__fixtures__/ACCEPTANCE.md +69 -7
- package/src/__fixtures__/compose/apply-failed-in-app.jsonl +12 -0
- package/src/__fixtures__/compose/state-app.json +66 -0
- package/src/__fixtures__/compose/state-edge.json +40 -0
- package/src/__fixtures__/compose/state-net.json +58 -0
- package/src/__fixtures__/compose/three-app.plan.json +184 -0
- package/src/__fixtures__/compose/three-composed.plan.json +509 -0
- package/src/__fixtures__/compose/three-edge.plan.json +144 -0
- package/src/__fixtures__/compose/three-net.plan.json +174 -0
- package/src/__fixtures__/compose/unknown-composed.plan.json +228 -0
- package/src/__fixtures__/compose/working-after-apply.json +145 -0
- package/src/__fixtures__/config-blocks/app/main.tf +61 -0
- package/src/__fixtures__/config-blocks/app/modules/network/main.tf +4 -0
- package/src/__fixtures__/config-blocks/app-json/main.tf.json +39 -0
- package/src/__fixtures__/config-files/shadow/main.tf +3 -0
- package/src/__fixtures__/config-files/shadow/main.tofu +3 -0
- package/src/__fixtures__/config-files/shadow/other.tf +3 -0
- package/src/__fixtures__/config-files/tf/main.tf +11 -0
- package/src/__fixtures__/config-files/tf-json/main.tf.json +18 -0
- package/src/__fixtures__/config-files/tofu/main.tofu +11 -0
- package/src/__fixtures__/config-files/tofu-json/main.tofu.json +18 -0
- package/src/__fixtures__/config-files/tofu-only/main.tofu +3 -0
- package/src/__fixtures__/drift-record/README.md +11 -0
- package/src/__fixtures__/drift-record/live-plan-sections.json +27 -0
- package/src/__fixtures__/instance-forms/app/main.tf +110 -0
- package/src/__fixtures__/instance-forms/app/modules/svc/main.tf +3 -0
- package/src/__fixtures__/instance-vars/app/main.tf +52 -0
- package/src/__fixtures__/instance-vars/app/modules/app/main.tf +12 -0
- package/src/__fixtures__/instance-vars/app/modules/edge/main.tf +3 -0
- package/src/__fixtures__/instance-vars/app/terraform.tfvars +1 -0
- package/src/__fixtures__/live-module-keyed/live-graph.json +259 -0
- package/src/__fixtures__/live-module-keyed/live-plan.json +87 -0
- package/src/__fixtures__/live-module-keyed/root/estate.chdf.hcl +1 -0
- package/src/__fixtures__/live-module-keyed/root/main.tf +21 -0
- package/src/__fixtures__/live-module-keyed/root/modules/app/main.tf +17 -0
- package/src/__fixtures__/live-module-keyed/root/modules/edge/main.tf +7 -0
- package/src/__fixtures__/live-module-keyed/root/versions.tf +30 -0
- package/src/__fixtures__/live-module-keyed/show-state.json +146 -0
- package/src/__fixtures__/pr-loop/README.md +50 -0
- package/src/__fixtures__/pr-loop/chant.config.json +13 -0
- package/src/__fixtures__/pr-loop/estate.component.ts +10 -0
- package/src/__fixtures__/pr-loop/roots/a/main.tf +11 -0
- package/src/__fixtures__/pr-loop/roots/app/main.tf +19 -0
- package/src/__fixtures__/pr-loop/roots/b/main.tf +7 -0
- package/src/__fixtures__/pr-loop/roots/dns/main.tf +3 -0
- package/src/__fixtures__/pr-loop/roots/net/main.tf +7 -0
- package/src/__fixtures__/report/applied.show.json +51 -0
- package/src/__fixtures__/report/generate.ts +63 -0
- package/src/__fixtures__/report/plan.show.json +200 -0
- package/src/__fixtures__/report/tf-apply/change-set.json +170 -0
- package/src/__fixtures__/report/tf-apply/report.json +338 -0
- package/src/__fixtures__/report/tf-plan/change-set.json +170 -0
- package/src/__fixtures__/report/tf-plan/report.json +320 -0
- package/src/__fixtures__/three-cloud-azure.graph.json +290 -0
- package/src/applied-state.ts +52 -0
- package/src/backend-config.test.ts +105 -0
- package/src/backend-config.ts +133 -0
- package/src/behaviour/kinds.ts +4 -0
- package/src/behaviour/predict.ts +5 -2
- package/src/behaviour/reference-catalog.plugin.test.ts +65 -0
- package/src/behaviour/reference-catalog.ts +16 -5
- package/src/behaviour/request.ts +3 -2
- package/src/change-set-instances.test.ts +98 -0
- package/src/changed-roots.ts +14 -16
- package/src/commands.ts +4 -3
- package/src/components/capability-plugin.test.ts +172 -5
- package/src/components/capability-plugin.ts +212 -11
- package/src/components/change-set-graph.e2e.test.ts +11 -5
- package/src/components/compose-group.test.ts +486 -0
- package/src/components/compose-group.ts +489 -0
- package/src/components/demo-composed-roots.e2e.test.ts +440 -0
- package/src/components/member-outputs.test.ts +76 -0
- package/src/components/placeholder-preview.test.ts +147 -0
- package/src/components/placeholder-preview.ts +257 -0
- package/src/components/pr-loop-compose.tofu.e2e.test.ts +363 -0
- package/src/components/pr-loop.tofu.test.ts +112 -56
- package/src/compose/apply-events.test.ts +62 -0
- package/src/compose/apply-events.ts +100 -0
- package/src/compose/estate.choudoufu.e2e.test.ts +112 -0
- package/src/compose/estate.test.ts +421 -0
- package/src/compose/estate.ts +443 -0
- package/src/compose/hcl.ts +311 -0
- package/src/compose/module-address.ts +49 -0
- package/src/compose/root.test.ts +416 -0
- package/src/compose/root.ts +592 -0
- package/src/compose/run.test.ts +440 -0
- package/src/compose/run.tofu.test.ts +176 -0
- package/src/compose/run.ts +527 -0
- package/src/compose/slice.test.ts +184 -0
- package/src/compose/slice.ts +290 -0
- package/src/compose/state-address.ts +157 -0
- package/src/compose/state-merge.test.ts +131 -0
- package/src/compose/state-merge.ts +148 -0
- package/src/compose.test.ts +248 -0
- package/src/compose.ts +627 -0
- package/src/composites/terraform-apply-op.test.ts +9 -0
- package/src/composites/terraform-apply-op.ts +7 -3
- package/src/config-file-listers.test.ts +265 -0
- package/src/config.ts +44 -9
- package/src/describe-resources.engine-facts.test.ts +412 -0
- package/src/describe-resources.live-module-keyed.test.ts +290 -0
- package/src/describe-resources.ts +416 -70
- package/src/drift-read.ts +284 -0
- package/src/drift-record.test.ts +223 -0
- package/src/drift-record.ts +455 -0
- package/src/hcl/config-blocks.test.ts +174 -0
- package/src/hcl/edges.ts +21 -1
- package/src/hcl/files.ts +234 -0
- package/src/hcl/instance-forms.test.ts +94 -0
- package/src/hcl/instances.test.ts +137 -0
- package/src/hcl/instances.ts +422 -0
- package/src/hcl/parse.test.ts +139 -1
- package/src/hcl/parse.ts +149 -16
- package/src/hcl/roots.ts +6 -0
- package/src/hcl/suppressions.ts +8 -2
- package/src/index.ts +76 -0
- package/src/live-plan-reasons.ts +54 -0
- package/src/lsp/completions.test.ts +1 -1
- package/src/lsp/option-keys.ts +2 -0
- package/src/op/activities/choudoufu-plan-set-op.test.ts +111 -0
- package/src/op/activities/index.ts +9 -0
- package/src/op/activities/live-detect.ts +55 -26
- package/src/op/activities/pin-rollout.ts +3 -0
- package/src/op/activities/registry.test.ts +2 -1
- package/src/op/activities/terraform.test.ts +55 -0
- package/src/op/activities/terraform.ts +266 -22
- package/src/op/activity-contracts.test.ts +1 -0
- package/src/op/activity-contracts.ts +24 -1
- package/src/op/builders.ts +14 -0
- package/src/pin/bundle.test.ts +2 -0
- package/src/pin/edit.ts +30 -0
- package/src/pin/rollout.ts +16 -11
- package/src/plan-shape.test.ts +148 -0
- package/src/plan-shape.ts +128 -0
- package/src/plugin.ts +33 -19
- package/src/report.test.ts +82 -1
- package/src/report.ts +31 -54
- package/src/signal-only-root.test.ts +72 -0
- package/src/skills/chant-terraform.md +1 -1
- package/src/state/azure-blob.ts +223 -0
- package/src/state/backend-verdict.test.ts +187 -0
- package/src/state/backend.test.ts +127 -0
- package/src/state/backend.ts +488 -0
- package/src/state/exec.ts +111 -0
- package/src/state/gcs.ts +254 -0
- package/src/state/index.ts +15 -0
- package/src/state/locks.test.ts +84 -0
- package/src/state/locks.ts +201 -0
- package/src/state/object-store.ts +35 -0
- package/src/state/root-states-floci.e2e.test.ts +66 -0
- package/src/state/root-states.test.ts +462 -0
- package/src/state/root-states.ts +309 -0
- package/src/state/s3.ts +193 -0
- package/src/state/store.ts +225 -0
- package/src/terragrunt/affected.ts +2 -1
- package/src/workspace-kinds.test.ts +4 -17
- package/workspace-kinds.json +1 -24
- package/dist/workspace-member.d.ts +0 -30
- package/dist/workspace-member.d.ts.map +0 -1
- package/dist/workspace-member.js +0 -91
- package/schemas/pulumi.schema.json +0 -34
- package/src/testdata/pulumi-member/Pulumi.yaml +0 -2
- package/src/testdata/pulumi-member/stack-export.json +0 -53
- package/src/testdata/pulumi-unexported/Pulumi.yaml +0 -2
- package/src/workspace-member.test.ts +0 -89
- package/src/workspace-member.ts +0 -126
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Cloud Storage for a gcs backend's state, over its JSON API with
|
|
3
|
+
* `fetch` and `node:crypto`: an object's text and generation, a write, a
|
|
4
|
+
* write only when there is no object (the lock), and a delete.
|
|
5
|
+
*
|
|
6
|
+
* The identity, first match wins: a bearer token (`access_token`, or
|
|
7
|
+
* GOOGLE_OAUTH_ACCESS_TOKEN), else a credentials file or its JSON:
|
|
8
|
+
*
|
|
9
|
+
* external_account the job's OIDC token, traded at Google's STS, then the
|
|
10
|
+
* service account it impersonates.
|
|
11
|
+
* service_account a key: requests carry a self-signed JWT.
|
|
12
|
+
*
|
|
13
|
+
* With `impersonate_service_account`, every request carries that service
|
|
14
|
+
* account's token, which the identity asks IAM for.
|
|
15
|
+
*
|
|
16
|
+
* Lifted from terragucci's `report/gcs.ts` (c493e0e), without signed URLs,
|
|
17
|
+
* version listing and the re-plan's traded tokens.
|
|
18
|
+
*/
|
|
19
|
+
import { createSign } from "node:crypto";
|
|
20
|
+
import { readFileSync } from "node:fs";
|
|
21
|
+
import { defaultFetch, encodeStrict, StoreConflict, StoreError } from "./object-store.js";
|
|
22
|
+
export const GCS_ENDPOINT = "https://storage.googleapis.com";
|
|
23
|
+
/** IAM Credentials, where a token for an impersonated service account comes from. */
|
|
24
|
+
export const IAM_CREDENTIALS = "https://iamcredentials.googleapis.com";
|
|
25
|
+
const CLOUD_PLATFORM = "https://www.googleapis.com/auth/cloud-platform";
|
|
26
|
+
const READ_WRITE = "https://www.googleapis.com/auth/devstorage.read_write";
|
|
27
|
+
const b64url = (b) => Buffer.from(b).toString("base64url");
|
|
28
|
+
/** A JWT the key signs for itself, which Google's APIs take as an access token for the scope it names. */
|
|
29
|
+
export function selfSignedJwt(key, now) {
|
|
30
|
+
const iat = Math.floor(now.getTime() / 1000);
|
|
31
|
+
const head = b64url(JSON.stringify({ alg: "RS256", typ: "JWT", ...(key.private_key_id ? { kid: key.private_key_id } : {}) }));
|
|
32
|
+
const body = b64url(JSON.stringify({ iss: key.client_email, sub: key.client_email, scope: READ_WRITE, iat, exp: iat + 3600 }));
|
|
33
|
+
return `${head}.${body}.${createSign("RSA-SHA256").update(`${head}.${body}`).sign(key.private_key).toString("base64url")}`;
|
|
34
|
+
}
|
|
35
|
+
/** Google's error message, or the start of the body. */
|
|
36
|
+
const why = (text) => {
|
|
37
|
+
try {
|
|
38
|
+
const j = JSON.parse(text);
|
|
39
|
+
if (typeof j.error === "object" && j.error?.message)
|
|
40
|
+
return j.error.message;
|
|
41
|
+
if (typeof j.error === "string")
|
|
42
|
+
return [j.error, j.error_description].filter(Boolean).join(": ");
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Not JSON.
|
|
46
|
+
}
|
|
47
|
+
return text.slice(0, 300);
|
|
48
|
+
};
|
|
49
|
+
export class GcsClient {
|
|
50
|
+
constructor(target, fetchFn = defaultFetch, readFile = (f) => readFileSync(f, "utf-8")) {
|
|
51
|
+
this.target = target;
|
|
52
|
+
this.fetchFn = fetchFn;
|
|
53
|
+
this.readFile = readFile;
|
|
54
|
+
}
|
|
55
|
+
where(key) {
|
|
56
|
+
return `gs://${this.target.bucket}/${key}`;
|
|
57
|
+
}
|
|
58
|
+
credentials() {
|
|
59
|
+
if (this.creds)
|
|
60
|
+
return this.creds;
|
|
61
|
+
const source = this.target.credentialsFile ?? "the backend's credentials";
|
|
62
|
+
let c;
|
|
63
|
+
try {
|
|
64
|
+
c = JSON.parse(this.target.credentialsJson ?? this.readFile(this.target.credentialsFile ?? ""));
|
|
65
|
+
}
|
|
66
|
+
catch (e) {
|
|
67
|
+
throw new StoreError(`cannot read the credentials in ${source}: ${e.message}`);
|
|
68
|
+
}
|
|
69
|
+
if (c.type === "external_account" && c.token_url && c.audience && c.credential_source && (c.credential_source.file || c.credential_source.url))
|
|
70
|
+
return (this.creds = c);
|
|
71
|
+
if (c.type === "service_account" && c.client_email && c.private_key)
|
|
72
|
+
return (this.creds = c);
|
|
73
|
+
throw new StoreError(`${source} is not an external_account file with a credential_source file or url, nor a service_account key`);
|
|
74
|
+
}
|
|
75
|
+
async post(url, headers, body, what) {
|
|
76
|
+
const res = await this.fetchFn(url, { method: "POST", headers, body });
|
|
77
|
+
const text = await res.text();
|
|
78
|
+
if (!res.ok)
|
|
79
|
+
throw new StoreError(`${what}: ${res.status} ${why(text)}`);
|
|
80
|
+
try {
|
|
81
|
+
return JSON.parse(text);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
throw new StoreError(`${what}: the answer is not JSON`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** The job's OIDC token, from the file or URL the credentials name. */
|
|
88
|
+
async subjectToken(c) {
|
|
89
|
+
const src = c.credential_source;
|
|
90
|
+
let text;
|
|
91
|
+
if (src.file) {
|
|
92
|
+
try {
|
|
93
|
+
text = this.readFile(src.file);
|
|
94
|
+
}
|
|
95
|
+
catch (e) {
|
|
96
|
+
throw new StoreError(`cannot read the OIDC token in ${src.file}: ${e.message}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
const res = await this.fetchFn(src.url, { method: "GET", headers: src.headers ?? {} });
|
|
101
|
+
text = await res.text();
|
|
102
|
+
if (!res.ok)
|
|
103
|
+
throw new StoreError(`the OIDC token at ${src.url.split("?")[0]}: ${res.status} ${why(text)}`);
|
|
104
|
+
}
|
|
105
|
+
const field = src.format?.type === "json" ? src.format.subject_token_field_name : undefined;
|
|
106
|
+
let token = text.trim();
|
|
107
|
+
if (field) {
|
|
108
|
+
try {
|
|
109
|
+
token = String(JSON.parse(text)[field] ?? "");
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
throw new StoreError(`the job's OIDC token is not JSON with ${field}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (!token)
|
|
116
|
+
throw new StoreError("the job's OIDC token is empty");
|
|
117
|
+
return token;
|
|
118
|
+
}
|
|
119
|
+
async impersonateWith(url, bearer, sa) {
|
|
120
|
+
const r = await this.post(url, { "content-type": "application/json", authorization: `Bearer ${bearer}` }, JSON.stringify({ scope: [CLOUD_PLATFORM], lifetime: "3600s" }), `impersonating ${sa}`);
|
|
121
|
+
if (typeof r.accessToken !== "string")
|
|
122
|
+
throw new StoreError(`IAM answered the impersonation of ${sa} without a token`);
|
|
123
|
+
const at = typeof r.expireTime === "string" ? Date.parse(r.expireTime) : NaN;
|
|
124
|
+
return { token: r.accessToken, expires: Number.isNaN(at) ? Date.now() + 3600000 : at };
|
|
125
|
+
}
|
|
126
|
+
/** STS's federated token, then the impersonated service account's, when the credentials name one. */
|
|
127
|
+
async exchange(c) {
|
|
128
|
+
const sts = await this.post(c.token_url, { "content-type": "application/x-www-form-urlencoded" }, new URLSearchParams({
|
|
129
|
+
grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
|
|
130
|
+
audience: c.audience,
|
|
131
|
+
scope: CLOUD_PLATFORM,
|
|
132
|
+
requested_token_type: "urn:ietf:params:oauth:token-type:access_token",
|
|
133
|
+
subject_token: await this.subjectToken(c),
|
|
134
|
+
subject_token_type: c.subject_token_type,
|
|
135
|
+
}).toString(), `Google STS refused the OIDC token for ${c.audience}`);
|
|
136
|
+
if (typeof sts.access_token !== "string")
|
|
137
|
+
throw new StoreError("Google STS answered without a token");
|
|
138
|
+
const fed = { token: sts.access_token, expires: Date.now() + Number(sts.expires_in ?? 3600) * 1000 };
|
|
139
|
+
if (!c.service_account_impersonation_url)
|
|
140
|
+
return fed;
|
|
141
|
+
return this.impersonateWith(c.service_account_impersonation_url, fed.token, "the service account");
|
|
142
|
+
}
|
|
143
|
+
/** The identity's own token: the one the target gives, a key's self-signed JWT, or STS's for the OIDC token. */
|
|
144
|
+
async own() {
|
|
145
|
+
if (this.target.accessToken)
|
|
146
|
+
return { token: this.target.accessToken, expires: Date.now() + 3600000 };
|
|
147
|
+
const c = this.credentials();
|
|
148
|
+
return c.type === "service_account" ? { token: selfSignedJwt(c, new Date()), expires: Date.now() + 3600000 } : this.exchange(c);
|
|
149
|
+
}
|
|
150
|
+
/** An access token: once per client, again a minute before it expires. */
|
|
151
|
+
async bearer() {
|
|
152
|
+
const sa = this.target.impersonate;
|
|
153
|
+
const fresh = async () => {
|
|
154
|
+
if (!sa)
|
|
155
|
+
return this.own();
|
|
156
|
+
const base = await this.own();
|
|
157
|
+
return this.impersonateWith(`${(this.target.iamEndpoint ?? IAM_CREDENTIALS).replace(/\/+$/, "")}/v1/projects/-/serviceAccounts/${encodeURIComponent(sa)}:generateAccessToken`, base.token, sa);
|
|
158
|
+
};
|
|
159
|
+
const again = () => (this.token = fresh().catch((e) => {
|
|
160
|
+
this.token = undefined;
|
|
161
|
+
throw e;
|
|
162
|
+
}));
|
|
163
|
+
let held = await (this.token ?? again());
|
|
164
|
+
if (held.expires - Date.now() < 60000)
|
|
165
|
+
held = await again();
|
|
166
|
+
return held.token;
|
|
167
|
+
}
|
|
168
|
+
objectUrl(key) {
|
|
169
|
+
return `${this.target.endpoint}/storage/v1/b/${encodeURIComponent(this.target.bucket)}/o/${encodeStrict(key)}`;
|
|
170
|
+
}
|
|
171
|
+
/** Write an object; with `ifGenerationMatch`, only when the object still has that generation (`0`: there is none), else StoreConflict. */
|
|
172
|
+
async put(key, body, contentType, ifGenerationMatch) {
|
|
173
|
+
const t = this.target;
|
|
174
|
+
const url = `${t.endpoint}/upload/storage/v1/b/${encodeURIComponent(t.bucket)}/o?uploadType=media&name=${encodeStrict(key)}${ifGenerationMatch !== undefined ? `&ifGenerationMatch=${encodeStrict(ifGenerationMatch)}` : ""}`;
|
|
175
|
+
const res = await this.fetchFn(url, { method: "POST", headers: { "content-type": contentType, authorization: `Bearer ${await this.bearer()}` }, body });
|
|
176
|
+
if (res.status === 412)
|
|
177
|
+
throw new StoreConflict(`PUT ${this.where(key)}: 412, it changed since it was read`);
|
|
178
|
+
if (!res.ok)
|
|
179
|
+
throw new StoreError(`PUT ${this.where(key)}: ${res.status} ${why(await res.text())}`);
|
|
180
|
+
}
|
|
181
|
+
async get(key) {
|
|
182
|
+
const res = await this.fetchFn(`${this.objectUrl(key)}?alt=media`, { method: "GET", headers: { authorization: `Bearer ${await this.bearer()}` } });
|
|
183
|
+
if (res.status === 404)
|
|
184
|
+
return undefined;
|
|
185
|
+
if (!res.ok)
|
|
186
|
+
throw new StoreError(`GET ${this.where(key)}: ${res.status} ${why(await res.text())}`);
|
|
187
|
+
return res.text();
|
|
188
|
+
}
|
|
189
|
+
/** An object's metadata and never its body: whether it is there, and its generation. */
|
|
190
|
+
async head(key) {
|
|
191
|
+
const res = await this.fetchFn(this.objectUrl(key), { method: "GET", headers: { authorization: `Bearer ${await this.bearer()}` } });
|
|
192
|
+
const text = await res.text();
|
|
193
|
+
if (res.status === 404)
|
|
194
|
+
return { exists: false };
|
|
195
|
+
if (!res.ok)
|
|
196
|
+
throw new StoreError(`GET ${this.where(key)} metadata: ${res.status} ${why(text)}`);
|
|
197
|
+
let g;
|
|
198
|
+
try {
|
|
199
|
+
g = JSON.parse(text).generation;
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
// Metadata that is not JSON: no generation.
|
|
203
|
+
}
|
|
204
|
+
return { exists: true, ...(g !== undefined && g !== null && g !== "" ? { generation: String(g) } : {}) };
|
|
205
|
+
}
|
|
206
|
+
/** Write an object only when there is none (ifGenerationMatch=0): true when this write made it, false when one was there. */
|
|
207
|
+
async putIfAbsent(key, body, contentType) {
|
|
208
|
+
try {
|
|
209
|
+
await this.put(key, body, contentType, "0");
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
catch (e) {
|
|
213
|
+
if (e instanceof StoreConflict)
|
|
214
|
+
return false;
|
|
215
|
+
throw e;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/** Delete an object's live generation. One that is not there is deleted already. */
|
|
219
|
+
async remove(key) {
|
|
220
|
+
const res = await this.fetchFn(this.objectUrl(key), { method: "DELETE", headers: { authorization: `Bearer ${await this.bearer()}` } });
|
|
221
|
+
if (!res.ok && res.status !== 404)
|
|
222
|
+
throw new StoreError(`DELETE ${this.where(key)}: ${res.status} ${why(await res.text())}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A root's state for a composed run (#4437): where it is, its lock, its pull
|
|
3
|
+
* and its write, and the unwritten keep. Plain lexicon code, lifted from
|
|
4
|
+
* terragucci at c493e0e.
|
|
5
|
+
*/
|
|
6
|
+
export * from "./object-store.js";
|
|
7
|
+
export * from "./backend.js";
|
|
8
|
+
export * from "./store.js";
|
|
9
|
+
export * from "./exec.js";
|
|
10
|
+
export * from "./locks.js";
|
|
11
|
+
export * from "./root-states.js";
|
|
12
|
+
export { S3Client, s3FromEnv, type S3Target } from "./s3.js";
|
|
13
|
+
export { GcsClient, type GcsTarget } from "./gcs.js";
|
|
14
|
+
export { AzureBlobClient, type AzureTarget } from "./azure-blob.js";
|
|
15
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/state/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,cAAc,gBAAgB,CAAC;AAC/B,cAAc,WAAW,CAAC;AAC1B,cAAc,SAAS,CAAC;AACxB,cAAc,QAAQ,CAAC;AACvB,cAAc,SAAS,CAAC;AACxB,cAAc,eAAe,CAAC;AAC9B,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,QAAQ,EAAE,MAAM,MAAM,CAAC;AAC1D,OAAO,EAAE,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAClD,OAAO,EAAE,eAAe,EAAE,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A root's state for a composed run (#4437): where it is, its lock, its pull
|
|
3
|
+
* and its write, and the unwritten keep. Plain lexicon code, lifted from
|
|
4
|
+
* terragucci at c493e0e.
|
|
5
|
+
*/
|
|
6
|
+
export * from "./object-store.js";
|
|
7
|
+
export * from "./backend.js";
|
|
8
|
+
export * from "./store.js";
|
|
9
|
+
export * from "./exec.js";
|
|
10
|
+
export * from "./locks.js";
|
|
11
|
+
export * from "./root-states.js";
|
|
12
|
+
export { S3Client, s3FromEnv } from "./s3.js";
|
|
13
|
+
export { GcsClient } from "./gcs.js";
|
|
14
|
+
export { AzureBlobClient } from "./azure-blob.js";
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { LockHolder, StateStore } from "./store.js";
|
|
2
|
+
/** What a lock says holds it: the operation and a line of info, as the binary prints them when it finds the lock held. */
|
|
3
|
+
export interface LockOperation {
|
|
4
|
+
operation: string;
|
|
5
|
+
info: string;
|
|
6
|
+
}
|
|
7
|
+
/** The lock info a run writes, in the shape the binary writes and prints when it finds a lock held. */
|
|
8
|
+
export declare function lockInfo(path: string, now: string, op: LockOperation, who?: string): string;
|
|
9
|
+
/** Who holds a lock, from the lock info it left: what is there of it. */
|
|
10
|
+
export interface LockHolderInfo {
|
|
11
|
+
id: string;
|
|
12
|
+
who?: string;
|
|
13
|
+
operation?: string;
|
|
14
|
+
info?: string;
|
|
15
|
+
created?: string;
|
|
16
|
+
}
|
|
17
|
+
/** The holder a lock info names. */
|
|
18
|
+
export declare function holderOf(held: LockHolder): LockHolderInfo;
|
|
19
|
+
/** A lock held by another run stopped the take; nothing was pulled. */
|
|
20
|
+
export declare class StateLockedError extends Error {
|
|
21
|
+
readonly root: string;
|
|
22
|
+
readonly lockLocation: string;
|
|
23
|
+
readonly holder?: LockHolderInfo | undefined;
|
|
24
|
+
constructor(root: string, lockLocation: string, holder?: LockHolderInfo | undefined);
|
|
25
|
+
}
|
|
26
|
+
/** What taking a lock needs of a store: a caller with a store of its own (terragucci's, which also reads versions) passes that. */
|
|
27
|
+
export type LockStore = Pick<StateStore, "location" | "lockLocation" | "heldLock" | "lock" | "unlock" | "adopt">;
|
|
28
|
+
/** A lock taken, on one root's state. */
|
|
29
|
+
export interface HeldLock<S extends LockStore = StateStore> {
|
|
30
|
+
root: string;
|
|
31
|
+
store: S;
|
|
32
|
+
/** The lock was another run's, kept on purpose with a state it could not write, and this take took it over. */
|
|
33
|
+
adopted: boolean;
|
|
34
|
+
release: () => Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Take the lock of each state, in the order given. Releases what it took and
|
|
38
|
+
* throws StateLockedError when one is held and `adopt` does not take it over.
|
|
39
|
+
* `adopt` sees a held lock and says whether this take takes it over (a lock
|
|
40
|
+
* an earlier run of the same operation left held on purpose).
|
|
41
|
+
*/
|
|
42
|
+
export declare function takeLocks<S extends LockStore = StateStore>(stores: readonly {
|
|
43
|
+
root: string;
|
|
44
|
+
store: S;
|
|
45
|
+
}[], now: string, op: LockOperation, adopt?: (root: string, store: S, held: LockHolder) => Promise<boolean>, who?: string): Promise<HeldLock<S>[]>;
|
|
46
|
+
/**
|
|
47
|
+
* The lock of a workspace a state is read from, taken through the
|
|
48
|
+
* workspace's own API (Terraform Cloud's `actions/lock`), not a lock object
|
|
49
|
+
* beside a state. `lock` takes it with a reason the workspace shows, and
|
|
50
|
+
* returns its release; it throws when another run holds it.
|
|
51
|
+
*/
|
|
52
|
+
export interface SourceLock {
|
|
53
|
+
root: string;
|
|
54
|
+
/** The workspace, for messages. */
|
|
55
|
+
location: string;
|
|
56
|
+
lock(reason: string): Promise<() => Promise<void>>;
|
|
57
|
+
}
|
|
58
|
+
/** A source workspace's lock, taken. */
|
|
59
|
+
export interface HeldSourceLock {
|
|
60
|
+
root: string;
|
|
61
|
+
location: string;
|
|
62
|
+
release: () => Promise<void>;
|
|
63
|
+
}
|
|
64
|
+
/** A source workspace's lock could not be taken: another run holds it, or the API refused. Nothing was read. */
|
|
65
|
+
export declare class SourceLockedError extends Error {
|
|
66
|
+
readonly root: string;
|
|
67
|
+
readonly location: string;
|
|
68
|
+
readonly reason: string;
|
|
69
|
+
constructor(root: string, location: string, reason: string);
|
|
70
|
+
}
|
|
71
|
+
/** The reason a source workspace's lock shows: the operation and its info, as a state lock's info names them. */
|
|
72
|
+
export declare const sourceLockReason: (op: LockOperation) => string;
|
|
73
|
+
/**
|
|
74
|
+
* Take each source workspace's lock, in the order given, so no run writes a
|
|
75
|
+
* state while it is read. Releases what it took and throws SourceLockedError
|
|
76
|
+
* when one cannot be taken.
|
|
77
|
+
*/
|
|
78
|
+
export declare function takeSourceLocks(sources: readonly SourceLock[], op: LockOperation): Promise<HeldSourceLock[]>;
|
|
79
|
+
export interface LocksWithSourcesInput<S extends LockStore> {
|
|
80
|
+
/** The workspaces states are read from, locked first. */
|
|
81
|
+
sources: readonly SourceLock[];
|
|
82
|
+
/** The states written, locked after the sources, in the order given. */
|
|
83
|
+
stores: readonly {
|
|
84
|
+
root: string;
|
|
85
|
+
store: S;
|
|
86
|
+
}[];
|
|
87
|
+
now: string;
|
|
88
|
+
/** What the state locks name. */
|
|
89
|
+
op: LockOperation;
|
|
90
|
+
/** What a source workspace's lock names; default `op`. A run that only reads the source says so here. */
|
|
91
|
+
sourceOp?: LockOperation;
|
|
92
|
+
adopt?: (root: string, store: S, held: LockHolder) => Promise<boolean>;
|
|
93
|
+
who?: string;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Every lock a run that reads from source workspaces and writes states
|
|
97
|
+
* holds: the source workspaces' first, then the states'. When any cannot be
|
|
98
|
+
* taken, every lock taken so far is released, and the error is
|
|
99
|
+
* SourceLockedError or StateLockedError.
|
|
100
|
+
*/
|
|
101
|
+
export declare function takeLocksWithSources<S extends LockStore = StateStore>(input: LocksWithSourcesInput<S>): Promise<{
|
|
102
|
+
sources: HeldSourceLock[];
|
|
103
|
+
locks: HeldLock<S>[];
|
|
104
|
+
}>;
|
|
105
|
+
//# sourceMappingURL=locks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"locks.d.ts","sourceRoot":"","sources":["../../src/state/locks.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Taking a set of state locks in one order, each the way its backend takes
|
|
3
|
+
* it, with lock info that names the operation and the run. A lock another
|
|
4
|
+
* run holds stops the take: every lock taken so far is released, and the
|
|
5
|
+
* error names the holder from the lock info it left.
|
|
6
|
+
*
|
|
7
|
+
* A run can also hold the lock of a workspace a state is read from, which
|
|
8
|
+
* no backend block of the root names (a Terraform Cloud or Enterprise
|
|
9
|
+
* workspace a state migration moves from, terragucci#1274): the workspace's
|
|
10
|
+
* own lock, taken through its API before any state lock (`takeSourceLocks`,
|
|
11
|
+
* `takeLocksWithSources`).
|
|
12
|
+
*
|
|
13
|
+
* Lifted from terragucci's `migrate.ts` (c493e0e): `lockInfo`,
|
|
14
|
+
* `LockOperation`, `HeldLock` and `takeLocks`, and the source-workspace lock
|
|
15
|
+
* of its `takeLocks` (lines 1045 to 1056).
|
|
16
|
+
*/
|
|
17
|
+
import { randomUUID } from "node:crypto";
|
|
18
|
+
import { hostname } from "node:os";
|
|
19
|
+
/** The lock info a run writes, in the shape the binary writes and prints when it finds a lock held. */
|
|
20
|
+
export function lockInfo(path, now, op, who = `chant@${hostname()}`) {
|
|
21
|
+
return JSON.stringify({ ID: randomUUID(), Operation: op.operation, Info: op.info, Who: who, Version: "", Created: now, Path: path });
|
|
22
|
+
}
|
|
23
|
+
/** The holder a lock info names. */
|
|
24
|
+
export function holderOf(held) {
|
|
25
|
+
let v = {};
|
|
26
|
+
try {
|
|
27
|
+
v = JSON.parse(held.info);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// Lock info that is not JSON names no one.
|
|
31
|
+
}
|
|
32
|
+
const s = (k) => (typeof v[k] === "string" && v[k] !== "" ? v[k] : undefined);
|
|
33
|
+
return { id: held.id, ...(s("Who") ? { who: s("Who") } : {}), ...(s("Operation") ? { operation: s("Operation") } : {}), ...(s("Info") ? { info: s("Info") } : {}), ...(s("Created") ? { created: s("Created") } : {}) };
|
|
34
|
+
}
|
|
35
|
+
/** A lock held by another run stopped the take; nothing was pulled. */
|
|
36
|
+
export class StateLockedError extends Error {
|
|
37
|
+
constructor(root, lockLocation, holder) {
|
|
38
|
+
const by = holder ? [holder.who && `by ${holder.who}`, holder.operation, holder.info && `(${holder.info})`, holder.created && `since ${holder.created}`].filter(Boolean).join(" ") : "";
|
|
39
|
+
super(`${root}: its state is locked at ${lockLocation}${by ? ` ${by}` : ""}, so nothing was pulled`);
|
|
40
|
+
this.root = root;
|
|
41
|
+
this.lockLocation = lockLocation;
|
|
42
|
+
this.holder = holder;
|
|
43
|
+
this.name = "StateLockedError";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Take the lock of each state, in the order given. Releases what it took and
|
|
48
|
+
* throws StateLockedError when one is held and `adopt` does not take it over.
|
|
49
|
+
* `adopt` sees a held lock and says whether this take takes it over (a lock
|
|
50
|
+
* an earlier run of the same operation left held on purpose).
|
|
51
|
+
*/
|
|
52
|
+
export async function takeLocks(stores, now, op, adopt, who) {
|
|
53
|
+
const held = [];
|
|
54
|
+
const releaseAll = async () => {
|
|
55
|
+
for (const h of held.reverse())
|
|
56
|
+
await h.release().catch(() => { });
|
|
57
|
+
};
|
|
58
|
+
for (const { root, store } of stores) {
|
|
59
|
+
let got;
|
|
60
|
+
try {
|
|
61
|
+
got = await store.lock(lockInfo(store.location.replace(/^[a-z0-9]+:\/\//, ""), now, op, who));
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
await releaseAll();
|
|
65
|
+
throw new Error(`${root}: the lock ${store.lockLocation} could not be taken: ${e.message}`);
|
|
66
|
+
}
|
|
67
|
+
if (got) {
|
|
68
|
+
held.push({ root, store, adopted: false, release: () => store.unlock() });
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const info = await store.heldLock().catch(() => undefined);
|
|
72
|
+
if (info && adopt && (await adopt(root, store, info).catch(() => false))) {
|
|
73
|
+
store.adopt(info);
|
|
74
|
+
held.push({ root, store, adopted: true, release: () => store.unlock() });
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
await releaseAll();
|
|
78
|
+
throw new StateLockedError(root, store.lockLocation, info ? holderOf(info) : undefined);
|
|
79
|
+
}
|
|
80
|
+
return held;
|
|
81
|
+
}
|
|
82
|
+
/** A source workspace's lock could not be taken: another run holds it, or the API refused. Nothing was read. */
|
|
83
|
+
export class SourceLockedError extends Error {
|
|
84
|
+
constructor(root, location, reason) {
|
|
85
|
+
super(`${root}: the lock of the workspace it is read from (${location}) could not be taken: ${reason}`);
|
|
86
|
+
this.root = root;
|
|
87
|
+
this.location = location;
|
|
88
|
+
this.reason = reason;
|
|
89
|
+
this.name = "SourceLockedError";
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** The reason a source workspace's lock shows: the operation and its info, as a state lock's info names them. */
|
|
93
|
+
export const sourceLockReason = (op) => `${op.operation}: ${op.info}`;
|
|
94
|
+
/**
|
|
95
|
+
* Take each source workspace's lock, in the order given, so no run writes a
|
|
96
|
+
* state while it is read. Releases what it took and throws SourceLockedError
|
|
97
|
+
* when one cannot be taken.
|
|
98
|
+
*/
|
|
99
|
+
export async function takeSourceLocks(sources, op) {
|
|
100
|
+
const held = [];
|
|
101
|
+
for (const s of sources) {
|
|
102
|
+
try {
|
|
103
|
+
held.push({ root: s.root, location: s.location, release: await s.lock(sourceLockReason(op)) });
|
|
104
|
+
}
|
|
105
|
+
catch (e) {
|
|
106
|
+
for (const h of held.reverse())
|
|
107
|
+
await h.release().catch(() => { });
|
|
108
|
+
throw new SourceLockedError(s.root, s.location, e.message);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return held;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Every lock a run that reads from source workspaces and writes states
|
|
115
|
+
* holds: the source workspaces' first, then the states'. When any cannot be
|
|
116
|
+
* taken, every lock taken so far is released, and the error is
|
|
117
|
+
* SourceLockedError or StateLockedError.
|
|
118
|
+
*/
|
|
119
|
+
export async function takeLocksWithSources(input) {
|
|
120
|
+
const sources = await takeSourceLocks(input.sources, input.sourceOp ?? input.op);
|
|
121
|
+
try {
|
|
122
|
+
const locks = await takeLocks(input.stores, input.now, input.op, input.adopt, input.who);
|
|
123
|
+
return { sources, locks };
|
|
124
|
+
}
|
|
125
|
+
catch (e) {
|
|
126
|
+
for (const h of sources.reverse())
|
|
127
|
+
await h.release().catch(() => { });
|
|
128
|
+
throw e;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the state stores share: the fetch they call, their errors, and how a
|
|
3
|
+
* key goes into a URL. Lifted from terragucci's `report/object-store.ts`
|
|
4
|
+
* (c493e0e), cut to what a state's lock and write need.
|
|
5
|
+
*/
|
|
6
|
+
export type StoreFetch = (url: string, init: {
|
|
7
|
+
method: string;
|
|
8
|
+
headers: Record<string, string>;
|
|
9
|
+
body?: string | Uint8Array;
|
|
10
|
+
}) => Promise<{
|
|
11
|
+
ok: boolean;
|
|
12
|
+
status: number;
|
|
13
|
+
text(): Promise<string>;
|
|
14
|
+
/** A store that sends no headers (or a test's fake) gets no conditional writes. */
|
|
15
|
+
headers?: {
|
|
16
|
+
get(name: string): string | null;
|
|
17
|
+
};
|
|
18
|
+
}>;
|
|
19
|
+
export declare class StoreError extends Error {
|
|
20
|
+
}
|
|
21
|
+
/** A conditional write that lost: the object changed (412) or another write to it was in flight (409). */
|
|
22
|
+
export declare class StoreConflict extends StoreError {
|
|
23
|
+
}
|
|
24
|
+
/** A condition on a write: the version the object must still have, or `*` for "there is no object yet". */
|
|
25
|
+
export type StoreCondition = {
|
|
26
|
+
ifMatch: string;
|
|
27
|
+
} | {
|
|
28
|
+
ifNoneMatch: "*";
|
|
29
|
+
};
|
|
30
|
+
/** A key as a URL path: each segment percent-encoded the strict way (RFC 3986), the slashes kept. */
|
|
31
|
+
export declare const encodePath: (key: string) => string;
|
|
32
|
+
export declare const encodeStrict: (v: string) => string;
|
|
33
|
+
/** The first value of an XML element, unescaped. Enough for the small answers these stores send. */
|
|
34
|
+
export declare const xmlText: (xml: string, tag: string) => string | undefined;
|
|
35
|
+
/** The global fetch, as a StoreFetch. */
|
|
36
|
+
export declare const defaultFetch: StoreFetch;
|
|
37
|
+
//# sourceMappingURL=object-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"object-store.d.ts","sourceRoot":"","sources":["../../src/state/object-store.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,MAAM,UAAU,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,GAAG,UAAU,CAAA;CAAE,KAAK,OAAO,CAAC;IACvI,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IACxB,mFAAmF;IACnF,OAAO,CAAC,EAAE;QAAE,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;CAChD,CAAC,CAAC;AAEH,qBAAa,UAAW,SAAQ,KAAK;CAAG;AAExC,0GAA0G;AAC1G,qBAAa,aAAc,SAAQ,UAAU;CAAG;AAEhD,2GAA2G;AAC3G,MAAM,MAAM,cAAc,GAAG;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,WAAW,EAAE,GAAG,CAAA;CAAE,CAAC;AAExE,qGAAqG;AACrG,eAAO,MAAM,UAAU,QAAS,MAAM,KAAG,MAAoD,CAAC;AAE9F,eAAO,MAAM,YAAY,MAAO,MAAM,KAAG,MAA4G,CAAC;AAEtJ,oGAAoG;AACpG,eAAO,MAAM,OAAO,QAAS,MAAM,OAAO,MAAM,KAAG,MAAM,GAAG,SAG3D,CAAC;AAEF,yCAAyC;AACzC,eAAO,MAAM,YAAY,EAAE,UAA2D,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the state stores share: the fetch they call, their errors, and how a
|
|
3
|
+
* key goes into a URL. Lifted from terragucci's `report/object-store.ts`
|
|
4
|
+
* (c493e0e), cut to what a state's lock and write need.
|
|
5
|
+
*/
|
|
6
|
+
export class StoreError extends Error {
|
|
7
|
+
}
|
|
8
|
+
/** A conditional write that lost: the object changed (412) or another write to it was in flight (409). */
|
|
9
|
+
export class StoreConflict extends StoreError {
|
|
10
|
+
}
|
|
11
|
+
/** A key as a URL path: each segment percent-encoded the strict way (RFC 3986), the slashes kept. */
|
|
12
|
+
export const encodePath = (key) => key.split("/").map(encodeStrict).join("/");
|
|
13
|
+
export const encodeStrict = (v) => encodeURIComponent(v).replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
14
|
+
/** The first value of an XML element, unescaped. Enough for the small answers these stores send. */
|
|
15
|
+
export const xmlText = (xml, tag) => {
|
|
16
|
+
const m = new RegExp(`<${tag}>([^<]*)</${tag}>`).exec(xml);
|
|
17
|
+
return m?.[1].replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&").trim();
|
|
18
|
+
};
|
|
19
|
+
/** The global fetch, as a StoreFetch. */
|
|
20
|
+
export const defaultFetch = (url, init) => fetch(url, init);
|