Skip to content

Commit 2b92a24

Browse files
committed
Add force attach to storage class
Change-Id: Id07dbe608613d905dffa96aa119d7deadb1dca5d
1 parent 15862fe commit 2b92a24

File tree

12 files changed

+352
-78
lines changed

12 files changed

+352
-78
lines changed

pkg/common/parameters.go

+9
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const (
2929
ParameterKeyLabels = "labels"
3030
ParameterKeyProvisionedIOPSOnCreate = "provisioned-iops-on-create"
3131
ParameterKeyProvisionedThroughputOnCreate = "provisioned-throughput-on-create"
32+
ParameterForceAttach = "alpha-unsafe-force-attach"
3233

3334
// Parameters for VolumeSnapshotClass
3435
ParameterKeyStorageLocations = "storage-locations"
@@ -83,6 +84,8 @@ type DiskParameters struct {
8384
// Values: {int64}
8485
// Default: none
8586
ProvisionedThroughputOnCreate int64
87+
// Default: false
88+
ForceAttach bool
8689
}
8790

8891
// SnapshotParameters contains normalized and defaulted parameters for snapshots
@@ -155,6 +158,12 @@ func ExtractAndDefaultParameters(parameters map[string]string, driverName string
155158
return p, fmt.Errorf("parameters contain invalid provisionedThroughputOnCreate parameter: %w", err)
156159
}
157160
p.ProvisionedThroughputOnCreate = paramProvisionedThroughputOnCreate
161+
case ParameterForceAttach:
162+
paramForceAttach, err := ConvertStringToBool(v)
163+
if err != nil {
164+
return p, fmt.Errorf("parameters contain invalid parameterForceAttach parameter: %w", err)
165+
}
166+
p.ForceAttach = paramForceAttach
158167
default:
159168
return p, fmt.Errorf("parameters contains invalid option %q", k)
160169
}

pkg/common/parameters_test.go

+21
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,27 @@ func TestExtractAndDefaultParameters(t *testing.T) {
178178
Labels: map[string]string{"key1": "value1", "label-1": "value-a", "label-2": "label-value-2"},
179179
},
180180
},
181+
{
182+
name: "force attach parameters",
183+
parameters: map[string]string{ParameterForceAttach: "true"},
184+
expectParams: DiskParameters{
185+
DiskType: "pd-standard",
186+
ReplicationType: "none",
187+
ForceAttach: true,
188+
Tags: map[string]string{},
189+
Labels: map[string]string{},
190+
},
191+
},
192+
{
193+
name: "no force attach parameters",
194+
parameters: map[string]string{ParameterForceAttach: "false"},
195+
expectParams: DiskParameters{
196+
DiskType: "pd-standard",
197+
ReplicationType: "none",
198+
Tags: map[string]string{},
199+
Labels: map[string]string{},
200+
},
201+
},
181202
}
182203

183204
for _, tc := range tests {

pkg/common/utils.go

+11
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,17 @@ func ConvertMiStringToInt64(str string) (int64, error) {
284284
return volumehelpers.RoundUpToMiB(quantity)
285285
}
286286

287+
// ConvertStringToBool converts a true/false string into a bool, or error.
288+
func ConvertStringToBool(str string) (bool, error) {
289+
switch strings.ToLower(str) {
290+
case "true":
291+
return true, nil
292+
case "false":
293+
return false, nil
294+
}
295+
return false, fmt.Errorf("Unexpected boolean string %s", str)
296+
}
297+
287298
// ParseMachineType returns an extracted machineType from a URL, or empty if not found.
288299
// machineTypeUrl: Full or partial URL of the machine type resource, in the format:
289300
//

pkg/common/utils_test.go

+54
Original file line numberDiff line numberDiff line change
@@ -806,6 +806,60 @@ func TestConvertMiStringToInt64(t *testing.T) {
806806
}
807807
}
808808

