Skip to content

Commit 88100e9

Browse files
committed
return ResourceExhausted code in CreateVolume
If Openstack quotas for volumes are exceeded, cinder returns with a HTTP 413 response code: https://github.com/openstack/cinder/blob/c4f61c11ef9b590606a2a276bdf256622516181f/cinder/quota_utils.py#L130-L140 CSI spec defines that on a quota limit error, the driver should return a ResourceExhausted grpc error code: https://github.com/container-storage-interface/spec/blob/master/spec.md#createvolume-errors Signed-off-by: Lukas Hoehl <[email protected]>
1 parent 53a4505 commit 88100e9

File tree

4 files changed

+75
-16
lines changed

4 files changed

+75
-16
lines changed

pkg/csi/cinder/controllerserver.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package cinder
1818

1919
import (
2020
"context"
21+
"errors"
2122
"fmt"
2223
"slices"
2324
"sort"
@@ -233,6 +234,10 @@ func (cs *controllerServer) CreateVolume(ctx context.Context, req *csi.CreateVol
233234
vol, err := cloud.CreateVolume(ctx, opts, schedulerHints)
234235
if err != nil {
235236
klog.Errorf("Failed to CreateVolume: %v", err)
237+
quotaErr := openstack.ErrQuotaExceeded{}
238+
if errors.As(err, &quotaErr) {
239+
return nil, status.Errorf(codes.ResourceExhausted, "CreateVolume failed due to exceeded quota %v", quotaErr)
240+
}
236241
return nil, status.Errorf(codes.Internal, "CreateVolume failed with error %v", err)
237242
}
238243

@@ -661,7 +666,6 @@ func (cs *controllerServer) CreateSnapshot(ctx context.Context, req *csi.CreateS
661666
ReadyToUse: true,
662667
},
663668
}, nil
664-
665669
}
666670

667671
func (cs *controllerServer) createSnapshot(ctx context.Context, cloud openstack.IOpenStack, name string, volumeID string, parameters map[string]string) (snap *snapshots.Snapshot, err error) {
@@ -863,7 +867,6 @@ func (cs *controllerServer) ListSnapshots(ctx context.Context, req *csi.ListSnap
863867
Entries: sentries,
864868
NextToken: nextPageToken,
865869
}, nil
866-
867870
}
868871

869872
// ControllerGetCapabilities implements the default GRPC callout.

pkg/csi/cinder/controllerserver_test.go

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,19 @@ import (
2323
"github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/volumes"
2424
"github.com/stretchr/testify/assert"
2525
"github.com/stretchr/testify/mock"
26+
"google.golang.org/grpc/codes"
27+
"google.golang.org/grpc/status"
2628

2729
sharedcsi "k8s.io/cloud-provider-openstack/pkg/csi"
2830
"k8s.io/cloud-provider-openstack/pkg/csi/cinder/openstack"
2931
)
3032

31-
var fakeCs *controllerServer
32-
var fakeCsMultipleClouds *controllerServer
33-
var osmock *openstack.OpenStackMock
34-
var osmockRegionX *openstack.OpenStackMock
33+
var (
34+
fakeCs *controllerServer
35+
fakeCsMultipleClouds *controllerServer
36+
osmock *openstack.OpenStackMock
37+
osmockRegionX *openstack.OpenStackMock
38+
)
3539

