Skip to content

Commit eebf2b3

Browse files
authored
Merge pull request #488 from juju/johnsca/idempotent-bundle-deploy
#488 When a bundle is deployed which contains an application which already exists in the model, the CLI will skip adding the application but libjuju will raise an error. This fixes the latter behavior.
2 parents f973a80 + d6fae9e commit eebf2b3

2 files changed

Lines changed: 39 additions & 2 deletions

File tree

juju/bundle.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,13 @@ async def run(self, context):
329329
:param context: is used for any methods or properties required to
330330
perform a change.
331331
"""
332+
# NB: this should really be handled by the controller when generating the
333+
# bundle change plan, and this short-term workaround may be missing some
334+
# aspects of the logic which the CLI client contains to handle edge cases.
335+
if self.application in context.model.applications:
336+
log.debug('Skipping %s; already in model', self.application)
337+
return self.application
338+
332339
# resolve indirect references
333340
charm = context.resolve(self.charm)
334341
options = {}
@@ -579,6 +586,14 @@ async def run(self, context):
579586
ep1 = context.resolveRelation(self.endpoint1)
580587
ep2 = context.resolveRelation(self.endpoint2)
581588

589+
# NB: this should really be handled by the controller when generating the
590+
# bundle change plan, and this short-term workaround may be missing some
591+
# aspects of the logic which the CLI client contains to handle edge cases.
592+
existing = [rel for rel in context.model.relations if rel.matches(ep1, ep2)]
593+
if existing:
594+
log.info('Skipping %s <-> %s; already related', ep1, ep2)
595+
return existing[0]
596+
582597
log.info('Relating %s <-> %s', ep1, ep2)
583598
return await context.model.add_relation(ep1, ep2)
584599

tests/unit/test_bundle.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ async def test_run(self, event_loop):
166166
model = mock.Mock()
167167
model._deploy = base.AsyncMock(return_value=None)
168168
model._add_store_resources = base.AsyncMock(return_value=["resource1"])
169+
model.applications = {}
169170

170171
context = mock.Mock()
171172
context.resolve.return_value = "charm1"
@@ -192,6 +193,13 @@ async def test_run(self, event_loop):
192193
devices="devices",
193194
num_units="num_units")
194195

196+
# confirm that it's idempotent
197+
model.applications = {"application": None}
198+
result = await change.run(context)
199+
assert result == "application"
200+
model._add_store_resources.assert_called_once()
201+
model._deploy.assert_called_once()
202+
195203
@pytest.mark.asyncio
196204
async def test_run_local(self, event_loop):
197205
change = AddApplicationChange(1, [], params={"charm": "local:charm",
@@ -206,6 +214,7 @@ async def test_run_local(self, event_loop):
206214

207215
model = mock.Mock()
208216
model._deploy = base.AsyncMock(return_value=None)
217+
model.applications = {}
209218

210219
context = mock.Mock()
211220
context.resolve.return_value = "local:charm1"
@@ -407,19 +416,32 @@ async def test_run(self, event_loop):
407416
change = AddRelationChange(1, [], params={"endpoint1": "endpoint1",
408417
"endpoint2": "endpoint2"})
409418

419+
rel1 = mock.Mock(name="rel1", **{"matches.return_value": False})
420+
rel2 = mock.Mock(name="rel2", **{"matches.return_value": True})
421+
410422
model = mock.Mock()
411-
model.add_relation = base.AsyncMock(return_value="relation1")
423+
model.add_relation = base.AsyncMock(return_value=rel2)
412424

413425
context = mock.Mock()
414426
context.resolveRelation = mock.Mock(side_effect=['endpoint_1', 'endpoint_2'])
415427
context.model = model
428+
model.relations = [rel1]
416429

417430
result = await change.run(context)
418-
assert result == "relation1"
431+
assert result is rel2
419432

420433
model.add_relation.assert_called_once()
421434
model.add_relation.assert_called_with("endpoint_1", "endpoint_2")
422435

436+
# confirm that it's idempotent
437+
context.resolveRelation.side_effect = ['endpoint_1', 'endpoint_2']
438+
model.add_relation.reset_mock()
439+
model.add_relation.return_value = None
440+
model.relations = [rel1, rel2]
441+
result = await change.run(context)
442+
assert result is rel2
443+
assert not model.add_relation.called
444+
423445

424446
class TestAddUnitChange(unittest.TestCase):
425447

0 commit comments

Comments
 (0)