From 65d62809360e0c9481604130a6441a5c85cbecb5 Mon Sep 17 00:00:00 2001 From: Daniel Smith Date: Tue, 17 Jun 2014 16:23:52 -0700 Subject: [PATCH] Move etcd helper to util so it can be used elsewhere, too --- pkg/kubelet/kubelet_test.go | 21 ++-- pkg/registry/etcd_registry.go | 120 +++--------------- pkg/registry/etcd_registry_test.go | 134 ++++++++++---------- pkg/util/etcd_tools.go | 128 +++++++++++++++++++ pkg/util/etcd_tools_test.go | 135 +++++++++++++++++++++ pkg/{registry => util}/fake_etcd_client.go | 14 +-- 6 files changed, 364 insertions(+), 188 deletions(-) create mode 100644 pkg/util/etcd_tools.go create mode 100644 pkg/util/etcd_tools_test.go rename pkg/{registry => util}/fake_etcd_client.go (86%) diff --git a/pkg/kubelet/kubelet_test.go b/pkg/kubelet/kubelet_test.go index 46d8472639b..6eea97987ec 100644 --- a/pkg/kubelet/kubelet_test.go +++ b/pkg/kubelet/kubelet_test.go @@ -27,7 +27,6 @@ import ( "testing" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" - "github.com/GoogleCloudPlatform/kubernetes/pkg/registry" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/coreos/go-etcd/etcd" "github.com/fsouza/go-dockerclient" @@ -398,13 +397,13 @@ func (cr *channelReader) GetList() [][]api.ContainerManifest { } func TestGetKubeletStateFromEtcdNoData(t *testing.T) { - fakeClient := registry.MakeFakeEtcdClient(t) + fakeClient := util.MakeFakeEtcdClient(t) kubelet := Kubelet{ Client: fakeClient, } channel := make(chan []api.ContainerManifest) reader := startReading(channel) - fakeClient.Data["/registry/hosts/machine/kubelet"] = registry.EtcdResponseWithError{ + fakeClient.Data["/registry/hosts/machine/kubelet"] = util.EtcdResponseWithError{ R: &etcd.Response{}, E: nil, } @@ -420,13 +419,13 @@ func TestGetKubeletStateFromEtcdNoData(t *testing.T) { } func TestGetKubeletStateFromEtcd(t *testing.T) { - fakeClient := registry.MakeFakeEtcdClient(t) + fakeClient := util.MakeFakeEtcdClient(t) kubelet := Kubelet{ Client: fakeClient, } channel := make(chan []api.ContainerManifest) reader := startReading(channel) - fakeClient.Data["/registry/hosts/machine/kubelet"] = registry.EtcdResponseWithError{ + fakeClient.Data["/registry/hosts/machine/kubelet"] = util.EtcdResponseWithError{ R: &etcd.Response{ Node: &etcd.Node{ Value: util.MakeJSONString([]api.Container{}), @@ -444,13 +443,13 @@ func TestGetKubeletStateFromEtcd(t *testing.T) { } func TestGetKubeletStateFromEtcdNotFound(t *testing.T) { - fakeClient := registry.MakeFakeEtcdClient(t) + fakeClient := util.MakeFakeEtcdClient(t) kubelet := Kubelet{ Client: fakeClient, } channel := make(chan []api.ContainerManifest) reader := startReading(channel) - fakeClient.Data["/registry/hosts/machine/kubelet"] = registry.EtcdResponseWithError{ + fakeClient.Data["/registry/hosts/machine/kubelet"] = util.EtcdResponseWithError{ R: &etcd.Response{}, E: &etcd.EtcdError{ ErrorCode: 100, @@ -466,13 +465,13 @@ func TestGetKubeletStateFromEtcdNotFound(t *testing.T) { } func TestGetKubeletStateFromEtcdError(t *testing.T) { - fakeClient := registry.MakeFakeEtcdClient(t) + fakeClient := util.MakeFakeEtcdClient(t) kubelet := Kubelet{ Client: fakeClient, } channel := make(chan []api.ContainerManifest) reader := startReading(channel) - fakeClient.Data["/registry/hosts/machine/kubelet"] = registry.EtcdResponseWithError{ + fakeClient.Data["/registry/hosts/machine/kubelet"] = util.EtcdResponseWithError{ R: &etcd.Response{}, E: &etcd.EtcdError{ ErrorCode: 200, // non not found error @@ -554,7 +553,7 @@ func TestSyncManifestsDeletes(t *testing.T) { } func TestEventWriting(t *testing.T) { - fakeEtcd := registry.MakeFakeEtcdClient(t) + fakeEtcd := util.MakeFakeEtcdClient(t) kubelet := &Kubelet{ Client: fakeEtcd, } @@ -581,7 +580,7 @@ func TestEventWriting(t *testing.T) { } func TestEventWritingError(t *testing.T) { - fakeEtcd := registry.MakeFakeEtcdClient(t) + fakeEtcd := util.MakeFakeEtcdClient(t) kubelet := &Kubelet{ Client: fakeEtcd, } diff --git a/pkg/registry/etcd_registry.go b/pkg/registry/etcd_registry.go index fcf5ee064c6..b45233d907f 100644 --- a/pkg/registry/etcd_registry.go +++ b/pkg/registry/etcd_registry.go @@ -13,18 +13,19 @@ 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 registry import ( "encoding/json" "fmt" "log" - "reflect" "github.com/coreos/go-etcd/etcd" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" ) // TODO: Need to add a reconciler loop that makes sure that things in pods are reflected into @@ -68,11 +69,15 @@ func makePodKey(machine, podID string) string { return "/registry/hosts/" + machine + "/pods/" + podID } +func (registry *EtcdRegistry) helper() *util.EtcdHelper { + return &util.EtcdHelper{registry.etcdClient} +} + func (registry *EtcdRegistry) ListPods(query labels.Query) ([]api.Pod, error) { pods := []api.Pod{} for _, machine := range registry.machines { var machinePods []api.Pod - err := registry.extractList("/registry/hosts/"+machine+"/pods", &machinePods) + err := registry.helper().ExtractList("/registry/hosts/"+machine+"/pods", &machinePods) if err != nil { return pods, err } @@ -86,80 +91,6 @@ func (registry *EtcdRegistry) ListPods(query labels.Query) ([]api.Pod, error) { return pods, nil } -func (registry *EtcdRegistry) listEtcdNode(key string) ([]*etcd.Node, error) { - result, err := registry.etcdClient.Get(key, false, true) - if err != nil { - nodes := make([]*etcd.Node, 0) - if isEtcdNotFound(err) { - return nodes, nil - } else { - return nodes, err - } - } - return result.Node.Nodes, nil -} - -// Extract a go object per etcd node into a slice. -func (r *EtcdRegistry) extractList(key string, slicePtr interface{}) error { - nodes, err := r.listEtcdNode(key) - if err != nil { - return err - } - pv := reflect.ValueOf(slicePtr) - if pv.Type().Kind() != reflect.Ptr || pv.Type().Elem().Kind() != reflect.Slice { - // This should not happen at runtime. - panic("need ptr to slice") - } - v := pv.Elem() - for _, node := range nodes { - obj := reflect.New(v.Type().Elem()) - err = json.Unmarshal([]byte(node.Value), obj.Interface()) - if err != nil { - return err - } - v.Set(reflect.Append(v, obj.Elem())) - } - return nil -} - -// 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. -func (r *EtcdRegistry) extractObj(key string, objPtr interface{}, ignoreNotFound bool) error { - response, err := r.etcdClient.Get(key, false, false) - returnZero := false - if err != nil { - if ignoreNotFound && isEtcdNotFound(err) { - returnZero = true - } else { - return err - } - } - if !returnZero && (response.Node == nil || len(response.Node.Value) == 0) { - if ignoreNotFound { - returnZero = true - } else { - return fmt.Errorf("key '%v' found no nodes field: %#v", key, response) - } - } - if returnZero { - pv := reflect.ValueOf(objPtr) - pv.Elem().Set(reflect.Zero(pv.Type().Elem())) - return nil - } - return json.Unmarshal([]byte(response.Node.Value), objPtr) -} - -// json marshals obj, and stores under key. -func (r *EtcdRegistry) setObj(key string, obj interface{}) error { - data, err := json.Marshal(obj) - if err != nil { - return err - } - _, err = r.etcdClient.Set(key, string(data), 0) - return err -} - func (registry *EtcdRegistry) GetPod(podID string) (*api.Pod, error) { pod, _, err := registry.findPod(podID) return &pod, err @@ -170,12 +101,12 @@ func makeContainerKey(machine string) string { } func (registry *EtcdRegistry) loadManifests(machine string) (manifests []api.ContainerManifest, err error) { - err = registry.extractObj(makeContainerKey(machine), &manifests, true) + err = registry.helper().ExtractObj(makeContainerKey(machine), &manifests, true) return } func (registry *EtcdRegistry) updateManifests(machine string, manifests []api.ContainerManifest) error { - return registry.setObj(makeContainerKey(machine), manifests) + return registry.helper().SetObj(makeContainerKey(machine), manifests) } func (registry *EtcdRegistry) CreatePod(machineIn string, pod api.Pod) error { @@ -249,7 +180,7 @@ func (registry *EtcdRegistry) deletePodFromMachine(machine, podID string) error func (registry *EtcdRegistry) getPodForMachine(machine, podID string) (pod api.Pod, err error) { key := makePodKey(machine, podID) - err = registry.extractObj(key, &pod, false) + err = registry.helper().ExtractObj(key, &pod, false) if err != nil { return } @@ -267,26 +198,9 @@ func (registry *EtcdRegistry) findPod(podID string) (api.Pod, string, error) { return api.Pod{}, "", fmt.Errorf("pod not found %s", podID) } -func isEtcdNotFound(err error) bool { - if err == nil { - return false - } - switch err.(type) { - case *etcd.EtcdError: - etcdError := err.(*etcd.EtcdError) - if etcdError == nil { - return false - } - if etcdError.ErrorCode == 100 { - return true - } - } - return false -} - func (registry *EtcdRegistry) ListControllers() ([]api.ReplicationController, error) { var controllers []api.ReplicationController - err := registry.extractList("/registry/controllers", &controllers) + err := registry.helper().ExtractList("/registry/controllers", &controllers) return controllers, err } @@ -297,7 +211,7 @@ func makeControllerKey(id string) string { func (registry *EtcdRegistry) GetController(controllerID string) (*api.ReplicationController, error) { var controller api.ReplicationController key := makeControllerKey(controllerID) - err := registry.extractObj(key, &controller, false) + err := registry.helper().ExtractObj(key, &controller, false) if err != nil { return nil, err } @@ -310,7 +224,7 @@ func (registry *EtcdRegistry) CreateController(controller api.ReplicationControl } func (registry *EtcdRegistry) UpdateController(controller api.ReplicationController) error { - return registry.setObj(makeControllerKey(controller.ID), controller) + return registry.helper().SetObj(makeControllerKey(controller.ID), controller) } func (registry *EtcdRegistry) DeleteController(controllerID string) error { @@ -325,18 +239,18 @@ func makeServiceKey(name string) string { func (registry *EtcdRegistry) ListServices() (api.ServiceList, error) { var list api.ServiceList - err := registry.extractList("/registry/services/specs", &list.Items) + err := registry.helper().ExtractList("/registry/services/specs", &list.Items) return list, err } func (registry *EtcdRegistry) CreateService(svc api.Service) error { - return registry.setObj(makeServiceKey(svc.ID), svc) + return registry.helper().SetObj(makeServiceKey(svc.ID), svc) } func (registry *EtcdRegistry) GetService(name string) (*api.Service, error) { key := makeServiceKey(name) var svc api.Service - err := registry.extractObj(key, &svc, false) + err := registry.helper().ExtractObj(key, &svc, false) if err != nil { return nil, err } @@ -359,5 +273,5 @@ func (registry *EtcdRegistry) UpdateService(svc api.Service) error { } func (registry *EtcdRegistry) UpdateEndpoints(e api.Endpoints) error { - return registry.setObj("/registry/services/endpoints/"+e.Name, e) + return registry.helper().SetObj("/registry/services/endpoints/"+e.Name, e) } diff --git a/pkg/registry/etcd_registry_test.go b/pkg/registry/etcd_registry_test.go index 4e13dc9ab12..27b30884e44 100644 --- a/pkg/registry/etcd_registry_test.go +++ b/pkg/registry/etcd_registry_test.go @@ -26,8 +26,16 @@ import ( "github.com/coreos/go-etcd/etcd" ) +func MakeTestEtcdRegistry(client EtcdClient, machines []string) *EtcdRegistry { + registry := MakeEtcdRegistry(client, machines) + registry.manifestFactory = &BasicManifestFactory{ + serviceRegistry: &MockServiceRegistry{}, + } + return registry +} + func TestEtcdGetPod(t *testing.T) { - fakeClient := MakeFakeEtcdClient(t) + fakeClient := util.MakeFakeEtcdClient(t) fakeClient.Set("/registry/hosts/machine/pods/foo", util.MakeJSONString(api.Pod{JSONBase: api.JSONBase{ID: "foo"}}), 0) registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"}) pod, err := registry.GetPod("foo") @@ -38,8 +46,8 @@ func TestEtcdGetPod(t *testing.T) { } func TestEtcdGetPodNotFound(t *testing.T) { - fakeClient := MakeFakeEtcdClient(t) - fakeClient.Data["/registry/hosts/machine/pods/foo"] = EtcdResponseWithError{ + fakeClient := util.MakeFakeEtcdClient(t) + fakeClient.Data["/registry/hosts/machine/pods/foo"] = util.EtcdResponseWithError{ R: &etcd.Response{ Node: nil, }, @@ -55,8 +63,8 @@ func TestEtcdGetPodNotFound(t *testing.T) { } func TestEtcdCreatePod(t *testing.T) { - fakeClient := MakeFakeEtcdClient(t) - fakeClient.Data["/registry/hosts/machine/pods/foo"] = EtcdResponseWithError{ + fakeClient := util.MakeFakeEtcdClient(t) + fakeClient.Data["/registry/hosts/machine/pods/foo"] = util.EtcdResponseWithError{ R: &etcd.Response{ Node: nil, }, @@ -97,8 +105,8 @@ func TestEtcdCreatePod(t *testing.T) { } func TestEtcdCreatePodAlreadyExisting(t *testing.T) { - fakeClient := MakeFakeEtcdClient(t) - fakeClient.Data["/registry/hosts/machine/pods/foo"] = EtcdResponseWithError{ + fakeClient := util.MakeFakeEtcdClient(t) + fakeClient.Data["/registry/hosts/machine/pods/foo"] = util.EtcdResponseWithError{ R: &etcd.Response{ Node: &etcd.Node{ Value: util.MakeJSONString(api.Pod{JSONBase: api.JSONBase{ID: "foo"}}), @@ -118,14 +126,14 @@ func TestEtcdCreatePodAlreadyExisting(t *testing.T) { } func TestEtcdCreatePodWithContainersError(t *testing.T) { - fakeClient := MakeFakeEtcdClient(t) - fakeClient.Data["/registry/hosts/machine/pods/foo"] = EtcdResponseWithError{ + fakeClient := util.MakeFakeEtcdClient(t) + fakeClient.Data["/registry/hosts/machine/pods/foo"] = util.EtcdResponseWithError{ R: &etcd.Response{ Node: nil, }, E: &etcd.EtcdError{ErrorCode: 100}, } - fakeClient.Data["/registry/hosts/machine/kubelet"] = EtcdResponseWithError{ + fakeClient.Data["/registry/hosts/machine/kubelet"] = util.EtcdResponseWithError{ R: &etcd.Response{ Node: nil, }, @@ -150,14 +158,14 @@ func TestEtcdCreatePodWithContainersError(t *testing.T) { } func TestEtcdCreatePodWithContainersNotFound(t *testing.T) { - fakeClient := MakeFakeEtcdClient(t) - fakeClient.Data["/registry/hosts/machine/pods/foo"] = EtcdResponseWithError{ + fakeClient := util.MakeFakeEtcdClient(t) + fakeClient.Data["/registry/hosts/machine/pods/foo"] = util.EtcdResponseWithError{ R: &etcd.Response{ Node: nil, }, E: &etcd.EtcdError{ErrorCode: 100}, } - fakeClient.Data["/registry/hosts/machine/kubelet"] = EtcdResponseWithError{ + fakeClient.Data["/registry/hosts/machine/kubelet"] = util.EtcdResponseWithError{ R: &etcd.Response{ Node: nil, }, @@ -198,8 +206,8 @@ func TestEtcdCreatePodWithContainersNotFound(t *testing.T) { } func TestEtcdCreatePodWithExistingContainers(t *testing.T) { - fakeClient := MakeFakeEtcdClient(t) - fakeClient.Data["/registry/hosts/machine/pods/foo"] = EtcdResponseWithError{ + fakeClient := util.MakeFakeEtcdClient(t) + fakeClient.Data["/registry/hosts/machine/pods/foo"] = util.EtcdResponseWithError{ R: &etcd.Response{ Node: nil, }, @@ -245,7 +253,7 @@ func TestEtcdCreatePodWithExistingContainers(t *testing.T) { } func TestEtcdDeletePod(t *testing.T) { - fakeClient := MakeFakeEtcdClient(t) + fakeClient := util.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{ @@ -256,11 +264,11 @@ func TestEtcdDeletePod(t *testing.T) { registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"}) err := registry.DeletePod("foo") expectNoError(t, err) - if len(fakeClient.deletedKeys) != 1 { - t.Errorf("Expected 1 delete, found %#v", fakeClient.deletedKeys) + if len(fakeClient.DeletedKeys) != 1 { + t.Errorf("Expected 1 delete, found %#v", fakeClient.DeletedKeys) } - if fakeClient.deletedKeys[0] != key { - t.Errorf("Unexpected key: %s, expected %s", fakeClient.deletedKeys[0], key) + 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 != "[]" { @@ -269,7 +277,7 @@ func TestEtcdDeletePod(t *testing.T) { } func TestEtcdDeletePodMultipleContainers(t *testing.T) { - fakeClient := MakeFakeEtcdClient(t) + fakeClient := util.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{ @@ -279,11 +287,11 @@ func TestEtcdDeletePodMultipleContainers(t *testing.T) { registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"}) err := registry.DeletePod("foo") expectNoError(t, err) - if len(fakeClient.deletedKeys) != 1 { - t.Errorf("Expected 1 delete, found %#v", fakeClient.deletedKeys) + if len(fakeClient.DeletedKeys) != 1 { + t.Errorf("Expected 1 delete, found %#v", fakeClient.DeletedKeys) } - if fakeClient.deletedKeys[0] != key { - t.Errorf("Unexpected key: %s, expected %s", fakeClient.deletedKeys[0], key) + 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 @@ -297,9 +305,9 @@ func TestEtcdDeletePodMultipleContainers(t *testing.T) { } func TestEtcdEmptyListPods(t *testing.T) { - fakeClient := MakeFakeEtcdClient(t) + fakeClient := util.MakeFakeEtcdClient(t) key := "/registry/hosts/machine/pods" - fakeClient.Data[key] = EtcdResponseWithError{ + fakeClient.Data[key] = util.EtcdResponseWithError{ R: &etcd.Response{ Node: &etcd.Node{ Nodes: []*etcd.Node{}, @@ -316,9 +324,9 @@ func TestEtcdEmptyListPods(t *testing.T) { } func TestEtcdListPodsNotFound(t *testing.T) { - fakeClient := MakeFakeEtcdClient(t) + fakeClient := util.MakeFakeEtcdClient(t) key := "/registry/hosts/machine/pods" - fakeClient.Data[key] = EtcdResponseWithError{ + fakeClient.Data[key] = util.EtcdResponseWithError{ R: &etcd.Response{}, E: &etcd.EtcdError{ErrorCode: 100}, } @@ -331,9 +339,9 @@ func TestEtcdListPodsNotFound(t *testing.T) { } func TestEtcdListPods(t *testing.T) { - fakeClient := MakeFakeEtcdClient(t) + fakeClient := util.MakeFakeEtcdClient(t) key := "/registry/hosts/machine/pods" - fakeClient.Data[key] = EtcdResponseWithError{ + fakeClient.Data[key] = util.EtcdResponseWithError{ R: &etcd.Response{ Node: &etcd.Node{ Nodes: []*etcd.Node{ @@ -361,9 +369,9 @@ func TestEtcdListPods(t *testing.T) { } func TestEtcdListControllersNotFound(t *testing.T) { - fakeClient := MakeFakeEtcdClient(t) + fakeClient := util.MakeFakeEtcdClient(t) key := "/registry/controllers" - fakeClient.Data[key] = EtcdResponseWithError{ + fakeClient.Data[key] = util.EtcdResponseWithError{ R: &etcd.Response{}, E: &etcd.EtcdError{ErrorCode: 100}, } @@ -376,9 +384,9 @@ func TestEtcdListControllersNotFound(t *testing.T) { } func TestEtcdListServicesNotFound(t *testing.T) { - fakeClient := MakeFakeEtcdClient(t) + fakeClient := util.MakeFakeEtcdClient(t) key := "/registry/services/specs" - fakeClient.Data[key] = EtcdResponseWithError{ + fakeClient.Data[key] = util.EtcdResponseWithError{ R: &etcd.Response{}, E: &etcd.EtcdError{ErrorCode: 100}, } @@ -391,9 +399,9 @@ func TestEtcdListServicesNotFound(t *testing.T) { } func TestEtcdListControllers(t *testing.T) { - fakeClient := MakeFakeEtcdClient(t) + fakeClient := util.MakeFakeEtcdClient(t) key := "/registry/controllers" - fakeClient.Data[key] = EtcdResponseWithError{ + fakeClient.Data[key] = util.EtcdResponseWithError{ R: &etcd.Response{ Node: &etcd.Node{ Nodes: []*etcd.Node{ @@ -417,7 +425,7 @@ func TestEtcdListControllers(t *testing.T) { } func TestEtcdGetController(t *testing.T) { - fakeClient := MakeFakeEtcdClient(t) + fakeClient := util.MakeFakeEtcdClient(t) fakeClient.Set("/registry/controllers/foo", util.MakeJSONString(api.ReplicationController{JSONBase: api.JSONBase{ID: "foo"}}), 0) registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"}) ctrl, err := registry.GetController("foo") @@ -428,8 +436,8 @@ func TestEtcdGetController(t *testing.T) { } func TestEtcdGetControllerNotFound(t *testing.T) { - fakeClient := MakeFakeEtcdClient(t) - fakeClient.Data["/registry/controllers/foo"] = EtcdResponseWithError{ + fakeClient := util.MakeFakeEtcdClient(t) + fakeClient.Data["/registry/controllers/foo"] = util.EtcdResponseWithError{ R: &etcd.Response{ Node: nil, }, @@ -448,21 +456,21 @@ func TestEtcdGetControllerNotFound(t *testing.T) { } func TestEtcdDeleteController(t *testing.T) { - fakeClient := MakeFakeEtcdClient(t) + fakeClient := util.MakeFakeEtcdClient(t) registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"}) err := registry.DeleteController("foo") expectNoError(t, err) - if len(fakeClient.deletedKeys) != 1 { - t.Errorf("Expected 1 delete, found %#v", fakeClient.deletedKeys) + if len(fakeClient.DeletedKeys) != 1 { + t.Errorf("Expected 1 delete, found %#v", fakeClient.DeletedKeys) } key := "/registry/controllers/foo" - if fakeClient.deletedKeys[0] != key { - t.Errorf("Unexpected key: %s, expected %s", fakeClient.deletedKeys[0], key) + if fakeClient.DeletedKeys[0] != key { + t.Errorf("Unexpected key: %s, expected %s", fakeClient.DeletedKeys[0], key) } } func TestEtcdCreateController(t *testing.T) { - fakeClient := MakeFakeEtcdClient(t) + fakeClient := util.MakeFakeEtcdClient(t) registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"}) err := registry.CreateController(api.ReplicationController{ JSONBase: api.JSONBase{ @@ -481,7 +489,7 @@ func TestEtcdCreateController(t *testing.T) { } func TestEtcdUpdateController(t *testing.T) { - fakeClient := MakeFakeEtcdClient(t) + fakeClient := util.MakeFakeEtcdClient(t) fakeClient.Set("/registry/controllers/foo", util.MakeJSONString(api.ReplicationController{JSONBase: api.JSONBase{ID: "foo"}}), 0) registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"}) err := registry.UpdateController(api.ReplicationController{ @@ -498,9 +506,9 @@ func TestEtcdUpdateController(t *testing.T) { } func TestEtcdListServices(t *testing.T) { - fakeClient := MakeFakeEtcdClient(t) + fakeClient := util.MakeFakeEtcdClient(t) key := "/registry/services/specs" - fakeClient.Data[key] = EtcdResponseWithError{ + fakeClient.Data[key] = util.EtcdResponseWithError{ R: &etcd.Response{ Node: &etcd.Node{ Nodes: []*etcd.Node{ @@ -524,8 +532,8 @@ func TestEtcdListServices(t *testing.T) { } func TestEtcdCreateService(t *testing.T) { - fakeClient := MakeFakeEtcdClient(t) - fakeClient.Data["/registry/services/specs/foo"] = EtcdResponseWithError{ + fakeClient := util.MakeFakeEtcdClient(t) + fakeClient.Data["/registry/services/specs/foo"] = util.EtcdResponseWithError{ R: &etcd.Response{ Node: nil, }, @@ -547,7 +555,7 @@ func TestEtcdCreateService(t *testing.T) { } func TestEtcdGetService(t *testing.T) { - fakeClient := MakeFakeEtcdClient(t) + fakeClient := util.MakeFakeEtcdClient(t) fakeClient.Set("/registry/services/specs/foo", util.MakeJSONString(api.Service{JSONBase: api.JSONBase{ID: "foo"}}), 0) registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"}) service, err := registry.GetService("foo") @@ -558,8 +566,8 @@ func TestEtcdGetService(t *testing.T) { } func TestEtcdGetServiceNotFound(t *testing.T) { - fakeClient := MakeFakeEtcdClient(t) - fakeClient.Data["/registry/services/specs/foo"] = EtcdResponseWithError{ + fakeClient := util.MakeFakeEtcdClient(t) + fakeClient.Data["/registry/services/specs/foo"] = util.EtcdResponseWithError{ R: &etcd.Response{ Node: nil, }, @@ -575,25 +583,25 @@ func TestEtcdGetServiceNotFound(t *testing.T) { } func TestEtcdDeleteService(t *testing.T) { - fakeClient := MakeFakeEtcdClient(t) + fakeClient := util.MakeFakeEtcdClient(t) registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"}) err := registry.DeleteService("foo") expectNoError(t, err) - if len(fakeClient.deletedKeys) != 2 { - t.Errorf("Expected 2 delete, found %#v", fakeClient.deletedKeys) + if len(fakeClient.DeletedKeys) != 2 { + t.Errorf("Expected 2 delete, found %#v", fakeClient.DeletedKeys) } key := "/registry/services/specs/foo" - if fakeClient.deletedKeys[0] != key { - t.Errorf("Unexpected key: %s, expected %s", fakeClient.deletedKeys[0], key) + if fakeClient.DeletedKeys[0] != key { + t.Errorf("Unexpected key: %s, expected %s", fakeClient.DeletedKeys[0], key) } key = "/registry/services/endpoints/foo" - if fakeClient.deletedKeys[1] != key { - t.Errorf("Unexpected key: %s, expected %s", fakeClient.deletedKeys[1], key) + if fakeClient.DeletedKeys[1] != key { + t.Errorf("Unexpected key: %s, expected %s", fakeClient.DeletedKeys[1], key) } } func TestEtcdUpdateService(t *testing.T) { - fakeClient := MakeFakeEtcdClient(t) + fakeClient := util.MakeFakeEtcdClient(t) fakeClient.Set("/registry/services/specs/foo", util.MakeJSONString(api.Service{JSONBase: api.JSONBase{ID: "foo"}}), 0) registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"}) err := registry.UpdateService(api.Service{ @@ -610,7 +618,7 @@ func TestEtcdUpdateService(t *testing.T) { } func TestEtcdUpdateEndpoints(t *testing.T) { - fakeClient := MakeFakeEtcdClient(t) + fakeClient := util.MakeFakeEtcdClient(t) registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"}) endpoints := api.Endpoints{ Name: "foo", diff --git a/pkg/util/etcd_tools.go b/pkg/util/etcd_tools.go new file mode 100644 index 00000000000..55a4dd897b1 --- /dev/null +++ b/pkg/util/etcd_tools.go @@ -0,0 +1,128 @@ +/* +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 util + +import ( + "encoding/json" + "fmt" + "reflect" + + "github.com/coreos/go-etcd/etcd" +) + +// Interface exposing only the etcd operations needed by EtcdHelper. +type EtcdGetSet interface { + Get(key string, sort, recursive bool) (*etcd.Response, error) + Set(key, value string, ttl uint64) (*etcd.Response, error) +} + +// EtcdHelper offers common object marshalling/unmarshalling operations on an etcd client. +type EtcdHelper struct { + Client EtcdGetSet +} + +// Returns true iff err is an etcd not found error. +func IsEtcdNotFound(err error) bool { + if err == nil { + return false + } + switch err.(type) { + case *etcd.EtcdError: + etcdError := err.(*etcd.EtcdError) + if etcdError == nil { + return false + } + if etcdError.ErrorCode == 100 { + return true + } + } + return false +} + +func (h *EtcdHelper) listEtcdNode(key string) ([]*etcd.Node, error) { + result, err := h.Client.Get(key, false, true) + if err != nil { + nodes := make([]*etcd.Node, 0) + if IsEtcdNotFound(err) { + return nodes, nil + } else { + return nodes, err + } + } + return result.Node.Nodes, nil +} + +// Extract a go object per etcd node into a slice. +func (h *EtcdHelper) ExtractList(key string, slicePtr interface{}) error { + nodes, err := h.listEtcdNode(key) + if err != nil { + return err + } + pv := reflect.ValueOf(slicePtr) + if pv.Type().Kind() != reflect.Ptr || pv.Type().Elem().Kind() != reflect.Slice { + // This should not happen at runtime. + panic("need ptr to slice") + } + v := pv.Elem() + for _, node := range nodes { + obj := reflect.New(v.Type().Elem()) + err = json.Unmarshal([]byte(node.Value), obj.Interface()) + if err != nil { + return err + } + v.Set(reflect.Append(v, obj.Elem())) + } + return nil +} + +// 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. +func (h *EtcdHelper) ExtractObj(key string, objPtr interface{}, ignoreNotFound bool) error { + response, err := h.Client.Get(key, false, false) + returnZero := false + if err != nil { + if ignoreNotFound && IsEtcdNotFound(err) { + returnZero = true + } else { + return err + } + } + if !returnZero && (response.Node == nil || len(response.Node.Value) == 0) { + if ignoreNotFound { + returnZero = true + } else { + return fmt.Errorf("key '%v' found no nodes field: %#v", key, response) + } + } + if returnZero { + pv := reflect.ValueOf(objPtr) + pv.Elem().Set(reflect.Zero(pv.Type().Elem())) + return nil + } + return json.Unmarshal([]byte(response.Node.Value), objPtr) +} + +// SetObj marshals obj via json, and stores under key. +func (h *EtcdHelper) SetObj(key string, obj interface{}) error { + data, err := json.Marshal(obj) + if err != nil { + return err + } + _, err = h.Client.Set(key, string(data), 0) + return err +} diff --git a/pkg/util/etcd_tools_test.go b/pkg/util/etcd_tools_test.go new file mode 100644 index 00000000000..ebf11e835c7 --- /dev/null +++ b/pkg/util/etcd_tools_test.go @@ -0,0 +1,135 @@ +package util + +import ( + "fmt" + "reflect" + "testing" + + "github.com/coreos/go-etcd/etcd" +) + +type fakeEtcdGetSet struct { + get func(key string, sort, recursive bool) (*etcd.Response, error) + set func(key, value string, ttl uint64) (*etcd.Response, error) +} + +func TestIsNotFoundErr(t *testing.T) { + try := func(err error, isNotFound bool) { + if IsEtcdNotFound(err) != isNotFound { + t.Errorf("Expected %#v to return %v, but it did not", err, isNotFound) + } + } + try(&etcd.EtcdError{ErrorCode: 100}, true) + try(&etcd.EtcdError{ErrorCode: 101}, false) + try(nil, false) + 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{ + R: &etcd.Response{ + Node: &etcd.Node{ + Nodes: []*etcd.Node{ + { + Value: `{"id":"foo"}`, + }, + { + Value: `{"id":"bar"}`, + }, + { + Value: `{"id":"baz"}`, + }, + }, + }, + }, + } + expect := []testMarshalType{ + {"foo"}, + {"bar"}, + {"baz"}, + } + var got []testMarshalType + helper := EtcdHelper{fakeClient} + err := helper.ExtractList("/some/key", &got) + if err != nil { + t.Errorf("Unexpected error %#v", err) + } + if !reflect.DeepEqual(got, expect) { + t.Errorf("Wanted %#v, got %#v", expect, got) + } +} + +func TestExtractObj(t *testing.T) { + fakeClient := MakeFakeEtcdClient(t) + expect := testMarshalType{ID: "foo"} + fakeClient.Set("/some/key", MakeJSONString(expect), 0) + helper := EtcdHelper{fakeClient} + var got testMarshalType + err := helper.ExtractObj("/some/key", &got, false) + if err != nil { + t.Errorf("Unexpected error %#v", err) + } + if !reflect.DeepEqual(got, expect) { + t.Errorf("Wanted %#v, got %#v", expect, got) + } +} + +func TestExtractObjNotFoundErr(t *testing.T) { + fakeClient := MakeFakeEtcdClient(t) + fakeClient.Data["/some/key"] = EtcdResponseWithError{ + R: &etcd.Response{ + Node: nil, + }, + E: &etcd.EtcdError{ + ErrorCode: 100, + }, + } + fakeClient.Data["/some/key2"] = EtcdResponseWithError{ + R: &etcd.Response{ + Node: nil, + }, + } + fakeClient.Data["/some/key3"] = EtcdResponseWithError{ + R: &etcd.Response{ + Node: &etcd.Node{ + Value: "", + }, + }, + } + helper := EtcdHelper{fakeClient} + try := func(key string) { + var got testMarshalType + err := helper.ExtractObj(key, &got, false) + if err == nil { + t.Errorf("%s: wanted error but didn't get one", key) + } + err = helper.ExtractObj(key, &got, true) + if err != nil { + t.Errorf("%s: didn't want error but got %#v", key, err) + } + } + + try("/some/key") + try("/some/key2") + try("/some/key3") +} + +func TestSetObj(t *testing.T) { + obj := testMarshalType{ID: "foo"} + fakeClient := MakeFakeEtcdClient(t) + helper := EtcdHelper{fakeClient} + err := helper.SetObj("/some/key", obj) + if err != nil { + t.Errorf("Unexpected error %#v", err) + } + expect := MakeJSONString(obj) + got := fakeClient.Data["/some/key"].R.Node.Value + if expect != got { + t.Errorf("Wanted %v, got %v", expect, got) + } +} diff --git a/pkg/registry/fake_etcd_client.go b/pkg/util/fake_etcd_client.go similarity index 86% rename from pkg/registry/fake_etcd_client.go rename to pkg/util/fake_etcd_client.go index d5a54bda1bd..9fff51a41b8 100644 --- a/pkg/registry/fake_etcd_client.go +++ b/pkg/util/fake_etcd_client.go @@ -13,7 +13,7 @@ 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 registry +package util import ( "fmt" @@ -29,7 +29,7 @@ type EtcdResponseWithError struct { type FakeEtcdClient struct { Data map[string]EtcdResponseWithError - deletedKeys []string + DeletedKeys []string Err error t *testing.T Ix int @@ -71,18 +71,10 @@ func (f *FakeEtcdClient) Create(key, value string, ttl uint64) (*etcd.Response, return f.Set(key, value, ttl) } func (f *FakeEtcdClient) Delete(key string, recursive bool) (*etcd.Response, error) { - f.deletedKeys = append(f.deletedKeys, key) + f.DeletedKeys = append(f.DeletedKeys, key) return &etcd.Response{}, f.Err } func (f *FakeEtcdClient) Watch(prefix string, waitIndex uint64, recursive bool, receiver chan *etcd.Response, stop chan bool) (*etcd.Response, error) { return nil, fmt.Errorf("unimplemented") } - -func MakeTestEtcdRegistry(client EtcdClient, machines []string) *EtcdRegistry { - registry := MakeEtcdRegistry(client, machines) - registry.manifestFactory = &BasicManifestFactory{ - serviceRegistry: &MockServiceRegistry{}, - } - return registry -}