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