Major improvement in OSM charms
[osm/devops.git] / installers / charm / keystone / src / charm.py
index d5c169b..72d7090 100755 (executable)
@@ -1,44 +1,59 @@
 #!/usr/bin/env python3
-# Copyright 2020 Canonical Ltd.
+# 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
+# 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
+#         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.
+# 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
+
 
 import json
 import logging
+from cryptography.fernet import Fernet
 from datetime import datetime
-from typing import (
-    Any,
-    Dict,
-    List,
-    NoReturn,
-    Tuple,
-)
+from typing import Optional, NoReturn, List, Tuple
+from ipaddress import ip_network
 from urllib.parse import urlparse
 
-from cryptography.fernet import Fernet
-
-from ops.charm import CharmBase, EventBase, CharmEvents
-from ops.framework import StoredState, EventSource
 from ops.main import main
-from ops.model import (
-    ActiveStatus,
-    BlockedStatus,
-    # MaintenanceStatus,
-    WaitingStatus,
-    # ModelError,
+
+from opslib.osm.charm import CharmedOsmBase, RelationsMissing
+
+from opslib.osm.pod import (
+    ContainerV3Builder,
+    PodSpecV3Builder,
+    FilesV3Builder,
+    IngressResourceV3Builder,
 )
 
-LOGGER = logging.getLogger(__name__)
+
+from opslib.osm.validator import (
+    ModelValidator,
+    validator,
+)
+
+from opslib.osm.interfaces.mysql import MysqlClient
+from opslib.osm.interfaces.keystone import KeystoneServer
+
+
+logger = logging.getLogger(__name__)
+
 
 REQUIRED_SETTINGS = ["token_expiration"]
 
@@ -46,7 +61,7 @@ REQUIRED_SETTINGS = ["token_expiration"]
 DATABASE_NAME = "keystone"
 
 # We expect the keystone container to use the default port
-KEYSTONE_PORT = 5000
+PORT = 5000
 
 # Number of keys need might need to be adjusted in the future
 NUMBER_FERNET_KEYS = 2
@@ -57,311 +72,109 @@ CREDENTIAL_KEYS_PATH = "/etc/keystone/credential-keys"
 FERNET_KEYS_PATH = "/etc/keystone/fernet-keys"
 
 
-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()
-
+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_whitelist_source_range: Optional[str]
+    tls_secret_name: Optional[str]
+
+    @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
+
+
+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[bool]
+    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]
+
+
+class KeystoneCharm(CharmedOsmBase):
     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)
+        super().__init__(*args, oci_image="image")
         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)
+        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)
 
-        # Register relation events
-        self.framework.observe(
-            self.on.db_relation_changed, self._on_db_relation_changed
-        )
-        self.framework.observe(
-            self.on.db_relation_departed, self._on_db_relation_departed
-        )
         self.framework.observe(
-            self.on.keystone_relation_joined, self._publish_keystone_info
+            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 event.unit not 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
+    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,
             )
-        ):
-            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_departed(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 _check_missing_dependencies(self, config: ConfigModel):
+        missing_relations = []
+        if 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.
@@ -382,58 +195,8 @@ class KeystoneCharm(CharmBase):
 
         return (fernet_keys, credential_keys)
 
-    def _make_pod_files(
-        self, fernet_keys: List[str], credential_keys: List[str]
-    ) -> List[Dict[str, Any]]:
-        """Generating ConfigMap information.
-
-        Args:
-            fernet_keys (List[str]): keys for fernet.
-            credential_keys (List[str]): keys for credentials.
-
-        Returns:
-            List[Dict[str, Any]]: ConfigMap information.
-        """
-        files = [
-            {
-                "name": "fernet-keys",
-                "mountPath": FERNET_KEYS_PATH,
-                "files": [
-                    {"path": str(key_id), "content": value}
-                    for (key_id, value) in enumerate(fernet_keys)
-                ],
-            }
-        ]
-
-        files.append(
-            {
-                "name": "credential-keys",
-                "mountPath": CREDENTIAL_KEYS_PATH,
-                "files": [
-                    {"path": str(key_id), "content": value}
-                    for (key_id, value) in enumerate(credential_keys)
-                ],
-            }
-        )
-
-        return files
-
-    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
-
+    def _get_keys(self):
+        keys_timestamp = self.state.keys_timestamp
         if fernet_keys := self.state.fernet_keys:
             fernet_keys = json.loads(fernet_keys)
 
@@ -441,8 +204,7 @@ class KeystoneCharm(CharmBase):
             credential_keys = json.loads(credential_keys)
 
         now = datetime.now().timestamp()
-        keys_timestamp = self.state.keys_timestamp
-        token_expiration = self.model.config["token_expiration"]
+        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:
@@ -450,42 +212,553 @@ class KeystoneCharm(CharmBase):
             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):
