From 2edc70083c7cf92c70087fee548ff21b634a483b Mon Sep 17 00:00:00 2001 From: Nikos Date: Wed, 19 Apr 2023 16:49:07 +0300 Subject: [PATCH 01/21] Add network policies --- src/charm.py | 20 ++++++ src/k8s_network_policies.py | 117 ++++++++++++++++++++++++++++++++ tests/integration/conftest.py | 11 +++ tests/integration/test_charm.py | 8 ++- tests/unit/conftest.py | 5 ++ 5 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 src/k8s_network_policies.py create mode 100644 tests/integration/conftest.py diff --git a/src/charm.py b/src/charm.py index 51c18bb0..50dbf79c 100755 --- a/src/charm.py +++ b/src/charm.py @@ -54,6 +54,7 @@ from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus, ModelError, WaitingStatus from ops.pebble import Error, ExecError, Layer +from k8s_network_policies import K8sNetworkPoliciesHandler, NetworkPoliciesHandlerError from kratos import KratosAPI if TYPE_CHECKING: @@ -117,6 +118,7 @@ def __init__(self, *args: Any) -> None: port=KRATOS_PUBLIC_PORT, strip_prefix=True, ) + self.network_policy_handler = K8sNetworkPoliciesHandler(self) self.database = DatabaseRequires( self, @@ -151,9 +153,13 @@ def __init__(self, *args: Any) -> None: self.framework.observe(self.database.on.database_created, self._on_database_created) self.framework.observe(self.database.on.endpoints_changed, self._on_database_changed) self.framework.observe(self.admin_ingress.on.ready, self._on_admin_ingress_ready) + self.framework.observe(self.admin_ingress.on.ready, self._apply_network_policies) self.framework.observe(self.admin_ingress.on.revoked, self._on_ingress_revoked) + self.framework.observe(self.admin_ingress.on.revoked, self._apply_network_policies) self.framework.observe(self.public_ingress.on.ready, self._on_public_ingress_ready) + self.framework.observe(self.public_ingress.on.ready, self._apply_network_policies) self.framework.observe(self.public_ingress.on.revoked, self._on_ingress_revoked) + self.framework.observe(self.public_ingress.on.revoked, self._apply_network_policies) self.framework.observe( self.external_provider.on.client_config_changed, self._on_client_config_changed ) @@ -522,6 +528,20 @@ def _on_admin_ingress_ready(self, event: IngressPerAppReadyEvent) -> None: self._handle_status_update_config(event) self._update_kratos_endpoints_relation_data(event) + def _apply_network_policies(self, event: HookEvent) -> None: + if not self.unit.is_leader(): + return + + try: + self.network_policy_handler.apply_ingress_policy( + [ + ("admin", [self.admin_ingress.relation]), + ("public", [self.public_ingress.relation]), + ] + ) + except NetworkPoliciesHandlerError: + event.defer() + def _on_public_ingress_ready(self, event: IngressPerAppReadyEvent) -> None: if self.unit.is_leader(): logger.info("This app's public ingress URL: %s", event.url) diff --git a/src/k8s_network_policies.py b/src/k8s_network_policies.py new file mode 100644 index 00000000..2dc1dc0f --- /dev/null +++ b/src/k8s_network_policies.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +# Copyright 2022 Canonical Ltd. +# See LICENSE file for licensing details. + +"""A helper class for managing kubernetes network policies.""" + +import logging +from typing import List, Optional, Tuple, Union + +from lightkube import ApiError, Client +from lightkube.models.meta_v1 import LabelSelector, ObjectMeta +from lightkube.models.networking_v1 import ( + NetworkPolicyIngressRule, + NetworkPolicyPeer, + NetworkPolicyPort, + NetworkPolicySpec, +) +from lightkube.resources.networking_v1 import NetworkPolicy +from ops.charm import CharmBase +from ops.model import Relation + +logger = logging.getLogger(__name__) + + +class NetworkPoliciesHandlerError(Exception): + """Applying the network policies failed.""" + + +IngressPolicyDefinition = Tuple[Union[str, int], List[Relation]] + + +class K8sNetworkPoliciesHandler: + """A helper class for managing kubernetes network policies.""" + + def __init__(self, charm: CharmBase) -> None: + self._charm = charm + self.client = Client(field_manager=charm.app.name, namespace=charm.model.name) + + @property + def policy_name(self) -> str: + """The default policy name that will be created.""" + return f"{self._charm.app.name}-network-policy" + + def apply_ingress_policy( + self, policies: IngressPolicyDefinition, name: Optional[str] = None + ) -> None: + """Apply an ingress network policy about a related application. + + Policies can be defined for multiple ports at once to allow ingress traffic + from related applications + + If no policies are defined then all ingress traffic will be denied. + + Example usage: + + policies = [("admin", [admin_ingress_relation]), (8080, [public_ingress_relation])] + network_policy_handler.apply_ingress_policy(policies) + """ + if not name: + name = self.policy_name + + ingress = [] + for port, relations in policies: + selectors = [ + NetworkPolicyPeer( + podSelector=LabelSelector( + matchLabels={"app.kubernetes.io/name": relation.app.name} + ) + ) + for relation in relations + if relation and relation.app + ] + ingress.append( + NetworkPolicyIngressRule( + from_=selectors, + ports=[NetworkPolicyPort(port=port)], + ), + ) + + policy = NetworkPolicy( + metadata=ObjectMeta(name=name), + spec=NetworkPolicySpec( + podSelector=LabelSelector( + matchLabels={"app.kubernetes.io/name": self._charm.app.name} + ), + policyTypes=["Ingress"], + ingress=ingress, + ), + ) + + try: + self.client.apply( + policy, + namespace=self._charm.model.name, + ) + except ApiError as e: + if e.status.code == 403: + msg = f"Kubernetes resources patch failed: `juju trust` this application. {e}" + else: + msg = f"Kubernetes resources patch failed: {e}" + logger.error(msg) + raise NetworkPoliciesHandlerError() + + def delete_network_policy(self, name: Optional[str] = None) -> None: + """Delete a network policy rule.""" + if not name: + name = self.policy_name + + try: + self.client.delete(name, namespace=self._charm.model.name) + except ApiError as e: + if e.status.code == 403: + msg = f"Kubernetes resources patch failed: `juju trust` this application. {e}" + else: + msg = f"Kubernetes resources patch failed: {e}" + logger.error(msg) + raise NetworkPoliciesHandlerError() diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py new file mode 100644 index 00000000..9637d261 --- /dev/null +++ b/tests/integration/conftest.py @@ -0,0 +1,11 @@ +import os + +from lightkube import Client, KubeConfig +import pytest + +KUBECONFIG = os.environ.get("TESTING_KUBECONFIG", "~/.kube/config") + + +@pytest.fixture(scope="module") +def client() -> Client: + return Client(config=KubeConfig.from_file(KUBECONFIG)) diff --git a/tests/integration/test_charm.py b/tests/integration/test_charm.py index 7116bf75..49276413 100644 --- a/tests/integration/test_charm.py +++ b/tests/integration/test_charm.py @@ -10,6 +10,8 @@ import requests import yaml from pytest_operator.plugin import OpsTest +from lightkube import Client +from lightkube.resources.networking_v1 import NetworkPolicy logger = logging.getLogger(__name__) @@ -71,7 +73,7 @@ async def test_build_and_deploy(ops_test: OpsTest) -> None: assert ops_test.model.applications[APP_NAME].units[0].workload_status == "active" -async def test_ingress_relation(ops_test: OpsTest) -> None: +async def test_ingress_relation(ops_test: OpsTest, client: Client) -> None: await ops_test.model.deploy( TRAEFIK, application_name=TRAEFIK_PUBLIC_APP, @@ -94,6 +96,10 @@ async def test_ingress_relation(ops_test: OpsTest) -> None: timeout=1000, ) + # Validate network policies are created when ingress is provided + policy = client.get(NetworkPolicy, "kratos-network-policy") + assert policy + async def test_has_public_ingress(ops_test: OpsTest) -> None: # Get the traefik address and try to reach kratos diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 4c783db8..70f4b0fc 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -15,6 +15,11 @@ from kratos import KratosAPI +@pytest.fixture(autouse=True) +def lk_client(mocker): + return mocker.patch("charm.Client", autospec=True) + + @pytest.fixture() def harness(mocked_kubernetes_service_patcher: MagicMock) -> Harness: harness = Harness(KratosCharm) From bfe6ecb7002db2d9c687ba5c75c75eefb7f0c164 Mon Sep 17 00:00:00 2001 From: Nikos Date: Wed, 19 Apr 2023 17:11:26 +0300 Subject: [PATCH 02/21] Fix linting --- tests/integration/conftest.py | 5 ++++- tests/integration/test_charm.py | 2 +- tests/unit/conftest.py | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 9637d261..b9673490 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -1,7 +1,10 @@ +#!/usr/bin/env python3 +# Copyright 2022 Canonical Ltd. + import os -from lightkube import Client, KubeConfig import pytest +from lightkube import Client, KubeConfig KUBECONFIG = os.environ.get("TESTING_KUBECONFIG", "~/.kube/config") diff --git a/tests/integration/test_charm.py b/tests/integration/test_charm.py index 49276413..72869465 100644 --- a/tests/integration/test_charm.py +++ b/tests/integration/test_charm.py @@ -9,9 +9,9 @@ import pytest import requests import yaml -from pytest_operator.plugin import OpsTest from lightkube import Client from lightkube.resources.networking_v1 import NetworkPolicy +from pytest_operator.plugin import OpsTest logger = logging.getLogger(__name__) diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 70f4b0fc..19c4fb32 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -17,7 +17,7 @@ @pytest.fixture(autouse=True) def lk_client(mocker): - return mocker.patch("charm.Client", autospec=True) + return mocker.patch("k8s_network_policies.Client", autospec=True) @pytest.fixture() From c72be88ff566d93dcf9ec6a3b19dc6a34d71ca7b Mon Sep 17 00:00:00 2001 From: Nikos Date: Thu, 20 Apr 2023 10:33:36 +0300 Subject: [PATCH 03/21] Fix dns --- src/k8s_network_policies.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/k8s_network_policies.py b/src/k8s_network_policies.py index 2dc1dc0f..132e9076 100644 --- a/src/k8s_network_policies.py +++ b/src/k8s_network_policies.py @@ -81,10 +81,14 @@ def apply_ingress_policy( metadata=ObjectMeta(name=name), spec=NetworkPolicySpec( podSelector=LabelSelector( - matchLabels={"app.kubernetes.io/name": self._charm.app.name} + matchLabels={ + "app.kubernetes.io/name": self._charm.app.name, + "kubernetes.io/metadata.name": self._charm.model.name, + } ), - policyTypes=["Ingress"], + policyTypes=["Ingress", "Egress"], ingress=ingress, + egress=[{}] ), ) From 2a4f7824d84ef9f8a7d2ca94934ef62e7f2c0868 Mon Sep 17 00:00:00 2001 From: Nikos Date: Thu, 20 Apr 2023 11:46:20 +0300 Subject: [PATCH 04/21] Working version This is fairly complex, will probably have to refactor it --- src/charm.py | 12 +++++++++--- src/k8s_network_policies.py | 23 +++++++++++++++++++---- tests/integration/test_charm.py | 2 +- 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/charm.py b/src/charm.py index 50dbf79c..3b7cb09b 100755 --- a/src/charm.py +++ b/src/charm.py @@ -54,7 +54,11 @@ from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus, ModelError, WaitingStatus from ops.pebble import Error, ExecError, Layer -from k8s_network_policies import K8sNetworkPoliciesHandler, NetworkPoliciesHandlerError +from k8s_network_policies import ( + K8sNetworkPoliciesHandler, + NetworkPoliciesHandlerError, + PortDefinition, +) from kratos import KratosAPI if TYPE_CHECKING: @@ -535,8 +539,10 @@ def _apply_network_policies(self, event: HookEvent) -> None: try: self.network_policy_handler.apply_ingress_policy( [ - ("admin", [self.admin_ingress.relation]), - ("public", [self.public_ingress.relation]), + (PortDefinition(1, KRATOS_PUBLIC_PORT - 1), ()), + (PortDefinition(KRATOS_PUBLIC_PORT), [self.public_ingress.relation]), + (PortDefinition(KRATOS_ADMIN_PORT), [self.admin_ingress.relation]), + (PortDefinition(KRATOS_ADMIN_PORT + 1, 65535), ()), ] ) except NetworkPoliciesHandlerError: diff --git a/src/k8s_network_policies.py b/src/k8s_network_policies.py index 132e9076..db760f58 100644 --- a/src/k8s_network_policies.py +++ b/src/k8s_network_policies.py @@ -5,6 +5,7 @@ """A helper class for managing kubernetes network policies.""" import logging +from dataclasses import dataclass from typing import List, Optional, Tuple, Union from lightkube import ApiError, Client @@ -26,7 +27,22 @@ class NetworkPoliciesHandlerError(Exception): """Applying the network policies failed.""" -IngressPolicyDefinition = Tuple[Union[str, int], List[Relation]] +@dataclass +class PortDefinition: + """Network Policy port definition.""" + + port: Union[str, int] + end_port: Optional[int] = None + protocol: Optional[str] = "TCP" + + def to_resource(self): + """Convert class to NetworkPolicyPort.""" + if not self.end_port: + return NetworkPolicyPort(port=self.port, protocol=self.protocol) + return NetworkPolicyPort(port=self.port, endPort=self.end_port, protocol=self.protocol) + + +IngressPolicyDefinition = Tuple[PortDefinition, List[Relation]] class K8sNetworkPoliciesHandler: @@ -73,7 +89,7 @@ def apply_ingress_policy( ingress.append( NetworkPolicyIngressRule( from_=selectors, - ports=[NetworkPolicyPort(port=port)], + ports=[port.to_resource()], ), ) @@ -86,9 +102,8 @@ def apply_ingress_policy( "kubernetes.io/metadata.name": self._charm.model.name, } ), - policyTypes=["Ingress", "Egress"], + policyTypes=["Ingress"], ingress=ingress, - egress=[{}] ), ) diff --git a/tests/integration/test_charm.py b/tests/integration/test_charm.py index 72869465..e18b26ca 100644 --- a/tests/integration/test_charm.py +++ b/tests/integration/test_charm.py @@ -97,7 +97,7 @@ async def test_ingress_relation(ops_test: OpsTest, client: Client) -> None: ) # Validate network policies are created when ingress is provided - policy = client.get(NetworkPolicy, "kratos-network-policy") + policy = client.get(NetworkPolicy, "kratos-network-policy", namespace=ops_test.model.name) assert policy From 51b8643c2c25e3c8df365ba075b6f253ddab07f5 Mon Sep 17 00:00:00 2001 From: Nikos Date: Fri, 21 Apr 2023 11:55:06 +0300 Subject: [PATCH 05/21] Fix tests --- tests/integration/test_charm.py | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/tests/integration/test_charm.py b/tests/integration/test_charm.py index e18b26ca..b621fc3c 100644 --- a/tests/integration/test_charm.py +++ b/tests/integration/test_charm.py @@ -51,26 +51,11 @@ async def test_build_and_deploy(ops_test: OpsTest) -> None: Assert on the unit status before any relations/configurations take place. """ - await ops_test.model.deploy( - POSTGRES, - channel="latest/edge", - trust=True, - ) charm = await ops_test.build_charm(".") resources = {"oci-image": METADATA["resources"]["oci-image"]["upstream-source"]} await ops_test.model.deploy( charm, resources=resources, application_name=APP_NAME, trust=True, series="jammy" ) - await ops_test.model.add_relation(APP_NAME, POSTGRES) - - async with ops_test.fast_forward(): - await ops_test.model.wait_for_idle( - apps=[APP_NAME, POSTGRES], - status="active", - raise_on_blocked=True, - timeout=1000, - ) - assert ops_test.model.applications[APP_NAME].units[0].workload_status == "active" async def test_ingress_relation(ops_test: OpsTest, client: Client) -> None: @@ -101,6 +86,24 @@ async def test_ingress_relation(ops_test: OpsTest, client: Client) -> None: assert policy +async def test_postgres_relation(ops_test: OpsTest) -> None: + await ops_test.model.deploy( + POSTGRES, + channel="14/stable", + trust=True, + ) + await ops_test.model.add_relation(APP_NAME, POSTGRES) + + async with ops_test.fast_forward(): + await ops_test.model.wait_for_idle( + apps=[APP_NAME, POSTGRES], + status="active", + raise_on_blocked=True, + timeout=1000, + ) + assert ops_test.model.applications[APP_NAME].units[0].workload_status == "active" + + async def test_has_public_ingress(ops_test: OpsTest) -> None: # Get the traefik address and try to reach kratos public_address = await get_unit_address(ops_test, TRAEFIK_PUBLIC_APP, 0) From c2e54751d79bae4fda03ea973add01f4fed4d251 Mon Sep 17 00:00:00 2001 From: Nikos Date: Fri, 21 Apr 2023 12:34:45 +0300 Subject: [PATCH 06/21] Move k8s_network_policies to library --- .../kratos/v0/kubernetes_network_policies.py | 42 ++++++++++++++++++- src/charm.py | 14 +++---- tests/unit/conftest.py | 2 +- 3 files changed, 48 insertions(+), 10 deletions(-) rename src/k8s_network_policies.py => lib/charms/kratos/v0/kubernetes_network_policies.py (75%) diff --git a/src/k8s_network_policies.py b/lib/charms/kratos/v0/kubernetes_network_policies.py similarity index 75% rename from src/k8s_network_policies.py rename to lib/charms/kratos/v0/kubernetes_network_policies.py index db760f58..ae6fa6e1 100644 --- a/src/k8s_network_policies.py +++ b/lib/charms/kratos/v0/kubernetes_network_policies.py @@ -1,8 +1,35 @@ #!/usr/bin/env python3 -# Copyright 2022 Canonical Ltd. +# Copyright 2023 Canonical Ltd. # See LICENSE file for licensing details. -"""A helper class for managing kubernetes network policies.""" +"""Interface library for creating network policies. +This library provides a Python API for creating kubernetes network policies. +## Getting Started +To get started using the library, you need to fetch the library using `charmcraft`. +```shell +cd some-charm +charmcraft fetch-lib charms.kratos.v0.kubernetes_network_policies +``` +Then, to initialise the library: +```python +from charms.kratos.v0.kubernetes_network_policies import ( + K8sNetworkPoliciesHandler, + NetworkPoliciesHandlerError, + PortDefinition, +) +Class SomeCharm(CharmBase): + def __init__(self, *args): + self.network_policy_handler = K8sNetworkPoliciesHandler(self) + + def some_event_function(): + policies = [(PortDefinition("admin"), [self.admin_ingress_relation]), (PortDefinition(8080), [])] + self.network_policy_handler.apply_ingress_policy(policies) +``` + +The function in this example will only allow traffic to the charm pod to the "admin" port from the app on the +other side of the `admin_ingress_relation` and all traffic to the "8080" port. Ingress traffic to all other ports +will be denied. +""" import logging from dataclasses import dataclass @@ -20,6 +47,17 @@ from ops.charm import CharmBase from ops.model import Relation + +# The unique Charmhub library identifier, never change it +LIBID = "f0a1c7a9bc084be09b1052810651b7ed" + +# Increment this major API version when introducing breaking changes +LIBAPI = 0 + +# Increment this PATCH version before using `charmcraft publish-lib` or reset +# to 0 if you are raising the major API version +LIBPATCH = 1 + logger = logging.getLogger(__name__) diff --git a/src/charm.py b/src/charm.py index 3b7cb09b..52418245 100755 --- a/src/charm.py +++ b/src/charm.py @@ -31,6 +31,11 @@ LoginUITooManyRelatedAppsError, ) from charms.kratos.v0.kratos_endpoints import KratosEndpointsProvider +from charms.kratos.v0.kubernetes_network_policies import ( + K8sNetworkPoliciesHandler, + NetworkPoliciesHandlerError, + PortDefinition, +) from charms.kratos_external_idp_integrator.v0.kratos_external_provider import ( ClientConfigChangedEvent, ExternalIdpRequirer, @@ -54,11 +59,6 @@ from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus, ModelError, WaitingStatus from ops.pebble import Error, ExecError, Layer -from k8s_network_policies import ( - K8sNetworkPoliciesHandler, - NetworkPoliciesHandlerError, - PortDefinition, -) from kratos import KratosAPI if TYPE_CHECKING: @@ -539,10 +539,10 @@ def _apply_network_policies(self, event: HookEvent) -> None: try: self.network_policy_handler.apply_ingress_policy( [ - (PortDefinition(1, KRATOS_PUBLIC_PORT - 1), ()), + (PortDefinition(1, KRATOS_PUBLIC_PORT - 1), []), (PortDefinition(KRATOS_PUBLIC_PORT), [self.public_ingress.relation]), (PortDefinition(KRATOS_ADMIN_PORT), [self.admin_ingress.relation]), - (PortDefinition(KRATOS_ADMIN_PORT + 1, 65535), ()), + (PortDefinition(KRATOS_ADMIN_PORT + 1, 65535), []), ] ) except NetworkPoliciesHandlerError: diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 19c4fb32..80b3e868 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -17,7 +17,7 @@ @pytest.fixture(autouse=True) def lk_client(mocker): - return mocker.patch("k8s_network_policies.Client", autospec=True) + return mocker.patch("charm.K8sNetworkPoliciesHandler") @pytest.fixture() From 08c463a8069704709d3b9298b10c14a9cf8fa41d Mon Sep 17 00:00:00 2001 From: Nikos Date: Fri, 21 Apr 2023 12:56:36 +0300 Subject: [PATCH 07/21] Refactor --- lib/charms/kratos/v0/kubernetes_network_policies.py | 4 +++- src/charm.py | 9 +++++++-- tests/integration/test_charm.py | 1 - tests/unit/conftest.py | 2 +- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/charms/kratos/v0/kubernetes_network_policies.py b/lib/charms/kratos/v0/kubernetes_network_policies.py index ae6fa6e1..ef5f5dde 100644 --- a/lib/charms/kratos/v0/kubernetes_network_policies.py +++ b/lib/charms/kratos/v0/kubernetes_network_policies.py @@ -80,10 +80,12 @@ def to_resource(self): return NetworkPolicyPort(port=self.port, endPort=self.end_port, protocol=self.protocol) +Port = Union[str, int] +IngressPolicyDefinition = Union[Tuple[Port], Tuple[int, str], Tuple[int, str, int]] IngressPolicyDefinition = Tuple[PortDefinition, List[Relation]] -class K8sNetworkPoliciesHandler: +class KubernetesNetworkPoliciesHandler: """A helper class for managing kubernetes network policies.""" def __init__(self, charm: CharmBase) -> None: diff --git a/src/charm.py b/src/charm.py index 52418245..058f0a36 100755 --- a/src/charm.py +++ b/src/charm.py @@ -32,7 +32,7 @@ ) from charms.kratos.v0.kratos_endpoints import KratosEndpointsProvider from charms.kratos.v0.kubernetes_network_policies import ( - K8sNetworkPoliciesHandler, + KubernetesNetworkPoliciesHandler, NetworkPoliciesHandlerError, PortDefinition, ) @@ -54,6 +54,7 @@ HookEvent, PebbleReadyEvent, RelationEvent, + RemoveEvent, ) from ops.main import main from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus, ModelError, WaitingStatus @@ -122,7 +123,7 @@ def __init__(self, *args: Any) -> None: port=KRATOS_PUBLIC_PORT, strip_prefix=True, ) - self.network_policy_handler = K8sNetworkPoliciesHandler(self) + self.network_policy_handler = KubernetesNetworkPoliciesHandler(self) self.database = DatabaseRequires( self, @@ -145,6 +146,7 @@ def __init__(self, *args: Any) -> None: self.framework.observe(self.on.kratos_pebble_ready, self._on_pebble_ready) self.framework.observe(self.on.config_changed, self._on_config_changed) + self.framework.observe(self.on.remove, self._cleanup) self.framework.observe( self.endpoints_provider.on.ready, self._update_kratos_endpoints_relation_data ) @@ -452,6 +454,9 @@ def _on_config_changed(self, event: ConfigChangedEvent) -> None: """Event Handler for config changed event.""" self._handle_status_update_config(event) + def _cleanup(self, event: RemoveEvent) -> None: + self.network_policy_handler.delete_network_policy() + def _update_kratos_endpoints_relation_data(self, event: RelationEvent) -> None: admin_endpoint = ( self.admin_ingress.url diff --git a/tests/integration/test_charm.py b/tests/integration/test_charm.py index b621fc3c..762387b7 100644 --- a/tests/integration/test_charm.py +++ b/tests/integration/test_charm.py @@ -98,7 +98,6 @@ async def test_postgres_relation(ops_test: OpsTest) -> None: await ops_test.model.wait_for_idle( apps=[APP_NAME, POSTGRES], status="active", - raise_on_blocked=True, timeout=1000, ) assert ops_test.model.applications[APP_NAME].units[0].workload_status == "active" diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 80b3e868..ae84f755 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -17,7 +17,7 @@ @pytest.fixture(autouse=True) def lk_client(mocker): - return mocker.patch("charm.K8sNetworkPoliciesHandler") + return mocker.patch("charm.KubernetesNetworkPoliciesHandler") @pytest.fixture() From f821b8d5a614b9e58c42e2def1db5abf8a4deb14 Mon Sep 17 00:00:00 2001 From: Nikos Date: Fri, 21 Apr 2023 13:10:56 +0300 Subject: [PATCH 08/21] Update CONTRIBUTING.md, update policy --- CONTRIBUTING.md | 2 ++ lib/charms/kratos/v0/kubernetes_network_policies.py | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 853de68b..2c79353b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,6 +38,8 @@ tox -e integration # integration tests tox # runs 'fmt', 'lint', and 'unit' environments ``` +Running the integration tests requires that you have a valid kubeconfig file at `~/.kube/config`. + ## Build charm Build the charm in this git repository using: diff --git a/lib/charms/kratos/v0/kubernetes_network_policies.py b/lib/charms/kratos/v0/kubernetes_network_policies.py index ef5f5dde..6573ec23 100644 --- a/lib/charms/kratos/v0/kubernetes_network_policies.py +++ b/lib/charms/kratos/v0/kubernetes_network_policies.py @@ -38,6 +38,7 @@ def some_event_function(): from lightkube import ApiError, Client from lightkube.models.meta_v1 import LabelSelector, ObjectMeta from lightkube.models.networking_v1 import ( + NetworkPolicyEgressRule, NetworkPolicyIngressRule, NetworkPolicyPeer, NetworkPolicyPort, @@ -139,11 +140,11 @@ def apply_ingress_policy( podSelector=LabelSelector( matchLabels={ "app.kubernetes.io/name": self._charm.app.name, - "kubernetes.io/metadata.name": self._charm.model.name, } ), - policyTypes=["Ingress"], + policyTypes=["Ingress", "Egress"], ingress=ingress, + egress=[NetworkPolicyEgressRule()], ), ) From d3f5a2fc5f3e5864232d934f98bfdec8412bd086 Mon Sep 17 00:00:00 2001 From: Nikos Date: Fri, 21 Apr 2023 14:50:36 +0300 Subject: [PATCH 09/21] Allow traffic only from pebble ports --- src/charm.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/charm.py b/src/charm.py index 058f0a36..59a7ccb0 100755 --- a/src/charm.py +++ b/src/charm.py @@ -544,10 +544,12 @@ def _apply_network_policies(self, event: HookEvent) -> None: try: self.network_policy_handler.apply_ingress_policy( [ - (PortDefinition(1, KRATOS_PUBLIC_PORT - 1), []), + # (PortDefinition(1, KRATOS_PUBLIC_PORT - 1), []), (PortDefinition(KRATOS_PUBLIC_PORT), [self.public_ingress.relation]), (PortDefinition(KRATOS_ADMIN_PORT), [self.admin_ingress.relation]), - (PortDefinition(KRATOS_ADMIN_PORT + 1, 65535), []), + (PortDefinition(38812), []), + (PortDefinition(38813), []), + # (PortDefinition(KRATOS_ADMIN_PORT + 1, 65535), []), ] ) except NetworkPoliciesHandlerError: From f5b33f76b1c82fad58a26b4d38b4c5b840148883 Mon Sep 17 00:00:00 2001 From: Nikos Date: Fri, 21 Apr 2023 17:47:39 +0300 Subject: [PATCH 10/21] Simplify implementation --- .../kratos/v0/kubernetes_network_policies.py | 21 ++----------------- src/charm.py | 11 ++++------ 2 files changed, 6 insertions(+), 26 deletions(-) diff --git a/lib/charms/kratos/v0/kubernetes_network_policies.py b/lib/charms/kratos/v0/kubernetes_network_policies.py index 6573ec23..a2bdab2a 100644 --- a/lib/charms/kratos/v0/kubernetes_network_policies.py +++ b/lib/charms/kratos/v0/kubernetes_network_policies.py @@ -66,24 +66,8 @@ class NetworkPoliciesHandlerError(Exception): """Applying the network policies failed.""" -@dataclass -class PortDefinition: - """Network Policy port definition.""" - - port: Union[str, int] - end_port: Optional[int] = None - protocol: Optional[str] = "TCP" - - def to_resource(self): - """Convert class to NetworkPolicyPort.""" - if not self.end_port: - return NetworkPolicyPort(port=self.port, protocol=self.protocol) - return NetworkPolicyPort(port=self.port, endPort=self.end_port, protocol=self.protocol) - - Port = Union[str, int] -IngressPolicyDefinition = Union[Tuple[Port], Tuple[int, str], Tuple[int, str, int]] -IngressPolicyDefinition = Tuple[PortDefinition, List[Relation]] +IngressPolicyDefinition = Tuple[Port, List[Relation]] class KubernetesNetworkPoliciesHandler: @@ -130,7 +114,7 @@ def apply_ingress_policy( ingress.append( NetworkPolicyIngressRule( from_=selectors, - ports=[port.to_resource()], + ports=[NetworkPolicyPort(port=port, protocol="TCP")], ), ) @@ -144,7 +128,6 @@ def apply_ingress_policy( ), policyTypes=["Ingress", "Egress"], ingress=ingress, - egress=[NetworkPolicyEgressRule()], ), ) diff --git a/src/charm.py b/src/charm.py index 59a7ccb0..734db395 100755 --- a/src/charm.py +++ b/src/charm.py @@ -34,7 +34,6 @@ from charms.kratos.v0.kubernetes_network_policies import ( KubernetesNetworkPoliciesHandler, NetworkPoliciesHandlerError, - PortDefinition, ) from charms.kratos_external_idp_integrator.v0.kratos_external_provider import ( ClientConfigChangedEvent, @@ -544,12 +543,10 @@ def _apply_network_policies(self, event: HookEvent) -> None: try: self.network_policy_handler.apply_ingress_policy( [ - # (PortDefinition(1, KRATOS_PUBLIC_PORT - 1), []), - (PortDefinition(KRATOS_PUBLIC_PORT), [self.public_ingress.relation]), - (PortDefinition(KRATOS_ADMIN_PORT), [self.admin_ingress.relation]), - (PortDefinition(38812), []), - (PortDefinition(38813), []), - # (PortDefinition(KRATOS_ADMIN_PORT + 1, 65535), []), + (KRATOS_PUBLIC_PORT, [self.public_ingress.relation]), + (KRATOS_ADMIN_PORT, [self.admin_ingress.relation]), + (38812, []), + (38813, []), ] ) except NetworkPoliciesHandlerError: From e000e8e1b27bd9d1d83b69e5af2ec694196532da Mon Sep 17 00:00:00 2001 From: Nikos Date: Tue, 25 Apr 2023 15:11:19 +0300 Subject: [PATCH 11/21] Refactor --- .../kratos/v0/kubernetes_network_policies.py | 17 ++++++++--------- src/charm.py | 8 ++++---- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/lib/charms/kratos/v0/kubernetes_network_policies.py b/lib/charms/kratos/v0/kubernetes_network_policies.py index a2bdab2a..2a2a1d11 100644 --- a/lib/charms/kratos/v0/kubernetes_network_policies.py +++ b/lib/charms/kratos/v0/kubernetes_network_policies.py @@ -2,8 +2,9 @@ # Copyright 2023 Canonical Ltd. # See LICENSE file for licensing details. -"""Interface library for creating network policies. -This library provides a Python API for creating kubernetes network policies. +"""Library for creating network policies. +This library provides a Python API for creating kubernetes ingress network policies. + ## Getting Started To get started using the library, you need to fetch the library using `charmcraft`. ```shell @@ -15,7 +16,6 @@ from charms.kratos.v0.kubernetes_network_policies import ( K8sNetworkPoliciesHandler, NetworkPoliciesHandlerError, - PortDefinition, ) Class SomeCharm(CharmBase): def __init__(self, *args): @@ -38,7 +38,6 @@ def some_event_function(): from lightkube import ApiError, Client from lightkube.models.meta_v1 import LabelSelector, ObjectMeta from lightkube.models.networking_v1 import ( - NetworkPolicyEgressRule, NetworkPolicyIngressRule, NetworkPolicyPeer, NetworkPolicyPort, @@ -82,8 +81,8 @@ def policy_name(self) -> str: """The default policy name that will be created.""" return f"{self._charm.app.name}-network-policy" - def apply_ingress_policy( - self, policies: IngressPolicyDefinition, name: Optional[str] = None + def apply_ingress_policies( + self, policies: List[IngressPolicyDefinition], name: Optional[str] = None ) -> None: """Apply an ingress network policy about a related application. @@ -126,7 +125,7 @@ def apply_ingress_policy( "app.kubernetes.io/name": self._charm.app.name, } ), - policyTypes=["Ingress", "Egress"], + policyTypes=["Ingress"], ingress=ingress, ), ) @@ -144,13 +143,13 @@ def apply_ingress_policy( logger.error(msg) raise NetworkPoliciesHandlerError() - def delete_network_policy(self, name: Optional[str] = None) -> None: + def delete_ingress_policies(self, name: Optional[str] = None) -> None: """Delete a network policy rule.""" if not name: name = self.policy_name try: - self.client.delete(name, namespace=self._charm.model.name) + self.client.delete(NetworkPolicy, name, namespace=self._charm.model.name) except ApiError as e: if e.status.code == 403: msg = f"Kubernetes resources patch failed: `juju trust` this application. {e}" diff --git a/src/charm.py b/src/charm.py index 734db395..f70fa06e 100755 --- a/src/charm.py +++ b/src/charm.py @@ -454,7 +454,7 @@ def _on_config_changed(self, event: ConfigChangedEvent) -> None: self._handle_status_update_config(event) def _cleanup(self, event: RemoveEvent) -> None: - self.network_policy_handler.delete_network_policy() + self.network_policy_handler.delete_ingress_policies() def _update_kratos_endpoints_relation_data(self, event: RelationEvent) -> None: admin_endpoint = ( @@ -541,10 +541,10 @@ def _apply_network_policies(self, event: HookEvent) -> None: return try: - self.network_policy_handler.apply_ingress_policy( + self.network_policy_handler.apply_ingress_policies( [ - (KRATOS_PUBLIC_PORT, [self.public_ingress.relation]), - (KRATOS_ADMIN_PORT, [self.admin_ingress.relation]), + ("public", [self.public_ingress.relation]), + ("admin", [self.admin_ingress.relation]), (38812, []), (38813, []), ] From 47ea9cbdd5fa6aac4c59b0061f19b2d8a6f3273e Mon Sep 17 00:00:00 2001 From: Nikos Date: Tue, 25 Apr 2023 15:12:37 +0300 Subject: [PATCH 12/21] Add comment --- tests/integration/test_charm.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/integration/test_charm.py b/tests/integration/test_charm.py index 762387b7..c6b471d4 100644 --- a/tests/integration/test_charm.py +++ b/tests/integration/test_charm.py @@ -59,6 +59,8 @@ async def test_build_and_deploy(ops_test: OpsTest) -> None: async def test_ingress_relation(ops_test: OpsTest, client: Client) -> None: + # We deploy ingress before running the database migrations to make sure that + # we don't break migrations by applying the network policies (e.g. by breaking dns). await ops_test.model.deploy( TRAEFIK, application_name=TRAEFIK_PUBLIC_APP, From 70544be778c5983d7203bfd8dc5eb63ff10db9c5 Mon Sep 17 00:00:00 2001 From: Nikos Date: Tue, 25 Apr 2023 15:33:32 +0300 Subject: [PATCH 13/21] Target named port We can't use the named port because we have named the ports on the Service and not on the Pod --- src/charm.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/charm.py b/src/charm.py index f70fa06e..d3ad1d6e 100755 --- a/src/charm.py +++ b/src/charm.py @@ -543,8 +543,8 @@ def _apply_network_policies(self, event: HookEvent) -> None: try: self.network_policy_handler.apply_ingress_policies( [ - ("public", [self.public_ingress.relation]), - ("admin", [self.admin_ingress.relation]), + (KRATOS_PUBLIC_PORT, [self.public_ingress.relation]), + (KRATOS_ADMIN_PORT, [self.admin_ingress.relation]), (38812, []), (38813, []), ] From 5bcc3d0443cdd362b65d25bb84b9ed3900c93642 Mon Sep 17 00:00:00 2001 From: Nikos Date: Tue, 25 Apr 2023 17:58:25 +0300 Subject: [PATCH 14/21] Do not defer event on failure --- src/charm.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/charm.py b/src/charm.py index d3ad1d6e..c09492b5 100755 --- a/src/charm.py +++ b/src/charm.py @@ -158,13 +158,9 @@ def __init__(self, *args: Any) -> None: self.framework.observe(self.database.on.database_created, self._on_database_created) self.framework.observe(self.database.on.endpoints_changed, self._on_database_changed) self.framework.observe(self.admin_ingress.on.ready, self._on_admin_ingress_ready) - self.framework.observe(self.admin_ingress.on.ready, self._apply_network_policies) self.framework.observe(self.admin_ingress.on.revoked, self._on_ingress_revoked) - self.framework.observe(self.admin_ingress.on.revoked, self._apply_network_policies) self.framework.observe(self.public_ingress.on.ready, self._on_public_ingress_ready) - self.framework.observe(self.public_ingress.on.ready, self._apply_network_policies) self.framework.observe(self.public_ingress.on.revoked, self._on_ingress_revoked) - self.framework.observe(self.public_ingress.on.revoked, self._apply_network_policies) self.framework.observe( self.external_provider.on.client_config_changed, self._on_client_config_changed ) @@ -535,8 +531,9 @@ def _on_admin_ingress_ready(self, event: IngressPerAppReadyEvent) -> None: self._handle_status_update_config(event) self._update_kratos_endpoints_relation_data(event) + self._apply_network_policies() - def _apply_network_policies(self, event: HookEvent) -> None: + def _apply_network_policies(self) -> None: if not self.unit.is_leader(): return @@ -550,7 +547,7 @@ def _apply_network_policies(self, event: HookEvent) -> None: ] ) except NetworkPoliciesHandlerError: - event.defer() + self.unit.status = BlockedStatus("Failed to apply network policies") def _on_public_ingress_ready(self, event: IngressPerAppReadyEvent) -> None: if self.unit.is_leader(): @@ -558,6 +555,7 @@ def _on_public_ingress_ready(self, event: IngressPerAppReadyEvent) -> None: self._handle_status_update_config(event) self._update_kratos_endpoints_relation_data(event) + self._apply_network_policies() def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent) -> None: if self.unit.is_leader(): @@ -565,6 +563,7 @@ def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent) -> None: self._handle_status_update_config(event) self._update_kratos_endpoints_relation_data(event) + self._apply_network_policies() def _on_client_config_changed(self, event: ClientConfigChangedEvent) -> None: domain_url = self._domain_url From 0b024e9190bcf0e3300d1b11cdff93255ae4f107 Mon Sep 17 00:00:00 2001 From: Nikos Date: Fri, 28 Apr 2023 13:05:04 +0300 Subject: [PATCH 15/21] Requested fixes --- lib/charms/kratos/v0/kubernetes_network_policies.py | 5 +++-- tests/integration/conftest.py | 2 +- tests/unit/conftest.py | 8 +++++--- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/charms/kratos/v0/kubernetes_network_policies.py b/lib/charms/kratos/v0/kubernetes_network_policies.py index 2a2a1d11..91e8bf71 100644 --- a/lib/charms/kratos/v0/kubernetes_network_policies.py +++ b/lib/charms/kratos/v0/kubernetes_network_policies.py @@ -62,7 +62,7 @@ def some_event_function(): class NetworkPoliciesHandlerError(Exception): - """Applying the network policies failed.""" + """Raised when applying the network policies failed.""" Port = Union[str, int] @@ -84,7 +84,7 @@ def policy_name(self) -> str: def apply_ingress_policies( self, policies: List[IngressPolicyDefinition], name: Optional[str] = None ) -> None: - """Apply an ingress network policy about a related application. + """Apply an ingress network policy for a related application. Policies can be defined for multiple ports at once to allow ingress traffic from related applications @@ -130,6 +130,7 @@ def apply_ingress_policies( ), ) + try: self.client.apply( policy, diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index b9673490..b93eade8 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2022 Canonical Ltd. +# Copyright 2023 Canonical Ltd. import os diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index ae84f755..ece26619 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -15,13 +15,15 @@ from kratos import KratosAPI -@pytest.fixture(autouse=True) -def lk_client(mocker): +@pytest.fixture() +def mocked_kubernetes_policy_handler(mocker): return mocker.patch("charm.KubernetesNetworkPoliciesHandler") @pytest.fixture() -def harness(mocked_kubernetes_service_patcher: MagicMock) -> Harness: +def harness( + mocked_kubernetes_service_patcher: MagicMock, mocked_kubernetes_policy_handler: MagicMock +) -> Harness: harness = Harness(KratosCharm) harness.set_model_name("kratos-model") harness.set_can_connect("kratos", True) From a9aa410db3ca893002d852d7ecc1cae7b0b0e31c Mon Sep 17 00:00:00 2001 From: Nikos Date: Fri, 28 Apr 2023 13:05:13 +0300 Subject: [PATCH 16/21] Add unit tests --- .../unit/test_kubernetes_network_policies.py | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 tests/unit/test_kubernetes_network_policies.py diff --git a/tests/unit/test_kubernetes_network_policies.py b/tests/unit/test_kubernetes_network_policies.py new file mode 100644 index 00000000..1945b393 --- /dev/null +++ b/tests/unit/test_kubernetes_network_policies.py @@ -0,0 +1,69 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + +from unittest.mock import MagicMock + +import pytest +from charms.kratos.v0.kubernetes_network_policies import ( + KubernetesNetworkPoliciesHandler, + NetworkPoliciesHandlerError, +) +from httpx import Response +from lightkube import ApiError, Client + + +@pytest.fixture() +def mock_charm() -> MagicMock: + charm = MagicMock() + charm.app = MagicMock() + charm.app.name = "app" + charm.model = MagicMock() + charm.model.name = "model" + return charm + + +@pytest.fixture() +def policy_handler(mock_charm: MagicMock) -> KubernetesNetworkPoliciesHandler: + handler = KubernetesNetworkPoliciesHandler(mock_charm) + handler.client = MagicMock(spec=Client) + return handler + + +def test_apply_ingress_policies_allow(policy_handler: KubernetesNetworkPoliciesHandler) -> None: + policy_handler.apply_ingress_policies([(8080, [])]) + + policy_handler.client.apply.assert_called() + + +def test_apply_ingress_policies_allow_no_trust( + policy_handler: KubernetesNetworkPoliciesHandler, caplog: pytest.LogCaptureFixture +) -> None: + resp = Response(status_code=403, json={"message": "Forbidden", "code": 403}) + policy_handler.client.apply = MagicMock(side_effect=ApiError(response=resp)) + + with pytest.raises(NetworkPoliciesHandlerError): + policy_handler.apply_ingress_policies([(8080, [])]) + + assert caplog.messages[0].startswith( + "Kubernetes resources patch failed: `juju trust` this application." + ) + + +def test_delete_ingress_policies(policy_handler: KubernetesNetworkPoliciesHandler) -> None: + policy_handler.delete_ingress_policies() + + policy_handler.client.delete.assert_called() + + +def test_delete_ingress_policies_allow_no_trust( + policy_handler: KubernetesNetworkPoliciesHandler, caplog: pytest.LogCaptureFixture +) -> None: + resp = Response(status_code=403, json={"message": "Forbidden", "code": 403}) + policy_handler.client.delete = MagicMock(side_effect=ApiError(response=resp)) + + with pytest.raises(NetworkPoliciesHandlerError): + policy_handler.delete_ingress_policies([(8080, [])]) + + assert caplog.messages[0].startswith( + "Kubernetes resources patch failed: `juju trust` this application." + ) From b902862431cdd5aa952ffd3996cebb1d63caa7e3 Mon Sep 17 00:00:00 2001 From: Nikos Date: Fri, 28 Apr 2023 15:09:26 +0300 Subject: [PATCH 17/21] Add integration test --- src/charm.py | 1 + tests/integration/test_charm.py | 45 ++++++++++++++++++++------------- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/src/charm.py b/src/charm.py index c09492b5..a25b1205 100755 --- a/src/charm.py +++ b/src/charm.py @@ -450,6 +450,7 @@ def _on_config_changed(self, event: ConfigChangedEvent) -> None: self._handle_status_update_config(event) def _cleanup(self, event: RemoveEvent) -> None: + logger.info("Removing charm") self.network_policy_handler.delete_ingress_policies() def _update_kratos_endpoints_relation_data(self, event: RelationEvent) -> None: diff --git a/tests/integration/test_charm.py b/tests/integration/test_charm.py index c6b471d4..6da19fa4 100644 --- a/tests/integration/test_charm.py +++ b/tests/integration/test_charm.py @@ -51,11 +51,26 @@ async def test_build_and_deploy(ops_test: OpsTest) -> None: Assert on the unit status before any relations/configurations take place. """ + await ops_test.model.deploy( + POSTGRES, + channel="14/stable", + trust=True, + ) charm = await ops_test.build_charm(".") resources = {"oci-image": METADATA["resources"]["oci-image"]["upstream-source"]} await ops_test.model.deploy( charm, resources=resources, application_name=APP_NAME, trust=True, series="jammy" ) + await ops_test.model.add_relation(APP_NAME, POSTGRES) + + async with ops_test.fast_forward(): + await ops_test.model.wait_for_idle( + apps=[APP_NAME, POSTGRES], + status="active", + raise_on_blocked=True, + timeout=1000, + ) + assert ops_test.model.applications[APP_NAME].units[0].workload_status == "active" async def test_ingress_relation(ops_test: OpsTest, client: Client) -> None: @@ -88,23 +103,6 @@ async def test_ingress_relation(ops_test: OpsTest, client: Client) -> None: assert policy -async def test_postgres_relation(ops_test: OpsTest) -> None: - await ops_test.model.deploy( - POSTGRES, - channel="14/stable", - trust=True, - ) - await ops_test.model.add_relation(APP_NAME, POSTGRES) - - async with ops_test.fast_forward(): - await ops_test.model.wait_for_idle( - apps=[APP_NAME, POSTGRES], - status="active", - timeout=1000, - ) - assert ops_test.model.applications[APP_NAME].units[0].workload_status == "active" - - async def test_has_public_ingress(ops_test: OpsTest) -> None: # Get the traefik address and try to reach kratos public_address = await get_unit_address(ops_test, TRAEFIK_PUBLIC_APP, 0) @@ -263,3 +261,16 @@ async def test_identity_schemas_config(ops_test: OpsTest) -> None: resp = requests.get(f"http://{public_address}/{ops_test.model.name}-{APP_NAME}/schemas") assert original_resp == resp.json() + + +@pytest.mark.skip( + reason=("sometimes the event hook is not fired so the resources don't get cleaned up.") +) +async def test_charm_removal(ops_test: OpsTest, client: Client) -> None: + await ops_test.model.remove_application( + APP_NAME, force=True, block_until_done=True, destroy_storage=True + ) + + # Validate network policies are removed when ingress is provided + policy = client.get(NetworkPolicy, "kratos-network-policy", namespace=ops_test.model.name) + assert not policy From e711dd37ecfb86cc703fdf7fef9a9f561e228e76 Mon Sep 17 00:00:00 2001 From: Nikos Date: Fri, 28 Apr 2023 15:32:10 +0300 Subject: [PATCH 18/21] Swallow error if applying network policies failed --- src/charm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/charm.py b/src/charm.py index a25b1205..022b4068 100755 --- a/src/charm.py +++ b/src/charm.py @@ -548,7 +548,7 @@ def _apply_network_policies(self) -> None: ] ) except NetworkPoliciesHandlerError: - self.unit.status = BlockedStatus("Failed to apply network policies") + pass def _on_public_ingress_ready(self, event: IngressPerAppReadyEvent) -> None: if self.unit.is_leader(): From 02bb8306ea1786b4b5713079596617dc0aed956c Mon Sep 17 00:00:00 2001 From: Nikos Date: Fri, 28 Apr 2023 15:42:06 +0300 Subject: [PATCH 19/21] Mock lk client --- tests/unit/test_kubernetes_network_policies.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/unit/test_kubernetes_network_policies.py b/tests/unit/test_kubernetes_network_policies.py index 1945b393..198a7cf0 100644 --- a/tests/unit/test_kubernetes_network_policies.py +++ b/tests/unit/test_kubernetes_network_policies.py @@ -4,6 +4,7 @@ from unittest.mock import MagicMock import pytest +from pytest_mock import MockerFixture from charms.kratos.v0.kubernetes_network_policies import ( KubernetesNetworkPoliciesHandler, NetworkPoliciesHandlerError, @@ -12,6 +13,11 @@ from lightkube import ApiError, Client +@pytest.fixture(autouse=True) +def mock_lk_client(mocker: MockerFixture) -> None: + mocker.patch("charms.kratos.v0.kubernetes_network_policies.Client") + + @pytest.fixture() def mock_charm() -> MagicMock: charm = MagicMock() From a43a119c7ace0f37e9f3186906b1b7b6286be206 Mon Sep 17 00:00:00 2001 From: Nikos Date: Fri, 28 Apr 2023 15:42:51 +0300 Subject: [PATCH 20/21] Add type hint --- tests/unit/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index ece26619..4484861f 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -16,7 +16,7 @@ @pytest.fixture() -def mocked_kubernetes_policy_handler(mocker): +def mocked_kubernetes_policy_handler(mocker: MockerFixture) -> None: return mocker.patch("charm.KubernetesNetworkPoliciesHandler") From 7306c38402dbefe6a8cd3ca3a3ec9956c8aed704 Mon Sep 17 00:00:00 2001 From: Nikos Date: Fri, 28 Apr 2023 15:43:21 +0300 Subject: [PATCH 21/21] Fix linting --- tests/unit/test_kubernetes_network_policies.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_kubernetes_network_policies.py b/tests/unit/test_kubernetes_network_policies.py index 198a7cf0..94476b8a 100644 --- a/tests/unit/test_kubernetes_network_policies.py +++ b/tests/unit/test_kubernetes_network_policies.py @@ -4,13 +4,13 @@ from unittest.mock import MagicMock import pytest -from pytest_mock import MockerFixture from charms.kratos.v0.kubernetes_network_policies import ( KubernetesNetworkPoliciesHandler, NetworkPoliciesHandlerError, ) from httpx import Response from lightkube import ApiError, Client +from pytest_mock import MockerFixture @pytest.fixture(autouse=True)