Skip to content

Commit 35ef84f

Browse files
authored
Merge pull request #749 from cderici/fix-series-for-local-charms-and-bundles-3.0
#749 #### Description This change fixes the use of `base` in `CharmOrigin` in basically all code paths for local and charmhub deployments for both charms and bundles. Initially started to fix the charm deploy issues such as in [parca-k8s](https://github.com/jnsgruk/parca-k8s-operator/actions/runs/3277144022/jobs/5394048300#step:4:1), this grew out of it pretty quickly, as I started to run tests against the 3.0 controller with the new facades/clients. This should also fix the [LP #1992833](https://bugs.launchpad.net/juju/+bug/1992833), though we need a confirmation on that. #### QA Steps To qa this we need a `3.0` controller. So for the local charm deployment issue, run `juju download parca-k8s --channel=edge` and then run: ```python application = await model.deploy( './parca.charm', ) ``` with libjuju and it should work. For the bundle parts, I manually made sure that all of the following integration tests are working against the latest juju `3.0`. Note that in the CI below we won't see any of this because juju latest/stable is still 2.9 (i.e. almost all our tests are running against 2.9) and our latest-edge jenkins job (against 3.0) is still not working. Here are the individual tests that need to pass (they were all failing without this change): ``` tox -e integration -- tests/integration/test_model.py::test_deploy_local_charm tox -e integration -- tests/integration/test_model.py::test_deploy_bundle tox -e integration -- tests/integration/test_model.py::test_deploy_local_bundle_file tox -e integration -- tests/integration/test_model.py::test_deploy_local_bundle_include_file tox -e integration -- tests/integration/test_model.py::test_deploy_bundle_local_charms tox -e integration -- tests/integration/test_model.py::test_deploy_bundle_with_multiple_overlays_with_include_files tox -e integration -- tests/integration/test_model.py::test_deploy_local_bundle_include_base64 tox -e integration -- tests/integration/test_model.py::test_deploy_trusted_bundle tox -e integration -- tests/integration/test_model.py::test_deploy_local_bundle_with_overlay_multi tox -e integration -- tests/integration/test_model.py::test_deploy_bundle_local_resource_relative_path tox -e integration -- tests/integration/test_model.py::test_deploy_local_bundle_dir tox -e integration -- tests/integration/test_model.py::test_deploy_bundle_with_multi_overlay_as_argument ``` #### Notes & Discussion Note that I rely on the CI tests to make sure we didn't regress on the 2.9 support. So we should be careful in looking at the CI to make sure none of the related tests are failing. Failure means that a change I made to support 3.0 broke 2.9 support. In that case we gotta fix it before we land this.
2 parents 4c43628 + 7e29286 commit 35ef84f

5 files changed

Lines changed: 227 additions & 58 deletions

File tree

juju/bundle.py

Lines changed: 48 additions & 23 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 . import utils, jasyncio, charmhub
17+
from . import utils, jasyncio
1818
from .origin import Channel
1919
from .url import Schema, URL
2020

@@ -129,7 +129,7 @@ async def _handle_local_charms(self, bundle, bundle_dir):
129129
default_series or
130130
await get_charm_series(charm_dir, self.model)
131131
)
132-
if not series:
132+
if not self.model.connection().is_using_old_client and not series:
133133
raise JujuError(
134134
"Couldn't determine series for charm at {}. "
135135
"Add a 'series' key to the bundle.".format(charm_dir))
@@ -148,15 +148,23 @@ async def _handle_local_charms(self, bundle, bundle_dir):
148148
])
149149

150150
# Update the 'charm:' entry for each app with the new 'local:' url.
151-
for app_name, charm_url, (charm_dir, _) in zip(apps, charm_urls, args):
151+
for app_name, charm_url, (charm_dir, series) in zip(apps, charm_urls, args):
152+
metadata = utils.get_local_charm_metadata(charm_dir)
152153
resources = await self.model.add_local_resources(
153154
app_name,
154155
charm_url,
155-
utils.get_local_charm_metadata(charm_dir),
156+
metadata,
156157
resources=bundle.get('applications', {app_name: {}})[app_name].get("resources", {}),
157158
)
158159
apps_dict[app_name]['charm'] = charm_url
159160
apps_dict[app_name]["resources"] = resources
161+
origin = client.CharmOrigin(source="local", risk="stable")
162+
if not self.model.connection().is_using_old_client:
163+
origin.base = utils.get_local_charm_base(series, '',
164+
metadata,
165+
charm_dir,
166+
client.Base)
167+
self.origins[charm_url] = {str(None): origin}
160168

161169
return bundle
162170

