diff --git a/changelogs/fragments/102-wait-updated-daemonset-pods.yaml b/changelogs/fragments/102-wait-updated-daemonset-pods.yaml new file mode 100644 index 00000000..2fc981de --- /dev/null +++ b/changelogs/fragments/102-wait-updated-daemonset-pods.yaml @@ -0,0 +1,3 @@ +--- +minor_changes: + - k8s - wait for all pods to update when rolling out daemonset changes (https://github.com/ansible-collections/kubernetes.core/pull/102). diff --git a/changelogs/fragments/90-k8s-add-parameter-patch_only.yml b/changelogs/fragments/90-k8s-add-parameter-patch_only.yml new file mode 100644 index 00000000..ed58b20b --- /dev/null +++ b/changelogs/fragments/90-k8s-add-parameter-patch_only.yml @@ -0,0 +1,3 @@ +--- +minor_changes: + - k8s - support ``patched`` value for ``state`` option. patched state is an existing resource that has a given patch applied (https://github.com/ansible-collections/kubernetes.core/pull/90). diff --git a/molecule/default/converge.yml b/molecule/default/converge.yml index 8a2d2f57..a2e7bfb0 100644 --- a/molecule/default/converge.yml +++ b/molecule/default/converge.yml @@ -141,6 +141,14 @@ tags: - always + - name: Include patched.yml + include_tasks: + file: tasks/patched.yml + apply: + tags: [ patched, k8s ] + tags: + - always + roles: - role: helm tags: diff --git a/molecule/default/tasks/patched.yml b/molecule/default/tasks/patched.yml new file mode 100644 index 00000000..9c0f9077 --- /dev/null +++ b/molecule/default/tasks/patched.yml @@ -0,0 +1,123 @@ +--- +- block: + - set_fact: + patch_only_namespace: + first: patched-namespace-1 + second: patched-namespace-2 + + - name: Ensure namespace {{ patch_only_namespace.first }} exist + kubernetes.core.k8s: + definition: + apiVersion: v1 + kind: Namespace + metadata: + name: "{{ patch_only_namespace.first }}" + labels: + existingLabel: "labelValue" + annotations: + existingAnnotation: "annotationValue" + wait: yes + + - name: Ensure namespace {{ patch_only_namespace.second }} does not exist + kubernetes.core.k8s_info: + kind: namespace + name: "{{ patch_only_namespace.second }}" + register: second_namespace + + - name: assert that second namespace does not exist + assert: + that: + - second_namespace.resources | length == 0 + + - name: apply patch on existing resource + kubernetes.core.k8s: + state: patched + wait: yes + definition: | + --- + apiVersion: v1 + kind: Namespace + metadata: + name: "{{ patch_only_namespace.first }}" + labels: + ansible: patched + --- + apiVersion: v1 + kind: Namespace + metadata: + name: "{{ patch_only_namespace.second }}" + labels: + ansible: patched + register: patch_resource + + - name: assert that patch succeed + assert: + that: + - patch_resource.changed + - patch_resource.result.results | selectattr('warning', 'defined') | list | length == 1 + + - name: Ensure namespace {{ patch_only_namespace.first }} was patched correctly + kubernetes.core.k8s_info: + kind: namespace + name: "{{ patch_only_namespace.first }}" + register: first_namespace + + - name: assert labels are as expected + assert: + that: + - first_namespace.resources[0].metadata.labels.ansible == "patched" + - first_namespace.resources[0].metadata.labels.existingLabel == "labelValue" + - first_namespace.resources[0].metadata.annotations.existingAnnotation == "annotationValue" + - name: Ensure namespace {{ patch_only_namespace.second }} was not created + kubernetes.core.k8s_info: + kind: namespace + name: "{{ patch_only_namespace.second }}" + register: second_namespace + + - name: assert that second namespace does not exist + assert: + that: + - second_namespace.resources | length == 0 + + - name: patch all resources (create if does not exist) + kubernetes.core.k8s: + state: present + definition: | + --- + apiVersion: v1 + kind: Namespace + metadata: + name: "{{ patch_only_namespace.first }}" + labels: + patch: ansible + --- + apiVersion: v1 + kind: Namespace + metadata: + name: "{{ patch_only_namespace.second }}" + labels: + patch: ansible + wait: yes + register: patch_resource + + - name: Ensure namespace {{ patch_only_namespace.second }} was created + kubernetes.core.k8s_info: + kind: namespace + name: "{{ patch_only_namespace.second }}" + register: second_namespace + + - name: assert that second namespace exist + assert: + that: + - second_namespace.resources | length == 1 + + always: + - name: Remove namespace + kubernetes.core.k8s: + kind: Namespace + name: "{{ item }}" + state: absent + with_items: + - "{{ patch_only_namespace.first }}" + - "{{ patch_only_namespace.second }}" + ignore_errors: true diff --git a/molecule/default/tasks/waiter.yml b/molecule/default/tasks/waiter.yml index 303cb2d1..44fc42b3 100644 --- a/molecule/default/tasks/waiter.yml +++ b/molecule/default/tasks/waiter.yml @@ -54,6 +54,9 @@ vars: k8s_pod_name: wait-ds k8s_pod_image: gcr.io/kuar-demo/kuard-amd64:1 + k8s_pod_command: + - sleep + - "600" register: ds - name: Check that daemonset wait worked @@ -82,6 +85,9 @@ vars: k8s_pod_name: wait-ds k8s_pod_image: gcr.io/kuar-demo/kuard-amd64:2 + k8s_pod_command: + - sleep + - "600" register: update_ds_check_mode check_mode: yes @@ -112,6 +118,9 @@ vars: k8s_pod_name: wait-ds k8s_pod_image: gcr.io/kuar-demo/kuard-amd64:3 + k8s_pod_command: + - sleep + - "600" register: ds - name: Get updated pods diff --git a/plugins/module_utils/common.py b/plugins/module_utils/common.py index 5e4a3b87..c868ee35 100644 --- a/plugins/module_utils/common.py +++ b/plugins/module_utils/common.py @@ -404,6 +404,7 @@ class K8sAnsibleMixin(object): def _daemonset_ready(daemonset): return (daemonset.status and daemonset.status.desiredNumberScheduled is not None + and daemonset.status.updatedNumberScheduled == daemonset.status.desiredNumberScheduled and daemonset.status.numberReady == daemonset.status.desiredNumberScheduled and daemonset.status.observedGeneration == daemonset.metadata.generation and not daemonset.status.unavailableReplicas) @@ -548,7 +549,8 @@ class K8sAnsibleMixin(object): if self.params['validate'] is not None: self.warnings = self.validate(definition) result = self.perform_action(resource, definition) - result['warnings'] = self.warnings + if self.warnings: + result['warnings'] = self.warnings changed = changed or result['changed'] results.append(result) @@ -690,6 +692,7 @@ class K8sAnsibleMixin(object): else: self.fail_json(msg=build_error_msg(definition['kind'], origin_name, msg), **result) return result + else: if apply: if self.check_mode: @@ -734,7 +737,14 @@ class K8sAnsibleMixin(object): return result if not existing: - if self.check_mode: + if state == 'patched': + # Silently skip this resource (do not raise an error) as 'patch_only' is set to true + result['changed'] = False + result['warning'] = "resource 'kind={kind},name={name}' was not found but will not be created as 'state'\ + parameter has been set to '{state}'".format( + kind=definition['kind'], name=origin_name, state=state) + return result + elif self.check_mode: k8s_obj = _encode_stringdata(definition) else: try: @@ -784,7 +794,7 @@ class K8sAnsibleMixin(object): match = False diffs = [] - if existing and force: + if state == 'present' and existing and force: if self.check_mode: k8s_obj = _encode_stringdata(definition) else: diff --git a/plugins/modules/k8s.py b/plugins/modules/k8s.py index d5a4f45a..7fed71b7 100644 --- a/plugins/modules/k8s.py +++ b/plugins/modules/k8s.py @@ -30,7 +30,6 @@ description: - Supports check mode. extends_documentation_fragment: - - kubernetes.core.k8s_state_options - kubernetes.core.k8s_name_options - kubernetes.core.k8s_resource_options - kubernetes.core.k8s_auth_options @@ -38,6 +37,21 @@ extends_documentation_fragment: - kubernetes.core.k8s_delete_options options: + state: + description: + - Determines if an object should be created, patched, or deleted. When set to C(present), an object will be + created, if it does not already exist. If set to C(absent), an existing object will be deleted. If set to + C(present), an existing object will be patched, if its attributes differ from those specified using + I(resource_definition) or I(src). + - C(patched) state is an existing resource that has a given patch applied. If the resource doesn't exist, silently skip it (do not raise an error). + type: str + default: present + choices: [ absent, present, patched ] + force: + description: + - If set to C(yes), and I(state) is C(present), an existing object will be replaced. + type: bool + default: no merge_type: description: - Whether to override the default patch merge approach with a specific type. By default, the strategic @@ -235,7 +249,6 @@ EXAMPLES = r''' wait_condition: type: Progressing status: Unknown - reason: DeploymentPaused # Wait for this service to have acquired an External IP - name: Create ingress and wait for ip to be assigned @@ -245,12 +258,24 @@ EXAMPLES = r''' wait_property: property: status.loadBalancer.ingress[*].ip +# Wait for containers inside pod to be running - name: Create Pod and wait for containers for be running kubernetes.core.k8s: template: pod.yaml wait: yes wait_property: property: status.containerStatuses[*].state.running + +# Patch existing namespace : add label +- name: add label to existing namespace + kubernetes.core.k8s: + state: patched + kind: Namespace + name: patch_namespace + definition: + metadata: + labels: + support: patch ''' RETURN = r''' @@ -299,7 +324,7 @@ import copy from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import AnsibleModule from ansible_collections.kubernetes.core.plugins.module_utils.args_common import ( - AUTH_ARG_SPEC, WAIT_ARG_SPEC, NAME_ARG_SPEC, COMMON_ARG_SPEC, RESOURCE_ARG_SPEC, DELETE_OPTS_ARG_SPEC) + AUTH_ARG_SPEC, WAIT_ARG_SPEC, NAME_ARG_SPEC, RESOURCE_ARG_SPEC, DELETE_OPTS_ARG_SPEC) def validate_spec(): @@ -311,8 +336,7 @@ def validate_spec(): def argspec(): - argument_spec = copy.deepcopy(COMMON_ARG_SPEC) - argument_spec.update(copy.deepcopy(NAME_ARG_SPEC)) + argument_spec = copy.deepcopy(NAME_ARG_SPEC) argument_spec.update(copy.deepcopy(RESOURCE_ARG_SPEC)) argument_spec.update(copy.deepcopy(AUTH_ARG_SPEC)) argument_spec.update(copy.deepcopy(WAIT_ARG_SPEC)) @@ -323,6 +347,9 @@ def argspec(): argument_spec['template'] = dict(type='raw', default=None) argument_spec['delete_options'] = dict(type='dict', default=None, options=copy.deepcopy(DELETE_OPTS_ARG_SPEC)) argument_spec['continue_on_error'] = dict(type='bool', default=False) + argument_spec['state'] = dict(default='present', choices=['present', 'absent', 'patched']) + argument_spec['force'] = dict(type='bool', default=False) + return argument_spec @@ -334,6 +361,7 @@ def execute_module(module, k8s_ansible_mixin): k8s_ansible_mixin.fail_json = k8s_ansible_mixin.module.fail_json k8s_ansible_mixin.fail = k8s_ansible_mixin.module.fail_json k8s_ansible_mixin.exit_json = k8s_ansible_mixin.module.exit_json + k8s_ansible_mixin.warn = k8s_ansible_mixin.module.warn k8s_ansible_mixin.warnings = [] k8s_ansible_mixin.kind = k8s_ansible_mixin.params.get('kind')