Merge pull request #2282 from bgrant0607/docgen

Automatic API generation via go-restful
This commit is contained in:
Daniel Smith 2014-11-14 13:12:25 -08:00
commit 9430bb38b8
18 changed files with 408 additions and 142 deletions

View File

@ -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()}},

View File

@ -20,6 +20,8 @@ import (
"strings"
)
// TODO: Address these per #1502
func IsPullAlways(p PullPolicy) bool {
return pullPoliciesEqual(p, PullAlways)
}

26
pkg/api/unversioned.go Normal file
View File

@ -0,0 +1,26 @@
/*
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"`
}

View File

@ -20,13 +20,17 @@ import (
"encoding/json"
"io/ioutil"
"net/http"
"path"
"reflect"
"strings"
"time"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/healthz"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
"github.com/GoogleCloudPlatform/kubernetes/pkg/version"
"github.com/emicklei/go-restful"
"github.com/golang/glog"
)
@ -39,7 +43,7 @@ type Mux interface {
// defaultAPIServer exposes nested objects for testability.
type defaultAPIServer struct {
http.Handler
group *APIGroup
group *APIGroupVersion
}
const (
@ -49,32 +53,36 @@ const (
// Handle returns a Handler function that exposes the provided storage interfaces
// as RESTful resources at prefix, serialized by codec, and also includes the support
// http resources.
func Handle(storage map[string]RESTStorage, codec runtime.Codec, prefix string, selfLinker runtime.SelfLinker) http.Handler {
group := NewAPIGroup(storage, codec, prefix, selfLinker)
mux := http.NewServeMux()
group.InstallREST(mux, prefix)
InstallSupport(mux)
func Handle(storage map[string]RESTStorage, codec runtime.Codec, root string, version string, selfLinker runtime.SelfLinker) http.Handler {
prefix := root + "/" + version
group := NewAPIGroupVersion(storage, codec, prefix, selfLinker)
container := restful.NewContainer()
mux := container.ServeMux
group.InstallREST(container, root, version)
ws := new(restful.WebService)
InstallSupport(container, ws)
container.Add(ws)
return &defaultAPIServer{mux, group}
}
// APIGroup is a http.Handler that exposes multiple RESTStorage objects
// TODO: This is a whole API version right now. Maybe should rename it.
// APIGroupVersion is a http.Handler that exposes multiple RESTStorage objects
// It handles URLs of the form:
// /${storage_key}[/${object_name}]
// Where 'storage_key' points to a RESTStorage object stored in storage.
//
// TODO: consider migrating this to go-restful which is a more full-featured version of the same thing.
type APIGroup struct {
type APIGroupVersion struct {
handler RESTHandler
}
// NewAPIGroup returns an object that will serve a set of REST resources and their
// NewAPIGroupVersion returns an object that will serve a set of REST resources and their
// associated operations. The provided codec controls serialization and deserialization.
// This is a helper method for registering multiple sets of REST handlers under different
// prefixes onto a server.
// TODO: add multitype codec serialization
func NewAPIGroup(storage map[string]RESTStorage, codec runtime.Codec, canonicalPrefix string, selfLinker runtime.SelfLinker) *APIGroup {
return &APIGroup{RESTHandler{
func NewAPIGroupVersion(storage map[string]RESTStorage, codec runtime.Codec, canonicalPrefix string, selfLinker runtime.SelfLinker) *APIGroupVersion {
return &APIGroupVersion{RESTHandler{
storage: storage,
codec: codec,
canonicalPrefix: canonicalPrefix,
@ -85,70 +93,196 @@ func NewAPIGroup(storage map[string]RESTStorage, codec runtime.Codec, canonicalP
}}
}
func InstallValidator(mux Mux, servers map[string]Server) {
validator, err := NewValidator(servers)
if err != nil {
glog.Errorf("failed to set up validator: %v", err)
return
}
mux.Handle("/validate", validator)
// This magic incantation returns *ptrToObject for an arbitrary pointer
func indirectArbitraryPointer(ptrToObject interface{}) interface{} {
return reflect.Indirect(reflect.ValueOf(ptrToObject)).Interface()
}
// InstallREST registers the REST handlers (storage, watch, and operations) into a mux.
// It is expected that the provided prefix will serve all operations. Path MUST NOT end
// in a slash.
func (g *APIGroup) InstallREST(mux Mux, paths ...string) {
func registerResourceHandlers(ws *restful.WebService, version string, path string, storage RESTStorage, kinds map[string]reflect.Type, h restful.RouteFunction) {
glog.V(3).Infof("Installing /%s/%s\n", version, path)
object := storage.New()
_, kind, err := api.Scheme.ObjectVersionAndKind(object)
if err != nil {
glog.Warningf("error getting kind: %v\n", err)
return
}
versionedPtr, err := api.Scheme.New(version, kind)
if err != nil {
glog.Warningf("error making object: %v\n", err)
return
}
versionedObject := indirectArbitraryPointer(versionedPtr)
glog.V(3).Infoln("type: ", reflect.TypeOf(versionedObject))
// See github.com/emicklei/go-restful/blob/master/jsr311.go for routing logic
// and status-code behavior
ws.Route(ws.POST(path).To(h).
Doc("create a " + kind).
Operation("create" + kind).
Reads(versionedObject)) // from the request
// TODO: This seems like a hack. Add NewList() to storage?
listKind := kind + "List"
if _, ok := kinds[listKind]; !ok {
glog.V(1).Infof("no list type: %v\n", listKind)
} else {
versionedListPtr, err := api.Scheme.New(version, listKind)
if err != nil {
glog.Errorf("error making list: %v\n", err)
} else {
versionedList := indirectArbitraryPointer(versionedListPtr)
glog.V(3).Infoln("type: ", reflect.TypeOf(versionedList))
ws.Route(ws.GET(path).To(h).
Doc("list objects of kind "+kind).
Operation("list"+kind).
Returns(http.StatusOK, "OK", versionedList))
}
}
ws.Route(ws.GET(path + "/{name}").To(h).
Doc("read the specified " + kind).
Operation("read" + kind).
Param(ws.PathParameter("name", "name of the "+kind).DataType("string")).
Writes(versionedObject)) // on the response
ws.Route(ws.PUT(path + "/{name}").To(h).
Doc("update the specified " + kind).
Operation("update" + kind).
Param(ws.PathParameter("name", "name of the "+kind).DataType("string")).
Reads(versionedObject)) // from the request
// TODO: Support PATCH
ws.Route(ws.DELETE(path + "/{name}").To(h).
Doc("delete the specified " + kind).
Operation("delete" + kind).
Param(ws.PathParameter("name", "name of the "+kind).DataType("string")))
}
// InstallREST registers the REST handlers (storage, watch, and operations) into a restful Container.
// It is expected that the provided path root prefix will serve all operations. Root MUST NOT end
// in a slash. A restful WebService is created for the group and version.
func (g *APIGroupVersion) InstallREST(container *restful.Container, root string, version string) {
prefix := path.Join(root, version)
restHandler := &g.handler
strippedHandler := http.StripPrefix(prefix, restHandler)
watchHandler := &WatchHandler{
storage: g.handler.storage,
codec: g.handler.codec,
canonicalPrefix: g.handler.canonicalPrefix,
selfLinker: g.handler.selfLinker,
}
proxyHandler := &ProxyHandler{prefix + "/proxy/", g.handler.storage, g.handler.codec}
redirectHandler := &RedirectHandler{g.handler.storage, g.handler.codec}
opHandler := &OperationHandler{g.handler.ops, g.handler.codec}
for _, prefix := range paths {
prefix = strings.TrimRight(prefix, "/")
proxyHandler := &ProxyHandler{prefix + "/proxy/", g.handler.storage, g.handler.codec}
mux.Handle(prefix+"/", http.StripPrefix(prefix, restHandler))
// Note: update GetAttribs() when adding a handler.
mux.Handle(prefix+"/watch/", http.StripPrefix(prefix+"/watch/", watchHandler))
mux.Handle(prefix+"/proxy/", http.StripPrefix(prefix+"/proxy/", proxyHandler))
mux.Handle(prefix+"/redirect/", http.StripPrefix(prefix+"/redirect/", redirectHandler))
mux.Handle(prefix+"/operations", http.StripPrefix(prefix+"/operations", opHandler))
mux.Handle(prefix+"/operations/", http.StripPrefix(prefix+"/operations/", opHandler))
// Create a new WebService for this APIGroupVersion at the specified path prefix
// TODO: Pass in more descriptive documentation
ws := new(restful.WebService)
ws.Path(prefix)
ws.Doc("API at " + root + ", version " + version)
// TODO: change to restful.MIME_JSON when we convert YAML->JSON and set content type in client
ws.Consumes("*/*")
ws.Produces(restful.MIME_JSON)
// TODO: require json on input
//ws.Consumes(restful.MIME_JSON)
// TODO: add scheme to APIGroupVersion rather than using api.Scheme
kinds := api.Scheme.KnownTypes(version)
glog.V(4).Infof("InstallREST: %v kinds: %#v", version, kinds)
// TODO: #2057: Return API resources on "/".
// TODO: Add status documentation using Returns()
// Errors (see api/errors/errors.go as well as go-restful router):
// http.StatusNotFound, http.StatusMethodNotAllowed,
// http.StatusUnsupportedMediaType, http.StatusNotAcceptable,
// http.StatusBadRequest, http.StatusUnauthorized, http.StatusForbidden,
// http.StatusRequestTimeout, http.StatusConflict, http.StatusPreconditionFailed,
// 422 (StatusUnprocessableEntity), http.StatusInternalServerError,
// http.StatusServiceUnavailable
// and api error codes
// Note that if we specify a versioned Status object here, we may need to
// create one for the tests, also
// Success:
// http.StatusOK, http.StatusCreated, http.StatusAccepted, http.StatusNoContent
//
// test/integration/auth_test.go is currently the most comprehensive status code test
// TODO: eliminate all the restful wrappers
// TODO: create a separate handler per verb
h := func(req *restful.Request, resp *restful.Response) {
glog.V(4).Infof("User-Agent: %s\n", req.HeaderParameter("User-Agent"))
strippedHandler.ServeHTTP(resp.ResponseWriter, req.Request)
}
for path, storage := range g.handler.storage {
registerResourceHandlers(ws, version, path, storage, kinds, h)
}
// TODO: port the rest of these. Sadly, if we don't, we'll have inconsistent
// API behavior, as well as lack of documentation
mux := container.ServeMux
// Note: update GetAttribs() when adding a handler.
mux.Handle(prefix+"/watch/", http.StripPrefix(prefix+"/watch/", watchHandler))
mux.Handle(prefix+"/proxy/", http.StripPrefix(prefix+"/proxy/", proxyHandler))
mux.Handle(prefix+"/redirect/", http.StripPrefix(prefix+"/redirect/", redirectHandler))
mux.Handle(prefix+"/operations", http.StripPrefix(prefix+"/operations", opHandler))
mux.Handle(prefix+"/operations/", http.StripPrefix(prefix+"/operations/", opHandler))
container.Add(ws)
}
// TODO: Convert to go-restful
func InstallValidator(mux Mux, servers map[string]Server) {
validator, err := NewValidator(servers)
if err != nil {
glog.Errorf("failed to set up validator: %v", err)
return
}
if validator != nil {
mux.Handle("/validate", validator)
}
}
// InstallSupport registers the APIServer support functions into a mux.
func InstallSupport(mux Mux) {
healthz.InstallHandler(mux)
mux.HandleFunc("/version", handleVersion)
mux.HandleFunc("/", handleIndex)
// TODO: document all handlers
// InstallSupport registers the APIServer support functions
func InstallSupport(container *restful.Container, ws *restful.WebService) {
// TODO: convert healthz to restful and remove container arg
healthz.InstallHandler(container.ServeMux)
ws.Route(ws.GET("/").To(handleIndex))
ws.Route(ws.GET("/version").To(handleVersion))
}
// InstallLogsSupport registers the APIServer log support function into a mux.
func InstallLogsSupport(mux Mux) {
// TODO: use restful: ws.Route(ws.GET("/logs/{logpath:*}").To(fileHandler))
// See github.com/emicklei/go-restful/blob/master/examples/restful-serve-static.go
mux.Handle("/logs/", http.StripPrefix("/logs/", http.FileServer(http.Dir("/var/log/"))))
}
// handleVersion writes the server's version information.
func handleVersion(w http.ResponseWriter, req *http.Request) {
writeRawJSON(http.StatusOK, version.Get(), w)
func handleVersion(req *restful.Request, resp *restful.Response) {
// TODO: use restful's Response methods
writeRawJSON(http.StatusOK, version.Get(), resp.ResponseWriter)
}
// APIVersionHandler returns a handler which will list the provided versions as available.
func APIVersionHandler(versions ...string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
writeRawJSON(http.StatusOK, version.APIVersions{Versions: versions}, w)
})
func APIVersionHandler(versions ...string) restful.RouteFunction {
return func(req *restful.Request, resp *restful.Response) {
// TODO: use restful's Response methods
writeRawJSON(http.StatusOK, api.APIVersions{Versions: versions}, resp.ResponseWriter)
}
}
// writeJSON renders an object as JSON to the response.
func writeJSON(statusCode int, codec runtime.Codec, object runtime.Object, w http.ResponseWriter) {
output, err := codec.Encode(object)
if err != nil {
// Note: If codec is broken, this results in an infinite recursion
errorJSON(err, codec, w)
return
}

View File

@ -32,8 +32,7 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
apierrs "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/testapi"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta"
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
@ -45,12 +44,54 @@ func convert(obj runtime.Object) (runtime.Object, error) {
return obj, nil
}
var codec = testapi.Codec()
var selfLinker = latest.SelfLinker
// This creates a fake API version, similar to api/latest.go
const testVersion = "version"
var versions = []string{testVersion}
var codec = runtime.CodecFor(api.Scheme, testVersion)
var accessor = meta.NewAccessor()
var versioner runtime.ResourceVersioner = accessor
var selfLinker runtime.SelfLinker = accessor
var mapper meta.RESTMapper
func interfacesFor(version string) (*meta.VersionInterfaces, error) {
switch version {
case testVersion:
return &meta.VersionInterfaces{
Codec: codec,
ObjectConvertor: api.Scheme,
MetadataAccessor: accessor,
}, nil
default:
return nil, fmt.Errorf("unsupported storage version: %s (valid: %s)", version, strings.Join(versions, ", "))
}
}
func init() {
api.Scheme.AddKnownTypes("", &Simple{}, &SimpleList{})
api.Scheme.AddKnownTypes(testapi.Version(), &Simple{}, &SimpleList{})
// Certain API objects are returned regardless of the contents of storage:
// api.Status is returned in errors
// api.ServerOp/api.ServerOpList are returned by /operations
// "internal" version
api.Scheme.AddKnownTypes("", &Simple{}, &SimpleList{},
&api.Status{}, &api.ServerOp{}, &api.ServerOpList{})
// "version" version
// TODO: Use versioned api objects?
api.Scheme.AddKnownTypes(testVersion, &Simple{}, &SimpleList{},
&api.Status{}, &api.ServerOp{}, &api.ServerOpList{})
defMapper := meta.NewDefaultRESTMapper(
versions,
func(version string) (*meta.VersionInterfaces, bool) {
interfaces, err := interfacesFor(version)
if err != nil {
return nil, false
}
return interfaces, true
},
)
defMapper.Add(api.Scheme, true, versions...)
mapper = defMapper
}
type Simple struct {
@ -204,23 +245,24 @@ func TestNotFound(t *testing.T) {
type T struct {
Method string
Path string
Status int
}
cases := map[string]T{
"PATCH method": {"PATCH", "/prefix/version/foo"},
"GET long prefix": {"GET", "/prefix/"},
"GET missing storage": {"GET", "/prefix/version/blah"},
"GET with extra segment": {"GET", "/prefix/version/foo/bar/baz"},
"POST with extra segment": {"POST", "/prefix/version/foo/bar"},
"DELETE without extra segment": {"DELETE", "/prefix/version/foo"},
"DELETE with extra segment": {"DELETE", "/prefix/version/foo/bar/baz"},
"PUT without extra segment": {"PUT", "/prefix/version/foo"},
"PUT with extra segment": {"PUT", "/prefix/version/foo/bar/baz"},
"watch missing storage": {"GET", "/prefix/version/watch/"},
"watch with bad method": {"POST", "/prefix/version/watch/foo/bar"},
"PATCH method": {"PATCH", "/prefix/version/foo", http.StatusMethodNotAllowed},
"GET long prefix": {"GET", "/prefix/", http.StatusNotFound},
"GET missing storage": {"GET", "/prefix/version/blah", http.StatusNotFound},
"GET with extra segment": {"GET", "/prefix/version/foo/bar/baz", http.StatusNotFound},
"POST with extra segment": {"POST", "/prefix/version/foo/bar", http.StatusMethodNotAllowed},
"DELETE without extra segment": {"DELETE", "/prefix/version/foo", http.StatusMethodNotAllowed},
"DELETE with extra segment": {"DELETE", "/prefix/version/foo/bar/baz", http.StatusNotFound},
"PUT without extra segment": {"PUT", "/prefix/version/foo", http.StatusMethodNotAllowed},
"PUT with extra segment": {"PUT", "/prefix/version/foo/bar/baz", http.StatusNotFound},
"watch missing storage": {"GET", "/prefix/version/watch/", http.StatusNotFound},
"watch with bad method": {"POST", "/prefix/version/watch/foo/bar", http.StatusNotFound},
}
handler := Handle(map[string]RESTStorage{
"foo": &SimpleRESTStorage{},
}, codec, "/prefix/version", selfLinker)
}, codec, "/prefix", testVersion, selfLinker)
server := httptest.NewServer(handler)
defer server.Close()
client := http.Client{}
@ -235,14 +277,14 @@ func TestNotFound(t *testing.T) {
t.Errorf("unexpected error: %v", err)
}
if response.StatusCode != http.StatusNotFound {
t.Errorf("Expected %d for %s (%s), Got %#v", http.StatusNotFound, v, k, response)
if response.StatusCode != v.Status {
t.Errorf("Expected %d for %s (%s), Got %#v", v.Status, v, k, response)
}
}
}
func TestVersion(t *testing.T) {
handler := Handle(map[string]RESTStorage{}, codec, "/prefix/version", selfLinker)
handler := Handle(map[string]RESTStorage{}, codec, "/prefix", testVersion, selfLinker)
server := httptest.NewServer(handler)
defer server.Close()
client := http.Client{}
@ -276,7 +318,7 @@ func TestSimpleList(t *testing.T) {
t: t,
expectedSet: "/prefix/version/simple",
}
handler := Handle(storage, codec, "/prefix/version", selfLinker)
handler := Handle(storage, codec, "/prefix", testVersion, selfLinker)
server := httptest.NewServer(handler)
defer server.Close()
@ -299,7 +341,7 @@ func TestErrorList(t *testing.T) {
errors: map[string]error{"list": fmt.Errorf("test Error")},
}
storage["simple"] = &simpleStorage
handler := Handle(storage, codec, "/prefix/version", selfLinker)
handler := Handle(storage, codec, "/prefix", testVersion, selfLinker)
server := httptest.NewServer(handler)
defer server.Close()
@ -309,7 +351,7 @@ func TestErrorList(t *testing.T) {
}
if resp.StatusCode != http.StatusInternalServerError {
t.Errorf("Unexpected status: %d, Expected: %d, %#v", resp.StatusCode, http.StatusOK, resp)
t.Errorf("Unexpected status: %d, Expected: %d, %#v", resp.StatusCode, http.StatusInternalServerError, resp)
}
}
@ -324,7 +366,7 @@ func TestNonEmptyList(t *testing.T) {
},
}
storage["simple"] = &simpleStorage
handler := Handle(storage, codec, "/prefix/version", selfLinker)
handler := Handle(storage, codec, "/prefix", testVersion, selfLinker)
server := httptest.NewServer(handler)
defer server.Close()
@ -366,7 +408,7 @@ func TestGet(t *testing.T) {
expectedSet: "/prefix/version/simple/id",
}
storage["simple"] = &simpleStorage
handler := Handle(storage, codec, "/prefix/version", selfLinker)
handler := Handle(storage, codec, "/prefix", testVersion, selfLinker)
server := httptest.NewServer(handler)
defer server.Close()
@ -391,7 +433,7 @@ func TestGetMissing(t *testing.T) {
errors: map[string]error{"get": apierrs.NewNotFound("simple", "id")},
}
storage["simple"] = &simpleStorage
handler := Handle(storage, codec, "/prefix/version", selfLinker)
handler := Handle(storage, codec, "/prefix", testVersion, selfLinker)
server := httptest.NewServer(handler)
defer server.Close()
@ -410,7 +452,7 @@ func TestDelete(t *testing.T) {
simpleStorage := SimpleRESTStorage{}
ID := "id"
storage["simple"] = &simpleStorage
handler := Handle(storage, codec, "/prefix/version", selfLinker)
handler := Handle(storage, codec, "/prefix", testVersion, selfLinker)
server := httptest.NewServer(handler)
defer server.Close()
@ -433,7 +475,7 @@ func TestDeleteMissing(t *testing.T) {
errors: map[string]error{"delete": apierrs.NewNotFound("simple", ID)},
}
storage["simple"] = &simpleStorage
handler := Handle(storage, codec, "/prefix/version", selfLinker)
handler := Handle(storage, codec, "/prefix", testVersion, selfLinker)
server := httptest.NewServer(handler)
defer server.Close()
@ -458,7 +500,7 @@ func TestUpdate(t *testing.T) {
t: t,
expectedSet: "/prefix/version/simple/" + ID,
}
handler := Handle(storage, codec, "/prefix/version", selfLinker)
handler := Handle(storage, codec, "/prefix", testVersion, selfLinker)
server := httptest.NewServer(handler)
defer server.Close()
@ -467,7 +509,8 @@ func TestUpdate(t *testing.T) {
}
body, err := codec.Encode(item)
if err != nil {
t.Errorf("unexpected error: %v", err)
// The following cases will fail, so die now
t.Fatalf("unexpected error: %v", err)
}
client := http.Client{}
@ -477,7 +520,7 @@ func TestUpdate(t *testing.T) {
t.Errorf("unexpected error: %v", err)
}
if simpleStorage.updated.Name != item.Name {
if simpleStorage.updated == nil || simpleStorage.updated.Name != item.Name {
t.Errorf("Unexpected update value %#v, expected %#v.", simpleStorage.updated, item)
}
if !selfLinker.called {
@ -492,7 +535,7 @@ func TestUpdateMissing(t *testing.T) {
errors: map[string]error{"update": apierrs.NewNotFound("simple", ID)},
}
storage["simple"] = &simpleStorage
handler := Handle(storage, codec, "/prefix/version", selfLinker)
handler := Handle(storage, codec, "/prefix", testVersion, selfLinker)
server := httptest.NewServer(handler)
defer server.Close()
@ -527,7 +570,7 @@ func TestCreate(t *testing.T) {
}
handler := Handle(map[string]RESTStorage{
"foo": simpleStorage,
}, codec, "/prefix/version", selfLinker)
}, codec, "/prefix", testVersion, selfLinker)
handler.(*defaultAPIServer).group.handler.asyncOpWait = 0
server := httptest.NewServer(handler)
defer server.Close()
@ -570,7 +613,7 @@ func TestCreateNotFound(t *testing.T) {
// See https://github.com/GoogleCloudPlatform/kubernetes/pull/486#discussion_r15037092.
errors: map[string]error{"create": apierrs.NewNotFound("simple", "id")},
},
}, codec, "/prefix/version", selfLinker)
}, codec, "/prefix", testVersion, selfLinker)
server := httptest.NewServer(handler)
defer server.Close()
client := http.Client{}
@ -635,7 +678,7 @@ func TestSyncCreate(t *testing.T) {
}
handler := Handle(map[string]RESTStorage{
"foo": &storage,
}, codec, "/prefix/version", selfLinker)
}, codec, "/prefix", testVersion, selfLinker)
server := httptest.NewServer(handler)
defer server.Close()
client := http.Client{}
@ -708,7 +751,7 @@ func TestAsyncDelayReturnsError(t *testing.T) {
return nil, apierrs.NewAlreadyExists("foo", "bar")
},
}
handler := Handle(map[string]RESTStorage{"foo": &storage}, codec, "/prefix/version", selfLinker)
handler := Handle(map[string]RESTStorage{"foo": &storage}, codec, "/prefix", testVersion, selfLinker)
handler.(*defaultAPIServer).group.handler.asyncOpWait = time.Millisecond / 2
server := httptest.NewServer(handler)
defer server.Close()
@ -732,7 +775,7 @@ func TestAsyncCreateError(t *testing.T) {
name: "bar",
expectedSet: "/prefix/version/foo/bar",
}
handler := Handle(map[string]RESTStorage{"foo": &storage}, codec, "/prefix/version", selfLinker)
handler := Handle(map[string]RESTStorage{"foo": &storage}, codec, "/prefix", testVersion, selfLinker)
handler.(*defaultAPIServer).group.handler.asyncOpWait = 0
server := httptest.NewServer(handler)
defer server.Close()
@ -784,7 +827,7 @@ func (*UnregisteredAPIObject) IsAnAPIObject() {}
func TestWriteJSONDecodeError(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
writeJSON(http.StatusOK, latest.Codec, &UnregisteredAPIObject{"Undecodable"}, w)
writeJSON(http.StatusOK, codec, &UnregisteredAPIObject{"Undecodable"}, w)
}))
defer server.Close()
status := expectApiStatus(t, "GET", server.URL, nil, http.StatusInternalServerError)
@ -832,7 +875,7 @@ func TestSyncCreateTimeout(t *testing.T) {
}
handler := Handle(map[string]RESTStorage{
"foo": &storage,
}, codec, "/prefix/version", selfLinker)
}, codec, "/prefix", testVersion, selfLinker)
server := httptest.NewServer(handler)
defer server.Close()
@ -864,7 +907,7 @@ func TestCORSAllowedOrigins(t *testing.T) {
}
handler := CORS(
Handle(map[string]RESTStorage{}, codec, "/prefix/version", selfLinker),
Handle(map[string]RESTStorage{}, codec, "/prefix", testVersion, selfLinker),
allowedOriginRegexps, nil, nil, "true",
)
server := httptest.NewServer(handler)

View File

@ -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

View File

@ -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)
}

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -63,7 +63,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
@ -92,12 +92,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)

View File

@ -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
}

View File

@ -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

View File

@ -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")
versionHandler := apiserver.APIVersionHandler("v1beta1", "v1beta2")
m.mux.Handle(c.APIPrefix, versionHandler)
apiserver.InstallSupport(m.mux)
serversToValidate := m.getServersToValidate(c)
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.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

View File

@ -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)
}

View File

@ -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"`
}

View File

@ -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.