github.com/bigcommerce/nomad@v0.9.3-bc/command/agent/alloc_endpoint.go (about) 1 package agent 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "io" 8 "net" 9 "net/http" 10 "strconv" 11 "strings" 12 13 "github.com/golang/snappy" 14 "github.com/gorilla/websocket" 15 cstructs "github.com/hashicorp/nomad/client/structs" 16 "github.com/hashicorp/nomad/nomad/structs" 17 "github.com/hashicorp/nomad/plugins/drivers" 18 "github.com/ugorji/go/codec" 19 ) 20 21 const ( 22 allocNotFoundErr = "allocation not found" 23 resourceNotFoundErr = "resource not found" 24 ) 25 26 func (s *HTTPServer) AllocsRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) { 27 if req.Method != "GET" { 28 return nil, CodedError(405, ErrInvalidMethod) 29 } 30 31 args := structs.AllocListRequest{} 32 if s.parse(resp, req, &args.Region, &args.QueryOptions) { 33 return nil, nil 34 } 35 36 var out structs.AllocListResponse 37 if err := s.agent.RPC("Alloc.List", &args, &out); err != nil { 38 return nil, err 39 } 40 41 setMeta(resp, &out.QueryMeta) 42 if out.Allocations == nil { 43 out.Allocations = make([]*structs.AllocListStub, 0) 44 } 45 for _, alloc := range out.Allocations { 46 alloc.SetEventDisplayMessages() 47 } 48 return out.Allocations, nil 49 } 50 51 func (s *HTTPServer) AllocSpecificRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) { 52 reqSuffix := strings.TrimPrefix(req.URL.Path, "/v1/allocation/") 53 54 // tokenize the suffix of the path to get the alloc id and find the action 55 // invoked on the alloc id 56 tokens := strings.Split(reqSuffix, "/") 57 if len(tokens) > 2 || len(tokens) < 1 { 58 return nil, CodedError(404, resourceNotFoundErr) 59 } 60 allocID := tokens[0] 61 62 if len(tokens) == 1 { 63 return s.allocGet(allocID, resp, req) 64 } 65 66 switch tokens[1] { 67 case "stop": 68 return s.allocStop(allocID, resp, req) 69 } 70 71 return nil, CodedError(404, resourceNotFoundErr) 72 } 73 74 func (s *HTTPServer) allocGet(allocID string, resp http.ResponseWriter, req *http.Request) (interface{}, error) { 75 if req.Method != "GET" { 76 return nil, CodedError(405, ErrInvalidMethod) 77 } 78 79 args := structs.AllocSpecificRequest{ 80 AllocID: allocID, 81 } 82 if s.parse(resp, req, &args.Region, &args.QueryOptions) { 83 return nil, nil 84 } 85 86 var out structs.SingleAllocResponse 87 if err := s.agent.RPC("Alloc.GetAlloc", &args, &out); err != nil { 88 return nil, err 89 } 90 91 setMeta(resp, &out.QueryMeta) 92 if out.Alloc == nil { 93 return nil, CodedError(404, "alloc not found") 94 } 95 96 // Decode the payload if there is any 97 alloc := out.Alloc 98 if alloc.Job != nil && len(alloc.Job.Payload) != 0 { 99 decoded, err := snappy.Decode(nil, alloc.Job.Payload) 100 if err != nil { 101 return nil, err 102 } 103 alloc = alloc.Copy() 104 alloc.Job.Payload = decoded 105 } 106 alloc.SetEventDisplayMessages() 107 108 return alloc, nil 109 } 110 111 func (s *HTTPServer) allocStop(allocID string, resp http.ResponseWriter, req *http.Request) (interface{}, error) { 112 if !(req.Method == "POST" || req.Method == "PUT") { 113 return nil, CodedError(405, ErrInvalidMethod) 114 } 115 116 sr := &structs.AllocStopRequest{ 117 AllocID: allocID, 118 } 119 s.parseWriteRequest(req, &sr.WriteRequest) 120 121 var out structs.AllocStopResponse 122 rpcErr := s.agent.RPC("Alloc.Stop", &sr, &out) 123 124 if rpcErr != nil { 125 if structs.IsErrUnknownAllocation(rpcErr) { 126 rpcErr = CodedError(404, allocNotFoundErr) 127 } 128 } 129 130 return &out, rpcErr 131 } 132 133 func (s *HTTPServer) ClientAllocRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) { 134 reqSuffix := strings.TrimPrefix(req.URL.Path, "/v1/client/allocation/") 135 136 // tokenize the suffix of the path to get the alloc id and find the action 137 // invoked on the alloc id 138 tokens := strings.Split(reqSuffix, "/") 139 if len(tokens) != 2 { 140 return nil, CodedError(404, resourceNotFoundErr) 141 } 142 allocID := tokens[0] 143 switch tokens[1] { 144 case "stats": 145 return s.allocStats(allocID, resp, req) 146 case "exec": 147 return s.allocExec(allocID, resp, req) 148 case "snapshot": 149 if s.agent.client == nil { 150 return nil, clientNotRunning 151 } 152 return s.allocSnapshot(allocID, resp, req) 153 case "restart": 154 return s.allocRestart(allocID, resp, req) 155 case "gc": 156 return s.allocGC(allocID, resp, req) 157 case "signal": 158 return s.allocSignal(allocID, resp, req) 159 } 160 161 return nil, CodedError(404, resourceNotFoundErr) 162 } 163 164 func (s *HTTPServer) ClientGCRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) { 165 // Get the requested Node ID 166 requestedNode := req.URL.Query().Get("node_id") 167 168 // Build the request and parse the ACL token 169 args := structs.NodeSpecificRequest{ 170 NodeID: requestedNode, 171 } 172 s.parse(resp, req, &args.QueryOptions.Region, &args.QueryOptions) 173 174 // Determine the handler to use 175 useLocalClient, useClientRPC, useServerRPC := s.rpcHandlerForNode(requestedNode) 176 177 // Make the RPC 178 var reply structs.GenericResponse 179 var rpcErr error 180 if useLocalClient { 181 rpcErr = s.agent.Client().ClientRPC("Allocations.GarbageCollectAll", &args, &reply) 182 } else if useClientRPC { 183 rpcErr = s.agent.Client().RPC("ClientAllocations.GarbageCollectAll", &args, &reply) 184 } else if useServerRPC { 185 rpcErr = s.agent.Server().RPC("ClientAllocations.GarbageCollectAll", &args, &reply) 186 } else { 187 rpcErr = CodedError(400, "No local Node and node_id not provided") 188 } 189 190 if rpcErr != nil { 191 if structs.IsErrNoNodeConn(rpcErr) { 192 rpcErr = CodedError(404, rpcErr.Error()) 193 } 194 } 195 196 return nil, rpcErr 197 } 198 199 func (s *HTTPServer) allocRestart(allocID string, resp http.ResponseWriter, req *http.Request) (interface{}, error) { 200 // Build the request and parse the ACL token 201 args := structs.AllocRestartRequest{ 202 AllocID: allocID, 203 TaskName: "", 204 } 205 s.parse(resp, req, &args.QueryOptions.Region, &args.QueryOptions) 206 207 // Explicitly parse the body separately to disallow overriding AllocID in req Body. 208 var reqBody struct { 209 TaskName string 210 } 211 err := json.NewDecoder(req.Body).Decode(&reqBody) 212 if err != nil && err != io.EOF { 213 return nil, err 214 } 215 if reqBody.TaskName != "" { 216 args.TaskName = reqBody.TaskName 217 } 218 219 // Determine the handler to use 220 useLocalClient, useClientRPC, useServerRPC := s.rpcHandlerForAlloc(allocID) 221 222 // Make the RPC 223 var reply structs.GenericResponse 224 var rpcErr error 225 if useLocalClient { 226 rpcErr = s.agent.Client().ClientRPC("Allocations.Restart", &args, &reply) 227 } else if useClientRPC { 228 rpcErr = s.agent.Client().RPC("ClientAllocations.Restart", &args, &reply) 229 } else if useServerRPC { 230 rpcErr = s.agent.Server().RPC("ClientAllocations.Restart", &args, &reply) 231 } else { 232 rpcErr = CodedError(400, "No local Node and node_id not provided") 233 } 234 235 if rpcErr != nil { 236 if structs.IsErrNoNodeConn(rpcErr) || structs.IsErrUnknownAllocation(rpcErr) { 237 rpcErr = CodedError(404, rpcErr.Error()) 238 } 239 } 240 241 return reply, rpcErr 242 } 243 244 func (s *HTTPServer) allocGC(allocID string, resp http.ResponseWriter, req *http.Request) (interface{}, error) { 245 // Build the request and parse the ACL token 246 args := structs.AllocSpecificRequest{ 247 AllocID: allocID, 248 } 249 s.parse(resp, req, &args.QueryOptions.Region, &args.QueryOptions) 250 251 // Determine the handler to use 252 useLocalClient, useClientRPC, useServerRPC := s.rpcHandlerForAlloc(allocID) 253 254 // Make the RPC 255 var reply structs.GenericResponse 256 var rpcErr error 257 if useLocalClient { 258 rpcErr = s.agent.Client().ClientRPC("Allocations.GarbageCollect", &args, &reply) 259 } else if useClientRPC { 260 rpcErr = s.agent.Client().RPC("ClientAllocations.GarbageCollect", &args, &reply) 261 } else if useServerRPC { 262 rpcErr = s.agent.Server().RPC("ClientAllocations.GarbageCollect", &args, &reply) 263 } else { 264 rpcErr = CodedError(400, "No local Node and node_id not provided") 265 } 266 267 if rpcErr != nil { 268 if structs.IsErrNoNodeConn(rpcErr) || structs.IsErrUnknownAllocation(rpcErr) { 269 rpcErr = CodedError(404, rpcErr.Error()) 270 } 271 } 272 273 return nil, rpcErr 274 } 275 276 func (s *HTTPServer) allocSignal(allocID string, resp http.ResponseWriter, req *http.Request) (interface{}, error) { 277 if !(req.Method == "POST" || req.Method == "PUT") { 278 return nil, CodedError(405, ErrInvalidMethod) 279 } 280 281 // Build the request and parse the ACL token 282 args := structs.AllocSignalRequest{} 283 err := decodeBody(req, &args) 284 if err != nil { 285 return nil, CodedError(400, fmt.Sprintf("Failed to decode body: %v", err)) 286 } 287 s.parse(resp, req, &args.QueryOptions.Region, &args.QueryOptions) 288 args.AllocID = allocID 289 290 // Determine the handler to use 291 useLocalClient, useClientRPC, useServerRPC := s.rpcHandlerForAlloc(allocID) 292 293 // Make the RPC 294 var reply structs.GenericResponse 295 var rpcErr error 296 if useLocalClient { 297 rpcErr = s.agent.Client().ClientRPC("Allocations.Signal", &args, &reply) 298 } else if useClientRPC { 299 rpcErr = s.agent.Client().RPC("ClientAllocations.Signal", &args, &reply) 300 } else if useServerRPC { 301 rpcErr = s.agent.Server().RPC("ClientAllocations.Signal", &args, &reply) 302 } else { 303 rpcErr = CodedError(400, "No local Node and node_id not provided") 304 } 305 306 if rpcErr != nil { 307 if structs.IsErrNoNodeConn(rpcErr) || structs.IsErrUnknownAllocation(rpcErr) { 308 rpcErr = CodedError(404, rpcErr.Error()) 309 } 310 } 311 312 return reply, rpcErr 313 } 314 315 func (s *HTTPServer) allocSnapshot(allocID string, resp http.ResponseWriter, req *http.Request) (interface{}, error) { 316 var secret string 317 s.parseToken(req, &secret) 318 if !s.agent.Client().ValidateMigrateToken(allocID, secret) { 319 return nil, structs.ErrPermissionDenied 320 } 321 322 allocFS, err := s.agent.Client().GetAllocFS(allocID) 323 if err != nil { 324 return nil, fmt.Errorf(allocNotFoundErr) 325 } 326 if err := allocFS.Snapshot(resp); err != nil { 327 return nil, fmt.Errorf("error making snapshot: %v", err) 328 } 329 return nil, nil 330 } 331 332 func (s *HTTPServer) allocStats(allocID string, resp http.ResponseWriter, req *http.Request) (interface{}, error) { 333 334 // Build the request and parse the ACL token 335 task := req.URL.Query().Get("task") 336 args := cstructs.AllocStatsRequest{ 337 AllocID: allocID, 338 Task: task, 339 } 340 s.parse(resp, req, &args.QueryOptions.Region, &args.QueryOptions) 341 342 // Determine the handler to use 343 useLocalClient, useClientRPC, useServerRPC := s.rpcHandlerForAlloc(allocID) 344 345 // Make the RPC 346 var reply cstructs.AllocStatsResponse 347 var rpcErr error 348 if useLocalClient { 349 rpcErr = s.agent.Client().ClientRPC("Allocations.Stats", &args, &reply) 350 } else if useClientRPC { 351 rpcErr = s.agent.Client().RPC("ClientAllocations.Stats", &args, &reply) 352 } else if useServerRPC { 353 rpcErr = s.agent.Server().RPC("ClientAllocations.Stats", &args, &reply) 354 } else { 355 rpcErr = CodedError(400, "No local Node and node_id not provided") 356 } 357 358 if rpcErr != nil { 359 if structs.IsErrNoNodeConn(rpcErr) || structs.IsErrUnknownAllocation(rpcErr) { 360 rpcErr = CodedError(404, rpcErr.Error()) 361 } 362 } 363 364 return reply.Stats, rpcErr 365 } 366 367 func (s *HTTPServer) allocExec(allocID string, resp http.ResponseWriter, req *http.Request) (interface{}, error) { 368 // Build the request and parse the ACL token 369 task := req.URL.Query().Get("task") 370 cmdJsonStr := req.URL.Query().Get("command") 371 var command []string 372 err := json.Unmarshal([]byte(cmdJsonStr), &command) 373 if err != nil { 374 // this shouldn't happen, []string is always be serializable to json 375 return nil, fmt.Errorf("failed to marshal command into json: %v", err) 376 } 377 378 ttyB := false 379 if tty := req.URL.Query().Get("tty"); tty != "" { 380 ttyB, err = strconv.ParseBool(tty) 381 if err != nil { 382 return nil, fmt.Errorf("tty value is not a boolean: %v", err) 383 } 384 } 385 386 args := cstructs.AllocExecRequest{ 387 AllocID: allocID, 388 Task: task, 389 Cmd: command, 390 Tty: ttyB, 391 } 392 s.parse(resp, req, &args.QueryOptions.Region, &args.QueryOptions) 393 394 conn, err := s.wsUpgrader.Upgrade(resp, req, nil) 395 if err != nil { 396 return nil, fmt.Errorf("failed to upgrade connection: %v", err) 397 } 398 399 return s.execStreamImpl(conn, &args) 400 } 401 402 func (s *HTTPServer) execStreamImpl(ws *websocket.Conn, args *cstructs.AllocExecRequest) (interface{}, error) { 403 allocID := args.AllocID 404 method := "Allocations.Exec" 405 406 // Get the correct handler 407 localClient, remoteClient, localServer := s.rpcHandlerForAlloc(allocID) 408 var handler structs.StreamingRpcHandler 409 var handlerErr error 410 if localClient { 411 handler, handlerErr = s.agent.Client().StreamingRpcHandler(method) 412 } else if remoteClient { 413 handler, handlerErr = s.agent.Client().RemoteStreamingRpcHandler(method) 414 } else if localServer { 415 handler, handlerErr = s.agent.Server().StreamingRpcHandler(method) 416 } 417 418 if handlerErr != nil { 419 return nil, CodedError(500, handlerErr.Error()) 420 } 421 422 // Create a pipe connecting the (possibly remote) handler to the http response 423 httpPipe, handlerPipe := net.Pipe() 424 decoder := codec.NewDecoder(httpPipe, structs.MsgpackHandle) 425 encoder := codec.NewEncoder(httpPipe, structs.MsgpackHandle) 426 427 // Create a goroutine that closes the pipe if the connection closes. 428 ctx, cancel := context.WithCancel(context.Background()) 429 go func() { 430 <-ctx.Done() 431 httpPipe.Close() 432 433 // don't close ws - wait to drain messages 434 }() 435 436 // Create a channel that decodes the results 437 errCh := make(chan HTTPCodedError, 2) 438 439 // stream response 440 go func() { 441 defer cancel() 442 443 // Send the request 444 if err := encoder.Encode(args); err != nil { 445 errCh <- CodedError(500, err.Error()) 446 return 447 } 448 449 go forwardExecInput(encoder, ws, errCh) 450 451 for { 452 select { 453 case <-ctx.Done(): 454 errCh <- nil 455 return 456 default: 457 } 458 459 var res cstructs.StreamErrWrapper 460 err := decoder.Decode(&res) 461 if isClosedError(err) { 462 ws.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) 463 errCh <- nil 464 return 465 } 466 467 if err != nil { 468 errCh <- CodedError(500, err.Error()) 469 return 470 } 471 decoder.Reset(httpPipe) 472 473 if err := res.Error; err != nil { 474 code := 500 475 if err.Code != nil { 476 code = int(*err.Code) 477 } 478 errCh <- CodedError(code, err.Error()) 479 return 480 } 481 482 if err := ws.WriteMessage(websocket.TextMessage, res.Payload); err != nil { 483 errCh <- CodedError(500, err.Error()) 484 return 485 } 486 } 487 }() 488 489 // start streaming request to streaming RPC - returns when streaming completes or errors 490 handler(handlerPipe) 491 // stop streaming background goroutines for streaming - but not websocket activity 492 cancel() 493 // retreieve any error and/or wait until goroutine stop and close errCh connection before 494 // closing websocket connection 495 codedErr := <-errCh 496 497 if isClosedError(codedErr) { 498 codedErr = nil 499 } else if codedErr != nil { 500 ws.WriteMessage(websocket.CloseMessage, 501 websocket.FormatCloseMessage(toWsCode(codedErr.Code()), codedErr.Error())) 502 } 503 ws.Close() 504 505 return nil, codedErr 506 } 507 508 func toWsCode(httpCode int) int { 509 switch httpCode { 510 case 500: 511 return websocket.CloseInternalServerErr 512 default: 513 // placeholder error code 514 return websocket.ClosePolicyViolation 515 } 516 } 517 518 func isClosedError(err error) bool { 519 if err == nil { 520 return false 521 } 522 523 return err == io.EOF || 524 err == io.ErrClosedPipe || 525 strings.Contains(err.Error(), "closed") || 526 strings.Contains(err.Error(), "EOF") 527 } 528 529 // forwardExecInput forwards exec input (e.g. stdin) from websocket connection 530 // to the streaming RPC connection to client 531 func forwardExecInput(encoder *codec.Encoder, ws *websocket.Conn, errCh chan<- HTTPCodedError) { 532 for { 533 sf := &drivers.ExecTaskStreamingRequestMsg{} 534 err := ws.ReadJSON(sf) 535 if err == io.EOF { 536 return 537 } 538 539 if err != nil { 540 errCh <- CodedError(500, err.Error()) 541 return 542 } 543 544 err = encoder.Encode(sf) 545 if err != nil { 546 errCh <- CodedError(500, err.Error()) 547 } 548 } 549 }