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/plugins/module_utils/common.py b/plugins/module_utils/common.py index 86b7ae9a..06f31dae 100644 --- a/plugins/module_utils/common.py +++ b/plugins/module_utils/common.py @@ -529,7 +529,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) @@ -670,6 +671,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: @@ -713,7 +715,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: @@ -762,7 +771,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 f70923ff..27a5ebaf 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 @@ -236,6 +250,17 @@ EXAMPLES = r''' type: Progressing status: Unknown reason: DeploymentPaused + +# 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''' @@ -284,7 +309,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(): @@ -296,8 +321,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)) @@ -308,6 +332,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 @@ -319,6 +346,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')