Moving exporter charms to use opslib
[osm/devops.git] / installers / charm / kafka-exporter / src / pod_spec.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 from ipaddress import ip_network
24 import logging
25 from typing import Any, Dict, List
26 from urllib.parse import urlparse
27
28 logger = logging.getLogger(__name__)
29
30
31 def _validate_ip_network(network: str) -> bool:
32 """Validate IP network.
33
34 Args:
35 network (str): IP network range.
36
37 Returns:
38 bool: True if valid, false otherwise.
39 """
40 if not network:
41 return True
42
43 try:
44 ip_network(network)
45 except ValueError:
46 return False
47
48 return True
49
50
51 def _validate_data(config_data: Dict[str, Any], relation_data: Dict[str, Any]) -> bool:
52 """Validates passed information.
53
54 Args:
55 config_data (Dict[str, Any]): configuration information.
56 relation_data (Dict[str, Any]): relation information
57
58 Raises:
59 ValueError: when config and/or relation data is not valid.
60 """
61 config_validators = {
62 "site_url": lambda value, _: isinstance(value, str)
63 if value is not None
64 else True,
65 "cluster_issuer": lambda value, _: isinstance(value, str)
66 if value is not None
67 else True,
68 "ingress_whitelist_source_range": lambda value, _: _validate_ip_network(value),
69 "tls_secret_name": lambda value, _: isinstance(value, str)
70 if value is not None
71 else True,
72 }
73 relation_validators = {
74 "kafka_host": lambda value, _: isinstance(value, str) and len(value) > 0,
75 "kafka_port": lambda value, _: isinstance(value, str)
76 and len(value) > 0
77 and int(value) > 0,
78 }
79 problems = []
80
81 for key, validator in config_validators.items():
82 valid = validator(config_data.get(key), config_data)
83
84 if not valid:
85 problems.append(key)
86
87 for key, validator in relation_validators.items():
88 valid = validator(relation_data.get(key), relation_data)
89
90 if not valid:
91 problems.append(key)
92
93 if len(problems) > 0:
94 raise ValueError("Errors found in: {}".format(", ".join(problems)))
95
96 return True
97
98
99 def _make_pod_ports(port: int) -> List[Dict[str, Any]]:
100 """Generate pod ports details.
101
102 Args:
103 port (int): port to expose.
104
105 Returns:
106 List[Dict[str, Any]]: pod port details.
107 """
108 return [{"name": "kafka-exporter", "containerPort": port, "protocol": "TCP"}]
109
110
111 def _make_pod_envconfig(
112 config: Dict[str, Any], relation_state: Dict[str, Any]
113 ) -> Dict[str, Any]:
114 """Generate pod environment configuration.
115
116 Args:
117 config (Dict[str, Any]): configuration information.
118 relation_state (Dict[str, Any]): relation state information.
119
120 Returns:
121 Dict[str, Any]: pod environment configuration.
122 """
123 envconfig = {}
124
125 return envconfig
126
127
128 def _make_pod_ingress_resources(
129 config: Dict[str, Any], app_name: str, port: int
130 ) -> List[Dict[str, Any]]:
131 """Generate pod ingress resources.
132
133 Args:
134 config (Dict[str, Any]): configuration information.
135 app_name (str): application name.
136 port (int): port to expose.
137
138 Returns:
139 List[Dict[str, Any]]: pod ingress resources.
140 """
141 site_url = config.get("site_url")
142
143 if not site_url:
144 return
145
146 parsed = urlparse(site_url)
147
148 if not parsed.scheme.startswith("http"):
149 return
150
151 ingress_whitelist_source_range = config["ingress_whitelist_source_range"]
152 cluster_issuer = config["cluster_issuer"]
153
154 annotations = {}
155
156 if ingress_whitelist_source_range:
157 annotations[
158 "nginx.ingress.kubernetes.io/whitelist-source-range"
159 ] = ingress_whitelist_source_range
160
161 if cluster_issuer:
162 annotations["cert-manager.io/cluster-issuer"] = cluster_issuer
163
164 ingress_spec_tls = None
165
166 if parsed.scheme == "https":
167 ingress_spec_tls = [{"hosts": [parsed.hostname]}]
168 tls_secret_name = config["tls_secret_name"]
169 if tls_secret_name:
170 ingress_spec_tls[0]["secretName"] = tls_secret_name
171 else:
172 annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
173
174 ingress = {
175 "name": "{}-ingress".format(app_name),
176 "annotations": annotations,
177 "spec": {
178 "rules": [
179 {
180 "host": parsed.hostname,
181 "http": {
182 "paths": [
183 {
184 "path": "/",
185 "backend": {
186 "serviceName": app_name,
187 "servicePort": port,
188 },
189 }
190 ]
191 },
192 }
193 ]
194 },
195 }
196 if ingress_spec_tls:
197 ingress["spec"]["tls"] = ingress_spec_tls
198
199 return [ingress]
200
201
202 def _make_readiness_probe(port: int) -> Dict[str, Any]:
203 """Generate readiness probe.
204
205 Args:
206 port (int): service port.
207
208 Returns:
209 Dict[str, Any]: readiness probe.
210 """
211 return {
212 "httpGet": {
213 "path": "/api/health",
214 "port": port,
215 },
216 "initialDelaySeconds": 10,
217 "periodSeconds": 10,
218 "timeoutSeconds": 5,
219 "successThreshold": 1,
220 "failureThreshold": 3,
221 }
222
223
224 def _make_liveness_probe(port: int) -> Dict[str, Any]:
225 """Generate liveness probe.
226
227 Args:
228 port (int): service port.
229
230 Returns:
231 Dict[str, Any]: liveness probe.
232 """
233 return {
234 "httpGet": {
235 "path": "/api/health",
236 "port": port,
237 },
238 "initialDelaySeconds": 60,
239 "timeoutSeconds": 30,
240 "failureThreshold": 10,
241 }
242
243
244 def _make_pod_command(relation: Dict[str, Any]) -> List[str]:
245 """Generate the startup command.
246
247 Args:
248 relation (Dict[str, Any]): Relation information.
249
250 Returns:
251 List[str]: command to startup the process.
252 """
253 command = [
254 "kafka_exporter",
255 "--kafka.server={}:{}".format(
256 relation.get("kafka_host"), relation.get("kafka_port")
257 ),
258 ]
259
260 return command
261
262
263 def make_pod_spec(
264 image_info: Dict[str, str],
265 config: Dict[str, Any],
266 relation_state: Dict[str, Any],
267 app_name: str = "kafka-exporter",
268 port: int = 9308,
269 ) -> Dict[str, Any]:
270 """Generate the pod spec information.
271
272 Args:
273 image_info (Dict[str, str]): Object provided by
274 OCIImageResource("image").fetch().
275 config (Dict[str, Any]): Configuration information.
276 relation_state (Dict[str, Any]): Relation state information.
277 app_name (str, optional): Application name. Defaults to "ro".
278 port (int, optional): Port for the container. Defaults to 9090.
279
280 Returns:
281 Dict[str, Any]: Pod spec dictionary for the charm.
282 """
283 if not image_info:
284 return None
285
286 _validate_data(config, relation_state)
287
288 ports = _make_pod_ports(port)
289 env_config = _make_pod_envconfig(config, relation_state)
290 readiness_probe = _make_readiness_probe(port)
291 liveness_probe = _make_liveness_probe(port)
292 ingress_resources = _make_pod_ingress_resources(config, app_name, port)
293 command = _make_pod_command(relation_state)
294
295 return {
296 "version": 3,
297 "containers": [
298 {
299 "name": app_name,
300 "imageDetails": image_info,
301 "imagePullPolicy": "Always",
302 "ports": ports,
303 "envConfig": env_config,
304 "command": command,
305 "kubernetes": {
306 "readinessProbe": readiness_probe,
307 "livenessProbe": liveness_probe,
308 },
309 }
310 ],
311 "kubernetesResources": {
312 "ingressResources": ingress_resources or [],
313 },
314 }