Skip to content

Commit c9540e6

Browse files
authored
Merge pull request #516 from tlm/resources-ch-2.9
#516 Adds support to pylib Juju for deploying charmhub charms with associated resources.
2 parents ef05ad8 + 20384dc commit c9540e6

5 files changed

Lines changed: 132 additions & 7 deletions

File tree

examples/charmhub_deploy_k8s.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""
2+
This example:
3+
4+
1. Connects to the current model
5+
2. Deploy a charm and waits until it reports itself active
6+
3. Destroys the unit and application
7+
8+
"""
9+
from juju import loop
10+
from juju.model import Model
11+
12+
13+
async def main():
14+
model = Model()
15+
print('Connecting to model')
16+
await model.connect()
17+
18+
try:
19+
print('Deploying ')
20+
application = await model.deploy(
21+
'ch:juju-qa-test',
22+
application_name='juju-qa-test2',
23+
channel='2.0/edge',
24+
)
25+
26+
print('Waiting for active')
27+
await model.block_until(
28+
lambda: all(unit.workload_status == 'active'
29+
for unit in application.units))
30+
31+
finally:
32+
print('Disconnecting from model')
33+
await model.disconnect()
34+
35+
if __name__ == '__main__':
36+
loop.run(main())
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""
2+
This example:
3+
4+
1. Connects to the current model
5+
2. Deploy a charm and waits until it reports itself active
6+
3. Destroys the unit and application
7+
8+
"""
9+
from juju import loop
10+
from juju.model import Model
11+
12+
13+
async def main():
14+
model = Model()
15+
print('Connecting to model')
16+
await model.connect()
17+
18+
try:
19+
print('Deploying ')
20+
application = await model.deploy(
21+
'ch:juju-qa-test',
22+
application_name='juju-qa-test',
23+
channel='2.0/edge',
24+
)
25+
26+
print('Waiting for active')
27+
await model.block_until(
28+
lambda: all(unit.workload_status == 'active'
29+
for unit in application.units))
30+
31+
finally:
32+
print('Disconnecting from model')
33+
await model.disconnect()
34+
35+
if __name__ == '__main__':
36+
loop.run(main())

juju/client/overrides.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,11 @@ class ResourcesFacade(Type):
6363
"""
6464

6565
@ReturnMapping(_client.AddPendingResourcesResult)
66-
async def AddPendingResources(self, application_tag="", charm_url="", resources=None):
66+
async def AddPendingResources(self,
67+
application_tag="",
68+
charm_url="",
69+
charm_origin=None,
70+
resources=None):
6771
"""Fix the calling signature of AddPendingResources.
6872
6973
The ResourcesFacade doesn't conform to the standard facade pattern in
@@ -81,11 +85,12 @@ async def AddPendingResources(self, application_tag="", charm_url="", resources=
8185
_params = dict()
8286
msg = dict(type='Resources',
8387
request='AddPendingResources',
84-
version=1,
88+
version=2,
8589
params=_params)
8690
_params['tag'] = application_tag
8791
_params['url'] = charm_url
8892
_params['resources'] = resources
93+
_params['charm-origin'] = charm_origin
8994
reply = await self.rpc(msg)
9095
return reply
9196

juju/model.py

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1595,10 +1595,13 @@ async def deploy(
15951595
# XXX: we're dropping local resources here, but we don't
15961596
# actually support them yet anyway
15971597
if not res.is_local:
1598-
await self._add_charm(identifier, res.origin)
1598+
add_charm_res = await self._add_charm(identifier, res.origin)
1599+
1600+
if Schema.CHARM_HUB.matches(url.schema):
1601+
resources = await self._add_charmhub_resources(res.app_name,
1602+
identifier,
1603+
add_charm_res.charm_origin)
15991604

1600-
# TODO (stickupkid): Handle charmhub charms, for now we'll only
1601-
# handle charmstore charms.
16021605
if Schema.CHARM_STORE.matches(url.schema):
16031606
resources = await self._add_store_resources(res.app_name,
16041607
identifier)
@@ -1632,6 +1635,7 @@ async def deploy(
16321635
num_units=num_units,
16331636
placement=parse_placement(to),
16341637
devices=devices,
1638+
charm_origin=add_charm_res.charm_origin,
16351639
)
16361640

16371641
async def _add_charm(self, charm_url, origin):
@@ -1684,6 +1688,49 @@ async def _resolve_architecture(self, url):
16841688

16851689
return DEFAULT_ARCHITECTURE
16861690

1691+
async def _add_charmhub_resources(self, application,
1692+
entity_url,
1693+
origin,
1694+
overrides=None):
1695+
charm_facade = client.CharmsFacade.from_connection(self.connection())
1696+
res = await charm_facade.CharmInfo(entity_url)
1697+
1698+
resources = []
1699+
for resource in res.meta.resources.values():
1700+
resources.append({
1701+
'description': resource.description,
1702+
'name': resource.name,
1703+
'path': resource.path,
1704+
'type_': resource.type_,
1705+
'origin': 'store',
1706+
'revision': -1,
1707+
})
1708+
1709+
if overrides:
1710+
names = {r['name'] for r in resources}
1711+
unknown = overrides.keys() - names
1712+
if unknown:
1713+
raise JujuError('Unrecognized resource{}: {}'.format(
1714+
's' if len(unknown) > 1 else '',
1715+
', '.join(unknown)))
1716+
for resource in resources:
1717+
if resource['name'] in overrides:
1718+
resource['revision'] = overrides[resource['name']]
1719+
1720+
resources_facade = client.ResourcesFacade.from_connection(
1721+
self.connection())
1722+
response = await resources_facade.AddPendingResources(
1723+
application_tag=tag.application(application),
1724+
charm_url=entity_url,
1725+
charm_origin=origin,
1726+
resources=[client.CharmResource(**resource) for resource in resources],
1727+
)
1728+
1729+
resource_map = {resource['name']: pid
1730+
for resource, pid
1731+
in zip(resources, response.pending_ids)}
1732+
return resource_map
1733+
16871734
async def _add_store_resources(self, application, entity_url,
16881735
overrides=None):
16891736
entity = await self.charmstore.entity(entity_url,
@@ -1729,7 +1776,7 @@ async def _add_store_resources(self, application, entity_url,
17291776
async def _deploy(self, charm_url, application, series, config,
17301777
constraints, endpoint_bindings, resources, storage,
17311778
channel=None, num_units=None, placement=None,
1732-
devices=None):
1779+
devices=None, charm_origin=None):
17331780
"""Logic shared between `Model.deploy` and `BundleHandler.deploy`.
17341781
"""
17351782
log.info('Deploying %s', charm_url)
@@ -1747,6 +1794,7 @@ async def _deploy(self, charm_url, application, series, config,
17471794
application=application,
17481795
series=series,
17491796
channel=channel,
1797+
charm_origin=charm_origin,
17501798
config_yaml=config,
17511799
constraints=parse_constraints(constraints),
17521800
endpoint_bindings=endpoint_bindings,

tests/integration/test_application.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ async def test_deploy_charmstore_charm(event_loop):
107107
@pytest.mark.asyncio
108108
async def test_deploy_charmhub_charm(event_loop):
109109
async with base.CleanModel() as model:
110-
app = await model.deploy('hello-juju')
110+
app = await model.deploy('ch:hello-juju')
111111
await model.block_until(lambda: (len(app.units) > 0 and
112112
app.units[0].machine))
113113
assert 'hello-juju' in app.data['charm-url']

0 commit comments

Comments
 (0)