drain_test.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571
  1. /*
  2. Copyright 2015 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 cmd
  14. import (
  15. "bytes"
  16. "io"
  17. "io/ioutil"
  18. "net/http"
  19. "net/url"
  20. "os"
  21. "reflect"
  22. "strings"
  23. "testing"
  24. "time"
  25. "github.com/spf13/cobra"
  26. "k8s.io/kubernetes/pkg/api"
  27. "k8s.io/kubernetes/pkg/api/testapi"
  28. "k8s.io/kubernetes/pkg/api/unversioned"
  29. "k8s.io/kubernetes/pkg/apis/batch"
  30. "k8s.io/kubernetes/pkg/apis/extensions"
  31. "k8s.io/kubernetes/pkg/client/unversioned/fake"
  32. "k8s.io/kubernetes/pkg/conversion"
  33. cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
  34. "k8s.io/kubernetes/pkg/runtime"
  35. )
  36. var node *api.Node
  37. var cordoned_node *api.Node
  38. func TestMain(m *testing.M) {
  39. // Create a node.
  40. node = &api.Node{
  41. ObjectMeta: api.ObjectMeta{
  42. Name: "node",
  43. CreationTimestamp: unversioned.Time{Time: time.Now()},
  44. },
  45. Spec: api.NodeSpec{
  46. ExternalID: "node",
  47. },
  48. Status: api.NodeStatus{},
  49. }
  50. clone, _ := conversion.NewCloner().DeepCopy(node)
  51. // A copy of the same node, but cordoned.
  52. cordoned_node = clone.(*api.Node)
  53. cordoned_node.Spec.Unschedulable = true
  54. os.Exit(m.Run())
  55. }
  56. func TestCordon(t *testing.T) {
  57. tests := []struct {
  58. description string
  59. node *api.Node
  60. expected *api.Node
  61. cmd func(*cmdutil.Factory, io.Writer) *cobra.Command
  62. arg string
  63. expectFatal bool
  64. }{
  65. {
  66. description: "node/node syntax",
  67. node: cordoned_node,
  68. expected: node,
  69. cmd: NewCmdUncordon,
  70. arg: "node/node",
  71. expectFatal: false,
  72. },
  73. {
  74. description: "uncordon for real",
  75. node: cordoned_node,
  76. expected: node,
  77. cmd: NewCmdUncordon,
  78. arg: "node",
  79. expectFatal: false,
  80. },
  81. {
  82. description: "uncordon does nothing",
  83. node: node,
  84. expected: node,
  85. cmd: NewCmdUncordon,
  86. arg: "node",
  87. expectFatal: false,
  88. },
  89. {
  90. description: "cordon does nothing",
  91. node: cordoned_node,
  92. expected: cordoned_node,
  93. cmd: NewCmdCordon,
  94. arg: "node",
  95. expectFatal: false,
  96. },
  97. {
  98. description: "cordon for real",
  99. node: node,
  100. expected: cordoned_node,
  101. cmd: NewCmdCordon,
  102. arg: "node",
  103. expectFatal: false,
  104. },
  105. {
  106. description: "cordon missing node",
  107. node: node,
  108. expected: node,
  109. cmd: NewCmdCordon,
  110. arg: "bar",
  111. expectFatal: true,
  112. },
  113. {
  114. description: "uncordon missing node",
  115. node: node,
  116. expected: node,
  117. cmd: NewCmdUncordon,
  118. arg: "bar",
  119. expectFatal: true,
  120. },
  121. }
  122. for _, test := range tests {
  123. f, tf, codec, ns := NewAPIFactory()
  124. new_node := &api.Node{}
  125. updated := false
  126. tf.Client = &fake.RESTClient{
  127. NegotiatedSerializer: ns,
  128. Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
  129. m := &MyReq{req}
  130. switch {
  131. case m.isFor("GET", "/nodes/node"):
  132. return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: objBody(codec, test.node)}, nil
  133. case m.isFor("GET", "/nodes/bar"):
  134. return &http.Response{StatusCode: 404, Header: defaultHeader(), Body: stringBody("nope")}, nil
  135. case m.isFor("PUT", "/nodes/node"):
  136. data, err := ioutil.ReadAll(req.Body)
  137. if err != nil {
  138. t.Fatalf("%s: unexpected error: %v", test.description, err)
  139. }
  140. defer req.Body.Close()
  141. if err := runtime.DecodeInto(codec, data, new_node); err != nil {
  142. t.Fatalf("%s: unexpected error: %v", test.description, err)
  143. }
  144. if !reflect.DeepEqual(test.expected.Spec, new_node.Spec) {
  145. t.Fatalf("%s: expected:\n%v\nsaw:\n%v\n", test.description, test.expected.Spec, new_node.Spec)
  146. }
  147. updated = true
  148. return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: objBody(codec, new_node)}, nil
  149. default:
  150. t.Fatalf("%s: unexpected request: %v %#v\n%#v", test.description, req.Method, req.URL, req)
  151. return nil, nil
  152. }
  153. }),
  154. }
  155. tf.ClientConfig = defaultClientConfig()
  156. buf := bytes.NewBuffer([]byte{})
  157. cmd := test.cmd(f, buf)
  158. saw_fatal := false
  159. func() {
  160. defer func() {
  161. // Recover from the panic below.
  162. _ = recover()
  163. // Restore cmdutil behavior
  164. cmdutil.DefaultBehaviorOnFatal()
  165. }()
  166. cmdutil.BehaviorOnFatal(func(e string, code int) { saw_fatal = true; panic(e) })
  167. cmd.SetArgs([]string{test.arg})
  168. cmd.Execute()
  169. }()
  170. if test.expectFatal {
  171. if !saw_fatal {
  172. t.Fatalf("%s: unexpected non-error", test.description)
  173. }
  174. if updated {
  175. t.Fatalf("%s: unexpcted update", test.description)
  176. }
  177. }
  178. if !test.expectFatal && saw_fatal {
  179. t.Fatalf("%s: unexpected error", test.description)
  180. }
  181. if !reflect.DeepEqual(test.expected.Spec, test.node.Spec) && !updated {
  182. t.Fatalf("%s: node never updated", test.description)
  183. }
  184. }
  185. }
  186. func TestDrain(t *testing.T) {
  187. labels := make(map[string]string)
  188. labels["my_key"] = "my_value"
  189. rc := api.ReplicationController{
  190. ObjectMeta: api.ObjectMeta{
  191. Name: "rc",
  192. Namespace: "default",
  193. CreationTimestamp: unversioned.Time{Time: time.Now()},
  194. Labels: labels,
  195. SelfLink: testapi.Default.SelfLink("replicationcontrollers", "rc"),
  196. },
  197. Spec: api.ReplicationControllerSpec{
  198. Selector: labels,
  199. },
  200. }
  201. rc_anno := make(map[string]string)
  202. rc_anno[api.CreatedByAnnotation] = refJson(t, &rc)
  203. rc_pod := api.Pod{
  204. ObjectMeta: api.ObjectMeta{
  205. Name: "bar",
  206. Namespace: "default",
  207. CreationTimestamp: unversioned.Time{Time: time.Now()},
  208. Labels: labels,
  209. Annotations: rc_anno,
  210. },
  211. Spec: api.PodSpec{
  212. NodeName: "node",
  213. },
  214. }
  215. ds := extensions.DaemonSet{
  216. ObjectMeta: api.ObjectMeta{
  217. Name: "ds",
  218. Namespace: "default",
  219. CreationTimestamp: unversioned.Time{Time: time.Now()},
  220. SelfLink: "/apis/extensions/v1beta1/namespaces/default/daemonsets/ds",
  221. },
  222. Spec: extensions.DaemonSetSpec{
  223. Selector: &unversioned.LabelSelector{MatchLabels: labels},
  224. },
  225. }
  226. ds_anno := make(map[string]string)
  227. ds_anno[api.CreatedByAnnotation] = refJson(t, &ds)
  228. ds_pod := api.Pod{
  229. ObjectMeta: api.ObjectMeta{
  230. Name: "bar",
  231. Namespace: "default",
  232. CreationTimestamp: unversioned.Time{Time: time.Now()},
  233. Labels: labels,
  234. Annotations: ds_anno,
  235. },
  236. Spec: api.PodSpec{
  237. NodeName: "node",
  238. },
  239. }
  240. job := batch.Job{
  241. ObjectMeta: api.ObjectMeta{
  242. Name: "job",
  243. Namespace: "default",
  244. CreationTimestamp: unversioned.Time{Time: time.Now()},
  245. SelfLink: "/apis/extensions/v1beta1/namespaces/default/jobs/job",
  246. },
  247. Spec: batch.JobSpec{
  248. Selector: &unversioned.LabelSelector{MatchLabels: labels},
  249. },
  250. }
  251. job_pod := api.Pod{
  252. ObjectMeta: api.ObjectMeta{
  253. Name: "bar",
  254. Namespace: "default",
  255. CreationTimestamp: unversioned.Time{Time: time.Now()},
  256. Labels: labels,
  257. Annotations: map[string]string{api.CreatedByAnnotation: refJson(t, &job)},
  258. },
  259. }
  260. rs := extensions.ReplicaSet{
  261. ObjectMeta: api.ObjectMeta{
  262. Name: "rs",
  263. Namespace: "default",
  264. CreationTimestamp: unversioned.Time{Time: time.Now()},
  265. Labels: labels,
  266. SelfLink: testapi.Default.SelfLink("replicasets", "rs"),
  267. },
  268. Spec: extensions.ReplicaSetSpec{
  269. Selector: &unversioned.LabelSelector{MatchLabels: labels},
  270. },
  271. }
  272. rs_anno := make(map[string]string)
  273. rs_anno[api.CreatedByAnnotation] = refJson(t, &rs)
  274. rs_pod := api.Pod{
  275. ObjectMeta: api.ObjectMeta{
  276. Name: "bar",
  277. Namespace: "default",
  278. CreationTimestamp: unversioned.Time{Time: time.Now()},
  279. Labels: labels,
  280. Annotations: rs_anno,
  281. },
  282. Spec: api.PodSpec{
  283. NodeName: "node",
  284. },
  285. }
  286. naked_pod := api.Pod{
  287. ObjectMeta: api.ObjectMeta{
  288. Name: "bar",
  289. Namespace: "default",
  290. CreationTimestamp: unversioned.Time{Time: time.Now()},
  291. Labels: labels,
  292. },
  293. Spec: api.PodSpec{
  294. NodeName: "node",
  295. },
  296. }
  297. emptydir_pod := api.Pod{
  298. ObjectMeta: api.ObjectMeta{
  299. Name: "bar",
  300. Namespace: "default",
  301. CreationTimestamp: unversioned.Time{Time: time.Now()},
  302. Labels: labels,
  303. },
  304. Spec: api.PodSpec{
  305. NodeName: "node",
  306. Volumes: []api.Volume{
  307. {
  308. Name: "scratch",
  309. VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{Medium: ""}},
  310. },
  311. },
  312. },
  313. }
  314. tests := []struct {
  315. description string
  316. node *api.Node
  317. expected *api.Node
  318. pods []api.Pod
  319. rcs []api.ReplicationController
  320. replicaSets []extensions.ReplicaSet
  321. args []string
  322. expectFatal bool
  323. expectDelete bool
  324. }{
  325. {
  326. description: "RC-managed pod",
  327. node: node,
  328. expected: cordoned_node,
  329. pods: []api.Pod{rc_pod},
  330. rcs: []api.ReplicationController{rc},
  331. args: []string{"node"},
  332. expectFatal: false,
  333. expectDelete: true,
  334. },
  335. {
  336. description: "DS-managed pod",
  337. node: node,
  338. expected: cordoned_node,
  339. pods: []api.Pod{ds_pod},
  340. rcs: []api.ReplicationController{rc},
  341. args: []string{"node"},
  342. expectFatal: true,
  343. expectDelete: false,
  344. },
  345. {
  346. description: "DS-managed pod with --ignore-daemonsets",
  347. node: node,
  348. expected: cordoned_node,
  349. pods: []api.Pod{ds_pod},
  350. rcs: []api.ReplicationController{rc},
  351. args: []string{"node", "--ignore-daemonsets"},
  352. expectFatal: false,
  353. expectDelete: false,
  354. },
  355. {
  356. description: "Job-managed pod",
  357. node: node,
  358. expected: cordoned_node,
  359. pods: []api.Pod{job_pod},
  360. rcs: []api.ReplicationController{rc},
  361. args: []string{"node"},
  362. expectFatal: false,
  363. expectDelete: true,
  364. },
  365. {
  366. description: "RS-managed pod",
  367. node: node,
  368. expected: cordoned_node,
  369. pods: []api.Pod{rs_pod},
  370. replicaSets: []extensions.ReplicaSet{rs},
  371. args: []string{"node"},
  372. expectFatal: false,
  373. expectDelete: true,
  374. },
  375. {
  376. description: "naked pod",
  377. node: node,
  378. expected: cordoned_node,
  379. pods: []api.Pod{naked_pod},
  380. rcs: []api.ReplicationController{},
  381. args: []string{"node"},
  382. expectFatal: true,
  383. expectDelete: false,
  384. },
  385. {
  386. description: "naked pod with --force",
  387. node: node,
  388. expected: cordoned_node,
  389. pods: []api.Pod{naked_pod},
  390. rcs: []api.ReplicationController{},
  391. args: []string{"node", "--force"},
  392. expectFatal: false,
  393. expectDelete: true,
  394. },
  395. {
  396. description: "pod with EmptyDir",
  397. node: node,
  398. expected: cordoned_node,
  399. pods: []api.Pod{emptydir_pod},
  400. args: []string{"node", "--force"},
  401. expectFatal: true,
  402. expectDelete: false,
  403. },
  404. {
  405. description: "pod with EmptyDir and --delete-local-data",
  406. node: node,
  407. expected: cordoned_node,
  408. pods: []api.Pod{emptydir_pod},
  409. args: []string{"node", "--force", "--delete-local-data=true"},
  410. expectFatal: false,
  411. expectDelete: true,
  412. },
  413. {
  414. description: "empty node",
  415. node: node,
  416. expected: cordoned_node,
  417. pods: []api.Pod{},
  418. rcs: []api.ReplicationController{rc},
  419. args: []string{"node"},
  420. expectFatal: false,
  421. expectDelete: false,
  422. },
  423. }
  424. for _, test := range tests {
  425. new_node := &api.Node{}
  426. deleted := false
  427. f, tf, codec, ns := NewAPIFactory()
  428. tf.Client = &fake.RESTClient{
  429. NegotiatedSerializer: ns,
  430. Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
  431. m := &MyReq{req}
  432. switch {
  433. case m.isFor("GET", "/nodes/node"):
  434. return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: objBody(codec, test.node)}, nil
  435. case m.isFor("GET", "/namespaces/default/replicationcontrollers/rc"):
  436. return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: objBody(codec, &test.rcs[0])}, nil
  437. case m.isFor("GET", "/namespaces/default/daemonsets/ds"):
  438. return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: objBody(testapi.Extensions.Codec(), &ds)}, nil
  439. case m.isFor("GET", "/namespaces/default/jobs/job"):
  440. return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: objBody(testapi.Extensions.Codec(), &job)}, nil
  441. case m.isFor("GET", "/namespaces/default/replicasets/rs"):
  442. return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: objBody(testapi.Extensions.Codec(), &test.replicaSets[0])}, nil
  443. case m.isFor("GET", "/pods"):
  444. values, err := url.ParseQuery(req.URL.RawQuery)
  445. if err != nil {
  446. t.Fatalf("%s: unexpected error: %v", test.description, err)
  447. }
  448. get_params := make(url.Values)
  449. get_params["fieldSelector"] = []string{"spec.nodeName=node"}
  450. if !reflect.DeepEqual(get_params, values) {
  451. t.Fatalf("%s: expected:\n%v\nsaw:\n%v\n", test.description, get_params, values)
  452. }
  453. return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: objBody(codec, &api.PodList{Items: test.pods})}, nil
  454. case m.isFor("GET", "/replicationcontrollers"):
  455. return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: objBody(codec, &api.ReplicationControllerList{Items: test.rcs})}, nil
  456. case m.isFor("PUT", "/nodes/node"):
  457. data, err := ioutil.ReadAll(req.Body)
  458. if err != nil {
  459. t.Fatalf("%s: unexpected error: %v", test.description, err)
  460. }
  461. defer req.Body.Close()
  462. if err := runtime.DecodeInto(codec, data, new_node); err != nil {
  463. t.Fatalf("%s: unexpected error: %v", test.description, err)
  464. }
  465. if !reflect.DeepEqual(test.expected.Spec, new_node.Spec) {
  466. t.Fatalf("%s: expected:\n%v\nsaw:\n%v\n", test.description, test.expected.Spec, new_node.Spec)
  467. }
  468. return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: objBody(codec, new_node)}, nil
  469. case m.isFor("DELETE", "/namespaces/default/pods/bar"):
  470. deleted = true
  471. return &http.Response{StatusCode: 204, Header: defaultHeader(), Body: objBody(codec, &test.pods[0])}, nil
  472. default:
  473. t.Fatalf("%s: unexpected request: %v %#v\n%#v", test.description, req.Method, req.URL, req)
  474. return nil, nil
  475. }
  476. }),
  477. }
  478. tf.ClientConfig = defaultClientConfig()
  479. buf := bytes.NewBuffer([]byte{})
  480. cmd := NewCmdDrain(f, buf)
  481. saw_fatal := false
  482. func() {
  483. defer func() {
  484. // Recover from the panic below.
  485. _ = recover()
  486. // Restore cmdutil behavior
  487. cmdutil.DefaultBehaviorOnFatal()
  488. }()
  489. cmdutil.BehaviorOnFatal(func(e string, code int) { saw_fatal = true; panic(e) })
  490. cmd.SetArgs(test.args)
  491. cmd.Execute()
  492. }()
  493. if test.expectFatal {
  494. if !saw_fatal {
  495. t.Fatalf("%s: unexpected non-error", test.description)
  496. }
  497. }
  498. if test.expectDelete {
  499. if !deleted {
  500. t.Fatalf("%s: pod never deleted", test.description)
  501. }
  502. }
  503. if !test.expectDelete {
  504. if deleted {
  505. t.Fatalf("%s: unexpected delete", test.description)
  506. }
  507. }
  508. }
  509. }
  510. type MyReq struct {
  511. Request *http.Request
  512. }
  513. func (m *MyReq) isFor(method string, path string) bool {
  514. req := m.Request
  515. return method == req.Method && (req.URL.Path == path || req.URL.Path == strings.Join([]string{"/api/v1", path}, "") || req.URL.Path == strings.Join([]string{"/apis/extensions/v1beta1", path}, ""))
  516. }
  517. func refJson(t *testing.T, o runtime.Object) string {
  518. ref, err := api.GetReference(o)
  519. if err != nil {
  520. t.Fatalf("unexpected error: %v", err)
  521. }
  522. _, _, codec, _ := NewAPIFactory()
  523. json, err := runtime.Encode(codec, &api.SerializedReference{Reference: *ref})
  524. if err != nil {
  525. t.Fatalf("unexpected error: %v", err)
  526. }
  527. return string(json)
  528. }