Skip to content

Commit

Permalink
Update revision due to outdated snap pkgs (#81)
Browse files Browse the repository at this point in the history
* Update revision due to outdated snap pkgs

* Bump charm libs

* Update to latest revision

* Bump libs
  • Loading branch information
dragomirp authored Jun 7, 2023
1 parent 5fcd3a7 commit 80ceb22
Show file tree
Hide file tree
Showing 3 changed files with 278 additions and 9 deletions.
46 changes: 45 additions & 1 deletion lib/charms/postgresql_k8s/v0/postgresql.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@

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


logger = logging.getLogger(__name__)
Expand All @@ -50,6 +50,10 @@ class PostgreSQLDeleteUserError(Exception):
"""Exception raised when deleting a user fails."""


class PostgreSQLEnableDisableExtensionError(Exception):
"""Exception raised when enabling/disabling an extension fails."""


class PostgreSQLGetPostgreSQLVersionError(Exception):
"""Exception raised when retrieving PostgreSQL version fails."""

Expand Down Expand Up @@ -237,6 +241,46 @@ def delete_user(self, user: str) -> None:
logger.error(f"Failed to delete user: {e}")
raise PostgreSQLDeleteUserError()

def enable_disable_extension(self, extension: str, enable: bool, database: str = None) -> None:
"""Enables or disables a PostgreSQL extension.
Args:
extension: the name of the extensions.
enable: whether the extension should be enabled or disabled.
database: optional database where to enable/disable the extension.
Raises:
PostgreSQLEnableDisableExtensionError if the operation fails.
"""
statement = (
f"CREATE EXTENSION IF NOT EXISTS {extension};"
if enable
else f"DROP EXTENSION IF EXISTS {extension};"
)
connection = None
try:
if database is not None:
databases = [database]
else:
# Retrieve all the databases.
with self._connect_to_database() as connection, connection.cursor() as cursor:
cursor.execute("SELECT datname FROM pg_database WHERE NOT datistemplate;")
databases = {database[0] for database in cursor.fetchall()}

# Enable/disabled the extension in each database.
for database in databases:
with self._connect_to_database(
database=database
) as connection, connection.cursor() as cursor:
cursor.execute(statement)
except psycopg2.errors.UniqueViolation:
pass
except psycopg2.Error:
raise PostgreSQLEnableDisableExtensionError()
finally:
if connection is not None:
connection.close()

def get_postgresql_version(self) -> str:
"""Returns the PostgreSQL version.
Expand Down
2 changes: 1 addition & 1 deletion src/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
# Snap constants.
PGBOUNCER_EXECUTABLE = "charmed-postgresql.pgbouncer"
POSTGRESQL_SNAP_NAME = "charmed-postgresql"
SNAP_PACKAGES = [(POSTGRESQL_SNAP_NAME, {"revision": 57})]
SNAP_PACKAGES = [(POSTGRESQL_SNAP_NAME, {"revision": 59})]

SNAP_COMMON_PATH = "/var/snap/charmed-postgresql/common"
SNAP_CURRENT_PATH = "/var/snap/charmed-postgresql/current"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,19 @@ def _on_cluster2_database_created(self, event: DatabaseCreatedEvent) -> None:
```
When it's needed to check whether a plugin (extension) is enabled on the PostgreSQL
charm, you can use the is_postgresql_plugin_enabled method. To use that, you need to
add the following dependency to your charmcraft.yaml file:
```yaml
parts:
charm:
charm-binary-python-packages:
- psycopg[binary]
```
### Provider Charm
Following an example of using the DatabaseRequestedEvent, in the context of the
Expand Down Expand Up @@ -303,7 +316,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 = 9
LIBPATCH = 12

PYDEPS = ["ops>=2.0.0"]

Expand Down Expand Up @@ -456,7 +469,7 @@ def set_tls_ca(self, relation_id: int, tls_ca: str) -> None:
relation_id: the identifier for a particular relation.
tls_ca: TLS certification authority.
"""
self._update_relation_data(relation_id, {"tls_ca": tls_ca})
self._update_relation_data(relation_id, {"tls-ca": tls_ca})


class DataRequires(Object, ABC):
Expand Down Expand Up @@ -667,12 +680,20 @@ def database(self) -> Optional[str]:

@property
def endpoints(self) -> Optional[str]:
"""Returns a comma separated list of read/write endpoints."""
"""Returns a comma separated list of read/write endpoints.
In VM charms, this is the primary's address.
In kubernetes charms, this is the service to the primary pod.
"""
return self.relation.data[self.relation.app].get("endpoints")

@property
def read_only_endpoints(self) -> Optional[str]:
"""Returns a comma separated list of read only endpoints."""
"""Returns a comma separated list of read only endpoints.
In VM charms, this is the address of all the secondary instances.
In kubernetes charms, this is the service to all replica pod instances.
"""
return self.relation.data[self.relation.app].get("read-only-endpoints")

@property
Expand Down Expand Up @@ -766,6 +787,10 @@ def set_endpoints(self, relation_id: int, connection_strings: str) -> None:
This function writes in the application data bag, therefore,
only the leader unit can call it.
In VM charms, only the primary's address should be passed as an endpoint.
In kubernetes charms, the service endpoint to the primary pod should be
passed as an endpoint.
Args:
relation_id: the identifier for a particular relation.
connection_strings: database hosts and ports comma separated list.
Expand Down Expand Up @@ -915,6 +940,48 @@ def _get_relation_alias(self, relation_id: int) -> Optional[str]:
return relation.data[self.local_unit].get("alias")
return None

def is_postgresql_plugin_enabled(self, plugin: str, relation_index: int = 0) -> bool:
"""Returns whether a plugin is enabled in the database.
Args:
plugin: name of the plugin to check.
relation_index: optional relation index to check the database
(default: 0 - first relation).
PostgreSQL only.
"""
# Psycopg 3 is imported locally to avoid the need of its package installation
# when relating to a database charm other than PostgreSQL.
import psycopg

# Return False if no relation is established.
if len(self.relations) == 0:
return False

relation_data = self.fetch_relation_data()[self.relations[relation_index].id]
host = relation_data.get("endpoints")

# Return False if there is no endpoint available.
if host is None:
return False

host = host.split(":")[0]
user = relation_data.get("username")
password = relation_data.get("password")
connection_string = (
f"host='{host}' dbname='{self.database}' user='{user}' password='{password}'"
)
try:
with psycopg.connect(connection_string) as connection:
with connection.cursor() as cursor:
cursor.execute(f"SELECT TRUE FROM pg_extension WHERE extname='{plugin}';")
return cursor.fetchone() is not None
except psycopg.Error as e:
logger.exception(
f"failed to check whether {plugin} plugin is enabled in the database: %s", str(e)
)
return False

def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None:
"""Event emitted when the application joins the database relation."""
# If relations aliases were provided, assign one to the relation.
Expand Down Expand Up @@ -1019,7 +1086,7 @@ def topic(self) -> Optional[str]:

@property
def bootstrap_server(self) -> Optional[str]:
"""Returns a a comma-seperated list of broker uris."""
"""Returns a comma-separated list of broker uris."""
return self.relation.data[self.relation.app].get("endpoints")

@property
Expand Down Expand Up @@ -1108,7 +1175,7 @@ def set_zookeeper_uris(self, relation_id: int, zookeeper_uris: str) -> None:
Args:
relation_id: the identifier for a particular relation.
zookeeper_uris: comma-seperated list of ZooKeeper server uris.
zookeeper_uris: comma-separated list of ZooKeeper server uris.
"""
self._update_relation_data(relation_id, {"zookeeper-uris": zookeeper_uris})

Expand Down Expand Up @@ -1159,7 +1226,7 @@ def _on_relation_changed_event(self, event: RelationChangedEvent) -> None:
# “endpoints_changed“ event if “topic_created“ is triggered.
return

# Emit an endpoints (bootstap-server) changed event if the Kafka endpoints
# Emit an endpoints (bootstrap-server) changed event if the Kafka endpoints
# added or changed this info in the relation databag.
if "endpoints" in diff.added or "endpoints" in diff.changed:
# Emit the default event (the one without an alias).
Expand All @@ -1168,3 +1235,161 @@ def _on_relation_changed_event(self, event: RelationChangedEvent) -> None:
event.relation, app=event.app, unit=event.unit
) # here check if this is the right design
return


# Opensearch related events


class OpenSearchProvidesEvent(RelationEvent):
"""Base class for OpenSearch events."""

@property
def index(self) -> Optional[str]:
"""Returns the index that was requested."""
return self.relation.data[self.relation.app].get("index")


class IndexRequestedEvent(OpenSearchProvidesEvent, ExtraRoleEvent):
"""Event emitted when a new index is requested for use on this relation."""


class OpenSearchProvidesEvents(CharmEvents):
"""OpenSearch events.
This class defines the events that OpenSearch can emit.
"""

index_requested = EventSource(IndexRequestedEvent)


class OpenSearchRequiresEvent(DatabaseRequiresEvent):
"""Base class for OpenSearch requirer events."""


class IndexCreatedEvent(AuthenticationEvent, OpenSearchRequiresEvent):
"""Event emitted when a new index is created for use on this relation."""


class OpenSearchRequiresEvents(CharmEvents):
"""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


class OpenSearchProvides(DataProvides):
"""Provider-side of the OpenSearch relation."""

on = OpenSearchProvidesEvents()

def __init__(self, charm: CharmBase, relation_name: str) -> None:
super().__init__(charm, relation_name)

def _on_relation_changed(self, event: RelationChangedEvent) -> None:
"""Event emitted when the relation has changed."""
# Only the leader should handle this event.
if not self.local_unit.is_leader():
return

# Check which data has changed to emit customs events.
diff = self._diff(event)

# Emit an index requested event if the setup key (index name and optional extra user roles)
# have been added to the relation databag by the application.
if "index" in diff.added:
self.on.index_requested.emit(event.relation, app=event.app, unit=event.unit)

def set_index(self, relation_id: int, index: str) -> None:
"""Set the index in the application relation databag.
Args:
relation_id: the identifier for a particular relation.
index: the index as it is _created_ on the provider charm. This needn't match the
requested index, and can be used to present a different index name if, for example,
the requested index is invalid.
"""
self._update_relation_data(relation_id, {"index": index})

def set_endpoints(self, relation_id: int, endpoints: str) -> None:
"""Set the endpoints in the application relation databag.
Args:
relation_id: the identifier for a particular relation.
endpoints: the endpoint addresses for opensearch nodes.
"""
self._update_relation_data(relation_id, {"endpoints": endpoints})

def set_version(self, relation_id: int, version: str) -> None:
"""Set the opensearch version in the application relation databag.
Args:
relation_id: the identifier for a particular relation.
version: database version.
"""
self._update_relation_data(relation_id, {"version": version})


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

on = OpenSearchRequiresEvents()

def __init__(
self, charm, relation_name: str, index: str, extra_user_roles: Optional[str] = None
):
"""Manager of OpenSearch client relations."""
super().__init__(charm, relation_name, extra_user_roles)
self.charm = charm
self.index = index

def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None:
"""Event emitted when the application joins the OpenSearch relation."""
# Sets both index and extra user roles in the relation if the roles are provided.
# Otherwise, sets only the index.
data = {"index": self.index}
if self.extra_user_roles:
data["extra-user-roles"] = self.extra_user_roles

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

def _on_relation_changed_event(self, event: RelationChangedEvent) -> None:
"""Event emitted when the OpenSearch relation has changed.
This event triggers individual custom events depending on the changing relation.
"""
# Check which data has changed to emit customs events.
diff = self._diff(event)

# Check if authentication has updated, emit event if so
updates = {"username", "password", "tls", "tls-ca"}
if len(set(diff._asdict().keys()) - updates) < len(diff):
logger.info("authentication updated at: %s", datetime.now())
self.on.authentication_updated.emit(event.relation, app=event.app, unit=event.unit)

# Check if the index is created
# (the OpenSearch charm shares the credentials).
if "username" in diff.added and "password" in diff.added:
# Emit the default event (the one without an alias).
logger.info("index created at: %s", datetime.now())
self.on.index_created.emit(event.relation, app=event.app, unit=event.unit)

# To avoid unnecessary application restarts do not trigger
# “endpoints_changed“ event if “index_created“ is triggered.
return

# Emit a endpoints changed event if the OpenSearch application added or changed this info
# in the relation databag.
if "endpoints" in diff.added or "endpoints" in diff.changed:
# Emit the default event (the one without an alias).
logger.info("endpoints changed on %s", datetime.now())
self.on.endpoints_changed.emit(
event.relation, app=event.app, unit=event.unit
) # here check if this is the right design
return

0 comments on commit 80ceb22

Please sign in to comment.