attach_test.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  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 attach
  14. import (
  15. "fmt"
  16. "io"
  17. "net/http"
  18. "net/url"
  19. "strings"
  20. "testing"
  21. "time"
  22. corev1 "k8s.io/api/core/v1"
  23. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  24. "k8s.io/apimachinery/pkg/runtime"
  25. "k8s.io/apimachinery/pkg/runtime/schema"
  26. "k8s.io/cli-runtime/pkg/genericclioptions"
  27. restclient "k8s.io/client-go/rest"
  28. "k8s.io/client-go/rest/fake"
  29. "k8s.io/client-go/tools/remotecommand"
  30. "k8s.io/kubernetes/pkg/kubectl/cmd/exec"
  31. cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing"
  32. "k8s.io/kubernetes/pkg/kubectl/polymorphichelpers"
  33. "k8s.io/kubernetes/pkg/kubectl/scheme"
  34. )
  35. type fakeRemoteAttach struct {
  36. method string
  37. url *url.URL
  38. err error
  39. }
  40. func (f *fakeRemoteAttach) Attach(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.err
  44. }
  45. func fakeAttachablePodFn(pod *corev1.Pod) polymorphichelpers.AttachablePodForObjectFunc {
  46. return func(getter genericclioptions.RESTClientGetter, obj runtime.Object, timeout time.Duration) (*corev1.Pod, error) {
  47. return pod, nil
  48. }
  49. }
  50. func TestPodAndContainerAttach(t *testing.T) {
  51. tests := []struct {
  52. name string
  53. args []string
  54. options *AttachOptions
  55. expectError string
  56. expectedPodName string
  57. expectedContainerName string
  58. obj *corev1.Pod
  59. }{
  60. {
  61. name: "empty",
  62. options: &AttachOptions{GetPodTimeout: 1},
  63. expectError: "at least 1 argument is required",
  64. },
  65. {
  66. name: "too many args",
  67. options: &AttachOptions{GetPodTimeout: 2},
  68. args: []string{"one", "two", "three"},
  69. expectError: "at most 2 arguments",
  70. },
  71. {
  72. name: "no container, no flags",
  73. options: &AttachOptions{GetPodTimeout: defaultPodLogsTimeout},
  74. args: []string{"foo"},
  75. expectedPodName: "foo",
  76. expectedContainerName: "bar",
  77. obj: attachPod(),
  78. },
  79. {
  80. name: "container in flag",
  81. options: &AttachOptions{StreamOptions: exec.StreamOptions{ContainerName: "bar"}, GetPodTimeout: 10000000},
  82. args: []string{"foo"},
  83. expectedPodName: "foo",
  84. expectedContainerName: "bar",
  85. obj: attachPod(),
  86. },
  87. {
  88. name: "init container in flag",
  89. options: &AttachOptions{StreamOptions: exec.StreamOptions{ContainerName: "initfoo"}, GetPodTimeout: 30},
  90. args: []string{"foo"},
  91. expectedPodName: "foo",
  92. expectedContainerName: "initfoo",
  93. obj: attachPod(),
  94. },
  95. {
  96. name: "non-existing container",
  97. options: &AttachOptions{StreamOptions: exec.StreamOptions{ContainerName: "wrong"}, GetPodTimeout: 10},
  98. args: []string{"foo"},
  99. expectedPodName: "foo",
  100. expectError: "container not found",
  101. obj: attachPod(),
  102. },
  103. {
  104. name: "no container, no flags, pods and name",
  105. options: &AttachOptions{GetPodTimeout: 10000},
  106. args: []string{"pods", "foo"},
  107. expectedPodName: "foo",
  108. expectedContainerName: "bar",
  109. obj: attachPod(),
  110. },
  111. {
  112. name: "invalid get pod timeout value",
  113. options: &AttachOptions{GetPodTimeout: 0},
  114. args: []string{"pod/foo"},
  115. expectedPodName: "foo",
  116. expectedContainerName: "bar",
  117. obj: attachPod(),
  118. expectError: "must be higher than zero",
  119. },
  120. }
  121. for _, test := range tests {
  122. t.Run(test.name, func(t *testing.T) {
  123. // setup opts to fetch our test pod
  124. test.options.AttachablePodFn = fakeAttachablePodFn(test.obj)
  125. test.options.Resources = test.args
  126. if err := test.options.Validate(); err != nil {
  127. if !strings.Contains(err.Error(), test.expectError) {
  128. t.Errorf("unexpected error: expected %q, got %q", test.expectError, err)
  129. }
  130. return
  131. }
  132. pod, err := test.options.findAttachablePod(&corev1.Pod{
  133. ObjectMeta: metav1.ObjectMeta{Name: "test-pod", Namespace: "test"},
  134. Spec: corev1.PodSpec{
  135. Containers: []corev1.Container{
  136. {
  137. Name: "foobar",
  138. },
  139. },
  140. },
  141. })
  142. if err != nil {
  143. if !strings.Contains(err.Error(), test.expectError) {
  144. t.Errorf("unexpected error: expected %q, got %q", err, test.expectError)
  145. }
  146. return
  147. }
  148. if pod.Name != test.expectedPodName {
  149. t.Errorf("unexpected pod name: expected %q, got %q", test.expectedContainerName, pod.Name)
  150. }
  151. container, err := test.options.containerToAttachTo(attachPod())
  152. if err != nil {
  153. if !strings.Contains(err.Error(), test.expectError) {
  154. t.Errorf("unexpected error: expected %q, got %q", err, test.expectError)
  155. }
  156. return
  157. }
  158. if container.Name != test.expectedContainerName {
  159. t.Errorf("unexpected container name: expected %q, got %q", test.expectedContainerName, container.Name)
  160. }
  161. if test.options.PodName != test.expectedPodName {
  162. t.Errorf("%s: expected: %s, got: %s", test.name, test.expectedPodName, test.options.PodName)
  163. }
  164. if len(test.expectError) > 0 {
  165. t.Fatalf("expected error %q, but saw none", test.expectError)
  166. }
  167. })
  168. }
  169. }
  170. func TestAttach(t *testing.T) {
  171. version := "v1"
  172. tests := []struct {
  173. name, version, podPath, fetchPodPath, attachPath, container string
  174. pod *corev1.Pod
  175. remoteAttachErr bool
  176. exepctedErr string
  177. }{
  178. {
  179. name: "pod attach",
  180. version: version,
  181. podPath: "/api/" + version + "/namespaces/test/pods/foo",
  182. fetchPodPath: "/namespaces/test/pods/foo",
  183. attachPath: "/api/" + version + "/namespaces/test/pods/foo/attach",
  184. pod: attachPod(),
  185. container: "bar",
  186. },
  187. {
  188. name: "pod attach error",
  189. version: version,
  190. podPath: "/api/" + version + "/namespaces/test/pods/foo",
  191. fetchPodPath: "/namespaces/test/pods/foo",
  192. attachPath: "/api/" + version + "/namespaces/test/pods/foo/attach",
  193. pod: attachPod(),
  194. remoteAttachErr: true,
  195. container: "bar",
  196. exepctedErr: "attach error",
  197. },
  198. {
  199. name: "container not found error",
  200. version: version,
  201. podPath: "/api/" + version + "/namespaces/test/pods/foo",
  202. fetchPodPath: "/namespaces/test/pods/foo",
  203. attachPath: "/api/" + version + "/namespaces/test/pods/foo/attach",
  204. pod: attachPod(),
  205. container: "foo",
  206. exepctedErr: "cannot attach to the container: container not found (foo)",
  207. },
  208. }
  209. for _, test := range tests {
  210. t.Run(test.name, func(t *testing.T) {
  211. tf := cmdtesting.NewTestFactory().WithNamespace("test")
  212. defer tf.Cleanup()
  213. codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
  214. ns := scheme.Codecs
  215. tf.Client = &fake.RESTClient{
  216. GroupVersion: schema.GroupVersion{Group: "", Version: "v1"},
  217. NegotiatedSerializer: ns,
  218. Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
  219. switch p, m := req.URL.Path, req.Method; {
  220. case p == test.podPath && m == "GET":
  221. body := cmdtesting.ObjBody(codec, test.pod)
  222. return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: body}, nil
  223. case p == test.fetchPodPath && m == "GET":
  224. body := cmdtesting.ObjBody(codec, test.pod)
  225. return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: body}, nil
  226. default:
  227. t.Errorf("%s: unexpected request: %s %#v\n%#v", p, req.Method, req.URL, req)
  228. return nil, fmt.Errorf("unexpected request")
  229. }
  230. }),
  231. }
  232. tf.ClientConfigVal = &restclient.Config{APIPath: "/api", ContentConfig: restclient.ContentConfig{NegotiatedSerializer: scheme.Codecs, GroupVersion: &schema.GroupVersion{Version: test.version}}}
  233. remoteAttach := &fakeRemoteAttach{}
  234. if test.remoteAttachErr {
  235. remoteAttach.err = fmt.Errorf("attach error")
  236. }
  237. options := &AttachOptions{
  238. StreamOptions: exec.StreamOptions{
  239. ContainerName: test.container,
  240. IOStreams: genericclioptions.NewTestIOStreamsDiscard(),
  241. },
  242. Attach: remoteAttach,
  243. GetPodTimeout: 1000,
  244. }
  245. options.restClientGetter = tf
  246. options.Namespace = "test"
  247. options.Resources = []string{"foo"}
  248. options.Builder = tf.NewBuilder
  249. options.AttachablePodFn = fakeAttachablePodFn(test.pod)
  250. options.AttachFunc = func(opts *AttachOptions, containerToAttach *corev1.Container, raw bool, sizeQueue remotecommand.TerminalSizeQueue) func() error {
  251. return func() error {
  252. u, err := url.Parse(fmt.Sprintf("%s?container=%s", test.attachPath, containerToAttach.Name))
  253. if err != nil {
  254. return err
  255. }
  256. return options.Attach.Attach("POST", u, nil, nil, nil, nil, raw, sizeQueue)
  257. }
  258. }
  259. err := options.Run()
  260. if test.exepctedErr != "" && err.Error() != test.exepctedErr {
  261. t.Errorf("%s: Unexpected exec error: %v", test.name, err)
  262. return
  263. }
  264. if test.exepctedErr == "" && err != nil {
  265. t.Errorf("%s: Unexpected error: %v", test.name, err)
  266. return
  267. }
  268. if test.exepctedErr != "" {
  269. return
  270. }
  271. if remoteAttach.url.Path != test.attachPath {
  272. t.Errorf("%s: Did not get expected path for exec request: %q %q", test.name, test.attachPath, remoteAttach.url.Path)
  273. return
  274. }
  275. if remoteAttach.method != "POST" {
  276. t.Errorf("%s: Did not get method for attach request: %s", test.name, remoteAttach.method)
  277. }
  278. if remoteAttach.url.Query().Get("container") != "bar" {
  279. t.Errorf("%s: Did not have query parameters: %s", test.name, remoteAttach.url.Query())
  280. }
  281. })
  282. }
  283. }
  284. func TestAttachWarnings(t *testing.T) {
  285. version := "v1"
  286. tests := []struct {
  287. name, container, version, podPath, fetchPodPath, expectedErr string
  288. pod *corev1.Pod
  289. stdin, tty bool
  290. }{
  291. {
  292. name: "fallback tty if not supported",
  293. version: version,
  294. podPath: "/api/" + version + "/namespaces/test/pods/foo",
  295. fetchPodPath: "/namespaces/test/pods/foo",
  296. pod: attachPod(),
  297. stdin: true,
  298. tty: true,
  299. expectedErr: "Unable to use a TTY - container bar did not allocate one",
  300. },
  301. }
  302. for _, test := range tests {
  303. t.Run(test.name, func(t *testing.T) {
  304. tf := cmdtesting.NewTestFactory().WithNamespace("test")
  305. defer tf.Cleanup()
  306. streams, _, _, bufErr := genericclioptions.NewTestIOStreams()
  307. codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
  308. ns := scheme.Codecs
  309. tf.Client = &fake.RESTClient{
  310. GroupVersion: schema.GroupVersion{Group: "", Version: "v1"},
  311. NegotiatedSerializer: ns,
  312. Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
  313. switch p, m := req.URL.Path, req.Method; {
  314. case p == test.podPath && m == "GET":
  315. body := cmdtesting.ObjBody(codec, test.pod)
  316. return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: body}, nil
  317. case p == test.fetchPodPath && m == "GET":
  318. body := cmdtesting.ObjBody(codec, test.pod)
  319. return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: body}, nil
  320. default:
  321. t.Errorf("%s: unexpected request: %s %#v\n%#v", p, req.Method, req.URL, req)
  322. return nil, fmt.Errorf("unexpected request")
  323. }
  324. }),
  325. }
  326. tf.ClientConfigVal = &restclient.Config{APIPath: "/api", ContentConfig: restclient.ContentConfig{NegotiatedSerializer: scheme.Codecs, GroupVersion: &schema.GroupVersion{Version: test.version}}}
  327. options := &AttachOptions{
  328. StreamOptions: exec.StreamOptions{
  329. Stdin: test.stdin,
  330. TTY: test.tty,
  331. ContainerName: test.container,
  332. IOStreams: streams,
  333. },
  334. Attach: &fakeRemoteAttach{},
  335. GetPodTimeout: 1000,
  336. }
  337. options.restClientGetter = tf
  338. options.Namespace = "test"
  339. options.Resources = []string{"foo"}
  340. options.Builder = tf.NewBuilder
  341. options.AttachablePodFn = fakeAttachablePodFn(test.pod)
  342. options.AttachFunc = func(opts *AttachOptions, containerToAttach *corev1.Container, raw bool, sizeQueue remotecommand.TerminalSizeQueue) func() error {
  343. return func() error {
  344. u, err := url.Parse("http://foo.bar")
  345. if err != nil {
  346. return err
  347. }
  348. return options.Attach.Attach("POST", u, nil, nil, nil, nil, raw, sizeQueue)
  349. }
  350. }
  351. if err := options.Run(); err != nil {
  352. t.Fatal(err)
  353. }
  354. if test.stdin && test.tty {
  355. if !test.pod.Spec.Containers[0].TTY {
  356. if !strings.Contains(bufErr.String(), test.expectedErr) {
  357. t.Errorf("%s: Expected TTY fallback warning for attach request: %s", test.name, bufErr.String())
  358. return
  359. }
  360. }
  361. }
  362. })
  363. }
  364. }
  365. func attachPod() *corev1.Pod {
  366. return &corev1.Pod{
  367. ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "test", ResourceVersion: "10"},
  368. Spec: corev1.PodSpec{
  369. RestartPolicy: corev1.RestartPolicyAlways,
  370. DNSPolicy: corev1.DNSClusterFirst,
  371. Containers: []corev1.Container{
  372. {
  373. Name: "bar",
  374. },
  375. },
  376. InitContainers: []corev1.Container{
  377. {
  378. Name: "initfoo",
  379. },
  380. },
  381. },
  382. Status: corev1.PodStatus{
  383. Phase: corev1.PodRunning,
  384. },
  385. }
  386. }