diff --git a/cmd/apiserver/apiserver.go b/cmd/apiserver/apiserver.go index 0aa28abe70..137a3f1598 100644 --- a/cmd/apiserver/apiserver.go +++ b/cmd/apiserver/apiserver.go @@ -49,6 +49,7 @@ import ( var ( port = flag.Uint("port", 8080, "The port to listen on. Default 8080") address = util.IP(net.ParseIP("127.0.0.1")) + readOnlyPort = flag.Uint("read_only_port", 7080, "The port from which to serve read-only resources. If 0, don't serve on a read-only address.") apiPrefix = flag.String("api_prefix", "/api", "The prefix for API requests on the server. Default '/api'.") storageVersion = flag.String("storage_version", "", "The version to store resources with. Defaults to server preferred") cloudProvider = flag.String("cloud_provider", "", "The provider for cloud services. Empty string for no provider.") @@ -230,11 +231,25 @@ func main() { handler = handlers.NewRequestAuthenticator(userContexts, bearertoken.New(auth), handlers.Unauthorized, handler) } - handler = apiserver.RecoverPanics(handler) + if *readOnlyPort != 0 { + // Allow 1 read-only request per second, allow up to 20 in a burst before enforcing. + rl := util.NewTokenBucketRateLimiter(1.0, 20) + readOnlyServer := &http.Server{ + Addr: net.JoinHostPort(address.String(), strconv.Itoa(int(*readOnlyPort))), + Handler: apiserver.RecoverPanics(apiserver.ReadOnly(apiserver.RateLimit(rl, handler))), + ReadTimeout: 5 * time.Minute, + WriteTimeout: 5 * time.Minute, + MaxHeaderBytes: 1 << 20, + } + go func() { + defer util.HandleCrash() + glog.Fatal(readOnlyServer.ListenAndServe()) + }() + } s := &http.Server{ Addr: net.JoinHostPort(address.String(), strconv.Itoa(int(*port))), - Handler: handler, + Handler: apiserver.RecoverPanics(handler), ReadTimeout: 5 * time.Minute, WriteTimeout: 5 * time.Minute, MaxHeaderBytes: 1 << 20, diff --git a/pkg/apiserver/apiserver_test.go b/pkg/apiserver/apiserver_test.go index 6141fc4fa5..bd8a9aecae 100644 --- a/pkg/apiserver/apiserver_test.go +++ b/pkg/apiserver/apiserver_test.go @@ -200,7 +200,7 @@ func TestNotFound(t *testing.T) { for k, v := range cases { request, err := http.NewRequest(v.Method, server.URL+v.Path, nil) if err != nil { - t.Errorf("unexpected error: %v", err) + t.Fatalf("unexpected error: %v", err) } response, err := client.Do(request) diff --git a/pkg/apiserver/handlers.go b/pkg/apiserver/handlers.go index 0bf403f4f9..9d5d0b94ab 100644 --- a/pkg/apiserver/handlers.go +++ b/pkg/apiserver/handlers.go @@ -24,9 +24,34 @@ import ( "strings" "github.com/GoogleCloudPlatform/kubernetes/pkg/httplog" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/golang/glog" ) +// ReadOnly passes all GET requests on to handler, and returns an error on all other requests. +func ReadOnly(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + if req.Method == "GET" { + handler.ServeHTTP(w, req) + return + } + w.WriteHeader(http.StatusForbidden) + fmt.Fprintf(w, "This is a read-only endpoint.") + }) +} + +// RateLimit uses rl to rate limit accepting requests to 'handler'. +func RateLimit(rl util.RateLimiter, handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + if rl.CanAccept() { + handler.ServeHTTP(w, req) + return + } + w.WriteHeader(http.StatusServiceUnavailable) + fmt.Fprintf(w, "Rate limit exceeded.") + }) +} + // RecoverPanics wraps an http Handler to recover and log panics. func RecoverPanics(handler http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { diff --git a/pkg/apiserver/handlers_test.go b/pkg/apiserver/handlers_test.go new file mode 100644 index 0000000000..bf41e6a205 --- /dev/null +++ b/pkg/apiserver/handlers_test.go @@ -0,0 +1,59 @@ +/* +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 apiserver + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +type fakeRL bool + +func (fakeRL) Stop() {} +func (f fakeRL) CanAccept() bool { return bool(f) } + +func TestRateLimit(t *testing.T) { + for _, allow := range []bool{true, false} { + rl := fakeRL(allow) + server := httptest.NewServer(RateLimit(rl, http.HandlerFunc( + func(w http.ResponseWriter, req *http.Request) { + if !allow { + t.Errorf("Unexpected call") + } + }, + ))) + http.Get(server.URL) + } +} + +func TestReadOnly(t *testing.T) { + server := httptest.NewServer(ReadOnly(http.HandlerFunc( + func(w http.ResponseWriter, req *http.Request) { + if req.Method != "GET" { + t.Errorf("Unexpected call: %v", req.Method) + } + }, + ))) + for _, verb := range []string{"GET", "POST", "PUT", "DELETE", "CREATE"} { + req, err := http.NewRequest(verb, server.URL, nil) + if err != nil { + t.Fatalf("Couldn't make request: %v", err) + } + http.DefaultClient.Do(req) + } +}