Skip to content
Snippets Groups Projects
charm.py 9.94 KiB
Newer Older
#!/usr/bin/env python3
# Copyright 2020 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.

import logging

from urllib.parse import urlparse

from ops.charm import CharmBase

# from ops.framework import StoredState
from ops.main import main
from ops.model import (
    ActiveStatus,
    BlockedStatus,
    # MaintenanceStatus,
    WaitingStatus,
    # ModelError,
)
from ops.framework import StoredState

logger = logging.getLogger(__name__)

REQUIRED_SETTINGS = []

DATABASE_NAME = "keystone"  # This is hardcoded in the keystone container script
# We expect the keystone container to use the default port
KEYSTONE_PORT = 5000


class KeystoneCharm(CharmBase):

    state = StoredState()

    def __init__(self, *args):
        super().__init__(*args)

        # 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)

        # Register relation events
        self.state.set_default(
            db_host=None, db_port=None, db_user=None, db_password=None
        )
        self.framework.observe(
            self.on.db_relation_changed, self._on_db_relation_changed
        )
        self.framework.observe(
            self.on.keystone_relation_joined, self._publish_keystone_info
        )

    def _publish_keystone_info(self, event):
        config = self.model.config
        if self.unit.is_leader():
            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):
        self.state.db_host = event.relation.data[event.unit].get("host")
        self.state.db_port = event.relation.data[event.unit].get("port", 3306)
        self.state.db_user = "root"  # event.relation.data[event.unit].get("user")
        self.state.db_password = event.relation.data[event.unit].get("root_password")
        if self.state.db_host:
            self.configure_pod(event)

    def _check_settings(self):
        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):
        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):
        return [
            {"name": "keystone", "containerPort": KEYSTONE_PORT, "protocol": "TCP"},
        ]

    def _make_pod_envconfig(self):
        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_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"]

            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_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):
        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 configure_pod(self, event):
        """Assemble the pod spec and apply it, if possible."""

        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()
            return

        # 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()

        pod_spec = {
            "version": 3,
            "containers": [
                {
                    "name": self.framework.model.app.name,
                    "imageDetails": image_details,
                    "ports": ports,
                    "envConfig": env_config,
                }
            ],
            "kubernetesResources": {"ingressResources": ingress_resources or []},
        }
        self.model.pod.set_spec(pod_spec)
        self.unit.status = ActiveStatus()


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