From d75a3d5021411b6c997cccc9ead3711ab4ee59c8 Mon Sep 17 00:00:00 2001 From: Clayton Coleman Date: Sat, 27 Dec 2014 16:48:27 -0500 Subject: [PATCH 1/3] Move Resource functionality to its own package Create a unified Builder object for working with files, selectors, types, and items that makes it easier to get multi-object functionality. Supports all of the behaviors previously in resource.go, but with additional flexibility to allow multi-type retrieval and access, directories, URLs, nested objects, and lists. --- pkg/kubectl/cmd/helpers.go | 3 + pkg/kubectl/resource/builder.go | 563 ++++++++++++++++++++++++ pkg/kubectl/resource/builder_test.go | 619 +++++++++++++++++++++++++++ pkg/kubectl/resource/doc.go | 24 ++ pkg/kubectl/resource/helper.go | 172 ++++++++ pkg/kubectl/resource/helper_test.go | 463 ++++++++++++++++++++ pkg/kubectl/resource/interfaces.go | 44 ++ pkg/kubectl/resource/mapper.go | 97 +++++ pkg/kubectl/resource/selector.go | 80 ++++ pkg/kubectl/resource/visitor.go | 422 ++++++++++++++++++ 10 files changed, 2487 insertions(+) create mode 100644 pkg/kubectl/resource/builder.go create mode 100644 pkg/kubectl/resource/builder_test.go create mode 100644 pkg/kubectl/resource/doc.go create mode 100644 pkg/kubectl/resource/helper.go create mode 100644 pkg/kubectl/resource/helper_test.go create mode 100644 pkg/kubectl/resource/interfaces.go create mode 100644 pkg/kubectl/resource/mapper.go create mode 100644 pkg/kubectl/resource/selector.go create mode 100644 pkg/kubectl/resource/visitor.go diff --git a/pkg/kubectl/cmd/helpers.go b/pkg/kubectl/cmd/helpers.go index 4b42e5124c5..942c95e33e8 100644 --- a/pkg/kubectl/cmd/helpers.go +++ b/pkg/kubectl/cmd/helpers.go @@ -105,6 +105,7 @@ func FirstNonEmptyString(args ...string) string { } // Return a list of file names of a certain type within a given directory. +// TODO: replace with resource.Builder func GetFilesFromDir(directory string, fileType string) []string { files := []string{} @@ -121,6 +122,7 @@ func GetFilesFromDir(directory string, fileType string) []string { // ReadConfigData reads the bytes from the specified filesytem or network // location or from stdin if location == "-". +// TODO: replace with resource.Builder func ReadConfigData(location string) ([]byte, error) { if len(location) == 0 { return nil, fmt.Errorf("location given but empty") @@ -144,6 +146,7 @@ func ReadConfigData(location string) ([]byte, error) { return ReadConfigDataFromLocation(location) } +// TODO: replace with resource.Builder func ReadConfigDataFromLocation(location string) ([]byte, error) { // we look for http:// or https:// to determine if valid URL, otherwise do normal file IO if strings.Index(location, "http://") == 0 || strings.Index(location, "https://") == 0 { diff --git a/pkg/kubectl/resource/builder.go b/pkg/kubectl/resource/builder.go new file mode 100644 index 00000000000..87553d6d2dd --- /dev/null +++ b/pkg/kubectl/resource/builder.go @@ -0,0 +1,563 @@ +/* +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 resource + +import ( + "fmt" + "io" + "net/url" + "os" + "reflect" + "strings" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util/errors" + "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" +) + +// Builder provides convenience functions for taking arguments and parameters +// from the command line and converting them to a list of resources to iterate +// over using the Visitor interface. +type Builder struct { + mapper *Mapper + + errs []error + + paths []Visitor + stream bool + dir bool + + selector labels.Selector + + resources []string + + namespace string + name string + + defaultNamespace bool + requireNamespace bool + + flatten bool + latest bool + + singleResourceType bool + continueOnError bool +} + +// NewBuilder creates a builder that operates on generic objects. +func NewBuilder(mapper meta.RESTMapper, typer runtime.ObjectTyper, clientMapper ClientMapper) *Builder { + return &Builder{ + mapper: &Mapper{typer, mapper, clientMapper}, + } +} + +// Filename is parameters passed via a filename argument which may be URLs, the "-" argument indicating +// STDIN, or paths to files or directories. If ContinueOnError() is set prior to this method being called, +// objects on the path that are unrecognized will be ignored (but logged at V(2)). +func (b *Builder) FilenameParam(paths ...string) *Builder { + for _, s := range paths { + switch { + case s == "-": + b.Stdin() + case strings.Index(s, "http://") == 0 || strings.Index(s, "https://") == 0: + url, err := url.Parse(s) + if err != nil { + b.errs = append(b.errs, fmt.Errorf("the URL passed to filename %q is not valid: %v", s, err)) + continue + } + b.URL(url) + default: + b.Path(s) + } + } + return b +} + +// URL accepts a number of URLs directly. +func (b *Builder) URL(urls ...*url.URL) *Builder { + for _, u := range urls { + b.paths = append(b.paths, &URLVisitor{ + Mapper: b.mapper, + URL: u, + }) + } + return b +} + +// Stdin will read objects from the standard input. If ContinueOnError() is set +// prior to this method being called, objects in the stream that are unrecognized +// will be ignored (but logged at V(2)). +func (b *Builder) Stdin() *Builder { + return b.Stream(os.Stdin, "STDIN") +} + +// Stream will read objects from the provided reader, and if an error occurs will +// include the name string in the error message. If ContinueOnError() is set +// prior to this method being called, objects in the stream that are unrecognized +// will be ignored (but logged at V(2)). +func (b *Builder) Stream(r io.Reader, name string) *Builder { + b.stream = true + b.paths = append(b.paths, NewStreamVisitor(r, b.mapper, name, b.continueOnError)) + return b +} + +// Path is a set of filesystem paths that may be files containing one or more +// resources. If ContinueOnError() is set prior to this method being called, +// objects on the path that are unrecognized will be ignored (but logged at V(2)). +func (b *Builder) Path(paths ...string) *Builder { + for _, p := range paths { + i, err := os.Stat(p) + if os.IsNotExist(err) { + b.errs = append(b.errs, fmt.Errorf("the path %q does not exist", p)) + continue + } + if err != nil { + b.errs = append(b.errs, fmt.Errorf("the path %q cannot be accessed: %v", p, err)) + continue + } + var visitor Visitor + if i.IsDir() { + b.dir = true + visitor = &DirectoryVisitor{ + Mapper: b.mapper, + Path: p, + Extensions: []string{".json", ".yaml"}, + Recursive: false, + IgnoreErrors: b.continueOnError, + } + } else { + visitor = &PathVisitor{ + Mapper: b.mapper, + Path: p, + IgnoreErrors: b.continueOnError, + } + } + b.paths = append(b.paths, visitor) + } + return b +} + +// ResourceTypes is a list of types of resources to operate on, when listing objects on +// the server or retrieving objects that match a selector. +func (b *Builder) ResourceTypes(types ...string) *Builder { + b.resources = append(b.resources, types...) + return b +} + +// SelectorParam defines a selector that should be applied to the object types to load. +// This will not affect files loaded from disk or URL. If the parameter is empty it is +// a no-op - to select all resources invoke `b.Selector(labels.Everything)`. +func (b *Builder) SelectorParam(s string) *Builder { + selector, err := labels.ParseSelector(s) + if err != nil { + b.errs = append(b.errs, fmt.Errorf("the provided selector %q is not valid: %v", s, err)) + } + if selector.Empty() { + return b + } + return b.Selector(selector) +} + +// Selector accepts a selector directly, and if non nil will trigger a list action. +func (b *Builder) Selector(selector labels.Selector) *Builder { + b.selector = selector + return b +} + +// The namespace that these resources should be assumed to under - used by DefaultNamespace() +// and RequireNamespace() +func (b *Builder) NamespaceParam(namespace string) *Builder { + b.namespace = namespace + return b +} + +// DefaultNamespace instructs the builder to set the namespace value for any object found +// to NamespaceParam() if empty. +func (b *Builder) DefaultNamespace() *Builder { + b.defaultNamespace = true + return b +} + +// RequireNamespace instructs the builder to set the namespace value for any object found +// to NamespaceParam() if empty, and if the value on the resource does not match +// NamespaceParam() an error will be returned. +func (b *Builder) RequireNamespace() *Builder { + b.requireNamespace = true + return b +} + +// ResourceTypeOrNameArgs indicates that the builder should accept one or two arguments +// of the form `([,,...]| )`. When one argument is received, the types +// provided will be retrieved from the server (and be comma delimited). When two arguments are +// received, they must be a single type and name. If more than two arguments are provided an +// error is set. +func (b *Builder) ResourceTypeOrNameArgs(args ...string) *Builder { + switch len(args) { + case 2: + b.name = args[1] + b.ResourceTypes(SplitResourceArgument(args[0])...) + case 1: + b.ResourceTypes(SplitResourceArgument(args[0])...) + if b.selector == nil { + b.selector = labels.Everything() + } + case 0: + default: + b.errs = append(b.errs, fmt.Errorf("when passing arguments, must be resource or resource and name")) + } + return b +} + +// ResourceTypeAndNameArgs expects two arguments, a resource type, and a resource name. The resource +// matching that type and and name will be retrieved from the server. +func (b *Builder) ResourceTypeAndNameArgs(args ...string) *Builder { + switch len(args) { + case 2: + b.name = args[1] + b.ResourceTypes(SplitResourceArgument(args[0])...) + case 0: + default: + b.errs = append(b.errs, fmt.Errorf("when passing arguments, must be resource and name")) + } + return b +} + +// Flatten will convert any objects with a field named "Items" that is an array of runtime.Object +// compatible types into individual entries and give them their own items. The original object +// is not passed to any visitors. +func (b *Builder) Flatten() *Builder { + b.flatten = true + return b +} + +// Latest will fetch the latest copy of any objects loaded from URLs or files from the server. +func (b *Builder) Latest() *Builder { + b.latest = true + return b +} + +// ContinueOnError will attempt to load and visit as many objects as possible, even if some visits +// return errors or some objects cannot be loaded. The default behavior is to terminate after +// the first error is returned from a VisitorFunc. +func (b *Builder) ContinueOnError() *Builder { + b.continueOnError = true + return b +} + +func (b *Builder) SingleResourceType() *Builder { + b.singleResourceType = true + return b +} + +func (b *Builder) resourceMappings() ([]*meta.RESTMapping, error) { + if len(b.resources) > 1 && b.singleResourceType { + return nil, fmt.Errorf("you may only specify a single resource type") + } + mappings := []*meta.RESTMapping{} + for _, r := range b.resources { + version, kind, err := b.mapper.VersionAndKindForResource(r) + if err != nil { + return nil, err + } + mapping, err := b.mapper.RESTMapping(kind, version) + if err != nil { + return nil, err + } + mappings = append(mappings, mapping) + } + return mappings, nil +} + +func (b *Builder) visitorResult() *Result { + if len(b.errs) > 0 { + return &Result{err: errors.NewAggregate(b.errs)} + } + + // visit selectors + if b.selector != nil { + if len(b.name) != 0 { + return &Result{err: fmt.Errorf("name cannot be provided when a selector is specified")} + } + if len(b.resources) == 0 { + return &Result{err: fmt.Errorf("at least one resource must be specified to use a selector")} + } + // empty selector has different error message for paths being provided + if len(b.paths) != 0 { + if b.selector.Empty() { + return &Result{err: fmt.Errorf("when paths, URLs, or stdin is provided as input, you may not specify a resource by arguments as well")} + } else { + return &Result{err: fmt.Errorf("a selector may not be specified when path, URL, or stdin is provided as input")} + } + } + mappings, err := b.resourceMappings() + if err != nil { + return &Result{err: err} + } + + visitors := []Visitor{} + for _, mapping := range mappings { + client, err := b.mapper.ClientForMapping(mapping) + if err != nil { + return &Result{err: err} + } + visitors = append(visitors, NewSelector(client, mapping, b.namespace, b.selector)) + } + if b.continueOnError { + return &Result{visitor: EagerVisitorList(visitors), sources: visitors} + } + return &Result{visitor: VisitorList(visitors), sources: visitors} + } + + // visit single item specified by name + if len(b.name) != 0 { + if len(b.paths) != 0 { + return &Result{singular: true, err: fmt.Errorf("when paths, URLs, or stdin is provided as input, you may not specify a resource by arguments as well")} + } + if len(b.resources) == 0 { + return &Result{singular: true, err: fmt.Errorf("you must provide a resource and a resource name together")} + } + if len(b.resources) > 1 { + return &Result{singular: true, err: fmt.Errorf("you must specify only one resource")} + } + if len(b.namespace) == 0 { + return &Result{singular: true, err: fmt.Errorf("namespace may not be empty when retrieving a resource by name")} + } + mappings, err := b.resourceMappings() + if err != nil { + return &Result{singular: true, err: err} + } + client, err := b.mapper.ClientForMapping(mappings[0]) + if err != nil { + return &Result{singular: true, err: err} + } + info := NewInfo(client, mappings[0], b.namespace, b.name) + if err := info.Get(); err != nil { + return &Result{singular: true, err: err} + } + return &Result{singular: true, visitor: info, sources: []Visitor{info}} + } + + // visit items specified by paths + if len(b.paths) != 0 { + singular := !b.dir && !b.stream && len(b.paths) == 1 + if len(b.resources) != 0 { + return &Result{singular: singular, err: fmt.Errorf("when paths, URLs, or stdin is provided as input, you may not specify resource arguments as well")} + } + + var visitors Visitor + if b.continueOnError { + visitors = EagerVisitorList(b.paths) + } else { + visitors = VisitorList(b.paths) + } + + // only items from disk can be refetched + if b.latest { + // must flatten lists prior to fetching + if b.flatten { + visitors = NewFlattenListVisitor(visitors, b.mapper) + } + visitors = NewDecoratedVisitor(visitors, RetrieveLatest) + } + return &Result{singular: singular, visitor: visitors, sources: b.paths} + } + + return &Result{err: fmt.Errorf("you must provide one or more resources by argument or filename")} +} + +// Do returns a Result object with a Visitor for the resources identified by the Builder. +// The visitor will respect the error behavior specified by ContinueOnError. Note that stream +// inputs are consumed by the first execution - use Infos() or Object() on the Result to capture a list +// for further iteration. +func (b *Builder) Do() *Result { + r := b.visitorResult() + if r.err != nil { + return r + } + if b.flatten { + r.visitor = NewFlattenListVisitor(r.visitor, b.mapper) + } + helpers := []VisitorFunc{} + if b.defaultNamespace { + helpers = append(helpers, SetNamespace(b.namespace)) + } + if b.requireNamespace { + helpers = append(helpers, RequireNamespace(b.namespace)) + } + r.visitor = NewDecoratedVisitor(r.visitor, helpers...) + return r +} + +// Result contains helper methods for dealing with the outcome of a Builder. +type Result struct { + err error + visitor Visitor + + sources []Visitor + singular bool + + // populated by a call to Infos + info []*Info +} + +// Err returns one or more errors (via a util.ErrorList) that occurred prior +// to visiting the elements in the visitor. To see all errors including those +// that occur during visitation, invoke Infos(). +func (r *Result) Err() error { + return r.err +} + +// Visit implements the Visitor interface on the items described in the Builder. +// Note that some visitor sources are not traversable more than once, or may +// return different results. If you wish to operate on the same set of resources +// multiple times, use the Infos() method. +func (r *Result) Visit(fn VisitorFunc) error { + if r.err != nil { + return r.err + } + return r.visitor.Visit(fn) +} + +// IntoSingular sets the provided boolean pointer to true if the Builder input +// reflected a single item, or multiple. +func (r *Result) IntoSingular(b *bool) *Result { + *b = r.singular + return r +} + +// Infos returns an array of all of the resource infos retrieved via traversal. +// Will attempt to traverse the entire set of visitors only once, and will return +// a cached list on subsequent calls. +func (r *Result) Infos() ([]*Info, error) { + if r.err != nil { + return nil, r.err + } + if r.info != nil { + return r.info, nil + } + infos := []*Info{} + err := r.visitor.Visit(func(info *Info) error { + infos = append(infos, info) + return nil + }) + r.info, r.err = infos, err + return infos, err +} + +// Object returns a single object representing the output of a single visit to all +// found resources. If the Builder was a singular context (expected to return a +// single resource by user input) and only a single resource was found, the resource +// will be returned as is. Otherwise, the returned resources will be part of an +// api.List. The ResourceVersion of the api.List will be set only if it is identical +// across all infos returned. +func (r *Result) Object() (runtime.Object, error) { + infos, err := r.Infos() + if err != nil { + return nil, err + } + + versions := util.StringSet{} + objects := []runtime.Object{} + for _, info := range infos { + if info.Object != nil { + objects = append(objects, info.Object) + versions.Insert(info.ResourceVersion) + } + } + + if len(objects) == 1 { + if r.singular { + return objects[0], nil + } + // if the item is a list already, don't create another list + if _, err := runtime.GetItemsPtr(objects[0]); err == nil { + return objects[0], nil + } + } + + version := "" + if len(versions) == 1 { + version = versions.List()[0] + } + return &api.List{ + ListMeta: api.ListMeta{ + ResourceVersion: version, + }, + Items: objects, + }, err +} + +// ResourceMapping returns a single meta.RESTMapping representing the +// resources located by the builder, or an error if more than one +// mapping was found. +func (r *Result) ResourceMapping() (*meta.RESTMapping, error) { + if r.err != nil { + return nil, r.err + } + mappings := map[string]*meta.RESTMapping{} + for i := range r.sources { + m, ok := r.sources[i].(ResourceMapping) + if !ok { + return nil, fmt.Errorf("a resource mapping could not be loaded from %v", reflect.TypeOf(r.sources[i])) + } + mapping := m.ResourceMapping() + mappings[mapping.Resource] = mapping + } + if len(mappings) != 1 { + return nil, fmt.Errorf("expected only a single resource type") + } + for _, mapping := range mappings { + return mapping, nil + } + return nil, nil +} + +// Watch retrieves changes that occur on the server to the specified resource. +// It currently supports watching a single source - if the resource source +// (selectors or pure types) can be watched, they will be, otherwise the list +// will be visited (equivalent to the Infos() call) and if there is a single +// resource present, it will be watched, otherwise an error will be returned. +func (r *Result) Watch(resourceVersion string) (watch.Interface, error) { + if r.err != nil { + return nil, r.err + } + if len(r.sources) != 1 { + return nil, fmt.Errorf("you may only watch a single resource or type of resource at a time") + } + w, ok := r.sources[0].(Watchable) + if !ok { + info, err := r.Infos() + if err != nil { + return nil, err + } + if len(info) != 1 { + return nil, fmt.Errorf("watch is only supported on a single resource - %d resources were found", len(info)) + } + return info[0].Watch(resourceVersion) + } + return w.Watch(resourceVersion) +} + +func SplitResourceArgument(arg string) []string { + set := util.NewStringSet() + set.Insert(strings.Split(arg, ",")...) + return set.List() +} diff --git a/pkg/kubectl/resource/builder_test.go b/pkg/kubectl/resource/builder_test.go new file mode 100644 index 00000000000..eeb59736e1f --- /dev/null +++ b/pkg/kubectl/resource/builder_test.go @@ -0,0 +1,619 @@ +/* +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 resource + +import ( + "bytes" + "io" + "io/ioutil" + "net/http" + "net/http/httptest" + "reflect" + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util/errors" + "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" + watchjson "github.com/GoogleCloudPlatform/kubernetes/pkg/watch/json" +) + +func stringBody(body string) io.ReadCloser { + return ioutil.NopCloser(bytes.NewReader([]byte(body))) +} + +func watchBody(events ...watch.Event) string { + buf := &bytes.Buffer{} + enc := watchjson.NewEncoder(buf, latest.Codec) + for _, e := range events { + enc.Encode(&e) + } + return buf.String() +} + +func fakeClient() ClientMapper { + return ClientMapperFunc(func(*meta.RESTMapping) (RESTClient, error) { + return &client.FakeRESTClient{}, nil + }) +} + +func fakeClientWith(t *testing.T, data map[string]string) ClientMapper { + return ClientMapperFunc(func(*meta.RESTMapping) (RESTClient, error) { + return &client.FakeRESTClient{ + Codec: latest.Codec, + Client: client.HTTPClientFunc(func(req *http.Request) (*http.Response, error) { + p := req.URL.Path + q := req.URL.RawQuery + if len(q) != 0 { + p = p + "?" + q + } + body, ok := data[p] + if !ok { + t.Fatalf("unexpected request: %s (%s)\n%#v", p, req.URL, req) + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: stringBody(body), + }, nil + }), + }, nil + }) +} + +func testData() (*api.PodList, *api.ServiceList) { + pods := &api.PodList{ + ListMeta: api.ListMeta{ + ResourceVersion: "15", + }, + Items: []api.Pod{ + { + ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: "test", ResourceVersion: "10"}, + }, + { + ObjectMeta: api.ObjectMeta{Name: "bar", Namespace: "test", ResourceVersion: "11"}, + }, + }, + } + svc := &api.ServiceList{ + ListMeta: api.ListMeta{ + ResourceVersion: "16", + }, + Items: []api.Service{ + { + ObjectMeta: api.ObjectMeta{Name: "baz", Namespace: "test", ResourceVersion: "12"}, + }, + }, + } + return pods, svc +} + +func streamTestData() (io.Reader, *api.PodList, *api.ServiceList) { + pods, svc := testData() + r, w := io.Pipe() + go func() { + defer w.Close() + w.Write([]byte(runtime.EncodeOrDie(latest.Codec, pods))) + w.Write([]byte(runtime.EncodeOrDie(latest.Codec, svc))) + }() + return r, pods, svc +} + +type testVisitor struct { + InjectErr error + Infos []*Info +} + +func (v *testVisitor) Handle(info *Info) error { + v.Infos = append(v.Infos, info) + return v.InjectErr +} + +func (v *testVisitor) Objects() []runtime.Object { + objects := []runtime.Object{} + for i := range v.Infos { + objects = append(objects, v.Infos[i].Object) + } + return objects +} + +func TestPathBuilder(t *testing.T) { + b := NewBuilder(latest.RESTMapper, api.Scheme, fakeClient()). + FilenameParam("../../../examples/guestbook/redis-master.json") + + test := &testVisitor{} + singular := false + + err := b.Do().IntoSingular(&singular).Visit(test.Handle) + if err != nil || !singular || len(test.Infos) != 1 { + t.Fatalf("unexpected response: %v %f %#v", err, singular, test.Infos) + } + + info := test.Infos[0] + if info.Name != "redis-master" || info.Namespace != "" || info.Object == nil { + t.Errorf("unexpected info: %#v", info) + } +} + +func TestPathBuilderWithMultiple(t *testing.T) { + b := NewBuilder(latest.RESTMapper, api.Scheme, fakeClient()). + FilenameParam("../../../examples/guestbook/redis-master.json"). + FilenameParam("../../../examples/guestbook/redis-master.json"). + NamespaceParam("test").DefaultNamespace() + + test := &testVisitor{} + singular := false + + err := b.Do().IntoSingular(&singular).Visit(test.Handle) + if err != nil || singular || len(test.Infos) != 2 { + t.Fatalf("unexpected response: %v %f %#v", err, singular, test.Infos) + } + + info := test.Infos[1] + if info.Name != "redis-master" || info.Namespace != "test" || info.Object == nil { + t.Errorf("unexpected info: %#v", info) + } +} + +func TestDirectoryBuilder(t *testing.T) { + b := NewBuilder(latest.RESTMapper, api.Scheme, fakeClient()). + FilenameParam("../../../examples/guestbook"). + NamespaceParam("test").DefaultNamespace() + + test := &testVisitor{} + singular := false + + err := b.Do().IntoSingular(&singular).Visit(test.Handle) + if err != nil || singular || len(test.Infos) < 4 { + t.Fatalf("unexpected response: %v %f %#v", err, singular, test.Infos) + } + + found := false + for _, info := range test.Infos { + if info.Name == "redis-master" && info.Namespace == "test" && info.Object != nil { + found = true + } + } + if !found { + t.Errorf("unexpected responses: %#v", test.Infos) + } +} + +func TestURLBuilder(t *testing.T) { + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte(runtime.EncodeOrDie(latest.Codec, &api.Pod{ObjectMeta: api.ObjectMeta{Namespace: "foo", Name: "test"}}))) + })) + defer s.Close() + + b := NewBuilder(latest.RESTMapper, api.Scheme, fakeClient()). + FilenameParam(s.URL). + NamespaceParam("test") + + test := &testVisitor{} + singular := false + + err := b.Do().IntoSingular(&singular).Visit(test.Handle) + if err != nil || !singular || len(test.Infos) != 1 { + t.Fatalf("unexpected response: %v %f %#v", err, singular, test.Infos) + } + info := test.Infos[0] + if info.Name != "test" || info.Namespace != "foo" || info.Object == nil { + t.Errorf("unexpected info: %#v", info) + } +} + +func TestURLBuilderRequireNamespace(t *testing.T) { + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte(runtime.EncodeOrDie(latest.Codec, &api.Pod{ObjectMeta: api.ObjectMeta{Namespace: "foo", Name: "test"}}))) + })) + defer s.Close() + + b := NewBuilder(latest.RESTMapper, api.Scheme, fakeClient()). + FilenameParam(s.URL). + NamespaceParam("test").RequireNamespace() + + test := &testVisitor{} + singular := false + + err := b.Do().IntoSingular(&singular).Visit(test.Handle) + if err == nil || !singular || len(test.Infos) != 0 { + t.Fatalf("unexpected response: %v %f %#v", err, singular, test.Infos) + } +} + +func TestResourceByName(t *testing.T) { + pods, _ := testData() + b := NewBuilder(latest.RESTMapper, api.Scheme, fakeClientWith(t, map[string]string{ + "/ns/test/pods/foo": runtime.EncodeOrDie(latest.Codec, &pods.Items[0]), + })). + NamespaceParam("test") + + test := &testVisitor{} + singular := false + + if b.Do().Err() == nil { + t.Errorf("unexpected non-error") + } + + b.ResourceTypeOrNameArgs("pods", "foo") + + err := b.Do().IntoSingular(&singular).Visit(test.Handle) + if err != nil || !singular || len(test.Infos) != 1 { + t.Fatalf("unexpected response: %v %f %#v", err, singular, test.Infos) + } + if !reflect.DeepEqual(&pods.Items[0], test.Objects()[0]) { + t.Errorf("unexpected object: %#v", test.Objects()) + } + + mapping, err := b.Do().ResourceMapping() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if mapping.Resource != "pods" { + t.Errorf("unexpected resource mapping: %#v", mapping) + } +} + +func TestResourceByNameAndEmptySelector(t *testing.T) { + pods, _ := testData() + b := NewBuilder(latest.RESTMapper, api.Scheme, fakeClientWith(t, map[string]string{ + "/ns/test/pods/foo": runtime.EncodeOrDie(latest.Codec, &pods.Items[0]), + })). + NamespaceParam("test"). + SelectorParam(""). + ResourceTypeOrNameArgs("pods", "foo") + + singular := false + infos, err := b.Do().IntoSingular(&singular).Infos() + if err != nil || !singular || len(infos) != 1 { + t.Fatalf("unexpected response: %v %f %#v", err, singular, infos) + } + if !reflect.DeepEqual(&pods.Items[0], infos[0].Object) { + t.Errorf("unexpected object: %#v", infos[0]) + } + + mapping, err := b.Do().ResourceMapping() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if mapping.Resource != "pods" { + t.Errorf("unexpected resource mapping: %#v", mapping) + } +} + +func TestSelector(t *testing.T) { + pods, svc := testData() + b := NewBuilder(latest.RESTMapper, api.Scheme, fakeClientWith(t, map[string]string{ + "/ns/test/pods?labels=a%3Db": runtime.EncodeOrDie(latest.Codec, pods), + "/ns/test/services?labels=a%3Db": runtime.EncodeOrDie(latest.Codec, svc), + })). + SelectorParam("a=b"). + NamespaceParam("test"). + Flatten() + + test := &testVisitor{} + singular := false + + if b.Do().Err() == nil { + t.Errorf("unexpected non-error") + } + + b.ResourceTypeOrNameArgs("pods,service") + + err := b.Do().IntoSingular(&singular).Visit(test.Handle) + if err != nil || singular || len(test.Infos) != 3 { + t.Fatalf("unexpected response: %v %f %#v", err, singular, test.Infos) + } + if !reflect.DeepEqual([]runtime.Object{&pods.Items[0], &pods.Items[1], &svc.Items[0]}, test.Objects()) { + t.Errorf("unexpected visited objects: %#v", test.Objects()) + } + + if _, err := b.Do().ResourceMapping(); err == nil { + t.Errorf("unexpected non-error") + } +} + +func TestSelectorRequiresKnownTypes(t *testing.T) { + b := NewBuilder(latest.RESTMapper, api.Scheme, fakeClient()). + SelectorParam("a=b"). + NamespaceParam("test"). + ResourceTypes("unknown") + + if b.Do().Err() == nil { + t.Errorf("unexpected non-error") + } +} + +func TestSingleResourceType(t *testing.T) { + b := NewBuilder(latest.RESTMapper, api.Scheme, fakeClient()). + SelectorParam("a=b"). + SingleResourceType(). + ResourceTypeOrNameArgs("pods,services") + + if b.Do().Err() == nil { + t.Errorf("unexpected non-error") + } +} + +func TestStream(t *testing.T) { + r, pods, rc := streamTestData() + b := NewBuilder(latest.RESTMapper, api.Scheme, fakeClient()). + NamespaceParam("test").Stream(r, "STDIN").Flatten() + + test := &testVisitor{} + singular := false + + err := b.Do().IntoSingular(&singular).Visit(test.Handle) + if err != nil || singular || len(test.Infos) != 3 { + t.Fatalf("unexpected response: %v %f %#v", err, singular, test.Infos) + } + if !reflect.DeepEqual([]runtime.Object{&pods.Items[0], &pods.Items[1], &rc.Items[0]}, test.Objects()) { + t.Errorf("unexpected visited objects: %#v", test.Objects()) + } +} + +func TestMultipleObject(t *testing.T) { + r, pods, svc := streamTestData() + obj, err := NewBuilder(latest.RESTMapper, api.Scheme, fakeClient()). + NamespaceParam("test").Stream(r, "STDIN").Flatten(). + Do().Object() + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + expected := &api.List{ + Items: []runtime.Object{ + &pods.Items[0], + &pods.Items[1], + &svc.Items[0], + }, + } + if !reflect.DeepEqual(expected, obj) { + t.Errorf("unexpected visited objects: %#v", obj) + } +} + +func TestSingularObject(t *testing.T) { + obj, err := NewBuilder(latest.RESTMapper, api.Scheme, fakeClient()). + NamespaceParam("test").DefaultNamespace(). + FilenameParam("../../../examples/guestbook/redis-master.json"). + Flatten(). + Do().Object() + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + pod, ok := obj.(*api.Pod) + if !ok { + t.Fatalf("unexpected object: %#v", obj) + } + if pod.Name != "redis-master" || pod.Namespace != "test" { + t.Errorf("unexpected pod: %#v", pod) + } +} + +func TestListObject(t *testing.T) { + pods, _ := testData() + b := NewBuilder(latest.RESTMapper, api.Scheme, fakeClientWith(t, map[string]string{ + "/ns/test/pods?labels=a%3Db": runtime.EncodeOrDie(latest.Codec, pods), + })). + SelectorParam("a=b"). + NamespaceParam("test"). + ResourceTypeOrNameArgs("pods"). + Flatten() + + obj, err := b.Do().Object() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + list, ok := obj.(*api.List) + if !ok { + t.Fatalf("unexpected object: %#v", obj) + } + if list.ResourceVersion != pods.ResourceVersion || len(list.Items) != 2 { + t.Errorf("unexpected list: %#v", list) + } + + mapping, err := b.Do().ResourceMapping() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if mapping.Resource != "pods" { + t.Errorf("unexpected resource mapping: %#v", mapping) + } +} + +func TestListObjectWithDifferentVersions(t *testing.T) { + pods, svc := testData() + obj, err := NewBuilder(latest.RESTMapper, api.Scheme, fakeClientWith(t, map[string]string{ + "/ns/test/pods?labels=a%3Db": runtime.EncodeOrDie(latest.Codec, pods), + "/ns/test/services?labels=a%3Db": runtime.EncodeOrDie(latest.Codec, svc), + })). + SelectorParam("a=b"). + NamespaceParam("test"). + ResourceTypeOrNameArgs("pods,services"). + Flatten(). + Do().Object() + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + list, ok := obj.(*api.List) + if !ok { + t.Fatalf("unexpected object: %#v", obj) + } + // resource version differs between type lists, so it's not possible to get a single version. + if list.ResourceVersion != "" || len(list.Items) != 3 { + t.Errorf("unexpected list: %#v", list) + } +} + +func TestWatch(t *testing.T) { + pods, _ := testData() + w, err := NewBuilder(latest.RESTMapper, api.Scheme, fakeClientWith(t, map[string]string{ + "/watch/ns/test/pods/redis-master?resourceVersion=10": watchBody(watch.Event{ + Type: watch.Added, + Object: &pods.Items[0], + }), + })). + NamespaceParam("test").DefaultNamespace(). + FilenameParam("../../../examples/guestbook/redis-master.json").Flatten(). + Do().Watch("10") + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + defer w.Stop() + ch := w.ResultChan() + select { + case obj := <-ch: + if obj.Type != watch.Added { + t.Fatalf("unexpected watch event", obj) + } + pod, ok := obj.Object.(*api.Pod) + if !ok { + t.Fatalf("unexpected object: %#v", obj) + } + if pod.Name != "foo" || pod.ResourceVersion != "10" { + t.Errorf("unexpected pod: %#v", pod) + } + } +} + +func TestWatchMultipleError(t *testing.T) { + _, err := NewBuilder(latest.RESTMapper, api.Scheme, fakeClient()). + NamespaceParam("test").DefaultNamespace(). + FilenameParam("../../../examples/guestbook/redis-master.json").Flatten(). + FilenameParam("../../../examples/guestbook/redis-master.json").Flatten(). + Do().Watch("") + + if err == nil { + t.Fatalf("unexpected non-error") + } +} + +func TestLatest(t *testing.T) { + r, _, _ := streamTestData() + newPod := &api.Pod{ + ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: "test", ResourceVersion: "13"}, + } + newPod2 := &api.Pod{ + ObjectMeta: api.ObjectMeta{Name: "bar", Namespace: "test", ResourceVersion: "14"}, + } + newSvc := &api.Service{ + ObjectMeta: api.ObjectMeta{Name: "baz", Namespace: "test", ResourceVersion: "15"}, + } + + b := NewBuilder(latest.RESTMapper, api.Scheme, fakeClientWith(t, map[string]string{ + "/ns/test/pods/foo": runtime.EncodeOrDie(latest.Codec, newPod), + "/ns/test/pods/bar": runtime.EncodeOrDie(latest.Codec, newPod2), + "/ns/test/services/baz": runtime.EncodeOrDie(latest.Codec, newSvc), + })). + NamespaceParam("other").Stream(r, "STDIN").Flatten().Latest() + + test := &testVisitor{} + singular := false + + err := b.Do().IntoSingular(&singular).Visit(test.Handle) + if err != nil || singular || len(test.Infos) != 3 { + t.Fatalf("unexpected response: %v %f %#v", err, singular, test.Infos) + } + if !reflect.DeepEqual([]runtime.Object{newPod, newPod2, newSvc}, test.Objects()) { + t.Errorf("unexpected visited objects: %#v", test.Objects()) + } +} + +func TestIgnoreStreamErrors(t *testing.T) { + pods, svc := testData() + + r, w := io.Pipe() + go func() { + defer w.Close() + w.Write([]byte(`{}`)) + w.Write([]byte(runtime.EncodeOrDie(latest.Codec, &pods.Items[0]))) + }() + + r2, w2 := io.Pipe() + go func() { + defer w2.Close() + w2.Write([]byte(`{}`)) + w2.Write([]byte(runtime.EncodeOrDie(latest.Codec, &svc.Items[0]))) + }() + + b := NewBuilder(latest.RESTMapper, api.Scheme, fakeClient()). + ContinueOnError(). // TODO: order seems bad, but allows clients to determine what they want... + Stream(r, "1").Stream(r2, "2") + + test := &testVisitor{} + singular := false + + err := b.Do().IntoSingular(&singular).Visit(test.Handle) + if err != nil || singular || len(test.Infos) != 2 { + t.Fatalf("unexpected response: %v %f %#v", err, singular, test.Infos) + } + + if !reflect.DeepEqual([]runtime.Object{&pods.Items[0], &svc.Items[0]}, test.Objects()) { + t.Errorf("unexpected visited objects: %#v", test.Objects()) + } +} + +func TestReceiveMultipleErrors(t *testing.T) { + pods, svc := testData() + + r, w := io.Pipe() + go func() { + defer w.Close() + w.Write([]byte(`{}`)) + w.Write([]byte(runtime.EncodeOrDie(latest.Codec, &pods.Items[0]))) + }() + + r2, w2 := io.Pipe() + go func() { + defer w2.Close() + w2.Write([]byte(`{}`)) + w2.Write([]byte(runtime.EncodeOrDie(latest.Codec, &svc.Items[0]))) + }() + + b := NewBuilder(latest.RESTMapper, api.Scheme, fakeClient()). + Stream(r, "1").Stream(r2, "2"). + ContinueOnError() + + test := &testVisitor{} + singular := false + + err := b.Do().IntoSingular(&singular).Visit(test.Handle) + if err == nil || singular || len(test.Infos) != 0 { + t.Fatalf("unexpected response: %v %f %#v", err, singular, test.Infos) + } + + errs, ok := err.(errors.Aggregate) + if !ok { + t.Fatalf("unexpected error: %v", reflect.TypeOf(err)) + } + if len(errs.Errors()) != 2 { + t.Errorf("unexpected errors", errs) + } +} diff --git a/pkg/kubectl/resource/doc.go b/pkg/kubectl/resource/doc.go new file mode 100644 index 00000000000..60ba119eda9 --- /dev/null +++ b/pkg/kubectl/resource/doc.go @@ -0,0 +1,24 @@ +/* +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 resource assists clients in dealing with RESTful objects that match the +// Kubernetes API conventions. The Helper object provides simple CRUD operations +// on resources. The Visitor interface makes it easy to deal with multiple resources +// in bulk for retrieval and operation. The Builder object simplifies converting +// standard command line arguments and parameters into a Visitor that can iterate +// over all of the identified resources, whether on the server or on the local +// filesystem. +package resource diff --git a/pkg/kubectl/resource/helper.go b/pkg/kubectl/resource/helper.go new file mode 100644 index 00000000000..e12aec49571 --- /dev/null +++ b/pkg/kubectl/resource/helper.go @@ -0,0 +1,172 @@ +/* +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 resource + +import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" +) + +// Helper provides methods for retrieving or mutating a RESTful +// resource. +type Helper struct { + // The name of this resource as the server would recognize it + Resource string + // A RESTClient capable of mutating this resource + RESTClient RESTClient + // A codec for decoding and encoding objects of this resource type. + Codec runtime.Codec + // An interface for reading or writing the resource version of this + // type. + Versioner runtime.ResourceVersioner +} + +// NewHelper creates a Helper from a ResourceMapping +func NewHelper(client RESTClient, mapping *meta.RESTMapping) *Helper { + return &Helper{ + RESTClient: client, + Resource: mapping.Resource, + Codec: mapping.Codec, + Versioner: mapping.MetadataAccessor, + } +} + +func (m *Helper) Get(namespace, name string) (runtime.Object, error) { + return m.RESTClient.Get(). + Namespace(namespace). + Resource(m.Resource). + Name(name). + Do(). + Get() +} + +func (m *Helper) List(namespace string, selector labels.Selector) (runtime.Object, error) { + return m.RESTClient.Get(). + Namespace(namespace). + Resource(m.Resource). + SelectorParam("labels", selector). + Do(). + Get() +} + +func (m *Helper) Watch(namespace, resourceVersion string, labelSelector, fieldSelector labels.Selector) (watch.Interface, error) { + return m.RESTClient.Get(). + Prefix("watch"). + Namespace(namespace). + Resource(m.Resource). + Param("resourceVersion", resourceVersion). + SelectorParam("labels", labelSelector). + SelectorParam("fields", fieldSelector). + Watch() +} + +func (m *Helper) WatchSingle(namespace, name, resourceVersion string) (watch.Interface, error) { + return m.RESTClient.Get(). + Prefix("watch"). + Namespace(namespace). + Resource(m.Resource). + Name(name). + Param("resourceVersion", resourceVersion). + Watch() +} + +func (m *Helper) Delete(namespace, name string) error { + return m.RESTClient.Delete(). + Namespace(namespace). + Resource(m.Resource). + Name(name). + Do(). + Error() +} + +func (m *Helper) Create(namespace string, modify bool, data []byte) error { + if modify { + obj, err := m.Codec.Decode(data) + if err != nil { + // We don't know how to check a version on this object, but create it anyway + return createResource(m.RESTClient, m.Resource, namespace, data) + } + + // Attempt to version the object based on client logic. + version, err := m.Versioner.ResourceVersion(obj) + if err != nil { + // We don't know how to clear the version on this object, so send it to the server as is + return createResource(m.RESTClient, m.Resource, namespace, data) + } + if version != "" { + if err := m.Versioner.SetResourceVersion(obj, ""); err != nil { + return err + } + newData, err := m.Codec.Encode(obj) + if err != nil { + return err + } + data = newData + } + } + + return createResource(m.RESTClient, m.Resource, namespace, data) +} + +func createResource(c RESTClient, resource, namespace string, data []byte) error { + return c.Post().Namespace(namespace).Resource(resource).Body(data).Do().Error() +} + +func (m *Helper) Update(namespace, name string, overwrite bool, data []byte) error { + c := m.RESTClient + + obj, err := m.Codec.Decode(data) + if err != nil { + // We don't know how to handle this object, but update it anyway + return updateResource(c, m.Resource, namespace, name, data) + } + + // Attempt to version the object based on client logic. + version, err := m.Versioner.ResourceVersion(obj) + if err != nil { + // We don't know how to version this object, so send it to the server as is + return updateResource(c, m.Resource, namespace, name, data) + } + if version == "" && overwrite { + // Retrieve the current version of the object to overwrite the server object + serverObj, err := c.Get().Namespace(namespace).Resource(m.Resource).Name(name).Do().Get() + if err != nil { + // The object does not exist, but we want it to be created + return updateResource(c, m.Resource, namespace, name, data) + } + serverVersion, err := m.Versioner.ResourceVersion(serverObj) + if err != nil { + return err + } + if err := m.Versioner.SetResourceVersion(obj, serverVersion); err != nil { + return err + } + newData, err := m.Codec.Encode(obj) + if err != nil { + return err + } + data = newData + } + + return updateResource(c, m.Resource, namespace, name, data) +} + +func updateResource(c RESTClient, resource, namespace, name string, data []byte) error { + return c.Put().Namespace(namespace).Resource(resource).Name(name).Body(data).Do().Error() +} diff --git a/pkg/kubectl/resource/helper_test.go b/pkg/kubectl/resource/helper_test.go new file mode 100644 index 00000000000..1b4a8c8eac5 --- /dev/null +++ b/pkg/kubectl/resource/helper_test.go @@ -0,0 +1,463 @@ +/* +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 resource + +import ( + "bytes" + "errors" + "io" + "io/ioutil" + "net/http" + "reflect" + "strings" + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/testapi" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" +) + +func objBody(obj runtime.Object) io.ReadCloser { + return ioutil.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(testapi.Codec(), obj)))) +} + +// splitPath returns the segments for a URL path. +func splitPath(path string) []string { + path = strings.Trim(path, "/") + if path == "" { + return []string{} + } + return strings.Split(path, "/") +} + +func TestHelperDelete(t *testing.T) { + tests := []struct { + Err bool + Req func(*http.Request) bool + Resp *http.Response + HttpErr error + }{ + { + HttpErr: errors.New("failure"), + Err: true, + }, + { + Resp: &http.Response{ + StatusCode: http.StatusNotFound, + Body: objBody(&api.Status{Status: api.StatusFailure}), + }, + Err: true, + }, + { + Resp: &http.Response{ + StatusCode: http.StatusOK, + Body: objBody(&api.Status{Status: api.StatusSuccess}), + }, + Req: func(req *http.Request) bool { + if req.Method != "DELETE" { + t.Errorf("unexpected method: %#v", req) + return false + } + parts := splitPath(req.URL.Path) + if parts[1] != "bar" { + t.Errorf("url doesn't contain namespace: %#v", req) + return false + } + if parts[2] != "foo" { + t.Errorf("url doesn't contain name: %#v", req) + return false + } + return true + }, + }, + } + for _, test := range tests { + client := &client.FakeRESTClient{ + Codec: testapi.Codec(), + Resp: test.Resp, + Err: test.HttpErr, + } + modifier := &Helper{ + RESTClient: client, + } + err := modifier.Delete("bar", "foo") + if (err != nil) != test.Err { + t.Errorf("unexpected error: %t %v", test.Err, err) + } + if err != nil { + continue + } + if test.Req != nil && !test.Req(client.Req) { + t.Errorf("unexpected request: %#v", client.Req) + } + } +} + +func TestHelperCreate(t *testing.T) { + expectPost := func(req *http.Request) bool { + if req.Method != "POST" { + t.Errorf("unexpected method: %#v", req) + return false + } + parts := splitPath(req.URL.Path) + if parts[1] != "bar" { + t.Errorf("url doesn't contain namespace: %#v", req) + return false + } + return true + } + + tests := []struct { + Resp *http.Response + RespFunc client.HTTPClientFunc + HttpErr error + Modify bool + Object runtime.Object + + ExpectObject runtime.Object + Err bool + Req func(*http.Request) bool + }{ + { + HttpErr: errors.New("failure"), + Err: true, + }, + { + Resp: &http.Response{ + StatusCode: http.StatusNotFound, + Body: objBody(&api.Status{Status: api.StatusFailure}), + }, + Err: true, + }, + { + Resp: &http.Response{ + StatusCode: http.StatusOK, + Body: objBody(&api.Status{Status: api.StatusSuccess}), + }, + Object: &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo"}}, + ExpectObject: &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo"}}, + Req: expectPost, + }, + { + Modify: false, + Object: &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo", ResourceVersion: "10"}}, + ExpectObject: &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo", ResourceVersion: "10"}}, + Resp: &http.Response{StatusCode: http.StatusOK, Body: objBody(&api.Status{Status: api.StatusSuccess})}, + Req: expectPost, + }, + { + Modify: true, + Object: &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo", ResourceVersion: "10"}}, + ExpectObject: &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo"}}, + Resp: &http.Response{StatusCode: http.StatusOK, Body: objBody(&api.Status{Status: api.StatusSuccess})}, + Req: expectPost, + }, + } + for i, test := range tests { + client := &client.FakeRESTClient{ + Codec: testapi.Codec(), + Resp: test.Resp, + Err: test.HttpErr, + } + if test.RespFunc != nil { + client.Client = test.RespFunc + } + modifier := &Helper{ + RESTClient: client, + Codec: testapi.Codec(), + Versioner: testapi.MetadataAccessor(), + } + data := []byte{} + if test.Object != nil { + data = []byte(runtime.EncodeOrDie(testapi.Codec(), test.Object)) + } + err := modifier.Create("bar", test.Modify, data) + if (err != nil) != test.Err { + t.Errorf("%d: unexpected error: %t %v", i, test.Err, err) + } + if err != nil { + continue + } + if test.Req != nil && !test.Req(client.Req) { + t.Errorf("%d: unexpected request: %#v", i, client.Req) + } + body, err := ioutil.ReadAll(client.Req.Body) + if err != nil { + t.Fatalf("%d: unexpected error: %#v", i, err) + } + t.Logf("got body: %s", string(body)) + expect := []byte{} + if test.ExpectObject != nil { + expect = []byte(runtime.EncodeOrDie(testapi.Codec(), test.ExpectObject)) + } + if !reflect.DeepEqual(expect, body) { + t.Errorf("%d: unexpected body: %s", i, string(body)) + } + + } +} + +func TestHelperGet(t *testing.T) { + tests := []struct { + Err bool + Req func(*http.Request) bool + Resp *http.Response + HttpErr error + }{ + { + HttpErr: errors.New("failure"), + Err: true, + }, + { + Resp: &http.Response{ + StatusCode: http.StatusNotFound, + Body: objBody(&api.Status{Status: api.StatusFailure}), + }, + Err: true, + }, + { + Resp: &http.Response{ + StatusCode: http.StatusOK, + Body: objBody(&api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo"}}), + }, + Req: func(req *http.Request) bool { + if req.Method != "GET" { + t.Errorf("unexpected method: %#v", req) + return false + } + parts := splitPath(req.URL.Path) + if parts[1] != "bar" { + t.Errorf("url doesn't contain namespace: %#v", req) + return false + } + if parts[2] != "foo" { + t.Errorf("url doesn't contain name: %#v", req) + return false + } + return true + }, + }, + } + for _, test := range tests { + client := &client.FakeRESTClient{ + Codec: testapi.Codec(), + Resp: test.Resp, + Err: test.HttpErr, + } + modifier := &Helper{ + RESTClient: client, + } + obj, err := modifier.Get("bar", "foo") + if (err != nil) != test.Err { + t.Errorf("unexpected error: %t %v", test.Err, err) + } + if err != nil { + continue + } + if obj.(*api.Pod).Name != "foo" { + t.Errorf("unexpected object: %#v", obj) + } + if test.Req != nil && !test.Req(client.Req) { + t.Errorf("unexpected request: %#v", client.Req) + } + } +} + +func TestHelperList(t *testing.T) { + tests := []struct { + Err bool + Req func(*http.Request) bool + Resp *http.Response + HttpErr error + }{ + { + HttpErr: errors.New("failure"), + Err: true, + }, + { + Resp: &http.Response{ + StatusCode: http.StatusNotFound, + Body: objBody(&api.Status{Status: api.StatusFailure}), + }, + Err: true, + }, + { + Resp: &http.Response{ + StatusCode: http.StatusOK, + Body: objBody(&api.PodList{ + Items: []api.Pod{{ + ObjectMeta: api.ObjectMeta{Name: "foo"}, + }, + }, + }), + }, + Req: func(req *http.Request) bool { + if req.Method != "GET" { + t.Errorf("unexpected method: %#v", req) + return false + } + if req.URL.Path != "/ns/bar" { + t.Errorf("url doesn't contain name: %#v", req.URL) + return false + } + if req.URL.Query().Get("labels") != labels.SelectorFromSet(labels.Set{"foo": "baz"}).String() { + t.Errorf("url doesn't contain query parameters: %#v", req.URL) + return false + } + return true + }, + }, + } + for _, test := range tests { + client := &client.FakeRESTClient{ + Codec: testapi.Codec(), + Resp: test.Resp, + Err: test.HttpErr, + } + modifier := &Helper{ + RESTClient: client, + } + obj, err := modifier.List("bar", labels.SelectorFromSet(labels.Set{"foo": "baz"})) + if (err != nil) != test.Err { + t.Errorf("unexpected error: %t %v", test.Err, err) + } + if err != nil { + continue + } + if obj.(*api.PodList).Items[0].Name != "foo" { + t.Errorf("unexpected object: %#v", obj) + } + if test.Req != nil && !test.Req(client.Req) { + t.Errorf("unexpected request: %#v", client.Req) + } + } +} + +func TestHelperUpdate(t *testing.T) { + expectPut := func(req *http.Request) bool { + if req.Method != "PUT" { + t.Errorf("unexpected method: %#v", req) + return false + } + parts := splitPath(req.URL.Path) + if parts[1] != "bar" { + t.Errorf("url doesn't contain namespace: %#v", req.URL) + return false + } + if parts[2] != "foo" { + t.Errorf("url doesn't contain name: %#v", req) + return false + } + return true + } + + tests := []struct { + Resp *http.Response + RespFunc client.HTTPClientFunc + HttpErr error + Overwrite bool + Object runtime.Object + + ExpectObject runtime.Object + Err bool + Req func(*http.Request) bool + }{ + { + HttpErr: errors.New("failure"), + Err: true, + }, + { + Object: &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo"}}, + Resp: &http.Response{ + StatusCode: http.StatusNotFound, + Body: objBody(&api.Status{Status: api.StatusFailure}), + }, + Err: true, + }, + { + Object: &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo"}}, + ExpectObject: &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo"}}, + Resp: &http.Response{ + StatusCode: http.StatusOK, + Body: objBody(&api.Status{Status: api.StatusSuccess}), + }, + Req: expectPut, + }, + { + Object: &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo"}}, + ExpectObject: &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo", ResourceVersion: "10"}}, + + Overwrite: true, + RespFunc: func(req *http.Request) (*http.Response, error) { + if req.Method == "PUT" { + return &http.Response{StatusCode: http.StatusOK, Body: objBody(&api.Status{Status: api.StatusSuccess})}, nil + } + return &http.Response{StatusCode: http.StatusOK, Body: objBody(&api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo", ResourceVersion: "10"}})}, nil + }, + Req: expectPut, + }, + { + Object: &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo", ResourceVersion: "10"}}, + ExpectObject: &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo", ResourceVersion: "10"}}, + Resp: &http.Response{StatusCode: http.StatusOK, Body: objBody(&api.Status{Status: api.StatusSuccess})}, + Req: expectPut, + }, + } + for i, test := range tests { + client := &client.FakeRESTClient{ + Codec: testapi.Codec(), + Resp: test.Resp, + Err: test.HttpErr, + } + if test.RespFunc != nil { + client.Client = test.RespFunc + } + modifier := &Helper{ + RESTClient: client, + Codec: testapi.Codec(), + Versioner: testapi.MetadataAccessor(), + } + data := []byte{} + if test.Object != nil { + data = []byte(runtime.EncodeOrDie(testapi.Codec(), test.Object)) + } + err := modifier.Update("bar", "foo", test.Overwrite, data) + if (err != nil) != test.Err { + t.Errorf("%d: unexpected error: %t %v", i, test.Err, err) + } + if err != nil { + continue + } + if test.Req != nil && !test.Req(client.Req) { + t.Errorf("%d: unexpected request: %#v", i, client.Req) + } + body, err := ioutil.ReadAll(client.Req.Body) + if err != nil { + t.Fatalf("%d: unexpected error: %#v", i, err) + } + t.Logf("got body: %s", string(body)) + expect := []byte{} + if test.ExpectObject != nil { + expect = []byte(runtime.EncodeOrDie(testapi.Codec(), test.ExpectObject)) + } + if !reflect.DeepEqual(expect, body) { + t.Errorf("%d: unexpected body: %s", i, string(body)) + } + } +} diff --git a/pkg/kubectl/resource/interfaces.go b/pkg/kubectl/resource/interfaces.go new file mode 100644 index 00000000000..09315d99763 --- /dev/null +++ b/pkg/kubectl/resource/interfaces.go @@ -0,0 +1,44 @@ +/* +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 resource + +import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" +) + +// RESTClient is a client helper for dealing with RESTful resources +// in a generic way. +type RESTClient interface { + Get() *client.Request + Post() *client.Request + Delete() *client.Request + Put() *client.Request +} + +// ClientMapper retrieves a client object for a given mapping +type ClientMapper interface { + ClientForMapping(mapping *meta.RESTMapping) (RESTClient, error) +} + +// ClientMapperFunc implements ClientMapper for a function +type ClientMapperFunc func(mapping *meta.RESTMapping) (RESTClient, error) + +// ClientForMapping implements ClientMapper +func (f ClientMapperFunc) ClientForMapping(mapping *meta.RESTMapping) (RESTClient, error) { + return f(mapping) +} diff --git a/pkg/kubectl/resource/mapper.go b/pkg/kubectl/resource/mapper.go new file mode 100644 index 00000000000..874eddbe87b --- /dev/null +++ b/pkg/kubectl/resource/mapper.go @@ -0,0 +1,97 @@ +/* +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 resource + +import ( + "fmt" + "reflect" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" +) + +// Mapper is a convenience struct for holding references to the three interfaces +// needed to create Info for arbitrary objects. +type Mapper struct { + runtime.ObjectTyper + meta.RESTMapper + ClientMapper +} + +// InfoForData creates an Info object for the given data. An error is returned +// if any of the decoding or client lookup steps fail. Name and namespace will be +// set into Info if the mapping's MetadataAccessor can retrieve them. +func (m *Mapper) InfoForData(data []byte, source string) (*Info, error) { + version, kind, err := m.DataVersionAndKind(data) + if err != nil { + return nil, fmt.Errorf("unable to get type info from %q: %v", source, err) + } + mapping, err := m.RESTMapping(kind, version) + if err != nil { + return nil, fmt.Errorf("unable to recognize %q: %v", source, err) + } + obj, err := mapping.Codec.Decode(data) + if err != nil { + return nil, fmt.Errorf("unable to load %q: %v", source, err) + } + client, err := m.ClientForMapping(mapping) + if err != nil { + return nil, fmt.Errorf("unable to connect to a server to handle %q: %v", mapping.Resource, err) + } + name, _ := mapping.MetadataAccessor.Name(obj) + namespace, _ := mapping.MetadataAccessor.Namespace(obj) + resourceVersion, _ := mapping.MetadataAccessor.ResourceVersion(obj) + return &Info{ + Mapping: mapping, + Client: client, + Namespace: namespace, + Name: name, + + Object: obj, + ResourceVersion: resourceVersion, + }, nil +} + +// InfoForData creates an Info object for the given Object. An error is returned +// if the object cannot be introspected. Name and namespace will be set into Info +// if the mapping's MetadataAccessor can retrieve them. +func (m *Mapper) InfoForObject(obj runtime.Object) (*Info, error) { + version, kind, err := m.ObjectVersionAndKind(obj) + if err != nil { + return nil, fmt.Errorf("unable to get type info from the object %q: %v", reflect.TypeOf(obj), err) + } + mapping, err := m.RESTMapping(kind, version) + if err != nil { + return nil, fmt.Errorf("unable to recognize %q: %v", kind, err) + } + client, err := m.ClientForMapping(mapping) + if err != nil { + return nil, fmt.Errorf("unable to connect to a server to handle %q: %v", mapping.Resource, err) + } + name, _ := mapping.MetadataAccessor.Name(obj) + namespace, _ := mapping.MetadataAccessor.Namespace(obj) + resourceVersion, _ := mapping.MetadataAccessor.ResourceVersion(obj) + return &Info{ + Mapping: mapping, + Client: client, + Namespace: namespace, + Name: name, + + Object: obj, + ResourceVersion: resourceVersion, + }, nil +} diff --git a/pkg/kubectl/resource/selector.go b/pkg/kubectl/resource/selector.go new file mode 100644 index 00000000000..379be13bec4 --- /dev/null +++ b/pkg/kubectl/resource/selector.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 resource + +import ( + "github.com/golang/glog" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" +) + +// Selector is a Visitor for resources that match a label selector. +type Selector struct { + Client RESTClient + Mapping *meta.RESTMapping + Namespace string + Selector labels.Selector +} + +// NewSelector creates a resource selector which hides details of getting items by their label selector. +func NewSelector(client RESTClient, mapping *meta.RESTMapping, namespace string, selector labels.Selector) *Selector { + return &Selector{ + Client: client, + Mapping: mapping, + Namespace: namespace, + Selector: selector, + } +} + +// Visit implements Visitor +func (r *Selector) Visit(fn VisitorFunc) error { + list, err := NewHelper(r.Client, r.Mapping).List(r.Namespace, r.Selector) + if err != nil { + if errors.IsBadRequest(err) || errors.IsNotFound(err) { + if r.Selector.Empty() { + glog.V(2).Infof("Unable to list %q: %v", r.Mapping.Resource, err) + } else { + glog.V(2).Infof("Unable to find %q that match the selector %q: %v", r.Mapping.Resource, r.Selector, err) + } + return nil + } + return err + } + accessor := r.Mapping.MetadataAccessor + resourceVersion, _ := accessor.ResourceVersion(list) + info := &Info{ + Client: r.Client, + Mapping: r.Mapping, + Namespace: r.Namespace, + + Object: list, + ResourceVersion: resourceVersion, + } + return fn(info) +} + +func (r *Selector) Watch(resourceVersion string) (watch.Interface, error) { + return NewHelper(r.Client, r.Mapping).Watch(r.Namespace, resourceVersion, r.Selector, labels.Everything()) +} + +// ResourceMapping returns the mapping for this resource and implements ResourceMapping +func (r *Selector) ResourceMapping() *meta.RESTMapping { + return r.Mapping +} diff --git a/pkg/kubectl/resource/visitor.go b/pkg/kubectl/resource/visitor.go new file mode 100644 index 00000000000..4d820a138ee --- /dev/null +++ b/pkg/kubectl/resource/visitor.go @@ -0,0 +1,422 @@ +/* +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 resource + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "os" + "path/filepath" + + "github.com/golang/glog" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util/errors" + "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" +) + +// Visitor lets clients walk a list of resources. +type Visitor interface { + Visit(VisitorFunc) error +} + +// VisitorFunc implements the Visitor interface for a matching function +type VisitorFunc func(*Info) error + +// Watchable describes a resource that can be watched for changes that occur on the server, +// beginning after the provided resource version. +type Watchable interface { + Watch(resourceVersion string) (watch.Interface, error) +} + +// ResourceMapping allows an object to return the resource mapping associated with +// the resource or resources it represents. +type ResourceMapping interface { + ResourceMapping() *meta.RESTMapping +} + +// Info contains temporary info to execute a REST call, or show the results +// of an already completed REST call. +type Info struct { + Client RESTClient + Mapping *meta.RESTMapping + Namespace string + Name string + + // Optional, this is the most recent value returned by the server if available + runtime.Object + // Optional, this is the most recent resource version the server knows about for + // this type of resource. It may not match the resource version of the object, + // but if set it should be equal to or newer than the resource version of the + // object (however the server defines resource version). + ResourceVersion string +} + +// NewInfo returns a new info object +func NewInfo(client RESTClient, mapping *meta.RESTMapping, namespace, name string) *Info { + return &Info{ + Client: client, + Mapping: mapping, + Namespace: namespace, + Name: name, + } +} + +// Visit implements Visitor +func (i *Info) Visit(fn VisitorFunc) error { + return fn(i) +} + +func (i *Info) Get() error { + obj, err := NewHelper(i.Client, i.Mapping).Get(i.Namespace, i.Name) + if err != nil { + return err + } + i.Object = obj + i.ResourceVersion, _ = i.Mapping.MetadataAccessor.ResourceVersion(obj) + return nil +} + +// Watch returns server changes to this object after it was retrieved. +func (i *Info) Watch(resourceVersion string) (watch.Interface, error) { + return NewHelper(i.Client, i.Mapping).WatchSingle(i.Namespace, i.Name, resourceVersion) +} + +// ResourceMapping returns the mapping for this resource and implements ResourceMapping +func (i *Info) ResourceMapping() *meta.RESTMapping { + return i.Mapping +} + +// VisitorList implements Visit for the sub visitors it contains. The first error +// returned from a child Visitor will terminate iteration. +type VisitorList []Visitor + +// Visit implements Visitor +func (l VisitorList) Visit(fn VisitorFunc) error { + for i := range l { + if err := l[i].Visit(fn); err != nil { + return err + } + } + return nil +} + +// EagerVisitorList implements Visit for the sub visitors it contains. All errors +// will be captured and returned at the end of iteration. +type EagerVisitorList []Visitor + +// Visit implements Visitor, and gathers errors that occur during processing until +// all sub visitors have been visited. +func (l EagerVisitorList) Visit(fn VisitorFunc) error { + errs := []error(nil) + for i := range l { + if err := l[i].Visit(func(info *Info) error { + if err := fn(info); err != nil { + errs = append(errs, err) + } + return nil + }); err != nil { + errs = append(errs, err) + } + } + return errors.NewAggregate(errs) +} + +// PathVisitor visits a given path and returns an object representing the file +// at that path. +type PathVisitor struct { + *Mapper + // The file path to load + Path string + // Whether to ignore files that are not recognized as API objects + IgnoreErrors bool +} + +func (v *PathVisitor) Visit(fn VisitorFunc) error { + data, err := ioutil.ReadFile(v.Path) + if err != nil { + return fmt.Errorf("unable to read %q: %v", v.Path, err) + } + info, err := v.Mapper.InfoForData(data, v.Path) + if err != nil { + if v.IgnoreErrors { + return err + } + glog.V(2).Infof("Unable to load file %q: %v", v.Path, err) + return nil + } + return fn(info) +} + +// DirectoryVisitor loads the specified files from a directory and passes them +// to visitors. +type DirectoryVisitor struct { + *Mapper + // The directory or file to start from + Path string + // Whether directories are recursed + Recursive bool + // The file extensions to include. If empty, all files are read. + Extensions []string + // Whether to ignore files that are not recognized as API objects + IgnoreErrors bool +} + +func (v *DirectoryVisitor) ignoreFile(path string) bool { + if len(v.Extensions) == 0 { + return false + } + ext := filepath.Ext(path) + for _, s := range v.Extensions { + if s == ext { + return false + } + } + return true +} + +func (v *DirectoryVisitor) Visit(fn VisitorFunc) error { + return filepath.Walk(v.Path, func(path string, fi os.FileInfo, err error) error { + if err != nil { + return err + } + + if fi.IsDir() { + if path != v.Path && !v.Recursive { + return filepath.SkipDir + } + return nil + } + if v.ignoreFile(path) { + return nil + } + + data, err := ioutil.ReadFile(path) + if err != nil { + return fmt.Errorf("unable to read %q: %v", path, err) + } + info, err := v.Mapper.InfoForData(data, path) + if err != nil { + if v.IgnoreErrors { + return err + } + glog.V(2).Infof("Unable to load file %q: %v", path, err) + return nil + } + return fn(info) + }) +} + +// URLVisitor downloads the contents of a URL, and if successful, returns +// an info object representing the downloaded object. +type URLVisitor struct { + *Mapper + URL *url.URL +} + +func (v *URLVisitor) Visit(fn VisitorFunc) error { + res, err := http.Get(v.URL.String()) + if err != nil { + return fmt.Errorf("unable to access URL %q: %v\n", v.URL, err) + } + defer res.Body.Close() + if res.StatusCode != 200 { + return fmt.Errorf("unable to read URL %q, server reported %d %s", v.URL, res.StatusCode, res.Status) + } + data, err := ioutil.ReadAll(res.Body) + if err != nil { + return fmt.Errorf("unable to read URL %q: %v\n", v.URL, err) + } + info, err := v.Mapper.InfoForData(data, v.URL.String()) + if err != nil { + return err + } + return fn(info) +} + +// DecoratedVisitor will invoke the decorators in order prior to invoking the visitor function +// passed to Visit. An error will terminate the visit. +type DecoratedVisitor struct { + visitor Visitor + decorators []VisitorFunc +} + +// NewDecoratedVisitor will create a visitor that invokes the provided visitor functions before +// the user supplied visitor function is invoked, giving them the opportunity to mutate the Info +// object or terminate early with an error. +func NewDecoratedVisitor(v Visitor, fn ...VisitorFunc) Visitor { + if len(fn) == 0 { + return v + } + return DecoratedVisitor{v, fn} +} + +// Visit implements Visitor +func (v DecoratedVisitor) Visit(fn VisitorFunc) error { + return v.visitor.Visit(func(info *Info) error { + for i := range v.decorators { + if err := v.decorators[i](info); err != nil { + return err + } + } + return fn(info) + }) +} + +// FlattenListVisitor flattens any objects that runtime.ExtractList recognizes as a list +// - has an "Items" public field that is a slice of runtime.Objects or objects satisfying +// that interface - into multiple Infos. An error on any sub item (for instance, if a List +// contains an object that does not have a registered client or resource) will terminate +// the visit. +// TODO: allow errors to be aggregated? +type FlattenListVisitor struct { + Visitor + *Mapper +} + +// NewFlattenListVisitor creates a visitor that will expand list style runtime.Objects +// into individual items and then visit them individually. +func NewFlattenListVisitor(v Visitor, mapper *Mapper) Visitor { + return FlattenListVisitor{v, mapper} +} + +func (v FlattenListVisitor) Visit(fn VisitorFunc) error { + return v.Visitor.Visit(func(info *Info) error { + if info.Object == nil { + return fn(info) + } + items, err := runtime.ExtractList(info.Object) + if err != nil { + return fn(info) + } + for i := range items { + item, err := v.InfoForObject(items[i]) + if err != nil { + return err + } + if len(info.ResourceVersion) != 0 { + item.ResourceVersion = info.ResourceVersion + } + if err := fn(item); err != nil { + return err + } + } + return nil + }) +} + +// StreamVisitor reads objects from an io.Reader and walks them. A stream visitor can only be +// visited once. +// TODO: depends on objects being in JSON format before being passed to decode - need to implement +// a stream decoder method on runtime.Codec to properly handle this. +type StreamVisitor struct { + io.Reader + *Mapper + + Source string + IgnoreErrors bool +} + +// NewStreamVisitor creates a visitor that will return resources that were encoded into the provided +// stream. If ignoreErrors is set, unrecognized or invalid objects will be skipped and logged. +func NewStreamVisitor(r io.Reader, mapper *Mapper, source string, ignoreErrors bool) Visitor { + return &StreamVisitor{r, mapper, source, ignoreErrors} +} + +// Visit implements Visitor over a stream. +func (v *StreamVisitor) Visit(fn VisitorFunc) error { + d := json.NewDecoder(v.Reader) + for { + ext := runtime.RawExtension{} + if err := d.Decode(&ext); err != nil { + if err == io.EOF { + return nil + } + return err + } + info, err := v.InfoForData(ext.RawJSON, v.Source) + if err != nil { + if v.IgnoreErrors { + glog.V(2).Infof("Unable to read item from stream %q: %v", err) + glog.V(4).Infof("Unreadable: %s", string(ext.RawJSON)) + continue + } + return err + } + if err := fn(info); err != nil { + return err + } + } + return nil +} + +func UpdateObjectNamespace(info *Info) error { + if info.Object != nil { + return info.Mapping.MetadataAccessor.SetNamespace(info.Object, info.Namespace) + } + return nil +} + +// SetNamespace ensures that every Info object visited will have a namespace +// set. If info.Object is set, it will be mutated as well. +func SetNamespace(namespace string) VisitorFunc { + return func(info *Info) error { + if len(info.Namespace) == 0 { + info.Namespace = namespace + UpdateObjectNamespace(info) + } + return nil + } +} + +// RequireNamespace will either set a namespace if none is provided on the +// Info object, or if the namespace is set and does not match the provided +// value, returns an error. This is intended to guard against administrators +// accidentally operating on resources outside their namespace. +func RequireNamespace(namespace string) VisitorFunc { + return func(info *Info) error { + if len(info.Namespace) == 0 { + info.Namespace = namespace + UpdateObjectNamespace(info) + return nil + } + if info.Namespace != namespace { + return fmt.Errorf("the namespace from the provided object %q does not match the namespace %q. You must pass '--namespace=%s' to perform this operation.", info.Namespace, namespace, info.Namespace) + } + return nil + } +} + +// RetrieveLatest updates the Object on each Info by invoking a standard client +// Get. +func RetrieveLatest(info *Info) error { + if len(info.Name) == 0 || len(info.Namespace) == 0 { + return nil + } + obj, err := NewHelper(info.Client, info.Mapping).Get(info.Namespace, info.Name) + if err != nil { + return err + } + info.Object = obj + info.ResourceVersion, _ = info.Mapping.MetadataAccessor.ResourceVersion(obj) + return nil +} From a1ee782df5395362c95e63eeb374c3932a7a97c9 Mon Sep 17 00:00:00 2001 From: Clayton Coleman Date: Sat, 27 Dec 2014 23:49:51 -0500 Subject: [PATCH 2/3] Use resource package, delete older code --- pkg/kubectl/cmd/create.go | 4 +- pkg/kubectl/cmd/delete.go | 6 +- pkg/kubectl/cmd/get.go | 11 +- pkg/kubectl/cmd/resource.go | 123 +---------- pkg/kubectl/cmd/update.go | 4 +- pkg/kubectl/resthelper.go | 146 ------------ pkg/kubectl/resthelper_test.go | 393 --------------------------------- 7 files changed, 26 insertions(+), 661 deletions(-) delete mode 100644 pkg/kubectl/resthelper.go delete mode 100644 pkg/kubectl/resthelper_test.go diff --git a/pkg/kubectl/cmd/create.go b/pkg/kubectl/cmd/create.go index f81458a9412..2e6138c4f62 100644 --- a/pkg/kubectl/cmd/create.go +++ b/pkg/kubectl/cmd/create.go @@ -20,7 +20,7 @@ import ( "fmt" "io" - "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl" + "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/resource" "github.com/spf13/cobra" ) @@ -57,7 +57,7 @@ Examples: checkErr(err) } - err = kubectl.NewRESTHelper(client, mapping).Create(namespace, true, data) + err = resource.NewHelper(client, mapping).Create(namespace, true, data) checkErr(err) fmt.Fprintf(out, "%s\n", name) }, diff --git a/pkg/kubectl/cmd/delete.go b/pkg/kubectl/cmd/delete.go index 9d220d6eeac..39e9ea781d7 100644 --- a/pkg/kubectl/cmd/delete.go +++ b/pkg/kubectl/cmd/delete.go @@ -23,7 +23,7 @@ import ( "github.com/golang/glog" "github.com/spf13/cobra" - "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl" + "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/resource" ) func (f *Factory) NewCmdDelete(out io.Writer) *cobra.Command { @@ -59,9 +59,9 @@ Examples: checkErr(err) selector := GetFlagString(cmd, "selector") found := 0 - ResourcesFromArgsOrFile(cmd, args, filename, selector, f.Typer, f.Mapper, f.Client, schema).Visit(func(r *ResourceInfo) error { + ResourcesFromArgsOrFile(cmd, args, filename, selector, f.Typer, f.Mapper, f.Client, schema).Visit(func(r *resource.Info) error { found++ - if err := kubectl.NewRESTHelper(r.Client, r.Mapping).Delete(r.Namespace, r.Name); err != nil { + if err := resource.NewHelper(r.Client, r.Mapping).Delete(r.Namespace, r.Name); err != nil { return err } fmt.Fprintf(out, "%s\n", r.Name) diff --git a/pkg/kubectl/cmd/get.go b/pkg/kubectl/cmd/get.go index f458bd1a3c1..46079a9800d 100644 --- a/pkg/kubectl/cmd/get.go +++ b/pkg/kubectl/cmd/get.go @@ -21,7 +21,9 @@ import ( "io" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl" + "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/resource" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/spf13/cobra" ) @@ -70,8 +72,13 @@ Examples: printer, err := kubectl.GetPrinter(outputFormat, templateFile, outputVersion, mapping.ObjectConvertor, defaultPrinter) checkErr(err) - restHelper := kubectl.NewRESTHelper(client, mapping) - obj, err := restHelper.Get(namespace, name, labelSelector) + restHelper := resource.NewHelper(client, mapping) + var obj runtime.Object + if len(name) == 0 { + obj, err = restHelper.List(namespace, labelSelector) + } else { + obj, err = restHelper.Get(namespace, name) + } checkErr(err) isWatch, isWatchOnly := GetFlagBool(cmd, "watch"), GetFlagBool(cmd, "watch-only") diff --git a/pkg/kubectl/cmd/resource.go b/pkg/kubectl/cmd/resource.go index 9055a308d58..93da904cd80 100644 --- a/pkg/kubectl/cmd/resource.go +++ b/pkg/kubectl/cmd/resource.go @@ -18,122 +18,19 @@ package cmd import ( "fmt" - "log" "strings" - "github.com/golang/glog" "github.com/spf13/cobra" - "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/validation" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl" + "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/resource" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" ) -// ResourceInfo contains temporary info to execute REST call -type ResourceInfo struct { - Client kubectl.RESTClient - Mapping *meta.RESTMapping - Namespace string - Name string - - // Optional, this is the most recent value returned by the server if available - runtime.Object -} - -// ResourceVisitor lets clients walk the list of resources -type ResourceVisitor interface { - Visit(func(*ResourceInfo) error) error -} - -type ResourceVisitorList []ResourceVisitor - -// Visit implements ResourceVisitor -func (l ResourceVisitorList) Visit(fn func(r *ResourceInfo) error) error { - for i := range l { - if err := l[i].Visit(fn); err != nil { - return err - } - } - return nil -} - -func NewResourceInfo(client kubectl.RESTClient, mapping *meta.RESTMapping, namespace, name string) *ResourceInfo { - return &ResourceInfo{ - Client: client, - Mapping: mapping, - Namespace: namespace, - Name: name, - } -} - -// Visit implements ResourceVisitor -func (r *ResourceInfo) Visit(fn func(r *ResourceInfo) error) error { - return fn(r) -} - -// ResourceSelector is a facade for all the resources fetched via label selector -type ResourceSelector struct { - Client kubectl.RESTClient - Mapping *meta.RESTMapping - Namespace string - Selector labels.Selector -} - -// NewResourceSelector creates a resource selector which hides details of getting items by their label selector. -func NewResourceSelector(client kubectl.RESTClient, mapping *meta.RESTMapping, namespace string, selector labels.Selector) *ResourceSelector { - return &ResourceSelector{ - Client: client, - Mapping: mapping, - Namespace: namespace, - Selector: selector, - } -} - -// Visit implements ResourceVisitor -func (r *ResourceSelector) Visit(fn func(r *ResourceInfo) error) error { - list, err := kubectl.NewRESTHelper(r.Client, r.Mapping).List(r.Namespace, r.Selector) - if err != nil { - if errors.IsBadRequest(err) || errors.IsNotFound(err) { - glog.V(2).Infof("Unable to perform a label selector query on %s with labels %s: %v", r.Mapping.Resource, r.Selector, err) - return nil - } - return err - } - items, err := runtime.ExtractList(list) - if err != nil { - return err - } - accessor := meta.NewAccessor() - for i := range items { - name, err := accessor.Name(items[i]) - if err != nil { - // items without names cannot be visited - glog.V(2).Infof("Found %s with labels %s, but can't access the item by name.", r.Mapping.Resource, r.Selector) - continue - } - item := &ResourceInfo{ - Client: r.Client, - Mapping: r.Mapping, - Namespace: r.Namespace, - Name: name, - Object: items[i], - } - if err := fn(item); err != nil { - if errors.IsNotFound(err) { - glog.V(2).Infof("Found %s named %q, but can't be accessed now: %v", r.Mapping.Resource, name, err) - return nil - } - log.Printf("got error for resource %s: %v", r.Mapping.Resource, err) - return err - } - } - return nil -} - // ResourcesFromArgsOrFile computes a list of Resources by extracting info from filename or args. It will // handle label selectors provided. func ResourcesFromArgsOrFile( @@ -144,7 +41,7 @@ func ResourcesFromArgsOrFile( mapper meta.RESTMapper, clientBuilder func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.RESTClient, error), schema validation.Schema, -) ResourceVisitor { +) resource.Visitor { // handling filename & resource id if len(selector) == 0 { @@ -152,34 +49,34 @@ func ResourcesFromArgsOrFile( client, err := clientBuilder(cmd, mapping) checkErr(err) - return NewResourceInfo(client, mapping, namespace, name) + return resource.NewInfo(client, mapping, namespace, name) } labelSelector, err := labels.ParseSelector(selector) checkErr(err) namespace := GetKubeNamespace(cmd) - visitors := ResourceVisitorList{} + visitors := resource.VisitorList{} if len(args) != 1 { usageError(cmd, "Must specify the type of resource") } types := SplitResourceArgument(args[0]) for _, arg := range types { - resource := kubectl.ExpandResourceShortcut(arg) - if len(resource) == 0 { - usageError(cmd, "Unknown resource %s", resource) + resourceName := kubectl.ExpandResourceShortcut(arg) + if len(resourceName) == 0 { + usageError(cmd, "Unknown resource %s", resourceName) } - version, kind, err := mapper.VersionAndKindForResource(resource) + version, kind, err := mapper.VersionAndKindForResource(resourceName) checkErr(err) - mapping, err := mapper.RESTMapping(version, kind) + mapping, err := mapper.RESTMapping(kind, version) checkErr(err) client, err := clientBuilder(cmd, mapping) checkErr(err) - visitors = append(visitors, NewResourceSelector(client, mapping, namespace, labelSelector)) + visitors = append(visitors, resource.NewSelector(client, mapping, namespace, labelSelector)) } return visitors } diff --git a/pkg/kubectl/cmd/update.go b/pkg/kubectl/cmd/update.go index 8f812bc070c..32eac2ab534 100644 --- a/pkg/kubectl/cmd/update.go +++ b/pkg/kubectl/cmd/update.go @@ -20,7 +20,7 @@ import ( "fmt" "io" - "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl" + "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/resource" "github.com/spf13/cobra" ) @@ -52,7 +52,7 @@ Examples: err = CompareNamespaceFromFile(cmd, namespace) checkErr(err) - err = kubectl.NewRESTHelper(client, mapping).Update(namespace, name, true, data) + err = resource.NewHelper(client, mapping).Update(namespace, name, true, data) checkErr(err) fmt.Fprintf(out, "%s\n", name) }, diff --git a/pkg/kubectl/resthelper.go b/pkg/kubectl/resthelper.go deleted file mode 100644 index defe294be0d..00000000000 --- a/pkg/kubectl/resthelper.go +++ /dev/null @@ -1,146 +0,0 @@ -/* -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 kubectl - -import ( - "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" - "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" - "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" - "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" -) - -// RESTHelper provides methods for retrieving or mutating a RESTful -// resource. -type RESTHelper struct { - Resource string - // A RESTClient capable of mutating this resource - RESTClient RESTClient - // A codec for decoding and encoding objects of this resource type. - Codec runtime.Codec - // An interface for reading or writing the resource version of this - // type. - Versioner runtime.ResourceVersioner -} - -// NewRESTHelper creates a RESTHelper from a ResourceMapping -func NewRESTHelper(client RESTClient, mapping *meta.RESTMapping) *RESTHelper { - return &RESTHelper{ - RESTClient: client, - Resource: mapping.Resource, - Codec: mapping.Codec, - Versioner: mapping.MetadataAccessor, - } -} - -func (m *RESTHelper) Get(namespace, name string, selector labels.Selector) (runtime.Object, error) { - return m.RESTClient.Get().Resource(m.Resource).Namespace(namespace).Name(name).SelectorParam("labels", selector).Do().Get() -} - -func (m *RESTHelper) List(namespace string, selector labels.Selector) (runtime.Object, error) { - return m.RESTClient.Get().Resource(m.Resource).Namespace(namespace).SelectorParam("labels", selector).Do().Get() -} - -func (m *RESTHelper) Watch(namespace, resourceVersion string, labelSelector, fieldSelector labels.Selector) (watch.Interface, error) { - return m.RESTClient.Get(). - Prefix("watch"). - Namespace(namespace). - Resource(m.Resource). - Param("resourceVersion", resourceVersion). - SelectorParam("labels", labelSelector). - SelectorParam("fields", fieldSelector). - Watch() -} - -func (m *RESTHelper) Delete(namespace, name string) error { - return m.RESTClient.Delete().Namespace(namespace).Resource(m.Resource).Name(name).Do().Error() -} - -func (m *RESTHelper) Create(namespace string, modify bool, data []byte) error { - if modify { - obj, err := m.Codec.Decode(data) - if err != nil { - // We don't know how to check a version on this object, but create it anyway - return createResource(m.RESTClient, m.Resource, namespace, data) - } - - // Attempt to version the object based on client logic. - version, err := m.Versioner.ResourceVersion(obj) - if err != nil { - // We don't know how to clear the version on this object, so send it to the server as is - return createResource(m.RESTClient, m.Resource, namespace, data) - } - if version != "" { - if err := m.Versioner.SetResourceVersion(obj, ""); err != nil { - return err - } - newData, err := m.Codec.Encode(obj) - if err != nil { - return err - } - data = newData - } - } - - return createResource(m.RESTClient, m.Resource, namespace, data) -} - -func createResource(c RESTClient, resource, namespace string, data []byte) error { - return c.Post().Namespace(namespace).Resource(resource).Body(data).Do().Error() -} - -func (m *RESTHelper) Update(namespace, name string, overwrite bool, data []byte) error { - c := m.RESTClient - - obj, err := m.Codec.Decode(data) - if err != nil { - // We don't know how to handle this object, but update it anyway - return updateResource(c, m.Resource, namespace, name, data) - } - - // Attempt to version the object based on client logic. - version, err := m.Versioner.ResourceVersion(obj) - if err != nil { - // We don't know how to version this object, so send it to the server as is - return updateResource(c, m.Resource, namespace, name, data) - } - if version == "" && overwrite { - // Retrieve the current version of the object to overwrite the server object - serverObj, err := c.Get().Resource(m.Resource).Name(name).Do().Get() - if err != nil { - // The object does not exist, but we want it to be created - return updateResource(c, m.Resource, namespace, name, data) - } - serverVersion, err := m.Versioner.ResourceVersion(serverObj) - if err != nil { - return err - } - if err := m.Versioner.SetResourceVersion(obj, serverVersion); err != nil { - return err - } - newData, err := m.Codec.Encode(obj) - if err != nil { - return err - } - data = newData - } - - return updateResource(c, m.Resource, namespace, name, data) -} - -func updateResource(c RESTClient, resource, namespace, name string, data []byte) error { - return c.Put().Namespace(namespace).Resource(resource).Name(name).Body(data).Do().Error() -} diff --git a/pkg/kubectl/resthelper_test.go b/pkg/kubectl/resthelper_test.go deleted file mode 100644 index 94db10f48f3..00000000000 --- a/pkg/kubectl/resthelper_test.go +++ /dev/null @@ -1,393 +0,0 @@ -/* -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 kubectl - -import ( - "bytes" - "errors" - "io" - "io/ioutil" - "net/http" - "reflect" - "strings" - "testing" - - "github.com/GoogleCloudPlatform/kubernetes/pkg/api" - "github.com/GoogleCloudPlatform/kubernetes/pkg/api/testapi" - "github.com/GoogleCloudPlatform/kubernetes/pkg/client" - "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" - "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" -) - -func objBody(obj runtime.Object) io.ReadCloser { - return ioutil.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(testapi.Codec(), obj)))) -} - -// splitPath returns the segments for a URL path. -func splitPath(path string) []string { - path = strings.Trim(path, "/") - if path == "" { - return []string{} - } - return strings.Split(path, "/") -} - -func TestRESTHelperDelete(t *testing.T) { - tests := []struct { - Err bool - Req func(*http.Request) bool - Resp *http.Response - HttpErr error - }{ - { - HttpErr: errors.New("failure"), - Err: true, - }, - { - Resp: &http.Response{ - StatusCode: http.StatusNotFound, - Body: objBody(&api.Status{Status: api.StatusFailure}), - }, - Err: true, - }, - { - Resp: &http.Response{ - StatusCode: http.StatusOK, - Body: objBody(&api.Status{Status: api.StatusSuccess}), - }, - Req: func(req *http.Request) bool { - if req.Method != "DELETE" { - t.Errorf("unexpected method: %#v", req) - return false - } - parts := splitPath(req.URL.Path) - if parts[1] != "bar" { - t.Errorf("url doesn't contain namespace: %#v", req) - return false - } - if parts[2] != "foo" { - t.Errorf("url doesn't contain name: %#v", req) - return false - } - return true - }, - }, - } - for _, test := range tests { - client := &client.FakeRESTClient{ - Codec: testapi.Codec(), - Resp: test.Resp, - Err: test.HttpErr, - } - modifier := &RESTHelper{ - RESTClient: client, - } - err := modifier.Delete("bar", "foo") - if (err != nil) != test.Err { - t.Errorf("unexpected error: %t %v", test.Err, err) - } - if err != nil { - continue - } - if test.Req != nil && !test.Req(client.Req) { - t.Errorf("unexpected request: %#v", client.Req) - } - } -} - -func TestRESTHelperCreate(t *testing.T) { - expectPost := func(req *http.Request) bool { - if req.Method != "POST" { - t.Errorf("unexpected method: %#v", req) - return false - } - parts := splitPath(req.URL.Path) - if parts[1] != "bar" { - t.Errorf("url doesn't contain namespace: %#v", req) - return false - } - return true - } - - tests := []struct { - Resp *http.Response - RespFunc client.HTTPClientFunc - HttpErr error - Modify bool - Object runtime.Object - - ExpectObject runtime.Object - Err bool - Req func(*http.Request) bool - }{ - { - HttpErr: errors.New("failure"), - Err: true, - }, - { - Resp: &http.Response{ - StatusCode: http.StatusNotFound, - Body: objBody(&api.Status{Status: api.StatusFailure}), - }, - Err: true, - }, - { - Resp: &http.Response{ - StatusCode: http.StatusOK, - Body: objBody(&api.Status{Status: api.StatusSuccess}), - }, - Object: &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo"}}, - ExpectObject: &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo"}}, - Req: expectPost, - }, - { - Modify: false, - Object: &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo", ResourceVersion: "10"}}, - ExpectObject: &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo", ResourceVersion: "10"}}, - Resp: &http.Response{StatusCode: http.StatusOK, Body: objBody(&api.Status{Status: api.StatusSuccess})}, - Req: expectPost, - }, - { - Modify: true, - Object: &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo", ResourceVersion: "10"}}, - ExpectObject: &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo"}}, - Resp: &http.Response{StatusCode: http.StatusOK, Body: objBody(&api.Status{Status: api.StatusSuccess})}, - Req: expectPost, - }, - } - for i, test := range tests { - client := &client.FakeRESTClient{ - Codec: testapi.Codec(), - Resp: test.Resp, - Err: test.HttpErr, - } - if test.RespFunc != nil { - client.Client = test.RespFunc - } - modifier := &RESTHelper{ - RESTClient: client, - Codec: testapi.Codec(), - Versioner: testapi.MetadataAccessor(), - } - data := []byte{} - if test.Object != nil { - data = []byte(runtime.EncodeOrDie(testapi.Codec(), test.Object)) - } - err := modifier.Create("bar", test.Modify, data) - if (err != nil) != test.Err { - t.Errorf("%d: unexpected error: %t %v", i, test.Err, err) - } - if err != nil { - continue - } - if test.Req != nil && !test.Req(client.Req) { - t.Errorf("%d: unexpected request: %#v", i, client.Req) - } - body, err := ioutil.ReadAll(client.Req.Body) - if err != nil { - t.Fatalf("%d: unexpected error: %#v", i, err) - } - t.Logf("got body: %s", string(body)) - expect := []byte{} - if test.ExpectObject != nil { - expect = []byte(runtime.EncodeOrDie(testapi.Codec(), test.ExpectObject)) - } - if !reflect.DeepEqual(expect, body) { - t.Errorf("%d: unexpected body: %s", i, string(body)) - } - - } -} - -func TestRESTHelperGet(t *testing.T) { - tests := []struct { - Err bool - Req func(*http.Request) bool - Resp *http.Response - HttpErr error - }{ - { - HttpErr: errors.New("failure"), - Err: true, - }, - { - Resp: &http.Response{ - StatusCode: http.StatusNotFound, - Body: objBody(&api.Status{Status: api.StatusFailure}), - }, - Err: true, - }, - { - Resp: &http.Response{ - StatusCode: http.StatusOK, - Body: objBody(&api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo"}}), - }, - Req: func(req *http.Request) bool { - if req.Method != "GET" { - t.Errorf("unexpected method: %#v", req) - return false - } - parts := splitPath(req.URL.Path) - if parts[1] != "bar" { - t.Errorf("url doesn't contain namespace: %#v", req) - return false - } - if parts[2] != "foo" { - t.Errorf("url doesn't contain name: %#v", req) - return false - } - return true - }, - }, - } - for _, test := range tests { - client := &client.FakeRESTClient{ - Codec: testapi.Codec(), - Resp: test.Resp, - Err: test.HttpErr, - } - modifier := &RESTHelper{ - RESTClient: client, - } - obj, err := modifier.Get("bar", "foo", labels.Everything()) - if (err != nil) != test.Err { - t.Errorf("unexpected error: %t %v", test.Err, err) - } - if err != nil { - continue - } - if obj.(*api.Pod).Name != "foo" { - t.Errorf("unexpected object: %#v", obj) - } - if test.Req != nil && !test.Req(client.Req) { - t.Errorf("unexpected request: %#v", client.Req) - } - } -} - -func TestRESTHelperUpdate(t *testing.T) { - expectPut := func(req *http.Request) bool { - if req.Method != "PUT" { - t.Errorf("unexpected method: %#v", req) - return false - } - parts := splitPath(req.URL.Path) - if parts[1] != "bar" { - t.Errorf("url doesn't contain namespace: %#v", req.URL) - return false - } - if parts[2] != "foo" { - t.Errorf("url doesn't contain name: %#v", req) - return false - } - return true - } - - tests := []struct { - Resp *http.Response - RespFunc client.HTTPClientFunc - HttpErr error - Overwrite bool - Object runtime.Object - - ExpectObject runtime.Object - Err bool - Req func(*http.Request) bool - }{ - { - HttpErr: errors.New("failure"), - Err: true, - }, - { - Object: &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo"}}, - Resp: &http.Response{ - StatusCode: http.StatusNotFound, - Body: objBody(&api.Status{Status: api.StatusFailure}), - }, - Err: true, - }, - { - Object: &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo"}}, - ExpectObject: &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo"}}, - Resp: &http.Response{ - StatusCode: http.StatusOK, - Body: objBody(&api.Status{Status: api.StatusSuccess}), - }, - Req: expectPut, - }, - { - Object: &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo"}}, - ExpectObject: &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo", ResourceVersion: "10"}}, - - Overwrite: true, - RespFunc: func(req *http.Request) (*http.Response, error) { - if req.Method == "PUT" { - return &http.Response{StatusCode: http.StatusOK, Body: objBody(&api.Status{Status: api.StatusSuccess})}, nil - } - return &http.Response{StatusCode: http.StatusOK, Body: objBody(&api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo", ResourceVersion: "10"}})}, nil - }, - Req: expectPut, - }, - { - Object: &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo", ResourceVersion: "10"}}, - ExpectObject: &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo", ResourceVersion: "10"}}, - Resp: &http.Response{StatusCode: http.StatusOK, Body: objBody(&api.Status{Status: api.StatusSuccess})}, - Req: expectPut, - }, - } - for i, test := range tests { - client := &client.FakeRESTClient{ - Codec: testapi.Codec(), - Resp: test.Resp, - Err: test.HttpErr, - } - if test.RespFunc != nil { - client.Client = test.RespFunc - } - modifier := &RESTHelper{ - RESTClient: client, - Codec: testapi.Codec(), - Versioner: testapi.MetadataAccessor(), - } - data := []byte{} - if test.Object != nil { - data = []byte(runtime.EncodeOrDie(testapi.Codec(), test.Object)) - } - err := modifier.Update("bar", "foo", test.Overwrite, data) - if (err != nil) != test.Err { - t.Errorf("%d: unexpected error: %t %v", i, test.Err, err) - } - if err != nil { - continue - } - if test.Req != nil && !test.Req(client.Req) { - t.Errorf("%d: unexpected request: %#v", i, client.Req) - } - body, err := ioutil.ReadAll(client.Req.Body) - if err != nil { - t.Fatalf("%d: unexpected error: %#v", i, err) - } - t.Logf("got body: %s", string(body)) - expect := []byte{} - if test.ExpectObject != nil { - expect = []byte(runtime.EncodeOrDie(testapi.Codec(), test.ExpectObject)) - } - if !reflect.DeepEqual(expect, body) { - t.Errorf("%d: unexpected body: %s", i, string(body)) - } - } -} From 8a4f2259413bbf24d2cb75afff5baf5c0e3c8f18 Mon Sep 17 00:00:00 2001 From: Clayton Coleman Date: Sun, 28 Dec 2014 00:27:52 -0500 Subject: [PATCH 3/3] Make expandResourceShortcuts part of RESTMapper on client --- cmd/kubectl/kubectl.go | 10 +- pkg/kubectl/cmd/cmd.go | 204 ++++++++++++++++++++++--------- pkg/kubectl/cmd/cmd_test.go | 2 +- pkg/kubectl/cmd/create.go | 2 +- pkg/kubectl/cmd/createall.go | 2 +- pkg/kubectl/cmd/delete.go | 2 +- pkg/kubectl/cmd/get.go | 2 +- pkg/kubectl/cmd/log.go | 6 +- pkg/kubectl/cmd/proxy.go | 2 +- pkg/kubectl/cmd/resource.go | 8 +- pkg/kubectl/cmd/rollingupdate.go | 5 +- pkg/kubectl/cmd/update.go | 2 +- pkg/kubectl/cmd/version.go | 14 +-- pkg/kubectl/kubectl.go | 20 ++- 14 files changed, 185 insertions(+), 96 deletions(-) diff --git a/cmd/kubectl/kubectl.go b/cmd/kubectl/kubectl.go index 2f96c671963..5098d47454c 100644 --- a/cmd/kubectl/kubectl.go +++ b/cmd/kubectl/kubectl.go @@ -19,11 +19,15 @@ package main import ( "os" - "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd" + + "github.com/golang/glog" ) func main() { - clientBuilder := clientcmd.NewInteractiveClientConfig(clientcmd.Config{}, "", &clientcmd.ConfigOverrides{}, os.Stdin) - cmd.NewFactory(clientBuilder).Run(os.Stdout) + cmd := cmd.NewFactory().NewKubectlCommand(os.Stdout) + if err := cmd.Execute(); err != nil { + glog.Errorf("error: %v", err) + os.Exit(1) + } } diff --git a/pkg/kubectl/cmd/cmd.go b/pkg/kubectl/cmd/cmd.go index 71db26a8d83..f2fd6fb945d 100644 --- a/pkg/kubectl/cmd/cmd.go +++ b/pkg/kubectl/cmd/cmd.go @@ -33,6 +33,7 @@ import ( "github.com/golang/glog" "github.com/spf13/cobra" + "github.com/spf13/pflag" ) const ( @@ -42,55 +43,108 @@ const ( // Factory provides abstractions that allow the Kubectl command to be extended across multiple types // of resources and different API sets. type Factory struct { - ClientConfig clientcmd.ClientConfig - Mapper meta.RESTMapper - Typer runtime.ObjectTyper - Client func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.RESTClient, error) - Describer func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.Describer, error) - Printer func(cmd *cobra.Command, mapping *meta.RESTMapping, noHeaders bool) (kubectl.ResourcePrinter, error) - Validator func(*cobra.Command) (validation.Schema, error) + clients *clientCache + flags *pflag.FlagSet + + Mapper meta.RESTMapper + Typer runtime.ObjectTyper + + // Returns a client for accessing Kubernetes resources or an error. + Client func(cmd *cobra.Command) (*client.Client, error) + // Returns a client.Config for accessing the Kubernetes server. + ClientConfig func(cmd *cobra.Command) (*client.Config, error) + // Returns a RESTClient for working with the specified RESTMapping or an error. This is intended + // for working with arbitrary resources and is not guaranteed to point to a Kubernetes APIServer. + RESTClient func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.RESTClient, error) + // Returns a Describer for displaying the specified RESTMapping type or an error. + Describer func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.Describer, error) + // Returns a Printer for formatting objects of the given type or an error. + Printer func(cmd *cobra.Command, mapping *meta.RESTMapping, noHeaders bool) (kubectl.ResourcePrinter, error) + // Returns a schema that can validate objects stored on disk. + Validator func(*cobra.Command) (validation.Schema, error) } // NewFactory creates a factory with the default Kubernetes resources defined -func NewFactory(clientConfig clientcmd.ClientConfig) *Factory { - ret := &Factory{ - ClientConfig: clientConfig, - Mapper: latest.RESTMapper, - Typer: api.Scheme, - Printer: func(cmd *cobra.Command, mapping *meta.RESTMapping, noHeaders bool) (kubectl.ResourcePrinter, error) { - return kubectl.NewHumanReadablePrinter(noHeaders), nil - }, +func NewFactory() *Factory { + mapper := kubectl.ShortcutExpander{latest.RESTMapper} + + flags := pflag.NewFlagSet("", pflag.ContinueOnError) + clientConfig := DefaultClientConfig(flags) + clients := &clientCache{ + clients: make(map[string]*client.Client), + loader: clientConfig, } - ret.Validator = func(cmd *cobra.Command) (validation.Schema, error) { - if GetFlagBool(cmd, "validate") { - client, err := getClient(ret.ClientConfig, GetFlagBool(cmd, FlagMatchBinaryVersion)) + return &Factory{ + clients: clients, + flags: flags, + + Mapper: mapper, + Typer: api.Scheme, + + Client: func(cmd *cobra.Command) (*client.Client, error) { + return clients.ClientForVersion("") + }, + ClientConfig: func(cmd *cobra.Command) (*client.Config, error) { + return clients.ClientConfigForVersion("") + }, + RESTClient: func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.RESTClient, error) { + client, err := clients.ClientForVersion(mapping.APIVersion) if err != nil { return nil, err } - return &clientSwaggerSchema{client, api.Scheme}, nil - } else { + return client.RESTClient, nil + }, + Describer: func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.Describer, error) { + client, err := clients.ClientForVersion(mapping.APIVersion) + if err != nil { + return nil, err + } + describer, ok := kubectl.DescriberFor(mapping.Kind, client) + if !ok { + return nil, fmt.Errorf("no description has been implemented for %q", mapping.Kind) + } + return describer, nil + }, + Printer: func(cmd *cobra.Command, mapping *meta.RESTMapping, noHeaders bool) (kubectl.ResourcePrinter, error) { + return kubectl.NewHumanReadablePrinter(noHeaders), nil + }, + Validator: func(cmd *cobra.Command) (validation.Schema, error) { + if GetFlagBool(cmd, "validate") { + client, err := clients.ClientForVersion("") + if err != nil { + return nil, err + } + return &clientSwaggerSchema{client, api.Scheme}, nil + } return validation.NullSchema{}, nil - } + }, } - ret.Client = func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.RESTClient, error) { - return getClient(ret.ClientConfig, GetFlagBool(cmd, FlagMatchBinaryVersion)) - } - ret.Describer = func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.Describer, error) { - client, err := getClient(ret.ClientConfig, GetFlagBool(cmd, FlagMatchBinaryVersion)) - if err != nil { - return nil, err - } - describer, ok := kubectl.DescriberFor(mapping.Kind, client) - if !ok { - return nil, fmt.Errorf("no description has been implemented for %q", mapping.Kind) - } - return describer, nil - } - return ret } -func (f *Factory) Run(out io.Writer) { +// BindFlags adds any flags that are common to all kubectl sub commands. +func (f *Factory) BindFlags(flags *pflag.FlagSet) { + // any flags defined by external projects (not part of pflags) + util.AddAllFlagsToPFlagSet(flags) + + if f.flags != nil { + f.flags.VisitAll(func(flag *pflag.Flag) { + flags.AddFlag(flag) + }) + } + + // Globally persistent flags across all subcommands. + // TODO Change flag names to consts to allow safer lookup from subcommands. + // TODO Add a verbose flag that turns on glog logging. Probably need a way + // to do that automatically for every subcommand. + flags.BoolVar(&f.clients.matchVersion, FlagMatchBinaryVersion, false, "Require server version to match client version") + flags.String("ns-path", os.Getenv("HOME")+"/.kubernetes_ns", "Path to the namespace info file that holds the namespace context to use for CLI requests.") + flags.StringP("namespace", "n", "", "If present, the namespace scope for this CLI request.") + flags.Bool("validate", false, "If true, use a schema to validate the input before sending it") +} + +// NewKubectlCommand creates the `kubectl` command and its nested children. +func (f *Factory) NewKubectlCommand(out io.Writer) *cobra.Command { // Parent command to which all subcommands are added. cmds := &cobra.Command{ Use: "kubectl", @@ -101,15 +155,7 @@ Find more information at https://github.com/GoogleCloudPlatform/kubernetes.`, Run: runHelp, } - util.AddAllFlagsToPFlagSet(cmds.PersistentFlags()) - f.ClientConfig = getClientConfig(cmds) - - // Globally persistent flags across all subcommands. - // TODO Change flag names to consts to allow safer lookup from subcommands. - cmds.PersistentFlags().Bool(FlagMatchBinaryVersion, false, "Require server version to match client version") - cmds.PersistentFlags().String("ns-path", os.Getenv("HOME")+"/.kubernetes_ns", "Path to the namespace info file that holds the namespace context to use for CLI requests.") - cmds.PersistentFlags().StringP("namespace", "n", "", "If present, the namespace scope for this CLI request.") - cmds.PersistentFlags().Bool("validate", false, "If true, use a schema to validate the input before sending it") + f.BindFlags(cmds.PersistentFlags()) cmds.AddCommand(f.NewCmdVersion(out)) cmds.AddCommand(f.NewCmdProxy(out)) @@ -125,12 +171,10 @@ Find more information at https://github.com/GoogleCloudPlatform/kubernetes.`, cmds.AddCommand(f.NewCmdLog(out)) cmds.AddCommand(f.NewCmdRollingUpdate(out)) - if err := cmds.Execute(); err != nil { - os.Exit(1) - } + return cmds } -// getClientBuilder creates a clientcmd.ClientConfig that has a hierarchy like this: +// DefaultClientConfig creates a clientcmd.ClientConfig with the following hierarchy: // 1. Use the kubeconfig builder. The number of merges and overrides here gets a little crazy. Stay with me. // 1. Merge together the kubeconfig itself. This is done with the following hierarchy and merge rules: // 1. CommandLineLocation - this parsed from the command line, so it must be late bound @@ -162,13 +206,13 @@ Find more information at https://github.com/GoogleCloudPlatform/kubernetes.`, // 2. If the command line does not specify one, and the auth info has conflicting techniques, fail. // 3. If the command line specifies one and the auth info specifies another, honor the command line technique. // 2. Use default values and potentially prompt for auth information -func getClientConfig(cmd *cobra.Command) clientcmd.ClientConfig { +func DefaultClientConfig(flags *pflag.FlagSet) clientcmd.ClientConfig { loadingRules := clientcmd.NewClientConfigLoadingRules() loadingRules.EnvVarPath = os.Getenv(clientcmd.RecommendedConfigPathEnvVar) - cmd.PersistentFlags().StringVar(&loadingRules.CommandLinePath, "kubeconfig", "", "Path to the kubeconfig file to use for CLI requests.") + flags.StringVar(&loadingRules.CommandLinePath, "kubeconfig", "", "Path to the kubeconfig file to use for CLI requests.") overrides := &clientcmd.ConfigOverrides{} - overrides.BindFlags(cmd.PersistentFlags(), clientcmd.RecommendedConfigOverrideFlags("")) + overrides.BindFlags(flags, clientcmd.RecommendedConfigOverrideFlags("")) clientConfig := clientcmd.NewInteractiveDeferredLoadingClientConfig(loadingRules, overrides, os.Stdin) return clientConfig @@ -246,18 +290,55 @@ func (c *clientSwaggerSchema) ValidateBytes(data []byte) error { return schema.ValidateBytes(data) } -// TODO Need to only run server version match once per client host creation -func getClient(clientConfig clientcmd.ClientConfig, matchServerVersion bool) (*client.Client, error) { - config, err := clientConfig.ClientConfig() +// clientCache caches previously loaded clients for reuse, and ensures MatchServerVersion +// is invoked only once +type clientCache struct { + loader clientcmd.ClientConfig + clients map[string]*client.Client + defaultConfig *client.Config + matchVersion bool +} + +// ClientConfigForVersion returns the correct config for a server +func (c *clientCache) ClientConfigForVersion(version string) (*client.Config, error) { + if c.defaultConfig == nil { + config, err := c.loader.ClientConfig() + if err != nil { + return nil, err + } + c.defaultConfig = config + + if c.matchVersion { + if err := client.MatchesServerVersion(config); err != nil { + return nil, err + } + } + } + + // TODO: remove when SetKubernetesDefaults gets added + if len(version) == 0 { + version = c.defaultConfig.Version + } + + // TODO: have a better config copy method + config := *c.defaultConfig + + // TODO: call new client.SetKubernetesDefaults method + // instead of doing this + config.Version = version + return &config, nil +} + +// ClientForVersion initializes or reuses a client for the specified version, or returns an +// error if that is not possible +func (c *clientCache) ClientForVersion(version string) (*client.Client, error) { + config, err := c.ClientConfigForVersion(version) if err != nil { return nil, err } - if matchServerVersion { - err := client.MatchesServerVersion(config) - if err != nil { - return nil, err - } + if client, ok := c.clients[config.Version]; ok { + return client, nil } client, err := client.New(config) @@ -265,5 +346,6 @@ func getClient(clientConfig clientcmd.ClientConfig, matchServerVersion bool) (*c return nil, err } + c.clients[config.Version] = client return client, nil } diff --git a/pkg/kubectl/cmd/cmd_test.go b/pkg/kubectl/cmd/cmd_test.go index 2bbb8f5199e..2bc3db5c143 100644 --- a/pkg/kubectl/cmd/cmd_test.go +++ b/pkg/kubectl/cmd/cmd_test.go @@ -100,7 +100,7 @@ func NewTestFactory() (*Factory, *testFactory, runtime.Codec) { return &Factory{ Mapper: mapper, Typer: scheme, - Client: func(*cobra.Command, *meta.RESTMapping) (kubectl.RESTClient, error) { + RESTClient: func(*cobra.Command, *meta.RESTMapping) (kubectl.RESTClient, error) { return t.Client, t.Err }, Describer: func(*cobra.Command, *meta.RESTMapping) (kubectl.Describer, error) { diff --git a/pkg/kubectl/cmd/create.go b/pkg/kubectl/cmd/create.go index 2e6138c4f62..6d5502550d3 100644 --- a/pkg/kubectl/cmd/create.go +++ b/pkg/kubectl/cmd/create.go @@ -46,7 +46,7 @@ Examples: schema, err := f.Validator(cmd) checkErr(err) mapping, namespace, name, data := ResourceFromFile(cmd, filename, f.Typer, f.Mapper, schema) - client, err := f.Client(cmd, mapping) + client, err := f.RESTClient(cmd, mapping) checkErr(err) // use the default namespace if not specified, or check for conflict with the file's namespace diff --git a/pkg/kubectl/cmd/createall.go b/pkg/kubectl/cmd/createall.go index b1ea7bb2d9a..694fdf9d542 100644 --- a/pkg/kubectl/cmd/createall.go +++ b/pkg/kubectl/cmd/createall.go @@ -79,7 +79,7 @@ Examples: `, Run: func(cmd *cobra.Command, args []string) { clientFunc := func(mapper *meta.RESTMapping) (config.RESTClientPoster, error) { - client, err := f.Client(cmd, mapper) + client, err := f.RESTClient(cmd, mapper) checkErr(err) return client, nil } diff --git a/pkg/kubectl/cmd/delete.go b/pkg/kubectl/cmd/delete.go index 39e9ea781d7..a6e408a52e3 100644 --- a/pkg/kubectl/cmd/delete.go +++ b/pkg/kubectl/cmd/delete.go @@ -59,7 +59,7 @@ Examples: checkErr(err) selector := GetFlagString(cmd, "selector") found := 0 - ResourcesFromArgsOrFile(cmd, args, filename, selector, f.Typer, f.Mapper, f.Client, schema).Visit(func(r *resource.Info) error { + ResourcesFromArgsOrFile(cmd, args, filename, selector, f.Typer, f.Mapper, f.RESTClient, schema).Visit(func(r *resource.Info) error { found++ if err := resource.NewHelper(r.Client, r.Mapping).Delete(r.Namespace, r.Name); err != nil { return err diff --git a/pkg/kubectl/cmd/get.go b/pkg/kubectl/cmd/get.go index 46079a9800d..014a6165769 100644 --- a/pkg/kubectl/cmd/get.go +++ b/pkg/kubectl/cmd/get.go @@ -56,7 +56,7 @@ Examples: labelSelector, err := labels.ParseSelector(selector) checkErr(err) - client, err := f.Client(cmd, mapping) + client, err := f.RESTClient(cmd, mapping) checkErr(err) outputFormat := GetFlagString(cmd, "output") diff --git a/pkg/kubectl/cmd/log.go b/pkg/kubectl/cmd/log.go index 4b4eeea0316..1e88248e2f4 100644 --- a/pkg/kubectl/cmd/log.go +++ b/pkg/kubectl/cmd/log.go @@ -21,8 +21,6 @@ import ( "strconv" "github.com/spf13/cobra" - - "github.com/GoogleCloudPlatform/kubernetes/pkg/client" ) func (f *Factory) NewCmdLog(out io.Writer) *cobra.Command { @@ -46,9 +44,7 @@ Examples: } namespace := GetKubeNamespace(cmd) - config, err := f.ClientConfig.ClientConfig() - checkErr(err) - client, err := client.New(config) + client, err := f.Client(cmd) checkErr(err) podID := args[0] diff --git a/pkg/kubectl/cmd/proxy.go b/pkg/kubectl/cmd/proxy.go index 85f2e85d75f..f99f2bcee3c 100644 --- a/pkg/kubectl/cmd/proxy.go +++ b/pkg/kubectl/cmd/proxy.go @@ -33,7 +33,7 @@ func (f *Factory) NewCmdProxy(out io.Writer) *cobra.Command { port := GetFlagInt(cmd, "port") glog.Infof("Starting to serve on localhost:%d", port) - clientConfig, err := f.ClientConfig.ClientConfig() + clientConfig, err := f.ClientConfig(cmd) checkErr(err) server, err := kubectl.NewProxyServer(GetFlagString(cmd, "www"), clientConfig, port) diff --git a/pkg/kubectl/cmd/resource.go b/pkg/kubectl/cmd/resource.go index 93da904cd80..afee120932b 100644 --- a/pkg/kubectl/cmd/resource.go +++ b/pkg/kubectl/cmd/resource.go @@ -63,7 +63,7 @@ func ResourcesFromArgsOrFile( } types := SplitResourceArgument(args[0]) for _, arg := range types { - resourceName := kubectl.ExpandResourceShortcut(arg) + resourceName := arg if len(resourceName) == 0 { usageError(cmd, "Unknown resource %s", resourceName) } @@ -91,7 +91,7 @@ func ResourceFromArgsOrFile(cmd *cobra.Command, args []string, filename string, } if len(args) == 2 { - resource := kubectl.ExpandResourceShortcut(args[0]) + resource := args[0] namespace = GetKubeNamespace(cmd) name = args[1] if len(name) == 0 || len(resource) == 0 { @@ -129,7 +129,7 @@ func ResourceFromArgs(cmd *cobra.Command, args []string, mapper meta.RESTMapper) usageError(cmd, "Must provide resource and name command line params") } - resource := kubectl.ExpandResourceShortcut(args[0]) + resource := args[0] namespace = GetKubeNamespace(cmd) name = args[1] if len(name) == 0 || len(resource) == 0 { @@ -152,7 +152,7 @@ func ResourceOrTypeFromArgs(cmd *cobra.Command, args []string, mapper meta.RESTM usageError(cmd, "Must provide resource or a resource and name as command line params") } - resource := kubectl.ExpandResourceShortcut(args[0]) + resource := args[0] if len(resource) == 0 { usageError(cmd, "Must provide resource or a resource and name as command line params") } diff --git a/pkg/kubectl/cmd/rollingupdate.go b/pkg/kubectl/cmd/rollingupdate.go index 8d1e8b88b2a..5a8ed9818a6 100644 --- a/pkg/kubectl/cmd/rollingupdate.go +++ b/pkg/kubectl/cmd/rollingupdate.go @@ -21,7 +21,6 @@ import ( "io" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" - "github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl" "github.com/spf13/cobra" ) @@ -69,9 +68,7 @@ $ cat frontend-v2.json | kubectl rollingupdate frontend-v1 -f - err = CompareNamespaceFromFile(cmd, namespace) checkErr(err) - config, err := f.ClientConfig.ClientConfig() - checkErr(err) - client, err := client.New(config) + client, err := f.Client(cmd) checkErr(err) obj, err := mapping.Codec.Decode(data) diff --git a/pkg/kubectl/cmd/update.go b/pkg/kubectl/cmd/update.go index 32eac2ab534..eb7d05b1bbe 100644 --- a/pkg/kubectl/cmd/update.go +++ b/pkg/kubectl/cmd/update.go @@ -46,7 +46,7 @@ Examples: schema, err := f.Validator(cmd) checkErr(err) mapping, namespace, name, data := ResourceFromFile(cmd, filename, f.Typer, f.Mapper, schema) - client, err := f.Client(cmd, mapping) + client, err := f.RESTClient(cmd, mapping) checkErr(err) err = CompareNamespaceFromFile(cmd, namespace) diff --git a/pkg/kubectl/cmd/version.go b/pkg/kubectl/cmd/version.go index 9dbedc4b0d6..5e434a58e16 100644 --- a/pkg/kubectl/cmd/version.go +++ b/pkg/kubectl/cmd/version.go @@ -21,7 +21,6 @@ import ( "github.com/spf13/cobra" - "github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl" ) @@ -32,14 +31,13 @@ func (f *Factory) NewCmdVersion(out io.Writer) *cobra.Command { Run: func(cmd *cobra.Command, args []string) { if GetFlagBool(cmd, "client") { kubectl.GetClientVersion(out) - } else { - config, err := f.ClientConfig.ClientConfig() - checkErr(err) - client, err := client.New(config) - checkErr(err) - - kubectl.GetVersion(out, client) + return } + + client, err := f.Client(cmd) + checkErr(err) + + kubectl.GetVersion(out, client) }, } cmd.Flags().BoolP("client", "c", false, "Client version only (no server required)") diff --git a/pkg/kubectl/kubectl.go b/pkg/kubectl/kubectl.go index 4a5a1d9ac78..ea09542047d 100644 --- a/pkg/kubectl/kubectl.go +++ b/pkg/kubectl/kubectl.go @@ -26,6 +26,7 @@ import ( "strings" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" "github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" @@ -111,12 +112,23 @@ func makeImageList(spec *api.PodSpec) string { return strings.Join(listOfImages(spec), ",") } -// ExpandResourceShortcut will return the expanded version of resource +// ShortcutExpander is a RESTMapper that can be used for Kubernetes +// resources. +type ShortcutExpander struct { + meta.RESTMapper +} + +// VersionAndKindForResource implements meta.RESTMapper. It expands the resource first, then invokes the wrapped +// mapper. +func (e ShortcutExpander) VersionAndKindForResource(resource string) (defaultVersion, kind string, err error) { + resource = expandResourceShortcut(resource) + return e.RESTMapper.VersionAndKindForResource(resource) +} + +// expandResourceShortcut will return the expanded version of resource // (something that a pkg/api/meta.RESTMapper can understand), if it is // indeed a shortcut. Otherwise, will return resource unmodified. -// TODO: Combine with RESTMapper stuff to provide a general solution -// to this problem. -func ExpandResourceShortcut(resource string) string { +func expandResourceShortcut(resource string) string { shortForms := map[string]string{ "po": "pods", "rc": "replicationcontrollers",