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
28 from cryptography
.fernet
import Fernet
29 from datetime
import datetime
30 from typing
import Optional
, NoReturn
, List
, Tuple
31 from ipaddress
import ip_network
32 from urllib
.parse
import urlparse
34 from ops
.main
import main
36 from opslib
.osm
.charm
import CharmedOsmBase
, RelationsMissing
38 from opslib
.osm
.pod
import (
42 IngressResourceV3Builder
,
46 from opslib
.osm
.validator
import (
51 from opslib
.osm
.interfaces
.mysql
import MysqlClient
52 from opslib
.osm
.interfaces
.keystone
import KeystoneServer
55 logger
= logging
.getLogger(__name__
)
58 REQUIRED_SETTINGS
= ["token_expiration"]
60 # This is hardcoded in the keystone container script
61 DATABASE_NAME
= "keystone"
63 # We expect the keystone container to use the default port
66 # Number of keys need might need to be adjusted in the future
67 NUMBER_FERNET_KEYS
= 2
68 NUMBER_CREDENTIAL_KEYS
= 2
71 CREDENTIAL_KEYS_PATH
= "/etc/keystone/credential-keys"
72 FERNET_KEYS_PATH
= "/etc/keystone/fernet-keys"
75 class ConfigModel(ModelValidator
):
77 keystone_db_password
: str
85 project_domain_name
: str
88 site_url
: Optional
[str]
89 ingress_whitelist_source_range
: Optional
[str]
90 tls_secret_name
: Optional
[str]
92 @validator("max_file_size")
93 def validate_max_file_size(cls
, v
):
95 raise ValueError("value must be equal or greater than 0")
98 @validator("site_url")
99 def validate_site_url(cls
, v
):
102 if not parsed
.scheme
.startswith("http"):
103 raise ValueError("value must start with http")
106 @validator("ingress_whitelist_source_range")
107 def validate_ingress_whitelist_source_range(cls
, v
):
113 class ConfigLdapModel(ModelValidator
):
115 ldap_authentication_domain_name
: Optional
[str]
116 ldap_url
: Optional
[str]
117 ldap_bind_user
: Optional
[str]
118 ldap_bind_password
: Optional
[str]
119 ldap_chase_referrals
: Optional
[str]
120 ldap_page_size
: Optional
[int]
121 ldap_user_tree_dn
: Optional
[str]
122 ldap_user_objectclass
: Optional
[str]
123 ldap_user_id_attribute
: Optional
[str]
124 ldap_user_name_attribute
: Optional
[str]
125 ldap_user_pass_attribute
: Optional
[str]
126 ldap_user_filter
: Optional
[str]
127 ldap_user_enabled_attribute
: Optional
[str]
128 ldap_user_enabled_mask
: Optional
[int]
129 ldap_user_enabled_default
: Optional
[bool]
130 ldap_user_enabled_invert
: Optional
[bool]
131 ldap_group_objectclass
: Optional
[str]
132 ldap_group_tree_dn
: Optional
[str]
133 ldap_use_starttls
: Optional
[bool]
134 ldap_tls_cacert_base64
: Optional
[str]
135 ldap_tls_req_cert
: Optional
[str]
138 class KeystoneCharm(CharmedOsmBase
):
139 def __init__(self
, *args
) -> NoReturn
:
140 super().__init
__(*args
, oci_image
="image")
141 self
.state
.set_default(fernet_keys
=None)
142 self
.state
.set_default(credential_keys
=None)
143 self
.state
.set_default(keys_timestamp
=0)
145 self
.keystone_server
= KeystoneServer(self
, "keystone")
146 self
.mysql_client
= MysqlClient(self
, "db")
147 self
.framework
.observe(self
.on
["db"].relation_changed
, self
.configure_pod
)
148 self
.framework
.observe(self
.on
["db"].relation_broken
, self
.configure_pod
)
150 self
.framework
.observe(
151 self
.on
["keystone"].relation_joined
, self
._publish
_keystone
_info
154 def _publish_keystone_info(self
, event
):
155 if self
.unit
.is_leader():
156 config
= ConfigModel(**dict(self
.config
))
157 self
.keystone_server
.publish_info(
158 host
=f
"http://{self.app.name}:{PORT}/v3",
160 user_domain_name
=config
.user_domain_name
,
161 project_domain_name
=config
.project_domain_name
,
162 username
=config
.service_username
,
163 password
=config
.service_password
,
164 service
=config
.service_project
,
165 keystone_db_password
=config
.keystone_db_password
,
166 region_id
=config
.region_id
,
167 admin_username
=config
.admin_username
,
168 admin_password
=config
.admin_password
,
169 admin_project_name
=config
.admin_project
,
172 def _check_missing_dependencies(self
, config
: ConfigModel
):
173 missing_relations
= []
174 if self
.mysql_client
.is_missing_data_in_unit():
175 missing_relations
.append("mysql")
176 if missing_relations
:
177 raise RelationsMissing(missing_relations
)
179 def _generate_keys(self
) -> Tuple
[List
[str], List
[str]]:
180 """Generating new fernet tokens.
183 Tuple[List[str], List[str]]: contains two lists of strings. First
184 list contains strings that represent
185 the keys for fernet and the second
186 list contains strins that represent
187 the keys for credentials.
190 Fernet
.generate_key().decode() for _
in range(NUMBER_FERNET_KEYS
)
193 Fernet
.generate_key().decode() for _
in range(NUMBER_CREDENTIAL_KEYS
)
196 return (fernet_keys
, credential_keys
)
199 keys_timestamp
= self
.state
.keys_timestamp
200 if fernet_keys
:= self
.state
.fernet_keys
:
201 fernet_keys
= json
.loads(fernet_keys
)
203 if credential_keys
:= self
.state
.credential_keys
:
204 credential_keys
= json
.loads(credential_keys
)
206 now
= datetime
.now().timestamp()
207 token_expiration
= self
.config
["token_expiration"]
209 valid_keys
= (now
- keys_timestamp
) < token_expiration
210 if not credential_keys
or not fernet_keys
or not valid_keys
:
211 fernet_keys
, credential_keys
= self
._generate
_keys
()
212 self
.state
.fernet_keys
= json
.dumps(fernet_keys
)
213 self
.state
.credential_keys
= json
.dumps(credential_keys
)
214 self
.state
.keys_timestamp
= now
215 return credential_keys
, fernet_keys
217 def _build_files(self
, config
: ConfigModel
):
218 credentials_files_builder
= FilesV3Builder()
219 fernet_files_builder
= FilesV3Builder()
221 credential_keys
, fernet_keys
= self
._get
_keys
()
223 for (key_id
, value
) in enumerate(credential_keys
):
224 credentials_files_builder
.add_file(str(key_id
), value
)
225 for (key_id
, value
) in enumerate(fernet_keys
):
226 fernet_files_builder
.add_file(str(key_id
), value
)
227 return credentials_files_builder
.build(), fernet_files_builder
.build()
229 def build_pod_spec(self
, image_info
):
231 config
= ConfigModel(**dict(self
.config
))
232 config_ldap
= ConfigLdapModel(**dict(self
.config
))
234 self
._check
_missing
_dependencies
(config
)
235 # Create Builder for the PodSpec
236 pod_spec_builder
= PodSpecV3Builder()
238 container_builder
= ContainerV3Builder(self
.app
.name
, image_info
)
239 container_builder
.add_port(name
=self
.app
.name
, port
=PORT
)
241 credential_files
, fernet_files
= self
._build
_files
(config
)
242 container_builder
.add_volume_config(
243 "credential-keys", CREDENTIAL_KEYS_PATH
, credential_files
245 container_builder
.add_volume_config(
246 "fernet-keys", FERNET_KEYS_PATH
, fernet_files
248 container_builder
.add_envs(
250 "DB_HOST": self
.mysql_client
.host
,
251 "DB_PORT": self
.mysql_client
.port
,
252 "ROOT_DB_USER": "root",
253 "ROOT_DB_PASSWORD": self
.mysql_client
.root_password
,
254 "KEYSTONE_DB_PASSWORD": config
.keystone_db_password
,
255 "REGION_ID": config
.region_id
,
256 "KEYSTONE_HOST": self
.app
.name
,
257 "ADMIN_USERNAME": config
.admin_username
,
258 "ADMIN_PASSWORD": config
.admin_password
,
259 "ADMIN_PROJECT": config
.admin_project
,
260 "SERVICE_USERNAME": config
.service_username
,
261 "SERVICE_PASSWORD": config
.service_password
,
262 "SERVICE_PROJECT": config
.service_project
,
266 if config_ldap
.ldap_enabled
:
268 container_builder
.add_envs(
270 "LDAP_AUTHENTICATION_DOMAIN_NAME": config_ldap
.ldap_authentication_domain_name
,
271 "LDAP_URL": config_ldap
.ldap_url
,
272 "LDAP_PAGE_SIZE": config_ldap
.ldap_page_size
,
273 "LDAP_USER_OBJECTCLASS": config_ldap
.ldap_user_objectclass
,
274 "LDAP_USER_ID_ATTRIBUTE": config_ldap
.ldap_user_id_attribute
,
275 "LDAP_USER_NAME_ATTRIBUTE": config_ldap
.ldap_user_name_attribute
,
276 "LDAP_USER_PASS_ATTRIBUTE": config_ldap
.ldap_user_pass_attribute
,
277 "LDAP_USER_ENABLED_MASK": config_ldap
.ldap_user_enabled_mask
,
278 "LDAP_USER_ENABLED_DEFAULT": config_ldap
.ldap_user_enabled_default
,
279 "LDAP_USER_ENABLED_INVERT": config_ldap
.ldap_user_enabled_invert
,
280 "LDAP_GROUP_OBJECTCLASS": config_ldap
.ldap_group_objectclass
,
283 if config_ldap
.ldap_bind_user
:
284 container_builder
.add_envs(
285 {"LDAP_BIND_USER": config_ldap
.ldap_bind_user
}
288 if config_ldap
.ldap_bind_password
:
289 container_builder
.add_envs(
290 {"LDAP_BIND_PASSWORD": config_ldap
.ldap_bind_password
}
293 if config_ldap
.ldap_user_tree_dn
:
294 container_builder
.add_envs(
295 {"LDAP_USER_TREE_DN": config_ldap
.ldap_user_tree_dn
}
298 if config_ldap
.ldap_user_filter
:
299 container_builder
.add_envs(
300 {"LDAP_USER_FILTER": config_ldap
.ldap_user_filter
}
303 if config_ldap
.ldap_user_enabled_attribute
:
304 container_builder
.add_envs(
306 "LDAP_USER_ENABLED_ATTRIBUTE": config_ldap
.ldap_user_enabled_attribute
310 if config_ldap
.ldap_chase_referrals
:
311 container_builder
.add_envs(
312 {"LDAP_CHASE_REFERRALS": config_ldap
.ldap_chase_referrals
}
315 if config_ldap
.ldap_group_tree_dn
:
316 container_builder
.add_envs(
317 {"LDAP_GROUP_TREE_DN": config_ldap
.ldap_group_tree_dn
}
320 if config_ldap
.ldap_use_starttls
:
321 container_builder
.add_envs(
323 "LDAP_USE_STARTTLS": config_ldap
.ldap_use_starttls
,
324 "LDAP_TLS_CACERT_BASE64": config_ldap
.ldap_tls_cacert_base64
,
325 "LDAP_TLS_REQ_CERT": config_ldap
.ldap_tls_req_cert
,
328 container
= container_builder
.build()
329 # Add container to pod spec
330 pod_spec_builder
.add_container(container
)
331 # Add ingress resources to pod spec if site url exists
333 parsed
= urlparse(config
.site_url
)
335 "nginx.ingress.kubernetes.io/proxy-body-size": "{}".format(
336 str(config
.max_file_size
) + "m"
337 if config
.max_file_size
> 0
338 else config
.max_file_size
341 ingress_resource_builder
= IngressResourceV3Builder(
342 f
"{self.app.name}-ingress", annotations
345 if config
.ingress_whitelist_source_range
:
347 "nginx.ingress.kubernetes.io/whitelist-source-range"
348 ] = config
.ingress_whitelist_source_range
350 if parsed
.scheme
== "https":
351 ingress_resource_builder
.add_tls(
352 [parsed
.hostname
], config
.tls_secret_name
355 annotations
["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
357 ingress_resource_builder
.add_rule(parsed
.hostname
, self
.app
.name
, PORT
)
358 ingress_resource
= ingress_resource_builder
.build()
359 pod_spec_builder
.add_ingress_resource(ingress_resource
)
360 return pod_spec_builder
.build()
363 if __name__
== "__main__":
366 # LOGGER = logging.getLogger(__name__)
369 # class ConfigurePodEvent(EventBase):
370 # """Configure Pod event"""
375 # class KeystoneEvents(CharmEvents):
376 # """Keystone Events"""
378 # configure_pod = EventSource(ConfigurePodEvent)
380 # class KeystoneCharm(CharmBase):
381 # """Keystone K8s Charm"""
383 # state = StoredState()
384 # on = KeystoneEvents()
386 # def __init__(self, *args) -> NoReturn:
387 # """Constructor of the Charm object.
388 # Initializes internal state and register events it can handle.
390 # super().__init__(*args)
391 # self.state.set_default(db_host=None)
392 # self.state.set_default(db_port=None)
393 # self.state.set_default(db_user=None)
394 # self.state.set_default(db_password=None)
395 # self.state.set_default(pod_spec=None)
396 # self.state.set_default(fernet_keys=None)
397 # self.state.set_default(credential_keys=None)
398 # self.state.set_default(keys_timestamp=0)
400 # # Register all of the events we want to observe
401 # self.framework.observe(self.on.config_changed, self.configure_pod)
402 # self.framework.observe(self.on.start, self.configure_pod)
403 # self.framework.observe(self.on.upgrade_charm, self.configure_pod)
404 # self.framework.observe(self.on.leader_elected, self.configure_pod)
405 # self.framework.observe(self.on.update_status, self.configure_pod)
407 # # Registering custom internal events
408 # self.framework.observe(self.on.configure_pod, self.configure_pod)
410 # # Register relation events
411 # self.framework.observe(
412 # self.on.db_relation_changed, self._on_db_relation_changed
414 # self.framework.observe(
415 # self.on.db_relation_broken, self._on_db_relation_broken
417 # self.framework.observe(
418 # self.on.keystone_relation_joined, self._publish_keystone_info
421 # def _publish_keystone_info(self, event: EventBase) -> NoReturn:
422 # """Publishes keystone information for NBI usage through the keystone
426 # event (EventBase): Keystone relation event to update NBI.
428 # config = self.model.config
430 # "host": f"http://{self.app.name}:{KEYSTONE_PORT}/v3",
431 # "port": str(KEYSTONE_PORT),
432 # "keystone_db_password": config["keystone_db_password"],
433 # "region_id": config["region_id"],
434 # "user_domain_name": config["user_domain_name"],
435 # "project_domain_name": config["project_domain_name"],
436 # "admin_username": config["admin_username"],
437 # "admin_password": config["admin_password"],
438 # "admin_project_name": config["admin_project"],
439 # "username": config["service_username"],
440 # "password": config["service_password"],
441 # "service": config["service_project"],
443 # for k, v in rel_data.items():
444 # event.relation.data[self.model.unit][k] = v
446 # def _on_db_relation_changed(self, event: EventBase) -> NoReturn:
447 # """Reads information about the DB relation, in order for keystone to
451 # event (EventBase): DB relation event to access database
454 # if not event.unit in event.relation.data:
456 # relation_data = event.relation.data[event.unit]
457 # db_host = relation_data.get("host")
458 # db_port = int(relation_data.get("port", 3306))
460 # db_password = relation_data.get("root_password")
468 # self.state.db_host != db_host
469 # or self.state.db_port != db_port
470 # or self.state.db_user != db_user
471 # or self.state.db_password != db_password
474 # self.state.db_host = db_host
475 # self.state.db_port = db_port
476 # self.state.db_user = db_user
477 # self.state.db_password = db_password
478 # self.on.configure_pod.emit()
481 # def _on_db_relation_broken(self, event: EventBase) -> NoReturn:
482 # """Clears data from db relation.
485 # event (EventBase): DB relation event.
488 # self.state.db_host = None
489 # self.state.db_port = None
490 # self.state.db_user = None
491 # self.state.db_password = None
492 # self.on.configure_pod.emit()
494 # def _check_settings(self) -> str:
495 # """Check if there any settings missing from Keystone configuration.
498 # str: Information about the problems found (if any).
501 # config = self.model.config
503 # for setting in REQUIRED_SETTINGS:
504 # if not config.get(setting):
505 # problem = f"missing config {setting}"
506 # problems.append(problem)
508 # return ";".join(problems)
510 # def _make_pod_image_details(self) -> Dict[str, str]:
511 # """Generate the pod image details.
514 # Dict[str, str]: pod image details.
516 # config = self.model.config
518 # "imagePath": config["image"],
520 # if config["image_username"]:
521 # image_details.update(
523 # "username": config["image_username"],
524 # "password": config["image_password"],
527 # return image_details
529 # def _make_pod_ports(self) -> List[Dict[str, Any]]:
530 # """Generate the pod ports details.
533 # List[Dict[str, Any]]: pod ports details.
536 # {"name": "keystone", "containerPort": KEYSTONE_PORT, "protocol": "TCP"},
539 # def _make_pod_envconfig(self) -> Dict[str, Any]:
540 # """Generate pod environment configuraiton.
543 # Dict[str, Any]: pod environment configuration.
545 # config = self.model.config
548 # "DB_HOST": self.state.db_host,
549 # "DB_PORT": self.state.db_port,
550 # "ROOT_DB_USER": self.state.db_user,
551 # "ROOT_DB_PASSWORD": self.state.db_password,
552 # "KEYSTONE_DB_PASSWORD": config["keystone_db_password"],
553 # "REGION_ID": config["region_id"],
554 # "KEYSTONE_HOST": self.app.name,
555 # "ADMIN_USERNAME": config["admin_username"],
556 # "ADMIN_PASSWORD": config["admin_password"],
557 # "ADMIN_PROJECT": config["admin_project"],
558 # "SERVICE_USERNAME": config["service_username"],
559 # "SERVICE_PASSWORD": config["service_password"],
560 # "SERVICE_PROJECT": config["service_project"],
563 # if config.get("ldap_enabled"):
564 # envconfig["LDAP_AUTHENTICATION_DOMAIN_NAME"] = config[
565 # "ldap_authentication_domain_name"
567 # envconfig["LDAP_URL"] = config["ldap_url"]
568 # envconfig["LDAP_PAGE_SIZE"] = config["ldap_page_size"]
569 # envconfig["LDAP_USER_OBJECTCLASS"] = config["ldap_user_objectclass"]
570 # envconfig["LDAP_USER_ID_ATTRIBUTE"] = config["ldap_user_id_attribute"]
571 # envconfig["LDAP_USER_NAME_ATTRIBUTE"] = config["ldap_user_name_attribute"]
572 # envconfig["LDAP_USER_PASS_ATTRIBUTE"] = config["ldap_user_pass_attribute"]
573 # envconfig["LDAP_USER_ENABLED_MASK"] = config["ldap_user_enabled_mask"]
574 # envconfig["LDAP_USER_ENABLED_DEFAULT"] = config["ldap_user_enabled_default"]
575 # envconfig["LDAP_USER_ENABLED_INVERT"] = config["ldap_user_enabled_invert"]
576 # envconfig["LDAP_GROUP_OBJECTCLASS"] = config["ldap_group_objectclass"]
578 # if config["ldap_bind_user"]:
579 # envconfig["LDAP_BIND_USER"] = config["ldap_bind_user"]
581 # if config["ldap_bind_password"]:
582 # envconfig["LDAP_BIND_PASSWORD"] = config["ldap_bind_password"]
584 # if config["ldap_user_tree_dn"]:
585 # envconfig["LDAP_USER_TREE_DN"] = config["ldap_user_tree_dn"]
587 # if config["ldap_user_filter"]:
588 # envconfig["LDAP_USER_FILTER"] = config["ldap_user_filter"]
590 # if config["ldap_user_enabled_attribute"]:
591 # envconfig["LDAP_USER_ENABLED_ATTRIBUTE"] = config[
592 # "ldap_user_enabled_attribute"
595 # if config["ldap_chase_referrals"]:
596 # envconfig["LDAP_CHASE_REFERRALS"] = config["ldap_chase_referrals"]
598 # if config["ldap_group_tree_dn"]:
599 # envconfig["LDAP_GROUP_TREE_DN"] = config["ldap_group_tree_dn"]
601 # if config["ldap_use_starttls"]:
602 # envconfig["LDAP_USE_STARTTLS"] = config["ldap_use_starttls"]
603 # envconfig["LDAP_TLS_CACERT_BASE64"] = config["ldap_tls_cacert_base64"]
604 # envconfig["LDAP_TLS_REQ_CERT"] = config["ldap_tls_req_cert"]
608 # def _make_pod_ingress_resources(self) -> List[Dict[str, Any]]:
609 # """Generate pod ingress resources.
612 # List[Dict[str, Any]]: pod ingress resources.
614 # site_url = self.model.config["site_url"]
619 # parsed = urlparse(site_url)
621 # if not parsed.scheme.startswith("http"):
624 # max_file_size = self.model.config["max_file_size"]
625 # ingress_whitelist_source_range = self.model.config[
626 # "ingress_whitelist_source_range"
630 # "nginx.ingress.kubernetes.io/proxy-body-size": "{}m".format(max_file_size)
633 # if ingress_whitelist_source_range:
635 # "nginx.ingress.kubernetes.io/whitelist-source-range"
636 # ] = ingress_whitelist_source_range
638 # ingress_spec_tls = None
640 # if parsed.scheme == "https":
641 # ingress_spec_tls = [{"hosts": [parsed.hostname]}]
642 # tls_secret_name = self.model.config["tls_secret_name"]
643 # if tls_secret_name:
644 # ingress_spec_tls[0]["secretName"] = tls_secret_name
646 # annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
649 # "name": "{}-ingress".format(self.app.name),
650 # "annotations": annotations,
654 # "host": parsed.hostname,
660 # "serviceName": self.app.name,
661 # "servicePort": KEYSTONE_PORT,
670 # if ingress_spec_tls:
671 # ingress["spec"]["tls"] = ingress_spec_tls
675 # def _generate_keys(self) -> Tuple[List[str], List[str]]:
676 # """Generating new fernet tokens.
679 # Tuple[List[str], List[str]]: contains two lists of strings. First
680 # list contains strings that represent
681 # the keys for fernet and the second
682 # list contains strins that represent
683 # the keys for credentials.
686 # Fernet.generate_key().decode() for _ in range(NUMBER_FERNET_KEYS)
688 # credential_keys = [
689 # Fernet.generate_key().decode() for _ in range(NUMBER_CREDENTIAL_KEYS)
692 # return (fernet_keys, credential_keys)
694 # def configure_pod(self, event: EventBase) -> NoReturn:
695 # """Assemble the pod spec and apply it, if possible.
698 # event (EventBase): Hook or Relation event that started the
701 # if not self.state.db_host:
702 # self.unit.status = WaitingStatus("Waiting for database relation")
706 # if not self.unit.is_leader():
707 # self.unit.status = ActiveStatus("ready")
710 # if fernet_keys := self.state.fernet_keys:
711 # fernet_keys = json.loads(fernet_keys)
713 # if credential_keys := self.state.credential_keys:
714 # credential_keys = json.loads(credential_keys)
716 # now = datetime.now().timestamp()
717 # keys_timestamp = self.state.keys_timestamp
718 # token_expiration = self.model.config["token_expiration"]
720 # valid_keys = (now - keys_timestamp) < token_expiration
721 # if not credential_keys or not fernet_keys or not valid_keys:
722 # fernet_keys, credential_keys = self._generate_keys()
723 # self.state.fernet_keys = json.dumps(fernet_keys)
724 # self.state.credential_keys = json.dumps(credential_keys)
725 # self.state.keys_timestamp = now
727 # # Check problems in the settings
728 # problems = self._check_settings()
730 # self.unit.status = BlockedStatus(problems)
733 # self.unit.status = BlockedStatus("Assembling pod spec")
734 # image_details = self._make_pod_image_details()
735 # ports = self._make_pod_ports()
736 # env_config = self._make_pod_envconfig()
737 # ingress_resources = self._make_pod_ingress_resources()
738 # files = self._make_pod_files(fernet_keys, credential_keys)
744 # "name": self.framework.model.app.name,
745 # "imageDetails": image_details,
747 # "envConfig": env_config,
748 # "volumeConfig": files,
751 # "kubernetesResources": {"ingressResources": ingress_resources or []},
754 # if self.state.pod_spec != (
755 # pod_spec_json := json.dumps(pod_spec, sort_keys=True)
757 # self.state.pod_spec = pod_spec_json
758 # self.model.pod.set_spec(pod_spec)
760 # self.unit.status = ActiveStatus("ready")
763 # if __name__ == "__main__":
764 # main(KeystoneCharm)