From e9e0de7cba7bfbe81824efcd276dc1ab414ceec9 Mon Sep 17 00:00:00 2001 From: Dylan Stephano-Shachter Date: Fri, 12 Jul 2024 11:48:15 +0000 Subject: [PATCH 01/14] first pass --- .../istio_beacon_k8s/v0/service_mesh.py | 119 ++++++++++++++++++ src/charm.py | 2 +- tox.ini | 8 +- 3 files changed, 123 insertions(+), 6 deletions(-) create mode 100644 lib/charms/istio_beacon_k8s/v0/service_mesh.py diff --git a/lib/charms/istio_beacon_k8s/v0/service_mesh.py b/lib/charms/istio_beacon_k8s/v0/service_mesh.py new file mode 100644 index 0000000..b0ca043 --- /dev/null +++ b/lib/charms/istio_beacon_k8s/v0/service_mesh.py @@ -0,0 +1,119 @@ +# Copyright 2024 Canonical Ltd. +# See LICENSE file for licensing details. + +"""#Service Mesh Library. + +The service mesh library is used to facilitate adding you charmed application to a service mesh. +The library leverages the `service_mesh` interface and the `cross_model_mesh` interface + +To add service mesh support to your charm, instantiate a ServiceMeshConsumer object in the +`__init__` method of your charm: + +``` +from charms.istio_beacon_k8s.v0.service_mesh import IsCrossModelError, Policy, ServiceMeshConsumer + +... +self._mesh = ServiceMeshConsumer( + self, + policies=[ + Policy(relation="logging", endpoints=[f"*:{HTTP_LISTEN_PORT}"]), + ], +) +``` +""" + +import json +import logging +import re +from typing import List, Optional + +import pydantic +from ops import CharmBase, Object + +LIBID = "3f40cb7e3569454a92ac2541c5ca0a0c" # Never change this +LIBAPI = 0 +LIBPATCH = 1 + +logger = logging.getLogger(__name__) + + +class Policy(pydantic.BaseModel): + """Data type for holding a service mesh policy.""" + + relation: str + endpoints: List[str] + + +class ServiceMeshConsumer(Object): + """Class for managing the consumer side of the service_mesh relation interface.""" + + def __init__( + self, + charm: CharmBase, + mesh_relation_name: str = "service-mesh", + policies: Optional[List[Policy]] = None, + ): + """Class used for joining a service mesh. + + Args: + charm: The charm instantiating this object. + mesh_relation_name: The relation name as defined in metadata.yaml or charmcraft.yaml + for the relation which used the service_mesh interface. + policies: List of access policies this charm supports. + """ + super().__init__(charm, mesh_relation_name) + self._charm = charm + self._relations = self._charm.model.relations[mesh_relation_name] + self._policies = policies or [] + self.framework.observe( + self._charm.on[mesh_relation_name].relation_created, self._relations_changed + ) + self.framework.observe(self._charm.on.upgrade_charm, self._relations_changed) + for policy in self._policies: + self.framework.observe( + self._charm.on[policy.relation].relation_created, self._relations_changed + ) + self.framework.observe( + self._charm.on[policy.relation].relation_broken, self._relations_changed + ) + + def _relations_changed(self, _event): + self.update_service_mesh() + + def update_service_mesh(self): + """Update the service mesh. + + Gathers information from all relations of the charm and updates the mesh appropriately to + allow communication. + """ + logger.debug("Updating service mesh policies.") + policies = [] + cmr_matcher = re.compile(r"remote\-[a-f0-9]+") + for policy in self._policies: + for relation in self._charm.model.relations[policy.relation]: + if cmr_matcher.fullmatch(relation.app.name): + logger.debug( + f"Cross model relation found: {relation.name}. Currently not implemented. Skipping." + ) + else: + logger.debug(f"Found relation: {relation.name}. Creating policy.") + policies.append( + { + "app_name": relation.app.name, + "namespace": self._my_namespace(), + "endpoints": policy.endpoints, + } + ) + mesh_rel_data = { + "app_name": self._charm.app.name, + "model": self._my_namespace(), + "policies": policies, + } + for rel in self._relations: + rel.data[self._charm.app]["mesh_data"] = json.dumps(mesh_rel_data) + + def _my_namespace(self): + """Return the namespace of the running charm.""" + # This method currently assumes the namespace is the same as the model name. We + # should consider if there is a better way to do this. + return self._charm.model.name diff --git a/src/charm.py b/src/charm.py index d484197..77a7132 100755 --- a/src/charm.py +++ b/src/charm.py @@ -35,8 +35,8 @@ def update_mesh(self): manifest. """ for relation in self.model.relations["service-mesh"]: + logger.error(relation.data[relation.app]) # Update the mesh - pass self.unit.status = ops.ActiveStatus() diff --git a/tox.ini b/tox.ini index ba3d9a8..d49a75f 100644 --- a/tox.ini +++ b/tox.ini @@ -10,8 +10,8 @@ min_version = 4.0.0 [vars] src_path = {tox_root}/src tests_path = {tox_root}/tests -;lib_path = {tox_root}/lib/charms/operator_name_with_underscores -all_path = {[vars]src_path} {[vars]tests_path} +lib_path = {tox_root}/lib/charms/istio_beacon_k8s +all_path = {[vars]src_path} {[vars]tests_path} {[vars]lib_path} [testenv] set_env = @@ -27,10 +27,8 @@ pass_env = description = Apply coding style standards to code deps = black - ruff commands = black {[vars]all_path} - ruff check --fix {[vars]all_path} [testenv:lint] description = Check code against coding style standards @@ -75,7 +73,7 @@ deps = -r {tox_root}/requirements.txt commands = charm: pyright {posargs} {[vars]src_path} - lib: + lib: pyright {posargs} {[vars]lib_path} [testenv:integration] description = Run integration tests From e9a1513c60188889102df3f17f57f82ca52dd583 Mon Sep 17 00:00:00 2001 From: Dylan Stephano-Shachter Date: Wed, 17 Jul 2024 16:31:08 -0400 Subject: [PATCH 02/14] Fix import in doc. Co-authored-by: Andrew Scribner --- lib/charms/istio_beacon_k8s/v0/service_mesh.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/charms/istio_beacon_k8s/v0/service_mesh.py b/lib/charms/istio_beacon_k8s/v0/service_mesh.py index b0ca043..303b54d 100644 --- a/lib/charms/istio_beacon_k8s/v0/service_mesh.py +++ b/lib/charms/istio_beacon_k8s/v0/service_mesh.py @@ -10,7 +10,7 @@ `__init__` method of your charm: ``` -from charms.istio_beacon_k8s.v0.service_mesh import IsCrossModelError, Policy, ServiceMeshConsumer +from charms.istio_beacon_k8s.v0.service_mesh import Policy, ServiceMeshConsumer ... self._mesh = ServiceMeshConsumer( From 974eca60d648e1b1ced8660f3c9307e2c085fdf0 Mon Sep 17 00:00:00 2001 From: Dylan Stephano-Shachter Date: Mon, 22 Jul 2024 13:10:57 +0000 Subject: [PATCH 03/14] do not allow multiple mesh relations --- .../istio_beacon_k8s/v0/service_mesh.py | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/lib/charms/istio_beacon_k8s/v0/service_mesh.py b/lib/charms/istio_beacon_k8s/v0/service_mesh.py index 303b54d..624ad64 100644 --- a/lib/charms/istio_beacon_k8s/v0/service_mesh.py +++ b/lib/charms/istio_beacon_k8s/v0/service_mesh.py @@ -13,12 +13,15 @@ from charms.istio_beacon_k8s.v0.service_mesh import Policy, ServiceMeshConsumer ... -self._mesh = ServiceMeshConsumer( - self, - policies=[ - Policy(relation="logging", endpoints=[f"*:{HTTP_LISTEN_PORT}"]), - ], -) +try: + self._mesh = ServiceMeshConsumer( + self, + policies=[ + Policy(relation="logging", endpoints=[f"*:{HTTP_LISTEN_PORT}"]), + ], + ) +except ops.TooManyRelatedAppsError as e: + self.unit.status = BlockedStatus(e) ``` """ @@ -63,7 +66,7 @@ def __init__( """ super().__init__(charm, mesh_relation_name) self._charm = charm - self._relations = self._charm.model.relations[mesh_relation_name] + self._relation = self._charm.model.get_relation[mesh_relation_name] self._policies = policies or [] self.framework.observe( self._charm.on[mesh_relation_name].relation_created, self._relations_changed @@ -109,8 +112,7 @@ def update_service_mesh(self): "model": self._my_namespace(), "policies": policies, } - for rel in self._relations: - rel.data[self._charm.app]["mesh_data"] = json.dumps(mesh_rel_data) + self._relation.data[self._charm.app]["mesh_data"] = json.dumps(mesh_rel_data) def _my_namespace(self): """Return the namespace of the running charm.""" From 69cf81513a613f3493148bc88d2cf0ba77415438 Mon Sep 17 00:00:00 2001 From: Dylan Stephano-Shachter Date: Mon, 22 Jul 2024 13:14:54 +0000 Subject: [PATCH 04/14] better docstring --- lib/charms/istio_beacon_k8s/v0/service_mesh.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/charms/istio_beacon_k8s/v0/service_mesh.py b/lib/charms/istio_beacon_k8s/v0/service_mesh.py index 624ad64..2af0aff 100644 --- a/lib/charms/istio_beacon_k8s/v0/service_mesh.py +++ b/lib/charms/istio_beacon_k8s/v0/service_mesh.py @@ -48,7 +48,7 @@ class Policy(pydantic.BaseModel): class ServiceMeshConsumer(Object): - """Class for managing the consumer side of the service_mesh relation interface.""" + """Class used for joining a service mesh.""" def __init__( self, From 876f7500705738e37e4891851e19cedb0bc4d19c Mon Sep 17 00:00:00 2001 From: Dylan Stephano-Shachter Date: Tue, 23 Jul 2024 16:43:30 +0000 Subject: [PATCH 05/14] codespell on lib --- tox.ini | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tox.ini b/tox.ini index d49a75f..3f79b77 100644 --- a/tox.ini +++ b/tox.ini @@ -37,10 +37,8 @@ deps = ruff codespell commands = - # if this charm owns a lib, uncomment "lib_path" variable - # and uncomment the following line - # codespell {[vars]lib_path} - codespell {tox_root} + codespell {[vars]src_path} + codespell {[vars]lib_path} ruff check {[vars]all_path} black --check --diff {[vars]all_path} From 0b37e60d815e6a443b987780e664b294b2df8484 Mon Sep 17 00:00:00 2001 From: Dylan Stephano-Shachter Date: Tue, 23 Jul 2024 17:10:10 +0000 Subject: [PATCH 06/14] rich endpoints --- .../istio_beacon_k8s/v0/service_mesh.py | 22 ++++++++++++++++++- pyproject.toml | 1 + 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/lib/charms/istio_beacon_k8s/v0/service_mesh.py b/lib/charms/istio_beacon_k8s/v0/service_mesh.py index 2af0aff..efee337 100644 --- a/lib/charms/istio_beacon_k8s/v0/service_mesh.py +++ b/lib/charms/istio_beacon_k8s/v0/service_mesh.py @@ -25,6 +25,7 @@ ``` """ +import enum import json import logging import re @@ -37,14 +38,33 @@ LIBAPI = 0 LIBPATCH = 1 +PYDEPS = ["pydantic"] + logger = logging.getLogger(__name__) +class Method(str, enum.Enum): + """HTTP method.""" + + connect = "CONNECT" + delete = "DELETE" + get = "GET" + head = "HEAD" + options = "OPTIONS" + patch = "PATCH" + post = "POST" + put = "PUT" + trace = "TRACE" + + class Policy(pydantic.BaseModel): """Data type for holding a service mesh policy.""" relation: str - endpoints: List[str] + hosts: List[str] + ports: List[int] + methods: List[Method] + paths: List[str] class ServiceMeshConsumer(Object): diff --git a/pyproject.toml b/pyproject.toml index 790f31b..1e4f0bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,7 @@ extend-ignore = [ "D408", "D409", "D413", + "N805", ] ignore = ["E501", "D107"] per-file-ignores = {"tests/*" = ["D100","D101","D102","D103","D104"]} From f6ca4ea5d6dca1ae64c40f10d1692a6efa46c0d4 Mon Sep 17 00:00:00 2001 From: Dylan Stephano-Shachter Date: Thu, 25 Jul 2024 14:27:31 +0000 Subject: [PATCH 07/14] Endpoint object --- lib/charms/istio_beacon_k8s/v0/service_mesh.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/charms/istio_beacon_k8s/v0/service_mesh.py b/lib/charms/istio_beacon_k8s/v0/service_mesh.py index efee337..c968b11 100644 --- a/lib/charms/istio_beacon_k8s/v0/service_mesh.py +++ b/lib/charms/istio_beacon_k8s/v0/service_mesh.py @@ -57,16 +57,22 @@ class Method(str, enum.Enum): trace = "TRACE" -class Policy(pydantic.BaseModel): - """Data type for holding a service mesh policy.""" +class Endpoint(pydantic.BaseModel): + """Data type for a policy endpoint.""" - relation: str hosts: List[str] ports: List[int] methods: List[Method] paths: List[str] +class Policy(pydantic.BaseModel): + """Data type for holding a service mesh policy.""" + + relation: str + endpoints: List[Endpoint] + + class ServiceMeshConsumer(Object): """Class used for joining a service mesh.""" From 311818f0d62ad79fdad5710ceba72d10b6ea3342 Mon Sep 17 00:00:00 2001 From: Dylan Stephano-Shachter Date: Thu, 25 Jul 2024 21:29:53 +0000 Subject: [PATCH 08/14] fix pydantic errors --- lib/charms/istio_beacon_k8s/v0/service_mesh.py | 4 +++- tox.ini | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/charms/istio_beacon_k8s/v0/service_mesh.py b/lib/charms/istio_beacon_k8s/v0/service_mesh.py index c968b11..59ea9b4 100644 --- a/lib/charms/istio_beacon_k8s/v0/service_mesh.py +++ b/lib/charms/istio_beacon_k8s/v0/service_mesh.py @@ -92,7 +92,7 @@ def __init__( """ super().__init__(charm, mesh_relation_name) self._charm = charm - self._relation = self._charm.model.get_relation[mesh_relation_name] + self._relation = self._charm.model.get_relation(mesh_relation_name) self._policies = policies or [] self.framework.observe( self._charm.on[mesh_relation_name].relation_created, self._relations_changed @@ -115,6 +115,8 @@ def update_service_mesh(self): Gathers information from all relations of the charm and updates the mesh appropriately to allow communication. """ + if self._relation is None: + return logger.debug("Updating service mesh policies.") policies = [] cmr_matcher = re.compile(r"remote\-[a-f0-9]+") diff --git a/tox.ini b/tox.ini index 3f79b77..1595490 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ [tox] no_package = True skip_missing_interpreters = True -env_list = lint, static, unit, scenario +env_list = lint, static-charm, static-lib, unit, scenario min_version = 4.0.0 [vars] @@ -68,6 +68,7 @@ commands = description = Run static type checks deps = pyright + lib: pydantic -r {tox_root}/requirements.txt commands = charm: pyright {posargs} {[vars]src_path} From 7a9800d2c6c20890eef2d41462aba6817900501f Mon Sep 17 00:00:00 2001 From: Dylan Stephano-Shachter Date: Thu, 25 Jul 2024 21:39:13 +0000 Subject: [PATCH 09/14] add service arg to Policy --- lib/charms/istio_beacon_k8s/v0/service_mesh.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/charms/istio_beacon_k8s/v0/service_mesh.py b/lib/charms/istio_beacon_k8s/v0/service_mesh.py index 59ea9b4..ca063f1 100644 --- a/lib/charms/istio_beacon_k8s/v0/service_mesh.py +++ b/lib/charms/istio_beacon_k8s/v0/service_mesh.py @@ -71,6 +71,7 @@ class Policy(pydantic.BaseModel): relation: str endpoints: List[Endpoint] + service: Optional[str] class ServiceMeshConsumer(Object): @@ -133,6 +134,7 @@ def update_service_mesh(self): "app_name": relation.app.name, "namespace": self._my_namespace(), "endpoints": policy.endpoints, + "service": policy.service, } ) mesh_rel_data = { From eebf403cda3b48134643ffba6c77e2afdd5d8462 Mon Sep 17 00:00:00 2001 From: Dylan Stephano-Shachter Date: Thu, 25 Jul 2024 21:41:25 +0000 Subject: [PATCH 10/14] serialize the endpoints --- lib/charms/istio_beacon_k8s/v0/service_mesh.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/charms/istio_beacon_k8s/v0/service_mesh.py b/lib/charms/istio_beacon_k8s/v0/service_mesh.py index ca063f1..e7b46a4 100644 --- a/lib/charms/istio_beacon_k8s/v0/service_mesh.py +++ b/lib/charms/istio_beacon_k8s/v0/service_mesh.py @@ -133,7 +133,7 @@ def update_service_mesh(self): { "app_name": relation.app.name, "namespace": self._my_namespace(), - "endpoints": policy.endpoints, + "endpoints": [endpoint.model_dump() for endpoint in policy.endpoints], "service": policy.service, } ) From 529c05230af5673fa4f74075de92d5b77d8c6334 Mon Sep 17 00:00:00 2001 From: Dylan Stephano-Shachter Date: Thu, 25 Jul 2024 21:48:28 +0000 Subject: [PATCH 11/14] use binary package for pydantic --- charmcraft.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/charmcraft.yaml b/charmcraft.yaml index 214388e..84bdad3 100644 --- a/charmcraft.yaml +++ b/charmcraft.yaml @@ -36,3 +36,10 @@ bases: provides: service-mesh: interface: service_mesh + +parts: + charm: + charm-binary-python-packages: + # Pydantic is not actually used by the lib but charmcraft install pydeps from libs even if + # they are not used. + - pydantic>2.0 From d6665d5eafb1cb42420ee312c9cc4f467dcdb50e Mon Sep 17 00:00:00 2001 From: Dylan Stephano-Shachter Date: Thu, 25 Jul 2024 21:57:46 +0000 Subject: [PATCH 12/14] update example in doc --- lib/charms/istio_beacon_k8s/v0/service_mesh.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/charms/istio_beacon_k8s/v0/service_mesh.py b/lib/charms/istio_beacon_k8s/v0/service_mesh.py index e7b46a4..cf89f42 100644 --- a/lib/charms/istio_beacon_k8s/v0/service_mesh.py +++ b/lib/charms/istio_beacon_k8s/v0/service_mesh.py @@ -17,7 +17,18 @@ self._mesh = ServiceMeshConsumer( self, policies=[ - Policy(relation="logging", endpoints=[f"*:{HTTP_LISTEN_PORT}"]), + Policy( + relation="logging", + endpoints=[ + Endpoint( + hosts=[self._my_host_name], + ports=[HTTP_LISTEN_PORT], + methods=["GET"], + paths=["/foo"], + ), + ], + service=self._my_k8s_service(), + ), ], ) except ops.TooManyRelatedAppsError as e: From efca89932b6d842ca8e1ffcc08a19b380f250d04 Mon Sep 17 00:00:00 2001 From: Dylan Stephano-Shachter Date: Thu, 25 Jul 2024 17:59:07 -0400 Subject: [PATCH 13/14] typo Co-authored-by: Leon <82407168+sed-i@users.noreply.github.com> --- lib/charms/istio_beacon_k8s/v0/service_mesh.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/charms/istio_beacon_k8s/v0/service_mesh.py b/lib/charms/istio_beacon_k8s/v0/service_mesh.py index cf89f42..1ade378 100644 --- a/lib/charms/istio_beacon_k8s/v0/service_mesh.py +++ b/lib/charms/istio_beacon_k8s/v0/service_mesh.py @@ -3,7 +3,7 @@ """#Service Mesh Library. -The service mesh library is used to facilitate adding you charmed application to a service mesh. +The service mesh library is used to facilitate adding your charmed application to a service mesh. The library leverages the `service_mesh` interface and the `cross_model_mesh` interface To add service mesh support to your charm, instantiate a ServiceMeshConsumer object in the From 229290820ca82fc3f3361c988c76be54e0968048 Mon Sep 17 00:00:00 2001 From: Dylan Stephano-Shachter Date: Thu, 25 Jul 2024 17:59:37 -0400 Subject: [PATCH 14/14] reword Co-authored-by: Leon <82407168+sed-i@users.noreply.github.com> --- lib/charms/istio_beacon_k8s/v0/service_mesh.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/charms/istio_beacon_k8s/v0/service_mesh.py b/lib/charms/istio_beacon_k8s/v0/service_mesh.py index 1ade378..4139b71 100644 --- a/lib/charms/istio_beacon_k8s/v0/service_mesh.py +++ b/lib/charms/istio_beacon_k8s/v0/service_mesh.py @@ -4,7 +4,7 @@ """#Service Mesh Library. The service mesh library is used to facilitate adding your charmed application to a service mesh. -The library leverages the `service_mesh` interface and the `cross_model_mesh` interface +The library leverages the `service_mesh` and `cross_model_mesh` interfaces. To add service mesh support to your charm, instantiate a ServiceMeshConsumer object in the `__init__` method of your charm: