Skip to main content

Tests Python Support

nac-artifacts

A Python library to discover and layer the schema, rules and test artifacts packaged with Network-as-Code modules and bundles. It is shared by nac-validate and nac-test.

Status: early development; the API may still change.

Overview

Schema, validation rules and test templates can come from three places. They are layered, highest priority first:

  1. Local: files and paths configured in the project itself
  2. Bundle: an archive (or directory) dropped into the project, e.g. a set of rules and tests
  3. Module: the nac/ directory shipped inside the installed Terraform module, however the module was sourced (registry, git, archive, local path)

nac-artifacts resolves the bundle and module layers. The tools decide how to merge what they find: for example, nac-validate replaces rules by rule ID and nac-test replaces templates by relative path.

Artifact layout

Modules and bundles use the same layout below their nac/ directory:

nac/
├── schema.yaml                 # schema for nac-validate
├── rules/                      # semantic validation rules for nac-validate
└── tests/
    ├── templates/              # test templates for nac-test
    ├── jinja_filters/          # Jinja filters for nac-test
    └── jinja_tests/            # Jinja tests for nac-test (optional)

Bundles

A bundle is a .zip or .tar.gz archive (or an already extracted directory) with a manifest.yaml at its root next to the nac/ directory. An archive made by zipping a single folder is accepted as well.

# manifest.yaml
name: acme-base           # required; identifies the bundle in messages, independent of the file name
version: "1.2.0"          # must be a string: quote it, an unquoted 1.10 is read as 1.1
module:                   # optional
  versions: ">=0.3,<0.4"  # PEP 440 specifier
  • Compatibility: if module.versions is set, it is checked against the one installed module that has a nac/ directory, whichever artifacts the caller asked for. A mismatch raises BundleError. The check is skipped, with a log message, when no module is installed, several are, or the module's version is unknown (local and git sources have none).
  • Location: any .zip, .tar.gz or .tgz file directly in .nac/, or an extracted .nac/bundle/ directory. The file name is free (it can carry a version or architecture, such as acme-nxos-1.2.0.zip) and is never parsed: identity comes from the manifest name.
  • Several bundles: all bundles in .nac/ (or all passed explicitly) are peers in one priority tier, above the module. They may coexist as long as they provide different artifacts, which allows splitting content (for example rules in one bundle and tests in another). If two bundles provide the same artifact, the tool reports an error naming both. A bundle's overrides.yaml affects the module but not its peer bundles.
  • Same name twice: two bundles with the same manifest name (typically an old and a new version left in .nac/) are an error, because a forgotten old version would otherwise keep providing artifacts the new one dropped or renamed.
  • Bundles for other tools: a bundle found in .nac/ that provides nothing the calling tool uses is skipped (logged at INFO). A bundle passed explicitly that provides nothing is an error. A bundle with an overrides.yaml that disables something always counts, even without any artifacts of its own, so a bundle can consist of nothing but a manifest and an overrides.yaml.
  • Safety: archives are checked before extraction (no absolute or .. paths, no symlinks or special files, limits on archive size, extracted size and file count). Extraction is atomic and cached by the archive's sha256; cache contents are trusted without being verified again. Cache entries not used for 30 days (typically old bundle versions) are removed.
  • Trust: bundles usually contain code that the tools execute (validation rules, Jinja filters), so a bundle needs the same trust as code in your own repository.

Disabling artifacts of lower layers

A layer can add or replace artifacts of the layers below it. To remove some, list them in an overrides.yaml:

disable:
  rules: ["102"]                    # nac-validate: by rule ID (exact match)
  templates: ["config/*.robot"]     # nac-test: by path relative to the templates directory

The same format is read from two places:

  • .nac/overrides.yaml in the project: disables artifacts of the bundle and the module
  • overrides.yaml at the root of a bundle (next to manifest.yaml): disables artifacts of the module

