Files
kubernetes/pkg/api/helper.go
Daniel Smith 2396bdfa1b Incorporate new types into versioned api system.
* Made externalize/internalize generic to prevent boilerplate.
* Add fuzz testing.
* All objects pass fuzz tests now.
* This turned up some things we'll need to fix eventually. Left TODOs.
2014-07-29 15:46:57 -07:00

515 lines
16 KiB
Go

/*
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 api
import (
"encoding/json"
"fmt"
"reflect"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1"
"gopkg.in/v1/yaml"
)
var versionMap = map[string]map[string]reflect.Type{}
// typeNamePath records go's name and path of a go struct.
type typeNamePath struct {
typeName string
typePath string
}
// typeNamePathToVersion allows one to figure out the version for a
// given go object.
var typeNamePathToVersion = map[typeNamePath]string{}
// ConversionFunc knows how to translate a type from one api version to another.
type ConversionFunc func(input interface{}) (output interface{}, err error)
// typeTuple indexes a conversionFunc by source and dest version, and
// the name of the type it operates on.
type typeTuple struct {
sourceVersion string
destVersion string
// Go name of this type.
typeName string
}
// conversionFuncs is a map of all known conversion functions.
var conversionFuncs = map[typeTuple]ConversionFunc{}
func init() {
AddKnownTypes("",
PodList{},
Pod{},
ReplicationControllerList{},
ReplicationController{},
ServiceList{},
Service{},
MinionList{},
Minion{},
Status{},
ServerOpList{},
ServerOp{},
ContainerManifestList{},
Endpoints{},
)
AddKnownTypes("v1beta1",
v1beta1.PodList{},
v1beta1.Pod{},
v1beta1.ReplicationControllerList{},
v1beta1.ReplicationController{},
v1beta1.ServiceList{},
v1beta1.Service{},
v1beta1.MinionList{},
v1beta1.Minion{},
v1beta1.Status{},
v1beta1.ServerOpList{},
v1beta1.ServerOp{},
v1beta1.ContainerManifestList{},
v1beta1.Endpoints{},
)
defaultCopyList := []string{
"PodList",
"Pod",
"ReplicationControllerList",
"ReplicationController",
"ServiceList",
"Service",
"MinionList",
"Minion",
"Status",
"ServerOpList",
"ServerOp",
"ContainerManifestList",
"Endpoints",
}
AddDefaultCopy("", "v1beta1", defaultCopyList...)
AddDefaultCopy("v1beta1", "", defaultCopyList...)
}
// AddKnownTypes registers the types of the arguments to the marshaller of the package api.
// Encode() refuses the object unless its type is registered with AddKnownTypes.
func AddKnownTypes(version string, types ...interface{}) {
knownTypes, found := versionMap[version]
if !found {
knownTypes = map[string]reflect.Type{}
versionMap[version] = knownTypes
}
for _, obj := range types {
t := reflect.TypeOf(obj)
if t.Kind() != reflect.Struct {
panic("All types must be structs.")
}
knownTypes[t.Name()] = t
typeNamePathToVersion[typeNamePath{
typeName: t.Name(),
typePath: t.PkgPath(),
}] = version
}
}
// New returns a new API object of the given version ("" for internal
// representation) and name, or an error if it hasn't been registered.
func New(versionName, typeName string) (interface{}, error) {
if types, ok := versionMap[versionName]; ok {
if t, ok := types[typeName]; ok {
return reflect.New(t).Interface(), nil
}
return nil, fmt.Errorf("No type '%v' for version '%v'", typeName, versionName)
}
return nil, fmt.Errorf("No version '%v'", versionName)
}
// AddExternalConversion adds a function to the list of conversion functions. The given
// function should know how to convert the internal representation of 'typeName' to the
// external, versioned representation ("v1beta1").
// TODO: When we make the next api version, this function will have to add a destination
// version parameter.
func AddExternalConversion(typeName string, fn ConversionFunc) {
conversionFuncs[typeTuple{"", "v1beta1", typeName}] = fn
}
// AddInternalConversion adds a function to the list of conversion functions. The given
// function should know how to convert the external, versioned representation of 'typeName'
// to the internal representation.
// TODO: When we make the next api version, this function will have to add a source
// version parameter.
func AddInternalConversion(typeName string, fn ConversionFunc) {
conversionFuncs[typeTuple{"v1beta1", "", typeName}] = fn
}
// AddDefaultCopy registers a general copying function for turning objects of version
// sourceVersion into the same object of version destVersion.
func AddDefaultCopy(sourceVersion, destVersion string, types ...string) {
for i := range types {
t := types[i]
conversionFuncs[typeTuple{sourceVersion, destVersion, t}] = func(in interface{}) (interface{}, error) {
out, err := New(destVersion, t)
if err != nil {
return nil, err
}
err = DefaultCopy(in, out)
if err != nil {
return nil, err
}
return out, nil
}
}
}
// FindJSONBase takes an arbitary api type, returns pointer to its JSONBase field.
// obj must be a pointer to an api type.
func FindJSONBase(obj interface{}) (JSONBaseInterface, error) {
v, err := enforcePtr(obj)
if err != nil {
return nil, err
}
t := v.Type()
name := t.Name()
if v.Kind() != reflect.Struct {
return nil, fmt.Errorf("expected struct, but got %v: %v (%#v)", v.Kind(), name, v.Interface())
}
jsonBase := v.FieldByName("JSONBase")
if !jsonBase.IsValid() {
return nil, fmt.Errorf("struct %v lacks embedded JSON type", name)
}
g, err := newGenericJSONBase(jsonBase)
if err != nil {
return nil, err
}
return g, nil
}
// FindJSONBaseRO takes an arbitary api type, return a copy of its JSONBase field.
// obj may be a pointer to an api type, or a non-pointer struct api type.
func FindJSONBaseRO(obj interface{}) (JSONBase, error) {
v := reflect.ValueOf(obj)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
if v.Kind() != reflect.Struct {
return JSONBase{}, fmt.Errorf("expected struct, but got %v (%#v)", v.Type().Name(), v.Interface())
}
jsonBase := v.FieldByName("JSONBase")
if !jsonBase.IsValid() {
return JSONBase{}, fmt.Errorf("struct %v lacks embedded JSON type", v.Type().Name())
}
return jsonBase.Interface().(JSONBase), nil
}
// EncodeOrDie is a version of Encode which will panic instead of returning an error. For tests.
func EncodeOrDie(obj interface{}) string {
bytes, err := Encode(obj)
if err != nil {
panic(err)
}
return string(bytes)
}
// Encode turns the given api object into an appropriate JSON string.
// Will return an error if the object doesn't have an embedded JSONBase.
// Obj may be a pointer to a struct, or a struct. If a struct, a copy
// must be made. If a pointer, the object may be modified before encoding,
// but will be put back into its original state before returning.
//
// Memory/wire format differences:
// * Having to keep track of the Kind and APIVersion fields makes tests
// very annoying, so the rule is that they are set only in wire format
// (json), not when in native (memory) format. This is possible because
// both pieces of information are implicit in the go typed object.
// * An exception: note that, if there are embedded API objects of known
// type, for example, PodList{... Items []Pod ...}, these embedded
// objects must be of the same version of the object they are embedded
// within, and their APIVersion and Kind must both be empty.
// * Note that the exception does not apply to the APIObject type, which
// recursively does Encode()/Decode(), and is capable of expressing any
// API object.
// * Only versioned objects should be encoded. This means that, if you pass
// a native object, Encode will convert it to a versioned object. For
// example, an api.Pod will get converted to a v1beta1.Pod. However, if
// you pass in an object that's already versioned (v1beta1.Pod), Encode
// will not modify it.
//
// The purpose of the above complex conversion behavior is to allow us to
// change the memory format yet not break compatibility with any stored
// objects, whether they be in our storage layer (e.g., etcd), or in user's
// config files.
//
// TODO/next steps: When we add our second versioned type, this package will
// need a version of Encode that lets you choose the wire version. A configurable
// default will be needed, to allow operating in clusters that haven't yet
// upgraded.
//
func Encode(obj interface{}) (data []byte, err error) {
obj = maybeCopy(obj)
obj, err = maybeExternalize(obj)
if err != nil {
return nil, err
}
jsonBase, err := prepareEncode(obj)
if err != nil {
return nil, err
}
data, err = json.MarshalIndent(obj, "", " ")
if err != nil {
return nil, err
}
// Leave these blank in memory.
jsonBase.SetKind("")
jsonBase.SetAPIVersion("")
return data, err
}
// Returns the API version of the go object, or an error if it's not a
// pointer or is unregistered.
func objAPIVersionAndName(obj interface{}) (apiVersion, name string, err error) {
v, err := enforcePtr(obj)
if err != nil {
return "", "", err
}
t := v.Type()
key := typeNamePath{
typeName: t.Name(),
typePath: t.PkgPath(),
}
if version, ok := typeNamePathToVersion[key]; !ok {
return "", "", fmt.Errorf("Unregistered type: %#v", key)
} else {
return version, t.Name(), nil
}
}
// maybeExternalize converts obj to an external object if it isn't one already.
// obj must be a pointer.
func maybeExternalize(obj interface{}) (interface{}, error) {
version, _, err := objAPIVersionAndName(obj)
if err != nil {
return nil, err
}
if version != "" {
// Object is already of an external versioned type.
return obj, nil
}
return externalize(obj)
}
// maybeCopy copies obj if it is not a pointer, to get a settable/addressable
// object. Guaranteed to return a pointer.
func maybeCopy(obj interface{}) interface{} {
v := reflect.ValueOf(obj)
if v.Kind() == reflect.Ptr {
return obj
}
v2 := reflect.New(v.Type())
v2.Elem().Set(v)
return v2.Interface()
}
// prepareEncode sets the APIVersion and Kind fields to match the go type in obj.
// Returns an error if the (version, name) pair isn't registered for the type or
// if the type is an internal, non-versioned object.
func prepareEncode(obj interface{}) (JSONBaseInterface, error) {
version, name, err := objAPIVersionAndName(obj)
if err != nil {
return nil, err
}
if version == "" {
return nil, fmt.Errorf("No version for '%v' (%#v); extremely inadvisable to write it in wire format.", name, obj)
}
jsonBase, err := FindJSONBase(obj)
if err != nil {
return nil, err
}
knownTypes, found := versionMap[version]
if !found {
return nil, fmt.Errorf("struct %s, %s won't be unmarshalable because it's not in known versions", version, name)
}
if _, contains := knownTypes[name]; !contains {
return nil, fmt.Errorf("struct %s, %s won't be unmarshalable because it's not in knownTypes", version, name)
}
jsonBase.SetAPIVersion(version)
jsonBase.SetKind(name)
return jsonBase, nil
}
// Ensures that obj is a pointer of some sort. Returns a reflect.Value of the
// dereferenced pointer, ensuring that it is settable/addressable.
// Returns an error if this is not possible.
func enforcePtr(obj interface{}) (reflect.Value, error) {
v := reflect.ValueOf(obj)
if v.Kind() != reflect.Ptr {
return reflect.Value{}, fmt.Errorf("expected pointer, but got %v", v.Type().Name())
}
return v.Elem(), nil
}
// VersionAndKind will return the APIVersion and Kind of the given wire-format
// enconding of an APIObject, or an error.
func VersionAndKind(data []byte) (version, kind string, err error) {
findKind := struct {
Kind string `json:"kind,omitempty" yaml:"kind,omitempty"`
APIVersion string `json:"apiVersion,omitempty" yaml:"apiVersion,omitempty"`
}{}
// yaml is a superset of json, so we use it to decode here. That way,
// we understand both.
err = yaml.Unmarshal(data, &findKind)
if err != nil {
return "", "", fmt.Errorf("couldn't get version/kind: %v", err)
}
return findKind.APIVersion, findKind.Kind, nil
}
// Decode converts a YAML or JSON string back into a pointer to an api object.
// Deduces the type based upon the APIVersion and Kind fields, which are set
// by Encode. Only versioned objects (APIVersion != "") are accepted. The object
// will be converted into the in-memory unversioned type before being returned.
func Decode(data []byte) (interface{}, error) {
version, kind, err := VersionAndKind(data)
if err != nil {
return nil, err
}
if version == "" {
return nil, fmt.Errorf("API Version not set in '%s'", string(data))
}
obj, err := New(version, kind)
if err != nil {
return nil, fmt.Errorf("Unable to create new object of type ('%s', '%s')", version, kind)
}
// yaml is a superset of json, so we use it to decode here. That way,
// we understand both.
err = yaml.Unmarshal(data, obj)
if err != nil {
return nil, err
}
obj, err = internalize(obj)
if err != nil {
return nil, err
}
jsonBase, err := FindJSONBase(obj)
if err != nil {
return nil, err
}
// Don't leave these set. Type and version info is deducible from go's type.
jsonBase.SetKind("")
jsonBase.SetAPIVersion("")
return obj, nil
}
// DecodeInto parses a YAML or JSON string and stores it in obj. Returns an error
// if data.Kind is set and doesn't match the type of obj. Obj should be a
// pointer to an api type.
// If obj's APIVersion doesn't match that in data, an attempt will be made to convert
// data into obj's version.
func DecodeInto(data []byte, obj interface{}) error {
dataVersion, dataKind, err := VersionAndKind(data)
if err != nil {
return err
}
objVersion, objKind, err := objAPIVersionAndName(obj)
if err != nil {
return err
}
if dataKind == "" {
// Assume objects with unset Kind fields are being unmarshalled into the
// correct type.
dataKind = objKind
}
if dataKind != objKind {
return fmt.Errorf("data of kind '%v', obj of type '%v'", dataKind, objKind)
}
if dataVersion == "" {
// Assume objects with unset Version fields are being unmarshalled into the
// correct type.
dataVersion = objVersion
}
if objVersion == dataVersion {
// Easy case!
err = yaml.Unmarshal(data, obj)
if err != nil {
return err
}
} else {
// TODO: look up in our map to see if we can do this dataVersion -> objVersion
// conversion.
if objVersion != "" || dataVersion != "v1beta1" {
return fmt.Errorf("Can't convert from '%v' to '%v' for type '%v'", dataVersion, objVersion, dataKind)
}
external, err := New(dataVersion, dataKind)
if err != nil {
return fmt.Errorf("Unable to create new object of type ('%s', '%s')", dataVersion, dataKind)
}
// yaml is a superset of json, so we use it to decode here. That way,
// we understand both.
err = yaml.Unmarshal(data, external)
if err != nil {
return err
}
internal, err := internalize(external)
if err != nil {
return err
}
// Copy to the provided object.
vObj := reflect.ValueOf(obj)
vInternal := reflect.ValueOf(internal)
if !vInternal.Type().AssignableTo(vObj.Type()) {
return fmt.Errorf("%s is not assignable to %s", vInternal.Type(), vObj.Type())
}
vObj.Elem().Set(vInternal.Elem())
}
jsonBase, err := FindJSONBase(obj)
if err != nil {
return err
}
// Don't leave these set. Type and version info is deducible from go's type.
jsonBase.SetKind("")
jsonBase.SetAPIVersion("")
return nil
}
func internalize(obj interface{}) (interface{}, error) {
objVersion, objKind, err := objAPIVersionAndName(obj)
if err != nil {
return nil, err
}
if fn, ok := conversionFuncs[typeTuple{objVersion, "", objKind}]; ok {
return fn(obj)
}
return nil, fmt.Errorf("No conversion handler that knows how to convert a '%v' from '%v'",
objKind, objVersion)
}
func externalize(obj interface{}) (interface{}, error) {
objVersion, objKind, err := objAPIVersionAndName(obj)
if err != nil {
return nil, err
}
if fn, ok := conversionFuncs[typeTuple{objVersion, "v1beta1", objKind}]; ok {
return fn(obj)
}
return nil, fmt.Errorf("No conversion handler that knows how to convert a '%v' from '%v' to '%v'",
objKind, objVersion, "v1beta1")
}