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

feat: add support for Pebble custom notices #108

Merged
merged 20 commits into from
Jun 11, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
51 changes: 51 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -714,6 +714,57 @@ def test_pebble_exec():
)
```

### Pebble Notices

Pebble can generate notices, which Juju will detect, and wake up the charm to
let it know that something has happened in the container. The most common
use-case is Pebble custom notices, which is a mechanism for the workload
application to trigger a charm event.

When the charm is notified, there might be a queue of existing notices, or just
the one that has triggered the event:

```python
import ops
import scenario

class MyCharm(ops.CharmBase):
def __init__(self, framework):
super().__init__(framework)
framework.observe(self.on["cont"].pebble_custom_notice, self._on_notice)

def _on_notice(self, event):
event.notice.key # == "example.com/c"
for notice in self.unit.get_container("cont").get_notices():
...

ctx = scenario.Context(MyCharm, meta={"name": "foo", "containers": "cont": {}})
tonyandrewmeyer marked this conversation as resolved.
Show resolved Hide resolved
notices = [
scenario.PebbleNotice(key="example.com/a", occurences=10),
PietroPasotti marked this conversation as resolved.
Show resolved Hide resolved
scenario.PebbleNotice(key="example.com/b", last_data={"bar": "baz"}),
scenario.PebbleNotice(key="example.com/c"),
]
cont = scenario.Container(notices=notices)
ctx.run(cont.notice_event, scenario.State(containers=[cont]))
```

Note that the `custom_notice_event` is accessed via the container, not the notice,
and is always for the last notice in the list. An `ops.pebble.Notice` does not
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the "is always for the last notice in the list" feels quite arbitrary, can you explain the reason for this choice?

Suppose I want to write a test and parametrize on a list of notices to verify that whatever notice fires first, the charm does X. Would I need to reorder the list on each iteration? Feels like an ugly test.

How about:

"custom_notice_event is by default for the last notice in the list; if you want a different one, you can pass it to the event as
cont.custom_notice_event(notice=my_notice)

(see 'relation-joined' for an event using a similar pattern (which tbh I'm not super happy about, but it turns out sometimes it's handy to parametrize after an event instance has already been generated))

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if life wouldn't be simpler if we grabbed the event from the notice itself, like:

event=scenario.PebbleNotice("foo").event

and have the consistency checker verify that the notice attached to the event is in some container in the state.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can see why Tony chose this -- it's not really arbitrary, as it's kind of how Pebble notices works: when a notice is recorded, the one you get the event about is always the last one in the pebble notices list (most recent). I agree it's a bit implicit, but it seems reasonable give how notices work.

Alternatively we would have to pass the event arg (choose one of Pietro's suggestions above, or do it however we end up doing such things in the v7 Scenario API).

I think we could start with Tony's reasonable default of using the last one. Put it this way: it would be unreasonable for the event to be about anything other than the last (most recent) notice.

Copy link
Collaborator Author

@tonyandrewmeyer tonyandrewmeyer Apr 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Edit-to-add: this was cross-posted with Ben's comment above, which is why it overlaps a bit.

the "is always for the last notice in the list" feels quite arbitrary, can you explain the reason for this choice?

I got boxed in by the notice needing to know which container it is from and the container wanting to have a list of notices it had, tried a few different approaches and this felt least bad (but still not ideal).

I think this would normally be the case in Juju/Pebble. I believe notices are written to storage, essentially appending them to a (per-container) list. When Juju picks that up and decides if it needs to fire a notice event it would have already handled most of the notices, and then fire off the new one.

However, I think this does fall down if there are multiple new events since Juju last processed the list. Maybe also if an event repeats - I'm not sure if that ends up last in the list or keeps its place but adjusts the count/times.

How about:

"custom_notice_event is by default for the last notice in the list; if you want a different one, you can pass it to the event as cont.custom_notice_event(notice=my_notice)

I did consider this, but all of the obj.[type]_event sugar are currently properties, and so we'd have to have Container.custom_notice_event be a regular method instead, and it feels wrong that it's inconsistent with the rest (but see below).

I could make the notice an Event attribute (that's how I started out, actually), so that you could do:

notice = scenario.PebbleNotice("example.com/path")
container = scenario.Container("mycontainer", notices=[notice])
evt = scenario.Event("mycontainer-pebble-custom-notice", container=container, notice=notice)
ctx.run(evt, state)

This would mean no sugar at all if you want a different notice, but it would at least be possible without reordering the list of notices in the container.

(see 'relation-joined' for an event using a similar pattern (which tbh I'm not super happy about, but it turns out sometimes it's handy to parametrize after an event instance has already been generated))

Ah, I missed the magic of how this happens (the Event.__call__ that lets you recreate the object). That's a neat trick 😄. So it could actually behave like both a property and a method. It is consistent with relation-joined, which is good.

I'm wondering if life wouldn't be simpler if we grabbed the event from the notice itself, like:

event=scenario.PebbleNotice("foo").event

and have the consistency checker verify that the notice attached to the event is in some container in the state.

I did start out with the event sugar property being on the notice itself, which does feel the most natural (and consistent). I then ran into trouble because the notice needs to know which container it's in to be able to snapshot itself. I tried setting that behind the scenes (with a post-init) when the notice was added to a container, but that ended up pretty messy.

You can do this:

container = scenario.Container("mycontainer")
notice = scenario.PebbleNotice("example.com/path", container=container)
container.notices.append(notice)

But that feels like we are violating the "treat these as immutable" guideline.

The Event needs to know the container that the notice is from in order to be able to write the snapshot (in deferred). The event doesn't know anything about the state, so can't really search for the notice in the state's containers. I couldn't figure a way around this, other than either setting the container on the notice (either explicitly or implicitly) or by having the event created off the container.

I think the Container.custom_notice_event being the last in the list and Container.custom_notice_event(notice=x) is the best option. I'll wait to see if @benhoyt has thoughts on this, but otherwise do that.

I also wonder if it should be Container.notice_event rather than custom_notice_event.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the Container.custom_notice_event being the last in the list and Container.custom_notice_event(notice=x) is the best option. I'll wait to see if @benhoyt has thoughts on this, but otherwise do that.

I agree with that -- that seems the nicest. And I think it's in line with the 7.0 discussion we had?

I also wonder if it should be Container.notice_event rather than custom_notice_event.

Oh, yes, good point -- because it could be any notice type (eg: change-update in the near future).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 from me for the Container.notice_event(notice: [str | PebbleNotice] = None) signature.

The default 'pick the last notice' behaviour on notice=None is still bugging me though.
I think it's unreasonable to expect that people will know how the pebble implementation works (i.e. that the last notice will be picked). I see why the default, but I expect most users will be surprised by this and if there's multiple notices in play, they'd have to dig through the documentation to figure out which notice will be triggered.

Thoughts on how we could mitigate:

# API:
class _BoundNotice:
    event: scenario.Event
    
notice: _BoundNotice = Container.get_notice(name)
last_notice: _BoundNotice = Container.last_notice

# usage:
container = scenario.Container(notices=...)
ctx.run(container.get_notice("canonical.com/foo").event, State())
ctx.run(container.last_notice.event, State())

This way it's always transparent which notice you are referring to.

Side-thought: can't help but notice (pun intended) how this fights with the proposal you had of turning all State data structures into mappings. If Container.notices were a mapping, it'd be weird to refer to its ordering.
Then we could just as well make the notice arg mandatory from the start

class Container(...):
    def notice_event(self, notice:str|PebbleNotice) -> Event: ...
    
container = Container(notices={'canonical.com/foo': foo})
container.notice_event('canonical.com/foo') 

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default 'pick the last notice' behaviour on notice=None is still bugging me though. I think it's unreasonable to expect that people will know how the pebble implementation works (i.e. that the last notice will be picked). I see why the default, but I expect most users will be surprised by this and if there's multiple notices in play, they'd have to dig through the documentation to figure out which notice will be triggered.

Fair enough. I have never loved it either, just found it the least unlikable option 😄.

I think that if notices were more tightly a Juju concept, then when Juju fired pebble-custom-notice you'd get a view of the state that only had the notices that already existed before the event.notice, and I think in this case it's ok - it's similar to the way that you don't put a charm secret in the state if the charm has no access to it, because from the charm's point of view, it doesn't know anything about it. Notices in the future haven't happened yet (from the point of view of the charm's event handler) so they shouldn't be included in the notices list, and the last one in the list is always the one you're hearing about.

However, Juju and Pebble operate pretty independently, so that's not how things will really happen. In practice, you could easily have a notice and then Juju takes a while to fire off the pebble-custom-notice event and in the meantime there have been x other notices as well. The charm needs to know how to handle this, which means that Scenario needs to be able to model it as well.

I talked this over with @benhoyt today and we agreed with your conclusion above, so let's find something better.

Thoughts on how we could mitigate:

# API:
class _BoundNotice:
    event: scenario.Event
    
notice: _BoundNotice = Container.get_notice(name)
last_notice: _BoundNotice = Container.last_notice

# usage:
container = scenario.Container(notices=...)
ctx.run(container.get_notice("canonical.com/foo").event, State())
ctx.run(container.last_notice.event, State())

This way it's always transparent which notice you are referring to.

Making it explicit works I think. I think it would be bound to a container not an event though? Like:

class _ContainedNotice:
    container: Container
    notice: Notice
    @property
    def event(self) -> Event:
        return Event(..., container=self.container, notice=self.notice)

class Container(...):
    def get_notice(key) -> _ContainedNotice:
        return _ContainedNotice(self, self.notices[key])

# usage
container = scenario.Container(notices=...)
ctx.run(container.get_notice("canonical.com/foo").event, state)

# usage after change updated is added
ctx.run(container.get_notice("123", type=ops.pebble.NoticeType.CHANGE_UPDATE).event, state)

If it's bound to an event, then get_notice has to create an event, which seems a bit odd, and not like e.g. get_container. It's a little more verbose than ctx.run(container.notice_event, state) but not too much, and way more clear about what's happening, and doesn't require the notice_event(notice=x) overload, which is probably good, on balance.

I think we could even leave off the last_notice shortcut until we see people actually needing it.

Side-thought: can't help but notice (pun intended) how this fights with the proposal you had of turning all State data structures into mappings.

I will get back to that as soon as I have a chance, by the way, and have been mulling it over. For this PR I was trying to match the existing Scenario API (assuming that this could make it into 6.x) rather than align with whatever might come in 7.0 (and I was assuming that whatever that ends up being would be generic enough that it could be applied to notices in the same way as containers, secrets, storages, etc).

The main concerns that lead to the 'use mappings' proposal were (a) lists put order into things that have no natural order and following on from that (b) it leads to a bunch of "find the thing I want by number" where the number is "magic" because, from the first point, there is no natural order.

Notices do have a natural order, although I think you're probably meant to ignore that or do sorting yourself based on one of the attributes.

If Container.notices were a mapping, it'd be weird to refer to its ordering.

This is a fair point, yes.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice! I like where this is going.
+1 for this approach from my side.

know which container it is in, but an `ops.PebbleCustomNoticeEvent` does know
which container did the notifying. If you need to generate an event for a different
notice, you can override the notice:

```python
ctx = scenario.Context(MyCharm, meta={"name": "foo", "containers": "cont": {}})
notices = [
scenario.PebbleNotice(key="example.com/a", occurences=10),
scenario.PebbleNotice(key="example.com/b", last_data={"bar": "baz"}),
scenario.PebbleNotice(key="example.com/c"),
]
cont = scenario.Container(notices=notices)
ctx.run(cont.notice_event(notice=notices[0]), scenario.State(containers=[cont]))
```

## Storage

If your charm defines `storage` in its metadata, you can use `scenario.Storage` to instruct Scenario to make (mocked) filesystem storage available to the charm at runtime.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ license.text = "Apache-2.0"
keywords = ["juju", "test"]

dependencies = [
"ops>=2.6",
"ops>=2.10",
"PyYAML>=6.0.1",
]
readme = "README.md"
Expand Down
2 changes: 2 additions & 0 deletions scenario/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
Model,
Mount,
Network,
PebbleNotice,
PeerRelation,
Port,
Relation,
Expand Down Expand Up @@ -41,6 +42,7 @@
"ExecOutput",
"Mount",
"Container",
"PebbleNotice",
"Address",
"BindAddress",
"Network",
Expand Down
14 changes: 10 additions & 4 deletions scenario/consistency_checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -513,14 +513,15 @@ def check_containers_consistency(
meta = charm_spec.meta
meta_containers = list(map(normalize_name, meta.get("containers", {})))
state_containers = [normalize_name(c.name) for c in state.containers]
all_notices = [notice.id for c in state.containers for notice in c.notices]
errors = []

# it's fine if you have containers in meta that are not in state.containers (yet), but it's
# not fine if:
# - you're processing a pebble-ready event and that container is not in state.containers or
# - you're processing a Pebble event and that container is not in state.containers or
# meta.containers
if event._is_workload_event:
evt_container_name = event.name[: -len("-pebble-ready")]
evt_container_name = event.name.split("_pebble_")[0]
if evt_container_name not in meta_containers:
errors.append(
f"the event being processed concerns container {evt_container_name!r}, but a "
Expand All @@ -529,8 +530,13 @@ def check_containers_consistency(
if evt_container_name not in state_containers:
errors.append(
f"the event being processed concerns container {evt_container_name!r}, but a "
f"container with that name is not present in the state. It's odd, but consistent, "
f"if it cannot connect; but it should at least be there.",
f"container with that name is not present in the state. It's odd, but "
f"consistent, if it cannot connect; but it should at least be there.",
PietroPasotti marked this conversation as resolved.
Show resolved Hide resolved
)
if event.notice and event.notice.id not in all_notices:
errors.append(
f"the event being processed concerns notice {event.notice!r}, but that "
"notice is not in any of the containers present in the state.",
)

# - a container in state.containers is not in meta.containers
Expand Down
6 changes: 6 additions & 0 deletions scenario/mocking.py
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,12 @@ def __init__(

self._root = container_root

# load any existing notices from the state
self._notices: Dict[Tuple[str, str], pebble.Notice] = {}
for container in state.containers:
for notice in container.notices:
self._notices[str(notice.type), notice.key] = notice._to_ops_notice()

def get_plan(self) -> pebble.Plan:
return self._container.plan

Expand Down
9 changes: 9 additions & 0 deletions scenario/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,15 @@ def _get_event_env(self, state: "State", event: "Event", charm_root: Path):
if container := event.container:
env.update({"JUJU_WORKLOAD_NAME": container.name})

if notice := event.notice:
env.update(
{
"JUJU_NOTICE_ID": notice.id,
"JUJU_NOTICE_TYPE": str(notice.type),
"JUJU_NOTICE_KEY": notice.key,
},
)

if storage := event.storage:
env.update({"JUJU_STORAGE_ID": f"{storage.name}/{storage.index}"})

Expand Down
114 changes: 113 additions & 1 deletion scenario/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
"collect_unit_status",
}
PEBBLE_READY_EVENT_SUFFIX = "_pebble_ready"
PEBBLE_CUSTOM_NOTICE_EVENT_SUFFIX = "_pebble_custom_notice"
RELATION_EVENTS_SUFFIX = {
"_relation_changed",
"_relation_broken",
Expand Down Expand Up @@ -609,6 +610,80 @@ class Mount(_DCBase):
src: Union[str, Path]


def _now_utc():
return datetime.datetime.now(tz=datetime.timezone.utc)


_next_notice_id_counter = 1


def next_notice_id(update=True):
tonyandrewmeyer marked this conversation as resolved.
Show resolved Hide resolved
global _next_notice_id_counter
cur = _next_notice_id_counter
if update:
_next_notice_id_counter += 1
return str(cur)


@dataclasses.dataclass(frozen=True)
class PebbleNotice(_DCBase):
key: str
"""The notice key, a string that differentiates notices of this type.

This is in the format ``domain/path``; for example:
``canonical.com/postgresql/backup`` or ``example.com/mycharm/notice``.
"""

id: str = dataclasses.field(default_factory=next_notice_id)
"""Unique ID for this notice."""

user_id: Optional[int] = None
"""UID of the user who may view this notice (None means notice is public)."""

type: Union[pebble.NoticeType, str] = pebble.NoticeType.CUSTOM
"""Type of the notice."""

first_occurred: datetime.datetime = dataclasses.field(default_factory=_now_utc)
"""The first time one of these notices (type and key combination) occurs."""

last_occurred: datetime.datetime = dataclasses.field(default_factory=_now_utc)
"""The last time one of these notices occurred."""

last_repeated: datetime.datetime = dataclasses.field(default_factory=_now_utc)
"""The time this notice was last repeated.

See Pebble's `Notices documentation <https://github.com/canonical/pebble/#notices>`_
for an explanation of what "repeated" means.
"""

occurrences: int = 1
"""The number of times one of these notices has occurred."""

last_data: Dict[str, str] = dataclasses.field(default_factory=dict)
tonyandrewmeyer marked this conversation as resolved.
Show resolved Hide resolved
"""Additional data captured from the last occurrence of one of these notices."""

repeat_after: Optional[datetime.timedelta] = None
"""Minimum time after one of these was last repeated before Pebble will repeat it again."""

expire_after: Optional[datetime.timedelta] = None
"""How long since one of these last occurred until Pebble will drop the notice."""

def _to_ops_notice(self) -> pebble.Notice:
return pebble.Notice(
id=self.id,
user_id=self.user_id,
type=self.type,
key=self.key,
first_occurred=self.first_occurred,
last_occurred=self.last_occurred,
last_repeated=self.last_repeated,
occurrences=self.occurrences,
last_data=self.last_data,
repeat_after=self.repeat_after,
expire_after=self.expire_after,
)


@dataclasses.dataclass(frozen=True)
class Container(_DCBase):
name: str
Expand Down Expand Up @@ -646,6 +721,8 @@ class Container(_DCBase):

exec_mock: _ExecMock = dataclasses.field(default_factory=dict)

notices: List[PebbleNotice] = dataclasses.field(default_factory=list)

def _render_services(self):
# copied over from ops.testing._TestingPebbleClient._render_services()
services = {} # type: Dict[str, pebble.Service]
Expand Down Expand Up @@ -713,6 +790,27 @@ def pebble_ready_event(self):
)
return Event(path=normalize_name(self.name + "-pebble-ready"), container=self)

@property
def notice_event(self):
"""Sugar to generate a <this container's name>-pebble-custom-notice event for the latest notice."""
if not self.notices:
raise RuntimeError("This container does not have any notices.")
# We assume this event is about the most recent notice.
notice = self.notices[-1]
if notice.type != pebble.NoticeType.CUSTOM:
raise RuntimeError("Scenario only knows about custom notices at this time.")
suffix = PEBBLE_CUSTOM_NOTICE_EVENT_SUFFIX.replace("_", "-")
if not self.can_connect:
logger.warning(
"you **can** fire pebble-custom-notice while the container cannot connect, "
"but that's most likely not what you want.",
)
return Event(
path=normalize_name(self.name + suffix),
container=self,
notice=notice,
)


