Major improvement in OSM charms

- Adapt all new operator charms to use the same pattern. They are all
using now this library that encapsulates the common logic for all
charms: https://github.com/davigar15/ops-lib-charmed-osm. That will be
eventually moved to gitlab, when it has a PyPI repository available
- Add unit tests to all charms
- Modify installer and bundles to point to the new charms
- Improve the build.sh script for building the charms

Change-Id: I0896ceb082d1b6a76b3560c07482a4135a220a3f
Signed-off-by: David Garcia <david.garcia@canonical.com>
diff --git a/installers/charm/keystone/.gitignore b/installers/charm/keystone/.gitignore
index 43fcb62..493739e 100644
--- a/installers/charm/keystone/.gitignore
+++ b/installers/charm/keystone/.gitignore
@@ -1,20 +1,30 @@
-# 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
+##
+
+venv
 .vscode
-.tox
 build
-keystone.charm
+*.charm
+.coverage
+coverage.xml
 .stestr
-.coverage*
-cover/
\ No newline at end of file
+cover
+release
diff --git a/installers/charm/keystone/.jujuignore b/installers/charm/keystone/.jujuignore
new file mode 100644
index 0000000..bf04eb4
--- /dev/null
+++ b/installers/charm/keystone/.jujuignore
@@ -0,0 +1,28 @@
+# 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
+##
+
+venv
+.vscode
+build
+prometheus.charm
+.coverage
+.stestr
+cover
diff --git a/installers/charm/keystone/.yamllint.yaml b/installers/charm/keystone/.yamllint.yaml
index 08ab437..d71fb69 100644
--- a/installers/charm/keystone/.yamllint.yaml
+++ b/installers/charm/keystone/.yamllint.yaml
@@ -1,16 +1,24 @@
-# 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
+##
+
 ---
 extends: default
 
@@ -20,6 +28,7 @@
   - ".yamllint"
 ignore: |
   .tox
+  cover/
   build/
-  mod/
-  lib/
+  venv
+  release/
diff --git a/installers/charm/keystone/config.yaml b/installers/charm/keystone/config.yaml
index 06ea060..a606d8e 100644
--- a/installers/charm/keystone/config.yaml
+++ b/installers/charm/keystone/config.yaml
@@ -12,21 +12,6 @@
 #     See the License for the specific language governing permissions and
 #     limitations under the License.
 options:
-  image:
-    type: string
-    default: opensourcemano/keystone:latest
-    description: The docker image to install.
-  image_username:
-    type: string
-    description: |
-      The username for accessing the registry specified in image.
-    default: ""
-  image_password:
-    type: string
-    description: |
-      The password associated with image_username for accessing
-      the registry specified in image.
-    default: ""
   max_file_size:
     type: int
     description: |
diff --git a/installers/charm/keystone/metadata.yaml b/installers/charm/keystone/metadata.yaml
index c04c121..38c03ed 100644
--- a/installers/charm/keystone/metadata.yaml
+++ b/installers/charm/keystone/metadata.yaml
@@ -18,6 +18,11 @@
 series:
     - kubernetes
 min-juju-version: 2.8.0
+resources:
+    image:
+        type: oci-image
+        description: OSM docker image for NBI
+        upstream-source: "opensourcemano/keystone:latest"
 requires:
     db:
         interface: mysql
diff --git a/installers/charm/keystone/requirements-test.txt b/installers/charm/keystone/requirements-test.txt
new file mode 100644
index 0000000..d7585f3
--- /dev/null
+++ b/installers/charm/keystone/requirements-test.txt
@@ -0,0 +1,31 @@
+# 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
+##
+-r requirements.txt
+coverage
+stestr
+mock
+black
+yamllint
+flake8
+safety
+requests-mock
+asynctest
+nose2
\ No newline at end of file
diff --git a/installers/charm/keystone/requirements.txt b/installers/charm/keystone/requirements.txt
index 5a4c0af..d42bd9e 100644
--- a/installers/charm/keystone/requirements.txt
+++ b/installers/charm/keystone/requirements.txt
@@ -1,15 +1,22 @@
-# 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
+##
 cryptography
