Skip to content
This repository has been archived by the owner on Nov 13, 2024. It is now read-only.

Commit

Permalink
added stripprefix support (#47)
Browse files Browse the repository at this point in the history
  • Loading branch information
PietroPasotti authored Nov 29, 2023
1 parent 6cd3166 commit efa65b7
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 33 deletions.
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
ops == 1.5.3
ops >= 2.0
Jinja2==3.0.3
typing_extensions
17 changes: 0 additions & 17 deletions run_tests

This file was deleted.

54 changes: 42 additions & 12 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import textwrap
from dataclasses import dataclass
from itertools import starmap
from typing import Iterable, Optional, Tuple
from typing import Iterable, Optional, Tuple, cast
from urllib.parse import urlparse

import jinja2
Expand Down Expand Up @@ -62,7 +62,8 @@ class RouteConfig:

root_url: str
rule: str
id_: str
service_name: str
strip_prefix: bool = False


@dataclass
Expand Down Expand Up @@ -107,7 +108,7 @@ def _check_var(obj: str, name):
valid = starmap(_check_var, ((self.rule, "rule"), (self.root_url, "root_url")))
return all(valid)

def render(self, model_name: str, unit_name: str, app_name: str):
def render(self, model_name: str, unit_name: str, app_name: str, strip_prefix: bool = False):
"""Fills in the blanks in the templates."""

def _render(obj: str):
Expand All @@ -129,8 +130,10 @@ def _render(obj: str):
rule = _render(self.rule)

# an easily recognizable id for the traefik services
id_ = "-".join((unit_name, model_name))
return RouteConfig(rule=rule, root_url=url, id_=id_)
service_name = "-".join((unit_name, model_name))
return RouteConfig(
rule=rule, root_url=url, service_name=service_name, strip_prefix=strip_prefix
)

@staticmethod
def generate_rule_from_url(url) -> str:
Expand Down Expand Up @@ -229,8 +232,15 @@ def root_url(self) -> Optional[str]:
"""The advertised url for the charm requesting ingress."""
return self._config.root_url

def _render_config(self, model_name: str, unit_name: str, app_name: str):
return self._config.render(model_name=model_name, unit_name=unit_name, app_name=app_name)
def _render_config(
self, model_name: str, unit_name: str, app_name: str, strip_prefix: bool = False
):
return self._config.render(
model_name=model_name,
unit_name=unit_name,
app_name=app_name,
strip_prefix=strip_prefix,
)

def _on_config_changed(self, _):
"""Check the config; set an active status if all is good."""
Expand Down Expand Up @@ -297,13 +307,15 @@ def _config_for_unit(self, unit_data: RequirerData) -> RouteConfig:
# if self._is_ready()...
unit_name = unit_data["name"] # pyright: ignore
model_name = unit_data["model"] # pyright: ignore
strip_prefix = bool(unit_data.get("strip-prefix", None))

# sanity checks
assert unit_name is not None, "remote unit did not provide its name"
assert "/" in unit_name, unit_name

return self._render_config(
model_name=model_name,
strip_prefix=strip_prefix,
unit_name=unit_name.replace("/", "-"),
app_name=unit_name.split("/")[0],
)
Expand Down Expand Up @@ -339,12 +351,16 @@ def _update(self):

@staticmethod
def _generate_traefik_unit_config(route_config: RouteConfig) -> "UnitConfig":
rule, config_id, url = route_config.rule, route_config.id_, route_config.root_url
rule, service_name, url = (
route_config.rule,
route_config.service_name,
route_config.root_url,
)

traefik_router_name = f"juju-{config_id}-router"
traefik_service_name = f"juju-{config_id}-service"
traefik_router_name = f"juju-{service_name}-router"
traefik_service_name = f"juju-{service_name}-service"

config: "UnitConfig" = {
config = {
"router": {
"rule": rule,
"service": traefik_service_name,
Expand All @@ -354,16 +370,30 @@ def _generate_traefik_unit_config(route_config: RouteConfig) -> "UnitConfig":
"service": {"loadBalancer": {"servers": [{"url": url}]}},
"service_name": traefik_service_name,
}
return config

if route_config.strip_prefix:
traefik_middleware_name = f"juju-sidecar-noprefix-{service_name}-service"
config["middleware_name"] = traefik_middleware_name
config["middleware"] = {"forceSlash": False, "prefixes": [f"/{service_name}"]}

return cast("UnitConfig", config)

@staticmethod
def _merge_traefik_configs(configs: Iterable["UnitConfig"]) -> "TraefikConfig":
middlewares = {
config.get("middleware_name"): config.get("middleware")
for config in configs
if config.get("middleware")
}
traefik_config = {
"http": {
"routers": {config["router_name"]: config["router"] for config in configs},
"services": {config["service_name"]: config["service"] for config in configs},
}
}
if middlewares:
traefik_config["http"]["middlewares"] = middlewares

return traefik_config # type: ignore


Expand Down
13 changes: 12 additions & 1 deletion src/types_.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

"""Types for TraefikRoute charm."""

from typing import List, Mapping
from typing import List, Mapping, Optional

try:
from typing import TypedDict
Expand Down Expand Up @@ -36,11 +36,22 @@ class Service(TypedDict): # noqa: D101
loadBalancer: Servers # noqa N815


class StripPrefixMiddleware(TypedDict): # noqa: D101
forceSlash: bool # noqa N815
prefixes: List[str]


class Middleware(TypedDict): # noqa: D101
stripPrefix: StripPrefixMiddleware # noqa N815


class UnitConfig(TypedDict): # noqa: D101
router_name: str
router: Router
service_name: str
service: Service
middleware_name: Optional[str]
middleware: Optional[Middleware]


class Http(TypedDict): # noqa: D101
Expand Down
70 changes: 70 additions & 0 deletions tests/scenario/test_route_interface.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import pytest
import yaml
from charm import TraefikRouteK8SCharm
from scenario import Context, Relation, State


@pytest.fixture
def ctx():
return Context(TraefikRouteK8SCharm)


@pytest.mark.parametrize("strip_prefix", (True, False))
def test_config_generation(ctx, strip_prefix):
ipu = Relation(
"ingress-per-unit",
remote_units_data={
0: {
"port": "81",
"host": "foo.com",
"model": "mymodel",
"name": "MimirMcPromFace/0",
"mode": "http",
"strip-prefix": "true" if strip_prefix else "false",
"redirect-https": "true",
"scheme": "http",
}
},
remote_app_name="prometheus",
)

route = Relation("traefik-route", remote_app_name="traefik")

state = State(
leader=True,
config={
"root_url": "{{juju_model}}-{{juju_unit}}.foo.bar/baz",
"rule": "Host(`{{juju_unit}}.bar.baz`)",
},
relations=[ipu, route],
)

state_out = ctx.run(ipu.changed_event, state)
route_out = state_out.get_relations("traefik-route")[0]
strip_prefix_cfg = {
"middlewares": {
"juju-sidecar-noprefix-MimirMcPromFace-0-mymodel-service": {
"forceSlash": False,
"prefixes": ["/MimirMcPromFace-0-mymodel"],
}
}
}
raw_expected_cfg = {
"http": {
"routers": {
"juju-MimirMcPromFace-0-mymodel-router": {
"entryPoints": ["web"],
"rule": "Host(`MimirMcPromFace-0.bar.baz`)",
"service": "juju-MimirMcPromFace-0-mymodel-service",
}
},
"services": {
"juju-MimirMcPromFace-0-mymodel-service": {
"loadBalancer": {"servers": [{"url": "mymodel-MimirMcPromFace-0.foo.bar/baz"}]}
}
},
**(strip_prefix_cfg if strip_prefix else {}),
}
}

assert route_out.local_app_data == {"config": yaml.safe_dump(raw_expected_cfg)}
12 changes: 10 additions & 2 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,18 @@ deps =
-r{toxinidir}/requirements.txt
commands =
coverage run --source={[vars]src_path} \
-m pytest --ignore={[vars]tst_path}integration -v --tb native -s {posargs}
-m pytest -v --tb native -s {[vars]tst_path}unit {posargs}
coverage report

[testenv:scenario]
description = Run integration tests
deps =
pytest
jsonschema
ops-scenario>=5.0
-r{toxinidir}/requirements.txt
commands =
pytest -v --tb native --log-cli-level=INFO -s {[vars]tst_path}scenario {posargs}

[testenv:integration]
description = Run integration tests
Expand All @@ -84,4 +92,4 @@ deps =
pytest-operator
-r{toxinidir}/requirements.txt
commands =
pytest -v --tb native --ignore={[vars]tst_path}unit --log-cli-level=INFO -s {posargs}
pytest -v --tb native --log-cli-level=INFO -s {[vars]tst_path}integration {posargs}

0 comments on commit efa65b7

Please sign in to comment.