Merge pull request #3196 from smarterclayton/allow_create_to_span_resources

Allow create to handle multiple resources, remove createall
This commit is contained in:
bgrant0607 2015-01-13 12:23:50 -08:00
commit de2e298fa9
18 changed files with 220 additions and 172 deletions

View File

@ -4,5 +4,5 @@
### Usage
```
$ flags2yaml image=dockerfile/nginx | simplegen - | cluster/kubectl.sh createall -f -
$ flags2yaml image=dockerfile/nginx | simplegen - | cluster/kubectl.sh create -f -
```

View File

@ -41,8 +41,8 @@ portSpec: 10001:6379
```
Output:
```
$ simplegen redismaster.yaml | cluster/kubectl.sh createall -f -
$ simplegen redisslave.yaml | cluster/kubectl.sh createall -f -
$ simplegen redismaster.yaml | cluster/kubectl.sh create -f -
$ simplegen redisslave.yaml | cluster/kubectl.sh create -f -
$ cluster/kubectl.sh get services
NAME LABELS SELECTOR IP PORT
kubernetes-ro component=apiserver,provider=kubernetes 10.0.0.2 80

View File

@ -16,8 +16,8 @@ limitations under the License.
// simplegen is a tool to generate simple services from a simple description
//
// $ simplegen myservice.json | kubectl createall -f -
// $ simplegen myservice.yaml | kubectl createall -f -
// $ simplegen myservice.json | kubectl create -f -
// $ simplegen myservice.yaml | kubectl create -f -
//
// This is completely separate from kubectl at the moment, until we figure out
// what the right integration approach is.

View File

@ -17,8 +17,8 @@ limitations under the License.
// srvexpand is a tool to generate non-trivial but regular services
// from a description free of most boilerplate
//
// $ srvexpand myservice.json | kubectl createall -f -
// $ srvexpand myservice.yaml | kubectl createall -f -
// $ srvexpand myservice.json | kubectl create -f -
// $ srvexpand myservice.yaml | kubectl create -f -
//
// This is completely separate from kubectl at the moment, until we figure out
// what the right integration approach is.

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
@ -165,7 +165,6 @@ Find more information at https://github.com/GoogleCloudPlatform/kubernetes.`,
cmds.AddCommand(f.NewCmdGet(out))
cmds.AddCommand(f.NewCmdDescribe(out))
cmds.AddCommand(f.NewCmdCreate(out))
cmds.AddCommand(f.NewCmdCreateAll(out))
cmds.AddCommand(f.NewCmdUpdate(out))
cmds.AddCommand(f.NewCmdDelete(out))

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"
@ -90,19 +92,27 @@ func (t *testDescriber) Describe(namespace, name string) (output string, err err
}
type testFactory struct {
Mapper meta.RESTMapper
Typer runtime.ObjectTyper
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{},
Mapper: mapper,
Typer: scheme,
}
return &Factory{
Mapper: mapper,
Typer: scheme,
RESTClient: func(*cobra.Command, *meta.RESTMapping) (kubectl.RESTClient, error) {
Object: func(*cobra.Command) (meta.RESTMapper, runtime.ObjectTyper) {
return t.Mapper, t.Typer
},
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 +121,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 +144,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

@ -1,120 +0,0 @@
/*
Copyright 2014 Google Inc. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd
import (
"fmt"
"io"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta"
"github.com/GoogleCloudPlatform/kubernetes/pkg/config"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
"github.com/ghodss/yaml"
"github.com/golang/glog"
"github.com/spf13/cobra"
)
// DataToObjects converts the raw JSON data into API objects
func DataToObjects(m meta.RESTMapper, t runtime.ObjectTyper, data []byte) (result []runtime.Object, errors []error) {
configObj := []runtime.RawExtension{}
if err := yaml.Unmarshal(data, &configObj); err != nil {
errors = append(errors, fmt.Errorf("config unmarshal: %v", err))
return result, errors
}
for i, in := range configObj {
version, kind, err := t.DataVersionAndKind(in.RawJSON)
if err != nil {
errors = append(errors, fmt.Errorf("item[%d] kind: %v", i, err))
continue
}
mapping, err := m.RESTMapping(kind, version)
if err != nil {
errors = append(errors, fmt.Errorf("item[%d] mapping: %v", i, err))
continue
}
obj, err := mapping.Codec.Decode(in.RawJSON)
if err != nil {
errors = append(errors, fmt.Errorf("item[%d] decode: %v", i, err))
continue
}
result = append(result, obj)
}
return
}
func (f *Factory) NewCmdCreateAll(out io.Writer) *cobra.Command {
cmd := &cobra.Command{
Use: "createall [-d directory] [-f filename]",
Short: "Create all resources specified in a directory, filename or stdin",
Long: `Create all resources contained in JSON file specified in a directory, filename or stdin
JSON and YAML formats are accepted.
Examples:
$ kubectl createall -d configs/
<creates all resources listed in JSON or YAML files, found recursively under the configs directory>
$ kubectl createall -f config.json
<creates all resources listed in config.json>
$ 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)
checkErr(err)
return client, nil
}
filename := GetFlagString(cmd, "filename")
directory := GetFlagString(cmd, "directory")
if (len(filename) == 0 && len(directory) == 0) || (len(filename) != 0 && len(directory) != 0) {
usageError(cmd, "Must pass a directory or filename to update")
}
files := []string{}
if len(filename) != 0 {
files = append(files, filename)
} else {
files = append(GetFilesFromDir(directory, ".json"), GetFilesFromDir(directory, ".yaml")...)
}
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)
errs = append(errs, applyErrs...)
if len(errs) > 0 {
for _, e := range errs {
glog.Error(e)
}
}
}
},
}
cmd.Flags().StringP("directory", "d", "", "Directory of JSON or YAML files to use to update the resource")
cmd.Flags().StringP("filename", "f", "", "Filename or URL to file to use to update the resource")
return cmd
}

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

@ -90,8 +90,8 @@ func TestGetUnknownSchemaObject(t *testing.T) {
// Verifies that schemas that are not in the master tree of Kubernetes can be retrieved via Get.
func TestGetSchemaObject(t *testing.T) {
f, tf, _ := NewTestFactory()
f.Mapper = latest.RESTMapper
f.Typer = api.Scheme
tf.Mapper = latest.RESTMapper
tf.Typer = api.Scheme
codec := latest.Codec
tf.Printer = &testPrinter{}
tf.Client = &client.FakeRESTClient{

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