nectar-conformance 0.1.0__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 (75) hide show
  1. nectar_conformance-0.1.0/PKG-INFO +117 -0
  2. nectar_conformance-0.1.0/README.md +92 -0
  3. nectar_conformance-0.1.0/nectar_conformance/__init__.py +3 -0
  4. nectar_conformance-0.1.0/nectar_conformance/cli/__init__.py +1 -0
  5. nectar_conformance-0.1.0/nectar_conformance/cli/commands/__init__.py +1 -0
  6. nectar_conformance-0.1.0/nectar_conformance/cli/commands/changelog_cmd.py +46 -0
  7. nectar_conformance-0.1.0/nectar_conformance/cli/commands/check_meta.py +112 -0
  8. nectar_conformance-0.1.0/nectar_conformance/cli/commands/check_run.py +136 -0
  9. nectar_conformance-0.1.0/nectar_conformance/cli/commands/diff.py +50 -0
  10. nectar_conformance-0.1.0/nectar_conformance/cli/commands/report_diff.py +52 -0
  11. nectar_conformance-0.1.0/nectar_conformance/cli/commands/version_cmd.py +110 -0
  12. nectar_conformance-0.1.0/nectar_conformance/cli/main.py +29 -0
  13. nectar_conformance-0.1.0/nectar_conformance/config.py +235 -0
  14. nectar_conformance-0.1.0/nectar_conformance/datasources/__init__.py +1 -0
  15. nectar_conformance-0.1.0/nectar_conformance/datasources/base.py +48 -0
  16. nectar_conformance-0.1.0/nectar_conformance/datasources/compile.py +110 -0
  17. nectar_conformance-0.1.0/nectar_conformance/datasources/puppetdb.py +178 -0
  18. nectar_conformance-0.1.0/nectar_conformance/datasources/static_repo.py +192 -0
  19. nectar_conformance-0.1.0/nectar_conformance/engine/__init__.py +1 -0
  20. nectar_conformance-0.1.0/nectar_conformance/engine/operators.py +103 -0
  21. nectar_conformance-0.1.0/nectar_conformance/engine/queries.py +72 -0
  22. nectar_conformance-0.1.0/nectar_conformance/engine/runner.py +331 -0
  23. nectar_conformance-0.1.0/nectar_conformance/engine/selectors.py +32 -0
  24. nectar_conformance-0.1.0/nectar_conformance/errors.py +38 -0
  25. nectar_conformance-0.1.0/nectar_conformance/model.py +113 -0
  26. nectar_conformance-0.1.0/nectar_conformance/plugins/__init__.py +1 -0
  27. nectar_conformance-0.1.0/nectar_conformance/plugins/base.py +31 -0
  28. nectar_conformance-0.1.0/nectar_conformance/plugins/registry.py +24 -0
  29. nectar_conformance-0.1.0/nectar_conformance/report/__init__.py +1 -0
  30. nectar_conformance-0.1.0/nectar_conformance/report/human.py +101 -0
  31. nectar_conformance-0.1.0/nectar_conformance/report/json_report.py +13 -0
  32. nectar_conformance-0.1.0/nectar_conformance/results/__init__.py +1 -0
  33. nectar_conformance-0.1.0/nectar_conformance/results/compare.py +66 -0
  34. nectar_conformance-0.1.0/nectar_conformance/results/model.py +196 -0
  35. nectar_conformance-0.1.0/nectar_conformance/results/serialise.py +72 -0
  36. nectar_conformance-0.1.0/nectar_conformance/rollout.py +109 -0
  37. nectar_conformance-0.1.0/nectar_conformance/rules/__init__.py +1 -0
  38. nectar_conformance-0.1.0/nectar_conformance/rules/changelog.py +311 -0
  39. nectar_conformance-0.1.0/nectar_conformance/rules/loader.py +84 -0
  40. nectar_conformance-0.1.0/nectar_conformance/rules/model.py +224 -0
  41. nectar_conformance-0.1.0/nectar_conformance/rules/schema.json +113 -0
  42. nectar_conformance-0.1.0/nectar_conformance/rules/schema.py +44 -0
  43. nectar_conformance-0.1.0/nectar_conformance/service.py +434 -0
  44. nectar_conformance-0.1.0/nectar_conformance/web/__init__.py +8 -0
  45. nectar_conformance-0.1.0/nectar_conformance/web/api.py +211 -0
  46. nectar_conformance-0.1.0/nectar_conformance/web/app.py +111 -0
  47. nectar_conformance-0.1.0/nectar_conformance/web/refresh.py +231 -0
  48. nectar_conformance-0.1.0/nectar_conformance/web/serialise.py +63 -0
  49. nectar_conformance-0.1.0/nectar_conformance/web/settings.py +63 -0
  50. nectar_conformance-0.1.0/nectar_conformance/web/store.py +71 -0
  51. nectar_conformance-0.1.0/nectar_conformance.egg-info/PKG-INFO +117 -0
  52. nectar_conformance-0.1.0/nectar_conformance.egg-info/SOURCES.txt +73 -0
  53. nectar_conformance-0.1.0/nectar_conformance.egg-info/dependency_links.txt +1 -0
  54. nectar_conformance-0.1.0/nectar_conformance.egg-info/entry_points.txt +14 -0
  55. nectar_conformance-0.1.0/nectar_conformance.egg-info/requires.txt +19 -0
  56. nectar_conformance-0.1.0/nectar_conformance.egg-info/top_level.txt +1 -0
  57. nectar_conformance-0.1.0/pyproject.toml +78 -0
  58. nectar_conformance-0.1.0/setup.cfg +4 -0
  59. nectar_conformance-0.1.0/tests/test_changelog.py +373 -0
  60. nectar_conformance-0.1.0/tests/test_cli.py +355 -0
  61. nectar_conformance-0.1.0/tests/test_compile.py +103 -0
  62. nectar_conformance-0.1.0/tests/test_config.py +75 -0
  63. nectar_conformance-0.1.0/tests/test_engine.py +236 -0
  64. nectar_conformance-0.1.0/tests/test_loader_schema.py +71 -0
  65. nectar_conformance-0.1.0/tests/test_operators.py +61 -0
  66. nectar_conformance-0.1.0/tests/test_puppetdb_source.py +111 -0
  67. nectar_conformance-0.1.0/tests/test_refresh.py +136 -0
  68. nectar_conformance-0.1.0/tests/test_report.py +38 -0
  69. nectar_conformance-0.1.0/tests/test_report_diff.py +45 -0
  70. nectar_conformance-0.1.0/tests/test_rollout.py +134 -0
  71. nectar_conformance-0.1.0/tests/test_service_web.py +149 -0
  72. nectar_conformance-0.1.0/tests/test_static_source.py +68 -0
  73. nectar_conformance-0.1.0/tests/test_web_api.py +127 -0
  74. nectar_conformance-0.1.0/tests/test_web_app.py +79 -0
  75. nectar_conformance-0.1.0/tests/test_web_internals.py +99 -0
