Skip to content

Commit

Permalink
[DPE-3049] templated config (#186)
Browse files Browse the repository at this point in the history
* Generate templated cfg side by side

* Templated config WIP

* Disable unit tests

* Legacy templating WIP

* Fix TLS

* Legacy admins

* Cleanup

* Remove update method

* Cleanup

* Linting

* Restart on peer change

* Full restart on peer change

* Revert "Full restart on peer change"

This reverts commit c3c8f13.

* Avoid early finalising of the new interface

* Try to suppress non leader warnings

* Fix port change

* Redundant rerener

* Unify check_backend

* Disable constant defers

* Defer instead of ignore

* Don't wait for retries

* Filter database

* [charm] Update charm dependencies

* Remove flag

* Reset flag

* Defer _on_change

* Filter out new relation

* Put mapping in peer data

* Str rel ids

* Update read only endpoints on peer drift

* Only one db per rel

* Missed dynamic params

* Suppress log error

* Restore auth file on pebble ready

* Don't read auth file

* Removing PgbConfig

* Reenable only working tests

* Add tests WIP

* Don't render the dict

* Upgrade tweaks

* Restoring unit tests

* Try not to rerender on legacy rel change

* Restore rerender on legacy rel change

* Restoring tests

* Container check in _on_config_change

* Dead code

* Unit tests

* Bump coveage

* Don't raise on blocked

* Switch status

* Don't update libjuju

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
  • Loading branch information
dragomirp and renovate[bot] authored Feb 14, 2024
1 parent b7c2876 commit 4952d4e
Show file tree
Hide file tree
Showing 30 changed files with 814 additions and 1,576 deletions.
385 changes: 2 additions & 383 deletions lib/charms/pgbouncer_k8s/v0/pgb.py

Large diffs are not rendered by default.

113 changes: 58 additions & 55 deletions poetry.lock

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,17 @@ repository = "https://github.com/canonical/pgbouncer-k8s-operator"

[tool.poetry.dependencies]
python = "^3.8.10"
ops = "^2.9.0"
cryptography = "^42.0.1"
ops = "^2.10.0"
cryptography = "^42.0.2"
jsonschema = "^4.21.1"
tenacity = "^8.2.3"
cosl = "^0.0.7"
poetry-core = "^1.8.1"
lightkube = "^0.15.0"
cosl = "^0.0.8"
poetry-core = "^1.9.0"
lightkube = "^0.15.1"
lightkube-models = "^1.29.0.6"
pydantic = "^1.10.14"
psycopg2 = "^2.9.9"
psycopg = {extras = ["c"], version = "^3.1.17"}
psycopg = {extras = ["c"], version = "^3.1.18"}

[tool.poetry.group.charm-libs.dependencies]
# data_platform_libs/v0/data_interfaces.py
Expand Down
359 changes: 180 additions & 179 deletions src/charm.py

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion src/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@
PGB_LOG_DIR = "/var/log/pgbouncer"
MONITORING_PASSWORD_KEY = "monitoring_password"
AUTH_FILE_DATABAG_KEY = "auth_file"
CFG_FILE_DATABAG_KEY = "cfg_file"

EXTENSIONS_BLOCKING_MESSAGE = "bad relation request - remote app requested extensions, which are unsupported. Please remove this relation."
CONTAINER_UNAVAILABLE_MESSAGE = "PgBouncer container currently unavailable"

SECRET_LABEL = "secret"
SECRET_INTERNAL_LABEL = "internal-secret"
Expand All @@ -36,7 +38,6 @@
UNIT_SCOPE = "unit"

SECRET_KEY_OVERRIDES = {
"cfg_file": "cfg-file",
"ca": "cauth",
"monitoring_password": "monitoring-password",
"auth_file": "auth-file",
Expand Down
110 changes: 60 additions & 50 deletions src/relations/backend_database.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"""

import logging
from typing import Dict, List, Set
from typing import Dict, List, Optional, Set

import psycopg2
from charms.data_platform_libs.v0.data_interfaces import (
Expand All @@ -55,13 +55,13 @@
BlockedStatus,
MaintenanceStatus,
Relation,
WaitingStatus,
)
from ops.pebble import ConnectionError
from ops.pebble import ConnectionError, PathError

from constants import (
APP_SCOPE,
AUTH_FILE_DATABAG_KEY,
AUTH_FILE_PATH,
BACKEND_RELATION_NAME,
MONITORING_PASSWORD_KEY,
PG,
Expand Down Expand Up @@ -148,7 +148,7 @@ def postgres(self) -> PostgreSQL:
)

@property
def auth_user(self):
def auth_user(self) -> Optional[str]:
"""Username for auth_user."""
if not self.relation:
return None
Expand All @@ -159,10 +159,19 @@ def auth_user(self):
return f"pgbouncer_auth_{username}".replace("-", "_")

@property
def stats_user(self):
def stats_user(self) -> str:
"""Username for stats."""
if not self.relation:
return ""
return f"pgbouncer_stats_{self.charm.app.name}".replace("-", "_")

@property
def auth_query(self) -> str:
"""Generate auth query."""
if not self.relation:
return ""
return f"SELECT username, password FROM {self.auth_user}.get_auth($1)"

@property
def postgres_databag(self) -> Dict:
"""Wrapper around accessing the remote application databag for the backend relation.
Expand All @@ -187,22 +196,11 @@ def ready(self) -> bool:
"""
# Check we have connection information
if not self.postgres:
logger.debug("Backend not ready: no connection info")
return False

try:
cfg = self.charm.read_pgb_config()
except FileNotFoundError:
# Not ready, no config
return False

# Check we can authenticate
if "auth_query" not in cfg["pgbouncer"].keys():
# Not ready, backend relation not initialised
return False
try:
cfg = self.charm.read_auth_file()
except FileNotFoundError:
# Not ready, no auth file to authenticate our pgb user
if not self.charm.get_secret(APP_SCOPE, AUTH_FILE_DATABAG_KEY):
logger.debug("Backend not ready: no auth file secret set")
return False

# Check we can actually connect to backend database by running a command.
Expand All @@ -212,7 +210,7 @@ def ready(self) -> bool:
cursor.execute("SELECT version();")
conn.close()
except (psycopg2.Error, psycopg2.OperationalError):
logger.error("PostgreSQL connection failed")
logger.warning("PostgreSQL connection failed")
return False

return True
Expand All @@ -223,6 +221,18 @@ def _on_database_created(self, event: DatabaseCreatedEvent) -> None:
Accesses user and password generated by the postgres charm and adds a user.
"""
if not self.charm.unit.is_leader():
# Pebble ready will set the config
if not self.charm.is_container_ready:
return

if not (auth_file := self.charm.get_secret(APP_SCOPE, AUTH_FILE_DATABAG_KEY)):
logger.debug("_on_database_created deferred: waiting for leader to initialise")
event.defer()
return
self.charm.render_auth_file(auth_file)
self.charm.render_pgb_config(reload_pgbouncer=True)
self.charm.toggle_monitoring_layer(True)
self.charm.unit.status = ActiveStatus()
return

logger.info("initialising pgbouncer backend relation")
Expand Down Expand Up @@ -261,21 +271,13 @@ def _on_database_created(self, event: DatabaseCreatedEvent) -> None:
self.charm.set_secret(APP_SCOPE, AUTH_FILE_DATABAG_KEY, auth_file)
self.charm.render_auth_file(auth_file)

cfg = self.charm.read_pgb_config()
cfg.add_user(user=self.stats_user, stats=True)
cfg["pgbouncer"][
"auth_query"
] = f"SELECT username, password FROM {self.auth_user}.get_auth($1)"
cfg["pgbouncer"]["auth_file"] = AUTH_FILE_PATH
self.charm.render_pgb_config(cfg)

self.charm.update_postgres_endpoints(reload_pgbouncer=True)
self.charm.render_pgb_config(reload_pgbouncer=True)
self.charm.toggle_monitoring_layer(True)

self.charm.unit.status = ActiveStatus("backend-database relation initialised.")

def _on_endpoints_changed(self, _):
self.charm.update_postgres_endpoints(reload_pgbouncer=True)
self.charm.render_pgb_config(reload_pgbouncer=True)
self.charm.update_client_connection_info()

def _on_relation_changed(self, _):
Expand All @@ -288,7 +290,7 @@ def _on_relation_changed(self, _):
logger.debug("_on_reltion_changed early exit: pebble ready not fired")
return

self.charm.update_postgres_endpoints(reload_pgbouncer=True)
self.charm.render_pgb_config(reload_pgbouncer=True)
self.charm.update_client_connection_info()

def _on_relation_departed(self, event: RelationDepartedEvent):
Expand All @@ -298,15 +300,15 @@ def _on_relation_departed(self, event: RelationDepartedEvent):
the postgres relation-broken hook removes the user needed to remove authentication for the
users we create.
"""
self.charm.render_pgb_config(reload_pgbouncer=True)
self.charm.update_client_connection_info()
self.charm.update_postgres_endpoints(reload_pgbouncer=True)

if event.departing_unit == self.charm.unit:
# This should only occur when the relation is being removed, not on scale-down
self.charm.peers.unit_databag.update(
{f"{BACKEND_RELATION_NAME}_{event.relation.id}_departing": "true"}
)
logger.error("added relation-departing flag to peer databag")
logger.warning("added relation-departing flag to peer databag")
return

if not self.charm.unit.is_leader() or event.departing_unit.app != self.charm.app:
Expand Down Expand Up @@ -343,27 +345,21 @@ def _on_relation_broken(self, event: RelationBrokenEvent):
"""
depart_flag = f"{BACKEND_RELATION_NAME}_{event.relation.id}_departing"
self.charm.toggle_monitoring_layer(False)
if (
self.charm.peers.unit_databag.get(depart_flag, False)
or not self.charm.unit.is_leader()
):
if self.charm.peers.unit_databag.get(depart_flag, False):
logging.info("exiting relation-broken hook - nothing to do")
return

try:
cfg = self.charm.read_pgb_config()
except FileNotFoundError:
event.defer()
return

cfg.remove_user(self.postgres.user)
cfg["pgbouncer"].pop("auth_user", None)
cfg["pgbouncer"].pop("auth_query", None)
cfg["pgbouncer"].pop("auth_file", None)
self.charm.render_pgb_config(cfg)

self.charm.delete_file(f"{PGB_DIR}/userlist.txt")
self.charm.peers.update_auth_file(auth_file=None)
self.charm.delete_file(f"{PGB_DIR}/userlist.txt")
except PathError:
logger.warning("Cannot delete userlist.txt")
if self.charm.unit.is_leader():
self.charm.remove_secret(APP_SCOPE, AUTH_FILE_DATABAG_KEY)

self.charm.render_pgb_config(reload_pgbouncer=True)
self.charm.unit.status = BlockedStatus(
"waiting for backend database relation to initialise"
)

def initialise_auth_function(self, dbs: List[str]):
"""Runs an SQL script to initialise the auth function.
Expand Down Expand Up @@ -412,3 +408,17 @@ def get_read_only_endpoints(self) -> Set[str]:
if not read_only_endpoints:
return set()
return set(read_only_endpoints.split(","))

def check_backend(self) -> bool:
"""Verifies backend is ready and updates status.
Returns:
bool signifying whether backend is ready or not
"""
if not self.ready:
# We can't relate an app to the backend database without a backend postgres relation
wait_str = "waiting for backend-database relation to connect"
logger.warning(wait_str)
self.charm.unit.status = WaitingStatus(wait_str)
return False
return True
Loading

0 comments on commit 4952d4e

Please sign in to comment.