Implement microversions

This patch introduces a new `microversion` parameter to the factory
method for the Client class.  The parameter is used to set
`default_microversion` in the HTTPClient.

Co-Authored-By: Andre Aranha <afariasa@redhat.com>
Co-Authored-By: Grzegorz Grasza <xek@redhat.com>
Depends-On: https://review.opendev.org/c/openstack/keystoneauth/+/865439

Change-Id: I2a0a5315daca8ce5bd6164cebbd2f917348c7675
This commit is contained in:
Douglas Mendizábal 2022-11-03 10:59:18 -05:00 committed by Grzegorz Grasza
parent 9a457b434d
commit b3f3912a71
6 changed files with 236 additions and 130 deletions

View File

@ -30,21 +30,22 @@ LOG = logging.getLogger(__name__)
_DEFAULT_SERVICE_TYPE = 'key-manager'
_DEFAULT_SERVICE_INTERFACE = 'public'
_DEFAULT_API_VERSION = 'v1'
# TODO(dmendiza) Default to '1.1'
_DEFAULT_API_MICROVERSION = '1.0'
_SUPPORTED_API_VERSION_MAP = {'v1': 'barbicanclient.v1.client.Client'}
class _HTTPClient(adapter.Adapter):
def __init__(self, session, project_id=None, **kwargs):
kwargs.setdefault('interface', _DEFAULT_SERVICE_INTERFACE)
kwargs.setdefault('service_type', _DEFAULT_SERVICE_TYPE)
kwargs.setdefault('version', _DEFAULT_API_VERSION)
endpoint = kwargs.pop('endpoint', None)
super(_HTTPClient, self).__init__(session, **kwargs)
if endpoint:
self.endpoint_override = '{0}/{1}'.format(endpoint, self.version)
kwargs['endpoint_override'] = "{}/{}/".format(
endpoint.rstrip('/'),
kwargs.get('version')
)
super().__init__(session, **kwargs)
if project_id is None:
self._default_headers = dict()
@ -122,61 +123,73 @@ class _HTTPClient(adapter.Adapter):
def Client(version=None, session=None, *args, **kwargs):
"""Barbican client used to interact with barbican service.
:param version: The API version to use.
:param session: An instance of keystoneauth1.session.Session that
can be either authenticated, or not authenticated. When using
a non-authenticated Session, you must provide some additional
parameters. When no session is provided it will default to a
non-authenticated Session.
:param endpoint: Barbican endpoint url. Required when a session is not
given, or when using a non-authenticated session.
non-authenticated Session. (optional)
:param endpoint: Barbican endpoint url override. Required when a
session is not given, or when using a non-authenticated session.
When using an authenticated session, the client will attempt
to get an endpoint from the session.
to get the endpoint from the Keystone service catalog. (optional)
:param project_id: The project ID used for context in Barbican.
Required when a session is not given, or when using a
non-authenticated session.
When using an authenticated session, the project ID will be
provided by the authentication mechanism.
provided by the authentication mechanism and this parameter
will be ignored. (optional)
:param verify: When a session is not given, the client will create
a non-authenticated session. This parameter is passed to the
session that is created. If set to False, it allows
barbicanclient to perform "insecure" TLS (https) requests.
The server's certificate will not be verified against any
certificate authorities.
certificate authorities. (optional)
WARNING: This option should be used with caution.
:param version: Used as an endpoint filter when using an authenticated
keystone session. When using a non-authenticated keystone session,
this value is appended to the required endpoint url override.
Defaults to 'v1'.
:param service_type: Used as an endpoint filter when using an
authenticated keystone session. Defaults to 'key-manager'.
authenticated keystone session.
Defaults to 'key-manager'.
:param service_name: Used as an endpoint filter when using an
authenticated keystone session.
:param interface: Used as an endpoint filter when using an
authenticated keystone session. Defaults to 'public'.
:param region_name: Used as an endpoint filter when using an
authenticated keystone session.
:param microversion: Specifiy an API Microversion to be used.
Defaults to '1.1'.
"""
LOG.debug("Creating Client object")
if not session:
session = ks_session.Session(verify=kwargs.pop('verify', True))
if session.auth is None and kwargs.get('auth') is None:
if not kwargs.get('endpoint'):
raise ValueError('Barbican endpoint url must be provided when '
'not using auth in the Keystone Session.')
if session.auth is None:
if kwargs.get('auth') is None:
if not kwargs.get('endpoint'):
raise ValueError('Barbican endpoint url must be provided when'
' not using auth in the Keystone Session.')
if kwargs.get('project_id') is None:
raise ValueError('Project ID must be provided when not using '
'auth in the Keystone Session')
else:
session.auth = kwargs['auth']
if kwargs.get('project_id') is None:
raise ValueError('Project ID must be provided when not using '
'auth in the Keystone Session')
if not version:
version = _DEFAULT_API_VERSION
kwargs['version'] = version or _DEFAULT_API_VERSION
kwargs.setdefault('service_type', _DEFAULT_SERVICE_TYPE)
kwargs.setdefault('interface', _DEFAULT_SERVICE_INTERFACE)
kwargs.setdefault('microversion', _DEFAULT_API_MICROVERSION)
try:
client_path = _SUPPORTED_API_VERSION_MAP[version]
client_path = _SUPPORTED_API_VERSION_MAP[kwargs['version']]
client_class = importutils.import_class(client_path)
return client_class(session=session, *args, **kwargs)
except (KeyError, ValueError):
supported_versions = ', '.join(_SUPPORTED_API_VERSION_MAP.keys())
msg = ("Invalid client version %(version)s; must be one of: "
"%(versions)s") % {'version': version,
"%(versions)s") % {'version': kwargs.get('version'),
'versions': supported_versions}
raise exceptions.UnsupportedVersion(msg)

