2019-10-16 20:53:39 +00:00
package nomad
import (
2020-08-06 17:51:29 +00:00
"errors"
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 )
2020-05-20 14:22:24 +00:00
if args . ID == "" {
return fmt . Errorf ( "missing volume ID" )
}
2019-10-16 20:53:39 +00:00
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-05-11 21:12:51 +00:00
Secrets : vol . Secrets ,
2020-05-15 12:16:01 +00:00
Parameters : vol . Parameters ,
Context : vol . Context ,
2020-02-18 16:08:44 +00:00
}
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-05-20 14:22:24 +00:00
if args . Volumes == nil || len ( args . Volumes ) == 0 {
return fmt . Errorf ( "missing volume definition" )
}
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-05-20 14:22:24 +00:00
if len ( args . VolumeIDs ) == 0 {
return fmt . Errorf ( "missing volume IDs" )
}
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-05-20 14:22:24 +00:00
if args . VolumeID == "" {
return fmt . Errorf ( "missing volume ID" )
}
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-08-06 17:51:29 +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 {
2020-08-06 17:51:29 +00:00
return fmt . Errorf ( "failed to find storage provider info for client %q, node plugin %q is not running or has not fingerprinted on this client" , targetNode . ID , plug . ID )
2020-04-10 20:47:21 +00:00
}
2020-04-23 15:06:23 +00:00
externalNodeID := targetCSIInfo . NodeInfo . ID
2020-08-06 17:51:29 +00:00
req . ExternalNodeID = externalNodeID // update with the target info
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 ,
2020-05-11 21:12:51 +00:00
Secrets : vol . Secrets ,
2020-05-15 12:16:01 +00:00
VolumeContext : vol . Context ,
2020-04-10 20:47:21 +00:00
}
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-08-06 17:51:29 +00:00
// Unpublish synchronously sends the NodeUnpublish, NodeUnstage, and
// ControllerUnpublish RPCs to the client. It handles errors according to the
// current claim state.
func ( v * CSIVolume ) Unpublish ( args * structs . CSIVolumeUnpublishRequest , reply * structs . CSIVolumeUnpublishResponse ) error {
if done , err := v . srv . forward ( "CSIVolume.Unpublish" , args , args , reply ) ; done {
return err
}
metricsStart := time . Now ( )
defer metrics . MeasureSince ( [ ] string { "nomad" , "volume" , "unpublish" } , metricsStart )
allowVolume := acl . NamespaceValidator ( acl . NamespaceCapabilityCSIMountVolume )
aclObj , err := v . srv . WriteACLObj ( & args . WriteRequest , true )
if err != nil {
return err
}
if ! allowVolume ( aclObj , args . RequestNamespace ( ) ) || ! aclObj . AllowPluginRead ( ) {
return structs . ErrPermissionDenied
}
if args . VolumeID == "" {
return fmt . Errorf ( "missing volume ID" )
}
if args . Claim == nil {
return fmt . Errorf ( "missing volume claim" )
}
ws := memdb . NewWatchSet ( )
state := v . srv . fsm . State ( )
vol , err := state . CSIVolumeByID ( ws , args . Namespace , args . VolumeID )
if err != nil {
return err
}
if vol == nil {
return fmt . Errorf ( "no such volume" )
}
claim := args . Claim
claim . Mode = structs . CSIVolumeClaimRelease
// previous checkpoints may have set the past claim state already.
// in practice we should never see CSIVolumeClaimStateControllerDetached
// but having an option for the state makes it easy to add a checkpoint
// in a backwards compatible way if we need one later
switch claim . State {
case structs . CSIVolumeClaimStateNodeDetached :
goto NODE_DETACHED
case structs . CSIVolumeClaimStateControllerDetached :
goto RELEASE_CLAIM
case structs . CSIVolumeClaimStateReadyToFree :
goto RELEASE_CLAIM
}
err = v . nodeUnpublishVolume ( vol , claim )
if err != nil {
return err
}
NODE_DETACHED :
2020-08-07 14:43:45 +00:00
err = v . controllerUnpublishVolume ( vol , claim )
2020-08-06 17:51:29 +00:00
if err != nil {
return err
}
RELEASE_CLAIM :
// advance a CSIVolumeClaimStateControllerDetached claim
claim . State = structs . CSIVolumeClaimStateReadyToFree
err = v . checkpointClaim ( vol , claim )
if err != nil {
return err
}
reply . Index = vol . ModifyIndex
v . srv . setQueryMeta ( & reply . QueryMeta )
return nil
}
func ( v * CSIVolume ) nodeUnpublishVolume ( vol * structs . CSIVolume , claim * structs . CSIVolumeClaim ) error {
2020-08-11 14:18:54 +00:00
if claim . AllocationID != "" {
err := v . nodeUnpublishVolumeImpl ( vol , claim )
if err != nil {
return err
}
claim . State = structs . CSIVolumeClaimStateNodeDetached
return v . checkpointClaim ( vol , claim )
}
// The RPC sent from the 'nomad node detach' command won't have an
// allocation ID set so we try to unpublish every terminal or invalid
// alloc on the node
allocIDs := [ ] string { }
state := v . srv . fsm . State ( )
vol , err := state . CSIVolumeDenormalize ( memdb . NewWatchSet ( ) , vol )
if err != nil {
return err
}
for allocID , alloc := range vol . ReadAllocs {
if alloc == nil {
rclaim , ok := vol . ReadClaims [ allocID ]
if ok && rclaim . NodeID == claim . NodeID {
allocIDs = append ( allocIDs , allocID )
}
} else {
if alloc . NodeID == claim . NodeID && alloc . TerminalStatus ( ) {
allocIDs = append ( allocIDs , allocID )
}
}
}
for allocID , alloc := range vol . WriteAllocs {
if alloc == nil {
wclaim , ok := vol . WriteClaims [ allocID ]
if ok && wclaim . NodeID == claim . NodeID {
allocIDs = append ( allocIDs , allocID )
}
} else {
if alloc . NodeID == claim . NodeID && alloc . TerminalStatus ( ) {
allocIDs = append ( allocIDs , allocID )
}
}
}
var merr multierror . Error
for _ , allocID := range allocIDs {
claim . AllocationID = allocID
err := v . nodeUnpublishVolumeImpl ( vol , claim )
if err != nil {
merr . Errors = append ( merr . Errors , err )
}
}
err = merr . ErrorOrNil ( )
if err != nil {
return err
}
claim . State = structs . CSIVolumeClaimStateNodeDetached
return v . checkpointClaim ( vol , claim )
}
func ( v * CSIVolume ) nodeUnpublishVolumeImpl ( vol * structs . CSIVolume , claim * structs . CSIVolumeClaim ) error {
2020-08-06 17:51:29 +00:00
req := & cstructs . ClientCSINodeDetachVolumeRequest {
PluginID : vol . PluginID ,
VolumeID : vol . ID ,
ExternalID : vol . RemoteID ( ) ,
AllocID : claim . AllocationID ,
NodeID : claim . NodeID ,
AttachmentMode : vol . AttachmentMode ,
AccessMode : vol . AccessMode ,
ReadOnly : claim . Mode == structs . CSIVolumeClaimRead ,
}
err := v . srv . RPC ( "ClientCSI.NodeDetachVolume" ,
req , & cstructs . ClientCSINodeDetachVolumeResponse { } )
if err != nil {
// we should only get this error if the Nomad node disconnects and
// is garbage-collected, so at this point we don't have any reason
// to operate as though the volume is attached to it.
2020-08-07 15:01:36 +00:00
if ! errors . Is ( err , structs . ErrUnknownNode ) {
2020-08-06 17:51:29 +00:00
return fmt . Errorf ( "could not detach from node: %w" , err )
}
}
2020-08-11 14:18:54 +00:00
return nil
2020-08-06 17:51:29 +00:00
}
2020-08-07 14:43:45 +00:00
func ( v * CSIVolume ) controllerUnpublishVolume ( vol * structs . CSIVolume , claim * structs . CSIVolumeClaim ) error {
2020-08-06 17:51:29 +00:00
2020-08-07 14:43:45 +00:00
if ! vol . ControllerRequired {
2020-08-06 17:51:29 +00:00
claim . State = structs . CSIVolumeClaimStateReadyToFree
return nil
}
2020-08-07 14:43:45 +00:00
// we only send a controller detach if a Nomad client no longer has
// any claim to the volume, so we need to check the status of claimed
// allocations
state := v . srv . fsm . State ( )
vol , err := state . CSIVolumeDenormalize ( memdb . NewWatchSet ( ) , vol )
if err != nil {
return err
}
for _ , alloc := range vol . ReadAllocs {
if alloc != nil && alloc . NodeID == claim . NodeID && ! alloc . TerminalStatus ( ) {
claim . State = structs . CSIVolumeClaimStateReadyToFree
return nil
}
}
for _ , alloc := range vol . WriteAllocs {
if alloc != nil && alloc . NodeID == claim . NodeID && ! alloc . TerminalStatus ( ) {
claim . State = structs . CSIVolumeClaimStateReadyToFree
return nil
}
}
2020-08-06 17:51:29 +00:00
// if the RPC is sent by a client node, it doesn't know the claim's
// external node ID.
if claim . ExternalNodeID == "" {
externalNodeID , err := v . lookupExternalNodeID ( vol , claim )
if err != nil {
return fmt . Errorf ( "missing external node ID: %v" , err )
}
claim . ExternalNodeID = externalNodeID
}
req := & cstructs . ClientCSIControllerDetachVolumeRequest {
VolumeID : vol . RemoteID ( ) ,
ClientCSINodeID : claim . ExternalNodeID ,
Secrets : vol . Secrets ,
}
req . PluginID = vol . PluginID
2020-08-07 14:43:45 +00:00
err = v . srv . RPC ( "ClientCSI.ControllerDetachVolume" , req ,
2020-08-06 17:51:29 +00:00
& cstructs . ClientCSIControllerDetachVolumeResponse { } )
if err != nil {
return fmt . Errorf ( "could not detach from controller: %v" , err )
}
claim . State = structs . CSIVolumeClaimStateReadyToFree
return v . checkpointClaim ( vol , claim )
}
// lookupExternalNodeID gets the CSI plugin's ID for a node. we look it up in
// the volume's claims first because it's possible the client has been stopped
// and GC'd by this point, so looking there is the last resort.
func ( v * CSIVolume ) lookupExternalNodeID ( vol * structs . CSIVolume , claim * structs . CSIVolumeClaim ) ( string , error ) {
for _ , rClaim := range vol . ReadClaims {
if rClaim . NodeID == claim . NodeID {
return rClaim . ExternalNodeID , nil
}
}
for _ , wClaim := range vol . WriteClaims {
if wClaim . NodeID == claim . NodeID {
return wClaim . ExternalNodeID , nil
}
}
for _ , pClaim := range vol . PastClaims {
if pClaim . NodeID == claim . NodeID {
return pClaim . ExternalNodeID , nil
}
}
// fallback to looking up the node plugin
ws := memdb . NewWatchSet ( )
state := v . srv . fsm . State ( )
targetNode , err := state . NodeByID ( ws , claim . NodeID )
if err != nil {
return "" , err
}
if targetNode == nil {
return "" , fmt . Errorf ( "%s: %s" , structs . ErrUnknownNodePrefix , claim . NodeID )
}
// get the the storage provider's ID for the client node (not
// Nomad's ID for the node)
targetCSIInfo , ok := targetNode . CSINodePlugins [ vol . PluginID ]
2020-08-07 15:01:36 +00:00
if ! ok || targetCSIInfo . NodeInfo == nil {
2020-08-06 17:51:29 +00:00
return "" , fmt . Errorf ( "failed to find storage provider info for client %q, node plugin %q is not running or has not fingerprinted on this client" , targetNode . ID , vol . PluginID )
}
return targetCSIInfo . NodeInfo . ID , nil
}
func ( v * CSIVolume ) checkpointClaim ( vol * structs . CSIVolume , claim * structs . CSIVolumeClaim ) error {
v . logger . Trace ( "checkpointing claim" )
req := structs . CSIVolumeClaimRequest {
VolumeID : vol . ID ,
AllocationID : claim . AllocationID ,
NodeID : claim . NodeID ,
Claim : claim . Mode ,
State : claim . State ,
WriteRequest : structs . WriteRequest {
Namespace : vol . Namespace ,
} ,
}
resp , index , err := v . srv . raftApply ( structs . CSIVolumeClaimRequestType , req )
if err != nil {
v . logger . Error ( "csi raft apply failed" , "error" , err )
return err
}
if respErr , ok := resp . ( error ) ; ok {
return respErr
}
vol . ModifyIndex = index
return nil
}
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 )
2020-05-20 14:22:24 +00:00
if args . ID == "" {
return fmt . Errorf ( "missing plugin ID" )
}
2020-01-28 15:28:34 +00:00
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 )
2020-05-20 14:22:24 +00:00
if args . ID == "" {
return fmt . Errorf ( "missing plugin ID" )
}
2020-05-06 20:49:12 +00:00
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
}