Skip to content
Snippets Groups Projects
charm.py 33.5 KiB
Newer Older
#!/usr/bin/env python3
# Copyright 2021 Canonical Ltd.
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#         http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
# For those usages not covered by the Apache License, Version 2.0 please
# contact: legal@canonical.com
#
# To get in touch with the maintainers, please contact:
# osm-charmers@lists.launchpad.net
##

# pylint: disable=E0213

from datetime import datetime
from ipaddress import ip_network
import json
import logging
from typing import List, NoReturn, Optional, Tuple
from urllib.parse import urlparse

from cryptography.fernet import Fernet
from ops.main import main
from opslib.osm.charm import CharmedOsmBase, RelationsMissing
from opslib.osm.interfaces.keystone import KeystoneServer
from opslib.osm.interfaces.mysql import MysqlClient
from opslib.osm.pod import (
    ContainerV3Builder,
    FilesV3Builder,
    IngressResourceV3Builder,
    PodRestartPolicy,
    PodSpecV3Builder,
from opslib.osm.validator import ModelValidator, validator


logger = logging.getLogger(__name__)

garciadav's avatar
garciadav committed
REQUIRED_SETTINGS = ["token_expiration"]
# This is hardcoded in the keystone container script
DATABASE_NAME = "keystone"

# We expect the keystone container to use the default port
PORT = 5000
# Number of keys need might need to be adjusted in the future
NUMBER_FERNET_KEYS = 2
NUMBER_CREDENTIAL_KEYS = 2

# Path for keys
CREDENTIAL_KEYS_PATH = "/etc/keystone/credential-keys"
FERNET_KEYS_PATH = "/etc/keystone/fernet-keys"

class ConfigModel(ModelValidator):
    region_id: str
    keystone_db_password: str
    admin_username: str
    admin_password: str
    admin_project: str
    service_username: str
    service_password: str
    service_project: str
    user_domain_name: str
    project_domain_name: str
    token_expiration: int
    max_file_size: int
    site_url: Optional[str]
    ingress_class: Optional[str]
    ingress_whitelist_source_range: Optional[str]
    tls_secret_name: Optional[str]
    mysql_host: Optional[str]
    mysql_port: Optional[int]
    mysql_root_password: Optional[str]
    image_pull_policy: str
    security_context: bool

    @validator("max_file_size")
    def validate_max_file_size(cls, v):
        if v < 0:
            raise ValueError("value must be equal or greater than 0")
        return v

    @validator("site_url")
    def validate_site_url(cls, v):
        if v:
            parsed = urlparse(v)
            if not parsed.scheme.startswith("http"):
                raise ValueError("value must start with http")
        return v

    @validator("ingress_whitelist_source_range")
    def validate_ingress_whitelist_source_range(cls, v):
        if v:
            ip_network(v)
        return v

    @validator("mysql_port")
    def validate_mysql_port(cls, v):
        if v and (v <= 0 or v >= 65535):
            raise ValueError("Mysql port out of range")
        return v

    @validator("image_pull_policy")
    def validate_image_pull_policy(cls, v):
        values = {
            "always": "Always",
            "ifnotpresent": "IfNotPresent",
            "never": "Never",
        }
        v = v.lower()
        if v not in values.keys():
            raise ValueError("value must be always, ifnotpresent or never")
        return values[v]


class ConfigLdapModel(ModelValidator):
    ldap_enabled: bool
    ldap_authentication_domain_name: Optional[str]
    ldap_url: Optional[str]
    ldap_bind_user: Optional[str]
    ldap_bind_password: Optional[str]
    ldap_chase_referrals: Optional[str]
    ldap_page_size: Optional[int]
    ldap_user_tree_dn: Optional[str]
    ldap_user_objectclass: Optional[str]
    ldap_user_id_attribute: Optional[str]
    ldap_user_name_attribute: Optional[str]
    ldap_user_pass_attribute: Optional[str]
    ldap_user_filter: Optional[str]
    ldap_user_enabled_attribute: Optional[str]
    ldap_user_enabled_mask: Optional[int]
    ldap_user_enabled_default: Optional[str]
    ldap_user_enabled_invert: Optional[bool]
    ldap_group_objectclass: Optional[str]
    ldap_group_tree_dn: Optional[str]
    ldap_use_starttls: Optional[bool]
    ldap_tls_cacert_base64: Optional[str]
    ldap_tls_req_cert: Optional[str]

    @validator
    def validate_ldap_user_enabled_default(cls, v):
        if v:
            if v not in ["true", "false"]:
                raise ValueError('must be equal to "true" or "false"')
        return v


class KeystoneCharm(CharmedOsmBase):
    def __init__(self, *args) -> NoReturn:
        super().__init__(
            *args,
            oci_image="image",
            mysql_uri=True,
        )
        self.state.set_default(fernet_keys=None)
        self.state.set_default(credential_keys=None)
        self.state.set_default(keys_timestamp=0)
        self.keystone_server = KeystoneServer(self, "keystone")
        self.mysql_client = MysqlClient(self, "db")
        self.framework.observe(self.on["db"].relation_changed, self.configure_pod)
        self.framework.observe(self.on["db"].relation_broken, self.configure_pod)
        self.framework.observe(self.on.update_status, self.configure_pod)
        self.framework.observe(
            self.on["keystone"].relation_joined, self._publish_keystone_info
    def _publish_keystone_info(self, event):
        if self.unit.is_leader():
            config = ConfigModel(**dict(self.config))
            self.keystone_server.publish_info(
                host=f"http://{self.app.name}:{PORT}/v3",
                port=PORT,
                user_domain_name=config.user_domain_name,
                project_domain_name=config.project_domain_name,
                username=config.service_username,
                password=config.service_password,
                service=config.service_project,
                keystone_db_password=config.keystone_db_password,
                region_id=config.region_id,
                admin_username=config.admin_username,
                admin_password=config.admin_password,
                admin_project_name=config.admin_project,
    def _check_missing_dependencies(self, config: ConfigModel, external_db: bool):
        missing_relations = []
        if not external_db and self.mysql_client.is_missing_data_in_unit():
            missing_relations.append("mysql")
        if missing_relations:
            raise RelationsMissing(missing_relations)
    def _generate_keys(self) -> Tuple[List[str], List[str]]:
        """Generating new fernet tokens.

        Returns:
            Tuple[List[str], List[str]]: contains two lists of strings. First
                                         list contains strings that represent
                                         the keys for fernet and the second
                                         list contains strins that represent
                                         the keys for credentials.
        """
        fernet_keys = [
            Fernet.generate_key().decode() for _ in range(NUMBER_FERNET_KEYS)
        ]
        credential_keys = [
            Fernet.generate_key().decode() for _ in range(NUMBER_CREDENTIAL_KEYS)
        ]
        return (fernet_keys, credential_keys)

    def _get_keys(self):
        keys_timestamp = self.state.keys_timestamp
        if fernet_keys := self.state.fernet_keys:
            fernet_keys = json.loads(fernet_keys)

        if credential_keys := self.state.credential_keys:
            credential_keys = json.loads(credential_keys)

        now = datetime.now().timestamp()
        token_expiration = self.config["token_expiration"]

        valid_keys = (now - keys_timestamp) < token_expiration
        if not credential_keys or not fernet_keys or not valid_keys:
            fernet_keys, credential_keys = self._generate_keys()
            self.state.fernet_keys = json.dumps(fernet_keys)
            self.state.credential_keys = json.dumps(credential_keys)
            self.state.keys_timestamp = now
        return credential_keys, fernet_keys

    def _build_files(
        self, config: ConfigModel, credential_keys: List, fernet_keys: List
    ):
        credentials_files_builder = FilesV3Builder()
        fernet_files_builder = FilesV3Builder()
        for key_id, _ in enumerate(credential_keys):
            credentials_files_builder.add_file(str(key_id), str(key_id), secret=True)
        for key_id, _ in enumerate(fernet_keys):
            fernet_files_builder.add_file(str(key_id), str(key_id), secret=True)
        return credentials_files_builder.build(), fernet_files_builder.build()

    def build_pod_spec(self, image_info, **kwargs):
        # Validate config
        config = ConfigModel(**dict(self.config))
        mysql_config = kwargs["mysql_config"]
        config_ldap = ConfigLdapModel(**dict(self.config))
        if mysql_config.mysql_uri and not self.mysql_client.is_missing_data_in_unit():
            raise Exception("Mysql data cannot be provided via config and relation")
        # Check relations
        external_db = True if mysql_config.mysql_uri else False
        self._check_missing_dependencies(config, external_db)
        # Create Builder for the PodSpec
        pod_spec_builder = PodSpecV3Builder(
            enable_security_context=config.security_context
        )
        container_builder = ContainerV3Builder(
            self.app.name,
            image_info,
            config.image_pull_policy,
            run_as_non_root=config.security_context,
        # Build files
        credential_keys, fernet_keys = self._get_keys()
        credential_files, fernet_files = self._build_files(
            config, credential_keys, fernet_keys
        )

        # Add pod secrets
        fernet_keys_secret_name = f"{self.app.name}-fernet-keys-secret"
        pod_spec_builder.add_secret(
            fernet_keys_secret_name,
            {str(key_id): value for (key_id, value) in enumerate(fernet_keys)},
        )
        credential_keys_secret_name = f"{self.app.name}-credential-keys-secret"
        pod_spec_builder.add_secret(
            credential_keys_secret_name,
            {str(key_id): value for (key_id, value) in enumerate(credential_keys)},
        )
        mysql_secret_name = f"{self.app.name}-mysql-secret"

        pod_spec_builder.add_secret(
            mysql_secret_name,
            {
                "host": mysql_config.host,
                "port": str(mysql_config.port),
                "user": mysql_config.username,
                "password": mysql_config.password,
            }
            if mysql_config.mysql_uri
            else {
                "host": self.mysql_client.host,
                "port": str(self.mysql_client.port),
                "user": "root",
                "password": self.mysql_client.root_password,
            },
        )
        keystone_secret_name = f"{self.app.name}-keystone-secret"
        pod_spec_builder.add_secret(
            keystone_secret_name,
            {
                "db_password": config.keystone_db_password,
                "admin_username": config.admin_username,
                "admin_password": config.admin_password,
                "admin_project": config.admin_project,
                "service_username": config.service_username,
                "service_password": config.service_password,
                "service_project": config.service_project,
            },
        )
        # Build Container
        container_builder.add_volume_config(
            "credential-keys",
            CREDENTIAL_KEYS_PATH,
            credential_files,
            secret_name=credential_keys_secret_name,
        )
        container_builder.add_volume_config(
            "fernet-keys",
            FERNET_KEYS_PATH,
            fernet_files,
            secret_name=fernet_keys_secret_name,
        container_builder.add_port(name=self.app.name, port=PORT)
        container_builder.add_envs(
            {
                "REGION_ID": config.region_id,
                "KEYSTONE_HOST": self.app.name,
            }
        )
        container_builder.add_secret_envs(
            secret_name=mysql_secret_name,
            envs={
                "DB_HOST": "host",
                "DB_PORT": "port",
                "ROOT_DB_USER": "user",
                "ROOT_DB_PASSWORD": "password",
            },
        )
        container_builder.add_secret_envs(
            secret_name=keystone_secret_name,
            envs={
                "KEYSTONE_DB_PASSWORD": "db_password",
                "ADMIN_USERNAME": "admin_username",
                "ADMIN_PASSWORD": "admin_password",
                "ADMIN_PROJECT": "admin_project",
                "SERVICE_USERNAME": "service_username",
                "SERVICE_PASSWORD": "service_password",
                "SERVICE_PROJECT": "service_project",
            },
        )
        ldap_secret_name = f"{self.app.name}-ldap-secret"
        if config_ldap.ldap_enabled:
            # Add ldap secrets and envs
            ldap_secrets = {
                "authentication_domain_name": config_ldap.ldap_authentication_domain_name,
                "url": config_ldap.ldap_url,
                "page_size": str(config_ldap.ldap_page_size),
                "user_objectclass": config_ldap.ldap_user_objectclass,
                "user_id_attribute": config_ldap.ldap_user_id_attribute,
                "user_name_attribute": config_ldap.ldap_user_name_attribute,
                "user_pass_attribute": config_ldap.ldap_user_pass_attribute,
                "user_enabled_mask": str(config_ldap.ldap_user_enabled_mask),
                "user_enabled_default": config_ldap.ldap_user_enabled_default,
                "user_enabled_invert": str(config_ldap.ldap_user_enabled_invert),
                "group_objectclass": config_ldap.ldap_group_objectclass,
            }
            ldap_envs = {
                "LDAP_AUTHENTICATION_DOMAIN_NAME": "authentication_domain_name",
                "LDAP_URL": "url",
                "LDAP_PAGE_SIZE": "page_size",
                "LDAP_USER_OBJECTCLASS": "user_objectclass",
                "LDAP_USER_ID_ATTRIBUTE": "user_id_attribute",
                "LDAP_USER_NAME_ATTRIBUTE": "user_name_attribute",
                "LDAP_USER_PASS_ATTRIBUTE": "user_pass_attribute",
                "LDAP_USER_ENABLED_MASK": "user_enabled_mask",
                "LDAP_USER_ENABLED_DEFAULT": "user_enabled_default",
                "LDAP_USER_ENABLED_INVERT": "user_enabled_invert",
                "LDAP_GROUP_OBJECTCLASS": "group_objectclass",
            }
            if config_ldap.ldap_bind_user:
                ldap_secrets["bind_user"] = config_ldap.ldap_bind_user
                ldap_envs["LDAP_BIND_USER"] = "bind_user"

            if config_ldap.ldap_bind_password:
                ldap_secrets["bind_password"] = config_ldap.ldap_bind_password
                ldap_envs["LDAP_BIND_PASSWORD"] = "bind_password"

            if config_ldap.ldap_user_tree_dn:
                ldap_secrets["user_tree_dn"] = config_ldap.ldap_user_tree_dn
                ldap_envs["LDAP_USER_TREE_DN"] = "user_tree_dn"

            if config_ldap.ldap_user_filter:
                ldap_secrets["user_filter"] = config_ldap.ldap_user_filter
                ldap_envs["LDAP_USER_FILTER"] = "user_filter"

            if config_ldap.ldap_user_enabled_attribute:
                ldap_secrets[
                    "user_enabled_attribute"
                ] = config_ldap.ldap_user_enabled_attribute
                ldap_envs["LDAP_USER_ENABLED_ATTRIBUTE"] = "user_enabled_attribute"
            if config_ldap.ldap_chase_referrals:
                ldap_secrets["chase_referrals"] = config_ldap.ldap_chase_referrals
                ldap_envs["LDAP_CHASE_REFERRALS"] = "chase_referrals"
            if config_ldap.ldap_group_tree_dn:
                ldap_secrets["group_tree_dn"] = config_ldap.ldap_group_tree_dn
                ldap_envs["LDAP_GROUP_TREE_DN"] = "group_tree_dn"
            if config_ldap.ldap_tls_cacert_base64:
                ldap_secrets["tls_cacert_base64"] = config_ldap.ldap_tls_cacert_base64
                ldap_envs["LDAP_TLS_CACERT_BASE64"] = "tls_cacert_base64"
            if config_ldap.ldap_use_starttls:
                ldap_secrets["use_starttls"] = str(config_ldap.ldap_use_starttls)
                ldap_secrets["tls_cacert_base64"] = config_ldap.ldap_tls_cacert_base64
                ldap_secrets["tls_req_cert"] = config_ldap.ldap_tls_req_cert
                ldap_envs["LDAP_USE_STARTTLS"] = "use_starttls"
                ldap_envs["LDAP_TLS_CACERT_BASE64"] = "tls_cacert_base64"
                ldap_envs["LDAP_TLS_REQ_CERT"] = "tls_req_cert"

            pod_spec_builder.add_secret(
                ldap_secret_name,
                ldap_secrets,
            )
            container_builder.add_secret_envs(
                secret_name=ldap_secret_name,
                envs=ldap_envs,
            )

        container = container_builder.build()
        # Add container to pod spec
        pod_spec_builder.add_container(container)
        # Add Pod Restart Policy
        restart_policy = PodRestartPolicy()
        restart_policy.add_secrets(
            secret_names=(mysql_secret_name, keystone_secret_name, ldap_secret_name)
        )
        pod_spec_builder.set_restart_policy(restart_policy)

        # Add ingress resources to pod spec if site url exists
        if config.site_url:
            parsed = urlparse(config.site_url)
            annotations = {
                "nginx.ingress.kubernetes.io/proxy-body-size": "{}".format(
                    str(config.max_file_size) + "m"
                    if config.max_file_size > 0
                    else config.max_file_size
            if config.ingress_class:
                annotations["kubernetes.io/ingress.class"] = config.ingress_class
            ingress_resource_builder = IngressResourceV3Builder(
                f"{self.app.name}-ingress", annotations
            )

            if config.ingress_whitelist_source_range:
                annotations[
                    "nginx.ingress.kubernetes.io/whitelist-source-range"
                ] = config.ingress_whitelist_source_range

            if parsed.scheme == "https":
                ingress_resource_builder.add_tls(
                    [parsed.hostname], config.tls_secret_name
                )
            else:
                annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"

            ingress_resource_builder.add_rule(parsed.hostname, self.app.name, PORT)
            ingress_resource = ingress_resource_builder.build()
            pod_spec_builder.add_ingress_resource(ingress_resource)
        return pod_spec_builder.build()


if __name__ == "__main__":
    main(KeystoneCharm)
496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895

# LOGGER = logging.getLogger(__name__)


# class ConfigurePodEvent(EventBase):
#     """Configure Pod event"""

#     pass


# class KeystoneEvents(CharmEvents):
#     """Keystone Events"""

#     configure_pod = EventSource(ConfigurePodEvent)

# class KeystoneCharm(CharmBase):
#     """Keystone K8s Charm"""

#     state = StoredState()
#     on = KeystoneEvents()

#     def __init__(self, *args) -> NoReturn:
#         """Constructor of the Charm object.
#         Initializes internal state and register events it can handle.
#         """
#         super().__init__(*args)
#         self.state.set_default(db_host=None)
#         self.state.set_default(db_port=None)
#         self.state.set_default(db_user=None)
#         self.state.set_default(db_password=None)
#         self.state.set_default(pod_spec=None)
#         self.state.set_default(fernet_keys=None)
#         self.state.set_default(credential_keys=None)
#         self.state.set_default(keys_timestamp=0)

#         # Register all of the events we want to observe
#         self.framework.observe(self.on.config_changed, self.configure_pod)
#         self.framework.observe(self.on.start, self.configure_pod)
#         self.framework.observe(self.on.upgrade_charm, self.configure_pod)
#         self.framework.observe(self.on.leader_elected, self.configure_pod)
#         self.framework.observe(self.on.update_status, self.configure_pod)

#         # Registering custom internal events
#         self.framework.observe(self.on.configure_pod, self.configure_pod)

#         # Register relation events
#         self.framework.observe(
#             self.on.db_relation_changed, self._on_db_relation_changed
#         )
#         self.framework.observe(
#             self.on.db_relation_broken, self._on_db_relation_broken
#         )
#         self.framework.observe(
#             self.on.keystone_relation_joined, self._publish_keystone_info
#         )

#     def _publish_keystone_info(self, event: EventBase) -> NoReturn:
#         """Publishes keystone information for NBI usage through the keystone
#            relation.

#         Args:
#             event (EventBase): Keystone relation event to update NBI.
#         """
#         config = self.model.config
#         rel_data = {
#             "host": f"http://{self.app.name}:{KEYSTONE_PORT}/v3",
#             "port": str(KEYSTONE_PORT),
#             "keystone_db_password": config["keystone_db_password"],
#             "region_id": config["region_id"],
#             "user_domain_name": config["user_domain_name"],
#             "project_domain_name": config["project_domain_name"],
#             "admin_username": config["admin_username"],
#             "admin_password": config["admin_password"],
#             "admin_project_name": config["admin_project"],
#             "username": config["service_username"],
#             "password": config["service_password"],
#             "service": config["service_project"],
#         }
#         for k, v in rel_data.items():
#             event.relation.data[self.model.unit][k] = v

#     def _on_db_relation_changed(self, event: EventBase) -> NoReturn:
#         """Reads information about the DB relation, in order for keystone to
#            access it.

#         Args:
#             event (EventBase): DB relation event to access database
#                                information.
#         """
#         if not event.unit in event.relation.data:
#             return
#         relation_data = event.relation.data[event.unit]
#         db_host = relation_data.get("host")
#         db_port = int(relation_data.get("port", 3306))
#         db_user = "root"
#         db_password = relation_data.get("root_password")

#         if (
#             db_host
#             and db_port
#             and db_user
#             and db_password
#             and (
#                 self.state.db_host != db_host
#                 or self.state.db_port != db_port
#                 or self.state.db_user != db_user
#                 or self.state.db_password != db_password
#             )
#         ):
#             self.state.db_host = db_host
#             self.state.db_port = db_port
#             self.state.db_user = db_user
#             self.state.db_password = db_password
#             self.on.configure_pod.emit()


#     def _on_db_relation_broken(self, event: EventBase) -> NoReturn:
#         """Clears data from db relation.

#         Args:
#             event (EventBase): DB relation event.

#         """
#         self.state.db_host = None
#         self.state.db_port = None
#         self.state.db_user = None
#         self.state.db_password = None
#         self.on.configure_pod.emit()

#     def _check_settings(self) -> str:
#         """Check if there any settings missing from Keystone configuration.

#         Returns:
#             str: Information about the problems found (if any).
#         """
#         problems = []
#         config = self.model.config

#         for setting in REQUIRED_SETTINGS:
#             if not config.get(setting):
#                 problem = f"missing config {setting}"
#                 problems.append(problem)

#         return ";".join(problems)

#     def _make_pod_image_details(self) -> Dict[str, str]:
#         """Generate the pod image details.

#         Returns:
#             Dict[str, str]: pod image details.
#         """
#         config = self.model.config
#         image_details = {
#             "imagePath": config["image"],
#         }
#         if config["image_username"]:
#             image_details.update(
#                 {
#                     "username": config["image_username"],
#                     "password": config["image_password"],
#                 }
#             )
#         return image_details

#     def _make_pod_ports(self) -> List[Dict[str, Any]]:
#         """Generate the pod ports details.

#         Returns:
#             List[Dict[str, Any]]: pod ports details.
#         """
#         return [
#             {"name": "keystone", "containerPort": KEYSTONE_PORT, "protocol": "TCP"},
#         ]

#     def _make_pod_envconfig(self) -> Dict[str, Any]:
#         """Generate pod environment configuraiton.

#         Returns:
#             Dict[str, Any]: pod environment configuration.
#         """
#         config = self.model.config

#         envconfig = {
#             "DB_HOST": self.state.db_host,
#             "DB_PORT": self.state.db_port,
#             "ROOT_DB_USER": self.state.db_user,
#             "ROOT_DB_PASSWORD": self.state.db_password,
#             "KEYSTONE_DB_PASSWORD": config["keystone_db_password"],
#             "REGION_ID": config["region_id"],
#             "KEYSTONE_HOST": self.app.name,
#             "ADMIN_USERNAME": config["admin_username"],
#             "ADMIN_PASSWORD": config["admin_password"],
#             "ADMIN_PROJECT": config["admin_project"],
#             "SERVICE_USERNAME": config["service_username"],
#             "SERVICE_PASSWORD": config["service_password"],
#             "SERVICE_PROJECT": config["service_project"],
#         }

#         if config.get("ldap_enabled"):
#             envconfig["LDAP_AUTHENTICATION_DOMAIN_NAME"] = config[
#                 "ldap_authentication_domain_name"
#             ]
#             envconfig["LDAP_URL"] = config["ldap_url"]
#             envconfig["LDAP_PAGE_SIZE"] = config["ldap_page_size"]
#             envconfig["LDAP_USER_OBJECTCLASS"] = config["ldap_user_objectclass"]
#             envconfig["LDAP_USER_ID_ATTRIBUTE"] = config["ldap_user_id_attribute"]
#             envconfig["LDAP_USER_NAME_ATTRIBUTE"] = config["ldap_user_name_attribute"]
#             envconfig["LDAP_USER_PASS_ATTRIBUTE"] = config["ldap_user_pass_attribute"]
#             envconfig["LDAP_USER_ENABLED_MASK"] = config["ldap_user_enabled_mask"]
#             envconfig["LDAP_USER_ENABLED_DEFAULT"] = config["ldap_user_enabled_default"]
#             envconfig["LDAP_USER_ENABLED_INVERT"] = config["ldap_user_enabled_invert"]
#             envconfig["LDAP_GROUP_OBJECTCLASS"] = config["ldap_group_objectclass"]

#             if config["ldap_bind_user"]:
#                 envconfig["LDAP_BIND_USER"] = config["ldap_bind_user"]

#             if config["ldap_bind_password"]:
#                 envconfig["LDAP_BIND_PASSWORD"] = config["ldap_bind_password"]

#             if config["ldap_user_tree_dn"]:
#                 envconfig["LDAP_USER_TREE_DN"] = config["ldap_user_tree_dn"]

#             if config["ldap_user_filter"]:
#                 envconfig["LDAP_USER_FILTER"] = config["ldap_user_filter"]

#             if config["ldap_user_enabled_attribute"]:
#                 envconfig["LDAP_USER_ENABLED_ATTRIBUTE"] = config[
#                     "ldap_user_enabled_attribute"
#                 ]

#             if config["ldap_chase_referrals"]:
#                 envconfig["LDAP_CHASE_REFERRALS"] = config["ldap_chase_referrals"]

#             if config["ldap_group_tree_dn"]:
#                 envconfig["LDAP_GROUP_TREE_DN"] = config["ldap_group_tree_dn"]

#             if config["ldap_use_starttls"]:
#                 envconfig["LDAP_USE_STARTTLS"] = config["ldap_use_starttls"]
#                 envconfig["LDAP_TLS_CACERT_BASE64"] = config["ldap_tls_cacert_base64"]
#                 envconfig["LDAP_TLS_REQ_CERT"] = config["ldap_tls_req_cert"]

#         return envconfig

#     def _make_pod_ingress_resources(self) -> List[Dict[str, Any]]:
#         """Generate pod ingress resources.

#         Returns:
#             List[Dict[str, Any]]: pod ingress resources.
#         """
#         site_url = self.model.config["site_url"]

#         if not site_url:
#             return

#         parsed = urlparse(site_url)

#         if not parsed.scheme.startswith("http"):
#             return

#         max_file_size = self.model.config["max_file_size"]
#         ingress_whitelist_source_range = self.model.config[
#             "ingress_whitelist_source_range"
#         ]

#         annotations = {
#             "nginx.ingress.kubernetes.io/proxy-body-size": "{}m".format(max_file_size)
#         }

#         if ingress_whitelist_source_range:
#             annotations[
#                 "nginx.ingress.kubernetes.io/whitelist-source-range"
#             ] = ingress_whitelist_source_range

#         ingress_spec_tls = None

#         if parsed.scheme == "https":
#             ingress_spec_tls = [{"hosts": [parsed.hostname]}]
#             tls_secret_name = self.model.config["tls_secret_name"]
#             if tls_secret_name:
#                 ingress_spec_tls[0]["secretName"] = tls_secret_name
#         else:
#             annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"

#         ingress = {
#             "name": "{}-ingress".format(self.app.name),
#             "annotations": annotations,
#             "spec": {
#                 "rules": [
#                     {
#                         "host": parsed.hostname,
#                         "http": {
#                             "paths": [
#                                 {
#                                     "path": "/",
#                                     "backend": {
#                                         "serviceName": self.app.name,
#                                         "servicePort": KEYSTONE_PORT,
#                                     },
#                                 }
#                             ]
#                         },
#                     }
#                 ],
#             },
#         }
#         if ingress_spec_tls:
#             ingress["spec"]["tls"] = ingress_spec_tls

#         return [ingress]

#     def _generate_keys(self) -> Tuple[List[str], List[str]]:
#         """Generating new fernet tokens.

#         Returns:
#             Tuple[List[str], List[str]]: contains two lists of strings. First
#                                          list contains strings that represent
#                                          the keys for fernet and the second
#                                          list contains strins that represent
#                                          the keys for credentials.
#         """
#         fernet_keys = [
#             Fernet.generate_key().decode() for _ in range(NUMBER_FERNET_KEYS)
#         ]
#         credential_keys = [
#             Fernet.generate_key().decode() for _ in range(NUMBER_CREDENTIAL_KEYS)
#         ]

#         return (fernet_keys, credential_keys)

#     def configure_pod(self, event: EventBase) -> NoReturn:
#         """Assemble the pod spec and apply it, if possible.

#         Args:
#             event (EventBase): Hook or Relation event that started the
#                                function.
#         """
#         if not self.state.db_host:
#             self.unit.status = WaitingStatus("Waiting for database relation")
#             event.defer()
#             return

#         if not self.unit.is_leader():
#             self.unit.status = ActiveStatus("ready")
#             return

#         if fernet_keys := self.state.fernet_keys:
#             fernet_keys = json.loads(fernet_keys)

#         if credential_keys := self.state.credential_keys:
#             credential_keys = json.loads(credential_keys)

#         now = datetime.now().timestamp()
#         keys_timestamp = self.state.keys_timestamp
#         token_expiration = self.model.config["token_expiration"]

#         valid_keys = (now - keys_timestamp) < token_expiration
#         if not credential_keys or not fernet_keys or not valid_keys:
#             fernet_keys, credential_keys = self._generate_keys()
#             self.state.fernet_keys = json.dumps(fernet_keys)
#             self.state.credential_keys = json.dumps(credential_keys)
#             self.state.keys_timestamp = now

#         # Check problems in the settings
#         problems = self._check_settings()
#         if problems:
#             self.unit.status = BlockedStatus(problems)
#             return

#         self.unit.status = BlockedStatus("Assembling pod spec")
#         image_details = self._make_pod_image_details()
#         ports = self._make_pod_ports()
#         env_config = self._make_pod_envconfig()
#         ingress_resources = self._make_pod_ingress_resources()
#         files = self._make_pod_files(fernet_keys, credential_keys)

#         pod_spec = {
#             "version": 3,
#             "containers": [
#                 {
#                     "name": self.framework.model.app.name,
#                     "imageDetails": image_details,
#                     "ports": ports,
#                     "envConfig": env_config,
#                     "volumeConfig": files,
#                 }
#             ],
#             "kubernetesResources": {"ingressResources": ingress_resources or []},
#         }

#         if self.state.pod_spec != (
#             pod_spec_json := json.dumps(pod_spec, sort_keys=True)
#         ):
#             self.state.pod_spec = pod_spec_json
#             self.model.pod.set_spec(pod_spec)

#         self.unit.status = ActiveStatus("ready")


# if __name__ == "__main__":
#     main(KeystoneCharm)