Fix validation error for ImagePullPolicy in charms
[osm/devops.git] / installers / charm / keystone / src / charm.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 # pylint: disable=E0213
24
25
26 from datetime import datetime
27 from ipaddress import ip_network
28 import json
29 import logging
30 from typing import List, NoReturn, Optional, Tuple
31 from urllib.parse import urlparse
32
33 from cryptography.fernet import Fernet
34 from ops.main import main
35 from opslib.osm.charm import CharmedOsmBase, RelationsMissing
36 from opslib.osm.interfaces.keystone import KeystoneServer
37 from opslib.osm.interfaces.mysql import MysqlClient
38 from opslib.osm.pod import (
39 ContainerV3Builder,
40 FilesV3Builder,
41 IngressResourceV3Builder,
42 PodSpecV3Builder,
43 )
44 from opslib.osm.validator import ModelValidator, validator
45
46
47 logger = logging.getLogger(__name__)
48
49
50 REQUIRED_SETTINGS = ["token_expiration"]
51
52 # This is hardcoded in the keystone container script
53 DATABASE_NAME = "keystone"
54
55 # We expect the keystone container to use the default port
56 PORT = 5000
57
58 # Number of keys need might need to be adjusted in the future
59 NUMBER_FERNET_KEYS = 2
60 NUMBER_CREDENTIAL_KEYS = 2
61
62 # Path for keys
63 CREDENTIAL_KEYS_PATH = "/etc/keystone/credential-keys"
64 FERNET_KEYS_PATH = "/etc/keystone/fernet-keys"
65
66
67 class ConfigModel(ModelValidator):
68 region_id: str
69 keystone_db_password: str
70 admin_username: str
71 admin_password: str
72 admin_project: str
73 service_username: str
74 service_password: str
75 service_project: str
76 user_domain_name: str
77 project_domain_name: str
78 token_expiration: int
79 max_file_size: int
80 site_url: Optional[str]
81 ingress_class: Optional[str]
82 ingress_whitelist_source_range: Optional[str]
83 tls_secret_name: Optional[str]
84 mysql_host: Optional[str]
85 mysql_port: Optional[int]
86 mysql_root_password: Optional[str]
87 image_pull_policy: str
88
89 @validator("max_file_size")
90 def validate_max_file_size(cls, v):
91 if v < 0:
92 raise ValueError("value must be equal or greater than 0")
93 return v
94
95 @validator("site_url")
96 def validate_site_url(cls, v):
97 if v:
98 parsed = urlparse(v)
99 if not parsed.scheme.startswith("http"):
100 raise ValueError("value must start with http")
101 return v
102
103 @validator("ingress_whitelist_source_range")
104 def validate_ingress_whitelist_source_range(cls, v):
105 if v:
106 ip_network(v)
107 return v
108
109 @validator("mysql_port")
110 def validate_mysql_port(cls, v):
111 if v and (v <= 0 or v >= 65535):
112 raise ValueError("Mysql port out of range")
113 return v
114
115 @validator("image_pull_policy")
116 def validate_image_pull_policy(cls, v):
117 values = {
118 "always": "Always",
119 "ifnotpresent": "IfNotPresent",
120 "never": "Never",
121 }
122 v = v.lower()
123 if v not in values.keys():
124 raise ValueError("value must be always, ifnotpresent or never")
125 return values[v]
126
127
128 class ConfigLdapModel(ModelValidator):
129 ldap_enabled: bool
130 ldap_authentication_domain_name: Optional[str]
131 ldap_url: Optional[str]
132 ldap_bind_user: Optional[str]
133 ldap_bind_password: Optional[str]
134 ldap_chase_referrals: Optional[str]
135 ldap_page_size: Optional[int]
136 ldap_user_tree_dn: Optional[str]
137 ldap_user_objectclass: Optional[str]
138 ldap_user_id_attribute: Optional[str]
139 ldap_user_name_attribute: Optional[str]
140 ldap_user_pass_attribute: Optional[str]
141 ldap_user_filter: Optional[str]
142 ldap_user_enabled_attribute: Optional[str]
143 ldap_user_enabled_mask: Optional[int]
144 ldap_user_enabled_default: Optional[str]
145 ldap_user_enabled_invert: Optional[bool]
146 ldap_group_objectclass: Optional[str]
147 ldap_group_tree_dn: Optional[str]
148 ldap_use_starttls: Optional[bool]
149 ldap_tls_cacert_base64: Optional[str]
150 ldap_tls_req_cert: Optional[str]
151
152 @validator
153 def validate_ldap_user_enabled_default(cls, v):
154 if v:
155 if v not in ["true", "false"]:
156 raise ValueError('must be equal to "true" or "false"')
157 return v
158
159
160 class KeystoneCharm(CharmedOsmBase):
161 def __init__(self, *args) -> NoReturn:
162 super().__init__(*args, oci_image="image")
163 self.state.set_default(fernet_keys=None)
164 self.state.set_default(credential_keys=None)
165 self.state.set_default(keys_timestamp=0)
166
167 self.keystone_server = KeystoneServer(self, "keystone")
168 self.mysql_client = MysqlClient(self, "db")
169 self.framework.observe(self.on["db"].relation_changed, self.configure_pod)
170 self.framework.observe(self.on["db"].relation_broken, self.configure_pod)
171
172 self.framework.observe(
173 self.on["keystone"].relation_joined, self._publish_keystone_info
174 )
175
176 def _publish_keystone_info(self, event):
177 if self.unit.is_leader():
178 config = ConfigModel(**dict(self.config))
179 self.keystone_server.publish_info(
180 host=f"http://{self.app.name}:{PORT}/v3",
181 port=PORT,
182 user_domain_name=config.user_domain_name,
183 project_domain_name=config.project_domain_name,
184 username=config.service_username,
185 password=config.service_password,
186 service=config.service_project,
187 keystone_db_password=config.keystone_db_password,
188 region_id=config.region_id,
189 admin_username=config.admin_username,
190 admin_password=config.admin_password,
191 admin_project_name=config.admin_project,
192 )
193
194 def _check_missing_dependencies(self, config: ConfigModel):
195 missing_relations = []
196 if not config.mysql_host and self.mysql_client.is_missing_data_in_unit():
197 missing_relations.append("mysql")
198 if missing_relations:
199 raise RelationsMissing(missing_relations)
200
201 def _validate_mysql_config(self, config: ConfigModel):
202 invalid_values = []
203 if not config.mysql_root_password:
204 invalid_values.append("Mysql root password must be provided")
205
206 if invalid_values:
207 raise ValueError("Invalid values: " + ", ".join(invalid_values))
208
209 def _generate_keys(self) -> Tuple[List[str], List[str]]:
210 """Generating new fernet tokens.
211
212 Returns:
213 Tuple[List[str], List[str]]: contains two lists of strings. First
214 list contains strings that represent
215 the keys for fernet and the second
216 list contains strins that represent
217 the keys for credentials.
218 """
219 fernet_keys = [
220 Fernet.generate_key().decode() for _ in range(NUMBER_FERNET_KEYS)
221 ]
222 credential_keys = [
223 Fernet.generate_key().decode() for _ in range(NUMBER_CREDENTIAL_KEYS)
224 ]
225
226 return (fernet_keys, credential_keys)
227
228 def _get_keys(self):
229 keys_timestamp = self.state.keys_timestamp
230 if fernet_keys := self.state.fernet_keys:
231 fernet_keys = json.loads(fernet_keys)
232
233 if credential_keys := self.state.credential_keys:
234 credential_keys = json.loads(credential_keys)
235
236 now = datetime.now().timestamp()
237 token_expiration = self.config["token_expiration"]
238
239 valid_keys = (now - keys_timestamp) < token_expiration
240 if not credential_keys or not fernet_keys or not valid_keys:
241 fernet_keys, credential_keys = self._generate_keys()
242 self.state.fernet_keys = json.dumps(fernet_keys)
243 self.state.credential_keys = json.dumps(credential_keys)
244 self.state.keys_timestamp = now
245 return credential_keys, fernet_keys
246
247 def _build_files(self, config: ConfigModel):
248 credentials_files_builder = FilesV3Builder()
249 fernet_files_builder = FilesV3Builder()
250
251 credential_keys, fernet_keys = self._get_keys()
252
253 for (key_id, value) in enumerate(credential_keys):
254 credentials_files_builder.add_file(str(key_id), value)
255 for (key_id, value) in enumerate(fernet_keys):
256 fernet_files_builder.add_file(str(key_id), value)
257 return credentials_files_builder.build(), fernet_files_builder.build()
258
259 def build_pod_spec(self, image_info):
260 # Validate config
261 config = ConfigModel(**dict(self.config))
262 config_ldap = ConfigLdapModel(**dict(self.config))
263
264 if config.mysql_host and not self.mysql_client.is_missing_data_in_unit():
265 raise Exception("Mysql data cannot be provided via config and relation")
266
267 if config.mysql_host:
268 self._validate_mysql_config(config)
269
270 # Check relations
271 self._check_missing_dependencies(config)
272
273 # Create Builder for the PodSpec
274 pod_spec_builder = PodSpecV3Builder()
275
276 # Build Container
277 container_builder = ContainerV3Builder(
278 self.app.name, image_info, config.image_pull_policy
279 )
280 container_builder.add_port(name=self.app.name, port=PORT)
281
282 # Build files
283 credential_files, fernet_files = self._build_files(config)
284 container_builder.add_volume_config(
285 "credential-keys", CREDENTIAL_KEYS_PATH, credential_files
286 )
287 container_builder.add_volume_config(
288 "fernet-keys", FERNET_KEYS_PATH, fernet_files
289 )
290 container_builder.add_envs(
291 {
292 "DB_HOST": config.mysql_host or self.mysql_client.host,
293 "DB_PORT": config.mysql_port or self.mysql_client.port,
294 "ROOT_DB_USER": "root",
295 "ROOT_DB_PASSWORD": config.mysql_root_password
296 or self.mysql_client.root_password,
297 "KEYSTONE_DB_PASSWORD": config.keystone_db_password,
298 "REGION_ID": config.region_id,
299 "KEYSTONE_HOST": self.app.name,
300 "ADMIN_USERNAME": config.admin_username,
301 "ADMIN_PASSWORD": config.admin_password,
302 "ADMIN_PROJECT": config.admin_project,
303 "SERVICE_USERNAME": config.service_username,
304 "SERVICE_PASSWORD": config.service_password,
305 "SERVICE_PROJECT": config.service_project,
306 }
307 )
308
309 if config_ldap.ldap_enabled:
310 container_builder.add_envs(
311 {
312 "LDAP_AUTHENTICATION_DOMAIN_NAME": config_ldap.ldap_authentication_domain_name,
313 "LDAP_URL": config_ldap.ldap_url,
314 "LDAP_PAGE_SIZE": config_ldap.ldap_page_size,
315 "LDAP_USER_OBJECTCLASS": config_ldap.ldap_user_objectclass,
316 "LDAP_USER_ID_ATTRIBUTE": config_ldap.ldap_user_id_attribute,
317 "LDAP_USER_NAME_ATTRIBUTE": config_ldap.ldap_user_name_attribute,
318 "LDAP_USER_PASS_ATTRIBUTE": config_ldap.ldap_user_pass_attribute,
319 "LDAP_USER_ENABLED_MASK": config_ldap.ldap_user_enabled_mask,
320 "LDAP_USER_ENABLED_DEFAULT": config_ldap.ldap_user_enabled_default,
321 "LDAP_USER_ENABLED_INVERT": config_ldap.ldap_user_enabled_invert,
322 "LDAP_GROUP_OBJECTCLASS": config_ldap.ldap_group_objectclass,
323 }
324 )
325 if config_ldap.ldap_bind_user:
326 container_builder.add_envs(
327 {"LDAP_BIND_USER": config_ldap.ldap_bind_user}
328 )
329
330 if config_ldap.ldap_bind_password:
331 container_builder.add_envs(
332 {"LDAP_BIND_PASSWORD": config_ldap.ldap_bind_password}
333 )
334
335 if config_ldap.ldap_user_tree_dn:
336 container_builder.add_envs(
337 {"LDAP_USER_TREE_DN": config_ldap.ldap_user_tree_dn}
338 )
339
340 if config_ldap.ldap_user_filter:
341 container_builder.add_envs(
342 {"LDAP_USER_FILTER": config_ldap.ldap_user_filter}
343 )
344
345 if config_ldap.ldap_user_enabled_attribute:
346 container_builder.add_envs(
347 {
348 "LDAP_USER_ENABLED_ATTRIBUTE": config_ldap.ldap_user_enabled_attribute
349 }
350 )
351
352 if config_ldap.ldap_chase_referrals:
353 container_builder.add_envs(
354 {"LDAP_CHASE_REFERRALS": config_ldap.ldap_chase_referrals}
355 )
356
357 if config_ldap.ldap_group_tree_dn:
358 container_builder.add_envs(
359 {"LDAP_GROUP_TREE_DN": config_ldap.ldap_group_tree_dn}
360 )
361
362 if config_ldap.ldap_tls_cacert_base64:
363 container_builder.add_envs(
364 {"LDAP_TLS_CACERT_BASE64": config_ldap.ldap_tls_cacert_base64}
365 )
366
367 if config_ldap.ldap_use_starttls:
368 container_builder.add_envs(
369 {
370 "LDAP_USE_STARTTLS": config_ldap.ldap_use_starttls,
371 "LDAP_TLS_CACERT_BASE64": config_ldap.ldap_tls_cacert_base64,
372 "LDAP_TLS_REQ_CERT": config_ldap.ldap_tls_req_cert,
373 }
374 )
375 container = container_builder.build()
376
377 # Add container to pod spec
378 pod_spec_builder.add_container(container)
379
380 # Add ingress resources to pod spec if site url exists
381 if config.site_url:
382 parsed = urlparse(config.site_url)
383 annotations = {
384 "nginx.ingress.kubernetes.io/proxy-body-size": "{}".format(
385 str(config.max_file_size) + "m"
386 if config.max_file_size > 0
387 else config.max_file_size
388 )
389 }
390 if config.ingress_class:
391 annotations["kubernetes.io/ingress.class"] = config.ingress_class
392 ingress_resource_builder = IngressResourceV3Builder(
393 f"{self.app.name}-ingress", annotations
394 )
395
396 if config.ingress_whitelist_source_range:
397 annotations[
398 "nginx.ingress.kubernetes.io/whitelist-source-range"
399 ] = config.ingress_whitelist_source_range
400
401 if parsed.scheme == "https":
402 ingress_resource_builder.add_tls(
403 [parsed.hostname], config.tls_secret_name
404 )
405 else:
406 annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
407
408 ingress_resource_builder.add_rule(parsed.hostname, self.app.name, PORT)
409 ingress_resource = ingress_resource_builder.build()
410 pod_spec_builder.add_ingress_resource(ingress_resource)
411 return pod_spec_builder.build()
412
413
414 if __name__ == "__main__":
415 main(KeystoneCharm)
416
417 # LOGGER = logging.getLogger(__name__)
418
419
420 # class ConfigurePodEvent(EventBase):
421 # """Configure Pod event"""
422
423 # pass
424
425
426 # class KeystoneEvents(CharmEvents):
427 # """Keystone Events"""
428
429 # configure_pod = EventSource(ConfigurePodEvent)
430
431 # class KeystoneCharm(CharmBase):
432 # """Keystone K8s Charm"""
433
434 # state = StoredState()
435 # on = KeystoneEvents()
436
437 # def __init__(self, *args) -> NoReturn:
438 # """Constructor of the Charm object.
439 # Initializes internal state and register events it can handle.
440 # """
441 # super().__init__(*args)
442 # self.state.set_default(db_host=None)
443 # self.state.set_default(db_port=None)
444 # self.state.set_default(db_user=None)
445 # self.state.set_default(db_password=None)
446 # self.state.set_default(pod_spec=None)
447 # self.state.set_default(fernet_keys=None)
448 # self.state.set_default(credential_keys=None)
449 # self.state.set_default(keys_timestamp=0)
450
451 # # Register all of the events we want to observe
452 # self.framework.observe(self.on.config_changed, self.configure_pod)
453 # self.framework.observe(self.on.start, self.configure_pod)
454 # self.framework.observe(self.on.upgrade_charm, self.configure_pod)
455 # self.framework.observe(self.on.leader_elected, self.configure_pod)
456 # self.framework.observe(self.on.update_status, self.configure_pod)
457
458 # # Registering custom internal events
459 # self.framework.observe(self.on.configure_pod, self.configure_pod)
460
461 # # Register relation events
462 # self.framework.observe(
463 # self.on.db_relation_changed, self._on_db_relation_changed
464 # )
465 # self.framework.observe(
466 # self.on.db_relation_broken, self._on_db_relation_broken
467 # )
468 # self.framework.observe(
469 # self.on.keystone_relation_joined, self._publish_keystone_info
470 # )
471
472 # def _publish_keystone_info(self, event: EventBase) -> NoReturn:
473 # """Publishes keystone information for NBI usage through the keystone
474 # relation.
475
476 # Args:
477 # event (EventBase): Keystone relation event to update NBI.
478 # """
479 # config = self.model.config
480 # rel_data = {
481 # "host": f"http://{self.app.name}:{KEYSTONE_PORT}/v3",
482 # "port": str(KEYSTONE_PORT),
483 # "keystone_db_password": config["keystone_db_password"],
484 # "region_id": config["region_id"],
485 # "user_domain_name": config["user_domain_name"],
486 # "project_domain_name": config["project_domain_name"],
487 # "admin_username": config["admin_username"],
488 # "admin_password": config["admin_password"],
489 # "admin_project_name": config["admin_project"],
490 # "username": config["service_username"],
491 # "password": config["service_password"],
492 # "service": config["service_project"],
493 # }
494 # for k, v in rel_data.items():
495 # event.relation.data[self.model.unit][k] = v
496
497 # def _on_db_relation_changed(self, event: EventBase) -> NoReturn:
498 # """Reads information about the DB relation, in order for keystone to
499 # access it.
500
501 # Args:
502 # event (EventBase): DB relation event to access database
503 # information.
504 # """
505 # if not event.unit in event.relation.data:
506 # return
507 # relation_data = event.relation.data[event.unit]
508 # db_host = relation_data.get("host")
509 # db_port = int(relation_data.get("port", 3306))
510 # db_user = "root"
511 # db_password = relation_data.get("root_password")
512
513 # if (
514 # db_host
515 # and db_port
516 # and db_user
517 # and db_password
518 # and (
519 # self.state.db_host != db_host
520 # or self.state.db_port != db_port
521 # or self.state.db_user != db_user
522 # or self.state.db_password != db_password
523 # )
524 # ):
525 # self.state.db_host = db_host
526 # self.state.db_port = db_port
527 # self.state.db_user = db_user
528 # self.state.db_password = db_password
529 # self.on.configure_pod.emit()
530
531
532 # def _on_db_relation_broken(self, event: EventBase) -> NoReturn:
533 # """Clears data from db relation.
534
535 # Args:
536 # event (EventBase): DB relation event.
537
538 # """
539 # self.state.db_host = None
540 # self.state.db_port = None
541 # self.state.db_user = None
542 # self.state.db_password = None
543 # self.on.configure_pod.emit()
544
545 # def _check_settings(self) -> str:
546 # """Check if there any settings missing from Keystone configuration.
547
548 # Returns:
549 # str: Information about the problems found (if any).
550 # """
551 # problems = []
552 # config = self.model.config
553
554 # for setting in REQUIRED_SETTINGS:
555 # if not config.get(setting):
556 # problem = f"missing config {setting}"
557 # problems.append(problem)
558
559 # return ";".join(problems)
560
561 # def _make_pod_image_details(self) -> Dict[str, str]:
562 # """Generate the pod image details.
563
564 # Returns:
565 # Dict[str, str]: pod image details.
566 # """
567 # config = self.model.config
568 # image_details = {
569 # "imagePath": config["image"],
570 # }
571 # if config["image_username"]:
572 # image_details.update(
573 # {
574 # "username": config["image_username"],
575 # "password": config["image_password"],
576 # }
577 # )
578 # return image_details
579
580 # def _make_pod_ports(self) -> List[Dict[str, Any]]:
581 # """Generate the pod ports details.
582
583 # Returns:
584 # List[Dict[str, Any]]: pod ports details.
585 # """
586 # return [
587 # {"name": "keystone", "containerPort": KEYSTONE_PORT, "protocol": "TCP"},
588 # ]
589
590 # def _make_pod_envconfig(self) -> Dict[str, Any]:
591 # """Generate pod environment configuraiton.
592
593 # Returns:
594 # Dict[str, Any]: pod environment configuration.
595 # """
596 # config = self.model.config
597
598 # envconfig = {
599 # "DB_HOST": self.state.db_host,
600 # "DB_PORT": self.state.db_port,
601 # "ROOT_DB_USER": self.state.db_user,
602 # "ROOT_DB_PASSWORD": self.state.db_password,
603 # "KEYSTONE_DB_PASSWORD": config["keystone_db_password"],
604 # "REGION_ID": config["region_id"],
605 # "KEYSTONE_HOST": self.app.name,
606 # "ADMIN_USERNAME": config["admin_username"],
607 # "ADMIN_PASSWORD": config["admin_password"],
608 # "ADMIN_PROJECT": config["admin_project"],
609 # "SERVICE_USERNAME": config["service_username"],
610 # "SERVICE_PASSWORD": config["service_password"],
611 # "SERVICE_PROJECT": config["service_project"],
612 # }
613
614 # if config.get("ldap_enabled"):
615 # envconfig["LDAP_AUTHENTICATION_DOMAIN_NAME"] = config[
616 # "ldap_authentication_domain_name"
617 # ]
618 # envconfig["LDAP_URL"] = config["ldap_url"]
619 # envconfig["LDAP_PAGE_SIZE"] = config["ldap_page_size"]
620 # envconfig["LDAP_USER_OBJECTCLASS"] = config["ldap_user_objectclass"]
621 # envconfig["LDAP_USER_ID_ATTRIBUTE"] = config["ldap_user_id_attribute"]
622 # envconfig["LDAP_USER_NAME_ATTRIBUTE"] = config["ldap_user_name_attribute"]
623 # envconfig["LDAP_USER_PASS_ATTRIBUTE"] = config["ldap_user_pass_attribute"]
624 # envconfig["LDAP_USER_ENABLED_MASK"] = config["ldap_user_enabled_mask"]
625 # envconfig["LDAP_USER_ENABLED_DEFAULT"] = config["ldap_user_enabled_default"]
626 # envconfig["LDAP_USER_ENABLED_INVERT"] = config["ldap_user_enabled_invert"]
627 # envconfig["LDAP_GROUP_OBJECTCLASS"] = config["ldap_group_objectclass"]
628
629 # if config["ldap_bind_user"]:
630 # envconfig["LDAP_BIND_USER"] = config["ldap_bind_user"]
631
632 # if config["ldap_bind_password"]:
633 # envconfig["LDAP_BIND_PASSWORD"] = config["ldap_bind_password"]
634
635 # if config["ldap_user_tree_dn"]:
636 # envconfig["LDAP_USER_TREE_DN"] = config["ldap_user_tree_dn"]
637
638 # if config["ldap_user_filter"]:
639 # envconfig["LDAP_USER_FILTER"] = config["ldap_user_filter"]
640
641 # if config["ldap_user_enabled_attribute"]:
642 # envconfig["LDAP_USER_ENABLED_ATTRIBUTE"] = config[
643 # "ldap_user_enabled_attribute"
644 # ]
645
646 # if config["ldap_chase_referrals"]:
647 # envconfig["LDAP_CHASE_REFERRALS"] = config["ldap_chase_referrals"]
648
649 # if config["ldap_group_tree_dn"]:
650 # envconfig["LDAP_GROUP_TREE_DN"] = config["ldap_group_tree_dn"]
651
652 # if config["ldap_use_starttls"]:
653 # envconfig["LDAP_USE_STARTTLS"] = config["ldap_use_starttls"]
654 # envconfig["LDAP_TLS_CACERT_BASE64"] = config["ldap_tls_cacert_base64"]
655 # envconfig["LDAP_TLS_REQ_CERT"] = config["ldap_tls_req_cert"]
656
657 # return envconfig
658
659 # def _make_pod_ingress_resources(self) -> List[Dict[str, Any]]:
660 # """Generate pod ingress resources.
661
662 # Returns:
663 # List[Dict[str, Any]]: pod ingress resources.
664 # """
665 # site_url = self.model.config["site_url"]
666
667 # if not site_url:
668 # return
669
670 # parsed = urlparse(site_url)
671
672 # if not parsed.scheme.startswith("http"):
673 # return
674
675 # max_file_size = self.model.config["max_file_size"]
676 # ingress_whitelist_source_range = self.model.config[
677 # "ingress_whitelist_source_range"
678 # ]
679
680 # annotations = {
681 # "nginx.ingress.kubernetes.io/proxy-body-size": "{}m".format(max_file_size)
682 # }
683
684 # if ingress_whitelist_source_range:
685 # annotations[
686 # "nginx.ingress.kubernetes.io/whitelist-source-range"
687 # ] = ingress_whitelist_source_range
688
689 # ingress_spec_tls = None
690
691 # if parsed.scheme == "https":
692 # ingress_spec_tls = [{"hosts": [parsed.hostname]}]
693 # tls_secret_name = self.model.config["tls_secret_name"]
694 # if tls_secret_name:
695 # ingress_spec_tls[0]["secretName"] = tls_secret_name
696 # else:
697 # annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
698
699 # ingress = {
700 # "name": "{}-ingress".format(self.app.name),
701 # "annotations": annotations,
702 # "spec": {
703 # "rules": [
704 # {
705 # "host": parsed.hostname,
706 # "http": {
707 # "paths": [
708 # {
709 # "path": "/",
710 # "backend": {
711 # "serviceName": self.app.name,
712 # "servicePort": KEYSTONE_PORT,
713 # },
714 # }
715 # ]
716 # },
717 # }
718 # ],
719 # },
720 # }
721 # if ingress_spec_tls:
722 # ingress["spec"]["tls"] = ingress_spec_tls
723
724 # return [ingress]
725
726 # def _generate_keys(self) -> Tuple[List[str], List[str]]:
727 # """Generating new fernet tokens.
728
729 # Returns:
730 # Tuple[List[str], List[str]]: contains two lists of strings. First
731 # list contains strings that represent
732 # the keys for fernet and the second
733 # list contains strins that represent
734 # the keys for credentials.
735 # """
736 # fernet_keys = [
737 # Fernet.generate_key().decode() for _ in range(NUMBER_FERNET_KEYS)
738 # ]
739 # credential_keys = [
740 # Fernet.generate_key().decode() for _ in range(NUMBER_CREDENTIAL_KEYS)
741 # ]
742
743 # return (fernet_keys, credential_keys)
744
745 # def configure_pod(self, event: EventBase) -> NoReturn:
746 # """Assemble the pod spec and apply it, if possible.
747
748 # Args:
749 # event (EventBase): Hook or Relation event that started the
750 # function.
751 # """
752 # if not self.state.db_host:
753 # self.unit.status = WaitingStatus("Waiting for database relation")
754 # event.defer()
755 # return
756
757 # if not self.unit.is_leader():
758 # self.unit.status = ActiveStatus("ready")
759 # return
760
761 # if fernet_keys := self.state.fernet_keys:
762 # fernet_keys = json.loads(fernet_keys)
763
764 # if credential_keys := self.state.credential_keys:
765 # credential_keys = json.loads(credential_keys)
766
767 # now = datetime.now().timestamp()
768 # keys_timestamp = self.state.keys_timestamp
769 # token_expiration = self.model.config["token_expiration"]
770
771 # valid_keys = (now - keys_timestamp) < token_expiration
772 # if not credential_keys or not fernet_keys or not valid_keys:
773 # fernet_keys, credential_keys = self._generate_keys()
774 # self.state.fernet_keys = json.dumps(fernet_keys)
775 # self.state.credential_keys = json.dumps(credential_keys)
776 # self.state.keys_timestamp = now
777
778 # # Check problems in the settings
779 # problems = self._check_settings()
780 # if problems:
781 # self.unit.status = BlockedStatus(problems)
782 # return
783
784 # self.unit.status = BlockedStatus("Assembling pod spec")
785 # image_details = self._make_pod_image_details()
786 # ports = self._make_pod_ports()
787 # env_config = self._make_pod_envconfig()
788 # ingress_resources = self._make_pod_ingress_resources()
789 # files = self._make_pod_files(fernet_keys, credential_keys)
790
791 # pod_spec = {
792 # "version": 3,
793 # "containers": [
794 # {
795 # "name": self.framework.model.app.name,
796 # "imageDetails": image_details,
797 # "ports": ports,
798 # "envConfig": env_config,
799 # "volumeConfig": files,
800 # }
801 # ],
802 # "kubernetesResources": {"ingressResources": ingress_resources or []},
803 # }
804
805 # if self.state.pod_spec != (
806 # pod_spec_json := json.dumps(pod_spec, sort_keys=True)
807 # ):
808 # self.state.pod_spec = pod_spec_json
809 # self.model.pod.set_spec(pod_spec)
810
811 # self.unit.status = ActiveStatus("ready")
812
813
814 # if __name__ == "__main__":
815 # main(KeystoneCharm)