blob: 632e96a485e149d0f5ab20c4a9d1c4042e968b2e [file] [log] [blame]
#!/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)