diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go
index d423c5b7e7f..3e0bbadb2b7 100644
--- a/pkg/apiserver/apiserver.go
+++ b/pkg/apiserver/apiserver.go
@@ -20,12 +20,14 @@ import (
"fmt"
"io/ioutil"
"net/http"
- "net/url"
+ "path"
"runtime/debug"
"strings"
"time"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
+ "github.com/GoogleCloudPlatform/kubernetes/pkg/healthz"
+ "github.com/GoogleCloudPlatform/kubernetes/pkg/httplog"
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
"github.com/GoogleCloudPlatform/kubernetes/pkg/tools"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
@@ -83,25 +85,37 @@ func MakeAsync(fn WorkFunc) <-chan interface{} {
//
// TODO: consider migrating this to go-restful which is a more full-featured version of the same thing.
type APIServer struct {
- prefix string
- storage map[string]RESTStorage
- ops *Operations
- logserver http.Handler
+ prefix string
+ storage map[string]RESTStorage
+ ops *Operations
+ mux *http.ServeMux
}
// New creates a new APIServer object.
// 'storage' contains a map of handlers.
// 'prefix' is the hosting path prefix.
func New(storage map[string]RESTStorage, prefix string) *APIServer {
- return &APIServer{
- storage: storage,
- prefix: prefix,
- ops: NewOperations(),
- logserver: http.StripPrefix("/logs/", http.FileServer(http.Dir("/var/log/"))),
+ s := &APIServer{
+ storage: storage,
+ prefix: strings.TrimRight(prefix, "/"),
+ ops: NewOperations(),
+ mux: http.NewServeMux(),
}
+
+ s.mux.Handle("/logs/", http.StripPrefix("/logs/", http.FileServer(http.Dir("/var/log/"))))
+ s.mux.HandleFunc(s.prefix+"/", s.ServeREST)
+ healthz.InstallHandler(s.mux)
+ s.mux.HandleFunc("/index.html", s.handleIndex)
+
+ // Handle both operations and operations/* with the same handler
+ opPrefix := path.Join(s.prefix, "operations")
+ s.mux.HandleFunc(opPrefix, s.handleOperationRequest)
+ s.mux.HandleFunc(opPrefix+"/", s.handleOperationRequest)
+
+ return s
}
-func (server *APIServer) handleIndex(w http.ResponseWriter) {
+func (server *APIServer) handleIndex(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(http.StatusOK)
// TODO: serve this out of a file?
data := "
Welcome to Kubernetes"
@@ -117,47 +131,37 @@ func (server *APIServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
glog.Infof("APIServer panic'd on %v %v: %#v\n%s\n", req.Method, req.RequestURI, x, debug.Stack())
}
}()
- defer MakeLogged(req, &w).StacktraceWhen(
- StatusIsNot(
+ defer httplog.MakeLogged(req, &w).StacktraceWhen(
+ httplog.StatusIsNot(
http.StatusOK,
http.StatusAccepted,
http.StatusConflict,
),
).Log()
- url, err := url.ParseRequestURI(req.RequestURI)
- if err != nil {
- server.error(err, w)
- return
- }
- if url.Path == "/index.html" || url.Path == "/" || url.Path == "" {
- server.handleIndex(w)
- return
- }
- if strings.HasPrefix(url.Path, "/logs/") {
- server.logserver.ServeHTTP(w, req)
- return
- }
- if !strings.HasPrefix(url.Path, server.prefix) {
+
+ // Dispatch via our mux.
+ server.mux.ServeHTTP(w, req)
+}
+
+// ServeREST handles requests to all our RESTStorage objects.
+func (server *APIServer) ServeREST(w http.ResponseWriter, req *http.Request) {
+ if !strings.HasPrefix(req.URL.Path, server.prefix) {
server.notFound(req, w)
return
}
- requestParts := strings.Split(url.Path[len(server.prefix):], "/")[1:]
+ requestParts := strings.Split(req.URL.Path[len(server.prefix):], "/")[1:]
if len(requestParts) < 1 {
server.notFound(req, w)
return
}
- if requestParts[0] == "operations" {
- server.handleOperationRequest(requestParts[1:], w, req)
- return
- }
storage := server.storage[requestParts[0]]
if storage == nil {
- LogOf(w).Addf("'%v' has no storage object", requestParts[0])
+ httplog.LogOf(w).Addf("'%v' has no storage object", requestParts[0])
server.notFound(req, w)
return
}
- server.handleREST(requestParts, url, req, w, storage)
+ server.handleREST(requestParts, req, w, storage)
}
func (server *APIServer) notFound(req *http.Request, w http.ResponseWriter) {
@@ -197,7 +201,7 @@ func (server *APIServer) finishReq(out <-chan interface{}, sync bool, timeout ti
status := http.StatusOK
switch stat := obj.(type) {
case api.Status:
- LogOf(w).Addf("programmer error: use *api.Status as a result, not api.Status.")
+ httplog.LogOf(w).Addf("programmer error: use *api.Status as a result, not api.Status.")
if stat.Code != 0 {
status = stat.Code
}
@@ -236,14 +240,14 @@ func parseTimeout(str string) time.Duration {
// sync=[false|true] Synchronous request (only applies to create, update, delete operations)
// timeout= Timeout for synchronous requests, only applies if sync=true
// labels= Used for filtering list operations
-func (server *APIServer) handleREST(parts []string, requestURL *url.URL, req *http.Request, w http.ResponseWriter, storage RESTStorage) {
- sync := requestURL.Query().Get("sync") == "true"
- timeout := parseTimeout(requestURL.Query().Get("timeout"))
+func (server *APIServer) handleREST(parts []string, req *http.Request, w http.ResponseWriter, storage RESTStorage) {
+ sync := req.URL.Query().Get("sync") == "true"
+ timeout := parseTimeout(req.URL.Query().Get("timeout"))
switch req.Method {
case "GET":
switch len(parts) {
case 1:
- selector, err := labels.ParseSelector(requestURL.Query().Get("labels"))
+ selector, err := labels.ParseSelector(req.URL.Query().Get("labels"))
if err != nil {
server.error(err, w)
return
@@ -325,7 +329,18 @@ func (server *APIServer) handleREST(parts []string, requestURL *url.URL, req *ht
}
}
-func (server *APIServer) handleOperationRequest(parts []string, w http.ResponseWriter, req *http.Request) {
+func (server *APIServer) handleOperationRequest(w http.ResponseWriter, req *http.Request) {
+ opPrefix := path.Join(server.prefix, "operations")
+ if !strings.HasPrefix(req.URL.Path, opPrefix) {
+ server.notFound(req, w)
+ return
+ }
+ trimmed := strings.TrimLeft(req.URL.Path[len(opPrefix):], "/")
+ parts := strings.Split(trimmed, "/")
+ if len(parts) > 1 {
+ server.notFound(req, w)
+ return
+ }
if req.Method != "GET" {
server.notFound(req, w)
}
diff --git a/pkg/apiserver/apiserver_test.go b/pkg/apiserver/apiserver_test.go
index f3371300c3a..f7d47ce1d4b 100644
--- a/pkg/apiserver/apiserver_test.go
+++ b/pkg/apiserver/apiserver_test.go
@@ -409,3 +409,37 @@ func TestSyncCreateTimeout(t *testing.T) {
t.Errorf("Unexpected status: %d, Expected: %d, %#v", response.StatusCode, 202, response)
}
}
+
+func TestOpGet(t *testing.T) {
+ simpleStorage := &SimpleRESTStorage{}
+ handler := New(map[string]RESTStorage{
+ "foo": simpleStorage,
+ }, "/prefix/version")
+ server := httptest.NewServer(handler)
+ client := http.Client{}
+
+ simple := Simple{Name: "foo"}
+ data, _ := api.Encode(simple)
+ request, err := http.NewRequest("POST", server.URL+"/prefix/version/foo", bytes.NewBuffer(data))
+ expectNoError(t, err)
+ response, err := client.Do(request)
+ expectNoError(t, err)
+ if response.StatusCode != http.StatusAccepted {
+ t.Errorf("Unexpected response %#v", response)
+ }
+
+ var itemOut api.Status
+ body, err := extractBody(response, &itemOut)
+ expectNoError(t, err)
+ if itemOut.Status != api.StatusWorking || itemOut.Details == "" {
+ t.Errorf("Unexpected status: %#v (%s)", itemOut, string(body))
+ }
+
+ req2, err := http.NewRequest("GET", server.URL+"/prefix/version/operations/"+itemOut.Details, nil)
+ expectNoError(t, err)
+ _, err = client.Do(req2)
+ expectNoError(t, err)
+ if response.StatusCode != http.StatusAccepted {
+ t.Errorf("Unexpected response %#v", response)
+ }
+}
diff --git a/pkg/healthz/healthz.go b/pkg/healthz/healthz.go
index 8ae6b97f73f..1f71bb88579 100644
--- a/pkg/healthz/healthz.go
+++ b/pkg/healthz/healthz.go
@@ -29,3 +29,7 @@ func handleHealthz(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
}
+
+func InstallHandler(mux *http.ServeMux) {
+ mux.HandleFunc("/healthz", handleHealthz)
+}
diff --git a/pkg/httplog/doc.go b/pkg/httplog/doc.go
new file mode 100644
index 00000000000..872879cadf3
--- /dev/null
+++ b/pkg/httplog/doc.go
@@ -0,0 +1,19 @@
+/*
+Copyright 2014 Google Inc. All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package httplog contains a helper object and functions to maintain a log
+// along with an http response.
+package httplog
diff --git a/pkg/apiserver/logger.go b/pkg/httplog/log.go
similarity index 85%
rename from pkg/apiserver/logger.go
rename to pkg/httplog/log.go
index 8690f80ae17..c65af57a2b6 100644
--- a/pkg/apiserver/logger.go
+++ b/pkg/httplog/log.go
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-package apiserver
+package httplog
import (
"fmt"
@@ -25,6 +25,18 @@ import (
"github.com/golang/glog"
)
+// Handler wraps all HTTP calls to delegate with nice logging.
+// delegate may use LogOf(w).Addf(...) to write additional info to
+// the per-request log message.
+//
+// Intended to wrap calls to your ServeMux.
+func Handler(delegate http.Handler, pred StacktracePred) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+ defer MakeLogged(req, &w).StacktraceWhen(pred).Log()
+ delegate.ServeHTTP(w, req)
+ })
+}
+
// StacktracePred returns true if a stacktrace should be logged for this status
type StacktracePred func(httpStatus int) (logStacktrace bool)
@@ -44,7 +56,7 @@ type respLogger struct {
// DefaultStacktracePred is the default implementation of StacktracePred.
func DefaultStacktracePred(status int) bool {
- return status != http.StatusOK && status != http.StatusAccepted
+ return status < http.StatusOK || status >= http.StatusBadRequest
}
// MakeLogged turns a normal response writer into a logged response writer.
@@ -60,6 +72,10 @@ func DefaultStacktracePred(status int) bool {
//
// Use LogOf(w).Addf(...) to log something along with the response result.
func MakeLogged(req *http.Request, w *http.ResponseWriter) *respLogger {
+ if _, ok := (*w).(*respLogger); ok {
+ // Don't double-wrap!
+ panic("multiple MakeLogged calls!")
+ }
rl := &respLogger{
startTime: time.Now(),
req: req,
diff --git a/pkg/kubelet/kubelet_server.go b/pkg/kubelet/kubelet_server.go
index 2bd33c5046c..e872a69b2a7 100644
--- a/pkg/kubelet/kubelet_server.go
+++ b/pkg/kubelet/kubelet_server.go
@@ -28,8 +28,8 @@ import (
"strings"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
- "github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver"
"github.com/google/cadvisor/info"
+ "github.com/GoogleCloudPlatform/kubernetes/pkg/httplog"
"gopkg.in/v1/yaml"
)
@@ -53,13 +53,14 @@ func (s *KubeletServer) error(w http.ResponseWriter, err error) {
}
func (s *KubeletServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
- defer apiserver.MakeLogged(req, &w).Log()
+ defer httplog.MakeLogged(req, &w).Log()
u, err := url.ParseRequestURI(req.RequestURI)
if err != nil {
s.error(w, err)
return
}
+ // TODO: use an http.ServeMux instead of a switch.
switch {
case u.Path == "/container" || u.Path == "/containers":
defer req.Body.Close()