Merge pull request #5851 from smarterclayton/support_input_streams

Support input streams being returned by the APIserver
This commit is contained in:
Derek Carr 2015-03-24 22:41:24 -04:00
commit cfb6f1199b
17 changed files with 722 additions and 137 deletions

View File

@ -26,6 +26,7 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta2"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta3"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
)
// Version is the string that represents the current external default version.
@ -122,9 +123,15 @@ func init() {
"Namespace": true,
}
// these kinds should be excluded from the list of resources
ignoredKinds := util.NewStringSet("ListOptions", "DeleteOptions", "Status", "ContainerManifest")
// enumerate all supported versions, get the kinds, and register with the mapper how to address our resources
for _, version := range versions {
for kind := range api.Scheme.KnownTypes(version) {
if ignoredKinds.Has(kind) {
continue
}
mixedCase, found := versionMixedCase[version]
if !found {
mixedCase = false

View File

@ -17,6 +17,7 @@ limitations under the License.
package rest
import (
"io"
"net/http"
"net/url"
@ -148,3 +149,21 @@ type Redirector interface {
// ResourceLocation should return the remote location of the given resource, and an optional transport to use to request it, or an error.
ResourceLocation(ctx api.Context, id string) (remoteLocation *url.URL, transport http.RoundTripper, err error)
}
// ResourceStreamer is an interface implemented by objects that prefer to be streamed from the server
// instead of decoded directly.
type ResourceStreamer interface {
// InputStream should return an io.Reader if the provided object supports streaming. The desired
// api version and a accept header (may be empty) are passed to the call. If no error occurs,
// the caller may return a content type string with the reader that indicates the type of the
// stream.
InputStream(apiVersion, acceptHeader string) (io.ReadCloser, string, error)
}
// StorageMetadata is an optional interface that callers can implement to provide additional
// information about their Storage objects.
type StorageMetadata interface {
// ProducesMIMETypes returns a list of the MIME types the specified HTTP verb (GET, POST, DELETE,
// PATCH) can respond with.
ProducesMIMETypes(verb string) []string
}

View File

@ -147,6 +147,10 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
patcher, isPatcher := storage.(rest.Patcher)
_, isWatcher := storage.(rest.Watcher)
_, isRedirector := storage.(rest.Redirector)
storageMeta, isMetadata := storage.(rest.StorageMetadata)
if !isMetadata {
storageMeta = defaultStorageMetadata{}
}
var versionedDeleterObject runtime.Object
switch {
@ -283,56 +287,70 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
//
// test/integration/auth_test.go is currently the most comprehensive status code test
reqScope := RequestScope{
ContextFunc: ctxFn,
Codec: mapping.Codec,
APIVersion: a.group.Version,
Resource: resource,
Kind: kind,
}
for _, action := range actions {
reqScope.Namer = action.Namer
m := monitorFilter(action.Verb, resource)
switch action.Verb {
case "GET": // Get a resource.
route := ws.GET(action.Path).To(GetResource(getter, ctxFn, action.Namer, mapping.Codec)).
route := ws.GET(action.Path).To(GetResource(getter, reqScope)).
Filter(m).
Doc("read the specified " + kind).
Operation("read" + kind).
Produces(append(storageMeta.ProducesMIMETypes(action.Verb), "application/json")...).
Writes(versionedObject)
addParams(route, action.Params)
ws.Route(route)
case "LIST": // List all resources of a kind.
route := ws.GET(action.Path).To(ListResource(lister, ctxFn, action.Namer, mapping.Codec, a.group.Version, resource)).
route := ws.GET(action.Path).To(ListResource(lister, reqScope)).
Filter(m).
Doc("list objects of kind " + kind).
Operation("list" + kind).
Produces("application/json").
Writes(versionedList)
addParams(route, action.Params)
ws.Route(route)
case "PUT": // Update a resource.
route := ws.PUT(action.Path).To(UpdateResource(updater, ctxFn, action.Namer, mapping.Codec, a.group.Typer, resource, admit)).
route := ws.PUT(action.Path).To(UpdateResource(updater, reqScope, a.group.Typer, admit)).
Filter(m).
Doc("replace the specified " + kind).
Operation("replace" + kind).
Produces(append(storageMeta.ProducesMIMETypes(action.Verb), "application/json")...).
Reads(versionedObject)
addParams(route, action.Params)
ws.Route(route)
case "PATCH": // Partially update a resource
route := ws.PATCH(action.Path).To(PatchResource(patcher, ctxFn, action.Namer, mapping.Codec, a.group.Typer, resource, admit)).
route := ws.PATCH(action.Path).To(PatchResource(patcher, reqScope, a.group.Typer, admit)).
Filter(m).
Doc("partially update the specified " + kind).
// TODO: toggle patch strategy by content type
// Consumes("application/merge-patch+json", "application/json-patch+json").
Operation("patch" + kind).
Produces(append(storageMeta.ProducesMIMETypes(action.Verb), "application/json")...).
Reads(versionedObject)
addParams(route, action.Params)
ws.Route(route)
case "POST": // Create a resource.
route := ws.POST(action.Path).To(CreateResource(creater, ctxFn, action.Namer, mapping.Codec, a.group.Typer, resource, admit)).
route := ws.POST(action.Path).To(CreateResource(creater, reqScope, a.group.Typer, admit)).
Filter(m).
Doc("create a " + kind).
Operation("create" + kind).
Produces(append(storageMeta.ProducesMIMETypes(action.Verb), "application/json")...).
Reads(versionedObject)
addParams(route, action.Params)
ws.Route(route)
case "DELETE": // Delete a resource.
route := ws.DELETE(action.Path).To(DeleteResource(gracefulDeleter, isGracefulDeleter, ctxFn, action.Namer, mapping.Codec, resource, kind, admit)).
route := ws.DELETE(action.Path).To(DeleteResource(gracefulDeleter, isGracefulDeleter, reqScope, admit)).
Filter(m).
Doc("delete a " + kind).
Operation("delete" + kind)
Operation("delete" + kind).
Produces(append(storageMeta.ProducesMIMETypes(action.Verb), "application/json")...)
if isGracefulDeleter {
route.Reads(versionedDeleterObject)
}
@ -343,6 +361,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
Filter(m).
Doc("watch a particular " + kind).
Operation("watch" + kind).
Produces("application/json").
Writes(versionedObject)
addParams(route, action.Params)
ws.Route(route)
@ -351,6 +370,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
Filter(m).
Doc("watch a list of " + kind).
Operation("watch" + kind + "list").
Produces("application/json").
Writes(versionedList)
addParams(route, action.Params)
ws.Route(route)
@ -630,3 +650,13 @@ func addParams(route *restful.RouteBuilder, params []*restful.Parameter) {
route.Param(param)
}
}
// defaultStorageMetadata provides default answers to rest.StorageMetadata.
type defaultStorageMetadata struct{}
// defaultStorageMetadata implements rest.StorageMetadata
var _ rest.StorageMetadata = defaultStorageMetadata{}
func (defaultStorageMetadata) ProducesMIMETypes(verb string) []string {
return nil
}

View File

@ -20,6 +20,7 @@ import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"path"
@ -196,6 +197,26 @@ func APIVersionHandler(versions ...string) restful.RouteFunction {
}
}
// write renders a returned runtime.Object to the response as a stream or an encoded object.
func write(statusCode int, apiVersion string, codec runtime.Codec, object runtime.Object, w http.ResponseWriter, req *http.Request) {
if stream, ok := object.(rest.ResourceStreamer); ok {
out, contentType, err := stream.InputStream(apiVersion, req.Header.Get("Accept"))
if err != nil {
errorJSONFatal(err, codec, w)
return
}
defer out.Close()
if len(contentType) == 0 {
contentType = "application/octet-stream"
}
w.Header().Set("Content-Type", contentType)
w.WriteHeader(statusCode)
io.Copy(w, out)
return
}
writeJSON(statusCode, codec, object, w)
}
// 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)

View File

@ -21,6 +21,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
@ -122,7 +123,8 @@ func init() {
// defaultAPIServer exposes nested objects for testability.
type defaultAPIServer struct {
http.Handler
group *APIGroupVersion
group *APIGroupVersion
container *restful.Container
}
// uses the default settings
@ -169,7 +171,7 @@ func handleInternal(storage map[string]rest.Storage, admissionControl admission.
ws := new(restful.WebService)
InstallSupport(mux, ws)
container.Add(ws)
return &defaultAPIServer{mux, group}
return &defaultAPIServer{mux, group, container}
}
type Simple struct {
@ -212,6 +214,8 @@ type SimpleRESTStorage struct {
updated *Simple
created *Simple
stream *SimpleStream
deleted string
deleteOptions *api.DeleteOptions
@ -243,8 +247,34 @@ func (storage *SimpleRESTStorage) List(ctx api.Context, label labels.Selector, f
return result, storage.errors["list"]
}
type SimpleStream struct {
version string
accept string
contentType string
err error
io.Reader
closed bool
}
func (s *SimpleStream) Close() error {
s.closed = true
return nil
}
func (s *SimpleStream) IsAnAPIObject() {}
func (s *SimpleStream) InputStream(version, accept string) (io.ReadCloser, string, error) {
s.version = version
s.accept = accept
return s, s.contentType, s.err
}
func (storage *SimpleRESTStorage) Get(ctx api.Context, id string) (runtime.Object, error) {
storage.checkContext(ctx)
if id == "binary" {
return storage.stream, storage.errors["get"]
}
return api.Scheme.CopyOrDie(&storage.item), storage.errors["get"]
}
@ -343,6 +373,15 @@ func (storage LegacyRESTStorage) Delete(ctx api.Context, id string) (runtime.Obj
return storage.SimpleRESTStorage.Delete(ctx, id, nil)
}
type MetadataRESTStorage struct {
*SimpleRESTStorage
types []string
}
func (m *MetadataRESTStorage) ProducesMIMETypes(method string) []string {
return m.types
}
func extractBody(response *http.Response, object runtime.Object) (string, error) {
defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body)
@ -646,6 +685,26 @@ func TestSelfLinkSkipsEmptyName(t *testing.T) {
}
}
func TestMetadata(t *testing.T) {
simpleStorage := &MetadataRESTStorage{&SimpleRESTStorage{}, []string{"text/plain"}}
h := handle(map[string]rest.Storage{"simple": simpleStorage})
ws := h.(*defaultAPIServer).container.RegisteredWebServices()
if len(ws) == 0 {
t.Fatal("no web services registered")
}
matches := map[string]int{}
for _, w := range ws {
for _, r := range w.Routes() {
s := strings.Join(r.Produces, ",")
i := matches[s]
matches[s] = i + 1
}
}
if matches["text/plain,application/json"] == 0 || matches["application/json"] == 0 || matches["*/*"] == 0 || len(matches) != 3 {
t.Errorf("unexpected mime types: %v", matches)
}
}
func TestGet(t *testing.T) {
storage := map[string]rest.Storage{}
simpleStorage := SimpleRESTStorage{
@ -685,6 +744,36 @@ func TestGet(t *testing.T) {
}
}
func TestGetBinary(t *testing.T) {
simpleStorage := SimpleRESTStorage{
stream: &SimpleStream{
contentType: "text/plain",
Reader: bytes.NewBufferString("response data"),
},
}
stream := simpleStorage.stream
server := httptest.NewServer(handle(map[string]rest.Storage{"simple": &simpleStorage}))
defer server.Close()
req, _ := http.NewRequest("GET", server.URL+"/api/version/simple/binary", nil)
req.Header.Add("Accept", "text/other, */*")
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Fatalf("unexpected response: %#v", resp)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if !stream.closed || stream.version != "version" || stream.accept != "text/other, */*" ||
resp.Header.Get("Content-Type") != stream.contentType || string(body) != "response data" {
t.Errorf("unexpected stream: %#v", stream)
}
}
func TestGetAlternateSelfLink(t *testing.T) {
storage := map[string]rest.Storage{}
simpleStorage := SimpleRESTStorage{

View File

@ -59,28 +59,38 @@ type ScopeNamer interface {
GenerateListLink(req *restful.Request) (path, query string, err error)
}
// RequestScope encapsulates common fields across all RESTful handler methods.
type RequestScope struct {
Namer ScopeNamer
ContextFunc
runtime.Codec
Resource string
Kind string
APIVersion string
}
// GetResource returns a function that handles retrieving a single resource from a rest.Storage object.
func GetResource(r rest.Getter, ctxFn ContextFunc, namer ScopeNamer, codec runtime.Codec) restful.RouteFunction {
func GetResource(r rest.Getter, scope RequestScope) restful.RouteFunction {
return func(req *restful.Request, res *restful.Response) {
w := res.ResponseWriter
namespace, name, err := namer.Name(req)
namespace, name, err := scope.Namer.Name(req)
if err != nil {
errorJSON(err, codec, w)
errorJSON(err, scope.Codec, w)
return
}
ctx := ctxFn(req)
ctx := scope.ContextFunc(req)
ctx = api.WithNamespace(ctx, namespace)
result, err := r.Get(ctx, name)
if err != nil {
errorJSON(err, codec, w)
errorJSON(err, scope.Codec, w)
return
}
if err := setSelfLink(result, req, namer); err != nil {
errorJSON(err, codec, w)
if err := setSelfLink(result, req, scope.Namer); err != nil {
errorJSON(err, scope.Codec, w)
return
}
writeJSON(http.StatusOK, codec, result, w)
write(http.StatusOK, scope.APIVersion, scope.Codec, result, w, req.Request)
}
}
@ -103,69 +113,69 @@ func parseSelectorQueryParams(query url.Values, version, apiResource string) (la
}
// ListResource returns a function that handles retrieving a list of resources from a rest.Storage object.
func ListResource(r rest.Lister, ctxFn ContextFunc, namer ScopeNamer, codec runtime.Codec, version, apiResource string) restful.RouteFunction {
func ListResource(r rest.Lister, scope RequestScope) restful.RouteFunction {
return func(req *restful.Request, res *restful.Response) {
w := res.ResponseWriter
namespace, err := namer.Namespace(req)
namespace, err := scope.Namer.Namespace(req)
if err != nil {
errorJSON(err, codec, w)
errorJSON(err, scope.Codec, w)
return
}
ctx := ctxFn(req)
ctx := scope.ContextFunc(req)
ctx = api.WithNamespace(ctx, namespace)
label, field, err := parseSelectorQueryParams(req.Request.URL.Query(), version, apiResource)
label, field, err := parseSelectorQueryParams(req.Request.URL.Query(), scope.APIVersion, scope.Resource)
if err != nil {
errorJSON(err, codec, w)
errorJSON(err, scope.Codec, w)
return
}
result, err := r.List(ctx, label, field)
if err != nil {
errorJSON(err, codec, w)
errorJSON(err, scope.Codec, w)
return
}
if err := setListSelfLink(result, req, namer); err != nil {
errorJSON(err, codec, w)
if err := setListSelfLink(result, req, scope.Namer); err != nil {
errorJSON(err, scope.Codec, w)
return
}
writeJSON(http.StatusOK, codec, result, w)
write(http.StatusOK, scope.APIVersion, scope.Codec, result, w, req.Request)
}
}
// CreateResource returns a function that will handle a resource creation.
func CreateResource(r rest.Creater, ctxFn ContextFunc, namer ScopeNamer, codec runtime.Codec, typer runtime.ObjectTyper, resource string, admit admission.Interface) restful.RouteFunction {
func CreateResource(r rest.Creater, scope RequestScope, typer runtime.ObjectTyper, admit admission.Interface) restful.RouteFunction {
return func(req *restful.Request, res *restful.Response) {
w := res.ResponseWriter
// TODO: we either want to remove timeout or document it (if we document, move timeout out of this function and declare it in api_installer)
timeout := parseTimeout(req.Request.URL.Query().Get("timeout"))
namespace, err := namer.Namespace(req)
namespace, err := scope.Namer.Namespace(req)
if err != nil {
errorJSON(err, codec, w)
errorJSON(err, scope.Codec, w)
return
}
ctx := ctxFn(req)
ctx := scope.ContextFunc(req)
ctx = api.WithNamespace(ctx, namespace)
body, err := readBody(req.Request)
if err != nil {
errorJSON(err, codec, w)
errorJSON(err, scope.Codec, w)
return
}
obj := r.New()
if err := codec.DecodeInto(body, obj); err != nil {
if err := scope.Codec.DecodeInto(body, obj); err != nil {
err = transformDecodeError(typer, err, obj, body)
errorJSON(err, codec, w)
errorJSON(err, scope.Codec, w)
return
}
err = admit.Admit(admission.NewAttributesRecord(obj, namespace, resource, "CREATE"))
err = admit.Admit(admission.NewAttributesRecord(obj, namespace, scope.Resource, "CREATE"))
if err != nil {
errorJSON(err, codec, w)
errorJSON(err, scope.Codec, w)
return
}
@ -177,73 +187,73 @@ func CreateResource(r rest.Creater, ctxFn ContextFunc, namer ScopeNamer, codec r
return out, err
})
if err != nil {
errorJSON(err, codec, w)
errorJSON(err, scope.Codec, w)
return
}
if err := setSelfLink(result, req, namer); err != nil {
errorJSON(err, codec, w)
if err := setSelfLink(result, req, scope.Namer); err != nil {
errorJSON(err, scope.Codec, w)
return
}
writeJSON(http.StatusCreated, codec, result, w)
write(http.StatusCreated, scope.APIVersion, scope.Codec, result, w, req.Request)
}
}
// PatchResource returns a function that will handle a resource patch
// TODO: Eventually PatchResource should just use AtomicUpdate and this routine should be a bit cleaner
func PatchResource(r rest.Patcher, ctxFn ContextFunc, namer ScopeNamer, codec runtime.Codec, typer runtime.ObjectTyper, resource string, admit admission.Interface) restful.RouteFunction {
func PatchResource(r rest.Patcher, scope RequestScope, typer runtime.ObjectTyper, admit admission.Interface) restful.RouteFunction {
return func(req *restful.Request, res *restful.Response) {
w := res.ResponseWriter
// TODO: we either want to remove timeout or document it (if we document, move timeout out of this function and declare it in api_installer)
timeout := parseTimeout(req.Request.URL.Query().Get("timeout"))
namespace, name, err := namer.Name(req)
namespace, name, err := scope.Namer.Name(req)
if err != nil {
errorJSON(err, codec, w)
errorJSON(err, scope.Codec, w)
return
}
obj := r.New()
// PATCH requires same permission as UPDATE
err = admit.Admit(admission.NewAttributesRecord(obj, namespace, resource, "UPDATE"))
err = admit.Admit(admission.NewAttributesRecord(obj, namespace, scope.Resource, "UPDATE"))
if err != nil {
errorJSON(err, codec, w)
errorJSON(err, scope.Codec, w)
return
}
ctx := ctxFn(req)
ctx := scope.ContextFunc(req)
ctx = api.WithNamespace(ctx, namespace)
original, err := r.Get(ctx, name)
if err != nil {
errorJSON(err, codec, w)
errorJSON(err, scope.Codec, w)
return
}
originalObjJs, err := codec.Encode(original)
originalObjJs, err := scope.Codec.Encode(original)
if err != nil {
errorJSON(err, codec, w)
errorJSON(err, scope.Codec, w)
return
}
patchJs, err := readBody(req.Request)
if err != nil {
errorJSON(err, codec, w)
errorJSON(err, scope.Codec, w)
return
}
patchedObjJs, err := jsonpatch.MergePatch(originalObjJs, patchJs)
if err != nil {
errorJSON(err, codec, w)
errorJSON(err, scope.Codec, w)
return
}
if err := codec.DecodeInto(patchedObjJs, obj); err != nil {
errorJSON(err, codec, w)
if err := scope.Codec.DecodeInto(patchedObjJs, obj); err != nil {
errorJSON(err, scope.Codec, w)
return
}
if err := checkName(obj, name, namespace, namer); err != nil {
errorJSON(err, codec, w)
if err := checkName(obj, name, namespace, scope.Namer); err != nil {
errorJSON(err, scope.Codec, w)
return
}
@ -253,56 +263,56 @@ func PatchResource(r rest.Patcher, ctxFn ContextFunc, namer ScopeNamer, codec ru
return obj, err
})
if err != nil {
errorJSON(err, codec, w)
errorJSON(err, scope.Codec, w)
return
}
if err := setSelfLink(result, req, namer); err != nil {
errorJSON(err, codec, w)
if err := setSelfLink(result, req, scope.Namer); err != nil {
errorJSON(err, scope.Codec, w)
return
}
writeJSON(http.StatusOK, codec, result, w)
write(http.StatusOK, scope.APIVersion, scope.Codec, result, w, req.Request)
}
}
// UpdateResource returns a function that will handle a resource update
func UpdateResource(r rest.Updater, ctxFn ContextFunc, namer ScopeNamer, codec runtime.Codec, typer runtime.ObjectTyper, resource string, admit admission.Interface) restful.RouteFunction {
func UpdateResource(r rest.Updater, scope RequestScope, typer runtime.ObjectTyper, admit admission.Interface) restful.RouteFunction {
return func(req *restful.Request, res *restful.Response) {
w := res.ResponseWriter
// TODO: we either want to remove timeout or document it (if we document, move timeout out of this function and declare it in api_installer)
timeout := parseTimeout(req.Request.URL.Query().Get("timeout"))
namespace, name, err := namer.Name(req)
namespace, name, err := scope.Namer.Name(req)
if err != nil {
errorJSON(err, codec, w)
errorJSON(err, scope.Codec, w)
return
}
ctx := ctxFn(req)
ctx := scope.ContextFunc(req)
ctx = api.WithNamespace(ctx, namespace)
body, err := readBody(req.Request)
if err != nil {
errorJSON(err, codec, w)
errorJSON(err, scope.Codec, w)
return
}
obj := r.New()
if err := codec.DecodeInto(body, obj); err != nil {
if err := scope.Codec.DecodeInto(body, obj); err != nil {
err = transformDecodeError(typer, err, obj, body)
errorJSON(err, codec, w)
errorJSON(err, scope.Codec, w)
return
}
if err := checkName(obj, name, namespace, namer); err != nil {
errorJSON(err, codec, w)
if err := checkName(obj, name, namespace, scope.Namer); err != nil {
errorJSON(err, scope.Codec, w)
return
}
err = admit.Admit(admission.NewAttributesRecord(obj, namespace, resource, "UPDATE"))
err = admit.Admit(admission.NewAttributesRecord(obj, namespace, scope.Resource, "UPDATE"))
if err != nil {
errorJSON(err, codec, w)
errorJSON(err, scope.Codec, w)
return
}
@ -313,12 +323,12 @@ func UpdateResource(r rest.Updater, ctxFn ContextFunc, namer ScopeNamer, codec r
return obj, err
})
if err != nil {
errorJSON(err, codec, w)
errorJSON(err, scope.Codec, w)
return
}
if err := setSelfLink(result, req, namer); err != nil {
errorJSON(err, codec, w)
if err := setSelfLink(result, req, scope.Namer); err != nil {
errorJSON(err, scope.Codec, w)
return
}
@ -326,46 +336,44 @@ func UpdateResource(r rest.Updater, ctxFn ContextFunc, namer ScopeNamer, codec r
if wasCreated {
status = http.StatusCreated
}
writeJSON(status, codec, result, w)
writeJSON(status, scope.Codec, result, w)
}
}
// DeleteResource returns a function that will handle a resource deletion
func DeleteResource(r rest.GracefulDeleter, checkBody bool, ctxFn ContextFunc, namer ScopeNamer, codec runtime.Codec, resource, kind string, admit admission.Interface) restful.RouteFunction {
func DeleteResource(r rest.GracefulDeleter, checkBody bool, scope RequestScope, admit admission.Interface) restful.RouteFunction {
return func(req *restful.Request, res *restful.Response) {
w := res.ResponseWriter
// TODO: we either want to remove timeout or document it (if we document, move timeout out of this function and declare it in api_installer)
timeout := parseTimeout(req.Request.URL.Query().Get("timeout"))
namespace, name, err := namer.Name(req)
namespace, name, err := scope.Namer.Name(req)
if err != nil {
errorJSON(err, codec, w)
errorJSON(err, scope.Codec, w)
return
}
ctx := ctxFn(req)
if len(namespace) > 0 {
ctx = api.WithNamespace(ctx, namespace)
}
ctx := scope.ContextFunc(req)
ctx = api.WithNamespace(ctx, namespace)
options := &api.DeleteOptions{}
if checkBody {
body, err := readBody(req.Request)
if err != nil {
errorJSON(err, codec, w)
errorJSON(err, scope.Codec, w)
return
}
if len(body) > 0 {
if err := codec.DecodeInto(body, options); err != nil {
errorJSON(err, codec, w)
if err := scope.Codec.DecodeInto(body, options); err != nil {
errorJSON(err, scope.Codec, w)
return
}
}
}
err = admit.Admit(admission.NewAttributesRecord(nil, namespace, resource, "DELETE"))
err = admit.Admit(admission.NewAttributesRecord(nil, namespace, scope.Resource, "DELETE"))
if err != nil {
errorJSON(err, codec, w)
errorJSON(err, scope.Codec, w)
return
}
@ -373,7 +381,7 @@ func DeleteResource(r rest.GracefulDeleter, checkBody bool, ctxFn ContextFunc, n
return r.Delete(ctx, name, options)
})
if err != nil {
errorJSON(err, codec, w)
errorJSON(err, scope.Codec, w)
return
}
@ -385,19 +393,19 @@ func DeleteResource(r rest.GracefulDeleter, checkBody bool, ctxFn ContextFunc, n
Code: http.StatusOK,
Details: &api.StatusDetails{
ID: name,
Kind: kind,
Kind: scope.Kind,
},
}
} else {
// when a non-status response is returned, set the self link
if _, ok := result.(*api.Status); !ok {
if err := setSelfLink(result, req, namer); err != nil {
errorJSON(err, codec, w)
if err := setSelfLink(result, req, scope.Namer); err != nil {
errorJSON(err, scope.Codec, w)
return
}
}
}
writeJSON(http.StatusOK, codec, result, w)
write(http.StatusOK, scope.APIVersion, scope.Codec, result, w, req.Request)
}
}
@ -449,11 +457,8 @@ func transformDecodeError(typer runtime.ObjectTyper, baseErr error, into runtime
func setSelfLink(obj runtime.Object, req *restful.Request, namer ScopeNamer) error {
// TODO: SelfLink generation should return a full URL?
path, query, err := namer.GenerateLink(req, obj)
if err == errEmptyName {
return nil
}
if err != nil {
return err
return nil
}
newURL := *req.Request.URL

View File

@ -366,7 +366,7 @@ func TestWatchControllers(t *testing.T) {
select {
case <-received:
case <-time.After(10 * time.Millisecond):
case <-time.After(100 * time.Millisecond):
t.Errorf("Expected 1 call but got 0")
}
}

View File

@ -54,6 +54,12 @@ type Converter struct {
// Map from a type to a function which applies defaults.
defaultingFuncs map[reflect.Type]reflect.Value
// Map from an input type to a function which can apply a key name mapping
inputFieldMappingFuncs map[reflect.Type]FieldMappingFunc
// Map from an input type to a set of default conversion flags.
inputDefaultFlags map[reflect.Type]FieldMatchingFlags
// If non-nil, will be called to print helpful debugging info. Quite verbose.
Debug DebugLogger
@ -71,6 +77,9 @@ func NewConverter() *Converter {
nameFunc: func(t reflect.Type) string { return t.Name() },
structFieldDests: map[typeNamePair][]typeNamePair{},
structFieldSources: map[typeNamePair][]typeNamePair{},
inputFieldMappingFuncs: map[reflect.Type]FieldMappingFunc{},
inputDefaultFlags: map[reflect.Type]FieldMatchingFlags{},
}
}
@ -98,12 +107,18 @@ type Scope interface {
Meta() *Meta
}
// FieldMappingFunc can convert an input field value into different values, depending on
// the value of the source or destination struct tags.
type FieldMappingFunc func(key string, sourceTag, destTag reflect.StructTag) (source string, dest string)
// Meta is supplied by Scheme, when it calls Convert.
type Meta struct {
SrcVersion string
DestVersion string
// TODO: If needed, add a user data field here.
// KeyNameMapping is an optional function which may map the listed key (field name)
// into a source and destination value.
KeyNameMapping FieldMappingFunc
}
// scope contains information about an ongoing conversion.
@ -301,6 +316,21 @@ func (c *Converter) RegisterDefaultingFunc(defaultingFunc interface{}) error {
return nil
}
// RegisterInputDefaults registers a field name mapping function, used when converting
// from maps to structs. Inputs to the conversion methods are checked for this type and a mapping
// applied automatically if the input matches in. A set of default flags for the input conversion
// may also be provided, which will be used when no explicit flags are requested.
func (c *Converter) RegisterInputDefaults(in interface{}, fn FieldMappingFunc, defaultFlags FieldMatchingFlags) error {
fv := reflect.ValueOf(in)
ft := fv.Type()
if ft.Kind() != reflect.Ptr {
return fmt.Errorf("expected pointer 'in' argument, got: %v", ft)
}
c.inputFieldMappingFuncs[ft] = fn
c.inputDefaultFlags[ft] = defaultFlags
return nil
}
// FieldMatchingFlags contains a list of ways in which struct fields could be
// copied. These constants may be | combined.
type FieldMatchingFlags int
@ -538,10 +568,16 @@ func (c *Converter) defaultConvert(sv, dv reflect.Value, scope *scope) error {
return nil
}
var stringType = reflect.TypeOf("")
func toKVValue(v reflect.Value) kvValue {
switch v.Kind() {
case reflect.Struct:
return structAdaptor(v)
case reflect.Map:
if v.Type().Key().AssignableTo(stringType) {
return stringMapAdaptor(v)
}
}
return nil
@ -561,15 +597,48 @@ type kvValue interface {
confirmSet(key string, v reflect.Value) bool
}
type stringMapAdaptor reflect.Value
func (a stringMapAdaptor) len() int {
return reflect.Value(a).Len()
}
func (a stringMapAdaptor) keys() []string {
v := reflect.Value(a)
keys := make([]string, v.Len())
for i, v := range v.MapKeys() {
if v.IsNil() {
continue
}
switch t := v.Interface().(type) {
case string:
keys[i] = t
}
}
return keys
}
func (a stringMapAdaptor) tagOf(key string) reflect.StructTag {
return ""
}
func (a stringMapAdaptor) value(key string) reflect.Value {
return reflect.Value(a).MapIndex(reflect.ValueOf(key))
}
func (a stringMapAdaptor) confirmSet(key string, v reflect.Value) bool {
return true
}
type structAdaptor reflect.Value
func (sa structAdaptor) len() int {
v := reflect.Value(sa)
func (a structAdaptor) len() int {
v := reflect.Value(a)
return v.Type().NumField()
}
func (sa structAdaptor) keys() []string {
v := reflect.Value(sa)
func (a structAdaptor) keys() []string {
v := reflect.Value(a)
t := v.Type()
keys := make([]string, t.NumField())
for i := range keys {
@ -578,8 +647,8 @@ func (sa structAdaptor) keys() []string {
return keys
}
func (sa structAdaptor) tagOf(key string) reflect.StructTag {
v := reflect.Value(sa)
func (a structAdaptor) tagOf(key string) reflect.StructTag {
v := reflect.Value(a)
field, ok := v.Type().FieldByName(key)
if ok {
return field.Tag
@ -587,12 +656,12 @@ func (sa structAdaptor) tagOf(key string) reflect.StructTag {
return ""
}
func (sa structAdaptor) value(key string) reflect.Value {
v := reflect.Value(sa)
func (a structAdaptor) value(key string) reflect.Value {
v := reflect.Value(a)
return v.FieldByName(key)
}
func (sa structAdaptor) confirmSet(key string, v reflect.Value) bool {
func (a structAdaptor) confirmSet(key string, v reflect.Value) bool {
return true
}
@ -608,6 +677,12 @@ func (c *Converter) convertKV(skv, dkv kvValue, scope *scope) error {
if scope.flags.IsSet(SourceToDest) {
lister = skv
}
var mapping FieldMappingFunc
if scope.meta != nil && scope.meta.KeyNameMapping != nil {
mapping = scope.meta.KeyNameMapping
}
for _, key := range lister.keys() {
if found, err := c.checkField(key, skv, dkv, scope); found {
if err != nil {
@ -615,23 +690,31 @@ func (c *Converter) convertKV(skv, dkv kvValue, scope *scope) error {
}
continue
}
df := dkv.value(key)
sf := skv.value(key)
stag := skv.tagOf(key)
dtag := dkv.tagOf(key)
skey := key
dkey := key
if mapping != nil {
skey, dkey = scope.meta.KeyNameMapping(key, stag, dtag)
}
df := dkv.value(dkey)
sf := skv.value(skey)
if !df.IsValid() || !sf.IsValid() {
switch {
case scope.flags.IsSet(IgnoreMissingFields):
// No error.
case scope.flags.IsSet(SourceToDest):
return scope.error("%v not present in dest", key)
return scope.error("%v not present in dest", dkey)
default:
return scope.error("%v not present in src", key)
return scope.error("%v not present in src", skey)
}
continue
}
scope.srcStack.top().key = key
scope.srcStack.top().tag = skv.tagOf(key)
scope.destStack.top().key = key
scope.destStack.top().tag = dkv.tagOf(key)
scope.srcStack.top().key = skey
scope.srcStack.top().tag = stag
scope.destStack.top().key = dkey
scope.destStack.top().tag = dtag
if err := c.convert(sf, df, scope); err != nil {
return err
}

View File

@ -20,6 +20,7 @@ import (
"fmt"
"reflect"
"strconv"
"strings"
"testing"
"github.com/google/gofuzz"
@ -173,6 +174,105 @@ func TestConverter_CallsRegisteredFunctions(t *testing.T) {
}
}
func TestConverter_MapsStringArrays(t *testing.T) {
type A struct {
Foo string
Baz int
Other string
}
c := NewConverter()
c.Debug = t
if err := c.RegisterConversionFunc(func(input *[]string, out *string, s Scope) error {
if len(*input) == 0 {
*out = ""
}
*out = (*input)[0]
return nil
}); err != nil {
t.Fatalf("unexpected error %v", err)
}
x := map[string][]string{
"Foo": {"bar"},
"Baz": {"1"},
"Other": {"", "test"},
"other": {"wrong"},
}
y := A{"test", 2, "something"}
if err := c.Convert(&x, &y, AllowDifferentFieldTypeNames, nil); err == nil {
t.Error("unexpected non-error")
}
if err := c.RegisterConversionFunc(func(input *[]string, out *int, s Scope) error {
if len(*input) == 0 {
*out = 0
}
str := (*input)[0]
i, err := strconv.Atoi(str)
if err != nil {
return err
}
*out = i
return nil
}); err != nil {
t.Fatalf("unexpected error %v", err)
}
if err := c.Convert(&x, &y, AllowDifferentFieldTypeNames, nil); err != nil {
t.Fatalf("unexpected error %v", err)
}
if !reflect.DeepEqual(y, A{"bar", 1, ""}) {
t.Errorf("unexpected result: %#v", y)
}
}
func TestConverter_MapsStringArraysWithMappingKey(t *testing.T) {
type A struct {
Foo string `json:"test"`
Baz int
Other string
}
c := NewConverter()
c.Debug = t
if err := c.RegisterConversionFunc(func(input *[]string, out *string, s Scope) error {
if len(*input) == 0 {
*out = ""
}
*out = (*input)[0]
return nil
}); err != nil {
t.Fatalf("unexpected error %v", err)
}
x := map[string][]string{
"Foo": {"bar"},
"test": {"baz"},
}
y := A{"", 0, ""}
if err := c.Convert(&x, &y, AllowDifferentFieldTypeNames|IgnoreMissingFields, &Meta{}); err != nil {
t.Fatalf("unexpected error %v", err)
}
if !reflect.DeepEqual(y, A{"bar", 0, ""}) {
t.Errorf("unexpected result: %#v", y)
}
mapping := func(key string, sourceTag, destTag reflect.StructTag) (source string, dest string) {
if s := destTag.Get("json"); len(s) > 0 {
return strings.SplitN(s, ",", 2)[0], key
}
return key, key
}
if err := c.Convert(&x, &y, AllowDifferentFieldTypeNames|IgnoreMissingFields, &Meta{KeyNameMapping: mapping}); err != nil {
t.Fatalf("unexpected error %v", err)
}
if !reflect.DeepEqual(y, A{"baz", 0, ""}) {
t.Errorf("unexpected result: %#v", y)
}
}
func TestConverter_fuzz(t *testing.T) {
// Use the same types from the scheme test.
table := []struct {

View File

@ -58,7 +58,8 @@ func (s *Scheme) Decode(data []byte) (interface{}, error) {
if err != nil {
return nil, err
}
if err := s.converter.Convert(obj, objOut, 0, s.generateConvertMeta(version, s.InternalVersion)); err != nil {
flags, meta := s.generateConvertMeta(version, s.InternalVersion, obj)
if err := s.converter.Convert(obj, objOut, flags, meta); err != nil {
return nil, err
}
obj = objOut
@ -101,7 +102,8 @@ func (s *Scheme) DecodeInto(data []byte, obj interface{}) error {
if err := json.Unmarshal(data, external); err != nil {
return err
}
if err := s.converter.Convert(external, obj, 0, s.generateConvertMeta(dataVersion, objVersion)); err != nil {
flags, meta := s.generateConvertMeta(dataVersion, objVersion, external)
if err := s.converter.Convert(external, obj, flags, meta); err != nil {
return err
}

View File

@ -67,7 +67,8 @@ func (s *Scheme) EncodeToVersion(obj interface{}, destVersion string) (data []by
if err != nil {
return nil, err
}
err = s.converter.Convert(obj, objOut, 0, s.generateConvertMeta(objVersion, destVersion))
flags, meta := s.generateConvertMeta(objVersion, destVersion, obj)
err = s.converter.Convert(obj, objOut, flags, meta)
if err != nil {
return nil, err
}

View File

@ -232,6 +232,14 @@ func (s *Scheme) AddDefaultingFuncs(defaultingFuncs ...interface{}) error {
return nil
}
// RegisterInputDefaults sets the provided field mapping function and field matching
// as the defaults for the provided input type. The fn may be nil, in which case no
// mapping will happen by default. Use this method to register a mechanism for handling
// a specific input type in conversion, such as a map[string]string to structs.
func (s *Scheme) RegisterInputDefaults(in interface{}, fn FieldMappingFunc, defaultFlags FieldMatchingFlags) error {
return s.converter.RegisterInputDefaults(in, fn, defaultFlags)
}
// Convert will attempt to convert in into out. Both must be pointers. For easy
// testing of conversion functions. Returns an error if the conversion isn't
// possible. You can call this with types that haven't been registered (for example,
@ -247,7 +255,11 @@ func (s *Scheme) Convert(in, out interface{}) error {
if v, _, err := s.ObjectVersionAndKind(out); err == nil {
outVersion = v
}
return s.converter.Convert(in, out, AllowDifferentFieldTypeNames, s.generateConvertMeta(inVersion, outVersion))
flags, meta := s.generateConvertMeta(inVersion, outVersion, in)
if flags == 0 {
flags = AllowDifferentFieldTypeNames
}
return s.converter.Convert(in, out, flags, meta)
}
// ConvertToVersion attempts to convert an input object to its matching Kind in another
@ -279,7 +291,8 @@ func (s *Scheme) ConvertToVersion(in interface{}, outVersion string) (interface{
return nil, err
}
if err := s.converter.Convert(in, out, 0, s.generateConvertMeta(inVersion, outVersion)); err != nil {
flags, meta := s.generateConvertMeta(inVersion, outVersion, in)
if err := s.converter.Convert(in, out, flags, meta); err != nil {
return nil, err
}
@ -290,11 +303,18 @@ func (s *Scheme) ConvertToVersion(in interface{}, outVersion string) (interface{
return out, nil
}
// Converter allows access to the converter for the scheme
func (s *Scheme) Converter() *Converter {
return s.converter
}
// generateConvertMeta constructs the meta value we pass to Convert.
func (s *Scheme) generateConvertMeta(srcVersion, destVersion string) *Meta {
return &Meta{
SrcVersion: srcVersion,
DestVersion: destVersion,
func (s *Scheme) generateConvertMeta(srcVersion, destVersion string, in interface{}) (FieldMatchingFlags, *Meta) {
t := reflect.TypeOf(in)
return s.converter.inputDefaultFlags[t], &Meta{
SrcVersion: srcVersion,
DestVersion: destVersion,
KeyNameMapping: s.converter.inputFieldMappingFuncs[t],
}
}

View File

@ -35,6 +35,10 @@ type Selector interface {
// requires.
RequiresExactMatch(field string) (value string, found bool)
// Transform returns a new copy of the selector after TransformFunc has been
// applied to the entire selector, or an error if fn returns an error.
Transform(fn TransformFunc) (Selector, error)
// String returns a human readable string that represents this selector.
String() string
}
@ -63,6 +67,14 @@ func (t *hasTerm) RequiresExactMatch(field string) (value string, found bool) {
return "", false
}
func (t *hasTerm) Transform(fn TransformFunc) (Selector, error) {
field, value, err := fn(t.field, t.value)
if err != nil {
return nil, err
}
return &hasTerm{field, value}, nil
}
func (t *hasTerm) String() string {
return fmt.Sprintf("%v=%v", t.field, t.value)
}
@ -83,6 +95,14 @@ func (t *notHasTerm) RequiresExactMatch(field string) (value string, found bool)
return "", false
}
func (t *notHasTerm) Transform(fn TransformFunc) (Selector, error) {
field, value, err := fn(t.field, t.value)
if err != nil {
return nil, err
}
return &notHasTerm{field, value}, nil
}
func (t *notHasTerm) String() string {
return fmt.Sprintf("%v!=%v", t.field, t.value)
}
@ -125,6 +145,18 @@ func (t andTerm) RequiresExactMatch(field string) (string, bool) {
return "", false
}
func (t andTerm) Transform(fn TransformFunc) (Selector, error) {
next := make([]Selector, len([]Selector(t)))
for i, s := range []Selector(t) {
n, err := s.Transform(fn)
if err != nil {
return nil, err
}
next[i] = n
}
return andTerm(next), nil
}
func (t andTerm) String() string {
var terms []string
for _, q := range t {
@ -183,31 +215,19 @@ func parseSelector(selector string, fn TransformFunc) (Selector, error) {
continue
}
if lhs, rhs, ok := try(part, "!="); ok {
lhs, rhs, err := fn(lhs, rhs)
if err != nil {
return nil, err
}
items = append(items, &notHasTerm{field: lhs, value: rhs})
} else if lhs, rhs, ok := try(part, "=="); ok {
lhs, rhs, err := fn(lhs, rhs)
if err != nil {
return nil, err
}
items = append(items, &hasTerm{field: lhs, value: rhs})
} else if lhs, rhs, ok := try(part, "="); ok {
lhs, rhs, err := fn(lhs, rhs)
if err != nil {
return nil, err
}
items = append(items, &hasTerm{field: lhs, value: rhs})
} else {
return nil, fmt.Errorf("invalid selector: '%s'; can't understand '%s'", selector, part)
}
}
if len(items) == 1 {
return items[0], nil
return items[0].Transform(fn)
}
return andTerm(items), nil
return andTerm(items).Transform(fn)
}
// OneTermEqualSelector returns an object that matches objects where one field/field equals one value.

78
pkg/runtime/conversion.go Normal file
View File

@ -0,0 +1,78 @@
/*
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 runtime
import (
"reflect"
"strconv"
"strings"
"github.com/GoogleCloudPlatform/kubernetes/pkg/conversion"
)
// JSONKeyMapper uses the struct tags on a conversion to determine the key value for
// the other side. Use when mapping from a map[string]* to a struct or vice versa.
func JSONKeyMapper(key string, sourceTag, destTag reflect.StructTag) (string, string) {
if s := destTag.Get("json"); len(s) > 0 {
return strings.SplitN(s, ",", 2)[0], key
}
if s := sourceTag.Get("json"); len(s) > 0 {
return key, strings.SplitN(s, ",", 2)[0]
}
return key, key
}
// DefaultStringConversions are helpers for converting []string and string to real values.
var DefaultStringConversions = []interface{}{
convertStringSliceToString,
convertStringSliceToInt,
convertStringSliceToInt64,
}
func convertStringSliceToString(input *[]string, out *string, s conversion.Scope) error {
if len(*input) == 0 {
*out = ""
}
*out = (*input)[0]
return nil
}
func convertStringSliceToInt(input *[]string, out *int, s conversion.Scope) error {
if len(*input) == 0 {
*out = 0
}
str := (*input)[0]
i, err := strconv.Atoi(str)
if err != nil {
return err
}
*out = i
return nil
}
func convertStringSliceToInt64(input *[]string, out *int64, s conversion.Scope) error {
if len(*input) == 0 {
*out = 0
}
str := (*input)[0]
i, err := strconv.ParseInt(str, 10, 64)
if err != nil {
return err
}
*out = i
return nil
}

View File

@ -0,0 +1,99 @@
/*
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 runtime_test
import (
"reflect"
"testing"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
)
type InternalComplex struct {
TypeMeta
String string
Integer int
Integer64 int64
Int64 int64
}
type ExternalComplex struct {
TypeMeta `json:",inline"`
String string `json:"string" description:"testing"`
Integer int `json:"int"`
Integer64 int64 `json:",omitempty"`
Int64 int64
}
func (*InternalComplex) IsAnAPIObject() {}
func (*ExternalComplex) IsAnAPIObject() {}
func TestStringMapConversion(t *testing.T) {
scheme := runtime.NewScheme()
scheme.Log(t)
scheme.AddKnownTypeWithName("", "Complex", &InternalComplex{})
scheme.AddKnownTypeWithName("external", "Complex", &ExternalComplex{})
testCases := map[string]struct {
input map[string][]string
errFn func(error) bool
expected runtime.Object
}{
"ignores omitempty": {
input: map[string][]string{
"String": {"not_used"},
"string": {"value"},
"int": {"1"},
"Integer64": {"2"},
},
expected: &ExternalComplex{String: "value", Integer: 1},
},
"returns error on bad int": {
input: map[string][]string{
"int": {"a"},
},
errFn: func(err error) bool { return err != nil },
expected: &ExternalComplex{},
},
"parses int64": {
input: map[string][]string{
"Int64": {"-1"},
},
expected: &ExternalComplex{Int64: -1},
},
"returns error on bad int64": {
input: map[string][]string{
"Int64": {"a"},
},
errFn: func(err error) bool { return err != nil },
expected: &ExternalComplex{},
},
}
for k, tc := range testCases {
out := &ExternalComplex{}
if err := scheme.Convert(&tc.input, out); (tc.errFn == nil && err != nil) || (tc.errFn != nil && !tc.errFn(err)) {
t.Errorf("%s: unexpected error: %v", k, err)
continue
} else if err != nil {
continue
}
if !reflect.DeepEqual(out, tc.expected) {
t.Errorf("%s: unexpected output: %#v", k, out)
}
}
}

View File

@ -23,6 +23,7 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/conversion"
)
// TODO: move me to pkg/api/meta
func IsListType(obj Object) bool {
_, err := GetItemsPtr(obj)
return err == nil
@ -32,6 +33,7 @@ func IsListType(obj Object) bool {
// If 'list' doesn't have an Items member, it's not really a list type
// and an error will be returned.
// This function will either return a pointer to a slice, or an error, but not both.
// TODO: move me to pkg/api/meta
func GetItemsPtr(list Object) (interface{}, error) {
v, err := conversion.EnforcePtr(list)
if err != nil {
@ -57,6 +59,7 @@ func GetItemsPtr(list Object) (interface{}, error) {
// ExtractList returns obj's Items element as an array of runtime.Objects.
// Returns an error if obj is not a List type (does not have an Items member).
// TODO: move me to pkg/api/meta
func ExtractList(obj Object) ([]Object, error) {
itemsPtr, err := GetItemsPtr(obj)
if err != nil {
@ -90,6 +93,7 @@ var objectSliceType = reflect.TypeOf([]Object{})
// objects.
// Returns an error if list is not a List type (does not have an Items member),
// or if any of the objects are not of the right type.
// TODO: move me to pkg/api/meta
func SetList(list Object, objects []Object) error {
itemsPtr, err := GetItemsPtr(list)
if err != nil {

View File

@ -217,6 +217,13 @@ func NewScheme() *Scheme {
); err != nil {
panic(err)
}
// Enable map[string][]string conversions by default
if err := s.raw.AddConversionFuncs(DefaultStringConversions...); err != nil {
panic(err)
}
if err := s.raw.RegisterInputDefaults(&map[string][]string{}, JSONKeyMapper, conversion.AllowDifferentFieldTypeNames|conversion.IgnoreMissingFields); err != nil {
panic(err)
}
return s
}