Skip to content

Commit 46a44f5

Browse files
authored
Merge pull request #729 from cderici/revise-application-upgrade-charm
#729 #### Description The `application.refresh()` method was crashing due to the resources checks because of the old CharmStore code. This adds a new method, namely `list_resources` into the charmhub module and replaces the old code with the new CharmHub checks using the new method. Flixes: #728 #### QA Steps Two tests were added, one for the `list_resources` method, and one for the `application.upgrade_charm`. Both of them need to pass, along with the other tests (modulo, we might still have some of those known intermittent CI failures). ``` tox -e integration -- tests/integration/test_charmhub.py::test_list_resources ``` ``` tox -e integration -- tests/integration/test_application.py::test_upgrade_charm_switch_channel ``` The `test_upgrade_charm_switch_channel` also includes the reproduce/test section from #728, so no need for a manual check. All CI tests need to pass.
2 parents bd01785 + cc779d6 commit 46a44f5

4 files changed

Lines changed: 184 additions & 59 deletions

File tree

juju/application.py

Lines changed: 102 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@
1818
import pathlib
1919

2020
from . import model, tag, utils, jasyncio
21+
from .url import URL, Schema
2122
from .status import derive_status
2223
from .annotationhelper import _get_annotations, _set_annotations
2324
from .client import client
2425
from .errors import JujuError, JujuApplicationConfigError
2526
from .bundle import get_charm_series
2627
from .placement import parse as parse_placement
28+
from .origin import Channel
2729

2830
log = logging.getLogger(__name__)
2931

