Skip to content

Commit 09e96ae

Browse files
amacaskillsunnylovestiramisu
authored andcommitted
fix bug where volume cloning topology requirements are ignored when chosing the location of the volume
1 parent d630e83 commit 09e96ae

File tree

3 files changed

+1022
-52
lines changed

3 files changed

+1022
-52
lines changed

pkg/gce-pd-csi-driver/controller.go

Lines changed: 126 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import (
3333
"k8s.io/apimachinery/pkg/util/uuid"
3434
"k8s.io/client-go/util/flowcontrol"
3535
"k8s.io/klog/v2"
36+
"k8s.io/utils/strings/slices"
3637

3738
"sigs.k8s.io/gcp-compute-persistent-disk-csi-driver/pkg/common"
3839
gce "sigs.k8s.io/gcp-compute-persistent-disk-csi-driver/pkg/gce-cloud-provider/compute"
@@ -99,6 +100,14 @@ type workItem struct {
99100
unpublishReq *csi.ControllerUnpublishVolumeRequest
100101
}
101102

103+
// locationRequirements are additional location topology requirements that must be respected when creating a volume.
104+
type locationRequirements struct {
105+
srcVolRegion string
106+
srcVolZone string
107+
srcReplicationType string
108+
cloneReplicationType string
109+
}
110+
102111
var _ csi.ControllerServer = &GCEControllerServer{}
103112

104113
const (
@@ -143,6 +152,44 @@ func isDiskReady(disk *gce.CloudDisk) (bool, error) {
143152
return false, nil
144153
}
145154

155+
// cloningLocationRequirements returns additional location requirements to be applied to the given create volume requests topology.
156+
// If the CreateVolumeRequest will use volume cloning, location requirements in compliance with the volume cloning limitations
157+
// will be returned: https://cloud.google.com/kubernetes-engine/docs/how-to/persistent-volumes/volume-cloning#limitations.
158+
func cloningLocationRequirements(req *csi.CreateVolumeRequest, cloneReplicationType string) (*locationRequirements, error) {
159+
if !useVolumeCloning(req) {
160+
return nil, nil
161+
}
162+
// If we are using volume cloning, this will be set.
163+
volSrc := req.VolumeContentSource.GetVolume()
164+
volSrcVolID := volSrc.GetVolumeId()
165+
166+
_, sourceVolKey, err := common.VolumeIDToKey(volSrcVolID)
167+
if err != nil {
168+
return nil, fmt.Errorf("volume ID is invalid: %w", err)
169+
}
170+
171+
isZonalSrcVol := sourceVolKey.Type() == meta.Zonal
172+
if isZonalSrcVol {
173+
region, err := common.GetRegionFromZones([]string{sourceVolKey.Zone})
174+
if err != nil {
175+
return nil, fmt.Errorf("failed to get region from zones: %w", err)
176+
}
177+
sourceVolKey.Region = region
178+
}
179+
180+
srcReplicationType := replicationTypeNone
181+
if !isZonalSrcVol {
182+
srcReplicationType = replicationTypeRegionalPD
183+
}
184+
185+
return &locationRequirements{srcVolZone: sourceVolKey.Zone, srcVolRegion: sourceVolKey.Region, srcReplicationType: srcReplicationType, cloneReplicationType: cloneReplicationType}, nil
186+
}
187+
188+
// useVolumeCloning returns true if the create volume request should be created with volume cloning.
189+
func useVolumeCloning(req *csi.CreateVolumeRequest) bool {
190+
return req.VolumeContentSource != nil && req.VolumeContentSource.GetVolume() != nil
191+
}
192+
146193
func (gceCS *GCEControllerServer) CreateVolume(ctx context.Context, req *csi.CreateVolumeRequest) (*csi.CreateVolumeResponse, error) {
147194
var err error
148195
// Validate arguments
@@ -178,12 +225,21 @@ func (gceCS *GCEControllerServer) CreateVolume(ctx context.Context, req *csi.Cre
178225
if multiWriter {
179226
gceAPIVersion = gce.GCEAPIVersionBeta
180227
}
228+
229+
var locationTopReq *locationRequirements
230+
if useVolumeCloning(req) {
231+
locationTopReq, err = cloningLocationRequirements(req, params.ReplicationType)
232+
if err != nil {
233+
return nil, status.Errorf(codes.InvalidArgument, "failed to get location requirements: %v", err.Error())
234+
}
235+
}
236+
181237
// Determine the zone or zones+region of the disk
182238
var zones []string
183239
var volKey *meta.Key
184240
switch params.ReplicationType {
185241
case replicationTypeNone:
186-
zones, err = pickZones(ctx, gceCS, req.GetAccessibilityRequirements(), 1)
242+
zones, err = pickZones(ctx, gceCS, req.GetAccessibilityRequirements(), 1, locationTopReq)
187243
if err != nil {
188244
return nil, status.Errorf(codes.InvalidArgument, "CreateVolume failed to pick zones for disk: %v", err.Error())
189245
}
@@ -193,7 +249,7 @@ func (gceCS *GCEControllerServer) CreateVolume(ctx context.Context, req *csi.Cre
193249
volKey = meta.ZonalKey(name, zones[0])
194250

195251
case replicationTypeRegionalPD:
196-
zones, err = pickZones(ctx, gceCS, req.GetAccessibilityRequirements(), 2)
252+
zones, err = pickZones(ctx, gceCS, req.GetAccessibilityRequirements(), 2, locationTopReq)
197253
if err != nil {
198254
return nil, status.Errorf(codes.InvalidArgument, "CreateVolume failed to pick zones for disk: %v", err.Error())
199255
}
@@ -1374,7 +1430,29 @@ func diskIsAttachedAndCompatible(deviceName string, instance *compute.Instance,
13741430
return false, nil
13751431
}
13761432

1377-
func pickZonesFromTopology(top *csi.TopologyRequirement, numZones int) ([]string, error) {
1433+
// pickZonesInRegion will remove any zones that are not in the given region.
1434+
func pickZonesInRegion(region string, zones []string) []string {
1435+
refinedZones := []string{}
1436+
for _, zone := range zones {
1437+
if strings.Contains(zone, region) {
1438+
refinedZones = append(refinedZones, zone)
1439+
}
1440+
}
1441+
return refinedZones
1442+
}
1443+
1444+
func prependZone(zone string, zones []string) []string {
1445+
newZones := []string{zone}
1446+
for i := 0; i < len(zones); i++ {
1447+
// Do not add a zone if it is equal to the zone that is already prepended to newZones.
1448+
if zones[i] != zone {
1449+
newZones = append(newZones, zones[i])
1450+
}
1451+
}
1452+
return newZones
1453+
}
1454+
1455+
func pickZonesFromTopology(top *csi.TopologyRequirement, numZones int, locationTopReq *locationRequirements) ([]string, error) {
13781456
reqZones, err := getZonesFromTopology(top.GetRequisite())
13791457
if err != nil {
13801458
return nil, fmt.Errorf("could not get zones from requisite topology: %w", err)
@@ -1384,6 +1462,39 @@ func pickZonesFromTopology(top *csi.TopologyRequirement, numZones int) ([]string
13841462
return nil, fmt.Errorf("could not get zones from preferred topology: %w", err)
13851463
}
13861464

1465+
if locationTopReq != nil {
1466+
srcVolZone := locationTopReq.srcVolZone
1467+
switch locationTopReq.cloneReplicationType {
1468+
// For zonal -> zonal cloning, the source disk zone must match the destination disk zone.
1469+
case replicationTypeNone:
1470+
// If the source volume zone is not in the topology requirement, we return an error.
1471+
if !slices.Contains(prefZones, srcVolZone) && !slices.Contains(reqZones, srcVolZone) {
1472+
volumeCloningReq := fmt.Sprintf("clone zone must match source disk zone: %s", srcVolZone)
1473+
return nil, fmt.Errorf("failed to find zone from topology %v: %s", top, volumeCloningReq)
1474+
}
1475+
return []string{srcVolZone}, nil
1476+
// For zonal or regional -> regional disk cloning, the source disk region must match the destination disk region.
1477+
case replicationTypeRegionalPD:
1478+
srcVolRegion := locationTopReq.srcVolRegion
1479+
prefZones = pickZonesInRegion(srcVolRegion, prefZones)
1480+
reqZones = pickZonesInRegion(srcVolRegion, reqZones)
1481+
1482+
if len(prefZones) == 0 && len(reqZones) == 0 {
1483+
volumeCloningReq := fmt.Sprintf("clone zone must reside in source disk region %s", srcVolRegion)
1484+
return nil, fmt.Errorf("failed to find zone from topology %v: %s", top, volumeCloningReq)
1485+
}
1486+
1487+
// For zonal -> regional disk cloning, one of the replicated zones must match the source zone.
1488+
if locationTopReq.srcReplicationType == replicationTypeNone {
1489+
if !slices.Contains(prefZones, srcVolZone) && !slices.Contains(reqZones, srcVolZone) {
1490+
volumeCloningReq := fmt.Sprintf("one of the replica zones of the clone must match the source disk zone: %s", srcVolZone)
1491+
return nil, fmt.Errorf("failed to find zone from topology %v: %s", top, volumeCloningReq)
1492+
}
1493+
prefZones = prependZone(srcVolZone, prefZones)
1494+
}
1495+
}
1496+
}
1497+
13871498
if numZones <= len(prefZones) {
13881499
return prefZones[0:numZones], nil
13891500
} else {
@@ -1442,16 +1553,25 @@ func getZoneFromSegment(seg map[string]string) (string, error) {
14421553
return zone, nil
14431554
}
14441555

1445-
func pickZones(ctx context.Context, gceCS *GCEControllerServer, top *csi.TopologyRequirement, numZones int) ([]string, error) {
1556+
func pickZones(ctx context.Context, gceCS *GCEControllerServer, top *csi.TopologyRequirement, numZones int, locationTopReq *locationRequirements) ([]string, error) {
14461557
var zones []string
14471558
var err error
14481559
if top != nil {
1449-
zones, err = pickZonesFromTopology(top, numZones)
1560+
zones, err = pickZonesFromTopology(top, numZones, locationTopReq)
14501561
if err != nil {
14511562
return nil, fmt.Errorf("failed to pick zones from topology: %w", err)
14521563
}
14531564
} else {
1454-
zones, err = getDefaultZonesInRegion(ctx, gceCS, []string{gceCS.CloudProvider.GetDefaultZone()}, numZones)
1565+
existingZones := []string{gceCS.CloudProvider.GetDefaultZone()}
1566+
// We set existingZones to the source volume zone so that for zonal -> zonal cloning, the clone is provisioned
1567+
// in the same zone as the source volume, and for zonal -> regional, one of the replicated zones will always
1568+
// be the zone of the source volume. For regional -> regional cloning, the srcVolZone will not be set, so we
1569+
// just use the default zone.
1570+
if locationTopReq != nil && locationTopReq.srcReplicationType == replicationTypeNone {
1571+
existingZones = []string{locationTopReq.srcVolZone}
1572+
}
1573+
// If topology is nil, then the Immediate binding mode was used without setting allowedTopologies in the storageclass.
1574+
zones, err = getDefaultZonesInRegion(ctx, gceCS, existingZones, numZones)
14551575
if err != nil {
14561576
return nil, fmt.Errorf("failed to get default %v zones in region: %w", numZones, err)
14571577
}

0 commit comments

Comments
 (0)