Merge pull request #5851 from smarterclayton/support_input_streams
Support input streams being returned by the APIserver
This commit is contained in:
@@ -26,6 +26,7 @@ import (
|
|||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta2"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta2"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta3"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta3"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Version is the string that represents the current external default version.
|
// Version is the string that represents the current external default version.
|
||||||
@@ -122,9 +123,15 @@ func init() {
|
|||||||
"Namespace": true,
|
"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
|
// enumerate all supported versions, get the kinds, and register with the mapper how to address our resources
|
||||||
for _, version := range versions {
|
for _, version := range versions {
|
||||||
for kind := range api.Scheme.KnownTypes(version) {
|
for kind := range api.Scheme.KnownTypes(version) {
|
||||||
|
if ignoredKinds.Has(kind) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
mixedCase, found := versionMixedCase[version]
|
mixedCase, found := versionMixedCase[version]
|
||||||
if !found {
|
if !found {
|
||||||
mixedCase = false
|
mixedCase = false
|
||||||
|
@@ -17,6 +17,7 @@ limitations under the License.
|
|||||||
package rest
|
package rest
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"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 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)
|
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)
|
patcher, isPatcher := storage.(rest.Patcher)
|
||||||
_, isWatcher := storage.(rest.Watcher)
|
_, isWatcher := storage.(rest.Watcher)
|
||||||
_, isRedirector := storage.(rest.Redirector)
|
_, isRedirector := storage.(rest.Redirector)
|
||||||
|
storageMeta, isMetadata := storage.(rest.StorageMetadata)
|
||||||
|
if !isMetadata {
|
||||||
|
storageMeta = defaultStorageMetadata{}
|
||||||
|
}
|
||||||
|
|
||||||
var versionedDeleterObject runtime.Object
|
var versionedDeleterObject runtime.Object
|
||||||
switch {
|
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
|
// 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 {
|
for _, action := range actions {
|
||||||
|
reqScope.Namer = action.Namer
|
||||||
m := monitorFilter(action.Verb, resource)
|
m := monitorFilter(action.Verb, resource)
|
||||||
switch action.Verb {
|
switch action.Verb {
|
||||||
case "GET": // Get a resource.
|
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).
|
Filter(m).
|
||||||
Doc("read the specified " + kind).
|
Doc("read the specified " + kind).
|
||||||
Operation("read" + kind).
|
Operation("read" + kind).
|
||||||
|
Produces(append(storageMeta.ProducesMIMETypes(action.Verb), "application/json")...).
|
||||||
Writes(versionedObject)
|
Writes(versionedObject)
|
||||||
addParams(route, action.Params)
|
addParams(route, action.Params)
|
||||||
ws.Route(route)
|
ws.Route(route)
|
||||||
case "LIST": // List all resources of a kind.
|
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).
|
Filter(m).
|
||||||
Doc("list objects of kind " + kind).
|
Doc("list objects of kind " + kind).
|
||||||
Operation("list" + kind).
|
Operation("list" + kind).
|
||||||
|
Produces("application/json").
|
||||||
Writes(versionedList)
|
Writes(versionedList)
|
||||||
addParams(route, action.Params)
|
addParams(route, action.Params)
|
||||||
ws.Route(route)
|
ws.Route(route)
|
||||||
case "PUT": // Update a resource.
|
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).
|
Filter(m).
|
||||||
Doc("replace the specified " + kind).
|
Doc("replace the specified " + kind).
|
||||||
Operation("replace" + kind).
|
Operation("replace" + kind).
|
||||||
|
Produces(append(storageMeta.ProducesMIMETypes(action.Verb), "application/json")...).
|
||||||
Reads(versionedObject)
|
Reads(versionedObject)
|
||||||
addParams(route, action.Params)
|
addParams(route, action.Params)
|
||||||
ws.Route(route)
|
ws.Route(route)
|
||||||
case "PATCH": // Partially update a resource
|
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).
|
Filter(m).
|
||||||
Doc("partially update the specified " + kind).
|
Doc("partially update the specified " + kind).
|
||||||
// TODO: toggle patch strategy by content type
|
// TODO: toggle patch strategy by content type
|
||||||
// Consumes("application/merge-patch+json", "application/json-patch+json").
|
// Consumes("application/merge-patch+json", "application/json-patch+json").
|
||||||
Operation("patch" + kind).
|
Operation("patch" + kind).
|
||||||
|
Produces(append(storageMeta.ProducesMIMETypes(action.Verb), "application/json")...).
|
||||||
Reads(versionedObject)
|
Reads(versionedObject)
|
||||||
addParams(route, action.Params)
|
addParams(route, action.Params)
|
||||||
ws.Route(route)
|
ws.Route(route)
|
||||||
case "POST": // Create a resource.
|
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).
|
Filter(m).
|
||||||
Doc("create a " + kind).
|
Doc("create a " + kind).
|
||||||
Operation("create" + kind).
|
Operation("create" + kind).
|
||||||
|
Produces(append(storageMeta.ProducesMIMETypes(action.Verb), "application/json")...).
|
||||||
Reads(versionedObject)
|
Reads(versionedObject)
|
||||||
addParams(route, action.Params)
|
addParams(route, action.Params)
|
||||||
ws.Route(route)
|
ws.Route(route)
|
||||||
case "DELETE": // Delete a resource.
|
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).
|
Filter(m).
|
||||||
Doc("delete a " + kind).
|
Doc("delete a " + kind).
|
||||||
Operation("delete" + kind)
|
Operation("delete" + kind).
|
||||||
|
Produces(append(storageMeta.ProducesMIMETypes(action.Verb), "application/json")...)
|
||||||
if isGracefulDeleter {
|
if isGracefulDeleter {
|
||||||
route.Reads(versionedDeleterObject)
|
route.Reads(versionedDeleterObject)
|
||||||
}
|
}
|
||||||
@@ -343,6 +361,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
|
|||||||
Filter(m).
|
Filter(m).
|
||||||
Doc("watch a particular " + kind).
|
Doc("watch a particular " + kind).
|
||||||
Operation("watch" + kind).
|
Operation("watch" + kind).
|
||||||
|
Produces("application/json").
|
||||||
Writes(versionedObject)
|
Writes(versionedObject)
|
||||||
addParams(route, action.Params)
|
addParams(route, action.Params)
|
||||||
ws.Route(route)
|
ws.Route(route)
|
||||||
@@ -351,6 +370,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
|
|||||||
Filter(m).
|
Filter(m).
|
||||||
Doc("watch a list of " + kind).
|
Doc("watch a list of " + kind).
|
||||||
Operation("watch" + kind + "list").
|
Operation("watch" + kind + "list").
|
||||||
|
Produces("application/json").
|
||||||
Writes(versionedList)
|
Writes(versionedList)
|
||||||
addParams(route, action.Params)
|
addParams(route, action.Params)
|
||||||
ws.Route(route)
|
ws.Route(route)
|
||||||
@@ -630,3 +650,13 @@ func addParams(route *restful.RouteBuilder, params []*restful.Parameter) {
|
|||||||
route.Param(param)
|
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"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
"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.
|
// writeJSON renders an object as JSON to the response.
|
||||||
func writeJSON(statusCode int, codec runtime.Codec, object runtime.Object, w http.ResponseWriter) {
|
func writeJSON(statusCode int, codec runtime.Codec, object runtime.Object, w http.ResponseWriter) {
|
||||||
output, err := codec.Encode(object)
|
output, err := codec.Encode(object)
|
||||||
|
@@ -21,6 +21,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
@@ -123,6 +124,7 @@ func init() {
|
|||||||
type defaultAPIServer struct {
|
type defaultAPIServer struct {
|
||||||
http.Handler
|
http.Handler
|
||||||
group *APIGroupVersion
|
group *APIGroupVersion
|
||||||
|
container *restful.Container
|
||||||
}
|
}
|
||||||
|
|
||||||
// uses the default settings
|
// uses the default settings
|
||||||
@@ -169,7 +171,7 @@ func handleInternal(storage map[string]rest.Storage, admissionControl admission.
|
|||||||
ws := new(restful.WebService)
|
ws := new(restful.WebService)
|
||||||
InstallSupport(mux, ws)
|
InstallSupport(mux, ws)
|
||||||
container.Add(ws)
|
container.Add(ws)
|
||||||
return &defaultAPIServer{mux, group}
|
return &defaultAPIServer{mux, group, container}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Simple struct {
|
type Simple struct {
|
||||||
@@ -212,6 +214,8 @@ type SimpleRESTStorage struct {
|
|||||||
updated *Simple
|
updated *Simple
|
||||||
created *Simple
|
created *Simple
|
||||||
|
|
||||||
|
stream *SimpleStream
|
||||||
|
|
||||||
deleted string
|
deleted string
|
||||||
deleteOptions *api.DeleteOptions
|
deleteOptions *api.DeleteOptions
|
||||||
|
|
||||||
@@ -243,8 +247,34 @@ func (storage *SimpleRESTStorage) List(ctx api.Context, label labels.Selector, f
|
|||||||
return result, storage.errors["list"]
|
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) {
|
func (storage *SimpleRESTStorage) Get(ctx api.Context, id string) (runtime.Object, error) {
|
||||||
storage.checkContext(ctx)
|
storage.checkContext(ctx)
|
||||||
|
if id == "binary" {
|
||||||
|
return storage.stream, storage.errors["get"]
|
||||||
|
}
|
||||||
return api.Scheme.CopyOrDie(&storage.item), 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)
|
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) {
|
func extractBody(response *http.Response, object runtime.Object) (string, error) {
|
||||||
defer response.Body.Close()
|
defer response.Body.Close()
|
||||||
body, err := ioutil.ReadAll(response.Body)
|
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) {
|
func TestGet(t *testing.T) {
|
||||||
storage := map[string]rest.Storage{}
|
storage := map[string]rest.Storage{}
|
||||||
simpleStorage := SimpleRESTStorage{
|
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) {
|
func TestGetAlternateSelfLink(t *testing.T) {
|
||||||
storage := map[string]rest.Storage{}
|
storage := map[string]rest.Storage{}
|
||||||
simpleStorage := SimpleRESTStorage{
|
simpleStorage := SimpleRESTStorage{
|
||||||
|
@@ -59,28 +59,38 @@ type ScopeNamer interface {
|
|||||||
GenerateListLink(req *restful.Request) (path, query string, err error)
|
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.
|
// 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) {
|
return func(req *restful.Request, res *restful.Response) {
|
||||||
w := res.ResponseWriter
|
w := res.ResponseWriter
|
||||||
namespace, name, err := namer.Name(req)
|
namespace, name, err := scope.Namer.Name(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorJSON(err, codec, w)
|
errorJSON(err, scope.Codec, w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx := ctxFn(req)
|
ctx := scope.ContextFunc(req)
|
||||||
ctx = api.WithNamespace(ctx, namespace)
|
ctx = api.WithNamespace(ctx, namespace)
|
||||||
|
|
||||||
result, err := r.Get(ctx, name)
|
result, err := r.Get(ctx, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorJSON(err, codec, w)
|
errorJSON(err, scope.Codec, w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := setSelfLink(result, req, namer); err != nil {
|
if err := setSelfLink(result, req, scope.Namer); err != nil {
|
||||||
errorJSON(err, codec, w)
|
errorJSON(err, scope.Codec, w)
|
||||||
return
|
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.
|
// 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) {
|
return func(req *restful.Request, res *restful.Response) {
|
||||||
w := res.ResponseWriter
|
w := res.ResponseWriter
|
||||||
|
|
||||||
namespace, err := namer.Namespace(req)
|
namespace, err := scope.Namer.Namespace(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorJSON(err, codec, w)
|
errorJSON(err, scope.Codec, w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx := ctxFn(req)
|
ctx := scope.ContextFunc(req)
|
||||||
ctx = api.WithNamespace(ctx, namespace)
|
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 {
|
if err != nil {
|
||||||
errorJSON(err, codec, w)
|
errorJSON(err, scope.Codec, w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := r.List(ctx, label, field)
|
result, err := r.List(ctx, label, field)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorJSON(err, codec, w)
|
errorJSON(err, scope.Codec, w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := setListSelfLink(result, req, namer); err != nil {
|
if err := setListSelfLink(result, req, scope.Namer); err != nil {
|
||||||
errorJSON(err, codec, w)
|
errorJSON(err, scope.Codec, w)
|
||||||
return
|
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.
|
// 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) {
|
return func(req *restful.Request, res *restful.Response) {
|
||||||
w := res.ResponseWriter
|
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)
|
// 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"))
|
timeout := parseTimeout(req.Request.URL.Query().Get("timeout"))
|
||||||
|
|
||||||
namespace, err := namer.Namespace(req)
|
namespace, err := scope.Namer.Namespace(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorJSON(err, codec, w)
|
errorJSON(err, scope.Codec, w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx := ctxFn(req)
|
ctx := scope.ContextFunc(req)
|
||||||
ctx = api.WithNamespace(ctx, namespace)
|
ctx = api.WithNamespace(ctx, namespace)
|
||||||
|
|
||||||
body, err := readBody(req.Request)
|
body, err := readBody(req.Request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorJSON(err, codec, w)
|
errorJSON(err, scope.Codec, w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
obj := r.New()
|
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)
|
err = transformDecodeError(typer, err, obj, body)
|
||||||
errorJSON(err, codec, w)
|
errorJSON(err, scope.Codec, w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = admit.Admit(admission.NewAttributesRecord(obj, namespace, resource, "CREATE"))
|
err = admit.Admit(admission.NewAttributesRecord(obj, namespace, scope.Resource, "CREATE"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorJSON(err, codec, w)
|
errorJSON(err, scope.Codec, w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,73 +187,73 @@ func CreateResource(r rest.Creater, ctxFn ContextFunc, namer ScopeNamer, codec r
|
|||||||
return out, err
|
return out, err
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorJSON(err, codec, w)
|
errorJSON(err, scope.Codec, w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := setSelfLink(result, req, namer); err != nil {
|
if err := setSelfLink(result, req, scope.Namer); err != nil {
|
||||||
errorJSON(err, codec, w)
|
errorJSON(err, scope.Codec, w)
|
||||||
return
|
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
|
// 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
|
// 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) {
|
return func(req *restful.Request, res *restful.Response) {
|
||||||
w := res.ResponseWriter
|
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)
|
// 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"))
|
timeout := parseTimeout(req.Request.URL.Query().Get("timeout"))
|
||||||
|
|
||||||
namespace, name, err := namer.Name(req)
|
namespace, name, err := scope.Namer.Name(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorJSON(err, codec, w)
|
errorJSON(err, scope.Codec, w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
obj := r.New()
|
obj := r.New()
|
||||||
// PATCH requires same permission as UPDATE
|
// 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 {
|
if err != nil {
|
||||||
errorJSON(err, codec, w)
|
errorJSON(err, scope.Codec, w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := ctxFn(req)
|
ctx := scope.ContextFunc(req)
|
||||||
ctx = api.WithNamespace(ctx, namespace)
|
ctx = api.WithNamespace(ctx, namespace)
|
||||||
|
|
||||||
original, err := r.Get(ctx, name)
|
original, err := r.Get(ctx, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorJSON(err, codec, w)
|
errorJSON(err, scope.Codec, w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
originalObjJs, err := codec.Encode(original)
|
originalObjJs, err := scope.Codec.Encode(original)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorJSON(err, codec, w)
|
errorJSON(err, scope.Codec, w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
patchJs, err := readBody(req.Request)
|
patchJs, err := readBody(req.Request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorJSON(err, codec, w)
|
errorJSON(err, scope.Codec, w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
patchedObjJs, err := jsonpatch.MergePatch(originalObjJs, patchJs)
|
patchedObjJs, err := jsonpatch.MergePatch(originalObjJs, patchJs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorJSON(err, codec, w)
|
errorJSON(err, scope.Codec, w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := codec.DecodeInto(patchedObjJs, obj); err != nil {
|
if err := scope.Codec.DecodeInto(patchedObjJs, obj); err != nil {
|
||||||
errorJSON(err, codec, w)
|
errorJSON(err, scope.Codec, w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := checkName(obj, name, namespace, namer); err != nil {
|
if err := checkName(obj, name, namespace, scope.Namer); err != nil {
|
||||||
errorJSON(err, codec, w)
|
errorJSON(err, scope.Codec, w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,56 +263,56 @@ func PatchResource(r rest.Patcher, ctxFn ContextFunc, namer ScopeNamer, codec ru
|
|||||||
return obj, err
|
return obj, err
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorJSON(err, codec, w)
|
errorJSON(err, scope.Codec, w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := setSelfLink(result, req, namer); err != nil {
|
if err := setSelfLink(result, req, scope.Namer); err != nil {
|
||||||
errorJSON(err, codec, w)
|
errorJSON(err, scope.Codec, w)
|
||||||
return
|
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
|
// 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) {
|
return func(req *restful.Request, res *restful.Response) {
|
||||||
w := res.ResponseWriter
|
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)
|
// 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"))
|
timeout := parseTimeout(req.Request.URL.Query().Get("timeout"))
|
||||||
|
|
||||||
namespace, name, err := namer.Name(req)
|
namespace, name, err := scope.Namer.Name(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorJSON(err, codec, w)
|
errorJSON(err, scope.Codec, w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx := ctxFn(req)
|
ctx := scope.ContextFunc(req)
|
||||||
ctx = api.WithNamespace(ctx, namespace)
|
ctx = api.WithNamespace(ctx, namespace)
|
||||||
|
|
||||||
body, err := readBody(req.Request)
|
body, err := readBody(req.Request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorJSON(err, codec, w)
|
errorJSON(err, scope.Codec, w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
obj := r.New()
|
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)
|
err = transformDecodeError(typer, err, obj, body)
|
||||||
errorJSON(err, codec, w)
|
errorJSON(err, scope.Codec, w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := checkName(obj, name, namespace, namer); err != nil {
|
if err := checkName(obj, name, namespace, scope.Namer); err != nil {
|
||||||
errorJSON(err, codec, w)
|
errorJSON(err, scope.Codec, w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = admit.Admit(admission.NewAttributesRecord(obj, namespace, resource, "UPDATE"))
|
err = admit.Admit(admission.NewAttributesRecord(obj, namespace, scope.Resource, "UPDATE"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorJSON(err, codec, w)
|
errorJSON(err, scope.Codec, w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,12 +323,12 @@ func UpdateResource(r rest.Updater, ctxFn ContextFunc, namer ScopeNamer, codec r
|
|||||||
return obj, err
|
return obj, err
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorJSON(err, codec, w)
|
errorJSON(err, scope.Codec, w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := setSelfLink(result, req, namer); err != nil {
|
if err := setSelfLink(result, req, scope.Namer); err != nil {
|
||||||
errorJSON(err, codec, w)
|
errorJSON(err, scope.Codec, w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,46 +336,44 @@ func UpdateResource(r rest.Updater, ctxFn ContextFunc, namer ScopeNamer, codec r
|
|||||||
if wasCreated {
|
if wasCreated {
|
||||||
status = http.StatusCreated
|
status = http.StatusCreated
|
||||||
}
|
}
|
||||||
writeJSON(status, codec, result, w)
|
writeJSON(status, scope.Codec, result, w)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteResource returns a function that will handle a resource deletion
|
// 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) {
|
return func(req *restful.Request, res *restful.Response) {
|
||||||
w := res.ResponseWriter
|
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)
|
// 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"))
|
timeout := parseTimeout(req.Request.URL.Query().Get("timeout"))
|
||||||
|
|
||||||
namespace, name, err := namer.Name(req)
|
namespace, name, err := scope.Namer.Name(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorJSON(err, codec, w)
|
errorJSON(err, scope.Codec, w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx := ctxFn(req)
|
ctx := scope.ContextFunc(req)
|
||||||
if len(namespace) > 0 {
|
|
||||||
ctx = api.WithNamespace(ctx, namespace)
|
ctx = api.WithNamespace(ctx, namespace)
|
||||||
}
|
|
||||||
|
|
||||||
options := &api.DeleteOptions{}
|
options := &api.DeleteOptions{}
|
||||||
if checkBody {
|
if checkBody {
|
||||||
body, err := readBody(req.Request)
|
body, err := readBody(req.Request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorJSON(err, codec, w)
|
errorJSON(err, scope.Codec, w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(body) > 0 {
|
if len(body) > 0 {
|
||||||
if err := codec.DecodeInto(body, options); err != nil {
|
if err := scope.Codec.DecodeInto(body, options); err != nil {
|
||||||
errorJSON(err, codec, w)
|
errorJSON(err, scope.Codec, w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = admit.Admit(admission.NewAttributesRecord(nil, namespace, resource, "DELETE"))
|
err = admit.Admit(admission.NewAttributesRecord(nil, namespace, scope.Resource, "DELETE"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorJSON(err, codec, w)
|
errorJSON(err, scope.Codec, w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,7 +381,7 @@ func DeleteResource(r rest.GracefulDeleter, checkBody bool, ctxFn ContextFunc, n
|
|||||||
return r.Delete(ctx, name, options)
|
return r.Delete(ctx, name, options)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorJSON(err, codec, w)
|
errorJSON(err, scope.Codec, w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,19 +393,19 @@ func DeleteResource(r rest.GracefulDeleter, checkBody bool, ctxFn ContextFunc, n
|
|||||||
Code: http.StatusOK,
|
Code: http.StatusOK,
|
||||||
Details: &api.StatusDetails{
|
Details: &api.StatusDetails{
|
||||||
ID: name,
|
ID: name,
|
||||||
Kind: kind,
|
Kind: scope.Kind,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// when a non-status response is returned, set the self link
|
// when a non-status response is returned, set the self link
|
||||||
if _, ok := result.(*api.Status); !ok {
|
if _, ok := result.(*api.Status); !ok {
|
||||||
if err := setSelfLink(result, req, namer); err != nil {
|
if err := setSelfLink(result, req, scope.Namer); err != nil {
|
||||||
errorJSON(err, codec, w)
|
errorJSON(err, scope.Codec, w)
|
||||||
return
|
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 {
|
func setSelfLink(obj runtime.Object, req *restful.Request, namer ScopeNamer) error {
|
||||||
// TODO: SelfLink generation should return a full URL?
|
// TODO: SelfLink generation should return a full URL?
|
||||||
path, query, err := namer.GenerateLink(req, obj)
|
path, query, err := namer.GenerateLink(req, obj)
|
||||||
if err == errEmptyName {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
newURL := *req.Request.URL
|
newURL := *req.Request.URL
|
||||||
|
@@ -366,7 +366,7 @@ func TestWatchControllers(t *testing.T) {
|
|||||||
|
|
||||||
select {
|
select {
|
||||||
case <-received:
|
case <-received:
|
||||||
case <-time.After(10 * time.Millisecond):
|
case <-time.After(100 * time.Millisecond):
|
||||||
t.Errorf("Expected 1 call but got 0")
|
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.
|
// Map from a type to a function which applies defaults.
|
||||||
defaultingFuncs map[reflect.Type]reflect.Value
|
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.
|
// If non-nil, will be called to print helpful debugging info. Quite verbose.
|
||||||
Debug DebugLogger
|
Debug DebugLogger
|
||||||
|
|
||||||
@@ -71,6 +77,9 @@ func NewConverter() *Converter {
|
|||||||
nameFunc: func(t reflect.Type) string { return t.Name() },
|
nameFunc: func(t reflect.Type) string { return t.Name() },
|
||||||
structFieldDests: map[typeNamePair][]typeNamePair{},
|
structFieldDests: map[typeNamePair][]typeNamePair{},
|
||||||
structFieldSources: 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
|
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.
|
// Meta is supplied by Scheme, when it calls Convert.
|
||||||
type Meta struct {
|
type Meta struct {
|
||||||
SrcVersion string
|
SrcVersion string
|
||||||
DestVersion 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.
|
// scope contains information about an ongoing conversion.
|
||||||
@@ -301,6 +316,21 @@ func (c *Converter) RegisterDefaultingFunc(defaultingFunc interface{}) error {
|
|||||||
return nil
|
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
|
// FieldMatchingFlags contains a list of ways in which struct fields could be
|
||||||
// copied. These constants may be | combined.
|
// copied. These constants may be | combined.
|
||||||
type FieldMatchingFlags int
|
type FieldMatchingFlags int
|
||||||
@@ -538,10 +568,16 @@ func (c *Converter) defaultConvert(sv, dv reflect.Value, scope *scope) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var stringType = reflect.TypeOf("")
|
||||||
|
|
||||||
func toKVValue(v reflect.Value) kvValue {
|
func toKVValue(v reflect.Value) kvValue {
|
||||||
switch v.Kind() {
|
switch v.Kind() {
|
||||||
case reflect.Struct:
|
case reflect.Struct:
|
||||||
return structAdaptor(v)
|
return structAdaptor(v)
|
||||||
|
case reflect.Map:
|
||||||
|
if v.Type().Key().AssignableTo(stringType) {
|
||||||
|
return stringMapAdaptor(v)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -561,15 +597,48 @@ type kvValue interface {
|
|||||||
confirmSet(key string, v reflect.Value) bool
|
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
|
type structAdaptor reflect.Value
|
||||||
|
|
||||||
func (sa structAdaptor) len() int {
|
func (a structAdaptor) len() int {
|
||||||
v := reflect.Value(sa)
|
v := reflect.Value(a)
|
||||||
return v.Type().NumField()
|
return v.Type().NumField()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sa structAdaptor) keys() []string {
|
func (a structAdaptor) keys() []string {
|
||||||
v := reflect.Value(sa)
|
v := reflect.Value(a)
|
||||||
t := v.Type()
|
t := v.Type()
|
||||||
keys := make([]string, t.NumField())
|
keys := make([]string, t.NumField())
|
||||||
for i := range keys {
|
for i := range keys {
|
||||||
@@ -578,8 +647,8 @@ func (sa structAdaptor) keys() []string {
|
|||||||
return keys
|
return keys
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sa structAdaptor) tagOf(key string) reflect.StructTag {
|
func (a structAdaptor) tagOf(key string) reflect.StructTag {
|
||||||
v := reflect.Value(sa)
|
v := reflect.Value(a)
|
||||||
field, ok := v.Type().FieldByName(key)
|
field, ok := v.Type().FieldByName(key)
|
||||||
if ok {
|
if ok {
|
||||||
return field.Tag
|
return field.Tag
|
||||||
@@ -587,12 +656,12 @@ func (sa structAdaptor) tagOf(key string) reflect.StructTag {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sa structAdaptor) value(key string) reflect.Value {
|
func (a structAdaptor) value(key string) reflect.Value {
|
||||||
v := reflect.Value(sa)
|
v := reflect.Value(a)
|
||||||
return v.FieldByName(key)
|
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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -608,6 +677,12 @@ func (c *Converter) convertKV(skv, dkv kvValue, scope *scope) error {
|
|||||||
if scope.flags.IsSet(SourceToDest) {
|
if scope.flags.IsSet(SourceToDest) {
|
||||||
lister = skv
|
lister = skv
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var mapping FieldMappingFunc
|
||||||
|
if scope.meta != nil && scope.meta.KeyNameMapping != nil {
|
||||||
|
mapping = scope.meta.KeyNameMapping
|
||||||
|
}
|
||||||
|
|
||||||
for _, key := range lister.keys() {
|
for _, key := range lister.keys() {
|
||||||
if found, err := c.checkField(key, skv, dkv, scope); found {
|
if found, err := c.checkField(key, skv, dkv, scope); found {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -615,23 +690,31 @@ func (c *Converter) convertKV(skv, dkv kvValue, scope *scope) error {
|
|||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
df := dkv.value(key)
|
stag := skv.tagOf(key)
|
||||||
sf := skv.value(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() {
|
if !df.IsValid() || !sf.IsValid() {
|
||||||
switch {
|
switch {
|
||||||
case scope.flags.IsSet(IgnoreMissingFields):
|
case scope.flags.IsSet(IgnoreMissingFields):
|
||||||
// No error.
|
// No error.
|
||||||
case scope.flags.IsSet(SourceToDest):
|
case scope.flags.IsSet(SourceToDest):
|
||||||
return scope.error("%v not present in dest", key)
|
return scope.error("%v not present in dest", dkey)
|
||||||
default:
|
default:
|
||||||
return scope.error("%v not present in src", key)
|
return scope.error("%v not present in src", skey)
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
scope.srcStack.top().key = key
|
scope.srcStack.top().key = skey
|
||||||
scope.srcStack.top().tag = skv.tagOf(key)
|
scope.srcStack.top().tag = stag
|
||||||
scope.destStack.top().key = key
|
scope.destStack.top().key = dkey
|
||||||
scope.destStack.top().tag = dkv.tagOf(key)
|
scope.destStack.top().tag = dtag
|
||||||
if err := c.convert(sf, df, scope); err != nil {
|
if err := c.convert(sf, df, scope); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@@ -20,6 +20,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/google/gofuzz"
|
"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) {
|
func TestConverter_fuzz(t *testing.T) {
|
||||||
// Use the same types from the scheme test.
|
// Use the same types from the scheme test.
|
||||||
table := []struct {
|
table := []struct {
|
||||||
|
@@ -58,7 +58,8 @@ func (s *Scheme) Decode(data []byte) (interface{}, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
obj = objOut
|
obj = objOut
|
||||||
@@ -101,7 +102,8 @@ func (s *Scheme) DecodeInto(data []byte, obj interface{}) error {
|
|||||||
if err := json.Unmarshal(data, external); err != nil {
|
if err := json.Unmarshal(data, external); err != nil {
|
||||||
return err
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -67,7 +67,8 @@ func (s *Scheme) EncodeToVersion(obj interface{}, destVersion string) (data []by
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@@ -232,6 +232,14 @@ func (s *Scheme) AddDefaultingFuncs(defaultingFuncs ...interface{}) error {
|
|||||||
return nil
|
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
|
// 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
|
// 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,
|
// 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 {
|
if v, _, err := s.ObjectVersionAndKind(out); err == nil {
|
||||||
outVersion = v
|
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
|
// 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
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,11 +303,18 @@ func (s *Scheme) ConvertToVersion(in interface{}, outVersion string) (interface{
|
|||||||
return out, nil
|
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.
|
// generateConvertMeta constructs the meta value we pass to Convert.
|
||||||
func (s *Scheme) generateConvertMeta(srcVersion, destVersion string) *Meta {
|
func (s *Scheme) generateConvertMeta(srcVersion, destVersion string, in interface{}) (FieldMatchingFlags, *Meta) {
|
||||||
return &Meta{
|
t := reflect.TypeOf(in)
|
||||||
|
return s.converter.inputDefaultFlags[t], &Meta{
|
||||||
SrcVersion: srcVersion,
|
SrcVersion: srcVersion,
|
||||||
DestVersion: destVersion,
|
DestVersion: destVersion,
|
||||||
|
KeyNameMapping: s.converter.inputFieldMappingFuncs[t],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -35,6 +35,10 @@ type Selector interface {
|
|||||||
// requires.
|
// requires.
|
||||||
RequiresExactMatch(field string) (value string, found bool)
|
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 returns a human readable string that represents this selector.
|
||||||
String() string
|
String() string
|
||||||
}
|
}
|
||||||
@@ -63,6 +67,14 @@ func (t *hasTerm) RequiresExactMatch(field string) (value string, found bool) {
|
|||||||
return "", false
|
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 {
|
func (t *hasTerm) String() string {
|
||||||
return fmt.Sprintf("%v=%v", t.field, t.value)
|
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
|
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 {
|
func (t *notHasTerm) String() string {
|
||||||
return fmt.Sprintf("%v!=%v", t.field, t.value)
|
return fmt.Sprintf("%v!=%v", t.field, t.value)
|
||||||
}
|
}
|
||||||
@@ -125,6 +145,18 @@ func (t andTerm) RequiresExactMatch(field string) (string, bool) {
|
|||||||
return "", false
|
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 {
|
func (t andTerm) String() string {
|
||||||
var terms []string
|
var terms []string
|
||||||
for _, q := range t {
|
for _, q := range t {
|
||||||
@@ -183,31 +215,19 @@ func parseSelector(selector string, fn TransformFunc) (Selector, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if lhs, rhs, ok := try(part, "!="); ok {
|
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})
|
items = append(items, ¬HasTerm{field: lhs, value: rhs})
|
||||||
} else if lhs, rhs, ok := try(part, "=="); ok {
|
} 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})
|
items = append(items, &hasTerm{field: lhs, value: rhs})
|
||||||
} else if lhs, rhs, ok := try(part, "="); ok {
|
} 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})
|
items = append(items, &hasTerm{field: lhs, value: rhs})
|
||||||
} else {
|
} else {
|
||||||
return nil, fmt.Errorf("invalid selector: '%s'; can't understand '%s'", selector, part)
|
return nil, fmt.Errorf("invalid selector: '%s'; can't understand '%s'", selector, part)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(items) == 1 {
|
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.
|
// 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"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/conversion"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TODO: move me to pkg/api/meta
|
||||||
func IsListType(obj Object) bool {
|
func IsListType(obj Object) bool {
|
||||||
_, err := GetItemsPtr(obj)
|
_, err := GetItemsPtr(obj)
|
||||||
return err == nil
|
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
|
// If 'list' doesn't have an Items member, it's not really a list type
|
||||||
// and an error will be returned.
|
// and an error will be returned.
|
||||||
// This function will either return a pointer to a slice, or an error, but not both.
|
// 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) {
|
func GetItemsPtr(list Object) (interface{}, error) {
|
||||||
v, err := conversion.EnforcePtr(list)
|
v, err := conversion.EnforcePtr(list)
|
||||||
if err != nil {
|
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.
|
// 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).
|
// 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) {
|
func ExtractList(obj Object) ([]Object, error) {
|
||||||
itemsPtr, err := GetItemsPtr(obj)
|
itemsPtr, err := GetItemsPtr(obj)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -90,6 +93,7 @@ var objectSliceType = reflect.TypeOf([]Object{})
|
|||||||
// objects.
|
// objects.
|
||||||
// Returns an error if list is not a List type (does not have an Items member),
|
// 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.
|
// 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 {
|
func SetList(list Object, objects []Object) error {
|
||||||
itemsPtr, err := GetItemsPtr(list)
|
itemsPtr, err := GetItemsPtr(list)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@@ -217,6 +217,13 @@ func NewScheme() *Scheme {
|
|||||||
); err != nil {
|
); err != nil {
|
||||||
panic(err)
|
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
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user