Fix Bug 2204 - Grafana Charm is failing to be deployed in Charmed OSM Master
[osm/devops.git] / installers / charm / grafana / src / charm.py
index 87776aa..caa0277 100755 (executable)
 from ipaddress import ip_network
 import logging
 from pathlib import Path
 from ipaddress import ip_network
 import logging
 from pathlib import Path
+import secrets
 from string import Template
 from typing import NoReturn, Optional
 from urllib.parse import urlparse
 
 from ops.main import main
 from opslib.osm.charm import CharmedOsmBase, RelationsMissing
 from string import Template
 from typing import NoReturn, Optional
 from urllib.parse import urlparse
 
 from ops.main import main
 from opslib.osm.charm import CharmedOsmBase, RelationsMissing
+from opslib.osm.interfaces.grafana import GrafanaCluster
+from opslib.osm.interfaces.mysql import MysqlClient
 from opslib.osm.interfaces.prometheus import PrometheusClient
 from opslib.osm.pod import (
     ContainerV3Builder,
     FilesV3Builder,
     IngressResourceV3Builder,
 from opslib.osm.interfaces.prometheus import PrometheusClient
 from opslib.osm.pod import (
     ContainerV3Builder,
     FilesV3Builder,
     IngressResourceV3Builder,
+    PodRestartPolicy,
     PodSpecV3Builder,
 )
 from opslib.osm.validator import ModelValidator, validator
     PodSpecV3Builder,
 )
 from opslib.osm.validator import ModelValidator, validator
@@ -43,10 +47,11 @@ from opslib.osm.validator import ModelValidator, validator
 
 logger = logging.getLogger(__name__)
 
 
 logger = logging.getLogger(__name__)
 
-PORT = 3000
-
 
 class ConfigModel(ModelValidator):
 
 class ConfigModel(ModelValidator):
+    log_level: str
+    port: int
+    admin_user: str
     max_file_size: int
     osm_dashboards: bool
     site_url: Optional[str]
     max_file_size: int
     osm_dashboards: bool
     site_url: Optional[str]
@@ -54,7 +59,18 @@ class ConfigModel(ModelValidator):
     ingress_class: Optional[str]
     ingress_whitelist_source_range: Optional[str]
     tls_secret_name: Optional[str]
     ingress_class: Optional[str]
     ingress_whitelist_source_range: Optional[str]
     tls_secret_name: Optional[str]
-    image_pull_policy: Optional[str]
+    image_pull_policy: str
+    security_context: bool
+
+    @validator("log_level")
+    def validate_log_level(cls, v):
+        allowed_values = ("debug", "info", "warn", "error", "critical")
+        if v not in allowed_values:
+            separator = '", "'
+            raise ValueError(
+                f'incorrect value. Allowed values are "{separator.join(allowed_values)}"'
+            )
+        return v
 
     @validator("max_file_size")
     def validate_max_file_size(cls, v):
 
     @validator("max_file_size")
     def validate_max_file_size(cls, v):
@@ -94,15 +110,20 @@ class GrafanaCharm(CharmedOsmBase):
 
     def __init__(self, *args) -> NoReturn:
         """Prometheus Charm constructor."""
 
     def __init__(self, *args) -> NoReturn:
         """Prometheus Charm constructor."""
-        super().__init__(*args, oci_image="image")
-
+        super().__init__(*args, oci_image="image", mysql_uri=True)
+        # Initialize relation objects
         self.prometheus_client = PrometheusClient(self, "prometheus")
         self.prometheus_client = PrometheusClient(self, "prometheus")
