From 0c39d7d3806f1a45148f159b8480ab86df9f23ff Mon Sep 17 00:00:00 2001 From: Ahmad Nurus S Date: Sat, 2 Feb 2019 21:12:11 +0700 Subject: [PATCH] Kubectl exec support resource/name format --- pkg/kubectl/cmd/exec/BUILD | 4 +- pkg/kubectl/cmd/exec/exec.go | 106 +++++++++++++++++++++--------- pkg/kubectl/cmd/exec/exec_test.go | 66 ++++++++++++------- test/cmd/exec.sh | 100 ++++++++++++++++++++++++++++ test/cmd/legacy-script.sh | 11 ++++ test/e2e/kubectl/kubectl.go | 9 +++ 6 files changed, 242 insertions(+), 54 deletions(-) create mode 100755 test/cmd/exec.sh diff --git a/pkg/kubectl/cmd/exec/BUILD b/pkg/kubectl/cmd/exec/BUILD index 1b827ab5e3e..e24c0c62333 100644 --- a/pkg/kubectl/cmd/exec/BUILD +++ b/pkg/kubectl/cmd/exec/BUILD @@ -7,6 +7,7 @@ go_library( visibility = ["//visibility:public"], deps = [ "//pkg/kubectl/cmd/util:go_default_library", + "//pkg/kubectl/polymorphichelpers:go_default_library", "//pkg/kubectl/scheme:go_default_library", "//pkg/kubectl/util/i18n:go_default_library", "//pkg/kubectl/util/interrupt:go_default_library", @@ -15,6 +16,7 @@ go_library( "//staging/src/k8s.io/api/core/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//staging/src/k8s.io/cli-runtime/pkg/genericclioptions:go_default_library", + "//staging/src/k8s.io/cli-runtime/pkg/resource:go_default_library", "//staging/src/k8s.io/client-go/kubernetes/typed/core/v1:go_default_library", "//staging/src/k8s.io/client-go/rest:go_default_library", "//staging/src/k8s.io/client-go/tools/remotecommand:go_default_library", @@ -33,11 +35,11 @@ go_test( "//pkg/kubectl/util/term:go_default_library", "//staging/src/k8s.io/api/core/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", "//staging/src/k8s.io/cli-runtime/pkg/genericclioptions:go_default_library", "//staging/src/k8s.io/client-go/rest:go_default_library", "//staging/src/k8s.io/client-go/rest/fake:go_default_library", "//staging/src/k8s.io/client-go/tools/remotecommand:go_default_library", - "//vendor/github.com/spf13/cobra:go_default_library", ], ) diff --git a/pkg/kubectl/cmd/exec/exec.go b/pkg/kubectl/cmd/exec/exec.go index d448bf804d3..9dc19344d1c 100644 --- a/pkg/kubectl/cmd/exec/exec.go +++ b/pkg/kubectl/cmd/exec/exec.go @@ -20,6 +20,7 @@ import ( "fmt" "io" "net/url" + "time" dockerterm "github.com/docker/docker/pkg/term" "github.com/spf13/cobra" @@ -27,10 +28,12 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/resource" coreclient "k8s.io/client-go/kubernetes/typed/core/v1" restclient "k8s.io/client-go/rest" "k8s.io/client-go/tools/remotecommand" cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" + "k8s.io/kubernetes/pkg/kubectl/polymorphichelpers" "k8s.io/kubernetes/pkg/kubectl/scheme" "k8s.io/kubernetes/pkg/kubectl/util/i18n" "k8s.io/kubernetes/pkg/kubectl/util/interrupt" @@ -40,27 +43,34 @@ import ( var ( execExample = templates.Examples(i18n.T(` - # Get output from running 'date' from pod 123456-7890, using the first container by default - kubectl exec 123456-7890 date + # Get output from running 'date' command from pod mypod, using the first container by default + kubectl exec mypod date - # Get output from running 'date' in ruby-container from pod 123456-7890 - kubectl exec 123456-7890 -c ruby-container date + # Get output from running 'date' command in ruby-container from pod mypod + kubectl exec mypod -c ruby-container date - # Switch to raw terminal mode, sends stdin to 'bash' in ruby-container from pod 123456-7890 + # Switch to raw terminal mode, sends stdin to 'bash' in ruby-container from pod mypod # and sends stdout/stderr from 'bash' back to the client - kubectl exec 123456-7890 -c ruby-container -i -t -- bash -il + kubectl exec mypod -c ruby-container -i -t -- bash -il - # List contents of /usr from the first container of pod 123456-7890 and sort by modification time. + # List contents of /usr from the first container of pod mypod and sort by modification time. # If the command you want to execute in the pod has any flags in common (e.g. -i), # you must use two dashes (--) to separate your command's flags/arguments. # Also note, do not surround your command and its flags/arguments with quotes # unless that is how you would execute it normally (i.e., do ls -t /usr, not "ls -t /usr"). - kubectl exec 123456-7890 -i -t -- ls -t /usr + kubectl exec mypod -i -t -- ls -t /usr + + # Get output from running 'date' command from the first pod of the deployment mydeployment, using the first container by default + kubectl exec deploy/mydeployment date + + # Get output from running 'date' command from the first pod of the service myservice, using the first container by default + kubectl exec svc/myservice date `)) ) const ( - execUsageStr = "expected 'exec POD_NAME COMMAND [ARG1] [ARG2] ... [ARGN]'.\nPOD_NAME and COMMAND are required arguments for the exec command" + execUsageStr = "expected 'exec (POD | TYPE/NAME) COMMAND [ARG1] [ARG2] ... [ARGN]'.\nPOD or TYPE/NAME and COMMAND are required arguments for the exec command" + defaultPodExecTimeout = 60 * time.Second ) func NewCmdExec(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { @@ -72,7 +82,7 @@ func NewCmdExec(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.C Executor: &DefaultRemoteExecutor{}, } cmd := &cobra.Command{ - Use: "exec POD [-c CONTAINER] -- COMMAND [args...]", + Use: "exec (POD | TYPE/NAME) [-c CONTAINER] -- COMMAND [args...]", DisableFlagsInUseLine: true, Short: i18n.T("Execute a command in a container"), Long: "Execute a command in a container.", @@ -84,6 +94,7 @@ func NewCmdExec(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.C cmdutil.CheckErr(options.Run()) }, } + cmdutil.AddPodRunningTimeoutFlag(cmd, defaultPodExecTimeout) cmd.Flags().StringVarP(&options.PodName, "pod", "p", options.PodName, "Pod name") cmd.Flags().MarkDeprecated("pod", "This flag is deprecated and will be removed in future. Use exec POD_NAME instead.") // TODO support UID @@ -137,14 +148,21 @@ type StreamOptions struct { type ExecOptions struct { StreamOptions - Command []string + ResourceName string + Command []string FullCmdName string SuggestedCmdUsage string - Executor RemoteExecutor - PodClient coreclient.PodsGetter - Config *restclient.Config + Builder func() *resource.Builder + ExecutablePodFn polymorphichelpers.AttachablePodForObjectFunc + restClientGetter genericclioptions.RESTClientGetter + + Pod *corev1.Pod + Executor RemoteExecutor + PodClient coreclient.PodsGetter + GetPodTimeout time.Duration + Config *restclient.Config } // Complete verifies command line arguments and loads data from the command environment @@ -159,32 +177,39 @@ func (p *ExecOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, argsIn []s } p.Command = argsIn } else { - p.PodName = argsIn[0] + p.ResourceName = argsIn[0] p.Command = argsIn[1:] - if len(p.Command) < 1 { - return cmdutil.UsageErrorf(cmd, execUsageStr) - } } - namespace, _, err := f.ToRawKubeConfigLoader().Namespace() + var err error + + p.Namespace, _, err = f.ToRawKubeConfigLoader().Namespace() if err != nil { return err } - p.Namespace = namespace + + p.ExecutablePodFn = polymorphichelpers.AttachablePodForObjectFn + + p.GetPodTimeout, err = cmdutil.GetPodRunningTimeoutFlag(cmd) + if err != nil { + return cmdutil.UsageErrorf(cmd, err.Error()) + } + + p.Builder = f.NewBuilder + p.restClientGetter = f cmdParent := cmd.Parent() if cmdParent != nil { p.FullCmdName = cmdParent.CommandPath() } if len(p.FullCmdName) > 0 && cmdutil.IsSiblingCommandExists(cmd, "describe") { - p.SuggestedCmdUsage = fmt.Sprintf("Use '%s describe pod/%s -n %s' to see all of the containers in this pod.", p.FullCmdName, p.PodName, p.Namespace) + p.SuggestedCmdUsage = fmt.Sprintf("Use '%s describe %s -n %s' to see all of the containers in this pod.", p.FullCmdName, p.ResourceName, p.Namespace) } - config, err := f.ToRESTConfig() + p.Config, err = f.ToRESTConfig() if err != nil { return err } - p.Config = config clientset, err := f.KubernetesClientSet() if err != nil { @@ -197,8 +222,8 @@ func (p *ExecOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, argsIn []s // Validate checks that the provided exec options are specified. func (p *ExecOptions) Validate() error { - if len(p.PodName) == 0 { - return fmt.Errorf("pod name must be specified") + if len(p.PodName) == 0 && len(p.ResourceName) == 0 { + return fmt.Errorf("pod or type/name must be specified") } if len(p.Command) == 0 { return fmt.Errorf("you must specify at least one command for the container") @@ -206,9 +231,6 @@ func (p *ExecOptions) Validate() error { if p.Out == nil || p.ErrOut == nil { return fmt.Errorf("both output and error output must be provided") } - if p.Executor == nil || p.PodClient == nil || p.Config == nil { - return fmt.Errorf("client, client config, and executor must be provided") - } return nil } @@ -266,11 +288,33 @@ func (o *StreamOptions) SetupTTY() term.TTY { // Run executes a validated remote execution against a pod. func (p *ExecOptions) Run() error { - pod, err := p.PodClient.Pods(p.Namespace).Get(p.PodName, metav1.GetOptions{}) - if err != nil { - return err + var err error + // we still need legacy pod getter when PodName in ExecOptions struct is provided, + // since there are any other command run this function by providing Podname with PodsGetter + // and without resource builder, eg: `kubectl cp`. + if len(p.PodName) != 0 { + p.Pod, err = p.PodClient.Pods(p.Namespace).Get(p.PodName, metav1.GetOptions{}) + if err != nil { + return err + } + } else { + builder := p.Builder(). + WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...). + NamespaceParam(p.Namespace).DefaultNamespace().ResourceNames("pods", p.ResourceName) + + obj, err := builder.Do().Object() + if err != nil { + return err + } + + p.Pod, err = p.ExecutablePodFn(p.restClientGetter, obj, p.GetPodTimeout) + if err != nil { + return err + } } + pod := p.Pod + if pod.Status.Phase == corev1.PodSucceeded || pod.Status.Phase == corev1.PodFailed { return fmt.Errorf("cannot exec into a container in a completed pod; current phase is %s", pod.Status.Phase) } diff --git a/pkg/kubectl/cmd/exec/exec_test.go b/pkg/kubectl/cmd/exec/exec_test.go index 4c603bcd364..16afe67d3c9 100644 --- a/pkg/kubectl/cmd/exec/exec_test.go +++ b/pkg/kubectl/cmd/exec/exec_test.go @@ -27,10 +27,9 @@ import ( "strings" "testing" - "github.com/spf13/cobra" - corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/cli-runtime/pkg/genericclioptions" restclient "k8s.io/client-go/rest" "k8s.io/client-go/rest/fake" @@ -62,6 +61,7 @@ func TestPodAndContainer(t *testing.T) { expectedPod string expectedContainer string expectedArgs []string + obj *corev1.Pod }{ { p: &ExecOptions{}, @@ -70,16 +70,18 @@ func TestPodAndContainer(t *testing.T) { name: "empty", }, { - p: &ExecOptions{StreamOptions: StreamOptions{PodName: "foo"}}, + p: &ExecOptions{}, argsLenAtDash: -1, expectError: true, name: "no cmd", + obj: execPod(), }, { - p: &ExecOptions{StreamOptions: StreamOptions{PodName: "foo", ContainerName: "bar"}}, + p: &ExecOptions{StreamOptions: StreamOptions{ContainerName: "bar"}}, argsLenAtDash: -1, expectError: true, name: "no cmd, w/ container", + obj: execPod(), }, { p: &ExecOptions{StreamOptions: StreamOptions{PodName: "foo"}}, @@ -88,6 +90,7 @@ func TestPodAndContainer(t *testing.T) { expectedPod: "foo", expectedArgs: []string{"cmd"}, name: "pod in flags", + obj: execPod(), }, { p: &ExecOptions{}, @@ -95,6 +98,7 @@ func TestPodAndContainer(t *testing.T) { argsLenAtDash: 0, expectError: true, name: "no pod, pod name is behind dash", + obj: execPod(), }, { p: &ExecOptions{}, @@ -102,6 +106,7 @@ func TestPodAndContainer(t *testing.T) { argsLenAtDash: -1, expectError: true, name: "no cmd, w/o flags", + obj: execPod(), }, { p: &ExecOptions{}, @@ -110,6 +115,7 @@ func TestPodAndContainer(t *testing.T) { expectedPod: "foo", expectedArgs: []string{"cmd"}, name: "cmd, w/o flags", + obj: execPod(), }, { p: &ExecOptions{}, @@ -118,6 +124,7 @@ func TestPodAndContainer(t *testing.T) { expectedPod: "foo", expectedArgs: []string{"cmd"}, name: "cmd, cmd is behind dash", + obj: execPod(), }, { p: &ExecOptions{StreamOptions: StreamOptions{ContainerName: "bar"}}, @@ -127,10 +134,12 @@ func TestPodAndContainer(t *testing.T) { expectedContainer: "bar", expectedArgs: []string{"cmd"}, name: "cmd, container in flag", + obj: execPod(), }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { + var err error tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() @@ -142,10 +151,13 @@ func TestPodAndContainer(t *testing.T) { } tf.ClientConfigVal = cmdtesting.DefaultClientConfig() - cmd := &cobra.Command{} + cmd := NewCmdExec(tf, genericclioptions.NewTestIOStreamsDiscard()) options := test.p options.ErrOut = bytes.NewBuffer([]byte{}) - err := options.Complete(tf, cmd, test.args, test.argsLenAtDash) + options.Out = bytes.NewBuffer([]byte{}) + err = options.Complete(tf, cmd, test.args, test.argsLenAtDash) + err = options.Validate() + if test.expectError && err == nil { t.Errorf("%s: unexpected non-error", test.name) } @@ -155,7 +167,9 @@ func TestPodAndContainer(t *testing.T) { if err != nil { return } - if options.PodName != test.expectedPod { + + pod, err := options.ExecutablePodFn(tf, test.obj, defaultPodExecTimeout) + if pod.Name != test.expectedPod { t.Errorf("%s: expected: %s, got: %s", test.name, test.expectedPod, options.PodName) } if options.ContainerName != test.expectedContainer { @@ -171,22 +185,26 @@ func TestPodAndContainer(t *testing.T) { func TestExec(t *testing.T) { version := "v1" tests := []struct { - name, podPath, execPath string - pod *corev1.Pod - execErr bool + name, version, podPath, fetchPodPath, execPath string + pod *corev1.Pod + execErr bool }{ { - name: "pod exec", - podPath: "/api/" + version + "/namespaces/test/pods/foo", - execPath: "/api/" + version + "/namespaces/test/pods/foo/exec", - pod: execPod(), + name: "pod exec", + version: version, + podPath: "/api/" + version + "/namespaces/test/pods/foo", + fetchPodPath: "/namespaces/test/pods/foo", + execPath: "/api/" + version + "/namespaces/test/pods/foo/exec", + pod: execPod(), }, { - name: "pod exec error", - podPath: "/api/" + version + "/namespaces/test/pods/foo", - execPath: "/api/" + version + "/namespaces/test/pods/foo/exec", - pod: execPod(), - execErr: true, + name: "pod exec error", + version: version, + podPath: "/api/" + version + "/namespaces/test/pods/foo", + fetchPodPath: "/namespaces/test/pods/foo", + execPath: "/api/" + version + "/namespaces/test/pods/foo/exec", + pod: execPod(), + execErr: true, }, } for _, test := range tests { @@ -198,19 +216,23 @@ func TestExec(t *testing.T) { ns := scheme.Codecs tf.Client = &fake.RESTClient{ + GroupVersion: schema.GroupVersion{Group: "", Version: "v1"}, NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == test.podPath && m == "GET": body := cmdtesting.ObjBody(codec, test.pod) return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: body}, nil + case p == test.fetchPodPath && m == "GET": + body := cmdtesting.ObjBody(codec, test.pod) + return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: body}, nil default: t.Errorf("%s: unexpected request: %s %#v\n%#v", test.name, req.Method, req.URL, req) return nil, fmt.Errorf("unexpected request") } }), } - tf.ClientConfigVal = cmdtesting.DefaultClientConfig() + tf.ClientConfigVal = &restclient.Config{APIPath: "/api", ContentConfig: restclient.ContentConfig{NegotiatedSerializer: scheme.Codecs, GroupVersion: &schema.GroupVersion{Version: test.version}}} ex := &fakeRemoteExecutor{} if test.execErr { ex.execErr = fmt.Errorf("exec error") @@ -223,8 +245,8 @@ func TestExec(t *testing.T) { }, Executor: ex, } - cmd := &cobra.Command{} - args := []string{"test", "command"} + cmd := NewCmdExec(tf, genericclioptions.NewTestIOStreamsDiscard()) + args := []string{"pod/foo", "command"} if err := params.Complete(tf, cmd, args, -1); err != nil { t.Fatal(err) } diff --git a/test/cmd/exec.sh b/test/cmd/exec.sh new file mode 100755 index 00000000000..db278da0160 --- /dev/null +++ b/test/cmd/exec.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash + +# Copyright 2019 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. + +set -o errexit +set -o nounset +set -o pipefail + +run_kubectl_exec_pod_tests() { + set -o nounset + set -o errexit + + create_and_use_new_namespace + kube::log::status "Testing kubectl exec POD COMMAND" + + ### Test execute non-existing POD + output_message=$(! kubectl exec abc date 2>&1) + # POD abc should error since it doesn't exist + kube::test::if_has_string "${output_message}" 'pods "abc" not found' + + ### Test execute existing POD + # Create test-pod + kubectl create -f hack/testdata/pod.yaml + # Execute existing POD + output_message=$(! kubectl exec test-pod date 2>&1) + # POD test-pod is exists this is shouldn't have output not found + kube::test::if_has_not_string "${output_message}" 'pods "test-pod" not found' + # These must be pass the validate + kube::test::if_has_not_string "${output_message}" 'pod or type/name must be specified' + + # Clean up + kubectl delete pods test-pod + + set +o nounset + set +o errexit +} + +run_kubectl_exec_resource_name_tests() { + set +o nounset + set +o errexit + + create_and_use_new_namespace + kube::log::status "Testing kubectl exec TYPE/NAME COMMAND" + + ### Test execute invalid resource type + output_message=$(! kubectl exec foo/bar date 2>&1) + # resource type foo should error since it's invalid + kube::test::if_has_string "${output_message}" 'error:' + + ### Test execute non-existing resources + output_message=$(! kubectl exec deployments/bar date 2>&1) + # resource type foo should error since it doesn't exist + kube::test::if_has_string "${output_message}" '"bar" not found' + + kubectl create -f hack/testdata/pod.yaml + kubectl create -f hack/testdata/frontend-replicaset.yaml + kubectl create -f hack/testdata/configmap.yaml + + ### Test execute non-implemented resources + output_message=$(! kubectl exec configmap/test-set-env-config date 2>&1) + # resource type configmap should error since configmap not implemented to be attached + kube::test::if_has_string "${output_message}" 'not implemented' + + ### Test execute exists and valid resource type. + # Just check the output, since test-cmd not run kubelet, pod never be assigned. + # and not really can run `kubectl exec` command + + output_message=$(! kubectl exec pods/test-pod date 2>&1) + # POD test-pod is exists this is shouldn't have output not found + kube::test::if_has_not_string "${output_message}" 'not found' + # These must be pass the validate + kube::test::if_has_not_string "${output_message}" 'pod or type/name must be specified' + + output_message=$(! kubectl exec replicaset/frontend date 2>&1) + # Replicaset frontend is valid and exists will select the first pod. + # and Shouldn't have output not found + kube::test::if_has_not_string "${output_message}" 'not found' + # These must be pass the validate + kube::test::if_has_not_string "${output_message}" 'pod or type/name must be specified' + + # Clean up + kubectl delete pods/test-pod + kubectl delete replicaset/frontend + kubectl delete configmap/test-set-env-config + + set +o nounset + set +o errexit +} diff --git a/test/cmd/legacy-script.sh b/test/cmd/legacy-script.sh index 83d8658e4d2..e84142069ab 100755 --- a/test/cmd/legacy-script.sh +++ b/test/cmd/legacy-script.sh @@ -38,6 +38,7 @@ source "${KUBE_ROOT}/test/cmd/create.sh" source "${KUBE_ROOT}/test/cmd/delete.sh" source "${KUBE_ROOT}/test/cmd/diff.sh" source "${KUBE_ROOT}/test/cmd/discovery.sh" +source "${KUBE_ROOT}/test/cmd/exec.sh" source "${KUBE_ROOT}/test/cmd/generic-resources.sh" source "${KUBE_ROOT}/test/cmd/get.sh" source "${KUBE_ROOT}/test/cmd/kubeadm.sh" @@ -506,6 +507,16 @@ runTests() { record_command run_kubectl_old_print_tests fi + ################ + # Kubectl exec # + ################ + + if kube::test::if_supports_resource "${pods}"; then + record_command run_kubectl_exec_pod_tests + if kube::test::if_supports_resource "${replicasets}" && kube::test::if_supports_resource "${configmaps}"; then + record_command run_kubectl_exec_resource_name_tests + fi + fi ###################### # Create # diff --git a/test/e2e/kubectl/kubectl.go b/test/e2e/kubectl/kubectl.go index 149491f3de8..9fba60fec92 100644 --- a/test/e2e/kubectl/kubectl.go +++ b/test/e2e/kubectl/kubectl.go @@ -78,6 +78,7 @@ const ( guestbookResponseTimeout = 3 * time.Minute simplePodSelector = "name=nginx" simplePodName = "nginx" + simplePodResourceName = "pod/nginx" nginxDefaultOutput = "Welcome to nginx!" simplePodPort = 80 pausePodSelector = "name=pause" @@ -408,6 +409,14 @@ var _ = SIGDescribe("Kubectl client", func() { } }) + ginkgo.It("should support exec using resource/name", func() { + ginkgo.By("executing a command in the container") + execOutput := framework.RunKubectlOrDie("exec", fmt.Sprintf("--namespace=%v", ns), simplePodResourceName, "echo", "running", "in", "container") + if e, a := "running in container", strings.TrimSpace(execOutput); e != a { + framework.Failf("Unexpected kubectl exec output. Wanted %q, got %q", e, a) + } + }) + ginkgo.It("should support exec through an HTTP proxy", func() { // Fail if the variable isn't set if framework.TestContext.Host == "" {