@@ -0,0 +1,117 @@
1
+ Metadata-Version: 2.4
2
+ Name: nectar-conformance
3
+ Version: 0.1.0
4
+ Summary: Conformance checker for Nectar puppet-managed OpenStack cloud sites
5
+ Author: ARDC Nectar Core Services
6
+ License: Apache-2.0
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: httpx>=0.24
10
+ Requires-Dist: PyYAML>=6
11
+ Requires-Dist: jsonschema>=4
12
+ Requires-Dist: packaging>=21
13
+ Requires-Dist: cliff>=4
14
+ Requires-Dist: rich>=13
15
+ Provides-Extra: test
16
+ Requires-Dist: pytest>=7; extra == "test"
17
+ Requires-Dist: pytest-cov>=4; extra == "test"
18
+ Requires-Dist: respx>=0.20; extra == "test"
19
+ Requires-Dist: fastapi>=0.110; extra == "test"
20
+ Requires-Dist: uvicorn>=0.27; extra == "test"
21
+ Provides-Extra: static
22
+ Provides-Extra: web
23
+ Requires-Dist: fastapi>=0.110; extra == "web"
24
+ Requires-Dist: uvicorn[standard]>=0.27; extra == "web"
25
+
26
+ # nectar-conformance
27
+
28
+ A conformance checker for Nectar puppet-managed OpenStack cloud sites.
29
+
30
+ Nectar Core Services manage every site through a central puppet server. Sites drift
31
+ from the published specification over time. This tool tells a site operator how their
32
+ site conforms to a versioned Nectar conformance specification, what is wrong or
33
+ missing, and how to fix it.
34
+
35
+ ## How it works
36
+
37
+ ```
38
+ DataSource -> normalised Model -> Engine (versioned rules) -> Report -> human / JSON
39
+ ```
40
+
41
+ - A **site is a puppet environment**. The tool identifies a site by the node's
42
+ `environment` in PuppetDB.
43
+ - The **PuppetDB** data source (primary) reads the compiled catalog (resources and
44
+ parameters, with hiera fully resolved) and facts for every node in the site's
45
+ environment. The **static repo** data source reads pre-compiled catalog JSON, or
46
+ compiles catalogs from a site repo, for pre-deployment and commissioning checks.
47
+ - **Checks** are curated by Core Services as value-free *definitions* (the logic) plus
48
+ per-version *manifests* (which checks apply and their expected values). Conformance
49
+ is versioned; several conformance versions are active at once.
50
+
51
+ ## Usage
52
+
53
+ The check definitions and conformance changelog live in the separate
54
+ [`nectar-conformance-checks`](https://review.rc.nectar.org.au) repository, not in this
55
+ tool. Every command that reads them needs a checks directory: pass `--checks-dir <path>`,
56
+ set `NECTAR_CONFORMANCE_CHECKS_DIR`, or set `checks_dir` in the config file. The examples
57
+ below omit it for brevity.
58
+
59
+ ```
60
+ nectar-conformance check run --site ardctest --conformance-version 2025.1
61
+ nectar-conformance check run --site ardctest --conformance-version 2025.1 --format json
62
+ nectar-conformance check list --conformance-version 2025.1
63
+ nectar-conformance check show glance.api.image_tag
64
+ nectar-conformance version list
65
+ nectar-conformance version diff 2024.1 2025.1
66
+ ```
67
+
68
+ Static source (no live PuppetDB), either pre-compiled catalogs or compile-from-repo:
69
+
70
+ ```
71
+ # pre-compiled catalog JSON, one file per node
72
+ nectar-conformance check run --site ardctest --source static \
73
+ --catalog-dir ./catalogs --facts-dir ./facts
74
+
75
+ # compile from the site repo (one node per facts file; compiler set via
76
+ # static.compile_command in config, defaults to octocatalog-diff)
77
+ nectar-conformance check run --site ardctest --source static \
78
+ --site-repo /path/to/site-repo --facts-dir ./facts
79
+ ```
80
+
81
+ ### Will a change fix conformance before it goes live?
82
+
83
+ A site is a puppet environment, and r10k deploys each branch of the control repo as its
84
+ own environment, so you can check a *proposed* change pre-merge and compare it to the
85
+ live site. Capture the live baseline, capture the proposed environment, and diff them:
86
+
87
+ ```
88
+ # baseline: the live site
89
+ nectar-conformance check run --site ardctest --conformance-version 2025.1 \
90
+ --format json > before.json
91
+
92
+ # the proposed change, deployed as a branch environment (--environment keeps the
93
+ # 'ardctest' label but queries that environment)
94
+ nectar-conformance check run --site ardctest --environment ardctest_fix_glance \
95
+ --conformance-version 2025.1 --format json > after.json
96
+
97
+ # what does the change fix, and does it break anything? (exit 1 if it regresses)
98
+ nectar-conformance report diff before.json after.json
99
+ ```
100
+
101
+ For a truly offline check (no deploy, no node runs), produce `after.json` with the
102
+ static source by compiling the proposed branch (`--source static --site-repo ...`); that
103
+ needs the environment's modules assembled, which is what octocatalog-diff / r10k do.
104
+
105
+ Exit codes: `0` conformant, `1` conformance failure at or above the severity
106
+ threshold, `2` usage error, `3` operational error (PuppetDB unreachable, etc.).
107
+ `report diff` exits `1` if the change introduces any new failure.
108
+
109
+ ## Development
110
+
111
+ ```
112
+ tox # run unit tests and lint
113
+ tox -e pep8 # lint only
114
+ ```
115
+
116
+ Releases use [reno](https://docs.openstack.org/reno/) for release notes. Contributions
117
+ go through gerrit; use conventional commits and `git commit -s`.
@@ -0,0 +1,92 @@
1
+ # nectar-conformance
2
+
3
+ A conformance checker for Nectar puppet-managed OpenStack cloud sites.
4
+
5
+ Nectar Core Services manage every site through a central puppet server. Sites drift
6
+ from the published specification over time. This tool tells a site operator how their
7
+ site conforms to a versioned Nectar conformance specification, what is wrong or
8
+ missing, and how to fix it.
9
+
10
+ ## How it works
11
+
12
+ ```
13
+ DataSource -> normalised Model -> Engine (versioned rules) -> Report -> human / JSON
14
+ ```
15
+
16
+ - A **site is a puppet environment**. The tool identifies a site by the node's
17
+ `environment` in PuppetDB.
18
+ - The **PuppetDB** data source (primary) reads the compiled catalog (resources and
19
+ parameters, with hiera fully resolved) and facts for every node in the site's
20
+ environment. The **static repo** data source reads pre-compiled catalog JSON, or
21
+ compiles catalogs from a site repo, for pre-deployment and commissioning checks.
22
+ - **Checks** are curated by Core Services as value-free *definitions* (the logic) plus
23
+ per-version *manifests* (which checks apply and their expected values). Conformance
24
+ is versioned; several conformance versions are active at once.
25
+
26
+ ## Usage
27
+
28
+ The check definitions and conformance changelog live in the separate
29
+ [`nectar-conformance-checks`](https://review.rc.nectar.org.au) repository, not in this
30
+ tool. Every command that reads them needs a checks directory: pass `--checks-dir <path>`,
31
+ set `NECTAR_CONFORMANCE_CHECKS_DIR`, or set `checks_dir` in the config file. The examples
32
+ below omit it for brevity.
33
+
34
+ ```
35
+ nectar-conformance check run --site ardctest --conformance-version 2025.1
36
+ nectar-conformance check run --site ardctest --conformance-version 2025.1 --format json
37
+ nectar-conformance check list --conformance-version 2025.1
38
+ nectar-conformance check show glance.api.image_tag
39
+ nectar-conformance version list
40
+ nectar-conformance version diff 2024.1 2025.1
41
+ ```
42
+
43
+ Static source (no live PuppetDB), either pre-compiled catalogs or compile-from-repo:
44
+
45
+ ```
46
+ # pre-compiled catalog JSON, one file per node
47
+ nectar-conformance check run --site ardctest --source static \
48
+ --catalog-dir ./catalogs --facts-dir ./facts
49
+
50
+ # compile from the site repo (one node per facts file; compiler set via
51
+ # static.compile_command in config, defaults to octocatalog-diff)
52
+ nectar-conformance check run --site ardctest --source static \
53
+ --site-repo /path/to/site-repo --facts-dir ./facts
54
+ ```
55
+
56
+ ### Will a change fix conformance before it goes live?
57
+
58
+ A site is a puppet environment, and r10k deploys each branch of the control repo as its
59
+ own environment, so you can check a *proposed* change pre-merge and compare it to the
60
+ live site. Capture the live baseline, capture the proposed environment, and diff them:
61
+
62
+ ```
63
+ # baseline: the live site
64
+ nectar-conformance check run --site ardctest --conformance-version 2025.1 \
65
+ --format json > before.json
66
+
67
+ # the proposed change, deployed as a branch environment (--environment keeps the
68
+ # 'ardctest' label but queries that environment)
69
+ nectar-conformance check run --site ardctest --environment ardctest_fix_glance \
70
+ --conformance-version 2025.1 --format json > after.json
71
+
72
+ # what does the change fix, and does it break anything? (exit 1 if it regresses)
73
+ nectar-conformance report diff before.json after.json
74
+ ```
75
+
76
+ For a truly offline check (no deploy, no node runs), produce `after.json` with the
77
+ static source by compiling the proposed branch (`--source static --site-repo ...`); that
78
+ needs the environment's modules assembled, which is what octocatalog-diff / r10k do.
79
+
80
+ Exit codes: `0` conformant, `1` conformance failure at or above the severity
81
+ threshold, `2` usage error, `3` operational error (PuppetDB unreachable, etc.).
82
+ `report diff` exits `1` if the change introduces any new failure.
83
+
84
+ ## Development
85
+
86
+ ```
87
+ tox # run unit tests and lint
88
+ tox -e pep8 # lint only
89
+ ```
90
+
91
+ Releases use [reno](https://docs.openstack.org/reno/) for release notes. Contributions
92
+ go through gerrit; use conventional commits and `git commit -s`.
@@ -0,0 +1,3 @@
1
+ """Conformance checker for Nectar puppet-managed OpenStack cloud sites."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1 @@
1
+ """Operator-facing command-line interface (cliff)."""
@@ -0,0 +1 @@
1
+ """cliff command implementations."""
@@ -0,0 +1,46 @@
1
+ """``changelog lint`` - structurally validate a checks directory.
2
+
3
+ The checks data lives in its own repository (``nectar-conformance-checks``); this is the
4
+ CI gate that repository runs to catch a malformed changelog or definition before merge.
5
+ It loads the definitions and changelog, runs :func:`changelog_lint`, and reports any
6
+ structural violations (unknown check ids, ``effective`` after ``due``, test due later
7
+ than prod, colliding entries).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from cliff.command import Command
13
+
14
+ from nectar_conformance import config as config_mod
15
+ from nectar_conformance.errors import ConformanceError
16
+ from nectar_conformance.service import lint_versions
17
+
18
+
19
+ class ChangelogLint(Command):
20
+ """Validate the conformance changelog and definitions are structurally sound."""
21
+
22
+ def get_parser(self, prog_name):
23
+ parser = super().get_parser(prog_name)
24
+ parser.add_argument("--config", help="path to a config file")
25
+ parser.add_argument("--checks-dir", help="checks dir to lint")
26
+ return parser
27
+
28
+ def take_action(self, parsed_args):
29
+ overrides = (
30
+ {"checks_dir": parsed_args.checks_dir}
31
+ if parsed_args.checks_dir
32
+ else None
33
+ )
34
+ cfg = config_mod.load(parsed_args.config, overrides)
35
+ try:
36
+ violations = lint_versions(cfg)
37
+ except ConformanceError as exc:
38
+ self.app.stderr.write(f"error: {exc}\n")
39
+ return 3
40
+ if violations:
41
+ self.app.stderr.write("changelog lint found problems:\n")
42
+ for v in violations:
43
+ self.app.stderr.write(f" - {v}\n")
44
+ return 1
45
+ self.app.stdout.write("changelog lint: ok\n")
46
+ return 0
@@ -0,0 +1,112 @@
1
+ """``check list`` and ``check show`` - inspect checks without touching PuppetDB."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from cliff.command import Command
6
+
7
+ from nectar_conformance import config as config_mod
8
+ from nectar_conformance.errors import ConformanceError
9
+ from nectar_conformance.service import get_check, list_checks
10
+
11
+
12
+ def _resolve_version(parsed_args, cfg):
13
+ return parsed_args.conformance_version or cfg.default_conformance_version
14
+
15
+
16
+ class CheckList(Command):
17
+ """List the checks that apply at a conformance version."""
18
+
19
+ def get_parser(self, prog_name):
20
+ parser = super().get_parser(prog_name)
21
+ parser.add_argument(
22
+ "--conformance-version", help="conformance version, e.g. 2026.1"
23
+ )
24
+ parser.add_argument("--config", help="path to a config file")
25
+ parser.add_argument("--checks-dir", help="load checks from this dir")
26
+ parser.add_argument(
27
+ "--severity", choices=["info", "warning", "error"], help="filter"
28
+ )
29
+ return parser
30
+
31
+ def take_action(self, parsed_args):
32
+ overrides = (
33
+ {"checks_dir": parsed_args.checks_dir}
34
+ if parsed_args.checks_dir
35
+ else None
36
+ )
37
+ cfg = config_mod.load(parsed_args.config, overrides)
38
+ version = _resolve_version(parsed_args, cfg)
39
+ if not version:
40
+ self.app.stderr.write(
41
+ "error: no conformance version (use --conformance-version)\n"
42
+ )
43
+ return 2
44
+ try:
45
+ rules = list_checks(cfg, version)
46
+ except ConformanceError as exc:
47
+ self.app.stderr.write(f"error: {exc}\n")
48
+ return 3
49
+ out = self.app.stdout
50
+ out.write(f"Checks for conformance {version}:\n")
51
+ for rule in rules:
52
+ if parsed_args.severity and rule.severity != parsed_args.severity:
53
+ continue
54
+ expected = (
55
+ ""
56
+ if rule.expected is None
57
+ else f" expected={rule.expected!r}"
58
+ )
59
+ section = rule.spec_section or "-"
60
+ out.write(
61
+ f" [{rule.severity:7}] {rule.id:30} ({section}){expected}\n"
62
+ )
63
+ return 0
64
+
65
+
66
+ class CheckShow(Command):
67
+ """Show the full definition of one check."""
68
+
69
+ def get_parser(self, prog_name):
70
+ parser = super().get_parser(prog_name)
71
+ parser.add_argument(
72
+ "check_id", help="check id, e.g. glance.api.image_tag"
73
+ )
74
+ parser.add_argument("--config", help="path to a config file")
75
+ parser.add_argument("--checks-dir", help="load checks from this dir")
76
+ return parser
77
+
78
+ def take_action(self, parsed_args):
79
+ overrides = (
80
+ {"checks_dir": parsed_args.checks_dir}
81
+ if parsed_args.checks_dir
82
+ else None
83
+ )
84
+ cfg = config_mod.load(parsed_args.config, overrides)
85
+ check = get_check(cfg, parsed_args.check_id)
86
+ if check is None:
87
+ self.app.stderr.write(
88
+ f"error: no such check '{parsed_args.check_id}'\n"
89
+ )
90
+ return 3
91
+ out = self.app.stdout
92
+ out.write(f"{check.id}\n")
93
+ out.write(f" title: {check.title}\n")
94
+ out.write(f" severity: {check.severity}\n")
95
+ out.write(f" spec_section: {check.spec_section}\n")
96
+ out.write(f" kind: {check.kind}\n")
97
+ out.write(
98
+ f" selector: {check.selector.type} {check.selector.params}\n"
99
+ )
100
+ if check.query is not None:
101
+ out.write(
102
+ f" query: {check.query.type} {check.query.params}\n"
103
+ )
104
+ if check.assertion_op is not None:
105
+ out.write(f" assertion: {check.assertion_op}\n")
106
+ if check.remediation is not None:
107
+ out.write(f" remediation: {check.remediation.guidance}\n")
108
+ if check.remediation.hiera_key:
109
+ out.write(
110
+ f" hiera_key={check.remediation.hiera_key}\n"
111
+ )
112
+ return 0
@@ -0,0 +1,136 @@
1
+ """``check run`` - evaluate a site against a conformance version."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from cliff.command import Command
6
+
7
+ from nectar_conformance import config as config_mod
8
+ from nectar_conformance.errors import ConformanceError
9
+ from nectar_conformance.report import human, json_report
10
+ from nectar_conformance.results.model import Report, Severity
11
+ from nectar_conformance.service import run_check
12
+
13
+ _RANK = {Severity.INFO: 1, Severity.WARNING: 2, Severity.ERROR: 3}
14
+ _THRESHOLD = {"info": 1, "warning": 2, "error": 3}
15
+
16
+ # Process exit codes.
17
+ EXIT_OK = 0
18
+ EXIT_NONCONFORMANT = 1
19
+ EXIT_OPERATIONAL = 3
20
+
21
+
22
+ def exit_code(report: Report, threshold: str) -> int:
23
+ worst = report.worst_failing_severity()
24
+ if worst is None:
25
+ return EXIT_OK
26
+ return (
27
+ EXIT_NONCONFORMANT
28
+ if _RANK[worst] >= _THRESHOLD[threshold]
29
+ else EXIT_OK
30
+ )
31
+
32
+
33
+ class CheckRun(Command):
34
+ """Run conformance checks for a site and report the results."""
35
+
36
+ def get_parser(self, prog_name):
37
+ parser = super().get_parser(prog_name)
38
+ parser.add_argument(
39
+ "--site", required=True, help="site (puppet environment) to check"
40
+ )
41
+ parser.add_argument(
42
+ "--environment",
43
+ help="puppet environment to query instead of the site name; use to check a "
44
+ "proposed branch environment before it goes live (report is still labelled --site)",
45
+ )
46
+ parser.add_argument(
47
+ "--conformance-version",
48
+ help="conformance version tag to pin the evaluation date to (e.g. 2026.1); "
49
+ "omit for a live run against today",
50
+ )
51
+ parser.add_argument(
52
+ "--as-of",
53
+ help="evaluate as if today were this date (YYYY-MM-DD), for scheduled and "
54
+ "what-if runs; overrides the version tag's pinned date",
55
+ )
56
+ parser.add_argument(
57
+ "--site-tier",
58
+ choices=["test", "prod"],
59
+ help="treat the site as this tier for dated changes (overrides config; "
60
+ "default prod)",
61
+ )
62
+ parser.add_argument(
63
+ "--source", choices=["puppetdb", "static"], help="data source"
64
+ )
65
+ parser.add_argument(
66
+ "--format", choices=["human", "json"], default="human"
67
+ )
68
+ parser.add_argument(
69
+ "--severity-threshold",
70
+ choices=["info", "warning", "error"],
71
+ default="error",
72
+ help="lowest severity that makes the run exit non-zero (default: error)",
73
+ )
74
+ parser.add_argument("--config", help="path to a config file")
75
+ parser.add_argument(
76
+ "--puppetdb-url", help="override the PuppetDB base URL"
77
+ )
78
+ parser.add_argument(
79
+ "--checks-dir",
80
+ help="load check definitions/manifests from this dir",
81
+ )
82
+ parser.add_argument(
83
+ "--site-repo", help="path to the site puppet repo (static source)"
84
+ )
85
+ parser.add_argument(
86
+ "--catalog-dir",
87
+ help="dir of compiled catalog JSON (static source)",
88
+ )
89
+ parser.add_argument(
90
+ "--facts-dir", help="dir of per-node facts JSON (static source)"
91
+ )
92
+ return parser
93
+
94
+ def take_action(self, parsed_args):
95
+ overrides: dict = {}
96
+ if parsed_args.puppetdb_url:
97
+ overrides.setdefault("puppetdb", {})["base_url"] = (
98
+ parsed_args.puppetdb_url
99
+ )
100
+ if parsed_args.source:
101
+ overrides["source"] = parsed_args.source
102
+ if parsed_args.checks_dir:
103
+ overrides["checks_dir"] = parsed_args.checks_dir
104
+
105
+ try:
106
+ cfg = config_mod.load(parsed_args.config, overrides)
107
+ if parsed_args.environment:
108
+ # Query this environment but keep the site label/identity.
109
+ cfg.site_environment[parsed_args.site] = (
110
+ parsed_args.environment
111
+ )
112
+ if parsed_args.site_tier:
113
+ # Override the site's tier for this run (mirrors --environment).
114
+ cfg.site_tier[parsed_args.site] = parsed_args.site_tier
115
+ source_kwargs = {
116
+ "site_repo": parsed_args.site_repo,
117
+ "catalog_dir": parsed_args.catalog_dir,
118
+ "facts_dir": parsed_args.facts_dir,
119
+ }
120
+ report = run_check(
121
+ cfg,
122
+ site=parsed_args.site,
123
+ version=parsed_args.conformance_version,
124
+ source=parsed_args.source,
125
+ source_kwargs=source_kwargs,
126
+ as_of=parsed_args.as_of,
127
+ )
128
+ except ConformanceError as exc:
129
+ self.app.stderr.write(f"error: {exc}\n")
130
+ return EXIT_OPERATIONAL
131
+
132
+ if parsed_args.format == "json":
133
+ json_report.render(report, self.app.stdout)
134
+ else:
135
+ human.render(report, self.app.stdout)
136
+ return exit_code(report, parsed_args.severity_threshold)
@@ -0,0 +1,50 @@
1
+ """``version diff`` - show how the check set differs between two versions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from cliff.command import Command
6
+
7
+ from nectar_conformance import config as config_mod
8
+ from nectar_conformance.errors import ConformanceError
9
+ from nectar_conformance.service import diff_versions
10
+
11
+
12
+ class DiffVersions(Command):
13
+ """Show checks added and removed between two conformance versions."""
14
+
15
+ def get_parser(self, prog_name):
16
+ parser = super().get_parser(prog_name)
17
+ parser.add_argument("version_a", help="the 'from' conformance version")
18
+ parser.add_argument("version_b", help="the 'to' conformance version")
19
+ parser.add_argument("--config", help="path to a config file")
20
+ parser.add_argument("--checks-dir", help="load checks from this dir")
21
+ return parser
22
+
23
+ def take_action(self, parsed_args):
24
+ overrides = (
25
+ {"checks_dir": parsed_args.checks_dir}
26
+ if parsed_args.checks_dir
27
+ else None
28
+ )
29
+ cfg = config_mod.load(parsed_args.config, overrides)
30
+ try:
31
+ diff = diff_versions(
32
+ cfg, parsed_args.version_a, parsed_args.version_b
33
+ )
34
+ except ConformanceError as exc:
35
+ self.app.stderr.write(f"error: {exc}\n")
36
+ return 3
37
+ out = self.app.stdout
38
+ out.write(f"{diff['from']} -> {diff['to']}\n")
39
+ out.write(f" changed ({len(diff['changed'])}):\n")
40
+ for change in diff["changed"]:
41
+ out.write(
42
+ f" ~ {change['check_id']}: {change['from']} -> {change['to']}\n"
43
+ )
44
+ out.write(f" added ({len(diff['added'])}):\n")
45
+ for cid in diff["added"]:
46
+ out.write(f" + {cid}\n")
47
+ out.write(f" removed ({len(diff['removed'])}):\n")
48
+ for cid in diff["removed"]:
49
+ out.write(f" - {cid}\n")
50
+ return 0
@@ -0,0 +1,52 @@
1
+ """``report diff`` - compare two conformance report JSON files (before vs after)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+
7
+ from cliff.command import Command
8
+
9
+ from nectar_conformance.results.compare import compare_reports
10
+
11
+
12
+ class ReportDiff(Command):
13
+ """Diff two 'check run --format json' reports to see what a change fixes or breaks."""
14
+
15
+ def get_parser(self, prog_name):
16
+ parser = super().get_parser(prog_name)
17
+ parser.add_argument("old", help="baseline report JSON (current/live)")
18
+ parser.add_argument("new", help="proposed report JSON (the change)")
19
+ return parser
20
+
21
+ def take_action(self, parsed_args):
22
+ try:
23
+ with open(parsed_args.old) as fh:
24
+ old = json.load(fh)
25
+ with open(parsed_args.new) as fh:
26
+ new = json.load(fh)
27
+ except (OSError, json.JSONDecodeError) as exc:
28
+ self.app.stderr.write(f"error: could not read report: {exc}\n")
29
+ return 3
30
+
31
+ diff = compare_reports(old, new)
32
+ out = self.app.stdout
33
+
34
+ def section(label, rows):
35
+ out.write(f"{label} ({len(rows)}):\n")
36
+ for r in rows:
37
+ arrow = f"{r['old']} -> {r['new']}"
38
+ out.write(f" {r['rule_id']} [{r['severity']}] {arrow}\n")
39
+
40
+ section("Fixed", diff["fixed"])
41
+ section("Regressed", diff["regressed"])
42
+ section("Still failing", diff["still_failing"])
43
+ if diff["added"] or diff["removed"]:
44
+ section("Added checks", diff["added"])
45
+ section("Removed checks", diff["removed"])
46
+
47
+ out.write(
48
+ f"\nSummary: {len(diff['fixed'])} fixed, {len(diff['regressed'])} regressed, "
49
+ f"{len(diff['still_failing'])} still failing\n"
50
+ )
51
+ # Non-zero exit if the change introduces any new failure, so CI can gate on it.
52
+ return 1 if diff["regressed"] else 0