#!/usr/bin/env python3 # Copyright 2021 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 ## # pylint: disable=E0213 from datetime import datetime from ipaddress import ip_network import json import logging from typing import List, NoReturn, Optional, Tuple from urllib.parse import urlparse from cryptography.fernet import Fernet from ops.main import main from opslib.osm.charm import CharmedOsmBase, RelationsMissing from opslib.osm.interfaces.keystone import KeystoneServer from opslib.osm.interfaces.mysql import MysqlClient from opslib.osm.pod import ( ContainerV3Builder, FilesV3Builder, IngressResourceV3Builder, PodRestartPolicy, PodSpecV3Builder, ) from opslib.osm.validator import ModelValidator, validator logger = logging.getLogger(__name__) REQUIRED_SETTINGS = ["token_expiration"] # This is hardcoded in the keystone container script DATABASE_NAME = "keystone" # We expect the keystone container to use the default port PORT = 5000 # Number of keys need might need to be adjusted in the future NUMBER_FERNET_KEYS = 2 NUMBER_CREDENTIAL_KEYS = 2 # Path for keys CREDENTIAL_KEYS_PATH = "/etc/keystone/credential-keys" FERNET_KEYS_PATH = "/etc/keystone/fernet-keys" class ConfigModel(ModelValidator): region_id: str keystone_db_password: str admin_username: str admin_password: str admin_project: str service_username: str service_password: str service_project: str user_domain_name: str project_domain_name: str token_expiration: int max_file_size: int site_url: Optional[str] ingress_class: Optional[str] ingress_whitelist_source_range: Optional[str] tls_secret_name: Optional[str] mysql_host: Optional[str] mysql_port: Optional[int] mysql_root_password: Optional[str] image_pull_policy: str security_context: bool @validator("max_file_size") def validate_max_file_size(cls, v): if v < 0: raise ValueError("value must be equal or greater than 0") return v @validator("site_url") def validate_site_url(cls, v): if v: parsed = urlparse(v) if not parsed.scheme.startswith("http"): raise ValueError("value must start with http") return v @validator("ingress_whitelist_source_range") def validate_ingress_whitelist_source_range(cls, v): if v: ip_network(v) 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] class ConfigLdapModel(ModelValidator): ldap_enabled: bool ldap_authentication_domain_name: Optional[str] ldap_url: Optional[str] ldap_bind_user: Optional[str] ldap_bind_password: Optional[str] ldap_chase_referrals: Optional[str] ldap_page_size: Optional[int] ldap_user_tree_dn: Optional[str] ldap_user_objectclass: Optional[str] ldap_user_id_attribute: Optional[str] ldap_user_name_attribute: Optional[str] ldap_user_pass_attribute: Optional[str] ldap_user_filter: Optional[str] ldap_user_enabled_attribute: Optional[str] ldap_user_enabled_mask: Optional[int] ldap_user_enabled_default: Optional[str] ldap_user_enabled_invert: Optional[bool] ldap_group_objectclass: Optional[str] ldap_group_tree_dn: Optional[str] ldap_use_starttls: Optional[bool] ldap_tls_cacert_base64: Optional[str] ldap_tls_req_cert: Optional[str] @validator def validate_ldap_user_enabled_default(cls, v): if v: if v not in ["true", "false"]: raise ValueError('must be equal to "true" or "false"') return v class KeystoneCharm(CharmedOsmBase): def __init__(self, *args) -> NoReturn: 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) self.keystone_server = KeystoneServer(self, "keystone") 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 ) def _publish_keystone_info(self, event): if self.unit.is_leader(): config = ConfigModel(**dict(self.config)) self.keystone_server.publish_info( host=f"http://{self.app.name}:{PORT}/v3", port=PORT, user_domain_name=config.user_domain_name, project_domain_name=config.project_domain_name, username=config.service_username, password=config.service_password, service=config.service_project, keystone_db_password=config.keystone_db_password, region_id=config.region_id, admin_username=config.admin_username, admin_password=config.admin_password, admin_project_name=config.admin_project, ) def _check_missing_dependencies(self, config: ConfigModel, external_db: bool): missing_relations = [] 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 _generate_keys(self) -> Tuple[List[str], List[str]]: """Generating new fernet tokens. Returns: Tuple[List[str], List[str]]: contains two lists of strings. First list contains strings that represent the keys for fernet and the second list contains strins that represent the keys for credentials. """ fernet_keys = [ Fernet.generate_key().decode() for _ in range(NUMBER_FERNET_KEYS) ] credential_keys = [ Fernet.generate_key().decode() for _ in range(NUMBER_CREDENTIAL_KEYS) ] return (fernet_keys, credential_keys) def _get_keys(self): keys_timestamp = self.state.keys_timestamp if fernet_keys := self.state.fernet_keys: fernet_keys = json.loads(fernet_keys) if credential_keys := self.state.credential_keys: credential_keys = json.loads(credential_keys) now = datetime.now().timestamp() token_expiration = self.config["token_expiration"] valid_keys = (now - keys_timestamp) < token_expiration if not credential_keys or not fernet_keys or not valid_keys: fernet_keys, credential_keys = self._generate_keys() self.state.fernet_keys = json.dumps(fernet_keys) self.state.credential_keys = json.dumps(credential_keys) self.state.keys_timestamp = now return credential_keys, fernet_keys def _build_files( self, config: ConfigModel, credential_keys: List, fernet_keys: List ): credentials_files_builder = FilesV3Builder() fernet_files_builder = FilesV3Builder() 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, **kwargs): # Validate config config = ConfigModel(**dict(self.config)) mysql_config = kwargs["mysql_config"] config_ldap = ConfigLdapModel(**dict(self.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 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( enable_security_context=config.security_context ) container_builder = ContainerV3Builder( self.app.name, image_info, config.image_pull_policy, run_as_non_root=config.security_context, ) # Build files 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, secret_name=credential_keys_secret_name, ) container_builder.add_volume_config( "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( { "REGION_ID": config.region_id, "KEYSTONE_HOST": self.app.name, } ) 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: # Add ldap secrets and envs ldap_secrets = { "authentication_domain_name": config_ldap.ldap_authentication_domain_name, "url": config_ldap.ldap_url, "page_size": str(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": str(config_ldap.ldap_user_enabled_mask), "user_enabled_default": config_ldap.ldap_user_enabled_default, "user_enabled_invert": str(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: ldap_secrets["bind_user"] = config_ldap.ldap_bind_user ldap_envs["LDAP_BIND_USER"] = "bind_user" if 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: 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: ldap_secrets["user_filter"] = config_ldap.ldap_user_filter ldap_envs["LDAP_USER_FILTER"] = "user_filter" if 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: ldap_secrets["chase_referrals"] = config_ldap.ldap_chase_referrals ldap_envs["LDAP_CHASE_REFERRALS"] = "chase_referrals" if 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: 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: ldap_secrets["use_starttls"] = str(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) annotations = { "nginx.ingress.kubernetes.io/proxy-body-size": "{}".format( str(config.max_file_size) + "m" if config.max_file_size > 0 else config.max_file_size ) } if config.ingress_class: annotations["kubernetes.io/ingress.class"] = config.ingress_class ingress_resource_builder = IngressResourceV3Builder( f"{self.app.name}-ingress", annotations ) if config.ingress_whitelist_source_range: annotations[ "nginx.ingress.kubernetes.io/whitelist-source-range" ] = config.ingress_whitelist_source_range if parsed.scheme == "https": ingress_resource_builder.add_tls( [parsed.hostname], config.tls_secret_name ) else: annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false" ingress_resource_builder.add_rule(parsed.hostname, self.app.name, PORT) ingress_resource = ingress_resource_builder.build() pod_spec_builder.add_ingress_resource(ingress_resource) return pod_spec_builder.build() if __name__ == "__main__": main(KeystoneCharm) # LOGGER = logging.getLogger(__name__) # class ConfigurePodEvent(EventBase): # """Configure Pod event""" # pass # class KeystoneEvents(CharmEvents): # """Keystone Events""" # configure_pod = EventSource(ConfigurePodEvent) # class KeystoneCharm(CharmBase): # """Keystone K8s Charm""" # state = StoredState() # on = KeystoneEvents() # def __init__(self, *args) -> NoReturn: # """Constructor of the Charm object. # Initializes internal state and register events it can handle. # """ # super().__init__(*args) # self.state.set_default(db_host=None) # self.state.set_default(db_port=None) # self.state.set_default(db_user=None) # self.state.set_default(db_password=None) # self.state.set_default(pod_spec=None) # self.state.set_default(fernet_keys=None) # self.state.set_default(credential_keys=None) # self.state.set_default(keys_timestamp=0) # # Register all of the events we want to observe # self.framework.observe(self.on.config_changed, self.configure_pod) # self.framework.observe(self.on.start, self.configure_pod) # self.framework.observe(self.on.upgrade_charm, self.configure_pod) # self.framework.observe(self.on.leader_elected, self.configure_pod) # self.framework.observe(self.on.update_status, self.configure_pod) # # Registering custom internal events # self.framework.observe(self.on.configure_pod, self.configure_pod) # # Register relation events # self.framework.observe( # self.on.db_relation_changed, self._on_db_relation_changed # ) # self.framework.observe( # self.on.db_relation_broken, self._on_db_relation_broken # ) # self.framework.observe( # self.on.keystone_relation_joined, self._publish_keystone_info # ) # def _publish_keystone_info(self, event: EventBase) -> NoReturn: # """Publishes keystone information for NBI usage through the keystone # relation. # Args: # event (EventBase): Keystone relation event to update NBI. # """ # config = self.model.config # rel_data = { # "host": f"http://{self.app.name}:{KEYSTONE_PORT}/v3", # "port": str(KEYSTONE_PORT), # "keystone_db_password": config["keystone_db_password"], # "region_id": config["region_id"], # "user_domain_name": config["user_domain_name"], # "project_domain_name": config["project_domain_name"], # "admin_username": config["admin_username"], # "admin_password": config["admin_password"], # "admin_project_name": config["admin_project"], # "username": config["service_username"], # "password": config["service_password"], # "service": config["service_project"], # } # for k, v in rel_data.items(): # event.relation.data[self.model.unit][k] = v # def _on_db_relation_changed(self, event: EventBase) -> NoReturn: # """Reads information about the DB relation, in order for keystone to # access it. # Args: # event (EventBase): DB relation event to access database # information. # """ # if not event.unit in event.relation.data: # return # relation_data = event.relation.data[event.unit] # db_host = relation_data.get("host") # db_port = int(relation_data.get("port", 3306)) # db_user = "root" # db_password = relation_data.get("root_password") # if ( # db_host # and db_port # and db_user # and db_password # and ( # self.state.db_host != db_host # or self.state.db_port != db_port # or self.state.db_user != db_user # or self.state.db_password != db_password # ) # ): # self.state.db_host = db_host # self.state.db_port = db_port # self.state.db_user = db_user # self.state.db_password = db_password # self.on.configure_pod.emit() # def _on_db_relation_broken(self, event: EventBase) -> NoReturn: # """Clears data from db relation. # Args: # event (EventBase): DB relation event. # """ # self.state.db_host = None # self.state.db_port = None # self.state.db_user = None # self.state.db_password = None # self.on.configure_pod.emit() # def _check_settings(self) -> str: # """Check if there any settings missing from Keystone configuration. # Returns: # str: Information about the problems found (if any). # """ # problems = [] # config = self.model.config # for setting in REQUIRED_SETTINGS: # if not config.get(setting): # problem = f"missing config {setting}" # problems.append(problem) # return ";".join(problems) # def _make_pod_image_details(self) -> Dict[str, str]: # """Generate the pod image details. # Returns: # Dict[str, str]: pod image details. # """ # config = self.model.config # image_details = { # "imagePath": config["image"], # } # if config["image_username"]: # image_details.update( # { # "username": config["image_username"], # "password": config["image_password"], # } # ) # return image_details # def _make_pod_ports(self) -> List[Dict[str, Any]]: # """Generate the pod ports details. # Returns: # List[Dict[str, Any]]: pod ports details. # """ # return [ # {"name": "keystone", "containerPort": KEYSTONE_PORT, "protocol": "TCP"}, # ] # def _make_pod_envconfig(self) -> Dict[str, Any]: # """Generate pod environment configuraiton. # Returns: # Dict[str, Any]: pod environment configuration. # """ # config = self.model.config # envconfig = { # "DB_HOST": self.state.db_host, # "DB_PORT": self.state.db_port, # "ROOT_DB_USER": self.state.db_user, # "ROOT_DB_PASSWORD": self.state.db_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"], # } # if config.get("ldap_enabled"): # envconfig["LDAP_AUTHENTICATION_DOMAIN_NAME"] = config[ # "ldap_authentication_domain_name" # ] # envconfig["LDAP_URL"] = config["ldap_url"] # envconfig["LDAP_PAGE_SIZE"] = config["ldap_page_size"] # envconfig["LDAP_USER_OBJECTCLASS"] = config["ldap_user_objectclass"] # envconfig["LDAP_USER_ID_ATTRIBUTE"] = config["ldap_user_id_attribute"] # envconfig["LDAP_USER_NAME_ATTRIBUTE"] = config["ldap_user_name_attribute"] # envconfig["LDAP_USER_PASS_ATTRIBUTE"] = config["ldap_user_pass_attribute"] # envconfig["LDAP_USER_ENABLED_MASK"] = config["ldap_user_enabled_mask"] # envconfig["LDAP_USER_ENABLED_DEFAULT"] = config["ldap_user_enabled_default"] # envconfig["LDAP_USER_ENABLED_INVERT"] = config["ldap_user_enabled_invert"] # envconfig["LDAP_GROUP_OBJECTCLASS"] = config["ldap_group_objectclass"] # if config["ldap_bind_user"]: # envconfig["LDAP_BIND_USER"] = config["ldap_bind_user"] # if config["ldap_bind_password"]: # envconfig["LDAP_BIND_PASSWORD"] = config["ldap_bind_password"] # if config["ldap_user_tree_dn"]: # envconfig["LDAP_USER_TREE_DN"] = config["ldap_user_tree_dn"] # if config["ldap_user_filter"]: # envconfig["LDAP_USER_FILTER"] = config["ldap_user_filter"] # if config["ldap_user_enabled_attribute"]: # envconfig["LDAP_USER_ENABLED_ATTRIBUTE"] = config[ # "ldap_user_enabled_attribute" # ] # if config["ldap_chase_referrals"]: # envconfig["LDAP_CHASE_REFERRALS"] = config["ldap_chase_referrals"] # if config["ldap_group_tree_dn"]: # envconfig["LDAP_GROUP_TREE_DN"] = config["ldap_group_tree_dn"] # if config["ldap_use_starttls"]: # envconfig["LDAP_USE_STARTTLS"] = config["ldap_use_starttls"] # envconfig["LDAP_TLS_CACERT_BASE64"] = config["ldap_tls_cacert_base64"] # envconfig["LDAP_TLS_REQ_CERT"] = config["ldap_tls_req_cert"] # return envconfig # def _make_pod_ingress_resources(self) -> List[Dict[str, Any]]: # """Generate pod ingress resources. # Returns: # List[Dict[str, Any]]: pod ingress resources. # """ # site_url = self.model.config["site_url"] # if not site_url: # return # parsed = urlparse(site_url) # if not parsed.scheme.startswith("http"): # return # max_file_size = self.model.config["max_file_size"] # ingress_whitelist_source_range = self.model.config[ # "ingress_whitelist_source_range" # ] # annotations = { # "nginx.ingress.kubernetes.io/proxy-body-size": "{}m".format(max_file_size) # } # if ingress_whitelist_source_range: # annotations[ # "nginx.ingress.kubernetes.io/whitelist-source-range" # ] = ingress_whitelist_source_range # ingress_spec_tls = None # if parsed.scheme == "https": # ingress_spec_tls = [{"hosts": [parsed.hostname]}] # tls_secret_name = self.model.config["tls_secret_name"] # if tls_secret_name: # ingress_spec_tls[0]["secretName"] = tls_secret_name # else: # annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false" # ingress = { # "name": "{}-ingress".format(self.app.name), # "annotations": annotations, # "spec": { # "rules": [ # { # "host": parsed.hostname, # "http": { # "paths": [ # { # "path": "/", # "backend": { # "serviceName": self.app.name, # "servicePort": KEYSTONE_PORT, # }, # } # ] # }, # } # ], # }, # } # if ingress_spec_tls: # ingress["spec"]["tls"] = ingress_spec_tls # return [ingress] # def _generate_keys(self) -> Tuple[List[str], List[str]]: # """Generating new fernet tokens. # Returns: # Tuple[List[str], List[str]]: contains two lists of strings. First # list contains strings that represent # the keys for fernet and the second # list contains strins that represent # the keys for credentials. # """ # fernet_keys = [ # Fernet.generate_key().decode() for _ in range(NUMBER_FERNET_KEYS) # ] # credential_keys = [ # Fernet.generate_key().decode() for _ in range(NUMBER_CREDENTIAL_KEYS) # ] # return (fernet_keys, credential_keys) # def configure_pod(self, event: EventBase) -> NoReturn: # """Assemble the pod spec and apply it, if possible. # Args: # event (EventBase): Hook or Relation event that started the # function. # """ # if not self.state.db_host: # self.unit.status = WaitingStatus("Waiting for database relation") # event.defer() # return # if not self.unit.is_leader(): # self.unit.status = ActiveStatus("ready") # return # if fernet_keys := self.state.fernet_keys: # fernet_keys = json.loads(fernet_keys) # if credential_keys := self.state.credential_keys: # credential_keys = json.loads(credential_keys) # now = datetime.now().timestamp() # keys_timestamp = self.state.keys_timestamp # token_expiration = self.model.config["token_expiration"] # valid_keys = (now - keys_timestamp) < token_expiration # if not credential_keys or not fernet_keys or not valid_keys: # fernet_keys, credential_keys = self._generate_keys() # self.state.fernet_keys = json.dumps(fernet_keys) # self.state.credential_keys = json.dumps(credential_keys) # self.state.keys_timestamp = now # # Check problems in the settings # problems = self._check_settings() # if problems: # self.unit.status = BlockedStatus(problems) # return # self.unit.status = BlockedStatus("Assembling pod spec") # image_details = self._make_pod_image_details() # ports = self._make_pod_ports() # env_config = self._make_pod_envconfig() # ingress_resources = self._make_pod_ingress_resources() # files = self._make_pod_files(fernet_keys, credential_keys) # pod_spec = { # "version": 3, # "containers": [ # { # "name": self.framework.model.app.name, # "imageDetails": image_details, # "ports": ports, # "envConfig": env_config, # "volumeConfig": files, # } # ], # "kubernetesResources": {"ingressResources": ingress_resources or []}, # } # if self.state.pod_spec != ( # pod_spec_json := json.dumps(pod_spec, sort_keys=True) # ): # self.state.pod_spec = pod_spec_json # self.model.pod.set_spec(pod_spec) # self.unit.status = ActiveStatus("ready") # if __name__ == "__main__": # main(KeystoneCharm)