Skip to content

Commit 0e901f8

Browse files
Implement deployment types
The following implements deployment types so that it's easier to reason about the logic of a deployment strategy (charmhub, charmstore and local charms/bundles). This is a quite a simple change, but does have a dramatic effect or readability of the deploy method in model.
1 parent 9abe4b6 commit 0e901f8

1 file changed

Lines changed: 156 additions & 84 deletions

File tree

juju/model.py

Lines changed: 156 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
428561
class 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

Comments
 (0)