{"id":41107366,"url":"https://github.com/bedag/storagegrid-operator","last_synced_at":"2026-01-22T15:25:21.929Z","repository":{"id":324942222,"uuid":"1058688903","full_name":"bedag/storagegrid-operator","owner":"bedag","description":"Operator to manage NetApp StorageGrid","archived":false,"fork":false,"pushed_at":"2025-12-30T12:35:51.000Z","size":260,"stargazers_count":1,"open_issues_count":12,"forks_count":0,"subscribers_count":0,"default_branch":"feat/initial-dev","last_synced_at":"2026-01-02T22:33:16.082Z","etag":null,"topics":["kubernetes-operator","netapp","storagegrid"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bedag.png","metadata":{"files":{"readme":".github/README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":"CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":"DCO","cla":null}},"created_at":"2025-09-17T12:22:27.000Z","updated_at":"2025-12-30T12:35:55.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/bedag/storagegrid-operator","commit_stats":null,"previous_names":["bedag/storagegrid-operator"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/bedag/storagegrid-operator","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bedag%2Fstoragegrid-operator","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bedag%2Fstoragegrid-operator/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bedag%2Fstoragegrid-operator/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bedag%2Fstoragegrid-operator/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bedag","download_url":"https://codeload.github.com/bedag/storagegrid-operator/tar.gz/refs/heads/feat/initial-dev","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bedag%2Fstoragegrid-operator/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28665210,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-22T14:01:31.714Z","status":"ssl_error","status_checked_at":"2026-01-22T13:59:23.143Z","response_time":144,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["kubernetes-operator","netapp","storagegrid"],"created_at":"2026-01-22T15:25:21.282Z","updated_at":"2026-01-22T15:25:21.916Z","avatar_url":"https://github.com/bedag.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# StorageGrid Operator\n\nA Kubernetes operator for managing NetApp StorageGrid S3 tenants and buckets as a native Kubernetes resource.\n\nThis operator is not created to manage your entire StorageGrid installation, but rather to provide a Kubernetes-native way to manage S3 resources on an existing StorageGrid backend.\n\n## Overview\n\nThe StorageGrid Operator provides a Kubernetes-native way to manage S3 resources on NetApp StorageGrid. It allows you to define tenants, buckets, and configurations as Kubernetes custom resources, with the operator handling the lifecycle management and synchronization with the StorageGrid backend.\n\n## Architecture\n\n```mermaid\ngraph TB\n    SG[StorageGrid]\n    STC[S3TenantClass]\n    STA[S3TenantAccount]\n    ST[S3Tenant]\n    SB[S3Bucket]\n    \n    SG --\u003e|owns| STC\n    SG --\u003e|owns| STA\n    STA --\u003e|uses| STC\n    STA --\u003e|owns| ST\n    ST --\u003e|has| SB\n    \n    subgraph \"Cluster Scoped\"\n        SG\n        STC\n        STA\n    end\n    \n    subgraph \"Namespace Scoped\"\n        ST\n        SB\n    end\n    \n    subgraph \"Relationships\"\n        SG -.-\u003e|refers to| Backend[StorageGrid Backend]\n        STC -.-\u003e|refers to loadbalancer endpoint| Backend\n        STA -.-\u003e|manages tenant account| Backend\n        SB -.-\u003e|manages bucket| Backend\n    end\n```\n\nTo better understand the architecture, please refer to the [Architecture Documentation](./docs/architecture/README.md).\n\n## Custom Resources\n\nThis operator revolves around the following Custom Resource Definitions (CRDs):\n* `StorageGrid`\n* `S3TenantClass`\n* `S3TenantAccount`\n* `S3Tenant`\n* `S3Bucket`\n\n### StorageGrid\nCluster-scoped resource representing a StorageGrid installation. Manages connection credentials and global configuration.\n\nThrough this you can specify the endpoint as well as defaults for tenants referring to this StorageGrid. \n\n### S3TenantClass\nCluster-scoped resource defining S3 loadbalancer endpoint within your StorageGrid installation. Used by S3TenantAccounts to determine which endpoint to use.\n\nThis is similiar to an IngressClass in Kubernetes and always points to an existing loadbalancer endpoint in StorageGrid. Through the `spec.enforce` field you can enforce that tenants using this class will only be able to access the grid through this loadbalancer endpoint.\n\nThe operator automatically discovers all endpoints from the gateway's certificate SANs. You can control which endpoints are exposed to tenants using the `spec.preferredEndpoints` field:\n- **Not set**: All discovered endpoints are exposed (default behavior)\n- **Set with default + nil additionalEndpoints**: Default endpoint plus all discovered endpoints\n- **Set with default + empty list `[]`**: Only the default endpoint is exposed\n- **Set with default + explicit list**: Default endpoint plus only the specified endpoints\n\nSee more details on the official docs: https://docs.netapp.com/us-en/storagegrid-116/admin/configuring-load-balancer-endpoints.html \n\n### S3TenantAccount\nCluster-scoped resource representing the actual tenant account in StorageGrid backend. Manages the tenant lifecycle, credentials, and quotas.\n\nYou can imagine the `S3TenantAccount` somewhat similiar to a `PersistentVolume` in Kubernetes. It is a cluster-wide resource that provides the actual backend tenant account in StorageGrid. \n\nThis resource in itself is not meant to be created directly, but rather through the `S3Tenant` resource. \n\nAll interaction with the actual StorageGrid backend happens through this resource.\n\nFor more details on how the `S3TenantAccount` works, see the [tenant relationship](./docs/architecture/tenant-relationship.md).\n\n### S3Tenant\nNamespace-scoped resource providing a namespace-local view of a tenant. Creates and manages the underlying S3TenantAccount.\n\nThis then is the `PersistentVolumeClaim` equivalent in our analogy. It is a namespace-scoped resource that application teams can create to request a tenant account in StorageGrid. The operator will then create the corresponding `S3TenantAccount` in the cluster scope. \n\n### S3Bucket\nNamespace-scoped resource for managing S3 buckets within a tenant. This is a basic interface to create and manage S3 buckets for your tenants and might be deprecated in the future in favor of more generic S3 operators. \n\nCurrently it supports basic bucket CRUD operations as well as defining a policy that gets applied to the bucket.\n\n## Features\n\n- **Declarative Management**: Define S3 resources using Kubernetes manifests\n- **Multi-Tenancy**: Support for multiple tenants with proper isolation\n- **Quota Management**: Configure storage quotas per tenant\n- **Credential Management**: Automatic generation and rotation of administrative and S3 credentials\n- **Webhook Validation**: Built-in validation for resource configurations\n- **Garbage Collection**: Proper cleanup cascade when resources are deleted\n- **Metadata Enrichment**: As NetApp doesn't support tags on tenants, we enrich the tenant description with useful metadata such as the namespace, owner, and custom fields.\n- **Event Observability**: Kubernetes events for state transitions, errors, and significant operations across all controllers\n\n## Event Observability\n\nThe operator emits Kubernetes events for state transitions, errors, and significant operations across all controllers. Events provide a user-visible timeline of operations without requiring log access.\n\n**Key characteristics:**\n- 64 unique event types across 5 controllers\n- Immediate emission for real-time visibility\n- State-change emission to prevent spam\n- Separate event streams per resource (no cross-resource propagation)\n\nFor detailed information on event architecture and implementation, see [Event Architecture](../docs/architecture/events.md).\n\n### Critical Events\n\n**S3 Endpoint Connectivity** - If bucket policy operations fail, check for:\n- `S3EndpointConnectionFailed`: Cannot reach S3 loadbalancer endpoint\n- `S3EndpointConnectionEstablished`: Connection successful\n\n**Backend Connection** - For tenant operations:\n- `BackendConnectionFailed`: Cannot reach StorageGrid management API\n- `BackendConnectionRestored`: Management API connection restored\n\n**Grid Health** - For overall grid status:\n- `GridUnhealthy`: Too many unavailable nodes\n- `GridHealthRecovered`: Grid has recovered\n\n## Installation\n\n### Prerequisites\n\n- Kubernetes cluster (v1.20+)\n- NetApp StorageGrid installation\n- `kubectl` configured to access your cluster\n\n### Required network access\n\nThe operator needs network access to the StorageGrid management endpoint as well as the S3 loadbalancer endpoints. Make sure that the cluster where the operator is running has access to these endpoints.\n\nYou can skip out on the S3 loadbalancer endpoints if you don't plan on using `spec.bucketPolicyJson`on your `S3Bucket` resource, but the management endpoint is required for all operations.\n\n### Deploy the Operator\n\nThis operator is currently only provided as source. You can deploy it by cloning the repository and applying the manifests:\n\n```bash\n# Clone the repository\ngit clone https://git.mgmtbi.ch/cloud/storagegrid-operator.git\ncd storagegrid-operator\n\n# Deploy the operator\nkubectl apply -f config/crd/bases/\nkubectl apply -f config/rbac/\nkubectl apply -f config/manager/\n```\n\n### Environment Variables\n\nIf you're not deploying the operator using the provided Kustomization, you can configure the operator using the following environment variables:\n\n- `OPERATOR_NAMESPACE`: The namespace where the operator is running (defaults to `storagegrid-operator-system`)\n\n## Usage\n\n### 1. Create a StorageGrid Resource\nMake sure to create your `Secret` containing the admin credentials for StorageGrid first.\n\n```yaml\napiVersion: v1\nkind: Secret\nmetadata:\n  name: storagegrid-credentials\n  namespace: storagegrid-operator-system\ntype: Opaque\ndata:\n  username: \u003cbase64-encoded-username\u003e\n  password: \u003cbase64-encoded-password\u003e\n```\n\nThen create the `StorageGrid` resource:\n\n```yaml\napiVersion: s3.bedag.ch/v1alpha1\nkind: StorageGrid\nmetadata:\n  name: my-storagegrid\nspec:\n  endpoint: https://storagegrid.example.com\n  credentialsSecret:\n    name: storagegrid-credentials\n    namespace: storagegrid-operator-system\n```\n\n### 2. Define an S3TenantClass\n\n```yaml\napiVersion: s3.bedag.ch/v1alpha1\nkind: S3TenantClass\nmetadata:\n  name: default\nspec:\n  storageGridRef:\n    name: my-storagegrid\n  backingID: \"gateway-endpoint-id\" # check your storagegrid for the correct ID\n  enforce: true\n  \n  # Optional: Control which endpoints are exposed to tenants\n  # If not set, all discovered endpoints from the gateway certificate are exposed\n  preferredEndpoints:\n    defaultEndpoint: \"s3.example.com\"  # Primary endpoint (always exposed)\n    # additionalEndpoints: []           # Empty list = only default endpoint\n    # additionalEndpoints:              # Omit = all discovered endpoints\n    #   - \"s3-backup.example.com\"       # Explicit list = only these + default\n```\n\n### 3. Create an S3Tenant\n\n```yaml\napiVersion: s3.bedag.ch/v1alpha1\nkind: S3Tenant\nmetadata:\n  name: my-tenant\n  namespace: default\nspec:\n  storageGridRef:\n    name: my-storagegrid\n  s3TenantClassName: default # or omit as it defaults to \"default\"\n  description: \"My application tenant\"\n  quota:\n    limit: \"100Gi\"\n  additionalTenantMetadata:\n    project: \"my-project\"\n    environment: \"production\"\n    owner: \"team-alpha\"\n```\n\n#### Available Annotations\nYou can use the following annotations on the `S3Tenant` resource to modify its behavior:\n```yaml\nmetadata:\n  annotations:\n    # Force recreation of S3 access keys on next reconciliation\n    tenant.s3.bedag.ch/recreate-s3-access-keys: \"true\" \n\n    # Force deletion and recreation of the tenant on next reconciliation\n    tenant.s3.bedag.ch/recreate-tenant: \"true\" \n\n    # As the change of the tenant class can lead to unexpected lose of access, this annotation must be set to allow the change of the tenant class.\n    tenant.s3.bedag.ch/allow-tenant-class-name-change: \"true\" \n\n    # The tenant is protected from accidental deletion, setting this annotation to \"true\" will allow deletion of the tenant.\n    tenant.s3.bedag.ch/allow-tenant-deletion: \"true\"\n```\n\n#### Secrets Created\n\nWhen the `S3Tenant` is created, the operator will create multiple `Secrets` in the same namespace containing the S3 access credentials as well as the admin for the grid URL of the tenant. The secrets will be named `s3-tenant-\u003ctenant-name\u003e-s3-admin-keypair` and `s3-tenant-\u003ctenant-name\u003e-admin-credentials`.\n\nThese secrets can be used by your applications for administrative access to the tenant or for S3 access.\n\n\u003e [!NOTE]  \n\u003e Through setting the `spec.adminSecretRef` or `spec.s3AdminKeysSecretRef` fields on the `S3Tenant`, you can customize the names of these secrets. On existing secrets, the operator will remove the old ones and create the new ones.\n\n#### How does the operator manage tenants?\n\nWhen you create an `S3Tenant`, the operator can either:\n1. **Create a new S3TenantAccount** automatically (default behavior)\n2. **Claim an existing S3TenantAccount** using `spec.s3TenantAccountRef` (see Claiming section below)\n\nThe `S3TenantAccount` manages the actual tenant account in StorageGrid and handles all interactions with the backend. It stores the root credentials in a `Secret` in the `storagegrid-operator-system` namespace, named `s3-tenant-\u003ctenant-name\u003e-root-credentials`.\n\nOn the `S3TenantAccount` you can additionally set the `admin.s3.bedag.ch/reset-admin-password` annotation to force a reset of the admin password on the next reconciliation.\n\n#### Tenant Deletion Policies\n\nThe `S3TenantAccount` supports deletion policies (configured via `status.tenantDeletionPolicy`) that control what happens to the StorageGrid tenant when the S3Tenant is deleted:\n\n- **`Delete`**: Completely removes the tenant from StorageGrid backend (default for new accounts)\n- **`Retain`**: Unbinds the S3Tenant but keeps the S3TenantAccount available for re-claiming. The account emits events and conditions to indicate it was retained. Transitions back to `PhaseReady`.\n- **`RetainThenDelete`**: Retains for a configured duration, then deletes\n\n**When using `Retain` policy for claimed accounts:**\n1. The S3Tenant is deleted\n2. The S3TenantAccount's `spec.s3TenantRef` is cleared (unbinding)\n3. The account is reset to PhaseReady\n4. The account becomes available for claiming by a new S3Tenant\n5. Operator-managed secrets remain intact (unlike full tenant deletion)\n6. The tenant in StorageGrid continues operating normally\n\nThis enables workflows like:\n- Safely testing tenant claiming without risk of data loss\n- Moving S3Tenant resources between namespaces while keeping the same backend account\n- Temporarily removing namespace-scoped access while preserving the account\n- Re-binding accounts to different S3Tenant resources\n\n**When using `Retain` policy for imported accounts:**\n1. Removes all operator-managed secrets (root, admin, S3 keys)\n2. Clears ownership metadata (`managed_by`, `cr_uid`, `cr_name`, `kubernetes_namespace`) from the tenant description\n3. Preserves user-provided description and custom metadata fields\n4. Leaves the tenant intact in StorageGrid, making it available for re-import\n\n\u003e [!TIP]\n\u003e Use `Retain` policy when you want to delete the S3Tenant but keep the S3TenantAccount available for re-claiming, or when you want to preserve an imported tenant for re-import.\n\n### 3.1 Importing Existing Tenants\n\nIf you have existing tenants in StorageGrid that were created outside of the operator, you can import them to bring them under Kubernetes management.\n\n**Important:** The import annotation (`admin.s3.bedag.ch/import-tenant-id`) is only supported on **S3TenantAccount** resources (cluster-scoped). Once imported, an S3Tenant (namespace-scoped) can claim the imported account using `spec.s3TenantAccountRef`.\n\n#### Prerequisites for Import\n\n1. **Tenant ID**: Find the existing tenant's ID from StorageGrid Admin UI or API\n2. **Root Credentials**: The root user password must be known and provided in a pre-created Secret (StorageGrid API limitation - cannot be rotated programmatically)\n3. **No existing ownership**: Check the tenant description in StorageGrid to ensure it's not already managed by another operator instance (look for `cr_uid` field)\n\n#### Import Process\n\n**Step 1: Create a Secret with root credentials**\n\nThe secret must be created in the operator's namespace (typically `storagegrid-operator-system`):\n\n```yaml\napiVersion: v1\nkind: Secret\nmetadata:\n  name: existing-tenant-root\n  namespace: storagegrid-operator-system  # Operator namespace, not application namespace\ntype: Opaque\nstringData:\n  username: \"root\"                       # Always \"root\"\n  password: \"existing-root-password\"  # Must be the actual root password from the backing StorageGrid *tenant*\n```\n\n**Step 2: Import the tenant as S3TenantAccount**\n\n```yaml\napiVersion: s3.bedag.ch/v1alpha1\nkind: S3TenantAccount  # Cluster-scoped resource - import happens here\nmetadata:\n  name: imported-tenant-account\n  annotations:\n    admin.s3.bedag.ch/import-tenant-id: \"12345678901234567890\"  # Tenant ID from StorageGrid\nspec:\n  storageGridRef:\n    name: my-storagegrid\n  s3TenantClassName: default\n  storageQuota: 100Gi # this will override any current settings - be aware of that\n  rootSecretRef:\n    name: existing-tenant-root  # REQUIRED for imports - references the pre-created secret\n  description: \"Imported from existing StorageGrid tenant\"\n```\n\n**Step 3: (Optional) Claim the imported account with an S3Tenant**\n\nAfter the S3TenantAccount is successfully imported and becomes available (Phase: Ready), you can create an S3Tenant to claim it for namespace-scoped access:\n\n```yaml\napiVersion: s3.bedag.ch/v1alpha1\nkind: S3Tenant\nmetadata:\n  name: my-imported-tenant\n  namespace: default  # Application namespace\nspec:\n  storageGridRef:\n    name: my-storagegrid\n  s3TenantClassName: default\n  storageQuota: 100Gi  # Must be \u003e= account quota\n  s3TenantAccountRef:\n    name: imported-tenant-account  # References the imported S3TenantAccount\n```\n\n#### Claiming Existing Accounts\n\nThe claiming pattern (similar to PersistentVolume/PersistentVolumeClaim) allows namespace-scoped S3Tenant resources to bind to cluster-scoped S3TenantAccount resources. This works for both imported accounts and pre-created accounts.\n\n**Two Ways to Claim:**\n\n**Option A - S3Tenant Claims Available Account:**\n```yaml\n# 1. Create S3TenantAccount (cluster-scoped, created by platform team)\napiVersion: s3.bedag.ch/v1alpha1\nkind: S3TenantAccount\nmetadata:\n  name: shared-tenant-account\nspec:\n  storageGridRef:\n    name: my-storagegrid\n  s3TenantClassName: premium\n  storageQuota: 500Gi\n  # No s3TenantRef - account is available for claiming\n\n---\n# 2. S3Tenant claims the account (namespace-scoped, created by app team)\napiVersion: s3.bedag.ch/v1alpha1\nkind: S3Tenant\nmetadata:\n  name: my-tenant\n  namespace: app-namespace\nspec:\n  storageGridRef:\n    name: my-storagegrid\n  s3TenantClassName: premium\n  storageQuota: 500Gi\n  s3TenantAccountRef:\n    name: shared-tenant-account  # Claim by name\n```\n\n**Option B - S3TenantAccount Pre-Binds to S3Tenant:**\n```yaml\n# 1. Create S3TenantAccount pre-bound to a specific tenant\napiVersion: s3.bedag.ch/v1alpha1\nkind: S3TenantAccount\nmetadata:\n  name: reserved-account\nspec:\n  storageGridRef:\n    name: my-storagegrid\n  s3TenantClassName: premium\n  storageQuota: 500Gi\n  s3TenantRef:  # Pre-bind to specific tenant\n    name: my-tenant\n    namespace: app-namespace\n\n---\n# 2. S3Tenant can only claim if it matches pre-binding\napiVersion: s3.bedag.ch/v1alpha1\nkind: S3Tenant\nmetadata:\n  name: my-tenant\n  namespace: app-namespace\nspec:\n  storageGridRef:\n    name: my-storagegrid\n  s3TenantClassName: premium\n  storageQuota: 500Gi\n  s3TenantAccountRef:\n    name: reserved-account  # Must match pre-binding\n```\n\n#### Claiming Validation Rules\n\nWhen an S3Tenant attempts to claim an S3TenantAccount, the following validations are enforced:\n\n1. **Not Already Bound**: Account must not be bound to a different tenant\n2. **Pre-Binding Match**: If account has `spec.s3TenantRef` set, the claiming tenant must match (name + namespace)\n3. **Same StorageGrid**: Both must reference the same StorageGrid instance\n4. **Quota Compatibility**: Tenant quota must be \u003e= account quota\n5. **Class Match**: Both must reference the same S3TenantClass\n6. **Immutability**: Once set, `spec.s3TenantAccountRef` on the S3Tenant cannot be changed\n\n#### Account Lifecycle States\n\nS3TenantAccount resources progress through these phases:\n\n- **PhaseReady**: Account exists in StorageGrid but is not bound to any S3Tenant (available for claiming)\n- **PhaseBound**: Account is actively bound to an S3Tenant (spec and status refs are set)\n- **PhaseRetainThenDelete**: S3Tenant was deleted with RetainThenDelete policy, waiting for retention period to expire\n- **PhaseDeleting**: Account is being deleted from StorageGrid\n\n**State Transitions:**\n```\nAvailable (PhaseReady) \n  ↓ (S3Tenant claims via s3TenantAccountRef)\nBound (PhaseBound)\n  ↓ (S3Tenant deleted with Retain policy)\nAvailable (PhaseReady)  [ConditionTypeRetained = True for observability]\n```\n\n**Note:** When an S3Tenant is deleted with Retain policy, the account returns directly to PhaseReady (unbound and available). The `ConditionTypeRetained` condition remains True to indicate the account came from a deleted tenant, providing an audit trail.\n\n**Important Notes on Deletion:**\n\n- **Deleting an S3Tenant** (namespace-scoped):\n  - If using **Retain** policy: Unbinds from S3TenantAccount but leaves the account available for re-claiming\n  - If using **Delete** policy: Also deletes the bound S3TenantAccount and the tenant in StorageGrid\n  - The deletion policy is determined by the S3TenantAccount's configuration\n\n- **Deleting an S3TenantAccount** (cluster-scoped):\n  - If the account is bound (PhaseBound), deletion is blocked until the S3Tenant is deleted first\n  - If the account is available (PhaseReady), it can be deleted directly\n  - Deletes the tenant from StorageGrid according to its deletion policy\n\n\u003e [!WARNING]\n\u003e You cannot delete a bound S3TenantAccount directly. You must first delete the claiming S3Tenant, which will unbind the account (if using Retain policy) or delete both resources (if using Delete policy).\n\n#### Import Behavior\n\n**Single Ownership Model:**\n- The operator takes **full ownership** of imported tenants\n- Only one operator can manage a tenant at a time and will track ownership via description\n- The import annotation is automatically removed after successful import\n\n**Ownership Tracking:**\n- Ownership is tracked via metadata in the tenant's description field in StorageGrid\n- Metadata includes: `managed_by`, `cr_uid`, `cr_name`, `kubernetes_namespace`, `last_reconciled`\n- This metadata is preserved even if the operator is uninstalled\n\n**Import States:**\n- **Unmanaged Tenant**: Import succeeds, operator takes ownership\n- **Already Imported by This CR**: Import is idempotent, succeeds without changes\n- **Managed by Different CR**: Import fails with ownership conflict error\n\n#### Resolving Import Conflicts\n\nIf you attempt to import a tenant that's already managed by another CR, you'll receive an error like:\n\n```\nCannot import tenant 12345678901234567890: already managed by another CR 'other-tenant' (UID: abc-123-def)\n\nConflict Resolution Options:\n1. Delete the other CR 'other-tenant' in namespace 'other-namespace' if it's stale\n2. Delete this CR and use the existing one instead\n3. If the tenant was orphaned, manually edit the tenant description in StorageGrid to remove the 'cr_uid' field\n```\n\n**Resolution Steps:**\n\n**Option 1 - Remove Stale CR:**\n```bash\n# If the other CR is from a deleted cluster or is no longer needed\nkubectl delete s3tenant other-tenant -n other-namespace\n# Wait for cleanup, then retry import\n```\n\n**Option 2 - Use Existing CR:**\n```bash\n# If the tenant is already managed elsewhere, use that CR instead\nkubectl delete s3tenant imported-tenant -n default\n```\n\n**Option 3 - Manual StorageGrid Cleanup:**\n\nIf the tenant was truly orphaned (previous cluster deleted, CR lost, or you used `Retain` deletion policy):\n\n1. Log into StorageGrid Admin UI\n2. Navigate to the tenant details\n3. Edit the tenant description\n4. Remove the ownership metadata lines (or the entire description):\n   ```\n   managed_by:storagegrid-operator\n   cr_uid:\u003csome-uid\u003e\n   cr_name:\u003csome-name\u003e\n   kubernetes_namespace:\u003csome-namespace\u003e\n   ```\n5. Save changes in StorageGrid\n6. Retry the import in Kubernetes\n\n\u003e [!TIP]\n\u003e If you used `Retain` deletion policy on the S3TenantAccount, the operator already removed the ownership metadata for you - the tenant is immediately ready for re-import without manual cleanup!\n\n#### Important Notes\n\n\u003e [!WARNING]  \n\u003e **Root Credentials Required**: Unlike newly created tenants, imports require `spec.rootSecretRef` to be set with the existing root password. This is a StorageGrid API limitation - root passwords cannot be rotated via API.\n\n\u003e [!NOTE]  \n\u003e **Import Annotation is Create-Only**: The `admin.s3.bedag.ch/import-tenant-id` annotation can only be set during resource creation. The webhook will reject attempts to add it during updates to prevent accidental tenant reassignment.\n\n\u003e [!TIP]  \n\u003e **Verify Before Import**: Check the tenant description in StorageGrid before importing to see if it's already managed by another operator instance.\n\n#### Post-Import Operations\n\nAfter successful import:\n- The operator creates admin credentials and S3 access keys (stored in Secrets)\n- The `rootSecretRef` continues to reference your pre-existing secret\n- All normal reconciliation and lifecycle operations work as expected\n- You can create `S3Bucket` resources that reference the imported tenant\n- Quota, description, and other spec fields can be updated normally\n\n### 4. Create S3 Buckets\n\n```yaml\napiVersion: s3.bedag.ch/v1alpha1\nkind: S3Bucket\nmetadata:\n  name: my-bucket\n  namespace: default\nspec:\n  s3TenantRef:\n    name: my-tenant\n  region: \"us-east-1\"\n```\n\n#### Bucket Lifecycle Phases\n\nBuckets have the following lifecycle phases:\n\n- **Pending**: Initial state, waiting for StorageGrid confirmation\n- **Ready**: Normal operation, bucket available for object storage\n- **Draining**: Automatically deleting all objects (see Draining Buckets below)\n- **Failed**: Error condition requiring intervention\n- **Deleting**: Finalizer cleanup, removing from StorageGrid\n\nMonitor bucket phase:\n```bash\nkubectl get s3bucket my-bucket -o jsonpath='{.status.phase}'\n```\n\n#### Draining Buckets\n\nBuckets cannot be deleted while they contain objects. Use the drain annotation to automatically delete all objects before bucket deletion:\n\n**Trigger a drain:**\n```bash\nkubectl annotate s3bucket my-bucket bucket.s3.bedag.ch/force-drain-bucket=true\n```\n\n**Monitor drain progress:**\n```bash\n# Watch phase transition to Draining\nkubectl get s3bucket my-bucket -w\n\n# Check detailed drain status\nkubectl get s3bucket my-bucket -o yaml | yq .status.drainStatus\n\n# View drain events\nkubectl describe s3bucket my-bucket\n```\n\n**Cancel an in-progress drain:**\n```bash\nkubectl annotate s3bucket my-bucket bucket.s3.bedag.ch/force-drain-bucket-\n```\n\n**Configure drain behavior:**\n\nDrain polling intervals and thresholds can be customized at the bucket or grid level:\n\n```yaml\n# Grid-level configuration (applies to all buckets)\n# Likely done by the grid administrator\napiVersion: s3.bedag.ch/v1alpha1\nkind: StorageGrid\nmetadata:\n  name: my-storagegrid\nspec:\n  operations:\n    drain:\n      initialPollInterval: \"3m\"        # Fast polling initially\n      longRunningPollInterval: \"30m\"   # Slower after 1 hour\n      stuckThreshold: \"3h\"             # Warning if no progress\n\n---\n# Bucket-level override (highest priority)\napiVersion: s3.bedag.ch/v1alpha1\nkind: S3Bucket\nmetadata:\n  name: my-large-bucket\nspec:\n  drainPollInterval: \"5m\"       # Custom polling interval\n  drainStuckThreshold: \"2h\"     # Custom stuck detection\n```\n\n**Drain States:**\n- Operator polls StorageGrid for progress every 3-30 minutes\n- Emits events for started, progress, stuck, complete, and canceled states\n- Automatically removes annotation when drain completes\n- Returns bucket to Ready phase after successful drain\n\nFor drain architecture details, see [Drain Operations Architecture](../docs/architecture/drain-operations.md).\n\n#### Deleting Tenants with Buckets\n\nTo delete a tenant that has buckets:\n\n1. **Drain all tenant buckets:**\n   ```bash\n   kubectl annotate s3buckets -l tenant=my-tenant bucket.s3.bedag.ch/force-drain-bucket=true\n   ```\n\n2. **Monitor drain progress:**\n   ```bash\n   kubectl get s3buckets -l tenant=my-tenant -w\n   ```\n\n3. **Delete empty buckets or wait for drain completion:**\n   ```bash\n   # Buckets auto-delete after draining if you delete them\n   kubectl delete s3buckets -l tenant=my-tenant\n   ```\n\n4. **Delete the tenant:**\n   ```bash\n   kubectl delete s3tenant my-tenant\n   ```\n\n#### Secrets Created\n\nWhen the `S3Bucket` is created, the operator will create a corresponding `Secret` in the same namespace containing the S3 access credentials for the bucket. The secret will be named `s3-bucket-\u003cbucket-name\u003e-credentials`.\n\nThis user will have full access to the bucket and may be used instead of the admin credentials of the `S3Tenant` to ensure proper least-privilege access.\n\n\u003e [!NOTE]  \n\u003e Same as with the `S3Tenant`, you can customize the name of this secret through the `spec.s3AdminKeysSecretRef` field on the `S3Bucket`.\n\n## Configuration\n\n### Endpoint Filtering\n\nThe operator discovers all endpoints from the StorageGrid gateway's certificate SANs. By default, all discovered endpoints (DNS names and VIPs) are exposed to tenants. You can control this using `preferredEndpoints` in the S3TenantClass:\n\n**Expose all discovered endpoints (default)**:\n```yaml\nspec:\n  # preferredEndpoints not set - all endpoints exposed, first as default\n```\n\n**Expose specific endpoints only**:\n```yaml\nspec:\n  preferredEndpoints:\n    defaultEndpoint: \"s3.example.com\"\n    additionalEndpoints:\n      - \"s3-backup.example.com\"\n      - \"192.168.1.100\"\n```\n\n**Expose only the default endpoint**:\n```yaml\nspec:\n  preferredEndpoints:\n    defaultEndpoint: \"s3.example.com\"\n    additionalEndpoints: []  # Empty list = default only\n```\n\n**Expose default + all discovered**:\n```yaml\nspec:\n  preferredEndpoints:\n    defaultEndpoint: \"s3.example.com\"\n    # additionalEndpoints omitted = include all discovered\n```\n\n**Behavior notes**:\n- Addresses not found in certificate SANs are kept with a warning event (admin knows best)\n- When `additionalEndpoints` is nil (unset), all discovered addresses are included\n- When `additionalEndpoints` is an empty list `[]`, only the default is exposed\n- The default address is always listed first in status\n- All addresses in the configuration point to the same gateway/loadbalancer\n\n### Tenant Metadata\n\nAs NetApp doesn't support tags on tenants, we enrich the tenant description with useful metadata.  \nThe operator automatically enriches tenant descriptions with metadata:\n\n- `kubernetes_namespace`: The namespace of the S3Tenant\n- `user_description`: Custom description field\n- Custom fields from `additionalTenantMetadata`\n\n### Webhooks\n\nThe operator includes validation webhooks for:\n- S3TenantAccount validation\n- S3Bucket validation\n\nTo disable webhooks, set the environment variable:\n```bash\nexport ENABLE_WEBHOOKS=false\n```\n\n## Development\n\n### Prerequisites\n\n- Go 1.21+\n- Docker\n- Kubebuilder v3.0+\n\n### Building\n\n```bash\n# Build the operator\nmake build\n\n# Build and push Docker image\nmake docker-build docker-push IMG=your-registry/storagegrid-operator:tag\n\n# Deploy to cluster\nmake deploy IMG=your-registry/storagegrid-operator:tag\n```\n\n### Testing\n\n```bash\n# Run unit tests\nmake test\n\n# Run with coverage\nmake test-coverage\n```\n\n### Code Generation\n\n```bash\n# Generate CRDs and code\nmake generate manifests\n```\n\n## Monitoring\n\nThe operator exposes metrics on port 8443 (HTTPS) or 8080 (HTTP). Health checks are available on port 8081.\n\n### Available Endpoints\n\n- `/metrics` - Prometheus metrics\n- `/healthz` - Health check\n- `/readyz` - Readiness check\n\n## Troubleshooting\n\n### Common Issues\n\n1. **StorageGrid Connection Issues**: Verify credentials and network connectivity\n\n### Debug Logging\n\nEnable debug logging by setting the log level:\n```bash\n--zap-log-level=1  # or higher for more verbose logging\n```\n\n## Contributing\n\n1. Fork the repository\n2. Create a feature branch\n3. Make your changes\n4. Add tests for new functionality\n5. Run the test suite\n6. Submit a pull request\n\n### Code Style\n\n- Follow standard Go conventions\n- Use `gofmt` for formatting\n- Add appropriate comments for exported functions\n- Include unit tests for new features\n\n## License\n\nLicensed under the Apache License, Version 2.0. See LICENSE file for details.\n\n## Support\n\nFor issues and questions:\n\n- Create an issue in the repository\n- Check existing documentation\n- Review the troubleshooting section\n\n## Roadmap\n\n- [x] Add Events\n- [x] Implement bucket drain annotation for automatic object deletion\n- [x] Allow the import of existing grid accounts as S3TenantAccount resources\n- [ ] Implement labels for all resources for easier filtering\n- [ ] Integrate proper e2e tests - currently unable to test against a real StorageGrid instance due to lack of grid docker license. \n- [ ] Write proper metrics of CRs created and backend calls\n- [ ] Allow the use of labels for `S3Tenant.spec.AllowedNamespaces` to allow more flexible tenant access control\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbedag%2Fstoragegrid-operator","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbedag%2Fstoragegrid-operator","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbedag%2Fstoragegrid-operator/lists"}