Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CSI: enforce single access mode at validation time #12337

Merged
merged 1 commit into from
Mar 23, 2022
Merged
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
3 changes: 3 additions & 0 deletions .changelog/12337.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:bug
csi: Fixed a bug where single-use access modes were not enforced during validation
```
2 changes: 1 addition & 1 deletion nomad/job_endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -756,7 +756,7 @@ func TestJobEndpoint_Register_ACL(t *testing.T) {
Type: structs.VolumeTypeCSI,
Source: "prod-db",
AttachmentMode: structs.CSIVolumeAttachmentModeBlockDevice,
AccessMode: structs.CSIVolumeAccessModeSingleNodeWriter,
AccessMode: structs.CSIVolumeAccessModeMultiNodeMultiWriter,
},
}

Expand Down
2 changes: 1 addition & 1 deletion nomad/structs/csi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1002,7 +1002,7 @@ func TestDeleteNodeForType_Monolith_NilController(t *testing.T) {

func TestDeleteNodeForType_Monolith_NilNode(t *testing.T) {
ci.Parallel(t)

plug := NewCSIPlugin("foo", 1000)

plug.Nodes["n0"] = nil
Expand Down
2 changes: 1 addition & 1 deletion nomad/structs/structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -6396,7 +6396,7 @@ func (tg *TaskGroup) Validate(j *Job) error {
canaries = tg.Update.Canary
}
for name, volReq := range tg.Volumes {
if err := volReq.Validate(canaries); err != nil {
if err := volReq.Validate(tg.Count, canaries); err != nil {
mErr.Errors = append(mErr.Errors, fmt.Errorf(
"Task group volume validation for %s failed: %v", name, err))
}
Expand Down
91 changes: 91 additions & 0 deletions nomad/structs/volume_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package structs

import (
"testing"

"github.com/hashicorp/nomad/ci"
"github.com/stretchr/testify/require"
)

func TestVolumeRequest_Validate(t *testing.T) {
ci.Parallel(t)

testCases := []struct {
name string
expected []string
canariesCount int
taskGroupCount int
req *VolumeRequest
}{
{
name: "host volume with empty source",
expected: []string{"volume has an empty source"},
req: &VolumeRequest{
Type: VolumeTypeHost,
},
},
{
name: "host volume with CSI volume config",
expected: []string{
"host volumes cannot have an access mode",
"host volumes cannot have an attachment mode",
"host volumes cannot have mount options",
"host volumes do not support per_alloc",
},
req: &VolumeRequest{
Type: VolumeTypeHost,
ReadOnly: false,
AccessMode: CSIVolumeAccessModeSingleNodeReader,
AttachmentMode: CSIVolumeAttachmentModeBlockDevice,
MountOptions: &CSIMountOptions{
FSType: "ext4",
MountFlags: []string{"ro"},
},
PerAlloc: true,
},
},
{
name: "CSI volume multi-reader-single-writer access mode",
expected: []string{
"volume with multi-node-single-writer access mode allows only one writer",
},
taskGroupCount: 2,
req: &VolumeRequest{
Type: VolumeTypeCSI,
AccessMode: CSIVolumeAccessModeMultiNodeSingleWriter,
},
},
{
name: "CSI volume single reader access mode",
expected: []string{
"volume with single-node-reader-only access mode allows only one reader",
},
taskGroupCount: 2,
req: &VolumeRequest{
Type: VolumeTypeCSI,
AccessMode: CSIVolumeAccessModeSingleNodeReader,
ReadOnly: true,
},
},
{
name: "CSI volume per-alloc with canaries",
expected: []string{"volume cannot be per_alloc when canaries are in use"},
canariesCount: 1,
req: &VolumeRequest{
Type: VolumeTypeCSI,
PerAlloc: true,
},
},
}

for _, tc := range testCases {
tc = tc
t.Run(tc.name, func(t *testing.T) {
err := tc.req.Validate(tc.taskGroupCount, tc.canariesCount)
for _, expected := range tc.expected {
require.Contains(t, err.Error(), expected)
}
})
}

}
95 changes: 61 additions & 34 deletions nomad/structs/volumes.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,54 +102,81 @@ type VolumeRequest struct {
PerAlloc bool
}

func (v *VolumeRequest) Validate(canaries int) error {
func (v *VolumeRequest) Validate(taskGroupCount, canaries int) error {
if !(v.Type == VolumeTypeHost ||
v.Type == VolumeTypeCSI) {
return fmt.Errorf("volume has unrecognized type %s", v.Type)
}

var mErr multierror.Error
if v.Type == VolumeTypeHost && v.AttachmentMode != CSIVolumeAttachmentModeUnknown {
mErr.Errors = append(mErr.Errors,
fmt.Errorf("host volumes cannot have an attachment mode"))
addErr := func(msg string, args ...interface{}) {
mErr.Errors = append(mErr.Errors, fmt.Errorf(msg, args...))
}
if v.Type == VolumeTypeHost && v.AccessMode != CSIVolumeAccessModeUnknown {
mErr.Errors = append(mErr.Errors,
fmt.Errorf("host volumes cannot have an access mode"))
}
if v.Type == VolumeTypeHost && v.MountOptions != nil {
mErr.Errors = append(mErr.Errors,
fmt.Errorf("host volumes cannot have mount options"))
}
if v.Type == VolumeTypeCSI && v.AttachmentMode == CSIVolumeAttachmentModeUnknown {
mErr.Errors = append(mErr.Errors,
fmt.Errorf("CSI volumes must have an attachment mode"))
}
if v.Type == VolumeTypeCSI && v.AccessMode == CSIVolumeAccessModeUnknown {
mErr.Errors = append(mErr.Errors,
fmt.Errorf("CSI volumes must have an access mode"))

if v.Source == "" {
addErr("volume has an empty source")
}

if v.AccessMode == CSIVolumeAccessModeSingleNodeReader || v.AccessMode == CSIVolumeAccessModeMultiNodeReader {
if !v.ReadOnly {
mErr.Errors = append(mErr.Errors,
fmt.Errorf("%s volumes must be read-only", v.AccessMode))
switch v.Type {

case VolumeTypeHost:
if v.AttachmentMode != CSIVolumeAttachmentModeUnknown {
addErr("host volumes cannot have an attachment mode")
}
if v.AccessMode != CSIVolumeAccessModeUnknown {
addErr("host volumes cannot have an access mode")
}
if v.MountOptions != nil {
addErr("host volumes cannot have mount options")
}
if v.PerAlloc {
addErr("host volumes do not support per_alloc")
}
}

if v.AttachmentMode == CSIVolumeAttachmentModeBlockDevice && v.MountOptions != nil {
mErr.Errors = append(mErr.Errors,
fmt.Errorf("block devices cannot have mount options"))
}
case VolumeTypeCSI:

if v.PerAlloc && canaries > 0 {
mErr.Errors = append(mErr.Errors,
fmt.Errorf("volume cannot be per_alloc when canaries are in use"))
}
switch v.AttachmentMode {
case CSIVolumeAttachmentModeUnknown:
addErr("CSI volumes must have an attachment mode")
case CSIVolumeAttachmentModeBlockDevice:
if v.MountOptions != nil {
addErr("block devices cannot have mount options")
}
}

switch v.AccessMode {
case CSIVolumeAccessModeUnknown:
addErr("CSI volumes must have an access mode")
case CSIVolumeAccessModeSingleNodeReader:
if !v.ReadOnly {
addErr("%s volumes must be read-only", v.AccessMode)
}
if taskGroupCount > 1 && !v.PerAlloc {
addErr("volume with %s access mode allows only one reader", v.AccessMode)
}
case CSIVolumeAccessModeSingleNodeWriter:
// note: we allow read-only mount of this volume, but only one
if taskGroupCount > 1 && !v.PerAlloc {
addErr("volume with %s access mode allows only one reader or writer", v.AccessMode)
}
case CSIVolumeAccessModeMultiNodeReader:
if !v.ReadOnly {
addErr("%s volumes must be read-only", v.AccessMode)
}
case CSIVolumeAccessModeMultiNodeSingleWriter:
if !v.ReadOnly && taskGroupCount > 1 && !v.PerAlloc {
addErr("volume with %s access mode allows only one writer", v.AccessMode)
}
case CSIVolumeAccessModeMultiNodeMultiWriter:
// note: we intentionally allow read-only mount of this mode
}

if v.PerAlloc && canaries > 0 {
addErr("volume cannot be per_alloc when canaries are in use")
}

if v.Source == "" {
mErr.Errors = append(mErr.Errors, fmt.Errorf("volume has an empty source"))
}

return mErr.ErrorOrNil()
}

Expand Down