Skip to content
This repository has been archived by the owner on Apr 2, 2024. It is now read-only.

Commit

Permalink
Supporting app level relation data (#232)
Browse files Browse the repository at this point in the history
Add app level relation data functions to the library.  App level relation data is data that the leader unit of an app can publish that will be visible to all participants of the relation.  This is contrasted with the existing system which is unit level relation data where a unit can publish to another unit on the relation exclusively.  Both forms are available.

Authors: seyeong.kim@canonical.com and Cory Johns.
  • Loading branch information
xtrusia committed Nov 30, 2021
1 parent f4f5ac6 commit 92a8ee4
Show file tree
Hide file tree
Showing 2 changed files with 124 additions and 2 deletions.
68 changes: 68 additions & 0 deletions charms/reactive/endpoints.py
Expand Up @@ -341,6 +341,8 @@ def __init__(self, relation_id):
self._units = None
self._departed_units = None
self._data = None
self._app_data = None
self._remote_app_data = None

@property
def relation_id(self):
Expand Down Expand Up @@ -444,6 +446,33 @@ def to_publish(self):
writeable=True)
return self._data

@property
def to_publish_app(self):
"""
This is the relation data that the local app publishes so it is
visible to all related units. Use this to communicate with related
apps. It is a writeable
:class:`~charms.reactive.endpoints.JSONUnitDataView`.
Only the leader can set the app-level relation data.
All values stored in this collection will be automatically JSON
encoded when they are published. This means that they need to be JSON
serializable! Mappings stored in this collection will be encoded with
sorted keys, to ensure that the encoded representation will only change
if the actual data changes.
Changes to this data are published at the end of a succesfull hook. The
data is reset when a hook fails.
"""
if self._app_data is None:
# using JSONUnitDataView though it's name includes unit.
self._app_data = JSONUnitDataView(
hookenv.relation_get(app=hookenv.application_name(),
rid=self.relation_id),
writeable=True)
return self._app_data

@property
def to_publish_raw(self):
"""
Expand All @@ -459,13 +488,52 @@ def to_publish_raw(self):
"""
return self.to_publish.data

@property
def to_publish_app_raw(self):
"""
This is the raw relation data that the app publishes so it is
visible to all related units. It is a writeable (by the leader only)
:class:`~charms.reactive.endpoints.UnitDataView`. **Only use this
for backwards compatibility with interfaces that do not use JSON
encoding.** Use
:attr:`~charms.reactive.endpoints.Relation.to_publish` instead.
Changes to this data are published at the end of a succesfull hook. The
data is reset when a hook fails.
"""
return self.to_publish_app.data

@property
def received_app(self):
"""
A :class:`~charms.reactive.endpoints.JSONUnitDataView` of the app-level
data received from this remote unit over the relation, with values
being automatically decoded as JSON.
"""
if self._remote_app_data is None:
# using JSONUnitDataView though it's name includes unit.
self._remote_app_data = JSONUnitDataView(hookenv.relation_get(
app=self.application_name,
rid=self.relation.relation_id))
return self._remote_app_data

@property
def received_app_raw(self):
"""
A :class:`~charms.reactive.endpoints.UnitDataView` of the raw app-level
data received from this remote unit over the relation.
"""
return self.received_app.raw_data

def _flush_data(self):
"""
If this relation's local unit data has been modified, publish it on the
relation. This should be automatically called.
"""
if self._data and self._data.modified:
hookenv.relation_set(self.relation_id, dict(self.to_publish.data))
if self._app_data and self._app_data.modified:
hookenv.relation_set(self.relation_id, dict(self.to_publish_app.data), app=True)

def _serialize(self):
return self.relation_id
Expand Down
58 changes: 56 additions & 2 deletions tests/test_endpoints.py
Expand Up @@ -57,6 +57,10 @@ def setUp(self):
mock.MagicMock(return_value='local/0'))
self.local_unit_p.start()

self.app_name_p = mock.patch('charmhelpers.core.hookenv.application_name',
mock.MagicMock(return_value='local'))
self.app_name_p.start()

