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.
91 lines
2.2 KiB
Go
91 lines
2.2 KiB
Go
package ceph
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestBuildBucketPolicyEmpty(t *testing.T) {
|
|
got, err := BuildBucketPolicy("data", nil)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if got != "" {
|
|
t.Fatalf("expected empty policy for no grants, got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestBuildBucketPolicyDeterministic(t *testing.T) {
|
|
a, err := BuildBucketPolicy("data", []Grant{
|
|
{UID: "reader", Level: LevelReadOnly},
|
|
{UID: "writer", Level: LevelReadWrite},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
b, err := BuildBucketPolicy("data", []Grant{
|
|
{UID: "writer", Level: LevelReadWrite},
|
|
{UID: "reader", Level: LevelReadOnly},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if a != b {
|
|
t.Fatalf("policy is order-dependent:\n a=%s\n b=%s", a, b)
|
|
}
|
|
}
|
|
|
|
func TestBuildBucketPolicyStructure(t *testing.T) {
|
|
raw, err := BuildBucketPolicy("data", []Grant{
|
|
{UID: "reader", Level: LevelReadOnly},
|
|
{UID: "admin", Level: LevelFull},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
var doc struct {
|
|
Version string `json:"Version"`
|
|
Statement []struct {
|
|
Effect string `json:"Effect"`
|
|
Principal map[string][]string `json:"Principal"`
|
|
Action []string `json:"Action"`
|
|
Resource []string `json:"Resource"`
|
|
} `json:"Statement"`
|
|
}
|
|
if err := json.Unmarshal([]byte(raw), &doc); err != nil {
|
|
t.Fatalf("policy is not valid JSON: %v\n%s", err, raw)
|
|
}
|
|
if doc.Version != "2012-10-17" {
|
|
t.Fatalf("unexpected version %q", doc.Version)
|
|
}
|
|
// read-only -> two statements (bucket + object); full -> one statement.
|
|
if len(doc.Statement) != 3 {
|
|
t.Fatalf("expected 3 statements, got %d", len(doc.Statement))
|
|
}
|
|
|
|
var sawFullWildcard, sawReaderPrincipal bool
|
|
for _, s := range doc.Statement {
|
|
if s.Effect != "Allow" {
|
|
t.Fatalf("expected Allow effect, got %q", s.Effect)
|
|
}
|
|
for _, a := range s.Action {
|
|
if a == "s3:*" {
|
|
sawFullWildcard = true
|
|
}
|
|
}
|
|
for _, p := range s.Principal["AWS"] {
|
|
if strings.HasSuffix(p, "user/reader") {
|
|
sawReaderPrincipal = true
|
|
}
|
|
}
|
|
}
|
|
if !sawFullWildcard {
|
|
t.Fatal("full grant did not produce an s3:* action")
|
|
}
|
|
if !sawReaderPrincipal {
|
|
t.Fatal("reader principal ARN missing")
|
|
}
|
|
}
|