Create should be able to accept multiple resources

This commit is contained in:
Clayton Coleman 2014-12-31 18:35:52 -05:00
parent 2151afe334
commit d1ab27762b
13 changed files with 212 additions and 46 deletions

View File

@ -48,16 +48,15 @@ type Factory struct {
clients *clientCache clients *clientCache
flags *pflag.FlagSet flags *pflag.FlagSet
Mapper meta.RESTMapper // Returns interfaces for dealing with arbitrary runtime.Objects.
Typer runtime.ObjectTyper Object func(cmd *cobra.Command) (meta.RESTMapper, runtime.ObjectTyper)
// Returns a client for accessing Kubernetes resources or an error. // Returns a client for accessing Kubernetes resources or an error.
Client func(cmd *cobra.Command) (*client.Client, error) Client func(cmd *cobra.Command) (*client.Client, error)
// Returns a client.Config for accessing the Kubernetes server. // Returns a client.Config for accessing the Kubernetes server.
ClientConfig func(cmd *cobra.Command) (*client.Config, error) ClientConfig func(cmd *cobra.Command) (*client.Config, error)
// Returns a RESTClient for working with the specified RESTMapping or an error. This is intended // 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. // 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. // Returns a Describer for displaying the specified RESTMapping type or an error.
Describer func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.Describer, 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. // Returns a Printer for formatting objects of the given type or an error.
@ -81,16 +80,17 @@ func NewFactory() *Factory {
clients: clients, clients: clients,
flags: flags, flags: flags,
Mapper: mapper, Object: func(cmd *cobra.Command) (meta.RESTMapper, runtime.ObjectTyper) {
Typer: api.Scheme, version := GetFlagString(cmd, "api-version")
return kubectl.OutputVersionMapper{mapper, version}, api.Scheme
},
Client: func(cmd *cobra.Command) (*client.Client, error) { Client: func(cmd *cobra.Command) (*client.Client, error) {
return clients.ClientForVersion("") return clients.ClientForVersion("")
}, },
ClientConfig: func(cmd *cobra.Command) (*client.Config, error) { ClientConfig: func(cmd *cobra.Command) (*client.Config, error) {
return clients.ClientConfigForVersion("") 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) client, err := clients.ClientForVersion(mapping.APIVersion)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -25,8 +25,10 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" "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"
. "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd" . "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd"
"github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/resource"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -93,16 +95,20 @@ type testFactory struct {
Client kubectl.RESTClient Client kubectl.RESTClient
Describer kubectl.Describer Describer kubectl.Describer
Printer kubectl.ResourcePrinter Printer kubectl.ResourcePrinter
Validator validation.Schema
Err error Err error
} }
func NewTestFactory() (*Factory, *testFactory, runtime.Codec) { func NewTestFactory() (*Factory, *testFactory, runtime.Codec) {
scheme, mapper, codec := newExternalScheme() scheme, mapper, codec := newExternalScheme()
t := &testFactory{} t := &testFactory{
Validator: validation.NullSchema{},
}
return &Factory{ return &Factory{
Mapper: mapper, Object: func(*cobra.Command) (meta.RESTMapper, runtime.ObjectTyper) {
Typer: scheme, return mapper, scheme
RESTClient: func(*cobra.Command, *meta.RESTMapping) (kubectl.RESTClient, error) { },
RESTClient: func(*cobra.Command, *meta.RESTMapping) (resource.RESTClient, error) {
return t.Client, t.Err return t.Client, t.Err
}, },
Describer: func(*cobra.Command, *meta.RESTMapping) (kubectl.Describer, error) { 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) { Printer: func(cmd *cobra.Command, mapping *meta.RESTMapping, noHeaders bool) (kubectl.ResourcePrinter, error) {
return t.Printer, t.Err return t.Printer, t.Err
}, },
Validator: func(cmd *cobra.Command) (validation.Schema, error) {
return t.Validator, t.Err
},
}, t, codec }, t, codec
} }
func NewAPIFactory() (*Factory, *testFactory, runtime.Codec) { func NewAPIFactory() (*Factory, *testFactory, runtime.Codec) {
t := &testFactory{} t := &testFactory{
Validator: validation.NullSchema{},
}
return &Factory{ return &Factory{
Mapper: latest.RESTMapper, Object: func(*cobra.Command) (meta.RESTMapper, runtime.ObjectTyper) {
Typer: api.Scheme, return latest.RESTMapper, api.Scheme
RESTClient: func(*cobra.Command, *meta.RESTMapping) (kubectl.RESTClient, error) { },
RESTClient: func(*cobra.Command, *meta.RESTMapping) (resource.RESTClient, error) {
return t.Client, t.Err return t.Client, t.Err
}, },
Describer: func(*cobra.Command, *meta.RESTMapping) (kubectl.Describer, error) { 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) { Printer: func(cmd *cobra.Command, mapping *meta.RESTMapping, noHeaders bool) (kubectl.ResourcePrinter, error) {
return t.Printer, t.Err return t.Printer, t.Err
}, },
Validator: func(cmd *cobra.Command) (validation.Schema, error) {
return t.Validator, t.Err
},
}, t, latest.Codec }, t, latest.Codec
} }

View File

@ -20,11 +20,16 @@ import (
"fmt" "fmt"
"io" "io"
"github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/resource"
"github.com/spf13/cobra" "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 { func (f *Factory) NewCmdCreate(out io.Writer) *cobra.Command {
flags := &struct {
Filenames util.StringList
}{}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "create -f filename", Use: "create -f filename",
Short: "Create a resource by filename or stdin", Short: "Create a resource by filename or stdin",
@ -39,29 +44,35 @@ Examples:
$ cat pod.json | kubectl create -f - $ cat pod.json | kubectl create -f -
<create a pod based on the json passed into stdin>`, <create a pod based on the json passed into stdin>`,
Run: func(cmd *cobra.Command, args []string) { 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) schema, err := f.Validator(cmd)
checkErr(err) 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 mapper, typer := f.Object(cmd)
if len(namespace) == 0 { r := resource.NewBuilder(mapper, typer, ClientMapperForCommand(cmd, f)).
namespace = GetKubeNamespace(cmd) ContinueOnError().
} else { NamespaceParam(GetKubeNamespace(cmd)).RequireNamespace().
err = CompareNamespaceFromFile(cmd, namespace) FilenameParam(flags.Filenames...).
checkErr(err) Flatten().
Do()
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 {
err = resource.NewHelper(client, mapping).Create(namespace, true, data) 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) 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 return cmd
} }

View File

@ -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())
}
}

