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