2 # Copyright 2021 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 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 GrafanaCharm(CharmBase
):
100 def __init__(self
, *args
) -> NoReturn
:
101 """Grafana Charm constructor."""
102 super().__init
__(*args
)
104 # Internal state initialization
105 self
.state
.set_default(pod_spec
=None)
107 self
.port
= GRAFANA_PORT
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
.prometheus_relation_changed
, self
.configure_pod
)
117 # Registering required relation broken events
118 self
.framework
.observe(self
.on
.prometheus_relation_broken
, self
.configure_pod
)
121 def relations_requirements(self
):
122 return [RelationDefinition("prometheus", ["host", "port"], Unit
)]
124 def get_relation_state(self
):
126 for relation_requirements
in self
.relations_requirements
:
127 data
= get_relation_data(self
, relation_requirements
)
128 relation_state
= {**relation_state
, **data
}
129 check_missing_relation_data(relation_state
, self
.relations_requirements
)
130 return relation_state
132 def configure_pod(self
, _
=None) -> NoReturn
:
133 """Assemble the pod spec and apply it, if possible.
136 event (EventBase): Hook or Relation event that started the
139 if not self
.unit
.is_leader():
140 self
.unit
.status
= ActiveStatus("ready")
143 relation_state
= None
145 relation_state
= self
.get_relation_state()
146 except RelationsMissing
as exc
:
147 logger
.exception("Relation missing error")
148 self
.unit
.status
= BlockedStatus(exc
.message
)
151 self
.unit
.status
= MaintenanceStatus("Assembling pod spec")
153 # Fetch image information
155 self
.unit
.status
= MaintenanceStatus("Fetching image information")
156 image_info
= self
.image
.fetch()
157 except OCIImageResourceError
:
158 self
.unit
.status
= BlockedStatus("Error fetching image information")
162 pod_spec
= make_pod_spec(
169 except ValueError as exc
:
170 logger
.exception("Config/Relation data validation error")
171 self
.unit
.status
= BlockedStatus(str(exc
))
174 if self
.state
.pod_spec
!= pod_spec
:
175 self
.model
.pod
.set_spec(pod_spec
)
176 self
.state
.pod_spec
= pod_spec
178 self
.unit
.status
= ActiveStatus("ready")
181 if __name__
== "__main__":