annotate_test.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571
  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 cmd
  14. import (
  15. "bytes"
  16. "net/http"
  17. "reflect"
  18. "strings"
  19. "testing"
  20. "k8s.io/kubernetes/pkg/api"
  21. "k8s.io/kubernetes/pkg/api/testapi"
  22. "k8s.io/kubernetes/pkg/client/restclient"
  23. "k8s.io/kubernetes/pkg/client/unversioned/fake"
  24. "k8s.io/kubernetes/pkg/runtime"
  25. )
  26. func TestValidateAnnotationOverwrites(t *testing.T) {
  27. tests := []struct {
  28. meta *api.ObjectMeta
  29. annotations map[string]string
  30. expectErr bool
  31. scenario string
  32. }{
  33. {
  34. meta: &api.ObjectMeta{
  35. Annotations: map[string]string{
  36. "a": "A",
  37. "b": "B",
  38. },
  39. },
  40. annotations: map[string]string{
  41. "a": "a",
  42. "c": "C",
  43. },
  44. scenario: "share first annotation",
  45. expectErr: true,
  46. },
  47. {
  48. meta: &api.ObjectMeta{
  49. Annotations: map[string]string{
  50. "a": "A",
  51. "c": "C",
  52. },
  53. },
  54. annotations: map[string]string{
  55. "b": "B",
  56. "c": "c",
  57. },
  58. scenario: "share second annotation",
  59. expectErr: true,
  60. },
  61. {
  62. meta: &api.ObjectMeta{
  63. Annotations: map[string]string{
  64. "a": "A",
  65. "c": "C",
  66. },
  67. },
  68. annotations: map[string]string{
  69. "b": "B",
  70. "d": "D",
  71. },
  72. scenario: "no overlap",
  73. },
  74. {
  75. meta: &api.ObjectMeta{},
  76. annotations: map[string]string{
  77. "a": "A",
  78. "b": "B",
  79. },
  80. scenario: "no annotations",
  81. },
  82. }
  83. for _, test := range tests {
  84. err := validateNoAnnotationOverwrites(test.meta, test.annotations)
  85. if test.expectErr && err == nil {
  86. t.Errorf("%s: unexpected non-error", test.scenario)
  87. } else if !test.expectErr && err != nil {
  88. t.Errorf("%s: unexpected error: %v", test.scenario, err)
  89. }
  90. }
  91. }
  92. func TestParseAnnotations(t *testing.T) {
  93. testURL := "https://test.com/index.htm?id=123#u=user-name"
  94. testJSON := `'{"kind":"SerializedReference","apiVersion":"v1","reference":{"kind":"ReplicationController","namespace":"default","name":"my-nginx","uid":"c544ee78-2665-11e5-8051-42010af0c213","apiVersion":"v1","resourceVersion":"61368"}}'`
  95. tests := []struct {
  96. annotations []string
  97. expected map[string]string
  98. expectedRemove []string
  99. scenario string
  100. expectedErr string
  101. expectErr bool
  102. }{
  103. {
  104. annotations: []string{"a=b", "c=d"},
  105. expected: map[string]string{"a": "b", "c": "d"},
  106. expectedRemove: []string{},
  107. scenario: "add two annotations",
  108. expectErr: false,
  109. },
  110. {
  111. annotations: []string{"url=" + testURL, api.CreatedByAnnotation + "=" + testJSON},
  112. expected: map[string]string{"url": testURL, api.CreatedByAnnotation: testJSON},
  113. expectedRemove: []string{},
  114. scenario: "add annotations with special characters",
  115. expectErr: false,
  116. },
  117. {
  118. annotations: []string{},
  119. expected: map[string]string{},
  120. expectedRemove: []string{},
  121. scenario: "add no annotations",
  122. expectErr: false,
  123. },
  124. {
  125. annotations: []string{"a=b", "c=d", "e-"},
  126. expected: map[string]string{"a": "b", "c": "d"},
  127. expectedRemove: []string{"e"},
  128. scenario: "add two annotations, remove one",
  129. expectErr: false,
  130. },
  131. {
  132. annotations: []string{"ab", "c=d"},
  133. expectedErr: "invalid annotation format: ab",
  134. scenario: "incorrect annotation input (missing =value)",
  135. expectErr: true,
  136. },
  137. {
  138. annotations: []string{"a="},
  139. expectedErr: "invalid annotation format: a=",
  140. scenario: "incorrect annotation input (missing value)",
  141. expectErr: true,
  142. },
  143. {
  144. annotations: []string{"ab", "a="},
  145. expectedErr: "invalid annotation format: ab, a=",
  146. scenario: "incorrect multiple annotation input (missing value)",
  147. expectErr: true,
  148. },
  149. }
  150. for _, test := range tests {
  151. annotations, remove, err := parseAnnotations(test.annotations)
  152. switch {
  153. case test.expectErr && err == nil:
  154. t.Errorf("%s: unexpected non-error, should return %v", test.scenario, test.expectedErr)
  155. case test.expectErr && err.Error() != test.expectedErr:
  156. t.Errorf("%s: unexpected error %v, expected %v", test.scenario, err, test.expectedErr)
  157. case !test.expectErr && err != nil:
  158. t.Errorf("%s: unexpected error %v", test.scenario, err)
  159. case !test.expectErr && !reflect.DeepEqual(annotations, test.expected):
  160. t.Errorf("%s: expected %v, got %v", test.scenario, test.expected, annotations)
  161. case !test.expectErr && !reflect.DeepEqual(remove, test.expectedRemove):
  162. t.Errorf("%s: expected %v, got %v", test.scenario, test.expectedRemove, remove)
  163. }
  164. }
  165. }
  166. func TestValidateAnnotations(t *testing.T) {
  167. tests := []struct {
  168. removeAnnotations []string
  169. newAnnotations map[string]string
  170. expectedErr string
  171. scenario string
  172. }{
  173. {
  174. expectedErr: "can not both modify and remove the following annotation(s) in the same command: a",
  175. removeAnnotations: []string{"a"},
  176. newAnnotations: map[string]string{"a": "b", "c": "d"},
  177. scenario: "remove an added annotation",
  178. },
  179. {
  180. expectedErr: "can not both modify and remove the following annotation(s) in the same command: a, c",
  181. removeAnnotations: []string{"a", "c"},
  182. newAnnotations: map[string]string{"a": "b", "c": "d"},
  183. scenario: "remove added annotations",
  184. },
  185. }
  186. for _, test := range tests {
  187. if err := validateAnnotations(test.removeAnnotations, test.newAnnotations); err == nil {
  188. t.Errorf("%s: unexpected non-error", test.scenario)
  189. } else if err.Error() != test.expectedErr {
  190. t.Errorf("%s: expected error %s, got %s", test.scenario, test.expectedErr, err.Error())
  191. }
  192. }
  193. }
  194. func TestUpdateAnnotations(t *testing.T) {
  195. tests := []struct {
  196. obj runtime.Object
  197. overwrite bool
  198. version string
  199. annotations map[string]string
  200. remove []string
  201. expected runtime.Object
  202. expectErr bool
  203. }{
  204. {
  205. obj: &api.Pod{
  206. ObjectMeta: api.ObjectMeta{
  207. Annotations: map[string]string{"a": "b"},
  208. },
  209. },
  210. annotations: map[string]string{"a": "b"},
  211. expectErr: true,
  212. },
  213. {
  214. obj: &api.Pod{
  215. ObjectMeta: api.ObjectMeta{
  216. Annotations: map[string]string{"a": "b"},
  217. },
  218. },
  219. annotations: map[string]string{"a": "c"},
  220. overwrite: true,
  221. expected: &api.Pod{
  222. ObjectMeta: api.ObjectMeta{
  223. Annotations: map[string]string{"a": "c"},
  224. },
  225. },
  226. },
  227. {
  228. obj: &api.Pod{
  229. ObjectMeta: api.ObjectMeta{
  230. Annotations: map[string]string{"a": "b"},
  231. },
  232. },
  233. annotations: map[string]string{"c": "d"},
  234. expected: &api.Pod{
  235. ObjectMeta: api.ObjectMeta{
  236. Annotations: map[string]string{"a": "b", "c": "d"},
  237. },
  238. },
  239. },
  240. {
  241. obj: &api.Pod{
  242. ObjectMeta: api.ObjectMeta{
  243. Annotations: map[string]string{"a": "b"},
  244. },
  245. },
  246. annotations: map[string]string{"c": "d"},
  247. version: "2",
  248. expected: &api.Pod{
  249. ObjectMeta: api.ObjectMeta{
  250. Annotations: map[string]string{"a": "b", "c": "d"},
  251. ResourceVersion: "2",
  252. },
  253. },
  254. },
  255. {
  256. obj: &api.Pod{
  257. ObjectMeta: api.ObjectMeta{
  258. Annotations: map[string]string{"a": "b"},
  259. },
  260. },
  261. annotations: map[string]string{},
  262. remove: []string{"a"},
  263. expected: &api.Pod{
  264. ObjectMeta: api.ObjectMeta{
  265. Annotations: map[string]string{},
  266. },
  267. },
  268. },
  269. {
  270. obj: &api.Pod{
  271. ObjectMeta: api.ObjectMeta{
  272. Annotations: map[string]string{"a": "b", "c": "d"},
  273. },
  274. },
  275. annotations: map[string]string{"e": "f"},
  276. remove: []string{"a"},
  277. expected: &api.Pod{
  278. ObjectMeta: api.ObjectMeta{
  279. Annotations: map[string]string{
  280. "c": "d",
  281. "e": "f",
  282. },
  283. },
  284. },
  285. },
  286. {
  287. obj: &api.Pod{
  288. ObjectMeta: api.ObjectMeta{
  289. Annotations: map[string]string{"a": "b", "c": "d"},
  290. },
  291. },
  292. annotations: map[string]string{"e": "f"},
  293. remove: []string{"g"},
  294. expected: &api.Pod{
  295. ObjectMeta: api.ObjectMeta{
  296. Annotations: map[string]string{
  297. "a": "b",
  298. "c": "d",
  299. "e": "f",
  300. },
  301. },
  302. },
  303. },
  304. {
  305. obj: &api.Pod{
  306. ObjectMeta: api.ObjectMeta{
  307. Annotations: map[string]string{"a": "b", "c": "d"},
  308. },
  309. },
  310. remove: []string{"e"},
  311. expected: &api.Pod{
  312. ObjectMeta: api.ObjectMeta{
  313. Annotations: map[string]string{
  314. "a": "b",
  315. "c": "d",
  316. },
  317. },
  318. },
  319. },
  320. {
  321. obj: &api.Pod{
  322. ObjectMeta: api.ObjectMeta{},
  323. },
  324. annotations: map[string]string{"a": "b"},
  325. expected: &api.Pod{
  326. ObjectMeta: api.ObjectMeta{
  327. Annotations: map[string]string{"a": "b"},
  328. },
  329. },
  330. },
  331. }
  332. for _, test := range tests {
  333. options := &AnnotateOptions{
  334. overwrite: test.overwrite,
  335. newAnnotations: test.annotations,
  336. removeAnnotations: test.remove,
  337. resourceVersion: test.version,
  338. }
  339. err := options.updateAnnotations(test.obj)
  340. if test.expectErr {
  341. if err == nil {
  342. t.Errorf("unexpected non-error: %v", test)
  343. }
  344. continue
  345. }
  346. if !test.expectErr && err != nil {
  347. t.Errorf("unexpected error: %v %v", err, test)
  348. }
  349. if !reflect.DeepEqual(test.obj, test.expected) {
  350. t.Errorf("expected: %v, got %v", test.expected, test.obj)
  351. }
  352. }
  353. }
  354. func TestAnnotateErrors(t *testing.T) {
  355. testCases := map[string]struct {
  356. args []string
  357. flags map[string]string
  358. errFn func(error) bool
  359. }{
  360. "no args": {
  361. args: []string{},
  362. errFn: func(err error) bool { return strings.Contains(err.Error(), "one or more resources must be specified") },
  363. },
  364. "not enough annotations": {
  365. args: []string{"pods"},
  366. errFn: func(err error) bool {
  367. return strings.Contains(err.Error(), "at least one annotation update is required")
  368. },
  369. },
  370. "no resources remove annotations": {
  371. args: []string{"pods-"},
  372. errFn: func(err error) bool { return strings.Contains(err.Error(), "one or more resources must be specified") },
  373. },
  374. "no resources add annotations": {
  375. args: []string{"pods=bar"},
  376. errFn: func(err error) bool { return strings.Contains(err.Error(), "one or more resources must be specified") },
  377. },
  378. }
  379. for k, testCase := range testCases {
  380. f, tf, _, _ := NewAPIFactory()
  381. tf.Printer = &testPrinter{}
  382. tf.Namespace = "test"
  383. tf.ClientConfig = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: testapi.Default.GroupVersion()}}
  384. buf := bytes.NewBuffer([]byte{})
  385. cmd := NewCmdAnnotate(f, buf)
  386. cmd.SetOutput(buf)
  387. for k, v := range testCase.flags {
  388. cmd.Flags().Set(k, v)
  389. }
  390. options := &AnnotateOptions{}
  391. err := options.Complete(f, buf, cmd, testCase.args)
  392. if !testCase.errFn(err) {
  393. t.Errorf("%s: unexpected error: %v", k, err)
  394. continue
  395. }
  396. if tf.Printer.(*testPrinter).Objects != nil {
  397. t.Errorf("unexpected print to default printer")
  398. }
  399. if buf.Len() > 0 {
  400. t.Errorf("buffer should be empty: %s", string(buf.Bytes()))
  401. }
  402. }
  403. }
  404. func TestAnnotateObject(t *testing.T) {
  405. pods, _, _ := testData()
  406. f, tf, codec, ns := NewAPIFactory()
  407. tf.Printer = &testPrinter{}
  408. tf.Client = &fake.RESTClient{
  409. NegotiatedSerializer: ns,
  410. Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
  411. switch req.Method {
  412. case "GET":
  413. switch req.URL.Path {
  414. case "/namespaces/test/pods/foo":
  415. return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: objBody(codec, &pods.Items[0])}, nil
  416. default:
  417. t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
  418. return nil, nil
  419. }
  420. case "PATCH":
  421. switch req.URL.Path {
  422. case "/namespaces/test/pods/foo":
  423. return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: objBody(codec, &pods.Items[0])}, nil
  424. default:
  425. t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
  426. return nil, nil
  427. }
  428. default:
  429. t.Fatalf("unexpected request: %s %#v\n%#v", req.Method, req.URL, req)
  430. return nil, nil
  431. }
  432. }),
  433. }
  434. tf.Namespace = "test"
  435. tf.ClientConfig = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: testapi.Default.GroupVersion()}}
  436. buf := bytes.NewBuffer([]byte{})
  437. cmd := NewCmdAnnotate(f, buf)
  438. cmd.SetOutput(buf)
  439. options := &AnnotateOptions{}
  440. args := []string{"pods/foo", "a=b", "c-"}
  441. if err := options.Complete(f, buf, cmd, args); err != nil {
  442. t.Fatalf("unexpected error: %v", err)
  443. }
  444. if err := options.Validate(args); err != nil {
  445. t.Fatalf("unexpected error: %v", err)
  446. }
  447. if err := options.RunAnnotate(); err != nil {
  448. t.Fatalf("unexpected error: %v", err)
  449. }
  450. }
  451. func TestAnnotateObjectFromFile(t *testing.T) {
  452. pods, _, _ := testData()
  453. f, tf, codec, ns := NewAPIFactory()
  454. tf.Printer = &testPrinter{}
  455. tf.Client = &fake.RESTClient{
  456. NegotiatedSerializer: ns,
  457. Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
  458. switch req.Method {
  459. case "GET":
  460. switch req.URL.Path {
  461. case "/namespaces/test/replicationcontrollers/cassandra":
  462. return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: objBody(codec, &pods.Items[0])}, nil
  463. default:
  464. t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
  465. return nil, nil
  466. }
  467. case "PATCH":
  468. switch req.URL.Path {
  469. case "/namespaces/test/replicationcontrollers/cassandra":
  470. return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: objBody(codec, &pods.Items[0])}, nil
  471. default:
  472. t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
  473. return nil, nil
  474. }
  475. default:
  476. t.Fatalf("unexpected request: %s %#v\n%#v", req.Method, req.URL, req)
  477. return nil, nil
  478. }
  479. }),
  480. }
  481. tf.Namespace = "test"
  482. tf.ClientConfig = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: testapi.Default.GroupVersion()}}
  483. buf := bytes.NewBuffer([]byte{})
  484. cmd := NewCmdAnnotate(f, buf)
  485. cmd.SetOutput(buf)
  486. options := &AnnotateOptions{}
  487. options.filenames = []string{"../../../examples/storage/cassandra/cassandra-controller.yaml"}
  488. args := []string{"a=b", "c-"}
  489. if err := options.Complete(f, buf, cmd, args); err != nil {
  490. t.Fatalf("unexpected error: %v", err)
  491. }
  492. if err := options.Validate(args); err != nil {
  493. t.Fatalf("unexpected error: %v", err)
  494. }
  495. if err := options.RunAnnotate(); err != nil {
  496. t.Fatalf("unexpected error: %v", err)
  497. }
  498. }
  499. func TestAnnotateMultipleObjects(t *testing.T) {
  500. pods, _, _ := testData()
  501. f, tf, codec, ns := NewAPIFactory()
  502. tf.Printer = &testPrinter{}
  503. tf.Client = &fake.RESTClient{
  504. NegotiatedSerializer: ns,
  505. Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
  506. switch req.Method {
  507. case "GET":
  508. switch req.URL.Path {
  509. case "/namespaces/test/pods":
  510. return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: objBody(codec, pods)}, nil
  511. default:
  512. t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
  513. return nil, nil
  514. }
  515. case "PATCH":
  516. switch req.URL.Path {
  517. case "/namespaces/test/pods/foo":
  518. return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: objBody(codec, &pods.Items[0])}, nil
  519. case "/namespaces/test/pods/bar":
  520. return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: objBody(codec, &pods.Items[1])}, nil
  521. default:
  522. t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
  523. return nil, nil
  524. }
  525. default:
  526. t.Fatalf("unexpected request: %s %#v\n%#v", req.Method, req.URL, req)
  527. return nil, nil
  528. }
  529. }),
  530. }
  531. tf.Namespace = "test"
  532. tf.ClientConfig = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: testapi.Default.GroupVersion()}}
  533. buf := bytes.NewBuffer([]byte{})
  534. cmd := NewCmdAnnotate(f, buf)
  535. cmd.SetOutput(buf)
  536. options := &AnnotateOptions{}
  537. options.all = true
  538. args := []string{"pods", "a=b", "c-"}
  539. if err := options.Complete(f, buf, cmd, args); err != nil {
  540. t.Fatalf("unexpected error: %v", err)
  541. }
  542. if err := options.Validate(args); err != nil {
  543. t.Fatalf("unexpected error: %v", err)
  544. }
  545. if err := options.RunAnnotate(); err != nil {
  546. t.Fatalf("unexpected error: %v", err)
  547. }
  548. }