2 # Copyright 2021 Canonical Ltd.
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
8 # http://www.apache.org/licenses/LICENSE-2.0
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
16 # For those usages not covered by the Apache License, Version 2.0 please
17 # contact: legal@canonical.com
19 # To get in touch with the maintainers, please contact:
20 # osm-charmers@lists.launchpad.net
23 # pylint: disable=E0213
26 from datetime
import datetime
27 from ipaddress
import ip_network
30 from typing
import List
, NoReturn
, Optional
, Tuple
31 from urllib
.parse
import urlparse
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 (
41 IngressResourceV3Builder
,
44 from opslib
.osm
.validator
import ModelValidator
, validator
47 logger
= logging
.getLogger(__name__
)
50 REQUIRED_SETTINGS
= ["token_expiration"]
52 # This is hardcoded in the keystone container script
53 DATABASE_NAME
= "keystone"
55 # We expect the keystone container to use the default port
58 # Number of keys need might need to be adjusted in the future
59 NUMBER_FERNET_KEYS
= 2
60 NUMBER_CREDENTIAL_KEYS
= 2
63 CREDENTIAL_KEYS_PATH
= "/etc/keystone/credential-keys"
64 FERNET_KEYS_PATH
= "/etc/keystone/fernet-keys"
67 class ConfigModel(ModelValidator
):
69 keystone_db_password
: str
77 project_domain_name
: str
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]
87 @validator("max_file_size")
88 def validate_max_file_size(cls
, v
):
90 raise ValueError("value must be equal or greater than 0")
93 @validator("site_url")
94 def validate_site_url(cls
, v
):
97 if not parsed
.scheme
.startswith("http"):
98 raise ValueError("value must start with http")
101 @validator("ingress_whitelist_source_range")
102 def validate_ingress_whitelist_source_range(cls
, v
):
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")
114 class ConfigLdapModel(ModelValidator
):
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]
139 def validate_ldap_user_enabled_default(cls
, v
):
141 if v
not in ["true", "false"]:
142 raise ValueError('must be equal to "true" or "false"')
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)
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
)
158 self
.framework
.observe(
159 self
.on
["keystone"].relation_joined
, self
._publish
_keystone
_info
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",
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
,
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
)
187 def _validate_mysql_config(self
, config
: ConfigModel
):
189 if not config
.mysql_root_password
:
190 invalid_values
.append("Mysql root password must be provided")
193 raise ValueError("Invalid values: " + ", ".join(invalid_values
))
195 def _generate_keys(self
) -> Tuple
[List
[str], List
[str]]:
196 """Generating new fernet tokens.
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.
206 Fernet
.generate_key().decode() for _
in range(NUMBER_FERNET_KEYS
)
209 Fernet
.generate_key().decode() for _
in range(NUMBER_CREDENTIAL_KEYS
)
212 return (fernet_keys
, credential_keys
)
215 keys_timestamp
= self
.state
.keys_timestamp
216 if fernet_keys
:= self
.state
.fernet_keys
:
217 fernet_keys
= json
.loads(fernet_keys
)
219 if credential_keys
:= self
.state
.credential_keys
:
220 credential_keys
= json
.loads(credential_keys
)
222 now
= datetime
.now().timestamp()
223 token_expiration
= self
.config
["token_expiration"]
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
233 def _build_files(self
, config
: ConfigModel
):
234 credentials_files_builder
= FilesV3Builder()
235 fernet_files_builder
= FilesV3Builder()
237 credential_keys
, fernet_keys
= self
._get
_keys
()
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()
245 def build_pod_spec(self
, image_info
):
247 config
= ConfigModel(**dict(self
.config
))
248 config_ldap
= ConfigLdapModel(**dict(self
.config
))
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")
253 if config
.mysql_host
:
254 self
._validate
_mysql
_config
(config
)
257 self
._check
_missing
_dependencies
(config
)
259 # Create Builder for the PodSpec
260 pod_spec_builder
= PodSpecV3Builder()
263 container_builder
= ContainerV3Builder(self
.app
.name
, image_info
)
264 container_builder
.add_port(name
=self
.app
.name
, port
=PORT
)
267 credential_files
, fernet_files
= self
._build
_files
(config
)
268 container_builder
.add_volume_config(
269 "credential-keys", CREDENTIAL_KEYS_PATH
, credential_files
271 container_builder
.add_volume_config(
272 "fernet-keys", FERNET_KEYS_PATH
, fernet_files
274 container_builder
.add_envs(
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
,
293 if config_ldap
.ldap_enabled
:
294 container_builder
.add_envs(
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
,
309 if config_ldap
.ldap_bind_user
:
310 container_builder
.add_envs(
311 {"LDAP_BIND_USER": config_ldap
.ldap_bind_user
}
314 if config_ldap
.ldap_bind_password
:
315 container_builder
.add_envs(
316 {"LDAP_BIND_PASSWORD": config_ldap
.ldap_bind_password
}
319 if config_ldap
.ldap_user_tree_dn
:
320 container_builder
.add_envs(
321 {"LDAP_USER_TREE_DN": config_ldap
.ldap_user_tree_dn
}
324 if config_ldap
.ldap_user_filter
:
325 container_builder
.add_envs(
326 {"LDAP_USER_FILTER": config_ldap
.ldap_user_filter
}
329 if config_ldap
.ldap_user_enabled_attribute
:
330 container_builder
.add_envs(
332 "LDAP_USER_ENABLED_ATTRIBUTE": config_ldap
.ldap_user_enabled_attribute
336 if config_ldap
.ldap_chase_referrals
:
337 container_builder
.add_envs(
338 {"LDAP_CHASE_REFERRALS": config_ldap
.ldap_chase_referrals
}
341 if config_ldap
.ldap_group_tree_dn
:
342 container_builder
.add_envs(
343 {"LDAP_GROUP_TREE_DN": config_ldap
.ldap_group_tree_dn
}
346 if config_ldap
.ldap_use_starttls
:
347 container_builder
.add_envs(
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
,
354 container
= container_builder
.build()
356 # Add container to pod spec
357 pod_spec_builder
.add_container(container
)
359 # Add ingress resources to pod spec if site url exists
361 parsed
= urlparse(config
.site_url
)
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
368 "kubernetes.io/ingress.class": "public",
370 ingress_resource_builder
= IngressResourceV3Builder(
371 f
"{self.app.name}-ingress", annotations
374 if config
.ingress_whitelist_source_range
:
376 "nginx.ingress.kubernetes.io/whitelist-source-range"
377 ] = config
.ingress_whitelist_source_range
379 if parsed
.scheme
== "https":
380 ingress_resource_builder
.add_tls(
381 [parsed
.hostname
], config
.tls_secret_name
384 annotations
["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
386 ingress_resource_builder
.add_rule(parsed
.hostname
, self
.app
.name
, PORT
)
387 ingress_resource
= ingress_resource_builder
.build()
388 pod_spec_builder
.add_ingress_resource(ingress_resource
)
389 return pod_spec_builder
.build()
392 if __name__
== "__main__":
395 # LOGGER = logging.getLogger(__name__)
398 # class ConfigurePodEvent(EventBase):
399 # """Configure Pod event"""
404 # class KeystoneEvents(CharmEvents):
405 # """Keystone Events"""
407 # configure_pod = EventSource(ConfigurePodEvent)
409 # class KeystoneCharm(CharmBase):
410 # """Keystone K8s Charm"""
412 # state = StoredState()
413 # on = KeystoneEvents()
415 # def __init__(self, *args) -> NoReturn:
416 # """Constructor of the Charm object.
417 # Initializes internal state and register events it can handle.
419 # super().__init__(*args)
420 # self.state.set_default(db_host=None)
421 # self.state.set_default(db_port=None)
422 # self.state.set_default(db_user=None)
423 # self.state.set_default(db_password=None)
424 # self.state.set_default(pod_spec=None)
425 # self.state.set_default(fernet_keys=None)
426 # self.state.set_default(credential_keys=None)
427 # self.state.set_default(keys_timestamp=0)
429 # # Register all of the events we want to observe
430 # self.framework.observe(self.on.config_changed, self.configure_pod)
431 # self.framework.observe(self.on.start, self.configure_pod)
432 # self.framework.observe(self.on.upgrade_charm, self.configure_pod)
433 # self.framework.observe(self.on.leader_elected, self.configure_pod)
434 # self.framework.observe(self.on.update_status, self.configure_pod)
436 # # Registering custom internal events
437 # self.framework.observe(self.on.configure_pod, self.configure_pod)
439 # # Register relation events
440 # self.framework.observe(
441 # self.on.db_relation_changed, self._on_db_relation_changed
443 # self.framework.observe(
444 # self.on.db_relation_broken, self._on_db_relation_broken
446 # self.framework.observe(
447 # self.on.keystone_relation_joined, self._publish_keystone_info
450 # def _publish_keystone_info(self, event: EventBase) -> NoReturn:
451 # """Publishes keystone information for NBI usage through the keystone
455 # event (EventBase): Keystone relation event to update NBI.
457 # config = self.model.config
459 # "host": f"http://{self.app.name}:{KEYSTONE_PORT}/v3",
460 # "port": str(KEYSTONE_PORT),
461 # "keystone_db_password": config["keystone_db_password"],
462 # "region_id": config["region_id"],
463 # "user_domain_name": config["user_domain_name"],
464 # "project_domain_name": config["project_domain_name"],
465 # "admin_username": config["admin_username"],
466 # "admin_password": config["admin_password"],
467 # "admin_project_name": config["admin_project"],
468 # "username": config["service_username"],
469 # "password": config["service_password"],
470 # "service": config["service_project"],
472 # for k, v in rel_data.items():
473 # event.relation.data[self.model.unit][k] = v
475 # def _on_db_relation_changed(self, event: EventBase) -> NoReturn:
476 # """Reads information about the DB relation, in order for keystone to
480 # event (EventBase): DB relation event to access database
483 # if not event.unit in event.relation.data:
485 # relation_data = event.relation.data[event.unit]
486 # db_host = relation_data.get("host")
487 # db_port = int(relation_data.get("port", 3306))
489 # db_password = relation_data.get("root_password")
497 # self.state.db_host != db_host
498 # or self.state.db_port != db_port
499 # or self.state.db_user != db_user
500 # or self.state.db_password != db_password
503 # self.state.db_host = db_host
504 # self.state.db_port = db_port
505 # self.state.db_user = db_user
506 # self.state.db_password = db_password
507 # self.on.configure_pod.emit()
510 # def _on_db_relation_broken(self, event: EventBase) -> NoReturn:
511 # """Clears data from db relation.
514 # event (EventBase): DB relation event.
517 # self.state.db_host = None
518 # self.state.db_port = None
519 # self.state.db_user = None
520 # self.state.db_password = None
521 # self.on.configure_pod.emit()
523 # def _check_settings(self) -> str:
524 # """Check if there any settings missing from Keystone configuration.
527 # str: Information about the problems found (if any).
530 # config = self.model.config
532 # for setting in REQUIRED_SETTINGS:
533 # if not config.get(setting):
534 # problem = f"missing config {setting}"
535 # problems.append(problem)
537 # return ";".join(problems)
539 # def _make_pod_image_details(self) -> Dict[str, str]:
540 # """Generate the pod image details.
543 # Dict[str, str]: pod image details.
545 # config = self.model.config
547 # "imagePath": config["image"],
549 # if config["image_username"]:
550 # image_details.update(
552 # "username": config["image_username"],
553 # "password": config["image_password"],
556 # return image_details
558 # def _make_pod_ports(self) -> List[Dict[str, Any]]:
559 # """Generate the pod ports details.
562 # List[Dict[str, Any]]: pod ports details.
565 # {"name": "keystone", "containerPort": KEYSTONE_PORT, "protocol": "TCP"},
568 # def _make_pod_envconfig(self) -> Dict[str, Any]:
569 # """Generate pod environment configuraiton.
572 # Dict[str, Any]: pod environment configuration.
574 # config = self.model.config
577 # "DB_HOST": self.state.db_host,
578 # "DB_PORT": self.state.db_port,
579 # "ROOT_DB_USER": self.state.db_user,
580 # "ROOT_DB_PASSWORD": self.state.db_password,
581 # "KEYSTONE_DB_PASSWORD": config["keystone_db_password"],
582 # "REGION_ID": config["region_id"],
583 # "KEYSTONE_HOST": self.app.name,
584 # "ADMIN_USERNAME": config["admin_username"],
585 # "ADMIN_PASSWORD": config["admin_password"],
586 # "ADMIN_PROJECT": config["admin_project"],
587 # "SERVICE_USERNAME": config["service_username"],
588 # "SERVICE_PASSWORD": config["service_password"],
589 # "SERVICE_PROJECT": config["service_project"],
592 # if config.get("ldap_enabled"):
593 # envconfig["LDAP_AUTHENTICATION_DOMAIN_NAME"] = config[
594 # "ldap_authentication_domain_name"
596 # envconfig["LDAP_URL"] = config["ldap_url"]
597 # envconfig["LDAP_PAGE_SIZE"] = config["ldap_page_size"]
598 # envconfig["LDAP_USER_OBJECTCLASS"] = config["ldap_user_objectclass"]
599 # envconfig["LDAP_USER_ID_ATTRIBUTE"] = config["ldap_user_id_attribute"]
600 # envconfig["LDAP_USER_NAME_ATTRIBUTE"] = config["ldap_user_name_attribute"]
601 # envconfig["LDAP_USER_PASS_ATTRIBUTE"] = config["ldap_user_pass_attribute"]
602 # envconfig["LDAP_USER_ENABLED_MASK"] = config["ldap_user_enabled_mask"]
603 # envconfig["LDAP_USER_ENABLED_DEFAULT"] = config["ldap_user_enabled_default"]
604 # envconfig["LDAP_USER_ENABLED_INVERT"] = config["ldap_user_enabled_invert"]
605 # envconfig["LDAP_GROUP_OBJECTCLASS"] = config["ldap_group_objectclass"]
607 # if config["ldap_bind_user"]:
608 # envconfig["LDAP_BIND_USER"] = config["ldap_bind_user"]
610 # if config["ldap_bind_password"]:
611 # envconfig["LDAP_BIND_PASSWORD"] = config["ldap_bind_password"]
613 # if config["ldap_user_tree_dn"]:
614 # envconfig["LDAP_USER_TREE_DN"] = config["ldap_user_tree_dn"]
616 # if config["ldap_user_filter"]:
617 # envconfig["LDAP_USER_FILTER"] = config["ldap_user_filter"]
619 # if config["ldap_user_enabled_attribute"]:
620 # envconfig["LDAP_USER_ENABLED_ATTRIBUTE"] = config[
621 # "ldap_user_enabled_attribute"
624 # if config["ldap_chase_referrals"]:
625 # envconfig["LDAP_CHASE_REFERRALS"] = config["ldap_chase_referrals"]
627 # if config["ldap_group_tree_dn"]:
628 # envconfig["LDAP_GROUP_TREE_DN"] = config["ldap_group_tree_dn"]
630 # if config["ldap_use_starttls"]:
631 # envconfig["LDAP_USE_STARTTLS"] = config["ldap_use_starttls"]
632 # envconfig["LDAP_TLS_CACERT_BASE64"] = config["ldap_tls_cacert_base64"]
633 # envconfig["LDAP_TLS_REQ_CERT"] = config["ldap_tls_req_cert"]
637 # def _make_pod_ingress_resources(self) -> List[Dict[str, Any]]:
638 # """Generate pod ingress resources.
641 # List[Dict[str, Any]]: pod ingress resources.
643 # site_url = self.model.config["site_url"]
648 # parsed = urlparse(site_url)
650 # if not parsed.scheme.startswith("http"):
653 # max_file_size = self.model.config["max_file_size"]
654 # ingress_whitelist_source_range = self.model.config[
655 # "ingress_whitelist_source_range"
659 # "nginx.ingress.kubernetes.io/proxy-body-size": "{}m".format(max_file_size)
662 # if ingress_whitelist_source_range:
664 # "nginx.ingress.kubernetes.io/whitelist-source-range"
665 # ] = ingress_whitelist_source_range
667 # ingress_spec_tls = None
669 # if parsed.scheme == "https":
670 # ingress_spec_tls = [{"hosts": [parsed.hostname]}]
671 # tls_secret_name = self.model.config["tls_secret_name"]
672 # if tls_secret_name:
673 # ingress_spec_tls[0]["secretName"] = tls_secret_name
675 # annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
678 # "name": "{}-ingress".format(self.app.name),
679 # "annotations": annotations,
683 # "host": parsed.hostname,
689 # "serviceName": self.app.name,
690 # "servicePort": KEYSTONE_PORT,
699 # if ingress_spec_tls:
700 # ingress["spec"]["tls"] = ingress_spec_tls
704 # def _generate_keys(self) -> Tuple[List[str], List[str]]:
705 # """Generating new fernet tokens.
708 # Tuple[List[str], List[str]]: contains two lists of strings. First
709 # list contains strings that represent
710 # the keys for fernet and the second
711 # list contains strins that represent
712 # the keys for credentials.
715 # Fernet.generate_key().decode() for _ in range(NUMBER_FERNET_KEYS)
717 # credential_keys = [
718 # Fernet.generate_key().decode() for _ in range(NUMBER_CREDENTIAL_KEYS)
721 # return (fernet_keys, credential_keys)
723 # def configure_pod(self, event: EventBase) -> NoReturn:
724 # """Assemble the pod spec and apply it, if possible.
727 # event (EventBase): Hook or Relation event that started the
730 # if not self.state.db_host:
731 # self.unit.status = WaitingStatus("Waiting for database relation")
735 # if not self.unit.is_leader():
736 # self.unit.status = ActiveStatus("ready")
739 # if fernet_keys := self.state.fernet_keys:
740 # fernet_keys = json.loads(fernet_keys)
742 # if credential_keys := self.state.credential_keys:
743 # credential_keys = json.loads(credential_keys)
745 # now = datetime.now().timestamp()
746 # keys_timestamp = self.state.keys_timestamp
747 # token_expiration = self.model.config["token_expiration"]
749 # valid_keys = (now - keys_timestamp) < token_expiration
750 # if not credential_keys or not fernet_keys or not valid_keys:
751 # fernet_keys, credential_keys = self._generate_keys()
752 # self.state.fernet_keys = json.dumps(fernet_keys)
753 # self.state.credential_keys = json.dumps(credential_keys)
754 # self.state.keys_timestamp = now
756 # # Check problems in the settings
757 # problems = self._check_settings()
759 # self.unit.status = BlockedStatus(problems)
762 # self.unit.status = BlockedStatus("Assembling pod spec")
763 # image_details = self._make_pod_image_details()
764 # ports = self._make_pod_ports()
765 # env_config = self._make_pod_envconfig()
766 # ingress_resources = self._make_pod_ingress_resources()
767 # files = self._make_pod_files(fernet_keys, credential_keys)
773 # "name": self.framework.model.app.name,
774 # "imageDetails": image_details,
776 # "envConfig": env_config,
777 # "volumeConfig": files,
780 # "kubernetesResources": {"ingressResources": ingress_resources or []},
783 # if self.state.pod_spec != (
784 # pod_spec_json := json.dumps(pod_spec, sort_keys=True)
786 # self.state.pod_spec = pod_spec_json
787 # self.model.pod.set_spec(pod_spec)
789 # self.unit.status = ActiveStatus("ready")
792 # if __name__ == "__main__":
793 # main(KeystoneCharm)