Add keystone charm and interface
[osm/devops.git] / installers / charm / keystone / src / charm.py
diff --git a/installers/charm/keystone/src/charm.py b/installers/charm/keystone/src/charm.py
new file mode 100755 (executable)
index 0000000..632e96a
--- /dev/null
@@ -0,0 +1,245 @@
+#!/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
+
+        return {
+            "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"],
+        }
+
+    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)