Skip to content

Commit 578f1d9

Browse files
feat(storage-constraints): unify parsing under Model._deploy
This allows both bundle.AddApplicationChange and model.Model.deploy to use either storage constraints in the form of {label: constraint} where constraint can be either a juju style storage constraints string, or a python dict with a 'count' entry and optionally 'pool' and 'size' entries.
1 parent f176d82 commit 578f1d9

3 files changed

Lines changed: 55 additions & 20 deletions

File tree

juju/bundle.py

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Copyright 2023 Canonical Ltd.
22
# Licensed under the Apache V2, see LICENCE file for details.
3+
from __future__ import annotations
34

45
import base64
56
import io
@@ -8,7 +9,7 @@
89
import zipfile
910
from contextlib import closing
1011
from pathlib import Path
11-
from typing import Dict, Optional
12+
from typing import TYPE_CHECKING, Mapping, cast
1213

1314
import requests
1415
import yaml
@@ -17,12 +18,15 @@
1718
from . import jasyncio, utils
1819
from .client import client
1920
from .constraints import parse as parse_constraints
20-
from .constraints import parse_storage_constraint
2121
from .errors import JujuError
2222
from .origin import Channel, Source
2323
from .url import URL, Schema
2424
from .utils import get_base_from_origin_or_channel
2525

26+
if TYPE_CHECKING:
27+
from .constraints import StorageConstraintDict
28+
from .model import Model
29+
2630
log = logging.getLogger(__name__)
2731

2832

