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 ipaddress
import ip_network
25 from typing
import Any
, Dict
, List
26 from urllib
.parse
import urlparse
27 from pathlib
import Path
28 from string
import Template
30 logger
= logging
.getLogger(__name__
)
33 def _validate_max_file_size(max_file_size
: int, site_url
: str) -> bool:
34 """Validate max_file_size.
37 max_file_size (int): maximum file size allowed.
38 site_url (str): endpoint url.
41 bool: True if valid, false otherwise.
46 parsed
= urlparse(site_url
)
48 if not parsed
.scheme
.startswith("http"):
51 if max_file_size
is None:
54 return max_file_size
>= 0
57 def _validate_ip_network(network
: str) -> bool:
58 """Validate IP network.
61 network (str): IP network range.
64 bool: True if valid, false otherwise.
77 def _validate_data(config_data
: Dict
[str, Any
], relation_data
: Dict
[str, Any
]) -> bool:
78 """Validates passed information.
81 config_data (Dict[str, Any]): configuration information.
82 relation_data (Dict[str, Any]): relation information
85 ValueError: when config and/or relation data is not valid.
88 "site_url": lambda value
, _
: isinstance(value
, str)
91 "max_file_size": lambda value
, values
: _validate_max_file_size(
92 value
, values
.get("site_url")
94 "ingress_whitelist_source_range": lambda value
, _
: _validate_ip_network(value
),
95 "tls_secret_name": lambda value
, _
: isinstance(value
, str)
99 relation_validators
= {
100 "prometheus_hostname": lambda value
, _
: (
101 isinstance(value
, str) and len(value
) > 0
103 "prometheus_port": lambda value
, _
: (
104 isinstance(value
, str) and len(value
) > 0 and int(value
) > 0
109 for key
, validator
in config_validators
.items():
110 valid
= validator(config_data
.get(key
), config_data
)
115 for key
, validator
in relation_validators
.items():
116 valid
= validator(relation_data
.get(key
), relation_data
)
121 if len(problems
) > 0:
122 logger
.debug(relation_data
)
123 raise ValueError("Errors found in: {}".format(", ".join(problems
)))
128 def _make_pod_ports(port
: int) -> List
[Dict
[str, Any
]]:
129 """Generate pod ports details.
132 port (int): port to expose.
135 List[Dict[str, Any]]: pod port details.
137 return [{"name": "grafana", "containerPort": port
, "protocol": "TCP"}]
140 def _make_pod_envconfig(
141 config
: Dict
[str, Any
], relation_state
: Dict
[str, Any
]
143 """Generate pod environment configuration.
146 config (Dict[str, Any]): configuration information.
147 relation_state (Dict[str, Any]): relation state information.
150 Dict[str, Any]: pod environment configuration.
157 def _make_pod_ingress_resources(
158 config
: Dict
[str, Any
], app_name
: str, port
: int
159 ) -> List
[Dict
[str, Any
]]:
160 """Generate pod ingress resources.
163 config (Dict[str, Any]): configuration information.
164 app_name (str): application name.
165 port (int): port to expose.
168 List[Dict[str, Any]]: pod ingress resources.
170 site_url
= config
.get("site_url")
175 parsed
= urlparse(site_url
)
177 if not parsed
.scheme
.startswith("http"):
180 max_file_size
= config
["max_file_size"]
181 ingress_whitelist_source_range
= config
["ingress_whitelist_source_range"]
184 "nginx.ingress.kubernetes.io/proxy-body-size": "{}".format(
185 str(max_file_size
) + "m" if max_file_size
> 0 else max_file_size
189 if ingress_whitelist_source_range
:
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"]
200 ingress_spec_tls
[0]["secretName"] = tls_secret_name
202 annotations
["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
205 "name": "{}-ingress".format(app_name
),
206 "annotations": annotations
,
210 "host": parsed
.hostname
,
216 "serviceName": app_name
,
227 ingress
["spec"]["tls"] = ingress_spec_tls
233 config
: Dict
[str, Any
], relation
: Dict
[str, Any
]
234 ) -> List
[Dict
[str, Any
]]:
235 """Generating ConfigMap information
238 config (Dict[str, Any]): configuration information.
239 relation (Dict[str, Any]): relation information.
242 List[Dict[str, Any]]: ConfigMap information.
244 template_data
= {**config
, **relation
}
247 if config
.get("osm_dashboards", False):
251 "path": "kafka_exporter_dashboard.json",
252 "content": Path("files/kafka_exporter_dashboard.json").read_text(),
255 "path": "mongodb_exporter_dashboard.json",
257 "files/mongodb_exporter_dashboard.json"
261 "path": "mysql_exporter_dashboard.json",
262 "content": Path("files/mysql_exporter_dashboard.json").read_text(),
265 "path": "nodes_exporter_dashboard.json",
266 "content": Path("files/nodes_exporter_dashboard.json").read_text(),
269 "path": "summary_dashboard.json",
270 "content": Path("files/summary_dashboard.json").read_text(),
277 "path": "dashboard_osm.yaml",
278 "content": Path("files/default_dashboards.yaml").read_text(),
284 "name": "dashboards",
285 "mountPath": "/etc/grafana/provisioning/dashboards/",
289 "name": "datasources",
290 "mountPath": "/etc/grafana/provisioning/datasources/",
293 "path": "datasource_prometheus.yaml",
295 Path("files/default_dashboards.yaml").read_text()
296 ).substitute(template_data
),
305 def _make_readiness_probe(port
: int) -> Dict
[str, Any
]:
306 """Generate readiness probe.
309 port (int): service port.
312 Dict[str, Any]: readiness probe.
316 "path": "/api/health",
319 "initialDelaySeconds": 10,
322 "successThreshold": 1,
323 "failureThreshold": 3,
327 def _make_liveness_probe(port
: int) -> Dict
[str, Any
]:
328 """Generate liveness probe.
331 port (int): service port.
334 Dict[str, Any]: liveness probe.
338 "path": "/api/health",
341 "initialDelaySeconds": 60,
342 "timeoutSeconds": 30,
343 "failureThreshold": 10,
348 image_info
: Dict
[str, str],
349 config
: Dict
[str, Any
],
350 relation_state
: Dict
[str, Any
],
351 app_name
: str = "grafana",
354 """Generate the pod spec information.
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.
365 Dict[str, Any]: Pod spec dictionary for the charm.
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
)
384 "imageDetails": image_info
,
385 "imagePullPolicy": "Always",
387 "envConfig": env_config
,
388 "volumeConfig": files
,
390 "readinessProbe": readiness_probe
,
391 "livenessProbe": liveness_probe
,
395 "kubernetesResources": {
396 "ingressResources": ingress_resources
or [],