-        self.framework.observe(
-            self.on["prometheus"].relation_changed, self.configure_pod
-        )
-        self.framework.observe(
-            self.on["prometheus"].relation_broken, self.configure_pod
-        )
+        self.grafana_cluster = GrafanaCluster(self, "cluster")
+        self.mysql_client = MysqlClient(self, "db")
+        # Observe events
+        event_observer_mapping = {
+            self.on["prometheus"].relation_changed: self.configure_pod,
+            self.on["prometheus"].relation_broken: self.configure_pod,
+            self.on["db"].relation_changed: self.configure_pod,
+            self.on["db"].relation_broken: self.configure_pod,
+        }
+        for event, observer in event_observer_mapping.items():
+            self.framework.observe(event, observer)
 
     def _build_dashboard_files(self, config: ConfigModel):
         files_builder = FilesV3Builder()
 
     def _build_dashboard_files(self, config: ConfigModel):
         files_builder = FilesV3Builder()
@@ -124,39 +145,86 @@ class GrafanaCharm(CharmedOsmBase):
 
     def _build_datasources_files(self):
         files_builder = FilesV3Builder()
 
     def _build_datasources_files(self):
         files_builder = FilesV3Builder()
+        prometheus_user = self.prometheus_client.user
+        prometheus_password = self.prometheus_client.password
+        enable_basic_auth = all([prometheus_user, prometheus_password])
+        kwargs = {
+            "prometheus_host": self.prometheus_client.hostname,
+            "prometheus_port": self.prometheus_client.port,
+            "enable_basic_auth": enable_basic_auth,
+            "user": "",
+            "password": "",
+        }
+        if enable_basic_auth:
+            kwargs["user"] = f"basic_auth_user: {prometheus_user}"
+            kwargs[
+                "password"
+            ] = f"secure_json_data:\n      basicAuthPassword: {prometheus_password}"
         files_builder.add_file(
             "datasource_prometheus.yaml",
             Template(Path("templates/default_datasources.yaml").read_text()).substitute(
         files_builder.add_file(
             "datasource_prometheus.yaml",
             Template(Path("templates/default_datasources.yaml").read_text()).substitute(
-                prometheus_host=self.prometheus_client.hostname,
-                prometheus_port=self.prometheus_client.port,
+                **kwargs
             ),
         )
         return files_builder.build()
 
             ),
         )
         return files_builder.build()
 
-    def _check_missing_dependencies(self):
+    def _check_missing_dependencies(self, config: ConfigModel, external_db: bool):
         missing_relations = []
 
         if self.prometheus_client.is_missing_data_in_app():
             missing_relations.append("prometheus")
 
         missing_relations = []
 
         if self.prometheus_client.is_missing_data_in_app():
             missing_relations.append("prometheus")
 
+        if not external_db and self.mysql_client.is_missing_data_in_unit():
+            missing_relations.append("db")
+
         if missing_relations:
             raise RelationsMissing(missing_relations)
 
         if missing_relations:
             raise RelationsMissing(missing_relations)
 
-    def build_pod_spec(self, image_info):
+    def build_pod_spec(self, image_info, **kwargs):
         # Validate config
         config = ConfigModel(**dict(self.config))
         # Validate config
         config = ConfigModel(**dict(self.config))
+        mysql_config = kwargs["mysql_config"]
+        if mysql_config.mysql_uri and not self.mysql_client.is_missing_data_in_unit():
+            raise Exception("Mysql data cannot be provided via config and relation")
+
         # Check relations
         # Check relations
-        self._check_missing_dependencies()
+        external_db = True if mysql_config.mysql_uri else False
+        self._check_missing_dependencies(config, external_db)
+
+        # Get initial password
+        admin_initial_password = self.grafana_cluster.admin_initial_password
+        if not admin_initial_password:
+            admin_initial_password = _generate_random_password()
+            self.grafana_cluster.set_initial_password(admin_initial_password)
+
         # Create Builder for the PodSpec
         # Create Builder for the PodSpec
