Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion hack/tools.mk
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ GOLANGCI_LINT_VERSION ?= v2.13.2
# renovate: datasource=github-releases depName=uber-go/mock
MOCKGEN_VERSION ?= v0.6.0
# renovate: datasource=github-releases depName=chainguard-dev/apko
APKO_VERSION ?= v1.2.45
APKO_VERSION ?= v1.3.0
# renovate: datasource=github-releases depName=ko-build/ko
KO_VERSION ?= v0.19.1

Expand Down
65 changes: 20 additions & 45 deletions pkg/csi/blockstorage/controllerserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,10 +237,10 @@ func (cs *controllerServer) CreateVolume(ctx context.Context, req *csi.CreateVol
}
}

// The encryption config is already set for volumes created from snapshot or volume. We MUST never set it when
// restoring from snapshot or volume.
// This is not true for volumeSourceType == Backup. The encryptionConfig must be set BUT the parameters can be different.
if volParams.Encrypted != nil && (volumeSourceType == "" || volumeSourceType == stackitclient.BackupSource) {
// A volume created from a content source (backup, snapshot or volume) inherits its
// encryption from that source; IaaS sets it. We MUST never send EncryptionParameters
// for such a restore. Only a fresh volume (i.e. without a source source) takes encryption parameters.
if volParams.Encrypted != nil && volumeSourceType == "" {
encrypted, err := strconv.ParseBool(*volParams.Encrypted)
if err != nil {
return nil, status.Error(codes.InvalidArgument, "parameter encrypted must be of type boolean")
Expand Down Expand Up @@ -351,45 +351,26 @@ func (cs *controllerServer) ControllerPublishVolume(ctx context.Context, req *cs
return nil, status.Error(codes.InvalidArgument, "[ControllerPublishVolume] Volume capability must be provided")
}

vol, err := cloud.GetVolume(ctx, volumeID)
if err != nil {
if stackiterrors.IsNotFound(err) {
return nil, status.Errorf(codes.NotFound, "[ControllerPublishVolume] Volume %s not found", volumeID)
}
return nil, status.Errorf(codes.Internal, "[ControllerPublishVolume] get volume failed with error %v", err)
}

_, err = cloud.GetServer(ctx, instanceID)
if err != nil {
if stackiterrors.IsNotFound(err) {
return nil, status.Errorf(codes.NotFound, "[ControllerPublishVolume] Instance %s not found", instanceID)
}
return nil, status.Errorf(codes.Internal, "[ControllerPublishVolume] GetInstanceByID failed with error %v", err)
}

// If Volume is already mounted to target instanceID, return OK
if vol.ServerId != nil && *vol.ServerId == instanceID {
return &csi.ControllerPublishVolumeResponse{}, nil
}

if vol.GetStatus() != stackitclient.VolumeAvailableStatus {
return nil, status.Errorf(codes.Internal, "[ControllerPublishVolume] Volume %s is not in an READY state. Got:%s Want:%s", volumeID, vol.GetStatus(), stackitclient.VolumeAvailableStatus)
}

// No pre-checks: IaaS validates the request (volume/server existence, state,
// attach limits) and returns the corresponding error from the attach API.
payload := iaas.AddVolumeToServerPayload{
DeleteOnTermination: new(false),
}
_, err = cloud.AttachVolume(ctx, instanceID, volumeID, payload)
if err != nil {
// Trigger's an immediate `NodeGetInfo` RPC call when MutableCSINodeAllocatableCount is enabled
if stackiterrors.IsTooManyDevicesError(err) {
return nil, status.Errorf(codes.ResourceExhausted, "[ControllerPublishVolume] Node can't accept any more volumes %v. All PCIe lanes are exhausted!", err)
}
switch err := cloud.AttachVolume(ctx, instanceID, volumeID, payload); {
case err == nil:
case stackiterrors.IsTooManyDevicesError(err):
return nil, status.Errorf(codes.ResourceExhausted, "[ControllerPublishVolume] Node can't accept any more volumes %v. All PCIe lanes are exhausted!", err)
case stackiterrors.IsNotFound(err):
return nil, status.Errorf(codes.NotFound, "[ControllerPublishVolume] volume %s or server %s not found: %v", volumeID, instanceID, err)
case stackiterrors.IsConflict(err):
// The attachment already exists. WaitDiskAttached verifies whether the attachment is ours before we report success.
klog.V(4).Infof("[ControllerPublishVolume] AttachVolume %s on %s conflicted, verifying attachment state: %v", volumeID, instanceID, err)
default:
klog.Errorf("Failed to AttachVolume: %v", err)
return nil, status.Errorf(codes.Internal, "[ControllerPublishVolume] Attach Volume failed with error %v", err)
}

err = cloud.WaitDiskAttached(ctx, instanceID, volumeID)
err := cloud.WaitDiskAttached(ctx, instanceID, volumeID)
if err != nil {
klog.Errorf("Failed to WaitDiskAttached: %v", err)
return nil, status.Errorf(codes.Internal, "[ControllerPublishVolume] failed to attach volume: %v", err)
Expand All @@ -412,16 +393,10 @@ func (cs *controllerServer) ControllerUnpublishVolume(ctx context.Context, req *
if volumeID == "" {
return nil, status.Error(codes.InvalidArgument, "[ControllerUnpublishVolume] Volume ID must be provided")
}
_, err := cloud.GetServer(ctx, instanceID)
if err != nil {
if stackiterrors.IsNotFound(err) {
klog.V(3).Infof("ControllerUnpublishVolume assuming volume %s is detached, because node %s does not exist", volumeID, instanceID)
return &csi.ControllerUnpublishVolumeResponse{}, nil
}
return nil, status.Errorf(codes.Internal, "[ControllerUnpublishVolume] GetInstanceByID failed with error %v", err)
}

err = cloud.DetachVolume(ctx, instanceID, volumeID)
// No server existence pre-check: IaaS returns not-found when the server (or the
// attachment) is already gone, which we treat as a successful detach below.
err := cloud.DetachVolume(ctx, instanceID, volumeID)
if err != nil {
if stackiterrors.IsNotFound(err) {
klog.V(3).Infof("ControllerUnpublishVolume assuming volume %s is detached, because it does not exist", volumeID)
Expand Down
127 changes: 118 additions & 9 deletions pkg/csi/blockstorage/controllerserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,80 @@ var _ = Describe("ControllerServer test", Ordered, func() {
Expect(resp.Volume.CapacityBytes).To(Equal(util.GIBIBYTE * 20))
})

It("should set encryption parameters for a fresh encrypted volume", func() {
req := &csi.CreateVolumeRequest{
Name: "encrypted volume",
VolumeCapabilities: stdVolCaps,
CapacityRange: stdCapRange,
Parameters: map[string]string{
"encrypted": "true",
"type": "perf1",
"kmsServiceAccount": "sa",
"kmsKeyID": "kid",
"kmsKeyringID": "krid",
"kmsKeyVersion": "1",
},
}

iaasClient.EXPECT().GetVolumesByName(gomock.Any(), "encrypted volume").Return([]iaas.Volume{}, nil)

var captured iaas.CreateVolumePayload
iaasClient.EXPECT().CreateVolume(gomock.Any(), gomock.Any()).
DoAndReturn(func(_ context.Context, payload iaas.CreateVolumePayload) (*iaas.Volume, error) {
captured = payload
return &iaas.Volume{Id: new("volume-id"), Size: new(int64(20))}, nil
})
iaasClient.EXPECT().WaitVolumeTargetStatusWithCustomBackoff(gomock.Any(), "volume-id", gomock.Any(), gomock.Any()).Return(nil)

_, err := fakeCs.CreateVolume(context.Background(), req)
Expect(err).ToNot(HaveOccurred())
Expect(captured.Source).To(BeNil())
Expect(captured.EncryptionParameters).ToNot(BeNil())
Expect(captured.EncryptionParameters.KekKeyId).To(Equal("kid"))
})

It("should never set encryption parameters when restoring from a backup source", func() {
req := &csi.CreateVolumeRequest{
Name: "backup restore",
VolumeCapabilities: stdVolCaps,
CapacityRange: stdCapRange,
Parameters: map[string]string{
"encrypted": "true",
"type": "perf1",
"kmsServiceAccount": "sa",
"kmsKeyID": "kid",
"kmsKeyringID": "krid",
"kmsKeyVersion": "1",
},
VolumeContentSource: &csi.VolumeContentSource{
Type: &csi.VolumeContentSource_Snapshot{
Snapshot: &csi.VolumeContentSource_SnapshotSource{SnapshotId: "source-id"},
},
},
}

iaasClient.EXPECT().GetVolumesByName(gomock.Any(), "backup restore").Return([]iaas.Volume{}, nil)
// Snapshot lookup misses, so the source is resolved as a backup.
iaasClient.EXPECT().GetSnapshot(gomock.Any(), "source-id").
Return(nil, &oapierror.GenericOpenAPIError{StatusCode: http.StatusNotFound})
iaasClient.EXPECT().GetBackup(gomock.Any(), "source-id").
Return(&iaas.Backup{Id: new("source-id"), Status: new(stackitclient.SnapshotReadyStatus)}, nil)

var captured iaas.CreateVolumePayload
iaasClient.EXPECT().CreateVolume(gomock.Any(), gomock.Any()).
DoAndReturn(func(_ context.Context, payload iaas.CreateVolumePayload) (*iaas.Volume, error) {
captured = payload
return &iaas.Volume{Id: new("volume-id"), Size: new(int64(20))}, nil
})
iaasClient.EXPECT().WaitVolumeTargetStatusWithCustomBackoff(gomock.Any(), "volume-id", gomock.Any(), gomock.Any()).Return(nil)

_, err := fakeCs.CreateVolume(context.Background(), req)
Expect(err).ToNot(HaveOccurred())
Expect(captured.Source).ToNot(BeNil())
Expect(captured.Source.Type).To(Equal(string(stackitclient.BackupSource)))
Expect(captured.EncryptionParameters).To(BeNil())
})

It("should not accept an empty volume name", func() {
req := &csi.CreateVolumeRequest{
Name: "",
Expand Down Expand Up @@ -654,29 +728,53 @@ var _ = Describe("ControllerServer test", Ordered, func() {
})
})
Describe("ControllerPublishVolume", func() {
It("should successfully attach volume to node", func() {
It("should attach the volume without any pre-checks", func() {
req := &csi.ControllerPublishVolumeRequest{
VolumeId: "fake",
NodeId: "fake",
VolumeCapability: stdVolCap,
}
iaasClient.EXPECT().AttachVolume(gomock.Any(), req.NodeId, req.VolumeId, gomock.Any()).Return(nil)
iaasClient.EXPECT().WaitDiskAttached(gomock.Any(), req.NodeId, req.VolumeId).Return(nil)
_, err := fakeCs.ControllerPublishVolume(context.Background(), req)
Expect(err).To(Not(HaveOccurred()))
})

It("should verify the attachment when the attach API reports a conflict", func() {
req := &csi.ControllerPublishVolumeRequest{
VolumeId: "fake",
NodeId: "fake",
VolumeCapability: stdVolCap,
}
iaasClient.EXPECT().GetVolume(gomock.Any(), req.VolumeId).Return(&iaas.Volume{Status: new("AVAILABLE")}, nil)
iaasClient.EXPECT().GetServer(gomock.Any(), "fake").Return(&iaas.Server{}, nil)
iaasClient.EXPECT().AttachVolume(gomock.Any(), req.NodeId, req.VolumeId, gomock.Any()).Return(req.VolumeId, nil)
iaasClient.EXPECT().AttachVolume(gomock.Any(), req.NodeId, req.VolumeId, gomock.Any()).Return(&oapierror.GenericOpenAPIError{
StatusCode: http.StatusConflict,
})
iaasClient.EXPECT().WaitDiskAttached(gomock.Any(), req.NodeId, req.VolumeId).Return(nil)
_, err := fakeCs.ControllerPublishVolume(context.Background(), req)
Expect(err).To(Not(HaveOccurred()))
})

It("should return not found when the attach API reports not found", func() {
req := &csi.ControllerPublishVolumeRequest{
VolumeId: "fake",
NodeId: "fake",
VolumeCapability: stdVolCap,
}
iaasClient.EXPECT().AttachVolume(gomock.Any(), req.NodeId, req.VolumeId, gomock.Any()).Return(&oapierror.GenericOpenAPIError{
StatusCode: http.StatusNotFound,
})
_, err := fakeCs.ControllerPublishVolume(context.Background(), req)
Expect(err).To(HaveOccurred())
Expect(status.Code(err)).To(Equal(codes.NotFound))
})

It("should return resource exhausted when node cannot attach more disks", func() {
req := &csi.ControllerPublishVolumeRequest{
VolumeId: "fake",
NodeId: "fake",
VolumeCapability: stdVolCap,
}
iaasClient.EXPECT().GetVolume(gomock.Any(), req.VolumeId).Return(&iaas.Volume{Status: new("AVAILABLE")}, nil)
iaasClient.EXPECT().GetServer(gomock.Any(), req.NodeId).Return(&iaas.Server{}, nil)
iaasClient.EXPECT().AttachVolume(gomock.Any(), req.NodeId, req.VolumeId, gomock.Any()).Return("", &oapierror.GenericOpenAPIError{
iaasClient.EXPECT().AttachVolume(gomock.Any(), req.NodeId, req.VolumeId, gomock.Any()).Return(&oapierror.GenericOpenAPIError{
StatusCode: http.StatusForbidden,
Body: []byte("maximum allowed number of disk devices"),
})
Expand All @@ -688,17 +786,28 @@ var _ = Describe("ControllerServer test", Ordered, func() {
})
})
Describe("ControllerUnpublishVolume", func() {
It("should successfully detach volume from node", func() {
It("should detach the volume without a server pre-check", func() {
req := &csi.ControllerUnpublishVolumeRequest{
VolumeId: "fake",
NodeId: "fake",
}
iaasClient.EXPECT().GetServer(gomock.Any(), "fake").Return(&iaas.Server{}, nil)
iaasClient.EXPECT().DetachVolume(gomock.Any(), req.NodeId, req.VolumeId).Return(nil)
iaasClient.EXPECT().WaitDiskDetached(gomock.Any(), req.NodeId, req.VolumeId).Return(nil)
_, err := fakeCs.ControllerUnpublishVolume(context.Background(), req)
Expect(err).To(Not(HaveOccurred()))
})

It("should treat a not-found detach as success", func() {
req := &csi.ControllerUnpublishVolumeRequest{
VolumeId: "fake",
NodeId: "fake",
}
iaasClient.EXPECT().DetachVolume(gomock.Any(), req.NodeId, req.VolumeId).Return(&oapierror.GenericOpenAPIError{
StatusCode: http.StatusNotFound,
})
_, err := fakeCs.ControllerUnpublishVolume(context.Background(), req)
Expect(err).To(Not(HaveOccurred()))
})
})
Describe("ControllerGetVolume", func() {
It("should get volume successfully", func() {
Expand Down
12 changes: 8 additions & 4 deletions pkg/csi/blockstorage/sanity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ var _ = Describe("CSI sanity test", Ordered, func() {
createdVolumes := make(map[string]*iaas.Volume)
createdSnapshots := make(map[string]*iaas.Snapshot)
createdBackups := make(map[string]*iaas.Backup)
createdInstances := make(map[string]*iaas.Server)
createdInstances := map[string]*iaas.Server{FakeInstanceID: {}}

// --- Mock Mounter Setup ---
mountPoints := make([]mountutils.MountPoint, 0)
Expand Down Expand Up @@ -323,14 +323,18 @@ var _ = Describe("CSI sanity test", Ordered, func() {
gomock.Any(), // instanceID
gomock.Any(), // volumeID
gomock.Any(), // payload
).DoAndReturn(func(_ context.Context, instanceID string, volumeID string, _ iaas.AddVolumeToServerPayload) (string, error) {
).DoAndReturn(func(_ context.Context, instanceID string, volumeID string, _ iaas.AddVolumeToServerPayload) error {
// IaaS validates volume and server existence and returns not-found for either.
if _, ok := createdInstances[instanceID]; !ok {
return &oapierror.GenericOpenAPIError{StatusCode: http.StatusNotFound}
}
vol, ok := createdVolumes[volumeID]
if !ok {
return "", &oapierror.GenericOpenAPIError{StatusCode: http.StatusNotFound}
return &oapierror.GenericOpenAPIError{StatusCode: http.StatusNotFound}
}
vol.ServerId = new(instanceID)
vol.Status = new("ATTACHED")
return *vol.Id, nil
return nil
}).AnyTimes()

iaasClient.EXPECT().WaitDiskAttached(
Expand Down
Loading