From e42aed79ce3e17af61944712ffffced8796a0b54 Mon Sep 17 00:00:00 2001 From: alesstimec Date: Fri, 23 Jun 2023 11:40:24 +0200 Subject: [PATCH] Updates to conform to the openfga relation interface. - uses secrets to transfer tokens via relations - updated to use the new state implementation --- .github/workflows/charm.yaml | 1 + charms/openfga-k8s/README.md | 2 +- .../lib/charms/openfga_k8s/v0/openfga.py | 15 +- charms/openfga-k8s/metadata.yaml | 2 +- charms/openfga-k8s/requirements.txt | 2 +- charms/openfga-k8s/src/charm.py | 535 +++++++----------- charms/openfga-k8s/src/state.py | 148 ++--- .../lib/charms/openfga_k8s/v0/openfga.py | 26 +- .../charms/openfga_requires/src/charm.py | 64 +-- .../charms/openfga_requires/src/state.py | 66 +++ .../tests/integration/test_charm.py | 18 +- .../tests/integration/test_upgrade.py | 26 +- charms/openfga-k8s/tests/unit/test_charm.py | 153 +++-- 13 files changed, 488 insertions(+), 570 deletions(-) create mode 100644 charms/openfga-k8s/tests/charms/openfga_requires/src/state.py diff --git a/.github/workflows/charm.yaml b/.github/workflows/charm.yaml index 417c3ec..4b55004 100644 --- a/.github/workflows/charm.yaml +++ b/.github/workflows/charm.yaml @@ -62,6 +62,7 @@ jobs: with: provider: microk8s microk8s-addons: "storage dns rbac registry" + juju-channel: 3.2/stable - name: Create local OCI images run: cd .. && make push-images-microk8s - name: Run integration tests diff --git a/charms/openfga-k8s/README.md b/charms/openfga-k8s/README.md index f8f2905..6fe51c9 100644 --- a/charms/openfga-k8s/README.md +++ b/charms/openfga-k8s/README.md @@ -6,7 +6,7 @@ This repository contains a [Juju Charm](https://charmhub.io/openfga-k8s) for dep ## Usage -Bootstrap a [microk8s controller](https://juju.is/docs/olm/microk8s) using juju `3.1` and create a new Juju model: +Bootstrap a [microk8s controller](https://juju.is/docs/olm/microk8s) using juju `3.2` and create a new Juju model: ```shell juju add-model openfga diff --git a/charms/openfga-k8s/lib/charms/openfga_k8s/v0/openfga.py b/charms/openfga-k8s/lib/charms/openfga_k8s/v0/openfga.py index f721896..8160715 100644 --- a/charms/openfga-k8s/lib/charms/openfga_k8s/v0/openfga.py +++ b/charms/openfga-k8s/lib/charms/openfga_k8s/v0/openfga.py @@ -49,6 +49,11 @@ def _on_openfga_store_created(self, event: OpenFGAStoreCreateEvent): logger.info("address {}".format(event.address)) logger.info("port {}".format(event.port)) logger.info("scheme {}".format(event.scheme)) + + if event.token_secret_id: + secret = self.model.get_secret(id=event.token_secret_id) + content = secret.get_content() + # and get the token with content["token"] ``` """ @@ -71,7 +76,7 @@ def _on_openfga_store_created(self, event: OpenFGAStoreCreateEvent): # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 2 +LIBPATCH = 3 logger = logging.getLogger(__name__) @@ -86,8 +91,8 @@ def store_id(self): return self.relation.data[self.relation.app].get("store_id") @property - def token(self): - return self.relation.data[self.relation.app].get("token") + def token_secret_id(self): + return self.relation.data[self.relation.app].get("token_secret_id") @property def address(self): @@ -149,5 +154,7 @@ def _on_relation_changed(self, event: RelationChangedEvent): """Handle the relation-changed event.""" if self.model.unit.is_leader(): self.on.openfga_store_created.emit( - event.relation, app=event.app, unit=event.unit + event.relation, + app=event.app, + unit=event.unit, ) diff --git a/charms/openfga-k8s/metadata.yaml b/charms/openfga-k8s/metadata.yaml index c4f7703..07c52bf 100644 --- a/charms/openfga-k8s/metadata.yaml +++ b/charms/openfga-k8s/metadata.yaml @@ -37,7 +37,7 @@ description: | Community section for third-party SDKs and tools. peers: - openfga-peer: + peer: interface: openfga-peer provides: diff --git a/charms/openfga-k8s/requirements.txt b/charms/openfga-k8s/requirements.txt index c85064e..9b2a4db 100644 --- a/charms/openfga-k8s/requirements.txt +++ b/charms/openfga-k8s/requirements.txt @@ -1,4 +1,4 @@ -ops >= 1.5.2 +ops >= 2.3.0 requests >= 2.25.1 jsonschema >= 3.2.0 cryptography >= 3.4.8 diff --git a/charms/openfga-k8s/src/charm.py b/charms/openfga-k8s/src/charm.py index 4941cac..ad6c806 100755 --- a/charms/openfga-k8s/src/charm.py +++ b/charms/openfga-k8s/src/charm.py @@ -15,19 +15,15 @@ # along with this program. If not, see . +import functools import logging import secrets import requests -from charms.data_platform_libs.v0.database_requires import ( - DatabaseEvent, - DatabaseRequires, -) +from charms.data_platform_libs.v0.database_requires import DatabaseEvent, DatabaseRequires 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.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch from charms.prometheus_k8s.v0.prometheus_scrape import MetricsEndpointProvider from charms.tls_certificates_interface.v1.tls_certificates import ( CertificateAvailableEvent, @@ -48,7 +44,8 @@ from ops.model import ActiveStatus, BlockedStatus, Relation, WaitingStatus from ops.pebble import ExecError from requests.models import Response -from state import PeerRelationState, RelationNotReadyError + +from state import State, requires_state logger = logging.getLogger(__name__) @@ -56,15 +53,10 @@ REQUIRED_SETTINGS = ["OPENFGA_DATASTORE_URI"] -STATE_KEY_CA = "ca" -STATE_KEY_CERTIFICATE = "certificate" -STATE_KEY_CHAIN = "chain" -STATE_KEY_CSR = "csr" -STATE_KEY_DB_URI = "db-uri" -STATE_KEY_DNS_NAME = "dns-name" -STATE_KEY_PRIVATE_KEY = "private-key" -STATE_KEY_SCHEMA_CREATED = "schema-created" -STATE_KEY_TOKEN = "openfga-token" +LOG_FILE = "/var/log/openfga-k8s" +LOGROTATE_CONFIG_PATH = "/etc/logrotate.d/openfga" + +OPENFGA_SERVER_PORT = 8080 LOG_FILE = "/var/log/openfga-k8s" LOGROTATE_CONFIG_PATH = "/etc/logrotate.d/openfga" @@ -72,24 +64,35 @@ OPENFGA_SERVER_PORT = 8080 +def must_be_leader(func): + @functools.wraps(func) + def wrapper(ref, event): + if ref.unit.is_leader(): + return func(ref, event) + else: + return + + return wrapper + + class OpenFGAOperatorCharm(CharmBase): """OpenFGA Operator Charm.""" def __init__(self, *args): super().__init__(*args) - self.framework.observe( - self.on.openfga_pebble_ready, self._on_openfga_pebble_ready - ) + + self._state = State(self.app, lambda: self.model.get_relation("peer")) + + self.framework.observe(self.on.openfga_pebble_ready, self._on_openfga_pebble_ready) self.framework.observe(self.on.config_changed, self._on_config_changed) self.framework.observe(self.on.update_status, self._on_update_status) self.framework.observe(self.on.leader_elected, self._on_leader_elected) self.framework.observe(self.on.start, self._on_start) self.framework.observe(self.on.stop, self._on_stop) + self.framework.observe(self.on.peer_relation_changed, self._on_peer_relation_changed) # Actions - self.framework.observe( - self.on.schema_upgrade_action, self._on_schema_upgrade_action - ) + self.framework.observe(self.on.schema_upgrade_action, self._on_schema_upgrade_action) # Grafana dashboard relation self._grafana_dashboards = GrafanaDashboardProvider( @@ -108,17 +111,13 @@ def __init__(self, *args): # Prometheus metrics endpoint relation self.metrics_endpoint = MetricsEndpointProvider( self, - jobs=[ - {"static_configs": [{"targets": [f"*:{OPENFGA_SERVER_PORT}"]}]} - ], + jobs=[{"static_configs": [{"targets": [f"*:{OPENFGA_SERVER_PORT}"]}]}], refresh_event=self.on.config_changed, relation_name="metrics-endpoint", ) # OpenFGA relation - self.framework.observe( - self.on.openfga_relation_changed, self._on_openfga_relation_changed - ) + self.framework.observe(self.on.openfga_relation_changed, self._on_openfga_relation_changed) # Certificates relation self.certificates = TLSCertificatesRequiresV1(self, "certificates") @@ -140,13 +139,9 @@ def __init__(self, *args): ) # Ingress relation - self.ingress = IngressPerAppRequirer( - self, relation_name="ingress", port=8080 - ) + self.ingress = IngressPerAppRequirer(self, relation_name="ingress", port=8080) self.framework.observe(self.ingress.on.ready, self._on_ingress_ready) - self.framework.observe( - self.ingress.on.revoked, self._on_ingress_revoked - ) + self.framework.observe(self.ingress.on.revoked, self._on_ingress_revoked) # Database relation self.database = DatabaseRequires( @@ -154,28 +149,16 @@ def __init__(self, *args): relation_name="database", database_name="openfga", ) - self.framework.observe( - self.database.on.database_created, self._on_database_event - ) + self.framework.observe(self.database.on.database_created, self._on_database_event) self.framework.observe( self.database.on.endpoints_changed, self._on_database_event, ) - self.framework.observe( - self.on.database_relation_broken, self._on_database_relation_broken - ) - - port_http = ServicePort( - 8080, name=f"{self.app.name}-http", protocol="TCP" - ) - port_grpc = ServicePort( - 8081, name=f"{self.app.name}-grpc", protocol="TCP" - ) - self.service_patcher = KubernetesServicePatch( - self, [port_http, port_grpc] - ) + self.framework.observe(self.on.database_relation_broken, self._on_database_relation_broken) - self.state = PeerRelationState(self.model, self.app, "openfga-peer") + port_http = ServicePort(8080, name=f"{self.app.name}-http", protocol="TCP") + port_grpc = ServicePort(8081, name=f"{self.app.name}-grpc", protocol="TCP") + self.service_patcher = KubernetesServicePatch(self, [port_http, port_grpc]) def _on_openfga_pebble_ready(self, event): """Workload pebble ready.""" @@ -200,35 +183,37 @@ def _on_update_status(self, _): """Update the status of the charm.""" self._ready() + @requires_state + def _create_token_secret(self, event): + if not self._state.token_secret_id: + token = secrets.token_urlsafe(32) + content = {"token": token} + secret = self.app.add_secret(content) + self._state.token_secret_id = secret.id + logger.info("created token secret {}".format(secret.id)) + + @must_be_leader + @requires_state def _on_leader_elected(self, event): """Leader elected.""" - if self.unit.is_leader(): - try: - # generate token if one is not present in the application - # data bucket of the peer relation - if not self.state.get(STATE_KEY_TOKEN): - token = secrets.token_urlsafe(32) - self.state.set(STATE_KEY_TOKEN, token) - - # generate the private key if one is not present in the - # application data bucket of the peer relation - if not self.state.get(STATE_KEY_PRIVATE_KEY): - private_key: bytes = generate_private_key(key_size=4096) - self.state.set(STATE_KEY_PRIVATE_KEY, private_key.decode()) - - except RelationNotReadyError: - event.defer() - return + + self._create_token_secret(event) + + # generate the private key if one is not present in the + # application data bucket of the peer relation + if not self._state.private_key: + private_key: bytes = generate_private_key(key_size=4096) + self._state.private_key = private_key.decode() + self._update_workload(event) + @requires_state def _update_workload(self, event): """' Update workload with all available configuration data.""" # Quickly update logrotates config each workload update - self._push_to_workload( - LOGROTATE_CONFIG_PATH, self._get_logrotate_config(), event - ) + self._push_to_workload(LOGROTATE_CONFIG_PATH, self._get_logrotate_config(), event) container = self.unit.get_container(WORKLOAD_CONTAINER) # make sure we can connect to the container @@ -237,31 +222,28 @@ def _update_workload(self, event): event.defer() return + self._create_token_secret(event) + + # Quickly update logrotates config each workload update + self._push_to_workload(LOGROTATE_CONFIG_PATH, self._get_logrotate_config(), event) + dnsname = "{}.{}-endpoints.{}.svc.cluster.local".format( self.unit.name.replace("/", "-"), self.app.name, self.model.name ) - try: - if self.state.get(STATE_KEY_DNS_NAME): - dnsname = self.state.get(STATE_KEY_DNS_NAME) + if self._state.dns_name: + dnsname = self._state.dns_name - # check if the database connection string has been - # recorded in the peer relation's application data bucket - if not self.state.get(STATE_KEY_DB_URI): - logger.info("waiting for postgresql relation") - self.unit.status = BlockedStatus( - "Waiting for postgresql relation" - ) - return + # check if the database connection string has been + # recorded in the peer relation's application data bucket + if not self._state.db_uri: + logger.info("waiting for postgresql relation") + self.unit.status = BlockedStatus("Waiting for postgresql relation") + return - # check if the schema has been upgraded - if not self.state.get(STATE_KEY_SCHEMA_CREATED): - logger.info("waiting for schema upgrade") - self.unit.status = BlockedStatus( - "Please run schema-upgrade action" - ) - return - except RelationNotReadyError: - event.defer() + # check if the schema has been upgraded + if not self._state.schema_created: + logger.info("waiting for schema upgrade") + self.unit.status = BlockedStatus("Please run schema-upgrade action") return # if openfga relation exists, make sure the address is @@ -269,49 +251,33 @@ def _update_workload(self, event): if self.unit.is_leader(): openfga_relation = self.model.get_relation("openfga") if openfga_relation and self.app in openfga_relation.data: - old_address = openfga_relation.data[self.app].get( - "address", "" - ) + old_address = openfga_relation.data[self.app].get("address", "") new_address = self._get_address(openfga_relation) if old_address != new_address: - openfga_relation.data[self.app].update( - {"address": new_address} - ) + openfga_relation.data[self.app].update({"address": new_address}) old_dns = openfga_relation.data[self.app].get("dns-name") if old_dns != dnsname: - openfga_relation.data[self.app].update( - {"dns-name": dnsname} - ) + openfga_relation.data[self.app].update({"dns-name": dnsname}) env_vars = map_config_to_env_vars(self) env_vars["OPENFGA_PLAYGROUND_ENABLED"] = "false" env_vars["OPENFGA_DATASTORE_ENGINE"] = "postgres" - env_vars["OPENFGA_DATASTORE_URI"] = self.state.get(STATE_KEY_DB_URI) - - token = "" - try: - token = self.state.get(STATE_KEY_TOKEN) - if token: - env_vars["OPENFGA_AUTHN_METHOD"] = "preshared" - env_vars["OPENFGA_AUTHN_PRESHARED_KEYS"] = self.state.get( - STATE_KEY_TOKEN - ) - - certificate = self.state.get(STATE_KEY_CERTIFICATE) - key = self.state.get(STATE_KEY_PRIVATE_KEY) - if certificate and key: - container.push( - "/app/certificate.pem", certificate, make_dirs=True - ) - container.push("/app/key.pem", key, make_dirs=True) - env_vars["OPENFGA_HTTP_TLS_ENABLED"] = "true" - env_vars["OPENFGA_HTTP_TLS_CERT"] = "/app/certificate.pem" - env_vars["OPENFGA_HTTP_TLS_KEY"] = "/app/key.pem" - env_vars["OPENFGA_GRPC_TLS_ENABLED"] = "true" - env_vars["OPENFGA_GRPC_TLS_CERT"] = "/app/certificate.pem" - env_vars["OPENFGA_GRPC_TLS_KEY"] = "/app/key.pem" - except RelationNotReadyError: - logger.info("could not information from the peer relation state") + env_vars["OPENFGA_DATASTORE_URI"] = self._state.db_uri + + secret = self.model.get_secret(id=self._state.token_secret_id) + secret_content = secret.get_content() + env_vars["OPENFGA_AUTHN_METHOD"] = "preshared" + env_vars["OPENFGA_AUTHN_PRESHARED_KEYS"] = secret_content["token"] + + if self._state.certificate and self._state.private_key: + container.push("/app/certificate.pem", self._state.certificate, make_dirs=True) + container.push("/app/key.pem", self._state.private_key, make_dirs=True) + env_vars["OPENFGA_HTTP_TLS_ENABLED"] = "true" + env_vars["OPENFGA_HTTP_TLS_CERT"] = "/app/certificate.pem" + env_vars["OPENFGA_HTTP_TLS_KEY"] = "/app/key.pem" + env_vars["OPENFGA_GRPC_TLS_ENABLED"] = "true" + env_vars["OPENFGA_GRPC_TLS_CERT"] = "/app/certificate.pem" + env_vars["OPENFGA_GRPC_TLS_KEY"] = "/app/key.pem" env_vars = {key: value for key, value in env_vars.items() if value} for setting in REQUIRED_SETTINGS: @@ -355,46 +321,50 @@ def _update_workload(self, event): event.defer() return + def _on_peer_relation_changed(self, event): + self._update_workload(event) + + @must_be_leader + @requires_state def _on_database_event(self, event: DatabaseEvent) -> None: """Database event handler.""" + # get the first endpoint from a comma separate list ep = event.endpoints.split(",", 1)[0] # compose the db connection string uri = f"postgresql://{event.username}:{event.password}@{ep}/openfga" # record the connection string - try: - self.state.set(STATE_KEY_DB_URI, uri) - except RelationNotReadyError: - event.defer() - return + self._state.db_uri = uri self._update_workload(event) + @must_be_leader + @requires_state def _on_database_relation_broken(self, event: DatabaseEvent) -> None: """Database relation broken handler.""" + # when the database relation is broken, we unset the # connection string and schema-created from the application # bucket of the peer relation - try: - self.state.unset(STATE_KEY_DB_URI, STATE_KEY_SCHEMA_CREATED) - except RelationNotReadyError: - event.defer() - return + del self._state.db_uri + del self._state.schema_created + self._update_workload(event) - def _ready(self): + def _ready(self) -> bool: container = self.unit.get_container(WORKLOAD_CONTAINER) - if not self.state.get(STATE_KEY_DB_URI): + if not self._state.is_ready(): + return False + + if not self._state.db_uri: self.unit.status = BlockedStatus("Waiting for postgresql relation") - return + return False - if not self.state.get(STATE_KEY_SCHEMA_CREATED): - self.unit.status = BlockedStatus( - "Please run schema-upgrade action" - ) - return + if not self._state.schema_created: + self.unit.status = BlockedStatus("Please run schema-upgrade action") + return False if container.can_connect(): plan = container.get_plan() @@ -417,19 +387,13 @@ def _ready(self): return True else: logger.debug("cannot connect to workload container") - self.unit.status = WaitingStatus( - "waiting for the OpenFGA workload" - ) + self.unit.status = WaitingStatus("waiting for the OpenFGA workload") return False + @must_be_leader + @requires_state def _on_openfga_relation_changed(self, event: RelationChangedEvent): """OpenFGA relation changed.""" - if not self.unit.is_leader(): - return - - if not self._ready(): - event.defer() - return # the requires side will put the store_name in its # application bucket @@ -437,26 +401,23 @@ def _on_openfga_relation_changed(self, event: RelationChangedEvent): if not store_name: return - token = "" dnsname = "{}.{}-endpoints.{}.svc.cluster.local".format( self.unit.name.replace("/", "-"), self.app.name, self.model.name ) - store_id = "" - try: - t = self.state.get(STATE_KEY_TOKEN) - if t: - token = t + if self._state.dns_name: + dnsname = self._state.dns_name - dns = self.state.get(STATE_KEY_DNS_NAME) - if dns: - dnsname = dns - - logger.info("creating store {}".format(store_name)) - store_id = self._create_openfga_store(store_name) - except RelationNotReadyError: + if not self._state.token_secret_id: event.defer() + logger.info("token secret not created yet") return + secret = self.model.get_secret(id=self._state.token_secret_id) + secret.grant(event.relation) + + logger.info("creating store {}".format(store_name)) + store_id = self._create_openfga_store(store_name) + if not store_id: logger.error("failed to create the openfga store") return @@ -465,7 +426,7 @@ def _on_openfga_relation_changed(self, event: RelationChangedEvent): # to connect to OpenFga data = { "store_id": store_id, - "token": token, + "token_secret_id": secret.id, "address": self._get_address(event.relation), "scheme": "http", "port": "8080", @@ -478,9 +439,7 @@ def _on_openfga_relation_changed(self, event: RelationChangedEvent): def _get_address(self, relation: Relation): """Returns the ip address to be used with the specified relation.""" - return self.model.get_binding( - relation - ).network.ingress_address.exploded + return self.model.get_binding(relation).network.ingress_address.exploded def _create_openfga_store(self, store_name: str): logger.info("creating store: {}".format(store_name)) @@ -489,7 +448,9 @@ def _create_openfga_store(self, store_name: str): headers = {} - token = self.state.get(STATE_KEY_TOKEN) + secret = self.model.get_secret(id=self._state.token_secret_id) + secret_content = secret.get_content() + token = secret_content.get("token", "") if token: headers = {"Authorization": "Bearer {}".format(token)} @@ -528,9 +489,7 @@ def _create_openfga_store(self, store_name: str): ) return "" - def _list_stores( - self, openfga_host: str, headers, continuation_token="" - ) -> list: + def _list_stores(self, openfga_host: str, headers, continuation_token="") -> list: # to list stores we need to issue a GET request to the /stores # endpoint response: Response = requests.get( @@ -539,9 +498,7 @@ def _list_stores( verify=False, ) if response.status_code != 200: - logger.error( - "to list existing openfga store: {}".format(response.json()) - ) + logger.error("to list existing openfga store: {}".format(response.json())) return None data = response.json() @@ -564,28 +521,19 @@ def _list_stores( ) ) + @must_be_leader + @requires_state def _on_schema_upgrade_action(self, event): """Performs a schema upgrade on the configurable database""" - if not self.unit.is_leader(): - event.set_results({"error": "unit is not the leader"}) - return - - db_uri = "" - try: - db_uri = self.state.get(STATE_KEY_DB_URI) - except RelationNotReadyError: - event.defer() - return + db_uri = self._state.db_uri if not db_uri: event.set_results({"error": "missing postgres relation"}) return container = self.unit.get_container(WORKLOAD_CONTAINER) if not container.can_connect(): - event.set_results( - {"error": "cannot connect to the workload container"} - ) + event.set_results({"error": "cannot connect to the workload container"}) return migration_process = container.exec( @@ -606,168 +554,114 @@ def _on_schema_upgrade_action(self, event): event.set_results({"result": "done"}) except ExecError as e: if "already exists" in e.stderr: - logger.info( - "schema migration failed because the schema already exists" - ) + logger.info("schema migration failed because the schema already exists") self.unit.status = WaitingStatus("Schema migration done") event.set_results({"result": "done"}) else: logger.error( - "failed to run schema migration: err {} db_uri {}".format( - e.stderr, db_uri - ) + "failed to run schema migration: err {} db_uri {}".format(e.stderr, db_uri) ) - event.set_results( - {"std-err": e.stderr, "std-out": stdout, "db_uri": db_uri} - ) - - try: - self.state.set(STATE_KEY_SCHEMA_CREATED, "true") - except RelationNotReadyError: - event.defer() - return + event.set_results({"std-err": e.stderr, "std-out": stdout, "db_uri": db_uri}) + self._state.schema_created = "true" logger.info("schema upgraded") self._update_workload(event) - def _on_certificates_relation_joined( - self, event: RelationJoinedEvent - ) -> None: - if not self.unit.is_leader(): - return - - peer_relation = self.model.get_relation("openfga-peer") - if not peer_relation: - self.unit.status = WaitingStatus( - "Waiting for peer relation to be created" - ) - event.defer() - return - + @must_be_leader + @requires_state + def _on_certificates_relation_joined(self, event: RelationJoinedEvent) -> None: dnsname = "{}.{}-endpoints.{}.svc.cluster.local".format( self.unit.name.replace("/", "-"), self.app.name, self.model.name ) - try: - if self.state.get(STATE_KEY_DNS_NAME): - dnsname = self.state.get(STATE_KEY_DNS_NAME) - private_key = self.state.get(STATE_KEY_PRIVATE_KEY) - csr = generate_csr( - private_key=private_key.encode(), - subject=dnsname, - ) - self.state.set(STATE_KEY_CSR, csr.decode()) - except RelationNotReadyError: - event.defer() - return + if self._state.dns_name: + dnsname = self._state.dns_name - self.certificates.request_certificate_creation( - certificate_signing_request=csr + private_key = self._state.private_key + csr = generate_csr( + private_key=private_key.encode(), + subject=dnsname, ) + self._state.csr = csr.decode() + + self.certificates.request_certificate_creation(certificate_signing_request=csr) + + @must_be_leader + @requires_state + def _on_certificate_available(self, event: CertificateAvailableEvent) -> None: + self._state.certificate = event.certificate + self._state.ca = event.ca + self._state.key_chain = event.chain - def _on_certificate_available( - self, event: CertificateAvailableEvent - ) -> None: - if not self.unit.is_leader(): - try: - self.state.set(STATE_KEY_CERTIFICATE, event.certificate) - self.state.set(STATE_KEY_CA, event.ca) - self.state.set(STATE_KEY_CHAIN, event.chain) - except RelationNotReadyError: - event.defer() - return self._update_workload(event) - def _on_certificate_expiring( - self, event: CertificateExpiringEvent - ) -> None: - if not self.unit.is_leader(): - old_csr = "" - private_key = "" - try: - old_csr = self.state.get(STATE_KEY_CSR) - private_key = self.state.get(STATE_KEY_PRIVATE_KEY) - except RelationNotReadyError: - event.defer() - return + @must_be_leader + @requires_state + def _on_certificate_expiring(self, event: CertificateExpiringEvent) -> None: + old_csr = self._state.csr + private_key = self._state.private_key - dnsname = "{}.{}-endpoints.{}.svc.cluster.local".format( - self.unit.name.replace("/", "-"), - self.app.name, - self.model.name, - ) - if self.state.get(STATE_KEY_DNS_NAME): - dnsname = self.state.get(STATE_KEY_DNS_NAME) + dnsname = "{}.{}-endpoints.{}.svc.cluster.local".format( + self.unit.name.replace("/", "-"), + self.app.name, + self.model.name, + ) + if self._state.dns_name: + dnsname = self._state.dns_name - new_csr = generate_csr( - private_key=private_key.encode(), subject=dnsname - ) - self.certificates.request_certificate_renewal( - old_certificate_signing_request=old_csr, - new_certificate_signing_request=new_csr, - ) - try: - self.state.set(STATE_KEY_CSR, new_csr.decode()) - except RelationNotReadyError: - event.defer() - return + new_csr = generate_csr(private_key=private_key.encode(), subject=dnsname) + self.certificates.request_certificate_renewal( + old_certificate_signing_request=old_csr, + new_certificate_signing_request=new_csr, + ) + + self._state.csr = new_csr.decode() self._update_workload() + @must_be_leader + @requires_state def _on_certificate_revoked(self, event: CertificateRevokedEvent) -> None: - if not self.unit.is_leader(): - old_csr = "" - private_key = "" - try: - old_csr = self.state.get(STATE_KEY_CSR) - private_key = self.state.get(STATE_KEY_PRIVATE_KEY) - - dnsname = "{}.{}-endpoints.{}.svc.cluster.local".format( - self.unit.name.replace("/", "-"), - self.app.name, - self.model.name, - ) - if self.state.get(STATE_KEY_DNS_NAME): - dnsname = self.state.get(STATE_KEY_DNS_NAME) + old_csr = self._state.csr + private_key = self._state.private_key - new_csr = generate_csr( - private_key=private_key.encode(), - subject=dnsname, - ) - self.certificates.request_certificate_renewal( - old_certificate_signing_request=old_csr, - new_certificate_signing_request=new_csr, - ) + dnsname = "{}.{}-endpoints.{}.svc.cluster.local".format( + self.unit.name.replace("/", "-"), + self.app.name, + self.model.name, + ) + if self._state.dns_name: + dnsname = self._state.dns_name - self.state.set(STATE_KEY_CSR, new_csr.decode()) - self.state.unset( - STATE_KEY_CERTIFICATE, STATE_KEY_CA, STATE_KEY_CHAIN - ) - except RelationNotReadyError: - event.defer() - return + new_csr = generate_csr( + private_key=private_key.encode(), + subject=dnsname, + ) + self.certificates.request_certificate_renewal( + old_certificate_signing_request=old_csr, + new_certificate_signing_request=new_csr, + ) + + self._state.csr = new_csr.decode() + del self._state.certificate + del self._state.ca + del self._state.key_chain self.unit.status = WaitingStatus("Waiting for new certificate") self._update_workload() + @must_be_leader + @requires_state def _on_ingress_ready(self, event: IngressPerAppReadyEvent): - if self.unit.is_leader(): - try: - self.state.set(STATE_KEY_DNS_NAME, event.url) - except RelationNotReadyError: - event.defer() - return + self._state.dns_name = event.url self._update_workload(event) + @must_be_leader + @requires_state def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent): - if self.unit.is_leader(): - try: - self.state.unset(STATE_KEY_DNS_NAME) - except RelationNotReadyError: - event.defer() - return + del self._state.dns_name self._update_workload(event) @@ -789,9 +683,7 @@ def _push_to_workload(self, filename, content, event): container = self.unit.get_container(WORKLOAD_CONTAINER) if container.can_connect(): - logger.info( - "pushing file {} to the workload container".format(filename) - ) + logger.info("pushing file {} to the workload container".format(filename)) container.push(filename, content, make_dirs=True) else: logger.info("workload container not ready - defering") @@ -803,8 +695,7 @@ def map_config_to_env_vars(charm, **additional_env): variables such that they can be passed directly to the pebble layer. """ env_mapped_config = { - "OPENFGA_{}".format(k.replace("-", "_").upper()): v - for k, v in charm.config.items() + "OPENFGA_{}".format(k.replace("-", "_").upper()): v for k, v in charm.config.items() } return {**env_mapped_config, **additional_env} diff --git a/charms/openfga-k8s/src/state.py b/charms/openfga-k8s/src/state.py index fad9130..3eb6898 100644 --- a/charms/openfga-k8s/src/state.py +++ b/charms/openfga-k8s/src/state.py @@ -1,73 +1,79 @@ -#!/usr/bin/env python3 -# Copyright 2022 Canonical Ltd. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 3, as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranties of -# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR -# PURPOSE. See the GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -from ops.model import Application, Model, Relation - - -class RelationNotReadyError(Exception): - """Is returned when the peer relation is not ready.""" - - pass - - -class PeerRelationState: - """RelationState uses the peer relation to store the state of the charm.""" - - def __init__( - self, - model: Model, - app: Application, - relation_name: str, - defaults: dict[str:str] = None, - ): - self._model = model - self._app = app - self._relation_name = relation_name - - if defaults: - relation = self._model.get_relation(relation_name) - if not relation: - raise RelationNotReadyError - else: - relation.data[self._app].update(defaults) - - def _get_relation(self) -> Relation: - relation = self._model.get_relation(self._relation_name) - return relation - - def set(self, key: str, value: str) -> None: - """Sets the value for the specified key.""" - relation = self._get_relation() - if not relation: - raise RelationNotReadyError - else: - relation.data[self._app].update({key: value}) +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. - def unset(self, *keys) -> None: - """Unsets the value for the specified key.""" - relation = self._get_relation() - if not relation: - raise RelationNotReadyError - else: - for key in keys: - relation.data[self._app].pop(key, None) - - def get(self, key: str) -> str: - """Returns the value for the specified key.""" - relation = self._get_relation() - if not relation: - return None +"""Manager for handling charm state.""" + +import functools +import json + + +def requires_state(func): + @functools.wraps(func) + def wrapper(self, event): + if self._state.is_ready(): + return func(self, event) else: - return relation.data[self._app].get(key, "") + event.defer() + return + + return wrapper + + +class State: + """A magic state that uses a relation as the data store. + + The get_relation callable is used to retrieve the relation. + As relation data values must be strings, all values are JSON encoded. + """ + + def __init__(self, app, get_relation): + """Construct. + + Args: + app: workload application + get_relation: get peer relation method + """ + # Use __dict__ to avoid calling __setattr__ and subsequent infinite recursion. + self.__dict__["_app"] = app + self.__dict__["_get_relation"] = get_relation + + def __setattr__(self, name, value): + """Set a value in the store with the given name. + + Args: + name: name of value to set in store. + value: value to set in store. + """ + v = json.dumps(value) + self._get_relation().data[self._app].update({name: v}) + + def __getattr__(self, name): + """Get from the store the value with the given name, or None. + + Args: + name: name of value to get from store. + + Returns: + value from store with given name. + """ + v = self._get_relation().data[self._app].get(name, "null") + return json.loads(v) + + def __delattr__(self, name): + """Delete the value with the given name from the store, if it exists. + + Args: + name: name of value to delete from store. + + Returns: + deleted value from store. + """ + return self._get_relation().data[self._app].pop(name, None) + + def is_ready(self): + """Report whether the relation is ready to be used. + + Returns: + A boolean representing whether the relation is ready to be used or not. + """ + return bool(self._get_relation()) diff --git a/charms/openfga-k8s/tests/charms/openfga_requires/lib/charms/openfga_k8s/v0/openfga.py b/charms/openfga-k8s/tests/charms/openfga_requires/lib/charms/openfga_k8s/v0/openfga.py index f721896..abbdafd 100644 --- a/charms/openfga-k8s/tests/charms/openfga_requires/lib/charms/openfga_k8s/v0/openfga.py +++ b/charms/openfga-k8s/tests/charms/openfga_requires/lib/charms/openfga_k8s/v0/openfga.py @@ -49,18 +49,18 @@ def _on_openfga_store_created(self, event: OpenFGAStoreCreateEvent): logger.info("address {}".format(event.address)) logger.info("port {}".format(event.port)) logger.info("scheme {}".format(event.scheme)) + + if event.token_secret_id: + secret = self.model.get_secret(id=event.token_secret_id) + content = secret.get_content() + # and get the token with content["token"] ``` """ import logging -from ops.charm import ( - CharmEvents, - RelationChangedEvent, - RelationEvent, - RelationJoinedEvent, -) +from ops.charm import CharmEvents, RelationChangedEvent, RelationEvent, RelationJoinedEvent from ops.framework import EventSource, Object # The unique Charmhub library identifier, never change it @@ -71,7 +71,7 @@ def _on_openfga_store_created(self, event: OpenFGAStoreCreateEvent): # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 2 +LIBPATCH = 3 logger = logging.getLogger(__name__) @@ -86,8 +86,8 @@ def store_id(self): return self.relation.data[self.relation.app].get("store_id") @property - def token(self): - return self.relation.data[self.relation.app].get("token") + def token_secret_id(self): + return self.relation.data[self.relation.app].get("token_secret_id") @property def address(self): @@ -128,9 +128,7 @@ class OpenFGARequires(Object): def __init__(self, charm, store_name: str): super().__init__(charm, RELATION_NAME) - self.framework.observe( - charm.on[RELATION_NAME].relation_joined, self._on_relation_joined - ) + self.framework.observe(charm.on[RELATION_NAME].relation_joined, self._on_relation_joined) self.framework.observe( charm.on[RELATION_NAME].relation_changed, self._on_relation_changed, @@ -149,5 +147,7 @@ def _on_relation_changed(self, event: RelationChangedEvent): """Handle the relation-changed event.""" if self.model.unit.is_leader(): self.on.openfga_store_created.emit( - event.relation, app=event.app, unit=event.unit + event.relation, + app=event.app, + unit=event.unit, ) diff --git a/charms/openfga-k8s/tests/charms/openfga_requires/src/charm.py b/charms/openfga-k8s/tests/charms/openfga_requires/src/charm.py index 4a03cc6..9a35c75 100755 --- a/charms/openfga-k8s/tests/charms/openfga_requires/src/charm.py +++ b/charms/openfga-k8s/tests/charms/openfga_requires/src/charm.py @@ -14,14 +14,13 @@ import logging -from charms.openfga_k8s.v0.openfga import ( - OpenFGARequires, - OpenFGAStoreCreateEvent, -) +from charms.openfga_k8s.v0.openfga import OpenFGARequires, OpenFGAStoreCreateEvent from ops.charm import CharmBase from ops.main import main from ops.model import ActiveStatus, BlockedStatus, WaitingStatus +from state import State + # Log messages can be retrieved using juju debug-log logger = logging.getLogger(__name__) @@ -34,6 +33,8 @@ class OpenfgaRequiresCharm(CharmBase): def __init__(self, *args): super().__init__(*args) + self._state = State(self.app, lambda: self.model.get_relation("openfga-test-peer")) + self.framework.observe(self.on.start, self._on_update_status) self.framework.observe(self.on.config_changed, self._on_update_status) self.framework.observe(self.on.update_status, self._on_update_status) @@ -44,50 +45,47 @@ def __init__(self, *args): self._on_openfga_store_created, ) - def _on_update_status(self, _): - openfga_relation = self.model.get_relation("openfga-test-peer") - if openfga_relation: - logger.info( - "relation data: {}".format(openfga_relation.data[self.app]) - ) - if "store_id" in openfga_relation.data[self.app]: - self.unit.status = ActiveStatus( - "running with store {}".format( - openfga_relation.data[self.app].get("store_id") - ) - ) - else: - self.unit.status = WaitingStatus( - "waiting for store information" + def _on_update_status(self, event): + if not self._state.is_ready(): + event.defer() + return + + if self._state.store_id: + self.unit.status = ActiveStatus( + "running with store {}".format( + self._state.store_id, ) + ) else: - self.unit.status = BlockedStatus("waiting for openfga relation") + self.unit.status = WaitingStatus("waiting for store information") def _on_openfga_store_created(self, event: OpenFGAStoreCreateEvent): if not self.unit.is_leader(): return + if not self._state.is_ready(): + event.defer() + return + if not event.store_id: return logger.info("store id {}".format(event.store_id)) - logger.info("token {}".format(event.token)) + logger.info("token_secret_id {}".format(event.token_secret_id)) logger.info("address {}".format(event.address)) logger.info("port {}".format(event.port)) logger.info("scheme {}".format(event.scheme)) - openfga_relation = self.model.get_relation("openfga-test-peer") - if not openfga_relation: - event.defer() - openfga_relation.data[self.app].update( - { - "store_id": event.store_id, - "token": event.token, - "address": event.address, - "port": event.port, - "scheme": event.scheme, - } - ) + if event.token_secret_id: + secret = self.model.get_secret(id=event.token_secret_id) + content = secret.get_content() + logger.info("secret content {}".format(content)) + + self._state.store_id = event.store_id + self._state.token_secret_id = event.token_secret_id + self._state.address = event.address + self._state.port = event.port + self._state.scheme = event.scheme if __name__ == "__main__": # pragma: nocover diff --git a/charms/openfga-k8s/tests/charms/openfga_requires/src/state.py b/charms/openfga-k8s/tests/charms/openfga_requires/src/state.py new file mode 100644 index 0000000..71a1654 --- /dev/null +++ b/charms/openfga-k8s/tests/charms/openfga_requires/src/state.py @@ -0,0 +1,66 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + +"""Manager for handling charm state.""" + +import json + + +class State: + """A magic state that uses a relation as the data store. + + The get_relation callable is used to retrieve the relation. + As relation data values must be strings, all values are JSON encoded. + """ + + def __init__(self, app, get_relation): + """Construct. + + Args: + app: workload application + get_relation: get peer relation method + """ + # Use __dict__ to avoid calling __setattr__ and subsequent infinite recursion. + self.__dict__["_app"] = app + self.__dict__["_get_relation"] = get_relation + + def __setattr__(self, name, value): + """Set a value in the store with the given name. + + Args: + name: name of value to set in store. + value: value to set in store. + """ + v = json.dumps(value) + self._get_relation().data[self._app].update({name: v}) + + def __getattr__(self, name): + """Get from the store the value with the given name, or None. + + Args: + name: name of value to get from store. + + Returns: + value from store with given name. + """ + v = self._get_relation().data[self._app].get(name, "null") + return json.loads(v) + + def __delattr__(self, name): + """Delete the value with the given name from the store, if it exists. + + Args: + name: name of value to delete from store. + + Returns: + deleted value from store. + """ + return self._get_relation().data[self._app].pop(name, None) + + def is_ready(self): + """Report whether the relation is ready to be used. + + Returns: + A boolean representing whether the relation is ready to be used or not. + """ + return bool(self._get_relation()) diff --git a/charms/openfga-k8s/tests/integration/test_charm.py b/charms/openfga-k8s/tests/integration/test_charm.py index d5c8338..ca3aa8e 100644 --- a/charms/openfga-k8s/tests/integration/test_charm.py +++ b/charms/openfga-k8s/tests/integration/test_charm.py @@ -40,9 +40,7 @@ async def test_build_and_deploy(ops_test: OpsTest): application_name=APP_NAME, series="jammy", ), - ops_test.model.deploy( - "postgresql-k8s", application_name="postgresql", channel="edge" - ), + ops_test.model.deploy("postgresql-k8s", application_name="postgresql", channel="edge"), ops_test.model.deploy( test_charm, application_name="openfga-requires", @@ -62,17 +60,11 @@ async def test_build_and_deploy(ops_test: OpsTest): await ops_test.model.integrate(APP_NAME, "postgresql:database") logger.debug("running schema-upgrade action") - openfga_unit = await utils.get_unit_by_name( - APP_NAME, "0", ops_test.model.units - ) + openfga_unit = await utils.get_unit_by_name(APP_NAME, "0", ops_test.model.units) for i in range(10): action: Action = await openfga_unit.run_action("schema-upgrade") result = await action.wait() - logger.info( - "attempt {} -> action result {} {}".format( - i, result.status, result.results - ) - ) + logger.info("attempt {} -> action result {} {}".format(i, result.status, result.results)) if result.results == {"result": "done", "return-code": 0}: break time.sleep(2) @@ -98,6 +90,4 @@ async def test_build_and_deploy(ops_test: OpsTest): openfga_requires_unit = await utils.get_unit_by_name( "openfga-requires", "0", ops_test.model.units ) - assert ( - "running with store" in openfga_requires_unit.workload_status_message - ) + assert "running with store" in openfga_requires_unit.workload_status_message diff --git a/charms/openfga-k8s/tests/integration/test_upgrade.py b/charms/openfga-k8s/tests/integration/test_upgrade.py index cc1e476..0a66aea 100644 --- a/charms/openfga-k8s/tests/integration/test_upgrade.py +++ b/charms/openfga-k8s/tests/integration/test_upgrade.py @@ -38,9 +38,7 @@ async def test_upgrade_running_application(ops_test: OpsTest): channel="edge", series="jammy", ), - ops_test.model.deploy( - "postgresql-k8s", application_name="postgresql", channel="edge" - ), + ops_test.model.deploy("postgresql-k8s", application_name="postgresql", channel="edge"), ops_test.model.deploy( test_charm, application_name="openfga-requires", @@ -60,17 +58,11 @@ async def test_upgrade_running_application(ops_test: OpsTest): await ops_test.model.integrate(APP_NAME, "postgresql:database") logger.debug("running schema-upgrade action") - openfga_unit = await utils.get_unit_by_name( - APP_NAME, "0", ops_test.model.units - ) + openfga_unit = await utils.get_unit_by_name(APP_NAME, "0", ops_test.model.units) for i in range(10): action: Action = await openfga_unit.run_action("schema-upgrade") result = await action.wait() - logger.info( - "attempt {} -> action result {} {}".format( - i, result.status, result.results - ) - ) + logger.info("attempt {} -> action result {} {}".format(i, result.status, result.results)) if result.results == {"result": "done", "return-code": 0}: break time.sleep(2) @@ -96,9 +88,7 @@ async def test_upgrade_running_application(ops_test: OpsTest): openfga_requires_unit = await utils.get_unit_by_name( "openfga-requires", "0", ops_test.model.units ) - assert ( - "running with store" in openfga_requires_unit.workload_status_message - ) + assert "running with store" in openfga_requires_unit.workload_status_message # Starting upgrade/refresh logger.debug("starting upgrade test") @@ -126,13 +116,9 @@ async def test_upgrade_running_application(ops_test: OpsTest): assert ops_test.model.applications[APP_NAME].status == "active" - upgraded_openfga_unit = await utils.get_unit_by_name( - APP_NAME, "0", ops_test.model.units - ) + upgraded_openfga_unit = await utils.get_unit_by_name(APP_NAME, "0", ops_test.model.units) - health = await upgraded_openfga_unit.run( - "curl -s http://localhost:8080/healthz" - ) + health = await upgraded_openfga_unit.run("curl -s http://localhost:8080/healthz") await health.wait() assert health.results.get("return-code") == 0 assert health.results.get("stdout").strip() == '{"status":"SERVING"}' diff --git a/charms/openfga-k8s/tests/unit/test_charm.py b/charms/openfga-k8s/tests/unit/test_charm.py index d7b102d..e066cad 100644 --- a/charms/openfga-k8s/tests/unit/test_charm.py +++ b/charms/openfga-k8s/tests/unit/test_charm.py @@ -9,19 +9,10 @@ import unittest from unittest.mock import MagicMock, patch -from charm import ( - STATE_KEY_CA, - STATE_KEY_CERTIFICATE, - STATE_KEY_CHAIN, - STATE_KEY_DB_URI, - STATE_KEY_DNS_NAME, - STATE_KEY_PRIVATE_KEY, - STATE_KEY_SCHEMA_CREATED, - STATE_KEY_TOKEN, - OpenFGAOperatorCharm, -) from ops.testing import Harness +from charm import OpenFGAOperatorCharm + logger = logging.getLogger(__name__) LOG_FILE = "/var/log/openfga-k8s" @@ -38,9 +29,7 @@ def setUp(self, *unused): self.tempdir = tempfile.TemporaryDirectory() self.addCleanup(self.tempdir.cleanup) - self.harness.charm.framework.charm_dir = pathlib.Path( - self.tempdir.name - ) + self.harness.charm.framework.charm_dir = pathlib.Path(self.tempdir.name) self.harness.container_pebble_ready("openfga") @@ -48,46 +37,38 @@ def setUp(self, *unused): def test_logrotate_config_pushed(self, get_logrotate_config: MagicMock): self.harness.set_leader(True) - rel_id = self.harness.add_relation("openfga-peer", "openfga") + rel_id = self.harness.add_relation("peer", "openfga") self.harness.add_relation_unit(rel_id, "openfga-k8s/1") - self.harness.update_relation_data( - rel_id, - "openfga-k8s", - { - STATE_KEY_TOKEN: "test-token", - STATE_KEY_SCHEMA_CREATED: "true", - STATE_KEY_DB_URI: "test-db-uri", - STATE_KEY_PRIVATE_KEY: "test-key", - STATE_KEY_CERTIFICATE: "test-cert", - STATE_KEY_CA: "test-ca", - STATE_KEY_CHAIN: "test-chain", - STATE_KEY_DNS_NAME: "test-dns-name", - }, - ) + self.harness.charm._state.token = "test-token" + self.harness.charm._state.schema_created = "true" + self.harness.charm._state.db_uri = "test-db-uri" + self.harness.charm._state.private_key = "test-key" + self.harness.charm._state.certificate = "test-cert" + self.harness.charm._state.ca = "test-ca" + self.harness.charm._state.key_chain = "test-chain" + self.harness.charm._state.dns_name = "test-dns-name" container = self.harness.model.unit.get_container("openfga") self.harness.charm.on.openfga_pebble_ready.emit(container) - get_logrotate_config.assert_called_once() + get_logrotate_config.assert_called() + + @patch("secrets.token_urlsafe") + def test_on_config_changed(self, token_urlsafe): + token_urlsafe.return_value = "a_test_secret" - def test_on_config_changed(self): self.harness.set_leader(True) - rel_id = self.harness.add_relation("openfga-peer", "openfga") + rel_id = self.harness.add_relation("peer", "openfga") self.harness.add_relation_unit(rel_id, "openfga-k8s/1") - self.harness.update_relation_data( - rel_id, - "openfga-k8s", - { - STATE_KEY_TOKEN: "test-token", - STATE_KEY_SCHEMA_CREATED: "true", - STATE_KEY_DB_URI: "test-db-uri", - STATE_KEY_PRIVATE_KEY: "test-key", - STATE_KEY_CERTIFICATE: "test-cert", - STATE_KEY_CA: "test-ca", - STATE_KEY_CHAIN: "test-chain", - STATE_KEY_DNS_NAME: "test-dns-name", - }, - ) + + self.harness.charm._state.token = "test-token" + self.harness.charm._state.schema_created = "true" + self.harness.charm._state.db_uri = "test-db-uri" + self.harness.charm._state.private_key = "test-key" + self.harness.charm._state.certificate = "test-cert" + self.harness.charm._state.ca = "test-ca" + self.harness.charm._state.key_chain = "test-chain" + self.harness.charm._state.dns_name = "test-dns-name" container = self.harness.model.unit.get_container("openfga") self.harness.charm.on.openfga_pebble_ready.emit(container) @@ -104,33 +85,30 @@ def test_on_config_changed(self): plan = self.harness.get_container_pebble_plan("openfga") self.maxDiff = None - self.assertEqual( - plan.to_dict(), - { - "services": { - "openfga": { - "override": "merge", - "startup": "disabled", - "summary": "OpenFGA", - "command": "sh -c '/app/openfga run | tee {LOG_FILE}'", - "environment": { - "OPENFGA_AUTHN_METHOD": "preshared", - "OPENFGA_AUTHN_PRESHARED_KEYS": "test-token", - "OPENFGA_DATASTORE_ENGINE": "postgres", - "OPENFGA_DATASTORE_URI": "test-db-uri", - "OPENFGA_GRPC_TLS_CERT": "/app/certificate.pem", - "OPENFGA_GRPC_TLS_ENABLED": "true", - "OPENFGA_GRPC_TLS_KEY": "/app/key.pem", - "OPENFGA_HTTP_TLS_CERT": "/app/certificate.pem", - "OPENFGA_HTTP_TLS_ENABLED": "true", - "OPENFGA_HTTP_TLS_KEY": "/app/key.pem", - "OPENFGA_LOG_LEVEL": "debug", - "OPENFGA_PLAYGROUND_ENABLED": "false", - }, + assert plan.to_dict() == { + "services": { + "openfga": { + "override": "merge", + "startup": "disabled", + "summary": "OpenFGA", + "command": "sh -c '/app/openfga run | tee {LOG_FILE}'", + "environment": { + "OPENFGA_AUTHN_METHOD": "preshared", + "OPENFGA_AUTHN_PRESHARED_KEYS": "a_test_secret", + "OPENFGA_DATASTORE_ENGINE": "postgres", + "OPENFGA_DATASTORE_URI": "test-db-uri", + "OPENFGA_GRPC_TLS_CERT": "/app/certificate.pem", + "OPENFGA_GRPC_TLS_ENABLED": "true", + "OPENFGA_GRPC_TLS_KEY": "/app/key.pem", + "OPENFGA_HTTP_TLS_CERT": "/app/certificate.pem", + "OPENFGA_HTTP_TLS_ENABLED": "true", + "OPENFGA_HTTP_TLS_KEY": "/app/key.pem", + "OPENFGA_LOG_LEVEL": "debug", + "OPENFGA_PLAYGROUND_ENABLED": "false", }, - } - }, - ) + }, + } + } @patch("charm.OpenFGAOperatorCharm._create_openfga_store") @patch("charm.OpenFGAOperatorCharm._get_address") @@ -145,17 +123,11 @@ def test_on_openfga_relation_joined( self.harness.set_leader(True) - rel_id = self.harness.add_relation("openfga-peer", "openfga") + rel_id = self.harness.add_relation("peer", "openfga") self.harness.add_relation_unit(rel_id, "openfga-k8s/1") - self.harness.update_relation_data( - rel_id, - "openfga-k8s", - { - STATE_KEY_TOKEN: "test-token", - STATE_KEY_SCHEMA_CREATED: "true", - STATE_KEY_DB_URI: "test-db-uri", - }, - ) + + self.harness.charm._state.schema_created = "true" + self.harness.charm._state.db_uri = "test_db_uri" self.harness.update_config( { @@ -175,11 +147,12 @@ def test_on_openfga_relation_joined( ) create_openfga_store.assert_called_with("test-store-name") - assert self.harness.get_relation_data(rel_id, "openfga-k8s") == { - "address": "10.10.0.17", - "port": "8080", - "scheme": "http", - "token": "test-token", - "store_id": "01GK13VYZK62Q1T0X55Q2BHYD6", - "dns_name": "openfga-k8s-0.openfga-k8s-endpoints.None.svc.cluster.local", - } + relation_data = self.harness.get_relation_data(rel_id, "openfga-k8s") + self.assertEqual(relation_data["address"], "10.10.0.17") + self.assertEqual(relation_data["port"], "8080") + self.assertEqual(relation_data["scheme"], "http") + self.assertRegexpMatches(relation_data["token_secret_id"], "secret:.*") + self.assertEqual(relation_data["store_id"], "01GK13VYZK62Q1T0X55Q2BHYD6") + self.assertEqual( + relation_data["dns_name"], "openfga-k8s-0.openfga-k8s-endpoints.None.svc.cluster.local" + )