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 }