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