Add support for multiple client spaces

Some users may not want to expose all vault clients
to the same networks. In particular they might want
to have some on the default access network and some
on an external network. This patch adds support for
new 'external' binding which clients can use to
talk to the vault api.

Change-Id: I0d393c71dcb127b14b8ffcacbd03bbf68f81a53b
Closes-Bug: #1826892
This commit is contained in:
Edward Hope-Morley 2019-05-17 10:26:27 +01:00
parent f9dbf243c7
commit c7e2c531ec
5 changed files with 137 additions and 34 deletions

View File

@ -46,7 +46,10 @@ options:
type: string
default:
description: |
Virtual IP to use api traffic
Virtual IP to use api traffic. You can provide up to two addresses
configured on the access or external bindings. If neither binding
is used then you can only provide one address that must be configured
on the default space.
channel:
type: string
default: stable

View File

@ -19,6 +19,10 @@ import requests
import hvac
import tenacity
from charmhelpers.contrib.network.ip import (
is_address_in_network,
resolve_network_cidr,
)
import charmhelpers.core.hookenv as hookenv
import charmhelpers.core.host as host
import charms.reactive
@ -134,10 +138,32 @@ get_cluster_url = functools.partial(get_vault_url,
binding='cluster', port=8201)
def get_vip(binding=None):
vip = hookenv.config('vip')
if not vip:
return None
vips = vip.split()
if len(vips) == 1:
return vips[0]
if not binding:
binding = 'access'
bound_cidr = resolve_network_cidr(
binding_address(binding)
)
for vip in vips:
if is_address_in_network(bound_cidr, vip):
return vip
return None
def get_access_address():
protocol = 'http'
addr = hookenv.config('dns-ha-access-record')
addr = addr or hookenv.config('vip')
addr = addr or get_vip('access')
addr = addr or binding_address('access')
if charms.reactive.is_state('vault.ssl.available'):
protocol = 'https'

View File

@ -20,6 +20,7 @@ tags:
- security
extra-bindings:
access:
external:
requires:
db:
interface: pgsql

View File

@ -354,17 +354,23 @@ def nagios_servicegroups_changed():
@when('ha.connected')
def cluster_connected(hacluster):
"""Configure HA resources in corosync"""
vip = config('vip')
dns_record = config('dns-ha-access-record')
if vip and dns_record:
vips = config('vip') or None
if vips and dns_record:
set_flag('config.dns_vip.invalid')
log("Unsupported configuration. vip and dns-ha cannot both be set",
level=ERROR)
return
else:
clear_flag('config.dns_vip.invalid')
if vip:
hacluster.add_vip('vault', vip)
if vips:
vips = vips.split()
for vip in vips:
if vip == vault.get_vip(binding='external'):
hacluster.add_vip('vault-ext', vip)
else:
hacluster.add_vip('vault', vip)
elif dns_record:
try:
ip = network_get_primary_address('access')
@ -449,7 +455,7 @@ def configure_secrets_backend():
unit = request['unit']
hostname = request['hostname']
access_address = request['access_address']
access_address = request['ingress_address']
isolated = request['isolated']
unit_name = unit.unit_name.replace('/', '-')
policy_name = approle_name = 'charm-{}'.format(unit_name)
@ -492,14 +498,28 @@ def configure_secrets_backend():
@when('secrets.connected')
def send_vault_url_and_ca():
secrets = endpoint_from_flag('secrets.connected')
vault_url_external = None
if is_flag_set('ha.available'):
if config('hostname'):
vault_url = vault.get_api_url(address=config('hostname'))
hostname = config('hostname')
if hostname:
vault_url = vault.get_api_url(address=hostname)
else:
vault_url = vault.get_api_url(address=config('vip'))
vip = vault.get_vip()
vault_url = vault.get_api_url(address=vip)
ext_vip = vault.get_vip(binding='external')
if ext_vip and ext_vip != vip:
vault_url_external = vault.get_api_url(address=ext_vip,
binding='external')
else:
vault_url = vault.get_api_url()
secrets.publish_url(vault_url=vault_url)
vault_url_external = vault.get_api_url(binding='external')
if vault_url_external == vault_url:
vault_url_external = None
secrets.publish_url(vault_url=vault_url, remote_binding='access')
if vault_url_external:
secrets.publish_url(vault_url=vault_url_external,
remote_binding='external')
if config('ssl-ca'):
secrets.publish_ca(vault_ca=config('ssl-ca'))

View File