@@ -281,20 +289,27 @@ async def _download_bundle(self, charm_url, origin):
281289
if self.charms_facade is None:
282290
raise JujuError('unable to download bundle for {} using the new charms facade. Upgrade controller to proceed.'.format(charm_url))
283291

292+
id = origin.id_ if origin.id_ else ''
293+
hash = origin.hash_ if origin.hash_ else ''
294+
charm_origin = {
295+
'source': origin.source,
296+
'type': origin.type_,
297+
'id': id,
298+
'hash': hash,
299+
'revision': origin.revision,
300+
'risk': origin.risk,
301+
'track': origin.track,
302+
'architecture': origin.architecture,
303+
}
304+
if self.model.connection().is_using_old_client:
305+
charm_origin['os'] = origin.os
306+
charm_origin['series'] = origin.series
307+
else:
308+
charm_origin['base'] = origin.base
309+
284310
resp = await self.charms_facade.GetDownloadInfos(entities=[{
285311
'charm-url': str(charm_url),
286-
'charm-origin': {
287-
'source': origin.source,
288-
'type': origin.type_,
289-
'id': origin.id_,
290-
'hash': origin.hash_,
291-
'revision': origin.revision,
292-
'risk': origin.risk,
293-
'track': origin.track,
294-
'architecture': origin.architecture,
295-
'os': origin.os,
296-
'series': origin.series,
297-
}
312+
'charm-origin': charm_origin
298313
}])
299314
if len(resp.results) != 1:
300315
raise JujuError("expected one result, received {}".format(resp.results))
@@ -341,10 +356,14 @@ async def _resolve_charms(self):
341356

342357
charm_url = URL.parse(spec['charm'])
343358
channel = None
359+
series = spec.get('series', None)
344360
track, risk = '', ''
345361
if 'channel' in spec:
346362
channel = Channel.parse(spec['channel'])
347363
track, risk = channel.track, channel.risk
364+
365+
# if not track and series:
366+
# track = utils.get_series_version(series)
348367
if self.charms_facade is not None:
349368
if cons is not None and cons['arch'] != '':
350369
architecture = cons['arch']
@@ -355,8 +374,10 @@ async def _resolve_charms(self):
355374
architecture=architecture,
356375
risk=risk,
357376
track=track)
377+
if not self.model.connection().is_using_old_client and series:
378+
origin.base = client.Base(
379+
channel=utils.get_series_version(series), name='ubuntu')
358380
charm_url, charm_origin, _ = await self.model._resolve_charm(charm_url, origin)
359-
360381
spec['charm'] = str(charm_url)
361382
else:
362383
results = await self.model.charmstore.entity(str(charm_url))
@@ -618,9 +639,6 @@ async def run(self, context):
618639
resources = await context.model._add_store_resources(
619640
self.application, charm, overrides=self.resources)
620641
elif Schema.CHARM_HUB.matches(url.schema):
621-
c_hub = charmhub.CharmHub(context.model)
622-
id_, _ = await c_hub.get_charm_id(url.name)
623-
origin.id_ = id_
624642
resources = await context.model._add_charmhub_resources(
625643
self.application, charm, origin, overrides=self.resources)
626644
else:
@@ -719,8 +737,6 @@ async def run(self, context):
719737
ch = None
720738
identifier = None
721739
if Schema.LOCAL.matches(url.schema):
722-
origin = client.CharmOrigin(source="local", risk="stable")
723-
context.origins[self.charm] = {str(None): origin}
724740
return self.charm
725741

726742
if Schema.CHARM_STORE.matches(url.schema):
@@ -741,6 +757,10 @@ async def run(self, context):
741757
architecture=arch,
742758
risk=ch.risk,
743759
track=ch.track)
760+
if not context.model.connection().is_using_old_client and self.series:
761+
origin.base = client.Base(
762+
channel=utils.get_series_version(self.series),
763+
name='ubuntu')
744764
identifier, origin, _ = await context.model._resolve_charm(url, origin)
745765

746766
if identifier is None:
@@ -826,7 +846,12 @@ async def run(self, context):
826846

827847
params['constraints'] = parse_constraints(self.constraints)
828848
params['jobs'] = params.get('jobs', ['JobHostUnits'])
829-
params['series'] = self.series
849+
if not context.model.connection().is_using_old_client:
850+
params['base'] = client.Base(
851+
channel=utils.get_series_version(self.series),
852+
name='ubuntu')
853+
else:
854+
params['series'] = self.series
830855

