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