exec_test.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. /*
  2. Copyright 2014 The Kubernetes Authors.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package exec
  14. import (
  15. "bytes"
  16. "fmt"
  17. "io"
  18. "io/ioutil"
  19. "net/http"
  20. "net/url"
  21. "reflect"
  22. "strings"
  23. "testing"
  24. corev1 "k8s.io/api/core/v1"
  25. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  26. "k8s.io/apimachinery/pkg/runtime/schema"
  27. "k8s.io/cli-runtime/pkg/genericclioptions"
  28. restclient "k8s.io/client-go/rest"
  29. "k8s.io/client-go/rest/fake"
  30. "k8s.io/client-go/tools/remotecommand"
  31. cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing"
  32. "k8s.io/kubernetes/pkg/kubectl/scheme"
  33. "k8s.io/kubernetes/pkg/kubectl/util/term"
  34. )
  35. type fakeRemoteExecutor struct {
  36. method string
  37. url *url.URL
  38. execErr error
  39. }
  40. func (f *fakeRemoteExecutor) Execute(method string, url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error {
  41. f.method = method
  42. f.url = url
  43. return f.execErr
  44. }
  45. func TestPodAndContainer(t *testing.T) {
  46. tests := []struct {
  47. args []string
  48. argsLenAtDash int
  49. p *ExecOptions
  50. name string
  51. expectError bool
  52. expectedPod string
  53. expectedContainer string
  54. expectedArgs []string
  55. obj *corev1.Pod
  56. }{
  57. {
  58. p: &ExecOptions{},
  59. argsLenAtDash: -1,
  60. expectError: true,
  61. name: "empty",
  62. },
  63. {
  64. p: &ExecOptions{},
  65. argsLenAtDash: -1,
  66. expectError: true,
  67. name: "no cmd",
  68. obj: execPod(),
  69. },
  70. {
  71. p: &ExecOptions{StreamOptions: StreamOptions{ContainerName: "bar"}},
  72. argsLenAtDash: -1,
  73. expectError: true,
  74. name: "no cmd, w/ container",
  75. obj: execPod(),
  76. },
  77. {
  78. p: &ExecOptions{},
  79. args: []string{"foo", "cmd"},
  80. argsLenAtDash: 0,
  81. expectError: true,
  82. name: "no pod, pod name is behind dash",
  83. obj: execPod(),
  84. },
  85. {
  86. p: &ExecOptions{},
  87. args: []string{"foo"},
  88. argsLenAtDash: -1,
  89. expectError: true,
  90. name: "no cmd, w/o flags",
  91. obj: execPod(),
  92. },
  93. {
  94. p: &ExecOptions{},
  95. args: []string{"foo", "cmd"},
  96. argsLenAtDash: -1,
  97. expectedPod: "foo",
  98. expectedArgs: []string{"cmd"},
  99. name: "cmd, w/o flags",
  100. obj: execPod(),
  101. },
  102. {
  103. p: &ExecOptions{},
  104. args: []string{"foo", "cmd"},
  105. argsLenAtDash: 1,
  106. expectedPod: "foo",
  107. expectedArgs: []string{"cmd"},
  108. name: "cmd, cmd is behind dash",
  109. obj: execPod(),
  110. },
  111. {
  112. p: &ExecOptions{StreamOptions: StreamOptions{ContainerName: "bar"}},
  113. args: []string{"foo", "cmd"},
  114. argsLenAtDash: -1,
  115. expectedPod: "foo",
  116. expectedContainer: "bar",
  117. expectedArgs: []string{"cmd"},
  118. name: "cmd, container in flag",
  119. obj: execPod(),
  120. },
  121. }
  122. for _, test := range tests {
  123. t.Run(test.name, func(t *testing.T) {
  124. var err error
  125. tf := cmdtesting.NewTestFactory().WithNamespace("test")
  126. defer tf.Cleanup()
  127. ns := scheme.Codecs
  128. tf.Client = &fake.RESTClient{
  129. NegotiatedSerializer: ns,
  130. Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { return nil, nil }),
  131. }
  132. tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
  133. cmd := NewCmdExec(tf, genericclioptions.NewTestIOStreamsDiscard())
  134. options := test.p
  135. options.ErrOut = bytes.NewBuffer([]byte{})
  136. options.Out = bytes.NewBuffer([]byte{})
  137. err = options.Complete(tf, cmd, test.args, test.argsLenAtDash)
  138. err = options.Validate()
  139. if test.expectError && err == nil {
  140. t.Errorf("%s: unexpected non-error", test.name)
  141. }
  142. if !test.expectError && err != nil {
  143. t.Errorf("%s: unexpected error: %v", test.name, err)
  144. }
  145. if err != nil {
  146. return
  147. }
  148. pod, err := options.ExecutablePodFn(tf, test.obj, defaultPodExecTimeout)
  149. if pod.Name != test.expectedPod {
  150. t.Errorf("%s: expected: %s, got: %s", test.name, test.expectedPod, options.PodName)
  151. }
  152. if options.ContainerName != test.expectedContainer {
  153. t.Errorf("%s: expected: %s, got: %s", test.name, test.expectedContainer, options.ContainerName)
  154. }
  155. if !reflect.DeepEqual(test.expectedArgs, options.Command) {
  156. t.Errorf("%s: expected: %v, got %v", test.name, test.expectedArgs, options.Command)
  157. }
  158. })
  159. }
  160. }
  161. func TestExec(t *testing.T) {
  162. version := "v1"
  163. tests := []struct {
  164. name, version, podPath, fetchPodPath, execPath string
  165. pod *corev1.Pod
  166. execErr bool
  167. }{
  168. {
  169. name: "pod exec",
  170. version: version,
  171. podPath: "/api/" + version + "/namespaces/test/pods/foo",
  172. fetchPodPath: "/namespaces/test/pods/foo",
  173. execPath: "/api/" + version + "/namespaces/test/pods/foo/exec",
  174. pod: execPod(),
  175. },
  176. {
  177. name: "pod exec error",
  178. version: version,
  179. podPath: "/api/" + version + "/namespaces/test/pods/foo",
  180. fetchPodPath: "/namespaces/test/pods/foo",
  181. execPath: "/api/" + version + "/namespaces/test/pods/foo/exec",
  182. pod: execPod(),
  183. execErr: true,
  184. },
  185. }
  186. for _, test := range tests {
  187. t.Run(test.name, func(t *testing.T) {
  188. tf := cmdtesting.NewTestFactory().WithNamespace("test")
  189. defer tf.Cleanup()
  190. codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
  191. ns := scheme.Codecs
  192. tf.Client = &fake.RESTClient{
  193. GroupVersion: schema.GroupVersion{Group: "", Version: "v1"},
  194. NegotiatedSerializer: ns,
  195. Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
  196. switch p, m := req.URL.Path, req.Method; {
  197. case p == test.podPath && m == "GET":
  198. body := cmdtesting.ObjBody(codec, test.pod)
  199. return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: body}, nil
  200. case p == test.fetchPodPath && m == "GET":
  201. body := cmdtesting.ObjBody(codec, test.pod)
  202. return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: body}, nil
  203. default:
  204. t.Errorf("%s: unexpected request: %s %#v\n%#v", test.name, req.Method, req.URL, req)
  205. return nil, fmt.Errorf("unexpected request")
  206. }
  207. }),
  208. }
  209. tf.ClientConfigVal = &restclient.Config{APIPath: "/api", ContentConfig: restclient.ContentConfig{NegotiatedSerializer: scheme.Codecs, GroupVersion: &schema.GroupVersion{Version: test.version}}}
  210. ex := &fakeRemoteExecutor{}
  211. if test.execErr {
  212. ex.execErr = fmt.Errorf("exec error")
  213. }
  214. params := &ExecOptions{
  215. StreamOptions: StreamOptions{
  216. PodName: "foo",
  217. ContainerName: "bar",
  218. IOStreams: genericclioptions.NewTestIOStreamsDiscard(),
  219. },
  220. Executor: ex,
  221. }
  222. cmd := NewCmdExec(tf, genericclioptions.NewTestIOStreamsDiscard())
  223. args := []string{"pod/foo", "command"}
  224. if err := params.Complete(tf, cmd, args, -1); err != nil {
  225. t.Fatal(err)
  226. }
  227. err := params.Run()
  228. if test.execErr && err != ex.execErr {
  229. t.Errorf("%s: Unexpected exec error: %v", test.name, err)
  230. return
  231. }
  232. if !test.execErr && err != nil {
  233. t.Errorf("%s: Unexpected error: %v", test.name, err)
  234. return
  235. }
  236. if test.execErr {
  237. return
  238. }
  239. if ex.url.Path != test.execPath {
  240. t.Errorf("%s: Did not get expected path for exec request", test.name)
  241. return
  242. }
  243. if strings.Count(ex.url.RawQuery, "container=bar") != 1 {
  244. t.Errorf("%s: Did not get expected container query param for exec request", test.name)
  245. return
  246. }
  247. if ex.method != "POST" {
  248. t.Errorf("%s: Did not get method for exec request: %s", test.name, ex.method)
  249. }
  250. })
  251. }
  252. }
  253. func execPod() *corev1.Pod {
  254. return &corev1.Pod{
  255. ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "test", ResourceVersion: "10"},
  256. Spec: corev1.PodSpec{
  257. RestartPolicy: corev1.RestartPolicyAlways,
  258. DNSPolicy: corev1.DNSClusterFirst,
  259. Containers: []corev1.Container{
  260. {
  261. Name: "bar",
  262. },
  263. },
  264. },
  265. Status: corev1.PodStatus{
  266. Phase: corev1.PodRunning,
  267. },
  268. }
  269. }
  270. func TestSetupTTY(t *testing.T) {
  271. streams, _, _, stderr := genericclioptions.NewTestIOStreams()
  272. // test 1 - don't attach stdin
  273. o := &StreamOptions{
  274. // InterruptParent: ,
  275. Stdin: false,
  276. IOStreams: streams,
  277. TTY: true,
  278. }
  279. tty := o.SetupTTY()
  280. if o.In != nil {
  281. t.Errorf("don't attach stdin: o.In should be nil")
  282. }
  283. if tty.In != nil {
  284. t.Errorf("don't attach stdin: tty.In should be nil")
  285. }
  286. if o.TTY {
  287. t.Errorf("don't attach stdin: o.TTY should be false")
  288. }
  289. if tty.Raw {
  290. t.Errorf("don't attach stdin: tty.Raw should be false")
  291. }
  292. if len(stderr.String()) > 0 {
  293. t.Errorf("don't attach stdin: stderr wasn't empty: %s", stderr.String())
  294. }
  295. // tests from here on attach stdin
  296. // test 2 - don't request a TTY
  297. o.Stdin = true
  298. o.In = &bytes.Buffer{}
  299. o.TTY = false
  300. tty = o.SetupTTY()
  301. if o.In == nil {
  302. t.Errorf("attach stdin, no TTY: o.In should not be nil")
  303. }
  304. if tty.In != o.In {
  305. t.Errorf("attach stdin, no TTY: tty.In should equal o.In")
  306. }
  307. if o.TTY {
  308. t.Errorf("attach stdin, no TTY: o.TTY should be false")
  309. }
  310. if tty.Raw {
  311. t.Errorf("attach stdin, no TTY: tty.Raw should be false")
  312. }
  313. if len(stderr.String()) > 0 {
  314. t.Errorf("attach stdin, no TTY: stderr wasn't empty: %s", stderr.String())
  315. }
  316. // test 3 - request a TTY, but stdin is not a terminal
  317. o.Stdin = true
  318. o.In = &bytes.Buffer{}
  319. o.ErrOut = stderr
  320. o.TTY = true
  321. tty = o.SetupTTY()
  322. if o.In == nil {
  323. t.Errorf("attach stdin, TTY, not a terminal: o.In should not be nil")
  324. }
  325. if tty.In != o.In {
  326. t.Errorf("attach stdin, TTY, not a terminal: tty.In should equal o.In")
  327. }
  328. if o.TTY {
  329. t.Errorf("attach stdin, TTY, not a terminal: o.TTY should be false")
  330. }
  331. if tty.Raw {
  332. t.Errorf("attach stdin, TTY, not a terminal: tty.Raw should be false")
  333. }
  334. if !strings.Contains(stderr.String(), "input is not a terminal") {
  335. t.Errorf("attach stdin, TTY, not a terminal: expected 'input is not a terminal' to stderr")
  336. }
  337. // test 4 - request a TTY, stdin is a terminal
  338. o.Stdin = true
  339. o.In = &bytes.Buffer{}
  340. stderr.Reset()
  341. o.TTY = true
  342. overrideStdin := ioutil.NopCloser(&bytes.Buffer{})
  343. overrideStdout := &bytes.Buffer{}
  344. overrideStderr := &bytes.Buffer{}
  345. o.overrideStreams = func() (io.ReadCloser, io.Writer, io.Writer) {
  346. return overrideStdin, overrideStdout, overrideStderr
  347. }
  348. o.isTerminalIn = func(tty term.TTY) bool {
  349. return true
  350. }
  351. tty = o.SetupTTY()
  352. if o.In != overrideStdin {
  353. t.Errorf("attach stdin, TTY, is a terminal: o.In should equal overrideStdin")
  354. }
  355. if tty.In != o.In {
  356. t.Errorf("attach stdin, TTY, is a terminal: tty.In should equal o.In")
  357. }
  358. if !o.TTY {
  359. t.Errorf("attach stdin, TTY, is a terminal: o.TTY should be true")
  360. }
  361. if !tty.Raw {
  362. t.Errorf("attach stdin, TTY, is a terminal: tty.Raw should be true")
  363. }
  364. if len(stderr.String()) > 0 {
  365. t.Errorf("attach stdin, TTY, is a terminal: stderr wasn't empty: %s", stderr.String())
  366. }
  367. if o.Out != overrideStdout {
  368. t.Errorf("attach stdin, TTY, is a terminal: o.Out should equal overrideStdout")
  369. }
  370. if tty.Out != o.Out {
  371. t.Errorf("attach stdin, TTY, is a terminal: tty.Out should equal o.Out")
  372. }
  373. }