Skip to content

Commit

Permalink
openstack: identity-service forwards compat (juju#730)
Browse files Browse the repository at this point in the history
* openstack: identity-service forwards compat

The new Keystone K8S operator exclusively uses the Application
data bag to provide endpoint information and credentials to
consuming services.

Check for the presence of data in the App data bag and use this
in preference to per-unit data as presented by the Keystone
machine charm.

This also requires some remapping and manipulation to massage
the new application data bag keys into the existing ctxt dict
used in templates.

* Test with app and unit data to ensure app data is preferred
  • Loading branch information
javacruft authored Sep 19, 2022
1 parent 28f26b7 commit 4137a3d
Show file tree
Hide file tree
Showing 2 changed files with 171 additions and 13 deletions.
73 changes: 63 additions & 10 deletions charmhelpers/contrib/openstack/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
network_get_primary_address,
WARNING,
service_name,
remote_service_name,
)

from charmhelpers.core.sysctl import create as sysctl_create
Expand Down Expand Up @@ -348,6 +349,14 @@ def db_ssl(rdata, ctxt, ssl_dir):

class IdentityServiceContext(OSContextGenerator):

_forward_compat_remaps = {
'admin_user': 'admin-user-name',
'service_username': 'service-user-name',
'service_tenant': 'service-project-name',
'service_tenant_id': 'service-project-id',
'service_domain': 'service-domain-name',
}

