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