Add barbican-secrets interface code and unit tests
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -3,3 +3,5 @@
|
|||||||
*__pycache__*
|
*__pycache__*
|
||||||
*.pyc
|
*.pyc
|
||||||
build
|
build
|
||||||
|
.unit-state.db
|
||||||
|
*.swp
|
||||||
|
6
.travis.yml
Normal file
6
.travis.yml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
language: python
|
||||||
|
python:
|
||||||
|
- "3.6"
|
||||||
|
install: pip install tox-travis
|
||||||
|
script:
|
||||||
|
- tox
|
@@ -2,3 +2,10 @@ name: barbican-secrets
|
|||||||
summary: Interface for a secrets plugin to the Barbican charm.
|
summary: Interface for a secrets plugin to the Barbican charm.
|
||||||
maintainer: OpenStack Charmers <openstack-charmers@lists.ubuntu.com>
|
maintainer: OpenStack Charmers <openstack-charmers@lists.ubuntu.com>
|
||||||
repo: https://github.com/openstack-charmers/charm-interface-barbican-secrets
|
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
28
provides.py
Normal 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
73
requires.py
Normal 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())
|
@@ -1,3 +1,7 @@
|
|||||||
flake8>=2.2.4,<=2.4.1
|
# Lint and unit test requirements
|
||||||
|
flake8
|
||||||
os-testr>=0.4.1
|
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
|
||||||
|
@@ -11,3 +11,12 @@
|
|||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
# 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 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()
|
||||||
|
@@ -11,3 +11,108 @@
|
|||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
# 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 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),
|
||||||
|
])
|
||||||
|
@@ -11,3 +11,97 @@
|
|||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
# 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 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')
|
||||||
|
Reference in New Issue
Block a user