@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,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a module sits in a Terraform module registry, and how a registry
|
|
3
|
+
* source (`<host>/<namespace>/<name>/<system>`) is split into those parts.
|
|
4
|
+
* Lifted from terragucci's `publish/registry.ts` (18f6431595) for chant#4565.
|
|
5
|
+
* No Node imports, so an edge function can carry it.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const DISCOVERY = ".well-known/terraform.json";
|
|
9
|
+
export const MODULES_V1 = "/v1/modules/";
|
|
10
|
+
export const DEFAULT_SYSTEM = "generic";
|
|
11
|
+
|
|
12
|
+
/** A registry namespace or module name: letters, digits, `-` and `_`, starting and ending on a letter or digit. */
|
|
13
|
+
export const REGISTRY_NAME = /^[0-9A-Za-z](?:[0-9A-Za-z_-]{0,62}[0-9A-Za-z])?$/;
|
|
14
|
+
/** A registry module's system (its target provider): lower-case letters and digits. */
|
|
15
|
+
export const REGISTRY_SYSTEM = /^[0-9a-z]{1,64}$/;
|
|
16
|
+
|
|
17
|
+
/** Where a module is in the registry. */
|
|
18
|
+
export interface RegistryAddress {
|
|
19
|
+
namespace: string;
|
|
20
|
+
name: string;
|
|
21
|
+
system: string;
|
|
22
|
+
/** `<host>/<namespace>/<name>/<system>`, the source a root writes. */
|
|
23
|
+
source: string;
|
|
24
|
+
/** The module's directory under the registry root: `v1/modules/<namespace>/<name>/<system>`. */
|
|
25
|
+
path: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** What placing a module needs from a registry's settings. */
|
|
29
|
+
export interface RegistryPlacement {
|
|
30
|
+
/** The registry's base URL; its host is what sources name. */
|
|
31
|
+
url: string;
|
|
32
|
+
namespace: string;
|
|
33
|
+
/** A namespace per path prefix, the longest prefix winning. */
|
|
34
|
+
namespaces?: Record<string, string>;
|
|
35
|
+
system?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** A module name the protocol cannot carry. */
|
|
39
|
+
export class RegistryNameError extends Error {
|
|
40
|
+
constructor(message: string) {
|
|
41
|
+
super(message);
|
|
42
|
+
this.name = "RegistryNameError";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The host a registry's sources name: its URL without the scheme, lower-cased. */
|
|
47
|
+
export const registryHost = (reg: Pick<RegistryPlacement, "url">): string => new URL(reg.url).host.toLowerCase();
|
|
48
|
+
|
|
49
|
+
/** The namespace a module's path goes under: the longest `namespaces` prefix it starts with, else `namespace`. */
|
|
50
|
+
export function namespaceFor(reg: Pick<RegistryPlacement, "namespace" | "namespaces">, rel: string): string {
|
|
51
|
+
let best: { prefix: string; ns: string } | undefined;
|
|
52
|
+
for (const [prefix, ns] of Object.entries(reg.namespaces ?? {})) {
|
|
53
|
+
const p = prefix.replace(/^\.\//, "");
|
|
54
|
+
const dir = p.endsWith("/") ? p : `${p}/`;
|
|
55
|
+
if ((rel.startsWith(dir) || rel === p.replace(/\/+$/, "")) && (!best || p.length > best.prefix.length)) best = { prefix: p, ns };
|
|
56
|
+
}
|
|
57
|
+
return best?.ns ?? reg.namespace;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** A module's place in the registry: its namespace, its name (the last segment of its path) and its system. */
|
|
61
|
+
export function registryAddress(reg: RegistryPlacement, rel: string): RegistryAddress {
|
|
62
|
+
const namespace = namespaceFor(reg, rel);
|
|
63
|
+
const name = rel.replace(/\/+$/, "").split("/").pop() ?? "";
|
|
64
|
+
const system = reg.system ?? DEFAULT_SYSTEM;
|
|
65
|
+
if (!REGISTRY_NAME.test(name)) throw new RegistryNameError(`${rel}: a registry module's name is letters, digits, - and _, and ${JSON.stringify(name)} is not; rename the directory`);
|
|
66
|
+
return { namespace, name, system, source: `${registryHost(reg)}/${namespace}/${name}/${system}`, path: `v1/modules/${namespace}/${name}/${system}` };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** A registry source split into its parts. */
|
|
70
|
+
export interface RegistrySource {
|
|
71
|
+
host: string;
|
|
72
|
+
namespace: string;
|
|
73
|
+
name: string;
|
|
74
|
+
system: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* A registry source split into its host and module, or undefined when the
|
|
79
|
+
* source is not one: `host/ns/name/system`, with an optional `//subdir` and
|
|
80
|
+
* query. The host must look like one (a dot, a port, or `localhost`), so a
|
|
81
|
+
* three-part public registry source with no host is not taken for one.
|
|
82
|
+
* `hosts` names bare hosts a CLI `host` block declares (a Nexus repository's
|
|
83
|
+
* name, chant#4605), which are taken as hosts too.
|
|
84
|
+
*/
|
|
85
|
+
export function parseRegistrySource(source: string, hosts: readonly string[] = []): RegistrySource | undefined {
|
|
86
|
+
const base = source.split("?")[0]!.split("//")[0]!;
|
|
87
|
+
const m = /^([a-z0-9.-]+(?::\d+)?)\/([^/]+)\/([^/]+)\/([^/]+)$/i.exec(base);
|
|
88
|
+
const known = (h: string) => hosts.some((k) => k.toLowerCase() === h.toLowerCase());
|
|
89
|
+
if (!m || (!m[1]!.includes(".") && !m[1]!.includes(":") && m[1] !== "localhost" && !known(m[1]!))) return undefined;
|
|
90
|
+
return { host: m[1]!.toLowerCase(), namespace: m[2]!, name: m[3]!, system: m[4]! };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** A key under a registry's prefix in its bucket or directory. */
|
|
94
|
+
export const keyFor = (prefix: string | undefined, path: string): string => {
|
|
95
|
+
const p = (prefix ?? "").replace(/^\/+|\/+$/g, "");
|
|
96
|
+
return p ? `${p}/${path}` : path;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/** The tarball's name beside its version. */
|
|
100
|
+
export const tarballName = (addr: Pick<RegistryAddress, "name">, version: string): string => `${addr.name}-${version}.tar.gz`;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading a registry's release catalog (chant#4599, terragucci#1289 bundle
|
|
3
|
+
* B): `catalog/index.json` beside the registry's files, the document
|
|
4
|
+
* `renderCatalogSite` writes (`@intentius/chant/lifecycle/release-catalog-site`).
|
|
5
|
+
*
|
|
6
|
+
* The request carries the token the CLIs would send the host (chant#4563,
|
|
7
|
+
* `readToken` in ./reader.ts), so a registry behind the edge function's
|
|
8
|
+
* `auth: token` check answers it. Per ruling B (2026-10-27) this reader is
|
|
9
|
+
* how a token-protected catalog is read; the edge function gains no cookie
|
|
10
|
+
* and no open path, and a browser reaches the catalog over the
|
|
11
|
+
* network-private path.
|
|
12
|
+
*/
|
|
13
|
+
import { credentialsHost, tokenVariable } from "@intentius/chant/terraform/credentials";
|
|
14
|
+
import { parseReleaseCatalog, ReleaseCatalogError, type ReleaseCatalog } from "@intentius/chant/lifecycle/release-catalog";
|
|
15
|
+
import { readToken, RegistryError, type RegistryFetch, type RegistryReadOptions } from "./reader";
|
|
16
|
+
|
|
17
|
+
/** Where the catalog sits under a registry's root. */
|
|
18
|
+
export const CATALOG_PATH = "catalog/index.json";
|
|
19
|
+
|
|
20
|
+
/** The catalog URL for a registry: a host (`modules.acme.dev`) or a base URL (`https://modules.acme.dev/` or `https://cdn.acme.dev/registry/`). */
|
|
21
|
+
export function catalogUrl(registry: string, scheme: "https" | "http" = "https"): string {
|
|
22
|
+
const base = registry.includes("://") ? registry : `${scheme}://${registry}`;
|
|
23
|
+
return new URL(CATALOG_PATH, base.endsWith("/") ? base : `${base}/`).toString();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Fetch and validate a registry's catalog. Throws `RegistryError` when the
|
|
28
|
+
* request fails or is refused (naming where a token goes, never a value),
|
|
29
|
+
* and when the answer is not a valid catalog. A registry with no catalog
|
|
30
|
+
* (404) gives null.
|
|
31
|
+
*/
|
|
32
|
+
export async function readRegistryCatalog(registry: string, options: RegistryReadOptions = {}): Promise<ReleaseCatalog | null> {
|
|
33
|
+
const url = catalogUrl(registry, options.scheme);
|
|
34
|
+
const host = new URL(url).host.toLowerCase();
|
|
35
|
+
const token = readToken(host, options);
|
|
36
|
+
const fetchFn: RegistryFetch = options.fetch ?? ((u, init) => fetch(u, init));
|
|
37
|
+
let res: Response;
|
|
38
|
+
try {
|
|
39
|
+
res = await fetchFn(url, { method: "GET", redirect: "follow", headers: { Accept: "application/json", ...(token ? { Authorization: `Bearer ${token}` } : {}) } });
|
|
40
|
+
} catch (e) {
|
|
41
|
+
throw new RegistryError(`${url}: ${(e as Error).message}`);
|
|
42
|
+
}
|
|
43
|
+
if (res.status === 404) return null;
|
|
44
|
+
if (res.status === 401 || res.status === 403) {
|
|
45
|
+
const variable = tokenVariable(host);
|
|
46
|
+
const where = variable ? `${variable} or a credentials "${credentialsHost(host)}" block` : `a credentials "${credentialsHost(host) ?? host}" block in credentials.tfrc.json`;
|
|
47
|
+
throw new RegistryError(token ? `${url} answered ${res.status}: the registry refused the token for ${host}` : `${url} answered ${res.status}: the catalog wants a token; set ${where}`);
|
|
48
|
+
}
|
|
49
|
+
if (!res.ok) throw new RegistryError(`${url} answered ${res.status}`);
|
|
50
|
+
try {
|
|
51
|
+
return parseReleaseCatalog(await res.text());
|
|
52
|
+
} catch (e) {
|
|
53
|
+
if (e instanceof ReleaseCatalogError) throw new RegistryError(`${url} is not a release catalog: ${e.problems.slice(0, 3).join("; ")}`);
|
|
54
|
+
throw e;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The deprecated-version check against a fixture catalog, the catalog reader
|
|
3
|
+
* with the registry's token (chant#4599), and the handler's page types.
|
|
4
|
+
*/
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { describe, expect, test } from "vitest";
|
|
7
|
+
import { buildReleaseCatalog } from "@intentius/chant/lifecycle/release-catalog";
|
|
8
|
+
import type { ReleaseRecord } from "@intentius/chant/lifecycle/release-ledger";
|
|
9
|
+
import { moduleConsumers } from "../pin/consumers";
|
|
10
|
+
import { catalogUrl, readRegistryCatalog } from "./catalog-read";
|
|
11
|
+
import { deprecatedPins } from "./deprecated";
|
|
12
|
+
import { registryContentType, registryHandler } from "./handler";
|
|
13
|
+
import { RegistryError } from "./reader";
|
|
14
|
+
|
|
15
|
+
const REPO = join(import.meta.dirname, "..", "pin", "__fixtures__", "consumers");
|
|
16
|
+
|
|
17
|
+
const rec = (releaseVersion: string, digest: string, timestamp: string): ReleaseRecord => ({
|
|
18
|
+
version: 1,
|
|
19
|
+
component: "acme/vpc/aws",
|
|
20
|
+
env: "registry",
|
|
21
|
+
digest,
|
|
22
|
+
releaseVersion,
|
|
23
|
+
gitSha: "a".repeat(40),
|
|
24
|
+
runId: "r",
|
|
25
|
+
timestamp,
|
|
26
|
+
actor: "ci",
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const catalog = buildReleaseCatalog(
|
|
30
|
+
{
|
|
31
|
+
releases: [rec("1.0.0", "sha256:v1", "2026-10-01T00:00:00.000Z"), rec("1.1.0", "sha256:v11", "2026-10-02T00:00:00.000Z"), rec("2.0.0", "sha256:v2", "2026-10-03T00:00:00.000Z")],
|
|
32
|
+
statuses: [
|
|
33
|
+
{ component: "acme/vpc/aws", digest: "sha256:v1", status: "deprecated", reason: "drops flow logs", replacement: "1.1.0", actor: "ana", timestamp: "2026-10-04T00:00:00.000Z" },
|
|
34
|
+
{ component: "acme/vpc/aws", digest: "sha256:v2", status: "yanked", reason: "breaks routes", replacement: "1.1.0", actor: "ana", timestamp: "2026-10-04T00:00:00.000Z" },
|
|
35
|
+
],
|
|
36
|
+
fields: [{ component: "acme/vpc/aws", engine: "terraform", fields: { address: "registry.acme.dev/acme/vpc/aws" } }],
|
|
37
|
+
},
|
|
38
|
+
{ generatedAt: "2026-10-27T00:00:00.000Z" },
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
describe("deprecatedPins", () => {
|
|
42
|
+
test("warns on a deprecated pin, refuses a yanked one, and passes an active one", async () => {
|
|
43
|
+
const consumers = await moduleConsumers(REPO, ["live/prod", "live/dev", "units/vpc"]);
|
|
44
|
+
const { warnings, refusals } = deprecatedPins(consumers, catalog);
|
|
45
|
+
expect(warnings.map((w) => [w.consumer.root, w.version, w.replacement])).toEqual([["live/prod", "1.0.0", "1.1.0"]]);
|
|
46
|
+
expect(warnings[0]!.message).toBe("live/prod/main.tf module.vpc pins acme/vpc/aws 1.0.0, which is deprecated: drops flow logs; move to 1.1.0");
|
|
47
|
+
// live/dev pins v2.0.0: the leading v is ignored, and its host's case too.
|
|
48
|
+
expect(refusals.map((r) => [r.consumer.root, r.version, r.status])).toEqual([["live/dev", "2.0.0", "yanked"]]);
|
|
49
|
+
expect(refusals[0]!.message).toContain("which is yanked: breaks routes; move to 1.1.0");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("leaves unpinned calls and modules the catalog does not list alone", async () => {
|
|
53
|
+
const consumers = await moduleConsumers(REPO, ["live/prod"]);
|
|
54
|
+
const { warnings, refusals } = deprecatedPins(consumers.filter((c) => c.address !== "module.vpc"), catalog);
|
|
55
|
+
expect(warnings).toEqual([]);
|
|
56
|
+
expect(refusals).toEqual([]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("matches a component by name when it has no terraform address, and a pin by digest", () => {
|
|
60
|
+
const byName = buildReleaseCatalog(
|
|
61
|
+
{
|
|
62
|
+
releases: [{ ...rec("3.1.0", "sha256:c31", "2026-10-01T00:00:00.000Z"), component: "oci://registry.acme.dev/modules/cache", releaseVersion: undefined }],
|
|
63
|
+
statuses: [{ component: "oci://registry.acme.dev/modules/cache", digest: "sha256:c31", status: "yanked", actor: "a", timestamp: "2026-10-02T00:00:00.000Z" }],
|
|
64
|
+
},
|
|
65
|
+
{ generatedAt: "2026-10-27T00:00:00.000Z" },
|
|
66
|
+
);
|
|
67
|
+
const call = { root: "r", file: "r/main.tf", address: "module.cache", kind: "terraform" as const, source: "oci://registry.acme.dev/modules/cache?digest=sha256:c31", module: "oci://registry.acme.dev/modules/cache", pin: "sha256:c31", at: "source" as const, param: "digest" as const };
|
|
68
|
+
const { refusals } = deprecatedPins([call], byName);
|
|
69
|
+
expect(refusals.map((r) => [r.version, r.reason])).toEqual([["sha256:c31", null]]);
|
|
70
|
+
expect(refusals[0]!.message).toBe("r/main.tf module.cache pins oci://registry.acme.dev/modules/cache sha256:c31, which is yanked");
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("readRegistryCatalog", () => {
|
|
75
|
+
const body = JSON.stringify(catalog);
|
|
76
|
+
|
|
77
|
+
test("sends the registry's token and validates the answer", async () => {
|
|
78
|
+
const seen: Array<{ url: string; auth: string | null }> = [];
|
|
79
|
+
const got = await readRegistryCatalog("modules.acme.dev", {
|
|
80
|
+
token: "t0k",
|
|
81
|
+
fetch: async (url, init) => {
|
|
82
|
+
seen.push({ url, auth: new Headers(init?.headers).get("authorization") });
|
|
83
|
+
return new Response(body, { status: 200, headers: { "content-type": "application/json" } });
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
expect(seen).toEqual([{ url: "https://modules.acme.dev/catalog/index.json", auth: "Bearer t0k" }]);
|
|
87
|
+
expect(got).toEqual(catalog);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("reads the token the CLIs would send", async () => {
|
|
91
|
+
let auth: string | null = null;
|
|
92
|
+
await readRegistryCatalog("modules.acme.dev", {
|
|
93
|
+
credentials: { env: { TF_TOKEN_modules_acme_dev: "from-env" } },
|
|
94
|
+
fetch: async (_url, init) => ((auth = new Headers(init?.headers).get("authorization")), new Response(body)),
|
|
95
|
+
});
|
|
96
|
+
expect(auth).toBe("Bearer from-env");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("a registry with no catalog gives null; a refusal names where the token goes and never a value", async () => {
|
|
100
|
+
expect(await readRegistryCatalog("https://cdn.acme.dev/registry", { token: false, fetch: async () => new Response("", { status: 404 }) })).toBeNull();
|
|
101
|
+
const refused = readRegistryCatalog("modules.acme.dev", { token: false, fetch: async () => new Response("", { status: 401 }) });
|
|
102
|
+
await expect(refused).rejects.toBeInstanceOf(RegistryError);
|
|
103
|
+
await expect(readRegistryCatalog("modules.acme.dev", { token: "secret-value", fetch: async () => new Response("", { status: 403 }) })).rejects.toThrow(/refused the token for modules\.acme\.dev/);
|
|
104
|
+
await expect(readRegistryCatalog("modules.acme.dev", { token: "secret-value", fetch: async () => new Response("", { status: 403 }) })).rejects.not.toThrow(/secret-value/);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("an answer that is not a catalog is an error", async () => {
|
|
108
|
+
await expect(readRegistryCatalog("modules.acme.dev", { token: false, fetch: async () => new Response("{\"contract\":1}") })).rejects.toThrow(/is not a release catalog/);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("catalogUrl takes a host or a base URL", () => {
|
|
112
|
+
expect(catalogUrl("modules.acme.dev")).toBe("https://modules.acme.dev/catalog/index.json");
|
|
113
|
+
expect(catalogUrl("https://cdn.acme.dev/registry")).toBe("https://cdn.acme.dev/registry/catalog/index.json");
|
|
114
|
+
expect(catalogUrl("localhost:8080", "http")).toBe("http://localhost:8080/catalog/index.json");
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("the handler serves catalog pages so a browser renders them", () => {
|
|
119
|
+
test.each([
|
|
120
|
+
["catalog/index.html", "text/html; charset=utf-8"],
|
|
121
|
+
["catalog/style.css", "text/css; charset=utf-8"],
|
|
122
|
+
["catalog/search.js", "text/javascript; charset=utf-8"],
|
|
123
|
+
["catalog/logo.SVG", "image/svg+xml"],
|
|
124
|
+
["catalog/index.json", "application/json"],
|
|
125
|
+
["v1/modules/acme/vpc/aws/versions", "application/json"],
|
|
126
|
+
["vpc-1.0.0.tar.gz", "application/gzip"],
|
|
127
|
+
["catalog.d/README", "application/octet-stream"],
|
|
128
|
+
["providers/x.zip", "application/octet-stream"],
|
|
129
|
+
])("%s is %s", (key, type) => {
|
|
130
|
+
expect(registryContentType(key)).toBe(type);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("on a request", async () => {
|
|
134
|
+
const handler = registryHandler(async (key) => (key === "catalog/index.html" ? { body: "<!doctype html>" } : {}));
|
|
135
|
+
const res = await handler(new Request("https://modules.acme.dev/catalog/index.html"));
|
|
136
|
+
expect(res.status).toBe(200);
|
|
137
|
+
expect(res.headers.get("content-type")).toBe("text/html; charset=utf-8");
|
|
138
|
+
});
|
|
139
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The deprecated-version check (chant#4599, terragucci#1289 bundle B): each
|
|
3
|
+
* module call (`moduleConsumers`, ../pin/consumers.ts) against the release
|
|
4
|
+
* catalog (`readRegistryCatalog`, ./catalog-read.ts). A call pinned to a
|
|
5
|
+
* deprecated version gets a warning, and one pinned to a yanked version a
|
|
6
|
+
* refusal, each naming the reason and the version to move to when the
|
|
7
|
+
* catalog has one.
|
|
8
|
+
*
|
|
9
|
+
* A catalog component is matched by the module address its `terraform`
|
|
10
|
+
* fields name (`terraformCatalogFields`, ./docs.ts), else by its name, both
|
|
11
|
+
* normalised as the scan normalises a call's source. A call's pin is matched
|
|
12
|
+
* to a version by the version (a leading `v` ignored) or by the digest. A
|
|
13
|
+
* call with no pin, or one naming a module or version the catalog does not
|
|
14
|
+
* list, is left alone: this check speaks only for what the catalog says.
|
|
15
|
+
*/
|
|
16
|
+
import type { CatalogComponent, CatalogVersion, ReleaseCatalog } from "@intentius/chant/lifecycle/release-catalog";
|
|
17
|
+
import { normalizeModuleSource, type ModuleConsumer } from "../pin/consumers";
|
|
18
|
+
|
|
19
|
+
/** One call on a version the catalog marks. */
|
|
20
|
+
export interface DeprecatedPin {
|
|
21
|
+
consumer: ModuleConsumer;
|
|
22
|
+
component: string;
|
|
23
|
+
/** The version as the catalog lists it (its version, or its digest when it has none). */
|
|
24
|
+
version: string;
|
|
25
|
+
status: "deprecated" | "yanked";
|
|
26
|
+
reason: string | null;
|
|
27
|
+
replacement: string | null;
|
|
28
|
+
/** One line for a person: where, what, why, and what to move to. */
|
|
29
|
+
message: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The check's result: warnings for deprecated pins, refusals for yanked ones. */
|
|
33
|
+
export interface DeprecatedPinsResult {
|
|
34
|
+
warnings: DeprecatedPin[];
|
|
35
|
+
refusals: DeprecatedPin[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const bare = (v: string): string => v.replace(/^v(?=\d)/, "");
|
|
39
|
+
|
|
40
|
+
function addressOf(c: CatalogComponent): string {
|
|
41
|
+
const tf = c.fields.terraform as { address?: unknown } | undefined;
|
|
42
|
+
return normalizeModuleSource(typeof tf?.address === "string" ? tf.address : c.name);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function versionFor(c: CatalogComponent, pin: string): CatalogVersion | undefined {
|
|
46
|
+
return c.versions.find((v) => (v.version !== null && bare(v.version) === bare(pin)) || v.digest === pin);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Check every call against the catalog. */
|
|
50
|
+
export function deprecatedPins(consumers: readonly ModuleConsumer[], catalog: ReleaseCatalog): DeprecatedPinsResult {
|
|
51
|
+
const byAddress = new Map<string, CatalogComponent>();
|
|
52
|
+
for (const c of catalog.components) byAddress.set(addressOf(c), c);
|
|
53
|
+
const out: DeprecatedPinsResult = { warnings: [], refusals: [] };
|
|
54
|
+
for (const consumer of consumers) {
|
|
55
|
+
if (consumer.pin === null) continue;
|
|
56
|
+
const component = byAddress.get(consumer.module);
|
|
57
|
+
if (!component) continue;
|
|
58
|
+
const v = versionFor(component, consumer.pin);
|
|
59
|
+
if (!v || v.status.state === "active") continue;
|
|
60
|
+
const version = v.version ?? v.digest;
|
|
61
|
+
const said = [`${consumer.file} ${consumer.address} pins ${component.name} ${version}, which is ${v.status.state}`];
|
|
62
|
+
if (v.status.reason) said.push(`: ${v.status.reason}`);
|
|
63
|
+
if (v.status.replacement) said.push(`; move to ${v.status.replacement}`);
|
|
64
|
+
const finding: DeprecatedPin = {
|
|
65
|
+
consumer,
|
|
66
|
+
component: component.name,
|
|
67
|
+
version,
|
|
68
|
+
status: v.status.state,
|
|
69
|
+
reason: v.status.reason,
|
|
70
|
+
replacement: v.status.replacement,
|
|
71
|
+
message: said.join(""),
|
|
72
|
+
};
|
|
73
|
+
(v.status.state === "yanked" ? out.refusals : out.warnings).push(finding);
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The module docs extractor (chant#4598): a module without a terraform-docs
|
|
3
|
+
* config, one with it (runner faked: found, no binary, failing, and no
|
|
4
|
+
* runner at all), one written as `.tf.json`, and one with `examples/`; then
|
|
5
|
+
* the catalog fields and site sections built from them.
|
|
6
|
+
*/
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { describe, expect, test } from "vitest";
|
|
9
|
+
import { buildReleaseCatalog } from "@intentius/chant/lifecycle/release-catalog";
|
|
10
|
+
import { renderCatalogSite } from "@intentius/chant/lifecycle/release-catalog-site";
|
|
11
|
+
import { moduleDocs, terraformCatalogFields, terraformCatalogSections, type TerraformDocsRunner } from "./docs";
|
|
12
|
+
|
|
13
|
+
const FIXTURES = join(import.meta.dirname, "__fixtures__", "docs");
|
|
14
|
+
|
|
15
|
+
describe("moduleDocs without a terraform-docs config", () => {
|
|
16
|
+
test("reads inputs, outputs, providers and the README from the HCL", async () => {
|
|
17
|
+
const docs = await moduleDocs(join(FIXTURES, "vpc"));
|
|
18
|
+
expect(docs.docsFrom).toBe("hcl");
|
|
19
|
+
expect(docs.config).toBeNull();
|
|
20
|
+
expect(docs.note).toBeNull();
|
|
21
|
+
expect(docs.markdown).toBeNull();
|
|
22
|
+
expect(docs.readme).toContain("A VPC with flow logs.");
|
|
23
|
+
expect(docs.inputs).toEqual([
|
|
24
|
+
{ name: "cidr", type: "string", required: true, description: "The VPC's IPv4 CIDR block.", sensitive: false, file: "variables.tf" },
|
|
25
|
+
{ name: "flow_log_key", type: "string", required: false, description: null, sensitive: true, file: "variables.tf" },
|
|
26
|
+
{ name: "tags", type: "map(string)", required: false, default: {}, description: "Tags for every resource.", sensitive: false, file: "variables.tf" },
|
|
27
|
+
]);
|
|
28
|
+
expect(docs.outputs).toEqual([
|
|
29
|
+
{ name: "arn", description: null, sensitive: true, file: "outputs.tf" },
|
|
30
|
+
{ name: "vpc_id", description: "The VPC's id.", sensitive: false, file: "outputs.tf" },
|
|
31
|
+
]);
|
|
32
|
+
expect(docs.providers).toEqual([{ name: "aws", source: "hashicorp/aws", version: ">= 5.0" }]);
|
|
33
|
+
expect(docs.requiredVersion).toBe(">= 1.5");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("never carries a sensitive default", async () => {
|
|
37
|
+
const docs = await moduleDocs(join(FIXTURES, "vpc"));
|
|
38
|
+
expect(JSON.stringify(docs)).not.toContain("not-a-real-key");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("a runner is not called when the module has no config", async () => {
|
|
42
|
+
let called = false;
|
|
43
|
+
const docs = await moduleDocs(join(FIXTURES, "vpc"), { terraformDocs: async () => ((called = true), "x") });
|
|
44
|
+
expect(called).toBe(false);
|
|
45
|
+
expect(docs.docsFrom).toBe("hcl");
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("moduleDocs with examples/", () => {
|
|
50
|
+
test("lists each example with its README and files", async () => {
|
|
51
|
+
const docs = await moduleDocs(join(FIXTURES, "vpc"));
|
|
52
|
+
expect(docs.examples).toEqual([
|
|
53
|
+
{ name: "complete", path: "examples/complete", readme: null, files: ["main.tf"] },
|
|
54
|
+
{ name: "simple", path: "examples/simple", readme: "# Simple\n", files: ["main.tf"] },
|
|
55
|
+
]);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("moduleDocs with a .terraform-docs.yml", () => {
|
|
60
|
+
const dir = join(FIXTURES, "documented");
|
|
61
|
+
|
|
62
|
+
test("runs terraform-docs with that file and keeps its markdown", async () => {
|
|
63
|
+
const calls: Array<{ dir: string; config: string }> = [];
|
|
64
|
+
const runner: TerraformDocsRunner = async (args) => {
|
|
65
|
+
calls.push(args);
|
|
66
|
+
return "## Inputs\n\n| Name | Type |\n|---|---|\n| name | string |\n";
|
|
67
|
+
};
|
|
68
|
+
const docs = await moduleDocs(dir, { terraformDocs: runner });
|
|
69
|
+
expect(calls).toEqual([{ dir, config: ".terraform-docs.yml" }]);
|
|
70
|
+
expect(docs.docsFrom).toBe("terraform-docs");
|
|
71
|
+
expect(docs.markdown).toContain("| name | string |");
|
|
72
|
+
expect(docs.config).toBe(".terraform-docs.yml");
|
|
73
|
+
expect(docs.inputs.map((i) => i.name)).toEqual(["name"]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("falls back to the HCL when the binary is missing, and says so", async () => {
|
|
77
|
+
const docs = await moduleDocs(dir, { terraformDocs: async () => null });
|
|
78
|
+
expect(docs.docsFrom).toBe("hcl");
|
|
79
|
+
expect(docs.markdown).toBeNull();
|
|
80
|
+
expect(docs.note).toBe(".terraform-docs.yml found, and no terraform-docs binary is installed");
|
|
81
|
+
expect(docs.inputs.map((i) => i.name)).toEqual(["name"]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("falls back when the runner fails", async () => {
|
|
85
|
+
const docs = await moduleDocs(dir, { terraformDocs: async () => { throw new Error("exit 1: bad config\nmore"); } });
|
|
86
|
+
expect(docs.docsFrom).toBe("hcl");
|
|
87
|
+
expect(docs.note).toBe("terraform-docs failed with .terraform-docs.yml: exit 1: bad config");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("falls back when no runner is given", async () => {
|
|
91
|
+
const docs = await moduleDocs(dir);
|
|
92
|
+
expect(docs.docsFrom).toBe("hcl");
|
|
93
|
+
expect(docs.note).toBe(".terraform-docs.yml found, and no terraform-docs runner was given");
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("moduleDocs on .tf.json files", () => {
|
|
98
|
+
test("reads the JSON syntax the same way", async () => {
|
|
99
|
+
const docs = await moduleDocs(join(FIXTURES, "json"));
|
|
100
|
+
expect(docs.inputs).toEqual([
|
|
101
|
+
{ name: "project", type: "string", required: true, description: "The project id.", sensitive: false, file: "main.tf.json" },
|
|
102
|
+
{ name: "region", type: "string", required: false, default: "us-central1", description: null, sensitive: false, file: "main.tf.json" },
|
|
103
|
+
]);
|
|
104
|
+
expect(docs.outputs).toEqual([{ name: "name", description: "The project id, echoed.", sensitive: false, file: "main.tf.json" }]);
|
|
105
|
+
expect(docs.providers).toEqual([{ name: "google", source: "hashicorp/google", version: "~> 6.0" }]);
|
|
106
|
+
expect(docs.readme).toBeNull();
|
|
107
|
+
expect(docs.examples).toEqual([]);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe("terraformCatalogFields and terraformCatalogSections", () => {
|
|
112
|
+
test("fill the catalog's terraform key and render on the version page", async () => {
|
|
113
|
+
const docs = await moduleDocs(join(FIXTURES, "vpc"));
|
|
114
|
+
const address = "registry.acme.dev/acme/vpc/aws";
|
|
115
|
+
const fields = terraformCatalogFields(docs, address);
|
|
116
|
+
expect(fields).toMatchObject({ address, docsFrom: "hcl", requiredVersion: ">= 1.5" });
|
|
117
|
+
expect((fields.inputs as Array<Record<string, unknown>>)[0]).not.toHaveProperty("file");
|
|
118
|
+
|
|
119
|
+
const catalog = buildReleaseCatalog(
|
|
120
|
+
{
|
|
121
|
+
releases: [{ version: 1, component: "acme/vpc/aws", env: "registry", digest: "sha256:v1", releaseVersion: "1.2.0", gitSha: "a".repeat(40), runId: "r", timestamp: "2026-10-01T00:00:00.000Z", actor: "ci" }],
|
|
122
|
+
fields: [
|
|
123
|
+
{ component: "acme/vpc/aws", engine: "terraform", fields: { address } },
|
|
124
|
+
{ component: "acme/vpc/aws", version: "1.2.0", engine: "terraform", fields },
|
|
125
|
+
],
|
|
126
|
+
},
|
|
127
|
+
{ generatedAt: "2026-10-27T00:00:00.000Z" },
|
|
128
|
+
);
|
|
129
|
+
const files = renderCatalogSite(catalog, terraformCatalogSections());
|
|
130
|
+
const page = new TextDecoder().decode(files.find((f) => f.path === "acme/vpc/aws/1.2.0/index.html")!.bytes);
|
|
131
|
+
expect(page).toContain("<h2>Inputs</h2>");
|
|
132
|
+
expect(page).toContain("<td>tags</td><td>map(string)</td><td>{}</td><td>no</td>");
|
|
133
|
+
expect(page).toContain("<td>flow_log_key</td><td>string</td><td>(sensitive)</td>");
|
|
134
|
+
expect(page).toContain("version = "1.2.0"");
|
|
135
|
+
expect(page).toContain("<h2>Requirements</h2>");
|
|
136
|
+
expect(page).toContain("<td>hashicorp/aws</td>");
|
|
137
|
+
const component = new TextDecoder().decode(files.find((f) => f.path === "acme/vpc/aws/index.html")!.bytes);
|
|
138
|
+
expect(component).toContain("source = "registry.acme.dev/acme/vpc/aws"");
|
|
139
|
+
});
|
|
140
|
+
});
|