2 # Copyright 2022 Canonical Ltd.
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
8 # http://www.apache.org/licenses/LICENSE-2.0
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
16 # For those usages not covered by the Apache License, Version 2.0 please
17 # contact: legal@canonical.com
19 # To get in touch with the maintainers, please contact:
20 # osm-charmers@lists.launchpad.net
23 # Learn more at: https://juju.is/docs/sdk
25 """OSM Temporal charm.
27 See more: https://charmhub.io/osm
34 from log
import log_event_handler
35 from typing
import Any
, Dict
37 from charms
.observability_libs
.v1
.kubernetes_service_patch
import KubernetesServicePatch
38 from charms
.osm_libs
.v0
.utils
import (
40 check_container_ready
,
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
50 from legacy_interfaces
import MysqlClient
52 logger
= logging
.getLogger(__name__
)
56 class OsmTemporalCharm(CharmBase
):
57 """OSM Temporal Kubernetes sidecar charm."""
59 _stored
= StoredState()
60 container_name
= "temporal"
61 service_name
= "temporal"
63 def __init__(self
, *args
):
64 super().__init
__(*args
)
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
_k
8s
_service
()
74 # ---------------------------------------------------------------------------
75 # Handlers for Charm Events
76 # ---------------------------------------------------------------------------
78 @log_event_handler(logger
)
79 def _on_config_changed(self
, event
) -> None:
80 """Handler for the config-changed event."""
82 self
._validate
_config
()
83 self
._check
_relations
()
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
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
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
()
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
106 @log_event_handler(logger
)
107 def _on_update_status(self
, _
=None) -> None:
108 """Handler for the update-status event."""
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
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
123 check_container_ready(self
.container
)
124 check_service_active(self
.container
, self
.service_name
)
125 self
.container
.stop(self
.container_name
)
128 self
._on
_update
_status
(event
)
130 # ---------------------------------------------------------------------------
131 # Validation and configuration and more
132 # ---------------------------------------------------------------------------
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
,
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
148 for event
, handler
in event_handler_mapping
.items():
149 self
.framework
.observe(event
, handler
)
151 def _validate_config(self
) -> None:
152 """Validate charm configuration.
155 CharmError: if charm configuration is invalid.
157 logger
.debug("validating charm config")
159 def _check_relations(self
) -> None:
160 """Validate charm relations.
163 CharmError: if charm configuration is invalid.
165 logger
.debug("check for missing relations")
166 missing_relations
= []
168 if not self
.config
.get("mysql-uri") and self
.db_client
.is_missing_data_in_unit():
169 missing_relations
.append("db")
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
)
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
185 logger
.info(f
"temporal host info set to {self.app.name} : {SERVICE_PORT}")
187 def _patch_k8s_service(self
) -> None:
188 port
= ServicePort(SERVICE_PORT
, name
=f
"{self.app.name}")
189 self
.service_patcher
= KubernetesServicePatch(self
, [port
])
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)
198 def _get_layer(self
) -> Dict
[str, Any
]:
199 """Get layer for Pebble."""
201 "summary": "Temporal layer",
202 "description": "pebble config layer for Temporal",
205 "override": "replace",
206 "summary": "temporal service",
207 "command": "/etc/temporal/entrypoint.sh autosetup",
208 "startup": "enabled",
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
,
229 if __name__
== "__main__": # pragma: no cover
230 main(OsmTemporalCharm
)