Split api into api, api/common, api/validation & apitools

This commit is contained in:
Daniel Smith 2014-08-29 15:48:41 -07:00
parent 5bed06f614
commit 6121e61f99
18 changed files with 249 additions and 110 deletions

19
pkg/api/common/doc.go Normal file
View File

@ -0,0 +1,19 @@
/*
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 common provides types useful for all versions of any object
// that conforms to the kubernetes API object expectations.
package common

View File

@ -14,10 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package api
package common
import (
"gopkg.in/v1/yaml"
"github.com/GoogleCloudPlatform/kubernetes/pkg/apitools"
)
// Encode()/Decode() are the canonical way of converting an API object to/from
@ -26,14 +28,14 @@ import (
// embedded within other API types.
// UnmarshalJSON implements the json.Unmarshaler interface.
func (a *APIObject) UnmarshalJSON(b []byte) error {
func (a *Object) UnmarshalJSON(b []byte) error {
// Handle JSON's "null": Decode() doesn't expect it.
if len(b) == 4 && string(b) == "null" {
a.Object = nil
return nil
}
obj, err := Decode(b)
obj, err := apitools.Decode(b)
if err != nil {
return err
}
@ -42,17 +44,17 @@ func (a *APIObject) UnmarshalJSON(b []byte) error {
}
// MarshalJSON implements the json.Marshaler interface.
func (a APIObject) MarshalJSON() ([]byte, error) {
func (a Object) MarshalJSON() ([]byte, error) {
if a.Object == nil {
// Encode unset/nil objects as JSON's "null".
return []byte("null"), nil
}
return Encode(a.Object)
return apitools.Encode(a.Object)
}
// SetYAML implements the yaml.Setter interface.
func (a *APIObject) SetYAML(tag string, value interface{}) bool {
func (a *Object) SetYAML(tag string, value interface{}) bool {
if value == nil {
a.Object = nil
return true
@ -67,7 +69,7 @@ func (a *APIObject) SetYAML(tag string, value interface{}) bool {
if err != nil {
panic("yaml can't reverse its own object")
}
obj, err := Decode(b)
obj, err := apitools.Decode(b)
if err != nil {
return false
}
@ -76,13 +78,13 @@ func (a *APIObject) SetYAML(tag string, value interface{}) bool {
}
// GetYAML implements the yaml.Getter interface.
func (a APIObject) GetYAML() (tag string, value interface{}) {
func (a Object) GetYAML() (tag string, value interface{}) {
if a.Object == nil {
value = "null"
return
}
// Encode returns JSON, which is conveniently a subset of YAML.
v, err := Encode(a.Object)
v, err := apitools.Encode(a.Object)
if err != nil {
panic("impossible to encode API object!")
}

View File

@ -14,40 +14,42 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package api
package common
import (
"encoding/json"
"reflect"
"testing"
"github.com/GoogleCloudPlatform/kubernetes/pkg/apitools"
)
func TestAPIObject(t *testing.T) {
func TestObject(t *testing.T) {
type EmbeddedTest struct {
JSONBase `yaml:",inline" json:",inline"`
Object APIObject `yaml:"object,omitempty" json:"object,omitempty"`
EmptyObject APIObject `yaml:"emptyObject,omitempty" json:"emptyObject,omitempty"`
Object Object `yaml:"object,omitempty" json:"object,omitempty"`
EmptyObject Object `yaml:"emptyObject,omitempty" json:"emptyObject,omitempty"`
}
AddKnownTypes("", EmbeddedTest{})
AddKnownTypes("v1beta1", EmbeddedTest{})
apitools.AddKnownTypes("", EmbeddedTest{})
apitools.AddKnownTypes("v1beta1", EmbeddedTest{})
outer := &EmbeddedTest{
JSONBase: JSONBase{ID: "outer"},
Object: APIObject{
Object: Object{
&EmbeddedTest{
JSONBase: JSONBase{ID: "inner"},
},
},
}
wire, err := Encode(outer)
wire, err := apitools.Encode(outer)
if err != nil {
t.Fatalf("Unexpected encode error '%v'", err)
}
t.Logf("Wire format is:\n%v\n", string(wire))
decoded, err := Decode(wire)
decoded, err := apitools.Decode(wire)
if err != nil {
t.Fatalf("Unexpected decode error %v", err)
}
@ -56,7 +58,7 @@ func TestAPIObject(t *testing.T) {
t.Errorf("Expected: %#v but got %#v", e, a)
}
// test JSON decoding, too, since api.Decode uses yaml unmarshalling.
// test JSON decoding, too, since apitools.Decode uses yaml unmarshalling.
var decodedViaJSON EmbeddedTest
err = json.Unmarshal(wire, &decodedViaJSON)
if err != nil {

61
pkg/api/common/types.go Normal file
View File

@ -0,0 +1,61 @@
/*
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 common
import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
)
// JSONBase is shared by all top level objects. The proper way to use it is to inline it in your type,
// like this:
// type MyAwesomeAPIObject struct {
// common.JSONBase `yaml:",inline" json:",inline"`
// ... // other fields
// }
//
// JSONBase is provided here for convenience. You may use it directlly from this package or define
// your own with the same fields.
//
type JSONBase struct {
Kind string `json:"kind,omitempty" yaml:"kind,omitempty"`
ID string `json:"id,omitempty" yaml:"id,omitempty"`
CreationTimestamp util.Time `json:"creationTimestamp,omitempty" yaml:"creationTimestamp,omitempty"`
SelfLink string `json:"selfLink,omitempty" yaml:"selfLink,omitempty"`
ResourceVersion uint64 `json:"resourceVersion,omitempty" yaml:"resourceVersion,omitempty"`
APIVersion string `json:"apiVersion,omitempty" yaml:"apiVersion,omitempty"`
}
// Object has appropriate encoder and decoder functions, such that on the wire, it's
// stored as a []byte, but in memory, the contained object is accessable as an interface{}
// via the Get() function. Only objects having a JSONBase may be stored via Object.
// The purpose of this is to allow an API object of type known only at runtime to be
// embedded within other API objects.
//
// Note that object assumes that you've registered all of your api types with the api package.
//
// Note that objects will be serialized into the api package's default external versioned type;
// this should be fixed in the future to use the version of the current Codec instead.
type Object struct {
Object interface{}
}
// Extension allows api objects with unknown types to be passed-through. This can be used
// to deal with the API objects from a plug-in. Extension objects still have functioning
// JSONBase features-- kind, version, resourceVersion, etc.
// TODO: Not implemented yet
type Extension struct {
}

View File

@ -14,6 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
// Package api includes all types used to communicate between the various
// parts of the Kubernetes system.
// Package api contains the latest (or "internal") version of the
// Kubernetes API objects. This is the API objects as represented in memory.
// The contract presented to clients is located in the versioned packages,
// which are sub-directories. The first one is "v1beta1". Those packages
// describe how a particular version is serialized to storage/network.
package api

40
pkg/api/register.go Normal file
View File

@ -0,0 +1,40 @@
/*
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 (
"github.com/GoogleCloudPlatform/kubernetes/pkg/apitools"
)
func init() {
apitools.AddKnownTypes("",
PodList{},
Pod{},
ReplicationControllerList{},
ReplicationController{},
ServiceList{},
Service{},
MinionList{},
Minion{},
Status{},
ServerOpList{},
ServerOp{},
ContainerManifestList{},
Endpoints{},
Binding{},
)
}

View File

@ -17,6 +17,7 @@ limitations under the License.
package api
import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/common"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
"github.com/GoogleCloudPlatform/kubernetes/pkg/watch"
"github.com/fsouza/go-dockerclient"
@ -521,14 +522,5 @@ type WatchEvent struct {
// For added or modified objects, this is the new object; for deleted objects,
// it's the state of the object immediately prior to its deletion.
Object APIObject
}
// APIObject has appropriate encoder and decoder functions, such that on the wire, it's
// stored as a []byte, but in memory, the contained object is accessable as an interface{}
// via the Get() function. Only objects having a JSONBase may be stored via APIObject.
// The purpose of this is to allow an API object of type known only at runtime to be
// embedded within other API objects.
type APIObject struct {
Object interface{}
Object common.Object
}

View File

@ -19,15 +19,14 @@ package v1beta1
import (
// Alias this so it can be easily changed when we cut the next version.
newer "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
// Also import under original name for Convert and AddConversionFuncs.
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/apitools"
)
func init() {
// Shortcut for sub-conversions. TODO: This should possibly be refactored
// such that this convert function is passed to each conversion func.
Convert := api.Convert
api.AddConversionFuncs(
Convert := apitools.Convert
apitools.AddConversionFuncs(
// EnvVar's Key is deprecated in favor of Name.
func(in *newer.EnvVar, out *EnvVar) error {
out.Value = in.Value

View File

@ -22,9 +22,10 @@ import (
newer "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1"
"github.com/GoogleCloudPlatform/kubernetes/pkg/apitools"
)
var Convert = newer.Convert
var Convert = apitools.Convert
func TestEnvConversion(t *testing.T) {
nonCanonical := []v1beta1.EnvVar{

View File

@ -17,11 +17,11 @@ limitations under the License.
package v1beta1
import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/apitools"
)
func init() {
api.AddKnownTypes("v1beta1",
apitools.AddKnownTypes("v1beta1",
PodList{},
Pod{},
ReplicationControllerList{},

View File

@ -17,6 +17,7 @@ limitations under the License.
package v1beta1
import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/common"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
"github.com/GoogleCloudPlatform/kubernetes/pkg/watch"
"github.com/GoogleCloudPlatform/kubernetes/third_party/docker-api-structs"
@ -521,14 +522,5 @@ type WatchEvent struct {
// For added or modified objects, this is the new object; for deleted objects,
// it's the state of the object immediately prior to its deletion.
Object APIObject
}
// APIObject has appropriate encoder and decoder functions, such that on the wire, it's
// stored as a []byte, but in memory, the contained object is accessable as an interface{}
// via the Get() function. Only objects having a JSONBase may be stored via APIObject.
// The purpose of this is to allow an API object of type known only at runtime to be
// embedded within other API objects.
type APIObject struct {
Object interface{}
Object common.Object
}

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package api
package validation
import (
"strings"

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package api
package validation
import (
"strings"

42
pkg/apitools/doc.go Normal file
View File

@ -0,0 +1,42 @@
/*
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 apitools includes helper functions for working with API objects
// that follow the kubernetes API object conventions, which are:
//
// 0. Your API objects have a common metadata struct member, JSONBase.
// 1. Your code refers to an internal set of API objects.
// 2. In a separate package, you have an external set of API objects.
// 3. The external set is considered to be versioned, and no breaking
// changes are ever made to it (fields may be added but not changed
// or removed).
// 4. As your api evolves, you'll make an additional versioned package
// with every major change.
// 5. Versioned packages have conversion functions which convert to
// and from the internal version.
// 6. You'll continue to support older versions according to your
// deprecation policy, and you can easily provide a program/library
// to update old versions into new versions because of 5.
// 7. All of your serializations and deserializations are handled in a
// centralized place.
//
// Package apitools provides a conversion helper to make 5 easy, and the
// Encode/Decode/DecodeInto trio to accomplish 7. You can also register
// additional "codecs" which use a version of your choice. It's
// recommended that you register your types with apitools in your
// package's init function.
//
package apitools

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package api
package apitools
import (
"fmt"
@ -39,34 +39,14 @@ type resourceVersioner interface {
ResourceVersion(obj interface{}) (uint64, error)
}
var Codec codec
var ResourceVersioner resourceVersioner
var ResourceVersioner resourceVersioner = NewJSONBaseResourceVersioner()
var conversionScheme = conversion.NewScheme()
var Codec codec = conversionScheme
func init() {
conversionScheme.InternalVersion = ""
conversionScheme.ExternalVersion = "v1beta1"
conversionScheme.MetaInsertionFactory = metaInsertion{}
AddKnownTypes("",
PodList{},
Pod{},
ReplicationControllerList{},
ReplicationController{},
ServiceList{},
Service{},
MinionList{},
Minion{},
Status{},
ServerOpList{},
ServerOp{},
ContainerManifestList{},
Endpoints{},
Binding{},
)
Codec = conversionScheme
ResourceVersioner = NewJSONBaseResourceVersioner()
}
// AddKnownTypes registers the types of the arguments to the marshaller of the package api.
@ -127,23 +107,6 @@ func FindJSONBase(obj interface{}) (JSONBaseInterface, error) {
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 {
return conversionScheme.EncodeOrDie(obj)

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package api_test
package apitools_test
import (
"encoding/json"
@ -25,6 +25,7 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
_ "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1"
"github.com/GoogleCloudPlatform/kubernetes/pkg/apitools"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
"github.com/fsouza/go-dockerclient"
"github.com/google/gofuzz"
@ -107,20 +108,20 @@ func objDiff(a, b interface{}) string {
func runTest(t *testing.T, source interface{}) {
name := reflect.TypeOf(source).Elem().Name()
apiObjectFuzzer.Fuzz(source)
j, err := api.FindJSONBase(source)
j, err := apitools.FindJSONBase(source)
if err != nil {
t.Fatalf("Unexpected error %v for %#v", err, source)
}
j.SetKind("")
j.SetAPIVersion("")
data, err := api.Encode(source)
data, err := apitools.Encode(source)
if err != nil {
t.Errorf("%v: %v (%#v)", name, err, source)
return
}
obj2, err := api.Decode(data)
obj2, err := apitools.Decode(data)
if err != nil {
t.Errorf("%v: %v", name, err)
return
@ -131,7 +132,7 @@ func runTest(t *testing.T, source interface{}) {
}
}
obj3 := reflect.New(reflect.TypeOf(source).Elem()).Interface()
err = api.DecodeInto(data, obj3)
err = apitools.DecodeInto(data, obj3)
if err != nil {
t.Errorf("2: %v: %v", name, err)
return
@ -173,8 +174,8 @@ func TestEncode_NonPtr(t *testing.T) {
Labels: map[string]string{"name": "foo"},
}
obj := interface{}(pod)
data, err := api.Encode(obj)
obj2, err2 := api.Decode(data)
data, err := apitools.Encode(obj)
obj2, err2 := apitools.Decode(data)
if err != nil || err2 != nil {
t.Fatalf("Failure: '%v' '%v'", err, err2)
}
@ -191,8 +192,8 @@ func TestEncode_Ptr(t *testing.T) {
Labels: map[string]string{"name": "foo"},
}
obj := interface{}(pod)
data, err := api.Encode(obj)
obj2, err2 := api.Decode(data)
data, err := apitools.Encode(obj)
obj2, err2 := apitools.Decode(data)
if err != nil || err2 != nil {
t.Fatalf("Failure: '%v' '%v'", err, err2)
}
@ -206,11 +207,11 @@ func TestEncode_Ptr(t *testing.T) {
func TestBadJSONRejection(t *testing.T) {
badJSONMissingKind := []byte(`{ }`)
if _, err := api.Decode(badJSONMissingKind); err == nil {
if _, err := apitools.Decode(badJSONMissingKind); err == nil {
t.Errorf("Did not reject despite lack of kind field: %s", badJSONMissingKind)
}
badJSONUnknownType := []byte(`{"kind": "bar"}`)
if _, err1 := api.Decode(badJSONUnknownType); err1 == nil {
if _, err1 := apitools.Decode(badJSONUnknownType); err1 == nil {
t.Errorf("Did not reject despite use of unknown type: %s", badJSONUnknownType)
}
/*badJSONKindMismatch := []byte(`{"kind": "Pod"}`)

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package api
package apitools
import (
"fmt"
@ -30,11 +30,11 @@ func NewJSONBaseResourceVersioner() resourceVersioner {
type jsonBaseResourceVersioner struct{}
func (v jsonBaseResourceVersioner) ResourceVersion(obj interface{}) (uint64, error) {
json, err := FindJSONBaseRO(obj)
json, err := FindJSONBase(obj)
if err != nil {
return 0, err
}
return json.ResourceVersion, nil
return json.ResourceVersion(), nil
}
func (v jsonBaseResourceVersioner) SetResourceVersion(obj interface{}, version uint64) error {

View File

@ -14,14 +14,24 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package api
package apitools
import (
"reflect"
"testing"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
)
func TestGenericJSONBase(t *testing.T) {
type JSONBase struct {
Kind string `json:"kind,omitempty" yaml:"kind,omitempty"`
ID string `json:"id,omitempty" yaml:"id,omitempty"`
CreationTimestamp util.Time `json:"creationTimestamp,omitempty" yaml:"creationTimestamp,omitempty"`
SelfLink string `json:"selfLink,omitempty" yaml:"selfLink,omitempty"`
ResourceVersion uint64 `json:"resourceVersion,omitempty" yaml:"resourceVersion,omitempty"`
APIVersion string `json:"apiVersion,omitempty" yaml:"apiVersion,omitempty"`
}
j := JSONBase{
ID: "foo",
APIVersion: "a",
@ -68,13 +78,25 @@ func TestGenericJSONBase(t *testing.T) {
}
func TestResourceVersionerOfAPI(t *testing.T) {
testCases := map[string]struct {
type JSONBase struct {
Kind string `json:"kind,omitempty" yaml:"kind,omitempty"`
ID string `json:"id,omitempty" yaml:"id,omitempty"`
CreationTimestamp util.Time `json:"creationTimestamp,omitempty" yaml:"creationTimestamp,omitempty"`
SelfLink string `json:"selfLink,omitempty" yaml:"selfLink,omitempty"`
ResourceVersion uint64 `json:"resourceVersion,omitempty" yaml:"resourceVersion,omitempty"`
APIVersion string `json:"apiVersion,omitempty" yaml:"apiVersion,omitempty"`
}
type MyAPIObject struct {
JSONBase `yaml:",inline" json:",inline"`
}
type T struct {
Object interface{}
Expected uint64
}{
"empty api object": {Service{}, 0},
"api object with version": {Service{JSONBase: JSONBase{ResourceVersion: 1}}, 1},
"pointer to api object with version": {&Service{JSONBase: JSONBase{ResourceVersion: 1}}, 1},
}
testCases := map[string]T{
"empty api object": {&MyAPIObject{}, 0},
"api object with version": {&MyAPIObject{JSONBase: JSONBase{ResourceVersion: 1}}, 1},
"pointer to api object with version": {&MyAPIObject{JSONBase: JSONBase{ResourceVersion: 1}}, 1},
}
versioning := NewJSONBaseResourceVersioner()
for key, testCase := range testCases {