Skip to content
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
10 changes: 10 additions & 0 deletions .github/codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
github_checks:
annotations: false
Comment on lines +1 to +2
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Disable annotations

coverage:
status:
project:
default:
target: 70%
Comment on lines +6 to +7
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Overall coverage should be over 70%

patch:
default:
target: 33%
Comment on lines +9 to +10
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Commit delta should be at least 33% covered.

62 changes: 30 additions & 32 deletions lib/charms/data_platform_libs/v0/data_interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ def _on_topic_requested(self, event: TopicRequestedEvent):

# Increment this PATCH version before using `charmcraft publish-lib` or reset
# to 0 if you are raising the major API version
LIBPATCH = 26
LIBPATCH = 27

PYDEPS = ["ops>=2.0.0"]

Expand Down Expand Up @@ -422,15 +422,15 @@ def diff(event: RelationChangedEvent, bucket: Union[Unit, Application]) -> Diff:
)

# These are the keys that were added to the databag and triggered this event.
added = new_data.keys() - old_data.keys() # pyright: ignore [reportGeneralTypeIssues]
added = new_data.keys() - old_data.keys() # pyright: ignore [reportAssignmentType]
# These are the keys that were removed from the databag and triggered this event.
deleted = old_data.keys() - new_data.keys() # pyright: ignore [reportGeneralTypeIssues]
deleted = old_data.keys() - new_data.keys() # pyright: ignore [reportAssignmentType]
# These are the keys that already existed in the databag,
# but had their values changed.
changed = {
key
for key in old_data.keys() & new_data.keys() # pyright: ignore [reportGeneralTypeIssues]
if old_data[key] != new_data[key] # pyright: ignore [reportGeneralTypeIssues]
for key in old_data.keys() & new_data.keys() # pyright: ignore [reportAssignmentType]
if old_data[key] != new_data[key] # pyright: ignore [reportAssignmentType]
}
# Convert the new_data to a serializable format and save it for a next diff check.
set_encoded_field(event.relation, bucket, "data", new_data)
Expand Down Expand Up @@ -1619,7 +1619,8 @@ def _delete_relation_data(self, relation: Relation, fields: List[str]) -> None:
current_data.get(relation.id, [])
):
logger.error(
"Non-existing secret %s was attempted to be removed.", non_existent
"Non-existing secret %s was attempted to be removed.",
", ".join(non_existent),
)

