diff --git a/ops-sunbeam/ops_sunbeam/charm.py b/ops-sunbeam/ops_sunbeam/charm.py index 5acb676d..edc4a0a3 100644 --- a/ops-sunbeam/ops_sunbeam/charm.py +++ b/ops-sunbeam/ops_sunbeam/charm.py @@ -104,7 +104,6 @@ class OSBaseOperatorCharm(ops.charm.CharmBase): self.bootstrap_status.set( MaintenanceStatus("Service not bootstrapped") ) - self.pebble_handlers = self.get_pebble_handlers() self.framework.observe(self.on.config_changed, self._on_config_changed) self.framework.observe(self.on.secret_changed, self._on_secret_changed) self.framework.observe(self.on.secret_rotate, self._on_secret_rotate) @@ -215,46 +214,6 @@ class OSBaseOperatorCharm(ops.charm.CharmBase): domain_name_sans.append(hostname) return domain_name_sans - def get_pebble_handlers(self) -> List[sunbeam_chandlers.PebbleHandler]: - """Pebble handlers for the operator.""" - return [ - sunbeam_chandlers.PebbleHandler( - self, - self.service_name, - self.service_name, - self.container_configs, - self.template_dir, - self.configure_charm, - ) - ] - - def get_named_pebble_handler( - self, container_name: str - ) -> sunbeam_chandlers.PebbleHandler: - """Get pebble handler matching container_name.""" - pebble_handlers = [ - h - for h in self.pebble_handlers - if h.container_name == container_name - ] - assert len(pebble_handlers) < 2, ( - "Multiple pebble handlers with the " "same name found." - ) - if pebble_handlers: - return pebble_handlers[0] - else: - return None - - def get_named_pebble_handlers( - self, container_names: List[str] - ) -> List[sunbeam_chandlers.PebbleHandler]: - """Get pebble handlers matching container_names.""" - return [ - h - for h in self.pebble_handlers - if h.container_name in container_names - ] - def check_leader_ready(self): """Check the leader is reporting as ready.""" if self.supports_peer_relation and not ( @@ -269,39 +228,10 @@ class OSBaseOperatorCharm(ops.charm.CharmBase): "Not all relations are ready" ) - def init_container_services(self): - """Run init on pebble handlers that are ready.""" - for ph in self.pebble_handlers: - if ph.pebble_ready: - logging.debug(f"Running init for {ph.service_name}") - ph.init_service(self.contexts()) - else: - logging.debug( - f"Not running init for {ph.service_name}," - " container not ready" - ) - raise sunbeam_guard.WaitingExceptionError( - "Payload container not ready" - ) - - def check_pebble_handlers_ready(self): - """Check pebble handlers are ready.""" - for ph in self.pebble_handlers: - if not ph.service_ready: - logging.debug( - f"Aborting container {ph.service_name} service not ready" - ) - raise sunbeam_guard.WaitingExceptionError( - "Container service not ready" - ) - def configure_unit(self, event: ops.framework.EventBase) -> None: """Run configuration on this unit.""" self.check_leader_ready() self.check_relation_handlers_ready() - self.init_container_services() - self.check_pebble_handlers_ready() - self.run_db_sync() self._state.unit_bootstrapped = True def configure_app_leader(self, event): @@ -324,10 +254,10 @@ class OSBaseOperatorCharm(ops.charm.CharmBase): else: self.configure_app_non_leader(event) - def add_pebble_health_checks(self): - """Add health checks for services in payload containers.""" - for ph in self.pebble_handlers: - ph.add_healthchecks() + def post_config_setup(self): + """Configuration steps after services have been setup.""" + logger.info("Setting active status") + self.status.set(ActiveStatus("")) def configure_charm(self, event: ops.framework.EventBase) -> None: """Catchall handler to configure charm services.""" @@ -335,20 +265,13 @@ class OSBaseOperatorCharm(ops.charm.CharmBase): self.configure_unit(event) self.configure_app(event) self.bootstrap_status.set(ActiveStatus()) - self.add_pebble_health_checks() - logger.info("Setting active status") - self.status.set(ActiveStatus("")) + self.post_config_setup() @property def supports_peer_relation(self) -> bool: """Whether the charm support the peers relation.""" return "peers" in self.meta.relations.keys() - @property - def container_configs(self) -> List[sunbeam_core.ContainerConfigFile]: - """Container configuration files for the operator.""" - return [] - @property def config_contexts( self, @@ -361,11 +284,6 @@ class OSBaseOperatorCharm(ops.charm.CharmBase): """Prefix for handlers.""" return self.service_name.replace("-", "_") - @property - def container_names(self) -> List[str]: - """Names of Containers that form part of this service.""" - return [self.service_name] - @property def template_dir(self) -> str: """Directory containing Jinja2 templates.""" @@ -410,14 +328,6 @@ class OSBaseOperatorCharm(ops.charm.CharmBase): # charms should handle the event if required pass - def containers_ready(self) -> bool: - """Determine whether all containers are ready for configuration.""" - for ph in self.pebble_handlers: - if not ph.service_ready: - logger.info(f"Container incomplete: {ph.container_name}") - return False - return True - def relation_handlers_ready(self) -> bool: """Determine whether all relations are ready for use.""" ready_relations = { @@ -472,6 +382,119 @@ class OSBaseOperatorCharm(ops.charm.CharmBase): """Has the lead unit announced that it is ready.""" return self.peers.is_leader_ready() + +class OSBaseOperatorCharmK8S(OSBaseOperatorCharm): + """Base charm class for k8s based charms.""" + + def __init__(self, framework: ops.framework.Framework) -> None: + """Run constructor.""" + super().__init__(framework) + self.pebble_handlers = self.get_pebble_handlers() + + def get_pebble_handlers(self) -> List[sunbeam_chandlers.PebbleHandler]: + """Pebble handlers for the operator.""" + return [ + sunbeam_chandlers.PebbleHandler( + self, + self.service_name, + self.service_name, + self.container_configs, + self.template_dir, + self.configure_charm, + ) + ] + + def get_named_pebble_handler( + self, container_name: str + ) -> sunbeam_chandlers.PebbleHandler: + """Get pebble handler matching container_name.""" + pebble_handlers = [ + h + for h in self.pebble_handlers + if h.container_name == container_name + ] + assert len(pebble_handlers) < 2, ( + "Multiple pebble handlers with the " "same name found." + ) + if pebble_handlers: + return pebble_handlers[0] + else: + return None + + def get_named_pebble_handlers( + self, container_names: List[str] + ) -> List[sunbeam_chandlers.PebbleHandler]: + """Get pebble handlers matching container_names.""" + return [ + h + for h in self.pebble_handlers + if h.container_name in container_names + ] + + def init_container_services(self): + """Run init on pebble handlers that are ready.""" + for ph in self.pebble_handlers: + if ph.pebble_ready: + logging.debug(f"Running init for {ph.service_name}") + ph.init_service(self.contexts()) + else: + logging.debug( + f"Not running init for {ph.service_name}," + " container not ready" + ) + raise sunbeam_guard.WaitingExceptionError( + "Payload container not ready" + ) + + def check_pebble_handlers_ready(self): + """Check pebble handlers are ready.""" + for ph in self.pebble_handlers: + if not ph.service_ready: + logging.debug( + f"Aborting container {ph.service_name} service not ready" + ) + raise sunbeam_guard.WaitingExceptionError( + "Container service not ready" + ) + + def configure_unit(self, event: ops.framework.EventBase) -> None: + """Run configuration on this unit.""" + self.check_leader_ready() + self.check_relation_handlers_ready() + self.init_container_services() + self.check_pebble_handlers_ready() + self.run_db_sync() + self._state.unit_bootstrapped = True + + def add_pebble_health_checks(self): + """Add health checks for services in payload containers.""" + for ph in self.pebble_handlers: + ph.add_healthchecks() + + def post_config_setup(self): + """Configuration steps after services have been setup.""" + self.add_pebble_health_checks() + logger.info("Setting active status") + self.status.set(ActiveStatus("")) + + @property + def container_configs(self) -> List[sunbeam_core.ContainerConfigFile]: + """Container configuration files for the operator.""" + return [] + + @property + def container_names(self) -> List[str]: + """Names of Containers that form part of this service.""" + return [self.service_name] + + def containers_ready(self) -> bool: + """Determine whether all containers are ready for configuration.""" + for ph in self.pebble_handlers: + if not ph.service_ready: + logger.info(f"Container incomplete: {ph.container_name}") + return False + return True + @property def db_sync_container_name(self) -> str: """Name of Containerto run db sync from.""" @@ -518,7 +541,7 @@ class OSBaseOperatorCharm(ops.charm.CharmBase): ) -class OSBaseOperatorAPICharm(OSBaseOperatorCharm): +class OSBaseOperatorAPICharm(OSBaseOperatorCharmK8S): """Base class for OpenStack API operators.""" mandatory_relations = {"database", "identity-service", "ingress-public"} diff --git a/ops-sunbeam/setup.py b/ops-sunbeam/setup.py index 3c8447ce..27cb4ad8 100644 --- a/ops-sunbeam/setup.py +++ b/ops-sunbeam/setup.py @@ -24,6 +24,8 @@ version = "0.0.1.dev1" install_require = [ 'ops', 'tenacity', + 'lightkube', + 'lightkube-models', ] tests_require = [ diff --git a/ops-sunbeam/unit_tests/test_charms.py b/ops-sunbeam/unit_tests/test_charms.py index 2db8a089..837a0b1b 100644 --- a/ops-sunbeam/unit_tests/test_charms.py +++ b/ops-sunbeam/unit_tests/test_charms.py @@ -62,7 +62,11 @@ tags: - misc subordinate: false +""" +CHARM_METADATA_K8S = ( + CHARM_METADATA + + """ containers: my-service: resource: mysvc-image @@ -80,6 +84,7 @@ resources: mysvc-image: type: oci-image """ +) API_CHARM_METADATA = """ name: my-service @@ -151,13 +156,6 @@ class MyCharm(sunbeam_charm.OSBaseOperatorCharm): """Log events.""" self.seen_events.append(type(event).__name__) - def _on_service_pebble_ready( - self, event: "ops.framework.EventBase" - ) -> None: - """Log pebble ready event.""" - self._log_event(event) - super()._on_service_pebble_ready(event) - def _on_config_changed(self, event: "ops.framework.EventBase") -> None: """Log config changed event.""" self._log_event(event) @@ -199,6 +197,54 @@ TEMPLATE_CONTENTS = """ """ +class MyCharmK8S(sunbeam_charm.OSBaseOperatorCharmK8S): + """Test charm for k8s.""" + + service_name = "my-service" + + def __init__(self, framework: "ops.framework.Framework") -> None: + """Run constructor.""" + self.seen_events = [] + self.render_calls = [] + self._template_dir = self._setup_templates() + super().__init__(framework) + + def _log_event(self, event: "ops.framework.EventBase") -> None: + """Log events.""" + self.seen_events.append(type(event).__name__) + + def _on_config_changed(self, event: "ops.framework.EventBase") -> None: + """Log config changed event.""" + self._log_event(event) + super()._on_config_changed(event) + + def configure_charm(self, event: "ops.framework.EventBase") -> None: + """Log configure_charm call.""" + self._log_event(event) + super().configure_charm(event) + + @property + def public_ingress_port(self) -> int: + """Charms default port.""" + return 789 + + def _setup_templates(self) -> str: + """Run temp templates dir setup.""" + tmpdir = tempfile.mkdtemp() + _template_dir = f"{tmpdir}/templates" + os.mkdir(_template_dir) + with open(f"{_template_dir}/my-service.conf.j2", "w") as f: + f.write("") + return _template_dir + + def _on_service_pebble_ready( + self, event: "ops.framework.EventBase" + ) -> None: + """Log pebble ready event.""" + self._log_event(event) + super()._on_service_pebble_ready(event) + + class MyAPICharm(sunbeam_charm.OSBaseOperatorAPICharm): """Test charm for testing OSBaseOperatorAPICharm.""" diff --git a/ops-sunbeam/unit_tests/test_compound_status.py b/ops-sunbeam/unit_tests/test_compound_status.py index 9b1851d9..ceb5f315 100644 --- a/ops-sunbeam/unit_tests/test_compound_status.py +++ b/ops-sunbeam/unit_tests/test_compound_status.py @@ -47,8 +47,8 @@ class TestCompoundStatus(test_utils.CharmTestCase): self.container_calls = test_utils.ContainerCalls() super().setUp(sunbeam_charm, self.PATCHES) self.harness = test_utils.get_harness( - test_charms.MyCharm, - test_charms.CHARM_METADATA, + test_charms.MyCharmK8S, + test_charms.CHARM_METADATA_K8S, self.container_calls, charm_config=test_charms.CHARM_CONFIG, initial_charm_config=test_charms.INITIAL_CHARM_CONFIG, diff --git a/ops-sunbeam/unit_tests/test_core.py b/ops-sunbeam/unit_tests/test_core.py index 6bedefab..96e33693 100644 --- a/ops-sunbeam/unit_tests/test_core.py +++ b/ops-sunbeam/unit_tests/test_core.py @@ -50,6 +50,34 @@ class TestOSBaseOperatorCharm(test_utils.CharmTestCase): self.harness.begin() self.addCleanup(self.harness.cleanup) + def test_write_config(self) -> None: + """Test writing config when charm is ready.""" + self.assertEqual(self.container_calls.push["my-service"], []) + + def test_relation_handlers_ready(self) -> None: + """Test relation handlers are ready.""" + self.assertTrue(self.harness.charm.relation_handlers_ready()) + + +class TestOSBaseOperatorCharmK8S(test_utils.CharmTestCase): + """Test for the OSBaseOperatorCharm class.""" + + PATCHES = [] + + def setUp(self) -> None: + """Charm test class setup.""" + self.container_calls = test_utils.ContainerCalls() + super().setUp(sunbeam_charm, self.PATCHES) + self.harness = test_utils.get_harness( + test_charms.MyCharmK8S, + test_charms.CHARM_METADATA_K8S, + self.container_calls, + charm_config=test_charms.CHARM_CONFIG, + initial_charm_config=test_charms.INITIAL_CHARM_CONFIG, + ) + self.harness.begin() + self.addCleanup(self.harness.cleanup) + def set_pebble_ready(self) -> None: """Set pebble ready event.""" self.harness.container_pebble_ready("my-service")