From 5630c24d49625bf96ef023594729aa8109f4f850 Mon Sep 17 00:00:00 2001 From: Devatoria Date: Tue, 4 Jun 2019 17:01:54 +0200 Subject: [PATCH 1/2] Rework standard actions Signed-off-by: Devatoria --- chaosk8s/__init__.py | 10 ++- chaosk8s/actions.py | 131 +++++++------------------------- chaosk8s/deployment/__init__.py | 0 chaosk8s/deployment/actions.py | 79 +++++++++++++++++++ chaosk8s/pod/actions.py | 30 +++++++- chaosk8s/replicaset/__init__.py | 0 chaosk8s/replicaset/actions.py | 33 ++++++++ chaosk8s/service/__init__.py | 0 chaosk8s/service/actions.py | 15 ++++ 9 files changed, 187 insertions(+), 111 deletions(-) create mode 100644 chaosk8s/deployment/__init__.py create mode 100644 chaosk8s/deployment/actions.py create mode 100644 chaosk8s/replicaset/__init__.py create mode 100644 chaosk8s/replicaset/actions.py create mode 100644 chaosk8s/service/__init__.py create mode 100644 chaosk8s/service/actions.py diff --git a/chaosk8s/__init__.py b/chaosk8s/__init__.py index bf233c0..895c8f2 100644 --- a/chaosk8s/__init__.py +++ b/chaosk8s/__init__.py @@ -1,14 +1,11 @@ # -*- coding: utf-8 -*- -import json import os import os.path from typing import List from chaoslib.discovery.discover import discover_actions, discover_probes, \ initialize_discovery_result -from chaoslib.exceptions import DiscoveryFailed -from chaoslib.types import Discovery, DiscoveredActivities, \ - DiscoveredSystemInfo, Secrets +from chaoslib.types import Discovery, DiscoveredActivities, Secrets from kubernetes import client, config from logzero import logger @@ -139,3 +136,8 @@ def load_exported_activities() -> List[DiscoveredActivities]: activities.extend(discover_probes("chaosk8s.pod.probes")) activities.extend(discover_actions("chaosk8s.node.actions")) return activities + + +def _log_deprecated(name: str, alt_name: str): + logger.warning("{} function is DEPRECATED and will be removed in the next releases, please use {} instead".format( + name, alt_name)) diff --git a/chaosk8s/actions.py b/chaosk8s/actions.py index 303254e..c90ff48 100644 --- a/chaosk8s/actions.py +++ b/chaosk8s/actions.py @@ -1,123 +1,44 @@ # -*- coding: utf-8 -*- -import json -import os.path - -import yaml -from chaoslib.exceptions import ActivityFailed from chaoslib.types import Secrets -from kubernetes import client -from kubernetes.client.rest import ApiException -from logzero import logger -from chaosk8s import create_k8s_api_client +from chaosk8s import _log_deprecated +from chaosk8s.deployment.actions import create_deployment, delete_deployment, scale_deployment +from chaosk8s.replicaset.actions import delete_replica_set +from chaosk8s.pod.actions import delete_pods +from chaosk8s.service.actions import delete_service __all__ = ["start_microservice", "kill_microservice", "scale_microservice", "remove_service_endpoint"] - -def start_microservice(spec_path: str, ns: str = "default", - secrets: Secrets = None): +def start_microservice(spec_path: str, ns: str = "default", secrets: Secrets = None): """ - Start a microservice described by the deployment config, which must be the - path to the JSON or YAML representation of the deployment. + !!!DEPRECATED!!! """ - api = create_k8s_api_client(secrets) - - with open(spec_path) as f: - p, ext = os.path.splitext(spec_path) - if ext == '.json': - deployment = json.loads(f.read()) - elif ext in ['.yml', '.yaml']: - deployment = yaml.load(f.read()) - else: - raise ActivityFailed( - "cannot process {path}".format(path=spec_path)) - - v1 = client.AppsV1beta1Api(api) - resp = v1.create_namespaced_deployment(ns, body=deployment) - return resp + _log_deprecated("start_microservice", "create_deployment") + create_deployment(spec_path, ns, secrets) - -def kill_microservice(name: str, ns: str = "default", - label_selector: str = "name in ({name})", - secrets: Secrets = None): +def remove_service_endpoint(name: str, ns: str = "default", secrets: Secrets = None): """ - Kill a microservice by `name` in the namespace `ns`. - - The microservice is killed by deleting the deployment for it without - a graceful period to trigger an abrupt termination. - - The selected resources are matched by the given `label_selector`. + !!!DEPRECATED!!! """ - label_selector = label_selector.format(name=name) - api = create_k8s_api_client(secrets) - - v1 = client.AppsV1beta1Api(api) - if label_selector: - ret = v1.list_namespaced_deployment(ns, label_selector=label_selector) - else: - ret = v1.list_namespaced_deployment(ns) - - logger.debug("Found {d} deployments named '{n}'".format( - d=len(ret.items), n=name)) - - body = client.V1DeleteOptions() - for d in ret.items: - res = v1.delete_namespaced_deployment( - d.metadata.name, ns, body) - - v1 = client.ExtensionsV1beta1Api(api) - if label_selector: - ret = v1.list_namespaced_replica_set(ns, label_selector=label_selector) - else: - ret = v1.list_namespaced_replica_set(ns) + _log_deprecated("remove_service_endpoint", "delete_service") + delete_service(name, ns, secrets) - logger.debug("Found {d} replica sets named '{n}'".format( - d=len(ret.items), n=name)) - - body = client.V1DeleteOptions() - for r in ret.items: - res = v1.delete_namespaced_replica_set( - r.metadata.name, ns, body) - - v1 = client.CoreV1Api(api) - if label_selector: - ret = v1.list_namespaced_pod(ns, label_selector=label_selector) - else: - ret = v1.list_namespaced_pod(ns) - - logger.debug("Found {d} pods named '{n}'".format( - d=len(ret.items), n=name)) - - body = client.V1DeleteOptions() - for p in ret.items: - res = v1.delete_namespaced_pod( - p.metadata.name, ns, body) - - -def remove_service_endpoint(name: str, ns: str = "default", - secrets: Secrets = None): +def scale_microservice(name: str, replicas: int, ns: str = "default", + secrets: Secrets = None): """ - Remove the service endpoint that sits in front of microservices (pods). + !!!DEPRECATED!!! """ - api = create_k8s_api_client(secrets) + _log_deprecated("scale_microserviceal", "scale_deployment") + scale_deployment(name, replicas, ns, secrets) - v1 = client.CoreV1Api(api) - v1.delete_namespaced_service(name, namespace=ns) - - -def scale_microservice(name: str, replicas: int, ns: str = "default", - secrets: Secrets = None): +def kill_microservice(name: str, ns: str = "default", + label_selector: str = "name in ({name})", + secrets: Secrets = None): """ - Scale a deployment up or down. The `name` is the name of the deployment. + !!!DEPRECATED!!! """ - api = create_k8s_api_client(secrets) - - v1 = client.ExtensionsV1beta1Api(api) - body = {"spec": {"replicas": replicas}} - try: - v1.patch_namespaced_deployment_scale(name, namespace=ns, body=body) - except ApiException as e: - raise ActivityFailed( - "failed to scale '{s}' to {r} replicas: {e}".format( - s=name, r=replicas, e=str(e))) + _log_deprecated("kill_microservice", "delete_deployment/delete_replica_set/delete_pods") + delete_deployment(name, ns, label_selector, secrets) + delete_replica_set(name, ns, label_selector, secrets) + delete_pods(name, ns, label_selector, secrets) diff --git a/chaosk8s/deployment/__init__.py b/chaosk8s/deployment/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/chaosk8s/deployment/actions.py b/chaosk8s/deployment/actions.py new file mode 100644 index 0000000..6532833 --- /dev/null +++ b/chaosk8s/deployment/actions.py @@ -0,0 +1,79 @@ +import json +import os.path +import yaml + +from chaoslib.exceptions import ActivityFailed +from chaoslib.types import Secrets +from kubernetes import client +from logzero import logger +from kubernetes.client.rest import ApiException + +from chaosk8s import create_k8s_api_client + +__all__ = ["create_deployment", "delete_deployment"] + + +def create_deployment(spec_path: str, ns: str = "default", secrets: Secrets = None): + """ + Create a deployment described by the deployment config, which must be the + path to the JSON or YAML representation of the deployment. + """ + api = create_k8s_api_client(secrets) + + with open(spec_path) as f: + p, ext = os.path.splitext(spec_path) + if ext == '.json': + deployment = json.loads(f.read()) + elif ext in ['.yml', '.yaml']: + deployment = yaml.load(f.read()) + else: + raise ActivityFailed( + "cannot process {path}".format(path=spec_path)) + + v1 = client.AppsV1beta1Api(api) + resp = v1.create_namespaced_deployment(ns, body=deployment) + return resp + + +def delete_deployment(name: str, ns: str = "default", + label_selector: str = "name in ({name})", + secrets: Secrets = None): + """ + Delete a deployment by `name` in the namespace `ns`. + + The deployment is deleted without a graceful period to trigger an abrupt termination. + + The selected resources are matched by the given `label_selector`. + """ + label_selector = label_selector.format(name=name) + api = create_k8s_api_client(secrets) + + v1 = client.AppsV1beta1Api(api) + if label_selector: + ret = v1.list_namespaced_deployment(ns, label_selector=label_selector) + else: + ret = v1.list_namespaced_deployment(ns) + + logger.debug("Found {d} deployments named '{n}'".format( + d=len(ret.items), n=name)) + + body = client.V1DeleteOptions() + for d in ret.items: + v1.delete_namespaced_deployment(d.metadata.name, ns, body) + + +def scale_deployment(name: str, replicas: int, ns: str = "default", + secrets: Secrets = None): + """ + Scale a deployment up or down. The `name` is the name of the deployment. + """ + api = create_k8s_api_client(secrets) + + v1 = client.ExtensionsV1beta1Api(api) + body = {"spec": {"replicas": replicas}} + try: + v1.patch_namespaced_deployment_scale(name, namespace=ns, body=body) + except ApiException as e: + raise ActivityFailed( + "failed to scale '{s}' to {r} replicas: {e}".format( + s=name, r=replicas, e=str(e))) diff --git a/chaosk8s/pod/actions.py b/chaosk8s/pod/actions.py index 3f78e2f..5d30e73 100644 --- a/chaosk8s/pod/actions.py +++ b/chaosk8s/pod/actions.py @@ -10,7 +10,7 @@ from chaosk8s import create_k8s_api_client -__all__ = ["terminate_pods"] +__all__ = ["terminate_pods", "delete_pods"] def terminate_pods(label_selector: str = None, name_pattern: str = None, @@ -93,4 +93,30 @@ def terminate_pods(label_selector: str = None, name_pattern: str = None, body = client.V1DeleteOptions(grace_period_seconds=grace_period) for p in pods: - res = v1.delete_namespaced_pod(p.metadata.name, ns, body=body) + v1.delete_namespaced_pod(p.metadata.name, ns, body=body) + + +def delete_pods(name: str, ns: str = "default", + label_selector: str = "name in ({name})", + secrets: Secrets = None): + """ + Delete pods by `name` in the namespace `ns`. + + The pods are deleted without a graceful period to trigger an abrupt termination. + + The selected resources are matched by the given `label_selector`. + """ + label_selector = label_selector.format(name=name) + api = create_k8s_api_client(secrets) + v1 = client.CoreV1Api(api) + if label_selector: + ret = v1.list_namespaced_pod(ns, label_selector=label_selector) + else: + ret = v1.list_namespaced_pod(ns) + + logger.debug("Found {d} pods named '{n}'".format( + d=len(ret.items), n=name)) + + body = client.V1DeleteOptions() + for p in ret.items: + v1.delete_namespaced_pod(p.metadata.name, ns, body) diff --git a/chaosk8s/replicaset/__init__.py b/chaosk8s/replicaset/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/chaosk8s/replicaset/actions.py b/chaosk8s/replicaset/actions.py new file mode 100644 index 0000000..3c8844a --- /dev/null +++ b/chaosk8s/replicaset/actions.py @@ -0,0 +1,33 @@ +from chaoslib.types import Secrets +from kubernetes import client +from logzero import logger + +from chaosk8s import create_k8s_api_client + +__all__ = ["delete_replica_set"] + + +def delete_replica_set(name: str, ns: str = "default", + label_selector: str = "name in ({name})", + secrets: Secrets = None): + """ + Delete a replica set by `name` in the namespace `ns`. + + The replica set is deleted without a graceful period to trigger an abrupt termination. + + The selected resources are matched by the given `label_selector`. + """ + label_selector = label_selector.format(name=name) + api = create_k8s_api_client(secrets) + v1 = client.ExtensionsV1beta1Api(api) + if label_selector: + ret = v1.list_namespaced_replica_set(ns, label_selector=label_selector) + else: + ret = v1.list_namespaced_replica_set(ns) + + logger.debug("Found {d} replica sets named '{n}'".format( + d=len(ret.items), n=name)) + + body = client.V1DeleteOptions() + for r in ret.items: + v1.delete_namespaced_replica_set(r.metadata.name, ns, body) diff --git a/chaosk8s/service/__init__.py b/chaosk8s/service/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/chaosk8s/service/actions.py b/chaosk8s/service/actions.py new file mode 100644 index 0000000..d2c1011 --- /dev/null +++ b/chaosk8s/service/actions.py @@ -0,0 +1,15 @@ +from chaoslib.types import Secrets +from kubernetes import client + +from chaosk8s import create_k8s_api_client + +__all__ = ["delete_service"] + + +def delete_service(name: str, ns: str = "default", secrets: Secrets = None): + """ + Remove the given service + """ + api = create_k8s_api_client(secrets) + v1 = client.CoreV1Api(api) + v1.delete_namespaced_service(name, namespace=ns) From 147928d99fc66d82b05d62b789206ec9fc9d3674 Mon Sep 17 00:00:00 2001 From: Devatoria Date: Wed, 5 Jun 2019 18:36:20 +0200 Subject: [PATCH 2/2] Rework actions tests Signed-off-by: Devatoria --- tests/test_deployment.py | 64 +++++++++++++++++++++++++ tests/{test_actions.py => test_node.py} | 35 -------------- tests/test_replicaset.py | 28 +++++++++++ tests/test_service.py | 15 ++++++ 4 files changed, 107 insertions(+), 35 deletions(-) create mode 100644 tests/test_deployment.py rename tests/{test_actions.py => test_node.py} (89%) create mode 100644 tests/test_replicaset.py create mode 100644 tests/test_service.py diff --git a/tests/test_deployment.py b/tests/test_deployment.py new file mode 100644 index 0000000..971c9d4 --- /dev/null +++ b/tests/test_deployment.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +from unittest.mock import patch, MagicMock, ANY, call + +import pytest +from chaoslib.exceptions import ActivityFailed +from kubernetes.client.models import V1DeploymentList, V1Deployment, V1ObjectMeta + +from chaosk8s.deployment.actions import create_deployment, delete_deployment, scale_deployment + + +@patch('chaosk8s.has_local_config_file', autospec=True) +def test_cannot_process_other_than_yaml_and_json(has_conf): + has_conf.return_value = False + path = "./tests/fixtures/invalid-k8s.txt" + with pytest.raises(ActivityFailed) as excinfo: + create_deployment(spec_path=path) + assert "cannot process {path}".format(path=path) in str(excinfo) + + +@patch('builtins.open', autospec=True) +@patch('chaosk8s.deployment.actions.json', autospec=True) +@patch('chaosk8s.deployment.actions.create_k8s_api_client', autospec=True) +@patch('chaosk8s.deployment.actions.client', autospec=True) +def test_create_deployment(client, api, json, open): + v1 = MagicMock() + client.AppsV1beta1Api.return_value = v1 + json.loads.return_value = {"Kind": "Deployment"} + + create_deployment(spec_path="depl.json") + + v1.create_namespaced_deployment.assert_called_with(ANY, body=json.loads.return_value) + + +@patch('chaosk8s.deployment.actions.create_k8s_api_client', autospec=True) +@patch('chaosk8s.deployment.actions.client', autospec=True) +def test_delete_deployment(client, api): + depl1 = V1Deployment(metadata=V1ObjectMeta(name="depl1")) + depl2 = V1Deployment(metadata=V1ObjectMeta(name="depl2")) + v1 = MagicMock() + client.AppsV1beta1Api.return_value = v1 + v1.list_namespaced_deployment.return_value = V1DeploymentList(items=(depl1, depl2)) + + delete_deployment("fake_name", "fake_ns") + + v1.list_namespaced_deployment.assert_called_with("fake_ns", label_selector=ANY) + v1.delete_namespaced_deployment.assert_has_calls( + calls=[ + call(depl1.metadata.name, "fake_ns", ANY), + call(depl2.metadata.name, "fake_ns", ANY) + ], + any_order=True + ) + + +@patch('chaosk8s.deployment.actions.create_k8s_api_client', autospec=True) +@patch('chaosk8s.deployment.actions.client', autospec=True) +def test_scale_deployment(client, api): + v1 = MagicMock() + client.ExtensionsV1beta1Api.return_value = v1 + + scale_deployment("fake", 3, "fake_ns") + + body = {"spec": {"replicas": 3}} + v1.patch_namespaced_deployment_scale.assert_called_with("fake", namespace="fake_ns", body=body) diff --git a/tests/test_actions.py b/tests/test_node.py similarity index 89% rename from tests/test_actions.py rename to tests/test_node.py index b957470..086b16f 100644 --- a/tests/test_actions.py +++ b/tests/test_node.py @@ -5,20 +5,10 @@ from chaoslib.exceptions import ActivityFailed from kubernetes.client.rest import ApiException -from chaosk8s.actions import start_microservice from chaosk8s.node.actions import cordon_node, create_node, delete_nodes, \ uncordon_node, drain_nodes -@patch('chaosk8s.has_local_config_file', autospec=True) -def test_cannot_process_other_than_yaml_and_json(has_conf): - has_conf.return_value = False - path = "./tests/fixtures/invalid-k8s.txt" - with pytest.raises(ActivityFailed) as excinfo: - start_microservice(spec_path=path) - assert "cannot process {path}".format(path=path) in str(excinfo) - - @patch('chaosk8s.has_local_config_file', autospec=True) @patch('chaosk8s.node.actions.client', autospec=True) @patch('chaosk8s.client') @@ -92,31 +82,6 @@ def test_delete_nodes(cl, client, has_conf): v1.delete_node.assert_called_with("mynode", ANY, grace_period_seconds=None) -@patch('chaosk8s.has_local_config_file', autospec=True) -@patch('chaosk8s.node.actions.client', autospec=True) -@patch('chaosk8s.client') -def test_delete_nodes(cl, client, has_conf): - has_conf.return_value = False - - v1 = MagicMock() - client.CoreV1Api.return_value = v1 - - node = MagicMock() - node.metadata.name = "mynode" - - result = MagicMock() - result.items = [node] - v1.list_node.return_value = result - - res = MagicMock() - res.status = "Success" - v1.delete_node.return_value = res - - delete_nodes(label_selector="k=mynode") - - v1.delete_node.assert_called_with("mynode", ANY, grace_period_seconds=None) - - @patch('chaosk8s.has_local_config_file', autospec=True) @patch('chaosk8s.node.actions.client', autospec=True) @patch('chaosk8s.client') diff --git a/tests/test_replicaset.py b/tests/test_replicaset.py new file mode 100644 index 0000000..c516801 --- /dev/null +++ b/tests/test_replicaset.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +from unittest.mock import patch, MagicMock, ANY, call + +from kubernetes.client.models import V1ReplicaSetList, V1ReplicaSet, V1ObjectMeta + +from chaosk8s.replicaset.actions import delete_replica_set + + +@patch('chaosk8s.replicaset.actions.create_k8s_api_client', autospec=True) +@patch('chaosk8s.replicaset.actions.client', autospec=True) +def test_create_deployment(client, api): + v1 = MagicMock() + client.ExtensionsV1beta1Api.return_value = v1 + v1.list_namespaced_replica_set.return_value = V1ReplicaSetList(items=( + V1ReplicaSet(metadata=V1ObjectMeta(name="repl1")), + V1ReplicaSet(metadata=V1ObjectMeta(name="repl2")) + )) + + delete_replica_set("fake", "fake_ns") + + v1.list_namespaced_replica_set.assert_called_with("fake_ns", label_selector="name in (fake)") + v1.delete_namespaced_replica_set.assert_has_calls( + [ + call("repl1", "fake_ns", ANY), + call("repl2", "fake_ns", ANY) + ], + any_order=True + ) diff --git a/tests/test_service.py b/tests/test_service.py new file mode 100644 index 0000000..735708c --- /dev/null +++ b/tests/test_service.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +from unittest.mock import patch, MagicMock + +from chaosk8s.service.actions import delete_service + + +@patch('chaosk8s.service.actions.create_k8s_api_client', autospec=True) +@patch('chaosk8s.service.actions.client', autospec=True) +def test_delete_service(client, api): + v1 = MagicMock() + client.CoreV1Api.return_value = v1 + + delete_service("fake", "fake_ns") + + v1.delete_namespaced_service.assert_called_with("fake", namespace="fake_ns")