@@ -641,92 +643,135 @@ async def refresh(
641643
if switch is not None and revision is not None:
642644
raise ValueError("switch and revision are mutually exclusive")
643645

644-
charms_facade = client.CharmsFacade.from_connection(self.connection)
645-
resources_facade = client.ResourcesFacade.from_connection(
646-
self.connection)
647646
app_facade = self._facade()
647+
resources_facade = client.ResourcesFacade.from_connection(self.connection)
648+
charms_facade = client.CharmsFacade.from_connection(self.connection)
648649

649-
charmstore = self.model.charmstore
650-
charmstore_entity = None
651-
652-
if switch is not None:
653-
charm_url = switch
654-
if not charm_url.startswith('cs:'):
655-
charm_url = 'cs:' + charm_url
650+
# 1 - Figure out the destination origin and destination charm_url
651+
# 2 - Then take care of the resources
652+
# 3 - Finally execute the upgrade
653+
654+
# Get the charm URL and charm origin of the given application is running at present.
655+
charm_url_origin_result = await app_facade.GetCharmURLOrigin(application=self.name)
656+
if charm_url_origin_result.error is not None:
657+
err = charm_url_origin_result.error
658+
raise JujuError(f'{err.code} : {err.message}')
659+
charm_url = switch or charm_url_origin_result.url
660+
origin = charm_url_origin_result.charm_origin
661+
662+
parsed_url = URL.parse(charm_url)
663+
charm_name = parsed_url.name
664+
665+
if parsed_url.schema is None:
666+
raise JujuError(f'A ch: or cs: schema is required for application refresh, given : {str(parsed_url)}')
667+
668+
if revision is not None:
669+
origin.revision = revision
670+
671+
# Make the source-specific changes to the origin/channel/url
672+
# (and also get the resources necessary to deploy the (destination) charm -- for later)
673+
if Schema.CHARM_HUB.matches(parsed_url.schema):
674+
origin.source = 'charm-hub'
675+
if channel:
676+
ch = Channel.parse(channel).normalize()
677+
origin.risk = ch.risk
678+
origin.track = ch.track
679+
680+
charmhub = self.model.charmhub
681+
charm_resources = await charmhub.list_resources(charm_name)
656682
else:
657-
charm_url = self.data['charm-url']
658-
charm_url = charm_url.rpartition('-')[0]
659-
if revision is not None:
660-
charm_url = "%s-%d" % (charm_url, revision)
661-
else:
662-
charmstore_entity = await charmstore.entity(charm_url,
663-
channel=channel)
664-
charm_url = charmstore_entity['Id']
665-
666-
if charm_url == self.data['charm-url']:
667-
raise JujuError('already running charm "%s"' % charm_url)
668-
669-
# TODO (caner) : this needs to be revisited and updated with the charmhub stuff
670-
origin = client.CharmOrigin(source="charm-store",
671-
risk=channel,
672-
)
673-
# Update charm
674-
await charms_facade.AddCharm(
675-
url=charm_url,
676-
force=force,
677-
charm_origin=origin,
678-
)
679-
680-
# Update resources
681-
if not charmstore_entity:
682-
charmstore_entity = await charmstore.entity(charm_url,
683-
channel=channel)
684-
store_resources = charmstore_entity['Meta']['resources']
685-
683+
charmstore = self.model.charmstore
684+
charmstore_entity = None
685+
if switch is None:
686+
charm_url = charm_url.rpartition('-')[0]
687+
if revision is not None:
688+
charm_url = "%s-%d" % (charm_url, revision)
689+
else:
690+
charmstore_entity = await charmstore.entity(charm_url, channel=channel)
691+
charm_url = charmstore_entity['Id']
692+
origin.source = 'charm-store'
693+
if channel:
694+
origin.risk = channel
695+
if charmstore_entity is None:
696+
charmstore_entity = await charmstore.entity(charm_url, channel=channel)
697+
charm_resources = charmstore_entity['Meta']['resources']
698+
699+
# Resolve the given charm URLs with an optionally specified preferred channel.
700+
# Channel provided via CharmOrigin.
701+
resolved_charm_with_channel_results = await charms_facade.ResolveCharms(
702+
resolve=[client.ResolveCharmWithChannel(
703+
charm_origin=origin,
704+
switch_charm=True if switch else False, # rpc expects boolean type
705+
reference=charm_url,
706+
)])
707+
resolved_charm = resolved_charm_with_channel_results.results[0]
708+
709+
# Get the destination origin and destination charm_url from the resolved charm
710+
if resolved_charm.error is not None:
711+
err = resolved_charm.error
712+
raise JujuError(f'{err.code} : {err.message}')
713+
dest_origin = resolved_charm.charm_origin
714+
charm_url = resolved_charm.url
715+
716+
# Add the charm with the destination url and origin
717+
charm_origin_result = await charms_facade.AddCharm(url=charm_url,
718+
force=force,
719+
charm_origin=dest_origin)
720+
if charm_origin_result.error is not None:
721+
err = charm_origin_result.error
722+
raise JujuError(f'{err.code} : {err.message}')
723+
724+
# Now take care of the resources:
725+
726+
# Already prepped the charm_resources
727+
# Now get the existing resources from the ResourcesFacade
686728
request_data = [client.Entity(self.tag)]
687729
response = await resources_facade.ListResources(entities=request_data)
688730
existing_resources = {
689731
resource.name: resource
690732
for resource in response.results[0].resources
691733
}
692734

735+
# Compute the difference btw resources needed and the existing resources
693736
resources_to_update = [
694-
resource for resource in store_resources
695-
if resource['Name'] not in existing_resources or
696-
existing_resources[resource['Name']].origin != 'upload'
737+
resource for resource in charm_resources
738+
if resource.get('Name', resource.get('name')) not in existing_resources or
739+
existing_resources[resource.get('Name', resource.get('name'))].origin != 'upload'
697740
]
698741

742+
# Update the resources
699743
if resources_to_update:
700-
request_data = [
701-
client.CharmResource(
702-
description=resource.get('Description'),
703-
fingerprint=resource['Fingerprint'],
704-
name=resource['Name'],
705-
path=resource['Path'],
706-
revision=resource['Revision'],
707-
size=resource['Size'],
708-
type_=resource['Type'],
744+
request_data = []
745+
for resource in resources_to_update:
746+
request_data.append(client.CharmResource(
747+
description=resource.get('Description', resource.get('description')),
748+
fingerprint=resource.get('Fingerprint', resource.get('fingerprint')),
749+
name=resource.get('Name', resource.get('name')),
750+
path=resource.get('Path', resource.get('filename')),
751+
revision=resource.get('Revision', resource.get('revision', -1)),
752+
size=resource.get('Size', resource.get('size')),
753+
type_=resource.get('Type', resource.get('type')),
709754
origin='store',
710-
) for resource in resources_to_update
711-
]
755+
))
712756
response = await resources_facade.AddPendingResources(
713757
application_tag=self.tag,
714758
charm_url=charm_url,
715-
resources=request_data
759+
resources=request_data,
760+
charm_origin=dest_origin,
716761
)
717762
pending_ids = response.pending_ids
718763
resource_ids = {
719-
resource['Name']: id
764+
resource.get('Name', resource.get('name')): id
720765
for resource, id in zip(resources_to_update, pending_ids)
721766
}
722767
else:
723768
resource_ids = None
724769

725-
# Update application
770+
# Update the application
726771
await app_facade.SetCharm(
727772
application=self.entity_id,
728-
channel=channel,
729773
charm_url=charm_url,
774+
charm_origin=dest_origin,
730775
config_settings=None,
731776
config_settings_yaml=None,
732777
force=force,

juju/charmhub.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,16 @@ async def is_subordinate(self, charm_name):
4141
# TODO (caner) : we should be able to recreate the channel-map through the
4242
# api call without needing the CharmHub facade
4343

44+
async def list_resources(self, charm_name):
45+
conn, headers, path_prefix = self.model.connection().https_connection()
46+
47+
model_conf = await self.model.get_config()
48+
charmhub_url = model_conf['charmhub-url']
49+
url = "{}/v2/charms/info/{}?fields=default-release.resources".format(charmhub_url.value, charm_name)
50+
_response = self.request_charmhub_with_retry(url, 5)
51+
response = json.loads(_response.text)
52+
return response['default-release']['resources']
53+
4454
async def info(self, name, channel=None):
4555
"""info displays detailed information about a CharmHub charm. The charm
4656
can be specified by the exact name.

tests/integration/test_application.py

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
from pathlib import Path
22

33
import pytest
4+
import logging
45

56
from .. import base
6-
from juju import jasyncio
7+
from juju import jasyncio, errors
8+
from juju.url import URL, Schema
79

810
MB = 1
911

12+
logger = logging.getLogger(__name__)
13+
1014

1115
@base.bootstrapped
1216
@pytest.mark.asyncio
@@ -167,6 +171,50 @@ async def test_upgrade_charm_channel(event_loop):
167171
assert app.data['charm-url'] != 'cs:ubuntu-0'
168172

169173

174+
@base.bootstrapped
175+
@pytest.mark.asyncio
176+
async def test_upgrade_charm_switch_channel(event_loop):
177+
# Note for future:
178+
# This test requires a charm that has different
179+
# revisions for different channels/risks.
180+
# Currently, we use juju-qa-test, but eventually
181+
# (when the 'edge' moves to 'stable') this test
182+
# will be testing nothing (if not failing).
183+
# So checks are in place for that.
184+
185+
async with base.CleanModel() as model:
186+
app = await model.deploy('juju-qa-test', channel='2.0/stable')
187+
await model.wait_for_idle(status='active')
188+
189+
charm_url = URL.parse(app.data['charm-url'])
190+
assert Schema.CHARM_HUB.matches(charm_url.schema)
191+
still22 = False
192+
try:
193+
assert charm_url.revision == 22
194+
still22 = True
195+
except AssertionError:
196+
logger.warning("Charm used in test_upgrade_charm_switch_channel "
197+
"seems to have been updated, the test needs to be revised")
198+
199+
await app.upgrade_charm(channel='2.0/edge')
200+
await model.wait_for_idle(status='active')
201+
202+
if still22:
203+
try:
204+
charm_url = URL.parse(app.data['charm-url'])
205+
assert charm_url.revision == 23
206+
except AssertionError:
207+
raise errors.JujuError("Either the upgrade has failed, or the used charm moved "
208+
"the candidate channel to stable, so no upgrade took place, "
209+
"the test needs to be revised.")
210+
211+
# Try with another charm too, just in case, no need to check revisions etc
212+
app = await model.deploy('ubuntu', channel='stable')
213+
await model.wait_for_idle(status='active')
214+
await app.upgrade_charm(channel='candidate')
215+
await model.wait_for_idle(status='active')
216+
217+
170218
@base.bootstrapped
171219
@pytest.mark.asyncio
172220
async def test_upgrade_charm_revision(event_loop):
@@ -187,7 +235,9 @@ async def test_upgrade_charm_switch(event_loop):
187235
await model.block_until(lambda: (len(app.units) > 0 and
188236
app.units[0].machine))
189237
assert app.data['charm-url'] == 'cs:ubuntu-0'
190-
await app.upgrade_charm(switch='ubuntu-8')
238+
with pytest.raises(errors.JujuError):
239+
await app.upgrade_charm(switch='ubuntu-8')
240+
await app.upgrade_charm(switch='cs:ubuntu-8')
191241
assert app.data['charm-url'] == 'cs:ubuntu-8'
192242

193243

@@ -205,6 +255,18 @@ async def test_upgrade_local_charm(event_loop):
205255
assert app.data['charm-url'] == 'local:focal/ubuntu-0'
206256

207257

258+
@base.bootstrapped
259+
@pytest.mark.asyncio
260+
async def test_upgrade_switch_charmstore_to_charmhub(event_loop):
261+
async with base.CleanModel() as model:
262+
app = await model.deploy('cs:ubuntu', series='focal')
263+
await model.wait_for_idle(status="active")
264+
assert app.data['charm-url'].startswith('cs:ubuntu')
265+
await app.upgrade_charm(channel='latest/stable', switch='ch:ubuntu-8')
266+
await model.wait_for_idle(status="active")
267+
assert app.data['charm-url'].startswith('ch:')
268+
269+
208270
@base.bootstrapped
209271
@pytest.mark.asyncio
210272
async def test_upgrade_charm_resource(event_loop):

tests/integration/test_charmhub.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,11 @@ async def test_subordinate_charm_zero_units(event_loop):
101101
app2 = await model.deploy('rsyslog-forwarder-ha', num_units=1)
102102
await jasyncio.sleep(5)
103103
assert len(app2.units) == 0
104+
105+
106+
@base.bootstrapped
107+
@pytest.mark.asyncio
108+
async def test_list_resources(event_loop):
109+
async with base.CleanModel() as model:
110+
resources = await model.charmhub.list_resources('postgresql')
111+
assert type(resources) == list and len(resources) > 0

0 commit comments

Comments
 (0)