2 # Copyright 2020 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
23 from ipaddress
import ip_network
24 from typing
import Any
, Callable
, Dict
, List
, NoReturn
25 from urllib
.parse
import urlparse
28 def _validate_max_file_size(max_file_size
: int, site_url
: str) -> bool:
29 """Validate max_file_size.
32 max_file_size (int): maximum file size allowed.
33 site_url (str): endpoint url.
36 bool: True if valid, false otherwise.
41 parsed
= urlparse(site_url
)
43 if not parsed
.scheme
.startswith("http"):
46 if max_file_size
is None:
49 return max_file_size
>= 0
52 def _validate_ip_network(network
: str) -> bool:
53 """Validate IP network.
56 network (str): IP network range.
59 bool: True if valid, false otherwise.
72 def _validate_keystone_config(keystone
: bool, value
: Any
, validator
: Callable
) -> bool:
73 """Validate keystone configurations.
76 keystone (bool): is keystone enabled, true if so, false otherwise.
77 value (Any): value to be validated.
78 validator (Callable): function to validate configuration.
81 bool: true if valid, false otherwise.
86 return validator(value
)
90 config_data
: Dict
[str, Any
], relation_data
: Dict
[str, Any
], keystone
: bool
92 """Validate input data.
95 config_data (Dict[str, Any]): configuration data.
96 relation_data (Dict[str, Any]): relation data.
97 keystone (bool): is keystone to be used.
100 "enable_test": lambda value
, _
: isinstance(value
, bool),
101 "database_commonkey": lambda value
, _
: (
102 isinstance(value
, str) and len(value
) > 1
104 "log_level": lambda value
, _
: (
105 isinstance(value
, str) and value
in ("INFO", "DEBUG")
107 "auth_backend": lambda value
, _
: (
108 isinstance(value
, str) and (value
== "internal" or value
== "keystone")
110 "site_url": lambda value
, _
: isinstance(value
, str)
113 "max_file_size": lambda value
, values
: _validate_max_file_size(
114 value
, values
.get("site_url")
116 "ingress_whitelist_source_range": lambda value
, _
: _validate_ip_network(value
),
117 "tls_secret_name": lambda value
, _
: isinstance(value
, str)
121 relation_validators
= {
122 "message_host": lambda value
, _
: isinstance(value
, str),
123 "message_port": lambda value
, _
: isinstance(value
, int) and value
> 0,
124 "database_uri": lambda value
, _
: (
125 isinstance(value
, str) and value
.startswith("mongodb://")
127 "prometheus_host": lambda value
, _
: isinstance(value
, str),
128 "prometheus_port": lambda value
, _
: isinstance(value
, int) and value
> 0,
129 "keystone_host": lambda value
, _
: _validate_keystone_config(
130 keystone
, value
, lambda x
: isinstance(x
, str) and len(x
) > 0
132 "keystone_port": lambda value
, _
: _validate_keystone_config(
133 keystone
, value
, lambda x
: isinstance(x
, int) and x
> 0
135 "keystone_user_domain_name": lambda value
, _
: _validate_keystone_config(
136 keystone
, value
, lambda x
: isinstance(x
, str) and len(x
) > 0
138 "keystone_project_domain_name": lambda value
, _
: _validate_keystone_config(
139 keystone
, value
, lambda x
: isinstance(x
, str) and len(x
) > 0
141 "keystone_username": lambda value
, _
: _validate_keystone_config(
142 keystone
, value
, lambda x
: isinstance(x
, str) and len(x
) > 0
144 "keystone_password": lambda value
, _
: _validate_keystone_config(
145 keystone
, value
, lambda x
: isinstance(x
, str) and len(x
) > 0
147 "keystone_service": lambda value
, _
: _validate_keystone_config(
148 keystone
, value
, lambda x
: isinstance(x
, str) and len(x
) > 0
153 for key
, validator
in config_validators
.items():
154 valid
= validator(config_data
.get(key
), config_data
)
159 for key
, validator
in relation_validators
.items():
160 valid
= validator(relation_data
.get(key
), relation_data
)
165 if len(problems
) > 0:
166 raise ValueError("Errors found in: {}".format(", ".join(problems
)))
169 def _make_pod_ports(port
: int) -> List
[Dict
[str, Any
]]:
170 """Generate pod ports details.
173 port (int): port to expose.
176 List[Dict[str, Any]]: pod port details.
178 return [{"name": "nbi", "containerPort": port
, "protocol": "TCP"}]
181 def _make_pod_envconfig(
182 config
: Dict
[str, Any
], relation_state
: Dict
[str, Any
]
184 """Generate pod environment configuration.
187 config (Dict[str, Any]): configuration information.
188 relation_state (Dict[str, Any]): relation state information.
191 Dict[str, Any]: pod environment configuration.
194 # General configuration
195 "ALLOW_ANONYMOUS_LOGIN": "yes",
196 "OSMNBI_SERVER_ENABLE_TEST": config
["enable_test"],
197 "OSMNBI_STATIC_DIR": "/app/osm_nbi/html_public",
198 # Kafka configuration
199 "OSMNBI_MESSAGE_HOST": relation_state
["message_host"],
200 "OSMNBI_MESSAGE_DRIVER": "kafka",
201 "OSMNBI_MESSAGE_PORT": relation_state
["message_port"],
202 # Database configuration
203 "OSMNBI_DATABASE_DRIVER": "mongo",
204 "OSMNBI_DATABASE_URI": relation_state
["database_uri"],
205 "OSMNBI_DATABASE_COMMONKEY": config
["database_commonkey"],
206 # Storage configuration
207 "OSMNBI_STORAGE_DRIVER": "mongo",
208 "OSMNBI_STORAGE_PATH": "/app/storage",
209 "OSMNBI_STORAGE_COLLECTION": "files",
210 "OSMNBI_STORAGE_URI": relation_state
["database_uri"],
211 # Prometheus configuration
212 "OSMNBI_PROMETHEUS_HOST": relation_state
["prometheus_host"],
213 "OSMNBI_PROMETHEUS_PORT": relation_state
["prometheus_port"],
215 "OSMNBI_LOG_LEVEL": config
["log_level"],
218 if config
["auth_backend"] == "internal":
219 envconfig
["OSMNBI_AUTHENTICATION_BACKEND"] = "internal"
220 elif config
["auth_backend"] == "keystone":
223 "OSMNBI_AUTHENTICATION_BACKEND": "keystone",
224 "OSMNBI_AUTHENTICATION_AUTH_URL": relation_state
["keystone_host"],
225 "OSMNBI_AUTHENTICATION_AUTH_PORT": relation_state
["keystone_port"],
226 "OSMNBI_AUTHENTICATION_USER_DOMAIN_NAME": relation_state
[
227 "keystone_user_domain_name"
229 "OSMNBI_AUTHENTICATION_PROJECT_DOMAIN_NAME": relation_state
[
230 "keystone_project_domain_name"
232 "OSMNBI_AUTHENTICATION_SERVICE_USERNAME": relation_state
[
235 "OSMNBI_AUTHENTICATION_SERVICE_PASSWORD": relation_state
[
238 "OSMNBI_AUTHENTICATION_SERVICE_PROJECT": relation_state
[
244 raise ValueError("auth_backend needs to be either internal or keystone")
249 def _make_pod_ingress_resources(
250 config
: Dict
[str, Any
], app_name
: str, port
: int
251 ) -> List
[Dict
[str, Any
]]:
252 """Generate pod ingress resources.
255 config (Dict[str, Any]): configuration information.
256 app_name (str): application name.
257 port (int): port to expose.
260 List[Dict[str, Any]]: pod ingress resources.
262 site_url
= config
.get("site_url")
267 parsed
= urlparse(site_url
)
269 if not parsed
.scheme
.startswith("http"):
272 max_file_size
= config
["max_file_size"]
273 ingress_whitelist_source_range
= config
["ingress_whitelist_source_range"]
276 "nginx.ingress.kubernetes.io/proxy-body-size": "{}".format(
277 str(max_file_size
) + "m" if max_file_size
> 0 else max_file_size
279 "nginx.ingress.kubernetes.io/backend-protocol": "HTTPS",
282 if ingress_whitelist_source_range
:
284 "nginx.ingress.kubernetes.io/whitelist-source-range"
285 ] = ingress_whitelist_source_range
287 ingress_spec_tls
= None
289 if parsed
.scheme
== "https":
290 ingress_spec_tls
= [{"hosts": [parsed
.hostname
]}]
291 tls_secret_name
= config
["tls_secret_name"]
293 ingress_spec_tls
[0]["secretName"] = tls_secret_name
295 annotations
["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
298 "name": "{}-ingress".format(app_name
),
299 "annotations": annotations
,
303 "host": parsed
.hostname
,
309 "serviceName": app_name
,
320 ingress
["spec"]["tls"] = ingress_spec_tls
325 def _make_startup_probe() -> Dict
[str, Any
]:
326 """Generate startup probe.
329 Dict[str, Any]: startup probe.
332 "exec": {"command": ["/usr/bin/pgrep python3"]},
333 "initialDelaySeconds": 60,
338 def _make_readiness_probe(port
: int) -> Dict
[str, Any
]:
339 """Generate readiness probe.
342 port (int): [description]
345 Dict[str, Any]: readiness probe.
352 "initialDelaySeconds": 45,
357 def _make_liveness_probe(port
: int) -> Dict
[str, Any
]:
358 """Generate liveness probe.
361 port (int): [description]
364 Dict[str, Any]: liveness probe.
371 "initialDelaySeconds": 45,
377 image_info
: Dict
[str, str],
378 config
: Dict
[str, Any
],
379 relation_state
: Dict
[str, Any
],
380 app_name
: str = "nbi",
383 """Generate the pod spec information.
386 image_info (Dict[str, str]): Object provided by
387 OCIImageResource("image").fetch().
388 config (Dict[str, Any]): Configuration information.
389 relation_state (Dict[str, Any]): Relation state information.
390 app_name (str, optional): Application name. Defaults to "nbi".
391 port (int, optional): Port for the container. Defaults to 9999.
394 Dict[str, Any]: Pod spec dictionary for the charm.
399 _validate_data(config
, relation_state
, config
.get("auth_backend") == "keystone")
401 ports
= _make_pod_ports(port
)
402 env_config
= _make_pod_envconfig(config
, relation_state
)
403 ingress_resources
= _make_pod_ingress_resources(config
, app_name
, port
)
410 "imageDetails": image_info
,
411 "imagePullPolicy": "Always",
413 "envConfig": env_config
,
416 "kubernetesResources": {
417 "ingressResources": ingress_resources
or [],