Add return code support to kubectl-exec and -run
This commit is contained in:
		 bindata-mockuser
					bindata-mockuser
				
			
				
					committed by
					
						 Dr. Stefan Schimanski
						Dr. Stefan Schimanski
					
				
			
			
				
	
			
			
			 Dr. Stefan Schimanski
						Dr. Stefan Schimanski
					
				
			
						parent
						
							6dcb0c9130
						
					
				
				
					commit
					e792d4117d
				
			
							
								
								
									
										55
									
								
								pkg/client/unversioned/remotecommand/errorstream.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								pkg/client/unversioned/remotecommand/errorstream.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | |||||||
|  | /* | ||||||
|  | Copyright 2016 The Kubernetes Authors. | ||||||
|  |  | ||||||
|  | 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 remotecommand | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"io" | ||||||
|  | 	"io/ioutil" | ||||||
|  |  | ||||||
|  | 	"k8s.io/kubernetes/pkg/util/runtime" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // errorStreamDecoder interprets the data on the error channel and creates a go error object from it. | ||||||
|  | type errorStreamDecoder interface { | ||||||
|  | 	decode(message []byte) error | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // watchErrorStream watches the errorStream for remote command error data, | ||||||
|  | // decodes it with the given errorStreamDecoder, sends the decoded error (or nil if the remote | ||||||
|  | // command exited successfully) to the returned error channel, and closes it. | ||||||
|  | // This function returns immediately. | ||||||
|  | func watchErrorStream(errorStream io.Reader, d errorStreamDecoder) chan error { | ||||||
|  | 	errorChan := make(chan error) | ||||||
|  |  | ||||||
|  | 	go func() { | ||||||
|  | 		defer runtime.HandleCrash() | ||||||
|  |  | ||||||
|  | 		message, err := ioutil.ReadAll(errorStream) | ||||||
|  | 		switch { | ||||||
|  | 		case err != nil && err != io.EOF: | ||||||
|  | 			errorChan <- fmt.Errorf("error reading from error stream: %s", err) | ||||||
|  | 		case len(message) > 0: | ||||||
|  | 			errorChan <- d.decode(message) | ||||||
|  | 		default: | ||||||
|  | 			errorChan <- nil | ||||||
|  | 		} | ||||||
|  | 		close(errorChan) | ||||||
|  | 	}() | ||||||
|  |  | ||||||
|  | 	return errorChan | ||||||
|  | } | ||||||
| @@ -162,6 +162,8 @@ func (e *streamExecutor) Stream(options StreamOptions) error { | |||||||
| 	var streamer streamProtocolHandler | 	var streamer streamProtocolHandler | ||||||
|  |  | ||||||
| 	switch protocol { | 	switch protocol { | ||||||
|  | 	case remotecommand.StreamProtocolV4Name: | ||||||
|  | 		streamer = newStreamProtocolV4(options) | ||||||
| 	case remotecommand.StreamProtocolV3Name: | 	case remotecommand.StreamProtocolV3Name: | ||||||
| 		streamer = newStreamProtocolV3(options) | 		streamer = newStreamProtocolV3(options) | ||||||
| 	case remotecommand.StreamProtocolV2Name: | 	case remotecommand.StreamProtocolV2Name: | ||||||
|   | |||||||
| @@ -88,27 +88,6 @@ func (p *streamProtocolV2) createStreams(conn streamCreator) error { | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (p *streamProtocolV2) setupErrorStreamReading() chan error { |  | ||||||
| 	errorChan := make(chan error) |  | ||||||
|  |  | ||||||
| 	go func() { |  | ||||||
| 		defer runtime.HandleCrash() |  | ||||||
|  |  | ||||||
| 		message, err := ioutil.ReadAll(p.errorStream) |  | ||||||
| 		switch { |  | ||||||
| 		case err != nil && err != io.EOF: |  | ||||||
| 			errorChan <- fmt.Errorf("error reading from error stream: %s", err) |  | ||||||
| 		case len(message) > 0: |  | ||||||
| 			errorChan <- fmt.Errorf("error executing remote command: %s", message) |  | ||||||
| 		default: |  | ||||||
| 			errorChan <- nil |  | ||||||
| 		} |  | ||||||
| 		close(errorChan) |  | ||||||
| 	}() |  | ||||||
|  |  | ||||||
| 	return errorChan |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (p *streamProtocolV2) copyStdin() { | func (p *streamProtocolV2) copyStdin() { | ||||||
| 	if p.Stdin != nil { | 	if p.Stdin != nil { | ||||||
| 		var once sync.Once | 		var once sync.Once | ||||||
| @@ -193,7 +172,7 @@ func (p *streamProtocolV2) stream(conn streamCreator) error { | |||||||
|  |  | ||||||
| 	// now that all the streams have been created, proceed with reading & copying | 	// now that all the streams have been created, proceed with reading & copying | ||||||
|  |  | ||||||
| 	errorChan := p.setupErrorStreamReading() | 	errorChan := watchErrorStream(p.errorStream, &errorDecoderV2{}) | ||||||
|  |  | ||||||
| 	p.copyStdin() | 	p.copyStdin() | ||||||
|  |  | ||||||
| @@ -207,3 +186,10 @@ func (p *streamProtocolV2) stream(conn streamCreator) error { | |||||||
| 	// waits for errorStream to finish reading with an error or nil | 	// waits for errorStream to finish reading with an error or nil | ||||||
| 	return <-errorChan | 	return <-errorChan | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // errorDecoderV2 interprets the error channel data as plain text. | ||||||
|  | type errorDecoderV2 struct{} | ||||||
|  |  | ||||||
|  | func (d *errorDecoderV2) decode(message []byte) error { | ||||||
|  | 	return fmt.Errorf("error executing remote command: %s", message) | ||||||
|  | } | ||||||
|   | |||||||
| @@ -199,7 +199,7 @@ func TestV2ErrorStreamReading(t *testing.T) { | |||||||
| 		h := newStreamProtocolV2(StreamOptions{}).(*streamProtocolV2) | 		h := newStreamProtocolV2(StreamOptions{}).(*streamProtocolV2) | ||||||
| 		h.errorStream = test.stream | 		h.errorStream = test.stream | ||||||
|  |  | ||||||
| 		ch := h.setupErrorStreamReading() | 		ch := watchErrorStream(h.errorStream, &errorDecoderV2{}) | ||||||
| 		if ch == nil { | 		if ch == nil { | ||||||
| 			t.Fatalf("%s: unexpected nil channel", test.name) | 			t.Fatalf("%s: unexpected nil channel", test.name) | ||||||
| 		} | 		} | ||||||
|   | |||||||
| @@ -90,7 +90,7 @@ func (p *streamProtocolV3) stream(conn streamCreator) error { | |||||||
|  |  | ||||||
| 	// now that all the streams have been created, proceed with reading & copying | 	// now that all the streams have been created, proceed with reading & copying | ||||||
|  |  | ||||||
| 	errorChan := p.setupErrorStreamReading() | 	errorChan := watchErrorStream(p.errorStream, &errorDecoderV3{}) | ||||||
|  |  | ||||||
| 	p.handleResizes() | 	p.handleResizes() | ||||||
|  |  | ||||||
| @@ -106,3 +106,7 @@ func (p *streamProtocolV3) stream(conn streamCreator) error { | |||||||
| 	// waits for errorStream to finish reading with an error or nil | 	// waits for errorStream to finish reading with an error or nil | ||||||
| 	return <-errorChan | 	return <-errorChan | ||||||
| } | } | ||||||
|  |  | ||||||
|  | type errorDecoderV3 struct { | ||||||
|  | 	errorDecoderV2 | ||||||
|  | } | ||||||
|   | |||||||
							
								
								
									
										119
									
								
								pkg/client/unversioned/remotecommand/v4.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								pkg/client/unversioned/remotecommand/v4.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,119 @@ | |||||||
|  | /* | ||||||
|  | Copyright 2016 The Kubernetes Authors. | ||||||
|  |  | ||||||
|  | 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 remotecommand | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"strconv" | ||||||
|  | 	"sync" | ||||||
|  |  | ||||||
|  | 	"k8s.io/kubernetes/pkg/api/unversioned" | ||||||
|  | 	"k8s.io/kubernetes/pkg/kubelet/server/remotecommand" | ||||||
|  | 	"k8s.io/kubernetes/pkg/util/exec" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // streamProtocolV4 implements version 4 of the streaming protocol for attach | ||||||
|  | // and exec. This version adds support for exit codes on the error stream through | ||||||
|  | // the use of unversioned.Status instead of plain text messages. | ||||||
|  | type streamProtocolV4 struct { | ||||||
|  | 	*streamProtocolV3 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var _ streamProtocolHandler = &streamProtocolV4{} | ||||||
|  |  | ||||||
|  | func newStreamProtocolV4(options StreamOptions) streamProtocolHandler { | ||||||
|  | 	return &streamProtocolV4{ | ||||||
|  | 		streamProtocolV3: newStreamProtocolV3(options).(*streamProtocolV3), | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (p *streamProtocolV4) createStreams(conn streamCreator) error { | ||||||
|  | 	return p.streamProtocolV3.createStreams(conn) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (p *streamProtocolV4) handleResizes() { | ||||||
|  | 	p.streamProtocolV3.handleResizes() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (p *streamProtocolV4) stream(conn streamCreator) error { | ||||||
|  | 	if err := p.createStreams(conn); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// now that all the streams have been created, proceed with reading & copying | ||||||
|  |  | ||||||
|  | 	errorChan := watchErrorStream(p.errorStream, &errorDecoderV4{}) | ||||||
|  |  | ||||||
|  | 	p.handleResizes() | ||||||
|  |  | ||||||
|  | 	p.copyStdin() | ||||||
|  |  | ||||||
|  | 	var wg sync.WaitGroup | ||||||
|  | 	p.copyStdout(&wg) | ||||||
|  | 	p.copyStderr(&wg) | ||||||
|  |  | ||||||
|  | 	// we're waiting for stdout/stderr to finish copying | ||||||
|  | 	wg.Wait() | ||||||
|  |  | ||||||
|  | 	// waits for errorStream to finish reading with an error or nil | ||||||
|  | 	return <-errorChan | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // errorDecoderV4 interprets the json-marshaled unversioned.Status on the error channel | ||||||
|  | // and creates an exec.ExitError from it. | ||||||
|  | type errorDecoderV4 struct{} | ||||||
|  |  | ||||||
|  | func (d *errorDecoderV4) decode(message []byte) error { | ||||||
|  | 	status := unversioned.Status{} | ||||||
|  | 	err := json.Unmarshal(message, &status) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("error stream protocol error: %v in %q", err, string(message)) | ||||||
|  | 	} | ||||||
|  | 	switch status.Status { | ||||||
|  | 	case unversioned.StatusSuccess: | ||||||
|  | 		return nil | ||||||
|  | 	case unversioned.StatusFailure: | ||||||
|  | 		if status.Reason == remotecommand.NonZeroExitCodeReason { | ||||||
|  | 			if status.Details == nil { | ||||||
|  | 				return errors.New("error stream protocol error: details must be set") | ||||||
|  | 			} | ||||||
|  | 			for i := range status.Details.Causes { | ||||||
|  | 				c := &status.Details.Causes[i] | ||||||
|  | 				if c.Type != remotecommand.ExitCodeCauseType { | ||||||
|  | 					continue | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				rc, err := strconv.ParseUint(c.Message, 10, 8) | ||||||
|  | 				if err != nil { | ||||||
|  | 					return fmt.Errorf("error stream protocol error: invalid exit code value %q", c.Message) | ||||||
|  | 				} | ||||||
|  | 				return exec.CodeExitError{ | ||||||
|  | 					Err:  fmt.Errorf("command terminated with exit code %d", rc), | ||||||
|  | 					Code: int(rc), | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			return fmt.Errorf("error stream protocol error: no %s cause given", remotecommand.ExitCodeCauseType) | ||||||
|  | 		} | ||||||
|  | 	default: | ||||||
|  | 		return errors.New("error stream protocol error: unknown error") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return fmt.Errorf(status.Message) | ||||||
|  | } | ||||||
							
								
								
									
										71
									
								
								pkg/client/unversioned/remotecommand/v4_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								pkg/client/unversioned/remotecommand/v4_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,71 @@ | |||||||
|  | /* | ||||||
|  | Copyright 2016 The Kubernetes Authors. | ||||||
|  |  | ||||||
|  | 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 remotecommand | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"testing" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestV4ErrorDecoder(t *testing.T) { | ||||||
|  | 	dec := errorDecoderV4{} | ||||||
|  |  | ||||||
|  | 	type Test struct { | ||||||
|  | 		message string | ||||||
|  | 		err     string | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, test := range []Test{ | ||||||
|  | 		{ | ||||||
|  | 			message: "{}", | ||||||
|  | 			err:     "error stream protocol error: unknown error", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			message: "{", | ||||||
|  | 			err:     "error stream protocol error: unexpected end of JSON input in \"{\"", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			message: `{"status": "Success" }`, | ||||||
|  | 			err:     "", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			message: `{"status": "Failure", "message": "foobar" }`, | ||||||
|  | 			err:     "foobar", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			message: `{"status": "Failure", "message": "foobar", "reason": "NonZeroExitCode", "details": {"causes": [{"reason": "foo"}] } }`, | ||||||
|  | 			err:     "error stream protocol error: no ExitCode cause given", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			message: `{"status": "Failure", "message": "foobar", "reason": "NonZeroExitCode", "details": {"causes": [{"reason": "ExitCode"}] } }`, | ||||||
|  | 			err:     "error stream protocol error: invalid exit code value \"\"", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			message: `{"status": "Failure", "message": "foobar", "reason": "NonZeroExitCode", "details": {"causes": [{"reason": "ExitCode", "message": "42"}] } }`, | ||||||
|  | 			err:     "command terminated with exit code 42", | ||||||
|  | 		}, | ||||||
|  | 	} { | ||||||
|  | 		err := dec.decode([]byte(test.message)) | ||||||
|  | 		want := test.err | ||||||
|  | 		if want == "" { | ||||||
|  | 			want = "<nil>" | ||||||
|  | 		} | ||||||
|  | 		if got := fmt.Sprintf("%v", err); got != want { | ||||||
|  | 			t.Errorf("wrong error for message %q: want=%q, got=%q", test.message, want, got) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @@ -37,6 +37,8 @@ import ( | |||||||
| 	cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" | 	cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" | ||||||
| 	"k8s.io/kubernetes/pkg/kubectl/resource" | 	"k8s.io/kubernetes/pkg/kubectl/resource" | ||||||
| 	"k8s.io/kubernetes/pkg/runtime" | 	"k8s.io/kubernetes/pkg/runtime" | ||||||
|  | 	uexec "k8s.io/kubernetes/pkg/util/exec" | ||||||
|  | 	"k8s.io/kubernetes/pkg/watch" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var ( | var ( | ||||||
| @@ -114,7 +116,7 @@ func addRunFlags(cmd *cobra.Command) { | |||||||
| 	cmd.Flags().StringP("labels", "l", "", "Labels to apply to the pod(s).") | 	cmd.Flags().StringP("labels", "l", "", "Labels to apply to the pod(s).") | ||||||
| 	cmd.Flags().BoolP("stdin", "i", false, "Keep stdin open on the container(s) in the pod, even if nothing is attached.") | 	cmd.Flags().BoolP("stdin", "i", false, "Keep stdin open on the container(s) in the pod, even if nothing is attached.") | ||||||
| 	cmd.Flags().BoolP("tty", "t", false, "Allocated a TTY for each container in the pod.") | 	cmd.Flags().BoolP("tty", "t", false, "Allocated a TTY for each container in the pod.") | ||||||
| 	cmd.Flags().Bool("attach", false, "If true, wait for the Pod to start running, and then attach to the Pod as if 'kubectl attach ...' were called.  Default false, unless '-i/--stdin' is set, in which case the default is true.") | 	cmd.Flags().Bool("attach", false, "If true, wait for the Pod to start running, and then attach to the Pod as if 'kubectl attach ...' were called.  Default false, unless '-i/--stdin' is set, in which case the default is true. With '--restart=Never' the exit code of the container process is returned.") | ||||||
| 	cmd.Flags().Bool("leave-stdin-open", false, "If the pod is started in interactive mode or with stdin, leave stdin open after the first attach completes. By default, stdin will be closed after the first attach completes.") | 	cmd.Flags().Bool("leave-stdin-open", false, "If the pod is started in interactive mode or with stdin, leave stdin open after the first attach completes. By default, stdin will be closed after the first attach completes.") | ||||||
| 	cmd.Flags().String("restart", "Always", "The restart policy for this Pod.  Legal values [Always, OnFailure, Never].  If set to 'Always' a deployment is created for this pod, if set to 'OnFailure', a job is created for this pod, if set to 'Never', a regular pod is created. For the latter two --replicas must be 1.  Default 'Always'") | 	cmd.Flags().String("restart", "Always", "The restart policy for this Pod.  Legal values [Always, OnFailure, Never].  If set to 'Always' a deployment is created for this pod, if set to 'OnFailure', a job is created for this pod, if set to 'Never', a regular pod is created. For the latter two --replicas must be 1.  Default 'Always'") | ||||||
| 	cmd.Flags().Bool("command", false, "If true and extra arguments are present, use them as the 'command' field in the container, rather than the 'args' field which is the default.") | 	cmd.Flags().Bool("command", false, "If true and extra arguments are present, use them as the 'command' field in the container, rather than the 'args' field which is the default.") | ||||||
| @@ -128,7 +130,6 @@ func addRunFlags(cmd *cobra.Command) { | |||||||
| } | } | ||||||
|  |  | ||||||
| func Run(f *cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer, cmd *cobra.Command, args []string, argsLenAtDash int) error { | func Run(f *cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer, cmd *cobra.Command, args []string, argsLenAtDash int) error { | ||||||
| 	quiet := cmdutil.GetFlagBool(cmd, "quiet") |  | ||||||
| 	if len(os.Args) > 1 && os.Args[1] == "run-container" { | 	if len(os.Args) > 1 && os.Args[1] == "run-container" { | ||||||
| 		printDeprecationWarning("run", "run-container") | 		printDeprecationWarning("run", "run-container") | ||||||
| 	} | 	} | ||||||
| @@ -243,6 +244,7 @@ func Run(f *cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer, cmd *cob | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if attach { | 	if attach { | ||||||
|  | 		quiet := cmdutil.GetFlagBool(cmd, "quiet") | ||||||
| 		opts := &AttachOptions{ | 		opts := &AttachOptions{ | ||||||
| 			StreamOptions: StreamOptions{ | 			StreamOptions: StreamOptions{ | ||||||
| 				In:    cmdIn, | 				In:    cmdIn, | ||||||
| @@ -273,11 +275,21 @@ func Run(f *cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer, cmd *cob | |||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 		err = handleAttachPod(f, client, attachablePod, opts, quiet) | 		err = handleAttachPod(f, client, attachablePod.Namespace, attachablePod.Name, opts, quiet) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		var pod *api.Pod | ||||||
|  | 		leaveStdinOpen := cmdutil.GetFlagBool(cmd, "leave-stdin-open") | ||||||
|  | 		waitForExitCode := !leaveStdinOpen && restartPolicy == api.RestartPolicyNever | ||||||
|  | 		if waitForExitCode { | ||||||
|  | 			pod, err = waitForPodTerminated(client, attachablePod.Namespace, attachablePod.Name, opts.Out, quiet) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		if remove { | 		if remove { | ||||||
| 			namespace, err = mapping.MetadataAccessor.Namespace(obj) | 			namespace, err = mapping.MetadataAccessor.Namespace(obj) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| @@ -295,9 +307,37 @@ func Run(f *cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer, cmd *cob | |||||||
| 				ResourceNames(mapping.Resource, name). | 				ResourceNames(mapping.Resource, name). | ||||||
| 				Flatten(). | 				Flatten(). | ||||||
| 				Do() | 				Do() | ||||||
| 			return ReapResult(r, f, cmdOut, true, true, 0, -1, false, mapper, quiet) | 			err = ReapResult(r, f, cmdOut, true, true, 0, -1, false, mapper, quiet) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// after removal is done, return successfully if we are not interested in the exit code | ||||||
|  | 		if !waitForExitCode { | ||||||
|  | 			return nil | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		switch pod.Status.Phase { | ||||||
|  | 		case api.PodSucceeded: | ||||||
|  | 			return nil | ||||||
|  | 		case api.PodFailed: | ||||||
|  | 			unknownRcErr := fmt.Errorf("pod %s/%s failed with unknown exit code", pod.Namespace, pod.Name) | ||||||
|  | 			if len(pod.Status.ContainerStatuses) == 0 || pod.Status.ContainerStatuses[0].State.Terminated == nil { | ||||||
|  | 				return unknownRcErr | ||||||
|  | 			} | ||||||
|  | 			// assume here that we have at most one status because kubectl-run only creates one container per pod | ||||||
|  | 			rc := pod.Status.ContainerStatuses[0].State.Terminated.ExitCode | ||||||
|  | 			if rc == 0 { | ||||||
|  | 				return unknownRcErr | ||||||
|  | 			} | ||||||
|  | 			return uexec.CodeExitError{ | ||||||
|  | 				Err:  fmt.Errorf("pod %s/%s terminated", pod.Namespace, pod.Name), | ||||||
|  | 				Code: int(rc), | ||||||
|  | 			} | ||||||
|  | 		default: | ||||||
|  | 			return fmt.Errorf("pod %s/%s left in phase %s", pod.Namespace, pod.Name, pod.Status.Phase) | ||||||
| 		} | 		} | ||||||
| 		return nil |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	outputFormat := cmdutil.GetFlagString(cmd, "output") | 	outputFormat := cmdutil.GetFlagString(cmd, "output") | ||||||
| @@ -325,37 +365,91 @@ func contains(resourcesList map[string]*unversioned.APIResourceList, resource un | |||||||
| 	return false | 	return false | ||||||
| } | } | ||||||
|  |  | ||||||
| func waitForPodRunning(c *client.Client, pod *api.Pod, out io.Writer, quiet bool) (status api.PodPhase, err error) { | // waitForPod watches the given pod until the exitCondition is true. Each two seconds | ||||||
| 	for { | // the tick function is called e.g. for progress output. | ||||||
| 		pod, err := c.Pods(pod.Namespace).Get(pod.Name) | func waitForPod(c *client.Client, ns, name string, exitCondition func(*api.Pod) bool, tick func(*api.Pod)) (*api.Pod, error) { | ||||||
| 		if err != nil { | 	pod, err := c.Pods(ns).Get(name) | ||||||
| 			return api.PodUnknown, err | 	if err != nil { | ||||||
| 		} | 		return nil, err | ||||||
| 		ready := false |  | ||||||
| 		if pod.Status.Phase == api.PodRunning { |  | ||||||
| 			ready = true |  | ||||||
| 			for _, status := range pod.Status.ContainerStatuses { |  | ||||||
| 				if !status.Ready { |  | ||||||
| 					ready = false |  | ||||||
| 					break |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 			if ready { |  | ||||||
| 				return api.PodRunning, nil |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 		if pod.Status.Phase == api.PodSucceeded || pod.Status.Phase == api.PodFailed { |  | ||||||
| 			return pod.Status.Phase, nil |  | ||||||
| 		} |  | ||||||
| 		if !quiet { |  | ||||||
| 			fmt.Fprintf(out, "Waiting for pod %s/%s to be running, status is %s, pod ready: %v\n", pod.Namespace, pod.Name, pod.Status.Phase, ready) |  | ||||||
| 		} |  | ||||||
| 		time.Sleep(2 * time.Second) |  | ||||||
| 	} | 	} | ||||||
|  | 	if exitCondition(pod) { | ||||||
|  | 		return pod, nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	tick(pod) | ||||||
|  |  | ||||||
|  | 	w, err := c.Pods(ns).Watch(api.SingleObject(api.ObjectMeta{Name: pod.Name, ResourceVersion: pod.ResourceVersion})) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	t := time.NewTicker(2 * time.Second) | ||||||
|  | 	defer t.Stop() | ||||||
|  | 	go func() { | ||||||
|  | 		for range t.C { | ||||||
|  | 			tick(pod) | ||||||
|  | 		} | ||||||
|  | 	}() | ||||||
|  |  | ||||||
|  | 	err = nil | ||||||
|  | 	result := pod | ||||||
|  | 	kubectl.WatchLoop(w, func(ev watch.Event) error { | ||||||
|  | 		switch ev.Type { | ||||||
|  | 		case watch.Added, watch.Modified: | ||||||
|  | 			pod = ev.Object.(*api.Pod) | ||||||
|  | 			if exitCondition(pod) { | ||||||
|  | 				result = pod | ||||||
|  | 				w.Stop() | ||||||
|  | 			} | ||||||
|  | 		case watch.Deleted: | ||||||
|  | 			w.Stop() | ||||||
|  | 		case watch.Error: | ||||||
|  | 			result = nil | ||||||
|  | 			err = fmt.Errorf("failed to watch pod %s/%s", ns, name) | ||||||
|  | 			w.Stop() | ||||||
|  | 		} | ||||||
|  | 		return nil | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	return result, err | ||||||
| } | } | ||||||
|  |  | ||||||
| func handleAttachPod(f *cmdutil.Factory, c *client.Client, pod *api.Pod, opts *AttachOptions, quiet bool) error { | func waitForPodRunning(c *client.Client, ns, name string, out io.Writer, quiet bool) (*api.Pod, error) { | ||||||
| 	status, err := waitForPodRunning(c, pod, opts.Out, quiet) | 	exitCondition := func(pod *api.Pod) bool { | ||||||
|  | 		switch pod.Status.Phase { | ||||||
|  | 		case api.PodRunning: | ||||||
|  | 			for _, status := range pod.Status.ContainerStatuses { | ||||||
|  | 				if !status.Ready { | ||||||
|  | 					return false | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 			return true | ||||||
|  | 		case api.PodSucceeded, api.PodFailed: | ||||||
|  | 			return true | ||||||
|  | 		default: | ||||||
|  | 			return false | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return waitForPod(c, ns, name, exitCondition, func(pod *api.Pod) { | ||||||
|  | 		if !quiet { | ||||||
|  | 			fmt.Fprintf(out, "Waiting for pod %s/%s to be running, status is %s, pod ready: false\n", pod.Namespace, pod.Name, pod.Status.Phase) | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func waitForPodTerminated(c *client.Client, ns, name string, out io.Writer, quiet bool) (*api.Pod, error) { | ||||||
|  | 	exitCondition := func(pod *api.Pod) bool { | ||||||
|  | 		return pod.Status.Phase == api.PodSucceeded || pod.Status.Phase == api.PodFailed | ||||||
|  | 	} | ||||||
|  | 	return waitForPod(c, ns, name, exitCondition, func(pod *api.Pod) { | ||||||
|  | 		if !quiet { | ||||||
|  | 			fmt.Fprintf(out, "Waiting for pod %s/%s to terminate, status is %s\n", pod.Namespace, pod.Name, pod.Status.Phase) | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func handleAttachPod(f *cmdutil.Factory, c *client.Client, ns, name string, opts *AttachOptions, quiet bool) error { | ||||||
|  | 	pod, err := waitForPodRunning(c, ns, name, opts.Out, quiet) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| @@ -363,7 +457,7 @@ func handleAttachPod(f *cmdutil.Factory, c *client.Client, pod *api.Pod, opts *A | |||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	if status == api.PodSucceeded || status == api.PodFailed { | 	if pod.Status.Phase == api.PodSucceeded || pod.Status.Phase == api.PodFailed { | ||||||
| 		req, err := f.LogsForObject(pod, &api.PodLogOptions{Container: ctrName}) | 		req, err := f.LogsForObject(pod, &api.PodLogOptions{Container: ctrName}) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| @@ -377,8 +471,8 @@ func handleAttachPod(f *cmdutil.Factory, c *client.Client, pod *api.Pod, opts *A | |||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	opts.Client = c | 	opts.Client = c | ||||||
| 	opts.PodName = pod.Name | 	opts.PodName = name | ||||||
| 	opts.Namespace = pod.Namespace | 	opts.Namespace = ns | ||||||
| 	if err := opts.Run(); err != nil { | 	if err := opts.Run(); err != nil { | ||||||
| 		fmt.Fprintf(opts.Out, "Error attaching, falling back to logs: %v\n", err) | 		fmt.Fprintf(opts.Out, "Error attaching, falling back to logs: %v\n", err) | ||||||
| 		req, err := f.LogsForObject(pod, &api.PodLogOptions{Container: ctrName}) | 		req, err := f.LogsForObject(pod, &api.PodLogOptions{Container: ctrName}) | ||||||
|   | |||||||
| @@ -39,6 +39,7 @@ import ( | |||||||
| 	"k8s.io/kubernetes/pkg/kubectl/resource" | 	"k8s.io/kubernetes/pkg/kubectl/resource" | ||||||
| 	"k8s.io/kubernetes/pkg/runtime" | 	"k8s.io/kubernetes/pkg/runtime" | ||||||
| 	utilerrors "k8s.io/kubernetes/pkg/util/errors" | 	utilerrors "k8s.io/kubernetes/pkg/util/errors" | ||||||
|  | 	utilexec "k8s.io/kubernetes/pkg/util/exec" | ||||||
| 	"k8s.io/kubernetes/pkg/util/sets" | 	"k8s.io/kubernetes/pkg/util/sets" | ||||||
| 	"k8s.io/kubernetes/pkg/util/strategicpatch" | 	"k8s.io/kubernetes/pkg/util/strategicpatch" | ||||||
|  |  | ||||||
| @@ -150,6 +151,9 @@ func checkErr(prefix string, err error, handleErr func(string, int)) { | |||||||
| 			} | 			} | ||||||
| 		case utilerrors.Aggregate: | 		case utilerrors.Aggregate: | ||||||
| 			handleErr(MultipleErrors(prefix, err.Errors()), DefaultErrorExitCode) | 			handleErr(MultipleErrors(prefix, err.Errors()), DefaultErrorExitCode) | ||||||
|  | 		case utilexec.ExitError: | ||||||
|  | 			// do not print anything, only terminate with given error | ||||||
|  | 			handleErr("", err.ExitStatus()) | ||||||
| 		default: // for any other error type | 		default: // for any other error type | ||||||
| 			msg, ok := StandardErrorMessage(err) | 			msg, ok := StandardErrorMessage(err) | ||||||
| 			if !ok { | 			if !ok { | ||||||
|   | |||||||
| @@ -34,6 +34,7 @@ import ( | |||||||
| 	"k8s.io/kubernetes/pkg/api/v1" | 	"k8s.io/kubernetes/pkg/api/v1" | ||||||
| 	"k8s.io/kubernetes/pkg/apis/extensions" | 	"k8s.io/kubernetes/pkg/apis/extensions" | ||||||
| 	"k8s.io/kubernetes/pkg/runtime" | 	"k8s.io/kubernetes/pkg/runtime" | ||||||
|  | 	uexec "k8s.io/kubernetes/pkg/util/exec" | ||||||
| 	"k8s.io/kubernetes/pkg/util/validation/field" | 	"k8s.io/kubernetes/pkg/util/validation/field" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -269,6 +270,16 @@ func TestCheckNoResourceMatchError(t *testing.T) { | |||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func TestCheckExitError(t *testing.T) { | ||||||
|  | 	testCheckError(t, []checkErrTestCase{ | ||||||
|  | 		{ | ||||||
|  | 			uexec.CodeExitError{Err: fmt.Errorf("pod foo/bar terminated"), Code: 42}, | ||||||
|  | 			"", | ||||||
|  | 			42, | ||||||
|  | 		}, | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  |  | ||||||
| func testCheckError(t *testing.T, tests []checkErrTestCase) { | func testCheckError(t *testing.T, tests []checkErrTestCase) { | ||||||
| 	var errReturned string | 	var errReturned string | ||||||
| 	var codeReturned int | 	var codeReturned int | ||||||
|   | |||||||
| @@ -26,6 +26,7 @@ import ( | |||||||
| 	dockertypes "github.com/docker/engine-api/types" | 	dockertypes "github.com/docker/engine-api/types" | ||||||
| 	"github.com/golang/glog" | 	"github.com/golang/glog" | ||||||
| 	kubecontainer "k8s.io/kubernetes/pkg/kubelet/container" | 	kubecontainer "k8s.io/kubernetes/pkg/kubelet/container" | ||||||
|  | 	utilexec "k8s.io/kubernetes/pkg/util/exec" | ||||||
| 	"k8s.io/kubernetes/pkg/util/term" | 	"k8s.io/kubernetes/pkg/util/term" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -74,7 +75,7 @@ func (*NsenterExecHandler) ExecInContainer(client DockerInterface, container *do | |||||||
| 			go io.Copy(stdout, p) | 			go io.Copy(stdout, p) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		return command.Wait() | 		err = command.Wait() | ||||||
| 	} else { | 	} else { | ||||||
| 		if stdin != nil { | 		if stdin != nil { | ||||||
| 			// Use an os.Pipe here as it returns true *os.File objects. | 			// Use an os.Pipe here as it returns true *os.File objects. | ||||||
| @@ -96,8 +97,13 @@ func (*NsenterExecHandler) ExecInContainer(client DockerInterface, container *do | |||||||
| 			command.Stderr = stderr | 			command.Stderr = stderr | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		return command.Run() | 		err = command.Run() | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	if exitErr, ok := err.(*exec.ExitError); ok { | ||||||
|  | 		return &utilexec.ExitErrorWrapper{ExitError: exitErr} | ||||||
|  | 	} | ||||||
|  | 	return err | ||||||
| } | } | ||||||
|  |  | ||||||
| // NativeExecHandler executes commands in Docker containers using Docker's exec API. | // NativeExecHandler executes commands in Docker containers using Docker's exec API. | ||||||
|   | |||||||
| @@ -17,12 +17,13 @@ limitations under the License. | |||||||
| package remotecommand | package remotecommand | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"errors" |  | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io" | 	"io" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
|  | 	apierrors "k8s.io/kubernetes/pkg/api/errors" | ||||||
|  | 	"k8s.io/kubernetes/pkg/api/unversioned" | ||||||
| 	"k8s.io/kubernetes/pkg/types" | 	"k8s.io/kubernetes/pkg/types" | ||||||
| 	"k8s.io/kubernetes/pkg/util/runtime" | 	"k8s.io/kubernetes/pkg/util/runtime" | ||||||
| 	"k8s.io/kubernetes/pkg/util/term" | 	"k8s.io/kubernetes/pkg/util/term" | ||||||
| @@ -47,8 +48,12 @@ func ServeAttach(w http.ResponseWriter, req *http.Request, attacher Attacher, po | |||||||
|  |  | ||||||
| 	err := attacher.AttachContainer(podName, uid, container, ctx.stdinStream, ctx.stdoutStream, ctx.stderrStream, ctx.tty, ctx.resizeChan) | 	err := attacher.AttachContainer(podName, uid, container, ctx.stdinStream, ctx.stdoutStream, ctx.stderrStream, ctx.tty, ctx.resizeChan) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		msg := fmt.Sprintf("error attaching to container: %v", err) | 		err = fmt.Errorf("error attaching to container: %v", err) | ||||||
| 		runtime.HandleError(errors.New(msg)) | 		runtime.HandleError(err) | ||||||
| 		fmt.Fprint(ctx.errorStream, msg) | 		ctx.writeStatus(apierrors.NewInternalError(err)) | ||||||
|  | 	} else { | ||||||
|  | 		ctx.writeStatus(&apierrors.StatusError{ErrStatus: unversioned.Status{ | ||||||
|  | 			Status: unversioned.StatusSuccess, | ||||||
|  | 		}}) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -36,6 +36,11 @@ const ( | |||||||
| 	// attachment/execution. It is the third version of the subprotocol and | 	// attachment/execution. It is the third version of the subprotocol and | ||||||
| 	// adds support for resizing container terminals. | 	// adds support for resizing container terminals. | ||||||
| 	StreamProtocolV3Name = "v3.channel.k8s.io" | 	StreamProtocolV3Name = "v3.channel.k8s.io" | ||||||
|  |  | ||||||
|  | 	// The SPDY subprotocol "v4.channel.k8s.io" is used for remote command | ||||||
|  | 	// attachment/execution. It is the 4th version of the subprotocol and | ||||||
|  | 	// adds support for exit codes. | ||||||
|  | 	StreamProtocolV4Name = "v4.channel.k8s.io" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var SupportedStreamingProtocols = []string{StreamProtocolV3Name, StreamProtocolV2Name, StreamProtocolV1Name} | var SupportedStreamingProtocols = []string{StreamProtocolV4Name, StreamProtocolV3Name, StreamProtocolV2Name, StreamProtocolV1Name} | ||||||
|   | |||||||
| @@ -17,18 +17,25 @@ limitations under the License. | |||||||
| package remotecommand | package remotecommand | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"errors" |  | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io" | 	"io" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"k8s.io/kubernetes/pkg/api" | 	"k8s.io/kubernetes/pkg/api" | ||||||
|  | 	apierrors "k8s.io/kubernetes/pkg/api/errors" | ||||||
|  | 	"k8s.io/kubernetes/pkg/api/unversioned" | ||||||
| 	"k8s.io/kubernetes/pkg/types" | 	"k8s.io/kubernetes/pkg/types" | ||||||
|  | 	utilexec "k8s.io/kubernetes/pkg/util/exec" | ||||||
| 	"k8s.io/kubernetes/pkg/util/runtime" | 	"k8s.io/kubernetes/pkg/util/runtime" | ||||||
| 	"k8s.io/kubernetes/pkg/util/term" | 	"k8s.io/kubernetes/pkg/util/term" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	NonZeroExitCodeReason = unversioned.StatusReason("NonZeroExitCode") | ||||||
|  | 	ExitCodeCauseType     = unversioned.CauseType("ExitCode") | ||||||
|  | ) | ||||||
|  |  | ||||||
| // Executor knows how to execute a command in a container in a pod. | // Executor knows how to execute a command in a container in a pod. | ||||||
| type Executor interface { | type Executor interface { | ||||||
| 	// ExecInContainer executes a command in a container in the pod, copying data | 	// ExecInContainer executes a command in a container in the pod, copying data | ||||||
| @@ -51,8 +58,29 @@ func ServeExec(w http.ResponseWriter, req *http.Request, executor Executor, podN | |||||||
|  |  | ||||||
| 	err := executor.ExecInContainer(podName, uid, container, cmd, ctx.stdinStream, ctx.stdoutStream, ctx.stderrStream, ctx.tty, ctx.resizeChan) | 	err := executor.ExecInContainer(podName, uid, container, cmd, ctx.stdinStream, ctx.stdoutStream, ctx.stderrStream, ctx.tty, ctx.resizeChan) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		msg := fmt.Sprintf("error executing command in container: %v", err) | 		if exitErr, ok := err.(utilexec.ExitError); ok && exitErr.Exited() { | ||||||
| 		runtime.HandleError(errors.New(msg)) | 			rc := exitErr.ExitStatus() | ||||||
| 		fmt.Fprint(ctx.errorStream, msg) | 			ctx.writeStatus(&apierrors.StatusError{ErrStatus: unversioned.Status{ | ||||||
|  | 				Status: unversioned.StatusFailure, | ||||||
|  | 				Reason: NonZeroExitCodeReason, | ||||||
|  | 				Details: &unversioned.StatusDetails{ | ||||||
|  | 					Causes: []unversioned.StatusCause{ | ||||||
|  | 						{ | ||||||
|  | 							Type:    ExitCodeCauseType, | ||||||
|  | 							Message: fmt.Sprintf("%d", rc), | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 				Message: fmt.Sprintf("command terminated with non-zero exit code: %v", exitErr), | ||||||
|  | 			}}) | ||||||
|  | 		} else { | ||||||
|  | 			err = fmt.Errorf("error executing command in container: %v", err) | ||||||
|  | 			runtime.HandleError(err) | ||||||
|  | 			ctx.writeStatus(apierrors.NewInternalError(err)) | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		ctx.writeStatus(&apierrors.StatusError{ErrStatus: unversioned.Status{ | ||||||
|  | 			Status: unversioned.StatusSuccess, | ||||||
|  | 		}}) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -25,6 +25,8 @@ import ( | |||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"k8s.io/kubernetes/pkg/api" | 	"k8s.io/kubernetes/pkg/api" | ||||||
|  | 	apierrors "k8s.io/kubernetes/pkg/api/errors" | ||||||
|  | 	"k8s.io/kubernetes/pkg/api/unversioned" | ||||||
| 	"k8s.io/kubernetes/pkg/util/httpstream" | 	"k8s.io/kubernetes/pkg/util/httpstream" | ||||||
| 	"k8s.io/kubernetes/pkg/util/httpstream/spdy" | 	"k8s.io/kubernetes/pkg/util/httpstream/spdy" | ||||||
| 	"k8s.io/kubernetes/pkg/util/runtime" | 	"k8s.io/kubernetes/pkg/util/runtime" | ||||||
| @@ -88,7 +90,7 @@ type context struct { | |||||||
| 	stdinStream  io.ReadCloser | 	stdinStream  io.ReadCloser | ||||||
| 	stdoutStream io.WriteCloser | 	stdoutStream io.WriteCloser | ||||||
| 	stderrStream io.WriteCloser | 	stderrStream io.WriteCloser | ||||||
| 	errorStream  io.WriteCloser | 	writeStatus  func(status *apierrors.StatusError) error | ||||||
| 	resizeStream io.ReadCloser | 	resizeStream io.ReadCloser | ||||||
| 	resizeChan   chan term.Size | 	resizeChan   chan term.Size | ||||||
| 	tty          bool | 	tty          bool | ||||||
| @@ -168,6 +170,8 @@ func createHttpStreamStreams(req *http.Request, w http.ResponseWriter, opts *opt | |||||||
|  |  | ||||||
| 	var handler protocolHandler | 	var handler protocolHandler | ||||||
| 	switch protocol { | 	switch protocol { | ||||||
|  | 	case StreamProtocolV4Name: | ||||||
|  | 		handler = &v4ProtocolHandler{} | ||||||
| 	case StreamProtocolV3Name: | 	case StreamProtocolV3Name: | ||||||
| 		handler = &v3ProtocolHandler{} | 		handler = &v3ProtocolHandler{} | ||||||
| 	case StreamProtocolV2Name: | 	case StreamProtocolV2Name: | ||||||
| @@ -206,6 +210,59 @@ type protocolHandler interface { | |||||||
| 	supportsTerminalResizing() bool | 	supportsTerminalResizing() bool | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // v4ProtocolHandler implements the V4 protocol version for streaming command execution. It only differs | ||||||
|  | // in from v3 in the error stream format using an json-marshaled unversioned.Status which carries | ||||||
|  | // the process' exit code. | ||||||
|  | type v4ProtocolHandler struct{} | ||||||
|  |  | ||||||
|  | func (*v4ProtocolHandler) waitForStreams(streams <-chan streamAndReply, expectedStreams int, expired <-chan time.Time) (*context, error) { | ||||||
|  | 	ctx := &context{} | ||||||
|  | 	receivedStreams := 0 | ||||||
|  | 	replyChan := make(chan struct{}) | ||||||
|  | 	stop := make(chan struct{}) | ||||||
|  | 	defer close(stop) | ||||||
|  | WaitForStreams: | ||||||
|  | 	for { | ||||||
|  | 		select { | ||||||
|  | 		case stream := <-streams: | ||||||
|  | 			streamType := stream.Headers().Get(api.StreamType) | ||||||
|  | 			switch streamType { | ||||||
|  | 			case api.StreamTypeError: | ||||||
|  | 				ctx.writeStatus = v4WriteStatusFunc(stream) // write json errors | ||||||
|  | 				go waitStreamReply(stream.replySent, replyChan, stop) | ||||||
|  | 			case api.StreamTypeStdin: | ||||||
|  | 				ctx.stdinStream = stream | ||||||
|  | 				go waitStreamReply(stream.replySent, replyChan, stop) | ||||||
|  | 			case api.StreamTypeStdout: | ||||||
|  | 				ctx.stdoutStream = stream | ||||||
|  | 				go waitStreamReply(stream.replySent, replyChan, stop) | ||||||
|  | 			case api.StreamTypeStderr: | ||||||
|  | 				ctx.stderrStream = stream | ||||||
|  | 				go waitStreamReply(stream.replySent, replyChan, stop) | ||||||
|  | 			case api.StreamTypeResize: | ||||||
|  | 				ctx.resizeStream = stream | ||||||
|  | 				go waitStreamReply(stream.replySent, replyChan, stop) | ||||||
|  | 			default: | ||||||
|  | 				runtime.HandleError(fmt.Errorf("Unexpected stream type: %q", streamType)) | ||||||
|  | 			} | ||||||
|  | 		case <-replyChan: | ||||||
|  | 			receivedStreams++ | ||||||
|  | 			if receivedStreams == expectedStreams { | ||||||
|  | 				break WaitForStreams | ||||||
|  | 			} | ||||||
|  | 		case <-expired: | ||||||
|  | 			// TODO find a way to return the error to the user. Maybe use a separate | ||||||
|  | 			// stream to report errors? | ||||||
|  | 			return nil, errors.New("timed out waiting for client to create streams") | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return ctx, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // supportsTerminalResizing returns true because v4ProtocolHandler supports it | ||||||
|  | func (*v4ProtocolHandler) supportsTerminalResizing() bool { return true } | ||||||
|  |  | ||||||
| // v3ProtocolHandler implements the V3 protocol version for streaming command execution. | // v3ProtocolHandler implements the V3 protocol version for streaming command execution. | ||||||
| type v3ProtocolHandler struct{} | type v3ProtocolHandler struct{} | ||||||
|  |  | ||||||
| @@ -222,7 +279,7 @@ WaitForStreams: | |||||||
| 			streamType := stream.Headers().Get(api.StreamType) | 			streamType := stream.Headers().Get(api.StreamType) | ||||||
| 			switch streamType { | 			switch streamType { | ||||||
| 			case api.StreamTypeError: | 			case api.StreamTypeError: | ||||||
| 				ctx.errorStream = stream | 				ctx.writeStatus = v1WriteStatusFunc(stream) | ||||||
| 				go waitStreamReply(stream.replySent, replyChan, stop) | 				go waitStreamReply(stream.replySent, replyChan, stop) | ||||||
| 			case api.StreamTypeStdin: | 			case api.StreamTypeStdin: | ||||||
| 				ctx.stdinStream = stream | 				ctx.stdinStream = stream | ||||||
| @@ -273,7 +330,7 @@ WaitForStreams: | |||||||
| 			streamType := stream.Headers().Get(api.StreamType) | 			streamType := stream.Headers().Get(api.StreamType) | ||||||
| 			switch streamType { | 			switch streamType { | ||||||
| 			case api.StreamTypeError: | 			case api.StreamTypeError: | ||||||
| 				ctx.errorStream = stream | 				ctx.writeStatus = v1WriteStatusFunc(stream) | ||||||
| 				go waitStreamReply(stream.replySent, replyChan, stop) | 				go waitStreamReply(stream.replySent, replyChan, stop) | ||||||
| 			case api.StreamTypeStdin: | 			case api.StreamTypeStdin: | ||||||
| 				ctx.stdinStream = stream | 				ctx.stdinStream = stream | ||||||
| @@ -321,7 +378,7 @@ WaitForStreams: | |||||||
| 			streamType := stream.Headers().Get(api.StreamType) | 			streamType := stream.Headers().Get(api.StreamType) | ||||||
| 			switch streamType { | 			switch streamType { | ||||||
| 			case api.StreamTypeError: | 			case api.StreamTypeError: | ||||||
| 				ctx.errorStream = stream | 				ctx.writeStatus = v1WriteStatusFunc(stream) | ||||||
|  |  | ||||||
| 				// This defer statement shouldn't be here, but due to previous refactoring, it ended up in | 				// This defer statement shouldn't be here, but due to previous refactoring, it ended up in | ||||||
| 				// here. This is what 1.0.x kubelets do, so we're retaining that behavior. This is fixed in | 				// here. This is what 1.0.x kubelets do, so we're retaining that behavior. This is fixed in | ||||||
| @@ -375,3 +432,26 @@ func handleResizeEvents(stream io.Reader, channel chan<- term.Size) { | |||||||
| 		channel <- size | 		channel <- size | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func v1WriteStatusFunc(stream io.WriteCloser) func(status *apierrors.StatusError) error { | ||||||
|  | 	return func(status *apierrors.StatusError) error { | ||||||
|  | 		if status.Status().Status == unversioned.StatusSuccess { | ||||||
|  | 			return nil // send error messages | ||||||
|  | 		} | ||||||
|  | 		_, err := stream.Write([]byte(status.Error())) | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // v4WriteStatusFunc returns a WriteStatusFunc that marshals a given api Status | ||||||
|  | // as json in the error channel. | ||||||
|  | func v4WriteStatusFunc(stream io.WriteCloser) func(status *apierrors.StatusError) error { | ||||||
|  | 	return func(status *apierrors.StatusError) error { | ||||||
|  | 		bs, err := json.Marshal(status.Status()) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 		_, err = stream.Write(bs) | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | } | ||||||
|   | |||||||
| @@ -32,6 +32,11 @@ const ( | |||||||
| 	stderrChannel | 	stderrChannel | ||||||
| 	errorChannel | 	errorChannel | ||||||
| 	resizeChannel | 	resizeChannel | ||||||
|  |  | ||||||
|  | 	preV4BinaryWebsocketProtocol = wsstream.ChannelWebSocketProtocol | ||||||
|  | 	preV4Base64WebsocketProtocol = wsstream.Base64ChannelWebSocketProtocol | ||||||
|  | 	v4BinaryWebsocketProtocol    = "v4." + wsstream.ChannelWebSocketProtocol | ||||||
|  | 	v4Base64WebsocketProtocol    = "v4." + wsstream.Base64ChannelWebSocketProtocol | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // createChannels returns the standard channel types for a shell connection (STDIN 0, STDOUT 1, STDERR 2) | // createChannels returns the standard channel types for a shell connection (STDIN 0, STDOUT 1, STDERR 2) | ||||||
| @@ -67,9 +72,30 @@ func writeChannel(real bool) wsstream.ChannelType { | |||||||
| // streams needed to perform an exec or an attach. | // streams needed to perform an exec or an attach. | ||||||
| func createWebSocketStreams(req *http.Request, w http.ResponseWriter, opts *options, idleTimeout time.Duration) (*context, bool) { | func createWebSocketStreams(req *http.Request, w http.ResponseWriter, opts *options, idleTimeout time.Duration) (*context, bool) { | ||||||
| 	channels := createChannels(opts) | 	channels := createChannels(opts) | ||||||
| 	conn := wsstream.NewConn(channels...) | 	conn := wsstream.NewConn(map[string]wsstream.ChannelProtocolConfig{ | ||||||
|  | 		"": { | ||||||
|  | 			Binary:   true, | ||||||
|  | 			Channels: channels, | ||||||
|  | 		}, | ||||||
|  | 		preV4BinaryWebsocketProtocol: { | ||||||
|  | 			Binary:   true, | ||||||
|  | 			Channels: channels, | ||||||
|  | 		}, | ||||||
|  | 		preV4Base64WebsocketProtocol: { | ||||||
|  | 			Binary:   false, | ||||||
|  | 			Channels: channels, | ||||||
|  | 		}, | ||||||
|  | 		v4BinaryWebsocketProtocol: { | ||||||
|  | 			Binary:   true, | ||||||
|  | 			Channels: channels, | ||||||
|  | 		}, | ||||||
|  | 		v4Base64WebsocketProtocol: { | ||||||
|  | 			Binary:   false, | ||||||
|  | 			Channels: channels, | ||||||
|  | 		}, | ||||||
|  | 	}) | ||||||
| 	conn.SetIdleTimeout(idleTimeout) | 	conn.SetIdleTimeout(idleTimeout) | ||||||
| 	streams, err := conn.Open(httplog.Unlogged(w), req) | 	negotiatedProtocol, streams, err := conn.Open(httplog.Unlogged(w), req) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		runtime.HandleError(fmt.Errorf("Unable to upgrade websocket connection: %v", err)) | 		runtime.HandleError(fmt.Errorf("Unable to upgrade websocket connection: %v", err)) | ||||||
| 		return nil, false | 		return nil, false | ||||||
| @@ -86,13 +112,21 @@ func createWebSocketStreams(req *http.Request, w http.ResponseWriter, opts *opti | |||||||
| 		streams[errorChannel].Write([]byte{}) | 		streams[errorChannel].Write([]byte{}) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return &context{ | 	ctx := &context{ | ||||||
| 		conn:         conn, | 		conn:         conn, | ||||||
| 		stdinStream:  streams[stdinChannel], | 		stdinStream:  streams[stdinChannel], | ||||||
| 		stdoutStream: streams[stdoutChannel], | 		stdoutStream: streams[stdoutChannel], | ||||||
| 		stderrStream: streams[stderrChannel], | 		stderrStream: streams[stderrChannel], | ||||||
| 		errorStream:  streams[errorChannel], |  | ||||||
| 		tty:          opts.tty, | 		tty:          opts.tty, | ||||||
| 		resizeStream: streams[resizeChannel], | 		resizeStream: streams[resizeChannel], | ||||||
| 	}, true | 	} | ||||||
|  |  | ||||||
|  | 	switch negotiatedProtocol { | ||||||
|  | 	case v4BinaryWebsocketProtocol, v4Base64WebsocketProtocol: | ||||||
|  | 		ctx.writeStatus = v4WriteStatusFunc(streams[errorChannel]) | ||||||
|  | 	default: | ||||||
|  | 		ctx.writeStatus = v1WriteStatusFunc(streams[errorChannel]) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return ctx, true | ||||||
| } | } | ||||||
|   | |||||||
| @@ -140,3 +140,28 @@ func (eew ExitErrorWrapper) ExitStatus() int { | |||||||
| 	} | 	} | ||||||
| 	return ws.ExitStatus() | 	return ws.ExitStatus() | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // CodeExitError is an implementation of ExitError consisting of an error object | ||||||
|  | // and an exit code (the upper bits of os.exec.ExitStatus). | ||||||
|  | type CodeExitError struct { | ||||||
|  | 	Err  error | ||||||
|  | 	Code int | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var _ ExitError = CodeExitError{} | ||||||
|  |  | ||||||
|  | func (e CodeExitError) Error() string { | ||||||
|  | 	return e.Err.Error() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (e CodeExitError) String() string { | ||||||
|  | 	return e.Err.Error() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (e CodeExitError) Exited() bool { | ||||||
|  | 	return true | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (e CodeExitError) ExitStatus() int { | ||||||
|  | 	return e.Code | ||||||
|  | } | ||||||
|   | |||||||
| @@ -36,6 +36,7 @@ import ( | |||||||
| 	"strconv" | 	"strconv" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"sync" | 	"sync" | ||||||
|  | 	"syscall" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"k8s.io/kubernetes/federation/client/clientset_generated/federation_release_1_4" | 	"k8s.io/kubernetes/federation/client/clientset_generated/federation_release_1_4" | ||||||
| @@ -62,6 +63,7 @@ import ( | |||||||
| 	"k8s.io/kubernetes/pkg/runtime" | 	"k8s.io/kubernetes/pkg/runtime" | ||||||
| 	sshutil "k8s.io/kubernetes/pkg/ssh" | 	sshutil "k8s.io/kubernetes/pkg/ssh" | ||||||
| 	"k8s.io/kubernetes/pkg/types" | 	"k8s.io/kubernetes/pkg/types" | ||||||
|  | 	uexec "k8s.io/kubernetes/pkg/util/exec" | ||||||
| 	labelsutil "k8s.io/kubernetes/pkg/util/labels" | 	labelsutil "k8s.io/kubernetes/pkg/util/labels" | ||||||
| 	"k8s.io/kubernetes/pkg/util/sets" | 	"k8s.io/kubernetes/pkg/util/sets" | ||||||
| 	"k8s.io/kubernetes/pkg/util/system" | 	"k8s.io/kubernetes/pkg/util/system" | ||||||
| @@ -1996,7 +1998,7 @@ func (b kubectlBuilder) Exec() (string, error) { | |||||||
|  |  | ||||||
| 	Logf("Running '%s %s'", cmd.Path, strings.Join(cmd.Args[1:], " ")) // skip arg[0] as it is printed separately | 	Logf("Running '%s %s'", cmd.Path, strings.Join(cmd.Args[1:], " ")) // skip arg[0] as it is printed separately | ||||||
| 	if err := cmd.Start(); err != nil { | 	if err := cmd.Start(); err != nil { | ||||||
| 		return "", fmt.Errorf("Error starting %v:\nCommand stdout:\n%v\nstderr:\n%v\nerror:\n%v\n", cmd, cmd.Stdout, cmd.Stderr, err) | 		return "", fmt.Errorf("error starting %v:\nCommand stdout:\n%v\nstderr:\n%v\nerror:\n%v\n", cmd, cmd.Stdout, cmd.Stderr, err) | ||||||
| 	} | 	} | ||||||
| 	errCh := make(chan error, 1) | 	errCh := make(chan error, 1) | ||||||
| 	go func() { | 	go func() { | ||||||
| @@ -2005,11 +2007,19 @@ func (b kubectlBuilder) Exec() (string, error) { | |||||||
| 	select { | 	select { | ||||||
| 	case err := <-errCh: | 	case err := <-errCh: | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return "", fmt.Errorf("Error running %v:\nCommand stdout:\n%v\nstderr:\n%v\nerror:\n%v\n", cmd, cmd.Stdout, cmd.Stderr, err) | 			var rc int = 127 | ||||||
|  | 			if ee, ok := err.(*exec.ExitError); ok { | ||||||
|  | 				Logf("rc: %d", rc) | ||||||
|  | 				rc = int(ee.Sys().(syscall.WaitStatus).ExitStatus()) | ||||||
|  | 			} | ||||||
|  | 			return "", uexec.CodeExitError{ | ||||||
|  | 				Err:  fmt.Errorf("error running %v:\nCommand stdout:\n%v\nstderr:\n%v\nerror:\n%v\n", cmd, cmd.Stdout, cmd.Stderr, err), | ||||||
|  | 				Code: rc, | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 	case <-b.timeout: | 	case <-b.timeout: | ||||||
| 		b.cmd.Process.Kill() | 		b.cmd.Process.Kill() | ||||||
| 		return "", fmt.Errorf("Timed out waiting for command %v:\nCommand stdout:\n%v\nstderr:\n%v\n", cmd, cmd.Stdout, cmd.Stderr) | 		return "", fmt.Errorf("timed out waiting for command %v:\nCommand stdout:\n%v\nstderr:\n%v\n", cmd, cmd.Stdout, cmd.Stderr) | ||||||
| 	} | 	} | ||||||
| 	Logf("stderr: %q", stderr.String()) | 	Logf("stderr: %q", stderr.String()) | ||||||
| 	return stdout.String(), nil | 	return stdout.String(), nil | ||||||
|   | |||||||
| @@ -51,6 +51,7 @@ import ( | |||||||
| 	"k8s.io/kubernetes/pkg/kubectl/cmd/util" | 	"k8s.io/kubernetes/pkg/kubectl/cmd/util" | ||||||
| 	"k8s.io/kubernetes/pkg/labels" | 	"k8s.io/kubernetes/pkg/labels" | ||||||
| 	"k8s.io/kubernetes/pkg/registry/generic/registry" | 	"k8s.io/kubernetes/pkg/registry/generic/registry" | ||||||
|  | 	uexec "k8s.io/kubernetes/pkg/util/exec" | ||||||
| 	utilnet "k8s.io/kubernetes/pkg/util/net" | 	utilnet "k8s.io/kubernetes/pkg/util/net" | ||||||
| 	"k8s.io/kubernetes/pkg/util/uuid" | 	"k8s.io/kubernetes/pkg/util/uuid" | ||||||
| 	"k8s.io/kubernetes/pkg/util/wait" | 	"k8s.io/kubernetes/pkg/util/wait" | ||||||
| @@ -348,6 +349,49 @@ var _ = framework.KubeDescribe("Kubectl client", func() { | |||||||
| 			} | 			} | ||||||
| 		}) | 		}) | ||||||
|  |  | ||||||
|  | 		It("should return command exit codes", func() { | ||||||
|  | 			nsFlag := fmt.Sprintf("--namespace=%v", ns) | ||||||
|  |  | ||||||
|  | 			By("execing into a container with a successful command") | ||||||
|  | 			_, err := framework.NewKubectlCommand(nsFlag, "exec", "nginx", "--", "/bin/sh", "-c", "exit 0").Exec() | ||||||
|  | 			ExpectNoError(err) | ||||||
|  |  | ||||||
|  | 			By("execing into a container with a failing command") | ||||||
|  | 			_, err = framework.NewKubectlCommand(nsFlag, "exec", "nginx", "--", "/bin/sh", "-c", "exit 42").Exec() | ||||||
|  | 			ee, ok := err.(uexec.ExitError) | ||||||
|  | 			Expect(ok).To(Equal(true)) | ||||||
|  | 			Expect(ee.ExitStatus()).To(Equal(42)) | ||||||
|  |  | ||||||
|  | 			By("running a successful command") | ||||||
|  | 			_, err = framework.NewKubectlCommand(nsFlag, "run", "-i", "--image="+busyboxImage, "--restart=Never", "success", "--", "/bin/sh", "-c", "exit 0").Exec() | ||||||
|  | 			ExpectNoError(err) | ||||||
|  |  | ||||||
|  | 			By("running a failing command") | ||||||
|  | 			_, err = framework.NewKubectlCommand(nsFlag, "run", "-i", "--image="+busyboxImage, "--restart=Never", "failure-1", "--", "/bin/sh", "-c", "exit 42").Exec() | ||||||
|  | 			ee, ok = err.(uexec.ExitError) | ||||||
|  | 			Expect(ok).To(Equal(true)) | ||||||
|  | 			Expect(ee.ExitStatus()).To(Equal(42)) | ||||||
|  |  | ||||||
|  | 			By("running a failing command without --restart=Never") | ||||||
|  | 			_, err = framework.NewKubectlCommand(nsFlag, "run", "-i", "--image="+busyboxImage, "--restart=OnFailure", "failure-2", "--", "/bin/sh", "-c", "cat && exit 42"). | ||||||
|  | 				WithStdinData("abcd1234"). | ||||||
|  | 				Exec() | ||||||
|  | 			ExpectNoError(err) | ||||||
|  |  | ||||||
|  | 			By("running a failing command without --restart=Never, but with --rm") | ||||||
|  | 			_, err = framework.NewKubectlCommand(nsFlag, "run", "-i", "--image="+busyboxImage, "--restart=OnFailure", "--rm", "failure-3", "--", "/bin/sh", "-c", "cat && exit 42"). | ||||||
|  | 				WithStdinData("abcd1234"). | ||||||
|  | 				Exec() | ||||||
|  | 			ExpectNoError(err) | ||||||
|  | 			framework.WaitForPodToDisappear(f.Client, ns, "failure-3", labels.Everything(), 2*time.Second, wait.ForeverTestTimeout) | ||||||
|  |  | ||||||
|  | 			By("running a failing command with --leave-stdin-open") | ||||||
|  | 			_, err = framework.NewKubectlCommand(nsFlag, "run", "-i", "--image="+busyboxImage, "--restart=Never", "failure-4", "--leave-stdin-open", "--", "/bin/sh", "-c", "exit 42"). | ||||||
|  | 				WithStdinData("abcd1234"). | ||||||
|  | 				Exec() | ||||||
|  | 			ExpectNoError(err) | ||||||
|  | 		}) | ||||||
|  |  | ||||||
| 		It("should support inline execution and attach", func() { | 		It("should support inline execution and attach", func() { | ||||||
| 			framework.SkipIfContainerRuntimeIs("rkt") // #23335 | 			framework.SkipIfContainerRuntimeIs("rkt") // #23335 | ||||||
| 			framework.SkipUnlessServerVersionGTE(jobsVersion, c) | 			framework.SkipUnlessServerVersionGTE(jobsVersion, c) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user