diff --git a/lib/ansible/module_utils/k8s/raw.py b/lib/ansible/module_utils/k8s/raw.py index 22e40f8c1bc..80659f23a64 100644 --- a/lib/ansible/module_utils/k8s/raw.py +++ b/lib/ansible/module_utils/k8s/raw.py @@ -66,6 +66,14 @@ class KubernetesRawModule(KubernetesAnsibleModule): strict=dict(type='bool', default=True) ) + @property + def condition_spec(self): + return dict( + type=dict(), + status=dict(default=True, choices=[True, False, "Unknown"]), + reason=dict() + ) + @property def argspec(self): argument_spec = copy.deepcopy(COMMON_ARG_SPEC) @@ -73,6 +81,7 @@ class KubernetesRawModule(KubernetesAnsibleModule): argument_spec['merge_type'] = dict(type='list', choices=['json', 'merge', 'strategic-merge']) argument_spec['wait'] = dict(type='bool', default=False) argument_spec['wait_timeout'] = dict(type='int', default=120) + argument_spec['wait_condition'] = dict(type='dict', default=None, options=self.condition_spec) argument_spec['validate'] = dict(type='dict', default=None, options=self.validate_spec) argument_spec['append_hash'] = dict(type='bool', default=False) return argument_spec @@ -211,6 +220,9 @@ class KubernetesRawModule(KubernetesAnsibleModule): existing = None wait = self.params.get('wait') wait_timeout = self.params.get('wait_timeout') + wait_condition = None + if self.params.get('wait_condition') and self.params['wait_condition'].get('type'): + wait_condition = self.params['wait_condition'] self.remove_aliases() @@ -280,7 +292,7 @@ class KubernetesRawModule(KubernetesAnsibleModule): success = True result['result'] = k8s_obj if wait and not self.check_mode: - success, result['result'], result['duration'] = self.wait(resource, definition, wait_timeout) + success, result['result'], result['duration'] = self.wait(resource, definition, wait_timeout, condition=wait_condition) result['changed'] = True result['method'] = 'create' if not success: @@ -305,7 +317,7 @@ class KubernetesRawModule(KubernetesAnsibleModule): success = True result['result'] = k8s_obj if wait: - success, result['result'], result['duration'] = self.wait(resource, definition, wait_timeout) + success, result['result'], result['duration'] = self.wait(resource, definition, wait_timeout, condition=wait_condition) match, diffs = self.diff_objects(existing.to_dict(), result['result'].to_dict()) result['changed'] = not match result['method'] = 'replace' @@ -333,7 +345,7 @@ class KubernetesRawModule(KubernetesAnsibleModule): success = True result['result'] = k8s_obj if wait: - success, result['result'], result['duration'] = self.wait(resource, definition, wait_timeout) + success, result['result'], result['duration'] = self.wait(resource, definition, wait_timeout, condition=wait_condition) match, diffs = self.diff_objects(existing.to_dict(), result['result']) result['result'] = k8s_obj result['changed'] = not match @@ -398,7 +410,7 @@ class KubernetesRawModule(KubernetesAnsibleModule): response = response.to_dict() return False, response, _wait_for_elapsed() - def wait(self, resource, definition, timeout, state='present'): + def wait(self, resource, definition, timeout, state='present', condition=None): def _deployment_ready(deployment): # FIXME: frustratingly bool(deployment.status) is True even if status is empty @@ -416,6 +428,23 @@ class KubernetesRawModule(KubernetesAnsibleModule): daemonset.status.numberReady == daemonset.status.desiredNumberScheduled and daemonset.status.observedGeneration == daemonset.metadata.generation) + def _custom_condition(resource): + if not resource.status or not resource.status.conditions: + return False + match = [x for x in resource.status.conditions if x.type == condition['type']] + if not match: + return False + # There should never be more than one condition of a specific type + match = match[0] + if match.status == 'Unknown': + return False + status = True if match.status == 'True' else False + if status == condition['status']: + if condition.get('reason'): + return match.reason == condition['reason'] + return True + return False + def _resource_absent(resource): return not resource @@ -425,8 +454,10 @@ class KubernetesRawModule(KubernetesAnsibleModule): Pod=_pod_ready ) kind = definition['kind'] - if state == 'present': - predicate = waiter.get(kind, lambda x: True) + if state == 'present' and not condition: + predicate = waiter.get(kind, lambda x: x) + elif state == 'present' and condition: + predicate = _custom_condition else: predicate = _resource_absent return self._wait_for(resource, definition['metadata']['name'], definition['metadata'].get('namespace'), predicate, timeout, state) diff --git a/lib/ansible/modules/clustering/k8s/k8s.py b/lib/ansible/modules/clustering/k8s/k8s.py index 2bf9e9f96d2..8ccfbb37624 100644 --- a/lib/ansible/modules/clustering/k8s/k8s.py +++ b/lib/ansible/modules/clustering/k8s/k8s.py @@ -64,7 +64,7 @@ options: - Whether to wait for certain resource kinds to end up in the desired state. By default the module exits once Kubernetes has received the request - Implemented for C(state=present) for C(Deployment), C(DaemonSet) and C(Pod), and for C(state=absent) for all resource kinds. - - For resource kinds without an implementation, C(wait) returns immediately. + - For resource kinds without an implementation, C(wait) returns immediately unless C(wait_condition) is set. default: no type: bool version_added: "2.8" @@ -73,6 +73,31 @@ options: - How long in seconds to wait for the resource to end up in the desired state. Ignored if C(wait) is not set. default: 120 version_added: "2.8" + wait_condition: + description: + - Specifies a custom condition on the status to wait for. Ignored if C(wait) is not set or is set to False. + suboptions: + type: + description: + - The type of condition to wait for. For example, the C(Pod) resource will set the C(Ready) condition (among others) + - Required if you are specifying a C(wait_condition). If left empty, the C(wait_condition) field will be ignored. + - The possible types for a condition are specific to each resource type in Kubernetes. See the API documentation of the status field + for a given resource to see possible choices. + status: + description: + - The value of the status field in your desired condition. + - For example, if a C(Deployment) is paused, the C(Progressing) C(type) will have the C(Unknown) status. + choices: + - True + - False + - Unknown + reason: + description: + - The value of the reason field in your desired condition + - For example, if a C(Deployment) is paused, The C(Progressing) c(type) will have the C(DeploymentPaused) reason. + - The possible reasons in a condition are specific to each resource type in Kubernetes. See the API documentation of the status field + for a given resource to see possible choices. + version_added: "2.8" validate: description: - how (if at all) to validate the resource definition against the kubernetes schema. diff --git a/test/integration/targets/k8s/playbooks/roles/k8s/tasks/waiter.yml b/test/integration/targets/k8s/playbooks/roles/k8s/tasks/waiter.yml index cbb6f825425..2da7cfb4439 100644 --- a/test/integration/targets/k8s/playbooks/roles/k8s/tasks/waiter.yml +++ b/test/integration/targets/k8s/playbooks/roles/k8s/tasks/waiter.yml @@ -242,6 +242,31 @@ - deploy.result.status.availableReplicas == deploy.result.status.replicas - updated_deploy_pods.resources[0].spec.containers[0].image.endswith(":2") + - name: pause a deployment + k8s: + definition: + apiVersion: extensions/v1beta1 + kind: Deployment + metadata: + name: wait-deploy + namespace: "{{ wait_namespace }}" + spec: + paused: True + wait: yes + wait_condition: + type: Progressing + status: Unknown + reason: DeploymentPaused + register: pause_deploy + + - name: check that paused deployment wait worked + assert: + that: + - condition.reason == "DeploymentPaused" + - condition.status == "Unknown" + vars: + condition: '{{ pause_deploy.result.status.conditions | selectattr("type", "Progressing")).0 }}' + - name: add a service based on the deployment k8s: definition: