diff --git a/controller/appcontroller.go b/controller/appcontroller.go index ee913902aa529..9714f5728643b 100644 --- a/controller/appcontroller.go +++ b/controller/appcontroller.go @@ -13,6 +13,7 @@ import ( "runtime/debug" "sort" "strconv" + "path/filepath" "strings" "sync" "time" @@ -637,6 +638,9 @@ func (ctrl *ApplicationController) getResourceTree(destCluster *appv1.Cluster, a orphanedNodesKeys := make([]kube.ResourceKey, 0) for k := range orphanedNodesMap { if k.Namespace != "" && proj.IsGroupKindNamePermitted(k.GroupKind(), k.Name, true) && !isKnownOrphanedResourceExclusion(k, proj) { + if ctrl.isOrphanedResourceIgnored(k) { + continue + } orphanedNodesKeys = append(orphanedNodesKeys, k) } } @@ -2778,3 +2782,19 @@ func (ctrl *ApplicationController) applyImpersonationConfig(config *rest.Config, } type ClusterFilterFunction func(c *appv1.Cluster, distributionFunction sharding.DistributionFunction) bool + +// isOrphanedResourceIgnored returns true if the given resource key matches +// any name pattern defined in resource.orphaned.ignore.namePatterns in argocd-cm +func (ctrl *ApplicationController) isOrphanedResourceIgnored(key kube.ResourceKey) bool { + config, err := ctrl.settingsMgr.GetOrphanedIgnoreConfig() + if err != nil || config == nil { + return false + } + for _, pattern := range config.NamePatterns { + matched, err := filepath.Match(pattern, key.Name) + if err == nil && matched { + return true + } + } + return false +} diff --git a/util/settings/settings.go b/util/settings/settings.go index 23dec85355b9d..728d1a9453d26 100644 --- a/util/settings/settings.go +++ b/util/settings/settings.go @@ -483,7 +483,10 @@ const ( resourceInclusionsKey = "resource.inclusions" // resourceIgnoreResourceUpdatesEnabledKey is the key to a boolean determining whether the resourceIgnoreUpdates feature is enabled resourceIgnoreResourceUpdatesEnabledKey = "resource.ignoreResourceUpdatesEnabled" + // resourceOrphanedIgnorePatternsKey is the key to a list of name patterns for orphaned resources to ignore + resourceOrphanedIgnorePatternsKey = "resource.orphaned.ignore.namePatterns" // resourceSensitiveAnnotationsKey is the key to list of annotations to mask in secret resource + // resourceSensitiveAnnotationsKey is the key to list of annotations to mask resourceSensitiveAnnotationsKey = "resource.sensitive.mask.annotations" // resourceCustomLabelKey is the key to a custom label to show in node info, if present resourceCustomLabelsKey = "resource.customLabels" @@ -2769,7 +2772,6 @@ func (m myFeatureGates) Enabled(f clientgofeatures.Feature) bool { return m.parent.Enabled(f) } -// FIXME: remove when we have proper WatchListClient and InOrderInformers support func ConfigureGoClientFeatures() { gates := clientgofeatures.FeatureGates() isWatchListEnabled := gates.Enabled(clientgofeatures.WatchListClient) @@ -2780,3 +2782,28 @@ func ConfigureGoClientFeatures() { clientgofeatures.ReplaceFeatureGates(wrapper) } } +// OrphanedResourceIgnoreConfig holds name patterns for orphaned resources +// that should be ignored during reconciliation + +type OrphanedResourceIgnoreConfig struct { + NamePatterns []string `json:"namePatterns,omitempty"` +} + +// GetOrphanedIgnoreConfig returns the config for ignoring orphaned +// resources by name pattern, read from argocd-cm +func (mgr *SettingsManager) GetOrphanedIgnoreConfig() (*OrphanedResourceIgnoreConfig, error) { + argoCDCM, err := mgr.getConfigMap() + if err != nil { + return nil, err + } + val, ok := argoCDCM.Data[resourceOrphanedIgnorePatternsKey] + if !ok || val == "" { + return &OrphanedResourceIgnoreConfig{}, nil + } + config := &OrphanedResourceIgnoreConfig{} + err = yaml.Unmarshal([]byte(val), config) + if err != nil { + return nil, fmt.Errorf("failed to parse %s: %w", resourceOrphanedIgnorePatternsKey, err) + } + return config, nil +} diff --git a/util/settings/settings_test.go b/util/settings/settings_test.go index 25674693cd10d..4551d28fa8401 100644 --- a/util/settings/settings_test.go +++ b/util/settings/settings_test.go @@ -2944,6 +2944,30 @@ func TestEscapeDollarSignsInMap(t *testing.T) { }) } + +func TestGetOrphanedIgnoreConfig_NoConfig(t *testing.T) { + _, settingsManager := fixtures(t.Context(), nil) + config, err := settingsManager.GetOrphanedIgnoreConfig() + require.NoError(t, err) + assert.Empty(t, config.NamePatterns) +} + +func TestGetOrphanedIgnoreConfig_WithPatterns(t *testing.T) { + _, settingsManager := fixtures(t.Context(), map[string]string{ + "resource.orphaned.ignore.namePatterns": "namePatterns:\n - \"*-cluster-config-map\"\n - \"*-config-map\"\n", + }) + config, err := settingsManager.GetOrphanedIgnoreConfig() + require.NoError(t, err) + assert.Equal(t, []string{"*-cluster-config-map", "*-config-map"}, config.NamePatterns) +} + +func TestGetOrphanedIgnoreConfig_NoMatch(t *testing.T) { + _, settingsManager := fixtures(t.Context(), map[string]string{ + "resource.orphaned.ignore.namePatterns": "namePatterns:\n - \"*-cluster-config-map\"\n", + }) + config, err := settingsManager.GetOrphanedIgnoreConfig() + require.NoError(t, err) + assert.NotContains(t, config.NamePatterns, "some-other-resource") func TestSettingsManager_GetWebhookRefreshJitter(t *testing.T) { tests := []struct { name string