diff --git a/docs/api-status.json b/docs/api-status.json index 4ee6641..6a12eb8 100644 --- a/docs/api-status.json +++ b/docs/api-status.json @@ -1764,7 +1764,20 @@ ] }, "rgw/admin": { - "preview_api": [], + "preview_api": [ + { + "name": "API.UnlinkBucket", + "comment": "UnlinkBucket unlink a bucket from a specified user\nPrimarily useful for changing bucket ownership.\n PREVIEW\n", + "added_in_version": "v0.15.0", + "expected_stable_version": "v0.17.0" + }, + { + "name": "API.LinkBucket", + "comment": "LinkBucket will link a bucket to a specified user\nunlinking the bucket from any previous user\n PREVIEW\n", + "added_in_version": "v0.15.0", + "expected_stable_version": "v0.17.0" + } + ], "stable_api": [ { "name": "API.ListBuckets", diff --git a/docs/api-status.md b/docs/api-status.md index 63b9873..c72ceb6 100644 --- a/docs/api-status.md +++ b/docs/api-status.md @@ -57,6 +57,8 @@ Snapshot.Set | v0.10.0 | | Name | Added in Version | Expected Stable Version | ---- | ---------------- | ----------------------- | +API.UnlinkBucket | v0.15.0 | v0.17.0 | +API.LinkBucket | v0.15.0 | v0.17.0 | ## Package: common/admin/manager diff --git a/rgw/admin/errors.go b/rgw/admin/errors.go index c0b41ed..ce9161a 100644 --- a/rgw/admin/errors.go +++ b/rgw/admin/errors.go @@ -90,6 +90,8 @@ var ( errMissingUserAccessKey = errors.New("missing user access key") errMissingUserDisplayName = errors.New("missing user display name") errMissingUserCap = errors.New("missing user capabilities") + errMissingBucketID = errors.New("missing bucket ID") + errMissingBucket = errors.New("missing bucket") ) // errorReason is the reason of the error diff --git a/rgw/admin/link.go b/rgw/admin/link.go new file mode 100644 index 0000000..c88f14e --- /dev/null +++ b/rgw/admin/link.go @@ -0,0 +1,44 @@ +//go:build ceph_preview +// +build ceph_preview + +package admin + +import ( + "context" + "net/http" +) + +// BucketLinkInput the bucket link/unlink input parameters +type BucketLinkInput struct { + Bucket string `url:"bucket" json:"bucket"` + BucketID string `url:"bucket-id" json:"bucket_id"` + UID string `url:"uid" json:"uid"` +} + +// UnlinkBucket unlink a bucket from a specified user +// Primarily useful for changing bucket ownership. +// PREVIEW +func (api *API) UnlinkBucket(ctx context.Context, link BucketLinkInput) error { + if link.UID == "" { + return errMissingUserID + } + if link.Bucket == "" { + return errMissingBucket + } + _, err := api.call(ctx, http.MethodPost, "/bucket", valueToURLParams(link)) + return err +} + +// LinkBucket will link a bucket to a specified user +// unlinking the bucket from any previous user +// PREVIEW +func (api *API) LinkBucket(ctx context.Context, link BucketLinkInput) error { + if link.UID == "" { + return errMissingUserID + } + if link.Bucket == "" { + return errMissingBucket + } + _, err := api.call(ctx, http.MethodPut, "/bucket", valueToURLParams(link)) + return err +} diff --git a/rgw/admin/link_test.go b/rgw/admin/link_test.go new file mode 100644 index 0000000..fec4967 --- /dev/null +++ b/rgw/admin/link_test.go @@ -0,0 +1,82 @@ +//go:build ceph_preview +// +build ceph_preview + +package admin + +import ( + "context" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +func (suite *RadosGWTestSuite) TestLink() { + suite.SetupConnection() + co, err := New(suite.endpoint, suite.accessKey, suite.secretKey, newDebugHTTPClient(http.DefaultClient)) + assert.NoError(suite.T(), err) + + suite.T().Run("create test user1", func(t *testing.T) { + user, err := co.CreateUser(context.Background(), User{ID: "test-user1", DisplayName: "test-user1", Email: "test1@example.com"}) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), "test-user1", user.ID) + assert.Zero(suite.T(), len(user.Caps)) + }) + + suite.T().Run("create test bucket", func(t *testing.T) { + s3, err := newS3Agent(suite.accessKey, suite.secretKey, suite.endpoint, true) + assert.NoError(t, err) + + err = s3.createBucket(suite.bucketTestName) + assert.NoError(t, err) + }) + + suite.T().Run("create test user2", func(t *testing.T) { + user, err := co.CreateUser(context.Background(), User{ID: "test-user2", DisplayName: "test-user2", Email: "test2@example.com"}) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), "test-user2", user.ID) + assert.Zero(suite.T(), len(user.Caps)) + }) + + suite.T().Run("link test-user2", func(t *testing.T) { + bucket, err := co.GetBucketInfo(context.Background(), Bucket{Bucket: suite.bucketTestName}) + assert.NoError(t, err) + + err = co.LinkBucket(context.Background(), BucketLinkInput{ + Bucket: suite.bucketTestName, + BucketID: bucket.ID, + UID: "test-user2", + }) + assert.NoError(t, err) + + bucket, err = co.GetBucketInfo(context.Background(), Bucket{Bucket: suite.bucketTestName}) + assert.NoError(t, err) + assert.Equal(t, bucket.Owner, "test-user2") + }) + + suite.T().Run("unlink test-user2", func(t *testing.T) { + bucket, err := co.GetBucketInfo(context.Background(), Bucket{Bucket: suite.bucketTestName}) + assert.NoError(t, err) + + err = co.UnlinkBucket(context.Background(), BucketLinkInput{ + Bucket: suite.bucketTestName, + UID: bucket.Owner, + }) + assert.NoError(t, err) + }) + + suite.T().Run("remove bucket", func(t *testing.T) { + err := co.RemoveBucket(context.Background(), Bucket{Bucket: suite.bucketTestName}) + assert.NoError(suite.T(), err) + }) + + suite.T().Run("delete test user1", func(t *testing.T) { + err := co.RemoveUser(context.Background(), User{ID: "test-user1"}) + assert.NoError(suite.T(), err) + }) + + suite.T().Run("delete test user2", func(t *testing.T) { + err := co.RemoveUser(context.Background(), User{ID: "test-user2"}) + assert.NoError(suite.T(), err) + }) +}