826d9d47f9
The CSI HTTP API has to transform the CSI volume to redact secrets, remove the claims fields, and to consolidate the allocation stubs into a single slice of alloc stubs. This was done manually in #8590 but this is a large amount of code and has proven both very bug prone (see #8659, #8666, #8699, #8735, and #12150) and requires updating lots of code every time we add a field to volumes or plugins. In #10202 we introduce encoding improvements for the `Node` struct that allow a more minimal transformation. Apply this same approach to serializing `structs.CSIVolume` to API responses. Also, the original reasoning behind #8590 for plugins no longer holds because the counts are now denormalized within the state store, so we can simply remove this transformation entirely.
166 lines
4.9 KiB
Go
166 lines
4.9 KiB
Go
package agent
|
|
|
|
import (
|
|
"bytes"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/hashicorp/nomad/api"
|
|
"github.com/hashicorp/nomad/ci"
|
|
"github.com/hashicorp/nomad/nomad/state"
|
|
"github.com/hashicorp/nomad/nomad/structs"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestHTTP_CSIEndpointPlugin(t *testing.T) {
|
|
ci.Parallel(t)
|
|
httpTest(t, nil, func(s *TestAgent) {
|
|
server := s.Agent.Server()
|
|
cleanup := state.CreateTestCSIPlugin(server.State(), "foo")
|
|
defer cleanup()
|
|
|
|
body := bytes.NewBuffer(nil)
|
|
req, err := http.NewRequest("GET", "/v1/plugin/csi/foo", body)
|
|
require.NoError(t, err)
|
|
|
|
resp := httptest.NewRecorder()
|
|
obj, err := s.Server.CSIPluginSpecificRequest(resp, req)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 200, resp.Code)
|
|
|
|
out, ok := obj.(*structs.CSIPlugin)
|
|
require.True(t, ok)
|
|
|
|
// ControllersExpected is 0 because this plugin was created without a job,
|
|
// which sets expected
|
|
require.Equal(t, 0, out.ControllersExpected)
|
|
require.Equal(t, 1, out.ControllersHealthy)
|
|
require.Len(t, out.Controllers, 1)
|
|
|
|
require.Equal(t, 0, out.NodesExpected)
|
|
require.Equal(t, 2, out.NodesHealthy)
|
|
require.Len(t, out.Nodes, 2)
|
|
})
|
|
}
|
|
|
|
func TestHTTP_CSIParseSecrets(t *testing.T) {
|
|
ci.Parallel(t)
|
|
testCases := []struct {
|
|
val string
|
|
expect structs.CSISecrets
|
|
}{
|
|
{"", nil},
|
|
{"one", nil},
|
|
{"one,two", nil},
|
|
{"one,two=value_two",
|
|
structs.CSISecrets(map[string]string{"two": "value_two"})},
|
|
{"one=value_one,one=overwrite",
|
|
structs.CSISecrets(map[string]string{"one": "overwrite"})},
|
|
{"one=value_one,two=value_two",
|
|
structs.CSISecrets(map[string]string{"one": "value_one", "two": "value_two"})},
|
|
}
|
|
for _, tc := range testCases {
|
|
req, _ := http.NewRequest("GET", "/v1/plugin/csi/foo", nil)
|
|
req.Header.Add("X-Nomad-CSI-Secrets", tc.val)
|
|
require.Equal(t, tc.expect, parseCSISecrets(req), tc.val)
|
|
}
|
|
}
|
|
|
|
func TestHTTP_CSIEndpointRegisterVolume(t *testing.T) {
|
|
ci.Parallel(t)
|
|
httpTest(t, nil, func(s *TestAgent) {
|
|
server := s.Agent.Server()
|
|
cleanup := state.CreateTestCSIPluginNodeOnly(server.State(), "foo")
|
|
defer cleanup()
|
|
|
|
args := structs.CSIVolumeRegisterRequest{
|
|
Volumes: []*structs.CSIVolume{{
|
|
ID: "bar",
|
|
PluginID: "foo",
|
|
RequestedCapabilities: []*structs.CSIVolumeCapability{{
|
|
AccessMode: structs.CSIVolumeAccessModeMultiNodeSingleWriter,
|
|
AttachmentMode: structs.CSIVolumeAttachmentModeFilesystem,
|
|
}},
|
|
}},
|
|
}
|
|
body := encodeReq(args)
|
|
req, err := http.NewRequest("PUT", "/v1/volumes", body)
|
|
require.NoError(t, err)
|
|
resp := httptest.NewRecorder()
|
|
_, err = s.Server.CSIVolumesRequest(resp, req)
|
|
require.NoError(t, err, "put error")
|
|
|
|
req, err = http.NewRequest("GET", "/v1/volume/csi/bar", nil)
|
|
require.NoError(t, err)
|
|
resp = httptest.NewRecorder()
|
|
raw, err := s.Server.CSIVolumeSpecificRequest(resp, req)
|
|
require.NoError(t, err, "get error")
|
|
out, ok := raw.(*structs.CSIVolume)
|
|
require.True(t, ok)
|
|
require.Equal(t, 1, out.ControllersHealthy)
|
|
require.Equal(t, 2, out.NodesHealthy)
|
|
|
|
req, err = http.NewRequest("DELETE", "/v1/volume/csi/bar/detach", nil)
|
|
require.NoError(t, err)
|
|
resp = httptest.NewRecorder()
|
|
_, err = s.Server.CSIVolumeSpecificRequest(resp, req)
|
|
require.Equal(t, CodedError(400, "detach requires node ID"), err)
|
|
})
|
|
}
|
|
|
|
func TestHTTP_CSIEndpointCreateVolume(t *testing.T) {
|
|
ci.Parallel(t)
|
|
httpTest(t, nil, func(s *TestAgent) {
|
|
server := s.Agent.Server()
|
|
cleanup := state.CreateTestCSIPlugin(server.State(), "foo")
|
|
defer cleanup()
|
|
|
|
args := structs.CSIVolumeCreateRequest{
|
|
Volumes: []*structs.CSIVolume{{
|
|
ID: "baz",
|
|
PluginID: "foo",
|
|
RequestedCapabilities: []*structs.CSIVolumeCapability{{
|
|
AccessMode: structs.CSIVolumeAccessModeMultiNodeSingleWriter,
|
|
AttachmentMode: structs.CSIVolumeAttachmentModeFilesystem,
|
|
}},
|
|
}},
|
|
}
|
|
body := encodeReq(args)
|
|
req, err := http.NewRequest("PUT", "/v1/volumes/create", body)
|
|
require.NoError(t, err)
|
|
resp := httptest.NewRecorder()
|
|
_, err = s.Server.CSIVolumesRequest(resp, req)
|
|
require.Error(t, err, "controller validate volume: No path to node")
|
|
|
|
req, err = http.NewRequest("DELETE", "/v1/volume/csi/baz", nil)
|
|
require.NoError(t, err)
|
|
resp = httptest.NewRecorder()
|
|
_, err = s.Server.CSIVolumeSpecificRequest(resp, req)
|
|
require.Error(t, err, "volume not found: baz")
|
|
})
|
|
}
|
|
|
|
func TestHTTP_CSIEndpointSnapshot(t *testing.T) {
|
|
ci.Parallel(t)
|
|
httpTest(t, nil, func(s *TestAgent) {
|
|
server := s.Agent.Server()
|
|
cleanup := state.CreateTestCSIPlugin(server.State(), "foo")
|
|
defer cleanup()
|
|
|
|
args := &api.CSISnapshotCreateRequest{
|
|
Snapshots: []*api.CSISnapshot{{
|
|
Name: "snap-*",
|
|
PluginID: "foo",
|
|
SourceVolumeID: "bar",
|
|
}},
|
|
}
|
|
body := encodeReq(args)
|
|
req, err := http.NewRequest("PUT", "/v1/volumes/snapshot", body)
|
|
require.NoError(t, err)
|
|
resp := httptest.NewRecorder()
|
|
_, err = s.Server.CSISnapshotsRequest(resp, req)
|
|
require.Error(t, err, "no such volume: bar")
|
|
})
|
|
}
|