View File

@ -78,8 +78,8 @@ Examples:
$ cat config.json | kubectl apply -f - $ cat config.json | kubectl apply -f -
<creates all resources listed in config.json>`, <creates all resources listed in config.json>`,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
clientFunc := func(mapper *meta.RESTMapping) (config.RESTClientPoster, error) { clientFunc := func(mapping *meta.RESTMapping) (config.RESTClientPoster, error) {
client, err := f.RESTClient(cmd, mapper) client, err := f.RESTClient(cmd, mapping)
checkErr(err) checkErr(err)
return client, nil return client, nil
} }
@ -98,12 +98,13 @@ Examples:
files = append(GetFilesFromDir(directory, ".json"), GetFilesFromDir(directory, ".yaml")...) files = append(GetFilesFromDir(directory, ".json"), GetFilesFromDir(directory, ".yaml")...)
} }
mapper, typer := f.Object(cmd)
for _, filename := range files { for _, filename := range files {
data, err := ReadConfigData(filename) data, err := ReadConfigData(filename)
checkErr(err) checkErr(err)
items, errs := DataToObjects(f.Mapper, f.Typer, data) items, errs := DataToObjects(mapper, typer, data)
applyErrs := config.CreateObjects(f.Typer, f.Mapper, clientFunc, items) applyErrs := config.CreateObjects(typer, mapper, clientFunc, items)
errs = append(errs, applyErrs...) errs = append(errs, applyErrs...)
if len(errs) > 0 { if len(errs) > 0 {

View File

@ -59,7 +59,8 @@ Examples:
checkErr(err) checkErr(err)
selector := GetFlagString(cmd, "selector") selector := GetFlagString(cmd, "selector")
found := 0 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++ found++
if err := resource.NewHelper(r.Client, r.Mapping).Delete(r.Namespace, r.Name); err != nil { if err := resource.NewHelper(r.Client, r.Mapping).Delete(r.Namespace, r.Name); err != nil {
return err return err

View File

@ -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 This command joins many API calls together to form a detailed description of a
given resource.`, given resource.`,
Run: func(cmd *cobra.Command, args []string) { 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) describer, err := f.Describer(cmd, mapping)
checkErr(err) checkErr(err)

View File

@ -74,11 +74,12 @@ Examples:
// TODO: return an error instead of using glog.Fatal and checkErr // TODO: return an error instead of using glog.Fatal and checkErr
func RunGet(f *Factory, out io.Writer, cmd *cobra.Command, args []string) { func RunGet(f *Factory, out io.Writer, cmd *cobra.Command, args []string) {
selector := GetFlagString(cmd, "selector") selector := GetFlagString(cmd, "selector")
mapper, typer := f.Object(cmd)
// handle watch separately since we cannot watch multiple resource types // handle watch separately since we cannot watch multiple resource types
isWatch, isWatchOnly := GetFlagBool(cmd, "watch"), GetFlagBool(cmd, "watch-only") isWatch, isWatchOnly := GetFlagBool(cmd, "watch"), GetFlagBool(cmd, "watch-only")
if isWatch || isWatchOnly { 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(). NamespaceParam(GetKubeNamespace(cmd)).DefaultNamespace().
SelectorParam(selector). SelectorParam(selector).
ResourceTypeOrNameArgs(args...). ResourceTypeOrNameArgs(args...).
@ -117,7 +118,7 @@ func RunGet(f *Factory, out io.Writer, cmd *cobra.Command, args []string) {
printer, generic, err := printerForCommand(cmd) printer, generic, err := printerForCommand(cmd)
checkErr(err) checkErr(err)
b := resource.NewBuilder(f.Mapper, f.Typer, ClientMapperForCommand(cmd, f)). b := resource.NewBuilder(mapper, typer, ClientMapperForCommand(cmd, f)).
NamespaceParam(GetKubeNamespace(cmd)).DefaultNamespace(). NamespaceParam(GetKubeNamespace(cmd)).DefaultNamespace().
SelectorParam(selector). SelectorParam(selector).
ResourceTypeOrNameArgs(args...). ResourceTypeOrNameArgs(args...).

View File

@ -24,7 +24,6 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/validation" "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/kubectl/resource"
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
@ -39,7 +38,7 @@ func ResourcesFromArgsOrFile(
filename, selector string, filename, selector string,
typer runtime.ObjectTyper, typer runtime.ObjectTyper,
mapper meta.RESTMapper, 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, schema validation.Schema,
requireNames bool, requireNames bool,
) resource.Visitor { ) resource.Visitor {

View File

@ -61,7 +61,8 @@ $ cat frontend-v2.json | kubectl rollingupdate frontend-v1 -f -
oldName := args[0] oldName := args[0]
schema, err := f.Validator(cmd) schema, err := f.Validator(cmd)
checkErr(err) 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" { if mapping.Kind != "ReplicationController" {
usageError(cmd, "%s does not specify a valid ReplicationController", filename) usageError(cmd, "%s does not specify a valid ReplicationController", filename)
} }

View File

@ -45,7 +45,8 @@ Examples:
} }
schema, err := f.Validator(cmd) schema, err := f.Validator(cmd)
checkErr(err) 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) client, err := f.RESTClient(cmd, mapping)
checkErr(err) checkErr(err)

View File

@ -112,6 +112,19 @@ func makeImageList(spec *api.PodSpec) string {
return strings.Join(listOfImages(spec), ",") 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 // ShortcutExpander is a RESTMapper that can be used for Kubernetes
// resources. // resources.
type ShortcutExpander struct { type ShortcutExpander struct {

View File

@ -262,6 +262,8 @@ func (b *Builder) ContinueOnError() *Builder {
return b 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 { func (b *Builder) SingleResourceType() *Builder {
b.singleResourceType = true b.singleResourceType = true
return b return b