-ops
+git+https://github.com/davigar15/ops-lib-charmed-osm/@e7f26cd29b322e175a23cadbe4546b7f2bbf111c
\ No newline at end of file
diff --git a/installers/charm/keystone/src/charm.py b/installers/charm/keystone/src/charm.py
index d5c169b..72d7090 100755
--- a/installers/charm/keystone/src/charm.py
+++ b/installers/charm/keystone/src/charm.py
@@ -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 @@
 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 @@
 FERNET_KEYS_PATH = "/etc/keystone/fernet-keys"
 
 
-class ConfigurePodEvent(EventBase):
-    """Configure Pod event"""
+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]
 
-    pass
+    @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 KeystoneEvents(CharmEvents):
-    """Keystone Events"""
-
-    configure_pod = EventSource(ConfigurePodEvent)
+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(CharmBase):
-    """Keystone K8s Charm"""
-
-    state = StoredState()
-    on = KeystoneEvents()
-
+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)
+        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)
 
-        # 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_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 @@
 
         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 @@
             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 @@
             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
 
-        # Check problems in the settings
-        problems = self._check_settings()
-        if problems:
-            self.unit.status = BlockedStatus(problems)
-            return
+    def _build_files(self, config: ConfigModel):
+        credentials_files_builder = FilesV3Builder()
+        fernet_files_builder = FilesV3Builder()
 
-        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)
+        credential_keys, fernet_keys = self._get_keys()
 
-        pod_spec = {
-            "version": 3,
-            "containers": [
+        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,
+            }
+        )
+
+        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 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_bind_password:
+                container_builder.add_envs(
+                    {"LDAP_BIND_PASSWORD": config_ldap.ldap_bind_password}
+                )
 
-        self.unit.status = ActiveStatus("ready")
+            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 config_ldap.ldap_chase_referrals:
+                container_builder.add_envs(
+                    {"LDAP_CHASE_REFERRALS": config_ldap.ldap_chase_referrals}
+                )
+
+            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)
diff --git a/installers/charm/keystone/tests/__init__.py b/installers/charm/keystone/tests/__init__.py
index 6004c6d..d0d973a 100644
--- a/installers/charm/keystone/tests/__init__.py
+++ b/installers/charm/keystone/tests/__init__.py
@@ -22,10 +22,10 @@
 
 """Init mocking for unit tests."""
 
-# import sys
-# import mock
+import sys
+import mock
 
-# sys.path.append("src")
+sys.path.append("src")
 
-# oci_image = mock.MagicMock()
-# sys.modules["oci_image"] = oci_image
+oci_image = mock.MagicMock()
+sys.modules["oci_image"] = oci_image
diff --git a/installers/charm/keystone/tests/test_charm.py b/installers/charm/keystone/tests/test_charm.py
index 8cb8604..756a5e4 100644
--- a/installers/charm/keystone/tests/test_charm.py
+++ b/installers/charm/keystone/tests/test_charm.py
@@ -1,5 +1,5 @@
 #!/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
@@ -20,9 +20,10 @@
 # osm-charmers@lists.launchpad.net
 ##
 
+import sys
 from typing import NoReturn
 import unittest
-
+from ops.model import ActiveStatus, BlockedStatus
 from ops.testing import Harness
 
 from charm import KeystoneCharm
@@ -33,14 +34,162 @@
 
     def setUp(self) -> NoReturn:
         """Test setup"""
+        self.image_info = sys.modules["oci_image"].OCIImageResource().fetch()
         self.harness = Harness(KeystoneCharm)
         self.harness.set_leader(is_leader=True)
         self.harness.begin()
+        self.config = {
+            "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": 10,
+            "max_file_size": 1,
+            "site_url": "http://keystone.com",
+            "ldap_enabled": False,
+        }
+        self.harness.update_config(self.config)
 
-    def test_on_start_without_relations(self) -> NoReturn:
-        """Test installation without any relation."""
+    def test_config_changed_no_relations(
+        self,
+    ) -> NoReturn:
+        """Test ingress resources without HTTP."""
+
         self.harness.charm.on.config_changed.emit()
 
