2 # Copyright 2020 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
24 from typing
import Dict
, List
, NoReturn
26 from ops
.charm
import CharmBase
27 from ops
.framework
import EventBase
, StoredState
28 from ops
.main
import main
29 from ops
.model
import ActiveStatus
, Application
, BlockedStatus
, MaintenanceStatus
, Unit
30 from oci_image
import OCIImageResource
, OCIImageResourceError
32 from pod_spec
import make_pod_spec
34 logger
= logging
.getLogger(__name__
)
39 class RelationsMissing(Exception):
40 def __init__(self
, missing_relations
: List
):
42 if missing_relations
and isinstance(missing_relations
, list):
43 self
.message
+= f
'Waiting for {", ".join(missing_relations)} relation'
44 if "," in self
.message
:
48 class RelationDefinition
:
49 def __init__(self
, relation_name
: str, keys
: List
, source_type
):
50 if source_type
!= Application
and source_type
!= Unit
:
52 "source_type should be ops.model.Application or ops.model.Unit"
54 self
.relation_name
= relation_name
56 self
.source_type
= source_type
59 def check_missing_relation_data(
61 expected_relations_data
: List
[RelationDefinition
],
63 missing_relations
= []
64 for relation_data
in expected_relations_data
:
66 f
"{relation_data.relation_name}_{k}" in data
for k
in relation_data
.keys
68 missing_relations
.append(relation_data
.relation_name
)
70 raise RelationsMissing(missing_relations
)
73 def get_relation_data(
75 relation_data
: RelationDefinition
,
78 relation
= charm
.model
.get_relation(relation_data
.relation_name
)
81 charm
.app
if relation_data
.source_type
== Application
else charm
.unit
83 expected_type
= relation_data
.source_type
84 for app_unit
in relation
.data
:
85 if app_unit
!= self_app_unit
and isinstance(app_unit
, expected_type
):
86 if all(k
in relation
.data
[app_unit
] for k
in relation_data
.keys
):
87 for k
in relation_data
.keys
:
88 data
[f
"{relation_data.relation_name}_{k}"] = relation
.data
[
95 class RoCharm(CharmBase
):
100 def __init__(self
, *args
) -> NoReturn
:
101 """RO Charm constructor."""
102 super().__init
__(*args
)
104 # Internal state initialization
105 self
.state
.set_default(pod_spec
=None)
108 self
.image
= OCIImageResource(self
, "image")
110 # Registering regular events
111 self
.framework
.observe(self
.on
.start
, self
.configure_pod
)
112 self
.framework
.observe(self
.on
.config_changed
, self
.configure_pod
)
114 # Registering required relation events
115 self
.framework
.observe(self
.on
.kafka_relation_changed
, self
.configure_pod
)
116 self
.framework
.observe(self
.on
.mongodb_relation_changed
, self
.configure_pod
)
117 self
.framework
.observe(self
.on
.mysql_relation_changed
, self
.configure_pod
)
119 # Registering required relation departed events
120 self
.framework
.observe(self
.on
.kafka_relation_departed
, self
.configure_pod
)
121 self
.framework
.observe(self
.on
.mongodb_relation_departed
, self
.configure_pod
)
122 self
.framework
.observe(self
.on
.mysql_relation_departed
, self
.configure_pod
)
124 # Registering required relation broken events
125 self
.framework
.observe(self
.on
.kafka_relation_broken
, self
.configure_pod
)
126 self
.framework
.observe(self
.on
.mongodb_relation_broken
, self
.configure_pod
)
127 self
.framework
.observe(self
.on
.mysql_relation_broken
, self
.configure_pod
)
129 # Registering provided relation events
130 self
.framework
.observe(self
.on
.ro_relation_joined
, self
._publish
_ro
_info
)
132 def _publish_ro_info(self
, event
: EventBase
) -> NoReturn
:
133 """Publishes RO information.
136 event (EventBase): RO relation event.
138 if self
.unit
.is_leader():
140 "host": self
.model
.app
.name
,
141 "port": str(RO_PORT
),
143 for k
, v
in rel_data
.items():
144 event
.relation
.data
[self
.app
][k
] = v
147 def relations_requirements(self
):
148 if self
.model
.config
["enable_ng_ro"]:
150 RelationDefinition("kafka", ["host", "port"], Unit
),
151 RelationDefinition("mongodb", ["connection_string"], Unit
),
156 "mysql", ["host", "port", "user", "password", "root_password"], Unit
160 def get_relation_state(self
):
162 for relation_requirements
in self
.relations_requirements
:
163 data
= get_relation_data(self
, relation_requirements
)
164 relation_state
= {**relation_state
, **data
}
165 check_missing_relation_data(relation_state
, self
.relations_requirements
)
166 return relation_state
168 def configure_pod(self
, _
=None) -> NoReturn
:
169 """Assemble the pod spec and apply it, if possible.
172 event (EventBase): Hook or Relation event that started the
175 if not self
.unit
.is_leader():
176 self
.unit
.status
= ActiveStatus("ready")
179 relation_state
= None
181 relation_state
= self
.get_relation_state()
182 except RelationsMissing
as exc
:
183 logger
.exception("Relation missing error")
184 self
.unit
.status
= BlockedStatus(exc
.message
)
187 self
.unit
.status
= MaintenanceStatus("Assembling pod spec")
189 # Fetch image information
191 self
.unit
.status
= MaintenanceStatus("Fetching image information")
192 image_info
= self
.image
.fetch()
193 except OCIImageResourceError
:
194 self
.unit
.status
= BlockedStatus("Error fetching image information")
198 pod_spec
= make_pod_spec(
205 except ValueError as exc
:
206 logger
.exception("Config/Relation data validation error")
207 self
.unit
.status
= BlockedStatus(str(exc
))
210 if self
.state
.pod_spec
!= pod_spec
:
211 self
.model
.pod
.set_spec(pod_spec
)
212 self
.state
.pod_spec
= pod_spec
214 self
.unit
.status
= ActiveStatus("ready")
217 if __name__
== "__main__":