Conversation
Mehdi-Bendriss
left a comment
There was a problem hiding this comment.
Thank you Mykola! Good work! I left some comments
- _blocking_reason: move below the handlers, add a check_running_operations
flag (create-backup rejects a backup already running; list-backups, being
read-only, passes False). Flag is ready for a future restore action.
- create-backup: surface BACKUP_IN_PROGRESS as a running status during the
blocking action; drop the redundant delete on upload ClientError (boto3's
managed upload aborts the multipart upload itself) — explicit cleanup now
only on rc!=0 / bad-RDB-magic, where the object did fully upload.
- list-backups: add an `output` (table/json) action parameter.
- storage-detaching: raise ValkeyBackupInProgressError instead of returning,
so the hook errors and Juju retries teardown until the backup finishes.
- move the S3 endpoint CA path onto WorkloadBase.backup_ca_path, kept
charm-process-local via JUJU_CHARM_DIR (boto3 runs charm-side, not in the
workload container, so it must not be a tls_paths/container path).
- s3_credentials property: return {} instead of None and type it
dict[str, Any] (tls-ca-chain is a list); drop the now-dead None guards in
list_backups/create_backup.
- create_backup: flatten the nested try/except, derive backup_id from a
single timestamp, tidy comments.
- nits: walrus operators, clearer variable names, docstrings, a log line.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Edu6gfuBJviyqpaYV3Pqde
Mehdi-Bendriss
left a comment
There was a problem hiding this comment.
Thank you Mykola!
Give all backup-related exceptions a common base so callers can catch the whole family with `except ValkeyBackupError`. ValkeyBackupInProgressError is only raised from storage-detaching (to retry the hook), which is not on any `except ValkeyBackupError` path, so this cannot be swallowed by the backup action/upload handlers. Addresses PR #59 review feedback. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014VSu7jtf5pbJY7GySibCWM
The S3 endpoint CA bundle is a charm-process-local file (boto3 runs in the charm process, not the workload container), so it does not belong on the WorkloadBase. Move it to BackupManager._backup_ca_path, derived from the ops charm_dir (self.state.charm.charm_dir) instead of reading os.environ ["JUJU_CHARM_DIR"] directly, matching how managers/topology.py already obtains the charm dir. Drop the now-unused os/pathlib/BACKUP_CA_FILENAME imports from base_workload. Addresses PR #59 review feedback. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014VSu7jtf5pbJY7GySibCWM
A misconfigured S3 integrator can send a tls-ca-chain whose items are not PEM certificates (base64 without an armour header, or stray strings); such a bundle is written to disk but boto3 cannot load it. Reject the chain unless every item carries a PEM "-----BEGIN ...-----" header, mirroring the key check in managers/tls.py. Addresses PR #59 review feedback. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014VSu7jtf5pbJY7GySibCWM
Answer two review questions inline: why _on_s3_credentials_changed trims whitespace and strips slashes from the integrator envelope (s3-integrator passes values through verbatim, and empty/"/" path or bucket would corrupt S3 keys or enumerate the whole bucket), and why us-east-1 is special when creating the bucket (AWS rejects LocationConstraint=us-east-1). Addresses PR #59 review feedback. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014VSu7jtf5pbJY7GySibCWM
skourta
left a comment
There was a problem hiding this comment.
LGTM. The only remaining concern is if the backup procedure (streaming) takes a long time the create backup action will time out.
reneradoi
left a comment
There was a problem hiding this comment.
Thank you Mykola, and sorry for the late review. I agree with the overall concept and have only some minor comments and questions.
|
Hint for the failing integration tests: #76 |
Address PR #59 review nits from @reneradoi: - add the `-> Bucket` return annotation on `_get_bucket_resource` - use `Path.as_posix()` instead of `str()` for the CA bundle path, matching the idiom used across the rest of the charm Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QnE2zKaNA8hVPq1ciWyX5w
Replace the vendored data_platform_libs/v0/s3 charm lib with the maintained object-storage-charmlib PyPI package (schema v1: shares credentials via Juju secrets, common s3/azure/gcs contract), per @reneradoi's review on PR #59. - add object-storage-charmlib ^1.0.0 to dependencies - import S3Requirer + StorageConnectionInfo{Changed,Gone}Event from object_storage and observe storage_connection_info_{changed,gone} - read credentials via get_storage_connection_info(); copy the returned S3Info TypedDict into a plain dict before envelope normalisation - delete lib/charms/data_platform_libs/v0/s3.py - update unit-test mocks to the new getter The s3 relation interface and endpoint name are unchanged; the new lib stays wire-compatible with both v0 and v1 s3-integrator providers. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QnE2zKaNA8hVPq1ciWyX5w
Address @skourta's review nit on PR #59: replace the dict-of-str S3 envelope (and the cast on s3_parameters['bucket']) with a typed pydantic S3Parameters model. - add S3Parameters in core/models.py: typed fields with hyphenated aliases plus field validators that fold in the whitespace/separator normalisation and required-field validation previously inlined in _on_s3_credentials_changed - ValkeyCluster.s3_credentials now returns S3Parameters | None - _on_s3_credentials_changed validates via model_validate (ValidationError -> skip), stores model_dump_json(by_alias=True), and dedups by value - BackupManager methods take S3Parameters and use typed attributes - remaining casts (S3ServiceResource, IO[bytes], region Literal) are boto3-stub artifacts, not envelope casts Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QnE2zKaNA8hVPq1ciWyX5w
There was a problem hiding this comment.
Hey Mykola! I've tested the PR, it looks really good UX wise and the implementation is also robust against failure scenarios. I am happy to approve, there is only one issue that we need to either solve in this PR or in the next one, and it is about safeguarding the backup creation on units in parallel: I was able to produce a situation where two units create a backup at the same time, which results in the exact same name for the backup file to be uploaded to s3 storage. Both units stream their backup (according to the logs), and there is no error. But we end up with only one backup in the s3 storage (see screenshot below, sorry it is a bit chaotic 😓 ). It is not clear which unit won the race, whether the file is corrupted or not, and there is no error from the s3 client either. This might be an edge case, but it could happen for example with scheduled backups. I think we need to safeguard it. The most simple solution would be to add the unit name to the backup file, but that's just an idea.
The S3 backup feature uploads RDB snapshots through boto3 and consumes the s3 relation via the data_platform_libs S3Requirer. Add the boto3 and mypy-boto3-s3 dependencies and vendor charms/data_platform_libs/v0/s3.py. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stream the on-disk RDB out of the workload with `valkey-cli --rdb -` over the exec_stream primitive (admin password passed via VALKEYCLI_AUTH, never on argv) and upload it to the configured S3 bucket; list-backups renders the stored snapshots. Adds the BackupStatuses, the s3_credentials/backup_id peer state and the s3 relation, the BackupManager (bucket construction, CA handling, upload via a counting reader), the backup event handlers and create-backup/ list-backups actions, and guards that refuse scale-down and storage detach while a backup is in progress. Type-clean under the whole-repo pyright gate and covered by tests/unit/test_backup.py. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Expose a MicroCeph S3 endpoint in the concierge VM and K8s configs so the backup integration tests have an S3 target to run against. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- _blocking_reason: move below the handlers, add a check_running_operations
flag (create-backup rejects a backup already running; list-backups, being
read-only, passes False). Flag is ready for a future restore action.
- create-backup: surface BACKUP_IN_PROGRESS as a running status during the
blocking action; drop the redundant delete on upload ClientError (boto3's
managed upload aborts the multipart upload itself) — explicit cleanup now
only on rc!=0 / bad-RDB-magic, where the object did fully upload.
- list-backups: add an `output` (table/json) action parameter.
- storage-detaching: raise ValkeyBackupInProgressError instead of returning,
so the hook errors and Juju retries teardown until the backup finishes.
- move the S3 endpoint CA path onto WorkloadBase.backup_ca_path, kept
charm-process-local via JUJU_CHARM_DIR (boto3 runs charm-side, not in the
workload container, so it must not be a tls_paths/container path).
- s3_credentials property: return {} instead of None and type it
dict[str, Any] (tls-ca-chain is a list); drop the now-dead None guards in
list_backups/create_backup.
- create_backup: flatten the nested try/except, derive backup_id from a
single timestamp, tidy comments.
- nits: walrus operators, clearer variable names, docstrings, a log line.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Edu6gfuBJviyqpaYV3Pqde
Give all backup-related exceptions a common base so callers can catch the whole family with `except ValkeyBackupError`. ValkeyBackupInProgressError is only raised from storage-detaching (to retry the hook), which is not on any `except ValkeyBackupError` path, so this cannot be swallowed by the backup action/upload handlers. Addresses PR #59 review feedback. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014VSu7jtf5pbJY7GySibCWM
The S3 endpoint CA bundle is a charm-process-local file (boto3 runs in the charm process, not the workload container), so it does not belong on the WorkloadBase. Move it to BackupManager._backup_ca_path, derived from the ops charm_dir (self.state.charm.charm_dir) instead of reading os.environ ["JUJU_CHARM_DIR"] directly, matching how managers/topology.py already obtains the charm dir. Drop the now-unused os/pathlib/BACKUP_CA_FILENAME imports from base_workload. Addresses PR #59 review feedback. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014VSu7jtf5pbJY7GySibCWM
A misconfigured S3 integrator can send a tls-ca-chain whose items are not PEM certificates (base64 without an armour header, or stray strings); such a bundle is written to disk but boto3 cannot load it. Reject the chain unless every item carries a PEM "-----BEGIN ...-----" header, mirroring the key check in managers/tls.py. Addresses PR #59 review feedback. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014VSu7jtf5pbJY7GySibCWM
Answer two review questions inline: why _on_s3_credentials_changed trims whitespace and strips slashes from the integrator envelope (s3-integrator passes values through verbatim, and empty/"/" path or bucket would corrupt S3 keys or enumerate the whole bucket), and why us-east-1 is special when creating the bucket (AWS rejects LocationConstraint=us-east-1). Addresses PR #59 review feedback. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014VSu7jtf5pbJY7GySibCWM
Address PR #59 review nits from @reneradoi: - add the `-> Bucket` return annotation on `_get_bucket_resource` - use `Path.as_posix()` instead of `str()` for the CA bundle path, matching the idiom used across the rest of the charm Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QnE2zKaNA8hVPq1ciWyX5w
Replace the vendored data_platform_libs/v0/s3 charm lib with the maintained object-storage-charmlib PyPI package (schema v1: shares credentials via Juju secrets, common s3/azure/gcs contract), per @reneradoi's review on PR #59. - add object-storage-charmlib ^1.0.0 to dependencies - import S3Requirer + StorageConnectionInfo{Changed,Gone}Event from object_storage and observe storage_connection_info_{changed,gone} - read credentials via get_storage_connection_info(); copy the returned S3Info TypedDict into a plain dict before envelope normalisation - delete lib/charms/data_platform_libs/v0/s3.py - update unit-test mocks to the new getter The s3 relation interface and endpoint name are unchanged; the new lib stays wire-compatible with both v0 and v1 s3-integrator providers. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QnE2zKaNA8hVPq1ciWyX5w
Address @skourta's review nit on PR #59: replace the dict-of-str S3 envelope (and the cast on s3_parameters['bucket']) with a typed pydantic S3Parameters model. - add S3Parameters in core/models.py: typed fields with hyphenated aliases plus field validators that fold in the whitespace/separator normalisation and required-field validation previously inlined in _on_s3_credentials_changed - ValkeyCluster.s3_credentials now returns S3Parameters | None - _on_s3_credentials_changed validates via model_validate (ValidationError -> skip), stores model_dump_json(by_alias=True), and dedups by value - BackupManager methods take S3Parameters and use typed attributes - remaining casts (S3ServiceResource, IO[bytes], region Literal) are boto3-stub artifacts, not envelope casts Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QnE2zKaNA8hVPq1ciWyX5w
Summary
Adds two Juju actions —
create-backupandlist-backups— that stream a fresh Valkey RDB snapshot from the targeted unit'svalkey-cli --rdb -stdout directly to S3 via boto3 multipart upload. Any unit (leader or follower) can run the actions; locking is per-unit.Credentials are supplied by relating the charm to the upstream
s3-integratorvia a news3-credentialsrelation (interfaces3, limit 1, optional).Depends on
#58 (
workload-exec-stream). Must be merged first — this PR usesworkload.exec_streamandCliClient.build_command_prefix.What's in the PR
Design highlights
bucket.upload_fileobj(proc.stdout, key, Config=TransferConfig(multipart_chunksize=8 MiB))reads chunks from the pipe; no on-disk staging.backup_idfield in the running unit's peer-unit databag is both the lock value and the operation identifier. Two backups can run concurrently on different units (each gets a distinct S3 key); two on the same unit are rejected.bucket.Object(key).delete()) whenvalkey-cliexits non-zero orupload_fileobjraises.Config(request_checksum_calculation="when_required", response_checksum_validation="when_required")(boto3#4400);CreateBucketConfigurationomitted forus-east-1(aws-sdk-js#3647);idempotent error tokens (
BucketAlreadyOwnedByYou,BucketAlreadyExists,BucketNameUnavailable).leader_electedto re-trigger_on_s3_credentials_changed— covers the case wherecredentials_gonefired only on the old leader.Action UX
Status surface:
BACKUP_IN_PROGRESS(maintenance, unit scope) while streamingBACKUP_S3_PARAMETERS_MISSING(blocked, app scope) when the relation is present but the integrator hasn't supplied bucket/credentials yetBACKUP_FAILED(blocked, unit scope) after a failure