nac-cli 0.1.0__tar.gz → 0.2.1__tar.gz

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 (73) hide show
  1. {nac_cli-0.1.0 → nac_cli-0.2.1}/.github/workflows/release.yml +2 -2
  2. {nac_cli-0.1.0 → nac_cli-0.2.1}/.github/workflows/test.yml +3 -3
  3. nac_cli-0.2.1/CHANGELOG.md +13 -0
  4. nac_cli-0.2.1/PKG-INFO +106 -0
  5. nac_cli-0.2.1/README.md +66 -0
  6. {nac_cli-0.1.0 → nac_cli-0.2.1}/pyproject.toml +1 -1
  7. nac_cli-0.2.1/release-notes.md +3 -0
  8. {nac_cli-0.1.0 → nac_cli-0.2.1}/src/nac/cli/main.py +27 -2
  9. {nac_cli-0.1.0 → nac_cli-0.2.1}/src/nac/cli/setup.py +27 -9
  10. {nac_cli-0.1.0 → nac_cli-0.2.1}/src/nac/cli/test.py +18 -9
  11. {nac_cli-0.1.0 → nac_cli-0.2.1}/src/nac/cli/validate.py +24 -7
  12. {nac_cli-0.1.0 → nac_cli-0.2.1}/src/nac/cli/versions.py +8 -5
  13. {nac_cli-0.1.0 → nac_cli-0.2.1}/src/nac/config.py +57 -5
  14. {nac_cli-0.1.0 → nac_cli-0.2.1}/src/nac/exceptions.py +4 -0
  15. {nac_cli-0.1.0 → nac_cli-0.2.1}/src/nac/manifest.py +114 -13
  16. {nac_cli-0.1.0 → nac_cli-0.2.1}/src/nac/tools.py +55 -1
  17. {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/unit/test_cli_commands.py +182 -9
  18. {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/unit/test_cli_setup.py +98 -0
  19. {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/unit/test_cli_versions.py +9 -8
  20. {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/unit/test_config.py +67 -0
  21. {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/unit/test_manifest.py +108 -0
  22. {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/unit/test_tools.py +42 -2
  23. nac_cli-0.2.1/uv.lock +811 -0
  24. nac_cli-0.1.0/CHANGELOG.md +0 -3
  25. nac_cli-0.1.0/PKG-INFO +0 -386
  26. nac_cli-0.1.0/README.md +0 -346
  27. nac_cli-0.1.0/release-notes.md +0 -2
  28. nac_cli-0.1.0/uv.lock +0 -872
  29. {nac_cli-0.1.0 → nac_cli-0.2.1}/.github/dependabot.yml +0 -0
  30. {nac_cli-0.1.0 → nac_cli-0.2.1}/.gitignore +0 -0
  31. {nac_cli-0.1.0 → nac_cli-0.2.1}/.pre-commit-config.yaml +0 -0
  32. {nac_cli-0.1.0 → nac_cli-0.2.1}/LICENSE +0 -0
  33. {nac_cli-0.1.0 → nac_cli-0.2.1}/scripts/license_headers.py +0 -0
  34. {nac_cli-0.1.0 → nac_cli-0.2.1}/src/nac/__init__.py +0 -0
  35. {nac_cli-0.1.0 → nac_cli-0.2.1}/src/nac/__main__.py +0 -0
  36. {nac_cli-0.1.0 → nac_cli-0.2.1}/src/nac/bootstrap.py +0 -0
  37. {nac_cli-0.1.0 → nac_cli-0.2.1}/src/nac/cli/__init__.py +0 -0
  38. {nac_cli-0.1.0 → nac_cli-0.2.1}/src/nac/cli/apply.py +0 -0
  39. {nac_cli-0.1.0 → nac_cli-0.2.1}/src/nac/cli/destroy.py +0 -0
  40. {nac_cli-0.1.0 → nac_cli-0.2.1}/src/nac/cli/init.py +0 -0
  41. {nac_cli-0.1.0 → nac_cli-0.2.1}/src/nac/cli/options.py +0 -0
  42. {nac_cli-0.1.0 → nac_cli-0.2.1}/src/nac/cli/passthrough.py +0 -0
  43. {nac_cli-0.1.0 → nac_cli-0.2.1}/src/nac/cli/plan.py +0 -0
  44. {nac_cli-0.1.0 → nac_cli-0.2.1}/src/nac/output.py +0 -0
  45. {nac_cli-0.1.0 → nac_cli-0.2.1}/src/nac/runner.py +0 -0
  46. {nac_cli-0.1.0 → nac_cli-0.2.1}/src/nac/terraform.py +0 -0
  47. {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/fixtures/nac-yaml/aci-lean/defaults.yaml +0 -0
  48. {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/fixtures/nac-yaml/aci-lean/nac.yaml +0 -0
  49. {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/fixtures/nac-yaml/filters-absent/nac.yaml +0 -0
  50. {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/fixtures/nac-yaml/filters-present/nac.yaml +0 -0
  51. {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/fixtures/nac-yaml/filters-present/tests/filters/.gitkeep +0 -0
  52. {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/fixtures/nac-yaml/invalid-syntax/nac.yaml +0 -0
  53. {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/fixtures/nac-yaml/iosxe-lean/nac.yaml +0 -0
  54. {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/fixtures/nac-yaml/minimal/nac.yaml +0 -0
  55. {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/fixtures/nac-yaml/nxos-full/nac.yaml +0 -0
  56. {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/fixtures/nac-yaml/nxos-lean/nac.yaml +0 -0
  57. {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/fixtures/nac-yaml/templates-absent/nac.yaml +0 -0
  58. {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/fixtures/nac-yaml/templates-present/nac.yaml +0 -0
  59. {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/fixtures/nac-yaml/templates-present/tests/templates/.gitkeep +0 -0
  60. {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/integration/fixtures/local_only/.terraform.lock.hcl +0 -0
  61. {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/integration/fixtures/local_only/broken/main.tf +0 -0
  62. {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/integration/fixtures/local_only/main.tf +0 -0
  63. {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/integration/fixtures/validate_test/.schema.yaml +0 -0
  64. {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/integration/fixtures/validate_test/data/main.yaml +0 -0
  65. {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/integration/fixtures/validate_test/templates/test.robot +0 -0
  66. {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/integration/test_streaming.py +0 -0
  67. {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/integration/test_validate_test.py +0 -0
  68. {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/unit/conftest.py +0 -0
  69. {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/unit/test_bootstrap.py +0 -0
  70. {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/unit/test_output.py +0 -0
  71. {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/unit/test_placeholder.py +0 -0
  72. {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/unit/test_runner.py +0 -0
  73. {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/unit/test_terraform.py +0 -0
@@ -20,7 +20,7 @@ jobs:
20
20
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
21
21
 
22
22
  - name: Install uv
23
- uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
23
+ uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
24
24
 
25
25
  - name: Check tag matches project version
26
26
  env:
@@ -102,7 +102,7 @@ jobs:
102
102
  id-token: write # Required for PyPI trusted publishing
103
103
  steps:
104
104
  - name: Install uv
105
- uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
105
+ uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
106
106
 
107
107
  - name: Download distributions
108
108
  uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
@@ -21,7 +21,7 @@ jobs:
21
21
  uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
22
22
 
23
23
  - name: Install uv
24
- uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
24
+ uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
25
25
  with:
26
26
  enable-cache: true
27
27
 
@@ -49,7 +49,7 @@ jobs:
49
49
  uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
50
50
 
51
51
  - name: Install uv
52
- uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
52
+ uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
53
53
  with:
54
54
  enable-cache: true
55
55
 
@@ -89,7 +89,7 @@ jobs:
89
89
  python-version: ${{ matrix.python }}
90
90
 
91
91
  - name: Install uv
92
- uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
92
+ uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
93
93
  with:
94
94
  enable-cache: true
95
95
 
@@ -0,0 +1,13 @@
1
+ ## 0.2.1
2
+
3
+ - Read `nac.yaml` defaults from a `config` section in the module's `nac/manifest.yaml`: `render.resource` (relative to the module, e.g. `local_sensitive_file.model` becomes `module.nxos.local_sensitive_file.model`), `render.output` and `env.required`. Settings in the project's `nac.yaml` take precedence key by key, an explicit `null` turns a default off, and an invalid `config` section is ignored with a warning; `nac setup` lists the defaults taken from the module
4
+ - Fail `nac validate` with a clear message if the render step didn't write `render.output` (e.g. because it doesn't match the module's `write_model_file`), and show the output path in the Render Summary
5
+
6
+ ## 0.2.0
7
+
8
+ - Only require `uv` when `nac-validate`/`nac-test` aren't installed locally in a satisfying version: `nac setup` no longer fails without `uv` if both tools resolve locally, and `nac validate`/`nac test` fail with a clear message (instead of a missing-executable error) when `uvx` would be needed but `uv` isn't on `PATH`
9
+ - Enable `nac-validate`'s and `nac-test`'s `--artifacts` option (schema, rules and test templates packaged with the module or with bundles) by default via `NAC_VALIDATE_ARTIFACTS`/`NAC_TEST_ARTIFACTS`; versions without the option ignore it, and an existing value of the env var or a passed-through `--no-artifacts` takes precedence
10
+
11
+ ## 0.1.0
12
+
13
+ - Initial release
nac_cli-0.2.1/PKG-INFO ADDED
@@ -0,0 +1,106 @@
1
+ Metadata-Version: 2.5
2
+ Name: nac-cli
3
+ Version: 0.2.1
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 `nac.yaml`
47
+ config file.
48
+
49
+ Every Network-as-Code repository runs the same workflow, regardless of the
50
+ architecture: validate the data model, plan, apply and test. Each step uses a
51
+ different tool with its own options: `terraform` or `tofu`, `nac-validate` and
52
+ `nac-test`. `nac` is a single CLI for the whole workflow. The repository is
53
+ described once in `nac.yaml`, and the same commands work on a laptop and in a
54
+ CI/CD pipeline:
55
+
56
+ ```bash
57
+ nac setup # verify prerequisites and report resolved tool versions
58
+ nac init # terraform/tofu init
59
+ nac validate # nac-validate against the configured data paths
60
+ nac plan # terraform/tofu plan -> plan.tfplan
61
+ nac apply # apply the saved plan (or interactively, without one)
62
+ nac test # nac-test against the configured data/templates/output
63
+ nac destroy # destroy the managed infrastructure
64
+ ```
65
+
66
+ Full documentation: https://netascode.cisco.com/docs/tools/nac/overview
67
+
68
+ ## How It Works
69
+
70
+ - **One configuration file:** `nac.yaml` defines where the data model,
71
+ schema, rules and test templates are located, which Terraform engine to use
72
+ and which environment variables are required. Every field has a default, so
73
+ a repository following the `data/` and `tests/` conventions works without a
74
+ `nac.yaml` at all.
75
+ - **Managed toolchain:** `nac-validate` and `nac-test` are used from a local
76
+ installation if one satisfies the configured version, or run via `uvx`
77
+ otherwise. If neither `terraform` nor `tofu` is installed, `nac setup` can
78
+ install OpenTofu.
79
+ - **Tested versions:** a Network-as-Code Terraform module can declare the
80
+ `nac-validate` and `nac-test` versions it was tested with in
81
+ `nac/manifest.yaml`. `nac` then uses a compatible version by default and
82
+ warns about versions outside of the tested range.
83
+ - **Pass-through:** arguments not known to `nac` are forwarded to the wrapped
84
+ tool, and the exit code of the wrapped tool is returned.
85
+
86
+ ## Installation
87
+
88
+ `nac` is published on PyPI as **`nac-cli`**; the command it installs is `nac`.
89
+ Install it with [uv](https://docs.astral.sh/uv/getting-started/installation/):
90
+
91
+ ```bash
92
+ uv tool install nac-cli
93
+ ```
94
+
95
+ > [!WARNING]
96
+ > Don't `pip install nac` or run `uvx nac` -- that is a different, unrelated
97
+ > package.
98
+
99
+ See the [installation docs](https://netascode.cisco.com/docs/tools/nac/installation/)
100
+ for prerequisites, and [usage](https://netascode.cisco.com/docs/tools/nac/usage/)
101
+ and [configuration](https://netascode.cisco.com/docs/tools/nac/configuration/)
102
+ for the full command and `nac.yaml` reference.
103
+
104
+ ## License
105
+
106
+ [MPL-2.0](LICENSE)
@@ -0,0 +1,66 @@
1
+ [![Tests](https://github.com/netascode/nac/actions/workflows/test.yml/badge.svg)](https://github.com/netascode/nac/actions/workflows/test.yml)
2
+ ![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")
3
+
4
+ # nac
5
+
6
+ An umbrella CLI for Network-as-Code workflows, driven by a single `nac.yaml`
7
+ config file.
8
+
9
+ Every Network-as-Code repository runs the same workflow, regardless of the
10
+ architecture: validate the data model, plan, apply and test. Each step uses a
11
+ different tool with its own options: `terraform` or `tofu`, `nac-validate` and
12
+ `nac-test`. `nac` is a single CLI for the whole workflow. The repository is
13
+ described once in `nac.yaml`, and the same commands work on a laptop and in a
14
+ CI/CD pipeline:
15
+
16
+ ```bash
17
+ nac setup # verify prerequisites and report resolved tool versions
18
+ nac init # terraform/tofu init
19
+ nac validate # nac-validate against the configured data paths
20
+ nac plan # terraform/tofu plan -> plan.tfplan
21
+ nac apply # apply the saved plan (or interactively, without one)
22
+ nac test # nac-test against the configured data/templates/output
23
+ nac destroy # destroy the managed infrastructure
24
+ ```
25
+
26
+ Full documentation: https://netascode.cisco.com/docs/tools/nac/overview
27
+
28
+ ## How It Works
29
+
30
+ - **One configuration file:** `nac.yaml` defines where the data model,
31
+ schema, rules and test templates are located, which Terraform engine to use
32
+ and which environment variables are required. Every field has a default, so
33
+ a repository following the `data/` and `tests/` conventions works without a
34
+ `nac.yaml` at all.
35
+ - **Managed toolchain:** `nac-validate` and `nac-test` are used from a local
36
+ installation if one satisfies the configured version, or run via `uvx`
37
+ otherwise. If neither `terraform` nor `tofu` is installed, `nac setup` can
38
+ install OpenTofu.
39
+ - **Tested versions:** a Network-as-Code Terraform module can declare the
40
+ `nac-validate` and `nac-test` versions it was tested with in
41
+ `nac/manifest.yaml`. `nac` then uses a compatible version by default and
42
+ warns about versions outside of the tested range.
43
+ - **Pass-through:** arguments not known to `nac` are forwarded to the wrapped
44
+ tool, and the exit code of the wrapped tool is returned.
45
+
46
+ ## Installation
47
+
48
+ `nac` is published on PyPI as **`nac-cli`**; the command it installs is `nac`.
49
+ Install it with [uv](https://docs.astral.sh/uv/getting-started/installation/):
50
+
51
+ ```bash
52
+ uv tool install nac-cli
53
+ ```
54
+
55
+ > [!WARNING]
56
+ > Don't `pip install nac` or run `uvx nac` -- that is a different, unrelated
57
+ > package.
58
+
59
+ See the [installation docs](https://netascode.cisco.com/docs/tools/nac/installation/)
60
+ for prerequisites, and [usage](https://netascode.cisco.com/docs/tools/nac/usage/)
61
+ and [configuration](https://netascode.cisco.com/docs/tools/nac/configuration/)
62
+ for the full command and `nac.yaml` reference.
63
+
64
+ ## License
65
+
66
+ [MPL-2.0](LICENSE)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nac-cli"
3
- version = "0.1.0"
3
+ version = "0.2.1"
4
4
  description = "An umbrella CLI wrapping terraform/tofu, nac-validate, and nac-test for Network-as-Code workflows."
5
5
  license = { text = "MPL-2.0" }
6
6
  readme = "README.md"
@@ -0,0 +1,3 @@
1
+
2
+ - Read `nac.yaml` defaults from a `config` section in the module's `nac/manifest.yaml`: `render.resource` (relative to the module, e.g. `local_sensitive_file.model` becomes `module.nxos.local_sensitive_file.model`), `render.output` and `env.required`. Settings in the project's `nac.yaml` take precedence key by key, an explicit `null` turns a default off, and an invalid `config` section is ignored with a warning; `nac setup` lists the defaults taken from the module
3
+ - Fail `nac validate` with a clear message if the render step didn't write `render.output` (e.g. because it doesn't match the module's `write_model_file`), and show the output path in the Render Summary
@@ -12,12 +12,23 @@ from typing import NoReturn, cast
12
12
  import typer
13
13
 
14
14
  from nac import terraform
15
- from nac.config import DEFAULT_CONFIG_PATH, NacConfig, load_config
15
+ from nac.config import (
16
+ DEFAULT_CONFIG_PATH,
17
+ NacConfig,
18
+ build_config,
19
+ merge_defaults,
20
+ read_raw_config,
21
+ )
16
22
  from nac.exceptions import ConfigError, EngineNotFoundError, NoEngineAvailableError
23
+ from nac.manifest import load_manifest
17
24
  from nac.output import ensure_utf8_streams
18
25
 
19
26
  from .options import ConfigPath, NoColor, Verbosity, VerbosityLevel, Version
20
27
 
28
+ # ctx.meta keys set by get_config
29
+ MANIFEST_META_KEY = "manifest"
30
+ MODULE_DEFAULTS_META_KEY = "module_defaults"
31
+
21
32
  app = typer.Typer(
22
33
  add_completion=False,
23
34
  help="An umbrella CLI wrapping terraform/tofu, nac-validate, and nac-test.",
@@ -47,13 +58,27 @@ def get_config(ctx: typer.Context) -> NacConfig:
47
58
  file present. `ctx.obj` starts out holding the raw --config path and
48
59
  is replaced with the resolved NacConfig on first call, so repeated
49
60
  calls within the same invocation don't re-read the file.
61
+
62
+ nac.yaml settings the project leaves unset are filled from the `config`
63
+ section of the initialized module's manifest. The manifest and the
64
+ dotted keys taken from it are stashed on `ctx.meta`.
50
65
  """
51
66
  if isinstance(ctx.obj, NacConfig):
52
67
  return ctx.obj
68
+ path = cast(Path, ctx.obj)
53
69
  try:
54
- cfg = load_config(cast(Path, ctx.obj))
70
+ raw = read_raw_config(path)
71
+ cfg = build_config(raw, path)
72
+ manifest = load_manifest(cfg.working_dir)
73
+ applied: list[str] = []
74
+ if manifest is not None and manifest.config:
75
+ merged, applied = merge_defaults(raw, manifest.config)
76
+ if applied:
77
+ cfg = build_config(merged, path)
55
78
  except ConfigError as exc:
56
79
  fail(str(exc))
80
+ ctx.meta[MANIFEST_META_KEY] = manifest
81
+ ctx.meta[MODULE_DEFAULTS_META_KEY] = applied
57
82
  ctx.obj = cfg
58
83
  return cfg
59
84
 
@@ -19,16 +19,12 @@ from nac.manifest import MANIFEST_FILE
19
19
  from nac.output import apply_no_color_env, echo_summary, format_command
20
20
  from nac.tools import ToolSource
21
21
 
22
- from .main import app, fail, get_config, get_no_color
22
+ from .main import MODULE_DEFAULTS_META_KEY, app, fail, get_config, get_no_color
23
23
  from .options import Prewarm, Yes
24
24
  from .versions import emit_warnings, get_manifest, tool_warnings
25
25
 
26
26
  logger = logging.getLogger(__name__)
27
27
 
28
- UV_NOT_FOUND = (
29
- "`uv` not found on PATH -- install it: "
30
- "https://docs.astral.sh/uv/getting-started/installation/"
31
- )
32
28
  TENV_NOT_FOUND = (
33
29
  "`tenv` not found on PATH -- required because `tools.terraform.version` "
34
30
  "is set. Install tenv: https://github.com/tofuutils/tenv"
@@ -89,9 +85,7 @@ def setup(ctx: typer.Context, yes: Yes = False, prewarm: Prewarm = False) -> Non
89
85
  cfg = get_config(ctx)
90
86
  no_color = get_no_color(ctx)
91
87
 
92
- if shutil.which("uv") is None:
93
- fail(UV_NOT_FOUND)
94
- lines: list[str] = ["uv: found"]
88
+ lines: list[str] = []
95
89
 
96
90
  if cfg.tools.terraform.version is not None and shutil.which("tenv") is None:
97
91
  fail(TENV_NOT_FOUND)
@@ -120,7 +114,14 @@ def setup(ctx: typer.Context, yes: Yes = False, prewarm: Prewarm = False) -> Non
120
114
 
121
115
  manifest = get_manifest(ctx, cfg)
122
116
  if manifest is not None:
123
- lines.append(f"tested versions: {manifest.module} ({MANIFEST_FILE})")
117
+ if manifest.tools:
118
+ lines.append(f"tested versions: {manifest.module} ({MANIFEST_FILE})")
119
+ module_defaults = ctx.meta.get(MODULE_DEFAULTS_META_KEY) or []
120
+ if module_defaults:
121
+ lines.append(
122
+ f"module defaults: {', '.join(module_defaults)} "
123
+ f"(from {manifest.module})"
124
+ )
124
125
 
125
126
  missing_env = [name for name in cfg.env.required if name not in os.environ]
126
127
  if missing_env:
@@ -151,6 +152,23 @@ def setup(ctx: typer.Context, yes: Yes = False, prewarm: Prewarm = False) -> Non
151
152
  test_line, test_source, test_constraint = _tool_summary_line(
152
153
  "nac-test", cfg.tools.nac_test
153
154
  )
155
+ uv_errors: list[str] = []
156
+ if validate_source.mode == "uvx":
157
+ uv_errors.append(
158
+ tools.uv_not_found_message(
159
+ "nac-validate", validate_source, validate_constraint
160
+ )
161
+ )
162
+ if test_source.mode == "uvx":
163
+ uv_errors.append(
164
+ tools.uv_not_found_message("nac-test", test_source, test_constraint)
165
+ )
166
+ if tools.uv_available():
167
+ lines.insert(0, "uv: found")
168
+ elif uv_errors:
169
+ fail("\n".join(uv_errors))
170
+ else:
171
+ lines.insert(0, "uv: not found (not needed, all tools resolve locally)")
154
172
  lines.append(validate_line)
155
173
  lines.append(test_line)
156
174
  warnings = [
@@ -6,9 +6,9 @@
6
6
  import typer
7
7
 
8
8
  from nac import runner, tools
9
- from nac.output import apply_no_color_env
9
+ from nac.exceptions import UvNotFoundError
10
10
 
11
- from .main import app, get_config, get_no_color
11
+ from .main import app, fail, get_config, get_no_color
12
12
  from .options import Artifacts
13
13
  from .passthrough import (
14
14
  PASSTHROUGH_CONTEXT_SETTINGS,
@@ -34,15 +34,24 @@ def test_(ctx: typer.Context, artifacts: Artifacts = False) -> None:
34
34
  log_path = cfg.working_dir / "test.txt" if artifacts else None
35
35
  manifest = get_manifest(ctx, cfg)
36
36
 
37
- if wants_help(ctx):
38
- show_help_and_exit(
39
- ctx,
37
+ try:
38
+ tool_argv = (
40
39
  tools.build_help_argv(
41
40
  "nac-test",
42
41
  tools.tool_constraint("nac-test", cfg.tools.nac_test, manifest),
43
- ),
42
+ )
43
+ if wants_help(ctx)
44
+ else tools.build_test_argv(cfg, manifest) + ctx.args
45
+ )
46
+ except UvNotFoundError as exc:
47
+ fail(str(exc))
48
+
49
+ if wants_help(ctx):
50
+ show_help_and_exit(
51
+ ctx,
52
+ tool_argv,
44
53
  cwd=cfg.working_dir,
45
- env=apply_no_color_env(no_color),
54
+ env=tools.build_tool_env("nac-test", no_color=no_color),
46
55
  no_color=no_color,
47
56
  tool_label="nac-test",
48
57
  usage_as="nac test",
@@ -52,9 +61,9 @@ def test_(ctx: typer.Context, artifacts: Artifacts = False) -> None:
52
61
  log_written = emit_warnings(warnings, no_color=no_color, log_path=log_path)
53
62
 
54
63
  code = runner.run_streaming(
55
- tools.build_test_argv(cfg, manifest) + ctx.args,
64
+ tool_argv,
56
65
  cwd=cfg.working_dir,
57
- env=apply_no_color_env(no_color),
66
+ env=tools.build_tool_env("nac-test", no_color=no_color),
58
67
  log_path=log_path,
59
68
  append=log_written,
60
69
  no_color=no_color,
@@ -8,9 +8,10 @@ import logging
8
8
  import typer
9
9
 
10
10
  from nac import runner, terraform, tools
11
- from nac.output import GREEN, apply_no_color_env, color, echo_summary
11
+ from nac.exceptions import UvNotFoundError
12
+ from nac.output import GREEN, color, echo_summary
12
13
 
13
- from .main import app, get_config, get_no_color, resolve_engine_or_exit
14
+ from .main import app, fail, get_config, get_no_color, resolve_engine_or_exit
14
15
  from .options import Artifacts
15
16
  from .passthrough import (
16
17
  PASSTHROUGH_CONTEXT_SETTINGS,
@@ -36,12 +37,21 @@ def validate(ctx: typer.Context, artifacts: Artifacts = False) -> None:
36
37
  manifest = get_manifest(ctx, cfg)
37
38
  constraint = tools.tool_constraint("nac-validate", cfg.tools.nac_validate, manifest)
38
39
 
40
+ try:
41
+ tool_argv = (
42
+ tools.build_help_argv("nac-validate", constraint)
43
+ if wants_help(ctx)
44
+ else tools.build_validate_argv(cfg, manifest) + ctx.args
45
+ )
46
+ except UvNotFoundError as exc:
47
+ fail(str(exc))
48
+
39
49
  if wants_help(ctx):
40
50
  show_help_and_exit(
41
51
  ctx,
42
- tools.build_help_argv("nac-validate", constraint),
52
+ tool_argv,
43
53
  cwd=cfg.working_dir,
44
- env=apply_no_color_env(no_color),
54
+ env=tools.build_tool_env("nac-validate", no_color=no_color),
45
55
  no_color=no_color,
46
56
  tool_label="nac-validate",
47
57
  usage_as="nac validate",
@@ -68,20 +78,27 @@ def validate(ctx: typer.Context, artifacts: Artifacts = False) -> None:
68
78
  log_written = True
69
79
  if code != 0:
70
80
  raise typer.Exit(code=code)
81
+ output = cfg.render.output
82
+ if output is not None and not (cfg.working_dir / output).exists():
83
+ fail(
84
+ f"Render step `{cfg.render.target}` did not write `{output}`. "
85
+ "Check that `render.output` matches the file written by the "
86
+ "render target (for Network-as-Code modules: `write_model_file`)."
87
+ )
71
88
  if quiet:
72
89
  echo_summary(
73
90
  "Render Summary",
74
91
  [
75
92
  f"Render: {color(GREEN, 'OK', no_color=no_color)} "
76
- f"({cfg.render.target})"
93
+ f"({cfg.render.target} -> {output})"
77
94
  ],
78
95
  no_color=no_color,
79
96
  )
80
97
 
81
98
  code = runner.run_streaming(
82
- tools.build_validate_argv(cfg, manifest) + ctx.args,
99
+ tool_argv,
83
100
  cwd=cfg.working_dir,
84
- env=apply_no_color_env(no_color),
101
+ env=tools.build_tool_env("nac-validate", no_color=no_color),
85
102
  log_path=log_path,
86
103
  append=log_written,
87
104
  no_color=no_color,
@@ -19,14 +19,17 @@ from nac.config import NacConfig
19
19
  from nac.manifest import Manifest, load_manifest
20
20
  from nac.output import YELLOW, color
21
21
 
22
- _MANIFEST_KEY = "manifest"
22
+ from .main import MANIFEST_META_KEY
23
23
 
24
24
 
25
25
  def get_manifest(ctx: typer.Context, cfg: NacConfig) -> Manifest | None:
26
- """Load (once per invocation) the manifest of the initialized module."""
27
- if _MANIFEST_KEY not in ctx.meta:
28
- ctx.meta[_MANIFEST_KEY] = load_manifest(cfg.working_dir)
29
- return cast("Manifest | None", ctx.meta[_MANIFEST_KEY])
26
+ """Load (once per invocation) the manifest of the initialized module.
27
+
28
+ Normally already loaded by `get_config`.
29
+ """
30
+ if MANIFEST_META_KEY not in ctx.meta:
31
+ ctx.meta[MANIFEST_META_KEY] = load_manifest(cfg.working_dir)
32
+ return cast("Manifest | None", ctx.meta[MANIFEST_META_KEY])
30
33
 
31
34
 
32
35
  def tool_warnings(
@@ -5,8 +5,9 @@
5
5
  # names as nac.yaml's own keys; pydantic only warns because they shadow
6
6
  # deprecated pydantic-v1 classmethods that this codebase never calls.
7
7
  import warnings
8
+ from collections.abc import Mapping
8
9
  from pathlib import Path
9
- from typing import Literal
10
+ from typing import Any, Literal
10
11
 
11
12
  import pydantic
12
13
  import ruamel.yaml
@@ -95,15 +96,28 @@ class NacConfig(BaseModel):
95
96
  env: EnvConfig = EnvConfig()
96
97
 
97
98
 
98
- def load_config(path: Path | str = DEFAULT_CONFIG_PATH) -> NacConfig:
99
+ def load_config(
100
+ path: Path | str = DEFAULT_CONFIG_PATH,
101
+ *,
102
+ defaults: Mapping[str, Any] | None = None,
103
+ ) -> NacConfig:
99
104
  """Load, validate, and resolve conventions for a nac.yaml file.
100
105
 
101
106
  A missing file is treated as an empty config -- every field defaults,
102
- so `nac` works with zero config present. Raises ConfigError (never a
103
- raw pydantic/ruamel traceback) on malformed YAML or a schema
104
- validation failure.
107
+ so `nac` works with zero config present. `defaults` (a module's
108
+ manifest `config`) fills in whatever the file doesn't set; see
109
+ `merge_defaults`. Raises ConfigError (never a raw pydantic/ruamel
110
+ traceback) on malformed YAML or a schema validation failure.
105
111
  """
106
112
  path = Path(path)
113
+ raw = read_raw_config(path)
114
+ if defaults is not None:
115
+ raw, _ = merge_defaults(raw, defaults)
116
+ return build_config(raw, path)
117
+
118
+
119
+ def read_raw_config(path: Path) -> dict[str, Any]:
120
+ """The nac.yaml mapping as written, without validation or defaults."""
107
121
  raw: object = {}
108
122
  if path.is_file():
109
123
  yaml = ruamel.yaml.YAML(typ="safe")
@@ -119,7 +133,11 @@ def load_config(path: Path | str = DEFAULT_CONFIG_PATH) -> NacConfig:
119
133
  raise ConfigError(
120
134
  f"Config file {path} must contain a YAML mapping at the top level"
121
135
  )
136
+ return raw
122
137
 
138
+
139
+ def build_config(raw: Mapping[str, Any], path: Path) -> NacConfig:
140
+ """Validate a raw nac.yaml mapping and apply filesystem conventions."""
123
141
  try:
124
142
  cfg = NacConfig.model_validate(raw)
125
143
  except pydantic.ValidationError as exc:
@@ -128,6 +146,40 @@ def load_config(path: Path | str = DEFAULT_CONFIG_PATH) -> NacConfig:
128
146
  return _apply_filesystem_defaults(cfg, path)
129
147
 
130
148
 
149
+ def merge_defaults(
150
+ raw: Mapping[str, Any], defaults: Mapping[str, Any]
151
+ ) -> tuple[dict[str, Any], list[str]]:
152
+ """Fill `raw` (nac.yaml as written) with `defaults` (from a manifest).
153
+
154
+ Mappings are merged key by key; any other value set in `raw` -- lists
155
+ and an explicit `null` included -- wins as a whole. So
156
+ `render: {output: x.yaml}` keeps the module's render target, while
157
+ `render: null` or `render: {target: null}` turns it off.
158
+
159
+ Returns the merged mapping and the dotted keys taken from `defaults`.
160
+ """
161
+ merged = dict(raw)
162
+ applied: list[str] = []
163
+ for key, value in defaults.items():
164
+ if key not in raw:
165
+ merged[key] = value
166
+ applied.extend(_leaf_keys(key, value))
167
+ elif isinstance(raw[key], Mapping) and isinstance(value, Mapping):
168
+ merged[key], nested = merge_defaults(raw[key], value)
169
+ applied.extend(f"{key}.{leaf}" for leaf in nested)
170
+ return merged, applied
171
+
172
+
173
+ def _leaf_keys(prefix: str, value: Any) -> list[str]:
174
+ if isinstance(value, Mapping) and value:
175
+ return [
176
+ leaf
177
+ for key, nested in value.items()
178
+ for leaf in _leaf_keys(f"{prefix}.{key}", nested)
179
+ ]
180
+ return [prefix]
181
+
182
+
131
183
  def _format_validation_error(path: Path, exc: pydantic.ValidationError) -> str:
132
184
  lines = [f"Invalid config file: {path}"]
133
185
  for err in exc.errors():
@@ -32,3 +32,7 @@ class BootstrapDeclinedError(BootstrapError):
32
32
 
33
33
  class ChecksumMismatchError(BootstrapError):
34
34
  """The downloaded archive's SHA-256 didn't match the published checksum."""
35
+
36
+
37
+ class UvNotFoundError(NacError):
38
+ """A wrapped tool needs `uvx` (no usable local install) but uv isn't on PATH."""