Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
jack-w-shaw committed Oct 23, 2023
1 parent 5134335 commit afeb4e7
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 91 deletions.
31 changes: 14 additions & 17 deletions examples/local_refresh.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,30 @@
# Copyright 2023 Canonical Ltd.
# Licensed under the Apache V2, see LICENCE file for details.

"""
This example:
1. Connects to the current model
2. Upgrades previously deployed ubuntu charm
"""
from juju import jasyncio
from juju.model import Model


async def main():
model = Model()
print('Connecting to model')
# connect to current model with current user, per Juju CLI
await model.connect()

try:
print('Get deployed application')
app = model.appplications["ubuntu"]

print('Refresh/Upgrade Ubuntu charm with local charm')
await app.refresh(path="path/to/local/ubuntu.charm")
print('path="/home/jack/charms/ubuntu"')
await depl(model, path="/home/jack/charms/ubuntu")
print('switch="/home/jack/charms/ubuntu"')
await depl(model, switch="/home/jack/charms/ubuntu")
print('switch="local:/home/jack/charms/ubuntu"')
await depl(model, switch="local:/home/jack/charms/ubuntu")
finally:
print('Disconnecting from model')
await model.disconnect()

async def depl(model, **kwargs):
try:
app = await model.deploy("ubuntu")
await model.block_until(lambda: app.units[0].workload_status == 'active')
await app.refresh(**kwargs)
finally:
await app.remove()
await model.block_until(lambda: not len(model.applications))

if __name__ == '__main__':
jasyncio.run(main())
61 changes: 40 additions & 21 deletions juju/bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,24 +223,28 @@ def _resolve_include_file_config(self, bundle_dir):

return self.bundle, self.overlays

async def fetch_plan(self, charm_url, origin, overlays=[]):
entity_id = charm_url.path()
is_local = Schema.LOCAL.matches(charm_url.schema)
async def fetch_plan(self, bundle, origin, overlays=[]):
bundle_dir = None

if is_local and os.path.isfile(entity_id):
bundle_yaml = Path(entity_id).read_text()
bundle_dir = Path(entity_id).parent
elif is_local and os.path.isdir(entity_id):
bundle_yaml = (Path(entity_id) / "bundle.yaml").read_text()
bundle_dir = Path(entity_id)
if is_local_bundle(str(bundle)):
path = str(bundle)
if path.startswith("local:"):
path = path[6:]
bundle_yaml, bundle_dir = read_local_bundle(path)

if Schema.CHARM_STORE.matches(charm_url.schema):
bundle_yaml = await self.charmstore.files(entity_id,
filename='bundle.yaml',
read_file=True)
elif Schema.CHARM_HUB.matches(charm_url.schema):
bundle_yaml = await self._download_bundle(charm_url, origin)
else:
if client.CharmsFacade.best_facade_version(self.connection()) < 3:
url = URL.parse(bundle, default_store=Schema.CHARM_STORE)
else:
url = URL.parse(bundle)
path = url.path

if Schema.CHARM_STORE.matches(url.schema):
bundle_yaml = await self.charmstore.files(bundle,
filename='bundle.yaml',
read_file=True)
elif Schema.CHARM_HUB.matches(url.schema):
bundle_yaml = await self._download_bundle(bundle, origin)

if not bundle_yaml:
raise JujuError('empty bundle, nothing to deploy')
Expand All @@ -267,7 +271,7 @@ async def fetch_plan(self, charm_url, origin, overlays=[]):

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

if is_local:
if is_local_bundle(path):
self.bundle = await self._handle_local_charms(self.bundle, bundle_dir)

self.bundle, self.overlays = self._resolve_include_file_config(bundle_dir)
Expand All @@ -278,7 +282,7 @@ async def fetch_plan(self, charm_url, origin, overlays=[]):
yaml_data = "---\n".join(_yaml_data)

self.plan = await self.bundle_facade.GetChanges(
bundleurl=entity_id,
bundleurl=path,
yaml=yaml_data)

