diff --git a/charms/jimm-k8s/lib/charms/openfga_k8s/v0/openfga.py b/charms/jimm-k8s/lib/charms/openfga_k8s/v0/openfga.py index f72189656..8160715ce 100644 --- a/charms/jimm-k8s/lib/charms/openfga_k8s/v0/openfga.py +++ b/charms/jimm-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/jimm-k8s/metadata.yaml b/charms/jimm-k8s/metadata.yaml index aa801b330..2fb40e907 100644 --- a/charms/jimm-k8s/metadata.yaml +++ b/charms/jimm-k8s/metadata.yaml @@ -22,11 +22,11 @@ issues: https://github.com/canonical/jimm/issues description: | JIMM is a juju controller, used in conjunction with the JaaS dashboard to provide a seamless way - to manage models, regardless of where their controllers reside or what cloud they may be running on. + to manage models, regardless of where their controllers reside or what cloud they may be running on. peers: - jimm: - interface: jimm + peer: + interface: jimm-peer provides: dashboard: diff --git a/charms/jimm-k8s/requirements.txt b/charms/jimm-k8s/requirements.txt index 4e1a427cc..7b6542db5 100644 --- a/charms/jimm-k8s/requirements.txt +++ b/charms/jimm-k8s/requirements.txt @@ -1,5 +1,5 @@ Jinja2 >= 2.11.3 -ops >= 1.5.2 +ops >= 2.0.0 charmhelpers >= 0.20.22 jsonschema >= 3.2.0 cryptography >= 3.4.8 diff --git a/charms/jimm-k8s/src/charm.py b/charms/jimm-k8s/src/charm.py index bbe38b1ea..05c54162a 100755 --- a/charms/jimm-k8s/src/charm.py +++ b/charms/jimm-k8s/src/charm.py @@ -20,6 +20,7 @@ import logging import os import socket +import functools import hvac import requests @@ -56,7 +57,7 @@ WaitingStatus, ) -from state import State +from state import State, requires_state, requires_state_setter logger = logging.getLogger(__name__) @@ -66,6 +67,12 @@ "JIMM_UUID", "JIMM_DSN", "CANDID_URL", + "OPENFGA_STORE", + "OPENFGA_AUTH_MODEL", + "OPENFGA_HOST", + "OPENFGA_SCHEME", + "OPENFGA_TOKEN", + "OPENFGA_PORT", ] DATABASE_NAME = "jimm" @@ -81,8 +88,9 @@ class JimmOperatorCharm(CharmBase): def __init__(self, *args): super().__init__(*args) - self._state = State(self.app, lambda: self.model.get_relation("jimm")) - + self._state = State(self.app, lambda: self.model.get_relation("peer")) + + self.framework.observe(self.on.peer_relation_changed, self._on_peer_relation_changed) self.framework.observe(self.on.jimm_pebble_ready, self._on_jimm_pebble_ready) self.framework.observe(self.on.config_changed, self._on_config_changed) self.framework.observe(self.on.update_status, self._on_update_status) @@ -183,19 +191,18 @@ def __init__(self, *args): self._dashboard_path = "/root/dashboard" self._dashboard_hash_path = "/root/dashboard/hash" + def _on_peer_relation_changed(self, event): + self._update_workload(event) + def _on_jimm_pebble_ready(self, event): self._update_workload(event) def _on_config_changed(self, event): self._update_workload(event) + @requires_state_setter def _on_leader_elected(self, event): - if not self._state.is_ready(): - event.defer() - logger.warning("State is not ready") - return - - if self.unit.is_leader() and not self._state.private_key: + if not self._state.private_key: private_key: bytes = generate_private_key(key_size=4096) self._state.private_key = private_key.decode() @@ -218,16 +225,11 @@ def _ensure_bakery_agent_file(self, event): self._push_to_workload(self._agent_filename, agent_data, event) + @requires_state def _update_workload(self, event): """' Update workload with all available configuration data.""" - if not self._state.is_ready(): - event.defer() - print(self._state.is_ready()) - logger.warning("State is not ready") - return - container = self.unit.get_container(WORKLOAD_CONTAINER) if not container.can_connect(): logger.info("cannot connect to the workload container - deferring the event") @@ -338,14 +340,8 @@ def _on_update_status(self, _): """Update the status of the charm.""" self._ready() + @requires_state_setter def _on_dashboard_relation_joined(self, event: RelationJoinedEvent): - if not self.unit.is_leader(): - return - - if not self._state.is_ready(): - event.defer() - logger.warning("State is not ready") - return dns_name = self._get_dns_name(event) if not dns_name: @@ -359,14 +355,10 @@ def _on_dashboard_relation_joined(self, event: RelationJoinedEvent): } ) + @requires_state_setter def _on_database_event(self, event: DatabaseEvent) -> None: """Database event handler.""" - if not self._state.is_ready(): - event.defer() - logger.warning("State is not ready") - return - # get the first endpoint from a comma separate list ep = event.endpoints.split(",", 1)[0] # compose the db connection string @@ -379,12 +371,9 @@ def _on_database_event(self, event: DatabaseEvent) -> None: self._update_workload(event) + @requires_state_setter def _on_database_relation_broken(self, event: DatabaseEvent) -> None: """Database relation broken handler.""" - if not self._state.is_ready(): - event.defer() - logger.warning("State is not ready") - return # when the database relation is broken, we unset the # connection string and schema-created from the application @@ -509,11 +498,8 @@ def _on_vault_relation_joined(self, event): event.relation.data[self.unit]["access_address"] = json.dumps(self._get_network_address(event)) event.relation.data[self.unit]["isolated"] = json.dumps(False) + @requires_state_setter def _on_vault_relation_changed(self, event): - if not self._state.is_ready(): - event.defer() - logger.warning("State is not ready") - return container = self.unit.get_container(WORKLOAD_CONTAINER) @@ -576,26 +562,24 @@ def _hash(self, filename): md5.update(data) return md5.hexdigest() + @requires_state_setter def _on_openfga_store_created(self, event: OpenFGAStoreCreateEvent): - if not self.unit.is_leader(): - return - - if not self._state.is_ready(): - event.defer() - logger.warning("State is not ready") - return if not event.store_id: return + secret = self.model.get_secret(id=event.token_secret_id) + secret_content = secret.get_content() + self._state.openfga_store_id = event.store_id - self._state.openfga_token = event.token + self._state.openfga_token = secret_content["token"] self._state.openfga_address = event.address self._state.openfga_port = event.port self._state.openfga_scheme = event.scheme self._update_workload(event) + @requires_state def _get_dns_name(self, event): if not self._state.is_ready(): event.defer() @@ -613,14 +597,8 @@ def _get_dns_name(self, event): return dns_name + @requires_state_setter def _on_certificates_relation_joined(self, event: RelationJoinedEvent) -> None: - if not self.unit.is_leader(): - return - - if not self._state.is_ready(): - event.defer() - logger.warning("State is not ready") - return dns_name = self._get_dns_name(event) if not dns_name: @@ -635,29 +613,17 @@ def _on_certificates_relation_joined(self, event: RelationJoinedEvent) -> None: self.certificates.request_certificate_creation(certificate_signing_request=csr) + @requires_state_setter def _on_certificate_available(self, event: CertificateAvailableEvent) -> None: - if not self.unit.is_leader(): - return - - if not self._state.is_ready(): - event.defer() - logger.warning("State is not ready") - return - + self._state.certificate = event.certificate self._state.ca = event.ca self._state.chain = event.chain self._update_workload(event) + @requires_state_setter def _on_certificate_expiring(self, event: CertificateExpiringEvent) -> None: - if not self.unit.is_leader(): - return - - if not self._state.is_ready(): - event.defer() - logger.warning("State is not ready") - return old_csr = self._state.csr private_key = self._state.private_key @@ -677,14 +643,8 @@ def _on_certificate_expiring(self, event: CertificateExpiringEvent) -> None: self._update_workload() + @requires_state_setter def _on_certificate_revoked(self, event: CertificateRevokedEvent) -> None: - if not self.unit.is_leader(): - return - - if not self._state.is_ready(): - event.defer() - logger.warning("State is not ready") - return old_csr = self._state.csr private_key = self._state.private_key @@ -709,37 +669,22 @@ def _on_certificate_revoked(self, event: CertificateRevokedEvent) -> None: self.unit.status = WaitingStatus("Waiting for new certificate") self._update_workload() + @requires_state_setter def _on_ingress_ready(self, event: IngressPerAppReadyEvent): - if not self.unit.is_leader(): - return - - if not self._state.is_ready(): - event.defer() - logger.warning("State is not ready") - return self._state.dns_name = event.url self._update_workload(event) + @requires_state_setter def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent): - if not self.unit.is_leader(): - return - - if not self._state.is_ready(): - event.defer() - logger.warning("State is not ready") - return del self._state.dns_name self._update_workload(event) + @requires_state_setter def _on_create_authorization_model_action(self, event: ActionEvent): - if not self._state.is_ready(): - event.defer() - logger.warning("State is not ready") - return model = event.params["model"] if not model: diff --git a/charms/jimm-k8s/src/state.py b/charms/jimm-k8s/src/state.py index 71a165423..e5901fb31 100644 --- a/charms/jimm-k8s/src/state.py +++ b/charms/jimm-k8s/src/state.py @@ -3,9 +3,34 @@ """Manager for handling charm state.""" +import functools import json + +def requires_state_setter(func): + @functools.wraps(func) + def wrapper(self, event): + if self.unit.is_leader() and self._state.is_ready(): + return func(self, event) + else: + return + + return wrapper + + +def requires_state(func): + @functools.wraps(func) + def wrapper(self, event): + if self._state.is_ready(): + return func(self, event) + else: + event.defer() + return + + return wrapper + + class State: """A magic state that uses a relation as the data store. diff --git a/charms/jimm-k8s/tests/integration/test_charm.py b/charms/jimm-k8s/tests/integration/test_charm.py index ba124647b..300cb31b2 100644 --- a/charms/jimm-k8s/tests/integration/test_charm.py +++ b/charms/jimm-k8s/tests/integration/test_charm.py @@ -44,6 +44,7 @@ async def test_build_and_deploy(ops_test: OpsTest, local_charm): "candid-url": "https://api.jujucharms.com/identity", "public-key": "izcYsQy3TePp6bLjqOo3IRPFvkQd2IKtyODGqC6SdFk=", "private-key": "ly/dzsI9Nt/4JxUILQeAX79qZ4mygDiuYGqc2ZEiDEc=", + "dns-name": "jimm.test.canonical.com", }, ) await ops_test.model.deploy( diff --git a/charms/jimm-k8s/tests/unit/test_charm.py b/charms/jimm-k8s/tests/unit/test_charm.py index 426656cc5..44950da1e 100644 --- a/charms/jimm-k8s/tests/unit/test_charm.py +++ b/charms/jimm-k8s/tests/unit/test_charm.py @@ -44,7 +44,7 @@ def setUp(self): self.addCleanup(self.tempdir.cleanup) self.harness.charm.framework.charm_dir = pathlib.Path(self.tempdir.name) - self.harness.add_relation("jimm", "jimm") + self.harness.add_relation("peer", "jimm") self.harness.container_pebble_ready("jimm") rel_id = self.harness.add_relation("ingress", "nginx-ingress") @@ -247,7 +247,7 @@ def test_dashboard_relation_joined(self): harness = Harness(JimmOperatorCharm) self.addCleanup(harness.cleanup) - id = harness.add_relation("jimm", "juju-jimm-k8s") + id = harness.add_relation("peer", "juju-jimm-k8s") harness.add_relation_unit(id, "juju-jimm-k8s/1") harness.begin() harness.set_leader(True) @@ -288,7 +288,7 @@ def test_vault_relation_joined(self, hvac_client_sys, gethostname, get_network_a harness = Harness(JimmOperatorCharm) self.addCleanup(harness.cleanup) - jimm_id = harness.add_relation("jimm", "juju-jimm-k8s") + jimm_id = harness.add_relation("peer", "juju-jimm-k8s") harness.add_relation_unit(jimm_id, "juju-jimm-k8s/1") dashboard_id = harness.add_relation("dashboard", "juju-dashboard") diff --git a/charms/jimm-k8s/tox.ini b/charms/jimm-k8s/tox.ini index 6da1feda6..c273f9130 100644 --- a/charms/jimm-k8s/tox.ini +++ b/charms/jimm-k8s/tox.ini @@ -61,7 +61,7 @@ commands = [testenv:integration] description = Run integration tests deps = - juju~=2.9 + juju~=3.2 pytest pytest-operator -r{toxinidir}/requirements.txt diff --git a/charms/jimm/lib/charms/openfga_k8s/v0/openfga.py b/charms/jimm/lib/charms/openfga_k8s/v0/openfga.py index f72189656..8160715ce 100644 --- a/charms/jimm/lib/charms/openfga_k8s/v0/openfga.py +++ b/charms/jimm/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/jimm/src/charm.py b/charms/jimm/src/charm.py index abc6b13a1..69d8436d6 100755 --- a/charms/jimm/src/charm.py +++ b/charms/jimm/src/charm.py @@ -408,13 +408,16 @@ def _on_dashboard_relation_joined(self, event): def _on_openfga_store_created(self, event: OpenFGAStoreCreateEvent): if not event.store_id: return - + + secret = self.model.get_secret(id=event.token_secret_id) + secret_content = secret.get_content() + args = { "openfga_host": event.address, "openfga_port": event.port, "openfga_scheme": event.scheme, "openfga_store": event.store_id, - "openfga_token": event.token, + "openfga_token": secret_content["token"], } with open(self._env_filename("openfga"), "wt") as f: