Coverage for src/pod_spec.py : 0%
Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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##
23import logging
24from ipaddress import ip_network
25from typing import Any, Dict, List
26from urllib.parse import urlparse
27from pathlib import Path
28from string import Template
30logger = logging.getLogger(__name__)
33def _validate_max_file_size(max_file_size: int, site_url: str) -> bool:
34 """Validate max_file_size.
36 Args:
37 max_file_size (int): maximum file size allowed.
38 site_url (str): endpoint url.
40 Returns:
41 bool: True if valid, false otherwise.
42 """
43 if not site_url:
44 return True
46 parsed = urlparse(site_url)
48 if not parsed.scheme.startswith("http"):
49 return True
51 if max_file_size is None:
52 return False
54 return max_file_size >= 0
57def _validate_ip_network(network: str) -> bool:
58 """Validate IP network.
60 Args:
61 network (str): IP network range.
63 Returns:
64 bool: True if valid, false otherwise.
65 """
66 if not network:
67 return True
69 try:
70 ip_network(network)
71 except ValueError:
72 return False
74 return True
77def _validate_data(config_data: Dict[str, Any], relation_data: Dict[str, Any]) -> bool:
78 """Validates passed information.
80 Args:
81 config_data (Dict[str, Any]): configuration information.
82 relation_data (Dict[str, Any]): relation information
84 Raises:
85 ValueError: when config and/or relation data is not valid.
86 """
87 config_validators = {
88 "site_url": lambda value, _: isinstance(value, str)
89 if value is not None
90 else True,
91 "max_file_size": lambda value, values: _validate_max_file_size(
92 value, values.get("site_url")
93 ),
94 "ingress_whitelist_source_range": lambda value, _: _validate_ip_network(value),
95 "tls_secret_name": lambda value, _: isinstance(value, str)
96 if value is not None
97 else True,
98 }
99 relation_validators = {
100 "prometheus_hostname": lambda value, _: (
101 isinstance(value, str) and len(value) > 0
102 ),
103 "prometheus_port": lambda value, _: (
104 isinstance(value, str) and len(value) > 0 and int(value) > 0
105 ),
106 }
107 problems = []
109 for key, validator in config_validators.items():
110 valid = validator(config_data.get(key), config_data)
112 if not valid:
113 problems.append(key)
115 for key, validator in relation_validators.items():
116 valid = validator(relation_data.get(key), relation_data)
118 if not valid:
119 problems.append(key)
121 if len(problems) > 0:
122 logger.debug(relation_data)
123 raise ValueError("Errors found in: {}".format(", ".join(problems)))
125 return True
128def _make_pod_ports(port: int) -> List[Dict[str, Any]]:
129 """Generate pod ports details.
131 Args:
132 port (int): port to expose.
134 Returns:
135 List[Dict[str, Any]]: pod port details.
136 """
137 return [{"name": "grafana", "containerPort": port, "protocol": "TCP"}]
140def _make_pod_envconfig(
141 config: Dict[str, Any], relation_state: Dict[str, Any]
142) -> Dict[str, Any]:
143 """Generate pod environment configuration.
145 Args:
146 config (Dict[str, Any]): configuration information.
147 relation_state (Dict[str, Any]): relation state information.
149 Returns:
150 Dict[str, Any]: pod environment configuration.
151 """
152 envconfig = {}
154 return envconfig
157def _make_pod_ingress_resources(
158 config: Dict[str, Any], app_name: str, port: int
159) -> List[Dict[str, Any]]:
160 """Generate pod ingress resources.
162 Args:
163 config (Dict[str, Any]): configuration information.
164 app_name (str): application name.
165 port (int): port to expose.
167 Returns:
168 List[Dict[str, Any]]: pod ingress resources.
169 """
170 site_url = config.get("site_url")
172 if not site_url:
173 return
175 parsed = urlparse(site_url)
177 if not parsed.scheme.startswith("http"):
178 return
180 max_file_size = config["max_file_size"]
181 ingress_whitelist_source_range = config["ingress_whitelist_source_range"]
183 annotations = {
184 "nginx.ingress.kubernetes.io/proxy-body-size": "{}".format(
185 str(max_file_size) + "m" if max_file_size > 0 else max_file_size
186 ),
187 }
189 if ingress_whitelist_source_range:
190 annotations[
191 "nginx.ingress.kubernetes.io/whitelist-source-range"
192 ] = ingress_whitelist_source_range
194 ingress_spec_tls = None
196 if parsed.scheme == "https":
197 ingress_spec_tls = [{"hosts": [parsed.hostname]}]
198 tls_secret_name = config["tls_secret_name"]
199 if tls_secret_name:
200 ingress_spec_tls[0]["secretName"] = tls_secret_name
201 else:
202 annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
204 ingress = {
205 "name": "{}-ingress".format(app_name),
206 "annotations": annotations,
207 "spec": {
208 "rules": [
209 {
210 "host": parsed.hostname,
211 "http": {
212 "paths": [
213 {
214 "path": "/",
215 "backend": {
216 "serviceName": app_name,
217 "servicePort": port,
218 },
219 }
220 ]
221 },
222 }
223 ]
224 },
225 }
226 if ingress_spec_tls:
227 ingress["spec"]["tls"] = ingress_spec_tls
229 return [ingress]
232def _make_pod_files(
233 config: Dict[str, Any], relation: Dict[str, Any]
234) -> List[Dict[str, Any]]:
235 """Generating ConfigMap information
237 Args:
238 config (Dict[str, Any]): configuration information.
239 relation (Dict[str, Any]): relation information.
241 Returns:
242 List[Dict[str, Any]]: ConfigMap information.
243 """
244 template_data = {**config, **relation}
245 dashboards = []
247 if config.get("osm_dashboards", False):
248 dashboards.extend(
249 [
250 {
251 "path": "kafka_exporter_dashboard.json",
252 "content": Path("files/kafka_exporter_dashboard.json").read_text(),
253 },
254 {
255 "path": "mongodb_exporter_dashboard.json",
256 "content": Path(
257 "files/mongodb_exporter_dashboard.json"
258 ).read_text(),
259 },
260 {
261 "path": "mysql_exporter_dashboard.json",
262 "content": Path("files/mysql_exporter_dashboard.json").read_text(),
263 },
264 {
265 "path": "nodes_exporter_dashboard.json",
266 "content": Path("files/nodes_exporter_dashboard.json").read_text(),
267 },
268 {
269 "path": "summary_dashboard.json",
270 "content": Path("files/summary_dashboard.json").read_text(),
271 },
272 ]
273 )
275 dashboards.append(
276 {
277 "path": "dashboard_osm.yaml",
278 "content": Path("files/default_dashboards.yaml").read_text(),
279 }
280 )
282 files = [
283 {
284 "name": "dashboards",
285 "mountPath": "/etc/grafana/provisioning/dashboards/",
286 "files": dashboards,
287 },
288 {
289 "name": "datasources",
290 "mountPath": "/etc/grafana/provisioning/datasources/",
291 "files": [
292 {
293 "path": "datasource_prometheus.yaml",
294 "content": Template(
295 Path("files/default_dashboards.yaml").read_text()
296 ).substitute(template_data),
297 }
298 ],
299 },
300 ]
302 return files
305def _make_readiness_probe(port: int) -> Dict[str, Any]:
306 """Generate readiness probe.
308 Args:
309 port (int): service port.
311 Returns:
312 Dict[str, Any]: readiness probe.
313 """
314 return {
315 "httpGet": {
316 "path": "/api/health",
317 "port": port,
318 },
319 "initialDelaySeconds": 10,
320 "periodSeconds": 10,
321 "timeoutSeconds": 5,
322 "successThreshold": 1,
323 "failureThreshold": 3,
324 }
327def _make_liveness_probe(port: int) -> Dict[str, Any]:
328 """Generate liveness probe.
330 Args:
331 port (int): service port.
333 Returns:
334 Dict[str, Any]: liveness probe.
335 """
336 return {
337 "httpGet": {
338 "path": "/api/health",
339 "port": port,
340 },
341 "initialDelaySeconds": 60,
342 "timeoutSeconds": 30,
343 "failureThreshold": 10,
344 }
347def make_pod_spec(
348 image_info: Dict[str, str],
349 config: Dict[str, Any],
350 relation_state: Dict[str, Any],
351 app_name: str = "grafana",
352 port: int = 3000,
353) -> Dict[str, Any]:
354 """Generate the pod spec information.
356 Args:
357 image_info (Dict[str, str]): Object provided by
358 OCIImageResource("image").fetch().
359 config (Dict[str, Any]): Configuration information.
360 relation_state (Dict[str, Any]): Relation state information.
361 app_name (str, optional): Application name. Defaults to "ro".
362 port (int, optional): Port for the container. Defaults to 9090.
364 Returns:
365 Dict[str, Any]: Pod spec dictionary for the charm.
366 """
367 if not image_info:
368 return None
370 _validate_data(config, relation_state)
372 ports = _make_pod_ports(port)
373 env_config = _make_pod_envconfig(config, relation_state)
374 files = _make_pod_files(config, relation_state)
375 readiness_probe = _make_readiness_probe(port)
376 liveness_probe = _make_liveness_probe(port)
377 ingress_resources = _make_pod_ingress_resources(config, app_name, port)
379 return {
380 "version": 3,
381 "containers": [
382 {
383 "name": app_name,
384 "imageDetails": image_info,
385 "imagePullPolicy": "Always",
386 "ports": ports,
387 "envConfig": env_config,
388 "volumeConfig": files,
389 "kubernetes": {
390 "readinessProbe": readiness_probe,
391 "livenessProbe": liveness_probe,
392 },
393 }
394 ],
395 "kubernetesResources": {
396 "ingressResources": ingress_resources or [],
397 },
398 }