Newer
Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
#!/usr/bin/env python3
# Copyright 2021 Canonical Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
# For those usages not covered by the Apache License, Version 2.0 please
# contact: legal@canonical.com
#
# To get in touch with the maintainers, please contact:
# osm-charmers@lists.launchpad.net
##
import logging
from typing import Dict, List, NoReturn
from ops.charm import CharmBase
from ops.framework import StoredState
from ops.main import main
from ops.model import ActiveStatus, Application, BlockedStatus, MaintenanceStatus, Unit
from oci_image import OCIImageResource, OCIImageResourceError
from pod_spec import make_pod_spec
logger = logging.getLogger(__name__)
GRAFANA_PORT = 3000
class RelationsMissing(Exception):
def __init__(self, missing_relations: List):
self.message = ""
if missing_relations and isinstance(missing_relations, list):
self.message += f'Waiting for {", ".join(missing_relations)} relation'
if "," in self.message:
self.message += "s"
class RelationDefinition:
def __init__(self, relation_name: str, keys: List, source_type):
if source_type != Application and source_type != Unit:
raise TypeError(
"source_type should be ops.model.Application or ops.model.Unit"
)
self.relation_name = relation_name
self.keys = keys
self.source_type = source_type
def check_missing_relation_data(
data: Dict,
expected_relations_data: List[RelationDefinition],
):
missing_relations = []
for relation_data in expected_relations_data:
if not all(
f"{relation_data.relation_name}_{k}" in data for k in relation_data.keys
):
missing_relations.append(relation_data.relation_name)
if missing_relations:
raise RelationsMissing(missing_relations)
def get_relation_data(
charm: CharmBase,
relation_data: RelationDefinition,
) -> Dict:
data = {}
relation = charm.model.get_relation(relation_data.relation_name)
if relation:
self_app_unit = (
charm.app if relation_data.source_type == Application else charm.unit
)
expected_type = relation_data.source_type
for app_unit in relation.data:
if app_unit != self_app_unit and isinstance(app_unit, expected_type):
if all(k in relation.data[app_unit] for k in relation_data.keys):
for k in relation_data.keys:
data[f"{relation_data.relation_name}_{k}"] = relation.data[
app_unit
].get(k)
break
return data
class GrafanaCharm(CharmBase):
"""Grafana Charm."""
state = StoredState()
def __init__(self, *args) -> NoReturn:
"""Grafana Charm constructor."""
super().__init__(*args)
# Internal state initialization
self.state.set_default(pod_spec=None)
self.port = GRAFANA_PORT
self.image = OCIImageResource(self, "image")
# Registering regular events
self.framework.observe(self.on.start, self.configure_pod)
self.framework.observe(self.on.config_changed, self.configure_pod)
# Registering required relation events
self.framework.observe(self.on.prometheus_relation_changed, self.configure_pod)
# Registering required relation broken events
self.framework.observe(self.on.prometheus_relation_broken, self.configure_pod)
@property
def relations_requirements(self):
return [RelationDefinition("prometheus", ["host", "port"], Unit)]
def get_relation_state(self):
relation_state = {}
for relation_requirements in self.relations_requirements:
data = get_relation_data(self, relation_requirements)
relation_state = {**relation_state, **data}
check_missing_relation_data(relation_state, self.relations_requirements)
return relation_state
def configure_pod(self, _=None) -> NoReturn:
"""Assemble the pod spec and apply it, if possible.
Args:
event (EventBase): Hook or Relation event that started the
function.
"""
if not self.unit.is_leader():
self.unit.status = ActiveStatus("ready")
return
relation_state = None
try:
relation_state = self.get_relation_state()
except RelationsMissing as exc:
logger.exception("Relation missing error")
self.unit.status = BlockedStatus(exc.message)
return
self.unit.status = MaintenanceStatus("Assembling pod spec")
# Fetch image information
try:
self.unit.status = MaintenanceStatus("Fetching image information")
image_info = self.image.fetch()
except OCIImageResourceError:
self.unit.status = BlockedStatus("Error fetching image information")
return
try:
pod_spec = make_pod_spec(
image_info,
self.model.config,
relation_state,
self.model.app.name,
self.port,
)
except ValueError as exc:
logger.exception("Config/Relation data validation error")
self.unit.status = BlockedStatus(str(exc))
return
if self.state.pod_spec != pod_spec:
self.model.pod.set_spec(pod_spec)
self.state.pod_spec = pod_spec
self.unit.status = ActiveStatus("ready")
if __name__ == "__main__":
main(GrafanaCharm)