From 66027353532c61c093931653676c4473629fe237 Mon Sep 17 00:00:00 2001 From: Alexander Ding Date: Mon, 10 Oct 2022 22:08:28 +0000 Subject: [PATCH 1/2] feat: migrate volume API group to library model --- integrationtests/utils.go | 11 + integrationtests/volume_test.go | 387 +++++++++++++++++++--- integrationtests/volume_v2alpha1_test.go | 12 +- pkg/volume/api/api.go | 388 +++++++++++++++++++++++ pkg/volume/types.go | 116 +++++++ pkg/volume/volume.go | 316 ++++++++++++++++++ pkg/volume/volume_test.go | 149 +++++++++ 7 files changed, 1325 insertions(+), 54 deletions(-) create mode 100644 pkg/volume/api/api.go create mode 100644 pkg/volume/types.go create mode 100644 pkg/volume/volume.go create mode 100644 pkg/volume/volume_test.go diff --git a/integrationtests/utils.go b/integrationtests/utils.go index 7a516992d..714ceb3fb 100644 --- a/integrationtests/utils.go +++ b/integrationtests/utils.go @@ -274,6 +274,17 @@ func diskInit(t *testing.T) (*VirtualHardDisk, func()) { return vhd, cleanup } +// sizeIsAround returns true if the actual size is around the expected size +// (considers the fact that some bytes were lost) +func sizeIsAround(t *testing.T, actualSize, expectedSize int64) bool { + // An upper bound on the number of bytes that are lost when creating or resizing a partition + var volumeSizeBytesLoss int64 = (20 * 1024 * 1024) + var lowerBound = expectedSize - volumeSizeBytesLoss + var upperBound = expectedSize + t.Logf("Checking that the size is inside the bounds: %d < (actual) %d < %d", lowerBound, actualSize, upperBound) + return lowerBound <= actualSize && actualSize <= upperBound +} + func pathExists(path string) (bool, error) { _, err := os.Stat(path) if err == nil { diff --git a/integrationtests/volume_test.go b/integrationtests/volume_test.go index afd99b70c..1ffc85951 100644 --- a/integrationtests/volume_test.go +++ b/integrationtests/volume_test.go @@ -2,14 +2,22 @@ package integrationtests import ( "context" + "fmt" + "os" + "path/filepath" + "strings" "testing" - "github.com/kubernetes-csi/csi-proxy/client/api/volume/v1beta3" - v1beta3client "github.com/kubernetes-csi/csi-proxy/client/groups/volume/v1beta3" + disk "github.com/kubernetes-csi/csi-proxy/pkg/disk" + diskapi "github.com/kubernetes-csi/csi-proxy/pkg/disk/api" + volume "github.com/kubernetes-csi/csi-proxy/pkg/volume" + volumeapi "github.com/kubernetes-csi/csi-proxy/pkg/volume/api" + + "github.com/stretchr/testify/require" ) -func runNegativeListVolumeRequest(t *testing.T, client *v1beta3client.Client, diskNum uint32) { - listRequest := &v1beta3.ListVolumesOnDiskRequest{ +func runNegativeListVolumeRequest(t *testing.T, client volume.Interface, diskNum uint32) { + listRequest := &volume.ListVolumesOnDiskRequest{ DiskNumber: diskNum, } _, err := client.ListVolumesOnDisk(context.TODO(), listRequest) @@ -18,8 +26,8 @@ func runNegativeListVolumeRequest(t *testing.T, client *v1beta3client.Client, di } } -func runNegativeIsVolumeFormattedRequest(t *testing.T, client *v1beta3client.Client, volumeID string) { - isVolumeFormattedRequest := &v1beta3.IsVolumeFormattedRequest{ +func runNegativeIsVolumeFormattedRequest(t *testing.T, client volume.Interface, volumeID string) { + isVolumeFormattedRequest := &volume.IsVolumeFormattedRequest{ VolumeId: volumeID, } _, err := client.IsVolumeFormatted(context.TODO(), isVolumeFormattedRequest) @@ -28,8 +36,8 @@ func runNegativeIsVolumeFormattedRequest(t *testing.T, client *v1beta3client.Cli } } -func runNegativeFormatVolumeRequest(t *testing.T, client *v1beta3client.Client, volumeID string) { - formatVolumeRequest := &v1beta3.FormatVolumeRequest{ +func runNegativeFormatVolumeRequest(t *testing.T, client volume.Interface, volumeID string) { + formatVolumeRequest := &volume.FormatVolumeRequest{ VolumeId: volumeID, } _, err := client.FormatVolume(context.TODO(), formatVolumeRequest) @@ -38,8 +46,8 @@ func runNegativeFormatVolumeRequest(t *testing.T, client *v1beta3client.Client, } } -func runNegativeResizeVolumeRequest(t *testing.T, client *v1beta3client.Client, volumeID string, size int64) { - resizeVolumeRequest := &v1beta3.ResizeVolumeRequest{ +func runNegativeResizeVolumeRequest(t *testing.T, client volume.Interface, volumeID string, size int64) { + resizeVolumeRequest := &volume.ResizeVolumeRequest{ VolumeId: volumeID, SizeBytes: size, } @@ -49,9 +57,9 @@ func runNegativeResizeVolumeRequest(t *testing.T, client *v1beta3client.Client, } } -func runNegativeMountVolumeRequest(t *testing.T, client *v1beta3client.Client, volumeID, targetPath string) { +func runNegativeMountVolumeRequest(t *testing.T, client volume.Interface, volumeID, targetPath string) { // Mount the volume - mountVolumeRequest := &v1beta3.MountVolumeRequest{ + mountVolumeRequest := &volume.MountVolumeRequest{ VolumeId: volumeID, TargetPath: targetPath, } @@ -62,9 +70,9 @@ func runNegativeMountVolumeRequest(t *testing.T, client *v1beta3client.Client, v } } -func runNegativeUnmountVolumeRequest(t *testing.T, client *v1beta3client.Client, volumeID, targetPath string) { +func runNegativeUnmountVolumeRequest(t *testing.T, client volume.Interface, volumeID, targetPath string) { // Unmount the volume - unmountVolumeRequest := &v1beta3.UnmountVolumeRequest{ + unmountVolumeRequest := &volume.UnmountVolumeRequest{ VolumeId: volumeID, TargetPath: targetPath, } @@ -74,9 +82,9 @@ func runNegativeUnmountVolumeRequest(t *testing.T, client *v1beta3client.Client, } } -func runNegativeVolumeStatsRequest(t *testing.T, client *v1beta3client.Client, volumeID string) { +func runNegativeVolumeStatsRequest(t *testing.T, client volume.Interface, volumeID string) { // Get VolumeStats - volumeStatsRequest := &v1beta3.GetVolumeStatsRequest{ + volumeStatsRequest := &volume.GetVolumeStatsRequest{ VolumeId: volumeID, } _, err := client.GetVolumeStats(context.TODO(), volumeStatsRequest) @@ -86,13 +94,8 @@ func runNegativeVolumeStatsRequest(t *testing.T, client *v1beta3client.Client, v } func negativeVolumeTests(t *testing.T) { - var client *v1beta3client.Client - var err error - - if client, err = v1beta3client.NewClient(); err != nil { - t.Fatalf("Client new error: %v", err) - } - defer client.Close() + client, err := volume.New(volumeapi.New()) + require.Nil(t, err) // Empty volume test runNegativeIsVolumeFormattedRequest(t, client, "") @@ -119,35 +122,12 @@ func negativeVolumeTests(t *testing.T) { runNegativeVolumeStatsRequest(t, client, "-1") } -// sizeIsAround returns true if the actual size is around the expected size -// (considers the fact that some bytes were lost) -func sizeIsAround(t *testing.T, actualSize, expectedSize int64) bool { - // An upper bound on the number of bytes that are lost when creating or resizing a partition - var volumeSizeBytesLoss int64 = (20 * 1024 * 1024) - var lowerBound = expectedSize - volumeSizeBytesLoss - var upperBound = expectedSize - t.Logf("Checking that the size is inside the bounds: %d < (actual) %d < %d", lowerBound, actualSize, upperBound) - return lowerBound <= actualSize && actualSize <= upperBound -} - func negativeDiskTests(t *testing.T) { - var client *v1beta3client.Client - var err error - - if client, err = v1beta3client.NewClient(); err != nil { - t.Fatalf("Client new error: %v", err) - } - defer client.Close() + _, err := volume.New(volumeapi.New()) + require.Nil(t, err) } func TestVolumeAPIs(t *testing.T) { - t.Run("NegativeDiskTests", func(t *testing.T) { - negativeDiskTests(t) - }) - t.Run("NegativeVolumeTests", func(t *testing.T) { - negativeVolumeTests(t) - }) - // TODO: These tests will fail on Github Actions because Hyper-V is disabled // see https://github.com/actions/virtual-environments/pull/2525 @@ -177,3 +157,314 @@ func TestVolumeAPIs(t *testing.T) { v2alpha1VolumeTests(t) }) } + +// volumeInit initializes a volume, it creates a VHD, initializes it, +// creates a partition with the max size and formats the volume corresponding to that partition +func volumeInit(volumeClient volume.Interface, t *testing.T) (*VirtualHardDisk, string, func()) { + vhd, vhdCleanup := diskInit(t) + + listRequest := &volume.ListVolumesOnDiskRequest{ + DiskNumber: vhd.DiskNumber, + } + listResponse, err := volumeClient.ListVolumesOnDisk(context.TODO(), listRequest) + if err != nil { + t.Fatalf("List response: %v", err) + } + + volumeIDsLen := len(listResponse.VolumeIds) + if volumeIDsLen != 1 { + t.Fatalf("Number of volumes not equal to 1: %d", volumeIDsLen) + } + volumeID := listResponse.VolumeIds[0] + t.Logf("VolumeId %v", volumeID) + + isVolumeFormattedRequest := &volume.IsVolumeFormattedRequest{ + VolumeId: volumeID, + } + isVolumeFormattedResponse, err := volumeClient.IsVolumeFormatted(context.TODO(), isVolumeFormattedRequest) + if err != nil { + t.Fatalf("Is volume formatted request error: %v", err) + } + if isVolumeFormattedResponse.Formatted { + t.Fatal("Volume formatted. Unexpected !!") + } + + formatVolumeRequest := &volume.FormatVolumeRequest{ + VolumeId: volumeID, + } + _, err = volumeClient.FormatVolume(context.TODO(), formatVolumeRequest) + if err != nil { + t.Fatalf("Volume format failed. Error: %v", err) + } + + isVolumeFormattedResponse, err = volumeClient.IsVolumeFormatted(context.TODO(), isVolumeFormattedRequest) + if err != nil { + t.Fatalf("Is volume formatted request error: %v", err) + } + if !isVolumeFormattedResponse.Formatted { + t.Fatal("Volume should be formatted. Unexpected !!") + } + return vhd, volumeID, vhdCleanup +} + +func getClosestVolumeFromTargetPathTests(diskClient disk.Interface, volumeClient volume.Interface, t *testing.T) { + t.Run("DriveLetterVolume", func(t *testing.T) { + vhd, _, vhdCleanup := volumeInit(volumeClient, t) + defer vhdCleanup() + + // vhd.Mount dir exists, because there are no volumes above it should return the C:\ volume + request := &volume.GetClosestVolumeIDFromTargetPathRequest{ + TargetPath: vhd.Mount, + } + response, err := volumeClient.GetClosestVolumeIDFromTargetPath(context.TODO(), request) + if err != nil { + t.Fatalf("GetClosestVolumeIDFromTargetPath request error, err=%v", err) + } + + // the C drive volume + targetb, err := runPowershellCmd(t, `(Get-Partition -DriveLetter C | Get-Volume).UniqueId`) + if err != nil { + t.Fatalf("Failed to get the C: drive volume") + } + cDriveVolume := strings.TrimSpace(string(targetb)) + + if response.VolumeId != cDriveVolume { + t.Fatalf("The volume from GetClosestVolumeIDFromTargetPath doesn't match the C: drive volume") + } + }) + t.Run("AncestorVolumeFromNestedDirectory", func(t *testing.T) { + var err error + vhd, volumeID, vhdCleanup := volumeInit(volumeClient, t) + defer vhdCleanup() + + // Mount the volume + mountVolumeRequest := &volume.MountVolumeRequest{ + VolumeId: volumeID, + TargetPath: vhd.Mount, + } + _, err = volumeClient.MountVolume(context.TODO(), mountVolumeRequest) + if err != nil { + t.Fatalf("Volume id %s mount to path %s failed. Error: %v", volumeID, vhd.Mount, err) + } + + // Unmount the volume + defer func() { + unmountVolumeRequest := &volume.UnmountVolumeRequest{ + VolumeId: volumeID, + TargetPath: vhd.Mount, + } + _, err = volumeClient.UnmountVolume(context.TODO(), unmountVolumeRequest) + if err != nil { + t.Fatalf("Volume id %s mount to path %s failed. Error: %v", volumeID, vhd.Mount, err) + } + }() + + nestedDirectory := filepath.Join(vhd.Mount, "foo/bar") + err = os.MkdirAll(nestedDirectory, os.ModeDir) + if err != nil { + t.Fatalf("Failed to create directory=%s", nestedDirectory) + } + + // the volume returned should be the VHD volume + request := &volume.GetClosestVolumeIDFromTargetPathRequest{ + TargetPath: nestedDirectory, + } + response, err := volumeClient.GetClosestVolumeIDFromTargetPath(context.TODO(), request) + if err != nil { + t.Fatalf("GetClosestVolumeIDFromTargetPath request error, err=%v", err) + } + + if response.VolumeId != volumeID { + t.Fatalf("The volume from GetClosestVolumeIDFromTargetPath doesn't match the VHD volume=%s", volumeID) + } + }) + + t.Run("SymlinkToVolume", func(t *testing.T) { + var err error + vhd, volumeID, vhdCleanup := volumeInit(volumeClient, t) + defer vhdCleanup() + + // Mount the volume + mountVolumeRequest := &volume.MountVolumeRequest{ + VolumeId: volumeID, + TargetPath: vhd.Mount, + } + _, err = volumeClient.MountVolume(context.TODO(), mountVolumeRequest) + if err != nil { + t.Fatalf("Volume id %s mount to path %s failed. Error: %v", volumeID, vhd.Mount, err) + } + + // Unmount the volume + defer func() { + unmountVolumeRequest := &volume.UnmountVolumeRequest{ + VolumeId: volumeID, + TargetPath: vhd.Mount, + } + _, err = volumeClient.UnmountVolume(context.TODO(), unmountVolumeRequest) + if err != nil { + t.Fatalf("Volume id %s mount to path %s failed. Error: %v", volumeID, vhd.Mount, err) + } + }() + + testPluginPath, _ := getTestPluginPath() + err = os.MkdirAll(testPluginPath, os.ModeDir) + if err != nil { + t.Fatalf("Failed to create directory=%s", testPluginPath) + } + + sourceSymlink := filepath.Join(testPluginPath, "source") + err = os.Symlink(vhd.Mount, sourceSymlink) + if err != nil { + t.Fatalf("Failed to create the symlink=%s", sourceSymlink) + } + + // the volume returned should be the VHD volume + var request *volume.GetClosestVolumeIDFromTargetPathRequest + var response *volume.GetClosestVolumeIDFromTargetPathResponse + request = &volume.GetClosestVolumeIDFromTargetPathRequest{ + TargetPath: sourceSymlink, + } + response, err = volumeClient.GetClosestVolumeIDFromTargetPath(context.TODO(), request) + if err != nil { + t.Fatalf("GetClosestVolumeIDFromTargetPath request error, err=%v", err) + } + + if response.VolumeId != volumeID { + t.Fatalf("The volume from GetClosestVolumeIDFromTargetPath doesn't match the VHD volume=%s", volumeID) + } + }) +} + +func mountVolumeTests(diskClient disk.Interface, volumeClient volume.Interface, t *testing.T) { + vhd, volumeID, vhdCleanup := volumeInit(volumeClient, t) + defer vhdCleanup() + + volumeStatsRequest := &volume.GetVolumeStatsRequest{ + VolumeId: volumeID, + } + + volumeStatsResponse, err := volumeClient.GetVolumeStats(context.TODO(), volumeStatsRequest) + if err != nil { + t.Fatalf("VolumeStats request error: %v", err) + } + // For a volume formatted with 1GB it should be around 1GB, in practice it was 1056947712 bytes or 0.9844GB + // let's compare with a range of +- 20MB + if !sizeIsAround(t, volumeStatsResponse.TotalBytes, vhd.InitialSize) { + t.Fatalf("volumeStatsResponse.TotalBytes reported is not valid, it is %v", volumeStatsResponse.TotalBytes) + } + + // Resize the disk to twice its size (from 1GB to 2GB) + // To resize a volume we need to resize the virtual hard disk first and then the partition + cmd := fmt.Sprintf("Resize-VHD -Path %s -SizeBytes %d", vhd.Path, int64(vhd.InitialSize*2)) + if out, err := runPowershellCmd(t, cmd); err != nil { + t.Fatalf("Error: %v. Command: %q. Out: %s.", err, cmd, out) + } + + // Resize the volume to 1.5GB + oldVolumeSize := volumeStatsResponse.TotalBytes + newVolumeSize := int64(float32(oldVolumeSize) * 1.5) + + // This is the max partition size when doing a resize to 2GB + // + // Get-PartitionSupportedSize -DiskNumber 7 -PartitionNumber 2 | ConvertTo-Json + // { + // "SizeMin": 404725760, + // "SizeMax": 2130689536 + // } + resizeVolumeRequest := &volume.ResizeVolumeRequest{ + VolumeId: volumeID, + // resize the partition to 1.5x times instead + SizeBytes: newVolumeSize, + } + + t.Logf("Attempt to resize volume from sizeBytes=%d to sizeBytes=%d", oldVolumeSize, newVolumeSize) + + _, err = volumeClient.ResizeVolume(context.TODO(), resizeVolumeRequest) + if err != nil { + t.Fatalf("Volume resize request failed. Error: %v", err) + } + + volumeStatsResponse, err = volumeClient.GetVolumeStats(context.TODO(), volumeStatsRequest) + if err != nil { + t.Fatalf("VolumeStats request after resize error: %v", err) + } + // resizing from 1GB to approximately 1.5GB + if !sizeIsAround(t, volumeStatsResponse.TotalBytes, newVolumeSize) { + t.Fatalf("VolumeSize reported should be greater than the old size, it is %v", volumeStatsResponse.TotalBytes) + } + + volumeDiskNumberRequest := &volume.GetDiskNumberFromVolumeIDRequest{ + VolumeId: volumeID, + } + + volumeDiskNumberResponse, err := volumeClient.GetDiskNumberFromVolumeID(context.TODO(), volumeDiskNumberRequest) + if err != nil { + t.Fatalf("GetDiskNumberFromVolumeID failed: %v", err) + } + + diskStatsRequest := &disk.GetDiskStatsRequest{ + DiskNumber: volumeDiskNumberResponse.DiskNumber, + } + + diskStatsResponse, err := diskClient.GetDiskStats(context.TODO(), diskStatsRequest) + if err != nil { + t.Fatalf("DiskStats request error: %v", err) + } + + if diskStatsResponse.TotalBytes < 0 { + t.Fatalf("Invalid disk size was returned %v", diskStatsResponse.TotalBytes) + } + + // Mount the volume + mountVolumeRequest := &volume.MountVolumeRequest{ + VolumeId: volumeID, + TargetPath: vhd.Mount, + } + _, err = volumeClient.MountVolume(context.TODO(), mountVolumeRequest) + if err != nil { + t.Fatalf("Volume id %s mount to path %s failed. Error: %v", volumeID, vhd.Mount, err) + } + + // Unmount the volume + unmountVolumeRequest := &volume.UnmountVolumeRequest{ + VolumeId: volumeID, + TargetPath: vhd.Mount, + } + _, err = volumeClient.UnmountVolume(context.TODO(), unmountVolumeRequest) + if err != nil { + t.Fatalf("Volume id %s mount to path %s failed. Error: %v", volumeID, vhd.Mount, err) + } +} + +func volumeTests(t *testing.T) { + volumeClient, err := volume.New(volumeapi.New()) + require.Nil(t, err) + + diskClient, err := disk.New(diskapi.New()) + require.Nil(t, err) + + t.Run("MountVolume", func(t *testing.T) { + mountVolumeTests(diskClient, volumeClient, t) + }) + t.Run("GetClosestVolumeFromTargetPath", func(t *testing.T) { + getClosestVolumeFromTargetPathTests(diskClient, volumeClient, t) + }) +} + +func TestVolume(t *testing.T) { + t.Run("NegativeDiskTests", func(t *testing.T) { + negativeDiskTests(t) + }) + t.Run("NegativeVolumeTests", func(t *testing.T) { + negativeVolumeTests(t) + }) + + // TODO: These tests will fail on Github Actions because Hyper-V is disabled + // see https://github.com/actions/virtual-environments/pull/2525 + + // these tests should be considered frozen from the API point of view + t.Run("volumeTests", func(t *testing.T) { + skipTestOnCondition(t, isRunningOnGhActions()) + volumeTests(t) + }) +} diff --git a/integrationtests/volume_v2alpha1_test.go b/integrationtests/volume_v2alpha1_test.go index 203afb2e8..ed9ee8090 100644 --- a/integrationtests/volume_v2alpha1_test.go +++ b/integrationtests/volume_v2alpha1_test.go @@ -15,9 +15,9 @@ import ( v2alpha1client "github.com/kubernetes-csi/csi-proxy/client/groups/volume/v2alpha1" ) -// volumeInit initializes a volume, it creates a VHD, initializes it, +// volumeInitV2Alpha1 initializes a volume, it creates a VHD, initializes it, // creates a partition with the max size and formats the volume corresponding to that partition -func volumeInit(volumeClient *v2alpha1client.Client, t *testing.T) (*VirtualHardDisk, string, func()) { +func volumeInitV2Alpha1(volumeClient *v2alpha1client.Client, t *testing.T) (*VirtualHardDisk, string, func()) { vhd, vhdCleanup := diskInit(t) listRequest := &v2alpha1.ListVolumesOnDiskRequest{ @@ -66,7 +66,7 @@ func volumeInit(volumeClient *v2alpha1client.Client, t *testing.T) (*VirtualHard func v2alpha1GetClosestVolumeFromTargetPathTests(diskClient *diskv1client.Client, volumeClient *v2alpha1client.Client, t *testing.T) { t.Run("DriveLetterVolume", func(t *testing.T) { - vhd, _, vhdCleanup := volumeInit(volumeClient, t) + vhd, _, vhdCleanup := volumeInitV2Alpha1(volumeClient, t) defer vhdCleanup() // vhd.Mount dir exists, because there are no volumes above it should return the C:\ volume @@ -94,7 +94,7 @@ func v2alpha1GetClosestVolumeFromTargetPathTests(diskClient *diskv1client.Client }) t.Run("AncestorVolumeFromNestedDirectory", func(t *testing.T) { var err error - vhd, volumeID, vhdCleanup := volumeInit(volumeClient, t) + vhd, volumeID, vhdCleanup := volumeInitV2Alpha1(volumeClient, t) defer vhdCleanup() // Mount the volume @@ -143,7 +143,7 @@ func v2alpha1GetClosestVolumeFromTargetPathTests(diskClient *diskv1client.Client t.Run("SymlinkToVolume", func(t *testing.T) { var err error - vhd, volumeID, vhdCleanup := volumeInit(volumeClient, t) + vhd, volumeID, vhdCleanup := volumeInitV2Alpha1(volumeClient, t) defer vhdCleanup() // Mount the volume @@ -198,7 +198,7 @@ func v2alpha1GetClosestVolumeFromTargetPathTests(diskClient *diskv1client.Client } func v2alpha1MountVolumeTests(diskClient *diskv1client.Client, volumeClient *v2alpha1client.Client, t *testing.T) { - vhd, volumeID, vhdCleanup := volumeInit(volumeClient, t) + vhd, volumeID, vhdCleanup := volumeInitV2Alpha1(volumeClient, t) defer vhdCleanup() volumeStatsRequest := &v2alpha1.GetVolumeStatsRequest{ diff --git a/pkg/volume/api/api.go b/pkg/volume/api/api.go new file mode 100644 index 000000000..440ecfaee --- /dev/null +++ b/pkg/volume/api/api.go @@ -0,0 +1,388 @@ +package api + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "regexp" + "strconv" + "strings" + + "github.com/kubernetes-csi/csi-proxy/pkg/utils" + "k8s.io/klog/v2" +) + +// API exposes the internal volume operations available in the server +type API interface { + // ListVolumesOnDisk lists volumes on a disk identified by a `diskNumber` and optionally a partition identified by `partitionNumber`. + ListVolumesOnDisk(diskNumber uint32, partitionNumber uint32) (volumeIDs []string, err error) + // MountVolume mounts the volume at the requested global staging target path. + MountVolume(volumeID, targetPath string) error + // UnmountVolume gracefully dismounts a volume. + UnmountVolume(volumeID, targetPath string) error + // IsVolumeFormatted checks if a volume is formatted with NTFS. + IsVolumeFormatted(volumeID string) (bool, error) + // FormatVolume formats a volume with the NTFS format. + FormatVolume(volumeID string) error + // ResizeVolume performs resizing of the partition and file system for a block based volume. + ResizeVolume(volumeID string, sizeBytes int64) error + // GetVolumeStats gets the volume information. + GetVolumeStats(volumeID string) (int64, int64, error) + // GetDiskNumberFromVolumeID returns the disk number for a given volumeID. + GetDiskNumberFromVolumeID(volumeID string) (uint32, error) + // GetVolumeIDFromTargetPath returns the volume id of a given target path. + GetVolumeIDFromTargetPath(targetPath string) (string, error) + // WriteVolumeCache writes the volume `volumeID`'s cache to disk. + WriteVolumeCache(volumeID string) error + // GetVolumeIDFromTargetPath returns the volume id of a given target path. + GetClosestVolumeIDFromTargetPath(targetPath string) (string, error) +} + +// volumeAPI implements the internal Volume APIs +type volumeAPI struct{} + +// verifies that the API is implemented +var _ API = &volumeAPI{} + +var ( + // VolumeRegexp matches a Windows Volume + // example: Volume{452e318a-5cde-421e-9831-b9853c521012} + // + // The field UniqueId has an additional prefix which is NOT included in the regex + // however the regex can match UniqueId too + // PS C:\disks> (Get-Disk -Number 1 | Get-Partition | Get-Volume).UniqueId + // \\?\Volume{452e318a-5cde-421e-9831-b9853c521012}\ + VolumeRegexp = regexp.MustCompile(`Volume\{[\w-]*\}`) +) + +// New - Construct a new Volume API Implementation. +func New() API { + return &volumeAPI{} +} + +func getVolumeSize(volumeID string) (int64, error) { + cmd := fmt.Sprintf("(Get-Volume -UniqueId \"%s\" | Get-partition).Size", volumeID) + out, err := utils.RunPowershellCmd(cmd) + + if err != nil || len(out) == 0 { + return -1, fmt.Errorf("error getting size of the partition from mount. cmd %s, output: %s, error: %v", cmd, string(out), err) + } + + outString := strings.TrimSpace(string(out)) + volumeSize, err := strconv.ParseInt(outString, 10, 64) + if err != nil { + return -1, fmt.Errorf("error parsing size of volume %s received %v trimmed to %v err %v", volumeID, out, outString, err) + } + + return volumeSize, nil +} + +// ListVolumesOnDisk - returns back list of volumes(volumeIDs) in a disk and a partition. +func (volumeAPI) ListVolumesOnDisk(diskNumber uint32, partitionNumber uint32) (volumeIDs []string, err error) { + var cmd string + if partitionNumber == 0 { + // 0 means that the partitionNumber wasn't set so we list all the partitions + cmd = fmt.Sprintf("(Get-Disk -Number %d | Get-Partition | Get-Volume).UniqueId", diskNumber) + } else { + cmd = fmt.Sprintf("(Get-Disk -Number %d | Get-Partition -PartitionNumber %d | Get-Volume).UniqueId", diskNumber, partitionNumber) + } + out, err := utils.RunPowershellCmd(cmd) + if err != nil { + return []string{}, fmt.Errorf("error list volumes on disk. cmd: %s, output: %s, error: %v", cmd, string(out), err) + } + + volumeIds := strings.Split(strings.TrimSpace(string(out)), "\r\n") + return volumeIds, nil +} + +// FormatVolume - Formats a volume with the NTFS format. +func (volumeAPI) FormatVolume(volumeID string) (err error) { + cmd := fmt.Sprintf("Get-Volume -UniqueId \"%s\" | Format-Volume -FileSystem ntfs -Confirm:$false", volumeID) + out, err := utils.RunPowershellCmd(cmd) + + if err != nil { + return fmt.Errorf("error formatting volume. cmd: %s, output: %s, error: %v", cmd, string(out), err) + } + // TODO: Do we need to handle anything for len(out) == 0 + return nil +} + +// WriteVolumeCache - Writes the file system cache to disk with the given volume id +func (volumeAPI) WriteVolumeCache(volumeID string) (err error) { + return writeCache(volumeID) +} + +// IsVolumeFormatted - Check if the volume is formatted with the pre specified filesystem(typically ntfs). +func (volumeAPI) IsVolumeFormatted(volumeID string) (bool, error) { + cmd := fmt.Sprintf("(Get-Volume -UniqueId \"%s\" -ErrorAction Stop).FileSystemType", volumeID) + out, err := utils.RunPowershellCmd(cmd) + if err != nil { + return false, fmt.Errorf("error checking if volume is formatted. cmd: %s, output: %s, error: %v", cmd, string(out), err) + } + stringOut := strings.TrimSpace(string(out)) + if len(stringOut) == 0 || strings.EqualFold(stringOut, "Unknown") { + return false, nil + } + return true, nil +} + +// MountVolume - mounts a volume to a path. This is done using the Add-PartitionAccessPath for presenting the volume via a path. +func (volumeAPI) MountVolume(volumeID, path string) error { + cmd := fmt.Sprintf("Get-Volume -UniqueId \"%s\" | Get-Partition | Add-PartitionAccessPath -AccessPath %s", volumeID, path) + out, err := utils.RunPowershellCmd(cmd) + if err != nil { + return fmt.Errorf("error mount volume to path. cmd: %s, output: %s, error: %v", cmd, string(out), err) + } + + return nil +} + +// UnmountVolume - unmounts the volume path by removing the partition access path +func (volumeAPI) UnmountVolume(volumeID, path string) error { + if err := writeCache(volumeID); err != nil { + return err + } + cmd := fmt.Sprintf("Get-Volume -UniqueId \"%s\" | Get-Partition | Remove-PartitionAccessPath -AccessPath %s", volumeID, path) + out, err := utils.RunPowershellCmd(cmd) + if err != nil { + return fmt.Errorf("error getting driver letter to mount volume. cmd: %s, output: %s,error: %v", cmd, string(out), err) + } + return nil +} + +// ResizeVolume - resizes a volume with the given size, if size == 0 then max supported size is used +func (volumeAPI) ResizeVolume(volumeID string, size int64) error { + // If size is 0 then we will resize to the maximum size possible, otherwise just resize to size + var cmd string + var out []byte + var err error + var finalSize int64 + var outString string + if size == 0 { + cmd = fmt.Sprintf("Get-Volume -UniqueId \"%s\" | Get-partition | Get-PartitionSupportedSize | Select SizeMax | ConvertTo-Json", volumeID) + out, err = utils.RunPowershellCmd(cmd) + + if err != nil || len(out) == 0 { + return fmt.Errorf("error getting sizemin,sizemax from mount. cmd: %s, output: %s, error: %v", cmd, string(out), err) + } + + var getVolumeSizing map[string]int64 + outString = string(out) + err = json.Unmarshal([]byte(outString), &getVolumeSizing) + if err != nil { + return fmt.Errorf("out %v outstring %v err %v", out, outString, err) + } + + sizeMax := getVolumeSizing["SizeMax"] + + finalSize = sizeMax + } else { + finalSize = size + } + + currentSize, err := getVolumeSize(volumeID) + if err != nil { + return fmt.Errorf("error getting the current size of volume (%s) with error (%v)", volumeID, err) + } + + //if the partition's size is already the size we want this is a noop, just return + if currentSize >= finalSize { + klog.V(2).Infof("Attempted to resize volume %s to a lower size, from currentBytes=%d wantedBytes=%d", volumeID, currentSize, finalSize) + return nil + } + + cmd = fmt.Sprintf("Get-Volume -UniqueId \"%s\" | Get-Partition | Resize-Partition -Size %d", volumeID, finalSize) + out, err = utils.RunPowershellCmd(cmd) + if err != nil { + return fmt.Errorf("error resizing volume. cmd: %s, output: %s size:%v, finalSize %v, error: %v", cmd, string(out), size, finalSize, err) + } + return nil +} + +// GetVolumeStats - retrieves the volume stats for a given volume +func (volumeAPI) GetVolumeStats(volumeID string) (int64, int64, error) { + // get the size and sizeRemaining for the volume + cmd := fmt.Sprintf("(Get-Volume -UniqueId \"%s\" | Select SizeRemaining,Size) | ConvertTo-Json", volumeID) + out, err := utils.RunPowershellCmd(cmd) + + if err != nil { + return -1, -1, fmt.Errorf("error getting capacity and used size of volume. cmd: %s, output: %s, error: %v", cmd, string(out), err) + } + + var getVolume map[string]int64 + outString := string(out) + err = json.Unmarshal([]byte(outString), &getVolume) + if err != nil { + return -1, -1, fmt.Errorf("out %v outstring %v err %v", out, outString, err) + } + + volumeSize := getVolume["Size"] + volumeSizeRemaining := getVolume["SizeRemaining"] + + volumeUsedSize := volumeSize - volumeSizeRemaining + return volumeSize, volumeUsedSize, nil +} + +// GetDiskNumberFromVolumeID - gets the disk number where the volume is. +func (volumeAPI) GetDiskNumberFromVolumeID(volumeID string) (uint32, error) { + // get the size and sizeRemaining for the volume + cmd := fmt.Sprintf("(Get-Volume -UniqueId \"%s\" | Get-Partition).DiskNumber", volumeID) + out, err := utils.RunPowershellCmd(cmd) + + if err != nil || len(out) == 0 { + return 0, fmt.Errorf("error getting disk number. cmd: %s, output: %s, error: %v", cmd, string(out), err) + } + + reg, err := regexp.Compile("[^0-9]+") + if err != nil { + return 0, fmt.Errorf("error compiling regex. err: %v", err) + } + diskNumberOutput := reg.ReplaceAllString(string(out), "") + + diskNumber, err := strconv.ParseUint(diskNumberOutput, 10, 32) + + if err != nil { + return 0, fmt.Errorf("error parsing disk number. cmd: %s, output: %s, error: %v", cmd, diskNumberOutput, err) + } + + return uint32(diskNumber), nil +} + +// GetVolumeIDFromTargetPath - gets the volume ID given a mount point, the function is recursive until it find a volume or errors out +func (volumeAPI) GetVolumeIDFromTargetPath(mount string) (string, error) { + volumeString, err := getTarget(mount) + + if err != nil { + return "", fmt.Errorf("error getting the volume for the mount %s, internal error %v", mount, err) + } + + return volumeString, nil +} + +func getTarget(mount string) (string, error) { + cmd := fmt.Sprintf("(Get-Item -Path %s).Target", mount) + out, err := utils.RunPowershellCmd(cmd) + if err != nil || len(out) == 0 { + return "", fmt.Errorf("error getting volume from mount. cmd: %s, output: %s, error: %v", cmd, string(out), err) + } + volumeString := strings.TrimSpace(string(out)) + if !strings.HasPrefix(volumeString, "Volume") { + return getTarget(volumeString) + } + + return ensureVolumePrefix(volumeString), nil +} + +// GetVolumeIDFromTargetPath returns the volume id of a given target path. +func (volumeAPI) GetClosestVolumeIDFromTargetPath(targetPath string) (string, error) { + volumeString, err := findClosestVolume(targetPath) + + if err != nil { + return "", fmt.Errorf("error getting the closest volume for the path=%s, err=%v", targetPath, err) + } + + return volumeString, nil +} + +// findClosestVolume finds the closest volume id for a given target path +// by following symlinks and moving up in the filesystem, if after moving up in the filesystem +// we get to a DriveLetter then the volume corresponding to this drive letter is returned instead. +func findClosestVolume(path string) (string, error) { + candidatePath := path + + // Run in a bounded loop to avoid doing an infinite loop + // while trying to follow symlinks + // + // The maximum path length in Windows is 260, it could be possible to end + // up in a sceneario where we do more than 256 iterations (e.g. by following symlinks from + // a place high in the hierarchy to a nested sibling location many times) + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#:~:text=In%20editions%20of%20Windows%20before,required%20to%20remove%20the%20limit. + // + // The number of iterations is 256, which is similar to the number of iterations in filepath-securejoin + // https://github.com/cyphar/filepath-securejoin/blob/64536a8a66ae59588c981e2199f1dcf410508e07/join.go#L51 + for i := 0; i < 256; i++ { + fi, err := os.Lstat(candidatePath) + if err != nil { + return "", err + } + isSymlink := fi.Mode()&os.ModeSymlink != 0 + + if isSymlink { + target, err := dereferenceSymlink(candidatePath) + if err != nil { + return "", err + } + // if it has the form Volume{volumeid} then it's a volume + if VolumeRegexp.Match([]byte(target)) { + // symlinks that are pointing to Volumes don't have this prefix + return ensureVolumePrefix(target), nil + } + // otherwise follow the symlink + candidatePath = target + } else { + // if it's not a symlink move one level up + previousPath := candidatePath + candidatePath = filepath.Dir(candidatePath) + + // if the new path is the same as the previous path then we reached the root path + if previousPath == candidatePath { + // find the volume for the root path (assuming that it's a DriveLetter) + target, err := getVolumeForDriveLetter(candidatePath[0:1]) + if err != nil { + return "", err + } + return target, nil + } + } + + } + + return "", fmt.Errorf("Failed to find the closest volume for path=%s", path) +} + +// ensureVolumePrefix makes sure that the volume has the Volume prefix +func ensureVolumePrefix(volume string) string { + prefix := "\\\\?\\" + if !strings.HasPrefix(volume, prefix) { + volume = prefix + volume + } + return volume +} + +// dereferenceSymlink dereferences the symlink `path` and returns the stdout. +func dereferenceSymlink(path string) (string, error) { + cmd := fmt.Sprintf(`(Get-Item -Path %s).Target`, path) + out, err := utils.RunPowershellCmd(cmd) + if err != nil { + return "", err + } + output := strings.TrimSpace(string(out)) + klog.V(8).Infof("Stdout: %s", output) + return output, nil +} + +// getVolumeForDriveLetter gets a volume from a drive letter (e.g. C:/). +func getVolumeForDriveLetter(path string) (string, error) { + if len(path) != 1 { + return "", fmt.Errorf("The path=%s is not a valid DriverLetter", path) + } + + cmd := fmt.Sprintf(`(Get-Partition -DriveLetter %s | Get-Volume).UniqueId`, path) + out, err := utils.RunPowershellCmd(cmd) + if err != nil { + return "", err + } + output := strings.TrimSpace(string(out)) + klog.V(8).Infof("Stdout: %s", output) + return output, nil +} + +func writeCache(volumeID string) error { + cmd := fmt.Sprintf("Get-Volume -UniqueId \"%s\" | Write-Volumecache", volumeID) + out, err := utils.RunPowershellCmd(cmd) + if err != nil { + return fmt.Errorf("error writing volume cache. cmd: %s, output: %s, error: %v", cmd, string(out), err) + } + return nil +} diff --git a/pkg/volume/types.go b/pkg/volume/types.go new file mode 100644 index 000000000..4584f68d4 --- /dev/null +++ b/pkg/volume/types.go @@ -0,0 +1,116 @@ +package volume + +type ListVolumesOnDiskRequest struct { + DiskNumber uint32 + PartitionNumber uint32 +} + +type ListVolumesOnDiskResponse struct { + VolumeIds []string +} + +type MountVolumeRequest struct { + VolumeId string + TargetPath string +} + +type MountVolumeResponse struct { +} + +type IsVolumeFormattedRequest struct { + VolumeId string +} + +type IsVolumeFormattedResponse struct { + Formatted bool +} + +type FormatVolumeRequest struct { + VolumeId string +} + +type FormatVolumeResponse struct { +} + +type WriteVolumeCacheRequest struct { + VolumeId string +} + +type WriteVolumeCacheResponse struct { +} + +type UnmountVolumeRequest struct { + VolumeId string + TargetPath string +} + +type UnmountVolumeResponse struct { +} + +type ResizeVolumeRequest struct { + VolumeId string + SizeBytes int64 +} + +type ResizeVolumeResponse struct { +} + +type GetVolumeStatsRequest struct { + VolumeId string +} + +type GetVolumeStatsResponse struct { + TotalBytes int64 + UsedBytes int64 +} + +type GetDiskNumberFromVolumeIDRequest struct { + VolumeId string +} + +type GetDiskNumberFromVolumeIDResponse struct { + DiskNumber uint32 +} + +type GetVolumeIDFromTargetPathRequest struct { + TargetPath string +} + +type GetVolumeIDFromTargetPathResponse struct { + VolumeId string +} + +type GetClosestVolumeIDFromTargetPathRequest struct { + TargetPath string +} + +type GetClosestVolumeIDFromTargetPathResponse struct { + VolumeId string +} + +// These structs are used in APIs less than v1beta3 and rerouted internally + +type DismountVolumeRequest struct { + VolumeId string + Path string +} +type DismountVolumeResponse struct{} +type VolumeDiskNumberRequest struct { + VolumeId string +} +type VolumeDiskNumberResponse struct { + DiskNumber int64 +} +type VolumeIDFromMountRequest struct { + Mount string +} +type VolumeIDFromMountResponse struct { + VolumeId string +} +type VolumeStatsRequest struct { + VolumeId string +} +type VolumeStatsResponse struct { + VolumeSize int64 + VolumeUsedSize int64 +} diff --git a/pkg/volume/volume.go b/pkg/volume/volume.go new file mode 100644 index 000000000..03d14164d --- /dev/null +++ b/pkg/volume/volume.go @@ -0,0 +1,316 @@ +package volume + +import ( + "context" + "fmt" + + volumeapi "github.com/kubernetes-csi/csi-proxy/pkg/volume/api" + "k8s.io/klog/v2" +) + +// Volume wraps the host API and implements the interface +type Volume struct { + hostAPI volumeapi.API +} + +type Interface interface { + DismountVolume(context.Context, *DismountVolumeRequest) (*DismountVolumeResponse, error) + FormatVolume(context.Context, *FormatVolumeRequest) (*FormatVolumeResponse, error) + GetClosestVolumeIDFromTargetPath(context.Context, *GetClosestVolumeIDFromTargetPathRequest) (*GetClosestVolumeIDFromTargetPathResponse, error) + GetDiskNumberFromVolumeID(context.Context, *GetDiskNumberFromVolumeIDRequest) (*GetDiskNumberFromVolumeIDResponse, error) + GetVolumeDiskNumber(context.Context, *VolumeDiskNumberRequest) (*VolumeDiskNumberResponse, error) + GetVolumeIDFromMount(context.Context, *VolumeIDFromMountRequest) (*VolumeIDFromMountResponse, error) + GetVolumeIDFromTargetPath(context.Context, *GetVolumeIDFromTargetPathRequest) (*GetVolumeIDFromTargetPathResponse, error) + GetVolumeStats(context.Context, *GetVolumeStatsRequest) (*GetVolumeStatsResponse, error) + IsVolumeFormatted(context.Context, *IsVolumeFormattedRequest) (*IsVolumeFormattedResponse, error) + ListVolumesOnDisk(context.Context, *ListVolumesOnDiskRequest) (*ListVolumesOnDiskResponse, error) + MountVolume(context.Context, *MountVolumeRequest) (*MountVolumeResponse, error) + ResizeVolume(context.Context, *ResizeVolumeRequest) (*ResizeVolumeResponse, error) + UnmountVolume(context.Context, *UnmountVolumeRequest) (*UnmountVolumeResponse, error) + VolumeStats(context.Context, *VolumeStatsRequest) (*VolumeStatsResponse, error) + WriteVolumeCache(context.Context, *WriteVolumeCacheRequest) (*WriteVolumeCacheResponse, error) +} + +var _ Interface = &Volume{} + +func New(hostAPI volumeapi.API) (*Volume, error) { + return &Volume{ + hostAPI: hostAPI, + }, nil +} + +func (v *Volume) ListVolumesOnDisk(context context.Context, request *ListVolumesOnDiskRequest) (*ListVolumesOnDiskResponse, error) { + klog.V(2).Infof("ListVolumesOnDisk: Request: %+v", request) + response := &ListVolumesOnDiskResponse{} + + volumeIDs, err := v.hostAPI.ListVolumesOnDisk(request.DiskNumber, request.PartitionNumber) + if err != nil { + klog.Errorf("failed ListVolumeOnDisk %v", err) + return response, err + } + + response.VolumeIds = volumeIDs + return response, nil +} + +func (v *Volume) MountVolume(context context.Context, request *MountVolumeRequest) (*MountVolumeResponse, error) { + klog.V(2).Infof("MountVolume: Request: %+v", request) + response := &MountVolumeResponse{} + + volumeID := request.VolumeId + if volumeID == "" { + klog.Errorf("volume id empty") + return response, fmt.Errorf("MountVolumeRequest.VolumeId is empty") + } + targetPath := request.TargetPath + if targetPath == "" { + klog.Errorf("targetPath empty") + return response, fmt.Errorf("MountVolumeRequest.TargetPath is empty") + } + + err := v.hostAPI.MountVolume(volumeID, targetPath) + if err != nil { + klog.Errorf("failed MountVolume %v", err) + return response, err + } + return response, nil +} + +func (v *Volume) DismountVolume(context context.Context, request *DismountVolumeRequest) (*DismountVolumeResponse, error) { + unmountVolumeRequest := &UnmountVolumeRequest{ + VolumeId: request.VolumeId, + TargetPath: request.Path, + } + _, err := v.UnmountVolume(context, unmountVolumeRequest) + if err != nil { + return nil, fmt.Errorf("Forward to UnmountVolume failed, err=%+v", err) + } + dismountVolumeResponse := &DismountVolumeResponse{} + return dismountVolumeResponse, nil +} + +func (v *Volume) UnmountVolume(context context.Context, request *UnmountVolumeRequest) (*UnmountVolumeResponse, error) { + klog.V(2).Infof("UnmountVolume: Request: %+v", request) + response := &UnmountVolumeResponse{} + + volumeID := request.VolumeId + if volumeID == "" { + klog.Errorf("volume id empty") + return response, fmt.Errorf("volume id empty") + } + targetPath := request.TargetPath + if targetPath == "" { + klog.Errorf("target path empty") + return response, fmt.Errorf("target path empty") + } + err := v.hostAPI.UnmountVolume(volumeID, targetPath) + if err != nil { + klog.Errorf("failed UnmountVolume %v", err) + return response, err + } + return response, nil +} + +func (v *Volume) IsVolumeFormatted(context context.Context, request *IsVolumeFormattedRequest) (*IsVolumeFormattedResponse, error) { + klog.V(2).Infof("IsVolumeFormatted: Request: %+v", request) + response := &IsVolumeFormattedResponse{} + + volumeID := request.VolumeId + if volumeID == "" { + klog.Errorf("volume id empty") + return response, fmt.Errorf("volume id empty") + } + isFormatted, err := v.hostAPI.IsVolumeFormatted(volumeID) + if err != nil { + klog.Errorf("failed IsVolumeFormatted %v", err) + return response, err + } + klog.V(5).Infof("IsVolumeFormatted: return: %v", isFormatted) + response.Formatted = isFormatted + return response, nil +} + +func (v *Volume) FormatVolume(context context.Context, request *FormatVolumeRequest) (*FormatVolumeResponse, error) { + klog.V(2).Infof("FormatVolume: Request: %+v", request) + response := &FormatVolumeResponse{} + + volumeID := request.VolumeId + if volumeID == "" { + klog.Errorf("volume id empty") + return response, fmt.Errorf("volume id empty") + } + + err := v.hostAPI.FormatVolume(volumeID) + if err != nil { + klog.Errorf("failed FormatVolume %v", err) + return response, err + } + return response, nil +} + +func (v *Volume) WriteVolumeCache(context context.Context, request *WriteVolumeCacheRequest) (*WriteVolumeCacheResponse, error) { + klog.V(2).Infof("WriteVolumeCache: Request: %+v", request) + response := &WriteVolumeCacheResponse{} + + volumeID := request.VolumeId + if volumeID == "" { + klog.Errorf("volume id empty") + return response, fmt.Errorf("volume id empty") + } + + err := v.hostAPI.WriteVolumeCache(volumeID) + if err != nil { + klog.Errorf("failed WriteVolumeCache %v", err) + return response, err + } + return response, nil +} + +func (v *Volume) ResizeVolume(context context.Context, request *ResizeVolumeRequest) (*ResizeVolumeResponse, error) { + klog.V(2).Infof("ResizeVolume: Request: %+v", request) + response := &ResizeVolumeResponse{} + + volumeID := request.VolumeId + if volumeID == "" { + klog.Errorf("volume id empty") + return response, fmt.Errorf("volume id empty") + } + sizeBytes := request.SizeBytes + // TODO : Validate size param + + err := v.hostAPI.ResizeVolume(volumeID, sizeBytes) + if err != nil { + klog.Errorf("failed ResizeVolume %v", err) + return response, err + } + return response, nil +} + +func (v *Volume) VolumeStats(context context.Context, request *VolumeStatsRequest) (*VolumeStatsResponse, error) { + getVolumeStatsRequest := &GetVolumeStatsRequest{ + VolumeId: request.VolumeId, + } + getVolumeStatsResponse, err := v.GetVolumeStats(context, getVolumeStatsRequest) + if err != nil { + return nil, fmt.Errorf("Forward to GetVolumeStats failed, err=%+v", err) + } + volumeStatsResponse := &VolumeStatsResponse{ + VolumeSize: getVolumeStatsResponse.TotalBytes, + VolumeUsedSize: getVolumeStatsResponse.UsedBytes, + } + return volumeStatsResponse, nil +} + +func (v *Volume) GetVolumeStats(context context.Context, request *GetVolumeStatsRequest) (*GetVolumeStatsResponse, error) { + klog.V(2).Infof("GetVolumeStats: Request: %+v", request) + volumeID := request.VolumeId + if volumeID == "" { + return nil, fmt.Errorf("volume id empty") + } + + totalBytes, usedBytes, err := v.hostAPI.GetVolumeStats(volumeID) + if err != nil { + klog.Errorf("failed GetVolumeStats %v", err) + return nil, err + } + + klog.V(2).Infof("VolumeStats: returned: Capacity %v Used %v", totalBytes, usedBytes) + + response := &GetVolumeStatsResponse{ + TotalBytes: totalBytes, + UsedBytes: usedBytes, + } + + return response, nil +} + +func (v *Volume) GetVolumeDiskNumber(context context.Context, request *VolumeDiskNumberRequest) (*VolumeDiskNumberResponse, error) { + getDiskNumberFromVolumeIDRequest := &GetDiskNumberFromVolumeIDRequest{ + VolumeId: request.VolumeId, + } + getDiskNumberFromVolumeIDResponse, err := v.GetDiskNumberFromVolumeID(context, getDiskNumberFromVolumeIDRequest) + if err != nil { + return nil, fmt.Errorf("Forward to GetDiskNumberFromVolumeID failed, err=%+v", err) + } + volumeStatsResponse := &VolumeDiskNumberResponse{ + DiskNumber: int64(getDiskNumberFromVolumeIDResponse.DiskNumber), + } + return volumeStatsResponse, nil +} + +func (v *Volume) GetDiskNumberFromVolumeID(context context.Context, request *GetDiskNumberFromVolumeIDRequest) (*GetDiskNumberFromVolumeIDResponse, error) { + klog.V(2).Infof("GetDiskNumberFromVolumeID: Request: %+v", request) + + volumeId := request.VolumeId + if volumeId == "" { + return nil, fmt.Errorf("volume id empty") + } + + diskNumber, err := v.hostAPI.GetDiskNumberFromVolumeID(volumeId) + if err != nil { + klog.Errorf("failed GetDiskNumberFromVolumeID %v", err) + return nil, err + } + + response := &GetDiskNumberFromVolumeIDResponse{ + DiskNumber: diskNumber, + } + + return response, nil +} + +func (v *Volume) GetVolumeIDFromMount(context context.Context, request *VolumeIDFromMountRequest) (*VolumeIDFromMountResponse, error) { + getVolumeIDFromTargetPathRequest := &GetVolumeIDFromTargetPathRequest{ + TargetPath: request.Mount, + } + getVolumeIDFromTargetPathResponse, err := v.GetVolumeIDFromTargetPath(context, getVolumeIDFromTargetPathRequest) + if err != nil { + return nil, fmt.Errorf("Forward to GetVolumeIDFromTargetPath failed, err=%+v", err) + } + volumeIDFromMountResponse := &VolumeIDFromMountResponse{ + VolumeId: getVolumeIDFromTargetPathResponse.VolumeId, + } + return volumeIDFromMountResponse, nil +} + +func (v *Volume) GetVolumeIDFromTargetPath(context context.Context, request *GetVolumeIDFromTargetPathRequest) (*GetVolumeIDFromTargetPathResponse, error) { + klog.V(2).Infof("GetVolumeIDFromTargetPath: Request: %+v", request) + + targetPath := request.TargetPath + if targetPath == "" { + return nil, fmt.Errorf("target path is empty") + } + + volume, err := v.hostAPI.GetVolumeIDFromTargetPath(targetPath) + if err != nil { + klog.Errorf("failed GetVolumeIDFromTargetPath: %v", err) + return nil, err + } + + response := &GetVolumeIDFromTargetPathResponse{ + VolumeId: volume, + } + + return response, nil +} + +func (v *Volume) GetClosestVolumeIDFromTargetPath(context context.Context, request *GetClosestVolumeIDFromTargetPathRequest) (*GetClosestVolumeIDFromTargetPathResponse, error) { + klog.V(2).Infof("GetClosestVolumeIDFromTargetPath: Request: %+v", request) + + targetPath := request.TargetPath + if targetPath == "" { + return nil, fmt.Errorf("target path is empty") + } + + volume, err := v.hostAPI.GetClosestVolumeIDFromTargetPath(targetPath) + if err != nil { + klog.Errorf("failed GetClosestVolumeIDFromTargetPath: %v", err) + return nil, err + } + + response := &GetClosestVolumeIDFromTargetPathResponse{ + VolumeId: volume, + } + + return response, nil +} diff --git a/pkg/volume/volume_test.go b/pkg/volume/volume_test.go new file mode 100644 index 000000000..3c4ddf5ee --- /dev/null +++ b/pkg/volume/volume_test.go @@ -0,0 +1,149 @@ +package volume + +import ( + "context" + "fmt" + "testing" + + volumeapi "github.com/kubernetes-csi/csi-proxy/pkg/volume/api" +) + +type fakeVolumeAPI struct { + diskVolMap map[uint32][]string +} + +var _ volumeapi.API = &fakeVolumeAPI{} + +func (volumeAPI *fakeVolumeAPI) Fill(diskToVolMapIn map[uint32][]string) { + for d, v := range diskToVolMapIn { + volumeAPI.diskVolMap[d] = v + } +} + +func (volumeAPI *fakeVolumeAPI) ListVolumesOnDisk(diskNumber uint32, partitionNumber uint32) (volumeIDs []string, err error) { + v := volumeAPI.diskVolMap[diskNumber] + if v == nil { + return nil, fmt.Errorf("returning error for %d list", diskNumber) + } + return v, nil +} + +func (volumeAPI *fakeVolumeAPI) MountVolume(volumeID, path string) error { + return nil +} + +func (volumeAPI *fakeVolumeAPI) UnmountVolume(volumeID, path string) error { + return nil +} + +func (volumeAPI *fakeVolumeAPI) IsVolumeFormatted(volumeID string) (bool, error) { + return true, nil +} + +func (volumeAPI *fakeVolumeAPI) FormatVolume(volumeID string) error { + return nil +} + +func (volumeAPI *fakeVolumeAPI) ResizeVolume(volumeID string, size int64) error { + return nil +} + +func (volumeAPI *fakeVolumeAPI) GetDiskNumberFromVolumeID(volumeID string) (uint32, error) { + return 0, nil +} + +func (volumeAPI *fakeVolumeAPI) GetVolumeIDFromTargetPath(mount string) (string, error) { + return "id", nil +} + +func (volumeAPI *fakeVolumeAPI) GetClosestVolumeIDFromTargetPath(mount string) (string, error) { + return "id", nil +} + +func (volumeAPI *fakeVolumeAPI) GetVolumeStats(volumeID string) (int64, int64, error) { + return -1, -1, nil +} + +func (volumeAPI *fakeVolumeAPI) WriteVolumeCache(volumeID string) error { + return nil +} + +func TestListVolumesOnDisk(t *testing.T) { + testCases := []struct { + name string + inputDiskNumber uint32 + expectedVolumeIds []string + isErrorExpected bool + expectedError error + }{ + { + name: "return two volumeIDs", + inputDiskNumber: 1, + expectedVolumeIds: []string{"volumeID1", "volumeID2"}, + isErrorExpected: false, + expectedError: nil, + }, + { + name: "return one volumeIDs", + inputDiskNumber: 2, + expectedVolumeIds: []string{"volumeID3"}, + isErrorExpected: false, + expectedError: nil, + }, + { + name: "return error", + inputDiskNumber: 3, + expectedVolumeIds: nil, + isErrorExpected: true, + expectedError: fmt.Errorf("returning error for 3 list"), + }, + } + + diskToVolMap := map[uint32][]string{ + 1: {"volumeID1", "volumeID2"}, + 2: {"volumeID3"}, + } + volAPI := &fakeVolumeAPI{ + diskVolMap: make(map[uint32][]string), + } + volAPI.Fill(diskToVolMap) + + client, err := New(volAPI) + if err != nil { + t.Fatalf("Volume server could not be initialized: %v", err) + } + + for _, tc := range testCases { + t.Logf("test case: %s", tc.name) + listInput := &ListVolumesOnDiskRequest{ + DiskNumber: tc.inputDiskNumber, + } + volumeListResponse, err := client.ListVolumesOnDisk(context.TODO(), listInput) + if tc.isErrorExpected { + if tc.expectedError.Error() != err.Error() { + t.Fatalf("Expected error: %v. Got error: %v", tc.expectedError, err) + } + } else { + if err != nil { + t.Fatalf("Error %v not expected", err) + } + + expectedVolumeIDMap := make(map[string]int) + for _, j := range tc.expectedVolumeIds { + expectedVolumeIDMap[j] = 0 + } + for _, i := range volumeListResponse.VolumeIds { + if _, found := expectedVolumeIDMap[i]; found == true { + expectedVolumeIDMap[i]++ + } else { + t.Fatalf("Found unexpected volume: %s", i) + } + } + for k, v := range expectedVolumeIDMap { + if v != 1 { + t.Fatalf("Volume: %s count: %d", k, v) + } + } + } + } +} From 1e7bd1897f69f5db42797d241415a65581dc3ca7 Mon Sep 17 00:00:00 2001 From: Alexander Ding Date: Wed, 12 Oct 2022 15:32:10 +0000 Subject: [PATCH 2/2] chore: move volumeInit to utils --- integrationtests/utils.go | 51 +++++++++++++++++++++ integrationtests/volume_test.go | 79 ++++++--------------------------- 2 files changed, 64 insertions(+), 66 deletions(-) diff --git a/integrationtests/utils.go b/integrationtests/utils.go index 714ceb3fb..895ccb37a 100644 --- a/integrationtests/utils.go +++ b/integrationtests/utils.go @@ -1,6 +1,7 @@ package integrationtests import ( + "context" "crypto/md5" "encoding/hex" "fmt" @@ -23,6 +24,7 @@ import ( "github.com/kubernetes-csi/csi-proxy/pkg/server" srvtypes "github.com/kubernetes-csi/csi-proxy/pkg/server/types" + "github.com/kubernetes-csi/csi-proxy/pkg/volume" ) // startServer starts the proxy's GRPC servers, and returns a function to shut them down when done with testing @@ -295,3 +297,52 @@ func pathExists(path string) (bool, error) { } return false, err } + +// volumeInit initializes a volume, it creates a VHD, initializes it, +// creates a partition with the max size and formats the volume corresponding to that partition +func volumeInit(volumeClient volume.Interface, t *testing.T) (*VirtualHardDisk, string, func()) { + vhd, vhdCleanup := diskInit(t) + + listRequest := &volume.ListVolumesOnDiskRequest{ + DiskNumber: vhd.DiskNumber, + } + listResponse, err := volumeClient.ListVolumesOnDisk(context.TODO(), listRequest) + if err != nil { + t.Fatalf("List response: %v", err) + } + + volumeIDsLen := len(listResponse.VolumeIds) + if volumeIDsLen != 1 { + t.Fatalf("Number of volumes not equal to 1: %d", volumeIDsLen) + } + volumeID := listResponse.VolumeIds[0] + t.Logf("VolumeId %v", volumeID) + + isVolumeFormattedRequest := &volume.IsVolumeFormattedRequest{ + VolumeId: volumeID, + } + isVolumeFormattedResponse, err := volumeClient.IsVolumeFormatted(context.TODO(), isVolumeFormattedRequest) + if err != nil { + t.Fatalf("Is volume formatted request error: %v", err) + } + if isVolumeFormattedResponse.Formatted { + t.Fatal("Volume formatted. Unexpected !!") + } + + formatVolumeRequest := &volume.FormatVolumeRequest{ + VolumeId: volumeID, + } + _, err = volumeClient.FormatVolume(context.TODO(), formatVolumeRequest) + if err != nil { + t.Fatalf("Volume format failed. Error: %v", err) + } + + isVolumeFormattedResponse, err = volumeClient.IsVolumeFormatted(context.TODO(), isVolumeFormattedRequest) + if err != nil { + t.Fatalf("Is volume formatted request error: %v", err) + } + if !isVolumeFormattedResponse.Formatted { + t.Fatal("Volume should be formatted. Unexpected !!") + } + return vhd, volumeID, vhdCleanup +} diff --git a/integrationtests/volume_test.go b/integrationtests/volume_test.go index 1ffc85951..5c4e45b6c 100644 --- a/integrationtests/volume_test.go +++ b/integrationtests/volume_test.go @@ -158,55 +158,6 @@ func TestVolumeAPIs(t *testing.T) { }) } -// volumeInit initializes a volume, it creates a VHD, initializes it, -// creates a partition with the max size and formats the volume corresponding to that partition -func volumeInit(volumeClient volume.Interface, t *testing.T) (*VirtualHardDisk, string, func()) { - vhd, vhdCleanup := diskInit(t) - - listRequest := &volume.ListVolumesOnDiskRequest{ - DiskNumber: vhd.DiskNumber, - } - listResponse, err := volumeClient.ListVolumesOnDisk(context.TODO(), listRequest) - if err != nil { - t.Fatalf("List response: %v", err) - } - - volumeIDsLen := len(listResponse.VolumeIds) - if volumeIDsLen != 1 { - t.Fatalf("Number of volumes not equal to 1: %d", volumeIDsLen) - } - volumeID := listResponse.VolumeIds[0] - t.Logf("VolumeId %v", volumeID) - - isVolumeFormattedRequest := &volume.IsVolumeFormattedRequest{ - VolumeId: volumeID, - } - isVolumeFormattedResponse, err := volumeClient.IsVolumeFormatted(context.TODO(), isVolumeFormattedRequest) - if err != nil { - t.Fatalf("Is volume formatted request error: %v", err) - } - if isVolumeFormattedResponse.Formatted { - t.Fatal("Volume formatted. Unexpected !!") - } - - formatVolumeRequest := &volume.FormatVolumeRequest{ - VolumeId: volumeID, - } - _, err = volumeClient.FormatVolume(context.TODO(), formatVolumeRequest) - if err != nil { - t.Fatalf("Volume format failed. Error: %v", err) - } - - isVolumeFormattedResponse, err = volumeClient.IsVolumeFormatted(context.TODO(), isVolumeFormattedRequest) - if err != nil { - t.Fatalf("Is volume formatted request error: %v", err) - } - if !isVolumeFormattedResponse.Formatted { - t.Fatal("Volume should be formatted. Unexpected !!") - } - return vhd, volumeID, vhdCleanup -} - func getClosestVolumeFromTargetPathTests(diskClient disk.Interface, volumeClient volume.Interface, t *testing.T) { t.Run("DriveLetterVolume", func(t *testing.T) { vhd, _, vhdCleanup := volumeInit(volumeClient, t) @@ -436,21 +387,6 @@ func mountVolumeTests(diskClient disk.Interface, volumeClient volume.Interface, } } -func volumeTests(t *testing.T) { - volumeClient, err := volume.New(volumeapi.New()) - require.Nil(t, err) - - diskClient, err := disk.New(diskapi.New()) - require.Nil(t, err) - - t.Run("MountVolume", func(t *testing.T) { - mountVolumeTests(diskClient, volumeClient, t) - }) - t.Run("GetClosestVolumeFromTargetPath", func(t *testing.T) { - getClosestVolumeFromTargetPathTests(diskClient, volumeClient, t) - }) -} - func TestVolume(t *testing.T) { t.Run("NegativeDiskTests", func(t *testing.T) { negativeDiskTests(t) @@ -463,8 +399,19 @@ func TestVolume(t *testing.T) { // see https://github.com/actions/virtual-environments/pull/2525 // these tests should be considered frozen from the API point of view - t.Run("volumeTests", func(t *testing.T) { + volumeClient, err := volume.New(volumeapi.New()) + require.Nil(t, err) + + diskClient, err := disk.New(diskapi.New()) + require.Nil(t, err) + + t.Run("MountVolume", func(t *testing.T) { skipTestOnCondition(t, isRunningOnGhActions()) - volumeTests(t) + mountVolumeTests(diskClient, volumeClient, t) + }) + + t.Run("GetClosestVolumeFromTargetPath", func(t *testing.T) { + skipTestOnCondition(t, isRunningOnGhActions()) + getClosestVolumeFromTargetPathTests(diskClient, volumeClient, t) }) }