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:
parent
c3770bf6f2
commit
960ebd981f
11 changed files with 174 additions and 24 deletions
2
changelogs/fragments/k8s_append_hash.yml
Normal file
2
changelogs/fragments/k8s_append_hash.yml
Normal 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
|
|
@ -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
|
||||||
````````````````````
|
````````````````````
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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"
|
||||||
|
|
40
lib/ansible/plugins/filter/k8s.py
Normal file
40
lib/ansible/plugins/filter/k8s.py
Normal 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
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
|
@ -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'"
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in a new issue