_RawStatusLiteral = Literal[
"waiting",
Expand Down Expand Up @@ -1191,6 +1289,8 @@ def _get_suffix_and_type(s: str) -> Tuple[str, _EventType]:
# Whether the event name indicates that this is a workload event.
if s.endswith(PEBBLE_READY_EVENT_SUFFIX):
return PEBBLE_READY_EVENT_SUFFIX, _EventType.workload
if s.endswith(PEBBLE_CUSTOM_NOTICE_EVENT_SUFFIX):
return PEBBLE_CUSTOM_NOTICE_EVENT_SUFFIX, _EventType.workload

if s in BUILTIN_EVENTS:
return "", _EventType.builtin
Expand All @@ -1217,6 +1317,9 @@ class Event(_DCBase):
# if this is a workload (container) event, the container it refers to
container: Optional[Container] = None

# if this is a Pebble notice event, the notice it refers to
notice: Optional[PebbleNotice] = None
tonyandrewmeyer marked this conversation as resolved.
Show resolved Hide resolved

# if this is an action event, the Action instance
action: Optional["Action"] = None

Expand Down Expand Up @@ -1397,6 +1500,14 @@ def deferred(self, handler: Callable, event_id: int = 1) -> DeferredEvent:
snapshot_data = {
"container_name": container.name,
}
if self.notice:
snapshot_data.update(
{
"notice_id": self.notice.id,
"notice_key": self.notice.key,
"notice_type": str(self.notice.type),
},
)

elif self._is_relation_event:
# this is a RelationEvent.
Expand Down Expand Up @@ -1460,8 +1571,9 @@ def deferred(
event_id: int = 1,
relation: Optional["Relation"] = None,
container: Optional["Container"] = None,
notice: Optional["PebbleNotice"] = None,
):
"""Construct a DeferredEvent from an Event or an event name."""
if isinstance(event, str):
event = Event(event, relation=relation, container=container)
event = Event(event, relation=relation, container=container, notice=notice)
return event.deferred(handler=handler, event_id=event_id)
10 changes: 10 additions & 0 deletions tests/test_consistency_checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,16 @@ def test_workload_event_without_container():
Event("foo-pebble-ready", container=Container("foo")),
_CharmSpec(MyCharm, {"containers": {"foo": {}}}),
)
assert_inconsistent(
State(),
Event("foo-pebble-custom-notice", container=Container("foo")),
_CharmSpec(MyCharm, {}),
)
assert_consistent(
State(containers=[Container("foo")]),
Event("foo-pebble-custom-notice", container=Container("foo")),
_CharmSpec(MyCharm, {"containers": {"foo": {}}}),
)


