From edc48ee5770293af78ac19ac752e7d422b402dae Mon Sep 17 00:00:00 2001 From: Alina Buzachis <49211501+alinabuzachis@users.noreply.github.com> Date: Tue, 20 Apr 2021 14:27:25 +0200 Subject: [PATCH] Add configmap/secret hash functionality (#48) * * * Add configmap/secret hash functionality Signed-off-by: Alina Buzachis * * Add changelog fragment Signed-off-by: Alina Buzachis --- .../fragments/48_hash-configmap-secret.yml | 3 + plugins/module_utils/common.py | 2 +- plugins/module_utils/hashes.py | 67 ++++++++++++++ tests/unit/module_utils/test_hashes.py | 85 +++++++++++++++++ tests/unit/module_utils/test_marshal.py | 92 +++++++++++++++++++ 5 files changed, 248 insertions(+), 1 deletion(-) create mode 100644 changelogs/fragments/48_hash-configmap-secret.yml create mode 100644 plugins/module_utils/hashes.py create mode 100644 tests/unit/module_utils/test_hashes.py create mode 100644 tests/unit/module_utils/test_marshal.py diff --git a/changelogs/fragments/48_hash-configmap-secret.yml b/changelogs/fragments/48_hash-configmap-secret.yml new file mode 100644 index 00000000..8ccb8af6 --- /dev/null +++ b/changelogs/fragments/48_hash-configmap-secret.yml @@ -0,0 +1,3 @@ +--- +minor_changes: +- Add configmap and secret hash functionality (https://github.com/ansible-collections/kubernetes.core/pull/48). diff --git a/plugins/module_utils/common.py b/plugins/module_utils/common.py index de52ac35..b872fb33 100644 --- a/plugins/module_utils/common.py +++ b/plugins/module_utils/common.py @@ -27,6 +27,7 @@ from datetime import datetime from distutils.version import LooseVersion from ansible_collections.kubernetes.core.plugins.module_utils.args_common import (AUTH_ARG_MAP, AUTH_ARG_SPEC) +from ansible_collections.kubernetes.core.plugins.module_utils.hashes import generate_hash from ansible.module_utils.basic import AnsibleModule, missing_required_lib from ansible.module_utils.six import iteritems, string_types @@ -60,7 +61,6 @@ except ImportError: K8S_CONFIG_HASH_IMP_ERR = None try: - from openshift.helper.hashes import generate_hash from openshift.dynamic.exceptions import KubernetesValidateMissing HAS_K8S_CONFIG_HASH = True except ImportError: diff --git a/plugins/module_utils/hashes.py b/plugins/module_utils/hashes.py new file mode 100644 index 00000000..5edbc6d9 --- /dev/null +++ b/plugins/module_utils/hashes.py @@ -0,0 +1,67 @@ +# Copyright [2017] [Red Hat, Inc.] +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Implement ConfigMapHash and SecretHash equivalents +# Based on https://github.com/kubernetes/kubernetes/pull/49961 + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import json +import hashlib + +try: + import string + maketrans = string.maketrans +except AttributeError: + maketrans = str.maketrans + +try: + from collections import OrderedDict +except ImportError: + from orderreddict import OrderedDict + + +def sorted_dict(unsorted_dict): + result = OrderedDict() + for (k, v) in sorted(unsorted_dict.items()): + if isinstance(v, dict): + v = sorted_dict(v) + result[k] = v + return result + + +def generate_hash(resource): + # Get name from metadata + resource['name'] = resource.get('metadata', {}).get('name', '') + if resource['kind'] == 'ConfigMap': + marshalled = marshal(sorted_dict(resource), ['data', 'kind', 'name']) + del(resource['name']) + return encode(marshalled) + if resource['kind'] == 'Secret': + marshalled = marshal(sorted_dict(resource), ['data', 'kind', 'name', 'type']) + del(resource['name']) + return encode(marshalled) + raise NotImplementedError + + +def marshal(data, keys): + ordered = OrderedDict() + for key in keys: + ordered[key] = data.get(key, "") + return json.dumps(ordered, separators=(',', ':')).encode('utf-8') + + +def encode(resource): + return hashlib.sha256(resource).hexdigest()[:10].translate(maketrans("013ae", "ghkmt")) diff --git a/tests/unit/module_utils/test_hashes.py b/tests/unit/module_utils/test_hashes.py new file mode 100644 index 00000000..4c58237b --- /dev/null +++ b/tests/unit/module_utils/test_hashes.py @@ -0,0 +1,85 @@ +# Copyright [2017] [Red Hat, Inc.] +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Test ConfigMapHash and SecretHash equivalents +# tests based on https://github.com/kubernetes/kubernetes/pull/49961 + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from ansible_collections.kubernetes.core.plugins.module_utils.hashes import generate_hash + +tests = [ + dict( + resource=dict( + kind="ConfigMap", + metadata=dict(name="foo"), + data=dict() + ), + expected="867km9574f", + ), + dict( + resource=dict( + kind="ConfigMap", + metadata=dict(name="foo"), + type="my-type", + data=dict() + ), + expected="867km9574f", + ), + dict( + resource=dict( + kind="ConfigMap", + metadata=dict(name="foo"), + data=dict( + key1="value1", + key2="value2") + ), + expected="gcb75dd9gb", + ), + dict( + resource=dict( + kind="Secret", + metadata=dict(name="foo"), + data=dict() + ), + expected="949tdgdkgg", + ), + dict( + resource=dict( + kind="Secret", + metadata=dict(name="foo"), + type="my-type", + data=dict() + ), + expected="dg474f9t76", + ), + + dict( + resource=dict( + kind="Secret", + metadata=dict(name="foo"), + data=dict( + key1="dmFsdWUx", + key2="dmFsdWUy") + ), + expected="tf72c228m4", + ) + +] + + +def test_hashes(): + for test in tests: + assert(generate_hash(test['resource']) == test['expected']) diff --git a/tests/unit/module_utils/test_marshal.py b/tests/unit/module_utils/test_marshal.py new file mode 100644 index 00000000..7d48cea7 --- /dev/null +++ b/tests/unit/module_utils/test_marshal.py @@ -0,0 +1,92 @@ +# Copyright [2017] [Red Hat, Inc.] +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Test ConfigMap and Secret marshalling +# tests based on https://github.com/kubernetes/kubernetes/pull/49961 + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from ansible_collections.kubernetes.core.plugins.module_utils.hashes import marshal, sorted_dict + +tests = [ + dict( + resource=dict( + kind="ConfigMap", + name="", + data=dict(), + ), + expected=b'{"data":{},"kind":"ConfigMap","name":""}' + ), + dict( + resource=dict( + kind="ConfigMap", + name="", + data=dict( + one="" + ), + ), + expected=b'{"data":{"one":""},"kind":"ConfigMap","name":""}' + ), + dict( + resource=dict( + kind="ConfigMap", + name="", + data=dict( + two="2", + one="", + three="3", + ), + ), + expected=b'{"data":{"one":"","three":"3","two":"2"},"kind":"ConfigMap","name":""}' + ), + dict( + resource=dict( + kind="Secret", + type="my-type", + name="", + data=dict(), + ), + expected=b'{"data":{},"kind":"Secret","name":"","type":"my-type"}' + ), + dict( + resource=dict( + kind="Secret", + type="my-type", + name="", + data=dict( + one="" + ), + ), + expected=b'{"data":{"one":""},"kind":"Secret","name":"","type":"my-type"}' + ), + dict( + resource=dict( + kind="Secret", + type="my-type", + name="", + data=dict( + two="Mg==", + one="", + three="Mw==", + ), + ), + expected=b'{"data":{"one":"","three":"Mw==","two":"Mg=="},"kind":"Secret","name":"","type":"my-type"}' + ), +] + + +def test_marshal(): + for test in tests: + assert(marshal(sorted_dict(test['resource']), sorted(list(test['resource'].keys()))) == test['expected'])