Major improvement in OSM charms
[osm/devops.git] / installers / charm / ng-ui / src / pod_spec.py
1 #!/usr/bin/env python3
2 # Copyright 2020 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,E0611
24
25
26 import logging
27 from pydantic import (
28 BaseModel,
29 conint,
30 IPvAnyNetwork,
31 PositiveInt,
32 validator,
33 )
34 from typing import Any, Dict, List, Optional
35 from urllib.parse import urlparse
36 from pathlib import Path
37 from string import Template
38
39 logger = logging.getLogger(__name__)
40
41
42 class ConfigData(BaseModel):
43 """Configuration data model."""
44
45 port: PositiveInt
46 site_url: Optional[str]
47 max_file_size: Optional[conint(ge=0)]
48 ingress_whitelist_source_range: Optional[IPvAnyNetwork]
49 tls_secret_name: Optional[str]
50
51 @validator("max_file_size", pre=True, always=True)
52 def validate_max_file_size(cls, value, values, **kwargs):
53 site_url = values.get("site_url")
54
55 if not site_url:
56 return value
57
58 parsed = urlparse(site_url)
59
60 if not parsed.scheme.startswith("http"):
61 return value
62
63 if value is None:
64 raise ValueError("max_file_size needs to be defined if site_url is defined")
65
66 return value
67
68 @validator("ingress_whitelist_source_range", pre=True, always=True)
69 def validate_ingress_whitelist_source_range(cls, value, values, **kwargs):
70 if not value:
71 return None
72
73 return value
74
75
76 class RelationData(BaseModel):
77 """Relation data model."""
78
79 nbi_host: str
80 nbi_port: PositiveInt
81
82
83 def _make_pod_ports(port: int) -> List[Dict[str, Any]]:
84 """Generate pod ports details.
85
86 Args:
87 port (int): Port to expose.
88
89 Returns:
90 List[Dict[str, Any]]: pod port details.
91 """
92 return [
93 {"name": "http", "containerPort": port, "protocol": "TCP"},
94 ]
95
96
97 def _make_pod_ingress_resources(
98 config: Dict[str, Any], app_name: str, port: int
99 ) -> List[Dict[str, Any]]:
100 """Generate pod ingress resources.
101
102 Args:
103 config (Dict[str, Any]): configuration information.
104 app_name (str): application name.
105 port (int): port to expose.
106
107 Returns:
108 List[Dict[str, Any]]: pod ingress resources.
109 """
110 site_url = config.get("site_url")
111
112 if not site_url:
113 return
114
115 parsed = urlparse(site_url)
116
117 if not parsed.scheme.startswith("http"):
118 return
119
120 max_file_size = config["max_file_size"]
121 ingress_whitelist_source_range = config["ingress_whitelist_source_range"]
122
123 annotations = {
124 "nginx.ingress.kubernetes.io/proxy-body-size": "{}".format(
125 str(max_file_size) + "m" if max_file_size > 0 else max_file_size
126 ),
127 }
128
129 if ingress_whitelist_source_range:
130 annotations[
131 "nginx.ingress.kubernetes.io/whitelist-source-range"
132 ] = ingress_whitelist_source_range
133
134 ingress_spec_tls = None
135
136 if parsed.scheme == "https":
137 ingress_spec_tls = [{"hosts": [parsed.hostname]}]
138 tls_secret_name = config["tls_secret_name"]
139 if tls_secret_name:
140 ingress_spec_tls[0]["secretName"] = tls_secret_name
141 else:
142 annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
143
144 ingress = {
145 "name": "{}-ingress".format(app_name),
146 "annotations": annotations,
147 "spec": {
148 "rules": [
149 {
150 "host": parsed.hostname,
151 "http": {
152 "paths": [
153 {
154 "path": "/",
155 "backend": {
156 "serviceName": app_name,
157 "servicePort": port,
158 },
159 }
160 ]
161 },
162 }
163 ]
164 },
165 }
166 if ingress_spec_tls:
167 ingress["spec"]["tls"] = ingress_spec_tls
168
169 return [ingress]
170
171
172 def _make_startup_probe() -> Dict[str, Any]:
173 """Generate startup probe.
174
175 Returns:
176 Dict[str, Any]: startup probe.
177 """
178 return {
179 "exec": {"command": ["/usr/bin/pgrep python3"]},
180 "initialDelaySeconds": 60,
181 "timeoutSeconds": 5,
182 }
183
184
185 def _make_readiness_probe(port: int) -> Dict[str, Any]:
186 """Generate readiness probe.
187
188 Args:
189 port (int): [description]
190
191 Returns:
192 Dict[str, Any]: readiness probe.
193 """
194 return {
195 "tcpSocket": {
196 "port": port,
197 },
198 "initialDelaySeconds": 45,
199 "timeoutSeconds": 5,
200 }
201
202
203 def _make_liveness_probe(port: int) -> Dict[str, Any]:
204 """Generate liveness probe.
205
206 Args:
207 port (int): [description]
208
209 Returns:
210 Dict[str, Any]: liveness probe.
211 """
212 return {
213 "tcpSocket": {
214 "port": port,
215 },
216 "initialDelaySeconds": 45,
217 "timeoutSeconds": 5,
218 }
219
220
221 def _make_pod_volume_config(
222 config: Dict[str, Any],
223 relation_state: Dict[str, Any],
224 ) -> List[Dict[str, Any]]:
225 """Generate volume config with files.
226
227 Args:
228 config (Dict[str, Any]): configuration information.
229
230 Returns:
231 Dict[str, Any]: volume config.
232 """
233 template_data = {**config, **relation_state}
234 template_data["max_file_size"] = f'{template_data["max_file_size"]}M'
235 return [
236 {
237 "name": "configuration",
238 "mountPath": "/etc/nginx/sites-available/",
239 "files": [
240 {
241 "path": "default",
242 "content": Template(Path("files/default").read_text()).substitute(
243 template_data
244 ),
245 }
246 ],
247 }
248 ]
249
250
251 def make_pod_spec(
252 image_info: Dict[str, str],
253 config: Dict[str, Any],
254 relation_state: Dict[str, Any],
255 app_name: str = "ng-ui",
256 ) -> Dict[str, Any]:
257 """Generate the pod spec information.
258
259 Args:
260 image_info (Dict[str, str]): Object provided by
261 OCIImageResource("image").fetch().
262 config (Dict[str, Any]): Configuration information.
263 relation_state (Dict[str, Any]): Relation state information.
264 app_name (str, optional): Application name. Defaults to "ng-ui".
265 port (int, optional): Port for the container. Defaults to 80.
266
267 Returns:
268 Dict[str, Any]: Pod spec dictionary for the charm.
269 """
270 if not image_info:
271 return None
272
273 ConfigData(**(config))
274 RelationData(**(relation_state))
275
276 ports = _make_pod_ports(config["port"])
277 ingress_resources = _make_pod_ingress_resources(config, app_name, config["port"])
278 kubernetes = {
279 # "startupProbe": _make_startup_probe(),
280 "readinessProbe": _make_readiness_probe(config["port"]),
281 "livenessProbe": _make_liveness_probe(config["port"]),
282 }
283 volume_config = _make_pod_volume_config(config, relation_state)
284 return {
285 "version": 3,
286 "containers": [
287 {
288 "name": app_name,
289 "imageDetails": image_info,
290 "imagePullPolicy": "Always",
291 "ports": ports,
292 "kubernetes": kubernetes,
293 "volumeConfig": volume_config,
294 }
295 ],
296 "kubernetesResources": {
297 "ingressResources": ingress_resources or [],
298 },
299 }