Common charm lib for providers and requirers of object storage relation interfaces
Project description
Object Storage Charmlib
The object-storage-charmlib is a Python charm interface library for communication between object storage integrator charms and the requirer charms that relate with it. This library implements a common object-storage contract and the relation/event plumbing to publish
and consume storage connection info.
The following object storage providers are currently supported:
- AWS S3 (and S3 compliant providers)
- Azure Blob Storage / Azure Data Lake Storage (ADLS)
- Google Cloud Storage (GCS)
When two charms are related over an object storage relation interface, the one providing the object storage credentials is termed as Provider and the one that consumes those credentials is termed as Requirer. A provider publishes the payload when the requirer asks for it.
Table of contents
- Installation
- S3Provider class
- S3Requirer class
- AzureStorageProvider class
- AzureStorageRequirer class
- GCSProvider class
- GCSRequirer class
- Versioning and compatibility
- The
PrematureDataAccessErrorException
Installation
The lib can be installed from PyPI using pip:
pip install object-storage-charmlib
If you're using Poetry as packaging tool in your charm project, you can add the lib to the charm dependencies:
[tool.poetry.dependencies]
object-storage-charmlib = "^0.1.0"
S3Provider class
The S3Provider class can be used by the provider charm (e.g., s3-integrator) to share S3 bucket and connection information to the requirer charm (e.g., postgresql).
The provider needs to instantiate the S3Provider class, and then listen to storage_connection_info_requested custom event. When handling the event, the provider needs to set the S3 storage connection information using the function set_storage_connection_info in the S3Provider class.
from object_storage import (
StorageConnectionInfoRequestedEvent,
S3Provider,
)
class ExampleProviderCharm(CharmBase):
def __init__(self, charm: CharmBase):
super().__init__(charm, "s3-provider")
self.s3_provider = S3Provider(self, S3_RELATION_NAME)
self.framework.observe(
self.s3_provider.on.storage_connection_info_requested,
self._on_storage_connection_info_requested,
)
def _on_storage_connection_info_requested(
self, event: StorageConnectionInfoRequestedEvent
) -> None:
if not self.charm.unit.is_leader():
return
bucket_name = self.charm.config.get("bucket")
access_key, secret_key = prepare_keys(self.charm.config.get("credentials"))
self.s3_provider.set_storage_connection_info(
relation_id=event.relation.id,
data={"bucket": bucket_name, "access-key": access_key, "secret-key": secret_key}
)
The function set_storage_connection_info accepts a relation_id for the relation to which the data is to be updated, along with the data payload dictionary. To delete an existing field in the relation data, the value of the field should be set as an empty string ("") in the data payload dictionary.
S3Requirer class
The S3Requirer class can be used by the requirer charm (e.g., postgresql) to request and receive S3 bucket and credentials from the provider charm (e.g., s3-integrator).
The requirer charm needs to instantiate the S3Requirer class -- optionally with additional request for a particular bucket and/or a path -- and then listen to custom events storage_connection_info_changed and storage_connection_info_gone. When handling the event, the requirer charm can access the S3 storage connection information shared by the
provider charm using the function get_storage_connection_info in the S3Requirer class.
from object_storage import (
StorageConnectionInfoChangedEvent,
StorageConnectionInfoGoneEvent,
S3Info,
S3Requirer,
)
class ExampleRequirerCharm(CharmBase):
def __init__(
self,
charm: CharmBase,
):
super().__init__(charm, "s3-requirer")
self.charm = charm
self.s3_client = S3Requirer(
charm,
relation_name,
bucket="test-bucket", # bucket requested by the requirer
path="test-path", # path requested by the requirer
)
self.framework.observe(
self.s3_client.on.storage_connection_info_changed, self._on_conn_info_changed
)
self.framework.observe(
self.s3_client.on.storage_connection_info_gone, self._on_conn_info_gone
)
def _on_conn_info_changed(self, event: StorageConnectionInfoChangedEvent):
# access data from the provider
connection_info: S3Info = self.s3_client.get_storage_connection_info()
process_connection_info(connection_info)
def _on_conn_info_gone(self, event: StorageConnectionInfoGoneEvent):
# credentials are removed
process_connection_info(None)
The get_storage_connection_info function in S3Requirer returns a typed dictionary of type S3Info which has the following definition:
S3Info = TypedDict(
"S3Info",
{
"access-key": str,
"secret-key": str,
"region": str,
"storage-class": str,
"attributes": str,
"bucket": str,
"endpoint": str,
"path": str,
"s3-api-version": str,
"s3-uri-style": str,
"tls-ca-chain": List[str],
"delete-older-than-days": str,
},
total=False,
)
AzureStorageProvider class
The AzureStorageProvider class can be used by the provider charm (e.g., azure-storage-integrator) to share Azure Blob Storage and Azure Data Lake Storage connection information to the requirer charm (e.g., mongodb).
The provider needs to instantiate the AzureStorageProvider class, and then listen to storage_connection_info_requested custom event. When handling the event, the provider needs to set the Azure Storage connection information using the function set_storage_connection_info in the AzureStorageProvider class.
from object_storage import (
AzureStorageProvider,
StorageConnectionInfoRequestedEvent,
)
class ExampleProviderCharm(CharmBase):
def __init__(self, charm: CharmBase):
super().__init__(charm, "azure-storage-provider")
self.azure_storage_provider = AzureStorageProvider(self, AZURE_STORAGE_RELATION_NAME)
self.framework.observe(
self.azure_storage_provider.on.storage_connection_info_requested,
self._on_storage_connection_info_requested,
)
def _on_storage_connection_info_requested(
self, event: StorageConnectionInfoRequestedEvent
) -> None:
if not self.charm.unit.is_leader():
return
container_name = self.charm.config.get("container")
secret_key = prepare_keys(self.charm.config.get("credentials"))
self.azure_storage_provider.set_storage_connection_info(
relation_id=event.relation.id,
data={"container": container_name, "secret-key": secret_key}
)
The function set_storage_connection_info accepts a relation_id for the relation to which the data is to be updated, along with the data payload dictionary. To delete an existing field in the relation data, the value of the field should be set as an empty string ("") in the data payload dictionary.
AzureStorageRequirer class
The AzureStorageRequirer class can be used by the requirer charm (e.g., mongodb) to request and receive Azure Storage credentials from the provider charm (e.g., azure-storage-integrator).
The requirer charm needs to instantiate the AzureStorageRequirer class -- optionally with additional request for a particular container -- and then listen to custom events storage_connection_info_changed and storage_connection_info_gone. When handling the event, the requirer charm can access the Azure Storage connection information shared by the
provider charm using the function get_storage_connection_info in the AzureStorageRequirer class.
from object_storage import (
AzureStorageInfo,
AzureStorageRequirer,
StorageConnectionInfoChangedEvent,
StorageConnectionInfoGoneEvent,
)
class ExampleRequirerCharm(CharmBase):
def __init__(
self,
charm: CharmBase,
):
super().__init__(charm, "azure-storage-requirer")
self.charm = charm
self.azure_storage_client = AzureStorageRequirer(
charm,
relation_name,
container="test-container" # container requested by the requirer
)
self.framework.observe(
self.azure_storage_client.on.storage_connection_info_changed, self._on_conn_info_changed
)
self.framework.observe(
self.azure_storage_client.on.storage_connection_info_gone, self._on_conn_info_gone
)
def _on_conn_info_changed(self, event: StorageConnectionInfoChangedEvent):
# access data from the provider
connection_info: AzureStorageInfo = self.azure_storage_client.get_storage_connection_info()
process_connection_info(connection_info)
def _on_conn_info_gone(self, event: StorageConnectionInfoGoneEvent):
# credentials are removed
process_connection_info(None)
The get_storage_connection_info function in AzureStorageRequirer returns a typed dictionary of type AzureStorageInfo which has the following definition:
AzureStorageInfo = TypedDict(
"AzureStorageInfo",
{
"container": str,
"storage-account": str,
"secret-key": str,
"connection-protocol": str,
"path": str,
"endpoint": str,
"resource-group": str,
},
total=False,
)
GCSProvider class
The GCSProvider class can be used by the provider charm (e.g., gcs-integrator) to share Google Cloud Storage connection information to the requirer charm (e.g., opensearch).
The provider needs to instantiate the GCSProvider class, and then listen to storage_connection_info_requested custom event. When handling the event, the provider needs to set the GCS connection information using the function set_storage_connection_info in the GCSProvider class.
from object_storage import (
GCSProvider,
StorageConnectionInfoRequestedEvent,
)
class ExampleProviderCharm(CharmBase):
def __init__(self, charm: CharmBase):
super().__init__(charm, "gcs-provider")
self.gcs_provider = GCSProvider(self, GCS_RELATION_NAME)
self.framework.observe(
self.gcs_provider.on.storage_connection_info_requested,
self._on_storage_connection_info_requested,
)
def _on_storage_connection_info_requested(
self, event: StorageConnectionInfoRequestedEvent
) -> None:
if not self.charm.unit.is_leader():
return
bucket_name = self.charm.config.get("bucket")
secret_key = prepare_keys(self.charm.config.get("credentials"))
self.gcs_provider.set_storage_connection_info(
relation_id=event.relation.id,
data={"bucket": bucket_name, "secret-key": secret_key}
)
The function set_storage_connection_info accepts a relation_id for the relation to which the data is to be updated, along with the data payload dictionary. To delete an existing field in the relation data, the value of the field should be set as an empty string ("") in the data payload dictionary.
GCSRequirer class
The GCSRequirer class can be used by the requirer charm (e.g., opensearch) to request and receive Google Cloud Storage credentials from the provider charm (e.g., gcs-integrator).
The requirer charm needs to instantiate the GCSRequirer class -- optionally with additional request for a particular bucket -- and then listen to custom events storage_connection_info_changed and storage_connection_info_gone. When handling the event, the requirer charm can access the GCS storage connection information shared by the provider charm using the function get_storage_connection_info in the GCSRequirer class.
from object_storage import (
GCSInfo,
GCSRequirer,
StorageConnectionInfoChangedEvent,
StorageConnectionInfoGoneEvent,
)
class ExampleRequirerCharm(CharmBase):
def __init__(
self,
charm: CharmBase,
):
super().__init__(charm, "gcs-requirer")
self.charm = charm
self.gcs_client = GCSRequirer(
charm,
relation_name,
bucket="test-bucket" # bucket requested by the requirer
)
self.framework.observe(
self.gcs_client.on.storage_connection_info_changed, self._on_conn_info_changed
)
self.framework.observe(
self.gcs_client.on.storage_connection_info_gone, self._on_conn_info_gone
)
def _on_conn_info_changed(self, event: StorageConnectionInfoChangedEvent):
# access data from the provider
connection_info: GCSInfo = self.gcs_client.get_storage_connection_info()
process_connection_info(connection_info)
def _on_conn_info_gone(self, event: StorageConnectionInfoGoneEvent):
# credentials are removed
process_connection_info(None)
The get_storage_connection_info function in GCSRequirer returns a typed dictionary of type GCSInfo which has the following definition:
GCSInfo = TypedDict(
"GCSInfo",
{
"bucket": str,
"secret-key": str,
"storage-class": str,
"path": str,
},
total=False,
)
Versioning and compatibility
This library consolidates the s3, azure_storage and gcs_storage charm libs that previously existed as separate charm libs into a single common lib, such that the provider and requirer classes across all object storage relation interfaces can reuse a common codebase and be better maintained in the long run.
This library currently follows schema v1 for relation payloads, and to distinguish this new schema with what existed before when the interface libs were separated, the older charmlib s3, azure_storage and gcs_storage are assumed to follow the schema v0.
What's new in schema v1?
- The provider now shares sensitive information over the relation as Juju secrets instead of plaintext. In the earlier
s3lib, this was shared as plaintext. However, for compatibility, the provider will still send the data as plaintext if it detects the requirer is still using the old lib. - The provider as well as requirers now advertise the version of schema they're using in the relation databag. This is done so that the other side can know the schema version this side is currently using, and act accordingly to ensure compatibility.
Compatibility notes
S3Providercan detect older requirers and keep backward-compatible behavior.S3Requirercan detect older providers and apply compatibility fallbacks. This however makes an assumption on a specific order of execution of relation events, and hence it is still recommended to upgrade the provider to new version at the earliest timeframe possible.
Migration guidance (old charmlibs to the new charmlib)
It is highly recommended that you first upgrade the storage integrator charm to the latest track and revision before you upgrade your charm to use the new object-storage-charmlib. Please follow the guide for s3-integrator, azure-storage-integrator and gcs-integrator respectively for this migration.
To upgrade your charms from using the old object storage charmlibs to the new lib, follow the following steps:
- Update your charm's dependencies to include the
object-storage-charmlibPython package. - Update charm codebase to use the new requirer classes, custom events and functions in the new lib from their old counterparts. Please follow the usage instructions for
S3Requirer,AzureStorageRequirerandGCSRequirerfor this purpose. A few common changes (however not an exhaustive list) are:- Update the references of
S3Requirer,AzureStorageRequirerandGCSRequirerto their counterparts from the new lib. - Listen to custom events
storage_connection_info_changedandstorage_connection_info_gonein the charm code instead ofs3_connection_info_changed,s3_connection_info_gone, etc. - Update function calls like
get_s3_connection_info,get_azure_storage_connection_infoandget_gcs_connection_infoto a more generic functionget_storage_connection_info.
- Update the references of
- Delete the old charm lib inside the
liborsrcsection of your charm codebase. - Update your charm's unit and integration tests to make them compatible with the newer lib.
Dependency pinning recommendation
For production charms, pin object-storage-charmlib to a compatible minor version range (for example ^0.1.0) and validate upgrades in integration tests before promoting to stable channels.
The PrematureDataAccessError exception
The PrematureDataAccessError exception is raised by the lib when the provider charm attempts to update the relation data before the relation protocol has been fully initialized.
There are valid use cases where the provider charm may want to update the connection information on charm lifecycle events like config-changed, etc. When the relation data is attempted to be updated when the relation is not completely initialized, there might be risks of the provider sharing secret data over plaintext, sharing data with incorrect schema with respect to the schema used by the requirer, etc. To prevent these edge cases, the lib raises PrematureDataAccessError when the set_storage_connection_info function is called while the relation is yet not fully initialized.
The calls to the function set_storage_connection_info in the handlers of events outside the context of the object storage relation should properly handle the PrematureDataAccessError, while deferring the event for execution later when the relation will have completed initialization. The following is an example of how this can be done, in the context of S3 interface.
from object_storage import (
PrematureDataAccessError,
S3Provider,
)
class ExampleProviderCharm(CharmBase):
def __init__(self, charm: CharmBase):
super().__init__(charm, "s3-provider")
self.s3_provider = S3Provider(self, S3_RELATION_NAME)
self.framework.observe(
charm.on.config_changed,
self._on_config_changed,
)
...
def _on_config_changed(
self, event
) -> None:
if not self.charm.unit.is_leader():
return
bucket_name = self.charm.config.get("bucket")
access_key, secret_key = prepare_keys(self.charm.config.get("credentials"))
try:
self.s3_provider.set_storage_connection_info(
relation_id=event.relation.id,
data={"bucket": bucket_name, "access-key": access_key, "secret-key": secret_key}
)
except PrematureDataAccessError:
logging.error("Attempted to update relation data before relation is initialized.")
event.defer()
return
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file object_storage_charmlib-1.0.0.tar.gz.
File metadata
- Download URL: object_storage_charmlib-1.0.0.tar.gz
- Upload date:
- Size: 30.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7bb853f9d1d14273e8bc603964e8582b93ea635aff0619612f7c8b6bce6da900
|
|
| MD5 |
58a357586bf4bccdf6ed364e49ceb062
|
|
| BLAKE2b-256 |
bf82dcdb48f2c04d789fa7b2ca8a9c704646283e1998891c9d1c25f5c945501f
|
Provenance
The following attestation bundles were made for object_storage_charmlib-1.0.0.tar.gz:
Publisher:
publish-lib.yaml on canonical/object-storage-integrator
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
object_storage_charmlib-1.0.0.tar.gz -
Subject digest:
7bb853f9d1d14273e8bc603964e8582b93ea635aff0619612f7c8b6bce6da900 - Sigstore transparency entry: 1211569957
- Sigstore integration time:
-
Permalink:
canonical/object-storage-integrator@27355b444c2a6c620b32bdda6816cc3d99e97eb0 -
Branch / Tag:
refs/tags/lib/v1.0.0 - Owner: https://github.com/canonical
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-lib.yaml@27355b444c2a6c620b32bdda6816cc3d99e97eb0 -
Trigger Event:
push
-
Statement type:
File details
Details for the file object_storage_charmlib-1.0.0-py3-none-any.whl.
File metadata
- Download URL: object_storage_charmlib-1.0.0-py3-none-any.whl
- Upload date:
- Size: 33.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
17e95686ddeac3dd83dc6f9276d75f967fdd84b5658e3660d6ef1fe72b3ad56e
|
|
| MD5 |
ea3cb2df35508f28cfc1732797195875
|
|
| BLAKE2b-256 |
adb5e791aa2ae783a419f728be6bb090606285142b267f172437965e769ce7d5
|
Provenance
The following attestation bundles were made for object_storage_charmlib-1.0.0-py3-none-any.whl:
Publisher:
publish-lib.yaml on canonical/object-storage-integrator
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
object_storage_charmlib-1.0.0-py3-none-any.whl -
Subject digest:
17e95686ddeac3dd83dc6f9276d75f967fdd84b5658e3660d6ef1fe72b3ad56e - Sigstore transparency entry: 1211570015
- Sigstore integration time:
-
Permalink:
canonical/object-storage-integrator@27355b444c2a6c620b32bdda6816cc3d99e97eb0 -
Branch / Tag:
refs/tags/lib/v1.0.0 - Owner: https://github.com/canonical
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-lib.yaml@27355b444c2a6c620b32bdda6816cc3d99e97eb0 -
Trigger Event:
push
-
Statement type: