mirror of
https://github.com/ansible-collections/kubernetes.core.git
synced 2026-05-06 21:12:37 +00:00
Merge pull request #481 from ansible-collections/2.x-refactor
Refactor common.py
This commit is contained in:
@@ -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"),
|
||||
|
||||
@@ -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"]
|
||||
]
|
||||
|
||||
355
plugins/module_utils/k8s/client.py
Normal file
355
plugins/module_utils/k8s/client.py
Normal file
@@ -0,0 +1,355 @@
|
||||
# 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 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,
|
||||
)
|
||||
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 (
|
||||
k8sdynamicclient,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.client.discovery import (
|
||||
LazyDiscoverer,
|
||||
)
|
||||
except ImportError:
|
||||
# Handled in module setup
|
||||
pass
|
||||
|
||||
try:
|
||||
import kubernetes
|
||||
from kubernetes.dynamic.exceptions import (
|
||||
ResourceNotFoundError,
|
||||
ResourceNotUniqueError,
|
||||
)
|
||||
from kubernetes.dynamic.resource import Resource
|
||||
except ImportError:
|
||||
# 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
|
||||
|
||||
urllib3.disable_warnings()
|
||||
except ImportError:
|
||||
# Handled in module setup
|
||||
pass
|
||||
|
||||
|
||||
_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
|
||||
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):
|
||||
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 _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):
|
||||
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())
|
||||
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, **kwargs):
|
||||
client = None
|
||||
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, **kwargs)
|
||||
_pool[digest] = client
|
||||
|
||||
return client
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
@cache
|
||||
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:
|
||||
"""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 _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.
|
||||
"""
|
||||
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:
|
||||
params["dry_run"] = True
|
||||
return params
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
if module:
|
||||
requires = module.requires
|
||||
else:
|
||||
requires = _requires
|
||||
if isinstance(auth_spec.get("kubeconfig"), dict):
|
||||
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")
|
||||
|
||||
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,
|
||||
client=client,
|
||||
dry_run=module.params.get("dry_run", False),
|
||||
)
|
||||
|
||||
return k8s_client
|
||||
171
plugins/module_utils/k8s/core.py
Normal file
171
plugins/module_utils/k8s/core.py
Normal file
@@ -0,0 +1,171 @@
|
||||
import traceback
|
||||
|
||||
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
|
||||
|
||||
|
||||
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,
|
||||
"check_pyyaml": 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)
|
||||
|
||||
if self.settings["check_pyyaml"]:
|
||||
self.requires("pyyaml")
|
||||
|
||||
@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 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:
|
||||
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
|
||||
|
||||
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))
|
||||
|
||||
|
||||
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 kubernetes_validate
|
||||
|
||||
versions["kubernetes-validate"] = kubernetes_validate.__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))
|
||||
12
plugins/module_utils/k8s/exceptions.py
Normal file
12
plugins/module_utils/k8s/exceptions.py
Normal file
@@ -0,0 +1,12 @@
|
||||
# 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):
|
||||
def __init__(self, message="", result=None):
|
||||
self.result = result or {}
|
||||
super().__init__(message)
|
||||
129
plugins/module_utils/k8s/resource.py
Normal file
129
plugins/module_utils/k8s/resource.py
Normal file
@@ -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, "rb") 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
|
||||
199
plugins/module_utils/k8s/runner.py
Normal file
199
plugins/module_utils/k8s/runner.py
Normal file
@@ -0,0 +1,199 @@
|
||||
# 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.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,
|
||||
)
|
||||
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 (
|
||||
ResourceTimeout,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.waiter import exists
|
||||
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 = []
|
||||
changed = False
|
||||
client = get_api_client(module)
|
||||
svc = K8sService(client, module)
|
||||
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": {}}
|
||||
warnings = []
|
||||
|
||||
if module.params.get("validate") is not None:
|
||||
warnings = validate(client, module, definition)
|
||||
|
||||
try:
|
||||
result = perform_action(svc, definition, module.params)
|
||||
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)
|
||||
|
||||
if module.params.get("continue_on_error"):
|
||||
result["error"] = error
|
||||
else:
|
||||
module.fail_json(**error)
|
||||
|
||||
if warnings:
|
||||
result.setdefault("warnings", []).extend(warnings)
|
||||
changed |= result["changed"]
|
||||
results.append(result)
|
||||
|
||||
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:
|
||||
origin_name = definition["metadata"].get("name")
|
||||
namespace = definition["metadata"].get("namespace")
|
||||
label_selectors = params.get("label_selectors")
|
||||
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
|
||||
definition["apiVersion"] = resource.group_version
|
||||
existing = svc.retrieve(resource, definition)
|
||||
|
||||
if state == "absent":
|
||||
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
|
||||
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=kind,
|
||||
name=origin_name,
|
||||
namespace=namespace,
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
if params.get("apply"):
|
||||
instance = 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
|
||||
instance = svc.create(resource, definition)
|
||||
result["method"] = "create"
|
||||
result["changed"] = True
|
||||
elif params.get("force", False):
|
||||
instance = svc.replace(resource, definition, existing)
|
||||
result["method"] = "replace"
|
||||
else:
|
||||
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
|
||||
502
plugins/module_utils/k8s/service.py
Normal file
502
plugins/module_utils/k8s/service.py
Normal file
@@ -0,0 +1,502 @@
|
||||
# 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.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 (
|
||||
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[Resource]:
|
||||
try:
|
||||
return self.client.resource(kind, api_version)
|
||||
except (ResourceNotFoundError, ResourceNotUniqueError):
|
||||
if fail:
|
||||
raise CoreException(
|
||||
"Failed to find exact match for %s.%s by [kind, name, singularName, shortNames]"
|
||||
% (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": {}}
|
||||
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 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) -> 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")
|
||||
existing: ResourceInstance = None
|
||||
|
||||
try:
|
||||
# ignore append_hash for resources other than ConfigMap and Secret
|
||||
if append_hash and definition["kind"] in ["ConfigMap", "Secret"]:
|
||||
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)
|
||||
if "name" in params or "label_selector" in params:
|
||||
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
|
||||
|
||||
return existing
|
||||
|
||||
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
|
||||
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 (
|
||||
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:
|
||||
namespace = definition["metadata"].get("namespace")
|
||||
name = definition["metadata"].get("name")
|
||||
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
|
||||
return k8s_obj
|
||||
|
||||
def apply(
|
||||
self,
|
||||
resource: Resource,
|
||||
definition: Dict,
|
||||
existing: Optional[ResourceInstance] = None,
|
||||
) -> 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:
|
||||
k8s_obj = dict_merge(existing.to_dict(), patch)
|
||||
else:
|
||||
k8s_obj = patch
|
||||
else:
|
||||
try:
|
||||
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()
|
||||
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")
|
||||
namespace = definition["metadata"].get("namespace")
|
||||
|
||||
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
|
||||
return k8s_obj
|
||||
|
||||
def update(
|
||||
self, resource: Resource, definition: Dict, existing: ResourceInstance
|
||||
) -> Dict:
|
||||
name = definition["metadata"].get("name")
|
||||
namespace = definition["metadata"].get("namespace")
|
||||
|
||||
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",
|
||||
]:
|
||||
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
|
||||
return k8s_obj
|
||||
|
||||
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")
|
||||
name = definition["metadata"].get("name")
|
||||
namespace = definition["metadata"].get("namespace")
|
||||
params = {}
|
||||
|
||||
if not exists(existing):
|
||||
return {}
|
||||
|
||||
# Delete the object
|
||||
if self.module.check_mode and not self.client.dry_run:
|
||||
return {}
|
||||
|
||||
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",
|
||||
"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).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
|
||||
|
||||
|
||||
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
|
||||
238
plugins/module_utils/k8s/waiter.py
Normal file
238
plugins/module_utils/k8s/waiter.py
Normal file
@@ -0,0 +1,238 @@
|
||||
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
|
||||
|
||||
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
|
||||
except ImportError:
|
||||
# These are defined only for the sake of Ansible's checked import requirement
|
||||
Resource = Any # type: ignore
|
||||
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
|
||||
# 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:
|
||||
# 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 updated_replicas == statefulset.spec.replicas
|
||||
and ready_replicas == 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,
|
||||
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, Dict, int]:
|
||||
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 = {}
|
||||
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
|
||||
|
||||
|
||||
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)
|
||||
@@ -391,6 +391,15 @@ 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.exceptions import (
|
||||
CoreException,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.runner import (
|
||||
run_module,
|
||||
)
|
||||
|
||||
|
||||
def validate_spec():
|
||||
@@ -437,28 +446,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 +454,17 @@ 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)
|
||||
try:
|
||||
run_module(module)
|
||||
except CoreException as e:
|
||||
module.fail_from_exception(e)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -141,36 +141,32 @@ 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.k8s.exceptions import (
|
||||
CoreException,
|
||||
)
|
||||
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 +200,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,18 +214,18 @@ 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,
|
||||
)
|
||||
|
||||
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__":
|
||||
|
||||
@@ -142,6 +142,19 @@ 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.exceptions import (
|
||||
CoreException,
|
||||
)
|
||||
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 +184,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 +194,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.client)
|
||||
else:
|
||||
k8s_copy = K8SCopyFromPod(module, k8s_ansible_mixin.client)
|
||||
k8s_copy = K8SCopyFromPod(module, client.client)
|
||||
|
||||
try:
|
||||
k8s_copy.run()
|
||||
@@ -206,15 +205,20 @@ 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"]],
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
execute_module(module)
|
||||
try:
|
||||
execute_module(module)
|
||||
except CoreException as e:
|
||||
module.fail_from_exception(e)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -136,6 +136,16 @@ 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_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import (
|
||||
CoreException,
|
||||
)
|
||||
|
||||
from ansible.module_utils._text import to_native
|
||||
|
||||
try:
|
||||
@@ -244,30 +254,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 +492,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,8 +501,12 @@ def main():
|
||||
error=to_native(k8s_import_exception),
|
||||
)
|
||||
|
||||
k8s_drain = K8sDrainAnsible(module)
|
||||
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__":
|
||||
|
||||
@@ -138,6 +138,15 @@ 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,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import (
|
||||
CoreException,
|
||||
)
|
||||
|
||||
try:
|
||||
from kubernetes.client.apis import core_v1_api
|
||||
@@ -157,10 +166,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 +236,18 @@ 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)
|
||||
try:
|
||||
client = get_api_client(module)
|
||||
execute_module(module, client.client)
|
||||
except CoreException as e:
|
||||
module.fail_from_exception(e)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -155,10 +155,22 @@ 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.exceptions import (
|
||||
CoreException,
|
||||
)
|
||||
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 +202,15 @@ 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)
|
||||
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__":
|
||||
|
||||
@@ -136,10 +136,22 @@ 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.exceptions import (
|
||||
CoreException,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.service import (
|
||||
diff_objects,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.waiter import (
|
||||
get_waiter,
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
from kubernetes.dynamic.exceptions import DynamicApiError
|
||||
@@ -185,7 +197,7 @@ def json_patch(existing, patch):
|
||||
return None, error
|
||||
|
||||
|
||||
def execute_module(k8s_module, module):
|
||||
def execute_module(module, client):
|
||||
kind = module.params.get("kind")
|
||||
api_version = module.params.get("api_version")
|
||||
name = module.params.get("name")
|
||||
@@ -200,19 +212,14 @@ 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)
|
||||
resource = client.resource(kind, api_version)
|
||||
|
||||
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 +234,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 +243,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 +263,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 = diff_objects(existing.to_dict(), obj)
|
||||
result["changed"] = not match
|
||||
if module._diff:
|
||||
result["diff"] = diffs
|
||||
@@ -274,13 +283,14 @@ 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()
|
||||
client = get_api_client(module)
|
||||
k8s_module.client = client
|
||||
execute_module(k8s_module, module)
|
||||
module = AnsibleK8SModule(
|
||||
module_class=AnsibleModule, argument_spec=args, supports_check_mode=True
|
||||
)
|
||||
try:
|
||||
client = get_api_client(module)
|
||||
execute_module(module, client)
|
||||
except CoreException as e:
|
||||
module.fail_from_exception(e)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -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,33 +193,32 @@ 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"):
|
||||
kwargs.setdefault("query_params", {}).update(
|
||||
{"previous": module.params["previous"]}
|
||||
)
|
||||
if params.get("previous"):
|
||||
kwargs.setdefault("query_params", {}).update({"previous": 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 +252,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 +261,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_from_exception(e)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -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,16 @@ 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)
|
||||
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__":
|
||||
|
||||
@@ -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 (
|
||||
diff_objects,
|
||||
)
|
||||
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,7 @@ 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,
|
||||
)
|
||||
|
||||
resource = client.resource(kind, api_version)
|
||||
multiple_scale = False
|
||||
try:
|
||||
existing = resource.get(
|
||||
@@ -211,11 +225,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 +291,28 @@ 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,
|
||||
module,
|
||||
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,8 +338,8 @@ def argspec():
|
||||
|
||||
|
||||
def scale(
|
||||
client,
|
||||
module,
|
||||
k8s_ansible_mixin,
|
||||
resource,
|
||||
existing_object,
|
||||
replicas,
|
||||
@@ -334,8 +352,8 @@ def scale(
|
||||
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 +362,37 @@ 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)
|
||||
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(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 = diff_objects(existing.to_dict(), result["result"])
|
||||
result["changed"] = not match
|
||||
if module._diff:
|
||||
result["diff"] = diffs
|
||||
@@ -381,19 +404,18 @@ 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)
|
||||
try:
|
||||
client = get_api_client(module=module)
|
||||
execute_module(client, module)
|
||||
except CoreException as e:
|
||||
module.fail_from_exception(e)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -154,6 +154,22 @@ 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.exceptions import (
|
||||
CoreException,
|
||||
)
|
||||
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 +211,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 +259,30 @@ 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)
|
||||
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__":
|
||||
|
||||
@@ -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__":
|
||||
|
||||
Reference in New Issue
Block a user