831856
if self.container_type == 'lxc':
832857
log.warning('Juju 2.0 does not support lxc containers. '

juju/model.py

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -824,7 +824,7 @@ async def add_local_charm_dir(self, charm_dir, series):
824824
log.debug('Uploaded local charm: %s -> %s', charm_dir, charm_url)
825825
return charm_url
826826

827-
def add_local_charm(self, charm_file, series, size=None):
827+
def add_local_charm(self, charm_file, series="", size=None):
828828
"""Upload a local charm archive to the model.
829829
830830
Returns the 'local:...' url that should be used to deploy the charm.
@@ -1770,7 +1770,6 @@ async def deploy(
17701770
charm_origin = add_charm_res.get('charm_origin', res.origin)
17711771
else:
17721772
charm_origin = add_charm_res.charm_origin
1773-
17741773
if Schema.CHARM_HUB.matches(url.schema):
17751774
resources = await self._add_charmhub_resources(res.app_name,
17761775
identifier,
@@ -1789,16 +1788,24 @@ async def deploy(
17891788
charm_dir = os.path.abspath(
17901789
os.path.expanduser(identifier))
17911790
charm_origin = res.origin
1791+
17921792
metadata = utils.get_local_charm_metadata(charm_dir)
1793+
# TODO (cderici) : pass the metadata into get_charm_series, as
1794+
# it also reads that file redundantly
1795+
series = series or await get_charm_series(charm_dir, self)
1796+
1797+
# If we're using a newer client, then the CharmOrigin needs a
1798+
# base
1799+
if not self.connection().is_using_old_client:
1800+
charm_origin.base = utils.get_local_charm_base(series,
1801+
channel,
1802+
metadata,
1803+
charm_dir,
1804+
client.Base)
1805+
17931806
if not application_name:
17941807
application_name = metadata['name']
1795-
series = series or await get_charm_series(charm_dir, self)
1796-
if not series:
1797-
model_config = await self.get_config()
1798-
default_series = model_config.get("default-series")
1799-
if default_series:
1800-
series = default_series.value
1801-
if not series:
1808+
if self.connection().is_using_old_client and not series:
18021809
raise JujuError(
18031810
"Couldn't determine series for charm at {}. "
18041811
"Pass a 'series' kwarg to Model.deploy().".format(
@@ -1853,14 +1860,19 @@ async def _resolve_charm(self, url, origin):
18531860
source = "charm-store"
18541861
else:
18551862
source = "charm-hub"
1863+
1864+
resolve_origin = {
1865+
'source': source,
1866+
'architecture': origin.architecture,
1867+
'track': origin.track,
1868+
'risk': origin.risk,
1869+
}
1870+
1871+
if not self.connection().is_using_old_client:
1872+
resolve_origin['base'] = origin.base
18561873
resp = await charms_facade.ResolveCharms(resolve=[{
18571874
'reference': str(url),
1858-
'charm-origin': {
1859-
'source': source,
1860-
'architecture': origin.architecture,
1861-
'track': origin.track,
1862-
'risk': origin.risk,
1863-
}
1875+
'charm-origin': resolve_origin
18641876
}])
18651877
if len(resp.results) != 1:
18661878
raise JujuError("expected one result, received {}".format(resp.results))

juju/utils.py

Lines changed: 136 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import yaml
1010
import zipfile
1111

12-
from . import jasyncio
12+
from . import jasyncio, origin, errors
1313

1414

1515
async def execute_process(*cmd, log=None):
@@ -234,19 +234,151 @@ def generate_user_controller_access_token(username, controller_endpoints, secret
234234
return base64.urlsafe_b64encode(registration_string)
235235

236236

237-
def get_local_charm_metadata(path):
237+
def get_local_charm_data(path, yaml_file):
238238
"""Retrieve Metadata of a Charm from its path
239239
240240
:patam str path: Path of charm directory or .charm file
241+
:patam str yaml_file: name of the yaml file, can be either
242+
"metadata.yaml", or "manifest.yaml"
241243
242244
:return: Object of charm metadata
243245
"""
244246
if str(path).endswith('.charm'):
245247
with zipfile.ZipFile(str(path), 'r') as charm_file:
246-
metadata = yaml.load(charm_file.read('metadata.yaml'), Loader=yaml.FullLoader)
248+
metadata = yaml.load(charm_file.read(yaml_file), Loader=yaml.FullLoader)
247249
else:
248250
entity_path = Path(path)
249-
metadata_path = entity_path / 'metadata.yaml'
251+
metadata_path = entity_path / yaml_file
250252
metadata = yaml.load(metadata_path.read_text(), Loader=yaml.FullLoader)
251253

252254
return metadata
255+
256+
257+
def get_local_charm_metadata(path):
258+
return get_local_charm_data(path, 'metadata.yaml')
259+
260+
261+
def get_local_charm_manifest(path):
262+
return get_local_charm_data(path, 'manifest.yaml')
263+
264+
265+
PRECISE = "precise"
266+
QUANTAL = "quantal"
267+
RARING = "raring"
268+
SAUCY = "saucy"
269+
TRUSTY = "trusty"
270+
UTOPIC = "utopic"
271+
VIVID = "vivid"
272+
WILY = "wily"
273+
XENIAL = "xenial"
274+
YAKKETY = "yakkety"
275+
ZESTY = "zesty"
276+
ARTFUL = "artful"
277+
BIONIC = "bionic"
278+
COSMIC = "cosmic"
279+
DISCO = "disco"
280+
EOAN = "eoan"
281+
FOCAL = "focal"
282+
GROOVY = "groovy"
283+
HIRSUTE = "hirsute"
284+
IMPISH = "impish"
285+
JAMMY = "jammy"
286+
KINETIC = "kinetic"
287+
288+
UBUNTU_SERIES = {
289+
PRECISE: "12.04",
290+
QUANTAL: "12.10",
291+
RARING: "13.04",
292+
SAUCY: "13.10",
293+
TRUSTY: "14.04",
294+
UTOPIC: "14.10",
295+
VIVID: "15.04",
296+
WILY: "15.10",
297+
XENIAL: "16.04",
298+
YAKKETY: "16.10",
299+
ZESTY: "17.04",
300+
ARTFUL: "17.10",
301+
BIONIC: "18.04",
302+
COSMIC: "18.10",
303+
DISCO: "19.04",
304+
EOAN: "19.10",
305+
FOCAL: "20.04",
306+
GROOVY: "20.10",
307+
HIRSUTE: "21.04",
308+
IMPISH: "21.10",
309+
JAMMY: "22.04",
310+
KINETIC: "22.10",
311+
}
312+
313+
314+
def get_series_version(series_name):
315+
if series_name not in UBUNTU_SERIES:
316+
raise errors.JujuError("Unknown series : %s", series_name)
317+
return UBUNTU_SERIES[series_name]
318+
319+
320+
def get_version_series(version):
321+
if version not in UBUNTU_SERIES.values():
322+
raise errors.JujuError("Unknown version : %s", version)
323+
return list(UBUNTU_SERIES.keys())[list(UBUNTU_SERIES.values()).index(version)]
324+
325+
326+
def get_local_charm_base(series, channel_from_arg, charm_metadata,
327+
charm_path, baseCls):
328+
"""Deduce the base [channel/osname] of a local charm based on what we
329+
know already
330+
331+
:param str series: This may come from the argument or the metadata.yaml
332+
:param str channel_from_arg: This is channel passed as argument, if any.
333+
:param dict charm_metadata: metadata.yaml
334+
:param str charm_path: Path of charm directory/.charm file
335+
:param class baseCls:
336+
:return: Instance of the baseCls with channel/osname informaiton
337+
"""
338+
339+
channel_for_base = ''
340+
os_name_for_base = ''
341+
# If user passed a channel_arg, then check the supported series against
342+
# the channel's track
343+
chnl_check = origin.Channel.parse(channel_from_arg) if channel_from_arg \
344+
else None
345+
if chnl_check:
346+
not_supported_error = errors.JujuError(
347+
"Given channel [track/risk] is not supported --"
348+
"\n - Given channel : %s"
349+
"\n - Series in Charm Metadata : %s" %
350+
(channel_from_arg, charm_metadata['series']))
351+
channel_for_base = chnl_check.track
352+
intented_series = get_version_series(channel_for_base)
353+
if intented_series not in charm_metadata['series']:
354+
raise not_supported_error
355+
# Also check the manifest if there's one
356+
charm_manifest = get_local_charm_manifest(charm_path)
357+
if 'bases' in charm_manifest:
358+
for base in charm_manifest['bases']:
359+
if channel_for_base == base['channel']:
360+
break
361+
else:
362+
raise not_supported_error
363+
364+
# If we know the series, use it to get a channel
365+
if channel_for_base == '':
366+
channel_for_base = get_series_version(series) if series else ''
367+
if channel_for_base:
368+
# we currently only support ubuntu series (statically)
369+
# TODO (cderici) : go juju/core/series/supported.go and get the
370+
# others here too
371+
os_name_for_base = 'ubuntu'
372+
373+
# Check the charm manifest
374+
if channel_for_base == '':
375+
charm_manifest = get_local_charm_manifest(charm_path)
376+
if 'bases' in charm_manifest:
377+
channel_for_base = charm_manifest['bases'][0]['channel']
378+
os_name_for_base = charm_manifest['bases'][0]['name']
379+
380+
if channel_for_base == '':
381+
raise errors.JujuError("Unable to determine base for charm : %s" %
382+
charm_path)
383+
384+
return baseCls(channel_for_base, os_name_for_base)

0 commit comments

Comments
 (0)