@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,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Variables read from another root's state (#4545, Atmos reads moved from
|
|
3
|
+
* terragucci's `atmos.ts`): an Atmos stack that sets a var with
|
|
4
|
+
* `!terraform.state <component> [<stack>] <output>` plans on the upstream
|
|
5
|
+
* instance's output, which only the job that holds a credential can read.
|
|
6
|
+
*
|
|
7
|
+
* In the `terraform-apply` step a read is `{ var, root, output }`: the var it
|
|
8
|
+
* sets, the `terraform.roots` entry whose state holds the value (its
|
|
9
|
+
* directory and workspace come from that entry), and the output followed by
|
|
10
|
+
* the keys into its value (`.subnets.private` is `["subnets", "private"]`).
|
|
11
|
+
* The step reads each upstream's `output -json` once and passes the values
|
|
12
|
+
* as `-var`, under the step's own `vars`, which win. A read whose upstream
|
|
13
|
+
* has no state, or no such output yet, waits: the root is not planned on a
|
|
14
|
+
* stand-in, and the wave says which upstream to apply first.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** One var a root reads from another root's state. */
|
|
18
|
+
export interface TerraformStateRead {
|
|
19
|
+
/** The variable it sets. */
|
|
20
|
+
var: string;
|
|
21
|
+
/** The `terraform.roots` entry whose state holds the value. */
|
|
22
|
+
root: string;
|
|
23
|
+
/** The output, then the keys into its value. */
|
|
24
|
+
output: string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** What filling a root's reads came to. */
|
|
28
|
+
export interface FilledStateReads {
|
|
29
|
+
/** The values, by var. Complete only when `waiting` and `errors` are empty. */
|
|
30
|
+
vars: Record<string, unknown>;
|
|
31
|
+
/** Reads whose upstream has no such value yet: the root waits for these upstreams to apply. */
|
|
32
|
+
waiting: Array<{ read: TerraformStateRead; why: string }>;
|
|
33
|
+
/** Upstreams whose state could not be read at all. */
|
|
34
|
+
errors: string[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** What is wrong with a step's `reads`, empty when nothing is. */
|
|
38
|
+
export function stateReadsProblems(reads: unknown): string[] {
|
|
39
|
+
if (reads === undefined) return [];
|
|
40
|
+
if (!Array.isArray(reads)) return ["reads must be a list of { var, root, output }"];
|
|
41
|
+
const problems: string[] = [];
|
|
42
|
+
const vars = new Set<string>();
|
|
43
|
+
for (const [i, r] of reads.entries()) {
|
|
44
|
+
const read = r as Partial<TerraformStateRead> | null;
|
|
45
|
+
if (!read || typeof read !== "object") {
|
|
46
|
+
problems.push(`reads[${i}] must be { var, root, output }`);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (typeof read.var !== "string" || read.var === "") problems.push(`reads[${i}].var must name a variable`);
|
|
50
|
+
else if (vars.has(read.var)) problems.push(`reads[${i}] sets ${read.var}, which an earlier read sets too`);
|
|
51
|
+
else vars.add(read.var);
|
|
52
|
+
if (typeof read.root !== "string" || read.root === "") problems.push(`reads[${i}].root must name a terraform.roots entry`);
|
|
53
|
+
if (!Array.isArray(read.output) || read.output.length === 0 || !read.output.every((k) => typeof k === "string" && k !== "")) problems.push(`reads[${i}].output must be the output's name, then any keys into its value`);
|
|
54
|
+
}
|
|
55
|
+
return problems;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The value a read names in its upstream's outputs, or undefined when the outputs do not have it. */
|
|
59
|
+
export function readOutputValue(outputs: Record<string, unknown>, output: readonly string[]): { value: unknown } | undefined {
|
|
60
|
+
const [name, ...keys] = output;
|
|
61
|
+
if (name === undefined || !(name in outputs)) return undefined;
|
|
62
|
+
let v: unknown = outputs[name];
|
|
63
|
+
for (const k of keys) {
|
|
64
|
+
if (!v || typeof v !== "object" || Array.isArray(v) || !(k in (v as Record<string, unknown>))) return undefined;
|
|
65
|
+
v = (v as Record<string, unknown>)[k];
|
|
66
|
+
}
|
|
67
|
+
return { value: v };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Fill `reads` from each upstream's outputs, read once per upstream through
|
|
72
|
+
* `outputsOf`, which throws when the state cannot be read.
|
|
73
|
+
*/
|
|
74
|
+
export async function fillStateReads(reads: readonly TerraformStateRead[], outputsOf: (root: string) => Promise<Record<string, unknown>>): Promise<FilledStateReads> {
|
|
75
|
+
const result: FilledStateReads = { vars: {}, waiting: [], errors: [] };
|
|
76
|
+
const cache = new Map<string, Promise<Record<string, unknown>>>();
|
|
77
|
+
for (const read of reads) {
|
|
78
|
+
if (!cache.has(read.root)) cache.set(read.root, outputsOf(read.root));
|
|
79
|
+
let outputs: Record<string, unknown>;
|
|
80
|
+
try {
|
|
81
|
+
outputs = await cache.get(read.root)!;
|
|
82
|
+
} catch (err) {
|
|
83
|
+
result.errors.push(`it reads ${read.var} from ${read.root}, whose state could not be read: ${err instanceof Error ? err.message : String(err)}`);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const got = readOutputValue(outputs, read.output);
|
|
87
|
+
if (!got) {
|
|
88
|
+
result.waiting.push({ read, why: Object.keys(outputs).length === 0 ? "has no state yet" : `has no output ${read.output.join(".")} yet` });
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
result.vars[read.var] = got.value;
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Why a root with waiting reads was not planned, naming what to apply first. */
|
|
97
|
+
export function waitingReadsReason(waiting: FilledStateReads["waiting"]): string {
|
|
98
|
+
const upstreams = [...new Set(waiting.map((w) => w.read.root))].sort();
|
|
99
|
+
return `not planned: ${waiting.map((w) => `it reads ${w.read.var} from ${w.read.root}, which ${w.why}`).join("; ")}; apply ${upstreams.join(", ")} first`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The reads an Atmos instance's edges file lists (`{ reads: [{ var,
|
|
104
|
+
* upstream, output }] }`, as terragucci's `atmos write` leaves beside each
|
|
105
|
+
* instance), with each upstream mapped to its `terraform.roots` entry by
|
|
106
|
+
* `rootOf` (by default the upstream's own path).
|
|
107
|
+
*/
|
|
108
|
+
export function atmosStateReads(edges: unknown, rootOf: (upstream: string) => string = (u) => u): TerraformStateRead[] {
|
|
109
|
+
const reads = (edges as { reads?: unknown } | null)?.reads;
|
|
110
|
+
if (!Array.isArray(reads)) return [];
|
|
111
|
+
return reads.flatMap((r) => {
|
|
112
|
+
const read = r as { var?: unknown; upstream?: unknown; output?: unknown } | null;
|
|
113
|
+
if (!read || typeof read.var !== "string" || typeof read.upstream !== "string" || !Array.isArray(read.output) || !read.output.every((k) => typeof k === "string")) return [];
|
|
114
|
+
return [{ var: read.var, root: rootOf(read.upstream), output: read.output as string[] }];
|
|
115
|
+
});
|
|
116
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sources a root uses (chant#3924): module calls and provider addresses
|
|
3
|
+
* down the local tree, implied providers, a Terragrunt unit's source, the
|
|
4
|
+
* normal forms, and the local calls that cannot be followed.
|
|
5
|
+
*/
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { describe, expect, test } from "vitest";
|
|
8
|
+
import { usedSources } from "./used-sources";
|
|
9
|
+
|
|
10
|
+
const FIX = join(import.meta.dirname, "__fixtures__", "used-sources");
|
|
11
|
+
const ROOT = join(FIX, "root");
|
|
12
|
+
|
|
13
|
+
describe("usedSources", () => {
|
|
14
|
+
test("lists the root's and its local modules' sources with their block paths", async () => {
|
|
15
|
+
const { used } = await usedSources(ROOT);
|
|
16
|
+
const rows = used.map((u) => [u.kind, u.block, u.source, u.normal]);
|
|
17
|
+
expect(rows).toEqual(
|
|
18
|
+
expect.arrayContaining([
|
|
19
|
+
["provider", "required_providers.aws", "hashicorp/aws", "registry.terraform.io/hashicorp/aws"],
|
|
20
|
+
["module", "module.vpc", "terraform-aws-modules/vpc/aws", "registry.terraform.io/terraform-aws-modules/vpc/aws"],
|
|
21
|
+
["module", "module.net", "./modules/net", "./modules/net"],
|
|
22
|
+
["provider", "provider.random", "hashicorp/random", "registry.terraform.io/hashicorp/random"],
|
|
23
|
+
["provider", "provider.google", "hashicorp/google", "registry.terraform.io/hashicorp/google"],
|
|
24
|
+
["module", "module.net > module.sg", "git::https://git.acme.dev/sg.git?ref=v1.0.0", "git::https://git.acme.dev/sg.git"],
|
|
25
|
+
["provider", "module.net > required_providers.thing", "Registry.Acme.dev/acme/thing", "registry.acme.dev/acme/thing"],
|
|
26
|
+
["provider", "module.net > provider.aws", "hashicorp/aws", "registry.terraform.io/hashicorp/aws"],
|
|
27
|
+
]),
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("names the file each source is written in, relative to the project", async () => {
|
|
32
|
+
const { used } = await usedSources(ROOT);
|
|
33
|
+
expect(used.find((u) => u.block === "module.net > module.sg")!.file).toBe("modules/net/main.tf");
|
|
34
|
+
expect(used.find((u) => u.block === "module.net > required_providers.thing")!.file).toBe("modules/net/versions.tf.json");
|
|
35
|
+
expect(used.find((u) => u.block === "provider.google")!.file).toBe("extra.tofu");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("says what implies a provider, and never implies the binary's own", async () => {
|
|
39
|
+
const { used } = await usedSources(ROOT);
|
|
40
|
+
expect(used.find((u) => u.block === "provider.random")!.implied).toBe('resource "random_id" "x"');
|
|
41
|
+
expect(used.find((u) => u.block === "module.net > provider.aws")!.implied).toBe('resource "aws_vpc" "v"');
|
|
42
|
+
expect(used.some((u) => u.block.endsWith("provider.terraform"))).toBe(false);
|
|
43
|
+
expect(used.some((u) => u.block === "provider.aws")).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("names local calls it cannot follow and stops at a cycle", async () => {
|
|
47
|
+
const { used, unread } = await usedSources(ROOT);
|
|
48
|
+
expect(unread.map((u) => [u.block, u.source])).toEqual([
|
|
49
|
+
["module.gone", "./modules/missing"],
|
|
50
|
+
["module.outside", "../../outside"],
|
|
51
|
+
]);
|
|
52
|
+
expect(unread[0]!.why).toContain("does not exist");
|
|
53
|
+
expect(unread[1]!.why).toContain("outside the project");
|
|
54
|
+
expect(used.filter((u) => u.block.startsWith("module.net > module.back > "))).toEqual([]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("reads only the root with followLocal off", async () => {
|
|
58
|
+
const { used, unread } = await usedSources(ROOT, { followLocal: false });
|
|
59
|
+
expect(used.some((u) => u.block.startsWith("module.net > "))).toBe(false);
|
|
60
|
+
expect(unread).toEqual([]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("leaves .tofu files out for terraform, and uses OpenTofu's registry for tofu", async () => {
|
|
64
|
+
const tf = await usedSources(ROOT, { binary: "terraform" });
|
|
65
|
+
expect(tf.used.some((u) => u.block === "provider.google")).toBe(false);
|
|
66
|
+
const tofu = await usedSources(ROOT, { binary: "tofu" });
|
|
67
|
+
expect(tofu.used.find((u) => u.block === "provider.google")!.normal).toBe("registry.opentofu.org/hashicorp/google");
|
|
68
|
+
expect(tofu.used.find((u) => u.block === "module.vpc")!.normal).toBe("registry.opentofu.org/terraform-aws-modules/vpc/aws");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("reads a Terragrunt unit's terraform source", async () => {
|
|
72
|
+
const { used } = await usedSources(join(FIX, "unit"), { project: FIX });
|
|
73
|
+
expect(used).toEqual([{ kind: "module", file: "unit/terragrunt.hcl", block: "terraform", source: "tfr:///acme/vpc/aws?version=1.0.0", normal: "registry.terraform.io/acme/vpc/aws" }]);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every module source and provider address a root uses (chant#3924), read
|
|
3
|
+
* from its files before `init` downloads anything, so a product can check
|
|
4
|
+
* them against an allow list (terragucci's `sources:`) or a proxy map
|
|
5
|
+
* (chant#4606) in a check job.
|
|
6
|
+
*
|
|
7
|
+
* For a root and, with `followLocal`, each local module it calls (`./` and
|
|
8
|
+
* `../`, followed down to the modules they call), it reads each `module`
|
|
9
|
+
* block's `source`, each `required_providers` entry, and the providers a
|
|
10
|
+
* module uses with no entry: a `provider` block, or a resource, data or
|
|
11
|
+
* ephemeral block's `provider` argument or type prefix, which `init` installs
|
|
12
|
+
* as `hashicorp/<name>`. In the root itself it also reads a Terragrunt unit's
|
|
13
|
+
* `terraform { source }`. Files are `.tf`, `.tofu` and their `.json` forms;
|
|
14
|
+
* `binary` narrows them to the set that binary reads, with OpenTofu's rule
|
|
15
|
+
* that `x.tofu` hides `x.tf`.
|
|
16
|
+
*
|
|
17
|
+
* Each source carries its file and its block path from the root
|
|
18
|
+
* (`module.a > module.b > required_providers.x`) and its normal form
|
|
19
|
+
* (./registry/source-forms.ts). A local call that cannot be followed
|
|
20
|
+
* (outside the project, or missing) is named in `unread`, since what it
|
|
21
|
+
* uses cannot be read.
|
|
22
|
+
*/
|
|
23
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
24
|
+
import { join, posix, relative, sep } from "node:path";
|
|
25
|
+
import { loadHcl2json, type Hcl2Json } from "@intentius/chant/terraform/hcl2json";
|
|
26
|
+
import { isJsonConfigFile, jsonConfigTree, selectConfigFiles } from "./hcl/files";
|
|
27
|
+
import { impliedProvider, normalizeModuleSource, providerAddress, withoutQuery, type SourceBinary } from "./registry/source-forms";
|
|
28
|
+
import type { RegistryProxy } from "./registry/proxies";
|
|
29
|
+
|
|
30
|
+
/** One module source or provider address a root uses. */
|
|
31
|
+
export interface UsedSource {
|
|
32
|
+
kind: "module" | "provider";
|
|
33
|
+
/** The file it is written in, relative to the project, `/`-separated. */
|
|
34
|
+
file: string;
|
|
35
|
+
/**
|
|
36
|
+
* The block that names it, after the module calls that reach it:
|
|
37
|
+
* `module.vpc`, `terraform` for a terragrunt.hcl's source,
|
|
38
|
+
* `required_providers.aws`, or `provider.aws` for an implied one.
|
|
39
|
+
*/
|
|
40
|
+
block: string;
|
|
41
|
+
/** The source or address as written (`hashicorp/<name>` for an implied provider). */
|
|
42
|
+
source: string;
|
|
43
|
+
/**
|
|
44
|
+
* The normal form: for a module, the source without its query, normalised
|
|
45
|
+
* ({@link normalizeModuleSource}); for a provider, `<host>/<namespace>/<type>`
|
|
46
|
+
* ({@link providerAddress}), or the source as written when it is not an address.
|
|
47
|
+
*/
|
|
48
|
+
normal: string;
|
|
49
|
+
/** For an implied provider: the block that implies it. */
|
|
50
|
+
implied?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A local module call that could not be followed. */
|
|
54
|
+
export interface UnreadSource {
|
|
55
|
+
file: string;
|
|
56
|
+
block: string;
|
|
57
|
+
source: string;
|
|
58
|
+
why: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface UsedSourcesOptions {
|
|
62
|
+
/** Follow `./` and `../` module calls down the tree. Default true. */
|
|
63
|
+
followLocal?: boolean;
|
|
64
|
+
/** The project's top: file paths are relative to it, and a call that leaves it is not followed. Default the root's directory. */
|
|
65
|
+
project?: string;
|
|
66
|
+
/** Read only the files this binary reads, and default provider hosts to its registry. Default: every `.tf` and `.tofu` file, Terraform's registry. */
|
|
67
|
+
binary?: SourceBinary;
|
|
68
|
+
/** The HCL parser. Default: core's lazily loaded hcl2json. */
|
|
69
|
+
parser?: Hcl2Json;
|
|
70
|
+
/** Registry proxies (chant#4606): a source through one gets its upstream's normal form. */
|
|
71
|
+
proxies?: readonly RegistryProxy[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
type Tree = Record<string, unknown>;
|
|
75
|
+
|
|
76
|
+
/** A block list as hcl2json writes it (an array) or a JSON file may (one object). */
|
|
77
|
+
const bodies = (v: unknown): Tree[] => (Array.isArray(v) ? (v as Tree[]) : v && typeof v === "object" ? [v as Tree] : []);
|
|
78
|
+
|
|
79
|
+
const ALL_SUFFIXES = [".tf", ".tofu", ".tf.json", ".tofu.json"];
|
|
80
|
+
|
|
81
|
+
/** The configuration files of one module directory, plus terragrunt.hcl when `terragrunt`. */
|
|
82
|
+
function moduleFiles(dir: string, terragrunt: boolean, binary: SourceBinary | undefined): string[] {
|
|
83
|
+
if (!existsSync(dir) || !statSync(dir).isDirectory()) return [];
|
|
84
|
+
const names = readdirSync(dir).filter((n) => {
|
|
85
|
+
try {
|
|
86
|
+
return statSync(join(dir, n)).isFile();
|
|
87
|
+
} catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
const config = binary ? selectConfigFiles(names, binary) : names.filter((n) => ALL_SUFFIXES.some((s) => n.endsWith(s))).sort();
|
|
92
|
+
return terragrunt && names.includes("terragrunt.hcl") ? [...config, "terragrunt.hcl"] : config;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The local name of the provider a resource, data or ephemeral block uses: its `provider` argument's, else its type's prefix. */
|
|
96
|
+
function providerOf(type: string, body: Tree): string {
|
|
97
|
+
const ref = typeof body.provider === "string" ? /^(?:\$\{)?([A-Za-z][A-Za-z0-9_-]*)/.exec(body.provider) : null;
|
|
98
|
+
return ref ? ref[1]! : type.split("_")[0]!;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const isLocal = (source: string): boolean => /^\.\.?\//.test(source);
|
|
102
|
+
|
|
103
|
+
interface DirRead {
|
|
104
|
+
providers: UsedSource[];
|
|
105
|
+
calls: { file: string; block: string; source: string }[];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function readDir(abs: string, shown: string, prefix: string, top: boolean, o: Required<Pick<UsedSourcesOptions, "parser">> & UsedSourcesOptions): Promise<DirRead> {
|
|
109
|
+
const providers: UsedSource[] = [];
|
|
110
|
+
const calls: DirRead["calls"] = [];
|
|
111
|
+
const declared = new Set<string>();
|
|
112
|
+
const implied = new Map<string, { file: string; what: string }>();
|
|
113
|
+
const provider = (file: string, block: string, source: string, what?: string): UsedSource => ({
|
|
114
|
+
kind: "provider",
|
|
115
|
+
file,
|
|
116
|
+
block,
|
|
117
|
+
source,
|
|
118
|
+
normal: providerAddress(source, { binary: o.binary, proxies: o.proxies }) ?? source.trim().toLowerCase(),
|
|
119
|
+
...(what ? { implied: what } : {}),
|
|
120
|
+
});
|
|
121
|
+
for (const name of moduleFiles(abs, top, o.binary)) {
|
|
122
|
+
const file = shown === "." ? name : posix.join(shown, name);
|
|
123
|
+
const text = readFileSync(join(abs, name), "utf-8");
|
|
124
|
+
let tree: Tree;
|
|
125
|
+
try {
|
|
126
|
+
tree = (isJsonConfigFile(name) ? jsonConfigTree(text) : await o.parser.parse(file, text)) as Tree;
|
|
127
|
+
} catch (e) {
|
|
128
|
+
throw new Error(`${file} could not be read (${(e as Error).message.split("\n")[0]}), so its module and provider sources are unknown`);
|
|
129
|
+
}
|
|
130
|
+
if (name === "terragrunt.hcl") {
|
|
131
|
+
for (const b of bodies(tree.terraform)) if (typeof b.source === "string") calls.push({ file, block: `${prefix}terraform`, source: b.source });
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
for (const [call, list] of Object.entries((tree.module as Tree | undefined) ?? {})) {
|
|
135
|
+
for (const b of bodies(list)) if (typeof b.source === "string") calls.push({ file, block: `${prefix}module.${call}`, source: b.source });
|
|
136
|
+
}
|
|
137
|
+
for (const t of bodies(tree.terraform)) {
|
|
138
|
+
for (const req of bodies(t.required_providers)) {
|
|
139
|
+
for (const [local, v] of Object.entries(req)) {
|
|
140
|
+
declared.add(local);
|
|
141
|
+
const source = v && typeof v === "object" && typeof (v as Tree).source === "string" ? ((v as Tree).source as string) : `hashicorp/${local}`;
|
|
142
|
+
providers.push(provider(file, `${prefix}required_providers.${local}`, source));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
for (const local of Object.keys((tree.provider as Tree | undefined) ?? {})) if (!implied.has(local)) implied.set(local, { file, what: `provider "${local}"` });
|
|
147
|
+
for (const kind of ["resource", "data", "ephemeral"]) {
|
|
148
|
+
for (const [type, named] of Object.entries((tree[kind] as Tree | undefined) ?? {})) {
|
|
149
|
+
for (const [name, list] of Object.entries((named as Tree) ?? {})) {
|
|
150
|
+
for (const b of bodies(list)) {
|
|
151
|
+
const local = providerOf(type, b);
|
|
152
|
+
if (!implied.has(local)) implied.set(local, { file, what: `${kind} "${type}" "${name}"` });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
for (const [local, at] of implied) {
|
|
159
|
+
const source = declared.has(local) ? undefined : impliedProvider(local);
|
|
160
|
+
if (source) providers.push(provider(at.file, `${prefix}provider.${local}`, source, at.what));
|
|
161
|
+
}
|
|
162
|
+
return { providers, calls };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Every module source and provider address the root at `dir` uses, and each
|
|
167
|
+
* local call that could not be followed. Throws, naming the file, when a
|
|
168
|
+
* file cannot be parsed, so a check fails loudly instead of checking less.
|
|
169
|
+
*/
|
|
170
|
+
export async function usedSources(dir: string, options: UsedSourcesOptions = {}): Promise<{ used: UsedSource[]; unread: UnreadSource[] }> {
|
|
171
|
+
const project = options.project ?? dir;
|
|
172
|
+
const o = { ...options, parser: options.parser ?? (await loadHcl2json()) };
|
|
173
|
+
const follow = options.followLocal !== false;
|
|
174
|
+
const used: UsedSource[] = [];
|
|
175
|
+
const unread: UnreadSource[] = [];
|
|
176
|
+
const shownOf = (abs: string) => relative(project, abs).split(sep).join("/") || ".";
|
|
177
|
+
|
|
178
|
+
const walk = async (rel: string, prefix: string, chain: string[], top: boolean): Promise<void> => {
|
|
179
|
+
const { providers, calls } = await readDir(join(project, rel), rel, prefix, top, o);
|
|
180
|
+
used.push(...providers);
|
|
181
|
+
for (const c of calls) {
|
|
182
|
+
used.push({ kind: "module", file: c.file, block: c.block, source: c.source, normal: isLocal(c.source) ? withoutQuery(c.source) : normalizeModuleSource(withoutQuery(c.source), { binary: o.binary, proxies: o.proxies }) });
|
|
183
|
+
if (!follow || !isLocal(c.source)) continue;
|
|
184
|
+
// A Terragrunt source's `//` marks the directory it copies; the module is the path with it read as one `/`.
|
|
185
|
+
const path = withoutQuery(c.source).replace(/\/\/+/g, "/");
|
|
186
|
+
const next = posix.normalize(posix.join(rel, path)).replace(/\/+$/, "") || ".";
|
|
187
|
+
if (next === ".." || next.startsWith("../")) {
|
|
188
|
+
unread.push({ file: c.file, block: c.block, source: c.source, why: "it is outside the project, so the sources it uses cannot be read" });
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (!existsSync(join(project, next))) {
|
|
192
|
+
unread.push({ file: c.file, block: c.block, source: c.source, why: `${next} does not exist, so the sources it uses cannot be read` });
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (chain.includes(next) || moduleFiles(join(project, next), false, o.binary).length === 0) continue;
|
|
196
|
+
await walk(next, `${c.block} > `, [...chain, next], false);
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const start = shownOf(dir);
|
|
201
|
+
if (start === ".." || start.startsWith("../")) throw new Error(`${dir} is outside the project ${project}`);
|
|
202
|
+
await walk(start, "", [start], true);
|
|
203
|
+
return { used, unread };
|
|
204
|
+
}
|