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