Add secret-management in Charmed OSM
[osm/devops.git] / installers / charm / keystone / src / charm.py
index 51ee6ad..808af3b 100755 (executable)
@@ -39,6 +39,7 @@ from opslib.osm.pod import (
     ContainerV3Builder,
     FilesV3Builder,
     IngressResourceV3Builder,
+    PodRestartPolicy,
     PodSpecV3Builder,
 )
 from opslib.osm.validator import ModelValidator, validator
@@ -159,7 +160,11 @@ class ConfigLdapModel(ModelValidator):
 
 class KeystoneCharm(CharmedOsmBase):
     def __init__(self, *args) -> NoReturn:
-        super().__init__(*args, oci_image="image")
+        super().__init__(
+            *args,
+            oci_image="image",
+            mysql_uri=True,
+        )
         self.state.set_default(fernet_keys=None)
         self.state.set_default(credential_keys=None)
         self.state.set_default(keys_timestamp=0)
@@ -168,6 +173,7 @@ class KeystoneCharm(CharmedOsmBase):
         self.mysql_client = MysqlClient(self, "db")
         self.framework.observe(self.on["db"].relation_changed, self.configure_pod)
         self.framework.observe(self.on["db"].relation_broken, self.configure_pod)
+        self.framework.observe(self.on.update_status, self.configure_pod)
 
         self.framework.observe(
             self.on["keystone"].relation_joined, self._publish_keystone_info
@@ -191,21 +197,13 @@ class KeystoneCharm(CharmedOsmBase):
                 admin_project_name=config.admin_project,
             )
 
-    def _check_missing_dependencies(self, config: ConfigModel):
+    def _check_missing_dependencies(self, config: ConfigModel, external_db: bool):
         missing_relations = []
-        if not config.mysql_host and self.mysql_client.is_missing_data_in_unit():
+        if not external_db and self.mysql_client.is_missing_data_in_unit():
             missing_relations.append("mysql")
         if missing_relations:
             raise RelationsMissing(missing_relations)
 
