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