nac-cli 0.1.0__py3-none-any.whl

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.
nac/terraform.py ADDED
@@ -0,0 +1,142 @@
1
+ # SPDX-License-Identifier: MPL-2.0
2
+ # Copyright (c) 2026 Daniel Schmidt
3
+
4
+ import os
5
+ import shutil
6
+ from collections.abc import Mapping
7
+
8
+ from nac import bootstrap
9
+ from nac.config import TerraformToolConfig
10
+ from nac.exceptions import EngineNotFoundError, NoEngineAvailableError
11
+
12
+ TOFU_VERSION_ENV = "TOFUENV_TOFU_VERSION"
13
+ TOFU_AUTO_INSTALL_ENV = "TOFUENV_AUTO_INSTALL"
14
+ TERRAFORM_VERSION_ENV = "TFENV_TERRAFORM_VERSION"
15
+ TERRAFORM_AUTO_INSTALL_ENV = "TFENV_AUTO_INSTALL"
16
+
17
+ DEFAULT_PLAN_FILE = "plan.tfplan"
18
+ DEFAULT_PLAN_TEXT_FILE = "plan.txt"
19
+ DEFAULT_PLAN_JSON_FILE = "plan.json"
20
+
21
+
22
+ def resolve_engine_binary(cfg: TerraformToolConfig) -> tuple[str, str]:
23
+ """Resolve which terraform/tofu binary to use.
24
+
25
+ Returns (engine_name, resolved_absolute_path). An explicit
26
+ `cfg.engine` is a hard requirement — it never falls back to the
27
+ other binary. When unset, `tofu` is tried before `terraform`, then
28
+ a previously bootstrap-installed `tofu` in the cache dir (FR-12),
29
+ so a machine set up via `nac setup` keeps working on subsequent
30
+ commands without re-prompting.
31
+ """
32
+ if cfg.engine is not None:
33
+ path = shutil.which(cfg.engine)
34
+ if path is None:
35
+ raise EngineNotFoundError(
36
+ f"`{cfg.engine}` not found on PATH — install it, or set "
37
+ f"`tools.terraform.version` to have `nac` manage it via `tenv`."
38
+ )
39
+ return cfg.engine, path
40
+
41
+ for engine in ("tofu", "terraform"):
42
+ path = shutil.which(engine)
43
+ if path is not None:
44
+ return engine, path
45
+
46
+ cached = bootstrap.find_cached_binary()
47
+ if cached is not None:
48
+ return "tofu", str(cached)
49
+
50
+ raise NoEngineAvailableError("Neither `tofu` nor `terraform` was found on PATH.")
51
+
52
+
53
+ def build_engine_env(
54
+ engine: str,
55
+ version: str | None,
56
+ base_env: Mapping[str, str] | None = None,
57
+ ) -> dict[str, str]:
58
+ """Build the subprocess environment for the resolved engine.
59
+
60
+ Returns a new dict (never mutates base_env). Only when `version` is
61
+ set does it add the matching tenv version-pin + auto-install vars.
62
+ """
63
+ env = dict(base_env if base_env is not None else os.environ)
64
+ if version is not None:
65
+ if engine == "tofu":
66
+ env[TOFU_VERSION_ENV] = version
67
+ env[TOFU_AUTO_INSTALL_ENV] = "true"
68
+ elif engine == "terraform":
69
+ env[TERRAFORM_VERSION_ENV] = version
70
+ env[TERRAFORM_AUTO_INSTALL_ENV] = "true"
71
+ return env
72
+
73
+
74
+ def build_init_argv(binary: str, *, no_color: bool = False) -> list[str]:
75
+ argv = [binary, "init"]
76
+ if no_color:
77
+ argv.append("-no-color")
78
+ return argv
79
+
80
+
81
+ def build_render_apply_argv(
82
+ binary: str, target: str, *, no_color: bool = False
83
+ ) -> list[str]:
84
+ argv = [binary, "apply", f"-target={target}", "-auto-approve"]
85
+ if no_color:
86
+ argv.append("-no-color")
87
+ return argv
88
+
89
+
90
+ def build_plan_argv(
91
+ binary: str, out_file: str = DEFAULT_PLAN_FILE, *, no_color: bool = False
92
+ ) -> list[str]:
93
+ argv = [binary, "plan", f"-out={out_file}"]
94
+ if no_color:
95
+ argv.append("-no-color")
96
+ return argv
97
+
98
+
99
+ def build_show_text_argv(
100
+ binary: str, plan_file: str = DEFAULT_PLAN_FILE, *, no_color: bool = False
101
+ ) -> list[str]:
102
+ argv = [binary, "show"]
103
+ if no_color:
104
+ argv.append("-no-color")
105
+ argv.append(plan_file)
106
+ return argv
107
+
108
+
109
+ def build_show_json_argv(binary: str, plan_file: str = DEFAULT_PLAN_FILE) -> list[str]:
110
+ return [binary, "show", "-json", plan_file]
111
+
112
+
113
+ def build_apply_argv(
114
+ binary: str,
115
+ *,
116
+ plan_file: str | None = None,
117
+ auto_approve: bool = False,
118
+ no_color: bool = False,
119
+ ) -> list[str]:
120
+ argv = [binary, "apply"]
121
+ if no_color:
122
+ argv.append("-no-color")
123
+ if plan_file is not None:
124
+ argv.append(plan_file)
125
+ return argv
126
+ if auto_approve:
127
+ argv.append("-auto-approve")
128
+ return argv
129
+
130
+
131
+ def build_destroy_argv(
132
+ binary: str,
133
+ *,
134
+ auto_approve: bool = False,
135
+ no_color: bool = False,
136
+ ) -> list[str]:
137
+ argv = [binary, "destroy"]
138
+ if no_color:
139
+ argv.append("-no-color")
140
+ if auto_approve:
141
+ argv.append("-auto-approve")
142
+ return argv
nac/tools.py ADDED
@@ -0,0 +1,169 @@
1
+ # SPDX-License-Identifier: MPL-2.0
2
+ # Copyright (c) 2026 Daniel Schmidt
3
+
4
+ import logging
5
+ import re
6
+ import shutil
7
+ import subprocess
8
+ from dataclasses import dataclass
9
+ from typing import Literal
10
+
11
+ from packaging.specifiers import InvalidSpecifier, SpecifierSet
12
+ from packaging.version import InvalidVersion, Version
13
+
14
+ from nac.config import NacConfig, resolve_test_data, resolve_validate_data
15
+ from nac.manifest import Manifest, compatible_range
16
+ from nac.output import format_command
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ _SPECIFIER_PREFIXES = ("==", "!=", ">=", "<=", "~=", ">", "<")
21
+ _VERSION_RE = re.compile(r"\d+(?:\.\d+)+(?:[a-zA-Z0-9.\-+]*)?")
22
+
23
+
24
+ def _pin_suffix(version: str) -> str:
25
+ v = version.strip()
26
+ return v if v.startswith(_SPECIFIER_PREFIXES) else f"=={v}"
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class ToolSource:
31
+ """Where a wrapped tool (`nac-validate`/`nac-test`) will be run from.
32
+
33
+ `version` is populated whenever a local binary was found, even if it
34
+ didn't satisfy `constraint` (mode is then "uvx") -- so callers can
35
+ explain *why* uvx was used instead of the local install.
36
+ """
37
+
38
+ mode: Literal["local", "uvx"]
39
+ path: str | None
40
+ version: str | None
41
+
42
+
43
+ def _local_tool_version(binary: str) -> str | None:
44
+ argv = [binary, "--version"]
45
+ logger.debug("Running %s", format_command(argv))
46
+ try:
47
+ result = subprocess.run(
48
+ argv,
49
+ capture_output=True,
50
+ text=True,
51
+ encoding="utf-8",
52
+ errors="replace",
53
+ check=False,
54
+ )
55
+ except OSError:
56
+ return None
57
+ text = (result.stdout or "") + (result.stderr or "")
58
+ match = _VERSION_RE.search(text)
59
+ return match.group(0) if match else None
60
+
61
+
62
+ def _satisfies(version: str, constraint: str) -> bool:
63
+ try:
64
+ spec = SpecifierSet(_pin_suffix(constraint))
65
+ return spec.contains(Version(version), prereleases=True)
66
+ except (InvalidSpecifier, InvalidVersion):
67
+ return False
68
+
69
+
70
+ def tool_constraint(
71
+ tool: Literal["nac-validate", "nac-test"],
72
+ explicit: str | None,
73
+ manifest: Manifest | None,
74
+ ) -> str | None:
75
+ """Version constraint to resolve `tool` with.
76
+
77
+ An explicit `tools.nac_validate`/`tools.nac_test` always wins. Otherwise,
78
+ if the module's manifest lists a tested version, default to the latest
79
+ semver-compatible release of it rather than the latest overall.
80
+ """
81
+ if explicit is not None:
82
+ return explicit
83
+ tested = manifest.tools.get(tool) if manifest is not None else None
84
+ return compatible_range(tested) if tested is not None else None
85
+
86
+
87
+ def exact_pin(constraint: str) -> str | None:
88
+ """The single version `constraint` allows (`1.2.0`, `==1.2.0`), else None."""
89
+ try:
90
+ specs = list(SpecifierSet(_pin_suffix(constraint)))
91
+ except InvalidSpecifier:
92
+ return None
93
+ if len(specs) != 1:
94
+ return None
95
+ spec = specs[0]
96
+ if spec.operator not in ("==", "===") or "*" in spec.version:
97
+ return None
98
+ return spec.version
99
+
100
+
101
+ def resolve_tool_source(
102
+ tool: Literal["nac-validate", "nac-test"], constraint: str | None
103
+ ) -> ToolSource:
104
+ """Prefer a local install that satisfies `constraint`; else fall back to uvx.
105
+
106
+ Mirrors the PATH-first resolution `nac.terraform.resolve_engine_binary`
107
+ does for terraform/tofu, adapted for the fact that there's no external
108
+ version manager here -- `nac` itself checks the constraint.
109
+ """
110
+ path = shutil.which(tool)
111
+ if path is None:
112
+ return ToolSource(mode="uvx", path=None, version=None)
113
+
114
+ version = _local_tool_version(path)
115
+ if constraint is None or (version is not None and _satisfies(version, constraint)):
116
+ return ToolSource(mode="local", path=path, version=version)
117
+ return ToolSource(mode="uvx", path=None, version=version)
118
+
119
+
120
+ def _tool_prefix(
121
+ tool: Literal["nac-validate", "nac-test"], constraint: str | None
122
+ ) -> list[str]:
123
+ source = resolve_tool_source(tool, constraint)
124
+ if source.mode == "local":
125
+ assert source.path is not None
126
+ return [source.path]
127
+ argv = ["uvx"]
128
+ if constraint is not None:
129
+ argv += ["--from", f"{tool}{_pin_suffix(constraint)}"]
130
+ return argv + [tool]
131
+
132
+
133
+ def build_validate_argv(cfg: NacConfig, manifest: Manifest | None = None) -> list[str]:
134
+ data = resolve_validate_data(cfg)
135
+ constraint = tool_constraint("nac-validate", cfg.tools.nac_validate, manifest)
136
+ argv = _tool_prefix("nac-validate", constraint)
137
+ argv += data
138
+ if cfg.validate.schema is not None:
139
+ argv += ["-s", cfg.validate.schema]
140
+ for rule in cfg.validate.rules or []:
141
+ argv += ["-r", rule]
142
+ return argv
143
+
144
+
145
+ def build_test_argv(cfg: NacConfig, manifest: Manifest | None = None) -> list[str]:
146
+ data = resolve_test_data(cfg)
147
+ constraint = tool_constraint("nac-test", cfg.tools.nac_test, manifest)
148
+ argv = _tool_prefix("nac-test", constraint)
149
+ for d in data:
150
+ argv += ["-d", d]
151
+ if cfg.test.templates is not None:
152
+ argv += ["-t", cfg.test.templates]
153
+ if cfg.test.filters is not None:
154
+ argv += ["-f", cfg.test.filters]
155
+ argv += ["-o", cfg.test.output]
156
+ return argv
157
+
158
+
159
+ def build_prewarm_argv(
160
+ tool: Literal["nac-validate", "nac-test"], version: str | None
161
+ ) -> list[str]:
162
+ name = f"{tool}{_pin_suffix(version)}" if version is not None else tool
163
+ return ["uv", "tool", "install", "--from", name, tool]
164
+
165
+
166
+ def build_help_argv(
167
+ tool: Literal["nac-validate", "nac-test"], version: str | None
168
+ ) -> list[str]:
169
+ return _tool_prefix(tool, version) + ["--help"]
@@ -0,0 +1,386 @@
1
+ Metadata-Version: 2.5
2
+ Name: nac-cli
3
+ Version: 0.1.0
4
+ Summary: An umbrella CLI wrapping terraform/tofu, nac-validate, and nac-test for Network-as-Code workflows.
5
+ Project-URL: homepage, https://github.com/netascode/nac
6
+ Project-URL: repository, https://github.com/netascode/nac
7
+ Project-URL: documentation, https://github.com/netascode/nac
8
+ Author-email: Daniel Schmidt <danischm@cisco.com>
9
+ Maintainer-email: Daniel Schmidt <danischm@cisco.com>
10
+ License: MPL-2.0
11
+ License-File: LICENSE
12
+ Keywords: automation,cli,nac,network-as-code,opentofu,terraform
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Topic :: Software Development :: Build Tools
24
+ Requires-Python: >=3.11
25
+ Requires-Dist: httpx>=0.28
26
+ Requires-Dist: packaging>=24
27
+ Requires-Dist: platformdirs>=4.3
28
+ Requires-Dist: pydantic>=2.9
29
+ Requires-Dist: ruamel-yaml>=0.19.1
30
+ Requires-Dist: typer>=0.25.1
31
+ Provides-Extra: dev
32
+ Requires-Dist: bandit[toml]>=1.9.1; extra == 'dev'
33
+ Requires-Dist: pre-commit>=4.4.0; extra == 'dev'
34
+ Requires-Dist: pytest-cov>=7.0.0; extra == 'dev'
35
+ Requires-Dist: pytest-mock>=3.15.1; extra == 'dev'
36
+ Requires-Dist: pytest>=9.0.1; extra == 'dev'
37
+ Requires-Dist: ruff>=0.14.5; extra == 'dev'
38
+ Requires-Dist: ty>=0.0.78; extra == 'dev'
39
+ Description-Content-Type: text/markdown
40
+
41
+ [![Tests](https://github.com/netascode/nac/actions/workflows/test.yml/badge.svg)](https://github.com/netascode/nac/actions/workflows/test.yml)
42
+ ![Python Support](https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-informational "Python Support: 3.11, 3.12, 3.13, 3.14")
43
+
44
+ # nac
45
+
46
+ An umbrella CLI for Network-as-Code workflows, driven by a single
47
+ `nac.yaml` config file.
48
+
49
+ ## Overview
50
+
51
+ Network-as-Code repos run the same workflow regardless of architecture:
52
+ validate the data, plan, apply, test. `nac` is one CLI for all of it. Instead
53
+ of juggling `terraform`/`tofu`, `nac-validate` and `nac-test` with their own
54
+ flags, you describe the repo once in `nac.yaml` and run `nac validate`,
55
+ `nac plan`, `nac apply`, `nac test`. The same commands work on a laptop and
56
+ in CI.
57
+
58
+ ## Installation
59
+
60
+ Install `nac` itself and there's nothing else to set up: it resolves and
61
+ runs the rest of the toolchain for you (via `uvx`, falling back to a local
62
+ install if one's already on `PATH`), so there's no separate
63
+ `nac-validate`/`nac-test` install or version-pinning to manage yourself.
64
+
65
+ The package is published on PyPI as **`nac-cli`**; the command it installs is
66
+ `nac`. Install [`uv`](https://docs.astral.sh/uv/getting-started/installation/)
67
+ first if you don't have it, then:
68
+
69
+ ```bash
70
+ uv tool install nac-cli
71
+ ```
72
+
73
+ Upgrade or uninstall with:
74
+
75
+ ```bash
76
+ uv tool upgrade nac-cli
77
+ uv tool uninstall nac-cli
78
+ ```
79
+
80
+ Pin a specific version with `uv tool install nac-cli==0.1.0`.
81
+
82
+ > [!WARNING]
83
+ > Don't `pip install nac` or run `uvx nac` -- that is a different, unrelated
84
+ > package.
85
+
86
+ ## Prerequisites
87
+
88
+ - [`uv`](https://docs.astral.sh/uv/getting-started/installation/) — always
89
+ required; `nac` uses it to run `nac-validate`/`nac-test` via `uvx`, so
90
+ you never install those two yourself.
91
+ - `terraform` or `tofu` on `PATH` — if neither is installed, `nac setup` can
92
+ bootstrap-install OpenTofu for you (see
93
+ [Tool/version management](#toolversion-management)).
94
+ - [`tenv`](https://github.com/tofuutils/tenv) — only needed if you pin
95
+ `tools.terraform.version` in `nac.yaml`.
96
+
97
+ Run `nac setup` to verify all of the above (plus any `env.required`
98
+ variables from your config) before running anything else.
99
+
100
+ ## Quickstart
101
+
102
+ A `nac.yaml` at the repo root is optional — every field defaults, so an
103
+ empty or missing file is perfectly valid for a repo that sticks to the
104
+ `data/`/`tests/` conventions and needs no env vars checked. The most
105
+ common lean config just declares the environment variables the wrapped tools
106
+ expect, so `nac setup` has something to check:
107
+
108
+ ```yaml
109
+ env:
110
+ required: [ACI_URL, ACI_USERNAME, ACI_PASSWORD]
111
+ ```
112
+
113
+ Then, from the repo root:
114
+
115
+ ```bash
116
+ nac setup # verify prerequisites, report resolved tool versions
117
+ nac init # terraform/tofu init
118
+ nac validate # nac-validate against the configured data paths
119
+ nac plan # terraform/tofu plan -> plan.tfplan
120
+ nac apply # apply the saved plan (or interactively, without one)
121
+ nac test # nac-test against the configured data/templates/output
122
+ nac destroy # destroy the managed infrastructure (interactive unless --auto-approve)
123
+ ```
124
+
125
+ Every field not shown above falls back to a sensible default — see
126
+ [`nac.yaml` reference](#nacyaml-reference) below for the full set of
127
+ defaults and a worked example for repos that render a `model.yaml`
128
+ before validating (e.g. NX-OS).
129
+
130
+ ## CLI reference
131
+
132
+ Global options, available on every subcommand:
133
+
134
+ | Option | Env var | Default | Description |
135
+ |---|---|---|---|
136
+ | `--config` | `NAC_CONFIG` | `./nac.yaml` | Path to the config file. |
137
+ | `-v`, `--verbosity` | `NAC_VERBOSITY` | `WARNING` | `DEBUG`\|`INFO`\|`WARNING`\|`ERROR`\|`CRITICAL`. |
138
+ | `--no-color` | `NO_COLOR` | off | Disable colored output. |
139
+ | `--version` | | | Show the version and exit. |
140
+
141
+ Every subcommand except `setup` forwards extra arguments to the wrapped tool
142
+ (e.g. `nac plan -var-file=prod.tfvars`), and `--help` shows `nac`'s help
143
+ followed by the wrapped tool's own options. The wrapped tool's exit code is
144
+ returned. `--artifacts`, available on the same subcommands, also writes the
145
+ command's output to `<command>.txt` in `working_dir` (e.g. `validate.txt`).
146
+
147
+ ### `nac setup`
148
+
149
+ Verifies prerequisites and reports resolved tool versions — a pre-flight for
150
+ CI and local use. Checks `uv` is on `PATH`; checks `tenv` only if
151
+ `tools.terraform.version` is set; resolves the `terraform`/`tofu` binary
152
+ (explicit `tools.terraform.engine`, or auto-detected: `tofu` then
153
+ `terraform`); checks every `env.required` variable is set (never prints
154
+ values); reports resolved `terraform`/`tofu`, `nac-validate`, and `nac-test`
155
+ versions, and warns if `nac-validate`/`nac-test` are outside the module's
156
+ [tested versions](#tested-versions). Fails fast with a non-zero exit on any
157
+ missing prerequisite (tested-versions warnings never fail it).
158
+
159
+ | Flag | Description |
160
+ |---|---|
161
+ | `--yes`, `--install` | Skip the confirmation prompt for the OpenTofu bootstrap install. |
162
+ | `--prewarm` | Pre-warm the `uvx` cache for `nac-validate`/`nac-test` — skipped for a tool that already resolves to a local install. |
163
+
164
+ ### `nac init`
165
+
166
+ Streams `terraform init` (or `tofu init`) in `working_dir`.
167
+
168
+ | Flag | Description |
169
+ |---|---|
170
+ | `--artifacts` | Also write the output to `init.txt`. |
171
+
172
+ ### `nac validate`
173
+
174
+ If `render.target` is set, first streams a targeted
175
+ `terraform apply -target=<target> -auto-approve` to produce `render.output`,
176
+ then streams `nac-validate` against the resolved data paths (`render.output`
177
+ if a render just happened, else `data.paths`). The render step is fully
178
+ implied by config, never a separate opt-in.
179
+
180
+ | Flag | Description |
181
+ |---|---|
182
+ | `--artifacts` | Also write the output (render step included) to `validate.txt`. |
183
+
184
+ ### `nac plan`
185
+
186
+ Streams `terraform plan -out=plan.tfplan`.
187
+
188
+ | Flag | Description |
189
+ |---|---|
190
+ | `--artifacts` | Also stream `terraform show` twice to emit `plan.txt` and `plan.json` alongside the saved plan. |
191
+
192
+ ### `nac apply`
193
+
194
+ If a saved `plan.tfplan` exists in `working_dir`, streams
195
+ `terraform apply plan.tfplan` (and deletes the plan file after a successful
196
+ apply); otherwise runs `terraform apply` interactively (stdin is inherited,
197
+ so the normal confirmation prompt works).
198
+
199
+ | Flag | Description |
200
+ |---|---|
201
+ | `--auto-approve` | Pass `-auto-approve` when applying without a saved plan file. |
202
+ | `--artifacts` | Also write the output to `apply.txt`. |
203
+
204
+ ### `nac destroy`
205
+
206
+ Streams `terraform destroy` (interactive unless `--auto-approve`, matching
207
+ `terraform destroy`'s own default confirmation prompt).
208
+
209
+ | Flag | Description |
210
+ |---|---|
211
+ | `--auto-approve` | Pass `-auto-approve` when destroying. |
212
+ | `--artifacts` | Also write the output to `destroy.txt`. |
213
+
214
+ ### `nac test`
215
+
216
+ Streams `nac-test` against the resolved data paths (`render.output` if
217
+ `render` is configured, else `data.paths`, plus `data.defaults` whenever
218
+ it's configured) and the `templates`/`filters`/`output` from the `test:`
219
+ config block.
220
+
221
+ | Flag | Description |
222
+ |---|---|
223
+ | `--artifacts` | Also write the output to `test.txt`. |
224
+
225
+ ## `nac.yaml` reference
226
+
227
+ A single `nac.yaml` at the repo root (path overridable with `--config`)
228
+ drives every subcommand. Every field is optional, and so is the file itself
229
+ — a missing `nac.yaml` is treated the same as an empty one, with every
230
+ field falling back to its default. That's a legitimate config for a repo
231
+ that needs no `env.required` checks and follows the `data/`/`tests/`
232
+ conventions, not just a fallback for ad hoc smoke-testing.
233
+
234
+ **Data resolution rules** (no boolean toggles anywhere — both follow
235
+ automatically from whether `render`/`render.target`/`data.defaults` are
236
+ configured):
237
+
238
+ - **validate data** = `render.output` if `render.target` is set, else
239
+ `data.paths`.
240
+ - **test data** = (`render.output` if `render` is configured at all, else
241
+ `data.paths`) + `data.defaults` (appended whenever it's configured).
242
+
243
+ **Field reference:**
244
+
245
+ | Field | Default | Notes |
246
+ |---|---|---|
247
+ | `working_dir` | `.` | Directory `terraform`/`tofu` runs in. |
248
+ | `data.paths` | `[data/]` | Raw data source(s) for `validate`/`test` when no render applies. |
249
+ | `data.defaults` | `defaults.yaml` if it exists in `working_dir`, else unset | Appended to `test` data whenever configured. |
250
+ | `render.target` | unset | Terraform target for a pre-validation render, e.g. `module.nxos.local_sensitive_file.model`. |
251
+ | `render.output` | unset | Rendered file path, e.g. `model.yaml`; required if `render.target` is set. |
252
+ | `validate.schema` | unset → flag omitted | Passed to `nac-validate -s`; omitted lets `nac-validate` use its own default. |
253
+ | `validate.rules` | unset → flag omitted | Passed to `nac-validate -r`; omitted lets `nac-validate` use its own default. |
254
+ | `test.templates` | `tests/templates` if that directory exists in `working_dir`, else unset → flag omitted | Passed to `nac-test -t`; if omitted, `nac-test` uses templates provided by a module or bundle. |
255
+ | `test.filters` | `tests/filters` if that directory exists in `working_dir`, else unset | Passed to `nac-test -f`. |
256
+ | `test.output` | `tests/results` | Passed to `nac-test -o`. |
257
+ | `tools.terraform.engine` | auto-detected: `tofu` if on `PATH`, else `terraform` | Explicit value always wins over detection. |
258
+ | `tools.terraform.version` | unset | Only set if you need `tenv` to pin/install a specific version. |
259
+ | `tools.nac_validate` | unset (latest compatible with the module's [tested version](#tested-versions); without a manifest: any local install, else latest via `uvx`) | Version/spec a local install must satisfy, else passed to `uvx --from nac-validate<spec>`; e.g. `1.2.0` or `>=0.9,<1.0`. |
260
+ | `tools.nac_test` | unset (latest compatible with the module's [tested version](#tested-versions); without a manifest: any local install, else latest via `uvx`) | Version/spec a local install must satisfy, else passed to `uvx --from nac-test<spec>`; e.g. `2.0.0` or `>=2.0,<3.0`. |
261
+ | `env.required` | `[]` | Variable names `nac setup` checks are present (values are never printed). |
262
+
263
+ **Worked examples.** Lean NX-OS (render + validate + test all use the
264
+ rendered `model.yaml`):
265
+
266
+ ```yaml
267
+ tools:
268
+ terraform:
269
+ engine: tofu
270
+
271
+ render:
272
+ target: module.nxos.local_sensitive_file.model
273
+ output: model.yaml
274
+
275
+ env:
276
+ required: [NXOS_USERNAME, NXOS_PASSWORD]
277
+ ```
278
+
279
+ Lean ACI (no `render` at all; `test` appends `data.defaults` since
280
+ `defaults.yaml` exists at the repo root):
281
+
282
+ ```yaml
283
+ env:
284
+ required: [ACI_URL, ACI_USERNAME, ACI_PASSWORD]
285
+ ```
286
+
287
+ Fully-expanded NX-OS config, with every field made explicit — useful when a
288
+ repo genuinely deviates from convention:
289
+
290
+ ```yaml
291
+ working_dir: .
292
+
293
+ data:
294
+ paths: [data/]
295
+ defaults: null
296
+
297
+ render:
298
+ target: module.nxos.local_sensitive_file.model
299
+ output: model.yaml
300
+
301
+ validate:
302
+ schema: .schema.yaml
303
+ rules: [.rules]
304
+
305
+ test:
306
+ templates: tests/templates
307
+ filters: tests/filters
308
+ output: tests/results
309
+
310
+ tools:
311
+ terraform:
312
+ engine: tofu
313
+ version: "1.9.5"
314
+ nac_validate: ">=0.9,<1.0"
315
+ nac_test: ">=2.0,<3.0"
316
+
317
+ env:
318
+ required: [NXOS_USERNAME, NXOS_PASSWORD]
319
+ ```
320
+
321
+ ## Tool/version management
322
+
323
+ - **`terraform`/`tofu`**: with no `tools.terraform.version` set, `nac`
324
+ resolves the binary directly via `PATH` — `tofu` first, `terraform` as
325
+ fallback (or whichever `tools.terraform.engine` names explicitly). If
326
+ neither is found, `nac setup` (interactively, or with `--yes`/`--install`)
327
+ offers to download and cache a verified OpenTofu binary itself — no
328
+ package manager or `tenv` required. Setting `tools.terraform.version`
329
+ switches to [`tenv`](https://github.com/tofuutils/tenv) for version
330
+ pinning/auto-install (`TOFUENV_TOFU_VERSION`/`TFENV_TERRAFORM_VERSION` +
331
+ the matching auto-install env var).
332
+ - **`nac-validate`/`nac-test`**: prefers a local install already on `PATH`
333
+ if it satisfies `tools.nac_validate`/`tools.nac_test` (unset means any
334
+ local version is acceptable) — same PATH-first philosophy as
335
+ `terraform`/`tofu` above. Otherwise falls back to `uvx`
336
+ (`uv tool run`) -- `uvx --from nac-validate==<version> nac-validate ...`.
337
+ Omit `tools.nac_validate`/`tools.nac_test` to let `uvx` resolve the latest
338
+ published release when no usable local install exists -- or, if the
339
+ module ships a [manifest](#tested-versions), the latest release compatible
340
+ with the tested one. `nac setup` reports which source (`local` or `uvx`)
341
+ each tool actually resolved to.
342
+
343
+ ## Tested versions
344
+
345
+ A Network-as-Code Terraform module can ship a `nac/manifest.yaml` --
346
+ alongside its other nac-specific content (schema, rules, test templates) --
347
+ listing the `nac-validate`/`nac-test` versions it was tested with:
348
+
349
+ ```yaml
350
+ schema: 1
351
+ tools:
352
+ nac-validate: "2.0.0"
353
+ nac-test: "2.0.0"
354
+ ```
355
+
356
+ `terraform`/`tofu` and provider versions don't belong here: the module's
357
+ `versions.tf` (`required_version`, `required_providers`) already constrains
358
+ them, and Terraform enforces that itself.
359
+
360
+ After `nac init` has downloaded the module, `nac` finds the manifest via
361
+ `.terraform/modules/modules.json` (the shallowest module that ships one,
362
+ so a local wrapper around the module works too). It is purely a hint --
363
+ nothing is ever enforced, and without a manifest (e.g. before the first
364
+ `init`) nothing changes:
365
+
366
+ - **Defaults:** with `tools.nac_validate`/`tools.nac_test` unset,
367
+ `nac-validate`/`nac-test` resolve to the latest release compatible with
368
+ the tested version instead of the latest overall -- a local install is
369
+ used if it's in that range, otherwise `uvx` fetches it.
370
+ - **Warnings:** an explicit `tools.nac_validate`/`tools.nac_test` value
371
+ always wins, but if it resolves to a version outside the tested,
372
+ semver-compatible range, `nac validate`/`nac test` (and `nac setup`)
373
+ print a warning (to stderr, and into the `--artifacts` file), plus a
374
+ short note after the output if the command then fails. Exit codes are
375
+ never changed. A range resolved through `uvx` (e.g. `>=1.0`) isn't
376
+ checked, since the version `uvx` picks isn't known up front; an exact pin
377
+ or a local install is.
378
+
379
+ "Compatible" means the tested version or newer, up to the next major --
380
+ or the next minor for `0.x` versions, where a minor bump may be breaking.
381
+ A tested `2.0.0` allows `>=2.0.0,<3`, a tested `0.13.1` allows
382
+ `>=0.13.1,<0.14`. Newer-but-compatible versions are only logged at `INFO`.
383
+
384
+ ## License
385
+
386
+ [MPL-2.0](LICENSE)