From 9ecb34833205952d7b727e19d94e5d4016216728 Mon Sep 17 00:00:00 2001 From: David Wilding Date: Fri, 26 Dec 2025 13:48:29 +0800 Subject: [PATCH 1/7] reduce wordy explanation of fetch_postgres_relation_data function --- .../integrate-your-charm-with-postgresql.md | 15 ++++++--------- examples/k8s-3-postgresql/src/charm.py | 13 +++++-------- examples/k8s-4-action/src/charm.py | 13 +++++-------- examples/k8s-5-observe/src/charm.py | 13 +++++-------- 4 files changed, 21 insertions(+), 33 deletions(-) diff --git a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md index 9dbb1d1dc..7fe52a414 100644 --- a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md +++ b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md @@ -145,18 +145,15 @@ framework.observe(self.database.on.endpoints_changed, self._on_database_created) ### Fetch the database authentication data -Now we need to extract the database authentication data and endpoints information. We can do that by adding a `fetch_postgres_relation_data` method to our charm class. Inside this method, we first retrieve relation data from the PostgreSQL using the `fetch_relation_data` method of the `database` object. We then log the retrieved data for debugging purposes. Next we process any non-empty data to extract endpoint information, the username, and the password and return this process data as a dictionary. Finally, we ensure that, if no data is retrieved, we return an empty dictionary, so that the caller knows that the database is not yet ready. +Now we need to extract the database authentication data and endpoints information. We can do that by adding a `fetch_postgres_relation_data` method to our charm class: ```python def fetch_postgres_relation_data(self) -> dict[str, str]: - """Fetch postgres relation data. - - This function retrieves relation data from a postgres database using - the `fetch_relation_data` method of the `database` object. The retrieved data is - then logged for debugging purposes, and any non-empty data is processed to extract - endpoint information, username, and password. This processed data is then returned as - a dictionary. If no data is retrieved, the unit is set to waiting status and - the program exits with a zero status code. + """Retrieve relation data from a postgres database. + + Any non-empty data is processed to extract endpoint information, username, + and password. If no data is retrieved, return an empty dictionary so that + the caller knows that the database is not yet ready. """ relations = self.database.fetch_relation_data() logger.debug('Got following database data: %s', relations) diff --git a/examples/k8s-3-postgresql/src/charm.py b/examples/k8s-3-postgresql/src/charm.py index eab79f255..19ebb6579 100755 --- a/examples/k8s-3-postgresql/src/charm.py +++ b/examples/k8s-3-postgresql/src/charm.py @@ -187,14 +187,11 @@ def get_app_environment(self) -> dict[str, str]: return env def fetch_postgres_relation_data(self) -> dict[str, str]: - """Fetch postgres relation data. - - This function retrieves relation data from a postgres database using - the `fetch_relation_data` method of the `database` object. The retrieved data is - then logged for debugging purposes, and any non-empty data is processed to extract - endpoint information, username, and password. This processed data is then returned as - a dictionary. If no data is retrieved, the unit is set to waiting status and - the program exits with a zero status code. + """Retrieve relation data from a postgres database. + + Any non-empty data is processed to extract endpoint information, username, + and password. If no data is retrieved, return an empty dictionary so that + the caller knows that the database is not yet ready. """ relations = self.database.fetch_relation_data() logger.debug('Got following database data: %s', relations) diff --git a/examples/k8s-4-action/src/charm.py b/examples/k8s-4-action/src/charm.py index 212359952..ca1e7738b 100755 --- a/examples/k8s-4-action/src/charm.py +++ b/examples/k8s-4-action/src/charm.py @@ -228,14 +228,11 @@ def get_app_environment(self) -> dict[str, str]: return env def fetch_postgres_relation_data(self) -> dict[str, str]: - """Fetch postgres relation data. - - This function retrieves relation data from a postgres database using - the `fetch_relation_data` method of the `database` object. The retrieved data is - then logged for debugging purposes, and any non-empty data is processed to extract - endpoint information, username, and password. This processed data is then returned as - a dictionary. If no data is retrieved, the unit is set to waiting status and - the program exits with a zero status code. + """Retrieve relation data from a postgres database. + + Any non-empty data is processed to extract endpoint information, username, + and password. If no data is retrieved, return an empty dictionary so that + the caller knows that the database is not yet ready. """ relations = self.database.fetch_relation_data() logger.debug('Got following database data: %s', relations) diff --git a/examples/k8s-5-observe/src/charm.py b/examples/k8s-5-observe/src/charm.py index 44dfe5eeb..50a55ca8b 100755 --- a/examples/k8s-5-observe/src/charm.py +++ b/examples/k8s-5-observe/src/charm.py @@ -249,14 +249,11 @@ def get_app_environment(self) -> dict[str, str]: return env def fetch_postgres_relation_data(self) -> dict[str, str]: - """Fetch postgres relation data. - - This function retrieves relation data from a postgres database using - the `fetch_relation_data` method of the `database` object. The retrieved data is - then logged for debugging purposes, and any non-empty data is processed to extract - endpoint information, username, and password. This processed data is then returned as - a dictionary. If no data is retrieved, the unit is set to waiting status and - the program exits with a zero status code. + """Retrieve relation data from a postgres database. + + Any non-empty data is processed to extract endpoint information, username, + and password. If no data is retrieved, return an empty dictionary so that + the caller knows that the database is not yet ready. """ relations = self.database.fetch_relation_data() logger.debug('Got following database data: %s', relations) From 7d700b3568aef37838686e5e82494fb816c9ac76 Mon Sep 17 00:00:00 2001 From: David Wilding Date: Fri, 26 Dec 2025 15:14:34 +0800 Subject: [PATCH 2/7] reorganise methods and simplify explanations --- .../integrate-your-charm-with-postgresql.md | 66 ++++++++----------- examples/k8s-3-postgresql/src/charm.py | 16 +---- examples/k8s-4-action/src/charm.py | 16 +---- examples/k8s-5-observe/src/charm.py | 16 +---- 4 files changed, 34 insertions(+), 80 deletions(-) diff --git a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md index 7fe52a414..493634783 100644 --- a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md +++ b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md @@ -145,16 +145,32 @@ framework.observe(self.database.on.endpoints_changed, self._on_database_created) ### Fetch the database authentication data -Now we need to extract the database authentication data and endpoints information. We can do that by adding a `fetch_postgres_relation_data` method to our charm class: +Our application consumes database authentication data in the form of environment variables. Let's define a method that prepares database authentication data in that form: ```python -def fetch_postgres_relation_data(self) -> dict[str, str]: - """Retrieve relation data from a postgres database. +def get_app_environment(self) -> dict[str, str]: + """Return a dictionary of environment variables for the application.""" + db_data = self.fetch_postgres_relation_data() + if not db_data: + return {} + env = { + key: value + for key, value in { + 'DEMO_SERVER_DB_HOST': db_data.get('db_host', None), + 'DEMO_SERVER_DB_PORT': db_data.get('db_port', None), + 'DEMO_SERVER_DB_USER': db_data.get('db_username', None), + 'DEMO_SERVER_DB_PASSWORD': db_data.get('db_password', None), + }.items() + if value is not None + } + return env +``` - Any non-empty data is processed to extract endpoint information, username, - and password. If no data is retrieved, return an empty dictionary so that - the caller knows that the database is not yet ready. - """ +This method depends on the following method, which extracts the database authentication data: + +```python +def fetch_postgres_relation_data(self) -> dict[str, str]: + """Retrieve relation data from a postgres database.""" relations = self.database.fetch_relation_data() logger.debug('Got following database data: %s', relations) for data in relations.values(): @@ -174,7 +190,9 @@ def fetch_postgres_relation_data(self) -> dict[str, str]: ### Share the authentication information with your application -Our application consumes database authentication information in the form of environment variables. Let's update the Pebble service definition with an `environment` key and let's set this key to a dynamic value. Update the `_update_layer_and_restart()` method to read in the environment and pass it in when creating the Pebble layer: +Let's change the Pebble service definition to include a dynamic `environment` key. + +First, update `_update_layer_and_restart()` to provide environment variables when creating the Pebble layer: ```python def _update_layer_and_restart(self) -> None: @@ -213,9 +231,9 @@ def _update_layer_and_restart(self) -> None: logger.info('Unable to connect to Pebble: %s', e) ``` -We've also removed three `self.unit.status = ` lines. We'll handle replacing those shortly. +In this version of the method, we removed three `self.unit.status = ` lines. We'll handle replacing those shortly. -Now, update your `_get_pebble_layer()` method to use the passed environment: +Next, update `_get_pebble_layer()` to put the environment variables in the Pebble layer: ```python def _get_pebble_layer(self, port: int, environment: dict[str, str]) -> ops.pebble.Layer: @@ -244,34 +262,6 @@ def _get_pebble_layer(self, port: int, environment: dict[str, str]) -> ops.pebbl return ops.pebble.Layer(pebble_layer) ``` -Now, let's define this method such that, every time it is called, it dynamically fetches database authentication data and also prepares the output in a form that our application can consume, as below: - -```python -def get_app_environment(self) -> dict[str, str]: - """Prepare environment variables for the application. - - This property method creates a dictionary containing environment variables - for the application. It retrieves the database authentication data by calling - the `fetch_postgres_relation_data` method and uses it to populate the dictionary. - If any of the values are not present, it will be set to None. - The method returns this dictionary as output. - """ - db_data = self.fetch_postgres_relation_data() - if not db_data: - return {} - env = { - key: value - for key, value in { - 'DEMO_SERVER_DB_HOST': db_data.get('db_host', None), - 'DEMO_SERVER_DB_PORT': db_data.get('db_port', None), - 'DEMO_SERVER_DB_USER': db_data.get('db_username', None), - 'DEMO_SERVER_DB_PASSWORD': db_data.get('db_password', None), - }.items() - if value is not None - } - return env -``` - Finally, let's define the method that is called on the database created event: ```python diff --git a/examples/k8s-3-postgresql/src/charm.py b/examples/k8s-3-postgresql/src/charm.py index 19ebb6579..c50472fa1 100755 --- a/examples/k8s-3-postgresql/src/charm.py +++ b/examples/k8s-3-postgresql/src/charm.py @@ -163,14 +163,7 @@ def _get_pebble_layer(self, port: int, environment: dict[str, str]) -> ops.pebbl return ops.pebble.Layer(pebble_layer) def get_app_environment(self) -> dict[str, str]: - """Prepare environment variables for the application. - - This property method creates a dictionary containing environment variables - for the application. It retrieves the database authentication data by calling - the `fetch_postgres_relation_data` method and uses it to populate the dictionary. - If any of the values are not present, it will be set to None. - The method returns this dictionary as output. - """ + """Return a dictionary of environment variables for the application.""" db_data = self.fetch_postgres_relation_data() if not db_data: return {} @@ -187,12 +180,7 @@ def get_app_environment(self) -> dict[str, str]: return env def fetch_postgres_relation_data(self) -> dict[str, str]: - """Retrieve relation data from a postgres database. - - Any non-empty data is processed to extract endpoint information, username, - and password. If no data is retrieved, return an empty dictionary so that - the caller knows that the database is not yet ready. - """ + """Retrieve relation data from a postgres database.""" relations = self.database.fetch_relation_data() logger.debug('Got following database data: %s', relations) for data in relations.values(): diff --git a/examples/k8s-4-action/src/charm.py b/examples/k8s-4-action/src/charm.py index ca1e7738b..01346c81b 100755 --- a/examples/k8s-4-action/src/charm.py +++ b/examples/k8s-4-action/src/charm.py @@ -204,14 +204,7 @@ def _get_pebble_layer(self, port: int, environment: dict[str, str]) -> ops.pebbl return ops.pebble.Layer(pebble_layer) def get_app_environment(self) -> dict[str, str]: - """Prepare environment variables for the application. - - This property method creates a dictionary containing environment variables - for the application. It retrieves the database authentication data by calling - the `fetch_postgres_relation_data` method and uses it to populate the dictionary. - If any of the values are not present, it will be set to None. - The method returns this dictionary as output. - """ + """Return a dictionary of environment variables for the application.""" db_data = self.fetch_postgres_relation_data() if not db_data: return {} @@ -228,12 +221,7 @@ def get_app_environment(self) -> dict[str, str]: return env def fetch_postgres_relation_data(self) -> dict[str, str]: - """Retrieve relation data from a postgres database. - - Any non-empty data is processed to extract endpoint information, username, - and password. If no data is retrieved, return an empty dictionary so that - the caller knows that the database is not yet ready. - """ + """Retrieve relation data from a postgres database.""" relations = self.database.fetch_relation_data() logger.debug('Got following database data: %s', relations) for data in relations.values(): diff --git a/examples/k8s-5-observe/src/charm.py b/examples/k8s-5-observe/src/charm.py index 50a55ca8b..6cdfa8c2a 100755 --- a/examples/k8s-5-observe/src/charm.py +++ b/examples/k8s-5-observe/src/charm.py @@ -225,14 +225,7 @@ def _get_pebble_layer(self, port: int, environment: dict[str, str]) -> ops.pebbl return ops.pebble.Layer(pebble_layer) def get_app_environment(self) -> dict[str, str]: - """Prepare environment variables for the application. - - This property method creates a dictionary containing environment variables - for the application. It retrieves the database authentication data by calling - the `fetch_postgres_relation_data` method and uses it to populate the dictionary. - If any of the values are not present, it will be set to None. - The method returns this dictionary as output. - """ + """Return a dictionary of environment variables for the application.""" db_data = self.fetch_postgres_relation_data() if not db_data: return {} @@ -249,12 +242,7 @@ def get_app_environment(self) -> dict[str, str]: return env def fetch_postgres_relation_data(self) -> dict[str, str]: - """Retrieve relation data from a postgres database. - - Any non-empty data is processed to extract endpoint information, username, - and password. If no data is retrieved, return an empty dictionary so that - the caller knows that the database is not yet ready. - """ + """Retrieve relation data from a postgres database.""" relations = self.database.fetch_relation_data() logger.debug('Got following database data: %s', relations) for data in relations.values(): From 6ab9674a75e20cf91104c206869a0c34ac4b6f67 Mon Sep 17 00:00:00 2001 From: David Wilding Date: Fri, 26 Dec 2025 15:22:46 +0800 Subject: [PATCH 3/7] move _on_database_created --- .../integrate-your-charm-with-postgresql.md | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md index 493634783..e5417ca98 100644 --- a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md +++ b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md @@ -84,7 +84,7 @@ To do so, we need to update our charm `src/charm.py` to do all of the following: ### Import the database interface libraries -First, at the top of the file, import the database interfaces library: +At the top of `src/charm.py`, import the database interfaces library: ```python # Import the 'data_interfaces' library. @@ -126,7 +126,7 @@ export PYTHONPATH=lib:$PYTHONPATH ### Add relation event observers -Next, in the `__init__` method, define a new instance of the 'DatabaseRequires' class. This is required to set the right permissions scope for the PostgreSQL charm. It will create a new user with a password and a database with the required name (below, `names_db`), and limit the user permissions to only this particular database (that is, below, `names_db`). +In the `__init__` method, define a new instance of the 'DatabaseRequires' class. This is required to set the right permissions scope for the PostgreSQL charm. It will create a new user with a password and a database with the required name (below, `names_db`), and limit the user permissions to only this particular database (that is, below, `names_db`). ```python @@ -135,7 +135,7 @@ Next, in the `__init__` method, define a new instance of the 'DatabaseRequires' self.database = DatabaseRequires(self, relation_name='database', database_name='names_db') ``` -Now, add event observers for all the database events: +Next, add event observers for all the database events: ```python # See https://charmhub.io/data-platform-libs/libraries/data_interfaces @@ -143,6 +143,18 @@ framework.observe(self.database.on.database_created, self._on_database_created) framework.observe(self.database.on.endpoints_changed, self._on_database_created) ``` +Finally, define the method that is called on the database created event: + +```python +def _on_database_created( + self, _: DatabaseCreatedEvent | DatabaseEndpointsChangedEvent +) -> None: + """Event is fired when postgres database is created or endpoint is changed.""" + self._update_layer_and_restart() +``` + +Now we need to make sure that our application knows how to access the database. + ### Fetch the database authentication data Our application consumes database authentication data in the form of environment variables. Let's define a method that prepares database authentication data in that form: @@ -262,16 +274,6 @@ def _get_pebble_layer(self, port: int, environment: dict[str, str]) -> ops.pebbl return ops.pebble.Layer(pebble_layer) ``` -Finally, let's define the method that is called on the database created event: - -```python -def _on_database_created( - self, _: DatabaseCreatedEvent | DatabaseEndpointsChangedEvent -) -> None: - """Event is fired when postgres database is created or endpoint is changed.""" - self._update_layer_and_restart() -``` - The diagram below illustrates the workflow for the case where the database relation exists and for the case where it does not: ![Integrate your charm with PostgreSQL](../../resources/integrate_your_charm_with_postgresql.png) From 62a4b581e78e869d222824531b54c25e035e0369 Mon Sep 17 00:00:00 2001 From: David Wilding Date: Fri, 26 Dec 2025 15:56:50 +0800 Subject: [PATCH 4/7] replace workflow diagram by text --- .../integrate_your_charm_with_postgresql.png | Bin 19035 -> 0 bytes .../integrate-your-charm-with-postgresql.md | 15 ++++++++++----- 2 files changed, 10 insertions(+), 5 deletions(-) delete mode 100644 docs/resources/integrate_your_charm_with_postgresql.png diff --git a/docs/resources/integrate_your_charm_with_postgresql.png b/docs/resources/integrate_your_charm_with_postgresql.png deleted file mode 100644 index c17393597f7cc1b4ee797826735b0db688fba8d8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19035 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 E0J%WL)Bpeg diff --git a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md index e5417ca98..08de008c8 100644 --- a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md +++ b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md @@ -153,7 +153,7 @@ def _on_database_created( self._update_layer_and_restart() ``` -Now we need to make sure that our application knows how to access the database. +We now need to make sure that our application knows how to access the database. ### Fetch the database authentication data @@ -243,7 +243,7 @@ def _update_layer_and_restart(self) -> None: logger.info('Unable to connect to Pebble: %s', e) ``` -In this version of the method, we removed three `self.unit.status = ` lines. We'll handle replacing those shortly. +We removed three `self.unit.status = ` lines from this version of the method. We'll handle replacing those shortly. Next, update `_get_pebble_layer()` to put the environment variables in the Pebble layer: @@ -274,9 +274,14 @@ def _get_pebble_layer(self, port: int, environment: dict[str, str]) -> ops.pebbl return ops.pebble.Layer(pebble_layer) ``` -The diagram below illustrates the workflow for the case where the database relation exists and for the case where it does not: +With these changes, we've made sure that our application knows how to access the database. -![Integrate your charm with PostgreSQL](../../resources/integrate_your_charm_with_postgresql.png) +When Pebble starts or restarts the service: + +* If there's a database relation and database authentication data is available from the relation, our application can get the database authentication data from environment variables. +* Otherwise, the service environment is empty, so our application can't get database authentication data. In this case, we'd like the unit to show `blocked` or `maintenance` status, depending on whether the Juju user needs to take action. + +We'll now make sure that the unit status is set correctly. (integrate-your-charm-with-postgresql-update-unit-status)= ## Update the unit status to reflect the relation state @@ -478,7 +483,7 @@ Now run `tox -e unit` to make sure all test cases pass. ## Write an integration test -Now that our charm integrates with the PostgreSQL database, if there's not a database relation, the app will be in `blocked` status instead of `active`. Let's tweak our existing integration test `test_deploy` accordingly, setting the expected status as `blocked` in `juju.wait`: +Now that our charm integrates with the PostgreSQL database, if there isn't a database relation, the app will be in `blocked` status instead of `active`. Let's tweak our existing integration test `test_deploy` accordingly, setting the expected status as `blocked` in `juju.wait`: ```python import logging From ba4e2efc5f696c7fe0ee5d750a0fc9a2ffdb20a4 Mon Sep 17 00:00:00 2001 From: David Wilding Date: Fri, 26 Dec 2025 16:03:15 +0800 Subject: [PATCH 5/7] small fixes --- .../integrate-your-charm-with-postgresql.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md index 08de008c8..f967de03e 100644 --- a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md +++ b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md @@ -143,7 +143,7 @@ framework.observe(self.database.on.database_created, self._on_database_created) framework.observe(self.database.on.endpoints_changed, self._on_database_created) ``` -Finally, define the method that is called on the database created event: +Finally, define the method that is called on the database events: ```python def _on_database_created( @@ -200,7 +200,7 @@ def fetch_postgres_relation_data(self) -> dict[str, str]: return {} ``` -### Share the authentication information with your application +### Share the authentication data with your application Let's change the Pebble service definition to include a dynamic `environment` key. @@ -483,7 +483,7 @@ Now run `tox -e unit` to make sure all test cases pass. ## Write an integration test -Now that our charm integrates with the PostgreSQL database, if there isn't a database relation, the app will be in `blocked` status instead of `active`. Let's tweak our existing integration test `test_deploy` accordingly, setting the expected status as `blocked` in `juju.wait`: +Now that our charm integrates with the PostgreSQL database, if there's not a database relation, the app will be in `blocked` status instead of `active`. Let's tweak our existing integration test `test_deploy` accordingly, setting the expected status as `blocked` in `juju.wait`: ```python import logging From 6e77f7e8aa77b1257222c6b9c468259ca68c4cd4 Mon Sep 17 00:00:00 2001 From: David Wilding Date: Wed, 7 Jan 2026 08:10:24 +0800 Subject: [PATCH 6/7] rename _on_database_created --- .../integrate-your-charm-with-postgresql.md | 6 +++--- examples/k8s-3-postgresql/src/charm.py | 6 +++--- examples/k8s-4-action/src/charm.py | 6 +++--- examples/k8s-5-observe/src/charm.py | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md index f967de03e..20e60a1f9 100644 --- a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md +++ b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md @@ -139,14 +139,14 @@ Next, add event observers for all the database events: ```python # See https://charmhub.io/data-platform-libs/libraries/data_interfaces -framework.observe(self.database.on.database_created, self._on_database_created) -framework.observe(self.database.on.endpoints_changed, self._on_database_created) +framework.observe(self.database.on.database_created, self._on_database_endpoint) +framework.observe(self.database.on.endpoints_changed, self._on_database_endpoint) ``` Finally, define the method that is called on the database events: ```python -def _on_database_created( +def _on_database_endpoint( self, _: DatabaseCreatedEvent | DatabaseEndpointsChangedEvent ) -> None: """Event is fired when postgres database is created or endpoint is changed.""" diff --git a/examples/k8s-3-postgresql/src/charm.py b/examples/k8s-3-postgresql/src/charm.py index c50472fa1..50907b056 100755 --- a/examples/k8s-3-postgresql/src/charm.py +++ b/examples/k8s-3-postgresql/src/charm.py @@ -66,8 +66,8 @@ def __init__(self, framework: ops.Framework) -> None: # The 'database_name' is the name of the database that our application requires. self.database = DatabaseRequires(self, relation_name='database', database_name='names_db') # See https://charmhub.io/data-platform-libs/libraries/data_interfaces - framework.observe(self.database.on.database_created, self._on_database_created) - framework.observe(self.database.on.endpoints_changed, self._on_database_created) + framework.observe(self.database.on.database_created, self._on_database_endpoint) + framework.observe(self.database.on.endpoints_changed, self._on_database_endpoint) def _on_demo_server_pebble_ready(self, _: ops.PebbleReadyEvent) -> None: self._update_layer_and_restart() @@ -96,7 +96,7 @@ def _on_collect_status(self, event: ops.CollectStatusEvent) -> None: # If nothing is wrong, then the status is active. event.add_status(ops.ActiveStatus()) - def _on_database_created( + def _on_database_endpoint( self, _: DatabaseCreatedEvent | DatabaseEndpointsChangedEvent ) -> None: """Event is fired when postgres database is created or endpoint is changed.""" diff --git a/examples/k8s-4-action/src/charm.py b/examples/k8s-4-action/src/charm.py index 01346c81b..415ff6353 100755 --- a/examples/k8s-4-action/src/charm.py +++ b/examples/k8s-4-action/src/charm.py @@ -75,8 +75,8 @@ def __init__(self, framework: ops.Framework) -> None: # The 'database_name' is the name of the database that our application requires. self.database = DatabaseRequires(self, relation_name='database', database_name='names_db') # See https://charmhub.io/data-platform-libs/libraries/data_interfaces - framework.observe(self.database.on.database_created, self._on_database_created) - framework.observe(self.database.on.endpoints_changed, self._on_database_created) + framework.observe(self.database.on.database_created, self._on_database_endpoint) + framework.observe(self.database.on.endpoints_changed, self._on_database_endpoint) # Events on charm actions that are run via 'juju run'. framework.observe(self.on.get_db_info_action, self._on_get_db_info_action) @@ -107,7 +107,7 @@ def _on_collect_status(self, event: ops.CollectStatusEvent) -> None: # If nothing is wrong, then the status is active. event.add_status(ops.ActiveStatus()) - def _on_database_created( + def _on_database_endpoint( self, _: DatabaseCreatedEvent | DatabaseEndpointsChangedEvent ) -> None: """Event is fired when postgres database is created.""" diff --git a/examples/k8s-5-observe/src/charm.py b/examples/k8s-5-observe/src/charm.py index 6cdfa8c2a..c8238099b 100755 --- a/examples/k8s-5-observe/src/charm.py +++ b/examples/k8s-5-observe/src/charm.py @@ -78,8 +78,8 @@ def __init__(self, framework: ops.Framework) -> None: # The 'database_name' is the name of the database that our application requires. self.database = DatabaseRequires(self, relation_name='database', database_name='names_db') # See https://charmhub.io/data-platform-libs/libraries/data_interfaces - framework.observe(self.database.on.database_created, self._on_database_created) - framework.observe(self.database.on.endpoints_changed, self._on_database_created) + framework.observe(self.database.on.database_created, self._on_database_endpoint) + framework.observe(self.database.on.endpoints_changed, self._on_database_endpoint) # Events on charm actions that are run via 'juju run'. framework.observe(self.on.get_db_info_action, self._on_get_db_info_action) # Enable pushing application logs to Loki. @@ -128,7 +128,7 @@ def _on_collect_status(self, event: ops.CollectStatusEvent) -> None: # If nothing is wrong, then the status is active. event.add_status(ops.ActiveStatus()) - def _on_database_created( + def _on_database_endpoint( self, _: DatabaseCreatedEvent | DatabaseEndpointsChangedEvent ) -> None: """Event is fired when postgres database is created.""" From 9eee9a6e69ab41d54b6e9c79bd05c69bbf7aab5f Mon Sep 17 00:00:00 2001 From: David Wilding Date: Wed, 7 Jan 2026 08:11:05 +0800 Subject: [PATCH 7/7] rename _update_layer_and_restart --- .../integrate-your-charm-with-postgresql.md | 8 ++++---- .../make-your-charm-configurable.md | 12 ++++++------ examples/k8s-2-configurable/src/charm.py | 6 +++--- examples/k8s-3-postgresql/src/charm.py | 8 ++++---- examples/k8s-4-action/src/charm.py | 8 ++++---- examples/k8s-5-observe/src/charm.py | 8 ++++---- 6 files changed, 25 insertions(+), 25 deletions(-) diff --git a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md index 20e60a1f9..9cf5cc946 100644 --- a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md +++ b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md @@ -150,7 +150,7 @@ def _on_database_endpoint( self, _: DatabaseCreatedEvent | DatabaseEndpointsChangedEvent ) -> None: """Event is fired when postgres database is created or endpoint is changed.""" - self._update_layer_and_restart() + self._replan_workload() ``` We now need to make sure that our application knows how to access the database. @@ -204,10 +204,10 @@ def fetch_postgres_relation_data(self) -> dict[str, str]: Let's change the Pebble service definition to include a dynamic `environment` key. -First, update `_update_layer_and_restart()` to provide environment variables when creating the Pebble layer: +First, update `_replan_workload()` to provide environment variables when creating the Pebble layer: ```python -def _update_layer_and_restart(self) -> None: +def _replan_workload(self) -> None: """Define and start a workload using the Pebble API. You'll need to specify the right entrypoint and environment @@ -322,7 +322,7 @@ def _on_collect_status(self, event: ops.CollectStatusEvent) -> None: event.add_status(ops.ActiveStatus()) ``` -We also want to clean up the code to remove the places where we're setting the status outside of this method, other than anywhere we're wanting a status to show up *during* the event execution (such as `MaintenanceStatus`). If you missed doing so above, in `_update_layer_and_restart`, remove the lines: +We also want to clean up the code to remove the places where we're setting the status outside of this method, other than anywhere we're wanting a status to show up *during* the event execution (such as `MaintenanceStatus`). If you missed doing so above, in `_replan_workload`, remove the lines: ```python self.unit.status = ops.ActiveStatus() diff --git a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/make-your-charm-configurable.md b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/make-your-charm-configurable.md index 6afa1e720..02559efb3 100644 --- a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/make-your-charm-configurable.md +++ b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/make-your-charm-configurable.md @@ -75,10 +75,10 @@ Now, define the handler, as below. Since configuring something like a port affec ```python def _on_config_changed(self, _: ops.ConfigChangedEvent) -> None: - self._update_layer_and_restart() + self._replan_workload() ``` -We'll define `_update_layer_and_restart` shortly. +We'll define `_replan_workload` shortly. ```{caution} @@ -95,7 +95,7 @@ self.container = self.unit.get_container('demo-server') Create a new method, as below. This method will get the current Pebble layer configuration and compare the new and the existing service definitions -- if they differ, it will update the layer and restart the service. ```python -def _update_layer_and_restart(self) -> None: +def _replan_workload(self) -> None: """Define and start a workload using the Pebble API. You'll need to specify the right entrypoint and environment @@ -130,7 +130,7 @@ def _update_layer_and_restart(self) -> None: self.unit.status = ops.MaintenanceStatus('Waiting for Pebble in workload container') ``` -When the config is loaded as part of creating the Pebble layer, if the config is invalid (in our case, if the port is set to 22), then a `ValueError` will be raised. The `_update_layer_and_restart` method handles that by logging the error and setting the status of the unit to blocked, letting the Juju user know that they need to take action. +When the config is loaded as part of creating the Pebble layer, if the config is invalid (in our case, if the port is set to 22), then a `ValueError` will be raised. The `_replan_workload` method handles that by logging the error and setting the status of the unit to blocked, letting the Juju user know that they need to take action. Now, crucially, update the `_get_pebble_layer` method to make the layer definition dynamic, as shown below. This will replace the static port `8000` with the port passed to the method. @@ -160,11 +160,11 @@ def _get_pebble_layer(self, port: int) -> ops.pebble.Layer: return ops.pebble.Layer(pebble_layer) ``` -As you may have noticed, the new `_update_layer_and_restart` method looks like a more advanced variant of the existing `_on_demo_server_pebble_ready` method. Remove the body of the `_on_demo_server_pebble_ready` method and replace it a call to `_update_layer_and_restart` like this: +As you may have noticed, the new `_replan_workload` method looks like a more advanced variant of the existing `_on_demo_server_pebble_ready` method. Remove the body of the `_on_demo_server_pebble_ready` method and replace it a call to `_replan_workload` like this: ```python def _on_demo_server_pebble_ready(self, _: ops.PebbleReadyEvent) -> None: - self._update_layer_and_restart() + self._replan_workload() ``` ## Validate your charm diff --git a/examples/k8s-2-configurable/src/charm.py b/examples/k8s-2-configurable/src/charm.py index c088fe53b..0fc76e678 100755 --- a/examples/k8s-2-configurable/src/charm.py +++ b/examples/k8s-2-configurable/src/charm.py @@ -53,12 +53,12 @@ def __init__(self, framework: ops.Framework) -> None: framework.observe(self.on.config_changed, self._on_config_changed) def _on_demo_server_pebble_ready(self, _: ops.PebbleReadyEvent) -> None: - self._update_layer_and_restart() + self._replan_workload() def _on_config_changed(self, _: ops.ConfigChangedEvent) -> None: - self._update_layer_and_restart() + self._replan_workload() - def _update_layer_and_restart(self) -> None: + def _replan_workload(self) -> None: """Define and start a workload using the Pebble API. You'll need to specify the right entrypoint and environment diff --git a/examples/k8s-3-postgresql/src/charm.py b/examples/k8s-3-postgresql/src/charm.py index 50907b056..b4a48f723 100755 --- a/examples/k8s-3-postgresql/src/charm.py +++ b/examples/k8s-3-postgresql/src/charm.py @@ -70,10 +70,10 @@ def __init__(self, framework: ops.Framework) -> None: framework.observe(self.database.on.endpoints_changed, self._on_database_endpoint) def _on_demo_server_pebble_ready(self, _: ops.PebbleReadyEvent) -> None: - self._update_layer_and_restart() + self._replan_workload() def _on_config_changed(self, _: ops.ConfigChangedEvent) -> None: - self._update_layer_and_restart() + self._replan_workload() def _on_collect_status(self, event: ops.CollectStatusEvent) -> None: try: @@ -100,9 +100,9 @@ def _on_database_endpoint( self, _: DatabaseCreatedEvent | DatabaseEndpointsChangedEvent ) -> None: """Event is fired when postgres database is created or endpoint is changed.""" - self._update_layer_and_restart() + self._replan_workload() - def _update_layer_and_restart(self) -> None: + def _replan_workload(self) -> None: """Define and start a workload using the Pebble API. You'll need to specify the right entrypoint and environment diff --git a/examples/k8s-4-action/src/charm.py b/examples/k8s-4-action/src/charm.py index 415ff6353..da6d50960 100755 --- a/examples/k8s-4-action/src/charm.py +++ b/examples/k8s-4-action/src/charm.py @@ -81,10 +81,10 @@ def __init__(self, framework: ops.Framework) -> None: framework.observe(self.on.get_db_info_action, self._on_get_db_info_action) def _on_demo_server_pebble_ready(self, _: ops.PebbleReadyEvent) -> None: - self._update_layer_and_restart() + self._replan_workload() def _on_config_changed(self, _: ops.ConfigChangedEvent) -> None: - self._update_layer_and_restart() + self._replan_workload() def _on_collect_status(self, event: ops.CollectStatusEvent) -> None: try: @@ -111,7 +111,7 @@ def _on_database_endpoint( self, _: DatabaseCreatedEvent | DatabaseEndpointsChangedEvent ) -> None: """Event is fired when postgres database is created.""" - self._update_layer_and_restart() + self._replan_workload() def _on_get_db_info_action(self, event: ops.ActionEvent) -> None: """Return information about the integrated database. @@ -143,7 +143,7 @@ def _on_get_db_info_action(self, event: ops.ActionEvent) -> None: ) event.set_results(output) - def _update_layer_and_restart(self) -> None: + def _replan_workload(self) -> None: """Define and start a workload using the Pebble API. You'll need to specify the right entrypoint and environment diff --git a/examples/k8s-5-observe/src/charm.py b/examples/k8s-5-observe/src/charm.py index c8238099b..f1d1c71dc 100755 --- a/examples/k8s-5-observe/src/charm.py +++ b/examples/k8s-5-observe/src/charm.py @@ -102,10 +102,10 @@ def __init__(self, framework: ops.Framework) -> None: ) def _on_demo_server_pebble_ready(self, _: ops.PebbleReadyEvent) -> None: - self._update_layer_and_restart() + self._replan_workload() def _on_config_changed(self, _: ops.ConfigChangedEvent) -> None: - self._update_layer_and_restart() + self._replan_workload() def _on_collect_status(self, event: ops.CollectStatusEvent) -> None: try: @@ -132,7 +132,7 @@ def _on_database_endpoint( self, _: DatabaseCreatedEvent | DatabaseEndpointsChangedEvent ) -> None: """Event is fired when postgres database is created.""" - self._update_layer_and_restart() + self._replan_workload() def _on_get_db_info_action(self, event: ops.ActionEvent) -> None: """Return information about the integrated database. @@ -164,7 +164,7 @@ def _on_get_db_info_action(self, event: ops.ActionEvent) -> None: ) event.set_results(output) - def _update_layer_and_restart(self) -> None: + def _replan_workload(self) -> None: """Define and start a workload using the Pebble API. You'll need to specify the right entrypoint and environment