github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/command/agent/csi_endpoint.go (about)

     1  package agent
     2  
     3  import (
     4  	"net/http"
     5  	"strconv"
     6  	"strings"
     7  
     8  	"github.com/hashicorp/nomad/nomad/structs"
     9  )
    10  
    11  func (s *HTTPServer) CSIVolumesRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
    12  	switch req.Method {
    13  	case http.MethodPut, http.MethodPost:
    14  		return s.csiVolumeRegister(resp, req)
    15  	case http.MethodGet:
    16  	default:
    17  		return nil, CodedError(405, ErrInvalidMethod)
    18  	}
    19  
    20  	// Type filters volume lists to a specific type. When support for non-CSI volumes is
    21  	// introduced, we'll need to dispatch here
    22  	query := req.URL.Query()
    23  	qtype, ok := query["type"]
    24  	if !ok {
    25  		return []*structs.CSIVolListStub{}, nil
    26  	}
    27  	if qtype[0] != "csi" {
    28  		return nil, nil
    29  	}
    30  
    31  	args := structs.CSIVolumeListRequest{}
    32  	if s.parse(resp, req, &args.Region, &args.QueryOptions) {
    33  		return nil, nil
    34  	}
    35  
    36  	args.Prefix = query.Get("prefix")
    37  	args.PluginID = query.Get("plugin_id")
    38  	args.NodeID = query.Get("node_id")
    39  
    40  	var out structs.CSIVolumeListResponse
    41  	if err := s.agent.RPC("CSIVolume.List", &args, &out); err != nil {
    42  		return nil, err
    43  	}
    44  
    45  	setMeta(resp, &out.QueryMeta)
    46  	return out.Volumes, nil
    47  }
    48  
    49  func (s *HTTPServer) CSIExternalVolumesRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
    50  	if req.Method != http.MethodGet {
    51  		return nil, CodedError(405, ErrInvalidMethod)
    52  	}
    53  
    54  	args := structs.CSIVolumeExternalListRequest{}
    55  	if s.parse(resp, req, &args.Region, &args.QueryOptions) {
    56  		return nil, nil
    57  	}
    58  
    59  	query := req.URL.Query()
    60  	args.PluginID = query.Get("plugin_id")
    61  
    62  	var out structs.CSIVolumeExternalListResponse
    63  	if err := s.agent.RPC("CSIVolume.ListExternal", &args, &out); err != nil {
    64  		return nil, err
    65  	}
    66  
    67  	setMeta(resp, &out.QueryMeta)
    68  	return out, nil
    69  }
    70  
    71  // CSIVolumeSpecificRequest dispatches GET and PUT
    72  func (s *HTTPServer) CSIVolumeSpecificRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
    73  	// Tokenize the suffix of the path to get the volume id
    74  	reqSuffix := strings.TrimPrefix(req.URL.Path, "/v1/volume/csi/")
    75  	tokens := strings.Split(reqSuffix, "/")
    76  	if len(tokens) < 1 {
    77  		return nil, CodedError(404, resourceNotFoundErr)
    78  	}
    79  	id := tokens[0]
    80  
    81  	if len(tokens) == 1 {
    82  		switch req.Method {
    83  		case http.MethodGet:
    84  			return s.csiVolumeGet(id, resp, req)
    85  		case http.MethodPut:
    86  			return s.csiVolumeRegister(resp, req)
    87  		case http.MethodDelete:
    88  			return s.csiVolumeDeregister(id, resp, req)
    89  		default:
    90  			return nil, CodedError(405, ErrInvalidMethod)
    91  		}
    92  	}
    93  
    94  	if len(tokens) == 2 {
    95  		switch req.Method {
    96  		case http.MethodPut:
    97  			if tokens[1] == "create" {
    98  				return s.csiVolumeCreate(resp, req)
    99  			}
   100  		case http.MethodDelete:
   101  			if tokens[1] == "detach" {
   102  				return s.csiVolumeDetach(id, resp, req)
   103  			}
   104  			if tokens[1] == "delete" {
   105  				return s.csiVolumeDelete(id, resp, req)
   106  			}
   107  		default:
   108  			return nil, CodedError(405, ErrInvalidMethod)
   109  		}
   110  	}
   111  
   112  	return nil, CodedError(404, resourceNotFoundErr)
   113  }
   114  
   115  func (s *HTTPServer) csiVolumeGet(id string, resp http.ResponseWriter, req *http.Request) (interface{}, error) {
   116  	args := structs.CSIVolumeGetRequest{
   117  		ID: id,
   118  	}
   119  	if s.parse(resp, req, &args.Region, &args.QueryOptions) {
   120  		return nil, nil
   121  	}
   122  
   123  	var out structs.CSIVolumeGetResponse
   124  	if err := s.agent.RPC("CSIVolume.Get", &args, &out); err != nil {
   125  		return nil, err
   126  	}
   127  
   128  	setMeta(resp, &out.QueryMeta)
   129  	if out.Volume == nil {
   130  		return nil, CodedError(404, "volume not found")
   131  	}
   132  
   133  	return out.Volume, nil
   134  }
   135  
   136  func (s *HTTPServer) csiVolumeRegister(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
   137  	switch req.Method {
   138  	case http.MethodPost, http.MethodPut:
   139  	default:
   140  		return nil, CodedError(405, ErrInvalidMethod)
   141  	}
   142  
   143  	args := structs.CSIVolumeRegisterRequest{}
   144  	if err := decodeBody(req, &args); err != nil {
   145  		return err, CodedError(400, err.Error())
   146  	}
   147  	s.parseWriteRequest(req, &args.WriteRequest)
   148  
   149  	var out structs.CSIVolumeRegisterResponse
   150  	if err := s.agent.RPC("CSIVolume.Register", &args, &out); err != nil {
   151  		return nil, err
   152  	}
   153  
   154  	setMeta(resp, &out.QueryMeta)
   155  
   156  	return nil, nil
   157  }
   158  
   159  func (s *HTTPServer) csiVolumeCreate(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
   160  	switch req.Method {
   161  	case http.MethodPost, http.MethodPut:
   162  	default:
   163  		return nil, CodedError(405, ErrInvalidMethod)
   164  	}
   165  
   166  	args := structs.CSIVolumeCreateRequest{}
   167  	if err := decodeBody(req, &args); err != nil {
   168  		return err, CodedError(400, err.Error())
   169  	}
   170  	s.parseWriteRequest(req, &args.WriteRequest)
   171  
   172  	var out structs.CSIVolumeCreateResponse
   173  	if err := s.agent.RPC("CSIVolume.Create", &args, &out); err != nil {
   174  		return nil, err
   175  	}
   176  
   177  	setMeta(resp, &out.QueryMeta)
   178  
   179  	return out, nil
   180  }
   181  
   182  func (s *HTTPServer) csiVolumeDeregister(id string, resp http.ResponseWriter, req *http.Request) (interface{}, error) {
   183  	if req.Method != http.MethodDelete {
   184  		return nil, CodedError(405, ErrInvalidMethod)
   185  	}
   186  
   187  	raw := req.URL.Query().Get("force")
   188  	var force bool
   189  	if raw != "" {
   190  		var err error
   191  		force, err = strconv.ParseBool(raw)
   192  		if err != nil {
   193  			return nil, CodedError(400, "invalid force value")
   194  		}
   195  	}
   196  
   197  	args := structs.CSIVolumeDeregisterRequest{
   198  		VolumeIDs: []string{id},
   199  		Force:     force,
   200  	}
   201  	s.parseWriteRequest(req, &args.WriteRequest)
   202  
   203  	var out structs.CSIVolumeDeregisterResponse
   204  	if err := s.agent.RPC("CSIVolume.Deregister", &args, &out); err != nil {
   205  		return nil, err
   206  	}
   207  
   208  	setMeta(resp, &out.QueryMeta)
   209  
   210  	return nil, nil
   211  }
   212  
   213  func (s *HTTPServer) csiVolumeDelete(id string, resp http.ResponseWriter, req *http.Request) (interface{}, error) {
   214  	if req.Method != http.MethodDelete {
   215  		return nil, CodedError(405, ErrInvalidMethod)
   216  	}
   217  
   218  	secrets := parseCSISecrets(req)
   219  	args := structs.CSIVolumeDeleteRequest{
   220  		VolumeIDs: []string{id},
   221  		Secrets:   secrets,
   222  	}
   223  	s.parseWriteRequest(req, &args.WriteRequest)
   224  
   225  	var out structs.CSIVolumeDeleteResponse
   226  	if err := s.agent.RPC("CSIVolume.Delete", &args, &out); err != nil {
   227  		return nil, err
   228  	}
   229  
   230  	setMeta(resp, &out.QueryMeta)
   231  
   232  	return nil, nil
   233  }
   234  
   235  func (s *HTTPServer) csiVolumeDetach(id string, resp http.ResponseWriter, req *http.Request) (interface{}, error) {
   236  	if req.Method != http.MethodDelete {
   237  		return nil, CodedError(405, ErrInvalidMethod)
   238  	}
   239  
   240  	nodeID := req.URL.Query().Get("node")
   241  	if nodeID == "" {
   242  		return nil, CodedError(400, "detach requires node ID")
   243  	}
   244  
   245  	args := structs.CSIVolumeUnpublishRequest{
   246  		VolumeID: id,
   247  		Claim: &structs.CSIVolumeClaim{
   248  			NodeID: nodeID,
   249  			Mode:   structs.CSIVolumeClaimGC,
   250  		},
   251  	}
   252  	s.parseWriteRequest(req, &args.WriteRequest)
   253  
   254  	var out structs.CSIVolumeUnpublishResponse
   255  	if err := s.agent.RPC("CSIVolume.Unpublish", &args, &out); err != nil {
   256  		return nil, err
   257  	}
   258  
   259  	setMeta(resp, &out.QueryMeta)
   260  	return nil, nil
   261  }
   262  
   263  func (s *HTTPServer) CSISnapshotsRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
   264  	switch req.Method {
   265  	case http.MethodPut, http.MethodPost:
   266  		return s.csiSnapshotCreate(resp, req)
   267  	case http.MethodDelete:
   268  		return s.csiSnapshotDelete(resp, req)
   269  	case http.MethodGet:
   270  		return s.csiSnapshotList(resp, req)
   271  	}
   272  	return nil, CodedError(405, ErrInvalidMethod)
   273  }
   274  
   275  func (s *HTTPServer) csiSnapshotCreate(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
   276  
   277  	args := structs.CSISnapshotCreateRequest{}
   278  	if err := decodeBody(req, &args); err != nil {
   279  		return err, CodedError(400, err.Error())
   280  	}
   281  	s.parseWriteRequest(req, &args.WriteRequest)
   282  
   283  	var out structs.CSISnapshotCreateResponse
   284  	if err := s.agent.RPC("CSIVolume.CreateSnapshot", &args, &out); err != nil {
   285  		return nil, err
   286  	}
   287  
   288  	setMeta(resp, &out.QueryMeta)
   289  	return out, nil
   290  }
   291  
   292  func (s *HTTPServer) csiSnapshotDelete(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
   293  
   294  	args := structs.CSISnapshotDeleteRequest{}
   295  	s.parseWriteRequest(req, &args.WriteRequest)
   296  
   297  	snap := &structs.CSISnapshot{Secrets: structs.CSISecrets{}}
   298  
   299  	query := req.URL.Query()
   300  	snap.PluginID = query.Get("plugin_id")
   301  	snap.ID = query.Get("snapshot_id")
   302  
   303  	secrets := parseCSISecrets(req)
   304  	snap.Secrets = secrets
   305  
   306  	args.Snapshots = []*structs.CSISnapshot{snap}
   307  
   308  	var out structs.CSISnapshotDeleteResponse
   309  	if err := s.agent.RPC("CSIVolume.DeleteSnapshot", &args, &out); err != nil {
   310  		return nil, err
   311  	}
   312  
   313  	setMeta(resp, &out.QueryMeta)
   314  	return nil, nil
   315  }
   316  
   317  func (s *HTTPServer) csiSnapshotList(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
   318  
   319  	args := structs.CSISnapshotListRequest{}
   320  	if s.parse(resp, req, &args.Region, &args.QueryOptions) {
   321  		return nil, nil
   322  	}
   323  
   324  	query := req.URL.Query()
   325  	args.PluginID = query.Get("plugin_id")
   326  	secrets := parseCSISecrets(req)
   327  	args.Secrets = secrets
   328  	var out structs.CSISnapshotListResponse
   329  	if err := s.agent.RPC("CSIVolume.ListSnapshots", &args, &out); err != nil {
   330  		return nil, err
   331  	}
   332  
   333  	setMeta(resp, &out.QueryMeta)
   334  	return out, nil
   335  }
   336  
   337  // CSIPluginsRequest lists CSI plugins
   338  func (s *HTTPServer) CSIPluginsRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
   339  	if req.Method != http.MethodGet {
   340  		return nil, CodedError(405, ErrInvalidMethod)
   341  	}
   342  
   343  	// Type filters plugin lists to a specific type. When support for non-CSI plugins is
   344  	// introduced, we'll need to dispatch here
   345  	query := req.URL.Query()
   346  	qtype, ok := query["type"]
   347  	if !ok {
   348  		return []*structs.CSIPluginListStub{}, nil
   349  	}
   350  	if qtype[0] != "csi" {
   351  		return nil, nil
   352  	}
   353  
   354  	args := structs.CSIPluginListRequest{}
   355  
   356  	if s.parse(resp, req, &args.Region, &args.QueryOptions) {
   357  		return nil, nil
   358  	}
   359  
   360  	var out structs.CSIPluginListResponse
   361  	if err := s.agent.RPC("CSIPlugin.List", &args, &out); err != nil {
   362  		return nil, err
   363  	}
   364  
   365  	setMeta(resp, &out.QueryMeta)
   366  	return out.Plugins, nil
   367  }
   368  
   369  // CSIPluginSpecificRequest list the job with CSIInfo
   370  func (s *HTTPServer) CSIPluginSpecificRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
   371  	if req.Method != http.MethodGet {
   372  		return nil, CodedError(405, ErrInvalidMethod)
   373  	}
   374  
   375  	// Tokenize the suffix of the path to get the plugin id
   376  	reqSuffix := strings.TrimPrefix(req.URL.Path, "/v1/plugin/csi/")
   377  	tokens := strings.Split(reqSuffix, "/")
   378  	if len(tokens) > 2 || len(tokens) < 1 {
   379  		return nil, CodedError(404, resourceNotFoundErr)
   380  	}
   381  	id := tokens[0]
   382  
   383  	args := structs.CSIPluginGetRequest{ID: id}
   384  	if s.parse(resp, req, &args.Region, &args.QueryOptions) {
   385  		return nil, nil
   386  	}
   387  
   388  	var out structs.CSIPluginGetResponse
   389  	if err := s.agent.RPC("CSIPlugin.Get", &args, &out); err != nil {
   390  		return nil, err
   391  	}
   392  
   393  	setMeta(resp, &out.QueryMeta)
   394  	if out.Plugin == nil {
   395  		return nil, CodedError(404, "plugin not found")
   396  	}
   397  
   398  	return out.Plugin, nil
   399  }
   400  
   401  // parseCSISecrets extracts a map of k/v pairs from the CSI secrets
   402  // header. Silently ignores invalid secrets
   403  func parseCSISecrets(req *http.Request) structs.CSISecrets {
   404  	secretsHeader := req.Header.Get("X-Nomad-CSI-Secrets")
   405  	if secretsHeader == "" {
   406  		return nil
   407  	}
   408  
   409  	secrets := map[string]string{}
   410  	secretkvs := strings.Split(secretsHeader, ",")
   411  	for _, secretkv := range secretkvs {
   412  		kv := strings.Split(secretkv, "=")
   413  		if len(kv) == 2 {
   414  			secrets[kv[0]] = kv[1]
   415  		}
   416  	}
   417  	if len(secrets) == 0 {
   418  		return nil
   419  	}
   420  	return structs.CSISecrets(secrets)
   421  }