Merge ~mthaddon/charm-k8s-openldap/+git/charm-k8s-openldap:password-action into charm-k8s-openldap:master
- Git
- lp:~mthaddon/charm-k8s-openldap/+git/charm-k8s-openldap
- password-action
- Merge into master
Proposed by
Tom Haddon
Status: | Merged |
---|---|
Approved by: | Tom Haddon |
Approved revision: | e5a26456306d412b7f8347c311f767f9f2856987 |
Merged at revision: | 82adb10deb3df836125a4b4638ca7a7b54ff99e3 |
Proposed branch: | ~mthaddon/charm-k8s-openldap/+git/charm-k8s-openldap:password-action |
Merge into: | charm-k8s-openldap:master |
Diff against target: |
649 lines (+322/-123) 8 files modified
README.md (+6/-1) actions.yaml (+3/-0) config.yaml (+0/-5) image-scripts/build-openldap.sh (+2/-0) requirements.txt (+1/-0) src/charm.py (+26/-34) src/leadership.py (+218/-0) tests/unit/test_charm.py (+66/-83) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Stuart Bishop (community) | Approve | ||
Canonical IS Reviewers | Pending | ||
Review via email: mp+395003@code.launchpad.net |
Commit message
Remove admin_password as a config option, add an action to retrieve it
Description of the change
Remove admin_password as a config option, add an action to retrieve it
To post a comment you must log in.
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote : | # |
Revision history for this message
Stuart Bishop (stub) wrote : | # |
Looks fine, comments inline. If you decide to drop RichLeaderData, the changes will be minor and unlikely to need a re-review.
review:
Approve
Revision history for this message
Stuart Bishop (stub) wrote : | # |
Still looks good, ta.
review:
Approve
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote : | # |
Change successfully merged at revision 82adb10deb3df83
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/README.md b/README.md | |||
2 | index bcde053..5fd477d 100644 | |||
3 | --- a/README.md | |||
4 | +++ b/README.md | |||
5 | @@ -11,10 +11,15 @@ cloud, attached to a controller using `juju add-k8s`. | |||
6 | 11 | 11 | ||
7 | 12 | To deploy this charm with both OpenLDAP and PostgreSQL inside a k8s model, run: | 12 | To deploy this charm with both OpenLDAP and PostgreSQL inside a k8s model, run: |
8 | 13 | 13 | ||
10 | 14 | juju deploy cs:~openldap-charmers/openldap --admin_password=admin | 14 | juju deploy cs:~openldap-charmers/openldap |
11 | 15 | juju deploy cs:~postgresql-charmers/postgresql-k8s postgresql | 15 | juju deploy cs:~postgresql-charmers/postgresql-k8s postgresql |
12 | 16 | juju add-relation openldap:db postgresql:db | 16 | juju add-relation openldap:db postgresql:db |
13 | 17 | 17 | ||
14 | 18 | To retrieve the auto-generated LDAP admin password, run, assuming you're using | ||
15 | 19 | Juju 2.x: | ||
16 | 20 | |||
17 | 21 | juju run-action openldap/0 --wait get-admin-password | ||
18 | 22 | |||
19 | 18 | ### Developing | 23 | ### Developing |
20 | 19 | 24 | ||
21 | 20 | Notes for deploying a test setup locally using microk8s: | 25 | Notes for deploying a test setup locally using microk8s: |
22 | diff --git a/actions.yaml b/actions.yaml | |||
23 | 21 | new file mode 100644 | 26 | new file mode 100644 |
24 | index 0000000..04086e6 | |||
25 | --- /dev/null | |||
26 | +++ b/actions.yaml | |||
27 | @@ -0,0 +1,3 @@ | |||
28 | 1 | get-admin-password: | ||
29 | 2 | description: > | ||
30 | 3 | Retrieve the auto-generated LDAP admin password. | ||
31 | diff --git a/config.yaml b/config.yaml | |||
32 | index 5db4484..c442062 100644 | |||
33 | --- a/config.yaml | |||
34 | +++ b/config.yaml | |||
35 | @@ -18,8 +18,3 @@ options: | |||
36 | 18 | description: | | 18 | description: | |
37 | 19 | The password associated with image_username for accessing the registry specified in image_path. | 19 | The password associated with image_username for accessing the registry specified in image_path. |
38 | 20 | default: '' | 20 | default: '' |
39 | 21 | admin_password: | ||
40 | 22 | type: string | ||
41 | 23 | description: | | ||
42 | 24 | The password for the OpenLDAP admin user. | ||
43 | 25 | default: '' | ||
44 | diff --git a/image-scripts/build-openldap.sh b/image-scripts/build-openldap.sh | |||
45 | index e696847..283466a 100755 | |||
46 | --- a/image-scripts/build-openldap.sh | |||
47 | +++ b/image-scripts/build-openldap.sh | |||
48 | @@ -6,6 +6,8 @@ apt-get update | |||
49 | 6 | apt-get -y dist-upgrade | 6 | apt-get -y dist-upgrade |
50 | 7 | apt-get --purge autoremove -y | 7 | apt-get --purge autoremove -y |
51 | 8 | apt-get install -y wget unixodbc make gcc unixodbc-dev groff-base ldap-utils odbc-postgresql gettext postgresql-client | 8 | apt-get install -y wget unixodbc make gcc unixodbc-dev groff-base ldap-utils odbc-postgresql gettext postgresql-client |
52 | 9 | # Needed for juju actions to work. | ||
53 | 10 | apt-get install -y python3-yaml | ||
54 | 9 | 11 | ||
55 | 10 | echo "Making build dir" | 12 | echo "Making build dir" |
56 | 11 | mkdir -p /srv/build | 13 | mkdir -p /srv/build |
57 | diff --git a/requirements.txt b/requirements.txt | |||
58 | index fd6adcd..12aa474 100644 | |||
59 | --- a/requirements.txt | |||
60 | +++ b/requirements.txt | |||
61 | @@ -1,2 +1,3 @@ | |||
62 | 1 | charmhelpers | ||
63 | 1 | ops | 2 | ops |
64 | 2 | ops-lib-pgsql | 3 | ops-lib-pgsql |
65 | diff --git a/src/charm.py b/src/charm.py | |||
66 | index 069d3dd..f8d5489 100755 | |||
67 | --- a/src/charm.py | |||
68 | +++ b/src/charm.py | |||
69 | @@ -4,6 +4,7 @@ | |||
70 | 4 | 4 | ||
71 | 5 | import logging | 5 | import logging |
72 | 6 | 6 | ||
73 | 7 | from charmhelpers.core import host | ||
74 | 7 | import ops.lib | 8 | import ops.lib |
75 | 8 | from ops.charm import ( | 9 | from ops.charm import ( |
76 | 9 | CharmBase, | 10 | CharmBase, |
77 | @@ -17,10 +18,10 @@ from ops.framework import ( | |||
78 | 17 | ) | 18 | ) |
79 | 18 | from ops.model import ( | 19 | from ops.model import ( |
80 | 19 | ActiveStatus, | 20 | ActiveStatus, |
81 | 20 | BlockedStatus, | ||
82 | 21 | MaintenanceStatus, | 21 | MaintenanceStatus, |
83 | 22 | WaitingStatus, | 22 | WaitingStatus, |
84 | 23 | ) | 23 | ) |
85 | 24 | from leadership import LeadershipSettings | ||
86 | 24 | 25 | ||
87 | 25 | 26 | ||
88 | 26 | pgsql = ops.lib.use("pgsql", 1, "postgresql-charmers@lists.launchpad.net") | 27 | pgsql = ops.lib.use("pgsql", 1, "postgresql-charmers@lists.launchpad.net") |
89 | @@ -28,7 +29,6 @@ pgsql = ops.lib.use("pgsql", 1, "postgresql-charmers@lists.launchpad.net") | |||
90 | 28 | logger = logging.getLogger(__name__) | 29 | logger = logging.getLogger(__name__) |
91 | 29 | 30 | ||
92 | 30 | 31 | ||
93 | 31 | REQUIRED_SETTINGS = ['admin_password'] | ||
94 | 32 | DATABASE_NAME = 'openldap' | 32 | DATABASE_NAME = 'openldap' |
95 | 33 | 33 | ||
96 | 34 | 34 | ||
97 | @@ -50,10 +50,13 @@ class OpenLDAPK8sCharm(CharmBase): | |||
98 | 50 | def __init__(self, *args): | 50 | def __init__(self, *args): |
99 | 51 | super().__init__(*args) | 51 | super().__init__(*args) |
100 | 52 | 52 | ||
101 | 53 | self.leader_data = LeadershipSettings() | ||
102 | 54 | |||
103 | 53 | self.framework.observe(self.on.start, self._configure_pod) | 55 | self.framework.observe(self.on.start, self._configure_pod) |
104 | 54 | self.framework.observe(self.on.config_changed, self._configure_pod) | 56 | self.framework.observe(self.on.config_changed, self._configure_pod) |
105 | 55 | self.framework.observe(self.on.leader_elected, self._configure_pod) | 57 | self.framework.observe(self.on.leader_elected, self._configure_pod) |
106 | 56 | self.framework.observe(self.on.upgrade_charm, self._configure_pod) | 58 | self.framework.observe(self.on.upgrade_charm, self._configure_pod) |
107 | 59 | self.framework.observe(self.on.get_admin_password_action, self._on_get_admin_password_action) | ||
108 | 57 | 60 | ||
109 | 58 | # database | 61 | # database |
110 | 59 | self._state.set_default(postgres=None) | 62 | self._state.set_default(postgres=None) |
111 | @@ -93,30 +96,6 @@ class OpenLDAPK8sCharm(CharmBase): | |||
112 | 93 | 96 | ||
113 | 94 | self.on.db_master_available.emit() | 97 | self.on.db_master_available.emit() |
114 | 95 | 98 | ||
115 | 96 | def _check_for_config_problems(self): | ||
116 | 97 | """Check for some simple configuration problems and return a | ||
117 | 98 | string describing them, otherwise return an empty string.""" | ||
118 | 99 | problems = [] | ||
119 | 100 | |||
120 | 101 | missing = self._missing_charm_settings() | ||
121 | 102 | if missing: | ||
122 | 103 | problems.append('required setting(s) empty: {}'.format(', '.join(sorted(missing)))) | ||
123 | 104 | |||
124 | 105 | return '; '.join(filter(None, problems)) | ||
125 | 106 | |||
126 | 107 | def _missing_charm_settings(self): | ||
127 | 108 | """Check configuration setting dependencies and return a list of | ||
128 | 109 | missing settings; otherwise return an empty list.""" | ||
129 | 110 | config = self.model.config | ||
130 | 111 | |||
131 | 112 | # Options in config.yaml are always present as at least "" | ||
132 | 113 | # so config[setting] will not fail with a KeyError if unset via juju. | ||
133 | 114 | # We define missing in terms of being required by the charm and explicitly | ||
134 | 115 | # set rather than default from config.yaml. | ||
135 | 116 | missing = {setting for setting in REQUIRED_SETTINGS if not config[setting]} | ||
136 | 117 | |||
137 | 118 | return sorted(missing) | ||
138 | 119 | |||
139 | 120 | def _make_pod_spec(self): | 99 | def _make_pod_spec(self): |
140 | 121 | """Return a pod spec with some core configuration.""" | 100 | """Return a pod spec with some core configuration.""" |
141 | 122 | config = self.model.config | 101 | config = self.model.config |
142 | @@ -142,9 +121,28 @@ class OpenLDAPK8sCharm(CharmBase): | |||
143 | 142 | ], | 121 | ], |
144 | 143 | } | 122 | } |
145 | 144 | 123 | ||
146 | 124 | def _on_get_admin_password_action(self, event): | ||
147 | 125 | """Handle on get-admin-password action.""" | ||
148 | 126 | admin_password = self.get_admin_password() | ||
149 | 127 | if admin_password: | ||
150 | 128 | event.set_results({"admin-password": self.get_admin_password()}) | ||
151 | 129 | else: | ||
152 | 130 | event.fail("LDAP admin password has not yet been set, please retry later.") | ||
153 | 131 | |||
154 | 132 | def get_admin_password(self): | ||
155 | 133 | """Get the LDAP admin password. | ||
156 | 134 | |||
157 | 135 | If a password hasn't been set yet, create one if we're the leader, | ||
158 | 136 | or return an empty string if we're not.""" | ||
159 | 137 | admin_password = self.leader_data["admin_password"] | ||
160 | 138 | if not admin_password: | ||
161 | 139 | if self.unit.is_leader: | ||
162 | 140 | admin_password = host.pwgen(40) | ||
163 | 141 | self.leader_data["admin_password"] = admin_password | ||
164 | 142 | return admin_password | ||
165 | 143 | |||
166 | 145 | def _make_pod_config(self): | 144 | def _make_pod_config(self): |
167 | 146 | """Return an envConfig with some core configuration.""" | 145 | """Return an envConfig with some core configuration.""" |
168 | 147 | config = self.model.config | ||
169 | 148 | pod_config = { | 146 | pod_config = { |
170 | 149 | 'POSTGRES_NAME': self._state.postgres['dbname'], | 147 | 'POSTGRES_NAME': self._state.postgres['dbname'], |
171 | 150 | 'POSTGRES_USER': self._state.postgres['user'], | 148 | 'POSTGRES_USER': self._state.postgres['user'], |
172 | @@ -153,8 +151,7 @@ class OpenLDAPK8sCharm(CharmBase): | |||
173 | 153 | 'POSTGRES_PORT': self._state.postgres['port'], | 151 | 'POSTGRES_PORT': self._state.postgres['port'], |
174 | 154 | } | 152 | } |
175 | 155 | 153 | ||
178 | 156 | if 'admin_password' in config: | 154 | pod_config['LDAP_ADMIN_PASSWORD'] = self.get_admin_password() |
177 | 157 | pod_config['LDAP_ADMIN_PASSWORD'] = config['admin_password'] | ||
179 | 158 | 155 | ||
180 | 159 | return pod_config | 156 | return pod_config |
181 | 160 | 157 | ||
182 | @@ -169,11 +166,6 @@ class OpenLDAPK8sCharm(CharmBase): | |||
183 | 169 | self.unit.status = ActiveStatus() | 166 | self.unit.status = ActiveStatus() |
184 | 170 | return | 167 | return |
185 | 171 | 168 | ||
186 | 172 | problems = self._check_for_config_problems() | ||
187 | 173 | if problems: | ||
188 | 174 | self.unit.status = BlockedStatus(problems) | ||
189 | 175 | return | ||
190 | 176 | |||
191 | 177 | self.unit.status = MaintenanceStatus('Assembling pod spec') | 169 | self.unit.status = MaintenanceStatus('Assembling pod spec') |
192 | 178 | pod_spec = self._make_pod_spec() | 170 | pod_spec = self._make_pod_spec() |
193 | 179 | 171 | ||
194 | diff --git a/src/leadership.py b/src/leadership.py | |||
195 | 180 | new file mode 100644 | 172 | new file mode 100644 |
196 | index 0000000..8e88779 | |||
197 | --- /dev/null | |||
198 | +++ b/src/leadership.py | |||
199 | @@ -0,0 +1,218 @@ | |||
200 | 1 | # This file is part of the PostgreSQL k8s Charm for Juju. | ||
201 | 2 | # Copyright 2020 Canonical Ltd. | ||
202 | 3 | # | ||
203 | 4 | # This program is free software: you can redistribute it and/or modify | ||
204 | 5 | # it under the terms of the GNU General Public License version 3, as | ||
205 | 6 | # published by the Free Software Foundation. | ||
206 | 7 | # | ||
207 | 8 | # This program is distributed in the hope that it will be useful, but | ||
208 | 9 | # WITHOUT ANY WARRANTY; without even the implied warranties of | ||
209 | 10 | # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR | ||
210 | 11 | # PURPOSE. See the GNU General Public License for more details. | ||
211 | 12 | # | ||
212 | 13 | # You should have received a copy of the GNU General Public License | ||
213 | 14 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
214 | 15 | |||
215 | 16 | # TODO: Most of all of this module should move into the Operator Framework core | ||
216 | 17 | |||
217 | 18 | import collections.abc | ||
218 | 19 | import subprocess | ||
219 | 20 | from typing import Any, Iterable, Dict, MutableMapping, Protocol | ||
220 | 21 | |||
221 | 22 | import ops | ||
222 | 23 | import yaml | ||
223 | 24 | |||
224 | 25 | |||
225 | 26 | class _Codec(Protocol): | ||
226 | 27 | def encode(self, value: Any) -> str: | ||
227 | 28 | raise NotImplementedError("encode") | ||
228 | 29 | |||
229 | 30 | def decode(self, key: str, value: str) -> Any: | ||
230 | 31 | raise NotImplementedError("decode") | ||
231 | 32 | |||
232 | 33 | |||
233 | 34 | class _ObjectABCMeta(type(ops.framework.Object), type(collections.abc.MutableMapping)): | ||
234 | 35 | """This metaclass can go once the Operator Framework drops Python 3.5 support. | ||
235 | 36 | |||
236 | 37 | Per ops.framework._Metaclass docstring. | ||
237 | 38 | """ | ||
238 | 39 | |||
239 | 40 | pass | ||
240 | 41 | |||
241 | 42 | |||
242 | 43 | class _PeerData(ops.framework.Object, collections.abc.MutableMapping, metaclass=_ObjectABCMeta): | ||
243 | 44 | """A bag of data shared between peer units. | ||
244 | 45 | |||
245 | 46 | Only the leader can set data. All peer units can read. | ||
246 | 47 | """ | ||
247 | 48 | |||
248 | 49 | def __init__(self, parent: ops.framework.Object, key: str, _store: MutableMapping, _codec: _Codec): | ||
249 | 50 | super().__init__(parent, key) | ||
250 | 51 | self._store = _store | ||
251 | 52 | self._codec = _codec | ||
252 | 53 | self._prefix = self.handle.path | ||
253 | 54 | |||
254 | 55 | def _prefixed_key(self, key: str) -> str: | ||
255 | 56 | return self._prefix + "/" + key | ||
256 | 57 | |||
257 | 58 | def __getitem__(self, key: str) -> Any: | ||
258 | 59 | if not isinstance(key, str): | ||
259 | 60 | raise TypeError(f"key must be a string, got {repr(key)} {type(key)}") | ||
260 | 61 | raw = self._store[self._prefixed_key(key)] | ||
261 | 62 | return self._codec.decode(key, raw) | ||
262 | 63 | |||
263 | 64 | def __setitem__(self, key: str, value: Any) -> None: | ||
264 | 65 | if not isinstance(key, str): | ||
265 | 66 | raise TypeError(f"key must be a string, got {repr(key)} {type(key)}") | ||
266 | 67 | if not self.model.unit.is_leader(): | ||
267 | 68 | raise RuntimeError("non-leader attempting to set peer data") | ||
268 | 69 | self._store[self._prefixed_key(key)] = self._codec.encode(value) | ||
269 | 70 | |||
270 | 71 | def __delitem__(self, key: str) -> None: | ||
271 | 72 | if not isinstance(key, str): | ||
272 | 73 | raise TypeError(f"key must be a string, got {repr(key)} {type(key)}") | ||
273 | 74 | if not self.model.unit.is_leader(): | ||
274 | 75 | raise RuntimeError("non-leader attempting to set peer data") | ||
275 | 76 | del self._store[self._prefixed_key(key)] | ||
276 | 77 | |||
277 | 78 | def __iter__(self) -> Iterable[str]: | ||
278 | 79 | return iter(self._store) | ||
279 | 80 | |||
280 | 81 | def __len__(self) -> int: | ||
281 | 82 | return len(self._store) | ||
282 | 83 | |||
283 | 84 | |||
284 | 85 | class LegacyLeaderData(_PeerData): | ||
285 | 86 | """Raw Juju Leadership settings, a bag of data shared between peers. | ||
286 | 87 | |||
287 | 88 | Only the leader can set data. All peer units can read. | ||
288 | 89 | |||
289 | 90 | Behavior matches the Juju leader-get and leader-set tools; keys and | ||
290 | 91 | values must be strings, Setting an value to the empty string is the | ||
291 | 92 | same as deleting the entry, and accessing a missing entry will | ||
292 | 93 | return an empty string. | ||
293 | 94 | |||
294 | 95 | This class provides access to legacy Juju Leadership data, and namespace | ||
295 | 96 | collisions may occur if multiple components attempt to use the same key. | ||
296 | 97 | """ | ||
297 | 98 | |||
298 | 99 | def __init__(self, parent, key=""): | ||
299 | 100 | super().__init__(parent, key, LeadershipSettings(), _RawCodec()) | ||
300 | 101 | |||
301 | 102 | def _prefixed_key(self, key: str) -> str: | ||
302 | 103 | return key | ||
303 | 104 | |||
304 | 105 | |||
305 | 106 | class RawLeaderData(_PeerData): | ||
306 | 107 | """Raw Juju Leadership settings, a bag of data shared between peers. | ||
307 | 108 | |||
308 | 109 | Only the leader can set data. All peer units can read. | ||
309 | 110 | |||
310 | 111 | Behavior matches the Juju leader-get and leader-set tools; keys and | ||
311 | 112 | values must be strings, Setting an value to the empty string is the | ||
312 | 113 | same as deleting the entry, and accessing a missing entry will | ||
313 | 114 | return an empty string. | ||
314 | 115 | |||
315 | 116 | Keys are automatically prefixed to avoid namespace collisions in the | ||
316 | 117 | Juju Leadership settings. | ||
317 | 118 | """ | ||
318 | 119 | |||
319 | 120 | def __init__(self, parent, key=""): | ||
320 | 121 | super().__init__(parent, key, LeadershipSettings(), _RawCodec()) | ||
321 | 122 | |||
322 | 123 | |||
323 | 124 | class RichLeaderData(_PeerData): | ||
324 | 125 | """Encoded Juju Leadership settings, a bag of data shared between peers. | ||
325 | 126 | |||
326 | 127 | Only the leader can set data. All peer units can read. | ||
327 | 128 | |||
328 | 129 | Operates as a standard Python MutableMapping. Keys must be strings. | ||
329 | 130 | Values may be anything that the yaml library can marshal. | ||
330 | 131 | |||
331 | 132 | Keys are automatically prefixed to avoid namespace collisions in the | ||
332 | 133 | Juju Leadership settings. | ||
333 | 134 | """ | ||
334 | 135 | |||
335 | 136 | def __init__(self, parent, key=""): | ||
336 | 137 | super().__init__(parent, key, LeadershipSettings(), _YAMLCodec()) | ||
337 | 138 | |||
338 | 139 | |||
339 | 140 | class _YAMLCodec(object): | ||
340 | 141 | def encode(self, value: Any) -> str: | ||
341 | 142 | return yaml.safe_dump(value) | ||
342 | 143 | |||
343 | 144 | def decode(self, key: str, value: str) -> Any: | ||
344 | 145 | if not value: | ||
345 | 146 | # Key never existed or was deleted. If set to | ||
346 | 147 | # empty string or none, value will contain | ||
347 | 148 | # the YAML representation. | ||
348 | 149 | raise KeyError(key) | ||
349 | 150 | return yaml.safe_load(value) | ||
350 | 151 | |||
351 | 152 | |||
352 | 153 | class _RawCodec(object): | ||
353 | 154 | def encode(self, value: Any) -> str: | ||
354 | 155 | if not isinstance(value, str): | ||
355 | 156 | raise TypeError(f"{self.__class__.__name__} only supports str values, got {type(value)}") | ||
356 | 157 | return value | ||
357 | 158 | |||
358 | 159 | def decode(self, value: str) -> Any: | ||
359 | 160 | return value | ||
360 | 161 | |||
361 | 162 | |||
362 | 163 | class LeadershipSettings(collections.abc.MutableMapping): | ||
363 | 164 | """Juju Leadership Settings data. | ||
364 | 165 | |||
365 | 166 | This class provides direct access to the Juju Leadership Settings, | ||
366 | 167 | a bag of data shared between peer units. Only the leader can set | ||
367 | 168 | items. Keys all share the same namespace, so beware of collisions. | ||
368 | 169 | |||
369 | 170 | This MutableMapping implements Juju behavior. Only strings are | ||
370 | 171 | supported as keys and values. Deleting an entry is the same as | ||
371 | 172 | setting it to the empty string. Attempting to read a missing | ||
372 | 173 | key will return the empty string (this class will never raise | ||
373 | 174 | a KeyError). | ||
374 | 175 | """ | ||
375 | 176 | |||
376 | 177 | __cls_cache = None | ||
377 | 178 | |||
378 | 179 | @property | ||
379 | 180 | def _cache_loaded(self) -> bool: | ||
380 | 181 | return self.__class__.__cls_cache is not None | ||
381 | 182 | |||
382 | 183 | @property | ||
383 | 184 | def _cache(self) -> Dict[str, str]: | ||
384 | 185 | # There might be multiple instances of LeadershipSettings, but | ||
385 | 186 | # the backend is shared, so the cache needs to be a class | ||
386 | 187 | # attribute. | ||
387 | 188 | cls = self.__class__ | ||
388 | 189 | if cls.__cls_cache is None: | ||
389 | 190 | cmd = ["leader-get", "--format=yaml"] | ||
390 | 191 | cls.__cls_cache = yaml.safe_load(subprocess.check_output(cmd).decode("UTF-8")) or {} | ||
391 | 192 | return cls.__cls_cache | ||
392 | 193 | |||
393 | 194 | def __getitem__(self, key: str) -> str: | ||
394 | 195 | return self._cache.get(key, "") | ||
395 | 196 | |||
396 | 197 | def __setitem__(self, key: str, value: str): | ||
397 | 198 | if "=" in key: | ||
398 | 199 | # Leave other validation to the leader-set tool | ||
399 | 200 | raise RuntimeError(f"LeadershipSettings keys may not contain '=', got {key}") | ||
400 | 201 | if value is None: | ||
401 | 202 | value = "" | ||
402 | 203 | cmd = ["leader-set", f"{key}={value}"] | ||
403 | 204 | subprocess.check_call(cmd) | ||
404 | 205 | if self._cache_loaded: | ||
405 | 206 | if value == "": | ||
406 | 207 | del self._cache[key] | ||
407 | 208 | else: | ||
408 | 209 | self._cache[key] = value | ||
409 | 210 | |||
410 | 211 | def __delitem__(self, key: str): | ||
411 | 212 | self[key] = "" | ||
412 | 213 | |||
413 | 214 | def __iter__(self) -> Iterable[str]: | ||
414 | 215 | return iter(self._cache) | ||
415 | 216 | |||
416 | 217 | def __len__(self) -> int: | ||
417 | 218 | return len(self._cache) | ||
418 | diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py | |||
419 | index 717c937..00aba40 100644 | |||
420 | --- a/tests/unit/test_charm.py | |||
421 | +++ b/tests/unit/test_charm.py | |||
422 | @@ -2,13 +2,13 @@ | |||
423 | 2 | # See LICENSE file for licensing details. | 2 | # See LICENSE file for licensing details. |
424 | 3 | 3 | ||
425 | 4 | import unittest | 4 | import unittest |
426 | 5 | from unittest.mock import patch | ||
427 | 5 | 6 | ||
428 | 6 | from charm import OpenLDAPK8sCharm | 7 | from charm import OpenLDAPK8sCharm |
429 | 7 | from collections import namedtuple | 8 | from collections import namedtuple |
430 | 8 | from ops import testing | 9 | from ops import testing |
431 | 9 | from ops.model import ( | 10 | from ops.model import ( |
432 | 10 | ActiveStatus, | 11 | ActiveStatus, |
433 | 11 | BlockedStatus, | ||
434 | 12 | WaitingStatus, | 12 | WaitingStatus, |
435 | 13 | ) | 13 | ) |
436 | 14 | 14 | ||
437 | @@ -18,34 +18,24 @@ CONFIG_ALL = { | |||
438 | 18 | 'image_path': 'example.com/openldap:latest', | 18 | 'image_path': 'example.com/openldap:latest', |
439 | 19 | 'image_username': 'image_user', | 19 | 'image_username': 'image_user', |
440 | 20 | 'image_password': 'image_pass', | 20 | 'image_password': 'image_pass', |
441 | 21 | 'admin_password': 'badmin_password', | ||
442 | 22 | } | 21 | } |
443 | 23 | 22 | ||
444 | 24 | CONFIG_IMAGE_NO_CREDS = { | 23 | CONFIG_IMAGE_NO_CREDS = { |
445 | 25 | 'image_path': 'example.com/openldap:latest', | 24 | 'image_path': 'example.com/openldap:latest', |
446 | 26 | 'image_username': '', | 25 | 'image_username': '', |
447 | 27 | 'image_password': '', | 26 | 'image_password': '', |
448 | 28 | 'admin_password': 'badmin_password', | ||
449 | 29 | } | 27 | } |
450 | 30 | 28 | ||
451 | 31 | CONFIG_IMAGE_NO_IMAGE = { | 29 | CONFIG_IMAGE_NO_IMAGE = { |
452 | 32 | 'image_path': '', | 30 | 'image_path': '', |
453 | 33 | 'image_username': '', | 31 | 'image_username': '', |
454 | 34 | 'image_password': '', | 32 | 'image_password': '', |
455 | 35 | 'admin_password': 'badmin_password', | ||
456 | 36 | } | 33 | } |
457 | 37 | 34 | ||
458 | 38 | CONFIG_IMAGE_NO_PASSWORD = { | 35 | CONFIG_IMAGE_NO_PASSWORD = { |
459 | 39 | 'image_path': 'example.com/openldap:latest', | 36 | 'image_path': 'example.com/openldap:latest', |
460 | 40 | 'image_username': 'production', | 37 | 'image_username': 'production', |
461 | 41 | 'image_password': '', | 38 | 'image_password': '', |
462 | 42 | 'admin_password': 'badmin_password', | ||
463 | 43 | } | ||
464 | 44 | |||
465 | 45 | CONFIG_NO_ADMIN_PASSWORD = { | ||
466 | 46 | 'image_path': 'example.com/openldap:latest', | ||
467 | 47 | 'image_username': 'production', | ||
468 | 48 | 'image_password': '', | ||
469 | 49 | } | 39 | } |
470 | 50 | 40 | ||
471 | 51 | DB_URI = { | 41 | DB_URI = { |
472 | @@ -63,12 +53,6 @@ class TestOpenLDAPK8sCharmHooksDisabled(unittest.TestCase): | |||
473 | 63 | self.harness.begin() | 53 | self.harness.begin() |
474 | 64 | self.harness.disable_hooks() | 54 | self.harness.disable_hooks() |
475 | 65 | 55 | ||
476 | 66 | def test_check_for_config_problems(self): | ||
477 | 67 | """Config problems as a string.""" | ||
478 | 68 | self.harness.update_config(CONFIG_NO_ADMIN_PASSWORD) | ||
479 | 69 | expected = 'required setting(s) empty: admin_password' | ||
480 | 70 | self.assertEqual(self.harness.charm._check_for_config_problems(), expected) | ||
481 | 71 | |||
482 | 72 | def test_make_pod_config(self): | 56 | def test_make_pod_config(self): |
483 | 73 | """Make basic, correct pod config.""" | 57 | """Make basic, correct pod config.""" |
484 | 74 | self.harness.update_config(CONFIG_IMAGE_NO_CREDS) | 58 | self.harness.update_config(CONFIG_IMAGE_NO_CREDS) |
485 | @@ -81,66 +65,58 @@ class TestOpenLDAPK8sCharmHooksDisabled(unittest.TestCase): | |||
486 | 81 | 'POSTGRES_PORT': '5432', | 65 | 'POSTGRES_PORT': '5432', |
487 | 82 | 'LDAP_ADMIN_PASSWORD': 'badmin_password', | 66 | 'LDAP_ADMIN_PASSWORD': 'badmin_password', |
488 | 83 | } | 67 | } |
504 | 84 | self.assertEqual(self.harness.charm._make_pod_config(), expected) | 68 | with patch.object(self.harness.charm, "get_admin_password") as get_admin_password: |
505 | 85 | 69 | get_admin_password.return_value = 'badmin_password' | |
506 | 86 | def test_make_pod_config_no_password(self): | 70 | self.assertEqual(self.harness.charm._make_pod_config(), expected) |
492 | 87 | """Missing admin password in config shouldn't explode at least.""" | ||
493 | 88 | self.harness.update_config(CONFIG_NO_ADMIN_PASSWORD) | ||
494 | 89 | self.harness.charm._state.postgres = DB_URI | ||
495 | 90 | expected = { | ||
496 | 91 | 'LDAP_ADMIN_PASSWORD': '', | ||
497 | 92 | 'POSTGRES_NAME': 'openldap', | ||
498 | 93 | 'POSTGRES_USER': 'ldap_user', | ||
499 | 94 | 'POSTGRES_PASSWORD': 'ldap_password', | ||
500 | 95 | 'POSTGRES_HOST': '1.1.1.1', | ||
501 | 96 | 'POSTGRES_PORT': '5432', | ||
502 | 97 | } | ||
503 | 98 | self.assertEqual(self.harness.charm._make_pod_config(), expected) | ||
507 | 99 | 71 | ||
508 | 100 | def test_make_pod_spec(self): | 72 | def test_make_pod_spec(self): |
509 | 101 | """Basic, correct pod spec.""" | 73 | """Basic, correct pod spec.""" |
510 | 102 | self.harness.update_config(CONFIG_ALL) | 74 | self.harness.update_config(CONFIG_ALL) |
511 | 103 | self.harness.charm._state.postgres = DB_URI | 75 | self.harness.charm._state.postgres = DB_URI |
531 | 104 | expected = { | 76 | with patch.object(self.harness.charm, "get_admin_password") as get_admin_password: |
532 | 105 | 'version': 3, | 77 | get_admin_password.return_value = 'badmin_password' |
533 | 106 | 'containers': [ | 78 | expected = { |
534 | 107 | { | 79 | 'version': 3, |
535 | 108 | 'name': 'openldap', | 80 | 'containers': [ |
536 | 109 | 'imageDetails': { | 81 | { |
537 | 110 | 'imagePath': 'example.com/openldap:latest', | 82 | 'name': 'openldap', |
538 | 111 | 'username': 'image_user', | 83 | 'imageDetails': { |
539 | 112 | 'password': 'image_pass', | 84 | 'imagePath': 'example.com/openldap:latest', |
540 | 113 | }, | 85 | 'username': 'image_user', |
541 | 114 | 'ports': [{'containerPort': 389, 'protocol': 'TCP'}], | 86 | 'password': 'image_pass', |
542 | 115 | 'envConfig': self.harness.charm._make_pod_config(), | 87 | }, |
543 | 116 | 'kubernetes': { | 88 | 'ports': [{'containerPort': 389, 'protocol': 'TCP'}], |
544 | 117 | 'readinessProbe': {'tcpSocket': {'port': 389}}, | 89 | 'envConfig': self.harness.charm._make_pod_config(), |
545 | 118 | }, | 90 | 'kubernetes': { |
546 | 119 | } | 91 | 'readinessProbe': {'tcpSocket': {'port': 389}}, |
547 | 120 | ], | 92 | }, |
548 | 121 | } | 93 | } |
549 | 122 | self.assertEqual(self.harness.charm._make_pod_spec(), expected) | 94 | ], |
550 | 95 | } | ||
551 | 96 | self.assertEqual(self.harness.charm._make_pod_spec(), expected) | ||
552 | 123 | 97 | ||
553 | 124 | def test_make_pod_spec_no_image_creds(self): | 98 | def test_make_pod_spec_no_image_creds(self): |
554 | 125 | self.harness.update_config(CONFIG_IMAGE_NO_CREDS) | 99 | self.harness.update_config(CONFIG_IMAGE_NO_CREDS) |
555 | 126 | self.harness.charm._state.postgres = DB_URI | 100 | self.harness.charm._state.postgres = DB_URI |
573 | 127 | expected = { | 101 | with patch.object(self.harness.charm, "get_admin_password") as get_admin_password: |
574 | 128 | 'version': 3, | 102 | get_admin_password.return_value = 'badmin_password' |
575 | 129 | 'containers': [ | 103 | expected = { |
576 | 130 | { | 104 | 'version': 3, |
577 | 131 | 'name': 'openldap', | 105 | 'containers': [ |
578 | 132 | 'imageDetails': { | 106 | { |
579 | 133 | 'imagePath': 'example.com/openldap:latest', | 107 | 'name': 'openldap', |
580 | 134 | }, | 108 | 'imageDetails': { |
581 | 135 | 'ports': [{'containerPort': 389, 'protocol': 'TCP'}], | 109 | 'imagePath': 'example.com/openldap:latest', |
582 | 136 | 'envConfig': self.harness.charm._make_pod_config(), | 110 | }, |
583 | 137 | 'kubernetes': { | 111 | 'ports': [{'containerPort': 389, 'protocol': 'TCP'}], |
584 | 138 | 'readinessProbe': {'tcpSocket': {'port': 389}}, | 112 | 'envConfig': self.harness.charm._make_pod_config(), |
585 | 139 | }, | 113 | 'kubernetes': { |
586 | 140 | } | 114 | 'readinessProbe': {'tcpSocket': {'port': 389}}, |
587 | 141 | ], | 115 | }, |
588 | 142 | } | 116 | } |
589 | 143 | self.assertEqual(self.harness.charm._make_pod_spec(), expected) | 117 | ], |
590 | 118 | } | ||
591 | 119 | self.assertEqual(self.harness.charm._make_pod_spec(), expected) | ||
592 | 144 | 120 | ||
593 | 145 | def test_configure_pod_no_postgres_relation(self): | 121 | def test_configure_pod_no_postgres_relation(self): |
594 | 146 | """Check that we block correctly without a Postgres relation.""" | 122 | """Check that we block correctly without a Postgres relation.""" |
595 | @@ -161,17 +137,6 @@ class TestOpenLDAPK8sCharmHooksDisabled(unittest.TestCase): | |||
596 | 161 | self.harness.charm._configure_pod(mock_event) | 137 | self.harness.charm._configure_pod(mock_event) |
597 | 162 | self.assertEqual(self.harness.charm.unit.status, expected) | 138 | self.assertEqual(self.harness.charm.unit.status, expected) |
598 | 163 | 139 | ||
599 | 164 | def test_configure_pod_config_problems(self): | ||
600 | 165 | """Test pod config with missing juju config options.""" | ||
601 | 166 | mock_event = MagicMock() | ||
602 | 167 | |||
603 | 168 | self.harness.update_config(CONFIG_NO_ADMIN_PASSWORD) | ||
604 | 169 | self.harness.charm._state.postgres = DB_URI | ||
605 | 170 | self.harness.set_leader(True) | ||
606 | 171 | expected = BlockedStatus('required setting(s) empty: admin_password') | ||
607 | 172 | self.harness.charm._configure_pod(mock_event) | ||
608 | 173 | self.assertEqual(self.harness.charm.unit.status, expected) | ||
609 | 174 | |||
610 | 175 | def test_configure_pod(self): | 140 | def test_configure_pod(self): |
611 | 176 | """Test pod configuration with everything working appropriately.""" | 141 | """Test pod configuration with everything working appropriately.""" |
612 | 177 | mock_event = MagicMock() | 142 | mock_event = MagicMock() |
613 | @@ -180,8 +145,10 @@ class TestOpenLDAPK8sCharmHooksDisabled(unittest.TestCase): | |||
614 | 180 | self.harness.charm._state.postgres = DB_URI | 145 | self.harness.charm._state.postgres = DB_URI |
615 | 181 | self.harness.set_leader(True) | 146 | self.harness.set_leader(True) |
616 | 182 | expected = ActiveStatus() | 147 | expected = ActiveStatus() |
619 | 183 | self.harness.charm._configure_pod(mock_event) | 148 | with patch.object(self.harness.charm, "get_admin_password") as get_admin_password: |
620 | 184 | self.assertEqual(self.harness.charm.unit.status, expected) | 149 | get_admin_password.return_value = 'badmin_password' |
621 | 150 | self.harness.charm._configure_pod(mock_event) | ||
622 | 151 | self.assertEqual(self.harness.charm.unit.status, expected) | ||
623 | 185 | 152 | ||
624 | 186 | def test_on_database_relation_joined(self): | 153 | def test_on_database_relation_joined(self): |
625 | 187 | mock_event = MagicMock() | 154 | mock_event = MagicMock() |
626 | @@ -206,5 +173,21 @@ class TestOpenLDAPK8sCharmHooksDisabled(unittest.TestCase): | |||
627 | 206 | mock_event.master = master | 173 | mock_event.master = master |
628 | 207 | mock_event.database = "openldap" | 174 | mock_event.database = "openldap" |
629 | 208 | 175 | ||
632 | 209 | self.harness.charm._on_master_changed(mock_event) | 176 | with patch.object(self.harness.charm, "get_admin_password") as get_admin_password: |
633 | 210 | self.assertEqual(self.harness.charm._state.postgres['dbname'], "openldap") | 177 | get_admin_password.return_value = 'badmin_password' |
634 | 178 | self.harness.charm._on_master_changed(mock_event) | ||
635 | 179 | self.assertEqual(self.harness.charm._state.postgres['dbname'], "openldap") | ||
636 | 180 | |||
637 | 181 | def test_on_get_admin_password_action(self): | ||
638 | 182 | mock_event = MagicMock() | ||
639 | 183 | |||
640 | 184 | self.harness.update_config(CONFIG_ALL) | ||
641 | 185 | with patch.object(self.harness.charm, "get_admin_password") as get_admin_password: | ||
642 | 186 | get_admin_password.return_value = 'badmin_password' | ||
643 | 187 | self.harness.charm._on_get_admin_password_action(mock_event) | ||
644 | 188 | mock_event.set_results.assert_called_with({"admin-password": "badmin_password"}) | ||
645 | 189 | # And now return an empty result. | ||
646 | 190 | get_admin_password.return_value = "" | ||
647 | 191 | mock_event.reset_mock() | ||
648 | 192 | self.harness.charm._on_get_admin_password_action(mock_event) | ||
649 | 193 | mock_event.fail.assert_called_with("LDAP admin password has not yet been set, please retry later.") |
This merge proposal is being monitored by mergebot. Change the status to Approved to merge.