diff --git a/changelogs/fragments/k8s_append_hash.yml b/changelogs/fragments/k8s_append_hash.yml new file mode 100644 index 00000000000..560397d7dd0 --- /dev/null +++ b/changelogs/fragments/k8s_append_hash.yml @@ -0,0 +1,2 @@ +minor_changes: + - k8s - append_hash parameter adds a hash to the name of ConfigMaps and Secrets for easier immutable resources diff --git a/docs/docsite/rst/user_guide/playbooks_filters.rst b/docs/docsite/rst/user_guide/playbooks_filters.rst index 03e1707087d..926a10e1fc2 100644 --- a/docs/docsite/rst/user_guide/playbooks_filters.rst +++ b/docs/docsite/rst/user_guide/playbooks_filters.rst @@ -1083,6 +1083,32 @@ To escape special characters within a regex, use the "regex_escape" filter:: {{ '^f.*o(.*)$' | regex_escape() }} +Kubernetes Filters +`````````````````` + +Use the "k8s_config_resource_name" filter to obtain the name of a Kubernetes ConfigMap or Secret, +including its hash:: + + {{ configmap_resource_definition | k8s_config_resource_name }} + +This can then be used to reference hashes in Pod specifications:: + + my_secret: + kind: Secret + name: my_secret_name + + deployment_resource: + kind: Deployment + spec: + template: + spec: + containers: + - envFrom: + - secretRef: + name: {{ my_secret | k8s_config_resource_name }} + +.. versionadded:: 2.8 + Other Useful Filters ```````````````````` diff --git a/lib/ansible/module_utils/k8s/raw.py b/lib/ansible/module_utils/k8s/raw.py index 586e39a6fbf..0730d431a0e 100644 --- a/lib/ansible/module_utils/k8s/raw.py +++ b/lib/ansible/module_utils/k8s/raw.py @@ -29,6 +29,8 @@ from ansible.module_utils.six import string_types from ansible.module_utils.k8s.common import KubernetesAnsibleModule from ansible.module_utils.common.dict_transformations import dict_merge +from distutils.version import LooseVersion + try: import yaml @@ -43,6 +45,12 @@ try: except ImportError: HAS_KUBERNETES_VALIDATE = False +try: + from openshift.helper.hashes import generate_hash + HAS_K8S_CONFIG_HASH = True +except ImportError: + HAS_K8S_CONFIG_HASH = False + class KubernetesRawModule(KubernetesAnsibleModule): @@ -62,6 +70,7 @@ class KubernetesRawModule(KubernetesAnsibleModule): argument_spec['wait'] = dict(type='bool', default=False) argument_spec['wait_timeout'] = dict(type='int', default=120) argument_spec['validate'] = dict(type='dict', default=None, options=self.validate_spec) + argument_spec['append_hash'] = dict(type='bool', default=False) return argument_spec def __init__(self, *args, **kwargs): @@ -83,6 +92,10 @@ class KubernetesRawModule(KubernetesAnsibleModule): if self.params['validate']: if LooseVersion(self.openshift_version) < LooseVersion("0.8.0"): self.fail_json(msg="openshift >= 0.8.0 is required for validate") + self.append_hash = self.params.get('append_hash') + if self.append_hash: + if not HAS_K8S_CONFIG_HASH: + self.fail_json(msg="openshift >= 0.7.2 is required for append_hash") if self.params['merge_type']: if LooseVersion(self.openshift_version) < LooseVersion("0.6.2"): self.fail_json(msg="openshift >= 0.6.2 is required for merge_type") @@ -96,7 +109,7 @@ class KubernetesRawModule(KubernetesAnsibleModule): self.resource_definitions = resource_definition else: self.resource_definitions = [resource_definition] - src = self.params.pop('src') + src = self.params.get('src') if src: self.resource_definitions = self.load_resource_definitions(src) @@ -181,7 +194,12 @@ class KubernetesRawModule(KubernetesAnsibleModule): return result try: - existing = resource.get(name=name, namespace=namespace) + # ignore append_hash for resources other than ConfigMap and Secret + if self.append_hash and definition['kind'] in ['ConfigMap', 'Secret']: + name = '%s-%s' % (name, generate_hash(definition)) + definition['metadata']['name'] = name + params = dict(name=name, namespace=namespace) + existing = resource.get(**params) except NotFoundError: # Remove traceback so that it doesn't show up in later failures try: @@ -207,7 +225,7 @@ class KubernetesRawModule(KubernetesAnsibleModule): # Delete the object if not self.check_mode: try: - k8s_obj = resource.delete(name, namespace=namespace) + k8s_obj = resource.delete(**params) result['result'] = k8s_obj.to_dict() except DynamicApiError as exc: self.fail_json(msg="Failed to delete object: {0}".format(exc.body), @@ -256,7 +274,7 @@ class KubernetesRawModule(KubernetesAnsibleModule): k8s_obj = definition else: try: - k8s_obj = resource.replace(definition, name=name, namespace=namespace).to_dict() + k8s_obj = resource.replace(definition, name=name, namespace=namespace, append_hash=self.append_hash).to_dict() except DynamicApiError as exc: msg = "Failed to replace object: {0}".format(exc.body) if self.warnings: diff --git a/lib/ansible/modules/clustering/k8s/k8s.py b/lib/ansible/modules/clustering/k8s/k8s.py index 30132e23fbc..2bf9e9f96d2 100644 --- a/lib/ansible/modules/clustering/k8s/k8s.py +++ b/lib/ansible/modules/clustering/k8s/k8s.py @@ -89,6 +89,16 @@ options: default: no type: bool version_added: "2.8" + append_hash: + description: + - Whether to append a hash to a resource name for immutability purposes + - Applies only to ConfigMap and Secret resources + - The parameter will be silently ignored for other resource kinds + - The full definition of an object is needed to generate the hash - this means that deleting an object created with append_hash + will only work if the same object is passed with state=absent (alternatively, just use state=absent with the name including + the generated hash and append_hash=no) + type: bool + version_added: "2.8" requirements: - "python >= 2.7" diff --git a/lib/ansible/plugins/filter/k8s.py b/lib/ansible/plugins/filter/k8s.py new file mode 100644 index 00000000000..f6cb0579859 --- /dev/null +++ b/lib/ansible/plugins/filter/k8s.py @@ -0,0 +1,40 @@ +# Copyright (c) 2017 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community' +} + + +try: + from openshift.helper.hashes import generate_hash + HAS_GENERATE_HASH = True +except ImportError: + HAS_GENERATE_HASH = False + +from ansible.errors import AnsibleFilterError + + +def k8s_config_resource_name(resource): + if not HAS_GENERATE_HASH: + raise AnsibleFilterError("k8s_config_resource_name requires openshift>=0.7.2") + try: + return resource['metadata']['name'] + '-' + generate_hash(resource) + except KeyError: + raise AnsibleFilterError("resource must have a metadata.name key to generate a resource name") + + +# ---- Ansible filters ---- +class FilterModule(object): + + def filters(self): + return { + 'k8s_config_resource_name': k8s_config_resource_name + } diff --git a/test/integration/targets/k8s/playbooks/full_test.yml b/test/integration/targets/k8s/playbooks/full_test.yml index e86f39176f1..033ebfc9e4f 100644 --- a/test/integration/targets/k8s/playbooks/full_test.yml +++ b/test/integration/targets/k8s/playbooks/full_test.yml @@ -3,6 +3,7 @@ gather_facts: no vars: ansible_python_interpreter: "{{ ansible_playbook_python }}" + playbook_namespace: ansible-test-k8s-full roles: - k8s diff --git a/test/integration/targets/k8s/playbooks/merge_type_fail.yml b/test/integration/targets/k8s/playbooks/merge_type_fail.yml deleted file mode 100644 index 1876ac4c8e3..00000000000 --- a/test/integration/targets/k8s/playbooks/merge_type_fail.yml +++ /dev/null @@ -1,16 +0,0 @@ -- hosts: localhost - connection: local - gather_facts: no - vars: - ansible_python_interpreter: "{{ ansible_playbook_python }}" - recreate_crd_default_merge_expectation: recreate_crd is failed - - tasks: - - python_requirements_facts: - dependencies: - - openshift==0.6.0 - - kubernetes==6.0.0 - - - include_role: - name: k8s - tasks_from: crd diff --git a/test/integration/targets/k8s/playbooks/older_openshift_fail.yml b/test/integration/targets/k8s/playbooks/older_openshift_fail.yml index fefa465e1a8..e9e2d2da671 100644 --- a/test/integration/targets/k8s/playbooks/older_openshift_fail.yml +++ b/test/integration/targets/k8s/playbooks/older_openshift_fail.yml @@ -31,7 +31,7 @@ assert: that: - k8s_append_hash is failed - - "k8s_append_hash.msg == 'openshift >= 0.7.FIXME is required for append_hash'" + - "k8s_append_hash.msg == 'openshift >= 0.7.2 is required for append_hash'" # merge_type - include_role: @@ -58,4 +58,4 @@ assert: that: - k8s_validate is failed - - "k8s_validate.msg == 'openshift >= 0.7.FIXME is required for validate'" + - "k8s_validate.msg == 'openshift >= 0.8.0 is required for validate'" diff --git a/test/integration/targets/k8s/playbooks/roles/k8s/tasks/append_hash.yml b/test/integration/targets/k8s/playbooks/roles/k8s/tasks/append_hash.yml new file mode 100644 index 00000000000..876e876a29b --- /dev/null +++ b/test/integration/targets/k8s/playbooks/roles/k8s/tasks/append_hash.yml @@ -0,0 +1,68 @@ +- block: + - name: Ensure that append_hash namespace exists + k8s: + kind: Namespace + name: append-hash + + - name: create k8s_resource variable + set_fact: + k8s_resource: + metadata: + name: config-map-test + namespace: append-hash + apiVersion: v1 + kind: ConfigMap + data: + hello: world + + - name: Create config map + k8s: + definition: "{{ k8s_resource }}" + append_hash: yes + register: k8s_configmap1 + + - name: check configmap is created with a hash + assert: + that: + - k8s_configmap1 is changed + - k8s_configmap1.result.metadata.name != 'config-map-test' + - k8s_configmap1.result.metadata.name[:-10] == 'config-map-test-' + + - name: recreate same config map + k8s: + definition: "{{ k8s_resource }}" + append_hash: yes + register: k8s_configmap2 + + - name: check configmaps are different + assert: + that: + - k8s_configmap2 is not changed + - k8s_configmap1.result.metadata.name == k8s_configmap2.result.metadata.name + + - name: add key to config map + k8s: + definition: + metadata: + name: config-map-test + namespace: append-hash + apiVersion: v1 + kind: ConfigMap + data: + hello: world + another: value + append_hash: yes + register: k8s_configmap3 + + - name: check configmaps are different + assert: + that: + - k8s_configmap3 is changed + - k8s_configmap1.result.metadata.name != k8s_configmap3.result.metadata.name + + always: + - name: ensure that namespace is removed + k8s: + kind: Namespace + name: append-hash + state: absent diff --git a/test/integration/targets/k8s/playbooks/roles/k8s/tasks/main.yml b/test/integration/targets/k8s/playbooks/roles/k8s/tasks/main.yml index 1c7253d69a9..fa5823bd673 100644 --- a/test/integration/targets/k8s/playbooks/roles/k8s/tasks/main.yml +++ b/test/integration/targets/k8s/playbooks/roles/k8s/tasks/main.yml @@ -278,15 +278,16 @@ - testing5 register: k8s_facts + - name: Resources are terminating if still in results assert: that: not item.resources or item.resources[0].status.phase == "Terminating" loop: "{{ k8s_facts.results }}" - include_tasks: crd.yml + - include_tasks: append_hash.yml always: - - name: Delete all namespaces k8s: state: absent diff --git a/test/integration/targets/k8s/runme.sh b/test/integration/targets/k8s/runme.sh index c639eda2c7f..7806f838879 100755 --- a/test/integration/targets/k8s/runme.sh +++ b/test/integration/targets/k8s/runme.sh @@ -28,7 +28,7 @@ ansible-playbook -v playbooks/validate_installed.yml "$@" virtualenv --system-site-packages --python "${PYTHON}" "${MYTMPDIR}/openshift-0.6.0" source "${MYTMPDIR}/openshift-0.6.0/bin/activate" $PYTHON -m pip install openshift==0.6.0 kubernetes==6.0.0 -ansible-playbook -v playbooks/merge_type_fail.yml "$@" +ansible-playbook -v playbooks/older_openshift_fail.yml "$@" # Run full test suite virtualenv --system-site-packages --python "${PYTHON}" "${MYTMPDIR}/openshift-recent"