@@ -425,6 +425,139 @@ def latest(self):
425425 return self .model .state .get_entity (self .entity_type , self .entity_id )
426426
427427
428+ class DeployTypeResult :
429+ """DeployTypeResult represents the result of a deployment type after a
430+ resolution.
431+ """
432+
433+ def __init__ (self , identifier , origin , app_name , is_local = False , is_bundle = False ):
434+ self .identifier = identifier
435+ self .origin = origin
436+ self .app_name = app_name
437+ self .is_local = is_local
438+ self .is_bundle = is_bundle
439+
440+
441+ class LocalDeployType :
442+ """LocalDeployType deals with local only deployments.
443+ """
444+
445+ async def resolve (self , url , architecture , app_name = None , channel = None , series = None , entity_url = None ):
446+ """resolve attempts to resolve a local charm or bundle using the url
447+ and architecture. If information is missing, it will attempt to backfill
448+ that information, before sending the result back.
449+ """
450+
451+ entity_url = url .path ()
452+ entity_path = Path (entity_url )
453+ bundle_path = entity_path / 'bundle.yaml'
454+
455+ identifier = entity_url
456+ origin = client .CharmOrigin (source = "local" , architecture = architecture )
457+ if not (entity_path .is_dir () or entity_path .is_file ()):
458+ raise JujuError ('{} path not found' .format (entity_url ))
459+
460+ is_bundle = (
461+ (entity_url .endswith (".yaml" ) and entity_path .exists ()) or
462+ bundle_path .exists ()
463+ )
464+
465+ if app_name is None :
466+ app_name = url .name
467+
468+ if not is_bundle :
469+ entity_url = url .path ()
470+ entity_path = Path (entity_url )
471+ if str (entity_path ).endswith ('.charm' ):
472+ with zipfile .ZipFile (entity_path , 'r' ) as charm_file :
473+ metadata = yaml .load (charm_file .read ('metadata.yaml' ), Loader = yaml .FullLoader )
474+ else :
475+ metadata_path = entity_path / 'metadata.yaml'
476+ metadata = yaml .load (metadata_path .read_text (), Loader = yaml .FullLoader )
477+ app_name = metadata ['name' ]
478+
479+ return DeployTypeResult (
480+ identifier = identifier ,
481+ origin = origin ,
482+ app_name = app_name ,
483+ is_local = True ,
484+ is_bundle = is_bundle ,
485+ )
486+
487+
488+ class CharmStoreDeployType :
489+ """CharmStoreDeployType defines a class for resolving and deploying charm
490+ store charms and bundle.
491+ """
492+
493+ def __init__ (self , charmstore , get_series ):
494+ self .charmstore = charmstore
495+ self .get_series = get_series
496+
497+ async def resolve (self , url , architecture , app_name = None , channel = None , series = None , entity_url = None ):
498+ """resolve attempts to resolve charmstore charms or bundles. A request
499+ to the charmstore is required to get more information about the
500+ underlying identifier.
501+ """
502+
503+ result = await self .charmstore .entity (str (url ),
504+ channel = channel ,
505+ include_stats = False )
506+ identifier = result ['Id' ]
507+ is_bundle = url .series == "bundle"
508+ if not series :
509+ series = self .get_series (entity_url , result )
510+
511+ if app_name is None and not is_bundle :
512+ app_name = result ['Meta' ]['charm-metadata' ]['Name' ]
513+
514+ origin = client .CharmOrigin (source = "charm-store" ,
515+ architecture = architecture ,
516+ risk = channel ,
517+ series = series )
518+
519+ return DeployTypeResult (
520+ identifier = identifier ,
521+ app_name = app_name ,
522+ origin = origin ,
523+ is_bundle = is_bundle ,
524+ )
525+
526+
527+ class CharmhubDeployType :
528+ """CharmhubDeployType defines a class for resolving and deploying charmhub
529+ charms and bundles.
530+ """
531+
532+ def __init__ (self , charm_resolver ):
533+ self .charm_resolver = charm_resolver
534+
535+ async def resolve (self , url , architecture , app_name = None , channel = None , series = None , entity_url = None ):
536+ """resolve attempts to resolve charmhub charms or bundles. A request to
537+ the charmhub API is required to correctly determine the charm url and
538+ underlying origin.
539+ """
540+
541+ ch = Channel ('latest' , 'stable' )
542+ if channel is not None :
543+ ch = Channel .parse (channel ).normalize ()
544+ origin = client .CharmOrigin (source = "charm-hub" ,
545+ architecture = architecture ,
546+ risk = ch .risk ,
547+ track = ch .track )
548+ charm_url , origin = await self .charm_resolver (url , origin )
549+
550+ if app_name is None :
551+ app_name = url .name
552+
553+ return DeployTypeResult (
554+ identifier = charm_url ,
555+ app_name = app_name ,
556+ origin = origin ,
557+ is_bundle = origin .type_ == "bundle" ,
558+ )
559+
560+
428561class Model :
429562 """
430563 The main API for interacting with a Juju model.
@@ -468,6 +601,12 @@ def __init__(
468601 self ._charmhub = CharmHub (self )
469602 self ._charmstore = CharmStore (self ._connector .loop )
470603
604+ self .deploy_types = {
605+ "local" : LocalDeployType (),
606+ "cs" : CharmStoreDeployType (self ._charmstore , self ._get_series ),
607+ "ch" : CharmhubDeployType (self ._resolve_charm ),
608+ }
609+
471610 def is_connected (self ):
472611 """Reports whether the Model is currently connected."""
473612 return self ._connector .is_connected ()
@@ -1419,78 +1558,25 @@ async def deploy(
14191558 if trust and (self .info .agent_version < client .Number .from_json ('2.4.0' )):
14201559 raise NotImplementedError ("trusted is not supported on model version {}" .format (self .info .agent_version ))
14211560
1422- # Attempt to resolve a charm or bundle based on the URL.
1423- # In an ideal world this should be moved to the controller, and we
1424- # wouldn't have to deal with this at all.
1425- is_local = False
1426- is_bundle = False
1427- identifier = None
1428- origin = None
1429- result = None
1430-
14311561 # Ensure what we pass in, is a string.
14321562 entity_url = str (entity_url )
14331563 if is_local_charm (entity_url ) and not entity_url .startswith ("local:" ):
14341564 entity_url = "local:{}" .format (entity_url )
14351565 url = URL .parse (str (entity_url ))
14361566 architecture = await self ._resolve_architecture (url )
14371567
1438- if Schema .LOCAL .matches (url .schema ):
1439- entity_url = url .path ()
1440- entity_path = Path (entity_url )
1441- bundle_path = entity_path / 'bundle.yaml'
1442-
1443- identifier = entity_url
1444- origin = client .CharmOrigin (source = "local" , architecture = architecture )
1445- if not (entity_path .is_dir () or entity_path .is_file ()):
1446- raise JujuError ('{} path not found' .format (entity_url ))
1447-
1448- is_local = True
1449- is_bundle = (
1450- (entity_url .endswith (".yaml" ) and entity_path .exists ()) or
1451- bundle_path .exists ()
1452- )
1453-
1454- elif Schema .CHARM_STORE .matches (url .schema ):
1455- result = await self .charmstore .entity (str (url ),
1456- channel = channel ,
1457- include_stats = False )
1458- identifier = result ['Id' ]
1459- origin = client .CharmOrigin (source = "charm-store" ,
1460- architecture = architecture ,
1461- risk = channel )
1462- is_bundle = url .series == "bundle"
1463- if not series :
1464- series = self ._get_series (entity_url , result )
1465-
1466- elif Schema .CHARM_HUB .matches (url .schema ):
1467- ch = Channel ('latest' , 'stable' )
1468- if channel :
1469- ch = Channel .parse (channel ).normalize ()
1470- origin = client .CharmOrigin (source = "charm-hub" ,
1471- architecture = architecture ,
1472- risk = ch .risk ,
1473- track = ch .track )
1474- charm_url , origin = await self ._resolve_charm (url , origin )
1475-
1476- identifier = charm_url
1477- is_bundle = origin .type_ == "bundle"
1478-
1479- if identifier is None :
1568+ if str (url .schema ) not in self .deploy_types :
1569+ raise JujuError ("unknown deploy type {}, expected charmhub, charmstore or local" .format (url .schema ))
1570+ res = await self .deploy_types [str (url .schema )].resolve (url , architecture , application_name , channel , series , entity_url )
1571+
1572+ if res .identifier is None :
14801573 raise JujuError ('unknown charm or bundle {}' .format (entity_url ))
1574+ identifier = res .identifier
14811575
1482- if not application_name :
1483- if Schema .CHARM_HUB .matches (url .schema ):
1484- # For charmhub charms, we don't have the metadata and we're not
1485- # going to get it, so fallback to the url and use that one if a
1486- # user didn't specify it.
1487- application_name = url .name
1488- elif result is not None and not is_bundle :
1489- application_name = result ['Meta' ]['charm-metadata' ]['Name' ]
1490-
1491- if is_bundle :
1576+ series = res .origin .series or series
1577+ if res .is_bundle :
14921578 handler = BundleHandler (self , trusted = trust , forced = force )
1493- await handler .fetch_plan (url , origin )
1579+ await handler .fetch_plan (url , res . origin )
14941580 await handler .execute_plan ()
14951581 extant_apps = {app for app in self .applications }
14961582 pending_apps = set (handler .applications ) - extant_apps
@@ -1508,27 +1594,15 @@ async def deploy(
15081594 else :
15091595 # XXX: we're dropping local resources here, but we don't
15101596 # actually support them yet anyway
1511- if not is_local :
1512- await self ._add_charm (identifier , origin )
1597+ if not res . is_local :
1598+ await self ._add_charm (identifier , res . origin )
15131599
15141600 # TODO (stickupkid): Handle charmhub charms, for now we'll only
15151601 # handle charmstore charms.
15161602 if Schema .CHARM_STORE .matches (url .schema ):
1517- resources = await self ._add_store_resources (application_name ,
1518- identifier ,
1519- entity = result )
1603+ resources = await self ._add_store_resources (res .app_name ,
1604+ identifier )
15201605 else :
1521- if not application_name :
1522- entity_url = url .path ()
1523- entity_path = Path (entity_url )
1524- if str (entity_path ).endswith ('.charm' ):
1525- with zipfile .ZipFile (entity_path , 'r' ) as charm_file :
1526- metadata = yaml .load (charm_file .read ('metadata.yaml' ), Loader = yaml .FullLoader )
1527- else :
1528- metadata_path = entity_path / 'metadata.yaml'
1529- metadata = yaml .load (metadata_path .read_text (), Loader = yaml .FullLoader )
1530- application_name = metadata ['name' ]
1531-
15321606 # We have a local charm dir that needs to be uploaded
15331607 charm_dir = os .path .abspath (
15341608 os .path .expanduser (identifier ))
@@ -1547,7 +1621,7 @@ async def deploy(
15471621
15481622 return await self ._deploy (
15491623 charm_url = identifier ,
1550- application = application_name ,
1624+ application = res . app_name ,
15511625 series = series ,
15521626 config = config ,
15531627 constraints = constraints ,
@@ -1611,11 +1685,9 @@ async def _resolve_architecture(self, url):
16111685 return DEFAULT_ARCHITECTURE
16121686
16131687 async def _add_store_resources (self , application , entity_url ,
1614- overrides = None , entity = None ):
1615- if not entity :
1616- # avoid extra charm store call if one was already made
1617- entity = await self .charmstore .entity (entity_url ,
1618- include_stats = False )
1688+ overrides = None ):
1689+ entity = await self .charmstore .entity (entity_url ,
1690+ include_stats = False )
16191691 resources = [
16201692 {
16211693 'description' : resource ['Description' ],
0 commit comments