diff --git a/hack/make-rules/test-cmd-util.sh b/hack/make-rules/test-cmd-util.sh index 685cffa0907..e4181d82d11 100755 --- a/hack/make-rules/test-cmd-util.sh +++ b/hack/make-rules/test-cmd-util.sh @@ -936,6 +936,9 @@ run_create_tests() { # Post-condition: jsonpath for .metadata.namespace should be empty for object since --namespace was not explicitly specified kube::test::if_empty_string "${output_message}" + kubectl create configmap tester-create-cm -o json --dry-run | kubectl create "${kube_flags[@]}" --raw /api/v1/namespaces/default/configmaps -f - + kubectl delete -ndefault "${kube_flags[@]}" configmap tester-create-cm + set +o nounset set +o errexit } @@ -3674,6 +3677,11 @@ run_kubectl_create_error_tests() { fi rm "${ERROR_FILE}" + # Posting a pod to namespaces should fail. Also tests --raw forcing the post location + [ "$( kubectl convert -f test/fixtures/doc-yaml/admin/limitrange/valid-pod.yaml -o json | kubectl create "${kube_flags[@]}" --raw /api/v1/namespaces -f - --v=8 2>&1 | grep 'cannot be handled as a Namespace: converting (v1.Pod)')" ] + + [ "$( kubectl create "${kube_flags[@]}" --raw /api/v1/namespaces -f test/fixtures/doc-yaml/admin/limitrange/valid-pod.yaml --edit 2>&1 | grep 'raw and --edit are mutually exclusive')" ] + set +o nounset set +o errexit } diff --git a/pkg/kubectl/cmd/create.go b/pkg/kubectl/cmd/create.go index 61433deb608..8caa921870d 100644 --- a/pkg/kubectl/cmd/create.go +++ b/pkg/kubectl/cmd/create.go @@ -19,13 +19,16 @@ package cmd import ( "fmt" "io" + "os" "runtime" + "strings" "github.com/spf13/cobra" + "net/url" + "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/kubernetes/pkg/kubectl" "k8s.io/kubernetes/pkg/kubectl/cmd/templates" cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" @@ -38,6 +41,7 @@ type CreateOptions struct { FilenameOptions resource.FilenameOptions Selector string EditBeforeCreate bool + Raw string } var ( @@ -71,7 +75,7 @@ func NewCmdCreate(f cmdutil.Factory, out, errOut io.Writer) *cobra.Command { defaultRunFunc(cmd, args) return } - cmdutil.CheckErr(ValidateArgs(cmd, args)) + cmdutil.CheckErr(options.ValidateArgs(cmd, args)) cmdutil.CheckErr(RunCreate(f, cmd, out, errOut, &options)) }, } @@ -89,6 +93,7 @@ func NewCmdCreate(f cmdutil.Factory, out, errOut io.Writer) *cobra.Command { cmdutil.AddDryRunFlag(cmd) cmdutil.AddInclude3rdPartyFlags(cmd) cmd.Flags().StringVarP(&options.Selector, "selector", "l", "", "Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2)") + cmd.Flags().StringVar(&options.Raw, "raw", options.Raw, "Raw URI to POST to the server. Uses the transport specified by the kubeconfig file.") // create subcommands cmd.AddCommand(NewCmdCreateNamespace(f, out)) @@ -106,14 +111,65 @@ func NewCmdCreate(f cmdutil.Factory, out, errOut io.Writer) *cobra.Command { return cmd } -func ValidateArgs(cmd *cobra.Command, args []string) error { +func (o *CreateOptions) ValidateArgs(cmd *cobra.Command, args []string) error { if len(args) != 0 { return cmdutil.UsageErrorf(cmd, "Unexpected args: %v", args) } + if len(o.Raw) > 0 { + if o.EditBeforeCreate { + return cmdutil.UsageErrorf(cmd, "--raw and --edit are mutually exclusive") + } + if len(o.FilenameOptions.Filenames) != 1 { + return cmdutil.UsageErrorf(cmd, "--raw can only use a single local file or stdin") + } + if strings.HasPrefix(o.FilenameOptions.Filenames[0], "http") { + return cmdutil.UsageErrorf(cmd, "--raw cannot read from a url") + } + if o.FilenameOptions.Recursive { + return cmdutil.UsageErrorf(cmd, "--raw and --recursive are mutually exclusive") + } + if len(o.Selector) > 0 { + return cmdutil.UsageErrorf(cmd, "--raw and --selector (-l) are mutually exclusive") + } + if len(cmdutil.GetFlagString(cmd, "output")) > 0 { + return cmdutil.UsageErrorf(cmd, "--raw and --output are mutually exclusive") + } + if _, err := url.ParseRequestURI(o.Raw); err != nil { + return cmdutil.UsageErrorf(cmd, "--raw must be a valid URL path: %v", err) + } + } + return nil } func RunCreate(f cmdutil.Factory, cmd *cobra.Command, out, errOut io.Writer, options *CreateOptions) error { + // raw only makes sense for a single file resource multiple objects aren't likely to do what you want. + // the validator enforces this, so + if len(options.Raw) > 0 { + restClient, err := f.RESTClient() + if err != nil { + return err + } + + var data io.ReadCloser + if options.FilenameOptions.Filenames[0] == "-" { + data = os.Stdin + } else { + data, err = os.Open(options.FilenameOptions.Filenames[0]) + if err != nil { + return err + } + } + // TODO post content with stream. Right now it ignores body content + bytes, err := restClient.Post().RequestURI(options.Raw).Body(data).DoRaw() + if err != nil { + return err + } + + fmt.Fprintf(out, "%v", string(bytes)) + return nil + } + if options.EditBeforeCreate { return RunEditOnCreate(f, out, errOut, cmd, &options.FilenameOptions) } diff --git a/pkg/kubectl/cmd/create_test.go b/pkg/kubectl/cmd/create_test.go index c21e1f313ed..2484761f464 100644 --- a/pkg/kubectl/cmd/create_test.go +++ b/pkg/kubectl/cmd/create_test.go @@ -33,7 +33,8 @@ func TestExtraArgsFail(t *testing.T) { f, _, _, _ := cmdtesting.NewAPIFactory() c := NewCmdCreate(f, buf, errBuf) - if ValidateArgs(c, []string{"rc"}) == nil { + options := CreateOptions{} + if options.ValidateArgs(c, []string{"rc"}) == nil { t.Errorf("unexpected non-error") } }