Adding ImagePullPolicy config option to OSM Charms
[osm/devops.git] / installers / charm / ro / src / charm.py
index 6851600..d87007e 100755 (executable)
 
 # pylint: disable=E0213
 
 
 # pylint: disable=E0213
 
+import base64
 import logging
 import logging
-from typing import NoReturn
+from typing import NoReturn, Optional
 
 from ops.main import main
 from opslib.osm.charm import CharmedOsmBase, RelationsMissing
 from opslib.osm.interfaces.kafka import KafkaClient
 from opslib.osm.interfaces.mongo import MongoClient
 from opslib.osm.interfaces.mysql import MysqlClient
 
 from ops.main import main
 from opslib.osm.charm import CharmedOsmBase, RelationsMissing
 from opslib.osm.interfaces.kafka import KafkaClient
 from opslib.osm.interfaces.mongo import MongoClient
 from opslib.osm.interfaces.mysql import MysqlClient
-from opslib.osm.pod import (
-    ContainerV3Builder,
-    PodSpecV3Builder,
-)
+from opslib.osm.pod import ContainerV3Builder, FilesV3Builder, PodSpecV3Builder
 from opslib.osm.validator import ModelValidator, validator
 
 from opslib.osm.validator import ModelValidator, validator
 
-
 logger = logging.getLogger(__name__)
 
 PORT = 9090
 
 
 logger = logging.getLogger(__name__)
 
 PORT = 9090
 
 
+def _check_certificate_data(name: str, content: str):
+    if not name or not content:
+        raise ValueError("certificate name and content must be a non-empty string")
+
+
+def _extract_certificates(certs_config: str):
+    certificates = {}
+    if certs_config:
+        cert_list = certs_config.split(",")
+        for cert in cert_list:
+            name, content = cert.split(":")
+            _check_certificate_data(name, content)
+            certificates[name] = content
+    return certificates
+
+
+def decode(content: str):
+    return base64.b64decode(content.encode("utf-8")).decode("utf-8")
+
+
 class ConfigModel(ModelValidator):
     enable_ng_ro: bool
     database_commonkey: str
 class ConfigModel(ModelValidator):
     enable_ng_ro: bool
     database_commonkey: str
+    mongodb_uri: Optional[str]
     log_level: str
     log_level: str
+    mysql_host: Optional[str]
+    mysql_port: Optional[int]
+    mysql_user: Optional[str]
+    mysql_password: Optional[str]
+    mysql_root_password: Optional[str]
     vim_database: str
     ro_database: str
     openmano_tenant: str
     vim_database: str
     ro_database: str
     openmano_tenant: str
+    certificates: Optional[str]
+    image_pull_policy: Optional[str]
 
     @validator("log_level")
     def validate_log_level(cls, v):
 
     @validator("log_level")
     def validate_log_level(cls, v):
@@ -56,6 +81,40 @@ class ConfigModel(ModelValidator):
             raise ValueError("value must be INFO or DEBUG")
         return v
 
             raise ValueError("value must be INFO or DEBUG")
         return v
 
+    @validator("certificates")
+    def validate_certificates(cls, v):
+        # Raises an exception if it cannot extract the certificates
+        _extract_certificates(v)
+        return v
+
+    @validator("mongodb_uri")
+    def validate_mongodb_uri(cls, v):
+        if v and not v.startswith("mongodb://"):
+            raise ValueError("mongodb_uri is not properly formed")
+        return v
+
+    @validator("mysql_port")
+    def validate_mysql_port(cls, v):
+        if v and (v <= 0 or v >= 65535):
+            raise ValueError("Mysql port out of range")
+        return v
+
+    @validator("image_pull_policy")
+    def validate_image_pull_policy(cls, v):
+        values = {
+            "always": "Always",
+            "ifnotpresent": "IfNotPresent",
+            "never": "Never",
+        }
+        v = v.lower()
+        if v not in values.keys():
+            raise ValueError("value must be always, ifnotpresent or never")
+        return values[v]
+
+    @property
+    def certificates_dict(cls):
+        return _extract_certificates(cls.certificates) if cls.certificates else {}
+
 
 class RoCharm(CharmedOsmBase):
     """GrafanaCharm Charm."""
 
 class RoCharm(CharmedOsmBase):
     """GrafanaCharm Charm."""
