Add barbican-secrets interface code and unit tests

This commit is contained in:
Frode Nordahl 2018-10-12 13:09:15 +02:00
parent ea50b1e502
commit 82acd4465e
No known key found for this signature in database
GPG Key ID: 6A5D59A3BA48373F
9 changed files with 330 additions and 2 deletions

2
.gitignore vendored
View File

@ -3,3 +3,5 @@
*__pycache__*
*.pyc
build
.unit-state.db
*.swp

6
.travis.yml Normal file
View File

@ -0,0 +1,6 @@
language: python
python:
- "3.6"
install: pip install tox-travis
script:
- tox

View File

@ -2,3 +2,10 @@ name: barbican-secrets
summary: Interface for a secrets plugin to the Barbican charm.
maintainer: OpenStack Charmers <openstack-charmers@lists.ubuntu.com>
repo: https://github.com/openstack-charmers/charm-interface-barbican-secrets
ignore:
- 'unit_tests'
- '.stestr.conf'
- 'test-requirements.txt'
- 'tox.ini'
- '.gitignore'
- '.travis.yml'

28
provides.py Normal file
View File

@ -0,0 +1,28 @@
# Copyright 2018 Canonical Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# the reactive framework unfortunately does not grok `import as` in conjunction
# with decorators on class instance methods, so we have to revert to `from ...`
# imports
from charms.reactive import Endpoint
class BarbicanSecretsProvides(Endpoint):
"""This is the barbican-{type}secrets end of the relation."""
def publish_plugin_info(self, name, data, reference=None):
for relation in self.relations:
relation.to_publish['name'] = name
relation.to_publish['data'] = data
relation.to_publish['reference'] = reference

73
requires.py Normal file
View File

@ -0,0 +1,73 @@
# Copyright 2018 Canonical Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# the reactive framework unfortunately does not grok `import as` in conjunction
# with decorators on class instance methods, so we have to revert to `from ...`
# imports
from charms.reactive import Endpoint, clear_flag, set_flag, when_any, when_not
class BarbicanSecretsRequires(Endpoint):
"""This is the Barbican 'end' of the relation."""
@when_any('endpoint.{endpoint_name}.changed.name',
'endpoint.{endpoint_name}.changed.reference',
'endpoint.{endpoint_name}.changed.data')
def new_plugin(self):
set_flag(
self.expand_name('{endpoint_name}.new-plugin'))
clear_flag(
self.expand_name('{endpoint_name}.changed.name'))
clear_flag(
self.expand_name('{endpoint_name}.changed.reference'))
clear_flag(
self.expand_name('{endpoint_name}.changed.data'))
@when_not('endpoint.{endpoint_name}.joined')
def broken(self):
clear_flag(
self.expand_name('{endpoint_name}.new-plugin'))
@property
def plugins(self):
for relation in self.relations:
for unit in relation.units:
name = unit.received['name']
reference = unit.received['reference']
data = unit.received['data']
if not (name and data):
continue
yield {
'name': name,
'reference': reference,
'data': data,
'relation_id': relation.relation_id,
'remote_unit_name': unit.unit_name,
}
@property
def plugins_string(self):
def plugin_name_or_reference():
for relation in self.relations:
for unit in relation.units:
name = unit.received['name']
reference = unit.received['reference']
data = unit.received['data']
if not (name and data):
continue
if reference:
yield reference
else:
yield name + '_plugin'
return ','.join(plugin_name_or_reference())

View File

@ -1,3 +1,7 @@
flake8>=2.2.4,<=2.4.1
# Lint and unit test requirements
flake8
os-testr>=0.4.1
charm-tools
charms.reactive
mock>=1.2
coverage>=3.6
git+https://github.com/openstack/charms.openstack.git#egg=charms.openstack

View File

@ -11,3 +11,12 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import sys
sys.path.append('src')
sys.path.append('src/lib')
# Mock out charmhelpers so that we can test without it.
import charms_openstack.test_mocks # noqa
charms_openstack.test_mocks.mock_charmhelpers()

View File

