mirror of
				https://github.com/k3s-io/kubernetes.git
				synced 2025-10-30 21:30:16 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			277 lines
		
	
	
		
			9.4 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			277 lines
		
	
	
		
			9.4 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| /*
 | |
| Copyright 2018 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 dryrun
 | |
| 
 | |
| import (
 | |
| 	"testing"
 | |
| 
 | |
| 	"k8s.io/api/core/v1"
 | |
| 	"k8s.io/apimachinery/pkg/api/errors"
 | |
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 | |
| 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 | |
| 	"k8s.io/apimachinery/pkg/runtime/schema"
 | |
| 	"k8s.io/apimachinery/pkg/types"
 | |
| 	"k8s.io/apimachinery/pkg/util/sets"
 | |
| 	"k8s.io/apiserver/pkg/features"
 | |
| 	utilfeature "k8s.io/apiserver/pkg/util/feature"
 | |
| 	utilfeaturetesting "k8s.io/apiserver/pkg/util/feature/testing"
 | |
| 	"k8s.io/client-go/dynamic"
 | |
| 	"k8s.io/kubernetes/test/integration/etcd"
 | |
| )
 | |
| 
 | |
| // Only add kinds to this list when this a virtual resource with get and create verbs that doesn't actually
 | |
| // store into it's kind.  We've used this downstream for mappings before.
 | |
| var kindWhiteList = sets.NewString()
 | |
| 
 | |
| // namespace used for all tests, do not change this
 | |
| const testNamespace = "dryrunnamespace"
 | |
| 
 | |
| func DryRunCreateTest(t *testing.T, rsc dynamic.ResourceInterface, obj *unstructured.Unstructured, gvResource schema.GroupVersionResource) {
 | |
| 	createdObj, err := rsc.Create(obj, metav1.CreateOptions{DryRun: []string{metav1.DryRunAll}})
 | |
| 	if err != nil {
 | |
| 		t.Fatalf("failed to dry-run create stub for %s: %#v", gvResource, err)
 | |
| 	}
 | |
| 	if obj.GroupVersionKind() != createdObj.GroupVersionKind() {
 | |
| 		t.Fatalf("created object doesn't have the same gvk as original object: got %v, expected %v",
 | |
| 			createdObj.GroupVersionKind(),
 | |
| 			obj.GroupVersionKind())
 | |
| 	}
 | |
| 
 | |
| 	if _, err := rsc.Get(obj.GetName(), metav1.GetOptions{}); !errors.IsNotFound(err) {
 | |
| 		t.Fatalf("object shouldn't exist: %v", err)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func DryRunPatchTest(t *testing.T, rsc dynamic.ResourceInterface, name string) {
 | |
| 	patch := []byte(`{"metadata":{"annotations":{"patch": "true"}}}`)
 | |
| 	obj, err := rsc.Patch(name, types.MergePatchType, patch, metav1.PatchOptions{DryRun: []string{metav1.DryRunAll}})
 | |
| 	if err != nil {
 | |
| 		t.Fatalf("failed to dry-run patch object: %v", err)
 | |
| 	}
 | |
| 	if v := obj.GetAnnotations()["patch"]; v != "true" {
 | |
| 		t.Fatalf("dry-run patched annotations should be returned, got: %v", obj.GetAnnotations())
 | |
| 	}
 | |
| 	obj, err = rsc.Get(obj.GetName(), metav1.GetOptions{})
 | |
| 	if err != nil {
 | |
| 		t.Fatalf("failed to get object: %v", err)
 | |
| 	}
 | |
| 	if v := obj.GetAnnotations()["patch"]; v == "true" {
 | |
| 		t.Fatalf("dry-run patched annotations should not be persisted, got: %v", obj.GetAnnotations())
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func getReplicasOrFail(t *testing.T, obj *unstructured.Unstructured) int64 {
 | |
| 	t.Helper()
 | |
| 	replicas, found, err := unstructured.NestedInt64(obj.UnstructuredContent(), "spec", "replicas")
 | |
| 	if err != nil {
 | |
| 		t.Fatalf("failed to get int64 for replicas: %v", err)
 | |
| 	}
 | |
| 	if !found {
 | |
| 		t.Fatal("object doesn't have spec.replicas")
 | |
| 	}
 | |
| 	return replicas
 | |
| }
 | |
| 
 | |
| func DryRunScalePatchTest(t *testing.T, rsc dynamic.ResourceInterface, name string) {
 | |
| 	obj, err := rsc.Get(name, metav1.GetOptions{}, "scale")
 | |
| 	if errors.IsNotFound(err) {
 | |
| 		return
 | |
| 	}
 | |
| 	if err != nil {
 | |
| 		t.Fatalf("failed to get object: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	replicas := getReplicasOrFail(t, obj)
 | |
| 	patch := []byte(`{"spec":{"replicas":10}}`)
 | |
| 	patchedObj, err := rsc.Patch(name, types.MergePatchType, patch, metav1.PatchOptions{DryRun: []string{metav1.DryRunAll}}, "scale")
 | |
| 	if err != nil {
 | |
| 		t.Fatalf("failed to dry-run patch object: %v", err)
 | |
| 	}
 | |
| 	if newReplicas := getReplicasOrFail(t, patchedObj); newReplicas != 10 {
 | |
| 		t.Fatalf("dry-run patch to replicas didn't return new value: %v", newReplicas)
 | |
| 	}
 | |
| 	persistedObj, err := rsc.Get(name, metav1.GetOptions{}, "scale")
 | |
| 	if err != nil {
 | |
| 		t.Fatalf("failed to get scale sub-resource")
 | |
| 	}
 | |
| 	if newReplicas := getReplicasOrFail(t, persistedObj); newReplicas != replicas {
 | |
| 		t.Fatalf("number of replicas changed, expected %v, got %v", replicas, newReplicas)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func DryRunScaleUpdateTest(t *testing.T, rsc dynamic.ResourceInterface, name string) {
 | |
| 	obj, err := rsc.Get(name, metav1.GetOptions{}, "scale")
 | |
| 	if errors.IsNotFound(err) {
 | |
| 		return
 | |
| 	}
 | |
| 	if err != nil {
 | |
| 		t.Fatalf("failed to get object: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	replicas := getReplicasOrFail(t, obj)
 | |
| 	if err := unstructured.SetNestedField(obj.Object, int64(10), "spec", "replicas"); err != nil {
 | |
| 		t.Fatalf("failed to set spec.replicas: %v", err)
 | |
| 	}
 | |
| 	updatedObj, err := rsc.Update(obj, metav1.UpdateOptions{DryRun: []string{metav1.DryRunAll}}, "scale")
 | |
| 	if err != nil {
 | |
| 		t.Fatalf("failed to dry-run update scale sub-resource: %v", err)
 | |
| 	}
 | |
| 	if newReplicas := getReplicasOrFail(t, updatedObj); newReplicas != 10 {
 | |
| 		t.Fatalf("dry-run update to replicas didn't return new value: %v", newReplicas)
 | |
| 	}
 | |
| 	persistedObj, err := rsc.Get(name, metav1.GetOptions{}, "scale")
 | |
| 	if err != nil {
 | |
| 		t.Fatalf("failed to get scale sub-resource")
 | |
| 	}
 | |
| 	if newReplicas := getReplicasOrFail(t, persistedObj); newReplicas != replicas {
 | |
| 		t.Fatalf("number of replicas changed, expected %v, got %v", replicas, newReplicas)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func DryRunUpdateTest(t *testing.T, rsc dynamic.ResourceInterface, name string) {
 | |
| 	var err error
 | |
| 	var obj *unstructured.Unstructured
 | |
| 	for i := 0; i < 3; i++ {
 | |
| 		obj, err = rsc.Get(name, metav1.GetOptions{})
 | |
| 		if err != nil {
 | |
| 			t.Fatalf("failed to retrieve object: %v", err)
 | |
| 		}
 | |
| 		obj.SetAnnotations(map[string]string{"update": "true"})
 | |
| 		obj, err = rsc.Update(obj, metav1.UpdateOptions{DryRun: []string{metav1.DryRunAll}})
 | |
| 		if err == nil || !errors.IsConflict(err) {
 | |
| 			break
 | |
| 		}
 | |
| 	}
 | |
| 	if err != nil {
 | |
| 		t.Fatalf("failed to dry-run update resource: %v", err)
 | |
| 	}
 | |
| 	if v := obj.GetAnnotations()["update"]; v != "true" {
 | |
| 		t.Fatalf("dry-run updated annotations should be returned, got: %v", obj.GetAnnotations())
 | |
| 	}
 | |
| 
 | |
| 	obj, err = rsc.Get(obj.GetName(), metav1.GetOptions{})
 | |
| 	if err != nil {
 | |
| 		t.Fatalf("failed to get object: %v", err)
 | |
| 	}
 | |
| 	if v := obj.GetAnnotations()["update"]; v == "true" {
 | |
| 		t.Fatalf("dry-run updated annotations should not be persisted, got: %v", obj.GetAnnotations())
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func DryRunDeleteCollectionTest(t *testing.T, rsc dynamic.ResourceInterface, name string) {
 | |
| 	err := rsc.DeleteCollection(&metav1.DeleteOptions{DryRun: []string{metav1.DryRunAll}}, metav1.ListOptions{})
 | |
| 	if err != nil {
 | |
| 		t.Fatalf("dry-run delete collection failed: %v", err)
 | |
| 	}
 | |
| 	obj, err := rsc.Get(name, metav1.GetOptions{})
 | |
| 	if err != nil {
 | |
| 		t.Fatalf("failed to get object: %v", err)
 | |
| 	}
 | |
| 	ts := obj.GetDeletionTimestamp()
 | |
| 	if ts != nil {
 | |
| 		t.Fatalf("object has a deletion timestamp after dry-run delete collection")
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func DryRunDeleteTest(t *testing.T, rsc dynamic.ResourceInterface, name string) {
 | |
| 	err := rsc.Delete(name, &metav1.DeleteOptions{DryRun: []string{metav1.DryRunAll}})
 | |
| 	if err != nil {
 | |
| 		t.Fatalf("dry-run delete failed: %v", err)
 | |
| 	}
 | |
| 	obj, err := rsc.Get(name, metav1.GetOptions{})
 | |
| 	if err != nil {
 | |
| 		t.Fatalf("failed to get object: %v", err)
 | |
| 	}
 | |
| 	ts := obj.GetDeletionTimestamp()
 | |
| 	if ts != nil {
 | |
| 		t.Fatalf("object has a deletion timestamp after dry-run delete")
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // TestDryRun tests dry-run on all types.
 | |
| func TestDryRun(t *testing.T) {
 | |
| 	defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DryRun, true)()
 | |
| 
 | |
| 	master := etcd.StartRealMasterOrDie(t)
 | |
| 	defer master.Cleanup()
 | |
| 
 | |
| 	if _, err := master.Client.CoreV1().Namespaces().Create(&v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: testNamespace}}); err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 
 | |
| 	dryrunData := etcd.GetEtcdStorageData()
 | |
| 
 | |
| 	// dry run specific stub overrides
 | |
| 	for resource, stub := range map[schema.GroupVersionResource]string{
 | |
| 		// need to change event's namespace field to match dry run test
 | |
| 		gvr("", "v1", "events"): `{"involvedObject": {"namespace": "dryrunnamespace"}, "message": "some data here", "metadata": {"name": "event1"}}`,
 | |
| 	} {
 | |
| 		data := dryrunData[resource]
 | |
| 		data.Stub = stub
 | |
| 		dryrunData[resource] = data
 | |
| 	}
 | |
| 
 | |
| 	for _, resourceToTest := range master.Resources {
 | |
| 		t.Run(resourceToTest.Mapping.Resource.String(), func(t *testing.T) {
 | |
| 			mapping := resourceToTest.Mapping
 | |
| 			gvk := resourceToTest.Mapping.GroupVersionKind
 | |
| 			gvResource := resourceToTest.Mapping.Resource
 | |
| 			kind := gvk.Kind
 | |
| 
 | |
| 			if kindWhiteList.Has(kind) {
 | |
| 				t.Skip("whitelisted")
 | |
| 			}
 | |
| 
 | |
| 			testData, hasTest := dryrunData[gvResource]
 | |
| 
 | |
| 			if !hasTest {
 | |
| 				t.Fatalf("no test data for %s.  Please add a test for your new type to etcd.GetEtcdStorageData().", gvResource)
 | |
| 			}
 | |
| 
 | |
| 			rsc, obj, err := etcd.JSONToUnstructured(testData.Stub, testNamespace, mapping, master.Dynamic)
 | |
| 			if err != nil {
 | |
| 				t.Fatalf("failed to unmarshal stub (%v): %v", testData.Stub, err)
 | |
| 			}
 | |
| 
 | |
| 			name := obj.GetName()
 | |
| 
 | |
| 			DryRunCreateTest(t, rsc, obj, gvResource)
 | |
| 
 | |
| 			if _, err := rsc.Create(obj, metav1.CreateOptions{}); err != nil {
 | |
| 				t.Fatalf("failed to create stub for %s: %#v", gvResource, err)
 | |
| 			}
 | |
| 
 | |
| 			DryRunUpdateTest(t, rsc, name)
 | |
| 			DryRunPatchTest(t, rsc, name)
 | |
| 			DryRunScalePatchTest(t, rsc, name)
 | |
| 			DryRunScaleUpdateTest(t, rsc, name)
 | |
| 			if resourceToTest.HasDeleteCollection {
 | |
| 				DryRunDeleteCollectionTest(t, rsc, name)
 | |
| 			}
 | |
| 			DryRunDeleteTest(t, rsc, name)
 | |
| 
 | |
| 			if err = rsc.Delete(obj.GetName(), metav1.NewDeleteOptions(0)); err != nil {
 | |
| 				t.Fatalf("deleting final object failed: %v", err)
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func gvr(g, v, r string) schema.GroupVersionResource {
 | |
| 	return schema.GroupVersionResource{Group: g, Version: v, Resource: r}
 | |
| }
 |