Files
my-rook-config/tests/integration/object/cosi/cosi.go
T
Joshua Hoblitt 3a4dc64f5b test: convert the COSI driver suite to the object test toolkit
Rework the COSI driver integration test onto the shared-store object test
toolkit: an ordered t.Run script with the (t, k8sh, store) signature, typed
clients, and watch-based waits, replacing kubectl-string manifests and
fixed-interval polling.

Drive the COSI bucket resources (BucketClass, BucketClaim, Bucket) with the
upstream sigs.k8s.io/container-object-storage-interface typed client, exposed
as k8sh.COSIClientset alongside the existing OBC client. Create the
CephCOSIDriver and its privileged user through typed clients, wait for the
driver Deployment with wait4, and verify the provisioned bucket through the
shared store's rgw admin client.

Install the COSI CRDs and central controller from the consolidated upstream
repo pinned to v0.2.2 (the former -api and -controller repos are retired) via
a kubectl -k fixture that is removed with t.Cleanup. The driver cannot trust
a TLS RGW endpoint, so the suite skips itself in the TLS pass rather than
being special-cased in the dispatcher.

Retire the now-unused COSIOperation client and the GetCOSIDriver,
GetBucketClass, and GetBucketClaim manifest helpers.

Signed-off-by: Joshua Hoblitt <josh@hoblitt.com>
2026-06-29 15:36:13 -07:00

242 lines
8.9 KiB
Go

