From d75a3d5021411b6c997cccc9ead3711ab4ee59c8 Mon Sep 17 00:00:00 2001 From: Clayton Coleman Date: Sat, 27 Dec 2014 16:48:27 -0500 Subject: [PATCH] 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 +}