2014-06-06 23:40:48 +00:00
|
|
|
/*
|
|
|
|
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.
|
|
|
|
*/
|
2014-06-16 05:34:16 +00:00
|
|
|
|
2014-06-06 23:40:48 +00:00
|
|
|
package apiserver
|
|
|
|
|
|
|
|
import (
|
2014-07-29 21:35:20 +00:00
|
|
|
"encoding/json"
|
2014-06-06 23:40:48 +00:00
|
|
|
"io/ioutil"
|
|
|
|
"net/http"
|
2014-11-11 07:11:45 +00:00
|
|
|
"path"
|
|
|
|
"reflect"
|
2014-06-06 23:40:48 +00:00
|
|
|
"strings"
|
2014-06-19 04:04:11 +00:00
|
|
|
"time"
|
2014-06-17 01:03:44 +00:00
|
|
|
|
2014-11-11 07:11:45 +00:00
|
|
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
2014-07-15 22:38:56 +00:00
|
|
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/healthz"
|
2014-09-06 02:22:03 +00:00
|
|
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
|
2014-07-25 19:28:20 +00:00
|
|
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/version"
|
2014-11-06 01:22:18 +00:00
|
|
|
|
2014-11-11 07:11:45 +00:00
|
|
|
"github.com/emicklei/go-restful"
|
2014-06-25 03:51:57 +00:00
|
|
|
"github.com/golang/glog"
|
2014-06-06 23:40:48 +00:00
|
|
|
)
|
|
|
|
|
2014-08-25 21:36:15 +00:00
|
|
|
// mux is an object that can register http handlers.
|
2014-10-23 20:56:18 +00:00
|
|
|
type Mux interface {
|
2014-08-09 21:12:55 +00:00
|
|
|
Handle(pattern string, handler http.Handler)
|
|
|
|
HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request))
|
|
|
|
}
|
|
|
|
|
2014-08-25 21:36:15 +00:00
|
|
|
// defaultAPIServer exposes nested objects for testability.
|
2014-08-09 21:12:55 +00:00
|
|
|
type defaultAPIServer struct {
|
|
|
|
http.Handler
|
2014-11-11 07:11:45 +00:00
|
|
|
group *APIGroupVersion
|
2014-08-09 21:12:55 +00:00
|
|
|
}
|
|
|
|
|
2014-09-09 20:59:39 +00:00
|
|
|
const (
|
|
|
|
StatusUnprocessableEntity = 422
|
|
|
|
)
|
|
|
|
|
2014-09-26 00:20:28 +00:00
|
|
|
// Handle returns a Handler function that exposes the provided storage interfaces
|
2014-08-09 21:12:55 +00:00
|
|
|
// as RESTful resources at prefix, serialized by codec, and also includes the support
|
|
|
|
// http resources.
|
2014-11-11 07:11:45 +00:00
|
|
|
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)
|
2014-09-04 17:55:30 +00:00
|
|
|
return &defaultAPIServer{mux, group}
|
2014-08-09 21:12:55 +00:00
|
|
|
}
|
|
|
|
|
2014-11-11 07:11:45 +00:00
|
|
|
// TODO: This is a whole API version right now. Maybe should rename it.
|
|
|
|
// APIGroupVersion is a http.Handler that exposes multiple RESTStorage objects
|
2014-06-06 23:40:48 +00:00
|
|
|
// It handles URLs of the form:
|
2014-08-09 21:12:55 +00:00
|
|
|
// /${storage_key}[/${object_name}]
|
|
|
|
// Where 'storage_key' points to a RESTStorage object stored in storage.
|
2014-06-06 23:40:48 +00:00
|
|
|
//
|
|
|
|
// TODO: consider migrating this to go-restful which is a more full-featured version of the same thing.
|
2014-11-11 07:11:45 +00:00
|
|
|
type APIGroupVersion struct {
|
2014-08-09 21:12:55 +00:00
|
|
|
handler RESTHandler
|
2014-06-06 23:40:48 +00:00
|
|
|
}
|
|
|
|
|
2014-11-11 07:11:45 +00:00
|
|
|
// NewAPIGroupVersion returns an object that will serve a set of REST resources and their
|
2014-08-09 21:12:55 +00:00
|
|
|
// 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.
|
2014-08-06 03:10:48 +00:00
|
|
|
// TODO: add multitype codec serialization
|
2014-11-11 07:11:45 +00:00
|
|
|
func NewAPIGroupVersion(storage map[string]RESTStorage, codec runtime.Codec, canonicalPrefix string, selfLinker runtime.SelfLinker) *APIGroupVersion {
|
|
|
|
return &APIGroupVersion{RESTHandler{
|
2014-09-26 00:20:28 +00:00
|
|
|
storage: storage,
|
|
|
|
codec: codec,
|
|
|
|
canonicalPrefix: canonicalPrefix,
|
|
|
|
selfLinker: selfLinker,
|
|
|
|
ops: NewOperations(),
|
2014-07-29 13:40:40 +00:00
|
|
|
// Delay just long enough to handle most simple write operations
|
|
|
|
asyncOpWait: time.Millisecond * 25,
|
2014-08-09 21:12:55 +00:00
|
|
|
}}
|
|
|
|
}
|
2014-08-06 17:06:42 +00:00
|
|
|
|
2014-11-11 07:11:45 +00:00
|
|
|
// This magic incantation returns *ptrToObject for an arbitrary pointer
|
|
|
|
func indirectArbitraryPointer(ptrToObject interface{}) interface{} {
|
|
|
|
return reflect.Indirect(reflect.ValueOf(ptrToObject)).Interface()
|
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
2014-11-02 20:52:31 +00:00
|
|
|
if err != nil {
|
2014-11-11 07:11:45 +00:00
|
|
|
glog.Warningf("error getting kind: %v\n", err)
|
2014-11-02 20:52:31 +00:00
|
|
|
return
|
|
|
|
}
|
2014-11-11 07:11:45 +00:00
|
|
|
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")))
|
2014-11-02 20:52:31 +00:00
|
|
|
}
|
|
|
|
|
2014-11-11 07:11:45 +00:00
|
|
|
// 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)
|
2014-08-09 21:12:55 +00:00
|
|
|
restHandler := &g.handler
|
2014-11-11 07:11:45 +00:00
|
|
|
strippedHandler := http.StripPrefix(prefix, restHandler)
|
2014-11-06 01:22:18 +00:00
|
|
|
watchHandler := &WatchHandler{
|
|
|
|
storage: g.handler.storage,
|
|
|
|
codec: g.handler.codec,
|
|
|
|
canonicalPrefix: g.handler.canonicalPrefix,
|
|
|
|
selfLinker: g.handler.selfLinker,
|
|
|
|
}
|
2014-11-11 07:11:45 +00:00
|
|
|
proxyHandler := &ProxyHandler{prefix + "/proxy/", g.handler.storage, g.handler.codec}
|
2014-08-25 21:36:15 +00:00
|
|
|
redirectHandler := &RedirectHandler{g.handler.storage, g.handler.codec}
|
2014-08-09 21:12:55 +00:00
|
|
|
opHandler := &OperationHandler{g.handler.ops, g.handler.codec}
|
|
|
|
|
2014-11-11 07:11:45 +00:00
|
|
|
// 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)
|
2014-08-09 21:12:55 +00:00
|
|
|
}
|
|
|
|
}
|
2014-07-22 20:09:22 +00:00
|
|
|
|
2014-11-11 07:11:45 +00:00
|
|
|
// 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))
|
2014-06-06 23:40:48 +00:00
|
|
|
}
|
|
|
|
|
2014-10-09 23:26:34 +00:00
|
|
|
// InstallLogsSupport registers the APIServer log support function into a mux.
|
2014-10-23 20:56:18 +00:00
|
|
|
func InstallLogsSupport(mux Mux) {
|
2014-11-11 07:11:45 +00:00
|
|
|
// TODO: use restful: ws.Route(ws.GET("/logs/{logpath:*}").To(fileHandler))
|
|
|
|
// See github.com/emicklei/go-restful/blob/master/examples/restful-serve-static.go
|
2014-10-09 23:26:34 +00:00
|
|
|
mux.Handle("/logs/", http.StripPrefix("/logs/", http.FileServer(http.Dir("/var/log/"))))
|
|
|
|
}
|
|
|
|
|
2014-09-02 10:00:28 +00:00
|
|
|
// handleVersion writes the server's version information.
|
2014-11-11 07:11:45 +00:00
|
|
|
func handleVersion(req *restful.Request, resp *restful.Response) {
|
|
|
|
// TODO: use restful's Response methods
|
|
|
|
writeRawJSON(http.StatusOK, version.Get(), resp.ResponseWriter)
|
2014-07-29 21:36:41 +00:00
|
|
|
}
|
|
|
|
|
2014-10-29 00:20:40 +00:00
|
|
|
// APIVersionHandler returns a handler which will list the provided versions as available.
|
2014-11-11 07:11:45 +00:00
|
|
|
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)
|
|
|
|
}
|
2014-10-29 00:20:40 +00:00
|
|
|
}
|
|
|
|
|
2014-09-02 10:00:28 +00:00
|
|
|
// writeJSON renders an object as JSON to the response.
|
2014-09-06 02:22:03 +00:00
|
|
|
func writeJSON(statusCode int, codec runtime.Codec, object runtime.Object, w http.ResponseWriter) {
|
2014-08-06 03:10:48 +00:00
|
|
|
output, err := codec.Encode(object)
|
2014-07-29 22:14:00 +00:00
|
|
|
if err != nil {
|
2014-11-11 07:11:45 +00:00
|
|
|
// Note: If codec is broken, this results in an infinite recursion
|
2014-07-31 18:26:34 +00:00
|
|
|
errorJSON(err, codec, w)
|
2014-07-29 22:14:00 +00:00
|
|
|
return
|
|
|
|
}
|
2014-07-29 22:54:20 +00:00
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
w.WriteHeader(statusCode)
|
2014-07-29 22:14:00 +00:00
|
|
|
w.Write(output)
|
|
|
|
}
|
|
|
|
|
2014-09-02 10:00:28 +00:00
|
|
|
// errorJSON renders an error to the response.
|
2014-09-06 02:22:03 +00:00
|
|
|
func errorJSON(err error, codec runtime.Codec, w http.ResponseWriter) {
|
2014-07-31 18:26:34 +00:00
|
|
|
status := errToAPIStatus(err)
|
|
|
|
writeJSON(status.Code, codec, status, w)
|
|
|
|
}
|
|
|
|
|
2014-07-29 22:14:00 +00:00
|
|
|
// writeRawJSON writes a non-API object in JSON.
|
|
|
|
func writeRawJSON(statusCode int, object interface{}, w http.ResponseWriter) {
|
|
|
|
output, err := json.Marshal(object)
|
|
|
|
if err != nil {
|
2014-07-31 18:26:34 +00:00
|
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
2014-07-29 22:14:00 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
w.WriteHeader(statusCode)
|
|
|
|
w.Write(output)
|
|
|
|
}
|
|
|
|
|
2014-07-29 22:13:02 +00:00
|
|
|
func parseTimeout(str string) time.Duration {
|
|
|
|
if str != "" {
|
|
|
|
timeout, err := time.ParseDuration(str)
|
|
|
|
if err == nil {
|
|
|
|
return timeout
|
|
|
|
}
|
|
|
|
glog.Errorf("Failed to parse: %#v '%s'", err, str)
|
|
|
|
}
|
|
|
|
return 30 * time.Second
|
|
|
|
}
|
|
|
|
|
2014-07-29 22:10:29 +00:00
|
|
|
func readBody(req *http.Request) ([]byte, error) {
|
|
|
|
defer req.Body.Close()
|
|
|
|
return ioutil.ReadAll(req.Body)
|
|
|
|
}
|
2014-08-06 17:06:42 +00:00
|
|
|
|
2014-09-02 10:00:28 +00:00
|
|
|
// splitPath returns the segments for a URL path.
|
2014-08-06 17:06:42 +00:00
|
|
|
func splitPath(path string) []string {
|
|
|
|
path = strings.Trim(path, "/")
|
|
|
|
if path == "" {
|
|
|
|
return []string{}
|
|
|
|
}
|
|
|
|
return strings.Split(path, "/")
|
|
|
|
}
|