blob: f51213daea3fcee9e937f6ce88c80cbb72a68119 [file] [log] [blame]
sousaedu1dd4c0d2020-11-04 17:43:47 +00001#!/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
23import logging
sousaedu1dd4c0d2020-11-04 17:43:47 +000024from typing import Any, Dict, NoReturn
25
26from ops.charm import CharmBase, CharmEvents
27from ops.framework import EventBase, EventSource, StoredState
28from ops.main import main
29from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus
30from oci_image import OCIImageResource, OCIImageResourceError
31
32from pod_spec import make_pod_spec
33
34LOGGER = logging.getLogger(__name__)
35
36MON_PORT = 8000
37
38
39class ConfigurePodEvent(EventBase):
40 """Configure Pod event"""
41
42 pass
43
44
45class MonEvents(CharmEvents):
46 """MON Events"""
47
48 configure_pod = EventSource(ConfigurePodEvent)
49
50
51class MonCharm(CharmBase):
52 """MON Charm."""
53
54 state = StoredState()
55 on = MonEvents()
56
57 def __init__(self, *args) -> NoReturn:
58 """MON Charm constructor."""
59 super().__init__(*args)
60
61 # Internal state initialization
62 self.state.set_default(pod_spec=None)
63
64 # Message bus data initialization
65 self.state.set_default(message_host=None)
66 self.state.set_default(message_port=None)
67
68 # Database data initialization
69 self.state.set_default(database_uri=None)
70
71 # Prometheus data initialization
72 self.state.set_default(prometheus_host=None)
73 self.state.set_default(prometheus_port=None)
74
75 self.port = MON_PORT
76 self.image = OCIImageResource(self, "image")
77
78 # Registering regular events
79 self.framework.observe(self.on.start, self.configure_pod)
80 self.framework.observe(self.on.config_changed, self.configure_pod)
81 self.framework.observe(self.on.upgrade_charm, self.configure_pod)
82
83 # Registering custom internal events
84 self.framework.observe(self.on.configure_pod, self.configure_pod)
85
86 # Registering required relation events
87 self.framework.observe(
88 self.on.kafka_relation_changed, self._on_kafka_relation_changed
89 )
90 self.framework.observe(
91 self.on.mongodb_relation_changed, self._on_mongodb_relation_changed
92 )
93 self.framework.observe(
94 self.on.prometheus_relation_changed, self._on_prometheus_relation_changed
95 )
96
97 # Registering required relation departed events
98 self.framework.observe(
99 self.on.kafka_relation_departed, self._on_kafka_relation_departed
100 )
101 self.framework.observe(
102 self.on.mongodb_relation_departed, self._on_mongodb_relation_departed
103 )
104 self.framework.observe(
105 self.on.prometheus_relation_departed, self._on_prometheus_relation_departed
106 )
107
108 def _on_kafka_relation_changed(self, event: EventBase) -> NoReturn:
109 """Reads information about the kafka relation.
110
111 Args:
112 event (EventBase): Kafka relation event.
113 """
sousaeduaeb1e5e2021-01-05 14:39:30 +0000114 message_host = event.relation.data[event.unit].get("host")
115 message_port = event.relation.data[event.unit].get("port")
sousaedu1dd4c0d2020-11-04 17:43:47 +0000116
117 if (
118 message_host
119 and message_port
120 and (
121 self.state.message_host != message_host
122 or self.state.message_port != message_port
123 )
124 ):
125 self.state.message_host = message_host
126 self.state.message_port = message_port
127 self.on.configure_pod.emit()
128
129 def _on_kafka_relation_departed(self, event: EventBase) -> NoReturn:
130 """Clear kafka relation data.
131
132 Args:
133 event (EventBase): Kafka relation event.
134 """
135 self.state.message_host = None
136 self.state.message_port = None
137 self.on.configure_pod.emit()
138
139 def _on_mongodb_relation_changed(self, event: EventBase) -> NoReturn:
140 """Reads information about the DB relation.
141
142 Args:
143 event (EventBase): DB relation event.
144 """
sousaeduaeb1e5e2021-01-05 14:39:30 +0000145 database_uri = event.relation.data[event.unit].get("connection_string")
sousaedu1dd4c0d2020-11-04 17:43:47 +0000146
147 if database_uri and self.state.database_uri != database_uri:
148 self.state.database_uri = database_uri
149 self.on.configure_pod.emit()
150
151 def _on_mongodb_relation_departed(self, event: EventBase) -> NoReturn:
152 """Clear mongodb relation data.
153
154 Args:
155 event (EventBase): DB relation event.
156 """
157 self.state.database_uri = None
158 self.on.configure_pod.emit()
159
160 def _on_prometheus_relation_changed(self, event: EventBase) -> NoReturn:
161 """Reads information about the prometheus relation.
162
163 Args:
164 event (EventBase): Prometheus relation event.
165 """
sousaeduaeb1e5e2021-01-05 14:39:30 +0000166 prometheus_host = event.relation.data[event.unit].get("hostname")
167 prometheus_port = event.relation.data[event.unit].get("port")
sousaedu1dd4c0d2020-11-04 17:43:47 +0000168
169 if (
170 prometheus_host
171 and prometheus_port
172 and (
173 self.state.prometheus_host != prometheus_host
174 or self.state.prometheus_port != prometheus_port
175 )
176 ):
177 self.state.prometheus_host = prometheus_host
178 self.state.prometheus_port = prometheus_port
179 self.on.configure_pod.emit()
180
181 def _on_prometheus_relation_departed(self, event: EventBase) -> NoReturn:
182 """Clear prometheus relation data.
183
184 Args:
185 event (EventBase): Prometheus relation event.
186 """
187 self.state.prometheus_host = None
188 self.state.prometheus_port = None
189 self.on.configure_pod.emit()
190
191 def _missing_relations(self) -> str:
192 """Checks if there missing relations.
193
194 Returns:
195 str: string with missing relations
196 """
197 data_status = {
198 "kafka": self.state.message_host,
199 "mongodb": self.state.database_uri,
200 "prometheus": self.state.prometheus_host,
201 }
202
203 missing_relations = [k for k, v in data_status.items() if not v]
204
205 return ", ".join(missing_relations)
206
207 @property
208 def relation_state(self) -> Dict[str, Any]:
209 """Collects relation state configuration for pod spec assembly.
210
211 Returns:
212 Dict[str, Any]: relation state information.
213 """
214 relation_state = {
215 "message_host": self.state.message_host,
216 "message_port": self.state.message_port,
217 "database_uri": self.state.database_uri,
218 "prometheus_host": self.state.prometheus_host,
219 "prometheus_port": self.state.prometheus_port,
220 }
221
222 return relation_state
223
224 def configure_pod(self, event: EventBase) -> NoReturn:
225 """Assemble the pod spec and apply it, if possible.
226
227 Args:
228 event (EventBase): Hook or Relation event that started the
229 function.
230 """
231 if missing := self._missing_relations():
232 self.unit.status = BlockedStatus(
233 "Waiting for {0} relation{1}".format(
234 missing, "s" if "," in missing else ""
235 )
236 )
237 return
238
239 if not self.unit.is_leader():
240 self.unit.status = ActiveStatus("ready")
241 return
242
243 self.unit.status = MaintenanceStatus("Assembling pod spec")
244
245 # Fetch image information
246 try:
247 self.unit.status = MaintenanceStatus("Fetching image information")
248 image_info = self.image.fetch()
249 except OCIImageResourceError:
250 self.unit.status = BlockedStatus("Error fetching image information")
251 return
252
253 try:
254 pod_spec = make_pod_spec(
255 image_info,
256 self.model.config,
257 self.relation_state,
258 self.model.app.name,
259 self.port,
260 )
sousaeduaeb1e5e2021-01-05 14:39:30 +0000261 except ValueError as exc:
sousaedu1dd4c0d2020-11-04 17:43:47 +0000262 LOGGER.exception("Config/Relation data validation error")
263 self.unit.status = BlockedStatus(str(exc))
264 return
265
266 if self.state.pod_spec != pod_spec:
267 self.model.pod.set_spec(pod_spec)
268 self.state.pod_spec = pod_spec
269
270 self.unit.status = ActiveStatus("ready")
271
272
273if __name__ == "__main__":
274 main(MonCharm)