From 8ea8f2855a890eeee4c87d894ebf8ec59c2176d0 Mon Sep 17 00:00:00 2001 From: Liam Young Date: Mon, 10 Oct 2022 17:06:06 +0000 Subject: [PATCH] Start all services in a container A container service layer is a dictionary of services so there may be more than once service defintion. Account for this by iterating of the services and starting each one in turn. Change-Id: I4cba2e0cda156a6852e71059b0dc0cb1948ce9e6 --- ops-sunbeam/ops_sunbeam/container_handlers.py | 24 ++++---- ops-sunbeam/ops_sunbeam/test_utils.py | 21 +++++++ ops-sunbeam/unit_tests/test_charms.py | 47 +++++++++++++++ ops-sunbeam/unit_tests/test_core.py | 59 +++++++++++++++++-- 4 files changed, 134 insertions(+), 17 deletions(-) diff --git a/ops-sunbeam/ops_sunbeam/container_handlers.py b/ops-sunbeam/ops_sunbeam/container_handlers.py index f33940b3..d109dbe7 100644 --- a/ops-sunbeam/ops_sunbeam/container_handlers.py +++ b/ops-sunbeam/ops_sunbeam/container_handlers.py @@ -256,6 +256,15 @@ class PebbleHandler(ops.charm.Object): else: self.status = '' + def _start_all(self) -> None: + """Start services in container.""" + container = self.charm.unit.get_container(self.container_name) + services = container.get_services() + for service_name, service in services.items(): + if service.is_running(): + container.stop(service_name) + container.start(service_name) + class ServicePebbleHandler(PebbleHandler): """Container handler for containers which manage a service.""" @@ -272,7 +281,7 @@ class ServicePebbleHandler(PebbleHandler): self._state.service_ready = True def start_service(self) -> None: - """Start service in container.""" + """Check and start services in container.""" container = self.charm.unit.get_container(self.container_name) if not container: logger.debug(f'{self.container_name} container is not ready. ' @@ -283,10 +292,7 @@ class ServicePebbleHandler(PebbleHandler): self.service_name, self.get_layer(), combine=True) - service = container.get_service(self.service_name) - if service.is_running(): - container.stop(self.service_name) - container.start(self.service_name) + self._start_all() class WSGIPebbleHandler(PebbleHandler): @@ -316,7 +322,7 @@ class WSGIPebbleHandler(PebbleHandler): self.wsgi_service_name = wsgi_service_name def start_wsgi(self) -> None: - """Start WSGI service.""" + """Check and start services in container.""" container = self.charm.unit.get_container(self.container_name) if not container: logger.debug( @@ -329,11 +335,7 @@ class WSGIPebbleHandler(PebbleHandler): self.service_name, self.get_layer(), combine=True) - service = container.get_service(self.wsgi_service_name) - if service.is_running(): - container.stop(self.wsgi_service_name) - - container.start(self.wsgi_service_name) + self._start_all() def start_service(self) -> None: """Start the service.""" diff --git a/ops-sunbeam/ops_sunbeam/test_utils.py b/ops-sunbeam/ops_sunbeam/test_utils.py index 2e7018b3..152dbbb2 100644 --- a/ops-sunbeam/ops_sunbeam/test_utils.py +++ b/ops-sunbeam/ops_sunbeam/test_utils.py @@ -25,6 +25,7 @@ import sys import typing import unittest import collections +from typing import List from mock import MagicMock, Mock, patch @@ -140,11 +141,23 @@ class ContainerCalls: def __init__(self) -> None: """Init container calls.""" + self.start = collections.defaultdict(list) self.push = collections.defaultdict(list) self.pull = collections.defaultdict(list) self.execute = collections.defaultdict(list) self.remove_path = collections.defaultdict(list) + def add_start(self, container_name: str, call: typing.Dict) -> None: + """Log a start call.""" + self.start[container_name].append(call) + + def started_services(self, container_name: str) -> List: + """Distinct unordered list of services that were started.""" + return list(set([ + svc + for svc_list in self.start[container_name] + for svc in svc_list])) + def add_push(self, container_name: str, call: typing.Dict) -> None: """Log a push call.""" self.push[container_name].append(call) @@ -607,6 +620,14 @@ def get_harness( process_mock.wait_output.return_value = ('', None) return process_mock + def start_services( + self, services: List[str], timeout: float = 30.0, + delay: float = 0.1,) -> None: + """Record start service events.""" + container_calls.add_start( + self.container_name, + services) + class _OSTestingModelBackend(_TestingModelBackend): def get_pebble(self, socket_path: str) -> _OSTestingPebbleClient: """Get the testing pebble client.""" diff --git a/ops-sunbeam/unit_tests/test_charms.py b/ops-sunbeam/unit_tests/test_charms.py index 5e8c9023..a2c15654 100644 --- a/ops-sunbeam/unit_tests/test_charms.py +++ b/ops-sunbeam/unit_tests/test_charms.py @@ -24,11 +24,13 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: import ops.framework +from typing import List sys.path.append("unit_tests/lib") # noqa sys.path.append("src") # noqa import ops_sunbeam.charm as sunbeam_charm +import ops_sunbeam.container_handlers as sunbeam_chandlers CHARM_CONFIG = """ options: @@ -255,3 +257,48 @@ class MyAPICharm(sunbeam_charm.OSBaseOperatorAPICharm): def healthcheck_http_url(self) -> str: """Healthcheck HTTP URL for the service.""" return f'http://localhost:{self.default_public_ingress_port}/v3' + + +class MultiSvcPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): + """Test pebble handler for multi service charm.""" + + def get_layer(self) -> dict: + """Glance API service pebble layer. + + :returns: pebble layer configuration for glance api service + """ + return { + "summary": f"{self.service_name} layer", + "description": "pebble config layer for glance api service", + "services": { + f"{self.service_name}": { + "override": "replace", + "summary": f"{self.service_name} standalone", + "command": "/usr/bin/glance-api", + "startup": "disabled", + }, + "apache forwarder": { + "override": "replace", + "summary": "apache", + "command": "/usr/sbin/apache2ctl -DFOREGROUND", + "startup": "disabled", + }, + }, + } + + +class TestMultiSvcCharm(MyAPICharm): + """Test class of multi service charm.""" + + def get_pebble_handlers(self) -> List[sunbeam_chandlers.PebbleHandler]: + """Pebble handlers for the service.""" + return [ + MultiSvcPebbleHandler( + self, + self.service_name, + self.service_name, + self.container_configs, + self.template_dir, + self.openstack_release, + self.configure_charm + )] diff --git a/ops-sunbeam/unit_tests/test_core.py b/ops-sunbeam/unit_tests/test_core.py index e3f6e10d..1560c2c1 100644 --- a/ops-sunbeam/unit_tests/test_core.py +++ b/ops-sunbeam/unit_tests/test_core.py @@ -73,21 +73,18 @@ class TestOSBaseOperatorCharm(test_utils.CharmTestCase): self.harness.charm.relation_handlers_ready()) -class TestOSBaseOperatorAPICharm(test_utils.CharmTestCase): +class _TestOSBaseOperatorAPICharm(test_utils.CharmTestCase): """Test for the OSBaseOperatorAPICharm class.""" PATCHES = [] - @mock.patch( - 'charms.observability_libs.v0.kubernetes_service_patch.' - 'KubernetesServicePatch') - def setUp(self, mock_svc_patch: mock.patch) -> None: + def setUp(self, charm_to_test: test_charms.MyAPICharm) -> 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.MyAPICharm, + charm_to_test, test_charms.API_CHARM_METADATA, self.container_calls, charm_config=test_charms.CHARM_CONFIG, @@ -116,6 +113,17 @@ class TestOSBaseOperatorAPICharm(test_utils.CharmTestCase): """Set pebble ready event.""" self.harness.container_pebble_ready('my-service') + +class TestOSBaseOperatorAPICharm(_TestOSBaseOperatorAPICharm): + """Test Charm with services.""" + + @mock.patch( + 'charms.observability_libs.v0.kubernetes_service_patch.' + 'KubernetesServicePatch') + def setUp(self, mock_svc_patch: mock.patch) -> None: + """Run test class setup.""" + super().setUp(test_charms.MyAPICharm) + def test_write_config(self) -> None: """Test when charm is ready configs are written correctly.""" test_utils.add_complete_ingress_relation(self.harness) @@ -150,6 +158,20 @@ class TestOSBaseOperatorAPICharm(test_utils.CharmTestCase): group='root', ) + def test_start_services(self) -> None: + """Test service is started.""" + test_utils.add_complete_ingress_relation(self.harness) + self.harness.set_leader() + test_utils.add_complete_peer_relation(self.harness) + self.set_pebble_ready() + self.harness.charm.leader_set({'foo': 'bar'}) + test_utils.add_api_relations(self.harness) + test_utils.add_complete_cloud_credentials_relation(self.harness) + self.harness.set_can_connect('my-service', True) + self.assertEqual( + self.container_calls.started_services('my-service'), + ['wsgi-my-service']) + def test__on_database_changed(self) -> None: """Test database is requested.""" rel_id = self.harness.add_relation('peers', 'my-service') @@ -300,3 +322,28 @@ class TestOSBaseOperatorAPICharm(test_utils.CharmTestCase): self.harness, ingress_rel_id, 'public') self.assertTrue( self.harness.charm.relation_handlers_ready()) + + +class TestOSBaseOperatorMultiSVCAPICharm(_TestOSBaseOperatorAPICharm): + """Test Charm with multiple services.""" + + @mock.patch( + 'charms.observability_libs.v0.kubernetes_service_patch.' + 'KubernetesServicePatch') + def setUp(self, mock_svc_patch: mock.patch) -> None: + """Charm test class setip.""" + super().setUp(test_charms.TestMultiSvcCharm) + + def test_start_services(self) -> None: + """Test multiple services are started.""" + test_utils.add_complete_ingress_relation(self.harness) + self.harness.set_leader() + test_utils.add_complete_peer_relation(self.harness) + self.set_pebble_ready() + self.harness.charm.leader_set({'foo': 'bar'}) + test_utils.add_api_relations(self.harness) + test_utils.add_complete_cloud_credentials_relation(self.harness) + self.harness.set_can_connect('my-service', True) + self.assertEqual( + sorted(self.container_calls.started_services('my-service')), + sorted(['apache forwarder', 'my-service']))