blob: 74a728c6bf87a17cebbf304ce0b1925b5fdd1bf8 [file] [log] [blame]
Mark Beierla86e0612023-01-10 16:26:06 -05001#!/usr/bin/env python3
2# Copyright 2022 Canonical Ltd.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
15#
16# For those usages not covered by the Apache License, Version 2.0 please
17# contact: legal@canonical.com
18#
19# To get in touch with the maintainers, please contact:
20# osm-charmers@lists.launchpad.net
21#
22#
23# Learn more at: https://juju.is/docs/sdk
24
25"""OSM Temporal charm.
26
27See more: https://charmhub.io/osm
28"""
29
30import logging
31import os
32import socket
33
34from log import log_event_handler
35from typing import Any, Dict
36
37from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch
38from charms.osm_libs.v0.utils import (
39 CharmError,
40 check_container_ready,
41 check_service_active,
42)
43from charms.osm_temporal.v0.temporal import TemporalProvides
44from lightkube.models.core_v1 import ServicePort
45from ops.charm import CharmBase, LeaderElectedEvent, RelationJoinedEvent
46from ops.framework import StoredState
47from ops.main import main
48from ops.model import ActiveStatus, Container
49
50from legacy_interfaces import MysqlClient
51
52logger = logging.getLogger(__name__)
Dario Faccine93311d2023-02-15 09:29:55 +010053SERVICE_PORT = 7233
Mark Beierla86e0612023-01-10 16:26:06 -050054
55
56class OsmTemporalCharm(CharmBase):
57 """OSM Temporal Kubernetes sidecar charm."""
58
59 _stored = StoredState()
60 container_name = "temporal"
61 service_name = "temporal"
62
63 def __init__(self, *args):
64 super().__init__(*args)
65
66 self.db_client = MysqlClient(self, "db")
67 self._observe_charm_events()
68 self.container: Container = self.unit.get_container(self.container_name)
69 self._stored.set_default(leader_ip="")
70 self._stored.set_default(unit_ip="")
71 self.temporal = TemporalProvides(self)
72 self._patch_k8s_service()
73
74 # ---------------------------------------------------------------------------
75 # Handlers for Charm Events
76 # ---------------------------------------------------------------------------
77
78 @log_event_handler(logger)
79 def _on_config_changed(self, event) -> None:
80 """Handler for the config-changed event."""
81 try:
82 self._validate_config()
83 self._check_relations()
84
85 if self.unit.is_leader():
86 leader_ip_value = socket.gethostbyname(socket.gethostname())
87 if leader_ip_value and leader_ip_value != self._stored.leader_ip:
88 self._stored.leader_ip = leader_ip_value
89
90 unit_ip_value = socket.gethostbyname(socket.gethostname())
91 if unit_ip_value and unit_ip_value != self._stored.unit_ip:
92 self._stored.unit_ip = unit_ip_value
93
94 # Check if the container is ready.
95 # Eventually it will become ready after the first pebble-ready event.
96 check_container_ready(self.container)
97 self._configure_service(self.container)
98 self._update_temporal_relation()
99
100 # Update charm status
101 self._on_update_status(event)
102 except CharmError as e:
103 logger.error(e.message)
104 self.unit.status = e.status
105
106 @log_event_handler(logger)
107 def _on_update_status(self, _=None) -> None:
108 """Handler for the update-status event."""
109 try:
110 self._validate_config()
111 self._check_relations()
112 check_service_active(self.container, self.service_name)
113 self.unit.status = ActiveStatus()
114 except CharmError as e:
115 logger.error(e.message)
116 self.unit.status = e.status
117
118 @log_event_handler(logger)
119 def _on_required_relation_broken(self, event) -> None:
120 """Handler for the kafka-broken event."""
121 # Check Pebble has started in the container
122 try:
123 check_container_ready(self.container)
124 check_service_active(self.container, self.service_name)
125 self.container.stop(self.container_name)
126 except CharmError:
127 pass
128 self._on_update_status(event)
129
130 # ---------------------------------------------------------------------------
131 # Validation and configuration and more
132 # ---------------------------------------------------------------------------
133
134 def _observe_charm_events(self) -> None:
135 event_handler_mapping = {
136 # Core lifecycle events
137 self.on.temporal_pebble_ready: self._on_config_changed,
138 self.on.config_changed: self._on_config_changed,
139 self.on.update_status: self._on_update_status,
140 self.on.temporal_relation_joined: self._update_temporal_relation,
141 }
142
143 # Relation events
144 for relation in [self.on[rel_name] for rel_name in ["db"]]:
145 event_handler_mapping[relation.relation_changed] = self._on_config_changed
146 event_handler_mapping[relation.relation_broken] = self._on_required_relation_broken
147
148 for event, handler in event_handler_mapping.items():
149 self.framework.observe(event, handler)
150
151 def _validate_config(self) -> None:
152 """Validate charm configuration.
153
154 Raises:
155 CharmError: if charm configuration is invalid.
156 """
157 logger.debug("validating charm config")
158
159 def _check_relations(self) -> None:
160 """Validate charm relations.
161
162 Raises:
163 CharmError: if charm configuration is invalid.
164 """
165 logger.debug("check for missing relations")
166 missing_relations = []
167
168 if not self.config.get("mysql-uri") and self.db_client.is_missing_data_in_unit():
169 missing_relations.append("db")
170
171 if missing_relations:
172 relations_str = ", ".join(missing_relations)
173 one_relation_missing = len(missing_relations) == 1
174 error_msg = f'need {relations_str} relation{"" if one_relation_missing else "s"}'
175 logger.warning(error_msg)
176 raise CharmError(error_msg)
177
178 def _update_temporal_relation(self, event: RelationJoinedEvent = None) -> None:
179 """Handler for the temporal-relation-joined event."""
180 logger.info(f"isLeader? {self.unit.is_leader()}")
181 if self.unit.is_leader():
Dario Faccine93311d2023-02-15 09:29:55 +0100182 self.temporal.set_host_info(
183 self.app.name, SERVICE_PORT, event.relation if event else None
184 )
Mark Beierla86e0612023-01-10 16:26:06 -0500185 logger.info(f"temporal host info set to {self.app.name} : {SERVICE_PORT}")
186
187 def _patch_k8s_service(self) -> None:
188 port = ServicePort(SERVICE_PORT, name=f"{self.app.name}")
189 self.service_patcher = KubernetesServicePatch(self, [port])
190
191 def _configure_service(self, container: Container) -> None:
192 """Add Pebble layer with the temporal service."""
193 logger.debug(f"configuring {self.app.name} service")
194 logger.info(f"{self._get_layer()}")
195 container.add_layer("temporal", self._get_layer(), combine=True)
196 container.replan()
197
198 def _get_layer(self) -> Dict[str, Any]:
199 """Get layer for Pebble."""
200 return {
201 "summary": "Temporal layer",
202 "description": "pebble config layer for Temporal",
203 "services": {
204 self.service_name: {
205 "override": "replace",
206 "summary": "temporal service",
207 "command": "/etc/temporal/entrypoint.sh autosetup",
208 "startup": "enabled",
209 "user": "root",
210 "group": "root",
Dario Faccine93311d2023-02-15 09:29:55 +0100211 "ports": [
212 7233,
213 ],
Mark Beierla86e0612023-01-10 16:26:06 -0500214 "environment": {
215 "DB": "mysql",
216 "DB_PORT": self.db_client.port,
217 "MYSQL_PWD": self.db_client.root_password,
218 "MYSQL_SEEDS": self.db_client.host,
219 "MYSQL_USER": "root",
220 "MYSQL_TX_ISOLATION_COMPAT": "true",
221 "BIND_ON_IP": "0.0.0.0",
222 "TEMPORAL_BROADCAST_ADDRESS": self._stored.unit_ip,
223 },
224 },
225 },
226 }
227
228
229if __name__ == "__main__": # pragma: no cover
230 main(OsmTemporalCharm)