diff --git a/pkg/registry/event/doc.go b/pkg/registry/event/doc.go new file mode 100644 index 00000000000..18aed52243e --- /dev/null +++ b/pkg/registry/event/doc.go @@ -0,0 +1,19 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package event provides Registry interface and it's REST +// implementation for storing Event api objects. +package event diff --git a/pkg/registry/event/registry.go b/pkg/registry/event/registry.go new file mode 100644 index 00000000000..e4cd9073a8f --- /dev/null +++ b/pkg/registry/event/registry.go @@ -0,0 +1,57 @@ +/* +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 event + +import ( + "path" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + etcderr "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors/etcd" + "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/generic" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/tools" +) + +// registry implements custom changes to generic.Etcd. +type registry struct { + *generic.Etcd + ttl uint64 +} + +// Create stores the object with a ttl, so that events don't stay in the system forever. +func (r registry) Create(ctx api.Context, id string, obj runtime.Object) error { + err := r.Etcd.Helper.CreateObj(r.Etcd.KeyFunc(id), obj, r.ttl) + return etcderr.InterpretCreateError(err, r.Etcd.EndpointName, id) +} + +// NewEtcdRegistry returns a registry which will store Events in the given +// EtcdHelper. ttl is the time that Events will be retained by the system. +func NewEtcdRegistry(h tools.EtcdHelper, ttl uint64) generic.Registry { + return registry{ + Etcd: &generic.Etcd{ + NewFunc: func() runtime.Object { return &api.Event{} }, + NewListFunc: func() runtime.Object { return &api.EventList{} }, + EndpointName: "events", + KeyRoot: "/registry/events", + KeyFunc: func(id string) string { + return path.Join("/registry/events", id) + }, + Helper: h, + }, + ttl: ttl, + } +} diff --git a/pkg/registry/event/registry_test.go b/pkg/registry/event/registry_test.go new file mode 100644 index 00000000000..fa60b9d5297 --- /dev/null +++ b/pkg/registry/event/registry_test.go @@ -0,0 +1,104 @@ +/* +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 event + +import ( + "reflect" + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/testapi" + "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/generic" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/tools" + + "github.com/coreos/go-etcd/etcd" +) + +var testTTL uint64 = 60 + +func NewTestEventEtcdRegistry(t *testing.T) (*tools.FakeEtcdClient, generic.Registry) { + f := tools.NewFakeEtcdClient(t) + f.TestIndex = true + h := tools.EtcdHelper{f, testapi.Codec(), tools.RuntimeVersionAdapter{testapi.ResourceVersioner()}} + return f, NewEtcdRegistry(h, testTTL) +} + +func TestEventCreate(t *testing.T) { + eventA := &api.Event{ + TypeMeta: api.TypeMeta{ID: "foo"}, + Reason: "forTesting", + } + eventB := &api.Event{ + TypeMeta: api.TypeMeta{ID: "foo"}, + Reason: "forTesting", + } + + nodeWithEventA := tools.EtcdResponseWithError{ + R: &etcd.Response{ + Node: &etcd.Node{ + Value: runtime.EncodeOrDie(testapi.Codec(), eventA), + ModifiedIndex: 1, + CreatedIndex: 1, + TTL: int64(testTTL), + }, + }, + E: nil, + } + + emptyNode := tools.EtcdResponseWithError{ + R: &etcd.Response{}, + E: tools.EtcdErrorNotFound, + } + + path := "/registry/events/foo" + key := "foo" + + table := map[string]struct { + existing tools.EtcdResponseWithError + expect tools.EtcdResponseWithError + toCreate runtime.Object + errOK func(error) bool + }{ + "normal": { + existing: emptyNode, + expect: nodeWithEventA, + toCreate: eventA, + errOK: func(err error) bool { return err == nil }, + }, + "preExisting": { + existing: nodeWithEventA, + expect: nodeWithEventA, + toCreate: eventB, + errOK: errors.IsAlreadyExists, + }, + } + + for name, item := range table { + fakeClient, registry := NewTestEventEtcdRegistry(t) + fakeClient.Data[path] = item.existing + err := registry.Create(api.NewContext(), key, item.toCreate) + if !item.errOK(err) { + t.Errorf("%v: unexpected error: %v", name, err) + } + + if e, a := item.expect, fakeClient.Data[path]; !reflect.DeepEqual(e, a) { + t.Errorf("%v:\n%s", name, runtime.ObjectDiff(e, a)) + } + } +} diff --git a/pkg/registry/event/rest.go b/pkg/registry/event/rest.go new file mode 100644 index 00000000000..b43088e2a29 --- /dev/null +++ b/pkg/registry/event/rest.go @@ -0,0 +1,121 @@ +/* +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 event + +import ( + "fmt" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/generic" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" +) + +// REST adapts an event registry into apiserver's RESTStorage model. +type REST struct { + registry generic.Registry +} + +// NewREST returns a new REST. You must use a registry created by +// NewEtcdRegistry unless you're testing. +func NewREST(registry generic.Registry) *REST { + return &REST{ + registry: registry, + } +} + +func (rs *REST) Create(ctx api.Context, obj runtime.Object) (<-chan runtime.Object, error) { + event, ok := obj.(*api.Event) + if !ok { + return nil, fmt.Errorf("invalid object type") + } + event.CreationTimestamp = util.Now() + + return apiserver.MakeAsync(func() (runtime.Object, error) { + err := rs.registry.Create(ctx, event.ID, event) + if err != nil { + return nil, err + } + return rs.registry.Get(ctx, event.ID) + }), nil +} + +func (rs *REST) Delete(ctx api.Context, id string) (<-chan runtime.Object, error) { + obj, err := rs.registry.Get(ctx, id) + if err != nil { + return nil, err + } + _, ok := obj.(*api.Event) + if !ok { + return nil, fmt.Errorf("invalid object type") + } + return apiserver.MakeAsync(func() (runtime.Object, error) { + return &api.Status{Status: api.StatusSuccess}, rs.registry.Delete(ctx, id) + }), nil +} + +func (rs *REST) Get(ctx api.Context, id string) (runtime.Object, error) { + obj, err := rs.registry.Get(ctx, id) + if err != nil { + return nil, err + } + event, ok := obj.(*api.Event) + if !ok { + return nil, fmt.Errorf("invalid object type") + } + return event, err +} + +func (rs *REST) getAttrs(obj runtime.Object) (objLabels, objFields labels.Set, err error) { + event, ok := obj.(*api.Event) + if !ok { + return nil, nil, fmt.Errorf("invalid object type") + } + return labels.Set{}, labels.Set{ + "InvolvedObject.Kind": event.InvolvedObject.Kind, + "InvolvedObject.Name": event.InvolvedObject.Name, + "InvolvedObject.UID": event.InvolvedObject.UID, + "InvolvedObject.APIVersion": event.InvolvedObject.APIVersion, + "InvolvedObject.ResourceVersion": fmt.Sprintf("%s", event.InvolvedObject.ResourceVersion), + "InvolvedObject.FieldPath": event.InvolvedObject.FieldPath, + "Status": event.Status, + "Reason": event.Reason, + }, nil +} + +func (rs *REST) List(ctx api.Context, label, field labels.Selector) (runtime.Object, error) { + return rs.registry.List(ctx, &generic.SelectionPredicate{label, field, rs.getAttrs}) +} + +// Watch returns Events events via a watch.Interface. +// It implements apiserver.ResourceWatcher. +func (rs *REST) Watch(ctx api.Context, label, field labels.Selector, resourceVersion uint64) (watch.Interface, error) { + return rs.registry.Watch(ctx, &generic.SelectionPredicate{label, field, rs.getAttrs}, resourceVersion) +} + +// New returns a new api.Event +func (*REST) New() runtime.Object { + return &api.Event{} +} + +// Update returns an error: Events are not mutable. +func (rs *REST) Update(ctx api.Context, obj runtime.Object) (<-chan runtime.Object, error) { + return nil, fmt.Errorf("not allowed: 'Event' objects are not mutable") +} diff --git a/pkg/registry/event/rest_test.go b/pkg/registry/event/rest_test.go new file mode 100644 index 00000000000..9bb1b0c67c9 --- /dev/null +++ b/pkg/registry/event/rest_test.go @@ -0,0 +1,226 @@ +/* +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 event + +import ( + "reflect" + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/testapi" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/registrytest" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" +) + +type testRegistry struct { + *registrytest.GenericRegistry +} + +func NewTestREST() (testRegistry, *REST) { + reg := testRegistry{registrytest.NewGeneric(nil)} + return reg, NewREST(reg) +} + +func TestRESTCreate(t *testing.T) { + _, rest := NewTestREST() + eventA := &api.Event{ + TypeMeta: api.TypeMeta{ID: "foo"}, + Reason: "forTesting", + } + c, err := rest.Create(api.NewContext(), eventA) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + if e, a := eventA, <-c; !reflect.DeepEqual(e, a) { + t.Errorf("diff: %s", runtime.ObjectDiff(e, a)) + } +} + +func TestRESTDelete(t *testing.T) { + _, rest := NewTestREST() + eventA := &api.Event{ + TypeMeta: api.TypeMeta{ID: "foo"}, + Reason: "forTesting", + } + c, err := rest.Create(api.NewContext(), eventA) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + <-c + c, err = rest.Delete(api.NewContext(), eventA.ID) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + if stat := (<-c).(*api.Status); stat.Status != api.StatusSuccess { + t.Errorf("unexpected status: %v", stat) + } +} + +func TestRESTGet(t *testing.T) { + _, rest := NewTestREST() + eventA := &api.Event{ + TypeMeta: api.TypeMeta{ID: "foo"}, + Reason: "forTesting", + } + c, err := rest.Create(api.NewContext(), eventA) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + <-c + got, err := rest.Get(api.NewContext(), eventA.ID) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + if e, a := eventA, got; !reflect.DeepEqual(e, a) { + t.Errorf("diff: %s", runtime.ObjectDiff(e, a)) + } +} + +func TestRESTgetAttrs(t *testing.T) { + _, rest := NewTestREST() + eventA := &api.Event{ + InvolvedObject: api.ObjectReference{ + Kind: "Pod", + Name: "foo", + UID: "long uid string", + APIVersion: testapi.Version(), + ResourceVersion: "0", + FieldPath: "", + }, + Status: "tested", + Reason: "forTesting", + } + label, field, err := rest.getAttrs(eventA) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + if e, a := label, (labels.Set{}); !reflect.DeepEqual(e, a) { + t.Errorf("diff: %s", runtime.ObjectDiff(e, a)) + } + expect := labels.Set{ + "InvolvedObject.Kind": "Pod", + "InvolvedObject.Name": "foo", + "InvolvedObject.UID": "long uid string", + "InvolvedObject.APIVersion": testapi.Version(), + "InvolvedObject.ResourceVersion": "0", + "InvolvedObject.FieldPath": "", + "Status": "tested", + "Reason": "forTesting", + } + if e, a := expect, field; !reflect.DeepEqual(e, a) { + t.Errorf("diff: %s", runtime.ObjectDiff(e, a)) + } +} + +func TestRESTUpdate(t *testing.T) { + _, rest := NewTestREST() + eventA := &api.Event{ + TypeMeta: api.TypeMeta{ID: "foo"}, + Reason: "forTesting", + } + c, err := rest.Create(api.NewContext(), eventA) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + <-c + _, err = rest.Update(api.NewContext(), eventA) + if err == nil { + t.Errorf("unexpected non-error") + } +} + +func TestRESTList(t *testing.T) { + reg, rest := NewTestREST() + eventA := &api.Event{ + InvolvedObject: api.ObjectReference{ + Kind: "Pod", + Name: "foo", + UID: "long uid string", + APIVersion: testapi.Version(), + ResourceVersion: "0", + FieldPath: "", + }, + Status: "tested", + Reason: "forTesting", + } + eventB := &api.Event{ + InvolvedObject: api.ObjectReference{ + Kind: "Pod", + Name: "bar", + UID: "other long uid string", + APIVersion: testapi.Version(), + ResourceVersion: "0", + FieldPath: "", + }, + Status: "tested", + Reason: "forTesting", + } + eventC := &api.Event{ + InvolvedObject: api.ObjectReference{ + Kind: "Pod", + Name: "baz", + UID: "yet another long uid string", + APIVersion: testapi.Version(), + ResourceVersion: "0", + FieldPath: "", + }, + Status: "untested", + Reason: "forTesting", + } + reg.ObjectList = &api.EventList{ + Items: []api.Event{*eventA, *eventB, *eventC}, + } + got, err := rest.List(api.NewContext(), labels.Everything(), labels.Set{"Status": "tested"}.AsSelector()) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + expect := &api.EventList{ + Items: []api.Event{*eventA, *eventB}, + } + if e, a := expect, got; !reflect.DeepEqual(e, a) { + t.Errorf("diff: %s", runtime.ObjectDiff(e, a)) + } +} + +func TestRESTWatch(t *testing.T) { + eventA := &api.Event{ + InvolvedObject: api.ObjectReference{ + Kind: "Pod", + Name: "foo", + UID: "long uid string", + APIVersion: testapi.Version(), + ResourceVersion: "0", + FieldPath: "", + }, + Status: "tested", + Reason: "forTesting", + } + reg, rest := NewTestREST() + wi, err := rest.Watch(api.NewContext(), labels.Everything(), labels.Everything(), 0) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + go func() { + reg.Mux.Action(watch.Added, eventA) + }() + got := <-wi.ResultChan() + if e, a := eventA, got.Object; !reflect.DeepEqual(e, a) { + t.Errorf("diff: %s", runtime.ObjectDiff(e, a)) + } +}