Skip to content

Commit 195191b

Browse files
authored
Merge pull request #463 from SimonRichardson/charm-hub-origin
#463 Requires #460 and #462 to land first The following starts to implement charm origin. These are required for the deploy commands where we're talking about origin, channels and platforms. If we make types of these then we can pass them around knowing exactly the correct types at hand. The code is pretty much a copy and pasted from juju itself with some modifications to channels that don't take into account branches.
2 parents d6d157f + 4a17427 commit 195191b

4 files changed

Lines changed: 332 additions & 40 deletions

File tree

juju/charmstore.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from functools import partial
2+
3+
import asyncio
4+
import theblues.charmstore
5+
import theblues.errors
6+
7+
8+
class CharmStore:
9+
"""
10+
Async wrapper around theblues.charmstore.CharmStore
11+
"""
12+
def __init__(self, loop, cs_timeout=20):
13+
self.loop = loop
14+
self._cs = theblues.charmstore.CharmStore(timeout=cs_timeout)
15+
16+
def __getattr__(self, name):
17+
"""
18+
Wrap method calls in coroutines that use run_in_executor to make them
19+
async.
20+
"""
21+
attr = getattr(self._cs, name)
22+
if not callable(attr):
23+
wrapper = partial(getattr, self._cs, name)
24+
setattr(self, name, wrapper)
25+
else:
26+
async def coro(*args, **kwargs):
27+
method = partial(attr, *args, **kwargs)
28+
for attempt in range(1, 4):
29+
try:
30+
return await self.loop.run_in_executor(None, method)
31+
except theblues.errors.ServerError:
32+
if attempt == 3:
33+
raise
34+
await asyncio.sleep(1, loop=self.loop)
35+
setattr(self, name, coro)
36+
wrapper = coro
37+
return wrapper

juju/model.py

Lines changed: 8 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,13 @@
1515
from pathlib import Path
1616

1717
import yaml
18-
19-
import theblues.charmstore
20-
import theblues.errors
2118
import websockets
2219

