Skip to content

Commit 931db8f

Browse files
authored
Merge pull request #507 from DomFleischmann/local-upgrades
#507 This commit will add support for upgrading from a locally built charm with local resources.
2 parents 81ea905 + 81bb081 commit 931db8f

7 files changed

Lines changed: 147 additions & 8 deletions

File tree

examples/local_refresh.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""
2+
This example:
3+
4+
1. Connects to the current model
5+
2. Upgrades previously deployed ubuntu charm
6+
7+
"""
8+
from juju import loop
9+
from juju.model import Model
10+
11+
12+
async def main():
13+
model = Model()
14+
print('Connecting to model')
15+
# connect to current model with current user, per Juju CLI
16+
await model.connect()
17+
18+
try:
19+
print('Get deployed application')
20+
app = model.appplications["ubuntu"]
21+
22+
print('Refresh/Upgrade Ubuntu charm with local charm')
23+
await app.refresh(path="path/to/local/ubuntu.charm")
24+
finally:
25+
print('Disconnecting from model')
26+
await model.disconnect()
27+
28+
29+
if __name__ == '__main__':
30+
loop.run(main())

juju/application.py

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,16 @@
1515
import asyncio
1616
import json
1717
import logging
18+
import os
1819

1920
from . import model, tag
2021
from .status import derive_status
2122
from .annotationhelper import _get_annotations, _set_annotations
2223
from .client import client
2324
from .errors import JujuError
25+
from .bundle import get_charm_series
2426
from .placement import parse as parse_placement
27+
from .charm import get_local_charm_metadata
2528

2629
log = logging.getLogger(__name__)
2730

@@ -578,9 +581,10 @@ async def refresh(
578581
:param str switch: Crossgrade charm url
579582
580583
"""
581-
# TODO: Support local upgrades
582584
if path is not None:
583-
raise NotImplementedError("path option is not implemented")
585+
await self.local_refresh(channel, force, force_series, force_units,
586+
path, resources)
587+
return
584588
if resources is not None:
585589
raise NotImplementedError("resources option is not implemented")
586590

