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

[DPE-2624] Authentication Changed event #96

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ jobs:
- juju-bootstrap-option: "2.9.44"
juju-snap-channel: "2.9/stable"
libjuju-version: "2.9.42.4"
- juju-bootstrap-option: "3.1.5"
juju-snap-channel: "3.1/stable"
- juju-bootstrap-option: "3.1.6"
juju-snap-channel: "3.1/edge"
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Using edge on the pipelines to avoid https://bugs.launchpad.net/juju/+bug/2031631

libjuju-version: "3.2.0.1"
name: ${{ matrix.tox-environments }} Juju ${{ matrix.juju-version.juju-snap-channel}} -- libjuju ${{ matrix.juju-version.libjuju-version }}
needs:
Expand Down
277 changes: 138 additions & 139 deletions lib/charms/data_platform_libs/v0/data_interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -898,10 +898,122 @@ def set_tls_ca(self, relation_id: int, tls_ca: str) -> None:
self.update_relation_data(relation_id, {"tls-ca": tls_ca})


# Authentication event


class AuthenticationEvent(RelationEvent):
"""Base class for authentication fields for events.

The amount of logic added here is not ideal -- but this was the only way to preserve
the interface when moving to Juju Secrets
"""

@property
def _secrets(self) -> dict:
"""Caching secrets to avoid fetching them each time a field is referrd.

DON'T USE the encapsulated helper variable outside of this function
"""
if not hasattr(self, "_cached_secrets"):
self._cached_secrets = {}
return self._cached_secrets

@property
def _jujuversion(self) -> JujuVersion:
"""Caching jujuversion to avoid a Juju call on each field evaluation.

DON'T USE the encapsulated helper variable outside of this function
"""
if not hasattr(self, "_cached_jujuversion"):
self._cached_jujuversion = None
if not self._cached_jujuversion:
self._cached_jujuversion = JujuVersion.from_environ()
return self._cached_jujuversion

def _get_secret(self, group) -> Optional[Dict[str, str]]:
"""Retrieveing secrets."""
if not self.app:
return
if not self._secrets.get(group):
self._secrets[group] = None
secret_field = f"{PROV_SECRET_PREFIX}{group}"
if secret_uri := self.relation.data[self.app].get(secret_field):
secret = self.framework.model.get_secret(id=secret_uri)
self._secrets[group] = secret.get_content()
return self._secrets[group]

@property
def secrets_enabled(self):
"""Is this Juju version allowing for Secrets usage?"""
return self._jujuversion.has_secrets

@property
def username(self) -> Optional[str]:
"""Returns the created username."""
if not self.relation.app:
return None

if self.secrets_enabled:
secret = self._get_secret("user")
if secret:
return secret.get("username")

return self.relation.data[self.relation.app].get("username")

@property
def password(self) -> Optional[str]:
"""Returns the password for the created user."""
if not self.relation.app:
return None

if self.secrets_enabled:
secret = self._get_secret("user")
if secret:
return secret.get("password")

return self.relation.data[self.relation.app].get("password")

@property
def tls(self) -> Optional[str]:
"""Returns whether TLS is configured."""
if not self.relation.app:
return None

if self.secrets_enabled:
secret = self._get_secret("tls")
if secret:
return secret.get("tls")

return self.relation.data[self.relation.app].get("tls")

@property
def tls_ca(self) -> Optional[str]:
"""Returns TLS CA."""
if not self.relation.app:
return None

if self.secrets_enabled:
secret = self._get_secret("tls")
if secret:
return secret.get("tls-ca")

return self.relation.data[self.relation.app].get("tls-ca")


class DataRequiresEvents(CharmEvents):
"""Database events.

This class defines the events that the database can emit.
"""

authentication_updated = EventSource(AuthenticationEvent)


class DataRequires(DataRelation):
"""Requires-side of the relation."""

SECRET_FIELDS = ["username", "password", "tls", "tls-ca", "uris"]
on = DataRequiresEvents() # pyright: ignore [reportGeneralTypeIssues]

def __init__(
self,
Expand Down Expand Up @@ -1049,10 +1161,30 @@ def _on_relation_created_event(self, event: RelationCreatedEvent) -> None:
event.relation, self.charm.app, REQ_SECRET_FIELDS, self.secret_fields
)

@abstractmethod
def _on_secret_changed_event(self, event: RelationChangedEvent) -> None:
def _on_secret_changed_event(self, event: SecretChangedEvent) -> None:
"""Event emitted when the relation data has changed."""
raise NotImplementedError
if not event.secret.label:
return