if self.plan.errors:
Expand Down Expand Up @@ -416,6 +420,21 @@ def is_local_charm(charm_url):
return charm_url.startswith('.') or charm_url.startswith('local:') or os.path.isabs(charm_url)


is_local_bundle = is_local_charm


def read_local_bundle(path):
path = Path(path)
if os.path.isfile(path):
bundle_yaml = path.read_text()
bundle_dir = path.parent
elif os.path.isdir(path):
bundle_yaml = (path / "bundle.yaml").read_text()
bundle_dir = path

return (bundle_yaml, bundle_dir)


async def get_charm_series(metadata, model):
"""Inspects the given metadata and returns a default series from its
metadata.yaml (the first item in the 'series' list).
Expand Down Expand Up @@ -709,14 +728,14 @@ async def run(self, context):

# We don't add local charms because they've already been added
# by self._handle_local_charms
url = URL.parse(str(self.charm))
ch = None
identifier = None
if Schema.LOCAL.matches(url.schema):
if is_local_charm(str(self.charm)):
origin = client.CharmOrigin(source="local", risk="stable")
context.origins[self.charm] = {str(None): origin}
return self.charm

url = URL.parse(str(self.charm))
ch = None
identifier = None
if Schema.CHARM_STORE.matches(url.schema):
entity_id = await context.charmstore.entityId(self.charm, channel=self.channel)
log.debug('Adding %s', entity_id)
Expand Down
90 changes: 43 additions & 47 deletions juju/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -452,18 +452,16 @@ class LocalDeployType:
"""LocalDeployType deals with local only deployments.
"""

async def resolve(self, url, architecture, app_name=None, channel=None, series=None, revision=None,
async def resolve(self, charm_path, architecture, app_name=None, channel=None, series=None, revision=None,
entity_url=None, force=False):
"""resolve attempts to resolve a local charm or bundle using the url
and architecture. If information is missing, it will attempt to backfill
that information, before sending the result back.
"""

entity_url = url.path()
entity_path = Path(entity_url)
entity_path = Path(charm_path)
bundle_path = entity_path / 'bundle.yaml'

identifier = entity_url
origin = client.CharmOrigin(source="local", architecture=architecture)
if not (entity_path.is_dir() or entity_path.is_file()):
raise JujuError('{} path not found'.format(entity_url))
Expand All @@ -474,21 +472,17 @@ async def resolve(self, url, architecture, app_name=None, channel=None, series=N
)

if app_name is None:
app_name = url.name

if not is_bundle:
entity_url = url.path()
entity_path = Path(entity_url)
if entity_path.suffix == '.charm':
with zipfile.ZipFile(str(entity_path), 'r') as charm_file:
metadata = yaml.load(charm_file.read('metadata.yaml'), Loader=yaml.FullLoader)
if is_bundle:
if entity_path.suffix == '.yaml' and entity_path.exists():
bundle = yaml.load(entity_path.read_text(), Loader=yaml.FullLoader)
else:
metadata_path = entity_path / 'metadata.yaml'
metadata = yaml.load(metadata_path.read_text(), Loader=yaml.FullLoader)
app_name = metadata['name']
bundle = yaml.load(bundle_path.read_text(), Loader=yaml.FullLoader)
app_name = bundle['name']
else:
app_name = utils.get_local_charm_metadata(entity_path)["name"]

return DeployTypeResult(
identifier=identifier,
identifier=charm_path,
origin=origin,
app_name=app_name,
is_local=True,
Expand Down Expand Up @@ -521,7 +515,7 @@ async def resolve(self, url, architecture, app_name=None, channel=None, series=N
include_stats=False)

identifier = result['Id']
is_bundle = url.series == "bundle" or url.parse(identifier).series == "bundle"
is_bundle = URL.parse(url).series == "bundle" or URL.parse(identifier).series == "bundle"
if not series:
series = "bundle" if is_bundle else self.get_series(entity_url, result)

Expand Down Expand Up @@ -578,7 +572,7 @@ async def resolve(self, url, architecture, app_name=None, channel=None, series=N
charm_url = URL.parse(charm_url_str)

if app_name is None:
app_name = url.name
app_name = charm_url.name

return DeployTypeResult(
identifier=str(charm_url),
Expand Down Expand Up @@ -629,9 +623,9 @@ def __init__(
self._charmstore = CharmStore()

self.deploy_types = {
"local": LocalDeployType(),
"cs": CharmStoreDeployType(self._charmstore, self._get_series),
"ch": CharmhubDeployType(self._resolve_charm),
Schema.LOCAL: LocalDeployType(),
Schema.CHARM_STORE: CharmStoreDeployType(self._charmstore, self._get_series),
Schema.CHARM_HUB: CharmhubDeployType(self._charmhub_resolve_charm),
}

def is_connected(self):
Expand Down Expand Up @@ -1603,7 +1597,7 @@ async def deploy(
storage=None, to=None, devices=None, trust=False):
"""Deploy a new service or bundle.
:param str entity_url: Charm or bundle url
:param str entity_url: Charm or bundle to deploy. Not necessarily a url
:param str application_name: Name to give the service
:param dict bind: <charm endpoint>:<network space> pairs
:param str channel: Charm store channel from which to retrieve
Expand Down Expand Up @@ -1646,23 +1640,30 @@ async def deploy(
raise NotImplementedError("trusted is not supported on model version {}".format(self.info.agent_version))

# Ensure what we pass in, is a string.
entity_url = str(entity_url)
if is_local_charm(entity_url) and not entity_url.startswith("local:"):
entity_url = "local:{}".format(entity_url)
entity = str(entity_url)
if is_local_charm(entity):
if entity.startswith("local:"):
entity = entity[6:]
architecture = await self._resolve_architecture()
schema = Schema.LOCAL

if client.CharmsFacade.best_facade_version(self.connection()) < 3:
url = URL.parse(str(entity_url), default_store=Schema.CHARM_STORE)
else:
url = URL.parse(str(entity_url))
if client.CharmsFacade.best_facade_version(self.connection()) < 3:
url = URL.parse(entity, default_store=Schema.CHARM_STORE)
else:
url = URL.parse(entity)
entity = str(url)

architecture = await self._resolve_architecture(url)
architecture = await self._resolve_architecture(url)
schema = url.schema
name = url.name

if str(url.schema) not in self.deploy_types:
raise JujuError("unknown deploy type {}, expected charmhub, charmstore or local".format(url.schema))
if schema not in self.deploy_types:
raise JujuError("unknown deploy type {}, expected charmhub, charmstore or local".format(schema))

res = await self.deploy_types[str(url.schema)].resolve(url, architecture,
application_name, channel, series,
revision, entity_url, force)
res = await self.deploy_types[schema].resolve(entity, architecture,
application_name, channel, series,
revision, entity_url, force)

if res.identifier is None:
raise JujuError('unknown charm or bundle {}'.format(entity_url))
Expand All @@ -1672,7 +1673,7 @@ async def deploy(
charm_series = series
if res.is_bundle:
handler = BundleHandler(self, trusted=trust, forced=force)
await handler.fetch_plan(url, res.origin, overlays=overlays)
await handler.fetch_plan(entity, res.origin, overlays=overlays)
await handler.execute_plan()
extant_apps = {app for app in self.applications}
pending_apps = handler.applications - extant_apps
Expand Down Expand Up @@ -1700,11 +1701,11 @@ async def deploy(
else:
charm_origin = add_charm_res.charm_origin

if Schema.CHARM_HUB.matches(url.schema):
if Schema.CHARM_HUB.matches(schema):
resources = await self._add_charmhub_resources(res.app_name,
identifier,
add_charm_res.charm_origin)
charm_info = await self.charmhub.info(url.name)
charm_info = await self.charmhub.info(name)
is_subordinate = False
try:
is_subordinate = charm_info.result.charm.subordinate
Expand All @@ -1715,7 +1716,7 @@ async def deploy(
raise JujuError("cannot use num_units with subordinate application")
num_units = 0

if Schema.CHARM_STORE.matches(url.schema):
if Schema.CHARM_STORE.matches(schema):
resources = await self._add_store_resources(res.app_name,
identifier)
else:
Expand Down Expand Up @@ -1777,23 +1778,18 @@ async def _add_charm(self, charm_url, origin):
client_facade = client.ClientFacade.from_connection(self.connection())
return await client_facade.AddCharm(channel=str(origin.risk), url=charm_url, force=False)

async def _resolve_charm(self, url, origin, force=False):
async def _charmhub_resolve_charm(self, url, origin, force=False):
charms_cls = client.CharmsFacade
if charms_cls.best_facade_version(self.connection()) < 3:
raise JujuError("resolve charm")

charms_facade = charms_cls.from_connection(self.connection())

if Schema.CHARM_STORE.matches(url.schema):
source = "charm-store"
else:
source = "charm-hub"

resp = await charms_facade.ResolveCharms(resolve=[{
'reference': str(url),
'charm-origin': {
'series': origin.series,
'source': source,
'source': "charm-hub",
'architecture': origin.architecture,
'track': origin.track,
'risk': origin.risk,
Expand All @@ -1820,8 +1816,8 @@ async def _resolve_charm(self, url, origin, force=False):

return result.url, result.charm_origin

async def _resolve_architecture(self, url):
if url.architecture:
async def _resolve_architecture(self, url=None):
if url is not None and url.architecture:
return url.architecture

constraints = await self.get_constraints()
Expand Down
20 changes: 14 additions & 6 deletions juju/url.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,11 @@ def parse(s, default_store=Schema.CHARM_HUB):
if u.query != "" or u.fragment != "" or u.username or u.password:
raise JujuError("charm or bundle URL {} has unrecognized parts".format(u))

if Schema.LOCAL.matches(u.scheme):
c = URL(Schema.LOCAL, name=u.path)
elif Schema.CHARM_STORE.matches(u.scheme) or \
if Schema.CHARM_STORE.matches(u.scheme) or \
(u.scheme == "" and Schema.CHARM_STORE.matches(default_store)):
c = parse_v1_url(Schema.CHARM_STORE, u, s)
else:
c = parse_v2_url(u, s)
c = parse_v2_url(u, s, default_store)

if not c or not c.schema:
raise JujuError("expected schema for charm or bundle URL {}".format(u))
Expand Down Expand Up @@ -121,8 +119,18 @@ def parse_v1_url(schema, u, s):
return c


def parse_v2_url(u, s):
c = URL(Schema.CHARM_HUB)
def parse_v2_url(u, s, default_store):
if not u.scheme:
c = URL(default_store)
elif Schema.CHARM_HUB.matches(u.scheme):
c = URL(Schema.CHARM_HUB)
elif Schema.LOCAL.matches(u.scheme):
c = URL(Schema.LOCAL)
else:
raise JujuError("invalid charm url schema {}".format(u.scheme))

# local:bionic/ubu-12
# local:/path/to/cha/ubunt

parts = u.path.split("/")
num = len(parts)
Expand Down
4 changes: 4 additions & 0 deletions tests/unit/test_url.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,7 @@ def test_parse_v2_revision(self):
def test_parse_v2_large_revision(self):
u = URL.parse("ch:mysql-12345")
self.assertEqual(u, URL(Schema.CHARM_HUB, name="mysql", revision=12345))

def test_parse_v2_without_store(self):
u = URL.parse("mysql-1", default_store=Schema.CHARM_HUB)
self.assertEqual(u, URL(Schema.CHARM_HUB, name="mysql", revision=1))

0 comments on commit afeb4e7

Please sign in to comment.