Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
78043a1
Redo
dragomirp Jun 18, 2026
2ade056
Merge branch '16/edge' into dpe-10062-cluster2
dragomirp Jun 18, 2026
6192b0b
Conditional password
dragomirp Jun 18, 2026
b78a78e
Merge branch '16/edge' into dpe-10062-cluster2
dragomirp Jun 18, 2026
6e45739
Port changes WIP
dragomirp Jun 22, 2026
f9f251c
Comment out raft logic
dragomirp Jun 24, 2026
b850820
Attr loop
dragomirp Jun 24, 2026
35c3581
Merge branch '16/edge' into dpe-10062-cluster2
dragomirp Jun 24, 2026
4f9840d
Don't pull psycopg2
dragomirp Jun 24, 2026
2c319f5
None lists and touching wrong path
dragomirp Jun 25, 2026
2aa5ce6
Wrong bootstrap paths
dragomirp Jun 26, 2026
2427b69
Versioned volume paths
dragomirp Jun 26, 2026
86f336d
Fix unit tests
dragomirp Jun 27, 2026
c17fe2f
Wait for switchover (K8s)
dragomirp Jun 27, 2026
187f9a9
Snaplib conditional import
dragomirp Jun 27, 2026
87a139a
K8s paths
dragomirp Jun 28, 2026
4c9ce4f
Move exceptions
dragomirp Jun 29, 2026
db9d422
Don't pull client class in managers
dragomirp Jun 29, 2026
245d2b6
Spaces ips
dragomirp Jun 29, 2026
80f5fb6
Exception
dragomirp Jun 29, 2026
bec12eb
Running members diff
dragomirp Jun 29, 2026
d4dd47b
Ensure slots retval
dragomirp Jun 29, 2026
f9b8789
Workload status check
dragomirp Jun 29, 2026
b7d76ff
Lazy load container
dragomirp Jun 29, 2026
b3133ac
Reduce update sync nodes retry timeout
dragomirp Jun 29, 2026
b079cb1
Revert container change
dragomirp Jun 29, 2026
512f51a
Reduce timeout
dragomirp Jun 30, 2026
1497c9a
[DPE-10062] Single kernel changes
dragomirp Jun 30, 2026
8f2d954
Merge branch 'dpe-10062-kernel-changes' into dpe-10062-cluster2
dragomirp Jun 30, 2026
ad37c17
[DPE-10062] Single kernel changes
dragomirp Jun 30, 2026
f6e8789
Merge branch 'dpe-10062-kernel-changes' into dpe-10062-cluster2
dragomirp Jun 30, 2026
60e7623
Merge branch '16/edge' into dpe-10062-cluster2
dragomirp Jun 30, 2026
fcfbcb7
K8s passes unit names
dragomirp Jun 30, 2026
1bc58e4
Dead code
dragomirp Jun 30, 2026
f1583b3
Unit tests and tweaks
dragomirp Jul 1, 2026
b776240
Merge branch '16/edge' into dpe-10062-tests
dragomirp Jul 2, 2026
e97be7e
VM tests
dragomirp Jul 2, 2026
93d1c46
K8s test
dragomirp Jul 2, 2026
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
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
[project]
name = "postgresql-charms-single-kernel"
description = "Shared and reusable code for PostgreSQL-related charms"
version = "16.3.1"
version = "16.3.2"
readme = "README.md"
license = {file = "LICENSE"}
authors = [
Expand Down Expand Up @@ -38,6 +38,7 @@ vm = [
"pysyncobj>=0.3.15; python_version >= '3.12'",
"psutil>=7.2.2; python_version >= '3.12'",
"charmlibs-snap>=1.0.1; python_version >= '3.12'",
"charmlibs-systemd>=1.0.0.post0; python_version >= '3.12'"
]
db-driver = [
"psycopg2>=2.9.10",
Expand Down
6 changes: 4 additions & 2 deletions single_kernel_postgresql/managers/patroni.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@
import logging
import os
import re
import subprocess
from contextlib import suppress
from functools import cached_property
from typing import Any, Literal, TypedDict

# Platform specific imports
with suppress(ImportError):
from charmlibs import snap
from charmlibs.systemd import daemon_reload
import psycopg2
import psycopg2.extras
import requests
Expand Down Expand Up @@ -412,6 +412,8 @@ def member_inactive(self) -> bool:
True if services is not running, starting or restarting. Retries over a period of 60
seconds times to allow server time to start up.
"""
if not self.workload.is_patroni_running():
return True
Comment on lines +415 to +416

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Missed from the VM implementation.

try:
response = self.cached_patroni_health
except RetryError:
Expand Down Expand Up @@ -828,7 +830,7 @@ def update_patroni_restart_condition(self, new_condition: str) -> None:
logger.debug(f"new patroni service file: {new_patroni_service}")
with open(VM_PATRONI_SERVICE_DEFAULT_PATH, "w") as patroni_service_file:
patroni_service_file.write(new_patroni_service)
subprocess.run(["/bin/systemctl", "daemon-reload"])
daemon_reload()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Missed from the VM implementation.


@property
def primary_endpoint_ready(self) -> bool:
Expand Down
231 changes: 231 additions & 0 deletions tests/unit/test_config_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
# Copyright 2021 Canonical Ltd.
# See LICENSE file for licensing details.
from unittest.mock import Mock, PropertyMock, mock_open, patch, sentinel

import pytest
from single_kernel_postgresql.config.enums import Substrates
from single_kernel_postgresql.core.state import CharmState
from single_kernel_postgresql.managers.config import ConfigManager
from single_kernel_postgresql.workload.k8s import K8sWorkload
from single_kernel_postgresql.workload.vm import VMWorkload


@pytest.fixture(autouse=True)
def config(substrate):
mock_charm = Mock()
mock_container = Mock()
workload = VMWorkload(".") if substrate == Substrates.VM else K8sWorkload(".", mock_container)
with patch(
"single_kernel_postgresql.workload.base.BaseWorkload.get_postgresql_version",
return_value="16.6",
):
config = ConfigManager(
state=CharmState(charm=mock_charm, substrate=substrate), workload=workload
)
yield config


def test_dict_to_hba_string(config):
mock_data = {
"ldapbasedn": "dc=example,dc=net",
"ldapbinddn": "cn=serviceuser,dc=example,dc=net",
"ldapbindpasswd": "password",
"ldaptls": False,
"ldapurl": "ldap://0.0.0.0:3893",
}

assert config._dict_to_hba_string(mock_data) == (
'ldapbasedn="dc=example,dc=net" '
'ldapbinddn="cn=serviceuser,dc=example,dc=net" '
'ldapbindpasswd="password" '
"ldaptls=0 "
'ldapurl="ldap://0.0.0.0:3893"'
)


def test_render_patroni_yml_file(substrate, config):
with (
patch(
"single_kernel_postgresql.workload.base.BaseWorkload.get_postgresql_version",
return_value="16.6",
),
patch("single_kernel_postgresql.managers.config.render_file") as _render_file,
patch("single_kernel_postgresql.managers.config.Template") as _template,
patch(
"single_kernel_postgresql.core.state.CharmState.config", new_callable=PropertyMock
) as _config,
patch(
"single_kernel_postgresql.core.state.CharmState.endpoint",
new_callable=PropertyMock,
return_value=sentinel.endpoint,
),
patch(
"single_kernel_postgresql.core.state.CharmState.model_name",
new_callable=PropertyMock,
return_value=sentinel.model_name,
),
patch(
"single_kernel_postgresql.core.state.CharmState.listen_ips",
new_callable=PropertyMock,
return_value=sentinel.listen_ips,
),
patch(
"single_kernel_postgresql.core.state.CharmState.unit_ip",
new_callable=PropertyMock,
return_value=sentinel.unit_ip,
),
patch(
"single_kernel_postgresql.core.peer_relation.PostgreSQLApplication.patroni_password",
new_callable=PropertyMock,
return_value=sentinel.patroni_pass,
),
patch(
"single_kernel_postgresql.core.peer_relation.PostgreSQLApplication.user_password",
new_callable=PropertyMock,
return_value=sentinel.user_pass,
),
patch(
"single_kernel_postgresql.core.peer_relation.PostgreSQLApplication.replication_password",
new_callable=PropertyMock,
return_value=sentinel.replication_pass,
),
patch(
"single_kernel_postgresql.core.peer_relation.PostgreSQLApplication.rewind_password",
new_callable=PropertyMock,
return_value=sentinel.rewind_pass,
),
patch(
"single_kernel_postgresql.core.peer_relation.PostgreSQLApplication.raft_password",
new_callable=PropertyMock,
return_value=sentinel.raft_pass,
),
patch(
"single_kernel_postgresql.core.peer_relation.PostgreSQLApplication.planned_units",
new_callable=PropertyMock,
return_value=1,
),
patch(
"single_kernel_postgresql.core.peer_relation.PostgreSQLApplication.cluster_name",
new_callable=PropertyMock,
return_value=sentinel.cluster_name,
),
patch(
"single_kernel_postgresql.core.peer_relation.PostgreSQLApplication.endpoints",
new_callable=PropertyMock,
return_value=["endpoint1", "endpoint2", "endpoint3"],
),
patch(
"single_kernel_postgresql.core.peer_relation.PostgreSQLApplication.members_ips",
new_callable=PropertyMock,
return_value=["ip1", "ip2", "ip3"],
),
patch(
"single_kernel_postgresql.core.peer_relation.PostgreSQLPeer.member_name",
new_callable=PropertyMock,
return_value=sentinel.member_name,
),
patch("builtins.open", mock_open(read_data="template")),
):
_config.return_value.synchronous_node_count = 1
_config.return_value.durability_maximum_lag_on_failover = (
sentinel.durability_maximum_lag_on_failover
)
_config.return_value.instance_password_encryption = sentinel.instance_password_encryption
_template.return_value.render.return_value = sentinel.template_output

config.render_patroni_yml_file()

_template.assert_called_once_with("template")
if substrate == Substrates.K8S:
_template.return_value.render.assert_called_once_with(
connectivity=False,
enable_ldap=False,
enable_tls=False,
member_name=sentinel.member_name,
superuser="operator",
superuser_password=sentinel.user_pass,
rewind_user="rewind",
rewind_password=sentinel.rewind_pass,
replication_password=sentinel.replication_pass,
enable_pgbackrest_archiving=False,
stanza=None,
restore_stanza=None,
restoring_backup=False,
backup_id=None,
pitr_target=None,
restore_timeline=None,
restore_to_latest=False,
is_creating_backup=False,
version="16",
synchronous_node_count=0,
maximum_lag_on_failover=sentinel.durability_maximum_lag_on_failover,
pg_parameters=None,
primary_cluster_endpoint=None,
ldap_parameters="",
patroni_password=sentinel.patroni_pass,
user_databases_map=None,
slots={},
instance_password_encryption=sentinel.instance_password_encryption,
extra_replication_endpoints=[],
endpoint=sentinel.endpoint,
endpoints=["endpoint1", "endpoint2", "endpoint3"],
is_no_sync_member=False,
namespace=sentinel.model_name,
storage_path="/var/lib/pg/data",
logs_storage_path="/var/lib/pg/logs",
pgdata_path="/var/lib/pg/data/16/main",
)
_render_file.assert_called_once_with(
substrate, "/var/lib/pg/data/patroni.yaml", sentinel.template_output, 0o644
)
else:
_template.return_value.render.assert_called_once_with(
connectivity=False,
enable_ldap=False,
enable_tls=False,
member_name=sentinel.member_name,
superuser="operator",
superuser_password=sentinel.user_pass,
rewind_user="rewind",
rewind_password=sentinel.rewind_pass,
replication_password=sentinel.replication_pass,
enable_pgbackrest_archiving=False,
stanza=None,
restore_stanza=None,
restoring_backup=False,
backup_id=None,
pitr_target=None,
restore_timeline=None,
restore_to_latest=False,
is_creating_backup=False,
version="16",
synchronous_node_count=0,
maximum_lag_on_failover=sentinel.durability_maximum_lag_on_failover,
pg_parameters=None,
primary_cluster_endpoint=None,
ldap_parameters="",
patroni_password=sentinel.patroni_pass,
user_databases_map=None,
slots={},
instance_password_encryption=sentinel.instance_password_encryption,
extra_replication_endpoints=[],
conf_path="/var/snap/charmed-postgresql/current/etc/patroni",
log_path="/var/snap/charmed-postgresql/common/var/log/patroni",
postgresql_log_path="/var/snap/charmed-postgresql/common/var/log/postgresql",
data_path="/var/snap/charmed-postgresql/common/var/lib/postgresql/16/main",
wal_dir="/var/snap/charmed-postgresql/common/data/logs/16/main",
partner_addrs=[],
peers_ips=["ip1", "ip2", "ip3"],
pgbackrest_configuration_file="--config=/var/snap/charmed-postgresql/current/etc/pgbackrest/pgbackrest.conf",
scope=sentinel.cluster_name,
self_ip=sentinel.unit_ip,
listen_ips=sentinel.listen_ips,
raft_password=sentinel.raft_pass,
watcher=None,
)
_render_file.assert_called_once_with(
substrate,
"/var/snap/charmed-postgresql/current/etc/patroni/patroni.yaml",
sentinel.template_output,
0o600,
)
Loading