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 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__
)
36 PROMETHEUS_PORT
= 9090
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 PrometheusCharm(CharmBase
):
96 """Prometheus Charm."""
100 def __init__(self
, *args
) -> NoReturn
:
101 """Prometheus Charm constructor."""
102 super().__init
__(*args
)
104 # Internal state initialization
105 self
.state
.set_default(pod_spec
=None)
107 self
.port
= PROMETHEUS_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 provided relation events
115 self
.framework
.observe(
116 self
.on
.prometheus_relation_joined
, self
._publish
_prometheus
_info
119 def _publish_prometheus_info(self
, event
: EventBase
) -> NoReturn
:
120 """Publishes Prometheus information.
123 event (EventBase): Prometheus relation event.
125 if self
.unit
.is_leader():
127 "host": self
.model
.app
.name
,
128 "port": str(PROMETHEUS_PORT
),
130 for k
, v
in rel_data
.items():
131 event
.relation
.data
[self
.app
][k
] = v
134 def relations_requirements(self
):
137 def get_relation_state(self
):
139 for relation_requirements
in self
.relations_requirements
:
140 data
= get_relation_data(self
, relation_requirements
)
141 relation_state
= {**relation_state
, **data
}
142 check_missing_relation_data(relation_state
, self
.relations_requirements
)
143 return relation_state
145 def configure_pod(self
, _
=None) -> NoReturn
:
146 """Assemble the pod spec and apply it, if possible.
149 event (EventBase): Hook or Relation event that started the
152 if not self
.unit
.is_leader():
153 self
.unit
.status
= ActiveStatus("ready")
156 relation_state
= None
158 relation_state
= self
.get_relation_state()
159 except RelationsMissing
as exc
:
160 logger
.exception("Relation missing error")
161 self
.unit
.status
= BlockedStatus(exc
.message
)
164 self
.unit
.status
= MaintenanceStatus("Assembling pod spec")
166 # Fetch image information
168 self
.unit
.status
= MaintenanceStatus("Fetching image information")
169 image_info
= self
.image
.fetch()
170 except OCIImageResourceError
:
171 self
.unit
.status
= BlockedStatus("Error fetching image information")
175 pod_spec
= make_pod_spec(
182 except ValueError as exc
:
183 logger
.exception("Config/Relation data validation error")
184 self
.unit
.status
= BlockedStatus(str(exc
))
187 if self
.state
.pod_spec
!= pod_spec
:
188 self
.model
.pod
.set_spec(pod_spec
)
189 self
.state
.pod_spec
= pod_spec
191 self
.unit
.status
= ActiveStatus("ready")
194 if __name__
== "__main__":
195 main(PrometheusCharm
)