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