Merge ~mthaddon/charm-k8s-openldap/+git/charm-k8s-openldap:password-action into charm-k8s-openldap: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)
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 :

This merge proposal is being monitored by mergebot. Change the status to Approved to merge.

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 82adb10deb3df836125a4b4638ca7a7b54ff99e3

Preview Diff

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

Subscribers

People subscribed via source and target branches

to all changes: