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
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

View File

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

View File

@ -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 -
<create a pod based on the json passed into stdin>`,
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
}

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 -
<creates all resources listed in config.json>`,
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 {

View File

@ -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

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
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)

View File

@ -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...).

View File

@ -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 {

View File

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

View File

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

View File

@ -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 {

View File

@ -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