| #!/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) |