@ -1,5 +1,5 @@
import mock
from unittest.mock import patch
from unittest.mock import patch, call
import charms.reactive
@ -490,7 +490,8 @@ class TestHandlers(unit_tests.test_utils.CharmTestCase):
self.assertFalse(handlers.validate_snap_channel('foobar'))
self.assertFalse(handlers.validate_snap_channel('0.10/foobar'))
def test_cluster_connected_vip(self):
@mock.patch.object(handlers.vault, 'get_vip')
def test_cluster_connected_vip(self, mock_get_vip):
charm_config = {
'vip': '10.1.1.1'}
self.config.side_effect = lambda x: charm_config.get(x)
@ -532,6 +533,7 @@ class TestHandlers(unit_tests.test_utils.CharmTestCase):
'hostname': 'juju-123456-0',
'isolated': True,
'access_address': '10.20.4.5',
'ingress_address': '10.20.4.5',
'unit': mock.MagicMock()
})
test_requests[-1]['unit'].unit_name = 'ceph-osd/0'
@ -541,6 +543,7 @@ class TestHandlers(unit_tests.test_utils.CharmTestCase):
'hostname': 'juju-789012-0',
'isolated': True,
'access_address': '10.20.4.20',
'ingress_address': '10.20.4.20',
'unit': mock.MagicMock()
})
test_requests[-1]['unit'].unit_name = 'omg/0'
@ -603,68 +606,118 @@ class TestHandlers(unit_tests.test_utils.CharmTestCase):
mock.call('secrets.refresh'),
])
@mock.patch.object(handlers, 'vault')
def test_send_vault_url_and_ca(self, _vault):
@mock.patch.object(handlers.vault.hookenv, 'network_get_primary_address')
def test_send_vault_url_and_ca(self, mock_network_get_primary_address):
_test_config = {
'vip': '10.5.100.1',
'ssl-ca': 'test-ca',
}
self.config.side_effect = lambda key: _test_config.get(key)
mock_secrets = mock.MagicMock()
def fake_network_get(binding=None):
return '10.5.0.23'
mock_network_get_primary_address.side_effect = fake_network_get
self.endpoint_from_flag.return_value = mock_secrets
self.is_flag_set.return_value = False
_vault.get_api_url.return_value = 'http://10.5.0.23:8200'
handlers.send_vault_url_and_ca()
self.endpoint_from_flag.assert_called_with('secrets.connected')
self.is_flag_set.assert_called_with('ha.available')
_vault.get_api_url.assert_called_once_with()
mock_secrets.publish_url.assert_called_once_with(
vault_url='http://10.5.0.23:8200'
vault_url='http://10.5.0.23:8200',
remote_binding='access'
)
mock_secrets.publish_ca.assert_called_once_with(
vault_ca='test-ca'
)
@mock.patch.object(handlers, 'vault')
def test_send_vault_url_and_ca_ha(self, _vault):
@mock.patch.object(handlers.vault.hookenv, 'network_get_primary_address')
def test_send_vault_url_and_ca_ext(self, mock_network_get_primary_address):
_test_config = {
'vip': '10.5.100.1',
'ssl-ca': 'test-ca',
}
self.config.side_effect = lambda key: _test_config.get(key)
mock_secrets = mock.MagicMock()
def fake_network_get(binding=None):
if binding == 'external':
return '10.6.0.23'
return '10.5.0.23'
mock_network_get_primary_address.side_effect = fake_network_get
self.endpoint_from_flag.return_value = mock_secrets
self.is_flag_set.return_value = True
_vault.get_api_url.return_value = 'http://10.5.100.1:8200'
self.is_flag_set.return_value = False
handlers.send_vault_url_and_ca()
self.endpoint_from_flag.assert_called_with('secrets.connected')
self.is_flag_set.assert_called_with('ha.available')
_vault.get_api_url.assert_called_once_with(address='10.5.100.1')
mock_secrets.publish_url.assert_called_once_with(
vault_url='http://10.5.100.1:8200'
mock_secrets.publish_url.assert_has_calls(
[call(vault_url='http://10.5.0.23:8200',
remote_binding='access'),
call(vault_url='http://10.6.0.23:8200',
remote_binding='external')]
)
mock_secrets.publish_ca.assert_called_once_with(
vault_ca='test-ca'
)
@mock.patch.object(handlers, 'vault')
def test_send_vault_url_and_ca_hostname(self, _vault):
@mock.patch('charmhelpers.contrib.network.ip.get_netmask_for_address')
@mock.patch.object(handlers.vault.hookenv, 'config')
@mock.patch.object(handlers.vault.hookenv, 'network_get_primary_address')
def test_send_vault_url_and_ca_ha(self,
mock_network_get_primary_address,
mock_config,
mock_get_netmask_for_address):
_test_config = {
'vip': '10.5.100.1 10.6.100.1',
'ssl-ca': 'test-ca',
'hostname': None
}
mock_get_netmask_for_address.return_value = 16
self.config.side_effect = lambda key: _test_config.get(key)
mock_config.side_effect = lambda key: _test_config.get(key)
mock_secrets = mock.MagicMock()
def fake_network_get(binding=None):
if binding == 'external':
return '10.6.0.23'
return '10.5.0.23'
mock_network_get_primary_address.side_effect = fake_network_get
self.endpoint_from_flag.return_value = mock_secrets
self.is_flag_set.return_value = True
handlers.send_vault_url_and_ca()
self.endpoint_from_flag.assert_called_with('secrets.connected')
self.is_flag_set.assert_called_with('ha.available')
mock_secrets.publish_url.assert_has_calls(
[call(vault_url='http://10.5.100.1:8200',
remote_binding='access'),
call(vault_url='http://10.6.100.1:8200',
remote_binding='external')]
)
mock_secrets.publish_ca.assert_called_once_with(
vault_ca='test-ca'
)
def test_send_vault_url_and_ca_hostname(self):
_test_config = {
'vip': '10.5.100.1',
'ssl-ca': 'test-ca',
'hostname': 'vault',
}
self.config.side_effect = lambda key: _test_config.get(key)
mock_secrets = mock.MagicMock()
self.endpoint_from_flag.return_value = mock_secrets
self.is_flag_set.return_value = True
_vault.get_api_url.return_value = 'https://vault:8200'
handlers.send_vault_url_and_ca()
self.endpoint_from_flag.assert_called_with('secrets.connected')
self.is_flag_set.assert_called_with('ha.available')
_vault.get_api_url.assert_called_once_with(address='vault')
mock_secrets.publish_url.assert_called_once_with(
vault_url='https://vault:8200'
mock_secrets.publish_url.assert_has_calls(
[call(vault_url='http://vault:8200', remote_binding='access')]
)
mock_secrets.publish_ca.assert_called_once_with(
vault_ca='test-ca'