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
49 from typing import Iterable, List, Optional, Pattern, Union
55 RECOMMENDED_VERSIONS_FILE = "/tmp/recommended_versions.yaml"
56 WAIVER_LIST_FILE = "/tmp/versions_xfail.txt"
60 LOGGER = logging.getLogger("onap-versions-status-inspector")
61 LOGGER.setLevel("INFO")
64 def parse_argv(argv: Optional[List[str]] = None) -> argparse.Namespace:
65 """Function for parsing command line arguments.
68 argv: Unparsed list of command line arguments.
71 Namespace with values from parsed arguments.
75 f"Author: {__author__}\n"
76 f"License: {__license__}\n"
77 f"Copyright: {__copyright__}\n"
80 parser = argparse.ArgumentParser(
81 formatter_class=argparse.RawTextHelpFormatter,
83 description=__summary__,
88 parser.add_argument("-c", "--config-file", help="Name of the kube-config file.")
94 help="Kubernetes field selector, to filter out containers objects.",
101 help="Path to file, where output will be saved.",
107 choices=("tabulate", "pprint", "json"),
109 help="Format of the output file (tabulate, pprint, json).",
116 help="Ignore containers without any versions.",
123 help="Path to YAML file, with list of acceptable software versions.",
129 help="Namespace to use to list pods."
130 "If empty pods are going to be listed from all namespaces",
134 "--check-istio-sidecar",
136 help="Add if you want to check istio sidecars also",
140 "--istio-sidecar-name",
141 default="istio-proxy",
142 help="Name of istio sidecar to filter out",
149 help="Enable debugging mode in the k8s API.",
156 help="Suppress printing text on standard output.",
163 help="Path of the waiver xfail file.",
170 version=f"{__title__} {__version__}",
171 help="Display version information and exit.",
175 "-h", "--help", action="help", help="Display this help text and exit."
178 args = parser.parse_args(argv)
183 @dataclasses.dataclass
184 class ContainerExtra:
185 "Data class, to storage extra informations about container."
192 @dataclasses.dataclass
193 class ContainerVersions:
194 "Data class, to storage software versions from container."
200 @dataclasses.dataclass
202 "Data class, to storage multiple informations about container."
207 extra: ContainerExtra
208 versions: ContainerVersions = None
211 def is_container_running(
212 status: kubernetes.client.models.v1_container_status.V1ContainerStatus,
214 """Function to determine if k8s cluster container is in running state.
217 status: Single item from container_statuses list, that represents container status.
220 If container is in running state.
223 if status.state.terminated:
226 if status.state.waiting:
229 if not status.state.running:
235 def list_all_containers(
236 api: kubernetes.client.api.core_v1_api.CoreV1Api,
238 namespace: Union[None, str],
239 check_istio_sidecars: bool,
240 istio_sidecar_name: str,
241 ) -> Iterable[ContainerInfo]:
242 """Get list of all containers names.
245 api: Client of the k8s cluster API.
246 field_selector: Kubernetes field selector, to filter out containers objects.
247 namespace: Namespace to limit reading pods from
248 check_istio_sidecars: Flag to enable/disable istio sidecars check.
250 istio_sidecar_name: If checking istio sidecars is disabled the name to filter
254 Objects for all containers in k8s cluster.
258 pods = api.list_namespaced_pod(namespace, field_selector=field_selector).items
260 pods = api.list_pod_for_all_namespaces(field_selector=field_selector).items
262 # Filtering to avoid testing integration or replica pods
266 if "replica" not in pod.metadata.name and "integration" not in pod.metadata.name
269 containers_statuses = (
270 (pod.metadata.namespace, pod.metadata.name, pod.status.container_statuses)
272 if pod.status.container_statuses
275 containers_status = (
276 itertools.product([namespace], [pod], statuses)
277 for namespace, pod, statuses in containers_statuses
280 containers_chained = itertools.chain.from_iterable(containers_status)
282 containers_fields = (
287 is_container_running(status),
291 for namespace, pod, status in containers_chained
296 namespace, pod, container, ContainerExtra(running, image, identifier)
298 for namespace, pod, container, running, image, identifier in containers_fields
301 if not check_istio_sidecars:
302 container_items = filter(
303 lambda container: container.container != istio_sidecar_name, container_items
306 yield from container_items
309 def sync_post_namespaced_pod_exec(
310 api: kubernetes.client.api.core_v1_api.CoreV1Api,
311 container: ContainerInfo,
312 command: Union[List[str], str],
314 """Function to execute command on selected container.
317 api: Client of the k8s cluster API.
318 container: Object, that represents container in k8s cluster.
319 command: Command to execute as a list of arguments or single string.
322 Dictionary that store informations about command execution.
323 * stdout - Standard output captured from execution.
324 * stderr - Standard error captured from execution.
325 * error - Error object that was received from kubernetes API.
326 * code - Exit code returned by executed process
327 or -1 if container is not running
328 or -2 if other failure occurred.
335 LOGGER.debug("sync_post_namespaced_pod_exec container= %s", container.pod)
337 client_stream = kubernetes.stream.stream(
338 api.connect_post_namespaced_pod_exec,
339 namespace=container.namespace,
341 container=container.container,
347 _request_timeout=1.0,
348 _preload_content=False,
350 client_stream.run_forever(timeout=5)
351 stdout = client_stream.read_stdout()
352 stderr = client_stream.read_stderr()
353 error = yaml.safe_load(
354 client_stream.read_channel(kubernetes.stream.ws_client.ERROR_CHANNEL)
359 if error["status"] == "Success"
361 if error["reason"] != "NonZeroExitCode"
362 else int(error["details"]["causes"][0]["message"])
365 kubernetes.client.rest.ApiException,
366 kubernetes.client.exceptions.ApiException,
368 LOGGER.debug("Discard unexpected k8s client Error..")
370 LOGGER.debug("Type Error, no error status")
381 def generate_python_binaries() -> List[str]:
382 """Function to generate list of names and paths for CPython binaries.
385 List of names and paths, to CPython binaries.
388 dirnames = ["", "/usr/bin/", "/usr/local/bin/"]
391 f"{major}.{minor}" for major, minor in itertools.product("23", string.digits)
394 suffixes = ["", "2", "3"] + majors_minors
396 basenames = [f"python{suffix}" for suffix in suffixes]
398 binaries = [f"{dir}{base}" for dir, base in itertools.product(dirnames, basenames)]
403 def generate_java_binaries() -> List[str]:
404 """Function to generate list of names and paths for OpenJDK binaries.
407 List of names and paths, to OpenJDK binaries.
413 "/usr/local/bin/java",
414 "/etc/alternatives/java",
415 "/usr/java/openjdk-14/bin/java",
421 def determine_versions_abstraction(
422 api: kubernetes.client.api.core_v1_api.CoreV1Api,
423 container: ContainerInfo,
427 """Function to determine list of software versions, that are installed in
431 api: Client of the k8s cluster API.
432 container: Object, that represents container in k8s cluster.
433 binaries: List of names and paths to the abstract software binaries.
434 extractor: Pattern to extract the version string from the output of the binary execution.
437 List of installed software versions.
440 commands = ([binary, "--version"] for binary in binaries)
441 commands_old = ([binary, "-version"] for binary in binaries)
442 commands_all = itertools.chain(commands, commands_old)
444 # TODO: This list comprehension should be parallelized
446 sync_post_namespaced_pod_exec(api, container, command)
447 for command in commands_all
451 f"{result['stdout']}{result['stderr']}"
452 for result in results
453 if result["code"] == 0
456 extractions = (extractor.search(success) for success in successes)
459 set(extraction.group(1) for extraction in extractions if extraction)
465 def determine_versions_of_python(
466 api: kubernetes.client.api.core_v1_api.CoreV1Api, container: ContainerInfo
468 """Function to determine list of CPython versions,
469 that are installed in given container.
472 api: Client of the k8s cluster API.
473 container: Object, that represents container in k8s cluster.
476 List of installed CPython versions.
479 extractor = re.compile("Python ([0-9.]+)")
481 binaries = generate_python_binaries()
483 versions = determine_versions_abstraction(api, container, binaries, extractor)
488 def determine_versions_of_java(
489 api: kubernetes.client.api.core_v1_api.CoreV1Api, container: ContainerInfo
491 """Function to determine list of OpenJDK versions,
492 that are installed in given container.
495 api: Client of the k8s cluster API.
496 container: Object, that represents container in k8s cluster.
499 List of installed OpenJDK versions.
502 extractor = re.compile('openjdk [version" ]*([0-9._]+)')
504 binaries = generate_java_binaries()
506 versions = determine_versions_abstraction(api, container, binaries, extractor)
511 def gather_containers_informations(
512 api: kubernetes.client.api.core_v1_api.CoreV1Api,
515 namespace: Union[None, str],
516 check_istio_sidecars: bool,
517 istio_sidecar_name: str,
518 ) -> List[ContainerInfo]:
519 """Get list of all containers names.
522 api: Client of the k8s cluster API.
523 field_selector: Kubernetes field selector, to filter out containers objects.
524 ignore_empty: Determines, if containers with empty versions should be ignored.
525 namespace: Namespace to limit reading pods from
526 check_istio_sidecars: Flag to enable/disable istio sidecars check.
528 istio_sidecar_name: If checking istio sidecars is disabled the name to filter
532 List of initialized objects for containers in k8s cluster.
537 api, field_selector, namespace, check_istio_sidecars, istio_sidecar_name
540 LOGGER.info("List of containers: %s", containers)
542 # TODO: This loop should be parallelized
543 for container in containers:
544 LOGGER.info("Container -----------------> %s", container)
545 python_versions = determine_versions_of_python(api, container)
546 java_versions = determine_versions_of_java(api, container)
547 container.versions = ContainerVersions(python_versions, java_versions)
548 LOGGER.info("Container versions: %s", container.versions)
551 containers = [c for c in containers if c.versions.python or c.versions.java]
556 def generate_output_tabulate(containers: Iterable[ContainerInfo]) -> str:
557 """Function for generate output string in tabulate format.
560 containers: List of items, that represents containers in k8s cluster.
563 Output string formatted by tabulate module.
580 container.extra.running,
581 " ".join(container.versions.python),
582 " ".join(container.versions.java),
584 for container in containers
587 output = tabulate.tabulate(rows, headers=headers)
592 def generate_output_pprint(containers: Iterable[ContainerInfo]) -> str:
593 """Function for generate output string in pprint format.
596 containers: List of items, that represents containers in k8s cluster.
599 Output string formatted by pprint module.
602 output = pprint.pformat(containers)
607 def generate_output_json(containers: Iterable[ContainerInfo]) -> str:
608 """Function for generate output string in JSON format.
611 containers: List of items, that represents containers in k8s cluster.
614 Output string formatted by json module.
619 "namespace": container.namespace,
620 "pod": container.pod,
621 "container": container.container,
623 "running": container.extra.running,
624 "image": container.extra.image,
625 "identifier": container.extra.identifier,
628 "python": container.versions.python,
629 "java": container.versions.java,
632 for container in containers
635 output = json.dumps(data, indent=4)
640 def generate_and_handle_output(
641 containers: List[ContainerInfo],
643 output_file: pathlib.Path,
646 """Generate and handle the output of the containers software versions.
649 containers: List of items, that represents containers in k8s cluster.
650 output_format: String that will determine output format (tabulate, pprint, json).
651 output_file: Path to file, where output will be save.
652 quiet: Determines if output should be printed, to stdout.
655 output_generators = {
656 "tabulate": generate_output_tabulate,
657 "pprint": generate_output_pprint,
658 "json": generate_output_json,
660 LOGGER.debug("output_generators: %s", output_generators)
662 output = output_generators[output_format](containers)
666 output_file.write_text(output)
667 except AttributeError:
668 LOGGER.error("Not possible to write_text")
674 def verify_versions_acceptability(
675 containers: List[ContainerInfo], acceptable: pathlib.Path, quiet: bool
677 """Function for verification of software versions installed in containers.
680 containers: List of items, that represents containers in k8s cluster.
681 acceptable: Path to the YAML file, with the software verification parameters.
682 quiet: Determines if output should be printed, to stdout.
685 0 if the verification was successful or 1 otherwise.
693 except AttributeError:
694 LOGGER.error("No acceptable file found")
697 if not acceptable.is_file():
698 raise FileNotFoundError(
699 "File with configuration for acceptable does not exists!"
702 with open(acceptable) as stream:
703 data = yaml.safe_load(stream)
705 python_acceptable = data.get("python3", [])
706 java_acceptable = data.get("java11", [])
708 python_not_acceptable = [
709 (container, "python3", version)
710 for container in containers
711 for version in container.versions.python
712 if version not in python_acceptable
715 java_not_acceptable = [
716 (container, "java11", version)
717 for container in containers
718 for version in container.versions.java
719 if version not in java_acceptable
722 if not python_not_acceptable and not java_not_acceptable:
728 LOGGER.error("List of not acceptable versions")
729 pprint.pprint(python_not_acceptable)
730 pprint.pprint(java_not_acceptable)
735 def main(argv: Optional[List[str]] = None) -> str:
736 """Main entrypoint of the module for verifying versions of CPython and
737 OpenJDK installed in k8s cluster containers.
740 argv: List of command line arguments.
743 args = parse_argv(argv)
745 kubernetes.config.load_kube_config(args.config_file)
747 api = kubernetes.client.CoreV1Api()
748 api.api_client.configuration.debug = args.debug
750 containers = gather_containers_informations(
755 args.check_istio_sidecar,
756 args.istio_sidecar_name,
759 generate_and_handle_output(
760 containers, args.output_format, args.output_file, args.quiet
763 code = verify_versions_acceptability(containers, args.acceptable, args.quiet)
768 if __name__ == "__main__":