2019-10-16 20:53:39 +00:00
|
|
|
package nomad
|
|
|
|
|
|
|
|
import (
|
2020-02-13 15:18:55 +00:00
|
|
|
"fmt"
|
2019-10-16 20:53:39 +00:00
|
|
|
"time"
|
|
|
|
|
|
|
|
metrics "github.com/armon/go-metrics"
|
|
|
|
log "github.com/hashicorp/go-hclog"
|
|
|
|
memdb "github.com/hashicorp/go-memdb"
|
|
|
|
multierror "github.com/hashicorp/go-multierror"
|
|
|
|
"github.com/hashicorp/nomad/acl"
|
2020-02-13 15:18:55 +00:00
|
|
|
cstructs "github.com/hashicorp/nomad/client/structs"
|
2019-10-16 20:53:39 +00:00
|
|
|
"github.com/hashicorp/nomad/nomad/state"
|
|
|
|
"github.com/hashicorp/nomad/nomad/structs"
|
|
|
|
)
|
|
|
|
|
|
|
|
// CSIVolume wraps the structs.CSIVolume with request data and server context
|
|
|
|
type CSIVolume struct {
|
|
|
|
srv *Server
|
|
|
|
logger log.Logger
|
|
|
|
}
|
|
|
|
|
|
|
|
// QueryACLObj looks up the ACL token in the request and returns the acl.ACL object
|
|
|
|
// - fallback to node secret ids
|
2020-02-11 11:41:18 +00:00
|
|
|
func (srv *Server) QueryACLObj(args *structs.QueryOptions, allowNodeAccess bool) (*acl.ACL, error) {
|
2019-10-16 20:53:39 +00:00
|
|
|
// Lookup the token
|
|
|
|
aclObj, err := srv.ResolveToken(args.AuthToken)
|
|
|
|
if err != nil {
|
|
|
|
// If ResolveToken had an unexpected error return that
|
2020-02-11 11:41:18 +00:00
|
|
|
if !structs.IsErrTokenNotFound(err) {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we don't allow access to this endpoint from Nodes, then return token
|
|
|
|
// not found.
|
|
|
|
if !allowNodeAccess {
|
|
|
|
return nil, structs.ErrTokenNotFound
|
|
|
|
}
|
2019-10-16 20:53:39 +00:00
|
|
|
|
|
|
|
ws := memdb.NewWatchSet()
|
2020-02-11 11:41:18 +00:00
|
|
|
// Attempt to lookup AuthToken as a Node.SecretID since nodes may call
|
|
|
|
// call this endpoint and don't have an ACL token.
|
2019-10-16 20:53:39 +00:00
|
|
|
node, stateErr := srv.fsm.State().NodeBySecretID(ws, args.AuthToken)
|
|
|
|
if stateErr != nil {
|
|
|
|
// Return the original ResolveToken error with this err
|
|
|
|
var merr multierror.Error
|
|
|
|
merr.Errors = append(merr.Errors, err, stateErr)
|
|
|
|
return nil, merr.ErrorOrNil()
|
|
|
|
}
|
|
|
|
|
2020-02-11 11:41:18 +00:00
|
|
|
// We did not find a Node for this ID, so return Token Not Found.
|
2019-10-16 20:53:39 +00:00
|
|
|
if node == nil {
|
|
|
|
return nil, structs.ErrTokenNotFound
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-11 11:41:18 +00:00
|
|
|
// Return either the users aclObj, or nil if ACLs are disabled.
|
2019-10-16 20:53:39 +00:00
|
|
|
return aclObj, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// WriteACLObj calls QueryACLObj for a WriteRequest
|
2020-02-11 11:41:18 +00:00
|
|
|
func (srv *Server) WriteACLObj(args *structs.WriteRequest, allowNodeAccess bool) (*acl.ACL, error) {
|
2019-10-16 20:53:39 +00:00
|
|
|
opts := &structs.QueryOptions{
|
|
|
|
Region: args.RequestRegion(),
|
|
|
|
Namespace: args.RequestNamespace(),
|
|
|
|
AuthToken: args.AuthToken,
|
|
|
|
}
|
2020-02-11 11:41:18 +00:00
|
|
|
return srv.QueryACLObj(opts, allowNodeAccess)
|
2019-10-16 20:53:39 +00:00
|
|
|
}
|
|
|
|
|
2020-01-28 15:28:34 +00:00
|
|
|
const (
|
|
|
|
csiVolumeTable = "csi_volumes"
|
|
|
|
csiPluginTable = "csi_plugins"
|
|
|
|
)
|
|
|
|
|
|
|
|
// replySetIndex sets the reply with the last index that modified the table
|
|
|
|
func (srv *Server) replySetIndex(table string, reply *structs.QueryMeta) error {
|
|
|
|
s := srv.fsm.State()
|
|
|
|
|
|
|
|
index, err := s.Index(table)
|
2019-10-16 20:53:39 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
reply.Index = index
|
|
|
|
|
|
|
|
// Set the query response
|
|
|
|
srv.setQueryMeta(reply)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// List replies with CSIVolumes, filtered by ACL access
|
|
|
|
func (v *CSIVolume) List(args *structs.CSIVolumeListRequest, reply *structs.CSIVolumeListResponse) error {
|
|
|
|
if done, err := v.srv.forward("CSIVolume.List", args, args, reply); done {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-03-17 21:32:39 +00:00
|
|
|
allowVolume := acl.NamespaceValidator(acl.NamespaceCapabilityCSIListVolume,
|
|
|
|
acl.NamespaceCapabilityCSIReadVolume,
|
|
|
|
acl.NamespaceCapabilityCSIMountVolume,
|
|
|
|
acl.NamespaceCapabilityListJobs)
|
2020-02-11 11:41:18 +00:00
|
|
|
aclObj, err := v.srv.QueryACLObj(&args.QueryOptions, false)
|
2019-10-16 20:53:39 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-03-17 21:32:39 +00:00
|
|
|
if !allowVolume(aclObj, args.RequestNamespace()) {
|
2020-03-17 15:35:34 +00:00
|
|
|
return structs.ErrPermissionDenied
|
|
|
|
}
|
|
|
|
|
2019-10-16 20:53:39 +00:00
|
|
|
metricsStart := time.Now()
|
|
|
|
defer metrics.MeasureSince([]string{"nomad", "volume", "list"}, metricsStart)
|
|
|
|
|
|
|
|
ns := args.RequestNamespace()
|
|
|
|
opts := blockingOptions{
|
|
|
|
queryOpts: &args.QueryOptions,
|
|
|
|
queryMeta: &reply.QueryMeta,
|
|
|
|
run: func(ws memdb.WatchSet, state *state.StateStore) error {
|
|
|
|
// Query all volumes
|
|
|
|
var err error
|
|
|
|
var iter memdb.ResultIterator
|
|
|
|
|
2020-03-11 16:47:14 +00:00
|
|
|
if args.NodeID != "" {
|
2020-03-31 21:16:47 +00:00
|
|
|
iter, err = state.CSIVolumesByNodeID(ws, args.NodeID)
|
2020-03-11 16:47:14 +00:00
|
|
|
} else if args.PluginID != "" {
|
2020-03-17 15:35:34 +00:00
|
|
|
iter, err = state.CSIVolumesByPluginID(ws, ns, args.PluginID)
|
2019-10-16 20:53:39 +00:00
|
|
|
} else {
|
2020-03-17 15:35:34 +00:00
|
|
|
iter, err = state.CSIVolumesByNamespace(ws, ns)
|
2019-10-16 20:53:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Collect results, filter by ACL access
|
2020-03-24 01:21:40 +00:00
|
|
|
vs := []*structs.CSIVolListStub{}
|
2019-10-16 20:53:39 +00:00
|
|
|
|
|
|
|
for {
|
|
|
|
raw := iter.Next()
|
|
|
|
if raw == nil {
|
|
|
|
break
|
|
|
|
}
|
2020-01-28 15:28:34 +00:00
|
|
|
|
2019-10-16 20:53:39 +00:00
|
|
|
vol := raw.(*structs.CSIVolume)
|
2020-03-09 20:58:12 +00:00
|
|
|
vol, err := state.CSIVolumeDenormalizePlugins(ws, vol.Copy())
|
2020-01-28 15:28:34 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2019-10-16 20:53:39 +00:00
|
|
|
|
2020-03-31 21:16:47 +00:00
|
|
|
// Remove (possibly again) by PluginID to handle passing both NodeID and PluginID
|
2020-03-11 16:47:14 +00:00
|
|
|
if args.PluginID != "" && args.PluginID != vol.PluginID {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2020-03-31 21:16:47 +00:00
|
|
|
// Remove by Namespace, since CSIVolumesByNodeID hasn't used the Namespace yet
|
|
|
|
if vol.Namespace != ns {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2020-03-17 15:35:34 +00:00
|
|
|
vs = append(vs, vol.Stub())
|
2019-10-16 20:53:39 +00:00
|
|
|
}
|
|
|
|
reply.Volumes = vs
|
2020-01-28 15:28:34 +00:00
|
|
|
return v.srv.replySetIndex(csiVolumeTable, &reply.QueryMeta)
|
2019-10-16 20:53:39 +00:00
|
|
|
}}
|
|
|
|
return v.srv.blockingRPC(&opts)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get fetches detailed information about a specific volume
|
|
|
|
func (v *CSIVolume) Get(args *structs.CSIVolumeGetRequest, reply *structs.CSIVolumeGetResponse) error {
|
|
|
|
if done, err := v.srv.forward("CSIVolume.Get", args, args, reply); done {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-03-17 21:32:39 +00:00
|
|
|
allowCSIAccess := acl.NamespaceValidator(acl.NamespaceCapabilityCSIReadVolume,
|
|
|
|
acl.NamespaceCapabilityCSIMountVolume,
|
|
|
|
acl.NamespaceCapabilityReadJob)
|
2020-02-11 11:41:18 +00:00
|
|
|
aclObj, err := v.srv.QueryACLObj(&args.QueryOptions, true)
|
2019-10-16 20:53:39 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-03-17 15:35:34 +00:00
|
|
|
ns := args.RequestNamespace()
|
|
|
|
if !allowCSIAccess(aclObj, ns) {
|
2019-10-16 20:53:39 +00:00
|
|
|
return structs.ErrPermissionDenied
|
|
|
|
}
|
|
|
|
|
|
|
|
metricsStart := time.Now()
|
|
|
|
defer metrics.MeasureSince([]string{"nomad", "volume", "get"}, metricsStart)
|
|
|
|
|
|
|
|
opts := blockingOptions{
|
|
|
|
queryOpts: &args.QueryOptions,
|
|
|
|
queryMeta: &reply.QueryMeta,
|
|
|
|
run: func(ws memdb.WatchSet, state *state.StateStore) error {
|
2020-03-17 15:35:34 +00:00
|
|
|
vol, err := state.CSIVolumeByID(ws, ns, args.ID)
|
2019-10-16 20:53:39 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2020-01-28 15:28:34 +00:00
|
|
|
if vol != nil {
|
|
|
|
vol, err = state.CSIVolumeDenormalize(ws, vol)
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return err
|
2019-10-16 20:53:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
reply.Volume = vol
|
2020-01-28 15:28:34 +00:00
|
|
|
return v.srv.replySetIndex(csiVolumeTable, &reply.QueryMeta)
|
2019-10-16 20:53:39 +00:00
|
|
|
}}
|
|
|
|
return v.srv.blockingRPC(&opts)
|
|
|
|
}
|
|
|
|
|
2020-04-10 20:47:21 +00:00
|
|
|
func (v *CSIVolume) pluginValidateVolume(req *structs.CSIVolumeRegisterRequest, vol *structs.CSIVolume) (*structs.CSIPlugin, error) {
|
|
|
|
state := v.srv.fsm.State()
|
2020-02-18 16:08:44 +00:00
|
|
|
ws := memdb.NewWatchSet()
|
|
|
|
|
|
|
|
plugin, err := state.CSIPluginByID(ws, vol.PluginID)
|
|
|
|
if err != nil {
|
2020-03-09 13:57:59 +00:00
|
|
|
return nil, err
|
2020-02-18 16:08:44 +00:00
|
|
|
}
|
|
|
|
if plugin == nil {
|
2020-03-09 13:57:59 +00:00
|
|
|
return nil, fmt.Errorf("no CSI plugin named: %s could be found", vol.PluginID)
|
2020-02-18 16:08:44 +00:00
|
|
|
}
|
|
|
|
|
2020-03-09 13:57:59 +00:00
|
|
|
vol.Provider = plugin.Provider
|
|
|
|
vol.ProviderVersion = plugin.Version
|
|
|
|
return plugin, nil
|
|
|
|
}
|
|
|
|
|
2020-04-10 20:47:21 +00:00
|
|
|
func (v *CSIVolume) controllerValidateVolume(req *structs.CSIVolumeRegisterRequest, vol *structs.CSIVolume, plugin *structs.CSIPlugin) error {
|
2020-03-09 13:57:59 +00:00
|
|
|
|
2020-02-18 16:08:44 +00:00
|
|
|
if !plugin.ControllerRequired {
|
|
|
|
// The plugin does not require a controller, so for now we won't do any
|
|
|
|
// further validation of the volume.
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-04-02 20:04:56 +00:00
|
|
|
method := "ClientCSI.ControllerValidateVolume"
|
2020-02-18 16:08:44 +00:00
|
|
|
cReq := &cstructs.ClientCSIControllerValidateVolumeRequest{
|
2020-03-12 19:08:19 +00:00
|
|
|
VolumeID: vol.RemoteID(),
|
2020-02-18 16:08:44 +00:00
|
|
|
AttachmentMode: vol.AttachmentMode,
|
|
|
|
AccessMode: vol.AccessMode,
|
|
|
|
}
|
2020-02-21 10:32:10 +00:00
|
|
|
cReq.PluginID = plugin.ID
|
2020-02-18 16:08:44 +00:00
|
|
|
cResp := &cstructs.ClientCSIControllerValidateVolumeResponse{}
|
|
|
|
|
2020-04-10 20:47:21 +00:00
|
|
|
return v.srv.RPC(method, cReq, cResp)
|
2020-02-18 16:08:44 +00:00
|
|
|
}
|
|
|
|
|
2019-10-16 20:53:39 +00:00
|
|
|
// Register registers a new volume
|
|
|
|
func (v *CSIVolume) Register(args *structs.CSIVolumeRegisterRequest, reply *structs.CSIVolumeRegisterResponse) error {
|
|
|
|
if done, err := v.srv.forward("CSIVolume.Register", args, args, reply); done {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-03-17 21:32:39 +00:00
|
|
|
allowVolume := acl.NamespaceValidator(acl.NamespaceCapabilityCSIWriteVolume)
|
2020-02-11 11:41:18 +00:00
|
|
|
aclObj, err := v.srv.WriteACLObj(&args.WriteRequest, false)
|
2019-10-16 20:53:39 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
metricsStart := time.Now()
|
|
|
|
defer metrics.MeasureSince([]string{"nomad", "volume", "register"}, metricsStart)
|
|
|
|
|
2020-03-17 21:32:39 +00:00
|
|
|
if !allowVolume(aclObj, args.RequestNamespace()) || !aclObj.AllowPluginRead() {
|
2019-10-16 20:53:39 +00:00
|
|
|
return structs.ErrPermissionDenied
|
|
|
|
}
|
|
|
|
|
2020-02-18 16:08:44 +00:00
|
|
|
// This is the only namespace we ACL checked, force all the volumes to use it.
|
|
|
|
// We also validate that the plugin exists for each plugin, and validate the
|
|
|
|
// capabilities when the plugin has a controller.
|
2020-02-03 20:40:28 +00:00
|
|
|
for _, vol := range args.Volumes {
|
|
|
|
vol.Namespace = args.RequestNamespace()
|
|
|
|
if err = vol.Validate(); err != nil {
|
2019-10-16 20:53:39 +00:00
|
|
|
return err
|
|
|
|
}
|
2020-03-17 21:32:39 +00:00
|
|
|
|
2020-04-10 20:47:21 +00:00
|
|
|
plugin, err := v.pluginValidateVolume(args, vol)
|
2020-03-09 13:57:59 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2020-04-10 20:47:21 +00:00
|
|
|
if err := v.controllerValidateVolume(args, vol, plugin); err != nil {
|
2020-02-18 16:08:44 +00:00
|
|
|
return err
|
|
|
|
}
|
2019-10-16 20:53:39 +00:00
|
|
|
}
|
|
|
|
|
2020-02-04 13:00:00 +00:00
|
|
|
resp, index, err := v.srv.raftApply(structs.CSIVolumeRegisterRequestType, args)
|
2019-10-16 20:53:39 +00:00
|
|
|
if err != nil {
|
2020-02-03 20:40:28 +00:00
|
|
|
v.logger.Error("csi raft apply failed", "error", err, "method", "register")
|
2019-10-16 20:53:39 +00:00
|
|
|
return err
|
|
|
|
}
|
2020-02-04 13:00:00 +00:00
|
|
|
if respErr, ok := resp.(error); ok {
|
|
|
|
return respErr
|
|
|
|
}
|
2019-10-16 20:53:39 +00:00
|
|
|
|
2020-02-03 20:40:28 +00:00
|
|
|
reply.Index = index
|
|
|
|
v.srv.setQueryMeta(&reply.QueryMeta)
|
|
|
|
return nil
|
2019-10-16 20:53:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Deregister removes a set of volumes
|
|
|
|
func (v *CSIVolume) Deregister(args *structs.CSIVolumeDeregisterRequest, reply *structs.CSIVolumeDeregisterResponse) error {
|
|
|
|
if done, err := v.srv.forward("CSIVolume.Deregister", args, args, reply); done {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-03-17 21:32:39 +00:00
|
|
|
allowVolume := acl.NamespaceValidator(acl.NamespaceCapabilityCSIWriteVolume)
|
2020-02-11 11:41:18 +00:00
|
|
|
aclObj, err := v.srv.WriteACLObj(&args.WriteRequest, false)
|
2019-10-16 20:53:39 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
metricsStart := time.Now()
|
|
|
|
defer metrics.MeasureSince([]string{"nomad", "volume", "deregister"}, metricsStart)
|
|
|
|
|
|
|
|
ns := args.RequestNamespace()
|
2020-03-17 21:32:39 +00:00
|
|
|
if !allowVolume(aclObj, ns) {
|
2019-10-16 20:53:39 +00:00
|
|
|
return structs.ErrPermissionDenied
|
|
|
|
}
|
|
|
|
|
2020-02-04 13:00:00 +00:00
|
|
|
resp, index, err := v.srv.raftApply(structs.CSIVolumeDeregisterRequestType, args)
|
2019-10-16 20:53:39 +00:00
|
|
|
if err != nil {
|
2020-02-03 20:40:28 +00:00
|
|
|
v.logger.Error("csi raft apply failed", "error", err, "method", "deregister")
|
2019-10-16 20:53:39 +00:00
|
|
|
return err
|
|
|
|
}
|
2020-02-04 13:00:00 +00:00
|
|
|
if respErr, ok := resp.(error); ok {
|
|
|
|
return respErr
|
|
|
|
}
|
|
|
|
|
|
|
|
reply.Index = index
|
|
|
|
v.srv.setQueryMeta(&reply.QueryMeta)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-03-16 19:59:42 +00:00
|
|
|
// Claim submits a change to a volume claim
|
2020-02-04 13:00:00 +00:00
|
|
|
func (v *CSIVolume) Claim(args *structs.CSIVolumeClaimRequest, reply *structs.CSIVolumeClaimResponse) error {
|
|
|
|
if done, err := v.srv.forward("CSIVolume.Claim", args, args, reply); done {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-03-17 21:32:39 +00:00
|
|
|
allowVolume := acl.NamespaceValidator(acl.NamespaceCapabilityCSIMountVolume)
|
2020-02-11 11:41:18 +00:00
|
|
|
aclObj, err := v.srv.WriteACLObj(&args.WriteRequest, true)
|
2020-02-04 13:00:00 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
metricsStart := time.Now()
|
|
|
|
defer metrics.MeasureSince([]string{"nomad", "volume", "claim"}, metricsStart)
|
|
|
|
|
2020-03-17 21:32:39 +00:00
|
|
|
if !allowVolume(aclObj, args.RequestNamespace()) || !aclObj.AllowPluginRead() {
|
2020-02-04 13:00:00 +00:00
|
|
|
return structs.ErrPermissionDenied
|
|
|
|
}
|
|
|
|
|
2020-04-29 15:57:19 +00:00
|
|
|
// COMPAT(1.0): the NodeID field was added after 0.11.0 and so we
|
|
|
|
// need to ensure it's been populated during upgrades from 0.11.0
|
|
|
|
// to later patch versions. Remove this block in 1.0
|
|
|
|
if args.Claim != structs.CSIVolumeClaimRelease && args.NodeID == "" {
|
|
|
|
state := v.srv.fsm.State()
|
|
|
|
ws := memdb.NewWatchSet()
|
|
|
|
alloc, err := state.AllocByID(ws, args.AllocationID)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if alloc == nil {
|
|
|
|
return fmt.Errorf("%s: %s",
|
|
|
|
structs.ErrUnknownAllocationPrefix, args.AllocationID)
|
|
|
|
}
|
|
|
|
args.NodeID = alloc.NodeID
|
|
|
|
}
|
|
|
|
|
2020-03-16 19:59:42 +00:00
|
|
|
if args.Claim != structs.CSIVolumeClaimRelease {
|
2020-04-29 15:57:19 +00:00
|
|
|
// if this is a new claim, add a Volume and PublishContext from the
|
|
|
|
// controller (if any) to the reply
|
2020-04-10 20:47:21 +00:00
|
|
|
err = v.controllerPublishVolume(args, reply)
|
2020-03-16 19:59:42 +00:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("controller publish: %v", err)
|
|
|
|
}
|
2020-02-13 15:18:55 +00:00
|
|
|
}
|
2020-02-04 13:00:00 +00:00
|
|
|
resp, index, err := v.srv.raftApply(structs.CSIVolumeClaimRequestType, args)
|
|
|
|
if err != nil {
|
|
|
|
v.logger.Error("csi raft apply failed", "error", err, "method", "claim")
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if respErr, ok := resp.(error); ok {
|
|
|
|
return respErr
|
|
|
|
}
|
2019-10-16 20:53:39 +00:00
|
|
|
|
2020-02-03 20:40:28 +00:00
|
|
|
reply.Index = index
|
|
|
|
v.srv.setQueryMeta(&reply.QueryMeta)
|
|
|
|
return nil
|
2020-01-28 15:28:34 +00:00
|
|
|
}
|
|
|
|
|
2020-04-10 20:47:21 +00:00
|
|
|
// controllerPublishVolume sends publish request to the CSI controller
|
|
|
|
// plugin associated with a volume, if any.
|
|
|
|
func (v *CSIVolume) controllerPublishVolume(req *structs.CSIVolumeClaimRequest, resp *structs.CSIVolumeClaimResponse) error {
|
2020-04-13 14:46:43 +00:00
|
|
|
plug, vol, err := v.volAndPluginLookup(req.RequestNamespace(), req.VolumeID)
|
2020-04-10 20:47:21 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set the Response volume from the lookup
|
|
|
|
resp.Volume = vol
|
|
|
|
|
|
|
|
// Validate the existence of the allocation, regardless of whether we need it
|
|
|
|
// now.
|
|
|
|
state := v.srv.fsm.State()
|
|
|
|
ws := memdb.NewWatchSet()
|
|
|
|
alloc, err := state.AllocByID(ws, req.AllocationID)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if alloc == nil {
|
|
|
|
return fmt.Errorf("%s: %s", structs.ErrUnknownAllocationPrefix, req.AllocationID)
|
|
|
|
}
|
|
|
|
|
|
|
|
// if no plugin was returned then controller validation is not required.
|
|
|
|
// Here we can return nil.
|
|
|
|
if plug == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-04-23 15:06:23 +00:00
|
|
|
// get Nomad's ID for the client node (not the storage provider's ID)
|
2020-04-10 20:47:21 +00:00
|
|
|
targetNode, err := state.NodeByID(ws, alloc.NodeID)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if targetNode == nil {
|
|
|
|
return fmt.Errorf("%s: %s", structs.ErrUnknownNodePrefix, alloc.NodeID)
|
|
|
|
}
|
2020-04-23 15:06:23 +00:00
|
|
|
|
|
|
|
// get the the storage provider's ID for the client node (not
|
|
|
|
// Nomad's ID for the node)
|
2020-04-10 20:47:21 +00:00
|
|
|
targetCSIInfo, ok := targetNode.CSINodePlugins[plug.ID]
|
|
|
|
if !ok {
|
|
|
|
return fmt.Errorf("Failed to find NodeInfo for node: %s", targetNode.ID)
|
|
|
|
}
|
2020-04-23 15:06:23 +00:00
|
|
|
externalNodeID := targetCSIInfo.NodeInfo.ID
|
2020-04-10 20:47:21 +00:00
|
|
|
|
|
|
|
method := "ClientCSI.ControllerAttachVolume"
|
|
|
|
cReq := &cstructs.ClientCSIControllerAttachVolumeRequest{
|
|
|
|
VolumeID: vol.RemoteID(),
|
2020-04-23 15:06:23 +00:00
|
|
|
ClientCSINodeID: externalNodeID,
|
2020-04-10 20:47:21 +00:00
|
|
|
AttachmentMode: vol.AttachmentMode,
|
|
|
|
AccessMode: vol.AccessMode,
|
|
|
|
ReadOnly: req.Claim == structs.CSIVolumeClaimRead,
|
|
|
|
}
|
|
|
|
cReq.PluginID = plug.ID
|
|
|
|
cResp := &cstructs.ClientCSIControllerAttachVolumeResponse{}
|
|
|
|
|
|
|
|
err = v.srv.RPC(method, cReq, cResp)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("attach volume: %v", err)
|
|
|
|
}
|
|
|
|
resp.PublishContext = cResp.PublishContext
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-04-13 14:46:43 +00:00
|
|
|
func (v *CSIVolume) volAndPluginLookup(namespace, volID string) (*structs.CSIPlugin, *structs.CSIVolume, error) {
|
|
|
|
state := v.srv.fsm.State()
|
|
|
|
ws := memdb.NewWatchSet()
|
|
|
|
|
|
|
|
vol, err := state.CSIVolumeByID(ws, namespace, volID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
if vol == nil {
|
|
|
|
return nil, nil, fmt.Errorf("volume not found: %s", volID)
|
|
|
|
}
|
|
|
|
if !vol.ControllerRequired {
|
|
|
|
return nil, vol, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// note: we do this same lookup in CSIVolumeByID but then throw
|
|
|
|
// away the pointer to the plugin rather than attaching it to
|
|
|
|
// the volume so we have to do it again here.
|
|
|
|
plug, err := state.CSIPluginByID(ws, vol.PluginID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
if plug == nil {
|
|
|
|
return nil, nil, fmt.Errorf("plugin not found: %s", vol.PluginID)
|
|
|
|
}
|
|
|
|
return plug, vol, nil
|
|
|
|
}
|
|
|
|
|
2020-03-17 21:32:39 +00:00
|
|
|
// allowCSIMount is called on Job register to check mount permission
|
|
|
|
func allowCSIMount(aclObj *acl.ACL, namespace string) bool {
|
|
|
|
return aclObj.AllowPluginRead() &&
|
|
|
|
aclObj.AllowNsOp(namespace, acl.NamespaceCapabilityCSIMountVolume)
|
|
|
|
}
|
|
|
|
|
2020-01-28 15:28:34 +00:00
|
|
|
// CSIPlugin wraps the structs.CSIPlugin with request data and server context
|
|
|
|
type CSIPlugin struct {
|
|
|
|
srv *Server
|
|
|
|
logger log.Logger
|
|
|
|
}
|
|
|
|
|
|
|
|
// List replies with CSIPlugins, filtered by ACL access
|
|
|
|
func (v *CSIPlugin) List(args *structs.CSIPluginListRequest, reply *structs.CSIPluginListResponse) error {
|
|
|
|
if done, err := v.srv.forward("CSIPlugin.List", args, args, reply); done {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-02-11 11:41:18 +00:00
|
|
|
aclObj, err := v.srv.QueryACLObj(&args.QueryOptions, false)
|
2020-01-28 15:28:34 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-03-18 19:29:03 +00:00
|
|
|
if !aclObj.AllowPluginList() {
|
2020-01-28 15:28:34 +00:00
|
|
|
return structs.ErrPermissionDenied
|
|
|
|
}
|
|
|
|
|
|
|
|
metricsStart := time.Now()
|
|
|
|
defer metrics.MeasureSince([]string{"nomad", "plugin", "list"}, metricsStart)
|
|
|
|
|
|
|
|
opts := blockingOptions{
|
|
|
|
queryOpts: &args.QueryOptions,
|
|
|
|
queryMeta: &reply.QueryMeta,
|
|
|
|
run: func(ws memdb.WatchSet, state *state.StateStore) error {
|
|
|
|
// Query all plugins
|
|
|
|
iter, err := state.CSIPlugins(ws)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Collect results
|
2020-03-24 01:21:40 +00:00
|
|
|
ps := []*structs.CSIPluginListStub{}
|
2020-01-28 15:28:34 +00:00
|
|
|
for {
|
|
|
|
raw := iter.Next()
|
|
|
|
if raw == nil {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
plug := raw.(*structs.CSIPlugin)
|
|
|
|
ps = append(ps, plug.Stub())
|
|
|
|
}
|
|
|
|
|
|
|
|
reply.Plugins = ps
|
|
|
|
return v.srv.replySetIndex(csiPluginTable, &reply.QueryMeta)
|
|
|
|
}}
|
|
|
|
return v.srv.blockingRPC(&opts)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get fetches detailed information about a specific plugin
|
|
|
|
func (v *CSIPlugin) Get(args *structs.CSIPluginGetRequest, reply *structs.CSIPluginGetResponse) error {
|
|
|
|
if done, err := v.srv.forward("CSIPlugin.Get", args, args, reply); done {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-02-11 11:41:18 +00:00
|
|
|
aclObj, err := v.srv.QueryACLObj(&args.QueryOptions, false)
|
2020-01-28 15:28:34 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-03-18 19:29:03 +00:00
|
|
|
if !aclObj.AllowPluginRead() {
|
2020-01-28 15:28:34 +00:00
|
|
|
return structs.ErrPermissionDenied
|
|
|
|
}
|
|
|
|
|
2020-03-18 19:29:03 +00:00
|
|
|
withAllocs := aclObj == nil ||
|
|
|
|
aclObj.AllowNsOp(args.RequestNamespace(), acl.NamespaceCapabilityReadJob)
|
|
|
|
|
2020-01-28 15:28:34 +00:00
|
|
|
metricsStart := time.Now()
|
|
|
|
defer metrics.MeasureSince([]string{"nomad", "plugin", "get"}, metricsStart)
|
|
|
|
|
|
|
|
opts := blockingOptions{
|
|
|
|
queryOpts: &args.QueryOptions,
|
|
|
|
queryMeta: &reply.QueryMeta,
|
|
|
|
run: func(ws memdb.WatchSet, state *state.StateStore) error {
|
|
|
|
plug, err := state.CSIPluginByID(ws, args.ID)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-03-18 19:29:03 +00:00
|
|
|
if plug == nil {
|
|
|
|
return nil
|
2020-01-28 15:28:34 +00:00
|
|
|
}
|
|
|
|
|
2020-03-18 19:29:03 +00:00
|
|
|
if withAllocs {
|
|
|
|
plug, err = state.CSIPluginDenormalize(ws, plug.Copy())
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Filter the allocation stubs by our namespace. withAllocs
|
|
|
|
// means we're allowed
|
|
|
|
var as []*structs.AllocListStub
|
|
|
|
for _, a := range plug.Allocations {
|
|
|
|
if a.Namespace == args.RequestNamespace() {
|
|
|
|
as = append(as, a)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
plug.Allocations = as
|
|
|
|
}
|
2020-01-28 15:28:34 +00:00
|
|
|
|
|
|
|
reply.Plugin = plug
|
|
|
|
return v.srv.replySetIndex(csiPluginTable, &reply.QueryMeta)
|
|
|
|
}}
|
|
|
|
return v.srv.blockingRPC(&opts)
|
2019-10-16 20:53:39 +00:00
|
|
|
}
|
2020-05-06 20:49:12 +00:00
|
|
|
|
|
|
|
// Delete deletes a plugin if it is unused
|
|
|
|
func (v *CSIPlugin) Delete(args *structs.CSIPluginDeleteRequest, reply *structs.CSIPluginDeleteResponse) error {
|
|
|
|
if done, err := v.srv.forward("CSIPlugin.Delete", args, args, reply); done {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check that it is a management token.
|
|
|
|
if aclObj, err := v.srv.ResolveToken(args.AuthToken); err != nil {
|
|
|
|
return err
|
|
|
|
} else if aclObj != nil && !aclObj.IsManagement() {
|
|
|
|
return structs.ErrPermissionDenied
|
|
|
|
}
|
|
|
|
|
|
|
|
metricsStart := time.Now()
|
|
|
|
defer metrics.MeasureSince([]string{"nomad", "plugin", "delete"}, metricsStart)
|
|
|
|
|
|
|
|
resp, index, err := v.srv.raftApply(structs.CSIPluginDeleteRequestType, args)
|
|
|
|
if err != nil {
|
|
|
|
v.logger.Error("csi raft apply failed", "error", err, "method", "delete")
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if respErr, ok := resp.(error); ok {
|
|
|
|
return respErr
|
|
|
|
}
|
|
|
|
|
|
|
|
reply.Index = index
|
|
|
|
v.srv.setQueryMeta(&reply.QueryMeta)
|
|
|
|
return nil
|
|
|
|
}
|