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