mirror of
https://github.com/ansible-collections/kubernetes.core.git
synced 2026-03-30 15:23:03 +00:00
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>
239 lines
7.8 KiB
Python
239 lines
7.8 KiB
Python
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)
|