Merge pull request #17852 from rook/mergify/bp/release-1.20/pr-17663

test: improve the shared-store object suite tests (backport #17663)
This commit is contained in:
mergify[bot] authored and GitHub committed 2026-06-29 20:52:12 +00:00
commit b0cc81d8b7
17 files changed
+1834 -1315

No files matched your search

+8 -13
View File
@@ -209,20 +209,15 @@ func runObjectE2ETest(helper *clients.TestClient, k8sh *utils.K8sHelper, install
// now test operation of the first object store
testObjectStoreOperations(s, helper, k8sh, settings, storeName, swiftAndKeystone)
// The namespaced test packages below all skip when TLS is enabled, so only set up the
// shared store when it will actually be used.
var sharedObjectStore *cephv1.CephObjectStore
if !tlsEnable {
var teardownSharedStore func()
sharedObjectStore, teardownSharedStore = sharedstore.Setup(s.T(), k8sh)
defer teardownSharedStore()
sharedObjectStore := sharedstore.Create(s.T(), k8sh, installer, tlsEnable,
bucketowner.Namespace, userkeys.Namespace, topickafka.Namespace, useropmask.Namespace, usercaps.Namespace)
defer sharedObjectStore.Destroy()
bucketowner.TestObjectBucketClaimBucketOwner(s.T(), k8sh, installer, logger, tlsEnable, sharedObjectStore)
userkeys.TestObjectStoreUserKeys(s.T(), k8sh, installer, logger, tlsEnable, sharedObjectStore)
topickafka.TestBucketTopicKafka(s.T(), k8sh, installer, logger, tlsEnable, sharedObjectStore)
useropmask.TestObjectStoreUserOpMask(s.T(), k8sh, installer, logger, tlsEnable, sharedObjectStore)
usercaps.TestObjectStoreUserCaps(s.T(), k8sh, installer, logger, tlsEnable, sharedObjectStore)
}
bucketowner.TestObjectBucketClaimBucketOwner(s.T(), k8sh, sharedObjectStore)
userkeys.TestObjectStoreUserKeys(s.T(), k8sh, sharedObjectStore)
topickafka.TestBucketTopicKafka(s.T(), k8sh, sharedObjectStore)
useropmask.TestObjectStoreUserOpMask(s.T(), k8sh, sharedObjectStore)
usercaps.TestObjectStoreUserCaps(s.T(), k8sh, sharedObjectStore)
bucketNotificationTestStoreName := "bucket-notification-" + storeName
createCephObjectStore(s.T(), helper, k8sh, installer, namespace, bucketNotificationTestStoreName, 1, tlsEnable, swiftAndKeystone)
+215
View File
@@ -0,0 +1,215 @@
# Object storage integration tests
This tree holds the new-style integration tests for rook's object storage
features. It is the model for converting the remaining old-style object tests
(`testObjectStoreOperations`, store lifecycle, bucket notifications, COSI);
follow the conventions here exactly so the conversion happens once.
## Execution model
- One Kubernetes cluster is shared by the whole `TestCephObjectSuite` run. The
testify suite in `tests/integration/ceph_object_test.go` owns cluster
lifecycle and dispatches into the packages here; these packages are plain Go
libraries, **not** standalone `go test` targets.
- A single shared `CephObjectStore` fixture (`util/sharedstore`) is created
once per suite pass and torn down after all packages run. It provides the
store CR, a go-ceph rgw admin client, and an SNS client, and is created with
or without TLS to match the pass. The packages run in both passes: the
with/without-TLS split runs them as separate parallel jobs, so the TLS pass
adds no serial runtime to a single job.
- Packages run **sequentially** and each test is an ordered script of
subtests. Do not use `t.Parallel`: the steps depend on prior steps, and the
cluster is shared.
## Layout
The tree mirrors `pkg/operator/ceph/object/*`: one top-level directory per
operator package, with a feature subdirectory layer when multiple test
packages cover subsets of one operator package. The operator ROOT package
(the CephObjectStore controller itself) maps to the tree root,
`tests/integration/object/` itself, not a subdirectory. Leaf package names
must not collide with std-lib package names (no `io`, no `http`).
| test package | operator package | covers |
|---|---|---|
| `bucket/owner` | `object/bucket` | OBC `bucketOwner` handling |
| `topic/kafka` | `object/topic` | CephBucketTopic kafka endpoints |
| `user/caps` | `object/user` | user capabilities |
| `user/keys` | `object/user` | explicit S3 key management |
| `user/opmask` | `object/user` | user op_mask |
| reserved: `bucket/rw`, `bucket/quota`, `bucket/policy`, `bucket/lifecycle`, `tests/integration/object/lifecycle`, `tests/integration/object/dependents`, `notification/*`, `cosi` | | future conversions |
Shared utilities live under `util/`:
- `wait4` — waits for object-test state: watch-based `Assert/Require` ×
`Create/Delete/Condition/Absent` for k8s resources, `Assert/RequireEventually`
polling for non-k8s state, `Assert/RequirePodLog` for a matching line in a
pod's log stream, and the shared timeout tiers.
- `ready` — readiness predicates for the wait4 helpers (`ObjectStoreUser`,
`BucketTopic`, `OBCBound`, ...).
- `fixture` — create-with-`t.Cleanup` helpers for pure-cleanup resources
(namespaces, StorageClasses).
- `obc` — pure constructors for the resources ObjectBucketClaim tests build
(the provisioner `StorageClass`; OBCs and per-OBC S3 clients as `bucket/*`
is converted).
- `secrets` — verification helpers for the Secret references object CRDs
publish in their status, shared by more than one package.
- `sharedstore` — the shared CephObjectStore fixture.
- `admin`, `sns`, `s3`, `tls` — client builders and TLS cert generation.
## Anatomy of a package
One exported entry function per package, called from the dispatcher:
```go
func TestObjectStoreUserCaps(t *testing.T, k8sh *utils.K8sHelper, store *sharedstore.Sharedstore) {
var (
defaultName = "test-usercaps"
objectStore = store.ObjectStore()
adminClient = store.AdminClient()
ns = &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: defaultName}}
osu1 = cephv1.CephObjectStoreUser{ /* fixture literal or constructor */ }
osuClient = k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name)
)
t.Run("ObjectStoreUser caps", func(t *testing.T) {
ctx := t.Context()
fixture.RequireNamespace(t, k8sh, ns)
t.Run(fmt.Sprintf("create CephObjectStoreUser %q", osu1.Name), func(t *testing.T) {
wait4.RequireCreate(ctx, t, osuClient, &osu1, ready.ObjectStoreUser, wait4.TimeoutLong)
})
// ... ordered subtests: act, verify, act, verify ...
})
}
```
Rules encoded in that shape:
- The entry signature is `(t, k8sh, store)` and never changes. New
dependencies are added as `Sharedstore` accessors, not parameters.
- The var block declares everything the test uses: fixture objects (use
constructors — `obc.StorageClass`, package-local helpers like
`awsKeySecret`/`objectUserKey` — to keep literals one line each), then the
typed clients bound to locals (`osuClient`, `obcClient`, `obClient`, ...).
Spelled-out client chains do not appear in test bodies.
- Exactly one outer `t.Run` per package, named `"<Subject> <feature>"`. It
must be unique across all object packages: it is the `-run` filter handle.
- `ctx := t.Context()` is the first line inside the outer `t.Run` — contexts
are test-scoped, so nothing outlives its test. (Helpers that wrap their own
`t.Run` bind their own `ctx := t.Context()` from the subtest's `t`.)
- `fixture.Require*` calls come next. Fixture teardown runs via `t.Cleanup`,
so it happens even when the test fails partway. Only PURE-CLEANUP teardown
belongs there; a deletion that asserts behavior ("deleting the OBC must not
delete the user") is an ordered subtest.
- Repeated multi-assert verification goes in a package-local
`check<Aspect>(t, deps, subject, expected ...)` helper that wraps its own
`t.Run` and takes the expected state variadically. Promote a helper to a
shared `util/` package named for its subject (e.g. `util/secrets`) only
when a second package needs it.
## Waiting
- State visible to the Kubernetes API uses the watch-based `wait4` verbs:
`RequireCreate` (create + wait ready), `AssertDelete`/`RequireDelete`
(delete + wait gone), `RequireCondition`/`AssertCondition` (wait for a
predicate on an existing resource), `AssertAbsent`/`RequireAbsent` (wait
for cascade deletion you did not issue). Predicates come from `util/ready`
or are inlined when test-specific.
- State NOT visible to the Kubernetes API (rgw admin ops, S3, SNS) uses
`wait4.AssertEventually`/`RequireEventually`, the assert/require-flavored
wrappers over the framework's `utils.Eventually` poll loop. Poll closures
must not call `assert` or `require` — return an error on transient failures
(it is surfaced in the timeout message) and assert on captured state after
the wait (capture-on-success: assign an outer variable inside the closure
just before returning nil). The closure receives a context bounded by the
wait deadline; thread it into the reads it performs.
- Log-based state (an operator or endpoint logging a line) uses
`wait4.AssertPodLog`/`RequirePodLog`, which follows the first matching pod's
log stream until a line satisfies the predicate or the timeout elapses.
- Timeouts come from the shared tiers: `wait4.TimeoutShort` (routine status
changes), `TimeoutMedium` (full reconcile, e.g. topic Ready with ARN),
`TimeoutLong` (first reconcile that may race rgw startup). Bespoke waits
(e.g. the shared store teardown ladder) pass explicit durations.
## assert vs require
- `require` (and `wait4.Require*`) for anything the rest of the (sub)test
cannot proceed without: creates, fetches of the object about to be
inspected, decoding, setup waits. Fail fast to avoid cascade noise.
- `assert` (and `wait4.Assert*`) for the properties under test — so every
violated property in a subtest is reported together — and for teardown
deletes, so one stuck finalizer does not strand the rest of cleanup.
- `wait4.RequireDelete` only when later steps depend on the deletion having
completed.
- Never `assert`/`require` inside an `Eventually` poll closure.
## Naming
- Subtest names are lowercase sentences: imperative for actions
(`"create CephObjectStoreUser %q"`, `"delete obc %q"`), declarative for
assertions (`"obc %q has bucketOwner %q set"`, `"no secrets in ns %q"`).
Resource names are always interpolated with `%q`. The `-v` output of a run
should read as the full scenario.
## Running and filtering
CI runs the suite as:
```
go test -v -timeout 2400s -failfast -run CephObjectSuite github.com/rook/rook/tests/integration
```
To select one package's tests through the suite:
```
go test -tags ceph_preview -run 'TestCephObjectSuite/TestWithoutTLS/ObjectStoreUser_keys' ./tests/integration
```
All build/vet/lint commands need the `ceph_preview` build tag or the module
does not type-check:
```
go build -tags ceph_preview ./tests/...
go vet -tags ceph_preview ./tests/integration/...
gofmt -l tests/integration
```
These are cluster-backed integration tests; the only end-to-end validation is
the CI object suite.
## Adding a new package (checklist)
1. Create `tests/integration/object/<operator-pkg>/<feature>/` per the layout
rules above.
2. Write the entry func following the anatomy section; pick a globally unique
outer `t.Run` name.
3. Wire one dispatcher line in `tests/integration/ceph_object_test.go` and,
if the package creates CephObjectStoreUsers, add its namespace to the
shared store's `AllowUsersInNamespaces`.
4. Verify with the build/vet/gofmt commands above.
## Conversion playbook (old-style tests)
Per old test: enumerate its behaviors; pick or create target package(s);
express fixtures as var-block constructors; map k8s waits to `wait4` verbs
and non-k8s polls to `Eventually`; write check* helpers for repeated
verification; wire the dispatcher; delete the old code; retire
`tests/framework/clients` methods whose last consumer is gone.
| old test | target package(s) | still needs (build in that PR) |
|---|---|---|
| `testObjectStoreOperations` S3 I/O + OBC-stays-Bound | `bucket/rw` | per-OBC S3 client (TLS-aware, from OBC secret) |
| `testObjectStoreOperations` user/bucket quotas | `bucket/quota` | per-OBC S3 client |
| `testObjectStoreOperations` bucket policy | `bucket/policy` | per-OBC S3 client |
| `testObjectStoreOperations` bucket lifecycle | `bucket/lifecycle` | per-OBC S3 client, move `lifecycleCmpOpts` out of the dispatcher |
| `testObjectStoreOperations` deletion-blocked-by-dependents | `tests/integration/object/dependents` | private-store fixture, store condition predicates in `ready` |
| `createCephObjectStore`/`runObjectE2ETestLite`/deletion asserts + zone.json canary | `tests/integration/object/lifecycle` | store create/health/delete helpers, `Sharedstore.Installer()` accessor |
| `testBucketNotifications` | `notification/*` | HTTP endpoint fixture, per-OBC S3 client (the pod-log waiter is now `wait4.Assert/RequirePodLog`) |
| `testCOSIDriver` | `cosi` | `kubectl -k` install fixture, COSI readiness waits |
| upgrade-suite object usage | stays in upgrade suite | switch to typed clients + `wait4`/`ready` |
+105 -341
View File
@@ -17,79 +17,34 @@ limitations under the License.
package owner
import (
"bufio"
"context"
"fmt"
"log"
"strings"
"testing"
"time"
"github.com/ceph/go-ceph/rgw/admin"
"github.com/coreos/pkg/capnslog"
bktv1alpha1 "github.com/kube-object-storage/lib-bucket-provisioner/pkg/apis/objectbucket.io/v1alpha1"
cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1"
"github.com/rook/rook/tests/framework/installer"
"github.com/rook/rook/tests/framework/utils"
utiladmin "github.com/rook/rook/tests/integration/object/util/admin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
storagev1 "k8s.io/api/storage/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1"
"github.com/rook/rook/tests/framework/utils"
"github.com/rook/rook/tests/integration/object/util/fixture"
"github.com/rook/rook/tests/integration/object/util/sharedstore"
"github.com/rook/rook/tests/integration/object/util/wait4"
)
func WaitForPodLogContainingText(k8sh *utils.K8sHelper, namespace string, selector *labels.Selector, text string, timeout time.Duration) error {
pods, err := k8sh.Clientset.CoreV1().Pods(namespace).List(
context.Background(),
metav1.ListOptions{
LabelSelector: (*selector).String(),
},
)
if err != nil {
return fmt.Errorf("failed to list pods: %v", err)
}
const Namespace = "test-bucketowner"
if len(pods.Items) == 0 {
return fmt.Errorf("no pods found with labels %v in namespace %q", selector, namespace)
}
// if there are multiple pods, just pick the first one
selectedPod := pods.Items[0]
log.Printf("Found pod %q (first match) with labels %v\n", selectedPod.Name, *selector)
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
req := k8sh.Clientset.CoreV1().Pods(namespace).GetLogs(selectedPod.Name, &corev1.PodLogOptions{})
logStream, err := req.Stream(ctx)
if err != nil {
return fmt.Errorf("failed to stream logs from pod %q: %v", selectedPod.Name, err)
}
defer logStream.Close()
scanner := bufio.NewScanner(logStream)
for scanner.Scan() {
line := scanner.Text()
if strings.Contains(line, text) {
break
}
}
// Check for scanner error (could be context timeout, etc.)
if err := scanner.Err(); err != nil {
return fmt.Errorf("error reading log stream: %v", err)
}
return nil
}
func TestObjectBucketClaimBucketOwner(t *testing.T, k8sh *utils.K8sHelper, installer *installer.CephInstaller, logger *capnslog.PackageLogger, tlsEnable bool, objectStore *cephv1.CephObjectStore) {
func TestObjectBucketClaimBucketOwner(t *testing.T, k8sh *utils.K8sHelper, store *sharedstore.Sharedstore) {
var (
defaultName = "test-bucketowner"
defaultName = Namespace
objectStore = store.ObjectStore()
adminClient = store.AdminClient()
ns = &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
@@ -97,16 +52,7 @@ func TestObjectBucketClaimBucketOwner(t *testing.T, k8sh *utils.K8sHelper, insta
},
}
storageClass = &storagev1.StorageClass{
ObjectMeta: metav1.ObjectMeta{
Name: defaultName,
},
Provisioner: objectStore.Namespace + ".ceph.rook.io/bucket",
Parameters: map[string]string{
"objectStoreName": objectStore.Name,
"objectStoreNamespace": objectStore.Namespace,
},
}
storageClass = fixture.StorageClass(defaultName, objectStore)
// test user without any quotas set
osu1 = cephv1.CephObjectStoreUser{
@@ -178,102 +124,48 @@ func TestObjectBucketClaimBucketOwner(t *testing.T, k8sh *utils.K8sHelper, insta
},
},
}
osuClient = k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name)
obcClient = k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBucketClaims(ns.Name)
obClient = k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBuckets()
)
t.Run("OBC bucketOwner", func(t *testing.T) {
if tlsEnable {
// There is lots of coverage of rgw working with tls enabled; skipping to save time.
// If tls is to be enabled, cert generation needs to be added and a
// different CephObjectStore name needs to be set for with/without tls as
// CephObjectStore does not currently cleanup the rgw realm.
t.Skip("skipping test for TLS enabled clusters")
}
ctx := t.Context()
var adminClient *admin.API
ctx := context.TODO()
t.Run(fmt.Sprintf("create ns %q", ns.Name), func(t *testing.T) {
_, err := k8sh.Clientset.CoreV1().Namespaces().Create(ctx, ns, metav1.CreateOptions{})
require.NoError(t, err)
})
t.Run(fmt.Sprintf("create sc %q", storageClass.Name), func(t *testing.T) {
_, err := k8sh.Clientset.StorageV1().StorageClasses().Create(ctx, storageClass, metav1.CreateOptions{})
require.NoError(t, err)
})
fixture.RequireNamespace(t, k8sh, ns)
fixture.RequireStorageClass(t, k8sh, storageClass)
// since this is an obc specific subtest we assume that CephObjectStoreUser
// is working and the rgw service state does not need to be inspected to
// confirm user creation.
t.Run(fmt.Sprintf("create CephObjectStoreUser %q", osu1.Name), func(t *testing.T) {
_, err := k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).Create(ctx, &osu1, metav1.CreateOptions{})
require.NoError(t, err)
// user creation may be slow right after rgw start up
osuReady := utils.Retry(120, time.Second, "CephObjectStoreUser is Ready", func() bool {
liveOsu, err := k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).Get(ctx, osu1.Name, metav1.GetOptions{})
if err != nil {
return false
}
if liveOsu.Status == nil {
return false
}
return liveOsu.Status.Phase == "Ready"
})
require.True(t, osuReady)
wait4.RequireCreate(ctx, t, osuClient, &osu1, wait4.ObjectStoreUser, wait4.TimeoutLong)
})
t.Run(fmt.Sprintf("create obc %q with bucketOwner %q", obc1.Name, obc1.Spec.AdditionalConfig["bucketOwner"]), func(t *testing.T) {
_, err := k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBucketClaims(ns.Name).Create(ctx, &obc1, metav1.CreateOptions{})
_, err := obcClient.Create(ctx, &obc1, metav1.CreateOptions{})
require.NoError(t, err)
})
t.Run(fmt.Sprintf("obc %q has bucketOwner %q set", obc1.Name, obc1.Spec.AdditionalConfig["bucketOwner"]), func(t *testing.T) {
var liveObc *bktv1alpha1.ObjectBucketClaim
obcBound := utils.Retry(40, time.Second, "OBC is Bound", func() bool {
var err error
liveObc, err = k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBucketClaims(ns.Name).Get(ctx, obc1.Name, metav1.GetOptions{})
if err != nil {
return false
}
return liveObc.Status.Phase == bktv1alpha1.ObjectBucketClaimStatusPhaseBound
})
require.True(t, obcBound)
liveObc := wait4.RequireCondition(ctx, t, obcClient, obc1.Name, wait4.OBCBound, wait4.TimeoutShort)
// verify that bucketOwner is set on the live obc
assert.Equal(t, obc1.Spec.AdditionalConfig["bucketOwner"], liveObc.Spec.AdditionalConfig["bucketOwner"])
})
t.Run(fmt.Sprintf("ob for obc %q has bucketOwner %q set", obc1.Name, obc1.Spec.AdditionalConfig["bucketOwner"]), func(t *testing.T) {
liveObc, err := k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBucketClaims(ns.Name).Get(ctx, obc1.Name, metav1.GetOptions{})
liveObc, err := obcClient.Get(ctx, obc1.Name, metav1.GetOptions{})
require.NoError(t, err)
obName := liveObc.Spec.ObjectBucketName
var liveOb *bktv1alpha1.ObjectBucket
obBound := utils.Retry(40, time.Second, "OB is Bound", func() bool {
var err error
liveOb, err = k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBuckets().Get(ctx, obName, metav1.GetOptions{})
if err != nil {
return false
}
return liveOb.Status.Phase == bktv1alpha1.ObjectBucketStatusPhaseBound
})
require.True(t, obBound)
liveOb := wait4.RequireCondition(ctx, t, obClient, obName, wait4.OBBound, wait4.TimeoutShort)
// verify that bucketOwner is set on the live ob
assert.Equal(t, obc1.Spec.AdditionalConfig["bucketOwner"], liveOb.Spec.Connection.AdditionalState["bucketOwner"])
})
// the rgw admin api is used to verify bucket ownership
t.Run("setup rgw admin api client", func(t *testing.T) {
var err error
adminClient, err = utiladmin.NewAdminClient(objectStore, installer, k8sh, tlsEnable)
require.NoError(t, err)
})
t.Run(fmt.Sprintf("bucket created with owner %q", obc1.Spec.AdditionalConfig["bucketOwner"]), func(t *testing.T) {
bucket, err := adminClient.GetBucketInfo(ctx, admin.Bucket{Bucket: obc1.Spec.BucketName})
require.NoError(t, err)
@@ -294,38 +186,23 @@ func TestObjectBucketClaimBucketOwner(t *testing.T, k8sh *utils.K8sHelper, insta
t.Run(fmt.Sprintf("create CephObjectStoreUser %q", osu2.Name), func(t *testing.T) {
// create user2
_, err := k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).Create(ctx, &osu2, metav1.CreateOptions{})
require.NoError(t, err)
osuReady := utils.Retry(40, time.Second, "CephObjectStoreUser is Ready", func() bool {
liveOsu, err := k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).Get(ctx, osu2.Name, metav1.GetOptions{})
if err != nil {
return false
}
if liveOsu.Status == nil {
return false
}
return liveOsu.Status.Phase == "Ready"
})
require.True(t, osuReady)
wait4.RequireCreate(ctx, t, osuClient, &osu2, wait4.ObjectStoreUser, wait4.TimeoutLong)
})
t.Run(fmt.Sprintf("update obc %q to bucketOwner %q", obc1.Name, osu2.Name), func(t *testing.T) {
// update obc bucketOwner
liveObc, err := k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBucketClaims(ns.Name).Get(ctx, obc1.Name, metav1.GetOptions{})
liveObc, err := obcClient.Get(ctx, obc1.Name, metav1.GetOptions{})
require.NoError(t, err)
liveObc.Spec.AdditionalConfig["bucketOwner"] = osu2.Name
_, err = k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBucketClaims(ns.Name).Update(ctx, liveObc, metav1.UpdateOptions{})
_, err = obcClient.Update(ctx, liveObc, metav1.UpdateOptions{})
require.NoError(t, err)
})
t.Run(fmt.Sprintf("obc %q has bucketOwner %q set", obc1.Name, osu2.Name), func(t *testing.T) {
// obc .Status.Phase does not appear to change when updating the obc
liveObc, err := k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBucketClaims(ns.Name).Get(ctx, obc1.Name, metav1.GetOptions{})
liveObc, err := obcClient.Get(ctx, obc1.Name, metav1.GetOptions{})
require.NoError(t, err)
// verify that bucketOwner is set on the live obc
@@ -334,39 +211,32 @@ func TestObjectBucketClaimBucketOwner(t *testing.T, k8sh *utils.K8sHelper, insta
t.Run(fmt.Sprintf("ob for obc %q has bucketOwner %q set", obc1.Name, osu2.Name), func(t *testing.T) {
// ob .Status.Phase does not appear to change when updating the obc
liveObc, err := k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBucketClaims(ns.Name).Get(ctx, obc1.Name, metav1.GetOptions{})
liveObc, err := obcClient.Get(ctx, obc1.Name, metav1.GetOptions{})
require.NoError(t, err)
obName := liveObc.Spec.ObjectBucketName
var liveOb *bktv1alpha1.ObjectBucket
inSync := utils.Retry(40, time.Second, "OB is Bound", func() bool {
var err error
liveOb, err = k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBuckets().Get(ctx, obName, metav1.GetOptions{})
if err != nil {
return false
}
return osu2.Name == liveOb.Spec.Connection.AdditionalState["bucketOwner"]
})
require.True(t, inSync)
liveOb := wait4.RequireCondition(ctx, t, obClient, obName,
func(ob *bktv1alpha1.ObjectBucket) bool {
return ob.Spec.Connection.AdditionalState["bucketOwner"] == osu2.Name
},
wait4.TimeoutShort)
// verify that bucketOwner is set on the live ob
assert.Equal(t, osu2.Name, liveOb.Spec.Connection.AdditionalState["bucketOwner"])
})
t.Run(fmt.Sprintf("bucket owner changed to %q", osu2.Name), func(t *testing.T) {
var bucket admin.Bucket
ownerSync := utils.Retry(40, time.Second, "bucket owner in sync", func() bool {
var err error
bucket, err = adminClient.GetBucketInfo(ctx, admin.Bucket{Bucket: obc1.Name})
wait4.AssertEventually(ctx, t, wait4.TimeoutShort, fmt.Sprintf("bucket %q owner is %q", obc1.Name, osu2.Name), func(ctx context.Context) error {
bucket, err := adminClient.GetBucketInfo(ctx, admin.Bucket{Bucket: obc1.Name})
if err != nil {
return false
return err
}
return bucket.Owner == osu2.Name
if bucket.Owner != osu2.Name {
return fmt.Errorf("bucket owner is %q, want %q", bucket.Owner, osu2.Name)
}
return nil
})
assert.True(t, ownerSync)
assert.Equal(t, osu2.Name, bucket.Owner)
})
// obc should not modify pre-existing users
@@ -382,78 +252,68 @@ func TestObjectBucketClaimBucketOwner(t *testing.T, k8sh *utils.K8sHelper, insta
t.Run(fmt.Sprintf("remove obc %q bucketOwner", obc1.Name), func(t *testing.T) {
// update/remove obc bucketOwner
liveObc, err := k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBucketClaims(ns.Name).Get(ctx, obc1.Name, metav1.GetOptions{})
liveObc, err := obcClient.Get(ctx, obc1.Name, metav1.GetOptions{})
require.NoError(t, err)
liveObc.Spec.AdditionalConfig = map[string]string{}
_, err = k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBucketClaims(ns.Name).Update(ctx, liveObc, metav1.UpdateOptions{})
_, err = obcClient.Update(ctx, liveObc, metav1.UpdateOptions{})
require.NoError(t, err)
})
t.Run(fmt.Sprintf("obc %q has no bucketOwner", obc1.Name), func(t *testing.T) {
// verify that bucketOwner is unset on the live obc
notSet := utils.Retry(40, time.Second, "bucketOwner not set", func() bool {
liveObc, err := k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBucketClaims(ns.Name).Get(ctx, obc1.Name, metav1.GetOptions{})
if err != nil {
return false
}
_, ok := liveObc.Spec.AdditionalConfig["bucketOwner"]
return !ok
})
assert.True(t, notSet)
wait4.AssertCondition(ctx, t, obcClient, obc1.Name,
func(obc *bktv1alpha1.ObjectBucketClaim) bool {
_, ok := obc.Spec.AdditionalConfig["bucketOwner"]
return !ok
},
wait4.TimeoutShort)
})
t.Run(fmt.Sprintf("ob for obc %q has no bucketOwner", obc1.Name), func(t *testing.T) {
// ob .Status.Phase does not appear to change when updating the obc
liveObc, err := k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBucketClaims(ns.Name).Get(ctx, obc1.Name, metav1.GetOptions{})
liveObc, err := obcClient.Get(ctx, obc1.Name, metav1.GetOptions{})
require.NoError(t, err)
obName := liveObc.Spec.ObjectBucketName
notSet := utils.Retry(40, time.Second, "bucketOwner not set", func() bool {
liveOb, err := k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBuckets().Get(ctx, obName, metav1.GetOptions{})
if err != nil {
return false
}
_, ok := liveOb.Spec.Connection.AdditionalState["bucketOwner"]
return !ok
})
assert.True(t, notSet)
wait4.AssertCondition(ctx, t, obClient, obName,
func(ob *bktv1alpha1.ObjectBucket) bool {
_, ok := ob.Spec.Connection.AdditionalState["bucketOwner"]
return !ok
},
wait4.TimeoutShort)
})
// the ob should retain the existing owner and not revert to a generated user
t.Run(fmt.Sprintf("bucket owner is still %q", osu2.Name), func(t *testing.T) {
var bucket admin.Bucket
ownerSync := utils.Retry(40, time.Second, "bucket owner in sync", func() bool {
var err error
bucket, err = adminClient.GetBucketInfo(ctx, admin.Bucket{Bucket: obc1.Name})
wait4.AssertEventually(ctx, t, wait4.TimeoutShort, fmt.Sprintf("bucket %q owner is still %q", obc1.Name, osu2.Name), func(ctx context.Context) error {
bucket, err := adminClient.GetBucketInfo(ctx, admin.Bucket{Bucket: obc1.Name})
if err != nil {
return false
return err
}
return bucket.Owner == osu2.Name
if bucket.Owner != osu2.Name {
return fmt.Errorf("bucket owner is %q, want %q", bucket.Owner, osu2.Name)
}
return nil
})
assert.True(t, ownerSync)
assert.Equal(t, osu2.Name, bucket.Owner)
})
// this covers setting bucketOwner on an obc initially created without an explicit owner
t.Run(fmt.Sprintf("update obc %q to bucketOwner %q", obc1.Name, osu1.Name), func(t *testing.T) {
// update obc bucketOwner
liveObc, err := k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBucketClaims(ns.Name).Get(ctx, obc1.Name, metav1.GetOptions{})
liveObc, err := obcClient.Get(ctx, obc1.Name, metav1.GetOptions{})
require.NoError(t, err)
liveObc.Spec.AdditionalConfig = map[string]string{"bucketOwner": osu1.Name}
_, err = k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBucketClaims(ns.Name).Update(ctx, liveObc, metav1.UpdateOptions{})
_, err = obcClient.Update(ctx, liveObc, metav1.UpdateOptions{})
require.NoError(t, err)
})
t.Run(fmt.Sprintf("obc %q has bucketOwner %q set", obc1.Name, osu1.Name), func(t *testing.T) {
// obc .Status.Phase does not appear to change when updating the obc
liveObc, err := k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBucketClaims(ns.Name).Get(ctx, obc1.Name, metav1.GetOptions{})
liveObc, err := obcClient.Get(ctx, obc1.Name, metav1.GetOptions{})
require.NoError(t, err)
// verify that bucketOwner is set on the live obc
@@ -462,94 +322,52 @@ func TestObjectBucketClaimBucketOwner(t *testing.T, k8sh *utils.K8sHelper, insta
t.Run(fmt.Sprintf("ob for obc %q has bucketOwner %q set", obc1.Name, osu1.Name), func(t *testing.T) {
// ob .Status.Phase does not appear to change when updating the obc
liveObc, err := k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBucketClaims(ns.Name).Get(ctx, obc1.Name, metav1.GetOptions{})
liveObc, err := obcClient.Get(ctx, obc1.Name, metav1.GetOptions{})
require.NoError(t, err)
obName := liveObc.Spec.ObjectBucketName
var liveOb *bktv1alpha1.ObjectBucket
inSync := utils.Retry(40, time.Second, "OB is Bound", func() bool {
var err error
liveOb, err = k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBuckets().Get(ctx, obName, metav1.GetOptions{})
if err != nil {
return false
}
return osu1.Name == liveOb.Spec.Connection.AdditionalState["bucketOwner"]
})
require.True(t, inSync)
liveOb := wait4.RequireCondition(ctx, t, obClient, obName,
func(ob *bktv1alpha1.ObjectBucket) bool {
return ob.Spec.Connection.AdditionalState["bucketOwner"] == osu1.Name
},
wait4.TimeoutShort)
// verify that bucketOwner is set on the live ob
assert.Equal(t, osu1.Name, liveOb.Spec.Connection.AdditionalState["bucketOwner"])
})
t.Run(fmt.Sprintf("bucket owner changed to %q", osu1.Name), func(t *testing.T) {
var bucket admin.Bucket
ownerSync := utils.Retry(40, time.Second, "bucket owner in sync", func() bool {
var err error
bucket, err = adminClient.GetBucketInfo(ctx, admin.Bucket{Bucket: obc1.Name})
wait4.AssertEventually(ctx, t, wait4.TimeoutShort, fmt.Sprintf("bucket %q owner is %q", obc1.Name, osu1.Name), func(ctx context.Context) error {
bucket, err := adminClient.GetBucketInfo(ctx, admin.Bucket{Bucket: obc1.Name})
if err != nil {
return false
return err
}
return bucket.Owner == osu1.Name
})
assert.True(t, ownerSync)
assert.Equal(t, osu1.Name, bucket.Owner)
})
t.Run(fmt.Sprintf("bucket owner changed to %q", osu1.Name), func(t *testing.T) {
var bucket admin.Bucket
ownerSync := utils.Retry(40, time.Second, "bucket owner in sync", func() bool {
var err error
bucket, err = adminClient.GetBucketInfo(ctx, admin.Bucket{Bucket: obc1.Name})
if err != nil {
return false
if bucket.Owner != osu1.Name {
return fmt.Errorf("bucket owner is %q, want %q", bucket.Owner, osu1.Name)
}
return bucket.Owner == osu1.Name
return nil
})
assert.True(t, ownerSync)
assert.Equal(t, osu1.Name, bucket.Owner)
})
t.Run(fmt.Sprintf("create obc %q with bucketOwner %q", obc2.Name, obc2.Spec.AdditionalConfig["bucketOwner"]), func(t *testing.T) {
_, err := k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBucketClaims(ns.Name).Create(ctx, &obc2, metav1.CreateOptions{})
_, err := obcClient.Create(ctx, &obc2, metav1.CreateOptions{})
require.NoError(t, err)
})
t.Run(fmt.Sprintf("obc %q has bucketOwner %q set", obc2.Name, obc2.Spec.AdditionalConfig["bucketOwner"]), func(t *testing.T) {
var liveObc *bktv1alpha1.ObjectBucketClaim
obcBound := utils.Retry(40, time.Second, "OBC is Bound", func() bool {
var err error
liveObc, err = k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBucketClaims(ns.Name).Get(ctx, obc2.Name, metav1.GetOptions{})
if err != nil {
return false
}
return liveObc.Status.Phase == bktv1alpha1.ObjectBucketClaimStatusPhaseBound
})
require.True(t, obcBound)
liveObc := wait4.RequireCondition(ctx, t, obcClient, obc2.Name, wait4.OBCBound, wait4.TimeoutShort)
// verify that bucketOwner is set on the live obc
assert.Equal(t, obc2.Spec.AdditionalConfig["bucketOwner"], liveObc.Spec.AdditionalConfig["bucketOwner"])
})
t.Run(fmt.Sprintf("ob for obc %q has bucketOwner %q set", obc2.Name, obc2.Spec.AdditionalConfig["bucketOwner"]), func(t *testing.T) {
liveObc, err := k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBucketClaims(ns.Name).Get(ctx, obc2.Name, metav1.GetOptions{})
liveObc, err := obcClient.Get(ctx, obc2.Name, metav1.GetOptions{})
require.NoError(t, err)
obName := liveObc.Spec.ObjectBucketName
var liveOb *bktv1alpha1.ObjectBucket
obBound := utils.Retry(40, time.Second, "OB is Bound", func() bool {
var err error
liveOb, err = k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBuckets().Get(ctx, obName, metav1.GetOptions{})
if err != nil {
return false
}
return liveOb.Status.Phase == bktv1alpha1.ObjectBucketStatusPhaseBound
})
require.True(t, obBound)
liveOb := wait4.RequireCondition(ctx, t, obClient, obName, wait4.OBBound, wait4.TimeoutShort)
// verify that bucketOwner is set on the live ob
assert.Equal(t, obc2.Spec.AdditionalConfig["bucketOwner"], liveOb.Spec.Connection.AdditionalState["bucketOwner"])
@@ -568,48 +386,26 @@ func TestObjectBucketClaimBucketOwner(t *testing.T, k8sh *utils.K8sHelper, insta
t.Run(fmt.Sprintf("delete obc %q", obc1.Name), func(t *testing.T) {
// lookup ob name
liveObc, err := k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBucketClaims(ns.Name).Get(ctx, obc1.Name, metav1.GetOptions{})
liveObc, err := obcClient.Get(ctx, obc1.Name, metav1.GetOptions{})
require.NoError(t, err)
obName := liveObc.Spec.ObjectBucketName
// delete obc
err = k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBucketClaims(ns.Name).Delete(ctx, obc1.Name, metav1.DeleteOptions{})
require.NoError(t, err)
// delete obc; the backing ob is garbage-collected by the provisioner
wait4.AssertDelete(ctx, t, obcClient, obc1.Name, wait4.TimeoutShort)
absent := utils.Retry(40, time.Second, "OBC is absent", func() bool {
_, err := k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBucketClaims(ns.Name).Get(ctx, obc1.Name, metav1.GetOptions{})
return err != nil
})
assert.True(t, absent)
absent = utils.Retry(40, time.Second, "OB is absent", func() bool {
_, err := k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBuckets().Get(ctx, obName, metav1.GetOptions{})
return err != nil
})
assert.True(t, absent)
wait4.AssertAbsent(ctx, t, obClient, obName, wait4.TimeoutShort)
})
t.Run(fmt.Sprintf("delete obc %q", obc2.Name), func(t *testing.T) {
// lookup ob name
liveObc, err := k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBucketClaims(ns.Name).Get(ctx, obc2.Name, metav1.GetOptions{})
liveObc, err := obcClient.Get(ctx, obc2.Name, metav1.GetOptions{})
require.NoError(t, err)
obName := liveObc.Spec.ObjectBucketName
// delete obc
err = k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBucketClaims(ns.Name).Delete(ctx, obc2.Name, metav1.DeleteOptions{})
require.NoError(t, err)
// delete obc; the backing ob is garbage-collected by the provisioner
wait4.AssertDelete(ctx, t, obcClient, obc2.Name, wait4.TimeoutShort)
absent := utils.Retry(40, time.Second, "OBC is absent", func() bool {
_, err := k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBucketClaims(ns.Name).Get(ctx, obc2.Name, metav1.GetOptions{})
return err != nil
})
assert.True(t, absent)
absent = utils.Retry(40, time.Second, "OB is absent", func() bool {
_, err := k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBuckets().Get(ctx, obName, metav1.GetOptions{})
return err != nil
})
assert.True(t, absent)
wait4.AssertAbsent(ctx, t, obClient, obName, wait4.TimeoutShort)
})
t.Run(fmt.Sprintf("user %q was not deleted by obc %q", osu1.Name, obc1.Name), func(t *testing.T) {
@@ -629,7 +425,7 @@ func TestObjectBucketClaimBucketOwner(t *testing.T, k8sh *utils.K8sHelper, insta
// test obc creation with bucketOwner set to a non-existent user, which should fail
// "failure" means the obc remains in Pending state
t.Run(fmt.Sprintf("create obc %q with non-existent bucketOwner %q", obcBogusOwner.Name, obcBogusOwner.Spec.AdditionalConfig["bucketOwner"]), func(t *testing.T) {
_, err := k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBucketClaims(ns.Name).Create(ctx, &obcBogusOwner, metav1.CreateOptions{})
_, err := obcClient.Create(ctx, &obcBogusOwner, metav1.CreateOptions{})
require.NoError(t, err)
})
@@ -637,14 +433,17 @@ func TestObjectBucketClaimBucketOwner(t *testing.T, k8sh *utils.K8sHelper, insta
selector := labels.SelectorFromSet(labels.Set{
"app": "rook-ceph-operator",
})
text := `error provisioning bucket: unable to get user \"test-bucket-owner-bogus-user\" creds: Ceph object user \"test-bucket-owner-bogus-user\" not found: NoSuchUser`
err := WaitForPodLogContainingText(k8sh, "object-ns-system", &selector, text, 10*time.Second)
require.NoError(t, err)
// match on the operator's "unable to get user ... creds" error
// mentioning the bogus owner rather than a hardcoded full log line,
// so the logger's quote encoding cannot break the match
bogusOwner := obcBogusOwner.Spec.AdditionalConfig["bucketOwner"]
wait4.RequirePodLog(ctx, t, k8sh, "object-ns-system", selector, wait4.TimeoutShort, func(line string) bool {
return strings.Contains(line, "unable to get user") && strings.Contains(line, bogusOwner)
})
})
t.Run(fmt.Sprintf("obc %q stays Pending", obcBogusOwner.Name), func(t *testing.T) {
liveObc, err := k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBucketClaims(obcBogusOwner.Namespace).Get(ctx, obcBogusOwner.Name, metav1.GetOptions{})
liveObc, err := obcClient.Get(ctx, obcBogusOwner.Name, metav1.GetOptions{})
require.NoError(t, err)
assert.True(t, bktv1alpha1.ObjectBucketClaimStatusPhasePending == liveObc.Status.Phase)
@@ -657,57 +456,22 @@ func TestObjectBucketClaimBucketOwner(t *testing.T, k8sh *utils.K8sHelper, insta
t.Run(fmt.Sprintf("delete obc %q", obcBogusOwner.Name), func(t *testing.T) {
// lookup ob name
liveObc, err := k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBucketClaims(ns.Name).Get(ctx, obcBogusOwner.Name, metav1.GetOptions{})
liveObc, err := obcClient.Get(ctx, obcBogusOwner.Name, metav1.GetOptions{})
require.NoError(t, err)
obName := liveObc.Spec.ObjectBucketName
// delete obc
err = k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBucketClaims(ns.Name).Delete(ctx, obcBogusOwner.Name, metav1.DeleteOptions{})
require.NoError(t, err)
// delete obc; the backing ob is garbage-collected by the provisioner
wait4.AssertDelete(ctx, t, obcClient, obcBogusOwner.Name, wait4.TimeoutShort)
absent := utils.Retry(40, time.Second, "OBC is absent", func() bool {
_, err := k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBucketClaims(ns.Name).Get(ctx, obcBogusOwner.Name, metav1.GetOptions{})
return err != nil
})
assert.True(t, absent)
absent = utils.Retry(40, time.Second, "OB is absent", func() bool {
_, err := k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBuckets().Get(ctx, obName, metav1.GetOptions{})
return err != nil
})
assert.True(t, absent)
wait4.AssertAbsent(ctx, t, obClient, obName, wait4.TimeoutShort)
})
t.Run(fmt.Sprintf("delete CephObjectStoreUser %q", osu2.Name), func(t *testing.T) {
err := k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).Delete(ctx, osu2.Name, metav1.DeleteOptions{})
require.NoError(t, err)
absent := utils.Retry(40, time.Second, "CephObjectStoreUser is absent", func() bool {
_, err := k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).Get(ctx, osu2.Name, metav1.GetOptions{})
return err != nil
})
assert.True(t, absent)
wait4.AssertDelete(ctx, t, osuClient, osu2.Name, wait4.TimeoutShort)
})
t.Run(fmt.Sprintf("delete CephObjectStoreUser %q", osu1.Name), func(t *testing.T) {
err := k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).Delete(ctx, osu1.Name, metav1.DeleteOptions{})
require.NoError(t, err)
absent := utils.Retry(40, time.Second, "CephObjectStoreUser is absent", func() bool {
_, err := k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).Get(ctx, osu1.Name, metav1.GetOptions{})
return err != nil
})
assert.True(t, absent)
})
t.Run(fmt.Sprintf("delete sc %q", storageClass.Name), func(t *testing.T) {
err := k8sh.Clientset.StorageV1().StorageClasses().Delete(ctx, storageClass.Name, metav1.DeleteOptions{})
require.NoError(t, err)
})
t.Run(fmt.Sprintf("delete ns %q", ns.Name), func(t *testing.T) {
err := k8sh.Clientset.CoreV1().Namespaces().Delete(ctx, ns.Name, metav1.DeleteOptions{})
require.NoError(t, err)
wait4.AssertDelete(ctx, t, osuClient, osu1.Name, wait4.TimeoutShort)
})
})
}
+151 -338
View File
@@ -22,117 +22,109 @@ import (
"fmt"
"net/url"
"testing"
"time"
"github.com/aws/aws-sdk-go-v2/service/sns"
"github.com/coreos/pkg/capnslog"
bktv1alpha1 "github.com/kube-object-storage/lib-bucket-provisioner/pkg/apis/objectbucket.io/v1alpha1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
storagev1 "k8s.io/api/storage/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1"
"github.com/rook/rook/tests/framework/installer"
"github.com/rook/rook/tests/framework/utils"
utilsns "github.com/rook/rook/tests/integration/object/util/sns"
"github.com/rook/rook/tests/integration/object/util/fixture"
"github.com/rook/rook/tests/integration/object/util/secrets"
"github.com/rook/rook/tests/integration/object/util/sharedstore"
"github.com/rook/rook/tests/integration/object/util/wait4"
)
func checkStatusSecrets(t *testing.T, k8sh *utils.K8sHelper, bt *cephv1.CephBucketTopic, expectedSecrets []*corev1.Secret) {
t.Run(fmt.Sprintf("cephBucketTopic %q has .status.secrets set", bt.Name), func(t *testing.T) {
ctx := context.TODO()
liveBt, err := k8sh.RookClientset.CephV1().CephBucketTopics(bt.Namespace).Get(ctx, bt.Name, metav1.GetOptions{})
require.NoError(t, err)
require.NotNil(t, liveBt.Status)
assert.Len(t, liveBt.Status.Secrets, len(expectedSecrets))
for _, secret := range expectedSecrets {
secretRef, err := func(secretName string, keys []cephv1.SecretReference) (cephv1.SecretReference, error) {
for _, secretRef := range keys {
if secretRef.Name == secretName {
return secretRef, nil
}
}
return cephv1.SecretReference{}, fmt.Errorf("secretReference for secret %q not found in CephObjectStoreUser.status.keys", secret.Name)
}(secret.Name, liveBt.Status.Secrets)
require.NoError(t, err)
// fetch the live secret for UID and ResourceVersion
liveSecret, err := k8sh.Clientset.CoreV1().Secrets(secret.Namespace).Get(ctx, secret.Name, metav1.GetOptions{})
require.NoError(t, err)
assert.Equal(t, liveSecret.Name, secretRef.Name)
assert.Equal(t, liveSecret.Namespace, secretRef.Namespace)
assert.Equal(t, liveSecret.UID, secretRef.UID)
assert.Equal(t, liveSecret.ResourceVersion, secretRef.ResourceVersion)
}
})
func checkStatusSecrets(t *testing.T, k8sh *utils.K8sHelper, bt *cephv1.CephBucketTopic, expected ...*corev1.Secret) {
secrets.RequireStatusRefs(t, k8sh,
k8sh.RookClientset.CephV1().CephBucketTopics(bt.Namespace), bt.Name,
fmt.Sprintf("cephBucketTopic %q has .status.secrets set", bt.Name),
func(topic *cephv1.CephBucketTopic) []cephv1.SecretReference {
if topic.Status == nil {
return nil
}
return topic.Status.Secrets
},
expected...)
}
func checkRgwTopicEndpoint(t *testing.T, snsClient *sns.Client, arn, user, pass string) {
t.Run(fmt.Sprintf("rgw topic arn %q has basic auth set", arn), func(t *testing.T) {
ctx := context.TODO()
ctx := t.Context()
var uri *url.URL
inSync := utils.Retry(40, time.Second, "rgw topic basic auth in sync", func() bool {
wait4.RequireEventually(ctx, t, wait4.TimeoutShort, "rgw topic basic auth in sync", func(ctx context.Context) error {
topicAttrs, err := snsClient.GetTopicAttributes(ctx, &sns.GetTopicAttributesInput{
TopicArn: &arn,
})
require.NoError(t, err)
if err != nil {
return err
}
// the sns endpoint attributes are returned as JSON
var endpointJSON map[string]interface{}
err = json.Unmarshal([]byte(topicAttrs.Attributes["EndPoint"]), &endpointJSON)
require.NoError(t, err)
if err := json.Unmarshal([]byte(topicAttrs.Attributes["EndPoint"]), &endpointJSON); err != nil {
return err
}
uri, err = url.Parse(string(endpointJSON["EndpointAddress"].(string)))
require.NoError(t, err)
addr, ok := endpointJSON["EndpointAddress"].(string)
if !ok {
return fmt.Errorf("topic endpoint has no EndpointAddress string: %v", endpointJSON["EndpointAddress"])
}
uriPassword, _ := uri.User.Password()
parsed, err := url.Parse(addr)
if err != nil {
return err
}
return user == uri.User.Username() && pass == uriPassword
uriPassword, _ := parsed.User.Password()
if user != parsed.User.Username() || pass != uriPassword {
return fmt.Errorf("topic basic auth not yet in sync for user %q", parsed.User.Username())
}
// capture the matching sample for the post-wait assertions
uri = parsed
return nil
})
require.True(t, inSync)
assert.Equal(t, "kafka", uri.Scheme)
assert.Equal(t, "kafka.example.com:9094", uri.Host)
assert.Equal(t, user, uri.User.Username())
uriPassword, _ := uri.User.Password()
assert.Equal(t, pass, uriPassword)
})
}
// Note that .status.ARN is nil while .status.phase == Reconciling
func cephBucketTopicReady(t *testing.T, k8sh *utils.K8sHelper, bt *cephv1.CephBucketTopic) *cephv1.CephBucketTopic {
ctx := context.TODO()
var liveBt *cephv1.CephBucketTopic
btReady := utils.Retry(60, time.Second, fmt.Sprintf("CephBucketTopic %q is Ready", bt.Name), func() bool {
var err error
liveBt, err = k8sh.RookClientset.CephV1().CephBucketTopics(bt.Namespace).Get(ctx, bt.Name, metav1.GetOptions{})
if err != nil {
return false
}
if liveBt.Status == nil {
return false
}
return liveBt.Status.Phase == string(cephv1.ConditionReady)
})
require.True(t, btReady)
require.NotNil(t, liveBt.Status.ARN)
return liveBt
// kvSecret returns a Secret holding a single key/value pair.
func kvSecret(ns, name, key, value string) *corev1.Secret {
return &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: ns,
},
Data: map[string][]byte{
key: []byte(value),
},
}
}
func TestBucketTopicKafka(t *testing.T, k8sh *utils.K8sHelper, installer *installer.CephInstaller, logger *capnslog.PackageLogger, tlsEnable bool, objectStore *cephv1.CephObjectStore) {
// secretKeySelector returns a selector for key within the named secret.
func secretKeySelector(name, key string) *corev1.SecretKeySelector {
return &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: name,
},
Key: key,
}
}
const Namespace = "test-topickafka"
func TestBucketTopicKafka(t *testing.T, k8sh *utils.K8sHelper, store *sharedstore.Sharedstore) {
var (
defaultName = "test-topickafka"
defaultName = Namespace
objectStore = store.ObjectStore()
ns = &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
@@ -140,56 +132,12 @@ func TestBucketTopicKafka(t *testing.T, k8sh *utils.K8sHelper, installer *instal
},
}
secret1 = &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: defaultName + "-secret1",
Namespace: ns.Name,
},
Data: map[string][]byte{
"user-name": []byte("kafka-user1"),
},
}
secret1 = kvSecret(ns.Name, defaultName+"-secret1", "user-name", "kafka-user1")
secret2 = kvSecret(ns.Name, defaultName+"-secret2", "password", "kafka-pass2")
secret3 = kvSecret(ns.Name, defaultName+"-secret3", "user-name", "kafka-user3")
secret4 = kvSecret(ns.Name, defaultName+"-secret4", "password", "kafka-pass4")
secret2 = &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: defaultName + "-secret2",
Namespace: ns.Name,
},
Data: map[string][]byte{
"password": []byte("kafka-pass2"),
},
}
secret3 = &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: defaultName + "-secret3",
Namespace: ns.Name,
},
Data: map[string][]byte{
"user-name": []byte("kafka-user3"),
},
}
secret4 = &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: defaultName + "-secret4",
Namespace: ns.Name,
},
Data: map[string][]byte{
"password": []byte("kafka-pass4"),
},
}
storageClass = &storagev1.StorageClass{
ObjectMeta: metav1.ObjectMeta{
Name: defaultName,
},
Provisioner: objectStore.Namespace + ".ceph.rook.io/bucket",
Parameters: map[string]string{
"objectStoreName": objectStore.Name,
"objectStoreNamespace": objectStore.Namespace,
},
}
storageClass = fixture.StorageClass(defaultName, objectStore)
bt1 = &cephv1.CephBucketTopic{
ObjectMeta: metav1.ObjectMeta{
@@ -202,21 +150,11 @@ func TestBucketTopicKafka(t *testing.T, k8sh *utils.K8sHelper, installer *instal
Persistent: false,
Endpoint: cephv1.TopicEndpointSpec{
Kafka: &cephv1.KafkaEndpointSpec{
URI: "kafka://kafka.example.com:9094",
AckLevel: "broker",
UseSSL: false,
UserSecretRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: secret1.Name,
},
Key: "user-name",
},
PasswordSecretRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: secret2.Name,
},
Key: "password",
},
URI: "kafka://kafka.example.com:9094",
AckLevel: "broker",
UseSSL: false,
UserSecretRef: secretKeySelector(secret1.Name, "user-name"),
PasswordSecretRef: secretKeySelector(secret2.Name, "password"),
},
},
},
@@ -249,275 +187,172 @@ func TestBucketTopicKafka(t *testing.T, k8sh *utils.K8sHelper, installer *instal
},
}
ctx = context.TODO()
snsClient *sns.Client
snsClient = store.SnsClient()
btClient = k8sh.RookClientset.CephV1().CephBucketTopics(ns.Name)
bnClient = k8sh.RookClientset.CephV1().CephBucketNotifications(ns.Name)
obcClient = k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBucketClaims(ns.Name)
obClient = k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBuckets()
secretClient = k8sh.Clientset.CoreV1().Secrets(ns.Name)
)
t.Run("CephBucketTopic kafka", func(t *testing.T) {
if tlsEnable {
// Skip testing with and without TLS to reduce test time
t.Skip("skipping test for TLS enabled clusters")
ctx := t.Context()
fixture.RequireNamespace(t, k8sh, ns)
for _, secret := range []*corev1.Secret{secret1, secret2, secret3, secret4} {
t.Run(fmt.Sprintf("create secret %q", secret.Name), func(t *testing.T) {
_, err := secretClient.Create(ctx, secret, metav1.CreateOptions{})
require.NoError(t, err)
})
}
t.Run(fmt.Sprintf("create ns %q", ns.Name), func(t *testing.T) {
_, err := k8sh.Clientset.CoreV1().Namespaces().Create(ctx, ns, metav1.CreateOptions{})
require.NoError(t, err)
})
t.Run(fmt.Sprintf("create secret %q", secret1.Name), func(t *testing.T) {
_, err := k8sh.Clientset.CoreV1().Secrets(secret1.Namespace).Create(ctx, secret1, metav1.CreateOptions{})
require.NoError(t, err)
})
t.Run(fmt.Sprintf("create secret %q", secret2.Name), func(t *testing.T) {
_, err := k8sh.Clientset.CoreV1().Secrets(secret2.Namespace).Create(ctx, secret2, metav1.CreateOptions{})
require.NoError(t, err)
})
t.Run(fmt.Sprintf("create secret %q", secret3.Name), func(t *testing.T) {
_, err := k8sh.Clientset.CoreV1().Secrets(secret3.Namespace).Create(ctx, secret3, metav1.CreateOptions{})
require.NoError(t, err)
})
t.Run(fmt.Sprintf("create secret %q", secret4.Name), func(t *testing.T) {
_, err := k8sh.Clientset.CoreV1().Secrets(secret4.Namespace).Create(ctx, secret4, metav1.CreateOptions{})
require.NoError(t, err)
})
// the sc, obc, and CephBucketNotification are essentially unused for testing but are created for completeness
t.Run(fmt.Sprintf("create sc %q", storageClass.Name), func(t *testing.T) {
_, err := k8sh.Clientset.StorageV1().StorageClasses().Create(ctx, storageClass, metav1.CreateOptions{})
require.NoError(t, err)
})
fixture.RequireStorageClass(t, k8sh, storageClass)
t.Run(fmt.Sprintf("create obc %q", obc1.Name), func(t *testing.T) {
_, err := k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBucketClaims(obc1.Namespace).Create(ctx, obc1, metav1.CreateOptions{})
_, err := obcClient.Create(ctx, obc1, metav1.CreateOptions{})
require.NoError(t, err)
})
t.Run(fmt.Sprintf("create CephBucketTopic %q", bt1.Name), func(t *testing.T) {
_, err := k8sh.RookClientset.CephV1().CephBucketTopics(bt1.Namespace).Create(ctx, bt1, metav1.CreateOptions{})
require.NoError(t, err)
// user creation may be slow right after rgw start up
cephBucketTopicReady(t, k8sh, bt1)
// topic creation may be slow right after rgw start up
wait4.RequireCreate(ctx, t, btClient, bt1, wait4.BucketTopic, wait4.TimeoutMedium)
})
t.Run(fmt.Sprintf("create CephBucketNotification %q", bn1.Name), func(t *testing.T) {
_, err := k8sh.RookClientset.CephV1().CephBucketNotifications(bn1.Namespace).Create(ctx, bn1, metav1.CreateOptions{})
_, err := bnClient.Create(ctx, bn1, metav1.CreateOptions{})
require.NoError(t, err)
})
t.Run("setup sns client", func(t *testing.T) {
var err error
snsClient, err = utilsns.NewClient(objectStore, k8sh, installer, tlsEnable)
require.NoError(t, err)
})
{
liveBt := cephBucketTopicReady(t, k8sh, bt1)
checkRgwTopicEndpoint(t, snsClient, *liveBt.Status.ARN, string(secret1.Data["user-name"]), string(secret2.Data["password"]))
secrets := []*corev1.Secret{secret1, secret2}
checkStatusSecrets(t, k8sh, bt1, secrets)
}
liveBt := wait4.RequireCondition(ctx, t, btClient, bt1.Name, wait4.BucketTopic, wait4.TimeoutMedium)
checkRgwTopicEndpoint(t, snsClient, *liveBt.Status.ARN, string(secret1.Data["user-name"]), string(secret2.Data["password"]))
checkStatusSecrets(t, k8sh, bt1, secret1, secret2)
t.Run("updating referenced secrets reconciles rgw topic", func(t *testing.T) {
t.Run(fmt.Sprintf("update kafka auth on CephBucketTopic %q", bt1.Name), func(t *testing.T) {
liveBt, err := k8sh.RookClientset.CephV1().CephBucketTopics(bt1.Namespace).Get(ctx, bt1.Name, metav1.GetOptions{})
liveBt, err := btClient.Get(ctx, bt1.Name, metav1.GetOptions{})
require.NoError(t, err)
liveBt.Spec.Endpoint.Kafka.UserSecretRef = &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: secret3.Name,
},
Key: "user-name",
}
liveBt.Spec.Endpoint.Kafka.UserSecretRef = secretKeySelector(secret3.Name, "user-name")
liveBt.Spec.Endpoint.Kafka.PasswordSecretRef = secretKeySelector(secret4.Name, "password")
liveBt.Spec.Endpoint.Kafka.PasswordSecretRef = &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: secret4.Name,
},
Key: "password",
}
_, err = k8sh.RookClientset.CephV1().CephBucketTopics(liveBt.Namespace).Update(ctx, liveBt, metav1.UpdateOptions{})
_, err = btClient.Update(ctx, liveBt, metav1.UpdateOptions{})
require.NoError(t, err)
})
{
liveBt := cephBucketTopicReady(t, k8sh, bt1)
checkRgwTopicEndpoint(t, snsClient, *liveBt.Status.ARN, string(secret3.Data["user-name"]), string(secret4.Data["password"]))
secrets := []*corev1.Secret{secret3, secret4}
checkStatusSecrets(t, k8sh, bt1, secrets)
}
liveBt := wait4.RequireCondition(ctx, t, btClient, bt1.Name, wait4.BucketTopic, wait4.TimeoutMedium)
checkRgwTopicEndpoint(t, snsClient, *liveBt.Status.ARN, string(secret3.Data["user-name"]), string(secret4.Data["password"]))
checkStatusSecrets(t, k8sh, bt1, secret3, secret4)
})
t.Run("removing referenced secrets reconciles rgw topic", func(t *testing.T) {
t.Run(fmt.Sprintf("delete kafka auth on CephBucketTopic %q", bt1.Name), func(t *testing.T) {
liveBt, err := k8sh.RookClientset.CephV1().CephBucketTopics(bt1.Namespace).Get(ctx, bt1.Name, metav1.GetOptions{})
liveBt, err := btClient.Get(ctx, bt1.Name, metav1.GetOptions{})
require.NoError(t, err)
liveBt.Spec.Endpoint.Kafka.UserSecretRef = nil
liveBt.Spec.Endpoint.Kafka.PasswordSecretRef = nil
_, err = k8sh.RookClientset.CephV1().CephBucketTopics(liveBt.Namespace).Update(ctx, liveBt, metav1.UpdateOptions{})
_, err = btClient.Update(ctx, liveBt, metav1.UpdateOptions{})
require.NoError(t, err)
})
{
liveBt := cephBucketTopicReady(t, k8sh, bt1)
checkRgwTopicEndpoint(t, snsClient, *liveBt.Status.ARN, "", "")
secrets := []*corev1.Secret{}
checkStatusSecrets(t, k8sh, bt1, secrets)
}
liveBt := wait4.RequireCondition(ctx, t, btClient, bt1.Name, wait4.BucketTopic, wait4.TimeoutMedium)
checkRgwTopicEndpoint(t, snsClient, *liveBt.Status.ARN, "", "")
checkStatusSecrets(t, k8sh, bt1)
})
t.Run("adding new referenced secrets reconciles rgw topic", func(t *testing.T) {
t.Run(fmt.Sprintf("add kafka auth on CephBucketTopic %q", bt1.Name), func(t *testing.T) {
liveBt, err := k8sh.RookClientset.CephV1().CephBucketTopics(bt1.Namespace).Get(ctx, bt1.Name, metav1.GetOptions{})
liveBt, err := btClient.Get(ctx, bt1.Name, metav1.GetOptions{})
require.NoError(t, err)
liveBt.Spec.Endpoint.Kafka.UserSecretRef = &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: secret3.Name,
},
Key: "user-name",
}
liveBt.Spec.Endpoint.Kafka.UserSecretRef = secretKeySelector(secret3.Name, "user-name")
liveBt.Spec.Endpoint.Kafka.PasswordSecretRef = secretKeySelector(secret4.Name, "password")
liveBt.Spec.Endpoint.Kafka.PasswordSecretRef = &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: secret4.Name,
},
Key: "password",
}
_, err = k8sh.RookClientset.CephV1().CephBucketTopics(liveBt.Namespace).Update(ctx, liveBt, metav1.UpdateOptions{})
_, err = btClient.Update(ctx, liveBt, metav1.UpdateOptions{})
require.NoError(t, err)
})
{
liveBt := cephBucketTopicReady(t, k8sh, bt1)
checkRgwTopicEndpoint(t, snsClient, *liveBt.Status.ARN, string(secret3.Data["user-name"]), string(secret4.Data["password"]))
secrets := []*corev1.Secret{secret3, secret4}
checkStatusSecrets(t, k8sh, bt1, secrets)
}
liveBt := wait4.RequireCondition(ctx, t, btClient, bt1.Name, wait4.BucketTopic, wait4.TimeoutMedium)
checkRgwTopicEndpoint(t, snsClient, *liveBt.Status.ARN, string(secret3.Data["user-name"]), string(secret4.Data["password"]))
checkStatusSecrets(t, k8sh, bt1, secret3, secret4)
})
t.Run("updating secrets reconciles rgw topic", func(t *testing.T) {
t.Run(fmt.Sprintf("update secret %q data", secret3.Name), func(t *testing.T) {
liveSecret, err := k8sh.Clientset.CoreV1().Secrets(secret3.Namespace).Get(ctx, secret3.Name, metav1.GetOptions{})
liveSecret, err := secretClient.Get(ctx, secret3.Name, metav1.GetOptions{})
require.NoError(t, err)
liveSecret.Data = map[string][]byte{
"user-name": []byte("kafka-user3-updated"),
}
_, err = k8sh.Clientset.CoreV1().Secrets(secret3.Namespace).Update(ctx, liveSecret, metav1.UpdateOptions{})
_, err = secretClient.Update(ctx, liveSecret, metav1.UpdateOptions{})
require.NoError(t, err)
})
t.Run(fmt.Sprintf("update secret %q data", secret4.Name), func(t *testing.T) {
liveSecret, err := k8sh.Clientset.CoreV1().Secrets(secret3.Namespace).Get(ctx, secret4.Name, metav1.GetOptions{})
liveSecret, err := secretClient.Get(ctx, secret4.Name, metav1.GetOptions{})
require.NoError(t, err)
liveSecret.Data = map[string][]byte{
"password": []byte("kafka-pass4-updated"),
}
_, err = k8sh.Clientset.CoreV1().Secrets(secret4.Namespace).Update(ctx, liveSecret, metav1.UpdateOptions{})
_, err = secretClient.Update(ctx, liveSecret, metav1.UpdateOptions{})
require.NoError(t, err)
})
{
liveBt := cephBucketTopicReady(t, k8sh, bt1)
liveBt := wait4.RequireCondition(ctx, t, btClient, bt1.Name, wait4.BucketTopic, wait4.TimeoutMedium)
liveSecret3, err := k8sh.Clientset.CoreV1().Secrets(secret3.Namespace).Get(ctx, secret3.Name, metav1.GetOptions{})
require.NoError(t, err)
liveSecret3, err := secretClient.Get(ctx, secret3.Name, metav1.GetOptions{})
require.NoError(t, err)
liveSecret4, err := k8sh.Clientset.CoreV1().Secrets(secret4.Namespace).Get(ctx, secret4.Name, metav1.GetOptions{})
require.NoError(t, err)
liveSecret4, err := secretClient.Get(ctx, secret4.Name, metav1.GetOptions{})
require.NoError(t, err)
checkRgwTopicEndpoint(t, snsClient, *liveBt.Status.ARN, string(liveSecret3.Data["user-name"]), string(liveSecret4.Data["password"]))
secrets := []*corev1.Secret{liveSecret3, liveSecret4}
checkStatusSecrets(t, k8sh, bt1, secrets)
}
checkRgwTopicEndpoint(t, snsClient, *liveBt.Status.ARN, string(liveSecret3.Data["user-name"]), string(liveSecret4.Data["password"]))
checkStatusSecrets(t, k8sh, bt1, liveSecret3, liveSecret4)
})
t.Run("deleting a referenced secret triggers a reconcile, which fails", func(t *testing.T) {
t.Run(fmt.Sprintf("delete secret %q", secret4.Name), func(t *testing.T) {
err := k8sh.Clientset.CoreV1().Secrets(secret4.Namespace).Delete(ctx, secret4.Name, metav1.DeleteOptions{})
err := secretClient.Delete(ctx, secret4.Name, metav1.DeleteOptions{})
require.NoError(t, err)
})
t.Run(fmt.Sprintf("CephBucketTopic %q has phase ReconcileFailed", bt1.Name), func(t *testing.T) {
btReady := utils.Retry(40, time.Second, "CephBucketTopic is ReconcileFailed", func() bool {
liveBt, err := k8sh.RookClientset.CephV1().CephBucketTopics(bt1.Namespace).Get(ctx, bt1.Name, metav1.GetOptions{})
if err != nil {
return false
}
if liveBt.Status == nil {
return false
}
return liveBt.Status.Phase == string(cephv1.ReconcileFailed)
})
require.True(t, btReady)
wait4.RequireCondition(ctx, t, btClient, bt1.Name, wait4.BucketTopicPhase(string(cephv1.ReconcileFailed)), wait4.TimeoutShort)
})
t.Run(fmt.Sprintf("create secret %q", secret4.Name), func(t *testing.T) {
_, err := k8sh.Clientset.CoreV1().Secrets(secret4.Namespace).Create(ctx, secret4, metav1.CreateOptions{})
_, err := secretClient.Create(ctx, secret4, metav1.CreateOptions{})
require.NoError(t, err)
})
cephBucketTopicReady(t, k8sh, bt1)
wait4.RequireCondition(ctx, t, btClient, bt1.Name, wait4.BucketTopic, wait4.TimeoutMedium)
})
t.Run(fmt.Sprintf("delete CephBucketNotification %q", bn1.Name), func(t *testing.T) {
err := k8sh.RookClientset.CephV1().CephBucketNotifications(bn1.Namespace).Delete(ctx, bn1.Name, metav1.DeleteOptions{})
require.NoError(t, err)
absent := utils.Retry(40, time.Second, "CephBucketNotification is absent", func() bool {
_, err := k8sh.RookClientset.CephV1().CephBucketNotifications(bt1.Namespace).Get(ctx, bt1.Name, metav1.GetOptions{})
return err != nil
})
assert.True(t, absent)
wait4.AssertDelete(ctx, t, bnClient, bn1.Name, wait4.TimeoutShort)
})
t.Run(fmt.Sprintf("no CephBucketNotification(s) in ns %q", ns.Name), func(t *testing.T) {
list, err := k8sh.RookClientset.CephV1().CephBucketNotifications(ns.Name).List(ctx, metav1.ListOptions{})
list, err := bnClient.List(ctx, metav1.ListOptions{})
require.NoError(t, err)
assert.Len(t, list.Items, 0)
})
t.Run(fmt.Sprintf("delete CephBucketTopic %q", bt1.Name), func(t *testing.T) {
err := k8sh.RookClientset.CephV1().CephBucketTopics(bt1.Namespace).Delete(ctx, bt1.Name, metav1.DeleteOptions{})
require.NoError(t, err)
absent := utils.Retry(40, time.Second, "CephBucketTopic is absent", func() bool {
_, err := k8sh.RookClientset.CephV1().CephBucketTopics(bt1.Namespace).Get(ctx, bt1.Name, metav1.GetOptions{})
return err != nil
})
assert.True(t, absent)
wait4.AssertDelete(ctx, t, btClient, bt1.Name, wait4.TimeoutShort)
})
t.Run(fmt.Sprintf("no CephBucketTopic(s) in ns %q", ns.Name), func(t *testing.T) {
list, err := k8sh.RookClientset.CephV1().CephBucketTopics(ns.Name).List(ctx, metav1.ListOptions{})
list, err := btClient.List(ctx, metav1.ListOptions{})
require.NoError(t, err)
assert.Len(t, list.Items, 0)
@@ -525,29 +360,17 @@ func TestBucketTopicKafka(t *testing.T, k8sh *utils.K8sHelper, installer *instal
t.Run(fmt.Sprintf("delete obc %q", obc1.Name), func(t *testing.T) {
// lookup ob name
liveObc, err := k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBucketClaims(obc1.Namespace).Get(ctx, obc1.Name, metav1.GetOptions{})
liveObc, err := obcClient.Get(ctx, obc1.Name, metav1.GetOptions{})
require.NoError(t, err)
obName := liveObc.Spec.ObjectBucketName
// delete obc
err = k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBucketClaims(obc1.Namespace).Delete(ctx, obc1.Name, metav1.DeleteOptions{})
require.NoError(t, err)
absent := utils.Retry(40, time.Second, "OBC is absent", func() bool {
_, err := k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBucketClaims(obc1.Namespace).Get(ctx, obc1.Name, metav1.GetOptions{})
return err != nil
})
assert.True(t, absent)
absent = utils.Retry(40, time.Second, "OB is absent", func() bool {
_, err := k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBuckets().Get(ctx, obName, metav1.GetOptions{})
return err != nil
})
assert.True(t, absent)
// delete obc; the backing ob is garbage-collected by the provisioner
wait4.AssertDelete(ctx, t, obcClient, obc1.Name, wait4.TimeoutShort)
wait4.AssertAbsent(ctx, t, obClient, obName, wait4.TimeoutShort)
})
t.Run(fmt.Sprintf("no obc(s) in ns %q", ns.Name), func(t *testing.T) {
list, err := k8sh.BucketClientset.ObjectbucketV1alpha1().ObjectBucketClaims(ns.Name).List(ctx, metav1.ListOptions{})
list, err := obcClient.List(ctx, metav1.ListOptions{})
require.NoError(t, err)
assert.Len(t, list.Items, 0)
@@ -555,61 +378,51 @@ func TestBucketTopicKafka(t *testing.T, k8sh *utils.K8sHelper, installer *instal
t.Run("CephBucketTopic deletion did not remove any secrets", func(t *testing.T) {
t.Run(fmt.Sprintf("secret %q still exists", secret1.Name), func(t *testing.T) {
_, err := k8sh.Clientset.CoreV1().Secrets(secret1.Namespace).Get(ctx, secret1.Name, metav1.GetOptions{})
_, err := secretClient.Get(ctx, secret1.Name, metav1.GetOptions{})
require.NoError(t, err)
})
t.Run(fmt.Sprintf("secret %q still exists", secret2.Name), func(t *testing.T) {
_, err := k8sh.Clientset.CoreV1().Secrets(secret2.Namespace).Get(ctx, secret2.Name, metav1.GetOptions{})
_, err := secretClient.Get(ctx, secret2.Name, metav1.GetOptions{})
require.NoError(t, err)
})
t.Run(fmt.Sprintf("secret %q still exists", secret3.Name), func(t *testing.T) {
_, err := k8sh.Clientset.CoreV1().Secrets(secret3.Namespace).Get(ctx, secret3.Name, metav1.GetOptions{})
_, err := secretClient.Get(ctx, secret3.Name, metav1.GetOptions{})
require.NoError(t, err)
})
t.Run(fmt.Sprintf("secret %q still exists", secret4.Name), func(t *testing.T) {
_, err := k8sh.Clientset.CoreV1().Secrets(secret4.Namespace).Get(ctx, secret4.Name, metav1.GetOptions{})
_, err := secretClient.Get(ctx, secret4.Name, metav1.GetOptions{})
require.NoError(t, err)
})
})
t.Run(fmt.Sprintf("delete sc %q", storageClass.Name), func(t *testing.T) {
err := k8sh.Clientset.StorageV1().StorageClasses().Delete(ctx, storageClass.Name, metav1.DeleteOptions{})
require.NoError(t, err)
})
t.Run(fmt.Sprintf("delete secret %q", secret4.Name), func(t *testing.T) {
err := k8sh.Clientset.CoreV1().Secrets(secret4.Namespace).Delete(ctx, secret4.Name, metav1.DeleteOptions{})
err := secretClient.Delete(ctx, secret4.Name, metav1.DeleteOptions{})
require.NoError(t, err)
})
t.Run(fmt.Sprintf("delete secret %q", secret3.Name), func(t *testing.T) {
err := k8sh.Clientset.CoreV1().Secrets(secret3.Namespace).Delete(ctx, secret3.Name, metav1.DeleteOptions{})
err := secretClient.Delete(ctx, secret3.Name, metav1.DeleteOptions{})
require.NoError(t, err)
})
t.Run(fmt.Sprintf("delete secret %q", secret2.Name), func(t *testing.T) {
err := k8sh.Clientset.CoreV1().Secrets(secret3.Namespace).Delete(ctx, secret2.Name, metav1.DeleteOptions{})
err := secretClient.Delete(ctx, secret2.Name, metav1.DeleteOptions{})
require.NoError(t, err)
})
t.Run(fmt.Sprintf("delete secret %q", secret1.Name), func(t *testing.T) {
err := k8sh.Clientset.CoreV1().Secrets(secret1.Namespace).Delete(ctx, secret1.Name, metav1.DeleteOptions{})
err := secretClient.Delete(ctx, secret1.Name, metav1.DeleteOptions{})
require.NoError(t, err)
})
t.Run(fmt.Sprintf("no secrets in ns %q", ns.Name), func(t *testing.T) {
secrets, err := k8sh.Clientset.CoreV1().Secrets(ns.Name).List(ctx, metav1.ListOptions{})
secrets, err := secretClient.List(ctx, metav1.ListOptions{})
require.NoError(t, err)
assert.Len(t, secrets.Items, 0)
})
t.Run(fmt.Sprintf("delete ns %q", ns.Name), func(t *testing.T) {
err := k8sh.Clientset.CoreV1().Namespaces().Delete(ctx, ns.Name, metav1.DeleteOptions{})
require.NoError(t, err)
})
})
}
+33 -69
View File
@@ -20,10 +20,8 @@ import (
"context"
"fmt"
"testing"
"time"
"github.com/ceph/go-ceph/rgw/admin"
"github.com/coreos/pkg/capnslog"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -31,14 +29,19 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1"
"github.com/rook/rook/tests/framework/installer"
"github.com/rook/rook/tests/framework/utils"
utiladmin "github.com/rook/rook/tests/integration/object/util/admin"
"github.com/rook/rook/tests/integration/object/util/fixture"
"github.com/rook/rook/tests/integration/object/util/sharedstore"
"github.com/rook/rook/tests/integration/object/util/wait4"
)
func TestObjectStoreUserCaps(t *testing.T, k8sh *utils.K8sHelper, installer *installer.CephInstaller, logger *capnslog.PackageLogger, tlsEnable bool, objectStore *cephv1.CephObjectStore) {
const Namespace = "test-usercaps"
func TestObjectStoreUserCaps(t *testing.T, k8sh *utils.K8sHelper, store *sharedstore.Sharedstore) {
var (
defaultName = "test-usercaps"
defaultName = Namespace
objectStore = store.ObjectStore()
adminClient = store.AdminClient()
ns = &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
@@ -56,47 +59,18 @@ func TestObjectStoreUserCaps(t *testing.T, k8sh *utils.K8sHelper, installer *ins
ClusterNamespace: objectStore.Namespace,
},
}
osuClient = k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name)
)
t.Run("ObjectStoreUser caps", func(t *testing.T) {
if tlsEnable {
// Skip testing with and without TLS to reduce test time
t.Skip("skipping test for TLS enabled clusters")
}
ctx := t.Context()
var adminClient *admin.API
ctx := context.TODO()
t.Run(fmt.Sprintf("create ns %q", ns.Name), func(t *testing.T) {
_, err := k8sh.Clientset.CoreV1().Namespaces().Create(ctx, ns, metav1.CreateOptions{})
require.NoError(t, err)
})
fixture.RequireNamespace(t, k8sh, ns)
t.Run(fmt.Sprintf("create CephObjectStoreUser %q", osu1.Name), func(t *testing.T) {
_, err := k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).Create(ctx, &osu1, metav1.CreateOptions{})
require.NoError(t, err)
// user creation may be slow right after rgw start up
osuReady := utils.Retry(120, time.Second, "CephObjectStoreUser is Ready", func() bool {
liveOsu, err := k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).Get(ctx, osu1.Name, metav1.GetOptions{})
if err != nil {
return false
}
if liveOsu.Status == nil {
return false
}
return liveOsu.Status.Phase == string(cephv1.ConditionReady)
})
require.True(t, osuReady)
})
// wait for cosu user to be ready so we know rgw admin api is ready
t.Run("setup rgw admin api client", func(t *testing.T) {
var err error
adminClient, err = utiladmin.NewAdminClient(objectStore, installer, k8sh, tlsEnable)
require.NoError(t, err)
wait4.RequireCreate(ctx, t, osuClient, &osu1, wait4.ObjectStoreUser, wait4.TimeoutLong)
})
t.Run(fmt.Sprintf("verify caps on CephObjectStoreUser %q", osu1.Name), func(t *testing.T) {
@@ -106,7 +80,7 @@ func TestObjectStoreUserCaps(t *testing.T, k8sh *utils.K8sHelper, installer *ins
})
t.Run(fmt.Sprintf("update caps on CephObjectStoreUser %q", osu1.Name), func(t *testing.T) {
liveOsu, err := k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).Get(ctx, osu1.Name, metav1.GetOptions{})
liveOsu, err := osuClient.Get(ctx, osu1.Name, metav1.GetOptions{})
require.NoError(t, err)
liveOsu.Spec.Capabilities = &cephv1.ObjectUserCapSpec{
@@ -114,18 +88,18 @@ func TestObjectStoreUserCaps(t *testing.T, k8sh *utils.K8sHelper, installer *ins
Usage: "read",
}
_, err = k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).Update(ctx, liveOsu, metav1.UpdateOptions{})
_, err = osuClient.Update(ctx, liveOsu, metav1.UpdateOptions{})
require.NoError(t, err)
})
t.Run(fmt.Sprintf("verify caps on CephObjectStoreUser %q", osu1.Name), func(t *testing.T) {
converged := utils.Retry(30, time.Second, "caps updated", func() bool {
wait4.AssertEventually(ctx, t, wait4.TimeoutShort, "rgw user caps match spec", func(ctx context.Context) error {
liveUser, err := adminClient.GetUser(ctx, admin.User{ID: osu1.Name})
if err != nil {
return false
return err
}
return cmp.Equal(liveUser.Caps, []admin.UserCapSpec{
if !cmp.Equal(liveUser.Caps, []admin.UserCapSpec{
{
Type: "buckets",
Perm: "*",
@@ -134,56 +108,46 @@ func TestObjectStoreUserCaps(t *testing.T, k8sh *utils.K8sHelper, installer *ins
Type: "usage",
Perm: "read",
},
})
}) {
return fmt.Errorf("caps not yet in sync: %+v", liveUser.Caps)
}
return nil
})
assert.True(t, converged)
})
t.Run(fmt.Sprintf("remove caps on CephObjectStoreUser %q", osu1.Name), func(t *testing.T) {
liveOsu, err := k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).Get(ctx, osu1.Name, metav1.GetOptions{})
liveOsu, err := osuClient.Get(ctx, osu1.Name, metav1.GetOptions{})
require.NoError(t, err)
liveOsu.Spec.Capabilities = nil
_, err = k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).Update(ctx, liveOsu, metav1.UpdateOptions{})
_, err = osuClient.Update(ctx, liveOsu, metav1.UpdateOptions{})
require.NoError(t, err)
})
t.Run(fmt.Sprintf("verify caps on CephObjectStoreUser %q returns to default", osu1.Name), func(t *testing.T) {
converged := utils.Retry(30, time.Second, "caps updated", func() bool {
wait4.AssertEventually(ctx, t, wait4.TimeoutShort, "rgw user caps are empty", func(ctx context.Context) error {
liveUser, err := adminClient.GetUser(ctx, admin.User{ID: osu1.Name})
if err != nil {
return false
return err
}
return len(liveUser.Caps) == 0
if len(liveUser.Caps) != 0 {
return fmt.Errorf("caps not yet empty: %+v", liveUser.Caps)
}
return nil
})
assert.True(t, converged)
})
t.Run(fmt.Sprintf("delete CephObjectStoreUser %q", osu1.Name), func(t *testing.T) {
err := k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).Delete(ctx, osu1.Name, metav1.DeleteOptions{})
require.NoError(t, err)
absent := utils.Retry(40, time.Second, "CephObjectStoreUser is absent", func() bool {
_, err := k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).Get(ctx, osu1.Name, metav1.GetOptions{})
return err != nil
})
assert.True(t, absent)
wait4.AssertDelete(ctx, t, osuClient, osu1.Name, wait4.TimeoutShort)
})
t.Run(fmt.Sprintf("no CephObjectStoreUsers in ns %q", ns.Name), func(t *testing.T) {
osus, err := k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).List(ctx, metav1.ListOptions{})
osus, err := osuClient.List(ctx, metav1.ListOptions{})
require.NoError(t, err)
assert.Len(t, osus.Items, 0)
})
t.Run(fmt.Sprintf("delete ns %q", ns.Name), func(t *testing.T) {
err := k8sh.Clientset.CoreV1().Namespaces().Delete(ctx, ns.Name, metav1.DeleteOptions{})
require.NoError(t, err)
})
})
}
+151 -405
View File
@@ -20,19 +20,19 @@ import (
"context"
"fmt"
"testing"
"time"
"github.com/ceph/go-ceph/rgw/admin"
"github.com/coreos/pkg/capnslog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1"
"github.com/rook/rook/tests/framework/installer"
"github.com/rook/rook/tests/framework/utils"
utiladmin "github.com/rook/rook/tests/integration/object/util/admin"
"github.com/rook/rook/tests/integration/object/util/fixture"
"github.com/rook/rook/tests/integration/object/util/secrets"
"github.com/rook/rook/tests/integration/object/util/sharedstore"
"github.com/rook/rook/tests/integration/object/util/wait4"
)
// generate the secret name the operator is expected to generate for the CephObjectStoreUser
@@ -40,6 +40,40 @@ func generateObjectStoreUserSecretName(osu cephv1.CephObjectStoreUser) string {
return "rook-ceph-object-user-" + osu.Spec.Store + "-" + osu.Name
}
// awsKeySecret returns a Secret holding an s3 access/secret key pair under the
// conventional AWS env var names.
func awsKeySecret(ns, name, accessKey, secretKey string) *corev1.Secret {
return &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: ns,
},
Data: map[string][]byte{
"AWS_ACCESS_KEY_ID": []byte(accessKey),
"AWS_SECRET_ACCESS_KEY": []byte(secretKey),
},
}
}
// objectUserKey returns an ObjectUserKey referencing the access/secret key
// pair stored in the named awsKeySecret secret.
func objectUserKey(secretName string) cephv1.ObjectUserKey {
return cephv1.ObjectUserKey{
AccessKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: secretName,
},
Key: "AWS_ACCESS_KEY_ID",
},
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: secretName,
},
Key: "AWS_SECRET_ACCESS_KEY",
},
}
}
// find a UserKeySpec by AccessKey value
func findUserKeySpec(keys []admin.UserKeySpec, accessKey string) (admin.UserKeySpec, error) {
for _, k := range keys {
@@ -50,71 +84,63 @@ func findUserKeySpec(keys []admin.UserKeySpec, accessKey string) (admin.UserKeyS
return admin.UserKeySpec{}, fmt.Errorf("UserKeySpec with AccessKey %q not found", accessKey)
}
func checkStatusKeys(t *testing.T, k8sh *utils.K8sHelper, osu cephv1.CephObjectStoreUser, expectedSecrets []*corev1.Secret) {
t.Run(fmt.Sprintf("cephObjectStoreUser %q has .status.keys set", osu.Name), func(t *testing.T) {
ctx := context.TODO()
liveOsu, err := k8sh.RookClientset.CephV1().CephObjectStoreUsers(osu.Namespace).Get(ctx, osu.Name, metav1.GetOptions{})
require.NoError(t, err)
require.NotNil(t, liveOsu.Status)
assert.Len(t, liveOsu.Status.Keys, len(expectedSecrets))
for _, secret := range expectedSecrets {
secretRef, err := func(secretName string, keys []cephv1.SecretReference) (cephv1.SecretReference, error) {
for _, secretRef := range keys {
if secretRef.Name == secretName {
return secretRef, nil
}
}
return cephv1.SecretReference{}, fmt.Errorf("secretReference for secret %q not found in CephObjectStoreUser.status.keys", secret.Name)
}(secret.Name, liveOsu.Status.Keys)
require.NoError(t, err)
// fetch the live secret for UID and ResourceVersion
liveSecret, err := k8sh.Clientset.CoreV1().Secrets(osu.Namespace).Get(ctx, secret.Name, metav1.GetOptions{})
require.NoError(t, err)
assert.Equal(t, liveSecret.Name, secretRef.Name)
assert.Equal(t, liveSecret.Namespace, secretRef.Namespace)
assert.Equal(t, liveSecret.UID, secretRef.UID)
assert.Equal(t, liveSecret.ResourceVersion, secretRef.ResourceVersion)
}
})
func checkStatusKeys(t *testing.T, k8sh *utils.K8sHelper, osu cephv1.CephObjectStoreUser, expected ...*corev1.Secret) {
secrets.RequireStatusRefs(t, k8sh,
k8sh.RookClientset.CephV1().CephObjectStoreUsers(osu.Namespace), osu.Name,
fmt.Sprintf("cephObjectStoreUser %q has .status.keys set", osu.Name),
func(u *cephv1.CephObjectStoreUser) []cephv1.SecretReference {
if u.Status == nil {
return nil
}
return u.Status.Keys
},
expected...)
}
func checkRgwUserKeys(t *testing.T, adminClient *admin.API, osu cephv1.CephObjectStoreUser, expectedSecrets []*corev1.Secret, accessKeyName, secretKeyName string) {
t.Run(fmt.Sprintf("rgw user %q has keys set", osu.Name), func(t *testing.T) {
ctx := context.TODO()
// requireRgwUserKeys verifies every expected key on the rgw user — reporting
// all divergences rather than stopping at the first — and then aborts the
// caller if any check failed: the steps that follow assume the keys are in
// sync, so continuing would only cascade noise.
func requireRgwUserKeys(t *testing.T, adminClient *admin.API, osu cephv1.CephObjectStoreUser, accessKeyName, secretKeyName string, expected ...*corev1.Secret) {
t.Helper()
for _, secret := range expectedSecrets {
var keySpec admin.UserKeySpec
if !t.Run(fmt.Sprintf("rgw user %q has keys set", osu.Name), func(t *testing.T) {
ctx := t.Context()
for _, secret := range expected {
// assume that the .Phase doesn't change when updating keys
inSync := utils.Retry(40, time.Second, fmt.Sprintf("CephObjectStoreUser has key in sync with secret %q ", secret.Name), func() bool {
wait4.AssertEventually(ctx, t, wait4.TimeoutShort, fmt.Sprintf("rgw user key in sync with secret %q", secret.Name), func(ctx context.Context) error {
liveUser, err := adminClient.GetUser(ctx, admin.User{ID: osu.Name})
if err != nil {
return false
return err
}
keySpec, err = findUserKeySpec(liveUser.Keys, string(secret.Data[accessKeyName]))
return err == nil
keySpec, err := findUserKeySpec(liveUser.Keys, string(secret.Data[accessKeyName]))
if err != nil {
return err
}
if keySpec.SecretKey != string(secret.Data[secretKeyName]) {
return fmt.Errorf("secret key for %q not yet in sync", secret.Name)
}
return nil
})
require.True(t, inSync)
assert.Equal(t, string(secret.Data[accessKeyName]), keySpec.AccessKey)
assert.Equal(t, string(secret.Data[secretKeyName]), keySpec.SecretKey)
}
// check that no extra keys are present
liveUser, err := adminClient.GetUser(ctx, admin.User{ID: osu.Name})
require.NoError(t, err)
assert.Equal(t, len(expectedSecrets), len(liveUser.Keys))
})
assert.Equal(t, len(expected), len(liveUser.Keys))
}) {
t.FailNow()
}
}
func TestObjectStoreUserKeys(t *testing.T, k8sh *utils.K8sHelper, installer *installer.CephInstaller, logger *capnslog.PackageLogger, tlsEnable bool, objectStore *cephv1.CephObjectStore) {
const Namespace = "test-userkeys"
func TestObjectStoreUserKeys(t *testing.T, k8sh *utils.K8sHelper, store *sharedstore.Sharedstore) {
var (
defaultName = "test-userkeys"
defaultName = Namespace
objectStore = store.ObjectStore()
adminClient = store.AdminClient()
ns = &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
@@ -122,60 +148,11 @@ func TestObjectStoreUserKeys(t *testing.T, k8sh *utils.K8sHelper, installer *ins
},
}
secret1 = &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: defaultName + "-secret1",
Namespace: ns.Name,
},
Data: map[string][]byte{
"AWS_ACCESS_KEY_ID": []byte("P1ANF2BP4K2LPGOR5SBB"),
"AWS_SECRET_ACCESS_KEY": []byte("yfCHPhhOed0vJkqZsGEODyJrdKqHD09OMTWCnwjX"),
},
}
secret2 = &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: defaultName + "-secret2",
Namespace: ns.Name,
},
Data: map[string][]byte{
"AWS_ACCESS_KEY_ID": []byte("2I6RPUTQFMNCSYEXZ6VM"),
"AWS_SECRET_ACCESS_KEY": []byte("uY066SWPfaOVlDeYc7GJyOTfkDejyDdXrqehS6wx"),
},
}
secret3 = &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: defaultName + "-secret3",
Namespace: ns.Name,
},
Data: map[string][]byte{
"AWS_ACCESS_KEY_ID": []byte("J4D0P20F3EDR51OSND7Y"),
"AWS_SECRET_ACCESS_KEY": []byte("jn89OpkXNoDlIHVVQ23mE2DZgPmuDK3WVH5ExOvQ"),
},
}
secret4 = &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: defaultName + "-secret4",
Namespace: ns.Name,
},
Data: map[string][]byte{
"AWS_ACCESS_KEY_ID": []byte("MPZX7DG5WJWQ6VPCSLYT"),
"AWS_SECRET_ACCESS_KEY": []byte("phh7DIxnLPeSD2V6FUouhmnWrKlKRD5dBykyXozX"),
},
}
secret5 = &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: defaultName + "-secret5",
Namespace: ns.Name,
},
Data: map[string][]byte{
"AWS_ACCESS_KEY_ID": []byte("7TNSSANCO5KXK23IPT91"),
"AWS_SECRET_ACCESS_KEY": []byte("HksEDf0hEh3PtTvl7s9x6CyXfkWuY8eAMYAcvH5l"),
},
}
secret1 = awsKeySecret(ns.Name, defaultName+"-secret1", "P1ANF2BP4K2LPGOR5SBB", "yfCHPhhOed0vJkqZsGEODyJrdKqHD09OMTWCnwjX")
secret2 = awsKeySecret(ns.Name, defaultName+"-secret2", "2I6RPUTQFMNCSYEXZ6VM", "uY066SWPfaOVlDeYc7GJyOTfkDejyDdXrqehS6wx")
secret3 = awsKeySecret(ns.Name, defaultName+"-secret3", "J4D0P20F3EDR51OSND7Y", "jn89OpkXNoDlIHVVQ23mE2DZgPmuDK3WVH5ExOvQ")
secret4 = awsKeySecret(ns.Name, defaultName+"-secret4", "MPZX7DG5WJWQ6VPCSLYT", "phh7DIxnLPeSD2V6FUouhmnWrKlKRD5dBykyXozX")
secret5 = awsKeySecret(ns.Name, defaultName+"-secret5", "7TNSSANCO5KXK23IPT91", "HksEDf0hEh3PtTvl7s9x6CyXfkWuY8eAMYAcvH5l")
osu1 = cephv1.CephObjectStoreUser{
ObjectMeta: metav1.ObjectMeta{
@@ -186,208 +163,85 @@ func TestObjectStoreUserKeys(t *testing.T, k8sh *utils.K8sHelper, installer *ins
Store: objectStore.Name,
ClusterNamespace: objectStore.Namespace,
Keys: []cephv1.ObjectUserKey{
{
AccessKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: secret1.Name,
},
Key: "AWS_ACCESS_KEY_ID",
},
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: secret1.Name,
},
Key: "AWS_SECRET_ACCESS_KEY",
},
},
{
AccessKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: secret2.Name,
},
Key: "AWS_ACCESS_KEY_ID",
},
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: secret2.Name,
},
Key: "AWS_SECRET_ACCESS_KEY",
},
},
{
AccessKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: secret3.Name,
},
Key: "AWS_ACCESS_KEY_ID",
},
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: secret3.Name,
},
Key: "AWS_SECRET_ACCESS_KEY",
},
},
objectUserKey(secret1.Name),
objectUserKey(secret2.Name),
objectUserKey(secret3.Name),
},
},
}
osuClient = k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name)
secretClient = k8sh.Clientset.CoreV1().Secrets(ns.Name)
)
t.Run("ObjectStoreUser keys", func(t *testing.T) {
if tlsEnable {
// Skip testing with and without TLS to reduce test time
t.Skip("skipping test for TLS enabled clusters")
ctx := t.Context()
fixture.RequireNamespace(t, k8sh, ns)
for _, secret := range []*corev1.Secret{secret1, secret2, secret3, secret4, secret5} {
t.Run(fmt.Sprintf("create secret %q", secret.Name), func(t *testing.T) {
_, err := secretClient.Create(ctx, secret, metav1.CreateOptions{})
require.NoError(t, err)
})
}
var adminClient *admin.API
ctx := context.TODO()
t.Run(fmt.Sprintf("create ns %q", ns.Name), func(t *testing.T) {
_, err := k8sh.Clientset.CoreV1().Namespaces().Create(ctx, ns, metav1.CreateOptions{})
require.NoError(t, err)
})
t.Run(fmt.Sprintf("create secret %q", secret1.Name), func(t *testing.T) {
_, err := k8sh.Clientset.CoreV1().Secrets(ns.Name).Create(ctx, secret1, metav1.CreateOptions{})
require.NoError(t, err)
})
t.Run(fmt.Sprintf("create secret %q", secret2.Name), func(t *testing.T) {
_, err := k8sh.Clientset.CoreV1().Secrets(ns.Name).Create(ctx, secret2, metav1.CreateOptions{})
require.NoError(t, err)
})
t.Run(fmt.Sprintf("create secret %q", secret3.Name), func(t *testing.T) {
_, err := k8sh.Clientset.CoreV1().Secrets(ns.Name).Create(ctx, secret3, metav1.CreateOptions{})
require.NoError(t, err)
})
t.Run(fmt.Sprintf("create secret %q", secret4.Name), func(t *testing.T) {
_, err := k8sh.Clientset.CoreV1().Secrets(ns.Name).Create(ctx, secret4, metav1.CreateOptions{})
require.NoError(t, err)
})
t.Run(fmt.Sprintf("create secret %q", secret5.Name), func(t *testing.T) {
_, err := k8sh.Clientset.CoreV1().Secrets(ns.Name).Create(ctx, secret5, metav1.CreateOptions{})
require.NoError(t, err)
})
t.Run(fmt.Sprintf("create CephObjectStoreUser %q", osu1.Name), func(t *testing.T) {
_, err := k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).Create(ctx, &osu1, metav1.CreateOptions{})
require.NoError(t, err)
// user creation may be slow right after rgw start up
osuReady := utils.Retry(120, time.Second, "CephObjectStoreUser is Ready", func() bool {
liveOsu, err := k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).Get(ctx, osu1.Name, metav1.GetOptions{})
if err != nil {
return false
}
if liveOsu.Status == nil {
return false
}
return liveOsu.Status.Phase == string(cephv1.ConditionReady)
})
require.True(t, osuReady)
wait4.RequireCreate(ctx, t, osuClient, &osu1, wait4.ObjectStoreUser, wait4.TimeoutLong)
})
t.Run("setup rgw admin api client", func(t *testing.T) {
var err error
adminClient, err = utiladmin.NewAdminClient(objectStore, installer, k8sh, tlsEnable)
require.NoError(t, err)
})
{
secrets := []*corev1.Secret{secret1, secret2, secret3}
checkRgwUserKeys(t, adminClient, osu1, secrets, "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY")
checkStatusKeys(t, k8sh, osu1, secrets)
}
requireRgwUserKeys(t, adminClient, osu1, "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", secret1, secret2, secret3)
checkStatusKeys(t, k8sh, osu1, secret1, secret2, secret3)
t.Run(fmt.Sprintf("update keys on CephObjectStoreUser %q", osu1.Name), func(t *testing.T) {
liveOsu, err := k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).Get(ctx, osu1.Name, metav1.GetOptions{})
liveOsu, err := osuClient.Get(ctx, osu1.Name, metav1.GetOptions{})
require.NoError(t, err)
liveOsu.Spec.Keys = []cephv1.ObjectUserKey{
{
AccessKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: secret4.Name,
},
Key: "AWS_ACCESS_KEY_ID",
},
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: secret4.Name,
},
Key: "AWS_SECRET_ACCESS_KEY",
},
},
{
AccessKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: secret5.Name,
},
Key: "AWS_ACCESS_KEY_ID",
},
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: secret5.Name,
},
Key: "AWS_SECRET_ACCESS_KEY",
},
},
objectUserKey(secret4.Name),
objectUserKey(secret5.Name),
}
_, err = k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).Update(ctx, liveOsu, metav1.UpdateOptions{})
_, err = osuClient.Update(ctx, liveOsu, metav1.UpdateOptions{})
require.NoError(t, err)
})
{
secrets := []*corev1.Secret{secret4, secret5}
checkRgwUserKeys(t, adminClient, osu1, secrets, "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY")
checkStatusKeys(t, k8sh, osu1, secrets)
}
requireRgwUserKeys(t, adminClient, osu1, "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", secret4, secret5)
checkStatusKeys(t, k8sh, osu1, secret4, secret5)
// test transition from explicit keys -> automatic secret creation
// when all explicit keys are removed from CephObjectStoreUser, one should
// be left in place and the operator should [still] create a k8s secret for it
t.Run(fmt.Sprintf("remove all keys set on CephObjectStoreUser %q", osu1.Name), func(t *testing.T) {
liveOsu, err := k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).Get(ctx, osu1.Name, metav1.GetOptions{})
liveOsu, err := osuClient.Get(ctx, osu1.Name, metav1.GetOptions{})
require.NoError(t, err)
liveOsu.Spec.Keys = nil
_, err = k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).Update(ctx, liveOsu, metav1.UpdateOptions{})
_, err = osuClient.Update(ctx, liveOsu, metav1.UpdateOptions{})
require.NoError(t, err)
// wait for the number of keys to drop to 1
inSync := utils.Retry(40, time.Second, "CephObjectStoreUser has 1 key", func() bool {
wait4.RequireEventually(ctx, t, wait4.TimeoutShort, "rgw user has exactly 1 key", func(ctx context.Context) error {
liveUser, err := adminClient.GetUser(ctx, admin.User{ID: osu1.Name})
if err != nil {
return false
return err
}
return len(liveUser.Keys) == 1
if len(liveUser.Keys) != 1 {
return fmt.Errorf("rgw user has %d keys, want 1", len(liveUser.Keys))
}
return nil
})
require.True(t, inSync)
})
// keys updated on user
{
// fetch automatic secret as it should be the only key set on the rgw user
secretName := generateObjectStoreUserSecretName(osu1)
liveSecret, err := k8sh.Clientset.CoreV1().Secrets(ns.Name).Get(ctx, secretName, metav1.GetOptions{})
require.NoError(t, err)
// fetch the automatic secret; it should hold the only key set on the rgw user
autoSecret, err := secretClient.Get(ctx, generateObjectStoreUserSecretName(osu1), metav1.GetOptions{})
require.NoError(t, err)
secrets := []*corev1.Secret{liveSecret}
checkRgwUserKeys(t, adminClient, osu1, secrets, "AccessKey", "SecretKey")
}
requireRgwUserKeys(t, adminClient, osu1, "AccessKey", "SecretKey", autoSecret)
t.Run(fmt.Sprintf("cephObjectStoreUser %q .status.keys is unset", osu1.Name), func(t *testing.T) {
liveOsu, err := k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).Get(ctx, osu1.Name, metav1.GetOptions{})
liveOsu, err := osuClient.Get(ctx, osu1.Name, metav1.GetOptions{})
require.NoError(t, err)
require.NotNil(t, liveOsu.Status)
@@ -396,96 +250,27 @@ func TestObjectStoreUserKeys(t *testing.T, k8sh *utils.K8sHelper, installer *ins
// test transition automatic secret creation -> explicit keys
t.Run(fmt.Sprintf("add keys to CephObjectStoreUser %q", osu1.Name), func(t *testing.T) {
liveOsu, err := k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).Get(ctx, osu1.Name, metav1.GetOptions{})
liveOsu, err := osuClient.Get(ctx, osu1.Name, metav1.GetOptions{})
require.NoError(t, err)
liveOsu.Spec.Keys = []cephv1.ObjectUserKey{
{
AccessKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: secret1.Name,
},
Key: "AWS_ACCESS_KEY_ID",
},
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: secret1.Name,
},
Key: "AWS_SECRET_ACCESS_KEY",
},
},
{
AccessKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: secret2.Name,
},
Key: "AWS_ACCESS_KEY_ID",
},
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: secret2.Name,
},
Key: "AWS_SECRET_ACCESS_KEY",
},
},
{
AccessKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: secret3.Name,
},
Key: "AWS_ACCESS_KEY_ID",
},
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: secret3.Name,
},
Key: "AWS_SECRET_ACCESS_KEY",
},
},
{
AccessKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: secret4.Name,
},
Key: "AWS_ACCESS_KEY_ID",
},
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: secret4.Name,
},
Key: "AWS_SECRET_ACCESS_KEY",
},
},
{
AccessKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: secret5.Name,
},
Key: "AWS_ACCESS_KEY_ID",
},
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: secret5.Name,
},
Key: "AWS_SECRET_ACCESS_KEY",
},
},
objectUserKey(secret1.Name),
objectUserKey(secret2.Name),
objectUserKey(secret3.Name),
objectUserKey(secret4.Name),
objectUserKey(secret5.Name),
}
_, err = k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).Update(ctx, liveOsu, metav1.UpdateOptions{})
_, err = osuClient.Update(ctx, liveOsu, metav1.UpdateOptions{})
require.NoError(t, err)
})
{
secrets := []*corev1.Secret{secret1, secret2, secret3, secret4, secret5}
checkRgwUserKeys(t, adminClient, osu1, secrets, "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY")
checkStatusKeys(t, k8sh, osu1, secrets)
}
requireRgwUserKeys(t, adminClient, osu1, "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", secret1, secret2, secret3, secret4, secret5)
checkStatusKeys(t, k8sh, osu1, secret1, secret2, secret3, secret4, secret5)
// updating a secret already referenced by a CephObjectStoreUser should trigger a reconcile
t.Run(fmt.Sprintf("update secret %q data", secret1.Name), func(t *testing.T) {
liveSecret, err := k8sh.Clientset.CoreV1().Secrets(secret1.Namespace).Get(ctx, secret1.Name, metav1.GetOptions{})
liveSecret, err := secretClient.Get(ctx, secret1.Name, metav1.GetOptions{})
require.NoError(t, err)
liveSecret.Data = map[string][]byte{
@@ -493,59 +278,34 @@ func TestObjectStoreUserKeys(t *testing.T, k8sh *utils.K8sHelper, installer *ins
"AWS_SECRET_ACCESS_KEY": []byte("bar"),
}
_, err = k8sh.Clientset.CoreV1().Secrets(secret1.Namespace).Update(ctx, liveSecret, metav1.UpdateOptions{})
_, err = secretClient.Update(ctx, liveSecret, metav1.UpdateOptions{})
require.NoError(t, err)
})
{
// fetch secret that we modified
liveSecret, err := k8sh.Clientset.CoreV1().Secrets(secret1.Namespace).Get(ctx, secret1.Name, metav1.GetOptions{})
require.NoError(t, err)
updatedSecret1, err := secretClient.Get(ctx, secret1.Name, metav1.GetOptions{})
require.NoError(t, err)
secrets := []*corev1.Secret{liveSecret, secret2, secret3, secret4, secret5}
checkRgwUserKeys(t, adminClient, osu1, secrets, "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY")
checkStatusKeys(t, k8sh, osu1, secrets)
}
requireRgwUserKeys(t, adminClient, osu1, "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", updatedSecret1, secret2, secret3, secret4, secret5)
checkStatusKeys(t, k8sh, osu1, updatedSecret1, secret2, secret3, secret4, secret5)
// deleting a secret referenced by a CephObjectStoreUser should trigger a reconcile (which fails)
t.Run(fmt.Sprintf("delete secret %q", secret1.Name), func(t *testing.T) {
err := k8sh.Clientset.CoreV1().Secrets(ns.Name).Delete(ctx, secret1.Name, metav1.DeleteOptions{})
err := secretClient.Delete(ctx, secret1.Name, metav1.DeleteOptions{})
require.NoError(t, err)
})
t.Run(fmt.Sprintf("CephObjectStoreUser %q has phase ReconcileFailed", osu1.Name), func(t *testing.T) {
// user creation may be slow right after rgw start up
osuReady := utils.Retry(40, time.Second, "CephObjectStoreUser is ReconcileFailed", func() bool {
liveOsu, err := k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).Get(ctx, osu1.Name, metav1.GetOptions{})
if err != nil {
return false
}
if liveOsu.Status == nil {
return false
}
return liveOsu.Status.Phase == string(cephv1.ReconcileFailed)
})
require.True(t, osuReady)
wait4.RequireCondition(ctx, t, osuClient, osu1.Name, wait4.ObjectStoreUserPhase(string(cephv1.ReconcileFailed)), wait4.TimeoutShort)
})
t.Run("missing referenced secret should not block CephObjectStoreUser deletion", func(t *testing.T) {
t.Run(fmt.Sprintf("delete CephObjectStoreUser %q", osu1.Name), func(t *testing.T) {
err := k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).Delete(ctx, osu1.Name, metav1.DeleteOptions{})
require.NoError(t, err)
absent := utils.Retry(40, time.Second, "CephObjectStoreUser is absent", func() bool {
_, err := k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).Get(ctx, osu1.Name, metav1.GetOptions{})
return err != nil
})
assert.True(t, absent)
wait4.AssertDelete(ctx, t, osuClient, osu1.Name, wait4.TimeoutShort)
})
})
t.Run(fmt.Sprintf("no CephObjectStoreUsers in ns %q", ns.Name), func(t *testing.T) {
osus, err := k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).List(ctx, metav1.ListOptions{})
osus, err := osuClient.List(ctx, metav1.ListOptions{})
require.NoError(t, err)
assert.Len(t, osus.Items, 0)
@@ -558,39 +318,25 @@ func TestObjectStoreUserKeys(t *testing.T, k8sh *utils.K8sHelper, installer *ins
// check that CephObjectStoreUser removal did not delete any secret resources
{
secrets := []*corev1.Secret{
// secret1 was deleted in a previous step
secret2,
secret3,
secret4,
secret5,
}
// secret1 was deleted in a previous step
for _, secret := range []*corev1.Secret{secret2, secret3, secret4, secret5} {
t.Run(fmt.Sprintf("secret %q still exists", secret.Name), func(t *testing.T) {
_, err := secretClient.Get(ctx, secret.Name, metav1.GetOptions{})
require.NoError(t, err)
})
for _, secret := range secrets {
t.Run(fmt.Sprintf("secret %q still exists", secret.Name), func(t *testing.T) {
_, err := k8sh.Clientset.CoreV1().Secrets(secret.Namespace).Get(ctx, secret.Name, metav1.GetOptions{})
require.NoError(t, err)
})
// cleanup the secrets created for the test
t.Run(fmt.Sprintf("delete secret %q", secret.Name), func(t *testing.T) {
err := k8sh.Clientset.CoreV1().Secrets(secret.Namespace).Delete(ctx, secret.Name, metav1.DeleteOptions{})
require.NoError(t, err)
})
}
// cleanup the secrets created for the test
t.Run(fmt.Sprintf("delete secret %q", secret.Name), func(t *testing.T) {
err := secretClient.Delete(ctx, secret.Name, metav1.DeleteOptions{})
require.NoError(t, err)
})
}
t.Run(fmt.Sprintf("no secrets in ns %q", ns.Name), func(t *testing.T) {
secrets, err := k8sh.Clientset.CoreV1().Secrets(ns.Name).List(ctx, metav1.ListOptions{})
secrets, err := secretClient.List(ctx, metav1.ListOptions{})
require.NoError(t, err)
assert.Len(t, secrets.Items, 0)
})
t.Run(fmt.Sprintf("delete ns %q", ns.Name), func(t *testing.T) {
err := k8sh.Clientset.CoreV1().Namespaces().Delete(ctx, ns.Name, metav1.DeleteOptions{})
require.NoError(t, err)
})
})
}
+41 -76
View File
@@ -20,24 +20,27 @@ import (
"context"
"fmt"
"testing"
"time"
"github.com/ceph/go-ceph/rgw/admin"
"github.com/coreos/pkg/capnslog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1"
"github.com/rook/rook/tests/framework/installer"
"github.com/rook/rook/tests/framework/utils"
utiladmin "github.com/rook/rook/tests/integration/object/util/admin"
"github.com/rook/rook/tests/integration/object/util/fixture"
"github.com/rook/rook/tests/integration/object/util/sharedstore"
"github.com/rook/rook/tests/integration/object/util/wait4"
)
func TestObjectStoreUserOpMask(t *testing.T, k8sh *utils.K8sHelper, installer *installer.CephInstaller, logger *capnslog.PackageLogger, tlsEnable bool, objectStore *cephv1.CephObjectStore) {
const Namespace = "test-useropmask"
func TestObjectStoreUserOpMask(t *testing.T, k8sh *utils.K8sHelper, store *sharedstore.Sharedstore) {
var (
defaultName = "test-useropmask"
defaultName = Namespace
objectStore = store.ObjectStore()
adminClient = store.AdminClient()
ns = &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
@@ -55,47 +58,18 @@ func TestObjectStoreUserOpMask(t *testing.T, k8sh *utils.K8sHelper, installer *i
ClusterNamespace: objectStore.Namespace,
},
}
osuClient = k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name)
)
t.Run("ObjectStoreUser keys", func(t *testing.T) {
if tlsEnable {
// Skip testing with and without TLS to reduce test time
t.Skip("skipping test for TLS enabled clusters")
}
t.Run("ObjectStoreUser op_mask", func(t *testing.T) {
ctx := t.Context()
var adminClient *admin.API
ctx := context.TODO()
t.Run(fmt.Sprintf("create ns %q", ns.Name), func(t *testing.T) {
_, err := k8sh.Clientset.CoreV1().Namespaces().Create(ctx, ns, metav1.CreateOptions{})
require.NoError(t, err)
})
fixture.RequireNamespace(t, k8sh, ns)
t.Run(fmt.Sprintf("create CephObjectStoreUser %q", osu1.Name), func(t *testing.T) {
_, err := k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).Create(ctx, &osu1, metav1.CreateOptions{})
require.NoError(t, err)
// user creation may be slow right after rgw start up
osuReady := utils.Retry(120, time.Second, "CephObjectStoreUser is Ready", func() bool {
liveOsu, err := k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).Get(ctx, osu1.Name, metav1.GetOptions{})
if err != nil {
return false
}
if liveOsu.Status == nil {
return false
}
return liveOsu.Status.Phase == string(cephv1.ConditionReady)
})
require.True(t, osuReady)
})
// wait for cosu user to be ready so we know rgw admin api is ready
t.Run("setup rgw admin api client", func(t *testing.T) {
var err error
adminClient, err = utiladmin.NewAdminClient(objectStore, installer, k8sh, tlsEnable)
require.NoError(t, err)
wait4.RequireCreate(ctx, t, osuClient, &osu1, wait4.ObjectStoreUser, wait4.TimeoutLong)
})
t.Run(fmt.Sprintf("verify op_mask set on CephObjectStoreUser %q", osu1.Name), func(t *testing.T) {
@@ -105,95 +79,86 @@ func TestObjectStoreUserOpMask(t *testing.T, k8sh *utils.K8sHelper, installer *i
})
t.Run(fmt.Sprintf("update op-mask on CephObjectStoreUser %q", osu1.Name), func(t *testing.T) {
liveOsu, err := k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).Get(ctx, osu1.Name, metav1.GetOptions{})
liveOsu, err := osuClient.Get(ctx, osu1.Name, metav1.GetOptions{})
require.NoError(t, err)
liveOsu.Spec.OpMask = &[]cephv1.ObjectUserOpMask{"read"}
_, err = k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).Update(ctx, liveOsu, metav1.UpdateOptions{})
_, err = osuClient.Update(ctx, liveOsu, metav1.UpdateOptions{})
require.NoError(t, err)
})
t.Run(fmt.Sprintf("verify op_mask set on CephObjectStoreUser %q", osu1.Name), func(t *testing.T) {
converged := utils.Retry(30, time.Second, "OpMask updated", func() bool {
wait4.AssertEventually(ctx, t, wait4.TimeoutShort, `rgw user op_mask is "read"`, func(ctx context.Context) error {
liveUser, err := adminClient.GetUser(ctx, admin.User{ID: osu1.Name})
if err != nil {
return false
return err
}
return liveUser.OpMask == "read"
if liveUser.OpMask != "read" {
return fmt.Errorf("op_mask not yet %q: %q", "read", liveUser.OpMask)
}
return nil
})
assert.True(t, converged)
})
t.Run(fmt.Sprintf("remove op-mask on CephObjectStoreUser %q", osu1.Name), func(t *testing.T) {
liveOsu, err := k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).Get(ctx, osu1.Name, metav1.GetOptions{})
liveOsu, err := osuClient.Get(ctx, osu1.Name, metav1.GetOptions{})
require.NoError(t, err)
liveOsu.Spec.OpMask = nil
_, err = k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).Update(ctx, liveOsu, metav1.UpdateOptions{})
_, err = osuClient.Update(ctx, liveOsu, metav1.UpdateOptions{})
require.NoError(t, err)
})
t.Run(fmt.Sprintf("verify op_mask set on CephObjectStoreUser %q returns to default", osu1.Name), func(t *testing.T) {
converged := utils.Retry(30, time.Second, "OpMask updated", func() bool {
wait4.AssertEventually(ctx, t, wait4.TimeoutShort, "rgw user op_mask returns to default", func(ctx context.Context) error {
liveUser, err := adminClient.GetUser(ctx, admin.User{ID: osu1.Name})
if err != nil {
return false
return err
}
return liveUser.OpMask == "read, write, delete"
if liveUser.OpMask != "read, write, delete" {
return fmt.Errorf("op_mask not yet default: %q", liveUser.OpMask)
}
return nil
})
assert.True(t, converged)
})
t.Run(fmt.Sprintf("set empty op-mask (remove all ops) on CephObjectStoreUser %q", osu1.Name), func(t *testing.T) {
liveOsu, err := k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).Get(ctx, osu1.Name, metav1.GetOptions{})
liveOsu, err := osuClient.Get(ctx, osu1.Name, metav1.GetOptions{})
require.NoError(t, err)
liveOsu.Spec.OpMask = &[]cephv1.ObjectUserOpMask{}
_, err = k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).Update(ctx, liveOsu, metav1.UpdateOptions{})
_, err = osuClient.Update(ctx, liveOsu, metav1.UpdateOptions{})
require.NoError(t, err)
})
t.Run(fmt.Sprintf("verify op_mask set on CephObjectStoreUser %q is empty", osu1.Name), func(t *testing.T) {
converged := utils.Retry(30, time.Second, "OpMask updated", func() bool {
wait4.AssertEventually(ctx, t, wait4.TimeoutShort, "rgw user op_mask is empty", func(ctx context.Context) error {
liveUser, err := adminClient.GetUser(ctx, admin.User{ID: osu1.Name})
if err != nil {
return false
return err
}
return liveUser.OpMask == "<none>"
if liveUser.OpMask != "<none>" {
return fmt.Errorf("op_mask not yet empty: %q", liveUser.OpMask)
}
return nil
})
assert.True(t, converged)
})
t.Run(fmt.Sprintf("delete CephObjectStoreUser %q", osu1.Name), func(t *testing.T) {
err := k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).Delete(ctx, osu1.Name, metav1.DeleteOptions{})
require.NoError(t, err)
absent := utils.Retry(40, time.Second, "CephObjectStoreUser is absent", func() bool {
_, err := k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).Get(ctx, osu1.Name, metav1.GetOptions{})
return err != nil
})
assert.True(t, absent)
wait4.AssertDelete(ctx, t, osuClient, osu1.Name, wait4.TimeoutShort)
})
t.Run(fmt.Sprintf("no CephObjectStoreUsers in ns %q", ns.Name), func(t *testing.T) {
osus, err := k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name).List(ctx, metav1.ListOptions{})
osus, err := osuClient.List(ctx, metav1.ListOptions{})
require.NoError(t, err)
assert.Len(t, osus.Items, 0)
})
t.Run(fmt.Sprintf("delete ns %q", ns.Name), func(t *testing.T) {
err := k8sh.Clientset.CoreV1().Namespaces().Delete(ctx, ns.Name, metav1.DeleteOptions{})
require.NoError(t, err)
})
})
}
@@ -14,11 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package admin
// Package client builds the clients the object tests use to reach a
// CephObjectStore — the rgw admin and SNS clients and the S3 credentials and
// endpoint they need — along with the store's TLS certificate and a
// verification-skipping HTTP client for the TLS pass.
package client
import (
"context"
"crypto/tls"
"net/http"
"github.com/ceph/go-ceph/rgw/admin"
@@ -27,29 +30,22 @@ import (
cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1"
"github.com/rook/rook/tests/framework/installer"
"github.com/rook/rook/tests/framework/utils"
utils3 "github.com/rook/rook/tests/integration/object/util/s3"
)
func NewAdminClient(objectStore *cephv1.CephObjectStore, installer *installer.CephInstaller, k8sh *utils.K8sHelper, tlsEnable bool) (*admin.API, error) {
accessKey, secretKey, err := utils3.GetS3Credentials(objectStore, installer)
accessKey, secretKey, err := GetS3Credentials(objectStore, installer)
if err != nil {
return nil, errors.Wrap(err, "failed to get s3 credentials")
}
endpoint, err := utils3.GetS3Endpoint(objectStore, k8sh, tlsEnable)
endpoint, err := GetS3Endpoint(objectStore, k8sh, tlsEnable)
if err != nil {
return nil, errors.Wrap(err, "failed to get s3 endpoint")
}
httpClient := &http.Client{}
if tlsEnable {
httpClient.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
// nolint:gosec // skip TLS verification as this is a test
InsecureSkipVerify: true,
},
}
httpClient = InsecureHTTPClient()
}
adminClient, err := admin.New(endpoint, accessKey, secretKey, httpClient)
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package s3
package client
import (
"context"
@@ -64,12 +64,12 @@ func GetS3Endpoint(objectStore *cephv1.CephObjectStore, k8sh *utils.K8sHelper, t
return "", errors.Wrap(err, "failed to get objectstore svc")
}
schema := "http://"
schema, port := "http://", "80"
if tlsEnable {
schema = "https://"
schema, port = "https://", "443"
}
endpoint := schema + svc.Spec.ClusterIP + ":80"
endpoint := schema + svc.Spec.ClusterIP + ":" + port
return endpoint, nil
}
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package sns
package client
import (
"context"
@@ -29,7 +29,6 @@ import (
cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1"
"github.com/rook/rook/tests/framework/installer"
"github.com/rook/rook/tests/framework/utils"
utils3 "github.com/rook/rook/tests/integration/object/util/s3"
)
// based on the s3 endpoint example in the official docs:
@@ -48,16 +47,15 @@ func (r *snsResolverV2) ResolveEndpoint(ctx context.Context, params sns.Endpoint
}, nil
}
func NewClient(objectStore *cephv1.CephObjectStore, k8sh *utils.K8sHelper, installer *installer.CephInstaller, tlsEnable bool) (*sns.Client, error) {
func NewSNSClient(objectStore *cephv1.CephObjectStore, k8sh *utils.K8sHelper, installer *installer.CephInstaller, tlsEnable bool) (*sns.Client, error) {
ctx := context.TODO()
accessKey, secretKey, err := utils3.GetS3Credentials(objectStore, installer)
accessKey, secretKey, err := GetS3Credentials(objectStore, installer)
if err != nil {
return nil, errors.Wrap(err, "failed to get s3 credentials")
}
cfg, err := config.LoadDefaultConfig(
ctx,
loadOpts := []func(*config.LoadOptions) error{
config.WithRegion("us-east-1"),
config.WithCredentialsProvider(
credentials.NewStaticCredentialsProvider(
@@ -66,12 +64,17 @@ func NewClient(objectStore *cephv1.CephObjectStore, k8sh *utils.K8sHelper, insta
"",
),
),
)
}
if tlsEnable {
loadOpts = append(loadOpts, config.WithHTTPClient(InsecureHTTPClient()))
}
cfg, err := config.LoadDefaultConfig(ctx, loadOpts...)
if err != nil {
return nil, errors.Wrap(err, "failed to load default aws config")
}
endpoint, err := utils3.GetS3Endpoint(objectStore, k8sh, tlsEnable)
endpoint, err := GetS3Endpoint(objectStore, k8sh, tlsEnable)
if err != nil {
return nil, errors.Wrap(err, "failed to get sns endpoint")
}
@@ -0,0 +1,85 @@
/*
Copyright 2026 The Rook Authors. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package client
import (
"context"
cryptotls "crypto/tls"
"fmt"
"net/http"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/rook/rook/tests/framework/utils"
)
// InsecureHTTPClient returns an http.Client that skips TLS verification, for
// reaching a test CephObjectStore served with a cluster-signed cert the test
// process does not trust.
func InsecureHTTPClient() *http.Client {
return &http.Client{
Transport: &http.Transport{
// nolint:gosec // skip TLS verification as this is a test
TLSClientConfig: &cryptotls.Config{InsecureSkipVerify: true},
},
}
}
// GenerateRgwTLSCertSecret generates a cluster-signed TLS certificate for
// rgwServiceName and stores it as an Opaque Secret named secretName in
// namespace. The secret holds the concatenated key, certificate, and CA bundle
// under the data key "cert", which is the format a CephObjectStore expects when
// referenced via Gateway.SSLCertificateRef.
func GenerateRgwTLSCertSecret(t *testing.T, k8sh *utils.K8sHelper, namespace, secretName, rgwServiceName string) {
ctx := context.TODO()
root, err := utils.FindRookRoot()
require.NoError(t, err, "failed to get rook root")
tlscertdir := t.TempDir()
cmdArgs := utils.CommandArgs{
Command: filepath.Join(root, "tests/scripts/generate-tls-config.sh"),
CmdArgs: []string{tlscertdir, rgwServiceName, namespace},
}
cmdOut := utils.ExecuteCommand(cmdArgs)
require.NoError(t, cmdOut.Err)
tlsKeyIn, err := os.ReadFile(filepath.Join(tlscertdir, rgwServiceName+".key"))
require.NoError(t, err)
tlsCertIn, err := os.ReadFile(filepath.Join(tlscertdir, rgwServiceName+".crt"))
require.NoError(t, err)
tlsCaCertIn, err := os.ReadFile(filepath.Join(tlscertdir, rgwServiceName+".ca"))
require.NoError(t, err)
secretCertOut := fmt.Sprintf("%s%s%s", tlsKeyIn, tlsCertIn, tlsCaCertIn)
tlsK8sSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Namespace: namespace,
},
Data: map[string][]byte{
"cert": []byte(secretCertOut),
},
}
_, err = k8sh.Clientset.CoreV1().Secrets(namespace).Create(ctx, tlsK8sSecret, metav1.CreateOptions{})
require.NoError(t, err)
}
@@ -0,0 +1,74 @@
/*
Copyright 2026 The Rook Authors. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package fixture provides setup helpers for resources whose teardown is pure
// cleanup rather than part of what a test asserts. Each helper creates the
// resource and registers its deletion via t.Cleanup, so the resource is
// removed even when the test fails partway through and never reaches its
// trailing teardown steps — important because the tests share one cluster.
//
// Deletions that are themselves assertions (e.g. "deleting the OBC must not
// delete the user") do not belong here; keep those as ordered subtests.
package fixture
import (
"context"
"testing"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
storagev1 "k8s.io/api/storage/v1"
kerrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/rook/rook/tests/framework/utils"
)
// The helpers manage their own contexts rather than taking one from the
// caller: cleanup runs after the test finishes, when a test-scoped context
// (e.g. t.Context()) has already been canceled.
// RequireNamespace creates ns and registers its deletion via t.Cleanup.
// Deletion is issued without waiting for it to complete; namespace teardown is
// asynchronous and nothing in these tests depends on it finishing.
func RequireNamespace(t *testing.T, k8sh *utils.K8sHelper, ns *corev1.Namespace) {
t.Helper()
_, err := k8sh.Clientset.CoreV1().Namespaces().Create(context.Background(), ns, metav1.CreateOptions{})
require.NoError(t, err)
t.Cleanup(func() {
err := k8sh.Clientset.CoreV1().Namespaces().Delete(context.Background(), ns.Name, metav1.DeleteOptions{})
if err != nil && !kerrors.IsNotFound(err) {
t.Errorf("failed to delete namespace %q: %v", ns.Name, err)
}
})
}
// RequireStorageClass creates sc and registers its deletion via t.Cleanup.
func RequireStorageClass(t *testing.T, k8sh *utils.K8sHelper, sc *storagev1.StorageClass) {
t.Helper()
_, err := k8sh.Clientset.StorageV1().StorageClasses().Create(context.Background(), sc, metav1.CreateOptions{})
require.NoError(t, err)
t.Cleanup(func() {
err := k8sh.Clientset.StorageV1().StorageClasses().Delete(context.Background(), sc.Name, metav1.DeleteOptions{})
if err != nil && !kerrors.IsNotFound(err) {
t.Errorf("failed to delete storageclass %q: %v", sc.Name, err)
}
})
}
@@ -0,0 +1,39 @@
/*
Copyright 2026 The Rook Authors. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package fixture
import (
storagev1 "k8s.io/api/storage/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1"
)
// StorageClass returns a StorageClass for the lib-bucket-provisioner bucket
// provisioner backed by objectStore, for use by ObjectBucketClaims.
func StorageClass(name string, objectStore *cephv1.CephObjectStore) *storagev1.StorageClass {
return &storagev1.StorageClass{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Provisioner: objectStore.Namespace + ".ceph.rook.io/bucket",
Parameters: map[string]string{
"objectStoreName": objectStore.Name,
"objectStoreNamespace": objectStore.Namespace,
},
}
}
@@ -0,0 +1,94 @@
/*
Copyright 2026 The Rook Authors. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package secrets holds verification helpers for the Secret references that
// object CRDs publish in their status, shared by more than one object test
// package. Verification helpers for a different resource belong in a package
// named for that resource, not here.
package secrets
import (
"context"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1"
"github.com/rook/rook/tests/framework/utils"
"github.com/rook/rook/tests/integration/object/util/wait4"
)
// RequireStatusRefs runs a subtest named desc that waits for the status secret
// references returned by refsOf to reach len(expected) — the controller
// repopulates them asynchronously after a reconcile — and then asserts each ref
// against its live secret via AssertRefs. refsOf must tolerate an unpopulated
// status (return nil).
func RequireStatusRefs[T, L runtime.Object](
t *testing.T,
k8sh *utils.K8sHelper,
client wait4.NamespacedWatcher[T, L],
name, desc string,
refsOf func(T) []cephv1.SecretReference,
expected ...*corev1.Secret,
) {
t.Helper()
t.Run(desc, func(t *testing.T) {
ctx := t.Context()
live := wait4.RequireCondition(ctx, t, client, name, func(obj T) bool {
return len(refsOf(obj)) == len(expected)
}, wait4.TimeoutShort)
AssertRefs(ctx, t, k8sh, refsOf(live), expected...)
})
}
// AssertRefs verifies that refs contains exactly one SecretReference per
// expected Secret and that each matches the live secret's Name, Namespace,
// UID, and ResourceVersion. Live secrets are fetched from each expected
// secret's own namespace.
func AssertRefs(ctx context.Context, t *testing.T, k8sh *utils.K8sHelper, refs []cephv1.SecretReference, expected ...*corev1.Secret) {
t.Helper()
assert.Len(t, refs, len(expected))
for _, secret := range expected {
secretRef, err := findSecretRef(refs, secret.Name)
require.NoError(t, err)
// fetch the live secret for UID and ResourceVersion
liveSecret, err := k8sh.Clientset.CoreV1().Secrets(secret.Namespace).Get(ctx, secret.Name, metav1.GetOptions{})
require.NoError(t, err)
assert.Equal(t, liveSecret.Name, secretRef.Name)
assert.Equal(t, liveSecret.Namespace, secretRef.Namespace)
assert.Equal(t, liveSecret.UID, secretRef.UID)
assert.Equal(t, liveSecret.ResourceVersion, secretRef.ResourceVersion)
}
}
// findSecretRef returns the SecretReference for the named secret.
func findSecretRef(refs []cephv1.SecretReference, name string) (cephv1.SecretReference, error) {
for _, ref := range refs {
if ref.Name == name {
return ref, nil
}
}
return cephv1.SecretReference{}, fmt.Errorf("secretReference for secret %q not found", name)
}
@@ -22,59 +22,154 @@ package sharedstore
import (
"context"
"fmt"
"testing"
"time"
cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1"
"github.com/rook/rook/tests/framework/utils"
"github.com/aws/aws-sdk-go-v2/service/sns"
"github.com/ceph/go-ceph/rgw/admin"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1"
"github.com/rook/rook/tests/framework/installer"
"github.com/rook/rook/tests/framework/utils"
"github.com/rook/rook/tests/integration/object/util/client"
"github.com/rook/rook/tests/integration/object/util/wait4"
)
type Sharedstore struct {
adminClient *admin.API
snsClient *sns.Client
objectStore *cephv1.CephObjectStore
tlsEnable bool
destroy func()
}
func (s *Sharedstore) AdminClient() *admin.API {
return s.adminClient
}
func (s *Sharedstore) SnsClient() *sns.Client {
return s.snsClient
}
func (s *Sharedstore) TLSEnabled() bool {
return s.tlsEnable
}
func (s *Sharedstore) ObjectStore() *cephv1.CephObjectStore {
return s.objectStore
}
func (s *Sharedstore) Destroy() {
s.destroy()
}
// Setup creates a shared CephObjectStore and its NodePort Service in
// ObjectStoreNamespace, waits for the store to become Ready, and returns a
// teardown function that deletes both resources. The teardown function should
// be deferred by the caller.
func Setup(t *testing.T, k8sh *utils.K8sHelper) (*cephv1.CephObjectStore, func()) {
func Create(t *testing.T, k8sh *utils.K8sHelper, installer *installer.CephInstaller, tlsEnable bool, allowedNamespaces ...string) *Sharedstore {
t.Helper()
s := &Sharedstore{tlsEnable: tlsEnable}
ctx := context.TODO()
ns := "object-ns"
storeName := "sharedstore"
// securePort is the in-container RGW TLS listener port. The NodePort Service
// below exposes the conventional 443 externally and forwards to it.
const securePort int32 = 8443
certSecretName := storeName + "-tls"
rgwServiceName := "rook-ceph-rgw-" + storeName
realm := &cephv1.CephObjectRealm{
ObjectMeta: metav1.ObjectMeta{
Name: storeName,
Namespace: ns,
},
Spec: cephv1.ObjectRealmSpec{
DefaultRealm: true,
},
}
zoneGroup := &cephv1.CephObjectZoneGroup{
ObjectMeta: metav1.ObjectMeta{
Name: storeName,
Namespace: ns,
},
Spec: cephv1.ObjectZoneGroupSpec{
Realm: realm.Name,
},
}
zone := &cephv1.CephObjectZone{
ObjectMeta: metav1.ObjectMeta{
Name: storeName,
Namespace: ns,
},
Spec: cephv1.ObjectZoneSpec{
ZoneGroup: zoneGroup.Name,
SharedPools: cephv1.ObjectSharedPoolsSpec{
PoolPlacements: []cephv1.PoolPlacementSpec{
{
Name: "default",
Default: true,
MetadataPoolName: storeName + ".rgw.buckets.index",
DataPoolName: storeName + ".rgw.buckets.data",
StorageClasses: []cephv1.PlacementStorageClassSpec{
{
Name: "FOO",
DataPoolName: storeName + ".rgw.buckets.data.foo",
},
},
},
},
},
},
}
poolNames := map[string]string{
"rgw.root": ".rgw.root",
storeName + ".rgw.control": "",
storeName + ".rgw.meta": "",
storeName + ".rgw.log": "",
storeName + ".rgw.otp": "",
storeName + ".rgw.buckets.index": "",
storeName + ".rgw.buckets.data": "",
storeName + ".rgw.buckets.data.foo": "",
}
objectStore := &cephv1.CephObjectStore{
ObjectMeta: metav1.ObjectMeta{
Name: "test-shared",
Namespace: "object-ns",
Name: storeName,
Namespace: ns,
},
Spec: cephv1.ObjectStoreSpec{
MetadataPool: cephv1.PoolSpec{
Replicated: cephv1.ReplicatedSpec{
Size: 1,
RequireSafeReplicaSize: false,
},
},
DataPool: cephv1.PoolSpec{
Replicated: cephv1.ReplicatedSpec{
Size: 1,
RequireSafeReplicaSize: false,
},
Zone: cephv1.ZoneSpec{
Name: zone.Name,
},
Gateway: cephv1.GatewaySpec{
Port: 80,
Instances: 1,
},
// Allow CephObjectStoreUser resources from every namespace used by
// the sub-package tests under tests/integration/object.
AllowUsersInNamespaces: []string{
"test-bucketowner",
"test-topickafka",
"test-usercaps",
"test-userkeys",
"test-useropmask",
},
AllowUsersInNamespaces: allowedNamespaces,
},
}
if tlsEnable {
// TLS-only store: omit the plain Port and serve https on securePort,
// referencing the cert secret generated below.
objectStore.Spec.Gateway = cephv1.GatewaySpec{
SecurePort: securePort,
SSLCertificateRef: certSecretName,
Instances: 1,
}
}
svc := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
// The service name must match ObjectStoreName so that
@@ -101,30 +196,142 @@ func Setup(t *testing.T, k8sh *utils.K8sHelper) (*cephv1.CephObjectStore, func()
},
}
_, err := k8sh.RookClientset.CephV1().CephObjectStores(objectStore.Namespace).Create(ctx, objectStore, metav1.CreateOptions{})
require.NoError(t, err)
osReady := utils.Retry(180, time.Second, "shared CephObjectStore is Ready", func() bool {
liveOs, err := k8sh.RookClientset.CephV1().CephObjectStores(objectStore.Namespace).Get(ctx, objectStore.Name, metav1.GetOptions{})
if err != nil {
return false
}
if liveOs.Status == nil {
return false
}
return liveOs.Status.Phase == cephv1.ConditionReady
})
require.True(t, osReady, "shared CephObjectStore did not become Ready")
_, err = k8sh.Clientset.CoreV1().Services(objectStore.Namespace).Create(ctx, svc, metav1.CreateOptions{})
require.NoError(t, err)
return objectStore, func() {
if err := k8sh.Clientset.CoreV1().Services(objectStore.Namespace).Delete(ctx, objectStore.Name, metav1.DeleteOptions{}); err != nil {
t.Logf("failed to delete shared object store service: %v", err)
}
if err := k8sh.RookClientset.CephV1().CephObjectStores(objectStore.Namespace).Delete(ctx, objectStore.Name, metav1.DeleteOptions{}); err != nil {
t.Logf("failed to delete shared CephObjectStore: %v", err)
if tlsEnable {
// Expose the conventional https port externally, forwarding to the RGW
// TLS container port.
svc.Spec.Ports = []corev1.ServicePort{
{
Name: "https",
Port: 443,
Protocol: corev1.ProtocolTCP,
TargetPort: intstr.FromInt(int(securePort)),
},
}
}
canaryUser := &cephv1.CephObjectStoreUser{
ObjectMeta: metav1.ObjectMeta{
Name: storeName + "-canary",
Namespace: ns,
},
Spec: cephv1.ObjectStoreUserSpec{
Store: objectStore.Name,
ClusterNamespace: objectStore.Namespace,
},
}
var pools []*cephv1.CephBlockPool
for k, v := range poolNames {
// .rgw.root is the cluster-global realm metadata pool shared by every
// object store. The operator sets pg_num_min=8 on it when reconciling a
// normal CephObjectStore (e.g. the COSI test store), so it must have
// pg_num>=8 or that reconcile fails with "pg_num_min 8 > pg_num 1". The
// store-private pools are not shared and can stay at the minimal pg_num.
pgNum := "1"
if v == ".rgw.root" {
pgNum = "8"
}
pools = append(pools, &cephv1.CephBlockPool{
ObjectMeta: metav1.ObjectMeta{
Name: k,
Namespace: ns,
},
Spec: cephv1.NamedBlockPoolSpec{
Name: v,
PoolSpec: cephv1.PoolSpec{
Replicated: cephv1.ReplicatedSpec{
Size: 1,
RequireSafeReplicaSize: false,
},
Parameters: map[string]string{
"pg_autoscale_mode": "off",
"pg_num": pgNum,
},
},
},
})
}
ceph := k8sh.RookClientset.CephV1()
{
_, err := ceph.CephObjectRealms(ns).Create(ctx, realm, metav1.CreateOptions{})
require.NoError(t, err)
}
{
_, err := ceph.CephObjectZoneGroups(ns).Create(ctx, zoneGroup, metav1.CreateOptions{})
require.NoError(t, err)
}
{
_, err := ceph.CephObjectZones(ns).Create(ctx, zone, metav1.CreateOptions{})
require.NoError(t, err)
}
for _, p := range pools {
_, err := ceph.CephBlockPools(ns).Create(ctx, p, metav1.CreateOptions{})
require.NoError(t, err)
}
// the cert secret must exist before the store is created so the operator can
// mount it when the rgw pod first reconciles
if tlsEnable {
client.GenerateRgwTLSCertSecret(t, k8sh, ns, certSecretName, rgwServiceName)
}
wait4.RequireCreate(ctx, t, ceph.CephObjectStores(ns), objectStore, wait4.ObjectStore,
3*time.Minute, "shared CephObjectStore did not become Ready")
{
_, err := k8sh.Clientset.CoreV1().Services(ns).Create(ctx, svc, metav1.CreateOptions{})
require.NoError(t, err)
}
t.Run("setup rgw admin api client", func(t *testing.T) {
// user creation may be slow right after rgw start up
wait4.RequireCreate(ctx, t, ceph.CephObjectStoreUsers(ns), canaryUser, wait4.ObjectStoreUser, 2*time.Minute)
// cleanup the canary user immediately so it does not block the
// CephObjectStore's deletion later
wait4.AssertDelete(ctx, t, ceph.CephObjectStoreUsers(ns), canaryUser.Name, time.Minute)
// the canary user becoming ready tells us the rgw admin api is ready
var err error
s.adminClient, err = client.NewAdminClient(objectStore, installer, k8sh, tlsEnable)
require.NoError(t, err)
s.snsClient, err = client.NewSNSClient(objectStore, k8sh, installer, tlsEnable)
require.NoError(t, err)
})
s.objectStore = objectStore
s.destroy = func() {
t.Run(fmt.Sprintf("destroy CephObjectStore %s", objectStore.Name), func(t *testing.T) {
wait4.AssertDelete(ctx, t, k8sh.Clientset.CoreV1().Services(ns), svc.Name, 30*time.Second)
wait4.AssertDelete(ctx, t, ceph.CephObjectStores(ns), objectStore.Name, 5*time.Minute)
// The multisite CRs must be deleted before the pools. Their deletion
// finalizers run radosgw-admin, which reads the realm/zonegroup/zone
// metadata stored in the .rgw.root pool. Deleting the pools first
// removes that metadata, causing radosgw-admin to fail (exit status 2)
// and the zone controller to loop forever, so the CephObjectZone CR
// never finishes deleting.
wait4.AssertDelete(ctx, t, ceph.CephObjectZones(ns), zone.Name, 2*time.Minute)
wait4.AssertDelete(ctx, t, ceph.CephObjectZoneGroups(ns), zoneGroup.Name, time.Minute)
wait4.AssertDelete(ctx, t, ceph.CephObjectRealms(ns), realm.Name, time.Minute)
for _, p := range pools {
wait4.AssertDelete(ctx, t, ceph.CephBlockPools(ns), p.Name, time.Minute)
}
if tlsEnable {
wait4.AssertDelete(ctx, t, k8sh.Clientset.CoreV1().Secrets(ns), certSecretName, 30*time.Second)
}
})
}
return s
}
@@ -0,0 +1,71 @@
/*
Copyright 2026 The Rook Authors. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package wait4
import (
bktv1alpha1 "github.com/kube-object-storage/lib-bucket-provisioner/pkg/apis/objectbucket.io/v1alpha1"
cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1"
)
// ObjectStore reports whether a CephObjectStore has been reconciled to Ready.
func ObjectStore(os *cephv1.CephObjectStore) bool {
return os.Status != nil && os.Status.Phase == cephv1.ConditionReady
}
// ObjectStoreUser reports whether a CephObjectStoreUser has been reconciled to
// Ready.
func ObjectStoreUser(u *cephv1.CephObjectStoreUser) bool {
return ObjectStoreUserPhase(string(cephv1.ConditionReady))(u)
}
// ObjectStoreUserPhase returns a predicate reporting whether a
// CephObjectStoreUser's .status.phase matches phase. The parameter is a plain
// string because the cephv1 phase constants are a mix of ConditionType and
// ConditionReason (e.g. string(cephv1.ReconcileFailed)).
func ObjectStoreUserPhase(phase string) func(*cephv1.CephObjectStoreUser) bool {
return func(u *cephv1.CephObjectStoreUser) bool {
return u.Status != nil && u.Status.Phase == phase
}
}
// BucketTopic reports whether a CephBucketTopic has been reconciled to Ready
// with its ARN populated. Note that .status.ARN is nil while .status.phase ==
// Reconciling, so the phase check alone is not sufficient.
func BucketTopic(bt *cephv1.CephBucketTopic) bool {
return BucketTopicPhase(string(cephv1.ConditionReady))(bt) && bt.Status.ARN != nil
}
// BucketTopicPhase returns a predicate reporting whether a CephBucketTopic's
// .status.phase matches phase. The parameter is a plain string because the
// cephv1 phase constants are a mix of ConditionType and ConditionReason (e.g.
// string(cephv1.ReconcileFailed)).
func BucketTopicPhase(phase string) func(*cephv1.CephBucketTopic) bool {
return func(bt *cephv1.CephBucketTopic) bool {
return bt.Status != nil && bt.Status.Phase == phase
}
}
// OBCBound reports whether an ObjectBucketClaim has reached the Bound phase.
func OBCBound(obc *bktv1alpha1.ObjectBucketClaim) bool {
return obc.Status.Phase == bktv1alpha1.ObjectBucketClaimStatusPhaseBound
}
// OBBound reports whether an ObjectBucket has reached the Bound phase.
func OBBound(ob *bktv1alpha1.ObjectBucket) bool {
return ob.Status.Phase == bktv1alpha1.ObjectBucketStatusPhaseBound
}
@@ -0,0 +1,488 @@
/*
Copyright 2026 The Rook Authors. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package wait4
import (
"bufio"
"context"
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/tools/cache"
watchtools "k8s.io/client-go/tools/watch"
"github.com/rook/rook/tests/framework/utils"
)
// Default timeouts for waiting on operator reconciliation in the object
// tests. Bespoke waits (e.g. the shared store's multi-minute teardown ladder)
// still pass explicit durations.
const (
// TimeoutShort covers routine status changes on an already-running
// operator and rgw: condition waits, deletions, field updates.
TimeoutShort = 40 * time.Second
// TimeoutMedium covers resource creation that needs a full reconcile,
// such as a CephBucketTopic becoming Ready with its ARN set.
TimeoutMedium = time.Minute
// TimeoutLong covers first-reconcile waits that may race rgw startup,
// such as CephObjectStoreUser creation right after the store comes up.
TimeoutLong = 2 * time.Minute
)
// NamespacedWatcher is satisfied by any typed namespaced client (corev1
// ServiceInterface, cephv1 CephObjectStoreUserInterface, etc.). T is the
// resource's pointer type and L its list type, both inferred at the call site
// from the client's Get/List return types.
type NamespacedWatcher[T, L runtime.Object] interface {
Get(ctx context.Context, name string, opts metav1.GetOptions) (T, error)
List(ctx context.Context, opts metav1.ListOptions) (L, error)
Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error)
}
// nameScopedListWatch returns a ListWatch restricted to a single resource name.
func nameScopedListWatch[T, L runtime.Object](ctx context.Context, client NamespacedWatcher[T, L], name string) *cache.ListWatch {
fs := fields.OneTermEqualSelector("metadata.name", name).String()
return &cache.ListWatch{
ListFunc: func(opts metav1.ListOptions) (runtime.Object, error) {
opts.FieldSelector = fs
return client.List(ctx, opts)
},
WatchFunc: func(opts metav1.ListOptions) (watch.Interface, error) {
opts.FieldSelector = fs
return client.Watch(ctx, opts)
},
}
}
// heartbeatInterval is how often a watch-based wait logs progress while it is
// still blocked.
const heartbeatInterval = 15 * time.Second
// heartbeat logs a periodic progress line for a watch-based wait that would
// otherwise block silently, so a slow reconcile shows up in CI output instead
// of looking like a hang. It logs nothing for waits shorter than
// heartbeatInterval; the caller must invoke the returned stop func when the
// wait returns.
func heartbeat(t *testing.T, desc string) (stop func()) {
t.Helper()
done := make(chan struct{})
go func() {
ticker := time.NewTicker(heartbeatInterval)
defer ticker.Stop()
for elapsed := heartbeatInterval; ; elapsed += heartbeatInterval {
select {
case <-done:
return
case <-ticker.C:
t.Logf("still waiting for %s (%s)", desc, elapsed)
}
}
}()
return func() { close(done) }
}
// waitForAbsent blocks until the named resource is gone or the timeout elapses.
//
// The wait is race-free against fast deletions: UntilWithSync performs an
// initial List (scoped to the resource name via a field selector) before
// starting the watch, and the precondition treats an empty store as "already
// gone". Subsequent deletion events are guaranteed to be delivered because the
// watch resumes from the List's resourceVersion.
func waitForAbsent[T, L runtime.Object](ctx context.Context, t *testing.T, client NamespacedWatcher[T, L], name string, timeout time.Duration) error {
var objType T
lw := nameScopedListWatch(ctx, client, name)
waitCtx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
precondition := func(store cache.Store) (bool, error) {
// lw is name-scoped, so an empty store unambiguously means the object
// was already gone by the time the reflector's initial list completed.
return len(store.List()) == 0, nil
}
condition := func(ev watch.Event) (bool, error) {
return ev.Type == watch.Deleted, nil
}
defer heartbeat(t, fmt.Sprintf("%T %q to be removed", objType, name))()
if _, err := watchtools.UntilWithSync(waitCtx, lw, objType, precondition, condition); err != nil {
return fmt.Errorf("%T %q is still present: %w", objType, name, err)
}
return nil
}
// waitForCondition blocks until the named resource satisfies cond or the timeout
// elapses, returning the matching object. The initial List makes it race-free in
// the same way as waitForAbsent: a resource that already satisfies cond is
// detected immediately, and subsequent updates arrive as watch events.
func waitForCondition[T, L runtime.Object](ctx context.Context, t *testing.T, client NamespacedWatcher[T, L], name string, cond func(T) bool, timeout time.Duration) (T, error) {
var objType T
var match T
lw := nameScopedListWatch(ctx, client, name)
waitCtx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
precondition := func(store cache.Store) (bool, error) {
for _, o := range store.List() {
if typed, ok := o.(T); ok && cond(typed) {
match = typed
return true, nil
}
}
return false, nil
}
condition := func(ev watch.Event) (bool, error) {
switch ev.Type {
case watch.Added, watch.Modified:
if typed, ok := ev.Object.(T); ok && cond(typed) {
match = typed
return true, nil
}
case watch.Deleted:
return false, fmt.Errorf("%T %q was deleted while waiting for condition", objType, name)
}
return false, nil
}
defer heartbeat(t, fmt.Sprintf("%T %q to be ready", objType, name))()
if _, err := watchtools.UntilWithSync(waitCtx, lw, objType, precondition, condition); err != nil {
return match, fmt.Errorf("%T %q did not satisfy condition: %w", objType, name, err)
}
return match, nil
}
// NamespacedDeleter is a NamespacedWatcher that can also delete by name.
type NamespacedDeleter[T, L runtime.Object] interface {
NamespacedWatcher[T, L]
Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error
}
func deleteAndWait[T, L runtime.Object](ctx context.Context, t *testing.T, client NamespacedDeleter[T, L], name string, timeout time.Duration) error {
var objType T
if err := client.Delete(ctx, name, metav1.DeleteOptions{}); err != nil {
return fmt.Errorf("failed to delete %T %q: %w", objType, name, err)
}
return waitForAbsent[T, L](ctx, t, client, name, timeout)
}
// AssertDelete deletes a single named resource and blocks until it is gone,
// reporting failures (including timeout) via assert.Fail (non-fatal). It returns
// true on success. Prefer this for teardown/cleanup, so a stuck finalizer on one
// resource does not prevent cleanup of the rest; use RequireDelete when later
// steps depend on the deletion having completed.
func AssertDelete[T, L runtime.Object](
ctx context.Context,
t *testing.T,
client NamespacedDeleter[T, L],
name string,
timeout time.Duration,
msgAndArgs ...interface{},
) bool {
t.Helper()
if err := deleteAndWait(ctx, t, client, name, timeout); err != nil {
return assert.Fail(t, err.Error(), msgAndArgs...)
}
return true
}
// RequireDelete is the fatal counterpart of AssertDelete: it stops the test
// (FailNow) if the resource cannot be deleted or is not gone before the timeout.
func RequireDelete[T, L runtime.Object](
ctx context.Context,
t *testing.T,
client NamespacedDeleter[T, L],
name string,
timeout time.Duration,
msgAndArgs ...interface{},
) {
t.Helper()
require.NoError(t, deleteAndWait(ctx, t, client, name, timeout), msgAndArgs...)
}
// AssertAbsent waits for a named resource to disappear without deleting it
// itself, reporting failures via assert.Fail (non-fatal). Use it for resources
// removed as a side effect of another action — e.g. an ObjectBucket that is
// garbage-collected when its ObjectBucketClaim is deleted. To delete and wait,
// use AssertDelete/RequireDelete.
func AssertAbsent[T, L runtime.Object](
ctx context.Context,
t *testing.T,
client NamespacedWatcher[T, L],
name string,
timeout time.Duration,
msgAndArgs ...interface{},
) bool {
t.Helper()
if err := waitForAbsent(ctx, t, client, name, timeout); err != nil {
return assert.Fail(t, err.Error(), msgAndArgs...)
}
return true
}
// RequireAbsent is the fatal counterpart of AssertAbsent.
func RequireAbsent[T, L runtime.Object](
ctx context.Context,
t *testing.T,
client NamespacedWatcher[T, L],
name string,
timeout time.Duration,
msgAndArgs ...interface{},
) {
t.Helper()
require.NoError(t, waitForAbsent(ctx, t, client, name, timeout), msgAndArgs...)
}
// NamespacedReadyWaiter is a NamespacedWatcher that can also create.
type NamespacedReadyWaiter[T, L runtime.Object] interface {
NamespacedWatcher[T, L]
Create(ctx context.Context, obj T, opts metav1.CreateOptions) (T, error)
}
// createAndWaitReady creates obj and, when ready is non-nil, blocks until ready
// reports true for the live object or the timeout elapses, returning the live
// object. Pass a nil ready predicate for resources that are usable as soon as
// they are created (Namespaces, Services, Secrets, ...).
//
// Unlike deletion, "ready" has no universal representation across resource
// types, so the readiness check is supplied by the caller.
func createAndWaitReady[T, L runtime.Object](
ctx context.Context,
t *testing.T,
client NamespacedReadyWaiter[T, L],
obj T,
ready func(T) bool,
timeout time.Duration,
) (T, error) {
var zero T
created, err := client.Create(ctx, obj, metav1.CreateOptions{})
if err != nil {
return zero, fmt.Errorf("failed to create %T: %w", obj, err)
}
if ready == nil {
return created, nil
}
accessor, err := meta.Accessor(created)
if err != nil {
return zero, fmt.Errorf("failed to read metadata of %T: %w", created, err)
}
return waitForCondition[T, L](ctx, t, client, accessor.GetName(), ready, timeout)
}
// AssertCreate creates obj and waits for the ready predicate, reporting failures
// (including timeout) via assert.Fail (non-fatal) like AssertDelete. It returns
// the live object and true on success. Prefer this for cleanup/best-effort
// creation where the test should continue on failure; for setup preconditions
// use RequireCreate.
func AssertCreate[T, L runtime.Object](
ctx context.Context,
t *testing.T,
client NamespacedReadyWaiter[T, L],
obj T,
ready func(T) bool,
timeout time.Duration,
msgAndArgs ...interface{},
) (T, bool) {
t.Helper()
live, err := createAndWaitReady(ctx, t, client, obj, ready, timeout)
if err != nil {
var zero T
assert.Fail(t, err.Error(), msgAndArgs...)
return zero, false
}
return live, true
}
// RequireCreate is the fatal counterpart of AssertCreate: it stops the test
// (FailNow) if the resource cannot be created or does not become ready, and
// returns the live object. This is the common case for setup preconditions, so
// the call site needs no follow-up require.True.
func RequireCreate[T, L runtime.Object](
ctx context.Context,
t *testing.T,
client NamespacedReadyWaiter[T, L],
obj T,
ready func(T) bool,
timeout time.Duration,
msgAndArgs ...interface{},
) T {
t.Helper()
live, err := createAndWaitReady(ctx, t, client, obj, ready, timeout)
require.NoError(t, err, msgAndArgs...)
return live
}
// AssertCondition waits for the named resource to satisfy cond, reporting
// failures (including timeout) via assert.Fail (non-fatal). It returns the
// matching object and true on success. Use it to wait on an already-created
// resource — e.g. an OBC reaching Bound, or a status field changing after an
// update; for the create-and-wait case use AssertCreate/RequireCreate.
func AssertCondition[T, L runtime.Object](
ctx context.Context,
t *testing.T,
client NamespacedWatcher[T, L],
name string,
cond func(T) bool,
timeout time.Duration,
msgAndArgs ...interface{},
) (T, bool) {
t.Helper()
match, err := waitForCondition(ctx, t, client, name, cond, timeout)
if err != nil {
var zero T
assert.Fail(t, err.Error(), msgAndArgs...)
return zero, false
}
return match, true
}
// RequireCondition is the fatal counterpart of AssertCondition.
func RequireCondition[T, L runtime.Object](
ctx context.Context,
t *testing.T,
client NamespacedWatcher[T, L],
name string,
cond func(T) bool,
timeout time.Duration,
msgAndArgs ...interface{},
) T {
t.Helper()
match, err := waitForCondition(ctx, t, client, name, cond, timeout)
require.NoError(t, err, msgAndArgs...)
return match
}
// AssertEventually polls cond until it returns nil or the timeout elapses,
// reporting failure via assert.Fail (non-fatal). It is for state the
// Kubernetes API cannot watch (the rgw admin API, S3, SNS); k8s resources
// should use the watch-based helpers above. The poll loop is utils.Eventually;
// this adds the assert-flavored, non-fatal reporting that pairs with the
// AssertCreate/AssertDelete/AssertCondition family.
//
// cond must not call assert or require: report transient failures by returning
// an error (which is surfaced in the timeout message), and make assertions on
// captured state after the wait. cond receives a context bounded by the wait
// deadline; thread it into the reads it performs.
func AssertEventually(ctx context.Context, t *testing.T, timeout time.Duration, desc string, cond func(context.Context) error) bool {
t.Helper()
if err := utils.Eventually(ctx, t, timeout, desc, cond); err != nil {
return assert.Fail(t, err.Error())
}
return true
}
// RequireEventually is the fatal counterpart of AssertEventually: it stops
// the test (FailNow) if cond does not succeed before the timeout. Use it
// when subsequent statements consume the polled result.
func RequireEventually(ctx context.Context, t *testing.T, timeout time.Duration, desc string, cond func(context.Context) error) {
t.Helper()
require.NoError(t, utils.Eventually(ctx, t, timeout, desc, cond))
}
// matchPodLog follows the logs of the first pod matching selector until match
// returns true for a line, the stream ends, or the wait deadline passes.
// Reaching the end of the stream without a match is an error: following the
// stream (rather than scanning a snapshot) is what lets the wait distinguish
// "absent" from "not logged yet".
func matchPodLog(ctx context.Context, t *testing.T, k8sh *utils.K8sHelper, namespace string, selector labels.Selector, timeout time.Duration, match func(line string) bool) error {
t.Helper()
waitCtx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
pods, err := k8sh.Clientset.CoreV1().Pods(namespace).List(waitCtx, metav1.ListOptions{LabelSelector: selector.String()})
if err != nil {
return fmt.Errorf("failed to list pods: %w", err)
}
if len(pods.Items) == 0 {
return fmt.Errorf("no pods found with labels %v in namespace %q", selector, namespace)
}
// if there are multiple pods, just pick the first one
pod := pods.Items[0]
t.Logf("matching against logs of pod %q (first match for labels %v)", pod.Name, selector)
req := k8sh.Clientset.CoreV1().Pods(namespace).GetLogs(pod.Name, &corev1.PodLogOptions{Follow: true})
logStream, err := req.Stream(waitCtx)
if err != nil {
return fmt.Errorf("failed to stream logs from pod %q: %w", pod.Name, err)
}
defer logStream.Close()
scanner := bufio.NewScanner(logStream)
for scanner.Scan() {
if match(scanner.Text()) {
return nil
}
}
if err := scanner.Err(); err != nil {
return fmt.Errorf("no matching log line from pod %q: %w", pod.Name, err)
}
return fmt.Errorf("log stream from pod %q ended without a matching line", pod.Name)
}
// AssertPodLog follows the logs of the first pod matching selector and reports
// failure via assert.Fail (non-fatal) if no line satisfies match before the
// timeout. It is for log-based state the Kubernetes API cannot watch (e.g. an
// operator logging a reconcile error); for a setup precondition use
// RequirePodLog.
func AssertPodLog(
ctx context.Context,
t *testing.T,
k8sh *utils.K8sHelper,
namespace string,
selector labels.Selector,
timeout time.Duration,
match func(line string) bool,
msgAndArgs ...interface{},
) bool {
t.Helper()
if err := matchPodLog(ctx, t, k8sh, namespace, selector, timeout, match); err != nil {
return assert.Fail(t, err.Error(), msgAndArgs...)
}
return true
}
// RequirePodLog is the fatal counterpart of AssertPodLog.
func RequirePodLog(
ctx context.Context,
t *testing.T,
k8sh *utils.K8sHelper,
namespace string,
selector labels.Selector,
timeout time.Duration,
match func(line string) bool,
msgAndArgs ...interface{},
) {
t.Helper()
require.NoError(t, matchPodLog(ctx, t, k8sh, namespace, selector, timeout, match), msgAndArgs...)
}