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
24 from pydantic
import (
33 from typing
import Any
, Dict
, List
, Optional
34 from urllib
.parse
import urlparse
36 logger
= logging
.getLogger(__name__
)
39 class ConfigData(BaseModel
):
40 """Configuration data model."""
43 database_commonkey
: constr(min_length
=1)
44 log_level
: constr(regex
=r
"^(INFO|DEBUG)$")
45 auth_backend
: constr(regex
=r
"^(internal|keystone)$")
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]
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")
58 parsed
= urlparse(site_url
)
60 if not parsed
.scheme
.startswith("http"):
64 raise ValueError("max_file_size needs to be defined if site_url is defined")
68 @validator("ingress_whitelist_source_range", pre
=True, always
=True)
69 def validate_ingress_whitelist_source_range(cls
, value
, values
, **kwargs
):
76 class RelationData(BaseModel
):
77 """Relation data model."""
80 message_port
: PositiveInt
81 database_uri
: constr(regex
=r
"^(mongo://)")
83 prometheus_port
: PositiveInt
85 keystone_host
: Optional
[constr(min_length
=1)]
86 keystone_port
: Optional
[PositiveInt
]
87 keystone_user_domain_name
: Optional
[constr(min_length
=1)]
88 keystone_project_domain_name
: Optional
[constr(min_length
=1)]
89 keystone_username
: Optional
[constr(min_length
=1)]
90 keystone_password
: Optional
[constr(min_length
=1)]
91 keystone_service
: Optional
[constr(min_length
=1)]
93 @validator("keystone_host", pre
=True, always
=True)
94 def validate_keystone_host(cls
, value
, values
, **kwargs
):
95 keystone
= values
.get("keystone")
102 "keystone_host needs to be defined if keystone is configured"
107 @validator("keystone_port", pre
=True, always
=True)
108 def validate_keystone_port(cls
, value
, values
, **kwargs
):
109 keystone
= values
.get("keystone")
116 "keystone_port needs to be defined if keystone is configured"
121 @validator("keystone_user_domain_name", pre
=True, always
=True)
122 def validate_keystone_user_domain_name(cls
, value
, values
, **kwargs
):
123 keystone
= values
.get("keystone")
130 "keystone_user_domain_name needs to be defined if keystone is configured"
135 @validator("keystone_project_domain_name", pre
=True, always
=True)
136 def validate_keystone_project_domain_name(cls
, value
, values
, **kwargs
):
137 keystone
= values
.get("keystone")
144 "keystone_project_domain_name needs to be defined if keystone is configured"
149 @validator("keystone_username", pre
=True, always
=True)
150 def validate_keystone_username(cls
, value
, values
, **kwargs
):
151 keystone
= values
.get("keystone")
158 "keystone_username needs to be defined if keystone is configured"
163 @validator("keystone_password", pre
=True, always
=True)
164 def validate_keystone_password(cls
, value
, values
, **kwargs
):
165 keystone
= values
.get("keystone")
172 "keystone_password needs to be defined if keystone is configured"
177 @validator("keystone_service", pre
=True, always
=True)
178 def validate_keystone_service(cls
, value
, values
, **kwargs
):
179 keystone
= values
.get("keystone")
186 "keystone_service needs to be defined if keystone is configured"
192 def _make_pod_ports(port
: int) -> List
[Dict
[str, Any
]]:
193 """Generate pod ports details.
196 port (int): port to expose.
199 List[Dict[str, Any]]: pod port details.
201 return [{"name": "nbi", "containerPort": port
, "protocol": "TCP"}]
204 def _make_pod_envconfig(
205 config
: Dict
[str, Any
], relation_state
: Dict
[str, Any
]
207 """Generate pod environment configuration.
210 config (Dict[str, Any]): configuration information.
211 relation_state (Dict[str, Any]): relation state information.
214 Dict[str, Any]: pod environment configuration.
217 # General configuration
218 "ALLOW_ANONYMOUS_LOGIN": "yes",
219 "OSMNBI_SERVER_ENABLE_TEST": config
["enable_test"],
220 "OSMNBI_STATIC_DIR": "/app/osm_nbi/html_public",
221 # Kafka configuration
222 "OSMNBI_MESSAGE_HOST": relation_state
["message_host"],
223 "OSMNBI_MESSAGE_DRIVER": "kafka",
224 "OSMNBI_MESSAGE_PORT": relation_state
["message_port"],
225 # Database configuration
226 "OSMNBI_DATABASE_DRIVER": "mongo",
227 "OSMNBI_DATABASE_URI": relation_state
["database_uri"],
228 "OSMNBI_DATABASE_COMMONKEY": config
["database_commonkey"],
229 # Storage configuration
230 "OSMNBI_STORAGE_DRIVER": "mongo",
231 "OSMNBI_STORAGE_PATH": "/app/storage",
232 "OSMNBI_STORAGE_COLLECTION": "files",
233 "OSMNBI_STORAGE_URI": relation_state
["database_uri"],
234 # Prometheus configuration
235 "OSMNBI_PROMETHEUS_HOST": relation_state
["prometheus_host"],
236 "OSMNBI_PROMETHEUS_PORT": relation_state
["prometheus_port"],
238 "OSMNBI_LOG_LEVEL": config
["log_level"],
241 if config
["auth_backend"] == "internal":
242 envconfig
["OSMNBI_AUTHENTICATION_BACKEND"] = "internal"
243 elif config
["auth_backend"] == "keystone":
246 "OSMNBI_AUTHENTICATION_BACKEND": "keystone",
247 "OSMNBI_AUTHENTICATION_AUTH_URL": relation_state
["keystone_host"],
248 "OSMNBI_AUTHENTICATION_AUTH_PORT": relation_state
["keystone_port"],
249 "OSMNBI_AUTHENTICATION_USER_DOMAIN_NAME": relation_state
[
250 "keystone_user_domain_name"
252 "OSMNBI_AUTHENTICATION_PROJECT_DOMAIN_NAME": relation_state
[
253 "keystone_project_domain_name"
255 "OSMNBI_AUTHENTICATION_SERVICE_USERNAME": relation_state
[
258 "OSMNBI_AUTHENTICATION_SERVICE_PASSWORD": relation_state
[
261 "OSMNBI_AUTHENTICATION_SERVICE_PROJECT": relation_state
[
267 raise ValueError("auth_backend needs to be either internal or keystone")
272 def _make_pod_ingress_resources(
273 config
: Dict
[str, Any
], app_name
: str, port
: int
274 ) -> List
[Dict
[str, Any
]]:
275 """Generate pod ingress resources.
278 config (Dict[str, Any]): configuration information.
279 app_name (str): application name.
280 port (int): port to expose.
283 List[Dict[str, Any]]: pod ingress resources.
285 site_url
= config
.get("site_url")
290 parsed
= urlparse(site_url
)
292 if not parsed
.scheme
.startswith("http"):
295 max_file_size
= config
["max_file_size"]
296 ingress_whitelist_source_range
= config
["ingress_whitelist_source_range"]
299 "nginx.ingress.kubernetes.io/proxy-body-size": "{}".format(
300 max_file_size
+ "m" if max_file_size
> 0 else max_file_size
302 "nginx.ingress.kubernetes.io/backend-protocol": "HTTPS",
305 if ingress_whitelist_source_range
:
307 "nginx.ingress.kubernetes.io/whitelist-source-range"
308 ] = ingress_whitelist_source_range
310 ingress_spec_tls
= None
312 if parsed
.scheme
== "https":
313 ingress_spec_tls
= [{"hosts": [parsed
.hostname
]}]
314 tls_secret_name
= config
["tls_secret_name"]
316 ingress_spec_tls
[0]["secretName"] = tls_secret_name
318 annotations
["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
321 "name": "{}-ingress".format(app_name
),
322 "annotations": annotations
,
326 "host": parsed
.hostname
,
332 "serviceName": app_name
,
343 ingress
["spec"]["tls"] = ingress_spec_tls
348 def _make_startup_probe() -> Dict
[str, Any
]:
349 """Generate startup probe.
352 Dict[str, Any]: startup probe.
355 "exec": {"command": ["/usr/bin/pgrep python3"]},
356 "initialDelaySeconds": 60,
361 def _make_readiness_probe(port
: int) -> Dict
[str, Any
]:
362 """Generate readiness probe.
365 port (int): [description]
368 Dict[str, Any]: readiness probe.
375 "initialDelaySeconds": 45,
380 def _make_liveness_probe(port
: int) -> Dict
[str, Any
]:
381 """Generate liveness probe.
384 port (int): [description]
387 Dict[str, Any]: liveness probe.
394 "initialDelaySeconds": 45,
400 image_info
: Dict
[str, str],
401 config
: Dict
[str, Any
],
402 relation_state
: Dict
[str, Any
],
403 app_name
: str = "nbi",
406 """Generate the pod spec information.
409 image_info (Dict[str, str]): Object provided by
410 OCIImageResource("image").fetch().
411 config (Dict[str, Any]): Configuration information.
412 relation_state (Dict[str, Any]): Relation state information.
413 app_name (str, optional): Application name. Defaults to "nbi".
414 port (int, optional): Port for the container. Defaults to 9999.
417 Dict[str, Any]: Pod spec dictionary for the charm.
422 ConfigData(**(config
))
425 keystone
=True if config
.get("auth_backend") == "keystone" else False,
428 ports
= _make_pod_ports(port
)
429 env_config
= _make_pod_envconfig(config
, relation_state
)
430 ingress_resources
= _make_pod_ingress_resources(config
, app_name
, port
)
437 "imageDetails": image_info
,
438 "imagePullPolicy": "Always",
440 "envConfig": env_config
,
443 "kubernetesResources": {
444 "ingressResources": ingress_resources
or [],