Skip to content

Commit cd49a4f

Browse files
authored
Merge pull request #971 from jack-w-shaw/JUJU-4829_fix_refresh_bug-master
#971 Port forward #968 Check if the switch param is pointing to a local charm. If it is, run local_refresh as if it were provided This is how the Juju CLI works, which was missing from pylibjuju. Replicate this. Also push url parsing of switch kwarg behind a check if it's local charm, meaning we avoid the possibility where we attempt to parse a path as if it were a url Resolves #963 ### QA Steps ``` tox -e integration -- tests/integration/test_application.py::test_refresh_charmhub_to_local ``` All CI tests need to pass. Verify the following example script runs successfully: ``` from juju import jasyncio from juju.model import Model async def main(): model = Model() print('Connecting to model') await model.connect() try: print('path="/home/jack/charms/ubuntu"') await depl(model, path="/home/jack/charms/ubuntu") print('switch="/home/jack/charms/ubuntu"') await depl(model, switch="/home/jack/charms/ubuntu") print('switch="local:/home/jack/charms/ubuntu"') await depl(model, switch="local:/home/jack/charms/ubuntu") finally: print('Disconnecting from model') await model.disconnect() async def depl(model, **kwargs): try: app = await model.deploy("ubuntu") await model.block_until(lambda: all(u[0].workload_status == 'active' for u in app.units)) await app.refresh(**kwargs) finally: await app.remove() await model.block_until(lambda: not len(model.applications)) if __name__ == '__main__': jasyncio.run(main()) ```
2 parents a677a64 + 76698a5 commit cd49a4f

3 files changed

Lines changed: 40 additions & 13 deletions

File tree

juju/application.py

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44
import hashlib
55
import json
66
import logging
7-
import pathlib
7+
from pathlib import Path
88

99
from . import model, tag, utils, jasyncio
1010
from .url import URL
1111
from .status import derive_status
1212
from .annotationhelper import _get_annotations, _set_annotations
1313
from .client import client
1414
from .errors import JujuError, JujuApplicationConfigError
15-
from .bundle import get_charm_series
15+
from .bundle import get_charm_series, is_local_charm
1616
from .placement import parse as parse_placement
1717
from .origin import Channel, Source
1818

@@ -661,6 +661,8 @@ async def refresh(
661661
:param str switch: Crossgrade charm url
662662
663663
"""
664+
if switch is not None and path is not None:
665+
raise ValueError("switch and path are mutually exclusive")
664666

665667
if switch is not None and revision is not None:
666668
raise ValueError("switch and revision are mutually exclusive")
@@ -677,17 +679,18 @@ async def refresh(
677679
if charm_url_origin_result.error is not None:
678680
err = charm_url_origin_result.error
679681
raise JujuError(f'{err.code} : {err.message}')
680-
charm_url = switch or charm_url_origin_result.url
681682
origin = charm_url_origin_result.charm_origin
682683

683-
if path is not None:
684+
if path is not None or (switch is not None and is_local_charm(switch)):
684685
await self.local_refresh(origin, force, force_series,
685-
force_units, path, resources)
686+
force_units, path or switch, resources)
686687
return
687688

688689
if resources is not None:
689690
raise NotImplementedError("resources option is not implemented")
690691

692+
# If switch is not None at this point, that means it's a switch to a store charm
693+
charm_url = switch or charm_url_origin_result.url
691694
parsed_url = URL.parse(charm_url)
692695
charm_name = parsed_url.name
693696

@@ -733,8 +736,6 @@ async def refresh(
733736
err = charm_origin_result.error
734737
raise JujuError(f'{err.code} : {err.message}')
735738

736-
# Now take care of the resources:
737-
738739
# Already prepped the charm_resources
739740
# Now get the existing resources from the ResourcesFacade
740741
request_data = [client.Entity(self.tag)]
@@ -808,22 +809,22 @@ async def local_refresh(
808809
path=None, resources=None):
809810
"""Refresh the charm for this application with a local charm.
810811
811-
:param str channel: Channel to use when getting the charm from the
812-
charm store, e.g. 'development'
812+
:param dict charm_origin: The charm origin of the destination charm
813+
we're refreshing to
814+
:param bool force: Refresh even if validation checks fail
813815
:param bool force_series: Refresh even if series of deployed
814816
application is not supported by the new charm
815817
:param bool force_units: Refresh all units immediately, even if in
816818
error state
817819
:param str path: Refresh to a charm located at path
818820
:param dict resources: Dictionary of resource name/filepath pairs
819-
:param int revision: Explicit refresh revision
820-
:param str switch: Crossgrade charm url
821821
822822
"""
823823
app_facade = self._facade()
824824

825-
if not isinstance(path, pathlib.Path):
826-
path = pathlib.Path(path)
825+
if isinstance(path, str) and path.startswith("local:"):
826+
path = path[6:]
827+
path = Path(path)
827828
charm_dir = path.expanduser().resolve()
828829
model_config = await self.get_config()
829830

tests/integration/test_application.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,21 @@ async def test_upgrade_charm_resource_same_rev_no_update(event_loop):
219219

220220

221221
@base.bootstrapped
222+
@pytest.mark.asyncio
223+
async def test_refresh_charmhub_to_local(event_loop):
224+
charm_path = INTEGRATION_TEST_DIR / 'charm'
225+
async with base.CleanModel() as model:
226+
app = await model.deploy('ubuntu', application_name='ubu-path')
227+
await app.refresh(path=str(charm_path))
228+
assert app.data['charm-url'].startswith('local:')
229+
230+
app = await model.deploy('ubuntu', application_name='ubu-switch')
231+
await app.refresh(switch=str(charm_path))
232+
assert app.data['charm-url'].startswith('local:')
233+
234+
235+
@base.bootstrapped
236+
@pytest.mark.asyncio
222237
async def test_trusted(event_loop):
223238
async with base.CleanModel() as model:
224239
await model.deploy('ubuntu', trust=True)

tests/unit/test_application.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,3 +166,14 @@ async def test_unexpose_endpoints_on_29_controller(self, mock_conn):
166166
application="panther",
167167
exposed_endpoints=["alpha", "beta"]
168168
)
169+
170+
171+
class TestRefreshApplication(unittest.IsolatedAsyncioTestCase):
172+
@mock.patch("juju.model.Model.connection")
173+
async def test_refresh_mutually_exclusive_kwargs(self, mock_conn):
174+
app = Application(entity_id="app-id", model=Model())
175+
with self.assertRaises(ValueError):
176+
await app.refresh(switch="charm1", revision=10)
177+
178+
with self.assertRaises(ValueError):
179+
await app.refresh(switch="charm1", path="/path/to/charm2")

0 commit comments

Comments
 (0)