-
Notifications
You must be signed in to change notification settings - Fork 11
[DPE-8863] Custom username and prefixes for v1 #258
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
c09144b
5509544
519be02
f61dd66
371fba2
d0513ac
a1e10e4
d77f03c
f112bbb
32bc391
e8b3da7
2b2ce9a
0e4d23d
d1f6e55
cd95a34
0e84996
0ba4f92
ed183ca
3262016
dd7386b
4a7774a
5c172e8
4ae1370
557065c
9e37b63
5d5a17b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -312,7 +312,7 @@ def _on_resource_requested(self, event: ResourceRequestedEvent) -> None: | |||||||||||||||||||
|
|
||||||||||||||||||||
| # Increment this PATCH version before using `charmcraft publish-lib` or reset | ||||||||||||||||||||
| # to 0 if you are raising the major API version | ||||||||||||||||||||
| LIBPATCH = 3 | ||||||||||||||||||||
| LIBPATCH = 4 | ||||||||||||||||||||
|
|
||||||||||||||||||||
| PYDEPS = ["ops>=2.0.0", "pydantic>=2.11"] | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
@@ -496,6 +496,9 @@ def store_new_data( | |||||||||||||||||||
| MtlsSecretStr = Annotated[OptionalSecretStr, Field(exclude=True, default=None), "mtls"] | ||||||||||||||||||||
| ExtraSecretStr = Annotated[OptionalSecretStr, Field(exclude=True, default=None), "extra"] | ||||||||||||||||||||
| EntitySecretStr = Annotated[OptionalSecretStr, Field(exclude=True, default=None), "entity"] | ||||||||||||||||||||
| RequestedEntitySecretStr = Annotated[ | ||||||||||||||||||||
| OptionalSecretStr, Field(exclude=True, default=None), "requested-entity" | ||||||||||||||||||||
| ] | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| class Scope(Enum): | ||||||||||||||||||||
|
|
@@ -833,9 +836,16 @@ def extract_secrets(self, info: ValidationInfo): | |||||||||||||||||||
| if not secret_uri: | ||||||||||||||||||||
| continue | ||||||||||||||||||||
|
|
||||||||||||||||||||
| secret = repository.get_secret( | ||||||||||||||||||||
| secret_group, secret_uri=secret_uri, short_uuid=short_uuid | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| try: | ||||||||||||||||||||
| secret = repository.get_secret( | ||||||||||||||||||||
| secret_group, secret_uri=secret_uri, short_uuid=short_uuid | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| except SecretNotFoundError: | ||||||||||||||||||||
| # v0 deletes the requested entity secret | ||||||||||||||||||||
| if secret_group == "requested-entity": | ||||||||||||||||||||
| logger.debug("Missing requested entity secret") | ||||||||||||||||||||
| continue | ||||||||||||||||||||
| raise | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if not secret: | ||||||||||||||||||||
| logger.info(f"No secret for group {secret_group} and short uuid {short_uuid}") | ||||||||||||||||||||
|
|
@@ -1002,6 +1012,13 @@ class RequirerCommonModel(CommonModel): | |||||||||||||||||||
| entity_permissions: list[EntityPermissionModel] | None = Field(default=None) | ||||||||||||||||||||
| secret_mtls: SecretString | None = Field(default=None) | ||||||||||||||||||||
| mtls_cert: MtlsSecretStr = Field(default=None) | ||||||||||||||||||||
| secret_requested_entity: SecretString | None = Field( | ||||||||||||||||||||
| default=None, | ||||||||||||||||||||
| validation_alias=AliasChoices("requested-entity-secret", "secret-requested-entity"), | ||||||||||||||||||||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should match both the old and new secret field. |
||||||||||||||||||||
| ) | ||||||||||||||||||||
| entity_name: RequestedEntitySecretStr = Field(default=None) | ||||||||||||||||||||
| entity_password: RequestedEntitySecretStr = Field(default=None, serialization_alias="password") | ||||||||||||||||||||
| prefix_matching: Literal["all", "only-existing"] | None = Field(default=None) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| @model_validator(mode="after") | ||||||||||||||||||||
| def validate_fields(self): | ||||||||||||||||||||
|
|
@@ -1015,6 +1032,9 @@ def validate_fields(self): | |||||||||||||||||||
| if self.entity_type == "GROUP" and self.extra_user_roles: | ||||||||||||||||||||
| raise ValueError("Inconsistent entity information. Use extra_group_roles instead") | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if self.entity_password and not self.entity_name: | ||||||||||||||||||||
| raise ValueError("Unable to set entity password without an entity name") | ||||||||||||||||||||
|
|
||||||||||||||||||||
| return self | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
@@ -1047,6 +1067,7 @@ class ResourceProviderModel(ProviderCommonModel): | |||||||||||||||||||
| entity_name: EntitySecretStr = Field(default=None) | ||||||||||||||||||||
| entity_password: EntitySecretStr = Field(default=None) | ||||||||||||||||||||
| version: str | None = Field(default=None) | ||||||||||||||||||||
| prefix_resources: str | None = Field(default=None) | ||||||||||||||||||||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wanted to use PlainSerializer to sort the CSV list, but it seems that the
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Interesting.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What I tried to do was:
With the serialiser being: data-platform-libs/lib/charms/data_platform_libs/v1/data_interfaces.py Lines 1032 to 1036 in 4a7774a
But then the field got turned into a secret. I guess that the |
||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| class RequirerDataContractV0(RequirerCommonModel): | ||||||||||||||||||||
|
|
@@ -2090,6 +2111,12 @@ class AuthenticationUpdatedEvent(ResourceRequirerEvent[TResourceProviderModel]): | |||||||||||||||||||
| pass | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| class ResourcePrefixResourcesChangedEvent(ResourceRequirerEvent[TResourceProviderModel]): | ||||||||||||||||||||
| """Prefix resources have changed.""" | ||||||||||||||||||||
|
|
||||||||||||||||||||
| pass | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| # Error Propagation Events | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
@@ -2148,6 +2175,7 @@ class ResourceRequiresEvents(CharmEvents, Generic[TResourceProviderModel]): | |||||||||||||||||||
| authentication_updated = EventSource(AuthenticationUpdatedEvent) | ||||||||||||||||||||
| status_raised = EventSource(StatusRaisedEvent) | ||||||||||||||||||||
| status_resolved = EventSource(StatusResolvedEvent) | ||||||||||||||||||||
| prefix_resources_changed = EventSource(ResourcePrefixResourcesChangedEvent) | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| ############################################################################## | ||||||||||||||||||||
|
|
@@ -2632,6 +2660,11 @@ def set_response(self, relation_id: int, response: ResourceProviderModel): | |||||||||||||||||||
| self.interface.write_model( | ||||||||||||||||||||
| relation_id, response, context={"version": "v0"} | ||||||||||||||||||||
| ) # {"database": "database-name", "secret-user": "uri", ...} | ||||||||||||||||||||
| # Set expected prefix field if present | ||||||||||||||||||||
| if response.prefix_resources: | ||||||||||||||||||||
| self.interface.repository(relation_id).write_field( | ||||||||||||||||||||
| "prefix-databases", response.prefix_resources | ||||||||||||||||||||
| ) | ||||||||||||||||||||
|
Comment on lines
+2664
to
+2667
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a better way to inject this field?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, this is because the field is renamed ?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the data-platform-libs/lib/charms/data_platform_libs/v1/data_interfaces.py Lines 2588 to 2596 in ba0faad
|
||||||||||||||||||||
| return | ||||||||||||||||||||
|
|
||||||||||||||||||||
| model = self.interface.build_model(relation_id, DataContractV1[response.__class__]) | ||||||||||||||||||||
|
|
@@ -2875,6 +2908,10 @@ def __init__( | |||||||||||||||||||
| f"{relation_alias}_read_only_endpoints_changed", | ||||||||||||||||||||
| ResourceReadOnlyEndpointsChangedEvent, | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| self.on.define_event( | ||||||||||||||||||||
| f"{relation_alias}_prefix_resources_changed", | ||||||||||||||||||||
| ResourcePrefixResourcesChangedEvent, | ||||||||||||||||||||
| ) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| ############################################################################## | ||||||||||||||||||||
| # Extra useful functions | ||||||||||||||||||||
|
|
@@ -3241,3 +3278,11 @@ def _handle_event( | |||||||||||||||||||
| ) | ||||||||||||||||||||
| self._emit_aliased_event(event, "authentication_updated", response) | ||||||||||||||||||||
| return | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if "prefix-resources" in _diff.added or "prefix-resources" in _diff.changed: | ||||||||||||||||||||
| logger.info(f"prefix resources updated for {response.resource} at {datetime.now()}") | ||||||||||||||||||||
| getattr(self.on, "prefix_resources_changed").emit( | ||||||||||||||||||||
| event.relation, app=event.app, unit=event.unit, response=response | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| self._emit_aliased_event(event, "prefix_resources_changed", response) | ||||||||||||||||||||
| return | ||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -208,7 +208,7 @@ def _on_database_pebble_ready(self, event: WorkloadEvent) -> None: | |
| "command": "/usr/local/bin/docker-entrypoint.sh postgres", | ||
| "startup": "enabled", | ||
| "environment": { | ||
| "PGDATA": "/var/lib/postgresql/data/pgdata", | ||
| "PGDATA": "/var/lib/postgresql/data/pgdata/data", | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For Canonical k8s compatibility (non-empty volume is provided). |
||
| "POSTGRES_PASSWORD": self._stored.password, | ||
| }, | ||
| } | ||
|
|
@@ -228,23 +228,30 @@ def _on_resource_requested(self, event: ResourceRequestedEvent) -> None: | |
|
|
||
| resource = request.resource | ||
| extra_user_roles = request.extra_user_roles | ||
| username = request.entity_name | ||
| password = request.entity_password | ||
|
|
||
| username = f"relation_{relation_id}_{request.request_id}" | ||
| password = self._new_password() | ||
| if resource[-1] == "*": | ||
| resources = [f"{resource[:-1]}1", f"{resource[:-1]}2"] | ||
| else: | ||
| resources = [resource] | ||
| username = username or f"relation_{relation_id}_{request.request_id}" | ||
| password = password or self._new_password() | ||
| connection_string = ( | ||
| "dbname='postgres' user='postgres' host='localhost' " | ||
| f"password='{self._stored.password}' connect_timeout=10" | ||
| ) | ||
| connection = psycopg2.connect(connection_string) | ||
| connection.autocommit = True | ||
| cursor = connection.cursor() | ||
| # Create the database, user and password. Also gives the user access to the database. | ||
| cursor.execute(f"CREATE DATABASE {resource};") | ||
| cursor.execute(f"CREATE USER {username} WITH ENCRYPTED PASSWORD '{password}';") | ||
| cursor.execute(f"GRANT ALL PRIVILEGES ON DATABASE {resource} TO {username};") | ||
| # Add the roles to the user. | ||
| if extra_user_roles: | ||
| cursor.execute(f"ALTER USER {username} {extra_user_roles};") | ||
| # Create the database, user and password. Also gives the user access to the database. | ||
| for resource in resources: | ||
| cursor.execute(f"CREATE DATABASE {resource};") | ||
| cursor.execute(f"GRANT ALL PRIVILEGES ON DATABASE {resource} TO {username};") | ||
| # Add the roles to the user. | ||
| if extra_user_roles: | ||
| cursor.execute(f"ALTER USER {username} {extra_user_roles};") | ||
| # Get the database version. | ||
| cursor.execute("SELECT version();") | ||
| version = cursor.fetchone()[0] | ||
|
|
@@ -272,6 +279,7 @@ def _on_resource_requested(self, event: ResourceRequestedEvent) -> None: | |
| username=username, | ||
| endpoints=f"{self.model.get_binding('database').network.bind_address}:5432", | ||
| version=version, | ||
| prefix_resources=",".join(resources) if len(resources) > 1 else None, | ||
| ) | ||
| self.database.set_response(event.relation.id, response) | ||
| self.unit.status = ActiveStatus() | ||
|
|
@@ -294,8 +302,11 @@ def _on_resource_entity_requested(self, event: ResourceEntityRequestedEvent) -> | |
| entity_type = request.entity_type | ||
|
|
||
| # Generate a entity-name and a entity-password for the application. | ||
| rolename = self._new_rolename() | ||
| password = self._new_password() | ||
| rolename = request.entity_name | ||
| password = request.entity_password | ||
|
|
||
| rolename = rolename or self._new_rolename() | ||
| password = password or self._new_password() | ||
|
|
||
| # Connect to the database. | ||
| connection_string = ( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
v0 will delete the helper secret once the relation was established.