github.com/karlem/nomad@v0.10.2-rc1/command/agent/http.go (about) 1 package agent 2 3 import ( 4 "bytes" 5 "crypto/tls" 6 "encoding/json" 7 "fmt" 8 "net" 9 "net/http" 10 "net/http/pprof" 11 "os" 12 "strconv" 13 "strings" 14 "time" 15 16 "github.com/NYTimes/gziphandler" 17 assetfs "github.com/elazarl/go-bindata-assetfs" 18 "github.com/gorilla/websocket" 19 log "github.com/hashicorp/go-hclog" 20 "github.com/hashicorp/nomad/helper/tlsutil" 21 "github.com/hashicorp/nomad/nomad/structs" 22 "github.com/rs/cors" 23 "github.com/ugorji/go/codec" 24 ) 25 26 const ( 27 // ErrInvalidMethod is used if the HTTP method is not supported 28 ErrInvalidMethod = "Invalid method" 29 30 // ErrEntOnly is the error returned if accessing an enterprise only 31 // endpoint 32 ErrEntOnly = "Nomad Enterprise only endpoint" 33 ) 34 35 var ( 36 // Set to false by stub_asset if the ui build tag isn't enabled 37 uiEnabled = true 38 39 // Overridden if the ui build tag isn't enabled 40 stubHTML = "" 41 42 // allowCORS sets permissive CORS headers for a handler 43 allowCORS = cors.New(cors.Options{ 44 AllowedOrigins: []string{"*"}, 45 AllowedMethods: []string{"HEAD", "GET"}, 46 AllowedHeaders: []string{"*"}, 47 AllowCredentials: true, 48 }) 49 ) 50 51 // HTTPServer is used to wrap an Agent and expose it over an HTTP interface 52 type HTTPServer struct { 53 agent *Agent 54 mux *http.ServeMux 55 listener net.Listener 56 listenerCh chan struct{} 57 logger log.Logger 58 Addr string 59 60 wsUpgrader *websocket.Upgrader 61 } 62 63 // NewHTTPServer starts new HTTP server over the agent 64 func NewHTTPServer(agent *Agent, config *Config) (*HTTPServer, error) { 65 // Start the listener 66 lnAddr, err := net.ResolveTCPAddr("tcp", config.normalizedAddrs.HTTP) 67 if err != nil { 68 return nil, err 69 } 70 ln, err := config.Listener("tcp", lnAddr.IP.String(), lnAddr.Port) 71 if err != nil { 72 return nil, fmt.Errorf("failed to start HTTP listener: %v", err) 73 } 74 75 // If TLS is enabled, wrap the listener with a TLS listener 76 if config.TLSConfig.EnableHTTP { 77 tlsConf, err := tlsutil.NewTLSConfiguration(config.TLSConfig, config.TLSConfig.VerifyHTTPSClient, true) 78 if err != nil { 79 return nil, err 80 } 81 82 tlsConfig, err := tlsConf.IncomingTLSConfig() 83 if err != nil { 84 return nil, err 85 } 86 ln = tls.NewListener(tcpKeepAliveListener{ln.(*net.TCPListener)}, tlsConfig) 87 } 88 89 // Create the mux 90 mux := http.NewServeMux() 91 92 wsUpgrader := &websocket.Upgrader{ 93 ReadBufferSize: 2048, 94 WriteBufferSize: 2048, 95 } 96 97 // Create the server 98 srv := &HTTPServer{ 99 agent: agent, 100 mux: mux, 101 listener: ln, 102 listenerCh: make(chan struct{}), 103 logger: agent.httpLogger, 104 Addr: ln.Addr().String(), 105 wsUpgrader: wsUpgrader, 106 } 107 srv.registerHandlers(config.EnableDebug) 108 109 // Handle requests with gzip compression 110 gzip, err := gziphandler.GzipHandlerWithOpts(gziphandler.MinSize(0)) 111 if err != nil { 112 return nil, err 113 } 114 115 go func() { 116 defer close(srv.listenerCh) 117 http.Serve(ln, gzip(mux)) 118 }() 119 120 return srv, nil 121 } 122 123 // tcpKeepAliveListener sets TCP keep-alive timeouts on accepted 124 // connections. It's used by NewHttpServer so 125 // dead TCP connections eventually go away. 126 type tcpKeepAliveListener struct { 127 *net.TCPListener 128 } 129 130 func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) { 131 tc, err := ln.AcceptTCP() 132 if err != nil { 133 return 134 } 135 tc.SetKeepAlive(true) 136 tc.SetKeepAlivePeriod(30 * time.Second) 137 return tc, nil 138 } 139 140 // Shutdown is used to shutdown the HTTP server 141 func (s *HTTPServer) Shutdown() { 142 if s != nil { 143 s.logger.Debug("shutting down http server") 144 s.listener.Close() 145 <-s.listenerCh // block until http.Serve has returned. 146 } 147 } 148 149 // registerHandlers is used to attach our handlers to the mux 150 func (s *HTTPServer) registerHandlers(enableDebug bool) { 151 s.mux.HandleFunc("/v1/jobs", s.wrap(s.JobsRequest)) 152 s.mux.HandleFunc("/v1/jobs/parse", s.wrap(s.JobsParseRequest)) 153 s.mux.HandleFunc("/v1/job/", s.wrap(s.JobSpecificRequest)) 154 155 s.mux.HandleFunc("/v1/nodes", s.wrap(s.NodesRequest)) 156 s.mux.HandleFunc("/v1/node/", s.wrap(s.NodeSpecificRequest)) 157 158 s.mux.HandleFunc("/v1/allocations", s.wrap(s.AllocsRequest)) 159 s.mux.HandleFunc("/v1/allocation/", s.wrap(s.AllocSpecificRequest)) 160 161 s.mux.HandleFunc("/v1/evaluations", s.wrap(s.EvalsRequest)) 162 s.mux.HandleFunc("/v1/evaluation/", s.wrap(s.EvalSpecificRequest)) 163 164 s.mux.HandleFunc("/v1/deployments", s.wrap(s.DeploymentsRequest)) 165 s.mux.HandleFunc("/v1/deployment/", s.wrap(s.DeploymentSpecificRequest)) 166 167 s.mux.HandleFunc("/v1/acl/policies", s.wrap(s.ACLPoliciesRequest)) 168 s.mux.HandleFunc("/v1/acl/policy/", s.wrap(s.ACLPolicySpecificRequest)) 169 170 s.mux.HandleFunc("/v1/acl/bootstrap", s.wrap(s.ACLTokenBootstrap)) 171 s.mux.HandleFunc("/v1/acl/tokens", s.wrap(s.ACLTokensRequest)) 172 s.mux.HandleFunc("/v1/acl/token", s.wrap(s.ACLTokenSpecificRequest)) 173 s.mux.HandleFunc("/v1/acl/token/", s.wrap(s.ACLTokenSpecificRequest)) 174 175 s.mux.Handle("/v1/client/fs/", wrapCORS(s.wrap(s.FsRequest))) 176 s.mux.HandleFunc("/v1/client/gc", s.wrap(s.ClientGCRequest)) 177 s.mux.Handle("/v1/client/stats", wrapCORS(s.wrap(s.ClientStatsRequest))) 178 s.mux.Handle("/v1/client/allocation/", wrapCORS(s.wrap(s.ClientAllocRequest))) 179 180 s.mux.HandleFunc("/v1/agent/self", s.wrap(s.AgentSelfRequest)) 181 s.mux.HandleFunc("/v1/agent/join", s.wrap(s.AgentJoinRequest)) 182 s.mux.HandleFunc("/v1/agent/members", s.wrap(s.AgentMembersRequest)) 183 s.mux.HandleFunc("/v1/agent/force-leave", s.wrap(s.AgentForceLeaveRequest)) 184 s.mux.HandleFunc("/v1/agent/servers", s.wrap(s.AgentServersRequest)) 185 s.mux.HandleFunc("/v1/agent/keyring/", s.wrap(s.KeyringOperationRequest)) 186 s.mux.HandleFunc("/v1/agent/health", s.wrap(s.HealthRequest)) 187 s.mux.HandleFunc("/v1/agent/monitor", s.wrap(s.AgentMonitor)) 188 189 s.mux.HandleFunc("/v1/metrics", s.wrap(s.MetricsRequest)) 190 191 s.mux.HandleFunc("/v1/validate/job", s.wrap(s.ValidateJobRequest)) 192 193 s.mux.HandleFunc("/v1/regions", s.wrap(s.RegionListRequest)) 194 195 s.mux.HandleFunc("/v1/status/leader", s.wrap(s.StatusLeaderRequest)) 196 s.mux.HandleFunc("/v1/status/peers", s.wrap(s.StatusPeersRequest)) 197 198 s.mux.HandleFunc("/v1/search", s.wrap(s.SearchRequest)) 199 200 s.mux.HandleFunc("/v1/operator/raft/", s.wrap(s.OperatorRequest)) 201 s.mux.HandleFunc("/v1/operator/autopilot/configuration", s.wrap(s.OperatorAutopilotConfiguration)) 202 s.mux.HandleFunc("/v1/operator/autopilot/health", s.wrap(s.OperatorServerHealth)) 203 204 s.mux.HandleFunc("/v1/system/gc", s.wrap(s.GarbageCollectRequest)) 205 s.mux.HandleFunc("/v1/system/reconcile/summaries", s.wrap(s.ReconcileJobSummaries)) 206 207 s.mux.HandleFunc("/v1/operator/scheduler/configuration", s.wrap(s.OperatorSchedulerConfiguration)) 208 209 if uiEnabled { 210 s.mux.Handle("/ui/", http.StripPrefix("/ui/", handleUI(http.FileServer(&UIAssetWrapper{FileSystem: assetFS()})))) 211 } else { 212 // Write the stubHTML 213 s.mux.HandleFunc("/ui/", func(w http.ResponseWriter, r *http.Request) { 214 w.Write([]byte(stubHTML)) 215 }) 216 } 217 s.mux.Handle("/", handleRootFallthrough()) 218 219 if enableDebug { 220 s.mux.HandleFunc("/debug/pprof/", pprof.Index) 221 s.mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) 222 s.mux.HandleFunc("/debug/pprof/profile", pprof.Profile) 223 s.mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) 224 s.mux.HandleFunc("/debug/pprof/trace", pprof.Trace) 225 } 226 227 // Register enterprise endpoints. 228 s.registerEnterpriseHandlers() 229 } 230 231 // HTTPCodedError is used to provide the HTTP error code 232 type HTTPCodedError interface { 233 error 234 Code() int 235 } 236 237 type UIAssetWrapper struct { 238 FileSystem *assetfs.AssetFS 239 } 240 241 func (fs *UIAssetWrapper) Open(name string) (http.File, error) { 242 if file, err := fs.FileSystem.Open(name); err == nil { 243 return file, nil 244 } else { 245 // serve index.html instead of 404ing 246 if err == os.ErrNotExist { 247 return fs.FileSystem.Open("index.html") 248 } 249 return nil, err 250 } 251 } 252 253 func CodedError(c int, s string) HTTPCodedError { 254 return &codedError{s, c} 255 } 256 257 type codedError struct { 258 s string 259 code int 260 } 261 262 func (e *codedError) Error() string { 263 return e.s 264 } 265 266 func (e *codedError) Code() int { 267 return e.code 268 } 269 270 func handleUI(h http.Handler) http.Handler { 271 return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 272 header := w.Header() 273 header.Add("Content-Security-Policy", "default-src 'none'; connect-src *; img-src 'self' data:; script-src 'self'; style-src 'self' 'unsafe-inline'; form-action 'none'; frame-ancestors 'none'") 274 h.ServeHTTP(w, req) 275 return 276 }) 277 } 278 279 func handleRootFallthrough() http.Handler { 280 return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 281 if req.URL.Path == "/" { 282 http.Redirect(w, req, "/ui/", 307) 283 } else { 284 w.WriteHeader(http.StatusNotFound) 285 } 286 }) 287 } 288 289 // wrap is used to wrap functions to make them more convenient 290 func (s *HTTPServer) wrap(handler func(resp http.ResponseWriter, req *http.Request) (interface{}, error)) func(resp http.ResponseWriter, req *http.Request) { 291 f := func(resp http.ResponseWriter, req *http.Request) { 292 setHeaders(resp, s.agent.config.HTTPAPIResponseHeaders) 293 // Invoke the handler 294 reqURL := req.URL.String() 295 start := time.Now() 296 defer func() { 297 s.logger.Debug("request complete", "method", req.Method, "path", reqURL, "duration", time.Now().Sub(start)) 298 }() 299 obj, err := handler(resp, req) 300 301 // Check for an error 302 HAS_ERR: 303 if err != nil { 304 code := 500 305 errMsg := err.Error() 306 if http, ok := err.(HTTPCodedError); ok { 307 code = http.Code() 308 } else if ecode, emsg, ok := structs.CodeFromRPCCodedErr(err); ok { 309 code = ecode 310 errMsg = emsg 311 } else { 312 // RPC errors get wrapped, so manually unwrap by only looking at their suffix 313 if strings.HasSuffix(errMsg, structs.ErrPermissionDenied.Error()) { 314 errMsg = structs.ErrPermissionDenied.Error() 315 code = 403 316 } else if strings.HasSuffix(errMsg, structs.ErrTokenNotFound.Error()) { 317 errMsg = structs.ErrTokenNotFound.Error() 318 code = 403 319 } 320 } 321 322 resp.WriteHeader(code) 323 resp.Write([]byte(errMsg)) 324 s.logger.Error("request failed", "method", req.Method, "path", reqURL, "error", err, "code", code) 325 return 326 } 327 328 prettyPrint := false 329 if v, ok := req.URL.Query()["pretty"]; ok { 330 if len(v) > 0 && (len(v[0]) == 0 || v[0] != "0") { 331 prettyPrint = true 332 } 333 } 334 335 // Write out the JSON object 336 if obj != nil { 337 var buf bytes.Buffer 338 if prettyPrint { 339 enc := codec.NewEncoder(&buf, structs.JsonHandlePretty) 340 err = enc.Encode(obj) 341 if err == nil { 342 buf.Write([]byte("\n")) 343 } 344 } else { 345 enc := codec.NewEncoder(&buf, structs.JsonHandle) 346 err = enc.Encode(obj) 347 } 348 if err != nil { 349 goto HAS_ERR 350 } 351 resp.Header().Set("Content-Type", "application/json") 352 resp.Write(buf.Bytes()) 353 } 354 } 355 return f 356 } 357 358 // decodeBody is used to decode a JSON request body 359 func decodeBody(req *http.Request, out interface{}) error { 360 dec := json.NewDecoder(req.Body) 361 return dec.Decode(&out) 362 } 363 364 // setIndex is used to set the index response header 365 func setIndex(resp http.ResponseWriter, index uint64) { 366 resp.Header().Set("X-Nomad-Index", strconv.FormatUint(index, 10)) 367 } 368 369 // setKnownLeader is used to set the known leader header 370 func setKnownLeader(resp http.ResponseWriter, known bool) { 371 s := "true" 372 if !known { 373 s = "false" 374 } 375 resp.Header().Set("X-Nomad-KnownLeader", s) 376 } 377 378 // setLastContact is used to set the last contact header 379 func setLastContact(resp http.ResponseWriter, last time.Duration) { 380 lastMsec := uint64(last / time.Millisecond) 381 resp.Header().Set("X-Nomad-LastContact", strconv.FormatUint(lastMsec, 10)) 382 } 383 384 // setMeta is used to set the query response meta data 385 func setMeta(resp http.ResponseWriter, m *structs.QueryMeta) { 386 setIndex(resp, m.Index) 387 setLastContact(resp, m.LastContact) 388 setKnownLeader(resp, m.KnownLeader) 389 } 390 391 // setHeaders is used to set canonical response header fields 392 func setHeaders(resp http.ResponseWriter, headers map[string]string) { 393 for field, value := range headers { 394 resp.Header().Set(http.CanonicalHeaderKey(field), value) 395 } 396 } 397 398 // parseWait is used to parse the ?wait and ?index query params 399 // Returns true on error 400 func parseWait(resp http.ResponseWriter, req *http.Request, b *structs.QueryOptions) bool { 401 query := req.URL.Query() 402 if wait := query.Get("wait"); wait != "" { 403 dur, err := time.ParseDuration(wait) 404 if err != nil { 405 resp.WriteHeader(400) 406 resp.Write([]byte("Invalid wait time")) 407 return true 408 } 409 b.MaxQueryTime = dur 410 } 411 if idx := query.Get("index"); idx != "" { 412 index, err := strconv.ParseUint(idx, 10, 64) 413 if err != nil { 414 resp.WriteHeader(400) 415 resp.Write([]byte("Invalid index")) 416 return true 417 } 418 b.MinQueryIndex = index 419 } 420 return false 421 } 422 423 // parseConsistency is used to parse the ?stale query params. 424 func parseConsistency(req *http.Request, b *structs.QueryOptions) { 425 query := req.URL.Query() 426 if _, ok := query["stale"]; ok { 427 b.AllowStale = true 428 } 429 } 430 431 // parsePrefix is used to parse the ?prefix query param 432 func parsePrefix(req *http.Request, b *structs.QueryOptions) { 433 query := req.URL.Query() 434 if prefix := query.Get("prefix"); prefix != "" { 435 b.Prefix = prefix 436 } 437 } 438 439 // parseRegion is used to parse the ?region query param 440 func (s *HTTPServer) parseRegion(req *http.Request, r *string) { 441 if other := req.URL.Query().Get("region"); other != "" { 442 *r = other 443 } else if *r == "" { 444 *r = s.agent.config.Region 445 } 446 } 447 448 // parseNamespace is used to parse the ?namespace parameter 449 func parseNamespace(req *http.Request, n *string) { 450 if other := req.URL.Query().Get("namespace"); other != "" { 451 *n = other 452 } else if *n == "" { 453 *n = structs.DefaultNamespace 454 } 455 } 456 457 // parseToken is used to parse the X-Nomad-Token param 458 func (s *HTTPServer) parseToken(req *http.Request, token *string) { 459 if other := req.Header.Get("X-Nomad-Token"); other != "" { 460 *token = other 461 return 462 } 463 } 464 465 // parse is a convenience method for endpoints that need to parse multiple flags 466 func (s *HTTPServer) parse(resp http.ResponseWriter, req *http.Request, r *string, b *structs.QueryOptions) bool { 467 s.parseRegion(req, r) 468 s.parseToken(req, &b.AuthToken) 469 parseConsistency(req, b) 470 parsePrefix(req, b) 471 parseNamespace(req, &b.Namespace) 472 return parseWait(resp, req, b) 473 } 474 475 // parseWriteRequest is a convenience method for endpoints that need to parse a 476 // write request. 477 func (s *HTTPServer) parseWriteRequest(req *http.Request, w *structs.WriteRequest) { 478 parseNamespace(req, &w.Namespace) 479 s.parseToken(req, &w.AuthToken) 480 s.parseRegion(req, &w.Region) 481 } 482 483 // wrapCORS wraps a HandlerFunc in allowCORS and returns a http.Handler 484 func wrapCORS(f func(http.ResponseWriter, *http.Request)) http.Handler { 485 return allowCORS.Handler(http.HandlerFunc(f)) 486 }