Skip to content

Commit 5bf84f7

Browse files
author
Amanda H. L. de Andrade Katz
authored
Add action to register user (#15)
1 parent bb3f738 commit 5bf84f7

23 files changed

Lines changed: 1019 additions & 312 deletions

.wokeignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
lib/charms/nginx_ingress_integrator/v0/nginx_route.py
2+
pyproject.toml

actions.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,19 @@ reset-instance:
66
Set a new server_name before running this action.
77
Once a server_name is configured, you must start a new instance if you wish a different one.
88
This actions will erase all data and create a instance with the new server_name.
9+
register-user:
10+
description: |
11+
Registers a user for the Synapse server.
12+
You need to supply a user name and whether that user should be an admin or not.
13+
properties:
14+
username:
15+
description: |
16+
When not using SSO, a user name is needed
17+
for the creation of a matrix account.
18+
type: string
19+
admin:
20+
description: Whether to create an admin user.
21+
type: boolean
22+
default: false
23+
required:
24+
- username

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ markers = [
2222
"requires_secrets: mark tests that require external secrets"
2323
]
2424

25+
[tool.pylint.'MESSAGES CONTROL']
26+
extension-pkg-whitelist = "pydantic"
27+
2528
# Formatting tools configuration
2629
[tool.black]
2730
line-length = 99

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
jsonschema == 4.17.3
2-
ops >= 2.2.0
2+
ops >= 2.4.1
33
pydantic == 1.10.10
44
ops-lib-pgsql >= 1.4
55
psycopg2-binary == 2.9.6
6+
requests>=2,<3

src/actions/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Copyright 2023 Canonical Ltd.
2+
# See LICENSE file for licensing details.
3+
4+
"""Actions package is used to run actions provided by the charm."""
5+
6+
from .register_user import RegisterUserError, register_user # noqa: F401
7+
8+
# Exporting methods to be used for another modules
9+
from .reset_instance import ResetInstanceError, reset_instance # noqa: F401

src/actions/register_user.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
#!/usr/bin/env python3
2+
3+
# Copyright 2023 Canonical Ltd.
4+
# See LICENSE file for licensing details.
5+
6+
"""Module to interact with Register User action."""
7+
8+
import logging
9+
10+
import ops
11+
12+
# pydantic is causing this no-name-in-module problem
13+
from pydantic import ValidationError # pylint: disable=no-name-in-module,import-error
14+
15+
import synapse
16+
from user import User
17+
18+
logger = logging.getLogger(__name__)
19+
20+
21+
class RegisterUserError(Exception):
22+
"""Exception raised when something fails while running register-user.
23+
24+
Attrs:
25+
msg (str): Explanation of the error.
26+
"""
27+
28+
def __init__(self, msg: str):
29+
"""Initialize a new instance of the RegisterUserError exception.
30+
31+
Args:
32+
msg (str): Explanation of the error.
33+
"""
34+
self.msg = msg
35+
36+
37+
def register_user(container: ops.Container, username: str, admin: bool) -> User:
38+
"""Run register user action.
39+
40+
Args:
41+
container: Container of the charm.
42+
username: username to be registered.
43+
admin: if user is admin.
44+
45+
Raises:
46+
RegisterUserError: if something goes wrong while registering the user.
47+
48+
Returns:
49+
User with password registered.
50+
"""
51+
try:
52+
registration_shared_secret = synapse.get_registration_shared_secret(container=container)
53+
if registration_shared_secret is None:
54+
raise RegisterUserError(
55+
"registration_shared_secret was not found, please check the logs"
56+
)
57+
user = User(username=username, admin=admin)
58+
synapse.register_user(registration_shared_secret=registration_shared_secret, user=user)
59+
return user
60+
except (ValidationError, synapse.APIError) as exc:
61+
raise RegisterUserError(str(exc)) from exc

src/actions/reset_instance.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
#!/usr/bin/env python3
2+
3+
# Copyright 2023 Canonical Ltd.
4+
# See LICENSE file for licensing details.
5+
6+
"""Module to interact with Reset Instance action."""
7+
8+
import logging
9+
import typing
10+
11+
import ops
12+
import psycopg2
13+
14+
import synapse
15+
from charm_state import CharmState
16+
from database_client import DatabaseClient, DatasourcePostgreSQL
17+
18+
logger = logging.getLogger(__name__)
19+
20+
21+
class ResetInstanceError(Exception):
22+
"""Exception raised when something fails while running reset-instance.
23+
24+
Attrs:
25+
msg (str): Explanation of the error.
26+
"""
27+
28+
def __init__(self, msg: str):
29+
"""Initialize a new instance of the ResetInstanceError exception.
30+
31+
Args:
32+
msg (str): Explanation of the error.
33+
"""
34+
self.msg = msg
35+
36+
37+
def reset_instance(
38+
container: ops.Container,
39+
charm_state: CharmState,
40+
datasource: typing.Optional[DatasourcePostgreSQL],
41+
) -> None:
42+
"""Run reset instance action.
43+
44+
Args:
45+
container: Container of the charm.
46+
charm_state: charm state from the charm.
47+
datasource: datasource to interact with the database.
48+
49+
Raises:
50+
ResetInstanceError: if something goes wrong while resetting the instance.
51+
"""
52+
try:
53+
if datasource is not None:
54+
logger.info("Erase Synapse database")
55+
# Connecting to template1 to make it possible to erase the database.
56+
# Otherwise PostgreSQL will prevent it if there are open connections.
57+
db_client = DatabaseClient(datasource=datasource, alternative_database="template1")
58+
db_client.erase()
59+
synapse.execute_migrate_config(container=container, charm_state=charm_state)
60+
except (psycopg2.Error, synapse.WorkloadError) as exc:
61+
raise ResetInstanceError(str(exc)) from exc

src/charm.py

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,16 @@
99
import typing
1010

1111
import ops
12-
import psycopg2
1312
from charms.nginx_ingress_integrator.v0.nginx_route import require_nginx_route
1413
from charms.traefik_k8s.v1.ingress import IngressPerAppRequirer
1514
from ops.charm import ActionEvent
1615
from ops.main import main
1716

17+
import actions
1818
from charm_state import CharmConfigInvalidError, CharmState
1919
from constants import SYNAPSE_CONTAINER_NAME, SYNAPSE_PORT
20-
from database_client import DatabaseClient
2120
from database_observer import DatabaseObserver
22-
from pebble import PebbleService
23-
from synapse import CommandMigrateConfigError, ServerNameModifiedError, Synapse
21+
from pebble import PebbleService, PebbleServiceError
2422

2523
logger = logging.getLogger(__name__)
2624

@@ -41,8 +39,7 @@ def __init__(self, *args: typing.Any) -> None:
4139
except CharmConfigInvalidError as exc:
4240
self.model.unit.status = ops.BlockedStatus(exc.msg)
4341
return
44-
self._synapse = Synapse(charm_state=self._charm_state)
45-
self.pebble_service = PebbleService(synapse=self._synapse)
42+
self.pebble_service = PebbleService(charm_state=self._charm_state)
4643
# service-hostname is a required field so we're hardcoding to the same
4744
# value as service-name. service-hostname should be set via Nginx
4845
# Ingress Integrator charm config.
@@ -64,6 +61,7 @@ def __init__(self, *args: typing.Any) -> None:
6461
self.framework.observe(self.on.config_changed, self._on_config_changed)
6562
self.framework.observe(self.on.reset_instance_action, self._on_reset_instance_action)
6663
self.framework.observe(self.on.synapse_pebble_ready, self._on_pebble_ready)
64+
self.framework.observe(self.on.register_user_action, self._on_register_user_action)
6765

6866
def change_config(self, _: ops.HookEvent) -> None:
6967
"""Change configuration."""
@@ -74,12 +72,7 @@ def change_config(self, _: ops.HookEvent) -> None:
7472
self.model.unit.status = ops.MaintenanceStatus("Configuring Synapse")
7573
try:
7674
self.pebble_service.change_config(container)
77-
except (
78-
CharmConfigInvalidError,
79-
CommandMigrateConfigError,
80-
ops.pebble.PathError,
81-
ServerNameModifiedError,
82-
) as exc:
75+
except PebbleServiceError as exc:
8376
self.model.unit.status = ops.BlockedStatus(str(exc))
8477
return
8578
self.model.unit.status = ops.ActiveStatus()
@@ -120,24 +113,39 @@ def _on_reset_instance_action(self, event: ActionEvent) -> None:
120113
self.model.unit.status = ops.MaintenanceStatus("Resetting Synapse instance")
121114
self.pebble_service.reset_instance(container)
122115
datasource = self.database.get_relation_as_datasource()
123-
if datasource is not None:
124-
logger.info("Erase Synapse database")
125-
# Connecting to template1 to make it possible to erase the database.
126-
# Otherwise PostgreSQL will prevent it if there are open connections.
127-
db_client = DatabaseClient(datasource=datasource, alternative_database="template1")
128-
db_client.erase()
129-
self._synapse.execute_migrate_config(container)
130-
logger.info("Start Synapse database")
116+
actions.reset_instance(
117+
container=container, charm_state=self._charm_state, datasource=datasource
118+
)
119+
logger.info("Start Synapse")
131120
self.pebble_service.replan(container)
132121
results["reset-instance"] = True
133-
except (psycopg2.Error, ops.pebble.PathError, CommandMigrateConfigError) as exc:
122+
except (PebbleServiceError, actions.ResetInstanceError) as exc:
134123
self.model.unit.status = ops.BlockedStatus(str(exc))
135124
event.fail(str(exc))
136125
return
137-
# results is a dict and set_results expects _SerializedData
138-
event.set_results(results) # type: ignore[arg-type]
126+
event.set_results(results)
139127
self.model.unit.status = ops.ActiveStatus()
140128

129+
def _on_register_user_action(self, event: ActionEvent) -> None:
130+
"""Reset instance and report action result.
131+
132+
Args:
133+
event: Event triggering the reset instance action.
134+
"""
135+
container = self.unit.get_container(SYNAPSE_CONTAINER_NAME)
136+
if not container.can_connect():
137+
self.unit.status = ops.MaintenanceStatus("Waiting for pebble")
138+
return
139+
try:
140+
user = actions.register_user(
141+
container=container, username=event.params["username"], admin=event.params["admin"]
142+
)
143+
except actions.RegisterUserError as exc:
144+
event.fail(str(exc))
145+
return
146+
results = {"register-user": True, "user-password": user.password}
147+
event.set_results(results)
148+
141149

142150
if __name__ == "__main__": # pragma: nocover
143151
main(SynapseCharm)

src/pebble.py

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,27 +10,44 @@
1010

1111
import ops
1212

13+
import synapse
14+
from charm_state import CharmState
1315
from constants import (
1416
CHECK_READY_NAME,
1517
SYNAPSE_COMMAND_PATH,
1618
SYNAPSE_CONTAINER_NAME,
1719
SYNAPSE_SERVICE_NAME,
1820
)
19-
from synapse import Synapse
2021

2122
logger = logging.getLogger(__name__)
2223

2324

25+
class PebbleServiceError(Exception):
26+
"""Exception raised when something fails while interacting with Pebble.
27+
28+
Attrs:
29+
msg (str): Explanation of the error.
30+
"""
31+
32+
def __init__(self, msg: str):
33+
"""Initialize a new instance of the PebbleServiceError exception.
34+
35+
Args:
36+
msg (str): Explanation of the error.
37+
"""
38+
self.msg = msg
39+
40+
2441
class PebbleService:
2542
"""The charm pebble service manager."""
2643

27-
def __init__(self, synapse: Synapse):
44+
def __init__(self, charm_state: CharmState):
2845
"""Initialize the pebble service.
2946
3047
Args:
31-
synapse: Instance to interact with Synapse.
48+
charm_state: Instance of CharmState.
3249
"""
33-
self._synapse = synapse
50+
self._charm_state = charm_state
3451

3552
def replan(self, container: ops.model.Container) -> None:
3653
"""Replan the pebble service.
@@ -46,27 +63,39 @@ def change_config(self, container: ops.model.Container) -> None:
4663
4764
Args:
4865
container: Charm container.
66+
67+
Raises:
68+
PebbleServiceError: if something goes wrong while interacting with Pebble.
4969
"""
50-
self._synapse.execute_migrate_config(container)
51-
self.replan(container)
70+
try:
71+
synapse.execute_migrate_config(container=container, charm_state=self._charm_state)
72+
self.replan(container)
73+
except (synapse.WorkloadError, ops.pebble.PathError) as exc:
74+
raise PebbleServiceError(str(exc)) from exc
5275

5376
def reset_instance(self, container: ops.model.Container) -> None:
5477
"""Reset instance.
5578
5679
Args:
5780
container: Charm container.
81+
82+
Raises:
83+
PebbleServiceError: if something goes wrong while interacting with Pebble.
5884
"""
5985
# This is needed in the case of relation with Postgresql.
6086
# If there is open connections it won't be possible to drop the database.
61-
logger.info("Replan service to not restart")
62-
container.add_layer(
63-
SYNAPSE_CONTAINER_NAME, self._pebble_layer_without_restart, combine=True
64-
)
65-
container.replan()
66-
logger.info("Stop Synapse instance")
67-
container.stop(SYNAPSE_SERVICE_NAME)
68-
logger.info("Erase Synapse data")
69-
self._synapse.reset_instance(container)
87+
try:
88+
logger.info("Replan service to not restart")
89+
container.add_layer(
90+
SYNAPSE_CONTAINER_NAME, self._pebble_layer_without_restart, combine=True
91+
)
92+
container.replan()
93+
logger.info("Stop Synapse instance")
94+
container.stop(SYNAPSE_SERVICE_NAME)
95+
logger.info("Erase Synapse data")
96+
synapse.reset_instance(container)
97+
except ops.pebble.PathError as exc:
98+
raise PebbleServiceError(str(exc)) from exc
7099

71100
@property
72101
def _pebble_layer(self) -> ops.pebble.LayerDict:
@@ -80,11 +109,11 @@ def _pebble_layer(self) -> ops.pebble.LayerDict:
80109
"summary": "Synapse application service",
81110
"startup": "enabled",
82111
"command": SYNAPSE_COMMAND_PATH,
83-
"environment": self._synapse.synapse_environment(),
112+
"environment": synapse.get_environment(self._charm_state),
84113
}
85114
},
86115
"checks": {
87-
CHECK_READY_NAME: self._synapse.check_ready(),
116+
CHECK_READY_NAME: synapse.check_ready(),
88117
},
89118
}
90119
return typing.cast(ops.pebble.LayerDict, layer)

0 commit comments

Comments
 (0)