Skip to content
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

Mesh lib #3

Merged
merged 14 commits into from
Jul 26, 2024
151 changes: 151 additions & 0 deletions lib/charms/istio_beacon_k8s/v0/service_mesh.py
dstathis marked this conversation as resolved.
Show resolved Hide resolved
dstathis marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# Copyright 2024 Canonical Ltd.
# See LICENSE file for licensing details.

"""#Service Mesh Library.
dstathis marked this conversation as resolved.
Show resolved Hide resolved

The service mesh library is used to facilitate adding you charmed application to a service mesh.
dstathis marked this conversation as resolved.
Show resolved Hide resolved
The library leverages the `service_mesh` interface and the `cross_model_mesh` interface
dstathis marked this conversation as resolved.
Show resolved Hide resolved

dstathis marked this conversation as resolved.
Show resolved Hide resolved
To add service mesh support to your charm, instantiate a ServiceMeshConsumer object in the
dstathis marked this conversation as resolved.
Show resolved Hide resolved
`__init__` method of your charm:

```
from charms.istio_beacon_k8s.v0.service_mesh import Policy, ServiceMeshConsumer

...
try:
self._mesh = ServiceMeshConsumer(
self,
policies=[
Policy(relation="logging", endpoints=[f"*:{HTTP_LISTEN_PORT}"]),
dstathis marked this conversation as resolved.
Show resolved Hide resolved
],
)
except ops.TooManyRelatedAppsError as e:
self.unit.status = BlockedStatus(e)
```
"""

import enum
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

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 Endpoint(pydantic.BaseModel):
"""Data type for a policy endpoint."""

hosts: List[str]
ports: List[int]
methods: List[Method]
paths: List[str]
dstathis marked this conversation as resolved.
Show resolved Hide resolved


class Policy(pydantic.BaseModel):
"""Data type for holding a service mesh policy."""

relation: str
endpoints: List[Endpoint]
service: Optional[str]


class ServiceMeshConsumer(Object):
"""Class used for joining a service mesh."""

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._relation = self._charm.model.get_relation(mesh_relation_name)
dstathis marked this conversation as resolved.
Show resolved Hide resolved
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.
"""
if self._relation is None:
return
logger.debug("Updating service mesh policies.")
policies = []
cmr_matcher = re.compile(r"remote\-[a-f0-9]+")
dstathis marked this conversation as resolved.
Show resolved Hide resolved
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."
dstathis marked this conversation as resolved.
Show resolved Hide resolved
)
else:
logger.debug(f"Found relation: {relation.name}. Creating policy.")
dstathis marked this conversation as resolved.
Show resolved Hide resolved
policies.append(
{
"app_name": relation.app.name,
"namespace": self._my_namespace(),
IbraAoad marked this conversation as resolved.
Show resolved Hide resolved
"endpoints": [endpoint.model_dump() for endpoint in policy.endpoints],
"service": policy.service,
}
)
mesh_rel_data = {
"app_name": self._charm.app.name,
"model": self._my_namespace(),
dstathis marked this conversation as resolved.
Show resolved Hide resolved
"policies": policies,
}
dstathis marked this conversation as resolved.
Show resolved Hide resolved
self._relation.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
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ extend-ignore = [
"D408",
"D409",
"D413",
"N805",
]
ignore = ["E501", "D107"]
per-file-ignores = {"tests/*" = ["D100","D101","D102","D103","D104"]}
Expand Down
2 changes: 1 addition & 1 deletion src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()


Expand Down
17 changes: 7 additions & 10 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@
[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]
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 =
Expand All @@ -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
Expand All @@ -39,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}

Expand Down Expand Up @@ -72,10 +68,11 @@ commands =
description = Run static type checks
deps =
pyright
lib: pydantic
-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
Expand Down
Loading