Rules:

  • A layer's entries only affect layers below it. Its own artifacts and the local layer (files and paths you pass to the tools) are never affected; to get a disabled artifact back, add a copy to the local layer.
  • Entries are declarative: they state what must not be active. An entry that matches nothing (a typo, a module that no longer has the artifact, no bundle installed) is not an error. The tools show which entries matched in --list-artifacts.
  • The structure is validated: unknown keys, wrong types, empty entries and absolute or .. template paths are errors (OverridesError). Rule IDs may be written unquoted (102).
  • Template patterns are globs (fnmatch; * also matches /, so lib/* covers nested files) and are case-sensitive.
  • Bundle overrides can switch validations off, so treat a bundle like code you run. Each tool logs a summary at INFO level and shows what was disabled and by which layer.

Usage

from pathlib import Path

from nac_artifacts import resolve_artifact_layers

layers = resolve_artifact_layers(
    Path.cwd(),
    provides=("schema.yaml", "rules"),  # paths below nac/ this tool needs
)
for layer in layers:  # bundles (peers) first, then the module
    print(layer.label, layer.version, layer.nac_dir)

# Entries to disable per layer (same order as layers), from .nac/overrides.yaml and bundles
from nac_artifacts import DisableMatcher, layer_disables

for layer, disabled in zip(layers, layer_disables(Path.cwd(), layers)):
    matcher = DisableMatcher(disabled.rules)  # exact; use glob=True for templates
    if matcher.match("102"):
        ...  # rule 102 is disabled for this layer
    print(matcher.unmatched())  # entries that matched nothing, for reporting
  • A layer is used only if it has at least one of the provides paths (or, for a bundle, an overrides.yaml that disables something), so each tool picks the layers relevant to it. A layer may therefore lack some or all of the paths. provides must be a non-empty sequence of paths relative to nac/ (a plain string is rejected).
  • Modules are found through the modules.json that terraform init and tofu init write (honouring TF_DATA_DIR). Only the root module and the modules it calls directly are considered. If several provide the requested paths, AmbiguousModuleError lists them; pass module_dir to select one.
  • Explicit module_dir and bundles paths are returned as absolute paths. Relative ones are resolved against the current working directory (as for command line arguments), not against project_dir.
  • Bundles are found in .nac/, or pass bundles. None and an empty sequence both mean auto-detection, so forwarding an empty list of command line options keeps the default. Archives are extracted once into .nac/cache/, which ignores itself in git.
  • Failures raise ArtifactError subclasses (ModuleDiscoveryError, AmbiguousModuleError, BundleError).

nac_artifacts.testing provides builders for tests of tools that use this library: installed modules with a modules.json, and bundles as zip or tar.gz.

Development

uv sync --extra dev
uv run pytest
uv run pre-commit run --all-files

License

MPL-2.0

Metadata

Release files for nac-artifacts 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for nac-artifacts 0.1.0
File Size Uploaded
nac_artifacts-0.1.0.tar.gz 44.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for nac-artifacts 0.1.0
File Interpreter ABI Platform
nac_artifacts-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 73.6 kB

Release files / nac_artifacts-0.1.0.tar.gz

Download URL nac_artifacts-0.1.0.tar.gz
Size 44.6 kB
Tags Source
SHA-256 checksum
How to use checksums
ccf843a36d878fcc4088e9bf30ffdc63a6c4fe9b24f1354dff4c48236873b9bb
BLAKE2b-256 checksum
How to use checksums
371b971498e079007d11c9e3aa296e50b0c4de37d71ec570f538b7bfc0ab6b31
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.1 {"installer":{"name":"uv","version":"0.13.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / nac_artifacts-0.1.0-py3-none-any.whl

Download URL nac_artifacts-0.1.0-py3-none-any.whl
Size 29.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ff56ca318e118683992f393c211aaf55369d056245c147440882ff00832134a1
BLAKE2b-256 checksum
How to use checksums
2d7e4e6db179b68ae185b7611e7863e8cb8cd7529091e119c91233ef57f8c84d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.1 {"installer":{"name":"uv","version":"0.13.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page