Skip to content

Commit

Permalink
feat: add create-admin action
Browse files Browse the repository at this point in the history
  • Loading branch information
amandahla committed Sep 24, 2024
1 parent 947ea7a commit fcd78be
Show file tree
Hide file tree
Showing 6 changed files with 121 additions and 36 deletions.
43 changes: 23 additions & 20 deletions charmcraft.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Copyright 2024 Canonical Ltd.
# See LICENSE file for licensing details.

type: charm
name: maubot
title: maubot
description: |
Expand All @@ -12,28 +13,17 @@ links:
source: https://github.com/canonical/maubot-operator
contact:
- https://launchpad.net/~canonical-is-devops

resources:
maubot-image:
type: oci-image
description: OCI image for maubot

# build properties
assumes:
- juju >= 3.4
base: [email protected]
build-base: [email protected]
containers:
maubot:
resource: maubot-image
mounts:
- storage: data
location: /data
storage:
data:
type: filesystem

type: charm
base: [email protected]
build-base: [email protected]
platforms:
amd64:

parts:
charm:
build-packages:
Expand All @@ -42,13 +32,26 @@ parts:
- libssl-dev
- pkg-config
- rustc

assumes:
- juju >= 3.4

platforms:
amd64:
requires:
postgresql:
interface: postgresql_client
limit: 1
ingress:
interface: ingress
resources:
maubot-image:
type: oci-image
description: OCI image for Maubot.
storage:
data:
type: filesystem
# actions
actions:
create-admin:
description: Create administrator user to Maubot.
params:
name:
type: string
description: The name of the administrator user.
3 changes: 2 additions & 1 deletion src-docs/charm.py.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ Maubot charm service.
**Global Variables**
---------------
- **MAUBOT_NAME**
- **MAUBOT_CONFIGURATION_PATH**


---

## <kbd>class</kbd> `MaubotCharm`
Maubot charm.

<a href="../src/charm.py#L39"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>
<a href="../src/charm.py#L41"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>

### <kbd>function</kbd> `__init__`

Expand Down
60 changes: 46 additions & 14 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"""Maubot charm service."""

import logging
import secrets
import typing

import ops
Expand All @@ -27,6 +28,7 @@
logger = logging.getLogger(__name__)

MAUBOT_NAME = "maubot"
MAUBOT_CONFIGURATION_PATH = "/data/config.yaml"


class MissingPostgreSQLRelationDataError(Exception):
Expand All @@ -43,49 +45,57 @@ def __init__(self, *args: typing.Any):
args: Arguments passed to the CharmBase parent constructor.
"""
super().__init__(*args)
self.container = self.unit.get_container(MAUBOT_NAME)
self.ingress = IngressPerAppRequirer(self, port=8080)
self.postgresql = DatabaseRequires(
self, relation_name="postgresql", database_name=self.app.name
)
self.framework.observe(self.on.maubot_pebble_ready, self._on_maubot_pebble_ready)
self.framework.observe(self.on.config_changed, self._on_config_changed)
# Actions events handlers
self.framework.observe(self.on.create_admin_action, self._on_create_admin_action)
# Integrations events handlers
self.framework.observe(self.postgresql.on.database_created, self._on_database_created)
self.framework.observe(self.postgresql.on.endpoints_changed, self._on_endpoints_changed)
self.framework.observe(self.ingress.on.ready, self._on_ingress_ready)
self.framework.observe(self.ingress.on.revoked, self._on_ingress_revoked)

def _configure_maubot(self, container: ops.Container) -> None:
"""Configure maubot.
def _get_configuration(self) -> dict:
"""Get Maubot configuration content.
Args:
container: Container of the charm.
Returns:
Maubot configuration file as a dict.
"""
config_content = str(
self.container.pull(MAUBOT_CONFIGURATION_PATH, encoding="utf-8").read()
)
return yaml.safe_load(config_content)

