Skip to content

Commit bd06a24

Browse files
[JUJU-2392] Cherrypick to fix wrong bases (#783)
* Cherrypicking to support bases.
1 parent 73a42dc commit bd06a24

4 files changed

Lines changed: 200 additions & 50 deletions

File tree

juju/bundle.py

Lines changed: 18 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -122,12 +122,13 @@ async def _handle_local_charms(self, bundle, bundle_dir):
122122
pass
123123
except FileNotFoundError:
124124
continue
125-
series = (
126-
app_dict.get('series') or
127-
default_series or
128-
await get_charm_series(charm_dir, self.model)
129-
)
125+
series = (app_dict.get('series') or default_series)
130126
if not series:
127+
metadata = utils.get_local_charm_metadata(charm_dir)
128+
series = await get_charm_series(metadata, self.model)
129+
if not series:
130+
metadata = utils.get_local_charm_metadata(charm_dir)
131+
series = await get_charm_series(metadata, self.model)
131132
raise JujuError(
132133
"Couldn't determine series for charm at {}. "
133134
"Add a 'series' key to the bundle.".format(charm_dir))
@@ -147,14 +148,18 @@ async def _handle_local_charms(self, bundle, bundle_dir):
147148

148149
# Update the 'charm:' entry for each app with the new 'local:' url.
149150
for app_name, charm_url, (charm_dir, _) in zip(apps, charm_urls, args):
151+
metadata = utils.get_local_charm_metadata(charm_dir)
150152
resources = await self.model.add_local_resources(
151153
app_name,
152154
charm_url,
153-
utils.get_local_charm_metadata(charm_dir),
155+
metadata,
154156
resources=bundle.get('applications', {app_name: {}})[app_name].get("resources", {}),
155157
)
156158
apps_dict[app_name]['charm'] = charm_url
157159
apps_dict[app_name]["resources"] = resources
160+
origin = client.CharmOrigin(source="local", risk="stable")
161+
162+
self.origins[charm_url] = {str(None): origin}
158163

159164
return bundle
160165

@@ -407,32 +412,17 @@ def is_local_charm(charm_url):
407412
return charm_url.startswith('.') or charm_url.startswith('local:') or os.path.isabs(charm_url)
408413

409414

410-
async def get_charm_series(path, model):
411-
"""Inspects the charm directory at ``path`` and returns a default
412-
series from its metadata.yaml (the first item in the 'series' list).
415+
async def get_charm_series(metadata, model):
416+
"""Inspects the given metadata and returns a default series from its
417+
metadata.yaml (the first item in the 'series' list).
413418
414-
Tries to extract the informiation from the given model if no
415-
series is determined from the path.
419+
Tries to extract the information from the given model if no
420+
series is determined from the given metadata.
416421
Returns None if no series can be determined.
417422
418423
"""
419-
path = Path(path)
420-
try:
421-
if path.suffix == '.charm':
422-
md = "metadata.yaml in %s" % path
423-
with zipfile.ZipFile(str(path), 'r') as charm_file:
424-
data = yaml.safe_load(charm_file.read('metadata.yaml'))
425-
else:
426-
md = path / "metadata.yaml"
427-
if not md.exists():
428-
return None
429-
data = yaml.safe_load(md.open())
430-
except yaml.YAMLError as exc:
431-
if hasattr(exc, "problem_mark"):
432-
mark = exc.problem_mark
433-
log.error("Error parsing YAML file {}, line {}, column: {}".format(md, mark.line, mark.column))
434-
raise
435-
_series = data.get('series')
424+
425+
_series = metadata.get('series')
436426
series = _series[0] if _series else None
437427

438428
if series is None:

juju/model.py

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1609,7 +1609,8 @@ async def deploy(
16091609
raise JujuError('unknown charm or bundle {}'.format(entity_url))
16101610
identifier = res.identifier
16111611

1612-
series = res.origin.series or series
1612+
# series = res.origin.series or series
1613+
charm_series = series
16131614
if res.is_bundle:
16141615
handler = BundleHandler(self, trusted=trust, forced=force)
16151616
await handler.fetch_plan(url, res.origin, overlays=overlays)
@@ -1660,24 +1661,27 @@ async def deploy(
16601661
identifier)
16611662
else:
16621663
# We have a local charm dir that needs to be uploaded
1663-
charm_dir = os.path.abspath(
1664-
os.path.expanduser(identifier))
1664+
charm_dir = os.path.abspath(os.path.expanduser(identifier))
16651665
charm_origin = res.origin
1666+
base = None
1667+
16661668
metadata = utils.get_local_charm_metadata(charm_dir)
1669+
charm_series = charm_series or await get_charm_series(metadata,
1670+
self)
1671+
base = utils.get_local_charm_base(
1672+
charm_series, channel, metadata, charm_dir, client.Base)
1673+
charm_origin.base = base
1674+
if not application_name:
1675+
application_name = metadata['name']
16671676
if not application_name:
16681677
application_name = metadata['name']
1669-
series = series or await get_charm_series(charm_dir, self)
1670-
if not series:
1671-
model_config = await self.get_config()
1672-
default_series = model_config.get("default-series")
1673-
if default_series:
1674-
series = default_series.value
1675-
if not series:
1678+
if base is None and charm_series is None:
16761679
raise JujuError(
1677-
"Couldn't determine series for charm at {}. "
1678-
"Pass a 'series' kwarg to Model.deploy().".format(
1679-
charm_dir))
1680-
identifier = await self.add_local_charm_dir(charm_dir, series)
1680+
"Either series or base is needed to deploy the "
1681+
"charm at {}. ".format(charm_dir))
1682+
1683+
identifier = await self.add_local_charm_dir(charm_dir,
1684+
charm_series)
16811685
resources = await self.add_local_resources(application_name,
16821686
identifier,
16831687
metadata,
@@ -1691,7 +1695,7 @@ async def deploy(
16911695
return await self._deploy(
16921696
charm_url=identifier,
16931697
application=res.app_name,
1694-
series=series,
1698+
series=charm_series,
16951699
config=config,
16961700
constraints=constraints,
16971701
endpoint_bindings=bind,

juju/utils.py

Lines changed: 157 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,172 @@ 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", or "charmcraft.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
252+
if not metadata_path.exists():
253+
return {}
250254
metadata = yaml.load(metadata_path.read_text(), Loader=yaml.FullLoader)
251255

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

tox.ini

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ passenv =
2626
TEST_AGENTS
2727
LXD_DIR
2828
deps =
29+
macaroonbakery
30+
theblues
31+
toposort
32+
typing-inspect
33+
paramiko
2934
asynctest
3035
ipdb
3136
mock
@@ -35,9 +40,7 @@ deps =
3540
Twine
3641
websockets
3742
kubernetes
38-
# use fork to pick up fix for https://github.com/aaugustin/websockets/pull/528
39-
git+https://github.com/johnsca/websockets@bug/client-redirects#egg=websockets ; python_version<'3.6'
40-
cffi <= 1.14.6 ; python_version<='3.5'
43+
cffi
4144

4245
[testenv:lint]
4346
commands =

0 commit comments

Comments
 (0)