74a728c6bf87a17cebbf304ce0b1925b5fdd1bf8
[osm/devops.git] / installers / charm / osm-temporal / src / charm.py
1 #!/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
27 See more: https://charmhub.io/osm
28 """
29
30 import logging
31 import os
32 import socket
33
34 from log import log_event_handler
35 from typing import Any, Dict
36
37 from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch
38 from charms.osm_libs.v0.utils import (
39 CharmError,
40 check_container_ready,
41 check_service_active,
42 )
43 from charms.osm_temporal.v0.temporal import TemporalProvides
44 from lightkube.models.core_v1 import ServicePort
45 from ops.charm import CharmBase, LeaderElectedEvent, RelationJoinedEvent
46 from ops.framework import StoredState
47 from ops.main import main
48 from ops.model import ActiveStatus, Container
49
50 from legacy_interfaces import MysqlClient
51
52 logger = logging.getLogger(__name__)
53 SERVICE_PORT = 7233
54
55
56 class 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():
182 self.temporal.set_host_info(
183 self.app.name, SERVICE_PORT, event.relation if event else None
184 )
185 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",
211 "ports": [
212 7233,
213 ],
214 "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
229 if __name__ == "__main__": # pragma: no cover
230 main(OsmTemporalCharm)