Skip to content

Commit f994ffe

Browse files
committed
Make version-policy-check more robust
particularly, report ABORT result (at all) Signed-off-by: Matthias Büchse <matthias.buechse@alasca.cloud>
1 parent a4c4af7 commit f994ffe

1 file changed

Lines changed: 91 additions & 134 deletions

File tree

Tests/kaas/scs_0210_version_policy/k8s_version_policy.py

Lines changed: 91 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -27,20 +27,19 @@
2727
SPDX-License-Identifier: CC-BY-SA-4.0
2828
"""
2929

30-
from collections import Counter
3130
from dataclasses import dataclass
3231
from datetime import datetime, timedelta
3332
from pathlib import Path
34-
import aiohttp
35-
import asyncio
3633
import contextlib
3734
import getopt
38-
import kubernetes_asyncio
3935
import logging
4036
import logging.config
4137
import re
42-
import requests
4338
import sys
39+
40+
import aiohttp
41+
import asyncio
42+
import kubernetes_asyncio
4443
import yaml
4544

4645

@@ -51,41 +50,11 @@
5150
CVE_SEVERITY = 8 # CRITICAL
5251

5352
HERE = Path(__file__).parent
54-
EOLDATA_FILE = "k8s-eol-data.yml"
55-
56-
logging_config = {
57-
"level": "INFO",
58-
"version": 1,
59-
"disable_existing_loggers": False,
60-
"formatters": {
61-
"k8s_version_policy": {
62-
"format": "%(levelname)s: %(message)s"
63-
}
64-
},
65-
"handlers": {
66-
"console": {
67-
"class": "logging.StreamHandler",
68-
"formatter": "k8s_version_policy",
69-
"stream": "ext://sys.stderr"
70-
}
71-
},
72-
"root": {
73-
"handlers": ["console"]
74-
}
75-
}
53+
EOLDATA_PATH = Path(HERE, "k8s-eol-data.yml")
7654

7755
logger = logging.getLogger(__name__)
7856

7957

80-
class CountingHandler(logging.Handler):
81-
def __init__(self, level=logging.NOTSET):
82-
super().__init__(level=level)
83-
self.bylevel = Counter()
84-
85-
def handle(self, record):
86-
self.bylevel[record.levelno] += 1
87-
88-
8958
class ConfigException(BaseException):
9059
"""Exception raised in a configuration error occurs"""
9160

@@ -94,63 +63,48 @@ class HelpException(BaseException):
9463
"""Exception raised if the help functionality is called"""
9564

9665

97-
class Config:
98-
kubeconfig = None
99-
context = None
100-
logging = logging_config
101-
102-
10366
def print_usage():
10467
print("""
10568
K8s Version Policy Compliance Check
10669
107-
Usage: k8s_version_policy.py [-h] -k|--kubeconfig PATH/TO/KUBECONFIG [--context CONTEXT]
70+
Usage: k8s_version_policy.py [-h] -k|--kubeconfig PATH/TO/KUBECONFIG
10871
10972
This tool checks whether the given cluster conforms to the SCS k8s version policy. It checks one
11073
cluster only, so it doesn't check whether multiple k8s branches are offered. The return code
11174
will be 0 precisely when all attempted checks are passed; otherwise check log messages.
11275
11376
-k/--kubeconfig PATH/TO/KUBECONFIG - Path to the kubeconfig of the server we want to check
114-
-C/--context CONTEXT - Optional: kubeconfig context to use
11577
-h - Output help
11678
""")
11779

11880

119-
def parse_arguments(argv):
120-
"""Parse cli arguments from the script call"""
121-
try:
122-
opts, args = getopt.gnu_getopt(argv, "C:k:h", ["context=", "kubeconfig=", "help"])
123-
except getopt.GetoptError:
124-
raise ConfigException
81+
class Config:
82+
def __init__(self, log_level=logging.INFO):
83+
self.kubeconfig = None
84+
self.log_level = log_level
12585

126-
config = Config()
127-
for opt in opts:
128-
if opt[0] == "-h" or opt[0] == "--help":
129-
raise HelpException
130-
if opt[0] == "-k" or opt[0] == "--kubeconfig":
131-
config.kubeconfig = opt[1]
132-
if opt[0] == "-C" or opt[0] == "--context":
133-
config.context = opt[1]
134-
return config
135-
136-
137-
def setup_logging(config_log):
138-
logging.config.dictConfig(config_log)
139-
loggers = [
140-
logging.getLogger(name)
141-
for name in logging.root.manager.loggerDict
142-
if not logging.getLogger(name).level
143-
]
144-
for log in loggers:
145-
log.setLevel(config_log['level'])
146-
147-
148-
def initialize_config(config):
149-
"""Initialize the configuration for the test script"""
150-
setup_logging(config.logging)
151-
if config.kubeconfig is None:
152-
raise ConfigException("A kubeconfig needs to be set in order to test a k8s cluster version.")
153-
return config
86+
def apply_argv(self, argv):
87+
"""Parse cli arguments from the script call"""
88+
try:
89+
opts, args = getopt.gnu_getopt(argv, "k:h", ["kubeconfig=", "help"])
90+
except getopt.GetoptError:
91+
raise ConfigException
92+
93+
for opt in opts:
94+
if opt[0] == "-h" or opt[0] == "--help":
95+
raise HelpException
96+
if opt[0] == "-k" or opt[0] == "--kubeconfig":
97+
self.kubeconfig = opt[1]
98+
99+
def setup(self):
100+
"""Initialize the configuration for the test script"""
101+
logging.basicConfig(format='%(levelname)s: %(message)s', level=self.log_level)
102+
for name in logging.root.manager.loggerDict:
103+
logger = logging.getLogger(name)
104+
if not logger.level:
105+
logger.setLevel(self.log_level)
106+
if self.kubeconfig is None:
107+
raise ConfigException("A kubeconfig needs to be set in order to test a k8s cluster version.")
154108

155109

156110
@dataclass(frozen=True, eq=True, order=True)
@@ -222,17 +176,18 @@ def age(self):
222176
return datetime.now() - self.released_at
223177

224178

225-
def fetch_k8s_releases_data() -> list[dict]:
179+
async def fetch_k8s_releases_data(session: aiohttp.ClientSession) -> list[dict]:
226180
github_headers = {
227181
"Accept": "application/vnd.github+json",
228182
"X-GitHub-Api-Version": "2022-11-28"
229183
}
230184

231185
# Request the latest 100 releases (the next are not needed, since these versions are too old)
232-
return requests.get(
186+
response = await session.get(
233187
"https://api.github.com/repos/kubernetes/kubernetes/releases?per_page=100",
234188
headers=github_headers,
235-
).json()
189+
)
190+
return await response.json()
236191

237192

238193
def parse_github_release_data(release_data: dict) -> K8sRelease:
@@ -451,76 +406,78 @@ def read_supported_k8s_branches(eol_data_path: Path) -> dict[K8sBranch, K8sBranc
451406
return {info.branch: info for info in infos}
452407

453408

454-
async def main(argv):
455-
try:
456-
config = initialize_config(parse_arguments(argv))
457-
except (OSError, ConfigException, HelpException) as e:
458-
logger.critical("%s", e)
459-
print_usage()
460-
return 1
461-
462-
counting_handler = CountingHandler(level=logging.INFO)
463-
logger.addHandler(counting_handler)
464-
465-
branch_infos = read_supported_k8s_branches(Path(HERE, EOLDATA_FILE))
409+
def determine_supported_branches(eoldata_path=EOLDATA_PATH):
410+
branch_infos = read_supported_k8s_branches(eoldata_path)
466411
supported_branches = {
467412
branch
468413
for branch, branch_info
469414
in branch_infos.items()
470415
if branch_info.is_supported()
471416
}
472417
if len(supported_branches) < 3:
473-
logger.warning("The EOL data in %s isn't up-to-date.", EOLDATA_FILE)
418+
logger.warning("The EOL data in %s isn't up-to-date.", eoldata_path.name)
474419
if len(supported_branches) < 2:
475-
logger.critical("The EOL data in %s is outdated and we cannot reliably run this script.", EOLDATA_FILE)
476-
return 1
477-
478-
connector = aiohttp.TCPConnector(limit=5)
479-
async with aiohttp.ClientSession(connector=connector) as session:
480-
cve_affected_ranges = await collect_cve_versions(session)
481-
releases_data = fetch_k8s_releases_data()
482-
483-
try:
484-
context_desc = f"context '{config.context}'" if config.context else "default context"
485-
logger.info("Checking cluster specified by %s in %s.", context_desc, config.kubeconfig)
486-
cluster = await get_k8s_cluster_info(config.kubeconfig, config.context)
487-
cluster_branch = cluster.version.branch
488-
489-
if cluster_branch not in supported_branches:
490-
logger.error("The K8s cluster version %s of cluster '%s' is already EOL.", cluster.version, cluster.name)
491-
elif check_k8s_version_recency(cluster.version, releases_data, cve_affected_ranges):
492-
logger.info(
493-
"The K8s cluster version %s of cluster '%s' is still in the recency time window.",
494-
cluster.version,
495-
cluster.name,
496-
)
497-
else:
420+
raise RuntimeError(f"The EOL data in {eoldata_path.name} is critically outdated!")
421+
return supported_branches
422+
423+
424+
def compute_version_policy_check(supported_branches, cve_affected_ranges, releases_data, cluster):
425+
if cluster.version.branch not in supported_branches:
426+
logger.error("The K8s cluster version %s of cluster '%s' is already EOL.", cluster.version, cluster.name)
427+
return False
428+
if check_k8s_version_recency(cluster.version, releases_data, cve_affected_ranges):
429+
logger.info(
430+
"The K8s cluster version %s of cluster '%s' is still in the recency time window.",
431+
cluster.version,
432+
cluster.name,
433+
)
434+
return True
435+
for affected_range in cve_affected_ranges:
436+
if cluster.version in affected_range:
498437
logger.error(
499-
"The K8s cluster version %s of cluster '%s' is outdated according to the standard.",
438+
"The K8s cluster version %s of cluster '%s' is an outdated version with a possible CRITICAL CVE.",
500439
cluster.version,
501440
cluster.name,
502441
)
442+
logger.error(
443+
"The K8s cluster version %s of cluster '%s' is outdated according to the standard.",
444+
cluster.version,
445+
cluster.name,
446+
)
447+
return False
448+
503449

504-
for affected_range in cve_affected_ranges:
505-
if cluster.version in affected_range:
506-
logger.error(
507-
"The K8s cluster version %s of cluster '%s' is an outdated version with a possible CRITICAL CVE.",
508-
cluster.version,
509-
cluster.name,
510-
)
450+
async def main(argv):
451+
config = Config()
452+
try:
453+
config.apply_argv(argv)
454+
config.setup()
455+
except HelpException:
456+
print_usage()
457+
return 0
511458
except BaseException as e:
512459
logger.critical("%s", e)
513-
logger.debug("Exception info", exc_info=True)
514460
return 1
515461

516-
c = counting_handler.bylevel
517-
logger.debug(
518-
"Total error / warning: "
519-
f"{c[logging.ERROR]} / {c[logging.WARNING]}"
520-
)
521-
if not c[logging.CRITICAL]:
522-
print("version-policy-check: " + ('PASS', 'FAIL')[min(1, c[logging.ERROR])])
523-
return min(127, c[logging.ERROR]) # cap at 127 due to OS restrictions
462+
try:
463+
supported_branches = determine_supported_branches()
464+
connector = aiohttp.TCPConnector(limit=5)
465+
logger.info("Checking cluster specified in %s.", config.kubeconfig)
466+
async with aiohttp.ClientSession(connector=connector) as session:
467+
cve_affected_ranges, releases_data, cluster = await asyncio.gather(
468+
collect_cve_versions(session),
469+
fetch_k8s_releases_data(session),
470+
get_k8s_cluster_info(config.kubeconfig),
471+
)
472+
result = compute_version_policy_check(
473+
supported_branches, cve_affected_ranges, releases_data, cluster)
474+
print("version-policy-check: " + ('FAIL', 'PASS')[bool(result)])
475+
except BaseException:
476+
print("version-policy-check: ABORT")
477+
logger.critical("Could not complete version-policy-check", exc_info=True)
478+
return 1
479+
480+
return 0
524481

525482

526483
if __name__ == "__main__":

0 commit comments

Comments
 (0)