nectar-conformance 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. nectar_conformance/__init__.py +3 -0
  2. nectar_conformance/cli/__init__.py +1 -0
  3. nectar_conformance/cli/commands/__init__.py +1 -0
  4. nectar_conformance/cli/commands/changelog_cmd.py +46 -0
  5. nectar_conformance/cli/commands/check_meta.py +112 -0
  6. nectar_conformance/cli/commands/check_run.py +136 -0
  7. nectar_conformance/cli/commands/diff.py +50 -0
  8. nectar_conformance/cli/commands/report_diff.py +52 -0
  9. nectar_conformance/cli/commands/version_cmd.py +110 -0
  10. nectar_conformance/cli/main.py +29 -0
  11. nectar_conformance/config.py +235 -0
  12. nectar_conformance/datasources/__init__.py +1 -0
  13. nectar_conformance/datasources/base.py +48 -0
  14. nectar_conformance/datasources/compile.py +110 -0
  15. nectar_conformance/datasources/puppetdb.py +178 -0
  16. nectar_conformance/datasources/static_repo.py +192 -0
  17. nectar_conformance/engine/__init__.py +1 -0
  18. nectar_conformance/engine/operators.py +103 -0
  19. nectar_conformance/engine/queries.py +72 -0
  20. nectar_conformance/engine/runner.py +331 -0
  21. nectar_conformance/engine/selectors.py +32 -0
  22. nectar_conformance/errors.py +38 -0
  23. nectar_conformance/model.py +113 -0
  24. nectar_conformance/plugins/__init__.py +1 -0
  25. nectar_conformance/plugins/base.py +31 -0
  26. nectar_conformance/plugins/registry.py +24 -0
  27. nectar_conformance/report/__init__.py +1 -0
  28. nectar_conformance/report/human.py +101 -0
  29. nectar_conformance/report/json_report.py +13 -0
  30. nectar_conformance/results/__init__.py +1 -0
  31. nectar_conformance/results/compare.py +66 -0
  32. nectar_conformance/results/model.py +196 -0
  33. nectar_conformance/results/serialise.py +72 -0
  34. nectar_conformance/rollout.py +109 -0
  35. nectar_conformance/rules/__init__.py +1 -0
  36. nectar_conformance/rules/changelog.py +311 -0
  37. nectar_conformance/rules/loader.py +84 -0
  38. nectar_conformance/rules/model.py +224 -0
  39. nectar_conformance/rules/schema.json +113 -0
  40. nectar_conformance/rules/schema.py +44 -0
  41. nectar_conformance/service.py +434 -0
  42. nectar_conformance/web/__init__.py +8 -0
  43. nectar_conformance/web/api.py +211 -0
  44. nectar_conformance/web/app.py +111 -0
  45. nectar_conformance/web/refresh.py +231 -0
  46. nectar_conformance/web/serialise.py +63 -0
  47. nectar_conformance/web/settings.py +63 -0
  48. nectar_conformance/web/store.py +71 -0
  49. nectar_conformance-0.1.0.dist-info/METADATA +117 -0
  50. nectar_conformance-0.1.0.dist-info/RECORD +53 -0
  51. nectar_conformance-0.1.0.dist-info/WHEEL +5 -0
  52. nectar_conformance-0.1.0.dist-info/entry_points.txt +14 -0
  53. nectar_conformance-0.1.0.dist-info/top_level.txt +1 -0
