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