diff --git a/src/README.md b/src/README.md index 3196ef7..dd6c6f6 100644 --- a/src/README.md +++ b/src/README.md @@ -18,3 +18,10 @@ deploying on bionic, you'll need to deploy a 9.x version of PostgreSQL. After deploying and relating the charm to postgresql, install the vault snap locally and use "vault init" to create the master key shards and the root token, and store them safely. + +## Network Spaces support + +The vault charm directly supports network binding via the 'access' +extra-binding and the 'cluster' peer relation. These allow the Vault +API and inter-unit Cluster addresses to be configured using Juju +network spaces. diff --git a/src/metadata.yaml b/src/metadata.yaml index 3316ce8..92f35e9 100644 --- a/src/metadata.yaml +++ b/src/metadata.yaml @@ -16,6 +16,8 @@ series: - bionic tags: - security +extra-bindings: + access: requires: db: interface: pgsql @@ -30,3 +32,6 @@ provides: nrpe-external-master: interface: nrpe-external-master scope: container +peers: + cluster: + interface: vault-ha diff --git a/src/reactive/vault.py b/src/reactive/vault.py index 9ebeb4c..e004a83 100644 --- a/src/reactive/vault.py +++ b/src/reactive/vault.py @@ -1,4 +1,5 @@ import base64 +import functools import hvac import psycopg2 import requests @@ -23,6 +24,7 @@ from charmhelpers.core.hookenv import ( application_version_set, atexit, local_unit, + network_get_primary_address, ) from charmhelpers.core.host import ( @@ -136,9 +138,10 @@ def configure_vault(context): key=context['etcd_tls_key_file'], cert=context['etcd_tls_cert_file'], ca=context['etcd_tls_ca_file']) - context['vault_api_url'] = get_api_url() - log("Etcd detected, setting vault_api_url to {}".format( - context['vault_api_url'])) + context['api_addr'] = get_api_url() + context['cluster_addr'] = get_cluster_url() + log("Etcd detected, setting api_addr to {}".format( + context['api_addr'])) else: log("Etcd not detected", level=DEBUG) log("Rendering vault.hcl.j2", level=DEBUG) @@ -162,15 +165,27 @@ def configure_vault(context): open_port(8200) -def get_api_url(): +def binding_address(binding): + try: + return network_get_primary_address(binding) + except NotImplementedError: + return unit_private_ip() + + +def get_vault_url(binding, port): protocol = 'http' - port = '8200' - ip = unit_private_ip() + ip = binding_address(binding) if is_state('vault.ssl.available'): protocol = 'https' return '{}://{}:{}'.format(protocol, ip, port) +get_api_url = functools.partial(get_vault_url, + binding='access', port=8200) +get_cluster_url = functools.partial(get_vault_url, + binding='cluster', port=8201) + + @when('snap.installed.vault') @when_not('configured') @when('db.master.available') diff --git a/src/templates/vault.hcl.j2 b/src/templates/vault.hcl.j2 index 2a9836a..f4719c8 100644 --- a/src/templates/vault.hcl.j2 +++ b/src/templates/vault.hcl.j2 @@ -1,5 +1,8 @@ -{%- if vault_api_url %} -api_addr = "{{ vault_api_url }}" +{%- if api_addr %} +api_addr = "{{ api_addr }}" +{%- endif %} +{%- if cluster_addr %} +cluster_addr = "{{ cluster_addr }}" {%- endif %} {%- if disable_mlock %} disable_mlock = true @@ -19,21 +22,19 @@ storage "mysql" { {%- if etcd_conn %} ha_storage "etcd" { ha_enabled = "true" - address = "{{ etcd_conn }}" - tls_ca_file = "{{ etcd_tls_ca_file }}" - tls_cert_file = "{{ etcd_tls_cert_file }}" - tls_key_file = "{{ etcd_tls_key_file }}" + address = "{{ etcd_conn }}" + tls_ca_file = "{{ etcd_tls_ca_file }}" + tls_cert_file = "{{ etcd_tls_cert_file }}" + tls_key_file = "{{ etcd_tls_key_file }}" etcd_api = "v3" } {%- endif %} listener "tcp" { - address = "0.0.0.0:8200" + address = "0.0.0.0:8200" {%- if ssl_available %} - tls_cert_file = "/var/snap/vault/common/vault.crt" - tls_key_file = "/var/snap/vault/common/vault.key" + tls_cert_file = "/var/snap/vault/common/vault.crt" + tls_key_file = "/var/snap/vault/common/vault.key" {%- else %} - tls_disable = 1 + tls_disable = 1 {%- endif %} -} - - +} \ No newline at end of file diff --git a/unit_tests/test_vault.py b/unit_tests/test_vault.py index e557c79..705aa47 100644 --- a/unit_tests/test_vault.py +++ b/unit_tests/test_vault.py @@ -64,6 +64,7 @@ class TestHandlers(unittest.TestCase): 'unit_private_ip', 'application_version_set', 'local_unit', + 'network_get_primary_address', ] self.patch_all() @@ -218,12 +219,15 @@ class TestHandlers(unittest.TestCase): ]) @patch.object(handlers, 'save_etcd_client_credentials') + @patch.object(handlers, 'get_cluster_url') @patch.object(handlers, 'can_restart') @patch.object(handlers, 'get_api_url') def test_configure_vault_etcd(self, get_api_url, can_restart, + get_cluster_url, save_etcd_client_credentials): can_restart.return_value = True - get_api_url.return_value = 'http://this-unit' + get_api_url.return_value = 'http://this-unit:8200' + get_cluster_url.return_value = 'http://this-unit:8201' self.config.return_value = {'disable-mlock': False} etcd_mock = mock.MagicMock() etcd_mock.connection_string.return_value = 'http://etcd' @@ -237,7 +241,8 @@ class TestHandlers(unittest.TestCase): 'etcd_tls_ca_file': '/var/snap/vault/common/etcd-ca.pem', 'etcd_tls_cert_file': '/var/snap/vault/common/etcd-cert.pem', 'etcd_tls_key_file': '/var/snap/vault/common/etcd.key', - 'vault_api_url': 'http://this-unit'} + 'api_addr': 'http://this-unit:8200', + 'cluster_addr': 'http://this-unit:8201'} render_calls = [ mock.call( 'vault.hcl.j2', @@ -294,13 +299,27 @@ class TestHandlers(unittest.TestCase): def test_get_api_url_ssl(self): self.is_state.return_value = True - self.unit_private_ip.return_value = '1.2.3.4' + self.network_get_primary_address.return_value = '1.2.3.4' self.assertEqual(handlers.get_api_url(), 'https://1.2.3.4:8200') + self.network_get_primary_address.assert_called_with('access') def test_get_api_url_nossl(self): self.is_state.return_value = False - self.unit_private_ip.return_value = '1.2.3.4' + self.network_get_primary_address.return_value = '1.2.3.4' self.assertEqual(handlers.get_api_url(), 'http://1.2.3.4:8200') + self.network_get_primary_address.assert_called_with('access') + + def test_get_cluster_url_ssl(self): + self.is_state.return_value = True + self.network_get_primary_address.return_value = '1.2.3.4' + self.assertEqual(handlers.get_cluster_url(), 'https://1.2.3.4:8201') + self.network_get_primary_address.assert_called_with('cluster') + + def test_get_cluster_url_nossl(self): + self.is_state.return_value = False + self.network_get_primary_address.return_value = '1.2.3.4' + self.assertEqual(handlers.get_cluster_url(), 'http://1.2.3.4:8201') + self.network_get_primary_address.assert_called_with('cluster') def test_cluster_connected(self): self.config.return_value = '10.1.1.1'