github.com/quite/nomad@v0.8.6/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 = fmt.Errorf("must provide a valid alloc id") 21 fileNameNotPresentErr = fmt.Errorf("must provide a file name") 22 taskNotPresentErr = fmt.Errorf("must provide task name") 23 logTypeNotPresentErr = fmt.Errorf("must provide log type (stdout/stderr)") 24 clientNotRunning = fmt.Errorf("node is not running a Nomad Client") 25 invalidOrigin = fmt.Errorf("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 // * offset: The offset to start streaming data at, defaults to zero. 198 // * origin: Either "start" or "end" and defines from where the offset is 199 // applied. Defaults to "start". 200 func (s *HTTPServer) Stream(resp http.ResponseWriter, req *http.Request) (interface{}, error) { 201 var allocID, path string 202 203 q := req.URL.Query() 204 205 if allocID = strings.TrimPrefix(req.URL.Path, "/v1/client/fs/stream/"); allocID == "" { 206 return nil, allocIDNotPresentErr 207 } 208 209 if path = q.Get("path"); path == "" { 210 return nil, fileNameNotPresentErr 211 } 212 213 var offset int64 214 offsetString := q.Get("offset") 215 if offsetString != "" { 216 var err error 217 if offset, err = strconv.ParseInt(offsetString, 10, 64); err != nil { 218 return nil, fmt.Errorf("error parsing offset: %v", err) 219 } 220 } 221 222 origin := q.Get("origin") 223 switch origin { 224 case "start", "end": 225 case "": 226 origin = "start" 227 default: 228 return nil, invalidOrigin 229 } 230 231 // Create the request arguments 232 fsReq := &cstructs.FsStreamRequest{ 233 AllocID: allocID, 234 Path: path, 235 Origin: origin, 236 Offset: offset, 237 Follow: true, 238 } 239 s.parse(resp, req, &fsReq.QueryOptions.Region, &fsReq.QueryOptions) 240 241 // Make the request 242 return s.fsStreamImpl(resp, req, "FileSystem.Stream", fsReq, fsReq.AllocID) 243 } 244 245 // Logs streams the content of a log blocking on EOF. The parameters are: 246 // * task: task name to stream logs for. 247 // * type: stdout/stderr to stream. 248 // * follow: A boolean of whether to follow the logs. 249 // * offset: The offset to start streaming data at, defaults to zero. 250 // * origin: Either "start" or "end" and defines from where the offset is 251 // applied. Defaults to "start". 252 func (s *HTTPServer) Logs(resp http.ResponseWriter, req *http.Request) (interface{}, error) { 253 var allocID, task, logType string 254 var plain, follow bool 255 var err error 256 257 q := req.URL.Query() 258 if allocID = strings.TrimPrefix(req.URL.Path, "/v1/client/fs/logs/"); allocID == "" { 259 return nil, allocIDNotPresentErr 260 } 261 262 if task = q.Get("task"); task == "" { 263 return nil, taskNotPresentErr 264 } 265 266 if followStr := q.Get("follow"); followStr != "" { 267 if follow, err = strconv.ParseBool(followStr); err != nil { 268 return nil, fmt.Errorf("Failed to parse follow field to boolean: %v", err) 269 } 270 } 271 272 if plainStr := q.Get("plain"); plainStr != "" { 273 if plain, err = strconv.ParseBool(plainStr); err != nil { 274 return nil, fmt.Errorf("Failed to parse plain field to boolean: %v", err) 275 } 276 } 277 278 logType = q.Get("type") 279 switch logType { 280 case "stdout", "stderr": 281 default: 282 return nil, logTypeNotPresentErr 283 } 284 285 var offset int64 286 offsetString := q.Get("offset") 287 if offsetString != "" { 288 var err error 289 if offset, err = strconv.ParseInt(offsetString, 10, 64); err != nil { 290 return nil, fmt.Errorf("error parsing offset: %v", err) 291 } 292 } 293 294 origin := q.Get("origin") 295 switch origin { 296 case "start", "end": 297 case "": 298 origin = "start" 299 default: 300 return nil, invalidOrigin 301 } 302 303 // Create the request arguments 304 fsReq := &cstructs.FsLogsRequest{ 305 AllocID: allocID, 306 Task: task, 307 LogType: logType, 308 Offset: offset, 309 Origin: origin, 310 PlainText: plain, 311 Follow: follow, 312 } 313 s.parse(resp, req, &fsReq.QueryOptions.Region, &fsReq.QueryOptions) 314 315 // Make the request 316 return s.fsStreamImpl(resp, req, "FileSystem.Logs", fsReq, fsReq.AllocID) 317 } 318 319 // fsStreamImpl is used to make a streaming filesystem call that serializes the 320 // args and then expects a stream of StreamErrWrapper results where the payload 321 // is copied to the response body. 322 func (s *HTTPServer) fsStreamImpl(resp http.ResponseWriter, 323 req *http.Request, method string, args interface{}, allocID string) (interface{}, error) { 324 325 // Get the correct handler 326 localClient, remoteClient, localServer := s.rpcHandlerForAlloc(allocID) 327 var handler structs.StreamingRpcHandler 328 var handlerErr error 329 if localClient { 330 handler, handlerErr = s.agent.Client().StreamingRpcHandler(method) 331 } else if remoteClient { 332 handler, handlerErr = s.agent.Client().RemoteStreamingRpcHandler(method) 333 } else if localServer { 334 handler, handlerErr = s.agent.Server().StreamingRpcHandler(method) 335 } 336 337 if handlerErr != nil { 338 return nil, CodedError(500, handlerErr.Error()) 339 } 340 341 // Create a pipe connecting the (possibly remote) handler to the http response 342 httpPipe, handlerPipe := net.Pipe() 343 decoder := codec.NewDecoder(httpPipe, structs.MsgpackHandle) 344 encoder := codec.NewEncoder(httpPipe, structs.MsgpackHandle) 345 346 // Create a goroutine that closes the pipe if the connection closes. 347 ctx, cancel := context.WithCancel(req.Context()) 348 go func() { 349 <-ctx.Done() 350 httpPipe.Close() 351 }() 352 353 // Create an output that gets flushed on every write 354 output := ioutils.NewWriteFlusher(resp) 355 356 // Create a channel that decodes the results 357 errCh := make(chan HTTPCodedError) 358 go func() { 359 defer cancel() 360 361 // Send the request 362 if err := encoder.Encode(args); err != nil { 363 errCh <- CodedError(500, err.Error()) 364 return 365 } 366 367 for { 368 select { 369 case <-ctx.Done(): 370 errCh <- nil 371 return 372 default: 373 } 374 375 var res cstructs.StreamErrWrapper 376 if err := decoder.Decode(&res); err != nil { 377 errCh <- CodedError(500, err.Error()) 378 return 379 } 380 decoder.Reset(httpPipe) 381 382 if err := res.Error; err != nil { 383 if err.Code != nil { 384 errCh <- CodedError(int(*err.Code), err.Error()) 385 return 386 } 387 } 388 389 if _, err := io.Copy(output, bytes.NewReader(res.Payload)); err != nil { 390 errCh <- CodedError(500, err.Error()) 391 return 392 } 393 } 394 }() 395 396 handler(handlerPipe) 397 cancel() 398 codedErr := <-errCh 399 400 // Ignore EOF and ErrClosedPipe errors. 401 if codedErr != nil && 402 (codedErr == io.EOF || 403 strings.Contains(codedErr.Error(), "closed") || 404 strings.Contains(codedErr.Error(), "EOF")) { 405 codedErr = nil 406 } 407 return nil, codedErr 408 }