1414from .client import client
1515from .constraints import parse as parse_constraints , parse_storage_constraint , parse_device_constraint
1616from .errors import JujuError
17- from .origin import Channel , Risk
17+ from .origin import Channel
1818from .url import Schema , URL
1919
2020log = logging .getLogger (__name__ )
@@ -34,10 +34,12 @@ def __init__(self, model, trusted=False, forced=False):
3434 self .plan = []
3535 self .references = {}
3636 self ._units_by_app = {}
37+ self .origins = {}
3738
3839 for unit_name , unit in model .units .items ():
3940 app_units = self ._units_by_app .setdefault (unit .application , [])
4041 app_units .append (unit_name )
42+
4143 self .bundle_facade = client .BundleFacade .from_connection (
4244 model .connection ())
4345 self .client_facade = client .ClientFacade .from_connection (
@@ -47,6 +49,14 @@ def __init__(self, model, trusted=False, forced=False):
4749 self .ann_facade = client .AnnotationsFacade .from_connection (
4850 model .connection ())
4951
52+ # Feature detect if we have the new charms facade, otherwise fallback
53+ # to the client facade, when making calls.
54+ if client .CharmsFacade .best_facade_version (model .connection ()) > 2 :
55+ self .charms_facade = client .CharmsFacade .from_connection (
56+ model .connection ())
57+ else :
58+ self .charms_facade = None
59+
5060 # This describes all the change types that the BundleHandler supports.
5161 change_type_cls = [AddApplicationChange ,
5262 AddCharmChange ,
@@ -168,10 +178,8 @@ async def fetch_plan(self, charm_url, origin):
168178 raise JujuError (self .plan .errors )
169179
170180 async def _download_bundle (self , charm_url , origin ):
171- charms_cls = client .CharmsFacade
172- if charms_cls .best_facade_version (self .model .connection ()) > 2 :
173- charms_facade = charms_cls .from_connection (self .model .connection ())
174- resp = await charms_facade .GetDownloadInfos (entities = [{
181+ if self .charms_facade is not None :
182+ resp = await self .charms_facade .GetDownloadInfos (entities = [{
175183 'charm-url' : str (charm_url ),
176184 'charm-origin' : {
177185 'source' : origin .source ,
@@ -206,8 +214,63 @@ def _get_bundle_yaml(self, archive):
206214 if member .filename == "bundle.yaml" :
207215 return archive .read (member )
208216 raise JujuError ("bundle.yaml not found" )
217+
218+ async def _resolve_charms (self ):
219+ deployed = dict ()
220+
221+ specs = self .applications_specs
222+ for name in self .applications :
223+ spec = specs [name ]
224+ app = self .model .applications .get (name , None )
225+
226+ cons = None
227+ if app is not None :
228+ deployed [name ] = name
229+
230+ if is_local_charm (spec ['charm' ]):
231+ spec .charm = self .model .applications [name ]
232+ continue
233+
234+ if spec ['charm' ] == app .charm_url :
235+ continue
236+
237+ cons = await app .get_constraints ()
238+
239+ if is_local_charm (spec ['charm' ]):
240+ continue
241+
242+ if self .charms_facade is not None :
243+ charm_url = URL .parse (spec ['charm' ])
244+ channel = None
245+ track , risk = '' , ''
246+ if 'channel' in spec :
247+ channel = Channel .parse (spec ['channel' ])
248+ track , risk = channel .track , channel .risk
249+
250+ if cons is not None and cons ['arch' ] != '' :
251+ architecture = cons ['arch' ]
252+ else :
253+ architecture = await self .model ._resolve_architecture (charm_url )
254+
255+ origin = client .CharmOrigin (source = "charm-hub" ,
256+ architecture = architecture ,
257+ risk = risk ,
258+ track = track )
259+ charm_url , charm_origin = await self .model ._resolve_charm (charm_url , origin )
260+
261+ spec ['charm' ] = str (charm_url )
262+
263+ if str (channel ) not in self .origins :
264+ self .origins [str (charm_url )] = {}
265+ self .origins [str (charm_url )][str (channel )] = charm_origin
266+ else :
267+ results = await self .client_facade .ResolveCharms (references = [spec ['charm' ]])
268+ # TODO (stickupkid): Ensure that this works as expected.
269+
209270
210271 async def execute_plan (self ):
272+ await self ._resolve_charms ()
273+
211274 changes = ChangeSet (self .plan .changes )
212275 for step in changes .sorted ():
213276 change_cls = self .change_types .get (step .method )
@@ -222,8 +285,13 @@ def applications(self):
222285 apps_dict = self .bundle .get ('applications' ,
223286 self .bundle .get ('services' , {}))
224287 return list (apps_dict .keys ())
288+
289+ @property
290+ def applications_specs (self ):
291+ return self .bundle .get ('applications' ,
292+ self .bundle .get ('services' , {}))
225293
226- def resolveRelation (self , reference ):
294+ def resolve_relation (self , reference ):
227295 parts = reference .split (":" , maxsplit = 1 )
228296 application = self .resolve (parts [0 ])
229297 if len (parts ) == 1 :
@@ -238,6 +306,10 @@ def resolve(self, reference):
238306 return reference
239307
240308
309+ def is_local_charm (charm_url ):
310+ return charm_url .startswith ('.' ) or charm_url .startswith ('local:' )
311+
312+
241313def get_charm_series (path ):
242314 """Inspects the charm directory at ``path`` and returns a default
243315 series from its metadata.yaml (the first item in the 'series' list).
@@ -354,13 +426,15 @@ def __init__(self, change_id, requires, params=None):
354426 self .resources = params [7 ]
355427 self .devices = None
356428 self .num_units = None
429+ self .channel = None
357430 else :
358431 # Juju 2.5+ sends devices before endpoint bindings, as well as num_units
359432 # There might be placement but we need to ignore that.
360433 self .devices = {k : parse_device_constraint (v ) for k , v in params [6 ].items ()}
361434 self .endpoint_bindings = params [7 ]
362435 self .resources = params [8 ]
363436 self .num_units = params [9 ]
437+ self .channel = params [10 ]
364438
365439 elif isinstance (params , dict ):
366440 AddApplicationChange .from_dict (self , params )
@@ -397,6 +471,14 @@ async def run(self, context):
397471 else :
398472 resources = {}
399473
474+ channel = None
475+ if self .channel is not None :
476+ channel = Channel .parse (self .channel ).normalize ()
477+
478+ origin = context .origins [str (url )][str (channel )]
479+ if origin is None :
480+ raise JujuError ("expected origin to be valid for application {} and charm {}" .format (self .application , self .charm ))
481+
400482 await context .model ._deploy (
401483 charm_url = charm ,
402484 application = self .application ,
@@ -484,6 +566,7 @@ async def run(self, context):
484566 # We don't add local charms because they've already been added
485567 # by self._handle_local_charms
486568 url = URL .parse (str (self .charm ))
569+ ch = None
487570 identifier = None
488571 if Schema .LOCAL .matches (url .schema ):
489572 return self .charm
@@ -511,7 +594,12 @@ async def run(self, context):
511594 raise JujuError ('unknown charm {}' .format (self .charm ))
512595
513596 await context .model ._add_charm (identifier , origin )
514- return url .path ()
597+
598+ if str (ch ) not in context .origins :
599+ context .origins [str (identifier )] = {}
600+ context .origins [str (identifier )][str (ch )] = origin
601+
602+ return str (identifier ) if identifier is not None else url .path ()
515603
516604 def __str__ (self ):
517605 series = ""
@@ -657,8 +745,8 @@ async def run(self, context):
657745 :param context: is used for any methods or properties required to
658746 perform a change.
659747 """
660- ep1 = context .resolveRelation (self .endpoint1 )
661- ep2 = context .resolveRelation (self .endpoint2 )
748+ ep1 = context .resolve_relation (self .endpoint1 )
749+ ep2 = context .resolve_relation (self .endpoint2 )
662750
663751 log .info ('Relating %s <-> %s' , ep1 , ep2 )
664752 return await context .model .add_relation (ep1 , ep2 )
0 commit comments