From 44a86bc7d1275a96140ffecf716336f1977e3045 Mon Sep 17 00:00:00 2001 From: Lang Martin Date: Wed, 18 Dec 2019 13:14:42 -0500 Subject: [PATCH] api: csi --- api/csi.go | 152 ++++++++++++++++++++++++++++++++++++++++++++++++ api/csi_test.go | 69 ++++++++++++++++++++++ 2 files changed, 221 insertions(+) create mode 100644 api/csi.go create mode 100644 api/csi_test.go diff --git a/api/csi.go b/api/csi.go new file mode 100644 index 000000000..5168614ef --- /dev/null +++ b/api/csi.go @@ -0,0 +1,152 @@ +package api + +import ( + "sort" + "time" +) + +// CSIVolumes is used to query the top level csi volumes +type CSIVolumes struct { + client *Client +} + +// CSIVolumes returns a handle on the allocs endpoints. +func (c *Client) CSIVolumes() *CSIVolumes { + return &CSIVolumes{client: c} +} + +// List returns all CSI volumes, ignoring driver +func (v *CSIVolumes) List(q *QueryOptions) ([]*CSIVolumeListStub, *QueryMeta, error) { + var resp []*CSIVolumeListStub + qm, err := v.client.query("/v1/csi/volumes", &resp, q) + if err != nil { + return nil, nil, err + } + sort.Sort(CSIVolumeIndexSort(resp)) + return resp, qm, nil +} + +// DriverList returns all CSI volumes for the specified driver +func (v *CSIVolumes) DriverList(driver string) ([]*CSIVolumeListStub, *QueryMeta, error) { + return v.List(&QueryOptions{Prefix: driver}) +} + +// Info is used to retrieve a single allocation. +func (v *CSIVolumes) Info(id string, q *QueryOptions) (*CSIVolume, *QueryMeta, error) { + var resp CSIVolume + qm, err := v.client.query("/v1/csi/volume/"+id, &resp, q) + if err != nil { + return nil, nil, err + } + return &resp, qm, nil +} + +func (v *CSIVolumes) Register(vol *CSIVolume, w *WriteOptions) error { + req := CSIVolumeRegisterRequest{ + Volumes: []*CSIVolume{vol}, + } + var resp struct{} + _, err := v.client.write("/v1/csi/volume/"+vol.ID, req, &resp, w) + return err +} + +func (v *CSIVolumes) Deregister(id string, w *WriteOptions) error { + _, err := v.client.delete("/v1/csi/volume/"+id, nil, w) + return err +} + +// CSIVolumeAttachmentMode duplicated in nomad/structs/csi.go +type CSIVolumeAttachmentMode string + +const ( + CSIVolumeAttachmentModeUnknown CSIVolumeAttachmentMode = "" + CSIVolumeAttachmentModeBlockDevice CSIVolumeAttachmentMode = "block-device" + CSIVolumeAttachmentModeFilesystem CSIVolumeAttachmentMode = "file-system" +) + +// CSIVolumeAccessMode duplicated in nomad/structs/csi.go +type CSIVolumeAccessMode string + +const ( + CSIVolumeAccessModeUnknown CSIVolumeAccessMode = "" + + CSIVolumeAccessModeSingleNodeReader CSIVolumeAccessMode = "single-node-reader-only" + CSIVolumeAccessModeSingleNodeWriter CSIVolumeAccessMode = "single-node-writer" + + CSIVolumeAccessModeMultiNodeReader CSIVolumeAccessMode = "multi-node-reader-only" + CSIVolumeAccessModeMultiNodeSingleWriter CSIVolumeAccessMode = "multi-node-single-writer" + CSIVolumeAccessModeMultiNodeMultiWriter CSIVolumeAccessMode = "multi-node-multi-writer" +) + +// CSIVolume is used for serialization, see also nomad/structs/csi.go +type CSIVolume struct { + ID string + Driver string + Namespace string + Topologies []*CSITopology + AccessMode CSIVolumeAccessMode + AttachmentMode CSIVolumeAttachmentMode + + // Combine structs.{Read,Write,Past}Allocs + Allocations []*AllocationListStub + + // Healthy is true iff all the denormalized plugin health fields are true, and the + // volume has not been marked for garbage collection + Healthy bool + VolumeGC time.Time + ControllerName string + ControllerHealthy bool + NodeHealthy int + NodeExpected int + ResourceExhausted time.Time + + CreatedIndex uint64 + ModifiedIndex uint64 +} + +type CSIVolumeIndexSort []*CSIVolumeListStub + +func (v CSIVolumeIndexSort) Len() int { + return len(v) +} + +func (v CSIVolumeIndexSort) Less(i, j int) bool { + return v[i].CreatedIndex > v[j].CreatedIndex +} + +func (v CSIVolumeIndexSort) Swap(i, j int) { + v[i], v[j] = v[j], v[i] +} + +// CSIVolumeListStub omits allocations. See also nomad/structs/csi.go +type CSIVolumeListStub struct { + ID string + Driver string + Namespace string + Topologies []*CSITopology + AccessMode CSIVolumeAccessMode + AttachmentMode CSIVolumeAttachmentMode + + // Healthy is true iff all the denormalized plugin health fields are true, and the + // volume has not been marked for garbage collection + Healthy bool + VolumeGC time.Time + ControllerName string + ControllerHealthy bool + NodeHealthy int + NodeExpected int + ResourceExhausted time.Time + + CreatedIndex uint64 + ModifiedIndex uint64 +} + +type CSIVolumeRegisterRequest struct { + Volumes []*CSIVolume + WriteRequest +} + +type CSIVolumeDeregisterRequest struct { + VolumeIDs []string + WriteRequest +} diff --git a/api/csi_test.go b/api/csi_test.go new file mode 100644 index 000000000..4e13ed4f9 --- /dev/null +++ b/api/csi_test.go @@ -0,0 +1,69 @@ +package api + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCSIVolumes_CRUD(t *testing.T) { + t.Parallel() + c, s, root := makeACLClient(t, nil, nil) + defer s.Stop() + v := c.CSIVolumes() + + // Successful empty result + vols, qm, err := v.List(nil) + assert.NoError(t, err) + assert.NotEqual(t, 0, qm.LastIndex) + assert.Equal(t, 0, len(vols)) + + // Authorized QueryOpts. Use the root token to just bypass ACL details + opts := &QueryOptions{ + Region: "global", + Namespace: "default", + AuthToken: root.SecretID, + } + + wpts := &WriteOptions{ + Region: "global", + Namespace: "default", + AuthToken: root.SecretID, + } + + // Register a volume + v.Register(&CSIVolume{ + ID: "DEADBEEF-63C7-407F-AE82-C99FBEF78FEB", + Driver: "minnie", + Namespace: "default", + AccessMode: CSIVolumeAccessModeMultiNodeSingleWriter, + AttachmentMode: CSIVolumeAttachmentModeFilesystem, + Topologies: []*CSITopology{{Segments: map[string]string{"foo": "bar"}}}, + }, wpts) + + // Successful result with volumes + vols, qm, err = v.List(opts) + assert.NoError(t, err) + assert.NotEqual(t, 0, qm.LastIndex) + assert.Equal(t, 1, len(vols)) + + // Successful info query + vol, qm, err := v.Info("DEADBEEF-63C7-407F-AE82-C99FBEF78FEB", opts) + assert.NoError(t, err) + assert.Equal(t, "minnie", vol.Driver) + assert.Equal(t, "bar", vol.Topologies[0].Segments["foo"]) + + // Deregister the volume + err = v.Deregister("DEADBEEF-63C7-407F-AE82-C99FBEF78FEB", wpts) + assert.NoError(t, err) + + // Successful empty result + vols, qm, err = v.List(nil) + assert.NoError(t, err) + assert.NotEqual(t, 0, qm.LastIndex) + assert.Equal(t, 0, len(vols)) + + // Failed info query + vol, qm, err = v.Info("DEADBEEF-63C7-407F-AE82-C99FBEF78FEB", opts) + assert.Error(t, err, "missing") +}