From e2eba043becadba972900a30877fdc304048e804 Mon Sep 17 00:00:00 2001 From: Adam Dyess Date: Fri, 15 Nov 2024 17:01:06 -0600 Subject: [PATCH] Testing for cloud integrations --- charms/worker/k8s/requirements.txt | 2 +- charms/worker/k8s/src/kube_control.py | 4 + .../k8s/tests/unit/test_cloud_integration.py | 196 ++++++++++++++++++ 3 files changed, 201 insertions(+), 1 deletion(-) create mode 100644 charms/worker/k8s/tests/unit/test_cloud_integration.py diff --git a/charms/worker/k8s/requirements.txt b/charms/worker/k8s/requirements.txt index eb36b1bf..1fd57d98 100644 --- a/charms/worker/k8s/requirements.txt +++ b/charms/worker/k8s/requirements.txt @@ -2,7 +2,7 @@ charm-lib-contextual-status @ git+https://github.com/charmed-kubernetes/charm-li charm-lib-interface-external-cloud-provider @ git+https://github.com/charmed-kubernetes/charm-lib-interface-external-cloud-provider@e1c5fc69e98100a7d43c0ad5a7969bba1ecbcd40 charm-lib-node-base @ git+https://github.com/charmed-kubernetes/layer-kubernetes-node-base@9b212854e768f13c26cc907bed51444e97e51b50#subdirectory=ops charm-lib-reconciler @ git+https://github.com/charmed-kubernetes/charm-lib-reconciler@f818cc30d1a22be43ffdfecf7fbd9c3fd2967502 -ops-interface-kube-control @ git+https://github.com/charmed-kubernetes/interface-kube-control.git@KU-1998/use-secrets#subdirectory=ops +ops-interface-kube-control @ git+https://github.com/charmed-kubernetes/interface-kube-control.git@main#subdirectory=ops ops.interface_aws @ git+https://github.com/charmed-kubernetes/interface-aws-integration@main#subdirectory=ops ops.interface_gcp @ git+https://github.com/charmed-kubernetes/interface-gcp-integration@main#subdirectory=ops ops.interface_azure @ git+https://github.com/charmed-kubernetes/interface-azure-integration@main#subdirectory=ops diff --git a/charms/worker/k8s/src/kube_control.py b/charms/worker/k8s/src/kube_control.py index 94821281..7745e3d2 100644 --- a/charms/worker/k8s/src/kube_control.py +++ b/charms/worker/k8s/src/kube_control.py @@ -66,3 +66,7 @@ def configure(charm: K8sCharmProtocol): kubelet_token=str(), proxy_token=str(), ) + + for user, _ in charm.kube_control.closed_auth_creds(): + log.info("TODO: Revoke auth-token for '%s'", user) + # charm.api_manager.remove_auth_token(cred.client_token.get_secret_value()) diff --git a/charms/worker/k8s/tests/unit/test_cloud_integration.py b/charms/worker/k8s/tests/unit/test_cloud_integration.py new file mode 100644 index 00000000..0936d3dd --- /dev/null +++ b/charms/worker/k8s/tests/unit/test_cloud_integration.py @@ -0,0 +1,196 @@ +import unittest.mock as mock +from pathlib import Path +from unittest import mock + +import ops +import ops.testing +import pytest +from charm import K8sCharm +from ops.interface_aws.requires import AWSIntegrationRequires +from ops.interface_azure.requires import AzureIntegrationRequires +from ops.interface_gcp.requires import GCPIntegrationRequires + + +@pytest.fixture(autouse=True) +def vendor_name(): + with mock.patch( + "charms.interface_external_cloud_provider.ExternalCloudProvider.name", + new_callable=mock.PropertyMock, + ) as mock_vendor_name: + mock_vendor_name.return_value = "aws" + yield mock_vendor_name + + +@pytest.fixture(params=["worker", "control-plane"]) +def harness(request): + """Craft a ops test harness. + + Args: + request: pytest request object + """ + meta = Path(__file__).parent / "../../charmcraft.yaml" + if request.param == "worker": + meta = Path(__file__).parent / "../../../charmcraft.yaml" + harness = ops.testing.Harness(K8sCharm, meta=meta.read_text()) + harness.begin() + harness.charm.is_worker = request.param == "worker" + with mock.patch.object(harness.charm, "get_cloud_name"): + with mock.patch.object(harness.charm, "get_cluster_name", return_value="my-cluster"): + yield harness + harness.cleanup() + + +@pytest.mark.parametrize( + "cloud_name, cloud_relation", + [ + ("aws", "aws"), + ("gce", "gcp"), + ("azure", "azure"), + ("unknown", None), + ], + ids=["aws", "gce", "azure", "unknown"], +) +def test_cloud_detection(harness, cloud_name, cloud_relation, vendor_name): + # Test that the cloud property returns the correct integration requires object + harness.charm.get_cloud_name.return_value = cloud_name + integration = harness.charm.cloud_integration + assert integration.cloud is None + if cloud_name != "unknown": + harness.add_relation(cloud_relation, "cloud-integrator") + assert integration.cloud + + +def test_cloud_aws(harness): + # Test that the cloud property returns the correct integration requires object + harness.charm.get_cloud_name.return_value = "aws" + # with mock.patch.object(harness.charm.cloud_integration, "cloud", callable=mock.PropertyMock) as mock_cloud: + with mock.patch( + "cloud_integration.CloudIntegration.cloud", + new_callable=mock.PropertyMock, + return_value=mock.create_autospec(AWSIntegrationRequires), + ) as mock_property: + mock_cloud = mock_property() + mock_cloud.evaluate_relation.return_value = None + event = mock.MagicMock() + harness.charm.cloud_integration.integrate(event) + if harness.charm.is_worker: + mock_cloud.tag_instance.assert_called_once_with( + {"kubernetes.io/cluster/my-cluster": "owned"} + ) + else: + mock_cloud.tag_instance.assert_called_once_with( + { + "kubernetes.io/cluster/my-cluster": "owned", + "k8s.io/role/master": "true", # wokeignore:rule=master + } + ) + mock_cloud.tag_instance_security_group.assert_called_once_with( + {"kubernetes.io/cluster/my-cluster": "owned"} + ) + mock_cloud.tag_instance_subnet.assert_called_once_with( + {"kubernetes.io/cluster/my-cluster": "owned"} + ) + mock_cloud.enable_object_storage_management.assert_called_once_with(["kubernetes-*"]) + if harness.charm.is_worker: + mock_cloud.enable_load_balancer_management.assert_not_called() + mock_cloud.enable_autoscaling_readonly.assert_not_called() + mock_cloud.enable_instance_modification.assert_not_called() + mock_cloud.enable_region_readonly.assert_not_called() + mock_cloud.enable_network_management.assert_not_called() + mock_cloud.enable_block_storage_management.assert_not_called() + else: + mock_cloud.enable_load_balancer_management.assert_called_once() + mock_cloud.enable_autoscaling_readonly.assert_called_once() + mock_cloud.enable_instance_modification.assert_called_once() + mock_cloud.enable_region_readonly.assert_called_once() + mock_cloud.enable_network_management.assert_called_once() + mock_cloud.enable_block_storage_management.assert_called_once() + mock_cloud.enable_instance_inspection.assert_called_once() + mock_cloud.enable_dns_management.assert_called_once() + mock_cloud.evaluate_relation.assert_called_once_with(event) + + +def test_cloud_gce(harness): + # Test that the cloud property returns the correct integration requires object + harness.charm.get_cloud_name.return_value = "gce" + with mock.patch( + "cloud_integration.CloudIntegration.cloud", + new_callable=mock.PropertyMock, + return_value=mock.create_autospec(GCPIntegrationRequires), + ) as mock_property: + mock_cloud = mock_property() + mock_cloud.evaluate_relation.return_value = None + event = mock.MagicMock() + harness.charm.cloud_integration.integrate(event) + + if harness.charm.is_worker: + mock_cloud.tag_instance.assert_called_once_with({"k8s-io-cluster-name": "my-cluster"}) + else: + mock_cloud.tag_instance.assert_called_once_with( + { + "k8s-io-cluster-name": "my-cluster", + "k8s-io-role-master": "master", # wokeignore:rule=master + } + ) + mock_cloud.enable_object_storage_management.assert_called_once() + if harness.charm.is_worker: + mock_cloud.enable_security_management.assert_not_called() + mock_cloud.enable_network_management.assert_not_called() + mock_cloud.enable_block_storage_management.assert_not_called() + else: + mock_cloud.enable_security_management.assert_called_once() + mock_cloud.enable_network_management.assert_called_once() + mock_cloud.enable_block_storage_management.assert_called_once() + mock_cloud.enable_instance_inspection.assert_called_once() + mock_cloud.enable_dns_management.assert_called_once() + mock_cloud.evaluate_relation.assert_called_once_with(event) + + +def test_cloud_azure(harness): + # Test that the cloud property returns the correct integration requires object + harness.charm.get_cloud_name.return_value = "azure" + with mock.patch( + "cloud_integration.CloudIntegration.cloud", + new_callable=mock.PropertyMock, + return_value=mock.create_autospec(AzureIntegrationRequires), + ) as mock_property: + mock_cloud = mock_property() + mock_cloud.evaluate_relation.return_value = None + event = mock.MagicMock() + harness.charm.cloud_integration.integrate(event) + if harness.charm.is_worker: + mock_cloud.tag_instance.assert_called_once_with({"k8s-io-cluster-name": "my-cluster"}) + else: + mock_cloud.tag_instance.assert_called_once_with( + { + "k8s-io-cluster-name": "my-cluster", + "k8s-io-role-master": "master", # wokeignore:rule=master + } + ) + mock_cloud.enable_object_storage_management.assert_called_once() + if harness.charm.is_worker: + mock_cloud.enable_security_management.assert_not_called() + mock_cloud.enable_loadbalancer_management.assert_not_called() + mock_cloud.enable_network_management.assert_not_called() + mock_cloud.enable_block_storage_management.assert_not_called() + else: + mock_cloud.enable_security_management.assert_called_once() + mock_cloud.enable_loadbalancer_management.assert_called_once() + mock_cloud.enable_network_management.assert_called_once() + mock_cloud.enable_block_storage_management.assert_called_once() + mock_cloud.enable_dns_management.assert_called_once() + mock_cloud.enable_instance_inspection.assert_called_once() + mock_cloud.evaluate_relation.assert_called_once_with(event) + + +def test_cloud_unknown(harness): + # Test that the cloud property returns the correct integration requires object + harness.charm.get_cloud_name.return_value = "unknown" + with mock.patch( + "cloud_integration.CloudIntegration.cloud", + new_callable=mock.PropertyMock, + return_value=None, + ) as mock_property: + event = mock.MagicMock() + harness.charm.cloud_integration.integrate(event) + assert mock_property.called