@@ -36,6 +36,7 @@ import (
36
36
"k8s.io/apimachinery/pkg/util/uuid"
37
37
"k8s.io/client-go/util/flowcontrol"
38
38
"k8s.io/klog/v2"
39
+ "k8s.io/utils/strings/slices"
39
40
40
41
"sigs.k8s.io/gcp-compute-persistent-disk-csi-driver/pkg/common"
41
42
gce "sigs.k8s.io/gcp-compute-persistent-disk-csi-driver/pkg/gce-cloud-provider/compute"
@@ -107,6 +108,14 @@ type workItem struct {
107
108
unpublishReq * csi.ControllerUnpublishVolumeRequest
108
109
}
109
110
111
+ // locationRequirements are additional location topology requirements that must be respected when creating a volume.
112
+ type locationRequirements struct {
113
+ srcVolRegion string
114
+ srcVolZone string
115
+ srcReplicationType string
116
+ cloneReplicationType string
117
+ }
118
+
110
119
var _ csi.ControllerServer = & GCEControllerServer {}
111
120
112
121
const (
@@ -151,6 +160,44 @@ func isDiskReady(disk *gce.CloudDisk) (bool, error) {
151
160
return false , nil
152
161
}
153
162
163
+ // cloningLocationRequirements returns additional location requirements to be applied to the given create volume requests topology.
164
+ // If the CreateVolumeRequest will use volume cloning, location requirements in compliance with the volume cloning limitations
165
+ // will be returned: https://cloud.google.com/kubernetes-engine/docs/how-to/persistent-volumes/volume-cloning#limitations.
166
+ func cloningLocationRequirements (req * csi.CreateVolumeRequest , cloneReplicationType string ) (* locationRequirements , error ) {
167
+ if ! useVolumeCloning (req ) {
168
+ return nil , nil
169
+ }
170
+ // If we are using volume cloning, this will be set.
171
+ volSrc := req .VolumeContentSource .GetVolume ()
172
+ volSrcVolID := volSrc .GetVolumeId ()
173
+
174
+ _ , sourceVolKey , err := common .VolumeIDToKey (volSrcVolID )
175
+ if err != nil {
176
+ return nil , fmt .Errorf ("volume ID is invalid: %w" , err )
177
+ }
178
+
179
+ isZonalSrcVol := sourceVolKey .Type () == meta .Zonal
180
+ if isZonalSrcVol {
181
+ region , err := common .GetRegionFromZones ([]string {sourceVolKey .Zone })
182
+ if err != nil {
183
+ return nil , fmt .Errorf ("failed to get region from zones: %w" , err )
184
+ }
185
+ sourceVolKey .Region = region
186
+ }
187
+
188
+ srcReplicationType := replicationTypeNone
189
+ if ! isZonalSrcVol {
190
+ srcReplicationType = replicationTypeRegionalPD
191
+ }
192
+
193
+ return & locationRequirements {srcVolZone : sourceVolKey .Zone , srcVolRegion : sourceVolKey .Region , srcReplicationType : srcReplicationType , cloneReplicationType : cloneReplicationType }, nil
194
+ }
195
+
196
+ // useVolumeCloning returns true if the create volume request should be created with volume cloning.
197
+ func useVolumeCloning (req * csi.CreateVolumeRequest ) bool {
198
+ return req .VolumeContentSource != nil && req .VolumeContentSource .GetVolume () != nil
199
+ }
200
+
154
201
func (gceCS * GCEControllerServer ) CreateVolume (ctx context.Context , req * csi.CreateVolumeRequest ) (* csi.CreateVolumeResponse , error ) {
155
202
var err error
156
203
// Validate arguments
@@ -186,12 +233,21 @@ func (gceCS *GCEControllerServer) CreateVolume(ctx context.Context, req *csi.Cre
186
233
if multiWriter {
187
234
gceAPIVersion = gce .GCEAPIVersionBeta
188
235
}
236
+
237
+ var locationTopReq * locationRequirements
238
+ if useVolumeCloning (req ) {
239
+ locationTopReq , err = cloningLocationRequirements (req , params .ReplicationType )
240
+ if err != nil {
241
+ return nil , status .Errorf (codes .InvalidArgument , "failed to get location requirements: %v" , err .Error ())
242
+ }
243
+ }
244
+
189
245
// Determine the zone or zones+region of the disk
190
246
var zones []string
191
247
var volKey * meta.Key
192
248
switch params .ReplicationType {
193
249
case replicationTypeNone :
194
- zones , err = pickZones (ctx , gceCS , req .GetAccessibilityRequirements (), 1 )
250
+ zones , err = pickZones (ctx , gceCS , req .GetAccessibilityRequirements (), 1 , locationTopReq )
195
251
if err != nil {
196
252
return nil , status .Errorf (codes .InvalidArgument , "CreateVolume failed to pick zones for disk: %v" , err .Error ())
197
253
}
@@ -201,7 +257,7 @@ func (gceCS *GCEControllerServer) CreateVolume(ctx context.Context, req *csi.Cre
201
257
volKey = meta .ZonalKey (name , zones [0 ])
202
258
203
259
case replicationTypeRegionalPD :
204
- zones , err = pickZones (ctx , gceCS , req .GetAccessibilityRequirements (), 2 )
260
+ zones , err = pickZones (ctx , gceCS , req .GetAccessibilityRequirements (), 2 , locationTopReq )
205
261
if err != nil {
206
262
return nil , status .Errorf (codes .InvalidArgument , "CreateVolume failed to pick zones for disk: %v" , err .Error ())
207
263
}
@@ -1382,7 +1438,29 @@ func diskIsAttachedAndCompatible(deviceName string, instance *compute.Instance,
1382
1438
return false , nil
1383
1439
}
1384
1440
1385
- func pickZonesFromTopology (top * csi.TopologyRequirement , numZones int ) ([]string , error ) {
1441
+ // pickZonesInRegion will remove any zones that are not in the given region.
1442
+ func pickZonesInRegion (region string , zones []string ) []string {
1443
+ refinedZones := []string {}
1444
+ for _ , zone := range zones {
1445
+ if strings .Contains (zone , region ) {
1446
+ refinedZones = append (refinedZones , zone )
1447
+ }
1448
+ }
1449
+ return refinedZones
1450
+ }
1451
+
1452
+ func prependZone (zone string , zones []string ) []string {
1453
+ newZones := []string {zone }
1454
+ for i := 0 ; i < len (zones ); i ++ {
1455
+ // Do not add a zone if it is equal to the zone that is already prepended to newZones.
1456
+ if zones [i ] != zone {
1457
+ newZones = append (newZones , zones [i ])
1458
+ }
1459
+ }
1460
+ return newZones
1461
+ }
1462
+
1463
+ func pickZonesFromTopology (top * csi.TopologyRequirement , numZones int , locationTopReq * locationRequirements ) ([]string , error ) {
1386
1464
reqZones , err := getZonesFromTopology (top .GetRequisite ())
1387
1465
if err != nil {
1388
1466
return nil , fmt .Errorf ("could not get zones from requisite topology: %w" , err )
@@ -1392,6 +1470,39 @@ func pickZonesFromTopology(top *csi.TopologyRequirement, numZones int) ([]string
1392
1470
return nil , fmt .Errorf ("could not get zones from preferred topology: %w" , err )
1393
1471
}
1394
1472
1473
+ if locationTopReq != nil {
1474
+ srcVolZone := locationTopReq .srcVolZone
1475
+ switch locationTopReq .cloneReplicationType {
1476
+ // For zonal -> zonal cloning, the source disk zone must match the destination disk zone.
1477
+ case replicationTypeNone :
1478
+ // If the source volume zone is not in the topology requirement, we return an error.
1479
+ if ! slices .Contains (prefZones , srcVolZone ) && ! slices .Contains (reqZones , srcVolZone ) {
1480
+ volumeCloningReq := fmt .Sprintf ("clone zone must match source disk zone: %s" , srcVolZone )
1481
+ return nil , fmt .Errorf ("failed to find zone from topology %v: %s" , top , volumeCloningReq )
1482
+ }
1483
+ return []string {srcVolZone }, nil
1484
+ // For zonal or regional -> regional disk cloning, the source disk region must match the destination disk region.
1485
+ case replicationTypeRegionalPD :
1486
+ srcVolRegion := locationTopReq .srcVolRegion
1487
+ prefZones = pickZonesInRegion (srcVolRegion , prefZones )
1488
+ reqZones = pickZonesInRegion (srcVolRegion , reqZones )
1489
+
1490
+ if len (prefZones ) == 0 && len (reqZones ) == 0 {
1491
+ volumeCloningReq := fmt .Sprintf ("clone zone must reside in source disk region %s" , srcVolRegion )
1492
+ return nil , fmt .Errorf ("failed to find zone from topology %v: %s" , top , volumeCloningReq )
1493
+ }
1494
+
1495
+ // For zonal -> regional disk cloning, one of the replicated zones must match the source zone.
1496
+ if locationTopReq .srcReplicationType == replicationTypeNone {
1497
+ if ! slices .Contains (prefZones , srcVolZone ) && ! slices .Contains (reqZones , srcVolZone ) {
1498
+ volumeCloningReq := fmt .Sprintf ("one of the replica zones of the clone must match the source disk zone: %s" , srcVolZone )
1499
+ return nil , fmt .Errorf ("failed to find zone from topology %v: %s" , top , volumeCloningReq )
1500
+ }
1501
+ prefZones = prependZone (srcVolZone , prefZones )
1502
+ }
1503
+ }
1504
+ }
1505
+
1395
1506
if numZones <= len (prefZones ) {
1396
1507
return prefZones [0 :numZones ], nil
1397
1508
} else {
@@ -1450,16 +1561,25 @@ func getZoneFromSegment(seg map[string]string) (string, error) {
1450
1561
return zone , nil
1451
1562
}
1452
1563
1453
- func pickZones (ctx context.Context , gceCS * GCEControllerServer , top * csi.TopologyRequirement , numZones int ) ([]string , error ) {
1564
+ func pickZones (ctx context.Context , gceCS * GCEControllerServer , top * csi.TopologyRequirement , numZones int , locationTopReq * locationRequirements ) ([]string , error ) {
1454
1565
var zones []string
1455
1566
var err error
1456
1567
if top != nil {
1457
- zones , err = pickZonesFromTopology (top , numZones )
1568
+ zones , err = pickZonesFromTopology (top , numZones , locationTopReq )
1458
1569
if err != nil {
1459
1570
return nil , fmt .Errorf ("failed to pick zones from topology: %w" , err )
1460
1571
}
1461
1572
} else {
1462
- zones , err = getDefaultZonesInRegion (ctx , gceCS , []string {gceCS .CloudProvider .GetDefaultZone ()}, numZones )
1573
+ existingZones := []string {gceCS .CloudProvider .GetDefaultZone ()}
1574
+ // We set existingZones to the source volume zone so that for zonal -> zonal cloning, the clone is provisioned
1575
+ // in the same zone as the source volume, and for zonal -> regional, one of the replicated zones will always
1576
+ // be the zone of the source volume. For regional -> regional cloning, the srcVolZone will not be set, so we
1577
+ // just use the default zone.
1578
+ if locationTopReq != nil && locationTopReq .srcReplicationType == replicationTypeNone {
1579
+ existingZones = []string {locationTopReq .srcVolZone }
1580
+ }
1581
+ // If topology is nil, then the Immediate binding mode was used without setting allowedTopologies in the storageclass.
1582
+ zones , err = getDefaultZonesInRegion (ctx , gceCS , existingZones , numZones )
1463
1583
if err != nil {
1464
1584
return nil , fmt .Errorf ("failed to get default %v zones in region: %w" , numZones , err )
1465
1585
}
0 commit comments