relation = self._relation_from_secret_label(event.secret.label)
if not relation:
logging.info(
f"Received secret {event.secret.label} but couldn't parse, seems irrelevant"
)
return

if relation.app == self.charm.app:
logging.info("Secret changed event ignored for Secret Owner")

remote_unit = None
for unit in relation.units:
if unit.app != self.charm.app:
remote_unit = unit

logger.info("authentication updated")
getattr(self.on, "authentication_updated").emit(
relation, app=relation.app, unit=remote_unit
)

# Mandatory internal overrides

Expand Down Expand Up @@ -1163,105 +1295,6 @@ def extra_user_roles(self) -> Optional[str]:
return self.relation.data[self.relation.app].get("extra-user-roles")


class AuthenticationEvent(RelationEvent):
"""Base class for authentication fields for events.

The amount of logic added here is not ideal -- but this was the only way to preserve
the interface when moving to Juju Secrets
"""

@property
def _secrets(self) -> dict:
"""Caching secrets to avoid fetching them each time a field is referrd.

DON'T USE the encapsulated helper variable outside of this function
"""
if not hasattr(self, "_cached_secrets"):
self._cached_secrets = {}
return self._cached_secrets

@property
def _jujuversion(self) -> JujuVersion:
"""Caching jujuversion to avoid a Juju call on each field evaluation.

DON'T USE the encapsulated helper variable outside of this function
"""
if not hasattr(self, "_cached_jujuversion"):
self._cached_jujuversion = None
if not self._cached_jujuversion:
self._cached_jujuversion = JujuVersion.from_environ()
return self._cached_jujuversion

def _get_secret(self, group) -> Optional[Dict[str, str]]:
"""Retrieveing secrets."""
if not self.app:
return
if not self._secrets.get(group):
self._secrets[group] = None
secret_field = f"{PROV_SECRET_PREFIX}{group}"
if secret_uri := self.relation.data[self.app].get(secret_field):
secret = self.framework.model.get_secret(id=secret_uri)
self._secrets[group] = secret.get_content()
return self._secrets[group]

@property
def secrets_enabled(self):
"""Is this Juju version allowing for Secrets usage?"""
return self._jujuversion.has_secrets

@property
def username(self) -> Optional[str]:
"""Returns the created username."""
if not self.relation.app:
return None

if self.secrets_enabled:
secret = self._get_secret("user")
if secret:
return secret.get("username")

return self.relation.data[self.relation.app].get("username")

@property
def password(self) -> Optional[str]:
"""Returns the password for the created user."""
if not self.relation.app:
return None

if self.secrets_enabled:
secret = self._get_secret("user")
if secret:
return secret.get("password")

return self.relation.data[self.relation.app].get("password")

@property
def tls(self) -> Optional[str]:
"""Returns whether TLS is configured."""
if not self.relation.app:
return None

if self.secrets_enabled:
secret = self._get_secret("tls")
if secret:
return secret.get("tls")

return self.relation.data[self.relation.app].get("tls")

@property
def tls_ca(self) -> Optional[str]:
"""Returns TLS CA."""
if not self.relation.app:
return None

if self.secrets_enabled:
secret = self._get_secret("tls")
if secret:
return secret.get("tls-ca")

return self.relation.data[self.relation.app].get("tls-ca")


# Database related events and fields


