...

Source file src/k8s.io/kubectl/pkg/cmd/attach/attach_test.go

Documentation: k8s.io/kubectl/pkg/cmd/attach

     1  /*
     2  Copyright 2014 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package attach
    18  
    19  import (
    20  	"bytes"
    21  	"fmt"
    22  	"io"
    23  	"net/http"
    24  	"net/url"
    25  	"strings"
    26  	"testing"
    27  	"time"
    28  
    29  	corev1 "k8s.io/api/core/v1"
    30  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    31  	"k8s.io/apimachinery/pkg/runtime"
    32  	"k8s.io/apimachinery/pkg/runtime/schema"
    33  	"k8s.io/cli-runtime/pkg/genericclioptions"
    34  	"k8s.io/cli-runtime/pkg/genericiooptions"
    35  	restclient "k8s.io/client-go/rest"
    36  	"k8s.io/client-go/rest/fake"
    37  	"k8s.io/client-go/tools/remotecommand"
    38  	"k8s.io/kubectl/pkg/cmd/exec"
    39  	cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
    40  	cmdutil "k8s.io/kubectl/pkg/cmd/util"
    41  	"k8s.io/kubectl/pkg/cmd/util/podcmd"
    42  	"k8s.io/kubectl/pkg/polymorphichelpers"
    43  	"k8s.io/kubectl/pkg/scheme"
    44  )
    45  
    46  type fakeRemoteAttach struct {
    47  	url *url.URL
    48  	err error
    49  }
    50  
    51  func (f *fakeRemoteAttach) Attach(url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error {
    52  	f.url = url
    53  	return f.err
    54  }
    55  
    56  func fakeAttachablePodFn(pod *corev1.Pod) polymorphichelpers.AttachablePodForObjectFunc {
    57  	return func(getter genericclioptions.RESTClientGetter, obj runtime.Object, timeout time.Duration) (*corev1.Pod, error) {
    58  		return pod, nil
    59  	}
    60  }
    61  
    62  func TestPodAndContainerAttach(t *testing.T) {
    63  	tests := []struct {
    64  		name                  string
    65  		args                  []string
    66  		options               *AttachOptions
    67  		expectError           string
    68  		expectedPodName       string
    69  		expectedContainerName string
    70  		expectOut             string
    71  		obj                   *corev1.Pod
    72  	}{
    73  		{
    74  			name:        "empty",
    75  			options:     &AttachOptions{GetPodTimeout: 1},
    76  			expectError: "at least 1 argument is required",
    77  		},
    78  		{
    79  			name:        "too many args",
    80  			options:     &AttachOptions{GetPodTimeout: 2},
    81  			args:        []string{"one", "two", "three"},
    82  			expectError: "at most 2 arguments",
    83  		},
    84  		{
    85  			name:                  "no container, no flags",
    86  			options:               &AttachOptions{GetPodTimeout: defaultPodLogsTimeout},
    87  			args:                  []string{"foo"},
    88  			expectedPodName:       "foo",
    89  			expectedContainerName: "bar",
    90  			obj:                   attachPod(),
    91  			expectOut:             `Defaulted container "bar" out of: bar, debugger (ephem), initfoo (init)`,
    92  		},
    93  		{
    94  			name:                  "no container, no flags, sets default expected container as annotation",
    95  			options:               &AttachOptions{GetPodTimeout: defaultPodLogsTimeout},
    96  			args:                  []string{"foo"},
    97  			expectedPodName:       "foo",
    98  			expectedContainerName: "bar",
    99  			obj:                   setDefaultContainer(attachPod(), "initfoo"),
   100  			expectOut:             ``,
   101  		},
   102  		{
   103  			name:                  "no container, no flags, sets default missing container as annotation",
   104  			options:               &AttachOptions{GetPodTimeout: defaultPodLogsTimeout},
   105  			args:                  []string{"foo"},
   106  			expectedPodName:       "foo",
   107  			expectedContainerName: "bar",
   108  			obj:                   setDefaultContainer(attachPod(), "does-not-exist"),
   109  			expectOut:             `Defaulted container "bar" out of: bar, debugger (ephem), initfoo (init)`,
   110  		},
   111  		{
   112  			name:                  "container in flag",
   113  			options:               &AttachOptions{StreamOptions: exec.StreamOptions{ContainerName: "bar"}, GetPodTimeout: 10000000},
   114  			args:                  []string{"foo"},
   115  			expectedPodName:       "foo",
   116  			expectedContainerName: "bar",
   117  			obj:                   attachPod(),
   118  		},
   119  		{
   120  			name:                  "init container in flag",
   121  			options:               &AttachOptions{StreamOptions: exec.StreamOptions{ContainerName: "initfoo"}, GetPodTimeout: 30},
   122  			args:                  []string{"foo"},
   123  			expectedPodName:       "foo",
   124  			expectedContainerName: "initfoo",
   125  			obj:                   attachPod(),
   126  		},
   127  		{
   128  			name:                  "ephemeral container in flag",
   129  			options:               &AttachOptions{StreamOptions: exec.StreamOptions{ContainerName: "debugger"}, GetPodTimeout: 30},
   130  			args:                  []string{"foo"},
   131  			expectedPodName:       "foo",
   132  			expectedContainerName: "debugger",
   133  			obj:                   attachPod(),
   134  		},
   135  		{
   136  			name:            "non-existing container",
   137  			options:         &AttachOptions{StreamOptions: exec.StreamOptions{ContainerName: "wrong"}, GetPodTimeout: 10},
   138  			args:            []string{"foo"},
   139  			expectedPodName: "foo",
   140  			expectError:     "container wrong not found in pod foo",
   141  			obj:             attachPod(),
   142  		},
   143  		{
   144  			name:                  "no container, no flags, pods and name",
   145  			options:               &AttachOptions{GetPodTimeout: 10000},
   146  			args:                  []string{"pods", "foo"},
   147  			expectedPodName:       "foo",
   148  			expectedContainerName: "bar",
   149  			obj:                   attachPod(),
   150  		},
   151  		{
   152  			name:                  "invalid get pod timeout value",
   153  			options:               &AttachOptions{GetPodTimeout: 0},
   154  			args:                  []string{"pod/foo"},
   155  			expectedPodName:       "foo",
   156  			expectedContainerName: "bar",
   157  			obj:                   attachPod(),
   158  			expectError:           "must be higher than zero",
   159  		},
   160  	}
   161  
   162  	for _, test := range tests {
   163  		t.Run(test.name, func(t *testing.T) {
   164  			// setup opts to fetch our test pod
   165  			test.options.AttachablePodFn = fakeAttachablePodFn(test.obj)
   166  			test.options.Resources = test.args
   167  
   168  			if err := test.options.Validate(); err != nil {
   169  				if test.expectError == "" || !strings.Contains(err.Error(), test.expectError) {
   170  					t.Errorf("unexpected error: expected %q, got %q", test.expectError, err)
   171  				}
   172  				return
   173  			}
   174  
   175  			pod, err := test.options.findAttachablePod(&corev1.Pod{
   176  				ObjectMeta: metav1.ObjectMeta{Name: "test-pod", Namespace: "test"},
   177  				Spec: corev1.PodSpec{
   178  					InitContainers: []corev1.Container{
   179  						{
   180  							Name: "initfoo",
   181  						},
   182  					},
   183  					Containers: []corev1.Container{
   184  						{
   185  							Name: "foobar",
   186  						},
   187  					},
   188  					EphemeralContainers: []corev1.EphemeralContainer{
   189  						{
   190  							EphemeralContainerCommon: corev1.EphemeralContainerCommon{
   191  								Name: "ephemfoo",
   192  							},
   193  						},
   194  					},
   195  				},
   196  			})
   197  			if err != nil {
   198  				if test.expectError == "" || !strings.Contains(err.Error(), test.expectError) {
   199  					t.Errorf("unexpected error: expected %q, got %q", err, test.expectError)
   200  				}
   201  				return
   202  			}
   203  
   204  			if pod.Name != test.expectedPodName {
   205  				t.Errorf("unexpected pod name: expected %q, got %q", test.expectedContainerName, pod.Name)
   206  			}
   207  
   208  			var buf bytes.Buffer
   209  			test.options.ErrOut = &buf
   210  			container, err := test.options.containerToAttachTo(attachPod())
   211  
   212  			if len(test.expectOut) > 0 && !strings.Contains(buf.String(), test.expectOut) {
   213  				t.Errorf("unexpected output: output did not contain %q\n---\n%s", test.expectOut, buf.String())
   214  			}
   215  
   216  			if err != nil {
   217  				if test.expectError == "" || !strings.Contains(err.Error(), test.expectError) {
   218  					t.Errorf("unexpected error: expected %q, got %q", test.expectError, err)
   219  				}
   220  				return
   221  			}
   222  
   223  			if container.Name != test.expectedContainerName {
   224  				t.Errorf("unexpected container name: expected %q, got %q", test.expectedContainerName, container.Name)
   225  			}
   226  
   227  			if test.options.PodName != test.expectedPodName {
   228  				t.Errorf("%s: expected: %s, got: %s", test.name, test.expectedPodName, test.options.PodName)
   229  			}
   230  
   231  			if len(test.expectError) > 0 {
   232  				t.Fatalf("expected error %q, but saw none", test.expectError)
   233  			}
   234  		})
   235  	}
   236  }
   237  
   238  func TestAttach(t *testing.T) {
   239  	version := "v1"
   240  	tests := []struct {
   241  		name, version, podPath, fetchPodPath, attachPath, container string
   242  		pod                                                         *corev1.Pod
   243  		remoteAttachErr                                             bool
   244  		expectedErr                                                 string
   245  	}{
   246  		{
   247  			name:         "pod attach",
   248  			version:      version,
   249  			podPath:      "/api/" + version + "/namespaces/test/pods/foo",
   250  			fetchPodPath: "/namespaces/test/pods/foo",
   251  			attachPath:   "/api/" + version + "/namespaces/test/pods/foo/attach",
   252  			pod:          attachPod(),
   253  			container:    "bar",
   254  		},
   255  		{
   256  			name:            "pod attach error",
   257  			version:         version,
   258  			podPath:         "/api/" + version + "/namespaces/test/pods/foo",
   259  			fetchPodPath:    "/namespaces/test/pods/foo",
   260  			attachPath:      "/api/" + version + "/namespaces/test/pods/foo/attach",
   261  			pod:             attachPod(),
   262  			remoteAttachErr: true,
   263  			container:       "bar",
   264  			expectedErr:     "attach error",
   265  		},
   266  		{
   267  			name:         "container not found error",
   268  			version:      version,
   269  			podPath:      "/api/" + version + "/namespaces/test/pods/foo",
   270  			fetchPodPath: "/namespaces/test/pods/foo",
   271  			attachPath:   "/api/" + version + "/namespaces/test/pods/foo/attach",
   272  			pod:          attachPod(),
   273  			container:    "foo",
   274  			expectedErr:  "cannot attach to the container: container foo not found in pod foo",
   275  		},
   276  	}
   277  	for _, test := range tests {
   278  		t.Run(test.name, func(t *testing.T) {
   279  			tf := cmdtesting.NewTestFactory().WithNamespace("test")
   280  			defer tf.Cleanup()
   281  
   282  			codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
   283  			ns := scheme.Codecs.WithoutConversion()
   284  
   285  			tf.Client = &fake.RESTClient{
   286  				GroupVersion:         schema.GroupVersion{Group: "", Version: "v1"},
   287  				NegotiatedSerializer: ns,
   288  				Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
   289  					switch p, m := req.URL.Path, req.Method; {
   290  					case p == test.podPath && m == "GET":
   291  						body := cmdtesting.ObjBody(codec, test.pod)
   292  						return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil
   293  					case p == test.fetchPodPath && m == "GET":
   294  						body := cmdtesting.ObjBody(codec, test.pod)
   295  						return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil
   296  					default:
   297  						t.Errorf("%s: unexpected request: %s %#v\n%#v", p, req.Method, req.URL, req)
   298  						return nil, fmt.Errorf("unexpected request")
   299  					}
   300  				}),
   301  			}
   302  			tf.ClientConfigVal = &restclient.Config{APIPath: "/api", ContentConfig: restclient.ContentConfig{NegotiatedSerializer: scheme.Codecs, GroupVersion: &schema.GroupVersion{Version: test.version}}}
   303  
   304  			remoteAttach := &fakeRemoteAttach{}
   305  			if test.remoteAttachErr {
   306  				remoteAttach.err = fmt.Errorf("attach error")
   307  			}
   308  			options := &AttachOptions{
   309  				StreamOptions: exec.StreamOptions{
   310  					ContainerName: test.container,
   311  					IOStreams:     genericiooptions.NewTestIOStreamsDiscard(),
   312  				},
   313  				Attach:        remoteAttach,
   314  				GetPodTimeout: 1000,
   315  			}
   316  
   317  			options.restClientGetter = tf
   318  			options.Namespace = "test"
   319  			options.Resources = []string{"foo"}
   320  			options.Builder = tf.NewBuilder
   321  			options.AttachablePodFn = fakeAttachablePodFn(test.pod)
   322  			options.AttachFunc = func(opts *AttachOptions, containerToAttach *corev1.Container, raw bool, sizeQueue remotecommand.TerminalSizeQueue) func() error {
   323  				return func() error {
   324  					u, err := url.Parse(fmt.Sprintf("%s?container=%s", test.attachPath, containerToAttach.Name))
   325  					if err != nil {
   326  						return err
   327  					}
   328  
   329  					return options.Attach.Attach(u, nil, nil, nil, nil, raw, sizeQueue)
   330  				}
   331  			}
   332  
   333  			err := options.Run()
   334  			if test.expectedErr != "" && err.Error() != test.expectedErr {
   335  				t.Errorf("%s: Unexpected exec error: %v", test.name, err)
   336  				return
   337  			}
   338  			if test.expectedErr == "" && err != nil {
   339  				t.Errorf("%s: Unexpected error: %v", test.name, err)
   340  				return
   341  			}
   342  			if test.expectedErr != "" {
   343  				return
   344  			}
   345  			if remoteAttach.url.Path != test.attachPath {
   346  				t.Errorf("%s: Did not get expected path for exec request: %q %q", test.name, test.attachPath, remoteAttach.url.Path)
   347  				return
   348  			}
   349  			if remoteAttach.url.Query().Get("container") != "bar" {
   350  				t.Errorf("%s: Did not have query parameters: %s", test.name, remoteAttach.url.Query())
   351  			}
   352  		})
   353  	}
   354  }
   355  
   356  func TestAttachWarnings(t *testing.T) {
   357  	version := "v1"
   358  	tests := []struct {
   359  		name, container, version, podPath, fetchPodPath, expectedErr string
   360  		pod                                                          *corev1.Pod
   361  		stdin, tty                                                   bool
   362  	}{
   363  		{
   364  			name:         "fallback tty if not supported",
   365  			version:      version,
   366  			podPath:      "/api/" + version + "/namespaces/test/pods/foo",
   367  			fetchPodPath: "/namespaces/test/pods/foo",
   368  			pod:          attachPod(),
   369  			stdin:        true,
   370  			tty:          true,
   371  			expectedErr:  "Unable to use a TTY - container bar did not allocate one",
   372  		},
   373  	}
   374  	for _, test := range tests {
   375  		t.Run(test.name, func(t *testing.T) {
   376  			tf := cmdtesting.NewTestFactory().WithNamespace("test")
   377  			defer tf.Cleanup()
   378  
   379  			streams, _, _, bufErr := genericiooptions.NewTestIOStreams()
   380  
   381  			codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
   382  			ns := scheme.Codecs.WithoutConversion()
   383  
   384  			tf.Client = &fake.RESTClient{
   385  				GroupVersion:         schema.GroupVersion{Group: "", Version: "v1"},
   386  				NegotiatedSerializer: ns,
   387  				Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
   388  					switch p, m := req.URL.Path, req.Method; {
   389  					case p == test.podPath && m == "GET":
   390  						body := cmdtesting.ObjBody(codec, test.pod)
   391  						return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil
   392  					case p == test.fetchPodPath && m == "GET":
   393  						body := cmdtesting.ObjBody(codec, test.pod)
   394  						return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil
   395  					default:
   396  						t.Errorf("%s: unexpected request: %s %#v\n%#v", p, req.Method, req.URL, req)
   397  						return nil, fmt.Errorf("unexpected request")
   398  					}
   399  				}),
   400  			}
   401  			tf.ClientConfigVal = &restclient.Config{APIPath: "/api", ContentConfig: restclient.ContentConfig{NegotiatedSerializer: scheme.Codecs, GroupVersion: &schema.GroupVersion{Version: test.version}}}
   402  
   403  			options := &AttachOptions{
   404  				StreamOptions: exec.StreamOptions{
   405  					Stdin:         test.stdin,
   406  					TTY:           test.tty,
   407  					ContainerName: test.container,
   408  					IOStreams:     streams,
   409  				},
   410  
   411  				Attach:        &fakeRemoteAttach{},
   412  				GetPodTimeout: 1000,
   413  			}
   414  
   415  			options.restClientGetter = tf
   416  			options.Namespace = "test"
   417  			options.Resources = []string{"foo"}
   418  			options.Builder = tf.NewBuilder
   419  			options.AttachablePodFn = fakeAttachablePodFn(test.pod)
   420  			options.AttachFunc = func(opts *AttachOptions, containerToAttach *corev1.Container, raw bool, sizeQueue remotecommand.TerminalSizeQueue) func() error {
   421  				return func() error {
   422  					u, err := url.Parse("http://foo.bar")
   423  					if err != nil {
   424  						return err
   425  					}
   426  
   427  					return options.Attach.Attach(u, nil, nil, nil, nil, raw, sizeQueue)
   428  				}
   429  			}
   430  
   431  			if err := options.Run(); err != nil {
   432  				t.Fatal(err)
   433  			}
   434  
   435  			if test.stdin && test.tty {
   436  				if !test.pod.Spec.Containers[0].TTY {
   437  					if !strings.Contains(bufErr.String(), test.expectedErr) {
   438  						t.Errorf("%s: Expected TTY fallback warning for attach request: %s", test.name, bufErr.String())
   439  						return
   440  					}
   441  				}
   442  			}
   443  		})
   444  	}
   445  }
   446  
   447  func attachPod() *corev1.Pod {
   448  	return &corev1.Pod{
   449  		ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "test", ResourceVersion: "10"},
   450  		Spec: corev1.PodSpec{
   451  			RestartPolicy: corev1.RestartPolicyAlways,
   452  			DNSPolicy:     corev1.DNSClusterFirst,
   453  			Containers: []corev1.Container{
   454  				{
   455  					Name: "bar",
   456  				},
   457  			},
   458  			InitContainers: []corev1.Container{
   459  				{
   460  					Name: "initfoo",
   461  				},
   462  			},
   463  			EphemeralContainers: []corev1.EphemeralContainer{
   464  				{
   465  					EphemeralContainerCommon: corev1.EphemeralContainerCommon{
   466  						Name: "debugger",
   467  					},
   468  				},
   469  			},
   470  		},
   471  		Status: corev1.PodStatus{
   472  			Phase: corev1.PodRunning,
   473  		},
   474  	}
   475  }
   476  
   477  func setDefaultContainer(pod *corev1.Pod, name string) *corev1.Pod {
   478  	if pod.Annotations == nil {
   479  		pod.Annotations = make(map[string]string)
   480  	}
   481  	pod.Annotations[podcmd.DefaultContainerAnnotationName] = name
   482  	return pod
   483  }
   484  
   485  func TestReattachMessage(t *testing.T) {
   486  	tests := []struct {
   487  		name          string
   488  		pod           *corev1.Pod
   489  		rawTTY, stdin bool
   490  		container     string
   491  		expected      string
   492  	}{
   493  		{
   494  			name:      "normal interactive session",
   495  			pod:       attachPod(),
   496  			container: "bar",
   497  			rawTTY:    true,
   498  			stdin:     true,
   499  			expected:  "Session ended, resume using",
   500  		},
   501  		{
   502  			name:      "no stdin",
   503  			pod:       attachPod(),
   504  			container: "bar",
   505  			rawTTY:    true,
   506  			stdin:     false,
   507  			expected:  "",
   508  		},
   509  		{
   510  			name:      "not connected to a real TTY",
   511  			pod:       attachPod(),
   512  			container: "bar",
   513  			rawTTY:    false,
   514  			stdin:     true,
   515  			expected:  "",
   516  		},
   517  		{
   518  			name: "no restarts",
   519  			pod: &corev1.Pod{
   520  				ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "test"},
   521  				Spec: corev1.PodSpec{
   522  					RestartPolicy: corev1.RestartPolicyNever,
   523  					Containers:    []corev1.Container{{Name: "bar"}},
   524  				},
   525  				Status: corev1.PodStatus{Phase: corev1.PodRunning},
   526  			},
   527  			container: "bar",
   528  			rawTTY:    true,
   529  			stdin:     true,
   530  			expected:  "",
   531  		},
   532  		{
   533  			name:      "ephemeral container",
   534  			pod:       attachPod(),
   535  			container: "debugger",
   536  			rawTTY:    true,
   537  			stdin:     true,
   538  			expected:  "Session ended, the ephemeral container will not be restarted",
   539  		},
   540  	}
   541  	for _, test := range tests {
   542  		t.Run(test.name, func(t *testing.T) {
   543  			options := &AttachOptions{
   544  				StreamOptions: exec.StreamOptions{
   545  					Stdin: test.stdin,
   546  				},
   547  				Pod: test.pod,
   548  			}
   549  			if msg := options.reattachMessage(test.container, test.rawTTY); test.expected == "" && msg != "" {
   550  				t.Errorf("reattachMessage(%v, %v) = %q, want empty string", test.container, test.rawTTY, msg)
   551  			} else if !strings.Contains(msg, test.expected) {
   552  				t.Errorf("reattachMessage(%v, %v) = %q, want string containing %q", test.container, test.rawTTY, msg, test.expected)
   553  			}
   554  		})
   555  	}
   556  }
   557  
   558  func TestCreateExecutor(t *testing.T) {
   559  	url, err := url.Parse("http://localhost:8080/index.html")
   560  	if err != nil {
   561  		t.Fatalf("unable to parse test url: %v", err)
   562  	}
   563  	config := cmdtesting.DefaultClientConfig()
   564  	// First, ensure that no environment variable creates the fallback executor.
   565  	executor, err := createExecutor(url, config)
   566  	if err != nil {
   567  		t.Fatalf("unable to create executor: %v", err)
   568  	}
   569  	if _, isFallback := executor.(*remotecommand.FallbackExecutor); !isFallback {
   570  		t.Errorf("expected fallback executor, got %#v", executor)
   571  	}
   572  	// Next, check turning on feature flag explicitly also creates fallback executor.
   573  	t.Setenv(string(cmdutil.RemoteCommandWebsockets), "true")
   574  	executor, err = createExecutor(url, config)
   575  	if err != nil {
   576  		t.Fatalf("unable to create executor: %v", err)
   577  	}
   578  	if _, isFallback := executor.(*remotecommand.FallbackExecutor); !isFallback {
   579  		t.Errorf("expected fallback executor, got %#v", executor)
   580  	}
   581  	// Finally, check explicit disabling does NOT create the fallback executor.
   582  	t.Setenv(string(cmdutil.RemoteCommandWebsockets), "false")
   583  	executor, err = createExecutor(url, config)
   584  	if err != nil {
   585  		t.Fatalf("unable to create executor: %v", err)
   586  	}
   587  	if _, isFallback := executor.(*remotecommand.FallbackExecutor); isFallback {
   588  		t.Errorf("expected fallback executor, got %#v", executor)
   589  	}
   590  }
   591  

View as plain text