Files
Joshua Hoblitt 6bf24ac507 test: extract the zone.json pool canary into its own package
The canary that checks every RGW zone.json *_pool field is covered by
Rook's zonePoolNSSuffix map was inline in runObjectE2ETest, running
against the legacy per-pass store before the shared store existed.

Move it into tests/integration/object/zonepools as a standalone
shared-store consumer. It now validates the shared store's zone, which
carries the real shared-pool placements the canary is meant to guard,
and runs first among the shared-store packages so it still sees a fresh
zone.

Add a Sharedstore.Installer() accessor so the packaged canary can run
radosgw-admin inside the cluster while keeping the uniform
(t, k8sh, store) package entry signature.

Signed-off-by: Joshua Hoblitt <josh@hoblitt.com>
2026-07-22 16:04:42 -07:00
..

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); 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/lifecycle object/bucket OBC bucketLifecycle management
bucket/owner object/bucket OBC bucketOwner handling
bucket/policy object/bucket OBC bucketPolicy management
bucket/quota object/bucket OBC maxObjects user quota + bucketMaxObjects/bucketMaxSize bucket quota
bucket/rw object/bucket OBC S3 read/write/delete + OBC-stays-Bound
cosi object/cosi CephCOSIDriver + COSI bucket provisioning
notification object/notification CephBucketNotification HTTP endpoint delivery
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
zonepools object zone.json pool fields covered by Rook's shared-pool mapping
reserved: tests/integration/object/lifecycle, tests/integration/object/dependents 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, the readiness predicates (ObjectStoreUser, BucketTopic, OBCBound, ...), and the shared timeout tiers.
  • fixture — create-with-t.Cleanup helpers for pure-cleanup resources (namespaces, StorageClasses).
  • obc — ObjectBucketClaim helpers: the provisioner StorageClass constructor, the create/bound and delete/absent lifecycle waiters, and a per-OBC S3 client.
  • secrets — verification helpers for the Secret references object CRDs publish in their status, shared by more than one package.
  • sharedstore — the shared CephObjectStore fixture.
  • client — rgw admin, SNS, and S3 client builders and TLS cert generation.

Anatomy of a package

One exported entry function per package, called from the dispatcher:

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, wait4.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 wait4 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 deletion-blocked-by-dependents tests/integration/object/dependents private-store fixture, store condition predicates in wait4
createCephObjectStore/runObjectE2ETestLite/deletion asserts tests/integration/object/lifecycle store create/health/delete helpers
upgrade-suite object usage stays in upgrade suite switch to typed clients + wait4