mirror of
				https://github.com/k3s-io/kubernetes.git
				synced 2025-10-30 21:30:16 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			2811 lines
		
	
	
		
			109 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			2811 lines
		
	
	
		
			109 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| /*
 | |
| Copyright 2016 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 garbagecollector
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"fmt"
 | |
| 	"net/http"
 | |
| 	"net/http/httptest"
 | |
| 	"reflect"
 | |
| 	"strings"
 | |
| 	"sync"
 | |
| 	"testing"
 | |
| 	"time"
 | |
| 
 | |
| 	"golang.org/x/time/rate"
 | |
| 
 | |
| 	"github.com/golang/groupcache/lru"
 | |
| 	"github.com/google/go-cmp/cmp"
 | |
| 	"github.com/stretchr/testify/assert"
 | |
| 
 | |
| 	_ "k8s.io/kubernetes/pkg/apis/core/install"
 | |
| 	"k8s.io/kubernetes/pkg/controller/garbagecollector/metaonly"
 | |
| 	"k8s.io/utils/pointer"
 | |
| 
 | |
| 	v1 "k8s.io/api/core/v1"
 | |
| 	"k8s.io/apimachinery/pkg/api/meta"
 | |
| 	"k8s.io/apimachinery/pkg/api/meta/testrestmapper"
 | |
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 | |
| 	"k8s.io/apimachinery/pkg/runtime"
 | |
| 	"k8s.io/apimachinery/pkg/runtime/schema"
 | |
| 	"k8s.io/apimachinery/pkg/types"
 | |
| 	"k8s.io/apimachinery/pkg/util/json"
 | |
| 	"k8s.io/apimachinery/pkg/util/sets"
 | |
| 	"k8s.io/apimachinery/pkg/util/strategicpatch"
 | |
| 	"k8s.io/client-go/discovery"
 | |
| 	"k8s.io/client-go/informers"
 | |
| 	"k8s.io/client-go/kubernetes"
 | |
| 	"k8s.io/client-go/kubernetes/fake"
 | |
| 	"k8s.io/client-go/metadata"
 | |
| 	fakemetadata "k8s.io/client-go/metadata/fake"
 | |
| 	"k8s.io/client-go/metadata/metadatainformer"
 | |
| 	restclient "k8s.io/client-go/rest"
 | |
| 	clientgotesting "k8s.io/client-go/testing"
 | |
| 	"k8s.io/client-go/tools/record"
 | |
| 	"k8s.io/client-go/util/workqueue"
 | |
| 	"k8s.io/controller-manager/pkg/informerfactory"
 | |
| 	"k8s.io/kubernetes/pkg/api/legacyscheme"
 | |
| 	c "k8s.io/kubernetes/pkg/controller"
 | |
| )
 | |
| 
 | |
| type testRESTMapper struct {
 | |
| 	meta.RESTMapper
 | |
| }
 | |
| 
 | |
| func (m *testRESTMapper) Reset() {
 | |
| 	meta.MaybeResetRESTMapper(m.RESTMapper)
 | |
| }
 | |
| 
 | |
| func TestGarbageCollectorConstruction(t *testing.T) {
 | |
| 	config := &restclient.Config{}
 | |
| 	tweakableRM := meta.NewDefaultRESTMapper(nil)
 | |
| 	rm := &testRESTMapper{meta.MultiRESTMapper{tweakableRM, testrestmapper.TestOnlyStaticRESTMapper(legacyscheme.Scheme)}}
 | |
| 	metadataClient, err := metadata.NewForConfig(config)
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 
 | |
| 	podResource := map[schema.GroupVersionResource]struct{}{
 | |
| 		{Version: "v1", Resource: "pods"}: {},
 | |
| 	}
 | |
| 	twoResources := map[schema.GroupVersionResource]struct{}{
 | |
| 		{Version: "v1", Resource: "pods"}:                     {},
 | |
| 		{Group: "tpr.io", Version: "v1", Resource: "unknown"}: {},
 | |
| 	}
 | |
| 	client := fake.NewSimpleClientset()
 | |
| 
 | |
| 	sharedInformers := informers.NewSharedInformerFactory(client, 0)
 | |
| 	metadataInformers := metadatainformer.NewSharedInformerFactory(metadataClient, 0)
 | |
| 	// No monitor will be constructed for the non-core resource, but the GC
 | |
| 	// construction will not fail.
 | |
| 	alwaysStarted := make(chan struct{})
 | |
| 	close(alwaysStarted)
 | |
| 	gc, err := NewGarbageCollector(client, metadataClient, rm, map[schema.GroupResource]struct{}{},
 | |
| 		informerfactory.NewInformerFactory(sharedInformers, metadataInformers), alwaysStarted)
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	assert.Equal(t, 0, len(gc.dependencyGraphBuilder.monitors))
 | |
| 
 | |
| 	// Make sure resource monitor syncing creates and stops resource monitors.
 | |
| 	tweakableRM.Add(schema.GroupVersionKind{Group: "tpr.io", Version: "v1", Kind: "unknown"}, nil)
 | |
| 	err = gc.resyncMonitors(twoResources)
 | |
| 	if err != nil {
 | |
| 		t.Errorf("Failed adding a monitor: %v", err)
 | |
| 	}
 | |
| 	assert.Equal(t, 2, len(gc.dependencyGraphBuilder.monitors))
 | |
| 
 | |
| 	err = gc.resyncMonitors(podResource)
 | |
| 	if err != nil {
 | |
| 		t.Errorf("Failed removing a monitor: %v", err)
 | |
| 	}
 | |
| 	assert.Equal(t, 1, len(gc.dependencyGraphBuilder.monitors))
 | |
| 
 | |
| 	// Make sure the syncing mechanism also works after Run() has been called
 | |
| 	ctx, cancel := context.WithCancel(context.Background())
 | |
| 	defer cancel()
 | |
| 	go gc.Run(ctx, 1)
 | |
| 
 | |
| 	err = gc.resyncMonitors(twoResources)
 | |
| 	if err != nil {
 | |
| 		t.Errorf("Failed adding a monitor: %v", err)
 | |
| 	}
 | |
| 	assert.Equal(t, 2, len(gc.dependencyGraphBuilder.monitors))
 | |
| 
 | |
| 	err = gc.resyncMonitors(podResource)
 | |
| 	if err != nil {
 | |
| 		t.Errorf("Failed removing a monitor: %v", err)
 | |
| 	}
 | |
| 	assert.Equal(t, 1, len(gc.dependencyGraphBuilder.monitors))
 | |
| }
 | |
| 
 | |
| // fakeAction records information about requests to aid in testing.
 | |
| type fakeAction struct {
 | |
| 	method string
 | |
| 	path   string
 | |
| 	query  string
 | |
| }
 | |
| 
 | |
| // String returns method=path to aid in testing
 | |
| func (f *fakeAction) String() string {
 | |
| 	return strings.Join([]string{f.method, f.path}, "=")
 | |
| }
 | |
| 
 | |
| type FakeResponse struct {
 | |
| 	statusCode int
 | |
| 	content    []byte
 | |
| }
 | |
| 
 | |
| // fakeActionHandler holds a list of fakeActions received
 | |
| type fakeActionHandler struct {
 | |
| 	// statusCode and content returned by this handler for different method + path.
 | |
| 	response map[string]FakeResponse
 | |
| 
 | |
| 	lock    sync.Mutex
 | |
| 	actions []fakeAction
 | |
| }
 | |
| 
 | |
| // ServeHTTP logs the action that occurred and always returns the associated status code
 | |
| func (f *fakeActionHandler) ServeHTTP(response http.ResponseWriter, request *http.Request) {
 | |
| 	func() {
 | |
| 		f.lock.Lock()
 | |
| 		defer f.lock.Unlock()
 | |
| 
 | |
| 		f.actions = append(f.actions, fakeAction{method: request.Method, path: request.URL.Path, query: request.URL.RawQuery})
 | |
| 		fakeResponse, ok := f.response[request.Method+request.URL.Path]
 | |
| 		if !ok {
 | |
| 			fakeResponse.statusCode = 200
 | |
| 			fakeResponse.content = []byte(`{"apiVersion": "v1", "kind": "List"}`)
 | |
| 		}
 | |
| 		response.Header().Set("Content-Type", "application/json")
 | |
| 		response.WriteHeader(fakeResponse.statusCode)
 | |
| 		response.Write(fakeResponse.content)
 | |
| 	}()
 | |
| 
 | |
| 	// This is to allow the fakeActionHandler to simulate a watch being opened
 | |
| 	if strings.Contains(request.URL.RawQuery, "watch=true") {
 | |
| 		hijacker, ok := response.(http.Hijacker)
 | |
| 		if !ok {
 | |
| 			return
 | |
| 		}
 | |
| 		connection, _, err := hijacker.Hijack()
 | |
| 		if err != nil {
 | |
| 			return
 | |
| 		}
 | |
| 		defer connection.Close()
 | |
| 		time.Sleep(30 * time.Second)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // testServerAndClientConfig returns a server that listens and a config that can reference it
 | |
| func testServerAndClientConfig(handler func(http.ResponseWriter, *http.Request)) (*httptest.Server, *restclient.Config) {
 | |
| 	srv := httptest.NewServer(http.HandlerFunc(handler))
 | |
| 	config := &restclient.Config{
 | |
| 		Host: srv.URL,
 | |
| 	}
 | |
| 	return srv, config
 | |
| }
 | |
| 
 | |
| type garbageCollector struct {
 | |
| 	*GarbageCollector
 | |
| 	stop chan struct{}
 | |
| }
 | |
| 
 | |
| func setupGC(t *testing.T, config *restclient.Config) garbageCollector {
 | |
| 	metadataClient, err := metadata.NewForConfig(config)
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 
 | |
| 	client := fake.NewSimpleClientset()
 | |
| 	sharedInformers := informers.NewSharedInformerFactory(client, 0)
 | |
| 	alwaysStarted := make(chan struct{})
 | |
| 	close(alwaysStarted)
 | |
| 	gc, err := NewGarbageCollector(client, metadataClient, &testRESTMapper{testrestmapper.TestOnlyStaticRESTMapper(legacyscheme.Scheme)}, ignoredResources, sharedInformers, alwaysStarted)
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	stop := make(chan struct{})
 | |
| 	go sharedInformers.Start(stop)
 | |
| 	return garbageCollector{gc, stop}
 | |
| }
 | |
| 
 | |
| func getPod(podName string, ownerReferences []metav1.OwnerReference) *v1.Pod {
 | |
| 	return &v1.Pod{
 | |
| 		TypeMeta: metav1.TypeMeta{
 | |
| 			Kind:       "Pod",
 | |
| 			APIVersion: "v1",
 | |
| 		},
 | |
| 		ObjectMeta: metav1.ObjectMeta{
 | |
| 			Name:            podName,
 | |
| 			Namespace:       "ns1",
 | |
| 			UID:             "456",
 | |
| 			OwnerReferences: ownerReferences,
 | |
| 		},
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func serilizeOrDie(t *testing.T, object interface{}) []byte {
 | |
| 	data, err := json.Marshal(object)
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	return data
 | |
| }
 | |
| 
 | |
| // test the attemptToDeleteItem function making the expected actions.
 | |
| func TestAttemptToDeleteItem(t *testing.T) {
 | |
| 	pod := getPod("ToBeDeletedPod", []metav1.OwnerReference{
 | |
| 		{
 | |
| 			Kind:       "ReplicationController",
 | |
| 			Name:       "owner1",
 | |
| 			UID:        "123",
 | |
| 			APIVersion: "v1",
 | |
| 		},
 | |
| 	})
 | |
| 	testHandler := &fakeActionHandler{
 | |
| 		response: map[string]FakeResponse{
 | |
| 			"GET" + "/api/v1/namespaces/ns1/replicationcontrollers/owner1": {
 | |
| 				404,
 | |
| 				[]byte{},
 | |
| 			},
 | |
| 			"GET" + "/api/v1/namespaces/ns1/pods/ToBeDeletedPod": {
 | |
| 				200,
 | |
| 				serilizeOrDie(t, pod),
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 	srv, clientConfig := testServerAndClientConfig(testHandler.ServeHTTP)
 | |
| 	defer srv.Close()
 | |
| 
 | |
| 	gc := setupGC(t, clientConfig)
 | |
| 	defer close(gc.stop)
 | |
| 
 | |
| 	item := &node{
 | |
| 		identity: objectReference{
 | |
| 			OwnerReference: metav1.OwnerReference{
 | |
| 				Kind:       pod.Kind,
 | |
| 				APIVersion: pod.APIVersion,
 | |
| 				Name:       pod.Name,
 | |
| 				UID:        pod.UID,
 | |
| 			},
 | |
| 			Namespace: pod.Namespace,
 | |
| 		},
 | |
| 		// owners are intentionally left empty. The attemptToDeleteItem routine should get the latest item from the server.
 | |
| 		owners:  nil,
 | |
| 		virtual: true,
 | |
| 	}
 | |
| 	err := gc.attemptToDeleteItem(context.TODO(), item)
 | |
| 	if err != nil {
 | |
| 		t.Errorf("Unexpected Error: %v", err)
 | |
| 	}
 | |
| 	if !item.virtual {
 | |
| 		t.Errorf("attemptToDeleteItem changed virtual to false unexpectedly")
 | |
| 	}
 | |
| 	expectedActionSet := sets.NewString()
 | |
| 	expectedActionSet.Insert("GET=/api/v1/namespaces/ns1/replicationcontrollers/owner1")
 | |
| 	expectedActionSet.Insert("DELETE=/api/v1/namespaces/ns1/pods/ToBeDeletedPod")
 | |
| 	expectedActionSet.Insert("GET=/api/v1/namespaces/ns1/pods/ToBeDeletedPod")
 | |
| 
 | |
| 	actualActionSet := sets.NewString()
 | |
| 	for _, action := range testHandler.actions {
 | |
| 		actualActionSet.Insert(action.String())
 | |
| 	}
 | |
| 	if !expectedActionSet.Equal(actualActionSet) {
 | |
| 		t.Errorf("expected actions:\n%v\n but got:\n%v\nDifference:\n%v", expectedActionSet,
 | |
| 			actualActionSet, expectedActionSet.Difference(actualActionSet))
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // verifyGraphInvariants verifies that all of a node's owners list the node as a
 | |
| // dependent and vice versa. uidToNode has all the nodes in the graph.
 | |
| func verifyGraphInvariants(scenario string, uidToNode map[types.UID]*node, t *testing.T) {
 | |
| 	for myUID, node := range uidToNode {
 | |
| 		for dependentNode := range node.dependents {
 | |
| 			found := false
 | |
| 			for _, owner := range dependentNode.owners {
 | |
| 				if owner.UID == myUID {
 | |
| 					found = true
 | |
| 					break
 | |
| 				}
 | |
| 			}
 | |
| 			if !found {
 | |
| 				t.Errorf("scenario: %s: node %s has node %s as a dependent, but it's not present in the latter node's owners list", scenario, node.identity, dependentNode.identity)
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		for _, owner := range node.owners {
 | |
| 			ownerNode, ok := uidToNode[owner.UID]
 | |
| 			if !ok {
 | |
| 				// It's possible that the owner node doesn't exist
 | |
| 				continue
 | |
| 			}
 | |
| 			if _, ok := ownerNode.dependents[node]; !ok {
 | |
| 				t.Errorf("node %s has node %s as an owner, but it's not present in the latter node's dependents list", node.identity, ownerNode.identity)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func createEvent(eventType eventType, selfUID string, owners []string) event {
 | |
| 	var ownerReferences []metav1.OwnerReference
 | |
| 	for i := 0; i < len(owners); i++ {
 | |
| 		ownerReferences = append(ownerReferences, metav1.OwnerReference{UID: types.UID(owners[i])})
 | |
| 	}
 | |
| 	return event{
 | |
| 		eventType: eventType,
 | |
| 		obj: &v1.Pod{
 | |
| 			ObjectMeta: metav1.ObjectMeta{
 | |
| 				UID:             types.UID(selfUID),
 | |
| 				OwnerReferences: ownerReferences,
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestProcessEvent(t *testing.T) {
 | |
| 	var testScenarios = []struct {
 | |
| 		name string
 | |
| 		// a series of events that will be supplied to the
 | |
| 		// GraphBuilder.graphChanges.
 | |
| 		events []event
 | |
| 	}{
 | |
| 		{
 | |
| 			name: "test1",
 | |
| 			events: []event{
 | |
| 				createEvent(addEvent, "1", []string{}),
 | |
| 				createEvent(addEvent, "2", []string{"1"}),
 | |
| 				createEvent(addEvent, "3", []string{"1", "2"}),
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "test2",
 | |
| 			events: []event{
 | |
| 				createEvent(addEvent, "1", []string{}),
 | |
| 				createEvent(addEvent, "2", []string{"1"}),
 | |
| 				createEvent(addEvent, "3", []string{"1", "2"}),
 | |
| 				createEvent(addEvent, "4", []string{"2"}),
 | |
| 				createEvent(deleteEvent, "2", []string{"doesn't matter"}),
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "test3",
 | |
| 			events: []event{
 | |
| 				createEvent(addEvent, "1", []string{}),
 | |
| 				createEvent(addEvent, "2", []string{"1"}),
 | |
| 				createEvent(addEvent, "3", []string{"1", "2"}),
 | |
| 				createEvent(addEvent, "4", []string{"3"}),
 | |
| 				createEvent(updateEvent, "2", []string{"4"}),
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "reverse test2",
 | |
| 			events: []event{
 | |
| 				createEvent(addEvent, "4", []string{"2"}),
 | |
| 				createEvent(addEvent, "3", []string{"1", "2"}),
 | |
| 				createEvent(addEvent, "2", []string{"1"}),
 | |
| 				createEvent(addEvent, "1", []string{}),
 | |
| 				createEvent(deleteEvent, "2", []string{"doesn't matter"}),
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	alwaysStarted := make(chan struct{})
 | |
| 	close(alwaysStarted)
 | |
| 	for _, scenario := range testScenarios {
 | |
| 		dependencyGraphBuilder := &GraphBuilder{
 | |
| 			informersStarted: alwaysStarted,
 | |
| 			graphChanges:     workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()),
 | |
| 			uidToNode: &concurrentUIDToNode{
 | |
| 				uidToNodeLock: sync.RWMutex{},
 | |
| 				uidToNode:     make(map[types.UID]*node),
 | |
| 			},
 | |
| 			attemptToDelete:  workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()),
 | |
| 			absentOwnerCache: NewReferenceCache(2),
 | |
| 		}
 | |
| 		for i := 0; i < len(scenario.events); i++ {
 | |
| 			dependencyGraphBuilder.graphChanges.Add(&scenario.events[i])
 | |
| 			dependencyGraphBuilder.processGraphChanges()
 | |
| 			verifyGraphInvariants(scenario.name, dependencyGraphBuilder.uidToNode.uidToNode, t)
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func BenchmarkReferencesDiffs(t *testing.B) {
 | |
| 	t.ReportAllocs()
 | |
| 	t.ResetTimer()
 | |
| 	for n := 0; n < t.N; n++ {
 | |
| 		old := []metav1.OwnerReference{{UID: "1"}, {UID: "2"}}
 | |
| 		new := []metav1.OwnerReference{{UID: "2"}, {UID: "3"}}
 | |
| 		referencesDiffs(old, new)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // TestDependentsRace relies on golang's data race detector to check if there is
 | |
| // data race among in the dependents field.
 | |
| func TestDependentsRace(t *testing.T) {
 | |
| 	gc := setupGC(t, &restclient.Config{})
 | |
| 	defer close(gc.stop)
 | |
| 
 | |
| 	const updates = 100
 | |
| 	owner := &node{dependents: make(map[*node]struct{})}
 | |
| 	ownerUID := types.UID("owner")
 | |
| 	gc.dependencyGraphBuilder.uidToNode.Write(owner)
 | |
| 	var wg sync.WaitGroup
 | |
| 	wg.Add(2)
 | |
| 	go func() {
 | |
| 		defer wg.Done()
 | |
| 		for i := 0; i < updates; i++ {
 | |
| 			dependent := &node{}
 | |
| 			gc.dependencyGraphBuilder.addDependentToOwners(dependent, []metav1.OwnerReference{{UID: ownerUID}})
 | |
| 			gc.dependencyGraphBuilder.removeDependentFromOwners(dependent, []metav1.OwnerReference{{UID: ownerUID}})
 | |
| 		}
 | |
| 	}()
 | |
| 	go func() {
 | |
| 		defer wg.Done()
 | |
| 		for i := 0; i < updates; i++ {
 | |
| 			gc.attemptToOrphan.Add(owner)
 | |
| 			gc.processAttemptToOrphanWorker()
 | |
| 		}
 | |
| 	}()
 | |
| 	wg.Wait()
 | |
| }
 | |
| 
 | |
| func podToGCNode(pod *v1.Pod) *node {
 | |
| 	return &node{
 | |
| 		identity: objectReference{
 | |
| 			OwnerReference: metav1.OwnerReference{
 | |
| 				Kind:       pod.Kind,
 | |
| 				APIVersion: pod.APIVersion,
 | |
| 				Name:       pod.Name,
 | |
| 				UID:        pod.UID,
 | |
| 			},
 | |
| 			Namespace: pod.Namespace,
 | |
| 		},
 | |
| 		// owners are intentionally left empty. The attemptToDeleteItem routine should get the latest item from the server.
 | |
| 		owners: nil,
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestAbsentOwnerCache(t *testing.T) {
 | |
| 	rc1Pod1 := getPod("rc1Pod1", []metav1.OwnerReference{
 | |
| 		{
 | |
| 			Kind:       "ReplicationController",
 | |
| 			Name:       "rc1",
 | |
| 			UID:        "1",
 | |
| 			APIVersion: "v1",
 | |
| 			Controller: pointer.BoolPtr(true),
 | |
| 		},
 | |
| 	})
 | |
| 	rc1Pod2 := getPod("rc1Pod2", []metav1.OwnerReference{
 | |
| 		{
 | |
| 			Kind:       "ReplicationController",
 | |
| 			Name:       "rc1",
 | |
| 			UID:        "1",
 | |
| 			APIVersion: "v1",
 | |
| 			Controller: pointer.BoolPtr(false),
 | |
| 		},
 | |
| 	})
 | |
| 	rc2Pod1 := getPod("rc2Pod1", []metav1.OwnerReference{
 | |
| 		{
 | |
| 			Kind:       "ReplicationController",
 | |
| 			Name:       "rc2",
 | |
| 			UID:        "2",
 | |
| 			APIVersion: "v1",
 | |
| 		},
 | |
| 	})
 | |
| 	rc3Pod1 := getPod("rc3Pod1", []metav1.OwnerReference{
 | |
| 		{
 | |
| 			Kind:       "ReplicationController",
 | |
| 			Name:       "rc3",
 | |
| 			UID:        "3",
 | |
| 			APIVersion: "v1",
 | |
| 		},
 | |
| 	})
 | |
| 	testHandler := &fakeActionHandler{
 | |
| 		response: map[string]FakeResponse{
 | |
| 			"GET" + "/api/v1/namespaces/ns1/pods/rc1Pod1": {
 | |
| 				200,
 | |
| 				serilizeOrDie(t, rc1Pod1),
 | |
| 			},
 | |
| 			"GET" + "/api/v1/namespaces/ns1/pods/rc1Pod2": {
 | |
| 				200,
 | |
| 				serilizeOrDie(t, rc1Pod2),
 | |
| 			},
 | |
| 			"GET" + "/api/v1/namespaces/ns1/pods/rc2Pod1": {
 | |
| 				200,
 | |
| 				serilizeOrDie(t, rc2Pod1),
 | |
| 			},
 | |
| 			"GET" + "/api/v1/namespaces/ns1/pods/rc3Pod1": {
 | |
| 				200,
 | |
| 				serilizeOrDie(t, rc3Pod1),
 | |
| 			},
 | |
| 			"GET" + "/api/v1/namespaces/ns1/replicationcontrollers/rc1": {
 | |
| 				404,
 | |
| 				[]byte{},
 | |
| 			},
 | |
| 			"GET" + "/api/v1/namespaces/ns1/replicationcontrollers/rc2": {
 | |
| 				404,
 | |
| 				[]byte{},
 | |
| 			},
 | |
| 			"GET" + "/api/v1/namespaces/ns1/replicationcontrollers/rc3": {
 | |
| 				404,
 | |
| 				[]byte{},
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 	srv, clientConfig := testServerAndClientConfig(testHandler.ServeHTTP)
 | |
| 	defer srv.Close()
 | |
| 	gc := setupGC(t, clientConfig)
 | |
| 	defer close(gc.stop)
 | |
| 	gc.absentOwnerCache = NewReferenceCache(2)
 | |
| 	gc.attemptToDeleteItem(context.TODO(), podToGCNode(rc1Pod1))
 | |
| 	gc.attemptToDeleteItem(context.TODO(), podToGCNode(rc2Pod1))
 | |
| 	// rc1 should already be in the cache, no request should be sent. rc1 should be promoted in the UIDCache
 | |
| 	gc.attemptToDeleteItem(context.TODO(), podToGCNode(rc1Pod2))
 | |
| 	// after this call, rc2 should be evicted from the UIDCache
 | |
| 	gc.attemptToDeleteItem(context.TODO(), podToGCNode(rc3Pod1))
 | |
| 	// check cache
 | |
| 	if !gc.absentOwnerCache.Has(objectReference{Namespace: "ns1", OwnerReference: metav1.OwnerReference{Kind: "ReplicationController", Name: "rc1", UID: "1", APIVersion: "v1"}}) {
 | |
| 		t.Errorf("expected rc1 to be in the cache")
 | |
| 	}
 | |
| 	if gc.absentOwnerCache.Has(objectReference{Namespace: "ns1", OwnerReference: metav1.OwnerReference{Kind: "ReplicationController", Name: "rc2", UID: "2", APIVersion: "v1"}}) {
 | |
| 		t.Errorf("expected rc2 to not exist in the cache")
 | |
| 	}
 | |
| 	if !gc.absentOwnerCache.Has(objectReference{Namespace: "ns1", OwnerReference: metav1.OwnerReference{Kind: "ReplicationController", Name: "rc3", UID: "3", APIVersion: "v1"}}) {
 | |
| 		t.Errorf("expected rc3 to be in the cache")
 | |
| 	}
 | |
| 	// check the request sent to the server
 | |
| 	count := 0
 | |
| 	for _, action := range testHandler.actions {
 | |
| 		if action.String() == "GET=/api/v1/namespaces/ns1/replicationcontrollers/rc1" {
 | |
| 			count++
 | |
| 		}
 | |
| 	}
 | |
| 	if count != 1 {
 | |
| 		t.Errorf("expected only 1 GET rc1 request, got %d", count)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestDeleteOwnerRefPatch(t *testing.T) {
 | |
| 	original := v1.Pod{
 | |
| 		ObjectMeta: metav1.ObjectMeta{
 | |
| 			UID: "100",
 | |
| 			OwnerReferences: []metav1.OwnerReference{
 | |
| 				{UID: "1"},
 | |
| 				{UID: "2"},
 | |
| 				{UID: "3"},
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 	originalData := serilizeOrDie(t, original)
 | |
| 	expected := v1.Pod{
 | |
| 		ObjectMeta: metav1.ObjectMeta{
 | |
| 			UID: "100",
 | |
| 			OwnerReferences: []metav1.OwnerReference{
 | |
| 				{UID: "1"},
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 	p, err := c.GenerateDeleteOwnerRefStrategicMergeBytes("100", []types.UID{"2", "3"})
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	patched, err := strategicpatch.StrategicMergePatch(originalData, p, v1.Pod{})
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	var got v1.Pod
 | |
| 	if err := json.Unmarshal(patched, &got); err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	if !reflect.DeepEqual(expected, got) {
 | |
| 		t.Errorf("expected: %#v,\ngot: %#v", expected, got)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestUnblockOwnerReference(t *testing.T) {
 | |
| 	trueVar := true
 | |
| 	falseVar := false
 | |
| 	original := v1.Pod{
 | |
| 		ObjectMeta: metav1.ObjectMeta{
 | |
| 			UID: "100",
 | |
| 			OwnerReferences: []metav1.OwnerReference{
 | |
| 				{UID: "1", BlockOwnerDeletion: &trueVar},
 | |
| 				{UID: "2", BlockOwnerDeletion: &falseVar},
 | |
| 				{UID: "3"},
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 	originalData := serilizeOrDie(t, original)
 | |
| 	expected := v1.Pod{
 | |
| 		ObjectMeta: metav1.ObjectMeta{
 | |
| 			UID: "100",
 | |
| 			OwnerReferences: []metav1.OwnerReference{
 | |
| 				{UID: "1", BlockOwnerDeletion: &falseVar},
 | |
| 				{UID: "2", BlockOwnerDeletion: &falseVar},
 | |
| 				{UID: "3"},
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 	accessor, err := meta.Accessor(&original)
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	n := node{
 | |
| 		owners: accessor.GetOwnerReferences(),
 | |
| 	}
 | |
| 	patch, err := n.unblockOwnerReferencesStrategicMergePatch()
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	patched, err := strategicpatch.StrategicMergePatch(originalData, patch, v1.Pod{})
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	var got v1.Pod
 | |
| 	if err := json.Unmarshal(patched, &got); err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	if !reflect.DeepEqual(expected, got) {
 | |
| 		t.Errorf("expected: %#v,\ngot: %#v", expected, got)
 | |
| 		t.Errorf("expected: %#v,\ngot: %#v", expected.OwnerReferences, got.OwnerReferences)
 | |
| 		for _, ref := range got.OwnerReferences {
 | |
| 			t.Errorf("ref.UID=%s, ref.BlockOwnerDeletion=%v", ref.UID, *ref.BlockOwnerDeletion)
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestOrphanDependentsFailure(t *testing.T) {
 | |
| 	testHandler := &fakeActionHandler{
 | |
| 		response: map[string]FakeResponse{
 | |
| 			"PATCH" + "/api/v1/namespaces/ns1/pods/pod": {
 | |
| 				409,
 | |
| 				[]byte{},
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 	srv, clientConfig := testServerAndClientConfig(testHandler.ServeHTTP)
 | |
| 	defer srv.Close()
 | |
| 
 | |
| 	gc := setupGC(t, clientConfig)
 | |
| 	defer close(gc.stop)
 | |
| 
 | |
| 	dependents := []*node{
 | |
| 		{
 | |
| 			identity: objectReference{
 | |
| 				OwnerReference: metav1.OwnerReference{
 | |
| 					Kind:       "Pod",
 | |
| 					APIVersion: "v1",
 | |
| 					Name:       "pod",
 | |
| 				},
 | |
| 				Namespace: "ns1",
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 	err := gc.orphanDependents(objectReference{}, dependents)
 | |
| 	expected := `the server reported a conflict`
 | |
| 	if err == nil || !strings.Contains(err.Error(), expected) {
 | |
| 		if err != nil {
 | |
| 			t.Errorf("expected error contains text %q, got %q", expected, err.Error())
 | |
| 		} else {
 | |
| 			t.Errorf("expected error contains text %q, got nil", expected)
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // TestGetDeletableResources ensures GetDeletableResources always returns
 | |
| // something usable regardless of discovery output.
 | |
| func TestGetDeletableResources(t *testing.T) {
 | |
| 	tests := map[string]struct {
 | |
| 		serverResources    []*metav1.APIResourceList
 | |
| 		err                error
 | |
| 		deletableResources map[schema.GroupVersionResource]struct{}
 | |
| 	}{
 | |
| 		"no error": {
 | |
| 			serverResources: []*metav1.APIResourceList{
 | |
| 				{
 | |
| 					// Valid GroupVersion
 | |
| 					GroupVersion: "apps/v1",
 | |
| 					APIResources: []metav1.APIResource{
 | |
| 						{Name: "pods", Namespaced: true, Kind: "Pod", Verbs: metav1.Verbs{"delete", "list", "watch"}},
 | |
| 						{Name: "services", Namespaced: true, Kind: "Service"},
 | |
| 					},
 | |
| 				},
 | |
| 				{
 | |
| 					// Invalid GroupVersion, should be ignored
 | |
| 					GroupVersion: "foo//whatever",
 | |
| 					APIResources: []metav1.APIResource{
 | |
| 						{Name: "bars", Namespaced: true, Kind: "Bar", Verbs: metav1.Verbs{"delete", "list", "watch"}},
 | |
| 					},
 | |
| 				},
 | |
| 				{
 | |
| 					// Valid GroupVersion, missing required verbs, should be ignored
 | |
| 					GroupVersion: "acme/v1",
 | |
| 					APIResources: []metav1.APIResource{
 | |
| 						{Name: "widgets", Namespaced: true, Kind: "Widget", Verbs: metav1.Verbs{"delete"}},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			err: nil,
 | |
| 			deletableResources: map[schema.GroupVersionResource]struct{}{
 | |
| 				{Group: "apps", Version: "v1", Resource: "pods"}: {},
 | |
| 			},
 | |
| 		},
 | |
| 		"nonspecific failure, includes usable results": {
 | |
| 			serverResources: []*metav1.APIResourceList{
 | |
| 				{
 | |
| 					GroupVersion: "apps/v1",
 | |
| 					APIResources: []metav1.APIResource{
 | |
| 						{Name: "pods", Namespaced: true, Kind: "Pod", Verbs: metav1.Verbs{"delete", "list", "watch"}},
 | |
| 						{Name: "services", Namespaced: true, Kind: "Service"},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			err: fmt.Errorf("internal error"),
 | |
| 			deletableResources: map[schema.GroupVersionResource]struct{}{
 | |
| 				{Group: "apps", Version: "v1", Resource: "pods"}: {},
 | |
| 			},
 | |
| 		},
 | |
| 		"partial discovery failure, includes usable results": {
 | |
| 			serverResources: []*metav1.APIResourceList{
 | |
| 				{
 | |
| 					GroupVersion: "apps/v1",
 | |
| 					APIResources: []metav1.APIResource{
 | |
| 						{Name: "pods", Namespaced: true, Kind: "Pod", Verbs: metav1.Verbs{"delete", "list", "watch"}},
 | |
| 						{Name: "services", Namespaced: true, Kind: "Service"},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			err: &discovery.ErrGroupDiscoveryFailed{
 | |
| 				Groups: map[schema.GroupVersion]error{
 | |
| 					{Group: "foo", Version: "v1"}: fmt.Errorf("discovery failure"),
 | |
| 				},
 | |
| 			},
 | |
| 			deletableResources: map[schema.GroupVersionResource]struct{}{
 | |
| 				{Group: "apps", Version: "v1", Resource: "pods"}: {},
 | |
| 			},
 | |
| 		},
 | |
| 		"discovery failure, no results": {
 | |
| 			serverResources:    nil,
 | |
| 			err:                fmt.Errorf("internal error"),
 | |
| 			deletableResources: map[schema.GroupVersionResource]struct{}{},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for name, test := range tests {
 | |
| 		t.Logf("testing %q", name)
 | |
| 		client := &fakeServerResources{
 | |
| 			PreferredResources: test.serverResources,
 | |
| 			Error:              test.err,
 | |
| 		}
 | |
| 		actual := GetDeletableResources(client)
 | |
| 		if !reflect.DeepEqual(test.deletableResources, actual) {
 | |
| 			t.Errorf("expected resources:\n%v\ngot:\n%v", test.deletableResources, actual)
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // TestGarbageCollectorSync ensures that a discovery client error
 | |
| // will not cause the garbage collector to block infinitely.
 | |
| func TestGarbageCollectorSync(t *testing.T) {
 | |
| 	serverResources := []*metav1.APIResourceList{
 | |
| 		{
 | |
| 			GroupVersion: "v1",
 | |
| 			APIResources: []metav1.APIResource{
 | |
| 				{Name: "pods", Namespaced: true, Kind: "Pod", Verbs: metav1.Verbs{"delete", "list", "watch"}},
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 	unsyncableServerResources := []*metav1.APIResourceList{
 | |
| 		{
 | |
| 			GroupVersion: "v1",
 | |
| 			APIResources: []metav1.APIResource{
 | |
| 				{Name: "pods", Namespaced: true, Kind: "Pod", Verbs: metav1.Verbs{"delete", "list", "watch"}},
 | |
| 				{Name: "secrets", Namespaced: true, Kind: "Secret", Verbs: metav1.Verbs{"delete", "list", "watch"}},
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 	fakeDiscoveryClient := &fakeServerResources{
 | |
| 		PreferredResources: serverResources,
 | |
| 		Error:              nil,
 | |
| 		Lock:               sync.Mutex{},
 | |
| 		InterfaceUsedCount: 0,
 | |
| 	}
 | |
| 
 | |
| 	testHandler := &fakeActionHandler{
 | |
| 		response: map[string]FakeResponse{
 | |
| 			"GET" + "/api/v1/pods": {
 | |
| 				200,
 | |
| 				[]byte("{}"),
 | |
| 			},
 | |
| 			"GET" + "/api/v1/secrets": {
 | |
| 				404,
 | |
| 				[]byte("{}"),
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 	srv, clientConfig := testServerAndClientConfig(testHandler.ServeHTTP)
 | |
| 	defer srv.Close()
 | |
| 	clientConfig.ContentConfig.NegotiatedSerializer = nil
 | |
| 	client, err := kubernetes.NewForConfig(clientConfig)
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 
 | |
| 	rm := &testRESTMapper{testrestmapper.TestOnlyStaticRESTMapper(legacyscheme.Scheme)}
 | |
| 	metadataClient, err := metadata.NewForConfig(clientConfig)
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 
 | |
| 	sharedInformers := informers.NewSharedInformerFactory(client, 0)
 | |
| 	alwaysStarted := make(chan struct{})
 | |
| 	close(alwaysStarted)
 | |
| 	gc, err := NewGarbageCollector(client, metadataClient, rm, map[schema.GroupResource]struct{}{}, sharedInformers, alwaysStarted)
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 
 | |
| 	ctx, cancel := context.WithCancel(context.Background())
 | |
| 	defer cancel()
 | |
| 	go gc.Run(ctx, 1)
 | |
| 	// The pseudo-code of GarbageCollector.Sync():
 | |
| 	// GarbageCollector.Sync(client, period, stopCh):
 | |
| 	//    wait.Until() loops with `period` until the `stopCh` is closed :
 | |
| 	//        wait.PollImmediateUntil() loops with 100ms (hardcode) util the `stopCh` is closed:
 | |
| 	//            GetDeletableResources()
 | |
| 	//            gc.resyncMonitors()
 | |
| 	//            cache.WaitForNamedCacheSync() loops with `syncedPollPeriod` (hardcoded to 100ms), until either its stop channel is closed after `period`, or all caches synced.
 | |
| 	//
 | |
| 	// Setting the period to 200ms allows the WaitForCacheSync() to check
 | |
| 	// for cache sync ~2 times in every wait.PollImmediateUntil() loop.
 | |
| 	//
 | |
| 	// The 1s sleep in the test allows GetDeletableResources and
 | |
| 	// gc.resyncMonitors to run ~5 times to ensure the changes to the
 | |
| 	// fakeDiscoveryClient are picked up.
 | |
| 	go gc.Sync(fakeDiscoveryClient, 200*time.Millisecond, ctx.Done())
 | |
| 
 | |
| 	// Wait until the sync discovers the initial resources
 | |
| 	time.Sleep(1 * time.Second)
 | |
| 
 | |
| 	err = expectSyncNotBlocked(fakeDiscoveryClient, &gc.workerLock)
 | |
| 	if err != nil {
 | |
| 		t.Fatalf("Expected garbagecollector.Sync to be running but it is blocked: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// Simulate the discovery client returning an error
 | |
| 	fakeDiscoveryClient.setPreferredResources(nil)
 | |
| 	fakeDiscoveryClient.setError(fmt.Errorf("error calling discoveryClient.ServerPreferredResources()"))
 | |
| 
 | |
| 	// Wait until sync discovers the change
 | |
| 	time.Sleep(1 * time.Second)
 | |
| 
 | |
| 	// Remove the error from being returned and see if the garbage collector sync is still working
 | |
| 	fakeDiscoveryClient.setPreferredResources(serverResources)
 | |
| 	fakeDiscoveryClient.setError(nil)
 | |
| 
 | |
| 	err = expectSyncNotBlocked(fakeDiscoveryClient, &gc.workerLock)
 | |
| 	if err != nil {
 | |
| 		t.Fatalf("Expected garbagecollector.Sync to still be running but it is blocked: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// Simulate the discovery client returning a resource the restmapper can resolve, but will not sync caches
 | |
| 	fakeDiscoveryClient.setPreferredResources(unsyncableServerResources)
 | |
| 	fakeDiscoveryClient.setError(nil)
 | |
| 
 | |
| 	// Wait until sync discovers the change
 | |
| 	time.Sleep(1 * time.Second)
 | |
| 
 | |
| 	// Put the resources back to normal and ensure garbage collector sync recovers
 | |
| 	fakeDiscoveryClient.setPreferredResources(serverResources)
 | |
| 	fakeDiscoveryClient.setError(nil)
 | |
| 
 | |
| 	err = expectSyncNotBlocked(fakeDiscoveryClient, &gc.workerLock)
 | |
| 	if err != nil {
 | |
| 		t.Fatalf("Expected garbagecollector.Sync to still be running but it is blocked: %v", err)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func expectSyncNotBlocked(fakeDiscoveryClient *fakeServerResources, workerLock *sync.RWMutex) error {
 | |
| 	before := fakeDiscoveryClient.getInterfaceUsedCount()
 | |
| 	t := 1 * time.Second
 | |
| 	time.Sleep(t)
 | |
| 	after := fakeDiscoveryClient.getInterfaceUsedCount()
 | |
| 	if before == after {
 | |
| 		return fmt.Errorf("discoveryClient.ServerPreferredResources() called %d times over %v", after-before, t)
 | |
| 	}
 | |
| 
 | |
| 	workerLockAcquired := make(chan struct{})
 | |
| 	go func() {
 | |
| 		workerLock.Lock()
 | |
| 		defer workerLock.Unlock()
 | |
| 		close(workerLockAcquired)
 | |
| 	}()
 | |
| 	select {
 | |
| 	case <-workerLockAcquired:
 | |
| 		return nil
 | |
| 	case <-time.After(t):
 | |
| 		return fmt.Errorf("workerLock blocked for at least %v", t)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| type fakeServerResources struct {
 | |
| 	PreferredResources []*metav1.APIResourceList
 | |
| 	Error              error
 | |
| 	Lock               sync.Mutex
 | |
| 	InterfaceUsedCount int
 | |
| }
 | |
| 
 | |
| func (*fakeServerResources) ServerResourcesForGroupVersion(groupVersion string) (*metav1.APIResourceList, error) {
 | |
| 	return nil, nil
 | |
| }
 | |
| 
 | |
| // Deprecated: use ServerGroupsAndResources instead.
 | |
| func (*fakeServerResources) ServerResources() ([]*metav1.APIResourceList, error) {
 | |
| 	return nil, nil
 | |
| }
 | |
| 
 | |
| func (*fakeServerResources) ServerGroupsAndResources() ([]*metav1.APIGroup, []*metav1.APIResourceList, error) {
 | |
| 	return nil, nil, nil
 | |
| }
 | |
| 
 | |
| func (f *fakeServerResources) ServerPreferredResources() ([]*metav1.APIResourceList, error) {
 | |
| 	f.Lock.Lock()
 | |
| 	defer f.Lock.Unlock()
 | |
| 	f.InterfaceUsedCount++
 | |
| 	return f.PreferredResources, f.Error
 | |
| }
 | |
| 
 | |
| func (f *fakeServerResources) setPreferredResources(resources []*metav1.APIResourceList) {
 | |
| 	f.Lock.Lock()
 | |
| 	defer f.Lock.Unlock()
 | |
| 	f.PreferredResources = resources
 | |
| }
 | |
| 
 | |
| func (f *fakeServerResources) setError(err error) {
 | |
| 	f.Lock.Lock()
 | |
| 	defer f.Lock.Unlock()
 | |
| 	f.Error = err
 | |
| }
 | |
| 
 | |
| func (f *fakeServerResources) getInterfaceUsedCount() int {
 | |
| 	f.Lock.Lock()
 | |
| 	defer f.Lock.Unlock()
 | |
| 	return f.InterfaceUsedCount
 | |
| }
 | |
| 
 | |
| func (*fakeServerResources) ServerPreferredNamespacedResources() ([]*metav1.APIResourceList, error) {
 | |
| 	return nil, nil
 | |
| }
 | |
| 
 | |
| func TestConflictingData(t *testing.T) {
 | |
| 	pod1ns1 := makeID("v1", "Pod", "ns1", "podname1", "poduid1")
 | |
| 	pod2ns1 := makeID("v1", "Pod", "ns1", "podname2", "poduid2")
 | |
| 	pod2ns2 := makeID("v1", "Pod", "ns2", "podname2", "poduid2")
 | |
| 	node1 := makeID("v1", "Node", "", "nodename", "nodeuid1")
 | |
| 
 | |
| 	role1v1beta1 := makeID("rbac.authorization.k8s.io/v1beta1", "Role", "ns1", "role1", "roleuid1")
 | |
| 	role1v1 := makeID("rbac.authorization.k8s.io/v1", "Role", "ns1", "role1", "roleuid1")
 | |
| 
 | |
| 	deployment1apps := makeID("apps/v1", "Deployment", "ns1", "deployment1", "deploymentuid1")
 | |
| 	deployment1extensions := makeID("extensions/v1beta1", "Deployment", "ns1", "deployment1", "deploymentuid1") // not served, still referenced
 | |
| 
 | |
| 	// when a reference is made to node1 from a namespaced resource, the virtual node inserted has namespace coordinates
 | |
| 	node1WithNamespace := makeID("v1", "Node", "ns1", "nodename", "nodeuid1")
 | |
| 
 | |
| 	// when a reference is made to pod1 from a cluster-scoped resource, the virtual node inserted has no namespace
 | |
| 	pod1nonamespace := makeID("v1", "Pod", "", "podname1", "poduid1")
 | |
| 
 | |
| 	badSecretReferenceWithDeploymentUID := makeID("v1", "Secret", "ns1", "secretname", string(deployment1apps.UID))
 | |
| 	badChildPod := makeID("v1", "Pod", "ns1", "badpod", "badpoduid")
 | |
| 	goodChildPod := makeID("v1", "Pod", "ns1", "goodpod", "goodpoduid")
 | |
| 
 | |
| 	var testScenarios = []struct {
 | |
| 		name           string
 | |
| 		initialObjects []runtime.Object
 | |
| 		steps          []step
 | |
| 	}{
 | |
| 		{
 | |
| 			name: "good child in ns1 -> cluster-scoped owner",
 | |
| 			steps: []step{
 | |
| 				// setup
 | |
| 				createObjectInClient("", "v1", "nodes", "", makeMetadataObj(node1)),
 | |
| 				createObjectInClient("", "v1", "pods", "ns1", makeMetadataObj(pod1ns1, node1)),
 | |
| 				// observe namespaced child with not-yet-observed cluster-scoped parent
 | |
| 				processEvent(makeAddEvent(pod1ns1, node1)),
 | |
| 				assertState(state{
 | |
| 					graphNodes:             []*node{makeNode(pod1ns1, withOwners(node1)), makeNode(node1WithNamespace, virtual)}, // virtual node1 (matching child namespace)
 | |
| 					pendingAttemptToDelete: []*node{makeNode(node1WithNamespace, virtual)},                                       // virtual node1 queued for attempted delete
 | |
| 				}),
 | |
| 				// handle queued delete of virtual node
 | |
| 				processAttemptToDelete(1),
 | |
| 				assertState(state{
 | |
| 					clientActions:          []string{"get /v1, Resource=nodes name=nodename"},
 | |
| 					graphNodes:             []*node{makeNode(pod1ns1, withOwners(node1)), makeNode(node1WithNamespace, virtual)}, // virtual node1 (matching child namespace)
 | |
| 					pendingAttemptToDelete: []*node{makeNode(node1WithNamespace, virtual)},                                       // virtual node1 still not observed, got requeued
 | |
| 				}),
 | |
| 				// observe cluster-scoped parent
 | |
| 				processEvent(makeAddEvent(node1)),
 | |
| 				assertState(state{
 | |
| 					graphNodes:             []*node{makeNode(pod1ns1, withOwners(node1)), makeNode(node1)}, // node1 switched to observed, fixed namespace coordinate
 | |
| 					pendingAttemptToDelete: []*node{makeNode(node1WithNamespace, virtual)},                 // virtual node1 queued for attempted delete
 | |
| 				}),
 | |
| 				// handle queued delete of virtual node
 | |
| 				// final state: child and parent present in graph, no queued actions
 | |
| 				processAttemptToDelete(1),
 | |
| 				assertState(state{
 | |
| 					graphNodes: []*node{makeNode(pod1ns1, withOwners(node1)), makeNode(node1)},
 | |
| 				}),
 | |
| 			},
 | |
| 		},
 | |
| 		// child in namespace A with owner reference to namespaced type in namespace B
 | |
| 		// * should be deleted immediately
 | |
| 		// * event should be logged in namespace A with involvedObject of bad-child indicating the error
 | |
| 		{
 | |
| 			name: "bad child in ns1 -> owner in ns2 (child first)",
 | |
| 			steps: []step{
 | |
| 				// 0,1: setup
 | |
| 				createObjectInClient("", "v1", "pods", "ns1", makeMetadataObj(pod1ns1, pod2ns1)),
 | |
| 				createObjectInClient("", "v1", "pods", "ns2", makeMetadataObj(pod2ns2)),
 | |
| 				// 2,3: observe namespaced child with not-yet-observed namespace-scoped parent
 | |
| 				processEvent(makeAddEvent(pod1ns1, pod2ns2)),
 | |
| 				assertState(state{
 | |
| 					graphNodes:             []*node{makeNode(pod1ns1, withOwners(pod2ns2)), makeNode(pod2ns1, virtual)}, // virtual pod2 (matching child namespace)
 | |
| 					pendingAttemptToDelete: []*node{makeNode(pod2ns1, virtual)},                                         // virtual pod2 queued for attempted delete
 | |
| 				}),
 | |
| 				// 4,5: observe parent
 | |
| 				processEvent(makeAddEvent(pod2ns2)),
 | |
| 				assertState(state{
 | |
| 					graphNodes:             []*node{makeNode(pod1ns1, withOwners(pod2ns2)), makeNode(pod2ns2)}, // pod2 is no longer virtual, namespace coordinate is corrected
 | |
| 					pendingAttemptToDelete: []*node{makeNode(pod2ns1, virtual), makeNode(pod1ns1)},             // virtual pod2 still queued for attempted delete, bad child pod1 queued because it disagreed with observed parent
 | |
| 					events:                 []string{`Warning OwnerRefInvalidNamespace ownerRef [v1/Pod, namespace: ns1, name: podname2, uid: poduid2] does not exist in namespace "ns1" involvedObject{kind=Pod,apiVersion=v1}`},
 | |
| 				}),
 | |
| 				// 6,7: handle queued delete of virtual parent
 | |
| 				processAttemptToDelete(1),
 | |
| 				assertState(state{
 | |
| 					graphNodes:             []*node{makeNode(pod1ns1, withOwners(pod2ns2)), makeNode(pod2ns2)},
 | |
| 					pendingAttemptToDelete: []*node{makeNode(pod1ns1)}, // bad child pod1 queued because it disagreed with observed parent
 | |
| 				}),
 | |
| 				// 8,9: handle queued delete of bad child
 | |
| 				processAttemptToDelete(1),
 | |
| 				assertState(state{
 | |
| 					clientActions: []string{
 | |
| 						"get /v1, Resource=pods ns=ns1 name=podname1",    // lookup of pod1 pre-delete
 | |
| 						"get /v1, Resource=pods ns=ns1 name=podname2",    // verification bad parent reference is absent
 | |
| 						"delete /v1, Resource=pods ns=ns1 name=podname1", // pod1 delete
 | |
| 					},
 | |
| 					graphNodes:       []*node{makeNode(pod1ns1, withOwners(pod2ns2)), makeNode(pod2ns2)},
 | |
| 					absentOwnerCache: []objectReference{pod2ns1}, // cached absence of bad parent
 | |
| 				}),
 | |
| 				// 10,11: observe delete issued in step 8
 | |
| 				// final state: parent present in graph, no queued actions
 | |
| 				processEvent(makeDeleteEvent(pod1ns1)),
 | |
| 				assertState(state{
 | |
| 					graphNodes:       []*node{makeNode(pod2ns2)}, // only good parent remains
 | |
| 					absentOwnerCache: []objectReference{pod2ns1}, // cached absence of bad parent
 | |
| 				}),
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "bad child in ns1 -> owner in ns2 (owner first)",
 | |
| 			steps: []step{
 | |
| 				// 0,1: setup
 | |
| 				createObjectInClient("", "v1", "pods", "ns1", makeMetadataObj(pod1ns1, pod2ns1)),
 | |
| 				createObjectInClient("", "v1", "pods", "ns2", makeMetadataObj(pod2ns2)),
 | |
| 				// 2,3: observe parent
 | |
| 				processEvent(makeAddEvent(pod2ns2)),
 | |
| 				assertState(state{
 | |
| 					graphNodes: []*node{makeNode(pod2ns2)},
 | |
| 				}),
 | |
| 				// 4,5: observe namespaced child with invalid cross-namespace reference to parent
 | |
| 				processEvent(makeAddEvent(pod1ns1, pod2ns1)),
 | |
| 				assertState(state{
 | |
| 					graphNodes:             []*node{makeNode(pod1ns1, withOwners(pod2ns1)), makeNode(pod2ns2)},
 | |
| 					pendingAttemptToDelete: []*node{makeNode(pod1ns1)}, // bad child queued for attempted delete
 | |
| 					events:                 []string{`Warning OwnerRefInvalidNamespace ownerRef [v1/Pod, namespace: ns1, name: podname2, uid: poduid2] does not exist in namespace "ns1" involvedObject{kind=Pod,apiVersion=v1}`},
 | |
| 				}),
 | |
| 				// 6,7: handle queued delete of bad child
 | |
| 				processAttemptToDelete(1),
 | |
| 				assertState(state{
 | |
| 					clientActions: []string{
 | |
| 						"get /v1, Resource=pods ns=ns1 name=podname1",    // lookup of pod1 pre-delete
 | |
| 						"get /v1, Resource=pods ns=ns1 name=podname2",    // verification bad parent reference is absent
 | |
| 						"delete /v1, Resource=pods ns=ns1 name=podname1", // pod1 delete
 | |
| 					},
 | |
| 					graphNodes:             []*node{makeNode(pod1ns1, withOwners(pod2ns1)), makeNode(pod2ns2)},
 | |
| 					pendingAttemptToDelete: []*node{},
 | |
| 					absentOwnerCache:       []objectReference{pod2ns1}, // cached absence of bad parent
 | |
| 				}),
 | |
| 				// 8,9: observe delete issued in step 6
 | |
| 				// final state: parent present in graph, no queued actions
 | |
| 				processEvent(makeDeleteEvent(pod1ns1)),
 | |
| 				assertState(state{
 | |
| 					graphNodes:       []*node{makeNode(pod2ns2)}, // only good parent remains
 | |
| 					absentOwnerCache: []objectReference{pod2ns1}, // cached absence of bad parent
 | |
| 				}),
 | |
| 			},
 | |
| 		},
 | |
| 		// child that is cluster-scoped with owner reference to namespaced type in namespace B
 | |
| 		// * should not be deleted
 | |
| 		// * event should be logged in namespace kube-system with involvedObject of bad-child indicating the error
 | |
| 		{
 | |
| 			name: "bad cluster-scoped child -> owner in ns1 (child first)",
 | |
| 			steps: []step{
 | |
| 				// setup
 | |
| 				createObjectInClient("", "v1", "nodes", "", makeMetadataObj(node1, pod1ns1)),
 | |
| 				createObjectInClient("", "v1", "pods", "ns1", makeMetadataObj(pod1ns1)),
 | |
| 				// 2,3: observe cluster-scoped child with not-yet-observed namespaced parent
 | |
| 				processEvent(makeAddEvent(node1, pod1ns1)),
 | |
| 				assertState(state{
 | |
| 					graphNodes:             []*node{makeNode(node1, withOwners(pod1nonamespace)), makeNode(pod1nonamespace, virtual)}, // virtual pod1 (with no namespace)
 | |
| 					pendingAttemptToDelete: []*node{makeNode(pod1nonamespace, virtual)},                                               // virtual pod1 queued for attempted delete
 | |
| 				}),
 | |
| 				// 4,5: handle queued delete of virtual pod1
 | |
| 				processAttemptToDelete(1),
 | |
| 				assertState(state{
 | |
| 					graphNodes:             []*node{makeNode(node1, withOwners(pod1nonamespace)), makeNode(pod1nonamespace, virtual)}, // virtual pod1 (with no namespace)
 | |
| 					pendingAttemptToDelete: []*node{},                                                                                 // namespace-scoped virtual object without a namespace coordinate not re-queued
 | |
| 				}),
 | |
| 				// 6,7: observe namespace-scoped parent
 | |
| 				processEvent(makeAddEvent(pod1ns1)),
 | |
| 				assertState(state{
 | |
| 					graphNodes:             []*node{makeNode(node1, withOwners(pod1nonamespace)), makeNode(pod1ns1)}, // pod1 namespace coordinate corrected, made non-virtual
 | |
| 					events:                 []string{`Warning OwnerRefInvalidNamespace ownerRef [v1/Pod, namespace: , name: podname1, uid: poduid1] does not exist in namespace "" involvedObject{kind=Node,apiVersion=v1}`},
 | |
| 					pendingAttemptToDelete: []*node{makeNode(node1, withOwners(pod1ns1))}, // bad cluster-scoped child added to attemptToDelete queue
 | |
| 				}),
 | |
| 				// 8,9: handle queued attempted delete of bad cluster-scoped child
 | |
| 				// final state: parent and child present in graph, no queued actions
 | |
| 				processAttemptToDelete(1),
 | |
| 				assertState(state{
 | |
| 					clientActions: []string{
 | |
| 						"get /v1, Resource=nodes name=nodename", // lookup of node pre-delete
 | |
| 					},
 | |
| 					graphNodes: []*node{makeNode(node1, withOwners(pod1nonamespace)), makeNode(pod1ns1)},
 | |
| 				}),
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "bad cluster-scoped child -> owner in ns1 (owner first)",
 | |
| 			steps: []step{
 | |
| 				// setup
 | |
| 				createObjectInClient("", "v1", "nodes", "", makeMetadataObj(node1, pod1ns1)),
 | |
| 				createObjectInClient("", "v1", "pods", "ns1", makeMetadataObj(pod1ns1)),
 | |
| 				// 2,3: observe namespace-scoped parent
 | |
| 				processEvent(makeAddEvent(pod1ns1)),
 | |
| 				assertState(state{
 | |
| 					graphNodes: []*node{makeNode(pod1ns1)},
 | |
| 				}),
 | |
| 				// 4,5: observe cluster-scoped child
 | |
| 				processEvent(makeAddEvent(node1, pod1ns1)),
 | |
| 				assertState(state{
 | |
| 					graphNodes:             []*node{makeNode(node1, withOwners(pod1nonamespace)), makeNode(pod1ns1)},
 | |
| 					events:                 []string{`Warning OwnerRefInvalidNamespace ownerRef [v1/Pod, namespace: , name: podname1, uid: poduid1] does not exist in namespace "" involvedObject{kind=Node,apiVersion=v1}`},
 | |
| 					pendingAttemptToDelete: []*node{makeNode(node1, withOwners(pod1ns1))}, // bad cluster-scoped child added to attemptToDelete queue
 | |
| 				}),
 | |
| 				// 6,7: handle queued attempted delete of bad cluster-scoped child
 | |
| 				// final state: parent and child present in graph, no queued actions
 | |
| 				processAttemptToDelete(1),
 | |
| 				assertState(state{
 | |
| 					clientActions: []string{
 | |
| 						"get /v1, Resource=nodes name=nodename", // lookup of node pre-delete
 | |
| 					},
 | |
| 					graphNodes: []*node{makeNode(node1, withOwners(pod1nonamespace)), makeNode(pod1ns1)},
 | |
| 				}),
 | |
| 			},
 | |
| 		},
 | |
| 		// child pointing at non-preferred still-served apiVersion of parent object (e.g. rbac/v1beta1)
 | |
| 		// * should not be deleted prematurely
 | |
| 		// * should not repeatedly poll attemptToDelete while waiting
 | |
| 		// * should be deleted when the actual parent is deleted
 | |
| 		{
 | |
| 			name: "good child -> existing owner with non-preferred accessible API version",
 | |
| 			steps: []step{
 | |
| 				// setup
 | |
| 				createObjectInClient("rbac.authorization.k8s.io", "v1", "roles", "ns1", makeMetadataObj(role1v1)),
 | |
| 				createObjectInClient("rbac.authorization.k8s.io", "v1beta1", "roles", "ns1", makeMetadataObj(role1v1beta1)),
 | |
| 				createObjectInClient("", "v1", "pods", "ns1", makeMetadataObj(pod1ns1, role1v1beta1)),
 | |
| 				// 3,4: observe child
 | |
| 				processEvent(makeAddEvent(pod1ns1, role1v1beta1)),
 | |
| 				assertState(state{
 | |
| 					graphNodes:             []*node{makeNode(pod1ns1, withOwners(role1v1beta1)), makeNode(role1v1beta1, virtual)},
 | |
| 					pendingAttemptToDelete: []*node{makeNode(role1v1beta1, virtual)}, // virtual parent enqueued for delete attempt
 | |
| 				}),
 | |
| 				// 5,6: handle queued attempted delete of virtual parent
 | |
| 				processAttemptToDelete(1),
 | |
| 				assertState(state{
 | |
| 					clientActions: []string{
 | |
| 						"get rbac.authorization.k8s.io/v1beta1, Resource=roles ns=ns1 name=role1", // lookup of node pre-delete
 | |
| 					},
 | |
| 					graphNodes:             []*node{makeNode(pod1ns1, withOwners(role1v1beta1)), makeNode(role1v1beta1, virtual)},
 | |
| 					pendingAttemptToDelete: []*node{makeNode(role1v1beta1, virtual)}, // not yet observed, still in the attemptToDelete queue
 | |
| 				}),
 | |
| 				// 7,8: observe parent via v1
 | |
| 				processEvent(makeAddEvent(role1v1)),
 | |
| 				assertState(state{
 | |
| 					graphNodes:             []*node{makeNode(pod1ns1, withOwners(role1v1beta1)), makeNode(role1v1)},               // parent version/virtual state gets corrected
 | |
| 					pendingAttemptToDelete: []*node{makeNode(role1v1beta1, virtual), makeNode(pod1ns1, withOwners(role1v1beta1))}, // virtual parent and mismatched child enqueued for delete attempt
 | |
| 				}),
 | |
| 				// 9,10: process attemptToDelete
 | |
| 				// virtual node dropped from attemptToDelete with no further action because the real node has been observed now
 | |
| 				processAttemptToDelete(1),
 | |
| 				assertState(state{
 | |
| 					graphNodes:             []*node{makeNode(pod1ns1, withOwners(role1v1beta1)), makeNode(role1v1)},
 | |
| 					pendingAttemptToDelete: []*node{makeNode(pod1ns1, withOwners(role1v1beta1))}, // mismatched child enqueued for delete attempt
 | |
| 				}),
 | |
| 				// 11,12: process attemptToDelete for mismatched parent
 | |
| 				processAttemptToDelete(1),
 | |
| 				assertState(state{
 | |
| 					clientActions: []string{
 | |
| 						"get /v1, Resource=pods ns=ns1 name=podname1",                             // lookup of child pre-delete
 | |
| 						"get rbac.authorization.k8s.io/v1beta1, Resource=roles ns=ns1 name=role1", // verifying parent is solid
 | |
| 					},
 | |
| 					graphNodes: []*node{makeNode(pod1ns1, withOwners(role1v1beta1)), makeNode(role1v1)},
 | |
| 				}),
 | |
| 				// 13,14: teardown
 | |
| 				deleteObjectFromClient("rbac.authorization.k8s.io", "v1", "roles", "ns1", "role1"),
 | |
| 				deleteObjectFromClient("rbac.authorization.k8s.io", "v1beta1", "roles", "ns1", "role1"),
 | |
| 				// 15,16: observe delete via v1
 | |
| 				processEvent(makeDeleteEvent(role1v1)),
 | |
| 				assertState(state{
 | |
| 					graphNodes:             []*node{makeNode(pod1ns1, withOwners(role1v1beta1))}, // only child remains
 | |
| 					absentOwnerCache:       []objectReference{role1v1},                           // cached absence of parent via v1
 | |
| 					pendingAttemptToDelete: []*node{makeNode(pod1ns1, withOwners(role1v1beta1))},
 | |
| 				}),
 | |
| 				// 17,18: process attemptToDelete for child
 | |
| 				processAttemptToDelete(1),
 | |
| 				assertState(state{
 | |
| 					clientActions: []string{
 | |
| 						"get /v1, Resource=pods ns=ns1 name=podname1",                             // lookup of child pre-delete
 | |
| 						"get rbac.authorization.k8s.io/v1beta1, Resource=roles ns=ns1 name=role1", // verifying parent is solid
 | |
| 						"delete /v1, Resource=pods ns=ns1 name=podname1",
 | |
| 					},
 | |
| 					absentOwnerCache: []objectReference{role1v1, role1v1beta1}, // cached absence of v1beta1 role
 | |
| 					graphNodes:       []*node{makeNode(pod1ns1, withOwners(role1v1beta1))},
 | |
| 				}),
 | |
| 				// 19,20: observe delete issued in step 17
 | |
| 				// final state: empty graph, no queued actions
 | |
| 				processEvent(makeDeleteEvent(pod1ns1)),
 | |
| 				assertState(state{
 | |
| 					absentOwnerCache: []objectReference{role1v1, role1v1beta1},
 | |
| 				}),
 | |
| 			},
 | |
| 		},
 | |
| 		// child pointing at no-longer-served apiVersion of still-existing parent object (e.g. extensions/v1beta1 deployment)
 | |
| 		// * should not be deleted (this is indistinguishable from referencing an unknown kind/version)
 | |
| 		// * virtual parent should not repeatedly poll attemptToDelete once real parent is observed
 | |
| 		{
 | |
| 			name: "child -> existing owner with inaccessible API version (child first)",
 | |
| 			steps: []step{
 | |
| 				// setup
 | |
| 				createObjectInClient("apps", "v1", "deployments", "ns1", makeMetadataObj(deployment1apps)),
 | |
| 				createObjectInClient("", "v1", "pods", "ns1", makeMetadataObj(pod1ns1, deployment1extensions)),
 | |
| 				// 2,3: observe child
 | |
| 				processEvent(makeAddEvent(pod1ns1, deployment1extensions)),
 | |
| 				assertState(state{
 | |
| 					graphNodes:             []*node{makeNode(pod1ns1, withOwners(deployment1extensions)), makeNode(deployment1extensions, virtual)},
 | |
| 					pendingAttemptToDelete: []*node{makeNode(deployment1extensions, virtual)}, // virtual parent enqueued for delete attempt
 | |
| 				}),
 | |
| 				// 4,5: handle queued attempted delete of virtual parent
 | |
| 				processAttemptToDelete(1),
 | |
| 				assertState(state{
 | |
| 					graphNodes:             []*node{makeNode(pod1ns1, withOwners(deployment1extensions)), makeNode(deployment1extensions, virtual)},
 | |
| 					pendingAttemptToDelete: []*node{makeNode(deployment1extensions, virtual)}, // requeued on restmapper error
 | |
| 				}),
 | |
| 				// 6,7: observe parent via v1
 | |
| 				processEvent(makeAddEvent(deployment1apps)),
 | |
| 				assertState(state{
 | |
| 					graphNodes:             []*node{makeNode(pod1ns1, withOwners(deployment1extensions)), makeNode(deployment1apps)},                // parent version/virtual state gets corrected
 | |
| 					pendingAttemptToDelete: []*node{makeNode(deployment1extensions, virtual), makeNode(pod1ns1, withOwners(deployment1extensions))}, // virtual parent and mismatched child enqueued for delete attempt
 | |
| 				}),
 | |
| 				// 8,9: process attemptToDelete
 | |
| 				// virtual node dropped from attemptToDelete with no further action because the real node has been observed now
 | |
| 				processAttemptToDelete(1),
 | |
| 				assertState(state{
 | |
| 					graphNodes:             []*node{makeNode(pod1ns1, withOwners(deployment1extensions)), makeNode(deployment1apps)},
 | |
| 					pendingAttemptToDelete: []*node{makeNode(pod1ns1, withOwners(deployment1extensions))}, // mismatched child enqueued for delete attempt
 | |
| 				}),
 | |
| 				// 10,11: process attemptToDelete for mismatched child
 | |
| 				processAttemptToDelete(1),
 | |
| 				assertState(state{
 | |
| 					clientActions: []string{
 | |
| 						"get /v1, Resource=pods ns=ns1 name=podname1", // lookup of child pre-delete
 | |
| 					},
 | |
| 					graphNodes:             []*node{makeNode(pod1ns1, withOwners(deployment1extensions)), makeNode(deployment1apps)},
 | |
| 					pendingAttemptToDelete: []*node{makeNode(pod1ns1, withOwners(deployment1extensions))}, // mismatched child still enqueued - restmapper error
 | |
| 				}),
 | |
| 				// 12: teardown
 | |
| 				deleteObjectFromClient("apps", "v1", "deployments", "ns1", "deployment1"),
 | |
| 				// 13,14: observe delete via v1
 | |
| 				processEvent(makeDeleteEvent(deployment1apps)),
 | |
| 				assertState(state{
 | |
| 					graphNodes:             []*node{makeNode(pod1ns1, withOwners(deployment1extensions))}, // only child remains
 | |
| 					absentOwnerCache:       []objectReference{deployment1apps},                            // cached absence of parent via v1
 | |
| 					pendingAttemptToDelete: []*node{makeNode(pod1ns1, withOwners(deployment1extensions))},
 | |
| 				}),
 | |
| 				// 17,18: process attemptToDelete for child
 | |
| 				processAttemptToDelete(1),
 | |
| 				assertState(state{
 | |
| 					clientActions: []string{
 | |
| 						"get /v1, Resource=pods ns=ns1 name=podname1", // lookup of child pre-delete
 | |
| 					},
 | |
| 					graphNodes:             []*node{makeNode(pod1ns1, withOwners(deployment1extensions))}, // only child remains
 | |
| 					absentOwnerCache:       []objectReference{deployment1apps},
 | |
| 					pendingAttemptToDelete: []*node{makeNode(pod1ns1, withOwners(deployment1extensions))}, // mismatched child still enqueued - restmapper error
 | |
| 				}),
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "child -> existing owner with inaccessible API version (owner first)",
 | |
| 			steps: []step{
 | |
| 				// setup
 | |
| 				createObjectInClient("apps", "v1", "deployments", "ns1", makeMetadataObj(deployment1apps)),
 | |
| 				createObjectInClient("", "v1", "pods", "ns1", makeMetadataObj(pod1ns1, deployment1extensions)),
 | |
| 				// 2,3: observe parent via v1
 | |
| 				processEvent(makeAddEvent(deployment1apps)),
 | |
| 				assertState(state{
 | |
| 					graphNodes: []*node{makeNode(deployment1apps)},
 | |
| 				}),
 | |
| 				// 4,5: observe child
 | |
| 				processEvent(makeAddEvent(pod1ns1, deployment1extensions)),
 | |
| 				assertState(state{
 | |
| 					graphNodes:             []*node{makeNode(pod1ns1, withOwners(deployment1extensions)), makeNode(deployment1apps)},
 | |
| 					pendingAttemptToDelete: []*node{makeNode(pod1ns1, withOwners(deployment1extensions))}, // mismatched child enqueued for delete attempt
 | |
| 				}),
 | |
| 				// 6,7: process attemptToDelete for mismatched child
 | |
| 				processAttemptToDelete(1),
 | |
| 				assertState(state{
 | |
| 					clientActions: []string{
 | |
| 						"get /v1, Resource=pods ns=ns1 name=podname1", // lookup of child pre-delete
 | |
| 					},
 | |
| 					graphNodes:             []*node{makeNode(pod1ns1, withOwners(deployment1extensions)), makeNode(deployment1apps)},
 | |
| 					pendingAttemptToDelete: []*node{makeNode(pod1ns1, withOwners(deployment1extensions))}, // mismatched child still enqueued - restmapper error
 | |
| 				}),
 | |
| 				// 8: teardown
 | |
| 				deleteObjectFromClient("apps", "v1", "deployments", "ns1", "deployment1"),
 | |
| 				// 9,10: observe delete via v1
 | |
| 				processEvent(makeDeleteEvent(deployment1apps)),
 | |
| 				assertState(state{
 | |
| 					graphNodes:             []*node{makeNode(pod1ns1, withOwners(deployment1extensions))}, // only child remains
 | |
| 					absentOwnerCache:       []objectReference{deployment1apps},                            // cached absence of parent via v1
 | |
| 					pendingAttemptToDelete: []*node{makeNode(pod1ns1, withOwners(deployment1extensions))},
 | |
| 				}),
 | |
| 				// 11,12: process attemptToDelete for child
 | |
| 				// final state: child with unresolveable ownerRef remains, queued in pendingAttemptToDelete
 | |
| 				processAttemptToDelete(1),
 | |
| 				assertState(state{
 | |
| 					clientActions: []string{
 | |
| 						"get /v1, Resource=pods ns=ns1 name=podname1", // lookup of child pre-delete
 | |
| 					},
 | |
| 					graphNodes:             []*node{makeNode(pod1ns1, withOwners(deployment1extensions))}, // only child remains
 | |
| 					absentOwnerCache:       []objectReference{deployment1apps},
 | |
| 					pendingAttemptToDelete: []*node{makeNode(pod1ns1, withOwners(deployment1extensions))}, // mismatched child still enqueued - restmapper error
 | |
| 				}),
 | |
| 			},
 | |
| 		},
 | |
| 		// child pointing at no-longer-served apiVersion of no-longer-existing parent object (e.g. extensions/v1beta1 deployment)
 | |
| 		// * should not be deleted (this is indistinguishable from referencing an unknown kind/version)
 | |
| 		// * should repeatedly poll attemptToDelete
 | |
| 		// * should not block deletion of legitimate children of missing deployment
 | |
| 		{
 | |
| 			name: "child -> non-existent owner with inaccessible API version (inaccessible parent apiVersion first)",
 | |
| 			steps: []step{
 | |
| 				// setup
 | |
| 				createObjectInClient("", "v1", "pods", "ns1", makeMetadataObj(pod1ns1, deployment1extensions)),
 | |
| 				createObjectInClient("", "v1", "pods", "ns1", makeMetadataObj(pod2ns1, deployment1apps)),
 | |
| 				// 2,3: observe child pointing at no-longer-served apiVersion
 | |
| 				processEvent(makeAddEvent(pod1ns1, deployment1extensions)),
 | |
| 				assertState(state{
 | |
| 					graphNodes:             []*node{makeNode(pod1ns1, withOwners(deployment1extensions)), makeNode(deployment1extensions, virtual)},
 | |
| 					pendingAttemptToDelete: []*node{makeNode(deployment1extensions, virtual)}, // virtual parent enqueued for delete attempt
 | |
| 				}),
 | |
| 				// 4,5: observe child pointing at served apiVersion where owner does not exist
 | |
| 				processEvent(makeAddEvent(pod2ns1, deployment1apps)),
 | |
| 				assertState(state{
 | |
| 					graphNodes:             []*node{makeNode(pod1ns1, withOwners(deployment1extensions)), makeNode(deployment1extensions, virtual), makeNode(pod2ns1, withOwners(deployment1apps))},
 | |
| 					pendingAttemptToDelete: []*node{makeNode(deployment1extensions, virtual), makeNode(pod2ns1, withOwners(deployment1apps))}, // mismatched child enqueued for delete attempt
 | |
| 				}),
 | |
| 				// 6,7: handle attempt to delete virtual parent for inaccessible apiVersion
 | |
| 				processAttemptToDelete(1),
 | |
| 				assertState(state{
 | |
| 					graphNodes:             []*node{makeNode(pod1ns1, withOwners(deployment1extensions)), makeNode(deployment1extensions, virtual), makeNode(pod2ns1, withOwners(deployment1apps))},
 | |
| 					pendingAttemptToDelete: []*node{makeNode(pod2ns1, withOwners(deployment1apps)), makeNode(deployment1extensions, virtual)}, // inaccessible parent requeued to end
 | |
| 				}),
 | |
| 				// 8,9: handle attempt to delete mismatched child
 | |
| 				processAttemptToDelete(1),
 | |
| 				assertState(state{
 | |
| 					clientActions: []string{
 | |
| 						"get /v1, Resource=pods ns=ns1 name=podname2",               // lookup of child pre-delete
 | |
| 						"get apps/v1, Resource=deployments ns=ns1 name=deployment1", // lookup of parent
 | |
| 						"delete /v1, Resource=pods ns=ns1 name=podname2",            // delete child
 | |
| 					},
 | |
| 					graphNodes:             []*node{makeNode(pod1ns1, withOwners(deployment1extensions)), makeNode(deployment1extensions, virtual), makeNode(pod2ns1, withOwners(deployment1apps))},
 | |
| 					absentOwnerCache:       []objectReference{deployment1apps},                // verifiably absent parent remembered
 | |
| 					pendingAttemptToDelete: []*node{makeNode(deployment1extensions, virtual)}, // mismatched child with verifiably absent parent deleted
 | |
| 				}),
 | |
| 				// 10,11: observe delete issued in step 8
 | |
| 				processEvent(makeDeleteEvent(pod2ns1)),
 | |
| 				assertState(state{
 | |
| 					graphNodes:             []*node{makeNode(pod1ns1, withOwners(deployment1extensions)), makeNode(deployment1extensions, virtual)},
 | |
| 					absentOwnerCache:       []objectReference{deployment1apps},
 | |
| 					pendingAttemptToDelete: []*node{makeNode(deployment1extensions, virtual)},
 | |
| 				}),
 | |
| 				// 12,13: final state: inaccessible parent requeued in attemptToDelete
 | |
| 				processAttemptToDelete(1),
 | |
| 				assertState(state{
 | |
| 					graphNodes:             []*node{makeNode(pod1ns1, withOwners(deployment1extensions)), makeNode(deployment1extensions, virtual)},
 | |
| 					absentOwnerCache:       []objectReference{deployment1apps},
 | |
| 					pendingAttemptToDelete: []*node{makeNode(deployment1extensions, virtual)},
 | |
| 				}),
 | |
| 			},
 | |
| 		},
 | |
| 
 | |
| 		{
 | |
| 			name: "child -> non-existent owner with inaccessible API version (accessible parent apiVersion first)",
 | |
| 			steps: []step{
 | |
| 				// setup
 | |
| 				createObjectInClient("", "v1", "pods", "ns1", makeMetadataObj(pod1ns1, deployment1extensions)),
 | |
| 				createObjectInClient("", "v1", "pods", "ns1", makeMetadataObj(pod2ns1, deployment1apps)),
 | |
| 				// 2,3: observe child pointing at served apiVersion where owner does not exist
 | |
| 				processEvent(makeAddEvent(pod2ns1, deployment1apps)),
 | |
| 				assertState(state{
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(pod2ns1, withOwners(deployment1apps)),
 | |
| 						makeNode(deployment1apps, virtual)},
 | |
| 					pendingAttemptToDelete: []*node{
 | |
| 						makeNode(deployment1apps, virtual)}, // virtual parent enqueued for delete attempt
 | |
| 				}),
 | |
| 				// 4,5: observe child pointing at no-longer-served apiVersion
 | |
| 				processEvent(makeAddEvent(pod1ns1, deployment1extensions)),
 | |
| 				assertState(state{
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(pod2ns1, withOwners(deployment1apps)),
 | |
| 						makeNode(deployment1apps, virtual),
 | |
| 						makeNode(pod1ns1, withOwners(deployment1extensions))},
 | |
| 					pendingAttemptToDelete: []*node{
 | |
| 						makeNode(deployment1apps, virtual),
 | |
| 						makeNode(pod1ns1, withOwners(deployment1extensions))}, // mismatched child enqueued for delete attempt
 | |
| 				}),
 | |
| 				// 6,7: handle attempt to delete virtual parent for accessible apiVersion
 | |
| 				processAttemptToDelete(1),
 | |
| 				assertState(state{
 | |
| 					clientActions: []string{
 | |
| 						"get apps/v1, Resource=deployments ns=ns1 name=deployment1", // lookup of parent, gets 404
 | |
| 					},
 | |
| 					pendingGraphChanges: []*event{makeVirtualDeleteEvent(deployment1apps)}, // virtual parent not found, queued virtual delete event
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(pod2ns1, withOwners(deployment1apps)),
 | |
| 						makeNode(deployment1apps, virtual),
 | |
| 						makeNode(pod1ns1, withOwners(deployment1extensions))},
 | |
| 					pendingAttemptToDelete: []*node{makeNode(pod1ns1, withOwners(deployment1extensions))},
 | |
| 				}),
 | |
| 				// 8,9: handle attempt to delete mismatched child
 | |
| 				processAttemptToDelete(1),
 | |
| 				assertState(state{
 | |
| 					clientActions: []string{
 | |
| 						"get /v1, Resource=pods ns=ns1 name=podname1", // lookup of child pre-delete
 | |
| 					},
 | |
| 					pendingGraphChanges: []*event{makeVirtualDeleteEvent(deployment1apps)},
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(pod2ns1, withOwners(deployment1apps)),
 | |
| 						makeNode(deployment1apps, virtual),
 | |
| 						makeNode(pod1ns1, withOwners(deployment1extensions))},
 | |
| 					pendingAttemptToDelete: []*node{makeNode(pod1ns1, withOwners(deployment1extensions))}, // restmapper on inaccessible parent, requeued
 | |
| 				}),
 | |
| 				// 10,11: handle queued virtual delete event
 | |
| 				processPendingGraphChanges(1),
 | |
| 				assertState(state{
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(pod2ns1, withOwners(deployment1apps)),
 | |
| 						makeNode(deployment1extensions, virtual), // deployment node changed identity to alternative virtual identity
 | |
| 						makeNode(pod1ns1, withOwners(deployment1extensions)),
 | |
| 					},
 | |
| 					absentOwnerCache: []objectReference{deployment1apps}, // absent apps/v1 parent remembered
 | |
| 					pendingAttemptToDelete: []*node{
 | |
| 						makeNode(pod1ns1, withOwners(deployment1extensions)), // child referencing inaccessible apiVersion
 | |
| 						makeNode(pod2ns1, withOwners(deployment1apps)),       // children of absent apps/v1 parent queued for delete attempt
 | |
| 						makeNode(deployment1extensions, virtual),             // new virtual parent queued for delete attempt
 | |
| 					},
 | |
| 				}),
 | |
| 
 | |
| 				// 12,13: handle attempt to delete child referencing inaccessible apiVersion
 | |
| 				processAttemptToDelete(1),
 | |
| 				assertState(state{
 | |
| 					clientActions: []string{
 | |
| 						"get /v1, Resource=pods ns=ns1 name=podname1", // lookup of child pre-delete
 | |
| 					},
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(pod2ns1, withOwners(deployment1apps)),
 | |
| 						makeNode(deployment1extensions, virtual),
 | |
| 						makeNode(pod1ns1, withOwners(deployment1extensions))},
 | |
| 					absentOwnerCache: []objectReference{deployment1apps},
 | |
| 					pendingAttemptToDelete: []*node{
 | |
| 						makeNode(pod2ns1, withOwners(deployment1apps)),       // children of absent apps/v1 parent queued for delete attempt
 | |
| 						makeNode(deployment1extensions, virtual),             // new virtual parent queued for delete attempt
 | |
| 						makeNode(pod1ns1, withOwners(deployment1extensions)), // child referencing inaccessible apiVersion - requeued to end
 | |
| 					},
 | |
| 				}),
 | |
| 
 | |
| 				// 14,15: handle attempt to delete child referencing accessible apiVersion
 | |
| 				processAttemptToDelete(1),
 | |
| 				assertState(state{
 | |
| 					clientActions: []string{
 | |
| 						"get /v1, Resource=pods ns=ns1 name=podname2",    // lookup of child pre-delete
 | |
| 						"delete /v1, Resource=pods ns=ns1 name=podname2", // parent absent, delete
 | |
| 					},
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(pod2ns1, withOwners(deployment1apps)),
 | |
| 						makeNode(deployment1extensions, virtual),
 | |
| 						makeNode(pod1ns1, withOwners(deployment1extensions))},
 | |
| 					absentOwnerCache: []objectReference{deployment1apps},
 | |
| 					pendingAttemptToDelete: []*node{
 | |
| 						makeNode(deployment1extensions, virtual),             // new virtual parent queued for delete attempt
 | |
| 						makeNode(pod1ns1, withOwners(deployment1extensions)), // child referencing inaccessible apiVersion
 | |
| 					},
 | |
| 				}),
 | |
| 
 | |
| 				// 16,17: handle attempt to delete virtual parent in inaccessible apiVersion
 | |
| 				processAttemptToDelete(1),
 | |
| 				assertState(state{
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(pod2ns1, withOwners(deployment1apps)),
 | |
| 						makeNode(deployment1extensions, virtual),
 | |
| 						makeNode(pod1ns1, withOwners(deployment1extensions))},
 | |
| 					absentOwnerCache: []objectReference{deployment1apps},
 | |
| 					pendingAttemptToDelete: []*node{
 | |
| 						makeNode(pod1ns1, withOwners(deployment1extensions)), // child referencing inaccessible apiVersion
 | |
| 						makeNode(deployment1extensions, virtual),             // virtual parent with inaccessible apiVersion - requeued to end
 | |
| 					},
 | |
| 				}),
 | |
| 
 | |
| 				// 18,19: observe delete of pod2 from step 14
 | |
| 				// final state: virtual parent for inaccessible apiVersion and child of that parent remain in graph, queued for delete attempts with backoff
 | |
| 				processEvent(makeDeleteEvent(pod2ns1)),
 | |
| 				assertState(state{
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(deployment1extensions, virtual),
 | |
| 						makeNode(pod1ns1, withOwners(deployment1extensions))},
 | |
| 					absentOwnerCache: []objectReference{deployment1apps},
 | |
| 					pendingAttemptToDelete: []*node{
 | |
| 						makeNode(pod1ns1, withOwners(deployment1extensions)), // child referencing inaccessible apiVersion
 | |
| 						makeNode(deployment1extensions, virtual),             // virtual parent with inaccessible apiVersion
 | |
| 					},
 | |
| 				}),
 | |
| 			},
 | |
| 		},
 | |
| 		// child pointing at incorrect apiVersion/kind of still-existing parent object (e.g. core/v1 Secret with uid=123, where an apps/v1 Deployment with uid=123 exists)
 | |
| 		// * should be deleted immediately
 | |
| 		// * should not trigger deletion of legitimate children of parent
 | |
| 		{
 | |
| 			name: "bad child -> existing owner with incorrect API version (bad child, good child, bad parent delete, good parent)",
 | |
| 			steps: []step{
 | |
| 				// setup
 | |
| 				createObjectInClient("apps", "v1", "deployments", "ns1", makeMetadataObj(deployment1apps)),
 | |
| 				createObjectInClient("", "v1", "pods", "ns1", makeMetadataObj(badChildPod, badSecretReferenceWithDeploymentUID)),
 | |
| 				createObjectInClient("", "v1", "pods", "ns1", makeMetadataObj(goodChildPod, deployment1apps)),
 | |
| 				// 3,4: observe bad child
 | |
| 				processEvent(makeAddEvent(badChildPod, badSecretReferenceWithDeploymentUID)),
 | |
| 				assertState(state{
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(badChildPod, withOwners(badSecretReferenceWithDeploymentUID)),
 | |
| 						makeNode(badSecretReferenceWithDeploymentUID, virtual)},
 | |
| 					pendingAttemptToDelete: []*node{
 | |
| 						makeNode(badSecretReferenceWithDeploymentUID, virtual)}, // virtual parent enqueued for delete attempt
 | |
| 				}),
 | |
| 
 | |
| 				// 5,6: observe good child
 | |
| 				processEvent(makeAddEvent(goodChildPod, deployment1apps)),
 | |
| 				assertState(state{
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(goodChildPod, withOwners(deployment1apps)), // good child added
 | |
| 						makeNode(badChildPod, withOwners(badSecretReferenceWithDeploymentUID)),
 | |
| 						makeNode(badSecretReferenceWithDeploymentUID, virtual)},
 | |
| 					pendingAttemptToDelete: []*node{
 | |
| 						makeNode(badSecretReferenceWithDeploymentUID, virtual), // virtual parent enqueued for delete attempt
 | |
| 						makeNode(goodChildPod, withOwners(deployment1apps)),    // good child enqueued for delete attempt
 | |
| 					},
 | |
| 				}),
 | |
| 
 | |
| 				// 7,8: process pending delete of virtual parent
 | |
| 				processAttemptToDelete(1),
 | |
| 				assertState(state{
 | |
| 					clientActions: []string{
 | |
| 						"get /v1, Resource=secrets ns=ns1 name=secretname", // lookup of bad parent reference
 | |
| 					},
 | |
| 					pendingGraphChanges: []*event{makeVirtualDeleteEvent(badSecretReferenceWithDeploymentUID)}, // bad virtual parent not found, queued virtual delete event
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(goodChildPod, withOwners(deployment1apps)),
 | |
| 						makeNode(badChildPod, withOwners(badSecretReferenceWithDeploymentUID)),
 | |
| 						makeNode(badSecretReferenceWithDeploymentUID, virtual)},
 | |
| 					pendingAttemptToDelete: []*node{
 | |
| 						makeNode(goodChildPod, withOwners(deployment1apps)), // good child enqueued for delete attempt
 | |
| 					},
 | |
| 				}),
 | |
| 
 | |
| 				// 9,10: process pending delete of good child, gets 200, remains
 | |
| 				processAttemptToDelete(1),
 | |
| 				assertState(state{
 | |
| 					clientActions: []string{
 | |
| 						"get /v1, Resource=pods ns=ns1 name=goodpod",                // lookup of child pre-delete
 | |
| 						"get apps/v1, Resource=deployments ns=ns1 name=deployment1", // lookup of good parent reference, returns 200
 | |
| 					},
 | |
| 					pendingGraphChanges: []*event{makeVirtualDeleteEvent(badSecretReferenceWithDeploymentUID)}, // bad virtual parent not found, queued virtual delete event
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(goodChildPod, withOwners(deployment1apps)),
 | |
| 						makeNode(badChildPod, withOwners(badSecretReferenceWithDeploymentUID)),
 | |
| 						makeNode(badSecretReferenceWithDeploymentUID, virtual)},
 | |
| 				}),
 | |
| 
 | |
| 				// 11,12: process virtual delete event of bad parent reference
 | |
| 				processPendingGraphChanges(1),
 | |
| 				assertState(state{
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(goodChildPod, withOwners(deployment1apps)),
 | |
| 						makeNode(badChildPod, withOwners(badSecretReferenceWithDeploymentUID)),
 | |
| 						makeNode(deployment1apps, virtual)}, // parent node switched to alternate identity, still virtual
 | |
| 					absentOwnerCache: []objectReference{badSecretReferenceWithDeploymentUID}, // remember absence of bad parent coordinates
 | |
| 					pendingAttemptToDelete: []*node{
 | |
| 						makeNode(badChildPod, withOwners(badSecretReferenceWithDeploymentUID)), // child of bad parent coordinates enqueued for delete attempt
 | |
| 						makeNode(deployment1apps, virtual),                                     // new alternate virtual parent identity queued for delete attempt
 | |
| 					},
 | |
| 				}),
 | |
| 
 | |
| 				// 13,14: process pending delete of bad child
 | |
| 				processAttemptToDelete(1),
 | |
| 				assertState(state{
 | |
| 					clientActions: []string{
 | |
| 						"get /v1, Resource=pods ns=ns1 name=badpod",    // lookup of child pre-delete
 | |
| 						"delete /v1, Resource=pods ns=ns1 name=badpod", // delete of bad child (absence of bad parent is cached)
 | |
| 					},
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(goodChildPod, withOwners(deployment1apps)),
 | |
| 						makeNode(badChildPod, withOwners(badSecretReferenceWithDeploymentUID)),
 | |
| 						makeNode(deployment1apps, virtual)}, // parent node switched to alternate identity, still virtual
 | |
| 					absentOwnerCache: []objectReference{badSecretReferenceWithDeploymentUID},
 | |
| 					pendingAttemptToDelete: []*node{
 | |
| 						makeNode(deployment1apps, virtual), // new alternate virtual parent identity queued for delete attempt
 | |
| 					},
 | |
| 				}),
 | |
| 
 | |
| 				// 15,16: process pending delete of new virtual parent
 | |
| 				processAttemptToDelete(1),
 | |
| 				assertState(state{
 | |
| 					clientActions: []string{
 | |
| 						"get apps/v1, Resource=deployments ns=ns1 name=deployment1", // lookup of virtual parent, returns 200
 | |
| 					},
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(goodChildPod, withOwners(deployment1apps)),
 | |
| 						makeNode(badChildPod, withOwners(badSecretReferenceWithDeploymentUID)),
 | |
| 						makeNode(deployment1apps, virtual)}, // parent node switched to alternate identity, still virtual
 | |
| 					absentOwnerCache: []objectReference{badSecretReferenceWithDeploymentUID},
 | |
| 					pendingAttemptToDelete: []*node{
 | |
| 						makeNode(deployment1apps, virtual), // requeued, not yet observed
 | |
| 					},
 | |
| 				}),
 | |
| 
 | |
| 				// 17,18: observe good parent
 | |
| 				processEvent(makeAddEvent(deployment1apps)),
 | |
| 				assertState(state{
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(goodChildPod, withOwners(deployment1apps)),
 | |
| 						makeNode(badChildPod, withOwners(badSecretReferenceWithDeploymentUID)),
 | |
| 						makeNode(deployment1apps)}, // parent node made non-virtual
 | |
| 					absentOwnerCache: []objectReference{badSecretReferenceWithDeploymentUID},
 | |
| 					pendingAttemptToDelete: []*node{
 | |
| 						makeNode(deployment1apps), // still queued, no longer virtual
 | |
| 					},
 | |
| 				}),
 | |
| 
 | |
| 				// 19,20: observe delete of bad child from step 13
 | |
| 				processEvent(makeDeleteEvent(badChildPod, badSecretReferenceWithDeploymentUID)),
 | |
| 				assertState(state{
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(goodChildPod, withOwners(deployment1apps)),
 | |
| 						// bad child node removed
 | |
| 						makeNode(deployment1apps)},
 | |
| 					absentOwnerCache: []objectReference{badSecretReferenceWithDeploymentUID},
 | |
| 					pendingAttemptToDelete: []*node{
 | |
| 						makeNode(deployment1apps), // still queued, no longer virtual
 | |
| 					},
 | |
| 				}),
 | |
| 
 | |
| 				// 21,22: process pending delete of good parent
 | |
| 				// final state: good parent in graph with correct coordinates, good children remain, no pending deletions
 | |
| 				processAttemptToDelete(1),
 | |
| 				assertState(state{
 | |
| 					clientActions: []string{
 | |
| 						"get apps/v1, Resource=deployments ns=ns1 name=deployment1", // lookup of good parent, returns 200
 | |
| 					},
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(goodChildPod, withOwners(deployment1apps)),
 | |
| 						makeNode(deployment1apps)},
 | |
| 					absentOwnerCache: []objectReference{badSecretReferenceWithDeploymentUID},
 | |
| 				}),
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "bad child -> existing owner with incorrect API version (bad child, good child, good parent, bad parent delete)",
 | |
| 			steps: []step{
 | |
| 				// setup
 | |
| 				createObjectInClient("apps", "v1", "deployments", "ns1", makeMetadataObj(deployment1apps)),
 | |
| 				createObjectInClient("", "v1", "pods", "ns1", makeMetadataObj(badChildPod, badSecretReferenceWithDeploymentUID)),
 | |
| 				createObjectInClient("", "v1", "pods", "ns1", makeMetadataObj(goodChildPod, deployment1apps)),
 | |
| 				// 3,4: observe bad child
 | |
| 				processEvent(makeAddEvent(badChildPod, badSecretReferenceWithDeploymentUID)),
 | |
| 				assertState(state{
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(badChildPod, withOwners(badSecretReferenceWithDeploymentUID)),
 | |
| 						makeNode(badSecretReferenceWithDeploymentUID, virtual)},
 | |
| 					pendingAttemptToDelete: []*node{
 | |
| 						makeNode(badSecretReferenceWithDeploymentUID, virtual)}, // virtual parent enqueued for delete attempt
 | |
| 				}),
 | |
| 
 | |
| 				// 5,6: observe good child
 | |
| 				processEvent(makeAddEvent(goodChildPod, deployment1apps)),
 | |
| 				assertState(state{
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(goodChildPod, withOwners(deployment1apps)), // good child added
 | |
| 						makeNode(badChildPod, withOwners(badSecretReferenceWithDeploymentUID)),
 | |
| 						makeNode(badSecretReferenceWithDeploymentUID, virtual)},
 | |
| 					pendingAttemptToDelete: []*node{
 | |
| 						makeNode(badSecretReferenceWithDeploymentUID, virtual), // virtual parent enqueued for delete attempt
 | |
| 						makeNode(goodChildPod, withOwners(deployment1apps)),    // good child enqueued for delete attempt
 | |
| 					},
 | |
| 				}),
 | |
| 
 | |
| 				// 7,8: process pending delete of virtual parent
 | |
| 				processAttemptToDelete(1),
 | |
| 				assertState(state{
 | |
| 					clientActions: []string{
 | |
| 						"get /v1, Resource=secrets ns=ns1 name=secretname", // lookup of bad parent reference
 | |
| 					},
 | |
| 					pendingGraphChanges: []*event{makeVirtualDeleteEvent(badSecretReferenceWithDeploymentUID)}, // bad virtual parent not found, queued virtual delete event
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(goodChildPod, withOwners(deployment1apps)),
 | |
| 						makeNode(badChildPod, withOwners(badSecretReferenceWithDeploymentUID)),
 | |
| 						makeNode(badSecretReferenceWithDeploymentUID, virtual)},
 | |
| 					pendingAttemptToDelete: []*node{
 | |
| 						makeNode(goodChildPod, withOwners(deployment1apps)), // good child enqueued for delete attempt
 | |
| 					},
 | |
| 				}),
 | |
| 
 | |
| 				// 9,10: process pending delete of good child, gets 200, remains
 | |
| 				processAttemptToDelete(1),
 | |
| 				assertState(state{
 | |
| 					clientActions: []string{
 | |
| 						"get /v1, Resource=pods ns=ns1 name=goodpod",                // lookup of child pre-delete
 | |
| 						"get apps/v1, Resource=deployments ns=ns1 name=deployment1", // lookup of good parent reference, returns 200
 | |
| 					},
 | |
| 					pendingGraphChanges: []*event{makeVirtualDeleteEvent(badSecretReferenceWithDeploymentUID)}, // bad virtual parent not found, queued virtual delete event
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(goodChildPod, withOwners(deployment1apps)),
 | |
| 						makeNode(badChildPod, withOwners(badSecretReferenceWithDeploymentUID)),
 | |
| 						makeNode(badSecretReferenceWithDeploymentUID, virtual)},
 | |
| 				}),
 | |
| 
 | |
| 				// 11,12: good parent add event
 | |
| 				insertEvent(makeAddEvent(deployment1apps)),
 | |
| 				assertState(state{
 | |
| 					pendingGraphChanges: []*event{
 | |
| 						makeAddEvent(deployment1apps),                                // good parent observation sneaked in
 | |
| 						makeVirtualDeleteEvent(badSecretReferenceWithDeploymentUID)}, // bad virtual parent not found, queued virtual delete event
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(goodChildPod, withOwners(deployment1apps)),
 | |
| 						makeNode(badChildPod, withOwners(badSecretReferenceWithDeploymentUID)),
 | |
| 						makeNode(badSecretReferenceWithDeploymentUID, virtual)},
 | |
| 				}),
 | |
| 
 | |
| 				// 13,14: process good parent add
 | |
| 				processPendingGraphChanges(1),
 | |
| 				assertState(state{
 | |
| 					pendingGraphChanges: []*event{
 | |
| 						makeVirtualDeleteEvent(badSecretReferenceWithDeploymentUID)}, // bad virtual parent still queued virtual delete event
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(goodChildPod, withOwners(deployment1apps)),
 | |
| 						makeNode(badChildPod, withOwners(badSecretReferenceWithDeploymentUID)),
 | |
| 						makeNode(deployment1apps)}, // parent node gets fixed, no longer virtual
 | |
| 					pendingAttemptToDelete: []*node{
 | |
| 						makeNode(badChildPod, withOwners(badSecretReferenceWithDeploymentUID))}, // child of bad parent coordinates enqueued for delete attempt
 | |
| 				}),
 | |
| 
 | |
| 				// 15,16: process virtual delete event of bad parent reference
 | |
| 				processPendingGraphChanges(1),
 | |
| 				assertState(state{
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(goodChildPod, withOwners(deployment1apps)),
 | |
| 						makeNode(badChildPod, withOwners(badSecretReferenceWithDeploymentUID)),
 | |
| 						makeNode(deployment1apps)},
 | |
| 					absentOwnerCache: []objectReference{badSecretReferenceWithDeploymentUID}, // remember absence of bad parent coordinates
 | |
| 					pendingAttemptToDelete: []*node{
 | |
| 						makeNode(badChildPod, withOwners(badSecretReferenceWithDeploymentUID)), // child of bad parent coordinates enqueued for delete attempt
 | |
| 					},
 | |
| 				}),
 | |
| 
 | |
| 				// 17,18: process pending delete of bad child
 | |
| 				processAttemptToDelete(1),
 | |
| 				assertState(state{
 | |
| 					clientActions: []string{
 | |
| 						"get /v1, Resource=pods ns=ns1 name=badpod",    // lookup of child pre-delete
 | |
| 						"delete /v1, Resource=pods ns=ns1 name=badpod", // delete of bad child (absence of bad parent is cached)
 | |
| 					},
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(goodChildPod, withOwners(deployment1apps)),
 | |
| 						makeNode(badChildPod, withOwners(badSecretReferenceWithDeploymentUID)),
 | |
| 						makeNode(deployment1apps)},
 | |
| 					absentOwnerCache: []objectReference{badSecretReferenceWithDeploymentUID},
 | |
| 				}),
 | |
| 
 | |
| 				// 19,20: observe delete of bad child from step 17
 | |
| 				// final state: good parent in graph with correct coordinates, good children remain, no pending deletions
 | |
| 				processEvent(makeDeleteEvent(badChildPod, badSecretReferenceWithDeploymentUID)),
 | |
| 				assertState(state{
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(goodChildPod, withOwners(deployment1apps)),
 | |
| 						// bad child node removed
 | |
| 						makeNode(deployment1apps)},
 | |
| 					absentOwnerCache: []objectReference{badSecretReferenceWithDeploymentUID},
 | |
| 				}),
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "bad child -> existing owner with incorrect API version (good child, bad child, good parent)",
 | |
| 			steps: []step{
 | |
| 				// setup
 | |
| 				createObjectInClient("apps", "v1", "deployments", "ns1", makeMetadataObj(deployment1apps)),
 | |
| 				createObjectInClient("", "v1", "pods", "ns1", makeMetadataObj(badChildPod, badSecretReferenceWithDeploymentUID)),
 | |
| 				createObjectInClient("", "v1", "pods", "ns1", makeMetadataObj(goodChildPod, deployment1apps)),
 | |
| 				// 3,4: observe good child
 | |
| 				processEvent(makeAddEvent(goodChildPod, deployment1apps)),
 | |
| 				assertState(state{
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(goodChildPod, withOwners(deployment1apps)), // good child added
 | |
| 						makeNode(deployment1apps, virtual)},                 // virtual parent added
 | |
| 					pendingAttemptToDelete: []*node{
 | |
| 						makeNode(deployment1apps, virtual), // virtual parent enqueued for delete attempt
 | |
| 					},
 | |
| 				}),
 | |
| 
 | |
| 				// 5,6: observe bad child
 | |
| 				processEvent(makeAddEvent(badChildPod, badSecretReferenceWithDeploymentUID)),
 | |
| 				assertState(state{
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(goodChildPod, withOwners(deployment1apps)),
 | |
| 						makeNode(deployment1apps, virtual),
 | |
| 						makeNode(badChildPod, withOwners(badSecretReferenceWithDeploymentUID))}, // bad child added
 | |
| 					pendingAttemptToDelete: []*node{
 | |
| 						makeNode(deployment1apps, virtual),                                     // virtual parent enqueued for delete attempt
 | |
| 						makeNode(badChildPod, withOwners(badSecretReferenceWithDeploymentUID)), // bad child enqueued for delete attempt
 | |
| 					},
 | |
| 				}),
 | |
| 
 | |
| 				// 7,8: process pending delete of virtual parent
 | |
| 				processAttemptToDelete(1),
 | |
| 				assertState(state{
 | |
| 					clientActions: []string{
 | |
| 						"get apps/v1, Resource=deployments ns=ns1 name=deployment1", // lookup of good parent reference, returns 200
 | |
| 					},
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(goodChildPod, withOwners(deployment1apps)),
 | |
| 						makeNode(deployment1apps, virtual),
 | |
| 						makeNode(badChildPod, withOwners(badSecretReferenceWithDeploymentUID))},
 | |
| 					pendingAttemptToDelete: []*node{
 | |
| 						makeNode(badChildPod, withOwners(badSecretReferenceWithDeploymentUID)), // bad child enqueued for delete attempt
 | |
| 						makeNode(deployment1apps, virtual),                                     // virtual parent requeued to end, still virtual
 | |
| 					},
 | |
| 				}),
 | |
| 
 | |
| 				// 9,10: process pending delete of bad child
 | |
| 				processAttemptToDelete(1),
 | |
| 				assertState(state{
 | |
| 					clientActions: []string{
 | |
| 						"get /v1, Resource=pods ns=ns1 name=badpod",        // lookup of child pre-delete
 | |
| 						"get /v1, Resource=secrets ns=ns1 name=secretname", // lookup of bad parent reference, returns 404
 | |
| 						"delete /v1, Resource=pods ns=ns1 name=badpod",     // delete of bad child
 | |
| 					},
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(goodChildPod, withOwners(deployment1apps)),
 | |
| 						makeNode(deployment1apps, virtual),
 | |
| 						makeNode(badChildPod, withOwners(badSecretReferenceWithDeploymentUID))},
 | |
| 					absentOwnerCache: []objectReference{badSecretReferenceWithDeploymentUID}, // remember absence of bad parent
 | |
| 					pendingAttemptToDelete: []*node{
 | |
| 						makeNode(deployment1apps, virtual), // virtual parent requeued to end, still virtual
 | |
| 					},
 | |
| 				}),
 | |
| 
 | |
| 				// 11,12: observe good parent
 | |
| 				processEvent(makeAddEvent(deployment1apps)),
 | |
| 				assertState(state{
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(goodChildPod, withOwners(deployment1apps)),
 | |
| 						makeNode(deployment1apps), // good parent no longer virtual
 | |
| 						makeNode(badChildPod, withOwners(badSecretReferenceWithDeploymentUID))},
 | |
| 					absentOwnerCache: []objectReference{badSecretReferenceWithDeploymentUID},
 | |
| 					pendingAttemptToDelete: []*node{
 | |
| 						makeNode(deployment1apps), // parent requeued to end, no longer virtual
 | |
| 					},
 | |
| 				}),
 | |
| 
 | |
| 				// 13,14: observe delete of bad child from step 9
 | |
| 				processEvent(makeDeleteEvent(badChildPod, badSecretReferenceWithDeploymentUID)),
 | |
| 				assertState(state{
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(goodChildPod, withOwners(deployment1apps)),
 | |
| 						// bad child node removed
 | |
| 						makeNode(deployment1apps)},
 | |
| 					absentOwnerCache: []objectReference{badSecretReferenceWithDeploymentUID},
 | |
| 					pendingAttemptToDelete: []*node{
 | |
| 						makeNode(deployment1apps), // parent requeued to end, no longer virtual
 | |
| 					},
 | |
| 				}),
 | |
| 
 | |
| 				// 15,16: process pending delete of good parent
 | |
| 				// final state: good parent in graph with correct coordinates, good children remain, no pending deletions
 | |
| 				processAttemptToDelete(1),
 | |
| 				assertState(state{
 | |
| 					clientActions: []string{
 | |
| 						"get apps/v1, Resource=deployments ns=ns1 name=deployment1", // lookup of good parent, returns 200
 | |
| 					},
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(goodChildPod, withOwners(deployment1apps)),
 | |
| 						makeNode(deployment1apps)},
 | |
| 					absentOwnerCache: []objectReference{badSecretReferenceWithDeploymentUID},
 | |
| 				}),
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			// https://github.com/kubernetes/kubernetes/issues/98040
 | |
| 			name: "cluster-scoped bad child, namespaced good child, missing parent",
 | |
| 			steps: []step{
 | |
| 				// setup
 | |
| 				createObjectInClient("", "v1", "pods", "ns1", makeMetadataObj(pod2ns1, pod1ns1)),     // good child
 | |
| 				createObjectInClient("", "v1", "nodes", "", makeMetadataObj(node1, pod1nonamespace)), // bad child
 | |
| 
 | |
| 				// 2,3: observe bad child
 | |
| 				processEvent(makeAddEvent(node1, pod1nonamespace)),
 | |
| 				assertState(state{
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(node1, withOwners(pod1nonamespace)),
 | |
| 						makeNode(pod1nonamespace, virtual)},
 | |
| 					pendingAttemptToDelete: []*node{
 | |
| 						makeNode(pod1nonamespace, virtual)}, // virtual parent queued for deletion
 | |
| 				}),
 | |
| 
 | |
| 				// 4,5: observe good child
 | |
| 				processEvent(makeAddEvent(pod2ns1, pod1ns1)),
 | |
| 				assertState(state{
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(node1, withOwners(pod1nonamespace)),
 | |
| 						makeNode(pod2ns1, withOwners(pod1ns1)),
 | |
| 						makeNode(pod1nonamespace, virtual)},
 | |
| 					pendingAttemptToDelete: []*node{
 | |
| 						makeNode(pod1nonamespace, virtual),     // virtual parent queued for deletion
 | |
| 						makeNode(pod2ns1, withOwners(pod1ns1)), // mismatched child queued for deletion
 | |
| 					},
 | |
| 				}),
 | |
| 
 | |
| 				// 6,7: process attemptToDelete of bad virtual parent coordinates
 | |
| 				processAttemptToDelete(1),
 | |
| 				assertState(state{
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(node1, withOwners(pod1nonamespace)),
 | |
| 						makeNode(pod2ns1, withOwners(pod1ns1)),
 | |
| 						makeNode(pod1nonamespace, virtual)},
 | |
| 					pendingAttemptToDelete: []*node{
 | |
| 						makeNode(pod2ns1, withOwners(pod1ns1))}, // mismatched child queued for deletion
 | |
| 				}),
 | |
| 
 | |
| 				// 8,9: process attemptToDelete of good child
 | |
| 				processAttemptToDelete(1),
 | |
| 				assertState(state{
 | |
| 					clientActions: []string{
 | |
| 						"get /v1, Resource=pods ns=ns1 name=podname2",    // get good child, returns 200
 | |
| 						"get /v1, Resource=pods ns=ns1 name=podname1",    // get missing parent, returns 404
 | |
| 						"delete /v1, Resource=pods ns=ns1 name=podname2", // delete good child
 | |
| 					},
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(node1, withOwners(pod1nonamespace)),
 | |
| 						makeNode(pod2ns1, withOwners(pod1ns1)),
 | |
| 						makeNode(pod1nonamespace, virtual)},
 | |
| 					absentOwnerCache: []objectReference{pod1ns1}, // missing parent cached
 | |
| 				}),
 | |
| 
 | |
| 				// 10,11: observe deletion of good child
 | |
| 				// steady-state is bad cluster child and bad virtual parent coordinates, with no retries
 | |
| 				processEvent(makeDeleteEvent(pod2ns1, pod1ns1)),
 | |
| 				assertState(state{
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(node1, withOwners(pod1nonamespace)),
 | |
| 						makeNode(pod1nonamespace, virtual)},
 | |
| 					absentOwnerCache: []objectReference{pod1ns1},
 | |
| 				}),
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			// https://github.com/kubernetes/kubernetes/issues/98040
 | |
| 			name: "cluster-scoped bad child, namespaced good child, late observed parent",
 | |
| 			steps: []step{
 | |
| 				// setup
 | |
| 				createObjectInClient("", "v1", "pods", "ns1", makeMetadataObj(pod1ns1)),              // good parent
 | |
| 				createObjectInClient("", "v1", "pods", "ns1", makeMetadataObj(pod2ns1, pod1ns1)),     // good child
 | |
| 				createObjectInClient("", "v1", "nodes", "", makeMetadataObj(node1, pod1nonamespace)), // bad child
 | |
| 
 | |
| 				// 3,4: observe bad child
 | |
| 				processEvent(makeAddEvent(node1, pod1nonamespace)),
 | |
| 				assertState(state{
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(node1, withOwners(pod1nonamespace)),
 | |
| 						makeNode(pod1nonamespace, virtual)},
 | |
| 					pendingAttemptToDelete: []*node{
 | |
| 						makeNode(pod1nonamespace, virtual)}, // virtual parent queued for deletion
 | |
| 				}),
 | |
| 
 | |
| 				// 5,6: observe good child
 | |
| 				processEvent(makeAddEvent(pod2ns1, pod1ns1)),
 | |
| 				assertState(state{
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(node1, withOwners(pod1nonamespace)),
 | |
| 						makeNode(pod2ns1, withOwners(pod1ns1)),
 | |
| 						makeNode(pod1nonamespace, virtual)},
 | |
| 					pendingAttemptToDelete: []*node{
 | |
| 						makeNode(pod1nonamespace, virtual),      // virtual parent queued for deletion
 | |
| 						makeNode(pod2ns1, withOwners(pod1ns1))}, // mismatched child queued for deletion
 | |
| 				}),
 | |
| 
 | |
| 				// 7,8: process attemptToDelete of bad virtual parent coordinates
 | |
| 				processAttemptToDelete(1),
 | |
| 				assertState(state{
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(node1, withOwners(pod1nonamespace)),
 | |
| 						makeNode(pod2ns1, withOwners(pod1ns1)),
 | |
| 						makeNode(pod1nonamespace, virtual)},
 | |
| 					pendingAttemptToDelete: []*node{
 | |
| 						makeNode(pod2ns1, withOwners(pod1ns1))}, // mismatched child queued for deletion
 | |
| 				}),
 | |
| 
 | |
| 				// 9,10: process attemptToDelete of good child
 | |
| 				processAttemptToDelete(1),
 | |
| 				assertState(state{
 | |
| 					clientActions: []string{
 | |
| 						"get /v1, Resource=pods ns=ns1 name=podname2", // get good child, returns 200
 | |
| 						"get /v1, Resource=pods ns=ns1 name=podname1", // get late-observed parent, returns 200
 | |
| 					},
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(node1, withOwners(pod1nonamespace)),
 | |
| 						makeNode(pod2ns1, withOwners(pod1ns1)),
 | |
| 						makeNode(pod1nonamespace, virtual)},
 | |
| 				}),
 | |
| 
 | |
| 				// 11,12: late observe good parent
 | |
| 				processEvent(makeAddEvent(pod1ns1)),
 | |
| 				assertState(state{
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(node1, withOwners(pod1nonamespace)),
 | |
| 						makeNode(pod2ns1, withOwners(pod1ns1)),
 | |
| 						makeNode(pod1ns1)},
 | |
| 					// warn about bad node reference
 | |
| 					events: []string{`Warning OwnerRefInvalidNamespace ownerRef [v1/Pod, namespace: , name: podname1, uid: poduid1] does not exist in namespace "" involvedObject{kind=Node,apiVersion=v1}`},
 | |
| 					pendingAttemptToDelete: []*node{
 | |
| 						makeNode(node1, withOwners(pod1nonamespace))}, // queue bad cluster-scoped child for delete attempt
 | |
| 				}),
 | |
| 
 | |
| 				// 13,14: process attemptToDelete of bad child
 | |
| 				// steady state is bad cluster-scoped child remaining with no retries, good parent and good child in graph
 | |
| 				processAttemptToDelete(1),
 | |
| 				assertState(state{
 | |
| 					clientActions: []string{
 | |
| 						"get /v1, Resource=nodes name=nodename", // get bad child, returns 200
 | |
| 					},
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(node1, withOwners(pod1nonamespace)),
 | |
| 						makeNode(pod2ns1, withOwners(pod1ns1)),
 | |
| 						makeNode(pod1ns1)},
 | |
| 				}),
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			// https://github.com/kubernetes/kubernetes/issues/98040
 | |
| 			name: "namespaced good child, cluster-scoped bad child, missing parent",
 | |
| 			steps: []step{
 | |
| 				// setup
 | |
| 				createObjectInClient("", "v1", "pods", "ns1", makeMetadataObj(pod2ns1, pod1ns1)),     // good child
 | |
| 				createObjectInClient("", "v1", "nodes", "", makeMetadataObj(node1, pod1nonamespace)), // bad child
 | |
| 
 | |
| 				// 2,3: observe good child
 | |
| 				processEvent(makeAddEvent(pod2ns1, pod1ns1)),
 | |
| 				assertState(state{
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(pod2ns1, withOwners(pod1ns1)),
 | |
| 						makeNode(pod1ns1, virtual)},
 | |
| 					pendingAttemptToDelete: []*node{
 | |
| 						makeNode(pod1ns1, virtual)}, // virtual parent queued for deletion
 | |
| 				}),
 | |
| 
 | |
| 				// 4,5: observe bad child
 | |
| 				processEvent(makeAddEvent(node1, pod1nonamespace)),
 | |
| 				assertState(state{
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(pod2ns1, withOwners(pod1ns1)),
 | |
| 						makeNode(node1, withOwners(pod1nonamespace)),
 | |
| 						makeNode(pod1ns1, virtual)},
 | |
| 					pendingAttemptToDelete: []*node{
 | |
| 						makeNode(pod1ns1, virtual),                   // virtual parent queued for deletion
 | |
| 						makeNode(node1, withOwners(pod1nonamespace)), // mismatched child queued for deletion
 | |
| 					},
 | |
| 				}),
 | |
| 
 | |
| 				// 6,7: process attemptToDelete of good virtual parent coordinates
 | |
| 				processAttemptToDelete(1),
 | |
| 				assertState(state{
 | |
| 					clientActions: []string{
 | |
| 						"get /v1, Resource=pods ns=ns1 name=podname1", // lookup of missing parent, returns 404
 | |
| 					},
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(node1, withOwners(pod1nonamespace)),
 | |
| 						makeNode(pod2ns1, withOwners(pod1ns1)),
 | |
| 						makeNode(pod1ns1, virtual)},
 | |
| 					pendingGraphChanges: []*event{makeVirtualDeleteEvent(pod1ns1)}, // virtual parent not found, queued virtual delete event
 | |
| 					pendingAttemptToDelete: []*node{
 | |
| 						makeNode(node1, withOwners(pod1nonamespace)), // mismatched child still queued for deletion
 | |
| 					},
 | |
| 				}),
 | |
| 
 | |
| 				// 8,9: process attemptToDelete of bad cluster child
 | |
| 				processAttemptToDelete(1),
 | |
| 				assertState(state{
 | |
| 					clientActions: []string{
 | |
| 						"get /v1, Resource=nodes name=nodename", // lookup of existing node
 | |
| 					},
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(node1, withOwners(pod1nonamespace)),
 | |
| 						makeNode(pod2ns1, withOwners(pod1ns1)),
 | |
| 						makeNode(pod1ns1, virtual)},
 | |
| 					pendingGraphChanges: []*event{makeVirtualDeleteEvent(pod1ns1)}, // virtual parent virtual delete event still enqueued
 | |
| 				}),
 | |
| 
 | |
| 				// 10,11: process virtual delete event for good virtual parent coordinates
 | |
| 				processPendingGraphChanges(1),
 | |
| 				assertState(state{
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(node1, withOwners(pod1nonamespace)),
 | |
| 						makeNode(pod2ns1, withOwners(pod1ns1)),
 | |
| 						makeNode(pod1nonamespace, virtual)}, // missing virtual parent replaced with alternate coordinates, still virtual
 | |
| 					absentOwnerCache: []objectReference{pod1ns1}, // cached absence of missing parent
 | |
| 					pendingAttemptToDelete: []*node{
 | |
| 						makeNode(pod2ns1, withOwners(pod1ns1)), // good child of missing parent enqueued for deletion
 | |
| 						makeNode(pod1nonamespace, virtual),     // new virtual parent coordinates enqueued for deletion
 | |
| 					},
 | |
| 				}),
 | |
| 
 | |
| 				// 12,13: process attemptToDelete of good child
 | |
| 				processAttemptToDelete(1),
 | |
| 				assertState(state{
 | |
| 					clientActions: []string{
 | |
| 						"get /v1, Resource=pods ns=ns1 name=podname2",    // lookup of good child
 | |
| 						"delete /v1, Resource=pods ns=ns1 name=podname2", // delete of good child
 | |
| 					},
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(node1, withOwners(pod1nonamespace)),
 | |
| 						makeNode(pod2ns1, withOwners(pod1ns1)),
 | |
| 						makeNode(pod1nonamespace, virtual)},
 | |
| 					absentOwnerCache: []objectReference{pod1ns1},
 | |
| 					pendingAttemptToDelete: []*node{
 | |
| 						makeNode(pod1nonamespace, virtual), // new virtual parent coordinates enqueued for deletion
 | |
| 					},
 | |
| 				}),
 | |
| 
 | |
| 				// 14,15: observe deletion of good child
 | |
| 				processEvent(makeDeleteEvent(pod2ns1, pod1ns1)),
 | |
| 				assertState(state{
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(node1, withOwners(pod1nonamespace)),
 | |
| 						makeNode(pod1nonamespace, virtual)},
 | |
| 					absentOwnerCache: []objectReference{pod1ns1},
 | |
| 					pendingAttemptToDelete: []*node{
 | |
| 						makeNode(pod1nonamespace, virtual), // new virtual parent coordinates enqueued for deletion
 | |
| 					},
 | |
| 				}),
 | |
| 
 | |
| 				// 16,17: process attemptToDelete of bad virtual parent coordinates
 | |
| 				// steady-state is bad cluster child and bad virtual parent coordinates, with no retries
 | |
| 				processAttemptToDelete(1),
 | |
| 				assertState(state{
 | |
| 					graphNodes: []*node{
 | |
| 						makeNode(node1, withOwners(pod1nonamespace)),
 | |
| 						makeNode(pod1nonamespace, virtual)},
 | |
| 					absentOwnerCache: []objectReference{pod1ns1},
 | |
| 				}),
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	alwaysStarted := make(chan struct{})
 | |
| 	close(alwaysStarted)
 | |
| 	for _, scenario := range testScenarios {
 | |
| 		t.Run(scenario.name, func(t *testing.T) {
 | |
| 
 | |
| 			absentOwnerCache := NewReferenceCache(100)
 | |
| 
 | |
| 			eventRecorder := record.NewFakeRecorder(100)
 | |
| 			eventRecorder.IncludeObject = true
 | |
| 
 | |
| 			metadataClient := fakemetadata.NewSimpleMetadataClient(fakemetadata.NewTestScheme())
 | |
| 
 | |
| 			tweakableRM := meta.NewDefaultRESTMapper(nil)
 | |
| 			tweakableRM.AddSpecific(
 | |
| 				schema.GroupVersionKind{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "Role"},
 | |
| 				schema.GroupVersionResource{Group: "rbac.authorization.k8s.io", Version: "v1", Resource: "roles"},
 | |
| 				schema.GroupVersionResource{Group: "rbac.authorization.k8s.io", Version: "v1", Resource: "role"},
 | |
| 				meta.RESTScopeNamespace,
 | |
| 			)
 | |
| 			tweakableRM.AddSpecific(
 | |
| 				schema.GroupVersionKind{Group: "rbac.authorization.k8s.io", Version: "v1beta1", Kind: "Role"},
 | |
| 				schema.GroupVersionResource{Group: "rbac.authorization.k8s.io", Version: "v1beta1", Resource: "roles"},
 | |
| 				schema.GroupVersionResource{Group: "rbac.authorization.k8s.io", Version: "v1beta1", Resource: "role"},
 | |
| 				meta.RESTScopeNamespace,
 | |
| 			)
 | |
| 			tweakableRM.AddSpecific(
 | |
| 				schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"},
 | |
| 				schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"},
 | |
| 				schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployment"},
 | |
| 				meta.RESTScopeNamespace,
 | |
| 			)
 | |
| 			restMapper := &testRESTMapper{meta.MultiRESTMapper{tweakableRM, testrestmapper.TestOnlyStaticRESTMapper(legacyscheme.Scheme)}}
 | |
| 
 | |
| 			// set up our workqueues
 | |
| 			attemptToDelete := newTrackingWorkqueue()
 | |
| 			attemptToOrphan := newTrackingWorkqueue()
 | |
| 			graphChanges := newTrackingWorkqueue()
 | |
| 
 | |
| 			gc := &GarbageCollector{
 | |
| 				metadataClient:   metadataClient,
 | |
| 				restMapper:       restMapper,
 | |
| 				attemptToDelete:  attemptToDelete,
 | |
| 				attemptToOrphan:  attemptToOrphan,
 | |
| 				absentOwnerCache: absentOwnerCache,
 | |
| 				dependencyGraphBuilder: &GraphBuilder{
 | |
| 					eventRecorder:    eventRecorder,
 | |
| 					metadataClient:   metadataClient,
 | |
| 					informersStarted: alwaysStarted,
 | |
| 					graphChanges:     graphChanges,
 | |
| 					uidToNode: &concurrentUIDToNode{
 | |
| 						uidToNodeLock: sync.RWMutex{},
 | |
| 						uidToNode:     make(map[types.UID]*node),
 | |
| 					},
 | |
| 					attemptToDelete:  attemptToDelete,
 | |
| 					absentOwnerCache: absentOwnerCache,
 | |
| 				},
 | |
| 			}
 | |
| 
 | |
| 			ctx := stepContext{
 | |
| 				t:               t,
 | |
| 				gc:              gc,
 | |
| 				eventRecorder:   eventRecorder,
 | |
| 				metadataClient:  metadataClient,
 | |
| 				attemptToDelete: attemptToDelete,
 | |
| 				attemptToOrphan: attemptToOrphan,
 | |
| 				graphChanges:    graphChanges,
 | |
| 			}
 | |
| 			for i, s := range scenario.steps {
 | |
| 				ctx.t.Logf("%d: %s", i, s.name)
 | |
| 				s.check(ctx)
 | |
| 				if ctx.t.Failed() {
 | |
| 					return
 | |
| 				}
 | |
| 				verifyGraphInvariants(fmt.Sprintf("after step %d", i), gc.dependencyGraphBuilder.uidToNode.uidToNode, t)
 | |
| 				if ctx.t.Failed() {
 | |
| 					return
 | |
| 				}
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func makeID(groupVersion string, kind string, namespace, name, uid string) objectReference {
 | |
| 	return objectReference{
 | |
| 		OwnerReference: metav1.OwnerReference{APIVersion: groupVersion, Kind: kind, Name: name, UID: types.UID(uid)},
 | |
| 		Namespace:      namespace,
 | |
| 	}
 | |
| }
 | |
| 
 | |
| type nodeTweak func(*node) *node
 | |
| 
 | |
| func virtual(n *node) *node {
 | |
| 	n.virtual = true
 | |
| 	return n
 | |
| }
 | |
| func withOwners(ownerReferences ...objectReference) nodeTweak {
 | |
| 	return func(n *node) *node {
 | |
| 		var owners []metav1.OwnerReference
 | |
| 		for _, o := range ownerReferences {
 | |
| 			owners = append(owners, o.OwnerReference)
 | |
| 		}
 | |
| 		n.owners = owners
 | |
| 		return n
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func makeNode(identity objectReference, tweaks ...nodeTweak) *node {
 | |
| 	n := &node{identity: identity}
 | |
| 	for _, tweak := range tweaks {
 | |
| 		n = tweak(n)
 | |
| 	}
 | |
| 	return n
 | |
| }
 | |
| 
 | |
| func makeAddEvent(identity objectReference, owners ...objectReference) *event {
 | |
| 	gv, err := schema.ParseGroupVersion(identity.APIVersion)
 | |
| 	if err != nil {
 | |
| 		panic(err)
 | |
| 	}
 | |
| 	return &event{
 | |
| 		eventType: addEvent,
 | |
| 		gvk:       gv.WithKind(identity.Kind),
 | |
| 		obj:       makeObj(identity, owners...),
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func makeVirtualDeleteEvent(identity objectReference, owners ...objectReference) *event {
 | |
| 	e := makeDeleteEvent(identity, owners...)
 | |
| 	e.virtual = true
 | |
| 	return e
 | |
| }
 | |
| 
 | |
| func makeDeleteEvent(identity objectReference, owners ...objectReference) *event {
 | |
| 	gv, err := schema.ParseGroupVersion(identity.APIVersion)
 | |
| 	if err != nil {
 | |
| 		panic(err)
 | |
| 	}
 | |
| 	return &event{
 | |
| 		eventType: deleteEvent,
 | |
| 		gvk:       gv.WithKind(identity.Kind),
 | |
| 		obj:       makeObj(identity, owners...),
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func makeObj(identity objectReference, owners ...objectReference) *metaonly.MetadataOnlyObject {
 | |
| 	obj := &metaonly.MetadataOnlyObject{
 | |
| 		TypeMeta:   metav1.TypeMeta{APIVersion: identity.APIVersion, Kind: identity.Kind},
 | |
| 		ObjectMeta: metav1.ObjectMeta{Namespace: identity.Namespace, UID: identity.UID, Name: identity.Name},
 | |
| 	}
 | |
| 	for _, owner := range owners {
 | |
| 		obj.ObjectMeta.OwnerReferences = append(obj.ObjectMeta.OwnerReferences, owner.OwnerReference)
 | |
| 	}
 | |
| 	return obj
 | |
| }
 | |
| 
 | |
| func makeMetadataObj(identity objectReference, owners ...objectReference) *metav1.PartialObjectMetadata {
 | |
| 	obj := &metav1.PartialObjectMetadata{
 | |
| 		TypeMeta:   metav1.TypeMeta{APIVersion: identity.APIVersion, Kind: identity.Kind},
 | |
| 		ObjectMeta: metav1.ObjectMeta{Namespace: identity.Namespace, UID: identity.UID, Name: identity.Name},
 | |
| 	}
 | |
| 	for _, owner := range owners {
 | |
| 		obj.ObjectMeta.OwnerReferences = append(obj.ObjectMeta.OwnerReferences, owner.OwnerReference)
 | |
| 	}
 | |
| 	return obj
 | |
| }
 | |
| 
 | |
| type stepContext struct {
 | |
| 	t               *testing.T
 | |
| 	gc              *GarbageCollector
 | |
| 	eventRecorder   *record.FakeRecorder
 | |
| 	metadataClient  *fakemetadata.FakeMetadataClient
 | |
| 	attemptToDelete *trackingWorkqueue
 | |
| 	attemptToOrphan *trackingWorkqueue
 | |
| 	graphChanges    *trackingWorkqueue
 | |
| }
 | |
| 
 | |
| type step struct {
 | |
| 	name  string
 | |
| 	check func(stepContext)
 | |
| }
 | |
| 
 | |
| func processPendingGraphChanges(count int) step {
 | |
| 	return step{
 | |
| 		name: "processPendingGraphChanges",
 | |
| 		check: func(ctx stepContext) {
 | |
| 			ctx.t.Helper()
 | |
| 			if count <= 0 {
 | |
| 				// process all
 | |
| 				for ctx.gc.dependencyGraphBuilder.graphChanges.Len() != 0 {
 | |
| 					ctx.gc.dependencyGraphBuilder.processGraphChanges()
 | |
| 				}
 | |
| 			} else {
 | |
| 				for i := 0; i < count; i++ {
 | |
| 					if ctx.gc.dependencyGraphBuilder.graphChanges.Len() == 0 {
 | |
| 						ctx.t.Errorf("expected at least %d pending changes, got %d", count, i+1)
 | |
| 						return
 | |
| 					}
 | |
| 					ctx.gc.dependencyGraphBuilder.processGraphChanges()
 | |
| 				}
 | |
| 			}
 | |
| 		},
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func processAttemptToDelete(count int) step {
 | |
| 	return step{
 | |
| 		name: "processAttemptToDelete",
 | |
| 		check: func(ctx stepContext) {
 | |
| 			ctx.t.Helper()
 | |
| 			if count <= 0 {
 | |
| 				// process all
 | |
| 				for ctx.gc.dependencyGraphBuilder.attemptToDelete.Len() != 0 {
 | |
| 					ctx.gc.processAttemptToDeleteWorker(context.TODO())
 | |
| 				}
 | |
| 			} else {
 | |
| 				for i := 0; i < count; i++ {
 | |
| 					if ctx.gc.dependencyGraphBuilder.attemptToDelete.Len() == 0 {
 | |
| 						ctx.t.Errorf("expected at least %d pending changes, got %d", count, i+1)
 | |
| 						return
 | |
| 					}
 | |
| 					ctx.gc.processAttemptToDeleteWorker(context.TODO())
 | |
| 				}
 | |
| 			}
 | |
| 		},
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func insertEvent(e *event) step {
 | |
| 	return step{
 | |
| 		name: "insertEvent",
 | |
| 		check: func(ctx stepContext) {
 | |
| 			ctx.t.Helper()
 | |
| 			// drain queue into items
 | |
| 			var items []interface{}
 | |
| 			for ctx.gc.dependencyGraphBuilder.graphChanges.Len() > 0 {
 | |
| 				item, _ := ctx.gc.dependencyGraphBuilder.graphChanges.Get()
 | |
| 				ctx.gc.dependencyGraphBuilder.graphChanges.Done(item)
 | |
| 				items = append(items, item)
 | |
| 			}
 | |
| 
 | |
| 			// add the new event
 | |
| 			ctx.gc.dependencyGraphBuilder.graphChanges.Add(e)
 | |
| 
 | |
| 			// reappend the items
 | |
| 			for _, item := range items {
 | |
| 				ctx.gc.dependencyGraphBuilder.graphChanges.Add(item)
 | |
| 			}
 | |
| 		},
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func processEvent(e *event) step {
 | |
| 	return step{
 | |
| 		name: "processEvent",
 | |
| 		check: func(ctx stepContext) {
 | |
| 			ctx.t.Helper()
 | |
| 			if ctx.gc.dependencyGraphBuilder.graphChanges.Len() != 0 {
 | |
| 				ctx.t.Fatalf("events present in graphChanges, must process pending graphChanges before calling processEvent")
 | |
| 			}
 | |
| 			ctx.gc.dependencyGraphBuilder.graphChanges.Add(e)
 | |
| 			ctx.gc.dependencyGraphBuilder.processGraphChanges()
 | |
| 		},
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func createObjectInClient(group, version, resource, namespace string, obj *metav1.PartialObjectMetadata) step {
 | |
| 	return step{
 | |
| 		name: "createObjectInClient",
 | |
| 		check: func(ctx stepContext) {
 | |
| 			ctx.t.Helper()
 | |
| 			if len(ctx.metadataClient.Actions()) > 0 {
 | |
| 				ctx.t.Fatal("cannot call createObjectInClient with pending client actions, call assertClientActions to check and clear first")
 | |
| 			}
 | |
| 			gvr := schema.GroupVersionResource{Group: group, Version: version, Resource: resource}
 | |
| 			var c fakemetadata.MetadataClient
 | |
| 			if namespace == "" {
 | |
| 				c = ctx.metadataClient.Resource(gvr).(fakemetadata.MetadataClient)
 | |
| 			} else {
 | |
| 				c = ctx.metadataClient.Resource(gvr).Namespace(namespace).(fakemetadata.MetadataClient)
 | |
| 			}
 | |
| 			if _, err := c.CreateFake(obj, metav1.CreateOptions{}); err != nil {
 | |
| 				ctx.t.Fatal(err)
 | |
| 			}
 | |
| 			ctx.metadataClient.ClearActions()
 | |
| 		},
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func deleteObjectFromClient(group, version, resource, namespace, name string) step {
 | |
| 	return step{
 | |
| 		name: "deleteObjectFromClient",
 | |
| 		check: func(ctx stepContext) {
 | |
| 			ctx.t.Helper()
 | |
| 			if len(ctx.metadataClient.Actions()) > 0 {
 | |
| 				ctx.t.Fatal("cannot call deleteObjectFromClient with pending client actions, call assertClientActions to check and clear first")
 | |
| 			}
 | |
| 			gvr := schema.GroupVersionResource{Group: group, Version: version, Resource: resource}
 | |
| 			var c fakemetadata.MetadataClient
 | |
| 			if namespace == "" {
 | |
| 				c = ctx.metadataClient.Resource(gvr).(fakemetadata.MetadataClient)
 | |
| 			} else {
 | |
| 				c = ctx.metadataClient.Resource(gvr).Namespace(namespace).(fakemetadata.MetadataClient)
 | |
| 			}
 | |
| 			if err := c.Delete(context.TODO(), name, metav1.DeleteOptions{}); err != nil {
 | |
| 				ctx.t.Fatal(err)
 | |
| 			}
 | |
| 			ctx.metadataClient.ClearActions()
 | |
| 		},
 | |
| 	}
 | |
| }
 | |
| 
 | |
| type state struct {
 | |
| 	events                 []string
 | |
| 	clientActions          []string
 | |
| 	graphNodes             []*node
 | |
| 	pendingGraphChanges    []*event
 | |
| 	pendingAttemptToDelete []*node
 | |
| 	pendingAttemptToOrphan []*node
 | |
| 	absentOwnerCache       []objectReference
 | |
| }
 | |
| 
 | |
| func assertState(s state) step {
 | |
| 	return step{
 | |
| 		name: "assertState",
 | |
| 		check: func(ctx stepContext) {
 | |
| 			ctx.t.Helper()
 | |
| 
 | |
| 			{
 | |
| 				for _, absent := range s.absentOwnerCache {
 | |
| 					if !ctx.gc.absentOwnerCache.Has(absent) {
 | |
| 						ctx.t.Errorf("expected absent owner %s was not in the absentOwnerCache", absent)
 | |
| 					}
 | |
| 				}
 | |
| 				if len(s.absentOwnerCache) != ctx.gc.absentOwnerCache.cache.Len() {
 | |
| 					// only way to inspect is to drain them all, but that's ok because we're failing the test anyway
 | |
| 					ctx.gc.absentOwnerCache.cache.OnEvicted = func(key lru.Key, item interface{}) {
 | |
| 						found := false
 | |
| 						for _, absent := range s.absentOwnerCache {
 | |
| 							if absent == key {
 | |
| 								found = true
 | |
| 								break
 | |
| 							}
 | |
| 						}
 | |
| 						if !found {
 | |
| 							ctx.t.Errorf("unexpected item in absent owner cache: %s", key)
 | |
| 						}
 | |
| 					}
 | |
| 					ctx.gc.absentOwnerCache.cache.Clear()
 | |
| 					ctx.t.Error("unexpected items in absent owner cache")
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			{
 | |
| 				var actualEvents []string
 | |
| 				// drain sent events
 | |
| 			loop:
 | |
| 				for {
 | |
| 					select {
 | |
| 					case event := <-ctx.eventRecorder.Events:
 | |
| 						actualEvents = append(actualEvents, event)
 | |
| 					default:
 | |
| 						break loop
 | |
| 					}
 | |
| 				}
 | |
| 				if !reflect.DeepEqual(actualEvents, s.events) {
 | |
| 					ctx.t.Logf("expected:\n%s", strings.Join(s.events, "\n"))
 | |
| 					ctx.t.Logf("actual:\n%s", strings.Join(actualEvents, "\n"))
 | |
| 					ctx.t.Fatalf("did not get expected events")
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			{
 | |
| 				var actualClientActions []string
 | |
| 				for _, action := range ctx.metadataClient.Actions() {
 | |
| 					s := fmt.Sprintf("%s %s", action.GetVerb(), action.GetResource())
 | |
| 					if action.GetNamespace() != "" {
 | |
| 						s += " ns=" + action.GetNamespace()
 | |
| 					}
 | |
| 					if get, ok := action.(clientgotesting.GetAction); ok && get.GetName() != "" {
 | |
| 						s += " name=" + get.GetName()
 | |
| 					}
 | |
| 					actualClientActions = append(actualClientActions, s)
 | |
| 				}
 | |
| 				if (len(s.clientActions) > 0 || len(actualClientActions) > 0) && !reflect.DeepEqual(s.clientActions, actualClientActions) {
 | |
| 					ctx.t.Logf("expected:\n%s", strings.Join(s.clientActions, "\n"))
 | |
| 					ctx.t.Logf("actual:\n%s", strings.Join(actualClientActions, "\n"))
 | |
| 					ctx.t.Fatalf("did not get expected client actions")
 | |
| 				}
 | |
| 				ctx.metadataClient.ClearActions()
 | |
| 			}
 | |
| 
 | |
| 			{
 | |
| 				if l := len(ctx.gc.dependencyGraphBuilder.uidToNode.uidToNode); l != len(s.graphNodes) {
 | |
| 					ctx.t.Errorf("expected %d nodes, got %d", len(s.graphNodes), l)
 | |
| 				}
 | |
| 				for _, n := range s.graphNodes {
 | |
| 					graphNode, ok := ctx.gc.dependencyGraphBuilder.uidToNode.Read(n.identity.UID)
 | |
| 					if !ok {
 | |
| 						ctx.t.Errorf("%s: no node in graph with uid=%s", n.identity.UID, n.identity.UID)
 | |
| 						continue
 | |
| 					}
 | |
| 					if graphNode.identity != n.identity {
 | |
| 						ctx.t.Errorf("%s: expected identity %v, got %v", n.identity.UID, n.identity, graphNode.identity)
 | |
| 					}
 | |
| 					if graphNode.virtual != n.virtual {
 | |
| 						ctx.t.Errorf("%s: expected virtual %v, got %v", n.identity.UID, n.virtual, graphNode.virtual)
 | |
| 					}
 | |
| 					if (len(graphNode.owners) > 0 || len(n.owners) > 0) && !reflect.DeepEqual(graphNode.owners, n.owners) {
 | |
| 						expectedJSON, _ := json.Marshal(n.owners)
 | |
| 						actualJSON, _ := json.Marshal(graphNode.owners)
 | |
| 						ctx.t.Errorf("%s: expected owners %s, got %s", n.identity.UID, expectedJSON, actualJSON)
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			{
 | |
| 				for i := range s.pendingGraphChanges {
 | |
| 					e := s.pendingGraphChanges[i]
 | |
| 					if len(ctx.graphChanges.pendingList) < i+1 {
 | |
| 						ctx.t.Errorf("graphChanges: expected %d events, got %d", len(s.pendingGraphChanges), ctx.graphChanges.Len())
 | |
| 						break
 | |
| 					}
 | |
| 
 | |
| 					a := ctx.graphChanges.pendingList[i].(*event)
 | |
| 					if !reflect.DeepEqual(e, a) {
 | |
| 						objectDiff := ""
 | |
| 						if !reflect.DeepEqual(e.obj, a.obj) {
 | |
| 							objectDiff = "\nobjectDiff:\n" + cmp.Diff(e.obj, a.obj)
 | |
| 						}
 | |
| 						oldObjectDiff := ""
 | |
| 						if !reflect.DeepEqual(e.oldObj, a.oldObj) {
 | |
| 							oldObjectDiff = "\noldObjectDiff:\n" + cmp.Diff(e.oldObj, a.oldObj)
 | |
| 						}
 | |
| 						ctx.t.Errorf("graphChanges[%d]: expected\n%#v\ngot\n%#v%s%s", i, e, a, objectDiff, oldObjectDiff)
 | |
| 					}
 | |
| 				}
 | |
| 				if ctx.graphChanges.Len() > len(s.pendingGraphChanges) {
 | |
| 					for i, a := range ctx.graphChanges.pendingList[len(s.pendingGraphChanges):] {
 | |
| 						ctx.t.Errorf("graphChanges[%d]: unexpected event: %v", len(s.pendingGraphChanges)+i, a)
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			{
 | |
| 				for i := range s.pendingAttemptToDelete {
 | |
| 					e := s.pendingAttemptToDelete[i].identity
 | |
| 					e_virtual := s.pendingAttemptToDelete[i].virtual
 | |
| 					if ctx.attemptToDelete.Len() < i+1 {
 | |
| 						ctx.t.Errorf("attemptToDelete: expected %d events, got %d", len(s.pendingAttemptToDelete), ctx.attemptToDelete.Len())
 | |
| 						break
 | |
| 					}
 | |
| 					a := ctx.attemptToDelete.pendingList[i].(*node).identity
 | |
| 					a_virtual := ctx.attemptToDelete.pendingList[i].(*node).virtual
 | |
| 					if !reflect.DeepEqual(e, a) {
 | |
| 						ctx.t.Errorf("attemptToDelete[%d]: expected %v, got %v", i, e, a)
 | |
| 					}
 | |
| 					if e_virtual != a_virtual {
 | |
| 						ctx.t.Errorf("attemptToDelete[%d]: expected virtual node %v, got non-virtual node %v", i, e, a)
 | |
| 					}
 | |
| 				}
 | |
| 				if ctx.attemptToDelete.Len() > len(s.pendingAttemptToDelete) {
 | |
| 					for i, a := range ctx.attemptToDelete.pendingList[len(s.pendingAttemptToDelete):] {
 | |
| 						ctx.t.Errorf("attemptToDelete[%d]: unexpected node: %v", len(s.pendingAttemptToDelete)+i, a.(*node).identity)
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			{
 | |
| 				for i := range s.pendingAttemptToOrphan {
 | |
| 					e := s.pendingAttemptToOrphan[i].identity
 | |
| 					if ctx.attemptToOrphan.Len() < i+1 {
 | |
| 						ctx.t.Errorf("attemptToOrphan: expected %d events, got %d", len(s.pendingAttemptToOrphan), ctx.attemptToOrphan.Len())
 | |
| 						break
 | |
| 					}
 | |
| 					a := ctx.attemptToOrphan.pendingList[i].(*node).identity
 | |
| 					if !reflect.DeepEqual(e, a) {
 | |
| 						ctx.t.Errorf("attemptToOrphan[%d]: expected %v, got %v", i, e, a)
 | |
| 					}
 | |
| 				}
 | |
| 				if ctx.attemptToOrphan.Len() > len(s.pendingAttemptToOrphan) {
 | |
| 					for i, a := range ctx.attemptToOrphan.pendingList[len(s.pendingAttemptToOrphan):] {
 | |
| 						ctx.t.Errorf("attemptToOrphan[%d]: unexpected node: %v", len(s.pendingAttemptToOrphan)+i, a.(*node).identity)
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| }
 | |
| 
 | |
| // trackingWorkqueue implements RateLimitingInterface,
 | |
| // allows introspection of the items in the queue,
 | |
| // and treats AddAfter and AddRateLimited the same as Add
 | |
| // so they are always synchronous.
 | |
| type trackingWorkqueue struct {
 | |
| 	limiter     workqueue.RateLimitingInterface
 | |
| 	pendingList []interface{}
 | |
| 	pendingMap  map[interface{}]struct{}
 | |
| }
 | |
| 
 | |
| var _ = workqueue.RateLimitingInterface(&trackingWorkqueue{})
 | |
| 
 | |
| func newTrackingWorkqueue() *trackingWorkqueue {
 | |
| 	return &trackingWorkqueue{
 | |
| 		limiter:    workqueue.NewRateLimitingQueue(&workqueue.BucketRateLimiter{Limiter: rate.NewLimiter(rate.Inf, 100)}),
 | |
| 		pendingMap: map[interface{}]struct{}{},
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (t *trackingWorkqueue) Add(item interface{}) {
 | |
| 	t.queue(item)
 | |
| 	t.limiter.Add(item)
 | |
| }
 | |
| func (t *trackingWorkqueue) AddAfter(item interface{}, duration time.Duration) {
 | |
| 	t.Add(item)
 | |
| }
 | |
| func (t *trackingWorkqueue) AddRateLimited(item interface{}) {
 | |
| 	t.Add(item)
 | |
| }
 | |
| func (t *trackingWorkqueue) Get() (interface{}, bool) {
 | |
| 	item, shutdown := t.limiter.Get()
 | |
| 	t.dequeue(item)
 | |
| 	return item, shutdown
 | |
| }
 | |
| func (t *trackingWorkqueue) Done(item interface{}) {
 | |
| 	t.limiter.Done(item)
 | |
| }
 | |
| func (t *trackingWorkqueue) Forget(item interface{}) {
 | |
| 	t.limiter.Forget(item)
 | |
| }
 | |
| func (t *trackingWorkqueue) NumRequeues(item interface{}) int {
 | |
| 	return 0
 | |
| }
 | |
| func (t *trackingWorkqueue) Len() int {
 | |
| 	if e, a := len(t.pendingList), len(t.pendingMap); e != a {
 | |
| 		panic(fmt.Errorf("pendingList != pendingMap: %d / %d", e, a))
 | |
| 	}
 | |
| 	if e, a := len(t.pendingList), t.limiter.Len(); e != a {
 | |
| 		panic(fmt.Errorf("pendingList != limiter.Len(): %d / %d", e, a))
 | |
| 	}
 | |
| 	return len(t.pendingList)
 | |
| }
 | |
| func (t *trackingWorkqueue) ShutDown() {
 | |
| 	t.limiter.ShutDown()
 | |
| }
 | |
| func (t *trackingWorkqueue) ShutDownWithDrain() {
 | |
| 	t.limiter.ShutDownWithDrain()
 | |
| }
 | |
| func (t *trackingWorkqueue) ShuttingDown() bool {
 | |
| 	return t.limiter.ShuttingDown()
 | |
| }
 | |
| 
 | |
| func (t *trackingWorkqueue) queue(item interface{}) {
 | |
| 	if _, queued := t.pendingMap[item]; queued {
 | |
| 		// fmt.Printf("already queued: %#v\n", item)
 | |
| 		return
 | |
| 	}
 | |
| 	t.pendingMap[item] = struct{}{}
 | |
| 	t.pendingList = append(t.pendingList, item)
 | |
| }
 | |
| func (t *trackingWorkqueue) dequeue(item interface{}) {
 | |
| 	if _, queued := t.pendingMap[item]; !queued {
 | |
| 		// fmt.Printf("not queued: %#v\n", item)
 | |
| 		return
 | |
| 	}
 | |
| 	delete(t.pendingMap, item)
 | |
| 	newPendingList := []interface{}{}
 | |
| 	for _, p := range t.pendingList {
 | |
| 		if p == item {
 | |
| 			continue
 | |
| 		}
 | |
| 		newPendingList = append(newPendingList, p)
 | |
| 	}
 | |
| 	t.pendingList = newPendingList
 | |
| }
 |