@ -11,3 +11,108 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import mock
import unittest
import provides
_hook_args = {}
def mock_hook(*args, **kwargs):
def inner(f):
# remember what we were passed. Note that we can't actually determine
# the class we're attached to, as the decorator only gets the function.
_hook_args[f.__name__] = dict(args=args, kwargs=kwargs)
return f
return inner
class TestBarbicanSecretsProvides(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls._patched_hook = mock.patch('charms.reactive.hook', mock_hook)
cls._patched_hook_started = cls._patched_hook.start()
# force providesto rerun the mock_hook decorator:
# try except is Python2/Python3 compatibility as Python3 has moved
# reload to importlib.
try:
reload(provides)
except NameError:
import importlib
importlib.reload(provides)
@classmethod
def tearDownClass(cls):
cls._patched_hook.stop()
cls._patched_hook_started = None
cls._patched_hook = None
# and fix any breakage we did to the module
try:
reload(provides)
except NameError:
import importlib
importlib.reload(provides)
def setUp(self):
self.bsr = provides.BarbicanSecretsProvides('some-relation', [])
self._patches = {}
self._patches_start = {}
def tearDown(self):
self.bsr = None
for k, v in self._patches.items():
v.stop()
setattr(self, k, None)
self._patches = None
self._patches_start = None
def patch_bsr(self, attr, return_value=None):
mocked = mock.patch.object(self.bsr, attr)
self._patches[attr] = mocked
started = mocked.start()
started.return_value = return_value
self._patches_start[attr] = started
setattr(self, attr, started)
def patch_topublish(self):
self.patch_bsr('_relations')
relation = mock.MagicMock()
to_publish = mock.PropertyMock()
type(relation).to_publish = to_publish
self._relations.__iter__.return_value = [relation]
return relation.to_publish
def test_registered_hooks(self):
# test that the hooks actually registered the relation expressions that
# are meaningful for this interface: this is to handle regressions.
# The keys are the function names that the hook attaches to.
hook_patterns = {
'joined': ('{provides:barbican-secrets}-relation-joined', ),
'changed': ('{provides:barbican-secrets}-relation-changed', ),
'departed': (
'{provides:barbican-secrets}-relation-{broken,departed}', ),
}
for k, v in _hook_args.items():
self.assertEqual(hook_patterns[k], v['args'])
def test_publish_plugin_info(self):
to_publish = self.patch_topublish()
name = 'FAKENAME'
reference = 'FAKEREFERENCE'
data = {'a': 'A', 'b': 'B'}
self.bsr.publish_plugin_info(name, data, reference=reference)
to_publish.__setitem__.assert_has_calls([
mock.call('name', name),
mock.call('data', data),
mock.call('reference', reference),
])
self.bsr.publish_plugin_info(name, data)
to_publish.__setitem__.assert_has_calls([
mock.call('name', name),
mock.call('data', data),
mock.call('reference', None),
])

View File

@ -11,3 +11,97 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import mock
import requires
import charms_openstack.test_utils as test_utils
_hook_args = {}
class TestRegisteredHooks(test_utils.TestRegisteredHooks):
def test_hooks(self):
defaults = []
hook_set = {
'when_any': {
'new_plugin': ('endpoint.{endpoint_name}.changed.name',
'endpoint.{endpoint_name}.changed.reference',
'endpoint.{endpoint_name}.changed.data',),
},
'when_not': {
'broken': ('endpoint.{endpoint_name}.joined',),
},
}
# test that the hooks were registered via the
# reactive.barbican_handlers
self.registered_hooks_test_helper(requires, hook_set, defaults)
class TestBarbicanSecretRequires(test_utils.PatchHelper):
def setUp(self):
super().setUp()
self.bsr = requires.BarbicanSecretsRequires('some-relation', [])
self._patches = {}
self._patches_start = {}
def tearDown(self):
self.bsr = None
for k, v in self._patches.items():
v.stop()
setattr(self, k, None)
self._patches = None
self._patches_start = None
def patch_bsr(self, attr, return_value=None):
mocked = mock.patch.object(self.bsr, attr)
self._patches[attr] = mocked
started = mocked.start()
started.return_value = return_value
self._patches_start[attr] = started
setattr(self, attr, started)
def test_new_plugin(self):
self.patch_object(requires, 'set_flag')
self.patch_object(requires, 'clear_flag')
self.bsr.new_plugin()
self.set_flag.assert_called_with('some-relation.new-plugin')
self.clear_flag.assert_has_calls([
mock.call('some-relation.changed.name'),
mock.call('some-relation.changed.reference'),
mock.call('some-relation.changed.data'),
])
def test_broken(self):
self.patch_object(requires, 'clear_flag')
self.bsr.broken()
self.clear_flag.assert_called_with('some-relation.new-plugin')
def test_plugins(self):
self.patch_bsr('_relations')
relation = mock.MagicMock()
unit = mock.PropertyMock()
type(relation).units = [unit]
relation.relation_id = 'RELATION_ID'
self._relations.__iter__.return_value = [relation]
result = next(self.bsr.plugins)
self.assertEqual(result['name'], unit.received['name'])
self.assertEqual(result['reference'], unit.received['reference'])
self.assertEqual(result['data'], unit.received['data'])
self.assertEqual(result['relation_id'], relation.relation_id)
self.assertEqual(result['remote_unit_name'], unit.unit_name)
def test_plugins_string(self):
self.patch_bsr('_relations')
relation = mock.MagicMock()
unit = mock.MagicMock()
unit.received = {'name': 'NAME', 'reference': None, 'data': 'DATA'}
relation.units = [unit]
self._relations.__iter__.return_value = [relation]
self.assertEqual(self.bsr.plugins_string, 'NAME_plugin')
unit.received = {'name': 'NAME', 'reference': 'PLUGINREFERENCE',
'data': 'DATA'}
self.assertEqual(self.bsr.plugins_string, 'PLUGINREFERENCE')