def __init__(self,
service=None,
service_user=None,
Expand Down Expand Up @@ -400,23 +409,34 @@ def _get_keystone_authtoken_ctxt(self, ctxt, keystonemiddleware_os_rel):
# 'www_authenticate_uri' replaced 'auth_uri' since Stein,
# see keystonemiddleware upstream sources for more info
if CompareOpenStackReleases(keystonemiddleware_os_rel) >= 'stein':
c.update((
('www_authenticate_uri', "{}://{}:{}/v3".format(
ctxt.get('service_protocol', ''),
ctxt.get('service_host', ''),
ctxt.get('service_port', ''))),))
if 'public_auth_url' in ctxt:
c.update((
('www_authenticate_uri', '{}/v3'.format(
ctxt.get('public_auth_url'))),))
else:
c.update((
('www_authenticate_uri', "{}://{}:{}/v3".format(
ctxt.get('service_protocol', ''),
ctxt.get('service_host', ''),
ctxt.get('service_port', ''))),))
else:
c.update((
('auth_uri', "{}://{}:{}/v3".format(
ctxt.get('service_protocol', ''),
ctxt.get('service_host', ''),
ctxt.get('service_port', ''))),))

if 'internal_auth_url' in ctxt:
c.update((
('auth_url', ctxt.get('internal_auth_url')),))
else:
c.update((
('auth_url', "{}://{}:{}/v3".format(
ctxt.get('auth_protocol', ''),
ctxt.get('auth_host', ''),
ctxt.get('auth_port', ''))),))

c.update((
('auth_url', "{}://{}:{}/v3".format(
ctxt.get('auth_protocol', ''),
ctxt.get('auth_host', ''),
ctxt.get('auth_port', ''))),
('project_domain_name', ctxt.get('admin_domain_name', '')),
('user_domain_name', ctxt.get('admin_domain_name', '')),
('project_name', ctxt.get('admin_tenant_name', '')),
Expand Down Expand Up @@ -444,7 +464,27 @@ def __call__(self):
for rid in relation_ids(self.rel_name):
self.related = True
for unit in related_units(rid):
rdata = relation_get(rid=rid, unit=unit)
rdata = {}
# NOTE(jamespage):
# forwards compat with application data
# bag driven approach to relation.
_adata = relation_get(rid=rid, app=remote_service_name(rid))
if _adata:
# New app data bag uses - instead of _
# in key names - remap for compat with
# existing relation data keys
for key, value in _adata.items():
if key == 'api-version':
rdata[key.replace('-', '_')] = value.strip('v')
else:
rdata[key.replace('-', '_')] = value
# Re-map some keys for backwards compatibility
for target, source in self._forward_compat_remaps.items():
rdata[target] = _adata.get(source)
else:
# No app data bag presented - fallback
# to legacy unit based relation data
rdata = relation_get(rid=rid, unit=unit)
serv_host = rdata.get('service_host')
serv_host = format_ipv6_addr(serv_host) or serv_host
auth_host = rdata.get('auth_host')
Expand Down Expand Up @@ -478,6 +518,19 @@ def __call__(self):
'service_project_id': rdata.get('service_tenant_id'),
'service_domain_id': rdata.get('service_domain_id')})

# NOTE:
# keystone-k8s operator presents full URLS
# for all three endpoints - public and internal are
# externally addressable for machine based charm
if 'public_auth_url' in rdata:
ctxt.update({
'public_auth_url': rdata.get('public_auth_url'),
})
if 'internal_auth_url' in rdata:
ctxt.update({
'internal_auth_url': rdata.get('internal_auth_url'),
})

# we keep all veriables in ctxt for compatibility and
# add nested dictionary for keystone_authtoken generic
# templating
Expand Down
111 changes: 108 additions & 3 deletions tests/contrib/openstack/test_os_contexts.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,16 @@ class FakeRelation(object):
passwd = self.relation_get('password', rid='mysql:0', unit='mysql/0')
'''

def __init__(self, relation_data):
self.relation_data = relation_data
def __init__(self, relation_data=None, app_data=None):
self.relation_data = relation_data or {}
self.app_data = app_data or {}

def get(self, attribute=None, unit=None, rid=None):
def get(self, attribute=None, unit=None, rid=None, app=None):
if app:
if attribute is None:
return self.app_data
else:
return self.app_data.get(attribute)
if not rid or rid == 'foo:0':
if attribute is None:
return self.relation_data
Expand Down Expand Up @@ -147,6 +153,36 @@ def relation_units(self, relation_id):
'internal_protocol': 'http',
}

IDENTITY_SERVICE_RELATION_APP_HTTP = {
'admin-auth-url': 'http://keystoneadmin.local:80/keystone',
'admin-domain-id': 'adm-dom-id',
'admin-domain-name': 'admin_domain',
'admin-project-id': 'adm-proj-id',
'admin-project-name': 'admin',
'admin-user-id': 'adm-user-id',
'admin-user-name': 'admin',
'api-version': 'v3',
'auth-host': 'keystoneadmin.local',
'auth-port': '5000',
'auth-protocol': 'http',
'internal-auth-url': 'http://keystoneinternal.local:80/keystone',
'internal-host': 'keystoneinternal.local',
'internal-port': '5000',
'internal-protocol': 'http',
'public-auth-url': 'http://keystonepublic.local:80/keystone',
'service-domain-id': 'svc-dom-id',
'service-domain-name': 'admin_domain',
'service-host': 'keystonepublic.local',
'service-password': 'foo',
'service-port': '5000',
'service-project-id': 'svc-proj-id',
'service-project-name': 'services',
'service-protocol': 'http',
'service-user-id': 'svc-user-id',
'service-user-name': 'svc-user-name',
'service-type': 'volume',
}

IDENTITY_SERVICE_RELATION_UNSET = {
'service_port': '5000',
'service_host': 'keystonehost.local',
Expand Down Expand Up @@ -697,6 +733,7 @@ def relation_units(self, relation_id):
'resolve_address',
'is_ipv6_disabled',
'get_installed_version',
'remote_service_name',
]


Expand Down Expand Up @@ -1190,6 +1227,41 @@ def test_identity_service_context_with_data_https(self, *args):
result.pop('keystone_authtoken')
self.assertEquals(result, expected)

@patch.object(context, 'filter_installed_packages', return_value=[])
@patch.object(context, 'os_release', return_value='rocky')
def test_identity_service_app_context_with_data_http(self, *args):
'''Test identity-service context for forwards compatibility'''
relation = FakeRelation(app_data=IDENTITY_SERVICE_RELATION_APP_HTTP,
relation_data=IDENTITY_SERVICE_RELATION_HTTPS)
self.relation_get.side_effect = relation.get
identity_service = context.IdentityServiceContext()
result = identity_service()
expected = {
'admin_password': 'foo',
'admin_domain_name': 'admin_domain',
'admin_tenant_name': 'services',
'admin_tenant_id': 'svc-proj-id',
'admin_domain_id': 'svc-dom-id',
'service_project_id': 'svc-proj-id',
'service_domain_id': 'svc-dom-id',
'admin_user': 'svc-user-name',
'auth_host': 'keystoneadmin.local',
'auth_port': '5000',
'auth_protocol': 'http',
'service_host': 'keystonepublic.local',
'service_port': '5000',
'service_protocol': 'http',
'service_type': 'volume',
'internal_host': 'keystoneinternal.local',
'internal_port': '5000',
'internal_protocol': 'http',
'api_version': '3',
'public_auth_url': 'http://keystonepublic.local:80/keystone',
'internal_auth_url': 'http://keystoneinternal.local:80/keystone',
}
result.pop('keystone_authtoken')
self.assertEquals(result, expected)

@patch.object(context, 'filter_installed_packages', return_value=[])
@patch.object(context, 'os_release', return_value='rocky')
def test_identity_service_context_with_data_versioned(self, *args):
Expand Down Expand Up @@ -1321,6 +1393,39 @@ def test_keystone_authtoken_www_authenticate_uri_stein_apiv3(self, mock_os_relea

self.assertEquals(keystone_authtoken, expected)

@patch.object(context, 'filter_installed_packages')
@patch.object(context, 'os_release')
def test_keystone_authtoken_www_authenticate_uri_forwards_compat(
self,
mock_os_release,
mock_filter_installed_packages):
relation = FakeRelation(app_data=IDENTITY_SERVICE_RELATION_APP_HTTP)
self.relation_get.side_effect = relation.get

mock_filter_installed_packages.return_value = []
mock_os_release.return_value = 'stein'

identity_service = context.IdentityServiceContext()

cfg_ctx = identity_service()

keystone_authtoken = cfg_ctx.get('keystone_authtoken', {})

expected = collections.OrderedDict((
('auth_type', 'password'),
('www_authenticate_uri', 'http://keystonepublic.local:80/keystone/v3'),
('auth_url', 'http://keystoneinternal.local:80/keystone'),
('project_domain_name', 'admin_domain'),
('user_domain_name', 'admin_domain'),
('project_name', 'services'),
('username', 'svc-user-name'),
('password', 'foo'),
('signing_dir', ''),
('service_type', 'volume'),
))

self.assertEquals(keystone_authtoken, expected)

def test_amqp_context_with_data(self):
'''Test amqp context with all required data'''
relation = FakeRelation(relation_data=AMQP_RELATION)
Expand Down

0 comments on commit 4137a3d

Please sign in to comment.