Skip to content

Commit 0e8c45f

Browse files
authored
Merge pull request #1105 from Aflynn50/fix-parse-storage-constraints
#1105 It seems a previous change for allow bundle storage constraints was done at the wrong level and affected regular storage constraints as well. This fix moves the previous change to only affect bundle storage constraints. #### QA Steps TODO
2 parents 3911d1f + 04f9a1d commit 0e8c45f

3 files changed

Lines changed: 117 additions & 15 deletions

File tree

juju/bundle.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@
99
import base64
1010
from contextlib import closing
1111
from pathlib import Path
12+
from typing import Dict, Optional
1213

1314
import yaml
1415

1516
from toposort import toposort_flatten
1617

1718
from .client import client
1819
from .constraints import parse as parse_constraints
20+
from .constraints import parse_storage_constraint
1921
from .errors import JujuError
2022
from . import utils, jasyncio
2123
from .origin import Channel, Source
@@ -554,14 +556,20 @@ class AddApplicationChange(ChangeInfo):
554556
this will be used to scale the application.
555557
:options: holds application options.
556558
:constraints: optional application constraints.
557-
:storage: optional storage constraints.
559+
:storage: optional storage constraints, in the form of `{label: constraint}`.
560+
The label is a string specified by the charm, while the constraint is a string following
561+
`the juju storage constraint directive format <https://juju.is/docs/juju/storage-constraint>`_,
562+
specifying the storage pool, number of volumes, and size of each volume.
558563
:devices: optional devices constraints.
559564
:endpoint_bindings: optional endpoint bindings
560565
:resources: identifies the revision to use for each resource of the
561566
application's charm.
562567
:local_resources: identifies the path to the local resource of the
563568
application's charm.
564569
"""
570+
571+
storage: Optional[Dict[str, str]]
572+
565573
@staticmethod
566574
def method():
567575
"""method returns an associated ID for the Juju API call.
@@ -630,7 +638,11 @@ async def run(self, context):
630638
constraints=self.constraints,
631639
endpoint_bindings=self.endpoint_bindings,
632640
resources=resources,
633-
storage=self.storage,
641+
storage={
642+
label: parse_storage_constraint(constraint)
643+
for label, constraint
644+
in (self.storage or {}).items()
645+
},
634646
channel=self.channel,
635647
devices=self.devices,
636648
num_units=self.num_units,

