diff --git a/README.md b/README.md index 21939ae7..6a2cf135 100644 --- a/README.md +++ b/README.md @@ -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), ) ``` diff --git a/scenario/consistency_checker.py b/scenario/consistency_checker.py index c8aebbf9..584bad67 100644 --- a/scenario/consistency_checker.py +++ b/scenario/consistency_checker.py @@ -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) diff --git a/scenario/context.py b/scenario/context.py index 9de78b1c..a3e2a8ea 100644 --- a/scenario/context.py +++ b/scenario/context.py @@ -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. @@ -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.: @@ -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. diff --git a/scenario/mocking.py b/scenario/mocking.py index 1081b4ac..a55397d2 100644 --- a/scenario/mocking.py +++ b/scenario/mocking.py @@ -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): diff --git a/scenario/state.py b/scenario/state.py index a36a7a0a..c0d573fa 100644 --- a/scenario/state.py +++ b/scenario/state.py @@ -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. @@ -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 @@ -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"]: diff --git a/tests/test_consistency_checker.py b/tests/test_consistency_checker.py index d5929d98..78c85102 100644 --- a/tests/test_consistency_checker.py +++ b/tests/test_consistency_checker.py @@ -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( @@ -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, @@ -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, diff --git a/tests/test_e2e/test_cloud_spec.py b/tests/test_e2e/test_cloud_spec.py index 357061d2..8ce413f8 100644 --- a/tests/test_e2e/test_cloud_spec.py +++ b/tests/test_e2e/test_cloud_spec.py @@ -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 @@ -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()