@@ -98,23 +157,66 @@ class RoCharm(CharmedOsmBase):
         if config.enable_ng_ro:
             if self.kafka_client.is_missing_data_in_unit():
                 missing_relations.append("kafka")
         if config.enable_ng_ro:
             if self.kafka_client.is_missing_data_in_unit():
                 missing_relations.append("kafka")
-            if self.mongodb_client.is_missing_data_in_unit():
+            if not config.mongodb_uri and self.mongodb_client.is_missing_data_in_unit():
                 missing_relations.append("mongodb")
         else:
                 missing_relations.append("mongodb")
         else:
-            if self.mysql_client.is_missing_data_in_unit():
+            if not config.mysql_host and self.mysql_client.is_missing_data_in_unit():
                 missing_relations.append("mysql")
         if missing_relations:
             raise RelationsMissing(missing_relations)
 
                 missing_relations.append("mysql")
         if missing_relations:
             raise RelationsMissing(missing_relations)
 
+    def _validate_mysql_config(self, config: ConfigModel):
+        invalid_values = []
+        if not config.mysql_user:
+            invalid_values.append("Mysql user is empty")
+        if not config.mysql_password:
+            invalid_values.append("Mysql password is empty")
+        if not config.mysql_root_password:
+            invalid_values.append("Mysql root password empty")
+
+        if invalid_values:
+            raise ValueError("Invalid values: " + ", ".join(invalid_values))
+
+    def _build_cert_files(
+        self,
+        config: ConfigModel,
+    ):
+        cert_files_builder = FilesV3Builder()
+        for name, content in config.certificates_dict.items():
+            cert_files_builder.add_file(name, decode(content), mode=0o600)
+        return cert_files_builder.build()
+
     def build_pod_spec(self, image_info):
         # Validate config
         config = ConfigModel(**dict(self.config))
     def build_pod_spec(self, image_info):
         # Validate config
         config = ConfigModel(**dict(self.config))
+
+        if config.enable_ng_ro:
+            if config.mongodb_uri and not self.mongodb_client.is_missing_data_in_unit():
+                raise Exception(
+                    "Mongodb data cannot be provided via config and relation"
+                )
+        else:
+            if config.mysql_host and not self.mysql_client.is_missing_data_in_unit():
+                raise Exception("Mysql data cannot be provided via config and relation")
+
+            if config.mysql_host:
+                self._validate_mysql_config(config)
+
         # Check relations
         self._check_missing_dependencies(config)
         # Check relations
         self._check_missing_dependencies(config)
+
         # Create Builder for the PodSpec
         pod_spec_builder = PodSpecV3Builder()
         # Create Builder for the PodSpec
         pod_spec_builder = PodSpecV3Builder()
+
         # Build Container
         # Build Container
-        container_builder = ContainerV3Builder(self.app.name, image_info)
+        container_builder = ContainerV3Builder(
+            self.app.name, image_info, config.image_pull_policy
+        )
+        certs_files = self._build_cert_files(config)
+
+        if certs_files:
+            container_builder.add_volume_config("certs", "/certs", certs_files)
+
         container_builder.add_port(name=self.app.name, port=PORT)
         container_builder.add_http_readiness_probe(
             "/ro/" if config.enable_ng_ro else "/openmano/tenants",
         container_builder.add_port(name=self.app.name, port=PORT)
         container_builder.add_http_readiness_probe(
             "/ro/" if config.enable_ng_ro else "/openmano/tenants",
@@ -137,6 +239,7 @@ class RoCharm(CharmedOsmBase):
                 "OSMRO_LOG_LEVEL": config.log_level,
             }
         )
                 "OSMRO_LOG_LEVEL": config.log_level,
             }
         )
