3 # COPYRIGHT NOTICE STARTS HERE
5 # Copyright 2020 Samsung Electronics Co., Ltd.
6 # Copyright 2023 Deutsche Telekom AG
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
12 # http://www.apache.org/licenses/LICENSE-2.0
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
20 # COPYRIGHT NOTICE ENDS HERE
23 k8s_bin_versions_inspector is a module for verifying versions of CPython and
24 OpenJDK binaries installed in the kubernetes cluster containers.
27 __title__ = "k8s_bin_versions_inspector"
29 "Module for verifying versions of CPython and OpenJDK binaries installed"
30 " in the kubernetes cluster containers."
33 __author__ = "kkkk.k@samsung.com"
34 __license__ = "Apache-2.0"
35 __copyright__ = "Copyright 2020 Samsung Electronics Co., Ltd."
37 from typing import Iterable, List, Optional, Pattern, Union
55 def parse_argv(argv: Optional[List[str]] = None) -> argparse.Namespace:
56 """Function for parsing command line arguments.
59 argv: Unparsed list of command line arguments.
62 Namespace with values from parsed arguments.
66 f"Author: {__author__}\n"
67 f"License: {__license__}\n"
68 f"Copyright: {__copyright__}\n"
71 parser = argparse.ArgumentParser(
72 formatter_class=argparse.RawTextHelpFormatter,
74 description=__summary__,
79 parser.add_argument("-c", "--config-file", help="Name of the kube-config file.")
85 help="Kubernetes field selector, to filter out containers objects.",
92 help="Path to file, where output will be saved.",
98 choices=("tabulate", "pprint", "json"),
100 help="Format of the output file (tabulate, pprint, json).",
107 help="Ignore containers without any versions.",
114 help="Path to YAML file, with list of acceptable software versions.",
120 help="Namespace to use to list pods."
121 "If empty pods are going to be listed from all namespaces"
125 "--check-istio-sidecar",
127 help="Add if you want to check istio sidecars also"
131 "--istio-sidecar-name",
132 default="istio-proxy",
133 help="Name of istio sidecar to filter out"
140 help="Enable debugging mode in the k8s API.",
147 help="Suppress printing text on standard output.",
154 version=f"{__title__} {__version__}",
155 help="Display version information and exit.",
159 "-h", "--help", action="help", help="Display this help text and exit."
162 args = parser.parse_args(argv)
167 @dataclasses.dataclass
168 class ContainerExtra:
169 "Data class, to storage extra informations about container."
176 @dataclasses.dataclass
177 class ContainerVersions:
178 "Data class, to storage software versions from container."
184 @dataclasses.dataclass
186 "Data class, to storage multiple informations about container."
191 extra: ContainerExtra
192 versions: ContainerVersions = None
195 def is_container_running(
196 status: kubernetes.client.models.v1_container_status.V1ContainerStatus,
198 """Function to determine if k8s cluster container is in running state.
201 status: Single item from container_statuses list, that represents container status.
204 If container is in running state.
207 if status.state.terminated:
210 if status.state.waiting:
213 if not status.state.running:
219 def list_all_containers(
220 api: kubernetes.client.api.core_v1_api.CoreV1Api,
222 namespace: Union[None, str],
223 check_istio_sidecars: bool,
224 istio_sidecar_name: str
225 ) -> Iterable[ContainerInfo]:
226 """Get list of all containers names.
229 api: Client of the k8s cluster API.
230 field_selector: Kubernetes field selector, to filter out containers objects.
231 namespace: Namespace to limit reading pods from
232 check_istio_sidecars: Flag to enable/disable istio sidecars check.
234 istio_sidecar_name: If checking istio sidecars is disabled the name to filter
238 Objects for all containers in k8s cluster.
242 pods = api.list_namespaced_pod(namespace, field_selector=field_selector).items
244 pods = api.list_pod_for_all_namespaces(field_selector=field_selector).items
246 containers_statuses = (
247 (pod.metadata.namespace, pod.metadata.name, pod.status.container_statuses)
249 if pod.status.container_statuses
252 containers_status = (
253 itertools.product([namespace], [pod], statuses)
254 for namespace, pod, statuses in containers_statuses
257 containers_chained = itertools.chain.from_iterable(containers_status)
259 containers_fields = (
264 is_container_running(status),
268 for namespace, pod, status in containers_chained
273 namespace, pod, container, ContainerExtra(running, image, identifier)
275 for namespace, pod, container, running, image, identifier in containers_fields
278 if not check_istio_sidecars:
279 container_items = filter(lambda container: container.container != istio_sidecar_name, container_items)
281 yield from container_items
284 def sync_post_namespaced_pod_exec(
285 api: kubernetes.client.api.core_v1_api.CoreV1Api,
286 container: ContainerInfo,
287 command: Union[List[str], str],
289 """Function to execute command on selected container.
292 api: Client of the k8s cluster API.
293 container: Object, that represents container in k8s cluster.
294 command: Command to execute as a list of arguments or single string.
297 Dictionary that store informations about command execution.
298 * stdout - Standard output captured from execution.
299 * stderr - Standard error captured from execution.
300 * error - Error object that was received from kubernetes API.
301 * code - Exit code returned by executed process
302 or -1 if container is not running
303 or -2 if other failure occurred.
307 client = kubernetes.stream.stream(
308 api.connect_post_namespaced_pod_exec,
309 namespace=container.namespace,
311 container=container.container,
317 _request_timeout=1.0,
318 _preload_content=False,
321 kubernetes.client.rest.ApiException,
322 kubernetes.client.exceptions.ApiException,
325 if container.extra.running:
335 client.run_forever(timeout=5)
337 stdout = client.read_stdout()
338 stderr = client.read_stderr()
339 error = yaml.safe_load(
340 client.read_channel(kubernetes.stream.ws_client.ERROR_CHANNEL)
343 # TODO: Is there really no better way, to check
344 # execution exit code in python k8s API client?
349 if error["status"] == "Success"
351 if error["reason"] != "NonZeroExitCode"
352 else int(error["details"]["causes"][0]["message"])
365 def generate_python_binaries() -> List[str]:
366 """Function to generate list of names and paths for CPython binaries.
369 List of names and paths, to CPython binaries.
372 dirnames = ["", "/usr/bin/", "/usr/local/bin/"]
375 f"{major}.{minor}" for major, minor in itertools.product("23", string.digits)
378 suffixes = ["", "2", "3"] + majors_minors
380 basenames = [f"python{suffix}" for suffix in suffixes]
382 binaries = [f"{dir}{base}" for dir, base in itertools.product(dirnames, basenames)]
387 def generate_java_binaries() -> List[str]:
388 """Function to generate list of names and paths for OpenJDK binaries.
391 List of names and paths, to OpenJDK binaries.
397 "/usr/local/bin/java",
398 "/etc/alternatives/java",
399 "/usr/java/openjdk-14/bin/java",
405 def determine_versions_abstraction(
406 api: kubernetes.client.api.core_v1_api.CoreV1Api,
407 container: ContainerInfo,
411 """Function to determine list of software versions, that are installed in
415 api: Client of the k8s cluster API.
416 container: Object, that represents container in k8s cluster.
417 binaries: List of names and paths to the abstract software binaries.
418 extractor: Pattern to extract the version string from the output of the binary execution.
421 List of installed software versions.
424 commands = ([binary, "--version"] for binary in binaries)
425 commands_old = ([binary, "-version"] for binary in binaries)
426 commands_all = itertools.chain(commands, commands_old)
428 # TODO: This list comprehension should be parallelized
430 sync_post_namespaced_pod_exec(api, container, command)
431 for command in commands_all
435 f"{result['stdout']}{result['stderr']}"
436 for result in results
437 if result["code"] == 0
440 extractions = (extractor.search(success) for success in successes)
443 set(extraction.group(1) for extraction in extractions if extraction)
449 def determine_versions_of_python(
450 api: kubernetes.client.api.core_v1_api.CoreV1Api, container: ContainerInfo
452 """Function to determine list of CPython versions,
453 that are installed in given container.
456 api: Client of the k8s cluster API.
457 container: Object, that represents container in k8s cluster.
460 List of installed CPython versions.
463 extractor = re.compile("Python ([0-9.]+)")
465 binaries = generate_python_binaries()
467 versions = determine_versions_abstraction(api, container, binaries, extractor)
472 def determine_versions_of_java(
473 api: kubernetes.client.api.core_v1_api.CoreV1Api, container: ContainerInfo
475 """Function to determine list of OpenJDK versions,
476 that are installed in given container.
479 api: Client of the k8s cluster API.
480 container: Object, that represents container in k8s cluster.
483 List of installed OpenJDK versions.
486 extractor = re.compile('openjdk [version" ]*([0-9._]+)')
488 binaries = generate_java_binaries()
490 versions = determine_versions_abstraction(api, container, binaries, extractor)
495 def gather_containers_informations(
496 api: kubernetes.client.api.core_v1_api.CoreV1Api,
499 namespace: Union[None, str],
500 check_istio_sidecars: bool,
501 istio_sidecar_name: str
502 ) -> List[ContainerInfo]:
503 """Get list of all containers names.
506 api: Client of the k8s cluster API.
507 field_selector: Kubernetes field selector, to filter out containers objects.
508 ignore_empty: Determines, if containers with empty versions should be ignored.
509 namespace: Namespace to limit reading pods from
510 check_istio_sidecars: Flag to enable/disable istio sidecars check.
512 istio_sidecar_name: If checking istio sidecars is disabled the name to filter
516 List of initialized objects for containers in k8s cluster.
519 containers = list(list_all_containers(api, field_selector, namespace,
520 check_istio_sidecars, istio_sidecar_name))
522 # TODO: This loop should be parallelized
523 for container in containers:
524 python_versions = determine_versions_of_python(api, container)
525 java_versions = determine_versions_of_java(api, container)
526 container.versions = ContainerVersions(python_versions, java_versions)
529 containers = [c for c in containers if c.versions.python or c.versions.java]
534 def generate_output_tabulate(containers: Iterable[ContainerInfo]) -> str:
535 """Function for generate output string in tabulate format.
538 containers: List of items, that represents containers in k8s cluster.
541 Output string formatted by tabulate module.
558 container.extra.running,
559 " ".join(container.versions.python),
560 " ".join(container.versions.java),
562 for container in containers
565 output = tabulate.tabulate(rows, headers=headers)
570 def generate_output_pprint(containers: Iterable[ContainerInfo]) -> str:
571 """Function for generate output string in pprint format.
574 containers: List of items, that represents containers in k8s cluster.
577 Output string formatted by pprint module.
580 output = pprint.pformat(containers)
585 def generate_output_json(containers: Iterable[ContainerInfo]) -> str:
586 """Function for generate output string in JSON format.
589 containers: List of items, that represents containers in k8s cluster.
592 Output string formatted by json module.
597 "namespace": container.namespace,
598 "pod": container.pod,
599 "container": container.container,
601 "running": container.extra.running,
602 "image": container.extra.image,
603 "identifier": container.extra.identifier,
606 "python": container.versions.python,
607 "java": container.versions.java,
610 for container in containers
613 output = json.dumps(data, indent=4)
618 def generate_and_handle_output(
619 containers: List[ContainerInfo],
621 output_file: pathlib.Path,
624 """Generate and handle the output of the containers software versions.
627 containers: List of items, that represents containers in k8s cluster.
628 output_format: String that will determine output format (tabulate, pprint, json).
629 output_file: Path to file, where output will be save.
630 quiet: Determines if output should be printed, to stdout.
633 output_generators = {
634 "tabulate": generate_output_tabulate,
635 "pprint": generate_output_pprint,
636 "json": generate_output_json,
639 output = output_generators[output_format](containers)
642 output_file.write_text(output)
648 def verify_versions_acceptability(
649 containers: List[ContainerInfo], acceptable: pathlib.Path, quiet: bool
651 """Function for verification of software versions installed in containers.
654 containers: List of items, that represents containers in k8s cluster.
655 acceptable: Path to the YAML file, with the software verification parameters.
656 quiet: Determines if output should be printed, to stdout.
659 0 if the verification was successful or 1 otherwise.
665 if not acceptable.is_file():
666 raise FileNotFoundError(
667 "File with configuration for acceptable does not exists!"
671 "python": {"type": "list", "schema": {"type": "string"}},
672 "java": {"type": "list", "schema": {"type": "string"}},
675 validator = cerberus.Validator(schema)
677 with open(acceptable) as stream:
678 data = yaml.safe_load(stream)
680 if not validator.validate(data):
681 raise cerberus.SchemaError(
682 "Schema of file with configuration for acceptable is not valid."
685 python_acceptable = data.get("python", [])
686 java_acceptable = data.get("java", [])
688 python_not_acceptable = [
689 (container, "python", version)
690 for container in containers
691 for version in container.versions.python
692 if version not in python_acceptable
695 java_not_acceptable = [
696 (container, "java", version)
697 for container in containers
698 for version in container.versions.java
699 if version not in java_acceptable
702 if not python_not_acceptable and not java_not_acceptable:
708 print("List of not acceptable versions")
709 pprint.pprint(python_not_acceptable)
710 pprint.pprint(java_not_acceptable)
715 def main(argv: Optional[List[str]] = None) -> str:
716 """Main entrypoint of the module for verifying versions of CPython and
717 OpenJDK installed in k8s cluster containers.
720 argv: List of command line arguments.
723 args = parse_argv(argv)
725 kubernetes.config.load_kube_config(args.config_file)
727 api = kubernetes.client.CoreV1Api()
728 api.api_client.configuration.debug = args.debug
730 containers = gather_containers_informations(
731 api, args.field_selector, args.ignore_empty, args.namespace,
732 args.check_istio_sidecar, args.istio_sidecar_name
735 generate_and_handle_output(
736 containers, args.output_format, args.output_file, args.quiet
739 code = verify_versions_acceptability(containers, args.acceptable, args.quiet)
744 if __name__ == "__main__":