Skip to content

Commit

Permalink
Added unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
ilia1243 committed Nov 15, 2022
1 parent 8e146ab commit 968bf67
Show file tree
Hide file tree
Showing 15 changed files with 714 additions and 107 deletions.
10 changes: 5 additions & 5 deletions kubemarine/core/cluster.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,6 @@ def is_task_completed(self, task_path) -> bool:
from kubemarine.core import flow
return flow.is_task_completed(self, task_path)

def get_final_inventory(self):
return utils.get_final_inventory(self)

def get_facts_enrichment_fns(self):
return [
"kubemarine.kubernetes.add_node_enrichment",
Expand Down Expand Up @@ -348,7 +345,7 @@ def get_package_association_str_for_group(self, group: NodeGroup,
return results_values[0]
raise Exception(f'Too many values returned for package associations str "{association_key}" for package "{package}"')

def dump_finalized_inventory(self):
def make_finalized_inventory(self):
from kubemarine.core import defaults
from kubemarine.procedures import remove_node
from kubemarine import controlplane, cri, packages
Expand All @@ -367,7 +364,10 @@ def dump_finalized_inventory(self):
for finalize_fn in cluster_finalized_functions:
prepared_inventory = finalize_fn(self, prepared_inventory)

inventory_for_dump = defaults.prepare_for_dump(prepared_inventory, copy=False)
return defaults.prepare_for_dump(prepared_inventory, copy=False)

def dump_finalized_inventory(self):
inventory_for_dump = self.make_finalized_inventory()
data = yaml.dump(inventory_for_dump)
finalized_filename = "cluster_finalized.yaml"
utils.dump_file(self, data, finalized_filename)
Expand Down
4 changes: 2 additions & 2 deletions kubemarine/core/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from kubemarine.core.cluster import KubernetesCluster
from kubemarine.core.errors import KME
from kubemarine import jinja
from kubemarine.core import utils
from kubemarine.core import utils, static
from kubemarine.core.yaml_merger import default_merger
from kubemarine import controlplane

Expand Down Expand Up @@ -447,7 +447,7 @@ def compile_inventory(inventory, cluster):
# convert references in yaml to normal values
iterations = 100
root = deepcopy(inventory)
root['globals'] = cluster.globals
root['globals'] = static.GLOBALS

while iterations > 0:

Expand Down
39 changes: 25 additions & 14 deletions kubemarine/demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,31 +362,49 @@ def _create_connection_from_details(self, ip: str, conn_details: dict, gateway=N
)


def create_silent_context(parser: argparse.ArgumentParser, args: list, procedure: str = None):
args = list(args)
def create_silent_context(args: list = None, parser: argparse.ArgumentParser = None, procedure: str = None):
args = list(args) if args else []
# todo probably increase logging level to get rid of spam in logs.
if '--disable-dump' not in args:
args.append('--disable-dump')

if parser is None:
parser = flow.new_common_parser("Help text")
context = flow.create_context(parser, args, procedure=procedure)
del context['execution_arguments']['ansible_inventory_location']
context['preserve_inventory'] = False

return context


def new_cluster(inventory, procedure=None, fake=True, context: dict = None,
os_name='centos', os_version='7.9', net_interface='eth0'):
def new_cluster(inventory, procedure_inventory=None, context: dict = None,
fake=True) -> Union[KubernetesCluster, FakeKubernetesCluster]:
if context is None:
context = create_silent_context(flow.new_common_parser("Help text"), [], procedure=procedure)
context = create_silent_context()

nodes_context = generate_nodes_context(inventory)
nodes_context.update(context['nodes'])
context['nodes'] = nodes_context

# It is possible to disable FakeCluster and create real cluster Object for some business case
if fake:
cluster = FakeKubernetesCluster(inventory, context, procedure_inventory=procedure_inventory)
else:
cluster = KubernetesCluster(inventory, context, procedure_inventory=procedure_inventory)

cluster.enrich()
return cluster


def generate_nodes_context(inventory: dict, os_name='centos', os_version='7.9', net_interface='eth0') -> dict:
os_family = None

if os_name in ['centos', 'rhel']:
os_family = 'rhel'
elif os_name in ['ubuntu', 'debian']:
os_family = 'debian'

context = {}
for node in inventory['nodes']:
node_context = {
'name': node['name'],
Expand All @@ -405,16 +423,9 @@ def new_cluster(inventory, procedure=None, fake=True, context: dict = None,
connect_to = node['internal_address']
if node.get('address'):
connect_to = node['address']
context['nodes'][connect_to] = node_context

# It is possible to disable FakeCluster and create real cluster Object for some business case
if fake:
cluster = FakeKubernetesCluster(inventory, context)
else:
cluster = KubernetesCluster(inventory, context)
context[connect_to] = node_context

cluster.enrich()
return cluster
return context


def generate_inventory(balancer=1, master=1, worker=1, keepalived=0, haproxy_mntc=0):
Expand Down
28 changes: 15 additions & 13 deletions kubemarine/kubernetes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import math
import time
from copy import deepcopy
from typing import List
from typing import List, Dict, Union

import ruamel.yaml
import yaml
Expand All @@ -30,6 +30,11 @@

version_coredns_path_breakage = "v1.21.2"

ERROR_DOWNGRADE='Kubernetes old version \"%s\" is greater than new one \"%s\"'
ERROR_SAME='Kubernetes old version \"%s\" is the same as new one \"%s\"'
ERROR_MAJOR_RANGE_EXCEEDED='Major version \"%s\" rises to new \"%s\" more than one'
ERROR_MINOR_RANGE_EXCEEDED='Minor version \"%s\" rises to new \"%s\" more than one'


def add_node_enrichment(inventory, cluster):
if cluster.context.get('initial_procedure') != 'add_node':
Expand Down Expand Up @@ -933,7 +938,8 @@ def expect_kubernetes_version(cluster, version, timeout=None, retries=None, node
raise Exception('In the expected time, the nodes did not receive correct Kubernetes version')


def test_version(version):
def test_version(version: Union[list, str]):
version_list: list = version
# catch version without "v" at the first symbol
if isinstance(version, str):
if not version.startswith('v'):
Expand All @@ -957,34 +963,30 @@ def test_version(version):


def test_version_upgrade_possible(old, new, skip_equal=False):
versions = {
versions_unchanged = {
'old': old.strip(),
'new': new.strip()
}
versions_unchanged = versions.copy()
versions: Dict[str, List[int]] = {}

for v_type, version in versions.items():
for v_type, version in versions_unchanged.items():
versions[v_type] = test_version(version)

# test new is greater than old
if tuple(versions['old']) > tuple(versions['new']):
raise Exception('Kubernetes old version \"%s\" is greater than new one \"%s\"'
% (versions_unchanged['old'], versions_unchanged['new']))
raise Exception(ERROR_DOWNGRADE % (versions_unchanged['old'], versions_unchanged['new']))

# test new is the same as old
if tuple(versions['old']) == tuple(versions['new']) and not skip_equal:
raise Exception('Kubernetes old version \"%s\" is the same as new one \"%s\"'
% (versions_unchanged['old'], versions_unchanged['new']))
raise Exception(ERROR_SAME % (versions_unchanged['old'], versions_unchanged['new']))

# test major step is not greater than 1
if versions['new'][0] - versions['old'][0] > 1:
raise Exception('Major version \"%s\" rises to new \"%s\" more than one'
% (versions_unchanged['old'], versions_unchanged['new']))
raise Exception(ERROR_MAJOR_RANGE_EXCEEDED % (versions_unchanged['old'], versions_unchanged['new']))

# test minor step is not greater than 1
if versions['new'][1] - versions['old'][1] > 1:
raise Exception('Minor version \"%s\" rises to new \"%s\" more than one'
% (versions_unchanged['old'], versions_unchanged['new']))
raise Exception(ERROR_MINOR_RANGE_EXCEEDED % (versions_unchanged['old'], versions_unchanged['new']))


def recalculate_proper_timeout(nodes, timeout):
Expand Down
55 changes: 31 additions & 24 deletions kubemarine/packages.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@
from kubemarine.core.yaml_merger import default_merger


ERROR_GLOBAL_ASSOCIATIONS_REDEFINED_MULTIPLE_OS = \
"It is not supported to customize services.packages.associations section " \
"if nodes have different OS families. " \
"Please move the section to corresponding services.packages.associations.<os_family> section."

ERROR_MULTIPLE_PACKAGE_VERSIONS_DETECTED = \
"Multiple package versions detected %s for package '%s'. " \
"Align them to the single version manually or using corresponding task of install procedure. " \
"Alternatively, specify cache_versions=false for corresponding association."


def enrich_inventory_associations(inventory, cluster: KubernetesCluster):
associations: dict = inventory['services']['packages']['associations']
os_propagated_associations = {}
Expand All @@ -35,10 +46,7 @@ def enrich_inventory_associations(inventory, cluster: KubernetesCluster):
if associations:
os_family = cluster.get_os_family()
if os_family == 'multiple':
raise Exception(
"It is not supported to customize services.packages.associations section "
"if nodes have different OS families. "
"Please move the section to corresponding services.packages.associations.<os_family> section.")
raise Exception(ERROR_GLOBAL_ASSOCIATIONS_REDEFINED_MULTIPLE_OS)
elif os_family not in ('unknown', 'unsupported'):
# move remained associations properties to the specific OS family section and merge with priority
default_merger.merge(os_propagated_associations[os_family], associations)
Expand Down Expand Up @@ -159,7 +167,7 @@ def _cache_custom_packages(cluster: KubernetesCluster, inventory: dict,


def _detect_final_package(cluster: KubernetesCluster, detected_packages: Dict[str, Dict[str, List]],
package, ensured_association_only: bool):
package: str, ensured_association_only: bool) -> str:
# add package version to list only if it was found as installed
detected_package_versions = list(filter(lambda version: "not installed" not in version,
detected_packages[package].keys()))
Expand All @@ -169,10 +177,7 @@ def _detect_final_package(cluster: KubernetesCluster, detected_packages: Dict[st
return package
elif len(detected_package_versions) > 1:
if ensured_association_only:
raise Exception(
f"Multiple package versions detected {detected_packages[package]} for package '{package}'. "
f"Align them to the single version manually or using corresponding task of install procedure. "
f"Alternatively, specify cache_versions=false in corresponding association.")
raise Exception(ERROR_MULTIPLE_PACKAGE_VERSIONS_DETECTED % (str(detected_packages[package]), package))
else:
cluster.log.warning(
f"Multiple package versions detected {detected_packages[package]} for package '{package}'. "
Expand All @@ -194,7 +199,7 @@ def remove_unused_os_family_associations(cluster: KubernetesCluster, inventory:


def get_associations_os_family_keys():
return ['debian', 'rhel', 'rhel8']
return {'debian', 'rhel', 'rhel8'}


def get_package_manager(group: NodeGroup) -> apt or yum:
Expand Down Expand Up @@ -236,12 +241,23 @@ def upgrade(group: NodeGroup, include=None, exclude=None, **kwargs) -> NodeGroup
return get_package_manager(group).upgrade(group, include, exclude, **kwargs)


def detect_installed_package_version(group: NodeGroup, package: str, warn=True) -> NodeGroupResult:
def get_detect_package_version_cmd(os_family: str, package_name: str) -> str:
if os_family in ["rhel", "rhel8"]:
cmd = r"rpm -q %s" % package_name
else:
cmd = r"dpkg-query -f '${Package}=${Version}\n' -W %s" % package_name

# This is WA for RemoteExecutor, since any package failed others are not checked
# TODO: get rid of this WA and use warn=True in sudo
cmd += ' || true'
return cmd


def detect_installed_package_version(group: NodeGroup, package: str) -> NodeGroupResult:
"""
Detect package versions for each host on remote group
:param group: Group of nodes, where package should be found
:param package: package name, which version should be detected (eg. 'podman' and 'containerd')
:param warn: Suppress exception for non-found packages
:return: NodeGroupResults with package version on each host
Method generates different package query for different OS.
Expand All @@ -253,16 +269,7 @@ def detect_installed_package_version(group: NodeGroup, package: str, warn=True)
os_family = group.get_nodes_os()
package_name = get_package_name(os_family, package)

if os_family in ["rhel", "rhel8"]:
cmd = r"rpm -q %s" % package_name
else:
cmd = r"dpkg-query -f '${Package}=${Version}\n' -W %s" % package_name

# This is WA for RemoteExecutor, since any package failed others are not checked
# TODO: get rid of this WA and use warn=True in sudo
if warn:
cmd += ' || true'

cmd = get_detect_package_version_cmd(os_family, package_name)
return group.sudo(cmd)


Expand All @@ -286,7 +293,7 @@ def detect_installed_packages_version_groups(group: NodeGroup, packages_list: Li

with RemoteExecutor(cluster) as exe:
for package in packages_list:
detect_installed_package_version(group, package, warn=True)
detect_installed_package_version(group, package)

raw_result = exe.get_last_results()
results: Dict[str, Dict[str, List]] = {}
Expand All @@ -309,7 +316,7 @@ def detect_installed_packages_version_groups(group: NodeGroup, packages_list: Li

def get_package_name(os_family: str, package: str) -> str:
"""
Return the pure package name, whithout any part of version
Return the pure package name, without any part of version
"""

import re
Expand Down
6 changes: 5 additions & 1 deletion kubemarine/procedures/migrate_cri.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,16 @@ def _prepare_packages(cluster: KubernetesCluster, inventory: dict, finalization=
return inventory

if finalization:
# Merge global associations section as it has priority.
# Despite we enrich OS specific section inside system.enrich_upgrade_inventory,
# we still merge global associations section because it has priority during enrichment.
inventory["services"].setdefault("packages", {}).setdefault("associations", {})
default_merger.merge(inventory["services"]["packages"]["associations"],
cluster.procedure_inventory["packages"]["associations"])
else:
# Merge OS family specific section. It is already enriched in packages.enrich_inventory_associations
# This effectively allows to specify only global section but not for specific OS family.
# This restriction is because system.enrich_upgrade_inventory goes after packages.enrich_inventory_associations,
# but in future the restriction can be eliminated.
default_merger.merge(inventory["services"]["packages"]["associations"][cluster.get_os_family()],
cluster.procedure_inventory["packages"]["associations"])

Expand Down
14 changes: 5 additions & 9 deletions kubemarine/procedures/upgrade.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,26 +175,22 @@ def upgrade_finalize_inventory(cluster, inventory):
return inventory
upgrade_version = cluster.context.get("upgrade_version")

if not inventory['services'].get('kubeadm'):
inventory['services']['kubeadm'] = {}
inventory['services']['kubeadm']['kubernetesVersion'] = upgrade_version
inventory.setdefault("services", {}).setdefault("kubeadm", {})['kubernetesVersion'] = upgrade_version

# if thirdparties was not defined in procedure.yaml,
# then no need to forcibly place them: user may want to use default
if cluster.procedure_inventory.get(upgrade_version, {}).get('thirdparties'):
inventory['services']['thirdparties'] = cluster.procedure_inventory[upgrade_version]['thirdparties']

if cluster.procedure_inventory.get(upgrade_version, {}).get("plugins"):
if not inventory.get("plugins"):
inventory["plugins"] = {}
inventory.setdefault("plugins", {})
default_merger.merge(inventory["plugins"], cluster.procedure_inventory[upgrade_version]["plugins"])

if cluster.procedure_inventory.get(upgrade_version, {}).get("packages"):
if not inventory.get("services"):
inventory["services"] = {}
if not inventory["services"].get("packages"):
inventory["services"]["packages"] = {}
inventory['services'].setdefault("packages", {})
packages = cluster.procedure_inventory[upgrade_version]["packages"]
# Despite we enrich OS specific section inside system.enrich_upgrade_inventory,
# we still merge global associations section because it has priority during enrichment.
default_merger.merge(inventory["services"]["packages"], packages)

return inventory
Expand Down
5 changes: 4 additions & 1 deletion kubemarine/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,10 @@ def enrich_upgrade_inventory(inventory: dict, cluster: KubernetesCluster):

upgrade_ver = cluster.context["upgrade_version"]
packages_section = deepcopy(cluster.procedure_inventory.get(upgrade_ver, {}).get("packages", {}))
# move associations to the OS family specific section, and then merge with associations from procedure
# Move associations to the OS family specific section, and then merge with associations from procedure.
# This effectively allows to specify only global section but not for specific OS family.
# This restriction is because system.enrich_upgrade_inventory goes after packages.enrich_inventory_associations,
# but in future the restriction can be eliminated.
associations = packages_section.pop("associations", {})
default_merger.merge(inventory["services"]["packages"]["associations"][os_family], associations)

Expand Down
Loading

0 comments on commit 968bf67

Please sign in to comment.