809+
func TestConvertStringToBool(t *testing.T) {
810+
tests := []struct {
811+
desc string
812+
inputStr string
813+
expected bool
814+
expectError bool
815+
}{
816+
{
817+
desc: "valid true",
818+
inputStr: "true",
819+
expected: true,
820+
expectError: false,
821+
},
822+
{
823+
desc: "valid mixed case true",
824+
inputStr: "True",
825+
expected: true,
826+
expectError: false,
827+
},
828+
{
829+
desc: "valid false",
830+
inputStr: "false",
831+
expected: false,
832+
expectError: false,
833+
},
834+
{
835+
desc: "valid mixed case false",
836+
inputStr: "False",
837+
expected: false,
838+
expectError: false,
839+
},
840+
{
841+
desc: "invalid",
842+
inputStr: "yes",
843+
expected: false,
844+
expectError: true,
845+
},
846+
}
847+
for _, tc := range tests {
848+
t.Run(tc.desc, func(t *testing.T) {
849+
got, err := ConvertStringToBool(tc.inputStr)
850+
if err != nil && !tc.expectError {
851+
t.Errorf("Got error %v converting string to bool %s; expect no error", err, tc.inputStr)
852+
}
853+
if err == nil && tc.expectError {
854+
t.Errorf("Got no error converting string to bool %s; expect an error", tc.inputStr)
855+
}
856+
if err == nil && got != tc.expected {
857+
t.Errorf("Got %v for converting bool to int64; expect %v", got, tc.expected)
858+
}
859+
})
860+
}
861+
}
862+
809863
func TestParseMachineType(t *testing.T) {
810864
tests := []struct {
811865
desc string

pkg/gce-cloud-provider/compute/fake-gce.go

+9-8
Original file line numberDiff line numberDiff line change
@@ -246,15 +246,16 @@ func (cloud *FakeCloudProvider) DeleteDisk(ctx context.Context, project string,
246246
return nil
247247
}
248248

249-
func (cloud *FakeCloudProvider) AttachDisk(ctx context.Context, project string, volKey *meta.Key, readWrite, diskType, instanceZone, instanceName string) error {
249+
func (cloud *FakeCloudProvider) AttachDisk(ctx context.Context, project string, volKey *meta.Key, readWrite, diskType, instanceZone, instanceName string, forceAttach bool) error {
250250
source := cloud.GetDiskSourceURI(project, volKey)
251251

252252
attachedDiskV1 := &computev1.AttachedDisk{
253-
DeviceName: volKey.Name,
254-
Kind: diskKind,
255-
Mode: readWrite,
256-
Source: source,
257-
Type: diskType,
253+
DeviceName: volKey.Name,
254+
Kind: diskKind,
255+
Mode: readWrite,
256+
Source: source,
257+
Type: diskType,
258+
ForceAttach: forceAttach,
258259
}
259260
instance, ok := cloud.instances[instanceName]
260261
if !ok {
@@ -538,14 +539,14 @@ func (cloud *FakeBlockingCloudProvider) DetachDisk(ctx context.Context, project,
538539
return cloud.FakeCloudProvider.DetachDisk(ctx, project, deviceName, instanceZone, instanceName)
539540
}
540541

541-
func (cloud *FakeBlockingCloudProvider) AttachDisk(ctx context.Context, project string, volKey *meta.Key, readWrite, diskType, instanceZone, instanceName string) error {
542+
func (cloud *FakeBlockingCloudProvider) AttachDisk(ctx context.Context, project string, volKey *meta.Key, readWrite, diskType, instanceZone, instanceName string, forceAttach bool) error {
542543
execute := make(chan Signal)
543544
cloud.ReadyToExecute <- execute
544545
val := <-execute
545546
if val.ReportError {
546547
return fmt.Errorf("force mock error for AttachDisk: volkey %s", volKey)
547548
}
548-
return cloud.FakeCloudProvider.AttachDisk(ctx, project, volKey, readWrite, diskType, instanceZone, instanceName)
549+
return cloud.FakeCloudProvider.AttachDisk(ctx, project, volKey, readWrite, diskType, instanceZone, instanceName, forceAttach)
549550
}
550551

551552
func notFoundError() *googleapi.Error {

pkg/gce-cloud-provider/compute/gce-compute.go

+8-7
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ type GCECompute interface {
9292
ValidateExistingDisk(ctx context.Context, disk *CloudDisk, params common.DiskParameters, reqBytes, limBytes int64, multiWriter bool) error
9393
InsertDisk(ctx context.Context, project string, volKey *meta.Key, params common.DiskParameters, capBytes int64, capacityRange *csi.CapacityRange, replicaZones []string, snapshotID string, volumeContentSourceVolumeID string, multiWriter bool) error
9494
DeleteDisk(ctx context.Context, project string, volumeKey *meta.Key) error
95-
AttachDisk(ctx context.Context, project string, volKey *meta.Key, readWrite, diskType, instanceZone, instanceName string) error
95+
AttachDisk(ctx context.Context, project string, volKey *meta.Key, readWrite, diskType, instanceZone, instanceName string, forceAttach bool) error
9696
DetachDisk(ctx context.Context, project, deviceName, instanceZone, instanceName string) error
9797
GetDiskSourceURI(project string, volKey *meta.Key) string
9898
GetDiskTypeURI(project string, volKey *meta.Key, diskType string) string
@@ -700,7 +700,7 @@ func (cloud *CloudProvider) deleteRegionalDisk(ctx context.Context, project, reg
700700
return nil
701701
}
702702

703-
func (cloud *CloudProvider) AttachDisk(ctx context.Context, project string, volKey *meta.Key, readWrite, diskType, instanceZone, instanceName string) error {
703+
func (cloud *CloudProvider) AttachDisk(ctx context.Context, project string, volKey *meta.Key, readWrite, diskType, instanceZone, instanceName string, forceAttach bool) error {
704704
klog.V(5).Infof("Attaching disk %v to %s", volKey, instanceName)
705705
source := cloud.GetDiskSourceURI(project, volKey)
706706

@@ -709,11 +709,12 @@ func (cloud *CloudProvider) AttachDisk(ctx context.Context, project string, volK
709709
return fmt.Errorf("failed to get device name: %w", err)
710710
}
711711
attachedDiskV1 := &computev1.AttachedDisk{
712-
DeviceName: deviceName,
713-
Kind: diskKind,
714-
Mode: readWrite,
715-
Source: source,
716-
Type: diskType,
712+
DeviceName: deviceName,
713+
Kind: diskKind,
714+
Mode: readWrite,
715+
Source: source,
716+
Type: diskType,
717+
ForceAttach: forceAttach,
717718
}
718719

719720
op, err := cloud.service.Instances.AttachDisk(project, instanceZone, instanceName, attachedDiskV1).Context(ctx).Do()

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

+52-19
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,11 @@ type locationRequirements struct {
111111
cloneReplicationType string
112112
}
113113

114+
// PDCSIContext is the extracted VolumeContext from controller requests.
115+
type PDCSIContext struct {
116+
ForceAttach bool
117+
}
118+
114119
var _ csi.ControllerServer = &GCEControllerServer{}
115120

116121
const (
@@ -309,7 +314,7 @@ func (gceCS *GCEControllerServer) CreateVolume(ctx context.Context, req *csi.Cre
309314

310315
// If there is no validation error, immediately return success
311316
klog.V(4).Infof("CreateVolume succeeded for disk %v, it already exists and was compatible", volKey)
312-
return generateCreateVolumeResponse(existingDisk, zones), nil
317+
return generateCreateVolumeResponse(existingDisk, zones, params), nil
313318
}
314319

315320
snapshotID := ""
@@ -424,7 +429,7 @@ func (gceCS *GCEControllerServer) CreateVolume(ctx context.Context, req *csi.Cre
424429
}
425430

426431
klog.V(4).Infof("CreateVolume succeeded for disk %v", volKey)
427-
return generateCreateVolumeResponse(disk, zones), nil
432+
return generateCreateVolumeResponse(disk, zones, params), nil
428433

429434
}
430435

@@ -483,7 +488,7 @@ func (gceCS *GCEControllerServer) ControllerPublishVolume(ctx context.Context, r
483488
}
484489
}()
485490
// Only valid requests will be accepted
486-
_, _, err = gceCS.validateControllerPublishVolumeRequest(ctx, req)
491+
_, _, _, err = gceCS.validateControllerPublishVolumeRequest(ctx, req)
487492
if err != nil {
488493
return nil, err
489494
}
@@ -504,32 +509,37 @@ func (gceCS *GCEControllerServer) ControllerPublishVolume(ctx context.Context, r
504509
return resp, err
505510
}
506511

507-
func (gceCS *GCEControllerServer) validateControllerPublishVolumeRequest(ctx context.Context, req *csi.ControllerPublishVolumeRequest) (string, *meta.Key, error) {
512+
func (gceCS *GCEControllerServer) validateControllerPublishVolumeRequest(ctx context.Context, req *csi.ControllerPublishVolumeRequest) (string, *meta.Key, *PDCSIContext, error) {
508513
// Validate arguments
509514
volumeID := req.GetVolumeId()
510515
nodeID := req.GetNodeId()
511516
volumeCapability := req.GetVolumeCapability()
512517
if len(volumeID) == 0 {
513-
return "", nil, status.Error(codes.InvalidArgument, "ControllerPublishVolume Volume ID must be provided")
518+
return "", nil, nil, status.Error(codes.InvalidArgument, "ControllerPublishVolume Volume ID must be provided")
514519
}
515520
if len(nodeID) == 0 {
516-
return "", nil, status.Error(codes.InvalidArgument, "ControllerPublishVolume Node ID must be provided")
521+
return "", nil, nil, status.Error(codes.InvalidArgument, "ControllerPublishVolume Node ID must be provided")
517522
}
518523
if volumeCapability == nil {
519-
return "", nil, status.Error(codes.InvalidArgument, "ControllerPublishVolume Volume capability must be provided")
524+
return "", nil, nil, status.Error(codes.InvalidArgument, "ControllerPublishVolume Volume capability must be provided")
520525
}
521526

522527
project, volKey, err := common.VolumeIDToKey(volumeID)
523528
if err != nil {
524-
return "", nil, status.Errorf(codes.InvalidArgument, "ControllerPublishVolume volume ID is invalid: %v", err.Error())
529+
return "", nil, nil, status.Errorf(codes.InvalidArgument, "ControllerPublishVolume volume ID is invalid: %v", err.Error())
525530
}
526531

527532
// TODO(#253): Check volume capability matches for ALREADY_EXISTS
528533
if err = validateVolumeCapability(volumeCapability); err != nil {
529-
return "", nil, status.Errorf(codes.InvalidArgument, "VolumeCapabilities is invalid: %v", err.Error())
534+
return "", nil, nil, status.Errorf(codes.InvalidArgument, "VolumeCapabilities is invalid: %v", err.Error())
530535
}
531536

532-
return project, volKey, nil
537+
var pdcsiContext *PDCSIContext
538+
if pdcsiContext, err = extractVolumeContext(req.VolumeContext); err != nil {
539+
return "", nil, nil, status.Errorf(codes.InvalidArgument, "Invalid volume context: %v", err.Error())
540+
}
541+
542+
return project, volKey, pdcsiContext, nil
533543
}
534544

535545
func parseMachineType(machineTypeUrl string) string {
@@ -543,7 +553,7 @@ func parseMachineType(machineTypeUrl string) string {
543553

544554
func (gceCS *GCEControllerServer) executeControllerPublishVolume(ctx context.Context, req *csi.ControllerPublishVolumeRequest) (*csi.ControllerPublishVolumeResponse, error, string) {
545555
diskType := ""
546-
project, volKey, err := gceCS.validateControllerPublishVolumeRequest(ctx, req)
556+
project, volKey, pdcsiContext, err := gceCS.validateControllerPublishVolumeRequest(ctx, req)
547557
if err != nil {
548558
return nil, err, diskType
549559
}
@@ -615,7 +625,7 @@ func (gceCS *GCEControllerServer) executeControllerPublishVolume(ctx context.Con
615625
if err != nil {
616626
return nil, status.Errorf(codes.InvalidArgument, "could not split nodeID: %v", err.Error()), diskType
617627
}
618-
err = gceCS.CloudProvider.AttachDisk(ctx, project, volKey, readWrite, attachableDiskTypePersistent, instanceZone, instanceName)
628+
err = gceCS.CloudProvider.AttachDisk(ctx, project, volKey, readWrite, attachableDiskTypePersistent, instanceZone, instanceName, pdcsiContext.ForceAttach)
619629
if err != nil {
620630
var udErr *gce.UnsupportedDiskError
621631
if errors.As(err, &udErr) {
@@ -788,11 +798,6 @@ func (gceCS *GCEControllerServer) ValidateVolumeCapabilities(ctx context.Context
788798
return nil, common.LoggedError("Failed to getDisk: ", err)
789799
}
790800

791-
// Check Volume Context is Empty
792-
if len(req.GetVolumeContext()) != 0 {
793-
return generateFailedValidationMessage("VolumeContext expected to be empty but got %v", req.GetVolumeContext()), nil
794-
}
795-
796801
// Check volume capabilities supported by PD. These are the same for any PD
797802
if err := validateVolumeCapabilities(req.GetVolumeCapabilities()); err != nil {
798803
return generateFailedValidationMessage("VolumeCapabilities not valid: %v", err.Error()), nil
@@ -1667,7 +1672,35 @@ func getDefaultZonesInRegion(ctx context.Context, gceCS *GCEControllerServer, ex
16671672
return ret, nil
16681673
}
16691674

1670-
func generateCreateVolumeResponse(disk *gce.CloudDisk, zones []string) *csi.CreateVolumeResponse {
1675+
func paramsToVolumeContext(params common.DiskParameters) map[string]string {
1676+
context := map[string]string{}
1677+
if params.ForceAttach {
1678+
context[common.ParameterForceAttach] = "true"
1679+
}
1680+
if len(context) > 0 {
1681+
return context
1682+
}
1683+
return nil
1684+
}
1685+
1686+
func extractVolumeContext(context map[string]string) (*PDCSIContext, error) {
1687+
info := &PDCSIContext{}
1688+
for key, val := range context {
1689+
switch key {
1690+
case common.ParameterForceAttach:
1691+
b, err := common.ConvertStringToBool(val)
1692+
if err != nil {
1693+
return nil, fmt.Errorf("Bad volume context force attach: %v", err)
1694+
}
1695+
info.ForceAttach = b
1696+
default:
1697+
return nil, fmt.Errorf("Unknown VolumeContext key %v", key)
1698+
}
1699+
}
1700+
return info, nil
1701+
}
1702+
1703+
func generateCreateVolumeResponse(disk *gce.CloudDisk, zones []string, params common.DiskParameters) *csi.CreateVolumeResponse {
16711704
tops := []*csi.Topology{}
16721705
for _, zone := range zones {
16731706
tops = append(tops, &csi.Topology{
@@ -1679,7 +1712,7 @@ func generateCreateVolumeResponse(disk *gce.CloudDisk, zones []string) *csi.Crea
16791712
Volume: &csi.Volume{
16801713
CapacityBytes: realDiskSizeBytes,
16811714
VolumeId: cleanSelfLink(disk.GetSelfLink()),
1682-
VolumeContext: nil,
1715+
VolumeContext: paramsToVolumeContext(params),
16831716
AccessibleTopology: tops,
16841717
},
16851718
}

0 commit comments

Comments
 (0)