2 # Copyright 2020 Canonical Ltd.
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at
8 # http://www.apache.org/licenses/LICENSE-2.0
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
18 from urllib
.parse
import urlparse
20 from ops
.charm
import CharmBase
22 # from ops.framework import StoredState
23 from ops
.main
import main
24 from ops
.model
import (
31 from ops
.framework
import StoredState
33 logger
= logging
.getLogger(__name__
)
35 REQUIRED_SETTINGS
= []
37 DATABASE_NAME
= "keystone" # This is hardcoded in the keystone container script
38 # We expect the keystone container to use the default port
42 class KeystoneCharm(CharmBase
):
46 def __init__(self
, *args
):
47 super().__init
__(*args
)
49 # Register all of the events we want to observe
50 self
.framework
.observe(self
.on
.config_changed
, self
.configure_pod
)
51 self
.framework
.observe(self
.on
.start
, self
.configure_pod
)
52 self
.framework
.observe(self
.on
.upgrade_charm
, self
.configure_pod
)
54 # Register relation events
55 self
.state
.set_default(
56 db_host
=None, db_port
=None, db_user
=None, db_password
=None
58 self
.framework
.observe(
59 self
.on
.db_relation_changed
, self
._on
_db
_relation
_changed
61 self
.framework
.observe(
62 self
.on
.keystone_relation_joined
, self
._publish
_keystone
_info
65 def _publish_keystone_info(self
, event
):
66 config
= self
.model
.config
67 if self
.unit
.is_leader():
69 "host": f
"http://{self.app.name}:{KEYSTONE_PORT}/v3",
70 "port": str(KEYSTONE_PORT
),
71 "keystone_db_password": config
["keystone_db_password"],
72 "region_id": config
["region_id"],
73 "user_domain_name": config
["user_domain_name"],
74 "project_domain_name": config
["project_domain_name"],
75 "admin_username": config
["admin_username"],
76 "admin_password": config
["admin_password"],
77 "admin_project_name": config
["admin_project"],
78 "username": config
["service_username"],
79 "password": config
["service_password"],
80 "service": config
["service_project"],
82 for k
, v
in rel_data
.items():
83 event
.relation
.data
[self
.model
.unit
][k
] = v
85 def _on_db_relation_changed(self
, event
):
86 self
.state
.db_host
= event
.relation
.data
[event
.unit
].get("host")
87 self
.state
.db_port
= event
.relation
.data
[event
.unit
].get("port", 3306)
88 self
.state
.db_user
= "root" # event.relation.data[event.unit].get("user")
89 self
.state
.db_password
= event
.relation
.data
[event
.unit
].get("root_password")
90 if self
.state
.db_host
:
91 self
.configure_pod(event
)
93 def _check_settings(self
):
95 config
= self
.model
.config
97 for setting
in REQUIRED_SETTINGS
:
98 if not config
.get(setting
):
99 problem
= f
"missing config {setting}"
100 problems
.append(problem
)
102 return ";".join(problems
)
104 def _make_pod_image_details(self
):
105 config
= self
.model
.config
107 "imagePath": config
["image"],
109 if config
["image_username"]:
110 image_details
.update(
112 "username": config
["image_username"],
113 "password": config
["image_password"],
118 def _make_pod_ports(self
):
120 {"name": "keystone", "containerPort": KEYSTONE_PORT
, "protocol": "TCP"},
123 def _make_pod_envconfig(self
):
124 config
= self
.model
.config
127 "DB_HOST": self
.state
.db_host
,
128 "DB_PORT": self
.state
.db_port
,
129 "ROOT_DB_USER": self
.state
.db_user
,
130 "ROOT_DB_PASSWORD": self
.state
.db_password
,
131 "KEYSTONE_DB_PASSWORD": config
["keystone_db_password"],
132 "REGION_ID": config
["region_id"],
133 "KEYSTONE_HOST": self
.app
.name
,
134 "ADMIN_USERNAME": config
["admin_username"],
135 "ADMIN_PASSWORD": config
["admin_password"],
136 "ADMIN_PROJECT": config
["admin_project"],
137 "SERVICE_USERNAME": config
["service_username"],
138 "SERVICE_PASSWORD": config
["service_password"],
139 "SERVICE_PROJECT": config
["service_project"],
142 if config
.get("ldap_enabled"):
143 envconfig
["LDAP_AUTHENTICATION_DOMAIN_NAME"] = config
[
144 "ldap_authentication_domain_name"
146 envconfig
["LDAP_URL"] = config
["ldap_url"]
147 envconfig
["LDAP_USER_OBJECTCLASS"] = config
["ldap_user_objectclass"]
148 envconfig
["LDAP_USER_ID_ATTRIBUTE"] = config
["ldap_user_id_attribute"]
149 envconfig
["LDAP_USER_NAME_ATTRIBUTE"] = config
["ldap_user_name_attribute"]
150 envconfig
["LDAP_USER_PASS_ATTRIBUTE"] = config
["ldap_user_pass_attribute"]
151 envconfig
["LDAP_USER_ENABLED_MASK"] = config
["ldap_user_enabled_mask"]
152 envconfig
["LDAP_USER_ENABLED_DEFAULT"] = config
["ldap_user_enabled_default"]
153 envconfig
["LDAP_USER_ENABLED_INVERT"] = config
["ldap_user_enabled_invert"]
155 if config
["ldap_bind_user"]:
156 envconfig
["LDAP_BIND_USER"] = config
["ldap_bind_user"]
158 if config
["ldap_bind_password"]:
159 envconfig
["LDAP_BIND_PASSWORD"] = config
["ldap_bind_password"]
161 if config
["ldap_user_tree_dn"]:
162 envconfig
["LDAP_USER_TREE_DN"] = config
["ldap_user_tree_dn"]
164 if config
["ldap_user_filter"]:
165 envconfig
["LDAP_USER_FILTER"] = config
["ldap_user_filter"]
167 if config
["ldap_user_enabled_attribute"]:
168 envconfig
["LDAP_USER_ENABLED_ATTRIBUTE"] = config
[
169 "ldap_user_enabled_attribute"
172 if config
["ldap_use_starttls"]:
173 envconfig
["LDAP_USE_STARTTLS"] = config
["ldap_use_starttls"]
174 envconfig
["LDAP_TLS_CACERT_BASE64"] = config
["ldap_tls_cacert_base64"]
175 envconfig
["LDAP_TLS_REQ_CERT"] = config
["ldap_tls_req_cert"]
179 def _make_pod_ingress_resources(self
):
180 site_url
= self
.model
.config
["site_url"]
185 parsed
= urlparse(site_url
)
187 if not parsed
.scheme
.startswith("http"):
190 max_file_size
= self
.model
.config
["max_file_size"]
191 ingress_whitelist_source_range
= self
.model
.config
[
192 "ingress_whitelist_source_range"
196 "nginx.ingress.kubernetes.io/proxy-body-size": "{}m".format(max_file_size
)
199 if ingress_whitelist_source_range
:
201 "nginx.ingress.kubernetes.io/whitelist-source-range"
202 ] = ingress_whitelist_source_range
204 ingress_spec_tls
= None
206 if parsed
.scheme
== "https":
207 ingress_spec_tls
= [{"hosts": [parsed
.hostname
]}]
208 tls_secret_name
= self
.model
.config
["tls_secret_name"]
210 ingress_spec_tls
[0]["secretName"] = tls_secret_name
212 annotations
["nginx.ingress.kubernetes.io/ssl-redirect"] = "false"
215 "name": "{}-ingress".format(self
.app
.name
),
216 "annotations": annotations
,
220 "host": parsed
.hostname
,
226 "serviceName": self
.app
.name
,
227 "servicePort": KEYSTONE_PORT
,
237 ingress
["spec"]["tls"] = ingress_spec_tls
241 def configure_pod(self
, event
):
242 """Assemble the pod spec and apply it, if possible."""
244 if not self
.state
.db_host
:
245 self
.unit
.status
= WaitingStatus("Waiting for database relation")
249 if not self
.unit
.is_leader():
250 self
.unit
.status
= ActiveStatus()
253 # Check problems in the settings
254 problems
= self
._check
_settings
()
256 self
.unit
.status
= BlockedStatus(problems
)
259 self
.unit
.status
= BlockedStatus("Assembling pod spec")
260 image_details
= self
._make
_pod
_image
_details
()
261 ports
= self
._make
_pod
_ports
()
262 env_config
= self
._make
_pod
_envconfig
()
263 ingress_resources
= self
._make
_pod
_ingress
_resources
()
269 "name": self
.framework
.model
.app
.name
,
270 "imageDetails": image_details
,
272 "envConfig": env_config
,
275 "kubernetesResources": {"ingressResources": ingress_resources
or []},
277 self
.model
.pod
.set_spec(pod_spec
)
278 self
.unit
.status
= ActiveStatus()
281 if __name__
== "__main__":