14
14
import time
15
15
from pathlib import Path
16
16
from typing import Literal , get_args
17
+ from urllib .parse import urlparse
17
18
18
19
# First platform-specific import, will fail on wrong architecture
19
20
try :
35
36
from charms .grafana_k8s .v0 .grafana_dashboard import GrafanaDashboardProvider
36
37
from charms .loki_k8s .v1 .loki_push_api import LogProxyConsumer
37
38
from charms .postgresql_k8s .v0 .postgresql import (
39
+ ACCESS_GROUP_IDENTITY ,
38
40
ACCESS_GROUPS ,
39
41
REQUIRED_PLUGINS ,
40
42
PostgreSQL ,
88
90
APP_SCOPE ,
89
91
BACKUP_USER ,
90
92
DATABASE_DEFAULT_NAME ,
93
+ DATABASE_PORT ,
91
94
METRICS_PORT ,
92
95
MONITORING_PASSWORD_KEY ,
93
96
MONITORING_USER ,
@@ -193,10 +196,11 @@ def __init__(self, *args):
193
196
deleted_label = SECRET_DELETED_LABEL ,
194
197
)
195
198
196
- self ._postgresql_service = "postgresql"
199
+ self .postgresql_service = "postgresql"
197
200
self .rotate_logs_service = "rotate-logs"
198
201
self .pgbackrest_server_service = "pgbackrest server"
199
- self ._metrics_service = "metrics_server"
202
+ self .ldap_sync_service = "ldap-sync"
203
+ self .metrics_service = "metrics_server"
200
204
self ._unit = self .model .unit .name
201
205
self ._name = self .model .app .name
202
206
self ._namespace = self .model .name
@@ -586,7 +590,7 @@ def _on_peer_relation_changed(self, event: HookEvent) -> None: # noqa: C901
586
590
logger .debug ("on_peer_relation_changed early exit: Unit in blocked status" )
587
591
return
588
592
589
- services = container .pebble .get_services (names = [self ._postgresql_service ])
593
+ services = container .pebble .get_services (names = [self .postgresql_service ])
590
594
if (
591
595
(self .is_cluster_restoring_backup or self .is_cluster_restoring_to_time )
592
596
and len (services ) > 0
@@ -1463,7 +1467,7 @@ def _on_update_status(self, _) -> None:
1463
1467
if not self ._on_update_status_early_exit_checks (container ):
1464
1468
return
1465
1469
1466
- services = container .pebble .get_services (names = [self ._postgresql_service ])
1470
+ services = container .pebble .get_services (names = [self .postgresql_service ])
1467
1471
if len (services ) == 0 :
1468
1472
# Service has not been added nor started yet, so don't try to check Patroni API.
1469
1473
logger .debug ("on_update_status early exit: Service has not been added nor started yet" )
@@ -1476,10 +1480,10 @@ def _on_update_status(self, _) -> None:
1476
1480
and services [0 ].current != ServiceStatus .ACTIVE
1477
1481
):
1478
1482
logger .warning (
1479
- f"{ self ._postgresql_service } pebble service inactive, restarting service"
1483
+ f"{ self .postgresql_service } pebble service inactive, restarting service"
1480
1484
)
1481
1485
try :
1482
- container .restart (self ._postgresql_service )
1486
+ container .restart (self .postgresql_service )
1483
1487
except ChangeError :
1484
1488
logger .exception ("Failed to restart patroni" )
1485
1489
# If service doesn't recover fast, exit and wait for next hook run to re-check
@@ -1576,7 +1580,7 @@ def _handle_processes_failures(self) -> bool:
1576
1580
# https://github.com/canonical/pebble/issues/149 is resolved.
1577
1581
if not self ._patroni .member_started and self ._patroni .is_database_running :
1578
1582
try :
1579
- container .restart (self ._postgresql_service )
1583
+ container .restart (self .postgresql_service )
1580
1584
logger .info ("restarted Patroni because it was not running" )
1581
1585
except ChangeError :
1582
1586
logger .error ("failed to restart Patroni after checking that it was not running" )
@@ -1713,6 +1717,40 @@ def _update_endpoints(
1713
1717
endpoints .remove (endpoint )
1714
1718
self ._peers .data [self .app ]["endpoints" ] = json .dumps (endpoints )
1715
1719
1720
+ def _generate_ldap_service (self ) -> dict :
1721
+ """Generate the LDAP service definition."""
1722
+ ldap_params = self .get_ldap_parameters ()
1723
+
1724
+ ldap_url = urlparse (ldap_params ["ldapurl" ])
1725
+ ldap_host = ldap_url .hostname
1726
+ ldap_port = ldap_url .port
1727
+
1728
+ ldap_base_dn = ldap_params ["ldapbasedn" ]
1729
+ ldap_bind_username = ldap_params ["ldapbinddn" ]
1730
+ ldap_bing_password = ldap_params ["ldapbindpasswd" ]
1731
+ ldap_group_mappings = self .postgresql .build_postgresql_group_map (self .config .ldap_map )
1732
+
1733
+ return {
1734
+ "override" : "replace" ,
1735
+ "summary" : "synchronize LDAP users" ,
1736
+ "command" : "/start-ldap-synchronizer.sh" ,
1737
+ "startup" : "enabled" ,
1738
+ "environment" : {
1739
+ "LDAP_HOST" : ldap_host ,
1740
+ "LDAP_PORT" : ldap_port ,
1741
+ "LDAP_BASE_DN" : ldap_base_dn ,
1742
+ "LDAP_BIND_USERNAME" : ldap_bind_username ,
1743
+ "LDAP_BIND_PASSWORD" : ldap_bing_password ,
1744
+ "LDAP_GROUP_IDENTITY" : json .dumps (ACCESS_GROUP_IDENTITY ),
1745
+ "LDAP_GROUP_MAPPINGS" : json .dumps (ldap_group_mappings ),
1746
+ "POSTGRES_HOST" : "127.0.0.1" ,
1747
+ "POSTGRES_PORT" : DATABASE_PORT ,
1748
+ "POSTGRES_DATABASE" : DATABASE_DEFAULT_NAME ,
1749
+ "POSTGRES_USERNAME" : USER ,
1750
+ "POSTGRES_PASSWORD" : self .get_secret (APP_SCOPE , USER_PASSWORD_KEY ),
1751
+ },
1752
+ }
1753
+
1716
1754
def _generate_metrics_service (self ) -> dict :
1717
1755
"""Generate the metrics service definition."""
1718
1756
return {
@@ -1724,7 +1762,7 @@ def _generate_metrics_service(self) -> dict:
1724
1762
if self .get_secret ("app" , MONITORING_PASSWORD_KEY ) is not None
1725
1763
else "disabled"
1726
1764
),
1727
- "after" : [self ._postgresql_service ],
1765
+ "after" : [self .postgresql_service ],
1728
1766
"user" : WORKLOAD_OS_USER ,
1729
1767
"group" : WORKLOAD_OS_GROUP ,
1730
1768
"environment" : {
@@ -1743,7 +1781,7 @@ def _postgresql_layer(self) -> Layer:
1743
1781
"summary" : "postgresql + patroni layer" ,
1744
1782
"description" : "pebble config layer for postgresql + patroni" ,
1745
1783
"services" : {
1746
- self ._postgresql_service : {
1784
+ self .postgresql_service : {
1747
1785
"override" : "replace" ,
1748
1786
"summary" : "entrypoint of the postgresql + patroni image" ,
1749
1787
"command" : f"patroni { self ._storage_path } /patroni.yml" ,
@@ -1773,7 +1811,13 @@ def _postgresql_layer(self) -> Layer:
1773
1811
"user" : WORKLOAD_OS_USER ,
1774
1812
"group" : WORKLOAD_OS_GROUP ,
1775
1813
},
1776
- self ._metrics_service : self ._generate_metrics_service (),
1814
+ self .ldap_sync_service : {
1815
+ "override" : "replace" ,
1816
+ "summary" : "synchronize LDAP users" ,
1817
+ "command" : "/start-ldap-synchronizer.sh" ,
1818
+ "startup" : "disabled" ,
1819
+ },
1820
+ self .metrics_service : self ._generate_metrics_service (),
1777
1821
self .rotate_logs_service : {
1778
1822
"override" : "replace" ,
1779
1823
"summary" : "rotate logs" ,
@@ -1782,7 +1826,7 @@ def _postgresql_layer(self) -> Layer:
1782
1826
},
1783
1827
},
1784
1828
"checks" : {
1785
- self ._postgresql_service : {
1829
+ self .postgresql_service : {
1786
1830
"override" : "replace" ,
1787
1831
"level" : "ready" ,
1788
1832
"http" : {
@@ -1885,14 +1929,59 @@ def _restart(self, event: RunWithLock) -> None:
1885
1929
# Start or stop the pgBackRest TLS server service when TLS certificate change.
1886
1930
self .backup .start_stop_pgbackrest_service ()
1887
1931
1932
+ def _restart_metrics_service (self ) -> None :
1933
+ """Restart the monitoring service if the password was rotated."""
1934
+ container = self .unit .get_container ("postgresql" )
1935
+ current_layer = container .get_plan ()
1936
+
1937
+ metrics_service = current_layer .services [self .metrics_service ]
1938
+ data_source_name = metrics_service .environment .get ("DATA_SOURCE_NAME" , "" )
1939
+
1940
+ if metrics_service and not data_source_name .startswith (
1941
+ f"user={ MONITORING_USER } password={ self .get_secret ('app' , MONITORING_PASSWORD_KEY )} "
1942
+ ):
1943
+ container .add_layer (
1944
+ self .metrics_service ,
1945
+ Layer ({"services" : {self .metrics_service : self ._generate_metrics_service ()}}),
1946
+ combine = True ,
1947
+ )
1948
+ container .restart (self .metrics_service )
1949
+
1950
+ def _restart_ldap_sync_service (self ) -> None :
1951
+ """Restart the LDAP sync service in case any configuration changed."""
1952
+ if not self ._patroni .member_started :
1953
+ logger .debug ("Restart LDAP sync early exit: Patroni has not started yet" )
1954
+ return
1955
+
1956
+ container = self .unit .get_container ("postgresql" )
1957
+ sync_service = container .pebble .get_services (names = [self .ldap_sync_service ])
1958
+
1959
+ if not self .is_primary and sync_service [0 ].is_running ():
1960
+ logger .debug ("Stopping LDAP sync service. It must only run in the primary" )
1961
+ container .stop (self .pg_ldap_sync_service )
1962
+
1963
+ if self .is_primary and not self .is_ldap_enabled :
1964
+ logger .debug ("Stopping LDAP sync service" )
1965
+ container .stop (self .ldap_sync_service )
1966
+ return
1967
+
1968
+ if self .is_primary and self .is_ldap_enabled :
1969
+ container .add_layer (
1970
+ self .ldap_sync_service ,
1971
+ Layer ({"services" : {self .ldap_sync_service : self ._generate_ldap_service ()}}),
1972
+ combine = True ,
1973
+ )
1974
+ logger .debug ("Starting LDAP sync service" )
1975
+ container .restart (self .ldap_sync_service )
1976
+
1888
1977
@property
1889
1978
def _is_workload_running (self ) -> bool :
1890
1979
"""Returns whether the workload is running (in an active state)."""
1891
1980
container = self .unit .get_container ("postgresql" )
1892
1981
if not container .can_connect ():
1893
1982
return False
1894
1983
1895
- services = container .pebble .get_services (names = [self ._postgresql_service ])
1984
+ services = container .pebble .get_services (names = [self .postgresql_service ])
1896
1985
if len (services ) == 0 :
1897
1986
return False
1898
1987
@@ -1982,21 +2071,8 @@ def update_config(self, is_creating_backup: bool = False) -> bool:
1982
2071
})
1983
2072
1984
2073
self ._handle_postgresql_restart_need ()
1985
-
1986
- # Restart the monitoring service if the password was rotated
1987
- container = self .unit .get_container ("postgresql" )
1988
- current_layer = container .get_plan ()
1989
- if (
1990
- metrics_service := current_layer .services [self ._metrics_service ]
1991
- ) and not metrics_service .environment .get ("DATA_SOURCE_NAME" , "" ).startswith (
1992
- f"user={ MONITORING_USER } password={ self .get_secret ('app' , MONITORING_PASSWORD_KEY )} "
1993
- ):
1994
- container .add_layer (
1995
- self ._metrics_service ,
1996
- Layer ({"services" : {self ._metrics_service : self ._generate_metrics_service ()}}),
1997
- combine = True ,
1998
- )
1999
- container .restart (self ._metrics_service )
2074
+ self ._restart_metrics_service ()
2075
+ self ._restart_ldap_sync_service ()
2000
2076
2001
2077
return True
2002
2078
@@ -2010,6 +2086,9 @@ def _validate_config_options(self) -> None:
2010
2086
"instance_default_text_search_config config option has an invalid value"
2011
2087
)
2012
2088
2089
+ if not self .postgresql .validate_group_map (self .config .ldap_map ):
2090
+ raise ValueError ("ldap_map config option has an invalid value" )
2091
+
2013
2092
if not self .postgresql .validate_date_style (self .config .request_date_style ):
2014
2093
raise ValueError ("request_date_style config option has an invalid value" )
2015
2094
@@ -2081,14 +2160,14 @@ def _update_pebble_layers(self, replan: bool = True) -> None:
2081
2160
# Check if there are any changes to layer services.
2082
2161
if current_layer .services != new_layer .services :
2083
2162
# Changes were made, add the new layer.
2084
- container .add_layer (self ._postgresql_service , new_layer , combine = True )
2163
+ container .add_layer (self .postgresql_service , new_layer , combine = True )
2085
2164
logging .info ("Added updated layer 'postgresql' to Pebble plan" )
2086
2165
if replan :
2087
2166
container .replan ()
2088
2167
logging .info ("Restarted postgresql service" )
2089
2168
if current_layer .checks != new_layer .checks :
2090
2169
# Changes were made, add the new layer.
2091
- container .add_layer (self ._postgresql_service , new_layer , combine = True )
2170
+ container .add_layer (self .postgresql_service , new_layer , combine = True )
2092
2171
logging .info ("Updated health checks" )
2093
2172
2094
2173
def _unit_name_to_pod_name (self , unit_name : str ) -> str :
0 commit comments