blob: d65bdcfa35c8be6c5ebb4383de6c59bb06597ef8 [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.
#
# 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
##
import logging
from pydantic import ValidationError
from typing import Any, Dict, NoReturn
from ops.charm import CharmBase, CharmEvents
from ops.framework import EventBase, EventSource, StoredState
from ops.main import main
from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus
from oci_image import OCIImageResource, OCIImageResourceError
from pod_spec import make_pod_spec
logger = logging.getLogger(__name__)
LCM_PORT = 9999
class ConfigurePodEvent(EventBase):
"""Configure Pod event"""
pass
class LcmEvents(CharmEvents):
"""LCM Events"""
configure_pod = EventSource(ConfigurePodEvent)
class LcmCharm(CharmBase):
"""LCM Charm."""
state = StoredState()
on = LcmEvents()
def __init__(self, *args) -> NoReturn:
"""LCM Charm constructor."""
super().__init__(*args)
# Internal state initialization
self.state.set_default(pod_spec=None)
# Message bus data initialization
self.state.set_default(message_host=None)
self.state.set_default(message_port=None)
# Database data initialization
self.state.set_default(database_uri=None)
# RO data initialization
self.state.set_default(ro_host=None)
self.state.set_default(ro_port=None)
self.port = LCM_PORT
self.image = OCIImageResource(self, "image")
# Registering regular events
self.framework.observe(self.on.start, self.configure_pod)
self.framework.observe(self.on.config_changed, self.configure_pod)
self.framework.observe(self.on.upgrade_charm, self.configure_pod)
# Registering custom internal events
self.framework.observe(self.on.configure_pod, self.configure_pod)
# Registering required relation events
self.framework.observe(
self.on.kafka_relation_changed, self._on_kafka_relation_changed
)
self.framework.observe(
self.on.mongodb_relation_changed, self._on_mongodb_relation_changed
)
self.framework.observe(
self.on.ro_relation_changed, self._on_ro_relation_changed
)
# Registering required relation departed events
self.framework.observe(
self.on.kafka_relation_departed, self._on_kafka_relation_departed
)
self.framework.observe(
self.on.mongodb_relation_departed, self._on_mongodb_relation_departed
)
self.framework.observe(
self.on.ro_relation_departed, self._on_ro_relation_departed
)
def _on_kafka_relation_changed(self, event: EventBase) -> NoReturn:
"""Reads information about the kafka relation.
Args:
event (EventBase): Kafka relation event.
"""
data_loc = event.unit if event.unit else event.app
message_host = event.relation.data[data_loc].get("host")
message_port = event.relation.data[data_loc].get("port")
if (
message_host
and message_port
and (
self.state.message_host != message_host
or self.state.message_port != message_port
)
):
self.state.message_host = message_host
self.state.message_port = message_port
self.on.configure_pod.emit()
def _on_kafka_relation_departed(self, event: EventBase) -> NoReturn:
"""Clears data from kafka relation.
Args:
event (EventBase): Kafka relation event.
"""
self.state.message_host = None
self.state.message_port = None
self.on.configure_pod.emit()
def _on_mongodb_relation_changed(self, event: EventBase) -> NoReturn:
"""Reads information about the DB relation.
Args:
event (EventBase): DB relation event.
"""
data_loc = event.unit if event.unit else event.app
database_uri = event.relation.data[data_loc].get("connection_string")
if database_uri and self.state.database_uri != database_uri:
self.state.database_uri = database_uri
self.on.configure_pod.emit()
def _on_mongodb_relation_departed(self, event: EventBase) -> NoReturn:
"""Clears data from mongodb relation.
Args:
event (EventBase): DB relation event.
"""
self.state.database_uri = None
self.on.configure_pod.emit()
def _on_ro_relation_changed(self, event: EventBase) -> NoReturn:
"""Reads information about the RO relation.
Args:
event (EventBase): Keystone relation event.
"""
data_loc = event.unit if event.unit else event.app
ro_host = event.relation.data[data_loc].get("host")
ro_port = event.relation.data[data_loc].get("port")
if (
ro_host
and ro_port
and (self.state.ro_host != ro_host or self.state.ro_port != ro_port)
):
self.state.ro_host = ro_host
self.state.ro_port = ro_port
self.on.configure_pod.emit()
def _on_ro_relation_departed(self, event: EventBase) -> NoReturn:
"""Clears data from ro relation.
Args:
event (EventBase): Keystone relation event.
"""
self.state.ro_host = None
self.state.ro_port = None
self.on.configure_pod.emit()
def _missing_relations(self) -> str:
"""Checks if there missing relations.
Returns:
str: string with missing relations
"""
data_status = {
"kafka": self.state.message_host,
"mongodb": self.state.database_uri,
"ro": self.state.ro_host,
}
missing_relations = [k for k, v in data_status.items() if not v]
return ", ".join(missing_relations)
@property
def relation_state(self) -> Dict[str, Any]:
"""Collects relation state configuration for pod spec assembly.
Returns:
Dict[str, Any]: relation state information.
"""
relation_state = {
"message_host": self.state.message_host,
"message_port": self.state.message_port,
"database_uri": self.state.database_uri,
"ro_host": self.state.ro_host,
"ro_port": self.state.ro_port,
}
return relation_state
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 missing := self._missing_relations():
self.unit.status = BlockedStatus(
"Waiting for {0} relation{1}".format(
missing, "s" if "," in missing else ""
)
)
return
if not self.unit.is_leader():
self.unit.status = ActiveStatus("ready")
return
self.unit.status = MaintenanceStatus("Assembling pod spec")
# Fetch image information
try:
self.unit.status = MaintenanceStatus("Fetching image information")
image_info = self.image.fetch()
except OCIImageResourceError:
self.unit.status = BlockedStatus("Error fetching image information")
return
try:
pod_spec = make_pod_spec(
image_info,
self.model.config,
self.relation_state,
self.model.app.name,
self.port,
)
except ValidationError as exc:
logger.exception("Config/Relation data validation error")
self.unit.status = BlockedStatus(str(exc))
return
if self.state.pod_spec != pod_spec:
self.model.pod.set_spec(pod_spec)
self.state.pod_spec = pod_spec
self.unit.status = ActiveStatus("ready")
if __name__ == "__main__":
main(LcmCharm)