+        # Assertions
+        self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus)
+        self.assertTrue(
+            all(
+                relation in self.harness.charm.unit.status.message
+                for relation in ["mysql"]
+            )
+        )
+
+    def test_config_changed_non_leader(
+        self,
+    ) -> NoReturn:
+        """Test ingress resources without HTTP."""
+        self.harness.set_leader(is_leader=False)
+        self.harness.charm.on.config_changed.emit()
+
+        # Assertions
+        self.assertIsInstance(self.harness.charm.unit.status, ActiveStatus)
+
+    def test_with_relations(
+        self,
+    ) -> NoReturn:
+        "Test with relations"
+        self.initialize_mysql_relation()
+        # Verifying status
+        self.assertNotIsInstance(self.harness.charm.unit.status, BlockedStatus)
+
+    def initialize_mysql_relation(self):
+        relation_id = self.harness.add_relation("db", "mysql")
+        self.harness.add_relation_unit(relation_id, "mysql/0")
+        self.harness.update_relation_data(
+            relation_id,
+            "mysql/0",
+            {
+                "host": "mysql",
+                "port": 3306,
+                "user": "mano",
+                "password": "manopw",
+                "root_password": "rootmanopw",
+            },
+        )
+
 
 if __name__ == "__main__":
     unittest.main()
+
+
+# class TestCharm(unittest.TestCase):
+#     """Prometheus Charm unit tests."""
+
+#     def setUp(self) -> NoReturn:
+#         """Test setup"""
+#         self.image_info = sys.modules["oci_image"].OCIImageResource().fetch()
+#         self.harness = Harness(KeystoneCharm)
+#         self.harness.set_leader(is_leader=True)
+#         self.harness.begin()
+#         self.config = {
+#             "enable_ng_ro": True,
+#             "database_commonkey": "commonkey",
+#             "log_level": "INFO",
+#             "vim_database": "db_name",
+#             "ro_database": "ro_db_name",
+#             "openmano_tenant": "mano",
+#         }
+
+#     def test_config_changed_no_relations(
+#         self,
+#     ) -> NoReturn:
+#         """Test ingress resources without HTTP."""
+
+#         self.harness.charm.on.config_changed.emit()
+
+#         # Assertions
+#         self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus)
+#         self.assertTrue(
+#             all(
+#                 relation in self.harness.charm.unit.status.message
+#                 for relation in ["mongodb", "kafka"]
+#             )
+#         )
+
+#         # Disable ng-ro
+#         self.harness.update_config({"enable_ng_ro": False})
+#         self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus)
+#         self.assertTrue(
+#             all(
+#                 relation in self.harness.charm.unit.status.message
+#                 for relation in ["mysql"]
+#             )
+#         )
+
+#     def test_config_changed_non_leader(
+#         self,
+#     ) -> NoReturn:
+#         """Test ingress resources without HTTP."""
+#         self.harness.set_leader(is_leader=False)
+#         self.harness.charm.on.config_changed.emit()
+
+#         # Assertions
+#         self.assertIsInstance(self.harness.charm.unit.status, ActiveStatus)
+
+#     def test_with_relations_ng(
+#         self,
+#     ) -> NoReturn:
+#         "Test with relations (ng-ro)"
+
+#         # Initializing the kafka relation
+#         kafka_relation_id = self.harness.add_relation("kafka", "kafka")
+#         self.harness.add_relation_unit(kafka_relation_id, "kafka/0")
+#         self.harness.update_relation_data(
+#             kafka_relation_id, "kafka/0", {"host": "kafka", "port": 9092}
+#         )
+
+#         # Initializing the mongo relation
+#         mongodb_relation_id = self.harness.add_relation("mongodb", "mongodb")
+#         self.harness.add_relation_unit(mongodb_relation_id, "mongodb/0")
+#         self.harness.update_relation_data(
+#             mongodb_relation_id,
+#             "mongodb/0",
+#             {"connection_string": "mongodb://mongo:27017"},
+#         )
+
+#         self.harness.charm.on.config_changed.emit()
+
+#         # Verifying status
+#         self.assertNotIsInstance(self.harness.charm.unit.status, BlockedStatus)
+
+
+# if __name__ == "__main__":
+#     unittest.main()
diff --git a/installers/charm/keystone/tox.ini b/installers/charm/keystone/tox.ini
index 781f74e..1f9442e 100644
--- a/installers/charm/keystone/tox.ini
+++ b/installers/charm/keystone/tox.ini
@@ -1,4 +1,4 @@
-# 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
@@ -18,65 +18,98 @@
 # To get in touch with the maintainers, please contact:
 # osm-charmers@lists.launchpad.net
 ##
