Skip to content

Commit

Permalink
feat: Implement bootstrap raft action (#359)
Browse files Browse the repository at this point in the history
Co-authored-by: Ghislain Bourgeois <[email protected]>
  • Loading branch information
DanielArndt and ghislainbourgeois authored Jan 20, 2025
1 parent be4c676 commit 4ac790b
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 1 deletion.
9 changes: 9 additions & 0 deletions charmcraft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,15 @@ actions:
stored by the charm.
required: [secret-id]

bootstrap-raft:
description: >-
Bootstraps raft using a peers.json file. This action requires the
application to first be scaled to a single unit. Bootstrapping can help
recover when quorum is lost, however, it may cause uncommitted Raft log
entries to be committed. See
https://developer.hashicorp.com/vault/docs/concepts/integrated-storage#manual-recovery-using-peers-json
for more details.
create-backup:
description: >-
Creates a snapshot of the Raft backend and saves it to the S3 storage.
Expand Down
25 changes: 24 additions & 1 deletion src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
KVManager,
ManagerError,
PKIManager,
RaftManager,
TLSManager,
VaultCertsError,
)
Expand Down Expand Up @@ -254,10 +255,13 @@ def __init__(self, *args: Any):
]
for event in configure_events:
self.framework.observe(event, self._configure)
self.framework.observe(self.on.authorize_charm_action, self._on_authorize_charm_action)
self.framework.observe(
self.vault_kv.on.vault_kv_client_detached, self._on_vault_kv_client_detached
)

# Actions
self.framework.observe(self.on.authorize_charm_action, self._on_authorize_charm_action)
self.framework.observe(self.on.bootstrap_raft_action, self._on_bootstrap_raft_action)
self.framework.observe(self.on.create_backup_action, self._on_create_backup_action)
self.framework.observe(self.on.list_backups_action, self._on_list_backups_action)
self.framework.observe(self.on.restore_backup_action, self._on_restore_backup_action)
Expand Down Expand Up @@ -442,6 +446,25 @@ def _on_authorize_charm_action(self, event: ActionEvent):
event.fail(f"Vault returned an error while authorizing the charm: {str(e)}")
return

def _on_bootstrap_raft_action(self, event: ActionEvent):
"""Bootstraps the raft cluster when a single node is present.
This is useful when Vault has lost quorum. The application must first
be reduced to a single unit.
"""
if not self._api_address:
event.fail(message="Network bind address is not available")
return

try:
manager = RaftManager(self, self.machine, VAULT_SNAP_NAME, VAULT_STORAGE_PATH)
manager.bootstrap(self._node_id, self._api_address)
except ManagerError as e:
logger.error("Failed to bootstrap raft: %s", e)
event.fail(message=f"Failed to bootstrap raft: {e}")
return
event.set_results({"result": "Raft cluster bootstrapped successfully."})

def _get_vault_client(self) -> VaultClient | None:
if not self._api_address:
return None
Expand Down
4 changes: 4 additions & 0 deletions tests/unit/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
BackupManager,
KVManager,
PKIManager,
RaftManager,
TLSManager,
)

Expand Down Expand Up @@ -49,6 +50,9 @@ def setup(self):
self.mock_backup_manager = stack.enter_context(
patch("charm.BackupManager", autospec=BackupManager)
).return_value
self.mock_raft_manager = stack.enter_context(
patch("charm.RaftManager", autospec=RaftManager)
).return_value

self.mock_socket_fqdn = stack.enter_context(patch("socket.getfqdn"))
self.mock_pki_requirer_get_assigned_certificate = stack.enter_context(
Expand Down
42 changes: 42 additions & 0 deletions tests/unit/test_charm_bootstrap_raft_action.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#!/usr/bin/env python3
# Copyright 2025 Canonical Ltd.
# See LICENSE file for licensing details.


import ops.testing as testing
import pytest
from charms.vault_k8s.v0.vault_managers import ManagerError
from ops.testing import ActionFailed

from tests.unit.fixtures import VaultCharmFixtures


class TestCharmBootstrapRaftAction(VaultCharmFixtures):
def test_given_no_network_when_bootstrap_raft_action_then_fails(self):
state_in = testing.State(
leader=True,
)

with pytest.raises(ActionFailed) as e:
self.ctx.run(self.ctx.on.action("bootstrap-raft"), state_in)
assert e.value.message == "Network bind address is not available"

def test_when_bootstrap_raft_raises_manager_error_then_action_fails_with_error_message(self):
self.mock_raft_manager.bootstrap.side_effect = ManagerError("some error message")
peer_relation = testing.PeerRelation(
endpoint="vault-peers",
)
state_in = testing.State(
leader=False,
relations=[peer_relation],
networks={
testing.Network(
"vault-peers",
bind_addresses=[testing.BindAddress([testing.Address("1.2.1.2")])],
)
},
)

with pytest.raises(ActionFailed) as e:
self.ctx.run(self.ctx.on.action("bootstrap-raft"), state_in)
assert e.value.message == "Failed to bootstrap raft: some error message"

0 comments on commit 4ac790b

Please sign in to comment.