diff --git a/cmd/bucket-quota.go b/cmd/bucket-quota.go index d9779c21a..f515a86b2 100644 --- a/cmd/bucket-quota.go +++ b/cmd/bucket-quota.go @@ -43,6 +43,17 @@ func NewBucketQuotaSys() *BucketQuotaSys { return &BucketQuotaSys{} } +// getBucketQuotaSize returns the effective enforced hard-quota size. +func getBucketQuotaSize(quota *madmin.BucketQuota) uint64 { + if quota == nil || quota.Type != madmin.HardQuota { + return 0 + } + if quota.Size > 0 { + return quota.Size + } + return quota.Quota +} + var bucketStorageCache = cachevalue.New[DataUsageInfo]() // Init initialize bucket quota. @@ -110,14 +121,7 @@ func (sys *BucketQuotaSys) enforceQuotaHard(ctx context.Context, bucket string, return err } - var quotaSize uint64 - if q != nil && q.Type == madmin.HardQuota { - if q.Size > 0 { - quotaSize = q.Size - } else if q.Quota > 0 { - quotaSize = q.Quota - } - } + quotaSize := getBucketQuotaSize(q) if quotaSize > 0 { if uint64(size) >= quotaSize { // check if file size already exceeds the quota return BucketQuotaExceeded{Bucket: bucket} diff --git a/cmd/bucket-quota_test.go b/cmd/bucket-quota_test.go new file mode 100644 index 000000000..bdcefbb8e --- /dev/null +++ b/cmd/bucket-quota_test.go @@ -0,0 +1,77 @@ +// Copyright (c) 2015-2025 MinIO, Inc. +// Copyright (c) 2025-2026 PGSTY +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cmd + +import ( + "testing" + + "github.com/minio/madmin-go/v3" +) + +func TestGetBucketQuotaSize(t *testing.T) { + tests := []struct { + name string + quota *madmin.BucketQuota + want uint64 + }{ + {name: "nil"}, + {name: "empty", quota: &madmin.BucketQuota{}}, + {name: "current size", quota: &madmin.BucketQuota{Type: madmin.HardQuota, Size: 1024}, want: 1024}, + {name: "legacy quota", quota: &madmin.BucketQuota{Type: madmin.HardQuota, Quota: 2048}, want: 2048}, + {name: "size takes precedence", quota: &madmin.BucketQuota{Type: madmin.HardQuota, Size: 1024, Quota: 2048}, want: 1024}, + {name: "missing type", quota: &madmin.BucketQuota{Size: 1024}}, + {name: "unsupported type", quota: &madmin.BucketQuota{Type: "fifo", Size: 1024}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := getBucketQuotaSize(tt.quota); got != tt.want { + t.Fatalf("getBucketQuotaSize() = %d, want %d", got, tt.want) + } + }) + } +} + +func TestIsBktQuotaCfgReplicated(t *testing.T) { + hardQuota := func(size, legacy uint64) *madmin.BucketQuota { + return &madmin.BucketQuota{Type: madmin.HardQuota, Size: size, Quota: legacy} + } + + tests := []struct { + name string + quotas []*madmin.BucketQuota + want bool + }{ + {name: "none configured", quotas: []*madmin.BucketQuota{nil, nil}, want: true}, + {name: "missing from one site", quotas: []*madmin.BucketQuota{hardQuota(1024, 0), nil}}, + {name: "matching size", quotas: []*madmin.BucketQuota{hardQuota(1024, 0), hardQuota(1024, 0)}, want: true}, + {name: "different size", quotas: []*madmin.BucketQuota{hardQuota(1024, 0), hardQuota(2048, 0)}}, + {name: "equivalent representations", quotas: []*madmin.BucketQuota{hardQuota(1024, 0), hardQuota(0, 1024)}, want: true}, + {name: "different typeless size", quotas: []*madmin.BucketQuota{{Size: 1024}, {Size: 2048}}}, + {name: "different type", quotas: []*madmin.BucketQuota{hardQuota(1024, 0), {Type: "fifo", Size: 1024}}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := isBktQuotaCfgReplicated(len(tt.quotas), tt.quotas); got != tt.want { + t.Fatalf("isBktQuotaCfgReplicated() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/cmd/metrics-v2.go b/cmd/metrics-v2.go index e38750a58..b8d5c1290 100644 --- a/cmd/metrics-v2.go +++ b/cmd/metrics-v2.go @@ -3314,10 +3314,10 @@ func getBucketUsageMetrics(opts MetricsGroupOpts) *MetricsGroupV2 { VariableLabels: map[string]string{"bucket": bucket}, }) - if quota != nil && quota.Quota > 0 { + if quotaSize := getBucketQuotaSize(quota); quotaSize > 0 { metrics = append(metrics, MetricV2{ Description: getBucketUsageQuotaTotalBytesMD(), - Value: float64(quota.Quota), + Value: float64(quotaSize), VariableLabels: map[string]string{"bucket": bucket}, }) } diff --git a/cmd/metrics-v3-cluster-usage.go b/cmd/metrics-v3-cluster-usage.go index 38dc0aef3..3d05be951 100644 --- a/cmd/metrics-v3-cluster-usage.go +++ b/cmd/metrics-v3-cluster-usage.go @@ -167,8 +167,8 @@ func loadClusterUsageBucketMetrics(ctx context.Context, m MetricValues, c *metri m.Set(usageBucketVersionsCount, float64(usage.VersionsCount), "bucket", bucket) m.Set(usageBucketDeleteMarkersCount, float64(usage.DeleteMarkersCount), "bucket", bucket) - if quota != nil && quota.Quota > 0 { - m.Set(usageBucketQuotaTotalBytes, float64(quota.Quota), "bucket", bucket) + if quotaSize := getBucketQuotaSize(quota); quotaSize > 0 { + m.Set(usageBucketQuotaTotalBytes, float64(quotaSize), "bucket", bucket) } for k, v := range usage.ObjectSizesHistogram { diff --git a/cmd/site-replication.go b/cmd/site-replication.go index d15ec19b4..e7a82cfe4 100644 --- a/cmd/site-replication.go +++ b/cmd/site-replication.go @@ -3863,7 +3863,16 @@ func isBktQuotaCfgReplicated(total int, quotaCfgs []*madmin.BucketQuota) bool { prev = q continue } - if prev.Quota != q.Quota || prev.Type != q.Type { + if prev.Type != q.Type { + return false + } + if prev.Type == madmin.HardQuota { + if getBucketQuotaSize(prev) != getBucketQuotaSize(q) { + return false + } + continue + } + if prev.Size != q.Size || prev.Quota != q.Quota { return false } } diff --git a/cmd/veeam-sos-api.go b/cmd/veeam-sos-api.go index 03523e8e1..f82397fe9 100644 --- a/cmd/veeam-sos-api.go +++ b/cmd/veeam-sos-api.go @@ -25,7 +25,6 @@ import ( "os" "strings" - "github.com/minio/madmin-go/v3" "github.com/minio/minio/internal/logger" ) @@ -177,14 +176,7 @@ func veeamSOSAPIGetObject(ctx context.Context, bucket, object string, rs *HTTPRa Used: int64(binfo.Size), } - var quotaSize int64 - if q != nil && q.Type == madmin.HardQuota { - if q.Size > 0 { - quotaSize = int64(q.Size) - } else if q.Quota > 0 { - quotaSize = int64(q.Quota) - } - } + quotaSize := int64(getBucketQuotaSize(q)) if quotaSize == 0 { info := objAPI.StorageInfo(ctx, true)