d10ccf23335081c0da81b167a37b0ae1422c5cf1
[osm/devops.git] / installers / charm / grafana / src / charm.py
1 #!/usr/bin/env python3
2 # Copyright 2021 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
23 # pylint: disable=E0213
24
25 from ipaddress import ip_network
26 import logging
27 from pathlib import Path
28 from string import Template
29 from typing import NoReturn, Optional
30 from urllib.parse import urlparse
31
32 from ops.main import main
33 from opslib.osm.charm import CharmedOsmBase, RelationsMissing
34 from opslib.osm.interfaces.prometheus import PrometheusClient
35 from opslib.osm.pod import (
36 ContainerV3Builder,
37 FilesV3Builder,
38 IngressResourceV3Builder,
39 PodSpecV3Builder,
40 )
41 from opslib.osm.validator import ModelValidator, validator
42
43
44 logger = logging.getLogger(__name__)
45
46 PORT = 3000
47
48
49 class ConfigModel(ModelValidator):
50 max_file_size: int
51 osm_dashboards: bool
52 site_url: Optional[str]
53 ingress_whitelist_source_range: Optional[str]
54 tls_secret_name: Optional[str]
55
56 @validator("max_file_size")
57 def validate_max_file_size(cls, v):
58 if v < 0:
59 raise ValueError("value must be equal or greater than 0")
60 return v
61
62 @validator("site_url")
63 def validate_site_url(cls, v):
64 if v:
65 parsed = urlparse(v)
66 if not parsed.scheme.startswith("http"):
67 raise ValueError("value must start with http")
68 return v
69
70 @validator("ingress_whitelist_source_range")
71 def validate_ingress_whitelist_source_range(cls, v):
72 if v:
73 ip_network(v)
74 return v
75
76
77 class GrafanaCharm(CharmedOsmBase):
78 """GrafanaCharm Charm."""
79
80 def __init__(self, *args) -> NoReturn:
81 """Prometheus Charm constructor."""
82 super().__init__(*args, oci_image="image")
83
84 self.prometheus_client = PrometheusClient(self, "prometheus")
85 self.framework.observe(
86 self.on["prometheus"].relation_changed, self.configure_pod
87 )
88 self.framework.observe(
89 self.on["prometheus"].relation_broken, self.configure_pod
90 )
91
92 def _build_dashboard_files(self, config: ConfigModel):
93 files_builder = FilesV3Builder()
94 files_builder.add_file(
95 "dashboard_osm.yaml",
96 Path("files/default_dashboards.yaml").read_text(),
97 )
98 if config.osm_dashboards:
99 osm_dashboards_mapping = {
100 "kafka_exporter_dashboard.json": "files/kafka_exporter_dashboard.json",
101 "mongodb_exporter_dashboard.json": "files/mongodb_exporter_dashboard.json",
102 "mysql_exporter_dashboard.json": "files/mysql_exporter_dashboard.json",
103 "nodes_exporter_dashboard.json": "files/nodes_exporter_dashboard.json",
104 "summary_dashboard.json": "files/summary_dashboard.json",
105 }
106 for file_name, path in osm_dashboards_mapping.items():
107 files_builder.add_file(file_name, Path(path).read_text())
108 return files_builder.build()
109
110 def _build_datasources_files(self):
111 files_builder = FilesV3Builder()
112 files_builder.add_file(
113 "datasource_prometheus.yaml",
114 Template(Path("files/default_datasources.yaml").read_text()).substitute(
115 prometheus_host=self.prometheus_client.hostname,
116 prometheus_port=self.prometheus_client.port,
117 ),
118 )
119 return files_builder.build()
120
121 def _check_missing_dependencies(self):
122 missing_relations = []
123
124 if self.prometheus_client.is_missing_data_in_app():
125 missing_relations.append("prometheus")
126
127 if missing_relations:
128 raise RelationsMissing(missing_relations)
129
130 def build_pod_spec(self, image_info):
131 # Validate config
132 config = ConfigModel(**dict(self.config))
133 # Check relations
134 self._check_missing_dependencies()
135 # Create Builder for the PodSpec
136 pod_spec_builder = PodSpecV3Builder()
137 # Build Container
138 container_builder = ContainerV3Builder(self.app.name, image_info)
139 container_builder.add_port(name=self.app.name, port=PORT)
140 container_builder.add_http_readiness_probe(
141 "/api/health",
142 PORT,
143 initial_delay_seconds=10,
144 period_seconds=10,
145 timeout_seconds=5,
146 failure_threshold=3,
147 )
148 container_builder.add_http_liveness_probe(
149 "/api/health",
150 PORT,
151 initial_delay_seconds=60,
152 timeout_seconds=30,
153 failure_threshold=10,
154 )
155 container_builder.add_volume_config(
156 "dashboards",
157 "/etc/grafana/provisioning/dashboards/",
158 self._build_dashboard_files(config),
159 )
160 container_builder.add_volume_config(
161 "datasources",
162 "/etc/grafana/provisioning/datasources/",
163 self._build_datasources_files(),
164 )
165 container = container_builder.build()
166 # Add container to pod spec
167 pod_spec_builder.add_container(container)
168 # Add ingress resources to pod spec if site url exists
169 if config.site_url:
170 parsed = urlparse(config.site_url)
171 annotations = {
172 "nginx.ingress.kubernetes.io/proxy-body-size": "{}".format(
173 str(config.max_file_size) + "m"
174 if config.max_file_size > 0
175 else config.max_file_size
176 ),
177 }
178 ingress_resource_builder = IngressResourceV3Builder(
179 f"{self.app.name}-ingress", annotations
180 )
181
182 if config.ingress_whitelist_source_range:
183 annotations[
184 "nginx.ingress.kubernetes.io/whitelist-source-range"
185 ] = config.ingress_whitelist_source_range
186
187 if parsed.scheme == "https":
188 ingress_resource_builder.add_tls(
189 [parsed.hostname], config.tls_secret_name
190 )
191 else:
192 annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
193
194 ingress_resource_builder.add_rule(parsed.hostname, self.app.name, PORT)
195 ingress_resource = ingress_resource_builder.build()
196 pod_spec_builder.add_ingress_resource(ingress_resource)
197 return pod_spec_builder.build()
198
199
200 if __name__ == "__main__":
201 main(GrafanaCharm)