Skip to content

Commit

Permalink
Merge pull request #141 from tonyandrewmeyer/cloud-spec-on-model
Browse files Browse the repository at this point in the history
refactor!: move the cloud specification to Model
  • Loading branch information
tonyandrewmeyer authored Jun 11, 2024
2 parents b9e3a75 + b895d36 commit f1f58e5
Show file tree
Hide file tree
Showing 7 changed files with 52 additions and 29 deletions.
31 changes: 15 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -948,30 +948,29 @@ assert out.model.name == "my-model"
assert out.model.uuid == state_in.model.uuid
```

## CloudSpec
### CloudSpec

You can set CloudSpec information in the state (only `type` and `name` are required).
You can set CloudSpec information in the model (only `type` and `name` are required).

Example:

```python
import scenario

state = scenario.State(
cloud_spec=scenario.CloudSpec(
type="lxd",
name="localhost",
endpoint="https://127.0.0.1:8443",
credential=scenario.CloudCredential(
auth_type="clientcertificate",
attributes={
"client-cert": "foo",
"client-key": "bar",
"server-cert": "baz",
},
),
cloud_spec=scenario.CloudSpec(
type="lxd",
endpoint="https://127.0.0.1:8443",
credential=scenario.CloudCredential(
auth_type="clientcertificate",
attributes={
"client-cert": "foo",
"client-key": "bar",
"server-cert": "baz",
},
),
model=scenario.Model(name="my-vm-model", type="lxd"),
)
state = scenario.State(
model=scenario.Model(name="my-vm-model", type="lxd", cloud_spec=cloud_spec),
)
```

Expand Down
6 changes: 4 additions & 2 deletions scenario/consistency_checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -580,9 +580,11 @@ def check_cloudspec_consistency(
errors = []
warnings = []

if state.model.type == "kubernetes" and state.cloud_spec:
if state.model.type == "kubernetes" and state.model.cloud_spec:
errors.append(
"CloudSpec is only available for machine charms, not Kubernetes charms. Tell Scenario to simulate a machine substrate with: `scenario.State(..., model=scenario.Model(type='lxd'))`.",
"CloudSpec is only available for machine charms, not Kubernetes charms. "
"Tell Scenario to simulate a machine substrate with: "
"`scenario.State(..., model=scenario.Model(type='lxd'))`.",
)

return Results(errors, warnings)
4 changes: 4 additions & 0 deletions scenario/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ def __init__(
capture_framework_events: bool = False,
app_name: Optional[str] = None,
unit_id: Optional[int] = 0,
app_trusted: bool = False,
):
"""Represents a simulated charm's execution context.
Expand Down Expand Up @@ -225,6 +226,8 @@ def __init__(
:arg app_name: App name that this charm is deployed as. Defaults to the charm name as
defined in metadata.yaml.
:arg unit_id: Unit ID that this charm is deployed as. Defaults to 0.
:arg app_trusted: whether the charm has Juju trust (deployed with ``--trust`` or added with
``juju trust``). Defaults to False
:arg charm_root: virtual charm root the charm will be executed with.
If the charm, say, expects a `./src/foo/bar.yaml` file present relative to the
execution cwd, you need to use this. E.g.:
Expand Down Expand Up @@ -268,6 +271,7 @@ def __init__(

self._app_name = app_name
self._unit_id = unit_id
self.app_trusted = app_trusted
self._tmp = tempfile.TemporaryDirectory()

# config for what events to be captured in emitted_events.
Expand Down
11 changes: 8 additions & 3 deletions scenario/mocking.py
Original file line number Diff line number Diff line change
Expand Up @@ -632,11 +632,16 @@ def resource_get(self, resource_name: str) -> str:
)

def credential_get(self) -> CloudSpec:
if not self._state.cloud_spec:
if not self._context.app_trusted:
raise ModelError(
"ERROR cloud spec is empty, initialise it with `scenario.State(cloud_spec=scenario.CloudSpec(...))`",
"ERROR charm is not trusted, initialise Context with `app_trusted=True`",
)
return self._state.cloud_spec._to_ops()
if not self._state.model.cloud_spec:
raise ModelError(
"ERROR cloud spec is empty, initialise it with "
"`State(model=Model(..., cloud_spec=ops.CloudSpec(...)))`",
)
return self._state.model.cloud_spec._to_ops()


class _MockPebbleClient(_TestingPebbleClient):
Expand Down
6 changes: 3 additions & 3 deletions scenario/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,6 @@ class CloudCredential:

attributes: Dict[str, str] = dataclasses.field(default_factory=dict)
"""A dictionary containing cloud credentials.
For example, for AWS, it contains `access-key` and `secret-key`;
for Azure, `application-id`, `application-password` and `subscription-id`
can be found here.
Expand Down Expand Up @@ -641,6 +640,9 @@ class Model(_DCBase):
# TODO: make this exhaustive.
type: Literal["kubernetes", "lxd"] = "kubernetes"

cloud_spec: Optional[CloudSpec] = None
"""Cloud specification information (metadata) including credentials."""


# for now, proc mock allows you to map one command to one mocked output.
# todo extend: one input -> multiple outputs, at different times
Expand Down Expand Up @@ -991,8 +993,6 @@ class State(_DCBase):
"""Status of the unit."""
workload_version: str = ""
"""Workload version."""
cloud_spec: Optional[CloudSpec] = None
"""Cloud specification information (metadata) including credentials."""

def __post_init__(self):
for name in ["app_status", "unit_status"]:
Expand Down
5 changes: 3 additions & 2 deletions tests/test_consistency_checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,7 @@ def test_networks_consistency():

def test_cloudspec_consistency():
cloud_spec = CloudSpec(
name="localhost",
type="lxd",
endpoint="https://127.0.0.1:8443",
credential=CloudCredential(
Expand All @@ -588,7 +589,7 @@ def test_cloudspec_consistency():
)

assert_consistent(
State(cloud_spec=cloud_spec, model=Model(name="lxd-model", type="lxd")),
State(model=Model(name="lxd-model", type="lxd", cloud_spec=cloud_spec)),
Event("start"),
_CharmSpec(
MyCharm,
Expand All @@ -597,7 +598,7 @@ def test_cloudspec_consistency():
)

assert_inconsistent(
State(cloud_spec=cloud_spec, model=Model(name="k8s-model", type="kubernetes")),
State(model=Model(name="k8s-model", type="kubernetes", cloud_spec=cloud_spec)),
Event("start"),
_CharmSpec(
MyCharm,
Expand Down
18 changes: 15 additions & 3 deletions tests/test_e2e/test_cloud_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,11 @@ def test_get_cloud_spec():
},
),
)
ctx = scenario.Context(MyCharm, meta={"name": "foo"})
ctx = scenario.Context(MyCharm, meta={"name": "foo"}, app_trusted=True)
state = scenario.State(
cloud_spec=scenario_cloud_spec,
model=scenario.Model(name="lxd-model", type="lxd"),
model=scenario.Model(
name="lxd-model", type="lxd", cloud_spec=scenario_cloud_spec
),
)
with ctx.manager("start", state=state) as mgr:
assert mgr.charm.model.get_cloud_spec() == expected_cloud_spec
Expand All @@ -56,3 +57,14 @@ def test_get_cloud_spec_error():
with ctx.manager("start", state) as mgr:
with pytest.raises(ops.ModelError):
mgr.charm.model.get_cloud_spec()


def test_get_cloud_spec_untrusted():
cloud_spec = ops.CloudSpec(type="lxd", name="localhost")
ctx = scenario.Context(MyCharm, meta={"name": "foo"})
state = scenario.State(
model=scenario.Model(name="lxd-model", type="lxd", cloud_spec=cloud_spec),
)
with ctx.manager("start", state) as mgr:
with pytest.raises(ops.ModelError):
mgr.charm.model.get_cloud_spec()

0 comments on commit f1f58e5

Please sign in to comment.