blob: 446d2e0ce5e4e0d9983d80b0161fcae432589019 [file] [log] [blame]
David Garcia009a5d62020-08-27 16:53:44 +02001#!/usr/bin/env python3
David Garcia49379ce2021-02-24 13:48:22 +01002# Copyright 2021 Canonical Ltd.
David Garcia009a5d62020-08-27 16:53:44 +02003#
David Garcia49379ce2021-02-24 13:48:22 +01004# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
David Garcia009a5d62020-08-27 16:53:44 +02007#
David Garcia49379ce2021-02-24 13:48:22 +01008# http://www.apache.org/licenses/LICENSE-2.0
David Garcia009a5d62020-08-27 16:53:44 +02009#
David Garcia49379ce2021-02-24 13:48:22 +010010# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
15#
16# For those usages not covered by the Apache License, Version 2.0 please
17# contact: legal@canonical.com
18#
19# To get in touch with the maintainers, please contact:
20# osm-charmers@lists.launchpad.net
21##
22
23# pylint: disable=E0213
24
David Garcia009a5d62020-08-27 16:53:44 +020025
David Garciac753dc52021-03-17 15:28:47 +010026from datetime import datetime
27from ipaddress import ip_network
sousaedu738bf6f2020-10-10 00:25:26 +010028import json
David Garcia009a5d62020-08-27 16:53:44 +020029import logging
David Garciac753dc52021-03-17 15:28:47 +010030from typing import List, NoReturn, Optional, Tuple
David Garcia009a5d62020-08-27 16:53:44 +020031from urllib.parse import urlparse
32
David Garciac753dc52021-03-17 15:28:47 +010033from cryptography.fernet import Fernet
David Garcia009a5d62020-08-27 16:53:44 +020034from ops.main import main
David Garcia49379ce2021-02-24 13:48:22 +010035from opslib.osm.charm import CharmedOsmBase, RelationsMissing
David Garciac753dc52021-03-17 15:28:47 +010036from opslib.osm.interfaces.keystone import KeystoneServer
37from opslib.osm.interfaces.mysql import MysqlClient
David Garcia49379ce2021-02-24 13:48:22 +010038from opslib.osm.pod import (
39 ContainerV3Builder,
David Garcia49379ce2021-02-24 13:48:22 +010040 FilesV3Builder,
41 IngressResourceV3Builder,
David Garcia141d9352021-09-08 17:48:40 +020042 PodRestartPolicy,
David Garciac753dc52021-03-17 15:28:47 +010043 PodSpecV3Builder,
David Garcia009a5d62020-08-27 16:53:44 +020044)
David Garciac753dc52021-03-17 15:28:47 +010045from opslib.osm.validator import ModelValidator, validator
David Garcia49379ce2021-02-24 13:48:22 +010046
47
48logger = logging.getLogger(__name__)
49
David Garcia009a5d62020-08-27 16:53:44 +020050
David Garciaab11f842020-12-16 17:25:15 +010051REQUIRED_SETTINGS = ["token_expiration"]
David Garcia009a5d62020-08-27 16:53:44 +020052
sousaedu738bf6f2020-10-10 00:25:26 +010053# This is hardcoded in the keystone container script
54DATABASE_NAME = "keystone"
55
David Garcia009a5d62020-08-27 16:53:44 +020056# We expect the keystone container to use the default port
David Garcia49379ce2021-02-24 13:48:22 +010057PORT = 5000
David Garcia009a5d62020-08-27 16:53:44 +020058
sousaedu738bf6f2020-10-10 00:25:26 +010059# Number of keys need might need to be adjusted in the future
60NUMBER_FERNET_KEYS = 2
61NUMBER_CREDENTIAL_KEYS = 2
62
63# Path for keys
64CREDENTIAL_KEYS_PATH = "/etc/keystone/credential-keys"
65FERNET_KEYS_PATH = "/etc/keystone/fernet-keys"
66
David Garcia009a5d62020-08-27 16:53:44 +020067
David Garcia49379ce2021-02-24 13:48:22 +010068class ConfigModel(ModelValidator):
69 region_id: str
70 keystone_db_password: str
71 admin_username: str
72 admin_password: str
73 admin_project: str
74 service_username: str
75 service_password: str
76 service_project: str
77 user_domain_name: str
78 project_domain_name: str
79 token_expiration: int
80 max_file_size: int
81 site_url: Optional[str]
David Garciad68e0b42021-06-28 16:50:42 +020082 ingress_class: Optional[str]
David Garcia49379ce2021-02-24 13:48:22 +010083 ingress_whitelist_source_range: Optional[str]
84 tls_secret_name: Optional[str]
sousaedu996a5602021-05-03 00:22:43 +020085 mysql_host: Optional[str]
86 mysql_port: Optional[int]
87 mysql_root_password: Optional[str]
sousaedu0dc25b32021-08-30 16:33:33 +010088 image_pull_policy: str
sousaedu540d9372021-09-29 01:53:30 +010089 security_context: bool
David Garciaab11f842020-12-16 17:25:15 +010090
David Garcia49379ce2021-02-24 13:48:22 +010091 @validator("max_file_size")
92 def validate_max_file_size(cls, v):
93 if v < 0:
94 raise ValueError("value must be equal or greater than 0")
95 return v
96
97 @validator("site_url")
98 def validate_site_url(cls, v):
99 if v:
100 parsed = urlparse(v)
101 if not parsed.scheme.startswith("http"):
102 raise ValueError("value must start with http")
103 return v
104
105 @validator("ingress_whitelist_source_range")
106 def validate_ingress_whitelist_source_range(cls, v):
107 if v:
108 ip_network(v)
109 return v
David Garciaab11f842020-12-16 17:25:15 +0100110
sousaedu996a5602021-05-03 00:22:43 +0200111 @validator("mysql_port")
112 def validate_mysql_port(cls, v):
113 if v and (v <= 0 or v >= 65535):
114 raise ValueError("Mysql port out of range")
115 return v
116
sousaedu3ddbbd12021-08-24 19:57:24 +0100117 @validator("image_pull_policy")
118 def validate_image_pull_policy(cls, v):
119 values = {
120 "always": "Always",
121 "ifnotpresent": "IfNotPresent",
122 "never": "Never",
123 }
124 v = v.lower()
125 if v not in values.keys():
126 raise ValueError("value must be always, ifnotpresent or never")
127 return values[v]
128
David Garciaab11f842020-12-16 17:25:15 +0100129
David Garcia49379ce2021-02-24 13:48:22 +0100130class ConfigLdapModel(ModelValidator):
131 ldap_enabled: bool
132 ldap_authentication_domain_name: Optional[str]
133 ldap_url: Optional[str]
134 ldap_bind_user: Optional[str]
135 ldap_bind_password: Optional[str]
136 ldap_chase_referrals: Optional[str]
137 ldap_page_size: Optional[int]
138 ldap_user_tree_dn: Optional[str]
139 ldap_user_objectclass: Optional[str]
140 ldap_user_id_attribute: Optional[str]
141 ldap_user_name_attribute: Optional[str]
142 ldap_user_pass_attribute: Optional[str]
143 ldap_user_filter: Optional[str]
144 ldap_user_enabled_attribute: Optional[str]
145 ldap_user_enabled_mask: Optional[int]
David Garcia69bc1ab2021-05-05 16:51:40 +0200146 ldap_user_enabled_default: Optional[str]
David Garcia49379ce2021-02-24 13:48:22 +0100147 ldap_user_enabled_invert: Optional[bool]
148 ldap_group_objectclass: Optional[str]
149 ldap_group_tree_dn: Optional[str]
150 ldap_use_starttls: Optional[bool]
151 ldap_tls_cacert_base64: Optional[str]
152 ldap_tls_req_cert: Optional[str]
David Garciaab11f842020-12-16 17:25:15 +0100153
David Garcia69bc1ab2021-05-05 16:51:40 +0200154 @validator
155 def validate_ldap_user_enabled_default(cls, v):
156 if v:
157 if v not in ["true", "false"]:
158 raise ValueError('must be equal to "true" or "false"')
159 return v
160
David Garcia95ba7e12021-02-03 11:10:28 +0100161
David Garcia49379ce2021-02-24 13:48:22 +0100162class KeystoneCharm(CharmedOsmBase):
sousaedu738bf6f2020-10-10 00:25:26 +0100163 def __init__(self, *args) -> NoReturn:
David Garcia141d9352021-09-08 17:48:40 +0200164 super().__init__(
165 *args,
166 oci_image="image",
167 mysql_uri=True,
168 )
sousaedu738bf6f2020-10-10 00:25:26 +0100169 self.state.set_default(fernet_keys=None)
170 self.state.set_default(credential_keys=None)
171 self.state.set_default(keys_timestamp=0)
David Garcia009a5d62020-08-27 16:53:44 +0200172
David Garcia49379ce2021-02-24 13:48:22 +0100173 self.keystone_server = KeystoneServer(self, "keystone")
174 self.mysql_client = MysqlClient(self, "db")
175 self.framework.observe(self.on["db"].relation_changed, self.configure_pod)
176 self.framework.observe(self.on["db"].relation_broken, self.configure_pod)
David Garcia141d9352021-09-08 17:48:40 +0200177 self.framework.observe(self.on.update_status, self.configure_pod)
David Garcia009a5d62020-08-27 16:53:44 +0200178
David Garcia009a5d62020-08-27 16:53:44 +0200179 self.framework.observe(
David Garcia49379ce2021-02-24 13:48:22 +0100180 self.on["keystone"].relation_joined, self._publish_keystone_info
David Garcia009a5d62020-08-27 16:53:44 +0200181 )
182
David Garcia49379ce2021-02-24 13:48:22 +0100183 def _publish_keystone_info(self, event):
184 if self.unit.is_leader():
185 config = ConfigModel(**dict(self.config))
186 self.keystone_server.publish_info(
187 host=f"http://{self.app.name}:{PORT}/v3",
188 port=PORT,
189 user_domain_name=config.user_domain_name,
190 project_domain_name=config.project_domain_name,
191 username=config.service_username,
192 password=config.service_password,
193 service=config.service_project,
194 keystone_db_password=config.keystone_db_password,
195 region_id=config.region_id,
196 admin_username=config.admin_username,
197 admin_password=config.admin_password,
198 admin_project_name=config.admin_project,
David Garciaab11f842020-12-16 17:25:15 +0100199 )
David Garciaab11f842020-12-16 17:25:15 +0100200
David Garcia141d9352021-09-08 17:48:40 +0200201 def _check_missing_dependencies(self, config: ConfigModel, external_db: bool):
David Garcia49379ce2021-02-24 13:48:22 +0100202 missing_relations = []
David Garcia141d9352021-09-08 17:48:40 +0200203 if not external_db and self.mysql_client.is_missing_data_in_unit():
David Garcia49379ce2021-02-24 13:48:22 +0100204 missing_relations.append("mysql")
205 if missing_relations:
206 raise RelationsMissing(missing_relations)
David Garcia009a5d62020-08-27 16:53:44 +0200207
sousaedu738bf6f2020-10-10 00:25:26 +0100208 def _generate_keys(self) -> Tuple[List[str], List[str]]:
209 """Generating new fernet tokens.
David Garcia009a5d62020-08-27 16:53:44 +0200210
sousaedu738bf6f2020-10-10 00:25:26 +0100211 Returns:
212 Tuple[List[str], List[str]]: contains two lists of strings. First
213 list contains strings that represent
214 the keys for fernet and the second
215 list contains strins that represent
216 the keys for credentials.
217 """
218 fernet_keys = [
219 Fernet.generate_key().decode() for _ in range(NUMBER_FERNET_KEYS)
220 ]
221 credential_keys = [
222 Fernet.generate_key().decode() for _ in range(NUMBER_CREDENTIAL_KEYS)
223 ]
224
225 return (fernet_keys, credential_keys)
226
David Garcia49379ce2021-02-24 13:48:22 +0100227 def _get_keys(self):
228 keys_timestamp = self.state.keys_timestamp
sousaedu738bf6f2020-10-10 00:25:26 +0100229 if fernet_keys := self.state.fernet_keys:
230 fernet_keys = json.loads(fernet_keys)
231
232 if credential_keys := self.state.credential_keys:
233 credential_keys = json.loads(credential_keys)
234
235 now = datetime.now().timestamp()
David Garcia49379ce2021-02-24 13:48:22 +0100236 token_expiration = self.config["token_expiration"]
sousaedu738bf6f2020-10-10 00:25:26 +0100237
238 valid_keys = (now - keys_timestamp) < token_expiration
239 if not credential_keys or not fernet_keys or not valid_keys:
240 fernet_keys, credential_keys = self._generate_keys()
241 self.state.fernet_keys = json.dumps(fernet_keys)
242 self.state.credential_keys = json.dumps(credential_keys)
243 self.state.keys_timestamp = now
David Garcia49379ce2021-02-24 13:48:22 +0100244 return credential_keys, fernet_keys
sousaedu738bf6f2020-10-10 00:25:26 +0100245
David Garcia141d9352021-09-08 17:48:40 +0200246 def _build_files(
247 self, config: ConfigModel, credential_keys: List, fernet_keys: List
248 ):
David Garcia49379ce2021-02-24 13:48:22 +0100249 credentials_files_builder = FilesV3Builder()
250 fernet_files_builder = FilesV3Builder()
Dario Faccina24433b2023-02-01 16:56:28 +0100251 for key_id, _ in enumerate(credential_keys):
David Garcia141d9352021-09-08 17:48:40 +0200252 credentials_files_builder.add_file(str(key_id), str(key_id), secret=True)
Dario Faccina24433b2023-02-01 16:56:28 +0100253 for key_id, _ in enumerate(fernet_keys):
David Garcia141d9352021-09-08 17:48:40 +0200254 fernet_files_builder.add_file(str(key_id), str(key_id), secret=True)
David Garcia49379ce2021-02-24 13:48:22 +0100255 return credentials_files_builder.build(), fernet_files_builder.build()
256
David Garcia141d9352021-09-08 17:48:40 +0200257 def build_pod_spec(self, image_info, **kwargs):
David Garcia49379ce2021-02-24 13:48:22 +0100258 # Validate config
259 config = ConfigModel(**dict(self.config))
David Garcia141d9352021-09-08 17:48:40 +0200260 mysql_config = kwargs["mysql_config"]
David Garcia49379ce2021-02-24 13:48:22 +0100261 config_ldap = ConfigLdapModel(**dict(self.config))
sousaedu996a5602021-05-03 00:22:43 +0200262
David Garcia141d9352021-09-08 17:48:40 +0200263 if mysql_config.mysql_uri and not self.mysql_client.is_missing_data_in_unit():
sousaedu996a5602021-05-03 00:22:43 +0200264 raise Exception("Mysql data cannot be provided via config and relation")
David Garcia49379ce2021-02-24 13:48:22 +0100265 # Check relations
David Garcia141d9352021-09-08 17:48:40 +0200266 external_db = True if mysql_config.mysql_uri else False
267 self._check_missing_dependencies(config, external_db)
sousaedu996a5602021-05-03 00:22:43 +0200268
David Garcia49379ce2021-02-24 13:48:22 +0100269 # Create Builder for the PodSpec
sousaedu540d9372021-09-29 01:53:30 +0100270 pod_spec_builder = PodSpecV3Builder(
271 enable_security_context=config.security_context
272 )
sousaedu3ddbbd12021-08-24 19:57:24 +0100273 container_builder = ContainerV3Builder(
sousaedu540d9372021-09-29 01:53:30 +0100274 self.app.name,
275 image_info,
276 config.image_pull_policy,
277 run_as_non_root=config.security_context,
sousaedu3ddbbd12021-08-24 19:57:24 +0100278 )
sousaedu996a5602021-05-03 00:22:43 +0200279
David Garcia49379ce2021-02-24 13:48:22 +0100280 # Build files
David Garcia141d9352021-09-08 17:48:40 +0200281 credential_keys, fernet_keys = self._get_keys()
282 credential_files, fernet_files = self._build_files(
283 config, credential_keys, fernet_keys
284 )
285
286 # Add pod secrets
287 fernet_keys_secret_name = f"{self.app.name}-fernet-keys-secret"
288 pod_spec_builder.add_secret(
289 fernet_keys_secret_name,
290 {str(key_id): value for (key_id, value) in enumerate(fernet_keys)},
291 )
292 credential_keys_secret_name = f"{self.app.name}-credential-keys-secret"
293 pod_spec_builder.add_secret(
294 credential_keys_secret_name,
295 {str(key_id): value for (key_id, value) in enumerate(credential_keys)},
296 )
297 mysql_secret_name = f"{self.app.name}-mysql-secret"
298
299 pod_spec_builder.add_secret(
300 mysql_secret_name,
301 {
302 "host": mysql_config.host,
303 "port": str(mysql_config.port),
304 "user": mysql_config.username,
305 "password": mysql_config.password,
306 }
307 if mysql_config.mysql_uri
308 else {
309 "host": self.mysql_client.host,
310 "port": str(self.mysql_client.port),
311 "user": "root",
312 "password": self.mysql_client.root_password,
313 },
314 )
315 keystone_secret_name = f"{self.app.name}-keystone-secret"
316 pod_spec_builder.add_secret(
317 keystone_secret_name,
318 {
319 "db_password": config.keystone_db_password,
320 "admin_username": config.admin_username,
321 "admin_password": config.admin_password,
322 "admin_project": config.admin_project,
323 "service_username": config.service_username,
324 "service_password": config.service_password,
325 "service_project": config.service_project,
326 },
327 )
328 # Build Container
David Garcia49379ce2021-02-24 13:48:22 +0100329 container_builder.add_volume_config(
David Garcia141d9352021-09-08 17:48:40 +0200330 "credential-keys",
331 CREDENTIAL_KEYS_PATH,
332 credential_files,
333 secret_name=credential_keys_secret_name,
David Garcia49379ce2021-02-24 13:48:22 +0100334 )
335 container_builder.add_volume_config(
David Garcia141d9352021-09-08 17:48:40 +0200336 "fernet-keys",
337 FERNET_KEYS_PATH,
338 fernet_files,
339 secret_name=fernet_keys_secret_name,
David Garcia49379ce2021-02-24 13:48:22 +0100340 )
David Garcia141d9352021-09-08 17:48:40 +0200341 container_builder.add_port(name=self.app.name, port=PORT)
David Garcia49379ce2021-02-24 13:48:22 +0100342 container_builder.add_envs(
343 {
David Garcia49379ce2021-02-24 13:48:22 +0100344 "REGION_ID": config.region_id,
345 "KEYSTONE_HOST": self.app.name,
David Garcia49379ce2021-02-24 13:48:22 +0100346 }
347 )
David Garcia141d9352021-09-08 17:48:40 +0200348 container_builder.add_secret_envs(
349 secret_name=mysql_secret_name,
350 envs={
351 "DB_HOST": "host",
352 "DB_PORT": "port",
353 "ROOT_DB_USER": "user",
354 "ROOT_DB_PASSWORD": "password",
355 },
356 )
357 container_builder.add_secret_envs(
358 secret_name=keystone_secret_name,
359 envs={
360 "KEYSTONE_DB_PASSWORD": "db_password",
361 "ADMIN_USERNAME": "admin_username",
362 "ADMIN_PASSWORD": "admin_password",
363 "ADMIN_PROJECT": "admin_project",
364 "SERVICE_USERNAME": "service_username",
365 "SERVICE_PASSWORD": "service_password",
366 "SERVICE_PROJECT": "service_project",
367 },
368 )
369 ldap_secret_name = f"{self.app.name}-ldap-secret"
David Garcia49379ce2021-02-24 13:48:22 +0100370 if config_ldap.ldap_enabled:
David Garcia141d9352021-09-08 17:48:40 +0200371 # Add ldap secrets and envs
372 ldap_secrets = {
373 "authentication_domain_name": config_ldap.ldap_authentication_domain_name,
374 "url": config_ldap.ldap_url,
sousaedua8b30892021-10-07 14:18:51 +0100375 "page_size": str(config_ldap.ldap_page_size),
David Garcia141d9352021-09-08 17:48:40 +0200376 "user_objectclass": config_ldap.ldap_user_objectclass,
377 "user_id_attribute": config_ldap.ldap_user_id_attribute,
378 "user_name_attribute": config_ldap.ldap_user_name_attribute,
379 "user_pass_attribute": config_ldap.ldap_user_pass_attribute,
sousaedua8b30892021-10-07 14:18:51 +0100380 "user_enabled_mask": str(config_ldap.ldap_user_enabled_mask),
David Garcia141d9352021-09-08 17:48:40 +0200381 "user_enabled_default": config_ldap.ldap_user_enabled_default,
sousaedu158ca802021-10-08 06:52:29 +0100382 "user_enabled_invert": str(config_ldap.ldap_user_enabled_invert),
David Garcia141d9352021-09-08 17:48:40 +0200383 "group_objectclass": config_ldap.ldap_group_objectclass,
384 }
385 ldap_envs = {
386 "LDAP_AUTHENTICATION_DOMAIN_NAME": "authentication_domain_name",
387 "LDAP_URL": "url",
388 "LDAP_PAGE_SIZE": "page_size",
389 "LDAP_USER_OBJECTCLASS": "user_objectclass",
390 "LDAP_USER_ID_ATTRIBUTE": "user_id_attribute",
391 "LDAP_USER_NAME_ATTRIBUTE": "user_name_attribute",
392 "LDAP_USER_PASS_ATTRIBUTE": "user_pass_attribute",
393 "LDAP_USER_ENABLED_MASK": "user_enabled_mask",
394 "LDAP_USER_ENABLED_DEFAULT": "user_enabled_default",
395 "LDAP_USER_ENABLED_INVERT": "user_enabled_invert",
396 "LDAP_GROUP_OBJECTCLASS": "group_objectclass",
397 }
David Garcia49379ce2021-02-24 13:48:22 +0100398 if config_ldap.ldap_bind_user:
David Garcia141d9352021-09-08 17:48:40 +0200399 ldap_secrets["bind_user"] = config_ldap.ldap_bind_user
400 ldap_envs["LDAP_BIND_USER"] = "bind_user"
sousaedu738bf6f2020-10-10 00:25:26 +0100401
David Garcia49379ce2021-02-24 13:48:22 +0100402 if config_ldap.ldap_bind_password:
David Garcia141d9352021-09-08 17:48:40 +0200403 ldap_secrets["bind_password"] = config_ldap.ldap_bind_password
404 ldap_envs["LDAP_BIND_PASSWORD"] = "bind_password"
sousaedu738bf6f2020-10-10 00:25:26 +0100405
David Garcia49379ce2021-02-24 13:48:22 +0100406 if config_ldap.ldap_user_tree_dn:
David Garcia141d9352021-09-08 17:48:40 +0200407 ldap_secrets["user_tree_dn"] = config_ldap.ldap_user_tree_dn
408 ldap_envs["LDAP_USER_TREE_DN"] = "user_tree_dn"
David Garcia49379ce2021-02-24 13:48:22 +0100409
410 if config_ldap.ldap_user_filter:
David Garcia141d9352021-09-08 17:48:40 +0200411 ldap_secrets["user_filter"] = config_ldap.ldap_user_filter
412 ldap_envs["LDAP_USER_FILTER"] = "user_filter"
David Garcia49379ce2021-02-24 13:48:22 +0100413
414 if config_ldap.ldap_user_enabled_attribute:
David Garcia141d9352021-09-08 17:48:40 +0200415 ldap_secrets[
416 "user_enabled_attribute"
417 ] = config_ldap.ldap_user_enabled_attribute
418 ldap_envs["LDAP_USER_ENABLED_ATTRIBUTE"] = "user_enabled_attribute"
David Garcia49379ce2021-02-24 13:48:22 +0100419 if config_ldap.ldap_chase_referrals:
David Garcia141d9352021-09-08 17:48:40 +0200420 ldap_secrets["chase_referrals"] = config_ldap.ldap_chase_referrals
421 ldap_envs["LDAP_CHASE_REFERRALS"] = "chase_referrals"
David Garcia49379ce2021-02-24 13:48:22 +0100422
423 if config_ldap.ldap_group_tree_dn:
David Garcia141d9352021-09-08 17:48:40 +0200424 ldap_secrets["group_tree_dn"] = config_ldap.ldap_group_tree_dn
425 ldap_envs["LDAP_GROUP_TREE_DN"] = "group_tree_dn"
David Garcia49379ce2021-02-24 13:48:22 +0100426
sousaedu8f0f66f2021-06-17 12:43:59 +0100427 if config_ldap.ldap_tls_cacert_base64:
David Garcia141d9352021-09-08 17:48:40 +0200428 ldap_secrets["tls_cacert_base64"] = config_ldap.ldap_tls_cacert_base64
429 ldap_envs["LDAP_TLS_CACERT_BASE64"] = "tls_cacert_base64"
sousaedu8f0f66f2021-06-17 12:43:59 +0100430
David Garcia49379ce2021-02-24 13:48:22 +0100431 if config_ldap.ldap_use_starttls:
sousaedu158ca802021-10-08 06:52:29 +0100432 ldap_secrets["use_starttls"] = str(config_ldap.ldap_use_starttls)
David Garcia141d9352021-09-08 17:48:40 +0200433 ldap_secrets["tls_cacert_base64"] = config_ldap.ldap_tls_cacert_base64
434 ldap_secrets["tls_req_cert"] = config_ldap.ldap_tls_req_cert
435 ldap_envs["LDAP_USE_STARTTLS"] = "use_starttls"
436 ldap_envs["LDAP_TLS_CACERT_BASE64"] = "tls_cacert_base64"
437 ldap_envs["LDAP_TLS_REQ_CERT"] = "tls_req_cert"
438
439 pod_spec_builder.add_secret(
440 ldap_secret_name,
441 ldap_secrets,
442 )
443 container_builder.add_secret_envs(
444 secret_name=ldap_secret_name,
445 envs=ldap_envs,
446 )
447
David Garcia49379ce2021-02-24 13:48:22 +0100448 container = container_builder.build()
sousaedu996a5602021-05-03 00:22:43 +0200449
David Garcia49379ce2021-02-24 13:48:22 +0100450 # Add container to pod spec
451 pod_spec_builder.add_container(container)
sousaedu996a5602021-05-03 00:22:43 +0200452
David Garcia141d9352021-09-08 17:48:40 +0200453 # Add Pod Restart Policy
454 restart_policy = PodRestartPolicy()
455 restart_policy.add_secrets(
456 secret_names=(mysql_secret_name, keystone_secret_name, ldap_secret_name)
457 )
458 pod_spec_builder.set_restart_policy(restart_policy)
459
David Garcia49379ce2021-02-24 13:48:22 +0100460 # Add ingress resources to pod spec if site url exists
461 if config.site_url:
462 parsed = urlparse(config.site_url)
463 annotations = {
464 "nginx.ingress.kubernetes.io/proxy-body-size": "{}".format(
465 str(config.max_file_size) + "m"
466 if config.max_file_size > 0
467 else config.max_file_size
David Garciad68e0b42021-06-28 16:50:42 +0200468 )
David Garcia49379ce2021-02-24 13:48:22 +0100469 }
David Garciad68e0b42021-06-28 16:50:42 +0200470 if config.ingress_class:
471 annotations["kubernetes.io/ingress.class"] = config.ingress_class
David Garcia49379ce2021-02-24 13:48:22 +0100472 ingress_resource_builder = IngressResourceV3Builder(
473 f"{self.app.name}-ingress", annotations
474 )
475
476 if config.ingress_whitelist_source_range:
477 annotations[
478 "nginx.ingress.kubernetes.io/whitelist-source-range"
479 ] = config.ingress_whitelist_source_range
480
481 if parsed.scheme == "https":
482 ingress_resource_builder.add_tls(
483 [parsed.hostname], config.tls_secret_name
484 )
485 else:
486 annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
487
488 ingress_resource_builder.add_rule(parsed.hostname, self.app.name, PORT)
489 ingress_resource = ingress_resource_builder.build()
490 pod_spec_builder.add_ingress_resource(ingress_resource)
491 return pod_spec_builder.build()
David Garcia009a5d62020-08-27 16:53:44 +0200492
493
494if __name__ == "__main__":
495 main(KeystoneCharm)
David Garcia49379ce2021-02-24 13:48:22 +0100496
497# LOGGER = logging.getLogger(__name__)
498
499
500# class ConfigurePodEvent(EventBase):
501# """Configure Pod event"""
502
503# pass
504
505
506# class KeystoneEvents(CharmEvents):
507# """Keystone Events"""
508
509# configure_pod = EventSource(ConfigurePodEvent)
510
511# class KeystoneCharm(CharmBase):
512# """Keystone K8s Charm"""
513
514# state = StoredState()
515# on = KeystoneEvents()
516
517# def __init__(self, *args) -> NoReturn:
518# """Constructor of the Charm object.
519# Initializes internal state and register events it can handle.
520# """
521# super().__init__(*args)
522# self.state.set_default(db_host=None)
523# self.state.set_default(db_port=None)
524# self.state.set_default(db_user=None)
525# self.state.set_default(db_password=None)
526# self.state.set_default(pod_spec=None)
527# self.state.set_default(fernet_keys=None)
528# self.state.set_default(credential_keys=None)
529# self.state.set_default(keys_timestamp=0)
530
531# # Register all of the events we want to observe
532# self.framework.observe(self.on.config_changed, self.configure_pod)
533# self.framework.observe(self.on.start, self.configure_pod)
534# self.framework.observe(self.on.upgrade_charm, self.configure_pod)
535# self.framework.observe(self.on.leader_elected, self.configure_pod)
536# self.framework.observe(self.on.update_status, self.configure_pod)
537
538# # Registering custom internal events
539# self.framework.observe(self.on.configure_pod, self.configure_pod)
540
541# # Register relation events
542# self.framework.observe(
543# self.on.db_relation_changed, self._on_db_relation_changed
544# )
545# self.framework.observe(
546# self.on.db_relation_broken, self._on_db_relation_broken
547# )
548# self.framework.observe(
549# self.on.keystone_relation_joined, self._publish_keystone_info
550# )
551
552# def _publish_keystone_info(self, event: EventBase) -> NoReturn:
553# """Publishes keystone information for NBI usage through the keystone
554# relation.
555
556# Args:
557# event (EventBase): Keystone relation event to update NBI.
558# """
559# config = self.model.config
560# rel_data = {
561# "host": f"http://{self.app.name}:{KEYSTONE_PORT}/v3",
562# "port": str(KEYSTONE_PORT),
563# "keystone_db_password": config["keystone_db_password"],
564# "region_id": config["region_id"],
565# "user_domain_name": config["user_domain_name"],
566# "project_domain_name": config["project_domain_name"],
567# "admin_username": config["admin_username"],
568# "admin_password": config["admin_password"],
569# "admin_project_name": config["admin_project"],
570# "username": config["service_username"],
571# "password": config["service_password"],
572# "service": config["service_project"],
573# }
574# for k, v in rel_data.items():
575# event.relation.data[self.model.unit][k] = v
576
577# def _on_db_relation_changed(self, event: EventBase) -> NoReturn:
578# """Reads information about the DB relation, in order for keystone to
579# access it.
580
581# Args:
582# event (EventBase): DB relation event to access database
583# information.
584# """
585# if not event.unit in event.relation.data:
586# return
587# relation_data = event.relation.data[event.unit]
588# db_host = relation_data.get("host")
589# db_port = int(relation_data.get("port", 3306))
590# db_user = "root"
591# db_password = relation_data.get("root_password")
592
593# if (
594# db_host
595# and db_port
596# and db_user
597# and db_password
598# and (
599# self.state.db_host != db_host
600# or self.state.db_port != db_port
601# or self.state.db_user != db_user
602# or self.state.db_password != db_password
603# )
604# ):
605# self.state.db_host = db_host
606# self.state.db_port = db_port
607# self.state.db_user = db_user
608# self.state.db_password = db_password
609# self.on.configure_pod.emit()
610
611
612# def _on_db_relation_broken(self, event: EventBase) -> NoReturn:
613# """Clears data from db relation.
614
615# Args:
616# event (EventBase): DB relation event.
617
618# """
619# self.state.db_host = None
620# self.state.db_port = None
621# self.state.db_user = None
622# self.state.db_password = None
623# self.on.configure_pod.emit()
624
625# def _check_settings(self) -> str:
626# """Check if there any settings missing from Keystone configuration.
627
628# Returns:
629# str: Information about the problems found (if any).
630# """
631# problems = []
632# config = self.model.config
633
634# for setting in REQUIRED_SETTINGS:
635# if not config.get(setting):
636# problem = f"missing config {setting}"
637# problems.append(problem)
638
639# return ";".join(problems)
640
641# def _make_pod_image_details(self) -> Dict[str, str]:
642# """Generate the pod image details.
643
644# Returns:
645# Dict[str, str]: pod image details.
646# """
647# config = self.model.config
648# image_details = {
649# "imagePath": config["image"],
650# }
651# if config["image_username"]:
652# image_details.update(
653# {
654# "username": config["image_username"],
655# "password": config["image_password"],
656# }
657# )
658# return image_details
659
660# def _make_pod_ports(self) -> List[Dict[str, Any]]:
661# """Generate the pod ports details.
662
663# Returns:
664# List[Dict[str, Any]]: pod ports details.
665# """
666# return [
667# {"name": "keystone", "containerPort": KEYSTONE_PORT, "protocol": "TCP"},
668# ]
669
670# def _make_pod_envconfig(self) -> Dict[str, Any]:
671# """Generate pod environment configuraiton.
672
673# Returns:
674# Dict[str, Any]: pod environment configuration.
675# """
676# config = self.model.config
677
678# envconfig = {
679# "DB_HOST": self.state.db_host,
680# "DB_PORT": self.state.db_port,
681# "ROOT_DB_USER": self.state.db_user,
682# "ROOT_DB_PASSWORD": self.state.db_password,
683# "KEYSTONE_DB_PASSWORD": config["keystone_db_password"],
684# "REGION_ID": config["region_id"],
685# "KEYSTONE_HOST": self.app.name,
686# "ADMIN_USERNAME": config["admin_username"],
687# "ADMIN_PASSWORD": config["admin_password"],
688# "ADMIN_PROJECT": config["admin_project"],
689# "SERVICE_USERNAME": config["service_username"],
690# "SERVICE_PASSWORD": config["service_password"],
691# "SERVICE_PROJECT": config["service_project"],
692# }
693
694# if config.get("ldap_enabled"):
695# envconfig["LDAP_AUTHENTICATION_DOMAIN_NAME"] = config[
696# "ldap_authentication_domain_name"
697# ]
698# envconfig["LDAP_URL"] = config["ldap_url"]
699# envconfig["LDAP_PAGE_SIZE"] = config["ldap_page_size"]
700# envconfig["LDAP_USER_OBJECTCLASS"] = config["ldap_user_objectclass"]
701# envconfig["LDAP_USER_ID_ATTRIBUTE"] = config["ldap_user_id_attribute"]
702# envconfig["LDAP_USER_NAME_ATTRIBUTE"] = config["ldap_user_name_attribute"]
703# envconfig["LDAP_USER_PASS_ATTRIBUTE"] = config["ldap_user_pass_attribute"]
704# envconfig["LDAP_USER_ENABLED_MASK"] = config["ldap_user_enabled_mask"]
705# envconfig["LDAP_USER_ENABLED_DEFAULT"] = config["ldap_user_enabled_default"]
706# envconfig["LDAP_USER_ENABLED_INVERT"] = config["ldap_user_enabled_invert"]
707# envconfig["LDAP_GROUP_OBJECTCLASS"] = config["ldap_group_objectclass"]
708
709# if config["ldap_bind_user"]:
710# envconfig["LDAP_BIND_USER"] = config["ldap_bind_user"]
711
712# if config["ldap_bind_password"]:
713# envconfig["LDAP_BIND_PASSWORD"] = config["ldap_bind_password"]
714
715# if config["ldap_user_tree_dn"]:
716# envconfig["LDAP_USER_TREE_DN"] = config["ldap_user_tree_dn"]
717
718# if config["ldap_user_filter"]:
719# envconfig["LDAP_USER_FILTER"] = config["ldap_user_filter"]
720
721# if config["ldap_user_enabled_attribute"]:
722# envconfig["LDAP_USER_ENABLED_ATTRIBUTE"] = config[
723# "ldap_user_enabled_attribute"
724# ]
725
726# if config["ldap_chase_referrals"]:
727# envconfig["LDAP_CHASE_REFERRALS"] = config["ldap_chase_referrals"]
728
729# if config["ldap_group_tree_dn"]:
730# envconfig["LDAP_GROUP_TREE_DN"] = config["ldap_group_tree_dn"]
731
732# if config["ldap_use_starttls"]:
733# envconfig["LDAP_USE_STARTTLS"] = config["ldap_use_starttls"]
734# envconfig["LDAP_TLS_CACERT_BASE64"] = config["ldap_tls_cacert_base64"]
735# envconfig["LDAP_TLS_REQ_CERT"] = config["ldap_tls_req_cert"]
736
737# return envconfig
738
739# def _make_pod_ingress_resources(self) -> List[Dict[str, Any]]:
740# """Generate pod ingress resources.
741
742# Returns:
743# List[Dict[str, Any]]: pod ingress resources.
744# """
745# site_url = self.model.config["site_url"]
746
747# if not site_url:
748# return
749
750# parsed = urlparse(site_url)
751
752# if not parsed.scheme.startswith("http"):
753# return
754
755# max_file_size = self.model.config["max_file_size"]
756# ingress_whitelist_source_range = self.model.config[
757# "ingress_whitelist_source_range"
758# ]
759
760# annotations = {
761# "nginx.ingress.kubernetes.io/proxy-body-size": "{}m".format(max_file_size)
762# }
763
764# if ingress_whitelist_source_range:
765# annotations[
766# "nginx.ingress.kubernetes.io/whitelist-source-range"
767# ] = ingress_whitelist_source_range
768
769# ingress_spec_tls = None
770
771# if parsed.scheme == "https":
772# ingress_spec_tls = [{"hosts": [parsed.hostname]}]
773# tls_secret_name = self.model.config["tls_secret_name"]
774# if tls_secret_name:
775# ingress_spec_tls[0]["secretName"] = tls_secret_name
776# else:
777# annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
778
779# ingress = {
780# "name": "{}-ingress".format(self.app.name),
781# "annotations": annotations,
782# "spec": {
783# "rules": [
784# {
785# "host": parsed.hostname,
786# "http": {
787# "paths": [
788# {
789# "path": "/",
790# "backend": {
791# "serviceName": self.app.name,
792# "servicePort": KEYSTONE_PORT,
793# },
794# }
795# ]
796# },
797# }
798# ],
799# },
800# }
801# if ingress_spec_tls:
802# ingress["spec"]["tls"] = ingress_spec_tls
803
804# return [ingress]
805
806# def _generate_keys(self) -> Tuple[List[str], List[str]]:
807# """Generating new fernet tokens.
808
809# Returns:
810# Tuple[List[str], List[str]]: contains two lists of strings. First
811# list contains strings that represent
812# the keys for fernet and the second
813# list contains strins that represent
814# the keys for credentials.
815# """
816# fernet_keys = [
817# Fernet.generate_key().decode() for _ in range(NUMBER_FERNET_KEYS)
818# ]
819# credential_keys = [
820# Fernet.generate_key().decode() for _ in range(NUMBER_CREDENTIAL_KEYS)
821# ]
822
823# return (fernet_keys, credential_keys)
824
825# def configure_pod(self, event: EventBase) -> NoReturn:
826# """Assemble the pod spec and apply it, if possible.
827
828# Args:
829# event (EventBase): Hook or Relation event that started the
830# function.
831# """
832# if not self.state.db_host:
833# self.unit.status = WaitingStatus("Waiting for database relation")
834# event.defer()
835# return
836
837# if not self.unit.is_leader():
838# self.unit.status = ActiveStatus("ready")
839# return
840
841# if fernet_keys := self.state.fernet_keys:
842# fernet_keys = json.loads(fernet_keys)
843
844# if credential_keys := self.state.credential_keys:
845# credential_keys = json.loads(credential_keys)
846
847# now = datetime.now().timestamp()
848# keys_timestamp = self.state.keys_timestamp
849# token_expiration = self.model.config["token_expiration"]
850
851# valid_keys = (now - keys_timestamp) < token_expiration
852# if not credential_keys or not fernet_keys or not valid_keys:
853# fernet_keys, credential_keys = self._generate_keys()
854# self.state.fernet_keys = json.dumps(fernet_keys)
855# self.state.credential_keys = json.dumps(credential_keys)
856# self.state.keys_timestamp = now
857
858# # Check problems in the settings
859# problems = self._check_settings()
860# if problems:
861# self.unit.status = BlockedStatus(problems)
862# return
863
864# self.unit.status = BlockedStatus("Assembling pod spec")
865# image_details = self._make_pod_image_details()
866# ports = self._make_pod_ports()
867# env_config = self._make_pod_envconfig()
868# ingress_resources = self._make_pod_ingress_resources()
869# files = self._make_pod_files(fernet_keys, credential_keys)
870
871# pod_spec = {
872# "version": 3,
873# "containers": [
874# {
875# "name": self.framework.model.app.name,
876# "imageDetails": image_details,
877# "ports": ports,
878# "envConfig": env_config,
879# "volumeConfig": files,
880# }
881# ],
882# "kubernetesResources": {"ingressResources": ingress_resources or []},
883# }
884
885# if self.state.pod_spec != (
886# pod_spec_json := json.dumps(pod_spec, sort_keys=True)
887# ):
888# self.state.pod_spec = pod_spec_json
889# self.model.pod.set_spec(pod_spec)
890
891# self.unit.status = ActiveStatus("ready")
892
893
894# if __name__ == "__main__":
895# main(KeystoneCharm)