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
This commit is contained in:
Liam Young 2022-10-10 17:06:06 +00:00
parent 017e56809d
commit 8ea8f2855a
4 changed files with 134 additions and 17 deletions

View File

@ -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."""

View File

@ -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."""

View File

@ -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
)]

View File

@ -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']))