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_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]
88 @validator("max_file_size")
89 def validate_max_file_size(cls
, v
):
91 raise ValueError("value must be equal or greater than 0")
94 @validator("site_url")
95 def validate_site_url(cls
, v
):
98 if not parsed
.scheme
.startswith("http"):
99 raise ValueError("value must start with http")
102 @validator("ingress_whitelist_source_range")
103 def validate_ingress_whitelist_source_range(cls
, v
):
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")
115 class ConfigLdapModel(ModelValidator
):
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]
140 def validate_ldap_user_enabled_default(cls
, v
):
142 if v
not in ["true", "false"]:
143 raise ValueError('must be equal to "true" or "false"')
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)
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
)
159 self
.framework
.observe(
160 self
.on
["keystone"].relation_joined
, self
._publish
_keystone
_info
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",
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
,
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
)
188 def _validate_mysql_config(self
, config
: ConfigModel
):
190 if not config
.mysql_root_password
:
191 invalid_values
.append("Mysql root password must be provided")
194 raise ValueError("Invalid values: " + ", ".join(invalid_values
))
196 def _generate_keys(self
) -> Tuple
[List
[str], List
[str]]:
197 """Generating new fernet tokens.
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.
207 Fernet
.generate_key().decode() for _
in range(NUMBER_FERNET_KEYS
)
210 Fernet
.generate_key().decode() for _
in range(NUMBER_CREDENTIAL_KEYS
)
213 return (fernet_keys
, credential_keys
)
216 keys_timestamp
= self
.state
.keys_timestamp
217 if fernet_keys
:= self
.state
.fernet_keys
:
218 fernet_keys
= json
.loads(fernet_keys
)
220 if credential_keys
:= self
.state
.credential_keys
:
221 credential_keys
= json
.loads(credential_keys
)
223 now
= datetime
.now().timestamp()
224 token_expiration
= self
.config
["token_expiration"]
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
234 def _build_files(self
, config
: ConfigModel
):
235 credentials_files_builder
= FilesV3Builder()
236 fernet_files_builder
= FilesV3Builder()
238 credential_keys
, fernet_keys
= self
._get
_keys
()
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()
246 def build_pod_spec(self
, image_info
):
248 config
= ConfigModel(**dict(self
.config
))
249 config_ldap
= ConfigLdapModel(**dict(self
.config
))
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")
254 if config
.mysql_host
:
255 self
._validate
_mysql
_config
(config
)
258 self
._check
_missing
_dependencies
(config
)
260 # Create Builder for the PodSpec
261 pod_spec_builder
= PodSpecV3Builder()
264 container_builder
= ContainerV3Builder(self
.app
.name
, image_info
)
265 container_builder
.add_port(name
=self
.app
.name
, port
=PORT
)
268 credential_files
, fernet_files
= self
._build
_files
(config
)
269 container_builder
.add_volume_config(
270 "credential-keys", CREDENTIAL_KEYS_PATH
, credential_files
272 container_builder
.add_volume_config(
273 "fernet-keys", FERNET_KEYS_PATH
, fernet_files
275 container_builder
.add_envs(
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
,
294 if config_ldap
.ldap_enabled
:
295 container_builder
.add_envs(
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
,
310 if config_ldap
.ldap_bind_user
:
311 container_builder
.add_envs(
312 {"LDAP_BIND_USER": config_ldap
.ldap_bind_user
}
315 if config_ldap
.ldap_bind_password
:
316 container_builder
.add_envs(
317 {"LDAP_BIND_PASSWORD": config_ldap
.ldap_bind_password
}
320 if config_ldap
.ldap_user_tree_dn
:
321 container_builder
.add_envs(
322 {"LDAP_USER_TREE_DN": config_ldap
.ldap_user_tree_dn
}
325 if config_ldap
.ldap_user_filter
:
326 container_builder
.add_envs(
327 {"LDAP_USER_FILTER": config_ldap
.ldap_user_filter
}
330 if config_ldap
.ldap_user_enabled_attribute
:
331 container_builder
.add_envs(
333 "LDAP_USER_ENABLED_ATTRIBUTE": config_ldap
.ldap_user_enabled_attribute
337 if config_ldap
.ldap_chase_referrals
:
338 container_builder
.add_envs(
339 {"LDAP_CHASE_REFERRALS": config_ldap
.ldap_chase_referrals
}
342 if config_ldap
.ldap_group_tree_dn
:
343 container_builder
.add_envs(
344 {"LDAP_GROUP_TREE_DN": config_ldap
.ldap_group_tree_dn
}
347 if config_ldap
.ldap_use_starttls
:
348 container_builder
.add_envs(
350 "LDAP_USE_STARTTLS": config_ldap
.ldap_use_starttls
,
351 "LDAP_TLS_CACERT_BASE64": config_ldap
.ldap_tls_cacert_base64
,
352 "LDAP_TLS_REQ_CERT": config_ldap
.ldap_tls_req_cert
,
355 container
= container_builder
.build()
357 # Add container to pod spec
358 pod_spec_builder
.add_container(container
)
360 # Add ingress resources to pod spec if site url exists
362 parsed
= urlparse(config
.site_url
)
364 "nginx.ingress.kubernetes.io/proxy-body-size": "{}".format(
365 str(config
.max_file_size
) + "m"
366 if config
.max_file_size
> 0
367 else config
.max_file_size
370 if config
.ingress_class
:
371 annotations
["kubernetes.io/ingress.class"] = config
.ingress_class
372 ingress_resource_builder
= IngressResourceV3Builder(
373 f
"{self.app.name}-ingress", annotations
376 if config
.ingress_whitelist_source_range
:
378 "nginx.ingress.kubernetes.io/whitelist-source-range"
379 ] = config
.ingress_whitelist_source_range
381 if parsed
.scheme
== "https":
382 ingress_resource_builder
.add_tls(
383 [parsed
.hostname
], config
.tls_secret_name
386 annotations
["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
388 ingress_resource_builder
.add_rule(parsed
.hostname
, self
.app
.name
, PORT
)
389 ingress_resource
= ingress_resource_builder
.build()
390 pod_spec_builder
.add_ingress_resource(ingress_resource
)
391 return pod_spec_builder
.build()
394 if __name__
== "__main__":
397 # LOGGER = logging.getLogger(__name__)
400 # class ConfigurePodEvent(EventBase):
401 # """Configure Pod event"""
406 # class KeystoneEvents(CharmEvents):
407 # """Keystone Events"""
409 # configure_pod = EventSource(ConfigurePodEvent)
411 # class KeystoneCharm(CharmBase):
412 # """Keystone K8s Charm"""
414 # state = StoredState()
415 # on = KeystoneEvents()
417 # def __init__(self, *args) -> NoReturn:
418 # """Constructor of the Charm object.
419 # Initializes internal state and register events it can handle.
421 # super().__init__(*args)
422 # self.state.set_default(db_host=None)
423 # self.state.set_default(db_port=None)
424 # self.state.set_default(db_user=None)
425 # self.state.set_default(db_password=None)
426 # self.state.set_default(pod_spec=None)
427 # self.state.set_default(fernet_keys=None)
428 # self.state.set_default(credential_keys=None)
429 # self.state.set_default(keys_timestamp=0)
431 # # Register all of the events we want to observe
432 # self.framework.observe(self.on.config_changed, self.configure_pod)
433 # self.framework.observe(self.on.start, self.configure_pod)
434 # self.framework.observe(self.on.upgrade_charm, self.configure_pod)
435 # self.framework.observe(self.on.leader_elected, self.configure_pod)
436 # self.framework.observe(self.on.update_status, self.configure_pod)
438 # # Registering custom internal events
439 # self.framework.observe(self.on.configure_pod, self.configure_pod)
441 # # Register relation events
442 # self.framework.observe(
443 # self.on.db_relation_changed, self._on_db_relation_changed
445 # self.framework.observe(
446 # self.on.db_relation_broken, self._on_db_relation_broken
448 # self.framework.observe(
449 # self.on.keystone_relation_joined, self._publish_keystone_info
452 # def _publish_keystone_info(self, event: EventBase) -> NoReturn:
453 # """Publishes keystone information for NBI usage through the keystone
457 # event (EventBase): Keystone relation event to update NBI.
459 # config = self.model.config
461 # "host": f"http://{self.app.name}:{KEYSTONE_PORT}/v3",
462 # "port": str(KEYSTONE_PORT),
463 # "keystone_db_password": config["keystone_db_password"],
464 # "region_id": config["region_id"],
465 # "user_domain_name": config["user_domain_name"],
466 # "project_domain_name": config["project_domain_name"],
467 # "admin_username": config["admin_username"],
468 # "admin_password": config["admin_password"],
469 # "admin_project_name": config["admin_project"],
470 # "username": config["service_username"],
471 # "password": config["service_password"],
472 # "service": config["service_project"],
474 # for k, v in rel_data.items():
475 # event.relation.data[self.model.unit][k] = v
477 # def _on_db_relation_changed(self, event: EventBase) -> NoReturn:
478 # """Reads information about the DB relation, in order for keystone to
482 # event (EventBase): DB relation event to access database
485 # if not event.unit in event.relation.data:
487 # relation_data = event.relation.data[event.unit]
488 # db_host = relation_data.get("host")
489 # db_port = int(relation_data.get("port", 3306))
491 # db_password = relation_data.get("root_password")
499 # self.state.db_host != db_host
500 # or self.state.db_port != db_port
501 # or self.state.db_user != db_user
502 # or self.state.db_password != db_password
505 # self.state.db_host = db_host
506 # self.state.db_port = db_port
507 # self.state.db_user = db_user
508 # self.state.db_password = db_password
509 # self.on.configure_pod.emit()
512 # def _on_db_relation_broken(self, event: EventBase) -> NoReturn:
513 # """Clears data from db relation.
516 # event (EventBase): DB relation event.
519 # self.state.db_host = None
520 # self.state.db_port = None
521 # self.state.db_user = None
522 # self.state.db_password = None
523 # self.on.configure_pod.emit()
525 # def _check_settings(self) -> str:
526 # """Check if there any settings missing from Keystone configuration.
529 # str: Information about the problems found (if any).
532 # config = self.model.config
534 # for setting in REQUIRED_SETTINGS:
535 # if not config.get(setting):
536 # problem = f"missing config {setting}"
537 # problems.append(problem)
539 # return ";".join(problems)
541 # def _make_pod_image_details(self) -> Dict[str, str]:
542 # """Generate the pod image details.
545 # Dict[str, str]: pod image details.
547 # config = self.model.config
549 # "imagePath": config["image"],
551 # if config["image_username"]:
552 # image_details.update(
554 # "username": config["image_username"],
555 # "password": config["image_password"],
558 # return image_details
560 # def _make_pod_ports(self) -> List[Dict[str, Any]]:
561 # """Generate the pod ports details.
564 # List[Dict[str, Any]]: pod ports details.
567 # {"name": "keystone", "containerPort": KEYSTONE_PORT, "protocol": "TCP"},
570 # def _make_pod_envconfig(self) -> Dict[str, Any]:
571 # """Generate pod environment configuraiton.
574 # Dict[str, Any]: pod environment configuration.
576 # config = self.model.config
579 # "DB_HOST": self.state.db_host,
580 # "DB_PORT": self.state.db_port,
581 # "ROOT_DB_USER": self.state.db_user,
582 # "ROOT_DB_PASSWORD": self.state.db_password,
583 # "KEYSTONE_DB_PASSWORD": config["keystone_db_password"],
584 # "REGION_ID": config["region_id"],
585 # "KEYSTONE_HOST": self.app.name,
586 # "ADMIN_USERNAME": config["admin_username"],
587 # "ADMIN_PASSWORD": config["admin_password"],
588 # "ADMIN_PROJECT": config["admin_project"],
589 # "SERVICE_USERNAME": config["service_username"],
590 # "SERVICE_PASSWORD": config["service_password"],
591 # "SERVICE_PROJECT": config["service_project"],
594 # if config.get("ldap_enabled"):
595 # envconfig["LDAP_AUTHENTICATION_DOMAIN_NAME"] = config[
596 # "ldap_authentication_domain_name"
598 # envconfig["LDAP_URL"] = config["ldap_url"]
599 # envconfig["LDAP_PAGE_SIZE"] = config["ldap_page_size"]
600 # envconfig["LDAP_USER_OBJECTCLASS"] = config["ldap_user_objectclass"]
601 # envconfig["LDAP_USER_ID_ATTRIBUTE"] = config["ldap_user_id_attribute"]
602 # envconfig["LDAP_USER_NAME_ATTRIBUTE"] = config["ldap_user_name_attribute"]
603 # envconfig["LDAP_USER_PASS_ATTRIBUTE"] = config["ldap_user_pass_attribute"]
604 # envconfig["LDAP_USER_ENABLED_MASK"] = config["ldap_user_enabled_mask"]
605 # envconfig["LDAP_USER_ENABLED_DEFAULT"] = config["ldap_user_enabled_default"]
606 # envconfig["LDAP_USER_ENABLED_INVERT"] = config["ldap_user_enabled_invert"]
607 # envconfig["LDAP_GROUP_OBJECTCLASS"] = config["ldap_group_objectclass"]
609 # if config["ldap_bind_user"]:
610 # envconfig["LDAP_BIND_USER"] = config["ldap_bind_user"]
612 # if config["ldap_bind_password"]:
613 # envconfig["LDAP_BIND_PASSWORD"] = config["ldap_bind_password"]
615 # if config["ldap_user_tree_dn"]:
616 # envconfig["LDAP_USER_TREE_DN"] = config["ldap_user_tree_dn"]
618 # if config["ldap_user_filter"]:
619 # envconfig["LDAP_USER_FILTER"] = config["ldap_user_filter"]
621 # if config["ldap_user_enabled_attribute"]:
622 # envconfig["LDAP_USER_ENABLED_ATTRIBUTE"] = config[
623 # "ldap_user_enabled_attribute"
626 # if config["ldap_chase_referrals"]:
627 # envconfig["LDAP_CHASE_REFERRALS"] = config["ldap_chase_referrals"]
629 # if config["ldap_group_tree_dn"]:
630 # envconfig["LDAP_GROUP_TREE_DN"] = config["ldap_group_tree_dn"]
632 # if config["ldap_use_starttls"]:
633 # envconfig["LDAP_USE_STARTTLS"] = config["ldap_use_starttls"]
634 # envconfig["LDAP_TLS_CACERT_BASE64"] = config["ldap_tls_cacert_base64"]
635 # envconfig["LDAP_TLS_REQ_CERT"] = config["ldap_tls_req_cert"]
639 # def _make_pod_ingress_resources(self) -> List[Dict[str, Any]]:
640 # """Generate pod ingress resources.
643 # List[Dict[str, Any]]: pod ingress resources.
645 # site_url = self.model.config["site_url"]
650 # parsed = urlparse(site_url)
652 # if not parsed.scheme.startswith("http"):
655 # max_file_size = self.model.config["max_file_size"]
656 # ingress_whitelist_source_range = self.model.config[
657 # "ingress_whitelist_source_range"
661 # "nginx.ingress.kubernetes.io/proxy-body-size": "{}m".format(max_file_size)
664 # if ingress_whitelist_source_range:
666 # "nginx.ingress.kubernetes.io/whitelist-source-range"
667 # ] = ingress_whitelist_source_range
669 # ingress_spec_tls = None
671 # if parsed.scheme == "https":
672 # ingress_spec_tls = [{"hosts": [parsed.hostname]}]
673 # tls_secret_name = self.model.config["tls_secret_name"]
674 # if tls_secret_name:
675 # ingress_spec_tls[0]["secretName"] = tls_secret_name
677 # annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
680 # "name": "{}-ingress".format(self.app.name),
681 # "annotations": annotations,
685 # "host": parsed.hostname,
691 # "serviceName": self.app.name,
692 # "servicePort": KEYSTONE_PORT,
701 # if ingress_spec_tls:
702 # ingress["spec"]["tls"] = ingress_spec_tls
706 # def _generate_keys(self) -> Tuple[List[str], List[str]]:
707 # """Generating new fernet tokens.
710 # Tuple[List[str], List[str]]: contains two lists of strings. First
711 # list contains strings that represent
712 # the keys for fernet and the second
713 # list contains strins that represent
714 # the keys for credentials.
717 # Fernet.generate_key().decode() for _ in range(NUMBER_FERNET_KEYS)
719 # credential_keys = [
720 # Fernet.generate_key().decode() for _ in range(NUMBER_CREDENTIAL_KEYS)
723 # return (fernet_keys, credential_keys)
725 # def configure_pod(self, event: EventBase) -> NoReturn:
726 # """Assemble the pod spec and apply it, if possible.
729 # event (EventBase): Hook or Relation event that started the
732 # if not self.state.db_host:
733 # self.unit.status = WaitingStatus("Waiting for database relation")
737 # if not self.unit.is_leader():
738 # self.unit.status = ActiveStatus("ready")
741 # if fernet_keys := self.state.fernet_keys:
742 # fernet_keys = json.loads(fernet_keys)
744 # if credential_keys := self.state.credential_keys:
745 # credential_keys = json.loads(credential_keys)
747 # now = datetime.now().timestamp()
748 # keys_timestamp = self.state.keys_timestamp
749 # token_expiration = self.model.config["token_expiration"]
751 # valid_keys = (now - keys_timestamp) < token_expiration
752 # if not credential_keys or not fernet_keys or not valid_keys:
753 # fernet_keys, credential_keys = self._generate_keys()
754 # self.state.fernet_keys = json.dumps(fernet_keys)
755 # self.state.credential_keys = json.dumps(credential_keys)
756 # self.state.keys_timestamp = now
758 # # Check problems in the settings
759 # problems = self._check_settings()
761 # self.unit.status = BlockedStatus(problems)
764 # self.unit.status = BlockedStatus("Assembling pod spec")
765 # image_details = self._make_pod_image_details()
766 # ports = self._make_pod_ports()
767 # env_config = self._make_pod_envconfig()
768 # ingress_resources = self._make_pod_ingress_resources()
769 # files = self._make_pod_files(fernet_keys, credential_keys)
775 # "name": self.framework.model.app.name,
776 # "imageDetails": image_details,
778 # "envConfig": env_config,
779 # "volumeConfig": files,
782 # "kubernetesResources": {"ingressResources": ingress_resources or []},
785 # if self.state.pod_spec != (
786 # pod_spec_json := json.dumps(pod_spec, sort_keys=True)
788 # self.state.pod_spec = pod_spec_json
789 # self.model.pod.set_spec(pod_spec)
791 # self.unit.status = ActiveStatus("ready")
794 # if __name__ == "__main__":
795 # main(KeystoneCharm)