Add forwards compat with k8s operator

The new keystone-k8s operator uses the application data bag and
more up-to-date key names for endpoint and authentication information.

Check for this information and then fallback to the existing
keystone charm unit data bag data set if not found.

Update register_endpoints to also provide new application data
bag JSON encoded data when this method is called from a lead
unit.  This relies on the type and description of the endpoint
being provided which will require a charm change on adoption.

Change-Id: I921d173c64b12c35f5ffc17270a0fc2bb83891c4
This commit is contained in:
James Page 2022-09-08 09:33:46 +01:00
parent 9ad5cade97
commit 1a5142bac6
3 changed files with 148 additions and 17 deletions

View File

@ -23,6 +23,7 @@ class KeystoneAutoAccessors(type):
Metaclass that converts fields referenced by ``auto_accessors`` into Metaclass that converts fields referenced by ``auto_accessors`` into
accessor methods with very basic doc strings. accessor methods with very basic doc strings.
""" """
def __new__(cls, name, parents, dct): def __new__(cls, name, parents, dct):
for field in dct.get('auto_accessors', []): for field in dct.get('auto_accessors', []):
meth_name = field.replace('-', '_') meth_name = field.replace('-', '_')
@ -37,24 +38,64 @@ class KeystoneAutoAccessors(type):
@staticmethod @staticmethod
def _accessor(field): def _accessor(field):
def __accessor(self): def _accessor_internal(self):
return self.all_joined_units.received.get(field) # Use remapped or transposed key for application
return __accessor # data bag lookup for forwards compat
app_field = self._forward_compat_remaps.get(
field,
field.replace('_', '-')
)
return self.relations[0].received_app_raw.get(
app_field,
self.all_joined_units.received.get(field)
)
return _accessor_internal
class KeystoneRequires(reactive.Endpoint, metaclass=KeystoneAutoAccessors): class KeystoneRequires(reactive.Endpoint, metaclass=KeystoneAutoAccessors):
auto_accessors = ['service_host', 'service_protocol', auto_accessors = [
'service_port', 'service_tenant', 'service_username', 'service_host',
'service_password', 'service_tenant_id', 'auth_host', 'service_protocol',
'auth_protocol', 'auth_port', 'admin_token', 'ssl_key', 'service_port',
'ca_cert', 'ssl_cert', 'https_keystone', 'service_tenant',
'ssl_cert_admin', 'ssl_cert_internal', 'service_username',
'ssl_cert_public', 'ssl_key_admin', 'ssl_key_internal', 'service_password',
'ssl_key_public', 'api_version', 'service_domain', 'service_tenant_id',
'service_domain_id', 'ep_changed', 'auth_host',
'admin_domain_id', 'admin_user_id', 'admin_project_id', 'auth_protocol',
'service_type'] 'auth_port',
'admin_token',
'ssl_key',
'ca_cert',
'ssl_cert',
'https_keystone',
'ssl_cert_admin',
'ssl_cert_internal',
'ssl_cert_public',
'ssl_key_admin',
'ssl_key_internal',
'ssl_key_public',
'api_version',
'service_domain',
'service_domain_id',
'ep_changed',
'admin_domain_id',
'admin_user_id',
'admin_project_id',
'service_type',
'public-auth-url',
'internal-auth-url',
'admin-auth-url',
]
_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',
}
@reactive.when('endpoint.{endpoint_name}.joined') @reactive.when('endpoint.{endpoint_name}.joined')
def joined(self): def joined(self):
@ -146,7 +187,9 @@ class KeystoneRequires(reactive.Endpoint, metaclass=KeystoneAutoAccessors):
def register_endpoints(self, service, region, public_url, internal_url, def register_endpoints(self, service, region, public_url, internal_url,
admin_url, requested_roles=None, admin_url, requested_roles=None,
add_role_to_admin=None): add_role_to_admin=None,
service_type=None,
service_description=None):
""" """
Register this service with keystone Register this service with keystone
""" """
@ -166,6 +209,26 @@ class KeystoneRequires(reactive.Endpoint, metaclass=KeystoneAutoAccessors):
for relation in self.relations: for relation in self.relations:
relation.to_publish_raw.update(relation_info) relation.to_publish_raw.update(relation_info)
# NOTE: forwards compatible data presentation for keystone-k8s
if all((service_type,
service_description,
reactive.is_flag_set('leadership.is_leader'),)):
application_info = {
'region': region,
'service-endpoints': json.dumps([
{
'service_name': service,
'type': service_type,
'description': service_description,
'internal_url': internal_url,
'admin_url': admin_url,
'public_url': public_url,
}
], sort_keys=True)
}
for relation in self.relations:
relation.to_publish_app_raw.update(application_info)
def request_keystone_endpoint_information(self): def request_keystone_endpoint_information(self):
self.register_endpoints('None', 'None', 'None', 'None', 'None') self.register_endpoints('None', 'None', 'None', 'None', 'None')

View File

