resthandler_test.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473
  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 apiserver
  14. import (
  15. "errors"
  16. "fmt"
  17. "reflect"
  18. "testing"
  19. "time"
  20. "github.com/emicklei/go-restful"
  21. "github.com/evanphx/json-patch"
  22. "k8s.io/kubernetes/pkg/api"
  23. apierrors "k8s.io/kubernetes/pkg/api/errors"
  24. "k8s.io/kubernetes/pkg/api/rest"
  25. "k8s.io/kubernetes/pkg/api/testapi"
  26. "k8s.io/kubernetes/pkg/api/unversioned"
  27. "k8s.io/kubernetes/pkg/api/v1"
  28. "k8s.io/kubernetes/pkg/runtime"
  29. "k8s.io/kubernetes/pkg/types"
  30. "k8s.io/kubernetes/pkg/util/diff"
  31. "k8s.io/kubernetes/pkg/util/strategicpatch"
  32. )
  33. type testPatchType struct {
  34. unversioned.TypeMeta `json:",inline"`
  35. TestPatchSubType `json:",inline"`
  36. }
  37. // We explicitly make it public as private types doesn't
  38. // work correctly with json inlined types.
  39. type TestPatchSubType struct {
  40. StringField string `json:"theField"`
  41. }
  42. func (obj *testPatchType) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta }
  43. func TestPatchAnonymousField(t *testing.T) {
  44. originalJS := `{"kind":"testPatchType","theField":"my-value"}`
  45. patch := `{"theField": "changed!"}`
  46. expectedJS := `{"kind":"testPatchType","theField":"changed!"}`
  47. actualBytes, err := getPatchedJS(api.StrategicMergePatchType, []byte(originalJS), []byte(patch), &testPatchType{})
  48. if err != nil {
  49. t.Fatalf("unexpected error: %v", err)
  50. }
  51. if string(actualBytes) != expectedJS {
  52. t.Errorf("expected %v, got %v", expectedJS, string(actualBytes))
  53. }
  54. }
  55. type testPatcher struct {
  56. t *testing.T
  57. // startingPod is used for the first Update
  58. startingPod *api.Pod
  59. // updatePod is the pod that is used for conflict comparison and used for subsequent Update calls
  60. updatePod *api.Pod
  61. numUpdates int
  62. }
  63. func (p *testPatcher) New() runtime.Object {
  64. return &api.Pod{}
  65. }
  66. func (p *testPatcher) Update(ctx api.Context, name string, objInfo rest.UpdatedObjectInfo) (runtime.Object, bool, error) {
  67. currentPod := p.startingPod
  68. if p.numUpdates > 0 {
  69. currentPod = p.updatePod
  70. }
  71. p.numUpdates++
  72. obj, err := objInfo.UpdatedObject(ctx, currentPod)
  73. if err != nil {
  74. return nil, false, err
  75. }
  76. inPod := obj.(*api.Pod)
  77. if inPod.ResourceVersion != p.updatePod.ResourceVersion {
  78. return nil, false, apierrors.NewConflict(api.Resource("pods"), inPod.Name, fmt.Errorf("existing %v, new %v", p.updatePod.ResourceVersion, inPod.ResourceVersion))
  79. }
  80. return inPod, false, nil
  81. }
  82. func (p *testPatcher) Get(ctx api.Context, name string) (runtime.Object, error) {
  83. p.t.Fatal("Unexpected call to testPatcher.Get")
  84. return nil, errors.New("Unexpected call to testPatcher.Get")
  85. }
  86. type testNamer struct {
  87. namespace string
  88. name string
  89. }
  90. func (p *testNamer) Namespace(req *restful.Request) (namespace string, err error) {
  91. return p.namespace, nil
  92. }
  93. // Name returns the name from the request, and an optional namespace value if this is a namespace
  94. // scoped call. An error is returned if the name is not available.
  95. func (p *testNamer) Name(req *restful.Request) (namespace, name string, err error) {
  96. return p.namespace, p.name, nil
  97. }
  98. // ObjectName returns the namespace and name from an object if they exist, or an error if the object
  99. // does not support names.
  100. func (p *testNamer) ObjectName(obj runtime.Object) (namespace, name string, err error) {
  101. return p.namespace, p.name, nil
  102. }
  103. // SetSelfLink sets the provided URL onto the object. The method should return nil if the object
  104. // does not support selfLinks.
  105. func (p *testNamer) SetSelfLink(obj runtime.Object, url string) error {
  106. return errors.New("not implemented")
  107. }
  108. // GenerateLink creates a path and query for a given runtime object that represents the canonical path.
  109. func (p *testNamer) GenerateLink(req *restful.Request, obj runtime.Object) (path, query string, err error) {
  110. return "", "", errors.New("not implemented")
  111. }
  112. // GenerateLink creates a path and query for a list that represents the canonical path.
  113. func (p *testNamer) GenerateListLink(req *restful.Request) (path, query string, err error) {
  114. return "", "", errors.New("not implemented")
  115. }
  116. type patchTestCase struct {
  117. name string
  118. // admission chain to use, nil is fine
  119. admit updateAdmissionFunc
  120. // startingPod is used as the starting point for the first Update
  121. startingPod *api.Pod
  122. // changedPod is the "destination" pod for the patch. The test will create a patch from the startingPod to the changedPod
  123. // to use when calling the patch operation
  124. changedPod *api.Pod
  125. // updatePod is the pod that is used for conflict comparison and as the starting point for the second Update
  126. updatePod *api.Pod
  127. // expectedPod is the pod that you expect to get back after the patch is complete
  128. expectedPod *api.Pod
  129. expectedError string
  130. }
  131. func (tc *patchTestCase) Run(t *testing.T) {
  132. t.Logf("Starting test %s", tc.name)
  133. namespace := tc.startingPod.Namespace
  134. name := tc.startingPod.Name
  135. codec := testapi.Default.Codec()
  136. admit := tc.admit
  137. if admit == nil {
  138. admit = func(updatedObject runtime.Object, currentObject runtime.Object) error {
  139. return nil
  140. }
  141. }
  142. testPatcher := &testPatcher{}
  143. testPatcher.t = t
  144. testPatcher.startingPod = tc.startingPod
  145. testPatcher.updatePod = tc.updatePod
  146. ctx := api.NewDefaultContext()
  147. ctx = api.WithNamespace(ctx, namespace)
  148. namer := &testNamer{namespace, name}
  149. copier := runtime.ObjectCopier(api.Scheme)
  150. resource := unversioned.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}
  151. versionedObj := &v1.Pod{}
  152. for _, patchType := range []api.PatchType{api.JSONPatchType, api.MergePatchType, api.StrategicMergePatchType} {
  153. // TODO SUPPORT THIS!
  154. if patchType == api.JSONPatchType {
  155. continue
  156. }
  157. t.Logf("Working with patchType %v", patchType)
  158. originalObjJS, err := runtime.Encode(codec, tc.startingPod)
  159. if err != nil {
  160. t.Errorf("%s: unexpected error: %v", tc.name, err)
  161. return
  162. }
  163. changedJS, err := runtime.Encode(codec, tc.changedPod)
  164. if err != nil {
  165. t.Errorf("%s: unexpected error: %v", tc.name, err)
  166. return
  167. }
  168. patch := []byte{}
  169. switch patchType {
  170. case api.JSONPatchType:
  171. continue
  172. case api.StrategicMergePatchType:
  173. patch, err = strategicpatch.CreateStrategicMergePatch(originalObjJS, changedJS, versionedObj)
  174. if err != nil {
  175. t.Errorf("%s: unexpected error: %v", tc.name, err)
  176. return
  177. }
  178. case api.MergePatchType:
  179. patch, err = jsonpatch.CreateMergePatch(originalObjJS, changedJS)
  180. if err != nil {
  181. t.Errorf("%s: unexpected error: %v", tc.name, err)
  182. return
  183. }
  184. }
  185. resultObj, err := patchResource(ctx, admit, 1*time.Second, versionedObj, testPatcher, name, patchType, patch, namer, copier, resource, codec)
  186. if len(tc.expectedError) != 0 {
  187. if err == nil || err.Error() != tc.expectedError {
  188. t.Errorf("%s: expected error %v, but got %v", tc.name, tc.expectedError, err)
  189. return
  190. }
  191. } else {
  192. if err != nil {
  193. t.Errorf("%s: unexpected error: %v", tc.name, err)
  194. return
  195. }
  196. }
  197. if tc.expectedPod == nil {
  198. if resultObj != nil {
  199. t.Errorf("%s: unexpected result: %v", tc.name, resultObj)
  200. }
  201. return
  202. }
  203. resultPod := resultObj.(*api.Pod)
  204. // roundtrip to get defaulting
  205. expectedJS, err := runtime.Encode(codec, tc.expectedPod)
  206. if err != nil {
  207. t.Errorf("%s: unexpected error: %v", tc.name, err)
  208. return
  209. }
  210. expectedObj, err := runtime.Decode(codec, expectedJS)
  211. if err != nil {
  212. t.Errorf("%s: unexpected error: %v", tc.name, err)
  213. return
  214. }
  215. reallyExpectedPod := expectedObj.(*api.Pod)
  216. if !reflect.DeepEqual(*reallyExpectedPod, *resultPod) {
  217. t.Errorf("%s mismatch: %v\n", tc.name, diff.ObjectGoPrintDiff(reallyExpectedPod, resultPod))
  218. return
  219. }
  220. }
  221. }
  222. func TestPatchResourceWithVersionConflict(t *testing.T) {
  223. namespace := "bar"
  224. name := "foo"
  225. uid := types.UID("uid")
  226. fifteen := int64(15)
  227. thirty := int64(30)
  228. tc := &patchTestCase{
  229. name: "TestPatchResourceWithVersionConflict",
  230. startingPod: &api.Pod{},
  231. changedPod: &api.Pod{},
  232. updatePod: &api.Pod{},
  233. expectedPod: &api.Pod{},
  234. }
  235. tc.startingPod.Name = name
  236. tc.startingPod.Namespace = namespace
  237. tc.startingPod.UID = uid
  238. tc.startingPod.ResourceVersion = "1"
  239. tc.startingPod.APIVersion = "v1"
  240. tc.startingPod.Spec.ActiveDeadlineSeconds = &fifteen
  241. tc.changedPod.Name = name
  242. tc.changedPod.Namespace = namespace
  243. tc.changedPod.UID = uid
  244. tc.changedPod.ResourceVersion = "1"
  245. tc.changedPod.APIVersion = "v1"
  246. tc.changedPod.Spec.ActiveDeadlineSeconds = &thirty
  247. tc.updatePod.Name = name
  248. tc.updatePod.Namespace = namespace
  249. tc.updatePod.UID = uid
  250. tc.updatePod.ResourceVersion = "2"
  251. tc.updatePod.APIVersion = "v1"
  252. tc.updatePod.Spec.ActiveDeadlineSeconds = &fifteen
  253. tc.updatePod.Spec.NodeName = "anywhere"
  254. tc.expectedPod.Name = name
  255. tc.expectedPod.Namespace = namespace
  256. tc.expectedPod.UID = uid
  257. tc.expectedPod.ResourceVersion = "2"
  258. tc.expectedPod.Spec.ActiveDeadlineSeconds = &thirty
  259. tc.expectedPod.Spec.NodeName = "anywhere"
  260. tc.Run(t)
  261. }
  262. func TestPatchResourceWithConflict(t *testing.T) {
  263. namespace := "bar"
  264. name := "foo"
  265. uid := types.UID("uid")
  266. tc := &patchTestCase{
  267. name: "TestPatchResourceWithConflict",
  268. startingPod: &api.Pod{},
  269. changedPod: &api.Pod{},
  270. updatePod: &api.Pod{},
  271. expectedError: `Operation cannot be fulfilled on pods "foo": existing 2, new 1`,
  272. }
  273. tc.startingPod.Name = name
  274. tc.startingPod.Namespace = namespace
  275. tc.startingPod.UID = uid
  276. tc.startingPod.ResourceVersion = "1"
  277. tc.startingPod.APIVersion = "v1"
  278. tc.startingPod.Spec.NodeName = "here"
  279. tc.changedPod.Name = name
  280. tc.changedPod.Namespace = namespace
  281. tc.changedPod.UID = uid
  282. tc.changedPod.ResourceVersion = "1"
  283. tc.changedPod.APIVersion = "v1"
  284. tc.changedPod.Spec.NodeName = "there"
  285. tc.updatePod.Name = name
  286. tc.updatePod.Namespace = namespace
  287. tc.updatePod.UID = uid
  288. tc.updatePod.ResourceVersion = "2"
  289. tc.updatePod.APIVersion = "v1"
  290. tc.updatePod.Spec.NodeName = "anywhere"
  291. tc.Run(t)
  292. }
  293. func TestPatchWithAdmissionRejection(t *testing.T) {
  294. namespace := "bar"
  295. name := "foo"
  296. uid := types.UID("uid")
  297. fifteen := int64(15)
  298. thirty := int64(30)
  299. tc := &patchTestCase{
  300. name: "TestPatchWithAdmissionRejection",
  301. admit: func(updatedObject runtime.Object, currentObject runtime.Object) error {
  302. return errors.New("admission failure")
  303. },
  304. startingPod: &api.Pod{},
  305. changedPod: &api.Pod{},
  306. updatePod: &api.Pod{},
  307. expectedError: "admission failure",
  308. }
  309. tc.startingPod.Name = name
  310. tc.startingPod.Namespace = namespace
  311. tc.startingPod.UID = uid
  312. tc.startingPod.ResourceVersion = "1"
  313. tc.startingPod.APIVersion = "v1"
  314. tc.startingPod.Spec.ActiveDeadlineSeconds = &fifteen
  315. tc.changedPod.Name = name
  316. tc.changedPod.Namespace = namespace
  317. tc.changedPod.UID = uid
  318. tc.changedPod.ResourceVersion = "1"
  319. tc.changedPod.APIVersion = "v1"
  320. tc.changedPod.Spec.ActiveDeadlineSeconds = &thirty
  321. tc.Run(t)
  322. }
  323. func TestPatchWithVersionConflictThenAdmissionFailure(t *testing.T) {
  324. namespace := "bar"
  325. name := "foo"
  326. uid := types.UID("uid")
  327. fifteen := int64(15)
  328. thirty := int64(30)
  329. seen := false
  330. tc := &patchTestCase{
  331. name: "TestPatchWithVersionConflictThenAdmissionFailure",
  332. admit: func(updatedObject runtime.Object, currentObject runtime.Object) error {
  333. if seen {
  334. return errors.New("admission failure")
  335. }
  336. seen = true
  337. return nil
  338. },
  339. startingPod: &api.Pod{},
  340. changedPod: &api.Pod{},
  341. updatePod: &api.Pod{},
  342. expectedError: "admission failure",
  343. }
  344. tc.startingPod.Name = name
  345. tc.startingPod.Namespace = namespace
  346. tc.startingPod.UID = uid
  347. tc.startingPod.ResourceVersion = "1"
  348. tc.startingPod.APIVersion = "v1"
  349. tc.startingPod.Spec.ActiveDeadlineSeconds = &fifteen
  350. tc.changedPod.Name = name
  351. tc.changedPod.Namespace = namespace
  352. tc.changedPod.UID = uid
  353. tc.changedPod.ResourceVersion = "1"
  354. tc.changedPod.APIVersion = "v1"
  355. tc.changedPod.Spec.ActiveDeadlineSeconds = &thirty
  356. tc.updatePod.Name = name
  357. tc.updatePod.Namespace = namespace
  358. tc.updatePod.UID = uid
  359. tc.updatePod.ResourceVersion = "2"
  360. tc.updatePod.APIVersion = "v1"
  361. tc.updatePod.Spec.ActiveDeadlineSeconds = &fifteen
  362. tc.updatePod.Spec.NodeName = "anywhere"
  363. tc.Run(t)
  364. }
  365. func TestHasUID(t *testing.T) {
  366. testcases := []struct {
  367. obj runtime.Object
  368. hasUID bool
  369. }{
  370. {obj: nil, hasUID: false},
  371. {obj: &api.Pod{}, hasUID: false},
  372. {obj: nil, hasUID: false},
  373. {obj: runtime.Object(nil), hasUID: false},
  374. {obj: &api.Pod{ObjectMeta: api.ObjectMeta{UID: types.UID("A")}}, hasUID: true},
  375. }
  376. for i, tc := range testcases {
  377. actual, err := hasUID(tc.obj)
  378. if err != nil {
  379. t.Errorf("%d: unexpected error %v", i, err)
  380. continue
  381. }
  382. if tc.hasUID != actual {
  383. t.Errorf("%d: expected %v, got %v", i, tc.hasUID, actual)
  384. }
  385. }
  386. }