2018-02-20 23:23:37 +00:00
package controller
import (
"context"
2018-03-09 10:01:15 +00:00
"encoding/json"
2024-12-30 08:56:41 +00:00
stderrors "errors"
2018-03-09 10:01:15 +00:00
"fmt"
2026-02-06 22:16:37 +00:00
"maps"
2019-07-15 20:34:26 +00:00
"math"
2024-01-21 06:22:32 +00:00
"math/rand"
2020-12-10 19:48:27 +00:00
"net/http"
2018-07-07 07:54:06 +00:00
"reflect"
2018-05-15 07:36:11 +00:00
"runtime/debug"
2020-07-21 23:59:00 +00:00
"sort"
2020-01-08 22:07:36 +00:00
"strconv"
2018-11-27 21:38:00 +00:00
"strings"
2018-04-11 19:53:33 +00:00
"sync"
2018-05-05 00:01:57 +00:00
"time"
2018-04-11 19:53:33 +00:00
2026-02-12 14:29:40 +00:00
clustercache "github.com/argoproj/argo-cd/gitops-engine/pkg/cache"
"github.com/argoproj/argo-cd/gitops-engine/pkg/diff"
"github.com/argoproj/argo-cd/gitops-engine/pkg/health"
synccommon "github.com/argoproj/argo-cd/gitops-engine/pkg/sync/common"
resourceutil "github.com/argoproj/argo-cd/gitops-engine/pkg/sync/resource"
"github.com/argoproj/argo-cd/gitops-engine/pkg/utils/kube"
2020-12-10 02:05:34 +00:00
jsonpatch "github.com/evanphx/json-patch"
2018-11-30 21:50:27 +00:00
log "github.com/sirupsen/logrus"
2019-09-10 16:56:48 +00:00
"golang.org/x/sync/semaphore"
2025-01-03 16:10:00 +00:00
corev1 "k8s.io/api/core/v1"
2025-01-03 17:09:37 +00:00
apierrors "k8s.io/apimachinery/pkg/api/errors"
2018-02-26 15:43:35 +00:00
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2018-03-07 06:05:07 +00:00
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2020-07-22 00:25:41 +00:00
"k8s.io/apimachinery/pkg/labels"
2020-12-08 16:45:37 +00:00
apiruntime "k8s.io/apimachinery/pkg/runtime"
2022-03-22 17:57:30 +00:00
"k8s.io/apimachinery/pkg/runtime/schema"
2018-05-03 22:55:01 +00:00
"k8s.io/apimachinery/pkg/types"
2018-02-20 23:23:37 +00:00
"k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/wait"
2020-12-08 16:45:37 +00:00
"k8s.io/apimachinery/pkg/watch"
2023-09-22 19:49:09 +00:00
"k8s.io/client-go/informers"
informerv1 "k8s.io/client-go/informers/apps/v1"
2018-02-20 23:23:37 +00:00
"k8s.io/client-go/kubernetes"
2025-10-06 14:30:44 +00:00
"k8s.io/client-go/rest"
2018-02-20 23:23:37 +00:00
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/util/workqueue"
2025-05-22 09:35:12 +00:00
"k8s.io/utils/ptr"
2018-07-07 07:54:06 +00:00
2025-01-10 21:14:00 +00:00
commitclient "github.com/argoproj/argo-cd/v3/commitserver/apiclient"
"github.com/argoproj/argo-cd/v3/common"
statecache "github.com/argoproj/argo-cd/v3/controller/cache"
"github.com/argoproj/argo-cd/v3/controller/hydrator"
2025-07-01 16:33:04 +00:00
hydratortypes "github.com/argoproj/argo-cd/v3/controller/hydrator/types"
2025-01-10 21:14:00 +00:00
"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"
appclientset "github.com/argoproj/argo-cd/v3/pkg/client/clientset/versioned"
"github.com/argoproj/argo-cd/v3/pkg/client/informers/externalversions/application/v1alpha1"
applisters "github.com/argoproj/argo-cd/v3/pkg/client/listers/application/v1alpha1"
"github.com/argoproj/argo-cd/v3/reposerver/apiclient"
2025-05-20 19:48:09 +00:00
applog "github.com/argoproj/argo-cd/v3/util/app/log"
2025-01-10 21:14:00 +00:00
"github.com/argoproj/argo-cd/v3/util/argo"
argodiff "github.com/argoproj/argo-cd/v3/util/argo/diff"
"github.com/argoproj/argo-cd/v3/util/argo/normalizers"
"github.com/argoproj/argo-cd/v3/util/env"
"github.com/argoproj/argo-cd/v3/util/stats"
2023-06-05 13:19:14 +00:00
2025-01-10 21:14:00 +00:00
"github.com/argoproj/argo-cd/v3/pkg/ratelimiter"
appstatecache "github.com/argoproj/argo-cd/v3/util/cache/appstate"
"github.com/argoproj/argo-cd/v3/util/db"
"github.com/argoproj/argo-cd/v3/util/errors"
"github.com/argoproj/argo-cd/v3/util/glob"
"github.com/argoproj/argo-cd/v3/util/helm"
logutils "github.com/argoproj/argo-cd/v3/util/log"
settings_util "github.com/argoproj/argo-cd/v3/util/settings"
2018-02-20 23:23:37 +00:00
)
2018-04-11 19:53:33 +00:00
const (
2023-09-22 19:49:09 +00:00
updateOperationStateTimeout = 1 * time . Second
2023-10-30 21:53:11 +00:00
defaultDeploymentInformerResyncDuration = 10 * time . Second
2019-08-19 15:14:48 +00:00
// orphanedIndex contains application which monitor orphaned resources by namespace
orphanedIndex = "orphaned"
2018-04-11 19:53:33 +00:00
)
2019-07-15 20:34:26 +00:00
type CompareWith int
const (
2021-05-19 19:30:15 +00:00
// Compare live application state against state defined in latest git revision with no resolved revision caching.
CompareWithLatestForceResolve CompareWith = 3
2019-07-15 20:34:26 +00:00
// Compare live application state against state defined in latest git revision.
CompareWithLatest CompareWith = 2
// Compare live application state against state defined using revision of most recent comparison.
CompareWithRecent CompareWith = 1
// Skip comparison and only refresh application resources tree
ComparisonWithNothing CompareWith = 0
)
func ( a CompareWith ) Max ( b CompareWith ) CompareWith {
return CompareWith ( math . Max ( float64 ( a ) , float64 ( b ) ) )
}
2020-01-08 22:07:36 +00:00
func ( a CompareWith ) Pointer ( ) * CompareWith {
return & a
}
2018-02-20 23:23:37 +00:00
// ApplicationController is the controller for application resources.
type ApplicationController struct {
2020-01-08 22:07:36 +00:00
cache * appstatecache . Cache
namespace string
kubeClientset kubernetes . Interface
kubectl kube . Kubectl
applicationClientset appclientset . Interface
auditLogger * argo . AuditLogger
// queue contains app namespace/name
2024-08-23 22:28:34 +00:00
appRefreshQueue workqueue . TypedRateLimitingInterface [ string ]
2020-01-08 22:07:36 +00:00
// queue contains app namespace/name/comparisonType and used to request app refresh with the predefined comparison type
2024-08-23 22:28:34 +00:00
appComparisonTypeRefreshQueue workqueue . TypedRateLimitingInterface [ string ]
appOperationQueue workqueue . TypedRateLimitingInterface [ string ]
projectRefreshQueue workqueue . TypedRateLimitingInterface [ string ]
2024-12-16 21:59:09 +00:00
appHydrateQueue workqueue . TypedRateLimitingInterface [ string ]
2025-07-01 16:33:04 +00:00
hydrationQueue workqueue . TypedRateLimitingInterface [ hydratortypes . HydrationQueueKey ]
2020-01-08 22:07:36 +00:00
appInformer cache . SharedIndexInformer
appLister applisters . ApplicationLister
projInformer cache . SharedIndexInformer
appStateManager AppStateManager
stateCache statecache . LiveStateCache
statusRefreshTimeout time . Duration
2022-04-04 14:44:35 +00:00
statusHardRefreshTimeout time . Duration
2024-01-21 06:22:32 +00:00
statusRefreshJitter time . Duration
2020-01-08 22:07:36 +00:00
selfHealTimeout time . Duration
2025-07-07 17:38:37 +00:00
selfHealBackoff * wait . Backoff
2024-12-10 08:29:04 +00:00
syncTimeout time . Duration
2020-01-08 22:07:36 +00:00
db db . ArgoDB
settingsMgr * settings_util . SettingsManager
refreshRequestedApps map [ string ] CompareWith
refreshRequestedAppsMutex * sync . Mutex
metricsServer * metrics . MetricsServer
2025-01-29 18:06:29 +00:00
metricsClusterLabels [ ] string
2020-01-08 22:07:36 +00:00
kubectlSemaphore * semaphore . Weighted
2024-01-11 06:32:11 +00:00
clusterSharding sharding . ClusterShardingCache
2021-09-21 19:20:49 +00:00
projByNameCache sync . Map
2022-08-10 09:39:10 +00:00
applicationNamespaces [ ] string
2024-04-26 09:24:02 +00:00
ignoreNormalizerOpts normalizers . IgnoreNormalizerOpts
2024-02-11 18:32:17 +00:00
// dynamicClusterDistributionEnabled if disabled deploymentInformer is never initialized
dynamicClusterDistributionEnabled bool
deploymentInformer informerv1 . DeploymentInformer
2024-12-16 21:59:09 +00:00
hydrator * hydrator . Hydrator
2018-02-20 23:23:37 +00:00
}
// NewApplicationController creates new instance of ApplicationController.
2018-02-22 18:56:14 +00:00
func NewApplicationController (
2018-04-11 19:53:33 +00:00
namespace string ,
2019-01-08 22:53:45 +00:00
settingsMgr * settings_util . SettingsManager ,
2018-02-22 18:56:14 +00:00
kubeClientset kubernetes . Interface ,
applicationClientset appclientset . Interface ,
2019-07-13 00:17:23 +00:00
repoClientset apiclient . Clientset ,
2024-12-16 21:59:09 +00:00
commitClientset commitclient . Clientset ,
2019-10-16 22:46:45 +00:00
argoCache * appstatecache . Cache ,
2019-09-11 23:37:00 +00:00
kubectl kube . Kubectl ,
2018-02-26 15:43:35 +00:00
appResyncPeriod time . Duration ,
2022-04-04 14:44:35 +00:00
appHardResyncPeriod time . Duration ,
2024-01-21 06:22:32 +00:00
appResyncJitter time . Duration ,
2019-07-25 02:26:09 +00:00
selfHealTimeout time . Duration ,
2024-10-08 16:32:00 +00:00
selfHealBackoff * wait . Backoff ,
2024-12-10 08:29:04 +00:00
syncTimeout time . Duration ,
2023-11-02 15:51:16 +00:00
repoErrorGracePeriod time . Duration ,
2019-05-28 18:41:02 +00:00
metricsPort int ,
2021-02-10 01:27:08 +00:00
metricsCacheExpiration time . Duration ,
2021-10-08 17:15:51 +00:00
metricsApplicationLabels [ ] string ,
2024-09-05 06:41:35 +00:00
metricsApplicationConditions [ ] string ,
2025-01-29 18:06:29 +00:00
metricsClusterLabels [ ] string ,
2019-09-10 16:56:48 +00:00
kubectlParallelismLimit int64 ,
2022-08-17 21:03:24 +00:00
persistResourceHealth bool ,
2024-01-11 06:32:11 +00:00
clusterSharding sharding . ClusterShardingCache ,
2022-08-10 09:39:10 +00:00
applicationNamespaces [ ] string ,
2023-10-18 19:08:04 +00:00
rateLimiterConfig * ratelimiter . AppControllerRateLimiterConfig ,
2023-12-18 20:37:13 +00:00
serverSideDiff bool ,
2024-02-11 18:32:17 +00:00
dynamicClusterDistributionEnabled bool ,
2024-04-26 09:24:02 +00:00
ignoreNormalizerOpts normalizers . IgnoreNormalizerOpts ,
2024-09-23 03:02:17 +00:00
enableK8sEvent [ ] string ,
2024-12-16 21:59:09 +00:00
hydratorEnabled bool ,
2019-01-08 22:53:45 +00:00
) ( * ApplicationController , error ) {
2024-01-21 06:22:32 +00:00
log . Infof ( "appResyncPeriod=%v, appHardResyncPeriod=%v, appResyncJitter=%v" , appResyncPeriod , appHardResyncPeriod , appResyncJitter )
2018-11-09 17:58:07 +00:00
db := db . NewDB ( namespace , settingsMgr , kubeClientset )
2023-10-18 19:08:04 +00:00
if rateLimiterConfig == nil {
rateLimiterConfig = ratelimiter . GetDefaultAppRateLimiterConfig ( )
log . Info ( "Using default workqueue rate limiter config" )
}
2018-09-11 21:28:53 +00:00
ctrl := ApplicationController {
2024-02-11 18:32:17 +00:00
cache : argoCache ,
namespace : namespace ,
kubeClientset : kubeClientset ,
kubectl : kubectl ,
applicationClientset : applicationClientset ,
2024-12-16 21:59:09 +00:00
appRefreshQueue : workqueue . NewTypedRateLimitingQueueWithConfig ( ratelimiter . NewCustomAppControllerRateLimiter [ string ] ( rateLimiterConfig ) , workqueue . TypedRateLimitingQueueConfig [ string ] { Name : "app_reconciliation_queue" } ) ,
appOperationQueue : workqueue . NewTypedRateLimitingQueueWithConfig ( ratelimiter . NewCustomAppControllerRateLimiter [ string ] ( rateLimiterConfig ) , workqueue . TypedRateLimitingQueueConfig [ string ] { Name : "app_operation_processing_queue" } ) ,
projectRefreshQueue : workqueue . NewTypedRateLimitingQueueWithConfig ( ratelimiter . NewCustomAppControllerRateLimiter [ string ] ( rateLimiterConfig ) , workqueue . TypedRateLimitingQueueConfig [ string ] { Name : "project_reconciliation_queue" } ) ,
appComparisonTypeRefreshQueue : workqueue . NewTypedRateLimitingQueue ( ratelimiter . NewCustomAppControllerRateLimiter [ string ] ( rateLimiterConfig ) ) ,
appHydrateQueue : workqueue . NewTypedRateLimitingQueueWithConfig ( ratelimiter . NewCustomAppControllerRateLimiter [ string ] ( rateLimiterConfig ) , workqueue . TypedRateLimitingQueueConfig [ string ] { Name : "app_hydration_queue" } ) ,
2025-07-01 16:33:04 +00:00
hydrationQueue : workqueue . NewTypedRateLimitingQueueWithConfig ( ratelimiter . NewCustomAppControllerRateLimiter [ hydratortypes . HydrationQueueKey ] ( rateLimiterConfig ) , workqueue . TypedRateLimitingQueueConfig [ hydratortypes . HydrationQueueKey ] { Name : "manifest_hydration_queue" } ) ,
2024-02-11 18:32:17 +00:00
db : db ,
statusRefreshTimeout : appResyncPeriod ,
statusHardRefreshTimeout : appHardResyncPeriod ,
statusRefreshJitter : appResyncJitter ,
refreshRequestedApps : make ( map [ string ] CompareWith ) ,
refreshRequestedAppsMutex : & sync . Mutex { } ,
2025-06-05 11:47:55 +00:00
auditLogger : argo . NewAuditLogger ( kubeClientset , common . ApplicationController , enableK8sEvent ) ,
2024-02-11 18:32:17 +00:00
settingsMgr : settingsMgr ,
selfHealTimeout : selfHealTimeout ,
2025-07-07 17:38:37 +00:00
selfHealBackoff : selfHealBackoff ,
2024-12-10 08:29:04 +00:00
syncTimeout : syncTimeout ,
2024-02-11 18:32:17 +00:00
clusterSharding : clusterSharding ,
projByNameCache : sync . Map { } ,
applicationNamespaces : applicationNamespaces ,
dynamicClusterDistributionEnabled : dynamicClusterDistributionEnabled ,
2024-04-26 09:24:02 +00:00
ignoreNormalizerOpts : ignoreNormalizerOpts ,
2025-01-29 18:06:29 +00:00
metricsClusterLabels : metricsClusterLabels ,
2018-11-28 21:38:02 +00:00
}
2024-12-16 21:59:09 +00:00
if hydratorEnabled {
2025-06-13 19:14:03 +00:00
ctrl . hydrator = hydrator . NewHydrator ( & ctrl , appResyncPeriod , commitClientset , repoClientset , db )
2024-12-16 21:59:09 +00:00
}
2019-09-10 16:56:48 +00:00
if kubectlParallelismLimit > 0 {
ctrl . kubectlSemaphore = semaphore . NewWeighted ( kubectlParallelismLimit )
}
2020-01-07 06:28:42 +00:00
kubectl . SetOnKubectlRun ( ctrl . onKubectlRun )
2020-12-08 16:45:37 +00:00
appInformer , appLister := ctrl . newApplicationInformerAndLister ( )
2019-11-24 22:12:47 +00:00
indexers := cache . Indexers { cache . NamespaceIndex : cache . MetaNamespaceIndexFunc }
projInformer := v1alpha1 . NewAppProjectInformer ( applicationClientset , namespace , appResyncPeriod , indexers )
2023-10-18 15:17:00 +00:00
var err error
_ , err = projInformer . AddEventHandler ( cache . ResourceEventHandlerFuncs {
2025-01-02 23:26:59 +00:00
AddFunc : func ( obj any ) {
2020-07-22 00:25:41 +00:00
if key , err := cache . MetaNamespaceKeyFunc ( obj ) ; err == nil {
2023-10-18 19:08:04 +00:00
ctrl . projectRefreshQueue . AddRateLimited ( key )
2021-12-07 22:15:11 +00:00
if projMeta , ok := obj . ( metav1 . Object ) ; ok {
ctrl . InvalidateProjectsCache ( projMeta . GetName ( ) )
}
2020-07-22 00:25:41 +00:00
}
} ,
2025-01-07 15:12:56 +00:00
UpdateFunc : func ( _ , new any ) {
2020-07-22 00:25:41 +00:00
if key , err := cache . MetaNamespaceKeyFunc ( new ) ; err == nil {
2023-10-18 19:08:04 +00:00
ctrl . projectRefreshQueue . AddRateLimited ( key )
2021-12-07 22:15:11 +00:00
if projMeta , ok := new . ( metav1 . Object ) ; ok {
ctrl . InvalidateProjectsCache ( projMeta . GetName ( ) )
}
2020-07-22 00:25:41 +00:00
}
} ,
2025-01-02 23:26:59 +00:00
DeleteFunc : func ( obj any ) {
2020-07-22 00:25:41 +00:00
if key , err := cache . DeletionHandlingMetaNamespaceKeyFunc ( obj ) ; err == nil {
2023-10-18 19:08:04 +00:00
// immediately push to queue for deletes
2020-07-22 00:25:41 +00:00
ctrl . projectRefreshQueue . Add ( key )
2021-12-07 22:15:11 +00:00
if projMeta , ok := obj . ( metav1 . Object ) ; ok {
ctrl . InvalidateProjectsCache ( projMeta . GetName ( ) )
}
2020-07-22 00:25:41 +00:00
}
} ,
} )
2023-10-18 15:17:00 +00:00
if err != nil {
return nil , err
}
2023-09-22 19:49:09 +00:00
2023-09-29 17:34:50 +00:00
factory := informers . NewSharedInformerFactoryWithOptions ( ctrl . kubeClientset , defaultDeploymentInformerResyncDuration , informers . WithNamespace ( settingsMgr . GetNamespace ( ) ) )
2024-02-11 18:32:17 +00:00
var deploymentInformer informerv1 . DeploymentInformer
// only initialize deployment informer if dynamic distribution is enabled
if dynamicClusterDistributionEnabled {
deploymentInformer = factory . Apps ( ) . V1 ( ) . Deployments ( )
}
2023-09-22 19:49:09 +00:00
2025-01-07 15:12:56 +00:00
readinessHealthCheck := func ( _ * http . Request ) error {
2024-02-11 18:32:17 +00:00
if dynamicClusterDistributionEnabled {
applicationControllerName := env . StringFromEnv ( common . EnvAppControllerName , common . DefaultApplicationControllerName )
appControllerDeployment , err := deploymentInformer . Lister ( ) . Deployments ( settingsMgr . GetNamespace ( ) ) . Get ( applicationControllerName )
if err != nil {
2025-01-08 20:26:02 +00:00
if ! apierrors . IsNotFound ( err ) {
2024-06-11 19:33:22 +00:00
return fmt . Errorf ( "error retrieving Application Controller Deployment: %w" , err )
2024-02-11 18:32:17 +00:00
}
2025-01-08 20:26:02 +00:00
appControllerDeployment = nil
2023-09-22 19:49:09 +00:00
}
2024-02-11 18:32:17 +00:00
if appControllerDeployment != nil {
if appControllerDeployment . Spec . Replicas != nil && int ( * appControllerDeployment . Spec . Replicas ) <= 0 {
return fmt . Errorf ( "application controller deployment replicas is not set or is less than 0, replicas: %d" , appControllerDeployment . Spec . Replicas )
}
shard := env . ParseNumFromEnv ( common . EnvControllerShard , - 1 , - math . MaxInt32 , math . MaxInt32 )
2025-02-10 03:25:34 +00:00
shard , err := sharding . GetOrUpdateShardFromConfigMap ( kubeClientset . ( * kubernetes . Clientset ) , settingsMgr , int ( * appControllerDeployment . Spec . Replicas ) , shard )
if err != nil {
2024-06-11 19:33:22 +00:00
return fmt . Errorf ( "error while updating the heartbeat for to the Shard Mapping ConfigMap: %w" , err )
2024-02-11 18:32:17 +00:00
}
2025-02-10 03:25:34 +00:00
// update the shard number in the clusterSharding, and resync all applications if the shard number is updated
if ctrl . clusterSharding . UpdateShard ( shard ) {
// update shard number in stateCache
ctrl . stateCache . UpdateShard ( shard )
// resync all applications
apps , err := ctrl . appLister . List ( labels . Everything ( ) )
if err != nil {
return err
}
for _ , app := range apps {
if ! ctrl . canProcessApp ( app ) {
continue
}
key , err := cache . MetaNamespaceKeyFunc ( app )
if err == nil {
ctrl . appRefreshQueue . AddRateLimited ( key )
ctrl . clusterSharding . AddApp ( app )
}
}
}
2023-09-22 19:49:09 +00:00
}
}
return nil
}
2019-05-28 18:41:02 +00:00
metricsAddr := fmt . Sprintf ( "0.0.0.0:%d" , metricsPort )
2023-10-18 15:17:00 +00:00
2025-06-06 17:25:29 +00:00
ctrl . metricsServer , err = metrics . NewMetricsServer ( metricsAddr , appLister , ctrl . canProcessApp , readinessHealthCheck , metricsApplicationLabels , metricsApplicationConditions , ctrl . db )
2020-10-09 20:16:54 +00:00
if err != nil {
return nil , err
}
2021-02-10 01:27:08 +00:00
if metricsCacheExpiration . Seconds ( ) != 0 {
err = ctrl . metricsServer . SetExpiration ( metricsCacheExpiration )
if err != nil {
return nil , err
}
}
2025-06-05 11:47:55 +00:00
stateCache := statecache . NewLiveStateCache ( db , appInformer , ctrl . settingsMgr , ctrl . metricsServer , ctrl . handleObjectUpdated , clusterSharding , argo . NewResourceTracking ( ) )
2025-07-03 17:16:58 +00:00
appStateManager := NewAppStateManager ( db , applicationClientset , repoClientset , namespace , kubectl , ctrl . onKubectlRun , ctrl . settingsMgr , stateCache , ctrl . metricsServer , argoCache , ctrl . statusRefreshTimeout , argo . NewResourceTracking ( ) , persistResourceHealth , repoErrorGracePeriod , serverSideDiff , ignoreNormalizerOpts )
2018-11-28 21:38:02 +00:00
ctrl . appInformer = appInformer
2019-02-22 23:20:34 +00:00
ctrl . appLister = appLister
2019-01-08 22:53:45 +00:00
ctrl . projInformer = projInformer
2023-09-22 19:49:09 +00:00
ctrl . deploymentInformer = deploymentInformer
2018-11-28 21:38:02 +00:00
ctrl . appStateManager = appStateManager
ctrl . stateCache = stateCache
2019-04-29 19:42:59 +00:00
2019-01-08 22:53:45 +00:00
return & ctrl , nil
2018-02-20 23:23:37 +00:00
}
2021-12-07 22:15:11 +00:00
func ( ctrl * ApplicationController ) InvalidateProjectsCache ( names ... string ) {
if len ( names ) > 0 {
for _ , name := range names {
ctrl . projByNameCache . Delete ( name )
}
2024-06-13 19:10:00 +00:00
} else if ctrl != nil {
2025-01-02 23:26:59 +00:00
ctrl . projByNameCache . Range ( func ( key , _ any ) bool {
2024-06-13 19:10:00 +00:00
ctrl . projByNameCache . Delete ( key )
return true
} )
2021-12-07 22:15:11 +00:00
}
2021-09-21 19:20:49 +00:00
}
2020-04-28 19:52:03 +00:00
func ( ctrl * ApplicationController ) GetMetricsServer ( ) * metrics . MetricsServer {
return ctrl . metricsServer
}
2020-10-27 21:10:24 +00:00
func ( ctrl * ApplicationController ) onKubectlRun ( command string ) ( kube . CleanupFunc , error ) {
2020-01-07 06:28:42 +00:00
ctrl . metricsServer . IncKubectlExec ( command )
if ctrl . kubectlSemaphore != nil {
if err := ctrl . kubectlSemaphore . Acquire ( context . Background ( ) , 1 ) ; err != nil {
return nil , err
}
ctrl . metricsServer . IncKubectlExecPending ( command )
}
2020-10-27 21:10:24 +00:00
return func ( ) {
2020-01-07 06:28:42 +00:00
if ctrl . kubectlSemaphore != nil {
ctrl . kubectlSemaphore . Release ( 1 )
ctrl . metricsServer . DecKubectlExecPending ( command )
}
2020-10-27 21:10:24 +00:00
} , nil
2020-01-07 06:28:42 +00:00
}
2025-01-03 16:10:00 +00:00
func isSelfReferencedApp ( app * appv1 . Application , ref corev1 . ObjectReference ) bool {
2019-05-13 18:17:32 +00:00
gvk := ref . GroupVersionKind ( )
return ref . UID == app . UID &&
ref . Name == app . Name &&
ref . Namespace == app . Namespace &&
gvk . Group == application . Group &&
gvk . Kind == application . ApplicationKind
2019-05-02 21:38:37 +00:00
}
2021-09-21 19:20:49 +00:00
func ( ctrl * ApplicationController ) newAppProjCache ( name string ) * appProjCache {
return & appProjCache { name : name , ctrl : ctrl }
}
type appProjCache struct {
name string
ctrl * ApplicationController
lock sync . Mutex
appProj * appv1 . AppProject
}
2022-08-10 09:39:10 +00:00
// GetAppProject gets an AppProject from the cache. If the AppProject is not
// yet cached, retrieves the AppProject from the K8s control plane and stores
// in the cache.
2021-09-21 19:20:49 +00:00
func ( projCache * appProjCache ) GetAppProject ( ctx context . Context ) ( * appv1 . AppProject , error ) {
projCache . lock . Lock ( )
defer projCache . lock . Unlock ( )
if projCache . appProj != nil {
return projCache . appProj , nil
}
2025-01-07 15:08:51 +00:00
proj , err := argo . GetAppProjectByName ( ctx , projCache . name , applisters . NewAppProjectLister ( projCache . ctrl . projInformer . GetIndexer ( ) ) , projCache . ctrl . namespace , projCache . ctrl . settingsMgr , projCache . ctrl . db )
2021-09-21 19:20:49 +00:00
if err != nil {
return nil , err
}
projCache . appProj = proj
return projCache . appProj , nil
}
2022-08-10 09:39:10 +00:00
// getAppProj gets the AppProject for the given Application app.
2019-08-19 15:14:48 +00:00
func ( ctrl * ApplicationController ) getAppProj ( app * appv1 . Application ) ( * appv1 . AppProject , error ) {
2024-12-11 10:29:37 +00:00
projCache , _ := ctrl . projByNameCache . Load ( app . Spec . GetProject ( ) )
if projCache == nil {
projCache = ctrl . newAppProjCache ( app . Spec . GetProject ( ) )
ctrl . projByNameCache . Store ( app . Spec . GetProject ( ) , projCache )
}
2022-08-10 09:39:10 +00:00
proj , err := projCache . ( * appProjCache ) . GetAppProject ( context . TODO ( ) )
if err != nil {
2025-01-03 17:09:37 +00:00
if apierrors . IsNotFound ( err ) {
2022-08-10 09:39:10 +00:00
return nil , err
}
2025-01-07 15:25:22 +00:00
return nil , fmt . Errorf ( "could not retrieve AppProject '%s' from cache: %w" , app . Spec . Project , err )
2022-08-10 09:39:10 +00:00
}
if ! proj . IsAppNamespacePermitted ( app , ctrl . namespace ) {
return nil , argo . ErrProjectNotPermitted ( app . GetName ( ) , app . GetNamespace ( ) , proj . GetName ( ) )
}
return proj , nil
2019-08-19 15:14:48 +00:00
}
2019-05-13 18:17:32 +00:00
2025-01-03 16:10:00 +00:00
func ( ctrl * ApplicationController ) handleObjectUpdated ( managedByApp map [ string ] bool , ref corev1 . ObjectReference ) {
2019-08-19 15:14:48 +00:00
// if namespaced resource is not managed by any app it might be orphaned resource of some other apps
if len ( managedByApp ) == 0 && ref . Namespace != "" {
2020-11-03 18:54:13 +00:00
// retrieve applications which monitor orphaned resources in the same namespace and refresh them unless resource is denied in app project
2019-08-19 15:14:48 +00:00
if objs , err := ctrl . appInformer . GetIndexer ( ) . ByIndex ( orphanedIndex , ref . Namespace ) ; err == nil {
for i := range objs {
app , ok := objs [ i ] . ( * appv1 . Application )
if ! ok {
continue
}
2019-08-20 22:48:37 +00:00
2022-08-10 09:39:10 +00:00
managedByApp [ app . InstanceName ( ctrl . namespace ) ] = true
2019-08-19 15:14:48 +00:00
}
}
2019-05-13 18:17:32 +00:00
}
2019-08-19 15:14:48 +00:00
for appName , isManagedResource := range managedByApp {
2022-08-10 09:39:10 +00:00
// The appName is given as <namespace>_<name>, but the indexer needs it
// format <namespace>/<name>
appKey := ctrl . toAppKey ( appName )
obj , exists , err := ctrl . appInformer . GetIndexer ( ) . GetByKey ( appKey )
app , ok := obj . ( * appv1 . Application )
if exists && err == nil && ok && isSelfReferencedApp ( app , ref ) {
2019-08-19 15:14:48 +00:00
// Don't force refresh app if related resource is application itself. This prevents infinite reconciliation loop.
2020-01-08 22:07:36 +00:00
continue
2019-08-19 15:14:48 +00:00
}
2019-05-13 18:17:32 +00:00
2020-10-09 20:16:54 +00:00
if ! ctrl . canProcessApp ( obj ) {
2023-01-25 14:14:29 +00:00
// Don't force refresh app if app belongs to a different controller shard or is outside the allowed namespaces.
2020-10-09 20:16:54 +00:00
continue
}
2025-05-20 19:48:09 +00:00
logCtx := log . WithFields ( applog . GetAppLogFields ( app ) )
2022-08-10 09:39:10 +00:00
// Enforce application's permission for the source namespace
_ , err = ctrl . getAppProj ( app )
if err != nil {
2025-09-27 21:47:21 +00:00
logCtx . WithError ( err ) . Errorf ( "Unable to determine project for app" )
2022-08-10 09:39:10 +00:00
continue
}
2020-01-08 22:07:36 +00:00
level := ComparisonWithNothing
if isManagedResource {
level = CompareWithRecent
2019-07-15 20:34:26 +00:00
}
2022-01-18 07:22:26 +00:00
2023-06-25 01:32:20 +00:00
namespace := ref . Namespace
if ref . Namespace == "" {
namespace = "(cluster-scoped)"
2022-01-18 07:22:26 +00:00
}
2024-05-13 20:07:24 +00:00
logCtx . WithFields ( log . Fields {
"comparison-level" : level ,
"namespace" : namespace ,
"name" : ref . Name ,
"api-version" : ref . APIVersion ,
"kind" : ref . Kind ,
"server" : app . Spec . Destination . Server ,
"cluster-name" : app . Spec . Destination . Name ,
2023-06-25 01:32:20 +00:00
} ) . Debug ( "Requesting app refresh caused by object update" )
2022-01-18 07:22:26 +00:00
2022-08-10 09:39:10 +00:00
ctrl . requestAppRefresh ( app . QualifiedName ( ) , & level , nil )
2019-05-01 16:42:45 +00:00
}
}
2021-12-22 16:57:48 +00:00
// setAppManagedResources will build a list of ResourceDiff based on the provided comparisonResult
// and persist app resources related data in the cache. Will return the persisted ApplicationTree.
2025-01-13 18:15:42 +00:00
func ( ctrl * ApplicationController ) setAppManagedResources ( destCluster * appv1 . Cluster , a * appv1 . Application , comparisonResult * comparisonResult ) ( * appv1 . ApplicationTree , error ) {
2024-07-04 10:18:18 +00:00
ts := stats . NewTimingStats ( )
defer func ( ) {
2025-05-20 19:48:09 +00:00
logCtx := log . WithFields ( applog . GetAppLogFields ( a ) )
2024-07-04 10:18:18 +00:00
for k , v := range ts . Timings ( ) {
logCtx = logCtx . WithField ( k , v . Milliseconds ( ) )
}
logCtx = logCtx . WithField ( "time_ms" , time . Since ( ts . StartTime ) . Milliseconds ( ) )
logCtx . Debug ( "Finished setting app managed resources" )
} ( )
2025-01-13 18:15:42 +00:00
managedResources , err := ctrl . hideSecretData ( destCluster , a , comparisonResult )
2024-07-04 10:18:18 +00:00
ts . AddCheckpoint ( "hide_secret_data_ms" )
2018-11-28 21:38:02 +00:00
if err != nil {
2024-06-11 19:33:22 +00:00
return nil , fmt . Errorf ( "error getting managed resources: %w" , err )
2019-02-13 23:20:40 +00:00
}
2025-01-13 18:15:42 +00:00
tree , err := ctrl . getResourceTree ( destCluster , a , managedResources )
2024-07-04 10:18:18 +00:00
ts . AddCheckpoint ( "get_resource_tree_ms" )
2019-02-13 23:20:40 +00:00
if err != nil {
2024-06-11 19:33:22 +00:00
return nil , fmt . Errorf ( "error getting resource tree: %w" , err )
2018-11-17 01:10:04 +00:00
}
2022-08-10 09:39:10 +00:00
err = ctrl . cache . SetAppResourcesTree ( a . InstanceName ( ctrl . namespace ) , tree )
2024-07-04 10:18:18 +00:00
ts . AddCheckpoint ( "set_app_resources_tree_ms" )
2019-02-13 23:20:40 +00:00
if err != nil {
2024-06-11 19:33:22 +00:00
return nil , fmt . Errorf ( "error setting app resource tree: %w" , err )
2021-11-03 19:46:05 +00:00
}
2022-08-10 09:39:10 +00:00
err = ctrl . cache . SetAppManagedResources ( a . InstanceName ( ctrl . namespace ) , managedResources )
2024-07-04 10:18:18 +00:00
ts . AddCheckpoint ( "set_app_managed_resources_ms" )
2021-11-03 19:46:05 +00:00
if err != nil {
2024-06-11 19:33:22 +00:00
return nil , fmt . Errorf ( "error setting app managed resources: %w" , err )
2019-02-13 23:20:40 +00:00
}
2021-11-03 19:46:05 +00:00
return tree , nil
2019-02-13 23:20:40 +00:00
}
2019-08-20 22:48:37 +00:00
// returns true of given resources exist in the namespace by default and not managed by the user
2020-07-09 23:19:47 +00:00
func isKnownOrphanedResourceExclusion ( key kube . ResourceKey , proj * appv1 . AppProject ) bool {
2019-08-20 22:48:37 +00:00
if key . Namespace == "default" && key . Group == "" && key . Kind == kube . ServiceKind && key . Name == "kubernetes" {
return true
}
if key . Group == "" && key . Kind == kube . ServiceAccountKind && key . Name == "default" {
return true
}
2021-02-17 10:25:21 +00:00
if key . Group == "" && key . Kind == "ConfigMap" && key . Name == "kube-root-ca.crt" {
return true
}
2020-07-09 23:19:47 +00:00
list := proj . Spec . OrphanedResources . Ignore
for _ , item := range list {
2020-07-20 20:55:19 +00:00
if item . Kind == "" || glob . Match ( item . Kind , key . Kind ) {
if glob . Match ( item . Group , key . Group ) {
if item . Name == "" || glob . Match ( item . Name , key . Name ) {
2020-07-09 23:19:47 +00:00
return true
}
}
}
}
2019-08-20 22:48:37 +00:00
return false
}
2025-01-13 18:15:42 +00:00
func ( ctrl * ApplicationController ) getResourceTree ( destCluster * appv1 . Cluster , a * appv1 . Application , managedResources [ ] * appv1 . ResourceDiff ) ( * appv1 . ApplicationTree , error ) {
2024-07-04 10:18:18 +00:00
ts := stats . NewTimingStats ( )
defer func ( ) {
2025-05-20 19:48:09 +00:00
logCtx := log . WithFields ( applog . GetAppLogFields ( a ) )
2024-07-04 10:18:18 +00:00
for k , v := range ts . Timings ( ) {
logCtx = logCtx . WithField ( k , v . Milliseconds ( ) )
}
logCtx = logCtx . WithField ( "time_ms" , time . Since ( ts . StartTime ) . Milliseconds ( ) )
logCtx . Debug ( "Finished getting resource tree" )
} ( )
2019-03-30 03:59:25 +00:00
nodes := make ( [ ] appv1 . ResourceNode , 0 )
2021-11-18 23:45:19 +00:00
proj , err := ctrl . getAppProj ( a )
2024-07-04 10:18:18 +00:00
ts . AddCheckpoint ( "get_app_proj_ms" )
2019-08-19 15:14:48 +00:00
if err != nil {
2023-01-09 21:32:27 +00:00
return nil , fmt . Errorf ( "failed to get project: %w" , err )
2019-08-19 15:14:48 +00:00
}
2022-12-16 20:47:08 +00:00
2019-08-19 15:14:48 +00:00
orphanedNodesMap := make ( map [ kube . ResourceKey ] appv1 . ResourceNode )
warnOrphaned := true
if proj . Spec . OrphanedResources != nil {
2025-01-13 18:15:42 +00:00
orphanedNodesMap , err = ctrl . stateCache . GetNamespaceTopLevelResources ( destCluster , a . Spec . Destination . Namespace )
2019-08-19 15:14:48 +00:00
if err != nil {
2023-01-09 21:32:27 +00:00
return nil , fmt . Errorf ( "failed to get namespace top-level resources: %w" , err )
2019-08-19 15:14:48 +00:00
}
warnOrphaned = proj . Spec . OrphanedResources . IsWarn ( )
}
2024-07-04 10:18:18 +00:00
ts . AddCheckpoint ( "get_orphaned_resources_ms" )
2024-07-19 15:37:13 +00:00
managedResourcesKeys := make ( [ ] kube . ResourceKey , 0 )
2019-03-14 21:54:34 +00:00
for i := range managedResources {
managedResource := managedResources [ i ]
2019-08-19 15:14:48 +00:00
delete ( orphanedNodesMap , kube . NewResourceKey ( managedResource . Group , managedResource . Kind , managedResource . Namespace , managedResource . Name ) )
2024-06-11 15:41:55 +00:00
live := & unstructured . Unstructured { }
2019-03-14 21:54:34 +00:00
err := json . Unmarshal ( [ ] byte ( managedResource . LiveState ) , & live )
if err != nil {
2023-01-09 21:32:27 +00:00
return nil , fmt . Errorf ( "failed to unmarshal live state of managed resources: %w" , err )
2019-03-14 21:54:34 +00:00
}
2019-03-30 03:59:25 +00:00
if live == nil {
2024-06-11 15:41:55 +00:00
target := & unstructured . Unstructured { }
2024-02-22 01:56:06 +00:00
err = json . Unmarshal ( [ ] byte ( managedResource . TargetState ) , & target )
if err != nil {
return nil , fmt . Errorf ( "failed to unmarshal target state of managed resources: %w" , err )
}
2019-03-30 03:59:25 +00:00
nodes = append ( nodes , appv1 . ResourceNode {
ResourceRef : appv1 . ResourceRef {
Version : target . GroupVersionKind ( ) . Version ,
Name : managedResource . Name ,
Kind : managedResource . Kind ,
Group : managedResource . Group ,
Namespace : managedResource . Namespace ,
} ,
2025-07-28 07:35:50 +00:00
Health : & appv1 . HealthStatus {
Status : health . HealthStatusMissing ,
} ,
2019-03-30 03:59:25 +00:00
} )
} else {
2024-07-19 15:37:13 +00:00
managedResourcesKeys = append ( managedResourcesKeys , kube . GetResourceKey ( live ) )
}
}
2025-12-15 02:30:41 +00:00
// Process managed resources and their children, including cross-namespace relationships
// from cluster-scoped parents (e.g., Crossplane CompositeResourceDefinitions)
2025-01-13 18:15:42 +00:00
err = ctrl . stateCache . IterateHierarchyV2 ( destCluster , managedResourcesKeys , func ( child appv1 . ResourceNode , _ string ) bool {
2025-12-03 20:55:28 +00:00
permitted , _ := proj . IsResourcePermitted ( schema . GroupKind { Group : child . Group , Kind : child . Kind } , child . Name , child . Namespace , destCluster , func ( project string ) ( [ ] * appv1 . Cluster , error ) {
2024-07-19 15:37:13 +00:00
clusters , err := ctrl . db . GetProjectClusters ( context . TODO ( ) , project )
2018-11-17 01:10:04 +00:00
if err != nil {
2024-07-19 15:37:13 +00:00
return nil , fmt . Errorf ( "failed to get project clusters: %w" , err )
2018-11-17 01:10:04 +00:00
}
2024-07-19 15:37:13 +00:00
return clusters , nil
} )
if ! permitted {
return false
2018-11-28 21:38:02 +00:00
}
2024-07-19 15:37:13 +00:00
nodes = append ( nodes , child )
return true
} )
if err != nil {
return nil , fmt . Errorf ( "failed to iterate resource hierarchy v2: %w" , err )
2018-11-28 21:38:02 +00:00
}
2024-07-04 10:18:18 +00:00
ts . AddCheckpoint ( "process_managed_resources_ms" )
2019-08-19 15:14:48 +00:00
orphanedNodes := make ( [ ] appv1 . ResourceNode , 0 )
2024-07-19 15:37:13 +00:00
orphanedNodesKeys := make ( [ ] kube . ResourceKey , 0 )
2019-08-19 15:14:48 +00:00
for k := range orphanedNodesMap {
2025-12-03 20:55:28 +00:00
if k . Namespace != "" && proj . IsGroupKindNamePermitted ( k . GroupKind ( ) , k . Name , true ) && ! isKnownOrphanedResourceExclusion ( k , proj ) {
2024-07-19 15:37:13 +00:00
orphanedNodesKeys = append ( orphanedNodesKeys , k )
}
}
2025-12-15 02:30:41 +00:00
// Process orphaned resources
2025-01-13 18:15:42 +00:00
err = ctrl . stateCache . IterateHierarchyV2 ( destCluster , orphanedNodesKeys , func ( child appv1 . ResourceNode , appName string ) bool {
2024-07-19 15:37:13 +00:00
belongToAnotherApp := false
if appName != "" {
appKey := ctrl . toAppKey ( appName )
if _ , exists , err := ctrl . appInformer . GetIndexer ( ) . GetByKey ( appKey ) ; exists && err == nil {
belongToAnotherApp = true
}
}
2022-09-08 11:33:10 +00:00
2024-07-19 15:37:13 +00:00
if belongToAnotherApp {
return false
}
2022-09-08 11:33:10 +00:00
2025-12-03 20:55:28 +00:00
permitted , _ := proj . IsResourcePermitted ( schema . GroupKind { Group : child . Group , Kind : child . Kind } , child . Name , child . Namespace , destCluster , func ( project string ) ( [ ] * appv1 . Cluster , error ) {
2024-07-19 15:37:13 +00:00
return ctrl . db . GetProjectClusters ( context . TODO ( ) , project )
} )
2022-09-08 11:33:10 +00:00
2024-07-19 15:37:13 +00:00
if ! permitted {
return false
2019-08-19 15:14:48 +00:00
}
2024-07-19 15:37:13 +00:00
orphanedNodes = append ( orphanedNodes , child )
return true
} )
if err != nil {
return nil , err
2019-08-19 15:14:48 +00:00
}
2024-07-19 15:37:13 +00:00
2019-08-19 18:12:07 +00:00
var conditions [ ] appv1 . ApplicationCondition
2019-08-19 15:14:48 +00:00
if len ( orphanedNodes ) > 0 && warnOrphaned {
2019-08-19 18:12:07 +00:00
conditions = [ ] appv1 . ApplicationCondition { {
2019-08-19 15:14:48 +00:00
Type : appv1 . ApplicationConditionOrphanedResourceWarning ,
Message : fmt . Sprintf ( "Application has %d orphaned resources" , len ( orphanedNodes ) ) ,
2019-08-19 18:12:07 +00:00
} }
2019-08-19 15:14:48 +00:00
}
2024-11-05 13:18:36 +00:00
ctrl . metricsServer . SetOrphanedResourcesMetric ( a , len ( orphanedNodes ) )
2019-08-19 18:12:07 +00:00
a . Status . SetConditions ( conditions , map [ appv1 . ApplicationConditionType ] bool { appv1 . ApplicationConditionOrphanedResourceWarning : true } )
2020-07-21 23:59:00 +00:00
sort . Slice ( orphanedNodes , func ( i , j int ) bool {
return orphanedNodes [ i ] . ResourceRef . String ( ) < orphanedNodes [ j ] . ResourceRef . String ( )
} )
2024-07-04 10:18:18 +00:00
ts . AddCheckpoint ( "process_orphaned_resources_ms" )
2021-01-11 20:35:47 +00:00
2025-01-13 18:15:42 +00:00
hosts , err := ctrl . getAppHosts ( destCluster , a , nodes )
2021-01-11 20:35:47 +00:00
if err != nil {
2023-01-09 21:32:27 +00:00
return nil , fmt . Errorf ( "failed to get app hosts: %w" , err )
2021-01-11 20:35:47 +00:00
}
2024-07-04 10:18:18 +00:00
ts . AddCheckpoint ( "get_app_hosts_ms" )
2021-01-11 20:35:47 +00:00
return & appv1 . ApplicationTree { Nodes : nodes , OrphanedNodes : orphanedNodes , Hosts : hosts } , nil
}
2025-01-13 18:15:42 +00:00
func ( ctrl * ApplicationController ) getAppHosts ( destCluster * appv1 . Cluster , a * appv1 . Application , appNodes [ ] appv1 . ResourceNode ) ( [ ] appv1 . HostInfo , error ) {
2024-07-04 10:18:18 +00:00
ts := stats . NewTimingStats ( )
defer func ( ) {
2025-05-20 19:48:09 +00:00
logCtx := log . WithFields ( applog . GetAppLogFields ( a ) )
2024-07-04 10:18:18 +00:00
for k , v := range ts . Timings ( ) {
logCtx = logCtx . WithField ( k , v . Milliseconds ( ) )
}
logCtx = logCtx . WithField ( "time_ms" , time . Since ( ts . StartTime ) . Milliseconds ( ) )
logCtx . Debug ( "Finished getting app hosts" )
} ( )
2025-01-03 16:10:00 +00:00
supportedResourceNames := map [ corev1 . ResourceName ] bool {
corev1 . ResourceCPU : true ,
corev1 . ResourceStorage : true ,
corev1 . ResourceMemory : true ,
2021-01-11 20:35:47 +00:00
}
appPods := map [ kube . ResourceKey ] bool { }
for _ , node := range appNodes {
if node . Group == "" && node . Kind == kube . PodKind {
appPods [ kube . NewResourceKey ( node . Group , node . Kind , node . Namespace , node . Name ) ] = true
}
}
2021-05-18 23:01:23 +00:00
2021-01-11 20:35:47 +00:00
allNodesInfo := map [ string ] statecache . NodeInfo { }
allPodsByNode := map [ string ] [ ] statecache . PodInfo { }
appPodsByNode := map [ string ] [ ] statecache . PodInfo { }
2025-01-13 18:15:42 +00:00
err := ctrl . stateCache . IterateResources ( destCluster , func ( res * clustercache . Resource , info * statecache . ResourceInfo ) {
2021-01-11 20:35:47 +00:00
key := res . ResourceKey ( )
2021-05-18 23:01:23 +00:00
2021-01-11 20:35:47 +00:00
switch {
case info . NodeInfo != nil && key . Group == "" && key . Kind == "Node" :
allNodesInfo [ key . Name ] = * info . NodeInfo
case info . PodInfo != nil && key . Group == "" && key . Kind == kube . PodKind :
if appPods [ key ] {
appPodsByNode [ info . PodInfo . NodeName ] = append ( appPodsByNode [ info . PodInfo . NodeName ] , * info . PodInfo )
} else {
allPodsByNode [ info . PodInfo . NodeName ] = append ( allPodsByNode [ info . PodInfo . NodeName ] , * info . PodInfo )
}
}
} )
2024-07-04 10:18:18 +00:00
ts . AddCheckpoint ( "iterate_resources_ms" )
2021-01-11 20:35:47 +00:00
if err != nil {
return nil , err
}
var hosts [ ] appv1 . HostInfo
for nodeName , appPods := range appPodsByNode {
node , ok := allNodesInfo [ nodeName ]
if ! ok {
continue
}
neighbors := allPodsByNode [ nodeName ]
2025-01-03 16:10:00 +00:00
resources := map [ corev1 . ResourceName ] appv1 . HostResourceInfo { }
2021-01-11 20:35:47 +00:00
for name , resource := range node . Capacity {
info := resources [ name ]
info . ResourceName = name
info . Capacity += resource . MilliValue ( )
resources [ name ] = info
}
for _ , pod := range appPods {
for name , resource := range pod . ResourceRequests {
if ! supportedResourceNames [ name ] {
continue
}
info := resources [ name ]
info . RequestedByApp += resource . MilliValue ( )
resources [ name ] = info
}
}
for _ , pod := range neighbors {
for name , resource := range pod . ResourceRequests {
2025-01-03 16:10:00 +00:00
if ! supportedResourceNames [ name ] || pod . Phase == corev1 . PodSucceeded || pod . Phase == corev1 . PodFailed {
2021-01-11 20:35:47 +00:00
continue
}
info := resources [ name ]
info . RequestedByNeighbors += resource . MilliValue ( )
resources [ name ] = info
}
}
var resourcesInfo [ ] appv1 . HostResourceInfo
for _ , info := range resources {
if supportedResourceNames [ info . ResourceName ] && info . Capacity > 0 {
resourcesInfo = append ( resourcesInfo , info )
}
}
sort . Slice ( resourcesInfo , func ( i , j int ) bool {
return resourcesInfo [ i ] . ResourceName < resourcesInfo [ j ] . ResourceName
} )
2025-06-13 15:19:27 +00:00
allowedNodeLabels := ctrl . settingsMgr . GetAllowedNodeLabels ( )
nodeLabels := make ( map [ string ] string )
for _ , label := range allowedNodeLabels {
if val , ok := node . Labels [ label ] ; ok {
nodeLabels [ label ] = val
}
}
hosts = append ( hosts , appv1 . HostInfo { Name : nodeName , SystemInfo : node . SystemInfo , ResourcesInfo : resourcesInfo , Labels : nodeLabels } )
2021-01-11 20:35:47 +00:00
}
2024-07-04 10:18:18 +00:00
ts . AddCheckpoint ( "process_app_pods_by_node_ms" )
2021-01-11 20:35:47 +00:00
return hosts , nil
2018-11-28 21:38:02 +00:00
}
2025-01-13 18:15:42 +00:00
func ( ctrl * ApplicationController ) hideSecretData ( destCluster * appv1 . Cluster , app * appv1 . Application , comparisonResult * comparisonResult ) ( [ ] * appv1 . ResourceDiff , error ) {
2019-02-22 21:19:10 +00:00
items := make ( [ ] * appv1 . ResourceDiff , len ( comparisonResult . managedResources ) )
for i := range comparisonResult . managedResources {
res := comparisonResult . managedResources [ i ]
2018-12-04 01:39:55 +00:00
item := appv1 . ResourceDiff {
2021-01-28 00:13:29 +00:00
Namespace : res . Namespace ,
Name : res . Name ,
Group : res . Group ,
Kind : res . Kind ,
Hook : res . Hook ,
ResourceVersion : res . ResourceVersion ,
2018-11-28 21:38:02 +00:00
}
2018-12-07 23:40:55 +00:00
target := res . Target
live := res . Live
resDiff := res . Diff
if res . Kind == kube . SecretKind && res . Group == "" {
var err error
2024-10-30 16:52:37 +00:00
target , live , err = diff . HideSecretData ( res . Target , res . Live , ctrl . settingsMgr . GetSensitiveAnnotations ( ) )
2018-12-07 23:40:55 +00:00
if err != nil {
2024-06-11 19:33:22 +00:00
return nil , fmt . Errorf ( "error hiding secret data: %w" , err )
2018-12-07 23:40:55 +00:00
}
2020-05-13 20:34:43 +00:00
compareOptions , err := ctrl . settingsMgr . GetResourceCompareOptions ( )
if err != nil {
2024-06-11 19:33:22 +00:00
return nil , fmt . Errorf ( "error getting resource compare options: %w" , err )
2020-05-13 20:34:43 +00:00
}
2021-12-22 16:57:48 +00:00
resourceOverrides , err := ctrl . settingsMgr . GetResourceOverrides ( )
if err != nil {
2024-06-11 19:33:22 +00:00
return nil , fmt . Errorf ( "error getting resource overrides: %w" , err )
2021-12-22 16:57:48 +00:00
}
appLabelKey , err := ctrl . settingsMgr . GetAppInstanceLabelKey ( )
if err != nil {
2024-06-11 19:33:22 +00:00
return nil , fmt . Errorf ( "error getting app instance label key: %w" , err )
2021-12-22 16:57:48 +00:00
}
trackingMethod , err := ctrl . settingsMgr . GetTrackingMethod ( )
if err != nil {
2024-06-11 19:33:22 +00:00
return nil , fmt . Errorf ( "error getting tracking method: %w" , err )
2021-12-22 16:57:48 +00:00
}
2025-01-13 18:15:42 +00:00
clusterCache , err := ctrl . stateCache . GetClusterCache ( destCluster )
2022-05-31 17:21:22 +00:00
if err != nil {
2024-06-11 19:33:22 +00:00
return nil , fmt . Errorf ( "error getting cluster cache: %w" , err )
2022-05-31 17:21:22 +00:00
}
2021-12-22 16:57:48 +00:00
diffConfig , err := argodiff . NewDiffConfigBuilder ( ) .
2024-04-26 09:24:02 +00:00
WithDiffSettings ( app . Spec . IgnoreDifferences , resourceOverrides , compareOptions . IgnoreAggregatedRoles , ctrl . ignoreNormalizerOpts ) .
2021-12-22 16:57:48 +00:00
WithTracking ( appLabelKey , trackingMethod ) .
WithNoCache ( ) .
WithLogger ( logutils . NewLogrusLogger ( logutils . NewWithCurrentConfig ( ) ) ) .
2022-05-31 17:21:22 +00:00
WithGVKParser ( clusterCache . GetGVKParser ( ) ) .
2021-12-22 16:57:48 +00:00
Build ( )
if err != nil {
2024-06-11 19:33:22 +00:00
return nil , fmt . Errorf ( "appcontroller error building diff config: %w" , err )
2021-12-22 16:57:48 +00:00
}
diffResult , err := argodiff . StateDiff ( live , target , diffConfig )
2019-12-26 22:42:56 +00:00
if err != nil {
2024-06-11 19:33:22 +00:00
return nil , fmt . Errorf ( "error applying diff: %w" , err )
2019-12-26 22:42:56 +00:00
}
2021-12-22 16:57:48 +00:00
resDiff = diffResult
2018-12-07 23:40:55 +00:00
}
2018-11-28 21:38:02 +00:00
if live != nil {
data , err := json . Marshal ( live )
if err != nil {
2024-06-11 19:33:22 +00:00
return nil , fmt . Errorf ( "error marshaling live json: %w" , err )
2018-11-17 01:10:04 +00:00
}
2018-11-28 21:38:02 +00:00
item . LiveState = string ( data )
} else {
item . LiveState = "null"
}
if target != nil {
data , err := json . Marshal ( target )
if err != nil {
2024-06-11 19:33:22 +00:00
return nil , fmt . Errorf ( "error marshaling target json: %w" , err )
2018-11-17 01:10:04 +00:00
}
2018-11-28 21:38:02 +00:00
item . TargetState = string ( data )
} else {
item . TargetState = "null"
2018-11-17 01:10:04 +00:00
}
2019-12-26 22:42:56 +00:00
item . PredictedLiveState = string ( resDiff . PredictedLive )
item . NormalizedLiveState = string ( resDiff . NormalizedLive )
2021-01-28 00:13:29 +00:00
item . Modified = resDiff . Modified
2018-11-28 21:38:02 +00:00
items [ i ] = & item
2018-11-17 01:10:04 +00:00
}
2019-02-13 23:20:40 +00:00
return items , nil
2018-11-17 01:10:04 +00:00
}
2018-02-20 23:23:37 +00:00
// Run starts the Application CRD controller.
2018-05-11 18:50:32 +00:00
func ( ctrl * ApplicationController ) Run ( ctx context . Context , statusProcessors int , operationProcessors int ) {
2018-02-20 23:23:37 +00:00
defer runtime . HandleCrash ( )
2018-05-11 18:50:32 +00:00
defer ctrl . appRefreshQueue . ShutDown ( )
2020-01-08 22:07:36 +00:00
defer ctrl . appComparisonTypeRefreshQueue . ShutDown ( )
defer ctrl . appOperationQueue . ShutDown ( )
2020-07-22 00:25:41 +00:00
defer ctrl . projectRefreshQueue . ShutDown ( )
2024-12-16 21:59:09 +00:00
defer ctrl . appHydrateQueue . ShutDown ( )
defer ctrl . hydrationQueue . ShutDown ( )
2018-02-20 23:23:37 +00:00
2020-06-04 18:36:26 +00:00
ctrl . RegisterClusterSecretUpdater ( ctx )
2025-01-29 18:06:29 +00:00
ctrl . metricsServer . RegisterClustersInfoSource ( ctx , ctrl . stateCache , ctrl . db , ctrl . metricsClusterLabels )
2020-06-04 18:36:26 +00:00
2024-02-11 18:32:17 +00:00
if ctrl . dynamicClusterDistributionEnabled {
// only start deployment informer if dynamic distribution is enabled
go ctrl . deploymentInformer . Informer ( ) . Run ( ctx . Done ( ) )
}
2018-02-20 23:23:37 +00:00
2024-01-11 06:32:11 +00:00
clusters , err := ctrl . db . ListClusters ( ctx )
if err != nil {
2025-09-27 21:47:21 +00:00
log . WithError ( err ) . Warn ( "Cannot init sharding. Error while querying clusters list from database" )
2024-01-11 06:32:11 +00:00
} else {
2024-03-01 18:56:48 +00:00
appItems , err := ctrl . getAppList ( metav1 . ListOptions { } )
if err != nil {
2025-09-27 21:47:21 +00:00
log . WithError ( err ) . Warn ( "Cannot init sharding. Error while querying application list from database" )
2024-03-01 18:56:48 +00:00
} else {
ctrl . clusterSharding . Init ( clusters , appItems )
}
2024-01-11 06:32:11 +00:00
}
2025-04-17 02:03:14 +00:00
go ctrl . appInformer . Run ( ctx . Done ( ) )
go ctrl . projInformer . Run ( ctx . Done ( ) )
2020-06-12 22:05:01 +00:00
errors . CheckError ( ctrl . stateCache . Init ( ) )
2019-01-08 22:53:45 +00:00
if ! cache . WaitForCacheSync ( ctx . Done ( ) , ctrl . appInformer . HasSynced , ctrl . projInformer . HasSynced ) {
2018-02-20 23:23:37 +00:00
log . Error ( "Timed out waiting for caches to sync" )
return
}
2019-06-21 22:59:05 +00:00
go func ( ) { errors . CheckError ( ctrl . stateCache . Run ( ctx ) ) } ( )
2019-02-22 23:20:34 +00:00
go func ( ) { errors . CheckError ( ctrl . metricsServer . ListenAndServe ( ) ) } ( )
2018-09-10 15:20:17 +00:00
2026-02-06 22:16:37 +00:00
for range statusProcessors {
2018-05-11 18:50:32 +00:00
go wait . Until ( func ( ) {
for ctrl . processAppRefreshQueueItem ( ) {
}
} , time . Second , ctx . Done ( ) )
}
2026-02-06 22:16:37 +00:00
for range operationProcessors {
2018-05-11 18:50:32 +00:00
go wait . Until ( func ( ) {
for ctrl . processAppOperationQueueItem ( ) {
}
} , time . Second , ctx . Done ( ) )
2018-02-20 23:23:37 +00:00
}
2020-01-08 22:07:36 +00:00
go wait . Until ( func ( ) {
for ctrl . processAppComparisonTypeQueueItem ( ) {
}
} , time . Second , ctx . Done ( ) )
2020-07-22 00:25:41 +00:00
go wait . Until ( func ( ) {
for ctrl . processProjectQueueItem ( ) {
}
} , time . Second , ctx . Done ( ) )
2024-12-16 21:59:09 +00:00
if ctrl . hydrator != nil {
go wait . Until ( func ( ) {
for ctrl . processAppHydrateQueueItem ( ) {
}
} , time . Second , ctx . Done ( ) )
go wait . Until ( func ( ) {
for ctrl . processHydrationQueueItem ( ) {
}
} , time . Second , ctx . Done ( ) )
}
2018-02-20 23:23:37 +00:00
<- ctx . Done ( )
}
2022-08-10 09:39:10 +00:00
// requestAppRefresh adds a request for given app to the refresh queue. appName
// needs to be the qualified name of the application, i.e. <namespace>/<name>.
2020-01-08 22:07:36 +00:00
func ( ctrl * ApplicationController ) requestAppRefresh ( appName string , compareWith * CompareWith , after * time . Duration ) {
2022-08-10 09:39:10 +00:00
key := ctrl . toAppKey ( appName )
2022-12-16 20:47:08 +00:00
2020-01-08 22:07:36 +00:00
if compareWith != nil && after != nil {
2025-01-16 05:42:25 +00:00
ctrl . appComparisonTypeRefreshQueue . AddAfter ( fmt . Sprintf ( "%s/%d" , key , * compareWith ) , * after )
2020-01-08 22:07:36 +00:00
} else {
if compareWith != nil {
ctrl . refreshRequestedAppsMutex . Lock ( )
2022-08-10 09:39:10 +00:00
ctrl . refreshRequestedApps [ key ] = compareWith . Max ( ctrl . refreshRequestedApps [ key ] )
2020-01-08 22:07:36 +00:00
ctrl . refreshRequestedAppsMutex . Unlock ( )
}
if after != nil {
ctrl . appRefreshQueue . AddAfter ( key , * after )
} else {
2023-10-18 19:08:04 +00:00
ctrl . appRefreshQueue . AddRateLimited ( key )
2020-01-08 22:07:36 +00:00
}
}
2018-04-11 19:53:33 +00:00
}
2019-07-15 20:34:26 +00:00
func ( ctrl * ApplicationController ) isRefreshRequested ( appName string ) ( bool , CompareWith ) {
2018-12-18 02:23:35 +00:00
ctrl . refreshRequestedAppsMutex . Lock ( )
defer ctrl . refreshRequestedAppsMutex . Unlock ( )
2019-07-15 20:34:26 +00:00
level , ok := ctrl . refreshRequestedApps [ appName ]
2018-04-11 19:53:33 +00:00
if ok {
2018-12-18 02:23:35 +00:00
delete ( ctrl . refreshRequestedApps , appName )
2018-04-11 19:53:33 +00:00
}
2019-07-15 20:34:26 +00:00
return ok , level
2018-04-11 19:53:33 +00:00
}
2018-05-16 23:30:28 +00:00
func ( ctrl * ApplicationController ) processAppOperationQueueItem ( ) ( processNext bool ) {
2018-05-11 18:50:32 +00:00
appKey , shutdown := ctrl . appOperationQueue . Get ( )
if shutdown {
2018-05-16 23:30:28 +00:00
processNext = false
2025-09-22 04:05:06 +00:00
return processNext
2018-05-11 18:50:32 +00:00
}
2018-11-19 20:25:45 +00:00
processNext = true
2018-05-16 23:30:28 +00:00
defer func ( ) {
if r := recover ( ) ; r != nil {
2025-09-27 21:47:21 +00:00
log . WithField ( "appkey" , appKey ) . Errorf ( "Recovered from panic: %+v\n%s" , r , debug . Stack ( ) )
2018-05-16 23:30:28 +00:00
}
ctrl . appOperationQueue . Done ( appKey )
} ( )
2024-08-23 22:28:34 +00:00
obj , exists , err := ctrl . appInformer . GetIndexer ( ) . GetByKey ( appKey )
2018-05-11 18:50:32 +00:00
if err != nil {
2025-09-27 21:47:21 +00:00
log . WithField ( "appkey" , appKey ) . WithError ( err ) . Error ( "Failed to get application from informer index" )
2025-09-22 04:05:06 +00:00
return processNext
2018-05-11 18:50:32 +00:00
}
if ! exists {
// This happens after app was deleted, but the work queue still had an entry for it.
2025-09-22 04:05:06 +00:00
return processNext
2018-05-11 18:50:32 +00:00
}
2020-10-23 19:05:41 +00:00
origApp , ok := obj . ( * appv1 . Application )
2018-05-11 18:50:32 +00:00
if ! ok {
2025-09-27 21:47:21 +00:00
log . WithField ( "appkey" , appKey ) . Warn ( "Key in index is not an application" )
2025-09-22 04:05:06 +00:00
return processNext
2018-05-16 23:30:28 +00:00
}
2020-10-23 19:05:41 +00:00
app := origApp . DeepCopy ( )
2025-05-20 19:48:09 +00:00
logCtx := log . WithFields ( applog . GetAppLogFields ( app ) )
2024-07-04 10:18:18 +00:00
ts := stats . NewTimingStats ( )
defer func ( ) {
for k , v := range ts . Timings ( ) {
logCtx = logCtx . WithField ( k , v . Milliseconds ( ) )
}
logCtx = logCtx . WithField ( "time_ms" , time . Since ( ts . StartTime ) . Milliseconds ( ) )
logCtx . Debug ( "Finished processing app operation queue item" )
} ( )
2020-10-23 19:05:41 +00:00
2018-05-16 23:30:28 +00:00
if app . Operation != nil {
2022-09-08 11:33:10 +00:00
// If we get here, we are about to process an operation, but we cannot rely on informer since it might have stale data.
2021-10-20 00:16:31 +00:00
// So always retrieve the latest version to ensure it is not stale to avoid unnecessary syncing.
// We cannot rely on informer since applications might be updated by both application controller and api server.
2025-03-27 16:37:52 +00:00
freshApp , err := ctrl . applicationClientset . ArgoprojV1alpha1 ( ) . Applications ( app . ObjectMeta . Namespace ) . Get ( context . Background ( ) , app . Name , metav1 . GetOptions { } )
2021-10-20 00:16:31 +00:00
if err != nil {
2025-09-27 21:47:21 +00:00
logCtx . WithError ( err ) . Error ( "Failed to retrieve latest application state" )
2025-09-22 04:05:06 +00:00
return processNext
2021-10-20 00:16:31 +00:00
}
app = freshApp
2021-10-20 15:03:55 +00:00
}
2024-07-04 10:18:18 +00:00
ts . AddCheckpoint ( "get_fresh_app_ms" )
2021-10-20 00:16:31 +00:00
2021-10-20 15:03:55 +00:00
if app . Operation != nil {
2018-05-16 23:30:28 +00:00
ctrl . processRequestedAppOperation ( app )
2024-07-04 10:18:18 +00:00
ts . AddCheckpoint ( "process_requested_app_operation_ms" )
2023-12-18 16:40:23 +00:00
} else if app . DeletionTimestamp != nil {
if err = ctrl . finalizeApplicationDeletion ( app , func ( project string ) ( [ ] * appv1 . Cluster , error ) {
2022-09-08 11:33:10 +00:00
return ctrl . db . GetProjectClusters ( context . Background ( ) , project )
2023-12-18 16:40:23 +00:00
} ) ; err != nil {
2018-11-19 20:25:45 +00:00
ctrl . setAppCondition ( app , appv1 . ApplicationCondition {
Type : appv1 . ApplicationConditionDeletionError ,
Message : err . Error ( ) ,
} )
message := fmt . Sprintf ( "Unable to delete application resources: %v" , err . Error ( ) )
2025-01-07 15:08:51 +00:00
ctrl . logAppEvent ( context . TODO ( ) , app , argo . EventInfo { Reason : argo . EventReasonStatusRefreshed , Type : corev1 . EventTypeWarning } , message )
2025-12-05 20:27:03 +00:00
} else {
// Clear DeletionError condition if deletion is progressing successfully
app . Status . SetConditions ( [ ] appv1 . ApplicationCondition { } , map [ appv1 . ApplicationConditionType ] bool { appv1 . ApplicationConditionDeletionError : true } )
2018-11-19 20:25:45 +00:00
}
2024-07-04 10:18:18 +00:00
ts . AddCheckpoint ( "finalize_application_deletion_ms" )
2018-05-11 18:50:32 +00:00
}
2025-09-22 04:05:06 +00:00
return processNext
2018-05-16 23:30:28 +00:00
}
2020-01-08 22:07:36 +00:00
func ( ctrl * ApplicationController ) processAppComparisonTypeQueueItem ( ) ( processNext bool ) {
key , shutdown := ctrl . appComparisonTypeRefreshQueue . Get ( )
processNext = true
defer func ( ) {
if r := recover ( ) ; r != nil {
2025-09-27 21:47:21 +00:00
log . WithField ( "appkey" , key ) . Errorf ( "Recovered from panic: %+v\n%s" , r , debug . Stack ( ) )
2020-01-08 22:07:36 +00:00
}
ctrl . appComparisonTypeRefreshQueue . Done ( key )
} ( )
if shutdown {
processNext = false
2025-09-22 04:05:06 +00:00
return processNext
2020-01-08 22:07:36 +00:00
}
2024-08-23 22:28:34 +00:00
if parts := strings . Split ( key , "/" ) ; len ( parts ) != 3 {
2025-09-27 21:47:21 +00:00
log . WithField ( "appkey" , key ) . Warn ( "Unexpected key format in appComparisonTypeRefreshTypeQueue. Key should consist of namespace/name/comparisonType" )
2020-01-08 22:07:36 +00:00
} else {
2025-01-07 15:25:22 +00:00
compareWith , err := strconv . Atoi ( parts [ 2 ] )
if err != nil {
2025-09-27 21:47:21 +00:00
log . WithField ( "appkey" , key ) . WithError ( err ) . Warn ( "Unable to parse comparison type" )
2025-09-22 04:05:06 +00:00
return processNext
2020-01-08 22:07:36 +00:00
}
2025-01-07 15:25:22 +00:00
ctrl . requestAppRefresh ( ctrl . toAppQualifiedName ( parts [ 1 ] , parts [ 0 ] ) , CompareWith ( compareWith ) . Pointer ( ) , nil )
2020-01-08 22:07:36 +00:00
}
2025-09-22 04:05:06 +00:00
return processNext
2020-01-08 22:07:36 +00:00
}
2020-07-22 00:25:41 +00:00
func ( ctrl * ApplicationController ) processProjectQueueItem ( ) ( processNext bool ) {
key , shutdown := ctrl . projectRefreshQueue . Get ( )
processNext = true
defer func ( ) {
if r := recover ( ) ; r != nil {
2025-09-27 21:47:21 +00:00
log . WithField ( "key" , key ) . Errorf ( "Recovered from panic: %+v\n%s" , r , debug . Stack ( ) )
2020-07-22 00:25:41 +00:00
}
ctrl . projectRefreshQueue . Done ( key )
} ( )
if shutdown {
processNext = false
2025-09-22 04:05:06 +00:00
return processNext
2020-07-22 00:25:41 +00:00
}
2024-08-23 22:28:34 +00:00
obj , exists , err := ctrl . projInformer . GetIndexer ( ) . GetByKey ( key )
2020-07-22 00:25:41 +00:00
if err != nil {
2025-09-27 21:47:21 +00:00
log . WithField ( "key" , key ) . WithError ( err ) . Error ( "Failed to get project from informer index" )
2025-09-22 04:05:06 +00:00
return processNext
2020-07-22 00:25:41 +00:00
}
if ! exists {
// This happens after appproj was deleted, but the work queue still had an entry for it.
2025-09-22 04:05:06 +00:00
return processNext
2020-07-22 00:25:41 +00:00
}
origProj , ok := obj . ( * appv1 . AppProject )
if ! ok {
2025-09-27 21:47:21 +00:00
log . WithField ( "key" , key ) . Warnf ( "Key in index is not an appproject" )
2025-09-22 04:05:06 +00:00
return processNext
2020-07-22 00:25:41 +00:00
}
if origProj . DeletionTimestamp != nil && origProj . HasFinalizer ( ) {
if err := ctrl . finalizeProjectDeletion ( origProj . DeepCopy ( ) ) ; err != nil {
2025-09-27 21:47:21 +00:00
log . WithError ( err ) . Warn ( "Failed to finalize project deletion" )
2020-07-22 00:25:41 +00:00
}
}
2025-09-22 04:05:06 +00:00
return processNext
2020-07-22 00:25:41 +00:00
}
func ( ctrl * ApplicationController ) finalizeProjectDeletion ( proj * appv1 . AppProject ) error {
apps , err := ctrl . appLister . Applications ( ctrl . namespace ) . List ( labels . Everything ( ) )
if err != nil {
2022-10-19 19:21:32 +00:00
return fmt . Errorf ( "error listing applications: %w" , err )
2020-07-22 00:25:41 +00:00
}
appsCount := 0
for i := range apps {
if apps [ i ] . Spec . GetProject ( ) == proj . Name {
appsCount ++
}
}
if appsCount == 0 {
return ctrl . removeProjectFinalizer ( proj )
}
2025-01-07 15:25:22 +00:00
log . Infof ( "Cannot remove project '%s' finalizer as is referenced by %d applications" , proj . Name , appsCount )
2020-07-22 00:25:41 +00:00
return nil
}
func ( ctrl * ApplicationController ) removeProjectFinalizer ( proj * appv1 . AppProject ) error {
proj . RemoveFinalizer ( )
var patch [ ] byte
2025-01-02 23:26:59 +00:00
patch , _ = json . Marshal ( map [ string ] any {
"metadata" : map [ string ] any {
2020-07-22 00:25:41 +00:00
"finalizers" : proj . Finalizers ,
} ,
} )
2020-08-05 18:36:40 +00:00
_ , err := ctrl . applicationClientset . ArgoprojV1alpha1 ( ) . AppProjects ( ctrl . namespace ) . Patch ( context . Background ( ) , proj . Name , types . MergePatchType , patch , metav1 . PatchOptions { } )
2020-07-22 00:25:41 +00:00
return err
}
2020-05-27 17:22:13 +00:00
// shouldBeDeleted returns whether a given resource obj should be deleted on cascade delete of application app
2019-12-26 22:08:14 +00:00
func ( ctrl * ApplicationController ) shouldBeDeleted ( app * appv1 . Application , obj * unstructured . Unstructured ) bool {
2023-04-24 20:41:21 +00:00
return ! kube . IsCRD ( obj ) && ! isSelfReferencedApp ( app , kube . GetObjectRef ( obj ) ) &&
! resourceutil . HasAnnotationOption ( obj , synccommon . AnnotationSyncOptions , synccommon . SyncOptionDisableDeletion ) &&
! resourceutil . HasAnnotationOption ( obj , helm . ResourcePolicyAnnotation , helm . ResourcePolicyKeep )
2019-12-26 22:08:14 +00:00
}
2025-01-13 18:15:42 +00:00
func ( ctrl * ApplicationController ) getPermittedAppLiveObjects ( destCluster * appv1 . Cluster , app * appv1 . Application , proj * appv1 . AppProject , projectClusters func ( project string ) ( [ ] * appv1 . Cluster , error ) ) ( map [ kube . ResourceKey ] * unstructured . Unstructured , error ) {
objsMap , err := ctrl . stateCache . GetManagedLiveObjs ( destCluster , app , [ ] * unstructured . Unstructured { } )
2019-12-26 22:08:14 +00:00
if err != nil {
return nil , err
}
// Don't delete live resources which are not permitted in the app project
for k , v := range objsMap {
2025-01-13 18:15:42 +00:00
permitted , err := proj . IsLiveResourcePermitted ( v , destCluster , projectClusters )
2022-09-08 11:33:10 +00:00
if err != nil {
return nil , err
}
if ! permitted {
2019-12-26 22:08:14 +00:00
delete ( objsMap , k )
}
2019-12-09 16:39:20 +00:00
}
2019-12-26 22:08:14 +00:00
return objsMap , nil
2019-05-06 19:49:29 +00:00
}
2023-12-18 16:40:23 +00:00
func ( ctrl * ApplicationController ) finalizeApplicationDeletion ( app * appv1 . Application , projectClusters func ( project string ) ( [ ] * appv1 . Cluster , error ) ) error {
2025-05-20 19:48:09 +00:00
logCtx := log . WithFields ( applog . GetAppLogFields ( app ) )
2018-05-16 23:30:28 +00:00
// Get refreshed application info, since informer app copy might be stale
2020-08-05 18:36:40 +00:00
app , err := ctrl . applicationClientset . ArgoprojV1alpha1 ( ) . Applications ( app . Namespace ) . Get ( context . Background ( ) , app . Name , metav1 . GetOptions { } )
2018-05-16 23:30:28 +00:00
if err != nil {
2025-01-03 17:09:37 +00:00
if ! apierrors . IsNotFound ( err ) {
2025-09-27 21:47:21 +00:00
logCtx . WithError ( err ) . Error ( "Unable to get refreshed application info prior deleting resources" )
2018-05-16 23:30:28 +00:00
}
2023-12-18 16:40:23 +00:00
return nil
2018-05-15 07:36:11 +00:00
}
2019-12-26 22:08:14 +00:00
proj , err := ctrl . getAppProj ( app )
if err != nil {
2023-12-18 16:40:23 +00:00
return err
2018-11-19 20:25:45 +00:00
}
2025-12-05 20:27:03 +00:00
// Get destination cluster
2025-01-13 18:15:42 +00:00
destCluster , err := argo . GetDestinationCluster ( context . Background ( ) , app . Spec . Destination , ctrl . db )
if err != nil {
2025-09-27 21:47:21 +00:00
logCtx . WithError ( err ) . Warn ( "Unable to get destination cluster" )
2023-12-18 16:40:23 +00:00
app . UnSetCascadedDeletion ( )
2025-09-08 15:50:23 +00:00
app . UnSetPostDeleteFinalizerAll ( )
2025-12-05 20:27:03 +00:00
app . UnSetPreDeleteFinalizerAll ( )
2023-12-18 16:40:23 +00:00
if err := ctrl . updateFinalizers ( app ) ; err != nil {
return err
2019-04-08 22:08:48 +00:00
}
2023-12-18 16:40:23 +00:00
logCtx . Infof ( "Resource entries removed from undefined cluster" )
return nil
2018-11-28 21:38:02 +00:00
}
2025-12-05 20:27:03 +00:00
2025-01-13 18:15:42 +00:00
clusterRESTConfig , err := destCluster . RESTConfig ( )
2024-10-06 14:55:26 +00:00
if err != nil {
return err
}
config := metrics . AddMetricsTransportWrapper ( ctrl . metricsServer , app , clusterRESTConfig )
2019-04-29 19:42:59 +00:00
2025-10-06 14:30:44 +00:00
// Apply impersonation config if necessary
if err := ctrl . applyImpersonationConfig ( config , proj , app , destCluster ) ; err != nil {
return fmt . Errorf ( "cannot apply impersonation: %w" , err )
}
2025-12-05 20:27:03 +00:00
// Handle PreDelete hooks - run them before any deletion occurs
if app . HasPreDeleteFinalizer ( ) {
objsMap , err := ctrl . getPermittedAppLiveObjects ( destCluster , app , proj , projectClusters )
if err != nil {
return fmt . Errorf ( "error getting permitted app live objects: %w" , err )
}
done , err := ctrl . executePreDeleteHooks ( app , proj , objsMap , config , logCtx )
if err != nil {
return fmt . Errorf ( "error executing pre-delete hooks: %w" , err )
}
if ! done {
// PreDelete hooks are still running - wait for them to complete
return nil
}
// PreDelete hooks are done - remove the finalizer so we can continue with deletion
app . UnSetPreDeleteFinalizer ( )
if err := ctrl . updateFinalizers ( app ) ; err != nil {
return fmt . Errorf ( "error updating pre-delete finalizers: %w" , err )
}
}
2023-12-18 16:40:23 +00:00
if app . CascadedDeletion ( ) {
2024-10-24 07:08:24 +00:00
deletionApproved := app . IsDeletionConfirmed ( app . DeletionTimestamp . Time )
2023-12-18 16:40:23 +00:00
logCtx . Infof ( "Deleting resources" )
2021-06-29 20:32:21 +00:00
// ApplicationDestination points to a valid cluster, so we may clean up the live objects
2023-12-18 16:40:23 +00:00
objs := make ( [ ] * unstructured . Unstructured , 0 )
2025-01-13 18:15:42 +00:00
objsMap , err := ctrl . getPermittedAppLiveObjects ( destCluster , app , proj , projectClusters )
2021-06-29 20:32:21 +00:00
if err != nil {
2023-12-18 16:40:23 +00:00
return err
2021-06-29 20:32:21 +00:00
}
2021-03-15 16:27:41 +00:00
2021-06-29 20:32:21 +00:00
for k := range objsMap {
// Wait for objects pending deletion to complete before proceeding with next sync wave
if objsMap [ k ] . GetDeletionTimestamp ( ) != nil {
logCtx . Infof ( "%d objects remaining for deletion" , len ( objsMap ) )
2023-12-18 16:40:23 +00:00
return nil
2021-06-29 20:32:21 +00:00
}
2021-03-15 16:27:41 +00:00
2021-06-29 20:32:21 +00:00
if ctrl . shouldBeDeleted ( app , objsMap [ k ] ) {
objs = append ( objs , objsMap [ k ] )
2024-10-24 07:08:24 +00:00
if res , ok := app . Status . FindResource ( k ) ; ok && res . RequiresDeletionConfirmation && ! deletionApproved {
logCtx . Infof ( "Resource %v requires manual confirmation to delete" , k )
return nil
}
2021-06-29 20:32:21 +00:00
}
}
2018-11-28 21:38:02 +00:00
2021-06-29 20:32:21 +00:00
filteredObjs := FilterObjectsForDeletion ( objs )
propagationPolicy := metav1 . DeletePropagationForeground
if app . GetPropagationPolicy ( ) == appv1 . BackgroundPropagationPolicyFinalizer {
propagationPolicy = metav1 . DeletePropagationBackground
}
logCtx . Infof ( "Deleting application's resources with %s propagation policy" , propagationPolicy )
err = kube . RunAllAsync ( len ( filteredObjs ) , func ( i int ) error {
obj := filteredObjs [ i ]
return ctrl . kubectl . DeleteResource ( context . Background ( ) , config , obj . GroupVersionKind ( ) , obj . GetName ( ) , obj . GetNamespace ( ) , metav1 . DeleteOptions { PropagationPolicy : & propagationPolicy } )
} )
if err != nil {
2023-12-18 16:40:23 +00:00
return err
2021-06-29 20:32:21 +00:00
}
2025-01-13 18:15:42 +00:00
objsMap , err = ctrl . getPermittedAppLiveObjects ( destCluster , app , proj , projectClusters )
2021-06-29 20:32:21 +00:00
if err != nil {
2023-12-18 16:40:23 +00:00
return err
2021-06-29 20:32:21 +00:00
}
for k , obj := range objsMap {
if ! ctrl . shouldBeDeleted ( app , obj ) {
delete ( objsMap , k )
}
}
if len ( objsMap ) > 0 {
logCtx . Infof ( "%d objects remaining for deletion" , len ( objsMap ) )
2023-12-18 16:40:23 +00:00
return nil
2019-05-06 19:49:29 +00:00
}
2023-12-18 16:40:23 +00:00
logCtx . Infof ( "Successfully deleted %d resources" , len ( objs ) )
app . UnSetCascadedDeletion ( )
return ctrl . updateFinalizers ( app )
2019-05-06 19:49:29 +00:00
}
2021-06-29 20:32:21 +00:00
2023-12-18 16:40:23 +00:00
if app . HasPostDeleteFinalizer ( ) {
2025-01-13 18:15:42 +00:00
objsMap , err := ctrl . getPermittedAppLiveObjects ( destCluster , app , proj , projectClusters )
2023-12-18 16:40:23 +00:00
if err != nil {
return err
}
2021-06-29 20:32:21 +00:00
2023-12-18 16:40:23 +00:00
done , err := ctrl . executePostDeleteHooks ( app , proj , objsMap , config , logCtx )
if err != nil {
return err
}
if ! done {
return nil
}
app . UnSetPostDeleteFinalizer ( )
return ctrl . updateFinalizers ( app )
2019-02-13 23:20:40 +00:00
}
2021-03-15 16:27:41 +00:00
2025-12-05 20:27:03 +00:00
if app . HasPreDeleteFinalizer ( "cleanup" ) {
objsMap , err := ctrl . getPermittedAppLiveObjects ( destCluster , app , proj , projectClusters )
if err != nil {
return fmt . Errorf ( "error getting permitted app live objects for pre-delete cleanup: %w" , err )
}
done , err := ctrl . cleanupPreDeleteHooks ( objsMap , config , logCtx )
if err != nil {
return fmt . Errorf ( "error cleaning up pre-delete hooks: %w" , err )
}
if ! done {
return nil
}
app . UnSetPreDeleteFinalizer ( "cleanup" )
return ctrl . updateFinalizers ( app )
}
2023-12-18 16:40:23 +00:00
if app . HasPostDeleteFinalizer ( "cleanup" ) {
2025-01-13 18:15:42 +00:00
objsMap , err := ctrl . getPermittedAppLiveObjects ( destCluster , app , proj , projectClusters )
2023-12-18 16:40:23 +00:00
if err != nil {
return err
}
done , err := ctrl . cleanupPostDeleteHooks ( objsMap , config , logCtx )
if err != nil {
return err
}
if ! done {
return nil
}
app . UnSetPostDeleteFinalizer ( "cleanup" )
return ctrl . updateFinalizers ( app )
2018-11-19 20:25:45 +00:00
}
2018-11-28 21:38:02 +00:00
2025-12-05 20:27:03 +00:00
if ! app . CascadedDeletion ( ) && ! app . HasPostDeleteFinalizer ( ) && ! app . HasPreDeleteFinalizer ( ) {
2023-12-18 16:40:23 +00:00
if err := ctrl . cache . SetAppManagedResources ( app . Name , nil ) ; err != nil {
return err
}
if err := ctrl . cache . SetAppResourcesTree ( app . Name , nil ) ; err != nil {
return err
}
ctrl . projectRefreshQueue . Add ( fmt . Sprintf ( "%s/%s" , ctrl . namespace , app . Spec . GetProject ( ) ) )
2021-06-29 20:32:21 +00:00
}
2023-12-18 16:40:23 +00:00
return nil
2018-05-16 23:30:28 +00:00
}
2023-12-18 16:40:23 +00:00
func ( ctrl * ApplicationController ) updateFinalizers ( app * appv1 . Application ) error {
2022-08-10 09:39:10 +00:00
_ , err := ctrl . getAppProj ( app )
if err != nil {
2022-10-19 19:21:32 +00:00
return fmt . Errorf ( "error getting project: %w" , err )
2022-08-10 09:39:10 +00:00
}
2023-12-18 16:40:23 +00:00
2021-03-15 16:27:41 +00:00
var patch [ ] byte
2025-01-02 23:26:59 +00:00
patch , _ = json . Marshal ( map [ string ] any {
"metadata" : map [ string ] any {
2021-03-15 16:27:41 +00:00
"finalizers" : app . Finalizers ,
} ,
} )
2022-08-10 09:39:10 +00:00
_ , err = ctrl . applicationClientset . ArgoprojV1alpha1 ( ) . Applications ( app . Namespace ) . Patch ( context . Background ( ) , app . Name , types . MergePatchType , patch , metav1 . PatchOptions { } )
2021-03-15 16:27:41 +00:00
return err
}
2018-05-16 23:30:28 +00:00
func ( ctrl * ApplicationController ) setAppCondition ( app * appv1 . Application , condition appv1 . ApplicationCondition ) {
2025-05-20 19:48:09 +00:00
logCtx := log . WithFields ( applog . GetAppLogFields ( app ) )
2020-10-09 20:16:54 +00:00
// do nothing if app already has same condition
for _ , c := range app . Status . Conditions {
if c . Message == condition . Message && c . Type == condition . Type {
return
}
}
2019-10-17 02:29:52 +00:00
app . Status . SetConditions ( [ ] appv1 . ApplicationCondition { condition } , map [ appv1 . ApplicationConditionType ] bool { condition . Type : true } )
2018-05-16 23:30:28 +00:00
var patch [ ] byte
2025-01-02 23:26:59 +00:00
patch , err := json . Marshal ( map [ string ] any {
"status" : map [ string ] any {
2018-05-16 23:30:28 +00:00
"conditions" : app . Status . Conditions ,
} ,
} )
if err == nil {
2020-08-05 18:36:40 +00:00
_ , err = ctrl . applicationClientset . ArgoprojV1alpha1 ( ) . Applications ( app . Namespace ) . Patch ( context . Background ( ) , app . Name , types . MergePatchType , patch , metav1 . PatchOptions { } )
2018-05-16 23:30:28 +00:00
}
if err != nil {
2025-09-27 21:47:21 +00:00
logCtx . WithError ( err ) . Error ( "Unable to set application condition" )
2018-05-16 23:30:28 +00:00
}
}
func ( ctrl * ApplicationController ) processRequestedAppOperation ( app * appv1 . Application ) {
2025-05-20 19:48:09 +00:00
logCtx := log . WithFields ( applog . GetAppLogFields ( app ) )
2018-07-07 07:54:06 +00:00
var state * appv1 . OperationState
2018-05-15 07:36:11 +00:00
// Recover from any unexpected panics and automatically set the status to be failed
defer func ( ) {
if r := recover ( ) ; r != nil {
2018-09-11 21:28:53 +00:00
logCtx . Errorf ( "Recovered from panic: %+v\n%s" , r , debug . Stack ( ) )
2020-05-15 17:01:18 +00:00
state . Phase = synccommon . OperationError
2018-05-15 07:36:11 +00:00
if rerr , ok := r . ( error ) ; ok {
state . Message = rerr . Error ( )
2018-05-11 18:50:32 +00:00
} else {
2018-05-15 07:36:11 +00:00
state . Message = fmt . Sprintf ( "%v" , r )
2018-05-11 18:50:32 +00:00
}
2018-07-14 00:13:31 +00:00
ctrl . setOperationState ( app , state )
2018-05-15 07:36:11 +00:00
}
} ( )
2024-07-04 10:18:18 +00:00
ts := stats . NewTimingStats ( )
defer func ( ) {
for k , v := range ts . Timings ( ) {
logCtx = logCtx . WithField ( k , v . Milliseconds ( ) )
}
logCtx = logCtx . WithField ( "time_ms" , time . Since ( ts . StartTime ) . Milliseconds ( ) )
logCtx . Debug ( "Finished processing requested app operation" )
} ( )
2025-08-19 13:27:43 +00:00
terminatingCause := ""
2018-07-14 00:13:31 +00:00
if isOperationInProgress ( app ) {
2018-07-07 07:54:06 +00:00
state = app . Status . OperationState . DeepCopy ( )
2025-01-22 21:13:51 +00:00
switch {
2025-08-19 13:27:43 +00:00
case state . Phase == synccommon . OperationTerminating :
logCtx . Infof ( "Resuming in-progress operation. phase: %s, message: %s" , state . Phase , state . Message )
case ctrl . syncTimeout != time . Duration ( 0 ) && time . Now ( ) . After ( state . StartedAt . Add ( ctrl . syncTimeout ) ) :
state . Phase = synccommon . OperationTerminating
state . Message = "operation is terminating due to timeout"
terminatingCause = "controller sync timeout"
ctrl . setOperationState ( app , state )
logCtx . Infof ( "Terminating in-progress operation due to timeout. Started at: %v, timeout: %v" , state . StartedAt , ctrl . syncTimeout )
case state . Phase == synccommon . OperationRunning && state . FinishedAt != nil :
2025-07-16 17:39:30 +00:00
// Failed operation with retry strategy might be in-progress and has completion time
2020-07-28 17:14:17 +00:00
retryAt , err := app . Status . OperationState . Operation . Retry . NextRetryAt ( state . FinishedAt . Time , state . RetryCount )
if err != nil {
2025-07-16 17:39:30 +00:00
state . Phase = synccommon . OperationError
2020-07-28 17:14:17 +00:00
state . Message = err . Error ( )
ctrl . setOperationState ( app , state )
return
}
retryAfter := time . Until ( retryAt )
2025-09-11 15:19:00 +00:00
2020-07-28 17:14:17 +00:00
if retryAfter > 0 {
logCtx . Infof ( "Skipping retrying in-progress operation. Attempting again at: %s" , retryAt . Format ( time . RFC3339 ) )
2022-08-10 09:39:10 +00:00
ctrl . requestAppRefresh ( app . QualifiedName ( ) , CompareWithLatest . Pointer ( ) , & retryAfter )
2020-07-28 17:14:17 +00:00
return
}
2025-09-11 15:19:00 +00:00
// Remove the desired revisions if the sync failed and we are retrying. The latest revision from the source will be used.
extraMsg := ""
if state . Operation . Retry . Refresh {
extraMsg += " with latest revisions"
state . Operation . Sync . Revision = ""
state . Operation . Sync . Revisions = nil
}
2025-01-07 15:25:22 +00:00
// Get rid of sync results and null out previous operation completion time
2025-07-16 17:39:30 +00:00
// This will start the retry attempt
2025-09-11 15:19:00 +00:00
state . Message = fmt . Sprintf ( "Retrying operation%s. Attempt #%d" , extraMsg , state . RetryCount )
2025-07-16 17:39:30 +00:00
state . FinishedAt = nil
2025-01-07 15:25:22 +00:00
state . SyncResult = nil
2025-07-16 17:39:30 +00:00
ctrl . setOperationState ( app , state )
2025-09-11 15:19:00 +00:00
logCtx . Infof ( "Retrying operation%s. Attempt #%d" , extraMsg , state . RetryCount )
2025-01-22 21:13:51 +00:00
default :
2020-07-28 17:14:17 +00:00
logCtx . Infof ( "Resuming in-progress operation. phase: %s, message: %s" , state . Phase , state . Message )
}
2018-05-15 07:36:11 +00:00
} else {
2025-07-16 17:39:30 +00:00
state = NewOperationState ( * app . Operation )
2018-07-14 00:13:31 +00:00
ctrl . setOperationState ( app , state )
2024-12-10 08:29:04 +00:00
if ctrl . syncTimeout != time . Duration ( 0 ) {
// Schedule a check during which the timeout would be checked.
ctrl . appOperationQueue . AddAfter ( ctrl . toAppKey ( app . QualifiedName ( ) ) , ctrl . syncTimeout )
}
2018-09-11 21:28:53 +00:00
logCtx . Infof ( "Initialized new operation: %v" , * app . Operation )
2018-05-15 07:36:11 +00:00
}
2024-07-04 10:18:18 +00:00
ts . AddCheckpoint ( "initial_operation_stage_ms" )
2019-06-18 02:09:43 +00:00
2025-08-19 13:27:43 +00:00
terminating := state . Phase == synccommon . OperationTerminating
2025-07-03 17:16:58 +00:00
project , err := ctrl . getAppProj ( app )
2025-07-16 17:39:30 +00:00
if err == nil {
2025-07-03 17:16:58 +00:00
// Start or resume the sync
ctrl . appStateManager . SyncAppState ( app , project , state )
2025-07-16 17:39:30 +00:00
} else {
state . Phase = synccommon . OperationError
state . Message = fmt . Sprintf ( "Failed to load application project: %v" , err )
2022-08-10 09:39:10 +00:00
}
2025-07-03 17:16:58 +00:00
ts . AddCheckpoint ( "sync_app_state_ms" )
2022-08-10 09:39:10 +00:00
2025-03-27 16:37:52 +00:00
switch state . Phase {
case synccommon . OperationRunning :
2018-07-14 00:13:31 +00:00
// It's possible for an app to be terminated while we were operating on it. We do not want
// to clobber the Terminated state with Running. Get the latest app state to check for this.
2025-03-27 16:37:52 +00:00
freshApp , err := ctrl . applicationClientset . ArgoprojV1alpha1 ( ) . Applications ( app . Namespace ) . Get ( context . Background ( ) , app . Name , metav1 . GetOptions { } )
2018-07-14 00:13:31 +00:00
if err == nil {
2020-05-15 17:01:18 +00:00
if freshApp . Status . OperationState != nil && freshApp . Status . OperationState . Phase == synccommon . OperationTerminating {
state . Phase = synccommon . OperationTerminating
2018-07-14 00:13:31 +00:00
state . Message = "operation is terminating"
// after this, we will get requeued to the workqueue, but next time the
// SyncAppState will operate in a Terminating phase, allowing the worker to perform
// cleanup (e.g. delete jobs, workflows, etc...)
}
}
2025-03-27 16:37:52 +00:00
case synccommon . OperationFailed , synccommon . OperationError :
2020-08-06 00:58:59 +00:00
if ! terminating && ( state . RetryCount < state . Operation . Retry . Limit || state . Operation . Retry . Limit < 0 ) {
2020-07-28 17:14:17 +00:00
now := metav1 . Now ( )
if retryAt , err := state . Operation . Retry . NextRetryAt ( now . Time , state . RetryCount ) ; err != nil {
2025-07-03 17:16:58 +00:00
state . Phase = synccommon . OperationError
2020-07-28 17:14:17 +00:00
state . Message = fmt . Sprintf ( "%s (failed to retry: %v)" , state . Message , err )
} else {
2025-08-19 13:27:43 +00:00
// Set FinishedAt explicitly on a Running phase. This is a unique condition that will allow this
// function to perform a retry the next time the operation is processed.
2020-07-28 17:14:17 +00:00
state . Phase = synccommon . OperationRunning
2025-08-19 13:27:43 +00:00
state . FinishedAt = & now
2020-07-28 17:14:17 +00:00
state . RetryCount ++
2025-08-19 13:27:43 +00:00
state . Message = fmt . Sprintf ( "%s. Retrying attempt #%d at %s." , state . Message , state . RetryCount , retryAt . Format ( time . Kitchen ) )
}
} else {
if terminating && terminatingCause != "" {
state . Message = fmt . Sprintf ( "%s, triggered by %s" , state . Message , terminatingCause )
}
if state . RetryCount > 0 {
state . Message = fmt . Sprintf ( "%s (retried %d times)." , state . Message , state . RetryCount )
2020-07-28 17:14:17 +00:00
}
}
2018-07-14 00:13:31 +00:00
}
ctrl . setOperationState ( app , state )
2024-07-04 10:18:18 +00:00
ts . AddCheckpoint ( "final_set_operation_state" )
2021-12-11 05:23:37 +00:00
if state . Phase . Completed ( ) && ( app . Operation . Sync != nil && ! app . Operation . Sync . DryRun ) {
2018-07-12 19:39:46 +00:00
// if we just completed an operation, force a refresh so that UI will report up-to-date
// sync/health information
2020-01-08 22:07:36 +00:00
if _ , err := cache . MetaNamespaceKeyFunc ( app ) ; err == nil {
2026-02-10 14:09:01 +00:00
var compareWith CompareWith
if state . Operation . InitiatedBy . Automated {
// Do not force revision resolution on automated operations because
// this would cause excessive Ls-Remote requests on monorepo commits
compareWith = CompareWithLatest
} else {
// Force app refresh with using most recent resolved revision after sync,
// so UI won't show a just synced application being out of sync if it was
// synced after commit but before app. refresh (see #18153)
compareWith = CompareWithLatestForceResolve
}
ctrl . requestAppRefresh ( app . QualifiedName ( ) , compareWith . Pointer ( ) , nil )
2019-07-25 02:26:09 +00:00
} else {
2025-09-27 21:47:21 +00:00
logCtx . WithError ( err ) . Warn ( "Fails to requeue application" )
2019-07-25 02:26:09 +00:00
}
2018-07-12 19:39:46 +00:00
}
2024-07-04 10:18:18 +00:00
ts . AddCheckpoint ( "request_app_refresh_ms" )
2018-05-11 18:50:32 +00:00
}
2018-07-14 00:13:31 +00:00
func ( ctrl * ApplicationController ) setOperationState ( app * appv1 . Application , state * appv1 . OperationState ) {
2025-05-20 19:48:09 +00:00
logCtx := log . WithFields ( applog . GetAppLogFields ( app ) )
2023-07-26 21:02:56 +00:00
if state . Phase == "" {
// expose any bugs where we neglect to set phase
panic ( "no phase was set" )
}
if state . Phase . Completed ( ) {
now := metav1 . Now ( )
state . FinishedAt = & now
}
2025-01-02 23:26:59 +00:00
patch := map [ string ] any {
"status" : map [ string ] any {
2023-07-26 21:02:56 +00:00
"operationState" : state ,
} ,
}
if state . Phase . Completed ( ) {
// If operation is completed, clear the operation field to indicate no operation is
// in progress.
patch [ "operation" ] = nil
}
if reflect . DeepEqual ( app . Status . OperationState , state ) {
logCtx . Infof ( "No operation updates necessary to '%s'. Skipping patch" , app . QualifiedName ( ) )
return
}
patchJSON , err := json . Marshal ( patch )
if err != nil {
2025-09-27 21:47:21 +00:00
logCtx . WithError ( err ) . Error ( "error marshaling json" )
2023-07-26 21:02:56 +00:00
return
}
if app . Status . OperationState != nil && app . Status . OperationState . FinishedAt != nil && state . FinishedAt == nil {
patchJSON , err = jsonpatch . MergeMergePatches ( patchJSON , [ ] byte ( ` { "status": { "operationState": { "finishedAt": null}}} ` ) )
2018-05-15 07:36:11 +00:00
if err != nil {
2025-09-27 21:47:21 +00:00
logCtx . WithError ( err ) . Error ( "error merging operation state patch" )
2023-07-26 21:02:56 +00:00
return
2020-12-10 02:05:34 +00:00
}
2023-07-26 21:02:56 +00:00
}
2020-12-10 02:05:34 +00:00
2023-07-26 21:02:56 +00:00
kube . RetryUntilSucceed ( context . Background ( ) , updateOperationStateTimeout , "Update application operation state" , logutils . NewLogrusLogger ( logutils . NewWithCurrentConfig ( ) ) , func ( ) error {
2023-10-24 01:15:53 +00:00
_ , err := ctrl . PatchAppWithWriteBack ( context . Background ( ) , app . Name , app . Namespace , types . MergePatchType , patchJSON , metav1 . PatchOptions { } )
2018-05-15 07:36:11 +00:00
if err != nil {
2019-05-30 19:39:54 +00:00
// Stop retrying updating deleted application
2025-01-03 17:09:37 +00:00
if apierrors . IsNotFound ( err ) {
2019-05-30 19:39:54 +00:00
return nil
}
2023-07-26 21:02:56 +00:00
// kube.RetryUntilSucceed logs failed attempts at "debug" level, but we want to know if this fails. Log a
// warning.
2025-09-27 21:47:21 +00:00
logCtx . WithError ( err ) . Warn ( "error patching application with operation state" )
2022-10-19 19:21:32 +00:00
return fmt . Errorf ( "error patching application with operation state: %w" , err )
2018-05-11 18:50:32 +00:00
}
2018-05-15 07:36:11 +00:00
return nil
2020-07-27 20:33:08 +00:00
} )
2023-07-26 21:02:56 +00:00
logCtx . Infof ( "updated '%s' operation (phase: %s)" , app . QualifiedName ( ) , state . Phase )
if state . Phase . Completed ( ) {
eventInfo := argo . EventInfo { Reason : argo . EventReasonOperationCompleted }
var messages [ ] string
if state . Operation . Sync != nil && len ( state . Operation . Sync . Resources ) > 0 {
messages = [ ] string { "Partial sync operation" }
} else {
messages = [ ] string { "Sync operation" }
}
if state . SyncResult != nil {
messages = append ( messages , "to" , state . SyncResult . Revision )
}
if state . Phase . Successful ( ) {
2025-01-03 16:10:00 +00:00
eventInfo . Type = corev1 . EventTypeNormal
2023-07-26 21:02:56 +00:00
messages = append ( messages , "succeeded" )
} else {
2025-01-03 16:10:00 +00:00
eventInfo . Type = corev1 . EventTypeWarning
2023-07-26 21:02:56 +00:00
messages = append ( messages , "failed:" , state . Message )
}
2025-01-07 15:08:51 +00:00
ctrl . logAppEvent ( context . TODO ( ) , app , eventInfo , strings . Join ( messages , " " ) )
2025-06-06 17:25:29 +00:00
destCluster , err := argo . GetDestinationCluster ( context . Background ( ) , app . Spec . Destination , ctrl . db )
if err != nil {
2025-09-27 21:47:21 +00:00
logCtx . WithError ( err ) . Warn ( "Unable to get destination cluster, setting dest_server label to empty string in sync metric" )
2025-06-06 17:25:29 +00:00
}
destServer := ""
if destCluster != nil {
destServer = destCluster . Server
}
ctrl . metricsServer . IncSync ( app , destServer , state )
2025-06-16 23:00:49 +00:00
ctrl . metricsServer . IncAppSyncDuration ( app , destServer , state )
2023-07-26 21:02:56 +00:00
}
2018-05-11 18:50:32 +00:00
}
2023-10-24 01:15:53 +00:00
// writeBackToInformer writes a just recently updated App back into the informer cache.
// This prevents the situation where the controller operates on a stale app and repeats work
func ( ctrl * ApplicationController ) writeBackToInformer ( app * appv1 . Application ) {
2025-05-20 19:48:09 +00:00
logCtx := log . WithFields ( applog . GetAppLogFields ( app ) ) . WithField ( "informer-writeBack" , true )
2023-10-24 01:15:53 +00:00
err := ctrl . appInformer . GetStore ( ) . Update ( app )
if err != nil {
2025-09-27 21:47:21 +00:00
logCtx . WithError ( err ) . Error ( "failed to update informer store" )
2023-10-24 01:15:53 +00:00
return
}
}
// PatchAppWithWriteBack patches an application and writes it back to the informer cache
func ( ctrl * ApplicationController ) PatchAppWithWriteBack ( ctx context . Context , name , ns string , pt types . PatchType , data [ ] byte , opts metav1 . PatchOptions , subresources ... string ) ( result * appv1 . Application , err error ) {
patchedApp , err := ctrl . applicationClientset . ArgoprojV1alpha1 ( ) . Applications ( ns ) . Patch ( ctx , name , pt , data , opts , subresources ... )
if err != nil {
return patchedApp , err
}
ctrl . writeBackToInformer ( patchedApp )
return patchedApp , err
}
2018-05-16 23:30:28 +00:00
func ( ctrl * ApplicationController ) processAppRefreshQueueItem ( ) ( processNext bool ) {
2025-03-27 16:37:52 +00:00
patchDuration := time . Duration ( 0 ) // time spent in doing patch/update calls
setOpDuration := time . Duration ( 0 ) // time spent in doing Operation patch calls in autosync
2018-05-11 18:50:32 +00:00
appKey , shutdown := ctrl . appRefreshQueue . Get ( )
2018-02-20 23:23:37 +00:00
if shutdown {
2018-05-16 23:30:28 +00:00
processNext = false
2025-09-22 04:05:06 +00:00
return processNext
2018-02-20 23:23:37 +00:00
}
2018-07-12 19:39:46 +00:00
processNext = true
2018-05-16 23:30:28 +00:00
defer func ( ) {
if r := recover ( ) ; r != nil {
2025-09-27 21:47:21 +00:00
log . WithField ( "appkey" , appKey ) . Errorf ( "Recovered from panic: %+v\n%s" , r , debug . Stack ( ) )
2018-05-16 23:30:28 +00:00
}
2024-07-31 21:56:26 +00:00
// We want to have app operation update happen after the sync, so there's no race condition
// and app updates not proceeding. See https://github.com/argoproj/argo-cd/issues/18500.
ctrl . appOperationQueue . AddRateLimited ( appKey )
2018-05-16 23:30:28 +00:00
ctrl . appRefreshQueue . Done ( appKey )
} ( )
2024-08-23 22:28:34 +00:00
obj , exists , err := ctrl . appInformer . GetIndexer ( ) . GetByKey ( appKey )
2018-02-22 18:56:14 +00:00
if err != nil {
2025-09-27 21:47:21 +00:00
log . WithField ( "appkey" , appKey ) . WithError ( err ) . Error ( "Failed to get application from informer index" )
2025-09-22 04:05:06 +00:00
return processNext
2018-02-22 18:56:14 +00:00
}
if ! exists {
// This happens after app was deleted, but the work queue still had an entry for it.
2025-09-22 04:05:06 +00:00
return processNext
2018-02-22 18:56:14 +00:00
}
2018-12-04 10:52:57 +00:00
origApp , ok := obj . ( * appv1 . Application )
2018-02-22 18:56:14 +00:00
if ! ok {
2025-09-27 21:47:21 +00:00
log . WithField ( "appkey" , appKey ) . Warn ( "Key in index is not an application" )
2025-09-22 04:05:06 +00:00
return processNext
2018-02-22 18:56:14 +00:00
}
2020-06-22 18:04:25 +00:00
origApp = origApp . DeepCopy ( )
2022-04-04 14:44:35 +00:00
needRefresh , refreshType , comparisonLevel := ctrl . needRefreshAppStatus ( origApp , ctrl . statusRefreshTimeout , ctrl . statusHardRefreshTimeout )
2018-12-18 02:23:35 +00:00
if ! needRefresh {
2025-09-22 04:05:06 +00:00
return processNext
2018-07-12 02:12:30 +00:00
}
2020-03-16 18:51:59 +00:00
app := origApp . DeepCopy ( )
2025-05-20 19:48:09 +00:00
logCtx := log . WithFields ( applog . GetAppLogFields ( app ) ) . WithFields ( log . Fields {
2024-05-13 20:07:24 +00:00
"comparison-level" : comparisonLevel ,
"dest-server" : origApp . Spec . Destination . Server ,
"dest-name" : origApp . Spec . Destination . Name ,
"dest-namespace" : origApp . Spec . Destination . Namespace ,
2023-10-06 20:49:33 +00:00
} )
2022-12-16 20:47:08 +00:00
2019-01-07 21:46:11 +00:00
startTime := time . Now ( )
2024-07-10 01:15:10 +00:00
ts := stats . NewTimingStats ( )
2025-06-06 17:25:29 +00:00
var destCluster * appv1 . Cluster
2019-01-07 21:46:11 +00:00
defer func ( ) {
2019-03-28 20:37:53 +00:00
reconcileDuration := time . Since ( startTime )
2025-06-06 17:25:29 +00:00
// We may or may not get to the point in the code where destCluster is set. Populate the dest_server label on a
// best-effort basis.
destServer := ""
if destCluster != nil {
destServer = destCluster . Server
}
ctrl . metricsServer . IncReconcile ( origApp , destServer , reconcileDuration )
2024-07-04 10:18:18 +00:00
for k , v := range ts . Timings ( ) {
logCtx = logCtx . WithField ( k , v . Milliseconds ( ) )
}
2020-03-16 18:51:59 +00:00
logCtx . WithFields ( log . Fields {
2023-10-06 20:49:33 +00:00
"time_ms" : reconcileDuration . Milliseconds ( ) ,
2025-03-27 16:37:52 +00:00
"patch_ms" : patchDuration . Milliseconds ( ) ,
"setop_ms" : setOpDuration . Milliseconds ( ) ,
2020-03-16 18:51:59 +00:00
} ) . Info ( "Reconciliation completed" )
2019-01-07 21:46:11 +00:00
} ( )
2019-03-14 21:54:34 +00:00
2019-07-15 20:34:26 +00:00
if comparisonLevel == ComparisonWithNothing {
2025-01-22 16:39:55 +00:00
// If the destination cluster is invalid, fallback to the normal reconciliation flow
2025-06-06 17:25:29 +00:00
if destCluster , err = argo . GetDestinationCluster ( context . Background ( ) , app . Spec . Destination , ctrl . db ) ; err == nil {
2025-01-22 16:39:55 +00:00
managedResources := make ( [ ] * appv1 . ResourceDiff , 0 )
if err := ctrl . cache . GetAppManagedResources ( app . InstanceName ( ctrl . namespace ) , & managedResources ) ; err == nil {
var tree * appv1 . ApplicationTree
if tree , err = ctrl . getResourceTree ( destCluster , app , managedResources ) ; err == nil {
app . Status . Summary = tree . GetSummary ( app )
if err := ctrl . cache . SetAppResourcesTree ( app . InstanceName ( ctrl . namespace ) , tree ) ; err != nil {
2025-09-27 21:47:21 +00:00
logCtx . WithError ( err ) . Error ( "Failed to cache resources tree" )
2025-09-22 04:05:06 +00:00
return processNext
2025-01-22 16:39:55 +00:00
}
2019-03-14 21:54:34 +00:00
}
2020-10-09 20:16:54 +00:00
2025-03-27 16:37:52 +00:00
patchDuration = ctrl . persistAppStatus ( origApp , & app . Status )
2025-09-22 04:05:06 +00:00
return processNext
2025-01-22 16:39:55 +00:00
}
logCtx . Warnf ( "Failed to get cached managed resources for tree reconciliation, fall back to full reconciliation" )
2019-03-14 21:54:34 +00:00
}
}
2024-07-04 10:18:18 +00:00
ts . AddCheckpoint ( "comparison_with_nothing_ms" )
2018-07-10 21:45:18 +00:00
2019-12-26 22:08:14 +00:00
project , hasErrors := ctrl . refreshAppConditions ( app )
2024-07-04 10:18:18 +00:00
ts . AddCheckpoint ( "refresh_app_conditions_ms" )
2024-12-06 12:13:36 +00:00
now := metav1 . Now ( )
2019-08-22 16:36:27 +00:00
if hasErrors {
2018-12-04 10:52:57 +00:00
app . Status . Sync . Status = appv1 . SyncStatusCodeUnknown
2020-05-15 17:01:18 +00:00
app . Status . Health . Status = health . HealthStatusUnknown
2025-03-27 16:37:52 +00:00
patchDuration = ctrl . persistAppStatus ( origApp , & app . Status )
2022-03-22 17:57:30 +00:00
2022-08-10 09:39:10 +00:00
if err := ctrl . cache . SetAppResourcesTree ( app . InstanceName ( ctrl . namespace ) , & appv1 . ApplicationTree { } ) ; err != nil {
2025-09-27 21:47:21 +00:00
logCtx . WithError ( err ) . Warn ( "failed to set app resource tree" )
2022-03-22 17:57:30 +00:00
}
2022-08-10 09:39:10 +00:00
if err := ctrl . cache . SetAppManagedResources ( app . InstanceName ( ctrl . namespace ) , nil ) ; err != nil {
2025-09-27 21:47:21 +00:00
logCtx . WithError ( err ) . Warn ( "failed to set app managed resources tree" )
2022-03-22 17:57:30 +00:00
}
2024-07-04 10:18:18 +00:00
ts . AddCheckpoint ( "process_refresh_app_conditions_errors_ms" )
2025-09-22 04:05:06 +00:00
return processNext
2018-07-12 02:12:30 +00:00
}
2018-07-10 21:45:18 +00:00
2025-06-06 17:25:29 +00:00
destCluster , err = argo . GetDestinationCluster ( context . Background ( ) , app . Spec . Destination , ctrl . db )
2025-01-22 16:39:55 +00:00
if err != nil {
2025-09-27 21:47:21 +00:00
logCtx . WithError ( err ) . Error ( "Failed to get destination cluster" )
2025-01-22 16:39:55 +00:00
// exit the reconciliation. ctrl.refreshAppConditions should have caught the error
2025-09-22 04:05:06 +00:00
return processNext
2025-01-22 16:39:55 +00:00
}
2019-06-18 02:09:43 +00:00
var localManifests [ ] string
2019-09-11 20:53:31 +00:00
if opState := app . Status . OperationState ; opState != nil && opState . Operation . Sync != nil {
2019-06-18 02:09:43 +00:00
localManifests = opState . Operation . Sync . Manifests
}
2022-12-16 20:47:08 +00:00
revisions := make ( [ ] string , 0 )
sources := make ( [ ] appv1 . ApplicationSource , 0 )
hasMultipleSources := app . Spec . HasMultipleSources ( )
2019-08-20 15:43:29 +00:00
2022-12-16 20:47:08 +00:00
// If we have multiple sources, we use all the sources under `sources` field and ignore source under `source` field.
// else we use the source under the source field.
if hasMultipleSources {
for _ , source := range app . Spec . Sources {
// We do not perform any filtering of duplicate sources.
// Argo CD will apply and update the resources generated from the sources automatically
// based on the order in which manifests were generated
sources = append ( sources , source )
revisions = append ( revisions , source . TargetRevision )
}
if comparisonLevel == CompareWithRecent {
revisions = app . Status . Sync . Revisions
}
} else {
revision := app . Spec . GetSource ( ) . TargetRevision
if comparisonLevel == CompareWithRecent {
revision = app . Status . Sync . Revision
}
revisions = append ( revisions , revision )
sources = append ( sources , app . Spec . GetSource ( ) )
}
2025-07-02 22:32:31 +00:00
compareResult , err := ctrl . appStateManager . CompareAppState ( app , project , revisions , sources , refreshType == appv1 . RefreshTypeHard , comparisonLevel == CompareWithLatestForceResolve , localManifests , hasMultipleSources )
2025-01-13 18:15:42 +00:00
2024-07-04 10:18:18 +00:00
ts . AddCheckpoint ( "compare_app_state_ms" )
2022-12-16 20:47:08 +00:00
2025-03-27 16:37:52 +00:00
if stderrors . Is ( err , ErrCompareStateRepo ) {
2025-09-27 21:47:21 +00:00
logCtx . WithError ( err ) . Warn ( "Ignoring temporary failed attempt to compare app state against repo" )
2025-09-22 04:05:06 +00:00
return processNext // short circuit if git error is encountered
2023-11-02 15:51:16 +00:00
}
2020-03-16 18:51:59 +00:00
for k , v := range compareResult . timings {
logCtx = logCtx . WithField ( k , v . Milliseconds ( ) )
}
2019-08-20 15:43:29 +00:00
2019-09-06 22:37:25 +00:00
ctrl . normalizeApplication ( origApp , app )
2024-07-04 10:18:18 +00:00
ts . AddCheckpoint ( "normalize_application_ms" )
2025-01-22 16:39:55 +00:00
2025-01-13 18:15:42 +00:00
tree , err := ctrl . setAppManagedResources ( destCluster , app , compareResult )
2024-07-04 10:18:18 +00:00
ts . AddCheckpoint ( "set_app_managed_resources_ms" )
2019-02-13 23:20:40 +00:00
if err != nil {
2025-09-27 21:47:21 +00:00
logCtx . WithError ( err ) . Error ( "Failed to cache app resources" )
2019-04-02 15:48:34 +00:00
} else {
2023-01-04 18:40:51 +00:00
app . Status . Summary = tree . GetSummary ( app )
2019-02-13 23:20:40 +00:00
}
2018-09-11 21:28:53 +00:00
2024-10-12 17:32:46 +00:00
canSync , _ := project . Spec . SyncWindows . Matches ( app ) . CanSync ( false )
if canSync {
2025-07-14 14:36:05 +00:00
syncErrCond , opDuration := ctrl . autoSync ( app , compareResult . syncStatus , compareResult . resources , compareResult . revisionsMayHaveChanges )
2025-03-27 16:37:52 +00:00
setOpDuration = opDuration
2019-12-26 22:08:14 +00:00
if syncErrCond != nil {
app . Status . SetConditions (
[ ] appv1 . ApplicationCondition { * syncErrCond } ,
map [ appv1 . ApplicationConditionType ] bool { appv1 . ApplicationConditionSyncError : true } ,
)
2019-10-08 22:20:19 +00:00
} else {
2019-12-26 22:08:14 +00:00
app . Status . SetConditions (
[ ] appv1 . ApplicationCondition { } ,
map [ appv1 . ApplicationConditionType ] bool { appv1 . ApplicationConditionSyncError : true } ,
)
2019-10-01 22:23:09 +00:00
}
2019-12-26 22:08:14 +00:00
} else {
2020-03-16 18:51:59 +00:00
logCtx . Info ( "Sync prevented by sync window" )
2019-10-01 22:23:09 +00:00
}
2024-07-04 10:18:18 +00:00
ts . AddCheckpoint ( "auto_sync_ms" )
2018-09-11 21:28:53 +00:00
2021-05-19 19:30:15 +00:00
if app . Status . ReconciledAt == nil || comparisonLevel >= CompareWithLatest {
2020-08-31 17:18:12 +00:00
app . Status . ReconciledAt = & now
2019-10-28 23:44:23 +00:00
}
2018-12-04 10:52:57 +00:00
app . Status . Sync = * compareResult . syncStatus
2025-05-20 15:26:07 +00:00
app . Status . Health . Status = compareResult . healthStatus
2018-12-04 10:52:57 +00:00
app . Status . Resources = compareResult . resources
2020-07-21 23:59:00 +00:00
sort . Slice ( app . Status . Resources , func ( i , j int ) bool {
return resourceStatusKey ( app . Status . Resources [ i ] ) < resourceStatusKey ( app . Status . Resources [ j ] )
} )
2019-03-18 21:39:32 +00:00
app . Status . SourceType = compareResult . appSourceType
2022-12-16 20:47:08 +00:00
app . Status . SourceTypes = compareResult . appSourceTypes
2023-05-27 22:08:59 +00:00
app . Status . ControllerNamespace = ctrl . namespace
2024-07-04 10:18:18 +00:00
ts . AddCheckpoint ( "app_status_update_ms" )
2025-12-05 20:27:03 +00:00
// Update finalizers BEFORE persisting status to avoid race condition where app shows "Synced"
// but doesn't have finalizers yet, which would allow deletion without running pre-delete hooks
if ( compareResult . hasPreDeleteHooks != app . HasPreDeleteFinalizer ( ) ||
compareResult . hasPreDeleteHooks != app . HasPreDeleteFinalizer ( "cleanup" ) ) &&
app . GetDeletionTimestamp ( ) == nil {
if compareResult . hasPreDeleteHooks {
app . SetPreDeleteFinalizer ( )
app . SetPreDeleteFinalizer ( "cleanup" )
} else {
app . UnSetPreDeleteFinalizer ( )
app . UnSetPreDeleteFinalizer ( "cleanup" )
}
if err := ctrl . updateFinalizers ( app ) ; err != nil {
logCtx . Errorf ( "Failed to update pre-delete finalizers: %v" , err )
}
}
if ( compareResult . hasPostDeleteHooks != app . HasPostDeleteFinalizer ( ) ||
compareResult . hasPostDeleteHooks != app . HasPostDeleteFinalizer ( "cleanup" ) ) &&
2023-12-18 16:40:23 +00:00
app . GetDeletionTimestamp ( ) == nil {
if compareResult . hasPostDeleteHooks {
app . SetPostDeleteFinalizer ( )
app . SetPostDeleteFinalizer ( "cleanup" )
} else {
app . UnSetPostDeleteFinalizer ( )
app . UnSetPostDeleteFinalizer ( "cleanup" )
}
if err := ctrl . updateFinalizers ( app ) ; err != nil {
2025-12-05 20:27:03 +00:00
logCtx . WithError ( err ) . Error ( "Failed to update post-delete finalizers" )
2023-12-18 16:40:23 +00:00
}
}
2024-07-04 10:18:18 +00:00
ts . AddCheckpoint ( "process_finalizers_ms" )
2025-12-05 20:27:03 +00:00
patchDuration = ctrl . persistAppStatus ( origApp , & app . Status )
// This is a partly a duplicate of patch_ms, but more descriptive and allows to have measurement for the next step.
ts . AddCheckpoint ( "persist_app_status_ms" )
2025-09-22 04:05:06 +00:00
return processNext
2018-02-20 23:23:37 +00:00
}
2024-12-16 21:59:09 +00:00
func ( ctrl * ApplicationController ) processAppHydrateQueueItem ( ) ( processNext bool ) {
appKey , shutdown := ctrl . appHydrateQueue . Get ( )
if shutdown {
processNext = false
2025-09-22 04:05:06 +00:00
return processNext
2024-12-16 21:59:09 +00:00
}
processNext = true
defer func ( ) {
if r := recover ( ) ; r != nil {
2025-09-27 21:47:21 +00:00
log . WithField ( "appkey" , appKey ) . Errorf ( "Recovered from panic: %+v\n%s" , r , debug . Stack ( ) )
2024-12-16 21:59:09 +00:00
}
ctrl . appHydrateQueue . Done ( appKey )
} ( )
obj , exists , err := ctrl . appInformer . GetIndexer ( ) . GetByKey ( appKey )
if err != nil {
2025-09-27 21:47:21 +00:00
log . WithField ( "appkey" , appKey ) . WithError ( err ) . Error ( "Failed to get application from informer index" )
2025-09-22 04:05:06 +00:00
return processNext
2024-12-16 21:59:09 +00:00
}
if ! exists {
// This happens after app was deleted, but the work queue still had an entry for it.
2025-09-22 04:05:06 +00:00
return processNext
2024-12-16 21:59:09 +00:00
}
origApp , ok := obj . ( * appv1 . Application )
if ! ok {
2025-09-27 21:47:21 +00:00
log . WithField ( "appkey" , appKey ) . Warn ( "Key in index is not an application" )
2025-09-22 04:05:06 +00:00
return processNext
2024-12-16 21:59:09 +00:00
}
2025-09-30 21:44:43 +00:00
ctrl . hydrator . ProcessAppHydrateQueueItem ( origApp . DeepCopy ( ) )
2024-12-16 21:59:09 +00:00
2025-05-20 19:48:09 +00:00
log . WithFields ( applog . GetAppLogFields ( origApp ) ) . Debug ( "Successfully processed app hydrate queue item" )
2025-09-22 04:05:06 +00:00
return processNext
2024-12-16 21:59:09 +00:00
}
func ( ctrl * ApplicationController ) processHydrationQueueItem ( ) ( processNext bool ) {
hydrationKey , shutdown := ctrl . hydrationQueue . Get ( )
if shutdown {
processNext = false
2025-09-22 04:05:06 +00:00
return processNext
2024-12-16 21:59:09 +00:00
}
processNext = true
logCtx := log . WithFields ( log . Fields {
"sourceRepoURL" : hydrationKey . SourceRepoURL ,
"sourceTargetRevision" : hydrationKey . SourceTargetRevision ,
"destinationBranch" : hydrationKey . DestinationBranch ,
} )
2025-09-27 21:47:21 +00:00
defer func ( ) {
if r := recover ( ) ; r != nil {
logCtx . Errorf ( "Recovered from panic: %+v\n%s" , r , debug . Stack ( ) )
}
ctrl . hydrationQueue . Done ( hydrationKey )
} ( )
2024-12-16 21:59:09 +00:00
logCtx . Debug ( "Processing hydration queue item" )
ctrl . hydrator . ProcessHydrationQueueItem ( hydrationKey )
logCtx . Debug ( "Successfully processed hydration queue item" )
2025-09-22 04:05:06 +00:00
return processNext
2024-12-16 21:59:09 +00:00
}
2020-07-21 23:59:00 +00:00
func resourceStatusKey ( res appv1 . ResourceStatus ) string {
return strings . Join ( [ ] string { res . Group , res . Kind , res . Namespace , res . Name } , "/" )
}
2023-02-24 18:30:18 +00:00
func currentSourceEqualsSyncedSource ( app * appv1 . Application ) bool {
if app . Spec . HasMultipleSources ( ) {
return app . Spec . Sources . Equals ( app . Status . Sync . ComparedTo . Sources )
}
2024-12-16 21:59:09 +00:00
source := app . Spec . GetSource ( )
return source . Equals ( & app . Status . Sync . ComparedTo . Source )
2023-02-24 18:30:18 +00:00
}
2018-07-12 19:39:46 +00:00
// needRefreshAppStatus answers if application status needs to be refreshed.
// Returns true if application never been compared, has changed or comparison result has expired.
2023-05-19 13:55:08 +00:00
// Additionally, it returns whether full refresh was requested or not.
2019-03-14 21:54:34 +00:00
// If full refresh is requested then target and live state should be reconciled, else only live state tree should be updated.
2022-04-04 14:44:35 +00:00
func ( ctrl * ApplicationController ) needRefreshAppStatus ( app * appv1 . Application , statusRefreshTimeout , statusHardRefreshTimeout time . Duration ) ( bool , appv1 . RefreshType , CompareWith ) {
2025-05-20 19:48:09 +00:00
logCtx := log . WithFields ( applog . GetAppLogFields ( app ) )
2018-07-12 19:39:46 +00:00
var reason string
2019-07-15 20:34:26 +00:00
compareWith := CompareWithLatest
2018-12-18 02:23:35 +00:00
refreshType := appv1 . RefreshTypeNormal
2024-01-21 06:22:32 +00:00
2022-04-04 14:44:35 +00:00
softExpired := app . Status . ReconciledAt == nil || app . Status . ReconciledAt . Add ( statusRefreshTimeout ) . Before ( time . Now ( ) . UTC ( ) )
hardExpired := ( app . Status . ReconciledAt == nil || app . Status . ReconciledAt . Add ( statusHardRefreshTimeout ) . Before ( time . Now ( ) . UTC ( ) ) ) && statusHardRefreshTimeout . Seconds ( ) != 0
2019-10-22 22:23:15 +00:00
if requestedType , ok := app . IsRefreshRequested ( ) ; ok {
2021-05-19 19:30:15 +00:00
compareWith = CompareWithLatestForceResolve
2019-10-22 22:23:15 +00:00
// user requested app refresh.
refreshType = requestedType
reason = fmt . Sprintf ( "%s refresh requested" , refreshType )
2022-12-16 20:47:08 +00:00
} else {
2023-02-24 18:30:18 +00:00
if ! currentSourceEqualsSyncedSource ( app ) {
2022-12-16 20:47:08 +00:00
reason = "spec.source differs"
compareWith = CompareWithLatestForceResolve
2023-02-24 18:30:18 +00:00
if app . Spec . HasMultipleSources ( ) {
reason = "at least one of the spec.sources differs"
}
2022-12-16 20:47:08 +00:00
} else if hardExpired || softExpired {
// The commented line below mysteriously crashes if app.Status.ReconciledAt is nil
// reason = fmt.Sprintf("comparison expired. reconciledAt: %v, expiry: %v", app.Status.ReconciledAt, statusRefreshTimeout)
2023-10-18 15:17:00 +00:00
// TODO: find existing Golang bug or create a new one
2022-12-16 20:47:08 +00:00
reconciledAtStr := "never"
if app . Status . ReconciledAt != nil {
reconciledAtStr = app . Status . ReconciledAt . String ( )
}
reason = fmt . Sprintf ( "comparison expired, requesting refresh. reconciledAt: %v, expiry: %v" , reconciledAtStr , statusRefreshTimeout )
if hardExpired {
reason = fmt . Sprintf ( "comparison expired, requesting hard refresh. reconciledAt: %v, expiry: %v" , reconciledAtStr , statusHardRefreshTimeout )
refreshType = appv1 . RefreshTypeHard
}
2025-01-13 18:15:42 +00:00
} else if ! reflect . DeepEqual ( app . Spec . Destination , app . Status . Sync . ComparedTo . Destination ) {
2022-12-16 20:47:08 +00:00
reason = "spec.destination differs"
2023-05-19 13:55:08 +00:00
} else if app . HasChangedManagedNamespaceMetadata ( ) {
reason = "spec.syncPolicy.managedNamespaceMetadata differs"
2023-07-07 19:18:10 +00:00
} else if ! app . Spec . IgnoreDifferences . Equals ( app . Status . Sync . ComparedTo . IgnoreDifferences ) {
reason = "spec.ignoreDifferences differs"
2022-12-16 20:47:08 +00:00
} else if requested , level := ctrl . isRefreshRequested ( app . QualifiedName ( ) ) ; requested {
compareWith = level
reason = "controller refresh requested"
}
2018-07-12 19:39:46 +00:00
}
2019-10-22 22:23:15 +00:00
2018-07-12 19:39:46 +00:00
if reason != "" {
2019-07-15 20:34:26 +00:00
logCtx . Infof ( "Refreshing app status (%s), level (%d)" , reason , compareWith )
return true , refreshType , compareWith
2018-07-12 19:39:46 +00:00
}
2019-07-15 20:34:26 +00:00
return false , refreshType , compareWith
2018-07-12 19:39:46 +00:00
}
2019-12-26 22:08:14 +00:00
func ( ctrl * ApplicationController ) refreshAppConditions ( app * appv1 . Application ) ( * appv1 . AppProject , bool ) {
2019-08-22 16:36:27 +00:00
errorConditions := make ( [ ] appv1 . ApplicationCondition , 0 )
2019-08-19 15:14:48 +00:00
proj , err := ctrl . getAppProj ( app )
2018-07-09 17:45:03 +00:00
if err != nil {
2022-11-19 01:57:34 +00:00
errorConditions = append ( errorConditions , ctrl . projectErrorToCondition ( err , app ) )
2018-07-09 17:45:03 +00:00
} else {
2019-04-29 22:04:25 +00:00
specConditions , err := argo . ValidatePermissions ( context . Background ( ) , & app . Spec , proj , ctrl . db )
2018-07-09 17:45:03 +00:00
if err != nil {
2019-08-22 16:36:27 +00:00
errorConditions = append ( errorConditions , appv1 . ApplicationCondition {
2018-07-10 21:45:18 +00:00
Type : appv1 . ApplicationConditionUnknownError ,
Message : err . Error ( ) ,
} )
2018-07-09 17:45:03 +00:00
} else {
2019-08-22 16:36:27 +00:00
errorConditions = append ( errorConditions , specConditions ... )
2018-07-09 17:45:03 +00:00
}
}
2019-08-22 16:36:27 +00:00
app . Status . SetConditions ( errorConditions , map [ appv1 . ApplicationConditionType ] bool {
2019-10-11 00:26:53 +00:00
appv1 . ApplicationConditionInvalidSpecError : true ,
appv1 . ApplicationConditionUnknownError : true ,
2019-08-19 15:14:48 +00:00
} )
2019-12-26 22:08:14 +00:00
return proj , len ( errorConditions ) > 0
2018-05-07 15:38:25 +00:00
}
2018-11-30 21:50:27 +00:00
// normalizeApplication normalizes an application.spec and additionally persists updates if it changed
2019-09-06 22:37:25 +00:00
func ( ctrl * ApplicationController ) normalizeApplication ( orig , app * appv1 . Application ) {
app . Spec = * argo . NormalizeApplicationSpec ( & app . Spec )
2025-05-20 19:48:09 +00:00
logCtx := log . WithFields ( applog . GetAppLogFields ( app ) )
2022-12-16 20:47:08 +00:00
2019-03-04 08:56:36 +00:00
patch , modified , err := diff . CreateTwoWayMergePatch ( orig , app , appv1 . Application { } )
2022-12-16 20:47:08 +00:00
2018-11-30 21:50:27 +00:00
if err != nil {
2025-09-27 21:47:21 +00:00
logCtx . WithError ( err ) . Error ( "error constructing app spec patch" )
2018-11-30 21:50:27 +00:00
} else if modified {
2023-10-24 01:15:53 +00:00
_ , err := ctrl . PatchAppWithWriteBack ( context . Background ( ) , app . Name , app . Namespace , types . MergePatchType , patch , metav1 . PatchOptions { } )
2018-11-30 21:50:27 +00:00
if err != nil {
2025-09-27 21:47:21 +00:00
logCtx . WithError ( err ) . Error ( "Error persisting normalized application spec" )
2018-11-30 21:50:27 +00:00
} else {
logCtx . Infof ( "Normalized app spec: %s" , string ( patch ) )
}
}
}
2025-06-16 21:20:57 +00:00
func createMergePatch ( orig , newV any ) ( [ ] byte , bool , error ) {
2024-08-01 18:55:55 +00:00
origBytes , err := json . Marshal ( orig )
if err != nil {
return nil , false , err
}
2025-06-16 21:20:57 +00:00
newBytes , err := json . Marshal ( newV )
2024-08-01 18:55:55 +00:00
if err != nil {
return nil , false , err
}
patch , err := jsonpatch . CreateMergePatch ( origBytes , newBytes )
if err != nil {
return nil , false , err
}
return patch , string ( patch ) != "{}" , nil
}
2018-11-30 21:50:27 +00:00
// persistAppStatus persists updates to application status. If no changes were made, it is a no-op
2025-03-27 16:37:52 +00:00
func ( ctrl * ApplicationController ) persistAppStatus ( orig * appv1 . Application , newStatus * appv1 . ApplicationStatus ) ( patchDuration time . Duration ) {
2025-05-20 19:48:09 +00:00
logCtx := log . WithFields ( applog . GetAppLogFields ( orig ) )
2018-12-04 10:52:57 +00:00
if orig . Status . Sync . Status != newStatus . Sync . Status {
message := fmt . Sprintf ( "Updated sync status: %s -> %s" , orig . Status . Sync . Status , newStatus . Sync . Status )
2025-01-07 15:08:51 +00:00
ctrl . logAppEvent ( context . TODO ( ) , orig , argo . EventInfo { Reason : argo . EventReasonResourceUpdated , Type : corev1 . EventTypeNormal } , message )
2018-07-12 19:39:46 +00:00
}
2018-12-04 10:52:57 +00:00
if orig . Status . Health . Status != newStatus . Health . Status {
2025-05-20 15:26:07 +00:00
// Update the last transition time to now. This should be the ONLY place in code where this is set, because it's
// the only place that is reliably aware of the previous and updated health statuses.
now := metav1 . Now ( )
newStatus . Health . LastTransitionTime = & now
2018-12-04 10:52:57 +00:00
message := fmt . Sprintf ( "Updated health status: %s -> %s" , orig . Status . Health . Status , newStatus . Health . Status )
2025-01-07 15:08:51 +00:00
ctrl . logAppEvent ( context . TODO ( ) , orig , argo . EventInfo { Reason : argo . EventReasonResourceUpdated , Type : corev1 . EventTypeNormal } , message )
2025-05-20 15:26:07 +00:00
} else {
// make sure the last transition time is the same and populated if the health is the same
newStatus . Health . LastTransitionTime = orig . Status . Health . LastTransitionTime
2018-07-12 19:39:46 +00:00
}
2018-12-18 02:23:35 +00:00
var newAnnotations map [ string ] string
if orig . GetAnnotations ( ) != nil {
newAnnotations = make ( map [ string ] string )
2026-02-06 22:16:37 +00:00
maps . Copy ( newAnnotations , orig . GetAnnotations ( ) )
2021-05-19 19:43:04 +00:00
delete ( newAnnotations , appv1 . AnnotationKeyRefresh )
2024-12-16 21:59:09 +00:00
delete ( newAnnotations , appv1 . AnnotationKeyHydrate )
2018-12-18 02:23:35 +00:00
}
2024-08-01 18:55:55 +00:00
patch , modified , err := createMergePatch (
2018-12-18 02:23:35 +00:00
& appv1 . Application { ObjectMeta : metav1 . ObjectMeta { Annotations : orig . GetAnnotations ( ) } , Status : orig . Status } ,
2024-08-01 18:55:55 +00:00
& appv1 . Application { ObjectMeta : metav1 . ObjectMeta { Annotations : newAnnotations } , Status : * newStatus } )
2018-07-12 19:39:46 +00:00
if err != nil {
2025-09-27 21:47:21 +00:00
logCtx . WithError ( err ) . Error ( "Error constructing app status patch" )
2025-09-22 04:05:06 +00:00
return patchDuration
2018-07-12 19:39:46 +00:00
}
2018-11-30 21:50:27 +00:00
if ! modified {
2018-09-24 15:52:43 +00:00
logCtx . Infof ( "No status changes. Skipping patch" )
2025-09-22 04:05:06 +00:00
return patchDuration
2018-05-03 22:55:01 +00:00
}
2023-09-21 05:18:38 +00:00
// calculate time for path call
start := time . Now ( )
defer func ( ) {
2025-03-27 16:37:52 +00:00
patchDuration = time . Since ( start )
2023-09-21 05:18:38 +00:00
} ( )
2023-10-24 01:15:53 +00:00
_ , err = ctrl . PatchAppWithWriteBack ( context . Background ( ) , orig . Name , orig . Namespace , types . MergePatchType , patch , metav1 . PatchOptions { } )
2018-02-22 18:56:14 +00:00
if err != nil {
2025-09-27 21:47:21 +00:00
logCtx . WithError ( err ) . Warn ( "Error updating application" )
2018-04-11 19:53:33 +00:00
} else {
2018-09-24 15:52:43 +00:00
logCtx . Infof ( "Update successful" )
2018-02-22 18:56:14 +00:00
}
2025-03-27 16:37:52 +00:00
return patchDuration
2018-02-22 18:56:14 +00:00
}
2018-09-11 21:28:53 +00:00
// autoSync will initiate a sync operation for an application configured with automated sync
2025-07-14 14:36:05 +00:00
func ( ctrl * ApplicationController ) autoSync ( app * appv1 . Application , syncStatus * appv1 . SyncStatus , resources [ ] appv1 . ResourceStatus , shouldCompareRevisions bool ) ( * appv1 . ApplicationCondition , time . Duration ) {
2025-05-20 19:48:09 +00:00
logCtx := log . WithFields ( applog . GetAppLogFields ( app ) )
2024-07-04 10:18:18 +00:00
ts := stats . NewTimingStats ( )
defer func ( ) {
for k , v := range ts . Timings ( ) {
logCtx = logCtx . WithField ( k , v . Milliseconds ( ) )
}
logCtx = logCtx . WithField ( "time_ms" , time . Since ( ts . StartTime ) . Milliseconds ( ) )
logCtx . Debug ( "Finished auto sync" )
} ( )
2025-03-21 11:47:01 +00:00
if app . Spec . SyncPolicy == nil || ! app . Spec . SyncPolicy . IsAutomatedSyncEnabled ( ) {
2023-09-21 05:18:38 +00:00
return nil , 0
2018-09-11 21:28:53 +00:00
}
2022-12-16 20:47:08 +00:00
2018-09-11 21:28:53 +00:00
if app . Operation != nil {
logCtx . Infof ( "Skipping auto-sync: another operation is in progress" )
2023-09-21 05:18:38 +00:00
return nil , 0
2018-09-11 21:28:53 +00:00
}
2019-03-04 08:56:36 +00:00
if app . DeletionTimestamp != nil && ! app . DeletionTimestamp . IsZero ( ) {
logCtx . Infof ( "Skipping auto-sync: deletion in progress" )
2023-09-21 05:18:38 +00:00
return nil , 0
2019-03-04 08:56:36 +00:00
}
2019-10-01 22:23:09 +00:00
2018-09-11 21:28:53 +00:00
// Only perform auto-sync if we detect OutOfSync status. This is to prevent us from attempting
// a sync when application is already in a Synced or Unknown state
2018-12-04 10:52:57 +00:00
if syncStatus . Status != appv1 . SyncStatusCodeOutOfSync {
logCtx . Infof ( "Skipping auto-sync: application status is %s" , syncStatus . Status )
2023-09-21 05:18:38 +00:00
return nil , 0
2018-09-11 21:28:53 +00:00
}
2018-09-24 15:52:43 +00:00
2020-03-18 21:19:54 +00:00
if ! app . Spec . SyncPolicy . Automated . Prune {
requirePruneOnly := true
for _ , r := range resources {
if r . Status != appv1 . SyncStatusCodeSynced && ! r . RequiresPruning {
requirePruneOnly = false
break
}
}
if requirePruneOnly {
logCtx . Infof ( "Skipping auto-sync: need to prune extra resources only but automated prune is disabled" )
2023-09-21 05:18:38 +00:00
return nil , 0
2020-03-18 21:19:54 +00:00
}
}
2025-09-11 15:19:00 +00:00
source := ptr . To ( app . Spec . GetSource ( ) )
2025-07-14 14:36:05 +00:00
desiredRevisions := [ ] string { syncStatus . Revision }
if app . Spec . HasMultipleSources ( ) {
2025-09-11 15:19:00 +00:00
source = nil
2025-07-14 14:36:05 +00:00
desiredRevisions = syncStatus . Revisions
}
2024-08-15 11:38:35 +00:00
2019-07-25 02:26:09 +00:00
op := appv1 . Operation {
Sync : & appv1 . SyncOperation {
2025-09-11 15:19:00 +00:00
Source : source ,
2025-07-14 14:36:05 +00:00
Revision : syncStatus . Revision ,
2020-02-10 22:09:25 +00:00
Prune : app . Spec . SyncPolicy . Automated . Prune ,
SyncOptions : app . Spec . SyncPolicy . SyncOptions ,
2025-09-11 15:19:00 +00:00
Sources : app . Spec . Sources ,
2025-07-14 14:36:05 +00:00
Revisions : syncStatus . Revisions ,
2019-07-25 02:26:09 +00:00
} ,
2020-03-18 20:32:39 +00:00
InitiatedBy : appv1 . OperationInitiator { Automated : true } ,
2020-07-28 17:14:17 +00:00
Retry : appv1 . RetryStrategy { Limit : 5 } ,
}
if app . Spec . SyncPolicy . Retry != nil {
op . Retry = * app . Spec . SyncPolicy . Retry
2019-07-25 02:26:09 +00:00
}
2025-07-07 17:38:37 +00:00
2018-09-11 21:28:53 +00:00
// It is possible for manifests to remain OutOfSync even after a sync/kubectl apply (e.g.
// auto-sync with pruning disabled). We need to ensure that we do not keep Syncing an
// application in an infinite loop. To detect this, we only attempt the Sync if the revision
2018-09-24 15:52:43 +00:00
// and parameter overrides are different from our most recent sync operation.
2025-07-14 14:36:05 +00:00
alreadyAttempted , lastAttemptedRevisions , lastAttemptedPhase := alreadyAttemptedSync ( app , desiredRevisions , shouldCompareRevisions )
ts . AddCheckpoint ( "already_attempted_sync_ms" )
2025-07-07 17:38:37 +00:00
if alreadyAttempted {
2025-07-14 14:36:05 +00:00
if ! lastAttemptedPhase . Successful ( ) {
logCtx . Warnf ( "Skipping auto-sync: failed previous sync attempt to %s and will not retry for %s" , lastAttemptedRevisions , desiredRevisions )
message := fmt . Sprintf ( "Failed last sync attempt to %s: %s" , lastAttemptedRevisions , app . Status . OperationState . Message )
2023-09-21 05:18:38 +00:00
return & appv1 . ApplicationCondition { Type : appv1 . ApplicationConditionSyncError , Message : message } , 0
2018-09-11 21:28:53 +00:00
}
2025-07-14 14:36:05 +00:00
if ! app . Spec . SyncPolicy . Automated . SelfHeal {
logCtx . Infof ( "Skipping auto-sync: most recent sync already to %s" , desiredRevisions )
2025-07-07 17:38:37 +00:00
return nil , 0
2019-07-25 02:26:09 +00:00
}
2025-07-07 17:38:37 +00:00
// Self heal will trigger a new sync operation when the desired state changes and cause the application to
// be OutOfSync when it was previously synced Successfully. This means SelfHeal should only ever be attempted
// when the revisions have not changed, and where the previous sync to these revision was successful
2025-12-10 18:39:00 +00:00
if app . Status . OperationState != nil && app . Status . OperationState . Operation . Sync != nil {
op . Sync . SelfHealAttemptsCount = app . Status . OperationState . Operation . Sync . SelfHealAttemptsCount
2025-07-07 17:38:37 +00:00
}
2025-07-14 14:36:05 +00:00
if remainingTime := ctrl . selfHealRemainingBackoff ( app , int ( op . Sync . SelfHealAttemptsCount ) ) ; remainingTime > 0 {
logCtx . Infof ( "Skipping auto-sync: already attempted sync to %s with timeout %v (retrying in %v)" , lastAttemptedRevisions , ctrl . selfHealTimeout , remainingTime )
2025-07-07 17:38:37 +00:00
ctrl . requestAppRefresh ( app . QualifiedName ( ) , CompareWithLatest . Pointer ( ) , & remainingTime )
return nil , 0
}
op . Sync . SelfHealAttemptsCount ++
for _ , resource := range resources {
if resource . Status != appv1 . SyncStatusCodeSynced {
op . Sync . Resources = append ( op . Sync . Resources , appv1 . SyncOperationResource {
Kind : resource . Kind ,
Group : resource . Group ,
Name : resource . Name ,
} )
2025-01-08 20:26:02 +00:00
}
}
2018-09-11 21:28:53 +00:00
}
2024-07-04 10:18:18 +00:00
ts . AddCheckpoint ( "already_attempted_check_ms" )
2019-07-25 02:26:09 +00:00
2020-10-07 04:10:50 +00:00
if app . Spec . SyncPolicy . Automated . Prune && ! app . Spec . SyncPolicy . Automated . AllowEmpty {
2020-07-29 02:52:30 +00:00
bAllNeedPrune := true
for _ , r := range resources {
if ! r . RequiresPruning {
bAllNeedPrune = false
}
}
if bAllNeedPrune {
2025-07-14 14:36:05 +00:00
message := fmt . Sprintf ( "Skipping sync attempt to %s: auto-sync will wipe out all resources" , desiredRevisions )
2024-09-04 14:58:15 +00:00
logCtx . Warn ( message )
2023-09-21 05:18:38 +00:00
return & appv1 . ApplicationCondition { Type : appv1 . ApplicationConditionSyncError , Message : message } , 0
2020-07-29 02:52:30 +00:00
}
}
2023-09-21 05:18:38 +00:00
2018-09-11 21:28:53 +00:00
appIf := ctrl . applicationClientset . ArgoprojV1alpha1 ( ) . Applications ( app . Namespace )
2024-07-04 10:18:18 +00:00
ts . AddCheckpoint ( "get_applications_ms" )
2023-09-21 05:18:38 +00:00
start := time . Now ( )
2023-10-24 01:15:53 +00:00
updatedApp , err := argo . SetAppOperation ( appIf , app . Name , & op )
2024-07-04 10:18:18 +00:00
ts . AddCheckpoint ( "set_app_operation_ms" )
2023-09-21 05:18:38 +00:00
setOpTime := time . Since ( start )
2018-09-11 21:28:53 +00:00
if err != nil {
2024-12-30 08:56:41 +00:00
if stderrors . Is ( err , argo . ErrAnotherOperationInProgress ) {
2023-10-23 15:40:13 +00:00
// skipping auto-sync because another operation is in progress and was not noticed due to stale data in informer
// it is safe to skip auto-sync because it is already running
2025-09-27 21:47:21 +00:00
logCtx . WithError ( err ) . Warnf ( "Failed to initiate auto-sync to %s" , desiredRevisions )
2023-10-23 15:40:13 +00:00
return nil , 0
}
2025-09-27 21:47:21 +00:00
logCtx . WithError ( err ) . Errorf ( "Failed to initiate auto-sync to %s" , desiredRevisions )
2023-09-21 05:18:38 +00:00
return & appv1 . ApplicationCondition { Type : appv1 . ApplicationConditionSyncError , Message : err . Error ( ) } , setOpTime
2018-09-11 21:28:53 +00:00
}
2025-01-07 15:25:22 +00:00
ctrl . writeBackToInformer ( updatedApp )
2024-07-04 10:18:18 +00:00
ts . AddCheckpoint ( "write_back_to_informer_ms" )
2024-05-13 13:22:17 +00:00
2025-07-16 17:39:30 +00:00
message := fmt . Sprintf ( "Initiated automated sync to %s" , desiredRevisions )
2025-01-07 15:08:51 +00:00
ctrl . logAppEvent ( context . TODO ( ) , app , argo . EventInfo { Reason : argo . EventReasonOperationStarted , Type : corev1 . EventTypeNormal } , message )
2018-09-24 15:52:43 +00:00
logCtx . Info ( message )
2023-09-21 05:18:38 +00:00
return nil , setOpTime
2018-09-11 21:28:53 +00:00
}
2025-07-14 14:36:05 +00:00
// alreadyAttemptedSync returns whether the most recently synced revision(s) exactly match the given desiredRevisions
// and for the same application source. If the revision(s) have changed or the Application source configuration has been updated,
// it will return false, indicating that a new sync should be attempted.
// When newRevisionHasChanges is false, due to commits not having direct changes on the application, it will not compare the revision(s), but only the sources.
// It also returns the last synced revisions if any, and the result of that last sync operation.
func alreadyAttemptedSync ( app * appv1 . Application , desiredRevisions [ ] string , newRevisionHasChanges bool ) ( bool , [ ] string , synccommon . OperationPhase ) {
if app . Status . OperationState == nil {
// The operation state may be removed when new operations are triggered
return false , [ ] string { } , ""
}
if app . Status . OperationState . SyncResult == nil {
// If the sync has completed without result, it is very likely that an error happened
// We don't want to resync with auto-sync indefinitely. We should have retried the configured amount of time already
// In this case, a manual action to restore the app may be required
log . WithFields ( applog . GetAppLogFields ( app ) ) . Warn ( "Already attempted sync: sync does not have any results" )
return app . Status . OperationState . Phase . Completed ( ) , [ ] string { } , app . Status . OperationState . Phase
}
if newRevisionHasChanges {
log . WithFields ( applog . GetAppLogFields ( app ) ) . Infof ( "Already attempted sync: comparing synced revisions to %s" , desiredRevisions )
if app . Spec . HasMultipleSources ( ) {
if ! reflect . DeepEqual ( app . Status . OperationState . SyncResult . Revisions , desiredRevisions ) {
return false , app . Status . OperationState . SyncResult . Revisions , app . Status . OperationState . Phase
2024-09-06 15:40:48 +00:00
}
} else {
2025-07-14 14:36:05 +00:00
if len ( desiredRevisions ) != 1 || app . Status . OperationState . SyncResult . Revision != desiredRevisions [ 0 ] {
return false , [ ] string { app . Status . OperationState . SyncResult . Revision } , app . Status . OperationState . Phase
2024-09-06 15:40:48 +00:00
}
2022-12-16 20:47:08 +00:00
}
2025-07-14 14:36:05 +00:00
} else {
log . WithFields ( applog . GetAppLogFields ( app ) ) . Debugf ( "Already attempted sync: revisions %s have no changes" , desiredRevisions )
2022-12-16 20:47:08 +00:00
}
2025-07-14 14:36:05 +00:00
log . WithFields ( applog . GetAppLogFields ( app ) ) . Debug ( "Already attempted sync: comparing sources" )
if app . Spec . HasMultipleSources ( ) {
return reflect . DeepEqual ( app . Spec . Sources , app . Status . OperationState . SyncResult . Sources ) , app . Status . OperationState . SyncResult . Revisions , app . Status . OperationState . Phase
2018-09-24 15:52:43 +00:00
}
2025-07-14 14:36:05 +00:00
return reflect . DeepEqual ( app . Spec . GetSource ( ) , app . Status . OperationState . SyncResult . Source ) , [ ] string { app . Status . OperationState . SyncResult . Revision } , app . Status . OperationState . Phase
2019-07-25 02:26:09 +00:00
}
2025-07-07 17:38:37 +00:00
func ( ctrl * ApplicationController ) selfHealRemainingBackoff ( app * appv1 . Application , selfHealAttemptsCount int ) time . Duration {
2019-07-25 02:26:09 +00:00
if app . Status . OperationState == nil {
2025-07-07 17:38:37 +00:00
return time . Duration ( 0 )
2019-07-25 02:26:09 +00:00
}
2025-05-22 09:35:12 +00:00
var timeSinceOperation * time . Duration
if app . Status . OperationState . FinishedAt != nil {
timeSinceOperation = ptr . To ( time . Since ( app . Status . OperationState . FinishedAt . Time ) )
}
2019-07-25 02:26:09 +00:00
var retryAfter time . Duration
2025-07-07 17:38:37 +00:00
if ctrl . selfHealBackoff == nil {
2025-05-22 09:35:12 +00:00
if timeSinceOperation == nil {
2024-10-08 16:32:00 +00:00
retryAfter = ctrl . selfHealTimeout
} else {
2025-05-22 09:35:12 +00:00
retryAfter = ctrl . selfHealTimeout - * timeSinceOperation
2024-10-08 16:32:00 +00:00
}
2019-07-25 02:26:09 +00:00
} else {
2025-07-07 17:38:37 +00:00
backOff := * ctrl . selfHealBackoff
backOff . Steps = selfHealAttemptsCount
2024-10-08 16:32:00 +00:00
var delay time . Duration
2024-12-19 14:07:10 +00:00
steps := backOff . Steps
2026-02-06 22:16:37 +00:00
for range steps {
2024-10-08 16:32:00 +00:00
delay = backOff . Step ( )
}
2025-05-22 09:35:12 +00:00
if timeSinceOperation == nil {
2024-10-08 16:32:00 +00:00
retryAfter = delay
} else {
2025-05-22 09:35:12 +00:00
retryAfter = delay - * timeSinceOperation
2024-10-08 16:32:00 +00:00
}
2019-07-25 02:26:09 +00:00
}
2025-07-07 17:38:37 +00:00
return retryAfter
}
2023-09-11 23:17:02 +00:00
// isAppNamespaceAllowed returns whether the application is allowed in the
// namespace it's residing in.
func ( ctrl * ApplicationController ) isAppNamespaceAllowed ( app * appv1 . Application ) bool {
2024-08-13 16:14:16 +00:00
return app . Namespace == ctrl . namespace || glob . MatchStringInList ( ctrl . applicationNamespaces , app . Namespace , glob . REGEXP )
2023-09-11 23:17:02 +00:00
}
2025-01-02 23:26:59 +00:00
func ( ctrl * ApplicationController ) canProcessApp ( obj any ) bool {
2020-10-09 20:16:54 +00:00
app , ok := obj . ( * appv1 . Application )
if ! ok {
return false
}
2023-01-25 14:14:29 +00:00
// Only process given app if it exists in a watched namespace, or in the
// control plane's namespace.
2023-09-11 23:17:02 +00:00
if ! ctrl . isAppNamespaceAllowed ( app ) {
2023-01-25 14:14:29 +00:00
return false
}
2023-02-23 17:02:50 +00:00
if annotations := app . GetAnnotations ( ) ; annotations != nil {
if skipVal , ok := annotations [ common . AnnotationKeyAppSkipReconcile ] ; ok {
2025-05-20 19:48:09 +00:00
logCtx := log . WithFields ( applog . GetAppLogFields ( app ) )
2023-02-23 17:02:50 +00:00
if skipReconcile , err := strconv . ParseBool ( skipVal ) ; err == nil {
if skipReconcile {
logCtx . Debugf ( "Skipping Application reconcile based on annotation %s" , common . AnnotationKeyAppSkipReconcile )
return false
}
} else {
2025-09-27 21:47:21 +00:00
logCtx . WithError ( err ) . Debugf ( "Unable to determine if Application should skip reconcile based on annotation %s" , common . AnnotationKeyAppSkipReconcile )
2023-02-23 17:02:50 +00:00
}
}
}
2025-01-13 18:15:42 +00:00
destCluster , err := argo . GetDestinationCluster ( context . Background ( ) , app . Spec . Destination , ctrl . db )
2024-01-11 06:32:11 +00:00
if err != nil {
return ctrl . clusterSharding . IsManagedCluster ( nil )
2020-10-09 20:16:54 +00:00
}
2025-01-13 18:15:42 +00:00
return ctrl . clusterSharding . IsManagedCluster ( destCluster )
2020-10-09 20:16:54 +00:00
}
2020-12-08 16:45:37 +00:00
func ( ctrl * ApplicationController ) newApplicationInformerAndLister ( ) ( cache . SharedIndexInformer , applisters . ApplicationLister ) {
2022-08-10 09:39:10 +00:00
watchNamespace := ctrl . namespace
// If we have at least one additional namespace configured, we need to
// watch on them all.
if len ( ctrl . applicationNamespaces ) > 0 {
watchNamespace = ""
}
2022-04-04 14:44:35 +00:00
refreshTimeout := ctrl . statusRefreshTimeout
if ctrl . statusHardRefreshTimeout . Seconds ( ) != 0 && ( ctrl . statusHardRefreshTimeout < ctrl . statusRefreshTimeout ) {
refreshTimeout = ctrl . statusHardRefreshTimeout
}
2020-12-08 16:45:37 +00:00
informer := cache . NewSharedIndexInformer (
& cache . ListWatch {
ListFunc : func ( options metav1 . ListOptions ) ( apiruntime . Object , error ) {
2022-08-10 09:39:10 +00:00
// We are only interested in apps that exist in namespaces the
// user wants to be enabled.
appList , err := ctrl . applicationClientset . ArgoprojV1alpha1 ( ) . Applications ( watchNamespace ) . List ( context . TODO ( ) , options )
if err != nil {
return nil , err
}
newItems := [ ] appv1 . Application { }
for _ , app := range appList . Items {
2023-09-11 23:17:02 +00:00
if ctrl . isAppNamespaceAllowed ( & app ) {
2022-08-10 09:39:10 +00:00
newItems = append ( newItems , app )
}
}
appList . Items = newItems
return appList , nil
2020-12-08 16:45:37 +00:00
} ,
WatchFunc : func ( options metav1 . ListOptions ) ( watch . Interface , error ) {
2022-08-10 09:39:10 +00:00
return ctrl . applicationClientset . ArgoprojV1alpha1 ( ) . Applications ( watchNamespace ) . Watch ( context . TODO ( ) , options )
2020-12-08 16:45:37 +00:00
} ,
} ,
& appv1 . Application { } ,
2022-04-04 14:44:35 +00:00
refreshTimeout ,
2020-12-08 16:45:37 +00:00
cache . Indexers {
2025-01-02 23:26:59 +00:00
cache . NamespaceIndex : func ( obj any ) ( [ ] string , error ) {
2020-12-08 16:45:37 +00:00
app , ok := obj . ( * appv1 . Application )
if ok {
2023-09-11 23:17:02 +00:00
// We only generally work with applications that are in one
// the allowed namespaces.
if ctrl . isAppNamespaceAllowed ( app ) {
// If the application is not allowed to use the project,
// log an error.
if _ , err := ctrl . getAppProj ( app ) ; err != nil {
ctrl . setAppCondition ( app , ctrl . projectErrorToCondition ( err , app ) )
2025-01-13 18:15:42 +00:00
} else if _ , err = argo . GetDestinationCluster ( context . Background ( ) , app . Spec . Destination , ctrl . db ) ; err != nil {
ctrl . setAppCondition ( app , appv1 . ApplicationCondition { Type : appv1 . ApplicationConditionInvalidSpecError , Message : err . Error ( ) } )
2023-09-11 23:17:02 +00:00
}
2022-08-10 09:39:10 +00:00
}
2020-12-08 16:45:37 +00:00
}
return cache . MetaNamespaceIndexFunc ( obj )
} ,
2025-01-02 23:26:59 +00:00
orphanedIndex : func ( obj any ) ( i [ ] string , e error ) {
2020-12-08 16:45:37 +00:00
app , ok := obj . ( * appv1 . Application )
if ! ok {
return nil , nil
}
2023-09-11 23:17:02 +00:00
if ! ctrl . isAppNamespaceAllowed ( app ) {
return nil , nil
}
proj , err := ctrl . getAppProj ( app )
2020-12-08 16:45:37 +00:00
if err != nil {
return nil , nil
}
if proj . Spec . OrphanedResources != nil {
return [ ] string { app . Spec . Destination . Namespace } , nil
}
return nil , nil
} ,
} ,
2018-02-20 23:23:37 +00:00
)
2020-12-08 16:45:37 +00:00
lister := applisters . NewApplicationLister ( informer . GetIndexer ( ) )
2023-10-18 15:17:00 +00:00
_ , err := informer . AddEventHandler (
2018-02-20 23:23:37 +00:00
cache . ResourceEventHandlerFuncs {
2025-01-02 23:26:59 +00:00
AddFunc : func ( obj any ) {
2020-10-09 20:16:54 +00:00
if ! ctrl . canProcessApp ( obj ) {
return
}
2018-02-20 23:23:37 +00:00
key , err := cache . MetaNamespaceKeyFunc ( obj )
if err == nil {
2023-10-18 19:08:04 +00:00
ctrl . appRefreshQueue . AddRateLimited ( key )
2018-02-20 23:23:37 +00:00
}
2024-03-01 18:56:48 +00:00
newApp , newOK := obj . ( * appv1 . Application )
if err == nil && newOK {
ctrl . clusterSharding . AddApp ( newApp )
}
2018-02-20 23:23:37 +00:00
} ,
2025-01-02 23:26:59 +00:00
UpdateFunc : func ( old , new any ) {
2020-10-09 20:16:54 +00:00
if ! ctrl . canProcessApp ( new ) {
return
}
2018-02-20 23:23:37 +00:00
key , err := cache . MetaNamespaceKeyFunc ( new )
2018-09-11 21:28:53 +00:00
if err != nil {
return
}
2024-01-21 06:22:32 +00:00
2020-01-08 22:07:36 +00:00
var compareWith * CompareWith
2024-01-21 06:22:32 +00:00
var delay * time . Duration
2018-09-11 21:28:53 +00:00
oldApp , oldOK := old . ( * appv1 . Application )
newApp , newOK := new . ( * appv1 . Application )
2024-01-21 06:22:32 +00:00
if oldOK && newOK {
if automatedSyncEnabled ( oldApp , newApp ) {
2025-05-20 19:48:09 +00:00
log . WithFields ( applog . GetAppLogFields ( newApp ) ) . Info ( "Enabled automated sync" )
2024-01-21 06:22:32 +00:00
compareWith = CompareWithLatest . Pointer ( )
}
if ctrl . statusRefreshJitter != 0 && oldApp . ResourceVersion == newApp . ResourceVersion {
// Handler is refreshing the apps, add a random jitter to spread the load and avoid spikes
jitter := time . Duration ( float64 ( ctrl . statusRefreshJitter ) * rand . Float64 ( ) )
delay = & jitter
}
2018-02-20 23:23:37 +00:00
}
2024-01-21 06:22:32 +00:00
ctrl . requestAppRefresh ( newApp . QualifiedName ( ) , compareWith , delay )
2024-07-31 21:56:26 +00:00
if ! newOK || ( delay != nil && * delay != time . Duration ( 0 ) ) {
ctrl . appOperationQueue . AddRateLimited ( key )
}
2024-12-16 21:59:09 +00:00
if ctrl . hydrator != nil {
ctrl . appHydrateQueue . AddRateLimited ( newApp . QualifiedName ( ) )
}
2024-03-01 18:56:48 +00:00
ctrl . clusterSharding . UpdateApp ( newApp )
2018-02-20 23:23:37 +00:00
} ,
2025-01-02 23:26:59 +00:00
DeleteFunc : func ( obj any ) {
2020-10-09 20:16:54 +00:00
if ! ctrl . canProcessApp ( obj ) {
return
}
2018-02-20 23:23:37 +00:00
// IndexerInformer uses a delta queue, therefore for deletes we have to use this
// key function.
key , err := cache . DeletionHandlingMetaNamespaceKeyFunc ( obj )
if err == nil {
2023-10-18 19:08:04 +00:00
// for deletes, we immediately add to the refresh queue
2018-09-11 21:28:53 +00:00
ctrl . appRefreshQueue . Add ( key )
2018-02-20 23:23:37 +00:00
}
2024-03-01 18:56:48 +00:00
delApp , delOK := obj . ( * appv1 . Application )
if err == nil && delOK {
ctrl . clusterSharding . DeleteApp ( delApp )
}
2018-02-20 23:23:37 +00:00
} ,
} ,
)
2023-10-18 15:17:00 +00:00
if err != nil {
return nil , nil
}
2020-12-08 16:45:37 +00:00
return informer , lister
2018-02-20 23:23:37 +00:00
}
2018-07-07 07:54:06 +00:00
2022-11-19 01:57:34 +00:00
func ( ctrl * ApplicationController ) projectErrorToCondition ( err error , app * appv1 . Application ) appv1 . ApplicationCondition {
var condition appv1 . ApplicationCondition
2025-01-03 17:09:37 +00:00
if apierrors . IsNotFound ( err ) {
2022-11-19 01:57:34 +00:00
condition = appv1 . ApplicationCondition {
Type : appv1 . ApplicationConditionInvalidSpecError ,
Message : fmt . Sprintf ( "Application referencing project %s which does not exist" , app . Spec . Project ) ,
}
} else {
condition = appv1 . ApplicationCondition { Type : appv1 . ApplicationConditionUnknownError , Message : err . Error ( ) }
}
return condition
}
2020-06-04 18:36:26 +00:00
func ( ctrl * ApplicationController ) RegisterClusterSecretUpdater ( ctx context . Context ) {
2024-01-11 06:32:11 +00:00
updater := NewClusterInfoUpdater ( ctrl . stateCache , ctrl . db , ctrl . appLister . Applications ( "" ) , ctrl . cache , ctrl . clusterSharding . IsManagedCluster , ctrl . getAppProj , ctrl . namespace )
2020-06-04 18:36:26 +00:00
go updater . Run ( ctx )
}
2018-07-14 00:13:31 +00:00
func isOperationInProgress ( app * appv1 . Application ) bool {
2018-07-07 07:54:06 +00:00
return app . Status . OperationState != nil && ! app . Status . OperationState . Phase . Completed ( )
}
2018-09-11 21:28:53 +00:00
2020-01-08 22:07:36 +00:00
// automatedSyncEnabled tests if an app went from auto-sync disabled to enabled.
2018-09-11 21:28:53 +00:00
// if it was toggled to be enabled, the informer handler will force a refresh
2020-01-08 22:07:36 +00:00
func automatedSyncEnabled ( oldApp * appv1 . Application , newApp * appv1 . Application ) bool {
oldEnabled := false
oldSelfHealEnabled := false
2025-03-21 11:47:01 +00:00
if oldApp . Spec . SyncPolicy != nil && oldApp . Spec . SyncPolicy . IsAutomatedSyncEnabled ( ) {
2020-01-08 22:07:36 +00:00
oldEnabled = true
oldSelfHealEnabled = oldApp . Spec . SyncPolicy . Automated . SelfHeal
}
newEnabled := false
newSelfHealEnabled := false
2025-03-21 11:47:01 +00:00
if newApp . Spec . SyncPolicy != nil && newApp . Spec . SyncPolicy . IsAutomatedSyncEnabled ( ) {
2020-01-08 22:07:36 +00:00
newEnabled = true
newSelfHealEnabled = newApp . Spec . SyncPolicy . Automated . SelfHeal
}
if ! oldEnabled && newEnabled {
return true
2018-09-11 21:28:53 +00:00
}
2020-01-08 22:07:36 +00:00
if ! oldSelfHealEnabled && newSelfHealEnabled {
2018-09-11 21:28:53 +00:00
return true
}
// nothing changed
return false
}
2022-08-10 09:39:10 +00:00
// toAppKey returns the application key from a given appName, that is, it will
// replace underscores with forward-slashes to become a <namespace>/<name>
// format. If the appName is an unqualified name (such as, "app"), it will use
// the controller's namespace in the key.
func ( ctrl * ApplicationController ) toAppKey ( appName string ) string {
if ! strings . Contains ( appName , "_" ) && ! strings . Contains ( appName , "/" ) {
return ctrl . namespace + "/" + appName
} else if strings . Contains ( appName , "/" ) {
return appName
}
2025-01-07 15:25:22 +00:00
return strings . ReplaceAll ( appName , "_" , "/" )
2022-08-10 09:39:10 +00:00
}
func ( ctrl * ApplicationController ) toAppQualifiedName ( appName , appNamespace string ) string {
return fmt . Sprintf ( "%s/%s" , appNamespace , appName )
}
2023-06-05 13:19:14 +00:00
2024-03-01 18:56:48 +00:00
func ( ctrl * ApplicationController ) getAppList ( options metav1 . ListOptions ) ( * appv1 . ApplicationList , error ) {
watchNamespace := ctrl . namespace
// If we have at least one additional namespace configured, we need to
// watch on them all.
if len ( ctrl . applicationNamespaces ) > 0 {
watchNamespace = ""
}
appList , err := ctrl . applicationClientset . ArgoprojV1alpha1 ( ) . Applications ( watchNamespace ) . List ( context . TODO ( ) , options )
if err != nil {
return nil , err
}
newItems := [ ] appv1 . Application { }
for _ , app := range appList . Items {
if ctrl . isAppNamespaceAllowed ( & app ) {
newItems = append ( newItems , app )
}
}
appList . Items = newItems
return appList , nil
}
2025-01-07 15:08:51 +00:00
func ( ctrl * ApplicationController ) logAppEvent ( ctx context . Context , a * appv1 . Application , eventInfo argo . EventInfo , message string ) {
eventLabels := argo . GetAppEventLabels ( ctx , a , applisters . NewAppProjectLister ( ctrl . projInformer . GetIndexer ( ) ) , ctrl . namespace , ctrl . settingsMgr , ctrl . db )
2024-06-17 17:27:54 +00:00
ctrl . auditLogger . LogAppEvent ( a , eventInfo , message , "" , eventLabels )
}
2025-10-06 14:30:44 +00:00
func ( ctrl * ApplicationController ) applyImpersonationConfig ( config * rest . Config , proj * appv1 . AppProject , app * appv1 . Application , destCluster * appv1 . Cluster ) error {
impersonationEnabled , err := ctrl . settingsMgr . IsImpersonationEnabled ( )
if err != nil {
return fmt . Errorf ( "error getting impersonation setting: %w" , err )
}
if ! impersonationEnabled {
return nil
}
user , err := deriveServiceAccountToImpersonate ( proj , app , destCluster )
if err != nil {
return fmt . Errorf ( "error deriving service account to impersonate: %w" , err )
}
config . Impersonate = rest . ImpersonationConfig {
UserName : user ,
}
return nil
}
2024-01-25 14:51:43 +00:00
type ClusterFilterFunction func ( c * appv1 . Cluster , distributionFunction sharding . DistributionFunction ) bool