From e554a137dc0b7bf5051c50216167592dc73806cf Mon Sep 17 00:00:00 2001 From: Guillaume Belanger Date: Fri, 26 Jan 2024 16:26:46 -0500 Subject: [PATCH] feat: Start vaultd service (#19) --- metadata.yaml | 9 +++----- src/charm.py | 40 ++++++++++++++++++++++++----------- src/machine.py | 5 +++++ src/templates/vault.hcl.j2 | 1 + tests/unit/config.hcl | 5 +++-- tests/unit/test_charm.py | 43 +++++++++++++++++++++++++++++++------- 6 files changed, 76 insertions(+), 27 deletions(-) diff --git a/metadata.yaml b/metadata.yaml index 9a4f711..2cb8eca 100644 --- a/metadata.yaml +++ b/metadata.yaml @@ -3,7 +3,7 @@ name: vault-dev -display-name: Vault Operator +display-name: Vault summary: A tool for managing secrets description: | Vault secures, stores, and tightly controls access to @@ -23,12 +23,9 @@ assumes: - juju >= 3.1 storage: - vault-raft: + vault: type: filesystem - location: /var/snap/vault/common/raft - config: - type: filesystem - location: /var/snap/vault/common/config + location: /var/snap/vault/common peers: vault-peers: diff --git a/src/charm.py b/src/charm.py index 95ae5d9..c71ae33 100755 --- a/src/charm.py +++ b/src/charm.py @@ -22,13 +22,13 @@ CONFIG_TEMPLATE_DIR_PATH = "src/templates/" CONFIG_TEMPLATE_NAME = "vault.hcl.j2" PEER_RELATION_NAME = "vault-peers" -VAULT_CONFIG_PATH = "/var/snap/vault/common/config" +VAULT_CONFIG_PATH = "/var/snap/vault/common" VAULT_CONFIG_FILE_NAME = "vault.hcl" VAULT_PORT = 8200 VAULT_CLUSTER_PORT = 8201 VAULT_SNAP_NAME = "vault" -VAULT_SNAP_CHANNEL = "1.12/stable" -VAULT_SNAP_REVISION = 2166 +VAULT_SNAP_CHANNEL = "latest/edge" +VAULT_SNAP_REVISION = 2177 VAULT_STORAGE_PATH = "/var/snap/vault/common/raft" @@ -77,7 +77,11 @@ def config_file_content_matches(existing_content: str, new_content: str) -> bool return existing_config_hcl == new_content_hcl new_retry_joins = new_content_hcl["storage"]["raft"].pop("retry_join", []) - existing_retry_joins = existing_config_hcl["storage"]["raft"].pop("retry_join", []) + + try: + existing_retry_joins = existing_config_hcl["storage"]["raft"].pop("retry_join", []) + except KeyError: + existing_retry_joins = [] # If there is only one retry join, it is a dict if isinstance(new_retry_joins, dict): @@ -105,7 +109,7 @@ def __init__(self, *args): self, scrape_configs=[ { - "scheme": "https", + "scheme": "http", "tls_config": {"insecure_skip_verify": True}, "metrics_path": "/v1/sys/metrics", "static_configs": [{"targets": [f"*:{VAULT_PORT}"]}], @@ -136,7 +140,9 @@ def _configure(self, _): return self.unit.status = MaintenanceStatus("Installing Vault") self._install_vault_snap() + self._create_backend_directory() self._generate_vault_config_file() + self._start_vault_service() self._set_peer_relation_node_api_address() self.unit.status = ActiveStatus() @@ -149,11 +155,21 @@ def _install_vault_snap(self) -> None: snap.SnapState.Latest, channel=VAULT_SNAP_CHANNEL, revision=VAULT_SNAP_REVISION ) vault_snap.hold() - + logger.info("Vault snap installed") except snap.SnapError as e: logger.error("An exception occurred when installing Vault. Reason: %s", str(e)) raise e + def _create_backend_directory(self) -> None: + self.machine.make_dir(path=VAULT_STORAGE_PATH) + + def _start_vault_service(self) -> None: + """Start the Vault service.""" + snap_cache = snap.SnapCache() + vault_snap = snap_cache[VAULT_SNAP_NAME] + vault_snap.start(services=["vaultd"]) + logger.info("Vault service started") + def _generate_vault_config_file(self) -> None: """Create the Vault config file and push it to the Machine.""" assert self._cluster_address @@ -238,23 +254,23 @@ def _bind_address(self) -> Optional[str]: @property def _api_address(self) -> Optional[str]: - """Returns the IP with the https schema and vault port. + """Returns the IP with the http schema and vault port. - Example: "https://1.2.3.4:8200" + Example: "http://1.2.3.4:8200" """ if not self._bind_address: return None - return f"https://{self._bind_address}:{VAULT_PORT}" + return f"http://{self._bind_address}:{VAULT_PORT}" @property def _cluster_address(self) -> Optional[str]: - """Return the IP with the https schema and vault port. + """Return the IP with the http schema and vault port. - Example: "https://1.2.3.4:8201" + Example: "http://1.2.3.4:8201" """ if not self._bind_address: return None - return f"https://{self._bind_address}:{VAULT_CLUSTER_PORT}" + return f"http://{self._bind_address}:{VAULT_CLUSTER_PORT}" @property def _node_id(self) -> str: diff --git a/src/machine.py b/src/machine.py index 96f99e3..2bb9dca 100644 --- a/src/machine.py +++ b/src/machine.py @@ -7,6 +7,7 @@ import logging import os +from pathlib import Path logger = logging.getLogger(__name__) @@ -52,3 +53,7 @@ def push(self, path: str, source: str) -> None: with open(path, "w") as write_file: write_file.write(source) logger.info("Pushed file %s", path) + + def make_dir(self, path: str) -> None: + """Create a directory.""" + Path(path).mkdir(parents=True, exist_ok=True) diff --git a/src/templates/vault.hcl.j2 b/src/templates/vault.hcl.j2 index 2a0c0e2..1f346c3 100644 --- a/src/templates/vault.hcl.j2 +++ b/src/templates/vault.hcl.j2 @@ -13,6 +13,7 @@ listener "tcp" { unauthenticated_metrics_access = true } address = "{{ tcp_address }}" + tls_disable = 1 } default_lease_ttl = "{{ default_lease_ttl }}" max_lease_ttl = "{{ max_lease_ttl }}" diff --git a/tests/unit/config.hcl b/tests/unit/config.hcl index c9a58ae..19763b8 100644 --- a/tests/unit/config.hcl +++ b/tests/unit/config.hcl @@ -8,12 +8,13 @@ listener "tcp" { unauthenticated_metrics_access = true } address = "[::]:8200" + tls_disable = 1 } default_lease_ttl = "168h" max_lease_ttl = "720h" disable_mlock = true -cluster_addr = "https://1.2.1.2:8201" -api_addr = "https://1.2.1.2:8200" +cluster_addr = "http://1.2.1.2:8201" +api_addr = "http://1.2.1.2:8200" telemetry { disable_hostname = true prometheus_retention_time = "12h" diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py index 784f8cd..05e40ed 100644 --- a/tests/unit/test_charm.py +++ b/tests/unit/test_charm.py @@ -2,6 +2,7 @@ # See LICENSE file for licensing details. import unittest +from typing import List, Optional from unittest.mock import patch import hcl @@ -27,6 +28,10 @@ def ensure(self, state, channel, revision): def hold(self): self.hold_called = True + def start(self, services: Optional[List[str]] = None, enable: Optional[bool] = False) -> None: + self.start_called = True + self.start_called_with = {"services": services, "enable": enable} + class MockNetwork: def __init__(self, bind_address: str): @@ -54,6 +59,9 @@ def push(self, path: str, source: str) -> None: def pull(self, path: str) -> str: pass + def make_dir(self, path: str) -> None: + pass + def read_file(path: str) -> str: """Read a file and returns as a string. @@ -107,7 +115,6 @@ def setUp(self, patch_machine): self.mock_machine = MockMachine() self.model_name = "whatever" patch_machine.return_value = self.mock_machine - self.app_name = "vault" self.harness = ops.testing.Harness(VaultOperatorCharm) self.harness.set_model_name(self.model_name) self.addCleanup(self.harness.cleanup) @@ -116,7 +123,7 @@ def setUp(self, patch_machine): def _set_peer_relation(self) -> int: """Set the peer relation and return the relation id.""" return self.harness.add_relation( - relation_name=PEER_RELATION_NAME, remote_app=self.app_name + relation_name=PEER_RELATION_NAME, remote_app=self.harness.charm.app.name ) def _set_other_node_api_address_in_peer_relation(self, relation_id: int, unit_name: str): @@ -144,7 +151,7 @@ def test_given_vault_snap_uninstalled_when_configure_then_vault_snap_installed( mock_snap_cache.assert_called_with() assert vault_snap.ensure_called - assert vault_snap.ensure_called_with == (SnapState.Latest, "1.12/stable", 2166) + assert vault_snap.ensure_called_with == (SnapState.Latest, "latest/edge", 2177) assert vault_snap.hold_called @patch("ops.model.Model.get_binding") @@ -160,9 +167,7 @@ def test_given_config_file_not_exists_when_configure_then_config_file_pushed( self.harness.charm.on.install.emit() assert self.mock_machine.push_called - assert ( - self.mock_machine.push_called_with["path"] == "/var/snap/vault/common/config/vault.hcl" - ) + assert self.mock_machine.push_called_with["path"] == "/var/snap/vault/common/vault.hcl" pushed_content_hcl = hcl.loads(self.mock_machine.push_called_with["source"]) self.assertEqual(pushed_content_hcl, expected_content_hcl) @@ -194,6 +199,30 @@ def test_given_unit_not_leader_and_peer_addresses_unavailable_when_configure_the "Waiting for other units to provide their addresses" ) + @patch("ops.model.Model.get_binding") + @patch("charms.operator_libs_linux.v1.snap.SnapCache") + def test_given_when_configure_then_service_started(self, mock_snap_cache, patch_get_binding): + patch_get_binding.return_value = MockBinding(bind_address="1.2.1.2") + vault_snap = MockSnapObject("vault") + snap_cache = {"vault": vault_snap} + mock_snap_cache.return_value = snap_cache + self.harness.set_leader(is_leader=False) + peer_relation_id = self._set_peer_relation() + other_unit_name = f"{self.harness.charm.app.name}/1" + self.harness.add_relation_unit( + relation_id=peer_relation_id, remote_unit_name=other_unit_name + ) + + self._set_other_node_api_address_in_peer_relation( + relation_id=peer_relation_id, unit_name=other_unit_name + ) + + self.harness.charm.on.install.emit() + + mock_snap_cache.assert_called_with() + assert vault_snap.start_called + assert vault_snap.start_called_with == {"enable": False, "services": ["vaultd"]} + @patch("ops.model.Model.get_binding") @patch("charms.operator_libs_linux.v1.snap.SnapCache") def test_given_unit_not_leader_and_peer_addresses_available_when_configure_then_status_is_active( @@ -202,7 +231,7 @@ def test_given_unit_not_leader_and_peer_addresses_available_when_configure_then_ patch_get_binding.return_value = MockBinding(bind_address="1.2.1.2") self.harness.set_leader(is_leader=False) peer_relation_id = self._set_peer_relation() - other_unit_name = f"{self.app_name}/1" + other_unit_name = f"{self.harness.charm.app.name}/1" self.harness.add_relation_unit( relation_id=peer_relation_id, remote_unit_name=other_unit_name )