Refactoring POL Charm to use Operator Framework
[osm/devops.git] / installers / charm / pol / src / charm.py
1 #!/usr/bin/env python3
2 # Copyright 2020 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 import logging
24 from pydantic import ValidationError
25 from typing import Any, Dict, NoReturn
26
27 from ops.charm import CharmBase, CharmEvents
28 from ops.framework import EventBase, EventSource, StoredState
29 from ops.main import main
30 from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus
31 from oci_image import OCIImageResource, OCIImageResourceError
32
33 from pod_spec import make_pod_spec
34
35 logger = logging.getLogger(__name__)
36
37
38 class ConfigurePodEvent(EventBase):
39 """Configure Pod event"""
40
41 pass
42
43
44 class PolEvents(CharmEvents):
45 """POL Events"""
46
47 configure_pod = EventSource(ConfigurePodEvent)
48
49
50 class PolCharm(CharmBase):
51 """POL Charm."""
52
53 state = StoredState()
54 on = PolEvents()
55
56 def __init__(self, *args) -> NoReturn:
57 """POL Charm constructor."""
58 super().__init__(*args)
59
60 # Internal state initialization
61 self.state.set_default(pod_spec=None)
62
63 # Message bus data initialization
64 self.state.set_default(message_host=None)
65 self.state.set_default(message_port=None)
66
67 # Database data initialization
68 self.state.set_default(database_uri=None)
69
70 self.image = OCIImageResource(self, "image")
71
72 # Registering regular events
73 self.framework.observe(self.on.start, self.configure_pod)
74 self.framework.observe(self.on.config_changed, self.configure_pod)
75 self.framework.observe(self.on.upgrade_charm, self.configure_pod)
76
77 # Registering custom internal events
78 self.framework.observe(self.on.configure_pod, self.configure_pod)
79
80 # Registering required relation events
81 self.framework.observe(
82 self.on.kafka_relation_changed, self._on_kafka_relation_changed
83 )
84 self.framework.observe(
85 self.on.mongodb_relation_changed, self._on_mongodb_relation_changed
86 )
87
88 # Registering required relation departed events
89 self.framework.observe(
90 self.on.kafka_relation_departed, self._on_kafka_relation_departed
91 )
92 self.framework.observe(
93 self.on.mongodb_relation_departed, self._on_mongodb_relation_departed
94 )
95
96 def _on_kafka_relation_changed(self, event: EventBase) -> NoReturn:
97 """Reads information about the kafka relation.
98
99 Args:
100 event (EventBase): Kafka relation event.
101 """
102 data_loc = event.unit if event.unit else event.app
103
104 message_host = event.relation.data[data_loc].get("host")
105 message_port = event.relation.data[data_loc].get("port")
106
107 if (
108 message_host
109 and message_port
110 and (
111 self.state.message_host != message_host
112 or self.state.message_port != message_port
113 )
114 ):
115 self.state.message_host = message_host
116 self.state.message_port = message_port
117 self.on.configure_pod.emit()
118
119 def _on_kafka_relation_departed(self, event: EventBase) -> NoReturn:
120 """Clear kafka relation data.
121
122 Args:
123 event (EventBase): Kafka relation event.
124 """
125 self.state.message_host = None
126 self.state.message_port = None
127 self.on.configure_pod.emit()
128
129 def _on_mongodb_relation_changed(self, event: EventBase) -> NoReturn:
130 """Reads information about the DB relation.
131
132 Args:
133 event (EventBase): DB relation event.
134 """
135 data_loc = event.unit if event.unit else event.app
136
137 database_uri = event.relation.data[data_loc].get("connection_string")
138
139 if database_uri and self.state.database_uri != database_uri:
140 self.state.database_uri = database_uri
141 self.on.configure_pod.emit()
142
143 def _on_mongodb_relation_departed(self, event: EventBase) -> NoReturn:
144 """Clear mongodb relation data.
145
146 Args:
147 event (EventBase): DB relation event.
148 """
149 self.state.database_uri = None
150 self.on.configure_pod.emit()
151
152 def _missing_relations(self) -> str:
153 """Checks if there missing relations.
154
155 Returns:
156 str: string with missing relations
157 """
158 data_status = {
159 "kafka": self.state.message_host,
160 "mongodb": self.state.database_uri,
161 }
162
163 missing_relations = [k for k, v in data_status.items() if not v]
164
165 return ", ".join(missing_relations)
166
167 @property
168 def relation_state(self) -> Dict[str, Any]:
169 """Collects relation state configuration for pod spec assembly.
170
171 Returns:
172 Dict[str, Any]: relation state information.
173 """
174 relation_state = {
175 "message_host": self.state.message_host,
176 "message_port": self.state.message_port,
177 "database_uri": self.state.database_uri,
178 }
179
180 return relation_state
181
182 def configure_pod(self, event: EventBase) -> NoReturn:
183 """Assemble the pod spec and apply it, if possible.
184
185 Args:
186 event (EventBase): Hook or Relation event that started the
187 function.
188 """
189 if missing := self._missing_relations():
190 self.unit.status = BlockedStatus(
191 "Waiting for {0} relation{1}".format(
192 missing, "s" if "," in missing else ""
193 )
194 )
195 return
196
197 if not self.unit.is_leader():
198 self.unit.status = ActiveStatus("ready")
199 return
200
201 self.unit.status = MaintenanceStatus("Assembling pod spec")
202
203 # Fetch image information
204 try:
205 self.unit.status = MaintenanceStatus("Fetching image information")
206 image_info = self.image.fetch()
207 except OCIImageResourceError:
208 self.unit.status = BlockedStatus("Error fetching image information")
209 return
210
211 try:
212 pod_spec = make_pod_spec(
213 image_info,
214 self.model.config,
215 self.relation_state,
216 self.model.app.name,
217 )
218 except ValidationError as exc:
219 logger.exception("Config/Relation data validation error")
220 self.unit.status = BlockedStatus(str(exc))
221 return
222
223 if self.state.pod_spec != pod_spec:
224 self.model.pod.set_spec(pod_spec)
225 self.state.pod_spec = pod_spec
226
227 self.unit.status = ActiveStatus("ready")
228
229
230 if __name__ == "__main__":
231 main(PolCharm)