diff --git a/CHANGELOG.md b/CHANGELOG.md index 42703dccd6..ef7814a53f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,11 @@ ### Enhancements - Bump `containerd` version to 1.4.8 (PR [#3466](https://github.com/scality/metalk8s/pull/3466)). +- [#3487](https://github.com/scality/metalk8s/issues/3487) - Make Salt + Kubernetes execution module more flexible relying on `DynamicClient` + from `python-kubernetes` + (PR[#3510](https://github.com/scality/metalk8s/pull/3510)) + ## Release 2.10.3 (in development) ### Enhancements diff --git a/buildchain/buildchain/salt_tree.py b/buildchain/buildchain/salt_tree.py index af1dbce703..0b62d152b4 100644 --- a/buildchain/buildchain/salt_tree.py +++ b/buildchain/buildchain/salt_tree.py @@ -641,7 +641,6 @@ def _get_parts(self) -> Iterator[str]: Path("salt/_states/metalk8s_sysctl.py"), Path("salt/_states/metalk8s_volumes.py"), Path("salt/_utils/metalk8s_utils.py"), - Path("salt/_utils/kubernetes_utils.py"), Path("salt/_utils/pillar_utils.py"), Path("salt/_utils/volume_utils.py"), # This image is defined here and not in the `image` module since it is diff --git a/salt/_modules/metalk8s_drain.py b/salt/_modules/metalk8s_drain.py index 77cb61f840..2f9bfb20e2 100644 --- a/salt/_modules/metalk8s_drain.py +++ b/salt/_modules/metalk8s_drain.py @@ -16,8 +16,8 @@ from salt.exceptions import CommandExecutionError try: + import kubernetes from kubernetes.client.rest import ApiException - from kubernetes.client.models.v1_delete_options import V1DeleteOptions from kubernetes.client.models.v1_object_meta import V1ObjectMeta from kubernetes.client.models.v1beta1_eviction import V1beta1Eviction from urllib3.exceptions import HTTPError @@ -68,7 +68,7 @@ def _mirrorpod_filter(pod): """ mirror_annotation = "kubernetes.io/config.mirror" - annotations = pod["metadata"]["annotations"] + annotations = pod["metadata"].get("annotations") if annotations and mirror_annotation in annotations: return False, "" return True, "" @@ -81,8 +81,8 @@ def _has_local_storage(pod): - pod: kubernetes pod object Returns: True if the pod uses local storage, False if not """ - for volume in pod["spec"]["volumes"]: - if volume["empty_dir"] is not None: + for volume in pod["spec"].get("volumes", []): + if volume.get("emptyDir") is not None: return True return False @@ -98,9 +98,9 @@ def _get_controller_of(pod): - pod: kubernetes pod object Returns: the reference to a controller object """ - if pod["metadata"]["owner_references"]: - for owner_ref in pod["metadata"]["owner_references"]: - if owner_ref["controller"]: + if pod["metadata"].get("ownerReferences"): + for owner_ref in pod["metadata"]["ownerReferences"]: + if owner_ref.get("controller"): return owner_ref return None @@ -233,7 +233,7 @@ def get_controller(self, namespace, controller_ref): return __salt__["metalk8s_kubernetes.get_object"]( name=controller_ref["name"], kind=controller_ref["kind"], - apiVersion=controller_ref["api_version"], + apiVersion=controller_ref["apiVersion"], namespace=namespace, **self._kwargs ) @@ -494,11 +494,7 @@ def evict_pod(name, namespace="default", grace_period=1, **kwargs): Returns: whether the eviction was successfully created or not Raises: CommandExecutionError in case of API error """ - kind_info = __utils__["metalk8s_kubernetes.get_kind_info"]( - {"kind": "PodEviction", "apiVersion": "v1"} - ) - - delete_options = V1DeleteOptions() + delete_options = kubernetes.client.V1DeleteOptions() if grace_period >= 0: delete_options.grace_period = grace_period @@ -506,13 +502,22 @@ def evict_pod(name, namespace="default", grace_period=1, **kwargs): kubeconfig, context = __salt__["metalk8s_kubernetes.get_kubeconfig"](**kwargs) - client = kind_info.client - client.configure(config_file=kubeconfig, context=context) + client = kubernetes.dynamic.DynamicClient( + kubernetes.config.new_client_from_config(kubeconfig, context) + ) + + # DynamicClient does not handle Pod eviction, so compute the path manually + path = ( + client.resources.get(api_version="v1", kind="Pod").path( + name=name, namespace=namespace + ) + + "/eviction" + ) try: - client.create( - name=name, - namespace=namespace, + client.request( + "post", + path, body=V1beta1Eviction(delete_options=delete_options, metadata=object_meta), ) except (ApiException, HTTPError) as exc: diff --git a/salt/_modules/metalk8s_kubernetes.py b/salt/_modules/metalk8s_kubernetes.py index d02b0e4bb6..534651ab11 100644 --- a/salt/_modules/metalk8s_kubernetes.py +++ b/salt/_modules/metalk8s_kubernetes.py @@ -21,9 +21,11 @@ try: import kubernetes.client as k8s_client + import kubernetes + from kubernetes.dynamic.exceptions import ResourceNotFoundError from kubernetes.client.rest import ApiException except ImportError: - MISSING_DEPS.append("kubernetes.client") + MISSING_DEPS.append("kubernetes") try: from urllib3.exceptions import HTTPError @@ -40,34 +42,19 @@ def __virtual__(): error_msg = "Missing dependencies: {}".format(", ".join(MISSING_DEPS)) return False, error_msg - if "metalk8s_kubernetes.get_kind_info" not in __utils__: - return False, "Missing `metalk8s_kubernetes` utils module" - return __virtualname__ -def _extract_obj_and_kind_info(manifest, force_custom_object=False): - try: - kind_info = __utils__["metalk8s_kubernetes.get_kind_info"](manifest) - obj = __utils__["metalk8s_kubernetes.convert_manifest_to_object"]( - manifest, force_custom_object=force_custom_object - ) - except ValueError as exc: - raise CommandExecutionError("Invalid manifest") from exc - - return obj, kind_info - - def _handle_error(exception, action): """Wrap an exception raised during a call to the K8s API. - Note that 'retrieve' and 'delete' will not re-raise if the error is just + Note that 'get' and 'delete' will not re-raise if the error is just a "404 NOT FOUND", and instead return `None`. """ base_msg = "Failed to {} object".format(action) if ( - action in ["delete", "retrieve"] + action in ["delete", "get"] and isinstance(exception, ApiException) and exception.status == 404 ): @@ -80,10 +67,10 @@ def _object_manipulation_function(action): """Generate an execution function based on a CRUD method to use.""" assert action in ( "create", - "retrieve", + "get", "replace", "delete", - "update", + "patch", ), 'Method "{}" is not supported'.format(action) def method( @@ -101,11 +88,11 @@ def method( ): if manifest is None: if ( - action in ["retrieve", "delete", "update"] + action in ["get", "delete", "patch"] and name and kind and apiVersion - and (action != "update" or patch) + and (action != "patch" or patch) ): # Build a simple manifest using kwargs information as # get/delete do not need a full body @@ -132,11 +119,11 @@ def method( if not manifest: needed_params = ['"manifest"', '"name" (path to a file)'] - if action in ["retrieve", "delete", "update"]: + if action in ["get", "delete", "patch"]: needed_params.append( " and ".join( ['"name"', '"kind"', '"apiVersion"'] - + (['"patch"'] if action == "update" else []) + + (['"patch"'] if action == "patch" else []) ) ) raise CommandExecutionError( @@ -159,26 +146,35 @@ def method( log.debug("%sing object with manifest: %s", action[:-1].capitalize(), manifest) - obj, kind_info = _extract_obj_and_kind_info( - manifest, - # NOTE: For "retrieve", "delete" and "update" we don't need a full - # kubernetes object we only need "kind", "apiVersion", "name" - # (, "namespace")(and a patch for "update"), so we can not - # create a Python kubernetes objects as some required field may - # not be in the manifest - force_custom_object=action in ["retrieve", "delete", "update"], + kubeconfig, context = __salt__["metalk8s_kubernetes.get_kubeconfig"](**kwargs) + + client = kubernetes.dynamic.DynamicClient( + kubernetes.config.new_client_from_config(kubeconfig, context) ) + try: + api = client.resources.get( + api_version=manifest["apiVersion"], kind=manifest["kind"] + ) + except ResourceNotFoundError as exc: + raise CommandExecutionError( + "Kind '{}' from apiVersion '{}' is unknown".format( + manifest["kind"], manifest["apiVersion"] + ) + ) from exc + + method_func = getattr(api, action) + call_kwargs = {} if action != "create": - call_kwargs["name"] = obj.metadata.name - if kind_info.scope == "namespaced": - call_kwargs["namespace"] = obj.metadata.namespace + call_kwargs["name"] = manifest["metadata"]["name"] + if api.namespaced: + call_kwargs["namespace"] = manifest["metadata"].get("namespace") if action == "delete": call_kwargs["body"] = k8s_client.V1DeleteOptions( propagation_policy="Foreground" ) - elif action == "update": + elif action == "patch": if patch: call_kwargs["body"] = patch else: @@ -189,39 +185,30 @@ def method( call_kwargs["body"]["metadata"].pop("name") # Namespace may be empty so add a default to not failing call_kwargs["body"]["metadata"].pop("namespace", None) - elif action != "retrieve": - call_kwargs["body"] = obj + elif action != "get": + call_kwargs["body"] = manifest if action == "replace" and old_object: # Some attributes have to be preserved # otherwise exceptions will be thrown - if "resource_version" in old_object["metadata"]: - call_kwargs["body"].metadata.resource_version = old_object["metadata"][ - "resource_version" - ] if "resourceVersion" in old_object["metadata"]: - call_kwargs["body"].metadata.resourceVersion = old_object["metadata"][ - "resourceVersion" - ] - # Keep `cluster_ip` and `health_check_node_port` if not present in the body - if obj.api_version == "v1" and obj.kind == "Service": - if not call_kwargs["body"].spec.cluster_ip: - call_kwargs["body"].spec.cluster_ip = old_object["spec"][ - "cluster_ip" - ] - if ( - obj.spec.type == "LoadBalancer" - and not call_kwargs["body"].spec.health_check_node_port + call_kwargs["body"]["metadata"]["resourceVersion"] = old_object[ + "metadata" + ]["resourceVersion"] + # Keep `clusterIP` and `healthCheckNodePort` if not present in the body + if api.api_version == "v1" and api.kind == "Service": + if not call_kwargs["body"]["spec"].get("clusterIP"): + call_kwargs["body"]["spec"]["clusterIP"] = old_object["spec"].get( + "clusterIP" + ) + if call_kwargs["body"]["spec"].get( + "type" + ) == "LoadBalancer" and not call_kwargs["body"]["spec"].get( + "healthCheckNodePort" ): - call_kwargs["body"].spec.health_check_node_port = old_object[ + call_kwargs["body"]["spec"]["healthCheckNodePort"] = old_object[ "spec" - ]["health_check_node_port"] - - kubeconfig, context = __salt__["metalk8s_kubernetes.get_kubeconfig"](**kwargs) - - client = kind_info.client - client.configure(config_file=kubeconfig, context=context) - method_func = getattr(client, action) + ].get("healthCheckNodePort") log.debug("Running '%s' with: %s", action, call_kwargs) @@ -231,7 +218,6 @@ def method( return _handle_error(exc, action) # NOTE: result is always either a standard `kubernetes.client` model, - # or a `CustomObject` as defined in the __utils__ module. return result.to_dict() base_doc = """ @@ -270,7 +256,7 @@ def method( base_doc=base_doc, cmd=action, ) - elif action in ["retrieve", "delete"]: + elif action in ["get", "delete"]: method.__doc__ = """{base_doc} Ability to {verb} an object using object description 'name', 'kind' and 'apiVersion'. @@ -289,12 +275,12 @@ def method( ), base_doc=base_doc, verb=action, - cmd="get" if action == "retrieve" else action, + cmd=action, ) - elif action == "update": + elif action == "patch": method.__doc__ = """{base_doc} Ability to {verb} an object using object description and a patch 'name', - 'kind', 'apiVersion' and patch'. + 'kind', 'apiVersion' and 'patch'. CLI Examples: @@ -316,7 +302,7 @@ def method( patch2=str([{"op": "remove", "path": "/metadata/labels/test.12"}]), base_doc=base_doc, verb=action, - cmd=action, + cmd="update", ) return method @@ -325,8 +311,8 @@ def method( create_object = _object_manipulation_function("create") delete_object = _object_manipulation_function("delete") replace_object = _object_manipulation_function("replace") -get_object = _object_manipulation_function("retrieve") -update_object = _object_manipulation_function("update") +get_object = _object_manipulation_function("get") +update_object = _object_manipulation_function("patch") # Check if a specific object exists @@ -364,42 +350,38 @@ def list_objects( salt-call metalk8s_kubernetes.list_objects kind="Pod" apiVersion="v1" namespace="kube-system" salt-call metalk8s_kubernetes.list_objects kind="Pod" apiVersion="v1" all_namespaces=True field_selector="spec.nodeName=bootstrap" """ + kubeconfig, context = __salt__["metalk8s_kubernetes.get_kubeconfig"](**kwargs) + + client = kubernetes.dynamic.DynamicClient( + kubernetes.config.new_client_from_config(kubeconfig, context) + ) try: - kind_info = __utils__["metalk8s_kubernetes.get_kind_info"]( - { - "kind": kind, - "apiVersion": apiVersion, - } - ) - except ValueError as exc: + api = client.resources.get(api_version=apiVersion, kind=kind) + except ResourceNotFoundError as exc: raise CommandExecutionError( - 'Unsupported resource "{}/{}"'.format(apiVersion, kind) + "Kind '{}' from apiVersion '{}' is unknown".format(kind, apiVersion) ) from exc + api = client.resources.get(api_version=apiVersion, kind=kind) call_kwargs = {} if all_namespaces: call_kwargs["all_namespaces"] = True - elif kind_info.scope == "namespaced": + elif api.namespaced: call_kwargs["namespace"] = namespace if field_selector: call_kwargs["field_selector"] = field_selector if label_selector: call_kwargs["label_selector"] = label_selector - kubeconfig, context = __salt__["metalk8s_kubernetes.get_kubeconfig"](**kwargs) - - client = kind_info.client - client.configure(config_file=kubeconfig, context=context) - try: - result = client.list(**call_kwargs) + result = api.get(**call_kwargs) except (ApiException, HTTPError) as exc: base_msg = 'Failed to list resources "{}/{}"'.format(apiVersion, kind) if "namespace" in call_kwargs: base_msg += ' in namespace "{}"'.format(namespace) raise CommandExecutionError(base_msg) from exc - return [obj.to_dict() for obj in result.items] + return result.to_dict()["items"] def get_object_digest(path=None, checksum="sha256", *args, **kwargs): diff --git a/salt/_modules/metalk8s_kubernetes_utils.py b/salt/_modules/metalk8s_kubernetes_utils.py index ba667c2548..e11e63c4b3 100644 --- a/salt/_modules/metalk8s_kubernetes_utils.py +++ b/salt/_modules/metalk8s_kubernetes_utils.py @@ -12,15 +12,10 @@ MISSING_DEPS = [] try: - import kubernetes.client + import kubernetes from kubernetes.client.rest import ApiException except ImportError: - MISSING_DEPS.append("kubernetes.client") - -try: - import kubernetes.config -except ImportError: - MISSING_DEPS.append("kubernetes.config") + MISSING_DEPS.append("kubernetes") try: from urllib3.exceptions import HTTPError @@ -89,19 +84,14 @@ def get_version_info(**kwargs): """ kubeconfig, context = get_kubeconfig(**kwargs) - api_client = kubernetes.config.new_client_from_config( - config_file=kubeconfig, context=context - ) - - api_instance = kubernetes.client.VersionApi(api_client=api_client) - try: - version_info = api_instance.get_code() + client = kubernetes.dynamic.DynamicClient( + kubernetes.config.new_client_from_config(kubeconfig, context) + ) + return client.version["kubernetes"] except (ApiException, HTTPError) as exc: raise CommandExecutionError("Failed to get version info") from exc - return version_info.to_dict() - def ping(**kwargs): """Check connection with the API server. diff --git a/salt/_pillar/metalk8s_nodes.py b/salt/_pillar/metalk8s_nodes.py index 09955b8fcb..e9a6b5b90d 100644 --- a/salt/_pillar/metalk8s_nodes.py +++ b/salt/_pillar/metalk8s_nodes.py @@ -54,7 +54,7 @@ def get_cluster_version(kubeconfig=None): return __utils__["pillar_utils.errors_to_dict"]( "Unable to read namespace information {}".format(exc) ) - annotations = namespace["metadata"]["annotations"] + annotations = namespace["metadata"].get("annotations", {}) annotation_key = "metalk8s.scality.com/cluster-version" if not annotations or annotation_key not in annotations: return __utils__["pillar_utils.errors_to_dict"]( @@ -64,26 +64,12 @@ def get_cluster_version(kubeconfig=None): return annotations[annotation_key] -def iso_timestamp_converter(timestamp): - if timestamp is None: - return None - return timestamp.isoformat() - - def get_storage_classes(kubeconfig=None): storage_classes = {} storageclass_list = __salt__["metalk8s_kubernetes.list_objects"]( kind="StorageClass", apiVersion="storage.k8s.io/v1", kubeconfig=kubeconfig ) for storageclass in storageclass_list: - # Need to convert the datetime object in storageclass to ISO format in - # order to make them serializable. - storageclass["metadata"]["creation_timestamp"] = iso_timestamp_converter( - storageclass["metadata"]["creation_timestamp"] - ) - storageclass["metadata"]["deletion_timestamp"] = iso_timestamp_converter( - storageclass["metadata"]["deletion_timestamp"] - ) storage_classes[storageclass["metadata"]["name"]] = storageclass return storage_classes diff --git a/salt/_states/metalk8s_cordon.py b/salt/_states/metalk8s_cordon.py index 784944375f..bd2292029a 100644 --- a/salt/_states/metalk8s_cordon.py +++ b/salt/_states/metalk8s_cordon.py @@ -30,7 +30,7 @@ def _node_set_unschedulable(name, value, **kwargs): ret["result"] = True ret["changes"] = { "old": {"unschedulable": unschedulable}, - "new": {"unschedulable": res["spec"]["unschedulable"]}, + "new": {"unschedulable": res["spec"].get("unschedulable")}, } ret["comment"] = "Node {0} {1}ed".format(name, action) diff --git a/salt/_states/metalk8s_kubernetes.py b/salt/_states/metalk8s_kubernetes.py index 9037d826a1..254f0471e0 100644 --- a/salt/_states/metalk8s_kubernetes.py +++ b/salt/_states/metalk8s_kubernetes.py @@ -178,9 +178,7 @@ def object_updated(name, manifest=None, **kwargs): cmp_manifest = None if cmp_manifest: - new_obj = __utils__["metalk8s_kubernetes.camel_to_snake"](cmp_manifest) - - if not __utils__["dictdiffer.recursive_diff"](obj, new_obj).diffs: + if not __utils__["dictdiffer.recursive_diff"](obj, cmp_manifest).diffs: ret["comment"] = "The object is already good" return ret diff --git a/salt/_utils/kubernetes_utils.py b/salt/_utils/kubernetes_utils.py deleted file mode 100644 index bef203911a..0000000000 --- a/salt/_utils/kubernetes_utils.py +++ /dev/null @@ -1,898 +0,0 @@ -"""Utility methods for manipulation of Kubernetes objects in Python. -""" -import datetime -from functools import partial -import inspect -import keyword -import operator -from pprint import pformat -import re - -from salt.ext import six -from salt.utils.dictdiffer import recursive_diff - -try: - import kubernetes.config - import kubernetes.client as k8s_client - import kubernetes.client.api as k8s_apis - - # Workaround for https://github.com/kubernetes-client/python/issues/376 - def set_conditions(self, conditions): - if conditions is None: - conditions = [] - self._conditions = conditions - - setattr( - k8s_client.V1beta1CustomResourceDefinitionStatus, - "conditions", - property( - fget=k8s_client.V1beta1CustomResourceDefinitionStatus.conditions.fget, - fset=set_conditions, - ), - ) - # End of workaround -except ImportError: - HAS_LIBS = False -else: - HAS_LIBS = True - ALL_APIS = frozenset( - api for _, api in inspect.getmembers(k8s_apis, inspect.isclass) - ) - - -__virtualname__ = "metalk8s_kubernetes" - - -def __virtual__(): - if not HAS_LIBS: - return False, "Missing dependencies: kubernetes" - - return __virtualname__ - - -# Roughly equivalent to an Enum, for Python 2 -class ObjectScope(object): - NAMESPACE = "namespaced" - CLUSTER = "cluster" - - ALL_VALUES = (NAMESPACE, CLUSTER) - - def __init__(self, value): - if value not in self.ALL_VALUES: - raise ValueError("Value must be one of {}.".format(self.ALL_VALUES)) - self.value = value - - def __eq__(self, other): - if isinstance(other, ObjectScope): - return self.value == other.value - if isinstance(other, six.string_types): - return self.value == other - return NotImplemented - - def __repr__(self): - return "ObjectScope({})".format(self.value) - - -class ApiClient(object): - CRUD_METHODS = { - "create": "create", - "retrieve": "read", - "update": "patch", - "delete": "delete", - "replace": "replace", - "list": "list", - } - - def __init__(self, api_cls, name, method_names=None, all_namespaces_name=None): - if api_cls not in ALL_APIS: - raise ValueError("`api_cls` must be an API from `kubernetes.client.api`") - methods = self.CRUD_METHODS - if isinstance(method_names, six.string_types): - method_names = [method_names] - if isinstance(method_names, list): - methods = { - func_name: self.CRUD_METHODS[func_name] for func_name in method_names - } - if isinstance(method_names, dict): - methods = method_names - - self._api_cls = api_cls - self._name = name - self._all_namespaces_name = all_namespaces_name - self._api = None - self._client = None - - # Attach the API CRUD methods at construction, so we can fail at - # import-time instead of runtime - self._api_methods = { - method: getattr(api_cls, self._method_name(verb)) - for method, verb in methods.items() - } - - if self._all_namespaces_name and "list" in methods: - self._api_methods["list_all_namespaces"] = getattr( - api_cls, self._all_namespaces_name - ) - - api_cls = property(operator.attrgetter("_api_cls")) - name = property(operator.attrgetter("_name")) - - create = property(lambda self: self._method("create")) - retrieve = property(lambda self: self._method("retrieve")) - update = property(lambda self: self._method("update")) - delete = property(lambda self: self._method("delete")) - replace = property(lambda self: self._method("replace")) - - @property - def list(self): - def _list(all_namespaces=False, *args, **kwargs): - if all_namespaces: - assert ( - self._all_namespaces_name - ), 'Cannot use "all_namespaces" for this client' - return self._method("list_all_namespaces")(*args, **kwargs) - return self._method("list")(*args, **kwargs) - - return _list - - def configure(self, config_file=None, context=None, persist_config=False): - self._client = kubernetes.config.new_client_from_config( - config_file, context, persist_config - ) - - @property - def api(self): - if self._api is None: - assert ( - self._client is not None - ), "Cannot use API without configuring the client first" - self._api = self.api_cls(api_client=self._client) - return self._api - - def _method_name(self, verb): - return "{}_{}".format(verb, self.name) - - def _method(self, verb): - # Inject the API instance as the first argument, since those methods - # are not classmethods, yet stored unbound - return partial(self._api_methods[verb], self.api) - - -class KindInfo(object): - """Wrapper for holding attributes related to an object kind. - - In particular, it holds a model class, and a wrapper `ApiClient` to use the - relevant API through simple CRUD methods. - CRUD methods can be filtered using `method_names` argument usefull for - object that may not have all CRUD methods. - """ - - def __init__(self, model, api_cls, name, method_names=None): - self._model = model - if name.startswith("namespaced_"): - self._scope = ObjectScope("namespaced") - all_ns_method = "list_{}_for_all_namespaces".format( - name[len("namespaced_") :] - ) - else: - self._scope = ObjectScope("cluster") - all_ns_method = None - self._client = ApiClient( - api_cls, name, method_names=method_names, all_namespaces_name=all_ns_method - ) - - model = property(operator.attrgetter("_model")) - client = property(operator.attrgetter("_client")) - scope = property(operator.attrgetter("_scope")) - - -if HAS_LIBS: - KNOWN_STD_KINDS = { - # /api/v1/ {{{ - ("v1", "ConfigMap"): KindInfo( - model=k8s_client.V1ConfigMap, - api_cls=k8s_client.CoreV1Api, - name="namespaced_config_map", - ), - ("v1", "Endpoints"): KindInfo( - model=k8s_client.V1Endpoints, - api_cls=k8s_client.CoreV1Api, - name="namespaced_endpoints", - ), - ("v1", "Namespace"): KindInfo( - model=k8s_client.V1Namespace, - api_cls=k8s_client.CoreV1Api, - name="namespace", - ), - ("v1", "Node"): KindInfo( - model=k8s_client.V1Node, - api_cls=k8s_client.CoreV1Api, - name="node", - ), - ("v1", "Pod"): KindInfo( - model=k8s_client.V1Pod, api_cls=k8s_client.CoreV1Api, name="namespaced_pod" - ), - ("v1", "PodEviction"): KindInfo( - model=k8s_client.V1beta1Eviction, - api_cls=k8s_client.CoreV1Api, - name="namespaced_pod_eviction", - method_names="create", - ), - ("v1", "Secret"): KindInfo( - model=k8s_client.V1Secret, - api_cls=k8s_client.CoreV1Api, - name="namespaced_secret", - ), - ("v1", "Service"): KindInfo( - model=k8s_client.V1Service, - api_cls=k8s_client.CoreV1Api, - name="namespaced_service", - ), - ("v1", "ServiceAccount"): KindInfo( - model=k8s_client.V1ServiceAccount, - api_cls=k8s_client.CoreV1Api, - name="namespaced_service_account", - ), - ("v1", "ReplicationController"): KindInfo( - model=k8s_client.V1ReplicationController, - api_cls=k8s_client.CoreV1Api, - name="namespaced_replication_controller", - ), - # }}} - # /apis/apps/v1/ {{{ - ("apps/v1", "DaemonSet"): KindInfo( - model=k8s_client.V1DaemonSet, - api_cls=k8s_client.AppsV1Api, - name="namespaced_daemon_set", - ), - ("apps/v1", "Deployment"): KindInfo( - model=k8s_client.V1Deployment, - api_cls=k8s_client.AppsV1Api, - name="namespaced_deployment", - ), - ("apps/v1", "ReplicaSet"): KindInfo( - model=k8s_client.V1ReplicaSet, - api_cls=k8s_client.AppsV1Api, - name="namespaced_replica_set", - ), - ("apps/v1", "StatefulSet"): KindInfo( - model=k8s_client.V1StatefulSet, - api_cls=k8s_client.AppsV1Api, - name="namespaced_stateful_set", - ), - # }}} - # /apis/extensions/v1beta1/ {{{ - ("extensions/v1beta1", "Ingress"): KindInfo( - model=k8s_client.ExtensionsV1beta1Ingress, - api_cls=k8s_client.ExtensionsV1beta1Api, - name="namespaced_ingress", - ), - # }}} - # /apis/apiextensions.k8s.io/v1/ {{{ - ("apiextensions.k8s.io/v1", "CustomResourceDefinition"): KindInfo( - model=k8s_client.V1CustomResourceDefinition, - api_cls=k8s_client.ApiextensionsV1Api, - name="custom_resource_definition", - ), - # }}} - # /apis/apiextensions.k8s.io/v1beta1/ {{{ - ("apiextensions.k8s.io/v1beta1", "CustomResourceDefinition"): KindInfo( - model=k8s_client.V1beta1CustomResourceDefinition, - api_cls=k8s_client.ApiextensionsV1beta1Api, - name="custom_resource_definition", - ), - # }}} - # /apis/apiregistration.k8s.io/v1/ {{{ - ("apiregistration.k8s.io/v1", "APIService"): KindInfo( - model=k8s_client.V1APIService, - api_cls=k8s_client.ApiregistrationV1Api, - name="api_service", - ), - # }}} - # /apis/apiregistration.k8s.io/v1beta1/ {{{ - ("apiregistration.k8s.io/v1beta1", "APIService"): KindInfo( - model=k8s_client.V1beta1APIService, - api_cls=k8s_client.ApiregistrationV1beta1Api, - name="api_service", - ), - # }}} - # /apis/batch/v1beta1/ {{{ - ("batch/v1beta1", "CronJob"): KindInfo( - model=k8s_client.V1beta1CronJob, - api_cls=k8s_client.BatchV1beta1Api, - name="namespaced_cron_job", - ), - # }}} - # /apis/networking.k8s.io/v1beta1/ {{{ - ("networking.k8s.io/v1beta1", "Ingress"): KindInfo( - model=k8s_client.NetworkingV1beta1Ingress, - api_cls=k8s_client.NetworkingV1beta1Api, - name="namespaced_ingress", - ), - # }}} - # /apis/policy/v1beta1/ {{{ - ("policy/v1beta1", "PodDisruptionBudget"): KindInfo( - model=k8s_client.V1beta1PodDisruptionBudget, - api_cls=k8s_client.PolicyV1beta1Api, - name="namespaced_pod_disruption_budget", - ), - ("policy/v1beta1", "PodSecurityPolicy"): KindInfo( - model=k8s_client.PolicyV1beta1PodSecurityPolicy, - api_cls=k8s_client.PolicyV1beta1Api, - name="pod_security_policy", - ), - # }}} - # /apis/rbac.authorization.k8s.io/v1/ {{{ - ("rbac.authorization.k8s.io/v1", "ClusterRole"): KindInfo( - model=k8s_client.V1ClusterRole, - api_cls=k8s_client.RbacAuthorizationV1Api, - name="cluster_role", - ), - ("rbac.authorization.k8s.io/v1", "ClusterRoleBinding"): KindInfo( - model=k8s_client.V1ClusterRoleBinding, - api_cls=k8s_client.RbacAuthorizationV1Api, - name="cluster_role_binding", - ), - ("rbac.authorization.k8s.io/v1", "Role"): KindInfo( - model=k8s_client.V1Role, - api_cls=k8s_client.RbacAuthorizationV1Api, - name="namespaced_role", - ), - ("rbac.authorization.k8s.io/v1", "RoleBinding"): KindInfo( - model=k8s_client.V1RoleBinding, - api_cls=k8s_client.RbacAuthorizationV1Api, - name="namespaced_role_binding", - ), - # }}} - # /apis/rbac.authorization.k8s.io/v1beta1/ {{{ - ("rbac.authorization.k8s.io/v1beta1", "ClusterRole"): KindInfo( - model=k8s_client.V1beta1ClusterRole, - api_cls=k8s_client.RbacAuthorizationV1beta1Api, - name="cluster_role", - ), - ("rbac.authorization.k8s.io/v1beta1", "ClusterRoleBinding"): KindInfo( - model=k8s_client.V1beta1ClusterRoleBinding, - api_cls=k8s_client.RbacAuthorizationV1beta1Api, - name="cluster_role_binding", - ), - ("rbac.authorization.k8s.io/v1beta1", "Role"): KindInfo( - model=k8s_client.V1beta1Role, - api_cls=k8s_client.RbacAuthorizationV1beta1Api, - name="namespaced_role", - ), - ("rbac.authorization.k8s.io/v1beta1", "RoleBinding"): KindInfo( - model=k8s_client.V1beta1RoleBinding, - api_cls=k8s_client.RbacAuthorizationV1beta1Api, - name="namespaced_role_binding", - ), - # }}} - # /apis/storage.k8s.io/v1/ {{{ - ("storage.k8s.io/v1", "StorageClass"): KindInfo( - model=k8s_client.V1StorageClass, - api_cls=k8s_client.StorageV1Api, - name="storage_class", - ), - # }}} - } - - -# CustomResources cannot rely on statically declared models, which is why their -# management is treated differently from "standard" objects. - - -class CustomApiClient(ApiClient): - CRUD_METHODS = dict(ApiClient.CRUD_METHODS, retrieve="get") - - def __init__(self, group, version, kind, plural, scope): - self._group = group - self._version = version - self._scope = ObjectScope(scope) - self._kind = kind - self._plural = plural - - super(CustomApiClient, self).__init__( - api_cls=k8s_apis.CustomObjectsApi, - name="{}_custom_object".format(self.scope.value), - ) - - group = property(operator.attrgetter("_group")) - version = property(operator.attrgetter("_version")) - scope = property(operator.attrgetter("_scope")) - kind = property(operator.attrgetter("_kind")) - plural = property(operator.attrgetter("_plural")) - - def _method(self, verb): - """Return a CRUD method for this CustomApiClient. - - This is constructed as a partial application of the appropriate - method from the `CustomObjectsApi`, and casts the resulting dict as - a `CustomObject`. - """ - base_method = super(CustomApiClient, self)._method(verb) - - def method(*args, **kwargs): - kwargs.update( - { - "group": self.group, - "version": self.version, - "plural": self.plural, - } - ) - - # Convert body to_dict if it's a CustomObject as - # `python-kubernetes` want a dict or a specific objects with - # some attributes like `openapi_types`, `attributes_map`, ... - if isinstance(kwargs.get("body"), CustomObject): - kwargs["body"] = kwargs["body"].to_dict() - - result = base_method(*args, **kwargs) - - # TODO: do we have a result for `delete` methods? - return CustomObject(result) - - method.__doc__ = "{verb} a {kind} {scope} object.".format( - verb=verb.capitalize(), - kind="{s.group}/{s.version}/{s.kind}".format(s=self), - scope=self.scope.value, - ) - - return method - - def _method_name(self, verb): - # Override super(_method_name) for `delete` as two different functions - # exist to delete a custom object, one that take a `name` argument - # and another one that does not - # In our case we want to use the one with `name` - # See: https://github.com/scality/metalk8s/issues/2621 - # NOTE: This may need to be removed once we will change the - # python-kubernetes version installed in salt-master - if verb == "delete": - return "{}_{}_0".format(verb, self.name) - return super(CustomApiClient, self)._method_name(verb) - - -class CRKindInfo(object): - """Equivalent of `KindInfo` for custom objects. - - Note that CRUD methods are partial applications of the `kubernetes.client` - methods used to manipulate custom objects, using details from the - statically provided information in `KNOWN_CUSTOM_KINDS`. - """ - - def __init__(self, api_version, kind, scope, plural): - group, _, version = api_version.rpartition("/") - if not (group and version): - raise ValueError( - "Malformed 'apiVersion': {} " - "(expected format '/')".format(api_version) - ) - - self._api_version = api_version - self._scope = scope - self._client = CustomApiClient( - group=group, version=version, kind=kind, plural=plural, scope=scope - ) - self._kind = kind - - api_version = property(operator.attrgetter("_api_version")) - kind = property(operator.attrgetter("_kind")) - scope = property(operator.attrgetter("_scope")) - client = property(operator.attrgetter("_client")) - - @property - def key(self): - # Used for indexing in `KNOWN_CUSTOM_KINDS` - return (self.api_version, self.kind) - - -if HAS_LIBS: - _CUSTOM_KINDS = [ - CRKindInfo( - "monitoring.coreos.com/v1", - "Alertmanager", - scope="namespaced", - plural="alertmanagers", - ), - CRKindInfo( - "monitoring.coreos.com/v1", - "Prometheus", - scope="namespaced", - plural="prometheuses", - ), - CRKindInfo( - "monitoring.coreos.com/v1", - "PrometheusRule", - scope="namespaced", - plural="prometheusrules", - ), - CRKindInfo( - "monitoring.coreos.com/v1", - "ServiceMonitor", - scope="namespaced", - plural="servicemonitors", - ), - CRKindInfo( - "storage.metalk8s.scality.com/v1alpha1", - "Volume", - scope="cluster", - plural="volumes", - ), - ] - - KNOWN_CUSTOM_KINDS = {kind.key: kind for kind in _CUSTOM_KINDS} - - -class _DictWrapper(object): - """Wrapper for dynamic attribute access support over a simple dict. - - Implemented by storing the dicts and overloading the `__getattribute__` - to fallback on the internal `_fields` for retrieving an attribute and - returning a `_DictWrapper` also overloading the `__setattr__` to being - able to change a value from the origin dict. - - Used by `CustomObject` to emulate the other models for `kubernetes.client`. - - >>> example = _DictWrapper({ - ... 'a': {'b': 'value'}, - ... 'c': [{'subkey': 'subvalue'}] - ... }) - >>> example.a.b - 'value' - >>> example.c[0].subkey - 'subvalue' - >>> example.unknown - Traceback (most recent call last): - ... - AttributeError: Custom object has no attribute 'unknown' - """ - - def __init__(self, fields): - self._fields = fields - - @classmethod - def from_value(cls, value): - if isinstance(value, dict): - return cls(value) - if isinstance(value, list): - return [cls.from_value(val) for val in value] - return value - - def to_dict(self): - return self._fields - - def __repr__(self): - return repr(self._fields) - - def __getattribute__(self, name): - try: - return super(_DictWrapper, self).__getattribute__(name) - except AttributeError: - if name in self._fields: - return self.from_value(self._fields[name]) - for key in self._fields: - if _convert_attribute_name(key) == name: - return self.from_value(self._fields[key]) - raise AttributeError( # pylint: disable=raise-missing-from - "Custom object has no attribute '{}'".format(name) - ) - - def __setattr__(self, name, value): - # First check for class values then retrieve from dict - if name in ["_fields"]: - super(_DictWrapper, self).__setattr__(name, value) - else: - self._fields[name] = value - - -class CustomObject(object): - """Helper class to generate a "model" interface for CustomResources. - - The source manifest is converted to use snake case for all its keys and - sub-keys, as is done in the `kubernetes.client`. - - This fake "model" also uses a `_DictWrapper` internally to provide dynamic - attribute access on the instance's manifest. - """ - - def __init__(self, manifest): - self._attr_dict = _DictWrapper(manifest) - - def to_dict(self): - return self._attr_dict.to_dict() - - def to_str(self): - return pformat(self.to_dict()) - - def __repr__(self): - return self.to_str() - - def __eq__(self, other): - if not isinstance(other, CustomObject): - return False - - return self.to_dict() == other.to_dict() - - def __getattribute__(self, name): - try: - return super(CustomObject, self).__getattribute__(name) - except AttributeError: - return getattr(self._attr_dict, name) - - def __setattr__(self, name, value): - # First check for class values then retrieve from dict - if name in ["_attr_dict"]: - super(CustomObject, self).__setattr__(name, value) - else: - setattr(self._attr_dict, name, value) - - -def get_kind_info(manifest): - try: - api_version = manifest["apiVersion"] - kind = manifest["kind"] - except KeyError: - raise ValueError( # pylint: disable=raise-missing-from - "Make sure to provide a valid Kubernetes manifest, including" - " `kind` and `apiVersion` fields." - ) - - # Check for custom Kinds first and then standard Kinds - kind_info = KNOWN_CUSTOM_KINDS.get( - (api_version, kind), KNOWN_STD_KINDS.get((api_version, kind)) - ) - - if kind_info is None: - raise ValueError( - "Unknown object type provided: {}/{}. Make sure it is" - " registered properly.".format(api_version, kind) - ) - - return kind_info - - -def convert_manifest_to_object(manifest, force_custom_object=False): - """Convert a YAML representation of a K8s object to its Python model. - - This method can only convert known object kinds, as declared in the - constants `KNOWN_STD_KINDS` or `KNOWN_CUSTOM_KINDS` in this module. - - It will also create the Python objects from sub-dicts when necessary before - building the final result. - - In some case we need to force the use of `CustomObject` for example when - we want an object but we don't have all the required field so we can not - use the real Python kubernetes object. (e.g.: delete, get, update) - """ - kind_info = get_kind_info(manifest) - - if force_custom_object or isinstance(kind_info, CRKindInfo): - return CustomObject(manifest) - - return _build_standard_object(kind_info.model, manifest) - - -def _build_standard_object(model, manifest): - """Construct an instance of `model` based on its `manifest`. - - This method assumes `model` to be a member of `kubernetes.client.models`, - so that it can use its `attribute_map` and `openapi_types` attributes. - """ - # `model.attribute_map` contain all attribute correspondance between - # snake case and YAML style (camel case) so we need to reverse it - # e.g.: { - # 'status': 'status', 'kind': 'kind', 'spec': 'spec', - # 'api_version': 'apiVersion', 'metadata': 'metadata' - # } - reverse_attr_map = {value: key for key, value in model.attribute_map.items()} - - kwargs = {} - for src_key, src_value in manifest.items(): - key = reverse_attr_map.get(src_key, src_key) - type_str = model.openapi_types.get(key) - - if type_str is None: - raise ValueError( - 'Unsupported attribute {} for "{}" object.'.format( - src_key, model.__name__ - ) - ) - - try: - value = _cast_value(src_value, type_str) - except TypeError as exc: - raise ValueError( - 'Invalid value for attribute {} of a "{}" object'.format( - src_key, model.__name__ - ) - ) from exc - - kwargs[key] = value - - return model(**kwargs) - - -DICT_PATTERN = re.compile(r"^dict\(str,\s?(?P\S+)\)$") -LIST_PATTERN = re.compile(r"^list\[(?P\S+)\]$") - - -def _cast_value(value, type_string): - """Attempt to cast a value given a type declaration as a string. - - Used exclusively by `_build_standard_object`, relying on the models - `openapi_types` declarations for converting manifests into Python objects. - """ - # Special case for None used for exemple when patching to remove key - if value is None: - return value - - if type_string == "str": - if not isinstance(value, six.string_types): - raise _type_error(value, expected="a string") - return value - - if type_string == "bool": - if not isinstance(value, bool): - raise _type_error(value, expected="a boolean") - return value - - if type_string == "int": - if not isinstance(value, six.integer_types): - raise _type_error(value, expected="an integer") - return value - - if type_string == "float": - if not isinstance(value, six.integer_types + (float,)): - raise _type_error(value, expected="a float") - return float(value) - - if type_string == "object": - # NOTE: this corresponds to fields accepting different types, such as - # either string or integer (e.g. for ports or thresholds). As such, we - # don't attempt validation. Note however that some cases may require - # casting into specific objects, which we don't handle yet. - return value - - if type_string == "datetime": - # YAML only supports dates as strings, though we don't know in advance - # what format would be used in source manifests (most likely, there - # wouldn't be any date). We thus pick the Swagger `date-time` string - # format (see swagger.io/docs/specification/data-models/data-types/). - try: - return datetime.datetime.strptime(value, "%Y-%m-%dT%H:%M:%SZ") - except (TypeError, ValueError): - # pylint: disable=raise-missing-from - raise _type_error(value, expected="a date-time string") - - dict_match = DICT_PATTERN.match(type_string) - if dict_match is not None: - if not isinstance(value, dict): - raise _type_error(value, expected="a dictionary") - - if not all(isinstance(key, six.string_types) for key in value.keys()): - raise _type_error(value, expected="a dictionary with string keys only") - - value_type_str = dict_match.group("value_type") - return {key: _cast_value(val, value_type_str) for key, val in value.items()} - - list_match = LIST_PATTERN.match(type_string) - if list_match is not None: - if not isinstance(value, list): - raise _type_error(value, expected="a list") - - value_type_str = list_match.group("value_type") - return [_cast_value(val, value_type_str) for val in value] - - try: - model = getattr(k8s_client.models, type_string) - except AttributeError: - # This should never happen, otherwise this function should get updated - raise ValueError( # pylint: disable=raise-missing-from - "Unknown type string provided: {}.".format(type_string) - ) - - if not isinstance(value, dict): - raise _type_error( - value, expected='a dict to cast as a "{}"'.format(model.__name__) - ) - - return _build_standard_object(model, value) - - -def _type_error(value, expected): - return TypeError( - "Expected {}, received: {} (type: {})".format(expected, value, type(value)) - ) - - -def validate_manifest(manifest): - """Ensure a Kubernetes object is strictly conformant to its OpenAPI schema. - - This method relies on the K8s client models, which are generated from said - schema. The approach is to cast the original `manifest` dict into such - models, and then obtain the dict representation from the model back again, - ensuring that nothing is lost from the original manifest (the conversion - also checks that no "unsupported" field is provided). - - For CustomResources, this only checks that the apiVersion and kind are - registered in this file. - """ - obj = convert_manifest_to_object(manifest) - result = obj.to_dict() - - # We use the same logic as `kubernetes.client` for converting attribute - # names. - desired = _cast_dict_keys(manifest, key_cast=_convert_attribute_name) - - # We only check fields provided in the source, since the conversion - # may add empty or default values for optional fields - dictdiff = recursive_diff(desired, result) - return not dictdiff.removed() and not dictdiff.changed() - - -def _cast_dict_keys(data, key_cast): - """Converts all dict keys in `data` using `key_cast`, recursively. - - Can be used on any type, as this method will apply itself on dict and list - members, otherwise behaving as no-op. - - >>> _cast_dict_keys({'key': 'value', 'other': {'a': 'b'}}, str.capitalize) - {'Key': 'value', 'Other': {'A': 'b'}} - >>> _cast_dict_keys(['1', '2', {'3': '4'}], int) - ['1', '2', {3: '4'}] - >>> _cast_dict_keys(({'a': 1}, {'b': 2}), str.capitalize) - ({'a': 1}, {'b': 2}) - >>> _cast_dict_keys(1, str.capitalize) - 1 - """ - if isinstance(data, dict): - return { - key_cast(key): _cast_dict_keys(value, key_cast) - for key, value in data.items() - } - - if isinstance(data, list): - return [_cast_dict_keys(value, key_cast) for value in data] - - return data - - -def _convert_attribute_name(key): - """Translation of attribute names from K8s YAML style to Python snake case. - - Supports all special values present in the `kubernetes.client.models`. - Note the last example, where the conversion yields an unexpected result. - This is actually what is done in the source code of `V1ServiceSpec`, and - the same behaviour exists in other models. - - Python keywords are also prefixed with an underscore. - - >>> _convert_attribute_name('kind') - 'kind' - >>> _convert_attribute_name('apiVersion') - 'api_version' - >>> _convert_attribute_name('JSONPath') - 'json_path' - >>> _convert_attribute_name('volumeID') - 'volume_id' - >>> _convert_attribute_name('continue') # Python keyword - '_continue' - >>> _convert_attribute_name('$ref') # Remove '$' prefix - 'ref' - >>> _convert_attribute_name('openAPIV3Schema') # Numbers count as caps - 'open_apiv3_schema' - >>> _convert_attribute_name('externalIPs') # Weird result... - 'external_i_ps' - """ - if keyword.iskeyword(key): - return "_{}".format(key) - if key.startswith("$"): - # Only two supported values, '$ref' and '$schema' - return key[1:] - for pattern in ["([a-z])([A-Z0-9])", "([A-Z0-9])([A-Z0-9][a-z])"]: - key = re.sub(pattern, r"\1_\2", key) - return key.lower() - - -def camel_to_snake(source): - """Translation of attribute names from K8s YAML style to Python snake case.""" - return _cast_dict_keys(source, _convert_attribute_name) diff --git a/salt/metalk8s/kubernetes/coredns/deployed.sls b/salt/metalk8s/kubernetes/coredns/deployed.sls index c0ee71619d..974921f241 100644 --- a/salt/metalk8s/kubernetes/coredns/deployed.sls +++ b/salt/metalk8s/kubernetes/coredns/deployed.sls @@ -60,7 +60,7 @@ Create coredns service: spec: selector: k8s-app: kube-dns - cluster_ip: {{ cluster_dns_ip }} + clusterIP: {{ cluster_dns_ip }} ports: - name: dns port: 53 @@ -115,7 +115,7 @@ Create coredns cluster role binding: kind: ClusterRoleBinding metadata: name: system:coredns - role_ref: + roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: system:coredns diff --git a/salt/metalk8s/kubernetes/kube-proxy/deployed.sls b/salt/metalk8s/kubernetes/kube-proxy/deployed.sls index 5f9f0dace7..d5be05b9cc 100644 --- a/salt/metalk8s/kubernetes/kube-proxy/deployed.sls +++ b/salt/metalk8s/kubernetes/kube-proxy/deployed.sls @@ -202,7 +202,7 @@ Deploy kube-proxy (RoleBinding): metadata: name: kube-proxy namespace: kube-system - role_ref: + roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: kube-proxy diff --git a/salt/tests/requirements.txt b/salt/tests/requirements.txt index de1d8c9b2d..6efc0abb16 100644 --- a/salt/tests/requirements.txt +++ b/salt/tests/requirements.txt @@ -4,21 +4,21 @@ # # tox -e pip-compile # -attrs==20.3.0 \ - --hash=sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6 \ - --hash=sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700 \ +attrs==21.2.0 \ + --hash=sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1 \ + --hash=sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb \ # via pytest -cachetools==4.2.1 \ - --hash=sha256:1d9d5f567be80f7c07d765e21b814326d78c61eb0c3a637dffc0e5d1796cb2e2 \ - --hash=sha256:f469e29e7aa4cff64d8de4aad95ce76de8ea1125a16c68e0d93f65c3c3dc92e9 \ +cachetools==4.2.2 \ + --hash=sha256:2cc0b89715337ab6dbba85b5b50effe2b0c74e035d83ee8ed637cf52f12ae001 \ + --hash=sha256:61b5ed1e22a0924aed1d23b478f37e8d52549ff8a961de2909c69bf950020cff \ # via google-auth -certifi==2020.12.5 \ - --hash=sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c \ - --hash=sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830 \ +certifi==2021.5.30 \ + --hash=sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee \ + --hash=sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8 \ # via kubernetes, requests -chardet==4.0.0 \ - --hash=sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa \ - --hash=sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5 \ +charset-normalizer==2.0.4 \ + --hash=sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b \ + --hash=sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3 \ # via requests coverage==5.5 \ --hash=sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c \ @@ -74,138 +74,145 @@ coverage==5.5 \ --hash=sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d \ --hash=sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6 \ # via pytest-cov -distro==1.5.0 \ - --hash=sha256:0e58756ae38fbd8fc3020d54badb8eae17c5b9dcbed388b17bb55b8a5928df92 \ - --hash=sha256:df74eed763e18d10d0da624258524ae80486432cd17392d9c3d96f5e83cd2799 \ +distro==1.6.0 \ + --hash=sha256:83f5e5a09f9c5f68f60173de572930effbcc0287bb84fdc4426cb4168c088424 \ + --hash=sha256:c8713330ab31a034623a9515663ed87696700b55f04556b97c39cd261aa70dc7 \ # via salt etcd3==0.12.0 \ --hash=sha256:89a704cb389bf0a010a1fa050ce19342d23bf6371ebda1c21cfe8ff3ed488726 \ # via -r salt/tests/requirements.in -google-auth==1.28.0 \ - --hash=sha256:9bd436d19ab047001a1340720d2b629eb96dd503258c524921ec2af3ee88a80e \ - --hash=sha256:dcaba3aa9d4e0e96fd945bf25a86b6f878fcb05770b67adbeb50a63ca4d28a5e \ +google-auth==2.0.1 \ + --hash=sha256:c012c8be7c442c8309ca8fa0876fef33f5fd977c467be1e1c1c2f721e8ebd73c \ + --hash=sha256:ea1af050b3e06eb73e4470f704d23007307bc0e87c13e015f6b90460f1407bd3 \ # via kubernetes -grpcio==1.36.1 \ - --hash=sha256:02030e1afd3247f2b159df9dff959ec79dd4047b1c4dd4eec9e3d1642efbd504 \ - --hash=sha256:0648a6d5d7ddcd9c8462d7d961660ee024dad6b88152ee3a521819e611830edf \ - --hash=sha256:09af8ceb91860086216edc6e5ea15f9beb2cf81687faa43b7c03216f5b73e244 \ - --hash=sha256:1030e74ddd0fa6e3bad7944f0c68cf1251b15bcd70641f0ad3858fdf2b8602a0 \ - --hash=sha256:1056b558acfd575d774644826df449e1402a03e456a3192fafb6b06d1069bf80 \ - --hash=sha256:20b7c4c5513e1135a2261e56830c0e710f205fee92019b92fe132d7f16a5cfd8 \ - --hash=sha256:216fbd2a488e74c3b96e240e4054c85c4c99102a439bc9f556936991643f43bc \ - --hash=sha256:24d4c2c5e540e666c52225953d6813afc8ccf9bf46db6a72edd4e8d606656248 \ - --hash=sha256:2abaa9f0d83bd0b26f6d0d1fc4b97d73bde3ceac36ab857f70d3cabcf31c5c79 \ - --hash=sha256:3a6295aa692806218e97bb687a71cd768450ed99e2acddc488f18d738edef463 \ - --hash=sha256:3c5204e05e18268dd6a1099ca6c106fd9d00bcae1e37d5a5186094c55044c941 \ - --hash=sha256:3e75643d21db7d68acd541d3fec66faaa8061d12b511e101b529ff12a276bb9b \ - --hash=sha256:45ea10dd133a43b10c0b4326834107ebccfee25dab59b312b78e018c2d72a1f0 \ - --hash=sha256:4c05ed54b2a00df01e633bebec819b512bf0c60f8f5b3b36dd344dc673b02fea \ - --hash=sha256:4dc7295dc9673f7af22c1e38c2a2c24ecbd6773a4c5ed5a46ed38ad4dcf2bf6c \ - --hash=sha256:52ec563da45d06319224ebbda53501d25594de64ee1b2786e119ba4a2f1ce40c \ - --hash=sha256:5378189fb897567f4929f75ab67a3e0da4f8967806246cb9cfa1fa06bfbdb0d5 \ - --hash=sha256:5e4920a8fb5d17b2c5ba980db0ac1c925bbee3e5d70e96da3ec4fb1c8600d68f \ - --hash=sha256:6b30682180053eebc87802c2f249d2f59b430e1a18e8808575dde0d22a968b2c \ - --hash=sha256:6f6f8a8b57e40347d0bf32c2135037dae31d63d3b19007b4c426a11b76deaf65 \ - --hash=sha256:76daa3c4d58fcf40f7969bdb4270335e96ee0382a050cadcd97d7332cd0251a3 \ - --hash=sha256:7863c2a140e829b1f4c6d67bf0bf15e5321ac4766d0a295e2682970d9dd4b091 \ - --hash=sha256:7cbeac9bbe6a4a7fce4a89c892c249135dd9f5f5219ede157174c34a456188f0 \ - --hash=sha256:7e32bc01dfaa7a51c547379644ea619a2161d6969affdac3bbd173478d26673d \ - --hash=sha256:85a6035ae75ce964f78f19cf913938596ccf068b149fcd79f4371268bcb9aa7c \ - --hash=sha256:8a89190de1985a54ef311650cf9687ffb81de038973fd32e452636ddae36b29f \ - --hash=sha256:9a18299827a70be0507f98a65393b1c7f6c004fe2ca995fe23ffac534dd187a7 \ - --hash=sha256:a602d6b30760bbbb2fe776caaa914a0d404636cafc3f2322718bf8002d7b1e55 \ - --hash=sha256:a66ea59b20f3669df0f0c6a3bd57b985e5b2d1dcf3e4c29819bb8dc232d0fd38 \ - --hash=sha256:b003e24339030ed356f59505d1065b89e1f443ef41ce71ca9069be944c0d2e6b \ - --hash=sha256:bab743cdac1d6d8326c65d1d091d0740b39966dfab06519f74a03b3d128b8454 \ - --hash=sha256:c18739fecb90760b183bfcb4da1cf2c6bf57e38f7baa2c131d5f67d9a4c8365d \ - --hash=sha256:cbd82c479338fc1c0e5c3db09752b61fe47d40c6e38e4be8657153712fa76674 \ - --hash=sha256:dee9971aef20fc09ed897420446c4d0926cd1d7630f343333288523ca5b44bb2 \ - --hash=sha256:e1b9e906aa6f7577016e86ed7f3a69cae7dab4e41356584dc7980f76ea65035f \ - --hash=sha256:e3a83c5db16f95daac1d96cf3c9018d765579b5a29bb336758d793028e729921 \ - --hash=sha256:eafafc7e040e36aa926edc731ab52c23465981888779ae64bfc8ad85888ed4f3 \ - --hash=sha256:ec753c022b39656f88409fbf9f2d3b28497e3f17aa678f884d78776b41ebe6bd \ - --hash=sha256:ed16bfeda02268e75e038c58599d52afc7097d749916c079b26bc27a66900f7d \ - --hash=sha256:f214076eb13da9e65c1aa9877b51fca03f51a82bd8691358e1a1edd9ff341330 \ - --hash=sha256:f22c11772eff25ba1ca536e760b8c34ba56f2a9d66b6842cb11770a8f61f879d \ - --hash=sha256:f241116d4bf1a8037ff87f16914b606390824e50902bdbfa2262e855fbf07fe5 \ - --hash=sha256:f3f70505207ee1cee65f60a799fd8e06e07861409aa0d55d834825a79b40c297 \ - --hash=sha256:f591597bb25eae0094ead5a965555e911453e5f35fdbdaa83be11ef107865697 \ - --hash=sha256:f6efa62ca1fe02cd34ec35f53446f04a15fe2c886a4e825f5679936a573d2cbf \ - --hash=sha256:f7740d9d9451f3663df11b241ac05cafc0efaa052d2fdca6640c4d3748eaf6e2 \ +grpcio==1.39.0 \ + --hash=sha256:02e8a8b41db8e13df53078355b439363e4ac46d0ac9a8a461a39e42829e2bcf8 \ + --hash=sha256:050901a5baa6c4ca445e1781ef4c32d864f965ccec70c46cd5ad92d15e282c6a \ + --hash=sha256:1ab44dde4e1b225d3fc873535ca6e642444433131dd2891a601b75fb46c87c11 \ + --hash=sha256:2068a2b896ac67103c4a5453d5435fafcbb1a2f41eaf25148d08780096935cee \ + --hash=sha256:20f57c5d09a36e0d0c8fe16ee1905f4307edb1d04f6034b56320f7fbc1a1071a \ + --hash=sha256:25731b2c20a4ed51bea7e3952d5e83d408a5df32d03c7553457b2e6eb8bcb16c \ + --hash=sha256:27e2c6213fc04e71a862bacccb51f3c8e722255933f01736ace183e92d860ee6 \ + --hash=sha256:2a4308875b9b986000513c6b04c2e7424f436a127f15547036c42d3cf8289374 \ + --hash=sha256:2a958ad794292e12d8738a06754ebaf71662e635a89098916c18715b27ca2b5b \ + --hash=sha256:2bc7eebb405aac2d7eecfaa881fd73b489f99c01470d7193b4431a6ce199b9c3 \ + --hash=sha256:366b6b35b3719c5570588e21d866460c5666ae74e3509c2a5a73ca79997abdaf \ + --hash=sha256:3c14e2087f809973d5ee8ca64f772a089ead0167286f3f21fdda8b6029b50abb \ + --hash=sha256:3c57fa7fec932767bc553bfb956759f45026890255bd232b2f797c3bc4dfeba2 \ + --hash=sha256:3cccf470fcaab65a1b0a826ff34bd7c0861eb82ed957a83c6647a983459e4ecd \ + --hash=sha256:4039645b8b5d19064766f3a6fa535f1db52a61c4d4de97a6a8945331a354d527 \ + --hash=sha256:4163e022f365406be2da78db890035463371effea172300ce5af8a768142baf3 \ + --hash=sha256:4258b778ce09ffa3b7c9a26971c216a34369e786771afbf4f98afe223f27d248 \ + --hash=sha256:43c57987e526d1b893b85099424387b22de6e3eee4ea7188443de8d657d11cc0 \ + --hash=sha256:43e0f5c49f985c94332794aa6c4f15f3a1ced336f0c6a6c8946c67b5ab111ae9 \ + --hash=sha256:46cfb0f2b757673bfd36ab4b0e3d61988cc1a0d47e0597e91462dcbef7528f35 \ + --hash=sha256:46d510a7af777d2f38ef4c1d25491add37cad24143012f3eebe72dc5c6d0fc4c \ + --hash=sha256:476fa94ba8efb09213baabd757f6f93e839794d8ae0eaa371347d6899e8f57a0 \ + --hash=sha256:4b3fcc1878a1a5b71e1ecdfe82c74f7cd9eadaa43e25be0d67676dcec0c9d39f \ + --hash=sha256:5091b4a5ee8454a8f0c8ac45946ca25d6142c3be4b1fba141f1d62a6e0b5c696 \ + --hash=sha256:5127f4ba1f52fda28037ae465cf4b0e5fabe89d5ac1d64d15b073b46b7db5e16 \ + --hash=sha256:52100d800390d58492ed1093de6faccd957de6fc29b1a0e5948c84f275d9228f \ + --hash=sha256:544e1c1a133b43893e03e828c8325be5b82e20d3b0ef0ee3942d32553052a1b5 \ + --hash=sha256:5628e7cc69079159f9465388ff21fde1e1a780139f76dd99d319119d45156f45 \ + --hash=sha256:57974361a459d6fe04c9ae0af1845974606612249f467bbd2062d963cb90f407 \ + --hash=sha256:691f5b3a75f072dfb7b093f46303f493b885b7a42f25a831868ffaa22ee85f9d \ + --hash=sha256:6ba6ad60009da2258cf15a72c51b7e0c2f58c8da517e97550881e488839e56c6 \ + --hash=sha256:6d51be522b573cec14798d4742efaa69d234bedabce122fec2d5489abb3724d4 \ + --hash=sha256:7b95b3329446408e2fe6db9b310d263303fa1a94649d08ec1e1cc12506743d26 \ + --hash=sha256:88dbef504b491b96e3238a6d5360b04508c34c62286080060c85fddd3caf7137 \ + --hash=sha256:8ed1e52ad507a54d20e6aaedf4b3edcab18cc12031eafe6de898f97513d8997b \ + --hash=sha256:a1fb9936b86b5efdea417fe159934bcad82a6f8c6ab7d1beec4bf3a78324d975 \ + --hash=sha256:a2733994b05ee5382da1d0378f6312b72c5cb202930c7fa20c794a24e96a1a34 \ + --hash=sha256:a6211150765cc2343e69879dfb856718b0f7477a4618b5f9a8f6c3ee84c047c0 \ + --hash=sha256:a659f7c634cacfcf14657687a9fa3265b0a1844b1c19d140f3b66aebfba1a66b \ + --hash=sha256:b0ff14dd872030e6b2fce8a6811642bd30d93833f794d3782c7e9eb2f01234cc \ + --hash=sha256:b236eb4b50d83754184b248b8b1041bb1546287fff7618c4b7001b9f257bb903 \ + --hash=sha256:c44958a24559f875d902d5c1acb0ae43faa5a84f6120d1d0d800acb52f96516e \ + --hash=sha256:c8fe430add656b92419f6cd0680b64fbe6347c831d89a7788324f5037dfb3359 \ + --hash=sha256:cd2e39a199bcbefb3f4b9fa6677c72b0e67332915550fed3bd7c28b454bf917d \ + --hash=sha256:cffdccc94e63710dd6ead01849443390632c8e0fec52dc26e4fddf9f28ac9280 \ + --hash=sha256:d5a105f5a595b89a0e394e5b147430b115333d07c55efb0c0eddc96055f0d951 \ + --hash=sha256:dc3a24022a90c1754e54315009da6f949b48862c1d06daa54f9a28f89a5efacb \ + --hash=sha256:de83a045005703e7b9e67b61c38bb72cd49f68d9d2780d2c675353a3a3f2816f \ + --hash=sha256:e98aca5cfe05ca29950b3d99006b9ddb54fde6451cd12cb2db1443ae3b9fa076 \ + --hash=sha256:ed845ba6253c4032d5a01b7fb9db8fe80299e9a437e695a698751b0b191174be \ + --hash=sha256:f2621c82fbbff1496993aa5fbf60e235583c7f970506e818671ad52000b6f310 \ # via etcd3 -idna==2.10 \ - --hash=sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6 \ - --hash=sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0 \ +idna==3.2 \ + --hash=sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a \ + --hash=sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3 \ # via requests -importlib-metadata==3.9.0 \ - --hash=sha256:036eae7ebbd41db176774c42e80f3288a1e41c7ebfc8ed099a94653973ebd00f \ - --hash=sha256:6fd684b4c6c7bb36d57e93d57fc244b5ffc08faa1c298bcda3dfbbbf19d7550a \ +importlib-metadata==4.6.4 \ + --hash=sha256:7b30a78db2922d78a6f47fb30683156a14f3c6aa5cc23f77cc8967e9ab2d002f \ + --hash=sha256:ed5157fef23a4bc4594615a0dd8eba94b2bb36bf2a343fa3d8bb2fa0a62a99d5 \ # via pluggy, pytest iniconfig==1.1.1 \ --hash=sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3 \ --hash=sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32 \ # via pytest -jinja2==2.11.3 \ - --hash=sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419 \ - --hash=sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6 \ +jinja2==3.0.1 \ + --hash=sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4 \ + --hash=sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4 \ # via salt -kubernetes==12.0.1 \ - --hash=sha256:23c85d8571df8f56e773f1a413bc081537536dc47e2b5e8dc2e6262edb2c57ca \ - --hash=sha256:ec52ea01d52e2ec3da255992f7e859f3a76f2bdb51cf65ba8cd71dfc309d8daa \ +kubernetes==18.20.0 \ + --hash=sha256:0c72d00e7883375bd39ae99758425f5e6cb86388417cf7cc84305c211b2192cf \ + --hash=sha256:ff31ec17437293e7d4e1459f1228c42d27c7724dfb56b4868aba7a901a5b72c9 \ # via -r salt/tests/requirements.in -markupsafe==1.1.1 \ - --hash=sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473 \ - --hash=sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161 \ - --hash=sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235 \ - --hash=sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5 \ - --hash=sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42 \ - --hash=sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f \ - --hash=sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39 \ - --hash=sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff \ - --hash=sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b \ - --hash=sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014 \ - --hash=sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f \ - --hash=sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1 \ - --hash=sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e \ - --hash=sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183 \ - --hash=sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66 \ - --hash=sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b \ - --hash=sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1 \ - --hash=sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15 \ - --hash=sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1 \ - --hash=sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85 \ - --hash=sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1 \ - --hash=sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e \ - --hash=sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b \ - --hash=sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905 \ - --hash=sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850 \ - --hash=sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0 \ - --hash=sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735 \ - --hash=sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d \ - --hash=sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb \ - --hash=sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e \ - --hash=sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d \ - --hash=sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c \ - --hash=sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1 \ - --hash=sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2 \ - --hash=sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21 \ - --hash=sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2 \ - --hash=sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5 \ - --hash=sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7 \ - --hash=sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b \ - --hash=sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8 \ - --hash=sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6 \ - --hash=sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193 \ - --hash=sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f \ - --hash=sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b \ - --hash=sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f \ - --hash=sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2 \ - --hash=sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5 \ - --hash=sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c \ - --hash=sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032 \ - --hash=sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7 \ - --hash=sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be \ - --hash=sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621 \ +markupsafe==2.0.1 \ + --hash=sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298 \ + --hash=sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64 \ + --hash=sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b \ + --hash=sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567 \ + --hash=sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff \ + --hash=sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724 \ + --hash=sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74 \ + --hash=sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646 \ + --hash=sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35 \ + --hash=sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6 \ + --hash=sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6 \ + --hash=sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad \ + --hash=sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26 \ + --hash=sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38 \ + --hash=sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac \ + --hash=sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7 \ + --hash=sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6 \ + --hash=sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75 \ + --hash=sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f \ + --hash=sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135 \ + --hash=sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8 \ + --hash=sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a \ + --hash=sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a \ + --hash=sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9 \ + --hash=sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864 \ + --hash=sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914 \ + --hash=sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18 \ + --hash=sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8 \ + --hash=sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2 \ + --hash=sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d \ + --hash=sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b \ + --hash=sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b \ + --hash=sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f \ + --hash=sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb \ + --hash=sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833 \ + --hash=sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28 \ + --hash=sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415 \ + --hash=sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902 \ + --hash=sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d \ + --hash=sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9 \ + --hash=sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d \ + --hash=sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145 \ + --hash=sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066 \ + --hash=sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c \ + --hash=sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1 \ + --hash=sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f \ + --hash=sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53 \ + --hash=sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134 \ + --hash=sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85 \ + --hash=sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5 \ + --hash=sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94 \ + --hash=sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509 \ + --hash=sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51 \ + --hash=sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872 \ # via jinja2, salt mock==3.0.5 \ --hash=sha256:83657d894c90d5681d62155c82bda9c1187827525880eda8ff5df4ec813437c3 \ @@ -241,13 +248,13 @@ msgpack==1.0.2 \ --hash=sha256:fae04496f5bc150eefad4e9571d1a76c55d021325dcd484ce45065ebbdd00984 \ --hash=sha256:fe07bc6735d08e492a327f496b7850e98cb4d112c56df69b0c844dbebcbb47f6 \ # via salt -oauthlib==3.1.0 \ - --hash=sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889 \ - --hash=sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea \ +oauthlib==3.1.1 \ + --hash=sha256:42bf6354c2ed8c6acb54d971fce6f88193d97297e18602a3a886603f9d7730cc \ + --hash=sha256:8f0215fcc533dd8dd1bee6f4c412d4f0cd7297307d43ac61666389e3bc3198a3 \ # via requests-oauthlib -packaging==20.9 \ - --hash=sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5 \ - --hash=sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a \ +packaging==21.0 \ + --hash=sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7 \ + --hash=sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14 \ # via pytest parameterized==0.7.4 \ --hash=sha256:190f8cc7230eee0b56b30d7f074fd4d165f7c45e6077582d0813c8557e738490 \ @@ -257,27 +264,34 @@ pluggy==0.13.1 \ --hash=sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0 \ --hash=sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d \ # via pytest -protobuf==3.15.6 \ - --hash=sha256:0f2da2fcc4102b6c3b57f03c9d8d5e37c63f8bc74deaa6cb54e0cc4524a77247 \ - --hash=sha256:1655fc0ba7402560d749de13edbfca1ac45d1753d8f4e5292989f18f5a00c215 \ - --hash=sha256:1771ef20e88759c4d81db213e89b7a1fc53937968e12af6603c658ee4bcbfa38 \ - --hash=sha256:1a66261a402d05c8ad8c1fde8631837307bf8d7e7740a4f3941fc3277c2e1528 \ - --hash=sha256:24f4697f57b8520c897a401b7f9a5ae45c369e22c572e305dfaf8053ecb49687 \ - --hash=sha256:256c0b2e338c1f3228d3280707606fe5531fde85ab9d704cde6fdeb55112531f \ - --hash=sha256:2b974519a2ae83aa1e31cff9018c70bbe0e303a46a598f982943c49ae1d4fcd3 \ - --hash=sha256:30fe4249a364576f9594180589c3f9c4771952014b5f77f0372923fc7bafbbe2 \ - --hash=sha256:45a91fc6f9aa86d3effdeda6751882b02de628519ba06d7160daffde0c889ff8 \ - --hash=sha256:70054ae1ce5dea7dec7357db931fcf487f40ea45b02cb719ee6af07eb1e906fb \ - --hash=sha256:74ac159989e2b02d761188a2b6f4601ff5e494d9b9d863f5ad6e98e5e0c54328 \ - --hash=sha256:822ac7f87fc2fb9b24edd2db390538b60ef50256e421ca30d65250fad5a3d477 \ - --hash=sha256:83c7c7534f050cb25383bb817159416601d1cc46c40bc5e851ec8bbddfc34a2f \ - --hash=sha256:88d8f21d1ac205eedb6dea943f8204ed08201b081dba2a966ab5612788b9bb1e \ - --hash=sha256:9ec20a6ded7d0888e767ad029dbb126e604e18db744ac0a428cf746e040ccecd \ - --hash=sha256:9ec220d90eda8bb7a7a1434a8aed4fe26d7e648c1a051c2885f3f5725b6aa71a \ - --hash=sha256:b9069e45b6e78412fba4a314ea38b4a478686060acf470d2b131b3a2c50484ec \ - --hash=sha256:d9ed0955b794f1e5f367e27f8a8ff25501eabe34573f003f06639c366ca75f73 \ - --hash=sha256:eaada29bbf087dea7d8bce4d1d604fc768749e8809e9c295922accd7c8fce4d5 \ - --hash=sha256:eac23a3e56175b710f3da9a9e8e2aa571891fbec60e0c5a06db1c7b1613b5cfd \ +protobuf==3.17.3 \ + --hash=sha256:13ee7be3c2d9a5d2b42a1030976f760f28755fcf5863c55b1460fd205e6cd637 \ + --hash=sha256:145ce0af55c4259ca74993ddab3479c78af064002ec8227beb3d944405123c71 \ + --hash=sha256:14c1c9377a7ffbeaccd4722ab0aa900091f52b516ad89c4b0c3bb0a4af903ba5 \ + --hash=sha256:1556a1049ccec58c7855a78d27e5c6e70e95103b32de9142bae0576e9200a1b0 \ + --hash=sha256:26010f693b675ff5a1d0e1bdb17689b8b716a18709113288fead438703d45539 \ + --hash=sha256:2ae692bb6d1992afb6b74348e7bb648a75bb0d3565a3f5eea5bec8f62bd06d87 \ + --hash=sha256:2bfb815216a9cd9faec52b16fd2bfa68437a44b67c56bee59bc3926522ecb04e \ + --hash=sha256:4ffbd23640bb7403574f7aff8368e2aeb2ec9a5c6306580be48ac59a6bac8bde \ + --hash=sha256:59e5cf6b737c3a376932fbfb869043415f7c16a0cf176ab30a5bbc419cd709c1 \ + --hash=sha256:6902a1e4b7a319ec611a7345ff81b6b004b36b0d2196ce7a748b3493da3d226d \ + --hash=sha256:6ce4d8bf0321e7b2d4395e253f8002a1a5ffbcfd7bcc0a6ba46712c07d47d0b4 \ + --hash=sha256:6d847c59963c03fd7a0cd7c488cadfa10cda4fff34d8bc8cba92935a91b7a037 \ + --hash=sha256:72804ea5eaa9c22a090d2803813e280fb273b62d5ae497aaf3553d141c4fdd7b \ + --hash=sha256:7a4c97961e9e5b03a56f9a6c82742ed55375c4a25f2692b625d4087d02ed31b9 \ + --hash=sha256:85d6303e4adade2827e43c2b54114d9a6ea547b671cb63fafd5011dc47d0e13d \ + --hash=sha256:8727ee027157516e2c311f218ebf2260a18088ffb2d29473e82add217d196b1c \ + --hash=sha256:99938f2a2d7ca6563c0ade0c5ca8982264c484fdecf418bd68e880a7ab5730b1 \ + --hash=sha256:9b7a5c1022e0fa0dbde7fd03682d07d14624ad870ae52054849d8960f04bc764 \ + --hash=sha256:a22b3a0dbac6544dacbafd4c5f6a29e389a50e3b193e2c70dae6bbf7930f651d \ + --hash=sha256:a38bac25f51c93e4be4092c88b2568b9f407c27217d3dd23c7a57fa522a17554 \ + --hash=sha256:a981222367fb4210a10a929ad5983ae93bd5a050a0824fc35d6371c07b78caf6 \ + --hash=sha256:ab6bb0e270c6c58e7ff4345b3a803cc59dbee19ddf77a4719c5b635f1d547aa8 \ + --hash=sha256:c56c050a947186ba51de4f94ab441d7f04fcd44c56df6e922369cc2e1a92d683 \ + --hash=sha256:e76d9686e088fece2450dbc7ee905f9be904e427341d289acbe9ad00b78ebd47 \ + --hash=sha256:ebcb546f10069b56dc2e3da35e003a02076aaa377caf8530fe9789570984a8d2 \ + --hash=sha256:f0e59430ee953184a703a324b8ec52f571c6c4259d496a19d1cabcdc19dabc62 \ + --hash=sha256:ffea251f5cd3c0b9b43c7a7a912777e0bc86263436a87c2555242a348817221b \ # via etcd3 psutil==5.8.0 \ --hash=sha256:0066a82f7b1b37d334e68697faba68e5ad5e858279fd6351c8ca6024e8d6ba64 \ @@ -353,25 +367,25 @@ pycryptodomex==3.10.1 \ --hash=sha256:f933ecf4cb736c7af60a6a533db2bf569717f2318b265f92907acff1db43bc34 \ --hash=sha256:fc9c55dc1ed57db76595f2d19a479fc1c3a1be2c9da8de798a93d286c5f65f38 \ # via salt -pyfakefs==4.4.0 \ - --hash=sha256:082d863e0e2a74351f697da404e329a91e18e5055942e59d1b836e8459b2c94c \ - --hash=sha256:1ac3b2845dabe69af56c20691b9347914581195ccdde352535fb7d4ff0055c19 \ +pyfakefs==4.5.0 \ + --hash=sha256:46234fa2dbce8ffb693ec906638449342afe83e45fa832932351d5050048159b \ + --hash=sha256:58b017b3437bbe97803a23755876c6d6aeb5aea37e52cec15e5d86b59c4c7295 \ # via -r salt/tests/requirements.in pyparsing==2.4.7 \ --hash=sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1 \ --hash=sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b \ # via packaging -pytest-cov==2.11.1 \ - --hash=sha256:359952d9d39b9f822d9d29324483e7ba04a3a17dd7d05aa6beb7ea01e359e5f7 \ - --hash=sha256:bdb9fdb0b85a7cc825269a4c56b48ccaa5c7e365054b6038772c32ddcdc969da \ +pytest-cov==2.12.1 \ + --hash=sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a \ + --hash=sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7 \ # via -r salt/tests/requirements.in -pytest==6.2.2 \ - --hash=sha256:9d1edf9e7d0b84d72ea3dbcdfd22b35fb543a5e8f2a60092dd578936bf63d7f9 \ - --hash=sha256:b574b57423e818210672e07ca1fa90aaf194a4f63f3ab909a2c67ebb22913839 \ +pytest==6.2.4 \ + --hash=sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b \ + --hash=sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890 \ # via -r salt/tests/requirements.in, pytest-cov -python-dateutil==2.8.1 \ - --hash=sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c \ - --hash=sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a \ +python-dateutil==2.8.2 \ + --hash=sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86 \ + --hash=sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9 \ # via kubernetes pyyaml==5.4.1 \ --hash=sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf \ @@ -404,47 +418,52 @@ pyyaml==5.4.1 \ --hash=sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6 \ --hash=sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0 \ # via kubernetes, salt -pyzmq==22.0.3 \ - --hash=sha256:13465c1ff969cab328bc92f7015ce3843f6e35f8871ad79d236e4fbc85dbe4cb \ - --hash=sha256:23a74de4b43c05c3044aeba0d1f3970def8f916151a712a3ac1e5cd9c0bc2902 \ - --hash=sha256:26380487eae4034d6c2a3fb8d0f2dff6dd0d9dd711894e8d25aa2d1938950a33 \ - --hash=sha256:279cc9b51db48bec2db146f38e336049ac5a59e5f12fb3a8ad864e238c1c62e3 \ - --hash=sha256:2f971431aaebe0a8b54ac018e041c2f0b949a43745444e4dadcc80d0f0ef8457 \ - --hash=sha256:30df70f81fe210506aa354d7fd486a39b87d9f7f24c3d3f4f698ec5d96b8c084 \ - --hash=sha256:33acd2b9790818b9d00526135acf12790649d8d34b2b04d64558b469c9d86820 \ - --hash=sha256:38e3dca75d81bec4f2defa14b0a65b74545812bb519a8e89c8df96bbf4639356 \ - --hash=sha256:3e29f9cf85a40d521d048b55c63f59d6c772ac1c4bf51cdfc23b62a62e377c33 \ - --hash=sha256:3ef50d74469b03725d781a2a03c57537d86847ccde587130fe35caafea8f75c6 \ - --hash=sha256:4231943514812dfb74f44eadcf85e8dd8cf302b4d0bce450ce1357cac88dbfdc \ - --hash=sha256:4f34a173f813b38b83f058e267e30465ed64b22cd0cf6bad21148d3fa718f9bb \ - --hash=sha256:532af3e6dddea62d9c49062ece5add998c9823c2419da943cf95589f56737de0 \ - --hash=sha256:581787c62eaa0e0db6c5413cedc393ebbadac6ddfd22e1cf9a60da23c4f1a4b2 \ - --hash=sha256:60e63577b85055e4cc43892fecd877b86695ee3ef12d5d10a3c5d6e77a7cc1a3 \ - --hash=sha256:61e4bb6cd60caf1abcd796c3f48395e22c5b486eeca6f3a8797975c57d94b03e \ - --hash=sha256:6d4163704201fff0f3ab0cd5d7a0ea1514ecfffd3926d62ec7e740a04d2012c7 \ - --hash=sha256:7026f0353977431fc884abd4ac28268894bd1a780ba84bb266d470b0ec26d2ed \ - --hash=sha256:763c175294d861869f18eb42901d500eda7d3fa4565f160b3b2fd2678ea0ebab \ - --hash=sha256:81e7df0da456206201e226491aa1fc449da85328bf33bbeec2c03bb3a9f18324 \ - --hash=sha256:9221783dacb419604d5345d0e097bddef4459a9a95322de6c306bf1d9896559f \ - --hash=sha256:a558c5bc89d56d7253187dccc4e81b5bb0eac5ae9511eb4951910a1245d04622 \ - --hash=sha256:b25e5d339550a850f7e919fe8cb4c8eabe4c917613db48dab3df19bfb9a28969 \ - --hash=sha256:b62ea18c0458a65ccd5be90f276f7a5a3f26a6dea0066d948ce2fa896051420f \ - --hash=sha256:c0cde362075ee8f3d2b0353b283e203c2200243b5a15d5c5c03b78112a17e7d4 \ - --hash=sha256:c5e29fe4678f97ce429f076a2a049a3d0b2660ada8f2c621e5dc9939426056dd \ - --hash=sha256:d18ddc6741b51f3985978f2fda57ddcdae359662d7a6b395bc8ff2292fca14bd \ - --hash=sha256:da7d4d4c778c86b60949d17531e60c54ed3726878de8a7f8a6d6e7f8cc8c3205 \ - --hash=sha256:f52070871a0fd90a99130babf21f8af192304ec1e995bec2a9533efc21ea4452 \ - --hash=sha256:f5831eff6b125992ec65d973f5151c48003b6754030094723ac4c6e80a97c8c4 \ - --hash=sha256:f7f63ce127980d40f3e6a5fdb87abf17ce1a7c2bd8bf2c7560e1bbce8ab1f92d \ - --hash=sha256:ff1ea14075bbddd6f29bf6beb8a46d0db779bcec6b9820909584081ec119f8fd \ +pyzmq==22.2.1 \ + --hash=sha256:021e22a8c58ab294bd4b96448a2ca4e716e1d76600192ff84c33d71edb1fbd37 \ + --hash=sha256:0471d634c7fe48ff7d3849798da6c16afc71676dd890b5ae08eb1efe735c6fec \ + --hash=sha256:0d17bac19e934e9f547a8811b7c2a32651a7840f38086b924e2e3dcb2fae5c3a \ + --hash=sha256:200ac096cee5499964c90687306a7244b79ef891f773ed4cf15019fd1f3df330 \ + --hash=sha256:240b83b3a8175b2f616f80092cbb019fcd5c18598f78ffc6aa0ae9034b300f14 \ + --hash=sha256:246f27b88722cfa729bb04881e94484e40b085720d728c1b05133b3f331b0b7b \ + --hash=sha256:2534a036b777f957bd6b89b55fb2136775ca2659fb0f1c85036ba78d17d86fd5 \ + --hash=sha256:262f470e7acde18b7217aac78d19d2e29ced91a5afbeb7d98521ebf26461aa7e \ + --hash=sha256:2dd3896b3c952cf6c8013deda53c1df16bf962f355b5503d23521e0f6403ae3d \ + --hash=sha256:31c5dfb6df5148789835128768c01bf6402eb753d06f524f12f6786caf96fb44 \ + --hash=sha256:4842a8263cbaba6fce401bbe4e2b125321c401a01714e42624dabc554bfc2629 \ + --hash=sha256:50d007d5702171bc810c1e74498fa2c7bc5b50f9750697f7fd2a3e71a25aad91 \ + --hash=sha256:5933d1f4087de6e52906f72d92e1e4dcc630d371860b92c55d7f7a4b815a664c \ + --hash=sha256:620b0abb813958cb3ecb5144c177e26cde92fee6f43c4b9de6b329515532bf27 \ + --hash=sha256:631f932fb1fa4b76f31adf976f8056519bc6208a3c24c184581c3dd5be15066e \ + --hash=sha256:66375a6094af72a6098ed4403b15b4db6bf00013c6febc1baa832e7abda827f4 \ + --hash=sha256:6a5b4566f66d953601d0d47d4071897f550a265bafd52ebcad5ac7aad3838cbb \ + --hash=sha256:6d18c76676771fd891ca8e0e68da0bbfb88e30129835c0ade748016adb3b6242 \ + --hash=sha256:6e9c030222893afa86881d7485d3e841969760a16004bd23e9a83cca28b42778 \ + --hash=sha256:89200ab6ef9081c72a04ed84c52a50b60dcb0655375aeedb40689bc7c934715e \ + --hash=sha256:93705cb90baa9d6f75e8448861a1efd3329006f79095ab18846bd1eaa342f7c3 \ + --hash=sha256:a649065413ba4eab92a783a7caa4de8ce14cf46ba8a2a09951426143f1298adb \ + --hash=sha256:ac4497e4b7d134ee53ce5532d9cc3b640d6e71806a55062984e0c99a2f88f465 \ + --hash=sha256:b2c16d20bd0aef8e57bc9505fdd80ea0d6008020c3740accd96acf1b3d1b5347 \ + --hash=sha256:b3f57bee62e36be5c97712de32237c5589caee0d1154c2ad01a888accfae20bc \ + --hash=sha256:b4428302c389fffc0c9c07a78cad5376636b9d096f332acfe66b321ae9ff2c63 \ + --hash=sha256:b4a51c7d906dc263a0cc5590761e53e0a68f2c2fefe549cbef21c9ee5d2d98a4 \ + --hash=sha256:b921758f8b5098faa85f341bbdd5e36d5339de5e9032ca2b07d8c8e7bec5069b \ + --hash=sha256:c1b6619ceb33a8907f1cb82ff8afc8a133e7a5f16df29528e919734718600426 \ + --hash=sha256:c9cb0bd3a3cb7ccad3caa1d7b0d18ba71ed3a4a3610028e506a4084371d4d223 \ + --hash=sha256:d60a407663b7c2af781ab7f49d94a3d379dd148bb69ea8d9dd5bc69adf18097c \ + --hash=sha256:da7f7f3bb08bcf59a6b60b4e53dd8f08bb00c9e61045319d825a906dbb3c8fb7 \ + --hash=sha256:e66025b64c4724ba683d6d4a4e5ee23de12fe9ae683908f0c7f0f91b4a2fd94e \ + --hash=sha256:ed67df4eaa99a20d162d76655bda23160abdf8abf82a17f41dfd3962e608dbcc \ + --hash=sha256:f520e9fee5d7a2e09b051d924f85b977c6b4e224e56c0551c3c241bbeeb0ad8d \ + --hash=sha256:f5c84c5de9a773bbf8b22c51e28380999ea72e5e85b4db8edf5e69a7a0d4d9f9 \ + --hash=sha256:ff345d48940c834168f81fa1d4724675099f148f1ab6369748c4d712ed71bf7c \ # via salt requests-oauthlib==1.3.0 \ --hash=sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d \ --hash=sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a \ # via kubernetes -requests==2.25.1 \ - --hash=sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804 \ - --hash=sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e \ +requests==2.26.0 \ + --hash=sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24 \ + --hash=sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7 \ # via kubernetes, requests-oauthlib, salt rsa==4.7.2 \ --hash=sha256:78f9a9bf4e7be0c5ded4583326e7461e3a3c5aae24073648b4bdfa797d78c9d2 \ @@ -453,38 +472,38 @@ rsa==4.7.2 \ salt==3002.6 \ --hash=sha256:ffc478569363e1d17b6a3a0c421eaae9c079bbeabc4c7725a222d0fbf903a0a5 \ # via -r salt/tests/requirements.in -six==1.15.0 \ - --hash=sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259 \ - --hash=sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced \ - # via etcd3, google-auth, grpcio, kubernetes, mock, protobuf, python-dateutil, tenacity, websocket-client -tenacity==7.0.0 \ - --hash=sha256:5bd16ef5d3b985647fe28dfa6f695d343aa26479a04e8792b9d3c8f49e361ae1 \ - --hash=sha256:a0ce48587271515db7d3a5e700df9ae69cce98c4b57c23a4886da15243603dd8 \ +six==1.16.0 \ + --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ + --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 \ + # via etcd3, grpcio, kubernetes, mock, protobuf, python-dateutil +tenacity==8.0.1 \ + --hash=sha256:43242a20e3e73291a28bcbcacfd6e000b02d3857a9a9fff56b297a27afdc932f \ + --hash=sha256:f78f4ea81b0fabc06728c11dc2a8c01277bfc5181b321a4770471902e3eb844a \ # via etcd3 toml==0.10.2 \ --hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b \ --hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f \ - # via pytest -typing-extensions==3.7.4.3 \ - --hash=sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918 \ - --hash=sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c \ - --hash=sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f \ + # via pytest, pytest-cov +typing-extensions==3.10.0.0 \ + --hash=sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497 \ + --hash=sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342 \ + --hash=sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84 \ # via importlib-metadata urllib3==1.26.6 \ --hash=sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4 \ --hash=sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f \ # via -r salt/tests/requirements.in, kubernetes, requests -websocket-client==0.58.0 \ - --hash=sha256:44b5df8f08c74c3d82d28100fdc81f4536809ce98a17f0757557813275fbb663 \ - --hash=sha256:63509b41d158ae5b7f67eb4ad20fecbb4eee99434e73e140354dc3ff8e09716f \ +websocket-client==1.2.1 \ + --hash=sha256:0133d2f784858e59959ce82ddac316634229da55b498aac311f1620567a710ec \ + --hash=sha256:8dfb715d8a992f5712fff8c843adae94e22b22a99b2c5e6b0ec4a1a981cc4e0d \ # via kubernetes -zipp==3.4.1 \ - --hash=sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76 \ - --hash=sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098 \ +zipp==3.5.0 \ + --hash=sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3 \ + --hash=sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4 \ # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: -setuptools==54.2.0 \ - --hash=sha256:aa9c24fb83a9116b8d425e53bec24c7bfdbffc313c2159f9ed036d4a6dd32d7d \ - --hash=sha256:b726461910b9ba30f077880c228bea22121aec50b172edf39eb7ff026c054a11 \ +setuptools==57.4.0 \ + --hash=sha256:6bac238ffdf24e8806c61440e755192470352850f3419a52f26ffe0a1a64f465 \ + --hash=sha256:a49230977aa6cfb9d933614d2f7b79036e9945c4cdd7583163f4e920b83418d6 \ # via google-auth, kubernetes diff --git a/salt/tests/unit/modules/files/test_metalk8s_drain.yaml b/salt/tests/unit/modules/files/test_metalk8s_drain.yaml index 223bcc42b8..117f2f3d86 100644 --- a/salt/tests/unit/modules/files/test_metalk8s_drain.yaml +++ b/salt/tests/unit/modules/files/test_metalk8s_drain.yaml @@ -390,12 +390,12 @@ drain: datasets: __common: base-pod: &base_pod - api_version: v1 + apiVersion: v1 kind: Pod metadata: &base_pod_meta name: my-pod namespace: my-namespace - owner_references: null + ownerReferences: null annotations: {} uid: ede4ed1b-9a5e-4168-961c-b1bdad691ec7 spec: &base_pod_spec @@ -416,7 +416,7 @@ datasets: <<: *base_pod_spec volumes: - name: tmp-volume - empty_dir: {} + emptyDir: {} static-pod: &static_pod <<: *base_pod @@ -433,8 +433,8 @@ datasets: metadata: <<: *base_pod_meta name: my-finished-pod - owner_references: - - api_version: batch/v1 + ownerReferences: + - apiVersion: batch/v1 kind: Job controller: true name: my-job @@ -447,8 +447,8 @@ datasets: metadata: &replicaset_pod_meta <<: *base_pod_meta name: my-replicaset-pod - owner_references: - - api_version: apps/v1 + ownerReferences: + - apiVersion: apps/v1 kind: ReplicaSet controller: true name: my-replicaset @@ -458,8 +458,8 @@ datasets: metadata: <<: *base_pod_meta name: my-daemonset-pod - owner_references: - - api_version: apps/v1 + ownerReferences: + - apiVersion: apps/v1 kind: DaemonSet controller: true name: my-daemonset @@ -469,28 +469,28 @@ datasets: metadata: <<: *base_pod_meta name: my-custom-pod - owner_references: - - api_version: unkown/v1 + ownerReferences: + - apiVersion: unkown/v1 kind: Unknown controller: true name: my-unknown-controller common-replicaset: &common_replicaset - api_version: apps/v1 + apiVersion: apps/v1 kind: ReplicaSet metadata: name: my-replicaset namespace: my-namespace common-daemonset: &common_daemonset - api_version: apps/v1 + apiVersion: apps/v1 kind: DaemonSet metadata: name: my-daemonset namespace: my-namespace common-job: &common_job - api_version: batch/v1 + apiVersion: batch/v1 kind: Job metadata: name: my-job @@ -598,7 +598,7 @@ datasets: <<: *single_replicaset_dataset evictionmocks: - kind: EvictionMock - api_version: __tests__ + apiVersion: __tests__ pod: my-namespace/my-replicaset-pod locked: true @@ -606,6 +606,6 @@ datasets: <<: *single_replicaset_dataset evictionmocks: - kind: EvictionMock - api_version: __tests__ + apiVersion: __tests__ pod: my-namespace/my-replicaset-pod raises: true diff --git a/salt/tests/unit/modules/files/test_metalk8s_kubernetes.yaml b/salt/tests/unit/modules/files/test_metalk8s_kubernetes.yaml index 1590e57284..f514146657 100644 --- a/salt/tests/unit/modules/files/test_metalk8s_kubernetes.yaml +++ b/salt/tests/unit/modules/files/test_metalk8s_kubernetes.yaml @@ -4,14 +4,32 @@ common_tests: - raises: True result: 'Must provide one of .* to .* object' - # Error invalid manifest + # Error invalid manifest no apiVersion - manifest: invalid: manifest content: wrong a: b - info_scope: null raises: True - result: "Invalid manifest" + result: "apiVersion" + + # Error invalid manifest no kind + - manifest: + apiVersion: v1 + invalid: manifest + content: wrong + a: b + raises: True + result: "kind" + + # Error unknown object + - manifest: + apiVersion: v42 + kind: Banana + spec: + abc: def + namespaced: null + raises: True + result: "Kind 'Banana' from apiVersion 'v42' is unknown" # Error giving manifest and name (manifest file path) - name: /path/to/my/manifest.yaml @@ -202,7 +220,7 @@ replace_object: kind: Node metadata: name: my_node - resource_version: "123456" + resourceVersion: "123456" result: &simple_node_replace_result apiVersion: v1 kind: Node @@ -249,21 +267,22 @@ replace_object: heritage: salt resourceVersion: "123456" - # Replace service object (special case we need to keep cluster_ip) + # Replace service object (special case we need to keep clusterIP) - manifest: apiVersion: v1 kind: Service metadata: name: my_service + spec: {} old_object: # Old object is considered as k8s object so snake case apiVersion: v1 kind: Service metadata: name: my_service - resource_version: "123456" + resourceVersion: "123456" spec: - cluster_ip: "10.11.12.13" + clusterIP: "10.11.12.13" result: apiVersion: v1 kind: Service @@ -277,11 +296,11 @@ replace_object: spec: clusterIP: "10.11.12.13" - # Replace service object (special case: cluster_ip set in the manifest and + # Replace service object (special case: clusterIP set in the manifest and # old_object, keep the manifest one) # NOTE: This test just ensure we keep the one from input manifest and not # replace it with the one from old_object, but in practice apiServer - # will reject this call since cluster_ip is immutable field + # will reject this call since clusterIP is immutable field - manifest: apiVersion: v1 kind: Service @@ -295,9 +314,9 @@ replace_object: kind: Service metadata: name: my_service - resource_version: "123456" + resourceVersion: "123456" spec: - cluster_ip: "10.11.12.13" + clusterIP: "10.11.12.13" result: apiVersion: v1 kind: Service @@ -311,7 +330,7 @@ replace_object: spec: clusterIP: "20.21.22.23" - # Replace service object (special case we need to keep health_check_node_port) + # Replace service object (special case we need to keep healthCheckNodePort) - manifest: apiVersion: v1 kind: Service @@ -325,11 +344,11 @@ replace_object: kind: Service metadata: name: my_service - resource_version: "123456" + resourceVersion: "123456" spec: type: LoadBalancer - cluster_ip: "10.11.12.13" - health_check_node_port: "12345" + clusterIP: "10.11.12.13" + healthCheckNodePort: 12345 result: apiVersion: v1 kind: Service @@ -343,13 +362,13 @@ replace_object: spec: type: LoadBalancer clusterIP: "10.11.12.13" - healthCheckNodePort: "12345" + healthCheckNodePort: 12345 - # Replace service object (special case: health_check_node_port set in the manifest + # Replace service object (special case: healthCheckNodePort set in the manifest # and old_object, keep the manifest one) # NOTE: This test just ensure we keep the one from input manifest and not # replace it with the one from old_object, but in practice apiServer - # will reject this call since health_check_node_port is immutable field + # will reject this call since healthCheckNodePort is immutable field - manifest: apiVersion: v1 kind: Service @@ -358,18 +377,18 @@ replace_object: spec: clusterIP: "10.11.12.13" type: LoadBalancer - healthCheckNodePort: "12345" + healthCheckNodePort: 12345 old_object: # Old object is considered as k8s object so snake case apiVersion: v1 kind: Service metadata: name: my_service - resource_version: "123456" + resourceVersion: "123456" spec: - cluster_ip: "10.11.12.13" + clusterIP: "10.11.12.13" type: LoadBalancer - health_check_node_port: "9876" + healthCheckNodePort: 9876 result: apiVersion: v1 kind: Service @@ -383,7 +402,7 @@ replace_object: spec: clusterIP: "10.11.12.13" type: LoadBalancer - healthCheckNodePort: "12345" + healthCheckNodePort: 12345 # Error when replacing object - manifest: @@ -454,7 +473,7 @@ get_object: name: my_pod api_status_code: 0 raises: True - result: Failed to retrieve object + result: Failed to get object update_object: # Simple Node update (using manifest) @@ -505,7 +524,7 @@ update_object: kind: Node name: my_node raises: True - result: 'Must provide one of "manifest" or "name" \(path to a file\) or "name" and "kind" and "apiVersion" and "patch" to update object.' + result: 'Must provide one of "manifest" or "name" \(path to a file\) or "name" and "kind" and "apiVersion" and "patch" to patch object.' # Error when updating object - apiVersion: v1 @@ -516,7 +535,7 @@ update_object: labels: my.new.label: my-new-label raises: True - result: Failed to update object + result: Failed to patch object list_objects: # Simple list Pod @@ -531,7 +550,7 @@ list_objects: # Simple list Node - apiVersion: v1 kind: Node - info_scope: cluster + namespaced: False result: *list_object_result # List Pod for a specific namespace @@ -580,12 +599,12 @@ list_objects: label_selector: "my.simple.label=abcd" result: *list_object_result - # Invalid object listing - - apiVersion: v1 - kind: MyInvalidKind - info_scope: null + # Error listing invalid object + - apiVersion: v42 + kind: Banana + namespaced: null raises: True - result: 'Unsupported resource "v1/MyInvalidKind"' + result: "Kind 'Banana' from apiVersion 'v42' is unknown" # Error when listing namespaced resource (default namespace) - apiVersion: v1 @@ -605,7 +624,7 @@ list_objects: # Error when listing cluster resource - apiVersion: v1 kind: Node - info_scope: cluster + namespaced: False api_status_code: 0 raises: True result: 'Failed to list resources "v1/Node"' diff --git a/salt/tests/unit/modules/test_metalk8s_drain.py b/salt/tests/unit/modules/test_metalk8s_drain.py index a51b769ff2..7b6f2d49e1 100644 --- a/salt/tests/unit/modules/test_metalk8s_drain.py +++ b/salt/tests/unit/modules/test_metalk8s_drain.py @@ -54,7 +54,7 @@ def test_virtual_nominal(self): @parameterized.expand( [ - ("missing kubernetes", "kubernetes.client.rest"), + ("missing kubernetes", "kubernetes"), ("missing urllib3", "urllib3.exceptions"), ] ) @@ -109,16 +109,16 @@ def _create_mock(*args, **kwargs): elif create_raises == "HTTPError": raise HTTPError() - get_kind_info_mock = MagicMock() - create_mock = get_kind_info_mock.return_value.client.create - create_mock.side_effect = _create_mock + create_mock = MagicMock(side_effect=_create_mock) - utils_dict = { - "metalk8s_kubernetes.get_kind_info": get_kind_info_mock, - } - with patch.dict(metalk8s_drain.__utils__, utils_dict), capture_logs( - metalk8s_drain.log, logging.DEBUG - ) as captured: + dynamic_client_mock = MagicMock() + dynamic_client_mock.request.side_effect = create_mock + + dynamic_mock = MagicMock() + dynamic_mock.DynamicClient.return_value = dynamic_client_mock + with patch("kubernetes.dynamic", dynamic_mock), patch( + "kubernetes.config", MagicMock() + ), capture_logs(metalk8s_drain.log, logging.DEBUG) as captured: if raises: self.assertRaisesRegex( CommandExecutionError, result, metalk8s_drain.evict_pod, **kwargs diff --git a/salt/tests/unit/modules/test_metalk8s_kubernetes.py b/salt/tests/unit/modules/test_metalk8s_kubernetes.py index e5e5b29f32..ec047232b6 100644 --- a/salt/tests/unit/modules/test_metalk8s_kubernetes.py +++ b/salt/tests/unit/modules/test_metalk8s_kubernetes.py @@ -3,6 +3,7 @@ from unittest import TestCase from unittest.mock import MagicMock, patch +from kubernetes.dynamic.exceptions import ResourceNotFoundError from kubernetes.client.rest import ApiException from parameterized import param, parameterized from salt.utils import dictupdate, hashutils @@ -22,6 +23,26 @@ YAML_TESTS_CASES = yaml.safe_load(fd) +def _mock_k8s_dynamic(namespaced, action, mock): + def _resources_get_mock(api_version, kind): + if namespaced is None: + raise ResourceNotFoundError("Error !!") + obj = MagicMock() + obj.api_version = api_version + obj.kind = kind + obj.namespaced = namespaced + setattr(obj, action, mock) + return obj + + dynamic_client_mock = MagicMock() + dynamic_client_mock.resources.get.side_effect = _resources_get_mock + + dynamic_mock = MagicMock() + dynamic_mock.DynamicClient.return_value = dynamic_client_mock + + return dynamic_mock + + class Metalk8sKubernetesTestCase(TestCase, mixins.LoaderModuleMockMixin): """ TestCase for `metalk8s_kubernetes` module @@ -31,51 +52,6 @@ class Metalk8sKubernetesTestCase(TestCase, mixins.LoaderModuleMockMixin): @staticmethod def loader_module_globals(): - def _manifest_to_object_mock(manifest, **_): - # Simulate k8s object for key used in the module - # It's not really robust but here we only test the - # `metalk8s_kubernetes` module - obj = MagicMock() - - obj.api_version = manifest.get("apiVersion") - obj.kind = manifest.get("kind") - - meta = manifest.get("metadata", {}) - obj.metadata.name = meta.get("name") - obj.metadata.namespace = meta.get("namespace") - obj.metadata.resourceVersion = meta.get("resourceVersion") - obj.metadata.resource_version = meta.get("resource_version") - - obj.spec.cluster_ip = manifest.get("spec", {}).get("clusterIP") - obj.spec.health_check_node_port = manifest.get("spec", {}).get( - "healthCheckNodePort" - ) - obj.spec.type = manifest.get("spec", {}).get("type") - - def _to_dict(): - if obj.metadata.resourceVersion: - meta["resourceVersion"] = obj.metadata.resourceVersion - if obj.metadata.resource_version: - meta["resourceVersion"] = obj.metadata.resource_version - if obj.spec.cluster_ip: - manifest.setdefault("spec", {})["clusterIP"] = obj.spec.cluster_ip - if "cluster_ip" in manifest.get("spec", {}): - del manifest["spec"]["cluster_ip"] - if obj.spec.type: - manifest.setdefault("spec", {})["type"] = obj.spec.type - if obj.spec.health_check_node_port: - manifest.setdefault("spec", {})[ - "healthCheckNodePort" - ] = obj.spec.health_check_node_port - if "health_check_node_port" in manifest.get("spec", {}): - del manifest["spec"]["health_check_node_port"] - - return manifest - - obj.to_dict.side_effect = _to_dict - - return obj - salt_dict = { "metalk8s_kubernetes.get_kubeconfig": MagicMock( return_value=("/my/kube/config", "my-context") @@ -104,15 +80,7 @@ def _hashutil_digest(instr, checksum="md5"): # Consider we have no slots in these tests salt_obj.metalk8s.format_slots.side_effect = lambda manifest: manifest - return { - "__utils__": { - "metalk8s_kubernetes.get_kind_info": MagicMock(), - "metalk8s_kubernetes.convert_manifest_to_object": MagicMock( - side_effect=_manifest_to_object_mock - ), - }, - "__salt__": salt_obj, - } + return {"__salt__": salt_obj} def assertDictContainsSubset(self, subdict, maindict): return self.assertEqual(dict(maindict, **subdict), maindict) @@ -124,7 +92,12 @@ def test_virtual_success(self): reload(metalk8s_kubernetes) self.assertEqual(metalk8s_kubernetes.__virtual__(), "metalk8s_kubernetes") - @parameterized.expand(["kubernetes.client", ("urllib3.exceptions", "urllib3")]) + @parameterized.expand( + [ + "kubernetes", + ("urllib3.exceptions", "urllib3"), + ] + ) def test_virtual_fail_import(self, import_error_on, dep_error=None): """ Tests the return of `__virtual__` function, fail import @@ -139,18 +112,6 @@ def test_virtual_fail_import(self, import_error_on, dep_error=None): ), ) - def test_virtual_no_utils(self): - """ - Tests the return of `__virtual__` function, missing kubernetes utils - """ - with patch.dict(metalk8s_kubernetes.__utils__): - metalk8s_kubernetes.__utils__.pop("metalk8s_kubernetes.get_kind_info") - reload(metalk8s_kubernetes) - self.assertEqual( - metalk8s_kubernetes.__virtual__(), - (False, "Missing `metalk8s_kubernetes` utils module"), - ) - @utils.parameterized_from_cases( YAML_TESTS_CASES["create_object"] + YAML_TESTS_CASES["common_tests"] ) @@ -159,7 +120,7 @@ def test_create_object( result, raises=False, api_status_code=None, - info_scope="cluster", + namespaced=False, manifest_file_content=None, called_with=None, **kwargs @@ -174,16 +135,14 @@ def _create_mock(body, **_): status=api_status_code, reason="An error has occurred" ) - # body == object - return body - - get_kind_info_mock = MagicMock() - if not info_scope: - get_kind_info_mock.side_effect = ValueError("An error has occurred") - get_kind_info_mock.return_value.scope = info_scope + obj = MagicMock() + obj.to_dict.return_value = body + return obj - create_mock = get_kind_info_mock.return_value.client.create - create_mock.side_effect = _create_mock + create_mock = MagicMock(side_effect=_create_mock) + dynamic_mock = _mock_k8s_dynamic( + namespaced=namespaced, action="create", mock=create_mock + ) manifest_read_mock = MagicMock() # None = IOError @@ -195,13 +154,12 @@ def _create_mock(body, **_): else: manifest_read_mock.return_value = manifest_file_content - utils_dict = {"metalk8s_kubernetes.get_kind_info": get_kind_info_mock} salt_dict = { "metalk8s_kubernetes.read_and_render_yaml_file": manifest_read_mock } - with patch.dict(metalk8s_kubernetes.__utils__, utils_dict), patch.dict( - metalk8s_kubernetes.__salt__, salt_dict - ): + with patch("kubernetes.dynamic", dynamic_mock), patch( + "kubernetes.config", MagicMock() + ), patch.dict(metalk8s_kubernetes.__salt__, salt_dict): if raises: self.assertRaisesRegex( Exception, result, metalk8s_kubernetes.create_object, **kwargs @@ -220,7 +178,7 @@ def test_delete_object( result, raises=False, api_status_code=None, - info_scope="namespaced", + namespaced=True, manifest_file_content=None, called_with=None, **kwargs @@ -241,13 +199,10 @@ def _delete_mock(name, **_): res.to_dict.return_value = "<{} deleted object dict>".format(name) return res - get_kind_info_mock = MagicMock() - if not info_scope: - get_kind_info_mock.side_effect = ValueError("An error has occurred") - get_kind_info_mock.return_value.scope = info_scope - - delete_mock = get_kind_info_mock.return_value.client.delete - delete_mock.side_effect = _delete_mock + delete_mock = MagicMock(side_effect=_delete_mock) + dynamic_mock = _mock_k8s_dynamic( + namespaced=namespaced, action="delete", mock=delete_mock + ) manifest_read_mock = MagicMock() # None = IOError @@ -259,13 +214,12 @@ def _delete_mock(name, **_): else: manifest_read_mock.return_value = manifest_file_content - utils_dict = {"metalk8s_kubernetes.get_kind_info": get_kind_info_mock} salt_dict = { "metalk8s_kubernetes.read_and_render_yaml_file": manifest_read_mock } - with patch.dict(metalk8s_kubernetes.__utils__, utils_dict), patch.dict( - metalk8s_kubernetes.__salt__, salt_dict - ): + with patch("kubernetes.dynamic", dynamic_mock), patch( + "kubernetes.config", MagicMock() + ), patch.dict(metalk8s_kubernetes.__salt__, salt_dict): if raises: self.assertRaisesRegex( Exception, result, metalk8s_kubernetes.delete_object, **kwargs @@ -284,7 +238,7 @@ def test_replace_object( result, raises=False, api_status_code=None, - info_scope="cluster", + namespaced=False, manifest_file_content=None, called_with=None, **kwargs @@ -299,16 +253,14 @@ def _replace_mock(body, **_): status=api_status_code, reason="An error has occurred" ) - # body == object - return body - - get_kind_info_mock = MagicMock() - if not info_scope: - get_kind_info_mock.side_effect = ValueError("An error has occurred") - get_kind_info_mock.return_value.scope = info_scope + obj = MagicMock() + obj.to_dict.return_value = body + return obj - replace_mock = get_kind_info_mock.return_value.client.replace - replace_mock.side_effect = _replace_mock + replace_mock = MagicMock(side_effect=_replace_mock) + dynamic_mock = _mock_k8s_dynamic( + namespaced=namespaced, action="replace", mock=replace_mock + ) manifest_read_mock = MagicMock() # None = IOError @@ -320,13 +272,12 @@ def _replace_mock(body, **_): else: manifest_read_mock.return_value = manifest_file_content - utils_dict = {"metalk8s_kubernetes.get_kind_info": get_kind_info_mock} salt_dict = { "metalk8s_kubernetes.read_and_render_yaml_file": manifest_read_mock } - with patch.dict(metalk8s_kubernetes.__utils__, utils_dict), patch.dict( - metalk8s_kubernetes.__salt__, salt_dict - ): + with patch("kubernetes.dynamic", dynamic_mock), patch( + "kubernetes.config", MagicMock() + ), patch.dict(metalk8s_kubernetes.__salt__, salt_dict): if raises: self.assertRaisesRegex( Exception, result, metalk8s_kubernetes.replace_object, **kwargs @@ -347,7 +298,7 @@ def test_get_object( result, raises=False, api_status_code=None, - info_scope="namespaced", + namespaced=True, manifest_file_content=None, called_with=None, **kwargs @@ -356,7 +307,7 @@ def test_get_object( Tests the return of `get_object` function """ - def _retrieve_mock(name, **_): + def _get_mock(name, **_): if api_status_code is not None: raise ApiException( status=api_status_code, reason="An error has occurred" @@ -368,13 +319,10 @@ def _retrieve_mock(name, **_): res.to_dict.return_value = "<{} object dict>".format(name) return res - get_kind_info_mock = MagicMock() - if not info_scope: - get_kind_info_mock.side_effect = ValueError("An error has occurred") - get_kind_info_mock.return_value.scope = info_scope - - retrieve_mock = get_kind_info_mock.return_value.client.retrieve - retrieve_mock.side_effect = _retrieve_mock + get_mock = MagicMock(side_effect=_get_mock) + dynamic_mock = _mock_k8s_dynamic( + namespaced=namespaced, action="get", mock=get_mock + ) manifest_read_mock = MagicMock() # None = IOError @@ -386,24 +334,21 @@ def _retrieve_mock(name, **_): else: manifest_read_mock.return_value = manifest_file_content - utils_dict = {"metalk8s_kubernetes.get_kind_info": get_kind_info_mock} salt_dict = { "metalk8s_kubernetes.read_and_render_yaml_file": manifest_read_mock } - with patch.dict(metalk8s_kubernetes.__utils__, utils_dict), patch.dict( - metalk8s_kubernetes.__salt__, salt_dict - ): + with patch("kubernetes.dynamic", dynamic_mock), patch( + "kubernetes.config", MagicMock() + ), patch.dict(metalk8s_kubernetes.__salt__, salt_dict): if raises: self.assertRaisesRegex( Exception, result, metalk8s_kubernetes.get_object, **kwargs ) else: self.assertEqual(metalk8s_kubernetes.get_object(**kwargs), result) - retrieve_mock.assert_called_once() + get_mock.assert_called_once() if called_with: - self.assertDictContainsSubset( - called_with, retrieve_mock.call_args[1] - ) + self.assertDictContainsSubset(called_with, get_mock.call_args[1]) @utils.parameterized_from_cases( YAML_TESTS_CASES["update_object"] + YAML_TESTS_CASES["common_tests"] @@ -413,7 +358,7 @@ def test_update_object( result, raises=False, initial_obj=None, - info_scope="cluster", + namespaced=False, manifest_file_content=None, called_with=None, **kwargs @@ -422,7 +367,7 @@ def test_update_object( Tests the return of `update_object` function """ - def _update_mock(body, **_): + def _patch_mock(body, **_): if not initial_obj: raise ApiException(status=0, reason="An error has occurred") @@ -431,13 +376,10 @@ def _update_mock(body, **_): res.to_dict.return_value = dictupdate.update(initial_obj, body) return res - get_kind_info_mock = MagicMock() - if not info_scope: - get_kind_info_mock.side_effect = ValueError("An error has occurred") - get_kind_info_mock.return_value.scope = info_scope - - update_mock = get_kind_info_mock.return_value.client.update - update_mock.side_effect = _update_mock + patch_mock = MagicMock(side_effect=_patch_mock) + dynamic_mock = _mock_k8s_dynamic( + namespaced=namespaced, action="patch", mock=patch_mock + ) manifest_read_mock = MagicMock() # None = IOError @@ -449,22 +391,21 @@ def _update_mock(body, **_): else: manifest_read_mock.return_value = manifest_file_content - utils_dict = {"metalk8s_kubernetes.get_kind_info": get_kind_info_mock} salt_dict = { "metalk8s_kubernetes.read_and_render_yaml_file": manifest_read_mock } - with patch.dict(metalk8s_kubernetes.__utils__, utils_dict), patch.dict( - metalk8s_kubernetes.__salt__, salt_dict - ): + with patch("kubernetes.dynamic", dynamic_mock), patch( + "kubernetes.config", MagicMock() + ), patch.dict(metalk8s_kubernetes.__salt__, salt_dict): if raises: self.assertRaisesRegex( Exception, result, metalk8s_kubernetes.update_object, **kwargs ) else: self.assertEqual(metalk8s_kubernetes.update_object(**kwargs), result) - update_mock.assert_called_once() + patch_mock.assert_called_once() if called_with: - self.assertDictContainsSubset(called_with, update_mock.call_args[1]) + self.assertDictContainsSubset(called_with, patch_mock.call_args[1]) @parameterized.expand( [ @@ -496,7 +437,7 @@ def test_list_objects( result, raises=False, api_status_code=None, - info_scope="namespaced", + namespaced=True, called_with=None, **kwargs ): @@ -510,27 +451,25 @@ def _list_mock(**_): status=api_status_code, reason="An error has occurred" ) - res = MagicMock() # Do not return a real list object as it does not bring any value # in this test - obj1 = MagicMock() - obj1.to_dict.return_value = "" - obj2 = MagicMock() - obj2.to_dict.return_value = "" - - res.items = [obj1, obj2] + res = MagicMock() + res.to_dict.return_value = { + "items": [ + "", + "", + ] + } return res - get_kind_info_mock = MagicMock() - if not info_scope: - get_kind_info_mock.side_effect = ValueError("An error has occurred") - get_kind_info_mock.return_value.scope = info_scope + list_mock = MagicMock(side_effect=_list_mock) + dynamic_mock = _mock_k8s_dynamic( + namespaced=namespaced, action="get", mock=list_mock + ) - list_mock = get_kind_info_mock.return_value.client.list - list_mock.side_effect = _list_mock - - utils_dict = {"metalk8s_kubernetes.get_kind_info": get_kind_info_mock} - with patch.dict(metalk8s_kubernetes.__utils__, utils_dict): + with patch("kubernetes.dynamic", dynamic_mock), patch( + "kubernetes.config", MagicMock() + ): if raises: self.assertRaisesRegex( Exception, result, metalk8s_kubernetes.list_objects, **kwargs diff --git a/salt/tests/unit/modules/test_metalk8s_kubernetes_utils.py b/salt/tests/unit/modules/test_metalk8s_kubernetes_utils.py index 2a6a2e4d2e..613b97e6ba 100644 --- a/salt/tests/unit/modules/test_metalk8s_kubernetes_utils.py +++ b/salt/tests/unit/modules/test_metalk8s_kubernetes_utils.py @@ -3,9 +3,9 @@ from unittest import TestCase from unittest.mock import MagicMock, mock_open, patch -from kubernetes.client.rest import ApiException from parameterized import param, parameterized from salt.exceptions import CommandExecutionError +from urllib3.exceptions import HTTPError import yaml import metalk8s_kubernetes_utils @@ -39,8 +39,7 @@ def test_virtual_success(self): @parameterized.expand( [ - ("kubernetes.client"), - ("kubernetes.config"), + ("kubernetes"), (("urllib3.exceptions", "urllib3")), ] ) @@ -99,23 +98,25 @@ def test_get_kubeconfig( def test_get_version_info( self, result, - get_code_raise=False, + k8s_connection_raise=False, raises=False, ): """ Tests the return of `get_version_info` function """ kubeconfig_mock = MagicMock(return_value=("my_kubeconfig.conf", "my_context")) - api_instance_mock = MagicMock() - get_code_mock = api_instance_mock.return_value.get_code - if get_code_raise: - get_code_mock.side_effect = ApiException("Failed to get version info") - get_code_mock.return_value.to_dict.return_value = result + dynamic_client_mock = MagicMock() + dynamic_client_mock.version = {"kubernetes": result} + + dynamic_mock = MagicMock() + dynamic_mock.DynamicClient.return_value = dynamic_client_mock + if k8s_connection_raise: + dynamic_mock.DynamicClient.side_effect = HTTPError("Failed to connect") with patch("metalk8s_kubernetes_utils.get_kubeconfig", kubeconfig_mock), patch( - "kubernetes.config.new_client_from_config", MagicMock() - ), patch("kubernetes.client.VersionApi", api_instance_mock): + "kubernetes.dynamic", dynamic_mock + ), patch("kubernetes.config", MagicMock()): if raises: self.assertRaisesRegex( CommandExecutionError, @@ -124,7 +125,6 @@ def test_get_version_info( ) else: self.assertEqual(metalk8s_kubernetes_utils.get_version_info(), result) - get_code_mock.assert_called() @parameterized.expand( [ diff --git a/tests/conftest.py b/tests/conftest.py index 5b7754972f..879064b958 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -84,17 +84,17 @@ def kubeconfig(kubeconfig_data, tmp_path): @pytest.fixture def control_plane_ingress_ip(k8s_client): """Return the Control Plane Ingress IP from Kubernetes service""" - ingress_svc = k8s_client.read_namespaced_service( + ingress_svc = k8s_client.resources.get(api_version="v1", kind="Service").get( name="ingress-nginx-control-plane-controller", namespace="metalk8s-ingress", ) - return ingress_svc.spec.load_balancer_ip or ingress_svc.spec.external_i_ps[0] + return ingress_svc.spec.loadBalancerIP or ingress_svc.spec.externalIPs[0] @pytest.fixture def control_plane_ingress_ep(k8s_client, control_plane_ingress_ip): """Return the Control Plane Ingress Endpoint from Kubernetes service""" - ingress_svc = k8s_client.read_namespaced_service( + ingress_svc = k8s_client.resources.get(api_version="v1", kind="Service").get( name="ingress-nginx-control-plane-controller", namespace="metalk8s-ingress", ) @@ -104,49 +104,23 @@ def control_plane_ingress_ep(k8s_client, control_plane_ingress_ip): @pytest.fixture -def k8s_apiclient(kubeconfig): - """Return an ApiClient to use for interacting with all K8s APIs.""" - return kubernetes.config.new_client_from_config( +def k8s_client(request, kubeconfig): + """Return a DynamicClient to use for interaction with all K8s APIs.""" + k8s_apiclient = kubernetes.config.new_client_from_config( config_file=kubeconfig, persist_config=False ) + return kubernetes.dynamic.DynamicClient(k8s_apiclient) @pytest.fixture -def k8s_client(request, k8s_apiclient): - """Parametrized fixture to instantiate a client for a single K8s API. - - By default, this will return a CoreV1Api client. - One can decorate a test function to use another API, like so: - - ``` - @pytest.mark.parametrize( - 'k8s_client', ['AppsV1Api'], indirect=True - ) - def test_something(k8s_client): - assert k8s_client.list_namespaced_deployment(namespace="default") - ``` - - FIXME: this is not working as of right now, since `pytest-bdd` manipulates - fixtures in its own way through the various scenario/when/then/given - decorators. - """ - api_name = getattr(request, "param", "CoreV1Api") - api_cls = getattr(kubernetes.client, api_name, None) - - if api_cls is None: - pytest.fail( - "Unknown K8s API '{}' to use with `k8s_client` fixture.".format(api_name) - ) - - return api_cls(api_client=k8s_apiclient) - - -@pytest.fixture -def admin_sa(k8s_client, k8s_apiclient): +def admin_sa(k8s_client): """Fixture to create a ServiceAccount which is bind to `cluster-admin` ClusterRole and return the ServiceAccount name """ - rbac_k8s_client = kubernetes.client.RbacAuthorizationV1Api(api_client=k8s_apiclient) + sa_k8s_client = k8s_client.resources.get(api_version="v1", kind="ServiceAccount") + crb_k8s_client = k8s_client.resources.get( + api_version="rbac.authorization.k8s.io/v1", kind="ClusterRoleBinding" + ) sa_name = "test-admin" sa_namespace = "default" sa_manifest = { @@ -168,14 +142,12 @@ def admin_sa(k8s_client, k8s_apiclient): ], } - k8s_client.create_namespaced_service_account( - body=sa_manifest, namespace=sa_namespace - ) - rbac_k8s_client.create_cluster_role_binding(body=crb_manifest) + sa_k8s_client.create(body=sa_manifest, namespace=sa_namespace) + crb_k8s_client.create(body=crb_manifest) def _check_crb_exists(): try: - rbac_k8s_client.read_cluster_role_binding(name=sa_name) + crb_k8s_client.get(name=sa_name) except kubernetes.client.rest.ApiException as err: if err.status == 404: raise AssertionError("ClusterRoleBinding not yet created") @@ -183,9 +155,7 @@ def _check_crb_exists(): def _check_sa_exists(): try: - sa_obj = k8s_client.read_namespaced_service_account( - name=sa_name, namespace=sa_namespace - ) + sa_obj = sa_k8s_client.get(name=sa_name, namespace=sa_namespace) except kubernetes.client.rest.ApiException as err: if err.status == 404: raise AssertionError("ServiceAccount not yet created") @@ -195,7 +165,7 @@ def _check_sa_exists(): assert sa_obj.secrets[0].name try: - secret_obj = k8s_client.read_namespaced_secret( + secret_obj = k8s_client.resources.get(api_version="v1", kind="Secret").get( sa_obj.secrets[0].name, sa_namespace ) except kubernetes.client.rest.ApiException as err: @@ -214,14 +184,14 @@ def _check_sa_exists(): yield (sa_name, sa_namespace) try: - rbac_k8s_client.delete_cluster_role_binding( + crb_k8s_client.delete( name=sa_name, body=kubernetes.client.V1DeleteOptions(propagation_policy="Foreground"), ) except kubernetes.client.rest.ApiException: pass - k8s_client.delete_namespaced_service_account( + sa_k8s_client.delete( name=sa_name, namespace=sa_namespace, body=kubernetes.client.V1DeleteOptions(propagation_policy="Foreground"), @@ -477,6 +447,10 @@ def _verify_kubeapi_service(host): def etcdctl(k8s_client, command, ssh_config): """Run an etcdctl command inside the etcd container.""" + # NOTE: We use Kubernetes client instead of DynamicClient as it + # ease the execution of command in a Pod + client = kubernetes.client.CoreV1Api(api_client=k8s_client.client) + name = "etcd-{}".format(utils.get_node_name("bootstrap", ssh_config)) etcd_command = [ @@ -491,7 +465,7 @@ def etcdctl(k8s_client, command, ssh_config): "/etc/kubernetes/pki/etcd/server.crt", ] + command output = kubernetes.stream.stream( - k8s_client.connect_get_namespaced_pod_exec, + client.connect_get_namespaced_pod_exec, name=name, namespace="kube-system", command=etcd_command, diff --git a/tests/install/steps/test_expansion.py b/tests/install/steps/test_expansion.py index 6b983060c0..d67d562748 100644 --- a/tests/install/steps/test_expansion.py +++ b/tests/install/steps/test_expansion.py @@ -30,7 +30,9 @@ def declare_node(host, ssh_config, version, k8s_client, node_type, node_name): """Declare the given node in Kubernetes.""" node_ip = get_node_ip(host, node_name, ssh_config) node_manifest = get_node_manifest(node_type, version, node_ip, node_name) - k8s_client.create_node(body=node_from_manifest(node_manifest)) + k8s_client.resources.get(api_version="v1", kind="Node").create( + body=node_from_manifest(node_manifest) + ) @when(parsers.parse('we deploy the node "{node_name}"')) @@ -72,7 +74,7 @@ def deploy_node(host, ssh_config, version, node_name): def check_node_is_registered(k8s_client, node_name): """Check if the given node is registered in Kubernetes.""" try: - k8s_client.read_node(node_name) + k8s_client.resources.get(api_version="v1", kind="Node").get(name=node_name) except k8s.client.rest.ApiException as exn: pytest.fail(str(exn)) @@ -83,7 +85,11 @@ def check_node_status(k8s_client, node_name, expected_status): def _check_node_status(): try: - status = k8s_client.read_node_status(node_name).status + status = ( + k8s_client.resources.get(api_version="v1", kind="Node") + .get(name=node_name) + .status + ) except k8s.client.rest.ApiException as exn: raise AssertionError(exn) # If really not ready, status may not have been pushed yet. diff --git a/tests/kube_utils.py b/tests/kube_utils.py index e052d5589d..cda0097bab 100644 --- a/tests/kube_utils.py +++ b/tests/kube_utils.py @@ -108,9 +108,7 @@ def get_pods( if label: kwargs["label_selector"] = label - if namespace: - return k8s_client.list_namespaced_pod(namespace=namespace, **kwargs).items - return k8s_client.list_pod_for_all_namespaces(**kwargs).items + return k8s_client.resources.get(api_version="v1", kind="Pod").get(**kwargs).items def check_pod_status(k8s_client, name, namespace="default", state="Running"): @@ -123,7 +121,9 @@ def check_pod_status(k8s_client, name, namespace="default", state="Running"): def _check_pod_status(): try: - pod = k8s_client.read_namespaced_pod(name=name, namespace=namespace) + pod = k8s_client.resources.get(api_version="v1", kind="Pod").get( + name=name, namespace=namespace + ) except ApiException as err: if err.status == 404: raise AssertionError("Pod not yet created") @@ -142,9 +142,19 @@ def _check_pod_status(): class Client(abc.ABC): """Helper class for manipulation of K8s resources in tests.""" - def __init__(self, k8s_client, kind, retry_count, retry_delay): - self._client = k8s_client + def __init__( + self, + k8s_client, + kind, + api_version="v1", + namespace=None, + retry_count=10, + retry_delay=2, + ): self._kind = kind + self._api_version = api_version + self._namespace = namespace + self._client = k8s_client.resources.get(api_version=api_version, kind=kind) self._count = retry_count self._delay = retry_delay @@ -202,10 +212,7 @@ def check_deletion_marker(self, name): def _check_deletion_marker(): obj = self.get(name) assert obj is not None, "{} {} not found".format(self._kind, name) - if isinstance(obj, dict): - tstamp = obj["metadata"].get("deletionTimestamp") - else: - tstamp = obj.metadata.deletion_timestamp + tstamp = obj["metadata"].get("deletionTimestamp") assert tstamp is not None, "{} {} is not marked for deletion".format( self._kind, name ) @@ -217,29 +224,26 @@ def _check_deletion_marker(): name="checking that {} {} is marked for deletion".format(self._kind, name), ) - @abc.abstractmethod def list(self): """Return a list of existing objects.""" - pass + return self._client.get(namespace=self._namespace).items - @abc.abstractmethod def _create(self, body): """Create a new object using the given body.""" - pass + return self._client.create(body=body, namespace=self._namespace) - @abc.abstractmethod def _get(self, name): """Return the object identified by `name`, raise if not found.""" - pass + return self._client.get(name=name, namespace=self._namespace) - @abc.abstractmethod def _delete(self, name): """Delete the object identified by `name`. The object may be simply marked for deletion and stay around for a while. """ - pass + body = kubernetes.client.V1DeleteOptions() + return self._client.delete(name=name, namespace=self._namespace, body=body) # }}} @@ -248,53 +252,29 @@ def _delete(self, name): class VolumeClient(Client): def __init__(self, k8s_client, ssh_config): - super().__init__(k8s_client, kind="Volume", retry_count=30, retry_delay=4) self._ssh_config = ssh_config - self._group = "storage.metalk8s.scality.com" - self._version = "v1alpha1" - self._plural = "volumes" - - def list(self): - return self._client.list_cluster_custom_object( - group=self._group, version=self._version, plural=self._plural - )["items"] + super().__init__( + k8s_client, + kind="Volume", + api_version="storage.metalk8s.scality.com/v1alpha1", + retry_count=30, + retry_delay=4, + ) def _create(self, body): # Fixup the node name. body["spec"]["nodeName"] = utils.get_node_name( body["spec"]["nodeName"], self._ssh_config ) - self._client.create_cluster_custom_object( - group=self._group, version=self._version, plural=self._plural, body=body - ) - - def _get(self, name): - return self._client.get_cluster_custom_object( - group=self._group, version=self._version, plural=self._plural, name=name - ) - - def _delete(self, name): - body = kubernetes.client.V1DeleteOptions() - self._client.delete_cluster_custom_object( - group=self._group, - version=self._version, - plural=self._plural, - name=name, - body=body, - grace_period_seconds=0, - ) + self._client.create(body=body) def wait_for_status(self, name, status, wait_for_device_name=False): def _wait_for_status(): volume = self.get(name) assert volume is not None, "Volume not found" - try: - actual_status = volume["status"] - except KeyError: - assert ( - status == "Unknown" - ), "Unexpected status: expected {}, got none".format(status) + actual_status = volume.get("status") + assert actual_status, f"Unexpected status expected {status}, got none" phase = self.compute_phase(actual_status) assert phase == status, "Unexpected status: expected {}, got {}".format( @@ -303,7 +283,7 @@ def _wait_for_status(): if wait_for_device_name: assert ( - "deviceName" in actual_status + "deviceName" in actual_status.keys() ), "Volume status.deviceName has not been reconciled" return volume @@ -345,24 +325,7 @@ def get_error(volume_status): class PersistentVolumeClient(Client): def __init__(self, k8s_client): - super().__init__( - k8s_client, kind="PersistentVolume", retry_count=10, retry_delay=2 - ) - - def list(self): - return self._client.list_persistent_volume().items - - def _create(self, body): - self._client.create_persistent_volume(body=body) - - def _get(self, name): - return self._client.read_persistent_volume(name) - - def _delete(self, name): - body = kubernetes.client.V1DeleteOptions() - self._client.delete_persistent_volume( - name=name, body=body, grace_period_seconds=0 - ) + super().__init__(k8s_client, kind="PersistentVolume") # }}} @@ -371,42 +334,19 @@ def _delete(self, name): class PersistentVolumeClaimClient(Client): def __init__(self, k8s_client, namespace="default"): - super().__init__( - k8s_client, kind="PersistentVolumeClaim", retry_count=10, retry_delay=2 - ) - self._namespace = namespace + super().__init__(k8s_client, kind="PersistentVolumeClaim", namespace=namespace) def create_for_volume(self, volume, pv): """Create a PVC matching the given volume.""" assert pv is not None, "PersistentVolume {} not found".format(volume) body = PVC_TEMPLATE.format( volume_name=volume, - storage_class=pv.spec.storage_class_name, - access=pv.spec.access_modes[0], + storage_class=pv.spec.storageClassName, + access=pv.spec.accessModes[0], size=pv.spec.capacity["storage"], ) self.create_from_yaml(body) - def list(self): - return self._client.list_namespaced_persistent_volume_claim( - namespace=self._namespace - ).items - - def _create(self, body): - self._client.create_namespaced_persistent_volume_claim( - namespace=self._namespace, body=body - ) - - def _get(self, name): - return self._client.read_namespaced_persistent_volume_claim( - name=name, namespace=self._namespace - ) - - def _delete(self, name): - self._client.delete_namespaced_persistent_volume_claim( - name=name, namespace=self._namespace, grace_period_seconds=0 - ) - # }}} # PodClient {{{ @@ -414,9 +354,9 @@ def _delete(self, name): class PodClient(Client): def __init__(self, k8s_client, image, namespace="default"): - super().__init__(k8s_client, kind="Pod", retry_count=30, retry_delay=2) + super().__init__(k8s_client, kind="Pod", namespace=namespace, retry_count=30) + self._k8s_client = k8s_client self._image = image - self._namespace = namespace def create_with_volume(self, volume_name, command): """Create a pod using the specified volume.""" @@ -431,25 +371,15 @@ def create_with_volume(self, volume_name, command): # Wait for the Pod to be up and running. pod_name = "{}-pod".format(volume_name) utils.retry( - check_pod_status(self._client, pod_name), + check_pod_status(self._k8s_client, pod_name), times=self._count, wait=self._delay, name="wait for pod {}".format(pod_name), ) - def list(self): - return self._client.list_namespaced_pod(namespace=self._namespace).items - - def _create(self, body): - self._client.create_namespaced_pod(namespace=self._namespace, body=body) - - def _get(self, name): - return self._client.read_namespaced_pod(name=name, namespace=self._namespace) - def _delete(self, name): - self._client.delete_namespaced_pod( - name=name, namespace=self._namespace, grace_period_seconds=0 - ) + body = kubernetes.client.V1DeleteOptions(grace_period_seconds=0) + return self._client.delete(name=name, namespace=self._namespace, body=body) # }}} @@ -458,19 +388,12 @@ def _delete(self, name): class StorageClassClient(Client): def __init__(self, k8s_client): - super().__init__(k8s_client, kind="StorageClass", retry_count=10, retry_delay=2) - - def list(self): - return self._client.list_storage_class().items - - def _create(self, body): - self._client.create_storage_class(body=body) - - def _get(self, name): - return self._client.read_storage_class(name=name) - - def _delete(self, name): - self._client.delete_storage_class(name=name, grace_period_seconds=0) + super().__init__( + k8s_client, + kind="StorageClass", + api_version="storage.k8s.io/v1", + retry_count=10, + ) # }}} diff --git a/tests/post/features/service_configuration.feature b/tests/post/features/service_configuration.feature index a792c92c6b..f885c3a476 100644 --- a/tests/post/features/service_configuration.feature +++ b/tests/post/features/service_configuration.feature @@ -8,7 +8,7 @@ Feature: Cluster and Services Configurations When we update the CSC 'spec.deployment.replicas' to '3' And we apply the 'metalk8s.addons.dex.deployed' state And we wait for the rollout of 'deploy/dex' in namespace 'metalk8s-auth' to complete - Then we have '3' at 'status.available_replicas' for 'dex' Deployment in namespace 'metalk8s-auth' + Then we have '3' at 'status.availableReplicas' for 'dex' Deployment in namespace 'metalk8s-auth' Scenario: Update Admin static user password Given the Kubernetes API is available diff --git a/tests/post/steps/conftest.py b/tests/post/steps/conftest.py index 4bfb5ec93c..fa3974c814 100644 --- a/tests/post/steps/conftest.py +++ b/tests/post/steps/conftest.py @@ -12,10 +12,8 @@ @pytest.fixture -def volume_client(k8s_apiclient, ssh_config): - return kube_utils.VolumeClient( - CustomObjectsApi(api_client=k8s_apiclient), ssh_config - ) +def volume_client(k8s_client, ssh_config): + return kube_utils.VolumeClient(k8s_client, ssh_config) @pytest.fixture @@ -34,8 +32,8 @@ def pod_client(k8s_client, utils_image): @pytest.fixture -def sc_client(k8s_apiclient): - return kube_utils.StorageClassClient(StorageV1Api(api_client=k8s_apiclient)) +def sc_client(k8s_client): + return kube_utils.StorageClassClient(k8s_client) # }}} @@ -119,7 +117,7 @@ def test_volume(volume_client, name): @given("we are on a multi node cluster") def check_multi_node(k8s_client): - nodes = k8s_client.list_node() + nodes = k8s_client.resources.get(api_version="v1", kind="Node").get() if len(nodes.items) == 1: pytest.skip("We skip single node cluster for this test") diff --git a/tests/post/steps/test_authentication.py b/tests/post/steps/test_authentication.py index 670d71afef..97c4ca556e 100644 --- a/tests/post/steps/test_authentication.py +++ b/tests/post/steps/test_authentication.py @@ -80,7 +80,7 @@ def _wait_for_ingress_pod_and_container(): for pod in pods: assert all( - container.ready == True for container in pod.status.container_statuses + container.ready == True for container in pod.status.containerStatuses ) utils.retry( diff --git a/tests/post/steps/test_dns.py b/tests/post/steps/test_dns.py index 3ee9027533..0d8edf0ddb 100644 --- a/tests/post/steps/test_dns.py +++ b/tests/post/steps/test_dns.py @@ -21,7 +21,9 @@ def utils_pod(k8s_client, utils_image): manifest["spec"]["containers"][0]["image"] = utils_image pod_name = manifest["metadata"]["name"] - k8s_client.create_namespaced_pod(body=manifest, namespace="default") + pod_k8s_client = k8s_client.resources.get(api_version="v1", kind="Pod") + + pod_k8s_client.create(body=manifest, namespace="default") # Wait for the Pod to be ready utils.retry( @@ -36,7 +38,7 @@ def utils_pod(k8s_client, utils_image): yield pod_name # Clean-up resources - k8s_client.delete_namespaced_pod( + pod_k8s_client.delete( name=pod_name, namespace="default", body=client.V1DeleteOptions( diff --git a/tests/post/steps/test_ingress.py b/tests/post/steps/test_ingress.py index 4b7ee65cf2..313ef41284 100644 --- a/tests/post/steps/test_ingress.py +++ b/tests/post/steps/test_ingress.py @@ -52,8 +52,8 @@ def context(): def teardown(context, host, ssh_config, version, k8s_client): yield if "node_to_uncordon" in context: - k8s_client.patch_node( - context["node_to_uncordon"], {"spec": {"unschedulable": False}} + k8s_client.resources.get(api_version="v1", kind="Node").patch( + name=context["node_to_uncordon"], body={"spec": {"unschedulable": False}} ) if "bootstrap_to_restore" in context: @@ -146,16 +146,19 @@ def stop_cp_ingress_vip_node(context, k8s_client): context["node_to_uncordon"] = node_name # Cordon node - k8s_client.patch_node(node_name, {"spec": {"unschedulable": True}}) + k8s_client.resources.get(api_version="v1", kind="Node").patch( + name=node_name, body={"spec": {"unschedulable": True}} + ) + pod_k8s_client = k8s_client.resources.get(api_version="v1", kind="Pod") # Delete Control Plane Ingress Controller from node - cp_ingress_pods = k8s_client.list_namespaced_pod( - "metalk8s-ingress", + cp_ingress_pods = pod_k8s_client.get( + namespace="metalk8s-ingress", label_selector="app.kubernetes.io/instance=ingress-nginx-control-plane", field_selector="spec.nodeName={}".format(node_name), ) for pod in cp_ingress_pods.items: - k8s_client.delete_namespaced_pod(pod.metadata.name, pod.metadata.namespace) + pod_k8s_client.delete(name=pod.metadata.name, namespace=pod.metadata.namespace) @when(parsers.parse("we set control plane ingress IP to node '{node_name}' IP")) @@ -230,8 +233,8 @@ def get_node_hosting_cp_ingress_vip(k8s_client): "involvedObject.kind=Service", "involvedObject.name=ingress-nginx-control-plane-controller", ] - events = k8s_client.list_namespaced_event( - "metalk8s-ingress", + events = k8s_client.resources.get(api_version="v1", kind="Event").get( + namespace="metalk8s-ingress", field_selector=",".join(field_selectors), ) @@ -239,7 +242,7 @@ def get_node_hosting_cp_ingress_vip(k8s_client): match = None for event in sorted( - events.items, key=lambda event: event.last_timestamp, reverse=True + events.items, key=lambda event: event.lastTimestamp, reverse=True ): match = re.search(r'announcing from node "(?P.+)"', event.message) if match is not None: diff --git a/tests/post/steps/test_logging.py b/tests/post/steps/test_logging.py index 40afb9b07c..2fa1b531d0 100644 --- a/tests/post/steps/test_logging.py +++ b/tests/post/steps/test_logging.py @@ -1,10 +1,11 @@ +import datetime import os import pathlib import time import uuid import yaml -from kubernetes import client +import kubernetes.client import pytest from pytest_bdd import scenario, given, when, then, parsers @@ -58,8 +59,11 @@ def test_logging_pipeline_is_working(host): @given("the Loki API is available") def check_loki_api(k8s_client): def _check_loki_ready(): + # NOTE: We use Kubernetes client instead of DynamicClient as it + # ease the "service proxy path" + client = kubernetes.client.CoreV1Api(api_client=k8s_client.client) try: - response = k8s_client.connect_get_namespaced_service_proxy_with_path( + response = client.connect_get_namespaced_service_proxy_with_path( "loki:http-metrics", "metalk8s-logging", path="ready" ) except Exception as exc: # pylint: disable=broad-except @@ -81,8 +85,13 @@ def set_up_logger_pod(k8s_client, utils_image): name = manifest["metadata"]["name"] namespace = manifest["metadata"]["namespace"] - result = k8s_client.create_namespaced_pod(body=manifest, namespace=namespace) - pod_creation_ts = int(result.metadata.creation_timestamp.timestamp()) + pod_k8s_client = k8s_client.resources.get(api_version="v1", kind="Pod") + result = pod_k8s_client.create(body=manifest, namespace=namespace) + pod_creation_ts = int( + datetime.datetime.strptime( + result.metadata.creationTimestamp, "%Y-%m-%dT%H:%M:%SZ" + ).timestamp() + ) utils.retry( kube_utils.check_pod_status( @@ -98,10 +107,10 @@ def set_up_logger_pod(k8s_client, utils_image): yield pod_creation_ts - k8s_client.delete_namespaced_pod( + pod_k8s_client.delete( name=name, namespace=namespace, - body=client.V1DeleteOptions( + body=kubernetes.client.V1DeleteOptions( grace_period_seconds=0, ), ) @@ -131,7 +140,7 @@ def push_log_to_loki(k8s_client, context): } ] } - response = k8s_client.api_client.call_api( + response = k8s_client.client.call_api( "/api/v1/namespaces/{namespace}/services/{name}/proxy/{path}", "POST", path_params, @@ -236,7 +245,7 @@ def query_loki_api(k8s_client, content, route="query"): "namespace": "metalk8s-logging", "path": "loki/api/v1/{0}".format(route), } - response = k8s_client.api_client.call_api( + response = k8s_client.client.call_api( "/api/v1/namespaces/{namespace}/services/{name}/proxy/{path}", "GET", path_params, diff --git a/tests/post/steps/test_monitoring.py b/tests/post/steps/test_monitoring.py index e3a6fe46ea..958517174d 100644 --- a/tests/post/steps/test_monitoring.py +++ b/tests/post/steps/test_monitoring.py @@ -3,7 +3,6 @@ import random import string -import kubernetes.client from kubernetes.client.rest import ApiException import pytest @@ -104,12 +103,14 @@ def check_grafana_api(grafana_api): @given(parsers.parse("the '{name}' APIService exists")) -def apiservice_exists(host, name, k8s_apiclient, request): - client = kubernetes.client.ApiregistrationV1Api(api_client=k8s_apiclient) +def apiservice_exists(host, name, k8s_client, request): + client = k8s_client.resources.get( + api_version="apiregistration.k8s.io/v1", kind="APIService" + ) def _check_object_exists(): try: - _ = client.read_api_service(name) + _ = client.get(name=name) except ApiException as err: if err.status == 404: raise AssertionError("APIService not yet created") @@ -156,12 +157,14 @@ def _wait_job_status(): @then(parsers.parse("the '{name}' APIService is {condition}")) -def apiservice_condition_met(name, condition, k8s_apiclient): - client = kubernetes.client.ApiregistrationV1Api(api_client=k8s_apiclient) +def apiservice_condition_met(name, condition, k8s_client): + client = k8s_client.resources.get( + api_version="apiregistration.k8s.io/v1", kind="APIService" + ) def _check_object_exists(): try: - svc = client.read_api_service(name) + svc = client.get(name=name) ok = False for cond in svc.status.conditions: @@ -183,9 +186,9 @@ def _check_object_exists(): @then( parsers.parse("a pod with label '{label}' in namespace '{namespace}' has metrics") ) -def pod_has_metrics(label, namespace, k8s_apiclient): +def pod_has_metrics(label, namespace, k8s_client): def _pod_has_metrics(): - result = k8s_apiclient.call_api( + result = k8s_client.client.call_api( resource_path="/apis/metrics.k8s.io/v1beta1/" "namespaces/{namespace}/pods", method="GET", response_type=object, @@ -210,9 +213,9 @@ def _pod_has_metrics(): @then(parsers.parse("a node with label '{label}' has metrics")) -def node_has_metrics(label, k8s_apiclient): +def node_has_metrics(label, k8s_client): def _node_has_metrics(): - result = k8s_apiclient.call_api( + result = k8s_client.client.call_api( resource_path="/apis/metrics.k8s.io/v1beta1/nodes", method="GET", response_type=object, diff --git a/tests/post/steps/test_network.py b/tests/post/steps/test_network.py index 84c34dd3ad..936431d5de 100644 --- a/tests/post/steps/test_network.py +++ b/tests/post/steps/test_network.py @@ -13,7 +13,7 @@ def test_all_listening_processes(host): @given("we run on an untainted single node") def running_on_single_node_untainted(k8s_client): - nodes = k8s_client.list_node() + nodes = k8s_client.resources.get(api_version="v1", kind="Node").get() if len(nodes.items) != 1: pytest.skip("We skip multi nodes clusters for this test") diff --git a/tests/post/steps/test_salt_api.py b/tests/post/steps/test_salt_api.py index 7615ead4de..2d0db31b98 100644 --- a/tests/post/steps/test_salt_api.py +++ b/tests/post/steps/test_salt_api.py @@ -184,10 +184,10 @@ def have_no_perms(host, context): def _login_salt_api_sa(address, k8s_client, name, namespace, username=None): - service_account = k8s_client.read_namespaced_service_account( - name=name, namespace=namespace - ) - secret = k8s_client.read_namespaced_secret( + service_account = k8s_client.resources.get( + api_version="v1", kind="ServiceAccount" + ).get(name=name, namespace=namespace) + secret = k8s_client.resources.get(api_version="v1", kind="Secret").get( name=service_account.secrets[0].name, namespace=namespace ) token = base64.decodebytes(secret.data["token"].encode("utf-8")) diff --git a/tests/post/steps/test_sanity.py b/tests/post/steps/test_sanity.py index b003e28997..041107abf7 100644 --- a/tests/post/steps/test_sanity.py +++ b/tests/post/steps/test_sanity.py @@ -1,3 +1,4 @@ +import kubernetes.client from kubernetes.client import AppsV1Api from kubernetes.client.rest import ApiException import pytest @@ -6,15 +7,6 @@ from tests import kube_utils from tests import utils -# Fixtures {{{ - - -@pytest.fixture -def apps_client(k8s_apiclient): - return AppsV1Api(api_client=k8s_apiclient) - - -# }}} # Scenarios {{{ @@ -142,8 +134,12 @@ def _wait_for_pod(): _wait_for_pod, times=10, wait=3, name="wait for pod labeled '{}'".format(label) ) + # NOTE: We use Kubernetes client instead of DynamicClient as it + # ease the retrieving of Pod logs + client = kubernetes.client.CoreV1Api(api_client=k8s_client.client) + for container in pod.spec.containers: - logs = k8s_client.read_namespaced_pod_log( + logs = client.read_namespaced_pod_log( pod.metadata.name, namespace, container=container.name ) @@ -154,11 +150,12 @@ def _wait_for_pod(): @then("the static Pod in the namespace runs on nodes") def check_static_pod(k8s_client, name, namespace, role): + node_k8s_client = k8s_client.resources.get(api_version="v1", kind="Node") if role == "all": - nodes = k8s_client.list_node() + nodes = node_k8s_client.get() else: role_label = "node-role.kubernetes.io/{}=".format(role) - nodes = k8s_client.list_node(label_selector=role_label) + nodes = node_k8s_client.get(label_selector=role_label) pod_names = ["{}-{}".format(name, node.metadata.name) for node in nodes.items] for pod_name in pod_names: @@ -185,18 +182,18 @@ def check_static_pod(k8s_client, name, namespace, role): "replicas available" ) ) -def check_deployment(apps_client, name, namespace): +def check_deployment(k8s_client, name, namespace): def _wait_for_deployment(): try: - deploy = apps_client.read_namespaced_deployment( - name=name, namespace=namespace - ) + deploy = k8s_client.resources.get( + api_version="apps/v1", kind="Deployment" + ).get(name=name, namespace=namespace) except ApiException as exc: if exc.status == 404: pytest.fail("Deployment '{}/{}' does not exist".format(namespace, name)) raise - assert deploy.spec.replicas == deploy.status.available_replicas, ( + assert deploy.spec.replicas == deploy.status.availableReplicas, ( "Deployment is not ready yet (desired={desired}, " "available={status.available_replicas}, " "unavailable={status.unavailable_replicas})" @@ -217,12 +214,12 @@ def _wait_for_deployment(): "Pods ready" ) ) -def check_daemonset(apps_client, name, namespace): +def check_daemonset(k8s_client, name, namespace): def _wait_for_daemon_set(): try: - daemon_set = apps_client.read_namespaced_daemon_set( - name=name, namespace=namespace - ) + daemon_set = k8s_client.resources.get( + api_version="apps/v1", kind="DaemonSet" + ).get(name=name, namespace=namespace) except ApiException as exc: if exc.status == 404: pytest.fail("DaemonSet '{}/{}' does not exist".format(namespace, name)) @@ -250,12 +247,12 @@ def _wait_for_daemon_set(): "the StatefulSet in the namespace has all desired " "replicas available" ) -def check_statefulset(apps_client, name, namespace): +def check_statefulset(k8s_client, name, namespace): def _wait_for_stateful_set(): try: - stateful_set = apps_client.read_namespaced_stateful_set( - name=name, namespace=namespace - ) + stateful_set = k8s_client.resources.get( + api_version="apps/v1", kind="StatefulSet" + ).get(name=name, namespace=namespace) except ApiException as exc: if exc.status == 404: pytest.fail( @@ -264,10 +261,10 @@ def _wait_for_stateful_set(): raise desired = stateful_set.spec.replicas - ready = stateful_set.status.ready_replicas + ready = stateful_set.status.readyReplicas assert desired == ready, ( "StatefulSet is not ready yet (desired={}, ready={})" - ).format(desired, available) + ).format(desired, ready) utils.retry( _wait_for_stateful_set, diff --git a/tests/post/steps/test_seccomp.py b/tests/post/steps/test_seccomp.py index 034b325b0c..5e694bf5f3 100644 --- a/tests/post/steps/test_seccomp.py +++ b/tests/post/steps/test_seccomp.py @@ -47,12 +47,14 @@ def utils_pod(k8s_client, utils_image): "test": "seccomp1", } - k8s_client.create_namespaced_pod(body=manifest, namespace="default") + pod_k8s_client = k8s_client.resources.get(api_version="v1", kind="Pod") + + pod_k8s_client.create(body=manifest, namespace="default") try: yield pod_name finally: - k8s_client.delete_namespaced_pod( + pod_k8s_client.delete( name=pod_name, namespace="default", body=client.V1DeleteOptions( diff --git a/tests/post/steps/test_service_configuration.py b/tests/post/steps/test_service_configuration.py index fe5070fb79..94048b82d0 100644 --- a/tests/post/steps/test_service_configuration.py +++ b/tests/post/steps/test_service_configuration.py @@ -5,8 +5,6 @@ import pytest from pytest_bdd import scenario, given, then, when, parsers -from kubernetes.client import AppsV1Api - from tests import utils # Scenarios {{{ @@ -88,7 +86,7 @@ def apply_csc(csc, state): ) ) def get_deployments( - k8s_apiclient, + k8s_client, value, path, deployment, @@ -96,10 +94,11 @@ def get_deployments( ): def _wait_for_deployment(): try: - k8s_appsv1_client = AppsV1Api(api_client=k8s_apiclient) - response = k8s_appsv1_client.read_namespaced_deployment( - name=deployment, namespace=namespace - ).to_dict() + response = ( + k8s_client.resources.get(api_version="apps/v1", kind="Deployment") + .get(name=deployment, namespace=namespace) + .to_dict() + ) except Exception as exc: pytest.fail( "Unable to read {} Deployment with error: {!s}".format(deployment, exc) @@ -164,7 +163,7 @@ class ClusterServiceConfiguration: CSC_KEY = "config.yaml" def __init__(self, k8s_client, name, namespace, host, ssh_config, version): - self.client = k8s_client + self.client = k8s_client.resources.get(api_version="v1", kind="ConfigMap") self.name = name self.namespace = namespace self.host = host @@ -178,7 +177,7 @@ def get(self): return self.csc try: - response = self.client.read_namespaced_config_map(self.name, self.namespace) + response = self.client.get(name=self.name, namespace=self.namespace) except Exception as exc: raise ClusterServiceConfigurationError( "Unable to read {} ConfigMap in namespace {} with error: {!s}".format( @@ -225,7 +224,7 @@ def update(self, config, apply_config=True): } try: - self.client.patch_namespaced_config_map(self.name, self.namespace, patch) + self.client.patch(name=self.name, namespace=self.namespace, body=patch) except Exception as exc: raise ClusterServiceConfigurationError( "Unable to patch ConfigMap {} in namespace {} with error {!s}".format( diff --git a/tests/post/steps/test_solutions.py b/tests/post/steps/test_solutions.py index ed17d43d67..a4c53b2128 100644 --- a/tests/post/steps/test_solutions.py +++ b/tests/post/steps/test_solutions.py @@ -7,8 +7,6 @@ import pytest from pytest_bdd import scenario, given, then, when, parsers -import kubernetes.client -from kubernetes.client import AppsV1Api from kubernetes.client.rest import ApiException from tests import kube_utils @@ -53,7 +51,7 @@ def test_deploy_solution(host): @given(parsers.parse("no Solution '{name}' is imported")) -def is_absent_solution(host, name, k8s_client): +def is_absent_solution(host, name): with host.sudo(): assert ( name not in host.mount_point.get_mountpoints() @@ -285,7 +283,7 @@ def read_solution_environment(k8s_client, name): @then(parsers.parse("we have no Solution '{name}' archive mounted")) -def no_solution_mountpoint(host, name, k8s_client): +def no_solution_mountpoint(host, name): with host.sudo(): assert ( name not in host.mount_point.get_mountpoints() @@ -323,7 +321,11 @@ def no_solution_config(host): def get_configmap(k8s_client, name, namespace): try: - response = k8s_client.read_namespaced_config_map(name, namespace).to_dict() + response = ( + k8s_client.resources.get(api_version="v1", kind="ConfigMap") + .get(name=name, namespace=namespace) + .to_dict() + ) except Exception as exc: if isinstance(exc, ApiException) and exc.status == 404: return None @@ -333,7 +335,7 @@ def get_configmap(k8s_client, name, namespace): def get_environment(k8s_client, name): try: - response = k8s_client.list_namespace( + response = k8s_client.resources.get(api_version="v1", kind="Namespace").get( label_selector="{}={}".format(ENVIRONMENT_LABEL, name) ) except (ApiException) as exc: diff --git a/tests/post/steps/test_static_pods.py b/tests/post/steps/test_static_pods.py index 5ba899e1ac..ce4778011a 100644 --- a/tests/post/steps/test_static_pods.py +++ b/tests/post/steps/test_static_pods.py @@ -106,7 +106,9 @@ def set_up_static_pod(host, nodename, k8s_client, utils_image, transient_files): name="wait for Pod '{}'".format(fullname), ) - pod = k8s_client.read_namespaced_pod(name=fullname, namespace="default") + pod = k8s_client.resources.get(api_version="v1", kind="Pod").get( + name=fullname, namespace="default" + ) return pod.metadata.uid @@ -150,7 +152,9 @@ def wait_for_pod_reloaded(): name="wait for Pod '{}' to be reloaded".format(fullname), ) - pod = k8s_client.read_namespaced_pod(name=fullname, namespace="default") + pod = k8s_client.resources.get(api_version="v1", kind="Pod").get( + name=fullname, namespace="default" + ) assert pod.metadata.uid != static_pod_id diff --git a/tests/post/steps/test_versions.py b/tests/post/steps/test_versions.py index 3e904d7541..537041c21b 100644 --- a/tests/post/steps/test_versions.py +++ b/tests/post/steps/test_versions.py @@ -1,4 +1,3 @@ -from kubernetes.client import VersionApi from pytest_bdd import scenario, then from tests import versions @@ -12,12 +11,11 @@ def test_cluster_version(host): # Then @then("the Kubernetes version deployed is the same as the configured one") -def check_kubernetes_version(k8s_apiclient): +def check_kubernetes_version(k8s_client): # NOTE: the `vX.Y.Z` format is used by Kubernetes, not our buildchain configured_version = "v{}".format(versions.K8S_VERSION) - k8s_client = VersionApi(api_client=k8s_apiclient) - observed_version = k8s_client.get_code().git_version + observed_version = k8s_client.version["kubernetes"]["gitVersion"] assert configured_version == observed_version, ( "The running version of Kubernetes is '{}', while the expected version" diff --git a/tests/post/steps/test_volume.py b/tests/post/steps/test_volume.py index 8c6c374ca9..0b40cff3c4 100644 --- a/tests/post/steps/test_volume.py +++ b/tests/post/steps/test_volume.py @@ -301,7 +301,7 @@ def _check_pv_label(): pv = pv_client.get(name) assert pv is not None, "PersistentVolume {} not found".format(name) labels = pv.metadata.labels - assert key in labels, "Label {} is missing".format(key) + assert key in labels.keys(), "Label {} is missing".format(key) assert ( labels[key] == value ), "Unexpected value for label {}: expected {}, got {}".format( @@ -388,10 +388,14 @@ def check_volume_deletion_marker(name, volume_client): def check_file_content_inside_pod(volume_name, path, content, k8s_client): name = "{}-pod".format(volume_name) + # NOTE: We use Kubernetes client instead of DynamicClient as it + # ease the execution of command in a Pod + client = k8s.client.CoreV1Api(api_client=k8s_client.client) + def _check_file_content(): try: result = k8s.stream.stream( - k8s_client.connect_get_namespaced_pod_exec, + client.connect_get_namespaced_pod_exec, name=name, namespace="default", command=["cat", path], @@ -420,7 +424,9 @@ def _check_file_content(): def check_storage_is_created(context, host, name): volume = context.get(name) assert volume is not None, "volume {} not found in context".format(name) - assert "sparseLoopDevice" in volume["spec"], "unsupported volume type for this step" + assert ( + "sparseLoopDevice" in volume["spec"].keys() + ), "unsupported volume type for this step" uuid = volume["metadata"]["uid"] capacity = volume["spec"]["sparseLoopDevice"]["size"] # Check that the sparse file exists and has the proper size. @@ -438,7 +444,9 @@ def check_storage_is_created(context, host, name): def check_storage_is_deleted(context, host, name): volume = context.get(name) assert volume is not None, "volume {} not found in context".format(name) - assert "sparseLoopDevice" in volume["spec"], "unsupported volume type for this step" + assert ( + "sparseLoopDevice" in volume["spec"].keys() + ), "unsupported volume type for this step" uuid = volume["metadata"]["uid"] # Check that the sparse file is deleted. path = "/var/lib/metalk8s/storage/sparse/{}".format(uuid)