blob: 1920e762b851bf2a27ef67aed0b7bf902bbcf371 [file] [log] [blame]
#!/usr/bin/env python3
# Copyright 2021 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 typing import Dict, List, NoReturn
from ops.charm import CharmBase
from ops.framework import StoredState
from ops.main import main
from ops.model import ActiveStatus, Application, BlockedStatus, MaintenanceStatus, Unit
from oci_image import OCIImageResource, OCIImageResourceError
from pod_spec import make_pod_spec
logger = logging.getLogger(__name__)
GRAFANA_PORT = 3000
class RelationsMissing(Exception):
def __init__(self, missing_relations: List):
self.message = ""
if missing_relations and isinstance(missing_relations, list):
self.message += f'Waiting for {", ".join(missing_relations)} relation'
if "," in self.message:
self.message += "s"
class RelationDefinition:
def __init__(self, relation_name: str, keys: List, source_type):
if source_type != Application and source_type != Unit:
raise TypeError(
"source_type should be ops.model.Application or ops.model.Unit"
)
self.relation_name = relation_name
self.keys = keys
self.source_type = source_type
def check_missing_relation_data(
data: Dict,
expected_relations_data: List[RelationDefinition],
):
missing_relations = []
for relation_data in expected_relations_data:
if not all(
f"{relation_data.relation_name}_{k}" in data for k in relation_data.keys
):
missing_relations.append(relation_data.relation_name)
if missing_relations:
raise RelationsMissing(missing_relations)
def get_relation_data(
charm: CharmBase,
relation_data: RelationDefinition,
) -> Dict:
data = {}
relation = charm.model.get_relation(relation_data.relation_name)
if relation:
self_app_unit = (
charm.app if relation_data.source_type == Application else charm.unit
)
expected_type = relation_data.source_type
for app_unit in relation.data:
if app_unit != self_app_unit and isinstance(app_unit, expected_type):
if all(k in relation.data[app_unit] for k in relation_data.keys):
for k in relation_data.keys:
data[f"{relation_data.relation_name}_{k}"] = relation.data[
app_unit
].get(k)
break
return data
class GrafanaCharm(CharmBase):
"""Grafana Charm."""
state = StoredState()
def __init__(self, *args) -> NoReturn:
"""Grafana Charm constructor."""
super().__init__(*args)
# Internal state initialization
self.state.set_default(pod_spec=None)
self.port = GRAFANA_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)
# Registering required relation events
self.framework.observe(self.on.prometheus_relation_changed, self.configure_pod)
# Registering required relation broken events
self.framework.observe(self.on.prometheus_relation_broken, self.configure_pod)
@property
def relations_requirements(self):
return [RelationDefinition("prometheus", ["host", "port"], Unit)]
def get_relation_state(self):
relation_state = {}
for relation_requirements in self.relations_requirements:
data = get_relation_data(self, relation_requirements)
relation_state = {**relation_state, **data}
check_missing_relation_data(relation_state, self.relations_requirements)
return relation_state
def configure_pod(self, _=None) -> NoReturn:
"""Assemble the pod spec and apply it, if possible.
Args:
event (EventBase): Hook or Relation event that started the
function.
"""
if not self.unit.is_leader():
self.unit.status = ActiveStatus("ready")
return
relation_state = None
try:
relation_state = self.get_relation_state()
except RelationsMissing as exc:
logger.exception("Relation missing error")
self.unit.status = BlockedStatus(exc.message)
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,
relation_state,
self.model.app.name,
self.port,
)
except ValueError 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(GrafanaCharm)