diff --git a/pkg/registry/etcd/etcd.go b/pkg/registry/etcd/etcd.go index 7e6d1b2aa79..409121f01cc 100644 --- a/pkg/registry/etcd/etcd.go +++ b/pkg/registry/etcd/etcd.go @@ -120,7 +120,7 @@ func (r *Registry) CreatePod(pod *api.Pod) error { // DesiredState.Host == "" is a signal to the scheduler that this pod needs scheduling. pod.DesiredState.Status = api.PodRunning pod.DesiredState.Host = "" - err := r.CreateObj(makePodKey(pod.ID), pod) + err := r.CreateObj(makePodKey(pod.ID), pod, 0) return etcderr.InterpretCreateError(err, "pod", pod.ID) } @@ -254,7 +254,7 @@ func (r *Registry) GetController(controllerID string) (*api.ReplicationControlle // CreateController creates a new ReplicationController. func (r *Registry) CreateController(controller *api.ReplicationController) error { - err := r.CreateObj(makeControllerKey(controller.ID), controller) + err := r.CreateObj(makeControllerKey(controller.ID), controller, 0) return etcderr.InterpretCreateError(err, "replicationController", controller.ID) } @@ -284,7 +284,7 @@ func (r *Registry) ListServices() (*api.ServiceList, error) { // CreateService creates a new Service. func (r *Registry) CreateService(svc *api.Service) error { - err := r.CreateObj(makeServiceKey(svc.ID), svc) + err := r.CreateObj(makeServiceKey(svc.ID), svc, 0) return etcderr.InterpretCreateError(err, "service", svc.ID) } diff --git a/pkg/runtime/helper.go b/pkg/runtime/helper.go new file mode 100644 index 00000000000..dcede13cfa8 --- /dev/null +++ b/pkg/runtime/helper.go @@ -0,0 +1,91 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package runtime + +import ( + "fmt" + "reflect" +) + +// GetItemsPtr returns a pointer to the list object's Items member. +// If 'list' doesn't have an Items member, it's not really a list type +// and an error will be returned. +// This function will either return a pointer to a slice, or an error, but not both. +func GetItemsPtr(list Object) (interface{}, error) { + v := reflect.ValueOf(list) + if !v.IsValid() { + return nil, fmt.Errorf("nil list object") + } + items := v.Elem().FieldByName("Items") + if !items.IsValid() { + return nil, fmt.Errorf("no Items field in %#v", list) + } + if items.Kind() != reflect.Slice { + return nil, fmt.Errorf("Items field is not a slice") + } + return items.Addr().Interface(), nil +} + +// ExtractList returns obj's Items element as an array of runtime.Objects. +// Returns an error if obj is not a List type (does not have an Items member). +func ExtractList(obj Object) ([]Object, error) { + itemsPtr, err := GetItemsPtr(obj) + if err != nil { + return nil, err + } + items := reflect.ValueOf(itemsPtr).Elem() + list := make([]Object, items.Len()) + for i := range list { + raw := items.Index(i) + item, ok := raw.Addr().Interface().(Object) + if !ok { + return nil, fmt.Errorf("item in index %v isn't an object: %#v", i, raw.Interface()) + } + list[i] = item + } + return list, nil +} + +// SetList sets the given list object's Items member have the elements given in +// objects. +// Returns an error if list is not a List type (does not have an Items member), +// or if any of the objects are not of the right type. +func SetList(list Object, objects []Object) error { + itemsPtr, err := GetItemsPtr(list) + if err != nil { + return err + } + items := reflect.ValueOf(itemsPtr).Elem() + slice := reflect.MakeSlice(items.Type(), len(objects), len(objects)) + for i := range objects { + dest := slice.Index(i) + src := reflect.ValueOf(objects[i]) + if !src.IsValid() || src.IsNil() { + return fmt.Errorf("an object was nil") + } + src = src.Elem() // Object is a pointer, but the items in slice are not. + if src.Type().AssignableTo(dest.Type()) { + dest.Set(src) + } else if src.Type().ConvertibleTo(dest.Type()) { + dest.Set(src.Convert(dest.Type())) + } else { + return fmt.Errorf("wrong type: need %v, got %v", dest.Type(), src.Type()) + } + } + items.Set(slice) + return nil +} diff --git a/pkg/runtime/helper_test.go b/pkg/runtime/helper_test.go new file mode 100644 index 00000000000..f0d43522e0a --- /dev/null +++ b/pkg/runtime/helper_test.go @@ -0,0 +1,93 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package runtime_test + +import ( + "reflect" + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + + "github.com/google/gofuzz" +) + +func TestExtractList(t *testing.T) { + pl := &api.PodList{ + Items: []api.Pod{ + {JSONBase: api.JSONBase{ID: "1"}}, + {JSONBase: api.JSONBase{ID: "2"}}, + {JSONBase: api.JSONBase{ID: "3"}}, + }, + } + list, err := runtime.ExtractList(pl) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + if e, a := len(list), len(pl.Items); e != a { + t.Fatalf("Expected %v, got %v", e, a) + } + for i := range list { + if e, a := list[i].(*api.Pod).ID, pl.Items[i].ID; e != a { + t.Fatalf("Expected %v, got %v", e, a) + } + } +} + +func TestSetList(t *testing.T) { + pl := &api.PodList{} + list := []runtime.Object{ + &api.Pod{JSONBase: api.JSONBase{ID: "1"}}, + &api.Pod{JSONBase: api.JSONBase{ID: "2"}}, + &api.Pod{JSONBase: api.JSONBase{ID: "3"}}, + } + err := runtime.SetList(pl, list) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + if e, a := len(list), len(pl.Items); e != a { + t.Fatalf("Expected %v, got %v", e, a) + } + for i := range list { + if e, a := list[i].(*api.Pod).ID, pl.Items[i].ID; e != a { + t.Fatalf("Expected %v, got %v", e, a) + } + } +} + +func TestSetExtractListRoundTrip(t *testing.T) { + fuzzer := fuzz.New().NilChance(0).NumElements(1, 5) + for i := 0; i < 5; i++ { + start := &api.PodList{} + fuzzer.Fuzz(&start.Items) + + list, err := runtime.ExtractList(start) + if err != nil { + t.Errorf("Unexpected error %v", err) + continue + } + got := &api.PodList{} + err = runtime.SetList(got, list) + if err != nil { + t.Errorf("Unexpected error %v", err) + continue + } + if e, a := start, got; !reflect.DeepEqual(e, a) { + t.Fatalf("Expected %#v, got %#v", e, a) + } + } +} diff --git a/pkg/runtime/scheme.go b/pkg/runtime/scheme.go index 84cbc67cebf..4620e5a704b 100644 --- a/pkg/runtime/scheme.go +++ b/pkg/runtime/scheme.go @@ -400,28 +400,3 @@ func (metaInsertion) Interpret(in interface{}) (version, kind string) { m := in.(*metaInsertion) return m.JSONBase.APIVersion, m.JSONBase.Kind } - -// Extract list returns obj's Items element as an array of runtime.Objects. -// Returns an error if obj is not a List type (does not have an Items member). -func ExtractList(obj Object) ([]Object, error) { - v := reflect.ValueOf(obj) - if !v.IsValid() { - return nil, fmt.Errorf("nil object") - } - items := v.Elem().FieldByName("Items") - if !items.IsValid() { - return nil, fmt.Errorf("no Items field") - } - if items.Kind() != reflect.Slice { - return nil, fmt.Errorf("Items field is not a slice") - } - list := make([]Object, items.Len()) - for i := range list { - item, ok := items.Index(i).Addr().Interface().(Object) - if !ok { - return nil, fmt.Errorf("item in index %v isn't an object", i) - } - list[i] = item - } - return list, nil -} diff --git a/pkg/tools/etcd_tools.go b/pkg/tools/etcd_tools.go index 7432a49fad4..bc8f4e75a03 100644 --- a/pkg/tools/etcd_tools.go +++ b/pkg/tools/etcd_tools.go @@ -123,6 +123,7 @@ func (h *EtcdHelper) listEtcdNode(key string) ([]*etcd.Node, uint64, error) { } // ExtractList extracts a go object per etcd node into a slice with the resource version. +// DEPRECATED: Use ExtractToList instead, it's more convenient. func (h *EtcdHelper) ExtractList(key string, slicePtr interface{}, resourceVersion *uint64) error { nodes, index, err := h.listEtcdNode(key) if resourceVersion != nil { @@ -152,6 +153,27 @@ func (h *EtcdHelper) ExtractList(key string, slicePtr interface{}, resourceVersi return nil } +// ExtractToList is just like ExtractList, but it works on a ThingyList api object. +// extracts a go object per etcd node into a slice with the resource version. +func (h *EtcdHelper) ExtractToList(key string, listObj runtime.Object) error { + var resourceVersion uint64 + listPtr, err := runtime.GetItemsPtr(listObj) + if err != nil { + return err + } + err = h.ExtractList(key, listPtr, &resourceVersion) + if err != nil { + return err + } + if h.ResourceVersioner != nil { + err = h.ResourceVersioner.SetResourceVersion(listObj, resourceVersion) + if err != nil { + return err + } + } + return nil +} + // ExtractObj unmarshals json found at key into objPtr. On a not found error, will either return // a zero object of the requested type, or an error, depending on ignoreNotFound. Treats // empty responses and nil response nodes exactly like a not found error. @@ -185,8 +207,9 @@ func (h *EtcdHelper) bodyAndExtractObj(key string, objPtr runtime.Object, ignore return body, response.Node.ModifiedIndex, err } -// CreateObj adds a new object at a key unless it already exists. -func (h *EtcdHelper) CreateObj(key string, obj runtime.Object) error { +// CreateObj adds a new object at a key unless it already exists. 'ttl' is time-to-live in seconds, +// and 0 means forever. +func (h *EtcdHelper) CreateObj(key string, obj runtime.Object, ttl uint64) error { data, err := h.Codec.Encode(obj) if err != nil { return err @@ -197,7 +220,7 @@ func (h *EtcdHelper) CreateObj(key string, obj runtime.Object) error { } } - _, err = h.Client.Create(key, string(data), 0) + _, err = h.Client.Create(key, string(data), ttl) return err } diff --git a/pkg/tools/etcd_tools_test.go b/pkg/tools/etcd_tools_test.go index 32c937c8f2f..5c4f1a5bb86 100644 --- a/pkg/tools/etcd_tools_test.go +++ b/pkg/tools/etcd_tools_test.go @@ -65,7 +65,7 @@ func TestIsEtcdNotFound(t *testing.T) { try(fmt.Errorf("some other kind of error"), false) } -func TestExtractList(t *testing.T) { +func TestExtractToList(t *testing.T) { fakeClient := NewFakeEtcdClient(t) fakeClient.Data["/some/key"] = EtcdResponseWithError{ R: &etcd.Response{ @@ -88,27 +88,23 @@ func TestExtractList(t *testing.T) { }, }, } - expect := []api.Pod{ - {JSONBase: api.JSONBase{ID: "foo", ResourceVersion: 1}}, - {JSONBase: api.JSONBase{ID: "bar", ResourceVersion: 2}}, - {JSONBase: api.JSONBase{ID: "baz", ResourceVersion: 3}}, + expect := api.PodList{ + JSONBase: api.JSONBase{ResourceVersion: 10}, + Items: []api.Pod{ + {JSONBase: api.JSONBase{ID: "foo", ResourceVersion: 1}}, + {JSONBase: api.JSONBase{ID: "bar", ResourceVersion: 2}}, + {JSONBase: api.JSONBase{ID: "baz", ResourceVersion: 3}}, + }, } - var got []api.Pod + var got api.PodList helper := EtcdHelper{fakeClient, latest.Codec, versioner} - resourceVersion := uint64(0) - err := helper.ExtractList("/some/key", &got, &resourceVersion) + err := helper.ExtractToList("/some/key", &got) if err != nil { - t.Errorf("Unexpected error %#v", err) + t.Errorf("Unexpected error %v", err) } - if resourceVersion != 10 { - t.Errorf("Unexpected resource version %d", resourceVersion) - } - - for i := 0; i < len(expect); i++ { - if !reflect.DeepEqual(got[i], expect[i]) { - t.Errorf("\nWanted:\n%#v\nGot:\n%#v\n", expect[i], got[i]) - } + if e, a := expect, got; !reflect.DeepEqual(e, a) { + t.Errorf("Expected %#v, got %#v", e, a) } } @@ -167,6 +163,27 @@ func TestExtractObjNotFoundErr(t *testing.T) { try("/some/key3") } +func TestCreateObj(t *testing.T) { + obj := &api.Pod{JSONBase: api.JSONBase{ID: "foo"}} + fakeClient := NewFakeEtcdClient(t) + helper := EtcdHelper{fakeClient, latest.Codec, versioner} + err := helper.CreateObj("/some/key", obj, 5) + if err != nil { + t.Errorf("Unexpected error %#v", err) + } + data, err := latest.Codec.Encode(obj) + if err != nil { + t.Errorf("Unexpected error %#v", err) + } + node := fakeClient.Data["/some/key"].R.Node + if e, a := string(data), node.Value; e != a { + t.Errorf("Wanted %v, got %v", e, a) + } + if e, a := uint64(5), fakeClient.LastSetTTL; e != a { + t.Errorf("Wanted %v, got %v", e, a) + } +} + func TestSetObj(t *testing.T) { obj := &api.Pod{JSONBase: api.JSONBase{ID: "foo"}} fakeClient := NewFakeEtcdClient(t) diff --git a/pkg/tools/fake_etcd_client.go b/pkg/tools/fake_etcd_client.go index 0f3c078f7a5..bc8dec9330f 100644 --- a/pkg/tools/fake_etcd_client.go +++ b/pkg/tools/fake_etcd_client.go @@ -49,6 +49,7 @@ type FakeEtcdClient struct { Ix int TestIndex bool ChangeIndex uint64 + LastSetTTL uint64 // Will become valid after Watch is called; tester may write to it. Tester may // also read from it to verify that it's closed after injecting an error. @@ -135,6 +136,7 @@ func (f *FakeEtcdClient) nodeExists(key string) bool { } func (f *FakeEtcdClient) setLocked(key, value string, ttl uint64) (*etcd.Response, error) { + f.LastSetTTL = ttl if f.Err != nil { return nil, f.Err }