From c0f7a895a11ea258b65294e73364bf54e1fb1a84 Mon Sep 17 00:00:00 2001 From: ilia1243 <8808144+ilia1243@users.noreply.github.com> Date: Mon, 15 Jan 2024 12:58:12 +0300 Subject: [PATCH] Add unit tests --- kubemarine/procedures/install.py | 13 +- kubemarine/procedures/upgrade.py | 13 +- test/unit/core/test_cluster.py | 192 +++++++++++++- test/unit/core/test_env.py | 72 +++++- test/unit/core/test_flow.py | 87 ++++++- test/unit/test_migrate_kubemarine.py | 7 +- test/unit/test_packages.py | 9 + test/unit/test_upgrade.py | 361 +++++++++++++++++---------- test/unit/utils.py | 28 ++- 9 files changed, 605 insertions(+), 177 deletions(-) diff --git a/kubemarine/procedures/install.py b/kubemarine/procedures/install.py index 12979bf14..8fc54dc13 100755 --- a/kubemarine/procedures/install.py +++ b/kubemarine/procedures/install.py @@ -611,16 +611,23 @@ def overview(cluster: KubernetesCluster) -> None: def run_tasks(res: DynamicResources, tasks_filter: List[str] = None) -> None: - flow.run_tasks(res, tasks, cumulative_points=cumulative_points, tasks_filter=tasks_filter) + _run_tasks(res, tasks, tasks_filter) + + +def _run_tasks(res: DynamicResources, tasks_: OrderedDict, tasks_filter: List[str] = None) -> None: + flow.run_tasks(res, tasks_, cumulative_points=cumulative_points, tasks_filter=tasks_filter) class InstallAction(Action): - def __init__(self) -> None: + def __init__(self, tasks_: OrderedDict = None) -> None: super().__init__('install') + if tasks_ is None: + tasks_ = tasks + self.action_tasks = tasks_ self.target_version = "not supported" def run(self, res: DynamicResources) -> None: - run_tasks(res) + _run_tasks(res, self.action_tasks) self.target_version = kubernetes.get_kubernetes_version(res.cluster().inventory) diff --git a/kubemarine/procedures/upgrade.py b/kubemarine/procedures/upgrade.py index 56601e28e..bd8f37956 100755 --- a/kubemarine/procedures/upgrade.py +++ b/kubemarine/procedures/upgrade.py @@ -149,21 +149,22 @@ def _run(self, resources: DynamicResources) -> None: class UpgradeAction(Action): - def __init__(self, upgrade_step: int) -> None: + def __init__(self, upgrade_step: int, tasks_: OrderedDict = None) -> None: super().__init__(f'upgrade step {upgrade_step + 1}', recreate_inventory=True) + if tasks_ is None: + tasks_ = tasks + self.action_tasks = copy.deepcopy(tasks_) + if upgrade_step > 0 and 'cleanup_tmp_dir' in self.action_tasks: + del self.action_tasks['cleanup_tmp_dir'] self.upgrade_step = upgrade_step self.upgrade_version = 'not supported' def run(self, res: DynamicResources) -> None: - action_tasks = copy.deepcopy(tasks) - if self.upgrade_step > 0: - del action_tasks['cleanup_tmp_dir'] - context = res.context context['upgrade_step'] = self.upgrade_step res.reset_context(EnrichmentStage.DEFAULT) try: - flow.run_tasks(res, action_tasks) + flow.run_tasks(res, self.action_tasks) finally: cluster = res.cluster_unsafe() procedure_context = ({} if cluster is None or cluster.procedure_context is None diff --git a/test/unit/core/test_cluster.py b/test/unit/core/test_cluster.py index d04502145..033b55229 100644 --- a/test/unit/core/test_cluster.py +++ b/test/unit/core/test_cluster.py @@ -12,12 +12,22 @@ # 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. - - +import collections +import logging +import os.path +import tarfile +import tempfile import unittest +from typing import List, Optional, Set + +import yaml from kubemarine import demo +from kubemarine.core import utils, log, action, resources as res, flow +from kubemarine.core.cluster import EnrichmentStage from kubemarine.demo import FakeKubernetesCluster +from kubemarine.procedures import install, upgrade +from test.unit import utils as test_utils def get_os_family(cluster: FakeKubernetesCluster): @@ -26,8 +36,6 @@ def get_os_family(cluster: FakeKubernetesCluster): class KubernetesClusterTest(unittest.TestCase): - # TODO: add more tests - def setUp(self): self.cluster = demo.new_cluster(demo.generate_inventory(**demo.FULLHA)) @@ -90,7 +98,44 @@ def test_remove_node_different_os_get_os_family_single(self): cluster = demo.new_cluster(inventory, procedure_inventory=remove_node, context=context, nodes_context=nodes_context) self.assertEqual('debian', get_os_family(cluster), - msg="One node has different OS family and thus global OS family should be 'multiple'") + msg="The only node with different OS family is removed, " + "and global OS family should the specific remained") + + def test_remove_node_different_os_get_package_associations(self): + inventory = demo.generate_inventory(**demo.MINIHA_KEEPALIVED) + context = demo.create_silent_context(['fake.yaml'], procedure='remove_node') + host_different_os = inventory['nodes'][0]['address'] + remained_host = inventory['nodes'][1]['address'] + nodes_context = self._nodes_context_one_different_os(inventory, host_different_os) + remove_node = demo.generate_procedure_inventory('remove_node') + remove_node['nodes'] = [{"name": inventory["nodes"][0]["name"]}] + cluster = demo.new_cluster(inventory, procedure_inventory=remove_node, context=context, + nodes_context=nodes_context) + self.assertEqual('conntrack-tools', + cluster.get_package_association_for_node(host_different_os, 'conntrack', 'package_name'), + msg="Unexpected package associations of node to remove") + self.assertEqual('conntrack', + cluster.get_package_association_for_node(remained_host, 'conntrack', 'package_name'), + msg="Unexpected package associations of remained node") + + def test_upgrade_get_redefined_package_associations(self): + context = demo.create_silent_context(['fake.yaml'], procedure='upgrade') + context['upgrade_step'] = 0 + + inventory = demo.generate_inventory(**demo.MINIHA_KEEPALIVED) + inventory['services']['kubeadm'] = { + 'kubernetesVersion': 'v1.27.8' + } + upgrade = demo.generate_procedure_inventory('upgrade') + upgrade['upgrade_plan'] = ['v1.28.4'] + upgrade.setdefault('v1.28.4', {})['packages'] = { + 'associations': {'containerd': {'package_name': 'containerd_new'}} + } + cluster = demo.new_cluster(inventory, procedure_inventory=upgrade, context=context) + self.assertEqual('containerd_new', + cluster.get_package_association_for_node(cluster.nodes['all'].get_any_member().get_host(), + 'containerd', 'package_name'), + msg="Package associations are not redefined") def _nodes_context_one_different_os(self, inventory, host_different_os): nodes_context = demo.generate_nodes_context(inventory, os_name='ubuntu', os_version='20.04') @@ -102,5 +147,142 @@ def _nodes_context_one_different_os(self, inventory, host_different_os): return nodes_context +class FakeResources(test_utils.FakeResources): + def __init__(self, context: dict, inventory: dict, procedure_inventory: dict = None): + super().__init__(context, inventory, procedure_inventory, demo.generate_nodes_context(inventory)) + args: dict = context['execution_arguments'] + self.inventory_filepath = args['config'] + self.procedure_inventory_filepath: Optional[str] = args.get('procedure_config') + + def _store_finalized_inventory(self, finalized_inventory: dict) -> None: + super()._store_finalized_inventory(finalized_inventory) + utils.dump_file(self, yaml.dump(finalized_inventory), "cluster_finalized.yaml") + + +class ClusterStorageTest(unittest.TestCase): + def setUp(self): + self.inventory = demo.generate_inventory(**demo.ALLINONE) + self.procedure_inventory = None + + def prepare_context(self, args: list = None, procedure: str = 'install'): + self.tmpdir = tempfile.TemporaryDirectory() + + self.context = demo.create_silent_context(args, procedure) + self.context['preserve_inventory'] = True + self.args = self.context['execution_arguments'] + self.args['disable_dump'] = False + self.args['dump_location'] = self.tmpdir.name + self.args['config'] = os.path.join(self.tmpdir.name, 'cluster.yaml') + + utils.prepare_dump_directory(self.args['dump_location']) + utils.dump_file(self.context, yaml.dump(self.inventory), self.args['config'], dump_location=False) + if self.procedure_inventory is not None: + self.args['procedure_config'] = os.path.join(self.tmpdir.name, 'procedure.yaml') + utils.dump_file(self.context, yaml.dump(self.procedure_inventory), self.args['procedure_config'], dump_location=False) + + def tearDown(self): + logger = logging.getLogger("k8s.fake.local") + for h in logger.handlers: + if isinstance(h, log.FileHandlerWithHeader): + h.close() + self.tmpdir.cleanup() + + def _run_actions(self, actions: List[action.Action]) -> demo.FakeClusterStorage: + resources = FakeResources(self.context, self.inventory, + procedure_inventory=self.procedure_inventory) + try: + flow.run_actions(resources, actions) + except Exception: + pass + + return resources.cluster(EnrichmentStage.LIGHT).cluster_storage + + def _check_local_archive(self, local_archive_path: str, files: Set[str]): + with tarfile.open(local_archive_path, "r:gz") as tar: + self.assertEqual(files, set(tar.getnames())) + + def test_simple(self): + self.prepare_context() + cluster_storage = self._run_actions([install.InstallAction(collections.OrderedDict())]) + self._check_local_archive( + cluster_storage.uploaded_archives[0], + {'dump/procedure_parameters', + 'dump/cluster_initial.yaml', 'dump/cluster_light.yaml', 'dump/cluster.yaml', 'dump/cluster_finalized.yaml', + 'cluster.yaml', 'version'}) + + def test_upgrade_two_versions(self): + self.inventory['values'] = { + 'before': 'v1.26.11', 'through': 'v1.27.8', 'after': 'v1.28.4', + } + self.inventory['services']['kubeadm'] = { + 'kubernetesVersion': '{{ values.before }}' + } + self.procedure_inventory = demo.generate_procedure_inventory('upgrade') + self.procedure_inventory['upgrade_plan'] = ['{{ values.through }}', '{{ values.after }}'] + + self.prepare_context(['fake.yaml'], procedure='upgrade') + cluster_storage = self._run_actions([upgrade.UpgradeAction(i, collections.OrderedDict()) for i in range(2)]) + self.assertEqual(2, len(cluster_storage.uploaded_archives)) + self._check_local_archive( + cluster_storage.uploaded_archives[0], + {'dump/procedure_parameters', 'dump/procedure.yaml', + 'dump/cluster_initial.yaml', 'dump/cluster_light.yaml', 'dump/cluster_default.yaml', 'dump/cluster.yaml', 'dump/cluster_finalized.yaml', + 'cluster.yaml', 'version'}) + self._check_local_archive( + cluster_storage.uploaded_archives[1], + {'dump/procedure_parameters', 'dump/procedure.yaml', + 'dump/cluster_initial.yaml', 'dump/cluster.yaml', 'dump/cluster_finalized.yaml', + 'cluster.yaml', 'version'}) + + self.assertEqual({'debug.log', 'v1.27.8', 'v1.28.4'}, + set(os.listdir(os.path.join(self.tmpdir.name, 'dump')))) + + def test_upgrade_second_version_failed(self): + self.inventory['values'] = { + 'before': 'v1.26.11', 'through': 'v1.27.8', 'after': 'v1.28.4', + } + self.inventory['services']['kubeadm'] = { + 'kubernetesVersion': '{{ values.before }}' + } + self.procedure_inventory = demo.generate_procedure_inventory('upgrade') + self.procedure_inventory['upgrade_plan'] = ['{{ values.through }}', '{{ values.after }}'] + + self.prepare_context(['fake.yaml'], procedure='upgrade') + + def failed_task(_: FakeKubernetesCluster): + raise Exception("test") + + cluster_storage = self._run_actions([ + upgrade.UpgradeAction(0, collections.OrderedDict()), + upgrade.UpgradeAction(1, collections.OrderedDict({"test": failed_task})), + ]) + self.assertEqual(1, len(cluster_storage.uploaded_archives)) + self._check_local_archive( + cluster_storage.uploaded_archives[0], + {'dump/procedure_parameters', 'dump/procedure.yaml', + 'dump/cluster_initial.yaml', 'dump/cluster_light.yaml', 'dump/cluster_default.yaml', 'dump/cluster.yaml', 'dump/cluster_finalized.yaml', + 'cluster.yaml', 'version'}) + + self.assertEqual({'debug.log', 'v1.27.8', 'v1.28.4'}, + set(os.listdir(os.path.join(self.tmpdir.name, 'dump')))) + + def test_run_two_actions_second_failed(self): + self.prepare_context() + + def failed_action(_: res.DynamicResources): + raise Exception("test") + + cluster_storage = self._run_actions([ + test_utils.new_action("test_cluster1", action_=lambda resources: resources.cluster()), + test_utils.new_action("test_cluster2", action_=failed_action), + ]) + self.assertEqual(1, len(cluster_storage.uploaded_archives)) + self._check_local_archive( + cluster_storage.uploaded_archives[0], + {'dump/procedure_parameters', + 'dump/cluster_initial.yaml', 'dump/cluster_light.yaml', 'dump/cluster.yaml', + 'cluster.yaml', 'version'}) + + if __name__ == '__main__': unittest.main() diff --git a/test/unit/core/test_env.py b/test/unit/core/test_env.py index 2d20c7ddb..e5123391b 100644 --- a/test/unit/core/test_env.py +++ b/test/unit/core/test_env.py @@ -16,25 +16,19 @@ import os import tempfile import unittest -from typing import Dict, Optional +from typing import Dict, Optional, List from unittest import mock from kubemarine import demo, plugins -from kubemarine.core import utils, log, flow -from kubemarine.procedures import install +from kubemarine.core import utils, log, flow, action +from kubemarine.procedures import install, upgrade from test.unit import utils as test_utils class TestEnvironmentVariables(unittest.TestCase): def setUp(self): - self.tmpdir = tempfile.TemporaryDirectory() self.inventory = demo.generate_inventory(**demo.ALLINONE) - self.context = demo.create_silent_context(['--without-act']) - args = self.context['execution_arguments'] - args['disable_dump'] = False - args['dump_location'] = self.tmpdir.name - utils.prepare_dump_directory(args['dump_location']) - + self.procedure_inventory = None self.resources: Optional[demo.FakeResources] = None def tearDown(self): @@ -44,16 +38,29 @@ def tearDown(self): h.close() self.tmpdir.cleanup() + def prepare_context(self, args: list = None, procedure: str = 'install'): + self.tmpdir = tempfile.TemporaryDirectory() + + self.context = demo.create_silent_context(args, procedure) + self.args = self.context['execution_arguments'] + self.args['disable_dump'] = False + self.args['dump_location'] = self.tmpdir.name + utils.prepare_dump_directory(self.args['dump_location']) + def _new_resources(self) -> demo.FakeResources: return test_utils.FakeResources(self.context, self.inventory, + procedure_inventory=self.procedure_inventory, nodes_context=demo.generate_nodes_context(self.inventory)) - def _run(self, mock_environ: Dict[str, str]): + def _run(self, mock_environ: Dict[str, str], actions: List[action.Action] = None): self.resources = self._new_resources() with mock.patch.dict(os.environ, mock_environ): - flow.run_actions(self.resources, [install.InstallAction()]) + if actions is None: + actions = [install.InstallAction()] + flow.run_actions(self.resources, actions) def test_simple_miscellaneous_env_variables(self): + self.prepare_context(['--without-act']) self.inventory['values'] = { 'variable': '{{ env.ENV_NAME }}', } @@ -76,6 +83,7 @@ def test_simple_miscellaneous_env_variables(self): self.assertEqual('password123', config['password']) def test_substring_jinja_env_variables(self): + self.prepare_context(['--without-act']) self.inventory['plugins'] = {'my_plugin': {'installation': {'procedures': [ {'helm': { 'chart_path': __file__, @@ -94,6 +102,7 @@ def test_substring_jinja_env_variables(self): self.assertEqual('1.2.3', values['version']) def test_expression_jinja_env_variables(self): + self.prepare_context(['--without-act']) self.inventory['values'] = { 'variable': '{{ env.ENV_NAME1 | default("not defined") }}', } @@ -102,6 +111,7 @@ def test_expression_jinja_env_variables(self): self.assertEqual('not defined', inventory['values']['variable']) def test_recursive_env_variables(self): + self.prepare_context(['--without-act']) self.inventory['values'] = { 'variable1': '{{ values.variable3 }}', 'variable2': '{{ env.ENV_NAME }}', @@ -114,6 +124,7 @@ def test_recursive_env_variables(self): self.assertEqual('value-recursive', inventory['values']['variable3']) def test_plugin_template_apply_env_variables(self): + self.prepare_context() template_file = os.path.join(self.tmpdir.name, 'template.yaml.j2') with utils.open_external(template_file, 'w') as t: t.write('Some {{ env.ENV_VAR }}\n') @@ -134,6 +145,43 @@ def test_plugin_template_apply_env_variables(self): compiled_template = utils.read_external(os.path.join(self.tmpdir.name, 'dump', 'template.yaml')) self.assertIn('Some env_value', compiled_template, "Env variable should be expanded in dump files.") + def test_kubernetes_version_env_variable(self): + self.prepare_context(['--without-act']) + self.inventory['services']['kubeadm'] = { + 'kubernetesVersion': "{{ env.KUBERNETES_VERSION }}" + } + + self._run({'KUBERNETES_VERSION': 'v1.28.4'}) + + inventory = self.resources.working_inventory + self.assertEqual('v1.28.4', inventory['services']['kubeadm']['kubernetesVersion']) + self.assertEqual('https://storage.googleapis.com/kubernetes-release/release/v1.28.4/bin/linux/amd64/kubeadm', + inventory['services']['thirdparties']['/usr/bin/kubeadm']['source']) + + def test_kubernetes_version_upgrade_env_variable(self): + self.prepare_context(['fake_path.yaml', '--without-act'], procedure='upgrade') + self.inventory['services']['kubeadm'] = { + 'kubernetesVersion': "{{ env.KUBERNETES_VERSION }}" + } + self.inventory['services']['packages'] = { + 'associations': {'containerd': {'package_name': 'containerd_old'}} + } + self.procedure_inventory = demo.generate_procedure_inventory('upgrade') + self.procedure_inventory['upgrade_plan'] = ['{{ env.UPGRADE_VERSION }}'] + self.procedure_inventory.setdefault('{{ env.UPGRADE_VERSION }}', {})['packages'] = { + 'associations': {'containerd': {'package_name': 'containerd_new'}} + } + + self._run({'KUBERNETES_VERSION': 'v1.27.8', 'UPGRADE_VERSION': 'v1.28.4'}, + [upgrade.UpgradeAction(0)]) + + inventory = self.resources.working_inventory + self.assertEqual('v1.28.4', inventory['services']['kubeadm']['kubernetesVersion']) + self.assertEqual('https://storage.googleapis.com/kubernetes-release/release/v1.28.4/bin/linux/amd64/kubeadm', + inventory['services']['thirdparties']['/usr/bin/kubeadm']['source']) + self.assertEqual('containerd_new', + inventory['services']['packages']['associations']['rhel']['containerd']['package_name']) + if __name__ == '__main__': unittest.main() diff --git a/test/unit/core/test_flow.py b/test/unit/core/test_flow.py index 350721ff6..a01ae3956 100755 --- a/test/unit/core/test_flow.py +++ b/test/unit/core/test_flow.py @@ -22,7 +22,7 @@ import invoke -from kubemarine.core import flow, static, errors, utils +from kubemarine.core import flow, static, utils from kubemarine import demo from test.unit import utils as test_utils @@ -291,6 +291,7 @@ def test_detect_nodes_context(self): "Here should be all 4 calls of test_func") self.assertEqual("rhel", cluster.get_os_family()) + self.assertEqual(len(hosts), len(cluster.nodes_context)) for host, node_context in cluster.nodes_context.items(): self.assertEqual({'online': True, 'accessible': True, 'sudo': 'Root'}, node_context["access"]) self.assertEqual({'name': 'centos', 'version': '7.6', 'family': 'rhel'}, node_context["os"]) @@ -322,15 +323,58 @@ def test_any_offline_node_interrupts(self): context = demo.create_silent_context() res = demo.FakeResources(context, inventory, fake_shell=self.light_fake_shell) - exc = None - try: + with test_utils.assert_raises_kme(self, 'KME0006', escape=True, offline=[offline], inaccessible=[]): flow.run_tasks(res, tasks) - except Exception as e: - exc = e - self.assertIsNotNone(exc, msg="Exception should be raised") - self.assertIsInstance(exc, errors.FailException, msg="Exception should be raised") - self.assertTrue(f"['{offline}'] are not reachable." in str(exc.reason)) + def test_upgrade_all_nodes_inaccessible(self): + inventory = demo.generate_inventory(**demo.MINIHA_KEEPALIVED) + inventory['services']['kubeadm'] = { + 'kubernetesVersion': 'v1.27.8' + } + procedure_inventory = demo.generate_procedure_inventory('upgrade') + procedure_inventory['upgrade_plan'] = ['v1.28.4'] + + self._stub_detect_nodes_context(inventory, [], []) + context = demo.create_silent_context(['fake_path.yaml'], procedure='upgrade') + context['upgrade_step'] = 0 + res = demo.FakeResources(context, inventory, procedure_inventory=procedure_inventory, + fake_shell=self.light_fake_shell) + + with test_utils.assert_raises_kme(self, 'KME0006', escape=True, + offline=[node['address'] for node in inventory['nodes']], inaccessible=[]): + flow.run_tasks(res, tasks) + + def test_migrate_cri_all_nodes_inaccessible(self): + inventory = demo.generate_inventory(**demo.MINIHA_KEEPALIVED) + inventory['services']['cri'] = { + 'containerRuntime': 'docker' + } + procedure_inventory = demo.generate_procedure_inventory('migrate_cri') + + self._stub_detect_nodes_context(inventory, [], []) + context = demo.create_silent_context(['fake_path.yaml'], procedure='migrate_cri') + res = demo.FakeResources(context, inventory, procedure_inventory=procedure_inventory, + fake_shell=self.light_fake_shell) + + with test_utils.assert_raises_kme(self, 'KME0006', escape=True, + offline=[node['address'] for node in inventory['nodes']], inaccessible=[]): + flow.run_tasks(res, tasks) + + def test_all_nodes_offline_check_iaas(self): + inventory = demo.generate_inventory(**demo.FULLHA_KEEPALIVED) + self._stub_detect_nodes_context(inventory, [], []) + context = demo.create_silent_context(procedure='check_iaas') + res = demo.FakeResources(context, inventory, fake_shell=self.light_fake_shell) + flow.run_tasks(res, tasks) + cluster = res.cluster() + self.assertEqual(4, cluster.context["test_info"], + "Here should be all 4 calls of test_func") + + self.assertEqual("", cluster.get_os_family()) + for host, node_context in cluster.nodes_context.items(): + self.assertEqual({'online': False, 'accessible': False, 'sudo': "No"}, node_context["access"]) + self.assertEqual({'name': "", 'version': "", 'family': ""}, node_context["os"]) + self.assertEqual("", node_context["active_interface"]) def test_any_removed_node_can_be_offline(self): inventory = demo.generate_inventory(**demo.FULLHA_KEEPALIVED) @@ -349,10 +393,35 @@ def test_any_removed_node_can_be_offline(self): # no exception should occur flow.run_tasks(res, tasks) + def test_detect_nodes_context_removed_node_online(self): + inventory = demo.generate_inventory(**demo.FULLHA_KEEPALIVED) + hosts = [node["address"] for node in inventory["nodes"]] + self._stub_detect_nodes_context(inventory, hosts, hosts) + + i = random.randrange(len(inventory["nodes"])) + procedure_inventory = demo.generate_procedure_inventory('remove_node') + procedure_inventory["nodes"] = [{"name": inventory["nodes"][i]["name"]}] + + context = demo.create_silent_context(['fake_path.yaml'], procedure='remove_node') + res = demo.FakeResources(context, inventory, procedure_inventory=procedure_inventory, + fake_shell=self.light_fake_shell) + + flow.run_tasks(res, tasks) + cluster = res.cluster() + self.assertEqual(4, cluster.context["test_info"], + "Here should be all 4 calls of test_func") + + self.assertEqual("rhel", cluster.get_os_family()) + self.assertEqual(len(hosts), len(cluster.nodes_context)) + for host, node_context in cluster.nodes_context.items(): + self.assertEqual({'online': True, 'accessible': True, 'sudo': 'Root'}, node_context["access"]) + self.assertEqual({'name': 'centos', 'version': '7.6', 'family': 'rhel'}, node_context["os"]) + self.assertEqual('eth0', node_context["active_interface"]) + def test_kubernetes_version_not_allowed(self): k8s_versions = list(sorted(static.KUBERNETES_VERSIONS["compatibility_map"], key=utils.version_key)) k8s_latest = k8s_versions[-1] - not_allowed_version = test_utils.increment_version(k8s_latest) + not_allowed_version = test_utils.increment_version(k8s_latest) inventory = demo.generate_inventory(**demo.ALLINONE) inventory['services'].setdefault('kubeadm', {})['kubernetesVersion'] = not_allowed_version diff --git a/test/unit/test_migrate_kubemarine.py b/test/unit/test_migrate_kubemarine.py index b6843d277..4759f9ca3 100644 --- a/test/unit/test_migrate_kubemarine.py +++ b/test/unit/test_migrate_kubemarine.py @@ -24,7 +24,6 @@ from kubemarine.core.action import Action from kubemarine.core.cluster import EnrichmentStage, KubernetesCluster from kubemarine.core.patch import Patch, InventoryOnlyPatch, RegularPatch -from kubemarine.core.resources import DynamicResources from kubemarine.core.yaml_merger import default_merger from kubemarine.procedures import migrate_kubemarine from kubemarine.procedures.migrate_kubemarine import ( @@ -117,17 +116,13 @@ def _new_patch(self, id_: str, *, inventory_only: bool) -> Patch: if inventory_only: derive_from = InventoryOnlyPatch - class TheAction(Action): - def run(self, res: DynamicResources) -> None: - return - class ThePatch(derive_from): def __init__(self): super().__init__(id_) @property def action(self) -> Action: - return TheAction(id_) + return test_utils.new_action(id_) @property def description(self) -> str: diff --git a/test/unit/test_packages.py b/test/unit/test_packages.py index d74bbc0b7..f46113c44 100644 --- a/test/unit/test_packages.py +++ b/test/unit/test_packages.py @@ -216,6 +216,15 @@ def test_error_if_global_section_redefined_for_add_node_different_os(self): with self.assertRaisesRegex(Exception, packages.ERROR_GLOBAL_ASSOCIATIONS_REDEFINED_MULTIPLE_OS): demo.new_cluster(inventory, procedure_inventory=add_node, context=context, nodes_context=nodes_context) + def test_no_error_if_global_section_redefined_for_check_iaas_all_nodes_inaccessible(self): + inventory = demo.generate_inventory(**demo.MINIHA_KEEPALIVED) + expected_pkgs = 'docker-ce' + package_associations(inventory, None, 'docker')['package_name'] = expected_pkgs + context = demo.create_silent_context(procedure='check_iaas') + nodes_context = {node['address']: demo.generate_node_context(accessible=False) for node in inventory['nodes']} + # no error + demo.new_cluster(inventory, context=context, nodes_context=nodes_context) + def test_success_if_os_specific_section_redefined_for_add_node_different_os(self): inventory = demo.generate_inventory(**demo.MINIHA_KEEPALIVED) expected_pkgs = 'docker-ce' diff --git a/test/unit/test_upgrade.py b/test/unit/test_upgrade.py index 8c1717028..eadef6ac5 100755 --- a/test/unit/test_upgrade.py +++ b/test/unit/test_upgrade.py @@ -15,12 +15,12 @@ import itertools import re import unittest -from copy import deepcopy -from typing import List, Optional, Tuple +from typing import List +from unittest import mock from kubemarine import kubernetes from kubemarine.core import errors, utils as kutils, static, log, flow -from kubemarine.procedures import upgrade +from kubemarine.procedures import upgrade, install from kubemarine import demo from test.unit import utils @@ -88,94 +88,104 @@ def latest_patch_k8s_versions(self) -> List[str]: for _, versions in itertools.groupby(self.k8s_versions(), key=kutils.minor_version)] -def generate_upgrade_environment(old) -> Tuple[dict, dict]: - inventory = demo.generate_inventory(**demo.MINIHA_KEEPALIVED) - inventory['services']['kubeadm'] = { - 'kubernetesVersion': old - } - context = demo.create_silent_context(['fake_path.yaml', '--without-act'], procedure='upgrade') - return inventory, context - - def set_cri(inventory: dict, cri: str): inventory.setdefault('services', {}).setdefault('cri', {})['containerRuntime'] = cri -class UpgradeDefaultsEnrichment(unittest.TestCase): +class _AbstractUpgradeEnrichmentTest(unittest.TestCase): + def setUpVersions(self, old: str, _new: List[str]): + self.old = old + self._new = _new + self.inventory = demo.generate_inventory(**demo.MINIHA_KEEPALIVED) + self.inventory['services']['kubeadm'] = { + 'kubernetesVersion': old + } + self.context = demo.create_silent_context(['fake_path.yaml', '--without-act'], procedure='upgrade') + self.nodes_context = demo.generate_nodes_context(self.inventory) + self.upgrade = demo.generate_procedure_inventory('upgrade') + self.upgrade['upgrade_plan'] = _new + for new in _new: + self.upgrade[new] = {} + + @property + def new(self) -> str: + if len(self._new) != 1: + raise ValueError("Multiple upgrade versions") + + return self._new[0] - def prepare_inventory(self, old, new): - self.inventory, self.context = generate_upgrade_environment(old) + def run_actions(self) -> demo.FakeResources: + resources = utils.FakeResources(self.context, self.inventory, + procedure_inventory=self.upgrade, nodes_context=self.nodes_context) + actions = [upgrade.UpgradeAction(i) for i in range(len(self._new))] + flow.run_actions(resources, actions) + return resources + + def new_cluster(self): self.context['upgrade_step'] = 0 - self.upgrade = demo.generate_procedure_inventory('upgrade') - self.upgrade['upgrade_plan'] = [new] + return demo.new_cluster(self.inventory, procedure_inventory=self.upgrade, + context=self.context, nodes_context=self.nodes_context) + - def _new_cluster(self): - return demo.new_cluster(self.inventory, procedure_inventory=self.upgrade, context=self.context) +class UpgradeDefaultsEnrichment(_AbstractUpgradeEnrichmentTest): def test_correct_inventory(self): old_kubernetes_version = 'v1.24.2' new_kubernetes_version = 'v1.24.11' - self.prepare_inventory(old_kubernetes_version, new_kubernetes_version) - cluster = self._new_cluster() + self.setUpVersions(old_kubernetes_version, [new_kubernetes_version]) + cluster = self.new_cluster() self.assertEqual(new_kubernetes_version, cluster.inventory['services']['kubeadm']['kubernetesVersion']) def test_upgrade_with_default_admission(self): # Upgrade PSP->PSP kuber version old_kubernetes_version = 'v1.24.2' new_kubernetes_version = 'v1.24.11' - self.prepare_inventory(old_kubernetes_version, new_kubernetes_version) - cluster = self._new_cluster() + self.setUpVersions(old_kubernetes_version, [new_kubernetes_version]) + cluster = self.new_cluster() self.assertEqual("psp", cluster.inventory['rbac']['admission']) # Upgrade PSS->PSS kuber version old_kubernetes_version = 'v1.25.2' new_kubernetes_version = 'v1.25.7' - self.prepare_inventory(old_kubernetes_version, new_kubernetes_version) - cluster = self._new_cluster() + self.setUpVersions(old_kubernetes_version, [new_kubernetes_version]) + cluster = self.new_cluster() self.assertEqual("pss", cluster.inventory['rbac']['admission']) # Upgrade PSP->PSS kuber version old_kubernetes_version = 'v1.24.11' new_kubernetes_version = 'v1.25.7' - self.prepare_inventory(old_kubernetes_version, new_kubernetes_version) + self.setUpVersions(old_kubernetes_version, [new_kubernetes_version]) with self.assertRaisesRegex(Exception, "PSP is not supported in Kubernetes v1.25 or higher"): - self._new_cluster() + self.new_cluster() def test_incorrect_disable_eviction(self): old_kubernetes_version = 'v1.24.2' new_kubernetes_version = 'v1.24.11' - self.prepare_inventory(old_kubernetes_version, new_kubernetes_version) + self.setUpVersions(old_kubernetes_version, [new_kubernetes_version]) self.upgrade['disable-eviction'] = 'true' with self.assertRaisesRegex(errors.FailException, r"Actual instance type is 'string'\. Expected: 'boolean'\."): - self._new_cluster() + self.new_cluster() -class UpgradePackagesEnrichment(unittest.TestCase): +class UpgradePackagesEnrichment(_AbstractUpgradeEnrichmentTest): def setUp(self): - self.old = 'v1.24.2' - self.new = 'v1.24.11' - self.inventory, self.context = generate_upgrade_environment(self.old) - self.context['upgrade_step'] = 0 + self.setUpVersions('v1.24.2', ['v1.24.11']) + + def setUpVersions(self, old: str, _new: List[str]): + super().setUpVersions(old, _new) self.nodes_context = demo.generate_nodes_context(self.inventory, os_name='ubuntu', os_version='20.04') self.inventory['services'].update({'packages': {'associations': { 'docker': {}, 'containerd': {}, }}}) set_cri(self.inventory, 'containerd') - self.upgrade = demo.generate_procedure_inventory('upgrade') - self.upgrade['upgrade_plan'] = [self.new] - self.upgrade[self.new] = { - 'packages': { + for new in _new: + self.upgrade[new]['packages'] = { 'associations': { 'docker': {}, 'containerd': {} } } - } - - def _new_cluster(self): - return demo.new_cluster(deepcopy(self.inventory), procedure_inventory=deepcopy(self.upgrade), - context=self.context, nodes_context=self.nodes_context) def _patch_globals(self, package: str, os_family: str, *, equal=False): package_compatibility = static.GLOBALS['compatibility_map']['software'][package] @@ -189,7 +199,7 @@ def test_enrich_packages_propagate_associations(self): set_cri(self.inventory, 'docker') self.upgrade[self.new]['packages']['associations']['docker']['package_name'] = 'docker-ce' self.upgrade[self.new]['packages']['install'] = ['curl'] - cluster = self._new_cluster() + cluster = self.new_cluster() self.assertEqual(['curl'], cluster.inventory['services']['packages']['install']['include'], "Custom packages are enriched incorrectly") self.assertEqual('docker-ce', cluster.inventory['services']['packages']['associations']['debian']['docker']['package_name'], @@ -199,7 +209,7 @@ def test_final_inventory_enrich_global(self): set_cri(self.inventory, 'docker') self.upgrade[self.new]['packages']['associations']['docker']['package_name'] = 'docker-ce' self.upgrade[self.new]['packages']['install'] = ['curl'] - cluster = self._new_cluster() + cluster = self.new_cluster() final_inventory = cluster.formatted_inventory self.assertEqual(['curl'], final_inventory['services']['packages']['install']['include'], "Custom packages are enriched incorrectly") @@ -212,7 +222,28 @@ def test_require_package_redefinition(self): utils.assert_raises_kme(self, "KME0010", package='containerd', previous_version_spec='.*', next_version_spec='.*'): self._patch_globals('containerd', 'debian', equal=True) - self._new_cluster() + self.new_cluster() + + def test_require_package_redefinition_version_templates(self): + for template in (False, True): + with self.subTest(f"template: {template}"), \ + utils.assert_raises_kme( + self, "KME0010", escape=True, + package='containerd', + previous_version_spec=' for version v1.27.8', next_version_spec=' for version v1.28.4'): + target_version = 'v1.28.4' if not template else '{{ values.after }}' + self.setUpVersions('{{ values.before }}', + ['{{ values.through1 }}', '{{ values.through2 }}', target_version]) + self.inventory['values'] = { + 'before': 'v1.26.3', 'through1': 'v1.26.11', 'through2': 'v1.27.8', + } + if template: + self.inventory['values']['after'] = 'v1.28.4' + self.inventory['services']['packages']['associations']['containerd']['package_name'] = 'containerd-redefined' + self.upgrade['{{ values.through1 }}']['packages']['associations']['containerd']['package_name'] = 'containerd-upgrade1' + self.upgrade['{{ values.through2 }}']['packages']['associations']['containerd']['package_name'] = 'containerd-upgrade2' + + self.run_actions() def test_compatibility_upgrade_required(self): for os_name, os_family, os_version in ( @@ -233,7 +264,7 @@ def test_compatibility_upgrade_required(self): os_name=os_name, os_version=os_version) set_cri(self.inventory, cri) - cluster = self._new_cluster() + cluster = self.new_cluster() self.assertEqual(expected_upgrade_required, cri in cluster.context["upgrade"]["required"]['packages'], f"CRI was {'not' if expected_upgrade_required else 'unexpectedly'} scheduled for upgrade") @@ -261,7 +292,7 @@ def test_procedure_inventory_upgrade_required_inventory_default(self): self.setUp() self.upgrade[self.new]['packages']['associations']['containerd']['package_name'] = procedure_associations - cluster = self._new_cluster() + cluster = self.new_cluster() self.assertEqual(expected_upgrade_required, 'containerd' in cluster.context["upgrade"]["required"]['packages'], f"CRI was {'not' if expected_upgrade_required else 'unexpectedly'} scheduled for upgrade") @@ -278,18 +309,42 @@ def test_procedure_inventory_upgrade_required_inventory_redefined(self): self.inventory['services']['packages']['associations']['containerd']['package_name'] = 'containerd-inventory' self.upgrade[self.new]['packages']['associations']['containerd']['package_name'] = procedure_associations - cluster = self._new_cluster() + cluster = self.new_cluster() self.assertEqual(expected_upgrade_required, 'containerd' in cluster.context["upgrade"]["required"]['packages'], f"CRI was {'not' if expected_upgrade_required else 'unexpectedly'} scheduled for upgrade") + def test_no_custom_packages_upgrade_not_required(self): + self._run_upgrade_packages_and_check(False) + + def test_custom_packages_upgrade_not_required(self): + self.inventory['services']['packages']['install'] = ['curl'] + self._run_upgrade_packages_and_check(False) + + def test_custom_packages_procedure_extended_upgrade_required(self): + self.inventory['services']['packages']['install'] = ['curl'] + self.upgrade[self.new]['packages']['install'] = ['unzip', {'<<': 'merge'}] + self._run_upgrade_packages_and_check(True) + + def test_procedure_upgrade_custom_packages_upgrade_required(self): + self.upgrade[self.new]['packages']['upgrade'] = ['unzip'] + self._run_upgrade_packages_and_check(True) + + def _run_upgrade_packages_and_check(self, called: bool): + args = self.context['execution_arguments'] + args['without_act'] = False + args['tasks'] = 'packages' + with mock.patch.object(install, install.manage_custom_packages.__name__) as run: + self.run_actions() + self.assertEqual(called, run.called, f"Upgrade was {'not' if called else 'unexpectedly'} run") + def test_final_inventory_merge_packages(self): self.inventory['services']['packages'].setdefault('install', {})['include'] = ['curl'] self.upgrade[self.new]['packages']['install'] = ['unzip', {'<<': 'merge'}] self.inventory['services']['packages'].setdefault('upgrade', {})['exclude'] = ['conntrack'] self.upgrade[self.new]['packages'].setdefault('upgrade', {})['exclude'] = [{'<<': 'merge'}, 'socat'] - cluster = self._new_cluster() + cluster = self.new_cluster() self.assertEqual(['unzip', 'curl'], cluster.inventory['services']['packages']['install']['include']) self.assertEqual(['conntrack', 'socat'], cluster.inventory['services']['packages']['upgrade']['exclude']) @@ -309,20 +364,15 @@ def test_final_inventory_merge_packages(self): self.assertIsNone(final_inventory['services']['packages']['upgrade'].get('include')) -class UpgradePluginsEnrichment(unittest.TestCase): +class UpgradePluginsEnrichment(_AbstractUpgradeEnrichmentTest): def setUp(self): - self.old = 'v1.24.2' - self.new = 'v1.24.11' - self.inventory, self.context = generate_upgrade_environment(self.old) - self.context['upgrade_step'] = 0 - self.inventory['plugins'] = {} - self.upgrade = demo.generate_procedure_inventory('upgrade') - self.upgrade['upgrade_plan'] = [self.new] - self.upgrade[self.new] = {'plugins': {}} + self.setUpVersions('v1.24.2', ['v1.24.11']) - def _new_cluster(self): - return demo.new_cluster(deepcopy(self.inventory), procedure_inventory=deepcopy(self.upgrade), - context=self.context) + def setUpVersions(self, old: str, _new: List[str]): + super().setUpVersions(old, _new) + self.inventory['plugins'] = {} + for new in _new: + self.upgrade[new]['plugins'] = {} def _patch_globals(self, plugin: str, *, equal=False): fake_version = static.KUBERNETES_VERSIONS['compatibility_map'][self.old][plugin] @@ -337,7 +387,7 @@ def test_redefine_image_recursive(self): self.inventory['plugins'].setdefault('kubernetes-dashboard', {}).setdefault('dashboard', {})['image'] = 'A' self.upgrade[self.new]['plugins'].setdefault('kubernetes-dashboard', {}).setdefault('dashboard', {})['image'] = 'B' - cluster = self._new_cluster() + cluster = self.new_cluster() self.assertEqual('B', cluster.inventory['plugins']['kubernetes-dashboard']['dashboard']['image'], "Image was not enriched from procedure inventory") @@ -349,7 +399,7 @@ def test_require_image_redefinition_recursive(self): key='image', plugin_name='calico', previous_version_spec='.*', next_version_spec='.*'): self._patch_globals('calico', equal=True) - self._new_cluster() + self.new_cluster() def test_require_helper_pod_image_redefinition(self): self.inventory['plugins'].setdefault('local-path-provisioner', {})['helper-pod-image'] = 'A' @@ -358,7 +408,7 @@ def test_require_helper_pod_image_redefinition(self): key='helper-pod-image', plugin_name='local-path-provisioner', previous_version_spec='.*', next_version_spec='.*'): self._patch_globals('local-path-provisioner', equal=True) - self._new_cluster() + self.new_cluster() def test_require_version_redefinition(self): fake_version = static.KUBERNETES_VERSIONS['compatibility_map'][self.old]['nginx-ingress-controller'] @@ -368,23 +418,40 @@ def test_require_version_redefinition(self): key='version', plugin_name='nginx-ingress-controller', previous_version_spec='.*', next_version_spec='.*'): self._patch_globals('nginx-ingress-controller', equal=True) - self._new_cluster() + self.new_cluster() + + def test_require_image_redefinition_version_templates(self): + for template in (False, True): + with self.subTest(f"template: {template}"), \ + utils.assert_raises_kme( + self, "KME0009", escape=True, + key='image', plugin_name='kubernetes-dashboard', + previous_version_spec=' for version v1.27.8', next_version_spec=' for next version v1.28.4'): + target_version = 'v1.28.4' if not template else '{{ values.after }}' + self.setUpVersions('{{ values.before }}', + ['{{ values.through1 }}', '{{ values.through2 }}', target_version]) + self.inventory['values'] = { + 'before': 'v1.26.3', 'through1': 'v1.26.11', 'through2': 'v1.27.8', + } + if template: + self.inventory['values']['after'] = 'v1.28.4' + + self.inventory['plugins'].setdefault('kubernetes-dashboard', {}).setdefault('dashboard', {})['image'] = 'dashboard-redefined' + self.upgrade['{{ values.through1 }}']['plugins'].setdefault('kubernetes-dashboard', {}).setdefault('dashboard', {})['image'] = 'dashboard-upgrade1' + self.upgrade['{{ values.through2 }}']['plugins'].setdefault('kubernetes-dashboard', {}).setdefault('dashboard', {})['image'] = 'dashboard-upgrade2' + self.run_actions() -class ThirdpartiesEnrichment(unittest.TestCase): + +class ThirdpartiesEnrichment(_AbstractUpgradeEnrichmentTest): def setUp(self): - self.old = 'v1.24.2' - self.new = 'v1.24.11' - self.inventory, self.context = generate_upgrade_environment(self.old) - self.context['upgrade_step'] = 0 - self.inventory['services']['thirdparties'] = {} - self.upgrade = demo.generate_procedure_inventory('upgrade') - self.upgrade['upgrade_plan'] = [self.new] - self.upgrade[self.new] = {'thirdparties': {}} + self.setUpVersions('v1.24.2', ['v1.24.11']) - def _new_cluster(self): - return demo.new_cluster(deepcopy(self.inventory), procedure_inventory=deepcopy(self.upgrade), - context=self.context) + def setUpVersions(self, old: str, _new: List[str]): + super().setUpVersions(old, _new) + self.inventory['services']['thirdparties'] = {} + for new in _new: + self.upgrade[new]['thirdparties'] = {} def test_final_inventory(self): self.inventory['services']['thirdparties']['/usr/bin/kubeadm'] = { @@ -405,7 +472,7 @@ def test_final_inventory(self): self.upgrade[self.new]['thirdparties']['/custom1'] = 'custom1-new' self.upgrade[self.new]['thirdparties']['/custom2'] = 'custom2-new' - cluster = self._new_cluster() + cluster = self.new_cluster() thirdparties_section = cluster.inventory['services']['thirdparties'] self.assertEqual(all_thirdparties, set(thirdparties_section.keys())) @@ -443,7 +510,7 @@ def test_enrich_upgrade_unpack(self): set_cri(self.inventory, 'containerd') self.upgrade[self.new]['thirdparties']['/usr/bin/crictl.tar.gz'] = 'crictl-new' - cluster = self._new_cluster() + cluster = self.new_cluster() thirdparties_section = cluster.inventory['services']['thirdparties'] self.assertEqual('crictl-new', thirdparties_section['/usr/bin/crictl.tar.gz']['source']) self.assertEqual('/usr/bin/', thirdparties_section['/usr/bin/crictl.tar.gz']['unpack']) @@ -453,7 +520,7 @@ def test_require_source_redefinition(self): with utils.assert_raises_kme(self, "KME0011", key='source', thirdparty='/usr/bin/kubelet', previous_version_spec='.*', next_version_spec='.*'): - self._new_cluster() + self.new_cluster() def test_require_sha1_redefinition(self): self.inventory['services']['thirdparties']['/usr/bin/kubectl'] = { @@ -465,32 +532,48 @@ def test_require_sha1_redefinition(self): with utils.assert_raises_kme(self, "KME0011", key='sha1', thirdparty='/usr/bin/kubectl', previous_version_spec='.*', next_version_spec='.*'): - self._new_cluster() + self.new_cluster() + + def test_require_source_redefinition_version_templates(self): + for template in (False, True): + with self.subTest(f"template: {template}"), \ + utils.assert_raises_kme( + self, "KME0011", escape=True, + key='source', thirdparty='/usr/bin/kubeadm', + previous_version_spec=' for version v1.27.8', next_version_spec=' for next version v1.28.4'): + target_version = 'v1.28.4' if not template else '{{ values.after }}' + self.setUpVersions('{{ values.before }}', + ['{{ values.through1 }}', '{{ values.through2 }}', target_version]) + self.inventory['values'] = { + 'before': 'v1.26.3', 'through1': 'v1.26.11', 'through2': 'v1.27.8', + } + if template: + self.inventory['values']['after'] = 'v1.28.4' + self.inventory['services']['thirdparties']['/usr/bin/kubeadm'] = 'kubeadm-redefined' + self.upgrade['{{ values.through1 }}']['thirdparties']['/usr/bin/kubeadm'] = { + 'source': 'kubectl-upgrade1' + } + self.upgrade['{{ values.through2 }}']['thirdparties']['/usr/bin/kubeadm'] = 'kubectl-upgrade2' + self.run_actions() -class UpgradeContainerdConfigEnrichment(unittest.TestCase): + +class UpgradeContainerdConfigEnrichment(_AbstractUpgradeEnrichmentTest): def setUp(self): - self.old = 'v1.25.7' - self.new = 'v1.26.11' - self.inventory, self.context = generate_upgrade_environment(self.old) - self.context['upgrade_step'] = 0 + self.setUpVersions('v1.25.7', ['v1.26.11']) + + def setUpVersions(self, old: str, _new: List[str]): + super().setUpVersions(old, _new) self.nodes_context = demo.generate_nodes_context(self.inventory, os_name='ubuntu', os_version='20.04') self.inventory['services']['cri'].setdefault('containerdConfig', {})\ .setdefault('plugins."io.containerd.grpc.v1.cri"', {}) set_cri(self.inventory, 'containerd') - self.upgrade = demo.generate_procedure_inventory('upgrade') - self.upgrade['upgrade_plan'] = [self.new] - self.upgrade[self.new] = { - 'cri': { + for new in _new: + self.upgrade[new]['cri'] = { 'containerdConfig': { 'plugins."io.containerd.grpc.v1.cri"': {}, } } - } - - def _new_cluster(self): - return demo.new_cluster(deepcopy(self.inventory), procedure_inventory=deepcopy(self.upgrade), - context=self.context, nodes_context=self.nodes_context) def _patch_globals(self, fake_version: str, *, equal=False): package_compatibility = static.GLOBALS['compatibility_map']['software']['pause'] @@ -507,7 +590,7 @@ def test_enrich_and_finalize_inventory(self): self._grpc_cri(self.inventory['services'])['sandbox_image'] = 'pause-redefined' self._grpc_cri(self.upgrade[self.new])['sandbox_image'] = 'pause-new' - cluster = self._new_cluster() + cluster = self.new_cluster() sandbox_image = self._grpc_cri(cluster.inventory['services'])['sandbox_image'] self.assertEqual('pause-new', sandbox_image, "containerdConfig is enriched incorrectly") @@ -524,12 +607,32 @@ def test_require_sandbox_image_redefinition(self): with utils.backup_globals(), \ utils.assert_raises_kme(self, "KME0013", previous_version_spec='.*', next_version_spec='.*'): self._patch_globals('1.2', equal=True) - self._new_cluster() + self.new_cluster() + + def test_require_sandbox_image_redefinition_version_templates(self): + for template in (False, True): + with self.subTest(f"template: {template}"), \ + utils.assert_raises_kme( + self, "KME0013", escape=True, + previous_version_spec=' for version v1.27.8', next_version_spec=' for version v1.28.4'): + target_version = 'v1.28.4' if not template else '{{ values.after }}' + self.setUpVersions('{{ values.before }}', + ['{{ values.through1 }}', '{{ values.through2 }}', target_version]) + self.inventory['values'] = { + 'before': 'v1.26.3', 'through1': 'v1.26.11', 'through2': 'v1.27.8', + } + if template: + self.inventory['values']['after'] = 'v1.28.4' + self._grpc_cri(self.inventory['services'])['sandbox_image'] = 'pause-redefined' + self._grpc_cri(self.upgrade['{{ values.through1 }}'])['sandbox_image'] = 'pause-upgrade1' + self._grpc_cri(self.upgrade['{{ values.through2 }}'])['sandbox_image'] = 'pause-upgrade2' + + self.run_actions() def test_containerd_config_simple_upgrade_required(self): with utils.backup_globals(): self._patch_globals('1.2', equal=False) - cluster = self._new_cluster() + cluster = self.new_cluster() self.assertEqual(True, cluster.context["upgrade"]["required"]['containerdConfig'], "Containerd config was not scheduled for upgrade") @@ -547,7 +650,7 @@ def test_procedure_inventory_upgrade_required_inventory_default(self): self.setUp() self._grpc_cri(self.upgrade[self.new])['sandbox_image'] = f'registry.k8s.io/pause:{procedure_version}' - cluster = self._new_cluster() + cluster = self.new_cluster() self.assertEqual(expected_upgrade_required, cluster.context["upgrade"]["required"]['containerdConfig'], f"Containerd config was {'not' if expected_upgrade_required else 'unexpectedly'} " @@ -565,27 +668,17 @@ def test_procedure_inventory_upgrade_required_inventory_redefined(self): self._grpc_cri(self.inventory['services'])['sandbox_image'] = 'pause-inventory' self._grpc_cri(self.upgrade[self.new])['sandbox_image'] = procedure_pause - cluster = self._new_cluster() + cluster = self.new_cluster() self.assertEqual(expected_upgrade_required, cluster.context["upgrade"]["required"]['containerdConfig'], f"Containerd config was {'not' if expected_upgrade_required else 'unexpectedly'} " f"scheduled for upgrade") -class InventoryRecreation(unittest.TestCase): - def prepare_inventory(self, upgrade_plan: List[str]): - self.old = 'v1.24.2' - self.inventory, self.context = generate_upgrade_environment(self.old) +class InventoryRecreation(_AbstractUpgradeEnrichmentTest): + def setUpVersions(self, old: str, _new: List[str]): + super().setUpVersions(old, _new) self.inventory.setdefault('rbac', {})['admission'] = 'pss' - self.nodes_context = demo.generate_nodes_context(self.inventory) - self.upgrade = demo.generate_procedure_inventory('upgrade') - self.upgrade['upgrade_plan'] = upgrade_plan - self.actions = [] - for i, ver in enumerate(upgrade_plan): - self.upgrade[ver] = {} - self.actions.append(upgrade.UpgradeAction(i)) - - self.resources: Optional[demo.FakeResources] = None def package_names(self, services: dict, package: str, package_names) -> None: services.setdefault('packages', {}).setdefault('associations', {}) \ @@ -595,36 +688,42 @@ def sandbox_image(self, services: dict, sandbox_image: str) -> None: services.setdefault('cri', {}).setdefault('containerdConfig', {})\ .setdefault('plugins."io.containerd.grpc.v1.cri"', {})['sandbox_image'] = sandbox_image - def run_actions(self): - self.resources = utils.FakeResources(self.context, self.inventory, - procedure_inventory=self.upgrade, nodes_context=self.nodes_context) - flow.run_actions(self.resources, self.actions) - def test_plugins_iterative_image_redefinition(self): - self.prepare_inventory(['v1.24.11', 'v1.25.7', 'v1.26.11']) + self.setUpVersions('v1.24.2', ['v1.24.11', 'v1.25.7', 'v1.26.11']) self.upgrade['v1.25.7'].setdefault('plugins', {}).setdefault('calico', {}).setdefault('cni', {})['image'] = 'A' self.upgrade['v1.26.11'].setdefault('plugins', {}).setdefault('calico', {}).setdefault('cni', {})['image'] = 'B' - self.run_actions() + resources = self.run_actions() - actual_image = self.resources.inventory()['plugins']['calico']['cni']['image'] + actual_image = resources.inventory()['plugins']['calico']['cni']['image'] self.assertEqual('B', actual_image, "Plugin image was not redefined in recreated inventory.") + def test_plugins_iterative_custom_property_redefinition_target_unspecified(self): + self.setUpVersions('v1.24.2', ['v1.24.11', 'v1.25.7', 'v1.26.11']) + self.upgrade['v1.24.11'].setdefault('plugins', {}).setdefault('custom-plugin', {})['property'] = 'A' + self.upgrade['v1.25.7'].setdefault('plugins', {}).setdefault('custom-plugin', {})['property'] = 'B' + + resources = self.run_actions() + + final_property = resources.inventory().get('plugins', {}).get('custom-plugin', {}).get('property') + self.assertEqual('B', final_property, + "Custom property was not redefined in recreated inventory.") + def test_packages_iterative_package_names_redefinition(self): - self.prepare_inventory(['v1.24.11', 'v1.25.7', 'v1.26.11']) + self.setUpVersions('v1.24.2', ['v1.24.11', 'v1.25.7', 'v1.26.11']) set_cri(self.inventory, 'containerd') self.package_names(self.upgrade['v1.25.7'], 'containerd', 'A') self.package_names(self.upgrade['v1.26.11'], 'containerd', 'B') - self.run_actions() + resources = self.run_actions() - actual_package = self.resources.inventory()['services']['packages']['associations']['containerd']['package_name'] + actual_package = resources.inventory()['services']['packages']['associations']['containerd']['package_name'] self.assertEqual('B', actual_package, "Containerd packages associations were not redefined in recreated inventory.") def test_thirdparties_iterative_source_redefinition(self): - self.prepare_inventory(['v1.24.11', 'v1.25.7', 'v1.26.11']) + self.setUpVersions('v1.24.2', ['v1.24.11', 'v1.25.7', 'v1.26.11']) set_cri(self.inventory, 'containerd') self.upgrade['v1.25.7'].setdefault('thirdparties', {})['/usr/bin/calicoctl'] = 'A' self.upgrade['v1.26.11'].setdefault('thirdparties', {})['/usr/bin/calicoctl'] = { @@ -632,23 +731,23 @@ def test_thirdparties_iterative_source_redefinition(self): 'sha1': 'fake-sha1' } - self.run_actions() + resources = self.run_actions() - actual_thirdparty = self.resources.inventory()['services']['thirdparties']['/usr/bin/calicoctl'] + actual_thirdparty = resources.inventory()['services']['thirdparties']['/usr/bin/calicoctl'] self.assertEqual('B', actual_thirdparty['source'], "Source of /usr/bin/calicoctl was not redefined in recreated inventory.") self.assertEqual('fake-sha1', actual_thirdparty['sha1'], "sha1 of /usr/bin/calicoctl was not redefined in recreated inventory.") def test_iterative_sandbox_image_redefinition(self): - self.prepare_inventory(['v1.24.11', 'v1.25.7', 'v1.26.11']) + self.setUpVersions('v1.24.2', ['v1.24.11', 'v1.25.7', 'v1.26.11']) set_cri(self.inventory, 'containerd') self.sandbox_image(self.upgrade['v1.25.7'], 'A') self.sandbox_image(self.upgrade['v1.26.11'], 'B') - self.run_actions() + resources = self.run_actions() - actual_image = self.resources.inventory()['services']['cri']['containerdConfig']\ + actual_image = resources.inventory()['services']['cri']['containerdConfig']\ ['plugins."io.containerd.grpc.v1.cri"']['sandbox_image'] self.assertEqual('B', actual_image, "Containerd config was not redefined in recreated inventory.") diff --git a/test/unit/utils.py b/test/unit/utils.py index 7fd70bdf6..4997dc6a3 100644 --- a/test/unit/utils.py +++ b/test/unit/utils.py @@ -11,13 +11,14 @@ # 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. +import re import unittest from contextlib import contextmanager from copy import deepcopy -from typing import Dict, Any +from typing import Dict, Callable, Any from kubemarine import demo, packages -from kubemarine.core import utils, errors, static +from kubemarine.core import utils, errors, static, action, resources as res class FakeResources(demo.FakeResources): @@ -83,9 +84,16 @@ def increment_version(version: str, minor=False): @contextmanager -def assert_raises_kme(test: unittest.TestCase, code: str, **kwargs): - expected = errors.KME(code, **kwargs) - with test.assertRaisesRegex(errors.KME, str(expected)): +def assert_raises_kme(test: unittest.TestCase, code: str, *, escape: bool = False, **kwargs): + if code == 'KME0006': + exception = errors.KME0006(**kwargs) + else: + exception = errors.KME(code, **kwargs) + + msg_pattern = str(exception) + if escape: + msg_pattern = re.escape(msg_pattern) + with test.assertRaisesRegex(type(exception), msg_pattern): try: yield except errors.FailException as e: @@ -99,3 +107,13 @@ def backup_globals(): yield finally: static.GLOBALS = backup + + +def new_action(id_: str, *, action_: Callable[[res.DynamicResources], Any] = None) -> action.Action: + + class TheAction(action.Action): + def run(self, resources: res.DynamicResources) -> None: + if action_ is not None: + action_(resources) + + return TheAction(id_)