-        pod_spec_builder = PodSpecV3Builder()
+        pod_spec_builder = PodSpecV3Builder(
+            enable_security_context=config.security_context
+        )
+
+        # Add secrets to the pod
+        grafana_secret_name = f"{self.app.name}-admin-secret"
+        pod_spec_builder.add_secret(
+            grafana_secret_name,
+            {
+                "admin-password": admin_initial_password,
+                "mysql-url": mysql_config.mysql_uri or self.mysql_client.get_uri(),
+                "prometheus-user": self.prometheus_client.user,
+                "prometheus-password": self.prometheus_client.password,
+            },
+        )
+
         # Build Container
         container_builder = ContainerV3Builder(
         # Build Container
         container_builder = ContainerV3Builder(
-            self.app.name, image_info, config.image_pull_policy
+            self.app.name,
+            image_info,
+            config.image_pull_policy,
+            run_as_non_root=config.security_context,
         )
         )
-        container_builder.add_port(name=self.app.name, port=PORT)
+        container_builder.add_port(name=self.app.name, port=config.port)
         container_builder.add_http_readiness_probe(
             "/api/health",
         container_builder.add_http_readiness_probe(
             "/api/health",
-            PORT,
+            config.port,
             initial_delay_seconds=10,
             period_seconds=10,
             timeout_seconds=5,
             initial_delay_seconds=10,
             period_seconds=10,
             timeout_seconds=5,
@@ -164,7 +232,7 @@ class GrafanaCharm(CharmedOsmBase):
         )
         container_builder.add_http_liveness_probe(
             "/api/health",
         )
         container_builder.add_http_liveness_probe(
             "/api/health",
-            PORT,
+            config.port,
             initial_delay_seconds=60,
             timeout_seconds=30,
             failure_threshold=10,
             initial_delay_seconds=60,
             timeout_seconds=30,
             failure_threshold=10,
@@ -179,9 +247,30 @@ class GrafanaCharm(CharmedOsmBase):
             "/etc/grafana/provisioning/datasources/",
             self._build_datasources_files(),
         )
             "/etc/grafana/provisioning/datasources/",
             self._build_datasources_files(),
         )
+        container_builder.add_envs(
+            {
+                "GF_SERVER_HTTP_PORT": config.port,
+                "GF_LOG_LEVEL": config.log_level,
+                "GF_SECURITY_ADMIN_USER": config.admin_user,
+            }
+        )
+        container_builder.add_secret_envs(
+            secret_name=grafana_secret_name,
+            envs={
+                "GF_SECURITY_ADMIN_PASSWORD": "admin-password",
+                "GF_DATABASE_URL": "mysql-url",
+                "PROMETHEUS_USER": "prometheus-user",
+                "PROMETHEUS_PASSWORD": "prometheus-password",
+            },
+        )
         container = container_builder.build()
         container = container_builder.build()
-        # Add container to pod spec
         pod_spec_builder.add_container(container)
         pod_spec_builder.add_container(container)
+
+        # Add Pod restart policy
+        restart_policy = PodRestartPolicy()
+        restart_policy.add_secrets(secret_names=(grafana_secret_name,))
+        pod_spec_builder.set_restart_policy(restart_policy)
+
         # Add ingress resources to pod spec if site url exists
         if config.site_url:
             parsed = urlparse(config.site_url)
         # Add ingress resources to pod spec if site url exists
         if config.site_url:
             parsed = urlparse(config.site_url)
@@ -213,11 +302,17 @@ class GrafanaCharm(CharmedOsmBase):
             else:
                 annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
 
             else:
                 annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
 
-            ingress_resource_builder.add_rule(parsed.hostname, self.app.name, PORT)
+            ingress_resource_builder.add_rule(
+                parsed.hostname, self.app.name, config.port
+            )
             ingress_resource = ingress_resource_builder.build()
             pod_spec_builder.add_ingress_resource(ingress_resource)
         return pod_spec_builder.build()
 
 
             ingress_resource = ingress_resource_builder.build()
             pod_spec_builder.add_ingress_resource(ingress_resource)
         return pod_spec_builder.build()
 
 
+def _generate_random_password():
+    return secrets.token_hex(16)
+
+
 if __name__ == "__main__":
     main(GrafanaCharm)
 if __name__ == "__main__":
     main(GrafanaCharm)