diff --git a/pkg/api/latest/latest.go b/pkg/api/latest/latest.go index f46d153a4d9..92f88de9149 100644 --- a/pkg/api/latest/latest.go +++ b/pkg/api/latest/latest.go @@ -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 diff --git a/pkg/api/rest/rest.go b/pkg/api/rest/rest.go index 3698bf2948d..14e434c7749 100644 --- a/pkg/api/rest/rest.go +++ b/pkg/api/rest/rest.go @@ -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 +} diff --git a/pkg/apiserver/api_installer.go b/pkg/apiserver/api_installer.go index 6f6dc8e9c20..4dda0319b80 100644 --- a/pkg/apiserver/api_installer.go +++ b/pkg/apiserver/api_installer.go @@ -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 +} diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index f7ac60d8b80..a16b582fcc4 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -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) diff --git a/pkg/apiserver/apiserver_test.go b/pkg/apiserver/apiserver_test.go index 13d439e35e6..b753ec462fa 100644 --- a/pkg/apiserver/apiserver_test.go +++ b/pkg/apiserver/apiserver_test.go @@ -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{ diff --git a/pkg/apiserver/resthandler.go b/pkg/apiserver/resthandler.go index c8b25c5f1f7..df363a6fba3 100644 --- a/pkg/apiserver/resthandler.go +++ b/pkg/apiserver/resthandler.go @@ -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 diff --git a/pkg/controller/replication_controller_test.go b/pkg/controller/replication_controller_test.go index dbba1358f3d..f52139090b7 100644 --- a/pkg/controller/replication_controller_test.go +++ b/pkg/controller/replication_controller_test.go @@ -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") } } diff --git a/pkg/conversion/converter.go b/pkg/conversion/converter.go index c2ab406a69f..d337f831588 100644 --- a/pkg/conversion/converter.go +++ b/pkg/conversion/converter.go @@ -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 } diff --git a/pkg/conversion/converter_test.go b/pkg/conversion/converter_test.go index 4f807760eac..d400da1ecb3 100644 --- a/pkg/conversion/converter_test.go +++ b/pkg/conversion/converter_test.go @@ -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 { diff --git a/pkg/conversion/decode.go b/pkg/conversion/decode.go index 1c98fae9bbf..34418a59177 100644 --- a/pkg/conversion/decode.go +++ b/pkg/conversion/decode.go @@ -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 } diff --git a/pkg/conversion/encode.go b/pkg/conversion/encode.go index e954dad16c2..fa753e0d3a2 100644 --- a/pkg/conversion/encode.go +++ b/pkg/conversion/encode.go @@ -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 } diff --git a/pkg/conversion/scheme.go b/pkg/conversion/scheme.go index 15adea40446..5314d0995bb 100644 --- a/pkg/conversion/scheme.go +++ b/pkg/conversion/scheme.go @@ -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], } } diff --git a/pkg/fields/selector.go b/pkg/fields/selector.go index e0ff0830749..05eefba054a 100644 --- a/pkg/fields/selector.go +++ b/pkg/fields/selector.go @@ -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. diff --git a/pkg/runtime/conversion.go b/pkg/runtime/conversion.go new file mode 100644 index 00000000000..b47f9e6bbe7 --- /dev/null +++ b/pkg/runtime/conversion.go @@ -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 +} diff --git a/pkg/runtime/conversion_test.go b/pkg/runtime/conversion_test.go new file mode 100644 index 00000000000..a5a2dcfe179 --- /dev/null +++ b/pkg/runtime/conversion_test.go @@ -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) + } + } +} diff --git a/pkg/runtime/helper.go b/pkg/runtime/helper.go index 3f3427fd08b..3d4df08091e 100644 --- a/pkg/runtime/helper.go +++ b/pkg/runtime/helper.go @@ -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 { diff --git a/pkg/runtime/scheme.go b/pkg/runtime/scheme.go index 253c4957f88..7fc233767e7 100644 --- a/pkg/runtime/scheme.go +++ b/pkg/runtime/scheme.go @@ -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 }