def test_container_meta_mismatch():
Expand Down
23 changes: 22 additions & 1 deletion tests/test_e2e/test_deferred.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,14 @@
from ops.framework import Framework

from scenario import Context
from scenario.state import Container, DeferredEvent, Relation, State, deferred
from scenario.state import (
Container,
DeferredEvent,
PebbleNotice,
Relation,
State,
deferred,
)
from tests.helpers import trigger

CHARM_CALLED = 0
Expand Down Expand Up @@ -97,6 +104,20 @@ def test_deferred_workload_evt(mycharm):
assert asdict(evt2) == asdict(evt1)


def test_deferred_notice_evt(mycharm):
notice = PebbleNotice(key="example.com/bar")
ctr = Container("foo", notices=[notice])
evt1 = ctr.notice_event.deferred(handler=mycharm._on_event)
evt2 = deferred(
event="foo_pebble_custom_notice",
handler=mycharm._on_event,
container=ctr,
notice=notice,
)

assert asdict(evt2) == asdict(evt1)


def test_deferred_relation_event(mycharm):
mycharm.defer_next = 2

Expand Down
2 changes: 2 additions & 0 deletions tests/test_e2e/test_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
("foo_bar_baz_storage_detaching", _EventType.storage),
("foo_pebble_ready", _EventType.workload),
("foo_bar_baz_pebble_ready", _EventType.workload),
("foo_pebble_custom_notice", _EventType.workload),
("foo_bar_baz_pebble_custom_notice", _EventType.workload),
("secret_removed", _EventType.secret),
("pre_commit", _EventType.framework),
("commit", _EventType.framework),
Expand Down
Loading
Loading