+#######################################################################################
 
 [tox]
+envlist = flake8, cover, pylint, safety, yamllint
 skipsdist = True
-envlist = unit, lint
-sitepackages = False
-skip_missing_interpreters = False
 
 [testenv]
 basepython = python3.8
 setenv =
+  VIRTUAL_ENV={envdir}
   PYTHONHASHSEED=0
   PYTHONPATH = {toxinidir}/src
-  CHARM_NAME = keystone
+deps =  -r{toxinidir}/requirements.txt
 
+#######################################################################################
+[testenv:cover]
+deps =  {[testenv]deps}
+        -r{toxinidir}/requirements-test.txt
+commands =
+        sh -c 'rm -f nosetests.xml'
+        coverage erase
+        nose2 -C --coverage src
+        coverage report --omit='*tests*'
+        coverage html -d ./cover --omit='*tests*'
+        coverage xml -o coverage.xml --omit=*tests*
+whitelist_externals = sh
+
+#######################################################################################
+[testenv:safety]
+setenv =
+        LC_ALL=C.UTF-8
+        LANG=C.UTF-8
+deps =  {[testenv]deps}
+        -r{toxinidir}/requirements-test.txt
+commands =
+        - safety check --full-report
+
+#######################################################################################
+[testenv:flake8]
+deps = flake8
+commands =
+        flake8 src/ tests/
+
+#######################################################################################
+[testenv:pylint]
+deps =  {[testenv]deps}
+        -r{toxinidir}/requirements-test.txt
+        pylint
+commands =
+    pylint -E src
+
+#######################################################################################
+[testenv:black]
+deps =  {[testenv]deps}
+        -r{toxinidir}/requirements-test.txt
+        black
+commands =  black --check --diff . --exclude "build/|.tox/|mod/|lib/"
+
+#######################################################################################
+[testenv:yamllint]
+deps =  {[testenv]deps}
+        -r{toxinidir}/requirements-test.txt
+        yamllint
+commands = yamllint .
+
+#######################################################################################
 [testenv:build]
 passenv=HTTP_PROXY HTTPS_PROXY NO_PROXY
+deps =  {[testenv]deps}
+        -r{toxinidir}/requirements-test.txt
+        charmcraft
 whitelist_externals =
   charmcraft
-  rm
-  unzip
+  cp
 commands =
-  rm -rf release keystone.charm
   charmcraft build
-  unzip keystone.charm -d release
+  cp -r build release
 
-[testenv:unit]
-commands =
-  coverage erase
-  stestr run --slowest --test-path=./tests --top-dir=./
-  coverage combine
-  coverage html -d cover
-  coverage xml -o cover/coverage.xml
-  coverage report
-deps =
-  coverage
-  stestr
-  mock
-  ops
-  -rrequirements.txt
-setenv =
-  {[testenv]setenv}
-  PYTHON=coverage run
+#######################################################################################
+[flake8]
+ignore =
+        W291,
+        W293,
+        E123,
+        E125,
+        E226,
+        E241,
+exclude =
+        .git,
+        __pycache__,
+        .tox,
+max-line-length = 120
+show-source = True
+builtins = _
 
-[testenv:lint]
-deps =
-  black
-  yamllint
-  flake8
-commands =
-  black --check --diff . --exclude "build/|.tox/|mod/|lib/|release/"
-  yamllint .
-  flake8 . --max-line-length=100 --ignore="E501,W503,W504,F722" --exclude "build/ .tox/ mod/ lib/ release/"
-
-[coverage:run]
-branch = True
-concurrency = multiprocessing
-parallel = True
-source =
-  .
-omit =  
-  .tox/*
-  tests/*