@intentius/chant-lexicon-terraform 0.57.0

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.
Files changed (131) hide show
  1. package/README.md +53 -0
  2. package/dist/codegen/docs-cli.d.ts +3 -0
  3. package/dist/codegen/docs-cli.d.ts.map +1 -0
  4. package/dist/codegen/docs.d.ts +11 -0
  5. package/dist/codegen/docs.d.ts.map +1 -0
  6. package/dist/codegen/generate-cli.d.ts +3 -0
  7. package/dist/codegen/generate-cli.d.ts.map +1 -0
  8. package/dist/codegen/generate.d.ts +28 -0
  9. package/dist/codegen/generate.d.ts.map +1 -0
  10. package/dist/codegen/package.d.ts +17 -0
  11. package/dist/codegen/package.d.ts.map +1 -0
  12. package/dist/composites/terraform-apply-op.d.ts +88 -0
  13. package/dist/composites/terraform-apply-op.d.ts.map +1 -0
  14. package/dist/composites/terraform-watch-op.d.ts +124 -0
  15. package/dist/composites/terraform-watch-op.d.ts.map +1 -0
  16. package/dist/config.d.ts +83 -0
  17. package/dist/config.d.ts.map +1 -0
  18. package/dist/describe-resources.d.ts +127 -0
  19. package/dist/describe-resources.d.ts.map +1 -0
  20. package/dist/generated/index.d.ts +2 -0
  21. package/dist/generated/index.d.ts.map +1 -0
  22. package/dist/hcl/parse.d.ts +87 -0
  23. package/dist/hcl/parse.d.ts.map +1 -0
  24. package/dist/hcl/roots.d.ts +37 -0
  25. package/dist/hcl/roots.d.ts.map +1 -0
  26. package/dist/index.d.ts +12 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/integrity.json +12 -0
  29. package/dist/lint/audit-catalog.d.ts +12 -0
  30. package/dist/lint/audit-catalog.d.ts.map +1 -0
  31. package/dist/lint/audit-lineage.d.ts +21 -0
  32. package/dist/lint/audit-lineage.d.ts.map +1 -0
  33. package/dist/lint/post-synth/index.d.ts +3 -0
  34. package/dist/lint/post-synth/index.d.ts.map +1 -0
  35. package/dist/lint/post-synth/tf001.d.ts +17 -0
  36. package/dist/lint/post-synth/tf001.d.ts.map +1 -0
  37. package/dist/lint/rules/index.d.ts +5 -0
  38. package/dist/lint/rules/index.d.ts.map +1 -0
  39. package/dist/lint/rules/plan-before-apply.d.ts +30 -0
  40. package/dist/lint/rules/plan-before-apply.d.ts.map +1 -0
  41. package/dist/lsp/completions.d.ts +26 -0
  42. package/dist/lsp/completions.d.ts.map +1 -0
  43. package/dist/lsp/context.d.ts +70 -0
  44. package/dist/lsp/context.d.ts.map +1 -0
  45. package/dist/lsp/hover.d.ts +12 -0
  46. package/dist/lsp/hover.d.ts.map +1 -0
  47. package/dist/lsp/option-keys.d.ts +34 -0
  48. package/dist/lsp/option-keys.d.ts.map +1 -0
  49. package/dist/manifest.json +6 -0
  50. package/dist/meta.json +1 -0
  51. package/dist/okf/index.md +8 -0
  52. package/dist/okf/rules/TF001.md +11 -0
  53. package/dist/okf/rules/TF101.md +11 -0
  54. package/dist/op/activities/index.d.ts +22 -0
  55. package/dist/op/activities/index.d.ts.map +1 -0
  56. package/dist/op/activities/terraform.d.ts +213 -0
  57. package/dist/op/activities/terraform.d.ts.map +1 -0
  58. package/dist/op/builders.d.ts +56 -0
  59. package/dist/op/builders.d.ts.map +1 -0
  60. package/dist/package-cli.d.ts +3 -0
  61. package/dist/package-cli.d.ts.map +1 -0
  62. package/dist/plugin.d.ts +11 -0
  63. package/dist/plugin.d.ts.map +1 -0
  64. package/dist/rules/plan-before-apply.ts +143 -0
  65. package/dist/rules/tf001.ts +66 -0
  66. package/dist/serializer.d.ts +19 -0
  67. package/dist/serializer.d.ts.map +1 -0
  68. package/dist/skills/chant-terraform.md +92 -0
  69. package/dist/state-ownership.d.ts +27 -0
  70. package/dist/state-ownership.d.ts.map +1 -0
  71. package/dist/types/index.d.ts +2 -0
  72. package/dist/validate-cli.d.ts +3 -0
  73. package/dist/validate-cli.d.ts.map +1 -0
  74. package/dist/validate.d.ts +15 -0
  75. package/dist/validate.d.ts.map +1 -0
  76. package/package.json +75 -0
  77. package/src/__fixtures__/no-backend/main.tf +24 -0
  78. package/src/__fixtures__/show-state.json +68 -0
  79. package/src/__fixtures__/with-backend/main.tf +28 -0
  80. package/src/__fixtures__/with-module/main.tf +49 -0
  81. package/src/__fixtures__/with-module/modules/inner/main.tf +5 -0
  82. package/src/codegen/docs-cli.ts +4 -0
  83. package/src/codegen/docs.ts +50 -0
  84. package/src/codegen/generate-cli.ts +10 -0
  85. package/src/codegen/generate.ts +68 -0
  86. package/src/codegen/package.ts +50 -0
  87. package/src/composites/terraform-apply-op.acceptance.test.ts +138 -0
  88. package/src/composites/terraform-apply-op.test.ts +193 -0
  89. package/src/composites/terraform-apply-op.ts +166 -0
  90. package/src/composites/terraform-watch-op.test.ts +184 -0
  91. package/src/composites/terraform-watch-op.ts +204 -0
  92. package/src/config.ts +76 -0
  93. package/src/describe-resources.test.ts +342 -0
  94. package/src/describe-resources.ts +357 -0
  95. package/src/generated/index.d.ts +2 -0
  96. package/src/generated/index.ts +4 -0
  97. package/src/generated/lexicon-terraform.json +1 -0
  98. package/src/hcl/parse.ts +235 -0
  99. package/src/hcl/roots.ts +67 -0
  100. package/src/index.ts +65 -0
  101. package/src/lint/audit-catalog.ts +30 -0
  102. package/src/lint/audit-lineage.ts +21 -0
  103. package/src/lint/audit.test.ts +45 -0
  104. package/src/lint/post-synth/index.ts +7 -0
  105. package/src/lint/post-synth/post-synth.test.ts +95 -0
  106. package/src/lint/post-synth/tf001.ts +66 -0
  107. package/src/lint/rules/index.ts +7 -0
  108. package/src/lint/rules/plan-before-apply.test.ts +111 -0
  109. package/src/lint/rules/plan-before-apply.ts +143 -0
  110. package/src/lsp/completions.test.ts +120 -0
  111. package/src/lsp/completions.ts +101 -0
  112. package/src/lsp/context.test.ts +152 -0
  113. package/src/lsp/context.ts +349 -0
  114. package/src/lsp/hover.test.ts +82 -0
  115. package/src/lsp/hover.ts +44 -0
  116. package/src/lsp/option-keys.ts +106 -0
  117. package/src/op/activities/index.ts +48 -0
  118. package/src/op/activities/registry.test.ts +29 -0
  119. package/src/op/activities/terraform.test.ts +445 -0
  120. package/src/op/activities/terraform.ts +469 -0
  121. package/src/op/builders.test.ts +90 -0
  122. package/src/op/builders.ts +96 -0
  123. package/src/package-cli.ts +21 -0
  124. package/src/plugin.test.ts +271 -0
  125. package/src/plugin.ts +157 -0
  126. package/src/serializer.test.ts +26 -0
  127. package/src/serializer.ts +26 -0
  128. package/src/skills/chant-terraform.md +92 -0
  129. package/src/state-ownership.ts +32 -0
  130. package/src/validate-cli.ts +5 -0
  131. package/src/validate.ts +28 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state-ownership.d.ts","sourceRoot":"","sources":["../src/state-ownership.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAE9D;;;;;;;GAOG;AACH,eAAO,MAAM,8BAA8B,EAAE,WAI5C,CAAC"}
