Merge pull request #2293 from lhjnano/fix/getbucketversioning-empty-config

fix(posix): return empty config for GetBucketVersioning when unconfigured
This commit is contained in:
Ben McClelland
2026-08-26 21:22:24 -07:00
committed by GitHub
4 changed files with 139 additions and 11 deletions
+9 -4
View File
@@ -890,10 +890,6 @@ func (p *Posix) GetBucketVersioning(ctx context.Context, bucket string) (s3respo
}
defer release()
if !p.versioningEnabled() {
return s3response.GetBucketVersioningOutput{}, s3err.GetAPIError(s3err.ErrVersioningNotConfigured)
}
if !p.isBucketValid(bucket) {
return s3response.GetBucketVersioningOutput{}, s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket)
}
@@ -906,6 +902,15 @@ func (p *Posix) GetBucketVersioning(ctx context.Context, bucket string) (s3respo
return s3response.GetBucketVersioningOutput{}, fmt.Errorf("stat bucket: %w", err)
}
if !p.versioningEnabled() {
// No versioning directory is configured: report the bucket as
// never-versioned (200 with an empty configuration) to match AWS,
// instead of failing with VersioningNotConfigured. GUI clients
// (S3 Browser, CloudBerry, ...) poll this API while browsing
// buckets. PutBucketVersioning still rejects the request.
return s3response.GetBucketVersioningOutput{}, nil
}
vData, err := p.meta.RetrieveAttribute(nil, bucket, "", versioningKey)
if errors.Is(err, meta.ErrNoSuchKey) {
return s3response.GetBucketVersioningOutput{}, nil
+102
View File
@@ -0,0 +1,102 @@
// Copyright 2026 Versity Software
// This file is 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 posix
import (
"context"
"errors"
"os"
"testing"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/stretchr/testify/assert"
"github.com/versity/versitygw/backend/meta"
"github.com/versity/versitygw/s3err"
)
// newUnversionedGateway creates a Posix backend over a temporary root
// directory with no versioning directory configured, i.e. gateway-level
// versioning disabled, matching the default gateway configuration.
func newUnversionedGateway(t *testing.T) *Posix {
t.Helper()
p, err := New(t.TempDir(), meta.XattrMeta{}, PosixOpts{
ValidateBucketNames: true,
})
if err != nil {
t.Fatalf("init posix backend: %v", err)
}
return p
}
// TestVersioningUnconfigured covers the bucket versioning behavior when the
// gateway has no versioning directory configured: bucket validation still
// applies first, GetBucketVersioning returns an empty configuration, and
// PutBucketVersioning is rejected.
func TestVersioningUnconfigured(t *testing.T) {
// New() chdirs into the gateway root; restore the original working
// directory when the test completes.
t.Chdir(t.TempDir())
t.Run("get bucket versioning invalid bucket name", func(t *testing.T) {
p := newUnversionedGateway(t)
_, err := p.GetBucketVersioning(context.Background(), "bad/bucket")
if !errors.Is(err, s3err.GetAPIError(s3err.ErrInvalidBucketName)) {
t.Errorf("expected InvalidBucketName, got %v", err)
}
})
t.Run("get bucket versioning no such bucket", func(t *testing.T) {
p := newUnversionedGateway(t)
_, err := p.GetBucketVersioning(context.Background(), "does-not-exist")
if !errors.Is(err, s3err.GetAPIError(s3err.ErrNoSuchBucket)) {
t.Errorf("expected NoSuchBucket, got %v", err)
}
})
t.Run("get bucket versioning returns empty config", func(t *testing.T) {
p := newUnversionedGateway(t)
err := os.Mkdir("bucket", 0o755)
assert.NoError(t, err)
res, err := p.GetBucketVersioning(context.Background(), "bucket")
assert.NoError(t, err)
assert.Nil(t, res.Status)
})
t.Run("put bucket versioning invalid bucket name", func(t *testing.T) {
p := newUnversionedGateway(t)
err := p.PutBucketVersioning(context.Background(), "bad/bucket", types.BucketVersioningStatusEnabled)
if !errors.Is(err, s3err.GetAPIError(s3err.ErrInvalidBucketName)) {
t.Errorf("expected InvalidBucketName, got %v", err)
}
})
t.Run("put bucket versioning not configured", func(t *testing.T) {
p := newUnversionedGateway(t)
err := os.Mkdir("bucket", 0o755)
assert.NoError(t, err)
err = p.PutBucketVersioning(context.Background(), "bucket", types.BucketVersioningStatusEnabled)
if !errors.Is(err, s3err.GetAPIError(s3err.ErrVersioningNotConfigured)) {
t.Errorf("expected VersioningNotConfigured, got %v", err)
}
})
}
+1
View File
@@ -1846,6 +1846,7 @@ func TestVersioning(ts *TestState) {
func TestVersioningDisabled(ts *TestState) {
ts.Run(VersioningDisabled_GetBucketVersioning_not_configured)
ts.Run(VersioningDisabled_GetBucketVersioning_no_such_bucket)
ts.Run(VersioningDisabled_PutBucketVersioning_not_configured)
}
+27 -7
View File
@@ -3620,8 +3620,32 @@ func Versioning_AccessControl_GetObjectAttributes_policy(s *S3Conf) error {
func VersioningDisabled_GetBucketVersioning_not_configured(s *S3Conf) error {
testName := "VersioningDisabled_GetBucketVersioning_not_configured"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
err := putBucketVersioningStatus(s3client, bucket, types.BucketVersioningStatusEnabled)
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrVersioningNotConfigured)); err != nil {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.GetBucketVersioning(ctx, &s3.GetBucketVersioningInput{
Bucket: &bucket,
})
cancel()
if err != nil {
return err
}
if res.Status != "" {
return fmt.Errorf("expected empty versioning status when versioning is not configured, instead got %v",
res.Status)
}
return nil
})
}
func VersioningDisabled_GetBucketVersioning_no_such_bucket(s *S3Conf) error {
testName := "VersioningDisabled_GetBucketVersioning_no_such_bucket"
return actionHandlerNoSetup(s, testName, func(s3client *s3.Client, bucket string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.GetBucketVersioning(ctx, &s3.GetBucketVersioningInput{
Bucket: &bucket,
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrNoSuchBucket)); err != nil {
return err
}
@@ -3632,11 +3656,7 @@ func VersioningDisabled_GetBucketVersioning_not_configured(s *S3Conf) error {
func VersioningDisabled_PutBucketVersioning_not_configured(s *S3Conf) error {
testName := "VersioningDisabled_PutBucketVersioning_not_configured"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.GetBucketVersioning(ctx, &s3.GetBucketVersioningInput{
Bucket: &bucket,
})
cancel()
err := putBucketVersioningStatus(s3client, bucket, types.BucketVersioningStatusEnabled)
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrVersioningNotConfigured)); err != nil {
return err
}