Skip to content

Commit 49d3e86

Browse files
authored
Merge branch 'master' into juju-config-dir-fix-3.x
2 parents 1120738 + 03332e6 commit 49d3e86

7 files changed

Lines changed: 173 additions & 99 deletions

File tree

juju/application.py

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44
import hashlib
55
import json
66
import logging
7-
import pathlib
7+
from pathlib import Path
88

99
from . import model, tag, utils, jasyncio
1010
from .url import URL
1111
from .status import derive_status
1212
from .annotationhelper import _get_annotations, _set_annotations
1313
from .client import client
1414
from .errors import JujuError, JujuApplicationConfigError
15-
from .bundle import get_charm_series
15+
from .bundle import get_charm_series, is_local_charm
1616
from .placement import parse as parse_placement
1717
from .origin import Channel, Source
1818

@@ -661,6 +661,8 @@ async def refresh(
661661
:param str switch: Crossgrade charm url
662662
663663
"""
664+
if switch is not None and path is not None:
665+
raise ValueError("switch and path are mutually exclusive")
664666

665667
if switch is not None and revision is not None:
666668
raise ValueError("switch and revision are mutually exclusive")
@@ -677,17 +679,18 @@ async def refresh(
677679
if charm_url_origin_result.error is not None:
678680
err = charm_url_origin_result.error
679681
raise JujuError(f'{err.code} : {err.message}')
680-
charm_url = switch or charm_url_origin_result.url
681682
origin = charm_url_origin_result.charm_origin
682683

683-
if path is not None:
684+
if path is not None or (switch is not None and is_local_charm(switch)):
684685
await self.local_refresh(origin, force, force_series,
685-
force_units, path, resources)
686+
force_units, path or switch, resources)
686687
return
687688

688689
if resources is not None:
689690
raise NotImplementedError("resources option is not implemented")
690691

692+
# If switch is not None at this point, that means it's a switch to a store charm
693+
charm_url = switch or charm_url_origin_result.url
691694
parsed_url = URL.parse(charm_url)
692695
charm_name = parsed_url.name
693696

@@ -733,8 +736,6 @@ async def refresh(
733736
err = charm_origin_result.error
734737
raise JujuError(f'{err.code} : {err.message}')
735738

736-
# Now take care of the resources:
737-
738739
# Already prepped the charm_resources
739740
# Now get the existing resources from the ResourcesFacade
740741
request_data = [client.Entity(self.tag)]
@@ -808,22 +809,22 @@ async def local_refresh(
808809
path=None, resources=None):
809810
"""Refresh the charm for this application with a local charm.
810811
811-
:param str channel: Channel to use when getting the charm from the
812-
charm store, e.g. 'development'
812+
:param dict charm_origin: The charm origin of the destination charm
813+
we're refreshing to
814+
:param bool force: Refresh even if validation checks fail
813815
:param bool force_series: Refresh even if series of deployed
814816
application is not supported by the new charm
815817
:param bool force_units: Refresh all units immediately, even if in
816818
error state
817819
:param str path: Refresh to a charm located at path
818820
:param dict resources: Dictionary of resource name/filepath pairs
819-
:param int revision: Explicit refresh revision
820-
:param str switch: Crossgrade charm url
821821
822822
"""
823823
app_facade = self._facade()
824824

825-
if not isinstance(path, pathlib.Path):
826-
path = pathlib.Path(path)
825+
if isinstance(path, str) and path.startswith("local:"):
826+
path = path[6:]
827+
path = Path(path)
827828
charm_dir = path.expanduser().resolve()
828829
model_config = await self.get_config()
829830

juju/bundle.py

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ def _resolve_include_file_config(self, bundle_dir):
229229

230230
return self.bundle, self.overlays
231231

232-
async def fetch_plan(self, bundle_url, origin, overlays=[]):
232+
async def fetch_plan(self, bundle, origin, overlays=[]):
233233
"""fetch_plan is called by the model.deploy(). It gathers the information about the
234234
bundle to be deployed (whether local or CharmHub), straightens it up, applies overlays
235235
if any overlays are given. Validates the bundle against known issues. Resolves and adds
@@ -245,19 +245,21 @@ async def fetch_plan(self, bundle_url, origin, overlays=[]):
245245
246246
:returns: None
247247
"""
248-
entity_id = bundle_url.path()
249-
is_local = Schema.LOCAL.matches(bundle_url.schema)
250248
bundle_dir = None
251249

252-
if is_local and os.path.isfile(entity_id):
253-
bundle_yaml = Path(entity_id).read_text()
254-
bundle_dir = Path(entity_id).parent
255-
elif is_local and os.path.isdir(entity_id):
256-
bundle_yaml = (Path(entity_id) / "bundle.yaml").read_text()
257-
bundle_dir = Path(entity_id)
250+
if is_local_bundle(str(bundle)):
251+
path = str(bundle)
252+
if path.startswith("local:"):
253+
path = path[6:]
254+
bundle_yaml, bundle_dir = read_local_bundle(path)
258255

259-
if Schema.CHARM_HUB.matches(bundle_url.schema):
260-
bundle_yaml = await self._download_bundle(bundle_url, origin)
256+
else:
257+
if client.CharmsFacade.best_facade_version(self.model.connection()) < 3:
258+
url = URL.parse(bundle, default_store=Schema.CHARM_STORE)
259+
else:
260+
url = URL.parse(bundle)
261+
path = url.path()
262+
bundle_yaml = await self._download_bundle(bundle, origin)
261263

262264
if not bundle_yaml:
263265
raise JujuError('empty bundle, nothing to deploy')
@@ -284,7 +286,7 @@ async def fetch_plan(self, bundle_url, origin, overlays=[]):
284286

285287
self.bundle = await self._validate_bundle(self.bundle)
286288

287-
if is_local:
289+
if is_local_bundle(path):
288290
self.bundle = await self._handle_local_charms(self.bundle, bundle_dir)
289291

290292
self.bundle, self.overlays = self._resolve_include_file_config(bundle_dir)
@@ -295,7 +297,7 @@ async def fetch_plan(self, bundle_url, origin, overlays=[]):
295297
yaml_data = "---\n".join(_yaml_data)
296298

297299
self.plan = await self.bundle_facade.GetChangesMapArgs(
298-
bundleurl=entity_id,
300+
bundleurl=path,
299301
yaml=yaml_data)
300302

301303
if self.plan.errors and any(self.plan.errors):
@@ -389,7 +391,6 @@ async def _resolve_charms(self):
389391
track=track,
390392
base=base,
391393
)
392-
393394
charm_url, charm_origin = await self.model._resolve_charm(charm_url, origin)
394395
spec['charm'] = str(charm_url)
395396
else:
@@ -443,6 +444,21 @@ def is_local_charm(charm_url):
443444
return charm_url.startswith('.') or charm_url.startswith('local:') or os.path.isabs(charm_url)
444445

445446

447+
is_local_bundle = is_local_charm
448+
449+
450+
def read_local_bundle(path):
451+
path = Path(path)
452+
if os.path.isfile(path):
453+
bundle_yaml = path.read_text()
454+
bundle_dir = path.parent
455+
elif os.path.isdir(path):
456+
bundle_yaml = (path / "bundle.yaml").read_text()
457+
bundle_dir = path
458+
459+
return (bundle_yaml, bundle_dir)
460+
461+
446462
async def get_charm_series(metadata, model):
447463
"""Inspects the given metadata and returns a default series from its
448464
metadata.yaml (the first item in the 'series' list).
@@ -676,12 +692,12 @@ async def run(self, context):
676692

677693
# We don't add local charms because they've already been added
678694
# by self._handle_local_charms
695+
if is_local_charm(str(self.charm)):
696+
return self.charm
697+
679698
url = URL.parse(str(self.charm))
680699
ch = None
681700
identifier = None
682-
if Schema.LOCAL.matches(url.schema):
683-
return self.charm
684-
685701
if Schema.CHARM_HUB.matches(url.schema):
686702
ch = Channel('latest', 'stable')
687703
if self.channel:
@@ -700,7 +716,7 @@ async def run(self, context):
700716
if identifier is None:
701717
raise JujuError('unknown charm {}'.format(self.charm))
702718

703-
await context.model._add_charm(identifier, origin)
719+
await context.model._add_charm(str(identifier), origin)
704720

705721
if str(ch) not in context.origins:
706722
context.origins[str(identifier)] = {}

juju/model.py

Lines changed: 48 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -450,7 +450,7 @@ class LocalDeployType:
450450
"""LocalDeployType deals with local only deployments.
451451
"""
452452

453-
async def resolve(self, url, architecture,
453+
async def resolve(self, charm_path, architecture,
454454
app_name=None, channel=None, series=None,
455455
revision=None, entity_url=None, force=False,
456456
model_conf=None):
@@ -461,36 +461,27 @@ async def resolve(self, url, architecture,
461461
-- revision flag is ignored for local charms
462462
"""
463463

464-
entity_url = url.path()
465-
entity_path = Path(entity_url)
466-
bundle_path = entity_path / 'bundle.yaml'
464+
entity_path = Path(charm_path)
465+
if entity_path.suffix == '.yaml':
466+
bundle_path = entity_path
467+
else:
468+
bundle_path = entity_path / 'bundle.yaml'
467469

468-
identifier = entity_url
469470
origin = client.CharmOrigin(source="local", architecture=architecture)
470471
if not (entity_path.is_dir() or entity_path.is_file()):
471472
raise JujuError('{} path not found'.format(entity_url))
472473

473-
is_bundle = (
474-
(entity_path.suffix == ".yaml" and entity_path.exists()) or
475-
bundle_path.exists()
476-
)
474+
is_bundle = bundle_path.exists()
477475

478476
if app_name is None:
479-
app_name = url.name
480-
481-
if not is_bundle:
482-
entity_url = url.path()
483-
entity_path = Path(entity_url)
484-
if entity_path.suffix == '.charm':
485-
with zipfile.ZipFile(str(entity_path), 'r') as charm_file:
486-
metadata = yaml.load(charm_file.read('metadata.yaml'), Loader=yaml.FullLoader)
487-
else:
488-
metadata_path = entity_path / 'metadata.yaml'
489-
metadata = yaml.load(metadata_path.read_text(), Loader=yaml.FullLoader)
490-
app_name = metadata['name']
477+
if is_bundle:
478+
bundle_with_overlays = [b for b in yaml.safe_load_all(bundle_path.read_text())]
479+
app_name = bundle_with_overlays[0].get('name', '')
480+
else:
481+
app_name = utils.get_local_charm_metadata(entity_path)["name"]
491482

492483
return DeployTypeResult(
493-
identifier=identifier,
484+
identifier=charm_path,
494485
origin=origin,
495486
app_name=app_name,
496487
is_local=True,
@@ -541,7 +532,7 @@ async def resolve(self, url, architecture,
541532
raise JujuError('revision and channel are mutually exclusive when deploying a bundle. Please choose one.')
542533

543534
if app_name is None:
544-
app_name = url.name
535+
app_name = charm_url.name
545536

546537
return DeployTypeResult(
547538
identifier=str(charm_url),
@@ -591,8 +582,8 @@ def __init__(
591582
self._charmhub = CharmHub(self)
592583

593584
self.deploy_types = {
594-
"local": LocalDeployType(),
595-
"ch": CharmhubDeployType(self._resolve_charm),
585+
Schema.LOCAL: LocalDeployType(),
586+
Schema.CHARM_HUB: CharmhubDeployType(self._resolve_charm),
596587
}
597588

598589
def is_connected(self):
@@ -1670,7 +1661,7 @@ async def deploy(
16701661
storage=None, to=None, devices=None, trust=False, attach_storage=[]):
16711662
"""Deploy a new service or bundle.
16721663
1673-
:param str entity_url: Charm or bundle url
1664+
:param str entity_url: Charm or bundle to deploy. Charm url or file path
16741665
:param str application_name: Name to give the service
16751666
:param dict bind: <charm endpoint>:<network space> pairs
16761667
:param str channel: Charm store channel from which to retrieve
@@ -1716,26 +1707,33 @@ async def deploy(
17161707
raise JujuError("Expected attach_storage to be a list of strings, given {}".format(attach_storage))
17171708

17181709
# Ensure what we pass in, is a string.
1719-
entity_url = str(entity_url)
1720-
if is_local_charm(entity_url) and not entity_url.startswith("local:"):
1721-
entity_url = "local:{}".format(entity_url)
1710+
entity = str(entity_url)
1711+
if is_local_charm(entity):
1712+
if entity.startswith("local:"):
1713+
entity = entity[6:]
1714+
architecture = await self._resolve_architecture()
1715+
schema = Schema.LOCAL
17221716

1723-
if client.CharmsFacade.best_facade_version(self.connection()) < 3:
1724-
url = URL.parse(str(entity_url), default_store=Schema.CHARM_STORE)
17251717
else:
1726-
url = URL.parse(str(entity_url))
1718+
if client.CharmsFacade.best_facade_version(self.connection()) < 3:
1719+
url = URL.parse(entity, default_store=Schema.CHARM_STORE)
1720+
else:
1721+
url = URL.parse(entity)
1722+
entity = str(url)
17271723

1728-
architecture = await self._resolve_architecture(url)
1724+
architecture = await self._resolve_architecture(url)
1725+
schema = url.schema
1726+
name = url.name
17291727

1730-
if str(url.schema) not in self.deploy_types:
1731-
raise JujuError("unknown deploy type {}, expected charmhub or local".format(url.schema))
1728+
if schema not in self.deploy_types:
1729+
raise JujuError("unknown deploy type {}, expected charmhub or local".format(schema))
17321730

17331731
model_conf = await self.get_config()
1734-
res = await self.deploy_types[str(url.schema)].resolve(url, architecture,
1735-
application_name, channel,
1736-
series, revision,
1737-
entity_url, force,
1738-
model_conf)
1732+
res = await self.deploy_types[schema].resolve(entity, architecture,
1733+
application_name, channel,
1734+
series, revision,
1735+
entity_url, force,
1736+
model_conf)
17391737

17401738
if res.identifier is None:
17411739
raise JujuError('unknown charm or bundle {}'.format(entity_url))
@@ -1750,7 +1748,7 @@ async def deploy(
17501748

17511749
if res.is_bundle:
17521750
handler = BundleHandler(self, trusted=trust, forced=force)
1753-
await handler.fetch_plan(url, charm_origin, overlays=overlays)
1751+
await handler.fetch_plan(entity, charm_origin, overlays=overlays)
17541752
await handler.execute_plan()
17551753
extant_apps = {app for app in self.applications}
17561754
pending_apps = handler.applications - extant_apps
@@ -1778,8 +1776,7 @@ async def deploy(
17781776
charm_origin)
17791777
else:
17801778
charm_origin = add_charm_res.charm_origin
1781-
if Schema.CHARM_HUB.matches(url.schema):
1782-
1779+
if Schema.CHARM_HUB.matches(schema):
17831780
if client.ApplicationFacade.best_facade_version(self.connection()) >= 19:
17841781
server_side_deploy = True
17851782
else:
@@ -1793,7 +1790,7 @@ async def deploy(
17931790
identifier,
17941791
add_charm_res.charm_origin)
17951792

1796-
is_sub = await self.charmhub.is_subordinate(url.name)
1793+
is_sub = await self.charmhub.is_subordinate(name)
17971794
if is_sub:
17981795
if num_units > 1:
17991796
raise JujuError("cannot use num_units with subordinate application")
@@ -1879,7 +1876,7 @@ async def _resolve_charm(self, url, origin, force=False, series=None, model_conf
18791876
Returns the confirmed origin returned by the Juju API to be used in
18801877
calls like ApplicationFacade.Deploy
18811878
1882-
:returns str, client.CharmOrigin, [str]
1879+
:returns url.URL, client.CharmOrigin, [str]
18831880
"""
18841881
charms_cls = client.CharmsFacade
18851882
if charms_cls.best_facade_version(self.connection()) < 3:
@@ -1919,16 +1916,18 @@ async def _resolve_charm(self, url, origin, force=False, series=None, model_conf
19191916
result.charm_origin.base = utils.get_base_from_origin_or_channel(resolved_origin, selected_series)
19201917
charm_url.series = selected_series
19211918

1922-
return str(charm_url), resolved_origin
1919+
return charm_url, resolved_origin
19231920

1924-
async def _resolve_architecture(self, url):
1921+
async def _resolve_architecture(self, url=None):
19251922
"""_resolve_architecture returns the architecture for a given charm url.
1923+
If the charm url is absent, or doesn't specific an arch, we return the
1924+
default architecture from the model.
1925+
19261926
:param str url: the client.URL to determine the arch for
19271927
19281928
:returns str architecture for the given url
19291929
"""
1930-
1931-
if url.architecture:
1930+
if url is not None and url.architecture:
19321931
return url.architecture
19331932

19341933
constraints = await self.get_constraints()

0 commit comments

Comments
 (0)