k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/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 "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 }