diff --git a/contrib/completions/bash/kubectl b/contrib/completions/bash/kubectl index 0413c2a5260..7f3529fd3e2 100644 --- a/contrib/completions/bash/kubectl +++ b/contrib/completions/bash/kubectl @@ -351,6 +351,7 @@ _kubectl_create() flags_with_completion=() flags_completion=() + flags+=("--cache-schema") flags+=("--filename=") flags_with_completion+=("--filename") flags_completion+=("__handle_filename_extension_flag json|stdin|yaml|yml") @@ -377,6 +378,7 @@ _kubectl_replace() flags_with_completion=() flags_completion=() + flags+=("--cache-schema") flags+=("--cascade") flags+=("--filename=") flags_with_completion+=("--filename") @@ -519,6 +521,7 @@ _kubectl_rolling-update() flags_with_completion=() flags_completion=() + flags+=("--cache-schema") flags+=("--deployment-label-key=") flags+=("--dry-run") flags+=("--filename=") diff --git a/docs/man/man1/kubectl-create.1 b/docs/man/man1/kubectl-create.1 index d41c1073703..2392fdf8a27 100644 --- a/docs/man/man1/kubectl-create.1 +++ b/docs/man/man1/kubectl-create.1 @@ -20,6 +20,10 @@ JSON and YAML formats are accepted. .SH OPTIONS +.PP +\fB\-\-cache\-schema\fP=true + If true, use/store local schema files + .PP \fB\-f\fP, \fB\-\-filename\fP=[] Filename, directory, or URL to file to use to create the resource diff --git a/docs/man/man1/kubectl-replace.1 b/docs/man/man1/kubectl-replace.1 index b76175b1aa2..679e22616f2 100644 --- a/docs/man/man1/kubectl-replace.1 +++ b/docs/man/man1/kubectl-replace.1 @@ -26,6 +26,10 @@ Please refer to the models in .SH OPTIONS +.PP +\fB\-\-cache\-schema\fP=true + If true, use/store local schema files + .PP \fB\-\-cascade\fP=false Only relevant during a force replace. If true, cascade the deletion of the resources managed by this resource (e.g. Pods created by a ReplicationController). diff --git a/docs/man/man1/kubectl-rolling-update.1 b/docs/man/man1/kubectl-rolling-update.1 index 03cf264dc5e..e6a402be924 100644 --- a/docs/man/man1/kubectl-rolling-update.1 +++ b/docs/man/man1/kubectl-rolling-update.1 @@ -22,6 +22,10 @@ existing replication controller and overwrite at least one (common) label in its .SH OPTIONS +.PP +\fB\-\-cache\-schema\fP=true + If true, use/store local schema files + .PP \fB\-\-deployment\-label\-key\fP="deployment" The key to use to differentiate between two different controllers, default 'deployment'. Only relevant when \-\-image is specified, ignored otherwise diff --git a/docs/user-guide/kubectl/kubectl_create.md b/docs/user-guide/kubectl/kubectl_create.md index aeaf523944c..44e0eb99cd3 100644 --- a/docs/user-guide/kubectl/kubectl_create.md +++ b/docs/user-guide/kubectl/kubectl_create.md @@ -59,6 +59,7 @@ $ cat pod.json | kubectl create -f - ### Options ``` + --cache-schema[=true]: If true, use/store local schema files -f, --filename=[]: Filename, directory, or URL to file to use to create the resource -o, --output="": Output mode. Use "-o name" for shorter output (resource/name). --validate[=true]: If true, use a schema to validate the input before sending it @@ -96,7 +97,7 @@ $ cat pod.json | kubectl create -f - * [kubectl](kubectl.md) - kubectl controls the Kubernetes cluster manager -###### Auto generated by spf13/cobra at 2015-09-10 18:53:03.152429973 +0000 UTC +###### Auto generated by spf13/cobra at 2015-09-10 22:01:09.789168223 +0000 UTC [![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/user-guide/kubectl/kubectl_create.md?pixel)]() diff --git a/docs/user-guide/kubectl/kubectl_replace.md b/docs/user-guide/kubectl/kubectl_replace.md index 96a15b17b9e..649c3aae318 100644 --- a/docs/user-guide/kubectl/kubectl_replace.md +++ b/docs/user-guide/kubectl/kubectl_replace.md @@ -69,6 +69,7 @@ kubectl replace --force -f ./pod.json ### Options ``` + --cache-schema[=true]: If true, use/store local schema files --cascade[=false]: Only relevant during a force replace. If true, cascade the deletion of the resources managed by this resource (e.g. Pods created by a ReplicationController). -f, --filename=[]: Filename, directory, or URL to file to use to replace the resource. --force[=false]: Delete and re-create the specified resource @@ -110,7 +111,7 @@ kubectl replace --force -f ./pod.json * [kubectl](kubectl.md) - kubectl controls the Kubernetes cluster manager -###### Auto generated by spf13/cobra at 2015-09-10 18:53:03.153166598 +0000 UTC +###### Auto generated by spf13/cobra at 2015-09-10 22:01:09.789498374 +0000 UTC [![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/user-guide/kubectl/kubectl_replace.md?pixel)]() diff --git a/docs/user-guide/kubectl/kubectl_rolling-update.md b/docs/user-guide/kubectl/kubectl_rolling-update.md index 751c939335f..89252361930 100644 --- a/docs/user-guide/kubectl/kubectl_rolling-update.md +++ b/docs/user-guide/kubectl/kubectl_rolling-update.md @@ -69,6 +69,7 @@ $ kubectl rolling-update frontend --image=image:v2 ### Options ``` + --cache-schema[=true]: If true, use/store local schema files --deployment-label-key="deployment": The key to use to differentiate between two different controllers, default 'deployment'. Only relevant when --image is specified, ignored otherwise --dry-run[=false]: If true, print out the changes that would be made, but don't actually make them. -f, --filename=[]: Filename or URL to file to use to create the new replication controller. @@ -118,7 +119,7 @@ $ kubectl rolling-update frontend --image=image:v2 * [kubectl](kubectl.md) - kubectl controls the Kubernetes cluster manager -###### Auto generated by spf13/cobra at 2015-09-10 18:53:03.154895732 +0000 UTC +###### Auto generated by spf13/cobra at 2015-09-10 22:01:09.791014946 +0000 UTC [![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/user-guide/kubectl/kubectl_rolling-update.md?pixel)]() diff --git a/pkg/kubectl/cmd/cmd_test.go b/pkg/kubectl/cmd/cmd_test.go index 681af5ee3cf..da2f137e31e 100644 --- a/pkg/kubectl/cmd/cmd_test.go +++ b/pkg/kubectl/cmd/cmd_test.go @@ -160,7 +160,7 @@ func NewTestFactory() (*cmdutil.Factory, *testFactory, runtime.Codec) { Printer: func(mapping *meta.RESTMapping, noHeaders, withNamespace bool, wide bool, showAll bool, columnLabels []string) (kubectl.ResourcePrinter, error) { return t.Printer, t.Err }, - Validator: func(validate bool) (validation.Schema, error) { + Validator: func(validate, cacheSchema bool) (validation.Schema, error) { return t.Validator, t.Err }, DefaultNamespace: func() (string, bool, error) { @@ -215,7 +215,7 @@ func NewAPIFactory() (*cmdutil.Factory, *testFactory, runtime.Codec) { Printer: func(mapping *meta.RESTMapping, noHeaders, withNamespace bool, wide bool, showAll bool, columnLabels []string) (kubectl.ResourcePrinter, error) { return t.Printer, t.Err }, - Validator: func(validate bool) (validation.Schema, error) { + Validator: func(validate, cacheSchema bool) (validation.Schema, error) { return t.Validator, t.Err }, DefaultNamespace: func() (string, bool, error) { diff --git a/pkg/kubectl/cmd/create.go b/pkg/kubectl/cmd/create.go index f0d43ffe6ba..e392d5d4e90 100644 --- a/pkg/kubectl/cmd/create.go +++ b/pkg/kubectl/cmd/create.go @@ -65,7 +65,7 @@ func NewCmdCreate(f *cmdutil.Factory, out io.Writer) *cobra.Command { usage := "Filename, directory, or URL to file to use to create the resource" kubectl.AddJsonFilenameFlag(cmd, &options.Filenames, usage) cmd.MarkFlagRequired("filename") - cmdutil.AddValidateFlag(cmd) + cmdutil.AddValidateFlags(cmd) cmdutil.AddOutputFlagsForMutation(cmd) return cmd } @@ -78,7 +78,7 @@ func ValidateArgs(cmd *cobra.Command, args []string) error { } func RunCreate(f *cmdutil.Factory, cmd *cobra.Command, out io.Writer, options *CreateOptions) error { - schema, err := f.Validator(cmdutil.GetFlagBool(cmd, "validate")) + schema, err := f.Validator(cmdutil.GetFlagBool(cmd, "validate"), cmdutil.GetFlagBool(cmd, "cache-schema")) if err != nil { return err } diff --git a/pkg/kubectl/cmd/replace.go b/pkg/kubectl/cmd/replace.go index ad352707141..48694c8460b 100644 --- a/pkg/kubectl/cmd/replace.go +++ b/pkg/kubectl/cmd/replace.go @@ -81,7 +81,7 @@ func NewCmdReplace(f *cmdutil.Factory, out io.Writer) *cobra.Command { cmd.Flags().Bool("cascade", false, "Only relevant during a force replace. If true, cascade the deletion of the resources managed by this resource (e.g. Pods created by a ReplicationController).") cmd.Flags().Int("grace-period", -1, "Only relevant during a force replace. Period of time in seconds given to the old resource to terminate gracefully. Ignored if negative.") cmd.Flags().Duration("timeout", 0, "Only relevant during a force replace. The length of time to wait before giving up on a delete of the old resource, zero means determine a timeout from the size of the object") - cmdutil.AddValidateFlag(cmd) + cmdutil.AddValidateFlags(cmd) cmdutil.AddOutputFlagsForMutation(cmd) return cmd } @@ -90,7 +90,7 @@ func RunReplace(f *cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []st if len(os.Args) > 1 && os.Args[1] == "update" { printDeprecationWarning("replace", "update") } - schema, err := f.Validator(cmdutil.GetFlagBool(cmd, "validate")) + schema, err := f.Validator(cmdutil.GetFlagBool(cmd, "validate"), cmdutil.GetFlagBool(cmd, "cache-schema")) if err != nil { return err } @@ -143,7 +143,7 @@ func RunReplace(f *cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []st } func forceReplace(f *cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []string, shortOutput bool, options *ReplaceOptions) error { - schema, err := f.Validator(cmdutil.GetFlagBool(cmd, "validate")) + schema, err := f.Validator(cmdutil.GetFlagBool(cmd, "validate"), cmdutil.GetFlagBool(cmd, "cache-schema")) if err != nil { return err } diff --git a/pkg/kubectl/cmd/rollingupdate.go b/pkg/kubectl/cmd/rollingupdate.go index b85b0cf6887..fceb4e904fd 100644 --- a/pkg/kubectl/cmd/rollingupdate.go +++ b/pkg/kubectl/cmd/rollingupdate.go @@ -95,7 +95,7 @@ func NewCmdRollingUpdate(f *cmdutil.Factory, out io.Writer) *cobra.Command { cmd.Flags().String("deployment-label-key", "deployment", "The key to use to differentiate between two different controllers, default 'deployment'. Only relevant when --image is specified, ignored otherwise") cmd.Flags().Bool("dry-run", false, "If true, print out the changes that would be made, but don't actually make them.") cmd.Flags().Bool("rollback", false, "If true, this is a request to abort an existing rollout that is partially rolled out. It effectively reverses current and next and runs a rollout") - cmdutil.AddValidateFlag(cmd) + cmdutil.AddValidateFlags(cmd) cmdutil.AddPrinterFlags(cmd) return cmd } @@ -172,7 +172,7 @@ func RunRollingUpdate(f *cmdutil.Factory, out io.Writer, cmd *cobra.Command, arg mapper, typer := f.Object() if len(filename) != 0 { - schema, err := f.Validator(cmdutil.GetFlagBool(cmd, "validate")) + schema, err := f.Validator(cmdutil.GetFlagBool(cmd, "validate"), cmdutil.GetFlagBool(cmd, "cache-schema")) if err != nil { return err } diff --git a/pkg/kubectl/cmd/util/factory.go b/pkg/kubectl/cmd/util/factory.go index 3c16f49f4a5..6675a4ce4c6 100644 --- a/pkg/kubectl/cmd/util/factory.go +++ b/pkg/kubectl/cmd/util/factory.go @@ -20,7 +20,9 @@ import ( "flag" "fmt" "io" + "io/ioutil" "os" + "path" "strconv" "github.com/spf13/cobra" @@ -76,7 +78,7 @@ type Factory struct { // LabelsForObject returns the labels associated with the provided object LabelsForObject func(object runtime.Object) (map[string]string, error) // Returns a schema that can validate objects stored on disk. - Validator func(validate bool) (validation.Schema, error) + Validator func(validate, cacheSchema bool) (validation.Schema, error) // Returns the default namespace to use in cases where no // other namespace is specified and whether the namespace was // overriden. @@ -214,13 +216,17 @@ func NewFactory(optionalClientConfig clientcmd.ClientConfig) *Factory { } return kubectl.ReaperFor(mapping.Kind, client) }, - Validator: func(validate bool) (validation.Schema, error) { + Validator: func(validate, cacheSchema bool) (validation.Schema, error) { if validate { client, err := clients.ClientForVersion("") if err != nil { return nil, err } - return &clientSwaggerSchema{client, client.ExperimentalClient, api.Scheme}, nil + var cacheDir string + if cacheSchema { + cacheDir = "/tmp" + } + return &clientSwaggerSchema{client, client.ExperimentalClient, cacheDir}, nil } return validation.NullSchema{}, nil }, @@ -273,18 +279,42 @@ func getServicePorts(spec api.ServiceSpec) []string { } type clientSwaggerSchema struct { - c *client.Client - ec *client.ExperimentalClient - t runtime.ObjectTyper + c *client.Client + ec *client.ExperimentalClient + cacheDir string } -func getSchemaAndValidate(c *client.RESTClient, data []byte, group, version string) error { - schemaData, err := c.Get(). - AbsPath("/swaggerapi", group, version). - Do(). - Raw() - if err != nil { - return err +const schemaFileName = "schema.json" + +type schemaClient interface { + Get() *client.Request +} + +func getSchemaAndValidate(c schemaClient, data []byte, group, version, cacheDir string) (err error) { + var schemaData []byte + cacheFile := path.Join(cacheDir, group, version, schemaFileName) + + if len(cacheDir) != 0 { + if schemaData, err = ioutil.ReadFile(cacheFile); err != nil && !os.IsNotExist(err) { + return err + } + } + if schemaData == nil { + schemaData, err = c.Get(). + AbsPath("/swaggerapi", group, version). + Do(). + Raw() + if err != nil { + return err + } + if len(cacheDir) != 0 { + if err = os.MkdirAll(path.Join(cacheDir, group, version), 0755); err != nil { + return err + } + if err = ioutil.WriteFile(cacheFile, schemaData, 0644); err != nil { + return err + } + } } schema, err := validation.NewSwaggerSchemaFromBytes(schemaData) if err != nil { @@ -305,9 +335,9 @@ func (c *clientSwaggerSchema) ValidateBytes(data []byte) error { // If experimental fails, return error from stable api. // TODO: Figure out which group to try once multiple group support is merged // instead of trying everything. - err = getSchemaAndValidate(c.c.RESTClient, data, "api", version) + err = getSchemaAndValidate(c.c.RESTClient, data, "api", version, c.cacheDir) if err != nil && c.ec != nil { - errExp := getSchemaAndValidate(c.ec.RESTClient, data, "experimental", version) + errExp := getSchemaAndValidate(c.ec.RESTClient, data, "experimental", version, c.cacheDir) if errExp == nil { return nil } diff --git a/pkg/kubectl/cmd/util/factory_test.go b/pkg/kubectl/cmd/util/factory_test.go index f8213103c28..c4b5c917994 100644 --- a/pkg/kubectl/cmd/util/factory_test.go +++ b/pkg/kubectl/cmd/util/factory_test.go @@ -17,10 +17,20 @@ limitations under the License. package util import ( + "bytes" + "encoding/json" + "io/ioutil" + "net/http" + "os" + "path" "sort" + "strings" "testing" "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/testapi" + "k8s.io/kubernetes/pkg/api/validation" + client "k8s.io/kubernetes/pkg/client/unversioned" "k8s.io/kubernetes/pkg/client/unversioned/clientcmd" clientcmdapi "k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api" "k8s.io/kubernetes/pkg/kubectl" @@ -166,3 +176,101 @@ func TestFlagUnderscoreRenaming(t *testing.T) { t.Fatalf("Expected flag name to be valid-flag, got %s", factory.flags.Lookup("valid_flag").Name) } } + +func loadSchemaForTest() (validation.Schema, error) { + pathToSwaggerSpec := "../../../../api/swagger-spec/" + testapi.Default.Version() + ".json" + data, err := ioutil.ReadFile(pathToSwaggerSpec) + if err != nil { + return nil, err + } + return validation.NewSwaggerSchemaFromBytes(data) +} + +func TestValidateCachesSchema(t *testing.T) { + schema, err := loadSchemaForTest() + if err != nil { + t.Errorf("Error loading schema: %v", err) + t.FailNow() + } + output, err := json.Marshal(schema) + if err != nil { + t.Errorf("Error serializing schema: %v", err) + t.FailNow() + } + requests := map[string]int{} + + c := &client.FakeRESTClient{ + Codec: testapi.Default.Codec(), + Client: client.HTTPClientFunc(func(req *http.Request) (*http.Response, error) { + switch p, m := req.URL.Path, req.Method; { + case strings.HasPrefix(p, "/swaggerapi") && m == "GET": + requests[p] = requests[p] + 1 + return &http.Response{StatusCode: 200, Body: ioutil.NopCloser(bytes.NewBuffer(output))}, nil + default: + t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) + return nil, nil + } + }), + } + dir := os.TempDir() + "/schemaCache" + os.RemoveAll(dir) + + obj := &api.Pod{} + data, err := testapi.Default.Codec().Encode(obj) + if err != nil { + t.Errorf("unexpected error: %v", err) + t.FailNow() + } + + // Initial request, should use HTTP and write + if getSchemaAndValidate(c, data, "foo", "bar", dir); err != nil { + t.Errorf("unexpected error validating: %v", err) + } + if _, err := os.Stat(path.Join(dir, "foo", "bar", schemaFileName)); err != nil { + t.Errorf("unexpected missing cache file: %v", err) + } + if requests["/swaggerapi/foo/bar"] != 1 { + t.Errorf("expected 1 schema request, saw: %d", requests["/swaggerapi/foo/bar"]) + } + + // Same version and group, should skip HTTP + if getSchemaAndValidate(c, data, "foo", "bar", dir); err != nil { + t.Errorf("unexpected error validating: %v", err) + } + if requests["/swaggerapi/foo/bar"] != 1 { + t.Errorf("expected 1 schema request, saw: %d", requests["/swaggerapi/foo/bar"]) + } + + // Different API group, should go to HTTP and write + if getSchemaAndValidate(c, data, "foo", "baz", dir); err != nil { + t.Errorf("unexpected error validating: %v", err) + } + if _, err := os.Stat(path.Join(dir, "foo", "baz", schemaFileName)); err != nil { + t.Errorf("unexpected missing cache file: %v", err) + } + if requests["/swaggerapi/foo/baz"] != 1 { + t.Errorf("expected 1 schema request, saw: %d", requests["/swaggerapi/foo/baz"]) + } + + // Different version, should go to HTTP and write + if getSchemaAndValidate(c, data, "foo2", "bar", dir); err != nil { + t.Errorf("unexpected error validating: %v", err) + } + if _, err := os.Stat(path.Join(dir, "foo2", "bar", schemaFileName)); err != nil { + t.Errorf("unexpected missing cache file: %v", err) + } + if requests["/swaggerapi/foo2/bar"] != 1 { + t.Errorf("expected 1 schema request, saw: %d", requests["/swaggerapi/foo2/bar"]) + } + + // No cache dir, should go straight to HTTP and not write + if getSchemaAndValidate(c, data, "foo", "blah", ""); err != nil { + t.Errorf("unexpected error validating: %v", err) + } + if requests["/swaggerapi/foo/blah"] != 1 { + t.Errorf("expected 1 schema request, saw: %d", requests["/swaggerapi/foo/blah"]) + } + if _, err := os.Stat(path.Join(dir, "foo", "blah", schemaFileName)); err == nil || !os.IsNotExist(err) { + t.Errorf("unexpected cache file error: %v", err) + } +} diff --git a/pkg/kubectl/cmd/util/helpers.go b/pkg/kubectl/cmd/util/helpers.go index 4b39f616f3b..6a94c0df833 100644 --- a/pkg/kubectl/cmd/util/helpers.go +++ b/pkg/kubectl/cmd/util/helpers.go @@ -268,8 +268,9 @@ func GetFlagDuration(cmd *cobra.Command, flag string) time.Duration { return d } -func AddValidateFlag(cmd *cobra.Command) { +func AddValidateFlags(cmd *cobra.Command) { cmd.Flags().Bool("validate", true, "If true, use a schema to validate the input before sending it") + cmd.Flags().Bool("cache-schema", true, "If true, use/store local schema files") } func ReadConfigDataFromReader(reader io.Reader, source string) ([]byte, error) {