Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor!: move the cloud specification to Model #141

Merged
merged 5 commits into from
Jun 11, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
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._state.trusted:
tonyandrewmeyer marked this conversation as resolved.
Show resolved Hide resolved
raise ModelError(
"ERROR cloud spec is empty, initialise it with `scenario.State(cloud_spec=scenario.CloudSpec(...))`",
"ERROR charm is not trusted, initialise State with 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
8 changes: 5 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 @@ -965,6 +967,8 @@ class State(_DCBase):
"""Whether this charm has leadership."""
model: Model = Model()
"""The model this charm lives in."""
trusted: bool = False
"""Whether ``juju-trust`` has been run for this charm."""
secrets: List[Secret] = dataclasses.field(default_factory=list)
"""The secrets this charm has access to (as an owner, or as a grantee).
The presence of a secret in this list entails that the charm can read it.
Expand All @@ -991,8 +995,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
17 changes: 15 additions & 2 deletions tests/test_e2e/test_cloud_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,10 @@ def test_get_cloud_spec():
)
ctx = scenario.Context(MyCharm, meta={"name": "foo"})
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
),
trusted=True,
)
with ctx.manager("start", state=state) as mgr:
assert mgr.charm.model.get_cloud_spec() == expected_cloud_spec
Expand All @@ -56,3 +58,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()
Loading