From 299054e2f1c90cf9aa5969fbff9ae74a2cdfa75f Mon Sep 17 00:00:00 2001 From: Pablo Rodriguez Nava Date: Mon, 4 Sep 2023 17:17:52 +0200 Subject: [PATCH] Add ci_kustomize action plugin This action plugin is a direct replacement of the edpm_kustomize role. It takes a path to the file you want to kustomize and applies a set of kustomizations given though it's kustomizations parameter or through file ones placed on the same directory where the target manifest is placed. --- ci_framework/plugins/action/ci_kustomize.py | 762 ++++++++++++++++++ .../files/a-sorting-kustomization.yml | 16 + .../files/b-sorting-kustomization.yml | 13 + .../files/c-sorting-kustomization.yml | 13 + .../kustomize/files/cm-kustomization-1.yml | 10 + .../kustomize/files/cm-kustomization-2.yml | 10 + .../kustomize/files/kustomization.yaml | 10 + .../targets/kustomize/files/kustomization.yml | 10 + .../multiple-kustomizations-in-one-file.yml | 20 + .../files/single-kustomization-file-1.yaml | 10 + .../files/single-kustomization-file-2.yaml | 10 + .../targets/kustomize/files/testing-cm.yml | 11 + .../files/testing-combined-manifests.yml | 24 + .../kustomize/files/testing-deployment.yaml | 21 + .../targets/kustomize/tasks/main.yml | 53 ++ .../targets/kustomize/tasks/run_test_case.yml | 216 +++++ .../tasks/run_test_case_validate.yml | 56 ++ .../tasks/run_test_case_validate_failure.yml | 61 ++ .../tasks/run_test_case_validate_success.yml | 95 +++ .../ci_kustomize_extras_scenario.yml | 121 +++ .../ci_kustomize_failures_scenario.yml | 194 +++++ .../ci_kustomize_file_input_scenario.yml | 192 +++++ .../ci_kustomize_mixed_input_scenario.yml | 166 ++++ .../ci_kustomize_vars_input_scenario.yml | 239 ++++++ 24 files changed, 2333 insertions(+) create mode 100644 ci_framework/plugins/action/ci_kustomize.py create mode 100644 ci_framework/tests/integration/targets/kustomize/files/a-sorting-kustomization.yml create mode 100644 ci_framework/tests/integration/targets/kustomize/files/b-sorting-kustomization.yml create mode 100644 ci_framework/tests/integration/targets/kustomize/files/c-sorting-kustomization.yml create mode 100644 ci_framework/tests/integration/targets/kustomize/files/cm-kustomization-1.yml create mode 100644 ci_framework/tests/integration/targets/kustomize/files/cm-kustomization-2.yml create mode 100644 ci_framework/tests/integration/targets/kustomize/files/kustomization.yaml create mode 100644 ci_framework/tests/integration/targets/kustomize/files/kustomization.yml create mode 100644 ci_framework/tests/integration/targets/kustomize/files/multiple-kustomizations-in-one-file.yml create mode 100644 ci_framework/tests/integration/targets/kustomize/files/single-kustomization-file-1.yaml create mode 100644 ci_framework/tests/integration/targets/kustomize/files/single-kustomization-file-2.yaml create mode 100644 ci_framework/tests/integration/targets/kustomize/files/testing-cm.yml create mode 100644 ci_framework/tests/integration/targets/kustomize/files/testing-combined-manifests.yml create mode 100644 ci_framework/tests/integration/targets/kustomize/files/testing-deployment.yaml create mode 100644 ci_framework/tests/integration/targets/kustomize/tasks/main.yml create mode 100644 ci_framework/tests/integration/targets/kustomize/tasks/run_test_case.yml create mode 100644 ci_framework/tests/integration/targets/kustomize/tasks/run_test_case_validate.yml create mode 100644 ci_framework/tests/integration/targets/kustomize/tasks/run_test_case_validate_failure.yml create mode 100644 ci_framework/tests/integration/targets/kustomize/tasks/run_test_case_validate_success.yml create mode 100644 ci_framework/tests/integration/targets/kustomize/tasks/scenarios/ci_kustomize_extras_scenario.yml create mode 100644 ci_framework/tests/integration/targets/kustomize/tasks/scenarios/ci_kustomize_failures_scenario.yml create mode 100644 ci_framework/tests/integration/targets/kustomize/tasks/scenarios/ci_kustomize_file_input_scenario.yml create mode 100644 ci_framework/tests/integration/targets/kustomize/tasks/scenarios/ci_kustomize_mixed_input_scenario.yml create mode 100644 ci_framework/tests/integration/targets/kustomize/tasks/scenarios/ci_kustomize_vars_input_scenario.yml diff --git a/ci_framework/plugins/action/ci_kustomize.py b/ci_framework/plugins/action/ci_kustomize.py new file mode 100644 index 0000000000..c4b74b5310 --- /dev/null +++ b/ci_framework/plugins/action/ci_kustomize.py @@ -0,0 +1,762 @@ +#!/usr/bin/env python3 + +# Copyright Red Hat, Inc. +# Apache License Version 2.0 (see LICENSE) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + + +DOCUMENTATION = r""" +--- +action: ci_kustomize + +short_description: Applies a set of k8s Kustomizations to a set of manifests + +description: +- Allows applying a set of Kustomizations to a given set of manifests using + the kustomize or oc tools. +- kustomize or oc should be discoverable by in the PATH. +- This modules takes a set of manifest files pointed by I(target_path) and + applies to them the set of kustomizations, if available. +- The kustomization result is always saved in a single file and passed to + the caller in the O(result) field if success. +- Kustomizations can be passed by file and/or by I(kustomizations), they get + applied one by one. +- Filesystem Kustomizations are searched by default in the I(target_path), but + more search paths can be added by passing I(kustomizations_paths). +- Filesystem Kustomizations are applied by strictly alphabetical order. +- Kustomizations passed by I(kustomizations) are applied by apparition order. +- Kustomizations passed by I(kustomizations) are applied after the ones from + filesystem by default, if the contrary is not told so by + I(kustomization_files_goes_first). +- I(kustomizations) accepts a wide range of input types. More precisly: plain + string, list of strings, dict with a single kustomization or list of dicts. +- By contraints of the kustomize tool there is no way to apply the given + set of kustomizations to a set of more than one manifests, so, to avoid + diverging the behaviour between single kustomization runs and the rest + this module takes both sets of inputs and translates the result into + a single file. + +options: + target_path: + description: + - Path to the directory where the manifest exists or the specific manifest + to kustomize. + type: str + required: true + kustomizations: + description: + - Kustomizations to apply in list of dicts, list of strings, dict or + string format. + type: iterable + output_path: + description: + - The alternative path were Kustomization result should be copied. + - If not given I(target_path) is used if it points to a file. + - If I(target_path) points to a file 'cifmw-kustomization-result.yaml' + in I(target_path) will be used. + type: str + kustomizations_paths: + description: + - Additional paths where Kustomizations should be searched. + type: list + elements: str + preserve_workspace: + description: + - If true, the workspace is not deleted if success. + - If failure this option is ignored and the workspace is preserved. + type: bool + default: false + kustomization_files_goes_first: + description: + - If true, Kustomizations given by I(kustomizations) are applied before + the ones from filesystem. + type: bool + default: true + sort_ascending: + description: + - If true, file Kustomizations are ordered by ascending order of the file + name. Descnding orther oherwise. + the ones from filesystem. + type: bool + default: true +""" + +EXAMPLES = r""" +# Apply the kustomizations in `/home/user/source/k8s-manifets-dir` to the +# `target_path` manifest and output the result in `output_pat` +- name: Apply the file and variables kustomizations to multiple CRs + ci_kustomize: + target_path: /home/user/source/k8s-manifets-dir/manifest.yaml + output_path: /home/user/source/k8s-manifets-dir/out.yaml + +# Apply the given kustomizations in the kustomizations variable and in +# `/home/user/source/k8s-manifets-dir` and `extra_dir` dirs to the +# manifests available in the `target_path` dir +- name: Apply the file and variables kustomizations to multiple CRs + ci_kustomize: + target_path: /home/user/source/k8s-manifets-dir + kustomizations: + - apiVersion: kustomize.config.k8s.io/v1beta1 + kind: Kustomization + patches: + - patch: |- + - op: replace + path: /metadata/labels/release + value: "1.2.3.4" + target: + kind: Deployment + - |- + --- + apiVersion: kustomize.config.k8s.io/v1beta1 + kind: Kustomization + patches: + - patch: |- + - op: add + path: /metadata/labels/app + value: "my-app" + target: + kind: Deployment + - patch: |- + - op: add + path: /metadata/labels/app + value: "my-app" + target: + kind: ConfigMap + --- + apiVersion: kustomize.config.k8s.io/v1beta1 + kind: Kustomization + patches: + - patch: |- + - op: add + path: /metadata/annotations/imageregistry + value: "https://hub.docker.com/" + target: + kind: Deployment + kustomizations_paths: + - /home/user/source/prod-kustomizations +""" + +RETURN = r""" +count: + description: Total number of Kustomizations applied + returned: success + type: int + sample: 10 +kustomizations_paths: + description: Set of discovered and applied Kustomization files + returned: success + type: list + sample: + - /home/user/source/k8s/kustomization1.yaml + - /home/user/source/k8s/kustomization.yml +output_path: + description: Path to the result of the Kustomization + returned: success + type: str + sample: /home/user/source/k8s/deployment-manifest.yaml +result: + description: A list of the resulting Kustomized manifests. + returned: success + sample: + - apiVersion: v1 + kind: ConfigMap + metadata: + name: testing-cm + data: + test1.properties: | + test-var=test-value + - apiVersion: v1 + kind: Secret + metadata: + name: testing-secret + data: + .secret-file: dmFsdWUtMg0KDQo= +""" + +import fnmatch +import functools +import hashlib +import os +import re +import shutil +import subprocess +import typing + + +import dataclasses +import pathlib +import yaml + + +from ansible.plugins.action import ActionBase + + +@dataclasses.dataclass +class CifmwKustomizeResult: + count: int + kustomizations_paths: typing.List[str] + output_path: str + result: typing.List[typing.Dict[typing.Dict, typing.Any]] + changed: bool + + +class CifmwKustomizeException(Exception): + def __init__(self, error): + super().__init__(error) + self.error = str(error) + + def to_dict(self): + return {"error": self.error} + + +class CifmwKustomizeArgsValidationException(CifmwKustomizeException): + def __init__(self, error, argument, value=None): + super().__init__(error) + self.argument = argument + self.value = value + + def to_dict(self): + val = { + "argument": self.argument, + } + + if self.value: + val["value"] = self.value + + return {**val, **(super().to_dict())} + + +class CifmwKustomizeContentValidationException(CifmwKustomizeException): + def __init__(self, msg: str, kustomization_content=None): + super().__init__(msg) + self.kustomization_content = self.__safe_dump_content(kustomization_content) + + @staticmethod + def __safe_dump_content(kustomization_content): + if isinstance(kustomization_content, str): + return kustomization_content + + try: + # If a kustomization failed cause some validation + # safely handle it as a string + to_dump_content = ( + [kustomization_content] + if isinstance(kustomization_content, dict) + else kustomization_content + ) + return yaml.dump_all(to_dump_content) if to_dump_content else None + + except yaml.YAMLError: + return str(kustomization_content) + + def to_dict(self): + val = { + "kustomization": self.kustomization_content, + } + return {**val, **(super().to_dict())} + + +class CifmwKustomizeApplyKustomizationException(CifmwKustomizeException): + def __init__( + self, + msg: str, + details: str, + kustomization: typing.Dict[str, typing.Any], + kustomization_path, + ): + # Errors from kustomize are prefixed by "Error:" Get rid off that + super().__init__(re.sub(r"(?is)^error\s?:", "", msg).strip()) + self.details = details + self.kustomization = kustomization + self.kustomization_path = str( + kustomization_path.absolute() + if isinstance(kustomization_path, pathlib.Path) + else kustomization_path + ) + + def to_dict(self): + val = { + "details": self.details, + "kustomization": self.kustomization, + "kustomization_path": self.kustomization_path, + } + return {**val, **(super().to_dict())} + + +def sha1_file(file_path: typing.Union[str, os.PathLike]) -> str: + sha1 = hashlib.sha1() + with open(file_path, "rb") as f: + while True: + data = f.read(65536) + if not data: + break + sha1.update(data) + return sha1.hexdigest() + + +class CifmwKustomizeWrapper: + __CI_KUSTOMIZE_CMD_OPTS = [".", "-o"] + __CI_KUSTOMIZE_TOOLS_OPTS = {"kustomize": ["build"], "oc": ["kustomize"]} + __CI_KUSTOMIZE_WORKSPACE_DIR_NAME = "cifmw-kustomize-workspace" + __CI_KUSTOMIZE_FILES_GLOB_EXPRESSIONS = ["*.yaml", "*.yml"] + __CI_KUSTOMIZE_FILE_API_VERSION = "kustomize.config.k8s.io" + __CI_KUSTOMIZE_KIND_FIELD_NAME = "kind" + __CI_KUSTOMIZE_API_VERSION_FIELD_NAME = "apiVersion" + __CI_KUSTOMIZE_FILE_KIND = "Kustomization" + __CI_KUSTOMIZE_DEFAULT_RESULT_FILE_NAME = "cifmw-kustomization-result.yaml" + __CI_KUSTOMIZE_ENCODING = "utf-8" + + def __init__( + self, + target_path: typing.Union[str, os.PathLike], + kustomizations: typing.Union[ + str, typing.Dict, typing.List[typing.Union[str, typing.Dict]] + ] = None, + kustomizations_paths: typing.List[typing.Union[str, os.PathLike]] = None, + output_path: typing.Union[str, os.PathLike] = None, + kustomization_files_goes_first: bool = True, + tools_search_path: str = None, + preserve_workspace: bool = False, + sort_ascending: bool = True, + ): + self.__validate_inputs(target_path, output_path, kustomizations_paths) + + self.target_path = pathlib.Path(target_path) + target_base_path = ( + self.target_path.parent if self.target_path.is_file() else self.target_path + ) + + if output_path: + self.output_path = pathlib.Path(output_path) + elif self.target_path.is_file(): + self.output_path = self.target_path + else: + self.output_path = self.target_path.joinpath( + self.__CI_KUSTOMIZE_DEFAULT_RESULT_FILE_NAME + ) + + self.__workspace_dir = target_base_path.joinpath( + self.__CI_KUSTOMIZE_WORKSPACE_DIR_NAME + ) + self.__target_workspace_file = self.__workspace_dir.joinpath( + self.__CI_KUSTOMIZE_DEFAULT_RESULT_FILE_NAME + ) + self.__kustomization_scan_paths = [target_base_path] + [ + pathlib.Path(path) for path in (kustomizations_paths or []) + ] + self.__kustomization_files_goes_first = kustomization_files_goes_first + self.__preserve_workspace = preserve_workspace + self.__sort_ascending = sort_ascending + self.__input_kustomizations = self.__parse_kustomizations_input(kustomizations) + self.__kustomize_cmd = self.__create_kustomize_build_command( + self.__target_workspace_file, tools_search_path=tools_search_path + ) + + def __enter__(self): + if self.__workspace_dir.exists(): + shutil.rmtree(self.__workspace_dir) + self.__workspace_dir.mkdir() + + return self + + def __exit__(self, exc_type, _, __): + if not (exc_type or self.__preserve_workspace): + shutil.rmtree(self.__workspace_dir) + + @staticmethod + def __validate_inputs(target_path, output_path, kustomizations_paths): + if not target_path: + raise CifmwKustomizeArgsValidationException( + "target path is mandatory", "target_path" + ) + + if not os.path.exists(target_path): + raise CifmwKustomizeArgsValidationException( + "path does not exist", + "target_path", + value=target_path, + ) + + output_path = pathlib.Path(output_path) if output_path else None + if output_path and output_path.exists() and output_path.is_dir(): + raise CifmwKustomizeArgsValidationException( + "output file cannot point to a directory", + "output_path", + value=str(output_path.absolute()), + ) + + if kustomizations_paths and ( + (not isinstance(kustomizations_paths, list)) + or ( + not all( + (isinstance(path, str) or isinstance(path, pathlib.Path)) + for path in kustomizations_paths + ) + ) + ): + raise CifmwKustomizeArgsValidationException( + "kustomizations_paths should be a list of paths", + "kustomizations_paths", + value=kustomizations_paths, + ) + + def __copy_input_to_workspace(self): + if self.target_path.is_dir(): + manifests_contents = self.__get_manifests_paths_from_dir_candidates( + self.target_path, skip_paths=[self.output_path] + ) + # Fetch all the individual manifests from the target dir and dump + # them into a single file + with self.__target_workspace_file.open( + mode="w", encoding=self.__CI_KUSTOMIZE_ENCODING + ) as outfile: + combined_manifest_list = functools.reduce( + lambda x, y: x + y, + (cnt for cnt in manifests_contents.values()), + [], + ) + yaml.dump_all(combined_manifest_list, outfile) + else: + shutil.copy(self.target_path, self.__target_workspace_file) + + @classmethod + def __create_kustomize_build_command( + cls, target_path, tools_search_path=None + ) -> typing.List[str]: + kustomize_cmd = [cls.__find_kustomize_tool(tools_search_path)] + tool_name = os.path.basename(kustomize_cmd[0]) + if tool_name in cls.__CI_KUSTOMIZE_TOOLS_OPTS: + kustomize_cmd.extend(cls.__CI_KUSTOMIZE_TOOLS_OPTS[tool_name]) + + kustomize_cmd.extend(cls.__CI_KUSTOMIZE_CMD_OPTS) + kustomize_cmd.append(str(target_path.absolute())) + + return kustomize_cmd + + @classmethod + def __check_input_kustomizations( + cls, kustomizations_list: typing.List[typing.Dict[str, typing.Any]] + ): + for kustomization_content in kustomizations_list: + if cls.__CI_KUSTOMIZE_KIND_FIELD_NAME not in kustomization_content: + raise CifmwKustomizeContentValidationException( + "Kustomization input contains a manifest without" + f" {cls.__CI_KUSTOMIZE_KIND_FIELD_NAME} field", + kustomization_content=kustomization_content, + ) + if ( + kustomization_content[cls.__CI_KUSTOMIZE_KIND_FIELD_NAME] + != cls.__CI_KUSTOMIZE_FILE_KIND + ): + raise CifmwKustomizeContentValidationException( + "Kustomization input contains a manifest with a" + f" {cls.__CI_KUSTOMIZE_KIND_FIELD_NAME} that is not" + f" {cls.__CI_KUSTOMIZE_FILE_KIND}", + kustomization_content=kustomization_content, + ) + if cls.__CI_KUSTOMIZE_API_VERSION_FIELD_NAME not in kustomization_content: + raise CifmwKustomizeContentValidationException( + "Kustomization input contains a manifest without" + f" {cls.__CI_KUSTOMIZE_API_VERSION_FIELD_NAME} field", + kustomization_content=kustomization_content, + ) + if not kustomization_content[ + cls.__CI_KUSTOMIZE_API_VERSION_FIELD_NAME + ].startswith(cls.__CI_KUSTOMIZE_FILE_API_VERSION): + raise CifmwKustomizeContentValidationException( + "Kustomization input contains a manifest with a" + f" {cls.__CI_KUSTOMIZE_API_VERSION_FIELD_NAME} that is not" + f" {cls.__CI_KUSTOMIZE_FILE_API_VERSION}", + kustomization_content=kustomization_content, + ) + + @staticmethod + def __load_string_based_kustomization( + kustomization_content: str, + ) -> typing.Dict[str, typing.Any]: + # Try to parse the kustomizations as a string with multiple manifests + try: + return list(yaml.load_all(kustomization_content, Loader=yaml.Loader)) + except yaml.YAMLError as err: + raise CifmwKustomizeContentValidationException( + f"Failed to load a kustomization. YAML Error {str(err)}", + kustomization_content=kustomization_content, + ) from err + + @classmethod + def __parse_kustomizations_input( + cls, + kustomizations: typing.Union[ + str, typing.Dict, typing.List[typing.Union[str, typing.Dict]] + ], + ) -> typing.List[typing.Dict[str, typing.Any]]: + loaded_kustomizations = [] + + if isinstance(kustomizations, str): + loaded_kustomizations = cls.__load_string_based_kustomization( + kustomizations + ) + elif isinstance(kustomizations, dict): + # Assume single kustomization was given + loaded_kustomizations.append(kustomizations) + elif isinstance(kustomizations, list) and all( + (isinstance(item_value, str) or isinstance(item_value, dict)) + for item_value in kustomizations + ): + # The list of kustomizations is a list of strings or dicts + for item_value in kustomizations: + if isinstance(item_value, str): + loaded_kustomizations.extend( + cls.__load_string_based_kustomization(item_value) + ) + else: + loaded_kustomizations.append(item_value) + + elif not isinstance(kustomizations, type(None)): + raise CifmwKustomizeException("Unsupported kustomizations list type") + + cls.__check_input_kustomizations(loaded_kustomizations) + + return loaded_kustomizations + + @staticmethod + def __find_kustomize_tool(tools_search_path=None) -> str: + tool_path = shutil.which("oc", path=tools_search_path) or shutil.which( + "kustomize", path=tools_search_path + ) + if tool_path: + return tool_path + + raise CifmwKustomizeException("Cannot find oc nor kustomize in PATH") + + def __apply_kustomization( + self, kustomization_content: typing.Dict[str, typing.Any], index: int + ): + kustomization_path = self.__workspace_dir.joinpath("kustomization.yaml") + with kustomization_path.open( + "w", encoding=self.__CI_KUSTOMIZE_ENCODING + ) as kustomization_file: + yaml.dump(kustomization_content, kustomization_file) + + run_result = subprocess.run( + self.__kustomize_cmd, + encoding="utf-8", + capture_output=True, + cwd=self.__workspace_dir, + check=False, + ) + if run_result.returncode: + error = run_result.stderr or run_result.stdout + error_header = error.splitlines()[0] + raise CifmwKustomizeApplyKustomizationException( + error_header, + error, + kustomization_content, + kustomization_path, + ) + + kustomization_path.rename(kustomization_path.with_suffix(f".{index}.yml")) + + @classmethod + def __dict_is_manifest(cls, content) -> bool: + return ( + isinstance(content, dict) + and (cls.__CI_KUSTOMIZE_KIND_FIELD_NAME in content) + and (cls.__CI_KUSTOMIZE_API_VERSION_FIELD_NAME in content) + ) + + @classmethod + def __is_kustomization_content(cls, manifest_content) -> bool: + return ( + cls.__dict_is_manifest(manifest_content) + and manifest_content[cls.__CI_KUSTOMIZE_KIND_FIELD_NAME] + == cls.__CI_KUSTOMIZE_FILE_KIND + and manifest_content[cls.__CI_KUSTOMIZE_API_VERSION_FIELD_NAME].startswith( + cls.__CI_KUSTOMIZE_FILE_API_VERSION + ) + ) + + def __read_kustomize_candidates( + self, + ) -> typing.Dict[pathlib.Path, typing.List[typing.Any]]: + resulting_files_content = {} + for file_path in self.__get_all_yamls_in_scan_paths(): + try: + with file_path.open("r", encoding=self.__CI_KUSTOMIZE_ENCODING) as file: + for manifest_content in yaml.load_all(file, Loader=yaml.Loader): + if self.__is_kustomization_content(manifest_content): + if file_path not in resulting_files_content: + resulting_files_content[file_path] = [] + resulting_files_content[file_path].append(manifest_content) + except yaml.YAMLError: + continue + + return resulting_files_content + + @classmethod + def __get_manifests_paths_from_dir_candidates( + cls, base_dir: pathlib.Path, skip_paths: pathlib.Path = None + ) -> typing.Dict[pathlib.Path, typing.List[typing.Any]]: + resulting_files_content = {} + for file_path in cls.__get_yaml_files_in_path(base_dir, skip_paths=skip_paths): + file_content = file_path.read_text(encoding="utf-8") + try: + yaml_content = list(yaml.load_all(file_content, Loader=yaml.Loader)) + if all( + cls.__dict_is_manifest(manifest) + and (not cls.__is_kustomization_content(manifest)) + for manifest in yaml_content + ): + resulting_files_content[file_path] = yaml_content + except yaml.YAMLError: + continue + return resulting_files_content + + def __set_target_kustomization_resource( + self, kustomizations_list: typing.Dict[str, typing.Any] + ): + # Why this gross way of replacing the resources? + # This plugin copies in it's initialization all the targeted + # resources(or resource if a path to a file is given) always + # to a single file inside the workspace dir, that's how it works. + # Many input manifest files are translated into a single one, the + # one copied into the workspace dir, so, replacing the original + # content with a harcoded list that points to that file is fine. + for kustomization_content in kustomizations_list: + kustomization_content["resources"] = [self.__target_workspace_file.name] + + @classmethod + def __get_yaml_files_in_path( + cls, path: pathlib.Path, skip_paths: pathlib.Path = None + ) -> typing.List[pathlib.Path]: + avoid_paths = skip_paths or [] + results = [] + if ( + path.is_file() + and path not in avoid_paths + and any( + fnmatch.fnmatch(path, e) + for e in cls.__CI_KUSTOMIZE_FILES_GLOB_EXPRESSIONS + ) + ): + results.append(path) + elif path.is_dir(): + results.extend( + [ + f + for f_ in [ + path.glob(e) for e in cls.__CI_KUSTOMIZE_FILES_GLOB_EXPRESSIONS + ] + for f in f_ + if f not in avoid_paths + ] + ) + + return results + + def __get_all_yamls_in_scan_paths(self) -> typing.List[pathlib.Path]: + return sorted( + [ + f + for f_ in [ + self.__get_yaml_files_in_path(path) + for path in self.__kustomization_scan_paths + ] + for f in f_ + ], + key=lambda i: i.name, + reverse=not self.__sort_ascending, + ) + + def __create_kustomization_list(self): + files_candidates = self.__read_kustomize_candidates() + files_kustomizations = [ + kustomization + for file_kustomizations in files_candidates.values() + for kustomization in file_kustomizations + ] + + kustomizations = [] + if not self.__kustomization_files_goes_first: + kustomizations = self.__input_kustomizations + files_kustomizations + else: + kustomizations = files_kustomizations + self.__input_kustomizations + + self.__set_target_kustomization_resource(kustomizations) + + return kustomizations, list(files_candidates.keys()) + + def kustomize(self) -> CifmwKustomizeResult: + original_output_path_hash = ( + sha1_file(self.output_path) if self.output_path.exists() else None + ) + + self.__copy_input_to_workspace() + + kustomizations, discovered_files = self.__create_kustomization_list() + for index, kustomization in enumerate(kustomizations): + self.__apply_kustomization(kustomization, index) + + shutil.copy(self.__target_workspace_file, self.output_path) + + changed = (not original_output_path_hash) or ( + original_output_path_hash != sha1_file(self.output_path) + ) + + output_content = list( + yaml.safe_load_all( + self.output_path.read_text(encoding=self.__CI_KUSTOMIZE_ENCODING) + ) + ) + + return CifmwKustomizeResult( + len(kustomizations), + [str(path.absolute()) for path in discovered_files], + str(self.output_path.absolute()), + output_content, + changed, + ) + + +class ActionModule(ActionBase): + def run(self, tmp=None, task_vars=None): + if task_vars is None: + task_vars = dict() + + result = super(ActionModule, self).run(tmp, task_vars) + del tmp + + target_path = self._task.args.get("target_path", None) + kustomizations = self._task.args.get("kustomizations", None) + kustomizations_paths = self._task.args.get("kustomizations_paths", None) + output_path = self._task.args.get("output_path", None) + kustomization_files_goes_first = self._task.args.get( + "kustomization_files_goes_first", True + ) + preserve_workspace = self._task.args.get("preserve_workspace", False) + sort_ascending = self._task.args.get("sort_ascending", True) + final_environment = {} + self._compute_environment_string(final_environment) + + try: + with CifmwKustomizeWrapper( + target_path, + kustomizations=kustomizations, + kustomizations_paths=kustomizations_paths, + output_path=output_path, + kustomization_files_goes_first=kustomization_files_goes_first, + tools_search_path=final_environment.get("PATH", None), + preserve_workspace=preserve_workspace, + sort_ascending=sort_ascending, + ) as kustomize: + kustomize_result = kustomize.kustomize() + result.update(dataclasses.asdict(kustomize_result)) + result["failed"] = False + except CifmwKustomizeException as run_exception: + result = {**result, **(run_exception.to_dict())} + result["failed"] = True + + return result diff --git a/ci_framework/tests/integration/targets/kustomize/files/a-sorting-kustomization.yml b/ci_framework/tests/integration/targets/kustomize/files/a-sorting-kustomization.yml new file mode 100644 index 0000000000..d35a0f87ff --- /dev/null +++ b/ci_framework/tests/integration/targets/kustomize/files/a-sorting-kustomization.yml @@ -0,0 +1,16 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +patches: + - patch: |- + - op: replace + path: /metadata/labels/cifmw-label-1 + value: "cifmw-label-1-value" + - op: replace + path: /metadata/labels/cifmw-label-2 + value: "cifmw-label-2-value" + - op: replace + path: /metadata/labels/cifmw-label-3 + value: "cifmw-label-3-value" + target: + kind: Deployment diff --git a/ci_framework/tests/integration/targets/kustomize/files/b-sorting-kustomization.yml b/ci_framework/tests/integration/targets/kustomize/files/b-sorting-kustomization.yml new file mode 100644 index 0000000000..af963cd8e0 --- /dev/null +++ b/ci_framework/tests/integration/targets/kustomize/files/b-sorting-kustomization.yml @@ -0,0 +1,13 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +patches: + - patch: |- + - op: replace + path: /metadata/labels/cifmw-label-1 + value: "cifmw-label-1-value-override-b" + - op: replace + path: /metadata/labels/cifmw-label-2 + value: "cifmw-label-2-value-override-b" + target: + kind: Deployment diff --git a/ci_framework/tests/integration/targets/kustomize/files/c-sorting-kustomization.yml b/ci_framework/tests/integration/targets/kustomize/files/c-sorting-kustomization.yml new file mode 100644 index 0000000000..1b1bf981e3 --- /dev/null +++ b/ci_framework/tests/integration/targets/kustomize/files/c-sorting-kustomization.yml @@ -0,0 +1,13 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +patches: + - patch: |- + - op: replace + path: /metadata/labels/cifmw-label-2 + value: "cifmw-label-2-value-override-c" + - op: replace + path: /metadata/labels/cifmw-label-3 + value: "cifmw-label-3-value-override-c" + target: + kind: Deployment diff --git a/ci_framework/tests/integration/targets/kustomize/files/cm-kustomization-1.yml b/ci_framework/tests/integration/targets/kustomize/files/cm-kustomization-1.yml new file mode 100644 index 0000000000..b60422ac00 --- /dev/null +++ b/ci_framework/tests/integration/targets/kustomize/files/cm-kustomization-1.yml @@ -0,0 +1,10 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +patches: + - patch: |- + - op: replace + path: /metadata/labels/cifmw-label-1 + value: "cifmw-label-1-value" + target: + kind: ConfigMap diff --git a/ci_framework/tests/integration/targets/kustomize/files/cm-kustomization-2.yml b/ci_framework/tests/integration/targets/kustomize/files/cm-kustomization-2.yml new file mode 100644 index 0000000000..3657c73128 --- /dev/null +++ b/ci_framework/tests/integration/targets/kustomize/files/cm-kustomization-2.yml @@ -0,0 +1,10 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +patches: + - patch: |- + - op: replace + path: /metadata/labels/cifmw-label-2 + value: "cifmw-label-2-value" + target: + kind: ConfigMap diff --git a/ci_framework/tests/integration/targets/kustomize/files/kustomization.yaml b/ci_framework/tests/integration/targets/kustomize/files/kustomization.yaml new file mode 100644 index 0000000000..3dcc89d09f --- /dev/null +++ b/ci_framework/tests/integration/targets/kustomize/files/kustomization.yaml @@ -0,0 +1,10 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +patches: + - patch: |- + - op: replace + path: /metadata/labels/cifmw-label-1 + value: "cifmw-label-1-value" + target: + kind: Deployment diff --git a/ci_framework/tests/integration/targets/kustomize/files/kustomization.yml b/ci_framework/tests/integration/targets/kustomize/files/kustomization.yml new file mode 100644 index 0000000000..bbcfe49a19 --- /dev/null +++ b/ci_framework/tests/integration/targets/kustomize/files/kustomization.yml @@ -0,0 +1,10 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +patches: + - patch: |- + - op: replace + path: /metadata/labels/cifmw-label-2 + value: "cifmw-label-2-value" + target: + kind: Deployment diff --git a/ci_framework/tests/integration/targets/kustomize/files/multiple-kustomizations-in-one-file.yml b/ci_framework/tests/integration/targets/kustomize/files/multiple-kustomizations-in-one-file.yml new file mode 100644 index 0000000000..29fc03acac --- /dev/null +++ b/ci_framework/tests/integration/targets/kustomize/files/multiple-kustomizations-in-one-file.yml @@ -0,0 +1,20 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +patches: + - patch: |- + - op: replace + path: /metadata/labels/cifmw-label-4 + value: "cifmw-label-4-value" + target: + kind: Deployment +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +patches: + - patch: |- + - op: replace + path: /metadata/labels/cifmw-label-5 + value: "cifmw-label-5-value" + target: + kind: Deployment diff --git a/ci_framework/tests/integration/targets/kustomize/files/single-kustomization-file-1.yaml b/ci_framework/tests/integration/targets/kustomize/files/single-kustomization-file-1.yaml new file mode 100644 index 0000000000..47a2640727 --- /dev/null +++ b/ci_framework/tests/integration/targets/kustomize/files/single-kustomization-file-1.yaml @@ -0,0 +1,10 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +patches: + - patch: |- + - op: replace + path: /metadata/labels/cifmw-label-3 + value: "cifmw-label-3-value" + target: + kind: Deployment diff --git a/ci_framework/tests/integration/targets/kustomize/files/single-kustomization-file-2.yaml b/ci_framework/tests/integration/targets/kustomize/files/single-kustomization-file-2.yaml new file mode 100644 index 0000000000..3e470fba7c --- /dev/null +++ b/ci_framework/tests/integration/targets/kustomize/files/single-kustomization-file-2.yaml @@ -0,0 +1,10 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +patches: + - patch: |- + - op: replace + path: /metadata/labels/cifmw-label-6 + value: "cifmw-label-6-value" + target: + kind: Deployment diff --git a/ci_framework/tests/integration/targets/kustomize/files/testing-cm.yml b/ci_framework/tests/integration/targets/kustomize/files/testing-cm.yml new file mode 100644 index 0000000000..45d7e9dce4 --- /dev/null +++ b/ci_framework/tests/integration/targets/kustomize/files/testing-cm.yml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: testing-cm + labels: + app: test +data: + test1.properties: | + test-var=test-value + test2.properties: | + test-var2=test-value2 diff --git a/ci_framework/tests/integration/targets/kustomize/files/testing-combined-manifests.yml b/ci_framework/tests/integration/targets/kustomize/files/testing-combined-manifests.yml new file mode 100644 index 0000000000..bd20eab9d3 --- /dev/null +++ b/ci_framework/tests/integration/targets/kustomize/files/testing-combined-manifests.yml @@ -0,0 +1,24 @@ +apiVersion: v1 +kind: Secret +metadata: + name: testing-secret + labels: + app: test +data: + .secret-file: dmFsdWUtMg0KDQo= +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: testing-job + labels: + app: test +spec: + template: + spec: + containers: + - name: pi + image: perl:5.34.0 + command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"] + restartPolicy: Never + backoffLimit: 4 diff --git a/ci_framework/tests/integration/targets/kustomize/files/testing-deployment.yaml b/ci_framework/tests/integration/targets/kustomize/files/testing-deployment.yaml new file mode 100644 index 0000000000..b8155f3b50 --- /dev/null +++ b/ci_framework/tests/integration/targets/kustomize/files/testing-deployment.yaml @@ -0,0 +1,21 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: testing-deployment + labels: + app: nginx +spec: + replicas: 3 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.14.2 + ports: + - containerPort: 80 diff --git a/ci_framework/tests/integration/targets/kustomize/tasks/main.yml b/ci_framework/tests/integration/targets/kustomize/tasks/main.yml new file mode 100644 index 0000000000..371a3e4d6f --- /dev/null +++ b/ci_framework/tests/integration/targets/kustomize/tasks/main.yml @@ -0,0 +1,53 @@ +--- +- name: "Create a directory to download each kustomization tool" + ansible.builtin.tempfile: + state: directory + suffix: cifmw-ci-kustomize-bins-dir + register: _ci_kustomize_temporal_bins_dir + +- name: Set files dir as a fact + vars: + role_test_dir: "{{ ansible_env.HOME }}/code/ansible_collections/cifmw/general/tests/integration/targets/kustomize" + ansible.builtin.set_fact: + ci_kustomize_scenarios_dir_path: "{{ role_test_dir }}/tasks/scenarios" + ci_kustomize_files_dir_path: "{{ role_test_dir }}/files" + ci_kustomize_oc_bin_path: "{{ _ci_kustomize_temporal_bins_dir.path }}/oc" + ci_kustomize_kustomize_bin_path: "{{ _ci_kustomize_temporal_bins_dir.path }}/kustomize" + +- name: Create OC and kustomize binaries path + ansible.builtin.file: + path: "{{ item }}" + state: directory + loop: + - "{{ ci_kustomize_oc_bin_path }}" + - "{{ ci_kustomize_kustomize_bin_path }}" + +- name: Fetch oc from OCP mirror + ansible.builtin.unarchive: + src: https://mirror.openshift.com/pub/openshift-v4/x86_64/clients/ocp/stable/openshift-client-linux.tar.gz + dest: "{{ ci_kustomize_oc_bin_path }}" + remote_src: true + +- name: Fetch kustomize from sigs Github + ansible.builtin.unarchive: + src: https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize%2Fv5.1.1/kustomize_v5.1.1_linux_amd64.tar.gz + dest: "{{ ci_kustomize_kustomize_bin_path }}" + remote_src: true + +- name: Recursively find scenario files + ansible.builtin.find: + paths: "{{ ci_kustomize_scenarios_dir_path }}" + patterns: '*.yml' + register: ci_kustomize_scenarios_find + +- name: Run scenario file + ansible.builtin.include_tasks: "{{ scenario_path }}" + loop: "{{ ci_kustomize_scenarios_find.files | map(attribute='path') }}" + loop_control: + label: "{{ scenario_path }}" + loop_var: "scenario_path" + +- name: "Delete the temporal bins directory" + ansible.builtin.file: + path: "{{ _ci_kustomize_temporal_bins_dir.path }}" + state: absent diff --git a/ci_framework/tests/integration/targets/kustomize/tasks/run_test_case.yml b/ci_framework/tests/integration/targets/kustomize/tasks/run_test_case.yml new file mode 100644 index 0000000000..b196a33e47 --- /dev/null +++ b/ci_framework/tests/integration/targets/kustomize/tasks/run_test_case.yml @@ -0,0 +1,216 @@ +--- +- name: "[{{ kustomize_tc_name }}] Create temporary directory for the scenario" # noqa: name[template] + ansible.builtin.tempfile: + state: directory + suffix: cifmw-ci-kustomize-scenario-dir + register: _ci_kustomize_temporal_dir + +- name: "[{{ kustomize_tc_name }}] Create extra directories if requested" # noqa: name[template] + ansible.builtin.tempfile: + state: directory + suffix: cifmw-ci-kustomize-scenario-extras-dir + register: _ci_kustomize_extras_paths + loop: "{{ kustomize_tc_copy_kustomization_paths | default([]) }}" + +- name: "[{{ kustomize_tc_name }}] Set a fact with the all the files that needs to be copied" # noqa: name[template] + vars: + kustomizations_extra_paths: >- + {% set paths = {} -%} + {% for temp_paths_out in _ci_kustomize_extras_paths.results | default([]) -%} + {% if (temp_paths_out.item is string) -%} + {% set target_path = [temp_paths_out.path, temp_paths_out.item] | path_join -%} + {% set _ = paths.update({target_path: temp_paths_out.item}) -%} + {% else -%} + {% for item in temp_paths_out.item | default([]) -%} + {% set target_path = [temp_paths_out.path, item] | path_join -%} + {% set _ = paths.update({target_path: item}) -%} + {% endfor -%} + {% endif -%} + {% endfor -%} + {{ paths }} + kustomizations_paths: >- + {% set paths = {} -%} + {% for file_name in kustomize_tc_copy_kustomization_files | default([]) -%} + {% set target_path = [_ci_kustomize_temporal_dir.path, file_name] | path_join -%} + {% set _ = paths.update({target_path: file_name}) -%} + {% endfor -%} + {{ paths }} + target_dir_manifests_paths: >- + {% set paths = {} -%} + {% for file_name in kustomize_tc_target_dir_paths | default([]) -%} + {% set target_path = [_ci_kustomize_temporal_dir.path, file_name] | path_join -%} + {% set _ = paths.update({target_path: file_name}) -%} + {% endfor -%} + {{ paths }} + target_manifests_to_copy: >- + {{ + { + ( + [_ci_kustomize_temporal_dir.path, kustomize_tc_target_manifest_path] | path_join + ): kustomize_tc_target_manifest_path + } + if (kustomize_tc_target_manifest_path is defined) + else + ( + target_dir_manifests_paths + if (kustomize_tc_target_dir_paths is defined) + else {} + ) + }} + ansible.builtin.set_fact: + _ci_kustomize_extra_kustomizations: "{{ kustomizations_extra_paths }}" + _ci_kustomize_kustomizations: "{{ kustomizations_paths }}" + _ci_kustomize_to_copy: >- + {{ + target_manifests_to_copy | + combine(kustomizations_paths) | + combine(kustomizations_extra_paths) + }} + +# Typically used for testing non-existing path validations +- name: "[{{ kustomize_tc_name }}] Skip copying not existing files if told so" # noqa: name[template] + when: "kustomize_tc_skip_non_existing_sources | default(false)" + block: + - name: "[{{ kustomize_tc_name }}] Check if source file exits" # noqa: name[template] + ansible.builtin.stat: + path: "{{ ci_kustomize_files_dir_path }}/{{ item }}" + register: _ci_kustomize_source_files_stat + loop: "{{ _ci_kustomize_to_copy.values() | list | unique }}" + + - name: "[{{ kustomize_tc_name }}] Filter source files to include only the existing ones" # noqa: name[template] + vars: + existing_paths: >- + {{ + _ci_kustomize_source_files_stat.results | + selectattr('stat.exists', 'equalto', true) | + map(attribute='item') + }} + ansible.builtin.set_fact: + _ci_kustomize_to_copy: >- + {{ + _ci_kustomize_to_copy | + dict2items | + selectattr('value', 'in', existing_paths) | + items2dict + }} + +- name: "[{{ kustomize_tc_name }}] Copy files to each desidered dest" # noqa: name[template] + ansible.builtin.copy: + dest: "{{ item.key }}" + src: "{{ ci_kustomize_files_dir_path }}/{{ item.value }}" + remote_src: true + loop: "{{ _ci_kustomize_to_copy | dict2items }}" + +- name: "[{{ kustomize_tc_name }}] Run TC" # noqa: name[template] + environment: + PATH: >- + {{ + ( + kustomize_tc_extra_path + ':' + ansible_env.PATH + ) + if (kustomize_tc_extra_path is defined) else + ( + kustomize_tc_set_path + if (kustomize_tc_set_path is defined) else + ansible_env.PATH + ) + }} + vars: + _kustomizations_paths: >- + {% set paths = [] -%} + {% for temp_paths_out in _ci_kustomize_extras_paths.results | default([]) -%} + {% if (temp_paths_out.item is string) -%} + {% set _ = paths.append(([temp_paths_out.path, temp_paths_out.item] | path_join)) -%} + {% else -%} + {% set _ = paths.append(temp_paths_out.path) -%} + {% endif -%} + {% endfor -%} + {{ paths }} + ci_kustomize: + target_path: >- + {{ + ( + ([_ci_kustomize_temporal_dir.path, kustomize_tc_target_manifest_path] | path_join) + ) if (kustomize_tc_target_manifest_path is defined) + else + ( + _ci_kustomize_temporal_dir.path + if (kustomize_tc_target_dir_paths is defined) else omit + ) + }} + output_path: >- + {{ + ( + ([_ci_kustomize_temporal_dir.path, kustomize_tc_output_path] | path_join) + ) if (kustomize_tc_output_path is defined) else omit + }} + kustomizations: "{{ kustomize_tc_kustomizations | default(omit) }}" + kustomizations_paths: >- + {{ + _kustomizations_paths + if (kustomize_tc_kustomization_paths is not defined and _kustomizations_paths | length > 0) + else ( + kustomize_tc_kustomization_paths if (kustomize_tc_kustomization_paths is defined) + else omit + ) + }} + kustomization_files_goes_first: "{{ kustomize_tc_kustomization_files_goes_first | default(omit) }}" + preserve_workspace: "{{ kustomize_tc_preserve_workspace | default(omit) }}" + sort_ascending: "{{ kustomize_tc_sort_ascending | default(omit) }}" + register: _ci_kustomize_last_output + ignore_errors: true + loop: "{{ range(1, (3 if kustomize_tc_idempotence | default(false) else 2)) | list }}" + loop_control: + label: "Run {{ kustomize_tc_run_num }}" + loop_var: kustomize_tc_run_num + +- name: "[{{ kustomize_tc_name }}] Assert idempotence" # noqa: name[template] + when: "kustomize_tc_idempotence | default(false) | bool" + ansible.builtin.assert: + that: + - "_ci_kustomize_last_output.results is defined" + - "_ci_kustomize_last_output.results | length == 2" + - "(_ci_kustomize_last_output.results | last).changed is defined" + - "not (_ci_kustomize_last_output.results | last).changed" + quiet: true + +- name: "[{{ kustomize_tc_name }}] Verify ci_kustomize call" # noqa: name[template] + vars: + output: >- + {{ + _ci_kustomize_last_output.results | first + if _ci_kustomize_last_output.results is defined + else _ci_kustomize_last_output + }} + should_fail: "{{ kustomize_tc_should_fail | default(omit) }}" + should_change: "{{ kustomize_tc_should_change | default(omit) }}" + _ci_kustomize_tc_out_expected_file_name: >- + {{ + kustomize_tc_output_path + if (kustomize_tc_output_path is defined) + else ( + kustomize_tc_target_manifest_path + if (kustomize_tc_target_manifest_path is defined) + else 'cifmw-kustomization-result.yaml' + ) + }} + _ci_kustomize_tc_out_temp_path: >- + {{ + [ + _ci_kustomize_temporal_dir.path, + _ci_kustomize_tc_out_expected_file_name + ] | path_join + }} + ansible.builtin.include_tasks: run_test_case_validate.yml + +- name: "[{{ kustomize_tc_name }}] Delete temporal dirs" # noqa: name[template] + ansible.builtin.file: + path: "{{ item }}" + state: absent + loop: >- + {{ + [ _ci_kustomize_temporal_dir.path ] + + ( + _ci_kustomize_extras_paths.results | default([]) | map(attribute='path') + ) + }} diff --git a/ci_framework/tests/integration/targets/kustomize/tasks/run_test_case_validate.yml b/ci_framework/tests/integration/targets/kustomize/tasks/run_test_case_validate.yml new file mode 100644 index 0000000000..dde00b2762 --- /dev/null +++ b/ci_framework/tests/integration/targets/kustomize/tasks/run_test_case_validate.yml @@ -0,0 +1,56 @@ +--- +- name: "[{{ kustomize_tc_name }}] Assert that failed is present in the command output" # noqa: name[template] + ansible.builtin.assert: + that: "'failed' in output" + quiet: true + +- name: "[{{ kustomize_tc_name }}] Assert that the expected variables are present if success" # noqa: name[template] + when: "not output.failed" + ansible.builtin.assert: + that: + - "'count' in output" + - "output['count'] is number" + - "'result' in output" + # Check that is a list: Should be iterable and cannot be map or string + - "output['result'] is not mapping" + - "output['result'] is not string" + - "output['result'] is iterable" + - "'kustomizations_paths' in output" + - >- + " + output['kustomizations_paths'] is iterable and + ( + output['kustomizations_paths'] is not string and + output['kustomizations_paths'] is not mapping + ) + " + - "'changed' in output" + quiet: true + +- name: "[{{ kustomize_tc_name }}] Assert that the expected variables are present if failed" # noqa: name[template] + when: "output.failed" + ansible.builtin.assert: + that: + - "'error' in output" + - "'changed' in output" + quiet: true + +- name: "[{{ kustomize_tc_name }}] Print run output in case of success" # noqa: name[template] + when: "not output.failed" + ansible.builtin.debug: + var: _ci_kustomize_last_output + +- name: "[{{ kustomize_tc_name }}] Assert that the kustomization result failed/succeeded and changed base on param" # noqa: name[template] + ansible.builtin.assert: + that: + - "output['failed'] == should_fail" + - "output['changed'] == should_change" + quiet: true + +- name: "[{{ kustomize_tc_name }}] Validations for succeeded runs" # noqa: name[template] + when: "not output.failed" + ansible.builtin.include_tasks: run_test_case_validate_success.yml + +- name: "[{{ kustomize_tc_name }}] Validations for failed runs" # noqa: name[template] + when: "output.failed" + ansible.builtin.include_tasks: run_test_case_validate_failure.yml diff --git a/ci_framework/tests/integration/targets/kustomize/tasks/run_test_case_validate_failure.yml b/ci_framework/tests/integration/targets/kustomize/tasks/run_test_case_validate_failure.yml new file mode 100644 index 0000000000..491bb40a35 --- /dev/null +++ b/ci_framework/tests/integration/targets/kustomize/tasks/run_test_case_validate_failure.yml @@ -0,0 +1,61 @@ +--- +- name: "[{{ kustomize_tc_name }}] Assert that the error field is present and its content is the expected one" # noqa: name[template] + ansible.builtin.assert: + that: + - "'error' in output" + - "(kustomize_tc_should_contain_error | lower) in (output['error'] | lower)" + quiet: true + +- name: "[{{ kustomize_tc_name }}] Assert that the details field is present, if expected, and its content is the expected one" # noqa: name[template] + when: kustomize_tc_should_contain_details is defined + ansible.builtin.assert: + that: + - "'details' in output" + - "(kustomize_tc_should_contain_details | lower) in (output['details'] | lower)" + quiet: true + +- name: "[{{ kustomize_tc_name }}] Assert that an argument validation error is properly formated if expected" # noqa: name[template] + when: kustomize_tc_should_contain_invalid_argument is defined + ansible.builtin.assert: + that: + - "'argument' in output" + - "output['argument'] == kustomize_tc_should_contain_invalid_argument" + quiet: true + +- name: "[{{ kustomize_tc_name }}] Assert that the error kustomization fields are present, if expected, and its content is the expected one" # noqa: name[template] + when: kustomize_tc_should_contain_invalid_kustomization_run | default(false) + block: + - name: "[{{ kustomize_tc_name }}] Assert that the expected fields that points and contains the kustomization are present" # noqa: name[template] + ansible.builtin.assert: + that: + - "'kustomization' in output" + - "'kustomization_path' in output" + quiet: true + + - name: "[{{ kustomize_tc_name }}] Fetch kustomization pointed by kustomization_path" # noqa: name[template] + ansible.builtin.stat: + path: "{{ output['kustomization_path'] }}" + register: _ci_kustomize_path_failed_stat + + - name: "[{{ kustomize_tc_name }}] Assert that the path exists" # noqa: name[template] + ansible.builtin.assert: + that: "_ci_kustomize_path_failed_stat.stat.exists | bool" + quiet: true + + - name: "[{{ kustomize_tc_name }}] Fetch the kustomization_path file content" # noqa: name[template] + ansible.builtin.slurp: + src: "{{ output['kustomization_path'] }}" + register: _ci_kustomize_path_failed_slurp + + - name: "[{{ kustomize_tc_name }}] Assert that the fetched content is the same as the provided by the plugin exists" # noqa: name[template] + ansible.builtin.assert: + that: "(_ci_kustomize_path_failed_slurp['content'] | b64decode | from_yaml) == output['kustomization']" + quiet: true + +- name: "[{{ kustomize_tc_name }}] Assert that the kustomization content is the expected one in case of validation failure" # noqa: name[template] + when: kustomize_tc_should_contain_validation_error_content is defined + ansible.builtin.assert: + that: + - "'kustomization' in output" + - "kustomize_tc_should_contain_validation_error_content == output['kustomization']" + quiet: true diff --git a/ci_framework/tests/integration/targets/kustomize/tasks/run_test_case_validate_success.yml b/ci_framework/tests/integration/targets/kustomize/tasks/run_test_case_validate_success.yml new file mode 100644 index 0000000000..5eea35951d --- /dev/null +++ b/ci_framework/tests/integration/targets/kustomize/tasks/run_test_case_validate_success.yml @@ -0,0 +1,95 @@ +--- +- name: "[{{ kustomize_tc_name }}] Assert the expected labels and values are in place" # noqa: name[template] + when: "kustomize_tc_should_have_labels is defined" + ansible.builtin.assert: + that: + - "item.not_found is not defined" + - "item.value == item.expected" + quiet: true + loop: >- + {# This jinja2 makes the tedious task of fetching from the kustomization output, that's a list, -#} + {# all the labels and their values that are selected by the kustomize_tc_should_have_labels list -#} + {# That list tells this jinja2 script from what manifest validate the given labels by matching -#} + {# them by apiVersion, kind and name. -#} + {# The result is a list of a ternary with the label name, it's value and the expected value, that can be -#} + {# easy used in the taks assertion itself by comparing the expected to the actual value. -#} + {% set expected = [] -%} + {% for label_req in kustomize_tc_should_have_labels | default([]) -%} + {% set assert_data = None -%} + {% for manifest in output.result | default([]) -%} + {% if (manifest.apiVersion == label_req.apiVersion) and (manifest.kind == label_req.kind) and (manifest.metadata.name == label_req.name) -%} + {% set manifest_labels = ((manifest.metadata | default({'labels': {}})).labels | default({})) -%} + {% for req_label_kv in (label_req.labels | default({})) | dict2items -%} + {% set assert_data = {'label': req_label_kv.key, 'value': manifest_labels[req_label_kv.key] | default(''), 'expected': req_label_kv.value} -%} + {% set _ = expected.append(assert_data) -%} + {% endfor -%} + {% endif -%} + {% endfor -%} + {% if assert_data is not none -%} + {% set _ = expected.append({'not_found': label_req, 'value':'', 'expected': ''}) -%} + {% endif -%} + {% endfor -%} + {{ expected }} + +- name: "[{{ kustomize_tc_name }}] Assert that the expected number of kustomization has been applied" # noqa: name[template] + when: "kustomize_tc_should_apply_kustomizations_count is defined" + ansible.builtin.assert: + that: "output['count'] == (kustomize_tc_should_apply_kustomizations_count | int)" + quiet: true + +- name: "[{{ kustomize_tc_name }}] Assert that the expected kustomization files have been discovered" # noqa: name[template] + vars: + kustomizations_paths: >- + {{ + (_ci_kustomize_extra_kustomizations.keys() | list) + + (_ci_kustomize_kustomizations.keys() | list) + }} + ansible.builtin.assert: + that: "(output['kustomizations_paths'] | sort) == (kustomizations_paths | sort)" # noqa: name[template] + quiet: true + +- name: "[{{ kustomize_tc_name }}] Assert the output points to an output path" # noqa: name[template] + ansible.builtin.assert: + that: "'output_path' in output" + quiet: true + +- name: "[{{ kustomize_tc_name }}] Assert the output points to the proper output path if necessary" # noqa: name[template] + when: "_ci_kustomize_tc_out_temp_path | default('') | length > 0" + ansible.builtin.assert: + that: "output['output_path'] == _ci_kustomize_tc_out_temp_path" + quiet: true + +- name: "[{{ kustomize_tc_name }}] Fetch output file stat" # noqa: name[template] + ansible.builtin.stat: + path: "{{ output['output_path'] }}" + register: _ci_kustomize_output_path_stat + +- name: "[{{ kustomize_tc_name }}] Assert the expected output file exists" # noqa: name[template] + ansible.builtin.assert: + that: "_ci_kustomize_output_path_stat.stat.exists | bool" + quiet: true + +- name: "[{{ kustomize_tc_name }}] Fetch the output file content" # noqa: name[template] + ansible.builtin.slurp: + src: "{{ output['output_path'] }}" + register: _ci_kustomize_output_path_slurp + +- name: "[{{ kustomize_tc_name }}] Assert the file content is the same as the output in result" # noqa: name[template] + ansible.builtin.assert: + that: "(_ci_kustomize_output_path_slurp['content'] | b64decode | from_yaml_all) == output['result']" + quiet: true + +- name: "[{{ kustomize_tc_name }}] Check if the workspace dir is present" # noqa: name[template] + when: + - "not output.failed" + - "kustomize_tc_preserve_workspace | default(false) | bool" + block: + - name: "[{{ kustomize_tc_name }}] Check if the workspace dir is present" # noqa: name[template] + ansible.builtin.stat: + path: "{{ [_ci_kustomize_temporal_dir.path, 'cifmw-kustomize-workspace'] | path_join }}" + register: _ci_kustomize_workspace_dir_stat + + - name: "[{{ kustomize_tc_name }}] Assert workspace dir is presence" # noqa: name[template] + ansible.builtin.assert: + that: "_ci_kustomize_workspace_dir_stat.stat.exists == kustomize_tc_preserve_workspace" + quiet: true diff --git a/ci_framework/tests/integration/targets/kustomize/tasks/scenarios/ci_kustomize_extras_scenario.yml b/ci_framework/tests/integration/targets/kustomize/tasks/scenarios/ci_kustomize_extras_scenario.yml new file mode 100644 index 0000000000..e058a20009 --- /dev/null +++ b/ci_framework/tests/integration/targets/kustomize/tasks/scenarios/ci_kustomize_extras_scenario.yml @@ -0,0 +1,121 @@ +--- +- name: Apply a single string based kustomization using kustomize instead of oc + vars: + # Define the kustomization parameters + kustomize_tc_extra_path: "{{ ci_kustomize_kustomize_bin_path }}" + kustomize_tc_name: "tc-success-extras-bare-kustomize-bin" + kustomize_tc_target_manifest_path: "testing-deployment.yaml" + kustomize_tc_kustomizations: |- + apiVersion: kustomize.config.k8s.io/v1beta1 + kind: Kustomization + patches: + - patch: |- + - op: replace + path: /metadata/labels/cifmw-label-1 + value: "cifmw-label-1-value" + target: + kind: Deployment + # Define assertions + kustomize_tc_should_fail: false + kustomize_tc_should_change: true + kustomize_tc_should_have_labels: + - apiVersion: apps/v1 + kind: Deployment + name: testing-deployment + labels: + cifmw-label-1: "cifmw-label-1-value" + kustomize_tc_should_apply_kustomizations_count: 1 + ansible.builtin.include_tasks: run_test_case.yml + +- name: Apply colliding kustomizations with reversed priority + vars: + # Define the kustomization parameters + kustomize_tc_extra_path: "{{ ci_kustomize_oc_bin_path }}" + kustomize_tc_name: "tc-success-extras-reversed-priority" + kustomize_tc_target_manifest_path: "testing-deployment.yaml" + kustomize_tc_kustomization_files_goes_first: false + kustomize_tc_kustomizations: |- + apiVersion: kustomize.config.k8s.io/v1beta1 + kind: Kustomization + patches: + - patch: |- + - op: replace + path: /metadata/labels/cifmw-label-1 + value: "cifmw-label-1-value-from-string" + target: + kind: Deployment + # Define assertions + kustomize_tc_should_fail: false + kustomize_tc_should_change: true + kustomize_tc_should_have_labels: + - apiVersion: apps/v1 + kind: Deployment + name: testing-deployment + labels: + cifmw-label-1: "cifmw-label-1-value" + kustomize_tc_should_apply_kustomizations_count: 2 + # Define kustomization that needs to be copied to the TC temporal folder: + kustomize_tc_copy_kustomization_files: + - "kustomization.yaml" + ansible.builtin.include_tasks: run_test_case.yml + +- name: Apply kustomization overriding the output file + vars: + # Define the kustomization parameters + kustomize_tc_extra_path: "{{ ci_kustomize_oc_bin_path }}" + kustomize_tc_name: "tc-success-extras-override-output-file" + kustomize_tc_target_manifest_path: "testing-deployment.yaml" + kustomize_tc_output_path: "kustomization-result.yaml" + kustomize_tc_kustomizations: |- + apiVersion: kustomize.config.k8s.io/v1beta1 + kind: Kustomization + patches: + - patch: |- + - op: replace + path: /metadata/labels/cifmw-label-1 + value: "cifmw-label-1-value" + target: + kind: Deployment + # Define assertions + kustomize_tc_should_fail: false + kustomize_tc_should_change: true + kustomize_tc_should_have_labels: + - apiVersion: apps/v1 + kind: Deployment + name: testing-deployment + labels: + cifmw-label-1: "cifmw-label-1-value" + kustomize_tc_should_apply_kustomizations_count: 2 + # Define kustomization that needs to be copied to the TC temporal folder: + kustomize_tc_copy_kustomization_files: + - "kustomization.yaml" + ansible.builtin.include_tasks: run_test_case.yml + +- name: Preserve the workspace folder after a successful kustomization + vars: + # Define the kustomization parameters + kustomize_tc_extra_path: "{{ ci_kustomize_kustomize_bin_path }}" + kustomize_tc_name: "tc-success-extras-preserve-workspace-dir" + kustomize_tc_target_manifest_path: "testing-deployment.yaml" + kustomize_tc_kustomizations: |- + apiVersion: kustomize.config.k8s.io/v1beta1 + kind: Kustomization + patches: + - patch: |- + - op: replace + path: /metadata/labels/cifmw-label-1 + value: "cifmw-label-1-value" + target: + kind: Deployment + kustomize_tc_preserve_workspace: true + # Define assertions + kustomize_tc_should_fail: false + kustomize_tc_should_change: true + kustomize_tc_should_have_labels: + - apiVersion: apps/v1 + kind: Deployment + name: testing-deployment + labels: + cifmw-label-1: "cifmw-label-1-value" + kustomize_tc_should_apply_kustomizations_count: 1 + ansible.builtin.include_tasks: run_test_case.yml diff --git a/ci_framework/tests/integration/targets/kustomize/tasks/scenarios/ci_kustomize_failures_scenario.yml b/ci_framework/tests/integration/targets/kustomize/tasks/scenarios/ci_kustomize_failures_scenario.yml new file mode 100644 index 0000000000..68522d78fd --- /dev/null +++ b/ci_framework/tests/integration/targets/kustomize/tasks/scenarios/ci_kustomize_failures_scenario.yml @@ -0,0 +1,194 @@ +--- +- name: Apply an invalid kustomization that should fail when applied by kustomize + vars: + kustomize_tc_extra_path: "{{ ci_kustomize_oc_bin_path }}" + # Define the kustomization parameters + kustomize_tc_name: "tc-failure-invalid-kustomization-operation" + kustomize_tc_target_manifest_path: "testing-deployment.yaml" + kustomize_tc_kustomizations: |- + apiVersion: kustomize.config.k8s.io/v1beta1 + kind: Kustomization + patches: + - patch: |- + - op: nonexistingop + path: /metadata/labels/cifmw-label-2 + value: "cifmw-label-2-value" + target: + kind: Deployment + # Define assertions + kustomize_tc_should_fail: true + kustomize_tc_should_change: false + kustomize_tc_should_contain_error: "Unexpected kind: nonexistingop" + kustomize_tc_should_contain_details: "Unexpected kind" + kustomize_tc_should_contain_invalid_kustomization_run: true + ansible.builtin.include_tasks: run_test_case.yml + +- name: Apply a yaml that has no apiVersion + vars: + # Define the kustomization parameters + kustomize_tc_name: "tc-failure-invalid-kustomization-yaml-string" + kustomize_tc_target_manifest_path: "testing-deployment.yaml" + kustomize_tc_kustomizations: |- + apiVersion: kustomize.config.k8s.io/v1beta1 + kind: Kustomization + patches: + patch: |-sss + - op: replace + path: /metadata/labels/cifmw-label-2 + value: "cifmw-label-2-value" + target: + kind: Deployment + # Define assertions + kustomize_tc_should_fail: true + kustomize_tc_should_change: false + kustomize_tc_should_contain_error: "YAML Error" + kustomize_tc_should_contain_validation_error_content: "{{ kustomize_tc_kustomizations }}" + ansible.builtin.include_tasks: run_test_case.yml + +- name: Apply a yaml that has no apiVersion + vars: + # Define the kustomization parameters + kustomize_tc_name: "tc-failure-invalid-api-version" + kustomize_tc_target_manifest_path: "testing-deployment.yaml" + kustomize_tc_kustomizations: + kind: Kustomization + patches: + - patch: |- + - op: add + path: /metadata/labels/cifmw-label-1 + value: "cifmw-label-1-value" + target: + kind: Deployment + # Define assertions + kustomize_tc_should_fail: true + kustomize_tc_should_change: false + kustomize_tc_should_contain_error: "manifest without apiVersion" + kustomize_tc_should_contain_validation_error_content: "{{ kustomize_tc_kustomizations | to_nice_yaml(indent=2) }}" + ansible.builtin.include_tasks: run_test_case.yml + +- name: Apply a yaml that has an apiVersion that is not the kustomization one + vars: + # Define the kustomization parameters + kustomize_tc_name: "tc-failure-invalid-api-version" + kustomize_tc_target_manifest_path: "testing-deployment.yaml" + kustomize_tc_kustomizations: + apiVersion: apps/v1 + kind: Kustomization + patches: + - patch: |- + - op: add + path: /metadata/labels/cifmw-label-1 + value: "cifmw-label-1-value" + target: + kind: Deployment + # Define assertions + kustomize_tc_should_fail: true + kustomize_tc_should_change: false + kustomize_tc_should_contain_error: "that is not kustomize.config.k8s.io" + kustomize_tc_should_contain_validation_error_content: "{{ kustomize_tc_kustomizations | to_nice_yaml(indent=2) }}" + ansible.builtin.include_tasks: run_test_case.yml + +- name: Apply a yaml that has no kind + vars: + # Define the kustomization parameters + kustomize_tc_name: "tc-failure-no-kind-field" + kustomize_tc_target_manifest_path: "testing-deployment.yaml" + kustomize_tc_kustomizations: + apiVersion: kustomize.config.k8s.io/v1beta1 + patches: + - patch: |- + - op: add + path: /metadata/labels/cifmw-label-1 + value: "cifmw-label-1-value" + target: + kind: Deployment + # Define assertions + kustomize_tc_should_fail: true + kustomize_tc_should_change: false + kustomize_tc_should_contain_error: "manifest without kind" + kustomize_tc_should_contain_validation_error_content: "{{ kustomize_tc_kustomizations | to_nice_yaml(indent=2) }}" + ansible.builtin.include_tasks: run_test_case.yml + +- name: Apply a yaml that has a kind that is not kustomization + vars: + # Define the kustomization parameters + kustomize_tc_name: "tc-failure-kind-not-kustomization" + kustomize_tc_target_manifest_path: "testing-deployment.yaml" + kustomize_tc_kustomizations: + apiVersion: kustomize.config.k8s.io/v1beta1 + kind: Deployment + patches: + - patch: |- + - op: add + path: /metadata/labels/cifmw-label-1 + value: "cifmw-label-1-value" + target: + kind: Deployment + # Define assertions + kustomize_tc_should_fail: true + kustomize_tc_should_change: false + kustomize_tc_should_contain_error: "is not Kustomization" + kustomize_tc_should_contain_validation_error_content: "{{ kustomize_tc_kustomizations | to_nice_yaml(indent=2) }}" + ansible.builtin.include_tasks: run_test_case.yml + +- name: Assert that the target_path is mandatory + vars: + # Define the kustomization parameters + kustomize_tc_name: "tc-failure-target-path-mandatory" + # Define assertions + kustomize_tc_should_fail: true + kustomize_tc_should_change: false + kustomize_tc_should_contain_error: "is mandatory" + kustomize_tc_should_contain_invalid_argument: "target_path" + ansible.builtin.include_tasks: run_test_case.yml + +- name: Assert that the target_path exists + vars: + # Define the kustomization parameters + kustomize_tc_name: "tc-failure-target-path-exists" + kustomize_tc_target_manifest_path: "this-file-does-not-exist.yaml" + kustomize_tc_skip_non_existing_sources: true + # Define assertions + kustomize_tc_should_fail: true + kustomize_tc_should_change: false + kustomize_tc_should_contain_error: "path does not exist" + kustomize_tc_should_contain_invalid_argument: "target_path" + ansible.builtin.include_tasks: run_test_case.yml + +- name: Assert that if output_path given is not an existing dir + vars: + # Define the kustomization parameters + kustomize_tc_name: "tc-failure-output-file-not-dir" + kustomize_tc_target_manifest_path: "testing-deployment.yaml" + kustomize_tc_output_path: "{{ ansible_env.HOME }}" + # Define assertions + kustomize_tc_should_fail: true + kustomize_tc_should_change: false + kustomize_tc_should_contain_error: "output file cannot point to a directory" + kustomize_tc_should_contain_invalid_argument: "output_path" + ansible.builtin.include_tasks: run_test_case.yml + +- name: Assert that if no kustomization tool given is a formatted error raises + vars: + # Define the kustomization parameters + kustomize_tc_set_path: "/tmp/non-existing" + kustomize_tc_name: "tc-failure-no-kustomization-tool" + kustomize_tc_target_manifest_path: "testing-deployment.yaml" + # Define assertions + kustomize_tc_should_fail: true + kustomize_tc_should_change: false + kustomize_tc_should_contain_error: "Cannot find oc nor kustomize" + ansible.builtin.include_tasks: run_test_case.yml + +- name: Assert that kustomizations_paths has the proper format + vars: + # Define the kustomization parameters + kustomize_tc_name: "tc-failure-kustomizations-paths-format" + kustomize_tc_target_manifest_path: "testing-deployment.yaml" + kustomize_tc_kustomization_paths: "abcde1234" + # Define assertions + kustomize_tc_should_fail: true + kustomize_tc_should_change: false + kustomize_tc_should_contain_error: "list of paths" + kustomize_tc_should_contain_invalid_argument: "kustomizations_paths" + ansible.builtin.include_tasks: run_test_case.yml diff --git a/ci_framework/tests/integration/targets/kustomize/tasks/scenarios/ci_kustomize_file_input_scenario.yml b/ci_framework/tests/integration/targets/kustomize/tasks/scenarios/ci_kustomize_file_input_scenario.yml new file mode 100644 index 0000000000..6f310fb264 --- /dev/null +++ b/ci_framework/tests/integration/targets/kustomize/tasks/scenarios/ci_kustomize_file_input_scenario.yml @@ -0,0 +1,192 @@ +--- +- name: Apply the file based kustomizations to the CR + vars: + # Define the kustomization parameters + kustomize_tc_extra_path: "{{ ci_kustomize_oc_bin_path }}" + kustomize_tc_name: "tc-success-files-single-file-kustomization" + kustomize_tc_target_manifest_path: "testing-deployment.yaml" + # Define assertions + kustomize_tc_should_fail: false + kustomize_tc_should_change: true + kustomize_tc_should_have_labels: + - apiVersion: apps/v1 + kind: Deployment + name: testing-deployment + labels: + cifmw-label-1: "cifmw-label-1-value" + cifmw-label-2: "cifmw-label-2-value" + cifmw-label-3: "cifmw-label-3-value" + kustomize_tc_should_apply_kustomizations_count: 3 + # Define kustomization that needs to be copied to the TC temporal folder: + kustomize_tc_copy_kustomization_files: + - "kustomization.yml" + - "kustomization.yaml" + - "single-kustomization-file-1.yaml" + ansible.builtin.include_tasks: run_test_case.yml + +- name: Apply the file based kustomizations to a couple of CRs + vars: + kustomize_tc_idempotence: true + # Define the kustomization parameters + kustomize_tc_extra_path: "{{ ci_kustomize_oc_bin_path }}" + kustomize_tc_name: "tc-success-files-dir-target-kustomization" + kustomize_tc_target_dir_paths: + - "testing-deployment.yaml" + - "testing-cm.yml" + # Define assertions + kustomize_tc_should_fail: false + kustomize_tc_should_change: true + kustomize_tc_should_have_labels: + - apiVersion: apps/v1 + kind: Deployment + name: testing-deployment + labels: + cifmw-label-1: "cifmw-label-1-value" + cifmw-label-4: "cifmw-label-4-value" + cifmw-label-5: "cifmw-label-5-value" + - apiVersion: v1 + kind: ConfigMap + name: testing-cm + labels: + cifmw-label-1: "cifmw-label-1-value" + kustomize_tc_should_apply_kustomizations_count: 5 + # Define kustomization that needs to be copied to the TC temporal folder: + kustomize_tc_copy_kustomization_files: + - "kustomization.yml" + - "kustomization.yaml" + - "multiple-kustomizations-in-one-file.yml" + - "cm-kustomization-1.yml" + ansible.builtin.include_tasks: run_test_case.yml + +- name: Apply the files based kustomizations (with extras) to the CR + vars: + # Define the kustomization parameters + kustomize_tc_extra_path: "{{ ci_kustomize_oc_bin_path }}" + kustomize_tc_name: "tc-success-files-single-file-extras-kustomization" + kustomize_tc_target_manifest_path: "testing-deployment.yaml" + # Define assertions + kustomize_tc_should_fail: false + kustomize_tc_should_change: true + kustomize_tc_should_have_labels: + - apiVersion: apps/v1 + kind: Deployment + name: testing-deployment + labels: + cifmw-label-1: "cifmw-label-1-value" + cifmw-label-2: "cifmw-label-2-value" + cifmw-label-3: "cifmw-label-3-value" + cifmw-label-4: "cifmw-label-4-value" + cifmw-label-5: "cifmw-label-5-value" + cifmw-label-6: "cifmw-label-6-value" + kustomize_tc_should_apply_kustomizations_count: 6 + # Define kustomization that needs to be copied to the TC temporal folder: + kustomize_tc_copy_kustomization_files: + - "kustomization.yml" + - "kustomization.yaml" + - "single-kustomization-file-1.yaml" + kustomize_tc_copy_kustomization_paths: + - + - "single-kustomization-file-2.yaml" + - + - "multiple-kustomizations-in-one-file.yml" + ansible.builtin.include_tasks: run_test_case.yml + +- name: Apply the file based kustomizations to a couple of CRs (with extras) + vars: + kustomize_tc_idempotence: true + # Define the kustomization parameters + kustomize_tc_extra_path: "{{ ci_kustomize_oc_bin_path }}" + kustomize_tc_name: "tc-success-files-dir-target-extras-kustomization" + kustomize_tc_target_dir_paths: + - "testing-deployment.yaml" + - "testing-cm.yml" + # Define assertions + kustomize_tc_should_fail: false + kustomize_tc_should_change: true + kustomize_tc_should_have_labels: + - apiVersion: apps/v1 + kind: Deployment + name: testing-deployment + labels: + cifmw-label-1: "cifmw-label-1-value" + cifmw-label-4: "cifmw-label-4-value" + cifmw-label-5: "cifmw-label-5-value" + - apiVersion: v1 + kind: ConfigMap + name: testing-cm + labels: + cifmw-label-1: "cifmw-label-1-value" + cifmw-label-2: "cifmw-label-2-value" + kustomize_tc_should_apply_kustomizations_count: 6 + # Define kustomization that needs to be copied to the TC temporal folder: + kustomize_tc_copy_kustomization_files: + - "kustomization.yml" + - "kustomization.yaml" + - "cm-kustomization-1.yml" + kustomize_tc_copy_kustomization_paths: + # Non-list of list paths points ci_kustomize directly to a kustomization, + # otherwise, the path to the dir containing the kustomizations is passed + - "cm-kustomization-2.yml" + - + - "multiple-kustomizations-in-one-file.yml" + ansible.builtin.include_tasks: run_test_case.yml + +- name: Apply overlapping file based kustomizations to a CR (ascending) + vars: + kustomize_tc_idempotence: true + # Define the kustomization parameters + kustomize_tc_extra_path: "{{ ci_kustomize_oc_bin_path }}" + kustomize_tc_name: "tc-success-files-dir-sorting-asc-kustomization" + kustomize_tc_sort_ascending: true + kustomize_tc_target_manifest_path: "testing-deployment.yaml" + # Define assertions + kustomize_tc_should_fail: false + kustomize_tc_should_change: true + kustomize_tc_should_have_labels: + - apiVersion: apps/v1 + kind: Deployment + name: testing-deployment + labels: + cifmw-label-1: "cifmw-label-1-value-override-b" + cifmw-label-2: "cifmw-label-2-value-override-c" + cifmw-label-3: "cifmw-label-3-value-override-c" + kustomize_tc_should_apply_kustomizations_count: 3 + # Define kustomization that needs to be copied to the TC temporal folder: + kustomize_tc_copy_kustomization_files: + - "b-sorting-kustomization.yml" + kustomize_tc_copy_kustomization_paths: + - + - "c-sorting-kustomization.yml" + - + - "a-sorting-kustomization.yml" + ansible.builtin.include_tasks: run_test_case.yml + +- name: Apply overlapping file based kustomizations to a CR (descending) + vars: + kustomize_tc_idempotence: true + # Define the kustomization parameters + kustomize_tc_extra_path: "{{ ci_kustomize_oc_bin_path }}" + kustomize_tc_name: "tc-success-files-dir-sorting-dsc-kustomization" + kustomize_tc_sort_ascending: false + kustomize_tc_target_manifest_path: "testing-deployment.yaml" + # Define assertions + kustomize_tc_should_fail: false + kustomize_tc_should_change: true + kustomize_tc_should_have_labels: + - apiVersion: apps/v1 + kind: Deployment + name: testing-deployment + labels: + cifmw-label-1: "cifmw-label-1-value" + cifmw-label-2: "cifmw-label-2-value" + cifmw-label-3: "cifmw-label-3-value" + kustomize_tc_should_apply_kustomizations_count: 3 + # Define kustomization that needs to be copied to the TC temporal folder: + kustomize_tc_copy_kustomization_files: + - "a-sorting-kustomization.yml" + kustomize_tc_copy_kustomization_paths: + - + - "c-sorting-kustomization.yml" + - + - "b-sorting-kustomization.yml" + ansible.builtin.include_tasks: run_test_case.yml diff --git a/ci_framework/tests/integration/targets/kustomize/tasks/scenarios/ci_kustomize_mixed_input_scenario.yml b/ci_framework/tests/integration/targets/kustomize/tasks/scenarios/ci_kustomize_mixed_input_scenario.yml new file mode 100644 index 0000000000..84c72c1f1d --- /dev/null +++ b/ci_framework/tests/integration/targets/kustomize/tasks/scenarios/ci_kustomize_mixed_input_scenario.yml @@ -0,0 +1,166 @@ +--- +- name: Apply the file and variables kustomizations to the CR + vars: + # Define the kustomization parameters + kustomize_tc_extra_path: "{{ ci_kustomize_oc_bin_path }}" + kustomize_tc_name: "tc-success-mixed-single-file-kustomizations" + kustomize_tc_target_manifest_path: "testing-deployment.yaml" + kustomize_tc_kustomizations: + - apiVersion: kustomize.config.k8s.io/v1beta1 + kind: Kustomization + patches: + - patch: |- + - op: replace + path: /metadata/labels/cifmw-label-7 + value: "cifmw-label-7-value" + target: + kind: Deployment + - |- + --- + apiVersion: kustomize.config.k8s.io/v1beta1 + kind: Kustomization + patches: + - patch: |- + - op: add + path: /metadata/labels/cifmw-label-8 + value: "cifmw-label-8-value" + target: + kind: Deployment + --- + apiVersion: kustomize.config.k8s.io/v1beta1 + kind: Kustomization + patches: + - patch: |- + - op: add + path: /metadata/labels/cifmw-label-9 + value: "cifmw-label-9-value" + target: + kind: Deployment + # Define assertions + kustomize_tc_should_fail: false + kustomize_tc_should_change: true + kustomize_tc_should_have_labels: + - apiVersion: apps/v1 + kind: Deployment + name: testing-deployment + labels: + cifmw-label-1: "cifmw-label-1-value" + cifmw-label-2: "cifmw-label-2-value" + cifmw-label-3: "cifmw-label-3-value" + cifmw-label-4: "cifmw-label-4-value" + cifmw-label-5: "cifmw-label-5-value" + cifmw-label-6: "cifmw-label-6-value" + cifmw-label-7: "cifmw-label-7-value" + cifmw-label-8: "cifmw-label-8-value" + cifmw-label-9: "cifmw-label-9-value" + kustomize_tc_should_apply_kustomizations_count: 9 + # Define kustomization that needs to be copied to the TC temporal folder: + kustomize_tc_copy_kustomization_files: + - "kustomization.yml" + - "kustomization.yaml" + - "single-kustomization-file-1.yaml" + kustomize_tc_copy_kustomization_paths: + - + - "single-kustomization-file-2.yaml" + - + - "multiple-kustomizations-in-one-file.yml" + ansible.builtin.include_tasks: run_test_case.yml + +- name: Apply the file and variables kustomizations to multiple CRs + vars: + kustomize_tc_idempotence: true + # Define the kustomization parameters + kustomize_tc_extra_path: "{{ ci_kustomize_oc_bin_path }}" + kustomize_tc_name: "tc-success-mixed-dir-target-kustomizations" + kustomize_tc_target_dir_paths: + - "testing-deployment.yaml" + - "testing-cm.yml" + - "testing-cm.yml" + kustomize_tc_kustomizations: + - apiVersion: kustomize.config.k8s.io/v1beta1 + kind: Kustomization + patches: + - patch: |- + - op: replace + path: /metadata/labels/cifmw-label-7 + value: "cifmw-label-7-value" + target: + kind: Deployment + - apiVersion: kustomize.config.k8s.io/v1beta1 + kind: Kustomization + patches: + - patch: |- + - op: replace + path: /metadata/labels/cifmw-label-4 + value: "cifmw-label-4-value" + target: + kind: ConfigMap + resources: + - testing-cm.yml + - |- + --- + apiVersion: kustomize.config.k8s.io/v1beta1 + kind: Kustomization + patches: + - patch: |- + - op: add + path: /metadata/labels/cifmw-label-8 + value: "cifmw-label-8-value" + target: + kind: Deployment + - patch: |- + - op: add + path: /metadata/labels/cifmw-label-3 + value: "cifmw-label-3-value" + target: + kind: ConfigMap + --- + apiVersion: kustomize.config.k8s.io/v1beta1 + kind: Kustomization + patches: + - patch: |- + - op: add + path: /metadata/labels/cifmw-label-9 + value: "cifmw-label-9-value" + target: + kind: Deployment + # Define assertions + kustomize_tc_should_fail: false + kustomize_tc_should_change: true + kustomize_tc_should_have_labels: + - apiVersion: apps/v1 + kind: Deployment + name: testing-deployment + labels: + cifmw-label-1: "cifmw-label-1-value" + cifmw-label-2: "cifmw-label-2-value" + cifmw-label-3: "cifmw-label-3-value" + cifmw-label-4: "cifmw-label-4-value" + cifmw-label-5: "cifmw-label-5-value" + cifmw-label-6: "cifmw-label-6-value" + cifmw-label-7: "cifmw-label-7-value" + cifmw-label-8: "cifmw-label-8-value" + cifmw-label-9: "cifmw-label-9-value" + - apiVersion: v1 + kind: ConfigMap + name: testing-cm + labels: + cifmw-label-1: "cifmw-label-1-value" + cifmw-label-2: "cifmw-label-2-value" + cifmw-label-3: "cifmw-label-3-value" + kustomize_tc_should_apply_kustomizations_count: 12 + # Define kustomization that needs to be copied to the TC temporal folder: + kustomize_tc_copy_kustomization_files: + - "kustomization.yml" + - "kustomization.yaml" + - "single-kustomization-file-1.yaml" + kustomize_tc_copy_kustomization_paths: + - + - "cm-kustomization-2.yml" + - "single-kustomization-file-2.yaml" + # Non-list of list paths points ci_kustomize directly to a kustomization, + # otherwise, the path to the dir containing the kustomizations is passed + - "multiple-kustomizations-in-one-file.yml" + - + - "cm-kustomization-1.yml" + ansible.builtin.include_tasks: run_test_case.yml diff --git a/ci_framework/tests/integration/targets/kustomize/tasks/scenarios/ci_kustomize_vars_input_scenario.yml b/ci_framework/tests/integration/targets/kustomize/tasks/scenarios/ci_kustomize_vars_input_scenario.yml new file mode 100644 index 0000000000..f604414816 --- /dev/null +++ b/ci_framework/tests/integration/targets/kustomize/tasks/scenarios/ci_kustomize_vars_input_scenario.yml @@ -0,0 +1,239 @@ +--- +- name: Apply a single string based kustomization + vars: + kustomize_tc_idempotence: true + # Define the kustomization parameters + kustomize_tc_extra_path: "{{ ci_kustomize_oc_bin_path }}" + kustomize_tc_name: "tc-success-single-string-kustomization" + kustomize_tc_target_manifest_path: "testing-deployment.yaml" + kustomize_tc_kustomizations: |- + apiVersion: kustomize.config.k8s.io/v1beta1 + kind: Kustomization + patches: + - patch: |- + - op: replace + path: /metadata/labels/cifmw-label-1 + value: "cifmw-label-1-value" + target: + kind: Deployment + # Define assertions + kustomize_tc_should_fail: false + kustomize_tc_should_change: true + kustomize_tc_should_have_labels: + - apiVersion: apps/v1 + kind: Deployment + name: testing-deployment + labels: + cifmw-label-1: "cifmw-label-1-value" + kustomize_tc_should_apply_kustomizations_count: 1 + ansible.builtin.include_tasks: run_test_case.yml + +- name: Apply two different kustomizations on a single string targeting a directory + vars: + # Define the kustomization parameters + kustomize_tc_extra_path: "{{ ci_kustomize_oc_bin_path }}" + kustomize_tc_name: "tc-success-two-string-dir-kustomizations" + kustomize_tc_target_dir_paths: + - "testing-deployment.yaml" + - "testing-cm.yml" + kustomize_tc_kustomizations: |- + apiVersion: kustomize.config.k8s.io/v1beta1 + kind: Kustomization + patches: + - patch: |- + - op: replace + path: /metadata/labels/cifmw-label-1 + value: "cifmw-label-1-value" + target: + kind: Deployment + --- + apiVersion: kustomize.config.k8s.io/v1beta1 + kind: Kustomization + patches: + - patch: |- + - op: replace + path: /metadata/labels/cifmw-label-1 + value: "cifmw-label-1-value" + target: + kind: ConfigMap + # Define assertions + kustomize_tc_should_fail: false + kustomize_tc_should_change: true + kustomize_tc_should_have_labels: + - apiVersion: apps/v1 + kind: Deployment + name: testing-deployment + labels: + cifmw-label-1: "cifmw-label-1-value" + - apiVersion: v1 + kind: ConfigMap + name: testing-cm + labels: + cifmw-label-1: "cifmw-label-1-value" + kustomize_tc_should_apply_kustomizations_count: 2 + ansible.builtin.include_tasks: run_test_case.yml + +- name: Apply two different kustomizations on a single string targeting a combined file + vars: + # Define the kustomization parameters + kustomize_tc_extra_path: "{{ ci_kustomize_oc_bin_path }}" + kustomize_tc_name: "tc-success-two-string-combined-file-kustomizations" + kustomize_tc_target_manifest_path: "testing-combined-manifests.yml" + kustomize_tc_kustomizations: |- + apiVersion: kustomize.config.k8s.io/v1beta1 + kind: Kustomization + patches: + - patch: |- + - op: replace + path: /metadata/labels/cifmw-label-1 + value: "cifmw-label-1-value" + target: + kind: Job + --- + apiVersion: kustomize.config.k8s.io/v1beta1 + kind: Kustomization + patches: + - patch: |- + - op: replace + path: /metadata/labels/cifmw-label-1 + value: "cifmw-label-1-value" + target: + kind: Secret + # Define assertions + kustomize_tc_should_fail: false + kustomize_tc_should_change: true + kustomize_tc_should_have_labels: + - apiVersion: batch/v1 + kind: Jobs + name: testing-job + labels: + cifmw-label-1: "cifmw-label-1-value" + - apiVersion: v1 + kind: Secret + name: testing-secret + labels: + cifmw-label-1: "cifmw-label-1-value" + kustomize_tc_should_apply_kustomizations_count: 2 + ansible.builtin.include_tasks: run_test_case.yml + +- name: Apply a string with multiple string kustomizations + vars: + # Define the kustomization parameters + kustomize_tc_extra_path: "{{ ci_kustomize_oc_bin_path }}" + kustomize_tc_name: "tc-success-two-string-kustomizations" + kustomize_tc_target_manifest_path: "testing-deployment.yaml" + kustomize_tc_kustomizations: |- + --- + apiVersion: kustomize.config.k8s.io/v1beta1 + kind: Kustomization + patches: + - patch: |- + - op: replace + path: /metadata/labels/cifmw-label-1 + value: "cifmw-label-1-value" + target: + kind: Deployment + --- + apiVersion: kustomize.config.k8s.io/v1beta1 + kind: Kustomization + patches: + - patch: |- + - op: replace + path: /metadata/labels/cifmw-label-2 + value: "cifmw-label-2-value" + target: + kind: Deployment + # Define assertions + kustomize_tc_should_fail: false + kustomize_tc_should_change: true + kustomize_tc_should_have_labels: + - apiVersion: apps/v1 + kind: Deployment + name: testing-deployment + labels: + cifmw-label-1: "cifmw-label-1-value" + cifmw-label-2: "cifmw-label-2-value" + kustomize_tc_should_apply_kustomizations_count: 2 + ansible.builtin.include_tasks: run_test_case.yml + +- name: Apply a single dict based kustomization + vars: + # Define the kustomization parameters + kustomize_tc_extra_path: "{{ ci_kustomize_oc_bin_path }}" + kustomize_tc_name: "tc-success-single-dict-kustomization" + kustomize_tc_target_manifest_path: "testing-deployment.yaml" + kustomize_tc_kustomizations: + apiVersion: kustomize.config.k8s.io/v1beta1 + kind: Kustomization + patches: + - patch: |- + - op: add + path: /metadata/labels/cifmw-label-1 + value: "cifmw-label-1-value" + target: + kind: Deployment + # Define assertions + kustomize_tc_should_fail: false + kustomize_tc_should_change: true + kustomize_tc_should_have_labels: + - apiVersion: apps/v1 + kind: Deployment + name: testing-deployment + labels: + cifmw-label-1: "cifmw-label-1-value" + kustomize_tc_should_apply_kustomizations_count: 1 + ansible.builtin.include_tasks: run_test_case.yml + +- name: Apply a couple of mixed string/dict kustomizations + vars: + kustomize_tc_idempotence: true + # Define the kustomization parameters + kustomize_tc_extra_path: "{{ ci_kustomize_oc_bin_path }}" + kustomize_tc_name: "tc-success-mixed-string-dict-kustomizations" + kustomize_tc_target_manifest_path: "testing-deployment.yaml" + kustomize_tc_kustomizations: + - apiVersion: kustomize.config.k8s.io/v1beta1 + kind: Kustomization + patches: + - patch: |- + - op: replace + path: /metadata/labels/cifmw-label-1 + value: "cifmw-label-1-value" + target: + kind: Deployment + - |- + --- + apiVersion: kustomize.config.k8s.io/v1beta1 + kind: Kustomization + patches: + - patch: |- + - op: add + path: /metadata/labels/cifmw-label-2 + value: "cifmw-label-2-value" + target: + kind: Deployment + --- + apiVersion: kustomize.config.k8s.io/v1beta1 + kind: Kustomization + patches: + - patch: |- + - op: add + path: /metadata/labels/cifmw-label-3 + value: "cifmw-label-3-value" + target: + kind: Deployment + resources: + - testing-deployment.yaml + # Define assertions + kustomize_tc_should_fail: false + kustomize_tc_should_change: true + kustomize_tc_should_have_labels: + - apiVersion: apps/v1 + kind: Deployment + name: testing-deployment + labels: + cifmw-label-1: "cifmw-label-1-value" + cifmw-label-2: "cifmw-label-2-value" + cifmw-label-3: "cifmw-label-3-value" + kustomize_tc_should_apply_kustomizations_count: 3 + ansible.builtin.include_tasks: run_test_case.yml