package cache import ( "context" "errors" "net" "net/url" "sync" "testing" "time" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "github.com/argoproj/argo-cd/gitops-engine/pkg/cache" "github.com/argoproj/argo-cd/gitops-engine/pkg/cache/mocks" "github.com/argoproj/argo-cd/gitops-engine/pkg/health" "github.com/argoproj/argo-cd/gitops-engine/pkg/utils/kube" "github.com/stretchr/testify/mock" "k8s.io/client-go/kubernetes/fake" "github.com/argoproj/argo-cd/v3/common" "github.com/argoproj/argo-cd/v3/controller/metrics" "github.com/argoproj/argo-cd/v3/controller/sharding" "github.com/argoproj/argo-cd/v3/pkg/apis/application" appv1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1" dbmocks "github.com/argoproj/argo-cd/v3/util/db/mocks" argosettings "github.com/argoproj/argo-cd/v3/util/settings" ) type netError string func (n netError) Error() string { return string(n) } func (n netError) Timeout() bool { return false } func (n netError) Temporary() bool { return false } func fixtures(ctx context.Context, data map[string]string, opts ...func(secret *corev1.Secret)) (*fake.Clientset, *argosettings.SettingsManager) { cm := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: common.ArgoCDConfigMapName, Namespace: "default", Labels: map[string]string{ "app.kubernetes.io/part-of": "argocd", }, }, Data: data, } secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: common.ArgoCDSecretName, Namespace: "default", Labels: map[string]string{ "app.kubernetes.io/part-of": "argocd", }, }, Data: map[string][]byte{}, } for i := range opts { opts[i](secret) } kubeClient := fake.NewClientset(cm, secret) settingsManager := argosettings.NewSettingsManager(ctx, kubeClient, "default") return kubeClient, settingsManager } func TestHandleModEvent_HasChanges(_ *testing.T) { clusterCache := &mocks.ClusterCache{} clusterCache.EXPECT().Invalidate(mock.Anything, mock.Anything).Return().Once() clusterCache.EXPECT().EnsureSynced().Return(nil).Once() db := &dbmocks.ArgoDB{} db.EXPECT().GetApplicationControllerReplicas().Return(1) clustersCache := liveStateCache{ clusters: map[string]cache.ClusterCache{ "https://mycluster": clusterCache, }, clusterSharding: sharding.NewClusterSharding(db, 0, 1, common.DefaultShardingAlgorithm), } clustersCache.handleModEvent(&appv1.Cluster{ Server: "https://mycluster", Config: appv1.ClusterConfig{Username: "foo"}, }, &appv1.Cluster{ Server: "https://mycluster", Config: appv1.ClusterConfig{Username: "bar"}, Namespaces: []string{"default"}, }) } func TestHandleModEvent_ClusterExcluded(t *testing.T) { clusterCache := &mocks.ClusterCache{} clusterCache.EXPECT().Invalidate(mock.Anything, mock.Anything).Return().Once() clusterCache.EXPECT().EnsureSynced().Return(nil).Once() db := &dbmocks.ArgoDB{} db.EXPECT().GetApplicationControllerReplicas().Return(1).Maybe() clustersCache := liveStateCache{ db: nil, appInformer: nil, onObjectUpdated: func(_ map[string]bool, _ corev1.ObjectReference) { }, settingsMgr: &argosettings.SettingsManager{}, metricsServer: &metrics.MetricsServer{}, // returns a shard that never process any cluster clusterSharding: sharding.NewClusterSharding(db, 0, 1, common.DefaultShardingAlgorithm), resourceTracking: nil, clusters: map[string]cache.ClusterCache{"https://mycluster": clusterCache}, cacheSettings: cacheSettings{}, lock: sync.RWMutex{}, } clustersCache.handleModEvent(&appv1.Cluster{ Server: "https://mycluster", Config: appv1.ClusterConfig{Username: "foo"}, }, &appv1.Cluster{ Server: "https://mycluster", Config: appv1.ClusterConfig{Username: "bar"}, Namespaces: []string{"default"}, }) assert.Len(t, clustersCache.clusters, 1) } func TestHandleModEvent_NoChanges(_ *testing.T) { clusterCache := &mocks.ClusterCache{} clusterCache.EXPECT().Invalidate(mock.Anything).Panic("should not invalidate").Maybe() clusterCache.EXPECT().EnsureSynced().Return(nil).Panic("should not re-sync").Maybe() db := &dbmocks.ArgoDB{} db.EXPECT().GetApplicationControllerReplicas().Return(1).Maybe() clustersCache := liveStateCache{ clusters: map[string]cache.ClusterCache{ "https://mycluster": clusterCache, }, clusterSharding: sharding.NewClusterSharding(db, 0, 1, common.DefaultShardingAlgorithm), } clustersCache.handleModEvent(&appv1.Cluster{ Server: "https://mycluster", Config: appv1.ClusterConfig{Username: "bar"}, }, &appv1.Cluster{ Server: "https://mycluster", Config: appv1.ClusterConfig{Username: "bar"}, }) } func TestHandleAddEvent_ClusterExcluded(t *testing.T) { db := &dbmocks.ArgoDB{} db.EXPECT().GetApplicationControllerReplicas().Return(1).Maybe() clustersCache := liveStateCache{ clusters: map[string]cache.ClusterCache{}, clusterSharding: sharding.NewClusterSharding(db, 0, 2, common.DefaultShardingAlgorithm), } clustersCache.handleAddEvent(&appv1.Cluster{ Server: "https://mycluster", Config: appv1.ClusterConfig{Username: "bar"}, }) assert.Empty(t, clustersCache.clusters) } func TestHandleDeleteEvent_CacheDeadlock(t *testing.T) { testCluster := &appv1.Cluster{ Server: "https://mycluster", Config: appv1.ClusterConfig{Username: "bar"}, } db := &dbmocks.ArgoDB{} db.EXPECT().GetApplicationControllerReplicas().Return(1) fakeClient := fake.NewClientset() settingsMgr := argosettings.NewSettingsManager(t.Context(), fakeClient, "argocd") gitopsEngineClusterCache := &mocks.ClusterCache{} clustersCache := liveStateCache{ clusters: map[string]cache.ClusterCache{ testCluster.Server: gitopsEngineClusterCache, }, clusterSharding: sharding.NewClusterSharding(db, 0, 1, common.DefaultShardingAlgorithm), settingsMgr: settingsMgr, lock: sync.RWMutex{}, } channel := make(chan string) // Mocked lock held by the gitops-engine cluster cache gitopsEngineClusterCacheLock := sync.Mutex{} // Ensure completion of both EnsureSynced and Invalidate ensureSyncedCompleted := sync.Mutex{} invalidateCompleted := sync.Mutex{} // Locks to force trigger condition during test // Condition order: // EnsuredSynced -> Locks gitops-engine // handleDeleteEvent -> Locks liveStateCache // EnsureSynced via sync, newResource, populateResourceInfoHandler -> attempts to Lock liveStateCache // handleDeleteEvent via cluster.Invalidate -> attempts to Lock gitops-engine handleDeleteWasCalled := sync.Mutex{} engineHoldsEngineLock := sync.Mutex{} ensureSyncedCompleted.Lock() invalidateCompleted.Lock() handleDeleteWasCalled.Lock() engineHoldsEngineLock.Lock() gitopsEngineClusterCache.EXPECT().EnsureSynced().Run(func() { gitopsEngineClusterCacheLock.Lock() t.Log("EnsureSynced: Engine has engine lock") engineHoldsEngineLock.Unlock() defer gitopsEngineClusterCacheLock.Unlock() // Wait until handleDeleteEvent holds the liveStateCache lock handleDeleteWasCalled.Lock() // Try and obtain the liveStateCache lock clustersCache.lock.Lock() t.Log("EnsureSynced: Engine has LiveStateCache lock") clustersCache.lock.Unlock() ensureSyncedCompleted.Unlock() }).Return(nil).Once() gitopsEngineClusterCache.EXPECT().Invalidate().Run(func(_ ...cache.UpdateSettingsFunc) { // Allow EnsureSynced to continue now that we're in the deadlock condition handleDeleteWasCalled.Unlock() // Wait until gitops engine holds the gitops lock // This prevents timing issues if we reach this point before EnsureSynced has obtained the lock engineHoldsEngineLock.Lock() t.Log("Invalidate: Engine has engine lock") engineHoldsEngineLock.Unlock() // Lock engine lock gitopsEngineClusterCacheLock.Lock() t.Log("Invalidate: Invalidate has engine lock") gitopsEngineClusterCacheLock.Unlock() invalidateCompleted.Unlock() }).Return().Maybe() go func() { // Start the gitops-engine lock holds go func() { err := gitopsEngineClusterCache.EnsureSynced() if err != nil { assert.Fail(t, err.Error()) } }() // Run in background go clustersCache.handleDeleteEvent(testCluster.Server) // Allow execution to continue on clusters cache call to trigger lock ensureSyncedCompleted.Lock() invalidateCompleted.Lock() t.Log("Competing functions were able to obtain locks") invalidateCompleted.Unlock() ensureSyncedCompleted.Unlock() channel <- "PASSED" }() select { case str := <-channel: assert.Equal(t, "PASSED", str, str) case <-time.After(5 * time.Second): assert.Fail(t, "Ended up in deadlock") } } func TestIsRetryableError(t *testing.T) { var ( tlsHandshakeTimeoutErr net.Error = netError("net/http: TLS handshake timeout") ioTimeoutErr net.Error = netError("i/o timeout") connectionTimedout net.Error = netError("connection timed out") connectionReset net.Error = netError("connection reset by peer") ) t.Run("Nil", func(t *testing.T) { assert.False(t, isRetryableError(nil)) }) t.Run("ResourceQuotaConflictErr", func(t *testing.T) { assert.False(t, isRetryableError(apierrors.NewConflict(schema.GroupResource{}, "", nil))) assert.True(t, isRetryableError(apierrors.NewConflict(schema.GroupResource{Group: "v1", Resource: "resourcequotas"}, "", nil))) }) t.Run("ExceededQuotaErr", func(t *testing.T) { assert.False(t, isRetryableError(apierrors.NewForbidden(schema.GroupResource{}, "", nil))) assert.True(t, isRetryableError(apierrors.NewForbidden(schema.GroupResource{Group: "v1", Resource: "pods"}, "", errors.New("exceeded quota")))) }) t.Run("TooManyRequestsDNS", func(t *testing.T) { assert.True(t, isRetryableError(apierrors.NewTooManyRequests("", 0))) }) t.Run("DNSError", func(t *testing.T) { assert.True(t, isRetryableError(&net.DNSError{})) }) t.Run("OpError", func(t *testing.T) { assert.True(t, isRetryableError(&net.OpError{})) }) t.Run("UnknownNetworkError", func(t *testing.T) { assert.True(t, isRetryableError(net.UnknownNetworkError(""))) }) t.Run("ConnectionClosedErr", func(t *testing.T) { assert.False(t, isRetryableError(&url.Error{Err: errors.New("")})) assert.True(t, isRetryableError(&url.Error{Err: errors.New("Connection closed by foreign host")})) }) t.Run("TLSHandshakeTimeout", func(t *testing.T) { assert.True(t, isRetryableError(tlsHandshakeTimeoutErr)) }) t.Run("IOHandshakeTimeout", func(t *testing.T) { assert.True(t, isRetryableError(ioTimeoutErr)) }) t.Run("ConnectionTimeout", func(t *testing.T) { assert.True(t, isRetryableError(connectionTimedout)) }) t.Run("ConnectionReset", func(t *testing.T) { assert.True(t, isRetryableError(connectionReset)) }) } func Test_asResourceNode_owner_refs(t *testing.T) { resNode := asResourceNode(&cache.Resource{ ResourceVersion: "", Ref: corev1.ObjectReference{ APIVersion: "v1", }, OwnerRefs: []metav1.OwnerReference{ { APIVersion: "v1", Kind: "ConfigMap", Name: "cm-1", }, { APIVersion: "v1", Kind: "ConfigMap", Name: "cm-2", }, }, CreationTimestamp: nil, Info: nil, Resource: nil, }, nil) expected := appv1.ResourceNode{ ResourceRef: appv1.ResourceRef{ Version: "v1", }, ParentRefs: []appv1.ResourceRef{ { Group: "", Kind: "ConfigMap", Version: "v1", Name: "cm-1", }, { Group: "", Kind: "ConfigMap", Version: "v1", Name: "cm-2", }, }, Info: nil, NetworkingInfo: nil, ResourceVersion: "", Images: nil, Health: nil, CreatedAt: nil, } assert.Equal(t, expected, resNode) } func Test_getAppRecursive(t *testing.T) { for _, tt := range []struct { name string r *cache.Resource ns map[kube.ResourceKey]*cache.Resource wantName string wantOK assert.BoolAssertionFunc }{ { name: "ok: cm1->app1", r: &cache.Resource{ Ref: corev1.ObjectReference{ Name: "cm1", }, OwnerRefs: []metav1.OwnerReference{ {Name: "app1"}, }, }, ns: map[kube.ResourceKey]*cache.Resource{ kube.NewResourceKey("", "", "", "app1"): { Info: &ResourceInfo{ AppName: "app1", }, }, }, wantName: "app1", wantOK: assert.True, }, { name: "ok: cm1->cm2->app1", r: &cache.Resource{ Ref: corev1.ObjectReference{ Name: "cm1", }, OwnerRefs: []metav1.OwnerReference{ {Name: "cm2"}, }, }, ns: map[kube.ResourceKey]*cache.Resource{ kube.NewResourceKey("", "", "", "cm2"): { Ref: corev1.ObjectReference{ Name: "cm2", }, OwnerRefs: []metav1.OwnerReference{ {Name: "app1"}, }, }, kube.NewResourceKey("", "", "", "app1"): { Info: &ResourceInfo{ AppName: "app1", }, }, }, wantName: "app1", wantOK: assert.True, }, { name: "cm1->cm2->app1 & cm1->cm3->app1", r: &cache.Resource{ Ref: corev1.ObjectReference{ Name: "cm1", }, OwnerRefs: []metav1.OwnerReference{ {Name: "cm2"}, {Name: "cm3"}, }, }, ns: map[kube.ResourceKey]*cache.Resource{ kube.NewResourceKey("", "", "", "cm2"): { Ref: corev1.ObjectReference{ Name: "cm2", }, OwnerRefs: []metav1.OwnerReference{ {Name: "app1"}, }, }, kube.NewResourceKey("", "", "", "cm3"): { Ref: corev1.ObjectReference{ Name: "cm3", }, OwnerRefs: []metav1.OwnerReference{ {Name: "app1"}, }, }, kube.NewResourceKey("", "", "", "app1"): { Info: &ResourceInfo{ AppName: "app1", }, }, }, wantName: "app1", wantOK: assert.True, }, { // Nothing cycle. // Issue #11699, fixed #12667. name: "ok: cm1->cm2 & cm1->cm3->cm2 & cm1->cm3->app1", r: &cache.Resource{ Ref: corev1.ObjectReference{ Name: "cm1", }, OwnerRefs: []metav1.OwnerReference{ {Name: "cm2"}, {Name: "cm3"}, }, }, ns: map[kube.ResourceKey]*cache.Resource{ kube.NewResourceKey("", "", "", "cm2"): { Ref: corev1.ObjectReference{ Name: "cm2", }, }, kube.NewResourceKey("", "", "", "cm3"): { Ref: corev1.ObjectReference{ Name: "cm3", }, OwnerRefs: []metav1.OwnerReference{ {Name: "cm2"}, {Name: "app1"}, }, }, kube.NewResourceKey("", "", "", "app1"): { Info: &ResourceInfo{ AppName: "app1", }, }, }, wantName: "app1", wantOK: assert.True, }, { name: "cycle: cm1<->cm2", r: &cache.Resource{ Ref: corev1.ObjectReference{ Name: "cm1", }, OwnerRefs: []metav1.OwnerReference{ {Name: "cm2"}, }, }, ns: map[kube.ResourceKey]*cache.Resource{ kube.NewResourceKey("", "", "", "cm1"): { Ref: corev1.ObjectReference{ Name: "cm1", }, OwnerRefs: []metav1.OwnerReference{ {Name: "cm2"}, }, }, kube.NewResourceKey("", "", "", "cm2"): { Ref: corev1.ObjectReference{ Name: "cm2", }, OwnerRefs: []metav1.OwnerReference{ {Name: "cm1"}, }, }, }, wantName: "", wantOK: assert.False, }, { name: "cycle: cm1->cm2->cm3->cm1", r: &cache.Resource{ Ref: corev1.ObjectReference{ Name: "cm1", }, OwnerRefs: []metav1.OwnerReference{ {Name: "cm2"}, }, }, ns: map[kube.ResourceKey]*cache.Resource{ kube.NewResourceKey("", "", "", "cm1"): { Ref: corev1.ObjectReference{ Name: "cm1", }, OwnerRefs: []metav1.OwnerReference{ {Name: "cm2"}, }, }, kube.NewResourceKey("", "", "", "cm2"): { Ref: corev1.ObjectReference{ Name: "cm2", }, OwnerRefs: []metav1.OwnerReference{ {Name: "cm3"}, }, }, kube.NewResourceKey("", "", "", "cm3"): { Ref: corev1.ObjectReference{ Name: "cm3", }, OwnerRefs: []metav1.OwnerReference{ {Name: "cm1"}, }, }, }, wantName: "", wantOK: assert.False, }, } { t.Run(tt.name, func(t *testing.T) { visited := map[kube.ResourceKey]bool{} got, ok := getAppRecursive(tt.r, tt.ns, visited) assert.Equal(t, tt.wantName, got) tt.wantOK(t, ok) }) } } func TestSkipResourceUpdate(t *testing.T) { var ( hash1X = "x" hash2Y = "y" hash3X = "x" ) info := &ResourceInfo{ manifestHash: hash1X, Health: &health.HealthStatus{ Status: health.HealthStatusHealthy, Message: "default", }, } t.Run("Nil", func(t *testing.T) { assert.False(t, skipResourceUpdate(nil, nil)) }) t.Run("From Nil", func(t *testing.T) { assert.False(t, skipResourceUpdate(nil, info)) }) t.Run("To Nil", func(t *testing.T) { assert.False(t, skipResourceUpdate(info, nil)) }) t.Run("No hash", func(t *testing.T) { assert.False(t, skipResourceUpdate(&ResourceInfo{}, &ResourceInfo{})) }) t.Run("Same hash", func(t *testing.T) { assert.True(t, skipResourceUpdate(&ResourceInfo{ manifestHash: hash1X, }, &ResourceInfo{ manifestHash: hash1X, })) }) t.Run("Same hash value", func(t *testing.T) { assert.True(t, skipResourceUpdate(&ResourceInfo{ manifestHash: hash1X, }, &ResourceInfo{ manifestHash: hash3X, })) }) t.Run("Different hash value", func(t *testing.T) { assert.False(t, skipResourceUpdate(&ResourceInfo{ manifestHash: hash1X, }, &ResourceInfo{ manifestHash: hash2Y, })) }) t.Run("Same hash, empty health", func(t *testing.T) { assert.True(t, skipResourceUpdate(&ResourceInfo{ manifestHash: hash1X, Health: &health.HealthStatus{}, }, &ResourceInfo{ manifestHash: hash3X, Health: &health.HealthStatus{}, })) }) t.Run("Same hash, old health", func(t *testing.T) { assert.False(t, skipResourceUpdate(&ResourceInfo{ manifestHash: hash1X, Health: &health.HealthStatus{ Status: health.HealthStatusHealthy, }, }, &ResourceInfo{ manifestHash: hash3X, Health: nil, })) }) t.Run("Same hash, new health", func(t *testing.T) { assert.False(t, skipResourceUpdate(&ResourceInfo{ manifestHash: hash1X, Health: &health.HealthStatus{}, }, &ResourceInfo{ manifestHash: hash3X, Health: &health.HealthStatus{ Status: health.HealthStatusHealthy, }, })) }) t.Run("Same hash, same health", func(t *testing.T) { assert.True(t, skipResourceUpdate(&ResourceInfo{ manifestHash: hash1X, Health: &health.HealthStatus{ Status: health.HealthStatusHealthy, Message: "same", }, }, &ResourceInfo{ manifestHash: hash3X, Health: &health.HealthStatus{ Status: health.HealthStatusHealthy, Message: "same", }, })) }) t.Run("Same hash, different health status", func(t *testing.T) { assert.False(t, skipResourceUpdate(&ResourceInfo{ manifestHash: hash1X, Health: &health.HealthStatus{ Status: health.HealthStatusHealthy, Message: "same", }, }, &ResourceInfo{ manifestHash: hash3X, Health: &health.HealthStatus{ Status: health.HealthStatusDegraded, Message: "same", }, })) }) t.Run("Same hash, different health message", func(t *testing.T) { assert.True(t, skipResourceUpdate(&ResourceInfo{ manifestHash: hash1X, Health: &health.HealthStatus{ Status: health.HealthStatusHealthy, Message: "same", }, }, &ResourceInfo{ manifestHash: hash3X, Health: &health.HealthStatus{ Status: health.HealthStatusHealthy, Message: "different", }, })) }) } func TestShouldHashManifest(t *testing.T) { tests := []struct { name string appName string gvk schema.GroupVersionKind un *unstructured.Unstructured annotations map[string]string want bool }{ { name: "appName not empty gvk matches", appName: "MyApp", gvk: schema.GroupVersionKind{Group: application.Group, Kind: application.ApplicationKind}, un: &unstructured.Unstructured{}, want: true, }, { name: "appName empty", appName: "", gvk: schema.GroupVersionKind{Group: application.Group, Kind: application.ApplicationKind}, un: &unstructured.Unstructured{}, want: true, }, { name: "appName empty group not match", appName: "", gvk: schema.GroupVersionKind{Group: "group1", Kind: application.ApplicationKind}, un: &unstructured.Unstructured{}, want: false, }, { name: "appName empty kind not match", appName: "", gvk: schema.GroupVersionKind{Group: application.Group, Kind: "kind1"}, un: &unstructured.Unstructured{}, want: false, }, { name: "argocd.argoproj.io/ignore-resource-updates=true", appName: "", gvk: schema.GroupVersionKind{Group: application.Group, Kind: "kind1"}, un: &unstructured.Unstructured{}, annotations: map[string]string{"argocd.argoproj.io/ignore-resource-updates": "true"}, want: true, }, { name: "argocd.argoproj.io/ignore-resource-updates=invalid", appName: "", gvk: schema.GroupVersionKind{Group: application.Group, Kind: "kind1"}, un: &unstructured.Unstructured{}, annotations: map[string]string{"argocd.argoproj.io/ignore-resource-updates": "invalid"}, want: false, }, { name: "argocd.argoproj.io/ignore-resource-updates=false", appName: "", gvk: schema.GroupVersionKind{Group: application.Group, Kind: "kind1"}, un: &unstructured.Unstructured{}, annotations: map[string]string{"argocd.argoproj.io/ignore-resource-updates": "false"}, want: false, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { if test.annotations != nil { test.un.SetAnnotations(test.annotations) } got := shouldHashManifest(test.appName, test.gvk, test.un) require.Equalf(t, test.want, got, "test=%v", test.name) }) } } func Test_GetVersionsInfo_error_redacted(t *testing.T) { c := liveStateCache{} cluster := &appv1.Cluster{ Server: "https://localhost:1234", Config: appv1.ClusterConfig{ Username: "admin", Password: "password", }, } _, _, err := c.GetVersionsInfo(cluster) require.Error(t, err) assert.NotContains(t, err.Error(), "password") } func TestLoadCacheSettings(t *testing.T) { _, settingsManager := fixtures(t.Context(), map[string]string{ "application.instanceLabelKey": "testLabel", "application.resourceTrackingMethod": string(appv1.TrackingMethodLabel), "installationID": "123456789", }) ch := liveStateCache{ settingsMgr: settingsManager, } label, err := settingsManager.GetAppInstanceLabelKey() require.NoError(t, err) trackingMethod, err := settingsManager.GetTrackingMethod() require.NoError(t, err) res, err := ch.loadCacheSettings() require.NoError(t, err) assert.Equal(t, label, res.appInstanceLabelKey) assert.Equal(t, string(appv1.TrackingMethodLabel), trackingMethod) assert.Equal(t, "123456789", res.installationID) // By default the values won't be nil assert.NotNil(t, res.resourceOverrides) assert.NotNil(t, res.clusterSettings) assert.True(t, res.ignoreResourceUpdatesEnabled) } func Test_ownerRefGV(t *testing.T) { tests := []struct { name string input metav1.OwnerReference expected schema.GroupVersion }{ { name: "valid API Version", input: metav1.OwnerReference{ APIVersion: "apps/v1", }, expected: schema.GroupVersion{ Group: "apps", Version: "v1", }, }, { name: "custom defined version", input: metav1.OwnerReference{ APIVersion: "custom-version", }, expected: schema.GroupVersion{ Version: "custom-version", Group: "", }, }, { name: "empty APIVersion", input: metav1.OwnerReference{ APIVersion: "", }, expected: schema.GroupVersion{}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { res := ownerRefGV(tt.input) assert.Equal(t, tt.expected, res) }) } } func Test_asResourceNode_cross_namespace_parent(t *testing.T) { // Test that a namespaced resource with a cluster-scoped parent // correctly sets the parent namespace to empty string // Create a Role (namespaced) with an owner reference to a ClusterRole (cluster-scoped) roleResource := &cache.Resource{ Ref: corev1.ObjectReference{ APIVersion: "rbac.authorization.k8s.io/v1", Kind: "Role", Namespace: "my-namespace", Name: "my-role", }, OwnerRefs: []metav1.OwnerReference{ { APIVersion: "rbac.authorization.k8s.io/v1", Kind: "ClusterRole", Name: "my-cluster-role", UID: "cluster-role-uid", }, }, } // Create namespace resources map (ClusterRole won't be in here since it's cluster-scoped) namespaceResources := map[kube.ResourceKey]*cache.Resource{ // Add some other namespace resources but not the ClusterRole { Group: "rbac.authorization.k8s.io", Kind: "Role", Namespace: "my-namespace", Name: "other-role", }: { Ref: corev1.ObjectReference{ APIVersion: "rbac.authorization.k8s.io/v1", Kind: "Role", Namespace: "my-namespace", Name: "other-role", }, }, } resNode := asResourceNode(roleResource, namespaceResources) // The parent reference should have empty namespace since ClusterRole is cluster-scoped assert.Len(t, resNode.ParentRefs, 1) assert.Equal(t, "ClusterRole", resNode.ParentRefs[0].Kind) assert.Equal(t, "my-cluster-role", resNode.ParentRefs[0].Name) assert.Empty(t, resNode.ParentRefs[0].Namespace, "ClusterRole parent should have empty namespace") } func Test_asResourceNode_same_namespace_parent(t *testing.T) { // Test that a namespaced resource with a namespaced parent in the same namespace // correctly sets the parent namespace // Create a ReplicaSet with an owner reference to a Deployment (both namespaced) rsResource := &cache.Resource{ Ref: corev1.ObjectReference{ APIVersion: "apps/v1", Kind: "ReplicaSet", Namespace: "my-namespace", Name: "my-rs", }, OwnerRefs: []metav1.OwnerReference{ { APIVersion: "apps/v1", Kind: "Deployment", Name: "my-deployment", UID: "deployment-uid", }, }, } // Create namespace resources map with the Deployment deploymentKey := kube.ResourceKey{ Group: "apps", Kind: "Deployment", Namespace: "my-namespace", Name: "my-deployment", } namespaceResources := map[kube.ResourceKey]*cache.Resource{ deploymentKey: { Ref: corev1.ObjectReference{ APIVersion: "apps/v1", Kind: "Deployment", Namespace: "my-namespace", Name: "my-deployment", UID: "deployment-uid", }, Resource: &unstructured.Unstructured{ Object: map[string]any{ "apiVersion": "apps/v1", "kind": "Deployment", "metadata": map[string]any{ "name": "my-deployment", "namespace": "my-namespace", "uid": "deployment-uid", }, }, }, }, } resNode := asResourceNode(rsResource, namespaceResources) // The parent reference should have the same namespace assert.Len(t, resNode.ParentRefs, 1) assert.Equal(t, "Deployment", resNode.ParentRefs[0].Kind) assert.Equal(t, "my-deployment", resNode.ParentRefs[0].Name) assert.Equal(t, "my-namespace", resNode.ParentRefs[0].Namespace, "Deployment parent should have same namespace") }