3640
// Init Controller Server
3741
func init() {
@@ -94,7 +98,51 @@ func TestCreateVolume(t *testing.T) {
9498
assert.NotEqual(0, len(actualRes.Volume.VolumeId), "Volume Id is nil")
9599
assert.NotNil(actualRes.Volume.AccessibleTopology)
96100
assert.Equal(FakeAvailability, actualRes.Volume.AccessibleTopology[0].GetSegments()[topologyKey])
101+
}
102+
103+
// Test CreateVolume fails with quota exceeded error
104+
func TestCreateVolumeQuotaError(t *testing.T) {
105+
// mock OpenStack
106+
properties := map[string]string{cinderCSIClusterIDKey: FakeCluster}
107+
// CreateVolume(name string, size int, vtype, availability string, snapshotID string, sourceVolID string, sourceBackupID string, tags map[string]string) (string, string, int, error)
108+
osmock.On("CreateVolume", FakeVolName, mock.AnythingOfType("int"), FakeVolType, FakeAvailability, "", "", "", properties).Return(&volumes.Volume{}, openstack.ErrQuotaExceeded{})
97109

110+
osmock.On("GetVolumesByName", FakeVolName).Return(FakeVolListEmpty, nil)
111+
// Init assert
112+
assert := assert.New(t)
113+
114+
// Fake request
115+
fakeReq := &csi.CreateVolumeRequest{
116+
Name: FakeVolName,
117+
VolumeCapabilities: []*csi.VolumeCapability{
118+
{
119+
AccessMode: &csi.VolumeCapability_AccessMode{
120+
Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER,
121+
},
122+
},
123+
},
124+
125+
AccessibilityRequirements: &csi.TopologyRequirement{
126+
Requisite: []*csi.Topology{
127+
{
128+
Segments: map[string]string{topologyKey: FakeAvailability},
129+
},
130+
},
131+
},
132+
}
133+
134+
// Invoke CreateVolume
135+
_, err := fakeCs.CreateVolume(FakeCtx, fakeReq)
136+
if err == nil {
137+
t.Errorf("CreateVolume did not return an error")
138+
}
139+
statusErr, ok := status.FromError(err)
140+
if !ok {
141+
t.Errorf("CreateVolume did not return a grpc status as error, got %v", err)
142+
}
143+
144+
// Assert
145+
assert.Equal(statusErr.Code(), codes.ResourceExhausted)
98146
}
99147

100148
// Test CreateVolume with additional param
@@ -146,7 +194,6 @@ func TestCreateVolumeWithParam(t *testing.T) {
146194
assert.NotEqual(0, len(actualRes.Volume.VolumeId), "Volume Id is nil")
147195
assert.NotNil(actualRes.Volume.AccessibleTopology)
148196
assert.Equal(FakeAvailability, actualRes.Volume.AccessibleTopology[0].GetSegments()[topologyKey])
149-
150197
}
151198

152199
func TestCreateVolumeWithExtraMetadata(t *testing.T) {
@@ -192,7 +239,6 @@ func TestCreateVolumeWithExtraMetadata(t *testing.T) {
192239
if err != nil {
193240
t.Errorf("failed to CreateVolume: %v", err)
194241
}
195-
196242
}
197243

198244
func TestCreateVolumeFromSnapshot(t *testing.T) {
@@ -239,7 +285,6 @@ func TestCreateVolumeFromSnapshot(t *testing.T) {
239285
assert.NotEqual(0, len(actualRes.Volume.VolumeId), "Volume Id is nil")
240286

241287
assert.Equal(FakeSnapshotID, actualRes.Volume.ContentSource.GetSnapshot().SnapshotId)
242-
243288
}
244289

245290
func TestCreateVolumeFromSourceVolume(t *testing.T) {
@@ -286,7 +331,6 @@ func TestCreateVolumeFromSourceVolume(t *testing.T) {
286331
assert.NotEqual(0, len(actualRes.Volume.VolumeId), "Volume Id is nil")
287332

288333
assert.Equal(FakeVolID, actualRes.Volume.ContentSource.GetVolume().VolumeId)
289-
290334
}
291335

292336
// Test CreateVolumeDuplicate
@@ -436,6 +480,7 @@ func genFakeVolumeEntry(fakeVol volumes.Volume) *csi.ListVolumesResponse_Entry {
436480
},
437481
}
438482
}
483+
439484
func genFakeVolumeEntries(fakeVolumes []volumes.Volume) []*csi.ListVolumesResponse_Entry {
440485
entries := make([]*csi.ListVolumesResponse_Entry, 0, len(fakeVolumes))
441486
for _, fakeVol := range fakeVolumes {
@@ -800,7 +845,6 @@ func TestGlobalListVolumesMultipleClouds(t *testing.T) {
800845

801846
// Test CreateSnapshot
802847
func TestCreateSnapshot(t *testing.T) {
803-
804848
osmock.On("CreateSnapshot", FakeSnapshotName, FakeVolID, map[string]string{cinderCSIClusterIDKey: "cluster"}).Return(&FakeSnapshotRes, nil)
805849
osmock.On("ListSnapshots", map[string]string{"Name": FakeSnapshotName}).Return(FakeSnapshotListEmpty, "", nil)
806850
osmock.On("WaitSnapshotReady", FakeSnapshotID).Return(FakeSnapshotRes.Status, nil)
@@ -944,7 +988,6 @@ func TestControllerExpandVolume(t *testing.T) {
944988

945989
// Assert
946990
assert.Equal(expectedRes, actualRes)
947-
948991
}
949992

950993
func TestValidateVolumeCapabilities(t *testing.T) {
@@ -1000,13 +1043,11 @@ func TestValidateVolumeCapabilities(t *testing.T) {
10001043
}
10011044

10021045
actualRes2, err := fakeCs.ValidateVolumeCapabilities(FakeCtx, fakereq2)
1003-
10041046
if err != nil {
10051047
t.Errorf("failed to ValidateVolumeCapabilities: %v", err)
10061048
}
10071049

10081050
// assert
10091051
assert.Equal(expectedRes, actualRes)
10101052
assert.Equal(expectedRes2, actualRes2)
1011-
10121053
}

pkg/csi/cinder/openstack/openstack.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,8 +146,10 @@ func GetConfigFromFiles(configFilePaths []string) (Config, error) {
146146

147147
const defaultMaxVolAttachLimit int64 = 256
148148

149-
var OsInstances map[string]IOpenStack
150-
var configFiles = []string{"/etc/cloud.conf"}
149+
var (
150+
OsInstances map[string]IOpenStack
151+
configFiles = []string{"/etc/cloud.conf"}
152+
)
151153

152154
func InitOpenStackProvider(cfgFiles []string, httpEndpoint string) {
153155
OsInstances = make(map[string]IOpenStack)
@@ -239,3 +241,11 @@ func GetOpenStackProvider(cloudName string) (IOpenStack, error) {
239241
func (os *OpenStack) GetMetadataOpts() metadata.Opts {
240242
return os.metadataOpts
241243
}
244+
245+
type ErrQuotaExceeded struct {
246+
openstackError error
247+
}
248+
249+
func (e ErrQuotaExceeded) Error() string {
250+
return fmt.Sprintf("quota exceeded: %w", e.openstackError)
251+
}

pkg/csi/cinder/openstack/openstack_volumes.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@ package openstack
1919
import (
2020
"context"
2121
"fmt"
22+
"net/http"
2223
"net/url"
2324
"strings"
2425
"time"
2526

27+
"github.com/gophercloud/gophercloud/v2"
2628
"github.com/gophercloud/gophercloud/v2/openstack"
2729
"github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/volumes"
2830
"github.com/gophercloud/gophercloud/v2/openstack/compute/v2/volumeattach"
@@ -71,6 +73,9 @@ func (os *OpenStack) CreateVolume(ctx context.Context, opts *volumes.CreateOpts,
7173
opts.Description = volumeDescription
7274
vol, err := volumes.Create(ctx, blockstorageClient, opts, schedulerHints).Extract()
7375
if mc.ObserveRequest(err) != nil {
76+
if gophercloud.ResponseCodeIs(err, http.StatusRequestEntityTooLarge) {
77+
return nil, ErrQuotaExceeded{openstackError: err}
78+
}
7479
return nil, err
7580
}
7681

0 commit comments

Comments
 (0)