Files
kubernetes.core/plugins/module_utils/apply.py
GomathiselviS b066a2dda3 Cleanup GitHub workflows (#655)
* Cleanup gha

* test by removing matrix excludes

* Rename sanity tests

* trigger integration tests

* Fix ansible-lint workflow

* Fix concurrency

* Add ansible-lint config

* Add ansible-lint config

* Fix integration and lint issues

* integration wf

* fix yamllint issues

* fix yamllint issues

* update readme and add ignore-2.16.txt

* fix ansible-doc

* Add version

* Use /dev/random to generate random data

The GHA environment has difficultly generating entropy. Trying to read
from /dev/urandom just blocks forever. We don't care if the random data
is cryptographically secure; it's just garbage data for the test. Read
from /dev/random, instead. This is only used during the k8s_copy test
target.

This also removes the custom test module that was being used to generate
the files. It's not worth maintaining this for two task that can be
replaced with some simple command/shell tasks.

* Fix saniry errors

* test github_action fix

* Address review comments

* Remove default types

* review comments

* isort fixes

* remove tags

* Add setuptools to venv

* Test gh changes

* update changelog

* update ignore-2.16

* Fix indentation in inventory plugin example

* Update .github/workflows/integration-tests.yaml

* Update integration-tests.yaml

---------

Co-authored-by: Mike Graves <mgraves@redhat.com>
Co-authored-by: Bikouo Aubin <79859644+abikouo@users.noreply.github.com>
2023-11-10 16:33:40 +01:00

316 lines
11 KiB
Python

# 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.
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import json
from collections import OrderedDict
from ansible.module_utils.common.dict_transformations import dict_merge
from ansible_collections.kubernetes.core.plugins.module_utils.exceptions import (
ApplyException,
)
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.core import (
gather_versions,
)
from ansible_collections.kubernetes.core.plugins.module_utils.version import (
LooseVersion,
)
try:
from kubernetes.dynamic.exceptions import NotFoundError
except ImportError:
pass
LAST_APPLIED_CONFIG_ANNOTATION = "kubectl.kubernetes.io/last-applied-configuration"
POD_SPEC_SUFFIXES = {
"containers": "name",
"initContainers": "name",
"ephemeralContainers": "name",
"volumes": "name",
"imagePullSecrets": "name",
"containers.volumeMounts": "mountPath",
"containers.volumeDevices": "devicePath",
"containers.env": "name",
"containers.ports": "containerPort",
"initContainers.volumeMounts": "mountPath",
"initContainers.volumeDevices": "devicePath",
"initContainers.env": "name",
"initContainers.ports": "containerPort",
"ephemeralContainers.volumeMounts": "mountPath",
"ephemeralContainers.volumeDevices": "devicePath",
"ephemeralContainers.env": "name",
"ephemeralContainers.ports": "containerPort",
}
POD_SPEC_PREFIXES = [
"Pod.spec",
"Deployment.spec.template.spec",
"DaemonSet.spec.template.spec",
"StatefulSet.spec.template.spec",
"Job.spec.template.spec",
"Cronjob.spec.jobTemplate.spec.template.spec",
]
# patch merge keys taken from generated.proto files under
# staging/src/k8s.io/api in kubernetes/kubernetes
STRATEGIC_MERGE_PATCH_KEYS = {
"Service.spec.ports": "port",
"ServiceAccount.secrets": "name",
"ValidatingWebhookConfiguration.webhooks": "name",
"MutatingWebhookConfiguration.webhooks": "name",
}
STRATEGIC_MERGE_PATCH_KEYS.update(
{
"%s.%s" % (prefix, key): value
for prefix in POD_SPEC_PREFIXES
for key, value in POD_SPEC_SUFFIXES.items()
}
)
def annotate(desired):
return dict(
metadata=dict(
annotations={
LAST_APPLIED_CONFIG_ANNOTATION: json.dumps(
desired, separators=(",", ":"), indent=None, sort_keys=True
)
}
)
)
def apply_patch(actual, desired):
last_applied = (
actual["metadata"].get("annotations", {}).get(LAST_APPLIED_CONFIG_ANNOTATION)
)
if last_applied:
# ensure that last_applied doesn't come back as a dict of unicode key/value pairs
# json.loads can be used if we stop supporting python 2
last_applied = json.loads(last_applied)
patch = merge(
dict_merge(last_applied, annotate(last_applied)),
dict_merge(desired, annotate(desired)),
actual,
)
if patch:
return actual, patch
else:
return actual, actual
else:
return actual, dict_merge(desired, annotate(desired))
def apply_object(resource, definition, server_side=False):
try:
actual = resource.get(
name=definition["metadata"]["name"],
namespace=definition["metadata"].get("namespace"),
)
if server_side:
return actual, None
except NotFoundError:
return None, dict_merge(definition, annotate(definition))
return apply_patch(actual.to_dict(), definition)
def k8s_apply(resource, definition, **kwargs):
existing, desired = apply_object(resource, definition)
server_side = kwargs.get("server_side", False)
if server_side:
versions = gather_versions()
body = definition
if LooseVersion(versions["kubernetes"]) < LooseVersion("25.0.0"):
body = json.dumps(definition).encode()
# server_side_apply is forces content_type to 'application/apply-patch+yaml'
return resource.server_side_apply(
body=body,
name=definition["metadata"]["name"],
namespace=definition["metadata"].get("namespace"),
force_conflicts=kwargs.get("force_conflicts"),
field_manager=kwargs.get("field_manager"),
dry_run=kwargs.get("dry_run"),
)
if not existing:
return resource.create(
body=desired, namespace=definition["metadata"].get("namespace"), **kwargs
)
if existing == desired:
return resource.get(
name=definition["metadata"]["name"],
namespace=definition["metadata"].get("namespace"),
)
return resource.patch(
body=desired,
name=definition["metadata"]["name"],
namespace=definition["metadata"].get("namespace"),
content_type="application/merge-patch+json",
**kwargs
)
# The patch is the difference from actual to desired without deletions, plus deletions
# from last_applied to desired. To find it, we compute deletions, which are the deletions from
# last_applied to desired, and delta, which is the difference from actual to desired without
# deletions, and then apply delta to deletions as a patch, which should be strictly additive.
def merge(last_applied, desired, actual, position=None):
deletions = get_deletions(last_applied, desired)
delta = get_delta(last_applied, actual, desired, position or desired["kind"])
return dict_merge(deletions, delta)
def list_to_dict(lst, key, position):
result = OrderedDict()
for item in lst:
try:
result[item[key]] = item
except KeyError:
raise ApplyException(
"Expected key '%s' not found in position %s" % (key, position)
)
return result
# list_merge applies a strategic merge to a set of lists if the patchMergeKey is known
# each item in the list is compared based on the patchMergeKey - if two values with the
# same patchMergeKey differ, we take the keys that are in last applied, compare the
# actual and desired for those keys, and update if any differ
def list_merge(last_applied, actual, desired, position):
result = list()
if position in STRATEGIC_MERGE_PATCH_KEYS and last_applied:
patch_merge_key = STRATEGIC_MERGE_PATCH_KEYS[position]
last_applied_dict = list_to_dict(last_applied, patch_merge_key, position)
actual_dict = list_to_dict(actual, patch_merge_key, position)
desired_dict = list_to_dict(desired, patch_merge_key, position)
for key in desired_dict:
if key not in actual_dict or key not in last_applied_dict:
result.append(desired_dict[key])
else:
patch = merge(
last_applied_dict[key],
desired_dict[key],
actual_dict[key],
position,
)
result.append(dict_merge(actual_dict[key], patch))
for key in actual_dict:
if key not in desired_dict and key not in last_applied_dict:
result.append(actual_dict[key])
return result
else:
return desired
def recursive_list_diff(list1, list2, position=None):
result = (list(), list())
if position in STRATEGIC_MERGE_PATCH_KEYS:
patch_merge_key = STRATEGIC_MERGE_PATCH_KEYS[position]
dict1 = list_to_dict(list1, patch_merge_key, position)
dict2 = list_to_dict(list2, patch_merge_key, position)
dict1_keys = set(dict1.keys())
dict2_keys = set(dict2.keys())
for key in dict1_keys - dict2_keys:
result[0].append(dict1[key])
for key in dict2_keys - dict1_keys:
result[1].append(dict2[key])
for key in dict1_keys & dict2_keys:
diff = recursive_diff(dict1[key], dict2[key], position)
if diff:
# reinsert patch merge key to relate changes in other keys to
# a specific list element
diff[0].update({patch_merge_key: dict1[key][patch_merge_key]})
diff[1].update({patch_merge_key: dict2[key][patch_merge_key]})
result[0].append(diff[0])
result[1].append(diff[1])
if result[0] or result[1]:
return result
elif list1 != list2:
return (list1, list2)
return None
def recursive_diff(dict1, dict2, position=None):
if not position:
if "kind" in dict1 and dict1.get("kind") == dict2.get("kind"):
position = dict1["kind"]
left = dict((k, v) for (k, v) in dict1.items() if k not in dict2)
right = dict((k, v) for (k, v) in dict2.items() if k not in dict1)
for k in set(dict1.keys()) & set(dict2.keys()):
if position:
this_position = "%s.%s" % (position, k)
if isinstance(dict1[k], dict) and isinstance(dict2[k], dict):
result = recursive_diff(dict1[k], dict2[k], this_position)
if result:
left[k] = result[0]
right[k] = result[1]
elif isinstance(dict1[k], list) and isinstance(dict2[k], list):
result = recursive_list_diff(dict1[k], dict2[k], this_position)
if result:
left[k] = result[0]
right[k] = result[1]
elif dict1[k] != dict2[k]:
left[k] = dict1[k]
right[k] = dict2[k]
if left or right:
return left, right
else:
return None
def get_deletions(last_applied, desired):
patch = {}
for k, last_applied_value in last_applied.items():
desired_value = desired.get(k)
if isinstance(last_applied_value, dict) and isinstance(desired_value, dict):
p = get_deletions(last_applied_value, desired_value)
if p:
patch[k] = p
elif last_applied_value != desired_value:
patch[k] = desired_value
return patch
def get_delta(last_applied, actual, desired, position=None):
patch = {}
for k, desired_value in desired.items():
if position:
this_position = "%s.%s" % (position, k)
actual_value = actual.get(k)
if actual_value is None:
patch[k] = desired_value
elif isinstance(desired_value, dict):
p = get_delta(
last_applied.get(k, {}), actual_value, desired_value, this_position
)
if p:
patch[k] = p
elif isinstance(desired_value, list):
p = list_merge(
last_applied.get(k, []), actual_value, desired_value, this_position
)
if p:
patch[k] = [item for item in p if item is not None]
elif actual_value != desired_value:
patch[k] = desired_value
return patch