From 2a9d894c905ae2cfc1de44ea867b8df76261051a Mon Sep 17 00:00:00 2001 From: Mike Graves Date: Mon, 8 Nov 2021 08:09:45 -0500 Subject: [PATCH 01/24] Add new AnsibleK8SModule class (#269) * Add new AnsibleK8SModule class This class is intended to replace part of the K8SAnsibleMixin class and is part of a larger refactoring effort. * Fix sanity errors * Fix unit tests * Add mock to test requirements --- plugins/module_utils/k8s/core.py | 130 +++++++++++++++++++++++++++ test-requirements.txt | 2 + tests/sanity/refresh_ignore_files | 18 ++-- tests/unit/conftest.py | 44 +++++++++ tests/unit/module_utils/test_core.py | 91 +++++++++++++++++++ 5 files changed, 280 insertions(+), 5 deletions(-) create mode 100644 plugins/module_utils/k8s/core.py create mode 100644 tests/unit/conftest.py create mode 100644 tests/unit/module_utils/test_core.py diff --git a/plugins/module_utils/k8s/core.py b/plugins/module_utils/k8s/core.py new file mode 100644 index 00000000..2cff9563 --- /dev/null +++ b/plugins/module_utils/k8s/core.py @@ -0,0 +1,130 @@ +from distutils.version import LooseVersion +from typing import Optional + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.basic import missing_required_lib + + +class AnsibleK8SModule: + """A base module class for K8S modules. + + This class should be used instead of directly using AnsibleModule. If there + is a need for other methods or attributes to be proxied, they can be added + here. + """ + + default_settings = { + "check_k8s": True, + "module_class": AnsibleModule, + } + + def __init__(self, **kwargs) -> None: + local_settings = {} + for key in AnsibleK8SModule.default_settings: + try: + local_settings[key] = kwargs.pop(key) + except KeyError: + local_settings[key] = AnsibleK8SModule.default_settings[key] + self.settings = local_settings + + self._module = self.settings["module_class"](**kwargs) + + if self.settings["check_k8s"]: + self.requires("kubernetes") + self.has_at_least("kubernetes", "12.0.0", warn=True) + + @property + def check_mode(self): + return self._module.check_mode + + @property + def _diff(self): + return self._module._diff + + @property + def _name(self): + return self._module._name + + @property + def params(self): + return self._module.params + + def warn(self, *args, **kwargs): + return self._module.warn(*args, **kwargs) + + def deprecate(self, *args, **kwargs): + return self._module.deprecate(*args, **kwargs) + + def debug(self, *args, **kwargs): + return self._module.debug(*args, **kwargs) + + def exit_json(self, *args, **kwargs): + return self._module.exit_json(*args, **kwargs) + + def fail_json(self, *args, **kwargs): + return self._module.fail_json(*args, **kwargs) + + def _gather_versions(self) -> dict: + versions = {} + try: + import jsonpatch + + versions["jsonpatch"] = jsonpatch.__version__ + except ImportError: + pass + + try: + import kubernetes + + versions["kubernetes"] = kubernetes.__version__ + except ImportError: + pass + + try: + import yaml + + versions["pyyaml"] = yaml.__version__ + except ImportError: + pass + + return versions + + def has_at_least( + self, dependency: str, minimum: Optional[str] = None, warn: bool = False + ) -> bool: + """Check if a specific dependency is present at a minimum version. + + If a minimum version is not specified it will check only that the + dependency is present. Additionally, if ``warn`` is ``True``, a warning + will be emitted if the actual version is less than the specified + minimum version. + """ + dependencies = self._gather_versions() + current = dependencies.get(dependency) + if current is not None: + if minimum is None: + return True + supported = LooseVersion(current) >= LooseVersion(minimum) + if not supported and warn: + self.warn( + "{0}<{1} is not supported or tested. Some features may not work.".format( + dependency, minimum + ) + ) + return supported + return False + + def requires(self, dependency: str, minimum: Optional[str] = None) -> None: + """Fail if a specific dependency is not present at a minimum version. + + If a minimum version is not specified it will require only that the + dependency is present. This function calls ``fail_json()`` when the + dependency is not found at the required version and will stop module + execution. + """ + if not self.has_at_least(dependency, minimum): + if minimum is not None: + lib = "{0}>={1}".format(dependency, minimum) + else: + lib = dependency + self._module.fail_json(msg=missing_required_lib(lib)) diff --git a/test-requirements.txt b/test-requirements.txt index 880eec68..e20bfb9f 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,4 +1,6 @@ kubernetes-validate coverage==4.5.4 +mock pytest pytest-xdist +pytest-mock diff --git a/tests/sanity/refresh_ignore_files b/tests/sanity/refresh_ignore_files index ede60a83..6640baa5 100644 --- a/tests/sanity/refresh_ignore_files +++ b/tests/sanity/refresh_ignore_files @@ -9,28 +9,36 @@ module_dir = target_dir / "plugins" / "modules" module_utils_dir = target_dir / "plugins" / "module_utils" ignore_dir.mkdir(parents=True, exist_ok=True) -skip_list = [ +skip_list_2_6 = [ "compile-2.6!skip", # Py3.8+ + "import-2.6!skip", # Py3.8+ +] + +skip_list_3 = [ "compile-2.7!skip", # Py3.8+ "compile-3.5!skip", # Py3.8+ - "import-2.6!skip", # Py3.8+ "import-2.7!skip", # Py3.8+ "import-3.5!skip", # Py3.8+ "future-import-boilerplate!skip", # Py2 only "metaclass-boilerplate!skip", # Py2 only ] -for version in ["2.9", "2.10", "2.11", "2.12"]: +for version in ["2.9", "2.10", "2.11", "2.12", "2.13"]: ignore_file = ignore_dir / f"ignore-{version}.txt" ignore_content = ignore_file.read_text().split("\n") ignore_content.append(f"tests/sanity/refresh_ignore_files shebang!skip") - for f in module_dir.glob("*.py"): + if version == "2.13": + skip_list = skip_list_3 + else: + skip_list = skip_list_2_6 + skip_list_3 + + for f in module_dir.glob("**/*.py"): if f.is_symlink(): continue for test in skip_list: ignore_content.append(f"{f} {test}") - for f in module_utils_dir.glob("*.py"): + for f in module_utils_dir.glob("**/*.py"): if f.is_symlink(): continue for test in skip_list: diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py new file mode 100644 index 00000000..20615adb --- /dev/null +++ b/tests/unit/conftest.py @@ -0,0 +1,44 @@ +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import json +import sys +from io import BytesIO + +import pytest + +import ansible.module_utils.basic +from ansible.module_utils.six import string_types +from ansible.module_utils._text import to_bytes +from ansible.module_utils.common._collections_compat import MutableMapping + + +@pytest.fixture +def stdin(mocker, request): + old_args = ansible.module_utils.basic._ANSIBLE_ARGS + ansible.module_utils.basic._ANSIBLE_ARGS = None + old_argv = sys.argv + sys.argv = ["ansible_unittest"] + + if isinstance(request.param, string_types): + args = request.param + elif isinstance(request.param, MutableMapping): + if "ANSIBLE_MODULE_ARGS" not in request.param: + request.param = {"ANSIBLE_MODULE_ARGS": request.param} + if "_ansible_remote_tmp" not in request.param["ANSIBLE_MODULE_ARGS"]: + request.param["ANSIBLE_MODULE_ARGS"]["_ansible_remote_tmp"] = "/tmp" + if "_ansible_keep_remote_files" not in request.param["ANSIBLE_MODULE_ARGS"]: + request.param["ANSIBLE_MODULE_ARGS"]["_ansible_keep_remote_files"] = False + args = json.dumps(request.param) + else: + raise Exception("Malformed data to the stdin pytest fixture") + + fake_stdin = BytesIO(to_bytes(args, errors="surrogate_or_strict")) + mocker.patch("ansible.module_utils.basic.sys.stdin", mocker.MagicMock()) + mocker.patch("ansible.module_utils.basic.sys.stdin.buffer", fake_stdin) + + yield fake_stdin + + ansible.module_utils.basic._ANSIBLE_ARGS = old_args + sys.argv = old_argv diff --git a/tests/unit/module_utils/test_core.py b/tests/unit/module_utils/test_core.py new file mode 100644 index 00000000..189f1f95 --- /dev/null +++ b/tests/unit/module_utils/test_core.py @@ -0,0 +1,91 @@ +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import json + +import kubernetes +import pytest + +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.core import ( + AnsibleK8SModule, +) + +MINIMAL_K8S_VERSION = "12.0.0" +UNSUPPORTED_K8S_VERSION = "11.0.0" + + +@pytest.mark.parametrize("stdin", [{}], indirect=["stdin"]) +def test_no_warn(monkeypatch, stdin, capfd): + monkeypatch.setattr(kubernetes, "__version__", MINIMAL_K8S_VERSION) + + module = AnsibleK8SModule(argument_spec={}) + with pytest.raises(SystemExit): + module.exit_json() + out, err = capfd.readouterr() + + return_value = json.loads(out) + + assert return_value.get("exception") is None + assert return_value.get("warnings") is None + assert return_value.get("failed") is None + + +@pytest.mark.parametrize("stdin", [{}], indirect=["stdin"]) +def test_warn_on_k8s_version(monkeypatch, stdin, capfd): + monkeypatch.setattr(kubernetes, "__version__", UNSUPPORTED_K8S_VERSION) + + module = AnsibleK8SModule(argument_spec={}) + with pytest.raises(SystemExit): + module.exit_json() + out, err = capfd.readouterr() + + return_value = json.loads(out) + + assert return_value.get("warnings") is not None + warnings = return_value["warnings"] + assert len(warnings) == 1 + assert "kubernetes" in warnings[0] + assert MINIMAL_K8S_VERSION in warnings[0] + + +dependencies = [ + ["18.20.0", "12.0.1", False], + ["18.20.0", "18.20.0", True], + ["12.0.1", "18.20.0", True], +] + + +@pytest.mark.parametrize( + "stdin,desired,actual,result", [({}, *d) for d in dependencies], indirect=["stdin"] +) +def test_has_at_least(monkeypatch, stdin, desired, actual, result, capfd): + monkeypatch.setattr(kubernetes, "__version__", actual) + + module = AnsibleK8SModule(argument_spec={}) + + assert module.has_at_least("kubernetes", desired) is result + + +dependencies = [ + ["kubernetes", "18.20.0", "(kubernetes>=18.20.0)"], + ["foobar", "1.0.0", "(foobar>=1.0.0)"], + ["foobar", None, "(foobar)"], +] + + +@pytest.mark.parametrize( + "stdin,dependency,version,msg", [({}, *d) for d in dependencies], indirect=["stdin"] +) +def test_requires_fails_with_message( + monkeypatch, stdin, dependency, version, msg, capfd +): + monkeypatch.setattr(kubernetes, "__version__", "12.0.0") + module = AnsibleK8SModule(argument_spec={}) + with pytest.raises(SystemExit): + module.requires(dependency, version) + out, err = capfd.readouterr() + return_value = json.loads(out) + + assert return_value.get("failed") + assert msg in return_value.get("msg") From 42644ee26e7d18d8a913ee21cc81a1092c5ab309 Mon Sep 17 00:00:00 2001 From: Mike Graves Date: Tue, 16 Nov 2021 09:08:23 -0500 Subject: [PATCH 02/24] Add resource definition refactor (#278) Add resource definition refactor SUMMARY This refactors most of the logic around creating a list of functional resource definitions based on input parameters for the module. ISSUE TYPE COMPONENT NAME ADDITIONAL INFORMATION Reviewed-by: Alina Buzachis Reviewed-by: Abhijeet Kasurde Reviewed-by: Mike Graves Reviewed-by: None --- plugins/module_utils/k8s/resource.py | 129 ++++++++++++ .../module_utils/fixtures/definitions.yml | 11 ++ tests/unit/module_utils/test_resource.py | 187 ++++++++++++++++++ 3 files changed, 327 insertions(+) create mode 100644 plugins/module_utils/k8s/resource.py create mode 100644 tests/unit/module_utils/fixtures/definitions.yml create mode 100644 tests/unit/module_utils/test_resource.py diff --git a/plugins/module_utils/k8s/resource.py b/plugins/module_utils/k8s/resource.py new file mode 100644 index 00000000..69e83e86 --- /dev/null +++ b/plugins/module_utils/k8s/resource.py @@ -0,0 +1,129 @@ +# Copyright: (c) 2021, Red Hat | Ansible +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +import os +from typing import cast, Dict, Iterable, List, Optional, Union + +from ansible.module_utils.six import string_types + +try: + import yaml +except ImportError: + # Handled in module setup + pass + + +class ResourceDefinition(dict): + """Representation of a resource definition. + + This is a thin wrapper around a dictionary representation of a resource + definition, with a few properties defined for conveniently accessing the + commonly used fields. + """ + + @property + def kind(self) -> Optional[str]: + return self.get("kind") + + @property + def api_version(self) -> Optional[str]: + return self.get("apiVersion") + + @property + def namespace(self) -> Optional[str]: + metadata = self.get("metadata", {}) + return metadata.get("namespace") + + @property + def name(self) -> Optional[str]: + metadata = self.get("metadata", {}) + return metadata.get("name") + + +def create_definitions(params: Dict) -> List[ResourceDefinition]: + """Create a list of ResourceDefinitions from module inputs. + + This will take the module's inputs and return a list of ResourceDefintion + objects. The resource definitions returned by this function should be as + complete a definition as we can create based on the input. Any *List kinds + will be removed and replaced by the resources contained in it. + """ + if params.get("resource_definition"): + d = cast(Union[str, List, Dict], params.get("resource_definition")) + definitions = from_yaml(d) + elif params.get("src"): + d = cast(str, params.get("src")) + definitions = from_file(d) + else: + # We'll create an empty definition and let merge_params set values + # from the module parameters. + definitions = [{}] + + resource_definitions: List[Dict] = [] + for definition in definitions: + merge_params(definition, params) + kind = cast(Optional[str], definition.get("kind")) + if kind and kind.endswith("List"): + resource_definitions += flatten_list_kind(definition, params) + else: + resource_definitions.append(definition) + return list(map(ResourceDefinition, resource_definitions)) + + +def from_yaml(definition: Union[str, List, Dict]) -> Iterable[Dict]: + """Load resource definitions from a yaml definition.""" + definitions: List[Dict] = [] + if isinstance(definition, string_types): + definitions += yaml.safe_load_all(definition) + elif isinstance(definition, list): + for item in definition: + if isinstance(item, string_types): + definitions += yaml.safe_load_all(item) + else: + definitions.append(item) + else: + definition = cast(Dict, definition) + definitions.append(definition) + return filter(None, definitions) + + +def from_file(filepath: str) -> Iterable[Dict]: + """Load resource definitions from a path to a yaml file.""" + path = os.path.normpath(filepath) + with open(path, "r") as f: + definitions = list(yaml.safe_load_all(f)) + return filter(None, definitions) + + +def merge_params(definition: Dict, params: Dict) -> Dict: + """Merge module parameters with the resource definition. + + Fields in the resource definition take precedence over module parameters. + """ + definition.setdefault("kind", params.get("kind")) + definition.setdefault("apiVersion", params.get("api_version")) + metadata = definition.setdefault("metadata", {}) + # The following should only be set if we have values for them + if params.get("namespace"): + metadata.setdefault("namespace", params.get("namespace")) + if params.get("name"): + metadata.setdefault("name", params.get("name")) + if params.get("generate_name"): + metadata.setdefault("generateName", params.get("generate_name")) + return definition + + +def flatten_list_kind(definition: Dict, params: Dict) -> List[Dict]: + """Replace *List kind with the items it contains. + + This will take a definition for a *List resource and return a list of + definitions for the items contained within the List. + """ + items = [] + kind = cast(str, definition.get("kind"))[:-4] + api_version = definition.get("apiVersion") + for item in definition.get("items", []): + item.setdefault("kind", kind) + item.setdefault("apiVersion", api_version) + items.append(merge_params(item, params)) + return items diff --git a/tests/unit/module_utils/fixtures/definitions.yml b/tests/unit/module_utils/fixtures/definitions.yml new file mode 100644 index 00000000..d627bd5c --- /dev/null +++ b/tests/unit/module_utils/fixtures/definitions.yml @@ -0,0 +1,11 @@ +kind: Pod +apiVersion: v1 +metadata: + name: foo + namespace: bar +--- +kind: ConfigMap +apiVersion: v1 +metadata: + name: foo + namespace: bar diff --git a/tests/unit/module_utils/test_resource.py b/tests/unit/module_utils/test_resource.py new file mode 100644 index 00000000..8d7beda1 --- /dev/null +++ b/tests/unit/module_utils/test_resource.py @@ -0,0 +1,187 @@ +import os +from pathlib import Path + +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.resource import ( + create_definitions, + flatten_list_kind, + from_file, + from_yaml, + merge_params, +) + + +def test_create_definitions_loads_from_definition(): + params = { + "resource_definition": { + "kind": "Pod", + "apiVersion": "v1", + "metadata": {"name": "foo", "namespace": "bar"}, + } + } + results = create_definitions(params) + assert len(results) == 1 + assert results[0].kind == "Pod" + assert results[0].api_version == "v1" + assert results[0].name == "foo" + assert results[0].namespace == "bar" + + +def test_create_definitions_loads_from_file(): + current = Path(os.path.dirname(os.path.abspath(__file__))) + params = {"src": current / "fixtures/definitions.yml"} + results = create_definitions(params) + assert len(results) == 2 + assert results[0].kind == "Pod" + assert results[1].kind == "ConfigMap" + + +def test_create_definitions_loads_from_params(): + params = { + "kind": "Pod", + "api_version": "v1", + "name": "foo", + "namespace": "foobar", + } + results = create_definitions(params) + assert len(results) == 1 + assert results[0] == { + "kind": "Pod", + "apiVersion": "v1", + "metadata": {"name": "foo", "namespace": "foobar"}, + } + + +def test_create_definitions_loads_list_kind(): + params = { + "resource_definition": { + "kind": "PodList", + "apiVersion": "v1", + "items": [ + {"kind": "Pod", "metadata": {"name": "foo"}}, + {"kind": "Pod", "metadata": {"name": "bar"}}, + ], + } + } + results = create_definitions(params) + assert len(results) == 2 + assert results[0].name == "foo" + assert results[1].name == "bar" + + +def test_merge_params_does_not_overwrite(): + definition = { + "kind": "Pod", + "apiVersion": "v1", + "metadata": {"name": "foo", "namespace": "bar"}, + } + params = { + "kind": "Service", + "api_version": "v2", + "name": "baz", + "namespace": "gaz", + } + result = merge_params(definition, params) + assert result == definition + + +def test_merge_params_adds_module_params(): + params = { + "kind": "Pod", + "api_version": "v1", + "namespace": "bar", + "generate_name": "foo-", + } + result = merge_params({}, params) + assert result == { + "kind": "Pod", + "apiVersion": "v1", + "metadata": {"generateName": "foo-", "namespace": "bar"}, + } + + +def test_from_yaml_loads_string_docs(): + definition = """ +kind: Pod +apiVersion: v1 +metadata: + name: foo + namespace: bar +--- +kind: ConfigMap +apiVersion: v1 +metadata: + name: foo + namespace: bar +""" + result = list(from_yaml(definition)) + assert result[0]["kind"] == "Pod" + assert result[1]["kind"] == "ConfigMap" + + +def test_from_yaml_loads_list(): + definition = [ + """ + kind: Pod + apiVersion: v1 + metadata: + name: foo + namespace: bar + """, + """ + kind: ConfigMap + apiVersion: v1 + metadata: + name: foo + namespace: bar + """, + { + "kind": "Pod", + "apiVersion": "v1", + "metadata": {"name": "baz", "namespace": "bar"}, + }, + ] + result = list(from_yaml(definition)) + assert len(result) == 3 + assert result[0]["kind"] == "Pod" + assert result[1]["kind"] == "ConfigMap" + assert result[2]["metadata"]["name"] == "baz" + + +def test_from_yaml_loads_dictionary(): + definition = { + "kind": "Pod", + "apiVersion": "v1", + "metadata": {"name": "foo", "namespace": "bar"}, + } + result = list(from_yaml(definition)) + assert result[0]["kind"] == "Pod" + + +def test_from_file_loads_definitions(): + current = Path(os.path.dirname(os.path.abspath(__file__))) + result = list(from_file(current / "fixtures/definitions.yml")) + assert result[0]["kind"] == "Pod" + assert result[1]["kind"] == "ConfigMap" + + +def test_flatten_list_kind_flattens(): + definition = { + "kind": "PodList", + "apiVersion": "v1", + "items": [ + {"kind": "Pod", "metadata": {"name": "foo"}}, + {"kind": "Pod", "metadata": {"name": "bar"}}, + ], + } + result = flatten_list_kind(definition, {"namespace": "foobar"}) + assert len(result) == 2 + + assert result[0]["kind"] == "Pod" + assert result[0]["apiVersion"] == "v1" + assert result[0]["metadata"]["name"] == "foo" + assert result[0]["metadata"]["namespace"] == "foobar" + + assert result[1]["kind"] == "Pod" + assert result[1]["apiVersion"] == "v1" + assert result[1]["metadata"]["name"] == "bar" + assert result[1]["metadata"]["namespace"] == "foobar" From 7fb89a7b6fe1610b90cb514c3b6c48972761d8ba Mon Sep 17 00:00:00 2001 From: Alina Buzachis Date: Fri, 19 Nov 2021 17:59:43 +0100 Subject: [PATCH 03/24] Initial work K8S client class (#276) Initial work K8S client class SUMMARY Initial work on K8SClient Class. Reviewed-by: Mike Graves Reviewed-by: Alina Buzachis Reviewed-by: None --- plugins/module_utils/k8s/client.py | 253 +++++++++++++++++++++++++ tests/unit/module_utils/test_client.py | 159 ++++++++++++++++ 2 files changed, 412 insertions(+) create mode 100644 plugins/module_utils/k8s/client.py create mode 100644 tests/unit/module_utils/test_client.py diff --git a/plugins/module_utils/k8s/client.py b/plugins/module_utils/k8s/client.py new file mode 100644 index 00000000..055f1712 --- /dev/null +++ b/plugins/module_utils/k8s/client.py @@ -0,0 +1,253 @@ +# Copyright: (c) 2021, Red Hat | Ansible +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +import os +import hashlib +from distutils.version import LooseVersion +from typing import Any, Dict, List, Optional + +from ansible.module_utils.six import iteritems, string_types + +from ansible_collections.kubernetes.core.plugins.module_utils.args_common import ( + AUTH_ARG_MAP, + AUTH_ARG_SPEC, + AUTH_PROXY_HEADERS_SPEC, +) + +try: + from ansible_collections.kubernetes.core.plugins.module_utils import ( + k8sdynamicclient, + ) + from ansible_collections.kubernetes.core.plugins.module_utils.client.discovery import ( + LazyDiscoverer, + ) +except ImportError: + # Handled in module setup + pass + +try: + import kubernetes +except ImportError: + # Handled in module setup + pass + +try: + import urllib3 + + urllib3.disable_warnings() +except ImportError: + # Handled in module setup + pass + + +module = None +_pool = {} + + +def _requires_kubernetes_at_least(version: str): + if module: + module.requires("kubernetes", version) + else: + if LooseVersion(kubernetes.__version__) < LooseVersion(version): + raise Exception( + f"kubernetes >= {version} is required to use in-memory kubeconfig." + ) + + +def _create_auth_spec(module=None, **kwargs) -> Dict: + auth: Dict = {} + # If authorization variables aren't defined, look for them in environment variables + for true_name, arg_name in AUTH_ARG_MAP.items(): + if module and module.params.get(arg_name) is not None: + auth[true_name] = module.params.get(arg_name) + elif arg_name in kwargs and kwargs.get(arg_name) is not None: + auth[true_name] = kwargs.get(arg_name) + elif arg_name == "proxy_headers": + # specific case for 'proxy_headers' which is a dictionary + proxy_headers = {} + for key in AUTH_PROXY_HEADERS_SPEC.keys(): + env_value = os.getenv( + "K8S_AUTH_PROXY_HEADERS_{0}".format(key.upper()), None + ) + if env_value is not None: + if AUTH_PROXY_HEADERS_SPEC[key].get("type") == "bool": + env_value = env_value.lower() not in ["0", "false", "no"] + proxy_headers[key] = env_value + if proxy_headers is not {}: + auth[true_name] = proxy_headers + else: + env_value = os.getenv( + "K8S_AUTH_{0}".format(arg_name.upper()), None + ) or os.getenv("K8S_AUTH_{0}".format(true_name.upper()), None) + if env_value is not None: + if AUTH_ARG_SPEC[arg_name].get("type") == "bool": + env_value = env_value.lower() not in ["0", "false", "no"] + auth[true_name] = env_value + + return auth + + +def _load_config(auth: Dict) -> None: + kubeconfig = auth.get("kubeconfig") + optional_arg = { + "context": auth.get("context"), + "persist_config": auth.get("persist_config"), + } + if kubeconfig: + if isinstance(kubeconfig, string_types): + kubernetes.config.load_kube_config(config_file=kubeconfig, **optional_arg) + elif isinstance(kubeconfig, dict): + _requires_kubernetes_at_least("17.17.0") + kubernetes.config.load_kube_config_from_dict( + config_dict=kubeconfig, **optional_arg + ) + else: + kubernetes.config.load_kube_config(config_file=None, **optional_arg) + + +def _create_configuration(auth: Dict): + def auth_set(*names: list) -> bool: + return all(auth.get(name) for name in names) + + if auth_set("host"): + # Removing trailing slashes if any from hostname + auth["host"] = auth.get("host").rstrip("/") + + if auth_set("username", "password", "host") or auth_set("api_key", "host"): + # We have enough in the parameters to authenticate, no need to load incluster or kubeconfig + pass + elif auth_set("kubeconfig") or auth_set("context"): + try: + _load_config(auth) + except Exception as err: + raise err + + else: + # First try to do incluster config, then kubeconfig + try: + kubernetes.config.load_incluster_config() + except kubernetes.config.ConfigException: + try: + _load_config(auth) + except Exception as err: + raise err + + # Override any values in the default configuration with Ansible parameters + # As of kubernetes-client v12.0.0, get_default_copy() is required here + try: + configuration = kubernetes.client.Configuration().get_default_copy() + except AttributeError: + configuration = kubernetes.client.Configuration() + + for key, value in iteritems(auth): + if key in AUTH_ARG_MAP.keys() and value is not None: + if key == "api_key": + setattr( + configuration, key, {"authorization": "Bearer {0}".format(value)} + ) + elif key == "proxy_headers": + headers = urllib3.util.make_headers(**value) + setattr(configuration, key, headers) + else: + setattr(configuration, key, value) + + return configuration + + +def _configuration_digest(configuration) -> str: + m = hashlib.sha256() + for k in AUTH_ARG_MAP: + if not hasattr(configuration, k): + v = None + else: + v = getattr(configuration, k) + if v and k in ["ssl_ca_cert", "cert_file", "key_file"]: + with open(str(v), "r") as fd: + content = fd.read() + m.update(content.encode()) + else: + m.update(str(v).encode()) + digest = m.hexdigest() + + return digest + + +def cache(func): + def wrapper(*args): + client = None + digest = _configuration_digest(*args) + if digest in _pool: + client = _pool[digest] + else: + client = func(*args) + _pool[digest] = client + + return client + + return wrapper + + +@cache +def create_api_client(configuration): + return k8sdynamicclient.K8SDynamicClient( + kubernetes.client.ApiClient(configuration), discoverer=LazyDiscoverer + ) + + +class K8SClient: + """A Client class for K8S modules. + + This class has the primary purpose to proxy the kubernetes client and resource objects. + If there is a need for other methods or attributes to be proxied, they can be added here. + """ + + def __init__(self, configuration, client, dry_run: bool = False) -> None: + self.configuration = configuration + self.client = client + self.dry_run = dry_run + + @property + def resources(self) -> List[Any]: + return self.client.resources + + def _ensure_dry_run(self, params: Dict) -> Dict: + if self.dry_run: + params["dry_run"] = True + return params + + def validate(self, resource, **params): + pass + + def get(self, resource, **params): + return resource.get(**params) + + def delete(self, resource, **params): + return resource.delete(**self._ensure_dry_run(params)) + + def apply(self, resource, definition, namespace, **params): + return resource.apply( + definition, namespace=namespace, **self._ensure_dry_run(params) + ) + + def create(self, resource, definition, **params): + return resource.create(definition, **self._ensure_dry_run(params)) + + def replace(self, resource, definition, **params): + return resource.replace(definition, **self._ensure_dry_run(params)) + + def patch(self, resource, definition, **params): + return resource.patch(definition, **self._ensure_dry_run(params)) + + +def get_api_client(module=None, **kwargs: Optional[Any]) -> K8SClient: + auth_spec = _create_auth_spec(module, **kwargs) + configuration = _create_configuration(auth_spec) + client = create_api_client(configuration) + + k8s_client = K8SClient( + configuration=configuration, + client=client, + dry_run=module.params.get("dry_run", False), + ) + + return k8s_client.client diff --git a/tests/unit/module_utils/test_client.py b/tests/unit/module_utils/test_client.py new file mode 100644 index 00000000..cab9c937 --- /dev/null +++ b/tests/unit/module_utils/test_client.py @@ -0,0 +1,159 @@ +import os +import base64 +import tempfile +import yaml +import mock +from mock import MagicMock + +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.client import ( + _create_auth_spec, + _create_configuration, +) + +TEST_HOST = "test-host" +TEST_SSL_HOST = "https://test-host" +TEST_CLIENT_CERT = "/dev/null" +TEST_CLIENT_KEY = "/dev/null" +TEST_CERTIFICATE_AUTH = "/dev/null" +TEST_DATA = "test-data" +TEST_BEARER_TOKEN = "Bearer %s" % base64.standard_b64encode(TEST_DATA.encode()).decode() +TEST_KUBE_CONFIG = { + "current-context": "federal-context", + "contexts": [ + { + "name": "simple_token", + "context": {"cluster": "default", "user": "simple_token"}, + } + ], + "clusters": [{"name": "default", "cluster": {"server": TEST_HOST}}], + "users": [ + { + "name": "ssl-no_file", + "user": { + "token": TEST_BEARER_TOKEN, + "client-certificate": TEST_CLIENT_CERT, + "client-key": TEST_CLIENT_KEY, + }, + } + ], +} + +_temp_files = [] + + +def _remove_temp_file(): + for f in _temp_files: + os.remove(f) + + +def _create_temp_file(content=""): + handler, name = tempfile.mkstemp() + _temp_files.append(name) + os.write(handler, str.encode(content)) + os.close(handler) + return name + + +def test_create_auth_spec_ssl_no_options(): + module = MagicMock() + module.params = {} + actual_auth_spec = _create_auth_spec(module) + + assert "proxy_headers" in actual_auth_spec + + +def test_create_auth_spec_ssl_options(): + ssl_options = { + "host": TEST_SSL_HOST, + "token": TEST_BEARER_TOKEN, + "client_cert": TEST_CLIENT_CERT, + "client_key": TEST_CLIENT_KEY, + "ca_cert": TEST_CERTIFICATE_AUTH, + "validate_certs": True, + } + expected_auth_spec = { + "host": TEST_SSL_HOST, + "cert_file": TEST_CLIENT_CERT, + "key_file": TEST_CLIENT_KEY, + "ssl_ca_cert": TEST_CERTIFICATE_AUTH, + "verify_ssl": True, + "proxy_headers": {}, + } + + module = MagicMock() + module.params = ssl_options + actual_auth_spec = _create_auth_spec(module) + + assert expected_auth_spec.items() <= actual_auth_spec.items() + + +def test_create_auth_spec_ssl_options_no_verify(): + ssl_options = { + "host": TEST_SSL_HOST, + "token": TEST_BEARER_TOKEN, + "client_cert": TEST_CLIENT_CERT, + "client_key": TEST_CLIENT_KEY, + "validate_certs": False, + } + + expected_auth_spec = { + "host": TEST_SSL_HOST, + "cert_file": TEST_CLIENT_CERT, + "key_file": TEST_CLIENT_KEY, + "verify_ssl": False, + "proxy_headers": {}, + } + + module = MagicMock() + module.params = ssl_options + actual_auth_spec = _create_auth_spec(module) + + assert expected_auth_spec.items() <= actual_auth_spec.items() + + +@mock.patch.dict(os.environ, {"K8S_AUTH_PROXY_HEADERS_PROXY_BASIC_AUTH": "foo:bar"}) +@mock.patch.dict(os.environ, {"K8S_AUTH_PROXY_HEADERS_USER_AGENT": "foo/1.0"}) +@mock.patch.dict(os.environ, {"K8S_AUTH_CERT_FILE": TEST_CLIENT_CERT}) +def test_create_auth_spec_ssl_proxy(): + expected_auth_spec = { + "kubeconfig": "~/.kube/customconfig", + "verify_ssl": True, + "cert_file": TEST_CLIENT_CERT, + "proxy_headers": {"proxy_basic_auth": "foo:bar", "user_agent": "foo/1.0"}, + } + module = MagicMock() + options = {"validate_certs": True, "kubeconfig": "~/.kube/customconfig"} + + module.params = options + actual_auth_spec = _create_auth_spec(module) + + assert expected_auth_spec.items() <= actual_auth_spec.items() + + +def test_load_kube_config_from_file_path(): + config_file = _create_temp_file(yaml.safe_dump(TEST_KUBE_CONFIG)) + auth = {"kubeconfig": config_file, "context": "simple_token"} + actual_configuration = _create_configuration(auth) + + expected_configuration = { + "host": TEST_HOST, + "kubeconfig": config_file, + "context": "simple_token", + } + + assert expected_configuration.items() <= actual_configuration.__dict__.items() + _remove_temp_file() + + +def test_load_kube_config_from_dict(): + auth_spec = {"kubeconfig": TEST_KUBE_CONFIG, "context": "simple_token"} + actual_configuration = _create_configuration(auth_spec) + + expected_configuration = { + "host": TEST_HOST, + "kubeconfig": TEST_KUBE_CONFIG, + "context": "simple_token", + } + + assert expected_configuration.items() <= actual_configuration.__dict__.items() + _remove_temp_file() From f168a3f67f4f2d4ffa5ae048d402f7b4e3098e49 Mon Sep 17 00:00:00 2001 From: Mike Graves Date: Mon, 13 Dec 2021 11:06:13 -0500 Subject: [PATCH 04/24] Add new waiter (#306) Add new waiter SUMMARY This refactors the waiter logic from common.py into a separate module. ISSUE TYPE COMPONENT NAME ADDITIONAL INFORMATION Reviewed-by: None Reviewed-by: Alina Buzachis Reviewed-by: None --- plugins/module_utils/k8s/waiter.py | 204 ++++++++++++++++++ .../module_utils/fixtures/definitions.yml | 35 ++- .../module_utils/fixtures/deployments.yml | 48 +++++ tests/unit/module_utils/fixtures/pods.yml | 63 ++++++ tests/unit/module_utils/test_resource.py | 10 +- tests/unit/module_utils/test_waiter.py | 114 ++++++++++ 6 files changed, 463 insertions(+), 11 deletions(-) create mode 100644 plugins/module_utils/k8s/waiter.py create mode 100644 tests/unit/module_utils/fixtures/deployments.yml create mode 100644 tests/unit/module_utils/fixtures/pods.yml create mode 100644 tests/unit/module_utils/test_waiter.py diff --git a/plugins/module_utils/k8s/waiter.py b/plugins/module_utils/k8s/waiter.py new file mode 100644 index 00000000..c0cc41a6 --- /dev/null +++ b/plugins/module_utils/k8s/waiter.py @@ -0,0 +1,204 @@ +import time +from functools import partial +from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Union + +from ansible.module_utils.parsing.convert_bool import boolean + +try: + from kubernetes.dynamic.exceptions import NotFoundError + from kubernetes.dynamic.resource import Resource, ResourceField, ResourceInstance +except ImportError: + # These are defined only for the sake of Ansible's checked import requirement + Resource = Any # type: ignore + ResourceInstance = Any # type: ignore + pass + + +def deployment_ready(deployment: ResourceInstance) -> bool: + # FIXME: frustratingly bool(deployment.status) is True even if status is empty + # Furthermore deployment.status.availableReplicas == deployment.status.replicas == None if status is empty + # deployment.status.replicas is None is perfectly ok if desired replicas == 0 + # Scaling up means that we also need to check that we're not in a + # situation where status.replicas == status.availableReplicas + # but spec.replicas != status.replicas + return bool( + deployment.status + and deployment.spec.replicas == (deployment.status.replicas or 0) + and deployment.status.availableReplicas == deployment.status.replicas + and deployment.status.observedGeneration == deployment.metadata.generation + and not deployment.status.unavailableReplicas + ) + + +def pod_ready(pod: ResourceInstance) -> bool: + return bool( + pod.status + and pod.status.containerStatuses is not None + and all(container.ready for container in pod.status.containerStatuses) + ) + + +def daemonset_ready(daemonset: ResourceInstance) -> bool: + return bool( + daemonset.status + and daemonset.status.desiredNumberScheduled is not None + and daemonset.status.updatedNumberScheduled + == daemonset.status.desiredNumberScheduled + and daemonset.status.numberReady == daemonset.status.desiredNumberScheduled + and daemonset.status.observedGeneration == daemonset.metadata.generation + and not daemonset.status.unavailableReplicas + ) + + +def statefulset_ready(statefulset: ResourceInstance) -> bool: + return bool( + statefulset.status + and statefulset.spec.updateStrategy.type == "RollingUpdate" + and statefulset.status.observedGeneration + == (statefulset.metadata.generation or 0) + and statefulset.status.updateRevision == statefulset.status.currentRevision + and statefulset.status.updatedReplicas == statefulset.spec.replicas + and statefulset.status.readyReplicas == statefulset.spec.replicas + and statefulset.status.replicas == statefulset.spec.replicas + ) + + +def custom_condition(condition: Dict, resource: ResourceInstance) -> bool: + if not resource.status or not resource.status.conditions: + return False + matches = [x for x in resource.status.conditions if x.type == condition["type"]] + if not matches: + return False + # There should never be more than one condition of a specific type + match: ResourceField = matches[0] + if match.status == "Unknown": + if match.status == condition["status"]: + if "reason" not in condition: + return True + if condition["reason"]: + return match.reason == condition["reason"] + return False + status = True if match.status == "True" else False + if status == boolean(condition["status"], strict=False): + if condition.get("reason"): + return match.reason == condition["reason"] + return True + return False + + +def resource_absent(resource: ResourceInstance) -> bool: + return not exists(resource) + + +def exists(resource: Optional[ResourceInstance]) -> bool: + """Simple predicate to check for existence of a resource. + + While a List type resource technically always exists, this will only return + true if the List contains items.""" + return bool(resource) and not empty_list(resource) + + +RESOURCE_PREDICATES = { + "DaemonSet": daemonset_ready, + "Deployment": deployment_ready, + "Pod": pod_ready, + "StatefulSet": statefulset_ready, +} + + +def empty_list(resource: ResourceInstance) -> bool: + return resource.kind.endswith("List") and not resource.get("items") + + +def clock(total: int, interval: int) -> Iterator[int]: + start = time.monotonic() + yield 0 + while (time.monotonic() - start) < total: + time.sleep(interval) + yield int(time.monotonic() - start) + + +class Waiter: + def __init__( + self, client, resource: Resource, predicate: Callable[[ResourceInstance], bool] + ): + self.client = client + self.resource = resource + self.predicate = predicate + + def wait( + self, + definition: Dict, + timeout: int, + sleep: int, + label_selectors: Optional[List[str]] = None, + ) -> Tuple[bool, Optional[Dict], int]: + params = { + "name": definition["metadata"].get("name"), + "namespace": definition["metadata"].get("namespace"), + } + if label_selectors: + params["label_selector"] = ",".join(label_selectors) + instance: Optional[Dict] = None + response = None + elapsed = 0 + for i in clock(timeout, sleep): + elapsed = i + try: + response = self.client.get(self.resource, **params) + except NotFoundError: + pass + if self.predicate(response): + break + if response: + instance = response.to_dict() + return self.predicate(response), instance, elapsed + + +class DummyWaiter: + """A no-op waiter that simply returns the item being waited on. + + No API call will be made with this waiter; the function returns + immediately. This waiter is useful for waiting on resource instances in + check mode, for example. + """ + + def wait( + self, + definition: Dict, + timeout: int, + sleep: int, + label_selectors: Optional[List[str]] = None, + ) -> Tuple[bool, Optional[Dict], int]: + return True, definition, 0 + + +# The better solution would be typing.Protocol, but this is only in 3.8+ +SupportsWait = Union[Waiter, DummyWaiter] + + +def get_waiter( + client, + resource: Resource, + state: str = "present", + condition: Optional[Dict] = None, + check_mode: Optional[bool] = False, +) -> SupportsWait: + """Create a Waiter object based on the specified resource. + + This is a convenience method for creating a waiter from a resource. + Based on the arguments and the kind of resource, an appropriate waiter + will be returned. A waiter can also be created directly, of course. + """ + if check_mode: + return DummyWaiter() + if state == "present": + if condition: + predicate: Callable[[ResourceInstance], bool] = partial( + custom_condition, condition + ) + else: + predicate = RESOURCE_PREDICATES.get(resource.kind, exists) + else: + predicate = resource_absent + return Waiter(client, resource, predicate) diff --git a/tests/unit/module_utils/fixtures/definitions.yml b/tests/unit/module_utils/fixtures/definitions.yml index d627bd5c..a8f6de81 100644 --- a/tests/unit/module_utils/fixtures/definitions.yml +++ b/tests/unit/module_utils/fixtures/definitions.yml @@ -1,11 +1,34 @@ +--- +kind: Namespace +apiVersion: v1 +metadata: + name: test-1 +--- kind: Pod apiVersion: v1 metadata: - name: foo - namespace: bar + name: pod-1 + namespace: test-1 +spec: + containers: + - image: busybox + name: busybox --- -kind: ConfigMap +kind: PodList apiVersion: v1 -metadata: - name: foo - namespace: bar +metadata: {} +items: + - kind: Pod + apiVersion: v1 + metadata: + name: pod-1 + namespace: test-1 + spec: + containers: + - image: busybox + name: busybox +--- +kind: ConfigMapList +apiVersion: v1 +metadata: {} +items: [] diff --git a/tests/unit/module_utils/fixtures/deployments.yml b/tests/unit/module_utils/fixtures/deployments.yml new file mode 100644 index 00000000..530035fc --- /dev/null +++ b/tests/unit/module_utils/fixtures/deployments.yml @@ -0,0 +1,48 @@ +--- +kind: Deployment +apiVersion: apps/v1 +metadata: + name: deploy-1 + namespace: test-1 + generation: 1 +spec: + replicas: 2 + selector: + matchLabels: + app: foo + template: + metadata: + labels: + app: foo + spec: + containers: + - image: busybox + name: busybox +status: + availableReplicas: 2 + replicas: 2 + observedGeneration: 1 +--- +kind: Deployment +apiVersion: apps/v1 +metadata: + name: deploy-2 + namespace: test-1 + generation: 1 +spec: + replicas: 2 + selector: + matchLabels: + app: foo + template: + metadata: + labels: + app: foo + spec: + containers: + - image: busybox + name: busybox +status: + availableReplicas: 1 + replicas: 2 + observedGeneration: 1 diff --git a/tests/unit/module_utils/fixtures/pods.yml b/tests/unit/module_utils/fixtures/pods.yml new file mode 100644 index 00000000..250354e2 --- /dev/null +++ b/tests/unit/module_utils/fixtures/pods.yml @@ -0,0 +1,63 @@ +--- +kind: Pod +apiVersion: v1 +metadata: + namespace: test-1 + name: pod-1 +spec: + containers: + - image: busybox + name: busybox +status: + containerStatuses: + - name: busybox + ready: true + conditions: + - type: "www.example.com/gate" + status: "True" +--- +kind: Pod +apiVersion: v1 +metadata: + namespace: test-1 + name: pod-2 +spec: + containers: + - image: busybox + name: busybox +--- +kind: Pod +apiVersion: v1 +metadata: + namespace: test-1 + name: pod-3 +spec: + containers: + - image: busybox + name: busybox +status: + phase: Pending + conditions: + - type: "www.example.com/gate" + status: "Unknown" + containerStatuses: + - name: busybox + ready: true +--- +kind: Pod +apiVersion: v1 +metadata: + namespace: test-1 + name: pod-4 +spec: + containers: + - image: busybox + name: busybox +status: + phase: Pending + conditions: + - type: "www.example.com/other" + status: "Unknown" + containerStatuses: + - name: busybox + ready: true diff --git a/tests/unit/module_utils/test_resource.py b/tests/unit/module_utils/test_resource.py index 8d7beda1..c27c01a8 100644 --- a/tests/unit/module_utils/test_resource.py +++ b/tests/unit/module_utils/test_resource.py @@ -30,9 +30,9 @@ def test_create_definitions_loads_from_file(): current = Path(os.path.dirname(os.path.abspath(__file__))) params = {"src": current / "fixtures/definitions.yml"} results = create_definitions(params) - assert len(results) == 2 - assert results[0].kind == "Pod" - assert results[1].kind == "ConfigMap" + assert len(results) == 3 + assert results[0].kind == "Namespace" + assert results[1].kind == "Pod" def test_create_definitions_loads_from_params(): @@ -160,8 +160,8 @@ def test_from_yaml_loads_dictionary(): def test_from_file_loads_definitions(): current = Path(os.path.dirname(os.path.abspath(__file__))) result = list(from_file(current / "fixtures/definitions.yml")) - assert result[0]["kind"] == "Pod" - assert result[1]["kind"] == "ConfigMap" + assert result[0]["kind"] == "Namespace" + assert result[1]["kind"] == "Pod" def test_flatten_list_kind_flattens(): diff --git a/tests/unit/module_utils/test_waiter.py b/tests/unit/module_utils/test_waiter.py new file mode 100644 index 00000000..5ed76743 --- /dev/null +++ b/tests/unit/module_utils/test_waiter.py @@ -0,0 +1,114 @@ +import os +import time +from pathlib import Path +from unittest.mock import Mock + +import pytest +import yaml +from kubernetes.dynamic.resource import ResourceInstance +from kubernetes.dynamic.exceptions import NotFoundError + +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.waiter import ( + clock, + custom_condition, + deployment_ready, + DummyWaiter, + exists, + get_waiter, + pod_ready, + resource_absent, + Waiter, +) + + +def resources(filepath): + current = Path(os.path.dirname(os.path.abspath(__file__))) + with open(current / filepath) as fp: + return [ResourceInstance(None, d) for d in yaml.safe_load_all(fp)] + + +RESOURCES = resources("fixtures/definitions.yml") +PODS = resources("fixtures/pods.yml") +DEPLOYMENTS = resources("fixtures/deployments.yml") + + +def test_clock_times_out(): + start = time.monotonic() + for x in clock(5, 1): + pass + elapsed = int(time.monotonic() - start) + assert x == 5 + assert 5 <= elapsed <= 6 + + +@pytest.mark.parametrize( + "resource,expected", + zip(RESOURCES + [None, {}], [True, True, True, False, False, False]), +) +def test_exists_and_absent_checks_for_existence(resource, expected): + assert exists(resource) is expected + assert resource_absent(resource) is not expected + + +@pytest.mark.parametrize("pod,expected", zip(PODS, [True, False, True, True])) +def test_pod_ready_checks_readiness(pod, expected): + assert pod_ready(pod) is expected + + +@pytest.mark.parametrize("pod,expected", zip(PODS, [True, False, False, False])) +def test_custom_condition_checks_readiness(pod, expected): + condition = {"type": "www.example.com/gate", "status": "True"} + assert custom_condition(condition, pod) is expected + + +@pytest.mark.parametrize("deployment,expected", zip(DEPLOYMENTS, [True, False])) +def test_deployment_ready_checks_readiness(deployment, expected): + assert deployment_ready(deployment) is expected + + +def test_dummywaiter_returns_resource_immediately(): + resource = { + "kind": "Pod", + "apiVersion": "v1", + "metadata": {"name": "foopod", "namespace": "foobar"}, + } + result, instance, elapsed = DummyWaiter().wait(resource, 10, 100) + assert result is True + assert instance == resource + assert elapsed == 0 + + +def test_waiter_waits_for_missing_resource(): + spec = {"get.side_effect": NotFoundError(Mock())} + client = Mock(**spec) + resource = Mock() + result, instance, elapsed = Waiter(client, resource, exists).wait( + RESOURCES[0], 3, 1 + ) + assert result is False + assert instance is None + assert abs(elapsed - 3) <= 1 + + +@pytest.mark.parametrize("resource,expected", zip(RESOURCES, [True, True, True, False])) +def test_waiter_waits_for_resource_to_exist(resource, expected): + result = resource.to_dict() + spec = {"get.side_effect": [NotFoundError(Mock()), resource, resource, resource]} + client = Mock(**spec) + success, instance, elapsed = Waiter(client, Mock(), exists).wait(result, 3, 1) + assert success is expected + assert instance == result + assert abs(elapsed - 2) <= 1 + + +def test_get_waiter_returns_correct_waiter(): + assert get_waiter(Mock(), PODS[0]).predicate == pod_ready + waiter = get_waiter(Mock(), PODS[0], check_mode=True) + assert isinstance(waiter, DummyWaiter) + assert get_waiter(Mock(), PODS[0], state="absent").predicate == resource_absent + assert ( + get_waiter( + Mock(), PODS[0], condition={"type": "Ready", "status": "True"} + ).predicate.func + == custom_condition + ) From e2f54d34311e8856593fe34330ee72b029a85b60 Mon Sep 17 00:00:00 2001 From: Alina Buzachis Date: Thu, 13 Jan 2022 16:08:47 +0100 Subject: [PATCH 05/24] K8sService class (#307) K8sService class SUMMARY This refactors the perform_action() logic from common.py into a separate K8sService class. TODO: Unit tests. ISSUE TYPE New Module Pull Request COMPONENT NAME service.py Reviewed-by: Abhijeet Kasurde Reviewed-by: Mike Graves Reviewed-by: Alina Buzachis Reviewed-by: None --- plugins/module_utils/k8s/client.py | 2 +- plugins/module_utils/k8s/exceptions.py | 10 + plugins/module_utils/k8s/service.py | 620 ++++++++++++++++++++++++ plugins/module_utils/k8s/waiter.py | 20 +- tests/unit/module_utils/test_service.py | 373 ++++++++++++++ tests/unit/module_utils/test_waiter.py | 12 +- 6 files changed, 1029 insertions(+), 8 deletions(-) create mode 100644 plugins/module_utils/k8s/exceptions.py create mode 100644 plugins/module_utils/k8s/service.py create mode 100644 tests/unit/module_utils/test_service.py diff --git a/plugins/module_utils/k8s/client.py b/plugins/module_utils/k8s/client.py index 055f1712..1dc674fb 100644 --- a/plugins/module_utils/k8s/client.py +++ b/plugins/module_utils/k8s/client.py @@ -250,4 +250,4 @@ def get_api_client(module=None, **kwargs: Optional[Any]) -> K8SClient: dry_run=module.params.get("dry_run", False), ) - return k8s_client.client + return k8s_client diff --git a/plugins/module_utils/k8s/exceptions.py b/plugins/module_utils/k8s/exceptions.py new file mode 100644 index 00000000..38c52b8c --- /dev/null +++ b/plugins/module_utils/k8s/exceptions.py @@ -0,0 +1,10 @@ +# Copyright: (c) 2021, Red Hat | Ansible +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + + +class CoreException(Exception): + pass + + +class ResourceTimeout(CoreException): + pass diff --git a/plugins/module_utils/k8s/service.py b/plugins/module_utils/k8s/service.py new file mode 100644 index 00000000..ffdd289f --- /dev/null +++ b/plugins/module_utils/k8s/service.py @@ -0,0 +1,620 @@ +# Copyright: (c) 2021, Red Hat | Ansible +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from typing import Any, Dict, List, Optional, Tuple + +from ansible_collections.kubernetes.core.plugins.module_utils.hashes import ( + generate_hash, +) + +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.waiter import ( + Waiter, + exists, + resource_absent, + get_waiter, +) + +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import ( + CoreException, + ResourceTimeout, +) + +from ansible.module_utils.common.dict_transformations import dict_merge + +try: + from kubernetes.dynamic.exceptions import ( + NotFoundError, + ResourceNotFoundError, + ResourceNotUniqueError, + ConflictError, + ForbiddenError, + MethodNotAllowedError, + BadRequestError, + ) +except ImportError: + # Handled in module setup + pass + +try: + from kubernetes.dynamic.resource import Resource, ResourceInstance +except ImportError: + # These are defined only for the sake of Ansible's checked import requirement + Resource = Any # type: ignore + ResourceInstance = Any # type: ignore + +try: + from ansible_collections.kubernetes.core.plugins.module_utils.apply import ( + apply_object, + ) +except ImportError: + # Handled in module setup + pass + +try: + from ansible_collections.kubernetes.core.plugins.module_utils.apply import ( + recursive_diff, + ) +except ImportError: + from ansible.module_utils.common.dict_transformations import recursive_diff + + +try: + from ansible_collections.kubernetes.core.plugins.module_utils.common import ( + _encode_stringdata, + ) +except ImportError: + # Handled in module setup + pass + + +class K8sService: + """A Service class for K8S modules. + This class has the primary purpose is to perform work on the cluster (e.g., create, apply, replace, update, delete). + """ + + def __init__(self, client, module) -> None: + self.client = client + self.module = module + + def find_resource( + self, kind: str, api_version: str, fail: bool = False + ) -> Optional[ResourceInstance]: + for attribute in ["kind", "name", "singular_name"]: + try: + return self.client.resources.get( + **{"api_version": api_version, attribute: kind} + ) + except (ResourceNotFoundError, ResourceNotUniqueError): + pass + try: + return self.client.resources.get( + api_version=api_version, short_names=[kind] + ) + except (ResourceNotFoundError, ResourceNotUniqueError): + if fail: + raise CoreException( + "Failed to find exact match for %s.%s by [kind, name, singularName, shortNames]" + % (api_version, kind) + ) + + def create_project_request(self, definition: Dict) -> Dict: + definition["kind"] = "ProjectRequest" + results = {"changed": False, "result": {}} + resource = self.find_resource( + "ProjectRequest", definition["apiVersion"], fail=True + ) + if not self.module.check_mode: + try: + k8s_obj = self.client.create(resource, definition) + results["result"] = k8s_obj.to_dict() + except Exception as e: + reason = e.body if hasattr(e, "body") else e + msg = "Failed to create object: {0}".format(reason) + raise CoreException(msg) from e + + results["changed"] = True + + return results + + def diff_objects(self, existing: Dict, new: Dict) -> Tuple[bool, Dict]: + result: Dict = dict() + diff = recursive_diff(existing, new) + if not diff: + return True, result + + result["before"] = diff[0] + result["after"] = diff[1] + + # If only metadata.generation and metadata.resourceVersion changed, ignore it + ignored_keys = set(["generation", "resourceVersion"]) + + if list(result["after"].keys()) != ["metadata"] or list( + result["before"].keys() + ) != ["metadata"]: + return False, result + + if not set(result["after"]["metadata"].keys()).issubset(ignored_keys): + return False, result + if not set(result["before"]["metadata"].keys()).issubset(ignored_keys): + return False, result + + if hasattr(self.module, "warn"): + self.module.warn( + "No meaningful diff was generated, but the API may not be idempotent (only metadata.generation or metadata.resourceVersion were changed)" + ) + + return True, result + + def patch_resource( + self, + resource: Resource, + definition: Dict, + name: str, + namespace: str, + merge_type: str = None, + ) -> Dict: + if merge_type == "json": + self.module.deprecate( + msg="json as a merge_type value is deprecated. Please use the k8s_json_patch module instead.", + version="3.0.0", + collection_name="kubernetes.core", + ) + try: + params = dict(name=name, namespace=namespace) + if self.module.check_mode: + params["dry_run"] = "All" + if merge_type: + params["content_type"] = "application/{0}-patch+json".format(merge_type) + return self.client.patch(resource, definition, **params).to_dict() + except Exception as e: + reason = e.body if hasattr(e, "body") else e + msg = "Failed to patch object: {0}".format(reason) + raise CoreException(msg) from e + + def retrieve(self, resource: Resource, definition: Dict) -> Dict: + state = self.module.params.get("state", None) + append_hash = self.module.params.get("append_hash", False) + name = definition["metadata"].get("name") + namespace = definition["metadata"].get("namespace") + label_selectors = self.module.params.get("label_selectors") + results = { + "changed": False, + "result": {}, + } + existing = None + + try: + # ignore append_hash for resources other than ConfigMap and Secret + if append_hash and definition["kind"] in ["ConfigMap", "Secret"]: + name = "%s-%s" % (name, generate_hash(definition)) + definition["metadata"]["name"] = name + params = dict(name=name) + if namespace: + params["namespace"] = namespace + if label_selectors: + params["label_selector"] = ",".join(label_selectors) + existing = self.client.get(resource, **params) + except (NotFoundError, MethodNotAllowedError): + pass + except ForbiddenError as e: + if ( + definition["kind"] in ["Project", "ProjectRequest"] + and state != "absent" + ): + return self.create_project_request(definition) + reason = e.body if hasattr(e, "body") else e + msg = "Failed to retrieve requested object: {0}".format(reason) + raise CoreException(msg) from e + except Exception as e: + reason = e.body if hasattr(e, "body") else e + msg = "Failed to retrieve requested object: {0}".format(reason) + raise CoreException(msg) from e + + if existing: + results["result"] = existing.to_dict() + + return results + + def find( + self, + kind: str, + api_version: str, + name: str = None, + namespace: Optional[str] = None, + label_selectors: Optional[List[str]] = None, + field_selectors: Optional[List[str]] = None, + wait: Optional[bool] = False, + wait_sleep: Optional[int] = 5, + wait_timeout: Optional[int] = 120, + state: Optional[str] = "present", + condition: Optional[Dict] = None, + ) -> Dict: + resource = self.find_resource(kind, api_version) + api_found = bool(resource) + if not api_found: + return dict( + resources=[], + msg='Failed to find API for resource with apiVersion "{0}" and kind "{1}"'.format( + api_version, kind + ), + api_found=False, + ) + + if not label_selectors: + label_selectors = [] + if not field_selectors: + field_selectors = [] + + result = {"resources": [], "api_found": True} + + # With a timeout of 0 the waiter will do a single check and return, effectively not waiting. + if not wait: + wait_timeout = 0 + + if state == "present": + predicate = exists + else: + predicate = resource_absent + + waiter = Waiter(self.client, resource, predicate) + + # This is an initial check to get the resource or resources that we then need to wait on individually. + try: + success, resources, duration = waiter.wait( + timeout=wait_timeout, + sleep=wait_sleep, + name=name, + namespace=namespace, + label_selectors=label_selectors, + field_selectors=field_selectors, + ) + except BadRequestError: + return result + + # There is either no result or there is a List resource with no items + if ( + not resources + or resources["kind"].endswith("List") + and not resources.get("items") + ): + return result + + instances = resources.get("items") or [resources] + + if not wait: + result["resources"] = instances + return result + + # Now wait for the specified state of any resource instances we have found. + waiter = get_waiter(self.client, resource, state=state, condition=condition) + for instance in instances: + name = instance["metadata"].get("name") + namespace = instance["metadata"].get("namespace") + success, res, duration = waiter.wait( + timeout=wait_timeout, sleep=wait_sleep, name=name, namespace=namespace, + ) + if not success: + raise CoreException( + "Failed to gather information about %s(s) even" + " after waiting for %s seconds" % (res.get("kind"), duration) + ) + result["resources"].append(res) + return result + + def create(self, resource: Resource, definition: Dict) -> Dict: + origin_name = definition["metadata"].get("name") + namespace = definition["metadata"].get("namespace") + name = definition["metadata"].get("name") + wait = self.module.params.get("wait") + wait_sleep = self.module.params.get("wait_sleep") + wait_timeout = self.module.params.get("wait_timeout") + wait_condition = None + if self.module.params.get("wait_condition") and self.module.params[ + "wait_condition" + ].get("type"): + wait_condition = self.module.params["wait_condition"] + results = {"changed": False, "result": {}} + + if self.module.check_mode and not self.client.dry_run: + k8s_obj = _encode_stringdata(definition) + else: + params = {} + if self.module.check_mode: + params["dry_run"] = "All" + try: + k8s_obj = self.client.create( + resource, definition, namespace=namespace, **params + ).to_dict() + except ConflictError: + # Some resources, like ProjectRequests, can't be created multiple times, + # because the resources that they create don't match their kind + # In this case we'll mark it as unchanged and warn the user + self.module.warn( + "{0} was not found, but creating it returned a 409 Conflict error. This can happen \ + if the resource you are creating does not directly create a resource of the same kind.".format( + name + ) + ) + return results + except Exception as e: + reason = e.body if hasattr(e, "body") else e + msg = "Failed to create object: {0}".format(reason) + raise CoreException(msg) from e + + success = True + results["result"] = k8s_obj + + if wait and not self.module.check_mode: + definition["metadata"].update({"name": k8s_obj["metadata"]["name"]}) + waiter = get_waiter(self.client, resource, condition=wait_condition) + success, results["result"], results["duration"] = waiter.wait( + timeout=wait_timeout, sleep=wait_sleep, name=name, namespace=namespace, + ) + + results["changed"] = True + + if not success: + raise ResourceTimeout( + '"{0}" "{1}": Resource creation timed out'.format( + definition["kind"], origin_name + ), + **results + ) + + return results + + def apply( + self, + resource: Resource, + definition: Dict, + existing: Optional[ResourceInstance] = None, + ) -> Dict: + apply = self.module.params.get("apply", False) + origin_name = definition["metadata"].get("name") + name = definition["metadata"].get("name") + namespace = definition["metadata"].get("namespace") + wait = self.module.params.get("wait") + wait_sleep = self.module.params.get("wait_sleep") + wait_condition = None + if self.module.params.get("wait_condition") and self.module.params[ + "wait_condition" + ].get("type"): + wait_condition = self.module.params["wait_condition"] + wait_timeout = self.module.params.get("wait_timeout") + results = {"changed": False, "result": {}} + + if apply: + if self.module.check_mode and not self.client.dry_run: + ignored, patch = apply_object(resource, _encode_stringdata(definition)) + if existing: + k8s_obj = dict_merge(existing.to_dict(), patch) + else: + k8s_obj = patch + else: + try: + params = {} + if self.module.check_mode: + params["dry_run"] = "All" + k8s_obj = self.client.apply( + resource, definition, namespace=namespace, **params + ).to_dict() + except Exception as e: + reason = e.body if hasattr(e, "body") else e + msg = "Failed to apply object: {0}".format(reason) + raise CoreException(msg) from e + + success = True + results["result"] = k8s_obj + + if wait and not self.module.check_mode: + waiter = get_waiter(self.client, resource, condition=wait_condition) + success, results["result"], results["duration"] = waiter.wait( + timeout=wait_timeout, + sleep=wait_sleep, + name=name, + namespace=namespace, + ) + + if existing: + existing = existing.to_dict() + else: + existing = {} + + match, diffs = self.diff_objects(existing, results["result"]) + results["changed"] = not match + + if self.module._diff: + results["diff"] = diffs + + if not success: + raise ResourceTimeout( + '"{0}" "{1}": Resource apply timed out'.format( + definition["kind"], origin_name + ), + **results + ) + + return results + + def replace( + self, resource: Resource, definition: Dict, existing: ResourceInstance, + ) -> Dict: + append_hash = self.module.params.get("append_hash", False) + name = definition["metadata"].get("name") + origin_name = definition["metadata"].get("name") + namespace = definition["metadata"].get("namespace") + wait = self.module.params.get("wait") + wait_sleep = self.module.params.get("wait_sleep") + wait_timeout = self.module.params.get("wait_timeout") + wait_condition = None + if self.module.params.get("wait_condition") and self.module.params[ + "wait_condition" + ].get("type"): + wait_condition = self.module.params["wait_condition"] + results = {"changed": False, "result": {}} + + if self.module.check_mode and not self.module.client.dry_run: + k8s_obj = _encode_stringdata(definition) + else: + params = {} + if self.module.check_mode: + params["dry_run"] = "All" + try: + k8s_obj = self.client.replace( + resource, + definition, + name=name, + namespace=namespace, + append_hash=append_hash, + **params + ).to_dict() + except Exception as e: + reason = e.body if hasattr(e, "body") else e + msg = "Failed to replace object: {0}".format(reason) + raise CoreException(msg) from e + + match, diffs = self.diff_objects(existing.to_dict(), k8s_obj) + success = True + results["result"] = k8s_obj + + if wait and not self.module.check_mode: + waiter = get_waiter(self.client, resource, condition=wait_condition) + success, results["result"], results["duration"] = waiter.wait( + timeout=wait_timeout, sleep=wait_sleep, name=name, namespace=namespace, + ) + match, diffs = self.diff_objects(existing.to_dict(), results["result"]) + results["changed"] = not match + + if self.module._diff: + results["diff"] = diffs + + if not success: + raise ResourceTimeout( + '"{0}" "{1}": Resource replacement timed out'.format( + definition["kind"], origin_name + ), + **results + ) + + return results + + def update( + self, resource: Resource, definition: Dict, existing: ResourceInstance + ) -> Dict: + name = definition["metadata"].get("name") + origin_name = definition["metadata"].get("name") + namespace = definition["metadata"].get("namespace") + wait = self.module.params.get("wait") + wait_sleep = self.module.params.get("wait_sleep") + wait_timeout = self.module.params.get("wait_timeout") + wait_condition = None + if self.module.params.get("wait_condition") and self.module.params[ + "wait_condition" + ].get("type"): + wait_condition = self.module.params["wait_condition"] + results = {"changed": False, "result": {}} + + if self.module.check_mode and not self.module.client.dry_run: + k8s_obj = dict_merge(existing.to_dict(), _encode_stringdata(definition)) + else: + for merge_type in self.module.params.get("merge_type") or [ + "strategic-merge", + "merge", + ]: + k8s_obj = self.patch_resource( + resource, definition, name, namespace, merge_type=merge_type, + ) + + success = True + results["result"] = k8s_obj + + if wait and not self.module.check_mode: + waiter = get_waiter(self.client, resource, condition=wait_condition) + success, results["result"], results["duration"] = waiter.wait( + timeout=wait_timeout, sleep=wait_sleep, name=name, namespace=namespace, + ) + + match, diffs = self.diff_objects(existing.to_dict(), results["result"]) + results["changed"] = not match + + if self.module._diff: + results["diff"] = diffs + + if not success: + raise ResourceTimeout( + '"{0}" "{1}": Resource update timed out'.format( + definition["kind"], origin_name + ), + **results + ) + + return results + + def delete( + self, + resource: Resource, + definition: Dict, + existing: Optional[ResourceInstance] = None, + ) -> Dict: + delete_options = self.module.params.get("delete_options") + label_selectors = self.module.params.get("label_selectors") + origin_name = definition["metadata"].get("name") + name = definition["metadata"].get("name") + namespace = definition["metadata"].get("namespace") + wait = self.module.params.get("wait") + wait_sleep = self.module.params.get("wait_sleep") + wait_timeout = self.module.params.get("wait_timeout") + results = {"changed": False, "result": {}} + params = {} + + def _empty_resource_list() -> bool: + if existing and existing.kind.endswith("List"): + return existing.items == [] + return False + + if not existing or _empty_resource_list(): + # The object already does not exist + return results + else: + # Delete the object + results["changed"] = True + if self.module.check_mode and not self.client.dry_run: + return results + else: + if delete_options: + body = { + "apiVersion": "v1", + "kind": "DeleteOptions", + } + body.update(delete_options) + params["body"] = body + + if self.module.check_mode: + params["dry_run"] = "All" + try: + k8s_obj = self.client.delete(resource, **params) + results["result"] = k8s_obj.to_dict() + except Exception as e: + reason = e.body if hasattr(e, "body") else e + msg = "Failed to delete object: {0}".format(reason) + raise CoreException(msg) from e + + if wait and not self.module.check_mode: + waiter = get_waiter(self.client, resource, state="absent") + success, resource, duration = waiter.wait( + timeout=wait_timeout, + sleep=wait_sleep, + name=name, + namespace=namespace, + label_selectors=label_selectors, + ) + results["duration"] = duration + if not success: + raise ResourceTimeout( + '"{0}" "{1}": Resource deletion timed out'.format( + definition["kind"], origin_name + ), + **results + ) + + return results diff --git a/plugins/module_utils/k8s/waiter.py b/plugins/module_utils/k8s/waiter.py index c0cc41a6..8ab60ada 100644 --- a/plugins/module_utils/k8s/waiter.py +++ b/plugins/module_utils/k8s/waiter.py @@ -128,17 +128,27 @@ class Waiter: def wait( self, - definition: Dict, timeout: int, sleep: int, + name: Optional[str] = None, + namespace: Optional[str] = None, label_selectors: Optional[List[str]] = None, + field_selectors: Optional[List[str]] = None, ) -> Tuple[bool, Optional[Dict], int]: - params = { - "name": definition["metadata"].get("name"), - "namespace": definition["metadata"].get("namespace"), - } + params = {} + + if name: + params["name"] = name + + if namespace: + params["namespace"] = namespace + if label_selectors: params["label_selector"] = ",".join(label_selectors) + + if field_selectors: + params["field_selector"] = ",".join(field_selectors) + instance: Optional[Dict] = None response = None elapsed = 0 diff --git a/tests/unit/module_utils/test_service.py b/tests/unit/module_utils/test_service.py new file mode 100644 index 00000000..979f6d5c --- /dev/null +++ b/tests/unit/module_utils/test_service.py @@ -0,0 +1,373 @@ +from unittest.mock import Mock + +import pytest +from kubernetes.dynamic.resource import ResourceInstance, Resource + +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.service import ( + K8sService, +) + +from kubernetes.dynamic.exceptions import NotFoundError + +pod_definition = { + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "name": "foo", + "labels": {"environment": "production", "app": "nginx"}, + "namespace": "foo", + }, + "spec": { + "containers": [ + { + "name": "nginx", + "image": "nginx:1.14.2", + "command": ["/bin/sh", "-c", "sleep 10"], + } + ] + }, +} + +pod_definition_updated = { + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "name": "foo", + "labels": {"environment": "testing", "app": "nginx"}, + "namespace": "bar", + }, + "spec": { + "containers": [ + { + "name": "nginx", + "image": "nginx:1.14.2", + "command": ["/bin/sh", "-c", "sleep 10"], + } + ] + }, +} + + +@pytest.fixture(scope="module") +def mock_pod_resource_instance(): + return ResourceInstance(None, pod_definition) + + +@pytest.fixture(scope="module") +def mock_pod_updated_resource_instance(): + return ResourceInstance(None, pod_definition_updated) + + +def test_diff_objects_no_diff(): + svc = K8sService(Mock(), Mock()) + match, diff = svc.diff_objects(pod_definition, pod_definition) + + assert match is True + assert diff == {} + + +def test_diff_objects_meta_diff(): + svc = K8sService(Mock(), Mock()) + match, diff = svc.diff_objects(pod_definition, pod_definition_updated) + + assert match is False + assert diff["before"] == { + "metadata": {"labels": {"environment": "production"}, "namespace": "foo"} + } + assert diff["after"] == { + "metadata": {"labels": {"environment": "testing"}, "namespace": "bar"} + } + + +def test_diff_objects_spec_diff(): + pod_definition_updated = { + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "name": "foo", + "labels": {"environment": "production", "app": "nginx"}, + "namespace": "foo", + }, + "spec": { + "containers": [ + { + "name": "busybox", + "image": "busybox", + "command": ["/bin/sh", "-c", "sleep 3600"], + } + ] + }, + } + svc = K8sService(Mock(), Mock()) + match, diff = svc.diff_objects(pod_definition, pod_definition_updated) + + assert match is False + assert diff["before"]["spec"] == pod_definition["spec"] + assert diff["after"]["spec"] == pod_definition_updated["spec"] + + +def test_find_resource(): + mock_pod_resource = Resource( + api_version="v1", kind="Pod", namespaced=False, preferred=True, prefix="api" + ) + spec = {"resources.get.side_effect": [mock_pod_resource]} + client = Mock(**spec) + svc = K8sService(client, Mock()) + resource = svc.find_resource("Pod", "v1") + + assert isinstance(resource, Resource) + assert resource.to_dict().items() <= mock_pod_resource.to_dict().items() + + +def test_service_delete_existing_resource(mock_pod_resource_instance): + spec = {"delete.side_effect": [mock_pod_resource_instance]} + client = Mock(**spec) + module = Mock() + module.params = {} + module.check_mode = False + svc = K8sService(client, module) + results = svc.delete(Mock(), pod_definition, mock_pod_resource_instance) + + assert isinstance(results, dict) + assert results["changed"] is True + assert results["result"] == pod_definition + + +def test_service_delete_no_existing_resource(): + module = Mock() + module.params = {} + module.check_mode = False + svc = K8sService(Mock(), module) + results = svc.delete(Mock(), pod_definition) + + assert isinstance(results, dict) + assert results["changed"] is False + assert results["result"] == {} + + +def test_service_delete_existing_resource_check_mode(mock_pod_resource_instance): + module = Mock() + module.params = {"wait": False} + module.check_mode = True + svc = K8sService(Mock(), module) + results = svc.delete(Mock(), pod_definition, mock_pod_resource_instance) + + assert isinstance(results, dict) + assert results["changed"] is True + + +def test_service_create_resource(mock_pod_resource_instance): + spec = {"create.side_effect": [mock_pod_resource_instance]} + client = Mock(**spec) + module = Mock() + module.params = {} + module.check_mode = False + svc = K8sService(client, module) + results = svc.create(Mock(), pod_definition) + + assert isinstance(results, dict) + assert results["changed"] is True + assert results["result"] == pod_definition + + +def test_service_create_resource_check_mode(): + client = Mock() + client.dry_run = False + module = Mock() + module.params = {} + module.check_mode = True + svc = K8sService(client, module) + results = svc.create(Mock(), pod_definition) + + assert isinstance(results, dict) + assert results["changed"] is True + assert results["result"] == pod_definition + + +def test_service_retrieve_existing_resource(mock_pod_resource_instance): + spec = {"get.side_effect": [mock_pod_resource_instance]} + client = Mock(**spec) + module = Mock() + module.params = {} + svc = K8sService(client, module) + results = svc.retrieve(Mock(), pod_definition) + + assert isinstance(results, dict) + assert results["changed"] is False + assert results["result"] == pod_definition + + +def test_service_retrieve_no_existing_resource(): + spec = {"get.side_effect": [NotFoundError(Mock())]} + client = Mock(**spec) + module = Mock() + module.params = {} + svc = K8sService(client, module) + results = svc.retrieve(Mock(), pod_definition) + + assert isinstance(results, dict) + assert results["changed"] is False + assert results["result"] == {} + + +def test_create_project_request(): + project_definition = { + "apiVersion": "v1", + "kind": "ProjectRequest", + "metadata": {"name": "test"}, + } + spec = {"create.side_effect": [ResourceInstance(None, project_definition)]} + client = Mock(**spec) + module = Mock() + module.check_mode = False + module.params = {"state": "present"} + svc = K8sService(client, module) + results = svc.create_project_request(project_definition) + + assert isinstance(results, dict) + assert results["changed"] is True + assert results["result"] == project_definition + + +def test_service_apply_existing_resource(mock_pod_resource_instance): + spec = {"apply.side_effect": [ResourceInstance(None, pod_definition_updated)]} + client = Mock(**spec) + module = Mock() + module.params = {"apply": True} + module.check_mode = False + svc = K8sService(client, module) + results = svc.apply(Mock(), pod_definition_updated, mock_pod_resource_instance) + + assert isinstance(results, dict) + assert results["changed"] is True + assert results["diff"] is not {} + assert results["result"] == pod_definition_updated + + +def test_service_apply_existing_resource_no_diff(mock_pod_resource_instance): + spec = {"apply.side_effect": [mock_pod_resource_instance]} + client = Mock(**spec) + module = Mock() + module.params = {"apply": True} + module.check_mode = False + svc = K8sService(client, module) + results = svc.apply(Mock(), pod_definition, mock_pod_resource_instance) + + assert isinstance(results, dict) + assert results["changed"] is False + assert results["diff"] == {} + assert results["result"] == pod_definition + + +def test_service_apply_existing_resource_no_apply(mock_pod_resource_instance): + spec = {"apply.side_effect": [mock_pod_resource_instance]} + client = Mock(**spec) + module = Mock() + module.params = {"apply": False} + module.check_mode = False + svc = K8sService(client, module) + results = svc.apply(Mock(), pod_definition, mock_pod_resource_instance) + + assert isinstance(results, dict) + assert results["changed"] is False + assert results["result"] == {} + + +def test_service_replace_existing_resource_no_diff(mock_pod_resource_instance): + spec = {"replace.side_effect": [mock_pod_resource_instance]} + client = Mock(**spec) + module = Mock() + module.params = {} + module.check_mode = False + svc = K8sService(client, module) + results = svc.replace(Mock(), pod_definition, mock_pod_resource_instance) + + assert isinstance(results, dict) + assert results["changed"] is False + assert results["diff"] == {} + assert results["result"] == pod_definition + + +def test_service_replace_existing_resource( + mock_pod_resource_instance, mock_pod_updated_resource_instance +): + spec = {"replace.side_effect": [mock_pod_updated_resource_instance]} + client = Mock(**spec) + module = Mock() + module.params = {} + module.check_mode = False + svc = K8sService(client, module) + results = svc.replace(Mock(), pod_definition_updated, mock_pod_resource_instance) + + assert isinstance(results, dict) + assert results["changed"] is True + assert results["result"] == pod_definition_updated + assert results["diff"] != {} + assert results["diff"]["before"] is not {} + assert results["diff"]["after"] is not {} + + +def test_service_update_existing_resource( + mock_pod_resource_instance, mock_pod_updated_resource_instance +): + spec = {"replace.side_effect": [mock_pod_updated_resource_instance]} + client = Mock(**spec) + module = Mock() + module.params = {} + module.check_mode = False + svc = K8sService(client, module) + results = svc.replace(Mock(), pod_definition_updated, mock_pod_resource_instance) + + assert isinstance(results, dict) + assert results["changed"] is True + assert results["result"] == pod_definition_updated + assert results["diff"] != {} + assert results["diff"]["before"] is not {} + assert results["diff"]["after"] is not {} + + +def test_service_update_existing_resource_no_diff(mock_pod_updated_resource_instance): + spec = {"replace.side_effect": [mock_pod_updated_resource_instance]} + client = Mock(**spec) + module = Mock() + module.params = {} + module.check_mode = False + svc = K8sService(client, module) + results = svc.replace( + Mock(), pod_definition_updated, mock_pod_updated_resource_instance + ) + + assert isinstance(results, dict) + assert results["changed"] is False + assert results["result"] == pod_definition_updated + assert results["diff"] == {} + + +def test_service_find(mock_pod_resource_instance): + spec = {"get.side_effect": [mock_pod_resource_instance]} + client = Mock(**spec) + module = Mock() + module.params = {} + module.check_mode = False + svc = K8sService(client, module) + results = svc.find("Pod", "v1", name="foo", namespace="foo") + + assert isinstance(results, dict) + assert results["api_found"] is True + assert results["resources"] is not [] + assert len(results["resources"]) == 1 + assert results["resources"][0] == pod_definition + + +def test_service_find_error(): + spec = {"get.side_effect": [NotFoundError(Mock())]} + client = Mock(**spec) + module = Mock() + module.params = {} + module.check_mode = False + svc = K8sService(client, module) + results = svc.find("Pod", "v1", name="foo", namespace="foo") + + assert isinstance(results, dict) + assert results["api_found"] is True + assert results["resources"] == [] diff --git a/tests/unit/module_utils/test_waiter.py b/tests/unit/module_utils/test_waiter.py index 5ed76743..b2118895 100644 --- a/tests/unit/module_utils/test_waiter.py +++ b/tests/unit/module_utils/test_waiter.py @@ -83,7 +83,10 @@ def test_waiter_waits_for_missing_resource(): client = Mock(**spec) resource = Mock() result, instance, elapsed = Waiter(client, resource, exists).wait( - RESOURCES[0], 3, 1 + timeout=3, + sleep=1, + name=RESOURCES[0]["metadata"].get("name"), + namespace=RESOURCES[0]["metadata"].get("namespace"), ) assert result is False assert instance is None @@ -95,7 +98,12 @@ def test_waiter_waits_for_resource_to_exist(resource, expected): result = resource.to_dict() spec = {"get.side_effect": [NotFoundError(Mock()), resource, resource, resource]} client = Mock(**spec) - success, instance, elapsed = Waiter(client, Mock(), exists).wait(result, 3, 1) + success, instance, elapsed = Waiter(client, Mock(), exists).wait( + timeout=3, + sleep=1, + name=result["metadata"].get("name"), + namespace=result["metadata"].get("namespace"), + ) assert success is expected assert instance == result assert abs(elapsed - 2) <= 1 From d68da5bbddeb2d065bf96635b57f8f9d71c9728b Mon Sep 17 00:00:00 2001 From: Alina Buzachis Date: Thu, 13 Jan 2022 20:22:41 +0100 Subject: [PATCH 06/24] k8s runner (#309) k8s runner SUMMARY k8s runner Requires: #307 ISSUE TYPE New Module Pull Request Reviewed-by: Abhijeet Kasurde Reviewed-by: Alina Buzachis Reviewed-by: Mike Graves Reviewed-by: None --- plugins/module_utils/k8s/runner.py | 112 +++++++++++++++++++++++++ tests/unit/module_utils/test_runner.py | 53 ++++++++++++ 2 files changed, 165 insertions(+) create mode 100644 plugins/module_utils/k8s/runner.py create mode 100644 tests/unit/module_utils/test_runner.py diff --git a/plugins/module_utils/k8s/runner.py b/plugins/module_utils/k8s/runner.py new file mode 100644 index 00000000..2c9379d4 --- /dev/null +++ b/plugins/module_utils/k8s/runner.py @@ -0,0 +1,112 @@ +# Copyright: (c) 2021, Red Hat | Ansible +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from typing import Dict + +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.client import ( + get_api_client, +) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.resource import ( + create_definitions, +) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.service import ( + K8sService, +) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import ( + CoreException, +) + +from ansible_collections.kubernetes.core.plugins.module_utils.selector import ( + LabelSelectorFilter, +) + + +def validate(client, module, resource): + def _prepend_resource_info(resource, msg): + return "%s %s: %s" % (resource["kind"], resource["metadata"]["name"], msg) + + module.requires("kubernetes-validate") + + warnings, errors = client.validate( + resource, + module.params["validate"].get("version"), + module.params["validate"].get("strict"), + ) + + if errors and module.params["validate"]["fail_on_error"]: + module.fail_json( + msg="\n".join([_prepend_resource_info(resource, error) for error in errors]) + ) + return [_prepend_resource_info(resource, msg) for msg in warnings + errors] + + +def run_module(module) -> None: + results = [] + + client = get_api_client(module) + svc = K8sService(client, module) + definitions = create_definitions(module.params) + + for definition in definitions: + module.warnings = [] + + if module.params["validate"] is not None: + module.warnings = validate(client, module, definition) + + try: + result = perform_action(svc, definition, module.params) + except CoreException as e: + if module.warnings: + e["msg"] += "\n" + "\n ".join(module.warnings) + if module.params.get("continue_on_error"): + result = {"error": "{0}".format(e)} + else: + module.fail_json(msg=e) + if module.warnings: + result["warnings"] = module.warnings + + results.append(result) + + module.exit_json(**results) + + +def perform_action(svc, definition: Dict, params: Dict) -> Dict: + origin_name = definition["metadata"].get("name") + namespace = definition["metadata"].get("namespace") + label_selectors = params.get("label_selectors") + state = params.get("state", None) + result = {} + + resource = svc.find_resource(definition) + existing = svc.retrieve(resource, definition) + + if state == "absent": + result = svc.delete(resource, definition, existing) + result["method"] = "delete" + else: + if label_selectors: + filter_selector = LabelSelectorFilter(label_selectors) + if not filter_selector.isMatching(definition): + result["changed"] = False + result["msg"] = ( + "resource 'kind={kind},name={name},namespace={namespace}' " + "filtered by label_selectors.".format( + kind=definition["kind"], name=origin_name, namespace=namespace, + ) + ) + return result + + if params.get("apply"): + result = svc.apply(resource, definition, existing) + result["method"] = "apply" + elif not existing: + result = svc.create(resource, definition) + result["method"] = "create" + elif params.get("force", False): + result = svc.replace(resource, definition, existing) + result["method"] = "replace" + else: + result = svc.update(resource, definition, existing) + result["method"] = "update" + + return result diff --git a/tests/unit/module_utils/test_runner.py b/tests/unit/module_utils/test_runner.py new file mode 100644 index 00000000..21579c2c --- /dev/null +++ b/tests/unit/module_utils/test_runner.py @@ -0,0 +1,53 @@ +import pytest +from unittest.mock import MagicMock, call + +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.runner import ( + perform_action, +) + +definition = { + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "name": "foo", + "labels": {"environment": "production", "app": "nginx"}, + "namespace": "foo", + }, + "spec": { + "containers": [ + { + "name": "nginx", + "image": "nginx:1.14.2", + "command": ["/bin/sh", "-c", "sleep 10"], + } + ] + }, +} + + +@pytest.mark.parametrize( + "params, expected", + [ + ({"state": "absent"}, call.__setitem__("method", "delete")), + ({"apply": True}, call.__setitem__("method", "apply")), + ({"force": True}, call.__setitem__("method", "replace")), + ({"apply": False}, call.__setitem__("method", "update")), + ({}, call.__setitem__("method", "update")), + ], +) +def test_perform_action(params, expected): + module = MagicMock() + module.params = params + + result = perform_action(MagicMock(), definition, module.params) + result.assert_has_calls([expected], any_order=True) + + +def test_perform_action_create(): + spec = {"retrieve.side_effect": [{}]} + svc = MagicMock(**spec) + module = MagicMock() + module.params = {} + + result = perform_action(svc, definition, module.params) + result.assert_has_calls([call.__setitem__("method", "create")], any_order=True) From 346e303084f0bc7e25cd94663d49229b77aade3f Mon Sep 17 00:00:00 2001 From: Mike Graves Date: Mon, 17 Jan 2022 01:38:26 -0500 Subject: [PATCH 07/24] [backport/2.x] Migrate k8s_drain to use refactored code (#330) --- plugins/modules/k8s_drain.py | 36 +++++++++++------------------------- 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/plugins/modules/k8s_drain.py b/plugins/modules/k8s_drain.py index af3c6907..7ef12bd5 100644 --- a/plugins/modules/k8s_drain.py +++ b/plugins/modules/k8s_drain.py @@ -136,6 +136,12 @@ from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule impo from ansible_collections.kubernetes.core.plugins.module_utils.args_common import ( AUTH_ARG_SPEC, ) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.client import ( + get_api_client, +) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.core import ( + AnsibleK8SModule, +) from ansible.module_utils._text import to_native try: @@ -244,30 +250,9 @@ def filter_pods(pods, force, ignore_daemonset, delete_emptydir_data): class K8sDrainAnsible(object): - def __init__(self, module): - from ansible_collections.kubernetes.core.plugins.module_utils.common import ( - K8sAnsibleMixin, - get_api_client, - ) - + def __init__(self, module, client): self._module = module - self._k8s_ansible_mixin = K8sAnsibleMixin(module) - self._k8s_ansible_mixin.client = get_api_client(module=self._module) - - self._k8s_ansible_mixin.module = self._module - self._k8s_ansible_mixin.argspec = self._module.argument_spec - self._k8s_ansible_mixin.check_mode = self._module.check_mode - self._k8s_ansible_mixin.params = self._module.params - self._k8s_ansible_mixin.fail_json = self._module.fail_json - self._k8s_ansible_mixin.fail = self._module.fail_json - self._k8s_ansible_mixin.exit_json = self._module.exit_json - self._k8s_ansible_mixin.warn = self._module.warn - self._k8s_ansible_mixin.warnings = [] - - self._api_instance = core_v1_api.CoreV1Api( - self._k8s_ansible_mixin.client.client - ) - self._k8s_ansible_mixin.check_library_version() + self._api_instance = core_v1_api.CoreV1Api(client.client) # delete options self._drain_options = module.params.get("delete_options", {}) @@ -503,7 +488,7 @@ def argspec(): def main(): - module = AnsibleModule(argument_spec=argspec()) + module = AnsibleK8SModule(module_class=AnsibleModule, argument_spec=argspec()) if not HAS_EVICTION_API: module.fail_json( @@ -512,7 +497,8 @@ def main(): error=to_native(k8s_import_exception), ) - k8s_drain = K8sDrainAnsible(module) + client = get_api_client(module=module) + k8s_drain = K8sDrainAnsible(module, client.client) k8s_drain.execute_module() From e2e3f71ecfd5f115b54e2a1936ba5926864a0320 Mon Sep 17 00:00:00 2001 From: Mike Graves Date: Mon, 17 Jan 2022 06:13:58 -0500 Subject: [PATCH 08/24] [backport/2.2] Migrate k8s_log to new refactored code (#336) --- plugins/modules/k8s_log.py | 87 ++++++++++++++++++++------------------ 1 file changed, 46 insertions(+), 41 deletions(-) diff --git a/plugins/modules/k8s_log.py b/plugins/modules/k8s_log.py index db63e14b..023e9570 100644 --- a/plugins/modules/k8s_log.py +++ b/plugins/modules/k8s_log.py @@ -128,12 +128,22 @@ import copy from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import ( AnsibleModule, ) -from ansible.module_utils.six import PY2 - from ansible_collections.kubernetes.core.plugins.module_utils.args_common import ( AUTH_ARG_SPEC, NAME_ARG_SPEC, ) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.client import ( + get_api_client, +) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.core import ( + AnsibleK8SModule, +) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import ( + CoreException, +) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.service import ( + K8sService, +) def argspec(): @@ -151,32 +161,30 @@ def argspec(): return args -def execute_module(module, k8s_ansible_mixin): - name = module.params.get("name") - namespace = module.params.get("namespace") - label_selector = ",".join(module.params.get("label_selectors", {})) +def execute_module(svc, params): + name = params.get("name") + namespace = params.get("namespace") + label_selector = ",".join(params.get("label_selectors", {})) if name and label_selector: - module.fail(msg="Only one of name or label_selectors can be provided") + raise CoreException("Only one of name or label_selectors can be provided") - resource = k8s_ansible_mixin.find_resource( - module.params["kind"], module.params["api_version"], fail=True - ) - v1_pods = k8s_ansible_mixin.find_resource("Pod", "v1", fail=True) + resource = svc.find_resource(params["kind"], params["api_version"], fail=True) + v1_pods = svc.find_resource("Pod", "v1", fail=True) if "log" not in resource.subresources: if not name: - module.fail( - msg="name must be provided for resources that do not support the log subresource" + raise CoreException( + "name must be provided for resources that do not support the log subresource" ) instance = resource.get(name=name, namespace=namespace) - label_selector = ",".join(extract_selectors(module, instance)) + label_selector = ",".join(extract_selectors(instance)) resource = v1_pods if label_selector: instances = v1_pods.get(namespace=namespace, label_selector=label_selector) if not instances.items: - module.fail( - msg="No pods in namespace {0} matched selector {1}".format( + raise CoreException( + "No pods in namespace {0} matched selector {1}".format( namespace, label_selector ) ) @@ -185,12 +193,12 @@ def execute_module(module, k8s_ansible_mixin): resource = v1_pods kwargs = {} - if module.params.get("container"): - kwargs["query_params"] = dict(container=module.params["container"]) + if params.get("container"): + kwargs["query_params"] = {"container": params["container"]} - if module.params.get("since_seconds"): + if params.get("since_seconds"): kwargs.setdefault("query_params", {}).update( - {"sinceSeconds": module.params["since_seconds"]} + {"sinceSeconds": params["since_seconds"]} ) if module.params.get("previous"): @@ -198,20 +206,21 @@ def execute_module(module, k8s_ansible_mixin): {"previous": module.params["previous"]} ) - log = serialize_log( - resource.log.get(name=name, namespace=namespace, serialize=False, **kwargs) + response = resource.log.get( + name=name, namespace=namespace, serialize=False, **kwargs ) + log = response.data.decode("utf8") - module.exit_json(changed=False, log=log, log_lines=log.split("\n")) + return {"changed": False, "log": log, "log_lines": log.split("\n")} -def extract_selectors(module, instance): +def extract_selectors(instance): # Parses selectors on an object based on the specifications documented here: # https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors selectors = [] if not instance.spec.selector: - module.fail( - msg="{0} {1} does not support the log subresource directly, and no Pod selector was found on the object".format( + raise CoreException( + "{0} {1} does not support the log subresource directly, and no Pod selector was found on the object".format( "/".join(instance.group, instance.apiVersion), instance.kind ) ) @@ -245,8 +254,8 @@ def extract_selectors(module, instance): ) ) else: - module.fail( - msg="The k8s_log module does not support the {0} matchExpression operator".format( + raise CoreException( + "The k8s_log module does not support the {0} matchExpression operator".format( operator.lower() ) ) @@ -254,22 +263,18 @@ def extract_selectors(module, instance): return selectors -def serialize_log(response): - if PY2: - return response.data - return response.data.decode("utf8") - - def main(): - module = AnsibleModule(argument_spec=argspec(), supports_check_mode=True) - from ansible_collections.kubernetes.core.plugins.module_utils.common import ( - K8sAnsibleMixin, - get_api_client, + module = AnsibleK8SModule( + module_class=AnsibleModule, argument_spec=argspec(), supports_check_mode=True ) - k8s_ansible_mixin = K8sAnsibleMixin(module) - k8s_ansible_mixin.client = get_api_client(module=module) - execute_module(module, k8s_ansible_mixin) + try: + client = get_api_client(module=module) + svc = K8sService(client, module) + result = execute_module(svc, module.params) + module.exit_json(**result) + except CoreException as e: + module.fail_json(msg=e) if __name__ == "__main__": From afa6a74178ea66d56ef73b75de629e0aba751c04 Mon Sep 17 00:00:00 2001 From: Mike Graves Date: Tue, 18 Jan 2022 15:51:17 -0500 Subject: [PATCH 09/24] Migrate json_patch to use new refactored code (#339) Migrate json_patch to use new refactored code SUMMARY Migrate json_patch to use new refactored code ISSUE TYPE COMPONENT NAME k8s_json_patch ADDITIONAL INFORMATION Reviewed-by: Alina Buzachis Reviewed-by: None --- plugins/modules/k8s_json_patch.py | 48 +++++++++++++++++-------------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/plugins/modules/k8s_json_patch.py b/plugins/modules/k8s_json_patch.py index 27b5c8f4..eb2303ef 100644 --- a/plugins/modules/k8s_json_patch.py +++ b/plugins/modules/k8s_json_patch.py @@ -136,10 +136,19 @@ from ansible_collections.kubernetes.core.plugins.module_utils.args_common import AUTH_ARG_SPEC, WAIT_ARG_SPEC, ) -from ansible_collections.kubernetes.core.plugins.module_utils.common import ( +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.client import ( get_api_client, - K8sAnsibleMixin, ) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.core import ( + AnsibleK8SModule, +) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.service import ( + K8sService, +) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.waiter import ( + get_waiter, +) + try: from kubernetes.dynamic.exceptions import DynamicApiError @@ -185,7 +194,7 @@ def json_patch(existing, patch): return None, error -def execute_module(k8s_module, module): +def execute_module(module, svc): kind = module.params.get("kind") api_version = module.params.get("api_version") name = module.params.get("name") @@ -200,19 +209,15 @@ def execute_module(k8s_module, module): "type" ): wait_condition = module.params["wait_condition"] - # definition is needed for wait - definition = { - "kind": kind, - "metadata": {"name": name, "namespace": namespace}, - } def build_error_msg(kind, name, msg): return "%s %s: %s" % (kind, name, msg) - resource = k8s_module.find_resource(kind, api_version, fail=True) + client = svc.client + resource = svc.find_resource(kind, api_version, fail=True) try: - existing = resource.get(name=name, namespace=namespace) + existing = client.get(resource, name=name, namespace=namespace) except DynamicApiError as exc: msg = "Failed to retrieve requested object: {0}".format(exc.body) module.fail_json( @@ -227,7 +232,7 @@ def execute_module(k8s_module, module): msg=build_error_msg(kind, name, msg), error="", status="", reason="" ) - if module.check_mode and not k8s_module.supports_dry_run: + if module.check_mode and not client.dry_run: obj, error = json_patch(existing.to_dict(), patch) if error: module.fail_json(**error) @@ -236,7 +241,8 @@ def execute_module(k8s_module, module): if module.check_mode: params["dry_run"] = "All" try: - obj = resource.patch( + obj = client.patch( + resource, patch, name=name, namespace=namespace, @@ -255,10 +261,11 @@ def execute_module(k8s_module, module): success = True result = {"result": obj} if wait and not module.check_mode: - success, result["result"], result["duration"] = k8s_module.wait( - resource, definition, wait_sleep, wait_timeout, condition=wait_condition + waiter = get_waiter(client, resource, condition=wait_condition) + success, result["result"], result["duration"] = waiter.wait( + wait_timeout, wait_sleep, name, namespace ) - match, diffs = k8s_module.diff_objects(existing.to_dict(), obj) + match, diffs = svc.diff_objects(existing.to_dict(), obj) result["changed"] = not match if module._diff: result["diff"] = diffs @@ -274,13 +281,12 @@ def main(): args = copy.deepcopy(AUTH_ARG_SPEC) args.update(copy.deepcopy(WAIT_ARG_SPEC)) args.update(JSON_PATCH_ARGS) - module = AnsibleModule(argument_spec=args, supports_check_mode=True) - k8s_module = K8sAnsibleMixin(module) - k8s_module.params = module.params - k8s_module.check_library_version() + module = AnsibleK8SModule( + module_class=AnsibleModule, argument_spec=args, supports_check_mode=True + ) client = get_api_client(module) - k8s_module.client = client - execute_module(k8s_module, module) + svc = K8sService(client, module) + execute_module(module, svc) if __name__ == "__main__": From 9aa20f0fbefa97419448f1173e599bbd542bbc40 Mon Sep 17 00:00:00 2001 From: Alina Buzachis Date: Wed, 19 Jan 2022 16:02:07 +0100 Subject: [PATCH 10/24] Migrate k8s_info (#310) * Use refactored module_utils Signed-off-by: Alina Buzachis * Update k8s_info.py * Fix assertion Signed-off-by: Alina Buzachis --- plugins/modules/k8s_info.py | 30 ++++++++++--------- .../targets/k8s_validate/tasks/main.yml | 2 +- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/plugins/modules/k8s_info.py b/plugins/modules/k8s_info.py index 65d4cac8..7d831d1d 100644 --- a/plugins/modules/k8s_info.py +++ b/plugins/modules/k8s_info.py @@ -155,10 +155,19 @@ from ansible_collections.kubernetes.core.plugins.module_utils.args_common import AUTH_ARG_SPEC, WAIT_ARG_SPEC, ) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.core import ( + AnsibleK8SModule, +) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.client import ( + get_api_client, +) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.service import ( + K8sService, +) -def execute_module(module, k8s_ansible_mixin): - facts = k8s_ansible_mixin.kubernetes_facts( +def execute_module(module, svc): + facts = svc.find( module.params["kind"], module.params["api_version"], name=module.params["name"], @@ -190,19 +199,12 @@ def argspec(): def main(): - module = AnsibleModule(argument_spec=argspec(), supports_check_mode=True) - from ansible_collections.kubernetes.core.plugins.module_utils.common import ( - K8sAnsibleMixin, - get_api_client, + module = AnsibleK8SModule( + module_class=AnsibleModule, argument_spec=argspec(), supports_check_mode=True ) - - k8s_ansible_mixin = K8sAnsibleMixin(module) - k8s_ansible_mixin.client = get_api_client(module=module) - k8s_ansible_mixin.fail_json = module.fail_json - k8s_ansible_mixin.fail = module.fail_json - k8s_ansible_mixin.exit_json = module.exit_json - k8s_ansible_mixin.warn = module.warn - execute_module(module, k8s_ansible_mixin) + client = get_api_client(module) + svc = K8sService(client, module) + execute_module(module, svc) if __name__ == "__main__": diff --git a/tests/integration/targets/k8s_validate/tasks/main.yml b/tests/integration/targets/k8s_validate/tasks/main.yml index 18587560..f910c608 100644 --- a/tests/integration/targets/k8s_validate/tasks/main.yml +++ b/tests/integration/targets/k8s_validate/tasks/main.yml @@ -201,7 +201,7 @@ - name: assert that task failed with proper message assert: that: - - '"kubernetes >= 17.17.0 is required" in _result.msg' + - '"kubernetes >= 17.17.0 is required" in _result.module_stderr' when: - _stat.stat.exists - _stat.stat.readable From f5a0dd5946cb7b97ce0083dcfc03fbb1e912f03c Mon Sep 17 00:00:00 2001 From: Alina Buzachis Date: Thu, 20 Jan 2022 16:35:16 +0100 Subject: [PATCH 11/24] Update k8s_rollback to use refactored module_utils (#338) Update k8s_rollback to use refactored module_utils SUMMARY Update k8s_rollback to use refactored module_utils ISSUE TYPE Feature Pull Request COMPONENT NAME k8s_rollback Reviewed-by: Mike Graves Reviewed-by: None --- plugins/modules/k8s_rollback.py | 48 +++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/plugins/modules/k8s_rollback.py b/plugins/modules/k8s_rollback.py index df4f419d..335b6ee5 100644 --- a/plugins/modules/k8s_rollback.py +++ b/plugins/modules/k8s_rollback.py @@ -86,12 +86,23 @@ from ansible_collections.kubernetes.core.plugins.module_utils.args_common import AUTH_ARG_SPEC, NAME_ARG_SPEC, ) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.client import ( + get_api_client, +) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.core import ( + AnsibleK8SModule, +) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import ( + CoreException, +) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.service import ( + K8sService, +) -def get_managed_resource(module): +def get_managed_resource(kind): managed_resource = {} - kind = module.params["kind"] if kind == "DaemonSet": managed_resource["kind"] = "ControllerRevision" managed_resource["api_version"] = "apps/v1" @@ -99,14 +110,17 @@ def get_managed_resource(module): managed_resource["kind"] = "ReplicaSet" managed_resource["api_version"] = "apps/v1" else: - module.fail(msg="Cannot perform rollback on resource of kind {0}".format(kind)) + raise CoreException( + "Cannot perform rollback on resource of kind {0}".format(kind) + ) return managed_resource -def execute_module(module, k8s_ansible_mixin): +def execute_module(svc): results = [] + module = svc.module - resources = k8s_ansible_mixin.kubernetes_facts( + resources = svc.find( module.params["kind"], module.params["api_version"], module.params["name"], @@ -117,14 +131,16 @@ def execute_module(module, k8s_ansible_mixin): changed = False for resource in resources["resources"]: - result = perform_action(module, k8s_ansible_mixin, resource) + result = perform_action(svc, resource) changed = result["changed"] or changed results.append(result) module.exit_json(**{"changed": changed, "rollback_info": results}) -def perform_action(module, k8s_ansible_mixin, resource): +def perform_action(svc, resource): + module = svc.module + if module.params["kind"] == "DaemonSet": current_revision = resource["metadata"]["generation"] elif module.params["kind"] == "Deployment": @@ -132,8 +148,8 @@ def perform_action(module, k8s_ansible_mixin, resource): "deployment.kubernetes.io/revision" ] - managed_resource = get_managed_resource(module) - managed_resources = k8s_ansible_mixin.kubernetes_facts( + managed_resource = get_managed_resource(module.params["kind"]) + managed_resources = svc.find( managed_resource["kind"], managed_resource["api_version"], "", @@ -185,7 +201,7 @@ def perform_action(module, k8s_ansible_mixin, resource): rollback = resource if not module.check_mode: - rollback = k8s_ansible_mixin.client.request( + rollback = svc.client.client.request( "PATCH", "/apis/{0}/namespaces/{1}/{2}/{3}".format( module.params["api_version"], @@ -242,15 +258,13 @@ def get_previous_revision(all_resources, current_revision): def main(): - module = AnsibleModule(argument_spec=argspec(), supports_check_mode=True) - from ansible_collections.kubernetes.core.plugins.module_utils.common import ( - K8sAnsibleMixin, - get_api_client, + module = AnsibleK8SModule( + module_class=AnsibleModule, argument_spec=argspec(), supports_check_mode=True ) - k8s_ansible_mixin = K8sAnsibleMixin(module) - k8s_ansible_mixin.client = get_api_client(module=module) - execute_module(module, k8s_ansible_mixin) + client = get_api_client(module=module) + svc = K8sService(client, module) + execute_module(svc) if __name__ == "__main__": From 8171c994df061bf244aa3b422df15f78e92f5d92 Mon Sep 17 00:00:00 2001 From: Mike Graves Date: Thu, 20 Jan 2022 10:40:25 -0500 Subject: [PATCH 12/24] [backport/2.2] Migrate k8s_cp module to new refactored code (#329) Co-authored-by: Alina Buzachis --- plugins/module_utils/copy.py | 14 +++++++++--- plugins/module_utils/k8s/core.py | 4 ++++ plugins/modules/k8s_cp.py | 38 +++++++++++++++----------------- 3 files changed, 33 insertions(+), 23 deletions(-) diff --git a/plugins/module_utils/copy.py b/plugins/module_utils/copy.py index 0b7b58a0..3a335f6b 100644 --- a/plugins/module_utils/copy.py +++ b/plugins/module_utils/copy.py @@ -24,6 +24,9 @@ from abc import ABCMeta, abstractmethod import tarfile # from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import AnsibleModule +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import ( + CoreException, +) from ansible.module_utils._text import to_native try: @@ -376,12 +379,17 @@ class K8SCopyToPod(K8SCopy): ) -def check_pod(k8s_ansible_mixin, module): - resource = k8s_ansible_mixin.find_resource("Pod", None, True) +def check_pod(svc): + module = svc.module namespace = module.params.get("namespace") name = module.params.get("pod") container = module.params.get("container") + try: + resource = svc.find_resource("Pod", None, True) + except CoreException as e: + module.fail_json(msg=to_native(e)) + def _fail(exc): arg = {} if hasattr(exc, "body"): @@ -398,7 +406,7 @@ def check_pod(k8s_ansible_mixin, module): module.fail_json(msg=msg, **arg) try: - result = resource.get(name=name, namespace=namespace) + result = svc.client.get(resource, name=name, namespace=namespace) containers = [ c["name"] for c in result.to_dict()["status"]["containerStatuses"] ] diff --git a/plugins/module_utils/k8s/core.py b/plugins/module_utils/k8s/core.py index 2cff9563..0deca7eb 100644 --- a/plugins/module_utils/k8s/core.py +++ b/plugins/module_utils/k8s/core.py @@ -15,6 +15,7 @@ class AnsibleK8SModule: default_settings = { "check_k8s": True, + "check_pyyaml": True, "module_class": AnsibleModule, } @@ -33,6 +34,9 @@ class AnsibleK8SModule: self.requires("kubernetes") self.has_at_least("kubernetes", "12.0.0", warn=True) + if self.settings["check_pyyaml"]: + self.requires("pyyaml") + @property def check_mode(self): return self._module.check_mode diff --git a/plugins/modules/k8s_cp.py b/plugins/modules/k8s_cp.py index 11111a1d..c14b918d 100644 --- a/plugins/modules/k8s_cp.py +++ b/plugins/modules/k8s_cp.py @@ -142,6 +142,16 @@ import copy from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import ( AnsibleModule, ) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.client import ( + get_api_client, +) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.core import ( + AnsibleK8SModule, +) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.service import ( + K8sService, +) + from ansible_collections.kubernetes.core.plugins.module_utils.args_common import ( AUTH_ARG_SPEC, ) @@ -171,23 +181,9 @@ def argspec(): def execute_module(module): - - from ansible_collections.kubernetes.core.plugins.module_utils.common import ( - K8sAnsibleMixin, - get_api_client, - ) - - k8s_ansible_mixin = K8sAnsibleMixin(module, pyyaml_required=False) - k8s_ansible_mixin.check_library_version() - - k8s_ansible_mixin.module = module - k8s_ansible_mixin.argspec = module.argument_spec - k8s_ansible_mixin.params = k8s_ansible_mixin.module.params - k8s_ansible_mixin.fail_json = k8s_ansible_mixin.module.fail_json - k8s_ansible_mixin.fail = k8s_ansible_mixin.module.fail_json - - k8s_ansible_mixin.client = get_api_client(module=module) - containers = check_pod(k8s_ansible_mixin, module) + client = get_api_client(module=module) + svc = K8sService(client, module) + containers = check_pod(svc) if len(containers) > 1 and module.params.get("container") is None: module.fail_json( msg="Pod contains more than 1 container, option 'container' should be set" @@ -195,9 +191,9 @@ def execute_module(module): state = module.params.get("state") if state == "to_pod": - k8s_copy = K8SCopyToPod(module, k8s_ansible_mixin.client) + k8s_copy = K8SCopyToPod(module, client) else: - k8s_copy = K8SCopyFromPod(module, k8s_ansible_mixin.client) + k8s_copy = K8SCopyFromPod(module, client) try: k8s_copy.run() @@ -206,8 +202,10 @@ def execute_module(module): def main(): - module = AnsibleModule( + module = AnsibleK8SModule( + module_class=AnsibleModule, argument_spec=argspec(), + check_pyyaml=False, mutually_exclusive=[("local_path", "content")], required_if=[("state", "from_pod", ["local_path"])], required_one_of=[["local_path", "content"]], From 08a3d951d02c6371ed2d9b77408e639f064fbbe4 Mon Sep 17 00:00:00 2001 From: Mike Graves Date: Mon, 24 Jan 2022 10:42:40 -0500 Subject: [PATCH 13/24] Move module dependency functions outside of module (#342) Move module dependency functions outside of module SUMMARY This moves the has_at_least and requires functions that had been on the module to top level functions. The functions on the module now call these with a few added bits of functionality. Moving these functions to the top level and removing their requirement on having a module makes them usable in situations where we may not yet have a module, such as during client creation. ISSUE TYPE COMPONENT NAME ADDITIONAL INFORMATION Reviewed-by: Alina Buzachis Reviewed-by: None --- plugins/module_utils/k8s/client.py | 19 +-- plugins/module_utils/k8s/core.py | 134 ++++++++++-------- .../targets/k8s_validate/tasks/main.yml | 2 +- 3 files changed, 83 insertions(+), 72 deletions(-) diff --git a/plugins/module_utils/k8s/client.py b/plugins/module_utils/k8s/client.py index 1dc674fb..4cfac198 100644 --- a/plugins/module_utils/k8s/client.py +++ b/plugins/module_utils/k8s/client.py @@ -3,7 +3,6 @@ import os import hashlib -from distutils.version import LooseVersion from typing import Any, Dict, List, Optional from ansible.module_utils.six import iteritems, string_types @@ -13,6 +12,7 @@ from ansible_collections.kubernetes.core.plugins.module_utils.args_common import AUTH_ARG_SPEC, AUTH_PROXY_HEADERS_SPEC, ) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.core import requires try: from ansible_collections.kubernetes.core.plugins.module_utils import ( @@ -40,20 +40,9 @@ except ImportError: pass -module = None _pool = {} -def _requires_kubernetes_at_least(version: str): - if module: - module.requires("kubernetes", version) - else: - if LooseVersion(kubernetes.__version__) < LooseVersion(version): - raise Exception( - f"kubernetes >= {version} is required to use in-memory kubeconfig." - ) - - def _create_auth_spec(module=None, **kwargs) -> Dict: auth: Dict = {} # If authorization variables aren't defined, look for them in environment variables @@ -97,7 +86,6 @@ def _load_config(auth: Dict) -> None: if isinstance(kubeconfig, string_types): kubernetes.config.load_kube_config(config_file=kubeconfig, **optional_arg) elif isinstance(kubeconfig, dict): - _requires_kubernetes_at_least("17.17.0") kubernetes.config.load_kube_config_from_dict( config_dict=kubeconfig, **optional_arg ) @@ -241,6 +229,11 @@ class K8SClient: def get_api_client(module=None, **kwargs: Optional[Any]) -> K8SClient: auth_spec = _create_auth_spec(module, **kwargs) + if isinstance(auth_spec.get("kubeconfig"), dict): + if module: + module.requires("kubernetes", "17.17.0", "to use in-memory config") + else: + requires("kubernetes", "17.17.0", "to use in-memory config") configuration = _create_configuration(auth_spec) client = create_api_client(configuration) diff --git a/plugins/module_utils/k8s/core.py b/plugins/module_utils/k8s/core.py index 0deca7eb..4365922a 100644 --- a/plugins/module_utils/k8s/core.py +++ b/plugins/module_utils/k8s/core.py @@ -3,6 +3,7 @@ from typing import Optional from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import missing_required_lib +from ansible.module_utils.common.text.converters import to_text class AnsibleK8SModule: @@ -68,67 +69,84 @@ class AnsibleK8SModule: def fail_json(self, *args, **kwargs): return self._module.fail_json(*args, **kwargs) - def _gather_versions(self) -> dict: - versions = {} - try: - import jsonpatch - - versions["jsonpatch"] = jsonpatch.__version__ - except ImportError: - pass - - try: - import kubernetes - - versions["kubernetes"] = kubernetes.__version__ - except ImportError: - pass - - try: - import yaml - - versions["pyyaml"] = yaml.__version__ - except ImportError: - pass - - return versions - def has_at_least( self, dependency: str, minimum: Optional[str] = None, warn: bool = False ) -> bool: - """Check if a specific dependency is present at a minimum version. - - If a minimum version is not specified it will check only that the - dependency is present. Additionally, if ``warn`` is ``True``, a warning - will be emitted if the actual version is less than the specified - minimum version. - """ - dependencies = self._gather_versions() - current = dependencies.get(dependency) - if current is not None: - if minimum is None: - return True - supported = LooseVersion(current) >= LooseVersion(minimum) - if not supported and warn: - self.warn( - "{0}<{1} is not supported or tested. Some features may not work.".format( - dependency, minimum - ) + supported = has_at_least(dependency, minimum) + if not supported and warn: + self.warn( + "{0}<{1} is not supported or tested. Some features may not work.".format( + dependency, minimum ) - return supported - return False + ) + return supported - def requires(self, dependency: str, minimum: Optional[str] = None) -> None: - """Fail if a specific dependency is not present at a minimum version. + def requires( + self, + dependency: str, + minimum: Optional[str] = None, + reason: Optional[str] = None, + ) -> None: + try: + requires(dependency, minimum, reason=reason) + except Exception as e: + self.fail_json(msg=to_text(e)) - If a minimum version is not specified it will require only that the - dependency is present. This function calls ``fail_json()`` when the - dependency is not found at the required version and will stop module - execution. - """ - if not self.has_at_least(dependency, minimum): - if minimum is not None: - lib = "{0}>={1}".format(dependency, minimum) - else: - lib = dependency - self._module.fail_json(msg=missing_required_lib(lib)) + +def gather_versions() -> dict: + versions = {} + try: + import jsonpatch + + versions["jsonpatch"] = jsonpatch.__version__ + except ImportError: + pass + + try: + import kubernetes + + versions["kubernetes"] = kubernetes.__version__ + except ImportError: + pass + + try: + import yaml + + versions["pyyaml"] = yaml.__version__ + except ImportError: + pass + + return versions + + +def has_at_least(dependency: str, minimum: Optional[str] = None) -> bool: + """Check if a specific dependency is present at a minimum version. + + If a minimum version is not specified it will check only that the + dependency is present. + """ + dependencies = gather_versions() + current = dependencies.get(dependency) + if current is not None: + if minimum is None: + return True + supported = LooseVersion(current) >= LooseVersion(minimum) + return supported + return False + + +def requires( + dependency: str, minimum: Optional[str] = None, reason: Optional[str] = None +) -> None: + """Fail if a specific dependency is not present at a minimum version. + + If a minimum version is not specified it will require only that the + dependency is present. This function raises an exception when the + dependency is not found at the required version. + """ + if not has_at_least(dependency, minimum): + if minimum is not None: + lib = "{0}>={1}".format(dependency, minimum) + else: + lib = dependency + raise Exception(missing_required_lib(lib, reason=reason)) diff --git a/tests/integration/targets/k8s_validate/tasks/main.yml b/tests/integration/targets/k8s_validate/tasks/main.yml index f910c608..b05e2a56 100644 --- a/tests/integration/targets/k8s_validate/tasks/main.yml +++ b/tests/integration/targets/k8s_validate/tasks/main.yml @@ -201,7 +201,7 @@ - name: assert that task failed with proper message assert: that: - - '"kubernetes >= 17.17.0 is required" in _result.module_stderr' + - '"This is required to use in-memory config." in _result.msg' when: - _stat.stat.exists - _stat.stat.readable From 61faa1079ec49ff259c9724bf98354112e58db61 Mon Sep 17 00:00:00 2001 From: Alina Buzachis Date: Tue, 25 Jan 2022 02:33:44 +0100 Subject: [PATCH 14/24] [backport/2.2] Update k8s_scale to use module_utils refactored code --- plugins/modules/k8s_scale.py | 142 +++++++++++++++++++---------------- 1 file changed, 76 insertions(+), 66 deletions(-) diff --git a/plugins/modules/k8s_scale.py b/plugins/modules/k8s_scale.py index f0c209f3..5e5cc92e 100644 --- a/plugins/modules/k8s_scale.py +++ b/plugins/modules/k8s_scale.py @@ -143,6 +143,14 @@ result: import copy +try: + from kubernetes.dynamic.exceptions import NotFoundError +except ImportError: + # Handled in module setup + pass + +from ansible.module_utils._text import to_native + from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import ( AnsibleModule, ) @@ -151,7 +159,25 @@ from ansible_collections.kubernetes.core.plugins.module_utils.args_common import RESOURCE_ARG_SPEC, NAME_ARG_SPEC, ) - +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.client import ( + get_api_client, +) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.core import ( + AnsibleK8SModule, +) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import ( + CoreException, + ResourceTimeout, +) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.service import ( + K8sService, +) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.waiter import ( + get_waiter, +) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.resource import ( + create_definitions, +) SCALE_ARG_SPEC = { "replicas": {"type": "int", "required": True}, @@ -163,27 +189,20 @@ SCALE_ARG_SPEC = { } -def execute_module( - module, - k8s_ansible_mixin, -): - k8s_ansible_mixin.set_resource_definitions(module) - - definition = k8s_ansible_mixin.resource_definitions[0] - - name = definition["metadata"]["name"] - namespace = definition["metadata"].get("namespace") - api_version = definition["apiVersion"] - kind = definition["kind"] +def execute_module(client, module): current_replicas = module.params.get("current_replicas") replicas = module.params.get("replicas") resource_version = module.params.get("resource_version") - + definitions = create_definitions(module.params) + definition = definitions[0] + name = definition["metadata"].get("name") + namespace = definition["metadata"].get("namespace") + api_version = definition["apiVersion"] + kind = definition["kind"] label_selectors = module.params.get("label_selectors") if not label_selectors: label_selectors = [] continue_on_error = module.params.get("continue_on_error") - wait = module.params.get("wait") wait_time = module.params.get("wait_timeout") wait_sleep = module.params.get("wait_sleep") @@ -195,12 +214,8 @@ def execute_module( if wait: return_attributes["duration"] = 0 - resource = k8s_ansible_mixin.find_resource(kind, api_version, fail=True) - - from ansible_collections.kubernetes.core.plugins.module_utils.common import ( - NotFoundError, - ) - + svc = K8sService(client, module) + resource = svc.find_resource(kind, api_version, fail=True) multiple_scale = False try: existing = resource.get( @@ -211,11 +226,10 @@ def execute_module( multiple_scale = len(existing_items) > 1 else: existing_items = [existing] - except NotFoundError as exc: - module.fail_json( - msg="Failed to retrieve requested object: {0}".format(exc), - error=exc.value.get("status"), - ) + except NotFoundError as e: + reason = e.body if hasattr(e, "body") else e + msg = "Failed to retrieve requested object: {0}".format(reason) + raise CoreException(msg) from e if multiple_scale: # when scaling multiple resource, the 'result' is changed to 'results' and is a list @@ -278,23 +292,26 @@ def execute_module( if module.check_mode: result["result"] = existing.to_dict() else: - result["result"] = resource.patch(existing.to_dict()).to_dict() + result["result"] = client.patch(resource, existing.to_dict()).to_dict() else: - result = scale( - module, - k8s_ansible_mixin, - resource, - existing, - replicas, - wait, - wait_time, - wait_sleep, - ) + try: + result = scale( + client, + svc, + resource, + existing, + replicas, + wait, + wait_time, + wait_sleep, + ) + except CoreException as e: + module.fail_json(msg=to_native(e)) changed = changed or result["changed"] else: name = existing.metadata.name namespace = existing.metadata.namespace - existing = resource.get(name=name, namespace=namespace) + existing = client.get(resource, name=name, namespace=namespace) result = {"changed": False, "result": existing.to_dict()} if module._diff: result["diff"] = {} @@ -320,22 +337,16 @@ def argspec(): def scale( - module, - k8s_ansible_mixin, - resource, - existing_object, - replicas, - wait, - wait_time, - wait_sleep, + client, svc, resource, existing_object, replicas, wait, wait_time, wait_sleep, ): + module = svc.module name = existing_object.metadata.name namespace = existing_object.metadata.namespace kind = existing_object.kind if not hasattr(resource, "scale"): - module.fail_json( - msg="Cannot perform scale on resource of kind {0}".format(resource.kind) + raise CoreException( + "Cannot perform scale on resource of kind {0}".format(resource.kind) ) scale_obj = { @@ -344,32 +355,35 @@ def scale( "spec": {"replicas": replicas}, } - existing = resource.get(name=name, namespace=namespace) + existing = client.get(resource, name=name, namespace=namespace) result = dict() if module.check_mode: k8s_obj = copy.deepcopy(existing.to_dict()) k8s_obj["spec"]["replicas"] = replicas - match, diffs = k8s_ansible_mixin.diff_objects(existing.to_dict(), k8s_obj) + match, diffs = svc.diff_objects(existing.to_dict(), k8s_obj) if wait: result["duration"] = 0 result["result"] = k8s_obj else: try: resource.scale.patch(body=scale_obj) - except Exception as exc: - module.fail_json(msg="Scale request failed: {0}".format(exc)) + except Exception as e: + reason = e.body if hasattr(e, "body") else e + msg = "Scale request failed: {0}".format(reason) + raise CoreException(msg) from e - k8s_obj = resource.get(name=name, namespace=namespace).to_dict() + k8s_obj = client.get(resource, name=name, namespace=namespace).to_dict() result["result"] = k8s_obj - if wait and not module.check_mode: - success, result["result"], result["duration"] = k8s_ansible_mixin.wait( - resource, scale_obj, wait_sleep, wait_time + if wait: + waiter = get_waiter(svc.client, resource) + success, result["result"], result["duration"] = waiter.wait( + timeout=wait_time, sleep=wait_sleep, name=name, namespace=namespace, ) if not success: - module.fail_json(msg="Resource scaling timed out", **result) + raise ResourceTimeout("Resource scaling timed out", **result) - match, diffs = k8s_ansible_mixin.diff_objects(existing.to_dict(), k8s_obj) + match, diffs = svc.diff_objects(existing.to_dict(), k8s_obj) result["changed"] = not match if module._diff: result["diff"] = diffs @@ -381,19 +395,15 @@ def main(): mutually_exclusive = [ ("resource_definition", "src"), ] - module = AnsibleModule( + module = AnsibleK8SModule( + module_class=AnsibleModule, argument_spec=argspec(), mutually_exclusive=mutually_exclusive, supports_check_mode=True, ) - from ansible_collections.kubernetes.core.plugins.module_utils.common import ( - K8sAnsibleMixin, - get_api_client, - ) - k8s_ansible_mixin = K8sAnsibleMixin(module) - k8s_ansible_mixin.client = get_api_client(module=module) - execute_module(module, k8s_ansible_mixin) + client = get_api_client(module=module) + execute_module(client, module) if __name__ == "__main__": From 349e9f473a240a2aa3803cfa25e1533532b70deb Mon Sep 17 00:00:00 2001 From: Alina Buzachis Date: Tue, 25 Jan 2022 18:17:03 +0100 Subject: [PATCH 15/24] Refactor k8s_service to use new module_utils code (#327) Refactor k8s_service to use new module_utils code SUMMARY Refactor k8s_service to use new module_utils code ISSUE TYPE Feature Pull Request COMPONENT NAME k8s_service Reviewed-by: Mike Graves Reviewed-by: Alina Buzachis Reviewed-by: None --- plugins/modules/k8s_service.py | 69 +++++++++++++++++++++++++--------- 1 file changed, 52 insertions(+), 17 deletions(-) diff --git a/plugins/modules/k8s_service.py b/plugins/modules/k8s_service.py index 1e992f7b..4b931367 100644 --- a/plugins/modules/k8s_service.py +++ b/plugins/modules/k8s_service.py @@ -154,6 +154,19 @@ from ansible_collections.kubernetes.core.plugins.module_utils.args_common import COMMON_ARG_SPEC, RESOURCE_ARG_SPEC, ) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.core import ( + AnsibleK8SModule, +) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.client import ( + get_api_client, +) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.service import ( + K8sService, +) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.resource import ( + create_definitions, +) + SERVICE_ARG_SPEC = { "apply": {"type": "bool", "default": False}, @@ -195,10 +208,35 @@ def argspec(): return argument_spec -def execute_module(module, k8s_ansible_mixin): - """Module execution""" - k8s_ansible_mixin.set_resource_definitions(module) +def perform_action(svc, resource, definition, params): + state = params.get("state", None) + result = {} + existing = svc.retrieve(resource, definition) + + if state == "absent": + result = svc.delete(resource, definition, existing) + result["method"] = "delete" + else: + if params.get("apply"): + result = svc.apply(resource, definition, existing) + result["method"] = "apply" + elif not existing: + result = svc.create(resource, definition) + result["method"] = "create" + elif params.get("force", False): + result = svc.replace(resource, definition, existing) + result["method"] = "replace" + else: + result = svc.update(resource, definition, existing) + result["method"] = "update" + + return result + + +def execute_module(svc): + """ Module execution """ + module = svc.module api_version = "v1" selector = module.params.get("selector") service_type = module.params.get("type") @@ -218,28 +256,25 @@ def execute_module(module, k8s_ansible_mixin): def_meta["name"] = module.params.get("name") def_meta["namespace"] = module.params.get("namespace") - # 'resource_definition:' has lower priority than module parameters - definition = dict( - merge_dicts(k8s_ansible_mixin.resource_definitions[0], definition) - ) + definitions = create_definitions(module.params) - resource = k8s_ansible_mixin.find_resource("Service", api_version, fail=True) - definition = k8s_ansible_mixin.set_defaults(resource, definition) - result = k8s_ansible_mixin.perform_action(resource, definition) + # 'resource_definition:' has lower priority than module parameters + definition = dict(merge_dicts(definitions[0], definition)) + resource = svc.find_resource("Service", api_version, fail=True) + + result = perform_action(svc, resource, definition, module.params) module.exit_json(**result) def main(): - module = AnsibleModule(argument_spec=argspec(), supports_check_mode=True) - from ansible_collections.kubernetes.core.plugins.module_utils.common import ( - K8sAnsibleMixin, - get_api_client, + module = AnsibleK8SModule( + module_class=AnsibleModule, argument_spec=argspec(), supports_check_mode=True, ) - k8s_ansible_mixin = K8sAnsibleMixin(module) - k8s_ansible_mixin.client = get_api_client(module=module) - execute_module(module, k8s_ansible_mixin) + client = get_api_client(module=module) + svc = K8sService(client, module) + execute_module(svc) if __name__ == "__main__": From b62ea00ebfa43b07cab531447a41f8046ffb4819 Mon Sep 17 00:00:00 2001 From: Mike Graves Date: Wed, 26 Jan 2022 10:24:52 -0500 Subject: [PATCH 16/24] Refactor k8s_cluster_info to use new module_utils code (#325) Refactor k8s_cluster_info to use new module_utils code SUMMARY Refactor k8s_cluster_info to use new module_utils code ISSUE TYPE COMPONENT NAME k8s_cluster_info ADDITIONAL INFORMATION Reviewed-by: Alina Buzachis Reviewed-by: None --- plugins/modules/k8s_cluster_info.py | 36 +++++++++++------------------ 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/plugins/modules/k8s_cluster_info.py b/plugins/modules/k8s_cluster_info.py index e84b9eb7..1fb53713 100644 --- a/plugins/modules/k8s_cluster_info.py +++ b/plugins/modules/k8s_cluster_info.py @@ -141,36 +141,29 @@ apis: import copy -import traceback from collections import defaultdict -HAS_K8S = False try: from ansible_collections.kubernetes.core.plugins.module_utils.client.resource import ( ResourceList, ) +except ImportError: + # Handled during module setup + pass - HAS_K8S = True -except ImportError as e: - K8S_IMP_ERR = e - K8S_IMP_EXC = traceback.format_exc() - -from ansible.module_utils._text import to_native -from ansible.module_utils.basic import missing_required_lib -from ansible.module_utils.parsing.convert_bool import boolean from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import ( AnsibleModule, ) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.core import ( + AnsibleK8SModule, +) from ansible_collections.kubernetes.core.plugins.module_utils.args_common import ( AUTH_ARG_SPEC, ) def execute_module(module, client): - invalidate_cache = boolean( - module.params.get("invalidate_cache", True), strict=False - ) - if invalidate_cache: + if module.params.get("invalidate_cache"): client.resources.invalidate_cache() results = defaultdict(dict) for resource in list(client.resources): @@ -204,7 +197,7 @@ def execute_module(module, client): version_info = { "client": version, - "server": client.version, + "server": client.client.version, } module.exit_json( changed=False, apis=results, connection=connection, version=version_info @@ -218,14 +211,11 @@ def argspec(): def main(): - module = AnsibleModule(argument_spec=argspec(), supports_check_mode=True) - if not HAS_K8S: - module.fail_json( - msg=missing_required_lib("kubernetes"), - exception=K8S_IMP_EXC, - error=to_native(K8S_IMP_ERR), - ) - from ansible_collections.kubernetes.core.plugins.module_utils.common import ( + module = AnsibleK8SModule( + module_class=AnsibleModule, argument_spec=argspec(), supports_check_mode=True + ) + + from ansible_collections.kubernetes.core.plugins.module_utils.k8s.client import ( get_api_client, ) From 58a0fb16059fbd706671c8922dc11dffc9578c74 Mon Sep 17 00:00:00 2001 From: Alina Buzachis Date: Fri, 4 Feb 2022 15:49:43 +0100 Subject: [PATCH 17/24] Refactor k8s_exec to use new module_utils code (#328) * Refactor k8s_exec to use new module_utils code Signed-off-by: Alina Buzachis * Fix client Signed-off-by: Alina Buzachis --- plugins/modules/k8s_exec.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/plugins/modules/k8s_exec.py b/plugins/modules/k8s_exec.py index 646e1f9e..a1901321 100644 --- a/plugins/modules/k8s_exec.py +++ b/plugins/modules/k8s_exec.py @@ -138,6 +138,12 @@ from ansible.module_utils._text import to_native from ansible_collections.kubernetes.core.plugins.module_utils.common import ( AUTH_ARG_SPEC, ) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.core import ( + AnsibleK8SModule, +) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.client import ( + get_api_client, +) try: from kubernetes.client.apis import core_v1_api @@ -157,10 +163,9 @@ def argspec(): return spec -def execute_module(module, k8s_ansible_mixin): - +def execute_module(module, client): # Load kubernetes.client.Configuration - api = core_v1_api.CoreV1Api(k8s_ansible_mixin.client.client) + api = core_v1_api.CoreV1Api(client.client) # hack because passing the container as None breaks things optional_kwargs = {} @@ -228,18 +233,15 @@ def execute_module(module, k8s_ansible_mixin): def main(): - module = AnsibleModule( + module = AnsibleK8SModule( + module_class=AnsibleModule, + check_pyyaml=False, argument_spec=argspec(), supports_check_mode=True, ) - from ansible_collections.kubernetes.core.plugins.module_utils.common import ( - K8sAnsibleMixin, - get_api_client, - ) - k8s_ansible_mixin = K8sAnsibleMixin(module) - k8s_ansible_mixin.client = get_api_client(module=module) - execute_module(module, k8s_ansible_mixin) + client = get_api_client(module) + execute_module(module, client.client) if __name__ == "__main__": From 3bf147580f75cae83072f04c4d04fe904b67b100 Mon Sep 17 00:00:00 2001 From: Alina Buzachis Date: Fri, 4 Feb 2022 17:14:21 +0100 Subject: [PATCH 18/24] Migrate k8s (#311) * Use refactored module_utils Signed-off-by: Alina Buzachis * Fix runner Signed-off-by: Alina Buzachis * Fix runner Signed-off-by: Alina Buzachis * Update runner.py * black runner Signed-off-by: Alina Buzachis * Fix units Signed-off-by: Alina Buzachis * Fix ResourceTimeout Signed-off-by: Alina Buzachis * Attempt to fix 'Create custom resource' Signed-off-by: Alina Buzachis * Update svc.find_resource(..., fail=True) Signed-off-by: Alina Buzachis * Attempt to fix integration tests Signed-off-by: Alina Buzachis * Fix apiVersion for Job Signed-off-by: Alina Buzachis * Fix crd Signed-off-by: Alina Buzachis * Add exception = None Signed-off-by: Alina Buzachis * Fix apiVersion for definition Signed-off-by: Alina Buzachis * Fix assert Signed-off-by: Alina Buzachis * Fix returned results Signed-off-by: Alina Buzachis * Update runner to return results accordingly Signed-off-by: Alina Buzachis * Fix assert Signed-off-by: Alina Buzachis * Add validate-missing Signed-off-by: Alina Buzachis * Update client.py * Fix failures * Fix black formatting Co-authored-by: Mike Graves --- plugins/module_utils/k8s/client.py | 6 +- plugins/module_utils/k8s/core.py | 7 ++ plugins/module_utils/k8s/runner.py | 51 +++++++++---- plugins/module_utils/k8s/service.py | 73 ++++++++++++------- plugins/module_utils/k8s/waiter.py | 2 +- plugins/modules/k8s.py | 41 +++-------- .../integration/targets/k8s_gc/tasks/main.yml | 2 +- .../targets/k8s_merge_type/tasks/main.yml | 1 + .../targets/k8s_patched/tasks/main.yml | 2 +- .../targets/k8s_scale/tasks/main.yml | 2 +- .../targets/k8s_validate/tasks/main.yml | 2 +- tests/unit/module_utils/test_service.py | 9 +-- 12 files changed, 114 insertions(+), 84 deletions(-) diff --git a/plugins/module_utils/k8s/client.py b/plugins/module_utils/k8s/client.py index 4cfac198..0cef6c50 100644 --- a/plugins/module_utils/k8s/client.py +++ b/plugins/module_utils/k8s/client.py @@ -203,8 +203,10 @@ class K8SClient: params["dry_run"] = True return params - def validate(self, resource, **params): - pass + def validate( + self, resource, version: Optional[str] = None, strict: Optional[bool] = False + ): + return self.client.validate(resource, version, strict) def get(self, resource, **params): return resource.get(**params) diff --git a/plugins/module_utils/k8s/core.py b/plugins/module_utils/k8s/core.py index 4365922a..ac495ec9 100644 --- a/plugins/module_utils/k8s/core.py +++ b/plugins/module_utils/k8s/core.py @@ -109,6 +109,13 @@ def gather_versions() -> dict: except ImportError: pass + try: + import kubernetes_validate + + versions["kubernetes-validate"] = kubernetes_validate.__version__ + except ImportError: + pass + try: import yaml diff --git a/plugins/module_utils/k8s/runner.py b/plugins/module_utils/k8s/runner.py index 2c9379d4..bce2f7ff 100644 --- a/plugins/module_utils/k8s/runner.py +++ b/plugins/module_utils/k8s/runner.py @@ -3,6 +3,8 @@ from typing import Dict +from ansible.module_utils._text import to_native + from ansible_collections.kubernetes.core.plugins.module_utils.k8s.client import ( get_api_client, ) @@ -15,7 +17,6 @@ from ansible_collections.kubernetes.core.plugins.module_utils.k8s.service import from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import ( CoreException, ) - from ansible_collections.kubernetes.core.plugins.module_utils.selector import ( LabelSelectorFilter, ) @@ -42,32 +43,40 @@ def validate(client, module, resource): def run_module(module) -> None: results = [] - + changed = False client = get_api_client(module) svc = K8sService(client, module) definitions = create_definitions(module.params) for definition in definitions: - module.warnings = [] + result = {"changed": False, "result": {}, "warnings": []} + warnings = [] - if module.params["validate"] is not None: - module.warnings = validate(client, module, definition) + if module.params.get("validate") is not None: + warnings = validate(client, module, definition) try: result = perform_action(svc, definition, module.params) except CoreException as e: - if module.warnings: - e["msg"] += "\n" + "\n ".join(module.warnings) + msg = to_native(e) + if warnings: + msg += "\n" + "\n ".join(warnings) if module.params.get("continue_on_error"): - result = {"error": "{0}".format(e)} + result["error"] = {"msg": msg} else: - module.fail_json(msg=e) - if module.warnings: - result["warnings"] = module.warnings + module.fail_json(msg=msg) + if warnings: + result.setdefault("warnings", []) + result["warnings"] += warnings + + changed |= result["changed"] results.append(result) - module.exit_json(**results) + if len(results) == 1: + module.exit_json(**results[0]) + + module.exit_json(**{"changed": changed, "result": {"results": results}}) def perform_action(svc, definition: Dict, params: Dict) -> Dict: @@ -75,9 +84,13 @@ def perform_action(svc, definition: Dict, params: Dict) -> Dict: namespace = definition["metadata"].get("namespace") label_selectors = params.get("label_selectors") state = params.get("state", None) - result = {} + kind = definition.get("kind") + api_version = definition.get("apiVersion") + result = {"changed": False, "result": {}} - resource = svc.find_resource(definition) + resource = svc.find_resource(kind, api_version, fail=True) + definition["kind"] = resource.kind + definition["apiVersion"] = resource.group_version existing = svc.retrieve(resource, definition) if state == "absent": @@ -91,7 +104,7 @@ def perform_action(svc, definition: Dict, params: Dict) -> Dict: result["msg"] = ( "resource 'kind={kind},name={name},namespace={namespace}' " "filtered by label_selectors.".format( - kind=definition["kind"], name=origin_name, namespace=namespace, + kind=kind, name=origin_name, namespace=namespace, ) ) return result @@ -100,6 +113,14 @@ def perform_action(svc, definition: Dict, params: Dict) -> Dict: result = svc.apply(resource, definition, existing) result["method"] = "apply" elif not existing: + if state == "patched": + result.setdefault("warnings", []).append( + "resource 'kind={kind},name={name}' was not found but will not be " + "created as 'state' parameter has been set to '{state}'".format( + kind=kind, name=definition["metadata"].get("name"), state=state + ) + ) + return result result = svc.create(resource, definition) result["method"] = "create" elif params.get("force", False): diff --git a/plugins/module_utils/k8s/service.py b/plugins/module_utils/k8s/service.py index ffdd289f..6a23d770 100644 --- a/plugins/module_utils/k8s/service.py +++ b/plugins/module_utils/k8s/service.py @@ -171,29 +171,35 @@ class K8sService: msg = "Failed to patch object: {0}".format(reason) raise CoreException(msg) from e - def retrieve(self, resource: Resource, definition: Dict) -> Dict: + def retrieve(self, resource: Resource, definition: Dict) -> ResourceInstance: state = self.module.params.get("state", None) append_hash = self.module.params.get("append_hash", False) name = definition["metadata"].get("name") + generate_name = definition["metadata"].get("generateName") namespace = definition["metadata"].get("namespace") label_selectors = self.module.params.get("label_selectors") - results = { - "changed": False, - "result": {}, - } - existing = None + existing: ResourceInstance = None try: # ignore append_hash for resources other than ConfigMap and Secret if append_hash and definition["kind"] in ["ConfigMap", "Secret"]: - name = "%s-%s" % (name, generate_hash(definition)) - definition["metadata"]["name"] = name - params = dict(name=name) + if name: + name = "%s-%s" % (name, generate_hash(definition)) + definition["metadata"]["name"] = name + elif generate_name: + definition["metadata"]["generateName"] = "%s-%s" % ( + generate_name, + generate_hash(definition), + ) + params = {} + if name: + params["name"] = name if namespace: params["namespace"] = namespace if label_selectors: params["label_selector"] = ",".join(label_selectors) - existing = self.client.get(resource, **params) + if "name" in params or "label_selector" in params: + existing = self.client.get(resource, **params) except (NotFoundError, MethodNotAllowedError): pass except ForbiddenError as e: @@ -210,10 +216,7 @@ class K8sService: msg = "Failed to retrieve requested object: {0}".format(reason) raise CoreException(msg) from e - if existing: - results["result"] = existing.to_dict() - - return results + return existing def find( self, @@ -345,10 +348,12 @@ class K8sService: results["result"] = k8s_obj if wait and not self.module.check_mode: - definition["metadata"].update({"name": k8s_obj["metadata"]["name"]}) waiter = get_waiter(self.client, resource, condition=wait_condition) success, results["result"], results["duration"] = waiter.wait( - timeout=wait_timeout, sleep=wait_sleep, name=name, namespace=namespace, + timeout=wait_timeout, + sleep=wait_sleep, + name=k8s_obj["metadata"]["name"], + namespace=namespace, ) results["changed"] = True @@ -358,7 +363,7 @@ class K8sService: '"{0}" "{1}": Resource creation timed out'.format( definition["kind"], origin_name ), - **results + results, ) return results @@ -431,7 +436,7 @@ class K8sService: '"{0}" "{1}": Resource apply timed out'.format( definition["kind"], origin_name ), - **results + results, ) return results @@ -493,7 +498,7 @@ class K8sService: '"{0}" "{1}": Resource replacement timed out'.format( definition["kind"], origin_name ), - **results + results, ) return results @@ -514,16 +519,25 @@ class K8sService: wait_condition = self.module.params["wait_condition"] results = {"changed": False, "result": {}} - if self.module.check_mode and not self.module.client.dry_run: + if self.module.check_mode and not self.client.dry_run: k8s_obj = dict_merge(existing.to_dict(), _encode_stringdata(definition)) else: + exception = None for merge_type in self.module.params.get("merge_type") or [ "strategic-merge", "merge", ]: - k8s_obj = self.patch_resource( - resource, definition, name, namespace, merge_type=merge_type, - ) + try: + k8s_obj = self.patch_resource( + resource, definition, name, namespace, merge_type=merge_type, + ) + exception = None + except CoreException as e: + exception = e + continue + break + if exception: + raise exception success = True results["result"] = k8s_obj @@ -545,7 +559,7 @@ class K8sService: '"{0}" "{1}": Resource update timed out'.format( definition["kind"], origin_name ), - **results + results, ) return results @@ -581,6 +595,15 @@ class K8sService: if self.module.check_mode and not self.client.dry_run: return results else: + if name: + params["name"] = name + + if namespace: + params["namespace"] = namespace + + if label_selectors: + params["label_selector"] = ",".join(label_selectors) + if delete_options: body = { "apiVersion": "v1", @@ -614,7 +637,7 @@ class K8sService: '"{0}" "{1}": Resource deletion timed out'.format( definition["kind"], origin_name ), - **results + results, ) return results diff --git a/plugins/module_utils/k8s/waiter.py b/plugins/module_utils/k8s/waiter.py index 8ab60ada..d52491f4 100644 --- a/plugins/module_utils/k8s/waiter.py +++ b/plugins/module_utils/k8s/waiter.py @@ -157,7 +157,7 @@ class Waiter: try: response = self.client.get(self.resource, **params) except NotFoundError: - pass + response = None if self.predicate(response): break if response: diff --git a/plugins/modules/k8s.py b/plugins/modules/k8s.py index d7a42472..4b3c7462 100644 --- a/plugins/modules/k8s.py +++ b/plugins/modules/k8s.py @@ -391,6 +391,12 @@ from ansible_collections.kubernetes.core.plugins.module_utils.args_common import RESOURCE_ARG_SPEC, DELETE_OPTS_ARG_SPEC, ) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.core import ( + AnsibleK8SModule, +) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.runner import ( + run_module, +) def validate_spec(): @@ -437,28 +443,6 @@ def argspec(): return argument_spec -def execute_module(module, k8s_ansible_mixin): - k8s_ansible_mixin.module = module - k8s_ansible_mixin.argspec = module.argument_spec - k8s_ansible_mixin.check_mode = k8s_ansible_mixin.module.check_mode - k8s_ansible_mixin.params = k8s_ansible_mixin.module.params - k8s_ansible_mixin.fail_json = k8s_ansible_mixin.module.fail_json - k8s_ansible_mixin.fail = k8s_ansible_mixin.module.fail_json - k8s_ansible_mixin.exit_json = k8s_ansible_mixin.module.exit_json - k8s_ansible_mixin.warn = k8s_ansible_mixin.module.warn - k8s_ansible_mixin.warnings = [] - - k8s_ansible_mixin.kind = k8s_ansible_mixin.params.get("kind") - k8s_ansible_mixin.api_version = k8s_ansible_mixin.params.get("api_version") - k8s_ansible_mixin.name = k8s_ansible_mixin.params.get("name") - k8s_ansible_mixin.generate_name = k8s_ansible_mixin.params.get("generate_name") - k8s_ansible_mixin.namespace = k8s_ansible_mixin.params.get("namespace") - - k8s_ansible_mixin.check_library_version() - k8s_ansible_mixin.set_resource_definitions(module) - k8s_ansible_mixin.execute_module() - - def main(): mutually_exclusive = [ ("resource_definition", "src"), @@ -467,19 +451,14 @@ def main(): ("template", "src"), ("name", "generate_name"), ] - module = AnsibleModule( + + module = AnsibleK8SModule( + module_class=AnsibleModule, argument_spec=argspec(), mutually_exclusive=mutually_exclusive, supports_check_mode=True, ) - from ansible_collections.kubernetes.core.plugins.module_utils.common import ( - K8sAnsibleMixin, - get_api_client, - ) - - k8s_ansible_mixin = K8sAnsibleMixin(module) - k8s_ansible_mixin.client = get_api_client(module=module) - execute_module(module, k8s_ansible_mixin) + run_module(module) if __name__ == "__main__": diff --git a/tests/integration/targets/k8s_gc/tasks/main.yml b/tests/integration/targets/k8s_gc/tasks/main.yml index 923ffc3f..a2f60c8a 100644 --- a/tests/integration/targets/k8s_gc/tasks/main.yml +++ b/tests/integration/targets/k8s_gc/tasks/main.yml @@ -5,7 +5,7 @@ # This is a job definition that runs for 10 minutes and won't gracefully # shutdown. It allows us to test foreground vs background deletion. job_definition: - apiVersion: v1 + apiVersion: batch/v1 kind: Job metadata: name: "{{ gc_name }}" diff --git a/tests/integration/targets/k8s_merge_type/tasks/main.yml b/tests/integration/targets/k8s_merge_type/tasks/main.yml index 32e41d8b..3c544c76 100644 --- a/tests/integration/targets/k8s_merge_type/tasks/main.yml +++ b/tests/integration/targets/k8s_merge_type/tasks/main.yml @@ -97,6 +97,7 @@ - name: patch service using json merge patch kubernetes.core.k8s: kind: Deployment + api_version: apps/v1 namespace: "{{ k8s_patch_namespace }}" name: "{{ k8s_merge }}" merge_type: diff --git a/tests/integration/targets/k8s_patched/tasks/main.yml b/tests/integration/targets/k8s_patched/tasks/main.yml index 6fe514d3..8846d064 100644 --- a/tests/integration/targets/k8s_patched/tasks/main.yml +++ b/tests/integration/targets/k8s_patched/tasks/main.yml @@ -52,7 +52,7 @@ assert: that: - patch_resource.changed - - patch_resource.result.results | selectattr('warning', 'defined') | list | length == 1 + - patch_resource.result.results | selectattr('warnings', 'defined') | list | length == 1 - name: Ensure namespace {{ patch_only_namespace[0] }} was patched correctly kubernetes.core.k8s_info: diff --git a/tests/integration/targets/k8s_scale/tasks/main.yml b/tests/integration/targets/k8s_scale/tasks/main.yml index d58a01bc..eb2107d3 100644 --- a/tests/integration/targets/k8s_scale/tasks/main.yml +++ b/tests/integration/targets/k8s_scale/tasks/main.yml @@ -150,7 +150,7 @@ - name: Reapply the earlier deployment k8s: definition: - api_version: apps/v1 + apiVersion: apps/v1 kind: Deployment metadata: name: scale-deploy diff --git a/tests/integration/targets/k8s_validate/tasks/main.yml b/tests/integration/targets/k8s_validate/tasks/main.yml index b05e2a56..900e6f70 100644 --- a/tests/integration/targets/k8s_validate/tasks/main.yml +++ b/tests/integration/targets/k8s_validate/tasks/main.yml @@ -42,7 +42,7 @@ - assert: that: - k8s_no_validate is failed - - "k8s_no_validate.msg == 'kubernetes-validate python library is required to validate resources'" + - "'Failed to import the required Python library (kubernetes-validate)' in k8s_no_validate.msg" - file: path: "{{ virtualenv }}" diff --git a/tests/unit/module_utils/test_service.py b/tests/unit/module_utils/test_service.py index 979f6d5c..8ef836d8 100644 --- a/tests/unit/module_utils/test_service.py +++ b/tests/unit/module_utils/test_service.py @@ -192,9 +192,8 @@ def test_service_retrieve_existing_resource(mock_pod_resource_instance): svc = K8sService(client, module) results = svc.retrieve(Mock(), pod_definition) - assert isinstance(results, dict) - assert results["changed"] is False - assert results["result"] == pod_definition + assert isinstance(results, ResourceInstance) + assert results.to_dict() == pod_definition def test_service_retrieve_no_existing_resource(): @@ -205,9 +204,7 @@ def test_service_retrieve_no_existing_resource(): svc = K8sService(client, module) results = svc.retrieve(Mock(), pod_definition) - assert isinstance(results, dict) - assert results["changed"] is False - assert results["result"] == {} + assert results is None def test_create_project_request(): From 25644ac192c419ae0ee639caee9ffdc63673e29f Mon Sep 17 00:00:00 2001 From: Mike Graves Date: Mon, 14 Feb 2022 08:19:36 -0500 Subject: [PATCH 19/24] Move diff and wait to perform_action (#375) This primarily moves the diff and wait logic from the various service methods to perform_action to eliminate code duplication. I also moved the diff_objects function out of the service object and moved most of the find_resource logic to a new resource client method. We ended up with several modules creating a service object just to use one of these methods, so it seemed to make sense to make these more accessible. --- plugins/module_utils/k8s/client.py | 25 +- plugins/module_utils/k8s/exceptions.py | 4 +- plugins/module_utils/k8s/runner.py | 74 ++++- plugins/module_utils/k8s/service.py | 363 +++++++----------------- plugins/module_utils/k8s/waiter.py | 6 +- plugins/modules/k8s_json_patch.py | 12 +- plugins/modules/k8s_scale.py | 13 +- tests/unit/module_utils/test_client.py | 5 +- tests/unit/module_utils/test_runner.py | 126 ++++++-- tests/unit/module_utils/test_service.py | 174 ++++-------- tests/unit/module_utils/test_waiter.py | 2 +- 11 files changed, 356 insertions(+), 448 deletions(-) diff --git a/plugins/module_utils/k8s/client.py b/plugins/module_utils/k8s/client.py index 0cef6c50..39f751cc 100644 --- a/plugins/module_utils/k8s/client.py +++ b/plugins/module_utils/k8s/client.py @@ -27,9 +27,15 @@ except ImportError: try: import kubernetes + from kubernetes.dynamic.exceptions import ( + ResourceNotFoundError, + ResourceNotUniqueError, + ) + from kubernetes.dynamic.resource import Resource except ImportError: - # Handled in module setup - pass + # kubernetes import error is handled in module setup + # This is defined only for the sake of Ansible's checked import requirement + Resource = Any # type: ignore try: import urllib3 @@ -198,6 +204,21 @@ class K8SClient: def resources(self) -> List[Any]: return self.client.resources + def resource(self, kind: str, api_version: str) -> Resource: + """Fetch a kubernetes client resource. + + This will attempt to find a kubernetes resource trying, in order, kind, + name, singular_name and short_names. + """ + for attribute in ["kind", "name", "singular_name"]: + try: + return self.client.resources.get( + **{"api_version": api_version, attribute: kind} + ) + except (ResourceNotFoundError, ResourceNotUniqueError): + pass + return self.client.resources.get(api_version=api_version, short_names=[kind]) + def _ensure_dry_run(self, params: Dict) -> Dict: if self.dry_run: params["dry_run"] = True diff --git a/plugins/module_utils/k8s/exceptions.py b/plugins/module_utils/k8s/exceptions.py index 38c52b8c..f3a82c6b 100644 --- a/plugins/module_utils/k8s/exceptions.py +++ b/plugins/module_utils/k8s/exceptions.py @@ -7,4 +7,6 @@ class CoreException(Exception): class ResourceTimeout(CoreException): - pass + def __init__(self, message="", result=None): + self.result = result or {} + super().__init__(message) diff --git a/plugins/module_utils/k8s/runner.py b/plugins/module_utils/k8s/runner.py index bce2f7ff..c1fe24c4 100644 --- a/plugins/module_utils/k8s/runner.py +++ b/plugins/module_utils/k8s/runner.py @@ -13,10 +13,13 @@ from ansible_collections.kubernetes.core.plugins.module_utils.k8s.resource impor ) from ansible_collections.kubernetes.core.plugins.module_utils.k8s.service import ( K8sService, + diff_objects, ) from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import ( CoreException, + ResourceTimeout, ) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.waiter import exists from ansible_collections.kubernetes.core.plugins.module_utils.selector import ( LabelSelectorFilter, ) @@ -49,7 +52,7 @@ def run_module(module) -> None: definitions = create_definitions(module.params) for definition in definitions: - result = {"changed": False, "result": {}, "warnings": []} + result = {"changed": False, "result": {}} warnings = [] if module.params.get("validate") is not None: @@ -58,18 +61,21 @@ def run_module(module) -> None: try: result = perform_action(svc, definition, module.params) except CoreException as e: - msg = to_native(e) + try: + error = e.result + except AttributeError: + error = {} + error["msg"] = to_native(e) if warnings: - msg += "\n" + "\n ".join(warnings) + error.setdefault("warnings", []).extend(warnings) + if module.params.get("continue_on_error"): - result["error"] = {"msg": msg} + result["error"] = error else: - module.fail_json(msg=msg) + module.fail_json(**error) if warnings: - result.setdefault("warnings", []) - result["warnings"] += warnings - + result.setdefault("warnings", []).extend(warnings) changed |= result["changed"] results.append(result) @@ -86,7 +92,9 @@ def perform_action(svc, definition: Dict, params: Dict) -> Dict: state = params.get("state", None) kind = definition.get("kind") api_version = definition.get("apiVersion") + result = {"changed": False, "result": {}} + instance = {} resource = svc.find_resource(kind, api_version, fail=True) definition["kind"] = resource.kind @@ -94,8 +102,10 @@ def perform_action(svc, definition: Dict, params: Dict) -> Dict: existing = svc.retrieve(resource, definition) if state == "absent": - result = svc.delete(resource, definition, existing) + instance = svc.delete(resource, definition, existing) result["method"] = "delete" + if exists(existing): + result["changed"] = True else: if label_selectors: filter_selector = LabelSelectorFilter(label_selectors) @@ -110,7 +120,7 @@ def perform_action(svc, definition: Dict, params: Dict) -> Dict: return result if params.get("apply"): - result = svc.apply(resource, definition, existing) + instance = svc.apply(resource, definition, existing) result["method"] = "apply" elif not existing: if state == "patched": @@ -121,13 +131,51 @@ def perform_action(svc, definition: Dict, params: Dict) -> Dict: ) ) return result - result = svc.create(resource, definition) + instance = svc.create(resource, definition) result["method"] = "create" + result["changed"] = True elif params.get("force", False): - result = svc.replace(resource, definition, existing) + instance = svc.replace(resource, definition, existing) result["method"] = "replace" else: - result = svc.update(resource, definition, existing) + instance = svc.update(resource, definition, existing) result["method"] = "update" + # If needed, wait and/or create diff + success = True + + if result["method"] == "delete": + # wait logic is a bit different for delete as `instance` may be a status object + if params.get("wait") and not svc.module.check_mode: + success, waited, duration = svc.wait(resource, definition) + result["duration"] = duration + else: + if params.get("wait") and not svc.module.check_mode: + success, instance, duration = svc.wait(resource, instance) + result["duration"] = duration + + if result["method"] not in ("create", "delete"): + if existing: + existing = existing.to_dict() + else: + existing = {} + match, diffs = diff_objects(existing, instance) + if match and diffs: + result.setdefault("warnings", []).append( + "No meaningful diff was generated, but the API may not be idempotent " + "(only metadata.generation or metadata.resourceVersion were changed)" + ) + result["changed"] = not match + if svc.module._diff: + result["diff"] = diffs + + result["result"] = instance + if not success: + raise ResourceTimeout( + '"{0}" "{1}": Timed out waiting on resource'.format( + definition["kind"], origin_name + ), + result, + ) + return result diff --git a/plugins/module_utils/k8s/service.py b/plugins/module_utils/k8s/service.py index 6a23d770..c2f51fc8 100644 --- a/plugins/module_utils/k8s/service.py +++ b/plugins/module_utils/k8s/service.py @@ -16,7 +16,6 @@ from ansible_collections.kubernetes.core.plugins.module_utils.k8s.waiter import from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import ( CoreException, - ResourceTimeout, ) from ansible.module_utils.common.dict_transformations import dict_merge @@ -78,18 +77,9 @@ class K8sService: def find_resource( self, kind: str, api_version: str, fail: bool = False - ) -> Optional[ResourceInstance]: - for attribute in ["kind", "name", "singular_name"]: - try: - return self.client.resources.get( - **{"api_version": api_version, attribute: kind} - ) - except (ResourceNotFoundError, ResourceNotUniqueError): - pass + ) -> Optional[Resource]: try: - return self.client.resources.get( - api_version=api_version, short_names=[kind] - ) + return self.client.resource(kind, api_version) except (ResourceNotFoundError, ResourceNotUniqueError): if fail: raise CoreException( @@ -97,6 +87,32 @@ class K8sService: % (api_version, kind) ) + def wait( + self, resource: Resource, instance: Dict + ) -> Tuple[bool, Optional[Dict], int]: + wait_sleep = self.module.params.get("wait_sleep") + wait_timeout = self.module.params.get("wait_timeout") + wait_condition = None + if self.module.params.get("wait_condition") and self.module.params[ + "wait_condition" + ].get("type"): + wait_condition = self.module.params["wait_condition"] + state = "present" + if self.module.params.get("state") == "absent": + state = "absent" + label_selectors = self.module.params.get("label_selectors") + + waiter = get_waiter( + self.client, resource, condition=wait_condition, state=state + ) + return waiter.wait( + timeout=wait_timeout, + sleep=wait_sleep, + name=instance["metadata"].get("name"), + namespace=instance["metadata"].get("namespace"), + label_selectors=label_selectors, + ) + def create_project_request(self, definition: Dict) -> Dict: definition["kind"] = "ProjectRequest" results = {"changed": False, "result": {}} @@ -116,35 +132,6 @@ class K8sService: return results - def diff_objects(self, existing: Dict, new: Dict) -> Tuple[bool, Dict]: - result: Dict = dict() - diff = recursive_diff(existing, new) - if not diff: - return True, result - - result["before"] = diff[0] - result["after"] = diff[1] - - # If only metadata.generation and metadata.resourceVersion changed, ignore it - ignored_keys = set(["generation", "resourceVersion"]) - - if list(result["after"].keys()) != ["metadata"] or list( - result["before"].keys() - ) != ["metadata"]: - return False, result - - if not set(result["after"]["metadata"].keys()).issubset(ignored_keys): - return False, result - if not set(result["before"]["metadata"].keys()).issubset(ignored_keys): - return False, result - - if hasattr(self.module, "warn"): - self.module.warn( - "No meaningful diff was generated, but the API may not be idempotent (only metadata.generation or metadata.resourceVersion were changed)" - ) - - return True, result - def patch_resource( self, resource: Resource, @@ -305,17 +292,8 @@ class K8sService: return result def create(self, resource: Resource, definition: Dict) -> Dict: - origin_name = definition["metadata"].get("name") namespace = definition["metadata"].get("namespace") name = definition["metadata"].get("name") - wait = self.module.params.get("wait") - wait_sleep = self.module.params.get("wait_sleep") - wait_timeout = self.module.params.get("wait_timeout") - wait_condition = None - if self.module.params.get("wait_condition") and self.module.params[ - "wait_condition" - ].get("type"): - wait_condition = self.module.params["wait_condition"] results = {"changed": False, "result": {}} if self.module.check_mode and not self.client.dry_run: @@ -343,30 +321,7 @@ class K8sService: reason = e.body if hasattr(e, "body") else e msg = "Failed to create object: {0}".format(reason) raise CoreException(msg) from e - - success = True - results["result"] = k8s_obj - - if wait and not self.module.check_mode: - waiter = get_waiter(self.client, resource, condition=wait_condition) - success, results["result"], results["duration"] = waiter.wait( - timeout=wait_timeout, - sleep=wait_sleep, - name=k8s_obj["metadata"]["name"], - namespace=namespace, - ) - - results["changed"] = True - - if not success: - raise ResourceTimeout( - '"{0}" "{1}": Resource creation timed out'.format( - definition["kind"], origin_name - ), - results, - ) - - return results + return k8s_obj def apply( self, @@ -374,89 +329,34 @@ class K8sService: definition: Dict, existing: Optional[ResourceInstance] = None, ) -> Dict: - apply = self.module.params.get("apply", False) - origin_name = definition["metadata"].get("name") - name = definition["metadata"].get("name") namespace = definition["metadata"].get("namespace") - wait = self.module.params.get("wait") - wait_sleep = self.module.params.get("wait_sleep") - wait_condition = None - if self.module.params.get("wait_condition") and self.module.params[ - "wait_condition" - ].get("type"): - wait_condition = self.module.params["wait_condition"] - wait_timeout = self.module.params.get("wait_timeout") - results = {"changed": False, "result": {}} - - if apply: - if self.module.check_mode and not self.client.dry_run: - ignored, patch = apply_object(resource, _encode_stringdata(definition)) - if existing: - k8s_obj = dict_merge(existing.to_dict(), patch) - else: - k8s_obj = patch - else: - try: - params = {} - if self.module.check_mode: - params["dry_run"] = "All" - k8s_obj = self.client.apply( - resource, definition, namespace=namespace, **params - ).to_dict() - except Exception as e: - reason = e.body if hasattr(e, "body") else e - msg = "Failed to apply object: {0}".format(reason) - raise CoreException(msg) from e - - success = True - results["result"] = k8s_obj - - if wait and not self.module.check_mode: - waiter = get_waiter(self.client, resource, condition=wait_condition) - success, results["result"], results["duration"] = waiter.wait( - timeout=wait_timeout, - sleep=wait_sleep, - name=name, - namespace=namespace, - ) + if self.module.check_mode and not self.client.dry_run: + ignored, patch = apply_object(resource, _encode_stringdata(definition)) if existing: - existing = existing.to_dict() + k8s_obj = dict_merge(existing.to_dict(), patch) else: - existing = {} - - match, diffs = self.diff_objects(existing, results["result"]) - results["changed"] = not match - - if self.module._diff: - results["diff"] = diffs - - if not success: - raise ResourceTimeout( - '"{0}" "{1}": Resource apply timed out'.format( - definition["kind"], origin_name - ), - results, - ) - - return results + k8s_obj = patch + else: + try: + params = {} + if self.module.check_mode: + params["dry_run"] = "All" + k8s_obj = self.client.apply( + resource, definition, namespace=namespace, **params + ).to_dict() + except Exception as e: + reason = e.body if hasattr(e, "body") else e + msg = "Failed to apply object: {0}".format(reason) + raise CoreException(msg) from e + return k8s_obj def replace( self, resource: Resource, definition: Dict, existing: ResourceInstance, ) -> Dict: append_hash = self.module.params.get("append_hash", False) name = definition["metadata"].get("name") - origin_name = definition["metadata"].get("name") namespace = definition["metadata"].get("namespace") - wait = self.module.params.get("wait") - wait_sleep = self.module.params.get("wait_sleep") - wait_timeout = self.module.params.get("wait_timeout") - wait_condition = None - if self.module.params.get("wait_condition") and self.module.params[ - "wait_condition" - ].get("type"): - wait_condition = self.module.params["wait_condition"] - results = {"changed": False, "result": {}} if self.module.check_mode and not self.module.client.dry_run: k8s_obj = _encode_stringdata(definition) @@ -477,47 +377,13 @@ class K8sService: reason = e.body if hasattr(e, "body") else e msg = "Failed to replace object: {0}".format(reason) raise CoreException(msg) from e - - match, diffs = self.diff_objects(existing.to_dict(), k8s_obj) - success = True - results["result"] = k8s_obj - - if wait and not self.module.check_mode: - waiter = get_waiter(self.client, resource, condition=wait_condition) - success, results["result"], results["duration"] = waiter.wait( - timeout=wait_timeout, sleep=wait_sleep, name=name, namespace=namespace, - ) - match, diffs = self.diff_objects(existing.to_dict(), results["result"]) - results["changed"] = not match - - if self.module._diff: - results["diff"] = diffs - - if not success: - raise ResourceTimeout( - '"{0}" "{1}": Resource replacement timed out'.format( - definition["kind"], origin_name - ), - results, - ) - - return results + return k8s_obj def update( self, resource: Resource, definition: Dict, existing: ResourceInstance ) -> Dict: name = definition["metadata"].get("name") - origin_name = definition["metadata"].get("name") namespace = definition["metadata"].get("namespace") - wait = self.module.params.get("wait") - wait_sleep = self.module.params.get("wait_sleep") - wait_timeout = self.module.params.get("wait_timeout") - wait_condition = None - if self.module.params.get("wait_condition") and self.module.params[ - "wait_condition" - ].get("type"): - wait_condition = self.module.params["wait_condition"] - results = {"changed": False, "result": {}} if self.module.check_mode and not self.client.dry_run: k8s_obj = dict_merge(existing.to_dict(), _encode_stringdata(definition)) @@ -538,31 +404,7 @@ class K8sService: break if exception: raise exception - - success = True - results["result"] = k8s_obj - - if wait and not self.module.check_mode: - waiter = get_waiter(self.client, resource, condition=wait_condition) - success, results["result"], results["duration"] = waiter.wait( - timeout=wait_timeout, sleep=wait_sleep, name=name, namespace=namespace, - ) - - match, diffs = self.diff_objects(existing.to_dict(), results["result"]) - results["changed"] = not match - - if self.module._diff: - results["diff"] = diffs - - if not success: - raise ResourceTimeout( - '"{0}" "{1}": Resource update timed out'.format( - definition["kind"], origin_name - ), - results, - ) - - return results + return k8s_obj def delete( self, @@ -572,72 +414,65 @@ class K8sService: ) -> Dict: delete_options = self.module.params.get("delete_options") label_selectors = self.module.params.get("label_selectors") - origin_name = definition["metadata"].get("name") name = definition["metadata"].get("name") namespace = definition["metadata"].get("namespace") - wait = self.module.params.get("wait") - wait_sleep = self.module.params.get("wait_sleep") - wait_timeout = self.module.params.get("wait_timeout") - results = {"changed": False, "result": {}} params = {} - def _empty_resource_list() -> bool: - if existing and existing.kind.endswith("List"): - return existing.items == [] - return False + if not exists(existing): + return {} - if not existing or _empty_resource_list(): - # The object already does not exist - return results - else: - # Delete the object - results["changed"] = True - if self.module.check_mode and not self.client.dry_run: - return results - else: - if name: - params["name"] = name + # Delete the object + if self.module.check_mode and not self.client.dry_run: + return {} - if namespace: - params["namespace"] = namespace + if name: + params["name"] = name - if label_selectors: - params["label_selector"] = ",".join(label_selectors) + if namespace: + params["namespace"] = namespace - if delete_options: - body = { - "apiVersion": "v1", - "kind": "DeleteOptions", - } - body.update(delete_options) - params["body"] = body + if label_selectors: + params["label_selector"] = ",".join(label_selectors) - if self.module.check_mode: - params["dry_run"] = "All" - try: - k8s_obj = self.client.delete(resource, **params) - results["result"] = k8s_obj.to_dict() - except Exception as e: - reason = e.body if hasattr(e, "body") else e - msg = "Failed to delete object: {0}".format(reason) - raise CoreException(msg) from e + if delete_options: + body = { + "apiVersion": "v1", + "kind": "DeleteOptions", + } + body.update(delete_options) + params["body"] = body - if wait and not self.module.check_mode: - waiter = get_waiter(self.client, resource, state="absent") - success, resource, duration = waiter.wait( - timeout=wait_timeout, - sleep=wait_sleep, - name=name, - namespace=namespace, - label_selectors=label_selectors, - ) - results["duration"] = duration - if not success: - raise ResourceTimeout( - '"{0}" "{1}": Resource deletion timed out'.format( - definition["kind"], origin_name - ), - results, - ) + if self.module.check_mode: + params["dry_run"] = "All" + try: + k8s_obj = self.client.delete(resource, **params).to_dict() + except Exception as e: + reason = e.body if hasattr(e, "body") else e + msg = "Failed to delete object: {0}".format(reason) + raise CoreException(msg) from e + return k8s_obj - return results + +def diff_objects(existing: Dict, new: Dict) -> Tuple[bool, Dict]: + result = {} + diff = recursive_diff(existing, new) + if not diff: + return True, result + + result["before"] = diff[0] + result["after"] = diff[1] + + if list(result["after"].keys()) != ["metadata"] or list( + result["before"].keys() + ) != ["metadata"]: + return False, result + + # If only metadata.generation and metadata.resourceVersion changed, ignore it + ignored_keys = set(["generation", "resourceVersion"]) + + if not set(result["after"]["metadata"].keys()).issubset(ignored_keys): + return False, result + if not set(result["before"]["metadata"].keys()).issubset(ignored_keys): + return False, result + + return True, result diff --git a/plugins/module_utils/k8s/waiter.py b/plugins/module_utils/k8s/waiter.py index d52491f4..88013527 100644 --- a/plugins/module_utils/k8s/waiter.py +++ b/plugins/module_utils/k8s/waiter.py @@ -107,7 +107,7 @@ RESOURCE_PREDICATES = { def empty_list(resource: ResourceInstance) -> bool: - return resource.kind.endswith("List") and not resource.get("items") + return resource["kind"].endswith("List") and not resource.get("items") def clock(total: int, interval: int) -> Iterator[int]: @@ -134,7 +134,7 @@ class Waiter: namespace: Optional[str] = None, label_selectors: Optional[List[str]] = None, field_selectors: Optional[List[str]] = None, - ) -> Tuple[bool, Optional[Dict], int]: + ) -> Tuple[bool, Dict, int]: params = {} if name: @@ -149,7 +149,7 @@ class Waiter: if field_selectors: params["field_selector"] = ",".join(field_selectors) - instance: Optional[Dict] = None + instance = {} response = None elapsed = 0 for i in clock(timeout, sleep): diff --git a/plugins/modules/k8s_json_patch.py b/plugins/modules/k8s_json_patch.py index eb2303ef..61595454 100644 --- a/plugins/modules/k8s_json_patch.py +++ b/plugins/modules/k8s_json_patch.py @@ -143,7 +143,7 @@ from ansible_collections.kubernetes.core.plugins.module_utils.k8s.core import ( AnsibleK8SModule, ) from ansible_collections.kubernetes.core.plugins.module_utils.k8s.service import ( - K8sService, + diff_objects, ) from ansible_collections.kubernetes.core.plugins.module_utils.k8s.waiter import ( get_waiter, @@ -194,7 +194,7 @@ def json_patch(existing, patch): return None, error -def execute_module(module, svc): +def execute_module(module, client): kind = module.params.get("kind") api_version = module.params.get("api_version") name = module.params.get("name") @@ -213,8 +213,7 @@ def execute_module(module, svc): def build_error_msg(kind, name, msg): return "%s %s: %s" % (kind, name, msg) - client = svc.client - resource = svc.find_resource(kind, api_version, fail=True) + resource = client.resource(kind, api_version) try: existing = client.get(resource, name=name, namespace=namespace) @@ -265,7 +264,7 @@ def execute_module(module, svc): success, result["result"], result["duration"] = waiter.wait( wait_timeout, wait_sleep, name, namespace ) - match, diffs = svc.diff_objects(existing.to_dict(), obj) + match, diffs = diff_objects(existing.to_dict(), obj) result["changed"] = not match if module._diff: result["diff"] = diffs @@ -285,8 +284,7 @@ def main(): module_class=AnsibleModule, argument_spec=args, supports_check_mode=True ) client = get_api_client(module) - svc = K8sService(client, module) - execute_module(module, svc) + execute_module(module, client) if __name__ == "__main__": diff --git a/plugins/modules/k8s_scale.py b/plugins/modules/k8s_scale.py index 5e5cc92e..fb370d06 100644 --- a/plugins/modules/k8s_scale.py +++ b/plugins/modules/k8s_scale.py @@ -170,7 +170,7 @@ from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions imp ResourceTimeout, ) from ansible_collections.kubernetes.core.plugins.module_utils.k8s.service import ( - K8sService, + diff_objects, ) from ansible_collections.kubernetes.core.plugins.module_utils.k8s.waiter import ( get_waiter, @@ -214,8 +214,7 @@ def execute_module(client, module): if wait: return_attributes["duration"] = 0 - svc = K8sService(client, module) - resource = svc.find_resource(kind, api_version, fail=True) + resource = client.resource(kind, api_version) multiple_scale = False try: existing = resource.get( @@ -297,7 +296,7 @@ def execute_module(client, module): try: result = scale( client, - svc, + module, resource, existing, replicas, @@ -337,9 +336,8 @@ def argspec(): def scale( - client, svc, resource, existing_object, replicas, wait, wait_time, wait_sleep, + client, module, resource, existing_object, replicas, wait, wait_time, wait_sleep, ): - module = svc.module name = existing_object.metadata.name namespace = existing_object.metadata.namespace kind = existing_object.kind @@ -361,7 +359,6 @@ def scale( if module.check_mode: k8s_obj = copy.deepcopy(existing.to_dict()) k8s_obj["spec"]["replicas"] = replicas - match, diffs = svc.diff_objects(existing.to_dict(), k8s_obj) if wait: result["duration"] = 0 result["result"] = k8s_obj @@ -383,7 +380,7 @@ def scale( if not success: raise ResourceTimeout("Resource scaling timed out", **result) - match, diffs = svc.diff_objects(existing.to_dict(), k8s_obj) + match, diffs = diff_objects(existing.to_dict(), result["result"]) result["changed"] = not match if module._diff: result["diff"] = diffs diff --git a/tests/unit/module_utils/test_client.py b/tests/unit/module_utils/test_client.py index cab9c937..8265b768 100644 --- a/tests/unit/module_utils/test_client.py +++ b/tests/unit/module_utils/test_client.py @@ -43,7 +43,10 @@ _temp_files = [] def _remove_temp_file(): for f in _temp_files: - os.remove(f) + try: + os.remove(f) + except FileNotFoundError: + pass def _create_temp_file(content=""): diff --git a/tests/unit/module_utils/test_runner.py b/tests/unit/module_utils/test_runner.py index 21579c2c..45c6f29a 100644 --- a/tests/unit/module_utils/test_runner.py +++ b/tests/unit/module_utils/test_runner.py @@ -1,5 +1,8 @@ import pytest -from unittest.mock import MagicMock, call +from copy import deepcopy +from unittest.mock import Mock + +from kubernetes.dynamic.resource import ResourceInstance from ansible_collections.kubernetes.core.plugins.module_utils.k8s.runner import ( perform_action, @@ -24,30 +27,109 @@ definition = { }, } +modified_def = deepcopy(definition) +modified_def["metadata"]["labels"]["environment"] = "testing" + @pytest.mark.parametrize( - "params, expected", + "action, params, existing, instance, expected", [ - ({"state": "absent"}, call.__setitem__("method", "delete")), - ({"apply": True}, call.__setitem__("method", "apply")), - ({"force": True}, call.__setitem__("method", "replace")), - ({"apply": False}, call.__setitem__("method", "update")), - ({}, call.__setitem__("method", "update")), + ( + "delete", + {"state": "absent"}, + {}, + {}, + {"changed": False, "method": "delete", "result": {}}, + ), + ( + "delete", + {"state": "absent"}, + definition, + {"kind": "Status"}, + {"changed": True, "method": "delete", "result": {"kind": "Status"}}, + ), + ( + "apply", + {"apply": "yes"}, + {}, + definition, + {"changed": True, "method": "apply", "result": definition}, + ), + ( + "create", + {"state": "patched"}, + {}, + {}, + { + "changed": False, + "result": {}, + "warnings": [ + "resource 'kind=Pod,name=foo' was not found but will not be created as 'state' parameter has been set to 'patched'" + ], + }, + ), + ( + "create", + {}, + {}, + definition, + {"changed": True, "method": "create", "result": definition}, + ), + ( + "replace", + {"force": "yes"}, + definition, + definition, + {"changed": False, "method": "replace", "result": definition}, + ), + ( + "replace", + {"force": "yes"}, + definition, + modified_def, + {"changed": True, "method": "replace", "result": modified_def}, + ), + ( + "update", + {}, + definition, + definition, + {"changed": False, "method": "update", "result": definition}, + ), + ( + "update", + {}, + definition, + modified_def, + {"changed": True, "method": "update", "result": modified_def}, + ), + ( + "create", + {"label_selectors": ["app=foo"]}, + {}, + definition, + { + "changed": False, + "msg": "resource 'kind=Pod,name=foo,namespace=foo' filtered by label_selectors.", + }, + ), + ( + "create", + {"label_selectors": ["app=nginx"]}, + {}, + definition, + {"changed": True, "method": "create", "result": definition}, + ), ], ) -def test_perform_action(params, expected): - module = MagicMock() - module.params = params +def test_perform_action(action, params, existing, instance, expected): + svc = Mock() + svc.find_resource.return_value = Mock( + kind=definition["kind"], group_version=definition["apiVersion"] + ) + svc.retrieve.return_value = ResourceInstance(None, existing) if existing else None + spec = {action + ".return_value": instance} + svc.configure_mock(**spec) - result = perform_action(MagicMock(), definition, module.params) - result.assert_has_calls([expected], any_order=True) - - -def test_perform_action_create(): - spec = {"retrieve.side_effect": [{}]} - svc = MagicMock(**spec) - module = MagicMock() - module.params = {} - - result = perform_action(svc, definition, module.params) - result.assert_has_calls([call.__setitem__("method", "create")], any_order=True) + result = perform_action(svc, definition, params) + assert expected.items() <= result.items() diff --git a/tests/unit/module_utils/test_service.py b/tests/unit/module_utils/test_service.py index 8ef836d8..a1822de6 100644 --- a/tests/unit/module_utils/test_service.py +++ b/tests/unit/module_utils/test_service.py @@ -5,6 +5,7 @@ from kubernetes.dynamic.resource import ResourceInstance, Resource from ansible_collections.kubernetes.core.plugins.module_utils.k8s.service import ( K8sService, + diff_objects, ) from kubernetes.dynamic.exceptions import NotFoundError @@ -59,16 +60,14 @@ def mock_pod_updated_resource_instance(): def test_diff_objects_no_diff(): - svc = K8sService(Mock(), Mock()) - match, diff = svc.diff_objects(pod_definition, pod_definition) + match, diff = diff_objects(pod_definition, pod_definition) assert match is True assert diff == {} def test_diff_objects_meta_diff(): - svc = K8sService(Mock(), Mock()) - match, diff = svc.diff_objects(pod_definition, pod_definition_updated) + match, diff = diff_objects(pod_definition, pod_definition_updated) assert match is False assert diff["before"] == { @@ -98,8 +97,7 @@ def test_diff_objects_spec_diff(): ] }, } - svc = K8sService(Mock(), Mock()) - match, diff = svc.diff_objects(pod_definition, pod_definition_updated) + match, diff = diff_objects(pod_definition, pod_definition_updated) assert match is False assert diff["before"]["spec"] == pod_definition["spec"] @@ -110,7 +108,7 @@ def test_find_resource(): mock_pod_resource = Resource( api_version="v1", kind="Pod", namespaced=False, preferred=True, prefix="api" ) - spec = {"resources.get.side_effect": [mock_pod_resource]} + spec = {"resource.return_value": mock_pod_resource} client = Mock(**spec) svc = K8sService(client, Mock()) resource = svc.find_resource("Pod", "v1") @@ -122,38 +120,45 @@ def test_find_resource(): def test_service_delete_existing_resource(mock_pod_resource_instance): spec = {"delete.side_effect": [mock_pod_resource_instance]} client = Mock(**spec) - module = Mock() - module.params = {} - module.check_mode = False + module = Mock( + params={"delete_options": {"gracePeriodSeconds": 2}}, check_mode=False + ) + resource = Mock() svc = K8sService(client, module) - results = svc.delete(Mock(), pod_definition, mock_pod_resource_instance) + result = svc.delete(resource, pod_definition, mock_pod_resource_instance) - assert isinstance(results, dict) - assert results["changed"] is True - assert results["result"] == pod_definition + assert isinstance(result, dict) + assert result == mock_pod_resource_instance.to_dict() + client.delete.assert_called_with( + resource, + name=pod_definition["metadata"]["name"], + namespace=pod_definition["metadata"]["namespace"], + body={"apiVersion": "v1", "kind": "DeleteOptions", "gracePeriodSeconds": 2}, + ) def test_service_delete_no_existing_resource(): module = Mock() module.params = {} module.check_mode = False - svc = K8sService(Mock(), module) - results = svc.delete(Mock(), pod_definition) + client = Mock() + client.delete.return_value = mock_pod_resource_instance + svc = K8sService(client, module) + result = svc.delete(Mock(), pod_definition) - assert isinstance(results, dict) - assert results["changed"] is False - assert results["result"] == {} + assert result == {} + client.delete.assert_not_called() def test_service_delete_existing_resource_check_mode(mock_pod_resource_instance): - module = Mock() - module.params = {"wait": False} - module.check_mode = True - svc = K8sService(Mock(), module) - results = svc.delete(Mock(), pod_definition, mock_pod_resource_instance) + module = Mock(params={}, check_mode=True) + client = Mock(dry_run=False) + client.delete.return_value = mock_pod_resource_instance + svc = K8sService(client, module) + result = svc.delete(Mock(), pod_definition, mock_pod_resource_instance) - assert isinstance(results, dict) - assert results["changed"] is True + assert result == {} + client.delete.assert_not_called() def test_service_create_resource(mock_pod_resource_instance): @@ -163,25 +168,20 @@ def test_service_create_resource(mock_pod_resource_instance): module.params = {} module.check_mode = False svc = K8sService(client, module) - results = svc.create(Mock(), pod_definition) + result = svc.create(Mock(), pod_definition) - assert isinstance(results, dict) - assert results["changed"] is True - assert results["result"] == pod_definition + assert result == mock_pod_resource_instance.to_dict() def test_service_create_resource_check_mode(): - client = Mock() - client.dry_run = False - module = Mock() - module.params = {} - module.check_mode = True + client = Mock(dry_run=False) + client.create.return_value = mock_pod_resource_instance + module = Mock(params={}, check_mode=True) svc = K8sService(client, module) - results = svc.create(Mock(), pod_definition) + result = svc.create(Mock(), pod_definition) - assert isinstance(results, dict) - assert results["changed"] is True - assert results["result"] == pod_definition + assert result == pod_definition + client.create.assert_not_called() def test_service_retrieve_existing_resource(mock_pod_resource_instance): @@ -227,117 +227,39 @@ def test_create_project_request(): def test_service_apply_existing_resource(mock_pod_resource_instance): - spec = {"apply.side_effect": [ResourceInstance(None, pod_definition_updated)]} - client = Mock(**spec) - module = Mock() - module.params = {"apply": True} - module.check_mode = False - svc = K8sService(client, module) - results = svc.apply(Mock(), pod_definition_updated, mock_pod_resource_instance) - - assert isinstance(results, dict) - assert results["changed"] is True - assert results["diff"] is not {} - assert results["result"] == pod_definition_updated - - -def test_service_apply_existing_resource_no_diff(mock_pod_resource_instance): spec = {"apply.side_effect": [mock_pod_resource_instance]} client = Mock(**spec) module = Mock() module.params = {"apply": True} module.check_mode = False svc = K8sService(client, module) - results = svc.apply(Mock(), pod_definition, mock_pod_resource_instance) + result = svc.apply(Mock(), pod_definition_updated, mock_pod_resource_instance) - assert isinstance(results, dict) - assert results["changed"] is False - assert results["diff"] == {} - assert results["result"] == pod_definition + assert result == mock_pod_resource_instance.to_dict() -def test_service_apply_existing_resource_no_apply(mock_pod_resource_instance): - spec = {"apply.side_effect": [mock_pod_resource_instance]} - client = Mock(**spec) - module = Mock() - module.params = {"apply": False} - module.check_mode = False - svc = K8sService(client, module) - results = svc.apply(Mock(), pod_definition, mock_pod_resource_instance) - - assert isinstance(results, dict) - assert results["changed"] is False - assert results["result"] == {} - - -def test_service_replace_existing_resource_no_diff(mock_pod_resource_instance): +def test_service_replace_existing_resource(mock_pod_resource_instance): spec = {"replace.side_effect": [mock_pod_resource_instance]} client = Mock(**spec) module = Mock() module.params = {} module.check_mode = False svc = K8sService(client, module) - results = svc.replace(Mock(), pod_definition, mock_pod_resource_instance) + result = svc.replace(Mock(), pod_definition, mock_pod_resource_instance) - assert isinstance(results, dict) - assert results["changed"] is False - assert results["diff"] == {} - assert results["result"] == pod_definition + assert result == mock_pod_resource_instance.to_dict() -def test_service_replace_existing_resource( - mock_pod_resource_instance, mock_pod_updated_resource_instance -): - spec = {"replace.side_effect": [mock_pod_updated_resource_instance]} +def test_service_update_existing_resource(mock_pod_resource_instance): + spec = {"replace.side_effect": [mock_pod_resource_instance]} client = Mock(**spec) module = Mock() module.params = {} module.check_mode = False svc = K8sService(client, module) - results = svc.replace(Mock(), pod_definition_updated, mock_pod_resource_instance) + result = svc.replace(Mock(), pod_definition, mock_pod_resource_instance) - assert isinstance(results, dict) - assert results["changed"] is True - assert results["result"] == pod_definition_updated - assert results["diff"] != {} - assert results["diff"]["before"] is not {} - assert results["diff"]["after"] is not {} - - -def test_service_update_existing_resource( - mock_pod_resource_instance, mock_pod_updated_resource_instance -): - spec = {"replace.side_effect": [mock_pod_updated_resource_instance]} - client = Mock(**spec) - module = Mock() - module.params = {} - module.check_mode = False - svc = K8sService(client, module) - results = svc.replace(Mock(), pod_definition_updated, mock_pod_resource_instance) - - assert isinstance(results, dict) - assert results["changed"] is True - assert results["result"] == pod_definition_updated - assert results["diff"] != {} - assert results["diff"]["before"] is not {} - assert results["diff"]["after"] is not {} - - -def test_service_update_existing_resource_no_diff(mock_pod_updated_resource_instance): - spec = {"replace.side_effect": [mock_pod_updated_resource_instance]} - client = Mock(**spec) - module = Mock() - module.params = {} - module.check_mode = False - svc = K8sService(client, module) - results = svc.replace( - Mock(), pod_definition_updated, mock_pod_updated_resource_instance - ) - - assert isinstance(results, dict) - assert results["changed"] is False - assert results["result"] == pod_definition_updated - assert results["diff"] == {} + assert result == mock_pod_resource_instance.to_dict() def test_service_find(mock_pod_resource_instance): diff --git a/tests/unit/module_utils/test_waiter.py b/tests/unit/module_utils/test_waiter.py index b2118895..b5ce10a5 100644 --- a/tests/unit/module_utils/test_waiter.py +++ b/tests/unit/module_utils/test_waiter.py @@ -89,7 +89,7 @@ def test_waiter_waits_for_missing_resource(): namespace=RESOURCES[0]["metadata"].get("namespace"), ) assert result is False - assert instance is None + assert instance == {} assert abs(elapsed - 3) <= 1 From 92785f58da85387a514a17d23a897a29d84d6b9b Mon Sep 17 00:00:00 2001 From: Mike Graves Date: Thu, 9 Jun 2022 11:20:48 -0400 Subject: [PATCH 20/24] Port changes from main to refactored branch (#472) Port changes from main to refactored branch Depends-on: ansible/ansible-zuul-jobs#1563 SUMMARY This PR contains several commits that complete the rebase of the 2.x-refactor branch onto main. Most of the changes here had to be manually backported after rebasing as the original changes were to code that will be deprecated. In addition, rather than trying to manually sort out conflicts and changes to the sanity ignores, I rewrote the refresh_ignore_files script to fully automate the management of ignore files. Previously, these files were both manually edited and auto-generated. This should no longer be the case, and these files should never be manually edited going forward. For the purposes of reviewing and history, I kept all changes in separate commits tied to the original commit being backported. ISSUE TYPE COMPONENT NAME ADDITIONAL INFORMATION Reviewed-by: Jill R --- plugins/module_utils/k8s/client.py | 123 ++++++-- plugins/module_utils/k8s/core.py | 5 +- plugins/module_utils/k8s/resource.py | 2 +- plugins/module_utils/k8s/runner.py | 19 +- plugins/module_utils/k8s/service.py | 30 +- plugins/module_utils/k8s/waiter.py | 28 +- plugins/modules/k8s_cp.py | 4 +- plugins/modules/k8s_log.py | 6 +- plugins/modules/k8s_scale.py | 20 +- plugins/modules/k8s_service.py | 6 +- tests/config.yml | 2 + tests/sanity/ignore-2.10.txt | 402 +++++++++++--------------- tests/sanity/ignore-2.11.txt | 402 +++++++++++--------------- tests/sanity/ignore-2.12.txt | 241 +--------------- tests/sanity/ignore-2.13.txt | 187 +----------- tests/sanity/ignore-2.14.txt | 187 +----------- tests/sanity/ignore-2.9.txt | 412 +++++++++++---------------- tests/sanity/refresh_ignore_files | 145 +++++++--- 18 files changed, 827 insertions(+), 1394 deletions(-) create mode 100644 tests/config.yml diff --git a/plugins/module_utils/k8s/client.py b/plugins/module_utils/k8s/client.py index 39f751cc..7087e236 100644 --- a/plugins/module_utils/k8s/client.py +++ b/plugins/module_utils/k8s/client.py @@ -12,7 +12,9 @@ from ansible_collections.kubernetes.core.plugins.module_utils.args_common import AUTH_ARG_SPEC, AUTH_PROXY_HEADERS_SPEC, ) -from ansible_collections.kubernetes.core.plugins.module_utils.k8s.core import requires +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.core import ( + requires as _requires, +) try: from ansible_collections.kubernetes.core.plugins.module_utils import ( @@ -49,6 +51,25 @@ except ImportError: _pool = {} +class unique_string(str): + _low = None + + def __hash__(self): + return id(self) + + def __eq__(self, other): + return self is other + + def lower(self): + if self._low is None: + lower = str.lower(self) + if str.__eq__(lower, self): + self._low = self + else: + self._low = unique_string(lower) + return self._low + + def _create_auth_spec(module=None, **kwargs) -> Dict: auth: Dict = {} # If authorization variables aren't defined, look for them in environment variables @@ -148,7 +169,30 @@ def _create_configuration(auth: Dict): return configuration -def _configuration_digest(configuration) -> str: +def _create_headers(module=None, **kwargs): + header_map = { + "impersonate_user": "Impersonate-User", + "impersonate_groups": "Impersonate-Group", + } + + headers = {} + for arg_name, header_name in header_map.items(): + value = None + if module and module.params.get(arg_name) is not None: + value = module.params.get(arg_name) + elif arg_name in kwargs and kwargs.get(arg_name) is not None: + value = kwargs.get(arg_name) + else: + value = os.getenv("K8S_AUTH_{0}".format(arg_name.upper()), None) + if value is not None: + if AUTH_ARG_SPEC[arg_name].get("type") == "list": + value = [x for x in value.split(",") if x != ""] + if value: + headers[header_name] = value + return headers + + +def _configuration_digest(configuration, **kwargs) -> str: m = hashlib.sha256() for k in AUTH_ARG_MAP: if not hasattr(configuration, k): @@ -161,19 +205,36 @@ def _configuration_digest(configuration) -> str: m.update(content.encode()) else: m.update(str(v).encode()) + for k, v in kwargs.items(): + content = "{0}: {1}".format(k, v) + m.update(content.encode()) digest = m.hexdigest() return digest +def _set_header(client, header, value): + if isinstance(value, list): + for v in value: + client.set_default_header(header_name=unique_string(header), header_value=v) + else: + client.set_default_header(header_name=header, header_value=value) + + def cache(func): - def wrapper(*args): + def wrapper(*args, **kwargs): client = None - digest = _configuration_digest(*args) + hashable_kwargs = {} + for k, v in kwargs.items(): + if isinstance(v, list): + hashable_kwargs[k] = ",".join(sorted(v)) + else: + hashable_kwargs[k] = v + digest = _configuration_digest(*args, **hashable_kwargs) if digest in _pool: client = _pool[digest] else: - client = func(*args) + client = func(*args, **kwargs) _pool[digest] = client return client @@ -182,10 +243,11 @@ def cache(func): @cache -def create_api_client(configuration): - return k8sdynamicclient.K8SDynamicClient( - kubernetes.client.ApiClient(configuration), discoverer=LazyDiscoverer - ) +def create_api_client(configuration, **headers): + client = kubernetes.client.ApiClient(configuration) + for header, value in headers.items(): + _set_header(client, header, value) + return k8sdynamicclient.K8SDynamicClient(client, discoverer=LazyDiscoverer) class K8SClient: @@ -204,20 +266,32 @@ class K8SClient: def resources(self) -> List[Any]: return self.client.resources + def _find_resource_with_prefix( + self, prefix: str, kind: str, api_version: str + ) -> Resource: + for attribute in ["kind", "name", "singular_name"]: + try: + return self.client.resources.get( + **{"prefix": prefix, "api_version": api_version, attribute: kind} + ) + except (ResourceNotFoundError, ResourceNotUniqueError): + pass + return self.client.resources.get( + prefix=prefix, api_version=api_version, short_names=[kind] + ) + def resource(self, kind: str, api_version: str) -> Resource: """Fetch a kubernetes client resource. This will attempt to find a kubernetes resource trying, in order, kind, name, singular_name and short_names. """ - for attribute in ["kind", "name", "singular_name"]: - try: - return self.client.resources.get( - **{"api_version": api_version, attribute: kind} - ) - except (ResourceNotFoundError, ResourceNotUniqueError): - pass - return self.client.resources.get(api_version=api_version, short_names=[kind]) + try: + if api_version == "v1": + return self._find_resource_with_prefix("api", kind, api_version) + except ResourceNotFoundError: + pass + return self._find_resource_with_prefix(None, kind, api_version) def _ensure_dry_run(self, params: Dict) -> Dict: if self.dry_run: @@ -252,13 +326,18 @@ class K8SClient: def get_api_client(module=None, **kwargs: Optional[Any]) -> K8SClient: auth_spec = _create_auth_spec(module, **kwargs) + if module: + requires = module.requires + else: + requires = _requires if isinstance(auth_spec.get("kubeconfig"), dict): - if module: - module.requires("kubernetes", "17.17.0", "to use in-memory config") - else: - requires("kubernetes", "17.17.0", "to use in-memory config") + requires("kubernetes", "17.17.0", "to use in-memory config") + if auth_spec.get("no_proxy"): + requires("kubernetes", "19.15.0", "to use the no_proxy feature") + configuration = _create_configuration(auth_spec) - client = create_api_client(configuration) + headers = _create_headers(module, **kwargs) + client = create_api_client(configuration, **headers) k8s_client = K8SClient( configuration=configuration, diff --git a/plugins/module_utils/k8s/core.py b/plugins/module_utils/k8s/core.py index ac495ec9..b6bdd505 100644 --- a/plugins/module_utils/k8s/core.py +++ b/plugins/module_utils/k8s/core.py @@ -1,6 +1,9 @@ -from distutils.version import LooseVersion from typing import Optional +from ansible_collections.kubernetes.core.plugins.module_utils.version import ( + LooseVersion, +) + from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import missing_required_lib from ansible.module_utils.common.text.converters import to_text diff --git a/plugins/module_utils/k8s/resource.py b/plugins/module_utils/k8s/resource.py index 69e83e86..797979d7 100644 --- a/plugins/module_utils/k8s/resource.py +++ b/plugins/module_utils/k8s/resource.py @@ -90,7 +90,7 @@ def from_yaml(definition: Union[str, List, Dict]) -> Iterable[Dict]: def from_file(filepath: str) -> Iterable[Dict]: """Load resource definitions from a path to a yaml file.""" path = os.path.normpath(filepath) - with open(path, "r") as f: + with open(path, "rb") as f: definitions = list(yaml.safe_load_all(f)) return filter(None, definitions) diff --git a/plugins/module_utils/k8s/runner.py b/plugins/module_utils/k8s/runner.py index c1fe24c4..84b76c77 100644 --- a/plugins/module_utils/k8s/runner.py +++ b/plugins/module_utils/k8s/runner.py @@ -16,7 +16,6 @@ from ansible_collections.kubernetes.core.plugins.module_utils.k8s.service import diff_objects, ) from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import ( - CoreException, ResourceTimeout, ) from ansible_collections.kubernetes.core.plugins.module_utils.k8s.waiter import exists @@ -60,11 +59,15 @@ def run_module(module) -> None: try: result = perform_action(svc, definition, module.params) - except CoreException as e: + except Exception as e: try: error = e.result except AttributeError: error = {} + try: + error["reason"] = e.__cause__.reason + except AttributeError: + pass error["msg"] = to_native(e) if warnings: error.setdefault("warnings", []).extend(warnings) @@ -102,7 +105,13 @@ def perform_action(svc, definition: Dict, params: Dict) -> Dict: existing = svc.retrieve(resource, definition) if state == "absent": - instance = svc.delete(resource, definition, existing) + if exists(existing) and existing.kind.endswith("List"): + instance = [] + for item in existing.items: + r = svc.delete(resource, item, existing) + instance.append(r) + else: + instance = svc.delete(resource, definition, existing) result["method"] = "delete" if exists(existing): result["changed"] = True @@ -114,7 +123,9 @@ def perform_action(svc, definition: Dict, params: Dict) -> Dict: result["msg"] = ( "resource 'kind={kind},name={name},namespace={namespace}' " "filtered by label_selectors.".format( - kind=kind, name=origin_name, namespace=namespace, + kind=kind, + name=origin_name, + namespace=namespace, ) ) return result diff --git a/plugins/module_utils/k8s/service.py b/plugins/module_utils/k8s/service.py index c2f51fc8..121b63c0 100644 --- a/plugins/module_utils/k8s/service.py +++ b/plugins/module_utils/k8s/service.py @@ -14,11 +14,16 @@ from ansible_collections.kubernetes.core.plugins.module_utils.k8s.waiter import get_waiter, ) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.core import ( + requires, +) + from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import ( CoreException, ) from ansible.module_utils.common.dict_transformations import dict_merge +from ansible.module_utils._text import to_native try: from kubernetes.dynamic.exceptions import ( @@ -260,6 +265,9 @@ class K8sService: ) except BadRequestError: return result + except CoreException as e: + result["msg"] = to_native(e) + return result # There is either no result or there is a List resource with no items if ( @@ -281,7 +289,10 @@ class K8sService: name = instance["metadata"].get("name") namespace = instance["metadata"].get("namespace") success, res, duration = waiter.wait( - timeout=wait_timeout, sleep=wait_sleep, name=name, namespace=namespace, + timeout=wait_timeout, + sleep=wait_sleep, + name=name, + namespace=namespace, ) if not success: raise CoreException( @@ -331,6 +342,9 @@ class K8sService: ) -> Dict: namespace = definition["metadata"].get("namespace") + server_side_apply = self.module.params.get("server_side_apply") + if server_side_apply: + requires("kubernetes", "19.15.0", reason="to use server side apply") if self.module.check_mode and not self.client.dry_run: ignored, patch = apply_object(resource, _encode_stringdata(definition)) if existing: @@ -342,6 +356,9 @@ class K8sService: params = {} if self.module.check_mode: params["dry_run"] = "All" + if server_side_apply: + params["server_side"] = True + params.update(server_side_apply) k8s_obj = self.client.apply( resource, definition, namespace=namespace, **params ).to_dict() @@ -352,7 +369,10 @@ class K8sService: return k8s_obj def replace( - self, resource: Resource, definition: Dict, existing: ResourceInstance, + self, + resource: Resource, + definition: Dict, + existing: ResourceInstance, ) -> Dict: append_hash = self.module.params.get("append_hash", False) name = definition["metadata"].get("name") @@ -395,7 +415,11 @@ class K8sService: ]: try: k8s_obj = self.patch_resource( - resource, definition, name, namespace, merge_type=merge_type, + resource, + definition, + name, + namespace, + merge_type=merge_type, ) exception = None except CoreException as e: diff --git a/plugins/module_utils/k8s/waiter.py b/plugins/module_utils/k8s/waiter.py index 88013527..653e1708 100644 --- a/plugins/module_utils/k8s/waiter.py +++ b/plugins/module_utils/k8s/waiter.py @@ -4,6 +4,10 @@ from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Union from ansible.module_utils.parsing.convert_bool import boolean +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import ( + CoreException, +) + try: from kubernetes.dynamic.exceptions import NotFoundError from kubernetes.dynamic.resource import Resource, ResourceField, ResourceInstance @@ -13,6 +17,12 @@ except ImportError: ResourceInstance = Any # type: ignore pass +try: + from urllib3.exceptions import HTTPError +except ImportError: + # Handled during module setup + pass + def deployment_ready(deployment: ResourceInstance) -> bool: # FIXME: frustratingly bool(deployment.status) is True even if status is empty @@ -51,14 +61,17 @@ def daemonset_ready(daemonset: ResourceInstance) -> bool: def statefulset_ready(statefulset: ResourceInstance) -> bool: + # These may be None + updated_replicas = statefulset.status.updatedReplicas or 0 + ready_replicas = statefulset.status.readyReplicas or 0 return bool( statefulset.status and statefulset.spec.updateStrategy.type == "RollingUpdate" and statefulset.status.observedGeneration == (statefulset.metadata.generation or 0) and statefulset.status.updateRevision == statefulset.status.currentRevision - and statefulset.status.updatedReplicas == statefulset.spec.replicas - and statefulset.status.readyReplicas == statefulset.spec.replicas + and updated_replicas == statefulset.spec.replicas + and ready_replicas == statefulset.spec.replicas and statefulset.status.replicas == statefulset.spec.replicas ) @@ -153,13 +166,24 @@ class Waiter: response = None elapsed = 0 for i in clock(timeout, sleep): + exception = None elapsed = i try: response = self.client.get(self.resource, **params) except NotFoundError: response = None + # Retry connection errors as it may be intermittent network issues + except HTTPError as e: + exception = e if self.predicate(response): break + if exception: + msg = ( + "Exception '{0}' raised while trying to get resource using {1}".format( + exception, params + ) + ) + raise CoreException(msg) from exception if response: instance = response.to_dict() return self.predicate(response), instance, elapsed diff --git a/plugins/modules/k8s_cp.py b/plugins/modules/k8s_cp.py index c14b918d..810cf953 100644 --- a/plugins/modules/k8s_cp.py +++ b/plugins/modules/k8s_cp.py @@ -191,9 +191,9 @@ def execute_module(module): state = module.params.get("state") if state == "to_pod": - k8s_copy = K8SCopyToPod(module, client) + k8s_copy = K8SCopyToPod(module, client.client) else: - k8s_copy = K8SCopyFromPod(module, client) + k8s_copy = K8SCopyFromPod(module, client.client) try: k8s_copy.run() diff --git a/plugins/modules/k8s_log.py b/plugins/modules/k8s_log.py index 023e9570..e55bd6e5 100644 --- a/plugins/modules/k8s_log.py +++ b/plugins/modules/k8s_log.py @@ -201,10 +201,8 @@ def execute_module(svc, params): {"sinceSeconds": params["since_seconds"]} ) - if module.params.get("previous"): - kwargs.setdefault("query_params", {}).update( - {"previous": module.params["previous"]} - ) + if params.get("previous"): + kwargs.setdefault("query_params", {}).update({"previous": params["previous"]}) response = resource.log.get( name=name, namespace=namespace, serialize=False, **kwargs diff --git a/plugins/modules/k8s_scale.py b/plugins/modules/k8s_scale.py index fb370d06..561e11ca 100644 --- a/plugins/modules/k8s_scale.py +++ b/plugins/modules/k8s_scale.py @@ -291,7 +291,9 @@ def execute_module(client, module): if module.check_mode: result["result"] = existing.to_dict() else: - result["result"] = client.patch(resource, existing.to_dict()).to_dict() + result["result"] = client.patch( + resource, existing.to_dict() + ).to_dict() else: try: result = scale( @@ -336,7 +338,14 @@ def argspec(): def scale( - client, module, resource, existing_object, replicas, wait, wait_time, wait_sleep, + client, + module, + resource, + existing_object, + replicas, + wait, + wait_time, + wait_sleep, ): name = existing_object.metadata.name namespace = existing_object.metadata.namespace @@ -373,9 +382,12 @@ def scale( k8s_obj = client.get(resource, name=name, namespace=namespace).to_dict() result["result"] = k8s_obj if wait: - waiter = get_waiter(svc.client, resource) + waiter = get_waiter(client, resource) success, result["result"], result["duration"] = waiter.wait( - timeout=wait_time, sleep=wait_sleep, name=name, namespace=namespace, + timeout=wait_time, + sleep=wait_sleep, + name=name, + namespace=namespace, ) if not success: raise ResourceTimeout("Resource scaling timed out", **result) diff --git a/plugins/modules/k8s_service.py b/plugins/modules/k8s_service.py index 4b931367..eba278b1 100644 --- a/plugins/modules/k8s_service.py +++ b/plugins/modules/k8s_service.py @@ -235,7 +235,7 @@ def perform_action(svc, resource, definition, params): def execute_module(svc): - """ Module execution """ + """Module execution""" module = svc.module api_version = "v1" selector = module.params.get("selector") @@ -269,7 +269,9 @@ def execute_module(svc): def main(): module = AnsibleK8SModule( - module_class=AnsibleModule, argument_spec=argspec(), supports_check_mode=True, + module_class=AnsibleModule, + argument_spec=argspec(), + supports_check_mode=True, ) client = get_api_client(module=module) diff --git a/tests/config.yml b/tests/config.yml new file mode 100644 index 00000000..9e402bda --- /dev/null +++ b/tests/config.yml @@ -0,0 +1,2 @@ +modules: + python_requires: ">=3.6" diff --git a/tests/sanity/ignore-2.10.txt b/tests/sanity/ignore-2.10.txt index 433223cc..dd9afccb 100644 --- a/tests/sanity/ignore-2.10.txt +++ b/tests/sanity/ignore-2.10.txt @@ -1,262 +1,178 @@ -tests/integration/targets/k8s_scale/files/deployment.yaml yamllint!skip -tests/integration/targets/helm/files/appversionless-chart-v2/templates/configmap.yaml yamllint!skip -tests/integration/targets/helm/files/appversionless-chart/templates/configmap.yaml yamllint!skip -tests/integration/targets/helm/files/test-chart-v2/templates/configmap.yaml yamllint!skip -tests/integration/targets/helm/files/test-chart/templates/configmap.yaml yamllint!skip -plugins/module_utils/__init__.py compile-2.6!skip -plugins/module_utils/__init__.py compile-2.7!skip -plugins/module_utils/__init__.py compile-3.5!skip -plugins/module_utils/__init__.py future-import-boilerplate!skip -plugins/module_utils/__init__.py import-2.6!skip -plugins/module_utils/__init__.py import-2.7!skip -plugins/module_utils/__init__.py import-3.5!skip -plugins/module_utils/__init__.py metaclass-boilerplate!skip -plugins/module_utils/ansiblemodule.py compile-2.6!skip -plugins/module_utils/ansiblemodule.py compile-2.7!skip -plugins/module_utils/ansiblemodule.py compile-3.5!skip -plugins/module_utils/ansiblemodule.py future-import-boilerplate!skip -plugins/module_utils/ansiblemodule.py import-2.6!skip -plugins/module_utils/ansiblemodule.py import-2.7!skip -plugins/module_utils/ansiblemodule.py import-3.5!skip -plugins/module_utils/ansiblemodule.py metaclass-boilerplate!skip -plugins/module_utils/apply.py compile-2.6!skip -plugins/module_utils/apply.py compile-2.7!skip -plugins/module_utils/apply.py compile-3.5!skip -plugins/module_utils/apply.py future-import-boilerplate!skip -plugins/module_utils/apply.py import-2.6!skip -plugins/module_utils/apply.py import-2.7!skip -plugins/module_utils/apply.py import-3.5!skip -plugins/module_utils/apply.py metaclass-boilerplate!skip -plugins/module_utils/args_common.py compile-2.6!skip -plugins/module_utils/args_common.py compile-2.7!skip -plugins/module_utils/args_common.py compile-3.5!skip -plugins/module_utils/args_common.py future-import-boilerplate!skip -plugins/module_utils/args_common.py import-2.6!skip -plugins/module_utils/args_common.py import-2.7!skip -plugins/module_utils/args_common.py import-3.5!skip -plugins/module_utils/args_common.py metaclass-boilerplate!skip -plugins/module_utils/client/discovery.py future-import-boilerplate!skip -plugins/module_utils/client/discovery.py import-2.6!skip -plugins/module_utils/client/discovery.py import-2.7!skip -plugins/module_utils/client/discovery.py import-3.5!skip plugins/module_utils/client/discovery.py import-3.6!skip plugins/module_utils/client/discovery.py import-3.7!skip plugins/module_utils/client/discovery.py import-3.8!skip plugins/module_utils/client/discovery.py import-3.9!skip -plugins/module_utils/client/discovery.py metaclass-boilerplate!skip -plugins/module_utils/client/resource.py import-2.6!skip -plugins/module_utils/client/resource.py import-2.7!skip -plugins/module_utils/client/resource.py import-3.5!skip plugins/module_utils/client/resource.py import-3.6!skip plugins/module_utils/client/resource.py import-3.7!skip plugins/module_utils/client/resource.py import-3.8!skip plugins/module_utils/client/resource.py import-3.9!skip -plugins/module_utils/common.py compile-2.6!skip -plugins/module_utils/common.py compile-2.7!skip -plugins/module_utils/common.py compile-3.5!skip -plugins/module_utils/common.py future-import-boilerplate!skip -plugins/module_utils/common.py import-2.6!skip -plugins/module_utils/common.py import-2.7!skip -plugins/module_utils/common.py import-3.5!skip -plugins/module_utils/common.py metaclass-boilerplate!skip -plugins/module_utils/exceptions.py compile-2.6!skip -plugins/module_utils/exceptions.py compile-2.7!skip -plugins/module_utils/exceptions.py compile-3.5!skip -plugins/module_utils/exceptions.py future-import-boilerplate!skip -plugins/module_utils/exceptions.py import-2.6!skip -plugins/module_utils/exceptions.py import-2.7!skip -plugins/module_utils/exceptions.py import-3.5!skip -plugins/module_utils/exceptions.py metaclass-boilerplate!skip -plugins/module_utils/hashes.py compile-2.6!skip -plugins/module_utils/hashes.py compile-2.7!skip -plugins/module_utils/hashes.py compile-3.5!skip -plugins/module_utils/hashes.py future-import-boilerplate!skip -plugins/module_utils/hashes.py import-2.6!skip -plugins/module_utils/hashes.py import-2.7!skip -plugins/module_utils/hashes.py import-3.5!skip -plugins/module_utils/hashes.py metaclass-boilerplate!skip -plugins/module_utils/helm.py compile-2.6!skip -plugins/module_utils/helm.py compile-2.7!skip -plugins/module_utils/helm.py compile-3.5!skip -plugins/module_utils/helm.py future-import-boilerplate!skip -plugins/module_utils/helm.py import-2.6!skip -plugins/module_utils/helm.py import-2.7!skip -plugins/module_utils/helm.py import-3.5!skip -plugins/module_utils/helm.py metaclass-boilerplate!skip -plugins/module_utils/k8sdynamicclient.py compile-2.6!skip -plugins/module_utils/k8sdynamicclient.py compile-2.7!skip -plugins/module_utils/k8sdynamicclient.py compile-3.5!skip -plugins/module_utils/k8sdynamicclient.py future-import-boilerplate!skip -plugins/module_utils/k8sdynamicclient.py import-2.6!skip -plugins/module_utils/k8sdynamicclient.py import-2.7!skip -plugins/module_utils/k8sdynamicclient.py import-3.5!skip plugins/module_utils/k8sdynamicclient.py import-3.6!skip plugins/module_utils/k8sdynamicclient.py import-3.7!skip plugins/module_utils/k8sdynamicclient.py import-3.8!skip plugins/module_utils/k8sdynamicclient.py import-3.9!skip -plugins/module_utils/k8sdynamicclient.py metaclass-boilerplate!skip -plugins/modules/__init__.py compile-2.6!skip -plugins/modules/__init__.py compile-2.7!skip -plugins/modules/__init__.py compile-3.5!skip -plugins/modules/__init__.py future-import-boilerplate!skip -plugins/modules/__init__.py import-2.6!skip -plugins/modules/__init__.py import-2.7!skip -plugins/modules/__init__.py import-3.5!skip -plugins/modules/__init__.py metaclass-boilerplate!skip -plugins/modules/helm.py compile-2.6!skip -plugins/modules/helm.py compile-2.7!skip -plugins/modules/helm.py compile-3.5!skip -plugins/modules/helm.py future-import-boilerplate!skip -plugins/modules/helm.py import-2.6!skip -plugins/modules/helm.py import-2.7!skip -plugins/modules/helm.py import-3.5!skip -plugins/modules/helm.py metaclass-boilerplate!skip -plugins/modules/helm_info.py compile-2.6!skip -plugins/modules/helm_info.py compile-2.7!skip -plugins/modules/helm_info.py compile-3.5!skip -plugins/modules/helm_info.py future-import-boilerplate!skip -plugins/modules/helm_info.py import-2.6!skip -plugins/modules/helm_info.py import-2.7!skip -plugins/modules/helm_info.py import-3.5!skip -plugins/modules/helm_info.py metaclass-boilerplate!skip -plugins/modules/helm_plugin.py compile-2.6!skip -plugins/modules/helm_plugin.py compile-2.7!skip -plugins/modules/helm_plugin.py compile-3.5!skip -plugins/modules/helm_plugin.py future-import-boilerplate!skip -plugins/modules/helm_plugin.py import-2.6!skip -plugins/modules/helm_plugin.py import-2.7!skip -plugins/modules/helm_plugin.py import-3.5!skip -plugins/modules/helm_plugin.py metaclass-boilerplate!skip -plugins/modules/helm_plugin_info.py compile-2.6!skip -plugins/modules/helm_plugin_info.py compile-2.7!skip -plugins/modules/helm_plugin_info.py compile-3.5!skip -plugins/modules/helm_plugin_info.py future-import-boilerplate!skip -plugins/modules/helm_plugin_info.py import-2.6!skip -plugins/modules/helm_plugin_info.py import-2.7!skip -plugins/modules/helm_plugin_info.py import-3.5!skip -plugins/modules/helm_plugin_info.py metaclass-boilerplate!skip -plugins/modules/helm_repository.py compile-2.6!skip -plugins/modules/helm_repository.py compile-2.7!skip -plugins/modules/helm_repository.py compile-3.5!skip -plugins/modules/helm_repository.py future-import-boilerplate!skip -plugins/modules/helm_repository.py import-2.6!skip -plugins/modules/helm_repository.py import-2.7!skip -plugins/modules/helm_repository.py import-3.5!skip -plugins/modules/helm_repository.py metaclass-boilerplate!skip -plugins/modules/helm_template.py compile-2.6!skip -plugins/modules/helm_template.py compile-2.7!skip -plugins/modules/helm_template.py compile-3.5!skip -plugins/modules/helm_template.py future-import-boilerplate!skip -plugins/modules/helm_template.py import-2.6!skip -plugins/modules/helm_template.py import-2.7!skip -plugins/modules/helm_template.py import-3.5!skip -plugins/modules/helm_template.py metaclass-boilerplate!skip -plugins/modules/k8s.py compile-2.6!skip -plugins/modules/k8s.py compile-2.7!skip -plugins/modules/k8s.py compile-3.5!skip -plugins/modules/k8s.py future-import-boilerplate!skip -plugins/modules/k8s.py import-2.6!skip -plugins/modules/k8s.py import-2.7!skip -plugins/modules/k8s.py import-3.5!skip -plugins/modules/k8s.py metaclass-boilerplate!skip plugins/modules/k8s.py validate-modules:parameter-type-not-in-doc -plugins/modules/k8s.py validate-modules:return-syntax-error -plugins/modules/k8s_cluster_info.py compile-2.6!skip -plugins/modules/k8s_cluster_info.py compile-2.7!skip -plugins/modules/k8s_cluster_info.py compile-3.5!skip -plugins/modules/k8s_cluster_info.py future-import-boilerplate!skip -plugins/modules/k8s_cluster_info.py import-2.6!skip -plugins/modules/k8s_cluster_info.py import-2.7!skip -plugins/modules/k8s_cluster_info.py import-3.5!skip -plugins/modules/k8s_cluster_info.py metaclass-boilerplate!skip -plugins/modules/k8s_exec.py compile-2.6!skip -plugins/modules/k8s_exec.py compile-2.7!skip -plugins/modules/k8s_exec.py compile-3.5!skip -plugins/modules/k8s_exec.py future-import-boilerplate!skip -plugins/modules/k8s_exec.py import-2.6!skip -plugins/modules/k8s_exec.py import-2.7!skip -plugins/modules/k8s_exec.py import-3.5!skip -plugins/modules/k8s_exec.py metaclass-boilerplate!skip -plugins/modules/k8s_info.py compile-2.6!skip -plugins/modules/k8s_info.py compile-2.7!skip -plugins/modules/k8s_info.py compile-3.5!skip -plugins/modules/k8s_info.py future-import-boilerplate!skip -plugins/modules/k8s_info.py import-2.6!skip -plugins/modules/k8s_info.py import-2.7!skip -plugins/modules/k8s_info.py import-3.5!skip -plugins/modules/k8s_info.py metaclass-boilerplate!skip -plugins/modules/k8s_json_patch.py compile-2.6!skip -plugins/modules/k8s_json_patch.py compile-2.7!skip -plugins/modules/k8s_json_patch.py compile-3.5!skip -plugins/modules/k8s_json_patch.py future-import-boilerplate!skip -plugins/modules/k8s_json_patch.py import-2.6!skip -plugins/modules/k8s_json_patch.py import-2.7!skip -plugins/modules/k8s_json_patch.py import-3.5!skip -plugins/modules/k8s_json_patch.py metaclass-boilerplate!skip -plugins/modules/k8s_log.py compile-2.6!skip -plugins/modules/k8s_log.py compile-2.7!skip -plugins/modules/k8s_log.py compile-3.5!skip -plugins/modules/k8s_log.py future-import-boilerplate!skip -plugins/modules/k8s_log.py import-2.6!skip -plugins/modules/k8s_log.py import-2.7!skip -plugins/modules/k8s_log.py import-3.5!skip -plugins/modules/k8s_log.py metaclass-boilerplate!skip -plugins/modules/k8s_rollback.py compile-2.6!skip -plugins/modules/k8s_rollback.py compile-2.7!skip -plugins/modules/k8s_rollback.py compile-3.5!skip -plugins/modules/k8s_rollback.py future-import-boilerplate!skip -plugins/modules/k8s_rollback.py import-2.6!skip -plugins/modules/k8s_rollback.py import-2.7!skip -plugins/modules/k8s_rollback.py import-3.5!skip -plugins/modules/k8s_rollback.py metaclass-boilerplate!skip -plugins/modules/k8s_scale.py compile-2.6!skip -plugins/modules/k8s_scale.py compile-2.7!skip -plugins/modules/k8s_scale.py compile-3.5!skip -plugins/modules/k8s_scale.py future-import-boilerplate!skip -plugins/modules/k8s_scale.py import-2.6!skip -plugins/modules/k8s_scale.py import-2.7!skip -plugins/modules/k8s_scale.py import-3.5!skip -plugins/modules/k8s_scale.py metaclass-boilerplate!skip plugins/modules/k8s_scale.py validate-modules:parameter-type-not-in-doc -plugins/modules/k8s_scale.py validate-modules:return-syntax-error -plugins/modules/k8s_service.py compile-2.6!skip -plugins/modules/k8s_service.py compile-2.7!skip -plugins/modules/k8s_service.py compile-3.5!skip -plugins/modules/k8s_service.py future-import-boilerplate!skip -plugins/modules/k8s_service.py import-2.6!skip -plugins/modules/k8s_service.py import-2.7!skip -plugins/modules/k8s_service.py import-3.5!skip -plugins/modules/k8s_service.py metaclass-boilerplate!skip plugins/modules/k8s_service.py validate-modules:parameter-type-not-in-doc -plugins/modules/k8s_service.py validate-modules:return-syntax-error +tests/unit/module_utils/fixtures/definitions.yml yamllint!skip +tests/unit/module_utils/fixtures/deployments.yml yamllint!skip +tests/unit/module_utils/fixtures/pods.yml yamllint!skip +tests/integration/targets/helm/files/appversionless-chart-v2/templates/configmap.yaml yamllint!skip +tests/integration/targets/helm/files/appversionless-chart/templates/configmap.yaml yamllint!skip +tests/integration/targets/helm/files/test-chart-v2/templates/configmap.yaml yamllint!skip +tests/integration/targets/helm/files/test-chart/templates/configmap.yaml yamllint!skip +tests/integration/targets/k8s_scale/files/deployment.yaml yamllint!skip tests/sanity/refresh_ignore_files shebang!skip -tests/unit/module_utils/test_discoverer.py future-import-boilerplate!skip -tests/unit/module_utils/test_discoverer.py metaclass-boilerplate!skip -plugins/modules/k8s_cp.py compile-2.6!skip -plugins/modules/k8s_cp.py compile-2.7!skip -plugins/modules/k8s_cp.py import-2.6!skip -plugins/modules/k8s_cp.py import-2.7!skip -tests/integration/targets/k8s_copy/library/kubectl_file_compare.py compile-2.6!skip -tests/integration/targets/k8s_copy/library/kubectl_file_compare.py compile-2.7!skip -tests/integration/targets/k8s_copy/library/kubectl_file_compare.py compile-3.5!skip -tests/unit/module_utils/test_selector.py future-import-boilerplate!skip -tests/unit/module_utils/test_selector.py metaclass-boilerplate!skip -plugins/module_utils/selector.py future-import-boilerplate!skip -plugins/module_utils/selector.py metaclass-boilerplate!skip -plugins/lookup/kustomize.py future-import-boilerplate!skip -plugins/lookup/kustomize.py metaclass-boilerplate!skip -tests/integration/targets/helm/library/helm_test_version.py metaclass-boilerplate!skip -tests/integration/targets/helm/library/helm_test_version.py future-import-boilerplate!skip -plugins/modules/k8s_taint.py compile-2.7!skip -plugins/modules/k8s_taint.py compile-3.5!skip -plugins/modules/k8s_taint.py future-import-boilerplate!skip -plugins/modules/k8s_taint.py import-2.7!skip -plugins/modules/k8s_taint.py import-3.5!skip -plugins/modules/k8s_taint.py metaclass-boilerplate!skip +plugins/modules/k8s.py validate-modules:return-syntax-error +plugins/modules/k8s_scale.py validate-modules:return-syntax-error +plugins/modules/k8s_service.py validate-modules:return-syntax-error plugins/modules/k8s_taint.py validate-modules:return-syntax-error +plugins/doc_fragments/k8s_name_options.py future-import-boilerplate!skip +plugins/doc_fragments/k8s_auth_options.py future-import-boilerplate!skip +plugins/doc_fragments/helm_common_options.py future-import-boilerplate!skip +plugins/doc_fragments/k8s_state_options.py future-import-boilerplate!skip +plugins/doc_fragments/k8s_wait_options.py future-import-boilerplate!skip +plugins/doc_fragments/k8s_scale_options.py future-import-boilerplate!skip +plugins/doc_fragments/k8s_delete_options.py future-import-boilerplate!skip +plugins/doc_fragments/__init__.py future-import-boilerplate!skip +plugins/doc_fragments/k8s_resource_options.py future-import-boilerplate!skip +plugins/module_utils/helm.py future-import-boilerplate!skip +plugins/module_utils/apply.py future-import-boilerplate!skip +plugins/module_utils/hashes.py future-import-boilerplate!skip +plugins/module_utils/version.py future-import-boilerplate!skip +plugins/module_utils/_version.py future-import-boilerplate!skip plugins/module_utils/copy.py future-import-boilerplate!skip +plugins/module_utils/args_common.py future-import-boilerplate!skip +plugins/module_utils/__init__.py future-import-boilerplate!skip +plugins/module_utils/selector.py future-import-boilerplate!skip +plugins/module_utils/k8sdynamicclient.py future-import-boilerplate!skip +plugins/module_utils/common.py future-import-boilerplate!skip +plugins/module_utils/ansiblemodule.py future-import-boilerplate!skip +plugins/module_utils/exceptions.py future-import-boilerplate!skip +plugins/module_utils/client/resource.py future-import-boilerplate!skip +plugins/module_utils/client/discovery.py future-import-boilerplate!skip +plugins/module_utils/k8s/resource.py future-import-boilerplate!skip +plugins/module_utils/k8s/core.py future-import-boilerplate!skip +plugins/module_utils/k8s/waiter.py future-import-boilerplate!skip +plugins/module_utils/k8s/client.py future-import-boilerplate!skip +plugins/module_utils/k8s/runner.py future-import-boilerplate!skip +plugins/module_utils/k8s/service.py future-import-boilerplate!skip +plugins/module_utils/k8s/exceptions.py future-import-boilerplate!skip +plugins/connection/kubectl.py future-import-boilerplate!skip +plugins/inventory/k8s.py future-import-boilerplate!skip +plugins/lookup/k8s.py future-import-boilerplate!skip +plugins/lookup/kustomize.py future-import-boilerplate!skip +plugins/modules/k8s_scale.py future-import-boilerplate!skip +plugins/modules/helm_template.py future-import-boilerplate!skip +plugins/modules/k8s_exec.py future-import-boilerplate!skip +plugins/modules/helm.py future-import-boilerplate!skip +plugins/modules/helm_plugin_info.py future-import-boilerplate!skip +plugins/modules/helm_info.py future-import-boilerplate!skip +plugins/modules/helm_repository.py future-import-boilerplate!skip +plugins/modules/k8s_rollback.py future-import-boilerplate!skip +plugins/modules/k8s_log.py future-import-boilerplate!skip +plugins/modules/k8s_drain.py future-import-boilerplate!skip +plugins/modules/helm_plugin.py future-import-boilerplate!skip +plugins/modules/k8s_taint.py future-import-boilerplate!skip +plugins/modules/k8s.py future-import-boilerplate!skip +plugins/modules/k8s_service.py future-import-boilerplate!skip +plugins/modules/k8s_cluster_info.py future-import-boilerplate!skip +plugins/modules/k8s_info.py future-import-boilerplate!skip +plugins/modules/k8s_cp.py future-import-boilerplate!skip +plugins/modules/__init__.py future-import-boilerplate!skip +plugins/modules/k8s_json_patch.py future-import-boilerplate!skip +plugins/action/k8s_info.py future-import-boilerplate!skip +plugins/filter/k8s.py future-import-boilerplate!skip +tests/unit/conftest.py future-import-boilerplate!skip +tests/unit/utils/ansible_module_mock.py future-import-boilerplate!skip +tests/unit/module_utils/test_helm.py future-import-boilerplate!skip +tests/unit/module_utils/test_marshal.py future-import-boilerplate!skip +tests/unit/module_utils/test_discoverer.py future-import-boilerplate!skip +tests/unit/module_utils/test_hashes.py future-import-boilerplate!skip +tests/unit/module_utils/test_resource.py future-import-boilerplate!skip +tests/unit/module_utils/test_service.py future-import-boilerplate!skip +tests/unit/module_utils/test_waiter.py future-import-boilerplate!skip +tests/unit/module_utils/test_common.py future-import-boilerplate!skip +tests/unit/module_utils/test_selector.py future-import-boilerplate!skip +tests/unit/module_utils/test_apply.py future-import-boilerplate!skip +tests/unit/module_utils/test_runner.py future-import-boilerplate!skip +tests/unit/module_utils/test_client.py future-import-boilerplate!skip +tests/unit/module_utils/test_core.py future-import-boilerplate!skip +tests/unit/modules/test_helm_template_module.py future-import-boilerplate!skip +tests/unit/modules/test_helm_template.py future-import-boilerplate!skip +tests/unit/modules/test_module_helm.py future-import-boilerplate!skip +tests/unit/action/test_remove_omit.py future-import-boilerplate!skip +plugins/doc_fragments/k8s_name_options.py metaclass-boilerplate!skip +plugins/doc_fragments/k8s_auth_options.py metaclass-boilerplate!skip +plugins/doc_fragments/helm_common_options.py metaclass-boilerplate!skip +plugins/doc_fragments/k8s_state_options.py metaclass-boilerplate!skip +plugins/doc_fragments/k8s_wait_options.py metaclass-boilerplate!skip +plugins/doc_fragments/k8s_scale_options.py metaclass-boilerplate!skip +plugins/doc_fragments/k8s_delete_options.py metaclass-boilerplate!skip +plugins/doc_fragments/__init__.py metaclass-boilerplate!skip +plugins/doc_fragments/k8s_resource_options.py metaclass-boilerplate!skip +plugins/module_utils/helm.py metaclass-boilerplate!skip +plugins/module_utils/apply.py metaclass-boilerplate!skip +plugins/module_utils/hashes.py metaclass-boilerplate!skip +plugins/module_utils/version.py metaclass-boilerplate!skip +plugins/module_utils/_version.py metaclass-boilerplate!skip plugins/module_utils/copy.py metaclass-boilerplate!skip -plugins/module_utils/copy.py compile-2.6!skip -plugins/module_utils/copy.py compile-2.7!skip -plugins/module_utils/copy.py import-2.6!skip -plugins/module_utils/copy.py import-2.7!skip +plugins/module_utils/args_common.py metaclass-boilerplate!skip +plugins/module_utils/__init__.py metaclass-boilerplate!skip +plugins/module_utils/selector.py metaclass-boilerplate!skip +plugins/module_utils/k8sdynamicclient.py metaclass-boilerplate!skip +plugins/module_utils/common.py metaclass-boilerplate!skip +plugins/module_utils/ansiblemodule.py metaclass-boilerplate!skip +plugins/module_utils/exceptions.py metaclass-boilerplate!skip +plugins/module_utils/client/resource.py metaclass-boilerplate!skip +plugins/module_utils/client/discovery.py metaclass-boilerplate!skip +plugins/module_utils/k8s/resource.py metaclass-boilerplate!skip +plugins/module_utils/k8s/core.py metaclass-boilerplate!skip +plugins/module_utils/k8s/waiter.py metaclass-boilerplate!skip +plugins/module_utils/k8s/client.py metaclass-boilerplate!skip +plugins/module_utils/k8s/runner.py metaclass-boilerplate!skip +plugins/module_utils/k8s/service.py metaclass-boilerplate!skip +plugins/module_utils/k8s/exceptions.py metaclass-boilerplate!skip +plugins/connection/kubectl.py metaclass-boilerplate!skip +plugins/inventory/k8s.py metaclass-boilerplate!skip +plugins/lookup/k8s.py metaclass-boilerplate!skip +plugins/lookup/kustomize.py metaclass-boilerplate!skip +plugins/modules/k8s_scale.py metaclass-boilerplate!skip +plugins/modules/helm_template.py metaclass-boilerplate!skip +plugins/modules/k8s_exec.py metaclass-boilerplate!skip +plugins/modules/helm.py metaclass-boilerplate!skip +plugins/modules/helm_plugin_info.py metaclass-boilerplate!skip +plugins/modules/helm_info.py metaclass-boilerplate!skip +plugins/modules/helm_repository.py metaclass-boilerplate!skip +plugins/modules/k8s_rollback.py metaclass-boilerplate!skip +plugins/modules/k8s_log.py metaclass-boilerplate!skip +plugins/modules/k8s_drain.py metaclass-boilerplate!skip +plugins/modules/helm_plugin.py metaclass-boilerplate!skip +plugins/modules/k8s_taint.py metaclass-boilerplate!skip +plugins/modules/k8s.py metaclass-boilerplate!skip +plugins/modules/k8s_service.py metaclass-boilerplate!skip +plugins/modules/k8s_cluster_info.py metaclass-boilerplate!skip +plugins/modules/k8s_info.py metaclass-boilerplate!skip +plugins/modules/k8s_cp.py metaclass-boilerplate!skip +plugins/modules/__init__.py metaclass-boilerplate!skip +plugins/modules/k8s_json_patch.py metaclass-boilerplate!skip +plugins/action/k8s_info.py metaclass-boilerplate!skip +plugins/filter/k8s.py metaclass-boilerplate!skip +tests/unit/conftest.py metaclass-boilerplate!skip +tests/unit/utils/ansible_module_mock.py metaclass-boilerplate!skip +tests/unit/module_utils/test_helm.py metaclass-boilerplate!skip +tests/unit/module_utils/test_marshal.py metaclass-boilerplate!skip +tests/unit/module_utils/test_discoverer.py metaclass-boilerplate!skip +tests/unit/module_utils/test_hashes.py metaclass-boilerplate!skip +tests/unit/module_utils/test_resource.py metaclass-boilerplate!skip +tests/unit/module_utils/test_service.py metaclass-boilerplate!skip +tests/unit/module_utils/test_waiter.py metaclass-boilerplate!skip +tests/unit/module_utils/test_common.py metaclass-boilerplate!skip +tests/unit/module_utils/test_selector.py metaclass-boilerplate!skip +tests/unit/module_utils/test_apply.py metaclass-boilerplate!skip +tests/unit/module_utils/test_runner.py metaclass-boilerplate!skip +tests/unit/module_utils/test_client.py metaclass-boilerplate!skip +tests/unit/module_utils/test_core.py metaclass-boilerplate!skip +tests/unit/modules/test_helm_template_module.py metaclass-boilerplate!skip +tests/unit/modules/test_helm_template.py metaclass-boilerplate!skip +tests/unit/modules/test_module_helm.py metaclass-boilerplate!skip +tests/unit/action/test_remove_omit.py metaclass-boilerplate!skip diff --git a/tests/sanity/ignore-2.11.txt b/tests/sanity/ignore-2.11.txt index 433223cc..dd9afccb 100644 --- a/tests/sanity/ignore-2.11.txt +++ b/tests/sanity/ignore-2.11.txt @@ -1,262 +1,178 @@ -tests/integration/targets/k8s_scale/files/deployment.yaml yamllint!skip -tests/integration/targets/helm/files/appversionless-chart-v2/templates/configmap.yaml yamllint!skip -tests/integration/targets/helm/files/appversionless-chart/templates/configmap.yaml yamllint!skip -tests/integration/targets/helm/files/test-chart-v2/templates/configmap.yaml yamllint!skip -tests/integration/targets/helm/files/test-chart/templates/configmap.yaml yamllint!skip -plugins/module_utils/__init__.py compile-2.6!skip -plugins/module_utils/__init__.py compile-2.7!skip -plugins/module_utils/__init__.py compile-3.5!skip -plugins/module_utils/__init__.py future-import-boilerplate!skip -plugins/module_utils/__init__.py import-2.6!skip -plugins/module_utils/__init__.py import-2.7!skip -plugins/module_utils/__init__.py import-3.5!skip -plugins/module_utils/__init__.py metaclass-boilerplate!skip -plugins/module_utils/ansiblemodule.py compile-2.6!skip -plugins/module_utils/ansiblemodule.py compile-2.7!skip -plugins/module_utils/ansiblemodule.py compile-3.5!skip -plugins/module_utils/ansiblemodule.py future-import-boilerplate!skip -plugins/module_utils/ansiblemodule.py import-2.6!skip -plugins/module_utils/ansiblemodule.py import-2.7!skip -plugins/module_utils/ansiblemodule.py import-3.5!skip -plugins/module_utils/ansiblemodule.py metaclass-boilerplate!skip -plugins/module_utils/apply.py compile-2.6!skip -plugins/module_utils/apply.py compile-2.7!skip -plugins/module_utils/apply.py compile-3.5!skip -plugins/module_utils/apply.py future-import-boilerplate!skip -plugins/module_utils/apply.py import-2.6!skip -plugins/module_utils/apply.py import-2.7!skip -plugins/module_utils/apply.py import-3.5!skip -plugins/module_utils/apply.py metaclass-boilerplate!skip -plugins/module_utils/args_common.py compile-2.6!skip -plugins/module_utils/args_common.py compile-2.7!skip -plugins/module_utils/args_common.py compile-3.5!skip -plugins/module_utils/args_common.py future-import-boilerplate!skip -plugins/module_utils/args_common.py import-2.6!skip -plugins/module_utils/args_common.py import-2.7!skip -plugins/module_utils/args_common.py import-3.5!skip -plugins/module_utils/args_common.py metaclass-boilerplate!skip -plugins/module_utils/client/discovery.py future-import-boilerplate!skip -plugins/module_utils/client/discovery.py import-2.6!skip -plugins/module_utils/client/discovery.py import-2.7!skip -plugins/module_utils/client/discovery.py import-3.5!skip plugins/module_utils/client/discovery.py import-3.6!skip plugins/module_utils/client/discovery.py import-3.7!skip plugins/module_utils/client/discovery.py import-3.8!skip plugins/module_utils/client/discovery.py import-3.9!skip -plugins/module_utils/client/discovery.py metaclass-boilerplate!skip -plugins/module_utils/client/resource.py import-2.6!skip -plugins/module_utils/client/resource.py import-2.7!skip -plugins/module_utils/client/resource.py import-3.5!skip plugins/module_utils/client/resource.py import-3.6!skip plugins/module_utils/client/resource.py import-3.7!skip plugins/module_utils/client/resource.py import-3.8!skip plugins/module_utils/client/resource.py import-3.9!skip -plugins/module_utils/common.py compile-2.6!skip -plugins/module_utils/common.py compile-2.7!skip -plugins/module_utils/common.py compile-3.5!skip -plugins/module_utils/common.py future-import-boilerplate!skip -plugins/module_utils/common.py import-2.6!skip -plugins/module_utils/common.py import-2.7!skip -plugins/module_utils/common.py import-3.5!skip -plugins/module_utils/common.py metaclass-boilerplate!skip -plugins/module_utils/exceptions.py compile-2.6!skip -plugins/module_utils/exceptions.py compile-2.7!skip -plugins/module_utils/exceptions.py compile-3.5!skip -plugins/module_utils/exceptions.py future-import-boilerplate!skip -plugins/module_utils/exceptions.py import-2.6!skip -plugins/module_utils/exceptions.py import-2.7!skip -plugins/module_utils/exceptions.py import-3.5!skip -plugins/module_utils/exceptions.py metaclass-boilerplate!skip -plugins/module_utils/hashes.py compile-2.6!skip -plugins/module_utils/hashes.py compile-2.7!skip -plugins/module_utils/hashes.py compile-3.5!skip -plugins/module_utils/hashes.py future-import-boilerplate!skip -plugins/module_utils/hashes.py import-2.6!skip -plugins/module_utils/hashes.py import-2.7!skip -plugins/module_utils/hashes.py import-3.5!skip -plugins/module_utils/hashes.py metaclass-boilerplate!skip -plugins/module_utils/helm.py compile-2.6!skip -plugins/module_utils/helm.py compile-2.7!skip -plugins/module_utils/helm.py compile-3.5!skip -plugins/module_utils/helm.py future-import-boilerplate!skip -plugins/module_utils/helm.py import-2.6!skip -plugins/module_utils/helm.py import-2.7!skip -plugins/module_utils/helm.py import-3.5!skip -plugins/module_utils/helm.py metaclass-boilerplate!skip -plugins/module_utils/k8sdynamicclient.py compile-2.6!skip -plugins/module_utils/k8sdynamicclient.py compile-2.7!skip -plugins/module_utils/k8sdynamicclient.py compile-3.5!skip -plugins/module_utils/k8sdynamicclient.py future-import-boilerplate!skip -plugins/module_utils/k8sdynamicclient.py import-2.6!skip -plugins/module_utils/k8sdynamicclient.py import-2.7!skip -plugins/module_utils/k8sdynamicclient.py import-3.5!skip plugins/module_utils/k8sdynamicclient.py import-3.6!skip plugins/module_utils/k8sdynamicclient.py import-3.7!skip plugins/module_utils/k8sdynamicclient.py import-3.8!skip plugins/module_utils/k8sdynamicclient.py import-3.9!skip -plugins/module_utils/k8sdynamicclient.py metaclass-boilerplate!skip -plugins/modules/__init__.py compile-2.6!skip -plugins/modules/__init__.py compile-2.7!skip -plugins/modules/__init__.py compile-3.5!skip -plugins/modules/__init__.py future-import-boilerplate!skip -plugins/modules/__init__.py import-2.6!skip -plugins/modules/__init__.py import-2.7!skip -plugins/modules/__init__.py import-3.5!skip -plugins/modules/__init__.py metaclass-boilerplate!skip -plugins/modules/helm.py compile-2.6!skip -plugins/modules/helm.py compile-2.7!skip -plugins/modules/helm.py compile-3.5!skip -plugins/modules/helm.py future-import-boilerplate!skip -plugins/modules/helm.py import-2.6!skip -plugins/modules/helm.py import-2.7!skip -plugins/modules/helm.py import-3.5!skip -plugins/modules/helm.py metaclass-boilerplate!skip -plugins/modules/helm_info.py compile-2.6!skip -plugins/modules/helm_info.py compile-2.7!skip -plugins/modules/helm_info.py compile-3.5!skip -plugins/modules/helm_info.py future-import-boilerplate!skip -plugins/modules/helm_info.py import-2.6!skip -plugins/modules/helm_info.py import-2.7!skip -plugins/modules/helm_info.py import-3.5!skip -plugins/modules/helm_info.py metaclass-boilerplate!skip -plugins/modules/helm_plugin.py compile-2.6!skip -plugins/modules/helm_plugin.py compile-2.7!skip -plugins/modules/helm_plugin.py compile-3.5!skip -plugins/modules/helm_plugin.py future-import-boilerplate!skip -plugins/modules/helm_plugin.py import-2.6!skip -plugins/modules/helm_plugin.py import-2.7!skip -plugins/modules/helm_plugin.py import-3.5!skip -plugins/modules/helm_plugin.py metaclass-boilerplate!skip -plugins/modules/helm_plugin_info.py compile-2.6!skip -plugins/modules/helm_plugin_info.py compile-2.7!skip -plugins/modules/helm_plugin_info.py compile-3.5!skip -plugins/modules/helm_plugin_info.py future-import-boilerplate!skip -plugins/modules/helm_plugin_info.py import-2.6!skip -plugins/modules/helm_plugin_info.py import-2.7!skip -plugins/modules/helm_plugin_info.py import-3.5!skip -plugins/modules/helm_plugin_info.py metaclass-boilerplate!skip -plugins/modules/helm_repository.py compile-2.6!skip -plugins/modules/helm_repository.py compile-2.7!skip -plugins/modules/helm_repository.py compile-3.5!skip -plugins/modules/helm_repository.py future-import-boilerplate!skip -plugins/modules/helm_repository.py import-2.6!skip -plugins/modules/helm_repository.py import-2.7!skip -plugins/modules/helm_repository.py import-3.5!skip -plugins/modules/helm_repository.py metaclass-boilerplate!skip -plugins/modules/helm_template.py compile-2.6!skip -plugins/modules/helm_template.py compile-2.7!skip -plugins/modules/helm_template.py compile-3.5!skip -plugins/modules/helm_template.py future-import-boilerplate!skip -plugins/modules/helm_template.py import-2.6!skip -plugins/modules/helm_template.py import-2.7!skip -plugins/modules/helm_template.py import-3.5!skip -plugins/modules/helm_template.py metaclass-boilerplate!skip -plugins/modules/k8s.py compile-2.6!skip -plugins/modules/k8s.py compile-2.7!skip -plugins/modules/k8s.py compile-3.5!skip -plugins/modules/k8s.py future-import-boilerplate!skip -plugins/modules/k8s.py import-2.6!skip -plugins/modules/k8s.py import-2.7!skip -plugins/modules/k8s.py import-3.5!skip -plugins/modules/k8s.py metaclass-boilerplate!skip plugins/modules/k8s.py validate-modules:parameter-type-not-in-doc -plugins/modules/k8s.py validate-modules:return-syntax-error -plugins/modules/k8s_cluster_info.py compile-2.6!skip -plugins/modules/k8s_cluster_info.py compile-2.7!skip -plugins/modules/k8s_cluster_info.py compile-3.5!skip -plugins/modules/k8s_cluster_info.py future-import-boilerplate!skip -plugins/modules/k8s_cluster_info.py import-2.6!skip -plugins/modules/k8s_cluster_info.py import-2.7!skip -plugins/modules/k8s_cluster_info.py import-3.5!skip -plugins/modules/k8s_cluster_info.py metaclass-boilerplate!skip -plugins/modules/k8s_exec.py compile-2.6!skip -plugins/modules/k8s_exec.py compile-2.7!skip -plugins/modules/k8s_exec.py compile-3.5!skip -plugins/modules/k8s_exec.py future-import-boilerplate!skip -plugins/modules/k8s_exec.py import-2.6!skip -plugins/modules/k8s_exec.py import-2.7!skip -plugins/modules/k8s_exec.py import-3.5!skip -plugins/modules/k8s_exec.py metaclass-boilerplate!skip -plugins/modules/k8s_info.py compile-2.6!skip -plugins/modules/k8s_info.py compile-2.7!skip -plugins/modules/k8s_info.py compile-3.5!skip -plugins/modules/k8s_info.py future-import-boilerplate!skip -plugins/modules/k8s_info.py import-2.6!skip -plugins/modules/k8s_info.py import-2.7!skip -plugins/modules/k8s_info.py import-3.5!skip -plugins/modules/k8s_info.py metaclass-boilerplate!skip -plugins/modules/k8s_json_patch.py compile-2.6!skip -plugins/modules/k8s_json_patch.py compile-2.7!skip -plugins/modules/k8s_json_patch.py compile-3.5!skip -plugins/modules/k8s_json_patch.py future-import-boilerplate!skip -plugins/modules/k8s_json_patch.py import-2.6!skip -plugins/modules/k8s_json_patch.py import-2.7!skip -plugins/modules/k8s_json_patch.py import-3.5!skip -plugins/modules/k8s_json_patch.py metaclass-boilerplate!skip -plugins/modules/k8s_log.py compile-2.6!skip -plugins/modules/k8s_log.py compile-2.7!skip -plugins/modules/k8s_log.py compile-3.5!skip -plugins/modules/k8s_log.py future-import-boilerplate!skip -plugins/modules/k8s_log.py import-2.6!skip -plugins/modules/k8s_log.py import-2.7!skip -plugins/modules/k8s_log.py import-3.5!skip -plugins/modules/k8s_log.py metaclass-boilerplate!skip -plugins/modules/k8s_rollback.py compile-2.6!skip -plugins/modules/k8s_rollback.py compile-2.7!skip -plugins/modules/k8s_rollback.py compile-3.5!skip -plugins/modules/k8s_rollback.py future-import-boilerplate!skip -plugins/modules/k8s_rollback.py import-2.6!skip -plugins/modules/k8s_rollback.py import-2.7!skip -plugins/modules/k8s_rollback.py import-3.5!skip -plugins/modules/k8s_rollback.py metaclass-boilerplate!skip -plugins/modules/k8s_scale.py compile-2.6!skip -plugins/modules/k8s_scale.py compile-2.7!skip -plugins/modules/k8s_scale.py compile-3.5!skip -plugins/modules/k8s_scale.py future-import-boilerplate!skip -plugins/modules/k8s_scale.py import-2.6!skip -plugins/modules/k8s_scale.py import-2.7!skip -plugins/modules/k8s_scale.py import-3.5!skip -plugins/modules/k8s_scale.py metaclass-boilerplate!skip plugins/modules/k8s_scale.py validate-modules:parameter-type-not-in-doc -plugins/modules/k8s_scale.py validate-modules:return-syntax-error -plugins/modules/k8s_service.py compile-2.6!skip -plugins/modules/k8s_service.py compile-2.7!skip -plugins/modules/k8s_service.py compile-3.5!skip -plugins/modules/k8s_service.py future-import-boilerplate!skip -plugins/modules/k8s_service.py import-2.6!skip -plugins/modules/k8s_service.py import-2.7!skip -plugins/modules/k8s_service.py import-3.5!skip -plugins/modules/k8s_service.py metaclass-boilerplate!skip plugins/modules/k8s_service.py validate-modules:parameter-type-not-in-doc -plugins/modules/k8s_service.py validate-modules:return-syntax-error +tests/unit/module_utils/fixtures/definitions.yml yamllint!skip +tests/unit/module_utils/fixtures/deployments.yml yamllint!skip +tests/unit/module_utils/fixtures/pods.yml yamllint!skip +tests/integration/targets/helm/files/appversionless-chart-v2/templates/configmap.yaml yamllint!skip +tests/integration/targets/helm/files/appversionless-chart/templates/configmap.yaml yamllint!skip +tests/integration/targets/helm/files/test-chart-v2/templates/configmap.yaml yamllint!skip +tests/integration/targets/helm/files/test-chart/templates/configmap.yaml yamllint!skip +tests/integration/targets/k8s_scale/files/deployment.yaml yamllint!skip tests/sanity/refresh_ignore_files shebang!skip -tests/unit/module_utils/test_discoverer.py future-import-boilerplate!skip -tests/unit/module_utils/test_discoverer.py metaclass-boilerplate!skip -plugins/modules/k8s_cp.py compile-2.6!skip -plugins/modules/k8s_cp.py compile-2.7!skip -plugins/modules/k8s_cp.py import-2.6!skip -plugins/modules/k8s_cp.py import-2.7!skip -tests/integration/targets/k8s_copy/library/kubectl_file_compare.py compile-2.6!skip -tests/integration/targets/k8s_copy/library/kubectl_file_compare.py compile-2.7!skip -tests/integration/targets/k8s_copy/library/kubectl_file_compare.py compile-3.5!skip -tests/unit/module_utils/test_selector.py future-import-boilerplate!skip -tests/unit/module_utils/test_selector.py metaclass-boilerplate!skip -plugins/module_utils/selector.py future-import-boilerplate!skip -plugins/module_utils/selector.py metaclass-boilerplate!skip -plugins/lookup/kustomize.py future-import-boilerplate!skip -plugins/lookup/kustomize.py metaclass-boilerplate!skip -tests/integration/targets/helm/library/helm_test_version.py metaclass-boilerplate!skip -tests/integration/targets/helm/library/helm_test_version.py future-import-boilerplate!skip -plugins/modules/k8s_taint.py compile-2.7!skip -plugins/modules/k8s_taint.py compile-3.5!skip -plugins/modules/k8s_taint.py future-import-boilerplate!skip -plugins/modules/k8s_taint.py import-2.7!skip -plugins/modules/k8s_taint.py import-3.5!skip -plugins/modules/k8s_taint.py metaclass-boilerplate!skip +plugins/modules/k8s.py validate-modules:return-syntax-error +plugins/modules/k8s_scale.py validate-modules:return-syntax-error +plugins/modules/k8s_service.py validate-modules:return-syntax-error plugins/modules/k8s_taint.py validate-modules:return-syntax-error +plugins/doc_fragments/k8s_name_options.py future-import-boilerplate!skip +plugins/doc_fragments/k8s_auth_options.py future-import-boilerplate!skip +plugins/doc_fragments/helm_common_options.py future-import-boilerplate!skip +plugins/doc_fragments/k8s_state_options.py future-import-boilerplate!skip +plugins/doc_fragments/k8s_wait_options.py future-import-boilerplate!skip +plugins/doc_fragments/k8s_scale_options.py future-import-boilerplate!skip +plugins/doc_fragments/k8s_delete_options.py future-import-boilerplate!skip +plugins/doc_fragments/__init__.py future-import-boilerplate!skip +plugins/doc_fragments/k8s_resource_options.py future-import-boilerplate!skip +plugins/module_utils/helm.py future-import-boilerplate!skip +plugins/module_utils/apply.py future-import-boilerplate!skip +plugins/module_utils/hashes.py future-import-boilerplate!skip +plugins/module_utils/version.py future-import-boilerplate!skip +plugins/module_utils/_version.py future-import-boilerplate!skip plugins/module_utils/copy.py future-import-boilerplate!skip +plugins/module_utils/args_common.py future-import-boilerplate!skip +plugins/module_utils/__init__.py future-import-boilerplate!skip +plugins/module_utils/selector.py future-import-boilerplate!skip +plugins/module_utils/k8sdynamicclient.py future-import-boilerplate!skip +plugins/module_utils/common.py future-import-boilerplate!skip +plugins/module_utils/ansiblemodule.py future-import-boilerplate!skip +plugins/module_utils/exceptions.py future-import-boilerplate!skip +plugins/module_utils/client/resource.py future-import-boilerplate!skip +plugins/module_utils/client/discovery.py future-import-boilerplate!skip +plugins/module_utils/k8s/resource.py future-import-boilerplate!skip +plugins/module_utils/k8s/core.py future-import-boilerplate!skip +plugins/module_utils/k8s/waiter.py future-import-boilerplate!skip +plugins/module_utils/k8s/client.py future-import-boilerplate!skip +plugins/module_utils/k8s/runner.py future-import-boilerplate!skip +plugins/module_utils/k8s/service.py future-import-boilerplate!skip +plugins/module_utils/k8s/exceptions.py future-import-boilerplate!skip +plugins/connection/kubectl.py future-import-boilerplate!skip +plugins/inventory/k8s.py future-import-boilerplate!skip +plugins/lookup/k8s.py future-import-boilerplate!skip +plugins/lookup/kustomize.py future-import-boilerplate!skip +plugins/modules/k8s_scale.py future-import-boilerplate!skip +plugins/modules/helm_template.py future-import-boilerplate!skip +plugins/modules/k8s_exec.py future-import-boilerplate!skip +plugins/modules/helm.py future-import-boilerplate!skip +plugins/modules/helm_plugin_info.py future-import-boilerplate!skip +plugins/modules/helm_info.py future-import-boilerplate!skip +plugins/modules/helm_repository.py future-import-boilerplate!skip +plugins/modules/k8s_rollback.py future-import-boilerplate!skip +plugins/modules/k8s_log.py future-import-boilerplate!skip +plugins/modules/k8s_drain.py future-import-boilerplate!skip +plugins/modules/helm_plugin.py future-import-boilerplate!skip +plugins/modules/k8s_taint.py future-import-boilerplate!skip +plugins/modules/k8s.py future-import-boilerplate!skip +plugins/modules/k8s_service.py future-import-boilerplate!skip +plugins/modules/k8s_cluster_info.py future-import-boilerplate!skip +plugins/modules/k8s_info.py future-import-boilerplate!skip +plugins/modules/k8s_cp.py future-import-boilerplate!skip +plugins/modules/__init__.py future-import-boilerplate!skip +plugins/modules/k8s_json_patch.py future-import-boilerplate!skip +plugins/action/k8s_info.py future-import-boilerplate!skip +plugins/filter/k8s.py future-import-boilerplate!skip +tests/unit/conftest.py future-import-boilerplate!skip +tests/unit/utils/ansible_module_mock.py future-import-boilerplate!skip +tests/unit/module_utils/test_helm.py future-import-boilerplate!skip +tests/unit/module_utils/test_marshal.py future-import-boilerplate!skip +tests/unit/module_utils/test_discoverer.py future-import-boilerplate!skip +tests/unit/module_utils/test_hashes.py future-import-boilerplate!skip +tests/unit/module_utils/test_resource.py future-import-boilerplate!skip +tests/unit/module_utils/test_service.py future-import-boilerplate!skip +tests/unit/module_utils/test_waiter.py future-import-boilerplate!skip +tests/unit/module_utils/test_common.py future-import-boilerplate!skip +tests/unit/module_utils/test_selector.py future-import-boilerplate!skip +tests/unit/module_utils/test_apply.py future-import-boilerplate!skip +tests/unit/module_utils/test_runner.py future-import-boilerplate!skip +tests/unit/module_utils/test_client.py future-import-boilerplate!skip +tests/unit/module_utils/test_core.py future-import-boilerplate!skip +tests/unit/modules/test_helm_template_module.py future-import-boilerplate!skip +tests/unit/modules/test_helm_template.py future-import-boilerplate!skip +tests/unit/modules/test_module_helm.py future-import-boilerplate!skip +tests/unit/action/test_remove_omit.py future-import-boilerplate!skip +plugins/doc_fragments/k8s_name_options.py metaclass-boilerplate!skip +plugins/doc_fragments/k8s_auth_options.py metaclass-boilerplate!skip +plugins/doc_fragments/helm_common_options.py metaclass-boilerplate!skip +plugins/doc_fragments/k8s_state_options.py metaclass-boilerplate!skip +plugins/doc_fragments/k8s_wait_options.py metaclass-boilerplate!skip +plugins/doc_fragments/k8s_scale_options.py metaclass-boilerplate!skip +plugins/doc_fragments/k8s_delete_options.py metaclass-boilerplate!skip +plugins/doc_fragments/__init__.py metaclass-boilerplate!skip +plugins/doc_fragments/k8s_resource_options.py metaclass-boilerplate!skip +plugins/module_utils/helm.py metaclass-boilerplate!skip +plugins/module_utils/apply.py metaclass-boilerplate!skip +plugins/module_utils/hashes.py metaclass-boilerplate!skip +plugins/module_utils/version.py metaclass-boilerplate!skip +plugins/module_utils/_version.py metaclass-boilerplate!skip plugins/module_utils/copy.py metaclass-boilerplate!skip -plugins/module_utils/copy.py compile-2.6!skip -plugins/module_utils/copy.py compile-2.7!skip -plugins/module_utils/copy.py import-2.6!skip -plugins/module_utils/copy.py import-2.7!skip +plugins/module_utils/args_common.py metaclass-boilerplate!skip +plugins/module_utils/__init__.py metaclass-boilerplate!skip +plugins/module_utils/selector.py metaclass-boilerplate!skip +plugins/module_utils/k8sdynamicclient.py metaclass-boilerplate!skip +plugins/module_utils/common.py metaclass-boilerplate!skip +plugins/module_utils/ansiblemodule.py metaclass-boilerplate!skip +plugins/module_utils/exceptions.py metaclass-boilerplate!skip +plugins/module_utils/client/resource.py metaclass-boilerplate!skip +plugins/module_utils/client/discovery.py metaclass-boilerplate!skip +plugins/module_utils/k8s/resource.py metaclass-boilerplate!skip +plugins/module_utils/k8s/core.py metaclass-boilerplate!skip +plugins/module_utils/k8s/waiter.py metaclass-boilerplate!skip +plugins/module_utils/k8s/client.py metaclass-boilerplate!skip +plugins/module_utils/k8s/runner.py metaclass-boilerplate!skip +plugins/module_utils/k8s/service.py metaclass-boilerplate!skip +plugins/module_utils/k8s/exceptions.py metaclass-boilerplate!skip +plugins/connection/kubectl.py metaclass-boilerplate!skip +plugins/inventory/k8s.py metaclass-boilerplate!skip +plugins/lookup/k8s.py metaclass-boilerplate!skip +plugins/lookup/kustomize.py metaclass-boilerplate!skip +plugins/modules/k8s_scale.py metaclass-boilerplate!skip +plugins/modules/helm_template.py metaclass-boilerplate!skip +plugins/modules/k8s_exec.py metaclass-boilerplate!skip +plugins/modules/helm.py metaclass-boilerplate!skip +plugins/modules/helm_plugin_info.py metaclass-boilerplate!skip +plugins/modules/helm_info.py metaclass-boilerplate!skip +plugins/modules/helm_repository.py metaclass-boilerplate!skip +plugins/modules/k8s_rollback.py metaclass-boilerplate!skip +plugins/modules/k8s_log.py metaclass-boilerplate!skip +plugins/modules/k8s_drain.py metaclass-boilerplate!skip +plugins/modules/helm_plugin.py metaclass-boilerplate!skip +plugins/modules/k8s_taint.py metaclass-boilerplate!skip +plugins/modules/k8s.py metaclass-boilerplate!skip +plugins/modules/k8s_service.py metaclass-boilerplate!skip +plugins/modules/k8s_cluster_info.py metaclass-boilerplate!skip +plugins/modules/k8s_info.py metaclass-boilerplate!skip +plugins/modules/k8s_cp.py metaclass-boilerplate!skip +plugins/modules/__init__.py metaclass-boilerplate!skip +plugins/modules/k8s_json_patch.py metaclass-boilerplate!skip +plugins/action/k8s_info.py metaclass-boilerplate!skip +plugins/filter/k8s.py metaclass-boilerplate!skip +tests/unit/conftest.py metaclass-boilerplate!skip +tests/unit/utils/ansible_module_mock.py metaclass-boilerplate!skip +tests/unit/module_utils/test_helm.py metaclass-boilerplate!skip +tests/unit/module_utils/test_marshal.py metaclass-boilerplate!skip +tests/unit/module_utils/test_discoverer.py metaclass-boilerplate!skip +tests/unit/module_utils/test_hashes.py metaclass-boilerplate!skip +tests/unit/module_utils/test_resource.py metaclass-boilerplate!skip +tests/unit/module_utils/test_service.py metaclass-boilerplate!skip +tests/unit/module_utils/test_waiter.py metaclass-boilerplate!skip +tests/unit/module_utils/test_common.py metaclass-boilerplate!skip +tests/unit/module_utils/test_selector.py metaclass-boilerplate!skip +tests/unit/module_utils/test_apply.py metaclass-boilerplate!skip +tests/unit/module_utils/test_runner.py metaclass-boilerplate!skip +tests/unit/module_utils/test_client.py metaclass-boilerplate!skip +tests/unit/module_utils/test_core.py metaclass-boilerplate!skip +tests/unit/modules/test_helm_template_module.py metaclass-boilerplate!skip +tests/unit/modules/test_helm_template.py metaclass-boilerplate!skip +tests/unit/modules/test_module_helm.py metaclass-boilerplate!skip +tests/unit/action/test_remove_omit.py metaclass-boilerplate!skip diff --git a/tests/sanity/ignore-2.12.txt b/tests/sanity/ignore-2.12.txt index 73fa1b70..8572453e 100644 --- a/tests/sanity/ignore-2.12.txt +++ b/tests/sanity/ignore-2.12.txt @@ -1,248 +1,31 @@ -tests/integration/targets/k8s_scale/files/deployment.yaml yamllint!skip -tests/integration/targets/helm/files/appversionless-chart-v2/templates/configmap.yaml yamllint!skip -tests/integration/targets/helm/files/appversionless-chart/templates/configmap.yaml yamllint!skip -tests/integration/targets/helm/files/test-chart-v2/templates/configmap.yaml yamllint!skip -tests/integration/targets/helm/files/test-chart/templates/configmap.yaml yamllint!skip -plugins/module_utils/__init__.py compile-2.6!skip -plugins/module_utils/__init__.py compile-2.7!skip -plugins/module_utils/__init__.py compile-3.5!skip -plugins/module_utils/__init__.py future-import-boilerplate!skip -plugins/module_utils/__init__.py import-2.6!skip -plugins/module_utils/__init__.py import-2.7!skip -plugins/module_utils/__init__.py import-3.5!skip -plugins/module_utils/__init__.py metaclass-boilerplate!skip -plugins/module_utils/ansiblemodule.py compile-2.6!skip -plugins/module_utils/ansiblemodule.py compile-2.7!skip -plugins/module_utils/ansiblemodule.py compile-3.5!skip -plugins/module_utils/ansiblemodule.py future-import-boilerplate!skip -plugins/module_utils/ansiblemodule.py import-2.6!skip -plugins/module_utils/ansiblemodule.py import-2.7!skip -plugins/module_utils/ansiblemodule.py import-3.5!skip -plugins/module_utils/ansiblemodule.py metaclass-boilerplate!skip -plugins/module_utils/apply.py compile-2.6!skip -plugins/module_utils/apply.py compile-2.7!skip -plugins/module_utils/apply.py compile-3.5!skip -plugins/module_utils/apply.py future-import-boilerplate!skip -plugins/module_utils/apply.py import-2.6!skip -plugins/module_utils/apply.py import-2.7!skip -plugins/module_utils/apply.py import-3.5!skip -plugins/module_utils/apply.py metaclass-boilerplate!skip -plugins/module_utils/args_common.py compile-2.6!skip -plugins/module_utils/args_common.py compile-2.7!skip -plugins/module_utils/args_common.py compile-3.5!skip -plugins/module_utils/args_common.py future-import-boilerplate!skip -plugins/module_utils/args_common.py import-2.6!skip -plugins/module_utils/args_common.py import-2.7!skip -plugins/module_utils/args_common.py import-3.5!skip -plugins/module_utils/args_common.py metaclass-boilerplate!skip -plugins/module_utils/client/discovery.py future-import-boilerplate!skip -plugins/module_utils/client/discovery.py import-2.6!skip -plugins/module_utils/client/discovery.py import-2.7!skip -plugins/module_utils/client/discovery.py import-3.5!skip plugins/module_utils/client/discovery.py import-3.6!skip plugins/module_utils/client/discovery.py import-3.7!skip plugins/module_utils/client/discovery.py import-3.8!skip plugins/module_utils/client/discovery.py import-3.9!skip plugins/module_utils/client/discovery.py import-3.10!skip -plugins/module_utils/client/discovery.py metaclass-boilerplate!skip -plugins/module_utils/client/resource.py import-2.6!skip -plugins/module_utils/client/resource.py import-2.7!skip -plugins/module_utils/client/resource.py import-3.5!skip plugins/module_utils/client/resource.py import-3.6!skip plugins/module_utils/client/resource.py import-3.7!skip plugins/module_utils/client/resource.py import-3.8!skip plugins/module_utils/client/resource.py import-3.9!skip plugins/module_utils/client/resource.py import-3.10!skip -plugins/module_utils/common.py compile-2.6!skip -plugins/module_utils/common.py compile-2.7!skip -plugins/module_utils/common.py compile-3.5!skip -plugins/module_utils/common.py future-import-boilerplate!skip -plugins/module_utils/common.py import-2.6!skip -plugins/module_utils/common.py import-2.7!skip -plugins/module_utils/common.py import-3.5!skip -plugins/module_utils/common.py metaclass-boilerplate!skip -plugins/module_utils/exceptions.py compile-2.6!skip -plugins/module_utils/exceptions.py compile-2.7!skip -plugins/module_utils/exceptions.py compile-3.5!skip -plugins/module_utils/exceptions.py future-import-boilerplate!skip -plugins/module_utils/exceptions.py import-2.6!skip -plugins/module_utils/exceptions.py import-2.7!skip -plugins/module_utils/exceptions.py import-3.5!skip -plugins/module_utils/exceptions.py metaclass-boilerplate!skip -plugins/module_utils/hashes.py compile-2.6!skip -plugins/module_utils/hashes.py compile-2.7!skip -plugins/module_utils/hashes.py compile-3.5!skip -plugins/module_utils/hashes.py future-import-boilerplate!skip -plugins/module_utils/hashes.py import-2.6!skip -plugins/module_utils/hashes.py import-2.7!skip -plugins/module_utils/hashes.py import-3.5!skip -plugins/module_utils/hashes.py metaclass-boilerplate!skip -plugins/module_utils/helm.py compile-2.6!skip -plugins/module_utils/helm.py compile-2.7!skip -plugins/module_utils/helm.py compile-3.5!skip -plugins/module_utils/helm.py future-import-boilerplate!skip -plugins/module_utils/helm.py import-2.6!skip -plugins/module_utils/helm.py import-2.7!skip -plugins/module_utils/helm.py import-3.5!skip -plugins/module_utils/helm.py metaclass-boilerplate!skip -plugins/module_utils/k8sdynamicclient.py compile-2.6!skip -plugins/module_utils/k8sdynamicclient.py compile-2.7!skip -plugins/module_utils/k8sdynamicclient.py compile-3.5!skip -plugins/module_utils/k8sdynamicclient.py future-import-boilerplate!skip -plugins/module_utils/k8sdynamicclient.py import-2.6!skip -plugins/module_utils/k8sdynamicclient.py import-2.7!skip -plugins/module_utils/k8sdynamicclient.py import-3.5!skip plugins/module_utils/k8sdynamicclient.py import-3.6!skip plugins/module_utils/k8sdynamicclient.py import-3.7!skip plugins/module_utils/k8sdynamicclient.py import-3.8!skip plugins/module_utils/k8sdynamicclient.py import-3.9!skip plugins/module_utils/k8sdynamicclient.py import-3.10!skip -plugins/module_utils/k8sdynamicclient.py metaclass-boilerplate!skip -plugins/modules/__init__.py compile-2.6!skip -plugins/modules/__init__.py compile-2.7!skip -plugins/modules/__init__.py compile-3.5!skip -plugins/modules/__init__.py future-import-boilerplate!skip -plugins/modules/__init__.py import-2.6!skip -plugins/modules/__init__.py import-2.7!skip -plugins/modules/__init__.py import-3.5!skip -plugins/modules/__init__.py metaclass-boilerplate!skip -plugins/modules/helm.py compile-2.6!skip -plugins/modules/helm.py compile-2.7!skip -plugins/modules/helm.py compile-3.5!skip -plugins/modules/helm.py future-import-boilerplate!skip -plugins/modules/helm.py import-2.6!skip -plugins/modules/helm.py import-2.7!skip -plugins/modules/helm.py import-3.5!skip -plugins/modules/helm.py metaclass-boilerplate!skip -plugins/modules/helm_info.py compile-2.6!skip -plugins/modules/helm_info.py compile-2.7!skip -plugins/modules/helm_info.py compile-3.5!skip -plugins/modules/helm_info.py future-import-boilerplate!skip -plugins/modules/helm_info.py import-2.6!skip -plugins/modules/helm_info.py import-2.7!skip -plugins/modules/helm_info.py import-3.5!skip -plugins/modules/helm_info.py metaclass-boilerplate!skip -plugins/modules/helm_plugin.py compile-2.6!skip -plugins/modules/helm_plugin.py compile-2.7!skip -plugins/modules/helm_plugin.py compile-3.5!skip -plugins/modules/helm_plugin.py future-import-boilerplate!skip -plugins/modules/helm_plugin.py import-2.6!skip -plugins/modules/helm_plugin.py import-2.7!skip -plugins/modules/helm_plugin.py import-3.5!skip -plugins/modules/helm_plugin.py metaclass-boilerplate!skip -plugins/modules/helm_plugin_info.py compile-2.6!skip -plugins/modules/helm_plugin_info.py compile-2.7!skip -plugins/modules/helm_plugin_info.py compile-3.5!skip -plugins/modules/helm_plugin_info.py future-import-boilerplate!skip -plugins/modules/helm_plugin_info.py import-2.6!skip -plugins/modules/helm_plugin_info.py import-2.7!skip -plugins/modules/helm_plugin_info.py import-3.5!skip -plugins/modules/helm_plugin_info.py metaclass-boilerplate!skip -plugins/modules/helm_repository.py compile-2.6!skip -plugins/modules/helm_repository.py compile-2.7!skip -plugins/modules/helm_repository.py compile-3.5!skip -plugins/modules/helm_repository.py future-import-boilerplate!skip -plugins/modules/helm_repository.py import-2.6!skip -plugins/modules/helm_repository.py import-2.7!skip -plugins/modules/helm_repository.py import-3.5!skip -plugins/modules/helm_repository.py metaclass-boilerplate!skip -plugins/modules/helm_template.py compile-2.6!skip -plugins/modules/helm_template.py compile-2.7!skip -plugins/modules/helm_template.py compile-3.5!skip -plugins/modules/helm_template.py future-import-boilerplate!skip -plugins/modules/helm_template.py import-2.6!skip -plugins/modules/helm_template.py import-2.7!skip -plugins/modules/helm_template.py import-3.5!skip -plugins/modules/helm_template.py metaclass-boilerplate!skip -plugins/modules/k8s.py compile-2.6!skip -plugins/modules/k8s.py compile-2.7!skip -plugins/modules/k8s.py compile-3.5!skip -plugins/modules/k8s.py future-import-boilerplate!skip -plugins/modules/k8s.py import-2.6!skip -plugins/modules/k8s.py import-2.7!skip -plugins/modules/k8s.py import-3.5!skip -plugins/modules/k8s.py metaclass-boilerplate!skip plugins/modules/k8s.py validate-modules:parameter-type-not-in-doc -plugins/modules/k8s.py validate-modules:return-syntax-error -plugins/modules/k8s_cluster_info.py compile-2.6!skip -plugins/modules/k8s_cluster_info.py compile-2.7!skip -plugins/modules/k8s_cluster_info.py compile-3.5!skip -plugins/modules/k8s_cluster_info.py future-import-boilerplate!skip -plugins/modules/k8s_cluster_info.py import-2.6!skip -plugins/modules/k8s_cluster_info.py import-2.7!skip -plugins/modules/k8s_cluster_info.py import-3.5!skip -plugins/modules/k8s_cluster_info.py metaclass-boilerplate!skip -plugins/modules/k8s_exec.py compile-2.6!skip -plugins/modules/k8s_exec.py compile-2.7!skip -plugins/modules/k8s_exec.py compile-3.5!skip -plugins/modules/k8s_exec.py future-import-boilerplate!skip -plugins/modules/k8s_exec.py import-2.6!skip -plugins/modules/k8s_exec.py import-2.7!skip -plugins/modules/k8s_exec.py import-3.5!skip -plugins/modules/k8s_exec.py metaclass-boilerplate!skip -plugins/modules/k8s_info.py compile-2.6!skip -plugins/modules/k8s_info.py compile-2.7!skip -plugins/modules/k8s_info.py compile-3.5!skip -plugins/modules/k8s_info.py future-import-boilerplate!skip -plugins/modules/k8s_info.py import-2.6!skip -plugins/modules/k8s_info.py import-2.7!skip -plugins/modules/k8s_info.py import-3.5!skip -plugins/modules/k8s_info.py metaclass-boilerplate!skip -plugins/modules/k8s_json_patch.py compile-2.6!skip -plugins/modules/k8s_json_patch.py compile-2.7!skip -plugins/modules/k8s_json_patch.py compile-3.5!skip -plugins/modules/k8s_json_patch.py future-import-boilerplate!skip -plugins/modules/k8s_json_patch.py import-2.6!skip -plugins/modules/k8s_json_patch.py import-2.7!skip -plugins/modules/k8s_json_patch.py import-3.5!skip -plugins/modules/k8s_json_patch.py metaclass-boilerplate!skip -plugins/modules/k8s_log.py compile-2.6!skip -plugins/modules/k8s_log.py compile-2.7!skip -plugins/modules/k8s_log.py compile-3.5!skip -plugins/modules/k8s_log.py future-import-boilerplate!skip -plugins/modules/k8s_log.py import-2.6!skip -plugins/modules/k8s_log.py import-2.7!skip -plugins/modules/k8s_log.py import-3.5!skip -plugins/modules/k8s_log.py metaclass-boilerplate!skip -plugins/modules/k8s_rollback.py compile-2.6!skip -plugins/modules/k8s_rollback.py compile-2.7!skip -plugins/modules/k8s_rollback.py compile-3.5!skip -plugins/modules/k8s_rollback.py future-import-boilerplate!skip -plugins/modules/k8s_rollback.py import-2.6!skip -plugins/modules/k8s_rollback.py import-2.7!skip -plugins/modules/k8s_rollback.py import-3.5!skip -plugins/modules/k8s_rollback.py metaclass-boilerplate!skip -plugins/modules/k8s_scale.py compile-2.6!skip -plugins/modules/k8s_scale.py compile-2.7!skip -plugins/modules/k8s_scale.py compile-3.5!skip -plugins/modules/k8s_scale.py future-import-boilerplate!skip -plugins/modules/k8s_scale.py import-2.6!skip -plugins/modules/k8s_scale.py import-2.7!skip -plugins/modules/k8s_scale.py import-3.5!skip -plugins/modules/k8s_scale.py metaclass-boilerplate!skip plugins/modules/k8s_scale.py validate-modules:parameter-type-not-in-doc -plugins/modules/k8s_scale.py validate-modules:return-syntax-error -plugins/modules/k8s_service.py compile-2.6!skip -plugins/modules/k8s_service.py compile-2.7!skip -plugins/modules/k8s_service.py compile-3.5!skip -plugins/modules/k8s_service.py future-import-boilerplate!skip -plugins/modules/k8s_service.py import-2.6!skip -plugins/modules/k8s_service.py import-2.7!skip -plugins/modules/k8s_service.py import-3.5!skip -plugins/modules/k8s_service.py metaclass-boilerplate!skip plugins/modules/k8s_service.py validate-modules:parameter-type-not-in-doc -plugins/modules/k8s_service.py validate-modules:return-syntax-error +tests/unit/module_utils/fixtures/definitions.yml yamllint!skip +tests/unit/module_utils/fixtures/deployments.yml yamllint!skip +tests/unit/module_utils/fixtures/pods.yml yamllint!skip +tests/integration/targets/helm/files/appversionless-chart-v2/templates/configmap.yaml yamllint!skip +tests/integration/targets/helm/files/appversionless-chart/templates/configmap.yaml yamllint!skip +tests/integration/targets/helm/files/test-chart-v2/templates/configmap.yaml yamllint!skip +tests/integration/targets/helm/files/test-chart/templates/configmap.yaml yamllint!skip +tests/integration/targets/k8s_scale/files/deployment.yaml yamllint!skip tests/sanity/refresh_ignore_files shebang!skip -plugins/modules/k8s_cp.py compile-2.6!skip -plugins/modules/k8s_cp.py compile-2.7!skip -plugins/modules/k8s_cp.py import-2.6!skip -plugins/modules/k8s_cp.py import-2.7!skip -plugins/module_utils/selector.py future-import-boilerplate!skip -plugins/module_utils/selector.py metaclass-boilerplate!skip -plugins/modules/k8s_taint.py compile-2.7!skip -plugins/modules/k8s_taint.py compile-3.5!skip -plugins/modules/k8s_taint.py future-import-boilerplate!skip -plugins/modules/k8s_taint.py import-2.7!skip -plugins/modules/k8s_taint.py import-3.5!skip -plugins/modules/k8s_taint.py metaclass-boilerplate!skip -plugins/modules/k8s_taint.py validate-modules:return-syntax-error \ No newline at end of file +plugins/modules/k8s.py validate-modules:return-syntax-error +plugins/modules/k8s_scale.py validate-modules:return-syntax-error +plugins/modules/k8s_service.py validate-modules:return-syntax-error +plugins/modules/k8s_taint.py validate-modules:return-syntax-error diff --git a/tests/sanity/ignore-2.13.txt b/tests/sanity/ignore-2.13.txt index cde97150..8572453e 100644 --- a/tests/sanity/ignore-2.13.txt +++ b/tests/sanity/ignore-2.13.txt @@ -1,196 +1,31 @@ -tests/integration/targets/k8s_scale/files/deployment.yaml yamllint!skip -tests/integration/targets/helm/files/appversionless-chart-v2/templates/configmap.yaml yamllint!skip -tests/integration/targets/helm/files/appversionless-chart/templates/configmap.yaml yamllint!skip -tests/integration/targets/helm/files/test-chart-v2/templates/configmap.yaml yamllint!skip -tests/integration/targets/helm/files/test-chart/templates/configmap.yaml yamllint!skip -plugins/module_utils/__init__.py compile-2.7!skip -plugins/module_utils/__init__.py compile-3.5!skip -plugins/module_utils/__init__.py future-import-boilerplate!skip -plugins/module_utils/__init__.py import-2.7!skip -plugins/module_utils/__init__.py import-3.5!skip -plugins/module_utils/__init__.py metaclass-boilerplate!skip -plugins/module_utils/ansiblemodule.py compile-2.7!skip -plugins/module_utils/ansiblemodule.py compile-3.5!skip -plugins/module_utils/ansiblemodule.py future-import-boilerplate!skip -plugins/module_utils/ansiblemodule.py import-2.7!skip -plugins/module_utils/ansiblemodule.py import-3.5!skip -plugins/module_utils/ansiblemodule.py metaclass-boilerplate!skip -plugins/module_utils/apply.py compile-2.7!skip -plugins/module_utils/apply.py compile-3.5!skip -plugins/module_utils/apply.py future-import-boilerplate!skip -plugins/module_utils/apply.py import-2.7!skip -plugins/module_utils/apply.py import-3.5!skip -plugins/module_utils/apply.py metaclass-boilerplate!skip -plugins/module_utils/args_common.py compile-2.7!skip -plugins/module_utils/args_common.py compile-3.5!skip -plugins/module_utils/args_common.py future-import-boilerplate!skip -plugins/module_utils/args_common.py import-2.7!skip -plugins/module_utils/args_common.py import-3.5!skip -plugins/module_utils/args_common.py metaclass-boilerplate!skip -plugins/module_utils/client/discovery.py future-import-boilerplate!skip -plugins/module_utils/client/discovery.py import-2.7!skip -plugins/module_utils/client/discovery.py import-3.5!skip plugins/module_utils/client/discovery.py import-3.6!skip plugins/module_utils/client/discovery.py import-3.7!skip plugins/module_utils/client/discovery.py import-3.8!skip plugins/module_utils/client/discovery.py import-3.9!skip plugins/module_utils/client/discovery.py import-3.10!skip -plugins/module_utils/client/discovery.py metaclass-boilerplate!skip -plugins/module_utils/client/resource.py import-2.7!skip -plugins/module_utils/client/resource.py import-3.5!skip plugins/module_utils/client/resource.py import-3.6!skip plugins/module_utils/client/resource.py import-3.7!skip plugins/module_utils/client/resource.py import-3.8!skip plugins/module_utils/client/resource.py import-3.9!skip plugins/module_utils/client/resource.py import-3.10!skip -plugins/module_utils/common.py compile-2.7!skip -plugins/module_utils/common.py compile-3.5!skip -plugins/module_utils/common.py future-import-boilerplate!skip -plugins/module_utils/common.py import-2.7!skip -plugins/module_utils/common.py import-3.5!skip -plugins/module_utils/common.py metaclass-boilerplate!skip -plugins/module_utils/exceptions.py compile-2.7!skip -plugins/module_utils/exceptions.py compile-3.5!skip -plugins/module_utils/exceptions.py future-import-boilerplate!skip -plugins/module_utils/exceptions.py import-2.7!skip -plugins/module_utils/exceptions.py import-3.5!skip -plugins/module_utils/exceptions.py metaclass-boilerplate!skip -plugins/module_utils/hashes.py compile-2.7!skip -plugins/module_utils/hashes.py compile-3.5!skip -plugins/module_utils/hashes.py future-import-boilerplate!skip -plugins/module_utils/hashes.py import-2.7!skip -plugins/module_utils/hashes.py import-3.5!skip -plugins/module_utils/hashes.py metaclass-boilerplate!skip -plugins/module_utils/helm.py compile-2.7!skip -plugins/module_utils/helm.py compile-3.5!skip -plugins/module_utils/helm.py future-import-boilerplate!skip -plugins/module_utils/helm.py import-2.7!skip -plugins/module_utils/helm.py import-3.5!skip -plugins/module_utils/helm.py metaclass-boilerplate!skip -plugins/module_utils/k8sdynamicclient.py compile-2.7!skip -plugins/module_utils/k8sdynamicclient.py compile-3.5!skip -plugins/module_utils/k8sdynamicclient.py future-import-boilerplate!skip -plugins/module_utils/k8sdynamicclient.py import-2.7!skip -plugins/module_utils/k8sdynamicclient.py import-3.5!skip plugins/module_utils/k8sdynamicclient.py import-3.6!skip plugins/module_utils/k8sdynamicclient.py import-3.7!skip plugins/module_utils/k8sdynamicclient.py import-3.8!skip plugins/module_utils/k8sdynamicclient.py import-3.9!skip plugins/module_utils/k8sdynamicclient.py import-3.10!skip -plugins/module_utils/k8sdynamicclient.py metaclass-boilerplate!skip -plugins/modules/__init__.py compile-2.7!skip -plugins/modules/__init__.py compile-3.5!skip -plugins/modules/__init__.py future-import-boilerplate!skip -plugins/modules/__init__.py import-2.7!skip -plugins/modules/__init__.py import-3.5!skip -plugins/modules/__init__.py metaclass-boilerplate!skip -plugins/modules/helm.py compile-2.7!skip -plugins/modules/helm.py compile-3.5!skip -plugins/modules/helm.py future-import-boilerplate!skip -plugins/modules/helm.py import-2.7!skip -plugins/modules/helm.py import-3.5!skip -plugins/modules/helm.py metaclass-boilerplate!skip -plugins/modules/helm_info.py compile-2.7!skip -plugins/modules/helm_info.py compile-3.5!skip -plugins/modules/helm_info.py future-import-boilerplate!skip -plugins/modules/helm_info.py import-2.7!skip -plugins/modules/helm_info.py import-3.5!skip -plugins/modules/helm_info.py metaclass-boilerplate!skip -plugins/modules/helm_plugin.py compile-2.7!skip -plugins/modules/helm_plugin.py compile-3.5!skip -plugins/modules/helm_plugin.py future-import-boilerplate!skip -plugins/modules/helm_plugin.py import-2.7!skip -plugins/modules/helm_plugin.py import-3.5!skip -plugins/modules/helm_plugin.py metaclass-boilerplate!skip -plugins/modules/helm_plugin_info.py compile-2.7!skip -plugins/modules/helm_plugin_info.py compile-3.5!skip -plugins/modules/helm_plugin_info.py future-import-boilerplate!skip -plugins/modules/helm_plugin_info.py import-2.7!skip -plugins/modules/helm_plugin_info.py import-3.5!skip -plugins/modules/helm_plugin_info.py metaclass-boilerplate!skip -plugins/modules/helm_repository.py compile-2.7!skip -plugins/modules/helm_repository.py compile-3.5!skip -plugins/modules/helm_repository.py future-import-boilerplate!skip -plugins/modules/helm_repository.py import-2.7!skip -plugins/modules/helm_repository.py import-3.5!skip -plugins/modules/helm_repository.py metaclass-boilerplate!skip -plugins/modules/helm_template.py compile-2.7!skip -plugins/modules/helm_template.py compile-3.5!skip -plugins/modules/helm_template.py future-import-boilerplate!skip -plugins/modules/helm_template.py import-2.7!skip -plugins/modules/helm_template.py import-3.5!skip -plugins/modules/helm_template.py metaclass-boilerplate!skip -plugins/modules/k8s.py compile-2.7!skip -plugins/modules/k8s.py compile-3.5!skip -plugins/modules/k8s.py future-import-boilerplate!skip -plugins/modules/k8s.py import-2.7!skip -plugins/modules/k8s.py import-3.5!skip -plugins/modules/k8s.py metaclass-boilerplate!skip plugins/modules/k8s.py validate-modules:parameter-type-not-in-doc -plugins/modules/k8s.py validate-modules:return-syntax-error -plugins/modules/k8s_cluster_info.py compile-2.7!skip -plugins/modules/k8s_cluster_info.py compile-3.5!skip -plugins/modules/k8s_cluster_info.py future-import-boilerplate!skip -plugins/modules/k8s_cluster_info.py import-2.7!skip -plugins/modules/k8s_cluster_info.py import-3.5!skip -plugins/modules/k8s_cluster_info.py metaclass-boilerplate!skip -plugins/modules/k8s_exec.py compile-2.7!skip -plugins/modules/k8s_exec.py compile-3.5!skip -plugins/modules/k8s_exec.py future-import-boilerplate!skip -plugins/modules/k8s_exec.py import-2.7!skip -plugins/modules/k8s_exec.py import-3.5!skip -plugins/modules/k8s_exec.py metaclass-boilerplate!skip -plugins/modules/k8s_info.py compile-2.7!skip -plugins/modules/k8s_info.py compile-3.5!skip -plugins/modules/k8s_info.py future-import-boilerplate!skip -plugins/modules/k8s_info.py import-2.7!skip -plugins/modules/k8s_info.py import-3.5!skip -plugins/modules/k8s_info.py metaclass-boilerplate!skip -plugins/modules/k8s_json_patch.py compile-2.7!skip -plugins/modules/k8s_json_patch.py compile-3.5!skip -plugins/modules/k8s_json_patch.py future-import-boilerplate!skip -plugins/modules/k8s_json_patch.py import-2.7!skip -plugins/modules/k8s_json_patch.py import-3.5!skip -plugins/modules/k8s_json_patch.py metaclass-boilerplate!skip -plugins/modules/k8s_log.py compile-2.7!skip -plugins/modules/k8s_log.py compile-3.5!skip -plugins/modules/k8s_log.py future-import-boilerplate!skip -plugins/modules/k8s_log.py import-2.7!skip -plugins/modules/k8s_log.py import-3.5!skip -plugins/modules/k8s_log.py metaclass-boilerplate!skip -plugins/modules/k8s_rollback.py compile-2.7!skip -plugins/modules/k8s_rollback.py compile-3.5!skip -plugins/modules/k8s_rollback.py future-import-boilerplate!skip -plugins/modules/k8s_rollback.py import-2.7!skip -plugins/modules/k8s_rollback.py import-3.5!skip -plugins/modules/k8s_rollback.py metaclass-boilerplate!skip -plugins/modules/k8s_scale.py compile-2.7!skip -plugins/modules/k8s_scale.py compile-3.5!skip -plugins/modules/k8s_scale.py future-import-boilerplate!skip -plugins/modules/k8s_scale.py import-2.7!skip -plugins/modules/k8s_scale.py import-3.5!skip -plugins/modules/k8s_scale.py metaclass-boilerplate!skip plugins/modules/k8s_scale.py validate-modules:parameter-type-not-in-doc -plugins/modules/k8s_scale.py validate-modules:return-syntax-error -plugins/modules/k8s_service.py compile-2.7!skip -plugins/modules/k8s_service.py compile-3.5!skip -plugins/modules/k8s_service.py future-import-boilerplate!skip -plugins/modules/k8s_service.py import-2.7!skip -plugins/modules/k8s_service.py import-3.5!skip -plugins/modules/k8s_service.py metaclass-boilerplate!skip plugins/modules/k8s_service.py validate-modules:parameter-type-not-in-doc -plugins/modules/k8s_service.py validate-modules:return-syntax-error +tests/unit/module_utils/fixtures/definitions.yml yamllint!skip +tests/unit/module_utils/fixtures/deployments.yml yamllint!skip +tests/unit/module_utils/fixtures/pods.yml yamllint!skip +tests/integration/targets/helm/files/appversionless-chart-v2/templates/configmap.yaml yamllint!skip +tests/integration/targets/helm/files/appversionless-chart/templates/configmap.yaml yamllint!skip +tests/integration/targets/helm/files/test-chart-v2/templates/configmap.yaml yamllint!skip +tests/integration/targets/helm/files/test-chart/templates/configmap.yaml yamllint!skip +tests/integration/targets/k8s_scale/files/deployment.yaml yamllint!skip tests/sanity/refresh_ignore_files shebang!skip -plugins/modules/k8s_cp.py compile-2.7!skip -plugins/modules/k8s_cp.py import-2.7!skip -plugins/module_utils/selector.py future-import-boilerplate!skip -plugins/module_utils/selector.py metaclass-boilerplate!skip -plugins/modules/k8s_taint.py compile-2.7!skip -plugins/modules/k8s_taint.py compile-3.5!skip -plugins/modules/k8s_taint.py future-import-boilerplate!skip -plugins/modules/k8s_taint.py import-2.7!skip -plugins/modules/k8s_taint.py import-3.5!skip -plugins/modules/k8s_taint.py metaclass-boilerplate!skip +plugins/modules/k8s.py validate-modules:return-syntax-error +plugins/modules/k8s_scale.py validate-modules:return-syntax-error +plugins/modules/k8s_service.py validate-modules:return-syntax-error plugins/modules/k8s_taint.py validate-modules:return-syntax-error -plugins/module_utils/copy.py future-import-boilerplate!skip -plugins/module_utils/copy.py metaclass-boilerplate!skip diff --git a/tests/sanity/ignore-2.14.txt b/tests/sanity/ignore-2.14.txt index cde97150..8572453e 100644 --- a/tests/sanity/ignore-2.14.txt +++ b/tests/sanity/ignore-2.14.txt @@ -1,196 +1,31 @@ -tests/integration/targets/k8s_scale/files/deployment.yaml yamllint!skip -tests/integration/targets/helm/files/appversionless-chart-v2/templates/configmap.yaml yamllint!skip -tests/integration/targets/helm/files/appversionless-chart/templates/configmap.yaml yamllint!skip -tests/integration/targets/helm/files/test-chart-v2/templates/configmap.yaml yamllint!skip -tests/integration/targets/helm/files/test-chart/templates/configmap.yaml yamllint!skip -plugins/module_utils/__init__.py compile-2.7!skip -plugins/module_utils/__init__.py compile-3.5!skip -plugins/module_utils/__init__.py future-import-boilerplate!skip -plugins/module_utils/__init__.py import-2.7!skip -plugins/module_utils/__init__.py import-3.5!skip -plugins/module_utils/__init__.py metaclass-boilerplate!skip -plugins/module_utils/ansiblemodule.py compile-2.7!skip -plugins/module_utils/ansiblemodule.py compile-3.5!skip -plugins/module_utils/ansiblemodule.py future-import-boilerplate!skip -plugins/module_utils/ansiblemodule.py import-2.7!skip -plugins/module_utils/ansiblemodule.py import-3.5!skip -plugins/module_utils/ansiblemodule.py metaclass-boilerplate!skip -plugins/module_utils/apply.py compile-2.7!skip -plugins/module_utils/apply.py compile-3.5!skip -plugins/module_utils/apply.py future-import-boilerplate!skip -plugins/module_utils/apply.py import-2.7!skip -plugins/module_utils/apply.py import-3.5!skip -plugins/module_utils/apply.py metaclass-boilerplate!skip -plugins/module_utils/args_common.py compile-2.7!skip -plugins/module_utils/args_common.py compile-3.5!skip -plugins/module_utils/args_common.py future-import-boilerplate!skip -plugins/module_utils/args_common.py import-2.7!skip -plugins/module_utils/args_common.py import-3.5!skip -plugins/module_utils/args_common.py metaclass-boilerplate!skip -plugins/module_utils/client/discovery.py future-import-boilerplate!skip -plugins/module_utils/client/discovery.py import-2.7!skip -plugins/module_utils/client/discovery.py import-3.5!skip plugins/module_utils/client/discovery.py import-3.6!skip plugins/module_utils/client/discovery.py import-3.7!skip plugins/module_utils/client/discovery.py import-3.8!skip plugins/module_utils/client/discovery.py import-3.9!skip plugins/module_utils/client/discovery.py import-3.10!skip -plugins/module_utils/client/discovery.py metaclass-boilerplate!skip -plugins/module_utils/client/resource.py import-2.7!skip -plugins/module_utils/client/resource.py import-3.5!skip plugins/module_utils/client/resource.py import-3.6!skip plugins/module_utils/client/resource.py import-3.7!skip plugins/module_utils/client/resource.py import-3.8!skip plugins/module_utils/client/resource.py import-3.9!skip plugins/module_utils/client/resource.py import-3.10!skip -plugins/module_utils/common.py compile-2.7!skip -plugins/module_utils/common.py compile-3.5!skip -plugins/module_utils/common.py future-import-boilerplate!skip -plugins/module_utils/common.py import-2.7!skip -plugins/module_utils/common.py import-3.5!skip -plugins/module_utils/common.py metaclass-boilerplate!skip -plugins/module_utils/exceptions.py compile-2.7!skip -plugins/module_utils/exceptions.py compile-3.5!skip -plugins/module_utils/exceptions.py future-import-boilerplate!skip -plugins/module_utils/exceptions.py import-2.7!skip -plugins/module_utils/exceptions.py import-3.5!skip -plugins/module_utils/exceptions.py metaclass-boilerplate!skip -plugins/module_utils/hashes.py compile-2.7!skip -plugins/module_utils/hashes.py compile-3.5!skip -plugins/module_utils/hashes.py future-import-boilerplate!skip -plugins/module_utils/hashes.py import-2.7!skip -plugins/module_utils/hashes.py import-3.5!skip -plugins/module_utils/hashes.py metaclass-boilerplate!skip -plugins/module_utils/helm.py compile-2.7!skip -plugins/module_utils/helm.py compile-3.5!skip -plugins/module_utils/helm.py future-import-boilerplate!skip -plugins/module_utils/helm.py import-2.7!skip -plugins/module_utils/helm.py import-3.5!skip -plugins/module_utils/helm.py metaclass-boilerplate!skip -plugins/module_utils/k8sdynamicclient.py compile-2.7!skip -plugins/module_utils/k8sdynamicclient.py compile-3.5!skip -plugins/module_utils/k8sdynamicclient.py future-import-boilerplate!skip -plugins/module_utils/k8sdynamicclient.py import-2.7!skip -plugins/module_utils/k8sdynamicclient.py import-3.5!skip plugins/module_utils/k8sdynamicclient.py import-3.6!skip plugins/module_utils/k8sdynamicclient.py import-3.7!skip plugins/module_utils/k8sdynamicclient.py import-3.8!skip plugins/module_utils/k8sdynamicclient.py import-3.9!skip plugins/module_utils/k8sdynamicclient.py import-3.10!skip -plugins/module_utils/k8sdynamicclient.py metaclass-boilerplate!skip -plugins/modules/__init__.py compile-2.7!skip -plugins/modules/__init__.py compile-3.5!skip -plugins/modules/__init__.py future-import-boilerplate!skip -plugins/modules/__init__.py import-2.7!skip -plugins/modules/__init__.py import-3.5!skip -plugins/modules/__init__.py metaclass-boilerplate!skip -plugins/modules/helm.py compile-2.7!skip -plugins/modules/helm.py compile-3.5!skip -plugins/modules/helm.py future-import-boilerplate!skip -plugins/modules/helm.py import-2.7!skip -plugins/modules/helm.py import-3.5!skip -plugins/modules/helm.py metaclass-boilerplate!skip -plugins/modules/helm_info.py compile-2.7!skip -plugins/modules/helm_info.py compile-3.5!skip -plugins/modules/helm_info.py future-import-boilerplate!skip -plugins/modules/helm_info.py import-2.7!skip -plugins/modules/helm_info.py import-3.5!skip -plugins/modules/helm_info.py metaclass-boilerplate!skip -plugins/modules/helm_plugin.py compile-2.7!skip -plugins/modules/helm_plugin.py compile-3.5!skip -plugins/modules/helm_plugin.py future-import-boilerplate!skip -plugins/modules/helm_plugin.py import-2.7!skip -plugins/modules/helm_plugin.py import-3.5!skip -plugins/modules/helm_plugin.py metaclass-boilerplate!skip -plugins/modules/helm_plugin_info.py compile-2.7!skip -plugins/modules/helm_plugin_info.py compile-3.5!skip -plugins/modules/helm_plugin_info.py future-import-boilerplate!skip -plugins/modules/helm_plugin_info.py import-2.7!skip -plugins/modules/helm_plugin_info.py import-3.5!skip -plugins/modules/helm_plugin_info.py metaclass-boilerplate!skip -plugins/modules/helm_repository.py compile-2.7!skip -plugins/modules/helm_repository.py compile-3.5!skip -plugins/modules/helm_repository.py future-import-boilerplate!skip -plugins/modules/helm_repository.py import-2.7!skip -plugins/modules/helm_repository.py import-3.5!skip -plugins/modules/helm_repository.py metaclass-boilerplate!skip -plugins/modules/helm_template.py compile-2.7!skip -plugins/modules/helm_template.py compile-3.5!skip -plugins/modules/helm_template.py future-import-boilerplate!skip -plugins/modules/helm_template.py import-2.7!skip -plugins/modules/helm_template.py import-3.5!skip -plugins/modules/helm_template.py metaclass-boilerplate!skip -plugins/modules/k8s.py compile-2.7!skip -plugins/modules/k8s.py compile-3.5!skip -plugins/modules/k8s.py future-import-boilerplate!skip -plugins/modules/k8s.py import-2.7!skip -plugins/modules/k8s.py import-3.5!skip -plugins/modules/k8s.py metaclass-boilerplate!skip plugins/modules/k8s.py validate-modules:parameter-type-not-in-doc -plugins/modules/k8s.py validate-modules:return-syntax-error -plugins/modules/k8s_cluster_info.py compile-2.7!skip -plugins/modules/k8s_cluster_info.py compile-3.5!skip -plugins/modules/k8s_cluster_info.py future-import-boilerplate!skip -plugins/modules/k8s_cluster_info.py import-2.7!skip -plugins/modules/k8s_cluster_info.py import-3.5!skip -plugins/modules/k8s_cluster_info.py metaclass-boilerplate!skip -plugins/modules/k8s_exec.py compile-2.7!skip -plugins/modules/k8s_exec.py compile-3.5!skip -plugins/modules/k8s_exec.py future-import-boilerplate!skip -plugins/modules/k8s_exec.py import-2.7!skip -plugins/modules/k8s_exec.py import-3.5!skip -plugins/modules/k8s_exec.py metaclass-boilerplate!skip -plugins/modules/k8s_info.py compile-2.7!skip -plugins/modules/k8s_info.py compile-3.5!skip -plugins/modules/k8s_info.py future-import-boilerplate!skip -plugins/modules/k8s_info.py import-2.7!skip -plugins/modules/k8s_info.py import-3.5!skip -plugins/modules/k8s_info.py metaclass-boilerplate!skip -plugins/modules/k8s_json_patch.py compile-2.7!skip -plugins/modules/k8s_json_patch.py compile-3.5!skip -plugins/modules/k8s_json_patch.py future-import-boilerplate!skip -plugins/modules/k8s_json_patch.py import-2.7!skip -plugins/modules/k8s_json_patch.py import-3.5!skip -plugins/modules/k8s_json_patch.py metaclass-boilerplate!skip -plugins/modules/k8s_log.py compile-2.7!skip -plugins/modules/k8s_log.py compile-3.5!skip -plugins/modules/k8s_log.py future-import-boilerplate!skip -plugins/modules/k8s_log.py import-2.7!skip -plugins/modules/k8s_log.py import-3.5!skip -plugins/modules/k8s_log.py metaclass-boilerplate!skip -plugins/modules/k8s_rollback.py compile-2.7!skip -plugins/modules/k8s_rollback.py compile-3.5!skip -plugins/modules/k8s_rollback.py future-import-boilerplate!skip -plugins/modules/k8s_rollback.py import-2.7!skip -plugins/modules/k8s_rollback.py import-3.5!skip -plugins/modules/k8s_rollback.py metaclass-boilerplate!skip -plugins/modules/k8s_scale.py compile-2.7!skip -plugins/modules/k8s_scale.py compile-3.5!skip -plugins/modules/k8s_scale.py future-import-boilerplate!skip -plugins/modules/k8s_scale.py import-2.7!skip -plugins/modules/k8s_scale.py import-3.5!skip -plugins/modules/k8s_scale.py metaclass-boilerplate!skip plugins/modules/k8s_scale.py validate-modules:parameter-type-not-in-doc -plugins/modules/k8s_scale.py validate-modules:return-syntax-error -plugins/modules/k8s_service.py compile-2.7!skip -plugins/modules/k8s_service.py compile-3.5!skip -plugins/modules/k8s_service.py future-import-boilerplate!skip -plugins/modules/k8s_service.py import-2.7!skip -plugins/modules/k8s_service.py import-3.5!skip -plugins/modules/k8s_service.py metaclass-boilerplate!skip plugins/modules/k8s_service.py validate-modules:parameter-type-not-in-doc -plugins/modules/k8s_service.py validate-modules:return-syntax-error +tests/unit/module_utils/fixtures/definitions.yml yamllint!skip +tests/unit/module_utils/fixtures/deployments.yml yamllint!skip +tests/unit/module_utils/fixtures/pods.yml yamllint!skip +tests/integration/targets/helm/files/appversionless-chart-v2/templates/configmap.yaml yamllint!skip +tests/integration/targets/helm/files/appversionless-chart/templates/configmap.yaml yamllint!skip +tests/integration/targets/helm/files/test-chart-v2/templates/configmap.yaml yamllint!skip +tests/integration/targets/helm/files/test-chart/templates/configmap.yaml yamllint!skip +tests/integration/targets/k8s_scale/files/deployment.yaml yamllint!skip tests/sanity/refresh_ignore_files shebang!skip -plugins/modules/k8s_cp.py compile-2.7!skip -plugins/modules/k8s_cp.py import-2.7!skip -plugins/module_utils/selector.py future-import-boilerplate!skip -plugins/module_utils/selector.py metaclass-boilerplate!skip -plugins/modules/k8s_taint.py compile-2.7!skip -plugins/modules/k8s_taint.py compile-3.5!skip -plugins/modules/k8s_taint.py future-import-boilerplate!skip -plugins/modules/k8s_taint.py import-2.7!skip -plugins/modules/k8s_taint.py import-3.5!skip -plugins/modules/k8s_taint.py metaclass-boilerplate!skip +plugins/modules/k8s.py validate-modules:return-syntax-error +plugins/modules/k8s_scale.py validate-modules:return-syntax-error +plugins/modules/k8s_service.py validate-modules:return-syntax-error plugins/modules/k8s_taint.py validate-modules:return-syntax-error -plugins/module_utils/copy.py future-import-boilerplate!skip -plugins/module_utils/copy.py metaclass-boilerplate!skip diff --git a/tests/sanity/ignore-2.9.txt b/tests/sanity/ignore-2.9.txt index 0a58a934..ce09f79c 100644 --- a/tests/sanity/ignore-2.9.txt +++ b/tests/sanity/ignore-2.9.txt @@ -1,255 +1,171 @@ -tests/integration/targets/k8s_scale/files/deployment.yaml yamllint!skip +plugins/module_utils/client/discovery.py import-3.6!skip +plugins/module_utils/client/discovery.py import-3.7!skip +plugins/module_utils/client/discovery.py import-3.8!skip +plugins/module_utils/client/resource.py import-3.6!skip +plugins/module_utils/client/resource.py import-3.7!skip +plugins/module_utils/client/resource.py import-3.8!skip +plugins/module_utils/k8sdynamicclient.py import-3.6!skip +plugins/module_utils/k8sdynamicclient.py import-3.7!skip +plugins/module_utils/k8sdynamicclient.py import-3.8!skip +plugins/modules/k8s.py validate-modules:parameter-type-not-in-doc +plugins/modules/k8s_scale.py validate-modules:parameter-type-not-in-doc +plugins/modules/k8s_service.py validate-modules:parameter-type-not-in-doc +tests/unit/module_utils/fixtures/definitions.yml yamllint!skip +tests/unit/module_utils/fixtures/deployments.yml yamllint!skip +tests/unit/module_utils/fixtures/pods.yml yamllint!skip tests/integration/targets/helm/files/appversionless-chart-v2/templates/configmap.yaml yamllint!skip tests/integration/targets/helm/files/appversionless-chart/templates/configmap.yaml yamllint!skip tests/integration/targets/helm/files/test-chart-v2/templates/configmap.yaml yamllint!skip tests/integration/targets/helm/files/test-chart/templates/configmap.yaml yamllint!skip -plugins/module_utils/__init__.py compile-2.6!skip -plugins/module_utils/__init__.py compile-2.7!skip -plugins/module_utils/__init__.py compile-3.5!skip -plugins/module_utils/__init__.py future-import-boilerplate!skip -plugins/module_utils/__init__.py import-2.6!skip -plugins/module_utils/__init__.py import-2.7!skip -plugins/module_utils/__init__.py import-3.5!skip -plugins/module_utils/__init__.py metaclass-boilerplate!skip -plugins/module_utils/ansiblemodule.py compile-2.6!skip -plugins/module_utils/ansiblemodule.py compile-2.7!skip -plugins/module_utils/ansiblemodule.py compile-3.5!skip -plugins/module_utils/ansiblemodule.py future-import-boilerplate!skip -plugins/module_utils/ansiblemodule.py import-2.6!skip -plugins/module_utils/ansiblemodule.py import-2.7!skip -plugins/module_utils/ansiblemodule.py import-3.5!skip -plugins/module_utils/ansiblemodule.py metaclass-boilerplate!skip -plugins/module_utils/apply.py compile-2.6!skip -plugins/module_utils/apply.py compile-2.7!skip -plugins/module_utils/apply.py compile-3.5!skip -plugins/module_utils/apply.py future-import-boilerplate!skip -plugins/module_utils/apply.py import-2.6!skip -plugins/module_utils/apply.py import-2.7!skip -plugins/module_utils/apply.py import-3.5!skip -plugins/module_utils/apply.py metaclass-boilerplate!skip -plugins/module_utils/args_common.py compile-2.6!skip -plugins/module_utils/args_common.py compile-2.7!skip -plugins/module_utils/args_common.py compile-3.5!skip -plugins/module_utils/args_common.py future-import-boilerplate!skip -plugins/module_utils/args_common.py import-2.6!skip -plugins/module_utils/args_common.py import-2.7!skip -plugins/module_utils/args_common.py import-3.5!skip -plugins/module_utils/args_common.py metaclass-boilerplate!skip -plugins/module_utils/client/discovery.py future-import-boilerplate!skip -plugins/module_utils/client/discovery.py import-2.6!skip -plugins/module_utils/client/discovery.py import-2.7!skip -plugins/module_utils/client/discovery.py import-3.5!skip -plugins/module_utils/client/discovery.py import-3.6!skip -plugins/module_utils/client/discovery.py import-3.7!skip -plugins/module_utils/client/discovery.py import-3.8!skip -plugins/module_utils/client/discovery.py metaclass-boilerplate!skip -plugins/module_utils/client/resource.py import-2.6!skip -plugins/module_utils/client/resource.py import-2.7!skip -plugins/module_utils/client/resource.py import-3.5!skip -plugins/module_utils/client/resource.py import-3.6!skip -plugins/module_utils/client/resource.py import-3.7!skip -plugins/module_utils/client/resource.py import-3.8!skip -plugins/module_utils/common.py compile-2.6!skip -plugins/module_utils/common.py compile-2.7!skip -plugins/module_utils/common.py compile-3.5!skip -plugins/module_utils/common.py future-import-boilerplate!skip -plugins/module_utils/common.py import-2.6!skip -plugins/module_utils/common.py import-2.7!skip -plugins/module_utils/common.py import-3.5!skip -plugins/module_utils/common.py metaclass-boilerplate!skip -plugins/module_utils/exceptions.py compile-2.6!skip -plugins/module_utils/exceptions.py compile-2.7!skip -plugins/module_utils/exceptions.py compile-3.5!skip -plugins/module_utils/exceptions.py future-import-boilerplate!skip -plugins/module_utils/exceptions.py import-2.6!skip -plugins/module_utils/exceptions.py import-2.7!skip -plugins/module_utils/exceptions.py import-3.5!skip -plugins/module_utils/exceptions.py metaclass-boilerplate!skip -plugins/module_utils/hashes.py compile-2.6!skip -plugins/module_utils/hashes.py compile-2.7!skip -plugins/module_utils/hashes.py compile-3.5!skip -plugins/module_utils/hashes.py future-import-boilerplate!skip -plugins/module_utils/hashes.py import-2.6!skip -plugins/module_utils/hashes.py import-2.7!skip -plugins/module_utils/hashes.py import-3.5!skip -plugins/module_utils/hashes.py metaclass-boilerplate!skip -plugins/module_utils/helm.py compile-2.6!skip -plugins/module_utils/helm.py compile-2.7!skip -plugins/module_utils/helm.py compile-3.5!skip -plugins/module_utils/helm.py future-import-boilerplate!skip -plugins/module_utils/helm.py import-2.6!skip -plugins/module_utils/helm.py import-2.7!skip -plugins/module_utils/helm.py import-3.5!skip -plugins/module_utils/helm.py metaclass-boilerplate!skip -plugins/module_utils/k8sdynamicclient.py compile-2.6!skip -plugins/module_utils/k8sdynamicclient.py compile-2.7!skip -plugins/module_utils/k8sdynamicclient.py compile-3.5!skip -plugins/module_utils/k8sdynamicclient.py future-import-boilerplate!skip -plugins/module_utils/k8sdynamicclient.py import-2.6!skip -plugins/module_utils/k8sdynamicclient.py import-2.7!skip -plugins/module_utils/k8sdynamicclient.py import-3.5!skip -plugins/module_utils/k8sdynamicclient.py import-3.6!skip -plugins/module_utils/k8sdynamicclient.py import-3.7!skip -plugins/module_utils/k8sdynamicclient.py import-3.8!skip -plugins/module_utils/k8sdynamicclient.py metaclass-boilerplate!skip -plugins/modules/__init__.py compile-2.6!skip -plugins/modules/__init__.py compile-2.7!skip -plugins/modules/__init__.py compile-3.5!skip -plugins/modules/__init__.py future-import-boilerplate!skip -plugins/modules/__init__.py import-2.6!skip -plugins/modules/__init__.py import-2.7!skip -plugins/modules/__init__.py import-3.5!skip -plugins/modules/__init__.py metaclass-boilerplate!skip -plugins/modules/helm.py compile-2.6!skip -plugins/modules/helm.py compile-2.7!skip -plugins/modules/helm.py compile-3.5!skip -plugins/modules/helm.py future-import-boilerplate!skip -plugins/modules/helm.py import-2.6!skip -plugins/modules/helm.py import-2.7!skip -plugins/modules/helm.py import-3.5!skip -plugins/modules/helm.py metaclass-boilerplate!skip -plugins/modules/helm_info.py compile-2.6!skip -plugins/modules/helm_info.py compile-2.7!skip -plugins/modules/helm_info.py compile-3.5!skip -plugins/modules/helm_info.py future-import-boilerplate!skip -plugins/modules/helm_info.py import-2.6!skip -plugins/modules/helm_info.py import-2.7!skip -plugins/modules/helm_info.py import-3.5!skip -plugins/modules/helm_info.py metaclass-boilerplate!skip -plugins/modules/helm_plugin.py compile-2.6!skip -plugins/modules/helm_plugin.py compile-2.7!skip -plugins/modules/helm_plugin.py compile-3.5!skip -plugins/modules/helm_plugin.py future-import-boilerplate!skip -plugins/modules/helm_plugin.py import-2.6!skip -plugins/modules/helm_plugin.py import-2.7!skip -plugins/modules/helm_plugin.py import-3.5!skip -plugins/modules/helm_plugin.py metaclass-boilerplate!skip -plugins/modules/helm_plugin_info.py compile-2.6!skip -plugins/modules/helm_plugin_info.py compile-2.7!skip -plugins/modules/helm_plugin_info.py compile-3.5!skip -plugins/modules/helm_plugin_info.py future-import-boilerplate!skip -plugins/modules/helm_plugin_info.py import-2.6!skip -plugins/modules/helm_plugin_info.py import-2.7!skip -plugins/modules/helm_plugin_info.py import-3.5!skip -plugins/modules/helm_plugin_info.py metaclass-boilerplate!skip -plugins/modules/helm_repository.py compile-2.6!skip -plugins/modules/helm_repository.py compile-2.7!skip -plugins/modules/helm_repository.py compile-3.5!skip -plugins/modules/helm_repository.py future-import-boilerplate!skip -plugins/modules/helm_repository.py import-2.6!skip -plugins/modules/helm_repository.py import-2.7!skip -plugins/modules/helm_repository.py import-3.5!skip -plugins/modules/helm_repository.py metaclass-boilerplate!skip -plugins/modules/helm_template.py compile-2.6!skip -plugins/modules/helm_template.py compile-2.7!skip -plugins/modules/helm_template.py compile-3.5!skip -plugins/modules/helm_template.py future-import-boilerplate!skip -plugins/modules/helm_template.py import-2.6!skip -plugins/modules/helm_template.py import-2.7!skip -plugins/modules/helm_template.py import-3.5!skip -plugins/modules/helm_template.py metaclass-boilerplate!skip -plugins/modules/k8s.py compile-2.6!skip -plugins/modules/k8s.py compile-2.7!skip -plugins/modules/k8s.py compile-3.5!skip -plugins/modules/k8s.py future-import-boilerplate!skip -plugins/modules/k8s.py import-2.6!skip -plugins/modules/k8s.py import-2.7!skip -plugins/modules/k8s.py import-3.5!skip -plugins/modules/k8s.py metaclass-boilerplate!skip -plugins/modules/k8s.py validate-modules:parameter-type-not-in-doc -plugins/modules/k8s_cluster_info.py compile-2.6!skip -plugins/modules/k8s_cluster_info.py compile-2.7!skip -plugins/modules/k8s_cluster_info.py compile-3.5!skip -plugins/modules/k8s_cluster_info.py future-import-boilerplate!skip -plugins/modules/k8s_cluster_info.py import-2.6!skip -plugins/modules/k8s_cluster_info.py import-2.7!skip -plugins/modules/k8s_cluster_info.py import-3.5!skip -plugins/modules/k8s_cluster_info.py metaclass-boilerplate!skip -plugins/modules/k8s_exec.py compile-2.6!skip -plugins/modules/k8s_exec.py compile-2.7!skip -plugins/modules/k8s_exec.py compile-3.5!skip -plugins/modules/k8s_exec.py future-import-boilerplate!skip -plugins/modules/k8s_exec.py import-2.6!skip -plugins/modules/k8s_exec.py import-2.7!skip -plugins/modules/k8s_exec.py import-3.5!skip -plugins/modules/k8s_exec.py metaclass-boilerplate!skip -plugins/modules/k8s_info.py compile-2.6!skip -plugins/modules/k8s_info.py compile-2.7!skip -plugins/modules/k8s_info.py compile-3.5!skip -plugins/modules/k8s_info.py future-import-boilerplate!skip -plugins/modules/k8s_info.py import-2.6!skip -plugins/modules/k8s_info.py import-2.7!skip -plugins/modules/k8s_info.py import-3.5!skip -plugins/modules/k8s_info.py metaclass-boilerplate!skip -plugins/modules/k8s_json_patch.py compile-2.6!skip -plugins/modules/k8s_json_patch.py compile-2.7!skip -plugins/modules/k8s_json_patch.py compile-3.5!skip -plugins/modules/k8s_json_patch.py future-import-boilerplate!skip -plugins/modules/k8s_json_patch.py import-2.6!skip -plugins/modules/k8s_json_patch.py import-2.7!skip -plugins/modules/k8s_json_patch.py import-3.5!skip -plugins/modules/k8s_json_patch.py metaclass-boilerplate!skip -plugins/modules/k8s_log.py compile-2.6!skip -plugins/modules/k8s_log.py compile-2.7!skip -plugins/modules/k8s_log.py compile-3.5!skip -plugins/modules/k8s_log.py future-import-boilerplate!skip -plugins/modules/k8s_log.py import-2.6!skip -plugins/modules/k8s_log.py import-2.7!skip -plugins/modules/k8s_log.py import-3.5!skip -plugins/modules/k8s_log.py metaclass-boilerplate!skip -plugins/modules/k8s_rollback.py compile-2.6!skip -plugins/modules/k8s_rollback.py compile-2.7!skip -plugins/modules/k8s_rollback.py compile-3.5!skip -plugins/modules/k8s_rollback.py future-import-boilerplate!skip -plugins/modules/k8s_rollback.py import-2.6!skip -plugins/modules/k8s_rollback.py import-2.7!skip -plugins/modules/k8s_rollback.py import-3.5!skip -plugins/modules/k8s_rollback.py metaclass-boilerplate!skip -plugins/modules/k8s_scale.py compile-2.6!skip -plugins/modules/k8s_scale.py compile-2.7!skip -plugins/modules/k8s_scale.py compile-3.5!skip -plugins/modules/k8s_scale.py future-import-boilerplate!skip -plugins/modules/k8s_scale.py import-2.6!skip -plugins/modules/k8s_scale.py import-2.7!skip -plugins/modules/k8s_scale.py import-3.5!skip -plugins/modules/k8s_scale.py metaclass-boilerplate!skip -plugins/modules/k8s_scale.py validate-modules:parameter-type-not-in-doc -plugins/modules/k8s_service.py compile-2.6!skip -plugins/modules/k8s_service.py compile-2.7!skip -plugins/modules/k8s_service.py compile-3.5!skip -plugins/modules/k8s_service.py future-import-boilerplate!skip -plugins/modules/k8s_service.py import-2.6!skip -plugins/modules/k8s_service.py import-2.7!skip -plugins/modules/k8s_service.py import-3.5!skip -plugins/modules/k8s_service.py metaclass-boilerplate!skip -plugins/modules/k8s_service.py validate-modules:parameter-type-not-in-doc +tests/integration/targets/k8s_scale/files/deployment.yaml yamllint!skip tests/sanity/refresh_ignore_files shebang!skip -tests/unit/module_utils/test_discoverer.py future-import-boilerplate!skip -tests/unit/module_utils/test_discoverer.py metaclass-boilerplate!skip -plugins/modules/k8s_cp.py compile-2.6!skip -plugins/modules/k8s_cp.py compile-2.7!skip -plugins/modules/k8s_cp.py import-2.6!skip -plugins/modules/k8s_cp.py import-2.7!skip -tests/integration/targets/k8s_copy/library/kubectl_file_compare.py compile-2.6!skip -tests/integration/targets/k8s_copy/library/kubectl_file_compare.py compile-2.7!skip -tests/integration/targets/k8s_copy/library/kubectl_file_compare.py compile-3.5!skip -tests/unit/module_utils/test_selector.py future-import-boilerplate!skip -tests/unit/module_utils/test_selector.py metaclass-boilerplate!skip -plugins/module_utils/selector.py future-import-boilerplate!skip -plugins/module_utils/selector.py metaclass-boilerplate!skip -plugins/lookup/kustomize.py future-import-boilerplate!skip -plugins/lookup/kustomize.py metaclass-boilerplate!skip -tests/integration/targets/helm/library/helm_test_version.py metaclass-boilerplate!skip -tests/integration/targets/helm/library/helm_test_version.py future-import-boilerplate!skip -plugins/modules/k8s_taint.py compile-2.7!skip -plugins/modules/k8s_taint.py compile-3.5!skip -plugins/modules/k8s_taint.py future-import-boilerplate!skip -plugins/modules/k8s_taint.py import-2.7!skip -plugins/modules/k8s_taint.py import-3.5!skip -plugins/modules/k8s_taint.py metaclass-boilerplate!skip +plugins/doc_fragments/k8s_name_options.py future-import-boilerplate!skip +plugins/doc_fragments/k8s_auth_options.py future-import-boilerplate!skip +plugins/doc_fragments/helm_common_options.py future-import-boilerplate!skip +plugins/doc_fragments/k8s_state_options.py future-import-boilerplate!skip +plugins/doc_fragments/k8s_wait_options.py future-import-boilerplate!skip +plugins/doc_fragments/k8s_scale_options.py future-import-boilerplate!skip +plugins/doc_fragments/k8s_delete_options.py future-import-boilerplate!skip +plugins/doc_fragments/__init__.py future-import-boilerplate!skip +plugins/doc_fragments/k8s_resource_options.py future-import-boilerplate!skip +plugins/module_utils/helm.py future-import-boilerplate!skip +plugins/module_utils/apply.py future-import-boilerplate!skip +plugins/module_utils/hashes.py future-import-boilerplate!skip +plugins/module_utils/version.py future-import-boilerplate!skip +plugins/module_utils/_version.py future-import-boilerplate!skip plugins/module_utils/copy.py future-import-boilerplate!skip +plugins/module_utils/args_common.py future-import-boilerplate!skip +plugins/module_utils/__init__.py future-import-boilerplate!skip +plugins/module_utils/selector.py future-import-boilerplate!skip +plugins/module_utils/k8sdynamicclient.py future-import-boilerplate!skip +plugins/module_utils/common.py future-import-boilerplate!skip +plugins/module_utils/ansiblemodule.py future-import-boilerplate!skip +plugins/module_utils/exceptions.py future-import-boilerplate!skip +plugins/module_utils/client/resource.py future-import-boilerplate!skip +plugins/module_utils/client/discovery.py future-import-boilerplate!skip +plugins/module_utils/k8s/resource.py future-import-boilerplate!skip +plugins/module_utils/k8s/core.py future-import-boilerplate!skip +plugins/module_utils/k8s/waiter.py future-import-boilerplate!skip +plugins/module_utils/k8s/client.py future-import-boilerplate!skip +plugins/module_utils/k8s/runner.py future-import-boilerplate!skip +plugins/module_utils/k8s/service.py future-import-boilerplate!skip +plugins/module_utils/k8s/exceptions.py future-import-boilerplate!skip +plugins/connection/kubectl.py future-import-boilerplate!skip +plugins/inventory/k8s.py future-import-boilerplate!skip +plugins/lookup/k8s.py future-import-boilerplate!skip +plugins/lookup/kustomize.py future-import-boilerplate!skip +plugins/modules/k8s_scale.py future-import-boilerplate!skip +plugins/modules/helm_template.py future-import-boilerplate!skip +plugins/modules/k8s_exec.py future-import-boilerplate!skip +plugins/modules/helm.py future-import-boilerplate!skip +plugins/modules/helm_plugin_info.py future-import-boilerplate!skip +plugins/modules/helm_info.py future-import-boilerplate!skip +plugins/modules/helm_repository.py future-import-boilerplate!skip +plugins/modules/k8s_rollback.py future-import-boilerplate!skip +plugins/modules/k8s_log.py future-import-boilerplate!skip +plugins/modules/k8s_drain.py future-import-boilerplate!skip +plugins/modules/helm_plugin.py future-import-boilerplate!skip +plugins/modules/k8s_taint.py future-import-boilerplate!skip +plugins/modules/k8s.py future-import-boilerplate!skip +plugins/modules/k8s_service.py future-import-boilerplate!skip +plugins/modules/k8s_cluster_info.py future-import-boilerplate!skip +plugins/modules/k8s_info.py future-import-boilerplate!skip +plugins/modules/k8s_cp.py future-import-boilerplate!skip +plugins/modules/__init__.py future-import-boilerplate!skip +plugins/modules/k8s_json_patch.py future-import-boilerplate!skip +plugins/action/k8s_info.py future-import-boilerplate!skip +plugins/filter/k8s.py future-import-boilerplate!skip +tests/unit/conftest.py future-import-boilerplate!skip +tests/unit/utils/ansible_module_mock.py future-import-boilerplate!skip +tests/unit/module_utils/test_helm.py future-import-boilerplate!skip +tests/unit/module_utils/test_marshal.py future-import-boilerplate!skip +tests/unit/module_utils/test_discoverer.py future-import-boilerplate!skip +tests/unit/module_utils/test_hashes.py future-import-boilerplate!skip +tests/unit/module_utils/test_resource.py future-import-boilerplate!skip +tests/unit/module_utils/test_service.py future-import-boilerplate!skip +tests/unit/module_utils/test_waiter.py future-import-boilerplate!skip +tests/unit/module_utils/test_common.py future-import-boilerplate!skip +tests/unit/module_utils/test_selector.py future-import-boilerplate!skip +tests/unit/module_utils/test_apply.py future-import-boilerplate!skip +tests/unit/module_utils/test_runner.py future-import-boilerplate!skip +tests/unit/module_utils/test_client.py future-import-boilerplate!skip +tests/unit/module_utils/test_core.py future-import-boilerplate!skip +tests/unit/modules/test_helm_template_module.py future-import-boilerplate!skip +tests/unit/modules/test_helm_template.py future-import-boilerplate!skip +tests/unit/modules/test_module_helm.py future-import-boilerplate!skip +tests/unit/action/test_remove_omit.py future-import-boilerplate!skip +plugins/doc_fragments/k8s_name_options.py metaclass-boilerplate!skip +plugins/doc_fragments/k8s_auth_options.py metaclass-boilerplate!skip +plugins/doc_fragments/helm_common_options.py metaclass-boilerplate!skip +plugins/doc_fragments/k8s_state_options.py metaclass-boilerplate!skip +plugins/doc_fragments/k8s_wait_options.py metaclass-boilerplate!skip +plugins/doc_fragments/k8s_scale_options.py metaclass-boilerplate!skip +plugins/doc_fragments/k8s_delete_options.py metaclass-boilerplate!skip +plugins/doc_fragments/__init__.py metaclass-boilerplate!skip +plugins/doc_fragments/k8s_resource_options.py metaclass-boilerplate!skip +plugins/module_utils/helm.py metaclass-boilerplate!skip +plugins/module_utils/apply.py metaclass-boilerplate!skip +plugins/module_utils/hashes.py metaclass-boilerplate!skip +plugins/module_utils/version.py metaclass-boilerplate!skip +plugins/module_utils/_version.py metaclass-boilerplate!skip plugins/module_utils/copy.py metaclass-boilerplate!skip -plugins/module_utils/copy.py compile-2.6!skip -plugins/module_utils/copy.py compile-2.7!skip -plugins/module_utils/copy.py import-2.6!skip -plugins/module_utils/copy.py import-2.7!skip +plugins/module_utils/args_common.py metaclass-boilerplate!skip +plugins/module_utils/__init__.py metaclass-boilerplate!skip +plugins/module_utils/selector.py metaclass-boilerplate!skip +plugins/module_utils/k8sdynamicclient.py metaclass-boilerplate!skip +plugins/module_utils/common.py metaclass-boilerplate!skip +plugins/module_utils/ansiblemodule.py metaclass-boilerplate!skip +plugins/module_utils/exceptions.py metaclass-boilerplate!skip +plugins/module_utils/client/resource.py metaclass-boilerplate!skip +plugins/module_utils/client/discovery.py metaclass-boilerplate!skip +plugins/module_utils/k8s/resource.py metaclass-boilerplate!skip +plugins/module_utils/k8s/core.py metaclass-boilerplate!skip +plugins/module_utils/k8s/waiter.py metaclass-boilerplate!skip +plugins/module_utils/k8s/client.py metaclass-boilerplate!skip +plugins/module_utils/k8s/runner.py metaclass-boilerplate!skip +plugins/module_utils/k8s/service.py metaclass-boilerplate!skip +plugins/module_utils/k8s/exceptions.py metaclass-boilerplate!skip +plugins/connection/kubectl.py metaclass-boilerplate!skip +plugins/inventory/k8s.py metaclass-boilerplate!skip +plugins/lookup/k8s.py metaclass-boilerplate!skip +plugins/lookup/kustomize.py metaclass-boilerplate!skip +plugins/modules/k8s_scale.py metaclass-boilerplate!skip +plugins/modules/helm_template.py metaclass-boilerplate!skip +plugins/modules/k8s_exec.py metaclass-boilerplate!skip +plugins/modules/helm.py metaclass-boilerplate!skip +plugins/modules/helm_plugin_info.py metaclass-boilerplate!skip +plugins/modules/helm_info.py metaclass-boilerplate!skip +plugins/modules/helm_repository.py metaclass-boilerplate!skip +plugins/modules/k8s_rollback.py metaclass-boilerplate!skip +plugins/modules/k8s_log.py metaclass-boilerplate!skip +plugins/modules/k8s_drain.py metaclass-boilerplate!skip +plugins/modules/helm_plugin.py metaclass-boilerplate!skip +plugins/modules/k8s_taint.py metaclass-boilerplate!skip +plugins/modules/k8s.py metaclass-boilerplate!skip +plugins/modules/k8s_service.py metaclass-boilerplate!skip +plugins/modules/k8s_cluster_info.py metaclass-boilerplate!skip +plugins/modules/k8s_info.py metaclass-boilerplate!skip +plugins/modules/k8s_cp.py metaclass-boilerplate!skip +plugins/modules/__init__.py metaclass-boilerplate!skip +plugins/modules/k8s_json_patch.py metaclass-boilerplate!skip +plugins/action/k8s_info.py metaclass-boilerplate!skip +plugins/filter/k8s.py metaclass-boilerplate!skip +tests/unit/conftest.py metaclass-boilerplate!skip +tests/unit/utils/ansible_module_mock.py metaclass-boilerplate!skip +tests/unit/module_utils/test_helm.py metaclass-boilerplate!skip +tests/unit/module_utils/test_marshal.py metaclass-boilerplate!skip +tests/unit/module_utils/test_discoverer.py metaclass-boilerplate!skip +tests/unit/module_utils/test_hashes.py metaclass-boilerplate!skip +tests/unit/module_utils/test_resource.py metaclass-boilerplate!skip +tests/unit/module_utils/test_service.py metaclass-boilerplate!skip +tests/unit/module_utils/test_waiter.py metaclass-boilerplate!skip +tests/unit/module_utils/test_common.py metaclass-boilerplate!skip +tests/unit/module_utils/test_selector.py metaclass-boilerplate!skip +tests/unit/module_utils/test_apply.py metaclass-boilerplate!skip +tests/unit/module_utils/test_runner.py metaclass-boilerplate!skip +tests/unit/module_utils/test_client.py metaclass-boilerplate!skip +tests/unit/module_utils/test_core.py metaclass-boilerplate!skip +tests/unit/modules/test_helm_template_module.py metaclass-boilerplate!skip +tests/unit/modules/test_helm_template.py metaclass-boilerplate!skip +tests/unit/modules/test_module_helm.py metaclass-boilerplate!skip +tests/unit/action/test_remove_omit.py metaclass-boilerplate!skip diff --git a/tests/sanity/refresh_ignore_files b/tests/sanity/refresh_ignore_files index 6640baa5..85afa676 100644 --- a/tests/sanity/refresh_ignore_files +++ b/tests/sanity/refresh_ignore_files @@ -1,47 +1,124 @@ #!/usr/bin/env python3 + +import itertools + from pathlib import Path -target_dir = Path('.') -ignore_dir = target_dir / "tests" / "sanity" -module_dir = target_dir / "plugins" / "modules" -module_utils_dir = target_dir / "plugins" / "module_utils" -ignore_dir.mkdir(parents=True, exist_ok=True) +# Mapping of Ansible versions to supported Python versions +ANSIBLE_VERSIONS = { + "2.9": ["3.6", "3.7", "3.8"], + "2.10": ["3.6", "3.7", "3.8", "3.9"], + "2.11": ["3.6", "3.7", "3.8", "3.9"], + "2.12": ["3.6", "3.7", "3.8", "3.9", "3.10"], + "2.13": ["3.6", "3.7", "3.8", "3.9", "3.10"], + "2.14": ["3.6", "3.7", "3.8", "3.9", "3.10"], +} -skip_list_2_6 = [ - "compile-2.6!skip", # Py3.8+ - "import-2.6!skip", # Py3.8+ +IMPORT_SKIPS = [ + "plugins/module_utils/client/discovery.py", + "plugins/module_utils/client/resource.py", + "plugins/module_utils/k8sdynamicclient.py", ] -skip_list_3 = [ - "compile-2.7!skip", # Py3.8+ - "compile-3.5!skip", # Py3.8+ - "import-2.7!skip", # Py3.8+ - "import-3.5!skip", # Py3.8+ - "future-import-boilerplate!skip", # Py2 only - "metaclass-boilerplate!skip", # Py2 only +# Adds validate-modules:parameter-type-not-in-doc +PARAM_TYPE_SKIPS = [ + "plugins/modules/k8s.py", + "plugins/modules/k8s_scale.py", + "plugins/modules/k8s_service.py", ] -for version in ["2.9", "2.10", "2.11", "2.12", "2.13"]: - ignore_file = ignore_dir / f"ignore-{version}.txt" - ignore_content = ignore_file.read_text().split("\n") - ignore_content.append(f"tests/sanity/refresh_ignore_files shebang!skip") +# Adds validate-modules:return-syntax-error +RETURN_SYNTAX_SKIPS = [ + "plugins/modules/k8s.py", + "plugins/modules/k8s_scale.py", + "plugins/modules/k8s_service.py", + "plugins/modules/k8s_taint.py", +] - if version == "2.13": - skip_list = skip_list_3 +YAML_LINT_SKIPS = [ + "tests/unit/module_utils/fixtures/definitions.yml", + "tests/unit/module_utils/fixtures/deployments.yml", + "tests/unit/module_utils/fixtures/pods.yml", + "tests/integration/targets/helm/files/appversionless-chart-v2/templates/configmap.yaml", + "tests/integration/targets/helm/files/appversionless-chart/templates/configmap.yaml", + "tests/integration/targets/helm/files/test-chart-v2/templates/configmap.yaml", + "tests/integration/targets/helm/files/test-chart/templates/configmap.yaml", + "tests/integration/targets/k8s_scale/files/deployment.yaml", +] + +# Add shebang!skip +SHEBANG_SKIPS = [ + "tests/sanity/refresh_ignore_files", +] + + +def import_skips(*versions): + for f in IMPORT_SKIPS: + for v in versions: + yield f"{f} import-{v}!skip" + +def param_type_skips(): + for f in PARAM_TYPE_SKIPS: + yield f"{f} validate-modules:parameter-type-not-in-doc" + + +def return_syntax_skips(ansible_version): + if ansible_version != "2.9": + for f in RETURN_SYNTAX_SKIPS: + yield f"{f} validate-modules:return-syntax-error" else: - skip_list = skip_list_2_6 + skip_list_3 + yield - for f in module_dir.glob("**/*.py"): - if f.is_symlink(): - continue - for test in skip_list: - ignore_content.append(f"{f} {test}") - for f in module_utils_dir.glob("**/*.py"): - if f.is_symlink(): - continue - for test in skip_list: - ignore_content.append(f"{f} {test}") - ignore_file = ignore_dir / f"ignore-{version}.txt" - ignore_file.write_text("\n".join(sorted(set(ignore_content))).lstrip("\n")) + +def yaml_lint_skips(): + for f in YAML_LINT_SKIPS: + yield f"{f} yamllint!skip" + + +def shebang_skips(): + for f in SHEBANG_SKIPS: + yield f"{f} shebang!skip" + + +def import_boilerplate(path, ansible_version): + if ansible_version in ("2.9", "2.10", "2.11"): + for f in (p for p in path.glob("**/*.py") if not p.is_symlink()): + yield f"{f} future-import-boilerplate!skip" + else: + yield + + +def metaclass_boilerplate(path, ansible_version): + if ansible_version in ("2.9", "2.10", "2.11"): + for f in (p for p in path.glob("**/*.py") if not p.is_symlink()): + yield f"{f} metaclass-boilerplate!skip" + else: + yield + + +def main(): + target_dir = Path('.') + sanity_dir = target_dir / "tests" / "sanity" + plugins = target_dir / "plugins" + units = target_dir / "tests" / "unit" + + for ansible, python in ANSIBLE_VERSIONS.items(): + with open(sanity_dir / f"ignore-{ansible}.txt", "w") as fp: + ignores = itertools.chain( + import_skips(*python), + param_type_skips(), + yaml_lint_skips(), + shebang_skips(), + return_syntax_skips(ansible), + import_boilerplate(plugins, ansible), + import_boilerplate(units, ansible), + metaclass_boilerplate(plugins, ansible), + metaclass_boilerplate(units, ansible)) + for f in filter(None, ignores): + fp.write(f + "\n") + + +if __name__ == "__main__": + main() From beb53652db8c67f79f322c72f2195180cd20c54a Mon Sep 17 00:00:00 2001 From: Mike Graves Date: Wed, 15 Jun 2022 09:26:24 -0400 Subject: [PATCH 21/24] Ensure CoreExceptions are handled gracefully (#476) Ensure CoreExceptions are handled gracefully SUMMARY CoreExceptions, when raised, should have a reasonably helpful and actionable message associated with them. This adds a final check in module execution to gracefully fail from these exceptions. A new fail_from_exception method is added both to simplify exiting the module, and to ensure that any chained exceptions are available when using -vvv. ISSUE TYPE COMPONENT NAME ADDITIONAL INFORMATION Reviewed-by: Alina Buzachis Reviewed-by: Joseph Torcasso --- plugins/module_utils/k8s/client.py | 13 ++++++++++--- plugins/module_utils/k8s/core.py | 9 +++++++++ plugins/module_utils/k8s/runner.py | 9 ++++++++- plugins/modules/k8s.py | 8 +++++++- plugins/modules/k8s_cluster_info.py | 8 +++++++- plugins/modules/k8s_cp.py | 8 +++++++- plugins/modules/k8s_drain.py | 13 ++++++++++--- plugins/modules/k8s_exec.py | 10 ++++++++-- plugins/modules/k8s_info.py | 12 +++++++++--- plugins/modules/k8s_json_patch.py | 10 ++++++++-- plugins/modules/k8s_log.py | 2 +- plugins/modules/k8s_rollback.py | 9 ++++++--- plugins/modules/k8s_scale.py | 7 +++++-- plugins/modules/k8s_service.py | 12 +++++++++--- 14 files changed, 104 insertions(+), 26 deletions(-) diff --git a/plugins/module_utils/k8s/client.py b/plugins/module_utils/k8s/client.py index 7087e236..c9986d3c 100644 --- a/plugins/module_utils/k8s/client.py +++ b/plugins/module_utils/k8s/client.py @@ -15,6 +15,9 @@ from ansible_collections.kubernetes.core.plugins.module_utils.args_common import from ansible_collections.kubernetes.core.plugins.module_utils.k8s.core import ( requires as _requires, ) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import ( + CoreException, +) try: from ansible_collections.kubernetes.core.plugins.module_utils import ( @@ -335,9 +338,13 @@ def get_api_client(module=None, **kwargs: Optional[Any]) -> K8SClient: if auth_spec.get("no_proxy"): requires("kubernetes", "19.15.0", "to use the no_proxy feature") - configuration = _create_configuration(auth_spec) - headers = _create_headers(module, **kwargs) - client = create_api_client(configuration, **headers) + try: + configuration = _create_configuration(auth_spec) + headers = _create_headers(module, **kwargs) + client = create_api_client(configuration, **headers) + except kubernetes.config.ConfigException as e: + msg = "Could not create API client: {0}".format(e) + raise CoreException(msg) from e k8s_client = K8SClient( configuration=configuration, diff --git a/plugins/module_utils/k8s/core.py b/plugins/module_utils/k8s/core.py index b6bdd505..95815dee 100644 --- a/plugins/module_utils/k8s/core.py +++ b/plugins/module_utils/k8s/core.py @@ -1,3 +1,5 @@ +import traceback + from typing import Optional from ansible_collections.kubernetes.core.plugins.module_utils.version import ( @@ -72,6 +74,13 @@ class AnsibleK8SModule: def fail_json(self, *args, **kwargs): return self._module.fail_json(*args, **kwargs) + def fail_from_exception(self, exception): + msg = to_text(exception) + tb = "".join( + traceback.format_exception(None, exception, exception.__traceback__) + ) + return self.fail_json(msg=msg, exception=tb) + def has_at_least( self, dependency: str, minimum: Optional[str] = None, warn: bool = False ) -> bool: diff --git a/plugins/module_utils/k8s/runner.py b/plugins/module_utils/k8s/runner.py index 84b76c77..438b3211 100644 --- a/plugins/module_utils/k8s/runner.py +++ b/plugins/module_utils/k8s/runner.py @@ -8,6 +8,9 @@ from ansible.module_utils._text import to_native from ansible_collections.kubernetes.core.plugins.module_utils.k8s.client import ( get_api_client, ) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import ( + CoreException, +) from ansible_collections.kubernetes.core.plugins.module_utils.k8s.resource import ( create_definitions, ) @@ -48,7 +51,11 @@ def run_module(module) -> None: changed = False client = get_api_client(module) svc = K8sService(client, module) - definitions = create_definitions(module.params) + try: + definitions = create_definitions(module.params) + except Exception as e: + msg = "Failed to load resource definition: {0}".format(e) + raise CoreException(msg) from e for definition in definitions: result = {"changed": False, "result": {}} diff --git a/plugins/modules/k8s.py b/plugins/modules/k8s.py index 4b3c7462..31e5099e 100644 --- a/plugins/modules/k8s.py +++ b/plugins/modules/k8s.py @@ -394,6 +394,9 @@ from ansible_collections.kubernetes.core.plugins.module_utils.args_common import from ansible_collections.kubernetes.core.plugins.module_utils.k8s.core import ( AnsibleK8SModule, ) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import ( + CoreException, +) from ansible_collections.kubernetes.core.plugins.module_utils.k8s.runner import ( run_module, ) @@ -458,7 +461,10 @@ def main(): mutually_exclusive=mutually_exclusive, supports_check_mode=True, ) - run_module(module) + try: + run_module(module) + except CoreException as e: + module.fail_from_exception(e) if __name__ == "__main__": diff --git a/plugins/modules/k8s_cluster_info.py b/plugins/modules/k8s_cluster_info.py index 1fb53713..9cd2ac17 100644 --- a/plugins/modules/k8s_cluster_info.py +++ b/plugins/modules/k8s_cluster_info.py @@ -157,6 +157,9 @@ from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule impo from ansible_collections.kubernetes.core.plugins.module_utils.k8s.core import ( AnsibleK8SModule, ) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import ( + CoreException, +) from ansible_collections.kubernetes.core.plugins.module_utils.args_common import ( AUTH_ARG_SPEC, ) @@ -219,7 +222,10 @@ def main(): get_api_client, ) - execute_module(module, client=get_api_client(module=module)) + try: + execute_module(module, client=get_api_client(module=module)) + except CoreException as e: + module.fail_from_exception(e) if __name__ == "__main__": diff --git a/plugins/modules/k8s_cp.py b/plugins/modules/k8s_cp.py index 810cf953..cd277018 100644 --- a/plugins/modules/k8s_cp.py +++ b/plugins/modules/k8s_cp.py @@ -148,6 +148,9 @@ from ansible_collections.kubernetes.core.plugins.module_utils.k8s.client import from ansible_collections.kubernetes.core.plugins.module_utils.k8s.core import ( AnsibleK8SModule, ) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import ( + CoreException, +) from ansible_collections.kubernetes.core.plugins.module_utils.k8s.service import ( K8sService, ) @@ -212,7 +215,10 @@ def main(): supports_check_mode=True, ) - execute_module(module) + try: + execute_module(module) + except CoreException as e: + module.fail_from_exception(e) if __name__ == "__main__": diff --git a/plugins/modules/k8s_drain.py b/plugins/modules/k8s_drain.py index 7ef12bd5..31596d8c 100644 --- a/plugins/modules/k8s_drain.py +++ b/plugins/modules/k8s_drain.py @@ -142,6 +142,10 @@ from ansible_collections.kubernetes.core.plugins.module_utils.k8s.client import from ansible_collections.kubernetes.core.plugins.module_utils.k8s.core import ( AnsibleK8SModule, ) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import ( + CoreException, +) + from ansible.module_utils._text import to_native try: @@ -497,9 +501,12 @@ def main(): error=to_native(k8s_import_exception), ) - client = get_api_client(module=module) - k8s_drain = K8sDrainAnsible(module, client.client) - k8s_drain.execute_module() + try: + client = get_api_client(module=module) + k8s_drain = K8sDrainAnsible(module, client.client) + k8s_drain.execute_module() + except CoreException as e: + module.fail_from_exception(e) if __name__ == "__main__": diff --git a/plugins/modules/k8s_exec.py b/plugins/modules/k8s_exec.py index a1901321..c54c23c0 100644 --- a/plugins/modules/k8s_exec.py +++ b/plugins/modules/k8s_exec.py @@ -144,6 +144,9 @@ from ansible_collections.kubernetes.core.plugins.module_utils.k8s.core import ( from ansible_collections.kubernetes.core.plugins.module_utils.k8s.client import ( get_api_client, ) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import ( + CoreException, +) try: from kubernetes.client.apis import core_v1_api @@ -240,8 +243,11 @@ def main(): supports_check_mode=True, ) - client = get_api_client(module) - execute_module(module, client.client) + try: + client = get_api_client(module) + execute_module(module, client.client) + except CoreException as e: + module.fail_from_exception(e) if __name__ == "__main__": diff --git a/plugins/modules/k8s_info.py b/plugins/modules/k8s_info.py index 7d831d1d..4b29be11 100644 --- a/plugins/modules/k8s_info.py +++ b/plugins/modules/k8s_info.py @@ -161,6 +161,9 @@ from ansible_collections.kubernetes.core.plugins.module_utils.k8s.core import ( from ansible_collections.kubernetes.core.plugins.module_utils.k8s.client import ( get_api_client, ) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import ( + CoreException, +) from ansible_collections.kubernetes.core.plugins.module_utils.k8s.service import ( K8sService, ) @@ -202,9 +205,12 @@ def main(): module = AnsibleK8SModule( module_class=AnsibleModule, argument_spec=argspec(), supports_check_mode=True ) - client = get_api_client(module) - svc = K8sService(client, module) - execute_module(module, svc) + try: + client = get_api_client(module) + svc = K8sService(client, module) + execute_module(module, svc) + except CoreException as e: + module.fail_from_exception(e) if __name__ == "__main__": diff --git a/plugins/modules/k8s_json_patch.py b/plugins/modules/k8s_json_patch.py index 61595454..5ea8dbc9 100644 --- a/plugins/modules/k8s_json_patch.py +++ b/plugins/modules/k8s_json_patch.py @@ -142,6 +142,9 @@ from ansible_collections.kubernetes.core.plugins.module_utils.k8s.client import from ansible_collections.kubernetes.core.plugins.module_utils.k8s.core import ( AnsibleK8SModule, ) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import ( + CoreException, +) from ansible_collections.kubernetes.core.plugins.module_utils.k8s.service import ( diff_objects, ) @@ -283,8 +286,11 @@ def main(): module = AnsibleK8SModule( module_class=AnsibleModule, argument_spec=args, supports_check_mode=True ) - client = get_api_client(module) - execute_module(module, client) + try: + client = get_api_client(module) + execute_module(module, client) + except CoreException as e: + module.fail_from_exception(e) if __name__ == "__main__": diff --git a/plugins/modules/k8s_log.py b/plugins/modules/k8s_log.py index e55bd6e5..94e5d5f9 100644 --- a/plugins/modules/k8s_log.py +++ b/plugins/modules/k8s_log.py @@ -272,7 +272,7 @@ def main(): result = execute_module(svc, module.params) module.exit_json(**result) except CoreException as e: - module.fail_json(msg=e) + module.fail_from_exception(e) if __name__ == "__main__": diff --git a/plugins/modules/k8s_rollback.py b/plugins/modules/k8s_rollback.py index 335b6ee5..8dfab459 100644 --- a/plugins/modules/k8s_rollback.py +++ b/plugins/modules/k8s_rollback.py @@ -262,9 +262,12 @@ def main(): module_class=AnsibleModule, argument_spec=argspec(), supports_check_mode=True ) - client = get_api_client(module=module) - svc = K8sService(client, module) - execute_module(svc) + try: + client = get_api_client(module=module) + svc = K8sService(client, module) + execute_module(svc) + except CoreException as e: + module.fail_from_exception(e) if __name__ == "__main__": diff --git a/plugins/modules/k8s_scale.py b/plugins/modules/k8s_scale.py index 561e11ca..01edfa6d 100644 --- a/plugins/modules/k8s_scale.py +++ b/plugins/modules/k8s_scale.py @@ -411,8 +411,11 @@ def main(): supports_check_mode=True, ) - client = get_api_client(module=module) - execute_module(client, module) + try: + client = get_api_client(module=module) + execute_module(client, module) + except CoreException as e: + module.fail_from_exception(e) if __name__ == "__main__": diff --git a/plugins/modules/k8s_service.py b/plugins/modules/k8s_service.py index eba278b1..4eb280bf 100644 --- a/plugins/modules/k8s_service.py +++ b/plugins/modules/k8s_service.py @@ -160,6 +160,9 @@ from ansible_collections.kubernetes.core.plugins.module_utils.k8s.core import ( from ansible_collections.kubernetes.core.plugins.module_utils.k8s.client import ( get_api_client, ) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import ( + CoreException, +) from ansible_collections.kubernetes.core.plugins.module_utils.k8s.service import ( K8sService, ) @@ -274,9 +277,12 @@ def main(): supports_check_mode=True, ) - client = get_api_client(module=module) - svc = K8sService(client, module) - execute_module(svc) + try: + client = get_api_client(module=module) + svc = K8sService(client, module) + execute_module(svc) + except CoreException as e: + module.fail_from_exception(e) if __name__ == "__main__": From adf3503d4e8b2f4984d6914efa23eb37f1eb0b64 Mon Sep 17 00:00:00 2001 From: Mike Graves Date: Wed, 15 Jun 2022 10:41:10 -0400 Subject: [PATCH 22/24] Migrate k8s_taint to refactored code (#477) Migrate k8s_taint to refactored code Depends-on: #476 SUMMARY This changes the k8s_taint module to use the newly refactored code. ISSUE TYPE COMPONENT NAME ADDITIONAL INFORMATION Reviewed-by: Alina Buzachis --- plugins/modules/k8s_taint.py | 44 ++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/plugins/modules/k8s_taint.py b/plugins/modules/k8s_taint.py index 4146a0e2..bfa80db5 100644 --- a/plugins/modules/k8s_taint.py +++ b/plugins/modules/k8s_taint.py @@ -126,22 +126,29 @@ result: import copy -from ansible.module_utils.basic import AnsibleModule from ansible.module_utils._text import to_native -from ansible_collections.kubernetes.core.plugins.module_utils.common import ( - K8sAnsibleMixin, - get_api_client, +from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import ( + AnsibleModule, ) from ansible_collections.kubernetes.core.plugins.module_utils.args_common import ( AUTH_ARG_SPEC, ) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.client import ( + get_api_client, +) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.core import ( + AnsibleK8SModule, +) +from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import ( + CoreException, +) try: from kubernetes.client.api import core_v1_api from kubernetes.client.exceptions import ApiException except ImportError: - # ImportError are managed by the common module already. + # ImportErrors are handled during module setup pass @@ -191,21 +198,9 @@ def argspec(): class K8sTaintAnsible: - def __init__(self, module): + def __init__(self, module, client): self.module = module - self.k8s_ansible_mixin = K8sAnsibleMixin(module=self.module) - self.k8s_ansible_mixin.client = get_api_client(module=self.module) - self.k8s_ansible_mixin.module = self.module - self.k8s_ansible_mixin.argspec = self.module.argument_spec - self.k8s_ansible_mixin.check_mode = self.module.check_mode - self.k8s_ansible_mixin.params = self.module.params - self.k8s_ansible_mixin.fail_json = self.module.fail_json - self.k8s_ansible_mixin.fail = self.module.fail_json - self.k8s_ansible_mixin.exit_json = self.module.exit_json - self.k8s_ansible_mixin.warn = self.module.warn - self.k8s_ansible_mixin.warnings = [] - self.api_instance = core_v1_api.CoreV1Api(self.k8s_ansible_mixin.client.client) - self.k8s_ansible_mixin.check_library_version() + self.api_instance = core_v1_api.CoreV1Api(client.client) self.changed = False def get_node(self, name): @@ -301,12 +296,17 @@ class K8sTaintAnsible: def main(): - module = AnsibleModule( + module = AnsibleK8SModule( + module_class=AnsibleModule, argument_spec=argspec(), supports_check_mode=True, ) - k8s_taint = K8sTaintAnsible(module) - k8s_taint.execute_module() + try: + client = get_api_client(module) + k8s_taint = K8sTaintAnsible(module, client.client) + k8s_taint.execute_module() + except CoreException as e: + module.fail_from_exception(e) if __name__ == "__main__": From 4f1623fe9cf9d919bc47d29b274766a7583582ff Mon Sep 17 00:00:00 2001 From: Mike Graves Date: Fri, 17 Jun 2022 10:39:52 -0400 Subject: [PATCH 23/24] Add deprecation notice --- plugins/module_utils/common.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/plugins/module_utils/common.py b/plugins/module_utils/common.py index 08838a9f..6ed7c7f3 100644 --- a/plugins/module_utils/common.py +++ b/plugins/module_utils/common.py @@ -130,6 +130,7 @@ except ImportError as e: def configuration_digest(configuration, **kwargs): + """This function has been deprecated and will be removed in version 4.0.0.""" m = hashlib.sha256() for k in AUTH_ARG_MAP: if not hasattr(configuration, k): @@ -150,6 +151,8 @@ def configuration_digest(configuration, **kwargs): class unique_string(str): + """This function has been deprecated and will be removed in version 4.0.0.""" + _low = None def __hash__(self): @@ -169,6 +172,7 @@ class unique_string(str): def get_api_client(module=None, **kwargs): + """This function has been deprecated and will be removed in version 4.0.0. Please use module_utils.k8s.client.get_api_client() instead.""" auth = {} def _raise_or_fail(exc, msg): @@ -340,6 +344,11 @@ get_api_client._pool = {} class K8sAnsibleMixin(object): def __init__(self, module, pyyaml_required=True, *args, **kwargs): + module.deprecate( + msg="The K8sAnsibleMixin class has been deprecated and refactored into the module_utils/k8s/ directory.", + version="4.0.0", + collection_name="kubernetes.core", + ) if not HAS_K8S_MODULE_HELPER: module.fail_json( msg=missing_required_lib("kubernetes"), From 14fe6f1c55aefc9932017c718abc1bd4e8b9c3de Mon Sep 17 00:00:00 2001 From: Mike Graves Date: Fri, 17 Jun 2022 10:46:57 -0400 Subject: [PATCH 24/24] Add changelog fragment --- changelogs/fragments/481-refactor-common.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 changelogs/fragments/481-refactor-common.yml diff --git a/changelogs/fragments/481-refactor-common.yml b/changelogs/fragments/481-refactor-common.yml new file mode 100644 index 00000000..8727330a --- /dev/null +++ b/changelogs/fragments/481-refactor-common.yml @@ -0,0 +1,3 @@ +--- +major_changes: + - refactor K8sAnsibleMixin into module_utils/k8s/ (https://github.com/ansible-collections/kubernetes.core/pull/481).