123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473 |
- /*
- Copyright 2014 The Kubernetes Authors.
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- */
- package apiserver
- import (
- "errors"
- "fmt"
- "reflect"
- "testing"
- "time"
- "github.com/emicklei/go-restful"
- "github.com/evanphx/json-patch"
- "k8s.io/kubernetes/pkg/api"
- apierrors "k8s.io/kubernetes/pkg/api/errors"
- "k8s.io/kubernetes/pkg/api/rest"
- "k8s.io/kubernetes/pkg/api/testapi"
- "k8s.io/kubernetes/pkg/api/unversioned"
- "k8s.io/kubernetes/pkg/api/v1"
- "k8s.io/kubernetes/pkg/runtime"
- "k8s.io/kubernetes/pkg/types"
- "k8s.io/kubernetes/pkg/util/diff"
- "k8s.io/kubernetes/pkg/util/strategicpatch"
- )
- type testPatchType struct {
- unversioned.TypeMeta `json:",inline"`
- TestPatchSubType `json:",inline"`
- }
- // We explicitly make it public as private types doesn't
- // work correctly with json inlined types.
- type TestPatchSubType struct {
- StringField string `json:"theField"`
- }
- func (obj *testPatchType) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta }
- func TestPatchAnonymousField(t *testing.T) {
- originalJS := `{"kind":"testPatchType","theField":"my-value"}`
- patch := `{"theField": "changed!"}`
- expectedJS := `{"kind":"testPatchType","theField":"changed!"}`
- actualBytes, err := getPatchedJS(api.StrategicMergePatchType, []byte(originalJS), []byte(patch), &testPatchType{})
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if string(actualBytes) != expectedJS {
- t.Errorf("expected %v, got %v", expectedJS, string(actualBytes))
- }
- }
- type testPatcher struct {
- t *testing.T
- // startingPod is used for the first Update
- startingPod *api.Pod
- // updatePod is the pod that is used for conflict comparison and used for subsequent Update calls
- updatePod *api.Pod
- numUpdates int
- }
- func (p *testPatcher) New() runtime.Object {
- return &api.Pod{}
- }
- func (p *testPatcher) Update(ctx api.Context, name string, objInfo rest.UpdatedObjectInfo) (runtime.Object, bool, error) {
- currentPod := p.startingPod
- if p.numUpdates > 0 {
- currentPod = p.updatePod
- }
- p.numUpdates++
- obj, err := objInfo.UpdatedObject(ctx, currentPod)
- if err != nil {
- return nil, false, err
- }
- inPod := obj.(*api.Pod)
- if inPod.ResourceVersion != p.updatePod.ResourceVersion {
- return nil, false, apierrors.NewConflict(api.Resource("pods"), inPod.Name, fmt.Errorf("existing %v, new %v", p.updatePod.ResourceVersion, inPod.ResourceVersion))
- }
- return inPod, false, nil
- }
- func (p *testPatcher) Get(ctx api.Context, name string) (runtime.Object, error) {
- p.t.Fatal("Unexpected call to testPatcher.Get")
- return nil, errors.New("Unexpected call to testPatcher.Get")
- }
- type testNamer struct {
- namespace string
- name string
- }
- func (p *testNamer) Namespace(req *restful.Request) (namespace string, err error) {
- return p.namespace, nil
- }
- // Name returns the name from the request, and an optional namespace value if this is a namespace
- // scoped call. An error is returned if the name is not available.
- func (p *testNamer) Name(req *restful.Request) (namespace, name string, err error) {
- return p.namespace, p.name, nil
- }
- // ObjectName returns the namespace and name from an object if they exist, or an error if the object
- // does not support names.
- func (p *testNamer) ObjectName(obj runtime.Object) (namespace, name string, err error) {
- return p.namespace, p.name, nil
- }
- // SetSelfLink sets the provided URL onto the object. The method should return nil if the object
- // does not support selfLinks.
- func (p *testNamer) SetSelfLink(obj runtime.Object, url string) error {
- return errors.New("not implemented")
- }
- // GenerateLink creates a path and query for a given runtime object that represents the canonical path.
- func (p *testNamer) GenerateLink(req *restful.Request, obj runtime.Object) (path, query string, err error) {
- return "", "", errors.New("not implemented")
- }
- // GenerateLink creates a path and query for a list that represents the canonical path.
- func (p *testNamer) GenerateListLink(req *restful.Request) (path, query string, err error) {
- return "", "", errors.New("not implemented")
- }
- type patchTestCase struct {
- name string
- // admission chain to use, nil is fine
- admit updateAdmissionFunc
- // startingPod is used as the starting point for the first Update
- startingPod *api.Pod
- // changedPod is the "destination" pod for the patch. The test will create a patch from the startingPod to the changedPod
- // to use when calling the patch operation
- changedPod *api.Pod
- // updatePod is the pod that is used for conflict comparison and as the starting point for the second Update
- updatePod *api.Pod
- // expectedPod is the pod that you expect to get back after the patch is complete
- expectedPod *api.Pod
- expectedError string
- }
- func (tc *patchTestCase) Run(t *testing.T) {
- t.Logf("Starting test %s", tc.name)
- namespace := tc.startingPod.Namespace
- name := tc.startingPod.Name
- codec := testapi.Default.Codec()
- admit := tc.admit
- if admit == nil {
- admit = func(updatedObject runtime.Object, currentObject runtime.Object) error {
- return nil
- }
- }
- testPatcher := &testPatcher{}
- testPatcher.t = t
- testPatcher.startingPod = tc.startingPod
- testPatcher.updatePod = tc.updatePod
- ctx := api.NewDefaultContext()
- ctx = api.WithNamespace(ctx, namespace)
- namer := &testNamer{namespace, name}
- copier := runtime.ObjectCopier(api.Scheme)
- resource := unversioned.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}
- versionedObj := &v1.Pod{}
- for _, patchType := range []api.PatchType{api.JSONPatchType, api.MergePatchType, api.StrategicMergePatchType} {
- // TODO SUPPORT THIS!
- if patchType == api.JSONPatchType {
- continue
- }
- t.Logf("Working with patchType %v", patchType)
- originalObjJS, err := runtime.Encode(codec, tc.startingPod)
- if err != nil {
- t.Errorf("%s: unexpected error: %v", tc.name, err)
- return
- }
- changedJS, err := runtime.Encode(codec, tc.changedPod)
- if err != nil {
- t.Errorf("%s: unexpected error: %v", tc.name, err)
- return
- }
- patch := []byte{}
- switch patchType {
- case api.JSONPatchType:
- continue
- case api.StrategicMergePatchType:
- patch, err = strategicpatch.CreateStrategicMergePatch(originalObjJS, changedJS, versionedObj)
- if err != nil {
- t.Errorf("%s: unexpected error: %v", tc.name, err)
- return
- }
- case api.MergePatchType:
- patch, err = jsonpatch.CreateMergePatch(originalObjJS, changedJS)
- if err != nil {
- t.Errorf("%s: unexpected error: %v", tc.name, err)
- return
- }
- }
- resultObj, err := patchResource(ctx, admit, 1*time.Second, versionedObj, testPatcher, name, patchType, patch, namer, copier, resource, codec)
- if len(tc.expectedError) != 0 {
- if err == nil || err.Error() != tc.expectedError {
- t.Errorf("%s: expected error %v, but got %v", tc.name, tc.expectedError, err)
- return
- }
- } else {
- if err != nil {
- t.Errorf("%s: unexpected error: %v", tc.name, err)
- return
- }
- }
- if tc.expectedPod == nil {
- if resultObj != nil {
- t.Errorf("%s: unexpected result: %v", tc.name, resultObj)
- }
- return
- }
- resultPod := resultObj.(*api.Pod)
- // roundtrip to get defaulting
- expectedJS, err := runtime.Encode(codec, tc.expectedPod)
- if err != nil {
- t.Errorf("%s: unexpected error: %v", tc.name, err)
- return
- }
- expectedObj, err := runtime.Decode(codec, expectedJS)
- if err != nil {
- t.Errorf("%s: unexpected error: %v", tc.name, err)
- return
- }
- reallyExpectedPod := expectedObj.(*api.Pod)
- if !reflect.DeepEqual(*reallyExpectedPod, *resultPod) {
- t.Errorf("%s mismatch: %v\n", tc.name, diff.ObjectGoPrintDiff(reallyExpectedPod, resultPod))
- return
- }
- }
- }
- func TestPatchResourceWithVersionConflict(t *testing.T) {
- namespace := "bar"
- name := "foo"
- uid := types.UID("uid")
- fifteen := int64(15)
- thirty := int64(30)
- tc := &patchTestCase{
- name: "TestPatchResourceWithVersionConflict",
- startingPod: &api.Pod{},
- changedPod: &api.Pod{},
- updatePod: &api.Pod{},
- expectedPod: &api.Pod{},
- }
- tc.startingPod.Name = name
- tc.startingPod.Namespace = namespace
- tc.startingPod.UID = uid
- tc.startingPod.ResourceVersion = "1"
- tc.startingPod.APIVersion = "v1"
- tc.startingPod.Spec.ActiveDeadlineSeconds = &fifteen
- tc.changedPod.Name = name
- tc.changedPod.Namespace = namespace
- tc.changedPod.UID = uid
- tc.changedPod.ResourceVersion = "1"
- tc.changedPod.APIVersion = "v1"
- tc.changedPod.Spec.ActiveDeadlineSeconds = &thirty
- tc.updatePod.Name = name
- tc.updatePod.Namespace = namespace
- tc.updatePod.UID = uid
- tc.updatePod.ResourceVersion = "2"
- tc.updatePod.APIVersion = "v1"
- tc.updatePod.Spec.ActiveDeadlineSeconds = &fifteen
- tc.updatePod.Spec.NodeName = "anywhere"
- tc.expectedPod.Name = name
- tc.expectedPod.Namespace = namespace
- tc.expectedPod.UID = uid
- tc.expectedPod.ResourceVersion = "2"
- tc.expectedPod.Spec.ActiveDeadlineSeconds = &thirty
- tc.expectedPod.Spec.NodeName = "anywhere"
- tc.Run(t)
- }
- func TestPatchResourceWithConflict(t *testing.T) {
- namespace := "bar"
- name := "foo"
- uid := types.UID("uid")
- tc := &patchTestCase{
- name: "TestPatchResourceWithConflict",
- startingPod: &api.Pod{},
- changedPod: &api.Pod{},
- updatePod: &api.Pod{},
- expectedError: `Operation cannot be fulfilled on pods "foo": existing 2, new 1`,
- }
- tc.startingPod.Name = name
- tc.startingPod.Namespace = namespace
- tc.startingPod.UID = uid
- tc.startingPod.ResourceVersion = "1"
- tc.startingPod.APIVersion = "v1"
- tc.startingPod.Spec.NodeName = "here"
- tc.changedPod.Name = name
- tc.changedPod.Namespace = namespace
- tc.changedPod.UID = uid
- tc.changedPod.ResourceVersion = "1"
- tc.changedPod.APIVersion = "v1"
- tc.changedPod.Spec.NodeName = "there"
- tc.updatePod.Name = name
- tc.updatePod.Namespace = namespace
- tc.updatePod.UID = uid
- tc.updatePod.ResourceVersion = "2"
- tc.updatePod.APIVersion = "v1"
- tc.updatePod.Spec.NodeName = "anywhere"
- tc.Run(t)
- }
- func TestPatchWithAdmissionRejection(t *testing.T) {
- namespace := "bar"
- name := "foo"
- uid := types.UID("uid")
- fifteen := int64(15)
- thirty := int64(30)
- tc := &patchTestCase{
- name: "TestPatchWithAdmissionRejection",
- admit: func(updatedObject runtime.Object, currentObject runtime.Object) error {
- return errors.New("admission failure")
- },
- startingPod: &api.Pod{},
- changedPod: &api.Pod{},
- updatePod: &api.Pod{},
- expectedError: "admission failure",
- }
- tc.startingPod.Name = name
- tc.startingPod.Namespace = namespace
- tc.startingPod.UID = uid
- tc.startingPod.ResourceVersion = "1"
- tc.startingPod.APIVersion = "v1"
- tc.startingPod.Spec.ActiveDeadlineSeconds = &fifteen
- tc.changedPod.Name = name
- tc.changedPod.Namespace = namespace
- tc.changedPod.UID = uid
- tc.changedPod.ResourceVersion = "1"
- tc.changedPod.APIVersion = "v1"
- tc.changedPod.Spec.ActiveDeadlineSeconds = &thirty
- tc.Run(t)
- }
- func TestPatchWithVersionConflictThenAdmissionFailure(t *testing.T) {
- namespace := "bar"
- name := "foo"
- uid := types.UID("uid")
- fifteen := int64(15)
- thirty := int64(30)
- seen := false
- tc := &patchTestCase{
- name: "TestPatchWithVersionConflictThenAdmissionFailure",
- admit: func(updatedObject runtime.Object, currentObject runtime.Object) error {
- if seen {
- return errors.New("admission failure")
- }
- seen = true
- return nil
- },
- startingPod: &api.Pod{},
- changedPod: &api.Pod{},
- updatePod: &api.Pod{},
- expectedError: "admission failure",
- }
- tc.startingPod.Name = name
- tc.startingPod.Namespace = namespace
- tc.startingPod.UID = uid
- tc.startingPod.ResourceVersion = "1"
- tc.startingPod.APIVersion = "v1"
- tc.startingPod.Spec.ActiveDeadlineSeconds = &fifteen
- tc.changedPod.Name = name
- tc.changedPod.Namespace = namespace
- tc.changedPod.UID = uid
- tc.changedPod.ResourceVersion = "1"
- tc.changedPod.APIVersion = "v1"
- tc.changedPod.Spec.ActiveDeadlineSeconds = &thirty
- tc.updatePod.Name = name
- tc.updatePod.Namespace = namespace
- tc.updatePod.UID = uid
- tc.updatePod.ResourceVersion = "2"
- tc.updatePod.APIVersion = "v1"
- tc.updatePod.Spec.ActiveDeadlineSeconds = &fifteen
- tc.updatePod.Spec.NodeName = "anywhere"
- tc.Run(t)
- }
- func TestHasUID(t *testing.T) {
- testcases := []struct {
- obj runtime.Object
- hasUID bool
- }{
- {obj: nil, hasUID: false},
- {obj: &api.Pod{}, hasUID: false},
- {obj: nil, hasUID: false},
- {obj: runtime.Object(nil), hasUID: false},
- {obj: &api.Pod{ObjectMeta: api.ObjectMeta{UID: types.UID("A")}}, hasUID: true},
- }
- for i, tc := range testcases {
- actual, err := hasUID(tc.obj)
- if err != nil {
- t.Errorf("%d: unexpected error %v", i, err)
- continue
- }
- if tc.hasUID != actual {
- t.Errorf("%d: expected %v, got %v", i, tc.hasUID, actual)
- }
- }
- }
|