diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index 96373711b5d..8d589f1e6f4 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -28,7 +28,7 @@ import ( // RESTStorage is a generic interface for RESTful storage services type RESTStorage interface { - List(*url.URL) (interface{}, error) + List(LabelQuery) (interface{}, error) Get(id string) (interface{}, error) Delete(id string) error Extract(body string) (interface{}, error) @@ -141,28 +141,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 := ParseLabelQuery(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) } diff --git a/pkg/apiserver/apiserver_test.go b/pkg/apiserver/apiserver_test.go index 1b80d297f10..cc53ec101ad 100644 --- a/pkg/apiserver/apiserver_test.go +++ b/pkg/apiserver/apiserver_test.go @@ -22,7 +22,6 @@ import ( "io/ioutil" "net/http" "net/http/httptest" - "net/url" "reflect" "testing" ) @@ -50,7 +49,7 @@ type SimpleRESTStorage struct { updated Simple } -func (storage *SimpleRESTStorage) List(*url.URL) (interface{}, error) { +func (storage *SimpleRESTStorage) List(LabelQuery) (interface{}, error) { result := SimpleList{ Items: storage.list, } diff --git a/pkg/apiserver/labelquery.go b/pkg/apiserver/labelquery.go new file mode 100644 index 00000000000..1c7d174bf24 --- /dev/null +++ b/pkg/apiserver/labelquery.go @@ -0,0 +1,221 @@ +/* +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 apiserver + +import ( + "fmt" + "strings" +) + +// Labels allows you to present labels. +type Labels interface { + Get(label string) (value string) +} + +// A map of label:value. +type LabelSet map[string]string + +func (ls LabelSet) String() string { + query := make([]string, 0, len(ls)) + for key, value := range ls { + query = append(query, key+"="+value) + } + return strings.Join(query, ",") +} + +func (ls LabelSet) Get(label string) string { + return ls[label] +} + +// Represents a query. +type LabelQuery 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 +} + +// A single term of a label query. +type labelQueryTerm struct { + // Not inverts the meaning of the items in this term. + not bool + + // Exactly one of the below three items should be used. + + // If non-nil, we match LabelSet l iff l[*label] == *value. + label, value *string + + // A list of terms which must all match for this query term to return true. + and []labelQueryTerm + + // A list of terms, any one of which will cause this query term to return true. + // Parsing/printing not implemented. + or []labelQueryTerm +} + +func (l *labelQueryTerm) Matches(ls Labels) bool { + matches := !l.not + switch { + case l.label != nil && l.value != nil: + if ls.Get(*l.label) == *l.value { + return matches + } + return !matches + case len(l.and) > 0: + for i := range l.and { + if !l.and[i].Matches(ls) { + return !matches + } + } + return matches + case len(l.or) > 0: + for i := range l.or { + if l.or[i].Matches(ls) { + return matches + } + } + return !matches + } + + // Empty queries match everything + return matches +} + +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 +} + +// Takes a string repsenting a label query and returns an object suitable for matching, or an error. +func ParseLabelQuery(query string) (LabelQuery, error) { + parts := strings.Split(query, ",") + var items []labelQueryTerm + for _, part := range parts { + if part == "" { + continue + } + if lhs, rhs, ok := try(part, "!="); ok { + items = append(items, labelQueryTerm{not: true, label: &lhs, value: &rhs}) + } else if lhs, rhs, ok := try(part, "=="); ok { + items = append(items, labelQueryTerm{label: &lhs, value: &rhs}) + } else if lhs, rhs, ok := try(part, "="); ok { + items = append(items, labelQueryTerm{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 &labelQueryTerm{and: items}, nil +} + +// Returns this query as a string in a form that ParseLabelQuery can parse. +func (l *labelQueryTerm) String() (out string) { + if len(l.and) > 0 { + for _, part := range l.and { + if out != "" { + out += "," + } + out += part.String() + } + return + } else if l.label != nil && l.value != nil { + op := "=" + if l.not { + op = "!=" + } + return fmt.Sprintf("%v%v%v", *l.label, op, *l.value) + } + return "" +} + +/* +type parseErr struct { + Reason string + Pos token.Pos +} + +func (p parseErr) Error() string { + return fmt.Sprintf("%v: %v", p.Reason, p.Pos) +} + +func fromLiteral(expr *ast.BinaryExpr) (*labelQueryTerm, error) { + lhs, ok := expr.X.(*ast.Ident) + if !ok { + return nil, parseErr{"expected literal", expr.X.Pos()} + } + +} + +func fromBinaryExpr(expr *ast.BinaryExpr) (*labelQueryTerm, error) { + switch expr.Op { + case token.EQL, token.NEQ: + return fromLiteral(expr) + } + lhs, err := fromExpr(expr.X) + if err != nil { + return nil, err + } + rhs, err := fromExpr(expr.Y) + if err != nil { + return nil, err + } + switch expr.Op { + case token.AND, token.LAND: + return &labelQueryTerm{And: []LabelQuery{lhs, rhs}} + case token.OR, token.LOR: + return &labelQueryTerm{Or: []LabelQuery{lhs, rhs}} + } +} + +func fromUnaryExpr(expr *ast.UnaryExpr) (*labelQueryTerm, error) { + if expr.Op == token.NOT { + lqt, err := fromExpr(expr.X) + if err != nil { + return nil, err + } + lqt.not = !lqt.not + return lqt, nil + } + return nil, parseErr{"unrecognized unary expression", expr.OpPos} +} + +func fromExpr(expr ast.Expr) (*labelQueryTerm, error) { + switch v := expr.(type) { + case *ast.UnaryExpr: + return fromUnaryExpr(v) + case *ast.BinaryExpr: + return fromBinaryExpr(v) + } + return nil, parseErr{"unrecognized expression type", expr.Pos()} +} + +// Takes a string repsenting a label query and returns an object suitable for matching, or an error. +func ParseLabelQuery(query string) (LabelQuery, error) { + expr, err := parser.ParseExpr(query) + if err != nil { + return nil, err + } + log.Printf("%v: %v (%#v)", query, expr, expr) + return fromExpr(expr) +} +*/ diff --git a/pkg/apiserver/labelquery_test.go b/pkg/apiserver/labelquery_test.go new file mode 100644 index 00000000000..acc3964ee78 --- /dev/null +++ b/pkg/apiserver/labelquery_test.go @@ -0,0 +1,80 @@ +/* +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 apiserver + +import ( + "testing" +) + +func TestLabelQueryParse(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 := ParseLabelQuery(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 := ParseLabelQuery(test) + if err == nil { + t.Errorf("%v: did not get expected error\n", test) + } + } +} + +func shouldMatch(t *testing.T, query string, ls LabelSet) { + lq, err := ParseLabelQuery(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 shouldNotMatch(t *testing.T, query string, ls LabelSet) { + lq, err := ParseLabelQuery(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 TestSimpleLabel(t *testing.T) { + shouldMatch(t, "", LabelSet{"x": "y"}) + shouldMatch(t, "x=y", LabelSet{"x": "y"}) + shouldMatch(t, "x=y,z=w", LabelSet{"x": "y", "z": "w"}) + shouldMatch(t, "x!=y,z!=w", LabelSet{"x": "z", "z": "a"}) + shouldNotMatch(t, "x=y", LabelSet{"x": "z"}) + shouldNotMatch(t, "x=y,z=w", LabelSet{"x": "w", "z": "w"}) + shouldNotMatch(t, "x!=y,z!=w", LabelSet{"x": "z", "z": "w"}) +} 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{