Skip to content

Commit 61276ca

Browse files
Implement charm origin
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.
1 parent 487a2c1 commit 61276ca

3 files changed

Lines changed: 328 additions & 0 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/origin.py

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
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+
if track is None:
65+
track = ""
66+
self.track = track
67+
self.risk = risk
68+
69+
@staticmethod
70+
def parse(s):
71+
"""parse a channel from a given string.
72+
Parse does not take into account branches.
73+
74+
"""
75+
if not s:
76+
raise JujuError("channel cannot be empty")
77+
78+
p = s.split("/")
79+
80+
print(len(p))
81+
82+
risk = None
83+
track = None
84+
if len(p) == 1:
85+
if Risk.valid(p[0]):
86+
risk = p[0]
87+
else:
88+
track = p[0]
89+
risk = str(Risk.STABLE)
90+
elif len(p) == 2:
91+
track = p[0]
92+
risk = p[1]
93+
else:
94+
raise JujuError("channel is malformed and has too many components {}".format(s))
95+
96+
if risk is not None and not Risk.valid(risk):
97+
raise JujuError("risk in channel {} is not valid".format(s))
98+
if track is not None and track == "":
99+
raise JujuError("track in channel {} is not valid".format(s))
100+
101+
return Channel(track, risk)
102+
103+
def normalize(self):
104+
track = self.track if self.track != "latest" else ""
105+
risk = self.risk if self.risk != "" else ""
106+
return Channel(track, risk)
107+
108+
def __eq__(self, other):
109+
if isinstance(other, self.__class__):
110+
return self.track == other.track and self.risk == other.risk
111+
return False
112+
113+
def __str__(self):
114+
path = self.risk
115+
if self.track != "":
116+
path = "{}/{}".format(self.track, path)
117+
return path
118+
119+
120+
class Platform:
121+
"""ParsePlatform parses a string representing a store platform.
122+
Serialized version of platform can be expected to conform to the following:
123+
124+
1. Architecture is mandatory.
125+
2. OS is optional and can be dropped. Series is mandatory if OS wants
126+
to be displayed.
127+
3. Series is also optional.
128+
129+
To indicate something is missing `unknown` can be used in place.
130+
131+
Examples:
132+
133+
1. `<arch>/<os>/<series>`
134+
2. `<arch>`
135+
3. `<arch>/<series>`
136+
4. `<arch>/unknown/<series>`
137+
138+
"""
139+
def __init__(self, arch, series=None, os=None):
140+
self.arch = arch
141+
self.series = series
142+
self.os = os
143+
144+
@staticmethod
145+
def parse(s):
146+
if not s:
147+
raise JujuError("platform cannot be empty")
148+
149+
p = s.split("/")
150+
151+
arch = None
152+
os = None
153+
series = None
154+
if len(p) == 1:
155+
arch = p[0]
156+
elif len(p) == 2:
157+
arch = p[0]
158+
series = p[1]
159+
elif len(p) == 3:
160+
arch = p[0]
161+
os = p[1]
162+
series = p[2]
163+
else:
164+
raise JujuError("platform is malformed and has too many components {}".format(s))
165+
166+
if arch is None or arch == "":
167+
raise JujuError("architecture in platform {} is not valid".format(s))
168+
if os is not None and os == "":
169+
raise JujuError("os in platform {} is not valid".format(s))
170+
if series is not None and series == "":
171+
raise JujuError("series in platform {} is not valid".format(s))
172+
173+
return Platform(arch, series, os)
174+
175+
def normalize(self):
176+
os = self.os if self.os is not None or self.os != "unknown" else None
177+
series = self.series
178+
if series is None or series == "unknown":
179+
os = None
180+
series = None
181+
182+
return Platform(self.arch, series, os)
183+
184+
def __eq__(self, other):
185+
if isinstance(other, self.__class__):
186+
return self.arch == other.arch and self.os == other.os and self.series == other.series
187+
return False
188+
189+
def __str__(self):
190+
path = self.arch
191+
if self.os is not None and self.os != "":
192+
path = "{}/{}".format(path, self.os)
193+
if self.series is not None and self.series != "":
194+
path = "{}/{}".format(path, self.series)
195+
return path

