Skip to content

Commit

Permalink
feat: use openfga lib v1
Browse files Browse the repository at this point in the history
  • Loading branch information
nsklikas committed Dec 14, 2023
1 parent e747890 commit f5ee976
Show file tree
Hide file tree
Showing 8 changed files with 151 additions and 68 deletions.
4 changes: 3 additions & 1 deletion lib/charms/openfga_k8s/v1/openfga.py
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,9 @@ def update_relation_info(
logger.info(msg, exc_info=True)
raise DataValidationError(msg) from e

def update_server_info(self, grpc_api_url: Optional[str] = None, http_api_url: Optional[str] = None) -> None:
def update_server_info(
self, grpc_api_url: Optional[str] = None, http_api_url: Optional[str] = None
) -> None:
"""Update all the relations databags with the server info."""
if not self.model.unit.is_leader():
return
Expand Down
40 changes: 4 additions & 36 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
from charms.grafana_k8s.v0.grafana_dashboard import GrafanaDashboardProvider
from charms.loki_k8s.v0.loki_push_api import LogProxyConsumer
from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch
from charms.openfga_k8s.v0.openfga import OpenFGAProvider, OpenFGAStoreRequestEvent
from charms.openfga_k8s.v1.openfga import OpenFGAProvider, OpenFGAStoreRequestEvent
from charms.prometheus_k8s.v0.prometheus_scrape import MetricsEndpointProvider
from charms.traefik_k8s.v2.ingress import (
IngressPerAppReadyEvent,
Expand Down Expand Up @@ -198,26 +198,6 @@ def _on_update_status(self, _: UpdateStatusEvent) -> None:
"""Update the status of the charm."""
self._ready()

def _get_http_api_url(self, address: Optional[str] = None) -> str:
if not address:
address = "{}.{}-endpoints.{}.svc.cluster.local".format(
self.unit.name.replace("/", "-"), self.app.name, self.model.name
)
scheme = "http"
url = f"{scheme}://{address}:{OPENFGA_SERVER_HTTP_PORT}"

return url

def _get_grpc_api_url(self, address: Optional[str] = None) -> str:
if not address:
address = "{}.{}-endpoints.{}.svc.cluster.local".format(
self.unit.name.replace("/", "-"), self.app.name, self.model.name
)
scheme = "http"
url = f"{scheme}://{address}:{OPENFGA_SERVER_GRPC_PORT}"

return url

def _get_database_relation_info(self) -> Optional[Dict]:
"""Get database info from relation data bag."""
if not self.database.relations:
Expand Down Expand Up @@ -358,17 +338,8 @@ def _update_workload(self, event: HookEvent) -> None:
self.unit.status = BlockedStatus("Please run schema-upgrade action")
return

# if openfga relation exists, make sure the address is
# updated
if self.unit.is_leader():
openfga_relation = self.model.get_relation("openfga")
if openfga_relation and self.app in openfga_relation.data:
openfga_relation.data[self.app].update(
{
"address": self._get_address(openfga_relation),
"dns_name": self._domain_name,
}
)
# if openfga relation exists, make sure the address is updated
self.openfga_relation.update_server_info(http_api_url=self.http_ingress.url)

self._container.add_layer("openfga", self._pebble_layer, combine=True)
if not self._ready():
Expand Down Expand Up @@ -537,10 +508,7 @@ def _on_openfga_store_requested(self, event: OpenFGAStoreRequestEvent) -> None:

self.openfga_relation.update_relation_info(
store_id=store_id,
address=self._get_address(event.relation),
scheme="http",
port=str(OPENFGA_SERVER_HTTP_PORT),
dns_name=self._domain_name,
http_api_url=self.http_ingress.url,
token=token,
token_secret_id=token_secret_id,
relation_id=event.relation.id,
Expand Down
9 changes: 4 additions & 5 deletions tests/charms/openfga_requires/src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import logging
from typing import Any

from charms.openfga_k8s.v0.openfga import OpenFGARequires, OpenFGAStoreCreateEvent
from charms.openfga_k8s.v1.openfga import OpenFGARequires, OpenFGAStoreCreateEvent
from ops import EventBase
from ops.charm import CharmBase
from ops.main import main
Expand Down Expand Up @@ -54,6 +54,7 @@ def __init__(self, *args: Any) -> None:
def _on_update_status(self, event: EventBase) -> None:
info = self.openfga.get_store_info()
if not info:
self.unit.status = WaitingStatus("waiting for store information")
event.defer()
return

Expand All @@ -79,11 +80,9 @@ def _on_openfga_store_created(self, event: OpenFGAStoreCreateEvent) -> None:
return

logger.info("store id {}".format(info.store_id))
logger.info("token_secret_id {}".format(info.token_secret_id))
logger.info("token {}".format(info.token))
logger.info("address {}".format(info.address))
logger.info("port {}".format(info.port))
logger.info("scheme {}".format(info.scheme))
logger.info("grpc_api_url {}".format(info.grpc_api_url))
logger.info("http_api_url {}".format(info.http_api_url))

self._on_update_status(event)

Expand Down
2 changes: 1 addition & 1 deletion tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,6 @@ async def test_charm(ops_test: OpsTest) -> Path:
@pytest.fixture(scope="module", autouse=True)
def copy_libraries_into_tester_charm(ops_test: OpsTest) -> None:
"""Ensure that the tester charm uses the current libraries."""
lib = Path("lib/charms/openfga_k8s/v0/openfga.py")
lib = Path("lib/charms/openfga_k8s/v1/openfga.py")
Path("tests/integration/openfga_requires", lib.parent).mkdir(parents=True, exist_ok=True)
shutil.copyfile(lib.as_posix(), "tests/charms/openfga_requires/{}".format(lib.as_posix()))
89 changes: 80 additions & 9 deletions tests/integration/test_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,23 @@
from pathlib import Path

import pytest
import requests
import yaml
from pytest_operator.plugin import OpsTest

logger = logging.getLogger(__name__)

METADATA = yaml.safe_load(Path("./metadata.yaml").read_text())
APP_NAME = "openfga"
OPENFGA_APP = "openfga"
TRAEFIK_CHARM = "traefik-k8s"
TRAEFIK_GRPC_APP = "traefik-grpc"
TRAEFIK_HTTP_APP = "traefik-http"


async def get_unit_address(ops_test: OpsTest, app_name: str, unit_num: int) -> str:
"""Get private address of a unit."""
status = await ops_test.model.get_status() # noqa: F821
return status["applications"][app_name]["units"][f"{app_name}/{unit_num}"]["address"]


@pytest.mark.abort_on_fail
Expand All @@ -27,10 +37,7 @@ async def test_build_and_deploy(ops_test: OpsTest, charm: str, test_charm: str)
logger.debug("deploying charms")
await asyncio.gather(
ops_test.model.deploy(
charm,
resources=resources,
application_name=APP_NAME,
series="jammy",
charm, resources=resources, application_name=OPENFGA_APP, series="jammy", trust=True
),
ops_test.model.deploy(
"postgresql-k8s", application_name="postgresql", channel="edge", trust=True
Expand All @@ -43,16 +50,40 @@ async def test_build_and_deploy(ops_test: OpsTest, charm: str, test_charm: str)
)

logger.debug("adding postgresql relation")
await ops_test.model.integrate(APP_NAME, "postgresql:database")
await ops_test.model.integrate(OPENFGA_APP, "postgresql:database")
await ops_test.model.wait_for_idle(
apps=[OPENFGA_APP, "postgresql"],
status="active",
timeout=1000,
)

await ops_test.model.deploy(
TRAEFIK_CHARM,
application_name=TRAEFIK_GRPC_APP,
channel="latest/edge",
config={"external_hostname": "grpc"},
)
await ops_test.model.deploy(
TRAEFIK_CHARM,
application_name=TRAEFIK_HTTP_APP,
channel="latest/edge",
config={"external_hostname": "http"},
)
await ops_test.model.wait_for_idle(
apps=[APP_NAME, "postgresql"],
apps=[TRAEFIK_GRPC_APP, TRAEFIK_HTTP_APP],
status="active",
raise_on_blocked=True,
timeout=1000,
)

assert ops_test.model.applications[APP_NAME].status == "active"
await ops_test.model.integrate(f"{OPENFGA_APP}:grpc-ingress", TRAEFIK_GRPC_APP)
await ops_test.model.integrate(f"{OPENFGA_APP}:http-ingress", TRAEFIK_HTTP_APP)


async def test_requirer_charm_integration(ops_test: OpsTest) -> None:
assert ops_test.model.applications[OPENFGA_APP].status == "active"

await ops_test.model.integrate(APP_NAME, "openfga-requires")
await ops_test.model.integrate(OPENFGA_APP, "openfga-requires")
async with ops_test.fast_forward():
await ops_test.model.wait_for_idle(
apps=["openfga-requires"],
Expand All @@ -62,3 +93,43 @@ async def test_build_and_deploy(ops_test: OpsTest, charm: str, test_charm: str)

openfga_requires_unit = ops_test.model.applications["openfga-requires"].units[0]
assert "running with store" in openfga_requires_unit.workload_status_message


async def test_has_http_ingress(ops_test: OpsTest) -> None:
# Get the traefik address and try to reach openfga
http_address = await get_unit_address(ops_test, TRAEFIK_HTTP_APP, 0)

resp = requests.get(f"http://{http_address}/{ops_test.model.name}-{OPENFGA_APP}/stores")

assert resp.status_code == 401
assert resp.json()["code"] == "bearer_token_missing"


async def test_scale_up(ops_test: OpsTest) -> None:
"""Check that openfga works after it is scaled up."""
app = ops_test.model.applications[OPENFGA_APP]

await app.scale(3)

await ops_test.model.wait_for_idle(
apps=[OPENFGA_APP],
status="active",
raise_on_blocked=True,
timeout=1000,
wait_for_exact_units=3,
)


async def test_scale_down(ops_test: OpsTest) -> None:
"""Check that openfga works after it is scaled up."""
app = ops_test.model.applications[OPENFGA_APP]

await app.scale(3)

await ops_test.model.wait_for_idle(
apps=[OPENFGA_APP],
status="active",
raise_on_blocked=True,
timeout=1000,
wait_for_exact_units=3,
)
5 changes: 5 additions & 0 deletions tests/integration/test_upgrade.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@
APP_NAME = "openfga"


# TODO(nsklikas): Remove skip mark
@pytest.mark.skip(
reason="charm lib update is a breaking change, the requirer charm can't "
"integrate with previous openfga version"
)
@pytest.mark.abort_on_fail
async def test_upgrade_running_application(ops_test: OpsTest, charm: str, test_charm: str) -> None:
"""Deploy latest published charm and upgrade it with charm-under-test.
Expand Down
70 changes: 54 additions & 16 deletions tests/unit/test_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#
# Learn more about testing at: https://juju.is/docs/sdk/testing

import json
import logging
from unittest.mock import MagicMock

Expand Down Expand Up @@ -33,6 +34,17 @@ def setup_postgres_relation(harness: Harness) -> int:
return db_relation_id


def setup_ingress_relation(harness: Harness, type: str) -> int:
relation_id = harness.add_relation(f"{type}-ingress", f"{type}-traefik")
harness.add_relation_unit(relation_id, f"{type}-traefik/0")
harness.update_relation_data(
relation_id,
f"{type}-traefik",
{"ingress": json.dumps({"url": f"http://{type}:80/{harness.model.name}-openfga"})},
)
return relation_id


def setup_peer_relation(harness: Harness) -> None:
rel_id = harness.add_relation("peer", "openfga")
harness.add_relation_unit(rel_id, "openfga-k8s/1")
Expand Down Expand Up @@ -62,7 +74,7 @@ def test_on_config_changed(
"override": "merge",
"startup": "disabled",
"summary": "OpenFGA",
"command": f"sh -c 'openfga run 2>&1 | tee -a {LOG_FILE}'",
"command": f"sh -c 'openfga run --log-format json --log-level debug 2>&1 | tee -a {LOG_FILE}'",
"environment": {
"OPENFGA_AUTHN_METHOD": "preshared",
"OPENFGA_AUTHN_PRESHARED_KEYS": mocked_token_urlsafe.return_value,
Expand All @@ -81,9 +93,10 @@ def test_on_openfga_relation_joined(
mocked_token_urlsafe: MagicMock,
mocked_migration_is_needed: MagicMock,
mocked_dsn: MagicMock,
mocked_get_address: MagicMock,
mocked_create_openfga_store: MagicMock,
) -> None:
ip = "10.0.0.1"
harness.add_network(ip)
harness.container_pebble_ready("openfga")
setup_peer_relation(harness)
setup_postgres_relation(harness)
Expand All @@ -101,26 +114,56 @@ def test_on_openfga_relation_joined(
mocked_token_urlsafe.return_value, "test-store-name"
)
relation_data = harness.get_relation_data(rel_id, "openfga-k8s")
assert relation_data["address"] == mocked_get_address.return_value
assert relation_data["port"] == "8080"
assert relation_data["scheme"] == "http"
assert relation_data["grpc_api_url"] == f"http://{ip}:8081"
assert relation_data["http_api_url"] == f"http://{ip}:8080"
assert relation_data["token"] == mocked_token_urlsafe.return_value
assert relation_data["store_id"] == mocked_create_openfga_store.return_value
assert (
relation_data["dns_name"]
== "openfga-k8s-0.openfga-k8s-endpoints.openfga-model.svc.cluster.local"


def test_on_openfga_relation_joined_with_ingress(
harness: Harness,
mocked_token_urlsafe: MagicMock,
mocked_migration_is_needed: MagicMock,
mocked_dsn: MagicMock,
mocked_create_openfga_store: MagicMock,
) -> None:
ip = "10.0.0.1"
harness.add_network(ip)
harness.container_pebble_ready("openfga")
setup_peer_relation(harness)
setup_postgres_relation(harness)
setup_ingress_relation(harness, "grpc")
setup_ingress_relation(harness, "http")

rel_id = harness.add_relation("openfga", "openfga-client")
harness.add_relation_unit(rel_id, "openfga-client/0")

harness.update_relation_data(
rel_id,
"openfga-client",
{"store_name": "test-store-name"},
)

mocked_create_openfga_store.assert_called_with(
mocked_token_urlsafe.return_value, "test-store-name"
)
relation_data = harness.get_relation_data(rel_id, "openfga-k8s")
assert relation_data["grpc_api_url"] == f"http://{ip}:8081"
assert relation_data["http_api_url"] == "http://http:80/openfga-model-openfga"
assert relation_data["token"] == mocked_token_urlsafe.return_value
assert relation_data["store_id"] == mocked_create_openfga_store.return_value


def test_on_openfga_relation_joined_with_secrets(
harness: Harness,
mocked_token_urlsafe: MagicMock,
mocked_migration_is_needed: MagicMock,
mocked_dsn: MagicMock,
mocked_get_address: MagicMock,
mocked_create_openfga_store: MagicMock,
mocked_juju_version: MagicMock,
) -> None:
ip = "10.0.0.1"
harness.add_network(ip)
harness.container_pebble_ready("openfga")
setup_peer_relation(harness)
setup_postgres_relation(harness)
Expand All @@ -138,12 +181,7 @@ def test_on_openfga_relation_joined_with_secrets(
mocked_token_urlsafe.return_value, "test-store-name"
)
relation_data = harness.get_relation_data(rel_id, "openfga-k8s")
assert relation_data["address"] == mocked_get_address.return_value
assert relation_data["port"] == "8080"
assert relation_data["scheme"] == "http"
assert relation_data["grpc_api_url"] == f"http://{ip}:8081"
assert relation_data["http_api_url"] == f"http://{ip}:8080"
assert relation_data["token_secret_id"].startswith("secret:")
assert relation_data["store_id"] == mocked_create_openfga_store.return_value
assert (
relation_data["dns_name"]
== "openfga-k8s-0.openfga-k8s-endpoints.openfga-model.svc.cluster.local"
)

0 comments on commit f5ee976

Please sign in to comment.