/*
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 cosi
import (
"context"
"errors"
"fmt"
"testing"
"time"
"github.com/ceph/go-ceph/rgw/admin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
cosiv1 "sigs.k8s.io/container-object-storage-interface/client/apis/objectstorage/v1alpha1"
cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1"
cephcosi "github.com/rook/rook/pkg/operator/ceph/object/cosi"
"github.com/rook/rook/tests/framework/installer"
"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"
)
const Namespace = "test-cosi"
// cosiInstallKustomize installs the upstream COSI CRDs and central controller.
// It is pinned to the same release as the typed client in go.mod so the
// installed CRDs match the Go types this test builds. The repo root
// kustomization aggregates the CRDs (client/config/crd) and the controller.
const cosiInstallKustomize = "github.com/kubernetes-sigs/container-object-storage-interface?ref=v0.2.2"
func TestCephCOSIDriver(t *testing.T, k8sh *utils.K8sHelper, store *sharedstore.Sharedstore) {
var (
defaultName = Namespace
objectStore = store.ObjectStore()
adminClient = store.AdminClient()
operatorNamespace = installer.SystemNamespace(objectStore.Namespace)
ns = &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: defaultName,
},
}
// the COSI driver provisions buckets and users on behalf of bucket
// claims, so it needs a privileged object store user
osu = cephv1.CephObjectStoreUser{
ObjectMeta: metav1.ObjectMeta{
Name: defaultName + "-user",
Namespace: ns.Name,
},
Spec: cephv1.ObjectStoreUserSpec{
Store: objectStore.Name,
ClusterNamespace: objectStore.Namespace,
Capabilities: &cephv1.ObjectUserCapSpec{
User: "*",
Bucket: "*",
},
},
}
userSecretName = fmt.Sprintf("rook-ceph-object-user-%s-%s", objectStore.Name, osu.Name)
cephCOSIDriver = cephv1.CephCOSIDriver{
ObjectMeta: metav1.ObjectMeta{
Name: cephcosi.CephCOSIDriverName,
Namespace: operatorNamespace,
},
Spec: cephv1.CephCOSIDriverSpec{
DeploymentStrategy: cephv1.COSIDeploymentStrategyAuto,
},
}
bucketClass = cosiv1.BucketClass{
ObjectMeta: metav1.ObjectMeta{
Name: defaultName + "-bucketclass",
},
DriverName: cephcosi.CephCOSIDriverPrefix + ".ceph.objectstorage.k8s.io",
DeletionPolicy: cosiv1.DeletionPolicyDelete,
Parameters: map[string]string{
"objectStoreUserSecretName": userSecretName,
"objectStoreUserSecretNamespace": ns.Name,
},
}
bucketClaim = cosiv1.BucketClaim{
ObjectMeta: metav1.ObjectMeta{
Name: defaultName + "-bucketclaim",
Namespace: ns.Name,
},
Spec: cosiv1.BucketClaimSpec{
BucketClassName: bucketClass.Name,
Protocols: []cosiv1.Protocol{cosiv1.ProtocolS3},
},
}
osuClient = k8sh.RookClientset.CephV1().CephObjectStoreUsers(ns.Name)
driverClient = k8sh.RookClientset.CephV1().CephCOSIDrivers(operatorNamespace)
deployClient = k8sh.Clientset.AppsV1().Deployments(operatorNamespace)
bucketClassClient = k8sh.COSIClientset.ObjectstorageV1alpha1().BucketClasses()
bucketClaimClient = k8sh.COSIClientset.ObjectstorageV1alpha1().BucketClaims(ns.Name)
bucketClient = k8sh.COSIClientset.ObjectstorageV1alpha1().Buckets()
)
t.Run("COSI driver", func(t *testing.T) {
if store.TLSEnabled() {
// The ceph-cosi driver is deployed without any RGW CA trust (see
// pkg/operator/ceph/object/cosi/spec.go), so it cannot provision
// against a TLS-only object store endpoint.
t.Skip("ceph-cosi driver does not support a TLS object store endpoint")
}
ctx := t.Context()
fixture.RequireNamespace(t, k8sh, ns)
requireCOSIInstall(t, k8sh)
t.Run(fmt.Sprintf("create CephObjectStoreUser %q", osu.Name), func(t *testing.T) {
// user creation may be slow right after rgw start up
wait4.RequireCreate(ctx, t, osuClient, &osu, wait4.ObjectStoreUser, wait4.TimeoutLong)
})
t.Run(fmt.Sprintf("create CephCOSIDriver %q", cephCOSIDriver.Name), func(t *testing.T) {
_, err := driverClient.Create(ctx, &cephCOSIDriver, metav1.CreateOptions{})
require.NoError(t, err)
})
t.Run(fmt.Sprintf("ceph-cosi-driver Deployment %q becomes ready", cephCOSIDriver.Name), func(t *testing.T) {
// the driver pulls its container images on first reconcile, so allow
// more time than a routine reconcile wait
wait4.RequireCondition(ctx, t, deployClient, cephCOSIDriver.Name, deploymentReady, 3*time.Minute)
})
t.Run(fmt.Sprintf("create BucketClass %q", bucketClass.Name), func(t *testing.T) {
_, err := bucketClassClient.Create(ctx, &bucketClass, metav1.CreateOptions{})
require.NoError(t, err)
})
t.Run(fmt.Sprintf("create BucketClaim %q", bucketClaim.Name), func(t *testing.T) {
_, err := bucketClaimClient.Create(ctx, &bucketClaim, metav1.CreateOptions{})
require.NoError(t, err)
})
var backendBucket string
t.Run(fmt.Sprintf("BucketClaim %q becomes ready", bucketClaim.Name), func(t *testing.T) {
liveClaim := wait4.RequireCondition(ctx, t, bucketClaimClient, bucketClaim.Name,
func(bc *cosiv1.BucketClaim) bool {
return bc.Status.BucketReady && bc.Status.BucketName != ""
},
wait4.TimeoutLong)
backendBucket = liveClaim.Status.BucketName
})
t.Run("provisioned Bucket becomes ready", func(t *testing.T) {
require.NotEmpty(t, backendBucket, "BucketClaim did not report a bucket name")
wait4.RequireCondition(ctx, t, bucketClient, backendBucket,
func(b *cosiv1.Bucket) bool { return b.Status.BucketReady },
wait4.TimeoutLong)
})
t.Run("bucket exists in backend ceph", func(t *testing.T) {
require.NotEmpty(t, backendBucket, "BucketClaim did not report a bucket name")
info, err := adminClient.GetBucketInfo(ctx, admin.Bucket{Bucket: backendBucket})
require.NoError(t, err)
assert.Equal(t, backendBucket, info.Bucket)
})
t.Run(fmt.Sprintf("delete BucketClaim %q", bucketClaim.Name), func(t *testing.T) {
// the backing Bucket is garbage-collected by the COSI controller
wait4.AssertDelete(ctx, t, bucketClaimClient, bucketClaim.Name, wait4.TimeoutLong)
wait4.AssertAbsent(ctx, t, bucketClient, backendBucket, wait4.TimeoutShort)
})
t.Run("bucket removed from backend ceph", func(t *testing.T) {
// deletionPolicy Delete means deleting the claim deprovisions the bucket
wait4.AssertEventually(ctx, t, wait4.TimeoutShort, "backend bucket is deleted", func(ctx context.Context) error {
_, err := adminClient.GetBucketInfo(ctx, admin.Bucket{Bucket: backendBucket})
if err == nil {
return fmt.Errorf("bucket %q still exists in backend", backendBucket)
}
if !errors.Is(err, admin.ErrNoSuchBucket) {
return fmt.Errorf("checking backend bucket %q: %w", backendBucket, err)
}
return nil
})
})
t.Run(fmt.Sprintf("delete BucketClass %q", bucketClass.Name), func(t *testing.T) {
wait4.AssertDelete(ctx, t, bucketClassClient, bucketClass.Name, wait4.TimeoutShort)
})
t.Run(fmt.Sprintf("delete CephObjectStoreUser %q", osu.Name), func(t *testing.T) {
wait4.AssertDelete(ctx, t, osuClient, osu.Name, wait4.TimeoutShort)
})
t.Run(fmt.Sprintf("delete CephCOSIDriver %q", cephCOSIDriver.Name), func(t *testing.T) {
wait4.AssertDelete(ctx, t, driverClient, cephCOSIDriver.Name, wait4.TimeoutShort)
})
})
}
// requireCOSIInstall installs the upstream COSI CRDs and central controller and
// registers their removal via t.Cleanup. rook only deploys the ceph-cosi driver
// (from the CephCOSIDriver CR); the CRDs and the central controller that
// reconciles BucketClaims into Buckets come from upstream.
func requireCOSIInstall(t *testing.T, k8sh *utils.K8sHelper) {
t.Helper()
_, err := k8sh.Kubectl("create", "-k", cosiInstallKustomize)
require.NoError(t, err, "failed to install COSI CRDs and controller")
t.Cleanup(func() {
if _, err := k8sh.Kubectl("delete", "-k", cosiInstallKustomize, "--ignore-not-found=true"); err != nil {
t.Errorf("failed to uninstall COSI CRDs and controller: %v", err)
}
})
}
// deploymentReady reports whether a Deployment has at least one ready replica.
func deploymentReady(d *appsv1.Deployment) bool {
return d.Status.ReadyReplicas >= 1
}