123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307 |
- /*
- 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 (
- "context"
- "testing"
- v1 "k8s.io/api/core/v1"
- apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
- apierrors "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"
- "k8s.io/client-go/dynamic"
- "k8s.io/client-go/kubernetes"
- featuregatetesting "k8s.io/component-base/featuregate/testing"
- kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
- "k8s.io/kubernetes/test/integration/etcd"
- "k8s.io/kubernetes/test/integration/framework"
- )
- // 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{}); !apierrors.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 apierrors.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 apierrors.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 || !apierrors.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 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DryRun, true)()
- // start API server
- s, err := kubeapiservertesting.StartTestServer(t, kubeapiservertesting.NewDefaultTestServerOptions(), []string{
- "--disable-admission-plugins=ServiceAccount,StorageObjectInUseProtection",
- "--runtime-config=api/all=true",
- }, framework.SharedEtcd())
- if err != nil {
- t.Fatal(err)
- }
- defer s.TearDownFn()
- client, err := kubernetes.NewForConfig(s.ClientConfig)
- if err != nil {
- t.Fatal(err)
- }
- dynamicClient, err := dynamic.NewForConfig(s.ClientConfig)
- if err != nil {
- t.Fatal(err)
- }
- // create CRDs so we can make sure that custom resources do not get lost
- etcd.CreateTestCRDs(t, apiextensionsclientset.NewForConfigOrDie(s.ClientConfig), false, etcd.GetCustomResourceDefinitionData()...)
- if _, err := client.CoreV1().Namespaces().Create(context.TODO(), &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: testNamespace}}, metav1.CreateOptions{}); 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
- }
- // gather resources to test
- _, resources, err := client.Discovery().ServerGroupsAndResources()
- if err != nil {
- t.Fatalf("Failed to get ServerGroupsAndResources with error: %+v", err)
- }
- for _, resourceToTest := range etcd.GetResources(t, 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, dynamicClient)
- 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}
- }
|