@@ -684,6 +688,61 @@ async def refresh(
684688

685689
upgrade_charm = refresh
686690

691+
async def local_refresh(
692+
self, channel=None, force=False, force_series=False, force_units=False,
693+
path=None, resources=None):
694+
"""Refresh the charm for this application with a local charm.
695+
696+
:param str channel: Channel to use when getting the charm from the
697+
charm store, e.g. 'development'
698+
:param bool force_series: Refresh even if series of deployed
699+
application is not supported by the new charm
700+
:param bool force_units: Refresh all units immediately, even if in
701+
error state
702+
:param str path: Refresh to a charm located at path
703+
:param dict resources: Dictionary of resource name/filepath pairs
704+
:param int revision: Explicit refresh revision
705+
:param str switch: Crossgrade charm url
706+
707+
"""
708+
app_facade = self._facade()
709+
710+
charm_dir = os.path.abspath(
711+
os.path.expanduser(path))
712+
model_config = await self.get_config()
713+
714+
series = get_charm_series(charm_dir)
715+
if not series:
716+
model_config = await self.get_config()
717+
default_series = model_config.get("default-series")
718+
if default_series:
719+
series = default_series.value
720+
charm_url = await self.model.add_local_charm_dir(charm_dir, series)
721+
metadata = get_local_charm_metadata(path)
722+
if resources is not None:
723+
resources = await self.model.add_local_resources(self.entity_id,
724+
charm_url,
725+
metadata,
726+
resources=resources)
727+
728+
# Update application
729+
await app_facade.SetCharm(
730+
application=self.entity_id,
731+
channel=channel,
732+
charm_url=charm_url,
733+
config_settings=None,
734+
config_settings_yaml=None,
735+
force=force,
736+
force_series=force_series,
737+
force_units=force_units,
738+
resource_ids=resources,
739+
storage_constraints=None,
740+
)
741+
742+
await self.model.block_until(
743+
lambda: self.data['charm-url'] == charm_url
744+
)
745+
687746
async def get_metrics(self):
688747
"""Get metrics for this application's units.
689748

juju/charm.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,31 @@
1313
# limitations under the License.
1414

1515
import logging
16-
16+
import zipfile
17+
import yaml
18+
from pathlib import Path
1719
from . import model
1820

1921
log = logging.getLogger(__name__)
2022

2123

24+
def get_local_charm_metadata(path):
25+
"""Retrieve Metadata of a Charm from its path
26+
27+
:patam str path: Path of charm directory or .charm file
28+
29+
:return: Object of charm metadata
30+
"""
31+
if str(path).endswith('.charm'):
32+
with zipfile.ZipFile(path, 'r') as charm_file:
33+
metadata = yaml.load(charm_file.read('metadata.yaml'), Loader=yaml.FullLoader)
34+
else:
35+
entity_path = Path(path)
36+
metadata_path = entity_path / 'metadata.yaml'
37+
metadata = yaml.load(metadata_path.read_text(), Loader=yaml.FullLoader)
38+
39+
return metadata
40+
41+
2242
class Charm(model.ModelEntity):
2343
pass

juju/model.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1492,10 +1492,10 @@ async def deploy(
14921492
"Pass a 'series' kwarg to Model.deploy().".format(
14931493
charm_dir))
14941494
entity_id = await self.add_local_charm_dir(charm_dir, series)
1495-
resources = await self._add_local_resources(application_name,
1496-
entity_id,
1497-
metadata,
1498-
resources=resources)
1495+
resources = await self.add_local_resources(application_name,
1496+
entity_id,
1497+
metadata,
1498+
resources=resources)
14991499

15001500
if config is None:
15011501
config = {}
@@ -1561,7 +1561,7 @@ async def _add_store_resources(self, application, entity_url,
15611561
in zip(resources, response.pending_ids)}
15621562
return resource_map
15631563

1564-
async def _add_local_resources(self, application, entity_url, metadata, resources):
1564+
async def add_local_resources(self, application, entity_url, metadata, resources):
15651565
if not resources:
15661566
return None
15671567

tests/integration/test_application.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import asyncio
2+
from pathlib import Path
23

34
import pytest
45

@@ -143,6 +144,20 @@ async def test_upgrade_charm_switch(event_loop):
143144
assert app.data['charm-url'] == 'cs:ubuntu-8'
144145

145146

147+
@base.bootstrapped
148+
@pytest.mark.asyncio
149+
async def test_upgrade_local_charm(event_loop):
150+
async with base.CleanModel() as model:
151+
tests_dir = Path(__file__).absolute().parent
152+
charm_path = tests_dir / 'upgrade-charm'
153+
app = await model.deploy('ubuntu', series='focal')
154+
await model.wait_for_idle(wait_for_active=True)
155+
assert app.data['charm-url'].startswith('cs:ubuntu')
156+
await app.upgrade_charm(path=charm_path)
157+
await model.wait_for_idle() # nb: can't use wait_for_active because test charm goes to "waiting"
158+
assert app.data['charm-url'] == 'local:focal/ubuntu-0'
159+
160+
146161
@base.bootstrapped
147162
@pytest.mark.asyncio
148163
async def test_upgrade_charm_resource(event_loop):
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#!/bin/bash
2+
3+
resource_file="$(resource-get oci-image)"
4+
if [[ -z "$resource_file" ]]; then
5+
status-set waiting
6+
elif grep -q "ubuntu/latest" "$resource_file"; then
7+
status-set active
8+
else
9+
status-set error
10+
fi
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
name: ubuntu
2+
series: ["focal"]
3+
summary: "test"
4+
description: "test"
5+
maintainers: ["test"]

0 commit comments

Comments
 (0)