-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add network policies #56
base: main
Are you sure you want to change the base?
Changes from all commits
2edc700
bfe6ecb
c72be88
2a4f782
51b8643
c2e5475
08c463a
f821b8d
d3f5a2f
f5b33f7
e000e8e
47ea9cb
70544be
5bcc3d0
0b024e9
a9aa410
b902862
e711dd3
02bb830
a43a119
7306c38
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,160 @@ | ||
#!/usr/bin/env python3 | ||
# Copyright 2023 Canonical Ltd. | ||
# See LICENSE file for licensing details. | ||
|
||
"""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 | ||
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, | ||
) | ||
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 | ||
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 | ||
|
||
|
||
# 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__) | ||
|
||
|
||
class NetworkPoliciesHandlerError(Exception): | ||
"""Raised when applying the network policies failed.""" | ||
|
||
|
||
Port = Union[str, int] | ||
IngressPolicyDefinition = Tuple[Port, List[Relation]] | ||
|
||
|
||
class KubernetesNetworkPoliciesHandler: | ||
"""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_policies( | ||
self, policies: List[IngressPolicyDefinition], name: Optional[str] = None | ||
) -> None: | ||
"""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 | ||
|
||
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, protocol="TCP")], | ||
), | ||
) | ||
|
||
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_ingress_policies(self, name: Optional[str] = None) -> None: | ||
"""Delete a network policy rule.""" | ||
if not name: | ||
name = self.policy_name | ||
|
||
try: | ||
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}" | ||
else: | ||
msg = f"Kubernetes resources patch failed: {e}" | ||
logger.error(msg) | ||
raise NetworkPoliciesHandlerError() |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -31,6 +31,10 @@ | |
LoginUITooManyRelatedAppsError, | ||
) | ||
from charms.kratos.v0.kratos_endpoints import KratosEndpointsProvider | ||
from charms.kratos.v0.kubernetes_network_policies import ( | ||
KubernetesNetworkPoliciesHandler, | ||
NetworkPoliciesHandlerError, | ||
) | ||
from charms.kratos_external_idp_integrator.v0.kratos_external_provider import ( | ||
ClientConfigChangedEvent, | ||
ExternalIdpRequirer, | ||
|
@@ -49,6 +53,7 @@ | |
HookEvent, | ||
PebbleReadyEvent, | ||
RelationEvent, | ||
RemoveEvent, | ||
) | ||
from ops.main import main | ||
from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus, ModelError, WaitingStatus | ||
|
@@ -117,6 +122,7 @@ def __init__(self, *args: Any) -> None: | |
port=KRATOS_PUBLIC_PORT, | ||
strip_prefix=True, | ||
) | ||
self.network_policy_handler = KubernetesNetworkPoliciesHandler(self) | ||
|
||
self.database = DatabaseRequires( | ||
self, | ||
|
@@ -139,6 +145,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 | ||
) | ||
|
@@ -442,6 +449,10 @@ 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: | ||
logger.info("Removing charm") | ||
self.network_policy_handler.delete_ingress_policies() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It might be a juju bug, but when I try removing kratos it gets into terminated state with unit lost. When I use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So I tried testing it a little more and when I test it manually, the policy is removed. I wrote an integration test to validate this and in the test the policy is not removed. This is very weird. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As far as I can tell the stop event is never fired, this is most likely a juju bug and not something we can control. Let's leave this as it is for now, it is the best we can do and juju should (hopefully) fix it in a later version. |
||
|
||
def _update_kratos_endpoints_relation_data(self, event: RelationEvent) -> None: | ||
admin_endpoint = ( | ||
self.admin_ingress.url | ||
|
@@ -521,20 +532,39 @@ 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) -> None: | ||
if not self.unit.is_leader(): | ||
return | ||
|
||
try: | ||
self.network_policy_handler.apply_ingress_policies( | ||
[ | ||
(KRATOS_PUBLIC_PORT, [self.public_ingress.relation]), | ||
(KRATOS_ADMIN_PORT, [self.admin_ingress.relation]), | ||
(38812, []), | ||
(38813, []), | ||
] | ||
) | ||
except NetworkPoliciesHandlerError: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we retry applying the network policy? |
||
pass | ||
|
||
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) | ||
|
||
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(): | ||
logger.info("This app no longer has ingress") | ||
|
||
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 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
#!/usr/bin/env python3 | ||
# Copyright 2023 Canonical Ltd. | ||
|
||
import os | ||
|
||
import pytest | ||
from lightkube import Client, KubeConfig | ||
|
||
KUBECONFIG = os.environ.get("TESTING_KUBECONFIG", "~/.kube/config") | ||
|
||
|
||
@pytest.fixture(scope="module") | ||
def client() -> Client: | ||
return Client(config=KubeConfig.from_file(KUBECONFIG)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where are the unit tests for this file 👀