View File

@ -14,18 +14,59 @@
# limitations under the License.
import io
from requests_mock.contrib import fixture
import testtools
from barbicanclient import barbican as barb
from barbicanclient.barbican import Barbican
from barbicanclient import client
from barbicanclient import exceptions
from barbicanclient.tests import keystone_client_fixtures
from barbicanclient.tests import test_client
class WhenTestingBarbicanCLI(test_client.BaseEntityResource):
class WhenTestingBarbicanCLI(testtools.TestCase):
def setUp(self):
self._setUp('barbican')
super().setUp()
self.endpoint = 'http://localhost:9311/'
self.project_id = '1234567890abcdef1234567890abcdef'
self.responses = self.useFixture(fixture.Fixture())
self.responses.get(
'http://localhost:9311/v1/',
json={
'version': {
'id': 'v1',
'status': 'CURRENT',
'min_version': '1.0',
'max_version': '1.1',
'links': [{
'rel': 'self',
'href': 'http://192.168.122.110/key-manager/v1/'
}, {
'rel': 'describedby',
'type': 'text/html',
'href': 'https://docs.openstack.org/'}]}})
self.responses.get(
'http://localhost:9311/',
json={
"versions": {
"values": [{
"id": "v1",
"status": "stable",
"links": [{
"rel": "self",
"href": "http://localhost:9311/v1/"
}, {
"rel": "describedby",
"type": "text/html",
"href": "https://docs.openstack.org/"
}],
"media-types": [{
"type": "application/vnd.openstack.key-manager-v1"
"+json",
"base": "application/json",
}]}]}}
)
self.captured_stdout = io.StringIO()
self.captured_stderr = io.StringIO()
self.barbican = Barbican(
@ -86,12 +127,15 @@ class WhenTestingBarbicanCLI(test_client.BaseEntityResource):
'--no-auth --endpoint {0} --os-tenant-id {1}'
'secret list'.format(self.endpoint, self.project_id)
)
list_secrets_url = '{0}/v1/secrets'.format(self.endpoint)
list_secrets_url = '{0}/v1/secrets'.format(self.endpoint.rstrip('/'))
self.responses.get(list_secrets_url, json={"secrets": [], "total": 0})
client = self.create_and_assert_client(args)
secret_list = client.secrets.list()
self.assertTrue(self.responses._adapter.called)
self.assertEqual(1, self.responses._adapter.call_count)
# there should be two requests
# 1. requests sent by microversions check
# 2. the request under test.
self.assertEqual(2, self.responses._adapter.call_count)
self.assertEqual([], secret_list)
def test_should_error_if_required_keystone_auth_arguments_are_missing(
@ -158,7 +202,7 @@ class WhenTestingBarbicanCLI(test_client.BaseEntityResource):
self.assertEqual(1, response)
def test_default_endpoint_filter_kwargs_set_correctly(self):
auth_args = ('--no-auth --endpoint http://barbican_endpoint:9311/v1 '
auth_args = ('--no-auth --endpoint http://localhost:9311/ '
'--os-project-id project1')
argv, remainder = self.parser.parse_known_args(auth_args.split())
barbican_client = self.barbican.create_client(argv)
@ -171,7 +215,7 @@ class WhenTestingBarbicanCLI(test_client.BaseEntityResource):
self.assertIsNone(httpclient.service_name)
def test_endpoint_filter_kwargs_set_correctly(self):
auth_args = ('--no-auth --endpoint http://barbican_endpoint:9311 '
auth_args = ('--no-auth --endpoint http://localhost:9311/ '
'--os-project-id project1')
endpoint_filter_args = ('--interface private '
'--service-type custom-type '
@ -190,7 +234,7 @@ class WhenTestingBarbicanCLI(test_client.BaseEntityResource):
self.assertEqual('v1', httpclient.version)
def test_should_fail_if_provide_unsupported_api_version(self):
auth_args = ('--no-auth --endpoint http://barbican_endpoint:9311/v1 '
auth_args = ('--no-auth --endpoint http://localhost:9311/ '
'--os-project-id project1')
endpoint_filter_args = ('--interface private '
'--service-type custom-type '
@ -210,16 +254,16 @@ class WhenTestingBarbicanCLI(test_client.BaseEntityResource):
'--file foo --payload'
'secret get'.format(self.endpoint, self.project_id)
)
list_secrets_url = '{0}/v1/secrets'.format(self.endpoint)
list_secrets_url = '{0}/v1/secrets'.format(self.endpoint.rstrip('/'))
self.responses.get(list_secrets_url, json={"secrets": [], "total": 0})
client = self.create_and_assert_client(args)
secret_list = client.secrets.list()
self.assertTrue(self.responses._adapter.called)
self.assertEqual(1, self.responses._adapter.call_count)
self.assertEqual(2, self.responses._adapter.call_count)
self.assertEqual([], secret_list)
def test_insecure_true_kwargs_set_correctly(self):
auth_args = ('--no-auth --endpoint https://barbican_endpoint:9311/v1 '
auth_args = ('--no-auth --endpoint http://localhost:9311/ '
'--os-project-id project1')
endpoint_filter_args = ('--interface public '
'--service-type custom-type '
@ -235,29 +279,6 @@ class WhenTestingBarbicanCLI(test_client.BaseEntityResource):
httpclient = barbican_client.secrets._api
self.assertFalse(httpclient.session.verify)
def test_cafile_certfile_keyfile_kwargs_set_correctly(self):
auth_args = ('no_auth '
'--os-auth-url https://keystone_endpoint:5000/v2 '
'--os-auth-token f554ccb5-e157-4824-b67b-d139c87bc555 '
'--os-project-id project1')
endpoint_filter_args = ('--interface public '
'--service-type custom-type '
'--service-name Burrbican '
'--region-name RegionTwo '
'--barbican-api-version v1')
args = auth_args + ' ' + endpoint_filter_args
argv, remainder = self.parser.parse_known_args(args.split())
argv.os_cacert = 'ca.pem'
argv.os_cert = 'cert.pem'
argv.os_key = 'key.pem'
argv.os_identity_api_version = '2.0'
argv.os_tenant_name = 'my_tenant_name'
barbican_client = self.barbican.create_client(argv)
httpclient = barbican_client.secrets._api
self.assertEqual('ca.pem', httpclient.session.verify)
self.assertEqual('cert.pem', httpclient.session.cert[0])
self.assertEqual('key.pem', httpclient.session.cert[1])
class TestBarbicanWithKeystonePasswordAuth(
keystone_client_fixtures.KeystoneClientFixture):

View File

@ -27,8 +27,45 @@ class TestClient(testtools.TestCase):
def setUp(self):
super(TestClient, self).setUp()
self.responses = self.useFixture(fixture.Fixture())
self.endpoint = 'http://localhost:9311'
self.responses = self.useFixture(fixture.Fixture())
self.responses.get(
'http://localhost:9311/v1/',
json={
'version': {
'id': 'v1',
'status': 'CURRENT',
'min_version': '1.0',
'max_version': '1.1',
'links': [{
'rel': 'self',
'href': 'http://192.168.122.110/key-manager/v1/'
}, {
'rel': 'describedby',
'type': 'text/html',
'href': 'https://docs.openstack.org/'}]}})
self.responses.get(
'http://localhost:9311/',
json={
"versions": {
"values": [{
"id": "v1",
"status": "stable",
"links": [{
"rel": "self",
"href": "http://localhost:9311/v1/"
}, {
"rel": "describedby",
"type": "text/html",
"href": "https://docs.openstack.org/"
}],
"media-types": [{
"type": "application/vnd.openstack.key-manager-v1"
"+json",
"base": "application/json",
}]}]}}
)
self.project_id = 'project_id'
self.session = session.Session()
self.httpclient = client._HTTPClient(session=self.session,
@ -39,10 +76,11 @@ class TestClient(testtools.TestCase):
class WhenTestingClientInit(TestClient):
def test_api_version_is_appended_to_endpoint(self):
c = client._HTTPClient(session=self.session,
endpoint=self.endpoint,
project_id=self.project_id)
self.assertEqual('http://localhost:9311/v1', c.endpoint_override)
c = client.Client(session=self.session,
endpoint=self.endpoint,
project_id=self.project_id)
self.assertEqual('http://localhost:9311/v1/',
c.client.endpoint_override)
def test_default_headers_are_empty(self):
c = client._HTTPClient(session=self.session, endpoint=self.endpoint)
@ -65,17 +103,18 @@ class WhenTestingClientInit(TestClient):
**{"endpoint": self.endpoint})
def test_endpoint_override_starts_with_endpoint_url(self):
c = client._HTTPClient(session=self.session,
endpoint=self.endpoint,
project_id=self.project_id)
self.assertTrue(c.endpoint_override.startswith(self.endpoint))
c = client.Client(session=self.session,
endpoint=self.endpoint,
project_id=self.project_id)
self.assertTrue(c.client.endpoint_override.startswith(self.endpoint))
def test_endpoint_override_ends_with_default_api_version(self):
c = client._HTTPClient(session=self.session,
endpoint=self.endpoint,
project_id=self.project_id)
self.assertTrue(
c.endpoint_override.endswith(client._DEFAULT_API_VERSION))
c = client.Client(session=self.session,
endpoint=self.endpoint,
project_id=self.project_id)
self.assertTrue(c.client.endpoint_override.rstrip('/').endswith(
client._DEFAULT_API_VERSION
))
class WhenTestingClientPost(TestClient):
@ -83,7 +122,8 @@ class WhenTestingClientPost(TestClient):
def setUp(self):
super(WhenTestingClientPost, self).setUp()
self.httpclient = client._HTTPClient(session=self.session,
endpoint=self.endpoint)
endpoint=self.endpoint,
version='v1')
self.href = self.endpoint + '/v1/secrets/'
self.post_mock = self.responses.post(self.href, json={})
@ -274,13 +314,11 @@ class WhenTestingGetErrorMessage(TestClient):
self.assertEqual('test_text: oopsie', msg)
class BaseEntityResource(testtools.TestCase):
class BaseEntityResource(TestClient):
def _setUp(self, entity, entity_id='abcd1234-eabc-5678-9abc-abcdef012345'):
# TODO(dmendiza) Why are we calling super().setUp() from _setUp()?
super(BaseEntityResource, self).setUp()
self.responses = self.useFixture(fixture.Fixture())
self.endpoint = 'http://localhost:9311'
self.project_id = '1234567'
self.entity = entity
self.entity_id = entity_id

View File

@ -15,6 +15,8 @@
import logging
from keystoneauth1 import discover
from barbicanclient import client as base_client
from barbicanclient.v1 import acls
from barbicanclient.v1 import cas
@ -22,44 +24,41 @@ from barbicanclient.v1 import containers
from barbicanclient.v1 import orders
from barbicanclient.v1 import secrets
LOG = logging.getLogger(__name__)
_SUPPORTED_MICROVERSIONS = [(1, 0),
(1, 1)]
# For microversion 1.0, API status is "stable"
_STABLE = "STABLE"
class Client(object):
def __init__(self, session=None, *args, **kwargs):
"""Barbican client object used to interact with barbican service.
"""Barbican client implementation for API version v1
:param session: An instance of keystoneauth1.session.Session that
can be either authenticated, or not authenticated. When using
a non-authenticated Session, you must provide some additional
parameters. When no session is provided it will default to a
non-authenticated Session.
:param endpoint: Barbican endpoint url. Required when a session is not
given, or when using a non-authenticated session.
When using an authenticated session, the client will attempt
to get an endpoint from the session.
:param project_id: The project ID used for context in Barbican.
Required when a session is not given, or when using a
non-authenticated session.
When using an authenticated session, the project ID will be
provided by the authentication mechanism.
:param verify: When a session is not given, the client will create
a non-authenticated session. This parameter is passed to the
session that is created. If set to False, it allows
barbicanclient to perform "insecure" TLS (https) requests.
The server's certificate will not be verified against any
certificate authorities.
WARNING: This option should be used with caution.
:param service_type: Used as an endpoint filter when using an
authenticated keystone session. Defaults to 'key-management'.
:param service_name: Used as an endpoint filter when using an
authenticated keystone session.
:param interface: Used as an endpoint filter when using an
authenticated keystone session. Defaults to 'public'.
:param region_name: Used as an endpoint filter when using an
authenticated keystone session.
This class is dynamically loaded by the factory function
`barbicanclient.client.Client`. It's recommended to use that
function instead of making instances of this class directly.
"""
microversion = kwargs.pop('microversion', None)
if microversion:
if not self._validate_microversion(
session,
kwargs.get('endpoint'),
kwargs.get('version'),
kwargs.get('service_type'),
kwargs.get('service_name'),
kwargs.get('interface'),
kwargs.get('region_name'),
microversion
):
raise ValueError(
"Endpoint does not support microversion {}".format(
microversion))
kwargs['default_microversion'] = microversion
# TODO(dmendiza): This should be a private member
self.client = base_client._HTTPClient(session=session, *args, **kwargs)
self.secrets = secrets.SecretManager(self.client)
@ -67,3 +66,46 @@ class Client(object):
self.containers = containers.ContainerManager(self.client)
self.cas = cas.CAManager(self.client)
self.acls = acls.ACLManager(self.client)
def _validate_microversion(self, session, endpoint, version, service_type,
service_name, interface, region_name,
microversion):
# first we make sure that the microversion is something we understand
normalized = discover.normalize_version_number(microversion)
if normalized not in _SUPPORTED_MICROVERSIONS:
raise ValueError("Invalid microversion {}".format(microversion))
microversion = discover.version_to_string(normalized)
if not endpoint:
endpoint = session.get_endpoint(
service_type=service_type,
service_name=service_name,
interface=interface,
region_name=region_name,
version=version
)
resp = discover.get_version_data(
session, endpoint,
version_header='key-manager ' + microversion)
if resp:
resp = resp[0]
status = resp['status'].upper()
if status == _STABLE:
# status is only set to STABLE in two cases
# 1. when the server is older and is ignoring the microversion
# header
# 2. when we ask for microversion 1.0 and the server
# undertsands the header
# in either case min/max will be 1.0
min_ver = '1.0'
max_ver = '1.0'
else:
# any other status will have a min/max
min_ver = resp['version']['min_version']
max_ver = resp['version']['max_version']
return discover.version_between(min_ver, max_ver, microversion)
# TODO(afariasa) What should be returned? error?
return False

View File

@ -70,14 +70,6 @@ class WhenTestingClientConnectivity(BaseTestCase):
self.assertRaises(exceptions.HTTPClientError, client.orders.list)
self.assertRaises(exceptions.HTTPClientError, client.secrets.list)
def assert_client_cannot_get_endpoint(self, client):
self.assertRaises(ks_exceptions.EndpointNotFound,
client.containers.list)
self.assertRaises(ks_exceptions.EndpointNotFound,
client.orders.list)
self.assertRaises(ks_exceptions.EndpointNotFound,
client.secrets.list)
def test_can_access_server_if_endpoint_and_session_specified(self):
barbicanclient = client.Client(
endpoint=CONF.keymanager.url,
@ -112,25 +104,27 @@ class WhenTestingClientConnectivity(BaseTestCase):
self.assert_client_can_contact_barbican(barbicanclient)
def test_client_cannot_access_server_if_endpoint_filter_wrong(self):
barbicanclient = client.Client(
self.assertRaises(
ks_exceptions.EndpointNotFound,
client.Client,
project_id=CONF.keymanager.project_id,
auth=self.auth,
interface=client._DEFAULT_SERVICE_INTERFACE,
service_type='wrong-service-type',
version=client._DEFAULT_API_VERSION)
self.assert_client_cannot_get_endpoint(barbicanclient)
barbicanclient = client.Client(
self.assertRaises(
ks_exceptions.EndpointNotFound,
client.Client,
project_id=CONF.keymanager.project_id,
auth=self.auth,
interface='wrong-interface',
service_type=client._DEFAULT_SERVICE_TYPE,
version=client._DEFAULT_API_VERSION)
self.assert_client_cannot_get_endpoint(barbicanclient)
barbicanclient = client.Client(
self.assertRaises(
ks_exceptions.EndpointNotFound,
client.Client,
project_id=CONF.keymanager.project_id,
auth=self.auth,
interface=client._DEFAULT_SERVICE_INTERFACE,
@ -138,9 +132,9 @@ class WhenTestingClientConnectivity(BaseTestCase):
service_name='wrong-service-name',
version=client._DEFAULT_API_VERSION)
self.assert_client_cannot_get_endpoint(barbicanclient)
barbicanclient = client.Client(
self.assertRaises(
ks_exceptions.EndpointNotFound,
client.Client,
project_id=CONF.keymanager.project_id,
auth=self.auth,
interface=client._DEFAULT_SERVICE_INTERFACE,
@ -148,8 +142,6 @@ class WhenTestingClientConnectivity(BaseTestCase):
region_name='wrong-region-name',
version=client._DEFAULT_API_VERSION)
self.assert_client_cannot_get_endpoint(barbicanclient)
def test_cannot_create_client_if_nonexistent_version_specified(self):
self.assertRaises(exceptions.UnsupportedVersion,
client.Client,

View File

@ -8,7 +8,7 @@
pbr!=2.1.0,>=2.0.0 # Apache-2.0
requests>=2.14.2 # Apache-2.0
cliff!=2.9.0,>=2.8.0 # Apache-2.0
keystoneauth1>=3.4.0 # Apache-2.0
keystoneauth1>=5.1.1 # Apache-2.0
oslo.i18n>=3.15.3 # Apache-2.0
oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0
oslo.utils>=3.33.0 # Apache-2.0