diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0c05f732..85a8be73 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,7 +87,7 @@ jobs: # The 3.3.0 release of molecule introduced a breaking change. See # https://github.com/ansible-community/molecule/issues/3083 - name: Install molecule and openshift dependencies - run: pip install ansible "molecule<3.3.0" yamllint openshift flake8 + run: pip install ansible "molecule<3.3.0" yamllint openshift flake8 jsonpatch # The latest release doesn't work with Molecule currently. # See: https://github.com/ansible-community/molecule/issues/2757 diff --git a/changelogs/fragments/83-k8s-fix-merge_type-json.yaml b/changelogs/fragments/83-k8s-fix-merge_type-json.yaml new file mode 100644 index 00000000..b7f7cd8a --- /dev/null +++ b/changelogs/fragments/83-k8s-fix-merge_type-json.yaml @@ -0,0 +1,3 @@ +--- +bugfixes: + - k8s - fix merge_type option when set to json (https://github.com/ansible-collections/kubernetes.core/issues/54). diff --git a/molecule/default/converge.yml b/molecule/default/converge.yml index 6e9387a1..8a2d2f57 100644 --- a/molecule/default/converge.yml +++ b/molecule/default/converge.yml @@ -133,6 +133,14 @@ tags: - always + - name: Include merge_type.yml + include_tasks: + file: tasks/merge_type.yml + apply: + tags: [ merge_type, k8s ] + tags: + - always + roles: - role: helm tags: diff --git a/molecule/default/tasks/merge_type.yml b/molecule/default/tasks/merge_type.yml new file mode 100644 index 00000000..4a8f31e5 --- /dev/null +++ b/molecule/default/tasks/merge_type.yml @@ -0,0 +1,252 @@ +- block: + - name: Define common facts + set_fact: + k8s_patch_namespace: "patch" + k8s_strategic_merge: "strategic-merge" + k8s_merge: "json-merge" + k8s_json: "json-patch" + + - name: Ensure the namespace exist + kubernetes.core.k8s: + kind: namespace + name: "{{ k8s_patch_namespace }}" + + + # Strategic merge + - name: create a simple nginx deployment + kubernetes.core.k8s: + namespace: "{{ k8s_patch_namespace }}" + definition: + apiVersion: apps/v1 + kind: Deployment + metadata: + name: "{{ k8s_strategic_merge }}" + spec: + replicas: 2 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: "{{ k8s_strategic_merge }}-ctr" + image: nginx + tolerations: + - effect: NoSchedule + key: dedicated + value: "test-strategic-merge" + + + - name: patch service using strategic merge + kubernetes.core.k8s: + kind: Deployment + namespace: "{{ k8s_patch_namespace }}" + name: "{{ k8s_strategic_merge }}" + definition: + spec: + template: + spec: + containers: + - name: "{{ k8s_strategic_merge }}-ctr-2" + image: redis + register: depl_patch + + - name: validate that resource was patched + assert: + that: + - depl_patch.changed + + - name: describe "{{ k8s_strategic_merge }}" deployment + kubernetes.core.k8s_info: + kind: Deployment + name: "{{ k8s_strategic_merge }}" + namespace: "{{ k8s_patch_namespace }}" + register: deployment_out + + - name: assert that deployment contains expected images + assert: + that: + - deployment_out.resources[0].spec.template.spec.containers | selectattr('image','equalto','nginx') | list | length == 1 + - deployment_out.resources[0].spec.template.spec.containers | selectattr('image','equalto','redis') | list | length == 1 + + # Json merge + - name: create a simple nginx deployment (testing merge patch) + kubernetes.core.k8s: + namespace: "{{ k8s_patch_namespace }}" + definition: + apiVersion: apps/v1 + kind: Deployment + metadata: + name: "{{ k8s_merge }}" + spec: + replicas: 2 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: "{{ k8s_merge }}-ctr" + image: nginx + tolerations: + - effect: NoSchedule + key: dedicated + value: "test-strategic-merge" + + + - name: patch service using json merge patch + kubernetes.core.k8s: + kind: Deployment + namespace: "{{ k8s_patch_namespace }}" + name: "{{ k8s_merge }}" + merge_type: + - merge + definition: + spec: + template: + spec: + containers: + - name: "{{ k8s_merge }}-ctr-2" + image: python + register: merge_patch + + - name: validate that resource was patched + assert: + that: + - merge_patch.changed + + - name: describe "{{ k8s_merge }}" deployment + kubernetes.core.k8s_info: + kind: Deployment + name: "{{ k8s_merge }}" + namespace: "{{ k8s_patch_namespace }}" + register: merge_out + + - name: assert that deployment contains expected images + assert: + that: + - merge_out.resources[0].spec.template.spec.containers | list | length == 1 + - merge_out.resources[0].spec.template.spec.containers[0].image == 'python' + + # Json + - name: create simple pod + kubernetes.core.k8s: + namespace: "{{ k8s_patch_namespace }}" + definition: + apiVersion: v1 + kind: Pod + metadata: + name: "{{ k8s_json }}-pod" + labels: + name: "{{ k8s_json }}-pod" + spec: + containers: + - args: + - /bin/sh + - -c + - while true; do echo $(date); sleep 10; done + image: python:3.7-alpine + imagePullPolicy: Always + name: alpine + + - name: Patch pod - update container image + kubernetes.core.k8s: + kind: Pod + namespace: "{{ k8s_patch_namespace }}" + name: "{{ k8s_json }}-pod" + merge_type: + - json + definition: + - op: replace + path: /spec/containers/0/image + value: python:3.8-alpine + register: pod_patch + + - name: assert that patch was performed + assert: + that: + - pod_patch.changed + + - name: describe Pod after patching + kubernetes.core.k8s_info: + kind: Pod + name: "{{ k8s_json }}-pod" + namespace: "{{ k8s_patch_namespace }}" + register: describe_pod + + - name: assert that image name has changed + assert: + that: + - describe_pod.resources[0].spec.containers[0].image == 'python:3.8-alpine' + + - name: create a simple nginx deployment + kubernetes.core.k8s: + namespace: "{{ k8s_patch_namespace }}" + definition: + apiVersion: apps/v1 + kind: Deployment + metadata: + name: "{{ k8s_json }}-depl" + labels: + name: "{{ k8s_json }}-depl" + spec: + replicas: 2 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx-container + image: nginx + args: + - /bin/sh + - -c + - while true; do echo $(date); sleep 10; done + + - name: Patch Nginx deployment command + kubernetes.core.k8s: + kind: Deployment + namespace: "{{ k8s_patch_namespace }}" + name: "{{ k8s_json }}-depl" + merge_type: + - json + definition: + - op: add + path: '/spec/template/spec/containers/0/args/-' + value: 'touch /var/log' + register: patch_out + + - name: assert that patch succeed + assert: + that: + - patch_out.changed + + - name: describe deployment after patching + kubernetes.core.k8s_info: + kind: Deployment + name: "{{ k8s_json }}-depl" + namespace: "{{ k8s_patch_namespace }}" + register: describe_depl + + - name: assert that args changed on deployment + assert: + that: + - describe_depl.resources[0].spec.template.spec.containers[0].args | length == 4 + + always: + - name: Ensure namespace has been deleted + kubernetes.core.k8s: + kind: namespace + name: "{{ k8s_patch_namespace }}" + state: absent + ignore_errors: yes diff --git a/plugins/module_utils/common.py b/plugins/module_utils/common.py index 14d13e5c..203fa4a9 100644 --- a/plugins/module_utils/common.py +++ b/plugins/module_utils/common.py @@ -106,6 +106,17 @@ except ImportError as e: K8S_IMP_ERR = traceback.format_exc() +JSON_PATCH_IMP_ERR = None +try: + import jsonpatch + HAS_JSON_PATCH = True + jsonpatch_import_exception = None +except ImportError as e: + HAS_JSON_PATCH = False + jsonpatch_import_exception = e + JSON_PATCH_IMP_ERR = traceback.format_exc() + + def configuration_digest(configuration): m = hashlib.sha256() for k in AUTH_ARG_MAP: @@ -851,12 +862,42 @@ class K8sAnsibleMixin(object): self.fail_json(msg=msg, **result) return result + def json_patch(self, existing, definition, merge_type): + if merge_type == "json": + if not HAS_JSON_PATCH: + error = { + "msg": missing_required_lib('jsonpatch'), + "exception": JSON_PATCH_IMP_ERR, + "error": to_native(jsonpatch_import_exception) + } + return None, error + try: + patch = jsonpatch.JsonPatch([definition]) + result_patch = patch.apply(existing.to_dict()) + return result_patch, None + except jsonpatch.InvalidJsonPatch as e: + error = { + "msg": "invalid json patch", + "error": to_native(e) + } + return None, error + except jsonpatch.JsonPatchConflict as e: + error = { + "msg": "patch could not be applied due to conflict situation", + "error": to_native(e) + } + return None, error + return definition, None + def patch_resource(self, resource, definition, existing, name, namespace, merge_type=None): try: params = dict(name=name, namespace=namespace) if merge_type: params['content_type'] = 'application/{0}-patch+json'.format(merge_type) - k8s_obj = resource.patch(definition, **params).to_dict() + patch_data, error = self.json_patch(existing, definition, merge_type) + if error is not None: + return None, error + k8s_obj = resource.patch(patch_data, **params).to_dict() match, diffs = self.diff_objects(existing.to_dict(), k8s_obj) error = {} return k8s_obj, {} diff --git a/plugins/modules/k8s.py b/plugins/modules/k8s.py index 0672aa93..65152040 100644 --- a/plugins/modules/k8s.py +++ b/plugins/modules/k8s.py @@ -136,6 +136,7 @@ requirements: - "python >= 2.7" - "openshift >= 0.6" - "PyYAML >= 3.11" + - "jsonpatch" ''' EXAMPLES = r''' diff --git a/requirements.txt b/requirements.txt index 6ed70a79..9b495b2b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ openshift>=0.6.2 requests-oauthlib +jsonpatch