1ea1713d6e
Adds a Kubernetes operator that provisions Ceph RGW (S3) buckets and access keys declaratively through the Ceph manager dashboard REST API. Three CRDs in group ceph.unkin.net/v1alpha1: - ObjectStoreUser: creates an RGW user, delivers its key pair to a Secret - Bucket: creates an S3 bucket owned by an ObjectStoreUser; owns the bucket's aggregate S3 policy (union of all BucketAccess grants) - BucketAccess: grants read-only/read-write/full access, provisioning a dedicated user (or reusing a referenced one) and delivering RW/RO keys The internal/ceph client wraps the dashboard /api/auth, /api/rgw/user and /api/rgw/bucket endpoints with lazy token auth and re-auth on 401. Bucket policies are rendered deterministically and applied via the bucket policy API (Reef 18.2+). Credentials come from the cephrgw-credentials Secret via env. Includes generated CRDs/RBAC, samples, kind manifests, Woodpecker CI, and docs/ceph-setup.md covering the required Ceph dashboard account, RGW wiring and permissions.
153 lines
4.1 KiB
Go
153 lines
4.1 KiB
Go
package ceph
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/url"
|
|
)
|
|
|
|
// UserKey is an S3 access/secret key pair belonging to an RGW user.
|
|
type UserKey struct {
|
|
User string `json:"user"`
|
|
AccessKey string `json:"access_key"`
|
|
SecretKey string `json:"secret_key"`
|
|
}
|
|
|
|
// User is the subset of an RGW user record the operator consumes.
|
|
type User struct {
|
|
UID string `json:"user_id"`
|
|
DisplayName string `json:"display_name"`
|
|
Email string `json:"email"`
|
|
MaxBuckets int `json:"max_buckets"`
|
|
Suspended int `json:"suspended"`
|
|
Keys []UserKey `json:"keys"`
|
|
}
|
|
|
|
// S3Key returns the first access/secret key pair, if any.
|
|
func (u *User) S3Key() (UserKey, bool) {
|
|
if len(u.Keys) == 0 {
|
|
return UserKey{}, false
|
|
}
|
|
return u.Keys[0], true
|
|
}
|
|
|
|
// UserSpec describes the desired state of an RGW user.
|
|
type UserSpec struct {
|
|
UID string
|
|
DisplayName string
|
|
Email string
|
|
MaxBuckets *int32
|
|
Suspended bool
|
|
}
|
|
|
|
type createUserRequest struct {
|
|
UID string `json:"uid"`
|
|
DisplayName string `json:"display_name"`
|
|
Email string `json:"email,omitempty"`
|
|
MaxBuckets *int32 `json:"max_buckets,omitempty"`
|
|
Suspended bool `json:"suspended"`
|
|
GenerateKey bool `json:"generate_key"`
|
|
}
|
|
|
|
type updateUserRequest struct {
|
|
DisplayName string `json:"display_name"`
|
|
Email string `json:"email,omitempty"`
|
|
MaxBuckets *int32 `json:"max_buckets,omitempty"`
|
|
Suspended bool `json:"suspended"`
|
|
}
|
|
|
|
// GetUser fetches an RGW user by uid, returning an *APIError with status 404
|
|
// (see IsNotFound) when it does not exist.
|
|
func (c *Client) GetUser(ctx context.Context, uid string) (*User, error) {
|
|
var u User
|
|
if err := c.do(ctx, http.MethodGet, "/api/rgw/user/"+url.PathEscape(uid), nil, &u, ""); err != nil {
|
|
return nil, err
|
|
}
|
|
return &u, nil
|
|
}
|
|
|
|
// CreateUser creates an RGW user, asking the dashboard to generate an S3 key
|
|
// pair. The returned User carries the generated keys.
|
|
func (c *Client) CreateUser(ctx context.Context, spec UserSpec) (*User, error) {
|
|
req := createUserRequest{
|
|
UID: spec.UID,
|
|
DisplayName: firstNonEmpty(spec.DisplayName, spec.UID),
|
|
Email: spec.Email,
|
|
MaxBuckets: spec.MaxBuckets,
|
|
Suspended: spec.Suspended,
|
|
GenerateKey: true,
|
|
}
|
|
var u User
|
|
if err := c.do(ctx, http.MethodPost, "/api/rgw/user", req, &u, ""); err != nil {
|
|
return nil, err
|
|
}
|
|
return &u, nil
|
|
}
|
|
|
|
// UpdateUser reconciles the mutable attributes of an existing RGW user.
|
|
func (c *Client) UpdateUser(ctx context.Context, spec UserSpec) (*User, error) {
|
|
req := updateUserRequest{
|
|
DisplayName: firstNonEmpty(spec.DisplayName, spec.UID),
|
|
Email: spec.Email,
|
|
MaxBuckets: spec.MaxBuckets,
|
|
Suspended: spec.Suspended,
|
|
}
|
|
var u User
|
|
if err := c.do(ctx, http.MethodPut, "/api/rgw/user/"+url.PathEscape(spec.UID), req, &u, ""); err != nil {
|
|
return nil, err
|
|
}
|
|
return &u, nil
|
|
}
|
|
|
|
// DeleteUser removes an RGW user. A 404 is treated as success.
|
|
func (c *Client) DeleteUser(ctx context.Context, uid string) error {
|
|
err := c.do(ctx, http.MethodDelete, "/api/rgw/user/"+url.PathEscape(uid), nil, nil, "")
|
|
if IsNotFound(err) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
type quotaRequest struct {
|
|
QuotaType string `json:"quota_type"`
|
|
Enabled bool `json:"enabled"`
|
|
MaxSizeKb int64 `json:"max_size_kb"`
|
|
MaxObjects int64 `json:"max_objects"`
|
|
}
|
|
|
|
// SetUserQuota applies a quota to a user. quotaType is "user" or "bucket" (the
|
|
// latter sets the per-bucket default for buckets the user owns). A nil or
|
|
// negative limit means unlimited for that dimension.
|
|
func (c *Client) SetUserQuota(ctx context.Context, uid, quotaType string, enabled bool, maxSizeBytes, maxObjects *int64) error {
|
|
req := quotaRequest{
|
|
QuotaType: quotaType,
|
|
Enabled: enabled,
|
|
MaxSizeKb: bytesToKb(maxSizeBytes),
|
|
MaxObjects: valueOr(maxObjects, -1),
|
|
}
|
|
return c.do(ctx, http.MethodPut, "/api/rgw/user/"+url.PathEscape(uid)+"/quota", req, nil, "")
|
|
}
|
|
|
|
func firstNonEmpty(vals ...string) string {
|
|
for _, v := range vals {
|
|
if v != "" {
|
|
return v
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func bytesToKb(b *int64) int64 {
|
|
if b == nil || *b < 0 {
|
|
return -1
|
|
}
|
|
return *b / 1024
|
|
}
|
|
|
|
func valueOr(v *int64, fallback int64) int64 {
|
|
if v == nil || *v < 0 {
|
|
return fallback
|
|
}
|
|
return *v
|
|
}
|