From b19ff9d70a6ad2efc17a995edf3f348deb67d603 Mon Sep 17 00:00:00 2001 From: abikouo <79859644+abikouo@users.noreply.github.com> Date: Thu, 16 Dec 2021 17:48:00 +0100 Subject: [PATCH] k8s - add support for Server Side apply (#260) k8s - add support for Server Side apply SUMMARY Server side apply is now support for k8s module with this Pull request. The feature is not yet released on kubernetes-client, once this is done, we can merge this pull request. closes #87 ISSUE TYPE Feature Pull Request COMPONENT NAME k8s ADDITIONAL INFORMATION Reviewed-by: Mike Graves Reviewed-by: None Reviewed-by: None --- ...-k8s-add-support-for-server_side_apply.yml | 2 + molecule/default/tasks/apply.yml | 133 ++++++++++++++++++ plugins/module_utils/apply.py | 15 +- plugins/module_utils/common.py | 22 ++- plugins/modules/k8s.py | 43 ++++++ 5 files changed, 213 insertions(+), 2 deletions(-) create mode 100644 changelogs/fragments/260-k8s-add-support-for-server_side_apply.yml diff --git a/changelogs/fragments/260-k8s-add-support-for-server_side_apply.yml b/changelogs/fragments/260-k8s-add-support-for-server_side_apply.yml new file mode 100644 index 00000000..69532fb5 --- /dev/null +++ b/changelogs/fragments/260-k8s-add-support-for-server_side_apply.yml @@ -0,0 +1,2 @@ +minor_changes: + - k8s - add support for server_side_apply. (https://github.com/ansible-collections/kubernetes.core/issues/87). diff --git a/molecule/default/tasks/apply.yml b/molecule/default/tasks/apply.yml index 18c60d83..b01a16d4 100644 --- a/molecule/default/tasks/apply.yml +++ b/molecule/default/tasks/apply.yml @@ -896,6 +896,139 @@ that: - k8s_networkpolicy is not changed + # Server Side Apply + - name: Create Configmap using server side apply - field_manager not specified + k8s: + namespace: "{{ apply_namespace }}" + definition: + apiVersion: v1 + kind: ConfigMap + metadata: + name: server-side-cm + data: + key: value-0 + apply: yes + server_side_apply: + force_conflicts: false + register: result + ignore_errors: true + + - name: Check that configmap creation failed + assert: + that: + - result is failed + - '"field_manager" in result.msg' + + - name: Create Configmap using server side apply + k8s: + namespace: "{{ apply_namespace }}" + definition: + apiVersion: v1 + kind: ConfigMap + metadata: + name: server-side-cm + data: + key: value-0 + apply: yes + server_side_apply: + field_manager: "manager-00" + register: result + + - name: Check configmap was created with expected manager + assert: + that: + - result is changed + - result.result.metadata.managedFields | length == 1 + - result.result.metadata.managedFields[0].manager == 'manager-00' + + - name: Apply ConfigMap using same parameters + k8s: + namespace: "{{ apply_namespace }}" + definition: + apiVersion: v1 + kind: ConfigMap + metadata: + name: server-side-cm + data: + key: value-0 + apply: yes + server_side_apply: + field_manager: "manager-00" + register: result + + - name: Assert that nothing change using check_mode + assert: + that: + - result is not changed + + - name: Apply ConfigMap adding new manager + k8s: + namespace: "{{ apply_namespace }}" + definition: + apiVersion: v1 + kind: ConfigMap + metadata: + name: server-side-cm + data: + key: value-0 + apply: yes + server_side_apply: + field_manager: "manager-01" + register: result + + - name: Assert that number of manager has increased + assert: + that: + - result is changed + - result.result.metadata.managedFields | length == 2 + + - name: Apply changes to Configmap using new field_manager + k8s: + namespace: "{{ apply_namespace }}" + definition: + apiVersion: v1 + kind: ConfigMap + metadata: + name: server-side-cm + data: + key: value-1 + apply: yes + server_side_apply: + field_manager: "manager-02" + register: result + ignore_errors: true + + - name: assert that operation failed with conflicts + assert: + that: + - result is failed + - result.reason == 'Conflict' + + - name: Apply changes to Configmap using new field_manager and force_conflicts + k8s: + namespace: "{{ apply_namespace }}" + definition: + apiVersion: v1 + kind: ConfigMap + metadata: + name: server-side-cm + data: + key: value-1 + apply: yes + server_side_apply: + field_manager: "manager-02" + force_conflicts: true + register: result + + - name: assert that operation failed with conflicts + assert: + that: + - result is changed + - result.result.metadata.managedFields | length == 1 + - result.result.metadata.managedFields[0].manager == 'manager-02' + - result.result.data.key == 'value-1' + + always: - name: Remove namespace k8s: diff --git a/plugins/module_utils/apply.py b/plugins/module_utils/apply.py index a0ed6a4e..29e164a1 100644 --- a/plugins/module_utils/apply.py +++ b/plugins/module_utils/apply.py @@ -114,12 +114,14 @@ def apply_patch(actual, desired): return actual, dict_merge(desired, annotate(desired)) -def apply_object(resource, definition): +def apply_object(resource, definition, server_side=False): try: actual = resource.get( name=definition["metadata"]["name"], namespace=definition["metadata"].get("namespace"), ) + if server_side: + return actual, None except NotFoundError: return None, dict_merge(definition, annotate(definition)) return apply_patch(actual.to_dict(), definition) @@ -127,6 +129,17 @@ def apply_object(resource, definition): def k8s_apply(resource, definition, **kwargs): existing, desired = apply_object(resource, definition) + server_side = kwargs.get("server_side", False) + if server_side: + body = json.dumps(definition).encode() + # server_side_apply is forces content_type to 'application/apply-patch+yaml' + return resource.server_side_apply( + body=body, + name=definition["metadata"]["name"], + namespace=definition["metadata"].get("namespace"), + force_conflicts=kwargs.get("force_conflicts"), + field_manager=kwargs.get("field_manager"), + ) if not existing: return resource.create( body=desired, namespace=definition["metadata"].get("namespace"), **kwargs diff --git a/plugins/module_utils/common.py b/plugins/module_utils/common.py index c1579549..75545c08 100644 --- a/plugins/module_utils/common.py +++ b/plugins/module_utils/common.py @@ -490,7 +490,7 @@ class K8sAnsibleMixin(object): self.params.pop(alias) def load_resource_definitions(self, src): - """ Load the requested src path """ + """Load the requested src path""" result = None path = os.path.normpath(src) if not os.path.exists(path): @@ -834,6 +834,7 @@ class K8sAnsibleMixin(object): wait_condition = None continue_on_error = self.params.get("continue_on_error") label_selectors = self.params.get("label_selectors") + server_side_apply = self.params.get("server_side_apply") if self.params.get("wait_condition") and self.params["wait_condition"].get( "type" ): @@ -1028,6 +1029,7 @@ class K8sAnsibleMixin(object): namespace=namespace, ) ) + return result if apply: if self.check_mode and not self.supports_dry_run: @@ -1043,6 +1045,24 @@ class K8sAnsibleMixin(object): params = {} if self.check_mode: params["dry_run"] = "All" + if server_side_apply: + params["server_side"] = True + if LooseVersion(kubernetes.__version__) < LooseVersion( + "19.15.0" + ): + msg = "kubernetes >= 19.15.0 is required to use server side apply." + if continue_on_error: + result["error"] = dict(msg=msg) + return result + else: + self.fail_json( + msg=msg, version=kubernetes.__version__ + ) + if not server_side_apply.get("field_manager"): + self.fail( + msg="field_manager is required to use server side apply." + ) + params.update(server_side_apply) k8s_obj = resource.apply( definition, namespace=namespace, **params ).to_dict() diff --git a/plugins/modules/k8s.py b/plugins/modules/k8s.py index e805bc58..d7a42472 100644 --- a/plugins/modules/k8s.py +++ b/plugins/modules/k8s.py @@ -152,6 +152,26 @@ options: - mutually exclusive with C(name). type: str version_added: 2.3.0 + server_side_apply: + description: + - When this option is set, apply runs in the server instead of the client. + - Ignored if C(apply) is not set or is set to False. + - This option requires "kubernetes >= 19.15.0". + type: dict + version_added: 2.3.0 + suboptions: + field_manager: + type: str + description: + - Name of the manager used to track field ownership. + required: True + force_conflicts: + description: + - A conflict is a special status error that occurs when an Server Side Apply operation tries to change a field, + which another user also claims to manage. + - When set to True, server-side apply will force the changes against conflicts. + type: bool + default: False requirements: - "python >= 3.6" @@ -302,6 +322,19 @@ EXAMPLES = r""" - name: py image: python:3.7-alpine imagePullPolicy: IfNotPresent + +# Server side apply +- name: Create configmap using server side apply + kubernetes.core.k8s: + namespace: testing + definition: + apiVersion: v1 + kind: ConfigMap + metadata: + name: my-configmap + apply: yes + server_side_apply: + field_manager: ansible """ RETURN = r""" @@ -368,6 +401,13 @@ def validate_spec(): ) +def server_apply_spec(): + return dict( + field_manager=dict(type="str", required=True), + force_conflicts=dict(type="bool", default=False), + ) + + def argspec(): argument_spec = copy.deepcopy(NAME_ARG_SPEC) argument_spec.update(copy.deepcopy(RESOURCE_ARG_SPEC)) @@ -390,6 +430,9 @@ def argspec(): argument_spec["force"] = dict(type="bool", default=False) argument_spec["label_selectors"] = dict(type="list", elements="str") argument_spec["generate_name"] = dict() + argument_spec["server_side_apply"] = dict( + type="dict", default=None, options=server_apply_spec() + ) return argument_spec