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