def _configure_maubot(self) -> None:
"""Configure maubot."""
commands = [
["cp", "--update=none", "/example-config.yaml", "/data/config.yaml"],
["cp", "--update=none", "/example-config.yaml", MAUBOT_CONFIGURATION_PATH],
["mkdir", "-p", "/data/plugins", "/data/trash", "/data/dbs"],
]
for command in commands:
process = container.exec(command, combine_stderr=True)
process = self.container.exec(command, combine_stderr=True)
process.wait()
config_content = str(container.pull("/data/config.yaml", encoding="utf-8").read())
config = yaml.safe_load(config_content)
config = self._get_configuration()
config["database"] = self._get_postgresql_credentials()
container.push("/data/config.yaml", yaml.safe_dump(config))
self.container.push("/data/config.yaml", yaml.safe_dump(config))

def _reconcile(self) -> None:
"""Reconcile workload configuration."""
self.unit.status = ops.MaintenanceStatus()
container = self.unit.get_container(MAUBOT_NAME)
if not container.can_connect():
if not self.container.can_connect():
return
try:
self._configure_maubot(container)
self._configure_maubot()
except MissingPostgreSQLRelationDataError:
self.unit.status = ops.BlockedStatus("postgresql integration is required")
return
container.add_layer(MAUBOT_NAME, self._pebble_layer, combine=True)
container.replan()
self.container.add_layer(MAUBOT_NAME, self._pebble_layer, combine=True)
self.container.replan()
self.unit.status = ops.ActiveStatus()

def _on_maubot_pebble_ready(self, _: ops.PebbleReadyEvent) -> None:
Expand All @@ -104,6 +114,28 @@ def _on_ingress_revoked(self, _: IngressPerAppRevokedEvent) -> None:
"""Handle ingress revoked event."""
self._reconcile()

# Actions events handlers
def _on_create_admin_action(self, event: ops.ActionEvent) -> None:
"""Handle delete-profile action.
Args:
event: Action event.
"""
if (
not self.container.can_connect()
or MAUBOT_NAME not in self.container.get_plan().services
or not self.container.get_service(MAUBOT_NAME).is_running()
):
event.fail("maubot is not ready")
return
name = event.params["name"]
password = secrets.token_urlsafe(10)
config = self._get_configuration()
config["admins"][name] = password
self.container.push(MAUBOT_CONFIGURATION_PATH, yaml.safe_dump(config))
self.container.restart(MAUBOT_NAME)
event.set_results({"password": password})

# Integrations events handlers
def _on_database_created(self, _: DatabaseCreatedEvent) -> None:
"""Handle database created event."""
Expand Down
25 changes: 25 additions & 0 deletions tests/integration/test_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,28 @@ async def test_build_and_deploy(
)
assert response.status_code == 200
assert "Maubot Manager" in response.text


async def test_create_admin_action(ops_test: OpsTest):
"""
arrange: Maubot charm integrated with PostgreSQL.
act: run the create-admin action.
assert: the action results contains a password.
"""
name = "test"
assert ops_test.model
unit = ops_test.model.applications["penpot"].units[0]

action = await unit.run_action("create-admin", name=name)
await action.wait()

assert "password" in action.results
password = action.results["password"]
response = requests.post(
"http://127.0.0.1/_matrix/maubot/v1/auth/login",
timeout=5,
headers={"Host": "maubot.local"},
data=f'{{"username":"{name}","password":"{password}"}}',
)
assert response.status_code == 200
assert "token" in response.text
11 changes: 10 additions & 1 deletion tests/unit/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@ def harness_fixture():
)
root = harness.get_filesystem_root("maubot")
(root / "data").mkdir()
(root / "data" / "config.yaml").write_text("database: sqlite:maubot.db")
yaml_content = """\
database: sqlite:maubot.db
server:
hostname: 0.0.0.0
port: 29316
public_url: https://example.com
admins:
root:
"""
(root / "data" / "config.yaml").write_text(yaml_content)
yield harness
harness.cleanup()
15 changes: 15 additions & 0 deletions tests/unit/test_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,18 @@ def test_database_created(harness):
harness.charm._get_postgresql_credentials()
== "postgresql://someuser:somepasswd@dbhost:5432/maubot"
)


def test_create_admin_action(harness):
"""
arrange: initialize the testing harness and set up all required integration.
act: run create-admin charm action.
assert: ensure correct commands are executed.
"""
harness.set_leader()
harness.begin_with_initial_hooks()
set_postgresql_integration(harness)

action = harness.run_action("create-admin", {"name": "test"})

assert "password" in action.results

0 comments on commit fcd78be

Please sign in to comment.