mirror of
https://github.com/ansible-collections/kubernetes.core.git
synced 2026-03-27 05:43:02 +00:00
K8sService class (#307)
K8sService class SUMMARY This refactors the perform_action() logic from common.py into a separate K8sService class. TODO: Unit tests. ISSUE TYPE New Module Pull Request COMPONENT NAME service.py Reviewed-by: Abhijeet Kasurde <None> Reviewed-by: Mike Graves <mgraves@redhat.com> Reviewed-by: Alina Buzachis <None> Reviewed-by: None <None>
This commit is contained in:
committed by
Mike Graves
parent
f168a3f67f
commit
e2f54d3431
@@ -250,4 +250,4 @@ def get_api_client(module=None, **kwargs: Optional[Any]) -> K8SClient:
|
||||
dry_run=module.params.get("dry_run", False),
|
||||
)
|
||||
|
||||
return k8s_client.client
|
||||
return k8s_client
|
||||
|
||||
10
plugins/module_utils/k8s/exceptions.py
Normal file
10
plugins/module_utils/k8s/exceptions.py
Normal file
@@ -0,0 +1,10 @@
|
||||
# Copyright: (c) 2021, Red Hat | Ansible
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
|
||||
class CoreException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ResourceTimeout(CoreException):
|
||||
pass
|
||||
620
plugins/module_utils/k8s/service.py
Normal file
620
plugins/module_utils/k8s/service.py
Normal file
@@ -0,0 +1,620 @@
|
||||
# Copyright: (c) 2021, Red Hat | Ansible
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.hashes import (
|
||||
generate_hash,
|
||||
)
|
||||
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.waiter import (
|
||||
Waiter,
|
||||
exists,
|
||||
resource_absent,
|
||||
get_waiter,
|
||||
)
|
||||
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import (
|
||||
CoreException,
|
||||
ResourceTimeout,
|
||||
)
|
||||
|
||||
from ansible.module_utils.common.dict_transformations import dict_merge
|
||||
|
||||
try:
|
||||
from kubernetes.dynamic.exceptions import (
|
||||
NotFoundError,
|
||||
ResourceNotFoundError,
|
||||
ResourceNotUniqueError,
|
||||
ConflictError,
|
||||
ForbiddenError,
|
||||
MethodNotAllowedError,
|
||||
BadRequestError,
|
||||
)
|
||||
except ImportError:
|
||||
# Handled in module setup
|
||||
pass
|
||||
|
||||
try:
|
||||
from kubernetes.dynamic.resource import Resource, ResourceInstance
|
||||
except ImportError:
|
||||
# These are defined only for the sake of Ansible's checked import requirement
|
||||
Resource = Any # type: ignore
|
||||
ResourceInstance = Any # type: ignore
|
||||
|
||||
try:
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.apply import (
|
||||
apply_object,
|
||||
)
|
||||
except ImportError:
|
||||
# Handled in module setup
|
||||
pass
|
||||
|
||||
try:
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.apply import (
|
||||
recursive_diff,
|
||||
)
|
||||
except ImportError:
|
||||
from ansible.module_utils.common.dict_transformations import recursive_diff
|
||||
|
||||
|
||||
try:
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.common import (
|
||||
_encode_stringdata,
|
||||
)
|
||||
except ImportError:
|
||||
# Handled in module setup
|
||||
pass
|
||||
|
||||
|
||||
class K8sService:
|
||||
"""A Service class for K8S modules.
|
||||
This class has the primary purpose is to perform work on the cluster (e.g., create, apply, replace, update, delete).
|
||||
"""
|
||||
|
||||
def __init__(self, client, module) -> None:
|
||||
self.client = client
|
||||
self.module = module
|
||||
|
||||
def find_resource(
|
||||
self, kind: str, api_version: str, fail: bool = False
|
||||
) -> Optional[ResourceInstance]:
|
||||
for attribute in ["kind", "name", "singular_name"]:
|
||||
try:
|
||||
return self.client.resources.get(
|
||||
**{"api_version": api_version, attribute: kind}
|
||||
)
|
||||
except (ResourceNotFoundError, ResourceNotUniqueError):
|
||||
pass
|
||||
try:
|
||||
return self.client.resources.get(
|
||||
api_version=api_version, short_names=[kind]
|
||||
)
|
||||
except (ResourceNotFoundError, ResourceNotUniqueError):
|
||||
if fail:
|
||||
raise CoreException(
|
||||
"Failed to find exact match for %s.%s by [kind, name, singularName, shortNames]"
|
||||
% (api_version, kind)
|
||||
)
|
||||
|
||||
def create_project_request(self, definition: Dict) -> Dict:
|
||||
definition["kind"] = "ProjectRequest"
|
||||
results = {"changed": False, "result": {}}
|
||||
resource = self.find_resource(
|
||||
"ProjectRequest", definition["apiVersion"], fail=True
|
||||
)
|
||||
if not self.module.check_mode:
|
||||
try:
|
||||
k8s_obj = self.client.create(resource, definition)
|
||||
results["result"] = k8s_obj.to_dict()
|
||||
except Exception as e:
|
||||
reason = e.body if hasattr(e, "body") else e
|
||||
msg = "Failed to create object: {0}".format(reason)
|
||||
raise CoreException(msg) from e
|
||||
|
||||
results["changed"] = True
|
||||
|
||||
return results
|
||||
|
||||
def diff_objects(self, existing: Dict, new: Dict) -> Tuple[bool, Dict]:
|
||||
result: Dict = dict()
|
||||
diff = recursive_diff(existing, new)
|
||||
if not diff:
|
||||
return True, result
|
||||
|
||||
result["before"] = diff[0]
|
||||
result["after"] = diff[1]
|
||||
|
||||
# If only metadata.generation and metadata.resourceVersion changed, ignore it
|
||||
ignored_keys = set(["generation", "resourceVersion"])
|
||||
|
||||
if list(result["after"].keys()) != ["metadata"] or list(
|
||||
result["before"].keys()
|
||||
) != ["metadata"]:
|
||||
return False, result
|
||||
|
||||
if not set(result["after"]["metadata"].keys()).issubset(ignored_keys):
|
||||
return False, result
|
||||
if not set(result["before"]["metadata"].keys()).issubset(ignored_keys):
|
||||
return False, result
|
||||
|
||||
if hasattr(self.module, "warn"):
|
||||
self.module.warn(
|
||||
"No meaningful diff was generated, but the API may not be idempotent (only metadata.generation or metadata.resourceVersion were changed)"
|
||||
)
|
||||
|
||||
return True, result
|
||||
|
||||
def patch_resource(
|
||||
self,
|
||||
resource: Resource,
|
||||
definition: Dict,
|
||||
name: str,
|
||||
namespace: str,
|
||||
merge_type: str = None,
|
||||
) -> Dict:
|
||||
if merge_type == "json":
|
||||
self.module.deprecate(
|
||||
msg="json as a merge_type value is deprecated. Please use the k8s_json_patch module instead.",
|
||||
version="3.0.0",
|
||||
collection_name="kubernetes.core",
|
||||
)
|
||||
try:
|
||||
params = dict(name=name, namespace=namespace)
|
||||
if self.module.check_mode:
|
||||
params["dry_run"] = "All"
|
||||
if merge_type:
|
||||
params["content_type"] = "application/{0}-patch+json".format(merge_type)
|
||||
return self.client.patch(resource, definition, **params).to_dict()
|
||||
except Exception as e:
|
||||
reason = e.body if hasattr(e, "body") else e
|
||||
msg = "Failed to patch object: {0}".format(reason)
|
||||
raise CoreException(msg) from e
|
||||
|
||||
def retrieve(self, resource: Resource, definition: Dict) -> Dict:
|
||||
state = self.module.params.get("state", None)
|
||||
append_hash = self.module.params.get("append_hash", False)
|
||||
name = definition["metadata"].get("name")
|
||||
namespace = definition["metadata"].get("namespace")
|
||||
label_selectors = self.module.params.get("label_selectors")
|
||||
results = {
|
||||
"changed": False,
|
||||
"result": {},
|
||||
}
|
||||
existing = None
|
||||
|
||||
try:
|
||||
# ignore append_hash for resources other than ConfigMap and Secret
|
||||
if append_hash and definition["kind"] in ["ConfigMap", "Secret"]:
|
||||
name = "%s-%s" % (name, generate_hash(definition))
|
||||
definition["metadata"]["name"] = name
|
||||
params = dict(name=name)
|
||||
if namespace:
|
||||
params["namespace"] = namespace
|
||||
if label_selectors:
|
||||
params["label_selector"] = ",".join(label_selectors)
|
||||
existing = self.client.get(resource, **params)
|
||||
except (NotFoundError, MethodNotAllowedError):
|
||||
pass
|
||||
except ForbiddenError as e:
|
||||
if (
|
||||
definition["kind"] in ["Project", "ProjectRequest"]
|
||||
and state != "absent"
|
||||
):
|
||||
return self.create_project_request(definition)
|
||||
reason = e.body if hasattr(e, "body") else e
|
||||
msg = "Failed to retrieve requested object: {0}".format(reason)
|
||||
raise CoreException(msg) from e
|
||||
except Exception as e:
|
||||
reason = e.body if hasattr(e, "body") else e
|
||||
msg = "Failed to retrieve requested object: {0}".format(reason)
|
||||
raise CoreException(msg) from e
|
||||
|
||||
if existing:
|
||||
results["result"] = existing.to_dict()
|
||||
|
||||
return results
|
||||
|
||||
def find(
|
||||
self,
|
||||
kind: str,
|
||||
api_version: str,
|
||||
name: str = None,
|
||||
namespace: Optional[str] = None,
|
||||
label_selectors: Optional[List[str]] = None,
|
||||
field_selectors: Optional[List[str]] = None,
|
||||
wait: Optional[bool] = False,
|
||||
wait_sleep: Optional[int] = 5,
|
||||
wait_timeout: Optional[int] = 120,
|
||||
state: Optional[str] = "present",
|
||||
condition: Optional[Dict] = None,
|
||||
) -> Dict:
|
||||
resource = self.find_resource(kind, api_version)
|
||||
api_found = bool(resource)
|
||||
if not api_found:
|
||||
return dict(
|
||||
resources=[],
|
||||
msg='Failed to find API for resource with apiVersion "{0}" and kind "{1}"'.format(
|
||||
api_version, kind
|
||||
),
|
||||
api_found=False,
|
||||
)
|
||||
|
||||
if not label_selectors:
|
||||
label_selectors = []
|
||||
if not field_selectors:
|
||||
field_selectors = []
|
||||
|
||||
result = {"resources": [], "api_found": True}
|
||||
|
||||
# With a timeout of 0 the waiter will do a single check and return, effectively not waiting.
|
||||
if not wait:
|
||||
wait_timeout = 0
|
||||
|
||||
if state == "present":
|
||||
predicate = exists
|
||||
else:
|
||||
predicate = resource_absent
|
||||
|
||||
waiter = Waiter(self.client, resource, predicate)
|
||||
|
||||
# This is an initial check to get the resource or resources that we then need to wait on individually.
|
||||
try:
|
||||
success, resources, duration = waiter.wait(
|
||||
timeout=wait_timeout,
|
||||
sleep=wait_sleep,
|
||||
name=name,
|
||||
namespace=namespace,
|
||||
label_selectors=label_selectors,
|
||||
field_selectors=field_selectors,
|
||||
)
|
||||
except BadRequestError:
|
||||
return result
|
||||
|
||||
# There is either no result or there is a List resource with no items
|
||||
if (
|
||||
not resources
|
||||
or resources["kind"].endswith("List")
|
||||
and not resources.get("items")
|
||||
):
|
||||
return result
|
||||
|
||||
instances = resources.get("items") or [resources]
|
||||
|
||||
if not wait:
|
||||
result["resources"] = instances
|
||||
return result
|
||||
|
||||
# Now wait for the specified state of any resource instances we have found.
|
||||
waiter = get_waiter(self.client, resource, state=state, condition=condition)
|
||||
for instance in instances:
|
||||
name = instance["metadata"].get("name")
|
||||
namespace = instance["metadata"].get("namespace")
|
||||
success, res, duration = waiter.wait(
|
||||
timeout=wait_timeout, sleep=wait_sleep, name=name, namespace=namespace,
|
||||
)
|
||||
if not success:
|
||||
raise CoreException(
|
||||
"Failed to gather information about %s(s) even"
|
||||
" after waiting for %s seconds" % (res.get("kind"), duration)
|
||||
)
|
||||
result["resources"].append(res)
|
||||
return result
|
||||
|
||||
def create(self, resource: Resource, definition: Dict) -> Dict:
|
||||
origin_name = definition["metadata"].get("name")
|
||||
namespace = definition["metadata"].get("namespace")
|
||||
name = definition["metadata"].get("name")
|
||||
wait = self.module.params.get("wait")
|
||||
wait_sleep = self.module.params.get("wait_sleep")
|
||||
wait_timeout = self.module.params.get("wait_timeout")
|
||||
wait_condition = None
|
||||
if self.module.params.get("wait_condition") and self.module.params[
|
||||
"wait_condition"
|
||||
].get("type"):
|
||||
wait_condition = self.module.params["wait_condition"]
|
||||
results = {"changed": False, "result": {}}
|
||||
|
||||
if self.module.check_mode and not self.client.dry_run:
|
||||
k8s_obj = _encode_stringdata(definition)
|
||||
else:
|
||||
params = {}
|
||||
if self.module.check_mode:
|
||||
params["dry_run"] = "All"
|
||||
try:
|
||||
k8s_obj = self.client.create(
|
||||
resource, definition, namespace=namespace, **params
|
||||
).to_dict()
|
||||
except ConflictError:
|
||||
# Some resources, like ProjectRequests, can't be created multiple times,
|
||||
# because the resources that they create don't match their kind
|
||||
# In this case we'll mark it as unchanged and warn the user
|
||||
self.module.warn(
|
||||
"{0} was not found, but creating it returned a 409 Conflict error. This can happen \
|
||||
if the resource you are creating does not directly create a resource of the same kind.".format(
|
||||
name
|
||||
)
|
||||
)
|
||||
return results
|
||||
except Exception as e:
|
||||
reason = e.body if hasattr(e, "body") else e
|
||||
msg = "Failed to create object: {0}".format(reason)
|
||||
raise CoreException(msg) from e
|
||||
|
||||
success = True
|
||||
results["result"] = k8s_obj
|
||||
|
||||
if wait and not self.module.check_mode:
|
||||
definition["metadata"].update({"name": k8s_obj["metadata"]["name"]})
|
||||
waiter = get_waiter(self.client, resource, condition=wait_condition)
|
||||
success, results["result"], results["duration"] = waiter.wait(
|
||||
timeout=wait_timeout, sleep=wait_sleep, name=name, namespace=namespace,
|
||||
)
|
||||
|
||||
results["changed"] = True
|
||||
|
||||
if not success:
|
||||
raise ResourceTimeout(
|
||||
'"{0}" "{1}": Resource creation timed out'.format(
|
||||
definition["kind"], origin_name
|
||||
),
|
||||
**results
|
||||
)
|
||||
|
||||
return results
|
||||
|
||||
def apply(
|
||||
self,
|
||||
resource: Resource,
|
||||
definition: Dict,
|
||||
existing: Optional[ResourceInstance] = None,
|
||||
) -> Dict:
|
||||
apply = self.module.params.get("apply", False)
|
||||
origin_name = definition["metadata"].get("name")
|
||||
name = definition["metadata"].get("name")
|
||||
namespace = definition["metadata"].get("namespace")
|
||||
wait = self.module.params.get("wait")
|
||||
wait_sleep = self.module.params.get("wait_sleep")
|
||||
wait_condition = None
|
||||
if self.module.params.get("wait_condition") and self.module.params[
|
||||
"wait_condition"
|
||||
].get("type"):
|
||||
wait_condition = self.module.params["wait_condition"]
|
||||
wait_timeout = self.module.params.get("wait_timeout")
|
||||
results = {"changed": False, "result": {}}
|
||||
|
||||
if apply:
|
||||
if self.module.check_mode and not self.client.dry_run:
|
||||
ignored, patch = apply_object(resource, _encode_stringdata(definition))
|
||||
if existing:
|
||||
k8s_obj = dict_merge(existing.to_dict(), patch)
|
||||
else:
|
||||
k8s_obj = patch
|
||||
else:
|
||||
try:
|
||||
params = {}
|
||||
if self.module.check_mode:
|
||||
params["dry_run"] = "All"
|
||||
k8s_obj = self.client.apply(
|
||||
resource, definition, namespace=namespace, **params
|
||||
).to_dict()
|
||||
except Exception as e:
|
||||
reason = e.body if hasattr(e, "body") else e
|
||||
msg = "Failed to apply object: {0}".format(reason)
|
||||
raise CoreException(msg) from e
|
||||
|
||||
success = True
|
||||
results["result"] = k8s_obj
|
||||
|
||||
if wait and not self.module.check_mode:
|
||||
waiter = get_waiter(self.client, resource, condition=wait_condition)
|
||||
success, results["result"], results["duration"] = waiter.wait(
|
||||
timeout=wait_timeout,
|
||||
sleep=wait_sleep,
|
||||
name=name,
|
||||
namespace=namespace,
|
||||
)
|
||||
|
||||
if existing:
|
||||
existing = existing.to_dict()
|
||||
else:
|
||||
existing = {}
|
||||
|
||||
match, diffs = self.diff_objects(existing, results["result"])
|
||||
results["changed"] = not match
|
||||
|
||||
if self.module._diff:
|
||||
results["diff"] = diffs
|
||||
|
||||
if not success:
|
||||
raise ResourceTimeout(
|
||||
'"{0}" "{1}": Resource apply timed out'.format(
|
||||
definition["kind"], origin_name
|
||||
),
|
||||
**results
|
||||
)
|
||||
|
||||
return results
|
||||
|
||||
def replace(
|
||||
self, resource: Resource, definition: Dict, existing: ResourceInstance,
|
||||
) -> Dict:
|
||||
append_hash = self.module.params.get("append_hash", False)
|
||||
name = definition["metadata"].get("name")
|
||||
origin_name = definition["metadata"].get("name")
|
||||
namespace = definition["metadata"].get("namespace")
|
||||
wait = self.module.params.get("wait")
|
||||
wait_sleep = self.module.params.get("wait_sleep")
|
||||
wait_timeout = self.module.params.get("wait_timeout")
|
||||
wait_condition = None
|
||||
if self.module.params.get("wait_condition") and self.module.params[
|
||||
"wait_condition"
|
||||
].get("type"):
|
||||
wait_condition = self.module.params["wait_condition"]
|
||||
results = {"changed": False, "result": {}}
|
||||
|
||||
if self.module.check_mode and not self.module.client.dry_run:
|
||||
k8s_obj = _encode_stringdata(definition)
|
||||
else:
|
||||
params = {}
|
||||
if self.module.check_mode:
|
||||
params["dry_run"] = "All"
|
||||
try:
|
||||
k8s_obj = self.client.replace(
|
||||
resource,
|
||||
definition,
|
||||
name=name,
|
||||
namespace=namespace,
|
||||
append_hash=append_hash,
|
||||
**params
|
||||
).to_dict()
|
||||
except Exception as e:
|
||||
reason = e.body if hasattr(e, "body") else e
|
||||
msg = "Failed to replace object: {0}".format(reason)
|
||||
raise CoreException(msg) from e
|
||||
|
||||
match, diffs = self.diff_objects(existing.to_dict(), k8s_obj)
|
||||
success = True
|
||||
results["result"] = k8s_obj
|
||||
|
||||
if wait and not self.module.check_mode:
|
||||
waiter = get_waiter(self.client, resource, condition=wait_condition)
|
||||
success, results["result"], results["duration"] = waiter.wait(
|
||||
timeout=wait_timeout, sleep=wait_sleep, name=name, namespace=namespace,
|
||||
)
|
||||
match, diffs = self.diff_objects(existing.to_dict(), results["result"])
|
||||
results["changed"] = not match
|
||||
|
||||
if self.module._diff:
|
||||
results["diff"] = diffs
|
||||
|
||||
if not success:
|
||||
raise ResourceTimeout(
|
||||
'"{0}" "{1}": Resource replacement timed out'.format(
|
||||
definition["kind"], origin_name
|
||||
),
|
||||
**results
|
||||
)
|
||||
|
||||
return results
|
||||
|
||||
def update(
|
||||
self, resource: Resource, definition: Dict, existing: ResourceInstance
|
||||
) -> Dict:
|
||||
name = definition["metadata"].get("name")
|
||||
origin_name = definition["metadata"].get("name")
|
||||
namespace = definition["metadata"].get("namespace")
|
||||
wait = self.module.params.get("wait")
|
||||
wait_sleep = self.module.params.get("wait_sleep")
|
||||
wait_timeout = self.module.params.get("wait_timeout")
|
||||
wait_condition = None
|
||||
if self.module.params.get("wait_condition") and self.module.params[
|
||||
"wait_condition"
|
||||
].get("type"):
|
||||
wait_condition = self.module.params["wait_condition"]
|
||||
results = {"changed": False, "result": {}}
|
||||
|
||||
if self.module.check_mode and not self.module.client.dry_run:
|
||||
k8s_obj = dict_merge(existing.to_dict(), _encode_stringdata(definition))
|
||||
else:
|
||||
for merge_type in self.module.params.get("merge_type") or [
|
||||
"strategic-merge",
|
||||
"merge",
|
||||
]:
|
||||
k8s_obj = self.patch_resource(
|
||||
resource, definition, name, namespace, merge_type=merge_type,
|
||||
)
|
||||
|
||||
success = True
|
||||
results["result"] = k8s_obj
|
||||
|
||||
if wait and not self.module.check_mode:
|
||||
waiter = get_waiter(self.client, resource, condition=wait_condition)
|
||||
success, results["result"], results["duration"] = waiter.wait(
|
||||
timeout=wait_timeout, sleep=wait_sleep, name=name, namespace=namespace,
|
||||
)
|
||||
|
||||
match, diffs = self.diff_objects(existing.to_dict(), results["result"])
|
||||
results["changed"] = not match
|
||||
|
||||
if self.module._diff:
|
||||
results["diff"] = diffs
|
||||
|
||||
if not success:
|
||||
raise ResourceTimeout(
|
||||
'"{0}" "{1}": Resource update timed out'.format(
|
||||
definition["kind"], origin_name
|
||||
),
|
||||
**results
|
||||
)
|
||||
|
||||
return results
|
||||
|
||||
def delete(
|
||||
self,
|
||||
resource: Resource,
|
||||
definition: Dict,
|
||||
existing: Optional[ResourceInstance] = None,
|
||||
) -> Dict:
|
||||
delete_options = self.module.params.get("delete_options")
|
||||
label_selectors = self.module.params.get("label_selectors")
|
||||
origin_name = definition["metadata"].get("name")
|
||||
name = definition["metadata"].get("name")
|
||||
namespace = definition["metadata"].get("namespace")
|
||||
wait = self.module.params.get("wait")
|
||||
wait_sleep = self.module.params.get("wait_sleep")
|
||||
wait_timeout = self.module.params.get("wait_timeout")
|
||||
results = {"changed": False, "result": {}}
|
||||
params = {}
|
||||
|
||||
def _empty_resource_list() -> bool:
|
||||
if existing and existing.kind.endswith("List"):
|
||||
return existing.items == []
|
||||
return False
|
||||
|
||||
if not existing or _empty_resource_list():
|
||||
# The object already does not exist
|
||||
return results
|
||||
else:
|
||||
# Delete the object
|
||||
results["changed"] = True
|
||||
if self.module.check_mode and not self.client.dry_run:
|
||||
return results
|
||||
else:
|
||||
if delete_options:
|
||||
body = {
|
||||
"apiVersion": "v1",
|
||||
"kind": "DeleteOptions",
|
||||
}
|
||||
body.update(delete_options)
|
||||
params["body"] = body
|
||||
|
||||
if self.module.check_mode:
|
||||
params["dry_run"] = "All"
|
||||
try:
|
||||
k8s_obj = self.client.delete(resource, **params)
|
||||
results["result"] = k8s_obj.to_dict()
|
||||
except Exception as e:
|
||||
reason = e.body if hasattr(e, "body") else e
|
||||
msg = "Failed to delete object: {0}".format(reason)
|
||||
raise CoreException(msg) from e
|
||||
|
||||
if wait and not self.module.check_mode:
|
||||
waiter = get_waiter(self.client, resource, state="absent")
|
||||
success, resource, duration = waiter.wait(
|
||||
timeout=wait_timeout,
|
||||
sleep=wait_sleep,
|
||||
name=name,
|
||||
namespace=namespace,
|
||||
label_selectors=label_selectors,
|
||||
)
|
||||
results["duration"] = duration
|
||||
if not success:
|
||||
raise ResourceTimeout(
|
||||
'"{0}" "{1}": Resource deletion timed out'.format(
|
||||
definition["kind"], origin_name
|
||||
),
|
||||
**results
|
||||
)
|
||||
|
||||
return results
|
||||
@@ -128,17 +128,27 @@ class Waiter:
|
||||
|
||||
def wait(
|
||||
self,
|
||||
definition: Dict,
|
||||
timeout: int,
|
||||
sleep: int,
|
||||
name: Optional[str] = None,
|
||||
namespace: Optional[str] = None,
|
||||
label_selectors: Optional[List[str]] = None,
|
||||
field_selectors: Optional[List[str]] = None,
|
||||
) -> Tuple[bool, Optional[Dict], int]:
|
||||
params = {
|
||||
"name": definition["metadata"].get("name"),
|
||||
"namespace": definition["metadata"].get("namespace"),
|
||||
}
|
||||
params = {}
|
||||
|
||||
if name:
|
||||
params["name"] = name
|
||||
|
||||
if namespace:
|
||||
params["namespace"] = namespace
|
||||
|
||||
if label_selectors:
|
||||
params["label_selector"] = ",".join(label_selectors)
|
||||
|
||||
if field_selectors:
|
||||
params["field_selector"] = ",".join(field_selectors)
|
||||
|
||||
instance: Optional[Dict] = None
|
||||
response = None
|
||||
elapsed = 0
|
||||
|
||||
373
tests/unit/module_utils/test_service.py
Normal file
373
tests/unit/module_utils/test_service.py
Normal file
@@ -0,0 +1,373 @@
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
from kubernetes.dynamic.resource import ResourceInstance, Resource
|
||||
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.service import (
|
||||
K8sService,
|
||||
)
|
||||
|
||||
from kubernetes.dynamic.exceptions import NotFoundError
|
||||
|
||||
pod_definition = {
|
||||
"apiVersion": "v1",
|
||||
"kind": "Pod",
|
||||
"metadata": {
|
||||
"name": "foo",
|
||||
"labels": {"environment": "production", "app": "nginx"},
|
||||
"namespace": "foo",
|
||||
},
|
||||
"spec": {
|
||||
"containers": [
|
||||
{
|
||||
"name": "nginx",
|
||||
"image": "nginx:1.14.2",
|
||||
"command": ["/bin/sh", "-c", "sleep 10"],
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
pod_definition_updated = {
|
||||
"apiVersion": "v1",
|
||||
"kind": "Pod",
|
||||
"metadata": {
|
||||
"name": "foo",
|
||||
"labels": {"environment": "testing", "app": "nginx"},
|
||||
"namespace": "bar",
|
||||
},
|
||||
"spec": {
|
||||
"containers": [
|
||||
{
|
||||
"name": "nginx",
|
||||
"image": "nginx:1.14.2",
|
||||
"command": ["/bin/sh", "-c", "sleep 10"],
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def mock_pod_resource_instance():
|
||||
return ResourceInstance(None, pod_definition)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def mock_pod_updated_resource_instance():
|
||||
return ResourceInstance(None, pod_definition_updated)
|
||||
|
||||
|
||||
def test_diff_objects_no_diff():
|
||||
svc = K8sService(Mock(), Mock())
|
||||
match, diff = svc.diff_objects(pod_definition, pod_definition)
|
||||
|
||||
assert match is True
|
||||
assert diff == {}
|
||||
|
||||
|
||||
def test_diff_objects_meta_diff():
|
||||
svc = K8sService(Mock(), Mock())
|
||||
match, diff = svc.diff_objects(pod_definition, pod_definition_updated)
|
||||
|
||||
assert match is False
|
||||
assert diff["before"] == {
|
||||
"metadata": {"labels": {"environment": "production"}, "namespace": "foo"}
|
||||
}
|
||||
assert diff["after"] == {
|
||||
"metadata": {"labels": {"environment": "testing"}, "namespace": "bar"}
|
||||
}
|
||||
|
||||
|
||||
def test_diff_objects_spec_diff():
|
||||
pod_definition_updated = {
|
||||
"apiVersion": "v1",
|
||||
"kind": "Pod",
|
||||
"metadata": {
|
||||
"name": "foo",
|
||||
"labels": {"environment": "production", "app": "nginx"},
|
||||
"namespace": "foo",
|
||||
},
|
||||
"spec": {
|
||||
"containers": [
|
||||
{
|
||||
"name": "busybox",
|
||||
"image": "busybox",
|
||||
"command": ["/bin/sh", "-c", "sleep 3600"],
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
svc = K8sService(Mock(), Mock())
|
||||
match, diff = svc.diff_objects(pod_definition, pod_definition_updated)
|
||||
|
||||
assert match is False
|
||||
assert diff["before"]["spec"] == pod_definition["spec"]
|
||||
assert diff["after"]["spec"] == pod_definition_updated["spec"]
|
||||
|
||||
|
||||
def test_find_resource():
|
||||
mock_pod_resource = Resource(
|
||||
api_version="v1", kind="Pod", namespaced=False, preferred=True, prefix="api"
|
||||
)
|
||||
spec = {"resources.get.side_effect": [mock_pod_resource]}
|
||||
client = Mock(**spec)
|
||||
svc = K8sService(client, Mock())
|
||||
resource = svc.find_resource("Pod", "v1")
|
||||
|
||||
assert isinstance(resource, Resource)
|
||||
assert resource.to_dict().items() <= mock_pod_resource.to_dict().items()
|
||||
|
||||
|
||||
def test_service_delete_existing_resource(mock_pod_resource_instance):
|
||||
spec = {"delete.side_effect": [mock_pod_resource_instance]}
|
||||
client = Mock(**spec)
|
||||
module = Mock()
|
||||
module.params = {}
|
||||
module.check_mode = False
|
||||
svc = K8sService(client, module)
|
||||
results = svc.delete(Mock(), pod_definition, mock_pod_resource_instance)
|
||||
|
||||
assert isinstance(results, dict)
|
||||
assert results["changed"] is True
|
||||
assert results["result"] == pod_definition
|
||||
|
||||
|
||||
def test_service_delete_no_existing_resource():
|
||||
module = Mock()
|
||||
module.params = {}
|
||||
module.check_mode = False
|
||||
svc = K8sService(Mock(), module)
|
||||
results = svc.delete(Mock(), pod_definition)
|
||||
|
||||
assert isinstance(results, dict)
|
||||
assert results["changed"] is False
|
||||
assert results["result"] == {}
|
||||
|
||||
|
||||
def test_service_delete_existing_resource_check_mode(mock_pod_resource_instance):
|
||||
module = Mock()
|
||||
module.params = {"wait": False}
|
||||
module.check_mode = True
|
||||
svc = K8sService(Mock(), module)
|
||||
results = svc.delete(Mock(), pod_definition, mock_pod_resource_instance)
|
||||
|
||||
assert isinstance(results, dict)
|
||||
assert results["changed"] is True
|
||||
|
||||
|
||||
def test_service_create_resource(mock_pod_resource_instance):
|
||||
spec = {"create.side_effect": [mock_pod_resource_instance]}
|
||||
client = Mock(**spec)
|
||||
module = Mock()
|
||||
module.params = {}
|
||||
module.check_mode = False
|
||||
svc = K8sService(client, module)
|
||||
results = svc.create(Mock(), pod_definition)
|
||||
|
||||
assert isinstance(results, dict)
|
||||
assert results["changed"] is True
|
||||
assert results["result"] == pod_definition
|
||||
|
||||
|
||||
def test_service_create_resource_check_mode():
|
||||
client = Mock()
|
||||
client.dry_run = False
|
||||
module = Mock()
|
||||
module.params = {}
|
||||
module.check_mode = True
|
||||
svc = K8sService(client, module)
|
||||
results = svc.create(Mock(), pod_definition)
|
||||
|
||||
assert isinstance(results, dict)
|
||||
assert results["changed"] is True
|
||||
assert results["result"] == pod_definition
|
||||
|
||||
|
||||
def test_service_retrieve_existing_resource(mock_pod_resource_instance):
|
||||
spec = {"get.side_effect": [mock_pod_resource_instance]}
|
||||
client = Mock(**spec)
|
||||
module = Mock()
|
||||
module.params = {}
|
||||
svc = K8sService(client, module)
|
||||
results = svc.retrieve(Mock(), pod_definition)
|
||||
|
||||
assert isinstance(results, dict)
|
||||
assert results["changed"] is False
|
||||
assert results["result"] == pod_definition
|
||||
|
||||
|
||||
def test_service_retrieve_no_existing_resource():
|
||||
spec = {"get.side_effect": [NotFoundError(Mock())]}
|
||||
client = Mock(**spec)
|
||||
module = Mock()
|
||||
module.params = {}
|
||||
svc = K8sService(client, module)
|
||||
results = svc.retrieve(Mock(), pod_definition)
|
||||
|
||||
assert isinstance(results, dict)
|
||||
assert results["changed"] is False
|
||||
assert results["result"] == {}
|
||||
|
||||
|
||||
def test_create_project_request():
|
||||
project_definition = {
|
||||
"apiVersion": "v1",
|
||||
"kind": "ProjectRequest",
|
||||
"metadata": {"name": "test"},
|
||||
}
|
||||
spec = {"create.side_effect": [ResourceInstance(None, project_definition)]}
|
||||
client = Mock(**spec)
|
||||
module = Mock()
|
||||
module.check_mode = False
|
||||
module.params = {"state": "present"}
|
||||
svc = K8sService(client, module)
|
||||
results = svc.create_project_request(project_definition)
|
||||
|
||||
assert isinstance(results, dict)
|
||||
assert results["changed"] is True
|
||||
assert results["result"] == project_definition
|
||||
|
||||
|
||||
def test_service_apply_existing_resource(mock_pod_resource_instance):
|
||||
spec = {"apply.side_effect": [ResourceInstance(None, pod_definition_updated)]}
|
||||
client = Mock(**spec)
|
||||
module = Mock()
|
||||
module.params = {"apply": True}
|
||||
module.check_mode = False
|
||||
svc = K8sService(client, module)
|
||||
results = svc.apply(Mock(), pod_definition_updated, mock_pod_resource_instance)
|
||||
|
||||
assert isinstance(results, dict)
|
||||
assert results["changed"] is True
|
||||
assert results["diff"] is not {}
|
||||
assert results["result"] == pod_definition_updated
|
||||
|
||||
|
||||
def test_service_apply_existing_resource_no_diff(mock_pod_resource_instance):
|
||||
spec = {"apply.side_effect": [mock_pod_resource_instance]}
|
||||
client = Mock(**spec)
|
||||
module = Mock()
|
||||
module.params = {"apply": True}
|
||||
module.check_mode = False
|
||||
svc = K8sService(client, module)
|
||||
results = svc.apply(Mock(), pod_definition, mock_pod_resource_instance)
|
||||
|
||||
assert isinstance(results, dict)
|
||||
assert results["changed"] is False
|
||||
assert results["diff"] == {}
|
||||
assert results["result"] == pod_definition
|
||||
|
||||
|
||||
def test_service_apply_existing_resource_no_apply(mock_pod_resource_instance):
|
||||
spec = {"apply.side_effect": [mock_pod_resource_instance]}
|
||||
client = Mock(**spec)
|
||||
module = Mock()
|
||||
module.params = {"apply": False}
|
||||
module.check_mode = False
|
||||
svc = K8sService(client, module)
|
||||
results = svc.apply(Mock(), pod_definition, mock_pod_resource_instance)
|
||||
|
||||
assert isinstance(results, dict)
|
||||
assert results["changed"] is False
|
||||
assert results["result"] == {}
|
||||
|
||||
|
||||
def test_service_replace_existing_resource_no_diff(mock_pod_resource_instance):
|
||||
spec = {"replace.side_effect": [mock_pod_resource_instance]}
|
||||
client = Mock(**spec)
|
||||
module = Mock()
|
||||
module.params = {}
|
||||
module.check_mode = False
|
||||
svc = K8sService(client, module)
|
||||
results = svc.replace(Mock(), pod_definition, mock_pod_resource_instance)
|
||||
|
||||
assert isinstance(results, dict)
|
||||
assert results["changed"] is False
|
||||
assert results["diff"] == {}
|
||||
assert results["result"] == pod_definition
|
||||
|
||||
|
||||
def test_service_replace_existing_resource(
|
||||
mock_pod_resource_instance, mock_pod_updated_resource_instance
|
||||
):
|
||||
spec = {"replace.side_effect": [mock_pod_updated_resource_instance]}
|
||||
client = Mock(**spec)
|
||||
module = Mock()
|
||||
module.params = {}
|
||||
module.check_mode = False
|
||||
svc = K8sService(client, module)
|
||||
results = svc.replace(Mock(), pod_definition_updated, mock_pod_resource_instance)
|
||||
|
||||
assert isinstance(results, dict)
|
||||
assert results["changed"] is True
|
||||
assert results["result"] == pod_definition_updated
|
||||
assert results["diff"] != {}
|
||||
assert results["diff"]["before"] is not {}
|
||||
assert results["diff"]["after"] is not {}
|
||||
|
||||
|
||||
def test_service_update_existing_resource(
|
||||
mock_pod_resource_instance, mock_pod_updated_resource_instance
|
||||
):
|
||||
spec = {"replace.side_effect": [mock_pod_updated_resource_instance]}
|
||||
client = Mock(**spec)
|
||||
module = Mock()
|
||||
module.params = {}
|
||||
module.check_mode = False
|
||||
svc = K8sService(client, module)
|
||||
results = svc.replace(Mock(), pod_definition_updated, mock_pod_resource_instance)
|
||||
|
||||
assert isinstance(results, dict)
|
||||
assert results["changed"] is True
|
||||
assert results["result"] == pod_definition_updated
|
||||
assert results["diff"] != {}
|
||||
assert results["diff"]["before"] is not {}
|
||||
assert results["diff"]["after"] is not {}
|
||||
|
||||
|
||||
def test_service_update_existing_resource_no_diff(mock_pod_updated_resource_instance):
|
||||
spec = {"replace.side_effect": [mock_pod_updated_resource_instance]}
|
||||
client = Mock(**spec)
|
||||
module = Mock()
|
||||
module.params = {}
|
||||
module.check_mode = False
|
||||
svc = K8sService(client, module)
|
||||
results = svc.replace(
|
||||
Mock(), pod_definition_updated, mock_pod_updated_resource_instance
|
||||
)
|
||||
|
||||
assert isinstance(results, dict)
|
||||
assert results["changed"] is False
|
||||
assert results["result"] == pod_definition_updated
|
||||
assert results["diff"] == {}
|
||||
|
||||
|
||||
def test_service_find(mock_pod_resource_instance):
|
||||
spec = {"get.side_effect": [mock_pod_resource_instance]}
|
||||
client = Mock(**spec)
|
||||
module = Mock()
|
||||
module.params = {}
|
||||
module.check_mode = False
|
||||
svc = K8sService(client, module)
|
||||
results = svc.find("Pod", "v1", name="foo", namespace="foo")
|
||||
|
||||
assert isinstance(results, dict)
|
||||
assert results["api_found"] is True
|
||||
assert results["resources"] is not []
|
||||
assert len(results["resources"]) == 1
|
||||
assert results["resources"][0] == pod_definition
|
||||
|
||||
|
||||
def test_service_find_error():
|
||||
spec = {"get.side_effect": [NotFoundError(Mock())]}
|
||||
client = Mock(**spec)
|
||||
module = Mock()
|
||||
module.params = {}
|
||||
module.check_mode = False
|
||||
svc = K8sService(client, module)
|
||||
results = svc.find("Pod", "v1", name="foo", namespace="foo")
|
||||
|
||||
assert isinstance(results, dict)
|
||||
assert results["api_found"] is True
|
||||
assert results["resources"] == []
|
||||
@@ -83,7 +83,10 @@ def test_waiter_waits_for_missing_resource():
|
||||
client = Mock(**spec)
|
||||
resource = Mock()
|
||||
result, instance, elapsed = Waiter(client, resource, exists).wait(
|
||||
RESOURCES[0], 3, 1
|
||||
timeout=3,
|
||||
sleep=1,
|
||||
name=RESOURCES[0]["metadata"].get("name"),
|
||||
namespace=RESOURCES[0]["metadata"].get("namespace"),
|
||||
)
|
||||
assert result is False
|
||||
assert instance is None
|
||||
@@ -95,7 +98,12 @@ def test_waiter_waits_for_resource_to_exist(resource, expected):
|
||||
result = resource.to_dict()
|
||||
spec = {"get.side_effect": [NotFoundError(Mock()), resource, resource, resource]}
|
||||
client = Mock(**spec)
|
||||
success, instance, elapsed = Waiter(client, Mock(), exists).wait(result, 3, 1)
|
||||
success, instance, elapsed = Waiter(client, Mock(), exists).wait(
|
||||
timeout=3,
|
||||
sleep=1,
|
||||
name=result["metadata"].get("name"),
|
||||
namespace=result["metadata"].get("namespace"),
|
||||
)
|
||||
assert success is expected
|
||||
assert instance == result
|
||||
assert abs(elapsed - 2) <= 1
|
||||
|
||||
Reference in New Issue
Block a user