k8s append_hash (#48830)

* Add append_hash functionality to k8s module

append_hash adds a hash based on the contents of a ConfigMap
or Secret to the name - this enables immutable ConfigMaps and
Secrets.

* Provide k8s_config_resource_name plugin

The k8s_config_resource_name filter plugin provides a means of determining
the name of ConfigMaps and Secrets created with append_hash

* Add changelog fragment

* fix failing tests

* Update openshift version needed for append_hash
This commit is contained in:
Will Thames 2018-11-22 18:14:43 +10:00 committed by John R Barker
parent c3770bf6f2
commit 960ebd981f
11 changed files with 174 additions and 24 deletions

View file

@ -0,0 +1,2 @@
minor_changes:
- k8s - append_hash parameter adds a hash to the name of ConfigMaps and Secrets for easier immutable resources

View file

@ -1083,6 +1083,32 @@ To escape special characters within a regex, use the "regex_escape" filter::
{{ '^f.*o(.*)$' | regex_escape() }} {{ '^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 Other Useful Filters
```````````````````` ````````````````````

View file

@ -29,6 +29,8 @@ from ansible.module_utils.six import string_types
from ansible.module_utils.k8s.common import KubernetesAnsibleModule from ansible.module_utils.k8s.common import KubernetesAnsibleModule
from ansible.module_utils.common.dict_transformations import dict_merge from ansible.module_utils.common.dict_transformations import dict_merge
from distutils.version import LooseVersion
try: try:
import yaml import yaml
@ -43,6 +45,12 @@ try:
except ImportError: except ImportError:
HAS_KUBERNETES_VALIDATE = False 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): class KubernetesRawModule(KubernetesAnsibleModule):
@ -62,6 +70,7 @@ class KubernetesRawModule(KubernetesAnsibleModule):
argument_spec['wait'] = dict(type='bool', default=False) argument_spec['wait'] = dict(type='bool', default=False)
argument_spec['wait_timeout'] = dict(type='int', default=120) argument_spec['wait_timeout'] = dict(type='int', default=120)
argument_spec['validate'] = dict(type='dict', default=None, options=self.validate_spec) argument_spec['validate'] = dict(type='dict', default=None, options=self.validate_spec)
argument_spec['append_hash'] = dict(type='bool', default=False)
return argument_spec return argument_spec
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -83,6 +92,10 @@ class KubernetesRawModule(KubernetesAnsibleModule):
if self.params['validate']: if self.params['validate']:
if LooseVersion(self.openshift_version) < LooseVersion("0.8.0"): if LooseVersion(self.openshift_version) < LooseVersion("0.8.0"):
self.fail_json(msg="openshift >= 0.8.0 is required for validate") 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 self.params['merge_type']:
if LooseVersion(self.openshift_version) < LooseVersion("0.6.2"): if LooseVersion(self.openshift_version) < LooseVersion("0.6.2"):
self.fail_json(msg="openshift >= 0.6.2 is required for merge_type") 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 self.resource_definitions = resource_definition
else: else:
self.resource_definitions = [resource_definition] self.resource_definitions = [resource_definition]
src = self.params.pop('src') src = self.params.get('src')
if src: if src:
self.resource_definitions = self.load_resource_definitions(src) self.resource_definitions = self.load_resource_definitions(src)
@ -181,7 +194,12 @@ class KubernetesRawModule(KubernetesAnsibleModule):
return result return result
try: 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: except NotFoundError:
# Remove traceback so that it doesn't show up in later failures # Remove traceback so that it doesn't show up in later failures
try: try:
@ -207,7 +225,7 @@ class KubernetesRawModule(KubernetesAnsibleModule):
# Delete the object # Delete the object
if not self.check_mode: if not self.check_mode:
try: try:
k8s_obj = resource.delete(name, namespace=namespace) k8s_obj = resource.delete(**params)
result['result'] = k8s_obj.to_dict() result['result'] = k8s_obj.to_dict()
except DynamicApiError as exc: except DynamicApiError as exc:
self.fail_json(msg="Failed to delete object: {0}".format(exc.body), self.fail_json(msg="Failed to delete object: {0}".format(exc.body),
@ -256,7 +274,7 @@ class KubernetesRawModule(KubernetesAnsibleModule):
k8s_obj = definition k8s_obj = definition
else: else:
try: 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: except DynamicApiError as exc:
msg = "Failed to replace object: {0}".format(exc.body) msg = "Failed to replace object: {0}".format(exc.body)
if self.warnings: if self.warnings:

View file

@ -89,6 +89,16 @@ options:
default: no default: no
type: bool type: bool
version_added: "2.8" 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: requirements:
- "python >= 2.7" - "python >= 2.7"

View file

@ -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
}

View file

@ -3,6 +3,7 @@
gather_facts: no gather_facts: no
vars: vars:
ansible_python_interpreter: "{{ ansible_playbook_python }}" ansible_python_interpreter: "{{ ansible_playbook_python }}"
playbook_namespace: ansible-test-k8s-full
roles: roles:
- k8s - k8s

View file

@ -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

View file

@ -31,7 +31,7 @@
assert: assert:
that: that:
- k8s_append_hash is failed - 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 # merge_type
- include_role: - include_role:
@ -58,4 +58,4 @@
assert: assert:
that: that:
- k8s_validate is failed - 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'"

View file

@ -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

View file

@ -278,15 +278,16 @@
- testing5 - testing5
register: k8s_facts register: k8s_facts
- name: Resources are terminating if still in results - name: Resources are terminating if still in results
assert: assert:
that: not item.resources or item.resources[0].status.phase == "Terminating" that: not item.resources or item.resources[0].status.phase == "Terminating"
loop: "{{ k8s_facts.results }}" loop: "{{ k8s_facts.results }}"
- include_tasks: crd.yml - include_tasks: crd.yml
- include_tasks: append_hash.yml
always: always:
- name: Delete all namespaces - name: Delete all namespaces
k8s: k8s:
state: absent state: absent

View file

@ -28,7 +28,7 @@ ansible-playbook -v playbooks/validate_installed.yml "$@"
virtualenv --system-site-packages --python "${PYTHON}" "${MYTMPDIR}/openshift-0.6.0" virtualenv --system-site-packages --python "${PYTHON}" "${MYTMPDIR}/openshift-0.6.0"
source "${MYTMPDIR}/openshift-0.6.0/bin/activate" source "${MYTMPDIR}/openshift-0.6.0/bin/activate"
$PYTHON -m pip install openshift==0.6.0 kubernetes==6.0.0 $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 # Run full test suite
virtualenv --system-site-packages --python "${PYTHON}" "${MYTMPDIR}/openshift-recent" virtualenv --system-site-packages --python "${PYTHON}" "${MYTMPDIR}/openshift-recent"