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 }