diff --git a/README.md b/README.md
index 7563742..6ec93fc 100644
--- a/README.md
+++ b/README.md
@@ -50,6 +50,7 @@ To configure the S3 integrator charm, you may provide the following configuratio
- storage-class:the storage class for objects uploaded to the object storage.
- tls-ca-chain: the complete CA chain, which can be used for HTTPS validation.
- s3-api-version: the S3 protocol specific API signature.
+- experimental-delete-older-than-days: the amount of day after which backups going to be deleted. EXPERIMENTAL option.
The only mandatory fields for the integrator are access-key secret-key and bucket.
diff --git a/config.yaml b/config.yaml
index 0e59887..9a70a2d 100644
--- a/config.yaml
+++ b/config.yaml
@@ -36,3 +36,13 @@ options:
type: string
description: S3 protocol specific API signature.
default: ''
+ experimental-delete-older-than-days:
+ type: int
+ description: |
+ Full backups can be retained for a number of days. The removal of expired
+ backups happens imediatelly after finishing the first successful backup after
+ retention period.
+ When full backup expires, the all differential and incremental backups which
+ depends on this full backup also expires.
+ This option is EXPRERIMENTAL.
+ Allowed values are: from 1 to 9999999.
diff --git a/lib/charms/data_platform_libs/v0/s3.py b/lib/charms/data_platform_libs/v0/s3.py
index 7beb113..f5614aa 100644
--- a/lib/charms/data_platform_libs/v0/s3.py
+++ b/lib/charms/data_platform_libs/v0/s3.py
@@ -137,7 +137,7 @@ def _on_credential_gone(self, event: CredentialsGoneEvent):
# Increment this PATCH version before using `charmcraft publish-lib` or reset
# to 0 if you are raising the major API version
-LIBPATCH = 4
+LIBPATCH = 5
logger = logging.getLogger(__name__)
@@ -212,7 +212,7 @@ class S3CredentialEvents(CharmEvents):
class S3Provider(Object):
"""A provider handler for communicating S3 credentials to consumers."""
- on = S3CredentialEvents() # pyright: ignore [reportGeneralTypeIssues]
+ on = S3CredentialEvents() # pyright: ignore [reportAssignmentType]
def __init__(
self,
@@ -481,6 +481,18 @@ def set_s3_api_version(self, relation_id: int, s3_api_version: str) -> None:
"""
self.update_connection_info(relation_id, {"s3-api-version": s3_api_version})
+ def set_delete_older_than_days(self, relation_id: int, days: int) -> None:
+ """Sets the retention days for full backups in application databag.
+
+ This function writes in the application data bag, therefore,
+ only the leader unit can call it.
+
+ Args:
+ relation_id: the identifier for a particular relation.
+ days: the value.
+ """
+ self.update_connection_info(relation_id, {"delete-older-than-days": str(days)})
+
def set_attributes(self, relation_id: int, attributes: List[str]) -> None:
"""Sets the connection attributes in application databag.
@@ -580,6 +592,17 @@ def s3_api_version(self) -> Optional[str]:
return self.relation.data[self.relation.app].get("s3-api-version")
+ @property
+ def delete_older_than_days(self) -> Optional[int]:
+ """Returns the retention days for full backups."""
+ if not self.relation.app:
+ return None
+
+ days = self.relation.data[self.relation.app].get("delete-older-than-days")
+ if days is None:
+ return None
+ return int(days)
+
@property
def attributes(self) -> Optional[List[str]]:
"""Returns the attributes."""
@@ -613,7 +636,7 @@ class S3CredentialRequiresEvents(ObjectEvents):
class S3Requirer(Object):
"""Requires-side of the s3 relation."""
- on = S3CredentialRequiresEvents() # pyright: ignore[reportGeneralTypeIssues]
+ on = S3CredentialRequiresEvents() # pyright: ignore[reportAssignmentType]
def __init__(
self, charm: ops.charm.CharmBase, relation_name: str, bucket_name: Optional[str] = None
diff --git a/src/charm.py b/src/charm.py
index eba0930..2977858 100755
--- a/src/charm.py
+++ b/src/charm.py
@@ -19,7 +19,14 @@
from ops.charm import ActionEvent, ConfigChangedEvent, RelationChangedEvent, StartEvent
from ops.model import ActiveStatus, BlockedStatus
-from constants import KEYS_LIST, PEER, S3_LIST_OPTIONS, S3_MANDATORY_OPTIONS, S3_OPTIONS
+from constants import (
+ KEYS_LIST,
+ MAX_RETENTION_DAYS,
+ PEER,
+ S3_LIST_OPTIONS,
+ S3_MANDATORY_OPTIONS,
+ S3_OPTIONS,
+)
logger = logging.getLogger(__name__)
@@ -68,7 +75,7 @@ def _on_start(self, _: StartEvent) -> None:
if missing_options:
self.unit.status = ops.model.BlockedStatus(f"Missing parameters: {missing_options}")
- def _on_config_changed(self, _: ConfigChangedEvent) -> None:
+ def _on_config_changed(self, _: ConfigChangedEvent) -> None: # noqa: C901
"""Event handler for configuration changed events."""
# Only execute in the unit leader
if not self.unit.is_leader():
@@ -79,12 +86,32 @@ def _on_config_changed(self, _: ConfigChangedEvent) -> None:
# iterate over the option and check for updates
for option in S3_OPTIONS:
- if option not in self.config:
- logger.warning(f"Option {option} is not valid option!")
+ # experimental config options should be handled with the "experimental-" prefix
+ if option == "delete-older-than-days" and f"experimental-{option}" in self.config:
+ config_value = self.config[f"experimental-{option}"]
+ # check if new config value is inside allowed range
+ if config_value > 0 and config_value <= MAX_RETENTION_DAYS:
+ update_config.update({option: str(config_value)})
+ self.set_secret("app", option, str(config_value))
+ self.unit.status = ActiveStatus()
+ else:
+ logger.warning(
+ "Invalid value %s for config '%s'",
+ config_value,
+ option,
+ )
+ self.unit.status = BlockedStatus(
+ f"Option {option} value {config_value} outside allowed range [1, {MAX_RETENTION_DAYS}]."
+ )
continue
- # skip in case of empty config
- if self.config[option] == "":
- # reset previous value if present (e.g., juju model-config --reset PARAMETER)
+
+ # option possibly removed from the config
+ # (e.g. 'juju config --reset