+        credentials_files_builder = FilesV3Builder()
+        fernet_files_builder = FilesV3Builder()
+
+        credential_keys, fernet_keys = self._get_keys()
+
+        for (key_id, value) in enumerate(credential_keys):
+            credentials_files_builder.add_file(str(key_id), value)
+        for (key_id, value) in enumerate(fernet_keys):
+            fernet_files_builder.add_file(str(key_id), value)
+        return credentials_files_builder.build(), fernet_files_builder.build()
+
+    def build_pod_spec(self, image_info):
+        # Validate config
+        config = ConfigModel(**dict(self.config))
+        config_ldap = ConfigLdapModel(**dict(self.config))
+        # Check relations
+        self._check_missing_dependencies(config)
+        # Create Builder for the PodSpec
+        pod_spec_builder = PodSpecV3Builder()
+        # Build Container
+        container_builder = ContainerV3Builder(self.app.name, image_info)
+        container_builder.add_port(name=self.app.name, port=PORT)
+        # Build files
+        credential_files, fernet_files = self._build_files(config)
+        container_builder.add_volume_config(
+            "credential-keys", CREDENTIAL_KEYS_PATH, credential_files
+        )
+        container_builder.add_volume_config(
+            "fernet-keys", FERNET_KEYS_PATH, fernet_files
+        )
+        container_builder.add_envs(
+            {
+                "DB_HOST": self.mysql_client.host,
+                "DB_PORT": self.mysql_client.port,
+                "ROOT_DB_USER": "root",
+                "ROOT_DB_PASSWORD": self.mysql_client.root_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,
+            }
+        )
 
-        # 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": [
+        if config_ldap.ldap_enabled:
+
+            container_builder.add_envs(
                 {
-                    "name": self.framework.model.app.name,
-                    "imageDetails": image_details,
-                    "ports": ports,
-                    "envConfig": env_config,
-                    "volumeConfig": files,
+                    "LDAP_AUTHENTICATION_DOMAIN_NAME": config_ldap.ldap_authentication_domain_name,
+                    "LDAP_URL": config_ldap.ldap_url,
+                    "LDAP_PAGE_SIZE": config_ldap.ldap_page_size,
+                    "LDAP_USER_OBJECTCLASS": config_ldap.ldap_user_objectclass,
+                    "LDAP_USER_ID_ATTRIBUTE": config_ldap.ldap_user_id_attribute,
+                    "LDAP_USER_NAME_ATTRIBUTE": config_ldap.ldap_user_name_attribute,
+                    "LDAP_USER_PASS_ATTRIBUTE": config_ldap.ldap_user_pass_attribute,
+                    "LDAP_USER_ENABLED_MASK": config_ldap.ldap_user_enabled_mask,
+                    "LDAP_USER_ENABLED_DEFAULT": config_ldap.ldap_user_enabled_default,
+                    "LDAP_USER_ENABLED_INVERT": config_ldap.ldap_user_enabled_invert,
+                    "LDAP_GROUP_OBJECTCLASS": config_ldap.ldap_group_objectclass,
                 }
-            ],
-            "kubernetesResources": {"ingressResources": ingress_resources or []},
-        }
+            )
+            if config_ldap.ldap_bind_user:
+                container_builder.add_envs(
+                    {"LDAP_BIND_USER": config_ldap.ldap_bind_user}
+                )
+
+            if config_ldap.ldap_bind_password:
+                container_builder.add_envs(
+                    {"LDAP_BIND_PASSWORD": config_ldap.ldap_bind_password}
+                )
+
+            if config_ldap.ldap_user_tree_dn:
+                container_builder.add_envs(
+                    {"LDAP_USER_TREE_DN": config_ldap.ldap_user_tree_dn}
+                )
+
+            if config_ldap.ldap_user_filter:
+                container_builder.add_envs(
+                    {"LDAP_USER_FILTER": config_ldap.ldap_user_filter}
+                )
+
+            if config_ldap.ldap_user_enabled_attribute:
+                container_builder.add_envs(
+                    {
+                        "LDAP_USER_ENABLED_ATTRIBUTE": config_ldap.ldap_user_enabled_attribute
+                    }
+                )
 
-        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)
+            if config_ldap.ldap_chase_referrals:
+                container_builder.add_envs(
+                    {"LDAP_CHASE_REFERRALS": config_ldap.ldap_chase_referrals}
+                )
 
-        self.unit.status = ActiveStatus("ready")
+            if config_ldap.ldap_group_tree_dn:
+                container_builder.add_envs(
+                    {"LDAP_GROUP_TREE_DN": config_ldap.ldap_group_tree_dn}
+                )
+
+            if config_ldap.ldap_use_starttls:
+                container_builder.add_envs(
+                    {
+                        "LDAP_USE_STARTTLS": config_ldap.ldap_use_starttls,
+                        "LDAP_TLS_CACERT_BASE64": config_ldap.ldap_tls_cacert_base64,
+                        "LDAP_TLS_REQ_CERT": config_ldap.ldap_tls_req_cert,
+                    }
+                )
+        container = container_builder.build()
+        # Add container to pod spec
+        pod_spec_builder.add_container(container)
+        # 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
+                ),
+            }
+            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)
+
+# 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)