Commit cfb6f119 authored by Derek Carr's avatar Derek Carr

Merge pull request #5851 from smarterclayton/support_input_streams

Support input streams being returned by the APIserver
parents f5840695 bfb6b053
......@@ -26,6 +26,7 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta2"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta3"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
)
// Version is the string that represents the current external default version.
......@@ -122,9 +123,15 @@ func init() {
"Namespace": true,
}
// these kinds should be excluded from the list of resources
ignoredKinds := util.NewStringSet("ListOptions", "DeleteOptions", "Status", "ContainerManifest")
// enumerate all supported versions, get the kinds, and register with the mapper how to address our resources
for _, version := range versions {
for kind := range api.Scheme.KnownTypes(version) {
if ignoredKinds.Has(kind) {
continue
}
mixedCase, found := versionMixedCase[version]
if !found {
mixedCase = false
......
......@@ -17,6 +17,7 @@ limitations under the License.
package rest
import (
"io"
"net/http"
"net/url"
......@@ -148,3 +149,21 @@ type Redirector interface {
// ResourceLocation should return the remote location of the given resource, and an optional transport to use to request it, or an error.
ResourceLocation(ctx api.Context, id string) (remoteLocation *url.URL, transport http.RoundTripper, err error)
}
// ResourceStreamer is an interface implemented by objects that prefer to be streamed from the server
// instead of decoded directly.
type ResourceStreamer interface {
// InputStream should return an io.Reader if the provided object supports streaming. The desired
// api version and a accept header (may be empty) are passed to the call. If no error occurs,
// the caller may return a content type string with the reader that indicates the type of the
// stream.
InputStream(apiVersion, acceptHeader string) (io.ReadCloser, string, error)
}
// StorageMetadata is an optional interface that callers can implement to provide additional
// information about their Storage objects.
type StorageMetadata interface {
// ProducesMIMETypes returns a list of the MIME types the specified HTTP verb (GET, POST, DELETE,
// PATCH) can respond with.
ProducesMIMETypes(verb string) []string
}
......@@ -147,6 +147,10 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
patcher, isPatcher := storage.(rest.Patcher)
_, isWatcher := storage.(rest.Watcher)
_, isRedirector := storage.(rest.Redirector)
storageMeta, isMetadata := storage.(rest.StorageMetadata)
if !isMetadata {
storageMeta = defaultStorageMetadata{}
}
var versionedDeleterObject runtime.Object
switch {
......@@ -283,56 +287,70 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
//
// test/integration/auth_test.go is currently the most comprehensive status code test
reqScope := RequestScope{
ContextFunc: ctxFn,
Codec: mapping.Codec,
APIVersion: a.group.Version,
Resource: resource,
Kind: kind,
}
for _, action := range actions {
reqScope.Namer = action.Namer
m := monitorFilter(action.Verb, resource)
switch action.Verb {
case "GET": // Get a resource.
route := ws.GET(action.Path).To(GetResource(getter, ctxFn, action.Namer, mapping.Codec)).
route := ws.GET(action.Path).To(GetResource(getter, reqScope)).
Filter(m).
Doc("read the specified " + kind).
Operation("read" + kind).
Produces(append(storageMeta.ProducesMIMETypes(action.Verb), "application/json")...).
Writes(versionedObject)
addParams(route, action.Params)
ws.Route(route)
case "LIST": // List all resources of a kind.
route := ws.GET(action.Path).To(ListResource(lister, ctxFn, action.Namer, mapping.Codec, a.group.Version, resource)).
route := ws.GET(action.Path).To(ListResource(lister, reqScope)).
Filter(m).
Doc("list objects of kind " + kind).
Operation("list" + kind).
Produces("application/json").
Writes(versionedList)
addParams(route, action.Params)
ws.Route(route)
case "PUT": // Update a resource.
route := ws.PUT(action.Path).To(UpdateResource(updater, ctxFn, action.Namer, mapping.Codec, a.group.Typer, resource, admit)).
route := ws.PUT(action.Path).To(UpdateResource(updater, reqScope, a.group.Typer, admit)).
Filter(m).
Doc("replace the specified " + kind).
Operation("replace" + kind).
Produces(append(storageMeta.ProducesMIMETypes(action.Verb), "application/json")...).
Reads(versionedObject)
addParams(route, action.Params)
ws.Route(route)
case "PATCH": // Partially update a resource
route := ws.PATCH(action.Path).To(PatchResource(patcher, ctxFn, action.Namer, mapping.Codec, a.group.Typer, resource, admit)).
route := ws.PATCH(action.Path).To(PatchResource(patcher, reqScope, a.group.Typer, admit)).
Filter(m).
Doc("partially update the specified " + kind).
// TODO: toggle patch strategy by content type
// Consumes("application/merge-patch+json", "application/json-patch+json").
Operation("patch" + kind).
Produces(append(storageMeta.ProducesMIMETypes(action.Verb), "application/json")...).
Reads(versionedObject)
addParams(route, action.Params)
ws.Route(route)
case "POST": // Create a resource.
route := ws.POST(action.Path).To(CreateResource(creater, ctxFn, action.Namer, mapping.Codec, a.group.Typer, resource, admit)).
route := ws.POST(action.Path).To(CreateResource(creater, reqScope, a.group.Typer, admit)).
Filter(m).
Doc("create a " + kind).
Operation("create" + kind).
Produces(append(storageMeta.ProducesMIMETypes(action.Verb), "application/json")...).
Reads(versionedObject)
addParams(route, action.Params)
ws.Route(route)
case "DELETE": // Delete a resource.
route := ws.DELETE(action.Path).To(DeleteResource(gracefulDeleter, isGracefulDeleter, ctxFn, action.Namer, mapping.Codec, resource, kind, admit)).
route := ws.DELETE(action.Path).To(DeleteResource(gracefulDeleter, isGracefulDeleter, reqScope, admit)).
Filter(m).
Doc("delete a " + kind).
Operation("delete" + kind)
Operation("delete" + kind).
Produces(append(storageMeta.ProducesMIMETypes(action.Verb), "application/json")...)
if isGracefulDeleter {
route.Reads(versionedDeleterObject)
}
......@@ -343,6 +361,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
Filter(m).
Doc("watch a particular " + kind).
Operation("watch" + kind).
Produces("application/json").
Writes(versionedObject)
addParams(route, action.Params)
ws.Route(route)
......@@ -351,6 +370,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
Filter(m).
Doc("watch a list of " + kind).
Operation("watch" + kind + "list").
Produces("application/json").
Writes(versionedList)
addParams(route, action.Params)
ws.Route(route)
......@@ -630,3 +650,13 @@ func addParams(route *restful.RouteBuilder, params []*restful.Parameter) {
route.Param(param)
}
}
// defaultStorageMetadata provides default answers to rest.StorageMetadata.
type defaultStorageMetadata struct{}
// defaultStorageMetadata implements rest.StorageMetadata
var _ rest.StorageMetadata = defaultStorageMetadata{}
func (defaultStorageMetadata) ProducesMIMETypes(verb string) []string {
return nil
}
......@@ -20,6 +20,7 @@ import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"path"
......@@ -196,6 +197,26 @@ func APIVersionHandler(versions ...string) restful.RouteFunction {
}
}
// write renders a returned runtime.Object to the response as a stream or an encoded object.
func write(statusCode int, apiVersion string, codec runtime.Codec, object runtime.Object, w http.ResponseWriter, req *http.Request) {
if stream, ok := object.(rest.ResourceStreamer); ok {
out, contentType, err := stream.InputStream(apiVersion, req.Header.Get("Accept"))
if err != nil {
errorJSONFatal(err, codec, w)
return
}
defer out.Close()
if len(contentType) == 0 {
contentType = "application/octet-stream"
}
w.Header().Set("Content-Type", contentType)
w.WriteHeader(statusCode)
io.Copy(w, out)
return
}
writeJSON(statusCode, codec, object, w)
}
// writeJSON renders an object as JSON to the response.
func writeJSON(statusCode int, codec runtime.Codec, object runtime.Object, w http.ResponseWriter) {
output, err := codec.Encode(object)
......
......@@ -21,6 +21,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
......@@ -122,7 +123,8 @@ func init() {
// defaultAPIServer exposes nested objects for testability.
type defaultAPIServer struct {
http.Handler
group *APIGroupVersion
group *APIGroupVersion
container *restful.Container
}
// uses the default settings
......@@ -169,7 +171,7 @@ func handleInternal(storage map[string]rest.Storage, admissionControl admission.
ws := new(restful.WebService)
InstallSupport(mux, ws)
container.Add(ws)
return &defaultAPIServer{mux, group}
return &defaultAPIServer{mux, group, container}
}
type Simple struct {
......@@ -212,6 +214,8 @@ type SimpleRESTStorage struct {
updated *Simple
created *Simple
stream *SimpleStream
deleted string
deleteOptions *api.DeleteOptions
......@@ -243,8 +247,34 @@ func (storage *SimpleRESTStorage) List(ctx api.Context, label labels.Selector, f
return result, storage.errors["list"]
}
type SimpleStream struct {
version string
accept string
contentType string
err error
io.Reader
closed bool
}
func (s *SimpleStream) Close() error {
s.closed = true
return nil
}
func (s *SimpleStream) IsAnAPIObject() {}
func (s *SimpleStream) InputStream(version, accept string) (io.ReadCloser, string, error) {
s.version = version
s.accept = accept
return s, s.contentType, s.err
}
func (storage *SimpleRESTStorage) Get(ctx api.Context, id string) (runtime.Object, error) {
storage.checkContext(ctx)
if id == "binary" {
return storage.stream, storage.errors["get"]
}
return api.Scheme.CopyOrDie(&storage.item), storage.errors["get"]
}
......@@ -343,6 +373,15 @@ func (storage LegacyRESTStorage) Delete(ctx api.Context, id string) (runtime.Obj
return storage.SimpleRESTStorage.Delete(ctx, id, nil)
}
type MetadataRESTStorage struct {
*SimpleRESTStorage
types []string
}
func (m *MetadataRESTStorage) ProducesMIMETypes(method string) []string {
return m.types
}
func extractBody(response *http.Response, object runtime.Object) (string, error) {
defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body)
......@@ -646,6 +685,26 @@ func TestSelfLinkSkipsEmptyName(t *testing.T) {
}
}
func TestMetadata(t *testing.T) {
simpleStorage := &MetadataRESTStorage{&SimpleRESTStorage{}, []string{"text/plain"}}
h := handle(map[string]rest.Storage{"simple": simpleStorage})
ws := h.(*defaultAPIServer).container.RegisteredWebServices()
if len(ws) == 0 {
t.Fatal("no web services registered")
}
matches := map[string]int{}
for _, w := range ws {
for _, r := range w.Routes() {
s := strings.Join(r.Produces, ",")
i := matches[s]
matches[s] = i + 1
}
}
if matches["text/plain,application/json"] == 0 || matches["application/json"] == 0 || matches["*/*"] == 0 || len(matches) != 3 {
t.Errorf("unexpected mime types: %v", matches)
}
}
func TestGet(t *testing.T) {
storage := map[string]rest.Storage{}
simpleStorage := SimpleRESTStorage{
......@@ -685,6 +744,36 @@ func TestGet(t *testing.T) {
}
}
func TestGetBinary(t *testing.T) {
simpleStorage := SimpleRESTStorage{
stream: &SimpleStream{
contentType: "text/plain",
Reader: bytes.NewBufferString("response data"),
},
}
stream := simpleStorage.stream
server := httptest.NewServer(handle(map[string]rest.Storage{"simple": &simpleStorage}))
defer server.Close()
req, _ := http.NewRequest("GET", server.URL+"/api/version/simple/binary", nil)
req.Header.Add("Accept", "text/other, */*")
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Fatalf("unexpected response: %#v", resp)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if !stream.closed || stream.version != "version" || stream.accept != "text/other, */*" ||
resp.Header.Get("Content-Type") != stream.contentType || string(body) != "response data" {
t.Errorf("unexpected stream: %#v", stream)
}
}
func TestGetAlternateSelfLink(t *testing.T) {
storage := map[string]rest.Storage{}
simpleStorage := SimpleRESTStorage{
......
......@@ -366,7 +366,7 @@ func TestWatchControllers(t *testing.T) {
select {
case <-received:
case <-time.After(10 * time.Millisecond):
case <-time.After(100 * time.Millisecond):
t.Errorf("Expected 1 call but got 0")
}
}
......@@ -54,6 +54,12 @@ type Converter struct {
// Map from a type to a function which applies defaults.
defaultingFuncs map[reflect.Type]reflect.Value
// Map from an input type to a function which can apply a key name mapping
inputFieldMappingFuncs map[reflect.Type]FieldMappingFunc
// Map from an input type to a set of default conversion flags.
inputDefaultFlags map[reflect.Type]FieldMatchingFlags
// If non-nil, will be called to print helpful debugging info. Quite verbose.
Debug DebugLogger
......@@ -71,6 +77,9 @@ func NewConverter() *Converter {
nameFunc: func(t reflect.Type) string { return t.Name() },
structFieldDests: map[typeNamePair][]typeNamePair{},
structFieldSources: map[typeNamePair][]typeNamePair{},
inputFieldMappingFuncs: map[reflect.Type]FieldMappingFunc{},
inputDefaultFlags: map[reflect.Type]FieldMatchingFlags{},
}
}
......@@ -98,12 +107,18 @@ type Scope interface {
Meta() *Meta
}
// FieldMappingFunc can convert an input field value into different values, depending on
// the value of the source or destination struct tags.
type FieldMappingFunc func(key string, sourceTag, destTag reflect.StructTag) (source string, dest string)
// Meta is supplied by Scheme, when it calls Convert.
type Meta struct {
SrcVersion string
DestVersion string
// TODO: If needed, add a user data field here.
// KeyNameMapping is an optional function which may map the listed key (field name)
// into a source and destination value.
KeyNameMapping FieldMappingFunc
}
// scope contains information about an ongoing conversion.
......@@ -301,6 +316,21 @@ func (c *Converter) RegisterDefaultingFunc(defaultingFunc interface{}) error {
return nil
}
// RegisterInputDefaults registers a field name mapping function, used when converting
// from maps to structs. Inputs to the conversion methods are checked for this type and a mapping
// applied automatically if the input matches in. A set of default flags for the input conversion
// may also be provided, which will be used when no explicit flags are requested.
func (c *Converter) RegisterInputDefaults(in interface{}, fn FieldMappingFunc, defaultFlags FieldMatchingFlags) error {
fv := reflect.ValueOf(in)
ft := fv.Type()
if ft.Kind() != reflect.Ptr {
return fmt.Errorf("expected pointer 'in' argument, got: %v", ft)
}
c.inputFieldMappingFuncs[ft] = fn
c.inputDefaultFlags[ft] = defaultFlags
return nil
}
// FieldMatchingFlags contains a list of ways in which struct fields could be
// copied. These constants may be | combined.
type FieldMatchingFlags int
......@@ -538,10 +568,16 @@ func (c *Converter) defaultConvert(sv, dv reflect.Value, scope *scope) error {
return nil
}
var stringType = reflect.TypeOf("")
func toKVValue(v reflect.Value) kvValue {
switch v.Kind() {
case reflect.Struct:
return structAdaptor(v)
case reflect.Map:
if v.Type().Key().AssignableTo(stringType) {
return stringMapAdaptor(v)
}
}
return nil
......@@ -561,15 +597,48 @@ type kvValue interface {
confirmSet(key string, v reflect.Value) bool
}
type stringMapAdaptor reflect.Value
func (a stringMapAdaptor) len() int {
return reflect.Value(a).Len()
}
func (a stringMapAdaptor) keys() []string {
v := reflect.Value(a)
keys := make([]string, v.Len())
for i, v := range v.MapKeys() {
if v.IsNil() {
continue
}
switch t := v.Interface().(type) {
case string:
keys[i] = t
}
}
return keys
}
func (a stringMapAdaptor) tagOf(key string) reflect.StructTag {
return ""
}
func (a stringMapAdaptor) value(key string) reflect.Value {
return reflect.Value(a).MapIndex(reflect.ValueOf(key))
}
func (a stringMapAdaptor) confirmSet(key string, v reflect.Value) bool {
return true
}
type structAdaptor reflect.Value
func (sa structAdaptor) len() int {
v := reflect.Value(sa)
func (a structAdaptor) len() int {
v := reflect.Value(a)
return v.Type().NumField()
}
func (sa structAdaptor) keys() []string {
v := reflect.Value(sa)
func (a structAdaptor) keys() []string {
v := reflect.Value(a)
t := v.Type()
keys := make([]string, t.NumField())
for i := range keys {
......@@ -578,8 +647,8 @@ func (sa structAdaptor) keys() []string {
return keys
}
func (sa structAdaptor) tagOf(key string) reflect.StructTag {
v := reflect.Value(sa)
func (a structAdaptor) tagOf(key string) reflect.StructTag {
v := reflect.Value(a)
field, ok := v.Type().FieldByName(key)
if ok {
return field.Tag
......@@ -587,12 +656,12 @@ func (sa structAdaptor) tagOf(key string) reflect.StructTag {
return ""
}
func (sa structAdaptor) value(key string) reflect.Value {
v := reflect.Value(sa)
func (a structAdaptor) value(key string) reflect.Value {
v := reflect.Value(a)
return v.FieldByName(key)
}
func (sa structAdaptor) confirmSet(key string, v reflect.Value) bool {
func (a structAdaptor) confirmSet(key string, v reflect.Value) bool {
return true
}
......@@ -608,6 +677,12 @@ func (c *Converter) convertKV(skv, dkv kvValue, scope *scope) error {
if scope.flags.IsSet(SourceToDest) {
lister = skv
}
var mapping FieldMappingFunc
if scope.meta != nil && scope.meta.KeyNameMapping != nil {
mapping = scope.meta.KeyNameMapping
}
for _, key := range lister.keys() {
if found, err := c.checkField(key, skv, dkv, scope); found {
if err != nil {
......@@ -615,23 +690,31 @@ func (c *Converter) convertKV(skv, dkv kvValue, scope *scope) error {
}
continue
}
df := dkv.value(key)
sf := skv.value(key)
stag := skv.tagOf(key)
dtag := dkv.tagOf(key)
skey := key
dkey := key
if mapping != nil {
skey, dkey = scope.meta.KeyNameMapping(key, stag, dtag)
}
df := dkv.value(dkey)
sf := skv.value(skey)
if !df.IsValid() || !sf.IsValid() {
switch {
case scope.flags.IsSet(IgnoreMissingFields):
// No error.
case scope.flags.IsSet(SourceToDest):
return scope.error("%v not present in dest", key)
return scope.error("%v not present in dest", dkey)
default:
return scope.error("%v not present in src", key)
return scope.error("%v not present in src", skey)
}
continue
}
scope.srcStack.top().key = key
scope.srcStack.top().tag = skv.tagOf(key)
scope.destStack.top().key = key
scope.destStack.top().tag = dkv.tagOf(key)
scope.srcStack.top().key = skey
scope.srcStack.top().tag = stag
scope.destStack.top().key = dkey
scope.destStack.top().tag = dtag
if err := c.convert(sf, df, scope); err != nil {
return err
}
......
......@@ -20,6 +20,7 @@ import (
"fmt"
"reflect"
"strconv"
"strings"
"testing"
"github.com/google/gofuzz"
......@@ -173,6 +174,105 @@ func TestConverter_CallsRegisteredFunctions(t *testing.T) {
}
}
func TestConverter_MapsStringArrays(t *testing.T) {
type A struct {
Foo string
Baz int
Other string
}
c := NewConverter()
c.Debug = t
if err := c.RegisterConversionFunc(func(input *[]string, out *string, s Scope) error {
if len(*input) == 0 {
*out = ""
}
*out = (*input)[0]
return nil
}); err != nil {
t.Fatalf("unexpected error %v", err)
}
x := map[string][]string{
"Foo": {"bar"},
"Baz": {"1"},
"Other": {"", "test"},
"other": {"wrong"},
}
y := A{"test", 2, "something"}
if err := c.Convert(&x, &y, AllowDifferentFieldTypeNames, nil); err == nil {
t.Error("unexpected non-error")
}
if err := c.RegisterConversionFunc(func(input *[]string, out *int, s Scope) error {
if len(*input) == 0 {
*out = 0
}
str := (*input)[0]
i, err := strconv.Atoi(str)
if err != nil {
return err
}
*out = i
return nil
}); err != nil {
t.Fatalf("unexpected error %v", err)
}
if err := c.Convert(&x, &y, AllowDifferentFieldTypeNames, nil); err != nil {
t.Fatalf("unexpected error %v", err)
}
if !reflect.DeepEqual(y, A{"bar", 1, ""}) {
t.Errorf("unexpected result: %#v", y)
}
}
func TestConverter_MapsStringArraysWithMappingKey(t *testing.T) {
type A struct {
Foo string `json:"test"`
Baz int
Other string
}
c := NewConverter()
c.Debug = t
if err := c.RegisterConversionFunc(func(input *[]string, out *string, s Scope) error {
if len(*input) == 0 {
*out = ""
}
*out = (*input)[0]
return nil
}); err != nil {
t.Fatalf("unexpected error %v", err)
}
x := map[string][]string{
"Foo": {"bar"},
"test": {"baz"},
}
y := A{"", 0, ""}
if err := c.Convert(&x, &y, AllowDifferentFieldTypeNames|IgnoreMissingFields, &Meta{}); err != nil {
t.Fatalf("unexpected error %v", err)
}
if !reflect.DeepEqual(y, A{"bar", 0, ""}) {
t.Errorf("unexpected result: %#v", y)
}
mapping := func(key string, sourceTag, destTag reflect.StructTag) (source string, dest string) {
if s := destTag.Get("json"); len(s) > 0 {
return strings.SplitN(s, ",", 2)[0], key
}
return key, key
}
if err := c.Convert(&x, &y, AllowDifferentFieldTypeNames|IgnoreMissingFields, &Meta{KeyNameMapping: mapping}); err != nil {
t.Fatalf("unexpected error %v", err)
}
if !reflect.DeepEqual(y, A{"baz", 0, ""}) {
t.Errorf("unexpected result: %#v", y)
}
}
func TestConverter_fuzz(t *testing.T) {
// Use the same types from the scheme test.
table := []struct {
......
......@@ -58,7 +58,8 @@ func (s *Scheme) Decode(data []byte) (interface{}, error) {
if err != nil {
return nil, err
}
if err := s.converter.Convert(obj, objOut, 0, s.generateConvertMeta(version, s.InternalVersion)); err != nil {
flags, meta := s.generateConvertMeta(version, s.InternalVersion, obj)
if err := s.converter.Convert(obj, objOut, flags, meta); err != nil {
return nil, err
}
obj = objOut
......@@ -101,7 +102,8 @@ func (s *Scheme) DecodeInto(data []byte, obj interface{}) error {
if err := json.Unmarshal(data, external); err != nil {
return err
}
if err := s.converter.Convert(external, obj, 0, s.generateConvertMeta(dataVersion, objVersion)); err != nil {
flags, meta := s.generateConvertMeta(dataVersion, objVersion, external)
if err := s.converter.Convert(external, obj, flags, meta); err != nil {
return err
}
......
......@@ -67,7 +67,8 @@ func (s *Scheme) EncodeToVersion(obj interface{}, destVersion string) (data []by
if err != nil {
return nil, err
}
err = s.converter.Convert(obj, objOut, 0, s.generateConvertMeta(objVersion, destVersion))
flags, meta := s.generateConvertMeta(objVersion, destVersion, obj)
err = s.converter.Convert(obj, objOut, flags, meta)
if err != nil {
return nil, err
}
......
......@@ -232,6 +232,14 @@ func (s *Scheme) AddDefaultingFuncs(defaultingFuncs ...interface{}) error {
return nil
}
// RegisterInputDefaults sets the provided field mapping function and field matching
// as the defaults for the provided input type. The fn may be nil, in which case no
// mapping will happen by default. Use this method to register a mechanism for handling
// a specific input type in conversion, such as a map[string]string to structs.
func (s *Scheme) RegisterInputDefaults(in interface{}, fn FieldMappingFunc, defaultFlags FieldMatchingFlags) error {
return s.converter.RegisterInputDefaults(in, fn, defaultFlags)
}
// Convert will attempt to convert in into out. Both must be pointers. For easy
// testing of conversion functions. Returns an error if the conversion isn't
// possible. You can call this with types that haven't been registered (for example,
......@@ -247,7 +255,11 @@ func (s *Scheme) Convert(in, out interface{}) error {
if v, _, err := s.ObjectVersionAndKind(out); err == nil {
outVersion = v
}
return s.converter.Convert(in, out, AllowDifferentFieldTypeNames, s.generateConvertMeta(inVersion, outVersion))
flags, meta := s.generateConvertMeta(inVersion, outVersion, in)
if flags == 0 {
flags = AllowDifferentFieldTypeNames
}
return s.converter.Convert(in, out, flags, meta)
}
// ConvertToVersion attempts to convert an input object to its matching Kind in another
......@@ -279,7 +291,8 @@ func (s *Scheme) ConvertToVersion(in interface{}, outVersion string) (interface{
return nil, err
}
if err := s.converter.Convert(in, out, 0, s.generateConvertMeta(inVersion, outVersion)); err != nil {
flags, meta := s.generateConvertMeta(inVersion, outVersion, in)
if err := s.converter.Convert(in, out, flags, meta); err != nil {
return nil, err
}
......@@ -290,11 +303,18 @@ func (s *Scheme) ConvertToVersion(in interface{}, outVersion string) (interface{
return out, nil
}
// Converter allows access to the converter for the scheme
func (s *Scheme) Converter() *Converter {
return s.converter
}
// generateConvertMeta constructs the meta value we pass to Convert.
func (s *Scheme) generateConvertMeta(srcVersion, destVersion string) *Meta {
return &Meta{
SrcVersion: srcVersion,
DestVersion: destVersion,
func (s *Scheme) generateConvertMeta(srcVersion, destVersion string, in interface{}) (FieldMatchingFlags, *Meta) {
t := reflect.TypeOf(in)
return s.converter.inputDefaultFlags[t], &Meta{
SrcVersion: srcVersion,
DestVersion: destVersion,
KeyNameMapping: s.converter.inputFieldMappingFuncs[t],
}
}
......
......@@ -35,6 +35,10 @@ type Selector interface {
// requires.
RequiresExactMatch(field string) (value string, found bool)
// Transform returns a new copy of the selector after TransformFunc has been
// applied to the entire selector, or an error if fn returns an error.
Transform(fn TransformFunc) (Selector, error)
// String returns a human readable string that represents this selector.
String() string
}
......@@ -63,6 +67,14 @@ func (t *hasTerm) RequiresExactMatch(field string) (value string, found bool) {
return "", false
}
func (t *hasTerm) Transform(fn TransformFunc) (Selector, error) {
field, value, err := fn(t.field, t.value)
if err != nil {
return nil, err
}
return &hasTerm{field, value}, nil
}
func (t *hasTerm) String() string {
return fmt.Sprintf("%v=%v", t.field, t.value)
}
......@@ -83,6 +95,14 @@ func (t *notHasTerm) RequiresExactMatch(field string) (value string, found bool)
return "", false
}
func (t *notHasTerm) Transform(fn TransformFunc) (Selector, error) {
field, value, err := fn(t.field, t.value)
if err != nil {
return nil, err
}
return &notHasTerm{field, value}, nil
}
func (t *notHasTerm) String() string {
return fmt.Sprintf("%v!=%v", t.field, t.value)
}
......@@ -125,6 +145,18 @@ func (t andTerm) RequiresExactMatch(field string) (string, bool) {
return "", false
}
func (t andTerm) Transform(fn TransformFunc) (Selector, error) {
next := make([]Selector, len([]Selector(t)))
for i, s := range []Selector(t) {
n, err := s.Transform(fn)
if err != nil {
return nil, err
}
next[i] = n
}
return andTerm(next), nil
}
func (t andTerm) String() string {
var terms []string
for _, q := range t {
......@@ -183,31 +215,19 @@ func parseSelector(selector string, fn TransformFunc) (Selector, error) {
continue
}
if lhs, rhs, ok := try(part, "!="); ok {
lhs, rhs, err := fn(lhs, rhs)
if err != nil {
return nil, err
}
items = append(items, &notHasTerm{field: lhs, value: rhs})
} else if lhs, rhs, ok := try(part, "=="); ok {
lhs, rhs, err := fn(lhs, rhs)
if err != nil {
return nil, err
}
items = append(items, &hasTerm{field: lhs, value: rhs})
} else if lhs, rhs, ok := try(part, "="); ok {
lhs, rhs, err := fn(lhs, rhs)
if err != nil {
return nil, err
}
items = append(items, &hasTerm{field: lhs, value: rhs})
} else {
return nil, fmt.Errorf("invalid selector: '%s'; can't understand '%s'", selector, part)
}
}
if len(items) == 1 {
return items[0], nil
return items[0].Transform(fn)
}
return andTerm(items), nil
return andTerm(items).Transform(fn)
}
// OneTermEqualSelector returns an object that matches objects where one field/field equals one value.
......
/*
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
}
/*
Copyright 2014 Google Inc. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package runtime_test
import (
"reflect"
"testing"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
)
type InternalComplex struct {
TypeMeta
String string
Integer int
Integer64 int64
Int64 int64
}
type ExternalComplex struct {
TypeMeta `json:",inline"`
String string `json:"string" description:"testing"`
Integer int `json:"int"`
Integer64 int64 `json:",omitempty"`
Int64 int64
}
func (*InternalComplex) IsAnAPIObject() {}
func (*ExternalComplex) IsAnAPIObject() {}
func TestStringMapConversion(t *testing.T) {
scheme := runtime.NewScheme()
scheme.Log(t)
scheme.AddKnownTypeWithName("", "Complex", &InternalComplex{})
scheme.AddKnownTypeWithName("external", "Complex", &ExternalComplex{})
testCases := map[string]struct {
input map[string][]string
errFn func(error) bool
expected runtime.Object
}{
"ignores omitempty": {
input: map[string][]string{
"String": {"not_used"},
"string": {"value"},
"int": {"1"},
"Integer64": {"2"},
},
expected: &ExternalComplex{String: "value", Integer: 1},
},
"returns error on bad int": {
input: map[string][]string{
"int": {"a"},
},
errFn: func(err error) bool { return err != nil },
expected: &ExternalComplex{},
},
"parses int64": {
input: map[string][]string{
"Int64": {"-1"},
},
expected: &ExternalComplex{Int64: -1},
},
"returns error on bad int64": {
input: map[string][]string{
"Int64": {"a"},
},
errFn: func(err error) bool { return err != nil },
expected: &ExternalComplex{},
},
}
for k, tc := range testCases {
out := &ExternalComplex{}
if err := scheme.Convert(&tc.input, out); (tc.errFn == nil && err != nil) || (tc.errFn != nil && !tc.errFn(err)) {
t.Errorf("%s: unexpected error: %v", k, err)
continue
} else if err != nil {
continue
}
if !reflect.DeepEqual(out, tc.expected) {
t.Errorf("%s: unexpected output: %#v", k, out)
}
}
}
......@@ -23,6 +23,7 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/conversion"
)
// TODO: move me to pkg/api/meta
func IsListType(obj Object) bool {
_, err := GetItemsPtr(obj)
return err == nil
......@@ -32,6 +33,7 @@ func IsListType(obj Object) bool {
// If 'list' doesn't have an Items member, it's not really a list type
// and an error will be returned.
// This function will either return a pointer to a slice, or an error, but not both.
// TODO: move me to pkg/api/meta
func GetItemsPtr(list Object) (interface{}, error) {
v, err := conversion.EnforcePtr(list)
if err != nil {
......@@ -57,6 +59,7 @@ func GetItemsPtr(list Object) (interface{}, error) {
// ExtractList returns obj's Items element as an array of runtime.Objects.
// Returns an error if obj is not a List type (does not have an Items member).
// TODO: move me to pkg/api/meta
func ExtractList(obj Object) ([]Object, error) {
itemsPtr, err := GetItemsPtr(obj)
if err != nil {
......@@ -90,6 +93,7 @@ var objectSliceType = reflect.TypeOf([]Object{})
// objects.
// Returns an error if list is not a List type (does not have an Items member),
// or if any of the objects are not of the right type.
// TODO: move me to pkg/api/meta
func SetList(list Object, objects []Object) error {
itemsPtr, err := GetItemsPtr(list)
if err != nil {
......
......@@ -217,6 +217,13 @@ func NewScheme() *Scheme {
); err != nil {
panic(err)
}
// Enable map[string][]string conversions by default
if err := s.raw.AddConversionFuncs(DefaultStringConversions...); err != nil {
panic(err)
}
if err := s.raw.RegisterInputDefaults(&map[string][]string{}, JSONKeyMapper, conversion.AllowDifferentFieldTypeNames|conversion.IgnoreMissingFields); err != nil {
panic(err)
}
return s
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment