Commit 7583e1a6 authored by Brian Grant's avatar Brian Grant

Automatic API generation by adopting go-restful

parent dd29cd83
......@@ -105,7 +105,7 @@ func NewInvalid(kind, name string, errs ValidationErrorList) error {
}
return &statusError{api.Status{
Status: api.StatusFailure,
Code: 422, // RFC 4918
Code: 422, // RFC 4918: StatusUnprocessableEntity
Reason: api.StatusReasonInvalid,
Details: &api.StatusDetails{
Kind: kind,
......@@ -121,7 +121,7 @@ func NewBadRequest(reason string) error {
return &statusError{
api.Status{
Status: api.StatusFailure,
Code: 400,
Code: http.StatusBadRequest,
Reason: api.StatusReasonBadRequest,
Details: &api.StatusDetails{
Causes: []api.StatusCause{
......@@ -136,7 +136,7 @@ func NewBadRequest(reason string) error {
func NewInternalError(err error) error {
return &statusError{api.Status{
Status: api.StatusFailure,
Code: 500,
Code: http.StatusInternalServerError,
Reason: api.StatusReasonInternalError,
Details: &api.StatusDetails{
Causes: []api.StatusCause{{Message: err.Error()}},
......
......@@ -20,6 +20,8 @@ import (
"strings"
)
// TODO: Address these per #1502
func IsPullAlways(p PullPolicy) bool {
// Default to pull always
if len(p) == 0 {
......
/*
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 api
// This file contains API types that are unversioned.
// APIVersions lists the api versions that are available, to allow
// version negotiation. APIVersions isn't just an unnamed array of
// strings in order to allow for future evolution, though unversioned
type APIVersions struct {
Versions []string `json:"versions" yaml:"versions"`
}
......@@ -121,6 +121,7 @@ func RecoverPanics(handler http.Handler) http.Handler {
})
}
// TODO: use restful.CrossOriginResourceSharing
// Simple CORS implementation that wraps an http Handler
// For a more detailed implementation use https://github.com/martini-contrib/cors
// or implement CORS at your proxy layer
......
......@@ -19,16 +19,19 @@ package apiserver
import (
"fmt"
"net/http"
"github.com/emicklei/go-restful"
)
// handleIndex is the root index page for Kubernetes.
func handleIndex(w http.ResponseWriter, req *http.Request) {
if req.URL.Path != "/" && req.URL.Path != "/index.html" {
notFound(w, req)
func handleIndex(req *restful.Request, resp *restful.Response) {
// TODO: use restful's Request/Response methods
if req.Request.URL.Path != "/" && req.Request.URL.Path != "/index.html" {
notFound(resp.ResponseWriter, req.Request)
return
}
w.WriteHeader(http.StatusOK)
// TODO: serve this out of a file?
resp.ResponseWriter.WriteHeader(http.StatusOK)
// TODO: serve this out of a file
data := "<html><body>Welcome to Kubernetes</body></html>"
fmt.Fprint(w, data)
fmt.Fprint(resp.ResponseWriter, data)
}
......@@ -113,7 +113,7 @@ func TestOperationsList(t *testing.T) {
}
handler := Handle(map[string]RESTStorage{
"foo": simpleStorage,
}, codec, "/prefix/version", selfLinker)
}, codec, "/prefix", "version", selfLinker)
handler.(*defaultAPIServer).group.handler.asyncOpWait = 0
server := httptest.NewServer(handler)
defer server.Close()
......@@ -170,7 +170,7 @@ func TestOpGet(t *testing.T) {
}
handler := Handle(map[string]RESTStorage{
"foo": simpleStorage,
}, codec, "/prefix/version", selfLinker)
}, codec, "/prefix", "version", selfLinker)
handler.(*defaultAPIServer).group.handler.asyncOpWait = 0
server := httptest.NewServer(handler)
defer server.Close()
......
......@@ -165,7 +165,7 @@ func TestProxy(t *testing.T) {
}
handler := Handle(map[string]RESTStorage{
"foo": simpleStorage,
}, codec, "/prefix/version", selfLinker)
}, codec, "/prefix", "version", selfLinker)
server := httptest.NewServer(handler)
defer server.Close()
......
......@@ -31,7 +31,7 @@ func TestRedirect(t *testing.T) {
}
handler := Handle(map[string]RESTStorage{
"foo": simpleStorage,
}, codec, "/prefix/version", selfLinker)
}, codec, "/prefix", "version", selfLinker)
server := httptest.NewServer(handler)
defer server.Close()
......
......@@ -50,7 +50,7 @@ func TestWatchWebsocket(t *testing.T) {
_ = ResourceWatcher(simpleStorage) // Give compile error if this doesn't work.
handler := Handle(map[string]RESTStorage{
"foo": simpleStorage,
}, codec, "/api/version", selfLinker)
}, codec, "/api", "version", selfLinker)
server := httptest.NewServer(handler)
defer server.Close()
......@@ -104,7 +104,7 @@ func TestWatchHTTP(t *testing.T) {
simpleStorage := &SimpleRESTStorage{}
handler := Handle(map[string]RESTStorage{
"foo": simpleStorage,
}, codec, "/api/version", selfLinker)
}, codec, "/api", "version", selfLinker)
server := httptest.NewServer(handler)
defer server.Close()
client := http.Client{}
......@@ -167,7 +167,7 @@ func TestWatchParamParsing(t *testing.T) {
simpleStorage := &SimpleRESTStorage{}
handler := Handle(map[string]RESTStorage{
"foo": simpleStorage,
}, codec, "/api/version", selfLinker)
}, codec, "/api", "version", selfLinker)
server := httptest.NewServer(handler)
defer server.Close()
......@@ -239,7 +239,7 @@ func TestWatchProtocolSelection(t *testing.T) {
simpleStorage := &SimpleRESTStorage{}
handler := Handle(map[string]RESTStorage{
"foo": simpleStorage,
}, codec, "/api/version", selfLinker)
}, codec, "/api", "version", selfLinker)
server := httptest.NewServer(handler)
defer server.Close()
defer server.CloseClientConnections()
......
......@@ -62,7 +62,7 @@ func (c *Client) Services(namespace string) ServiceInterface {
// VersionInterface has a method to retrieve the server version.
type VersionInterface interface {
ServerVersion() (*version.Info, error)
ServerAPIVersions() (*version.APIVersions, error)
ServerAPIVersions() (*api.APIVersions, error)
}
// APIStatus is exposed by errors that can be converted to an api.Status object
......@@ -91,12 +91,12 @@ func (c *Client) ServerVersion() (*version.Info, error) {
}
// ServerAPIVersions retrieves and parses the list of API versions the server supports.
func (c *Client) ServerAPIVersions() (*version.APIVersions, error) {
func (c *Client) ServerAPIVersions() (*api.APIVersions, error) {
body, err := c.Get().AbsPath("/api").Do().Raw()
if err != nil {
return nil, err
}
var v version.APIVersions
var v api.APIVersions
err = json.Unmarshal(body, &v)
if err != nil {
return nil, fmt.Errorf("Got '%s': %v", string(body), err)
......
......@@ -71,7 +71,7 @@ func (c *Fake) ServerVersion() (*version.Info, error) {
return &versionInfo, nil
}
func (c *Fake) ServerAPIVersions() (*version.APIVersions, error) {
func (c *Fake) ServerAPIVersions() (*api.APIVersions, error) {
c.Actions = append(c.Actions, FakeAction{Action: "get-apiversions", Value: nil})
return &version.APIVersions{Versions: []string{"v1beta1", "v1beta2"}}, nil
return &api.APIVersions{Versions: []string{"v1beta1", "v1beta2"}}, nil
}
......@@ -20,20 +20,24 @@ import (
"net/http"
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authenticator"
"github.com/emicklei/go-restful"
)
// handleWhoAmI returns the user-string which this request is authenticated as (if any).
// Useful for debugging authentication. Always returns HTTP status okay and a human
// readable (not intended as API) description of authentication state of request.
func handleWhoAmI(auth authenticator.Request) func(w http.ResponseWriter, req *http.Request) {
return func(w http.ResponseWriter, req *http.Request) {
func handleWhoAmI(auth authenticator.Request) restful.RouteFunction {
return func(req *restful.Request, resp *restful.Response) {
// This is supposed to go away, so it's not worth the effort to convert to restful
w := resp.ResponseWriter
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
if auth == nil {
w.Write([]byte("NO AUTHENTICATION SUPPORT"))
return
}
userInfo, ok, err := auth.AuthenticateRequest(req)
userInfo, ok, err := auth.AuthenticateRequest(req.Request)
if err != nil {
w.Write([]byte("ERROR WHILE AUTHENTICATING"))
return
......
......@@ -17,10 +17,13 @@ limitations under the License.
package master
import (
"bytes"
_ "expvar"
"fmt"
"net"
"net/http"
"net/url"
rt "runtime"
"strconv"
"strings"
"time"
......@@ -51,6 +54,8 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/ui"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
"github.com/emicklei/go-restful"
"github.com/emicklei/go-restful/swagger"
"github.com/golang/glog"
)
......@@ -64,7 +69,6 @@ type Config struct {
MinionRegexp string
KubeletClient client.KubeletClient
PortalNet *net.IPNet
Mux apiserver.Mux
EnableLogsSupport bool
EnableUISupport bool
APIPrefix string
......@@ -101,6 +105,8 @@ type Master struct {
client *client.Client
portalNet *net.IPNet
mux apiserver.Mux
handlerContainer *restful.Container
rootWebService *restful.WebService
enableLogsSupport bool
enableUISupport bool
apiPrefix string
......@@ -218,6 +224,7 @@ func New(c *Config) *Master {
if c.KubeletClient == nil {
glog.Fatalf("master.New() called with config.KubeletClient == nil")
}
mx := http.NewServeMux()
m := &Master{
podRegistry: etcd.NewRegistry(c.EtcdHelper, boundPodFactory),
controllerRegistry: etcd.NewRegistry(c.EtcdHelper, nil),
......@@ -228,7 +235,9 @@ func New(c *Config) *Master {
minionRegistry: minionRegistry,
client: c.Client,
portalNet: c.PortalNet,
mux: http.NewServeMux(),
mux: mx,
handlerContainer: NewHandlerContainer(mx),
rootWebService: new(restful.WebService),
enableLogsSupport: c.EnableLogsSupport,
enableUISupport: c.EnableUISupport,
apiPrefix: c.APIPrefix,
......@@ -253,6 +262,7 @@ func (m *Master) HandleWithAuth(pattern string, handler http.Handler) {
// URLs into attributes that an Authorizer can understand, and have
// sensible policy defaults for plugged-in endpoints. This will be different
// for generic endpoints versus REST object endpoints.
// TODO: convert to go-restful
m.mux.Handle(pattern, handler)
}
......@@ -260,9 +270,31 @@ func (m *Master) HandleWithAuth(pattern string, handler http.Handler) {
// Applies the same authentication and authorization (if any is configured)
// to the request is used for the master's built-in endpoints.
func (m *Master) HandleFuncWithAuth(pattern string, handler func(http.ResponseWriter, *http.Request)) {
// TODO: convert to go-restful
m.mux.HandleFunc(pattern, handler)
}
func NewHandlerContainer(mux *http.ServeMux) *restful.Container {
container := restful.NewContainer()
container.ServeMux = mux
container.RecoverHandler(logStackOnRecover)
return container
}
//TODO: Unify with RecoverPanics?
func logStackOnRecover(panicReason interface{}, httpWriter http.ResponseWriter) {
var buffer bytes.Buffer
buffer.WriteString(fmt.Sprintf("recover from panic situation: - %v\r\n", panicReason))
for i := 2; ; i += 1 {
_, file, line, ok := rt.Caller(i)
if !ok {
break
}
buffer.WriteString(fmt.Sprintf(" %s:%d\r\n", file, line))
}
glog.Errorln(buffer.String())
}
func makeMinionRegistry(c *Config) minion.Registry {
var minionRegistry minion.Registry = etcd.NewRegistry(c.EtcdHelper, nil)
if c.HealthCheckMinions {
......@@ -286,6 +318,7 @@ func (m *Master) init(c *Config) {
authenticator = bearertoken.New(tokenAuthenticator)
}
// TODO: Factor out the core API registration
m.storage = map[string]apiserver.RESTStorage{
"pods": pod.NewREST(&pod.RESTConfig{
CloudProvider: c.Cloud,
......@@ -304,13 +337,17 @@ func (m *Master) init(c *Config) {
"bindings": binding.NewREST(m.bindingRegistry),
}
apiserver.NewAPIGroup(m.API_v1beta1()).InstallREST(m.mux, c.APIPrefix+"/v1beta1")
apiserver.NewAPIGroup(m.API_v1beta2()).InstallREST(m.mux, c.APIPrefix+"/v1beta2")
apiserver.NewAPIGroupVersion(m.API_v1beta1()).InstallREST(m.handlerContainer, c.APIPrefix, "v1beta1")
apiserver.NewAPIGroupVersion(m.API_v1beta2()).InstallREST(m.handlerContainer, c.APIPrefix, "v1beta2")
// TODO: InstallREST should register each version automatically
versionHandler := apiserver.APIVersionHandler("v1beta1", "v1beta2")
m.mux.Handle(c.APIPrefix, versionHandler)
apiserver.InstallSupport(m.mux)
serversToValidate := m.getServersToValidate(c)
m.rootWebService.Route(m.rootWebService.GET(c.APIPrefix).To(versionHandler))
apiserver.InstallSupport(m.handlerContainer, m.rootWebService)
// TODO: use go-restful
serversToValidate := m.getServersToValidate(c)
apiserver.InstallValidator(m.mux, serversToValidate)
if c.EnableLogsSupport {
apiserver.InstallLogsSupport(m.mux)
......@@ -319,8 +356,15 @@ func (m *Master) init(c *Config) {
ui.InstallSupport(m.mux)
}
// TODO: install runtime/pprof handler
// See github.com/emicklei/go-restful/blob/master/examples/restful-cpuprofiler-service.go
handler := http.Handler(m.mux.(*http.ServeMux))
// TODO: handle CORS and auth using go-restful
// See github.com/emicklei/go-restful/blob/master/examples/restful-CORS-filter.go, and
// github.com/emicklei/go-restful/blob/master/examples/restful-basic-authentication.go
if len(c.CorsAllowedOriginList) > 0 {
allowedOriginRegexps, err := util.CompileRegexps(c.CorsAllowedOriginList)
if err != nil {
......@@ -338,7 +382,23 @@ func (m *Master) init(c *Config) {
if authenticator != nil {
handler = handlers.NewRequestAuthenticator(userContexts, authenticator, handlers.Unauthorized, handler)
}
m.mux.HandleFunc("/_whoami", handleWhoAmI(authenticator))
// TODO: Remove temporary _whoami handler
m.rootWebService.Route(m.rootWebService.GET("/_whoami").To(handleWhoAmI(authenticator)))
// Install root web services
m.handlerContainer.Add(m.rootWebService)
// TODO: Make this optional?
// Enable swagger UI and discovery API
swaggerConfig := swagger.Config{
WebServices: m.handlerContainer.RegisteredWebServices(),
// TODO: Parameterize the path?
ApiPath: "/swaggerapi/",
// TODO: Distribute UI javascript and enable the UI
//SwaggerPath: "/swaggerui/",
//SwaggerFilePath: "/srv/apiserver/swagger/dist"
}
swagger.RegisterSwaggerService(swaggerConfig, m.handlerContainer)
m.Handler = handler
......
......@@ -96,11 +96,8 @@ func RunApiServer(cl *client.Client, etcdClient tools.EtcdClient, addr string, p
ReadOnlyPort: port,
PublicAddress: addr,
})
mux := http.NewServeMux()
apiserver.NewAPIGroup(m.API_v1beta1()).InstallREST(mux, "/api/v1beta1")
apiserver.NewAPIGroup(m.API_v1beta2()).InstallREST(mux, "/api/v1beta2")
apiserver.InstallSupport(mux)
handler.delegate = mux
handler.delegate = m.InsecureHandler
go http.ListenAndServe(fmt.Sprintf("%s:%d", addr, port), &handler)
}
......
......@@ -45,9 +45,3 @@ func Get() Info {
func (info Info) String() string {
return info.GitVersion
}
// APIVersions lists the api versions that are available, to allow
// version negotiation.
type APIVersions struct {
Versions []string `json:"versions" yaml:"versions"`
}
......@@ -242,6 +242,7 @@ var code200 = map[int]bool{200: true}
var code400 = map[int]bool{400: true}
var code403 = map[int]bool{403: true}
var code404 = map[int]bool{404: true}
var code405 = map[int]bool{405: true}
var code409 = map[int]bool{409: true}
var code422 = map[int]bool{422: true}
var code500 = map[int]bool{500: true}
......@@ -269,14 +270,14 @@ func getTestRequests() []struct {
// Non-standard methods (not expected to work,
// but expected to pass/fail authorization prior to
// failing validation.
{"PATCH", "/api/v1beta1/pods/a", "", code404},
{"OPTIONS", "/api/v1beta1/pods", "", code404},
{"OPTIONS", "/api/v1beta1/pods/a", "", code404},
{"HEAD", "/api/v1beta1/pods", "", code404},
{"HEAD", "/api/v1beta1/pods/a", "", code404},
{"TRACE", "/api/v1beta1/pods", "", code404},
{"TRACE", "/api/v1beta1/pods/a", "", code404},
{"NOSUCHVERB", "/api/v1beta1/pods", "", code404},
{"PATCH", "/api/v1beta1/pods/a", "", code405},
{"OPTIONS", "/api/v1beta1/pods", "", code405},
{"OPTIONS", "/api/v1beta1/pods/a", "", code405},
{"HEAD", "/api/v1beta1/pods", "", code405},
{"HEAD", "/api/v1beta1/pods/a", "", code405},
{"TRACE", "/api/v1beta1/pods", "", code405},
{"TRACE", "/api/v1beta1/pods/a", "", code405},
{"NOSUCHVERB", "/api/v1beta1/pods", "", code405},
// Normal methods on services
{"GET", "/api/v1beta1/services", "", code200},
......@@ -320,12 +321,12 @@ func getTestRequests() []struct {
{"DELETE", "/api/v1beta1/events/a" + syncFlags, "", code200},
// Normal methods on bindings
{"GET", "/api/v1beta1/bindings", "", code404}, // Bindings are write-only, so 404
{"GET", "/api/v1beta1/bindings", "", code405}, // Bindings are write-only
{"POST", "/api/v1beta1/pods" + syncFlags, aPod, code200}, // Need a pod to bind or you get a 404
{"POST", "/api/v1beta1/bindings" + syncFlags, aBinding, code200},
{"PUT", "/api/v1beta1/bindings/a" + syncFlags, aBinding, code500}, // See #2114 about why 500
{"GET", "/api/v1beta1/bindings", "", code404},
{"GET", "/api/v1beta1/bindings/a", "", code404},
{"GET", "/api/v1beta1/bindings", "", code405},
{"GET", "/api/v1beta1/bindings/a", "", code404}, // No bindings instances
{"DELETE", "/api/v1beta1/bindings/a" + syncFlags, "", code404},
// Non-existent object type.
......@@ -340,7 +341,8 @@ func getTestRequests() []struct {
{"GET", "/api/v1beta1/operations", "", code200},
{"GET", "/api/v1beta1/operations/1234567890", "", code404},
// Special verbs on pods
// Special verbs on nodes
// TODO: Will become 405 once these are converted to go-restful
{"GET", "/api/v1beta1/proxy/minions/a", "", code404},
{"GET", "/api/v1beta1/redirect/minions/a", "", code404},
// TODO: test .../watch/..., which doesn't end before the test timeout.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment