core: parse subvolumegroup bytes_quota reported as "infinite"

Ceph reports bytes_quota in `ceph fs subvolumegroup info` as the JSON
string "infinite" when no quota is set, and as a number otherwise. The
subvolumeGroupInfo.BytesQuota field is typed int64, so json.Unmarshal
fails with "cannot unmarshal string into Go value of type int64" for any
subvolume group without a quota (the default).

That unmarshal error is returned from getCephFSSubVolumeGroupInfo and
swallowed in CreateCephFSSubVolumeGroup: exec.ExitStatus reports ok=false
for the wrapped error, so the non-ENOENT guard does not fire and the
dedicated resize path (which enforces --no-shrink) is skipped.

Add an UnmarshalJSON on subvolumeGroupInfo that accepts either a number
or the "infinite" sentinel, mapping "infinite" to 0 (unlimited) so the
existing CmpInt64 comparison keeps working. Add a table-driven test
covering the finite and infinite cases.

Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
This commit is contained in:
Anas Khan committed 2026-07-15 11:34:57 +05:30
1 parent 72403b99d7
commit 7ee65e13be
2 files changed
+76

No files matched your search

+32
View File
@@ -96,6 +96,38 @@ type subvolumeGroupInfo struct {
DataPool string `json:"data_pool"`
}
// UnmarshalJSON handles the bytes_quota field, which Ceph reports as the JSON
// string "infinite" when no quota is set (see Ceph's Group.info) and as a
// number otherwise. The "infinite" sentinel is mapped to 0 (unlimited).
func (s *subvolumeGroupInfo) UnmarshalJSON(data []byte) error {
type subvolumeGroupInfoAlias subvolumeGroupInfo
aux := &struct {
BytesQuota json.RawMessage `json:"bytes_quota"`
*subvolumeGroupInfoAlias
}{
subvolumeGroupInfoAlias: (*subvolumeGroupInfoAlias)(s),
}
if err := json.Unmarshal(data, aux); err != nil {
return err
}
if len(aux.BytesQuota) == 0 || string(aux.BytesQuota) == "null" {
s.BytesQuota = 0
return nil
}
var quotaStr string
if err := json.Unmarshal(aux.BytesQuota, &quotaStr); err == nil {
if quotaStr == "infinite" {
s.BytesQuota = 0
return nil
}
return errors.Errorf("unexpected bytes_quota value %q", quotaStr)
}
return json.Unmarshal(aux.BytesQuota, &s.BytesQuota)
}
// getCephFSSubVolumeGroupInfo get subvolumegroup info of the group name.
// volName is the name of the Ceph FS volume, the same as the CephFilesystem CR name.
func getCephFSSubVolumeGroupInfo(context *clusterd.Context, clusterInfo *ClusterInfo, volName, groupName string) (*subvolumeGroupInfo, error) {
@@ -19,7 +19,10 @@ package client
import (
"testing"
"github.com/pkg/errors"
cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1"
"github.com/rook/rook/pkg/clusterd"
exectest "github.com/rook/rook/pkg/util/exec/test"
"github.com/stretchr/testify/assert"
)
@@ -79,3 +82,44 @@ func TestValidatePinningValues(t *testing.T) {
err = validatePinningValues(testData1)
assert.NoError(t, err)
}
func TestGetCephFSSubVolumeGroupInfo(t *testing.T) {
tests := []struct {
name string
output string
expectedQuota int64
expectedDataPool string
}{
{
name: "finite quota",
output: `{"bytes_quota":1099511627776,"bytes_used":0,"data_pool":"myfs-replicated"}`,
expectedQuota: 1099511627776,
expectedDataPool: "myfs-replicated",
},
{
// Ceph reports bytes_quota as the string "infinite" when no quota is set.
name: "infinite quota",
output: `{"bytes_quota":"infinite","bytes_used":0,"data_pool":"myfs-replicated"}`,
expectedQuota: 0,
expectedDataPool: "myfs-replicated",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
executor := &exectest.MockExecutor{}
executor.MockExecuteCommandWithOutput = func(command string, args ...string) (string, error) {
logger.Infof("Command: %s %v", command, args)
if args[0] == "fs" && args[1] == "subvolumegroup" && args[2] == "info" {
return tt.output, nil
}
return "", errors.Errorf("unexpected ceph command %q", args)
}
svgInfo, err := getCephFSSubVolumeGroupInfo(&clusterd.Context{Executor: executor}, AdminTestClusterInfo("mycluster"), "myfs", "csi")
assert.NoError(t, err)
assert.Equal(t, tt.expectedQuota, svgInfo.BytesQuota)
assert.Equal(t, tt.expectedDataPool, svgInfo.DataPool)
})
}
}