66
77import re
88import socket
9+ from contextlib import suppress
910from functools import cached_property
1011from typing import TYPE_CHECKING , Any , get_args
1112
1213from data_platform_helpers .advanced_statuses import StatusesState , StatusObject
1314from data_platform_helpers .advanced_statuses .types import Scope as AdvancedStatusesScope
14- from ops import ConfigData , JujuVersion , ModelError , Object , Relation , SecretNotFoundError , Unit
15+ from ops import (
16+ ConfigData ,
17+ JujuVersion ,
18+ ModelError ,
19+ Object ,
20+ Relation ,
21+ RelationNotFoundError ,
22+ SecretNotFoundError ,
23+ Unit ,
24+ )
1525
1626from single_kernel_postgresql .config .enums import Substrates
1727from single_kernel_postgresql .config .literals import (
1828 APP_SCOPE ,
29+ DATABASE ,
1930 PEER_RELATION ,
31+ REPLICATION_CONSUMER_RELATION ,
32+ REPLICATION_OFFER_RELATION ,
2033 SCOPES ,
2134 STATUS_PEERS_RELATION ,
2235)
@@ -153,6 +166,27 @@ def unit_ip(self) -> str | None:
153166 if binding := self .model .get_binding (PEER_RELATION ):
154167 return str (binding .network .bind_address )
155168
169+ @property
170+ def database_ip (self ) -> str | None :
171+ """Database endpoint address."""
172+ with suppress (RelationNotFoundError ):
173+ if binding := self .model .get_binding (DATABASE ):
174+ return str (binding .network .bind_address )
175+
176+ @property
177+ def replication_offer_ip (self ) -> str | None :
178+ """Async replication offer endpoint address."""
179+ with suppress (RelationNotFoundError ):
180+ if binding := self .model .get_binding (REPLICATION_OFFER_RELATION ):
181+ return str (binding .network .bind_address )
182+
183+ @property
184+ def replication_consumer_ip (self ) -> str | None :
185+ """Async replication consumer endpoint address."""
186+ with suppress (RelationNotFoundError ):
187+ if binding := self .model .get_binding (REPLICATION_CONSUMER_RELATION ):
188+ return str (binding .network .bind_address )
189+
156190 @property
157191 def fqdn (self ) -> str | None :
158192 """Current unit fqdn."""
@@ -165,17 +199,19 @@ def fqdn(self) -> str | None:
165199 def endpoint (self ) -> str | None :
166200 """Current unit endpoint."""
167201 if self .substrate == Substrates .K8S :
168- return self .fqdn
202+ return f" { self .peer . unit . name . replace ( '/' , '-' ) } . { self . application . app . name } -endpoints"
169203 else :
170204 return self .unit_ip
171205
172206 @property
173207 def endpoints (self ) -> set [str ]:
174208 """Returns the list of endpoints of the current members of the cluster."""
175209 if self .peer_relation :
176- return self .application .endpoints
177- else :
178- return {self .endpoint } if self .endpoint else set ()
210+ if self .substrate == Substrates .K8S :
211+ return self .application .endpoints
212+ else :
213+ return self .peer_members_ips
214+ return {self .endpoint } if self .endpoint else set ()
179215
180216 @property
181217 def model_name (self ) -> str :
@@ -185,7 +221,7 @@ def model_name(self) -> str:
185221 @cached_property
186222 def patroni_url (self ) -> str :
187223 """Patroni REST API URL."""
188- return f"https://{ self .unit_ip } :8007 "
224+ return f"https://{ self .endpoint } :8008 "
189225
190226 @property
191227 def peer_members_ips (self ) -> set [str ]:
@@ -227,7 +263,12 @@ def listen_ips(self) -> list[str]:
227263 ips = []
228264 if self .unit_ip :
229265 ips .append (self .unit_ip )
230- # TODO: Add other ips
266+ if self .database_ip and self .database_ip not in ips :
267+ ips .append (self .database_ip )
268+ if self .replication_offer_ip and self .replication_offer_ip not in ips :
269+ ips .append (self .replication_offer_ip )
270+ if self .replication_consumer_ip and self .replication_consumer_ip not in ips :
271+ ips .append (self .replication_consumer_ip )
231272 return ips
232273
233274 # -- Secrets
@@ -403,3 +444,38 @@ def _search_interpolated_status(
403444 if re .fullmatch (regex_pattern , present_status .message ) is not None :
404445 return present_status
405446 return None
447+
448+ @cached_property
449+ def synchronous_node_count (self ) -> int :
450+ """Number of expected sync standbys."""
451+ planned_units = self .application .planned_units
452+ if self .config .synchronous_node_count == "all" :
453+ return planned_units - 1
454+ elif self .config .synchronous_node_count == "majority" :
455+ return planned_units // 2
456+ # -1 for leader
457+ return (
458+ self .config .synchronous_node_count
459+ if self .config .synchronous_node_count < planned_units - 1
460+ else planned_units - 1
461+ )
462+
463+ @cached_property
464+ def synchronous_configuration (self ) -> dict [str , Any ]:
465+ """Synchronous mode configuration."""
466+ # Try to update synchronous_node_count.
467+ return {
468+ "synchronous_node_count" : self .synchronous_node_count ,
469+ "synchronous_mode_strict" : len (self .application .members_ips ) > 1
470+ and self .config .synchronous_mode_strict
471+ and self .synchronous_node_count > 0 ,
472+ }
473+
474+ def _build_service_name (self , service : str ) -> str :
475+ """Build a full k8s service name based on the service name."""
476+ return f"{ self .application .app .name } -{ service } .{ self .model_name } .svc.cluster.local"
477+
478+ @property
479+ def primary_endpoint (self ) -> str :
480+ """Returns the endpoint of the primary instance's service."""
481+ return self ._build_service_name ("primary" )
0 commit comments