diff --git a/pkg/gce-pd-csi-driver/controller.go b/pkg/gce-pd-csi-driver/controller.go index c9f3fb607..927aea2d2 100644 --- a/pkg/gce-pd-csi-driver/controller.go +++ b/pkg/gce-pd-csi-driver/controller.go @@ -20,6 +20,7 @@ import ( "math/rand" "regexp" "sort" + "strings" "time" "github.com/GoogleCloudPlatform/k8s-cloud-provider/pkg/cloud/meta" @@ -32,6 +33,7 @@ import ( "k8s.io/apimachinery/pkg/util/uuid" "k8s.io/client-go/util/flowcontrol" "k8s.io/klog/v2" + "k8s.io/utils/strings/slices" "sigs.k8s.io/gcp-compute-persistent-disk-csi-driver/pkg/common" gce "sigs.k8s.io/gcp-compute-persistent-disk-csi-driver/pkg/gce-cloud-provider/compute" @@ -98,6 +100,14 @@ type workItem struct { unpublishReq *csi.ControllerUnpublishVolumeRequest } +// locationRequirements are additional location topology requirements that must be respected when creating a volume. +type locationRequirements struct { + srcVolRegion string + srcVolZone string + srcReplicationType string + cloneReplicationType string +} + var _ csi.ControllerServer = &GCEControllerServer{} const ( @@ -142,6 +152,44 @@ func isDiskReady(disk *gce.CloudDisk) (bool, error) { return false, nil } +// cloningLocationRequirements returns additional location requirements to be applied to the given create volume requests topology. +// If the CreateVolumeRequest will use volume cloning, location requirements in compliance with the volume cloning limitations +// will be returned: https://cloud.google.com/kubernetes-engine/docs/how-to/persistent-volumes/volume-cloning#limitations. +func cloningLocationRequirements(req *csi.CreateVolumeRequest, cloneReplicationType string) (*locationRequirements, error) { + if !useVolumeCloning(req) { + return nil, nil + } + // If we are using volume cloning, this will be set. + volSrc := req.VolumeContentSource.GetVolume() + volSrcVolID := volSrc.GetVolumeId() + + _, sourceVolKey, err := common.VolumeIDToKey(volSrcVolID) + if err != nil { + return nil, fmt.Errorf("volume ID is invalid: %w", err) + } + + isZonalSrcVol := sourceVolKey.Type() == meta.Zonal + if isZonalSrcVol { + region, err := common.GetRegionFromZones([]string{sourceVolKey.Zone}) + if err != nil { + return nil, fmt.Errorf("failed to get region from zones: %w", err) + } + sourceVolKey.Region = region + } + + srcReplicationType := replicationTypeNone + if !isZonalSrcVol { + srcReplicationType = replicationTypeRegionalPD + } + + return &locationRequirements{srcVolZone: sourceVolKey.Zone, srcVolRegion: sourceVolKey.Region, srcReplicationType: srcReplicationType, cloneReplicationType: cloneReplicationType}, nil +} + +// useVolumeCloning returns true if the create volume request should be created with volume cloning. +func useVolumeCloning(req *csi.CreateVolumeRequest) bool { + return req.VolumeContentSource != nil && req.VolumeContentSource.GetVolume() != nil +} + func (gceCS *GCEControllerServer) CreateVolume(ctx context.Context, req *csi.CreateVolumeRequest) (*csi.CreateVolumeResponse, error) { var err error // Validate arguments @@ -177,12 +225,21 @@ func (gceCS *GCEControllerServer) CreateVolume(ctx context.Context, req *csi.Cre if multiWriter { gceAPIVersion = gce.GCEAPIVersionBeta } + + var locationTopReq *locationRequirements + if useVolumeCloning(req) { + locationTopReq, err = cloningLocationRequirements(req, params.ReplicationType) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "failed to get location requirements: %v", err.Error()) + } + } + // Determine the zone or zones+region of the disk var zones []string var volKey *meta.Key switch params.ReplicationType { case replicationTypeNone: - zones, err = pickZones(ctx, gceCS, req.GetAccessibilityRequirements(), 1) + zones, err = pickZones(ctx, gceCS, req.GetAccessibilityRequirements(), 1, locationTopReq) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "CreateVolume failed to pick zones for disk: %v", err.Error()) } @@ -192,7 +249,7 @@ func (gceCS *GCEControllerServer) CreateVolume(ctx context.Context, req *csi.Cre volKey = meta.ZonalKey(name, zones[0]) case replicationTypeRegionalPD: - zones, err = pickZones(ctx, gceCS, req.GetAccessibilityRequirements(), 2) + zones, err = pickZones(ctx, gceCS, req.GetAccessibilityRequirements(), 2, locationTopReq) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "CreateVolume failed to pick zones for disk: %v", err.Error()) } @@ -1358,7 +1415,29 @@ func diskIsAttachedAndCompatible(deviceName string, instance *compute.Instance, return false, nil } -func pickZonesFromTopology(top *csi.TopologyRequirement, numZones int) ([]string, error) { +// pickZonesInRegion will remove any zones that are not in the given region. +func pickZonesInRegion(region string, zones []string) []string { + refinedZones := []string{} + for _, zone := range zones { + if strings.Contains(zone, region) { + refinedZones = append(refinedZones, zone) + } + } + return refinedZones +} + +func prependZone(zone string, zones []string) []string { + newZones := []string{zone} + for i := 0; i < len(zones); i++ { + // Do not add a zone if it is equal to the zone that is already prepended to newZones. + if zones[i] != zone { + newZones = append(newZones, zones[i]) + } + } + return newZones +} + +func pickZonesFromTopology(top *csi.TopologyRequirement, numZones int, locationTopReq *locationRequirements) ([]string, error) { reqZones, err := getZonesFromTopology(top.GetRequisite()) if err != nil { return nil, fmt.Errorf("could not get zones from requisite topology: %w", err) @@ -1368,6 +1447,39 @@ func pickZonesFromTopology(top *csi.TopologyRequirement, numZones int) ([]string return nil, fmt.Errorf("could not get zones from preferred topology: %w", err) } + if locationTopReq != nil { + srcVolZone := locationTopReq.srcVolZone + switch locationTopReq.cloneReplicationType { + // For zonal -> zonal cloning, the source disk zone must match the destination disk zone. + case replicationTypeNone: + // If the source volume zone is not in the topology requirement, we return an error. + if !slices.Contains(prefZones, srcVolZone) && !slices.Contains(reqZones, srcVolZone) { + volumeCloningReq := fmt.Sprintf("clone zone must match source disk zone: %s", srcVolZone) + return nil, fmt.Errorf("failed to find zone from topology %v: %s", top, volumeCloningReq) + } + return []string{srcVolZone}, nil + // For zonal or regional -> regional disk cloning, the source disk region must match the destination disk region. + case replicationTypeRegionalPD: + srcVolRegion := locationTopReq.srcVolRegion + prefZones = pickZonesInRegion(srcVolRegion, prefZones) + reqZones = pickZonesInRegion(srcVolRegion, reqZones) + + if len(prefZones) == 0 && len(reqZones) == 0 { + volumeCloningReq := fmt.Sprintf("clone zone must reside in source disk region %s", srcVolRegion) + return nil, fmt.Errorf("failed to find zone from topology %v: %s", top, volumeCloningReq) + } + + // For zonal -> regional disk cloning, one of the replicated zones must match the source zone. + if locationTopReq.srcReplicationType == replicationTypeNone { + if !slices.Contains(prefZones, srcVolZone) && !slices.Contains(reqZones, srcVolZone) { + volumeCloningReq := fmt.Sprintf("one of the replica zones of the clone must match the source disk zone: %s", srcVolZone) + return nil, fmt.Errorf("failed to find zone from topology %v: %s", top, volumeCloningReq) + } + prefZones = prependZone(srcVolZone, prefZones) + } + } + } + if numZones <= len(prefZones) { return prefZones[0:numZones], nil } else { @@ -1426,16 +1538,25 @@ func getZoneFromSegment(seg map[string]string) (string, error) { return zone, nil } -func pickZones(ctx context.Context, gceCS *GCEControllerServer, top *csi.TopologyRequirement, numZones int) ([]string, error) { +func pickZones(ctx context.Context, gceCS *GCEControllerServer, top *csi.TopologyRequirement, numZones int, locationTopReq *locationRequirements) ([]string, error) { var zones []string var err error if top != nil { - zones, err = pickZonesFromTopology(top, numZones) + zones, err = pickZonesFromTopology(top, numZones, locationTopReq) if err != nil { return nil, fmt.Errorf("failed to pick zones from topology: %w", err) } } else { - zones, err = getDefaultZonesInRegion(ctx, gceCS, []string{gceCS.CloudProvider.GetDefaultZone()}, numZones) + existingZones := []string{gceCS.CloudProvider.GetDefaultZone()} + // We set existingZones to the source volume zone so that for zonal -> zonal cloning, the clone is provisioned + // in the same zone as the source volume, and for zonal -> regional, one of the replicated zones will always + // be the zone of the source volume. For regional -> regional cloning, the srcVolZone will not be set, so we + // just use the default zone. + if locationTopReq != nil && locationTopReq.srcReplicationType == replicationTypeNone { + existingZones = []string{locationTopReq.srcVolZone} + } + // If topology is nil, then the Immediate binding mode was used without setting allowedTopologies in the storageclass. + zones, err = getDefaultZonesInRegion(ctx, gceCS, existingZones, numZones) if err != nil { return nil, fmt.Errorf("failed to get default %v zones in region: %w", numZones, err) } diff --git a/pkg/gce-pd-csi-driver/controller_test.go b/pkg/gce-pd-csi-driver/controller_test.go index 254a83690..d49809c77 100644 --- a/pkg/gce-pd-csi-driver/controller_test.go +++ b/pkg/gce-pd-csi-driver/controller_test.go @@ -1118,8 +1118,112 @@ func TestCreateVolumeWithVolumeSourceFromSnapshot(t *testing.T) { } } +func TestCloningLocationRequirements(t *testing.T) { + testSourceVolumeName := "test-volume-source-name" + testZonalVolumeSourceID := fmt.Sprintf("projects/%s/zones/%s/disks/%s", project, zone, testSourceVolumeName) + testRegionalVolumeSourceID := fmt.Sprintf("projects/%s/regions/%s/disks/%s", project, region, testSourceVolumeName) + + testCases := []struct { + name string + sourceVolumeID string + nilVolumeContentSource bool + reqParameters map[string]string + requestCapacityRange *csi.CapacityRange + replicationType string + expectedLocationRequirements *locationRequirements + expectedErr bool + }{ + { + name: "success zonal disk clone of zonal source disk", + sourceVolumeID: testZonalVolumeSourceID, + requestCapacityRange: stdCapRange, + reqParameters: map[string]string{ + common.ParameterKeyReplicationType: replicationTypeNone, + }, + replicationType: replicationTypeNone, + expectedLocationRequirements: &locationRequirements{srcVolRegion: region, srcVolZone: zone, srcReplicationType: replicationTypeNone, cloneReplicationType: replicationTypeNone}, + expectedErr: false, + }, + { + name: "success regional disk clone of regional source disk", + sourceVolumeID: testRegionalVolumeSourceID, + requestCapacityRange: stdCapRange, + reqParameters: map[string]string{ + common.ParameterKeyReplicationType: replicationTypeRegionalPD, + }, + replicationType: replicationTypeRegionalPD, + expectedLocationRequirements: &locationRequirements{srcVolRegion: region, srcVolZone: "", srcReplicationType: replicationTypeRegionalPD, cloneReplicationType: replicationTypeRegionalPD}, + expectedErr: false, + }, + { + name: "success regional disk clone of zonal data source", + sourceVolumeID: testZonalVolumeSourceID, + requestCapacityRange: stdCapRange, + reqParameters: map[string]string{ + common.ParameterKeyReplicationType: replicationTypeRegionalPD, + }, + replicationType: replicationTypeRegionalPD, + expectedLocationRequirements: &locationRequirements{srcVolRegion: region, srcVolZone: zone, srcReplicationType: replicationTypeNone, cloneReplicationType: replicationTypeRegionalPD}, + expectedErr: false, + }, + { + name: "non-cloning CreateVolumeRequest", + nilVolumeContentSource: true, + requestCapacityRange: stdCapRange, + reqParameters: map[string]string{ + common.ParameterKeyReplicationType: replicationTypeRegionalPD, + }, + replicationType: replicationTypeRegionalPD, + expectedLocationRequirements: nil, + expectedErr: false, + }, + { + name: "failure invalid volumeID", + sourceVolumeID: fmt.Sprintf("projects/%s/disks/%s", project, testSourceVolumeName), + requestCapacityRange: stdCapRange, + reqParameters: map[string]string{ + common.ParameterKeyReplicationType: replicationTypeNone, + }, + replicationType: replicationTypeNone, + expectedLocationRequirements: nil, + expectedErr: true, + }, + } + + for _, tc := range testCases { + t.Logf("test case: %s", tc.name) + req := &csi.CreateVolumeRequest{ + Name: name, + CapacityRange: tc.requestCapacityRange, + VolumeCapabilities: stdVolCaps, + Parameters: tc.reqParameters, + VolumeContentSource: &csi.VolumeContentSource{ + Type: &csi.VolumeContentSource_Volume{ + Volume: &csi.VolumeContentSource_VolumeSource{ + VolumeId: tc.sourceVolumeID, + }, + }, + }, + } + if tc.nilVolumeContentSource { + req.VolumeContentSource = nil + } + + locationRequirements, err := cloningLocationRequirements(req, tc.replicationType) + + if err != nil != tc.expectedErr { + t.Fatalf("Got error %v, expected error %t", err, tc.expectedErr) + } + input := fmt.Sprintf("cloningLocationRequirements(%v, %s", req, tc.replicationType) + if fmt.Sprintf("%v", tc.expectedLocationRequirements) != fmt.Sprintf("%v", locationRequirements) { + t.Fatalf("%s returned unexpected diff got: %v, want %v", input, locationRequirements, tc.expectedLocationRequirements) + } + } +} + func TestCreateVolumeWithVolumeSourceFromVolume(t *testing.T) { testSourceVolumeName := "test-volume-source-name" + testCloneVolumeName := "test-volume-clone" testZonalVolumeSourceID := fmt.Sprintf("projects/%s/zones/%s/disks/%s", project, zone, testSourceVolumeName) testRegionalVolumeSourceID := fmt.Sprintf("projects/%s/regions/%s/disks/%s", project, region, testSourceVolumeName) testSecondZonalVolumeSourceID := fmt.Sprintf("projects/%s/zones/%s/disks/%s", project, "different-zone1", testSourceVolumeName) @@ -1131,14 +1235,33 @@ func TestCreateVolumeWithVolumeSourceFromVolume(t *testing.T) { common.ParameterKeyType: "test-type", common.ParameterKeyReplicationType: replicationTypeRegionalPD, common.ParameterKeyDiskEncryptionKmsKey: "encryption-key", } - topology := &csi.TopologyRequirement{ - Requisite: []*csi.Topology{ - { - Segments: map[string]string{common.TopologyKeyZone: zone}, - }, - { - Segments: map[string]string{common.TopologyKeyZone: secondZone}, - }, + requisiteTopology := []*csi.Topology{ + { + Segments: map[string]string{common.TopologyKeyZone: zone}, + }, + { + Segments: map[string]string{common.TopologyKeyZone: secondZone}, + }, + } + + requisiteAllRegionZonesTopology := []*csi.Topology{ + { + Segments: map[string]string{common.TopologyKeyZone: "country-region-fakethirdzone"}, + }, + { + Segments: map[string]string{common.TopologyKeyZone: zone}, + }, + { + Segments: map[string]string{common.TopologyKeyZone: secondZone}, + }, + } + + prefTopology := []*csi.Topology{ + { + Segments: map[string]string{common.TopologyKeyZone: zone}, + }, + { + Segments: map[string]string{common.TopologyKeyZone: secondZone}, }, } @@ -1153,39 +1276,326 @@ func TestCreateVolumeWithVolumeSourceFromVolume(t *testing.T) { requestCapacityRange *csi.CapacityRange sourceTopology *csi.TopologyRequirement requestTopology *csi.TopologyRequirement + expCloneKey *meta.Key + // Accessible topologies validates that the replica zones are valid for regional disk clones. + expAccessibleTop []*csi.Topology }{ + { - name: "success zonal disk clone of zonal source disk", + name: "success zonal -> zonal cloning, nil topology: immediate binding w/ no allowedTopologies", volumeOnCloud: true, sourceVolumeID: testZonalVolumeSourceID, requestCapacityRange: stdCapRange, sourceCapacityRange: stdCapRange, reqParameters: zonalParams, sourceReqParameters: zonalParams, - sourceTopology: topology, - requestTopology: topology, + // Source volume will be in the zone that is the first element of preferred topologies (country-region-zone) + sourceTopology: &csi.TopologyRequirement{ + Requisite: requisiteTopology, + Preferred: prefTopology, + }, + requestTopology: nil, + expCloneKey: &meta.Key{Name: testCloneVolumeName, Zone: zone, Region: ""}, + expAccessibleTop: []*csi.Topology{ + { + Segments: map[string]string{common.TopologyKeyZone: "country-region-zone"}, + }, + }, }, { - name: "success regional disk clone of regional source disk", + name: "success zonal -> zonal cloning, req = allowedTopologies, pref = req w/ randomly selected zone as first element: immediate binding w/ allowedTopologies", volumeOnCloud: true, - sourceVolumeID: testRegionalVolumeSourceID, + sourceVolumeID: testZonalVolumeSourceID, + requestCapacityRange: stdCapRange, + sourceCapacityRange: stdCapRange, + reqParameters: zonalParams, + sourceReqParameters: zonalParams, + // Source volume will be in the zone that is the first element of preferred topologies (country-region-zone) + sourceTopology: &csi.TopologyRequirement{ + Requisite: requisiteTopology, + Preferred: prefTopology, + }, + requestTopology: &csi.TopologyRequirement{ + Requisite: requisiteTopology, + Preferred: []*csi.Topology{ + { + Segments: map[string]string{common.TopologyKeyZone: secondZone}, + }, + { + Segments: map[string]string{common.TopologyKeyZone: zone}, + }, + }, + }, + expCloneKey: &meta.Key{Name: testCloneVolumeName, Zone: zone, Region: ""}, + expAccessibleTop: []*csi.Topology{ + { + Segments: map[string]string{common.TopologyKeyZone: "country-region-zone"}, + }, + }, + }, + { + name: "success zonal -> zonal cloning, req = allowedTopologies, pref = req w/ src zone as first element: delayed binding w/ allowedTopologies", + volumeOnCloud: true, + sourceVolumeID: testZonalVolumeSourceID, + requestCapacityRange: stdCapRange, + sourceCapacityRange: stdCapRange, + reqParameters: zonalParams, + sourceReqParameters: zonalParams, + // Source volume will be in the zone that is the first element of preferred topologies (country-region-zone) + sourceTopology: &csi.TopologyRequirement{ + Requisite: requisiteTopology, + Preferred: prefTopology, + }, + requestTopology: &csi.TopologyRequirement{ + Requisite: requisiteTopology, + Preferred: prefTopology, + }, + expCloneKey: &meta.Key{Name: testCloneVolumeName, Zone: zone, Region: ""}, + expAccessibleTop: []*csi.Topology{ + { + Segments: map[string]string{common.TopologyKeyZone: "country-region-zone"}, + }, + }, + }, + { + name: "success zonal -> zonal cloning, req = all zones in region, pref = req w/ src zone as first element: delayed binding without allowedTopologies", + volumeOnCloud: true, + sourceVolumeID: testZonalVolumeSourceID, + requestCapacityRange: stdCapRange, + sourceCapacityRange: stdCapRange, + reqParameters: zonalParams, + sourceReqParameters: zonalParams, + // Source volume will be in the zone that is the first element of preferred topologies (country-region-zone) + sourceTopology: &csi.TopologyRequirement{ + Requisite: requisiteTopology, + Preferred: prefTopology, + }, + requestTopology: &csi.TopologyRequirement{ + Requisite: requisiteAllRegionZonesTopology, + Preferred: prefTopology, + }, + expCloneKey: &meta.Key{Name: testCloneVolumeName, Zone: zone, Region: ""}, + expAccessibleTop: []*csi.Topology{ + { + Segments: map[string]string{common.TopologyKeyZone: "country-region-zone"}, + }, + }, + }, + { + name: "success zonal -> regional cloning, nil topology: immediate binding w/ no allowedTopologies", + volumeOnCloud: true, + sourceVolumeID: testZonalVolumeSourceID, requestCapacityRange: stdCapRange, sourceCapacityRange: stdCapRange, reqParameters: regionalParams, - sourceReqParameters: regionalParams, - sourceTopology: topology, - requestTopology: topology, + sourceReqParameters: zonalParams, + sourceTopology: &csi.TopologyRequirement{ + Requisite: requisiteTopology, + Preferred: prefTopology, + }, + requestTopology: nil, + expCloneKey: &meta.Key{Name: testCloneVolumeName, Zone: "", Region: "country-region"}, + expAccessibleTop: []*csi.Topology{ + { + Segments: map[string]string{common.TopologyKeyZone: "country-region-zone"}, + }, + { + Segments: map[string]string{common.TopologyKeyZone: "country-region-fakesecondzone"}, + }, + }, }, { - name: "success regional disk clone of zonal data source", + name: "success zonal -> regional cloning, req = allowedTopologies, pref = req w/ randomly selected zone as first element: immediate binding w/ allowedTopologies", + volumeOnCloud: true, + sourceVolumeID: testZonalVolumeSourceID, + requestCapacityRange: stdCapRange, + sourceCapacityRange: stdCapRange, + reqParameters: regionalParams, + sourceReqParameters: zonalParams, + sourceTopology: &csi.TopologyRequirement{ + Requisite: requisiteTopology, + Preferred: prefTopology, + }, + requestTopology: &csi.TopologyRequirement{ + Requisite: requisiteTopology, + Preferred: []*csi.Topology{ + { + Segments: map[string]string{common.TopologyKeyZone: secondZone}, + }, + { + Segments: map[string]string{common.TopologyKeyZone: zone}, + }, + }, + }, + expCloneKey: &meta.Key{Name: testCloneVolumeName, Zone: "", Region: "country-region"}, + expAccessibleTop: []*csi.Topology{ + { + Segments: map[string]string{common.TopologyKeyZone: "country-region-zone"}, + }, + { + Segments: map[string]string{common.TopologyKeyZone: "country-region-fakesecondzone"}, + }, + }, + }, + { + name: "success zonal -> regional cloning, req = allowedTopologies, pref = req w/ src zone as first element: delayed binding w/ allowedTopologies", + volumeOnCloud: true, + sourceVolumeID: testZonalVolumeSourceID, + requestCapacityRange: stdCapRange, + sourceCapacityRange: stdCapRange, + reqParameters: regionalParams, + sourceReqParameters: zonalParams, + sourceTopology: &csi.TopologyRequirement{ + Requisite: requisiteTopology, + Preferred: prefTopology, + }, + requestTopology: &csi.TopologyRequirement{ + Requisite: requisiteTopology, + Preferred: prefTopology, + }, + expCloneKey: &meta.Key{Name: testCloneVolumeName, Zone: "", Region: "country-region"}, + expAccessibleTop: []*csi.Topology{ + { + Segments: map[string]string{common.TopologyKeyZone: "country-region-zone"}, + }, + { + Segments: map[string]string{common.TopologyKeyZone: "country-region-fakesecondzone"}, + }, + }, + }, + { + name: "success zonal -> regional cloning, req = all zones in region, pref = req w/ src zone as first element: delayed binding without allowedTopologies", volumeOnCloud: true, sourceVolumeID: testZonalVolumeSourceID, requestCapacityRange: stdCapRange, sourceCapacityRange: stdCapRange, reqParameters: regionalParams, sourceReqParameters: zonalParams, - sourceTopology: topology, - requestTopology: topology, + sourceTopology: &csi.TopologyRequirement{ + Requisite: requisiteTopology, + Preferred: prefTopology, + }, + requestTopology: &csi.TopologyRequirement{ + Requisite: requisiteAllRegionZonesTopology, + Preferred: prefTopology, + }, + expCloneKey: &meta.Key{Name: testCloneVolumeName, Zone: "", Region: "country-region"}, + expAccessibleTop: []*csi.Topology{ + { + Segments: map[string]string{common.TopologyKeyZone: "country-region-zone"}, + }, + { + Segments: map[string]string{common.TopologyKeyZone: "country-region-fakesecondzone"}, + }, + }, + }, + { + name: "success regional -> regional cloning, nil topology: immediate binding w/ no allowedTopologies", + volumeOnCloud: true, + sourceVolumeID: testRegionalVolumeSourceID, + requestCapacityRange: stdCapRange, + sourceCapacityRange: stdCapRange, + reqParameters: regionalParams, + sourceReqParameters: regionalParams, + sourceTopology: &csi.TopologyRequirement{ + Requisite: requisiteTopology, + Preferred: prefTopology, + }, + requestTopology: nil, + expCloneKey: &meta.Key{Name: testCloneVolumeName, Zone: "", Region: "country-region"}, + expAccessibleTop: []*csi.Topology{ + { + Segments: map[string]string{common.TopologyKeyZone: "country-region-zone"}, + }, + { + Segments: map[string]string{common.TopologyKeyZone: "country-region-fakesecondzone"}, + }, + }, + }, + { + name: "success regional -> regional cloning, req = allowedTopologies, pref = req w/ randomly selected zone as first element: immediate binding w/ allowedTopologies", + volumeOnCloud: true, + sourceVolumeID: testRegionalVolumeSourceID, + requestCapacityRange: stdCapRange, + sourceCapacityRange: stdCapRange, + reqParameters: regionalParams, + sourceReqParameters: regionalParams, + sourceTopology: &csi.TopologyRequirement{ + Requisite: requisiteTopology, + Preferred: prefTopology, + }, + requestTopology: &csi.TopologyRequirement{ + Requisite: requisiteTopology, + Preferred: []*csi.Topology{ + { + Segments: map[string]string{common.TopologyKeyZone: secondZone}, + }, + { + Segments: map[string]string{common.TopologyKeyZone: zone}, + }, + }, + }, + expCloneKey: &meta.Key{Name: testCloneVolumeName, Zone: "", Region: "country-region"}, + expAccessibleTop: []*csi.Topology{ + { + Segments: map[string]string{common.TopologyKeyZone: "country-region-zone"}, + }, + { + Segments: map[string]string{common.TopologyKeyZone: "country-region-fakesecondzone"}, + }, + }, + }, + { + name: "success regional -> regional cloning, req = allowedTopologies, pref = req w/ src zone as first element: delayed binding w/ allowedTopologies", + volumeOnCloud: true, + sourceVolumeID: testRegionalVolumeSourceID, + requestCapacityRange: stdCapRange, + sourceCapacityRange: stdCapRange, + reqParameters: regionalParams, + sourceReqParameters: regionalParams, + sourceTopology: &csi.TopologyRequirement{ + Requisite: requisiteTopology, + Preferred: prefTopology, + }, + requestTopology: &csi.TopologyRequirement{ + Requisite: requisiteTopology, + Preferred: prefTopology, + }, + expCloneKey: &meta.Key{Name: testCloneVolumeName, Zone: "", Region: "country-region"}, + expAccessibleTop: []*csi.Topology{ + { + Segments: map[string]string{common.TopologyKeyZone: "country-region-zone"}, + }, + { + Segments: map[string]string{common.TopologyKeyZone: "country-region-fakesecondzone"}, + }, + }, + }, + { + name: "success regional -> regional cloning, req = all zones in region, pref = req w/ src zone as first element: delayed binding without allowedTopologies", + volumeOnCloud: true, + sourceVolumeID: testRegionalVolumeSourceID, + requestCapacityRange: stdCapRange, + sourceCapacityRange: stdCapRange, + reqParameters: regionalParams, + sourceReqParameters: regionalParams, + sourceTopology: &csi.TopologyRequirement{ + Requisite: requisiteTopology, + Preferred: prefTopology, + }, + requestTopology: &csi.TopologyRequirement{ + Requisite: requisiteTopology, + Preferred: prefTopology, + }, + expCloneKey: &meta.Key{Name: testCloneVolumeName, Zone: "", Region: "country-region"}, + expAccessibleTop: []*csi.Topology{ + { + Segments: map[string]string{common.TopologyKeyZone: "country-region-zone"}, + }, + { + Segments: map[string]string{common.TopologyKeyZone: "country-region-fakesecondzone"}, + }, + }, }, { name: "fail regional disk clone with no matching replica zone of zonal data source", @@ -1196,7 +1606,10 @@ func TestCreateVolumeWithVolumeSourceFromVolume(t *testing.T) { sourceCapacityRange: stdCapRange, reqParameters: regionalParams, sourceReqParameters: zonalParams, - sourceTopology: topology, + sourceTopology: &csi.TopologyRequirement{ + Requisite: requisiteTopology, + Preferred: prefTopology, + }, requestTopology: &csi.TopologyRequirement{ Requisite: []*csi.Topology{ { @@ -1219,8 +1632,14 @@ func TestCreateVolumeWithVolumeSourceFromVolume(t *testing.T) { sourceReqParameters: map[string]string{ common.ParameterKeyType: "different-type", }, - sourceTopology: topology, - requestTopology: topology, + sourceTopology: &csi.TopologyRequirement{ + Requisite: requisiteTopology, + Preferred: prefTopology, + }, + requestTopology: &csi.TopologyRequirement{ + Requisite: requisiteTopology, + Preferred: prefTopology, + }, }, { name: "fail zonal disk clone with different DiskEncryptionKMSKey", @@ -1234,8 +1653,14 @@ func TestCreateVolumeWithVolumeSourceFromVolume(t *testing.T) { common.ParameterKeyType: "test-type", common.ParameterKeyReplicationType: replicationTypeNone, common.ParameterKeyDiskEncryptionKmsKey: "different-encryption-key", }, - sourceTopology: topology, - requestTopology: topology, + sourceTopology: &csi.TopologyRequirement{ + Requisite: requisiteTopology, + Preferred: prefTopology, + }, + requestTopology: &csi.TopologyRequirement{ + Requisite: requisiteTopology, + Preferred: prefTopology, + }, }, { name: "fail zonal disk clone with different zone", @@ -1256,7 +1681,10 @@ func TestCreateVolumeWithVolumeSourceFromVolume(t *testing.T) { }, }, }, - requestTopology: topology, + requestTopology: &csi.TopologyRequirement{ + Requisite: requisiteTopology, + Preferred: prefTopology, + }, }, { name: "fail zonal disk clone of regional data source", @@ -1267,8 +1695,14 @@ func TestCreateVolumeWithVolumeSourceFromVolume(t *testing.T) { sourceCapacityRange: stdCapRange, reqParameters: zonalParams, sourceReqParameters: regionalParams, - sourceTopology: topology, - requestTopology: topology, + sourceTopology: &csi.TopologyRequirement{ + Requisite: requisiteTopology, + Preferred: prefTopology, + }, + requestTopology: &csi.TopologyRequirement{ + Requisite: requisiteTopology, + Preferred: prefTopology, + }, }, { @@ -1280,7 +1714,10 @@ func TestCreateVolumeWithVolumeSourceFromVolume(t *testing.T) { sourceCapacityRange: stdCapRange, reqParameters: stdParams, sourceReqParameters: stdParams, - requestTopology: topology, + requestTopology: &csi.TopologyRequirement{ + Requisite: requisiteTopology, + Preferred: prefTopology, + }, }, { name: "fail invalid source disk volume id format", @@ -1291,7 +1728,10 @@ func TestCreateVolumeWithVolumeSourceFromVolume(t *testing.T) { sourceCapacityRange: stdCapRange, reqParameters: stdParams, sourceReqParameters: stdParams, - requestTopology: topology, + requestTopology: &csi.TopologyRequirement{ + Requisite: requisiteTopology, + Preferred: prefTopology, + }, }, { name: "fail zonal disk clone with smaller disk capacity", @@ -1304,16 +1744,23 @@ func TestCreateVolumeWithVolumeSourceFromVolume(t *testing.T) { }, reqParameters: zonalParams, sourceReqParameters: zonalParams, - sourceTopology: topology, - requestTopology: topology, - }} + sourceTopology: &csi.TopologyRequirement{ + Requisite: requisiteTopology, + Preferred: prefTopology, + }, + requestTopology: &csi.TopologyRequirement{ + Requisite: requisiteTopology, + Preferred: prefTopology, + }, + }, + } for _, tc := range testCases { t.Logf("test case: %s", tc.name) gceDriver := initGCEDriver(t, nil) req := &csi.CreateVolumeRequest{ - Name: name, + Name: testCloneVolumeName, CapacityRange: tc.requestCapacityRange, VolumeCapabilities: stdVolCaps, Parameters: tc.reqParameters, @@ -1348,7 +1795,6 @@ func TestCreateVolumeWithVolumeSourceFromVolume(t *testing.T) { } resp, err := gceDriver.cs.CreateVolume(context.Background(), req) - t.Logf("response: %v err: %v", resp, err) if err != nil { serverError, ok := status.FromError(err) if !ok { @@ -1364,14 +1810,39 @@ func TestCreateVolumeWithVolumeSourceFromVolume(t *testing.T) { } // Make sure the response has the source volume. - sourceVolume := resp.GetVolume() - if sourceVolume.ContentSource == nil || sourceVolume.ContentSource.Type == nil || - sourceVolume.ContentSource.GetVolume() == nil || sourceVolume.ContentSource.GetVolume().VolumeId == "" { + respVolume := resp.GetVolume() + if respVolume.ContentSource == nil || respVolume.ContentSource.Type == nil || + respVolume.ContentSource.GetVolume() == nil || respVolume.ContentSource.GetVolume().VolumeId == "" { t.Fatalf("Expected volume content source to have volume ID, got none") } + // Validate that the cloned volume is in the region/zone that we expect + cloneVolID := respVolume.VolumeId + _, cloneVolKey, err := common.VolumeIDToKey(cloneVolID) + if err != nil { + t.Fatalf("failed to get key from volume id %q: %v", cloneVolID, err) + } + if cloneVolKey.String() != tc.expCloneKey.String() { + t.Fatalf("got clone volume key: %q, expected clone volume key: %q", cloneVolKey.String(), tc.expCloneKey.String()) + } + if !accessibleTopologiesEqual(respVolume.AccessibleTopology, tc.expAccessibleTop) { + t.Fatalf("got accessible topology: %q, expected accessible topology: %q", fmt.Sprintf("%+v", respVolume.AccessibleTopology), fmt.Sprintf("%+v", tc.expAccessibleTop)) + + } } } +func sortTopologies(in []*csi.Topology) { + sort.Slice(in, func(i, j int) bool { + return in[i].Segments[common.TopologyKeyZone] < in[j].Segments[common.TopologyKeyZone] + }) +} + +func accessibleTopologiesEqual(got []*csi.Topology, expected []*csi.Topology) bool { + sortTopologies(got) + sortTopologies(expected) + return fmt.Sprintf("%+v", got) == fmt.Sprintf("%+v", expected) +} + func TestCreateVolumeRandomRequisiteTopology(t *testing.T) { req := &csi.CreateVolumeRequest{ Name: "test-name", @@ -1802,10 +2273,93 @@ func TestGetZonesFromTopology(t *testing.T) { } } +func TestPickZonesInRegion(t *testing.T) { + testCases := []struct { + name string + region string + zones []string + expZones []string + }{ + { + name: "all zones in region", + region: "us-central1", + zones: []string{"us-central1-a", "us-central1-b", "us-central1-c"}, + expZones: []string{"us-central1-a", "us-central1-b", "us-central1-c"}, + }, + { + name: "removes zones not in region", + region: "us-central1", + zones: []string{"us-central1-a", "us-central1-b", "us-central1-c", "us-east1-a, us-west1-a"}, + expZones: []string{"us-central1-a", "us-central1-b", "us-central1-c"}, + }, + { + name: "region not in zones", + region: "us-west1", + zones: []string{"us-central1-a", "us-central1-b", "us-central1-c"}, + expZones: []string{}, + }, + { + name: "empty zones", + region: "us-central1", + zones: []string{}, + expZones: []string{}, + }, + } + for _, tc := range testCases { + t.Logf("test case: %s", tc.name) + gotZones := pickZonesInRegion(tc.region, tc.zones) + if !sets.NewString(gotZones...).Equal(sets.NewString(tc.expZones...)) { + t.Errorf("Got zones: %v, expected: %v", gotZones, tc.expZones) + } + } +} + +func TestPrependZone(t *testing.T) { + testCases := []struct { + name string + zone string + zones []string + expZones []string + }{ + { + name: "zone already at index 0", + zone: "us-central1-a", + zones: []string{"us-central1-a", "us-central1-b", "us-central1-c"}, + expZones: []string{"us-central1-a", "us-central1-b", "us-central1-c"}, + }, + { + name: "zone at index 1", + zone: "us-central1-b", + zones: []string{"us-central1-a", "us-central1-b", "us-central1-c"}, + expZones: []string{"us-central1-b", "us-central1-a", "us-central1-c"}, + }, + { + name: "zone not in zones", + zone: "us-central1-f", + zones: []string{"us-central1-a", "us-central1-b", "us-central1-c"}, + expZones: []string{"us-central1-f", "us-central1-a", "us-central1-b", "us-central1-c"}, + }, + { + name: "empty zones", + zone: "us-central1-a", + zones: []string{}, + expZones: []string{"us-central1-a"}, + }, + } + for _, tc := range testCases { + t.Logf("test case: %s", tc.name) + gotZones := prependZone(tc.zone, tc.zones) + if !zonesEqual(gotZones, tc.expZones) { + t.Errorf("Got zones: %v, expected: %v", gotZones, tc.expZones) + } + } +} + func TestPickZonesFromTopology(t *testing.T) { testCases := []struct { name string top *csi.TopologyRequirement + locReq *locationRequirements numZones int expZones []string expErr bool @@ -1839,6 +2393,36 @@ func TestPickZonesFromTopology(t *testing.T) { numZones: 2, expZones: []string{"topology-zone2", "topology-zone3"}, }, + { + name: "success: preferred, locationRequirements[region:us-central1, zone:us-central1-a, srcReplicationType:none, cloneReplicationType:none]", + top: &csi.TopologyRequirement{ + Requisite: []*csi.Topology{ + { + Segments: map[string]string{common.TopologyKeyZone: "us-central1-c"}, + }, + { + Segments: map[string]string{common.TopologyKeyZone: "us-central1-a"}, + }, + { + Segments: map[string]string{common.TopologyKeyZone: "us-central1-b"}, + }, + }, + Preferred: []*csi.Topology{ + { + Segments: map[string]string{common.TopologyKeyZone: "us-central1-b"}, + }, + { + Segments: map[string]string{common.TopologyKeyZone: "us-central1-c"}, + }, + { + Segments: map[string]string{common.TopologyKeyZone: "us-central1-a"}, + }, + }, + }, + locReq: &locationRequirements{srcVolRegion: "us-central1", srcVolZone: "us-central1-a", srcReplicationType: replicationTypeNone, cloneReplicationType: replicationTypeNone}, + numZones: 1, + expZones: []string{"us-central1-a"}, + }, { name: "success: preferred and requisite", top: &csi.TopologyRequirement{ @@ -1874,6 +2458,72 @@ func TestPickZonesFromTopology(t *testing.T) { numZones: 5, expZones: []string{"topology-zone2", "topology-zone3", "topology-zone1", "topology-zone5", "topology-zone6"}, }, + { + name: "success: preferred and requisite, locationRequirements[region:us-central1, zone:us-central1-a, srcReplicationType:regional-pd, cloneReplicationType:regional-pd]", + top: &csi.TopologyRequirement{ + Requisite: []*csi.Topology{ + { + Segments: map[string]string{common.TopologyKeyZone: "us-central1-c"}, + }, + { + Segments: map[string]string{common.TopologyKeyZone: "us-central1-d"}, + }, + { + Segments: map[string]string{common.TopologyKeyZone: "us-central1-f"}, + }, + { + Segments: map[string]string{common.TopologyKeyZone: "us-west1-a"}, + }, + }, + Preferred: []*csi.Topology{ + { + Segments: map[string]string{common.TopologyKeyZone: "us-central1-b"}, + }, + { + Segments: map[string]string{common.TopologyKeyZone: "us-central1-a"}, + }, + { + Segments: map[string]string{common.TopologyKeyZone: "us-east1-a"}, + }, + }, + }, + locReq: &locationRequirements{srcVolRegion: "us-central1", srcVolZone: "us-central1-a", srcReplicationType: replicationTypeRegionalPD, cloneReplicationType: replicationTypeRegionalPD}, + numZones: 5, + expZones: []string{"us-central1-b", "us-central1-c", "us-central1-a", "us-central1-d", "us-central1-f"}, + }, + { + name: "success: preferred and requisite, locationRequirements[region:us-central1, zone:us-central1-a, srcReplicationType:none, cloneReplicationType:regional-pd]", + top: &csi.TopologyRequirement{ + Requisite: []*csi.Topology{ + { + Segments: map[string]string{common.TopologyKeyZone: "us-central1-c"}, + }, + { + Segments: map[string]string{common.TopologyKeyZone: "us-central1-d"}, + }, + { + Segments: map[string]string{common.TopologyKeyZone: "us-central1-f"}, + }, + { + Segments: map[string]string{common.TopologyKeyZone: "us-west1-a"}, + }, + }, + Preferred: []*csi.Topology{ + { + Segments: map[string]string{common.TopologyKeyZone: "us-central1-b"}, + }, + { + Segments: map[string]string{common.TopologyKeyZone: "us-central1-a"}, + }, + { + Segments: map[string]string{common.TopologyKeyZone: "us-east1-a"}, + }, + }, + }, + locReq: &locationRequirements{srcVolRegion: "us-central1", srcVolZone: "us-central1-a", srcReplicationType: replicationTypeNone, cloneReplicationType: replicationTypeRegionalPD}, + numZones: 5, + expZones: []string{"us-central1-a", "us-central1-b", "us-central1-c", "us-central1-d", "us-central1-f"}, + }, { name: "fail: not enough topologies", top: &csi.TopologyRequirement{ @@ -1903,6 +2553,114 @@ func TestPickZonesFromTopology(t *testing.T) { numZones: 4, expErr: true, }, + { + name: "fail: no topologies that match locationRequirment, locationRequirements[region:us-east1, zone:us-east1-a, replicationType:none]", + top: &csi.TopologyRequirement{ + Requisite: []*csi.Topology{ + { + Segments: map[string]string{common.TopologyKeyZone: "us-central1-c"}, + }, + { + Segments: map[string]string{common.TopologyKeyZone: "us-central1-a"}, + }, + { + Segments: map[string]string{common.TopologyKeyZone: "us-central1-b"}, + }, + }, + Preferred: []*csi.Topology{ + { + Segments: map[string]string{common.TopologyKeyZone: "us-central1-b"}, + }, + { + Segments: map[string]string{common.TopologyKeyZone: "us-central1-c"}, + }, + { + Segments: map[string]string{common.TopologyKeyZone: "us-central1-a"}, + }, + }, + }, + locReq: &locationRequirements{srcVolRegion: "us-east1", srcVolZone: "us-east1-a", cloneReplicationType: replicationTypeNone}, + numZones: 1, + expErr: true, + }, + { + name: "fail: no topologies that match locationRequirment, locationRequirements[region:us-east1, zone:us-east1-a, replicationType:regional-pd]", + top: &csi.TopologyRequirement{ + Requisite: []*csi.Topology{ + { + Segments: map[string]string{common.TopologyKeyZone: "us-central1-c"}, + }, + { + Segments: map[string]string{common.TopologyKeyZone: "us-central1-a"}, + }, + { + Segments: map[string]string{common.TopologyKeyZone: "us-central1-b"}, + }, + }, + Preferred: []*csi.Topology{ + { + Segments: map[string]string{common.TopologyKeyZone: "us-central1-b"}, + }, + { + Segments: map[string]string{common.TopologyKeyZone: "us-central1-c"}, + }, + { + Segments: map[string]string{common.TopologyKeyZone: "us-central1-a"}, + }, + }, + }, + locReq: &locationRequirements{srcVolRegion: "us-east1", srcVolZone: "us-east1-a", cloneReplicationType: replicationTypeRegionalPD}, + numZones: 2, + expErr: true, + }, + { + name: "fail: not enough topologies, locationRequirements[region:us-central1, zone:us-central1-a, replicationType:regional-pd]", + top: &csi.TopologyRequirement{ + Requisite: []*csi.Topology{ + { + Segments: map[string]string{common.TopologyKeyZone: "us-central1-c"}, + }, + { + Segments: map[string]string{common.TopologyKeyZone: "us-central1-a"}, + }, + { + Segments: map[string]string{common.TopologyKeyZone: "us-central1-b"}, + }, + }, + Preferred: []*csi.Topology{ + { + Segments: map[string]string{common.TopologyKeyZone: "us-central1-b"}, + }, + { + Segments: map[string]string{common.TopologyKeyZone: "us-central1-c"}, + }, + { + Segments: map[string]string{common.TopologyKeyZone: "us-central1-a"}, + }, + }, + }, + locReq: &locationRequirements{srcVolRegion: "us-central1", srcVolZone: "us-central1-a", cloneReplicationType: replicationTypeRegionalPD}, + numZones: 4, + expErr: true, + }, + { + name: "success: only requisite, locationRequirements[region:us-central1, zone:us-central1-a, replicationType:regional-pd", + top: &csi.TopologyRequirement{ + Requisite: []*csi.Topology{ + { + Segments: map[string]string{common.TopologyKeyZone: "us-central1-c"}, + }, + { + Segments: map[string]string{common.TopologyKeyZone: "us-central1-a"}, + }, + { + Segments: map[string]string{common.TopologyKeyZone: "us-central1-b"}, + }, + }, + }, + numZones: 3, + expZones: []string{"us-central1-b", "us-central1-c", "us-central1-a"}, + }, { name: "success: only requisite", top: &csi.TopologyRequirement{ @@ -1924,12 +2682,12 @@ func TestPickZonesFromTopology(t *testing.T) { } for _, tc := range testCases { t.Logf("test case: %s", tc.name) - gotZones, err := pickZonesFromTopology(tc.top, tc.numZones) + gotZones, err := pickZonesFromTopology(tc.top, tc.numZones, tc.locReq) if err != nil && !tc.expErr { - t.Errorf("Did not expect error but got: %v", err) + t.Errorf("got error: %v, but did not expect error", err) } if err == nil && tc.expErr { - t.Errorf("Expected error but got none") + t.Errorf("got no error, but expected error") } if !sets.NewString(gotZones...).Equal(sets.NewString(tc.expZones...)) { t.Errorf("Expected zones: %v, but got: %v", tc.expZones, gotZones) @@ -1937,6 +2695,18 @@ func TestPickZonesFromTopology(t *testing.T) { } } +func zonesEqual(gotZones, expectedZones []string) bool { + if len(gotZones) != len(expectedZones) { + return false + } + for i := 0; i < len(gotZones); i++ { + if gotZones[i] != expectedZones[i] { + return false + } + } + return true +} + func TestPickRandAndConsecutive(t *testing.T) { rand.Seed(time.Now().UnixNano()) testCases := []struct { diff --git a/test/e2e/tests/single_zone_e2e_test.go b/test/e2e/tests/single_zone_e2e_test.go index 7c8d7e05a..960757995 100644 --- a/test/e2e/tests/single_zone_e2e_test.go +++ b/test/e2e/tests/single_zone_e2e_test.go @@ -359,8 +359,7 @@ var _ = Describe("GCE PD CSI Driver", func() { Expect(cloudDisk.Name).To(Equal(volName)) Expect(len(cloudDisk.ReplicaZones)).To(Equal(2)) for _, replicaZone := range cloudDisk.ReplicaZones { - tokens := strings.Split(replicaZone, "/") - actualZone := tokens[len(tokens)-1] + actualZone := zoneFromURL(replicaZone) gotRegion, err := common.GetRegionFromZones([]string{actualZone}) Expect(err).To(BeNil(), "failed to get region from actual zone %v", actualZone) Expect(gotRegion).To(Equal(region), "Got region from replica zone that did not match supplied region") @@ -677,8 +676,7 @@ var _ = Describe("GCE PD CSI Driver", func() { Expect(cloudDisk.Name).To(Equal(volName)) Expect(len(cloudDisk.ReplicaZones)).To(Equal(2)) for _, replicaZone := range cloudDisk.ReplicaZones { - tokens := strings.Split(replicaZone, "/") - actualZone := tokens[len(tokens)-1] + actualZone := zoneFromURL(replicaZone) gotRegion, err := common.GetRegionFromZones([]string{actualZone}) Expect(err).To(BeNil(), "failed to get region from actual zone %v", actualZone) Expect(gotRegion).To(Equal(region), "Got region from replica zone that did not match supplied region") @@ -1071,6 +1069,10 @@ var _ = Describe("GCE PD CSI Driver", func() { Expect(cloudDisk.Status).To(Equal(readyState)) Expect(cloudDisk.SizeGb).To(Equal(defaultSizeGb)) Expect(cloudDisk.Name).To(Equal(volName)) + // Validate the the clone disk zone matches the source disk zone. + _, srcKey, err := common.VolumeIDToKey(srcVolID) + Expect(err).To(BeNil(), "Could not get source volume key from id") + Expect(zoneFromURL(cloudDisk.Zone)).To(Equal(srcKey.Zone)) defer func() { // Delete Disk controllerClient.DeleteVolume(volID) @@ -1122,9 +1124,77 @@ var _ = Describe("GCE PD CSI Driver", func() { Expect(cloudDisk.SizeGb).To(Equal(defaultRepdSizeGb)) Expect(cloudDisk.Name).To(Equal(volName)) Expect(len(cloudDisk.ReplicaZones)).To(Equal(2)) + replicaZonesCompatible := false + _, srcKey, err := common.VolumeIDToKey(srcVolID) + Expect(err).To(BeNil(), "Could not get source volume key from id") for _, replicaZone := range cloudDisk.ReplicaZones { - tokens := strings.Split(replicaZone, "/") - actualZone := tokens[len(tokens)-1] + actualZone := zoneFromURL(replicaZone) + if actualZone == srcKey.Zone { + replicaZonesCompatible = true + } + gotRegion, err := common.GetRegionFromZones([]string{actualZone}) + Expect(err).To(BeNil(), "failed to get region from actual zone %v", actualZone) + Expect(gotRegion).To(Equal(region), "Got region from replica zone that did not match supplied region") + } + // Validate that one of the replicaZones of the clone matches the zone of the source disk. + Expect(replicaZonesCompatible).To(Equal(true)) + defer func() { + // Delete Disk + controllerClient.DeleteVolume(volID) + Expect(err).To(BeNil(), "DeleteVolume failed") + + // Validate Disk Deleted + _, err = computeService.RegionDisks.Get(p, region, volName).Do() + Expect(gce.IsGCEError(err, "notFound")).To(BeTrue(), "Expected disk to not be found") + }() + }) + + It("Should successfully create RePD from a RePD VolumeContentSource", func() { + Expect(testContexts).ToNot(BeEmpty()) + testContext := getRandomTestContext() + + controllerInstance := testContext.Instance + controllerClient := testContext.Client + + p, z, _ := controllerInstance.GetIdentity() + + region, err := common.GetRegionFromZones([]string{z}) + Expect(err).To(BeNil(), "Failed to get region from zones") + + // Create Source Disk + srcVolName := testNamePrefix + string(uuid.NewUUID()) + srcVolID, err := controllerClient.CreateVolume(srcVolName, map[string]string{ + common.ParameterKeyReplicationType: "regional-pd", + }, defaultRepdSizeGb, nil, nil) + // Create Disk + volName := testNamePrefix + string(uuid.NewUUID()) + volID, err := controllerClient.CreateVolume(volName, map[string]string{ + common.ParameterKeyReplicationType: "regional-pd", + }, defaultRepdSizeGb, nil, + &csi.VolumeContentSource{ + Type: &csi.VolumeContentSource_Volume{ + Volume: &csi.VolumeContentSource_VolumeSource{ + VolumeId: srcVolID, + }, + }, + }) + + Expect(err).To(BeNil(), "CreateVolume failed with error: %v", err) + + // Validate Disk Created + cloudDisk, err := computeService.RegionDisks.Get(p, region, volName).Do() + Expect(err).To(BeNil(), "Could not get disk from cloud directly") + Expect(cloudDisk.Type).To(ContainSubstring(standardDiskType)) + Expect(cloudDisk.Status).To(Equal(readyState)) + Expect(cloudDisk.SizeGb).To(Equal(defaultRepdSizeGb)) + Expect(cloudDisk.Name).To(Equal(volName)) + Expect(len(cloudDisk.ReplicaZones)).To(Equal(2)) + // Validate that the replicaZones of the clone match the replicaZones of the source disk. + srcCloudDisk, err := computeService.RegionDisks.Get(p, region, srcVolName).Do() + Expect(err).To(BeNil(), "Could not get source disk from cloud directly") + Expect(srcCloudDisk.ReplicaZones).To(Equal(cloudDisk.ReplicaZones)) + for _, replicaZone := range cloudDisk.ReplicaZones { + actualZone := zoneFromURL(replicaZone) gotRegion, err := common.GetRegionFromZones([]string{actualZone}) Expect(err).To(BeNil(), "failed to get region from actual zone %v", actualZone) Expect(gotRegion).To(Equal(region), "Got region from replica zone that did not match supplied region") @@ -1263,6 +1333,16 @@ func cleanSelfLink(selfLink string) string { return r.ReplaceAllString(selfLink, "") } +// Returns the zone from the URL with the format https://compute.googleapis.com/compute/v1/projects/{project}/zones/{zone}. +// Returns the empty string if the zone cannot be abstracted from the URL. +func zoneFromURL(url string) string { + tokens := strings.Split(url, "/") + if len(tokens) == 0 { + return "" + } + return tokens[len(tokens)-1] +} + func setupKeyRing(ctx context.Context, parentName string, keyRingId string) (*kmspb.CryptoKey, []string) { // Create KeyRing ringReq := &kmspb.CreateKeyRingRequest{