-    def _validate_mysql_config(self, config: ConfigModel):
-        invalid_values = []
-        if not config.mysql_root_password:
-            invalid_values.append("Mysql root password must be provided")
-
-        if invalid_values:
-            raise ValueError("Invalid values: " + ", ".join(invalid_values))
-
     def _generate_keys(self) -> Tuple[List[str], List[str]]:
         """Generating new fernet tokens.
 
@@ -244,139 +242,215 @@ class KeystoneCharm(CharmedOsmBase):
             self.state.keys_timestamp = now
         return credential_keys, fernet_keys
 
-    def _build_files(self, config: ConfigModel):
+    def _build_files(
+        self, config: ConfigModel, credential_keys: List, fernet_keys: List
+    ):
         credentials_files_builder = FilesV3Builder()
         fernet_files_builder = FilesV3Builder()
-
-        credential_keys, fernet_keys = self._get_keys()
-
-        for (key_id, value) in enumerate(credential_keys):
-            credentials_files_builder.add_file(str(key_id), value)
-        for (key_id, value) in enumerate(fernet_keys):
-            fernet_files_builder.add_file(str(key_id), value)
+        for (key_id, _) in enumerate(credential_keys):
+            credentials_files_builder.add_file(str(key_id), str(key_id), secret=True)
+        for (key_id, _) in enumerate(fernet_keys):
+            fernet_files_builder.add_file(str(key_id), str(key_id), secret=True)
         return credentials_files_builder.build(), fernet_files_builder.build()
 
-    def build_pod_spec(self, image_info):
+    def build_pod_spec(self, image_info, **kwargs):
         # Validate config
         config = ConfigModel(**dict(self.config))
+        mysql_config = kwargs["mysql_config"]
         config_ldap = ConfigLdapModel(**dict(self.config))
 
-        if config.mysql_host and not self.mysql_client.is_missing_data_in_unit():
+        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")
-
-        if config.mysql_host:
-            self._validate_mysql_config(config)
-
         # Check relations
-        self._check_missing_dependencies(config)
+        external_db = True if mysql_config.mysql_uri else False
+        self._check_missing_dependencies(config, external_db)
 
         # Create Builder for the PodSpec
         pod_spec_builder = PodSpecV3Builder()
-
-        # Build Container
         container_builder = ContainerV3Builder(
             self.app.name, image_info, config.image_pull_policy
         )
-        container_builder.add_port(name=self.app.name, port=PORT)
 
         # Build files
-        credential_files, fernet_files = self._build_files(config)
+        credential_keys, fernet_keys = self._get_keys()
+        credential_files, fernet_files = self._build_files(
+            config, credential_keys, fernet_keys
+        )
+
+        # Add pod secrets
+        fernet_keys_secret_name = f"{self.app.name}-fernet-keys-secret"
+        pod_spec_builder.add_secret(
+            fernet_keys_secret_name,
+            {str(key_id): value for (key_id, value) in enumerate(fernet_keys)},
+        )
+        credential_keys_secret_name = f"{self.app.name}-credential-keys-secret"
+        pod_spec_builder.add_secret(
+            credential_keys_secret_name,
+            {str(key_id): value for (key_id, value) in enumerate(credential_keys)},
+        )
+        mysql_secret_name = f"{self.app.name}-mysql-secret"
+
+        pod_spec_builder.add_secret(
+            mysql_secret_name,
+            {
+                "host": mysql_config.host,
+                "port": str(mysql_config.port),
+                "user": mysql_config.username,
+                "password": mysql_config.password,
+            }
+            if mysql_config.mysql_uri
+            else {
+                "host": self.mysql_client.host,
+                "port": str(self.mysql_client.port),
+                "user": "root",
+                "password": self.mysql_client.root_password,
+            },
+        )
+        keystone_secret_name = f"{self.app.name}-keystone-secret"
+        pod_spec_builder.add_secret(
+            keystone_secret_name,
+            {
+                "db_password": config.keystone_db_password,
+                "admin_username": config.admin_username,
+                "admin_password": config.admin_password,
+                "admin_project": config.admin_project,
+                "service_username": config.service_username,
+                "service_password": config.service_password,
+                "service_project": config.service_project,
+            },
+        )
+        # Build Container
         container_builder.add_volume_config(
-            "credential-keys", CREDENTIAL_KEYS_PATH, credential_files
+            "credential-keys",
+            CREDENTIAL_KEYS_PATH,
+            credential_files,
+            secret_name=credential_keys_secret_name,
         )
         container_builder.add_volume_config(
-            "fernet-keys", FERNET_KEYS_PATH, fernet_files
+            "fernet-keys",
+            FERNET_KEYS_PATH,
+            fernet_files,
+            secret_name=fernet_keys_secret_name,
         )
+        container_builder.add_port(name=self.app.name, port=PORT)
         container_builder.add_envs(
             {
-                "DB_HOST": config.mysql_host or self.mysql_client.host,
-                "DB_PORT": config.mysql_port or self.mysql_client.port,
-                "ROOT_DB_USER": "root",
-                "ROOT_DB_PASSWORD": config.mysql_root_password
-                or self.mysql_client.root_password,
-                "KEYSTONE_DB_PASSWORD": config.keystone_db_password,
                 "REGION_ID": config.region_id,
                 "KEYSTONE_HOST": self.app.name,
-                "ADMIN_USERNAME": config.admin_username,
-                "ADMIN_PASSWORD": config.admin_password,
-                "ADMIN_PROJECT": config.admin_project,
-                "SERVICE_USERNAME": config.service_username,
-                "SERVICE_PASSWORD": config.service_password,
-                "SERVICE_PROJECT": config.service_project,
             }
         )
-
+        container_builder.add_secret_envs(
+            secret_name=mysql_secret_name,
+            envs={
+                "DB_HOST": "host",
+                "DB_PORT": "port",
+                "ROOT_DB_USER": "user",
+                "ROOT_DB_PASSWORD": "password",
+            },
+        )
+        container_builder.add_secret_envs(
+            secret_name=keystone_secret_name,
+            envs={
+                "KEYSTONE_DB_PASSWORD": "db_password",
+                "ADMIN_USERNAME": "admin_username",
+                "ADMIN_PASSWORD": "admin_password",
+                "ADMIN_PROJECT": "admin_project",
+                "SERVICE_USERNAME": "service_username",
+                "SERVICE_PASSWORD": "service_password",
+                "SERVICE_PROJECT": "service_project",
+            },
+        )
+        ldap_secret_name = f"{self.app.name}-ldap-secret"
         if config_ldap.ldap_enabled:
-            container_builder.add_envs(
-                {
-                    "LDAP_AUTHENTICATION_DOMAIN_NAME": config_ldap.ldap_authentication_domain_name,
-                    "LDAP_URL": config_ldap.ldap_url,
-                    "LDAP_PAGE_SIZE": config_ldap.ldap_page_size,
-                    "LDAP_USER_OBJECTCLASS": config_ldap.ldap_user_objectclass,
-                    "LDAP_USER_ID_ATTRIBUTE": config_ldap.ldap_user_id_attribute,
-                    "LDAP_USER_NAME_ATTRIBUTE": config_ldap.ldap_user_name_attribute,
-                    "LDAP_USER_PASS_ATTRIBUTE": config_ldap.ldap_user_pass_attribute,
-                    "LDAP_USER_ENABLED_MASK": config_ldap.ldap_user_enabled_mask,
-                    "LDAP_USER_ENABLED_DEFAULT": config_ldap.ldap_user_enabled_default,
-                    "LDAP_USER_ENABLED_INVERT": config_ldap.ldap_user_enabled_invert,
-                    "LDAP_GROUP_OBJECTCLASS": config_ldap.ldap_group_objectclass,
-                }
-            )
+            # Add ldap secrets and envs
+            ldap_secrets = {
+                "authentication_domain_name": config_ldap.ldap_authentication_domain_name,
+                "url": config_ldap.ldap_url,
+                "page_size": config_ldap.ldap_page_size,
+                "user_objectclass": config_ldap.ldap_user_objectclass,
+                "user_id_attribute": config_ldap.ldap_user_id_attribute,
+                "user_name_attribute": config_ldap.ldap_user_name_attribute,
+                "user_pass_attribute": config_ldap.ldap_user_pass_attribute,
+                "user_enabled_mask": config_ldap.ldap_user_enabled_mask,
+                "user_enabled_default": config_ldap.ldap_user_enabled_default,
+                "user_enabled_invert": config_ldap.ldap_user_enabled_invert,
+                "group_objectclass": config_ldap.ldap_group_objectclass,
+            }
+            ldap_envs = {
+                "LDAP_AUTHENTICATION_DOMAIN_NAME": "authentication_domain_name",
+                "LDAP_URL": "url",
+                "LDAP_PAGE_SIZE": "page_size",
+                "LDAP_USER_OBJECTCLASS": "user_objectclass",
+                "LDAP_USER_ID_ATTRIBUTE": "user_id_attribute",
+                "LDAP_USER_NAME_ATTRIBUTE": "user_name_attribute",
+                "LDAP_USER_PASS_ATTRIBUTE": "user_pass_attribute",
+                "LDAP_USER_ENABLED_MASK": "user_enabled_mask",
+                "LDAP_USER_ENABLED_DEFAULT": "user_enabled_default",
+                "LDAP_USER_ENABLED_INVERT": "user_enabled_invert",
+                "LDAP_GROUP_OBJECTCLASS": "group_objectclass",
+            }
             if config_ldap.ldap_bind_user:
-                container_builder.add_envs(
-                    {"LDAP_BIND_USER": config_ldap.ldap_bind_user}
-                )
+                ldap_secrets["bind_user"] = config_ldap.ldap_bind_user
+                ldap_envs["LDAP_BIND_USER"] = "bind_user"
 
             if config_ldap.ldap_bind_password:
-                container_builder.add_envs(
-                    {"LDAP_BIND_PASSWORD": config_ldap.ldap_bind_password}
-                )
+                ldap_secrets["bind_password"] = config_ldap.ldap_bind_password
+                ldap_envs["LDAP_BIND_PASSWORD"] = "bind_password"
 
             if config_ldap.ldap_user_tree_dn:
-                container_builder.add_envs(
-                    {"LDAP_USER_TREE_DN": config_ldap.ldap_user_tree_dn}
-                )
+                ldap_secrets["user_tree_dn"] = config_ldap.ldap_user_tree_dn
+                ldap_envs["LDAP_USER_TREE_DN"] = "user_tree_dn"
 
             if config_ldap.ldap_user_filter:
-                container_builder.add_envs(
-                    {"LDAP_USER_FILTER": config_ldap.ldap_user_filter}
-                )
+                ldap_secrets["user_filter"] = config_ldap.ldap_user_filter
+                ldap_envs["LDAP_USER_FILTER"] = "user_filter"
 
             if config_ldap.ldap_user_enabled_attribute:
-                container_builder.add_envs(
-                    {
-                        "LDAP_USER_ENABLED_ATTRIBUTE": config_ldap.ldap_user_enabled_attribute
-                    }
-                )
-
+                ldap_secrets[
+                    "user_enabled_attribute"
+                ] = config_ldap.ldap_user_enabled_attribute
+                ldap_envs["LDAP_USER_ENABLED_ATTRIBUTE"] = "user_enabled_attribute"
             if config_ldap.ldap_chase_referrals:
-                container_builder.add_envs(
-                    {"LDAP_CHASE_REFERRALS": config_ldap.ldap_chase_referrals}
-                )
+                ldap_secrets["chase_referrals"] = config_ldap.ldap_chase_referrals
+                ldap_envs["LDAP_CHASE_REFERRALS"] = "chase_referrals"
 
             if config_ldap.ldap_group_tree_dn:
-                container_builder.add_envs(
-                    {"LDAP_GROUP_TREE_DN": config_ldap.ldap_group_tree_dn}
-                )
+                ldap_secrets["group_tree_dn"] = config_ldap.ldap_group_tree_dn
+                ldap_envs["LDAP_GROUP_TREE_DN"] = "group_tree_dn"
 
             if config_ldap.ldap_tls_cacert_base64:
-                container_builder.add_envs(
-                    {"LDAP_TLS_CACERT_BASE64": config_ldap.ldap_tls_cacert_base64}
-                )
+                ldap_secrets["tls_cacert_base64"] = config_ldap.ldap_tls_cacert_base64
+                ldap_envs["LDAP_TLS_CACERT_BASE64"] = "tls_cacert_base64"
 
             if config_ldap.ldap_use_starttls:
-                container_builder.add_envs(
-                    {
-                        "LDAP_USE_STARTTLS": config_ldap.ldap_use_starttls,
-                        "LDAP_TLS_CACERT_BASE64": config_ldap.ldap_tls_cacert_base64,
-                        "LDAP_TLS_REQ_CERT": config_ldap.ldap_tls_req_cert,
-                    }
-                )
+                ldap_secrets["use_starttls"] = config_ldap.ldap_use_starttls
+                ldap_secrets["tls_cacert_base64"] = config_ldap.ldap_tls_cacert_base64
+                ldap_secrets["tls_req_cert"] = config_ldap.ldap_tls_req_cert
+                ldap_envs["LDAP_USE_STARTTLS"] = "use_starttls"
+                ldap_envs["LDAP_TLS_CACERT_BASE64"] = "tls_cacert_base64"
+                ldap_envs["LDAP_TLS_REQ_CERT"] = "tls_req_cert"
+
+            pod_spec_builder.add_secret(
+                ldap_secret_name,
+                ldap_secrets,
+            )
+            container_builder.add_secret_envs(
+                secret_name=ldap_secret_name,
+                envs=ldap_envs,
+            )
+
         container = container_builder.build()
 
         # Add container to pod spec
         pod_spec_builder.add_container(container)
 
+        # Add Pod Restart Policy
+        restart_policy = PodRestartPolicy()
+        restart_policy.add_secrets(
+            secret_names=(mysql_secret_name, keystone_secret_name, ldap_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)