From a9b8cc68d5edc8105255cec9638ac75c19ce7f1c Mon Sep 17 00:00:00 2001 From: Mike Graves Date: Tue, 12 Jan 2021 14:17:18 -0500 Subject: [PATCH] Add support for configuring garbage collection (#334) * Add support for configuring garbage collection This surfaces deleteOptions functionality in a top-level delete_options parameter. * Add changelog fragment * Remove kind and apiVersion from delete_options * Add release version to docs --- changelogs/fragments/334-delete-options.yaml | 2 + molecule/default/converge.yml | 1 + molecule/default/tasks/gc.yml | 211 +++++++++++++++++++ plugins/doc_fragments/k8s_delete_options.py | 51 +++++ plugins/module_utils/common.py | 29 +++ plugins/modules/k8s.py | 5 +- 6 files changed, 298 insertions(+), 1 deletion(-) create mode 100644 changelogs/fragments/334-delete-options.yaml create mode 100644 molecule/default/tasks/gc.yml create mode 100644 plugins/doc_fragments/k8s_delete_options.py diff --git a/changelogs/fragments/334-delete-options.yaml b/changelogs/fragments/334-delete-options.yaml new file mode 100644 index 00000000..b6c146aa --- /dev/null +++ b/changelogs/fragments/334-delete-options.yaml @@ -0,0 +1,2 @@ +minor_changes: + - k8s - add a ``delete_options`` parameter to control garbage collection behavior when deleting a resource (https://github.com/ansible-collections/community.kubernetes/issues/253). diff --git a/molecule/default/converge.yml b/molecule/default/converge.yml index f6dcb454..22fd2b7e 100644 --- a/molecule/default/converge.yml +++ b/molecule/default/converge.yml @@ -30,6 +30,7 @@ - include_tasks: tasks/cluster_info.yml - include_tasks: tasks/access_review.yml - include_tasks: tasks/rollback.yml + - include_tasks: tasks/gc.yml roles: - helm diff --git a/molecule/default/tasks/gc.yml b/molecule/default/tasks/gc.yml new file mode 100644 index 00000000..a5b0cf52 --- /dev/null +++ b/molecule/default/tasks/gc.yml @@ -0,0 +1,211 @@ +--- +- vars: + gc_namespace: garbage + gc_name: garbage-job + # This is a job definition that runs for 10 minutes and won't gracefully + # shutdown. It allows us to test foreground vs background deletion. + job_definition: + apiVersion: v1 + kind: Job + metadata: + name: "{{ gc_name }}" + namespace: "{{ gc_namespace }}" + spec: + template: + metadata: + labels: + job: gc + spec: + containers: + - name: "{{ gc_name }}" + image: busybox + command: + - sleep + - "600" + restartPolicy: Never + + block: + - name: Ensure namespace exists + k8s: + definition: + apiVersion: v1 + kind: Namespace + metadata: + name: "{{ gc_namespace }}" + + - name: Add a job + k8s: + definition: "{{ job_definition }}" + + - name: Test that job's pod is running + k8s_info: + kind: Pod + namespace: "{{ gc_namespace }}" + label_selectors: + - "job=gc" + wait: yes + wait_timeout: 100 + register: job + + - name: Assert job's pod is running + assert: + that: job.resources[0].status.phase == "Running" + + - name: Delete job in foreground + k8s: + kind: Job + name: "{{ gc_name }}" + namespace: "{{ gc_namespace }}" + state: absent + wait: yes + wait_timeout: 100 + delete_options: + propagationPolicy: Foreground + + - name: Test job's pod does not exist + k8s_info: + kind: Pod + namespace: "{{ gc_namespace }}" + label_selectors: + - "job=gc" + register: job + + - name: Assert job's pod does not exist + assert: + that: not job.resources + + - name: Add a job + k8s: + definition: "{{ job_definition }}" + + - name: Test that job's pod is running + k8s_info: + kind: Pod + namespace: "{{ gc_namespace }}" + label_selectors: + - "job=gc" + wait: yes + wait_timeout: 100 + register: job + + - name: Assert job's pod is running + assert: + that: job.resources[0].status.phase == "Running" + + - name: Delete job in background + k8s: + kind: Job + name: "{{ gc_name }}" + namespace: "{{ gc_namespace }}" + state: absent + wait: yes + wait_timeout: 100 + delete_options: + propagationPolicy: "Background" + + # The default grace period is 30s so this pod should still be running. + - name: Test job's pod exists + k8s_info: + kind: Pod + namespace: "{{ gc_namespace }}" + label_selectors: + - "job=gc" + register: job + + - name: Assert job's pod still running + assert: + that: job.resources[0].status.phase == "Running" + + - name: Add a job + k8s: + definition: "{{ job_definition }}" + + - name: Test that job's pod is running + k8s_info: + kind: Pod + namespace: "{{ gc_namespace }}" + label_selectors: + - "job=gc" + wait: yes + wait_timeout: 100 + register: job + + - name: Assert job's pod is running + assert: + that: job.resources[0].status.phase == "Running" + + - name: Orphan the job's pod + k8s: + kind: Job + name: "{{ gc_name }}" + namespace: "{{ gc_namespace }}" + state: absent + wait: yes + wait_timeout: 100 + delete_options: + propagationPolicy: "Orphan" + + - name: Ensure grace period has expired + pause: + seconds: 60 + + - name: Test that job's pod is still running + k8s_info: + kind: Pod + namespace: "{{ gc_namespace }}" + label_selectors: + - "job=gc" + register: job + + - name: Assert job's pod is still running + assert: + that: job.resources[0].status.phase == "Running" + + - name: Add a job + k8s: + definition: "{{ job_definition }}" + register: job + + - name: Delete a job with failing precondition + k8s: + kind: Job + name: "{{ gc_name }}" + namespace: "{{ gc_namespace }}" + state: absent + delete_options: + preconditions: + uid: not-a-valid-uid + ignore_errors: yes + register: result + + - name: Assert that deletion failed + assert: + that: result is failed + + - name: Delete a job using a valid precondition + k8s: + kind: Job + name: "{{ gc_name }}" + namespace: "{{ gc_namespace }}" + state: absent + delete_options: + preconditions: + uid: "{{ job.result.metadata.uid }}" + + - name: Check that job is deleted + k8s_info: + kind: Job + namespace: "{{ gc_namespace }}" + name: "{{ gc_name }}" + register: job + + - name: Assert job is deleted + assert: + that: not job.resources + + always: + - name: Delete namespace + k8s: + kind: Namespace + name: "{{ gc_namespace }}" + state: absent diff --git a/plugins/doc_fragments/k8s_delete_options.py b/plugins/doc_fragments/k8s_delete_options.py new file mode 100644 index 00000000..053a4d0f --- /dev/null +++ b/plugins/doc_fragments/k8s_delete_options.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2020, Red Hat | Ansible +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +# Options for specifying object wait + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +class ModuleDocFragment(object): + + DOCUMENTATION = r''' +options: + delete_options: + type: dict + version_added: '1.2.0' + description: + - Configure behavior when deleting an object. + - Only used when I(state=absent). + suboptions: + propagationPolicy: + type: str + description: + - Use to control how dependent objects are deleted. + - If not specified, the default policy for the object type will be used. This may vary across object types. + choices: + - "Foreground" + - "Background" + - "Orphan" + gracePeriodSeconds: + type: int + description: + - Specify how many seconds to wait before forcefully terminating. + - Only implemented for Pod resources. + - If not specified, the default grace period for the object type will be used. + preconditions: + type: dict + description: + - Specify condition that must be met for delete to proceed. + suboptions: + resourceVersion: + type: str + description: + - Specify the resource version of the target object. + uid: + type: str + description: + - Specify the UID of the target object. +''' diff --git a/plugins/module_utils/common.py b/plugins/module_utils/common.py index d8ef196a..788c9df8 100644 --- a/plugins/module_utils/common.py +++ b/plugins/module_utils/common.py @@ -189,6 +189,27 @@ WAIT_ARG_SPEC = dict( ) ) +DELETE_OPTS_ARG_SPEC = { + 'propagationPolicy': { + 'choices': ['Foreground', 'Background', 'Orphan'], + }, + 'gracePeriodSeconds': { + 'type': 'int', + }, + 'preconditions': { + 'type': 'dict', + 'options': { + 'resourceVersion': { + 'type': 'str', + }, + 'uid': { + 'type': 'str', + } + } + } +} + + # Map kubernetes-client parameters to ansible parameters AUTH_ARG_MAP = { 'kubeconfig': 'kubeconfig', @@ -594,6 +615,7 @@ class K8sAnsibleMixin(object): return definition def perform_action(self, resource, definition): + delete_options = self.params.get('delete_options') result = {'changed': False, 'result': {}} state = self.params.get('state', None) force = self.params.get('force', False) @@ -646,6 +668,13 @@ class K8sAnsibleMixin(object): # Delete the object result['changed'] = True if not self.check_mode: + if delete_options: + body = { + 'apiVersion': 'v1', + 'kind': 'DeleteOptions', + } + body.update(delete_options) + params['body'] = body try: k8s_obj = resource.delete(**params) result['result'] = k8s_obj.to_dict() diff --git a/plugins/modules/k8s.py b/plugins/modules/k8s.py index 1ab31154..acd097da 100644 --- a/plugins/modules/k8s.py +++ b/plugins/modules/k8s.py @@ -34,6 +34,7 @@ extends_documentation_fragment: - community.kubernetes.k8s_resource_options - community.kubernetes.k8s_auth_options - community.kubernetes.k8s_wait_options + - community.kubernetes.k8s_delete_options notes: - If your OpenShift Python library is not 0.9.0 or newer and you are trying to @@ -252,7 +253,8 @@ import copy from ansible.module_utils.basic import AnsibleModule from ansible_collections.community.kubernetes.plugins.module_utils.common import ( - K8sAnsibleMixin, COMMON_ARG_SPEC, NAME_ARG_SPEC, RESOURCE_ARG_SPEC, AUTH_ARG_SPEC, WAIT_ARG_SPEC) + K8sAnsibleMixin, COMMON_ARG_SPEC, NAME_ARG_SPEC, RESOURCE_ARG_SPEC, AUTH_ARG_SPEC, + WAIT_ARG_SPEC, DELETE_OPTS_ARG_SPEC) class KubernetesModule(K8sAnsibleMixin): @@ -277,6 +279,7 @@ class KubernetesModule(K8sAnsibleMixin): argument_spec['append_hash'] = dict(type='bool', default=False) argument_spec['apply'] = dict(type='bool', default=False) argument_spec['template'] = dict(type='raw', default=None) + argument_spec['delete_options'] = dict(type='dict', default=None, options=copy.deepcopy(DELETE_OPTS_ARG_SPEC)) return argument_spec def __init__(self, k8s_kind=None, *args, **kwargs):