Skip to content

Commit c3e4a84

Browse files
Allow charmhub bundles to be deployed
1 parent aad7761 commit c3e4a84

4 files changed

Lines changed: 113 additions & 11 deletions

File tree

juju/application.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,14 @@ async def run(self, command, timeout=None):
481481
units=[],
482482
)
483483

484+
@property
485+
def charm_url(self):
486+
"""Get the charm url for a given application
487+
488+
:return string: The charm url for an application
489+
"""
490+
return self.safe_data['charm-url']
491+
484492
async def get_annotations(self):
485493
"""Get annotations on this application.
486494

juju/bundle.py

Lines changed: 97 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from .client import client
1515
from .constraints import parse as parse_constraints, parse_storage_constraint, parse_device_constraint
1616
from .errors import JujuError
17-
from .origin import Channel, Risk
17+
from .origin import Channel
1818
from .url import Schema, URL
1919

2020
log = 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+
241313
def 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)

juju/model.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1573,11 +1573,17 @@ async def _resolve_charm(self, url, origin):
15731573

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

1576+
if Schema.CHARM_STORE.matches(url.schema):
1577+
source = "charm-store"
1578+
else:
1579+
source = "charm-hub"
15761580
resp = await charms_facade.ResolveCharms(resolve=[{
15771581
'reference': str(url),
15781582
'charm-origin': {
1579-
'source': 'charm-hub',
1583+
'source': source,
15801584
'architecture': origin.architecture,
1585+
'track': origin.track,
1586+
'risk': origin.risk,
15811587
}
15821588
}])
15831589
if len(resp.results) != 1:

tests/unit/test_bundle.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,7 @@ async def test_run(self, event_loop):
411411
model.add_relation = base.AsyncMock(return_value="relation1")
412412

413413
context = mock.Mock()
414-
context.resolveRelation = mock.Mock(side_effect=['endpoint_1', 'endpoint_2'])
414+
context.resolve_relation = mock.Mock(side_effect=['endpoint_1', 'endpoint_2'])
415415
context.model = model
416416

417417
result = await change.run(context)

0 commit comments

Comments
 (0)