Skip to content

Commit

Permalink
Implement ListVolumes (#144)
Browse files Browse the repository at this point in the history
* Advertise ListVolumes which allows usage of Volume Health Monitoring

* Set abnormal on EIO

If we get an EIO error from statsfs then its safe to assume the PV is not happy

* Add unit tests for ListVolumes

The tests validate most of the returned fields of the Volume part of the
response but not the VolumeContext and ContentSource fields.

All of the fields of the VolumeStatus are validated.

The fake LinodeClient has been added in order to easily mock Linode API
responses.
  • Loading branch information
avestuk authored Dec 14, 2023
1 parent f28e59c commit 714b8df
Show file tree
Hide file tree
Showing 6 changed files with 323 additions and 42 deletions.
33 changes: 33 additions & 0 deletions pkg/linode-bs/capabilities.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package linodebs

import "github.com/container-storage-interface/spec/lib/go/csi"

func controllerCapabilities() []csi.ControllerServiceCapability_RPC_Type {
return []csi.ControllerServiceCapability_RPC_Type{
csi.ControllerServiceCapability_RPC_CREATE_DELETE_VOLUME,
csi.ControllerServiceCapability_RPC_PUBLISH_UNPUBLISH_VOLUME,
// csi.ControllerServiceCapability_RPC_CREATE_DELETE_SNAPSHOT,
// csi.ControllerServiceCapability_RPC_LIST_SNAPSHOTS,
csi.ControllerServiceCapability_RPC_PUBLISH_READONLY,
csi.ControllerServiceCapability_RPC_EXPAND_VOLUME,
csi.ControllerServiceCapability_RPC_CLONE_VOLUME,
csi.ControllerServiceCapability_RPC_LIST_VOLUMES,
csi.ControllerServiceCapability_RPC_VOLUME_CONDITION,
}
}

func nodeCapabilities() []csi.NodeServiceCapability_RPC_Type {
return []csi.NodeServiceCapability_RPC_Type{
csi.NodeServiceCapability_RPC_STAGE_UNSTAGE_VOLUME,
csi.NodeServiceCapability_RPC_EXPAND_VOLUME,
csi.NodeServiceCapability_RPC_GET_VOLUME_STATS,
csi.NodeServiceCapability_RPC_VOLUME_CONDITION,
}
}

func volumeCapabilitiesAccessMode() []csi.VolumeCapability_AccessMode_Mode {
return []csi.VolumeCapability_AccessMode_Mode{
csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER,
// csi.VolumeCapability_AccessMode_MULTI_NODE_READER_ONLY,
}
}
39 changes: 16 additions & 23 deletions pkg/linode-bs/controllerserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ const (
VolumeLifecycleNodePublishVolume VolumeLifecycle = "NodePublishVolume"
VolumeLifecycleNodeUnstageVolume VolumeLifecycle = "NodeUnstageVolume"
VolumeLifecycleNodeUnpublishVolume VolumeLifecycle = "NodeUnpublishVolume"

// Linode Volume Topology Region Label
VolumeTopologyRegion string = "topology.linode.com/region"
)

type LinodeControllerServer struct {
Expand Down Expand Up @@ -163,7 +166,7 @@ func (linodeCS *LinodeControllerServer) CreateVolume(ctx context.Context, req *c
AccessibleTopology: []*csi.Topology{
{
Segments: map[string]string{
"topology.linode.com/region": vol.Region,
VolumeTopologyRegion: vol.Region,
},
},
},
Expand Down Expand Up @@ -393,6 +396,11 @@ func (linodeCS *LinodeControllerServer) ListVolumes(ctx context.Context, req *cs
for _, vol := range volumes {
key := common.CreateLinodeVolumeKey(vol.ID, vol.Label)

var publishInfoVolumeName []string = make([]string, 0, 1)
if vol.LinodeID != nil {
publishInfoVolumeName = append(publishInfoVolumeName, fmt.Sprintf("%d", *vol.LinodeID))
}

entries = append(entries, &csi.ListVolumesResponse_Entry{
Volume: &csi.Volume{
VolumeId: key.GetVolumeKey(),
Expand All @@ -405,6 +413,12 @@ func (linodeCS *LinodeControllerServer) ListVolumes(ctx context.Context, req *cs
},
},
},
Status: &csi.ListVolumesResponse_VolumeStatus{
PublishedNodeIds: publishInfoVolumeName,
VolumeCondition: &csi.VolumeCondition{
Abnormal: false,
},
},
})
}

Expand All @@ -424,29 +438,8 @@ func (linodeCS *LinodeControllerServer) ControllerGetVolume(ctx context.Context,

// ControllerGetCapabilities returns the supported capabilities of controller service provided by this Plugin
func (linodeCS *LinodeControllerServer) ControllerGetCapabilities(ctx context.Context, req *csi.ControllerGetCapabilitiesRequest) (*csi.ControllerGetCapabilitiesResponse, error) {
newCap := func(cap csi.ControllerServiceCapability_RPC_Type) *csi.ControllerServiceCapability {
return &csi.ControllerServiceCapability{
Type: &csi.ControllerServiceCapability_Rpc{
Rpc: &csi.ControllerServiceCapability_RPC{
Type: cap,
},
},
}
}

var caps []*csi.ControllerServiceCapability
for _, capability := range []csi.ControllerServiceCapability_RPC_Type{
csi.ControllerServiceCapability_RPC_CREATE_DELETE_VOLUME,
csi.ControllerServiceCapability_RPC_PUBLISH_UNPUBLISH_VOLUME,
csi.ControllerServiceCapability_RPC_LIST_VOLUMES,
csi.ControllerServiceCapability_RPC_EXPAND_VOLUME,
csi.ControllerServiceCapability_RPC_CLONE_VOLUME,
} {
caps = append(caps, newCap(capability))
}

resp := &csi.ControllerGetCapabilitiesResponse{
Capabilities: caps,
Capabilities: linodeCS.Driver.cscap,
}

klog.V(4).Infoln("controller get capabilities called", map[string]interface{}{
Expand Down
231 changes: 231 additions & 0 deletions pkg/linode-bs/controllerserver_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
package linodebs

import (
"context"
"errors"
"fmt"
"testing"

"github.com/container-storage-interface/spec/lib/go/csi"
"github.com/linode/linode-blockstorage-csi-driver/pkg/common"
"github.com/linode/linodego"
)

func TestListVolumes(t *testing.T) {
cases := map[string]struct {
volumes []linodego.Volume
throwErr bool
}{
"volume attached to node": {
volumes: []linodego.Volume{
{
ID: 1,
Label: "foo",
Status: "",
Region: "danmaaag",
Size: 30,
LinodeID: createLinodeID(10),
FilesystemPath: "",
Tags: []string{},
},
},
throwErr: false,
},
"volume not attached": {
volumes: []linodego.Volume{
{
ID: 1,
Label: "bar",
Status: "",
Region: "",
Size: 30,
FilesystemPath: "",
},
},
throwErr: false,
},
"multiple volumes - with attachments": {
volumes: []linodego.Volume{
{
ID: 1,
Label: "foo",
Status: "",
Region: "",
Size: 30,
LinodeID: createLinodeID(5),
FilesystemPath: "",
Tags: []string{},
},
{
ID: 2,
Label: "foo",
Status: "",
Region: "",
Size: 60,
FilesystemPath: "",
Tags: []string{},
LinodeID: createLinodeID(10),
},
},
throwErr: false,
},
"multiple volumes - mixed attachments": {
volumes: []linodego.Volume{
{
ID: 1,
Label: "foo",
Status: "",
Region: "",
Size: 30,
LinodeID: createLinodeID(5),
FilesystemPath: "",
Tags: []string{},
},
{
ID: 2,
Label: "foo",
Status: "",
Region: "",
Size: 30,
FilesystemPath: "",
Tags: []string{},
LinodeID: nil,
},
},
throwErr: false,
},
"Linode API error": {
volumes: nil,
throwErr: true,
},
}

for c, tt := range cases {
t.Run(c, func(t *testing.T) {
cs := &LinodeControllerServer{
CloudProvider: &fakeLinodeClient{
volumes: tt.volumes,
throwErr: tt.throwErr,
},
}

listVolsResp, err := cs.ListVolumes(context.Background(), &csi.ListVolumesRequest{})
if err != nil {
if !tt.throwErr {
t.Fatalf("test case got unexpected err: %s", err)
}
return
}

for _, entry := range listVolsResp.Entries {
gotVol := entry.GetVolume()
if gotVol == nil {
t.Fatal("vol was nil")
}

var wantVol *linodego.Volume
for _, v := range tt.volumes {
v := v
// The issue is that the ID returned is
// not the same as what is passed in
key := common.CreateLinodeVolumeKey(v.ID, v.Label)
if gotVol.VolumeId == key.GetVolumeKey() {
wantVol = &v
break
}
}

if wantVol == nil {
t.Fatalf("failed to find input volume equivalent to: %#v", gotVol)
}

if gotVol.CapacityBytes != int64(wantVol.Size)*gigabyte {
t.Errorf("volume size not equal, got: %d, want: %d", gotVol.CapacityBytes, wantVol.Size*gigabyte)
}

for _, i := range gotVol.GetAccessibleTopology() {
region, ok := i.Segments[VolumeTopologyRegion]
if !ok {
t.Errorf("got empty region")
}

if region != wantVol.Region {
t.Errorf("regions do not match, got: %s, want: %s", region, wantVol.Region)
}
}

status := entry.GetStatus()
if status == nil {
t.Fatal("status was nil")
}

if status.VolumeCondition.Abnormal {
t.Errorf("got abnormal volume condition")
}

if len(status.GetPublishedNodeIds()) > 1 {
t.Errorf("volume was published on more than 1 node, got: %s", status.GetPublishedNodeIds())
}

switch publishedNodes := status.GetPublishedNodeIds(); {
case len(publishedNodes) == 0 && wantVol.LinodeID == nil:
// This case is fine - having it here prevents a segfault if we try to index into publishedNodes in the last case
case len(publishedNodes) == 0 && wantVol.LinodeID != nil:
t.Errorf("expected volume to be attached, got: %s, want: %d", status.GetPublishedNodeIds(), *wantVol.LinodeID)
case len(publishedNodes) != 0 && wantVol.LinodeID == nil:
t.Errorf("expected volume to be unattached, got: %s", publishedNodes)
case publishedNodes[0] != fmt.Sprintf("%d", *wantVol.LinodeID):
t.Fatalf("got: %s, want: %d published node id", status.GetPublishedNodeIds()[0], *wantVol.LinodeID)
}
}
})

}

}

type fakeLinodeClient struct {
volumes []linodego.Volume
throwErr bool
}

func (flc *fakeLinodeClient) ListInstances(context.Context, *linodego.ListOptions) ([]linodego.Instance, error) {
return nil, nil
}
func (flc *fakeLinodeClient) ListVolumes(context.Context, *linodego.ListOptions) ([]linodego.Volume, error) {
if flc.throwErr {
return nil, errors.New("sad times mate")
}
return flc.volumes, nil
}
func (flc *fakeLinodeClient) GetInstance(context.Context, int) (*linodego.Instance, error) {
return nil, nil
}
func (flc *fakeLinodeClient) GetVolume(context.Context, int) (*linodego.Volume, error) {
return nil, nil
}
func (flc *fakeLinodeClient) CreateVolume(context.Context, linodego.VolumeCreateOptions) (*linodego.Volume, error) {
return nil, nil
}
func (flc *fakeLinodeClient) CloneVolume(context.Context, int, string) (*linodego.Volume, error) {
return nil, nil
}
func (flc *fakeLinodeClient) AttachVolume(context.Context, int, *linodego.VolumeAttachOptions) (*linodego.Volume, error) {
return nil, nil
}
func (flc *fakeLinodeClient) DetachVolume(context.Context, int) error { return nil }
func (flc *fakeLinodeClient) WaitForVolumeLinodeID(context.Context, int, *int, int) (*linodego.Volume, error) {
return nil, nil
}
func (flc *fakeLinodeClient) WaitForVolumeStatus(context.Context, int, linodego.VolumeStatus, int) (*linodego.Volume, error) {
return nil, nil
}
func (flc *fakeLinodeClient) DeleteVolume(context.Context, int) error { return nil }
func (flc *fakeLinodeClient) ResizeVolume(context.Context, int, int) error { return nil }
func (flc *fakeLinodeClient) NewEventPoller(context.Context, any, linodego.EntityType, linodego.EventAction) (*linodego.EventPoller, error) {
return nil, nil
}

func createLinodeID(i int) *int {
return &i
}
21 changes: 3 additions & 18 deletions pkg/linode-bs/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,30 +73,15 @@ func (linodeDriver *LinodeDriver) SetupLinodeDriver(linodeClient linodeclient.Li
linodeDriver.bsPrefix = bsPrefix

// Adding Capabilities
vcam := []csi.VolumeCapability_AccessMode_Mode{
csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER,
// csi.VolumeCapability_AccessMode_MULTI_NODE_READER_ONLY,
}
vcam := volumeCapabilitiesAccessMode()
if err := linodeDriver.AddVolumeCapabilityAccessModes(vcam); err != nil {
return err
}
csc := []csi.ControllerServiceCapability_RPC_Type{
csi.ControllerServiceCapability_RPC_CREATE_DELETE_VOLUME,
csi.ControllerServiceCapability_RPC_PUBLISH_UNPUBLISH_VOLUME,
// csi.ControllerServiceCapability_RPC_CREATE_DELETE_SNAPSHOT,
// csi.ControllerServiceCapability_RPC_LIST_SNAPSHOTS,
csi.ControllerServiceCapability_RPC_PUBLISH_READONLY,
csi.ControllerServiceCapability_RPC_EXPAND_VOLUME,
csi.ControllerServiceCapability_RPC_CLONE_VOLUME,
}
csc := controllerCapabilities()
if err := linodeDriver.AddControllerServiceCapabilities(csc); err != nil {
return err
}
ns := []csi.NodeServiceCapability_RPC_Type{
csi.NodeServiceCapability_RPC_STAGE_UNSTAGE_VOLUME,
csi.NodeServiceCapability_RPC_EXPAND_VOLUME,
csi.NodeServiceCapability_RPC_GET_VOLUME_STATS,
}
ns := nodeCapabilities()
if err := linodeDriver.AddNodeServiceCapabilities(ns); err != nil {
return err
}
Expand Down
Loading

0 comments on commit 714b8df

Please sign in to comment.