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