_, normal_fields = self._process_secret_fields(
Expand Down Expand Up @@ -1686,12 +1687,8 @@ 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
"""
class RelationEventWithSecret(RelationEvent):
"""Base class for Relation Events that need to handle secrets."""

@property
def _secrets(self) -> dict:
Expand All @@ -1703,18 +1700,6 @@ def _secrets(self) -> dict:
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:
Expand All @@ -1730,7 +1715,15 @@ def _get_secret(self, group) -> Optional[Dict[str, str]]:
@property
def secrets_enabled(self):
"""Is this Juju version allowing for Secrets usage?"""
return self._jujuversion.has_secrets
return JujuVersion.from_environ().has_secrets


class AuthenticationEvent(RelationEventWithSecret):
"""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 username(self) -> Optional[str]:
Expand Down Expand Up @@ -1813,7 +1806,7 @@ class DatabaseProvidesEvents(CharmEvents):
database_requested = EventSource(DatabaseRequestedEvent)


class DatabaseRequiresEvent(RelationEvent):
class DatabaseRequiresEvent(RelationEventWithSecret):
"""Base class for database events."""

@property
Expand Down Expand Up @@ -1868,6 +1861,11 @@ def uris(self) -> Optional[str]:
if not self.relation.app:
return None

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

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

@property
Expand Down Expand Up @@ -1911,7 +1909,7 @@ class DatabaseRequiresEvents(CharmEvents):
class DatabaseProvides(DataProvides):
"""Provider-side of the database relations."""

on = DatabaseProvidesEvents() # pyright: ignore [reportGeneralTypeIssues]
on = DatabaseProvidesEvents() # pyright: ignore [reportAssignmentType]

def __init__(self, charm: CharmBase, relation_name: str) -> None:
super().__init__(charm, relation_name)
Expand Down Expand Up @@ -2006,7 +2004,7 @@ def set_version(self, relation_id: int, version: str) -> None:
class DatabaseRequires(DataRequires):
"""Requires-side of the database relation."""

on = DatabaseRequiresEvents() # pyright: ignore [reportGeneralTypeIssues]
on = DatabaseRequiresEvents() # pyright: ignore [reportAssignmentType]

def __init__(
self,
Expand Down Expand Up @@ -2335,7 +2333,7 @@ class KafkaRequiresEvents(CharmEvents):
class KafkaProvides(DataProvides):
"""Provider-side of the Kafka relation."""

on = KafkaProvidesEvents() # pyright: ignore [reportGeneralTypeIssues]
on = KafkaProvidesEvents() # pyright: ignore [reportAssignmentType]

def __init__(self, charm: CharmBase, relation_name: str) -> None:
super().__init__(charm, relation_name)
Expand Down Expand Up @@ -2396,7 +2394,7 @@ def set_zookeeper_uris(self, relation_id: int, zookeeper_uris: str) -> None:
class KafkaRequires(DataRequires):
"""Requires-side of the Kafka relation."""

on = KafkaRequiresEvents() # pyright: ignore [reportGeneralTypeIssues]
on = KafkaRequiresEvents() # pyright: ignore [reportAssignmentType]

def __init__(
self,
Expand Down Expand Up @@ -2533,7 +2531,7 @@ class OpenSearchRequiresEvents(CharmEvents):
class OpenSearchProvides(DataProvides):
"""Provider-side of the OpenSearch relation."""

on = OpenSearchProvidesEvents() # pyright: ignore[reportGeneralTypeIssues]
on = OpenSearchProvidesEvents() # pyright: ignore[reportAssignmentType]

def __init__(self, charm: CharmBase, relation_name: str) -> None:
super().__init__(charm, relation_name)
Expand Down Expand Up @@ -2586,7 +2584,7 @@ def set_version(self, relation_id: int, version: str) -> None:
class OpenSearchRequires(DataRequires):
"""Requires-side of the OpenSearch relation."""

on = OpenSearchRequiresEvents() # pyright: ignore[reportGeneralTypeIssues]
on = OpenSearchRequiresEvents() # pyright: ignore[reportAssignmentType]

def __init__(
self,
Expand Down
13 changes: 5 additions & 8 deletions lib/charms/postgresql_k8s/v0/postgresql.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,14 @@
Any charm using this library should import the `psycopg2` or `psycopg2-binary` dependency.
"""
import logging
from collections import OrderedDict
from typing import Dict, List, Optional, Set, Tuple

import psycopg2
from ops.model import Relation
from psycopg2 import sql
from psycopg2.sql import Composed

from collections import OrderedDict

# The unique Charmhub library identifier, never change it
LIBID = "24ee217a54e840a598ff21a079c3e678"

Expand All @@ -40,7 +39,6 @@

INVALID_EXTRA_USER_ROLE_BLOCKING_MESSAGE = "invalid role(s) for extra user roles"


REQUIRED_PLUGINS = {
"address_standardizer": ["postgis"],
"address_standardizer_data_us": ["postgis"],
Expand All @@ -53,7 +51,6 @@
for dependencies in REQUIRED_PLUGINS.values():
DEPENDENCY_PLUGINS |= set(dependencies)


logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -304,18 +301,18 @@ def enable_disable_extensions(self, extensions: Dict[str, bool], database: str =
cursor.execute("SELECT datname FROM pg_database WHERE NOT datistemplate;")
databases = {database[0] for database in cursor.fetchall()}

orderedExtensions = OrderedDict()
ordered_extensions = OrderedDict()
for plugin in DEPENDENCY_PLUGINS:
orderedExtensions[plugin] = extensions.get(plugin, False)
ordered_extensions[plugin] = extensions.get(plugin, False)
for extension, enable in extensions.items():
orderedExtensions[extension] = enable
ordered_extensions[extension] = enable

# Enable/disabled the extension in each database.
for database in databases:
with self._connect_to_database(
database=database
) as connection, connection.cursor() as cursor:
for extension, enable in orderedExtensions.items():
for extension, enable in ordered_extensions.items():
cursor.execute(
f"CREATE EXTENSION IF NOT EXISTS {extension};"
if enable
Expand Down
39 changes: 32 additions & 7 deletions lib/charms/rolling_ops/v0/rollingops.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def _on_trigger_restart(self, event):
"""
import logging
from enum import Enum
from typing import AnyStr, Callable
from typing import AnyStr, Callable, Optional

from ops.charm import ActionEvent, CharmBase, RelationChangedEvent
from ops.framework import EventBase, Object
Expand All @@ -88,7 +88,7 @@ def _on_trigger_restart(self, event):

# Increment this PATCH version before using `charmcraft publish-lib` or reset
# to 0 if you are raising the major API version
LIBPATCH = 2
LIBPATCH = 5


class LockNoRelationError(Exception):
Expand Down Expand Up @@ -261,7 +261,15 @@ class RunWithLock(EventBase):
class AcquireLock(EventBase):
"""Signals that this unit wants to acquire a lock."""

pass
def __init__(self, handle, callback_override: Optional[str] = None):
super().__init__(handle)
self.callback_override = callback_override or ""

def snapshot(self):
return {"callback_override": self.callback_override}

def restore(self, snapshot):
self.callback_override = snapshot["callback_override"]


class ProcessLocks(EventBase):
Expand Down Expand Up @@ -366,25 +374,42 @@ def _on_process_locks(self: CharmBase, event: ProcessLocks):
self.charm.on[self.name].run_with_lock.emit()
return

self.model.app.status = ActiveStatus()
if self.model.app.status.message == f"Beginning rolling {self.name}":
self.model.app.status = ActiveStatus()

def _on_acquire_lock(self: CharmBase, event: ActionEvent):
"""Request a lock."""
try:
Lock(self).acquire() # Updates relation data
# emit relation changed event in the edge case where aquire does not
relation = self.model.get_relation(self.name)
self.charm.on[self.name].relation_changed.emit(relation)

# persist callback override for eventual run
relation.data[self.charm.unit].update({"callback_override": event.callback_override})
self.charm.on[self.name].relation_changed.emit(relation, app=self.charm.app)

except LockNoRelationError:
logger.debug("No {} peer relation yet. Delaying rolling op.".format(self.name))
event.defer()

def _on_run_with_lock(self: CharmBase, event: RunWithLock):
lock = Lock(self)
self.model.unit.status = MaintenanceStatus("Executing {} operation".format(self.name))
self._callback(event)
relation = self.model.get_relation(self.name)

# default to instance callback if not set
callback_name = relation.data[self.charm.unit].get(
"callback_override", self._callback.__name__
)
callback = getattr(self.charm, callback_name)
callback(event)

lock.release() # Updates relation data
if lock.unit == self.model.unit:
self.charm.on[self.name].process_locks.emit()

self.model.unit.status = ActiveStatus()
# cleanup old callback overrides
relation.data[self.charm.unit].update({"callback_override": ""})

if self.model.unit.status.message == f"Executing {self.name} operation":
self.model.unit.status = ActiveStatus()
21 changes: 11 additions & 10 deletions lib/charms/tls_certificates_interface/v2/tls_certificates.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,6 @@ def _on_all_certificates_invalidated(self, event: AllCertificatesInvalidatedEven
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives.serialization import pkcs12
from cryptography.x509.extensions import Extension, ExtensionNotFound
from jsonschema import exceptions, validate # type: ignore[import-untyped]
from ops.charm import (
CharmBase,
Expand All @@ -308,7 +307,7 @@ def _on_all_certificates_invalidated(self, event: AllCertificatesInvalidatedEven

# Increment this PATCH version before using `charmcraft publish-lib` or reset
# to 0 if you are raising the major API version
LIBPATCH = 22
LIBPATCH = 24

PYDEPS = ["cryptography", "jsonschema"]

Expand Down Expand Up @@ -939,9 +938,11 @@ def generate_private_key(
key_bytes = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.BestAvailableEncryption(password)
if password
else serialization.NoEncryption(),
encryption_algorithm=(
serialization.BestAvailableEncryption(password)
if password
else serialization.NoEncryption()
),
)
return key_bytes

Expand Down Expand Up @@ -1676,7 +1677,7 @@ def get_assigned_certificates(self) -> List[Dict[str, str]]:
"""
final_list = []
for csr in self.get_certificate_signing_requests(fulfilled_only=True):
assert type(csr["certificate_signing_request"]) == str
assert isinstance(csr["certificate_signing_request"], str)
if cert := self._find_certificate_in_relation_data(csr["certificate_signing_request"]):
final_list.append(cert)
return final_list
Expand All @@ -1699,7 +1700,7 @@ def get_expiring_certificates(self) -> List[Dict[str, str]]:
"""
final_list = []
for csr in self.get_certificate_signing_requests(fulfilled_only=True):
assert type(csr["certificate_signing_request"]) == str
assert isinstance(csr["certificate_signing_request"], str)
if cert := self._find_certificate_in_relation_data(csr["certificate_signing_request"]):
expiry_time = _get_certificate_expiry_time(cert["certificate"])
if not expiry_time:
Expand All @@ -1719,11 +1720,12 @@ def get_certificate_signing_requests(
"""Gets the list of CSR's that were sent to the provider.

You can choose to get only the CSR's that have a certificate assigned or only the CSR's
that don't.
that don't.

Args:
fulfilled_only (bool): This option will discard CSRs that don't have certificates yet.
unfulfilled_only (bool): This option will discard CSRs that have certificates signed.

Returns:
List of CSR dictionaries. For example:
[
Expand All @@ -1733,10 +1735,9 @@ def get_certificate_signing_requests(
}
]
"""

final_list = []
for csr in self._requirer_csrs:
assert type(csr["certificate_signing_request"]) == str
assert isinstance(csr["certificate_signing_request"], str)
cert = self._find_certificate_in_relation_data(csr["certificate_signing_request"])
if (unfulfilled_only and cert) or (fulfilled_only and not cert):
continue
Expand Down
Loading