From d1ab27762bfae851b836a3632e245245f6ce0fb1 Mon Sep 17 00:00:00 2001 From: Clayton Coleman Date: Wed, 31 Dec 2014 18:35:52 -0500 Subject: [PATCH] Create should be able to accept multiple resources --- pkg/kubectl/cmd/cmd.go | 16 ++--- pkg/kubectl/cmd/cmd_test.go | 31 +++++--- pkg/kubectl/cmd/create.go | 47 +++++++----- pkg/kubectl/cmd/create_test.go | 120 +++++++++++++++++++++++++++++++ pkg/kubectl/cmd/createall.go | 9 +-- pkg/kubectl/cmd/delete.go | 3 +- pkg/kubectl/cmd/describe.go | 3 +- pkg/kubectl/cmd/get.go | 5 +- pkg/kubectl/cmd/resource.go | 3 +- pkg/kubectl/cmd/rollingupdate.go | 3 +- pkg/kubectl/cmd/update.go | 3 +- pkg/kubectl/kubectl.go | 13 ++++ pkg/kubectl/resource/builder.go | 2 + 13 files changed, 212 insertions(+), 46 deletions(-) create mode 100644 pkg/kubectl/cmd/create_test.go diff --git a/pkg/kubectl/cmd/cmd.go b/pkg/kubectl/cmd/cmd.go index 106c6d335ba..9d8efe5a7b1 100644 --- a/pkg/kubectl/cmd/cmd.go +++ b/pkg/kubectl/cmd/cmd.go @@ -48,16 +48,15 @@ type Factory struct { clients *clientCache flags *pflag.FlagSet - Mapper meta.RESTMapper - Typer runtime.ObjectTyper - + // Returns interfaces for dealing with arbitrary runtime.Objects. + Object func(cmd *cobra.Command) (meta.RESTMapper, 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) + RESTClient func(cmd *cobra.Command, mapping *meta.RESTMapping) (resource.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. @@ -81,16 +80,17 @@ func NewFactory() *Factory { clients: clients, flags: flags, - Mapper: mapper, - Typer: api.Scheme, - + Object: func(cmd *cobra.Command) (meta.RESTMapper, runtime.ObjectTyper) { + version := GetFlagString(cmd, "api-version") + return kubectl.OutputVersionMapper{mapper, version}, 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) { + RESTClient: func(cmd *cobra.Command, mapping *meta.RESTMapping) (resource.RESTClient, error) { client, err := clients.ClientForVersion(mapping.APIVersion) if err != nil { return nil, err diff --git a/pkg/kubectl/cmd/cmd_test.go b/pkg/kubectl/cmd/cmd_test.go index d8dea7ff1fb..a28e099291d 100644 --- a/pkg/kubectl/cmd/cmd_test.go +++ b/pkg/kubectl/cmd/cmd_test.go @@ -25,8 +25,10 @@ import ( "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/api/validation" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl" . "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd" + "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/resource" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/spf13/cobra" @@ -93,16 +95,20 @@ type testFactory struct { Client kubectl.RESTClient Describer kubectl.Describer Printer kubectl.ResourcePrinter + Validator validation.Schema Err error } func NewTestFactory() (*Factory, *testFactory, runtime.Codec) { scheme, mapper, codec := newExternalScheme() - t := &testFactory{} + t := &testFactory{ + Validator: validation.NullSchema{}, + } return &Factory{ - Mapper: mapper, - Typer: scheme, - RESTClient: func(*cobra.Command, *meta.RESTMapping) (kubectl.RESTClient, error) { + Object: func(*cobra.Command) (meta.RESTMapper, runtime.ObjectTyper) { + return mapper, scheme + }, + RESTClient: func(*cobra.Command, *meta.RESTMapping) (resource.RESTClient, error) { return t.Client, t.Err }, Describer: func(*cobra.Command, *meta.RESTMapping) (kubectl.Describer, error) { @@ -111,15 +117,21 @@ func NewTestFactory() (*Factory, *testFactory, runtime.Codec) { Printer: func(cmd *cobra.Command, mapping *meta.RESTMapping, noHeaders bool) (kubectl.ResourcePrinter, error) { return t.Printer, t.Err }, + Validator: func(cmd *cobra.Command) (validation.Schema, error) { + return t.Validator, t.Err + }, }, t, codec } func NewAPIFactory() (*Factory, *testFactory, runtime.Codec) { - t := &testFactory{} + t := &testFactory{ + Validator: validation.NullSchema{}, + } return &Factory{ - Mapper: latest.RESTMapper, - Typer: api.Scheme, - RESTClient: func(*cobra.Command, *meta.RESTMapping) (kubectl.RESTClient, error) { + Object: func(*cobra.Command) (meta.RESTMapper, runtime.ObjectTyper) { + return latest.RESTMapper, api.Scheme + }, + RESTClient: func(*cobra.Command, *meta.RESTMapping) (resource.RESTClient, error) { return t.Client, t.Err }, Describer: func(*cobra.Command, *meta.RESTMapping) (kubectl.Describer, error) { @@ -128,6 +140,9 @@ func NewAPIFactory() (*Factory, *testFactory, runtime.Codec) { Printer: func(cmd *cobra.Command, mapping *meta.RESTMapping, noHeaders bool) (kubectl.ResourcePrinter, error) { return t.Printer, t.Err }, + Validator: func(cmd *cobra.Command) (validation.Schema, error) { + return t.Validator, t.Err + }, }, t, latest.Codec } diff --git a/pkg/kubectl/cmd/create.go b/pkg/kubectl/cmd/create.go index 6d5502550d3..8705dda2767 100644 --- a/pkg/kubectl/cmd/create.go +++ b/pkg/kubectl/cmd/create.go @@ -20,11 +20,16 @@ import ( "fmt" "io" - "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/resource" "github.com/spf13/cobra" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/resource" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" ) func (f *Factory) NewCmdCreate(out io.Writer) *cobra.Command { + flags := &struct { + Filenames util.StringList + }{} cmd := &cobra.Command{ Use: "create -f filename", Short: "Create a resource by filename or stdin", @@ -39,29 +44,35 @@ Examples: $ cat pod.json | kubectl create -f - `, Run: func(cmd *cobra.Command, args []string) { - filename := GetFlagString(cmd, "filename") - if len(filename) == 0 { - usageError(cmd, "Must specify filename to create") - } schema, err := f.Validator(cmd) checkErr(err) - mapping, namespace, name, data := ResourceFromFile(cmd, filename, f.Typer, f.Mapper, schema) - client, err := f.RESTClient(cmd, mapping) - checkErr(err) - // use the default namespace if not specified, or check for conflict with the file's namespace - if len(namespace) == 0 { - namespace = GetKubeNamespace(cmd) - } else { - err = CompareNamespaceFromFile(cmd, namespace) - checkErr(err) - } + mapper, typer := f.Object(cmd) + r := resource.NewBuilder(mapper, typer, ClientMapperForCommand(cmd, f)). + ContinueOnError(). + NamespaceParam(GetKubeNamespace(cmd)).RequireNamespace(). + FilenameParam(flags.Filenames...). + Flatten(). + Do() - err = resource.NewHelper(client, mapping).Create(namespace, true, data) + err = r.Visit(func(info *resource.Info) error { + data, err := info.Mapping.Codec.Encode(info.Object) + if err != nil { + return err + } + if err := schema.ValidateBytes(data); err != nil { + return err + } + if err := resource.NewHelper(info.Client, info.Mapping).Create(info.Namespace, true, data); err != nil { + return err + } + // TODO: if generation of names added to server side, change this to use the server's name + fmt.Fprintf(out, "%s\n", info.Name) + return nil + }) checkErr(err) - fmt.Fprintf(out, "%s\n", name) }, } - cmd.Flags().StringP("filename", "f", "", "Filename or URL to file to use to create the resource") + cmd.Flags().VarP(&flags.Filenames, "filename", "f", "Filename, directory, or URL to file to use to create the resource") return cmd } diff --git a/pkg/kubectl/cmd/create_test.go b/pkg/kubectl/cmd/create_test.go new file mode 100644 index 00000000000..7658190ea6c --- /dev/null +++ b/pkg/kubectl/cmd/create_test.go @@ -0,0 +1,120 @@ +/* +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 cmd_test + +import ( + "bytes" + "net/http" + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" +) + +func TestCreateObject(t *testing.T) { + pods, _ := testData() + + f, tf, codec := NewAPIFactory() + tf.Printer = &testPrinter{} + tf.Client = &client.FakeRESTClient{ + Codec: codec, + Client: client.HTTPClientFunc(func(req *http.Request) (*http.Response, error) { + switch p, m := req.URL.Path, req.Method; { + case p == "/ns/test/pods" && m == "POST": + return &http.Response{StatusCode: 201, Body: objBody(codec, &pods.Items[0])}, nil + default: + t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) + return nil, nil + } + }), + } + buf := bytes.NewBuffer([]byte{}) + + cmd := f.NewCmdCreate(buf) + cmd.Flags().String("namespace", "test", "") + cmd.Flags().Set("filename", "../../../examples/guestbook/redis-master.json") + cmd.Run(cmd, []string{}) + + // uses the name from the file, not the response + if buf.String() != "redis-master\n" { + t.Errorf("unexpected output: %s", buf.String()) + } +} + +func TestCreateMultipleObject(t *testing.T) { + pods, svc := testData() + + f, tf, codec := NewAPIFactory() + tf.Printer = &testPrinter{} + tf.Client = &client.FakeRESTClient{ + Codec: codec, + Client: client.HTTPClientFunc(func(req *http.Request) (*http.Response, error) { + switch p, m := req.URL.Path, req.Method; { + case p == "/ns/test/pods" && m == "POST": + return &http.Response{StatusCode: 201, Body: objBody(codec, &pods.Items[0])}, nil + case p == "/ns/test/services" && m == "POST": + return &http.Response{StatusCode: 201, Body: objBody(codec, &svc.Items[0])}, nil + default: + t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) + return nil, nil + } + }), + } + buf := bytes.NewBuffer([]byte{}) + + cmd := f.NewCmdCreate(buf) + cmd.Flags().String("namespace", "test", "") + cmd.Flags().Set("filename", "../../../examples/guestbook/redis-master.json") + cmd.Flags().Set("filename", "../../../examples/guestbook/frontend-service.json") + cmd.Run(cmd, []string{}) + + if buf.String() != "redis-master\nfrontend\n" { + t.Errorf("unexpected output: %s", buf.String()) + } +} + +func TestCreateDirectory(t *testing.T) { + pods, svc := testData() + + f, tf, codec := NewAPIFactory() + tf.Printer = &testPrinter{} + tf.Client = &client.FakeRESTClient{ + Codec: codec, + Client: client.HTTPClientFunc(func(req *http.Request) (*http.Response, error) { + switch p, m := req.URL.Path, req.Method; { + case p == "/ns/test/pods" && m == "POST": + return &http.Response{StatusCode: 201, Body: objBody(codec, &pods.Items[0])}, nil + case p == "/ns/test/services" && m == "POST": + return &http.Response{StatusCode: 201, Body: objBody(codec, &svc.Items[0])}, nil + case p == "/ns/test/replicationControllers" && m == "POST": + return &http.Response{StatusCode: 201, Body: objBody(codec, &svc.Items[0])}, nil + default: + t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) + return nil, nil + } + }), + } + buf := bytes.NewBuffer([]byte{}) + + cmd := f.NewCmdCreate(buf) + cmd.Flags().String("namespace", "test", "") + cmd.Flags().Set("filename", "../../../examples/guestbook") + cmd.Run(cmd, []string{}) + + if buf.String() != "frontendController\nfrontend\nredis-master\nredis-master\nredisSlaveController\nredisslave\n" { + t.Errorf("unexpected output: %s", buf.String()) + } +} diff --git a/pkg/kubectl/cmd/createall.go b/pkg/kubectl/cmd/createall.go index 694fdf9d542..48b33dd3db3 100644 --- a/pkg/kubectl/cmd/createall.go +++ b/pkg/kubectl/cmd/createall.go @@ -78,8 +78,8 @@ Examples: $ cat config.json | kubectl apply -f - `, Run: func(cmd *cobra.Command, args []string) { - clientFunc := func(mapper *meta.RESTMapping) (config.RESTClientPoster, error) { - client, err := f.RESTClient(cmd, mapper) + clientFunc := func(mapping *meta.RESTMapping) (config.RESTClientPoster, error) { + client, err := f.RESTClient(cmd, mapping) checkErr(err) return client, nil } @@ -98,12 +98,13 @@ Examples: files = append(GetFilesFromDir(directory, ".json"), GetFilesFromDir(directory, ".yaml")...) } + mapper, typer := f.Object(cmd) for _, filename := range files { data, err := ReadConfigData(filename) checkErr(err) - items, errs := DataToObjects(f.Mapper, f.Typer, data) - applyErrs := config.CreateObjects(f.Typer, f.Mapper, clientFunc, items) + items, errs := DataToObjects(mapper, typer, data) + applyErrs := config.CreateObjects(typer, mapper, clientFunc, items) errs = append(errs, applyErrs...) if len(errs) > 0 { diff --git a/pkg/kubectl/cmd/delete.go b/pkg/kubectl/cmd/delete.go index ab6bfcd77eb..f7a45dc8a8a 100644 --- a/pkg/kubectl/cmd/delete.go +++ b/pkg/kubectl/cmd/delete.go @@ -59,7 +59,8 @@ Examples: checkErr(err) selector := GetFlagString(cmd, "selector") found := 0 - ResourcesFromArgsOrFile(cmd, args, filename, selector, f.Typer, f.Mapper, f.RESTClient, schema, true).Visit(func(r *resource.Info) error { + mapper, typer := f.Object(cmd) + ResourcesFromArgsOrFile(cmd, args, filename, selector, typer, mapper, f.RESTClient, schema, true).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/describe.go b/pkg/kubectl/cmd/describe.go index b66e8ea6d53..e5d27984ab2 100644 --- a/pkg/kubectl/cmd/describe.go +++ b/pkg/kubectl/cmd/describe.go @@ -32,7 +32,8 @@ func (f *Factory) NewCmdDescribe(out io.Writer) *cobra.Command { This command joins many API calls together to form a detailed description of a given resource.`, Run: func(cmd *cobra.Command, args []string) { - mapping, namespace, name := ResourceFromArgs(cmd, args, f.Mapper) + mapper, _ := f.Object(cmd) + mapping, namespace, name := ResourceFromArgs(cmd, args, mapper) describer, err := f.Describer(cmd, mapping) checkErr(err) diff --git a/pkg/kubectl/cmd/get.go b/pkg/kubectl/cmd/get.go index 16b3d1b7f7f..bc9f0d1579b 100644 --- a/pkg/kubectl/cmd/get.go +++ b/pkg/kubectl/cmd/get.go @@ -74,11 +74,12 @@ Examples: // TODO: return an error instead of using glog.Fatal and checkErr func RunGet(f *Factory, out io.Writer, cmd *cobra.Command, args []string) { selector := GetFlagString(cmd, "selector") + mapper, typer := f.Object(cmd) // handle watch separately since we cannot watch multiple resource types isWatch, isWatchOnly := GetFlagBool(cmd, "watch"), GetFlagBool(cmd, "watch-only") if isWatch || isWatchOnly { - r := resource.NewBuilder(f.Mapper, f.Typer, ClientMapperForCommand(cmd, f)). + r := resource.NewBuilder(mapper, typer, ClientMapperForCommand(cmd, f)). NamespaceParam(GetKubeNamespace(cmd)).DefaultNamespace(). SelectorParam(selector). ResourceTypeOrNameArgs(args...). @@ -117,7 +118,7 @@ func RunGet(f *Factory, out io.Writer, cmd *cobra.Command, args []string) { printer, generic, err := printerForCommand(cmd) checkErr(err) - b := resource.NewBuilder(f.Mapper, f.Typer, ClientMapperForCommand(cmd, f)). + b := resource.NewBuilder(mapper, typer, ClientMapperForCommand(cmd, f)). NamespaceParam(GetKubeNamespace(cmd)).DefaultNamespace(). SelectorParam(selector). ResourceTypeOrNameArgs(args...). diff --git a/pkg/kubectl/cmd/resource.go b/pkg/kubectl/cmd/resource.go index 43a41fee79a..060e9f966d7 100644 --- a/pkg/kubectl/cmd/resource.go +++ b/pkg/kubectl/cmd/resource.go @@ -24,7 +24,6 @@ import ( "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" @@ -39,7 +38,7 @@ func ResourcesFromArgsOrFile( filename, selector string, typer runtime.ObjectTyper, mapper meta.RESTMapper, - clientBuilder func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.RESTClient, error), + clientBuilder func(cmd *cobra.Command, mapping *meta.RESTMapping) (resource.RESTClient, error), schema validation.Schema, requireNames bool, ) resource.Visitor { diff --git a/pkg/kubectl/cmd/rollingupdate.go b/pkg/kubectl/cmd/rollingupdate.go index 5a8ed9818a6..1f20afb8239 100644 --- a/pkg/kubectl/cmd/rollingupdate.go +++ b/pkg/kubectl/cmd/rollingupdate.go @@ -61,7 +61,8 @@ $ cat frontend-v2.json | kubectl rollingupdate frontend-v1 -f - oldName := args[0] schema, err := f.Validator(cmd) checkErr(err) - mapping, namespace, newName, data := ResourceFromFile(cmd, filename, f.Typer, f.Mapper, schema) + mapper, typer := f.Object(cmd) + mapping, namespace, newName, data := ResourceFromFile(cmd, filename, typer, mapper, schema) if mapping.Kind != "ReplicationController" { usageError(cmd, "%s does not specify a valid ReplicationController", filename) } diff --git a/pkg/kubectl/cmd/update.go b/pkg/kubectl/cmd/update.go index eb7d05b1bbe..13403b20bf1 100644 --- a/pkg/kubectl/cmd/update.go +++ b/pkg/kubectl/cmd/update.go @@ -45,7 +45,8 @@ Examples: } schema, err := f.Validator(cmd) checkErr(err) - mapping, namespace, name, data := ResourceFromFile(cmd, filename, f.Typer, f.Mapper, schema) + mapper, typer := f.Object(cmd) + mapping, namespace, name, data := ResourceFromFile(cmd, filename, typer, mapper, schema) client, err := f.RESTClient(cmd, mapping) checkErr(err) diff --git a/pkg/kubectl/kubectl.go b/pkg/kubectl/kubectl.go index ea09542047d..07530b4ef25 100644 --- a/pkg/kubectl/kubectl.go +++ b/pkg/kubectl/kubectl.go @@ -112,6 +112,19 @@ func makeImageList(spec *api.PodSpec) string { return strings.Join(listOfImages(spec), ",") } +// OutputVersionMapper is a RESTMapper that will prefer mappings that +// correspond to a preferred output version (if feasible) +type OutputVersionMapper struct { + meta.RESTMapper + OutputVersion string +} + +// RESTMapping implements meta.RESTMapper by prepending the output version to the preferred version list. +func (m OutputVersionMapper) RESTMapping(kind string, versions ...string) (*meta.RESTMapping, error) { + preferred := append([]string{m.OutputVersion}, versions...) + return m.RESTMapper.RESTMapping(kind, preferred...) +} + // ShortcutExpander is a RESTMapper that can be used for Kubernetes // resources. type ShortcutExpander struct { diff --git a/pkg/kubectl/resource/builder.go b/pkg/kubectl/resource/builder.go index 87553d6d2dd..e589aa9325f 100644 --- a/pkg/kubectl/resource/builder.go +++ b/pkg/kubectl/resource/builder.go @@ -262,6 +262,8 @@ func (b *Builder) ContinueOnError() *Builder { return b } +// SingleResourceType will cause the builder to error if the user specifies more than a single type +// of resource. func (b *Builder) SingleResourceType() *Builder { b.singleResourceType = true return b