self.remote_unit = None
self.remote_unit_p = mock.patch('charmhelpers.core.hookenv.remote_unit')
mremote_unit = self.remote_unit_p.start()
Expand All @@ -70,12 +74,16 @@ def setUp(self):
self.relations = {
'test-endpoint': [
{
'local': {'app-key': 'value'},
'local/0': {'key': 'value'},
'unit': {'simple': 'value', 'complex': '[1, 2]'},
'unit/0': {'foo': 'yes'},
'unit/1': {},
},
{
'local': {},
'local/0': {},
'unit': {},
'unit/0': {'bar': '[1, 2]'},
'unit/1': {'foo': 'no'},
},
Expand All @@ -86,6 +94,13 @@ def _rel(rid):
rn, ri = rid.split(':')
return self.relations[rn][int(ri)]

def _rel_get(attribute=None, unit=None, rid=None, app=None):
data = _rel(rid)[unit or app]
if attribute is not None:
return data[attribute]
else:
return data

self.rel_ids_p = mock.patch('charmhelpers.core.hookenv.relation_ids')
rel_ids_m = self.rel_ids_p.start()
rel_ids_m.side_effect = lambda endpoint: [
Expand All @@ -95,10 +110,12 @@ def _rel(rid):
rel_units_m = self.rel_units_p.start()
rel_units_m.side_effect = lambda rid: [
key for key in _rel(rid).keys()
if (not key.startswith('local') and not _rel(rid)[key].get('departed'))]
if (not key.startswith('local') and
'/' in key and # exclude apps
not _rel(rid)[key].get('departed'))]
self.rel_get_p = mock.patch('charmhelpers.core.hookenv.relation_get')
rel_get_m = self.rel_get_p.start()
rel_get_m.side_effect = lambda unit, rid: _rel(rid)[unit]
rel_get_m.side_effect = _rel_get

self.rel_set_p = mock.patch('charmhelpers.core.hookenv.relation_set')
self.relation_set = self.rel_set_p.start()
Expand Down Expand Up @@ -424,6 +441,43 @@ def test_to_publish(self):
assert 'foo' not in rel.to_publish
assert rel.to_publish['foo'] is None

def test_to_publish_app(self):
Endpoint._startup()
tep = Endpoint.from_name('test-endpoint')
rel = tep.relations[0]

self.assertEqual(rel.to_publish_app_raw, {'app-key': 'value'})
rel._flush_data()
assert not self.relation_set.called

rel.to_publish_app_raw['app-key'] = 'new-value'
rel._flush_data()
self.relation_set.assert_called_once_with('test-endpoint:0', {'app-key': 'new-value'}, app=True)

self.relation_set.reset_mock()
rel.to_publish_app['app-key'] = {'new': 'complex'}
rel._flush_data()
self.relation_set.assert_called_once_with('test-endpoint:0', {'app-key': '{"new": "complex"}'}, app=True)

rel.to_publish_app.update({'app-key': 'new-new'})
self.assertEqual(rel.to_publish_app, {'app-key': 'new-new'})

rel.to_publish_app.update({'app-key': {'new': 'new'}})
self.assertEqual(rel.to_publish_app, {'app-key': {"new": "new"}})

assert 'foo' not in rel.to_publish
assert rel.to_publish.get('foo', 'one') == 'one'
assert 'foo' not in rel.to_publish
assert rel.to_publish.setdefault('foo', 'two') == 'two'
assert 'foo' in rel.to_publish
assert rel.to_publish['foo'] == 'two'
del rel.to_publish['foo']
assert 'foo' not in rel.to_publish
with self.assertRaises(KeyError):
del rel.to_publish['foo']
assert 'foo' not in rel.to_publish
assert rel.to_publish['foo'] is None

def test_handlers(self):
Handler._HANDLERS = {k: h for k, h in Handler._HANDLERS.items()
if hasattr(h, '_action') and
Expand Down

0 comments on commit 92a8ee4

Please sign in to comment.