juju/model.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
from .client import client, connector
3030
from .client.overrides import Caveat, Macaroon
3131
from .constraints import parse as parse_constraints
32-
from .constraints import parse_storage_constraint
3332
from .controller import Controller, ConnectedController
3433
from .delta import get_entity_class, get_entity_delta
3534
from .errors import JujuAPIError, JujuError, JujuModelConfigError, JujuBackupError
@@ -2116,7 +2115,7 @@ async def _deploy(self, charm_url, application, series, config,
21162115
devices=devices,
21172116
dryrun=False,
21182117
placement=placement,
2119-
storage={k: parse_storage_constraint(v) for k, v in (storage or dict()).items()},
2118+
storage=storage,
21202119
trust=trust,
21212120
base=charm_origin.base,
21222121
channel=channel,
@@ -2151,7 +2150,7 @@ async def _deploy(self, charm_url, application, series, config,
21512150
endpoint_bindings=endpoint_bindings,
21522151
num_units=num_units,
21532152
resources=resources,
2154-
storage={k: parse_storage_constraint(v) for k, v in (storage or dict()).items()},
2153+
storage=storage,
21552154
placement=placement,
21562155
devices=devices,
21572156
attach_storage=attach_storage,

tests/unit/test_bundle.py

Lines changed: 101 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
# Copyright 2023 Canonical Ltd.
22
# Licensed under the Apache V2, see LICENCE file for details.
33

4-
from pathlib import Path
54
import unittest
5+
from pathlib import Path
66
from unittest import mock
77
from mock import patch, Mock, ANY
8+
from typing import Dict, List, Tuple
89

910
import yaml
1011

@@ -23,6 +24,7 @@
2324
SetAnnotationsChange,
2425
)
2526
from juju import charmhub
27+
from juju import constraints
2628
from juju.client import client
2729
from toposort import CircularDependencyError
2830

@@ -121,14 +123,15 @@ def test_dict_params_missing_data(self):
121123

122124

123125
class TestAddApplicationChangeRun:
124-
125126
async def test_run_with_charmhub_charm(self):
127+
storage_label = "some-label"
128+
storage_constraint = "ebs,100G,1"
126129
change = AddApplicationChange(1, [], params={"charm": "charm",
127130
"series": "series",
128131
"application": "application",
129132
"options": "options",
130133
"constraints": "constraints",
131-
"storage": "storage",
134+
"storage": {storage_label: storage_constraint},
132135
"endpoint-bindings": "endpoint_bindings",
133136
"resources": "resources",
134137
"devices": "devices",
@@ -160,22 +163,106 @@ async def test_run_with_charmhub_charm(self):
160163
constraints="constraints",
161164
endpoint_bindings="endpoint_bindings",
162165
resources=["resource1"],
163-
storage="storage",
166+
storage={storage_label: constraints.parse_storage_constraint(storage_constraint)},
164167
devices="devices",
165168
channel="channel",
166169
charm_origin=ANY,
167170
num_units="num_units")
168171

172+
async def test_run_with_storage_variations(self):
173+
"""Test that various valid storage constraints are parsed as expected before model._deploy is called.
174+
175+
Uses the mock call logic from test_run_with_charmhub_charm, which will run before this test.
176+
"""
177+
storage_arg_pairs: List[Tuple[Dict[str, str], Dict[str, constraints.StorageConstraintDict]]] = [
178+
# (storage_arg_for_change, storage_arg_for_deploy)
179+
({'some-label': 'ebs,100G,1'}, {'some-label': {'count': 1, 'pool': 'ebs', 'size': 102400}}),
180+
({'some-label': 'ebs,2.1G,3'}, {'some-label': {'count': 3, 'pool': 'ebs', 'size': 2150}}),
181+
({'some-label': 'ebs,100G'}, {'some-label': {'count': 1, 'pool': 'ebs', 'size': 102400}}),
182+
({'some-label': 'ebs,2'}, {'some-label': {'count': 2, 'pool': 'ebs'}}),
183+
({'some-label': '200G,7'}, {'some-label': {'count': 7, 'size': 204800}}),
184+
({'some-label': 'ebs'}, {'some-label': {'count': 1, 'pool': 'ebs'}}),
185+
({'some-label': '10YB'}, {'some-label': {'count': 1, 'size': 11529215046068469760}}),
186+
({'some-label': '1'}, {'some-label': {'count': 1}}),
187+
({'some-label': '-1'}, {'some-label': {'count': 1}}),
188+
({'some-label': ''}, {'some-label': {'count': 1}}),
189+
(
190+
{
191+
'some-label': '2.1G,3',
192+
'data': '1MiB,70',
193+
'logs': 'ebs,-1',
194+
},
195+
{
196+
'some-label': {'count': 3, 'size': 2150},
197+
'data': {'count': 70, 'size': 1},
198+
'logs': {'count': 1, 'pool': 'ebs'}
199+
},
200+
),
201+
]
202+
for storage_arg_for_change, storage_arg_for_deploy in storage_arg_pairs:
203+
change = AddApplicationChange(
204+
1,
205+
[],
206+
params={
207+
"charm": "charm",
208+
"series": "series",
209+
"application": "application",
210+
"options": "options",
211+
"constraints": "constraints",
212+
"storage": storage_arg_for_change,
213+
"endpoint-bindings": "endpoint_bindings",
214+
"resources": "resources",
215+
"devices": "devices",
216+
"num-units": "num_units",
217+
"channel": "channel",
218+
},
219+
)
220+
# mock model
221+
model = Mock()
222+
model._deploy = mock.AsyncMock(return_value=None)
223+
model._add_charmhub_resources = mock.AsyncMock(return_value=["resource1"])
224+
model.applications = {}
225+
# mock context
226+
context = Mock()
227+
context.resolve.return_value = "ch:charm1"
228+
context.origins = {"ch:charm1": Mock()}
229+
context.trusted = False
230+
context.model = model
231+
# mock info_func
232+
info_func = mock.AsyncMock(return_value=["12345", "name"])
233+
# patch and call
234+
with patch.object(charmhub.CharmHub, 'get_charm_id', info_func):
235+
result = await change.run(context)
236+
assert result == "application"
237+
# asserts
238+
model._deploy.assert_called_once()
239+
model._deploy.assert_called_with(
240+
charm_url="ch:charm1",
241+
application="application",
242+
series="series",
243+
config="options",
244+
constraints="constraints",
245+
endpoint_bindings="endpoint_bindings",
246+
resources=["resource1"],
247+
storage=storage_arg_for_deploy, # we're testing this
248+
devices="devices",
249+
channel="channel",
250+
charm_origin=ANY,
251+
num_units="num_units",
252+
)
253+
169254
async def test_run_with_charmhub_charm_no_channel(self):
170255
"""Test to verify if when the given channel is None, the channel defaults to "local/stable", which
171256
is the default channel value for the Charm Hub
172257
"""
258+
storage_label = "some-label"
259+
storage_constraint = "ebs,100G,1"
173260
change = AddApplicationChange(1, [], params={"charm": "charm",
174261
"series": "series",
175262
"application": "application",
176263
"options": "options",
177264
"constraints": "constraints",
178-
"storage": "storage",
265+
"storage": {storage_label: storage_constraint},
179266
"endpoint-bindings": "endpoint_bindings",
180267
"resources": "resources",
181268
"devices": "devices",
@@ -208,19 +295,21 @@ async def test_run_with_charmhub_charm_no_channel(self):
208295
constraints="constraints",
209296
endpoint_bindings="endpoint_bindings",
210297
resources=["resource1"],
211-
storage="storage",
298+
storage={storage_label: constraints.parse_storage_constraint(storage_constraint)},
212299
devices="devices",
213300
channel="latest/stable",
214301
charm_origin=ANY,
215302
num_units="num_units")
216303

217304
async def test_run_local(self):
305+
storage_label = "some-label"
306+
storage_constraint = "ebs,100G,1"
218307
change = AddApplicationChange(1, [], params={"charm": "local:charm",
219308
"series": "series",
220309
"application": "application",
221310
"options": "options",
222311
"constraints": "constraints",
223-
"storage": "storage",
312+
"storage": {storage_label: storage_constraint},
224313
"endpoint-bindings": "endpoint_bindings",
225314
"devices": "devices",
226315
"num-units": "num_units"})
@@ -246,19 +335,21 @@ async def test_run_local(self):
246335
constraints="constraints",
247336
endpoint_bindings="endpoint_bindings",
248337
resources={},
249-
storage="storage",
338+
storage={storage_label: constraints.parse_storage_constraint(storage_constraint)},
250339
devices="devices",
251340
num_units="num_units",
252341
channel="",
253342
charm_origin=ANY)
254343

255344
async def test_run_no_series(self):
345+
storage_label = "some-label"
346+
storage_constraint = "ebs,100G,1"
256347
change = AddApplicationChange(1, [], params={"charm": "ch:charm1",
257348
"series": "",
258349
"application": "application",
259350
"options": "options",
260351
"constraints": "constraints",
261-
"storage": "storage",
352+
"storage": {storage_label: storage_constraint},
262353
"endpoint-bindings": "endpoint_bindings",
263354
"resources": "resources",
264355
"devices": "devices",
@@ -289,7 +380,7 @@ async def test_run_no_series(self):
289380
constraints="constraints",
290381
endpoint_bindings="endpoint_bindings",
291382
resources=["resource1"],
292-
storage="storage",
383+
storage={storage_label: constraints.parse_storage_constraint(storage_constraint)},
293384
devices="devices",
294385
channel="channel",
295386
charm_origin=ANY,

0 commit comments

Comments
 (0)