@intentius/chant-lexicon-terraform 0.124.54 → 0.124.56
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 -4
- package/dist/ci-setup.d.ts +11 -0
- package/dist/ci-setup.d.ts.map +1 -0
- package/dist/ci-setup.js +41 -0
- package/dist/components/capability-plugin.d.ts +40 -6
- package/dist/components/capability-plugin.d.ts.map +1 -1
- package/dist/components/capability-plugin.js +174 -30
- package/dist/components/compose-group.d.ts +1 -1
- package/dist/components/compose-group.d.ts.map +1 -1
- package/dist/components/compose-group.js +1 -1
- package/dist/components/release-inputs.d.ts +29 -0
- package/dist/components/release-inputs.d.ts.map +1 -0
- package/dist/components/release-inputs.js +57 -0
- package/dist/components/trace-facts.d.ts +16 -0
- package/dist/components/trace-facts.d.ts.map +1 -0
- package/dist/components/trace-facts.js +19 -0
- package/dist/cone.d.ts +53 -0
- package/dist/cone.d.ts.map +1 -0
- package/dist/cone.js +119 -0
- package/dist/cost-hold.d.ts +27 -0
- package/dist/cost-hold.d.ts.map +1 -0
- package/dist/cost-hold.js +21 -0
- package/dist/drift-rows.d.ts +61 -0
- package/dist/drift-rows.d.ts.map +1 -0
- package/dist/drift-rows.js +108 -0
- package/dist/exit-codes.d.ts +55 -0
- package/dist/exit-codes.d.ts.map +1 -0
- package/dist/exit-codes.js +48 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/integrity.json +7 -4
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/audit-catalog.js +27 -3
- package/dist/lint/audit-lineage.d.ts +14 -2
- package/dist/lint/audit-lineage.d.ts.map +1 -1
- package/dist/lint/post-synth/index.d.ts.map +1 -1
- package/dist/lint/post-synth/index.js +4 -0
- package/dist/lint/post-synth/record-store.d.ts +54 -0
- package/dist/lint/post-synth/record-store.d.ts.map +1 -0
- package/dist/lint/post-synth/record-store.js +104 -0
- package/dist/lint/post-synth/tf001.d.ts +2 -2
- package/dist/lint/post-synth/tf001.js +2 -2
- package/dist/lint/post-synth/tf024.d.ts +35 -11
- package/dist/lint/post-synth/tf024.d.ts.map +1 -1
- package/dist/lint/post-synth/tf024.js +92 -20
- package/dist/lint/post-synth/tf046.d.ts +64 -0
- package/dist/lint/post-synth/tf046.d.ts.map +1 -0
- package/dist/lint/post-synth/tf046.js +175 -0
- package/dist/lint/post-synth/tf047.d.ts +36 -0
- package/dist/lint/post-synth/tf047.d.ts.map +1 -0
- package/dist/lint/post-synth/tf047.js +94 -0
- package/dist/lock-rows.d.ts +36 -0
- package/dist/lock-rows.d.ts.map +1 -0
- package/dist/lock-rows.js +60 -0
- package/dist/lock-timeout.d.ts +22 -0
- package/dist/lock-timeout.d.ts.map +1 -0
- package/dist/lock-timeout.js +32 -0
- package/dist/manifest.json +1 -1
- package/dist/module-catalog/index.d.ts +69 -0
- package/dist/module-catalog/index.d.ts.map +1 -0
- package/dist/module-catalog/index.js +110 -0
- package/dist/module-catalog/refresh-cli.d.ts +4 -0
- package/dist/module-catalog/refresh-cli.d.ts.map +1 -0
- package/dist/module-catalog/refresh-cli.js +64 -0
- package/dist/module-catalog/static-tier.d.ts +6 -0
- package/dist/module-catalog/static-tier.d.ts.map +1 -0
- package/dist/module-catalog/static-tier.js +1650 -0
- package/dist/module-catalog/types.d.ts +65 -0
- package/dist/module-catalog/types.d.ts.map +1 -0
- package/dist/module-catalog/types.js +33 -0
- package/dist/okf/index.md +3 -1
- package/dist/okf/rules/TF024.md +2 -2
- package/dist/okf/rules/TF046.md +11 -0
- package/dist/okf/rules/TF047.md +11 -0
- package/dist/op/activities/terraform.d.ts +29 -0
- package/dist/op/activities/terraform.d.ts.map +1 -1
- package/dist/op/activities/terraform.js +19 -4
- package/dist/pin/consumers.d.ts +35 -0
- package/dist/pin/consumers.d.ts.map +1 -0
- package/dist/pin/consumers.js +70 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +5 -0
- package/dist/registry/address.d.ts +63 -0
- package/dist/registry/address.d.ts.map +1 -0
- package/dist/registry/address.js +65 -0
- package/dist/registry/catalog-read.d.ts +14 -0
- package/dist/registry/catalog-read.d.ts.map +1 -0
- package/dist/registry/catalog-read.js +58 -0
- package/dist/registry/deprecated.d.ts +37 -0
- package/dist/registry/deprecated.d.ts.map +1 -0
- package/dist/registry/deprecated.js +43 -0
- package/dist/registry/docs.d.ts +96 -0
- package/dist/registry/docs.d.ts.map +1 -0
- package/dist/registry/docs.js +210 -0
- package/dist/registry/edge/edge.d.ts +46 -0
- package/dist/registry/edge/edge.d.ts.map +1 -0
- package/dist/registry/edge/edge.js +194 -0
- package/dist/registry/edge/index.d.ts +11 -0
- package/dist/registry/edge/index.d.ts.map +1 -0
- package/dist/registry/edge/index.js +10 -0
- package/dist/registry/edge/lambda-edge.d.ts +76 -0
- package/dist/registry/edge/lambda-edge.d.ts.map +1 -0
- package/dist/registry/edge/lambda-edge.js +95 -0
- package/dist/registry/edge/sigv4-web.d.ts +33 -0
- package/dist/registry/edge/sigv4-web.d.ts.map +1 -0
- package/dist/registry/edge/sigv4-web.js +56 -0
- package/dist/registry/edge/token-source.d.ts +73 -0
- package/dist/registry/edge/token-source.d.ts.map +1 -0
- package/dist/registry/edge/token-source.js +115 -0
- package/dist/registry/edge/worker.d.ts +45 -0
- package/dist/registry/edge/worker.d.ts.map +1 -0
- package/dist/registry/edge/worker.js +26 -0
- package/dist/registry/handler.d.ts +25 -0
- package/dist/registry/handler.d.ts.map +1 -0
- package/dist/registry/handler.js +75 -0
- package/dist/registry/headers-file.d.ts +21 -0
- package/dist/registry/headers-file.d.ts.map +1 -0
- package/dist/registry/headers-file.js +57 -0
- package/dist/registry/index.d.ts +31 -0
- package/dist/registry/index.d.ts.map +1 -0
- package/dist/registry/index.js +30 -0
- package/dist/registry/mirror-paths.d.ts +9 -0
- package/dist/registry/mirror-paths.d.ts.map +1 -0
- package/dist/registry/mirror-paths.js +8 -0
- package/dist/registry/mirror.d.ts +130 -0
- package/dist/registry/mirror.d.ts.map +1 -0
- package/dist/registry/mirror.js +319 -0
- package/dist/registry/proxies.d.ts +73 -0
- package/dist/registry/proxies.d.ts.map +1 -0
- package/dist/registry/proxies.js +111 -0
- package/dist/registry/reader.d.ts +57 -0
- package/dist/registry/reader.d.ts.map +1 -0
- package/dist/registry/reader.js +198 -0
- package/dist/registry/semver.d.ts +16 -0
- package/dist/registry/semver.d.ts.map +1 -0
- package/dist/registry/semver.js +24 -0
- package/dist/registry/serve.d.ts +24 -0
- package/dist/registry/serve.d.ts.map +1 -0
- package/dist/registry/serve.js +64 -0
- package/dist/registry/source-forms.d.ts +79 -0
- package/dist/registry/source-forms.d.ts.map +1 -0
- package/dist/registry/source-forms.js +202 -0
- package/dist/registry/targets/artifactory.d.ts +28 -0
- package/dist/registry/targets/artifactory.d.ts.map +1 -0
- package/dist/registry/targets/artifactory.js +206 -0
- package/dist/registry/targets/fake-artifactory.d.ts +30 -0
- package/dist/registry/targets/fake-artifactory.d.ts.map +1 -0
- package/dist/registry/targets/fake-artifactory.js +142 -0
- package/dist/registry/targets/fake-gitlab.d.ts +39 -0
- package/dist/registry/targets/fake-gitlab.d.ts.map +1 -0
- package/dist/registry/targets/fake-gitlab.js +136 -0
- package/dist/registry/targets/fake-hcp.d.ts +50 -0
- package/dist/registry/targets/fake-hcp.d.ts.map +1 -0
- package/dist/registry/targets/fake-hcp.js +202 -0
- package/dist/registry/targets/fake-http.d.ts +31 -0
- package/dist/registry/targets/fake-http.d.ts.map +1 -0
- package/dist/registry/targets/fake-http.js +87 -0
- package/dist/registry/targets/fake-nexus.d.ts +27 -0
- package/dist/registry/targets/fake-nexus.d.ts.map +1 -0
- package/dist/registry/targets/fake-nexus.js +137 -0
- package/dist/registry/targets/gitlab.d.ts +53 -0
- package/dist/registry/targets/gitlab.d.ts.map +1 -0
- package/dist/registry/targets/gitlab.js +187 -0
- package/dist/registry/targets/hcp.d.ts +29 -0
- package/dist/registry/targets/hcp.d.ts.map +1 -0
- package/dist/registry/targets/hcp.js +310 -0
- package/dist/registry/targets/hosted.d.ts +62 -0
- package/dist/registry/targets/hosted.d.ts.map +1 -0
- package/dist/registry/targets/hosted.js +74 -0
- package/dist/registry/targets/index.d.ts +18 -0
- package/dist/registry/targets/index.d.ts.map +1 -0
- package/dist/registry/targets/index.js +17 -0
- package/dist/registry/targets/module-target.d.ts +68 -0
- package/dist/registry/targets/module-target.d.ts.map +1 -0
- package/dist/registry/targets/module-target.js +63 -0
- package/dist/registry/targets/nexus.d.ts +43 -0
- package/dist/registry/targets/nexus.d.ts.map +1 -0
- package/dist/registry/targets/nexus.js +250 -0
- package/dist/registry/targets/zip.d.ts +55 -0
- package/dist/registry/targets/zip.d.ts.map +1 -0
- package/dist/registry/targets/zip.js +278 -0
- package/dist/registry/writer.d.ts +101 -0
- package/dist/registry/writer.d.ts.map +1 -0
- package/dist/registry/writer.js +109 -0
- package/dist/rules/record-store.ts +122 -0
- package/dist/rules/tf001.ts +2 -2
- package/dist/rules/tf024.ts +105 -22
- package/dist/rules/tf046.ts +204 -0
- package/dist/rules/tf047.ts +114 -0
- package/dist/state/azure-blob.d.ts +8 -67
- package/dist/state/azure-blob.d.ts.map +1 -1
- package/dist/state/azure-blob.js +6 -199
- package/dist/state/gcs.d.ts +7 -61
- package/dist/state/gcs.d.ts.map +1 -1
- package/dist/state/gcs.js +5 -222
- package/dist/state/object-store.d.ts +4 -33
- package/dist/state/object-store.d.ts.map +1 -1
- package/dist/state/object-store.js +4 -17
- package/dist/state/s3.d.ts +11 -58
- package/dist/state/s3.d.ts.map +1 -1
- package/dist/state/s3.js +10 -161
- package/dist/state/store.js +2 -2
- package/dist/state-ops.d.ts +255 -0
- package/dist/state-ops.d.ts.map +1 -0
- package/dist/state-ops.js +305 -0
- package/dist/state-reads.d.ts +57 -0
- package/dist/state-reads.d.ts.map +1 -0
- package/dist/state-reads.js +104 -0
- package/dist/used-sources.d.ts +54 -0
- package/dist/used-sources.d.ts.map +1 -0
- package/dist/used-sources.js +157 -0
- package/package.json +19 -3
- package/src/__fixtures__/drift-record/README.md +5 -0
- package/src/__fixtures__/drift-rows/live-root-plan.json +70 -0
- package/src/__fixtures__/drift-rows/refresh-plan.json +61 -0
- package/src/__fixtures__/used-sources/root/extra.tofu +1 -0
- package/src/__fixtures__/used-sources/root/main.tf +31 -0
- package/src/__fixtures__/used-sources/root/modules/net/main.tf +14 -0
- package/src/__fixtures__/used-sources/root/modules/net/versions.tf.json +7 -0
- package/src/__fixtures__/used-sources/unit/terragrunt.hcl +3 -0
- package/src/ci-setup.ts +42 -0
- package/src/components/capability-plugin.test.ts +26 -14
- package/src/components/capability-plugin.ts +185 -31
- package/src/components/compose-group.ts +1 -1
- package/src/components/pr-loop-compose.tofu.e2e.test.ts +1 -1
- package/src/components/pr-loop.tofu.test.ts +1 -1
- package/src/components/release-inputs.ts +69 -0
- package/src/components/trace-facts.test.ts +21 -0
- package/src/components/trace-facts.ts +27 -0
- package/src/cone.ts +138 -0
- package/src/cost-hold.ts +29 -0
- package/src/describe-resources.engine-facts.test.ts +2 -3
- package/src/drift-rows.test.ts +135 -0
- package/src/drift-rows.ts +137 -0
- package/src/exit-codes.test.ts +23 -0
- package/src/exit-codes.ts +59 -0
- package/src/index.ts +3 -0
- package/src/lint/audit-catalog.ts +29 -3
- package/src/lint/audit-lineage.ts +14 -2
- package/src/lint/post-synth/fixtures/TF024/live-with-record-store/main.tf +18 -0
- package/src/lint/post-synth/fixtures/TF024/sidecar-with-backend/estate.chdf.hcl +1 -0
- package/src/lint/post-synth/fixtures/TF024/sidecar-with-backend/main.tf +10 -0
- package/src/lint/post-synth/fixtures/TF046/azurerm-no-resource-group/main.tf +15 -0
- package/src/lint/post-synth/fixtures/TF046/backend-config-only/main.tf +14 -0
- package/src/lint/post-synth/fixtures/TF046/derived-s3/backend.tf +7 -0
- package/src/lint/post-synth/fixtures/TF046/derived-s3/main.tf +9 -0
- package/src/lint/post-synth/fixtures/TF046/explicit-local/main.tf +16 -0
- package/src/lint/post-synth/fixtures/TF046/explicit-s3-beside-local-backend/main.tf +15 -0
- package/src/lint/post-synth/fixtures/TF046/implied-local/main.tf +9 -0
- package/src/lint/post-synth/fixtures/TF046/sidecar-implied/estate.chdf.hcl +1 -0
- package/src/lint/post-synth/fixtures/TF046/sidecar-implied/main.tf +5 -0
- package/src/lint/post-synth/fixtures/TF047/clear/main.tf +12 -0
- package/src/lint/post-synth/fixtures/TF047/refused/main.tf +12 -0
- package/src/lint/post-synth/fixtures/TF047/unknown-module/main.tf +12 -0
- package/src/lint/post-synth/fixtures/TF047/unknown-version/main.tf +12 -0
- package/src/lint/post-synth/index.ts +4 -0
- package/src/lint/post-synth/record-store.ts +122 -0
- package/src/lint/post-synth/tf001.ts +2 -2
- package/src/lint/post-synth/tf024.test.ts +26 -5
- package/src/lint/post-synth/tf024.ts +105 -22
- package/src/lint/post-synth/tf046.test.ts +137 -0
- package/src/lint/post-synth/tf046.ts +204 -0
- package/src/lint/post-synth/tf047.test.ts +56 -0
- package/src/lint/post-synth/tf047.ts +114 -0
- package/src/lock-rows.test.ts +70 -0
- package/src/lock-rows.ts +82 -0
- package/src/lock-timeout.ts +34 -0
- package/src/module-catalog/index.test.ts +93 -0
- package/src/module-catalog/index.ts +159 -0
- package/src/module-catalog/refresh-cli.ts +68 -0
- package/src/module-catalog/static-tier.ts +1659 -0
- package/src/module-catalog/types.ts +88 -0
- package/src/op/activities/terraform.ts +46 -5
- package/src/op-wave-step.test.ts +109 -0
- package/src/pin/__fixtures__/consumers/live/dev/main.tf +4 -0
- package/src/pin/__fixtures__/consumers/live/prod/main.tf +21 -0
- package/src/pin/__fixtures__/consumers/units/vpc/terragrunt.hcl +7 -0
- package/src/pin/consumers.test.ts +57 -0
- package/src/pin/consumers.ts +95 -0
- package/src/plugin.ts +6 -0
- package/src/registry/__fixtures__/docs/documented/.terraform-docs.yml +3 -0
- package/src/registry/__fixtures__/docs/documented/README.md +1 -0
- package/src/registry/__fixtures__/docs/documented/main.tf +8 -0
- package/src/registry/__fixtures__/docs/json/main.tf.json +14 -0
- package/src/registry/__fixtures__/docs/vpc/README.md +3 -0
- package/src/registry/__fixtures__/docs/vpc/examples/complete/main.tf +5 -0
- package/src/registry/__fixtures__/docs/vpc/examples/simple/README.md +1 -0
- package/src/registry/__fixtures__/docs/vpc/examples/simple/main.tf +4 -0
- package/src/registry/__fixtures__/docs/vpc/main.tf +14 -0
- package/src/registry/__fixtures__/docs/vpc/outputs.tf +9 -0
- package/src/registry/__fixtures__/docs/vpc/variables.tf +16 -0
- package/src/registry/__fixtures__/proxied-root/main.tf +12 -0
- package/src/registry/address.ts +100 -0
- package/src/registry/catalog-read.ts +56 -0
- package/src/registry/deprecated.test.ts +139 -0
- package/src/registry/deprecated.ts +76 -0
- package/src/registry/docs.test.ts +140 -0
- package/src/registry/docs.ts +299 -0
- package/src/registry/edge/bundle.test.ts +50 -0
- package/src/registry/edge/edge.test.ts +176 -0
- package/src/registry/edge/edge.ts +211 -0
- package/src/registry/edge/index.ts +10 -0
- package/src/registry/edge/lambda-edge.ts +127 -0
- package/src/registry/edge/sigv4-web.ts +79 -0
- package/src/registry/edge/token-source.test.ts +258 -0
- package/src/registry/edge/token-source.ts +169 -0
- package/src/registry/edge/worker.ts +61 -0
- package/src/registry/handler.ts +89 -0
- package/src/registry/headers-file.ts +61 -0
- package/src/registry/index.ts +31 -0
- package/src/registry/mirror-paths.ts +10 -0
- package/src/registry/mirror.test.ts +198 -0
- package/src/registry/mirror.ts +395 -0
- package/src/registry/proxies.test.ts +153 -0
- package/src/registry/proxies.ts +132 -0
- package/src/registry/reader.ts +225 -0
- package/src/registry/registry.test.ts +262 -0
- package/src/registry/semver.ts +34 -0
- package/src/registry/serve.ts +82 -0
- package/src/registry/source-forms.test.ts +87 -0
- package/src/registry/source-forms.ts +216 -0
- package/src/registry/targets/artifactory.test.ts +248 -0
- package/src/registry/targets/artifactory.ts +257 -0
- package/src/registry/targets/fake-artifactory.ts +158 -0
- package/src/registry/targets/fake-gitlab.ts +161 -0
- package/src/registry/targets/fake-hcp.ts +233 -0
- package/src/registry/targets/fake-http.ts +97 -0
- package/src/registry/targets/fake-nexus.ts +152 -0
- package/src/registry/targets/gitlab.test.ts +208 -0
- package/src/registry/targets/gitlab.ts +223 -0
- package/src/registry/targets/hcp.test.ts +230 -0
- package/src/registry/targets/hcp.ts +360 -0
- package/src/registry/targets/hosted.ts +117 -0
- package/src/registry/targets/index.ts +17 -0
- package/src/registry/targets/module-target.ts +131 -0
- package/src/registry/targets/nexus.test.ts +220 -0
- package/src/registry/targets/nexus.ts +314 -0
- package/src/registry/targets/zip.test.ts +65 -0
- package/src/registry/targets/zip.ts +287 -0
- package/src/registry/writer.ts +155 -0
- package/src/state/azure-blob.ts +6 -221
- package/src/state/gcs.ts +5 -252
- package/src/state/object-store.ts +4 -32
- package/src/state/s3.ts +10 -182
- package/src/state/store.ts +2 -2
- package/src/state-ops.test.ts +182 -0
- package/src/state-ops.ts +462 -0
- package/src/state-reads.ts +116 -0
- package/src/used-sources.test.ts +75 -0
- package/src/used-sources.ts +204 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
// chant#4605: the Nexus module target against the in-memory Nexus fake. No request leaves the
|
|
2
|
+
// process, no CLI config file is written, and no credential appears in a log line or an error.
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { PublishTargetError } from "@intentius/chant/lifecycle/release-publish";
|
|
5
|
+
import { createRedactor } from "@intentius/chant/lifecycle/publish-credentials";
|
|
6
|
+
import {
|
|
7
|
+
archiveContent,
|
|
8
|
+
moduleZip,
|
|
9
|
+
nexusHostBlock,
|
|
10
|
+
nexusHostLabel,
|
|
11
|
+
nexusModuleTarget,
|
|
12
|
+
nexusUploadUrl,
|
|
13
|
+
readModuleZipComment,
|
|
14
|
+
readZip,
|
|
15
|
+
registryVersions,
|
|
16
|
+
type ModuleVersionContext,
|
|
17
|
+
} from "../index";
|
|
18
|
+
import { createFakeNexus, type FakeNexusOptions } from "./fake-nexus";
|
|
19
|
+
import { moduleTarball } from "./fake-http";
|
|
20
|
+
|
|
21
|
+
const HOST = "nexus.example.com";
|
|
22
|
+
const URL_ = `https://${HOST}`;
|
|
23
|
+
const USER = "tf-publisher";
|
|
24
|
+
const TOKEN = "nexus-pass-0123456789abcdef";
|
|
25
|
+
const ENV = { NEXUS_USER: USER, NEXUS_TOKEN: TOKEN };
|
|
26
|
+
const BASIC = `Basic ${Buffer.from(`${USER}:${TOKEN}`).toString("base64")}`;
|
|
27
|
+
|
|
28
|
+
const fake = (over: Partial<FakeNexusOptions> = {}) => createFakeNexus({ host: HOST, repos: ["terraform-hosted"], users: { [USER]: TOKEN }, ...over });
|
|
29
|
+
|
|
30
|
+
const clock = () => {
|
|
31
|
+
let t = 0;
|
|
32
|
+
const sleeps: number[] = [];
|
|
33
|
+
return { now: () => t, sleep: async (ms: number) => void (sleeps.push(ms), (t += ms)), sleeps };
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const archiveOf = (text: string) => moduleTarball({ "main.tf": `# ${text}\n`, "outputs.tf": "" });
|
|
37
|
+
|
|
38
|
+
function context(env: NodeJS.ProcessEnv, over: Partial<ModuleVersionContext> = {}): ModuleVersionContext & { lines: string[] } {
|
|
39
|
+
const archive = over.archive ?? archiveOf("vpc 1.2.0");
|
|
40
|
+
const redactor = createRedactor(env, ["NEXUS_TOKEN"]);
|
|
41
|
+
const lines: string[] = [];
|
|
42
|
+
return {
|
|
43
|
+
cwd: "/nowhere",
|
|
44
|
+
env,
|
|
45
|
+
redactor,
|
|
46
|
+
log: (line) => lines.push(redactor.redact(line)),
|
|
47
|
+
provenance: { oidc: false, ci: null, reason: "not in CI" },
|
|
48
|
+
tag: null,
|
|
49
|
+
address: { namespace: "acme", name: "vpc", system: "aws" },
|
|
50
|
+
version: "1.2.0",
|
|
51
|
+
revision: "3f2a9c1d0b7e3f2a9c1d0b7e3f2a9c1d0b7e3f2a",
|
|
52
|
+
content: archiveContent(archive),
|
|
53
|
+
archive,
|
|
54
|
+
...over,
|
|
55
|
+
lines,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const target = (nx: ReturnType<typeof fake>, opts = {}, c = clock()) =>
|
|
60
|
+
nexusModuleTarget({ url: URL_, repo: "terraform-hosted", ...opts }, { fetch: nx.fetch, registryFetch: nx.fetch, sleep: c.sleep, now: c.now });
|
|
61
|
+
|
|
62
|
+
const errorOf = async (p: Promise<unknown>): Promise<PublishTargetError> => {
|
|
63
|
+
const e = await p.then(
|
|
64
|
+
() => undefined,
|
|
65
|
+
(err: unknown) => err,
|
|
66
|
+
);
|
|
67
|
+
expect(e).toBeInstanceOf(PublishTargetError);
|
|
68
|
+
return e as PublishTargetError;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const noSecret = (text: string) => {
|
|
72
|
+
expect(text).not.toContain(TOKEN);
|
|
73
|
+
expect(text).not.toContain(BASIC.slice(6));
|
|
74
|
+
expect(text).not.toMatch(/TerraformBearerToken\.fake/);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
describe("Nexus module target (chant#4605)", () => {
|
|
78
|
+
it("uploads the zip of the attested archive with one PUT, basic credentials and application/zip", async () => {
|
|
79
|
+
const nx = fake();
|
|
80
|
+
const ctx = context(ENV);
|
|
81
|
+
const t = target(nx);
|
|
82
|
+
expect(await t.plan(ctx)).toEqual([{ name: "acme/vpc/aws 1.2.0", destination: "terraform-hosted/acme/vpc/aws", digest: ctx.content }]);
|
|
83
|
+
const staged = await t.stage(ctx);
|
|
84
|
+
const puts = nx.requests.filter((r) => r.method === "PUT");
|
|
85
|
+
expect(puts).toHaveLength(1);
|
|
86
|
+
expect(puts[0]!.url).toBe(`${URL_}/repository/terraform-hosted/v1/modules/acme/vpc/aws/1.2.0/vpc-1.2.0.zip`);
|
|
87
|
+
expect(puts[0]!.headers.authorization).toBe(BASIC);
|
|
88
|
+
expect(puts[0]!.headers["content-type"]).toBe("application/zip");
|
|
89
|
+
expect(Buffer.compare(Buffer.from(puts[0]!.body!), moduleZip(ctx.archive, ctx.content, ctx.revision))).toBe(0);
|
|
90
|
+
expect(readModuleZipComment(readZip(puts[0]!.body!).comment)?.content).toBe(ctx.content);
|
|
91
|
+
noSecret(JSON.stringify(staged));
|
|
92
|
+
|
|
93
|
+
const published = await t.reveal(ctx, staged);
|
|
94
|
+
expect(published.destination).toBe("terraform-hosted/acme/vpc/aws");
|
|
95
|
+
expect(await t.held(ctx.address, "1.2.0", ctx)).toEqual({ content: ctx.content, revision: ctx.revision });
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("reads back through the registry protocol with an in-memory host block, never discovery", async () => {
|
|
99
|
+
const nx = fake();
|
|
100
|
+
const ctx = context(ENV);
|
|
101
|
+
const t = target(nx);
|
|
102
|
+
await t.reveal(ctx, await t.stage(ctx));
|
|
103
|
+
const paths = nx.requests.map((r) => new URL(r.url).pathname);
|
|
104
|
+
expect(paths).not.toContain("/.well-known/terraform.json");
|
|
105
|
+
expect(paths).toContain("/repository/terraform-hosted/v1/modules/acme/vpc/aws/versions");
|
|
106
|
+
expect(paths).toContain("/repository/terraform-hosted/v1/modules/acme/vpc/aws/1.2.0/download");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("reads with the 3.95 bearer token issued for the basic credentials", async () => {
|
|
110
|
+
const nx = fake();
|
|
111
|
+
const ctx = context(ENV);
|
|
112
|
+
const t = target(nx);
|
|
113
|
+
await t.reveal(ctx, await t.stage(ctx));
|
|
114
|
+
expect(nx.issued.size).toBe(1);
|
|
115
|
+
const read = nx.requests.find((r) => r.url.endsWith("/versions"))!;
|
|
116
|
+
expect(read.headers.authorization).toBe(`Bearer ${[...nx.issued][0]}`);
|
|
117
|
+
noSecret(ctx.lines.join("\n"));
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("reads with the basic credentials on a Nexus without the token call", async () => {
|
|
121
|
+
const nx = fake({ bearer: false });
|
|
122
|
+
const ctx = context(ENV);
|
|
123
|
+
const t = target(nx);
|
|
124
|
+
await t.reveal(ctx, await t.stage(ctx));
|
|
125
|
+
const read = nx.requests.find((r) => r.url.endsWith("/versions"))!;
|
|
126
|
+
expect(read.headers.authorization).toBe(BASIC);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("reads with TF_TOKEN_<nexus host> when it is set", async () => {
|
|
130
|
+
const nx = fake({ bearer: false });
|
|
131
|
+
const ctx = context({ ...ENV, TF_TOKEN_nexus_example_com: "cli-token-1234567890" });
|
|
132
|
+
const t = target(nx);
|
|
133
|
+
const read = await errorOf(t.versions(ctx.address, ctx));
|
|
134
|
+
// The fake does not know the CLI token: the read is refused, and the token is never printed.
|
|
135
|
+
expect(read.code).toBe("publish-read-failed");
|
|
136
|
+
expect(read.message).not.toContain("cli-token-1234567890");
|
|
137
|
+
expect(nx.requests.at(-1)!.headers.authorization).toBe("Bearer cli-token-1234567890");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("waits out a cached not-found after the upload", async () => {
|
|
141
|
+
const nx = fake({ cacheNotFound: 2 });
|
|
142
|
+
const c = clock();
|
|
143
|
+
const ctx = context(ENV);
|
|
144
|
+
const t = target(nx, { pollMs: 500 }, c);
|
|
145
|
+
await t.reveal(ctx, await t.stage(ctx));
|
|
146
|
+
expect(c.sleeps).toEqual([500, 500]);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("stops after the wait when the version is never listed", async () => {
|
|
150
|
+
const nx = fake({ cacheNotFound: 100 });
|
|
151
|
+
const ctx = context(ENV);
|
|
152
|
+
const t = target(nx, { waitMs: 1000, pollMs: 500 });
|
|
153
|
+
const e = await errorOf(t.reveal(ctx, await t.stage(ctx)));
|
|
154
|
+
expect(e.code).toBe("publish-reveal-missing");
|
|
155
|
+
expect(e.fix).toContain("3.95.0");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("sends nothing for a version already uploaded with the same content", async () => {
|
|
159
|
+
const nx = fake();
|
|
160
|
+
const ctx = context(ENV);
|
|
161
|
+
nx.seed("terraform-hosted", "acme", "vpc", "aws", "1.2.0", "vpc-1.2.0.zip", moduleZip(ctx.archive, ctx.content));
|
|
162
|
+
const staged = await target(nx).stage(ctx);
|
|
163
|
+
expect(staged.items[0]!.present).toBe(true);
|
|
164
|
+
expect(nx.requests.filter((r) => r.method === "PUT")).toHaveLength(0);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("refuses a version already uploaded with other content", async () => {
|
|
168
|
+
const nx = fake();
|
|
169
|
+
const ctx = context(ENV);
|
|
170
|
+
const other = archiveOf("other");
|
|
171
|
+
nx.seed("terraform-hosted", "acme", "vpc", "aws", "1.2.0", "vpc-1.2.0.zip", moduleZip(other, archiveContent(other)));
|
|
172
|
+
expect((await errorOf(target(nx).stage(ctx))).code).toBe("publish-version-held");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("refuses a 401 naming the variables, never their values", async () => {
|
|
176
|
+
const nx = fake();
|
|
177
|
+
const ctx = context({ NEXUS_USER: USER, NEXUS_TOKEN: "nexus-wrong-pass-0000" });
|
|
178
|
+
const e = await errorOf(target(nx).stage(ctx));
|
|
179
|
+
expect(e.code).toBe("publish-credentials-refused");
|
|
180
|
+
expect(`${e.message} ${e.fix}`).not.toContain("nexus-wrong-pass-0000");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("refuses to plan without a user or a token", async () => {
|
|
184
|
+
const nx = fake();
|
|
185
|
+
const e = await errorOf(target(nx).plan(context({ NEXUS_TOKEN: TOKEN })));
|
|
186
|
+
expect(e.code).toBe("publish-credentials-missing");
|
|
187
|
+
expect(e.message).toContain("NEXUS_USER");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("names the hosted floor on an upload Nexus refuses", async () => {
|
|
191
|
+
const nx = fake();
|
|
192
|
+
const ctx = context(ENV);
|
|
193
|
+
const failing: typeof nx.fetch = async (url, init) => (init?.method === "PUT" ? new Response("{}", { status: 500 }) : nx.fetch(url, init));
|
|
194
|
+
const e = await errorOf(nexusModuleTarget({ url: URL_, repo: "terraform-hosted" }, { fetch: failing, registryFetch: failing }).stage(ctx));
|
|
195
|
+
expect(e.code).toBe("publish-upload-failed");
|
|
196
|
+
expect(e.fix).toContain("3.89.0");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("gives the CLI host block consumers add, and refuses a label a host block cannot carry", () => {
|
|
200
|
+
expect(nexusHostBlock({ url: URL_, repo: "terraform-hosted" })).toBe(
|
|
201
|
+
`host "terraform-hosted" {\n services = {\n "modules.v1" = "${URL_}/repository/terraform-hosted/v1/modules/"\n }\n}\n`,
|
|
202
|
+
);
|
|
203
|
+
expect(() => nexusHostLabel({ repo: "terraform_hosted" })).toThrow(PublishTargetError);
|
|
204
|
+
expect(nexusHostLabel({ repo: "terraform_hosted", host: "tf-hosted" })).toBe("tf-hosted");
|
|
205
|
+
expect(nexusUploadUrl(URL_, "r", { namespace: "n", name: "m", system: "aws" }, "1.0.0")).toBe(`${URL_}/repository/r/v1/modules/n/m/aws/1.0.0/m-1.0.0.zip`);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("lets the registry reader take a bare host from its services map", async () => {
|
|
209
|
+
const nx = fake();
|
|
210
|
+
const ctx = context(ENV);
|
|
211
|
+
await target(nx).stage(ctx);
|
|
212
|
+
const versions = await registryVersions("terraform-hosted/acme/vpc/aws", {
|
|
213
|
+
services: { "terraform-hosted": `${URL_}/repository/terraform-hosted/v1/modules/` },
|
|
214
|
+
authorization: BASIC,
|
|
215
|
+
token: false,
|
|
216
|
+
fetch: nx.fetch,
|
|
217
|
+
});
|
|
218
|
+
expect(versions).toEqual(["1.2.0"]);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A Sonatype Nexus Repository hosted Terraform repository as a module target
|
|
3
|
+
* (chant#4605), from the research on terragucci#1299 and its ruling.
|
|
4
|
+
*
|
|
5
|
+
* Nexus has a Terraform format from 3.88 and hosted Terraform repositories
|
|
6
|
+
* from 3.89, which is this target's floor. The one write is the documented
|
|
7
|
+
* module upload (help.sonatype.com, CLI usage and options):
|
|
8
|
+
*
|
|
9
|
+
* PUT <url>/repository/<repo>/v1/modules/<namespace>/<name>/<system>/<version>/<name>-<version>.zip
|
|
10
|
+
*
|
|
11
|
+
* with `Content-Type: application/zip`, basic `user:token` credentials
|
|
12
|
+
* (ruling B) and the zip of the attested archive as the body (ruling A,
|
|
13
|
+
* ./zip.ts). The user is `NEXUS_USER` (or the declaration's `user` or
|
|
14
|
+
* `userEnv`) and the token `NEXUS_TOKEN` (or `tokenEnv`), a user token's
|
|
15
|
+
* pass code or the user's password.
|
|
16
|
+
*
|
|
17
|
+
* Nexus has no single registry host to discover from: a consumer declares a
|
|
18
|
+
* CLI `host "<name>"` block mapping `modules.v1` to the repository's
|
|
19
|
+
* `<url>/repository/<repo>/v1/modules/`, and writes
|
|
20
|
+
* `<name>/<namespace>/<name>/<system>`. `nexusHostBlock` gives that block.
|
|
21
|
+
* `versions` and `reveal` read the module the same way, with the block held
|
|
22
|
+
* in memory as the reader's `services` map (ruling C), so no CLI config file
|
|
23
|
+
* is written. The reads send `TF_TOKEN_<nexus host>` when it is set, else the
|
|
24
|
+
* bearer token Nexus 3.95 and later issue at `<repo>/v1/api/token` for the
|
|
25
|
+
* basic credentials, else the basic credentials themselves, which Nexus
|
|
26
|
+
* takes as header authentication before 3.95. Nexus before 3.95 kept a "not
|
|
27
|
+
* found" answer in its cache, so `reveal` waits for the version to be
|
|
28
|
+
* listed, up to `waitMs` (default 30s).
|
|
29
|
+
*
|
|
30
|
+
* `held` GETs the uploaded zip at its path. A chant zip names the attested
|
|
31
|
+
* content in its archive comment, so the same content comes back `present`
|
|
32
|
+
* with nothing sent and other content is refused.
|
|
33
|
+
*
|
|
34
|
+
* Not confirmed by the docs (terragucci#1299): Sonatype's page on creating a
|
|
35
|
+
* Terraform repository says GPG signing is mandatory for hosted repositories
|
|
36
|
+
* and also that it is optional for modules, so what a module upload does on
|
|
37
|
+
* a repository with no signing key is unknown; whether a second upload of a
|
|
38
|
+
* version is refused; whether a hosted repository serves
|
|
39
|
+
* `/.well-known/terraform.json` (this target never asks for it); and the
|
|
40
|
+
* answer shape of the 3.95 token call (this reads a JSON `token` field, else
|
|
41
|
+
* the body as text). Nexus shows no deprecation for a module version, so the
|
|
42
|
+
* bucket catalog carries it.
|
|
43
|
+
*/
|
|
44
|
+
import { PublishTargetError, type PublishContextBase, type PublishItem, type StagedTarget } from "@intentius/chant/lifecycle/release-publish";
|
|
45
|
+
import { terraformCredential } from "@intentius/chant/terraform/credentials";
|
|
46
|
+
import { createHash } from "node:crypto";
|
|
47
|
+
import { RegistryError, registryLocation, registryVersions, type RegistryReadOptions } from "../reader";
|
|
48
|
+
import {
|
|
49
|
+
answerError,
|
|
50
|
+
checkModuleVersion,
|
|
51
|
+
contentDigest,
|
|
52
|
+
defaultPublishFetch,
|
|
53
|
+
versionHeldError,
|
|
54
|
+
type HeldVersion,
|
|
55
|
+
type ModuleAddress,
|
|
56
|
+
type ModulePublishTarget,
|
|
57
|
+
type ModuleTargetDeps,
|
|
58
|
+
type ModuleVersionContext,
|
|
59
|
+
} from "./module-target";
|
|
60
|
+
import {
|
|
61
|
+
HOSTED_LIST_WAIT_MS,
|
|
62
|
+
HOSTED_POLL_MS,
|
|
63
|
+
defaultRegistryFetch,
|
|
64
|
+
getBytes,
|
|
65
|
+
locationContent,
|
|
66
|
+
needBasicCredential,
|
|
67
|
+
readBasicCredential,
|
|
68
|
+
revealMismatch,
|
|
69
|
+
waitListed,
|
|
70
|
+
writeRefused,
|
|
71
|
+
type BasicCredential,
|
|
72
|
+
type BasicCredentialEnv,
|
|
73
|
+
} from "./hosted";
|
|
74
|
+
import { heldContentOf, moduleZip } from "./zip";
|
|
75
|
+
|
|
76
|
+
/** How a Nexus module target is declared. */
|
|
77
|
+
export interface NexusModuleTargetOptions {
|
|
78
|
+
/** The Nexus base, such as `https://nexus.example.com`. */
|
|
79
|
+
url: string;
|
|
80
|
+
/** The hosted Terraform repository's name. */
|
|
81
|
+
repo: string;
|
|
82
|
+
/**
|
|
83
|
+
* The label of the consumers' CLI `host` block, which their sources start
|
|
84
|
+
* with. Default: the repository's name, lower-cased. It must be a valid host name.
|
|
85
|
+
*/
|
|
86
|
+
host?: string;
|
|
87
|
+
/** The variable the token (a user token's pass code, or the password) is read from. Default `NEXUS_TOKEN`. */
|
|
88
|
+
tokenEnv?: string;
|
|
89
|
+
/** The variable the user name (a user token's name code, or the user) is read from. Default `NEXUS_USER`. */
|
|
90
|
+
userEnv?: string;
|
|
91
|
+
/** The user name, in place of `userEnv`. */
|
|
92
|
+
user?: string;
|
|
93
|
+
/** How long `reveal` waits for Nexus to list the version. Default 30s. */
|
|
94
|
+
waitMs?: number;
|
|
95
|
+
/** How often it asks while it waits. Default 2s. */
|
|
96
|
+
pollMs?: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const NEXUS_TOKEN_ENV = "NEXUS_TOKEN";
|
|
100
|
+
export const NEXUS_USER_ENV = "NEXUS_USER";
|
|
101
|
+
/** The first Nexus release with hosted Terraform repositories. */
|
|
102
|
+
export const NEXUS_HOSTED_FLOOR = "3.89.0";
|
|
103
|
+
/** The first Nexus release with the bearer token call and the archive and cache fixes the research names. */
|
|
104
|
+
export const NEXUS_BEARER_FLOOR = "3.95.0";
|
|
105
|
+
|
|
106
|
+
const HOST_LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/;
|
|
107
|
+
|
|
108
|
+
/** The Nexus base, without a trailing slash. */
|
|
109
|
+
export function nexusBase(url: string): string {
|
|
110
|
+
try {
|
|
111
|
+
const u = new URL(url);
|
|
112
|
+
return `${u.origin}${u.pathname.replace(/\/+$/, "")}`;
|
|
113
|
+
} catch {
|
|
114
|
+
throw new PublishTargetError("publish-config-invalid", `${url} is not a URL`, "Set url to the Nexus base, such as https://nexus.example.com.");
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The repository's modules.v1 URL, ending in `/`. */
|
|
119
|
+
export const nexusModulesUrl = (base: string, repo: string): string => `${base}/repository/${encodeURIComponent(repo)}/v1/modules/`;
|
|
120
|
+
|
|
121
|
+
/** The upload path of one version's zip. */
|
|
122
|
+
export function nexusUploadUrl(base: string, repo: string, a: ModuleAddress, version: string): string {
|
|
123
|
+
const seg = [a.namespace, a.name, a.system, version].map(encodeURIComponent).join("/");
|
|
124
|
+
return `${nexusModulesUrl(base, repo)}${seg}/${encodeURIComponent(`${a.name}-${version}.zip`)}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The label of the consumers' `host` block. */
|
|
128
|
+
export function nexusHostLabel(o: Pick<NexusModuleTargetOptions, "repo" | "host">): string {
|
|
129
|
+
const label = (o.host ?? o.repo).toLowerCase();
|
|
130
|
+
if (!HOST_LABEL.test(label)) {
|
|
131
|
+
throw new PublishTargetError("publish-config-invalid", `${JSON.stringify(label)} is not a host name a CLI host block can carry`, "Set the declaration's host to a name of letters, digits and -, such as the repository's name without _.");
|
|
132
|
+
}
|
|
133
|
+
return label;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** The source consumers write for a module in the repository: `<host label>/<namespace>/<name>/<system>`. */
|
|
137
|
+
export const nexusSource = (label: string, a: ModuleAddress): string => `${label}/${a.namespace}/${a.name}/${a.system}`;
|
|
138
|
+
|
|
139
|
+
/** The CLI configuration block a consumer adds so its sources reach the repository. */
|
|
140
|
+
export function nexusHostBlock(o: Pick<NexusModuleTargetOptions, "url" | "repo" | "host">): string {
|
|
141
|
+
return `host "${nexusHostLabel(o)}" {\n services = {\n "modules.v1" = "${nexusModulesUrl(nexusBase(o.url), o.repo)}"\n }\n}\n`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function nexusModuleTarget(options: NexusModuleTargetOptions, deps: ModuleTargetDeps = {}): ModulePublishTarget<"nexus"> {
|
|
145
|
+
const put = deps.fetch ?? defaultPublishFetch;
|
|
146
|
+
const get = deps.registryFetch ?? defaultRegistryFetch;
|
|
147
|
+
const names: BasicCredentialEnv = { tokenEnv: options.tokenEnv ?? NEXUS_TOKEN_ENV, userEnv: options.userEnv ?? NEXUS_USER_ENV, ...(options.user ? { user: options.user } : {}) };
|
|
148
|
+
const wait = {
|
|
149
|
+
waitMs: options.waitMs ?? HOSTED_LIST_WAIT_MS,
|
|
150
|
+
pollMs: options.pollMs ?? HOSTED_POLL_MS,
|
|
151
|
+
sleep: deps.sleep ?? ((ms: number) => new Promise<void>((r) => setTimeout(r, ms))),
|
|
152
|
+
now: deps.now ?? Date.now,
|
|
153
|
+
};
|
|
154
|
+
const key = `nexus:${createHash("sha256")
|
|
155
|
+
.update(JSON.stringify({ url: options.url, repo: options.repo, host: options.host ?? null, tokenEnv: names.tokenEnv, userEnv: names.userEnv, user: options.user ?? null }))
|
|
156
|
+
.digest("hex")
|
|
157
|
+
.slice(0, 16)}`;
|
|
158
|
+
|
|
159
|
+
const where = () => {
|
|
160
|
+
const base = nexusBase(options.url);
|
|
161
|
+
const label = nexusHostLabel(options);
|
|
162
|
+
return { base, label, nexusHost: new URL(base).host.toLowerCase(), modules: nexusModulesUrl(base, options.repo) };
|
|
163
|
+
};
|
|
164
|
+
const fix = `Use a user token, or a user, that may add and edit components in the repository ${options.repo}.`;
|
|
165
|
+
const needCredential = (ctx: Pick<PublishContextBase, "env" | "redactor">) => needBasicCredential(ctx, names, "Nexus", fix);
|
|
166
|
+
|
|
167
|
+
/** The bearer token from Nexus 3.95's token call, per credential; null when the call is not there. */
|
|
168
|
+
const issued = new Map<string, string | null>();
|
|
169
|
+
const issuedToken = async (ctx: Pick<PublishContextBase, "redactor">, basic: BasicCredential): Promise<string | null> => {
|
|
170
|
+
if (issued.has(basic.header)) return issued.get(basic.header)!;
|
|
171
|
+
const { base } = where();
|
|
172
|
+
const url = `${base}/repository/${encodeURIComponent(options.repo)}/v1/api/token`;
|
|
173
|
+
const { status, bytes } = await getBytes(ctx, get, url, basic.header);
|
|
174
|
+
let token: string | null = null;
|
|
175
|
+
if (status >= 200 && status <= 299) {
|
|
176
|
+
const text = Buffer.from(bytes).toString("utf-8").trim();
|
|
177
|
+
try {
|
|
178
|
+
const doc = JSON.parse(text) as { token?: unknown };
|
|
179
|
+
token = typeof doc.token === "string" && doc.token ? doc.token : null;
|
|
180
|
+
} catch {
|
|
181
|
+
token = text || null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
ctx.redactor.add(token);
|
|
185
|
+
issued.set(basic.header, token);
|
|
186
|
+
return token;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
/** How the reads authenticate: the CLIs' token for the Nexus host, else the issued bearer token, else basic. */
|
|
190
|
+
const readAuth = async (ctx: Pick<PublishContextBase, "env" | "redactor">): Promise<string | undefined> => {
|
|
191
|
+
const { nexusHost } = where();
|
|
192
|
+
const cli = terraformCredential(nexusHost, { env: ctx.env })?.token;
|
|
193
|
+
if (cli) {
|
|
194
|
+
ctx.redactor.add(cli);
|
|
195
|
+
return `Bearer ${cli}`;
|
|
196
|
+
}
|
|
197
|
+
const basic = readBasicCredential(ctx, names);
|
|
198
|
+
if (!basic) return undefined;
|
|
199
|
+
const bearer = await issuedToken(ctx, basic);
|
|
200
|
+
return bearer ? `Bearer ${bearer}` : basic.header;
|
|
201
|
+
};
|
|
202
|
+
const readOptions = async (ctx: Pick<PublishContextBase, "env" | "redactor">): Promise<RegistryReadOptions> => {
|
|
203
|
+
const { label, modules } = where();
|
|
204
|
+
const authorization = await readAuth(ctx);
|
|
205
|
+
return { services: { [label]: modules }, token: false, fetch: get, ...(authorization ? { authorization } : {}) };
|
|
206
|
+
};
|
|
207
|
+
const read = async <T>(ctx: Pick<PublishContextBase, "redactor">, run: () => Promise<T>): Promise<T> => {
|
|
208
|
+
try {
|
|
209
|
+
return await run();
|
|
210
|
+
} catch (e) {
|
|
211
|
+
if (e instanceof RegistryError) throw answerError(ctx, "publish-read-failed", e.message);
|
|
212
|
+
throw e;
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const versions = async (address: ModuleAddress, ctx: Pick<PublishContextBase, "env" | "redactor">): Promise<string[]> => {
|
|
217
|
+
const o = await readOptions(ctx);
|
|
218
|
+
return read(ctx, () => registryVersions(nexusSource(where().label, address), o));
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const held = async (address: ModuleAddress, version: string, ctx: Pick<PublishContextBase, "env" | "redactor">): Promise<HeldVersion | undefined> => {
|
|
222
|
+
const { base } = where();
|
|
223
|
+
const url = nexusUploadUrl(base, options.repo, address, version);
|
|
224
|
+
const { status, bytes } = await getBytes(ctx, get, url, await readAuth(ctx));
|
|
225
|
+
if (status === 404) return undefined;
|
|
226
|
+
if (status === 401 || status === 403) {
|
|
227
|
+
throw answerError(ctx, "publish-credentials-refused", `Nexus answered ${status} to a read of ${url}`, `Set ${names.userEnv} and ${names.tokenEnv} to a user with read access to ${options.repo}.`);
|
|
228
|
+
}
|
|
229
|
+
if (status < 200 || status > 299) throw answerError(ctx, "publish-read-failed", `Nexus answered ${status} to a read of ${url}`);
|
|
230
|
+
return heldContentOf(bytes, contentDigest);
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const item = (ctx: ModuleVersionContext, present?: boolean): PublishItem => {
|
|
234
|
+
const { label } = where();
|
|
235
|
+
const a = ctx.address;
|
|
236
|
+
return { name: `${a.namespace}/${a.name}/${a.system} ${ctx.version}`, destination: nexusSource(label, a), digest: ctx.content, ...(present ? { present: true } : {}) };
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
kind: "nexus",
|
|
241
|
+
name: "Nexus Terraform repository",
|
|
242
|
+
destination: `${options.url} ${options.repo}`,
|
|
243
|
+
credentialEnv: [names.tokenEnv, ...(options.user ? [] : [names.userEnv])],
|
|
244
|
+
key,
|
|
245
|
+
versions,
|
|
246
|
+
held,
|
|
247
|
+
|
|
248
|
+
async plan(ctx) {
|
|
249
|
+
checkModuleVersion(ctx);
|
|
250
|
+
where();
|
|
251
|
+
needCredential(ctx);
|
|
252
|
+
return [item(ctx)];
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
async stage(ctx): Promise<StagedTarget> {
|
|
256
|
+
checkModuleVersion(ctx);
|
|
257
|
+
const credential = needCredential(ctx);
|
|
258
|
+
const { base, label } = where();
|
|
259
|
+
const source = nexusSource(label, ctx.address);
|
|
260
|
+
const staged = (present: boolean): StagedTarget => ({ target: "nexus", destination: source, items: [item(ctx, present)], state: { repo: options.repo, url: base, version: ctx.version } });
|
|
261
|
+
|
|
262
|
+
const before = await held(ctx.address, ctx.version, ctx);
|
|
263
|
+
if (before) {
|
|
264
|
+
if (before.content !== ctx.content) throw versionHeldError(source, ctx, before);
|
|
265
|
+
ctx.log(`${source} ${ctx.version} is already in ${options.repo} with this content; nothing sent`);
|
|
266
|
+
return staged(true);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const zip = moduleZip(ctx.archive, ctx.content, ctx.revision);
|
|
270
|
+
const url = nexusUploadUrl(base, options.repo, ctx.address, ctx.version);
|
|
271
|
+
let res: Awaited<ReturnType<typeof put>>;
|
|
272
|
+
try {
|
|
273
|
+
res = await put(url, { method: "PUT", headers: { authorization: credential.header, "content-type": "application/zip" }, body: zip });
|
|
274
|
+
} catch (e) {
|
|
275
|
+
throw answerError(ctx, "publish-upload-failed", `PUT ${url}: ${(e as Error).message}`);
|
|
276
|
+
}
|
|
277
|
+
const text = await res.text().catch(() => "");
|
|
278
|
+
if (res.ok) {
|
|
279
|
+
ctx.log(`uploaded ${source} ${ctx.version} to ${options.repo}`);
|
|
280
|
+
return staged(false);
|
|
281
|
+
}
|
|
282
|
+
if (res.status === 400 || res.status === 403 || res.status === 409) {
|
|
283
|
+
// A deployment policy that refuses a redeploy, or another run uploading first: judge what the repository holds.
|
|
284
|
+
const now = await held(ctx.address, ctx.version, ctx).catch(() => undefined);
|
|
285
|
+
if (now && now.content === ctx.content) return staged(true);
|
|
286
|
+
if (now) throw versionHeldError(source, ctx, now);
|
|
287
|
+
}
|
|
288
|
+
if (res.status === 401 || res.status === 403) throw writeRefused(ctx, "Nexus", res.status, `${source} ${ctx.version}`, credential, names, fix);
|
|
289
|
+
throw answerError(
|
|
290
|
+
ctx,
|
|
291
|
+
"publish-upload-failed",
|
|
292
|
+
`Nexus answered ${res.status} to the upload of ${source} ${ctx.version}: ${text.slice(0, 300)}`,
|
|
293
|
+
`Hosted Terraform repositories need Nexus ${NEXUS_HOSTED_FLOOR} or later; check that ${options.repo} is a hosted Terraform repository with a signing key.`,
|
|
294
|
+
);
|
|
295
|
+
},
|
|
296
|
+
|
|
297
|
+
async reveal(ctx, staged) {
|
|
298
|
+
const listed = await waitListed(() => versions(ctx.address, ctx), ctx.version, wait);
|
|
299
|
+
if (!listed) {
|
|
300
|
+
throw answerError(
|
|
301
|
+
ctx,
|
|
302
|
+
"publish-reveal-missing",
|
|
303
|
+
`${staged.destination} lists no ${ctx.version} ${Math.round(wait.waitMs / 1000)}s after the upload`,
|
|
304
|
+
`Nexus before ${NEXUS_BEARER_FLOOR} can keep an earlier "not found" in its cache; run the publish again once it has expired.`,
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
const o = await readOptions(ctx);
|
|
308
|
+
const location = await read(ctx, () => registryLocation(staged.destination, ctx.version, o));
|
|
309
|
+
const got = await locationContent(ctx, get, location, where().nexusHost, o.authorization);
|
|
310
|
+
if (got.content !== ctx.content) throw revealMismatch(ctx, staged.destination, ctx.version, got.content, ctx.content);
|
|
311
|
+
return { target: staged.target, destination: staged.destination, items: staged.items };
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// chant#4605: the zip of an attested module archive keeps every file, path and mode, is the
|
|
2
|
+
// same bytes every time, and names the attested content in its archive comment.
|
|
3
|
+
import { gzipSync } from "node:zlib";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { PublishTargetError } from "@intentius/chant/lifecycle/release-publish";
|
|
6
|
+
import { archiveContent, contentDigest, heldContentOf, moduleArchiveEntries, moduleZip, readModuleZipComment, readZip } from "../index";
|
|
7
|
+
import { moduleTarball } from "./fake-http";
|
|
8
|
+
|
|
9
|
+
const FILES = {
|
|
10
|
+
"main.tf": 'resource "null_resource" "a" {}\n',
|
|
11
|
+
"variables.tf": 'variable "cidr" {}\n',
|
|
12
|
+
"modules/sub/main.tf": "# sub\n",
|
|
13
|
+
"README.md": "# vpc\n",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
describe("module zip (chant#4605)", () => {
|
|
17
|
+
it("carries every file of the tarball at its path with its bytes", () => {
|
|
18
|
+
const archive = moduleTarball(FILES);
|
|
19
|
+
const zip = readZip(moduleZip(archive, archiveContent(archive)));
|
|
20
|
+
const files = Object.fromEntries(zip.entries.filter((e) => !e.dir).map((e) => [e.path, e.data.toString("utf-8")]));
|
|
21
|
+
expect(files).toEqual(FILES);
|
|
22
|
+
expect(zip.entries.filter((e) => e.dir).map((e) => e.path)).toEqual(["modules", "modules/sub"]);
|
|
23
|
+
expect(zip.entries.find((e) => e.path === "main.tf")!.mode).toBe(0o644);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("gives the same bytes for the same archive", () => {
|
|
27
|
+
const archive = moduleTarball(FILES);
|
|
28
|
+
const content = archiveContent(archive);
|
|
29
|
+
expect(Buffer.compare(moduleZip(archive, content, "abc"), moduleZip(archive, content, "abc"))).toBe(0);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("names the attested content and revision in the archive comment", () => {
|
|
33
|
+
const archive = moduleTarball(FILES);
|
|
34
|
+
const content = archiveContent(archive);
|
|
35
|
+
const zip = moduleZip(archive, content, "3f2a9c1d");
|
|
36
|
+
expect(readModuleZipComment(readZip(zip).comment)).toEqual({ content, revision: "3f2a9c1d" });
|
|
37
|
+
expect(heldContentOf(zip, contentDigest)).toEqual({ content, revision: "3f2a9c1d" });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("judges a gzip tarball by its uncompressed bytes and any other bytes by their own digest", () => {
|
|
41
|
+
const archive = moduleTarball(FILES);
|
|
42
|
+
expect(heldContentOf(archive, contentDigest).content).toBe(archiveContent(archive));
|
|
43
|
+
const plain = Buffer.from("not an archive");
|
|
44
|
+
expect(heldContentOf(plain, contentDigest).content).toBe(contentDigest(plain));
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("cuts ./ from paths", () => {
|
|
48
|
+
const entries = moduleArchiveEntries(moduleTarball({ "./main.tf": "x" }));
|
|
49
|
+
expect(entries.map((e) => e.path)).toEqual(["main.tf"]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("refuses bytes that are not a gzip tarball", () => {
|
|
53
|
+
expect(() => moduleArchiveEntries(Buffer.from("plain"))).toThrow(PublishTargetError);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("refuses a path outside the archive's root", () => {
|
|
57
|
+
const header = Buffer.alloc(512);
|
|
58
|
+
header.write("../evil.tf", 0);
|
|
59
|
+
header.write("0000644\0", 100);
|
|
60
|
+
header.write("00000000000\0", 124);
|
|
61
|
+
header.write("0", 156);
|
|
62
|
+
const tar = gzipSync(Buffer.concat([header, Buffer.alloc(1024)]));
|
|
63
|
+
expect(() => moduleArchiveEntries(tar)).toThrow(/outside its root/);
|
|
64
|
+
});
|
|
65
|
+
});
|