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.
- nectar_conformance/__init__.py +3 -0
- nectar_conformance/cli/__init__.py +1 -0
- nectar_conformance/cli/commands/__init__.py +1 -0
- nectar_conformance/cli/commands/changelog_cmd.py +46 -0
- nectar_conformance/cli/commands/check_meta.py +112 -0
- nectar_conformance/cli/commands/check_run.py +136 -0
- nectar_conformance/cli/commands/diff.py +50 -0
- nectar_conformance/cli/commands/report_diff.py +52 -0
- nectar_conformance/cli/commands/version_cmd.py +110 -0
- nectar_conformance/cli/main.py +29 -0
- nectar_conformance/config.py +235 -0
- nectar_conformance/datasources/__init__.py +1 -0
- nectar_conformance/datasources/base.py +48 -0
- nectar_conformance/datasources/compile.py +110 -0
- nectar_conformance/datasources/puppetdb.py +178 -0
- nectar_conformance/datasources/static_repo.py +192 -0
- nectar_conformance/engine/__init__.py +1 -0
- nectar_conformance/engine/operators.py +103 -0
- nectar_conformance/engine/queries.py +72 -0
- nectar_conformance/engine/runner.py +331 -0
- nectar_conformance/engine/selectors.py +32 -0
- nectar_conformance/errors.py +38 -0
- nectar_conformance/model.py +113 -0
- nectar_conformance/plugins/__init__.py +1 -0
- nectar_conformance/plugins/base.py +31 -0
- nectar_conformance/plugins/registry.py +24 -0
- nectar_conformance/report/__init__.py +1 -0
- nectar_conformance/report/human.py +101 -0
- nectar_conformance/report/json_report.py +13 -0
- nectar_conformance/results/__init__.py +1 -0
- nectar_conformance/results/compare.py +66 -0
- nectar_conformance/results/model.py +196 -0
- nectar_conformance/results/serialise.py +72 -0
- nectar_conformance/rollout.py +109 -0
- nectar_conformance/rules/__init__.py +1 -0
- nectar_conformance/rules/changelog.py +311 -0
- nectar_conformance/rules/loader.py +84 -0
- nectar_conformance/rules/model.py +224 -0
- nectar_conformance/rules/schema.json +113 -0
- nectar_conformance/rules/schema.py +44 -0
- nectar_conformance/service.py +434 -0
- nectar_conformance/web/__init__.py +8 -0
- nectar_conformance/web/api.py +211 -0
- nectar_conformance/web/app.py +111 -0
- nectar_conformance/web/refresh.py +231 -0
- nectar_conformance/web/serialise.py +63 -0
- nectar_conformance/web/settings.py +63 -0
- nectar_conformance/web/store.py +71 -0
- nectar_conformance-0.1.0.dist-info/METADATA +117 -0
- nectar_conformance-0.1.0.dist-info/RECORD +53 -0
- nectar_conformance-0.1.0.dist-info/WHEEL +5 -0
- nectar_conformance-0.1.0.dist-info/entry_points.txt +14 -0
- nectar_conformance-0.1.0.dist-info/top_level.txt +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())
|