mirror of
https://github.com/ansible-collections/kubernetes.core.git
synced 2026-05-06 13:02:37 +00:00
Port changes from main to refactored branch (#472)
Port changes from main to refactored branch Depends-on: ansible/ansible-zuul-jobs#1563 SUMMARY This PR contains several commits that complete the rebase of the 2.x-refactor branch onto main. Most of the changes here had to be manually backported after rebasing as the original changes were to code that will be deprecated. In addition, rather than trying to manually sort out conflicts and changes to the sanity ignores, I rewrote the refresh_ignore_files script to fully automate the management of ignore files. Previously, these files were both manually edited and auto-generated. This should no longer be the case, and these files should never be manually edited going forward. For the purposes of reviewing and history, I kept all changes in separate commits tied to the original commit being backported. ISSUE TYPE COMPONENT NAME ADDITIONAL INFORMATION Reviewed-by: Jill R <None>
This commit is contained in:
@@ -12,7 +12,9 @@ from ansible_collections.kubernetes.core.plugins.module_utils.args_common import
|
||||
AUTH_ARG_SPEC,
|
||||
AUTH_PROXY_HEADERS_SPEC,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.core import requires
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.core import (
|
||||
requires as _requires,
|
||||
)
|
||||
|
||||
try:
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils import (
|
||||
@@ -49,6 +51,25 @@ except ImportError:
|
||||
_pool = {}
|
||||
|
||||
|
||||
class unique_string(str):
|
||||
_low = None
|
||||
|
||||
def __hash__(self):
|
||||
return id(self)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self is other
|
||||
|
||||
def lower(self):
|
||||
if self._low is None:
|
||||
lower = str.lower(self)
|
||||
if str.__eq__(lower, self):
|
||||
self._low = self
|
||||
else:
|
||||
self._low = unique_string(lower)
|
||||
return self._low
|
||||
|
||||
|
||||
def _create_auth_spec(module=None, **kwargs) -> Dict:
|
||||
auth: Dict = {}
|
||||
# If authorization variables aren't defined, look for them in environment variables
|
||||
@@ -148,7 +169,30 @@ def _create_configuration(auth: Dict):
|
||||
return configuration
|
||||
|
||||
|
||||
def _configuration_digest(configuration) -> str:
|
||||
def _create_headers(module=None, **kwargs):
|
||||
header_map = {
|
||||
"impersonate_user": "Impersonate-User",
|
||||
"impersonate_groups": "Impersonate-Group",
|
||||
}
|
||||
|
||||
headers = {}
|
||||
for arg_name, header_name in header_map.items():
|
||||
value = None
|
||||
if module and module.params.get(arg_name) is not None:
|
||||
value = module.params.get(arg_name)
|
||||
elif arg_name in kwargs and kwargs.get(arg_name) is not None:
|
||||
value = kwargs.get(arg_name)
|
||||
else:
|
||||
value = os.getenv("K8S_AUTH_{0}".format(arg_name.upper()), None)
|
||||
if value is not None:
|
||||
if AUTH_ARG_SPEC[arg_name].get("type") == "list":
|
||||
value = [x for x in value.split(",") if x != ""]
|
||||
if value:
|
||||
headers[header_name] = value
|
||||
return headers
|
||||
|
||||
|
||||
def _configuration_digest(configuration, **kwargs) -> str:
|
||||
m = hashlib.sha256()
|
||||
for k in AUTH_ARG_MAP:
|
||||
if not hasattr(configuration, k):
|
||||
@@ -161,19 +205,36 @@ def _configuration_digest(configuration) -> str:
|
||||
m.update(content.encode())
|
||||
else:
|
||||
m.update(str(v).encode())
|
||||
for k, v in kwargs.items():
|
||||
content = "{0}: {1}".format(k, v)
|
||||
m.update(content.encode())
|
||||
digest = m.hexdigest()
|
||||
|
||||
return digest
|
||||
|
||||
|
||||
def _set_header(client, header, value):
|
||||
if isinstance(value, list):
|
||||
for v in value:
|
||||
client.set_default_header(header_name=unique_string(header), header_value=v)
|
||||
else:
|
||||
client.set_default_header(header_name=header, header_value=value)
|
||||
|
||||
|
||||
def cache(func):
|
||||
def wrapper(*args):
|
||||
def wrapper(*args, **kwargs):
|
||||
client = None
|
||||
digest = _configuration_digest(*args)
|
||||
hashable_kwargs = {}
|
||||
for k, v in kwargs.items():
|
||||
if isinstance(v, list):
|
||||
hashable_kwargs[k] = ",".join(sorted(v))
|
||||
else:
|
||||
hashable_kwargs[k] = v
|
||||
digest = _configuration_digest(*args, **hashable_kwargs)
|
||||
if digest in _pool:
|
||||
client = _pool[digest]
|
||||
else:
|
||||
client = func(*args)
|
||||
client = func(*args, **kwargs)
|
||||
_pool[digest] = client
|
||||
|
||||
return client
|
||||
@@ -182,10 +243,11 @@ def cache(func):
|
||||
|
||||
|
||||
@cache
|
||||
def create_api_client(configuration):
|
||||
return k8sdynamicclient.K8SDynamicClient(
|
||||
kubernetes.client.ApiClient(configuration), discoverer=LazyDiscoverer
|
||||
)
|
||||
def create_api_client(configuration, **headers):
|
||||
client = kubernetes.client.ApiClient(configuration)
|
||||
for header, value in headers.items():
|
||||
_set_header(client, header, value)
|
||||
return k8sdynamicclient.K8SDynamicClient(client, discoverer=LazyDiscoverer)
|
||||
|
||||
|
||||
class K8SClient:
|
||||
@@ -204,20 +266,32 @@ class K8SClient:
|
||||
def resources(self) -> List[Any]:
|
||||
return self.client.resources
|
||||
|
||||
def _find_resource_with_prefix(
|
||||
self, prefix: str, kind: str, api_version: str
|
||||
) -> Resource:
|
||||
for attribute in ["kind", "name", "singular_name"]:
|
||||
try:
|
||||
return self.client.resources.get(
|
||||
**{"prefix": prefix, "api_version": api_version, attribute: kind}
|
||||
)
|
||||
except (ResourceNotFoundError, ResourceNotUniqueError):
|
||||
pass
|
||||
return self.client.resources.get(
|
||||
prefix=prefix, api_version=api_version, short_names=[kind]
|
||||
)
|
||||
|
||||
def resource(self, kind: str, api_version: str) -> Resource:
|
||||
"""Fetch a kubernetes client resource.
|
||||
|
||||
This will attempt to find a kubernetes resource trying, in order, kind,
|
||||
name, singular_name and short_names.
|
||||
"""
|
||||
for attribute in ["kind", "name", "singular_name"]:
|
||||
try:
|
||||
return self.client.resources.get(
|
||||
**{"api_version": api_version, attribute: kind}
|
||||
)
|
||||
except (ResourceNotFoundError, ResourceNotUniqueError):
|
||||
pass
|
||||
return self.client.resources.get(api_version=api_version, short_names=[kind])
|
||||
try:
|
||||
if api_version == "v1":
|
||||
return self._find_resource_with_prefix("api", kind, api_version)
|
||||
except ResourceNotFoundError:
|
||||
pass
|
||||
return self._find_resource_with_prefix(None, kind, api_version)
|
||||
|
||||
def _ensure_dry_run(self, params: Dict) -> Dict:
|
||||
if self.dry_run:
|
||||
@@ -252,13 +326,18 @@ class K8SClient:
|
||||
|
||||
def get_api_client(module=None, **kwargs: Optional[Any]) -> K8SClient:
|
||||
auth_spec = _create_auth_spec(module, **kwargs)
|
||||
if module:
|
||||
requires = module.requires
|
||||
else:
|
||||
requires = _requires
|
||||
if isinstance(auth_spec.get("kubeconfig"), dict):
|
||||
if module:
|
||||
module.requires("kubernetes", "17.17.0", "to use in-memory config")
|
||||
else:
|
||||
requires("kubernetes", "17.17.0", "to use in-memory config")
|
||||
requires("kubernetes", "17.17.0", "to use in-memory config")
|
||||
if auth_spec.get("no_proxy"):
|
||||
requires("kubernetes", "19.15.0", "to use the no_proxy feature")
|
||||
|
||||
configuration = _create_configuration(auth_spec)
|
||||
client = create_api_client(configuration)
|
||||
headers = _create_headers(module, **kwargs)
|
||||
client = create_api_client(configuration, **headers)
|
||||
|
||||
k8s_client = K8SClient(
|
||||
configuration=configuration,
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
from distutils.version import LooseVersion
|
||||
from typing import Optional
|
||||
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.version import (
|
||||
LooseVersion,
|
||||
)
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
from ansible.module_utils.common.text.converters import to_text
|
||||
|
||||
@@ -90,7 +90,7 @@ def from_yaml(definition: Union[str, List, Dict]) -> Iterable[Dict]:
|
||||
def from_file(filepath: str) -> Iterable[Dict]:
|
||||
"""Load resource definitions from a path to a yaml file."""
|
||||
path = os.path.normpath(filepath)
|
||||
with open(path, "r") as f:
|
||||
with open(path, "rb") as f:
|
||||
definitions = list(yaml.safe_load_all(f))
|
||||
return filter(None, definitions)
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ from ansible_collections.kubernetes.core.plugins.module_utils.k8s.service import
|
||||
diff_objects,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import (
|
||||
CoreException,
|
||||
ResourceTimeout,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.waiter import exists
|
||||
@@ -60,11 +59,15 @@ def run_module(module) -> None:
|
||||
|
||||
try:
|
||||
result = perform_action(svc, definition, module.params)
|
||||
except CoreException as e:
|
||||
except Exception as e:
|
||||
try:
|
||||
error = e.result
|
||||
except AttributeError:
|
||||
error = {}
|
||||
try:
|
||||
error["reason"] = e.__cause__.reason
|
||||
except AttributeError:
|
||||
pass
|
||||
error["msg"] = to_native(e)
|
||||
if warnings:
|
||||
error.setdefault("warnings", []).extend(warnings)
|
||||
@@ -102,7 +105,13 @@ def perform_action(svc, definition: Dict, params: Dict) -> Dict:
|
||||
existing = svc.retrieve(resource, definition)
|
||||
|
||||
if state == "absent":
|
||||
instance = svc.delete(resource, definition, existing)
|
||||
if exists(existing) and existing.kind.endswith("List"):
|
||||
instance = []
|
||||
for item in existing.items:
|
||||
r = svc.delete(resource, item, existing)
|
||||
instance.append(r)
|
||||
else:
|
||||
instance = svc.delete(resource, definition, existing)
|
||||
result["method"] = "delete"
|
||||
if exists(existing):
|
||||
result["changed"] = True
|
||||
@@ -114,7 +123,9 @@ def perform_action(svc, definition: Dict, params: Dict) -> Dict:
|
||||
result["msg"] = (
|
||||
"resource 'kind={kind},name={name},namespace={namespace}' "
|
||||
"filtered by label_selectors.".format(
|
||||
kind=kind, name=origin_name, namespace=namespace,
|
||||
kind=kind,
|
||||
name=origin_name,
|
||||
namespace=namespace,
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
@@ -14,11 +14,16 @@ from ansible_collections.kubernetes.core.plugins.module_utils.k8s.waiter import
|
||||
get_waiter,
|
||||
)
|
||||
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.core import (
|
||||
requires,
|
||||
)
|
||||
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import (
|
||||
CoreException,
|
||||
)
|
||||
|
||||
from ansible.module_utils.common.dict_transformations import dict_merge
|
||||
from ansible.module_utils._text import to_native
|
||||
|
||||
try:
|
||||
from kubernetes.dynamic.exceptions import (
|
||||
@@ -260,6 +265,9 @@ class K8sService:
|
||||
)
|
||||
except BadRequestError:
|
||||
return result
|
||||
except CoreException as e:
|
||||
result["msg"] = to_native(e)
|
||||
return result
|
||||
|
||||
# There is either no result or there is a List resource with no items
|
||||
if (
|
||||
@@ -281,7 +289,10 @@ class K8sService:
|
||||
name = instance["metadata"].get("name")
|
||||
namespace = instance["metadata"].get("namespace")
|
||||
success, res, duration = waiter.wait(
|
||||
timeout=wait_timeout, sleep=wait_sleep, name=name, namespace=namespace,
|
||||
timeout=wait_timeout,
|
||||
sleep=wait_sleep,
|
||||
name=name,
|
||||
namespace=namespace,
|
||||
)
|
||||
if not success:
|
||||
raise CoreException(
|
||||
@@ -331,6 +342,9 @@ class K8sService:
|
||||
) -> Dict:
|
||||
namespace = definition["metadata"].get("namespace")
|
||||
|
||||
server_side_apply = self.module.params.get("server_side_apply")
|
||||
if server_side_apply:
|
||||
requires("kubernetes", "19.15.0", reason="to use server side apply")
|
||||
if self.module.check_mode and not self.client.dry_run:
|
||||
ignored, patch = apply_object(resource, _encode_stringdata(definition))
|
||||
if existing:
|
||||
@@ -342,6 +356,9 @@ class K8sService:
|
||||
params = {}
|
||||
if self.module.check_mode:
|
||||
params["dry_run"] = "All"
|
||||
if server_side_apply:
|
||||
params["server_side"] = True
|
||||
params.update(server_side_apply)
|
||||
k8s_obj = self.client.apply(
|
||||
resource, definition, namespace=namespace, **params
|
||||
).to_dict()
|
||||
@@ -352,7 +369,10 @@ class K8sService:
|
||||
return k8s_obj
|
||||
|
||||
def replace(
|
||||
self, resource: Resource, definition: Dict, existing: ResourceInstance,
|
||||
self,
|
||||
resource: Resource,
|
||||
definition: Dict,
|
||||
existing: ResourceInstance,
|
||||
) -> Dict:
|
||||
append_hash = self.module.params.get("append_hash", False)
|
||||
name = definition["metadata"].get("name")
|
||||
@@ -395,7 +415,11 @@ class K8sService:
|
||||
]:
|
||||
try:
|
||||
k8s_obj = self.patch_resource(
|
||||
resource, definition, name, namespace, merge_type=merge_type,
|
||||
resource,
|
||||
definition,
|
||||
name,
|
||||
namespace,
|
||||
merge_type=merge_type,
|
||||
)
|
||||
exception = None
|
||||
except CoreException as e:
|
||||
|
||||
@@ -4,6 +4,10 @@ from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Union
|
||||
|
||||
from ansible.module_utils.parsing.convert_bool import boolean
|
||||
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import (
|
||||
CoreException,
|
||||
)
|
||||
|
||||
try:
|
||||
from kubernetes.dynamic.exceptions import NotFoundError
|
||||
from kubernetes.dynamic.resource import Resource, ResourceField, ResourceInstance
|
||||
@@ -13,6 +17,12 @@ except ImportError:
|
||||
ResourceInstance = Any # type: ignore
|
||||
pass
|
||||
|
||||
try:
|
||||
from urllib3.exceptions import HTTPError
|
||||
except ImportError:
|
||||
# Handled during module setup
|
||||
pass
|
||||
|
||||
|
||||
def deployment_ready(deployment: ResourceInstance) -> bool:
|
||||
# FIXME: frustratingly bool(deployment.status) is True even if status is empty
|
||||
@@ -51,14 +61,17 @@ def daemonset_ready(daemonset: ResourceInstance) -> bool:
|
||||
|
||||
|
||||
def statefulset_ready(statefulset: ResourceInstance) -> bool:
|
||||
# These may be None
|
||||
updated_replicas = statefulset.status.updatedReplicas or 0
|
||||
ready_replicas = statefulset.status.readyReplicas or 0
|
||||
return bool(
|
||||
statefulset.status
|
||||
and statefulset.spec.updateStrategy.type == "RollingUpdate"
|
||||
and statefulset.status.observedGeneration
|
||||
== (statefulset.metadata.generation or 0)
|
||||
and statefulset.status.updateRevision == statefulset.status.currentRevision
|
||||
and statefulset.status.updatedReplicas == statefulset.spec.replicas
|
||||
and statefulset.status.readyReplicas == statefulset.spec.replicas
|
||||
and updated_replicas == statefulset.spec.replicas
|
||||
and ready_replicas == statefulset.spec.replicas
|
||||
and statefulset.status.replicas == statefulset.spec.replicas
|
||||
)
|
||||
|
||||
@@ -153,13 +166,24 @@ class Waiter:
|
||||
response = None
|
||||
elapsed = 0
|
||||
for i in clock(timeout, sleep):
|
||||
exception = None
|
||||
elapsed = i
|
||||
try:
|
||||
response = self.client.get(self.resource, **params)
|
||||
except NotFoundError:
|
||||
response = None
|
||||
# Retry connection errors as it may be intermittent network issues
|
||||
except HTTPError as e:
|
||||
exception = e
|
||||
if self.predicate(response):
|
||||
break
|
||||
if exception:
|
||||
msg = (
|
||||
"Exception '{0}' raised while trying to get resource using {1}".format(
|
||||
exception, params
|
||||
)
|
||||
)
|
||||
raise CoreException(msg) from exception
|
||||
if response:
|
||||
instance = response.to_dict()
|
||||
return self.predicate(response), instance, elapsed
|
||||
|
||||
Reference in New Issue
Block a user