@@ -287,9 +287,9 @@ def _on_database_requested(self, event: DatabaseRequestedEvent) -> None:
287287from typing_extensions import TypeAliasType , override
288288
289289try :
290- import psycopg
290+ import psycopg2
291291except ImportError :
292- psycopg = None
292+ psycopg2 = None
293293
294294# The unique Charmhub library identifier, never change it
295295LIBID = "6c3e6b6680d64e9c89e611d1a15f65be"
@@ -654,6 +654,7 @@ class PeerModel(BaseModel):
654654 populate_by_name = True ,
655655 serialize_by_alias = True ,
656656 alias_generator = lambda x : x .replace ("_" , "-" ),
657+ extra = "allow" ,
657658 )
658659
659660 @model_validator (mode = "after" )
@@ -680,6 +681,8 @@ def extract_secrets(self, info: ValidationInfo):
680681
681682 if value and field_info .annotation == OptionalSecretBool :
682683 value = SecretBool (json .loads (value ))
684+ elif value :
685+ value = SecretStr (value )
683686 setattr (self , field , value )
684687
685688 return self
@@ -706,9 +709,11 @@ def serialize_model(self, handler: SerializerFunctionWrapHandler, info: Serializ
706709 actual_value = (
707710 value .get_secret_value () if issubclass (value .__class__ , _SecretBase ) else value
708711 )
712+ if not isinstance (actual_value , str ):
713+ actual_value = json .dumps (actual_value )
709714
710715 if secret is None :
711- if actual_value :
716+ if value :
712717 secret = repository .add_secret (
713718 aliased_field ,
714719 actual_value ,
@@ -721,11 +726,9 @@ def serialize_model(self, handler: SerializerFunctionWrapHandler, info: Serializ
721726 content = secret .get_content ()
722727 full_content = copy .deepcopy (content )
723728
724- if actual_value is None :
725- full_content .pop (field , None )
729+ if value is None :
730+ full_content .pop (aliased_field , None )
726731 else :
727- if not isinstance (actual_value , str ):
728- actual_value = json .dumps (actual_value )
729732 full_content .update ({aliased_field : actual_value })
730733 secret .set_content (full_content )
731734 return handler (self )
@@ -744,6 +747,7 @@ class CommonModel(BaseModel):
744747 populate_by_name = True ,
745748 serialize_by_alias = True ,
746749 alias_generator = lambda x : x .replace ("_" , "-" ),
750+ extra = "allow" ,
747751 )
748752
749753 resource : str = Field (validation_alias = AliasChoices (* RESOURCE_ALIASES ), default = "" )
@@ -787,6 +791,9 @@ def extract_secrets(self, info: ValidationInfo):
787791 value = secret .get_content ().get (aliased_field )
788792 if value and field_info .annotation == OptionalSecretBool :
789793 value = SecretBool (json .loads (value ))
794+ elif value :
795+ value = SecretStr (value )
796+
790797 setattr (self , field , value )
791798 return self
792799
@@ -822,9 +829,11 @@ def serialize_model(self, handler: SerializerFunctionWrapHandler, info: Serializ
822829 actual_value = (
823830 value .get_secret_value () if issubclass (value .__class__ , _SecretBase ) else value
824831 )
832+ if not isinstance (actual_value , str ):
833+ actual_value = json .dumps (actual_value )
825834
826835 if secret is None :
827- if actual_value :
836+ if value :
828837 secret = repository .add_secret (
829838 aliased_field , actual_value , secret_group , short_uuid
830839 )
@@ -836,12 +845,10 @@ def serialize_model(self, handler: SerializerFunctionWrapHandler, info: Serializ
836845 content = secret .get_content ()
837846 full_content = copy .deepcopy (content )
838847
839- if actual_value is None :
840- full_content .pop (field , None )
848+ if value is None :
849+ full_content .pop (aliased_field , None )
841850 _encountered_secrets .add ((secret , secret_field ))
842851 else :
843- if not isinstance (actual_value , str ):
844- actual_value = json .dumps (actual_value )
845852 full_content .update ({aliased_field : actual_value })
846853 secret .set_content (full_content )
847854
@@ -1003,10 +1010,11 @@ class DataContractV1(BaseModel, Generic[TResourceProviderModel]):
10031010TCommonModel = TypeVar ("TCommonModel" , bound = CommonModel )
10041011
10051012
1006- def is_topic_value_acceptable (value : str | None ):
1013+ def is_topic_value_acceptable (value : str | None ) -> str | None :
10071014 """Check whether the given Kafka topic value is acceptable."""
10081015 if value and "*" in value [:3 ]:
10091016 raise ValueError (f"Error on topic '{ value } ',, unacceptable value." )
1017+ return value
10101018
10111019
10121020class KafkaRequestModel (RequirerCommonModel ):
@@ -1709,9 +1717,12 @@ def write_model(
17091717 """Writes the data stored in the model using the repository object."""
17101718 context = context or {}
17111719 dumped = model .model_dump (
1712- mode = "json" , context = {"repository" : repository } | context , exclude_none = True
1720+ mode = "json" , context = {"repository" : repository } | context , exclude_none = False
17131721 )
17141722 for field , value in dumped .items ():
1723+ if value is None :
1724+ repository .delete_field (field )
1725+ continue
17151726 dumped_value = value if isinstance (value , str ) else json .dumps (value )
17161727 repository .write_field (field , dumped_value )
17171728
@@ -1951,13 +1962,19 @@ class ResourceEntityCreatedEvent(ResourceRequirerEvent[TResourceProviderModel]):
19511962
19521963
19531964class ResourceEndpointsChangedEvent (ResourceRequirerEvent [TResourceProviderModel ]):
1954- """Read/Write enpoinds are changed."""
1965+ """Read/Write enpoints are changed."""
19551966
19561967 pass
19571968
19581969
19591970class ResourceReadOnlyEndpointsChangedEvent (ResourceRequirerEvent [TResourceProviderModel ]):
1960- """Read-only enpoinds are changed."""
1971+ """Read-only enpoints are changed."""
1972+
1973+ pass
1974+
1975+
1976+ class AuthenticationUpdatedEvent (ResourceRequirerEvent [TResourceProviderModel ]):
1977+ """Authentication was updated for a user."""
19611978
19621979 pass
19631980
@@ -1972,6 +1989,7 @@ class ResourceRequiresEvents(CharmEvents, Generic[TResourceProviderModel]):
19721989 resource_entity_created = EventSource (ResourceEntityCreatedEvent )
19731990 endpoints_changed = EventSource (ResourceEndpointsChangedEvent )
19741991 read_only_endpoints_changed = EventSource (ResourceReadOnlyEndpointsChangedEvent )
1992+ authentication_updated = EventSource (AuthenticationUpdatedEvent )
19751993
19761994
19771995##############################################################################
@@ -2072,6 +2090,34 @@ def compute_diff(
20722090
20732091 return _diff
20742092
2093+ def _relation_from_secret_label (self , secret_label : str ) -> Relation | None :
2094+ """Retrieve the relation that belongs to a secret label."""
2095+ contents = secret_label .split ("." )
2096+
2097+ if not (contents and len (contents ) >= 3 ):
2098+ return
2099+
2100+ try :
2101+ relation_id = int (contents [1 ])
2102+ except ValueError :
2103+ return
2104+
2105+ relation_name = contents [0 ]
2106+
2107+ try :
2108+ return self .model .get_relation (relation_name , relation_id )
2109+ except ModelError :
2110+ return
2111+
2112+ def _short_uuid_from_secret_label (self , secret_label : str ) -> str | None :
2113+ """Retrieve the relation that belongs to a secret label."""
2114+ contents = secret_label .split ("." )
2115+
2116+ if not (contents and len (contents ) >= 5 ):
2117+ return
2118+
2119+ return contents [2 ]
2120+
20752121
20762122class ResourceProviderEventHandler (EventHandlers , Generic [TRequirerCommonModel ]):
20772123 """Event Handler for resource provider."""
@@ -2204,34 +2250,6 @@ def _handle_bulk_event(
22042250 )
22052251 store_new_data (event .relation , self .component , new_data , request .request_id )
22062252
2207- def _relation_from_secret_label (self , secret_label : str ) -> Relation | None :
2208- """Retrieve the relation that belongs to a secret label."""
2209- contents = secret_label .split ("." )
2210-
2211- if not (contents and len (contents ) >= 3 ):
2212- return
2213-
2214- try :
2215- relation_id = int (contents [1 ])
2216- except ValueError :
2217- return
2218-
2219- relation_name = contents [0 ]
2220-
2221- try :
2222- return self .model .get_relation (relation_name , relation_id )
2223- except ModelError :
2224- return
2225-
2226- def _short_uuid_from_secret_label (self , secret_label : str ) -> str | None :
2227- """Retrieve the relation that belongs to a secret label."""
2228- contents = secret_label .split ("." )
2229-
2230- if not (contents and len (contents ) >= 5 ):
2231- return
2232-
2233- return contents [2 ]
2234-
22352253 @override
22362254 def _on_secret_changed_event (self , event : SecretChangedEvent ) -> None :
22372255 if not self .mtls_enabled :
@@ -2251,6 +2269,11 @@ def _on_secret_changed_event(self, event: SecretChangedEvent) -> None:
22512269
22522270 if relation .app == self .charm .app :
22532271 logging .info ("Secret changed event ignored for Secret Owner" )
2272+ return
2273+
2274+ if relation .name != self .relation_name :
2275+ logging .info ("Secret changed on wrong relation." )
2276+ return
22542277
22552278 remote_unit = None
22562279 for unit in relation .units :
@@ -2470,39 +2493,39 @@ def are_all_resources_created(self, rel_id: int) -> bool:
24702493 def _is_pg_plugin_enabled (plugin : str , connection_string : str ) -> bool :
24712494 # Actual checking method.
24722495 # No need to check for psycopg here, it's been checked before.
2473- if not psycopg :
2496+ if not psycopg2 :
24742497 return False
24752498
24762499 try :
2477- with psycopg .connect (connection_string ) as connection :
2500+ with psycopg2 .connect (connection_string ) as connection :
24782501 with connection .cursor () as cursor :
24792502 cursor .execute (
24802503 "SELECT TRUE FROM pg_extension WHERE extname=%s::text;" , (plugin ,)
24812504 )
24822505 return cursor .fetchone () is not None
2483- except psycopg .Error as e :
2506+ except psycopg2 .Error as e :
24842507 logger .exception (
24852508 f"failed to check whether { plugin } plugin is enabled in the database: %s" ,
24862509 str (e ),
24872510 )
24882511 return False
24892512
2490- def is_postgresql_plugin_enabled (self , plugin : str , relation_id : int = 0 ) -> bool :
2513+ def is_postgresql_plugin_enabled (self , plugin : str , relation_index : int = 0 ) -> bool :
24912514 """Returns whether a plugin is enabled in the database.
24922515
24932516 Args:
24942517 plugin: name of the plugin to check.
2495- relation_id : Optional index to check the database (default: 0 - first relation).
2518+ relation_index : Optional index to check the database (default: 0 - first relation).
24962519 """
2497- if not psycopg :
2520+ if not psycopg2 :
24982521 return False
24992522
25002523 # Can't check a non existing relation.
2501- if len (self .relations ) <= relation_id :
2524+ if len (self .relations ) <= relation_index :
25022525 return False
25032526
2504- relation_id = self .relations [relation_id ]. id
2505- model = self .interface .build_model (relation_id = relation_id )
2527+ relation = self .relations [relation_index ]
2528+ model = self .interface .build_model (relation_id = relation . id , component = relation . app )
25062529 for request in model .requests :
25072530 if request .endpoints and request .username and request .password :
25082531 host = request .endpoints .split (":" )[0 ]
@@ -2577,7 +2600,48 @@ def _get_relation_alias(self, relation_id: int) -> str | None:
25772600
25782601 def _on_secret_changed_event (self , event : SecretChangedEvent ):
25792602 """Event notifying about a new value of a secret."""
2580- pass
2603+ if not event .secret .label :
2604+ return
2605+ relation = self ._relation_from_secret_label (event .secret .label )
2606+ short_uuid = self ._short_uuid_from_secret_label (event .secret .label )
2607+
2608+ if not relation :
2609+ logging .info (
2610+ f"Received secret { event .secret .label } but couldn't parse, seems irrelevant"
2611+ )
2612+ return
2613+
2614+ if relation .app == self .charm .app :
2615+ logging .info ("Secret changed event ignored for Secret Owner" )
2616+ return
2617+
2618+ if relation .name != self .relation_name :
2619+ logging .info ("Secret changed on wrong relation." )
2620+ return
2621+
2622+ remote_unit = None
2623+ for unit in relation .units :
2624+ if unit .app != self .charm .app :
2625+ remote_unit = unit
2626+ break
2627+
2628+ response_model = self .interface .build_model (relation .id )
2629+ if not short_uuid :
2630+ return
2631+ for _response in response_model .requests :
2632+ if _response .request_id == short_uuid :
2633+ response = _response
2634+ break
2635+ else :
2636+ logger .info (f"Unknown request id { short_uuid } " )
2637+ return
2638+
2639+ getattr (self .on , "authentication_updated" ).emit (
2640+ relation ,
2641+ app = relation .app ,
2642+ unit = remote_unit ,
2643+ response = response ,
2644+ )
25812645
25822646 def _on_relation_created_event (self , event : RelationCreatedEvent ) -> None :
25832647 """Event emitted when the database relation is created."""
0 commit comments