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.
This commit is contained in:
Daniel Smith 2014-07-25 17:59:41 -07:00
parent b3cc696486
commit 2396bdfa1b
9 changed files with 743 additions and 796 deletions

View File

@ -68,6 +68,7 @@ func TestAPIObject(t *testing.T) {
// Things that Decode would have done for us:
decodedViaJSON.Kind = ""
decodedViaJSON.APIVersion = ""
if e, a := outer, &decodedViaJSON; !reflect.DeepEqual(e, a) {
t.Errorf("Expected: %#v but got %#v", e, a)

133
pkg/api/defaultcopy.go Normal file
View File

@ -0,0 +1,133 @@
/*
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 (
"fmt"
"reflect"
)
// DefaultCopy copies API objects to/from their corresponding types
// in a versioned package (e.g., v1beta1). Only suitable for types
// in which no fields changed.
// dest and src must both be pointers to API objects.
// Not safe for objects with cyclic references!
// TODO: Allow overrides, using the same function mechanism that
// util.Fuzzer allows.
func DefaultCopy(src, dest interface{}) error {
dv, sv := reflect.ValueOf(dest), reflect.ValueOf(src)
if dv.Kind() != reflect.Ptr {
return fmt.Errorf("Need pointer, but got %#v", dest)
}
if sv.Kind() != reflect.Ptr {
return fmt.Errorf("Need pointer, but got %#v", src)
}
dv = dv.Elem()
sv = sv.Elem()
if !dv.CanAddr() {
return fmt.Errorf("Can't write to dest")
}
// Ensure there's no reversed src/dest bugs by making src unwriteable.
sv = reflect.ValueOf(sv.Interface())
if sv.CanAddr() {
return fmt.Errorf("Can write to src, shouldn't be able to.")
}
return copyValue(sv, dv)
}
// Recursively copy sv into dv
func copyValue(sv, dv reflect.Value) error {
dt, st := dv.Type(), sv.Type()
if dt.Name() != st.Name() {
return fmt.Errorf("Type names don't match: %v, %v", dt.Name(), st.Name())
}
// This should handle all simple types.
if st.AssignableTo(dt) {
dv.Set(sv)
return nil
} else if st.ConvertibleTo(dt) {
dv.Set(sv.Convert(dt))
return nil
}
// For debugging, should you need to do that.
if false {
fmt.Printf("copyVal of %v.%v (%v) -> %v.%v (%v)\n",
st.PkgPath(), st.Name(), st.Kind(),
dt.PkgPath(), dt.Name(), dt.Kind())
}
switch dv.Kind() {
case reflect.Struct:
for i := 0; i < dt.NumField(); i++ {
f := dv.Type().Field(i)
df := dv.FieldByName(f.Name)
sf := sv.FieldByName(f.Name)
if !df.IsValid() || !sf.IsValid() {
return fmt.Errorf("%v not present in source and dest.", f.Name)
}
if err := copyValue(sf, df); err != nil {
return err
}
}
case reflect.Slice:
if sv.IsNil() {
// Don't make a zero-length slice.
dv.Set(reflect.Zero(dt))
return nil
}
dv.Set(reflect.MakeSlice(dt, sv.Len(), sv.Cap()))
for i := 0; i < sv.Len(); i++ {
if err := copyValue(sv.Index(i), dv.Index(i)); err != nil {
return err
}
}
case reflect.Ptr:
if sv.IsNil() {
// Don't copy a nil ptr!
dv.Set(reflect.Zero(dt))
return nil
}
dv.Set(reflect.New(dt.Elem()))
return copyValue(sv.Elem(), dv.Elem())
case reflect.Map:
if sv.IsNil() {
// Don't copy a nil ptr!
dv.Set(reflect.Zero(dt))
return nil
}
dv.Set(reflect.MakeMap(dt))
for _, sk := range sv.MapKeys() {
dk := reflect.New(dt.Key()).Elem()
if err := copyValue(sk, dk); err != nil {
return err
}
dkv := reflect.New(dt.Elem()).Elem()
if err := copyValue(sv.MapIndex(sk), dkv); err != nil {
return err
}
dv.SetMapIndex(dk, dkv)
}
default:
return fmt.Errorf("Couldn't copy %#v (%v) into %#v (%v)",
sv.Interface(), sv.Kind(), dv.Interface(), dv.Kind())
}
return nil
}

File diff suppressed because it is too large Load Diff

View File

@ -17,12 +17,82 @@ limitations under the License.
package api
import (
"encoding/json"
"flag"
"fmt"
"reflect"
"testing"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
)
var fuzzIters = flag.Int("fuzz_iters", 3, "How many fuzzing iterations to do.")
// apiObjectFuzzer can randomly populate api objects.
var apiObjectFuzzer = util.NewFuzzer(
func(j *JSONBase) {
// We have to customize the randomization of JSONBases because their
// APIVersion and Kind must remain blank in memory.
j.APIVersion = ""
j.Kind = ""
j.ID = util.RandString()
// TODO: Fix JSON/YAML packages and/or write custom encoding
// for uint64's. Somehow the LS *byte* of this is lost, but
// only when all 8 bytes are set.
j.ResourceVersion = util.RandUint64() >> 8
j.SelfLink = util.RandString()
j.CreationTimestamp = util.RandString()
},
func(intstr *util.IntOrString) {
// util.IntOrString will panic if its kind is set wrong.
if util.RandBool() {
intstr.Kind = util.IntstrInt
intstr.IntVal = int(util.RandUint64())
intstr.StrVal = ""
} else {
intstr.Kind = util.IntstrString
intstr.IntVal = 0
intstr.StrVal = util.RandString()
}
},
func(p *PodInfo) {
// The docker container type doesn't survive fuzzing.
// TODO: fix this.
*p = nil
},
)
func objDiff(a, b interface{}) string {
// An alternate diff attempt, in case json isn't showing you
// the difference. (reflect.DeepEqual makes a distinction between
// nil and empty slices, for example.)
return util.StringDiff(
fmt.Sprintf("%#v", a),
fmt.Sprintf("%#v", b),
)
ab, err := json.Marshal(a)
if err != nil {
panic("a")
}
bb, err := json.Marshal(b)
if err != nil {
panic("b")
}
return util.StringDiff(string(ab), string(bb))
}
func runTest(t *testing.T, source interface{}) {
name := reflect.TypeOf(source).Name()
name := reflect.TypeOf(source).Elem().Name()
apiObjectFuzzer.Fuzz(source)
j, err := FindJSONBase(source)
if err != nil {
t.Fatalf("Unexpected error %v for %#v", err, source)
}
j.SetKind("")
j.SetAPIVersion("")
data, err := Encode(source)
if err != nil {
t.Errorf("%v: %v (%#v)", name, err, source)
@ -32,58 +102,51 @@ func runTest(t *testing.T, source interface{}) {
if err != nil {
t.Errorf("%v: %v", name, err)
return
}
if !reflect.DeepEqual(source, obj2) {
t.Errorf("1: %v: wanted %#v, got %#v", name, source, obj2)
return
} else {
if !reflect.DeepEqual(source, obj2) {
t.Errorf("1: %v: diff: %v", name, objDiff(source, obj2))
return
}
}
obj3 := reflect.New(reflect.TypeOf(source).Elem()).Interface()
err = DecodeInto(data, obj3)
if err != nil {
t.Errorf("2: %v: %v", name, err)
return
}
if !reflect.DeepEqual(source, obj3) {
t.Errorf("3: %v: wanted %#v, got %#v", name, source, obj3)
return
} else {
if !reflect.DeepEqual(source, obj3) {
t.Errorf("3: %v: diff: %v", name, objDiff(source, obj3))
return
}
}
}
func TestTypes(t *testing.T) {
// TODO: auto-fill all fields.
table := []interface{}{
&Pod{
JSONBase: JSONBase{
ID: "mylittlepod",
},
Labels: map[string]string{
"name": "pinky",
},
},
&PodList{},
&Pod{},
&ServiceList{},
&Service{},
&ServiceList{
Items: []Service{
{
Labels: map[string]string{
"foo": "bar",
},
}, {
Labels: map[string]string{
"foo": "baz",
},
},
},
},
&ReplicationControllerList{},
&ReplicationController{},
&PodList{},
&MinionList{},
&Minion{},
&Status{},
&ServerOpList{},
&ServerOp{},
&ContainerManifestList{},
&Endpoints{},
}
for _, item := range table {
runTest(t, item)
// Try a few times, since runTest uses random values.
for i := 0; i < *fuzzIters; i++ {
runTest(t, item)
}
}
}
func TestNonPtr(t *testing.T) {
func TestEncode_NonPtr(t *testing.T) {
pod := Pod{
Labels: map[string]string{"name": "foo"},
}
@ -91,30 +154,30 @@ func TestNonPtr(t *testing.T) {
data, err := Encode(obj)
obj2, err2 := Decode(data)
if err != nil || err2 != nil {
t.Errorf("Failure: %v %v", err2, err2)
t.Fatalf("Failure: '%v' '%v'", err, err2)
}
if _, ok := obj2.(*Pod); !ok {
t.Errorf("Got wrong type")
t.Fatalf("Got wrong type")
}
if !reflect.DeepEqual(obj2, &pod) {
t.Errorf("Expected:\n %#v,\n Got:\n %#v", &pod, obj2)
}
}
func TestPtr(t *testing.T) {
pod := Pod{
func TestEncode_Ptr(t *testing.T) {
pod := &Pod{
Labels: map[string]string{"name": "foo"},
}
obj := interface{}(&pod)
obj := interface{}(pod)
data, err := Encode(obj)
obj2, err2 := Decode(data)
if err != nil || err2 != nil {
t.Errorf("Failure: %v %v", err2, err2)
t.Fatalf("Failure: '%v' '%v'", err, err2)
}
if _, ok := obj2.(*Pod); !ok {
t.Errorf("Got wrong type")
t.Fatalf("Got wrong type")
}
if !reflect.DeepEqual(obj2, &pod) {
if !reflect.DeepEqual(obj2, pod) {
t.Errorf("Expected:\n %#v,\n Got:\n %#v", &pod, obj2)
}
}

108
pkg/api/jsonbase.go Normal file
View File

@ -0,0 +1,108 @@
/*
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 (
"fmt"
"reflect"
)
// JSONBase lets you work with a JSONBase from any of the versioned or
// internal APIObjects.
type JSONBaseInterface interface {
APIVersion() string
SetAPIVersion(version string)
Kind() string
SetKind(kind string)
ResourceVersion() uint64
SetResourceVersion(version uint64)
}
type genericJSONBase struct {
apiVersion *string
kind *string
resourceVersion *uint64
}
func (g genericJSONBase) APIVersion() string {
return *g.apiVersion
}
func (g genericJSONBase) SetAPIVersion(version string) {
*g.apiVersion = version
}
func (g genericJSONBase) Kind() string {
return *g.kind
}
func (g genericJSONBase) SetKind(kind string) {
*g.kind = kind
}
func (g genericJSONBase) ResourceVersion() uint64 {
return *g.resourceVersion
}
func (g genericJSONBase) SetResourceVersion(version uint64) {
*g.resourceVersion = version
}
// fieldPtr puts the address address of fieldName, which must be a member of v,
// into dest, which must be an address of a variable to which this field's address
// can be assigned.
func fieldPtr(v reflect.Value, fieldName string, dest interface{}) error {
field := v.FieldByName(fieldName)
if !field.IsValid() {
return fmt.Errorf("Couldn't find %v field in %#v", fieldName, v.Interface())
}
v = reflect.ValueOf(dest)
if v.Kind() != reflect.Ptr {
return fmt.Errorf("dest should be ptr")
}
v = v.Elem()
field = field.Addr()
if field.Type().AssignableTo(v.Type()) {
v.Set(field)
return nil
}
if field.Type().ConvertibleTo(v.Type()) {
v.Set(field.Convert(v.Type()))
return nil
}
return fmt.Errorf("Couldn't assign/convert %v to %v", field.Type(), v.Type())
}
// newGenericJSONBase makes a new generic JSONBase from v, which must be an
// addressable/setable reflect.Value having the same fields as api.JSONBase.
// Returns an error if this isn't the case.
func newGenericJSONBase(v reflect.Value) (genericJSONBase, error) {
g := genericJSONBase{}
err := fieldPtr(v, "APIVersion", &g.apiVersion)
if err != nil {
return g, err
}
err = fieldPtr(v, "Kind", &g.kind)
if err != nil {
return g, err
}
err = fieldPtr(v, "ResourceVersion", &g.resourceVersion)
if err != nil {
return g, err
}
return g, nil
}

59
pkg/api/jsonbase_test.go Normal file
View File

@ -0,0 +1,59 @@
/*
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 (
"reflect"
"testing"
)
func TestGenericJSONBase(t *testing.T) {
j := JSONBase{
APIVersion: "a",
Kind: "b",
ResourceVersion: 1,
}
g, err := newGenericJSONBase(reflect.ValueOf(&j).Elem())
if err != nil {
t.Fatalf("new err: %v", err)
}
// Proove g supports JSONBaseInterface.
jbi := JSONBaseInterface(g)
if e, a := "a", jbi.APIVersion(); e != a {
t.Errorf("expected %v, got %v", e, a)
}
if e, a := "b", jbi.Kind(); e != a {
t.Errorf("expected %v, got %v", e, a)
}
if e, a := uint64(1), jbi.ResourceVersion(); e != a {
t.Errorf("expected %v, got %v", e, a)
}
jbi.SetAPIVersion("c")
jbi.SetKind("d")
jbi.SetResourceVersion(2)
if e, a := "c", j.APIVersion; e != a {
t.Errorf("expected %v, got %v", e, a)
}
if e, a := "d", j.Kind; e != a {
t.Errorf("expected %v, got %v", e, a)
}
if e, a := uint64(2), j.ResourceVersion; e != a {
t.Errorf("expected %v, got %v", e, a)
}
}

View File

@ -38,7 +38,7 @@ import (
// by the following regex:
// [a-z0-9]([-a-z0-9]*[a-z0-9])?
//
// DNS_SUBDOMAIN: This is a string, no more than 253 characters long, that conforms
// DNS_SUBDOMAIN: This is a string, no more than 253 characters long, that conforms
// to the definition of a "subdomain" in RFCs 1035 and 1123. This is captured
// by the following regex:
// [a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*
@ -223,7 +223,8 @@ type PodState struct {
// entry per container in the manifest. The value of this map is currently the output
// of `docker inspect`. This output format is *not* final and should not be relied
// upon.
// TODO: Make real decisions about what our info should look like.
// TODO: Make real decisions about what our info should look like. Re-enable fuzz test
// when we have done this.
Info PodInfo `json:"info,omitempty" yaml:"info,omitempty"`
}

View File

@ -38,7 +38,7 @@ import (
// by the following regex:
// [a-z0-9]([-a-z0-9]*[a-z0-9])?
//
// DNS_SUBDOMAIN: This is a string, no more than 253 characters long, that conforms
// DNS_SUBDOMAIN: This is a string, no more than 253 characters long, that conforms
// to the definition of a "subdomain" in RFCs 1035 and 1123. This is captured
// by the following regex:
// [a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*
@ -58,6 +58,12 @@ type ContainerManifest struct {
Containers []Container `yaml:"containers" json:"containers"`
}
// ContainerManifestList is used to communicate container manifests to kubelet.
type ContainerManifestList struct {
JSONBase `json:",inline" yaml:",inline"`
Items []ContainerManifest `json:"items,omitempty" yaml:"items,omitempty"`
}
// Volume represents a named volume in a pod that may be accessed by any containers in the pod.
type Volume struct {
// Required: This must be a DNS_LABEL. Each volume in a pod must have
@ -289,8 +295,8 @@ type Service struct {
// Endpoints is a collection of endpoints that implement the actual service, for example:
// Name: "mysql", Endpoints: ["10.10.1.1:1909", "10.10.2.2:8834"]
type Endpoints struct {
Name string
Endpoints []string
JSONBase `json:",inline" yaml:",inline"`
Endpoints []string `json:"endpoints,omitempty" yaml:"endpoints,omitempty"`
}
// Minion is a worker node in Kubernetenes.

View File

@ -148,7 +148,7 @@ func (h *EtcdHelper) bodyAndExtractObj(key string, objPtr interface{}, ignoreNot
body = response.Node.Value
err = api.DecodeInto([]byte(body), objPtr)
if jsonBase, err := api.FindJSONBase(objPtr); err == nil {
jsonBase.ResourceVersion = response.Node.ModifiedIndex
jsonBase.SetResourceVersion(response.Node.ModifiedIndex)
// Note that err shadows the err returned below, so we won't
// return an error just because we failed to find a JSONBase.
// This is intentional.