github.com/mongey/nomad@v0.5.2/command/agent/http.go (about) 1 package agent 2 3 import ( 4 "bytes" 5 "crypto/tls" 6 "encoding/json" 7 "fmt" 8 "io" 9 "log" 10 "net" 11 "net/http" 12 "net/http/pprof" 13 "strconv" 14 "time" 15 16 "github.com/NYTimes/gziphandler" 17 "github.com/hashicorp/nomad/helper/tlsutil" 18 "github.com/hashicorp/nomad/nomad/structs" 19 "github.com/ugorji/go/codec" 20 ) 21 22 const ( 23 // ErrInvalidMethod is used if the HTTP method is not supported 24 ErrInvalidMethod = "Invalid method" 25 26 // scadaHTTPAddr is the address associated with the 27 // HTTPServer. When populating an ACL token for a request, 28 // this is checked to switch between the ACLToken and 29 // AtlasACLToken 30 scadaHTTPAddr = "SCADA" 31 ) 32 33 var ( 34 // jsonHandle and jsonHandlePretty are the codec handles to JSON encode 35 // structs. The pretty handle will add indents for easier human consumption. 36 jsonHandle = &codec.JsonHandle{} 37 jsonHandlePretty = &codec.JsonHandle{Indent: 4} 38 ) 39 40 // HTTPServer is used to wrap an Agent and expose it over an HTTP interface 41 type HTTPServer struct { 42 agent *Agent 43 mux *http.ServeMux 44 listener net.Listener 45 logger *log.Logger 46 addr string 47 } 48 49 // NewHTTPServer starts new HTTP server over the agent 50 func NewHTTPServer(agent *Agent, config *Config, logOutput io.Writer) (*HTTPServer, error) { 51 // Start the listener 52 lnAddr, err := net.ResolveTCPAddr("tcp", config.normalizedAddrs.HTTP) 53 if err != nil { 54 return nil, err 55 } 56 ln, err := config.Listener("tcp", lnAddr.IP.String(), lnAddr.Port) 57 if err != nil { 58 return nil, fmt.Errorf("failed to start HTTP listener: %v", err) 59 } 60 61 // If TLS is enabled, wrap the listener with a TLS listener 62 if config.TLSConfig.EnableHTTP { 63 tlsConf := &tlsutil.Config{ 64 VerifyIncoming: false, 65 VerifyOutgoing: true, 66 VerifyServerHostname: config.TLSConfig.VerifyServerHostname, 67 CAFile: config.TLSConfig.CAFile, 68 CertFile: config.TLSConfig.CertFile, 69 KeyFile: config.TLSConfig.KeyFile, 70 } 71 tlsConfig, err := tlsConf.IncomingTLSConfig() 72 if err != nil { 73 return nil, err 74 } 75 ln = tls.NewListener(tcpKeepAliveListener{ln.(*net.TCPListener)}, tlsConfig) 76 } 77 78 // Create the mux 79 mux := http.NewServeMux() 80 81 // Create the server 82 srv := &HTTPServer{ 83 agent: agent, 84 mux: mux, 85 listener: ln, 86 logger: agent.logger, 87 addr: ln.Addr().String(), 88 } 89 srv.registerHandlers(config.EnableDebug) 90 91 // Start the server 92 go http.Serve(ln, gziphandler.GzipHandler(mux)) 93 return srv, nil 94 } 95 96 // newScadaHttp creates a new HTTP server wrapping the SCADA 97 // listener such that HTTP calls can be sent from the brokers. 98 func newScadaHttp(agent *Agent, list net.Listener) *HTTPServer { 99 // Create the mux 100 mux := http.NewServeMux() 101 102 // Create the server 103 srv := &HTTPServer{ 104 agent: agent, 105 mux: mux, 106 listener: list, 107 logger: agent.logger, 108 addr: scadaHTTPAddr, 109 } 110 srv.registerHandlers(false) // Never allow debug for SCADA 111 112 // Start the server 113 go http.Serve(list, gziphandler.GzipHandler(mux)) 114 return srv 115 } 116 117 // tcpKeepAliveListener sets TCP keep-alive timeouts on accepted 118 // connections. It's used by NewHttpServer so 119 // dead TCP connections eventually go away. 120 type tcpKeepAliveListener struct { 121 *net.TCPListener 122 } 123 124 func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) { 125 tc, err := ln.AcceptTCP() 126 if err != nil { 127 return 128 } 129 tc.SetKeepAlive(true) 130 tc.SetKeepAlivePeriod(30 * time.Second) 131 return tc, nil 132 } 133 134 // Shutdown is used to shutdown the HTTP server 135 func (s *HTTPServer) Shutdown() { 136 if s != nil { 137 s.logger.Printf("[DEBUG] http: Shutting down http server") 138 s.listener.Close() 139 } 140 } 141 142 // registerHandlers is used to attach our handlers to the mux 143 func (s *HTTPServer) registerHandlers(enableDebug bool) { 144 s.mux.HandleFunc("/v1/jobs", s.wrap(s.JobsRequest)) 145 s.mux.HandleFunc("/v1/job/", s.wrap(s.JobSpecificRequest)) 146 147 s.mux.HandleFunc("/v1/nodes", s.wrap(s.NodesRequest)) 148 s.mux.HandleFunc("/v1/node/", s.wrap(s.NodeSpecificRequest)) 149 150 s.mux.HandleFunc("/v1/allocations", s.wrap(s.AllocsRequest)) 151 s.mux.HandleFunc("/v1/allocation/", s.wrap(s.AllocSpecificRequest)) 152 153 s.mux.HandleFunc("/v1/evaluations", s.wrap(s.EvalsRequest)) 154 s.mux.HandleFunc("/v1/evaluation/", s.wrap(s.EvalSpecificRequest)) 155 156 s.mux.HandleFunc("/v1/client/fs/", s.wrap(s.FsRequest)) 157 s.mux.HandleFunc("/v1/client/stats", s.wrap(s.ClientStatsRequest)) 158 s.mux.HandleFunc("/v1/client/allocation/", s.wrap(s.ClientAllocRequest)) 159 s.mux.HandleFunc("/v1/client/gc", s.wrap(s.ClientGCRequest)) 160 161 s.mux.HandleFunc("/v1/agent/self", s.wrap(s.AgentSelfRequest)) 162 s.mux.HandleFunc("/v1/agent/join", s.wrap(s.AgentJoinRequest)) 163 s.mux.HandleFunc("/v1/agent/members", s.wrap(s.AgentMembersRequest)) 164 s.mux.HandleFunc("/v1/agent/force-leave", s.wrap(s.AgentForceLeaveRequest)) 165 s.mux.HandleFunc("/v1/agent/servers", s.wrap(s.AgentServersRequest)) 166 s.mux.HandleFunc("/v1/agent/keyring/", s.wrap(s.KeyringOperationRequest)) 167 168 s.mux.HandleFunc("/v1/regions", s.wrap(s.RegionListRequest)) 169 170 s.mux.HandleFunc("/v1/status/leader", s.wrap(s.StatusLeaderRequest)) 171 s.mux.HandleFunc("/v1/status/peers", s.wrap(s.StatusPeersRequest)) 172 173 s.mux.HandleFunc("/v1/system/gc", s.wrap(s.GarbageCollectRequest)) 174 s.mux.HandleFunc("/v1/system/reconcile/summaries", s.wrap(s.ReconcileJobSummaries)) 175 176 if enableDebug { 177 s.mux.HandleFunc("/debug/pprof/", pprof.Index) 178 s.mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) 179 s.mux.HandleFunc("/debug/pprof/profile", pprof.Profile) 180 s.mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) 181 } 182 } 183 184 // HTTPCodedError is used to provide the HTTP error code 185 type HTTPCodedError interface { 186 error 187 Code() int 188 } 189 190 func CodedError(c int, s string) HTTPCodedError { 191 return &codedError{s, c} 192 } 193 194 type codedError struct { 195 s string 196 code int 197 } 198 199 func (e *codedError) Error() string { 200 return e.s 201 } 202 203 func (e *codedError) Code() int { 204 return e.code 205 } 206 207 // wrap is used to wrap functions to make them more convenient 208 func (s *HTTPServer) wrap(handler func(resp http.ResponseWriter, req *http.Request) (interface{}, error)) func(resp http.ResponseWriter, req *http.Request) { 209 f := func(resp http.ResponseWriter, req *http.Request) { 210 setHeaders(resp, s.agent.config.HTTPAPIResponseHeaders) 211 // Invoke the handler 212 reqURL := req.URL.String() 213 start := time.Now() 214 defer func() { 215 s.logger.Printf("[DEBUG] http: Request %v (%v)", reqURL, time.Now().Sub(start)) 216 }() 217 obj, err := handler(resp, req) 218 219 // Check for an error 220 HAS_ERR: 221 if err != nil { 222 s.logger.Printf("[ERR] http: Request %v, error: %v", reqURL, err) 223 code := 500 224 if http, ok := err.(HTTPCodedError); ok { 225 code = http.Code() 226 } 227 resp.WriteHeader(code) 228 resp.Write([]byte(err.Error())) 229 return 230 } 231 232 prettyPrint := false 233 if v, ok := req.URL.Query()["pretty"]; ok { 234 if len(v) > 0 && (len(v[0]) == 0 || v[0] != "0") { 235 prettyPrint = true 236 } 237 } 238 239 // Write out the JSON object 240 if obj != nil { 241 var buf bytes.Buffer 242 if prettyPrint { 243 enc := codec.NewEncoder(&buf, jsonHandlePretty) 244 err = enc.Encode(obj) 245 if err == nil { 246 buf.Write([]byte("\n")) 247 } 248 } else { 249 enc := codec.NewEncoder(&buf, jsonHandle) 250 err = enc.Encode(obj) 251 } 252 if err != nil { 253 goto HAS_ERR 254 } 255 resp.Header().Set("Content-Type", "application/json") 256 resp.Write(buf.Bytes()) 257 } 258 } 259 return f 260 } 261 262 // decodeBody is used to decode a JSON request body 263 func decodeBody(req *http.Request, out interface{}) error { 264 dec := json.NewDecoder(req.Body) 265 return dec.Decode(&out) 266 } 267 268 // setIndex is used to set the index response header 269 func setIndex(resp http.ResponseWriter, index uint64) { 270 resp.Header().Set("X-Nomad-Index", strconv.FormatUint(index, 10)) 271 } 272 273 // setKnownLeader is used to set the known leader header 274 func setKnownLeader(resp http.ResponseWriter, known bool) { 275 s := "true" 276 if !known { 277 s = "false" 278 } 279 resp.Header().Set("X-Nomad-KnownLeader", s) 280 } 281 282 // setLastContact is used to set the last contact header 283 func setLastContact(resp http.ResponseWriter, last time.Duration) { 284 lastMsec := uint64(last / time.Millisecond) 285 resp.Header().Set("X-Nomad-LastContact", strconv.FormatUint(lastMsec, 10)) 286 } 287 288 // setMeta is used to set the query response meta data 289 func setMeta(resp http.ResponseWriter, m *structs.QueryMeta) { 290 setIndex(resp, m.Index) 291 setLastContact(resp, m.LastContact) 292 setKnownLeader(resp, m.KnownLeader) 293 } 294 295 // setHeaders is used to set canonical response header fields 296 func setHeaders(resp http.ResponseWriter, headers map[string]string) { 297 for field, value := range headers { 298 resp.Header().Set(http.CanonicalHeaderKey(field), value) 299 } 300 } 301 302 // parseWait is used to parse the ?wait and ?index query params 303 // Returns true on error 304 func parseWait(resp http.ResponseWriter, req *http.Request, b *structs.QueryOptions) bool { 305 query := req.URL.Query() 306 if wait := query.Get("wait"); wait != "" { 307 dur, err := time.ParseDuration(wait) 308 if err != nil { 309 resp.WriteHeader(400) 310 resp.Write([]byte("Invalid wait time")) 311 return true 312 } 313 b.MaxQueryTime = dur 314 } 315 if idx := query.Get("index"); idx != "" { 316 index, err := strconv.ParseUint(idx, 10, 64) 317 if err != nil { 318 resp.WriteHeader(400) 319 resp.Write([]byte("Invalid index")) 320 return true 321 } 322 b.MinQueryIndex = index 323 } 324 return false 325 } 326 327 // parseConsistency is used to parse the ?stale query params. 328 func parseConsistency(req *http.Request, b *structs.QueryOptions) { 329 query := req.URL.Query() 330 if _, ok := query["stale"]; ok { 331 b.AllowStale = true 332 } 333 } 334 335 // parsePrefix is used to parse the ?prefix query param 336 func parsePrefix(req *http.Request, b *structs.QueryOptions) { 337 query := req.URL.Query() 338 if prefix := query.Get("prefix"); prefix != "" { 339 b.Prefix = prefix 340 } 341 } 342 343 // parseRegion is used to parse the ?region query param 344 func (s *HTTPServer) parseRegion(req *http.Request, r *string) { 345 if other := req.URL.Query().Get("region"); other != "" { 346 *r = other 347 } else if *r == "" { 348 *r = s.agent.config.Region 349 } 350 } 351 352 // parse is a convenience method for endpoints that need to parse multiple flags 353 func (s *HTTPServer) parse(resp http.ResponseWriter, req *http.Request, r *string, b *structs.QueryOptions) bool { 354 s.parseRegion(req, r) 355 parseConsistency(req, b) 356 parsePrefix(req, b) 357 return parseWait(resp, req, b) 358 }