From 87b620ace4933f30db8c4834862a57d56f9a07a6 Mon Sep 17 00:00:00 2001 From: Patricia Reinoso Date: Tue, 20 Jun 2023 15:23:47 +0000 Subject: [PATCH 1/1] Integrate MON and Grafana Add actions for datasources in mon charm create, list and delete prometheus datasources requests are done using grafana API calls grafana-url, grafana-admin, grafana-password does not have default value. If they are not present in config the charm is blocked. Change-Id: Ia0138b8d3088654f65ea9d23a664619a4475d3d8 Signed-off-by: Patricia Reinoso --- installers/charm/osm-mon/actions.yaml | 25 ++ installers/charm/osm-mon/config.yaml | 3 - installers/charm/osm-mon/requirements.txt | 1 + installers/charm/osm-mon/src/charm.py | 74 ++++- .../osm-mon/src/grafana_datasource_handler.py | 120 ++++++++ .../osm-mon/tests/integration/test_charm.py | 66 +++-- .../integration/test_datasource_actions.py | 272 ++++++++++++++++++ .../charm/osm-mon/tests/unit/test_charm.py | 56 ++++ 8 files changed, 584 insertions(+), 33 deletions(-) create mode 100644 installers/charm/osm-mon/src/grafana_datasource_handler.py create mode 100644 installers/charm/osm-mon/tests/integration/test_datasource_actions.py diff --git a/installers/charm/osm-mon/actions.yaml b/installers/charm/osm-mon/actions.yaml index 0d73468f..4f1a1576 100644 --- a/installers/charm/osm-mon/actions.yaml +++ b/installers/charm/osm-mon/actions.yaml @@ -24,3 +24,28 @@ get-debug-mode-information: description: Get information to debug the container + +create-datasource: + description: Create a new prometheus datasource. + params: + name: + type: string + description: The name of the datasource. + url: + type: string + description: URL to prometheus. + required: [name, url] + additionalProperties: false + +list-datasources: + description: List datasources. + additionalProperties: false + +delete-datasource: + description: Delete a given datasource. + params: + name: + type: string + description: The name of the datasource. + required: [name] + additionalProperties: false diff --git a/installers/charm/osm-mon/config.yaml b/installers/charm/osm-mon/config.yaml index cb2eb99c..8f1d91f0 100644 --- a/installers/charm/osm-mon/config.yaml +++ b/installers/charm/osm-mon/config.yaml @@ -59,15 +59,12 @@ options: grafana-url: description: Grafana URL type: string - default: http://grafana:3000 grafana-user: description: Grafana user type: string - default: admin grafana-password: description: Grafana password type: string - default: admin keystone-enabled: description: MON will use Keystone backend type: boolean diff --git a/installers/charm/osm-mon/requirements.txt b/installers/charm/osm-mon/requirements.txt index 398d4ad3..44e5b92d 100644 --- a/installers/charm/osm-mon/requirements.txt +++ b/installers/charm/osm-mon/requirements.txt @@ -20,4 +20,5 @@ ops < 2.2 lightkube lightkube-models +requests # git+https://github.com/charmed-osm/config-validator/ diff --git a/installers/charm/osm-mon/src/charm.py b/installers/charm/osm-mon/src/charm.py index db72dfe7..45874624 100755 --- a/installers/charm/osm-mon/src/charm.py +++ b/installers/charm/osm-mon/src/charm.py @@ -29,6 +29,7 @@ See more: https://charmhub.io/osm import logging from typing import Any, Dict +from urllib.parse import urlparse from charms.data_platform_libs.v0.data_interfaces import DatabaseRequires from charms.kafka_k8s.v0.kafka import KafkaRequires, _KafkaAvailableEvent @@ -47,6 +48,11 @@ from ops.framework import EventSource, StoredState from ops.main import main from ops.model import ActiveStatus, Container +from grafana_datasource_handler import ( + DatasourceConfig, + GrafanaConfig, + GrafanaDataSourceHandler, +) from legacy_interfaces import KeystoneClient, PrometheusClient HOSTPATHS = [ @@ -161,6 +167,47 @@ class OsmMonCharm(CharmBase): } event.set_results(debug_info) + def _on_create_datasource_action(self, event: ActionEvent) -> None: + """Handler for the create-datasource action event.""" + url = event.params["url"] + if not self._is_valid_url(url): + event.fail(f"Invalid datasource url '{url}'") + return + grafana_config = self._get_grafana_config() + datasource_config = DatasourceConfig(event.params["name"], url) + response = GrafanaDataSourceHandler.create_datasource(grafana_config, datasource_config) + logger.debug(response) + if response.is_success: + event.set_results(response.results) + return + event.fail(response.message) + + def _on_list_datasources_action(self, event: ActionEvent) -> None: + """Handler for the list-datasource action event.""" + grafana_config = self._get_grafana_config() + response = GrafanaDataSourceHandler.list_datasources(grafana_config) + logger.debug(response) + if response.is_success: + event.set_results(response.results) + return + event.fail(response.message) + + def _on_delete_datasource_action(self, event: ActionEvent) -> None: + """Handler for the delete-datasource action event.""" + datasource_name = event.params["name"] + grafana_config = self._get_grafana_config() + response = GrafanaDataSourceHandler.delete_datasource(grafana_config, datasource_name) + logger.debug(response) + if not response.is_success: + event.fail(response.message) + + def _get_grafana_config(self) -> GrafanaConfig: + return GrafanaConfig( + self.config.get("grafana-user", ""), + self.config.get("grafana-password", ""), + self.config.get("grafana-url", ""), + ) + # --------------------------------------------------------------------------- # Validation and configuration and more # --------------------------------------------------------------------------- @@ -179,6 +226,9 @@ class OsmMonCharm(CharmBase): self.on["mongodb"].relation_broken: self._on_required_relation_broken, # Action events self.on.get_debug_mode_information_action: self._on_get_debug_mode_information_action, + self.on.create_datasource_action: self._on_create_datasource_action, + self.on.list_datasources_action: self._on_list_datasources_action, + self.on.delete_datasource_action: self._on_delete_datasource_action, } for relation in [self.on[rel_name] for rel_name in ["prometheus", "keystone"]]: event_handler_mapping[relation.relation_changed] = self._on_config_changed @@ -200,6 +250,24 @@ class OsmMonCharm(CharmBase): CharmError: if charm configuration is invalid. """ logger.debug("validating charm config") + missing_configs = [] + grafana_configs = ["grafana-url", "grafana-user", "grafana-password"] + for config in grafana_configs: + if not self.config.get(config): + missing_configs.append(config) + + if missing_configs: + config_str = ", ".join(missing_configs) + error_msg = f"need {config_str} config" + logger.warning(error_msg) + raise CharmError(error_msg) + + grafana_url = self.config["grafana-url"] + if not self._is_valid_url(grafana_url): + raise CharmError(f"Invalid value for grafana-url config: '{grafana_url}'") + + def _is_valid_url(self, url) -> bool: + return urlparse(url).hostname is not None def _check_relations(self) -> None: """Validate charm relations. @@ -254,9 +322,9 @@ class OsmMonCharm(CharmBase): "OSMMON_PROMETHEUS_URL": f"http://{self.prometheus_client.hostname}:{self.prometheus_client.port}", "OSMMON_PROMETHEUS_USER": self.prometheus_client.user, "OSMMON_PROMETHEUS_PASSWORD": self.prometheus_client.password, - "OSMMON_GRAFANA_URL": self.config["grafana-url"], - "OSMMON_GRAFANA_USER": self.config["grafana-user"], - "OSMMON_GRAFANA_PASSWORD": self.config["grafana-password"], + "OSMMON_GRAFANA_URL": self.config.get("grafana-url", ""), + "OSMMON_GRAFANA_USER": self.config.get("grafana-user", ""), + "OSMMON_GRAFANA_PASSWORD": self.config.get("grafana-password", ""), "OSMMON_KEYSTONE_ENABLED": self.config["keystone-enabled"], "OSMMON_KEYSTONE_URL": self.keystone_client.host, "OSMMON_KEYSTONE_DOMAIN_NAME": self.keystone_client.user_domain_name, diff --git a/installers/charm/osm-mon/src/grafana_datasource_handler.py b/installers/charm/osm-mon/src/grafana_datasource_handler.py new file mode 100644 index 00000000..9cd6acb0 --- /dev/null +++ b/installers/charm/osm-mon/src/grafana_datasource_handler.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +# Copyright 2022 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 +# +# +# Learn more at: https://juju.is/docs/sdk + +"""Module to handle Grafana datasource operations.""" + +import json +import logging +from dataclasses import dataclass + +import requests +from requests.auth import HTTPBasicAuth + +logger = logging.getLogger(__name__) + + +@dataclass +class GrafanaConfig: + """Values needed to make grafana API calls.""" + + user: str + password: str + url: str + + +@dataclass +class DatasourceConfig: + """Information about the datasource to create.""" + + name: str + url: str + + +@dataclass +class DatasourceResponse: + """Return value used by GrafanaDataSourceHandler operations.""" + + is_success: bool + message: str + results: dict + + +class GrafanaDataSourceHandler: + """Handle grafana datasource pperations.""" + + @staticmethod + def create_datasource( + grafana_config: GrafanaConfig, datasource_config: DatasourceConfig + ) -> DatasourceResponse: + """Calls the Grafana API to create a new prometheus datasource.""" + try: + auth = HTTPBasicAuth(grafana_config.user, grafana_config.password) + url = grafana_config.url + "/api/datasources" + datasource_data = { + "name": datasource_config.name, + "type": "prometheus", + "url": datasource_config.url, + "access": "proxy", + "readOnly": False, + "basicAuth": False, + } + response = requests.post(url, json=datasource_data, auth=auth) + response_content = response.json() + results = {"datasource-name": response_content.get("name")} + return DatasourceResponse( + response.ok, response_content.get("message"), results=results + ) + except Exception as e: + logger.debug(f"Exception processing request for creating datasource: {e}") + return DatasourceResponse(False, str(e), results={}) + + @staticmethod + def list_datasources(grafana_config: GrafanaConfig) -> DatasourceResponse: + """Calls the Grafana API to get a list of datasources.""" + try: + auth = HTTPBasicAuth(grafana_config.user, grafana_config.password) + url = grafana_config.url + "/api/datasources" + response = requests.get(url, auth=auth) + response_content = response.json() + results = {"datasources": json.dumps(response_content)} + message = response_content.get("message") if not response.ok else "" + return DatasourceResponse(response.ok, message=message, results=results) + except Exception as e: + logger.debug(f"Exception processing request to list datasources: {e}") + return DatasourceResponse(False, str(e), results={}) + + @staticmethod + def delete_datasource( + grafana_config: GrafanaConfig, datasource_name: str + ) -> DatasourceResponse: + """Calls the Grafana API to delete a given datasource by name.""" + try: + auth = HTTPBasicAuth(grafana_config.user, grafana_config.password) + url = grafana_config.url + f"/api/datasources/name/{datasource_name}" + response = requests.delete(url, auth=auth) + response_content = response.json() + return DatasourceResponse(response.ok, response_content.get("message"), results={}) + except Exception as e: + logger.debug(f"Exception processing request for deleting datasource: {e}") + return DatasourceResponse(False, str(e), results={}) diff --git a/installers/charm/osm-mon/tests/integration/test_charm.py b/installers/charm/osm-mon/tests/integration/test_charm.py index caf8deda..e14e191e 100644 --- a/installers/charm/osm-mon/tests/integration/test_charm.py +++ b/installers/charm/osm-mon/tests/integration/test_charm.py @@ -52,7 +52,7 @@ APPS = [KAFKA_APP, ZOOKEEPER_APP, KEYSTONE_APP, MONGO_DB_APP, MARIADB_APP, PROME @pytest.mark.abort_on_fail -async def test_mon_is_deployed(ops_test: OpsTest): +async def test_mon_and_other_charms_are_idle(ops_test: OpsTest): charm = await ops_test.build_charm(".") resources = {"mon-image": METADATA["resources"]["mon-image"]["upstream-source"]} @@ -71,11 +71,40 @@ async def test_mon_is_deployed(ops_test: OpsTest): await ops_test.run(*shlex.split(cmd), check=True) async with ops_test.fast_forward(): - await ops_test.model.wait_for_idle( - apps=APPS, - ) + await ops_test.model.wait_for_idle(apps=APPS) + + +@pytest.mark.abort_on_fail +async def test_mon_is_blocked_due_to_missing_grafana_config(ops_test: OpsTest): assert ops_test.model.applications[MON_APP].status == "blocked" unit = ops_test.model.applications[MON_APP].units[0] + assert ( + unit.workload_status_message == "need grafana-url, grafana-user, grafana-password config" + ) + + await ops_test.model.applications[MON_APP].set_config({"grafana-url": "new_value"}) + async with ops_test.fast_forward(): + await ops_test.model.wait_for_idle(apps=[MON_APP], status="blocked") + assert unit.workload_status_message == "need grafana-user, grafana-password config" + + await ops_test.model.applications[MON_APP].set_config({"grafana-password": "new_value"}) + async with ops_test.fast_forward(): + await ops_test.model.wait_for_idle(apps=[MON_APP], status="blocked") + assert unit.workload_status_message == "need grafana-user config" + + await ops_test.model.applications[MON_APP].set_config({"grafana-user": "new_value"}) + async with ops_test.fast_forward(): + await ops_test.model.wait_for_idle(apps=[MON_APP], status="blocked") + + assert unit.workload_status_message == "Invalid value for grafana-url config: 'new_value'" + await ops_test.model.applications[MON_APP].set_config({"grafana-url": "http://valid:92"}) + + +@pytest.mark.abort_on_fail +async def test_mon_is_blocked_due_to_missing_relations(ops_test: OpsTest): + async with ops_test.fast_forward(): + await ops_test.model.wait_for_idle(apps=[MON_APP], status="blocked") + unit = ops_test.model.applications[MON_APP].units[0] assert unit.workload_status_message == "need kafka, mongodb, prometheus, keystone relations" logger.info("Adding relations for other components") @@ -91,10 +120,7 @@ async def test_mon_is_deployed(ops_test: OpsTest): await ops_test.model.add_relation(MON_APP, PROMETHEUS_APP) async with ops_test.fast_forward(): - await ops_test.model.wait_for_idle( - apps=APPS, - status="active", - ) + await ops_test.model.wait_for_idle(apps=APPS, status="active") @pytest.mark.abort_on_fail @@ -129,19 +155,13 @@ async def test_mon_blocks_without_relation(ops_test: OpsTest, relation_to_remove assert unit.workload_status_message == f"need {relation_to_remove} relation" await ops_test.model.add_relation(MON_APP, relation_to_remove) async with ops_test.fast_forward(): - await ops_test.model.wait_for_idle( - apps=APPS, - status="active", - ) + await ops_test.model.wait_for_idle(apps=APPS, status="active") @pytest.mark.abort_on_fail async def test_mon_action_debug_mode_disabled(ops_test: OpsTest): async with ops_test.fast_forward(): - await ops_test.model.wait_for_idle( - apps=APPS, - status="active", - ) + await ops_test.model.wait_for_idle(apps=APPS, status="active") logger.info("Running action 'get-debug-mode-information'") action = ( await ops_test.model.applications[MON_APP] @@ -187,9 +207,7 @@ async def test_mon_integration_vca(ops_test: OpsTest): ), ) async with ops_test.fast_forward(): - await ops_test.model.wait_for_idle( - apps=[VCA_APP], - ) + await ops_test.model.wait_for_idle(apps=[VCA_APP]) controllers = (Path.home() / ".local/share/juju/controllers.yaml").read_text() accounts = (Path.home() / ".local/share/juju/accounts.yaml").read_text() public_key = (Path.home() / ".local/share/juju/ssh/juju_id_rsa.pub").read_text() @@ -202,13 +220,7 @@ async def test_mon_integration_vca(ops_test: OpsTest): } ) async with ops_test.fast_forward(): - await ops_test.model.wait_for_idle( - apps=APPS + [VCA_APP], - status="active", - ) + await ops_test.model.wait_for_idle(apps=APPS + [VCA_APP], status="active") await ops_test.model.add_relation(MON_APP, VCA_APP) async with ops_test.fast_forward(): - await ops_test.model.wait_for_idle( - apps=APPS + [VCA_APP], - status="active", - ) + await ops_test.model.wait_for_idle(apps=APPS + [VCA_APP], status="active") diff --git a/installers/charm/osm-mon/tests/integration/test_datasource_actions.py b/installers/charm/osm-mon/tests/integration/test_datasource_actions.py new file mode 100644 index 00000000..71d0706f --- /dev/null +++ b/installers/charm/osm-mon/tests/integration/test_datasource_actions.py @@ -0,0 +1,272 @@ +#!/usr/bin/env python3 +# Copyright 2022 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 +# +# Learn more about testing at: https://juju.is/docs/sdk/testing + +import asyncio +import json +import logging +from pathlib import Path + +import pytest +import yaml +from pytest_operator.plugin import OpsTest + +logger = logging.getLogger(__name__) + +METADATA = yaml.safe_load(Path("./metadata.yaml").read_text()) +MON_APP = METADATA["name"] +GRAFANA_CHARM = "grafana-k8s" +GRAFANA_APP = "grafana" +DS_NAME = "osm_prometheus" +DS_URL = "http://prometheus:9090" + + +async def _run_action_create_datasource(ops_test: OpsTest, name: str, url: str): + action = ( + await ops_test.model.applications[MON_APP] + .units[0] + .run_action( + action_name="create-datasource", + name=name, + url=url, + ) + ) + return await action.wait() + + +async def _run_action_list_datasources(ops_test: OpsTest): + action = ( + await ops_test.model.applications[MON_APP] + .units[0] + .run_action(action_name="list-datasources") + ) + return await action.wait() + + +async def _run_action_delete_datasource(ops_test: OpsTest, name: str): + action = ( + await ops_test.model.applications[MON_APP] + .units[0] + .run_action(action_name="delete-datasource", name=name) + ) + return await action.wait() + + +@pytest.mark.abort_on_fail +async def test_mon_and_grafana_are_idle(ops_test: OpsTest): + charm = await ops_test.build_charm(".") + resources = {"mon-image": METADATA["resources"]["mon-image"]["upstream-source"]} + + await asyncio.gather( + ops_test.model.deploy( + charm, resources=resources, application_name=MON_APP, series="jammy" + ), + ops_test.model.deploy(GRAFANA_CHARM, application_name=GRAFANA_APP, channel="stable"), + ) + async with ops_test.fast_forward(): + await ops_test.model.wait_for_idle(apps=[GRAFANA_APP], status="active") + await ops_test.model.wait_for_idle(apps=[MON_APP], status="blocked") + + +@pytest.mark.abort_on_fail +async def test_mon_cannot_create_datasource_grafana_url_is_not_set(ops_test: OpsTest): + action = await _run_action_create_datasource(ops_test, DS_NAME, DS_URL) + assert action.status == "failed" + assert "Invalid URL" in action.message + + +@pytest.mark.abort_on_fail +async def test_mon_cannot_list_datasources_grafana_url_is_not_set(ops_test: OpsTest): + action = await _run_action_list_datasources(ops_test) + assert action.status == "failed" + assert "Invalid URL" in action.message + + +@pytest.mark.abort_on_fail +async def test_mon_cannot_delete_datasource_grafana_url_is_not_set(ops_test: OpsTest): + action = await _run_action_delete_datasource(ops_test, "prometheus") + assert action.status == "failed" + assert "Invalid URL" in action.message + + +@pytest.mark.abort_on_fail +async def test_mon_cannot_create_datasource_due_to_invalid_grafana_password(ops_test: OpsTest): + await ops_test.model.applications[MON_APP].set_config( + {"grafana-url": f"http://{GRAFANA_APP}:3000", "grafana-user": "admin"} + ) + async with ops_test.fast_forward(): + await ops_test.model.wait_for_idle(apps=[MON_APP], status="blocked") + + action = await _run_action_create_datasource(ops_test, DS_NAME, DS_URL) + assert action.status == "failed" + assert action.message == "invalid username or password" + + +@pytest.mark.abort_on_fail +async def test_mon_cannot_create_datasource_invalid_url_fails(ops_test: OpsTest): + await ops_test.model.applications[MON_APP].set_config( + {"grafana-url": f"http://{GRAFANA_APP}:3000"} + ) + async with ops_test.fast_forward(): + await ops_test.model.wait_for_idle(apps=[MON_APP], status="blocked") + + action = await _run_action_create_datasource(ops_test, DS_NAME, "prometheus:9090") + assert action.status == "failed" + assert action.message == "Invalid datasource url 'prometheus:9090'" + + +async def _get_grafana_admin_password(ops_test: OpsTest): + action = ( + await ops_test.model.applications[GRAFANA_APP].units[0].run_action("get-admin-password") + ) + admin_password = (await action.wait()).results["admin-password"] + logger.info(f"Password obtained from {GRAFANA_APP} : {admin_password}") + assert admin_password != "Admin password has been changed by an administrator" + return admin_password + + +@pytest.mark.abort_on_fail +async def test_grafana_password_is_set_in_mon_config(ops_test: OpsTest): + admin_password = await _get_grafana_admin_password(ops_test) + + default_password = None + grafana_password_in_mon = ( + (await ops_test.model.applications[MON_APP].get_config()) + .get("grafana-password") + .get("value") + ) + assert grafana_password_in_mon == default_password + + await ops_test.model.applications[MON_APP].set_config({"grafana-password": admin_password}) + + async with ops_test.fast_forward(): + await ops_test.model.wait_for_idle(apps=[GRAFANA_APP], status="active") + await ops_test.model.wait_for_idle(apps=[MON_APP], status="blocked") + + grafana_password_in_mon = ( + (await ops_test.model.applications[MON_APP].get_config()) + .get("grafana-password") + .get("value") + ) + assert grafana_password_in_mon == admin_password + + +@pytest.mark.abort_on_fail +async def test_mon_create_datasource_successfully(ops_test: OpsTest): + action = await _run_action_create_datasource(ops_test, DS_NAME, DS_URL) + assert action.status == "completed" + assert action.results["datasource-name"] == DS_NAME + + +@pytest.mark.abort_on_fail +async def test_mon_list_datasources_returns_osm_prometheus(ops_test: OpsTest): + action = await _run_action_list_datasources(ops_test) + assert action.status == "completed" + datasources = json.loads(action.results.get("datasources")) + assert len(datasources) == 1 + assert datasources[0].get("name") == DS_NAME + assert datasources[0].get("type") == "prometheus" + + +@pytest.mark.abort_on_fail +async def test_mon_create_datasource_that_already_exists_returns_error_message(ops_test: OpsTest): + action = await _run_action_create_datasource(ops_test, DS_NAME, DS_URL) + assert action.status == "failed" + assert action.message == "data source with the same name already exists" + + +@pytest.mark.abort_on_fail +async def test_mon_delete_existing_datasource_returns_success_message(ops_test: OpsTest): + action = await _run_action_delete_datasource(ops_test, DS_NAME) + assert action.status == "completed" + + +@pytest.mark.abort_on_fail +async def test_mon_list_datasources_is_empty(ops_test: OpsTest): + action = await _run_action_list_datasources(ops_test) + assert action.status == "completed" + datasources = json.loads(action.results.get("datasources")) + assert len(datasources) == 0 + + +@pytest.mark.abort_on_fail +async def test_mon_delete_non_existing_datasource_returns_error_message(ops_test: OpsTest): + action = await _run_action_delete_datasource(ops_test, DS_NAME) + assert action.status == "failed" + assert action.message == "Data source not found" + + +@pytest.mark.abort_on_fail +async def test_mon_create_datasource_incorrect_grafana_host_fails(ops_test: OpsTest): + await ops_test.model.applications[MON_APP].set_config( + {"grafana-url": f"http://{GRAFANA_APP}-k8s:3000"} + ) + async with ops_test.fast_forward(): + await ops_test.model.wait_for_idle(apps=[MON_APP], status="blocked") + action = await _run_action_create_datasource(ops_test, DS_NAME, DS_URL) + assert action.status == "failed" + assert f"Failed to resolve '{GRAFANA_APP}-k8s'" in action.message + + +@pytest.mark.abort_on_fail +async def test_mon_list_datasources_incorrect_grafana_host_fails(ops_test: OpsTest): + action = await _run_action_list_datasources(ops_test) + assert action.status == "failed" + assert f"Failed to resolve '{GRAFANA_APP}-k8s'" in action.message + + +@pytest.mark.abort_on_fail +async def test_mon_delete_datasource_incorrect_grafana_host_fails(ops_test: OpsTest): + action = await _run_action_delete_datasource(ops_test, DS_NAME) + assert action.status == "failed" + assert f"Failed to resolve '{GRAFANA_APP}-k8s'" in action.message + + +@pytest.mark.abort_on_fail +async def test_mon_incorrect_grafana_port_returns_timeout_message(ops_test: OpsTest): + await ops_test.model.applications[MON_APP].set_config( + {"grafana-url": f"http://{GRAFANA_APP}:3001"} + ) + async with ops_test.fast_forward(): + await ops_test.model.wait_for_idle(apps=[MON_APP], status="blocked") + action = await _run_action_create_datasource(ops_test, DS_NAME, DS_URL) + assert action.status == "failed" + assert f"Connection to {GRAFANA_APP} timed out" in action.message + + +@pytest.mark.abort_on_fail +async def test_mon_list_datasources_incorrect_user_fails(ops_test: OpsTest): + await ops_test.model.applications[MON_APP].set_config( + {"grafana-url": f"http://{GRAFANA_APP}:3000", "grafana-user": "some_user"} + ) + async with ops_test.fast_forward(): + await ops_test.model.wait_for_idle(apps=[MON_APP], status="blocked") + action = await _run_action_list_datasources(ops_test) + assert action.status == "failed" + assert action.message == "invalid username or password" + + +@pytest.mark.abort_on_fail +async def test_mon_delete_datasource_incorrect_user_fails(ops_test: OpsTest): + action = await _run_action_delete_datasource(ops_test, DS_NAME) + assert action.status == "failed" + assert action.message == "invalid username or password" diff --git a/installers/charm/osm-mon/tests/unit/test_charm.py b/installers/charm/osm-mon/tests/unit/test_charm.py index 33598fe6..441eddbf 100644 --- a/installers/charm/osm-mon/tests/unit/test_charm.py +++ b/installers/charm/osm-mon/tests/unit/test_charm.py @@ -42,7 +42,27 @@ def harness(mocker: MockerFixture): harness.cleanup() +def _set_grafana_config(harness: Harness): + harness.update_config( + { + "grafana-url": "http://prometheus:1234", + "grafana-user": "user", + "grafana-password": "password", + } + ) + + +def test_default_grafana_config_is_invalid_charm_is_blocked(harness: Harness): + harness.charm.on.config_changed.emit() + assert type(harness.charm.unit.status) == BlockedStatus + assert ( + "need grafana-url, grafana-user, grafana-password config" + == harness.charm.unit.status.message + ) + + def test_missing_relations(harness: Harness): + _set_grafana_config(harness) harness.charm.on.config_changed.emit() assert type(harness.charm.unit.status) == BlockedStatus assert all( @@ -51,12 +71,48 @@ def test_missing_relations(harness: Harness): ) +def test_grafana_url_without_schema_block_status(harness: Harness): + harness.update_config( + {"grafana-url": "foo.com", "grafana-user": "user", "grafana-password": "password"} + ) + assert type(harness.charm.unit.status) == BlockedStatus + assert "Invalid value for grafana-url config: 'foo.com'" == harness.charm.unit.status.message + + +def test_grafana_url_with_port_without_schema_block_status(harness: Harness): + harness.update_config( + {"grafana-url": "foo.com:9090", "grafana-user": "user", "grafana-password": "password"} + ) + assert type(harness.charm.unit.status) == BlockedStatus + assert ( + "Invalid value for grafana-url config: 'foo.com:9090'" == harness.charm.unit.status.message + ) + + +def test_grafana_url_without_port_is_valid(harness: Harness): + _add_relations(harness) + harness.update_config( + {"grafana-url": "http://foo", "grafana-user": "user", "grafana-password": "password"} + ) + assert harness.charm.unit.status == ActiveStatus() + + +def test_grafana_url_with_port_is_valid(harness: Harness): + _add_relations(harness) + harness.update_config( + {"grafana-url": "http://foo:90", "grafana-user": "user", "grafana-password": "password"} + ) + assert harness.charm.unit.status == ActiveStatus() + + def test_ready(harness: Harness): + _set_grafana_config(harness) _add_relations(harness) assert harness.charm.unit.status == ActiveStatus() def test_container_stops_after_relation_broken(harness: Harness): + _set_grafana_config(harness) harness.charm.on[container_name].pebble_ready.emit(container_name) container = harness.charm.unit.get_container(container_name) relation_ids = _add_relations(harness) -- 2.25.1