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