...

Source file src/k8s.io/kubernetes/pkg/controller/namespace/deletion/namespaced_resources_deleter_test.go

Documentation: k8s.io/kubernetes/pkg/controller/namespace/deletion

     1  /*
     2  Copyright 2015 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 deletion
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"net/http"
    23  	"net/http/httptest"
    24  	"path"
    25  	"strings"
    26  	"sync"
    27  	"testing"
    28  
    29  	v1 "k8s.io/api/core/v1"
    30  	"k8s.io/apimachinery/pkg/api/errors"
    31  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    32  	"k8s.io/apimachinery/pkg/runtime"
    33  	"k8s.io/apimachinery/pkg/runtime/schema"
    34  	"k8s.io/apimachinery/pkg/util/sets"
    35  	"k8s.io/client-go/discovery"
    36  	"k8s.io/client-go/dynamic"
    37  	"k8s.io/client-go/kubernetes/fake"
    38  	"k8s.io/client-go/metadata"
    39  	metadatafake "k8s.io/client-go/metadata/fake"
    40  	restclient "k8s.io/client-go/rest"
    41  	core "k8s.io/client-go/testing"
    42  	"k8s.io/klog/v2/ktesting"
    43  	api "k8s.io/kubernetes/pkg/apis/core"
    44  )
    45  
    46  func TestFinalized(t *testing.T) {
    47  	testNamespace := &v1.Namespace{
    48  		Spec: v1.NamespaceSpec{
    49  			Finalizers: []v1.FinalizerName{"a", "b"},
    50  		},
    51  	}
    52  	if finalized(testNamespace) {
    53  		t.Errorf("Unexpected result, namespace is not finalized")
    54  	}
    55  	testNamespace.Spec.Finalizers = []v1.FinalizerName{}
    56  	if !finalized(testNamespace) {
    57  		t.Errorf("Expected object to be finalized")
    58  	}
    59  }
    60  
    61  func TestFinalizeNamespaceFunc(t *testing.T) {
    62  	mockClient := &fake.Clientset{}
    63  	testNamespace := &v1.Namespace{
    64  		ObjectMeta: metav1.ObjectMeta{
    65  			Name:            "test",
    66  			ResourceVersion: "1",
    67  		},
    68  		Spec: v1.NamespaceSpec{
    69  			Finalizers: []v1.FinalizerName{"kubernetes", "other"},
    70  		},
    71  	}
    72  	d := namespacedResourcesDeleter{
    73  		nsClient:       mockClient.CoreV1().Namespaces(),
    74  		finalizerToken: v1.FinalizerKubernetes,
    75  	}
    76  	d.finalizeNamespace(context.Background(), testNamespace)
    77  	actions := mockClient.Actions()
    78  	if len(actions) != 1 {
    79  		t.Errorf("Expected 1 mock client action, but got %v", len(actions))
    80  	}
    81  	if !actions[0].Matches("create", "namespaces") || actions[0].GetSubresource() != "finalize" {
    82  		t.Errorf("Expected finalize-namespace action %v", actions[0])
    83  	}
    84  	finalizers := actions[0].(core.CreateAction).GetObject().(*v1.Namespace).Spec.Finalizers
    85  	if len(finalizers) != 1 {
    86  		t.Errorf("There should be a single finalizer remaining")
    87  	}
    88  	if string(finalizers[0]) != "other" {
    89  		t.Errorf("Unexpected finalizer value, %v", finalizers[0])
    90  	}
    91  }
    92  
    93  func testSyncNamespaceThatIsTerminating(t *testing.T, versions *metav1.APIVersions) {
    94  	now := metav1.Now()
    95  	namespaceName := "test"
    96  	testNamespacePendingFinalize := &v1.Namespace{
    97  		ObjectMeta: metav1.ObjectMeta{
    98  			Name:              namespaceName,
    99  			ResourceVersion:   "1",
   100  			DeletionTimestamp: &now,
   101  		},
   102  		Spec: v1.NamespaceSpec{
   103  			Finalizers: []v1.FinalizerName{"kubernetes"},
   104  		},
   105  		Status: v1.NamespaceStatus{
   106  			Phase: v1.NamespaceTerminating,
   107  		},
   108  	}
   109  	testNamespaceFinalizeComplete := &v1.Namespace{
   110  		ObjectMeta: metav1.ObjectMeta{
   111  			Name:              namespaceName,
   112  			ResourceVersion:   "1",
   113  			DeletionTimestamp: &now,
   114  		},
   115  		Spec: v1.NamespaceSpec{},
   116  		Status: v1.NamespaceStatus{
   117  			Phase: v1.NamespaceTerminating,
   118  		},
   119  	}
   120  
   121  	// when doing a delete all of content, we will do a GET of a collection, and DELETE of a collection by default
   122  	metadataClientActionSet := sets.NewString()
   123  	resources := testResources()
   124  	groupVersionResources, _ := discovery.GroupVersionResources(resources)
   125  	for groupVersionResource := range groupVersionResources {
   126  		urlPath := path.Join([]string{
   127  			dynamic.LegacyAPIPathResolverFunc(schema.GroupVersionKind{Group: groupVersionResource.Group, Version: groupVersionResource.Version}),
   128  			groupVersionResource.Group,
   129  			groupVersionResource.Version,
   130  			"namespaces",
   131  			namespaceName,
   132  			groupVersionResource.Resource,
   133  		}...)
   134  		metadataClientActionSet.Insert((&fakeAction{method: "GET", path: urlPath}).String())
   135  		metadataClientActionSet.Insert((&fakeAction{method: "DELETE", path: urlPath}).String())
   136  	}
   137  
   138  	scenarios := map[string]struct {
   139  		testNamespace           *v1.Namespace
   140  		kubeClientActionSet     sets.String
   141  		metadataClientActionSet sets.String
   142  		gvrError                error
   143  		expectErrorOnDelete     error
   144  		expectStatus            *v1.NamespaceStatus
   145  	}{
   146  		"pending-finalize": {
   147  			testNamespace: testNamespacePendingFinalize,
   148  			kubeClientActionSet: sets.NewString(
   149  				strings.Join([]string{"get", "namespaces", ""}, "-"),
   150  				strings.Join([]string{"create", "namespaces", "finalize"}, "-"),
   151  				strings.Join([]string{"list", "pods", ""}, "-"),
   152  				strings.Join([]string{"update", "namespaces", "status"}, "-"),
   153  			),
   154  			metadataClientActionSet: metadataClientActionSet,
   155  		},
   156  		"complete-finalize": {
   157  			testNamespace: testNamespaceFinalizeComplete,
   158  			kubeClientActionSet: sets.NewString(
   159  				strings.Join([]string{"get", "namespaces", ""}, "-"),
   160  			),
   161  			metadataClientActionSet: sets.NewString(),
   162  		},
   163  		"groupVersionResourceErr": {
   164  			testNamespace: testNamespaceFinalizeComplete,
   165  			kubeClientActionSet: sets.NewString(
   166  				strings.Join([]string{"get", "namespaces", ""}, "-"),
   167  			),
   168  			metadataClientActionSet: sets.NewString(),
   169  			gvrError:                fmt.Errorf("test error"),
   170  		},
   171  		"groupVersionResourceErr-finalize": {
   172  			testNamespace: testNamespacePendingFinalize,
   173  			kubeClientActionSet: sets.NewString(
   174  				strings.Join([]string{"get", "namespaces", ""}, "-"),
   175  				strings.Join([]string{"list", "pods", ""}, "-"),
   176  				strings.Join([]string{"update", "namespaces", "status"}, "-"),
   177  			),
   178  			metadataClientActionSet: metadataClientActionSet,
   179  			gvrError:                fmt.Errorf("test error"),
   180  			expectErrorOnDelete:     fmt.Errorf("test error"),
   181  			expectStatus: &v1.NamespaceStatus{
   182  				Phase: v1.NamespaceTerminating,
   183  				Conditions: []v1.NamespaceCondition{
   184  					{Type: v1.NamespaceDeletionDiscoveryFailure},
   185  				},
   186  			},
   187  		},
   188  	}
   189  
   190  	for scenario, testInput := range scenarios {
   191  		t.Run(scenario, func(t *testing.T) {
   192  			testHandler := &fakeActionHandler{statusCode: 200}
   193  			srv, clientConfig := testServerAndClientConfig(testHandler.ServeHTTP)
   194  			defer srv.Close()
   195  
   196  			mockClient := fake.NewSimpleClientset(testInput.testNamespace)
   197  			metadataClient, err := metadata.NewForConfig(clientConfig)
   198  			if err != nil {
   199  				t.Fatal(err)
   200  			}
   201  
   202  			fn := func() ([]*metav1.APIResourceList, error) {
   203  				return resources, testInput.gvrError
   204  			}
   205  			_, ctx := ktesting.NewTestContext(t)
   206  			d := NewNamespacedResourcesDeleter(ctx, mockClient.CoreV1().Namespaces(), metadataClient, mockClient.CoreV1(), fn, v1.FinalizerKubernetes)
   207  			if err := d.Delete(ctx, testInput.testNamespace.Name); !matchErrors(err, testInput.expectErrorOnDelete) {
   208  				t.Errorf("expected error %q when syncing namespace, got %q, %v", testInput.expectErrorOnDelete, err, testInput.expectErrorOnDelete == err)
   209  			}
   210  
   211  			// validate traffic from kube client
   212  			actionSet := sets.NewString()
   213  			for _, action := range mockClient.Actions() {
   214  				actionSet.Insert(strings.Join([]string{action.GetVerb(), action.GetResource().Resource, action.GetSubresource()}, "-"))
   215  			}
   216  			if !actionSet.Equal(testInput.kubeClientActionSet) {
   217  				t.Errorf("mock client expected actions:\n%v\n but got:\n%v\nDifference:\n%v",
   218  					testInput.kubeClientActionSet, actionSet, testInput.kubeClientActionSet.Difference(actionSet))
   219  			}
   220  
   221  			// validate traffic from metadata client
   222  			actionSet = sets.NewString()
   223  			for _, action := range testHandler.actions {
   224  				actionSet.Insert(action.String())
   225  			}
   226  			if !actionSet.Equal(testInput.metadataClientActionSet) {
   227  				t.Errorf(" metadata client expected actions:\n%v\n but got:\n%v\nDifference:\n%v",
   228  					testInput.metadataClientActionSet, actionSet, testInput.metadataClientActionSet.Difference(actionSet))
   229  			}
   230  
   231  			// validate status conditions
   232  			if testInput.expectStatus != nil {
   233  				obj, err := mockClient.Tracker().Get(schema.GroupVersionResource{Version: "v1", Resource: "namespaces"}, testInput.testNamespace.Namespace, testInput.testNamespace.Name)
   234  				if err != nil {
   235  					t.Fatalf("Unexpected error in getting the namespace: %v", err)
   236  				}
   237  				ns, ok := obj.(*v1.Namespace)
   238  				if !ok {
   239  					t.Fatalf("Expected a namespace but received %v", obj)
   240  				}
   241  				if ns.Status.Phase != testInput.expectStatus.Phase {
   242  					t.Fatalf("Expected namespace status phase %v but received %v", testInput.expectStatus.Phase, ns.Status.Phase)
   243  				}
   244  				for _, expCondition := range testInput.expectStatus.Conditions {
   245  					nsCondition := getCondition(ns.Status.Conditions, expCondition.Type)
   246  					if nsCondition == nil {
   247  						t.Fatalf("Missing namespace status condition %v", expCondition.Type)
   248  					}
   249  				}
   250  			}
   251  		})
   252  	}
   253  }
   254  
   255  func TestRetryOnConflictError(t *testing.T) {
   256  	mockClient := &fake.Clientset{}
   257  	numTries := 0
   258  	retryOnce := func(ctx context.Context, namespace *v1.Namespace) (*v1.Namespace, error) {
   259  		numTries++
   260  		if numTries <= 1 {
   261  			return namespace, errors.NewConflict(api.Resource("namespaces"), namespace.Name, fmt.Errorf("ERROR"))
   262  		}
   263  		return namespace, nil
   264  	}
   265  	namespace := &v1.Namespace{}
   266  	d := namespacedResourcesDeleter{
   267  		nsClient: mockClient.CoreV1().Namespaces(),
   268  	}
   269  	_, err := d.retryOnConflictError(context.Background(), namespace, retryOnce)
   270  	if err != nil {
   271  		t.Errorf("Unexpected error %v", err)
   272  	}
   273  	if numTries != 2 {
   274  		t.Errorf("Expected %v, but got %v", 2, numTries)
   275  	}
   276  }
   277  
   278  func TestSyncNamespaceThatIsTerminatingNonExperimental(t *testing.T) {
   279  	testSyncNamespaceThatIsTerminating(t, &metav1.APIVersions{})
   280  }
   281  
   282  func TestSyncNamespaceThatIsTerminatingV1(t *testing.T) {
   283  	testSyncNamespaceThatIsTerminating(t, &metav1.APIVersions{Versions: []string{"apps/v1"}})
   284  }
   285  
   286  func TestSyncNamespaceThatIsActive(t *testing.T) {
   287  	mockClient := &fake.Clientset{}
   288  	testNamespace := &v1.Namespace{
   289  		ObjectMeta: metav1.ObjectMeta{
   290  			Name:            "test",
   291  			ResourceVersion: "1",
   292  		},
   293  		Spec: v1.NamespaceSpec{
   294  			Finalizers: []v1.FinalizerName{"kubernetes"},
   295  		},
   296  		Status: v1.NamespaceStatus{
   297  			Phase: v1.NamespaceActive,
   298  		},
   299  	}
   300  	fn := func() ([]*metav1.APIResourceList, error) {
   301  		return testResources(), nil
   302  	}
   303  	_, ctx := ktesting.NewTestContext(t)
   304  	d := NewNamespacedResourcesDeleter(ctx, mockClient.CoreV1().Namespaces(), nil, mockClient.CoreV1(),
   305  		fn, v1.FinalizerKubernetes)
   306  	err := d.Delete(ctx, testNamespace.Name)
   307  	if err != nil {
   308  		t.Errorf("Unexpected error when synching namespace %v", err)
   309  	}
   310  	if len(mockClient.Actions()) != 1 {
   311  		t.Errorf("Expected only one action from controller, but got: %d %v", len(mockClient.Actions()), mockClient.Actions())
   312  	}
   313  	action := mockClient.Actions()[0]
   314  	if !action.Matches("get", "namespaces") {
   315  		t.Errorf("Expected get namespaces, got: %v", action)
   316  	}
   317  }
   318  
   319  // matchError returns true if errors match, false if they don't, compares by error message only for convenience which should be sufficient for these tests
   320  func matchErrors(e1, e2 error) bool {
   321  	if e1 == nil && e2 == nil {
   322  		return true
   323  	}
   324  	if e1 != nil && e2 != nil {
   325  		return e1.Error() == e2.Error()
   326  	}
   327  	return false
   328  }
   329  
   330  // testServerAndClientConfig returns a server that listens and a config that can reference it
   331  func testServerAndClientConfig(handler func(http.ResponseWriter, *http.Request)) (*httptest.Server, *restclient.Config) {
   332  	srv := httptest.NewServer(http.HandlerFunc(handler))
   333  	config := &restclient.Config{
   334  		Host: srv.URL,
   335  	}
   336  	return srv, config
   337  }
   338  
   339  // fakeAction records information about requests to aid in testing.
   340  type fakeAction struct {
   341  	method string
   342  	path   string
   343  }
   344  
   345  // String returns method=path to aid in testing
   346  func (f *fakeAction) String() string {
   347  	return strings.Join([]string{f.method, f.path}, "=")
   348  }
   349  
   350  // fakeActionHandler holds a list of fakeActions received
   351  type fakeActionHandler struct {
   352  	// statusCode returned by this handler
   353  	statusCode int
   354  
   355  	lock    sync.Mutex
   356  	actions []fakeAction
   357  }
   358  
   359  // ServeHTTP logs the action that occurred and always returns the associated status code
   360  func (f *fakeActionHandler) ServeHTTP(response http.ResponseWriter, request *http.Request) {
   361  	f.lock.Lock()
   362  	defer f.lock.Unlock()
   363  
   364  	f.actions = append(f.actions, fakeAction{method: request.Method, path: request.URL.Path})
   365  	response.Header().Set("Content-Type", runtime.ContentTypeJSON)
   366  	response.WriteHeader(f.statusCode)
   367  	response.Write([]byte("{\"apiVersion\": \"v1\", \"kind\": \"List\",\"items\":null}"))
   368  }
   369  
   370  // testResources returns a mocked up set of resources across different api groups for testing namespace controller.
   371  func testResources() []*metav1.APIResourceList {
   372  	results := []*metav1.APIResourceList{
   373  		{
   374  			GroupVersion: "v1",
   375  			APIResources: []metav1.APIResource{
   376  				{
   377  					Name:       "pods",
   378  					Namespaced: true,
   379  					Kind:       "Pod",
   380  					Verbs:      []string{"get", "list", "delete", "deletecollection", "create", "update"},
   381  				},
   382  				{
   383  					Name:       "services",
   384  					Namespaced: true,
   385  					Kind:       "Service",
   386  					Verbs:      []string{"get", "list", "delete", "deletecollection", "create", "update"},
   387  				},
   388  			},
   389  		},
   390  		{
   391  			GroupVersion: "apps/v1",
   392  			APIResources: []metav1.APIResource{
   393  				{
   394  					Name:       "deployments",
   395  					Namespaced: true,
   396  					Kind:       "Deployment",
   397  					Verbs:      []string{"get", "list", "delete", "deletecollection", "create", "update"},
   398  				},
   399  			},
   400  		},
   401  	}
   402  	return results
   403  }
   404  
   405  func TestDeleteEncounters404(t *testing.T) {
   406  	now := metav1.Now()
   407  	ns1 := &v1.Namespace{
   408  		ObjectMeta: metav1.ObjectMeta{Name: "ns1", ResourceVersion: "1", DeletionTimestamp: &now},
   409  		Spec:       v1.NamespaceSpec{Finalizers: []v1.FinalizerName{"kubernetes"}},
   410  		Status:     v1.NamespaceStatus{Phase: v1.NamespaceActive},
   411  	}
   412  	ns2 := &v1.Namespace{
   413  		ObjectMeta: metav1.ObjectMeta{Name: "ns2", ResourceVersion: "1", DeletionTimestamp: &now},
   414  		Spec:       v1.NamespaceSpec{Finalizers: []v1.FinalizerName{"kubernetes"}},
   415  		Status:     v1.NamespaceStatus{Phase: v1.NamespaceActive},
   416  	}
   417  	mockClient := fake.NewSimpleClientset(ns1, ns2)
   418  
   419  	ns1FlakesNotFound := func(action core.Action) (handled bool, ret runtime.Object, err error) {
   420  		if action.GetNamespace() == "ns1" {
   421  			// simulate the flakes resource not existing when ns1 is processed
   422  			return true, nil, errors.NewNotFound(schema.GroupResource{}, "")
   423  		}
   424  		return false, nil, nil
   425  	}
   426  	mockMetadataClient := metadatafake.NewSimpleMetadataClient(metadatafake.NewTestScheme())
   427  	mockMetadataClient.PrependReactor("delete-collection", "flakes", ns1FlakesNotFound)
   428  	mockMetadataClient.PrependReactor("list", "flakes", ns1FlakesNotFound)
   429  
   430  	resourcesFn := func() ([]*metav1.APIResourceList, error) {
   431  		return []*metav1.APIResourceList{{
   432  			GroupVersion: "example.com/v1",
   433  			APIResources: []metav1.APIResource{{Name: "flakes", Namespaced: true, Kind: "Flake", Verbs: []string{"get", "list", "delete", "deletecollection", "create", "update"}}},
   434  		}}, nil
   435  	}
   436  	_, ctx := ktesting.NewTestContext(t)
   437  	d := NewNamespacedResourcesDeleter(ctx, mockClient.CoreV1().Namespaces(), mockMetadataClient, mockClient.CoreV1(), resourcesFn, v1.FinalizerKubernetes)
   438  
   439  	// Delete ns1 and get NotFound errors for the flakes resource
   440  	mockMetadataClient.ClearActions()
   441  	if err := d.Delete(ctx, ns1.Name); err != nil {
   442  		t.Fatal(err)
   443  	}
   444  	if len(mockMetadataClient.Actions()) != 3 ||
   445  		!mockMetadataClient.Actions()[0].Matches("delete-collection", "flakes") ||
   446  		!mockMetadataClient.Actions()[1].Matches("list", "flakes") ||
   447  		!mockMetadataClient.Actions()[2].Matches("list", "flakes") {
   448  		for _, action := range mockMetadataClient.Actions() {
   449  			t.Log("ns1", action)
   450  		}
   451  		t.Error("ns1: expected delete-collection -> fallback to list -> list to verify 0 items")
   452  	}
   453  
   454  	// Delete ns2
   455  	mockMetadataClient.ClearActions()
   456  	if err := d.Delete(ctx, ns2.Name); err != nil {
   457  		t.Fatal(err)
   458  	}
   459  	if len(mockMetadataClient.Actions()) != 2 ||
   460  		!mockMetadataClient.Actions()[0].Matches("delete-collection", "flakes") ||
   461  		!mockMetadataClient.Actions()[1].Matches("list", "flakes") {
   462  		for _, action := range mockMetadataClient.Actions() {
   463  			t.Log("ns2", action)
   464  		}
   465  		t.Error("ns2: expected delete-collection -> list to verify 0 items")
   466  	}
   467  }
   468  

View as plain text