@@ -599,7 +603,8 @@ class AddApplicationChange(ChangeInfo):
599603
:options: holds application options.
600604
:constraints: optional application constraints.
601605
:storage: optional storage constraints, in the form of `{label: constraint}`.
602-
The label is a string specified by the charm, while the constraint is a string following
606+
The label is a string specified by the charm, while the constraint is
607+
either a constraints.StorageConstraintDict, or a string following
603608
`the juju storage constraint directive format <https://juju.is/docs/juju/storage-constraint>`_,
604609
specifying the storage pool, number of volumes, and size of each volume.
605610
:devices: optional devices constraints.
@@ -610,7 +615,7 @@ class AddApplicationChange(ChangeInfo):
610615
application's charm.
611616
"""
612617

613-
storage: Optional[Dict[str, str]]
618+
storage: Mapping[str, str | StorageConstraintDict] | None = None
614619

615620
@staticmethod
616621
def method():
@@ -627,7 +632,8 @@ async def run(self, context):
627632
# NB: this should really be handled by the controller when generating the
628633
# bundle change plan, and this short-term workaround may be missing some
629634
# aspects of the logic which the CLI client contains to handle edge cases.
630-
if self.application in context.model.applications:
635+
model = cast("Model", context.model) # pyright: ignore[reportUnknownMemberType]
636+
if self.application in model.applications:
631637
log.debug("Skipping %s; already in model", self.application)
632638
return self.application
633639

@@ -637,9 +643,9 @@ async def run(self, context):
637643
if self.options is not None:
638644
options = self.options
639645
if context.trusted:
640-
if context.model.info.agent_version < client.Number.from_json("2.4.0"):
646+
if model.info.agent_version < client.Number.from_json("2.4.0"):
641647
raise NotImplementedError(
642-
f"trusted is not supported on model version {context.model.info.agent_version}"
648+
f"trusted is not supported on model version {model.info.agent_version}"
643649
)
644650
options["trust"] = "true"
645651

@@ -676,22 +682,19 @@ async def run(self, context):
676682
.get("resources", {})
677683
)
678684
if Schema.CHARM_HUB.matches(url.schema):
679-
resources = await context.model._add_charmhub_resources(
685+
resources = await model._add_charmhub_resources(
680686
self.application, charm, origin, overrides=self.resources
681687
)
682688

683-
await context.model._deploy(
689+
await model._deploy(
684690
charm_url=charm,
685691
application=self.application,
686692
series=self.series,
687693
config=options,
688694
constraints=self.constraints,
689695
endpoint_bindings=self.endpoint_bindings,
690696
resources=resources,
691-
storage={
692-
label: parse_storage_constraint(constraint)
693-
for label, constraint in (self.storage or {}).items()
694-
},
697+
storage=self.storage,
695698
channel=self.channel,
696699
devices=self.devices,
697700
num_units=self.num_units,

juju/constraints.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
#
2020

2121
import re
22-
from typing import Dict, List, Optional, TypedDict, Union
22+
from typing import Dict, List, Mapping, Optional, TypedDict, Union
2323

2424
from typing_extensions import NotRequired, Required
2525

@@ -190,6 +190,25 @@ def parse_storage_constraint(constraint: str) -> StorageConstraintDict:
190190
return storage
191191

192192

193+
def parse_storage_constraints(
194+
constraints: Optional[Mapping[str, Union[str, StorageConstraintDict]]] = None,
195+
) -> Dict[str, StorageConstraintDict]:
196+
if constraints is None:
197+
return {}
198+
parsed: dict[str, StorageConstraintDict] = {}
199+
for label, storage_constraint in constraints.items():
200+
if isinstance(storage_constraint, str):
201+
parsed[label] = parse_storage_constraint(storage_constraint)
202+
elif isinstance(storage_constraint, dict): # pyright: ignore[reportUnnecessaryIsInstance]
203+
parsed[label] = storage_constraint
204+
else:
205+
raise ValueError(
206+
f"Unexpected constraint {storage_constraint!r}"
207+
f" for label {label!r} in {constraints}"
208+
)
209+
return parsed
210+
211+
193212
DEVICE = re.compile(
194213
# original regex:
195214
# '^(?P<count>[0-9]+)?(?:^|,)(?P<type>[^,]+)(?:$|,(?!$))(?P<attrs>(?:[^=]+=[^;]+)+)*$'

juju/model.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
from .client import client, connection, connector
3333
from .client.overrides import Caveat, Macaroon
3434
from .constraints import parse as parse_constraints
35+
from .constraints import parse_storage_constraints
3536
from .controller import ConnectedController, Controller
3637
from .delta import get_entity_class, get_entity_delta
3738
from .errors import (
@@ -61,6 +62,7 @@
6162
if TYPE_CHECKING:
6263
from .application import Application
6364
from .client._definitions import FullStatus
65+
from .constraints import StorageConstraintDict
6466
from .machine import Machine
6567
from .relation import Relation
6668
from .remoteapplication import ApplicationOffer, RemoteApplication
@@ -1788,7 +1790,7 @@ async def deploy(
17881790
resources=None,
17891791
series=None,
17901792
revision=None,
1791-
storage=None,
1793+
storage: Mapping[str, str | StorageConstraintDict] | None = None,
17921794
to=None,
17931795
devices=None,
17941796
trust=False,
@@ -1813,7 +1815,11 @@ async def deploy(
18131815
:param str series: Series on which to deploy DEPRECATED: use --base (with Juju 3.1)
18141816
:param int revision: specifying a revision requires a channel for future upgrades for charms.
18151817
For bundles, revision and channel are mutually exclusive.
1816-
:param dict storage: Storage constraints TODO how do these look?
1818+
:param dict storage: optional storage constraints, in the form of `{label: constraint}`.
1819+
The label is a string specified by the charm, while the constraint is
1820+
a constraints.StorageConstraintsDict, or a string following
1821+
`the juju storage constraint directive format <https://juju.is/docs/juju/storage-constraint>`_,
1822+
specifying the storage pool, number of volumes, and size of each volume.
18171823
:param to: Placement directive as a string. For example:
18181824
18191825
'23' - place on machine 23
@@ -1829,8 +1835,6 @@ async def deploy(
18291835
:param str[] attach_storage: Existing storage to attach to the deployed unit
18301836
(not available on k8s models)
18311837
"""
1832-
if storage:
1833-
storage = {k: client.Constraints(**v) for k, v in storage.items()}
18341838
if trust and (self.info.agent_version < client.Number.from_json("2.4.0")):
18351839
raise NotImplementedError(
18361840
f"trusted is not supported on model version {self.info.agent_version}"
@@ -2251,7 +2255,7 @@ async def _deploy(
22512255
constraints,
22522256
endpoint_bindings,
22532257
resources,
2254-
storage,
2258+
storage: Mapping[str, str | StorageConstraintDict] | None,
22552259
channel=None,
22562260
num_units=None,
22572261
placement=None,
@@ -2261,9 +2265,18 @@ async def _deploy(
22612265
force=False,
22622266
server_side_deploy=False,
22632267
):
2264-
"""Logic shared between `Model.deploy` and `BundleHandler.deploy`."""
2268+
"""Logic shared between `Model.deploy` and `BundleHandler.deploy`.
2269+
2270+
:param dict storage: optional storage constraints, in the form of `{label: constraint}`.
2271+
The label is a string specified by the charm, while the constraint is
2272+
either a constraints.StorageConstraintDict, or a string following
2273+
`the juju storage constraint directive format <https://juju.is/docs/juju/storage-constraint>`_,
2274+
specifying the storage pool, number of volumes, and size of each volume.
2275+
"""
22652276
log.info("Deploying %s", charm_url)
22662277

2278+
storage = parse_storage_constraints(storage)
2279+
22672280
trust = config.get("trust", False)
22682281
# stringify all config values for API, and convert to YAML
22692282
config = {k: str(v) for k, v in config.items()}

0 commit comments

Comments
 (0)