Skip to content
Open
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
2 changes: 2 additions & 0 deletions testing/src/scenario/_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
PeerRelation,
Relation,
SubordinateRelation,
_strip_juju4_defaults,
)

if TYPE_CHECKING: # pragma: no cover
Expand Down Expand Up @@ -313,6 +314,7 @@ def exec(

# we make a copy to avoid mutating the input state
output_state = copy.deepcopy(state)
_strip_juju4_defaults(output_state, self._juju_version)

logger.info(' - generating virtual charm root')
with self._virtual_charm_root() as temporary_charm_root:
Expand Down
23 changes: 23 additions & 0 deletions testing/src/scenario/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -716,12 +716,35 @@ def _validate_databag(self, databag: dict[str, str]):


_DEFAULT_IP = '192.0.2.0'
# Juju 3.x populates `private-address` in every relation databag, but Juju 4
# does not. The dataclass `default_factory` runs at `Relation()` construction
# time, before the test's `juju_version` is known, so we always include
# `private-address` here and strip it later in `_strip_juju4_defaults` if the
# `Context.juju_version` is 4.0 or later.
_DEFAULT_JUJU_DATABAG = {
'egress-subnets': _DEFAULT_IP,
'ingress-address': _DEFAULT_IP,
'private-address': _DEFAULT_IP,
}

# Keys in `_DEFAULT_JUJU_DATABAG` that Juju 4 no longer auto-populates.
_JUJU_4_REMOVED_DATABAG_KEYS = ('private-address',)


def _strip_juju4_defaults(state: State, juju_version: str) -> None:
Comment thread
tonyandrewmeyer marked this conversation as resolved.
"""Strip Juju-3-only default databag keys from `state` for Juju 4+.

Mutates the databag dicts in place; only keys still set to the default
sentinel IP are removed, so explicit user-set values are preserved.
"""
if ops.JujuVersion(juju_version).major < 4:
return
for relation in state.relations:
for databag in relation._databags:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

My review agent tells me that this would remove private-address from local_app_data and remote_app_data - if someone has set private-address to the default value in those databags. I feel like we're safe to assume nobody would ever do that... What do you think?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I did wonder about this. There's some chance a charm could do this to make their Juju 4 databag look like a Juju 3 one. I'd argue that it's not a good idea, but it's not impossible.

If someone does that their tests would break and they wouldn't be able to get around that.

The only ideas I have for avoiding that are ugly (like a special object we set instead, which we can recognise later, but pretends to be the normal one when people work with their state).

I think this comes down to a decision about whether we want to have a fix in Scenario 8 so the Juju 4 behaviour is surfaced, only have a fix with something opt-in (where we could do this more cleanly), or only fix it in 9.0 (definitely could do it clean, but people's tests may need updates). Or someone comes up with an idea I haven't thought of, of course.

I lean towards what's in the PR, but I can definitely be convinced we should go another way.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm OK with what's in this PR. My knowledge is a bit limited here, so no objection if you and James decide to go in a different direction.

for key in _JUJU_4_REMOVED_DATABAG_KEYS:
if databag.get(key) == _DEFAULT_IP:
del databag[key]


@dataclasses.dataclass(frozen=True, kw_only=True)
class Relation(RelationBase):
Expand Down
64 changes: 64 additions & 0 deletions testing/tests/test_e2e/test_relations.py
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,70 @@ def test_relation_default_unit_data_peer():
assert relation.local_unit_data == _DEFAULT_JUJU_DATABAG


@pytest.mark.parametrize(
'juju_version,expect_private_address',
[('3.6.14', True), ('4.0.0', False), ('4.1.0', False)],
)
def test_private_address_default_dropped_on_juju_4(
juju_version: str, expect_private_address: bool
):
"""Juju 4 no longer auto-populates `private-address` in relation databags.

Verify the output state reflects that — the default key is preserved on
Juju 3, and stripped on Juju 4+.
"""
ctx = Context(
Charm,
meta={
'name': 'foo',
'requires': {'foo': {'interface': 'foo'}},
'peers': {'p': {'interface': 'p'}},
'provides': {'sub': {'interface': 'sub', 'scope': 'container'}},
},
juju_version=juju_version,
)
relation = Relation('foo')
peer = PeerRelation('p')
sub = SubordinateRelation('sub')
state_in = State(leader=True, relations={relation, peer, sub})

state_out = ctx.run(ctx.on.start(), state_in)

rel = state_out.get_relation(relation.id)
peer_rel = state_out.get_relation(peer.id)
sub_rel = state_out.get_relation(sub.id)
assert isinstance(rel, Relation)
assert isinstance(peer_rel, PeerRelation)
assert isinstance(sub_rel, SubordinateRelation)
assert ('private-address' in rel.local_unit_data) is expect_private_address
assert ('private-address' in rel.remote_units_data[0]) is expect_private_address
assert ('private-address' in peer_rel.local_unit_data) is expect_private_address
assert ('private-address' in sub_rel.local_unit_data) is expect_private_address
assert ('private-address' in sub_rel.remote_unit_data) is expect_private_address
Comment thread
dwilding marked this conversation as resolved.
# `egress-subnets` and `ingress-address` are still set on Juju 4.
assert rel.local_unit_data['ingress-address'] == '192.0.2.0'
assert rel.local_unit_data['egress-subnets'] == '192.0.2.0'


def test_private_address_explicit_value_preserved_on_juju_4():
"""Only the default sentinel IP is stripped — explicit user values stay."""
ctx = Context(
Charm,
meta={'name': 'foo', 'requires': {'foo': {'interface': 'foo'}}},
juju_version='4.0.0',
)
relation = Relation(
'foo',
local_unit_data={'private-address': '10.0.0.5', 'extra': 'kept'},
)
state_out = ctx.run(ctx.on.start(), State(leader=True, relations={relation}))

rel = state_out.get_relation(relation.id)
assert isinstance(rel, Relation)
assert rel.local_unit_data['private-address'] == '10.0.0.5'
assert rel.local_unit_data['extra'] == 'kept'


@pytest.mark.parametrize('evt_name', ('broken', 'created'))
def test_relation_events_no_remote_units(evt_name: str, caplog: pytest.LogCaptureFixture):
relation = Relation(
Expand Down
Loading