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.
- {nac_cli-0.1.0 → nac_cli-0.2.1}/.github/workflows/release.yml +2 -2
- {nac_cli-0.1.0 → nac_cli-0.2.1}/.github/workflows/test.yml +3 -3
- nac_cli-0.2.1/CHANGELOG.md +13 -0
- nac_cli-0.2.1/PKG-INFO +106 -0
- nac_cli-0.2.1/README.md +66 -0
- {nac_cli-0.1.0 → nac_cli-0.2.1}/pyproject.toml +1 -1
- nac_cli-0.2.1/release-notes.md +3 -0
- {nac_cli-0.1.0 → nac_cli-0.2.1}/src/nac/cli/main.py +27 -2
- {nac_cli-0.1.0 → nac_cli-0.2.1}/src/nac/cli/setup.py +27 -9
- {nac_cli-0.1.0 → nac_cli-0.2.1}/src/nac/cli/test.py +18 -9
- {nac_cli-0.1.0 → nac_cli-0.2.1}/src/nac/cli/validate.py +24 -7
- {nac_cli-0.1.0 → nac_cli-0.2.1}/src/nac/cli/versions.py +8 -5
- {nac_cli-0.1.0 → nac_cli-0.2.1}/src/nac/config.py +57 -5
- {nac_cli-0.1.0 → nac_cli-0.2.1}/src/nac/exceptions.py +4 -0
- {nac_cli-0.1.0 → nac_cli-0.2.1}/src/nac/manifest.py +114 -13
- {nac_cli-0.1.0 → nac_cli-0.2.1}/src/nac/tools.py +55 -1
- {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/unit/test_cli_commands.py +182 -9
- {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/unit/test_cli_setup.py +98 -0
- {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/unit/test_cli_versions.py +9 -8
- {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/unit/test_config.py +67 -0
- {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/unit/test_manifest.py +108 -0
- {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/unit/test_tools.py +42 -2
- nac_cli-0.2.1/uv.lock +811 -0
- nac_cli-0.1.0/CHANGELOG.md +0 -3
- nac_cli-0.1.0/PKG-INFO +0 -386
- nac_cli-0.1.0/README.md +0 -346
- nac_cli-0.1.0/release-notes.md +0 -2
- nac_cli-0.1.0/uv.lock +0 -872
- {nac_cli-0.1.0 → nac_cli-0.2.1}/.github/dependabot.yml +0 -0
- {nac_cli-0.1.0 → nac_cli-0.2.1}/.gitignore +0 -0
- {nac_cli-0.1.0 → nac_cli-0.2.1}/.pre-commit-config.yaml +0 -0
- {nac_cli-0.1.0 → nac_cli-0.2.1}/LICENSE +0 -0
- {nac_cli-0.1.0 → nac_cli-0.2.1}/scripts/license_headers.py +0 -0
- {nac_cli-0.1.0 → nac_cli-0.2.1}/src/nac/__init__.py +0 -0
- {nac_cli-0.1.0 → nac_cli-0.2.1}/src/nac/__main__.py +0 -0
- {nac_cli-0.1.0 → nac_cli-0.2.1}/src/nac/bootstrap.py +0 -0
- {nac_cli-0.1.0 → nac_cli-0.2.1}/src/nac/cli/__init__.py +0 -0
- {nac_cli-0.1.0 → nac_cli-0.2.1}/src/nac/cli/apply.py +0 -0
- {nac_cli-0.1.0 → nac_cli-0.2.1}/src/nac/cli/destroy.py +0 -0
- {nac_cli-0.1.0 → nac_cli-0.2.1}/src/nac/cli/init.py +0 -0
- {nac_cli-0.1.0 → nac_cli-0.2.1}/src/nac/cli/options.py +0 -0
- {nac_cli-0.1.0 → nac_cli-0.2.1}/src/nac/cli/passthrough.py +0 -0
- {nac_cli-0.1.0 → nac_cli-0.2.1}/src/nac/cli/plan.py +0 -0
- {nac_cli-0.1.0 → nac_cli-0.2.1}/src/nac/output.py +0 -0
- {nac_cli-0.1.0 → nac_cli-0.2.1}/src/nac/runner.py +0 -0
- {nac_cli-0.1.0 → nac_cli-0.2.1}/src/nac/terraform.py +0 -0
- {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/fixtures/nac-yaml/aci-lean/defaults.yaml +0 -0
- {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/fixtures/nac-yaml/aci-lean/nac.yaml +0 -0
- {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/fixtures/nac-yaml/filters-absent/nac.yaml +0 -0
- {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/fixtures/nac-yaml/filters-present/nac.yaml +0 -0
- {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/fixtures/nac-yaml/filters-present/tests/filters/.gitkeep +0 -0
- {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/fixtures/nac-yaml/invalid-syntax/nac.yaml +0 -0
- {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/fixtures/nac-yaml/iosxe-lean/nac.yaml +0 -0
- {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/fixtures/nac-yaml/minimal/nac.yaml +0 -0
- {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/fixtures/nac-yaml/nxos-full/nac.yaml +0 -0
- {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/fixtures/nac-yaml/nxos-lean/nac.yaml +0 -0
- {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/fixtures/nac-yaml/templates-absent/nac.yaml +0 -0
- {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/fixtures/nac-yaml/templates-present/nac.yaml +0 -0
- {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/fixtures/nac-yaml/templates-present/tests/templates/.gitkeep +0 -0
- {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/integration/fixtures/local_only/.terraform.lock.hcl +0 -0
- {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/integration/fixtures/local_only/broken/main.tf +0 -0
- {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/integration/fixtures/local_only/main.tf +0 -0
- {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/integration/fixtures/validate_test/.schema.yaml +0 -0
- {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/integration/fixtures/validate_test/data/main.yaml +0 -0
- {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/integration/fixtures/validate_test/templates/test.robot +0 -0
- {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/integration/test_streaming.py +0 -0
- {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/integration/test_validate_test.py +0 -0
- {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/unit/conftest.py +0 -0
- {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/unit/test_bootstrap.py +0 -0
- {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/unit/test_output.py +0 -0
- {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/unit/test_placeholder.py +0 -0
- {nac_cli-0.1.0 → nac_cli-0.2.1}/tests/unit/test_runner.py +0 -0
- {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@
|
|
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@
|
|
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@
|
|
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@
|
|
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@
|
|
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
|
+
[](https://github.com/netascode/nac/actions/workflows/test.yml)
|
|
42
|
+

|
|
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)
|
nac_cli-0.2.1/README.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
[](https://github.com/netascode/nac/actions/workflows/test.yml)
|
|
2
|
+

|
|
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)
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
38
|
-
|
|
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=
|
|
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
|
-
|
|
64
|
+
tool_argv,
|
|
56
65
|
cwd=cfg.working_dir,
|
|
57
|
-
env=
|
|
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.
|
|
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
|
-
|
|
52
|
+
tool_argv,
|
|
43
53
|
cwd=cfg.working_dir,
|
|
44
|
-
env=
|
|
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
|
-
|
|
99
|
+
tool_argv,
|
|
83
100
|
cwd=cfg.working_dir,
|
|
84
|
-
env=
|
|
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
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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(
|
|
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.
|
|
103
|
-
|
|
104
|
-
|
|
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."""
|