Skip to content

Commit 0bafc11

Browse files
authored
Merge pull request #744 from cderici/backport-upgrade-charm-fixes-onto-2.9
#744 #### Description This cherry-picks the `upgrade-charm` fixes from #729 and #742 to bring them onto `2.9` branch. Includes the fix for the `juju.errors.JujuAPIError: missing base name or channel not valid` error when upgrading local charms. #### QA Steps Same QA Steps for #729 and #742 should work here. ``` tox -e integration -- tests/integration/test_application.py::test_upgrade_local_charm ``` ``` tox -e integration -- tests/integration/test_charmhub.py::test_list_resources ``` ``` tox -e integration -- tests/integration/test_application.py::test_upgrade_charm_switch_channel ```
2 parents 7962798 + 37eacb6 commit 0bafc11

5 files changed

Lines changed: 228 additions & 58 deletions

File tree

juju/application.py

Lines changed: 107 additions & 55 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

@@ -609,98 +611,147 @@ async def refresh(
609611
:param str switch: Crossgrade charm url
610612
611613
"""
612-
if path is not None:
613-
await self.local_refresh(channel, force, force_series, force_units,
614-
path, resources)
615-
return
616614
if resources is not None:
617615
raise NotImplementedError("resources option is not implemented")
618616

619617
if switch is not None and revision is not None:
620618
raise ValueError("switch and revision are mutually exclusive")
621619

622-
client_facade = client.ClientFacade.from_connection(self.connection)
623-
resources_facade = client.ResourcesFacade.from_connection(
624-
self.connection)
625620
app_facade = self._facade()
621+
resources_facade = client.ResourcesFacade.from_connection(self.connection)
622+
charms_facade = client.CharmsFacade.from_connection(self.connection)
626623

627-
charmstore = self.model.charmstore
628-
charmstore_entity = None
624+
# 1 - Figure out the destination origin and destination charm_url
625+
# 2 - Then take care of the resources
626+
# 3 - Finally execute the upgrade
629627

630-
if switch is not None:
631-
charm_url = switch
632-
if not charm_url.startswith('cs:'):
633-
charm_url = 'cs:' + charm_url
634-
else:
635-
charm_url = self.data['charm-url']
636-
charm_url = charm_url.rpartition('-')[0]
637-
if revision is not None:
638-
charm_url = "%s-%d" % (charm_url, revision)
639-
else:
640-
charmstore_entity = await charmstore.entity(charm_url,
641-
channel=channel)
642-
charm_url = charmstore_entity['Id']
628+
# Get the charm URL and charm origin of the given application is running at present.
629+
charm_url_origin_result = await app_facade.GetCharmURLOrigin(application=self.name)
630+
if charm_url_origin_result.error is not None:
631+
err = charm_url_origin_result.error
632+
raise JujuError("%s : %s" % (err.code, err.message))
633+
charm_url = switch or charm_url_origin_result.url
634+
origin = charm_url_origin_result.charm_origin
643635

644-
if charm_url == self.data['charm-url']:
645-
raise JujuError('already running charm "%s"' % charm_url)
636+
if path is not None:
637+
await self.local_refresh(origin, force, force_series,
638+
force_units, path, resources)
639+
return
646640

647-
# Update charm
648-
await client_facade.AddCharm(
649-
url=charm_url,
650-
force=force,
651-
channel=channel
652-
)
641+
parsed_url = URL.parse(charm_url)
642+
charm_name = parsed_url.name
643+
644+
if parsed_url.schema is None:
645+
raise JujuError("A ch: or cs: schema is required for application "
646+
"refresh, given : %s " % str(parsed_url))
653647

654-
# Update resources
655-
if not charmstore_entity:
656-
charmstore_entity = await charmstore.entity(charm_url,
657-
channel=channel)
658-
store_resources = charmstore_entity['Meta']['resources']
648+
if revision is not None:
649+
origin.revision = revision
659650

651+
# Make the source-specific changes to the origin/channel/url
652+
# (and also get the resources necessary to deploy the (destination) charm -- for later)
653+
if Schema.CHARM_HUB.matches(parsed_url.schema):
654+
origin.source = 'charm-hub'
655+
if channel:
656+
ch = Channel.parse(channel).normalize()
657+
origin.risk = ch.risk
658+
origin.track = ch.track
659+
660+
charmhub = self.model.charmhub
661+
charm_resources = await charmhub.list_resources(charm_name)
662+
else:
663+
charmstore = self.model.charmstore
664+
charmstore_entity = None
665+
if switch is None:
666+
charm_url = charm_url.rpartition('-')[0]
667+
if revision is not None:
668+
charm_url = "%s-%d" % (charm_url, revision)
669+
else:
670+
charmstore_entity = await charmstore.entity(charm_url, channel=channel)
671+
charm_url = charmstore_entity['Id']
672+
origin.source = 'charm-store'
673+
if channel:
674+
origin.risk = channel
675+
if charmstore_entity is None:
676+
charmstore_entity = await charmstore.entity(charm_url, channel=channel)
677+
charm_resources = charmstore_entity['Meta']['resources']
678+
679+
# Resolve the given charm URLs with an optionally specified preferred channel.
680+
# Channel provided via CharmOrigin.
681+
resolved_charm_with_channel_results = await charms_facade.ResolveCharms(
682+
resolve=[client.ResolveCharmWithChannel(
683+
charm_origin=origin,
684+
switch_charm=True if switch else False, # rpc expects boolean type
685+
reference=charm_url,
686+
)])
687+
resolved_charm = resolved_charm_with_channel_results.results[0]
688+
689+
# Get the destination origin and destination charm_url from the resolved charm
690+
if resolved_charm.error is not None:
691+
err = resolved_charm.error
692+
raise JujuError("%s : %s" % (err.code, err.message))
693+
dest_origin = resolved_charm.charm_origin
694+
charm_url = resolved_charm.url
695+
696+
# Add the charm with the destination url and origin
697+
charm_origin_result = await charms_facade.AddCharm(url=charm_url,
698+
force=force,
699+
charm_origin=dest_origin)
700+
if charm_origin_result.error is not None:
701+
err = charm_origin_result.error
702+
raise JujuError("%s : %s" % (err.code, err.message))
703+
704+
# Now take care of the resources:
705+
706+
# Already prepped the charm_resources
707+
# Now get the existing resources from the ResourcesFacade
660708
request_data = [client.Entity(self.tag)]
661709
response = await resources_facade.ListResources(entities=request_data)
662710
existing_resources = {
663711
resource.name: resource
664712
for resource in response.results[0].resources
665713
}
666714

715+
# Compute the difference btw resources needed and the existing resources
667716
resources_to_update = [
668-
resource for resource in store_resources
669-
if resource['Name'] not in existing_resources or
670-
existing_resources[resource['Name']].origin != 'upload'
717+
resource for resource in charm_resources
718+
if resource.get('Name', resource.get('name')) not in existing_resources or
719+
existing_resources[resource.get('Name', resource.get('name'))].origin != 'upload'
671720
]
672721

722+
# Update the resources
673723
if resources_to_update:
674-
request_data = [
675-
client.CharmResource(
676-
description=resource.get('Description'),
677-
fingerprint=resource['Fingerprint'],
678-
name=resource['Name'],
679-
path=resource['Path'],
680-
revision=resource['Revision'],
681-
size=resource['Size'],
682-
type_=resource['Type'],
724+
request_data = []
725+
for resource in resources_to_update:
726+
request_data.append(client.CharmResource(
727+
description=resource.get('Description', resource.get('description')),
728+
fingerprint=resource.get('Fingerprint', resource.get('fingerprint')),
729+
name=resource.get('Name', resource.get('name')),
730+
path=resource.get('Path', resource.get('filename')),
731+
revision=resource.get('Revision', resource.get('revision', -1)),
732+
size=resource.get('Size', resource.get('size')),
733+
type_=resource.get('Type', resource.get('type')),
683734
origin='store',
684-
) for resource in resources_to_update
685-
]
735+
))
686736
response = await resources_facade.AddPendingResources(
687737
application_tag=self.tag,
688738
charm_url=charm_url,
689-
resources=request_data
739+
resources=request_data,
740+
charm_origin=dest_origin,
690741
)
691742
pending_ids = response.pending_ids
692743
resource_ids = {
693-
resource['Name']: id
744+
resource.get('Name', resource.get('name')): id
694745
for resource, id in zip(resources_to_update, pending_ids)
695746
}
696747
else:
697748
resource_ids = None
698749

699-
# Update application
750+
# Update the application
700751
await app_facade.SetCharm(
701752
application=self.entity_id,
702-
channel=channel,
703753
charm_url=charm_url,
754+
charm_origin=dest_origin,
704755
config_settings=None,
705756
config_settings_yaml=None,
706757
force=force,
@@ -717,7 +768,8 @@ async def refresh(
717768
upgrade_charm = refresh
718769

719770
async def local_refresh(
720-
self, channel=None, force=False, force_series=False, force_units=False,
771+
self, charm_origin=None, force=False, force_series=False,
772+
force_units=False,
721773
path=None, resources=None):
722774
"""Refresh the charm for this application with a local charm.
723775
@@ -760,7 +812,7 @@ async def local_refresh(
760812
# Update application
761813
await app_facade.SetCharm(
762814
application=self.entity_id,
763-
channel=channel,
815+
charm_origin=charm_origin,
764816
charm_url=charm_url,
765817
config_settings=None,
766818
config_settings_yaml=None,

juju/charmhub.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,56 @@
11
from .client import client
22
from .errors import JujuError
3+
from juju import jasyncio
4+
5+
import requests
6+
import json
37

48

59
class CharmHub:
610
def __init__(self, model):
711
self.model = model
812

13+
def request_charmhub_with_retry(self, url, retries):
14+
for attempt in range(retries):
15+
_response = requests.get(url)
16+
if _response.status_code == 200:
17+
return _response
18+
jasyncio.sleep(5)
19+
raise JujuError("Got {} from {}".format(_response.status_code, url))
20+
21+
async def get_charm_id(self, charm_name):
22+
conn, headers, path_prefix = self.model.connection().https_connection()
23+
24+
model_conf = await self.model.get_config()
25+
charmhub_url = model_conf['charmhub-url']
26+
url = "{}/v2/charms/info/{}".format(charmhub_url.value, charm_name)
27+
_response = self.request_charmhub_with_retry(url, 5)
28+
response = json.loads(_response.text)
29+
return response['id'], response['name']
30+
31+
async def is_subordinate(self, charm_name):
32+
conn, headers, path_prefix = self.model.connection().https_connection()
33+
34+
model_conf = await self.model.get_config()
35+
charmhub_url = model_conf['charmhub-url']
36+
url = "{}/v2/charms/info/{}?fields=default-release.revision.subordinate".format(charmhub_url.value, charm_name)
37+
_response = self.request_charmhub_with_retry(url, 5)
38+
response = json.loads(_response.text)
39+
return 'subordinate' in response['default-release']['revision']
40+
41+
# TODO (caner) : we should be able to recreate the channel-map through the
42+
# api call without needing the CharmHub facade
43+
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+
954
async def info(self, name, channel=None):
1055
"""info displays detailed information about a CharmHub charm. The charm
1156
can be specified by the exact name.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
name: simple
22
summary: A simple example charm with the new operator framework
3+
series: ["focal"]
34
description: |
45
Simple is an example charm

0 commit comments

Comments
 (0)