diff --git a/Dockerfile b/Dockerfile index f3f5eab..729f632 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,7 @@ ARG BUILD_DATE RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -a -ldflags "-w -s -X github.com/scaleway/scaleway-csi/driver.driverVersion=${TAG} -X github.com/scaleway/scaleway-csi/driver.buildDate=${BUILD_DATE} -X github.com/scaleway/scaleway-csi/driver.gitCommit=${COMMIT_SHA} " -o scaleway-csi ./cmd/scaleway-csi FROM alpine:3.11 -RUN apk update && apk add --no-cache e2fsprogs xfsprogs ca-certificates && update-ca-certificates +RUN apk update && apk add --no-cache e2fsprogs e2fsprogs-extra xfsprogs xfsprogs-extra ca-certificates && update-ca-certificates WORKDIR / COPY --from=builder /go/src/github.com/scaleway/scaleway-csi/scaleway-csi . ENTRYPOINT ["/scaleway-csi"] diff --git a/driver/controller.go b/driver/controller.go index 44b67ef..37d241e 100644 --- a/driver/controller.go +++ b/driver/controller.go @@ -25,6 +25,7 @@ var ( csi.ControllerServiceCapability_RPC_LIST_VOLUMES, csi.ControllerServiceCapability_RPC_CREATE_DELETE_SNAPSHOT, csi.ControllerServiceCapability_RPC_LIST_SNAPSHOTS, + csi.ControllerServiceCapability_RPC_EXPAND_VOLUME, } // supportedAccessModes represents the supported access modes for the Scaleway Block Volumes @@ -74,21 +75,26 @@ func (d *controllerService) CreateVolume(ctx context.Context, req *csi.CreateVol return nil, status.Errorf(codes.InvalidArgument, "volumeCapabilities not supported: %s", err) } - size, err := getVolumeRequestCapacity(req.GetCapacityRange()) - if err != nil { - return nil, status.Errorf(codes.OutOfRange, "capacityRange invalid: %s", err) - } - volumeType := scaleway.DefaultVolumeType for key, value := range req.GetParameters() { switch strings.ToLower(key) { case volumeTypeKey: - volumeType = instance.VolumeType(value) + volumeType = instance.VolumeVolumeType(value) default: return nil, status.Errorf(codes.InvalidArgument, "invalid parameter key %s", key) } } + minSize, maxSize, err := d.scaleway.GetVolumeLimits(string(volumeType)) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + + size, err := getVolumeRequestCapacity(minSize, maxSize, req.GetCapacityRange()) + if err != nil { + return nil, status.Errorf(codes.OutOfRange, "capacityRange invalid: %s", err) + } + scwVolumeName := d.config.Prefix + volumeName // TODO check all zones volume, err := d.scaleway.GetVolumeByName(scwVolumeName, size, volumeType) @@ -135,10 +141,8 @@ func (d *controllerService) CreateVolume(ctx context.Context, req *csi.CreateVol Zone: sourceSnapshotZone, }) if err != nil { - if ferr, ok := err.(*scw.ResponseError); ok { - if ferr.StatusCode == 404 { - return nil, status.Error(codes.NotFound, err.Error()) - } + if _, ok := err.(*scw.ResourceNotFoundError); ok { + return nil, status.Errorf(codes.NotFound, "snapshot %s not found", sourceSnapshotID) } return nil, status.Error(codes.Internal, err.Error()) } @@ -178,10 +182,8 @@ func (d *controllerService) CreateVolume(ctx context.Context, req *csi.CreateVol } volumeResp, err := d.scaleway.CreateVolume(volumeRequest) if err != nil { - if ferr, ok := err.(*scw.ResponseError); ok { - if ferr.StatusCode == 404 { - return nil, status.Error(codes.NotFound, err.Error()) - } + if _, ok := err.(*scw.ResourceNotFoundError); ok { + return nil, status.Error(codes.NotFound, err.Error()) } return nil, status.Error(codes.Internal, err.Error()) } @@ -248,12 +250,11 @@ func (d *controllerService) DeleteVolume(ctx context.Context, req *csi.DeleteVol Zone: volumeZone, }) if err != nil { - if ferr, ok := err.(*scw.ResponseError); ok { - if ferr.StatusCode == 404 { - klog.V(4).Infof("volume with ID %s not found", volumeID) - return &csi.DeleteVolumeResponse{}, nil - } + if _, ok := err.(*scw.ResourceNotFoundError); ok { + klog.V(4).Infof("volume with ID %s not found", volumeID) + return &csi.DeleteVolumeResponse{}, nil } + return nil, status.Error(codes.Internal, err.Error()) } if volumeResp.Volume.Server != nil { @@ -266,12 +267,11 @@ func (d *controllerService) DeleteVolume(ctx context.Context, req *csi.DeleteVol Zone: volumeResp.Volume.Zone, }) if err != nil { - if ferr, ok := err.(*scw.ResponseError); ok { - if ferr.StatusCode == 404 { - klog.V(4).Infof("volume with ID %s not found", volumeID) - return &csi.DeleteVolumeResponse{}, nil - } + if _, ok := err.(*scw.ResourceNotFoundError); ok { + klog.V(4).Infof("volume with ID %s not found", volumeID) + return &csi.DeleteVolumeResponse{}, nil } + return nil, status.Error(codes.Internal, err.Error()) } klog.V(4).Infof("volume with ID %s deleted", volumeID) @@ -308,10 +308,8 @@ func (d *controllerService) ControllerPublishVolume(ctx context.Context, req *cs Zone: volumeZone, }) if err != nil { - if ferr, ok := err.(*scw.ResponseError); ok { - if ferr.StatusCode == 404 { - return nil, status.Errorf(codes.NotFound, "volume %s not found", volumeID) - } + if _, ok := err.(*scw.ResourceNotFoundError); ok { + return nil, status.Errorf(codes.NotFound, "volume %s not found", volumeID) } return nil, status.Error(codes.Internal, err.Error()) } @@ -321,10 +319,8 @@ func (d *controllerService) ControllerPublishVolume(ctx context.Context, req *cs Zone: nodeZone, }) if err != nil { - if ferr, ok := err.(*scw.ResponseError); ok { - if ferr.StatusCode == 404 { - return nil, status.Errorf(codes.NotFound, "node %s not found", nodeID) - } + if _, ok := err.(*scw.ResourceNotFoundError); ok { + return nil, status.Errorf(codes.NotFound, "instance %s not found", volumeID) } return nil, status.Error(codes.Internal, err.Error()) } @@ -391,10 +387,8 @@ func (d *controllerService) ControllerUnpublishVolume(ctx context.Context, req * Zone: volumeZone, }) if err != nil { - if ferr, ok := err.(*scw.ResponseError); ok { - if ferr.StatusCode == 404 { - return &csi.ControllerUnpublishVolumeResponse{}, nil - } + if _, ok := err.(*scw.ResourceNotFoundError); ok { + return &csi.ControllerUnpublishVolumeResponse{}, nil } return nil, status.Error(codes.Internal, err.Error()) } @@ -408,10 +402,8 @@ func (d *controllerService) ControllerUnpublishVolume(ctx context.Context, req * Zone: nodeZone, }) if err != nil { - if ferr, ok := err.(*scw.ResponseError); ok { - if ferr.StatusCode == 404 { - return &csi.ControllerUnpublishVolumeResponse{}, nil - } + if _, ok := err.(*scw.ResourceNotFoundError); ok { + return &csi.ControllerUnpublishVolumeResponse{}, nil } return nil, status.Error(codes.Internal, err.Error()) } @@ -450,11 +442,10 @@ func (d *controllerService) ValidateVolumeCapabilities(ctx context.Context, req Zone: volumeZone, }) if err != nil { - if ferr, ok := err.(*scw.ResponseError); ok { - if ferr.StatusCode == 404 { - return nil, status.Errorf(codes.NotFound, "volume %s not found", volumeID) - } + if _, ok := err.(*scw.ResourceNotFoundError); ok { + return nil, status.Errorf(codes.NotFound, "volume %s not found", volumeID) } + return nil, status.Error(codes.Internal, err.Error()) } // TODO check stuff @@ -620,6 +611,7 @@ func (d *controllerService) CreateSnapshot(ctx context.Context, req *csi.CreateS // DeleteSnapshot deletes the given snapshot func (d *controllerService) DeleteSnapshot(ctx context.Context, req *csi.DeleteSnapshotRequest) (*csi.DeleteSnapshotResponse, error) { + klog.V(4).Infof("DeleteSnapshot called with %+v", *req) snapshotID, snapshotZone, err := getSnapshotIDAndZone(req.GetSnapshotId()) if err != nil { return nil, err @@ -630,12 +622,11 @@ func (d *controllerService) DeleteSnapshot(ctx context.Context, req *csi.DeleteS Zone: snapshotZone, }) if err != nil { - if ferr, ok := err.(*scw.ResponseError); ok { - if ferr.StatusCode == 404 { - klog.V(4).Infof("snapshot with ID %s not found", snapshotID) - return &csi.DeleteSnapshotResponse{}, nil - } + if _, ok := err.(*scw.ResourceNotFoundError); ok { + klog.V(4).Infof("snapshot with ID %s not found", snapshotID) + return &csi.DeleteSnapshotResponse{}, nil } + return nil, status.Error(codes.Internal, err.Error()) } return &csi.DeleteSnapshotResponse{}, nil @@ -646,6 +637,7 @@ func (d *controllerService) DeleteSnapshot(ctx context.Context, req *csi.DeleteS // they were created. ListSnapshots SHALL NOT list a snapshot that // is being created but has not been cut successfully yet. func (d *controllerService) ListSnapshots(ctx context.Context, req *csi.ListSnapshotsRequest) (*csi.ListSnapshotsResponse, error) { + klog.V(4).Infof("ListSnapshots called with %+v", *req) var numberResults int var err error @@ -722,6 +714,69 @@ func (d *controllerService) ListSnapshots(ctx context.Context, req *csi.ListSnap // ControllerExpandVolume expands the given volume func (d *controllerService) ControllerExpandVolume(ctx context.Context, req *csi.ControllerExpandVolumeRequest) (*csi.ControllerExpandVolumeResponse, error) { - klog.V(4).Infof("ControllerExpandVolume is not yet implemented") - return nil, status.Error(codes.Unimplemented, "ControllerExpandVolume is not yet implemented") + klog.V(4).Infof("ControllerExpandVolume called with %+v", *req) + volumeID, volumeZone, err := getVolumeIDAndZone(req.GetVolumeId()) + if err != nil { + return nil, err + } + + nodeExpansionRequired := true + + volumeCapability := req.GetVolumeCapability() + if volumeCapability != nil { + err := validateVolumeCapability(volumeCapability) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "volumeCapabilities not supported: %s", err) + } + switch volumeCapability.GetAccessType().(type) { + case *csi.VolumeCapability_Block: + nodeExpansionRequired = false + } + } + + volumeResp, err := d.scaleway.GetVolume(&instance.GetVolumeRequest{ + VolumeID: volumeID, + Zone: volumeZone, + }) + if err != nil { + if _, ok := err.(*scw.ResourceNotFoundError); ok { + return nil, status.Errorf(codes.NotFound, "volume %s not found", volumeID) + } + return nil, status.Error(codes.Internal, err.Error()) + } + + minSize, maxSize, err := d.scaleway.GetVolumeLimits(string(volumeResp.Volume.VolumeType)) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + + newSize, err := getVolumeRequestCapacity(minSize, maxSize, req.GetCapacityRange()) + if err != nil { + return nil, status.Errorf(codes.OutOfRange, "capacityRange invalid: %s", err) + } + + if newSize < int64(volumeResp.Volume.Size) { + return nil, status.Error(codes.InvalidArgument, "the new size of the volume will be less than the actual size") + } + + _, err = d.scaleway.UpdateVolume(&instance.UpdateVolumeRequest{ + Zone: volumeZone, + VolumeID: volumeID, + Size: scw.SizePtr(scw.Size(newSize)), + }) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + + vol, err := d.scaleway.WaitForVolume(&instance.WaitForVolumeRequest{ + VolumeID: volumeID, + }) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + if vol.State != instance.VolumeStateAvailable { + return nil, status.Errorf(codes.Internal, "volume %s is in state %s", volumeID, vol.State) + } + + return &csi.ControllerExpandVolumeResponse{CapacityBytes: newSize, NodeExpansionRequired: nodeExpansionRequired}, nil } diff --git a/driver/diskutils.go b/driver/diskutils.go index 9f89f85..c49b761 100644 --- a/driver/diskutils.go +++ b/driver/diskutils.go @@ -52,6 +52,9 @@ type DiskUtils interface { // GetStatfs return the statfs struct for the given path GetStatfs(path string) (*unix.Statfs_t, error) + + // Resize resizes the given volumes + Resize(targetPath string, devicePath string) error } type diskUtils struct{} @@ -311,3 +314,29 @@ func (d *diskUtils) GetStatfs(path string) (*unix.Statfs_t, error) { err := unix.Statfs(path, fs) return fs, err } + +func (d *diskUtils) Resize(targetPath string, devicePath string) error { + mountInfo, err := d.GetMountInfo(targetPath) + if err != nil { + return err + } + + switch mountInfo.fsType { + case "ext3", "ext4": + resize2fsPath, err := exec.LookPath("resize2fs") + if err != nil { + return err + } + resize2fsArgs := []string{devicePath} + return exec.Command(resize2fsPath, resize2fsArgs...).Run() + case "xfs": + xfsGrowfsPath, err := exec.LookPath("xfs_growfs") + if err != nil { + return err + } + xfsGrowfsArgs := []string{"-d", targetPath} + return exec.Command(xfsGrowfsPath, xfsGrowfsArgs...).Run() + } + + return fmt.Errorf("filesystem %s does not support resizing", mountInfo.fsType) +} diff --git a/driver/helpers.go b/driver/helpers.go index dcb6dc4..e66bef1 100644 --- a/driver/helpers.go +++ b/driver/helpers.go @@ -6,7 +6,6 @@ import ( "strings" "github.com/container-storage-interface/spec/lib/go/csi" - "github.com/scaleway/scaleway-csi/scaleway" "github.com/scaleway/scaleway-sdk-go/scw" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -159,9 +158,9 @@ func validateVolumeCapability(volumeCapability *csi.VolumeCapability) error { return errAccessModeNotSupported } -func getVolumeRequestCapacity(capacityRange *csi.CapacityRange) (int64, error) { +func getVolumeRequestCapacity(minSize int64, maxSize int64, capacityRange *csi.CapacityRange) (int64, error) { if capacityRange == nil { - return scaleway.MinimumVolumeSizeInBytes, nil + return minSize, nil } requiredBytes := capacityRange.GetRequiredBytes() @@ -171,26 +170,26 @@ func getVolumeRequestCapacity(capacityRange *csi.CapacityRange) (int64, error) { limitBytesSet := limitBytes > 0 if !requiredBytesSet && !limitBytesSet { - return scaleway.MinimumVolumeSizeInBytes, nil + return minSize, nil } if requiredBytesSet && limitBytesSet && limitBytes < requiredBytes { return 0, errLimitBytesLessThanRequiredBytes } - if requiredBytesSet && !limitBytesSet && requiredBytes < scaleway.MinimumVolumeSizeInBytes { + if requiredBytesSet && !limitBytesSet && requiredBytes < minSize { return 0, errRequiredBytesLessThanMinimun } - if limitBytesSet && limitBytes < scaleway.MinimumVolumeSizeInBytes { + if limitBytesSet && limitBytes < minSize { return 0, errLimitBytesLessThanMinimum } - if requiredBytesSet && requiredBytes > scaleway.MaximumVolumeSizeInBytes { + if requiredBytesSet && requiredBytes > maxSize { return 0, errRequiredBytesGreaterThanMaximun } - if !requiredBytesSet && limitBytesSet && limitBytes > scaleway.MaximumVolumeSizeInBytes { + if !requiredBytesSet && limitBytesSet && limitBytes > maxSize { return 0, errLimitBytesGreaterThanMaximum } @@ -206,7 +205,7 @@ func getVolumeRequestCapacity(capacityRange *csi.CapacityRange) (int64, error) { return limitBytes, nil } - return scaleway.MinimumVolumeSizeInBytes, nil + return minSize, nil } func newAccessibleTopology(zone scw.Zone) []*csi.Topology { diff --git a/driver/helpers_test.go b/driver/helpers_test.go index f7d7579..7973d31 100644 --- a/driver/helpers_test.go +++ b/driver/helpers_test.go @@ -8,7 +8,6 @@ import ( "testing" "github.com/container-storage-interface/spec/lib/go/csi" - "github.com/scaleway/scaleway-csi/scaleway" "github.com/scaleway/scaleway-sdk-go/scw" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -547,6 +546,8 @@ func Test_validateVolumeCapabilities(t *testing.T) { } func Test_getVolumeRequestCapacity(t *testing.T) { + var min int64 = 1000 + var max int64 = 1000000000 testsBench := []struct { capRange *csi.CapacityRange res int64 @@ -557,28 +558,28 @@ func Test_getVolumeRequestCapacity(t *testing.T) { RequiredBytes: 0, LimitBytes: 0, }, - res: scaleway.MinimumVolumeSizeInBytes, + res: min, err: nil, }, { capRange: &csi.CapacityRange{ - RequiredBytes: scaleway.MinimumVolumeSizeInBytes + 10, + RequiredBytes: min + 10, LimitBytes: 0, }, - res: scaleway.MinimumVolumeSizeInBytes + 10, + res: min + 10, err: nil, }, { capRange: &csi.CapacityRange{ RequiredBytes: 0, - LimitBytes: scaleway.MinimumVolumeSizeInBytes + 10, + LimitBytes: min + 10, }, - res: scaleway.MinimumVolumeSizeInBytes + 10, + res: min + 10, err: nil, }, { capRange: &csi.CapacityRange{ - RequiredBytes: scaleway.MinimumVolumeSizeInBytes - 10, + RequiredBytes: min - 10, LimitBytes: 0, }, res: 0, @@ -587,30 +588,30 @@ func Test_getVolumeRequestCapacity(t *testing.T) { { capRange: &csi.CapacityRange{ RequiredBytes: 0, - LimitBytes: scaleway.MinimumVolumeSizeInBytes - 10, + LimitBytes: min - 10, }, res: 0, err: errLimitBytesLessThanMinimum, }, { capRange: &csi.CapacityRange{ - RequiredBytes: scaleway.MinimumVolumeSizeInBytes + 10, - LimitBytes: scaleway.MinimumVolumeSizeInBytes + 5, + RequiredBytes: min + 10, + LimitBytes: min + 5, }, res: 0, err: errLimitBytesLessThanRequiredBytes, }, { capRange: &csi.CapacityRange{ - RequiredBytes: scaleway.MinimumVolumeSizeInBytes + 10, - LimitBytes: scaleway.MinimumVolumeSizeInBytes + 5, + RequiredBytes: min + 10, + LimitBytes: min + 5, }, res: 0, err: errLimitBytesLessThanRequiredBytes, }, { capRange: &csi.CapacityRange{ - RequiredBytes: scaleway.MaximumVolumeSizeInBytes + 10, + RequiredBytes: max + 10, LimitBytes: 0, }, res: 0, @@ -619,31 +620,31 @@ func Test_getVolumeRequestCapacity(t *testing.T) { { capRange: &csi.CapacityRange{ RequiredBytes: 0, - LimitBytes: scaleway.MaximumVolumeSizeInBytes + 10, + LimitBytes: max + 10, }, res: 0, err: errLimitBytesGreaterThanMaximum, }, { capRange: &csi.CapacityRange{ - RequiredBytes: scaleway.MinimumVolumeSizeInBytes + 10, - LimitBytes: scaleway.MinimumVolumeSizeInBytes + 10, + RequiredBytes: min + 10, + LimitBytes: min + 10, }, - res: scaleway.MinimumVolumeSizeInBytes + 10, + res: min + 10, err: nil, }, { capRange: &csi.CapacityRange{ - RequiredBytes: scaleway.MinimumVolumeSizeInBytes + 10, - LimitBytes: scaleway.MinimumVolumeSizeInBytes + 20, + RequiredBytes: min + 10, + LimitBytes: min + 20, }, - res: scaleway.MinimumVolumeSizeInBytes + 10, + res: min + 10, err: nil, }, } for _, test := range testsBench { - res, err := getVolumeRequestCapacity(test.capRange) + res, err := getVolumeRequestCapacity(min, max, test.capRange) Equals(t, test.err, err) Equals(t, test.res, res) } diff --git a/driver/identity.go b/driver/identity.go index 49e451d..7f4c74c 100644 --- a/driver/identity.go +++ b/driver/identity.go @@ -37,6 +37,13 @@ func (d *Driver) GetPluginCapabilities(ctx context.Context, req *csi.GetPluginCa }, }, }, + { + Type: &csi.PluginCapability_VolumeExpansion_{ + VolumeExpansion: &csi.PluginCapability_VolumeExpansion{ + Type: csi.PluginCapability_VolumeExpansion_ONLINE, + }, + }, + }, }, } diff --git a/driver/node.go b/driver/node.go index 4fb9b78..67ca075 100644 --- a/driver/node.go +++ b/driver/node.go @@ -242,6 +242,9 @@ func (d *nodeService) NodePublishVolume(ctx context.Context, req *csi.NodePublis if isMounted { blockDevice, err := d.diskUtils.IsBlockDevice(targetPath) + if err != nil { + return nil, status.Errorf(codes.Internal, "error checking stat for %s: %s", targetPath, err.Error()) + } if blockDevice && volumeCapability.GetMount() != nil || !blockDevice && volumeCapability.GetBlock() != nil { return nil, status.Error(codes.AlreadyExists, "cannot change volumeCapability type") } @@ -461,6 +464,13 @@ func (d *nodeService) NodeGetCapabilities(ctx context.Context, req *csi.NodeGetC }, }, }, + &csi.NodeServiceCapability{ + Type: &csi.NodeServiceCapability_Rpc{ + Rpc: &csi.NodeServiceCapability_RPC{ + Type: csi.NodeServiceCapability_RPC_EXPAND_VOLUME, + }, + }, + }, }, }, nil } @@ -480,5 +490,52 @@ func (d *nodeService) NodeGetInfo(ctx context.Context, req *csi.NodeGetInfoReque // NodeExpandVolume expands the given volume func (d *nodeService) NodeExpandVolume(ctx context.Context, req *csi.NodeExpandVolumeRequest) (*csi.NodeExpandVolumeResponse, error) { - return nil, status.Error(codes.Unimplemented, "NodeExpandVolume is not implemented yet") + klog.V(4).Infof("NodeExpandVolume called with %+v", *req) + volumeID, _, err := getVolumeIDAndZone(req.GetVolumeId()) + if err != nil { + return nil, err + } + + volumePath := req.GetVolumePath() + if volumePath == "" { + return nil, status.Error(codes.InvalidArgument, "volumePath not provided") + } + + devicePath, err := d.diskUtils.GetDevicePath(volumeID) + if err != nil { + if os.IsNotExist(err) { + return nil, status.Errorf(codes.NotFound, "volume %s is not mounted on node", volumeID) + } + return nil, status.Errorf(codes.Internal, "failed to get device path for volume %s: %v", volumeID, err) + } + + isBlock, err := d.diskUtils.IsBlockDevice(volumePath) + if err != nil { + return nil, status.Errorf(codes.Internal, "error checking stat for %s: %s", devicePath, err.Error()) + } + + volumeCapability := req.GetVolumeCapability() + if volumeCapability != nil { + err = validateVolumeCapabilities([]*csi.VolumeCapability{volumeCapability}) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "volumeCapability not supported: %s", err) + } + + switch volumeCapability.GetAccessType().(type) { + case *csi.VolumeCapability_Block: + isBlock = true + } + } + + // no need to resize if it's in block mode + if isBlock { + return &csi.NodeExpandVolumeResponse{}, nil + } + + err = d.diskUtils.Resize(volumePath, devicePath) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to resize volume %s mounted on %s: %v", volumeID, volumePath, err) + } + + return &csi.NodeExpandVolumeResponse{}, nil } diff --git a/driver/sanity_test.go b/driver/sanity_test.go index 52d8011..849eb17 100644 --- a/driver/sanity_test.go +++ b/driver/sanity_test.go @@ -11,7 +11,7 @@ import ( "time" "github.com/google/uuid" - "github.com/kubernetes-csi/csi-test/pkg/sanity" + "github.com/kubernetes-csi/csi-test/v3/pkg/sanity" "github.com/scaleway/scaleway-csi/scaleway" "github.com/scaleway/scaleway-sdk-go/api/instance/v1" "github.com/scaleway/scaleway-sdk-go/scw" @@ -19,15 +19,25 @@ import ( utilsio "k8s.io/utils/io" ) +type fakeHelper struct { + fakeDiskUtils + fakeInstanceAPI +} + func TestSanityCSI(t *testing.T) { endpoint := "/tmp/csi-testing.sock" nodeID := "fb094b6a-a732-4d5f-8283-bd6726ff5938" volumesMap := make(map[string]*instance.Volume) serversMap := map[string]*instance.Server{ nodeID: &instance.Server{ - ID: nodeID, - Volumes: make(map[string]*instance.Volume), - Zone: scw.ZoneFrPar1, + ID: nodeID, + Volumes: map[string]*instance.Volume{"fb094b6a-b73b-4d5f-8283-bd6726ff5938": { + ID: "fb094b6a-b73b-4d5f-8283-bd6726ff5938", + VolumeType: instance.VolumeVolumeTypeLSSD, + Zone: scw.ZoneFrPar1, + Name: "local", + }}, + Zone: scw.ZoneFrPar1, }, } snapshotsMap := make(map[string]*instance.Snapshot) @@ -37,42 +47,46 @@ func TestSanityCSI(t *testing.T) { Endpoint: fmt.Sprintf("unix://%s", endpoint), Mode: AllMode, } + fakeInstance := &fakeInstanceAPI{ + volumesMap: volumesMap, + serversMap: serversMap, + snapshotsMap: snapshotsMap, + defaultZone: scw.ZoneFrPar1, + } + fakeDiskUtils := &fakeDiskUtils{ + devices: diskUtilsDevices, + } + fakeHelper := &fakeHelper{ + fakeDiskUtils: *fakeDiskUtils, + fakeInstanceAPI: *fakeInstance, + } driver := &Driver{ config: driverConfig, controllerService: controllerService{ scaleway: &scaleway.Scaleway{ - InstanceAPI: &fakeInstanceAPI{ - volumesMap: volumesMap, - serversMap: serversMap, - snapshotsMap: snapshotsMap, - defaultZone: scw.ZoneFrPar1, - }, + InstanceAPI: fakeHelper, }, config: driverConfig, }, nodeService: nodeService{ - nodeID: nodeID, - nodeZone: scw.ZoneFrPar1, - diskUtils: &fakeDiskUtils{ - devices: diskUtilsDevices, - }, + nodeID: nodeID, + nodeZone: scw.ZoneFrPar1, + diskUtils: fakeHelper, }, } go driver.Run() // an error here woule fail the test anyway since the grpc server would not be started - config := &sanity.Config{ - TargetPath: os.TempDir() + "/csi-testing-target", - StagingPath: os.TempDir() + "/csi-testing-staging", - RemoveTargetPath: func(path string) error { - return os.RemoveAll(path) - }, - RemoveStagingPath: func(path string) error { - return os.RemoveAll(path) - }, - Address: endpoint, - IDGen: sanity.DefaultIDGenerator{}, + config := sanity.NewTestConfig() + config.Address = endpoint + config.TestNodeVolumeAttachLimit = true + config.TestVolumeExpandSize = config.TestVolumeSize * 2 + config.RemoveTargetPath = func(path string) error { + return os.RemoveAll(path) + } + config.RemoveStagingPath = func(path string) error { + return os.RemoveAll(path) } sanity.Test(t, config) driver.srv.GracefulStop() @@ -86,7 +100,20 @@ type fakeInstanceAPI struct { defaultZone scw.Zone } -func (s *fakeInstanceAPI) ListVolumes(req *instance.ListVolumesRequest, opts ...scw.RequestOption) (*instance.ListVolumesResponse, error) { +func (s *fakeHelper) ListVolumesTypes(req *instance.ListVolumesTypesRequest, opts ...scw.RequestOption) (*instance.ListVolumesTypesResponse, error) { + return &instance.ListVolumesTypesResponse{ + Volumes: map[string]*instance.VolumeType{ + instance.VolumeVolumeTypeBSSD.String(): { + Constraints: &instance.VolumeTypeConstraints{ + Max: 10 * 1000 * 1000 * 1000 * 1000, + Min: 1 * 1000 * 1000 * 1000, + }, + }, + }, + }, nil +} + +func (s *fakeHelper) ListVolumes(req *instance.ListVolumesRequest, opts ...scw.RequestOption) (*instance.ListVolumesResponse, error) { volumes := make([]*instance.Volume, 0) for _, v := range s.volumesMap { if req.Name == nil || strings.Contains(v.Name, *req.Name) { @@ -96,7 +123,7 @@ func (s *fakeInstanceAPI) ListVolumes(req *instance.ListVolumesRequest, opts ... return &instance.ListVolumesResponse{Volumes: volumes, TotalCount: uint32(len(volumes))}, nil } -func (s *fakeInstanceAPI) CreateVolume(req *instance.CreateVolumeRequest, opts ...scw.RequestOption) (*instance.CreateVolumeResponse, error) { +func (s *fakeHelper) CreateVolume(req *instance.CreateVolumeRequest, opts ...scw.RequestOption) (*instance.CreateVolumeResponse, error) { if req.Zone == "" { req.Zone = s.defaultZone } @@ -109,13 +136,13 @@ func (s *fakeInstanceAPI) CreateVolume(req *instance.CreateVolumeRequest, opts . } else if req.BaseVolume != nil { baseVol, ok := s.volumesMap[*req.BaseVolume] if !ok { - return nil, &scw.ResponseError{StatusCode: 404} + return nil, &scw.ResourceNotFoundError{} } volume.Size = baseVol.Size } else if req.BaseSnapshot != nil { baseSnap, ok := s.snapshotsMap[*req.BaseSnapshot] if !ok { - return nil, &scw.ResponseError{StatusCode: 404} + return nil, &scw.ResourceNotFoundError{} } volume.Size = baseSnap.Size } else { @@ -128,63 +155,106 @@ func (s *fakeInstanceAPI) CreateVolume(req *instance.CreateVolumeRequest, opts . return &instance.CreateVolumeResponse{Volume: volume}, nil } -func (s *fakeInstanceAPI) GetVolume(req *instance.GetVolumeRequest, opts ...scw.RequestOption) (*instance.GetVolumeResponse, error) { +func (s *fakeHelper) GetVolume(req *instance.GetVolumeRequest, opts ...scw.RequestOption) (*instance.GetVolumeResponse, error) { if vol, ok := s.volumesMap[req.VolumeID]; ok { return &instance.GetVolumeResponse{Volume: vol}, nil } - return nil, &scw.ResponseError{StatusCode: 404} + return nil, &scw.ResourceNotFoundError{} } -func (s *fakeInstanceAPI) DeleteVolume(req *instance.DeleteVolumeRequest, opts ...scw.RequestOption) error { +func (s *fakeHelper) UpdateVolume(req *instance.UpdateVolumeRequest, opts ...scw.RequestOption) (*instance.UpdateVolumeResponse, error) { + vol, ok := s.volumesMap[req.VolumeID] + if !ok { + return nil, &scw.ResourceNotFoundError{} + } + + if req.Name != nil { + vol.Name = *req.Name + } + // TODO add size + return &instance.UpdateVolumeResponse{ + Volume: vol, + }, nil +} + +func (s *fakeHelper) DeleteVolume(req *instance.DeleteVolumeRequest, opts ...scw.RequestOption) error { if _, ok := s.volumesMap[req.VolumeID]; ok { delete(s.volumesMap, req.VolumeID) return nil } - return &scw.ResponseError{StatusCode: 404} + return &scw.ResourceNotFoundError{} } -func (s *fakeInstanceAPI) GetServer(req *instance.GetServerRequest, opts ...scw.RequestOption) (*instance.GetServerResponse, error) { +func (s *fakeHelper) GetServer(req *instance.GetServerRequest, opts ...scw.RequestOption) (*instance.GetServerResponse, error) { if srv, ok := s.serversMap[req.ServerID]; ok { return &instance.GetServerResponse{Server: srv}, nil } - return nil, &scw.ResponseError{StatusCode: 404} + return nil, &scw.ResourceNotFoundError{} } -func (s *fakeInstanceAPI) AttachVolume(req *instance.AttachVolumeRequest, opts ...scw.RequestOption) (*instance.AttachVolumeResponse, error) { +func (s *fakeHelper) AttachVolume(req *instance.AttachVolumeRequest, opts ...scw.RequestOption) (*instance.AttachVolumeResponse, error) { if vol, ok := s.volumesMap[req.VolumeID]; ok { if srv, ok := s.serversMap[req.ServerID]; ok { for i := 0; i <= len(srv.Volumes); i++ { key := fmt.Sprintf("%d", i) + if existingVol, ok := srv.Volumes[key]; ok && existingVol.ID == vol.ID { + break + } if _, ok := srv.Volumes[key]; !ok { + vol.Server = &instance.ServerSummary{ + ID: req.ServerID, + } srv.Volumes[key] = vol break } } // an empty slot will always be found + s.devices[path.Join(diskByIDPath, diskSCWPrefix+req.VolumeID)] = &mountpoint{ + block: true, + } return &instance.AttachVolumeResponse{Server: srv}, nil } } - return nil, &scw.ResponseError{StatusCode: 404} + return nil, &scw.ResourceNotFoundError{} } -func (s *fakeInstanceAPI) DetachVolume(req *instance.DetachVolumeRequest, opts ...scw.RequestOption) (*instance.DetachVolumeResponse, error) { - if _, ok := s.volumesMap[req.VolumeID]; ok { +func (s *fakeHelper) DetachVolume(req *instance.DetachVolumeRequest, opts ...scw.RequestOption) (*instance.DetachVolumeResponse, error) { + if vol, ok := s.volumesMap[req.VolumeID]; ok { delete(s.volumesMap, req.VolumeID) + delete(s.devices, path.Join(diskByIDPath, diskSCWPrefix+req.VolumeID)) + + if srv, ok := s.serversMap[vol.Server.ID]; ok { + for i := 0; i <= len(srv.Volumes); i++ { + key := fmt.Sprintf("%d", i) + if v, ok := srv.Volumes[key]; ok && v.ID == req.VolumeID { + delete(srv.Volumes, key) + break + } + } // an empty slot will always be found + } + return &instance.DetachVolumeResponse{}, nil } - return nil, &scw.ResponseError{StatusCode: 404} + return nil, &scw.ResourceNotFoundError{} +} + +func (s *fakeHelper) WaitForVolume(req *instance.WaitForVolumeRequest) (*instance.Volume, error) { + if vol, ok := s.volumesMap[req.VolumeID]; ok { + return vol, nil + } + return nil, &scw.ResourceNotFoundError{} } -func (s *fakeInstanceAPI) GetSnapshot(req *instance.GetSnapshotRequest, opts ...scw.RequestOption) (*instance.GetSnapshotResponse, error) { +func (s *fakeHelper) GetSnapshot(req *instance.GetSnapshotRequest, opts ...scw.RequestOption) (*instance.GetSnapshotResponse, error) { snapshot, ok := s.snapshotsMap[req.SnapshotID] if !ok { - return nil, &scw.ResponseError{StatusCode: 404} + return nil, &scw.ResourceNotFoundError{} } return &instance.GetSnapshotResponse{ Snapshot: snapshot, }, nil } -func (s *fakeInstanceAPI) ListSnapshots(req *instance.ListSnapshotsRequest, opts ...scw.RequestOption) (*instance.ListSnapshotsResponse, error) { +func (s *fakeHelper) ListSnapshots(req *instance.ListSnapshotsRequest, opts ...scw.RequestOption) (*instance.ListSnapshotsResponse, error) { snapshots := make([]*instance.Snapshot, 0) for _, snap := range s.snapshotsMap { if req.Name == nil || strings.Contains(snap.Name, *req.Name) { @@ -197,14 +267,14 @@ func (s *fakeInstanceAPI) ListSnapshots(req *instance.ListSnapshotsRequest, opts return &instance.ListSnapshotsResponse{Snapshots: snapshots, TotalCount: uint32(len(snapshots))}, nil } -func (s *fakeInstanceAPI) CreateSnapshot(req *instance.CreateSnapshotRequest, opts ...scw.RequestOption) (*instance.CreateSnapshotResponse, error) { +func (s *fakeHelper) CreateSnapshot(req *instance.CreateSnapshotRequest, opts ...scw.RequestOption) (*instance.CreateSnapshotResponse, error) { if req.Zone == "" { req.Zone = s.defaultZone } volume, ok := s.volumesMap[req.VolumeID] if !ok { - return nil, &scw.ResponseError{StatusCode: 404} + return nil, &scw.ResourceNotFoundError{} } snapshot := &instance.Snapshot{} snapshot.ID = uuid.New().String() @@ -225,16 +295,16 @@ func (s *fakeInstanceAPI) CreateSnapshot(req *instance.CreateSnapshotRequest, op }, nil } -func (s *fakeInstanceAPI) DeleteSnapshot(req *instance.DeleteSnapshotRequest, opts ...scw.RequestOption) error { +func (s *fakeHelper) DeleteSnapshot(req *instance.DeleteSnapshotRequest, opts ...scw.RequestOption) error { if _, ok := s.snapshotsMap[req.SnapshotID]; ok { delete(s.snapshotsMap, req.SnapshotID) return nil } - return &scw.ResponseError{StatusCode: 404} + return &scw.ResourceNotFoundError{} } type mountpoint struct { - path string + targetPath string fsType string mountOptions []string block bool @@ -244,17 +314,28 @@ type fakeDiskUtils struct { devices map[string]*mountpoint } -func (d *fakeDiskUtils) FormatAndMount(targetPath string, devicePath string, fsType string, mountOptions []string) error { - return d.MountToTarget(devicePath, targetPath, fsType, mountOptions) +// FormatAndMount is only used for non block devices +func (s *fakeHelper) FormatAndMount(targetPath string, devicePath string, fsType string, mountOptions []string) error { + if fsType == "" { + fsType = defaultFSType + } + + s.devices[devicePath] = &mountpoint{ + targetPath: targetPath, + fsType: fsType, + mountOptions: mountOptions, + block: false, + } + return nil } -func (d *fakeDiskUtils) MountToTarget(sourcePath, targetPath, fsType string, mountOptions []string) error { +func (s *fakeHelper) MountToTarget(sourcePath, targetPath, fsType string, mountOptions []string) error { if fsType == "" { fsType = defaultFSType } - d.devices[targetPath] = &mountpoint{ - path: sourcePath, + s.devices[sourcePath] = &mountpoint{ + targetPath: targetPath, fsType: fsType, mountOptions: mountOptions, block: strings.HasPrefix(sourcePath, diskByIDPath), @@ -262,7 +343,7 @@ func (d *fakeDiskUtils) MountToTarget(sourcePath, targetPath, fsType string, mou return nil } -func (d *fakeDiskUtils) getDeviceType(devicePath string) (string, error) { +func (s *fakeHelper) getDeviceType(devicePath string) (string, error) { blkidPath, err := exec.LookPath("blkid") if err != nil { return "", err @@ -300,23 +381,33 @@ func (d *fakeDiskUtils) getDeviceType(devicePath string) (string, error) { return "", nil } -func (d *fakeDiskUtils) GetDevicePath(volumeID string) (string, error) { - return path.Join(diskByIDPath, diskSCWPrefix+volumeID), nil +func (s *fakeHelper) GetDevicePath(volumeID string) (string, error) { + if _, ok := s.devices[path.Join(diskByIDPath, diskSCWPrefix+volumeID)]; ok { + return path.Join(diskByIDPath, diskSCWPrefix+volumeID), nil + } + + return "", os.ErrNotExist } -func (d *fakeDiskUtils) IsSharedMounted(targetPath string, devicePath string) (bool, error) { +func (s *fakeHelper) IsSharedMounted(targetPath string, devicePath string) (bool, error) { if targetPath == "" { return false, errTargetPathEmpty } - if _, ok := d.devices[targetPath]; ok { - return true, nil + if d, ok := s.devices[devicePath]; ok { + return d.targetPath == targetPath, nil + } + + for _, tp := range s.devices { + if tp.targetPath == targetPath { + return true, nil + } } return false, nil } // taken from https://github.com/kubernetes/kubernetes/blob/master/pkg/util/mount/mount_linux.go -func (d *fakeDiskUtils) GetMountInfo(targetPath string) (*mountInfo, error) { +func (s *fakeHelper) GetMountInfo(targetPath string) (*mountInfo, error) { content, err := utilsio.ConsistentRead(procMountInfoPath, procMountInfoMaxListTries) if err != nil { return &mountInfo{}, err @@ -370,16 +461,16 @@ func (d *fakeDiskUtils) GetMountInfo(targetPath string) (*mountInfo, error) { return &mountInfo{}, nil } -func (d *fakeDiskUtils) IsBlockDevice(path string) (bool, error) { - for _, mp := range d.devices { - if mp.path == path { +func (s *fakeHelper) IsBlockDevice(path string) (bool, error) { + for _, mp := range s.devices { + if mp.targetPath == path { return mp.block, nil } } return false, fmt.Errorf("not found") // enough for csi sanity? } -func (d *fakeDiskUtils) GetStatfs(path string) (*unix.Statfs_t, error) { +func (s *fakeHelper) GetStatfs(path string) (*unix.Statfs_t, error) { return &unix.Statfs_t{ Blocks: 1000, Bsize: 4, @@ -388,3 +479,19 @@ func (d *fakeDiskUtils) GetStatfs(path string) (*unix.Statfs_t, error) { Ffree: 500, }, nil } + +func (s *fakeHelper) Resize(targetPath string, devicePath string) error { + mountInfo, err := s.GetMountInfo(targetPath) + if err != nil { + return err + } + + switch mountInfo.fsType { + case "ext3", "ext4": + return nil + case "xfs": + return nil + } + + return fmt.Errorf("filesystem %s does not support resizing", mountInfo.fsType) +} diff --git a/go.mod b/go.mod index 6ad05d7..9beaae6 100644 --- a/go.mod +++ b/go.mod @@ -7,12 +7,10 @@ require ( github.com/docker/docker v1.13.1 github.com/golang/protobuf v1.3.2 github.com/google/uuid v1.1.1 - github.com/kubernetes-csi/csi-test v2.2.0+incompatible - github.com/onsi/ginkgo v1.10.3 // indirect - github.com/onsi/gomega v1.7.1 // indirect - github.com/scaleway/scaleway-sdk-go v1.0.0-beta.6 - golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a - google.golang.org/grpc v1.21.1 + github.com/kubernetes-csi/csi-test/v3 v3.1.0 + github.com/scaleway/scaleway-sdk-go v1.0.0-beta.6.0.20200608175253-d0adc17352c5 + golang.org/x/sys v0.0.0-20191113165036-4c7a9d0fe056 + google.golang.org/grpc v1.25.1 k8s.io/klog v1.0.0 k8s.io/utils v0.0.0-20190607212802-c55fbcfc754a ) diff --git a/go.sum b/go.sum index c57e6d0..5587e79 100644 --- a/go.sum +++ b/go.sum @@ -1,58 +1,113 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/container-storage-interface/spec v1.2.0 h1:bD9KIVgaVKKkQ/UbVUY9kCaH/CJbhNxe0eeB4JeJV2s= github.com/container-storage-interface/spec v1.2.0/go.mod h1:6URME8mwIBbpVyZV93Ce5St17xBiQJQY67NDsuohiy4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dnaeon/go-vcr v1.0.1 h1:r8L/HqC0Hje5AXMu1ooW8oyQyOFv4GxqpL0nRP7SLLY= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= github.com/docker/docker v1.13.1 h1:IkZjBSIc8hBjLpqeAbeE5mca5mNgeatLHBy3GO78BWo= github.com/docker/docker v1.13.1/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/kubernetes-csi/csi-test v2.2.0+incompatible/go.mod h1:YxJ4UiuPWIhMBkxUKY5c267DyA0uDZ/MtAimhx/2TA0= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kubernetes-csi/csi-test/v3 v3.1.0 h1:XPiXQgEhDV9y+6vHAklH273PKA/ocqQdC8R+WcIKZKw= +github.com/kubernetes-csi/csi-test/v3 v3.1.0/go.mod h1:UWxYP5cDlD6iSNVKEiLFqfJnJinuhtI7MLt61rQQOfI= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.3 h1:OoxbjfXVZyod1fmWYhI7SEyaD8B00ynP3T+D5GiyHOY= github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.7.1 h1:K0jcRCwNQM3vFGh1ppMtDh/+7ApJrjldlX8fA0jDTLQ= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/scaleway/scaleway-sdk-go v1.0.0-beta.6 h1:C1/pvkxkGN/H03mDxLzItaceYJDBk1HdClgR15suAzI= -github.com/scaleway/scaleway-sdk-go v1.0.0-beta.6/go.mod h1:CJJ5VAbozOl0yEw7nHB9+7BXTJbIn6h7W+f6Gau5IP8= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/robertkrimen/otto v0.0.0-20191219234010-c382bd3c16ff/go.mod h1:xvqspoSXJTIpemEonrMDFq6XzwHYYgToXWj5eRX1OtY= +github.com/scaleway/scaleway-sdk-go v1.0.0-beta.6.0.20200608155405-2c0edbf44628 h1:7sBvEdHVZ+IkOAVh6rizkyQOnCz4xQ5V/qMXqtKF36I= +github.com/scaleway/scaleway-sdk-go v1.0.0-beta.6.0.20200608155405-2c0edbf44628/go.mod h1:CJJ5VAbozOl0yEw7nHB9+7BXTJbIn6h7W+f6Gau5IP8= +github.com/scaleway/scaleway-sdk-go v1.0.0-beta.6.0.20200608175253-d0adc17352c5 h1:EUEuEITdL9RtVnl0YEzrETOx633hkY7pB6z3ub+6qLE= +github.com/scaleway/scaleway-sdk-go v1.0.0-beta.6.0.20200608175253-d0adc17352c5/go.mod h1:CJJ5VAbozOl0yEw7nHB9+7BXTJbIn6h7W+f6Gau5IP8= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20191112182307-2180aed22343 h1:00ohfJ4K98s3m6BGUoBd8nyfp4Yl0GoIKvw5abItTjI= +golang.org/x/net v0.0.0-20191112182307-2180aed22343/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191113165036-4c7a9d0fe056 h1:dHtDnRWQtSx0Hjq9kvKFpBh9uPPKfQN70NZZmvssGwk= +golang.org/x/sys v0.0.0-20191113165036-4c7a9d0fe056/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/grpc v1.21.1 h1:j6XxA85m/6txkUCHvzlV5f+HBNl/1r5cZ2A/3IEFOO8= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20191114150713-6bbd007550de h1:dFEMUWudT9iV1JMk6i6NwbfIw2V/2VDFyDYCZFypRxE= +google.golang.org/genproto v0.0.0-20191114150713-6bbd007550de/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1 h1:wdKvqQk7IttEw92GoRyKG2IDrUIpgpj6H6m81yfeMW0= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= diff --git a/scaleway/scaleway.go b/scaleway/scaleway.go index 83a36b0..dac9a4b 100644 --- a/scaleway/scaleway.go +++ b/scaleway/scaleway.go @@ -2,21 +2,18 @@ package scaleway import ( "errors" + "fmt" "github.com/scaleway/scaleway-sdk-go/api/instance/v1" "github.com/scaleway/scaleway-sdk-go/scw" ) const ( - // MinimumVolumeSizeInBytes represents the size of the smallest block volume on Scaleway - MinimumVolumeSizeInBytes int64 = 1 * 1000 * 1000 * 1000 - // MaximumVolumeSizeInBytes represents the size of the biggest block volume on Scaleway - MaximumVolumeSizeInBytes int64 = 10 * 1000 * 1000 * 1000 * 1000 // MaxVolumesPerNode represents the number max of volumes attached to one node MaxVolumesPerNode = 16 // DefaultVolumeType is the default type for Scaleway Block volumes - DefaultVolumeType = instance.VolumeTypeBSSD + DefaultVolumeType = instance.VolumeVolumeTypeBSSD ) var ( @@ -80,12 +77,18 @@ type InstanceAPI interface { // GetServer is an interface for the SDK GetServer method GetServer(req *instance.GetServerRequest, opts ...scw.RequestOption) (*instance.GetServerResponse, error) + // UpdateVolume is an interface for the SDK UpdateVolume method + UpdateVolume(req *instance.UpdateVolumeRequest, opts ...scw.RequestOption) (*instance.UpdateVolumeResponse, error) + // AttachVolume is an interface for the SDK AttachVolume method AttachVolume(req *instance.AttachVolumeRequest, opts ...scw.RequestOption) (*instance.AttachVolumeResponse, error) // DetachVolume is an interface for the SDK DetachVolume method DetachVolume(req *instance.DetachVolumeRequest, opts ...scw.RequestOption) (*instance.DetachVolumeResponse, error) + // WaitForVolume is an interface for the SDK WaitForVolume method + WaitForVolume(req *instance.WaitForVolumeRequest) (*instance.Volume, error) + // GetSnapshot is an interface for the SDK GetSnapshot method GetSnapshot(req *instance.GetSnapshotRequest, opts ...scw.RequestOption) (*instance.GetSnapshotResponse, error) @@ -97,13 +100,29 @@ type InstanceAPI interface { // DeleteSnapshot is an interface for the SDK CreateSnapshot method DeleteSnapshot(req *instance.DeleteSnapshotRequest, opts ...scw.RequestOption) error + + // ListVolumesTypes is an interface for the SDK ListVolumesTypes method + ListVolumesTypes(req *instance.ListVolumesTypesRequest, opts ...scw.RequestOption) (*instance.ListVolumesTypesResponse, error) +} + +func (s *Scaleway) GetVolumeLimits(volumeType string) (int64, int64, error) { + volumeTypes, err := s.ListVolumesTypes(&instance.ListVolumesTypesRequest{}) + if err != nil { + return 0, 0, err + } + + if spec, ok := volumeTypes.Volumes[volumeType]; ok && spec.Constraints != nil { + return int64(spec.Constraints.Min), int64(spec.Constraints.Max), nil + } + + return 0, 0, fmt.Errorf("volume type %s not found", volumeType) } // GetVolumeByName is a helper to find a volume by it's name, type and given size -func (s *Scaleway) GetVolumeByName(name string, size int64, volumeType instance.VolumeType) (*instance.Volume, error) { +func (s *Scaleway) GetVolumeByName(name string, size int64, volumeType instance.VolumeVolumeType) (*instance.Volume, error) { volumesResp, err := s.ListVolumes(&instance.ListVolumesRequest{ Name: &name, - VolumeType: volumeType, + VolumeType: &volumeType, }, scw.WithAllPages()) if err != nil { return nil, err