@@ -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
@@ -0,0 +1,110 @@
1
+ """``version`` subcommands: list/lint versions and squash the changelog to a baseline."""
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 (
10
+ available_versions,
11
+ lint_versions,
12
+ squash_changelog,
13
+ )
14
+
15
+
16
+ class VersionList(Command):
17
+ """List available conformance versions."""
18
+
19
+ def get_parser(self, prog_name):
20
+ parser = super().get_parser(prog_name)
21
+ parser.add_argument("--config", help="path to a config file")
22
+ parser.add_argument("--checks-dir", help="load checks from this dir")
23
+ return parser
24
+
25
+ def take_action(self, parsed_args):
26
+ overrides = (
27
+ {"checks_dir": parsed_args.checks_dir}
28
+ if parsed_args.checks_dir
29
+ else None
30
+ )
31
+ cfg = config_mod.load(parsed_args.config, overrides)
32
+ try:
33
+ versions = available_versions(cfg)
34
+ violations = lint_versions(cfg)
35
+ except ConformanceError as exc:
36
+ self.app.stderr.write(f"error: {exc}\n")
37
+ return 3
38
+ out = self.app.stdout
39
+ out.write("Available conformance versions:\n")
40
+ default = cfg.default_conformance_version
41
+ for version in versions:
42
+ marker = " (default)" if version == default else ""
43
+ out.write(f" {version}{marker}\n")
44
+ if violations:
45
+ out.write("\nChangelog problems:\n")
46
+ for v in violations:
47
+ out.write(f" ! {v}\n")
48
+ return 1
49
+ return 0
50
+
51
+
52
+ class VersionSquash(Command):
53
+ """Squash the changelog to a fresh baseline named as a new conformance version.
54
+
55
+ Folds the changelog at the squash date into baseline directives carrying the latest
56
+ enforced values (per tier), carries scheduled rollouts forward, and archives the full
57
+ pre-squash log so no history is lost. Point --checks-dir at a nectar-conformance-checks
58
+ checkout (the squash rewrites files in place).
59
+ """
60
+
61
+ def get_parser(self, prog_name):
62
+ parser = super().get_parser(prog_name)
63
+ parser.add_argument(
64
+ "--name",
65
+ required=True,
66
+ help="name for the new conformance version (e.g. 2027.0)",
67
+ )
68
+ parser.add_argument(
69
+ "--as-of",
70
+ help="squash date (YYYY-MM-DD), on or before today; the new version is pinned "
71
+ "to it (default: today, i.e. when the squash happens)",
72
+ )
73
+ parser.add_argument("--config", help="path to a config file")
74
+ parser.add_argument(
75
+ "--checks-dir",
76
+ help="nectar-conformance-checks checkout to read and rewrite",
77
+ )
78
+ return parser
79
+
80
+ def take_action(self, parsed_args):
81
+ overrides = (
82
+ {"checks_dir": parsed_args.checks_dir}
83
+ if parsed_args.checks_dir
84
+ else None
85
+ )
86
+ cfg = config_mod.load(parsed_args.config, overrides)
87
+ try:
88
+ result = squash_changelog(
89
+ cfg, name=parsed_args.name, as_of=parsed_args.as_of
90
+ )
91
+ except ConformanceError as exc:
92
+ self.app.stderr.write(f"error: {exc}\n")
93
+ return 3
94
+ out = self.app.stdout
95
+ out.write(
96
+ f"Squashed to baseline {result.name} (as of {result.as_of}).\n"
97
+ )
98
+ out.write(
99
+ f" entries: {result.entries_before} -> {result.entries_after} "
100
+ f"({result.baselines} baseline, {result.carried} carried forward)\n"
101
+ )
102
+ out.write(
103
+ f" archived {result.archived} entries to {result.archive_path}\n"
104
+ )
105
+ out.write(f" wrote {result.changelog_path}\n")
106
+ out.write(
107
+ "\nNext: review the diff and add a release note. A no-flag `check run` still "
108
+ f"evaluates live; pass --conformance-version {result.name} to pin a report.\n"
109
+ )
110
+ return 0
@@ -0,0 +1,29 @@
1
+ """cliff application entry point."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ from cliff.app import App
8
+ from cliff.commandmanager import CommandManager
9
+
10
+ from nectar_conformance import __version__
11
+
12
+
13
+ class ConformanceApp(App):
14
+ def __init__(self):
15
+ super().__init__(
16
+ description="Conformance checker for Nectar puppet-managed cloud sites",
17
+ version=__version__,
18
+ command_manager=CommandManager("nectar_conformance.cli"),
19
+ deferred_help=True,
20
+ )
21
+
22
+
23
+ def main(argv=None):
24
+ argv = sys.argv[1:] if argv is None else argv
25
+ return ConformanceApp().run(argv)
26
+
27
+
28
+ if __name__ == "__main__":
29
+ sys.exit(main())