Expand Down Expand Up @@ -1371,7 +1404,7 @@ class DatabaseReadOnlyEndpointsChangedEvent(AuthenticationEvent, DatabaseRequire
"""Event emitted when the read only endpoints are changed."""


class DatabaseRequiresEvents(CharmEvents):
class DatabaseRequiresEvents(DataRequiresEvents):
"""Database events.

This class defines the events that the database can emit.
Expand Down Expand Up @@ -1520,10 +1553,6 @@ def __init__(
DatabaseReadOnlyEndpointsChangedEvent,
)

def _on_secret_changed_event(self, event: SecretChangedEvent):
"""Event notifying about a new value of a secret."""
pass

def _assign_relation_alias(self, relation_id: int) -> None:
"""Assigns an alias to a relation.

Expand Down Expand Up @@ -1788,7 +1817,7 @@ class BootstrapServerChangedEvent(AuthenticationEvent, KafkaRequiresEvent):
"""Event emitted when the bootstrap server is changed."""


class KafkaRequiresEvents(CharmEvents):
class KafkaRequiresEvents(DataRequiresEvents):
"""Kafka events.

This class defines the events that the Kafka can emit.
Expand Down Expand Up @@ -1907,10 +1936,6 @@ def _on_relation_created_event(self, event: RelationCreatedEvent) -> None:

self.update_relation_data(event.relation.id, relation_data)

def _on_secret_changed_event(self, event: SecretChangedEvent):
"""Event notifying about a new value of a secret."""
pass

def _on_relation_changed_event(self, event: RelationChangedEvent) -> None:
"""Event emitted when the Kafka relation has changed."""
# Check which data has changed to emit customs events.
Expand Down Expand Up @@ -1982,15 +2007,14 @@ class IndexCreatedEvent(AuthenticationEvent, OpenSearchRequiresEvent):
"""Event emitted when a new index is created for use on this relation."""


class OpenSearchRequiresEvents(CharmEvents):
class OpenSearchRequiresEvents(DataRequiresEvents):
"""OpenSearch events.

This class defines the events that the opensearch requirer can emit.
"""

index_created = EventSource(IndexCreatedEvent)
endpoints_changed = EventSource(DatabaseEndpointsChangedEvent)
authentication_updated = EventSource(AuthenticationEvent)


# OpenSearch Provides and Requires Objects
Expand Down Expand Up @@ -2079,31 +2103,6 @@ def _on_relation_created_event(self, event: RelationCreatedEvent) -> None:

self.update_relation_data(event.relation.id, data)

def _on_secret_changed_event(self, event: SecretChangedEvent):
"""Event notifying about a new value of a secret."""
if not event.secret.label:
return

relation = self._relation_from_secret_label(event.secret.label)
if not relation:
logging.info(
f"Received secret {event.secret.label} but couldn't parse, seems irrelevant"
)
return

if relation.app == self.charm.app:
logging.info("Secret changed event ignored for Secret Owner")

remote_unit = None
for unit in relation.units:
if unit.app != self.charm.app:
remote_unit = unit

logger.info("authentication updated")
getattr(self.on, "authentication_updated").emit(
relation, app=relation.app, unit=remote_unit
)

def _on_relation_changed_event(self, event: RelationChangedEvent) -> None:
"""Event emitted when the OpenSearch relation has changed.

Expand Down
8 changes: 4 additions & 4 deletions tests/integration/test_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
MULTIPLE_DATABASE_CLUSTERS_RELATION_NAME = "multiple-database-clusters"
ALIASED_MULTIPLE_DATABASE_CLUSTERS_RELATION_NAME = "aliased-multiple-database-clusters"

SECRET_REF_PREFIX = "secret-"
PROV_SECRET_PREFIX = "secret-"


@pytest.mark.abort_on_fail
Expand Down Expand Up @@ -281,7 +281,7 @@ async def test_an_application_can_request_multiple_databases(ops_test: OpsTest,
@pytest.mark.usefixtures("only_with_juju_secrets")
async def test_provider_with_additional_secrets(ops_test: OpsTest, database_charm):
# Let's make sure that there was enough time for the relation initialization to communicate secrets
sleep(5)
sleep(20)
secret_fields = await get_application_relation_data(
ops_test,
DATABASE_APP_NAME,
Expand All @@ -300,7 +300,7 @@ async def test_provider_with_additional_secrets(ops_test: OpsTest, database_char

# Get secret original value
secret_uri = await get_application_relation_data(
ops_test, APPLICATION_APP_NAME, SECOND_DATABASE_RELATION_NAME, f"{SECRET_REF_PREFIX}extra"
ops_test, APPLICATION_APP_NAME, SECOND_DATABASE_RELATION_NAME, f"{PROV_SECRET_PREFIX}extra"
)

secret_content = await get_juju_secret(ops_test, secret_uri)
Expand All @@ -315,7 +315,7 @@ async def test_provider_with_additional_secrets(ops_test: OpsTest, database_char

# Get secret after change
secret_uri = await get_application_relation_data(
ops_test, APPLICATION_APP_NAME, SECOND_DATABASE_RELATION_NAME, f"{SECRET_REF_PREFIX}extra"
ops_test, APPLICATION_APP_NAME, SECOND_DATABASE_RELATION_NAME, f"{PROV_SECRET_PREFIX}extra"
)

secret_content = await get_juju_secret(ops_test, secret_uri)
Expand Down