Fix validation error for ImagePullPolicy in charms
[osm/devops.git] / installers / charm / grafana / src / charm.py
index 87776aa..e20a052 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,
 from opslib.osm.interfaces.prometheus import PrometheusClient
 from opslib.osm.pod import (
     ContainerV3Builder,
@@ -43,10 +46,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 +58,17 @@ 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
+
+    @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 +108,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()
@@ -133,30 +152,46 @@ class GrafanaCharm(CharmedOsmBase):
         )
         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
         pod_spec_builder = PodSpecV3Builder()
         # Create Builder for the PodSpec
         pod_spec_builder = PodSpecV3Builder()
+
         # Build Container
         container_builder = ContainerV3Builder(
             self.app.name, image_info, config.image_pull_policy
         )
         # Build Container
         container_builder = ContainerV3Builder(
             self.app.name, image_info, config.image_pull_policy
         )
-        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 +199,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 +214,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,
+                "GF_SECURITY_ADMIN_PASSWORD": {
+                    "secret": {"name": "grafana-admin-secret", "key": "admin-password"}
+                },
+                "GF_DATABASE_URL": {
+                    "secret": {"name": "grafana-admin-secret", "key": "mysql-url"}
+                },
+            },
+        )
         container = container_builder.build()
         # Add container to pod spec
         pod_spec_builder.add_container(container)
         container = container_builder.build()
         # Add container to pod spec
         pod_spec_builder.add_container(container)
+        pod_spec_builder.add_secret(
+            "grafana-admin-secret",
+            {
+                "admin-password": admin_initial_password,
+                "mysql-url": mysql_config.mysql_uri or self.mysql_client.get_uri(),
+            },
+        )
         # 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 +269,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)