44import hashlib
55import json
66import logging
7- import pathlib
7+ import typing
8+ from pathlib import Path
89
9- from . import model , tag , utils , jasyncio
10- from .url import URL
11- from .status import derive_status
10+ from . import jasyncio , model , tag , utils
1211from .annotationhelper import _get_annotations , _set_annotations
12+ from .bundle import get_charm_series , is_local_charm
1313from .client import client
14- from .errors import JujuError , JujuApplicationConfigError
15- from .bundle import get_charm_series
16- from .placement import parse as parse_placement
14+ from .errors import JujuApplicationConfigError , JujuError
1715from .origin import Channel , Source
16+ from .placement import parse as parse_placement
17+ from .relation import Relation
18+ from .status import derive_status
19+ from .url import URL
20+ from .utils import block_until
1821
1922log = logging .getLogger (__name__ )
2023
@@ -59,14 +62,14 @@ def subordinate_units(self):
5962 return [u for u in self .units if u .is_subordinate ]
6063
6164 @property
62- def relations (self ):
65+ def relations (self ) -> typing . List [ Relation ] :
6366 return [rel for rel in self .model .relations if rel .matches (self .name )]
6467
6568 def related_applications (self , endpoint_name = None ):
6669 apps = {}
6770 for rel in self .relations :
6871 if rel .is_peer :
69- local_ep , remote_ep = rel .endpoints [ 0 ]
72+ local_ep , remote_ep = rel .endpoints
7073 else :
7174 def is_us (ep ):
7275 return ep .application .name == self .name
@@ -191,12 +194,13 @@ async def scale(self, scale=None, scale_change=None):
191194 scale_change = scale_change )
192195 ])
193196
194- async def destroy_relation (self , local_relation , remote_relation ):
197+ async def destroy_relation (self , local_relation , remote_relation , block_until_done : bool = False ):
195198 """Remove a relation to another application.
196199
197200 :param str local_relation: Name of relation on this application
198201 :param str remote_relation: Name of relation on the other
199202 application in the form '<application>[:<relation_name>]'
203+ :param bool block_until_done: Wait until the relation is completely removed.
200204
201205 """
202206 if ':' not in local_relation :
@@ -207,8 +211,16 @@ async def destroy_relation(self, local_relation, remote_relation):
207211 log .debug (
208212 'Destroying relation %s <-> %s' , local_relation , remote_relation )
209213
210- return await app_facade .DestroyRelation (endpoints = [
214+ await app_facade .DestroyRelation (endpoints = [
211215 local_relation , remote_relation ])
216+ if block_until_done :
217+ await block_until (
218+ lambda : not any (
219+ relation .matches (local_relation , remote_relation )
220+ for relation in self .relations
221+ )
222+ )
223+
212224 remove_relation = destroy_relation
213225
214226 async def destroy_unit (self , * unit_names ):
@@ -661,6 +673,8 @@ async def refresh(
661673 :param str switch: Crossgrade charm url
662674
663675 """
676+ if switch is not None and path is not None :
677+ raise ValueError ("switch and path are mutually exclusive" )
664678
665679 if switch is not None and revision is not None :
666680 raise ValueError ("switch and revision are mutually exclusive" )
@@ -677,17 +691,15 @@ async def refresh(
677691 if charm_url_origin_result .error is not None :
678692 err = charm_url_origin_result .error
679693 raise JujuError (f'{ err .code } : { err .message } ' )
680- charm_url = switch or charm_url_origin_result .url
681694 origin = charm_url_origin_result .charm_origin
682695
683- if path is not None :
696+ if path is not None or ( switch is not None and is_local_charm ( switch )) :
684697 await self .local_refresh (origin , force , force_series ,
685- force_units , path , resources )
698+ force_units , path or switch , resources )
686699 return
687700
688- if resources is not None :
689- raise NotImplementedError ("resources option is not implemented" )
690-
701+ # If switch is not None at this point, that means it's a switch to a store charm
702+ charm_url = switch or charm_url_origin_result .url
691703 parsed_url = URL .parse (charm_url )
692704 charm_name = parsed_url .name
693705
@@ -735,6 +747,20 @@ async def refresh(
735747
736748 # Now take care of the resources:
737749
750+ # user supplied resources to be used in refresh,
751+ # will override the default values if there's any
752+ arg_resources = resources or {}
753+
754+ # need to process the given resources, as they can be
755+ # paths or revisions
756+ _arg_res_filenames = {}
757+ _arg_res_revisions = {}
758+ for res , filename_or_rev in arg_resources .items ():
759+ if isinstance (filename_or_rev , int ):
760+ _arg_res_revisions [res ] = filename_or_rev
761+ else :
762+ _arg_res_filenames [res ] = filename_or_rev
763+
738764 # Already prepped the charm_resources
739765 # Now get the existing resources from the ResourcesFacade
740766 request_data = [client .Entity (self .tag )]
@@ -748,23 +774,25 @@ async def refresh(
748774 # Compute the difference btw resources needed and the existing resources
749775 resources_to_update = []
750776 for resource in charm_resources :
751- if utils .should_upgrade_resource (resource , existing_resources ):
777+ if utils .should_upgrade_resource (resource , existing_resources , arg_resources ):
752778 resources_to_update .append (resource )
753779
754780 # Update the resources
755781 if resources_to_update :
756782 request_data = []
757783 for resource in resources_to_update :
784+ res_name = resource .get ('Name' , resource .get ('name' ))
758785 request_data .append (client .CharmResource (
759786 description = resource .get ('Description' , resource .get ('description' )),
760- fingerprint = resource . get ( 'Fingerprint' , resource . get ( 'fingerprint' )) ,
761- name = resource .get ('Name' , resource . get ( 'name' )) ,
762- path = resource .get ('Path' , resource . get ( 'filename' )) ,
763- revision = resource .get ('Revision ' , resource . get ( 'revision' , - 1 )),
764- size = resource .get ('Size' , resource . get ( 'size' ) ),
787+ name = res_name ,
788+ path = _arg_res_filenames .get (res_name ,
789+ resource .get ('Path' ,
790+ resource .get ('filename ' , '' ) )),
791+ revision = _arg_res_revisions .get (res_name , - 1 ),
765792 type_ = resource .get ('Type' , resource .get ('type' )),
766793 origin = 'store' ,
767794 ))
795+
768796 response = await resources_facade .AddPendingResources (
769797 application_tag = self .tag ,
770798 charm_url = charm_url ,
@@ -808,22 +836,22 @@ async def local_refresh(
808836 path = None , resources = None ):
809837 """Refresh the charm for this application with a local charm.
810838
811- :param str channel: Channel to use when getting the charm from the
812- charm store, e.g. 'development'
839+ :param dict charm_origin: The charm origin of the destination charm
840+ we're refreshing to
841+ :param bool force: Refresh even if validation checks fail
813842 :param bool force_series: Refresh even if series of deployed
814843 application is not supported by the new charm
815844 :param bool force_units: Refresh all units immediately, even if in
816845 error state
817846 :param str path: Refresh to a charm located at path
818847 :param dict resources: Dictionary of resource name/filepath pairs
819- :param int revision: Explicit refresh revision
820- :param str switch: Crossgrade charm url
821848
822849 """
823850 app_facade = self ._facade ()
824851
825- if not isinstance (path , pathlib .Path ):
826- path = pathlib .Path (path )
852+ if isinstance (path , str ) and path .startswith ("local:" ):
853+ path = path [6 :]
854+ path = Path (path )
827855 charm_dir = path .expanduser ().resolve ()
828856 model_config = await self .get_config ()
829857
@@ -847,6 +875,17 @@ async def local_refresh(
847875 metadata ,
848876 resources = resources )
849877
878+ # We know this charm is a local charm, but this charm origin could be
879+ # the charm origin of a charmhub charm. Ensure that we update/remove
880+ # the appropriate fields.
881+ charm_origin .source = "local"
882+ charm_origin .track = None
883+ charm_origin .risk = None
884+ charm_origin .branch = None
885+ charm_origin .hash_ = None
886+ charm_origin .id_ = None
887+ charm_origin .revision = URL .parse (charm_url ).revision
888+
850889 set_charm_args = {
851890 'application' : self .entity_id ,
852891 'charm_origin' : charm_origin ,
0 commit comments