tests/unit/test_origin.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import unittest
2+
3+
from juju.origin import Channel, Origin, Platform, Risk, Source
4+
5+
6+
class TestRisk(unittest.TestCase):
7+
def test_valid_risk(self):
8+
self.assertTrue(Risk.valid("stable"))
9+
10+
def test_invalid_risk(self):
11+
self.assertFalse(Risk.valid("maybe"))
12+
13+
14+
class TestChannel(unittest.TestCase):
15+
def test_parse_risk_only(self):
16+
ch = Channel.parse("stable")
17+
self.assertEqual(ch, Channel(None, "stable"))
18+
19+
def test_parse_track_only(self):
20+
ch = Channel.parse("2.0.1")
21+
self.assertEqual(ch, Channel("2.0.1", "stable"))
22+
23+
def test_parse(self):
24+
ch = Channel.parse("latest/stable")
25+
self.assertEqual(ch, Channel("latest", "stable"))
26+
27+
def test_parse_numeric(self):
28+
ch = Channel.parse("2.0.7/stable")
29+
self.assertEqual(ch, Channel("2.0.7", "stable"))
30+
31+
def test_parse_then_normalize(self):
32+
ch = Channel.parse("latest/stable").normalize()
33+
self.assertEqual(ch, Channel(None, "stable"))
34+
35+
def test_str_risk_only(self):
36+
ch = Channel.parse("stable")
37+
self.assertEqual(str(ch), "stable")
38+
39+
def test_str_track_only(self):
40+
ch = Channel.parse("2.0.1")
41+
self.assertEqual(str(ch), "2.0.1/stable")
42+
43+
def test_str(self):
44+
ch = Channel.parse("latest/stable")
45+
self.assertEqual(str(ch), "latest/stable")
46+
47+
def test_str_numeric(self):
48+
ch = Channel.parse("2.0.7/stable")
49+
self.assertEqual(str(ch), "2.0.7/stable")
50+
51+
def test_str_then_normalize(self):
52+
ch = Channel.parse("latest/stable").normalize()
53+
self.assertEqual(str(ch), "stable")
54+
55+
56+
class TestPlatform(unittest.TestCase):
57+
def test_parse_arch_only(self):
58+
p = Platform.parse("architecture")
59+
self.assertEqual(p, Platform("architecture"))
60+
61+
def test_parse_arch_and_series(self):
62+
p = Platform.parse("architecture/series")
63+
self.assertEqual(p, Platform("architecture", "series"))
64+
65+
def test_parse(self):
66+
p = Platform.parse("architecture/os/series")
67+
self.assertEqual(p, Platform("architecture", "series", "os"))
68+
69+
def test_parse_with_unknowns(self):
70+
p = Platform.parse("architecture/unknown/unknown")
71+
self.assertEqual(p, Platform("architecture", "unknown", "unknown"))
72+
73+
def test_parse_with_unknowns_after_normalize(self):
74+
p = Platform.parse("architecture/unknown/unknown").normalize()
75+
self.assertEqual(p, Platform("architecture"))
76+
77+
def test_str_arch_only(self):
78+
p = Platform.parse("architecture")
79+
self.assertEqual(str(p), "architecture")
80+
81+
def test_str_arch_and_series(self):
82+
p = Platform.parse("architecture/series")
83+
self.assertEqual(str(p), "architecture/series")
84+
85+
def test_str(self):
86+
p = Platform.parse("architecture/os/series")
87+
self.assertEqual(str(p), "architecture/os/series")
88+
89+
90+
class TestOrigin(unittest.TestCase):
91+
def test_origin(self):
92+
ch = Channel.parse("latest/stable")
93+
p = Platform.parse("amd64/ubuntu/focal")
94+
95+
o = Origin(Source.CHARM_HUB, ch, p)
96+
self.assertEqual(str(o), "origin using source charm-hub for channel latest/stable and platform amd64/ubuntu/focal")

0 commit comments

Comments
 (0)