@@ -0,0 +1,2 @@
1
+ // Code generated by chant generate. DO NOT EDIT.
2
+ export {};
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env tsx
2
+ export {};
3
+ //# sourceMappingURL=validate-cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate-cli.d.ts","sourceRoot":"","sources":["../src/validate-cli.ts"],"names":[],"mappings":""}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Validate generated lexicon-terraform artifacts.
3
+ *
4
+ * Thin wrapper around the core validation framework
5
+ * with terraform-specific configuration.
6
+ */
7
+ import { type ValidateResult } from "@intentius/chant/codegen/validate";
8
+ export type { ValidateCheck, ValidateResult } from "@intentius/chant/codegen/validate";
9
+ /**
10
+ * Validate the generated lexicon-terraform artifacts.
11
+ */
12
+ export declare function validate(opts?: {
13
+ basePath?: string;
14
+ }): Promise<ValidateResult>;
15
+ //# sourceMappingURL=validate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,EAA4B,KAAK,cAAc,EAAE,MAAM,mCAAmC,CAAC;AAElG,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;AAKvF;;GAEG;AACH,wBAAsB,QAAQ,CAAC,IAAI,CAAC,EAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,cAAc,CAAC,CAQpF"}
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "@intentius/chant-lexicon-terraform",
3
+ "version": "0.57.0",
4
+ "type": "module",
5
+ "files": [
6
+ "src/",
7
+ "dist/"
8
+ ],
9
+ "exports": {
10
+ ".": {
11
+ "development": "./src/index.ts",
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./src/index.ts"
14
+ },
15
+ "./*": {
16
+ "development": "./src/*.ts",
17
+ "types": "./dist/*.d.ts",
18
+ "default": "./src/*.ts"
19
+ },
20
+ "./op/activities": {
21
+ "development": "./src/op/activities/index.ts",
22
+ "types": "./dist/op/activities/index.d.ts",
23
+ "default": "./src/op/activities/index.ts"
24
+ },
25
+ "./lint/post-synth": {
26
+ "development": "./src/lint/post-synth/index.ts",
27
+ "types": "./dist/lint/post-synth/index.d.ts",
28
+ "default": "./src/lint/post-synth/index.ts"
29
+ },
30
+ "./manifest": "./dist/manifest.json",
31
+ "./meta": "./dist/meta.json",
32
+ "./types": "./dist/types/index.d.ts"
33
+ },
34
+ "scripts": {
35
+ "generate": "npx tsx src/codegen/generate-cli.ts",
36
+ "validate": "npx tsx src/validate-cli.ts",
37
+ "docs": "npx tsx src/codegen/docs-cli.ts",
38
+ "build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && find dist -type f \\( -name \"*.js\" -o -name \"*.js.map\" \\) -delete",
39
+ "prepack": "npm run generate && npm run bundle && npm run validate && npm run build",
40
+ "bundle": "tsx src/package-cli.ts"
41
+ },
42
+ "devDependencies": {
43
+ "@intentius/chant": "*",
44
+ "tsc-alias": "^1.8.17",
45
+ "typescript": "^5.9.3",
46
+ "@cdktf/hcl2json": "^0.21.0",
47
+ "zod": "^4.3.6"
48
+ },
49
+ "publishConfig": {
50
+ "access": "public"
51
+ },
52
+ "peerDependencies": {
53
+ "zod": "^4.3.6",
54
+ "@intentius/chant": "^0.57.0",
55
+ "typescript": "^5.9.3"
56
+ },
57
+ "description": "terraform lexicon for chant, existing Terraform root modules as declarable entities",
58
+ "license": "Apache-2.0",
59
+ "homepage": "https://github.com/INTENTIUS/chant#readme",
60
+ "repository": {
61
+ "type": "git",
62
+ "url": "https://github.com/INTENTIUS/chant.git",
63
+ "directory": "lexicons/terraform"
64
+ },
65
+ "bugs": {
66
+ "url": "https://github.com/INTENTIUS/chant/issues"
67
+ },
68
+ "keywords": [
69
+ "chant",
70
+ "terraform",
71
+ "opentofu",
72
+ "hcl",
73
+ "iac"
74
+ ]
75
+ }
@@ -0,0 +1,24 @@
1
+ terraform {
2
+ required_version = ">= 1.5.0"
3
+
4
+ required_providers {
5
+ null = {
6
+ source = "hashicorp/null"
7
+ version = "~> 3.2"
8
+ }
9
+ }
10
+ }
11
+
12
+ provider "null" {}
13
+
14
+ resource "null_resource" "first" {
15
+ triggers = {
16
+ name = "first"
17
+ }
18
+ }
19
+
20
+ resource "null_resource" "second" {
21
+ triggers = {
22
+ name = "second"
23
+ }
24
+ }
@@ -0,0 +1,68 @@
1
+ {
2
+ "format_version": "1.0",
3
+ "terraform_version": "1.15.8",
4
+ "values": {
5
+ "root_module": {
6
+ "resources": [
7
+ {
8
+ "address": "null_resource.first",
9
+ "mode": "managed",
10
+ "type": "null_resource",
11
+ "name": "first",
12
+ "provider_name": "registry.terraform.io/hashicorp/null",
13
+ "schema_version": 0,
14
+ "values": {
15
+ "id": "1710777879587847269",
16
+ "triggers": {
17
+ "name": "first"
18
+ }
19
+ },
20
+ "sensitive_values": {
21
+ "triggers": {}
22
+ }
23
+ },
24
+ {
25
+ "address": "null_resource.second",
26
+ "mode": "managed",
27
+ "type": "null_resource",
28
+ "name": "second",
29
+ "provider_name": "registry.terraform.io/hashicorp/null",
30
+ "schema_version": 0,
31
+ "values": {
32
+ "id": "8141892773761384170",
33
+ "triggers": {
34
+ "name": "second"
35
+ }
36
+ },
37
+ "sensitive_values": {
38
+ "triggers": {}
39
+ }
40
+ }
41
+ ],
42
+ "child_modules": [
43
+ {
44
+ "resources": [
45
+ {
46
+ "address": "module.cdn.null_resource.edge",
47
+ "mode": "managed",
48
+ "type": "null_resource",
49
+ "name": "edge",
50
+ "provider_name": "registry.terraform.io/hashicorp/null",
51
+ "schema_version": 0,
52
+ "values": {
53
+ "id": "3525037590895298281",
54
+ "triggers": {
55
+ "name": "edge"
56
+ }
57
+ },
58
+ "sensitive_values": {
59
+ "triggers": {}
60
+ }
61
+ }
62
+ ],
63
+ "address": "module.cdn"
64
+ }
65
+ ]
66
+ }
67
+ }
68
+ }
@@ -0,0 +1,28 @@
1
+ terraform {
2
+ required_version = ">= 1.5.0"
3
+
4
+ backend "local" {
5
+ path = "terraform.tfstate"
6
+ }
7
+
8
+ required_providers {
9
+ null = {
10
+ source = "hashicorp/null"
11
+ version = "~> 3.2"
12
+ }
13
+ }
14
+ }
15
+
16
+ provider "null" {}
17
+
18
+ resource "null_resource" "first" {
19
+ triggers = {
20
+ name = "first"
21
+ }
22
+ }
23
+
24
+ resource "null_resource" "second" {
25
+ triggers = {
26
+ name = "second"
27
+ }
28
+ }
@@ -0,0 +1,49 @@
1
+ # The root `show-state.json` next door was recorded from: this configuration
2
+ # was applied with terraform 1.15.8 against a local backend, and the resulting
3
+ # `terraform show -json` saved verbatim. `null_resource.third` is declared here
4
+ # and deliberately absent from that state, which is what an OBSERVED-ABSENT
5
+ # entity looks like.
6
+
7
+ terraform {
8
+ required_version = ">= 1.5.0"
9
+
10
+ backend "local" {
11
+ path = "terraform.tfstate"
12
+ }
13
+
14
+ required_providers {
15
+ null = {
16
+ source = "hashicorp/null"
17
+ version = "~> 3.2"
18
+ }
19
+ }
20
+ }
21
+
22
+ provider "null" {}
23
+
24
+ resource "null_resource" "first" {
25
+ triggers = {
26
+ name = "first"
27
+ }
28
+ }
29
+
30
+ resource "null_resource" "second" {
31
+ triggers = {
32
+ name = "second"
33
+ }
34
+ }
35
+
36
+ resource "null_resource" "third" {
37
+ triggers = {
38
+ name = "third"
39
+ }
40
+ }
41
+
42
+ module "cdn" {
43
+ source = "./modules/inner"
44
+ }
45
+
46
+ variable "region" {
47
+ type = string
48
+ default = "us-east-1"
49
+ }
@@ -0,0 +1,5 @@
1
+ resource "null_resource" "edge" {
2
+ triggers = {
3
+ name = "edge"
4
+ }
5
+ }
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env tsx
2
+ import { generateDocs } from "./docs";
3
+
4
+ await generateDocs({ verbose: true });
@@ -0,0 +1,50 @@
1
+ import { dirname, join } from "path";
2
+ import { fileURLToPath } from "url";
3
+ import { docsPipeline, writeDocsSite } from "@intentius/chant/codegen/docs";
4
+ import type { DocsConfig } from "@intentius/chant/codegen/docs";
5
+
6
+ const overview = `
7
+ The Terraform lexicon reads an estate's existing \`.tf\` files. There is no
8
+ generated resource surface here and nothing is emitted back over the HCL:
9
+ Terraform's types live in provider registries, one schema per provider and
10
+ version, and an estate that already has a root module does not need a second,
11
+ generated copy of it.
12
+
13
+ What you declare is the directory. Each entry in \`terraform.roots\` names a
14
+ root module; \`buildRoots()\` parses it at build time and every HCL block joins
15
+ the build as an entity keyed \`<root>/<address>\`, carrying the block body
16
+ verbatim. Those entities are what the post-synth checks and \`chant audit\`
17
+ read.
18
+
19
+ See [Getting Started](getting-started/) for the config shape, the block-to-entity
20
+ mapping, and what TF001 reports.
21
+ `;
22
+
23
+ /**
24
+ * Generate the docs site for the terraform lexicon.
25
+ *
26
+ * `basePath` is what the unified site at /chant needs so assets and internal
27
+ * links resolve once this site is copied under it (scripts/build-docs.test.ts
28
+ * asserts it). Authored pages come from `docs/pages/` by default.
29
+ */
30
+ export async function generateDocs(options?: { verbose?: boolean }): Promise<void> {
31
+ const pkgDir = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
32
+
33
+ const config: DocsConfig = {
34
+ name: "terraform",
35
+ displayName: "Terraform",
36
+ description: "Existing Terraform root modules as declarable chant entities",
37
+ distDir: join(pkgDir, "dist"),
38
+ outDir: join(pkgDir, "docs"),
39
+ basePath: process.env.DOCS_BASE_PATH ?? "/chant/lexicons/terraform/",
40
+ overview,
41
+ serviceFromType: (type: string) => type.split("::")[1] ?? type,
42
+ };
43
+
44
+ const result = docsPipeline(config);
45
+ writeDocsSite(config, result);
46
+
47
+ if (options?.verbose) {
48
+ console.error(`Generated ${result.pages.size} documentation pages`);
49
+ }
50
+ }
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env tsx
2
+ import { generate, writeGeneratedFiles } from "./generate";
3
+ import { dirname } from "path";
4
+ import { fileURLToPath } from "url";
5
+
6
+ // src/codegen/generate-cli.ts -> src/codegen -> src -> package root.
7
+ // Two dirnames land in src/ and the first generate writes src/src/generated/ (#1614).
8
+ const pkgDir = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
9
+ const result = await generate({ verbose: true });
10
+ writeGeneratedFiles(result, pkgDir);
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Generation for the terraform lexicon: there is nothing to generate.
3
+ *
4
+ * Every other lexicon fetches one upstream schema and emits typed resource
5
+ * classes from it. Terraform has no such thing. Its resource surface lives in
6
+ * provider registries, one schema per provider and per version, and this
7
+ * lexicon does not model it: an estate's `.tf` files are read as they are
8
+ * (see `../hcl/parse.ts`) and the blocks become entities with their bodies
9
+ * carried verbatim. So `core`'s `generatePipeline` is not called at all, and
10
+ * this returns the empty result the packaging pipeline needs to assemble a
11
+ * bundle.
12
+ *
13
+ * The registry is still written, empty, because `validate` reads it and
14
+ * `writeBundleSpec` derives `dist/meta.json` from it. An empty registry is the
15
+ * honest statement that this lexicon exports no generated resource classes.
16
+ */
17
+
18
+ import { writeGeneratedArtifacts } from "@intentius/chant/codegen/generate";
19
+ import type { GenerateResult } from "@intentius/chant/codegen/generate";
20
+ import { dirname } from "path";
21
+ import { fileURLToPath } from "url";
22
+
23
+ /** Filename of the generated registry, read back by `../validate.ts`. */
24
+ export const LEXICON_JSON_FILENAME = "lexicon-terraform.json";
25
+
26
+ const EMPTY_INDEX_TS = `// Code generated by chant generate. DO NOT EDIT.
27
+ // The terraform lexicon models no upstream schema, so there are no resource
28
+ // classes to export. See src/codegen/generate.ts.
29
+ export {};
30
+ `;
31
+
32
+ const EMPTY_TYPES_DTS = `// Code generated by chant generate. DO NOT EDIT.
33
+ export {};
34
+ `;
35
+
36
+ export async function generate(options?: { verbose?: boolean; force?: boolean }): Promise<GenerateResult> {
37
+ const result: GenerateResult = {
38
+ lexiconJSON: `${JSON.stringify({}, null, 2)}\n`,
39
+ typesDTS: EMPTY_TYPES_DTS,
40
+ indexTS: EMPTY_INDEX_TS,
41
+ resources: 0,
42
+ properties: 0,
43
+ enums: 0,
44
+ warnings: [],
45
+ };
46
+
47
+ if (options?.verbose) {
48
+ console.error("terraform generates no types: the .tf files an estate already has are the input");
49
+ }
50
+
51
+ return result;
52
+ }
53
+
54
+ /**
55
+ * Write generated files to the package directory.
56
+ */
57
+ export function writeGeneratedFiles(result: GenerateResult, pkgDir?: string): void {
58
+ // This file lives in src/codegen/, so the package root is three levels up.
59
+ const dir = pkgDir ?? dirname(dirname(dirname(fileURLToPath(import.meta.url))));
60
+ writeGeneratedArtifacts({
61
+ baseDir: dir,
62
+ files: {
63
+ [LEXICON_JSON_FILENAME]: result.lexiconJSON,
64
+ "index.d.ts": result.typesDTS,
65
+ "index.ts": result.indexTS,
66
+ },
67
+ });
68
+ }
@@ -0,0 +1,50 @@
1
+ import { readFileSync } from "fs";
2
+ import { dirname, join } from "path";
3
+ import { fileURLToPath } from "url";
4
+ import { packagePipeline, collectSkills } from "@intentius/chant/codegen/package";
5
+ import { generate } from "./generate";
6
+ import { terraformPlugin } from "../plugin";
7
+
8
+ /**
9
+ * This package's `src/` directory. `packagePipeline` scans it for the rule
10
+ * files that go into `dist/rules/`, so it must be `src/` and not `src/codegen/`
11
+ * (which is where `dirname(import.meta.url)` alone lands, and where
12
+ * `lint/rules` does not exist). `lexicons/aws/src/codegen/package.ts` resolves
13
+ * it the same way.
14
+ */
15
+ const srcDir = dirname(dirname(fileURLToPath(import.meta.url)));
16
+
17
+ /** The published version, so the manifest cannot drift from package.json. */
18
+ function packageVersion(): string {
19
+ try {
20
+ const pkg = JSON.parse(readFileSync(join(srcDir, "..", "package.json"), "utf-8")) as { version?: string };
21
+ return pkg.version ?? "0.0.0";
22
+ } catch {
23
+ return "0.0.0";
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Package the terraform lexicon for distribution.
29
+ */
30
+ export async function packageLexicon(options?: { verbose?: boolean; force?: boolean }) {
31
+ const version = packageVersion();
32
+
33
+ const { spec, stats } = await packagePipeline(
34
+ {
35
+ generate: (opts) => generate({ verbose: opts?.verbose, force: opts?.force }),
36
+ buildManifest: () => ({
37
+ name: "terraform",
38
+ version,
39
+ chantVersion: ">=0.1.0",
40
+ namespace: "Terraform",
41
+ }),
42
+ srcDir,
43
+ collectSkills: () => collectSkills(terraformPlugin.skills?.() ?? []),
44
+ version,
45
+ },
46
+ options,
47
+ );
48
+
49
+ return { spec, stats };
50
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * The acceptance test is terraform, not a fixture (#2086).
3
+ *
4
+ * Everything else in this lexicon's Op suite proves the shape of what chant
5
+ * emits. This proves the shape runs: build a `TerraformApplyOp` with
6
+ * `gate: "never"` (the local executor refuses any Op containing a gate), hand
7
+ * it to `runOpLocally` with the activities the registry resolves by
8
+ * convention, and let a real `terraform` — or `tofu` — init, plan and apply
9
+ * the #2083 `with-backend` fixture root into a temp directory. Two
10
+ * `null_resource`s in the resulting state is the pass.
11
+ *
12
+ * The backend is `local`, so no credentials and no remote state are involved.
13
+ * The `hashicorp/null` provider still has to be downloaded from the registry,
14
+ * so the suite needs network as well as a binary. Both are gated, with the
15
+ * reason visible in the runner:
16
+ *
17
+ * - no `terraform` and no `tofu` on PATH,
18
+ * - `CHANT_OFFLINE` set, or `registry.terraform.io` not resolvable.
19
+ *
20
+ * Gating copied from `lexicons/k3s/src/serializer.acceptance.test.ts`, which
21
+ * skips the same way when Docker is absent.
22
+ */
23
+
24
+ import { execSync } from "node:child_process";
25
+ import { lookup } from "node:dns/promises";
26
+ import { cpSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
27
+ import { tmpdir } from "node:os";
28
+ import { join } from "node:path";
29
+ import { afterAll, describe, expect, it } from "vitest";
30
+ import { loadActivities, loadProfiles, runOpLocally, type OpConfig } from "@intentius/chant/op";
31
+ import { TerraformApplyOp } from "./terraform-apply-op";
32
+ import { terraformShow } from "../op/activities/terraform";
33
+
34
+ function onPath(cmd: string): boolean {
35
+ try {
36
+ execSync(`command -v ${cmd}`, { stdio: "ignore" });
37
+ return true;
38
+ } catch {
39
+ return false;
40
+ }
41
+ }
42
+
43
+ /** The registry has to answer for the provider download; a DNS answer is enough of a probe. */
44
+ async function registryResolvable(): Promise<boolean> {
45
+ try {
46
+ await Promise.race([
47
+ lookup("registry.terraform.io"),
48
+ new Promise((_r, reject) => setTimeout(() => reject(new Error("dns timeout")), 3000)),
49
+ ]);
50
+ return true;
51
+ } catch {
52
+ return false;
53
+ }
54
+ }
55
+
56
+ const binary: "terraform" | "tofu" | undefined = onPath("terraform")
57
+ ? "terraform"
58
+ : onPath("tofu")
59
+ ? "tofu"
60
+ : undefined;
61
+ const offline = Boolean(process.env.CHANT_OFFLINE);
62
+ const online = !offline && binary !== undefined && (await registryResolvable());
63
+
64
+ const skipReason = !binary
65
+ ? "no terraform or tofu binary on PATH"
66
+ : !online
67
+ ? offline
68
+ ? "CHANT_OFFLINE is set and the provider download needs the registry"
69
+ : "registry.terraform.io is unreachable and the provider download needs it"
70
+ : "";
71
+
72
+ const FIXTURE = join(import.meta.dirname, "..", "__fixtures__", "with-backend");
73
+ const workspaces: string[] = [];
74
+
75
+ afterAll(() => {
76
+ for (const dir of workspaces) rmSync(dir, { recursive: true, force: true });
77
+ });
78
+
79
+ describe.skipIf(skipReason !== "")(
80
+ `TerraformApplyOp applies a real root${skipReason ? ` (skipped: ${skipReason})` : ""}`,
81
+ () => {
82
+ it(
83
+ "inits, plans and applies the with-backend fixture, leaving two null_resources in state",
84
+ { timeout: 300_000 },
85
+ async () => {
86
+ const project = mkdtempSync(join(tmpdir(), "chant-tf-accept-"));
87
+ workspaces.push(project);
88
+ cpSync(FIXTURE, join(project, "root"), { recursive: true });
89
+ writeFileSync(
90
+ join(project, "chant.config.json"),
91
+ JSON.stringify(
92
+ { lexicons: ["terraform"], terraform: { binary, roots: { acceptance: { dir: "./root" } } } },
93
+ null,
94
+ 2,
95
+ ),
96
+ );
97
+
98
+ const { op } = TerraformApplyOp({
99
+ name: "terraform-acceptance",
100
+ root: "acceptance",
101
+ gate: "never",
102
+ cwd: project,
103
+ });
104
+
105
+ const activities = await loadActivities(["terraform"]);
106
+ expect([...activities.keys()]).toEqual(
107
+ expect.arrayContaining(["terraformInit", "terraformPlan", "terraformApply", "terraformShow"]),
108
+ );
109
+
110
+ const result = await runOpLocally(
111
+ (op as unknown as { props: OpConfig }).props,
112
+ activities,
113
+ await loadProfiles(),
114
+ );
115
+
116
+ expect(result.ok).toBe(true);
117
+ expect(result.records.map((r) => `${r.phase}:${r.status}`)).toEqual([
118
+ "Init:ok",
119
+ "Plan:ok",
120
+ "Apply:ok",
121
+ ]);
122
+
123
+ // The plan proposed two creates, and said so through the outcome the
124
+ // Plan step surfaces as the `Changed` search attribute.
125
+ const plan = result.records.find((r) => r.phase === "Plan")!;
126
+ expect(plan.outcome).toEqual({ name: "Changed", value: true });
127
+
128
+ // What terraform actually left behind.
129
+ const state = await terraformShow({ root: "acceptance", cwd: project });
130
+ const addresses = (
131
+ (state.json as { values?: { root_module?: { resources?: Array<{ address?: string }> } } }).values
132
+ ?.root_module?.resources ?? []
133
+ ).map((r) => r.address);
134
+ expect(addresses.sort()).toEqual(["null_resource.first", "null_resource.second"]);
135
+ },
136
+ );
137
+ },
138
+ );