k8s.io/kubernetes@v1.29.3/pkg/controller/namespace/deletion/namespaced_resources_deleter_test.go (about)

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