diff --git a/pkg/api/apiobj_test.go b/pkg/api/apiobj_test.go index 74666655919..7f437938f12 100644 --- a/pkg/api/apiobj_test.go +++ b/pkg/api/apiobj_test.go @@ -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) diff --git a/pkg/api/defaultcopy.go b/pkg/api/defaultcopy.go new file mode 100644 index 00000000000..d802ac438a7 --- /dev/null +++ b/pkg/api/defaultcopy.go @@ -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 +} diff --git a/pkg/api/helper.go b/pkg/api/helper.go index 4f1ebc75ead..3ff6e6c0cb0 100644 --- a/pkg/api/helper.go +++ b/pkg/api/helper.go @@ -25,11 +25,33 @@ import ( "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) -var versionMap = map[string]map[string]reflect.Type{} -var externalFuncs = map[string]ConversionFunc{} -var internalFuncs = map[string]ConversionFunc{} +// 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("", @@ -44,6 +66,8 @@ func init() { Status{}, ServerOpList{}, ServerOp{}, + ContainerManifestList{}, + Endpoints{}, ) AddKnownTypes("v1beta1", v1beta1.PodList{}, @@ -57,7 +81,28 @@ func init() { 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. @@ -70,23 +115,87 @@ func AddKnownTypes(version string, types ...interface{}) { } 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 } } -func AddExternalConversion(name string, fn ConversionFunc) { - externalFuncs[name] = fn +// 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) } -func AddInternalConversion(name string, fn ConversionFunc) { - internalFuncs[name] = fn +// 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{}) (*JSONBase, error) { - _, jsonBase, err := nameAndJSONBase(obj) - return jsonBase, err +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. @@ -106,47 +215,107 @@ func FindJSONBaseRO(obj interface{}) (JSONBase, error) { 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 -// will be made so that the object's Kind field can be set. If a pointer, -// we change the Kind field, marshal, and then set the kind field back to -// "". Having to keep track of the kind field makes tests very annoying, -// so the rule is it's set only in wire format (json), not when in native -// format. +// 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 = checkPtr(obj) - base, err := prepareEncode(obj) + obj = maybeCopy(obj) + obj, err = maybeExternalize(obj) if err != nil { return nil, err } - if len(base.APIVersion) == 0 { - out, err := externalize(obj) - if err != nil { - return nil, err - } - _, jsonBase, err := nameAndJSONBase(obj) - if err != nil { - return nil, err - } - jsonBase.Kind = "" - obj = out - _, err = prepareEncode(out) - if err != nil { - return nil, err - } + + jsonBase, err := prepareEncode(obj) + if err != nil { + return nil, err } data, err = json.MarshalIndent(obj, "", " ") - _, jsonBase, err := nameAndJSONBase(obj) if err != nil { return nil, err } - jsonBase.Kind = "" + // Leave these blank in memory. + jsonBase.SetKind("") + jsonBase.SetAPIVersion("") return data, err } -func checkPtr(obj interface{}) interface{} { +// 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 @@ -156,772 +325,190 @@ func checkPtr(obj interface{}) interface{} { return v2.Interface() } -func prepareEncode(obj interface{}) (*JSONBase, error) { - name, jsonBase, err := nameAndJSONBase(obj) +// 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 } - knownTypes, found := versionMap[jsonBase.APIVersion] + 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, %v won't be unmarshalable because it's not in known versions", jsonBase.APIVersion, obj) + 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 won't be unmarshalable because it's not in knownTypes", name) + return nil, fmt.Errorf("struct %s, %s won't be unmarshalable because it's not in knownTypes", version, name) } - jsonBase.Kind = name + jsonBase.SetAPIVersion(version) + jsonBase.SetKind(name) return jsonBase, nil } -// Returns the name of the type (sans pointer), and its kind field. Takes pointer-to-struct.. -func nameAndJSONBase(obj interface{}) (string, *JSONBase, error) { +// 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 "", nil, fmt.Errorf("expected pointer, but got %v", v.Type().Name()) + return reflect.Value{}, fmt.Errorf("expected pointer, but got %v", v.Type().Name()) } - v = v.Elem() - name := v.Type().Name() - if v.Kind() != reflect.Struct { - return "", nil, fmt.Errorf("expected struct, but got %v: %v (%#v)", v.Kind(), v.Type().Name(), v.Interface()) - } - jsonBase := v.FieldByName("JSONBase") - if !jsonBase.IsValid() { - return "", nil, fmt.Errorf("struct %v lacks embedded JSON type", name) - } - output, ok := jsonBase.Addr().Interface().(*JSONBase) - if !ok { - internal, err := internalize(jsonBase.Addr().Interface()) - if err != nil { - return name, nil, err - } - output = internal.(*JSONBase) - } - return name, output, nil + return v.Elem(), nil } -// Decode converts a JSON string back into a pointer to an api object. Deduces the type -// based upon the Kind field (set by encode). -func Decode(data []byte) (interface{}, error) { +// 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) + // 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 nil, fmt.Errorf("couldn't get kind: %#v", err) + return "", "", fmt.Errorf("couldn't get version/kind: %v", err) } - knownTypes, found := versionMap[findKind.APIVersion] - if !found { - return nil, fmt.Errorf("Unknown api verson: %s", findKind.APIVersion) + 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 } - objType, found := knownTypes[findKind.Kind] - if !found { - return nil, fmt.Errorf("%#v is not a known type for decoding", findKind) + if version == "" { + return nil, fmt.Errorf("API Version not set in '%s'", string(data)) } - obj := reflect.New(objType).Interface() + 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 } - if len(findKind.APIVersion) != 0 { - obj, err = internalize(obj) - if err != nil { - return nil, err - } - } - _, jsonBase, err := nameAndJSONBase(obj) + obj, err = internalize(obj) if err != nil { return nil, err } - // Don't leave these set. Track type with go's type. - jsonBase.Kind = "" + 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 JSON string and stores it in obj. Returns an error +// 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 { - internal, err := Decode(data) + dataVersion, dataKind, err := VersionAndKind(data) if err != nil { return err } - v := reflect.ValueOf(obj) - iv := reflect.ValueOf(internal) - if !iv.Type().AssignableTo(v.Type()) { - return fmt.Errorf("%s is not assignable to %s", v.Type(), iv.Type()) - } - v.Elem().Set(iv.Elem()) - name, jsonBase, err := nameAndJSONBase(obj) + objVersion, objKind, err := objAPIVersionAndName(obj) if err != nil { return err } - if jsonBase.Kind != "" && jsonBase.Kind != name { - return fmt.Errorf("data had kind %v, but passed object was of type %v", jsonBase.Kind, name) + if dataKind == "" { + // Assume objects with unset Kind fields are being unmarshalled into the + // correct type. + dataKind = objKind } - // Don't leave these set. Track type with go's type. - jsonBase.Kind = "" + 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 } -// TODO: Switch to registered functions for each type. func internalize(obj interface{}) (interface{}, error) { - v := reflect.ValueOf(obj) - if v.Kind() != reflect.Ptr { - value := reflect.New(v.Type()) - value.Elem().Set(v) - result, err := internalize(value.Interface()) - if err != nil { - return nil, err - } - return reflect.ValueOf(result).Elem().Interface(), nil + objVersion, objKind, err := objAPIVersionAndName(obj) + if err != nil { + return nil, err } - switch cObj := obj.(type) { - case *v1beta1.JSONBase: - obj := JSONBase(*cObj) - return &obj, nil - case *v1beta1.PodList: - var items []Pod - if cObj.Items != nil { - items = make([]Pod, len(cObj.Items)) - for ix := range cObj.Items { - iObj, err := internalize(cObj.Items[ix]) - if err != nil { - return nil, err - } - items[ix] = iObj.(Pod) - } - } - result := PodList{ - JSONBase: JSONBase(cObj.JSONBase), - Items: items, - } - result.APIVersion = "" - return &result, nil - case *v1beta1.Pod: - current, err := internalize(cObj.CurrentState) - if err != nil { - return nil, err - } - desired, err := internalize(cObj.DesiredState) - if err != nil { - return nil, err - } - result := Pod{ - JSONBase: JSONBase(cObj.JSONBase), - Labels: cObj.Labels, - CurrentState: current.(PodState), - DesiredState: desired.(PodState), - } - result.APIVersion = "" - return &result, nil - case *v1beta1.PodState: - manifest, err := internalize(cObj.Manifest) - if err != nil { - return nil, err - } - result := PodState{ - Manifest: manifest.(ContainerManifest), - Status: PodStatus(cObj.Status), - Host: cObj.Host, - HostIP: cObj.HostIP, - PodIP: cObj.PodIP, - Info: PodInfo(cObj.Info), - } - return &result, nil - case *v1beta1.ContainerManifest: - var volumes []Volume - if cObj.Volumes != nil { - volumes = make([]Volume, len(cObj.Volumes)) - for ix := range cObj.Volumes { - v, err := internalize(cObj.Volumes[ix]) - if err != nil { - return nil, err - } - volumes[ix] = *(v.(*Volume)) - } - } - var containers []Container - if cObj.Containers != nil { - containers = make([]Container, len(cObj.Containers)) - for ix := range cObj.Containers { - v, err := internalize(cObj.Containers[ix]) - if err != nil { - return nil, err - } - containers[ix] = v.(Container) - } - } - result := ContainerManifest{ - Version: cObj.Version, - ID: cObj.ID, - Volumes: volumes, - Containers: containers, - } - return &result, nil - case *v1beta1.Volume: - var src *VolumeSource - if cObj.Source != nil { - obj, err := internalize(cObj.Source) - if err != nil { - return nil, err - } - src = obj.(*VolumeSource) - } - result := &Volume{ - Name: cObj.Name, - Source: src, - } - return &result, nil - case *v1beta1.VolumeSource: - var hostDir *HostDirectory - if cObj.HostDirectory != nil { - hostDir = &HostDirectory{ - Path: cObj.HostDirectory.Path, - } - } - var emptyDir *EmptyDirectory - if cObj.EmptyDirectory != nil { - emptyDir = &EmptyDirectory{} - } - result := VolumeSource{ - HostDirectory: hostDir, - EmptyDirectory: emptyDir, - } - return &result, nil - case *v1beta1.Container: - ports := make([]Port, len(cObj.Ports)) - for ix := range cObj.Ports { - p, err := internalize(cObj.Ports[ix]) - if err != nil { - return nil, err - } - ports[ix] = (p.(Port)) - } - env := make([]EnvVar, len(cObj.Env)) - for ix := range cObj.Env { - e, err := internalize(cObj.Env[ix]) - if err != nil { - return nil, err - } - env[ix] = e.(EnvVar) - } - mounts := make([]VolumeMount, len(cObj.VolumeMounts)) - for ix := range cObj.VolumeMounts { - v, err := internalize(cObj.VolumeMounts[ix]) - if err != nil { - return nil, err - } - mounts[ix] = v.(VolumeMount) - } - var liveness *LivenessProbe - if cObj.LivenessProbe != nil { - probe, err := internalize(*cObj.LivenessProbe) - if err != nil { - return nil, err - } - live := probe.(LivenessProbe) - liveness = &live - } - result := Container{ - Name: cObj.Name, - Image: cObj.Image, - Command: cObj.Command, - WorkingDir: cObj.WorkingDir, - Ports: ports, - Env: env, - Memory: cObj.Memory, - CPU: cObj.CPU, - VolumeMounts: mounts, - LivenessProbe: liveness, - } - return &result, nil - case *v1beta1.Port: - result := Port(*cObj) - return &result, nil - case *v1beta1.EnvVar: - result := EnvVar(*cObj) - return &result, nil - case *v1beta1.VolumeMount: - result := VolumeMount(*cObj) - return &result, nil - case *v1beta1.LivenessProbe: - var http *HTTPGetProbe - if cObj.HTTPGet != nil { - httpProbe := HTTPGetProbe(*cObj.HTTPGet) - http = &httpProbe - } - result := LivenessProbe{ - Type: cObj.Type, - HTTPGet: http, - InitialDelaySeconds: cObj.InitialDelaySeconds, - } - return &result, nil - case *v1beta1.ReplicationControllerList: - var items []ReplicationController - if cObj.Items != nil { - items = make([]ReplicationController, len(cObj.Items)) - for ix := range cObj.Items { - rc, err := internalize(cObj.Items[ix]) - if err != nil { - return nil, err - } - items[ix] = rc.(ReplicationController) - } - } - result := ReplicationControllerList{ - JSONBase: JSONBase(cObj.JSONBase), - Items: items, - } - result.APIVersion = "" - return &result, nil - case *v1beta1.ReplicationController: - desired, err := internalize(cObj.DesiredState) - if err != nil { - return nil, err - } - result := ReplicationController{ - JSONBase: JSONBase(cObj.JSONBase), - DesiredState: desired.(ReplicationControllerState), - Labels: cObj.Labels, - } - result.APIVersion = "" - return &result, nil - case *v1beta1.ReplicationControllerState: - template, err := internalize(cObj.PodTemplate) - if err != nil { - return nil, err - } - result := ReplicationControllerState{ - Replicas: cObj.Replicas, - ReplicaSelector: cObj.ReplicaSelector, - PodTemplate: template.(PodTemplate), - } - return &result, nil - case *v1beta1.PodTemplate: - desired, err := internalize(cObj.DesiredState) - if err != nil { - return nil, err - } - return &PodTemplate{ - DesiredState: desired.(PodState), - Labels: cObj.Labels, - }, nil - case *v1beta1.ServiceList: - var services []Service - if cObj.Items != nil { - services = make([]Service, len(cObj.Items)) - for ix := range cObj.Items { - s, err := internalize(cObj.Items[ix]) - if err != nil { - return nil, err - } - services[ix] = s.(Service) - services[ix].APIVersion = "" - } - } - result := ServiceList{ - JSONBase: JSONBase(cObj.JSONBase), - Items: services, - } - result.APIVersion = "" - return &result, nil - case *v1beta1.Service: - result := Service{ - JSONBase: JSONBase(cObj.JSONBase), - Port: cObj.Port, - Labels: cObj.Labels, - Selector: cObj.Selector, - CreateExternalLoadBalancer: cObj.CreateExternalLoadBalancer, - ContainerPort: cObj.ContainerPort, - } - result.APIVersion = "" - return &result, nil - case *v1beta1.MinionList: - minions := make([]Minion, len(cObj.Items)) - for ix := range cObj.Items { - m, err := internalize(cObj.Items[ix]) - if err != nil { - return nil, err - } - minions[ix] = m.(Minion) - } - result := MinionList{ - JSONBase: JSONBase(cObj.JSONBase), - Items: minions, - } - result.APIVersion = "" - return &result, nil - case *v1beta1.Minion: - result := Minion{ - JSONBase: JSONBase(cObj.JSONBase), - HostIP: cObj.HostIP, - } - result.APIVersion = "" - return &result, nil - case *v1beta1.Status: - result := Status{ - JSONBase: JSONBase(cObj.JSONBase), - Status: cObj.Status, - Details: cObj.Details, - Code: cObj.Code, - } - result.APIVersion = "" - return &result, nil - case *v1beta1.ServerOpList: - ops := make([]ServerOp, len(cObj.Items)) - for ix := range cObj.Items { - o, err := internalize(cObj.Items[ix]) - if err != nil { - return nil, err - } - ops[ix] = o.(ServerOp) - } - result := ServerOpList{ - JSONBase: JSONBase(cObj.JSONBase), - Items: ops, - } - result.APIVersion = "" - return &result, nil - case *v1beta1.ServerOp: - result := ServerOp{ - JSONBase: JSONBase(cObj.JSONBase), - } - result.APIVersion = "" - return &result, nil - default: - fn, ok := internalFuncs[reflect.ValueOf(cObj).Elem().Type().Name()] - if !ok { - fmt.Printf("unknown object to internalize: %s", reflect.ValueOf(cObj).Type().Name()) - panic(fmt.Sprintf("unknown object to internalize: %s", reflect.ValueOf(cObj).Type().Name())) - } - return fn(cObj) + if fn, ok := conversionFuncs[typeTuple{objVersion, "", objKind}]; ok { + return fn(obj) } - return obj, nil + return nil, fmt.Errorf("No conversion handler that knows how to convert a '%v' from '%v'", + objKind, objVersion) } -// TODO: switch to registered functions for each type. func externalize(obj interface{}) (interface{}, error) { - v := reflect.ValueOf(obj) - if v.Kind() != reflect.Ptr { - value := reflect.New(v.Type()) - value.Elem().Set(v) - result, err := externalize(value.Interface()) - if err != nil { - return nil, err - } - return reflect.ValueOf(result).Elem().Interface(), nil + objVersion, objKind, err := objAPIVersionAndName(obj) + if err != nil { + return nil, err } - switch cObj := obj.(type) { - case *PodList: - var items []v1beta1.Pod - if cObj.Items != nil { - items = make([]v1beta1.Pod, len(cObj.Items)) - for ix := range cObj.Items { - iObj, err := externalize(cObj.Items[ix]) - if err != nil { - return nil, err - } - items[ix] = iObj.(v1beta1.Pod) - } - } - result := v1beta1.PodList{ - JSONBase: v1beta1.JSONBase(cObj.JSONBase), - Items: items, - } - result.APIVersion = "v1beta1" - return &result, nil - case *Pod: - current, err := externalize(cObj.CurrentState) - if err != nil { - return nil, err - } - desired, err := externalize(cObj.DesiredState) - if err != nil { - return nil, err - } - result := v1beta1.Pod{ - JSONBase: v1beta1.JSONBase(cObj.JSONBase), - Labels: cObj.Labels, - CurrentState: current.(v1beta1.PodState), - DesiredState: desired.(v1beta1.PodState), - } - result.APIVersion = "v1beta1" - return &result, nil - case *PodState: - manifest, err := externalize(cObj.Manifest) - if err != nil { - return nil, err - } - result := v1beta1.PodState{ - Manifest: manifest.(v1beta1.ContainerManifest), - Status: v1beta1.PodStatus(cObj.Status), - Host: cObj.Host, - HostIP: cObj.HostIP, - PodIP: cObj.PodIP, - Info: v1beta1.PodInfo(cObj.Info), - } - return &result, nil - case *ContainerManifest: - var volumes []v1beta1.Volume - if cObj.Volumes != nil { - volumes = make([]v1beta1.Volume, len(cObj.Volumes)) - for ix := range cObj.Volumes { - v, err := externalize(cObj.Volumes[ix]) - if err != nil { - return nil, err - } - volumes[ix] = *(v.(*v1beta1.Volume)) - } - } - var containers []v1beta1.Container - if cObj.Containers != nil { - containers = make([]v1beta1.Container, len(cObj.Containers)) - for ix := range cObj.Containers { - v, err := externalize(cObj.Containers[ix]) - if err != nil { - return nil, err - } - containers[ix] = v.(v1beta1.Container) - } - } - result := v1beta1.ContainerManifest{ - Version: cObj.Version, - ID: cObj.ID, - Volumes: volumes, - Containers: containers, - } - return &result, nil - case *Volume: - var src *v1beta1.VolumeSource - if cObj.Source != nil { - obj, err := externalize(cObj.Source) - if err != nil { - return nil, err - } - src = obj.(*v1beta1.VolumeSource) - } - result := &v1beta1.Volume{ - Name: cObj.Name, - Source: src, - } - return &result, nil - case *VolumeSource: - var hostDir *v1beta1.HostDirectory - if cObj.HostDirectory != nil { - hostDir = &v1beta1.HostDirectory{ - Path: cObj.HostDirectory.Path, - } - } - var emptyDir *v1beta1.EmptyDirectory - if cObj.EmptyDirectory != nil { - emptyDir = &v1beta1.EmptyDirectory{} - } - result := v1beta1.VolumeSource{ - HostDirectory: hostDir, - EmptyDirectory: emptyDir, - } - return &result, nil - case *Container: - ports := make([]v1beta1.Port, len(cObj.Ports)) - for ix := range cObj.Ports { - p, err := externalize(cObj.Ports[ix]) - if err != nil { - return nil, err - } - ports[ix] = p.(v1beta1.Port) - } - env := make([]v1beta1.EnvVar, len(cObj.Env)) - for ix := range cObj.Env { - e, err := externalize(cObj.Env[ix]) - if err != nil { - return nil, err - } - env[ix] = e.(v1beta1.EnvVar) - } - mounts := make([]v1beta1.VolumeMount, len(cObj.VolumeMounts)) - for ix := range cObj.VolumeMounts { - v, err := externalize(cObj.VolumeMounts[ix]) - if err != nil { - return nil, err - } - mounts[ix] = v.(v1beta1.VolumeMount) - } - var liveness *v1beta1.LivenessProbe - if cObj.LivenessProbe != nil { - probe, err := externalize(*cObj.LivenessProbe) - if err != nil { - return nil, err - } - live := probe.(v1beta1.LivenessProbe) - liveness = &live - } - result := v1beta1.Container{ - Name: cObj.Name, - Image: cObj.Image, - Command: cObj.Command, - WorkingDir: cObj.WorkingDir, - Ports: ports, - Env: env, - Memory: cObj.Memory, - CPU: cObj.CPU, - VolumeMounts: mounts, - LivenessProbe: liveness, - } - return &result, nil - case *Port: - result := v1beta1.Port(*cObj) - return &result, nil - case *EnvVar: - result := v1beta1.EnvVar(*cObj) - return &result, nil - case *VolumeMount: - result := v1beta1.VolumeMount(*cObj) - return &result, nil - case *LivenessProbe: - var http *v1beta1.HTTPGetProbe - if cObj.HTTPGet != nil { - httpProbe := v1beta1.HTTPGetProbe(*cObj.HTTPGet) - http = &httpProbe - } - result := v1beta1.LivenessProbe{ - Type: cObj.Type, - HTTPGet: http, - InitialDelaySeconds: cObj.InitialDelaySeconds, - } - return &result, nil - case *ReplicationControllerList: - items := make([]v1beta1.ReplicationController, len(cObj.Items)) - for ix := range cObj.Items { - rc, err := externalize(cObj.Items[ix]) - if err != nil { - return nil, err - } - items[ix] = rc.(v1beta1.ReplicationController) - } - result := v1beta1.ReplicationControllerList{ - JSONBase: v1beta1.JSONBase(cObj.JSONBase), - Items: items, - } - result.APIVersion = "v1beta1" - return &result, nil - case *ReplicationController: - desired, err := externalize(cObj.DesiredState) - if err != nil { - return nil, err - } - result := v1beta1.ReplicationController{ - JSONBase: v1beta1.JSONBase(cObj.JSONBase), - DesiredState: desired.(v1beta1.ReplicationControllerState), - Labels: cObj.Labels, - } - result.APIVersion = "v1beta1" - return &result, nil - case *ReplicationControllerState: - template, err := externalize(cObj.PodTemplate) - if err != nil { - return nil, err - } - result := v1beta1.ReplicationControllerState{ - Replicas: cObj.Replicas, - ReplicaSelector: cObj.ReplicaSelector, - PodTemplate: template.(v1beta1.PodTemplate), - } - return &result, nil - case *PodTemplate: - desired, err := externalize(cObj.DesiredState) - if err != nil { - return nil, err - } - return &v1beta1.PodTemplate{ - DesiredState: desired.(v1beta1.PodState), - Labels: cObj.Labels, - }, nil - case *ServiceList: - services := make([]v1beta1.Service, len(cObj.Items)) - for ix := range cObj.Items { - s, err := externalize(cObj.Items[ix]) - if err != nil { - return nil, err - } - services[ix] = s.(v1beta1.Service) - } - result := v1beta1.ServiceList{ - JSONBase: v1beta1.JSONBase(cObj.JSONBase), - Items: services, - } - result.APIVersion = "v1beta1" - return &result, nil - case *Service: - result := v1beta1.Service{ - JSONBase: v1beta1.JSONBase(cObj.JSONBase), - Port: cObj.Port, - Labels: cObj.Labels, - Selector: cObj.Selector, - CreateExternalLoadBalancer: cObj.CreateExternalLoadBalancer, - ContainerPort: cObj.ContainerPort, - } - result.APIVersion = "v1beta1" - return &result, nil - case *MinionList: - minions := make([]v1beta1.Minion, len(cObj.Items)) - for ix := range cObj.Items { - m, err := externalize(cObj.Items[ix]) - if err != nil { - return nil, err - } - minions[ix] = m.(v1beta1.Minion) - } - result := v1beta1.MinionList{ - JSONBase: v1beta1.JSONBase(cObj.JSONBase), - Items: minions, - } - result.APIVersion = "v1beta1" - return &result, nil - case *Minion: - result := v1beta1.Minion{ - JSONBase: v1beta1.JSONBase(cObj.JSONBase), - HostIP: cObj.HostIP, - } - result.APIVersion = "v1beta1" - return &result, nil - case *Status: - result := v1beta1.Status{ - JSONBase: v1beta1.JSONBase(cObj.JSONBase), - Status: cObj.Status, - Details: cObj.Details, - Code: cObj.Code, - } - result.APIVersion = "v1beta1" - return &result, nil - case *ServerOpList: - ops := make([]v1beta1.ServerOp, len(cObj.Items)) - for ix := range cObj.Items { - o, err := externalize(cObj.Items[ix]) - if err != nil { - return nil, err - } - ops[ix] = o.(v1beta1.ServerOp) - } - result := v1beta1.ServerOpList{ - JSONBase: v1beta1.JSONBase(cObj.JSONBase), - Items: ops, - } - result.APIVersion = "v1beta1" - return &result, nil - case *ServerOp: - result := v1beta1.ServerOp{ - JSONBase: v1beta1.JSONBase(cObj.JSONBase), - } - result.APIVersion = "v1beta1" - return &result, nil - default: - fn, ok := externalFuncs[reflect.ValueOf(cObj).Elem().Type().Name()] - if !ok { - panic(fmt.Sprintf("Unknown object to externalize: %#v %s", cObj, reflect.ValueOf(cObj).Type().Name())) - } - return fn(cObj) + if fn, ok := conversionFuncs[typeTuple{objVersion, "v1beta1", objKind}]; ok { + return fn(obj) } - panic(fmt.Sprintf("This should never happen %#v", obj)) - return obj, nil + return nil, fmt.Errorf("No conversion handler that knows how to convert a '%v' from '%v' to '%v'", + objKind, objVersion, "v1beta1") } diff --git a/pkg/api/helper_test.go b/pkg/api/helper_test.go index 324cff0cff4..defa5b6bede 100644 --- a/pkg/api/helper_test.go +++ b/pkg/api/helper_test.go @@ -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) } } diff --git a/pkg/api/jsonbase.go b/pkg/api/jsonbase.go new file mode 100644 index 00000000000..884615b7731 --- /dev/null +++ b/pkg/api/jsonbase.go @@ -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 +} diff --git a/pkg/api/jsonbase_test.go b/pkg/api/jsonbase_test.go new file mode 100644 index 00000000000..66259889d8a --- /dev/null +++ b/pkg/api/jsonbase_test.go @@ -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) + } +} diff --git a/pkg/api/types.go b/pkg/api/types.go index 485254b1d9a..e654ccb29f6 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -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 @@ -217,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"` } @@ -289,8 +296,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. diff --git a/pkg/api/v1beta1/types.go b/pkg/api/v1beta1/types.go index e08e008c66b..3b9b73bfe90 100644 --- a/pkg/api/v1beta1/types.go +++ b/pkg/api/v1beta1/types.go @@ -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. diff --git a/pkg/kubelet/config/etcd.go b/pkg/kubelet/config/etcd.go index 3ab7a0cd084..ef3210f5936 100644 --- a/pkg/kubelet/config/etcd.go +++ b/pkg/kubelet/config/etcd.go @@ -9,7 +9,7 @@ You may obtain a copy of the License at 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 sied. +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. */ @@ -110,12 +110,12 @@ func responseToPods(response *etcd.Response) ([]kubelet.Pod, error) { return pods, fmt.Errorf("no nodes field: %v", response) } - manifests := []api.ContainerManifest{} + manifests := api.ContainerManifestList{} if err := yaml.Unmarshal([]byte(response.Node.Value), &manifests); err != nil { return pods, fmt.Errorf("could not unmarshal manifests: %v", err) } - for i, manifest := range manifests { + for i, manifest := range manifests.Items { name := manifest.ID if name == "" { name = fmt.Sprintf("_%d", i+1) diff --git a/pkg/kubelet/config/etcd_test.go b/pkg/kubelet/config/etcd_test.go index dbf7039a2b6..0a302f0978d 100644 --- a/pkg/kubelet/config/etcd_test.go +++ b/pkg/kubelet/config/etcd_test.go @@ -24,7 +24,6 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet" "github.com/GoogleCloudPlatform/kubernetes/pkg/tools" - "github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/coreos/go-etcd/etcd" ) @@ -36,7 +35,9 @@ func TestGetEtcdData(t *testing.T) { fakeClient.Data["/registry/hosts/machine/kubelet"] = tools.EtcdResponseWithError{ R: &etcd.Response{ Node: &etcd.Node{ - Value: util.MakeJSONString([]api.ContainerManifest{api.ContainerManifest{ID: "foo"}}), + Value: api.EncodeOrDie(&api.ContainerManifestList{ + Items: []api.ContainerManifest{{ID: "foo"}}, + }), ModifiedIndex: 1, }, }, @@ -76,7 +77,9 @@ func TestGetEtcd(t *testing.T) { fakeClient.Data["/registry/hosts/machine/kubelet"] = tools.EtcdResponseWithError{ R: &etcd.Response{ Node: &etcd.Node{ - Value: util.MakeJSONString([]api.ContainerManifest{api.ContainerManifest{ID: "foo"}}), + Value: api.EncodeOrDie(&api.ContainerManifestList{ + Items: []api.ContainerManifest{{ID: "foo"}}, + }), ModifiedIndex: 1, }, }, @@ -103,7 +106,7 @@ func TestWatchEtcd(t *testing.T) { fakeClient.Data["/registry/hosts/machine/kubelet"] = tools.EtcdResponseWithError{ R: &etcd.Response{ Node: &etcd.Node{ - Value: util.MakeJSONString([]api.Container{}), + Value: api.EncodeOrDie(&api.ContainerManifestList{}), ModifiedIndex: 2, }, }, diff --git a/pkg/proxy/config/config.go b/pkg/proxy/config/config.go index 5fab579b6cf..c106a9a91fb 100644 --- a/pkg/proxy/config/config.go +++ b/pkg/proxy/config/config.go @@ -127,19 +127,19 @@ func (s *endpointsStore) Merge(source string, change interface{}) error { case ADD: glog.Infof("Adding new endpoint from source %s : %v", source, update.Endpoints) for _, value := range update.Endpoints { - endpoints[value.Name] = value + endpoints[value.ID] = value } case REMOVE: glog.Infof("Removing an endpoint %v", update) for _, value := range update.Endpoints { - delete(endpoints, value.Name) + delete(endpoints, value.ID) } case SET: glog.Infof("Setting endpoints %v", update) // Clear the old map entries by just creating a new map endpoints = make(map[string]api.Endpoints) for _, value := range update.Endpoints { - endpoints[value.Name] = value + endpoints[value.ID] = value } default: glog.Infof("Received invalid update type: %v", update) diff --git a/pkg/proxy/config/config_test.go b/pkg/proxy/config/config_test.go index 755e02c19f8..38152457739 100644 --- a/pkg/proxy/config/config_test.go +++ b/pkg/proxy/config/config_test.go @@ -83,7 +83,7 @@ func (s sortedEndpoints) Swap(i, j int) { s[i], s[j] = s[j], s[i] } func (s sortedEndpoints) Less(i, j int) bool { - return s[i].Name < s[j].Name + return s[i].ID < s[j].ID } type EndpointsHandlerMock struct { @@ -216,8 +216,14 @@ func TestNewMultipleSourcesEndpointsMultipleHandlersAddedAndNotified(t *testing. handler2 := NewEndpointsHandlerMock() config.RegisterHandler(handler) config.RegisterHandler(handler2) - endpointsUpdate1 := CreateEndpointsUpdate(ADD, api.Endpoints{Name: "foo", Endpoints: []string{"endpoint1", "endpoint2"}}) - endpointsUpdate2 := CreateEndpointsUpdate(ADD, api.Endpoints{Name: "bar", Endpoints: []string{"endpoint3", "endpoint4"}}) + endpointsUpdate1 := CreateEndpointsUpdate(ADD, api.Endpoints{ + JSONBase: api.JSONBase{ID: "foo"}, + Endpoints: []string{"endpoint1", "endpoint2"}, + }) + endpointsUpdate2 := CreateEndpointsUpdate(ADD, api.Endpoints{ + JSONBase: api.JSONBase{ID: "bar"}, + Endpoints: []string{"endpoint3", "endpoint4"}, + }) handler.Wait(2) handler2.Wait(2) channelOne <- endpointsUpdate1 @@ -236,8 +242,14 @@ func TestNewMultipleSourcesEndpointsMultipleHandlersAddRemoveSetAndNotified(t *t handler2 := NewEndpointsHandlerMock() config.RegisterHandler(handler) config.RegisterHandler(handler2) - endpointsUpdate1 := CreateEndpointsUpdate(ADD, api.Endpoints{Name: "foo", Endpoints: []string{"endpoint1", "endpoint2"}}) - endpointsUpdate2 := CreateEndpointsUpdate(ADD, api.Endpoints{Name: "bar", Endpoints: []string{"endpoint3", "endpoint4"}}) + endpointsUpdate1 := CreateEndpointsUpdate(ADD, api.Endpoints{ + JSONBase: api.JSONBase{ID: "foo"}, + Endpoints: []string{"endpoint1", "endpoint2"}, + }) + endpointsUpdate2 := CreateEndpointsUpdate(ADD, api.Endpoints{ + JSONBase: api.JSONBase{ID: "bar"}, + Endpoints: []string{"endpoint3", "endpoint4"}, + }) handler.Wait(2) handler2.Wait(2) channelOne <- endpointsUpdate1 @@ -248,7 +260,10 @@ func TestNewMultipleSourcesEndpointsMultipleHandlersAddRemoveSetAndNotified(t *t handler2.ValidateEndpoints(t, endpoints) // Add one more - endpointsUpdate3 := CreateEndpointsUpdate(ADD, api.Endpoints{Name: "foobar", Endpoints: []string{"endpoint5", "endpoint6"}}) + endpointsUpdate3 := CreateEndpointsUpdate(ADD, api.Endpoints{ + JSONBase: api.JSONBase{ID: "foobar"}, + Endpoints: []string{"endpoint5", "endpoint6"}, + }) handler.Wait(1) handler2.Wait(1) channelTwo <- endpointsUpdate3 @@ -257,7 +272,10 @@ func TestNewMultipleSourcesEndpointsMultipleHandlersAddRemoveSetAndNotified(t *t handler2.ValidateEndpoints(t, endpoints) // Update the "foo" service with new endpoints - endpointsUpdate1 = CreateEndpointsUpdate(ADD, api.Endpoints{Name: "foo", Endpoints: []string{"endpoint77"}}) + endpointsUpdate1 = CreateEndpointsUpdate(ADD, api.Endpoints{ + JSONBase: api.JSONBase{ID: "foo"}, + Endpoints: []string{"endpoint77"}, + }) handler.Wait(1) handler2.Wait(1) channelOne <- endpointsUpdate1 @@ -266,7 +284,7 @@ func TestNewMultipleSourcesEndpointsMultipleHandlersAddRemoveSetAndNotified(t *t handler2.ValidateEndpoints(t, endpoints) // Remove "bar" service - endpointsUpdate2 = CreateEndpointsUpdate(REMOVE, api.Endpoints{Name: "bar"}) + endpointsUpdate2 = CreateEndpointsUpdate(REMOVE, api.Endpoints{JSONBase: api.JSONBase{ID: "bar"}}) handler.Wait(1) handler2.Wait(1) channelTwo <- endpointsUpdate2 diff --git a/pkg/proxy/config/etcd.go b/pkg/proxy/config/etcd.go index 93f63a165bf..e0451306916 100644 --- a/pkg/proxy/config/etcd.go +++ b/pkg/proxy/config/etcd.go @@ -34,7 +34,6 @@ limitations under the License. package config import ( - "encoding/json" "fmt" "strings" "time" @@ -127,7 +126,7 @@ func (s ConfigSourceEtcd) GetServices() ([]api.Service, []api.Endpoints, error) // and create a Service entry for it. for i, node := range response.Node.Nodes { var svc api.Service - err = json.Unmarshal([]byte(node.Value), &svc) + err = api.DecodeInto([]byte(node.Value), &svc) if err != nil { glog.Errorf("Failed to load Service: %s (%#v)", node.Value, err) continue @@ -154,7 +153,9 @@ func (s ConfigSourceEtcd) GetEndpoints(service string) (api.Endpoints, error) { return api.Endpoints{}, err } // Parse all the endpoint specifications in this value. - return parseEndpoints(response.Node.Value) + var e api.Endpoints + err = api.DecodeInto([]byte(response.Node.Value), &e) + return e, err } // etcdResponseToService takes an etcd response and pulls it apart to find service. @@ -163,19 +164,13 @@ func etcdResponseToService(response *etcd.Response) (*api.Service, error) { return nil, fmt.Errorf("invalid response from etcd: %#v", response) } var svc api.Service - err := json.Unmarshal([]byte(response.Node.Value), &svc) + err := api.DecodeInto([]byte(response.Node.Value), &svc) if err != nil { return nil, err } return &svc, err } -func parseEndpoints(jsonString string) (api.Endpoints, error) { - var e api.Endpoints - err := json.Unmarshal([]byte(jsonString), &e) - return e, err -} - func (s ConfigSourceEtcd) WatchForChanges() { glog.Info("Setting up a watch for new services") watchChannel := make(chan *etcd.Response) @@ -220,7 +215,7 @@ func (s ConfigSourceEtcd) ProcessChange(response *etcd.Response) { func (s ConfigSourceEtcd) ProcessEndpointResponse(response *etcd.Response) { glog.Infof("Processing a change in endpoint configuration... %s", *response) var endpoints api.Endpoints - err := json.Unmarshal([]byte(response.Node.Value), &endpoints) + err := api.DecodeInto([]byte(response.Node.Value), &endpoints) if err != nil { glog.Errorf("Failed to parse service out of etcd key: %v : %+v", response.Node.Value, err) return diff --git a/pkg/proxy/config/etcd_test.go b/pkg/proxy/config/etcd_test.go deleted file mode 100644 index 9b1dafbcccb..00000000000 --- a/pkg/proxy/config/etcd_test.go +++ /dev/null @@ -1,56 +0,0 @@ -/* -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 config - -import ( - "encoding/json" - "reflect" - "testing" - - "github.com/GoogleCloudPlatform/kubernetes/pkg/api" -) - -const TomcatContainerEtcdKey = "/registry/services/tomcat/endpoints/tomcat-3bd5af34" -const TomcatService = "tomcat" -const TomcatContainerID = "tomcat-3bd5af34" - -func validateJSONParsing(t *testing.T, jsonString string, expectedEndpoints api.Endpoints, expectError bool) { - endpoints, err := parseEndpoints(jsonString) - if err == nil && expectError { - t.Errorf("validateJSONParsing did not get expected error when parsing %s", jsonString) - } - if err != nil && !expectError { - t.Errorf("validateJSONParsing got unexpected error %+v when parsing %s", err, jsonString) - } - if !reflect.DeepEqual(expectedEndpoints, endpoints) { - t.Errorf("Didn't get expected endpoints %+v got: %+v", expectedEndpoints, endpoints) - } -} - -func TestParseJsonEndpoints(t *testing.T) { - validateJSONParsing(t, "", api.Endpoints{}, true) - endpoints := api.Endpoints{ - Name: "foo", - Endpoints: []string{"foo", "bar", "baz"}, - } - data, err := json.Marshal(endpoints) - if err != nil { - t.Errorf("Unexpected error: %#v", err) - } - validateJSONParsing(t, string(data), endpoints, false) - // validateJSONParsing(t, "[{\"port\":8000,\"name\":\"mysql\",\"machine\":\"foo\"},{\"port\":9000,\"name\":\"mysql\",\"machine\":\"bar\"}]", []string{"foo:8000", "bar:9000"}, false) -} diff --git a/pkg/proxy/config/file.go b/pkg/proxy/config/file.go index 2b4b897aa73..f3f3608e786 100644 --- a/pkg/proxy/config/file.go +++ b/pkg/proxy/config/file.go @@ -112,7 +112,7 @@ func (s ConfigSourceFile) Run() { newEndpoints := make([]api.Endpoints, len(config.Services)) for i, service := range config.Services { newServices[i] = api.Service{JSONBase: api.JSONBase{ID: service.Name}, Port: service.Port} - newEndpoints[i] = api.Endpoints{Name: service.Name, Endpoints: service.Endpoints} + newEndpoints[i] = api.Endpoints{JSONBase: api.JSONBase{ID: service.Name}, Endpoints: service.Endpoints} } if !reflect.DeepEqual(lastServices, newServices) { serviceUpdate := ServiceUpdate{Op: SET, Services: newServices} diff --git a/pkg/proxy/proxier_test.go b/pkg/proxy/proxier_test.go index 5893b381ca2..8489d8d358f 100644 --- a/pkg/proxy/proxier_test.go +++ b/pkg/proxy/proxier_test.go @@ -52,7 +52,8 @@ func TestProxy(t *testing.T) { } lb := NewLoadBalancerRR() - lb.OnUpdate([]api.Endpoints{{"echo", []string{net.JoinHostPort("127.0.0.1", port)}}}) + lb.OnUpdate([]api.Endpoints{ + {JSONBase: api.JSONBase{ID: "echo"}, Endpoints: []string{net.JoinHostPort("127.0.0.1", port)}}}) p := NewProxier(lb) diff --git a/pkg/proxy/roundrobbin.go b/pkg/proxy/roundrobbin.go index 1f4c32794a4..b459318d104 100644 --- a/pkg/proxy/roundrobbin.go +++ b/pkg/proxy/roundrobbin.go @@ -89,15 +89,15 @@ func (impl LoadBalancerRR) OnUpdate(endpoints []api.Endpoints) { defer impl.lock.Unlock() // First update / add all new endpoints for services. for _, value := range endpoints { - existingEndpoints, exists := impl.endpointsMap[value.Name] + existingEndpoints, exists := impl.endpointsMap[value.ID] validEndpoints := impl.filterValidEndpoints(value.Endpoints) if !exists || !reflect.DeepEqual(existingEndpoints, validEndpoints) { - glog.Infof("LoadBalancerRR: Setting endpoints for %s to %+v", value.Name, value.Endpoints) - impl.endpointsMap[value.Name] = validEndpoints + glog.Infof("LoadBalancerRR: Setting endpoints for %s to %+v", value.ID, value.Endpoints) + impl.endpointsMap[value.ID] = validEndpoints // Start RR from the beginning if added or updated. - impl.rrIndex[value.Name] = 0 + impl.rrIndex[value.ID] = 0 } - tmp[value.Name] = true + tmp[value.ID] = true } // Then remove any endpoints no longer relevant for key, value := range impl.endpointsMap { diff --git a/pkg/proxy/roundrobbin_test.go b/pkg/proxy/roundrobbin_test.go index 1112141de80..55f0760bcbb 100644 --- a/pkg/proxy/roundrobbin_test.go +++ b/pkg/proxy/roundrobbin_test.go @@ -87,7 +87,10 @@ func TestLoadBalanceWorksWithSingleEndpoint(t *testing.T) { t.Errorf("Didn't fail with non-existent service") } endpoints := make([]api.Endpoints, 1) - endpoints[0] = api.Endpoints{Name: "foo", Endpoints: []string{"endpoint1:40"}} + endpoints[0] = api.Endpoints{ + JSONBase: api.JSONBase{ID: "foo"}, + Endpoints: []string{"endpoint1:40"}, + } loadBalancer.OnUpdate(endpoints) expectEndpoint(t, loadBalancer, "foo", "endpoint1:40") expectEndpoint(t, loadBalancer, "foo", "endpoint1:40") @@ -102,7 +105,10 @@ func TestLoadBalanceWorksWithMultipleEndpoints(t *testing.T) { t.Errorf("Didn't fail with non-existent service") } endpoints := make([]api.Endpoints, 1) - endpoints[0] = api.Endpoints{Name: "foo", Endpoints: []string{"endpoint:1", "endpoint:2", "endpoint:3"}} + endpoints[0] = api.Endpoints{ + JSONBase: api.JSONBase{ID: "foo"}, + Endpoints: []string{"endpoint:1", "endpoint:2", "endpoint:3"}, + } loadBalancer.OnUpdate(endpoints) expectEndpoint(t, loadBalancer, "foo", "endpoint:1") expectEndpoint(t, loadBalancer, "foo", "endpoint:2") @@ -117,7 +123,10 @@ func TestLoadBalanceWorksWithMultipleEndpointsAndUpdates(t *testing.T) { t.Errorf("Didn't fail with non-existent service") } endpoints := make([]api.Endpoints, 1) - endpoints[0] = api.Endpoints{Name: "foo", Endpoints: []string{"endpoint:1", "endpoint:2", "endpoint:3"}} + endpoints[0] = api.Endpoints{ + JSONBase: api.JSONBase{ID: "foo"}, + Endpoints: []string{"endpoint:1", "endpoint:2", "endpoint:3"}, + } loadBalancer.OnUpdate(endpoints) expectEndpoint(t, loadBalancer, "foo", "endpoint:1") expectEndpoint(t, loadBalancer, "foo", "endpoint:2") @@ -126,14 +135,16 @@ func TestLoadBalanceWorksWithMultipleEndpointsAndUpdates(t *testing.T) { expectEndpoint(t, loadBalancer, "foo", "endpoint:2") // Then update the configuration with one fewer endpoints, make sure // we start in the beginning again - endpoints[0] = api.Endpoints{Name: "foo", Endpoints: []string{"endpoint:8", "endpoint:9"}} + endpoints[0] = api.Endpoints{JSONBase: api.JSONBase{ID: "foo"}, + Endpoints: []string{"endpoint:8", "endpoint:9"}, + } loadBalancer.OnUpdate(endpoints) expectEndpoint(t, loadBalancer, "foo", "endpoint:8") expectEndpoint(t, loadBalancer, "foo", "endpoint:9") expectEndpoint(t, loadBalancer, "foo", "endpoint:8") expectEndpoint(t, loadBalancer, "foo", "endpoint:9") // Clear endpoints - endpoints[0] = api.Endpoints{Name: "foo", Endpoints: []string{}} + endpoints[0] = api.Endpoints{JSONBase: api.JSONBase{ID: "foo"}, Endpoints: []string{}} loadBalancer.OnUpdate(endpoints) endpoint, err = loadBalancer.LoadBalance("foo", nil) @@ -149,8 +160,14 @@ func TestLoadBalanceWorksWithServiceRemoval(t *testing.T) { t.Errorf("Didn't fail with non-existent service") } endpoints := make([]api.Endpoints, 2) - endpoints[0] = api.Endpoints{Name: "foo", Endpoints: []string{"endpoint:1", "endpoint:2", "endpoint:3"}} - endpoints[1] = api.Endpoints{Name: "bar", Endpoints: []string{"endpoint:4", "endpoint:5"}} + endpoints[0] = api.Endpoints{ + JSONBase: api.JSONBase{ID: "foo"}, + Endpoints: []string{"endpoint:1", "endpoint:2", "endpoint:3"}, + } + endpoints[1] = api.Endpoints{ + JSONBase: api.JSONBase{ID: "bar"}, + Endpoints: []string{"endpoint:4", "endpoint:5"}, + } loadBalancer.OnUpdate(endpoints) expectEndpoint(t, loadBalancer, "foo", "endpoint:1") expectEndpoint(t, loadBalancer, "foo", "endpoint:2") diff --git a/pkg/registry/endpoints.go b/pkg/registry/endpoints.go index 3696685e74d..f7d34f89899 100644 --- a/pkg/registry/endpoints.go +++ b/pkg/registry/endpoints.go @@ -88,7 +88,7 @@ func (e *EndpointController) SyncServiceEndpoints() error { endpoints[ix] = net.JoinHostPort(pod.CurrentState.PodIP, strconv.Itoa(port)) } err = e.serviceRegistry.UpdateEndpoints(api.Endpoints{ - Name: service.ID, + JSONBase: api.JSONBase{ID: service.ID}, Endpoints: endpoints, }) if err != nil { diff --git a/pkg/registry/etcd_registry.go b/pkg/registry/etcd_registry.go index 4bed72cf900..1c8075eea0c 100644 --- a/pkg/registry/etcd_registry.go +++ b/pkg/registry/etcd_registry.go @@ -112,9 +112,10 @@ func (registry *EtcdRegistry) runPod(pod api.Pod, machine string) error { } contKey := makeContainerKey(machine) - err = registry.helper().AtomicUpdate(contKey, &[]api.ContainerManifest{}, func(in interface{}) (interface{}, error) { - manifests := *in.(*[]api.ContainerManifest) - return append(manifests, manifest), nil + err = registry.helper().AtomicUpdate(contKey, &api.ContainerManifestList{}, func(in interface{}) (interface{}, error) { + manifests := *in.(*api.ContainerManifestList) + manifests.Items = append(manifests.Items, manifest) + return manifests, nil }) if err != nil { // Don't strand stuff. @@ -153,11 +154,11 @@ func (registry *EtcdRegistry) deletePodFromMachine(machine, podID string) error // Next, remove the pod from the machine atomically. contKey := makeContainerKey(machine) - return registry.helper().AtomicUpdate(contKey, &[]api.ContainerManifest{}, func(in interface{}) (interface{}, error) { - manifests := *in.(*[]api.ContainerManifest) - newManifests := make([]api.ContainerManifest, 0, len(manifests)) + return registry.helper().AtomicUpdate(contKey, &api.ContainerManifestList{}, func(in interface{}) (interface{}, error) { + manifests := in.(*api.ContainerManifestList) + newManifests := make([]api.ContainerManifest, 0, len(manifests.Items)) found := false - for _, manifest := range manifests { + for _, manifest := range manifests.Items { if manifest.ID != podID { newManifests = append(newManifests, manifest) } else { @@ -170,7 +171,8 @@ func (registry *EtcdRegistry) deletePodFromMachine(machine, podID string) error // However it is "deleted" so log it and move on glog.Infof("Couldn't find: %s in %#v", podID, manifests) } - return newManifests, nil + manifests.Items = newManifests + return manifests, nil }) } @@ -304,5 +306,5 @@ func (registry *EtcdRegistry) UpdateService(svc api.Service) error { // UpdateEndpoints update Endpoints of a Service. func (registry *EtcdRegistry) UpdateEndpoints(e api.Endpoints) error { - return registry.helper().SetObj("/registry/services/endpoints/"+e.Name, e) + return registry.helper().SetObj("/registry/services/endpoints/"+e.ID, e) } diff --git a/pkg/registry/etcd_registry_test.go b/pkg/registry/etcd_registry_test.go index 5d0718a3754..5ac151c4677 100644 --- a/pkg/registry/etcd_registry_test.go +++ b/pkg/registry/etcd_registry_test.go @@ -17,7 +17,6 @@ limitations under the License. package registry import ( - "encoding/json" "reflect" "testing" @@ -70,7 +69,7 @@ func TestEtcdCreatePod(t *testing.T) { }, E: tools.EtcdErrorNotFound, } - fakeClient.Set("/registry/hosts/machine/kubelet", util.MakeJSONString([]api.ContainerManifest{}), 0) + fakeClient.Set("/registry/hosts/machine/kubelet", api.EncodeOrDie(&api.ContainerManifestList{}), 0) registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"}) err := registry.CreatePod("machine", api.Pod{ JSONBase: api.JSONBase{ @@ -88,18 +87,20 @@ func TestEtcdCreatePod(t *testing.T) { }) expectNoError(t, err) resp, err := fakeClient.Get("/registry/hosts/machine/pods/foo", false, false) - expectNoError(t, err) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } var pod api.Pod - err = json.Unmarshal([]byte(resp.Node.Value), &pod) + err = api.DecodeInto([]byte(resp.Node.Value), &pod) expectNoError(t, err) if pod.ID != "foo" { t.Errorf("Unexpected pod: %#v %s", pod, resp.Node.Value) } - var manifests []api.ContainerManifest + var manifests api.ContainerManifestList resp, err = fakeClient.Get("/registry/hosts/machine/kubelet", false, false) expectNoError(t, err) - err = json.Unmarshal([]byte(resp.Node.Value), &manifests) - if len(manifests) != 1 || manifests[0].ID != "foo" { + err = api.DecodeInto([]byte(resp.Node.Value), &manifests) + if len(manifests.Items) != 1 || manifests.Items[0].ID != "foo" { t.Errorf("Unexpected manifest list: %#v", manifests) } } @@ -189,18 +190,20 @@ func TestEtcdCreatePodWithContainersNotFound(t *testing.T) { }) expectNoError(t, err) resp, err := fakeClient.Get("/registry/hosts/machine/pods/foo", false, false) - expectNoError(t, err) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } var pod api.Pod - err = json.Unmarshal([]byte(resp.Node.Value), &pod) + err = api.DecodeInto([]byte(resp.Node.Value), &pod) expectNoError(t, err) if pod.ID != "foo" { t.Errorf("Unexpected pod: %#v %s", pod, resp.Node.Value) } - var manifests []api.ContainerManifest + var manifests api.ContainerManifestList resp, err = fakeClient.Get("/registry/hosts/machine/kubelet", false, false) expectNoError(t, err) - err = json.Unmarshal([]byte(resp.Node.Value), &manifests) - if len(manifests) != 1 || manifests[0].ID != "foo" { + err = api.DecodeInto([]byte(resp.Node.Value), &manifests) + if len(manifests.Items) != 1 || manifests.Items[0].ID != "foo" { t.Errorf("Unexpected manifest list: %#v", manifests) } } @@ -213,9 +216,9 @@ func TestEtcdCreatePodWithExistingContainers(t *testing.T) { }, E: tools.EtcdErrorNotFound, } - fakeClient.Set("/registry/hosts/machine/kubelet", util.MakeJSONString([]api.ContainerManifest{ - { - ID: "bar", + fakeClient.Set("/registry/hosts/machine/kubelet", api.EncodeOrDie(api.ContainerManifestList{ + Items: []api.ContainerManifest{ + {ID: "bar"}, }, }), 0) registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"}) @@ -236,18 +239,20 @@ func TestEtcdCreatePodWithExistingContainers(t *testing.T) { }) expectNoError(t, err) resp, err := fakeClient.Get("/registry/hosts/machine/pods/foo", false, false) - expectNoError(t, err) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } var pod api.Pod - err = json.Unmarshal([]byte(resp.Node.Value), &pod) + err = api.DecodeInto([]byte(resp.Node.Value), &pod) expectNoError(t, err) if pod.ID != "foo" { t.Errorf("Unexpected pod: %#v %s", pod, resp.Node.Value) } - var manifests []api.ContainerManifest + var manifests api.ContainerManifestList resp, err = fakeClient.Get("/registry/hosts/machine/kubelet", false, false) expectNoError(t, err) - err = json.Unmarshal([]byte(resp.Node.Value), &manifests) - if len(manifests) != 2 || manifests[1].ID != "foo" { + err = api.DecodeInto([]byte(resp.Node.Value), &manifests) + if len(manifests.Items) != 2 || manifests.Items[1].ID != "foo" { t.Errorf("Unexpected manifest list: %#v", manifests) } } @@ -256,9 +261,9 @@ func TestEtcdDeletePod(t *testing.T) { fakeClient := tools.MakeFakeEtcdClient(t) key := "/registry/hosts/machine/pods/foo" fakeClient.Set(key, util.MakeJSONString(api.Pod{JSONBase: api.JSONBase{ID: "foo"}}), 0) - fakeClient.Set("/registry/hosts/machine/kubelet", util.MakeJSONString([]api.ContainerManifest{ - { - ID: "foo", + fakeClient.Set("/registry/hosts/machine/kubelet", api.EncodeOrDie(&api.ContainerManifestList{ + Items: []api.ContainerManifest{ + {ID: "foo"}, }, }), 0) registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"}) @@ -269,8 +274,13 @@ func TestEtcdDeletePod(t *testing.T) { } else if fakeClient.DeletedKeys[0] != key { t.Errorf("Unexpected key: %s, expected %s", fakeClient.DeletedKeys[0], key) } - response, _ := fakeClient.Get("/registry/hosts/machine/kubelet", false, false) - if response.Node.Value != "[]" { + response, err := fakeClient.Get("/registry/hosts/machine/kubelet", false, false) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + var manifests api.ContainerManifestList + api.DecodeInto([]byte(response.Node.Value), &manifests) + if len(manifests.Items) != 0 { t.Errorf("Unexpected container set: %s, expected empty", response.Node.Value) } } @@ -279,9 +289,11 @@ func TestEtcdDeletePodMultipleContainers(t *testing.T) { fakeClient := tools.MakeFakeEtcdClient(t) key := "/registry/hosts/machine/pods/foo" fakeClient.Set(key, util.MakeJSONString(api.Pod{JSONBase: api.JSONBase{ID: "foo"}}), 0) - fakeClient.Set("/registry/hosts/machine/kubelet", util.MakeJSONString([]api.ContainerManifest{ - {ID: "foo"}, - {ID: "bar"}, + fakeClient.Set("/registry/hosts/machine/kubelet", api.EncodeOrDie(&api.ContainerManifestList{ + Items: []api.ContainerManifest{ + {ID: "foo"}, + {ID: "bar"}, + }, }), 0) registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"}) err := registry.DeletePod("foo") @@ -292,13 +304,16 @@ func TestEtcdDeletePodMultipleContainers(t *testing.T) { if fakeClient.DeletedKeys[0] != key { t.Errorf("Unexpected key: %s, expected %s", fakeClient.DeletedKeys[0], key) } - response, _ := fakeClient.Get("/registry/hosts/machine/kubelet", false, false) - var manifests []api.ContainerManifest - json.Unmarshal([]byte(response.Node.Value), &manifests) - if len(manifests) != 1 { - t.Errorf("Unexpected manifest set: %#v, expected empty", manifests) + response, err := fakeClient.Get("/registry/hosts/machine/kubelet", false, false) + if err != nil { + t.Fatalf("Unexpected error %v", err) } - if manifests[0].ID != "bar" { + var manifests api.ContainerManifestList + api.DecodeInto([]byte(response.Node.Value), &manifests) + if len(manifests.Items) != 1 { + t.Fatalf("Unexpected manifest set: %#v, expected empty", manifests) + } + if manifests.Items[0].ID != "bar" { t.Errorf("Deleted wrong manifest: %#v", manifests) } } @@ -476,9 +491,11 @@ func TestEtcdCreateController(t *testing.T) { }) expectNoError(t, err) resp, err := fakeClient.Get("/registry/controllers/foo", false, false) - expectNoError(t, err) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } var ctrl api.ReplicationController - err = json.Unmarshal([]byte(resp.Node.Value), &ctrl) + err = api.DecodeInto([]byte(resp.Node.Value), &ctrl) expectNoError(t, err) if ctrl.ID != "foo" { t.Errorf("Unexpected pod: %#v %s", ctrl, resp.Node.Value) @@ -544,7 +561,7 @@ func TestEtcdCreateService(t *testing.T) { resp, err := fakeClient.Get("/registry/services/specs/foo", false, false) expectNoError(t, err) var service api.Service - err = json.Unmarshal([]byte(resp.Node.Value), &service) + err = api.DecodeInto([]byte(resp.Node.Value), &service) expectNoError(t, err) if service.ID != "foo" { t.Errorf("Unexpected service: %#v %s", service, resp.Node.Value) @@ -621,15 +638,17 @@ func TestEtcdUpdateEndpoints(t *testing.T) { fakeClient := tools.MakeFakeEtcdClient(t) registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"}) endpoints := api.Endpoints{ - Name: "foo", + JSONBase: api.JSONBase{ID: "foo"}, Endpoints: []string{"baz", "bar"}, } err := registry.UpdateEndpoints(endpoints) expectNoError(t, err) response, err := fakeClient.Get("/registry/services/endpoints/foo", false, false) - expectNoError(t, err) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } var endpointsOut api.Endpoints - err = json.Unmarshal([]byte(response.Node.Value), &endpointsOut) + err = api.DecodeInto([]byte(response.Node.Value), &endpointsOut) if !reflect.DeepEqual(endpoints, endpointsOut) { t.Errorf("Unexpected endpoints: %#v, expected %#v", endpointsOut, endpoints) } diff --git a/pkg/tools/etcd_tools.go b/pkg/tools/etcd_tools.go index 39539e1289c..4472b32a10e 100644 --- a/pkg/tools/etcd_tools.go +++ b/pkg/tools/etcd_tools.go @@ -112,7 +112,7 @@ func (h *EtcdHelper) ExtractList(key string, slicePtr interface{}) error { v := pv.Elem() for _, node := range nodes { obj := reflect.New(v.Type().Elem()) - err = json.Unmarshal([]byte(node.Value), obj.Interface()) + err = api.DecodeInto([]byte(node.Value), obj.Interface()) if err != nil { return err } @@ -146,9 +146,9 @@ func (h *EtcdHelper) bodyAndExtractObj(key string, objPtr interface{}, ignoreNot return "", 0, fmt.Errorf("key '%v' found no nodes field: %#v", key, response) } body = response.Node.Value - err = json.Unmarshal([]byte(body), objPtr) + 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. @@ -159,7 +159,7 @@ func (h *EtcdHelper) bodyAndExtractObj(key string, objPtr interface{}, ignoreNot // SetObj marshals obj via json, and stores under key. Will do an // atomic update if obj's ResourceVersion field is set. func (h *EtcdHelper) SetObj(key string, obj interface{}) error { - data, err := json.Marshal(obj) + data, err := api.Encode(obj) if err != nil { return err } diff --git a/pkg/tools/etcd_tools_test.go b/pkg/tools/etcd_tools_test.go index 0796bbb8773..905e5091b48 100644 --- a/pkg/tools/etcd_tools_test.go +++ b/pkg/tools/etcd_tools_test.go @@ -45,10 +45,6 @@ func TestIsNotFoundErr(t *testing.T) { try(fmt.Errorf("some other kind of error"), false) } -type testMarshalType struct { - ID string `json:"id"` -} - func TestExtractList(t *testing.T) { fakeClient := MakeFakeEtcdClient(t) fakeClient.Data["/some/key"] = EtcdResponseWithError{ @@ -68,12 +64,12 @@ func TestExtractList(t *testing.T) { }, }, } - expect := []testMarshalType{ - {"foo"}, - {"bar"}, - {"baz"}, + expect := []api.Pod{ + {JSONBase: api.JSONBase{ID: "foo"}}, + {JSONBase: api.JSONBase{ID: "bar"}}, + {JSONBase: api.JSONBase{ID: "baz"}}, } - var got []testMarshalType + var got []api.Pod helper := EtcdHelper{fakeClient} err := helper.ExtractList("/some/key", &got) if err != nil { @@ -86,10 +82,10 @@ func TestExtractList(t *testing.T) { func TestExtractObj(t *testing.T) { fakeClient := MakeFakeEtcdClient(t) - expect := testMarshalType{ID: "foo"} + expect := api.Pod{JSONBase: api.JSONBase{ID: "foo"}} fakeClient.Set("/some/key", util.MakeJSONString(expect), 0) helper := EtcdHelper{fakeClient} - var got testMarshalType + var got api.Pod err := helper.ExtractObj("/some/key", &got, false) if err != nil { t.Errorf("Unexpected error %#v", err) @@ -123,7 +119,7 @@ func TestExtractObjNotFoundErr(t *testing.T) { } helper := EtcdHelper{fakeClient} try := func(key string) { - var got testMarshalType + var got api.Pod err := helper.ExtractObj(key, &got, false) if err == nil { t.Errorf("%s: wanted error but didn't get one", key) @@ -140,14 +136,18 @@ func TestExtractObjNotFoundErr(t *testing.T) { } func TestSetObj(t *testing.T) { - obj := testMarshalType{ID: "foo"} + obj := api.Pod{JSONBase: api.JSONBase{ID: "foo"}} fakeClient := MakeFakeEtcdClient(t) helper := EtcdHelper{fakeClient} err := helper.SetObj("/some/key", obj) if err != nil { t.Errorf("Unexpected error %#v", err) } - expect := util.MakeJSONString(obj) + data, err := api.Encode(obj) + if err != nil { + t.Errorf("Unexpected error %#v", err) + } + expect := string(data) got := fakeClient.Data["/some/key"].R.Node.Value if expect != got { t.Errorf("Wanted %v, got %v", expect, got)