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 all 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)
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()
Loading