2320
from . import provisioner, tag, utils
2421
from .annotationhelper import _get_annotations, _set_annotations
2522
from .bundle import BundleHandler, get_charm_series
2623
from .charmhub import CharmHub
24+
from .charmstore import CharmStore
2725
from .client import client, connector
2826
from .client.client import ConfigValue, Value
2927
from .client.overrides import Caveat, Macaroon
@@ -461,6 +459,8 @@ def __init__(
461459
self._watch_stopped = asyncio.Event(loop=self._connector.loop)
462460
self._watch_received = asyncio.Event(loop=self._connector.loop)
463461
self._watch_stopped.set()
462+
463+
self._charmhub = CharmHub(self)
464464
self._charmstore = CharmStore(self._connector.loop)
465465

466466
def is_connected(self):
@@ -783,7 +783,11 @@ def charmhub(self):
783783
the charm-hub-url model config.
784784
785785
"""
786-
return CharmHub(self)
786+
return self._charmhub
787+
788+
@property
789+
def charmstore(self):
790+
return self._charmstore
787791

788792
async def get_info(self):
789793
"""Return a client.ModelInfo object for this Model.
@@ -1968,10 +1972,6 @@ def upload_backup(self, archive_path):
19681972
"""
19691973
raise NotImplementedError()
19701974

1971-
@property
1972-
def charmstore(self):
1973-
return self._charmstore
1974-
19751975
async def get_metrics(self, *tags):
19761976
"""Retrieve metrics.
19771977
@@ -2186,38 +2186,6 @@ def _create_consume_args(offer, macaroon, controller_info):
21862186
return arg
21872187

21882188

2189-
class CharmStore:
2190-
"""
2191-
Async wrapper around theblues.charmstore.CharmStore
2192-
"""
2193-
def __init__(self, loop, cs_timeout=20):
2194-
self.loop = loop
2195-
self._cs = theblues.charmstore.CharmStore(timeout=cs_timeout)
2196-
2197-
def __getattr__(self, name):
2198-
"""
2199-
Wrap method calls in coroutines that use run_in_executor to make them
2200-
async.
2201-
"""
2202-
attr = getattr(self._cs, name)
2203-
if not callable(attr):
2204-
wrapper = partial(getattr, self._cs, name)
2205-
setattr(self, name, wrapper)
2206-
else:
2207-
async def coro(*args, **kwargs):
2208-
method = partial(attr, *args, **kwargs)
2209-
for attempt in range(1, 4):
2210-
try:
2211-
return await self.loop.run_in_executor(None, method)
2212-
except theblues.errors.ServerError:
2213-
if attempt == 3:
2214-
raise
2215-
await asyncio.sleep(1, loop=self.loop)
2216-
setattr(self, name, coro)
2217-
wrapper = coro
2218-
return wrapper
2219-
2220-
22212189
class CharmArchiveGenerator:
22222190
"""
22232191
Create a Zip archive of a local charm directory for upload to a controller.

juju/origin.py

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
from enum import Enum
2+
from .errors import JujuError
3+
4+
5+
class Source(Enum):
6+
"""Source defines a origin source. Providing a hint to the controller about
7+
what the charm identity is from the URL and origin source.
8+
9+
"""
10+
LOCAL = "local"
11+
CHARM_STORE = "charm-store"
12+
CHARM_HUB = "charm-hub"
13+
14+
def __str__(self):
15+
return self.value
16+
17+
18+
class Origin:
19+
def __init__(self, source, channel, platform):
20+
self.source = source
21+
self.channel = channel
22+
self.platform = platform
23+
24+
def __str__(self):
25+
return "origin using source {} for channel {} and platform {}".format(str(self.source), self.channel, self.platform)
26+
27+
28+
class Risk(Enum):
29+
STABLE = "stable"
30+
CANDIDATE = "candidate"
31+
BETA = "beta"
32+
EDGE = "edge"
33+
34+
def __str__(self):
35+
return self.value
36+
37+
@staticmethod
38+
def valid(potential):
39+
for risk in [Risk.STABLE, Risk.CANDIDATE, Risk.BETA, Risk.EDGE]:
40+
if str(risk) == potential:
41+
return True
42+
return False
43+
44+
45+
class Channel:
46+
"""Channel identifies and describes completely a store channel.
47+
48+
A channel consists of, and is subdivided by, tracks, risk-levels and
49+
- Tracks enable snap developers to publish multiple supported releases of
50+
their application under the same snap name.
51+
- Risk-levels represent a progressive potential trade-off between stability
52+
and new features.
53+
54+
The complete channel name can be structured as three distinct parts separated
55+
by slashes:
56+
57+
<track>/<risk>
58+
59+
"""
60+
def __init__(self, track=None, risk=None):
61+
if not Risk.valid(risk):
62+
raise JujuError("unexpected risk {}".format(risk))
63+
64+
self.track = track or ""
65+
self.risk = risk
66+
67+
@staticmethod
68+
def parse(s):
69+
"""parse a channel from a given string.
70+
Parse does not take into account branches.
71+
72+
"""
73+
if not s:
74+
raise JujuError("channel cannot be empty")
75+
76+
p = s.split("/")
77+
78+
risk = None
79+
track = None
80+
if len(p) == 1:
81+
if Risk.valid(p[0]):
82+
risk = p[0]
83+
else:
84+
track = p[0]
85+
risk = str(Risk.STABLE)
86+
elif len(p) == 2:
87+
track = p[0]
88+
risk = p[1]
89+
else:
90+
raise JujuError("channel is malformed and has too many components {}".format(s))
91+
92+
if risk is not None and not Risk.valid(risk):
93+
raise JujuError("risk in channel {} is not valid".format(s))
94+
if track is not None and track == "":
95+
raise JujuError("track in channel {} is not valid".format(s))
96+
97+
return Channel(track, risk)
98+
99+
def normalize(self):
100+
track = self.track if self.track != "latest" else ""
101+
risk = self.risk if self.risk != "" else ""
102+
return Channel(track, risk)
103+
104+
def __eq__(self, other):
105+
if isinstance(other, self.__class__):
106+
return self.track == other.track and self.risk == other.risk
107+
return False
108+
109+
def __str__(self):
110+
path = self.risk
111+
if self.track != "":
112+
path = "{}/{}".format(self.track, path)
113+
return path
114+
115+
116+
class Platform:
117+
"""ParsePlatform parses a string representing a store platform.
118+
Serialized version of platform can be expected to conform to the following:
119+
120+
1. Architecture is mandatory.
121+
2. OS is optional and can be dropped. Series is mandatory if OS wants
122+
to be displayed.
123+
3. Series is also optional.
124+
125+
To indicate something is missing `unknown` can be used in place.
126+
127+
Examples:
128+
129+
1. `<arch>/<os>/<series>`
130+
2. `<arch>`
131+
3. `<arch>/<series>`
132+
4. `<arch>/unknown/<series>`
133+
134+
"""
135+
def __init__(self, arch, series=None, os=None):
136+
self.arch = arch
137+
self.series = series
138+
self.os = os
139+
140+
@staticmethod
141+
def parse(s):
142+
if not s:
143+
raise JujuError("platform cannot be empty")
144+
145+
p = s.split("/")
146+
147+
arch = None
148+
os = None
149+
series = None
150+
if len(p) == 1:
151+
arch = p[0]
152+
elif len(p) == 2:
153+
arch = p[0]
154+
series = p[1]
155+
elif len(p) == 3:
156+
arch = p[0]
157+
os = p[1]
158+
series = p[2]
159+
else:
160+
raise JujuError("platform is malformed and has too many components {}".format(s))
161+
162+
if not arch:
163+
raise JujuError("architecture in platform {} is not valid".format(s))
164+
if os is not None and os == "":
165+
raise JujuError("os in platform {} is not valid".format(s))
166+
if series is not None and series == "":
167+
raise JujuError("series in platform {} is not valid".format(s))
168+
169+
return Platform(arch, series, os)
170+
171+
def normalize(self):
172+
os = self.os if self.os is not None or self.os != "unknown" else None
173+
series = self.series
174+
if series is None or series == "unknown":
175+
os = None
176+
series = None
177+
178+
return Platform(self.arch, series, os)
179+
180+
def __eq__(self, other):
181+
if isinstance(other, self.__class__):
182+
return self.arch == other.arch and self.os == other.os and self.series == other.series
183+
return False
184+
185+
def __str__(self):
186+
path = self.arch
187+
if self.os is not None and self.os != "":
188+
path = "{}/{}".format(path, self.os)
189+
if self.series is not None and self.series != "":
190+
path = "{}/{}".format(path, self.series)
191+
return path

0 commit comments

Comments
 (0)