diff --git a/lib/charms/vault_k8s/v0/vault_tls.py b/lib/charms/vault_k8s/v0/vault_tls.py index 77b186a5..171e60b6 100644 --- a/lib/charms/vault_k8s/v0/vault_tls.py +++ b/lib/charms/vault_k8s/v0/vault_tls.py @@ -202,7 +202,6 @@ def configure_certificates(self, subject_ip: str) -> None: ) self._restart_vault() return - if self._should_request_new_certificate(): self._send_new_certificate_request_to_provider( self.pull_tls_file_from_workload(File.CSR), subject_ip @@ -246,7 +245,6 @@ def _generate_self_signed_certs(self, subject_ip: str) -> None: if not (private_key := self.pull_tls_file_from_workload(File.KEY)): private_key = generate_private_key().decode() self._push_tls_file_to_workload(File.KEY, private_key) - ca_private_key, ca_certificate = self._get_ca_certificate_secret() self._push_tls_file_to_workload(File.CA, ca_certificate) sans_ip = [subject_ip] diff --git a/src/charm.py b/src/charm.py index 45030895..eea38ef5 100755 --- a/src/charm.py +++ b/src/charm.py @@ -46,7 +46,6 @@ ) from charms.vault_k8s.v0.vault_s3 import S3, S3Error from charms.vault_k8s.v0.vault_tls import File, VaultCertsError, VaultTLSManager -from container import Container from cryptography import x509 from jinja2 import Environment, FileSystemLoader from ops import CharmBase, MaintenanceStatus @@ -69,6 +68,8 @@ ) from ops.pebble import ChangeError, Layer, PathError +from container import Container + logger = logging.getLogger(__name__) APPROLE_ROLE_NAME = "charm" @@ -385,7 +386,6 @@ def _configure(self, event: Optional[ConfigChangedEvent] = None) -> None: # noq self.tls.configure_certificates(self._ingress_address) if not self.unit.is_leader() and not self.tls.tls_file_pushed_to_workload(File.CA): return - self._generate_vault_config_file() self._set_pebble_plan() vault = Vault( diff --git a/test-requirements.in b/test-requirements.in index 39bfc594..e8b7e5b0 100644 --- a/test-requirements.in +++ b/test-requirements.in @@ -7,3 +7,4 @@ pytest-operator pytest-asyncio==0.21.2 ruff types-hvac +ops-scenario diff --git a/test-requirements.txt b/test-requirements.txt index 241876e7..d1d4a17f 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,92 +1,119 @@ # -# This file is autogenerated by pip-compile with Python 3.11 +# This file is autogenerated by pip-compile with Python 3.12 # by the following command: # # pip-compile --constraint=requirements.txt test-requirements.in # asttokens==2.4.1 # via stack-data -bcrypt==4.1.2 +bcrypt==4.2.0 # via paramiko -cachetools==5.3.3 +cachetools==5.4.0 # via google-auth certifi==2024.2.2 # via + # -c requirements.txt # kubernetes # requests cffi==1.16.0 # via + # -c requirements.txt # cryptography # pynacl charset-normalizer==3.3.2 - # via requests -codespell==2.2.6 + # via + # -c requirements.txt + # requests +codespell==2.3.0 # via -r test-requirements.in coverage[toml]==7.6.1 # via -r test-requirements.in cryptography==43.0.0 - # via paramiko + # via + # -c requirements.txt + # paramiko decorator==5.1.1 # via # ipdb # ipython executing==2.0.1 # via stack-data -google-auth==2.29.0 +google-auth==2.33.0 # via kubernetes hvac==2.3.0 - # via juju + # via + # -c requirements.txt + # juju idna==3.7 - # via requests + # via + # -c requirements.txt + # requests iniconfig==2.0.0 - # via pytest + # via + # -c requirements.txt + # pytest ipdb==0.13.13 # via pytest-operator -ipython==8.23.0 +ipython==8.26.0 # via ipdb jedi==0.19.1 # via ipython jinja2==3.1.4 - # via pytest-operator + # via + # -c requirements.txt + # pytest-operator juju==3.5.2.0 # via # -r test-requirements.in # pytest-operator -kubernetes==29.0.0 +kubernetes==30.1.0 # via juju macaroonbakery==1.3.4 # via juju markupsafe==2.1.5 - # via jinja2 + # via + # -c requirements.txt + # jinja2 matplotlib-inline==0.1.7 # via ipython mypy-extensions==1.0.0 # via typing-inspect -nodeenv==1.8.0 +nodeenv==1.9.1 # via pyright oauthlib==3.2.2 # via # kubernetes # requests-oauthlib +ops==2.15.0 + # via + # -c requirements.txt + # ops-scenario +ops-scenario==6.0.3 + # via + # -c requirements.txt + # -r test-requirements.in packaging==24.0 # via + # -c requirements.txt # juju # pytest -paramiko==3.4.0 +paramiko==3.4.1 # via juju parso==0.8.4 # via jedi pexpect==4.9.0 # via ipython pluggy==1.5.0 - # via pytest -prompt-toolkit==3.0.43 + # via + # -c requirements.txt + # pytest +prompt-toolkit==3.0.47 # via ipython -protobuf==5.26.1 +protobuf==5.27.3 # via macaroonbakery ptyprocess==0.7.0 # via pexpect -pure-eval==0.2.2 +pure-eval==0.2.3 # via stack-data pyasn1==0.6.0 # via @@ -96,8 +123,10 @@ pyasn1==0.6.0 pyasn1-modules==0.4.0 # via google-auth pycparser==2.22 - # via cffi -pygments==2.17.2 + # via + # -c requirements.txt + # cffi +pygments==2.18.0 # via ipython pymacaroons==0.13.0 # via macaroonbakery @@ -110,10 +139,11 @@ pyrfc3339==1.1 # via # juju # macaroonbakery -pyright==1.1.375 +pyright==1.1.376 # via -r test-requirements.in pytest==8.3.2 # via + # -c requirements.txt # -r test-requirements.in # pytest-asyncio # pytest-operator @@ -124,16 +154,22 @@ pytest-asyncio==0.21.2 pytest-operator==0.36.0 # via -r test-requirements.in python-dateutil==2.9.0.post0 - # via kubernetes + # via + # -c requirements.txt + # kubernetes pytz==2024.1 # via pyrfc3339 pyyaml==6.0.1 # via + # -c requirements.txt # juju # kubernetes + # ops + # ops-scenario # pytest-operator requests==2.32.3 # via + # -c requirements.txt # hvac # kubernetes # macaroonbakery @@ -142,10 +178,11 @@ requests-oauthlib==2.0.0 # via kubernetes rsa==4.9 # via google-auth -ruff==0.5.7 +ruff==0.6.1 # via -r test-requirements.in six==1.16.0 # via + # -c requirements.txt # asttokens # kubernetes # macaroonbakery @@ -161,25 +198,26 @@ traitlets==5.14.3 # matplotlib-inline types-hvac==2.3.0.20240621 # via -r test-requirements.in -types-requests==2.31.0.20240406 +types-requests==2.32.0.20240712 # via types-hvac typing-extensions==4.11.0 # via - # ipython + # -c requirements.txt # typing-inspect typing-inspect==0.9.0 # via juju urllib3==2.2.2 # via + # -c requirements.txt # kubernetes # requests # types-requests wcwidth==0.2.13 # via prompt-toolkit websocket-client==1.7.0 - # via kubernetes + # via + # -c requirements.txt + # kubernetes + # ops websockets==12.0 # via juju - -# The following packages are considered to be unsafe in a requirements file: -# setuptools diff --git a/tests/unit/lib/charms/vault_k8s/v0/test_vault_client.py b/tests/unit/lib/charms/vault_k8s/v0/test_vault_client.py index 1b8f3190..c3c59075 100644 --- a/tests/unit/lib/charms/vault_k8s/v0/test_vault_client.py +++ b/tests/unit/lib/charms/vault_k8s/v0/test_vault_client.py @@ -6,7 +6,6 @@ from unittest.mock import MagicMock, patch import requests -from charm import AUTOUNSEAL_POLICY_PATH from charms.vault_k8s.v0.vault_client import ( AppRole, AuditDeviceType, @@ -17,6 +16,8 @@ ) from hvac.exceptions import InvalidPath +from charm import AUTOUNSEAL_POLICY_PATH + TEST_PATH = "./tests/unit/lib/charms/vault_k8s/v0" diff --git a/tests/unit/lib/charms/vault_k8s/v0/test_vault_tls.py b/tests/unit/lib/charms/vault_k8s/v0/test_vault_tls.py index e6c973ea..7c0135de 100644 --- a/tests/unit/lib/charms/vault_k8s/v0/test_vault_tls.py +++ b/tests/unit/lib/charms/vault_k8s/v0/test_vault_tls.py @@ -2,133 +2,53 @@ # Copyright 2024 Canonical Ltd. # See LICENSE file for licensing details. -import json -import unittest +import datetime +import os +import tempfile from unittest.mock import Mock, patch -from charm import VAULT_CHARM_APPROLE_SECRET_LABEL, VaultCharm -from charms.vault_k8s.v0.vault_tls import CA_CERTIFICATE_JUJU_SECRET_LABEL, File -from ops import testing +import pytest +import scenario +from charms.tls_certificates_interface.v3.tls_certificates import ProviderCertificate +from charms.vault_k8s.v0.vault_tls import CA_CERTIFICATE_JUJU_SECRET_LABEL from ops.model import WaitingStatus -TLS_CERTIFICATES_LIB_PATH = "charms.tls_certificates_interface.v2.tls_certificates" -EXAMPLE_CA = "-----BEGIN CERTIFICATE-----\nMIIDTzCCAjegAwIBAgIUC+ohQChfeaZDz6MnMTXnJ6gkJN4wDQYJKoZIhvcNAQEL\nBQAwLDELMAkGA1UEBhMCVVMxHTAbBgNVBAMMFFZhdWx0IHNlbGYgc2lnbmVkIENB\nMCAXDTI0MDExOTA5NTcyMFoYDzIwNzQwMTA2MDk1NzIwWjAsMQswCQYDVQQGEwJV\nUzEdMBsGA1UEAwwUVmF1bHQgc2VsZiBzaWduZWQgQ0EwggEiMA0GCSqGSIb3DQEB\nAQUAA4IBDwAwggEKAoIBAQDHdlv5i5rbQjm9qVhGpHFhAincmuocP0OiJm/QANT/\nFqJKnwogMTkb69jn73nXQCqmiNT/r06tTux6nHCjdzjYu/SzfYIUTzrYFbiXJc8e\n9YfyO1bykiZ5W4QoZvuj2QqWp+n2fuXEoBKSbYzKnlwWSk0uhwdYkJ/yZU5zWnOO\nFSsFASGvgHMpo5NZ+qB9/r+jqwofpJbB7VsRlwVukZdqqblE2c4dL2KT/nv5DNko\nqTxQOymG+c/yiGkU5+UUWGyS34u51E9+iAhtome+Tl54PptCnXCyKqzjOz5Kj8F0\noGmsWEk0L1oSog9yNJaVN2pzZDLnHBvo6oSp+tPDX+exAgMBAAGjZzBlMB8GA1Ud\nDgQYBBYEFLAfjFTRAA+o3iE6xPyo3BPPg9LMMCEGA1UdIwQaMBiAFgQUsB+MVNEA\nD6jeITrE/KjcE8+D0swwDgYDVR0PAQH/BAQDAgKkMA8GA1UdEwEB/wQFMAMBAf8w\nDQYJKoZIhvcNAQELBQADggEBAJOkUoMxS3arCbtXXr+3O35FJThy2V643zyAQAoF\n/ndgZPMhPbx8WfAltkylUgj4LXcZw98BhYe49YfGvlQwu8oh/8ENavNzZVNdJcMO\nCliN1ZjV9goRXAl/sUesswMojGJTQaDuWfOXn4NCA6B72NDKz+vPVj1DLe/bDOQ2\noi5KbaJsRbkfW7dj61iwXtOW6PYT6gDeQLJRvLGU2v0ORXet7yu1aVkdPIsfhl6G\nfC+Tdubb1GTRF5JgzXxTMFgaKOmy5u9KHK36TLUX034YfYzksMya7y7SeRT18Uxd\npQSmmO0rHitHDAi9O1bsZCBmbyJhxyE3mGdYKhk1pouedUg=\n-----END CERTIFICATE-----" -EXAMPLE_CA_PK = """-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAx3Zb+Yua20I5valYRqRxYQIp3JrqHD9DoiZv0ADU/xaiSp8K\nIDE5G+vY5+9510AqpojU/69OrU7sepxwo3c42Lv0s32CFE862BW4lyXPHvWH8jtW\n8pImeVuEKGb7o9kKlqfp9n7lxKASkm2Myp5cFkpNLocHWJCf8mVOc1pzjhUrBQEh\nr4BzKaOTWfqgff6/o6sKH6SWwe1bEZcFbpGXaqm5RNnOHS9ik/57+QzZKKk8UDsp\nhvnP8ohpFOflFFhskt+LudRPfogIbaJnvk5eeD6bQp1wsiqs4zs+So/BdKBprFhJ\nNC9aEqIPcjSWlTdqc2Qy5xwb6OqEqfrTw1/nsQIDAQABAoIBAAddQQNopRyynZZ5\nZHn546RqQ7NnwNGu1ZzCAIopzbNmrt1EcUeY/vvJ7GXme0Ai7VrdXcfcPXJmoefb\nqMj67jgKUHw5W7kg57zx/bEO7fkS+vmgOT3sKXbSQIHhXinBf1jqoC0VUv8Rzehm\nxQuiEdJSMfalePRLJVdPaDhtaYDLCe4+8vzASi/K5XBN8poj2n8p1XoJghUTG5Hj\n6SN08XvOvM3/KfKyS1pa7TwkU4LESOcrhbXjpjFtJwj94UnpCxx9X+pxx8nR8Yzb\nTbNMgEpIRVYUqz2Z+2YjaSYpMeg4p8FqWnmMAzfLbvUQjpfh2p9DBeF/du33QWd9\nfgtSx5UCgYEA4mrg51s1I6mEp1yas19piW9GPxBYsZ2mjPqzBCqGApWyZHZ6EBb1\npWWlNv6hnszMgoY9RnPaL6nHYNpbqnkPH8MmU46T95bxbtVxfaH/+6J1f+bdr50v\n2+8KZ1iyJBlt7YWa4/foL9QiacKjKNfDL4fGonP54s44MJlNrcGUafMCgYEA4YXl\nRdKW9sLWG2/zPpx4uDIH+XLtFf6pjcNvMvPpM+kUd5tqyVwS8MchEAkr5AQ2Dj1W\nxXpPi28EV6tEyNLUz/JW0WvbR7Bjosjx550LCP+xhMSqoX1haBf4oSxfLUUzr70j\n5EazWjogjz0k6hQplofZQWXEMUPMPtOI+p0FjMsCgYEAmidCcMI8b8detcPq3+06\nIYRNQ2qRuHwphRq6/z8kdmYNSzEO8h1vqeiGj+bVixTMuKFE3s7J4mGpiVuhxXMe\nxPVNBt6wB4YRYvCXkH3Xly+I6Ef67zIJ/6fEYZCV2NYnbevlBQkoYEgCFhealpgw\nIBBFQR3NKIpW31/A72g479kCgYApWxZqMW4BnkUJDwR6LNNuY65Wrh6P8/0/w+D2\nZQgUvt9D97ojZsEKalnDyQrFa4hGIDVzTTSdCySutveMJC1mXLhS+wZhJRWAWn0R\nzhih89Gn2TC5IHbmUc8EL1Dcyl3qEjMsv1JQb2xdGAdW7Y+azRqoBXNu3VHtC3mJ\nC5Zi9QKBgA+SgYgHqkIcEpsZI+zsCCbqnBHlMIZI8uRGHx93Hc1kB/DjhEN7E+ek\nDzqXBlG4cfS7YB4eH1lG9S2y8AcpvRV6u+ggwhkLSX9OoeaevACWsLAC/Olu4jd8\nqtLungNLBJj5AcHycaiGOBzgLTFBZ7J4h4lAk145PnagniETf/gq\n-----END RSA PRIVATE KEY-----""" -EXAMPLE_PK = "-----BEGIN RSA PRIVATE KEY-----\nMIIEogIBAAKCAQEAyBE1PlC4FZKAqlRPzkb8/ARpcNhOaDTE8X3Ypy3q+yFV56F7\nyUNOLG5Z0afBdb+eCQI+TabjtSrzKBZYTrjNzQel9g+mUEI+Rn8JyiGiTQ6s1218\nTYlZCenzXj0v9uqu2kyUNEfZBn3/oiugyAX2+6Z52jjZy1/R3mWMt8dBnZnHZiOI\nqXSU8q76GS8j+GiBq3c65MN2A79yl9cKmYRfQLiqwQw6fB7sr0lg4sX6xbQgEApG\nrKRKLBqUeP2QiBZh0fwUk73fXBMgDqcZTrGBTwn46XxqSoaWJNAdGg3bDORfBevn\nRHpg44G1wOlT6xbPkyKDifn1iNR9yL4Egp3OywIDAQABAoIBABG69K0BGk9PHHf7\n4Na3E9SBz5ZglRJHGu0L6hdmylxXJ/XPKdk8TcFCRlN+Onbk9Gx39m2LTMLRe5sh\n39GaLyLsepjD6klSlZJJz+RJ9sg9dLPi0BFPCsUGJrtDUOzg/335K2k2tNUOdYk5\ntJYFcU38AvCD+Uk8xKyg80eWMQp2Xj0ZI9NrbDNvjnsPf92zBOB73nlHUTOFnfi/\ny3LJsotWmSBorn7gpmkGK7W5qyzihs6IHPvUitLfDessCHf7E7SyRu4E/ax2OvIR\nAEFw/ZQeTGJQYYGDULOXEcoeUlNyZXDXEVugi/KbFrxHbKru4EFKLHFuLMswHMse\nbwtg/4ECgYEA7UU//6tPb6QCbqiC0Tw93hoaKln8lgUMJCWbEeLhVFEjzn5sMEmg\nsga7qxPNxmwLfp8/LK2oDhsWMhII9+ZKjoO0nnl81CZCRtG4U9p7x/38GOFUGfAT\nO6jGoLCka/3qG8spsjjV8e6vnBpnjyq1qm8M3Mz8OVj9rdEzzf6A1ycCgYEA19wn\n5vDHXcbANO7/w7Iqhn+zJi1uQj/8lb80B2jVlaUhAVQtIowYcT2CN8Cgtzwj4kaG\n5Dlz1MaIOnVTpHGvGdbUs3HiBuovBgHfJ1+0AQZ7Kg3xIyKGTBNR41LW5t7QQxY9\nhrvd7cJwXbu+usY3v5R2Xkr6/hDx9vcLhy97sb0CgYAugfpvdPbXHUDUy/cIaFSA\nKoGid40JIugkVbK1qNEeI+Fu4lz2ghgbjTJP8EvPbvI52aEacteUHD8XhW14mg1X\nLf3DanDLbMxk8Uq+NP86TlCR1+kSRHqgoQ5+BOHVwSmYVRRROM7G41BMuug9qdN+\nGtJcnVl7LDRdU7ph0FcU1QKBgDKcdeaZ8cS1Av/mQaWasonSiyiaYk26PvjFWeea\n1uk9TF3JZMPC4UA70bpMueH8gdVd/+am6derrOk39SKLXSjLzBc+zmYcpmXcLnxG\n3ieXY21a030PbTmNFhgcpjJ/b4krP8XFaqWCf2Ia0P9t1khfANne7rZ/NpxXFCbg\nJTppAoGABNExOrNs60uhgc7kxc2N3ZCQY4c9F5nf8IkKKSHFF3rFrNIzV7eu0KUo\nkYgXPvh6FAbAuID4OlRFzMkmwt1cPc4eDMbHYnCOKHNtF2L4ogiiMAuFVL3ZBT78\nvj7/gYptiwR7oisVfFPhEx+HXMt96O0wxEHAjs/AmznN/mEfWyU=\n-----END RSA PRIVATE KEY-----\n" -EXAMPLE_CERTIFICATE = "-----BEGIN CERTIFICATE-----\nMIIDbjCCAlagAwIBAgIULrCQsNHR+53qw9UUwZWwt99kiVswDQYJKoZIhvcNAQEL\nBQAwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcM\nDVNhbiBGcmFuY2lzY28xEzARBgNVBAoMCk15IENvbXBhbnkxEzARBgNVBAMMCm15\nc2l0ZS5jb20wHhcNMjQwNTAxMTkzODExWhcNMjQwNTExMTkzODExWjBkMQswCQYD\nVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5j\naXNjbzETMBEGA1UECgwKTXkgQ29tcGFueTETMBEGA1UEAwwKbXlzaXRlLmNvbTCC\nASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJNLzpIkGRjkarfSut8opDBS\nPsUB0SDzwBceMUG4iPPUGgHhBFhamP64qF4ZfwG70/c7EL1tNmigMnp8GxA9puKt\nMVPPeTZKESvLM5bfTVYsxN/SzsYN7ffT8gX9pjtAHw0xXTJtXNQYa6LD1CTbS3EU\n+p1S/N62vwsc+oIbT2DDPI8kaYuu8jaffk0mJDG0njfC4gv2P4klaMw2LRGoKgSx\nmfO9IY1wjFiLiWoGCk/kKvDyvQOpeq8E0h2mBau1t64uALNxRFfRfZIuMqBQuWJC\nvimIKONEpG34g+Wo23hHU2MKRtizyMY+5OnM0ePVXKgh0aiakfCYfVmbrmUcQbsC\nAwEAAaMYMBYwFAYDVR0RBA0wC4IJbG9jYWxob3N0MA0GCSqGSIb3DQEBCwUAA4IB\nAQASD/9lCfQajzRHwxm038D2Ia05Wz4mSPx1jykAUMl0ogxcuYqMu4QpxsPSof+A\nOcZGaadX3W3l7lijwPNf3ptkwTuwftwFSwqM9WIkS9i6DCQVMQfyjisA2Rq9k3IP\nuRtqmdj0LuavFfOfZnWxRYkTyT5p5dw5Qz8WwBv20ojYUQa9Y9p2bw9irum/Ghwu\nNfemTalwUl7wYhhAVKF6vnjpPXs6J3yaQC7XzVHjkb11V+JgK+LUFZC9IeWafJd6\ng6DG+V8yXUwvwEA6EO+aupIaNWvYy0+ygUnRwRe+N1jJg9Zt/xxwXXYm1oLPJfHt\nWOfc7SNL1eqXfNnX5MPcfWPg\n-----END CERTIFICATE-----\n" - - -class MockNetwork: - def __init__(self, bind_address: str, ingress_address: str): - self.bind_address = bind_address - self.ingress_address = ingress_address - - -class MockBinding: - def __init__(self, bind_address: str, ingress_address: str): - self.network = MockNetwork(bind_address=bind_address, ingress_address=ingress_address) - - -class TestCharmTLS(unittest.TestCase): - def setUp(self): - self.model_name = "whatever" - self.harness = testing.Harness(VaultCharm) - self.addCleanup(self.harness.cleanup) - self.harness.set_model_name(name=self.model_name) - self.harness.begin() - self.container_name = "vault" - self.app_name = "vault-k8s" - - def get_valid_s3_params(self): - """Return a valid S3 parameters for mocking.""" - return { - "bucket": "BUCKET", - "access-key": "whatever access key", - "secret-key": "whatever secret key", - "endpoint": "http://ENDPOINT", - "region": "REGION", - } - - def _set_peer_relation(self) -> int: - """Set the peer relation and return the relation id.""" - return self.harness.add_relation(relation_name="vault-peers", remote_app=self.app_name) +from charm import VAULT_CHARM_APPROLE_SECRET_LABEL, VaultCharm - def _set_approle_secret( - self, - role_id: str, - secret_id: str, - ) -> None: - """Set the approle secret.""" - content = {"role-id": role_id, "secret-id": secret_id} - original_leader_state = self.harness.charm.unit.is_leader() - with self.harness.hooks_disabled(): - self.harness.set_leader(is_leader=True) - secret_id = self.harness.add_model_secret(owner=self.app_name, content=content) - secret = self.harness.model.get_secret(id=secret_id) - secret.set_info(label=VAULT_CHARM_APPROLE_SECRET_LABEL) - self.harness.set_leader(original_leader_state) +TLS_CERTIFICATES_LIB_PATH = "charms.tls_certificates_interface.v3.tls_certificates" +CERTIFICATE_TRANSFER_LIB_PATH = "charms.certificate_transfer_interface.v0.certificate_transfer" +VAULT_TLS_PATH = "charms.vault_k8s.v0.vault_tls" - def _set_ca_certificate_secret( - self, - private_key: str, - certificate: str, - ) -> None: - """Set the certificate secret.""" - content = { - "certificate": certificate, - "privatekey": private_key, - } - original_leader_state = self.harness.charm.unit.is_leader() - with self.harness.hooks_disabled(): - self.harness.set_leader(is_leader=True) - secret_id = self.harness.add_model_secret(owner=self.app_name, content=content) - secret = self.harness.model.get_secret(id=secret_id) - secret.set_info(label=CA_CERTIFICATE_JUJU_SECRET_LABEL) - self.harness.set_leader(original_leader_state) - def _set_other_node_api_address_in_peer_relation( - self, - relation_id: int, - unit_name: str, - ): - """Set the other node api address in the peer relation.""" - key_values = {"node_api_address": "http://5.2.1.9:8200"} - self.harness.update_relation_data( - app_or_unit=unit_name, - relation_id=relation_id, - key_values=key_values, - ) - - def _set_tls_access_certificate_relation(self): - """Set the peer relation and return the relation id.""" - return self.harness.add_relation( - relation_name="tls-certificates-access", remote_app="some-tls-provider" +class TestCharmTLS: + @pytest.fixture(autouse=True) + def context(self): + self.ctx = scenario.Context( + charm_type=VaultCharm, ) - @patch("ops.model.Model.get_binding") - def test_given_not_leader_and_ca_not_set_when_evaluate_status_then_status_is_waiting( - self, patch_get_binding - ): - self.harness.add_storage(storage_name="certs", attach=True) - self.harness.set_can_connect(container=self.container_name, val=True) - self.harness.set_leader(is_leader=False) - peer_relation_id = self._set_peer_relation() - other_unit_name = f"{self.app_name}/1" - self.harness.add_relation_unit( - relation_id=peer_relation_id, remote_unit_name=other_unit_name + def test_given_not_leader_and_ca_not_set_when_evaluate_status_then_status_is_waiting(self): + peer_relation = scenario.PeerRelation( + endpoint="vault-peers", + interface="vault-peer", + peers_data={ + 0: {"node_api_address": "http://5.2.1.9:8200"}, + }, ) - self._set_other_node_api_address_in_peer_relation( - relation_id=peer_relation_id, unit_name=other_unit_name + vault_container = scenario.Container( + name="vault", + can_connect=True, ) - patch_get_binding.return_value = MockBinding( - bind_address="1.2.1.2", ingress_address="10.1.0.1" + state_in = scenario.State( + containers=[vault_container], + leader=False, + relations=[peer_relation], ) - self.harness.evaluate_status() + state_out = self.ctx.run("collect_unit_status", state_in) - self.assertEqual( - self.harness.charm.unit.status, - WaitingStatus("Waiting for CA certificate to be accessible in the charm"), + assert state_out.unit_status == WaitingStatus( + "Waiting for CA certificate to be accessible in the charm" ) @patch("charms.vault_k8s.v0.vault_client.Vault.enable_audit_device", new=Mock) @@ -137,32 +57,60 @@ def test_given_not_leader_and_ca_not_set_when_evaluate_status_then_status_is_wai @patch("charms.vault_k8s.v0.vault_client.Vault.is_initialized", new=Mock) @patch("charms.vault_k8s.v0.vault_client.Vault.is_api_available", new=Mock) @patch("charms.vault_k8s.v0.vault_client.Vault.is_raft_cluster_healthy", new=Mock) - @patch("ops.model.Container.push", new=Mock) - @patch("ops.model.Container.restart", new=Mock) - @patch("ops.model.Model.get_binding") def test_given_unit_is_leader_and_ca_certificate_not_generated_when_configure_then_ca_certificate_is_generated( self, - patch_get_binding, ): - self.harness.add_storage(storage_name="certs", attach=True) - self.harness.set_can_connect(container=self.container_name, val=True) - self._set_peer_relation() - self._set_approle_secret( - role_id="whatever role id", - secret_id="whatever secret id", - ) - self.harness.set_leader(is_leader=True) - patch_get_binding.return_value = MockBinding( - bind_address="1.2.1.2", ingress_address="10.1.0.1" - ) - - self.harness.charm.on.config_changed.emit() - - secret = self.harness.model.get_secret( - label=CA_CERTIFICATE_JUJU_SECRET_LABEL - ).get_content() - assert secret["privatekey"].startswith("-----BEGIN RSA PRIVATE KEY-----") - assert secret["certificate"].startswith("-----BEGIN CERTIFICATE-----") + with tempfile.TemporaryDirectory() as temp_dir: + peer_relation = scenario.PeerRelation( + endpoint="vault-peers", + interface="vault-peer", + peers_data={ + 0: { + "node_api_address": "http://1.2.3.4", + }, + }, + ) + certs_mount = scenario.Mount("/vault/certs", temp_dir) + config_mount = scenario.Mount("/vault/config", temp_dir) + vault_container = scenario.Container( + name="vault", + can_connect=True, + mounts={ + "certs": certs_mount, + "config": config_mount, + }, + ) + certs_storage = scenario.Storage( + name="certs", + ) + config_storage = scenario.Storage( + name="config", + ) + state_in = scenario.State( + containers=[vault_container], + storage=[certs_storage, config_storage], + leader=True, + relations=[peer_relation], + ) + + state_out = self.ctx.run("update_status", state_in) + + # Assert the secret is created + assert state_out.secrets[0].label == CA_CERTIFICATE_JUJU_SECRET_LABEL + secret_content = state_out.secrets[0].contents[0] + assert secret_content["privatekey"].startswith("-----BEGIN RSA PRIVATE KEY-----") + assert secret_content["certificate"].startswith("-----BEGIN CERTIFICATE-----") + + # Assert the files are written to the correct location + ca_cert_path = temp_dir + "/ca.pem" + cert_path = temp_dir + "/cert.pem" + private_key_path = temp_dir + "/key.pem" + assert os.path.exists(ca_cert_path) + assert os.path.exists(cert_path) + assert os.path.exists(private_key_path) + assert open(ca_cert_path).read().startswith("-----BEGIN CERTIFICATE-----") + assert open(cert_path).read().startswith("-----BEGIN CERTIFICATE-----") + assert open(private_key_path).read().startswith("-----BEGIN RSA PRIVATE KEY-----") @patch("charms.vault_k8s.v0.vault_client.Vault.enable_audit_device", new=Mock) @patch("charms.vault_k8s.v0.vault_client.Vault.is_active", new=Mock) @@ -170,35 +118,51 @@ def test_given_unit_is_leader_and_ca_certificate_not_generated_when_configure_th @patch("charms.vault_k8s.v0.vault_client.Vault.is_initialized", new=Mock) @patch("charms.vault_k8s.v0.vault_client.Vault.is_api_available", new=Mock) @patch("charms.vault_k8s.v0.vault_client.Vault.is_raft_cluster_healthy", new=Mock) - @patch("ops.model.Container.restart") - @patch("ops.model.Container.exists") - @patch("ops.model.Model.get_binding") - def test_given_ca_certificate_not_pushed_to_workload_when_configure_then_ca_certificate_pushed( - self, patch_get_binding, patch_exists, patch_restart + @patch(f"{TLS_CERTIFICATES_LIB_PATH}.TLSCertificatesRequiresV3.request_certificate_creation") + def test_given_certificate_access_relation_when_relation_joined_then_new_request_is_created( + self, request_certificate_creation ): - self.harness.set_leader(is_leader=True) - root = self.harness.get_filesystem_root(self.container_name) - self.harness.add_storage(storage_name="certs", attach=True) - self.harness.add_storage(storage_name="config", attach=True) - - patch_exists.return_value = False - self._set_peer_relation() - self._set_ca_certificate_secret( - certificate=EXAMPLE_CA, - private_key=EXAMPLE_CA_PK, - ) - self._set_approle_secret( - role_id="whatever role id", - secret_id="whatever secret id", - ) - patch_get_binding.return_value = MockBinding( - bind_address="1.2.1.2", ingress_address="10.1.0.1" - ) - self.harness.set_can_connect(container=self.container_name, val=True) - - self.harness.charm.on.config_changed.emit() - patch_restart.assert_called_once() - self.assertEqual((root / "vault/certs/ca.pem").read_text(), EXAMPLE_CA) + with tempfile.TemporaryDirectory() as temp_dir: + certificates_relation = scenario.Relation( + endpoint="tls-certificates-access", + interface="tls-certificates", + remote_app_name="some-tls-provider", + ) + peer_relation = scenario.PeerRelation( + endpoint="vault-peers", + interface="vault-peer", + peers_data={ + 0: { + "node_api_address": "http://1.2.3.4", + }, + }, + ) + certs_mount = scenario.Mount("/vault/certs", temp_dir) + config_mount = scenario.Mount("/vault/config", temp_dir) + vault_container = scenario.Container( + name="vault", + can_connect=True, + mounts={ + "certs": certs_mount, + "config": config_mount, + }, + ) + certs_storage = scenario.Storage( + name="certs", + ) + config_storage = scenario.Storage( + name="config", + ) + state_in = scenario.State( + containers=[vault_container], + relations=[certificates_relation, peer_relation], + storage=[certs_storage, config_storage], + leader=True, + ) + + self.ctx.run("update_status", state_in) + + request_certificate_creation.assert_called_once() @patch("charms.vault_k8s.v0.vault_client.Vault.enable_audit_device", new=Mock) @patch("charms.vault_k8s.v0.vault_client.Vault.is_active", new=Mock) @@ -206,222 +170,257 @@ def test_given_ca_certificate_not_pushed_to_workload_when_configure_then_ca_cert @patch("charms.vault_k8s.v0.vault_client.Vault.is_initialized", new=Mock) @patch("charms.vault_k8s.v0.vault_client.Vault.is_api_available", new=Mock) @patch("charms.vault_k8s.v0.vault_client.Vault.is_raft_cluster_healthy", new=Mock) - @patch("ops.model.Container.restart", new=Mock) - @patch("socket.getfqdn") - @patch("ops.model.Container.exists") - @patch("ops.model.Model.get_binding") - def test_given_unit_certificate_not_stored_when_configure_then_unit_certificate_is_generated( - self, - patch_get_binding, - patch_exists, - patch_socket_getfqdn, - ): - self.harness.set_leader(is_leader=True) - fqdn = "banana" - patch_socket_getfqdn.return_value = fqdn - root = self.harness.get_filesystem_root(self.container_name) - self.harness.add_storage(storage_name="certs", attach=True) - self.harness.add_storage(storage_name="config", attach=True) - patch_exists.return_value = False - ingress_address = "10.1.0.1" - bind_address = "1.2.1.2" - self._set_peer_relation() - self._set_ca_certificate_secret( - certificate=EXAMPLE_CA, - private_key=EXAMPLE_CA_PK, - ) - self._set_approle_secret( - role_id="whatever role id", - secret_id="whatever secret id", - ) - self.harness.set_can_connect(container=self.container_name, val=True) - patch_get_binding.return_value = MockBinding( - bind_address=bind_address, ingress_address=ingress_address - ) - - self.harness.charm.on.config_changed.emit() - - assert ( - (root / "vault/certs/cert.pem").read_text().startswith("-----BEGIN CERTIFICATE-----") - ) - - def test_given_certificate_access_relation_when_relation_joined_then_new_request_is_created( - self, - ): - self.harness.set_leader(is_leader=True) - self.harness.add_storage(storage_name="certs", attach=True) - root = self.harness.get_filesystem_root(self.container_name) - self.harness.set_can_connect(container=self.container_name, val=True) - (root / "vault/certs/ca.pem").write_text(EXAMPLE_CA) - (root / "vault/certs/key.pem").write_text(EXAMPLE_PK) - (root / "vault/certs/cert.pem").write_text("old cert") - - self._set_peer_relation() - self._set_tls_access_certificate_relation() - self.harness.charm.tls.configure_certificates("1.1.1.1") - - assert (root / "vault/certs/csr.pem").exists() - + @patch( + f"{TLS_CERTIFICATES_LIB_PATH}.TLSCertificatesRequiresV3._find_certificate_in_relation_data" + ) def test_given_certificate_access_relation_when_cert_available_then_new_cert_saved( - self, - ): - self.harness.set_leader(is_leader=True) - self.harness.add_storage(storage_name="certs", attach=True) - root = self.harness.get_filesystem_root(self.container_name) - self.harness.set_can_connect(container=self.container_name, val=True) - (root / "vault/certs/key.pem").write_text(EXAMPLE_PK) - (root / "vault/certs/csr.pem").write_text("some csr") - - self._set_peer_relation() - rel_id = self._set_tls_access_certificate_relation() - - requirer_databag = [{"certificate_signing_request": "some csr", "ca": False}] - - provider_databag = [ - { - "ca": "some ca", - "chain": ["new cert"], - "certificate": EXAMPLE_CERTIFICATE, - "certificate_signing_request": "some csr", - "recommended_expiry_notification_time": 720, - } - ] - - self.harness.update_relation_data( - rel_id, "some-tls-provider", {"certificates": json.dumps(provider_databag)} - ) - self.harness.update_relation_data( - rel_id, - self.harness.charm.unit.name, - {"certificate_signing_requests": json.dumps(requirer_databag)}, - ) - - self.harness.charm._container.restart = Mock() # type: ignore [method-assign] - self.harness.charm.tls.configure_certificates("1.1.1.1") - - self.harness.charm._container.restart.assert_called_with(self.container_name) - assert (root / "vault/certs/cert.pem").exists() - assert (root / "vault/certs/ca.pem").exists() - - def test_given_certificate_access_relation_when_wrong_cert_available_then_saved_cert_not_changed( - self, + self, find_certificate_in_relation_data ): - self.harness.set_leader(is_leader=True) - self.harness.add_storage(storage_name="certs", attach=True) - root = self.harness.get_filesystem_root(self.container_name) - self.harness.set_can_connect(container=self.container_name, val=True) - (root / "vault/certs/key.pem").write_text(EXAMPLE_PK) - (root / "vault/certs/ca.pem").write_text(EXAMPLE_CA) - (root / "vault/certs/csr.pem").write_text("different csr") - (root / "vault/certs/cert.pem").write_text("different cert") - - self._set_peer_relation() - rel_id = self._set_tls_access_certificate_relation() - - provider_databag = [ - { - "ca": "some ca", - "chain": ["new cert"], - "certificate": EXAMPLE_CERTIFICATE, - "certificate_signing_request": "some csr", - } - ] + with tempfile.TemporaryDirectory() as temp_dir: + peer_relation = scenario.PeerRelation( + endpoint="vault-peers", + interface="vault-peer", + peers_data={ + 0: { + "node_api_address": "http://1.2.3.4", + }, + }, + ) + certificates_relation = scenario.Relation( + endpoint="tls-certificates-access", + interface="tls-certificates", + remote_app_name="some-tls-provider", + ) + find_certificate_in_relation_data.return_value = ProviderCertificate( + relation_id=certificates_relation.relation_id, + ca="some ca", + chain=["new cert"], + certificate="some cert", + revoked=False, + expiry_time=datetime.datetime.now() + datetime.timedelta(days=1), + application_name="some-tls-provider", + csr="some csr", + ) + certs_mount = scenario.Mount("/vault/certs", temp_dir) + config_mount = scenario.Mount("/vault/config", temp_dir) + vault_container = scenario.Container( + name="vault", + can_connect=True, + mounts={ + "certs": certs_mount, + "config": config_mount, + }, + ) + certs_storage = scenario.Storage( + name="certs", + ) + config_storage = scenario.Storage( + name="config", + ) + state_in = scenario.State( + containers=[vault_container], + storage=[certs_storage, config_storage], + leader=True, + relations=[peer_relation, certificates_relation], + ) + + self.ctx.run("update_status", state_in) + + # Assert the file is created + ca_cert_path = temp_dir + "/ca.pem" + cert_path = temp_dir + "/cert.pem" + assert os.path.exists(ca_cert_path) + assert os.path.exists(cert_path) + assert open(ca_cert_path).read().startswith("some ca") + assert open(cert_path).read().startswith("some cert") - self.harness.update_relation_data( - rel_id, "some-tls-provider", {"certificates": json.dumps(provider_databag)} - ) - - self.harness.charm.tls.configure_certificates("1.1.1.1") - assert (root / "vault/certs/cert.pem").read_text().startswith("different cert") - - @patch("ops.model.Model.get_binding") + @patch("charms.vault_k8s.v0.vault_client.Vault.enable_audit_device", new=Mock) + @patch("charms.vault_k8s.v0.vault_client.Vault.is_active", new=Mock) + @patch("charms.vault_k8s.v0.vault_client.Vault.is_sealed", new=Mock) + @patch("charms.vault_k8s.v0.vault_client.Vault.is_initialized", new=Mock) + @patch("charms.vault_k8s.v0.vault_client.Vault.is_api_available", new=Mock) + @patch("charms.vault_k8s.v0.vault_client.Vault.is_raft_cluster_healthy", new=Mock) + @patch(f"{VAULT_TLS_PATH}.generate_certificate") def test_given_certificate_access_relation_when_relation_left_then_previous_state_restored( - self, patch_get_binding + self, generate_certificate ): - self.harness.add_storage(storage_name="certs", attach=True) - self.harness.add_storage(storage_name="config", attach=True) - root = self.harness.get_filesystem_root(self.container_name) - self.harness.set_can_connect(container=self.container_name, val=True) - patch_get_binding.return_value = MockBinding( - bind_address="1.2.1.2", ingress_address="10.1.0.1" - ) - self._set_peer_relation() - self.harness.set_leader(is_leader=True) - (root / "vault/certs/csr.pem").write_text("first csr") - (root / "vault/certs/cert.pem").write_text("first cert") - (root / "vault/certs/ca.pem").write_text("first ca") - (root / "vault/certs/key.pem").write_text(EXAMPLE_PK) - - self._set_ca_certificate_secret( - certificate=EXAMPLE_CA, - private_key=EXAMPLE_CA_PK, - ) - self._set_approle_secret( - role_id="whatever role id", - secret_id="whatever secret id", - ) - - self.harness.charm._container.restart = Mock() # type: ignore [method-assign] - self.harness.charm.tls._on_tls_certificates_access_relation_broken(event=Mock()) - self.harness.charm._container.restart.assert_called_with(self.container_name) - assert not (root / "vault/certs/csr.pem").exists() - assert (root / "vault/certs/cert.pem").read_text().startswith("-----BEGIN CERTIFICATE") - assert (root / "vault/certs/key.pem").read_text().startswith("-----BEGIN RSA PRIVATE KEY") - assert (root / "vault/certs/ca.pem").read_text() == EXAMPLE_CA + generate_certificate.return_value = b"self signed cert" + with tempfile.TemporaryDirectory() as temp_dir: + peer_relation = scenario.PeerRelation( + endpoint="vault-peers", + interface="vault-peer", + peers_data={ + 0: { + "node_api_address": "http://1.2.3.4", + }, + }, + ) + certificates_relation = scenario.Relation( + endpoint="tls-certificates-access", + interface="tls-certificates", + remote_app_name="some-tls-provider", + ) + certs_mount = scenario.Mount("/vault/certs", temp_dir) + config_mount = scenario.Mount("/vault/config", temp_dir) + vault_container = scenario.Container( + name="vault", + can_connect=True, + mounts={ + "certs": certs_mount, + "config": config_mount, + }, + ) + certs_storage = scenario.Storage( + name="certs", + ) + config_storage = scenario.Storage( + name="config", + ) + state_in = scenario.State( + containers=[vault_container], + storage=[certs_storage, config_storage], + leader=True, + relations=[peer_relation, certificates_relation], + ) + + self.ctx.run(certificates_relation.broken_event, state_in) + + # Assert the file is created + ca_cert_path = temp_dir + "/cert.pem" + assert os.path.exists(ca_cert_path) + assert open(ca_cert_path).read() == "self signed cert" + @patch("charms.vault_k8s.v0.vault_client.Vault.enable_audit_device", new=Mock) + @patch("charms.vault_k8s.v0.vault_client.Vault.is_active", new=Mock) + @patch("charms.vault_k8s.v0.vault_client.Vault.is_sealed") + @patch("charms.vault_k8s.v0.vault_client.Vault.is_active_or_standby", new=Mock) + @patch("charms.vault_k8s.v0.vault_client.Vault.authenticate", new=Mock) + @patch("charms.vault_k8s.v0.vault_client.Vault.is_initialized", new=Mock) + @patch("charms.vault_k8s.v0.vault_client.Vault.is_api_available") + @patch("charms.vault_k8s.v0.vault_client.Vault.is_raft_cluster_healthy", new=Mock) + @patch(f"{CERTIFICATE_TRANSFER_LIB_PATH}.CertificateTransferProvides.set_certificate") def test_given_ca_cert_exists_when_certificate_transfer_relation_joins_then_ca_cert_is_advertised( - self, + self, set_certificate, is_api_available, is_sealed ): - self.harness.set_leader(is_leader=True) - self.harness.add_storage(storage_name="certs", attach=True) - root = self.harness.get_filesystem_root(self.container_name) - (root / "vault/certs/ca.pem").write_text(EXAMPLE_CA) - self.harness.set_can_connect(container=self.container_name, val=True) - - app = "traefik" - certificate_transfer_rel_id = self.harness.add_relation( - relation_name="send-ca-cert", remote_app=app - ) - - self.harness.add_relation_unit( - relation_id=certificate_transfer_rel_id, remote_unit_name=f"{app}/0" - ) - - self.harness.charm.tls.send_ca_cert() - - data = self.harness.get_relation_data(certificate_transfer_rel_id, self.harness.charm.unit) - ca_from_rel_data = data["ca"] - self.assertEqual(EXAMPLE_CA, ca_from_rel_data) + is_api_available.return_value = True + is_sealed.return_value = False + with tempfile.TemporaryDirectory() as temp_dir: + # Write the CA cert to the temp dir + ca_cert_path = temp_dir + "/ca.pem" + cert_path = temp_dir + "/cert.pem" + with open(ca_cert_path, "w") as f: + f.write("some ca") + with open(cert_path, "w") as f: + f.write("some cert") + peer_relation = scenario.PeerRelation( + endpoint="vault-peers", + interface="vault-peer", + peers_data={ + 0: {"node_api_address": "http://1.2.3.4"}, + }, + ) + cert_transfer_relation = scenario.Relation( + endpoint="send-ca-cert", + interface="certificate_transfer", + remote_app_name="whatever", + ) + certs_mount = scenario.Mount("/vault/certs", temp_dir) + config_mount = scenario.Mount("/vault/config", temp_dir) + vault_container = scenario.Container( + name="vault", + can_connect=True, + mounts={ + "certs": certs_mount, + "config": config_mount, + }, + ) + certs_storage = scenario.Storage( + name="certs", + ) + config_storage = scenario.Storage( + name="config", + ) + approle_secret = scenario.Secret( + id="0", + label=VAULT_CHARM_APPROLE_SECRET_LABEL, + contents={0: {"role-id": "some role id", "secret-id": "some secret"}}, + owner="app", + ) + ca_certificate_secret = scenario.Secret( + id="1", + label=CA_CERTIFICATE_JUJU_SECRET_LABEL, + contents={0: {"privatekey": "some private key", "certificate": "some cert"}}, + owner="app", + ) + state_in = scenario.State( + containers=[vault_container], + storage=[certs_storage, config_storage], + secrets=[approle_secret, ca_certificate_secret], + leader=True, + relations=[peer_relation, cert_transfer_relation], + ) + + self.ctx.run(cert_transfer_relation.joined_event, state_in) + + set_certificate.assert_called_once_with( + certificate="", + ca="some ca", + chain=[], + relation_id=cert_transfer_relation.relation_id, + ) + @patch("charms.vault_k8s.v0.vault_client.Vault.enable_audit_device", new=Mock) + @patch("charms.vault_k8s.v0.vault_client.Vault.is_active", new=Mock) + @patch("charms.vault_k8s.v0.vault_client.Vault.is_sealed") + @patch("charms.vault_k8s.v0.vault_client.Vault.is_active_or_standby", new=Mock) + @patch("charms.vault_k8s.v0.vault_client.Vault.authenticate", new=Mock) + @patch("charms.vault_k8s.v0.vault_client.Vault.is_initialized", new=Mock) + @patch("charms.vault_k8s.v0.vault_client.Vault.is_api_available") + @patch("charms.vault_k8s.v0.vault_client.Vault.is_raft_cluster_healthy", new=Mock) + @patch(f"{CERTIFICATE_TRANSFER_LIB_PATH}.CertificateTransferProvides.set_certificate") def test_given_ca_cert_is_not_stored_when_certificate_transfer_relation_joins_then_ca_cert_is_not_advertised( - self, + self, set_certificate, is_api_available, is_sealed ): - self._set_peer_relation() - self.harness.set_leader(is_leader=True) - self.harness.set_can_connect(container=self.container_name, val=True) - app = "traefik" - certificate_transfer_rel_id = self.harness.add_relation( - relation_name="send-ca-cert", remote_app=app - ) - - self.harness.add_relation_unit( - relation_id=certificate_transfer_rel_id, remote_unit_name=f"{app}/0" - ) - - relation_data = self.harness.get_relation_data( - certificate_transfer_rel_id, self.harness.charm.unit - ) - - self.assertNotIn("ca", relation_data) - - def test_given_ca_cert_is_not_available_when_certificate_written_then_tls_file_available_in_charm_returns_true( - self, - ): - self.harness.add_storage(storage_name="certs", attach=True) - root = self.harness.get_filesystem_root(self.container_name) - - assert not self.harness.charm.tls.tls_file_available_in_charm(File.CA) - - (root / "vault/certs/ca.pem").write_text(EXAMPLE_CA) - - assert self.harness.charm.tls.tls_file_available_in_charm(File.CA) + is_api_available.return_value = True + is_sealed.return_value = False + with tempfile.TemporaryDirectory() as temp_dir: + peer_relation = scenario.PeerRelation( + endpoint="vault-peers", + interface="vault-peer", + peers_data={ + 0: {"node_api_address": "http://1.2.3.4"}, + }, + ) + cert_transfer_relation = scenario.Relation( + endpoint="send-ca-cert", + interface="certificate_transfer", + remote_app_name="whatever", + ) + config_mount = scenario.Mount("/vault/config", temp_dir) + vault_container = scenario.Container( + name="vault", + can_connect=True, + mounts={ + "config": config_mount, + }, + ) + config_storage = scenario.Storage( + name="config", + ) + approle_secret = scenario.Secret( + id="0", + label=VAULT_CHARM_APPROLE_SECRET_LABEL, + contents={0: {"role-id": "some role id", "secret-id": "some secret"}}, + owner="app", + ) + state_in = scenario.State( + containers=[vault_container], + storage=[config_storage], + secrets=[approle_secret], + leader=True, + relations=[peer_relation, cert_transfer_relation], + ) + + self.ctx.run(cert_transfer_relation.joined_event, state_in) + + set_certificate.assert_not_called() diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py index 57b3e7b0..8a84efa8 100644 --- a/tests/unit/test_charm.py +++ b/tests/unit/test_charm.py @@ -11,17 +11,6 @@ import hcl # type: ignore[import-untyped] import requests from botocore.response import StreamingBody -from charm import ( - AUTOUNSEAL_MOUNT_PATH, - CHARM_POLICY_NAME, - CHARM_POLICY_PATH, - PKI_RELATION_NAME, - S3_RELATION_NAME, - TLS_CERTIFICATES_PKI_RELATION_NAME, - VAULT_CHARM_APPROLE_SECRET_LABEL, - VaultCharm, - config_file_content_matches, -) from charms.tls_certificates_interface.v3.tls_certificates import ( ProviderCertificate, ) @@ -45,6 +34,18 @@ from ops import pebble, testing from ops.model import ActiveStatus, BlockedStatus, WaitingStatus +from charm import ( + AUTOUNSEAL_MOUNT_PATH, + CHARM_POLICY_NAME, + CHARM_POLICY_PATH, + PKI_RELATION_NAME, + S3_RELATION_NAME, + TLS_CERTIFICATES_PKI_RELATION_NAME, + VAULT_CHARM_APPROLE_SECRET_LABEL, + VaultCharm, + config_file_content_matches, +) + S3_RELATION_LIB_PATH = "charms.data_platform_libs.v0.s3" S3_LIB_PATH = "charms.vault_k8s.v0.vault_s3" VAULT_KV_LIB_PATH = "charms.vault_k8s.v0.vault_kv" @@ -986,9 +987,9 @@ def test_given_content_uploaded_to_s3_when_create_backup_action_then_action_succ action_output = self.harness.run_action("create-backup") - self.assertIn("backup-id", action_output.results) + assert "backup-id" in action_output.results backup_id = action_output.results["backup-id"] - self.assertIn(f"vault-backup-{self.model_name}", backup_id) + assert f"vault-backup-{self.model_name}" in backup_id def test_given_s3_relation_not_created_when_list_backups_action_then_action_fails(self): self.harness.set_leader(is_leader=True)