diff --git a/hack/integration-test.sh b/hack/integration-test.sh index cc0b79f8485..482b15cc5fc 100755 --- a/hack/integration-test.sh +++ b/hack/integration-test.sh @@ -27,7 +27,7 @@ $(dirname $0)/build-go.sh ETCD_DIR=$(mktemp -d -t kube-integration.XXXXXX) trap "rm -rf ${ETCD_DIR}" EXIT -etcd -name test -data-dir ${ETCD_DIR} > /tmp/etcd.log & +(etcd -name test -data-dir ${ETCD_DIR} > /tmp/etcd.log) & ETCD_PID=$! sleep 5 diff --git a/hack/local-up-cluster.sh b/hack/local-up-cluster.sh index 36b1c44e6ec..95f9406aa2b 100755 --- a/hack/local-up-cluster.sh +++ b/hack/local-up-cluster.sh @@ -33,7 +33,7 @@ echo "Starting etcd" ETCD_DIR=$(mktemp -d -t kube-integration.XXXXXX) trap "rm -rf ${ETCD_DIR}" EXIT -etcd -name test -data-dir ${ETCD_DIR} &> /tmp/etcd.log & +(etcd -name test -data-dir ${ETCD_DIR} &> /tmp/etcd.log) & ETCD_PID=$! sleep 5 diff --git a/hack/local-up.sh b/hack/local-up.sh index 1308420ee7e..ff1ee378772 100755 --- a/hack/local-up.sh +++ b/hack/local-up.sh @@ -40,7 +40,7 @@ echo "Starting etcd" ETCD_DIR=$(mktemp -d -t kube-integration.XXXXXX) trap "rm -rf ${ETCD_DIR}" EXIT -etcd -name test -data-dir ${ETCD_DIR} > /tmp/etcd.log & +(etcd -name test -data-dir ${ETCD_DIR} > /tmp/etcd.log) & ETCD_PID=$! sleep 5 diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index 8da4c6e7be9..353a1844973 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -24,11 +24,13 @@ import ( "net/http" "net/url" "strings" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" ) // RESTStorage is a generic interface for RESTful storage services type RESTStorage interface { - List(*url.URL) (interface{}, error) + List(labels.Query) (interface{}, error) Get(id string) (interface{}, error) Delete(id string) error Extract(body string) (interface{}, error) @@ -38,7 +40,7 @@ type RESTStorage interface { // Status is a return value for calls that don't return other objects type Status struct { - success bool + Success bool } // ApiServer is an HTTPHandler that delegates to RESTStorage objects. @@ -141,28 +143,33 @@ func (server *ApiServer) readBody(req *http.Request) (string, error) { // PUT /foo/bar update 'bar' // DELETE /foo/bar delete 'bar' // Returns 404 if the method/pattern doesn't match one of these entries -func (server *ApiServer) handleREST(parts []string, url *url.URL, req *http.Request, w http.ResponseWriter, storage RESTStorage) { +func (server *ApiServer) handleREST(parts []string, requestUrl *url.URL, req *http.Request, w http.ResponseWriter, storage RESTStorage) { switch req.Method { case "GET": switch len(parts) { case 1: - controllers, err := storage.List(url) + query, err := labels.ParseQuery(requestUrl.Query().Get("labels")) + if err != nil { + server.error(err, w) + return + } + controllers, err := storage.List(query) if err != nil { server.error(err, w) return } server.write(200, controllers, w) case 2: - pod, err := storage.Get(parts[1]) + item, err := storage.Get(parts[1]) if err != nil { server.error(err, w) return } - if pod == nil { + if item == nil { server.notFound(req, w) return } - server.write(200, pod, w) + server.write(200, item, w) default: server.notFound(req, w) } @@ -195,7 +202,7 @@ func (server *ApiServer) handleREST(parts []string, url *url.URL, req *http.Requ server.error(err, w) return } - server.write(200, Status{success: true}, w) + server.write(200, Status{Success: true}, w) return case "PUT": if len(parts) != 2 { diff --git a/pkg/apiserver/apiserver_test.go b/pkg/apiserver/apiserver_test.go index 1b80d297f10..41197d5b7e3 100644 --- a/pkg/apiserver/apiserver_test.go +++ b/pkg/apiserver/apiserver_test.go @@ -22,9 +22,10 @@ import ( "io/ioutil" "net/http" "net/http/httptest" - "net/url" "reflect" "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" ) // TODO: This doesn't reduce typing enough to make it worth the less readable errors. Remove. @@ -50,7 +51,7 @@ type SimpleRESTStorage struct { updated Simple } -func (storage *SimpleRESTStorage) List(*url.URL) (interface{}, error) { +func (storage *SimpleRESTStorage) List(labels.Query) (interface{}, error) { result := SimpleList{ Items: storage.list, } diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go index eb0f619c999..9cd7cf404f6 100644 --- a/pkg/client/client_test.go +++ b/pkg/client/client_test.go @@ -245,30 +245,6 @@ func expectEqual(t *testing.T, expected, observed interface{}) { } } -func TestEncodeDecodeLabelQuery(t *testing.T) { - queryIn := map[string]string{ - "foo": "bar", - "baz": "blah", - } - queryString, _ := url.QueryUnescape(EncodeLabelQuery(queryIn)) - queryOut := DecodeLabelQuery(queryString) - expectEqual(t, queryIn, queryOut) -} - -func TestDecodeEmpty(t *testing.T) { - query := DecodeLabelQuery("") - if len(query) != 0 { - t.Errorf("Unexpected query: %#v", query) - } -} - -func TestDecodeBad(t *testing.T) { - query := DecodeLabelQuery("foo") - if len(query) != 0 { - t.Errorf("Unexpected query: %#v", query) - } -} - func TestGetController(t *testing.T) { expectedController := api.ReplicationController{ JSONBase: api.JSONBase{ diff --git a/pkg/labels/doc.go b/pkg/labels/doc.go new file mode 100644 index 00000000000..54318e41bb8 --- /dev/null +++ b/pkg/labels/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 labels implements a simple label system, parsing and matching queries +// with sets of labels. +package labels diff --git a/pkg/labels/labels.go b/pkg/labels/labels.go new file mode 100644 index 00000000000..156e18561c3 --- /dev/null +++ b/pkg/labels/labels.go @@ -0,0 +1,47 @@ +/* +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 labels + +import ( + "sort" + "strings" +) + +// Labels allows you to present labels independently from their storage. +type Labels interface { + Get(label string) (value string) +} + +// A map of label:value. Implements Labels. +type Set map[string]string + +// All labels listed as a human readable string. Conveniently, exactly the format +// that ParseQuery takes. +func (ls Set) String() string { + query := make([]string, 0, len(ls)) + for key, value := range ls { + query = append(query, key+"="+value) + } + // Sort for determinism. + sort.StringSlice(query).Sort() + return strings.Join(query, ",") +} + +// Implement Labels interface. +func (ls Set) Get(label string) string { + return ls[label] +} diff --git a/pkg/labels/labels_test.go b/pkg/labels/labels_test.go new file mode 100644 index 00000000000..c2df41703c3 --- /dev/null +++ b/pkg/labels/labels_test.go @@ -0,0 +1,43 @@ +/* +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 labels + +import ( + "testing" +) + +func matches(t *testing.T, ls Set, want string) { + if ls.String() != want { + t.Errorf("Expected '%s', but got '%s'", want, ls.String()) + } +} + +func TestSetString(t *testing.T) { + matches(t, Set{"x": "y"}, "x=y") + matches(t, Set{"foo": "bar"}, "foo=bar") + matches(t, Set{"foo": "bar", "baz": "qup"}, "baz=qup,foo=bar") + + // TODO: Make our label representation robust enough to handle labels + // with ",=!" characters in their names. +} + +func TestLabelGet(t *testing.T) { + ls := Set{"x": "y"} + if ls.Get("x") != "y" { + t.Errorf("Set.Get is broken") + } +} diff --git a/pkg/labels/query.go b/pkg/labels/query.go new file mode 100644 index 00000000000..5e563924a3c --- /dev/null +++ b/pkg/labels/query.go @@ -0,0 +1,123 @@ +/* +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 labels + +import ( + "fmt" + "strings" +) + +// Represents a label query. +type Query interface { + // Returns true if this query matches the given set of labels. + Matches(Labels) bool + + // Prints a human readable version of this label query. + String() string +} + +// Everything returns a query that matches all labels. +func Everything() Query { + return andTerm{} +} + +type hasTerm struct { + label, value string +} + +func (t *hasTerm) Matches(ls Labels) bool { + return ls.Get(t.label) == t.value +} + +func (t *hasTerm) String() string { + return fmt.Sprintf("%v=%v", t.label, t.value) +} + +type notHasTerm struct { + label, value string +} + +func (t *notHasTerm) Matches(ls Labels) bool { + return ls.Get(t.label) != t.value +} + +func (t *notHasTerm) String() string { + return fmt.Sprintf("%v!=%v", t.label, t.value) +} + +type andTerm []Query + +func (t andTerm) Matches(ls Labels) bool { + for _, q := range t { + if !q.Matches(ls) { + return false + } + } + return true +} + +func (t andTerm) String() string { + var terms []string + for _, q := range t { + terms = append(terms, q.String()) + } + return strings.Join(terms, ",") +} + +func try(queryPiece, op string) (lhs, rhs string, ok bool) { + pieces := strings.Split(queryPiece, op) + if len(pieces) == 2 { + return pieces[0], pieces[1], true + } + return "", "", false +} + +// Given a Set, return a Query which will match exactly that Set. +func QueryFromSet(ls Set) Query { + var items []Query + for label, value := range ls { + items = append(items, &hasTerm{label: label, value: value}) + } + if len(items) == 1 { + return items[0] + } + return andTerm(items) +} + +// Takes a string repsenting a label query and returns an object suitable for matching, or an error. +func ParseQuery(query string) (Query, error) { + parts := strings.Split(query, ",") + var items []Query + for _, part := range parts { + if part == "" { + continue + } + if lhs, rhs, ok := try(part, "!="); ok { + items = append(items, ¬HasTerm{label: lhs, value: rhs}) + } else if lhs, rhs, ok := try(part, "=="); ok { + items = append(items, &hasTerm{label: lhs, value: rhs}) + } else if lhs, rhs, ok := try(part, "="); ok { + items = append(items, &hasTerm{label: lhs, value: rhs}) + } else { + return nil, fmt.Errorf("invalid label query: '%s'; can't understand '%s'", query, part) + } + } + if len(items) == 1 { + return items[0], nil + } + return andTerm(items), nil +} diff --git a/pkg/labels/query_test.go b/pkg/labels/query_test.go new file mode 100644 index 00000000000..38dae8b4896 --- /dev/null +++ b/pkg/labels/query_test.go @@ -0,0 +1,123 @@ +/* +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 labels + +import ( + "testing" +) + +func TestQueryParse(t *testing.T) { + testGoodStrings := []string{ + "x=a,y=b,z=c", + "", + "x!=a,y=b", + } + testBadStrings := []string{ + "x=a||y=b", + "x==a==b", + } + for _, test := range testGoodStrings { + lq, err := ParseQuery(test) + if err != nil { + t.Errorf("%v: error %v (%#v)\n", test, err, err) + } + if test != lq.String() { + t.Errorf("%v restring gave: %v\n", test, lq.String()) + } + } + for _, test := range testBadStrings { + _, err := ParseQuery(test) + if err == nil { + t.Errorf("%v: did not get expected error\n", test) + } + } +} + +func expectMatch(t *testing.T, query string, ls Set) { + lq, err := ParseQuery(query) + if err != nil { + t.Errorf("Unable to parse %v as a query\n", query) + return + } + if !lq.Matches(ls) { + t.Errorf("Wanted %s to match '%s', but it did not.\n", query, ls) + } +} + +func expectNoMatch(t *testing.T, query string, ls Set) { + lq, err := ParseQuery(query) + if err != nil { + t.Errorf("Unable to parse %v as a query\n", query) + return + } + if lq.Matches(ls) { + t.Errorf("Wanted '%s' to not match '%s', but it did.", query, ls) + } +} + +func TestEverything(t *testing.T) { + if !Everything().Matches(Set{"x": "y"}) { + t.Errorf("Nil query didn't match") + } +} + +func TestLabelQueryMatches(t *testing.T) { + expectMatch(t, "", Set{"x": "y"}) + expectMatch(t, "x=y", Set{"x": "y"}) + expectMatch(t, "x=y,z=w", Set{"x": "y", "z": "w"}) + expectMatch(t, "x!=y,z!=w", Set{"x": "z", "z": "a"}) + expectNoMatch(t, "x=y", Set{"x": "z"}) + expectNoMatch(t, "x=y,z=w", Set{"x": "w", "z": "w"}) + expectNoMatch(t, "x!=y,z!=w", Set{"x": "z", "z": "w"}) + + labelset := Set{ + "foo": "bar", + "baz": "blah", + } + expectMatch(t, "foo=bar", labelset) + expectMatch(t, "baz=blah", labelset) + expectMatch(t, "foo=bar,baz=blah", labelset) + expectNoMatch(t, "foo=blah", labelset) + expectNoMatch(t, "baz=bar", labelset) + expectNoMatch(t, "foo=bar,foobar=bar,baz=blah", labelset) +} + +func expectMatchDirect(t *testing.T, query, ls Set) { + if !QueryFromSet(query).Matches(ls) { + t.Errorf("Wanted %s to match '%s', but it did not.\n", query, ls) + } +} + +func expectNoMatchDirect(t *testing.T, query, ls Set) { + if QueryFromSet(query).Matches(ls) { + t.Errorf("Wanted '%s' to not match '%s', but it did.", query, ls) + } +} + +func TestSetMatches(t *testing.T) { + labelset := Set{ + "foo": "bar", + "baz": "blah", + } + expectMatchDirect(t, Set{}, labelset) + expectMatchDirect(t, Set{"foo": "bar"}, labelset) + expectMatchDirect(t, Set{"baz": "blah"}, labelset) + expectMatchDirect(t, Set{"foo": "bar", "baz": "blah"}, labelset) + expectNoMatchDirect(t, Set{"foo": "=blah"}, labelset) + expectNoMatchDirect(t, Set{"baz": "=bar"}, labelset) + expectNoMatchDirect(t, Set{"foo": "=bar", "foobar": "bar", "baz": "blah"}, labelset) +} diff --git a/pkg/registry/controller_registry.go b/pkg/registry/controller_registry.go index 13f22a44857..1824ba965a2 100644 --- a/pkg/registry/controller_registry.go +++ b/pkg/registry/controller_registry.go @@ -18,10 +18,10 @@ package registry import ( "encoding/json" - "net/url" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" ) // Implementation of RESTStorage for the api server. @@ -35,13 +35,14 @@ func MakeControllerRegistryStorage(registry ControllerRegistry) apiserver.RESTSt } } -func (storage *ControllerRegistryStorage) List(*url.URL) (interface{}, error) { +func (storage *ControllerRegistryStorage) List(query labels.Query) (interface{}, error) { result := api.ReplicationControllerList{JSONBase: api.JSONBase{Kind: "cluster#replicationControllerList"}} controllers, err := storage.registry.ListControllers() if err == nil { - result = api.ReplicationControllerList{ - JSONBase: api.JSONBase{Kind: "cluster#replicationControllerList"}, - Items: controllers, + for _, controller := range controllers { + if query.Matches(labels.Set(controller.Labels)) { + result.Items = append(result.Items, controller) + } } } return result, err diff --git a/pkg/registry/controller_registry_test.go b/pkg/registry/controller_registry_test.go index b3d9d7ce5ec..14eac7e4986 100644 --- a/pkg/registry/controller_registry_test.go +++ b/pkg/registry/controller_registry_test.go @@ -23,6 +23,7 @@ import ( "testing" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" ) type MockControllerRegistry struct { @@ -71,7 +72,7 @@ func TestListEmptyControllerList(t *testing.T) { storage := ControllerRegistryStorage{ registry: &mockRegistry, } - controllers, err := storage.List(nil) + controllers, err := storage.List(labels.Everything()) expectNoError(t, err) if len(controllers.(api.ReplicationControllerList).Items) != 0 { t.Errorf("Unexpected non-zero ctrl list: %#v", controllers) @@ -96,7 +97,7 @@ func TestListControllerList(t *testing.T) { storage := ControllerRegistryStorage{ registry: &mockRegistry, } - controllersObj, err := storage.List(nil) + controllersObj, err := storage.List(labels.Everything()) controllers := controllersObj.(api.ReplicationControllerList) expectNoError(t, err) if len(controllers.Items) != 2 { diff --git a/pkg/registry/endpoints.go b/pkg/registry/endpoints.go index 46ce7f304be..5601db2f136 100644 --- a/pkg/registry/endpoints.go +++ b/pkg/registry/endpoints.go @@ -20,6 +20,7 @@ import ( "log" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" ) func MakeEndpointController(serviceRegistry ServiceRegistry, podRegistry PodRegistry) *EndpointController { @@ -41,7 +42,7 @@ func (e *EndpointController) SyncServiceEndpoints() error { } var resultErr error for _, service := range services.Items { - pods, err := e.podRegistry.ListPods(&service.Labels) + pods, err := e.podRegistry.ListPods(labels.QueryFromSet(labels.Set(service.Labels))) if err != nil { log.Printf("Error syncing service: %#v, skipping.", service) resultErr = err diff --git a/pkg/registry/etcd_registry.go b/pkg/registry/etcd_registry.go index b1c212c55d9..0f8062c96b5 100644 --- a/pkg/registry/etcd_registry.go +++ b/pkg/registry/etcd_registry.go @@ -23,6 +23,7 @@ import ( "github.com/coreos/go-etcd/etcd" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" ) // TODO: Need to add a reconciler loop that makes sure that things in pods are reflected into @@ -66,7 +67,7 @@ func makePodKey(machine, podID string) string { return "/registry/hosts/" + machine + "/pods/" + podID } -func (registry *EtcdRegistry) ListPods(query *map[string]string) ([]api.Pod, error) { +func (registry *EtcdRegistry) ListPods(query labels.Query) ([]api.Pod, error) { pods := []api.Pod{} for _, machine := range registry.machines { machinePods, err := registry.listPodsForMachine(machine) @@ -74,7 +75,7 @@ func (registry *EtcdRegistry) ListPods(query *map[string]string) ([]api.Pod, err return pods, err } for _, pod := range machinePods { - if LabelsMatch(pod, query) { + if query.Matches(labels.Set(pod.Labels)) { pods = append(pods, pod) } } diff --git a/pkg/registry/etcd_registry_test.go b/pkg/registry/etcd_registry_test.go index d117ea018ef..a9872b888fd 100644 --- a/pkg/registry/etcd_registry_test.go +++ b/pkg/registry/etcd_registry_test.go @@ -21,6 +21,7 @@ import ( "testing" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/coreos/go-etcd/etcd" ) @@ -307,7 +308,7 @@ func TestEtcdEmptyListPods(t *testing.T) { E: nil, } registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"}) - pods, err := registry.ListPods(nil) + pods, err := registry.ListPods(labels.Everything()) expectNoError(t, err) if len(pods) != 0 { t.Errorf("Unexpected pod list: %#v", pods) @@ -322,7 +323,7 @@ func TestEtcdListPodsNotFound(t *testing.T) { E: &etcd.EtcdError{ErrorCode: 100}, } registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"}) - pods, err := registry.ListPods(nil) + pods, err := registry.ListPods(labels.Everything()) expectNoError(t, err) if len(pods) != 0 { t.Errorf("Unexpected pod list: %#v", pods) @@ -348,7 +349,7 @@ func TestEtcdListPods(t *testing.T) { E: nil, } registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"}) - pods, err := registry.ListPods(nil) + pods, err := registry.ListPods(labels.Everything()) expectNoError(t, err) if len(pods) != 2 || pods[0].ID != "foo" || pods[1].ID != "bar" { t.Errorf("Unexpected pod list: %#v", pods) diff --git a/pkg/registry/interfaces.go b/pkg/registry/interfaces.go index db27abde8da..bf0e2728e97 100644 --- a/pkg/registry/interfaces.go +++ b/pkg/registry/interfaces.go @@ -17,13 +17,13 @@ package registry import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" ) -// PodRegistry is an interface implemented by things that know how to store Pod objects +// PodRegistry is an interface implemented by things that know how to store Pod objects. type PodRegistry interface { // ListPods obtains a list of pods that match query. - // Query may be nil in which case all pods are returned. - ListPods(query *map[string]string) ([]api.Pod, error) + ListPods(query labels.Query) ([]api.Pod, error) // Get a specific pod GetPod(podID string) (*api.Pod, error) // Create a pod based on a specification, schedule it onto a specific machine. @@ -34,7 +34,7 @@ type PodRegistry interface { DeletePod(podID string) error } -// ControllerRegistry is an interface for things that know how to store Controllers +// ControllerRegistry is an interface for things that know how to store Controllers. type ControllerRegistry interface { ListControllers() ([]api.ReplicationController, error) GetController(controllerId string) (*api.ReplicationController, error) @@ -42,3 +42,13 @@ type ControllerRegistry interface { UpdateController(controller api.ReplicationController) error DeleteController(controllerId string) error } + +// ServiceRegistry is an interface for things that know how to store services. +type ServiceRegistry interface { + ListServices() (api.ServiceList, error) + CreateService(svc api.Service) error + GetService(name string) (*api.Service, error) + DeleteService(name string) error + UpdateService(svc api.Service) error + UpdateEndpoints(e api.Endpoints) error +} diff --git a/pkg/registry/memory_registry.go b/pkg/registry/memory_registry.go index 894a3854251..a2175525914 100644 --- a/pkg/registry/memory_registry.go +++ b/pkg/registry/memory_registry.go @@ -17,6 +17,7 @@ package registry import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" ) // An implementation of PodRegistry and ControllerRegistry that is backed by memory @@ -35,10 +36,10 @@ func MakeMemoryRegistry() *MemoryRegistry { } } -func (registry *MemoryRegistry) ListPods(labelQuery *map[string]string) ([]api.Pod, error) { +func (registry *MemoryRegistry) ListPods(query labels.Query) ([]api.Pod, error) { result := []api.Pod{} for _, value := range registry.podData { - if LabelsMatch(value, labelQuery) { + if query.Matches(labels.Set(value.Labels)) { result = append(result, value) } } diff --git a/pkg/registry/memory_registry_test.go b/pkg/registry/memory_registry_test.go index 3de7902c60f..b9adb26d9c8 100644 --- a/pkg/registry/memory_registry_test.go +++ b/pkg/registry/memory_registry_test.go @@ -19,11 +19,12 @@ import ( "testing" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" ) func TestListPodsEmpty(t *testing.T) { registry := MakeMemoryRegistry() - pods, err := registry.ListPods(nil) + pods, err := registry.ListPods(labels.Everything()) expectNoError(t, err) if len(pods) != 0 { t.Errorf("Unexpected pod list: %#v", pods) @@ -33,7 +34,7 @@ func TestListPodsEmpty(t *testing.T) { func TestMemoryListPods(t *testing.T) { registry := MakeMemoryRegistry() registry.CreatePod("machine", api.Pod{JSONBase: api.JSONBase{ID: "foo"}}) - pods, err := registry.ListPods(nil) + pods, err := registry.ListPods(labels.Everything()) expectNoError(t, err) if len(pods) != 1 || pods[0].ID != "foo" { t.Errorf("Unexpected pod list: %#v", pods) diff --git a/pkg/registry/pod_registry.go b/pkg/registry/pod_registry.go index 95b150f99aa..c57a7c7f725 100644 --- a/pkg/registry/pod_registry.go +++ b/pkg/registry/pod_registry.go @@ -18,11 +18,11 @@ package registry import ( "encoding/json" "fmt" - "net/url" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver" "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" ) // PodRegistryStorage implements the RESTStorage interface in terms of a PodRegistry @@ -40,41 +40,11 @@ func MakePodRegistryStorage(registry PodRegistry, containerInfo client.Container } } -// LabelMatch tests to see if a Pod's labels map contains 'key' mapping to 'value' -func LabelMatch(pod api.Pod, queryKey, queryValue string) bool { - for key, value := range pod.Labels { - if queryKey == key && queryValue == value { - return true - } - } - return false -} - -// LabelMatch tests to see if a Pod's labels map contains all key/value pairs in 'labelQuery' -func LabelsMatch(pod api.Pod, labelQuery *map[string]string) bool { - if labelQuery == nil { - return true - } - for key, value := range *labelQuery { - if !LabelMatch(pod, key, value) { - return false - } - } - return true -} - -func (storage *PodRegistryStorage) List(url *url.URL) (interface{}, error) { +func (storage *PodRegistryStorage) List(query labels.Query) (interface{}, error) { var result api.PodList - var query *map[string]string - if url != nil { - queryMap := client.DecodeLabelQuery(url.Query().Get("labels")) - query = &queryMap - } pods, err := storage.registry.ListPods(query) if err == nil { - result = api.PodList{ - Items: pods, - } + result.Items = pods } result.Kind = "cluster#podList" return result, err diff --git a/pkg/registry/pod_registry_test.go b/pkg/registry/pod_registry_test.go index 217192e4fc9..3e6bda6208f 100644 --- a/pkg/registry/pod_registry_test.go +++ b/pkg/registry/pod_registry_test.go @@ -22,6 +22,7 @@ import ( "testing" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" ) type MockPodRegistry struct { @@ -35,7 +36,7 @@ func expectNoError(t *testing.T, err error) { } } -func (registry *MockPodRegistry) ListPods(*map[string]string) ([]api.Pod, error) { +func (registry *MockPodRegistry) ListPods(labels.Query) ([]api.Pod, error) { return registry.pods, registry.err } @@ -135,74 +136,6 @@ func TestExtractJson(t *testing.T) { } } -func expectLabelMatch(t *testing.T, pod api.Pod, key, value string) { - if !LabelMatch(pod, key, value) { - t.Errorf("Unexpected match failure: %#v %s %s", pod, key, value) - } -} - -func expectNoLabelMatch(t *testing.T, pod api.Pod, key, value string) { - if LabelMatch(pod, key, value) { - t.Errorf("Unexpected match success: %#v %s %s", pod, key, value) - } -} - -func expectLabelsMatch(t *testing.T, pod api.Pod, query *map[string]string) { - if !LabelsMatch(pod, query) { - t.Errorf("Unexpected match failure: %#v %#v", pod, *query) - } -} - -func expectNoLabelsMatch(t *testing.T, pod api.Pod, query *map[string]string) { - if LabelsMatch(pod, query) { - t.Errorf("Unexpected match success: %#v %#v", pod, *query) - } -} - -func TestLabelMatch(t *testing.T) { - pod := api.Pod{ - Labels: map[string]string{ - "foo": "bar", - "baz": "blah", - }, - } - expectLabelMatch(t, pod, "foo", "bar") - expectLabelMatch(t, pod, "baz", "blah") - expectNoLabelMatch(t, pod, "foo", "blah") - expectNoLabelMatch(t, pod, "baz", "bar") -} - -func TestLabelsMatch(t *testing.T) { - pod := api.Pod{ - Labels: map[string]string{ - "foo": "bar", - "baz": "blah", - }, - } - expectLabelsMatch(t, pod, &map[string]string{}) - expectLabelsMatch(t, pod, &map[string]string{ - "foo": "bar", - }) - expectLabelsMatch(t, pod, &map[string]string{ - "baz": "blah", - }) - expectLabelsMatch(t, pod, &map[string]string{ - "foo": "bar", - "baz": "blah", - }) - expectNoLabelsMatch(t, pod, &map[string]string{ - "foo": "blah", - }) - expectNoLabelsMatch(t, pod, &map[string]string{ - "baz": "bar", - }) - expectNoLabelsMatch(t, pod, &map[string]string{ - "foo": "bar", - "foobar": "bar", - "baz": "blah", - }) -} - func TestMakePodStatus(t *testing.T) { status := makePodStatus(map[string]interface{}{}) if status != "Pending" { diff --git a/pkg/registry/service_registry.go b/pkg/registry/service_registry.go index 218cc0eb4cc..aaf5c4f68a5 100644 --- a/pkg/registry/service_registry.go +++ b/pkg/registry/service_registry.go @@ -17,23 +17,14 @@ package registry import ( "encoding/json" - "net/url" "strconv" "strings" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" ) -type ServiceRegistry interface { - ListServices() (api.ServiceList, error) - CreateService(svc api.Service) error - GetService(name string) (*api.Service, error) - DeleteService(name string) error - UpdateService(svc api.Service) error - UpdateEndpoints(e api.Endpoints) error -} - type ServiceRegistryStorage struct { registry ServiceRegistry } @@ -59,12 +50,19 @@ func GetServiceEnvironmentVariables(registry ServiceRegistry, machine string) ([ return result, nil } -func (sr *ServiceRegistryStorage) List(*url.URL) (interface{}, error) { +func (sr *ServiceRegistryStorage) List(query labels.Query) (interface{}, error) { list, err := sr.registry.ListServices() if err != nil { return nil, err } list.Kind = "cluster#serviceList" + var filtered []api.Service + for _, service := range list.Items { + if query.Matches(labels.Set(service.Labels)) { + filtered = append(filtered, service) + } + } + list.Items = filtered return list, err }