...

Source file src/k8s.io/kubernetes/test/integration/dryrun/dryrun_test.go

Documentation: k8s.io/kubernetes/test/integration/dryrun

     1  /*
     2  Copyright 2018 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package dryrun
    18  
    19  import (
    20  	"context"
    21  	"testing"
    22  
    23  	v1 "k8s.io/api/core/v1"
    24  	apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
    25  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    26  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    27  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    28  	"k8s.io/apimachinery/pkg/runtime/schema"
    29  	"k8s.io/apimachinery/pkg/types"
    30  	"k8s.io/apimachinery/pkg/util/sets"
    31  	"k8s.io/client-go/dynamic"
    32  	"k8s.io/client-go/kubernetes"
    33  	"k8s.io/client-go/util/retry"
    34  	kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
    35  	"k8s.io/kubernetes/test/integration/etcd"
    36  	"k8s.io/kubernetes/test/integration/framework"
    37  )
    38  
    39  // Only add kinds to this list when this a virtual resource with get and create verbs that doesn't actually
    40  // store into it's kind.  We've used this downstream for mappings before.
    41  var kindAllowList = sets.NewString()
    42  
    43  // namespace used for all tests, do not change this
    44  const testNamespace = "dryrunnamespace"
    45  
    46  func DryRunCreateTest(t *testing.T, rsc dynamic.ResourceInterface, obj *unstructured.Unstructured, gvResource schema.GroupVersionResource) {
    47  	createdObj, err := rsc.Create(context.TODO(), obj, metav1.CreateOptions{DryRun: []string{metav1.DryRunAll}})
    48  	if err != nil {
    49  		t.Fatalf("failed to dry-run create stub for %s: %#v", gvResource, err)
    50  	}
    51  	if obj.GroupVersionKind() != createdObj.GroupVersionKind() {
    52  		t.Fatalf("created object doesn't have the same gvk as original object: got %v, expected %v",
    53  			createdObj.GroupVersionKind(),
    54  			obj.GroupVersionKind())
    55  	}
    56  
    57  	if _, err := rsc.Get(context.TODO(), obj.GetName(), metav1.GetOptions{}); !apierrors.IsNotFound(err) {
    58  		t.Fatalf("object shouldn't exist: %v", err)
    59  	}
    60  }
    61  
    62  func DryRunPatchTest(t *testing.T, rsc dynamic.ResourceInterface, name string) {
    63  	patch := []byte(`{"metadata":{"annotations":{"patch": "true"}}}`)
    64  	obj, err := rsc.Patch(context.TODO(), name, types.MergePatchType, patch, metav1.PatchOptions{DryRun: []string{metav1.DryRunAll}})
    65  	if err != nil {
    66  		t.Fatalf("failed to dry-run patch object: %v", err)
    67  	}
    68  	if v := obj.GetAnnotations()["patch"]; v != "true" {
    69  		t.Fatalf("dry-run patched annotations should be returned, got: %v", obj.GetAnnotations())
    70  	}
    71  	obj, err = rsc.Get(context.TODO(), obj.GetName(), metav1.GetOptions{})
    72  	if err != nil {
    73  		t.Fatalf("failed to get object: %v", err)
    74  	}
    75  	if v := obj.GetAnnotations()["patch"]; v == "true" {
    76  		t.Fatalf("dry-run patched annotations should not be persisted, got: %v", obj.GetAnnotations())
    77  	}
    78  }
    79  
    80  func getReplicasOrFail(t *testing.T, obj *unstructured.Unstructured) int64 {
    81  	t.Helper()
    82  	replicas, found, err := unstructured.NestedInt64(obj.UnstructuredContent(), "spec", "replicas")
    83  	if err != nil {
    84  		t.Fatalf("failed to get int64 for replicas: %v", err)
    85  	}
    86  	if !found {
    87  		t.Fatal("object doesn't have spec.replicas")
    88  	}
    89  	return replicas
    90  }
    91  
    92  func DryRunScalePatchTest(t *testing.T, rsc dynamic.ResourceInterface, name string) {
    93  	obj, err := rsc.Get(context.TODO(), name, metav1.GetOptions{}, "scale")
    94  	if apierrors.IsNotFound(err) {
    95  		return
    96  	}
    97  	if err != nil {
    98  		t.Fatalf("failed to get object: %v", err)
    99  	}
   100  
   101  	replicas := getReplicasOrFail(t, obj)
   102  	patch := []byte(`{"spec":{"replicas":10}}`)
   103  	patchedObj, err := rsc.Patch(context.TODO(), name, types.MergePatchType, patch, metav1.PatchOptions{DryRun: []string{metav1.DryRunAll}}, "scale")
   104  	if err != nil {
   105  		t.Fatalf("failed to dry-run patch object: %v", err)
   106  	}
   107  	if newReplicas := getReplicasOrFail(t, patchedObj); newReplicas != 10 {
   108  		t.Fatalf("dry-run patch to replicas didn't return new value: %v", newReplicas)
   109  	}
   110  	persistedObj, err := rsc.Get(context.TODO(), name, metav1.GetOptions{}, "scale")
   111  	if err != nil {
   112  		t.Fatalf("failed to get scale sub-resource")
   113  	}
   114  	if newReplicas := getReplicasOrFail(t, persistedObj); newReplicas != replicas {
   115  		t.Fatalf("number of replicas changed, expected %v, got %v", replicas, newReplicas)
   116  	}
   117  }
   118  
   119  func DryRunScaleUpdateTest(t *testing.T, rsc dynamic.ResourceInterface, name string) {
   120  	obj, err := rsc.Get(context.TODO(), name, metav1.GetOptions{}, "scale")
   121  	if apierrors.IsNotFound(err) {
   122  		return
   123  	}
   124  	if err != nil {
   125  		t.Fatalf("failed to get object: %v", err)
   126  	}
   127  
   128  	replicas := getReplicasOrFail(t, obj)
   129  	if err := unstructured.SetNestedField(obj.Object, int64(10), "spec", "replicas"); err != nil {
   130  		t.Fatalf("failed to set spec.replicas: %v", err)
   131  	}
   132  	updatedObj, err := rsc.Update(context.TODO(), obj, metav1.UpdateOptions{DryRun: []string{metav1.DryRunAll}}, "scale")
   133  	if err != nil {
   134  		t.Fatalf("failed to dry-run update scale sub-resource: %v", err)
   135  	}
   136  	if newReplicas := getReplicasOrFail(t, updatedObj); newReplicas != 10 {
   137  		t.Fatalf("dry-run update to replicas didn't return new value: %v", newReplicas)
   138  	}
   139  	persistedObj, err := rsc.Get(context.TODO(), name, metav1.GetOptions{}, "scale")
   140  	if err != nil {
   141  		t.Fatalf("failed to get scale sub-resource")
   142  	}
   143  	if newReplicas := getReplicasOrFail(t, persistedObj); newReplicas != replicas {
   144  		t.Fatalf("number of replicas changed, expected %v, got %v", replicas, newReplicas)
   145  	}
   146  }
   147  
   148  func DryRunUpdateTest(t *testing.T, rsc dynamic.ResourceInterface, name string) {
   149  	var err error
   150  	var obj *unstructured.Unstructured
   151  	err = retry.RetryOnConflict(retry.DefaultBackoff, func() error {
   152  		obj, err = rsc.Get(context.TODO(), name, metav1.GetOptions{})
   153  		if err != nil {
   154  			t.Fatalf("failed to retrieve object: %v", err)
   155  		}
   156  		obj.SetAnnotations(map[string]string{"update": "true"})
   157  		obj, err = rsc.Update(context.TODO(), obj, metav1.UpdateOptions{DryRun: []string{metav1.DryRunAll}})
   158  		if apierrors.IsConflict(err) {
   159  			t.Logf("conflict error: %v", err)
   160  		}
   161  		return err
   162  	})
   163  	if err != nil {
   164  		t.Fatalf("failed to dry-run update resource: %v", err)
   165  	}
   166  	if v := obj.GetAnnotations()["update"]; v != "true" {
   167  		t.Fatalf("dry-run updated annotations should be returned, got: %v", obj.GetAnnotations())
   168  	}
   169  
   170  	obj, err = rsc.Get(context.TODO(), obj.GetName(), metav1.GetOptions{})
   171  	if err != nil {
   172  		t.Fatalf("failed to get object: %v", err)
   173  	}
   174  	if v := obj.GetAnnotations()["update"]; v == "true" {
   175  		t.Fatalf("dry-run updated annotations should not be persisted, got: %v", obj.GetAnnotations())
   176  	}
   177  }
   178  
   179  func DryRunDeleteCollectionTest(t *testing.T, rsc dynamic.ResourceInterface, name string) {
   180  	err := rsc.DeleteCollection(context.TODO(), metav1.DeleteOptions{DryRun: []string{metav1.DryRunAll}}, metav1.ListOptions{})
   181  	if err != nil {
   182  		t.Fatalf("dry-run delete collection failed: %v", err)
   183  	}
   184  	obj, err := rsc.Get(context.TODO(), name, metav1.GetOptions{})
   185  	if err != nil {
   186  		t.Fatalf("failed to get object: %v", err)
   187  	}
   188  	ts := obj.GetDeletionTimestamp()
   189  	if ts != nil {
   190  		t.Fatalf("object has a deletion timestamp after dry-run delete collection")
   191  	}
   192  }
   193  
   194  func DryRunDeleteTest(t *testing.T, rsc dynamic.ResourceInterface, name string) {
   195  	err := rsc.Delete(context.TODO(), name, metav1.DeleteOptions{DryRun: []string{metav1.DryRunAll}})
   196  	if err != nil {
   197  		t.Fatalf("dry-run delete failed: %v", err)
   198  	}
   199  	obj, err := rsc.Get(context.TODO(), name, metav1.GetOptions{})
   200  	if err != nil {
   201  		t.Fatalf("failed to get object: %v", err)
   202  	}
   203  	ts := obj.GetDeletionTimestamp()
   204  	if ts != nil {
   205  		t.Fatalf("object has a deletion timestamp after dry-run delete")
   206  	}
   207  }
   208  
   209  // TestDryRun tests dry-run on all types.
   210  func TestDryRun(t *testing.T) {
   211  
   212  	// start API server
   213  	s, err := kubeapiservertesting.StartTestServer(t, kubeapiservertesting.NewDefaultTestServerOptions(), []string{
   214  		"--disable-admission-plugins=ServiceAccount,StorageObjectInUseProtection",
   215  		"--runtime-config=api/all=true",
   216  	}, framework.SharedEtcd())
   217  	if err != nil {
   218  		t.Fatal(err)
   219  	}
   220  	defer s.TearDownFn()
   221  
   222  	client, err := kubernetes.NewForConfig(s.ClientConfig)
   223  	if err != nil {
   224  		t.Fatal(err)
   225  	}
   226  	dynamicClient, err := dynamic.NewForConfig(s.ClientConfig)
   227  	if err != nil {
   228  		t.Fatal(err)
   229  	}
   230  
   231  	// create CRDs so we can make sure that custom resources do not get lost
   232  	etcd.CreateTestCRDs(t, apiextensionsclientset.NewForConfigOrDie(s.ClientConfig), false, etcd.GetCustomResourceDefinitionData()...)
   233  
   234  	if _, err := client.CoreV1().Namespaces().Create(context.TODO(), &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: testNamespace}}, metav1.CreateOptions{}); err != nil {
   235  		t.Fatal(err)
   236  	}
   237  
   238  	dryrunData := etcd.GetEtcdStorageData()
   239  
   240  	// dry run specific stub overrides
   241  	for resource, stub := range map[schema.GroupVersionResource]string{
   242  		// need to change event's namespace field to match dry run test
   243  		gvr("", "v1", "events"): `{"involvedObject": {"namespace": "dryrunnamespace"}, "message": "some data here", "metadata": {"name": "event1"}}`,
   244  	} {
   245  		data := dryrunData[resource]
   246  		data.Stub = stub
   247  		dryrunData[resource] = data
   248  	}
   249  
   250  	// gather resources to test
   251  	_, resources, err := client.Discovery().ServerGroupsAndResources()
   252  	if err != nil {
   253  		t.Fatalf("Failed to get ServerGroupsAndResources with error: %+v", err)
   254  	}
   255  
   256  	for _, resourceToTest := range etcd.GetResources(t, resources) {
   257  		t.Run(resourceToTest.Mapping.Resource.String(), func(t *testing.T) {
   258  			mapping := resourceToTest.Mapping
   259  			gvk := resourceToTest.Mapping.GroupVersionKind
   260  			gvResource := resourceToTest.Mapping.Resource
   261  			kind := gvk.Kind
   262  
   263  			if kindAllowList.Has(kind) {
   264  				t.Skip("allowlisted")
   265  			}
   266  
   267  			testData, hasTest := dryrunData[gvResource]
   268  
   269  			if !hasTest {
   270  				t.Fatalf("no test data for %s.  Please add a test for your new type to etcd.GetEtcdStorageData().", gvResource)
   271  			}
   272  
   273  			rsc, obj, err := etcd.JSONToUnstructured(testData.Stub, testNamespace, mapping, dynamicClient)
   274  			if err != nil {
   275  				t.Fatalf("failed to unmarshal stub (%v): %v", testData.Stub, err)
   276  			}
   277  
   278  			name := obj.GetName()
   279  
   280  			DryRunCreateTest(t, rsc, obj, gvResource)
   281  
   282  			if _, err := rsc.Create(context.TODO(), obj, metav1.CreateOptions{}); err != nil {
   283  				t.Fatalf("failed to create stub for %s: %#v", gvResource, err)
   284  			}
   285  
   286  			DryRunUpdateTest(t, rsc, name)
   287  			DryRunPatchTest(t, rsc, name)
   288  			DryRunScalePatchTest(t, rsc, name)
   289  			DryRunScaleUpdateTest(t, rsc, name)
   290  			if resourceToTest.HasDeleteCollection {
   291  				DryRunDeleteCollectionTest(t, rsc, name)
   292  			}
   293  			DryRunDeleteTest(t, rsc, name)
   294  
   295  			if err = rsc.Delete(context.TODO(), obj.GetName(), *metav1.NewDeleteOptions(0)); err != nil {
   296  				t.Fatalf("deleting final object failed: %v", err)
   297  			}
   298  		})
   299  	}
   300  }
   301  
   302  func gvr(g, v, r string) schema.GroupVersionResource {
   303  	return schema.GroupVersionResource{Group: g, Version: v, Resource: r}
   304  }
   305  

View as plain text