+
         if config.enable_ng_ro:
             container_builder.add_envs(
                 {
         if config.enable_ng_ro:
             container_builder.add_envs(
                 {
@@ -145,7 +248,8 @@ class RoCharm(CharmedOsmBase):
                     "OSMRO_MESSAGE_PORT": self.kafka_client.port,
                     # MongoDB configuration
                     "OSMRO_DATABASE_DRIVER": "mongo",
                     "OSMRO_MESSAGE_PORT": self.kafka_client.port,
                     # MongoDB configuration
                     "OSMRO_DATABASE_DRIVER": "mongo",
-                    "OSMRO_DATABASE_URI": self.mongodb_client.connection_string,
+                    "OSMRO_DATABASE_URI": config.mongodb_uri
+                    or self.mongodb_client.connection_string,
                     "OSMRO_DATABASE_COMMONKEY": config.database_commonkey,
                 }
             )
                     "OSMRO_DATABASE_COMMONKEY": config.database_commonkey,
                 }
             )
@@ -153,24 +257,30 @@ class RoCharm(CharmedOsmBase):
         else:
             container_builder.add_envs(
                 {
         else:
             container_builder.add_envs(
                 {
-                    "RO_DB_HOST": self.mysql_client.host,
-                    "RO_DB_OVIM_HOST": self.mysql_client.host,
-                    "RO_DB_PORT": self.mysql_client.port,
-                    "RO_DB_OVIM_PORT": self.mysql_client.port,
-                    "RO_DB_USER": self.mysql_client.user,
-                    "RO_DB_OVIM_USER": self.mysql_client.user,
-                    "RO_DB_PASSWORD": self.mysql_client.password,
-                    "RO_DB_OVIM_PASSWORD": self.mysql_client.password,
-                    "RO_DB_ROOT_PASSWORD": self.mysql_client.root_password,
-                    "RO_DB_OVIM_ROOT_PASSWORD": self.mysql_client.root_password,
+                    "RO_DB_HOST": config.mysql_host or self.mysql_client.host,
+                    "RO_DB_OVIM_HOST": config.mysql_host or self.mysql_client.host,
+                    "RO_DB_PORT": config.mysql_port or self.mysql_client.port,
+                    "RO_DB_OVIM_PORT": config.mysql_port or self.mysql_client.port,
+                    "RO_DB_USER": config.mysql_user or self.mysql_client.user,
+                    "RO_DB_OVIM_USER": config.mysql_user or self.mysql_client.user,
+                    "RO_DB_PASSWORD": config.mysql_password
+                    or self.mysql_client.password,
+                    "RO_DB_OVIM_PASSWORD": config.mysql_password
+                    or self.mysql_client.password,
+                    "RO_DB_ROOT_PASSWORD": config.mysql_root_password
+                    or self.mysql_client.root_password,
+                    "RO_DB_OVIM_ROOT_PASSWORD": config.mysql_root_password
+                    or self.mysql_client.root_password,
                     "RO_DB_NAME": config.ro_database,
                     "RO_DB_OVIM_NAME": config.vim_database,
                     "OPENMANO_TENANT": config.openmano_tenant,
                 }
             )
         container = container_builder.build()
                     "RO_DB_NAME": config.ro_database,
                     "RO_DB_OVIM_NAME": config.vim_database,
                     "OPENMANO_TENANT": config.openmano_tenant,
                 }
             )
         container = container_builder.build()
+
         # Add container to pod spec
         pod_spec_builder.add_container(container)
         # Add container to pod spec
         pod_spec_builder.add_container(container)
+
         return pod_spec_builder.build()
 
 
         return pod_spec_builder.build()