blob: 808af3bef2c72b6d0303b24e9c82dd73d2309b1a [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
David Garciaab11f842020-12-16 17:25:15 +010089
David Garcia49379ce2021-02-24 13:48:22 +010090 @validator("max_file_size")
91 def validate_max_file_size(cls, v):
92 if v < 0:
93 raise ValueError("value must be equal or greater than 0")
94 return v
95
96 @validator("site_url")
97 def validate_site_url(cls, v):
98 if v:
99 parsed = urlparse(v)
100 if not parsed.scheme.startswith("http"):
101 raise ValueError("value must start with http")
102 return v
103
104 @validator("ingress_whitelist_source_range")
105 def validate_ingress_whitelist_source_range(cls, v):
106 if v:
107 ip_network(v)
108 return v
David Garciaab11f842020-12-16 17:25:15 +0100109
sousaedu996a5602021-05-03 00:22:43 +0200110 @validator("mysql_port")
111 def validate_mysql_port(cls, v):
112 if v and (v <= 0 or v >= 65535):
113 raise ValueError("Mysql port out of range")
114 return v
115
sousaedu3ddbbd12021-08-24 19:57:24 +0100116 @validator("image_pull_policy")
117 def validate_image_pull_policy(cls, v):
118 values = {
119 "always": "Always",
120 "ifnotpresent": "IfNotPresent",
121 "never": "Never",
122 }
123 v = v.lower()
124 if v not in values.keys():
125 raise ValueError("value must be always, ifnotpresent or never")
126 return values[v]
127
David Garciaab11f842020-12-16 17:25:15 +0100128
David Garcia49379ce2021-02-24 13:48:22 +0100129class ConfigLdapModel(ModelValidator):
130 ldap_enabled: bool
131 ldap_authentication_domain_name: Optional[str]
132 ldap_url: Optional[str]
133 ldap_bind_user: Optional[str]
134 ldap_bind_password: Optional[str]
135 ldap_chase_referrals: Optional[str]
136 ldap_page_size: Optional[int]
137 ldap_user_tree_dn: Optional[str]
138 ldap_user_objectclass: Optional[str]
139 ldap_user_id_attribute: Optional[str]
140 ldap_user_name_attribute: Optional[str]
141 ldap_user_pass_attribute: Optional[str]
142 ldap_user_filter: Optional[str]
143 ldap_user_enabled_attribute: Optional[str]
144 ldap_user_enabled_mask: Optional[int]
David Garcia69bc1ab2021-05-05 16:51:40 +0200145 ldap_user_enabled_default: Optional[str]
David Garcia49379ce2021-02-24 13:48:22 +0100146 ldap_user_enabled_invert: Optional[bool]
147 ldap_group_objectclass: Optional[str]
148 ldap_group_tree_dn: Optional[str]
149 ldap_use_starttls: Optional[bool]
150 ldap_tls_cacert_base64: Optional[str]
151 ldap_tls_req_cert: Optional[str]
David Garciaab11f842020-12-16 17:25:15 +0100152
David Garcia69bc1ab2021-05-05 16:51:40 +0200153 @validator
154 def validate_ldap_user_enabled_default(cls, v):
155 if v:
156 if v not in ["true", "false"]:
157 raise ValueError('must be equal to "true" or "false"')
158 return v
159
David Garcia95ba7e12021-02-03 11:10:28 +0100160
David Garcia49379ce2021-02-24 13:48:22 +0100161class KeystoneCharm(CharmedOsmBase):
sousaedu738bf6f2020-10-10 00:25:26 +0100162 def __init__(self, *args) -> NoReturn:
David Garcia141d9352021-09-08 17:48:40 +0200163 super().__init__(
164 *args,
165 oci_image="image",
166 mysql_uri=True,
167 )
sousaedu738bf6f2020-10-10 00:25:26 +0100168 self.state.set_default(fernet_keys=None)
169 self.state.set_default(credential_keys=None)
170 self.state.set_default(keys_timestamp=0)
David Garcia009a5d62020-08-27 16:53:44 +0200171
David Garcia49379ce2021-02-24 13:48:22 +0100172 self.keystone_server = KeystoneServer(self, "keystone")
173 self.mysql_client = MysqlClient(self, "db")
174 self.framework.observe(self.on["db"].relation_changed, self.configure_pod)
175 self.framework.observe(self.on["db"].relation_broken, self.configure_pod)
David Garcia141d9352021-09-08 17:48:40 +0200176 self.framework.observe(self.on.update_status, self.configure_pod)
David Garcia009a5d62020-08-27 16:53:44 +0200177
David Garcia009a5d62020-08-27 16:53:44 +0200178 self.framework.observe(
David Garcia49379ce2021-02-24 13:48:22 +0100179 self.on["keystone"].relation_joined, self._publish_keystone_info
David Garcia009a5d62020-08-27 16:53:44 +0200180 )
181
David Garcia49379ce2021-02-24 13:48:22 +0100182 def _publish_keystone_info(self, event):
183 if self.unit.is_leader():
184 config = ConfigModel(**dict(self.config))
185 self.keystone_server.publish_info(
186 host=f"http://{self.app.name}:{PORT}/v3",
187 port=PORT,
188 user_domain_name=config.user_domain_name,
189 project_domain_name=config.project_domain_name,
190 username=config.service_username,
191 password=config.service_password,
192 service=config.service_project,
193 keystone_db_password=config.keystone_db_password,
194 region_id=config.region_id,
195 admin_username=config.admin_username,
196 admin_password=config.admin_password,
197 admin_project_name=config.admin_project,
David Garciaab11f842020-12-16 17:25:15 +0100198 )
David Garciaab11f842020-12-16 17:25:15 +0100199
David Garcia141d9352021-09-08 17:48:40 +0200200 def _check_missing_dependencies(self, config: ConfigModel, external_db: bool):
David Garcia49379ce2021-02-24 13:48:22 +0100201 missing_relations = []
David Garcia141d9352021-09-08 17:48:40 +0200202 if not external_db and self.mysql_client.is_missing_data_in_unit():
David Garcia49379ce2021-02-24 13:48:22 +0100203 missing_relations.append("mysql")
204 if missing_relations:
205 raise RelationsMissing(missing_relations)
David Garcia009a5d62020-08-27 16:53:44 +0200206
sousaedu738bf6f2020-10-10 00:25:26 +0100207 def _generate_keys(self) -> Tuple[List[str], List[str]]:
208 """Generating new fernet tokens.
David Garcia009a5d62020-08-27 16:53:44 +0200209
sousaedu738bf6f2020-10-10 00:25:26 +0100210 Returns:
211 Tuple[List[str], List[str]]: contains two lists of strings. First
212 list contains strings that represent
213 the keys for fernet and the second
214 list contains strins that represent
215 the keys for credentials.
216 """
217 fernet_keys = [
218 Fernet.generate_key().decode() for _ in range(NUMBER_FERNET_KEYS)
219 ]
220 credential_keys = [
221 Fernet.generate_key().decode() for _ in range(NUMBER_CREDENTIAL_KEYS)
222 ]
223
224 return (fernet_keys, credential_keys)
225
David Garcia49379ce2021-02-24 13:48:22 +0100226 def _get_keys(self):
227 keys_timestamp = self.state.keys_timestamp
sousaedu738bf6f2020-10-10 00:25:26 +0100228 if fernet_keys := self.state.fernet_keys:
229 fernet_keys = json.loads(fernet_keys)
230
231 if credential_keys := self.state.credential_keys:
232 credential_keys = json.loads(credential_keys)
233
234 now = datetime.now().timestamp()
David Garcia49379ce2021-02-24 13:48:22 +0100235 token_expiration = self.config["token_expiration"]
sousaedu738bf6f2020-10-10 00:25:26 +0100236
237 valid_keys = (now - keys_timestamp) < token_expiration
238 if not credential_keys or not fernet_keys or not valid_keys:
239 fernet_keys, credential_keys = self._generate_keys()
240 self.state.fernet_keys = json.dumps(fernet_keys)
241 self.state.credential_keys = json.dumps(credential_keys)
242 self.state.keys_timestamp = now
David Garcia49379ce2021-02-24 13:48:22 +0100243 return credential_keys, fernet_keys
sousaedu738bf6f2020-10-10 00:25:26 +0100244
David Garcia141d9352021-09-08 17:48:40 +0200245 def _build_files(
246 self, config: ConfigModel, credential_keys: List, fernet_keys: List
247 ):
David Garcia49379ce2021-02-24 13:48:22 +0100248 credentials_files_builder = FilesV3Builder()
249 fernet_files_builder = FilesV3Builder()
David Garcia141d9352021-09-08 17:48:40 +0200250 for (key_id, _) in enumerate(credential_keys):
251 credentials_files_builder.add_file(str(key_id), str(key_id), secret=True)
252 for (key_id, _) in enumerate(fernet_keys):
253 fernet_files_builder.add_file(str(key_id), str(key_id), secret=True)
David Garcia49379ce2021-02-24 13:48:22 +0100254 return credentials_files_builder.build(), fernet_files_builder.build()
255
David Garcia141d9352021-09-08 17:48:40 +0200256 def build_pod_spec(self, image_info, **kwargs):
David Garcia49379ce2021-02-24 13:48:22 +0100257 # Validate config
258 config = ConfigModel(**dict(self.config))
David Garcia141d9352021-09-08 17:48:40 +0200259 mysql_config = kwargs["mysql_config"]
David Garcia49379ce2021-02-24 13:48:22 +0100260 config_ldap = ConfigLdapModel(**dict(self.config))
sousaedu996a5602021-05-03 00:22:43 +0200261
David Garcia141d9352021-09-08 17:48:40 +0200262 if mysql_config.mysql_uri and not self.mysql_client.is_missing_data_in_unit():
sousaedu996a5602021-05-03 00:22:43 +0200263 raise Exception("Mysql data cannot be provided via config and relation")
David Garcia49379ce2021-02-24 13:48:22 +0100264 # Check relations
David Garcia141d9352021-09-08 17:48:40 +0200265 external_db = True if mysql_config.mysql_uri else False
266 self._check_missing_dependencies(config, external_db)
sousaedu996a5602021-05-03 00:22:43 +0200267
David Garcia49379ce2021-02-24 13:48:22 +0100268 # Create Builder for the PodSpec
269 pod_spec_builder = PodSpecV3Builder()
sousaedu3ddbbd12021-08-24 19:57:24 +0100270 container_builder = ContainerV3Builder(
271 self.app.name, image_info, config.image_pull_policy
272 )
sousaedu996a5602021-05-03 00:22:43 +0200273
David Garcia49379ce2021-02-24 13:48:22 +0100274 # Build files
David Garcia141d9352021-09-08 17:48:40 +0200275 credential_keys, fernet_keys = self._get_keys()
276 credential_files, fernet_files = self._build_files(
277 config, credential_keys, fernet_keys
278 )
279
280 # Add pod secrets
281 fernet_keys_secret_name = f"{self.app.name}-fernet-keys-secret"
282 pod_spec_builder.add_secret(
283 fernet_keys_secret_name,
284 {str(key_id): value for (key_id, value) in enumerate(fernet_keys)},
285 )
286 credential_keys_secret_name = f"{self.app.name}-credential-keys-secret"
287 pod_spec_builder.add_secret(
288 credential_keys_secret_name,
289 {str(key_id): value for (key_id, value) in enumerate(credential_keys)},
290 )
291 mysql_secret_name = f"{self.app.name}-mysql-secret"
292
293 pod_spec_builder.add_secret(
294 mysql_secret_name,
295 {
296 "host": mysql_config.host,
297 "port": str(mysql_config.port),
298 "user": mysql_config.username,
299 "password": mysql_config.password,
300 }
301 if mysql_config.mysql_uri
302 else {
303 "host": self.mysql_client.host,
304 "port": str(self.mysql_client.port),
305 "user": "root",
306 "password": self.mysql_client.root_password,
307 },
308 )
309 keystone_secret_name = f"{self.app.name}-keystone-secret"
310 pod_spec_builder.add_secret(
311 keystone_secret_name,
312 {
313 "db_password": config.keystone_db_password,
314 "admin_username": config.admin_username,
315 "admin_password": config.admin_password,
316 "admin_project": config.admin_project,
317 "service_username": config.service_username,
318 "service_password": config.service_password,
319 "service_project": config.service_project,
320 },
321 )
322 # Build Container
David Garcia49379ce2021-02-24 13:48:22 +0100323 container_builder.add_volume_config(
David Garcia141d9352021-09-08 17:48:40 +0200324 "credential-keys",
325 CREDENTIAL_KEYS_PATH,
326 credential_files,
327 secret_name=credential_keys_secret_name,
David Garcia49379ce2021-02-24 13:48:22 +0100328 )
329 container_builder.add_volume_config(
David Garcia141d9352021-09-08 17:48:40 +0200330 "fernet-keys",
331 FERNET_KEYS_PATH,
332 fernet_files,
333 secret_name=fernet_keys_secret_name,
David Garcia49379ce2021-02-24 13:48:22 +0100334 )
David Garcia141d9352021-09-08 17:48:40 +0200335 container_builder.add_port(name=self.app.name, port=PORT)
David Garcia49379ce2021-02-24 13:48:22 +0100336 container_builder.add_envs(
337 {
David Garcia49379ce2021-02-24 13:48:22 +0100338 "REGION_ID": config.region_id,
339 "KEYSTONE_HOST": self.app.name,
David Garcia49379ce2021-02-24 13:48:22 +0100340 }
341 )
David Garcia141d9352021-09-08 17:48:40 +0200342 container_builder.add_secret_envs(
343 secret_name=mysql_secret_name,
344 envs={
345 "DB_HOST": "host",
346 "DB_PORT": "port",
347 "ROOT_DB_USER": "user",
348 "ROOT_DB_PASSWORD": "password",
349 },
350 )
351 container_builder.add_secret_envs(
352 secret_name=keystone_secret_name,
353 envs={
354 "KEYSTONE_DB_PASSWORD": "db_password",
355 "ADMIN_USERNAME": "admin_username",
356 "ADMIN_PASSWORD": "admin_password",
357 "ADMIN_PROJECT": "admin_project",
358 "SERVICE_USERNAME": "service_username",
359 "SERVICE_PASSWORD": "service_password",
360 "SERVICE_PROJECT": "service_project",
361 },
362 )
363 ldap_secret_name = f"{self.app.name}-ldap-secret"
David Garcia49379ce2021-02-24 13:48:22 +0100364 if config_ldap.ldap_enabled:
David Garcia141d9352021-09-08 17:48:40 +0200365 # Add ldap secrets and envs
366 ldap_secrets = {
367 "authentication_domain_name": config_ldap.ldap_authentication_domain_name,
368 "url": config_ldap.ldap_url,
369 "page_size": config_ldap.ldap_page_size,
370 "user_objectclass": config_ldap.ldap_user_objectclass,
371 "user_id_attribute": config_ldap.ldap_user_id_attribute,
372 "user_name_attribute": config_ldap.ldap_user_name_attribute,
373 "user_pass_attribute": config_ldap.ldap_user_pass_attribute,
374 "user_enabled_mask": config_ldap.ldap_user_enabled_mask,
375 "user_enabled_default": config_ldap.ldap_user_enabled_default,
376 "user_enabled_invert": config_ldap.ldap_user_enabled_invert,
377 "group_objectclass": config_ldap.ldap_group_objectclass,
378 }
379 ldap_envs = {
380 "LDAP_AUTHENTICATION_DOMAIN_NAME": "authentication_domain_name",
381 "LDAP_URL": "url",
382 "LDAP_PAGE_SIZE": "page_size",
383 "LDAP_USER_OBJECTCLASS": "user_objectclass",
384 "LDAP_USER_ID_ATTRIBUTE": "user_id_attribute",
385 "LDAP_USER_NAME_ATTRIBUTE": "user_name_attribute",
386 "LDAP_USER_PASS_ATTRIBUTE": "user_pass_attribute",
387 "LDAP_USER_ENABLED_MASK": "user_enabled_mask",
388 "LDAP_USER_ENABLED_DEFAULT": "user_enabled_default",
389 "LDAP_USER_ENABLED_INVERT": "user_enabled_invert",
390 "LDAP_GROUP_OBJECTCLASS": "group_objectclass",
391 }
David Garcia49379ce2021-02-24 13:48:22 +0100392 if config_ldap.ldap_bind_user:
David Garcia141d9352021-09-08 17:48:40 +0200393 ldap_secrets["bind_user"] = config_ldap.ldap_bind_user
394 ldap_envs["LDAP_BIND_USER"] = "bind_user"
sousaedu738bf6f2020-10-10 00:25:26 +0100395
David Garcia49379ce2021-02-24 13:48:22 +0100396 if config_ldap.ldap_bind_password:
David Garcia141d9352021-09-08 17:48:40 +0200397 ldap_secrets["bind_password"] = config_ldap.ldap_bind_password
398 ldap_envs["LDAP_BIND_PASSWORD"] = "bind_password"
sousaedu738bf6f2020-10-10 00:25:26 +0100399
David Garcia49379ce2021-02-24 13:48:22 +0100400 if config_ldap.ldap_user_tree_dn:
David Garcia141d9352021-09-08 17:48:40 +0200401 ldap_secrets["user_tree_dn"] = config_ldap.ldap_user_tree_dn
402 ldap_envs["LDAP_USER_TREE_DN"] = "user_tree_dn"
David Garcia49379ce2021-02-24 13:48:22 +0100403
404 if config_ldap.ldap_user_filter:
David Garcia141d9352021-09-08 17:48:40 +0200405 ldap_secrets["user_filter"] = config_ldap.ldap_user_filter
406 ldap_envs["LDAP_USER_FILTER"] = "user_filter"
David Garcia49379ce2021-02-24 13:48:22 +0100407
408 if config_ldap.ldap_user_enabled_attribute:
David Garcia141d9352021-09-08 17:48:40 +0200409 ldap_secrets[
410 "user_enabled_attribute"
411 ] = config_ldap.ldap_user_enabled_attribute
412 ldap_envs["LDAP_USER_ENABLED_ATTRIBUTE"] = "user_enabled_attribute"
David Garcia49379ce2021-02-24 13:48:22 +0100413 if config_ldap.ldap_chase_referrals:
David Garcia141d9352021-09-08 17:48:40 +0200414 ldap_secrets["chase_referrals"] = config_ldap.ldap_chase_referrals
415 ldap_envs["LDAP_CHASE_REFERRALS"] = "chase_referrals"
David Garcia49379ce2021-02-24 13:48:22 +0100416
417 if config_ldap.ldap_group_tree_dn:
David Garcia141d9352021-09-08 17:48:40 +0200418 ldap_secrets["group_tree_dn"] = config_ldap.ldap_group_tree_dn
419 ldap_envs["LDAP_GROUP_TREE_DN"] = "group_tree_dn"
David Garcia49379ce2021-02-24 13:48:22 +0100420
sousaedu8f0f66f2021-06-17 12:43:59 +0100421 if config_ldap.ldap_tls_cacert_base64:
David Garcia141d9352021-09-08 17:48:40 +0200422 ldap_secrets["tls_cacert_base64"] = config_ldap.ldap_tls_cacert_base64
423 ldap_envs["LDAP_TLS_CACERT_BASE64"] = "tls_cacert_base64"
sousaedu8f0f66f2021-06-17 12:43:59 +0100424
David Garcia49379ce2021-02-24 13:48:22 +0100425 if config_ldap.ldap_use_starttls:
David Garcia141d9352021-09-08 17:48:40 +0200426 ldap_secrets["use_starttls"] = config_ldap.ldap_use_starttls
427 ldap_secrets["tls_cacert_base64"] = config_ldap.ldap_tls_cacert_base64
428 ldap_secrets["tls_req_cert"] = config_ldap.ldap_tls_req_cert
429 ldap_envs["LDAP_USE_STARTTLS"] = "use_starttls"
430 ldap_envs["LDAP_TLS_CACERT_BASE64"] = "tls_cacert_base64"
431 ldap_envs["LDAP_TLS_REQ_CERT"] = "tls_req_cert"
432
433 pod_spec_builder.add_secret(
434 ldap_secret_name,
435 ldap_secrets,
436 )
437 container_builder.add_secret_envs(
438 secret_name=ldap_secret_name,
439 envs=ldap_envs,
440 )
441
David Garcia49379ce2021-02-24 13:48:22 +0100442 container = container_builder.build()
sousaedu996a5602021-05-03 00:22:43 +0200443
David Garcia49379ce2021-02-24 13:48:22 +0100444 # Add container to pod spec
445 pod_spec_builder.add_container(container)
sousaedu996a5602021-05-03 00:22:43 +0200446
David Garcia141d9352021-09-08 17:48:40 +0200447 # Add Pod Restart Policy
448 restart_policy = PodRestartPolicy()
449 restart_policy.add_secrets(
450 secret_names=(mysql_secret_name, keystone_secret_name, ldap_secret_name)
451 )
452 pod_spec_builder.set_restart_policy(restart_policy)
453
David Garcia49379ce2021-02-24 13:48:22 +0100454 # Add ingress resources to pod spec if site url exists
455 if config.site_url:
456 parsed = urlparse(config.site_url)
457 annotations = {
458 "nginx.ingress.kubernetes.io/proxy-body-size": "{}".format(
459 str(config.max_file_size) + "m"
460 if config.max_file_size > 0
461 else config.max_file_size
David Garciad68e0b42021-06-28 16:50:42 +0200462 )
David Garcia49379ce2021-02-24 13:48:22 +0100463 }
David Garciad68e0b42021-06-28 16:50:42 +0200464 if config.ingress_class:
465 annotations["kubernetes.io/ingress.class"] = config.ingress_class
David Garcia49379ce2021-02-24 13:48:22 +0100466 ingress_resource_builder = IngressResourceV3Builder(
467 f"{self.app.name}-ingress", annotations
468 )
469
470 if config.ingress_whitelist_source_range:
471 annotations[
472 "nginx.ingress.kubernetes.io/whitelist-source-range"
473 ] = config.ingress_whitelist_source_range
474
475 if parsed.scheme == "https":
476 ingress_resource_builder.add_tls(
477 [parsed.hostname], config.tls_secret_name
478 )
479 else:
480 annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
481
482 ingress_resource_builder.add_rule(parsed.hostname, self.app.name, PORT)
483 ingress_resource = ingress_resource_builder.build()
484 pod_spec_builder.add_ingress_resource(ingress_resource)
485 return pod_spec_builder.build()
David Garcia009a5d62020-08-27 16:53:44 +0200486
487
488if __name__ == "__main__":
489 main(KeystoneCharm)
David Garcia49379ce2021-02-24 13:48:22 +0100490
491# LOGGER = logging.getLogger(__name__)
492
493
494# class ConfigurePodEvent(EventBase):
495# """Configure Pod event"""
496
497# pass
498
499
500# class KeystoneEvents(CharmEvents):
501# """Keystone Events"""
502
503# configure_pod = EventSource(ConfigurePodEvent)
504
505# class KeystoneCharm(CharmBase):
506# """Keystone K8s Charm"""
507
508# state = StoredState()
509# on = KeystoneEvents()
510
511# def __init__(self, *args) -> NoReturn:
512# """Constructor of the Charm object.
513# Initializes internal state and register events it can handle.
514# """
515# super().__init__(*args)
516# self.state.set_default(db_host=None)
517# self.state.set_default(db_port=None)
518# self.state.set_default(db_user=None)
519# self.state.set_default(db_password=None)
520# self.state.set_default(pod_spec=None)
521# self.state.set_default(fernet_keys=None)
522# self.state.set_default(credential_keys=None)
523# self.state.set_default(keys_timestamp=0)
524
525# # Register all of the events we want to observe
526# self.framework.observe(self.on.config_changed, self.configure_pod)
527# self.framework.observe(self.on.start, self.configure_pod)
528# self.framework.observe(self.on.upgrade_charm, self.configure_pod)
529# self.framework.observe(self.on.leader_elected, self.configure_pod)
530# self.framework.observe(self.on.update_status, self.configure_pod)
531
532# # Registering custom internal events
533# self.framework.observe(self.on.configure_pod, self.configure_pod)
534
535# # Register relation events
536# self.framework.observe(
537# self.on.db_relation_changed, self._on_db_relation_changed
538# )
539# self.framework.observe(
540# self.on.db_relation_broken, self._on_db_relation_broken
541# )
542# self.framework.observe(
543# self.on.keystone_relation_joined, self._publish_keystone_info
544# )
545
546# def _publish_keystone_info(self, event: EventBase) -> NoReturn:
547# """Publishes keystone information for NBI usage through the keystone
548# relation.
549
550# Args:
551# event (EventBase): Keystone relation event to update NBI.
552# """
553# config = self.model.config
554# rel_data = {
555# "host": f"http://{self.app.name}:{KEYSTONE_PORT}/v3",
556# "port": str(KEYSTONE_PORT),
557# "keystone_db_password": config["keystone_db_password"],
558# "region_id": config["region_id"],
559# "user_domain_name": config["user_domain_name"],
560# "project_domain_name": config["project_domain_name"],
561# "admin_username": config["admin_username"],
562# "admin_password": config["admin_password"],
563# "admin_project_name": config["admin_project"],
564# "username": config["service_username"],
565# "password": config["service_password"],
566# "service": config["service_project"],
567# }
568# for k, v in rel_data.items():
569# event.relation.data[self.model.unit][k] = v
570
571# def _on_db_relation_changed(self, event: EventBase) -> NoReturn:
572# """Reads information about the DB relation, in order for keystone to
573# access it.
574
575# Args:
576# event (EventBase): DB relation event to access database
577# information.
578# """
579# if not event.unit in event.relation.data:
580# return
581# relation_data = event.relation.data[event.unit]
582# db_host = relation_data.get("host")
583# db_port = int(relation_data.get("port", 3306))
584# db_user = "root"
585# db_password = relation_data.get("root_password")
586
587# if (
588# db_host
589# and db_port
590# and db_user
591# and db_password
592# and (
593# self.state.db_host != db_host
594# or self.state.db_port != db_port
595# or self.state.db_user != db_user
596# or self.state.db_password != db_password
597# )
598# ):
599# self.state.db_host = db_host
600# self.state.db_port = db_port
601# self.state.db_user = db_user
602# self.state.db_password = db_password
603# self.on.configure_pod.emit()
604
605
606# def _on_db_relation_broken(self, event: EventBase) -> NoReturn:
607# """Clears data from db relation.
608
609# Args:
610# event (EventBase): DB relation event.
611
612# """
613# self.state.db_host = None
614# self.state.db_port = None
615# self.state.db_user = None
616# self.state.db_password = None
617# self.on.configure_pod.emit()
618
619# def _check_settings(self) -> str:
620# """Check if there any settings missing from Keystone configuration.
621
622# Returns:
623# str: Information about the problems found (if any).
624# """
625# problems = []
626# config = self.model.config
627
628# for setting in REQUIRED_SETTINGS:
629# if not config.get(setting):
630# problem = f"missing config {setting}"
631# problems.append(problem)
632
633# return ";".join(problems)
634
635# def _make_pod_image_details(self) -> Dict[str, str]:
636# """Generate the pod image details.
637
638# Returns:
639# Dict[str, str]: pod image details.
640# """
641# config = self.model.config
642# image_details = {
643# "imagePath": config["image"],
644# }
645# if config["image_username"]:
646# image_details.update(
647# {
648# "username": config["image_username"],
649# "password": config["image_password"],
650# }
651# )
652# return image_details
653
654# def _make_pod_ports(self) -> List[Dict[str, Any]]:
655# """Generate the pod ports details.
656
657# Returns:
658# List[Dict[str, Any]]: pod ports details.
659# """
660# return [
661# {"name": "keystone", "containerPort": KEYSTONE_PORT, "protocol": "TCP"},
662# ]
663
664# def _make_pod_envconfig(self) -> Dict[str, Any]:
665# """Generate pod environment configuraiton.
666
667# Returns:
668# Dict[str, Any]: pod environment configuration.
669# """
670# config = self.model.config
671
672# envconfig = {
673# "DB_HOST": self.state.db_host,
674# "DB_PORT": self.state.db_port,
675# "ROOT_DB_USER": self.state.db_user,
676# "ROOT_DB_PASSWORD": self.state.db_password,
677# "KEYSTONE_DB_PASSWORD": config["keystone_db_password"],
678# "REGION_ID": config["region_id"],
679# "KEYSTONE_HOST": self.app.name,
680# "ADMIN_USERNAME": config["admin_username"],
681# "ADMIN_PASSWORD": config["admin_password"],
682# "ADMIN_PROJECT": config["admin_project"],
683# "SERVICE_USERNAME": config["service_username"],
684# "SERVICE_PASSWORD": config["service_password"],
685# "SERVICE_PROJECT": config["service_project"],
686# }
687
688# if config.get("ldap_enabled"):
689# envconfig["LDAP_AUTHENTICATION_DOMAIN_NAME"] = config[
690# "ldap_authentication_domain_name"
691# ]
692# envconfig["LDAP_URL"] = config["ldap_url"]
693# envconfig["LDAP_PAGE_SIZE"] = config["ldap_page_size"]
694# envconfig["LDAP_USER_OBJECTCLASS"] = config["ldap_user_objectclass"]
695# envconfig["LDAP_USER_ID_ATTRIBUTE"] = config["ldap_user_id_attribute"]
696# envconfig["LDAP_USER_NAME_ATTRIBUTE"] = config["ldap_user_name_attribute"]
697# envconfig["LDAP_USER_PASS_ATTRIBUTE"] = config["ldap_user_pass_attribute"]
698# envconfig["LDAP_USER_ENABLED_MASK"] = config["ldap_user_enabled_mask"]
699# envconfig["LDAP_USER_ENABLED_DEFAULT"] = config["ldap_user_enabled_default"]
700# envconfig["LDAP_USER_ENABLED_INVERT"] = config["ldap_user_enabled_invert"]
701# envconfig["LDAP_GROUP_OBJECTCLASS"] = config["ldap_group_objectclass"]
702
703# if config["ldap_bind_user"]:
704# envconfig["LDAP_BIND_USER"] = config["ldap_bind_user"]
705
706# if config["ldap_bind_password"]:
707# envconfig["LDAP_BIND_PASSWORD"] = config["ldap_bind_password"]
708
709# if config["ldap_user_tree_dn"]:
710# envconfig["LDAP_USER_TREE_DN"] = config["ldap_user_tree_dn"]
711
712# if config["ldap_user_filter"]:
713# envconfig["LDAP_USER_FILTER"] = config["ldap_user_filter"]
714
715# if config["ldap_user_enabled_attribute"]:
716# envconfig["LDAP_USER_ENABLED_ATTRIBUTE"] = config[
717# "ldap_user_enabled_attribute"
718# ]
719
720# if config["ldap_chase_referrals"]:
721# envconfig["LDAP_CHASE_REFERRALS"] = config["ldap_chase_referrals"]
722
723# if config["ldap_group_tree_dn"]:
724# envconfig["LDAP_GROUP_TREE_DN"] = config["ldap_group_tree_dn"]
725
726# if config["ldap_use_starttls"]:
727# envconfig["LDAP_USE_STARTTLS"] = config["ldap_use_starttls"]
728# envconfig["LDAP_TLS_CACERT_BASE64"] = config["ldap_tls_cacert_base64"]
729# envconfig["LDAP_TLS_REQ_CERT"] = config["ldap_tls_req_cert"]
730
731# return envconfig
732
733# def _make_pod_ingress_resources(self) -> List[Dict[str, Any]]:
734# """Generate pod ingress resources.
735
736# Returns:
737# List[Dict[str, Any]]: pod ingress resources.
738# """
739# site_url = self.model.config["site_url"]
740
741# if not site_url:
742# return
743
744# parsed = urlparse(site_url)
745
746# if not parsed.scheme.startswith("http"):
747# return
748
749# max_file_size = self.model.config["max_file_size"]
750# ingress_whitelist_source_range = self.model.config[
751# "ingress_whitelist_source_range"
752# ]
753
754# annotations = {
755# "nginx.ingress.kubernetes.io/proxy-body-size": "{}m".format(max_file_size)
756# }
757
758# if ingress_whitelist_source_range:
759# annotations[
760# "nginx.ingress.kubernetes.io/whitelist-source-range"
761# ] = ingress_whitelist_source_range
762
763# ingress_spec_tls = None
764
765# if parsed.scheme == "https":
766# ingress_spec_tls = [{"hosts": [parsed.hostname]}]
767# tls_secret_name = self.model.config["tls_secret_name"]
768# if tls_secret_name:
769# ingress_spec_tls[0]["secretName"] = tls_secret_name
770# else:
771# annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
772
773# ingress = {
774# "name": "{}-ingress".format(self.app.name),
775# "annotations": annotations,
776# "spec": {
777# "rules": [
778# {
779# "host": parsed.hostname,
780# "http": {
781# "paths": [
782# {
783# "path": "/",
784# "backend": {
785# "serviceName": self.app.name,
786# "servicePort": KEYSTONE_PORT,
787# },
788# }
789# ]
790# },
791# }
792# ],
793# },
794# }
795# if ingress_spec_tls:
796# ingress["spec"]["tls"] = ingress_spec_tls
797
798# return [ingress]
799
800# def _generate_keys(self) -> Tuple[List[str], List[str]]:
801# """Generating new fernet tokens.
802
803# Returns:
804# Tuple[List[str], List[str]]: contains two lists of strings. First
805# list contains strings that represent
806# the keys for fernet and the second
807# list contains strins that represent
808# the keys for credentials.
809# """
810# fernet_keys = [
811# Fernet.generate_key().decode() for _ in range(NUMBER_FERNET_KEYS)
812# ]
813# credential_keys = [
814# Fernet.generate_key().decode() for _ in range(NUMBER_CREDENTIAL_KEYS)
815# ]
816
817# return (fernet_keys, credential_keys)
818
819# def configure_pod(self, event: EventBase) -> NoReturn:
820# """Assemble the pod spec and apply it, if possible.
821
822# Args:
823# event (EventBase): Hook or Relation event that started the
824# function.
825# """
826# if not self.state.db_host:
827# self.unit.status = WaitingStatus("Waiting for database relation")
828# event.defer()
829# return
830
831# if not self.unit.is_leader():
832# self.unit.status = ActiveStatus("ready")
833# return
834
835# if fernet_keys := self.state.fernet_keys:
836# fernet_keys = json.loads(fernet_keys)
837
838# if credential_keys := self.state.credential_keys:
839# credential_keys = json.loads(credential_keys)
840
841# now = datetime.now().timestamp()
842# keys_timestamp = self.state.keys_timestamp
843# token_expiration = self.model.config["token_expiration"]
844
845# valid_keys = (now - keys_timestamp) < token_expiration
846# if not credential_keys or not fernet_keys or not valid_keys:
847# fernet_keys, credential_keys = self._generate_keys()
848# self.state.fernet_keys = json.dumps(fernet_keys)
849# self.state.credential_keys = json.dumps(credential_keys)
850# self.state.keys_timestamp = now
851
852# # Check problems in the settings
853# problems = self._check_settings()
854# if problems:
855# self.unit.status = BlockedStatus(problems)
856# return
857
858# self.unit.status = BlockedStatus("Assembling pod spec")
859# image_details = self._make_pod_image_details()
860# ports = self._make_pod_ports()
861# env_config = self._make_pod_envconfig()
862# ingress_resources = self._make_pod_ingress_resources()
863# files = self._make_pod_files(fernet_keys, credential_keys)
864
865# pod_spec = {
866# "version": 3,
867# "containers": [
868# {
869# "name": self.framework.model.app.name,
870# "imageDetails": image_details,
871# "ports": ports,
872# "envConfig": env_config,
873# "volumeConfig": files,
874# }
875# ],
876# "kubernetesResources": {"ingressResources": ingress_resources or []},
877# }
878
879# if self.state.pod_spec != (
880# pod_spec_json := json.dumps(pod_spec, sort_keys=True)
881# ):
882# self.state.pod_spec = pod_spec_json
883# self.model.pod.set_spec(pod_spec)
884
885# self.unit.status = ActiveStatus("ready")
886
887
888# if __name__ == "__main__":
889# main(KeystoneCharm)