Skip to content

feat(provisioning): declare remote tiers in the Tenant spec - #282

Open
shurkus wants to merge 1 commit into
rustfs:mainfrom
shurkus:feat/issue-280-tenant-tiers
Open

shurkus wants to merge 1 commit into
rustfs:mainfrom
shurkus:feat/issue-280-tenant-tiers

Conversation

@shurkus

@shurkus shurkus commented Oct 18, 2026

Copy link
Copy Markdown

Type of Change

  • New Feature
  • Bug Fix
  • Documentation
  • Performance Improvement
  • Test/CI
  • Refactor
  • Other:

Related Issues

Closes #280

Summary of Changes

A Tenant can now declare remote storage tiers in spec.tiers. Bucket lifecycle transitions (#279) can then reference a tier that the same Tenant manages, so a Tenant, its tiers and its ILM rules are all managed through GitOps.

spec:
  tiers:
    - name: COLD
      type: S3            # S3 | RustFS | MinIO
      endpoint: https://s3.example.com
      bucket: rustfs-cold
      prefix: rustfs-a/
      region: us-east-1   # optional
      credsSecret:
        name: cold-tier   # accesskey / secretkey
  buckets:
    - name: app-data
      lifecycle:
        state: Present
        rules:
          - id: to-cold
            status: Enabled
            filter:
              prefix: ""
            transition:
              days: 7
              storageClass: COLD
  • Admin client: list_tiers, add_tier (PUT /rustfs/admin/v3/tier?force=) and edit_tier_credentials (POST /rustfs/admin/v3/tier/{name}) are added. The tier list parser keeps type, name, endpoint, bucket, prefix, region and access key, and drops secretKey. list_tier_names from Feature request: Transition rules in spec.buckets[].lifecycle #279 now uses the same parser.
  • Ordering: tiers are reconciled after users and before buckets. A lifecycle rule can reference a tier declared in the same Tenant and is applied in the same reconcile pass.
  • Missing tier: the operator registers it with the credentials from credsSecret.
  • Existing tier with the same settings: the operator adopts it and applies the Secret credentials once. RustFS never returns a tier secret key, so the operator cannot otherwise know whether the live key matches.
  • Existing tier with different settings: RustFS cannot change type, endpoint, bucket, prefix or region on a registered tier; only the credentials can be edited. A live tier with the same name and different settings is reported as TierConflict with the differing field names, and nothing is written. Removing it automatically would be destructive: removal is rejected while objects reference the tier, and the transitioned objects depend on the remote prefix.
  • Credentials: status.provisioning.tiers records the Secret resourceVersion, the Secret name and an access-key hash, as for users. A Secret update, a different credsSecret, or an access key in the RustFS tier list that differs from the Secret updates the tier through the edit API. The tier Secret is added to the Tenant reference index, so a Secret update enqueues the Tenant. If a RustFS version ever lists the access key as empty or REDACTED, only the Secret version is compared, so the operator does not rewrite credentials on every reconcile.
  • Non-empty remote prefix: without force, RustFS rejects a remote prefix that already holds objects with 409 XMinioAdminTierBackendInUse. The operator reports this as TierConflict instead of retrying. force: true sends force=true and is meant for a prefix that belongs to this Tenant, for example after a restore. It is used only when the operator adds the tier.
  • Removal: a tier removed from the spec is Retained in RustFS and status, the same deletionPolicy: Retain as policies, users and buckets.
  • Validation: the CRD rejects lowercase and reserved names (STANDARD, REDUCED_REDUNDANCY) with CEL. Reconcile repeats the RustFS checks exactly (Unicode uppercase, reserved names, valid XML text, since the name is also an S3 storage class). Errors are classified by status as elsewhere: timeouts, 429 and 5xx stay Pending and requeue; other 4xx and Secret errors fail with TierApplyFailed, TierSecretInvalid or TierConflict. The three new reasons are blocking and have next actions.
  • Console create/update requests accept tiers, the console shows tier status rows, and OpenAPI schemas, console-web types, the CRD YAML, the chart README, both user guides and CHANGELOG are updated.

Checklist

  • I have read and followed the CONTRIBUTING.md guidelines
  • Passed make pre-commit (fmt-check + clippy + test + console-lint + console-fmt-check)
  • Added/updated necessary tests
  • Documentation updated (if needed)
  • CHANGELOG.md updated under [Unreleased] (if user-visible change)
  • CI/CD passed (if applicable)

Impact

  • Breaking change (CRD/API compatibility)
  • Requires doc/config/deployment update
  • Other impact:

The CRD change is additive: spec.tiers and status.provisioning.tiers are new optional lists. The operator needs no new RBAC, because it already reads Secrets in the Tenant namespace for users.

Verification

make pre-commit
cargo nextest run --all
cargo test --doc --all
cargo test -p rustfs-admin tier
cargo test --lib tier
helm lint deploy/rustfs-operator && helm lint deploy/rustfs-tenant
helm template t deploy/rustfs-operator

New tests:

  • rustfs-admin: tier_settings_are_read_without_the_secret_key. The Feature request: Transition rules in spec.buckets[].lifecycle #279 parser tests now run against the shared parser.
  • Reconcile, against a mock admin API:
    • missing_tier_is_registered_with_secret_credentials checks the exact add payload and force=false;
    • owned_tier_with_unchanged_secret_is_not_rewritten;
    • rotated_tier_secret_only_updates_credentials;
    • existing_tier_is_adopted_once_and_out_of_band_access_key_is_corrected;
    • live_tier_with_different_settings_fails_without_writes;
    • non_empty_remote_prefix_is_a_conflict_unless_forced;
    • tier_name_and_secret_errors_fail_before_rustfs_writes;
    • transient_tier_list_failure_keeps_ownership_and_retries;
    • spec_tier_is_registered_before_a_bucket_lifecycle_references_it, an end-to-end run with a mocked Secret;
    • removed_tier_is_retained_with_its_ownership;
    • redacted_live_access_key_falls_back_to_the_secret_version;
    • tier_names_follow_rustfs_rules;
    • tier_secret_values_are_trimmed_and_required.
  • Types and schema: tier_deserializes_issue_yaml_shape_and_omits_defaults, tenant_crd_declares_tiers_with_rustfs_name_rules. The reference-index, referenced-secret, reason and OpenAPI tests also cover tiers.

The payload shapes, status codes and the "only credentials are editable" rule come from the RustFS 1.0.2-preview.2 tier handlers (rustfs/src/admin/handlers/tier.rs, crates/ecstore/src/services/tier/).

Checks were run in containers matching CI: Rust stable in rust:1-bookworm with Helm 3.21.3 and cargo-nextest, and console-web in node:24 with pnpm 10.28.1. All make pre-commit targets passed. cargo nextest run --all ran 784 tests with 784 passing, helm lint passed for both charts, and helm template renders. A server-side dry run of the generated CRD on Kubernetes v1.36.5 accepted the CEL rules.

Live check on a staging cluster. The cluster runs an operator image built from this branch (the operator binary on top of rustfs/operator:0.0.8) with the CRD from this branch. It manages a RustFS 1.0.2-preview.2 Tenant whose tiers point at Storj; STORJ and LATE had been registered earlier with rc ilm tier add.

Scenario Result
STORJ declared with the live settings Ready, adopted with one credential edit, then RustFS tier matches spec on later reconciles without writes
LATE declared with a different prefix Failed / TierConflict: live RustFS tier differs from spec in prefix, no write
LATE prefix corrected in the spec Adopted on the next reconcile and the Tenant returned to Ready
TIERS-TEST on an empty prefix Registered (RustFS tier was registered) and listed by rc ilm tier list with the declared settings
credsSecret updated (new resourceVersion) Both tiers that use the Secret got a credential edit within seconds; observedSecretResourceVersion followed the Secret
New object in a bucket whose rule targets STORJ, after the credential edit Transitioned within one 30 s poll (x-amz-storage-class: STORJ); GET returned the original bytes (same SHA-256)
Existing buckets with #279 transition rules Stayed Ready, lifecycle adopted without a PUT

Additional Notes

  • Only the S3-style backends (s3, rustfs, minio) are modeled. Azure, GCS and the other RustFS backends use different credential shapes and can follow if there is interest. The remote storageClass of an S3 tier is not modeled either.
  • deletionPolicy is Retain only, like the other provisioned items. A Delete policy would need the remove API and its in-use checks, and can be added separately.

Thank you for your contribution! Please ensure your PR follows the community standards (CODE_OF_CONDUCT.md) and sign the CLA if this is your first contribution.

`spec.tiers` registers S3, RustFS and MinIO remote tiers through the
RustFS tier admin API, so bucket lifecycle transitions can reference a
tier managed by the same Tenant. Tiers are reconciled before buckets.

Credentials come from `tiers[].credsSecret` (`accesskey`/`secretkey`).
RustFS never returns a tier secret key, so the applied Secret
resourceVersion is recorded in `status.provisioning.tiers`; a Secret
change, a different Secret, or an access key listed by RustFS that
differs from the Secret updates the credentials with the edit API.

Type, endpoint, bucket, prefix and region cannot change on a registered
tier: a live tier with the same name and other settings is reported as
`TierConflict` without writes, and a matching one is adopted. RustFS
rejects a remote prefix that already holds objects with 409; that is
reported as `TierConflict` too, and `force: true` registers the tier
anyway. Removed tiers are retained, like other provisioned items.

Closes rustfs#280
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: remote storage tiers in the Tenant CRD

1 participant