mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-29 14:37:00 +00:00
Merge pull request #5851 from smarterclayton/support_input_streams
Support input streams being returned by the APIserver
This commit is contained in:
commit
cfb6f1199b
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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{
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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],
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 ¬HasTerm{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, ¬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 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
78
pkg/runtime/conversion.go
Normal 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
|
||||
}
|
99
pkg/runtime/conversion_test.go
Normal file
99
pkg/runtime/conversion_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user