@ -81,4 +81,4 @@ commands = {posargs}
[flake8] [flake8]
# E402 ignore necessary for path append before sys module import in actions # E402 ignore necessary for path append before sys module import in actions
ignore = E402 ignore = E402 W503

View File

@ -10,6 +10,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import json
from unittest import mock from unittest import mock
@ -19,6 +20,35 @@ import charms_openstack.test_utils as test_utils
_hook_args = {} _hook_args = {}
IDENTITY_APP_DATA = {
'api-version': '3',
'auth-host': 'authhost',
'auth-port': '5000',
'auth-protocol': 'http',
'internal-host': 'internalhost',
'internal-port': '5000',
'internal-protocol': 'http',
'service-host': 'servicehost',
'service-port': '5000',
'service-protocol': 'http',
'admin-domain-name': 'admin-domain',
'admin-domain-id': 'ca9e66dd-920c-493c-8ebd-dcc893afcc3b',
'admin-project-name': 'admin',
'admin-project-id': '5c9fd12c-87eb-4688-931a-05da83db14ad',
'admin-user-name': 'admin',
'admin-user-id': 'cc28fa26-70bc-4acb-97a4-5614799257bb',
'service-domain-name': 'service-domain',
'service-domain-id': '8fa8e4c1-b9f6-44ae-b646-0626d44786c2',
'service-project-name': 'services',
'service-project-id': '0626e4d8-0846-4fd5-98c9-324fbbe24301',
'service-user-name': 'gnocchi',
'service-user-id': 'fa8c4a9a-f97c-41e7-a204-73571c5a7b51',
'service-password': 'foobar',
'internal-auth-url': 'http://internalhost:80/keystone',
'admin-auth-url': 'http://adminhost:80/keystone',
'public-auth-url': 'http://publichost:80/keystone',
}
class TestKeystoneRequires(test_utils.PatchHelper): class TestKeystoneRequires(test_utils.PatchHelper):
@ -73,6 +103,25 @@ class TestKeystoneRequires(test_utils.PatchHelper):
self.service_tenant.return_value = None self.service_tenant.return_value = None
assert self.target.base_data_complete() is False assert self.target.base_data_complete() is False
def test_app_data_complete(self):
relation = mock.MagicMock()
relation.received_app_raw.get.side_effect = (
lambda k, d: IDENTITY_APP_DATA.get(k, d)
)
self.target._relations = [relation]
self.assertEqual(self.target.service_host(), 'servicehost')
self.assertEqual(self.target.auth_host(), 'authhost')
self.assertEqual(
self.target.public_auth_url(), 'http://publichost:80/keystone')
self.assertEqual(self.target.service_tenant(), 'services')
self.assertEqual(self.target.service_password(), 'foobar')
self.assertEqual(
self.target.service_tenant_id(),
'0626e4d8-0846-4fd5-98c9-324fbbe24301')
self.assertTrue(self.target.base_data_complete())
self.assertFalse(self.target.ssl_data_complete())
self.assertFalse(self.target.ssl_data_complete_legacy())
def test_ssl_data_complete(self): def test_ssl_data_complete(self):
self.patch_target('ssl_cert_admin', '1') self.patch_target('ssl_cert_admin', '1')
self.patch_target('ssl_cert_internal', '2') self.patch_target('ssl_cert_internal', '2')
@ -152,10 +201,14 @@ class TestKeystoneRequires(test_utils.PatchHelper):
'endpoint.some-relation.changed') 'endpoint.some-relation.changed')
def test_register_endpoints(self): def test_register_endpoints(self):
self.patch_object(requires.reactive, 'is_flag_set')
self.is_flag_set.return_value = True
relation = mock.MagicMock() relation = mock.MagicMock()
self.patch_target('_relations') self.patch_target('_relations')
self._relations.__iter__.return_value = [relation] self._relations.__iter__.return_value = [relation]
self.target.register_endpoints('s', 'r', 'p_url', 'i_url', 'a_url') self.target.register_endpoints(
's', 'r', 'p_url', 'i_url', 'a_url',
service_type='stype', service_description='sdesc')
result = { result = {
'service': 's', 'service': 's',
'public_url': 'p_url', 'public_url': 'p_url',
@ -164,6 +217,21 @@ class TestKeystoneRequires(test_utils.PatchHelper):
'region': 'r', 'region': 'r',
} }
relation.to_publish_raw.update.assert_called_once_with(result) relation.to_publish_raw.update.assert_called_once_with(result)
# This should only happen when the charm is the leader and
# register_endpoints is called with type and description
# information.
relation.to_publish_app_raw.update.assert_called_once_with({
'region': 'r',
'service-endpoints': json.dumps([{
"admin_url": "a_url",
"description": "sdesc",
"internal_url": "i_url",
"public_url": "p_url",
"service_name": "s",
"type": "stype"}],
sort_keys=True
)
})
def test_register_endpoints_requested_roles(self): def test_register_endpoints_requested_roles(self):
relation = mock.MagicMock() relation = mock.MagicMock()