github.com/manicqin/nomad@v0.9.5/command/agent/fs_endpoint.go (about) 1 package agent 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "io" 8 "net" 9 "net/http" 10 "strconv" 11 "strings" 12 13 "github.com/docker/docker/pkg/ioutils" 14 cstructs "github.com/hashicorp/nomad/client/structs" 15 "github.com/hashicorp/nomad/nomad/structs" 16 "github.com/ugorji/go/codec" 17 ) 18 19 var ( 20 allocIDNotPresentErr = CodedError(400, "must provide a valid alloc id") 21 fileNameNotPresentErr = CodedError(400, "must provide a file name") 22 taskNotPresentErr = CodedError(400, "must provide task name") 23 logTypeNotPresentErr = CodedError(400, "must provide log type (stdout/stderr)") 24 clientNotRunning = CodedError(400, "node is not running a Nomad Client") 25 invalidOrigin = CodedError(400, "origin must be start or end") 26 ) 27 28 func (s *HTTPServer) FsRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) { 29 path := strings.TrimPrefix(req.URL.Path, "/v1/client/fs/") 30 switch { 31 case strings.HasPrefix(path, "ls/"): 32 return s.DirectoryListRequest(resp, req) 33 case strings.HasPrefix(path, "stat/"): 34 return s.FileStatRequest(resp, req) 35 case strings.HasPrefix(path, "readat/"): 36 return s.FileReadAtRequest(resp, req) 37 case strings.HasPrefix(path, "cat/"): 38 return s.FileCatRequest(resp, req) 39 case strings.HasPrefix(path, "stream/"): 40 return s.Stream(resp, req) 41 case strings.HasPrefix(path, "logs/"): 42 return s.Logs(resp, req) 43 default: 44 return nil, CodedError(404, ErrInvalidMethod) 45 } 46 } 47 48 func (s *HTTPServer) DirectoryListRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) { 49 var allocID, path string 50 51 if allocID = strings.TrimPrefix(req.URL.Path, "/v1/client/fs/ls/"); allocID == "" { 52 return nil, allocIDNotPresentErr 53 } 54 if path = req.URL.Query().Get("path"); path == "" { 55 path = "/" 56 } 57 58 // Create the request 59 args := &cstructs.FsListRequest{ 60 AllocID: allocID, 61 Path: path, 62 } 63 s.parse(resp, req, &args.QueryOptions.Region, &args.QueryOptions) 64 65 // Make the RPC 66 localClient, remoteClient, localServer := s.rpcHandlerForAlloc(allocID) 67 68 var reply cstructs.FsListResponse 69 var rpcErr error 70 if localClient { 71 rpcErr = s.agent.Client().ClientRPC("FileSystem.List", &args, &reply) 72 } else if remoteClient { 73 rpcErr = s.agent.Client().RPC("FileSystem.List", &args, &reply) 74 } else if localServer { 75 rpcErr = s.agent.Server().RPC("FileSystem.List", &args, &reply) 76 } 77 78 if rpcErr != nil { 79 if structs.IsErrNoNodeConn(rpcErr) || structs.IsErrUnknownAllocation(rpcErr) { 80 rpcErr = CodedError(404, rpcErr.Error()) 81 } 82 83 return nil, rpcErr 84 } 85 86 return reply.Files, nil 87 } 88 89 func (s *HTTPServer) FileStatRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) { 90 var allocID, path string 91 if allocID = strings.TrimPrefix(req.URL.Path, "/v1/client/fs/stat/"); allocID == "" { 92 return nil, allocIDNotPresentErr 93 } 94 if path = req.URL.Query().Get("path"); path == "" { 95 return nil, fileNameNotPresentErr 96 } 97 98 // Create the request 99 args := &cstructs.FsStatRequest{ 100 AllocID: allocID, 101 Path: path, 102 } 103 s.parse(resp, req, &args.QueryOptions.Region, &args.QueryOptions) 104 105 // Make the RPC 106 localClient, remoteClient, localServer := s.rpcHandlerForAlloc(allocID) 107 108 var reply cstructs.FsStatResponse 109 var rpcErr error 110 if localClient { 111 rpcErr = s.agent.Client().ClientRPC("FileSystem.Stat", &args, &reply) 112 } else if remoteClient { 113 rpcErr = s.agent.Client().RPC("FileSystem.Stat", &args, &reply) 114 } else if localServer { 115 rpcErr = s.agent.Server().RPC("FileSystem.Stat", &args, &reply) 116 } 117 118 if rpcErr != nil { 119 if structs.IsErrNoNodeConn(rpcErr) || structs.IsErrUnknownAllocation(rpcErr) { 120 rpcErr = CodedError(404, rpcErr.Error()) 121 } 122 123 return nil, rpcErr 124 } 125 126 return reply.Info, nil 127 } 128 129 func (s *HTTPServer) FileReadAtRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) { 130 var allocID, path string 131 var offset, limit int64 132 var err error 133 134 q := req.URL.Query() 135 136 if allocID = strings.TrimPrefix(req.URL.Path, "/v1/client/fs/readat/"); allocID == "" { 137 return nil, allocIDNotPresentErr 138 } 139 if path = q.Get("path"); path == "" { 140 return nil, fileNameNotPresentErr 141 } 142 143 if offset, err = strconv.ParseInt(q.Get("offset"), 10, 64); err != nil { 144 return nil, fmt.Errorf("error parsing offset: %v", err) 145 } 146 147 // Parse the limit 148 if limitStr := q.Get("limit"); limitStr != "" { 149 if limit, err = strconv.ParseInt(limitStr, 10, 64); err != nil { 150 return nil, fmt.Errorf("error parsing limit: %v", err) 151 } 152 } 153 154 // Create the request arguments 155 fsReq := &cstructs.FsStreamRequest{ 156 AllocID: allocID, 157 Path: path, 158 Offset: offset, 159 Origin: "start", 160 Limit: limit, 161 PlainText: true, 162 } 163 s.parse(resp, req, &fsReq.QueryOptions.Region, &fsReq.QueryOptions) 164 165 // Make the request 166 return s.fsStreamImpl(resp, req, "FileSystem.Stream", fsReq, fsReq.AllocID) 167 } 168 169 func (s *HTTPServer) FileCatRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) { 170 var allocID, path string 171 172 q := req.URL.Query() 173 174 if allocID = strings.TrimPrefix(req.URL.Path, "/v1/client/fs/cat/"); allocID == "" { 175 return nil, allocIDNotPresentErr 176 } 177 if path = q.Get("path"); path == "" { 178 return nil, fileNameNotPresentErr 179 } 180 181 // Create the request arguments 182 fsReq := &cstructs.FsStreamRequest{ 183 AllocID: allocID, 184 Path: path, 185 Origin: "start", 186 PlainText: true, 187 } 188 s.parse(resp, req, &fsReq.QueryOptions.Region, &fsReq.QueryOptions) 189 190 // Make the request 191 return s.fsStreamImpl(resp, req, "FileSystem.Stream", fsReq, fsReq.AllocID) 192 } 193 194 // Stream streams the content of a file blocking on EOF. 195 // The parameters are: 196 // * path: path to file to stream. 197 // * follow: A boolean of whether to follow the file, defaults to true. 198 // * offset: The offset to start streaming data at, defaults to zero. 199 // * origin: Either "start" or "end" and defines from where the offset is 200 // applied. Defaults to "start". 201 func (s *HTTPServer) Stream(resp http.ResponseWriter, req *http.Request) (interface{}, error) { 202 var allocID, path string 203 var err error 204 205 q := req.URL.Query() 206 207 if allocID = strings.TrimPrefix(req.URL.Path, "/v1/client/fs/stream/"); allocID == "" { 208 return nil, allocIDNotPresentErr 209 } 210 211 if path = q.Get("path"); path == "" { 212 return nil, fileNameNotPresentErr 213 } 214 215 follow := true 216 if followStr := q.Get("follow"); followStr != "" { 217 if follow, err = strconv.ParseBool(followStr); err != nil { 218 return nil, fmt.Errorf("failed to parse follow field to boolean: %v", err) 219 } 220 } 221 222 var offset int64 223 offsetString := q.Get("offset") 224 if offsetString != "" { 225 if offset, err = strconv.ParseInt(offsetString, 10, 64); err != nil { 226 return nil, fmt.Errorf("error parsing offset: %v", err) 227 } 228 } 229 230 origin := q.Get("origin") 231 switch origin { 232 case "start", "end": 233 case "": 234 origin = "start" 235 default: 236 return nil, invalidOrigin 237 } 238 239 // Create the request arguments 240 fsReq := &cstructs.FsStreamRequest{ 241 AllocID: allocID, 242 Path: path, 243 Origin: origin, 244 Offset: offset, 245 Follow: follow, 246 } 247 s.parse(resp, req, &fsReq.QueryOptions.Region, &fsReq.QueryOptions) 248 249 // Make the request 250 return s.fsStreamImpl(resp, req, "FileSystem.Stream", fsReq, fsReq.AllocID) 251 } 252 253 // Logs streams the content of a log blocking on EOF. The parameters are: 254 // * task: task name to stream logs for. 255 // * type: stdout/stderr to stream. 256 // * follow: A boolean of whether to follow the logs. 257 // * offset: The offset to start streaming data at, defaults to zero. 258 // * origin: Either "start" or "end" and defines from where the offset is 259 // applied. Defaults to "start". 260 func (s *HTTPServer) Logs(resp http.ResponseWriter, req *http.Request) (interface{}, error) { 261 var allocID, task, logType string 262 var plain, follow bool 263 var err error 264 265 q := req.URL.Query() 266 if allocID = strings.TrimPrefix(req.URL.Path, "/v1/client/fs/logs/"); allocID == "" { 267 return nil, allocIDNotPresentErr 268 } 269 270 if task = q.Get("task"); task == "" { 271 return nil, taskNotPresentErr 272 } 273 274 if followStr := q.Get("follow"); followStr != "" { 275 if follow, err = strconv.ParseBool(followStr); err != nil { 276 return nil, CodedError(400, fmt.Sprintf("failed to parse follow field to boolean: %v", err)) 277 } 278 } 279 280 if plainStr := q.Get("plain"); plainStr != "" { 281 if plain, err = strconv.ParseBool(plainStr); err != nil { 282 return nil, CodedError(400, fmt.Sprintf("failed to parse plain field to boolean: %v", err)) 283 } 284 } 285 286 logType = q.Get("type") 287 switch logType { 288 case "stdout", "stderr": 289 default: 290 return nil, logTypeNotPresentErr 291 } 292 293 var offset int64 294 offsetString := q.Get("offset") 295 if offsetString != "" { 296 var err error 297 if offset, err = strconv.ParseInt(offsetString, 10, 64); err != nil { 298 return nil, CodedError(400, fmt.Sprintf("error parsing offset: %v", err)) 299 } 300 } 301 302 origin := q.Get("origin") 303 switch origin { 304 case "start", "end": 305 case "": 306 origin = "start" 307 default: 308 return nil, invalidOrigin 309 } 310 311 // Create the request arguments 312 fsReq := &cstructs.FsLogsRequest{ 313 AllocID: allocID, 314 Task: task, 315 LogType: logType, 316 Offset: offset, 317 Origin: origin, 318 PlainText: plain, 319 Follow: follow, 320 } 321 s.parse(resp, req, &fsReq.QueryOptions.Region, &fsReq.QueryOptions) 322 323 // Make the request 324 return s.fsStreamImpl(resp, req, "FileSystem.Logs", fsReq, fsReq.AllocID) 325 } 326 327 // fsStreamImpl is used to make a streaming filesystem call that serializes the 328 // args and then expects a stream of StreamErrWrapper results where the payload 329 // is copied to the response body. 330 func (s *HTTPServer) fsStreamImpl(resp http.ResponseWriter, 331 req *http.Request, method string, args interface{}, allocID string) (interface{}, error) { 332 333 // Get the correct handler 334 localClient, remoteClient, localServer := s.rpcHandlerForAlloc(allocID) 335 var handler structs.StreamingRpcHandler 336 var handlerErr error 337 if localClient { 338 handler, handlerErr = s.agent.Client().StreamingRpcHandler(method) 339 } else if remoteClient { 340 handler, handlerErr = s.agent.Client().RemoteStreamingRpcHandler(method) 341 } else if localServer { 342 handler, handlerErr = s.agent.Server().StreamingRpcHandler(method) 343 } 344 345 if handlerErr != nil { 346 return nil, CodedError(500, handlerErr.Error()) 347 } 348 349 // Create a pipe connecting the (possibly remote) handler to the http response 350 httpPipe, handlerPipe := net.Pipe() 351 decoder := codec.NewDecoder(httpPipe, structs.MsgpackHandle) 352 encoder := codec.NewEncoder(httpPipe, structs.MsgpackHandle) 353 354 // Create a goroutine that closes the pipe if the connection closes. 355 ctx, cancel := context.WithCancel(req.Context()) 356 go func() { 357 <-ctx.Done() 358 httpPipe.Close() 359 }() 360 361 // Create an output that gets flushed on every write 362 output := ioutils.NewWriteFlusher(resp) 363 364 // Create a channel that decodes the results 365 errCh := make(chan HTTPCodedError) 366 go func() { 367 defer cancel() 368 369 // Send the request 370 if err := encoder.Encode(args); err != nil { 371 errCh <- CodedError(500, err.Error()) 372 return 373 } 374 375 for { 376 select { 377 case <-ctx.Done(): 378 errCh <- nil 379 return 380 default: 381 } 382 383 var res cstructs.StreamErrWrapper 384 if err := decoder.Decode(&res); err != nil { 385 errCh <- CodedError(500, err.Error()) 386 return 387 } 388 decoder.Reset(httpPipe) 389 390 if err := res.Error; err != nil { 391 code := 500 392 if err.Code != nil { 393 code = int(*err.Code) 394 } 395 396 errCh <- CodedError(code, err.Error()) 397 return 398 } 399 400 if _, err := io.Copy(output, bytes.NewReader(res.Payload)); err != nil { 401 errCh <- CodedError(500, err.Error()) 402 return 403 } 404 } 405 }() 406 407 handler(handlerPipe) 408 cancel() 409 codedErr := <-errCh 410 411 // Ignore EOF and ErrClosedPipe errors. 412 if codedErr != nil && 413 (codedErr == io.EOF || 414 strings.Contains(codedErr.Error(), "closed") || 415 strings.Contains(codedErr.Error(), "EOF")) { 416 codedErr = nil 417 } 418 return nil, codedErr 419 }