github.com/argoproj/argo-cd/v3@v3.2.1/notification_controller/controller/controller.go (about) 1 package controller 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "time" 8 9 "github.com/argoproj/argo-cd/v3/util/glob" 10 11 "github.com/argoproj/argo-cd/v3/util/notification/k8s" 12 13 service "github.com/argoproj/argo-cd/v3/util/notification/argocd" 14 15 argocert "github.com/argoproj/argo-cd/v3/util/cert" 16 17 "k8s.io/apimachinery/pkg/runtime/schema" 18 19 "github.com/argoproj/argo-cd/v3/util/notification/settings" 20 21 "github.com/argoproj/notifications-engine/pkg/api" 22 "github.com/argoproj/notifications-engine/pkg/controller" 23 "github.com/argoproj/notifications-engine/pkg/services" 24 "github.com/argoproj/notifications-engine/pkg/subscriptions" 25 httputil "github.com/argoproj/notifications-engine/pkg/util/http" 26 log "github.com/sirupsen/logrus" 27 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 29 "k8s.io/apimachinery/pkg/runtime" 30 "k8s.io/apimachinery/pkg/watch" 31 "k8s.io/client-go/dynamic" 32 "k8s.io/client-go/kubernetes" 33 "k8s.io/client-go/tools/cache" 34 35 "github.com/argoproj/argo-cd/v3/pkg/apis/application" 36 ) 37 38 const ( 39 resyncPeriod = 60 * time.Second 40 ) 41 42 var ( 43 applications = schema.GroupVersionResource{Group: application.Group, Version: "v1alpha1", Resource: application.ApplicationPlural} 44 appProjects = schema.GroupVersionResource{Group: application.Group, Version: "v1alpha1", Resource: application.AppProjectPlural} 45 ) 46 47 func newAppProjClient(client dynamic.Interface, namespace string) dynamic.ResourceInterface { 48 resClient := client.Resource(appProjects).Namespace(namespace) 49 return resClient 50 } 51 52 type NotificationController interface { 53 Run(ctx context.Context, processors int) 54 Init(ctx context.Context) error 55 } 56 57 type notificationController struct { 58 ctrl controller.NotificationController 59 appInformer cache.SharedIndexInformer 60 appProjInformer cache.SharedIndexInformer 61 secretInformer cache.SharedIndexInformer 62 configMapInformer cache.SharedIndexInformer 63 } 64 65 func NewController( 66 k8sClient kubernetes.Interface, 67 client dynamic.Interface, 68 argocdService service.Service, 69 namespace string, 70 applicationNamespaces []string, 71 appLabelSelector string, 72 registry *controller.MetricsRegistry, 73 secretName string, 74 configMapName string, 75 selfServiceNotificationEnabled bool, 76 ) *notificationController { 77 var appClient dynamic.ResourceInterface 78 79 namespaceableAppClient := client.Resource(applications) 80 appClient = namespaceableAppClient 81 82 if len(applicationNamespaces) == 0 { 83 appClient = namespaceableAppClient.Namespace(namespace) 84 } 85 appInformer := newInformer(appClient, namespace, applicationNamespaces, appLabelSelector) 86 appProjInformer := newInformer(newAppProjClient(client, namespace), namespace, []string{namespace}, "") 87 var notificationConfigNamespace string 88 if selfServiceNotificationEnabled { 89 notificationConfigNamespace = metav1.NamespaceAll 90 } else { 91 notificationConfigNamespace = namespace 92 } 93 secretInformer := k8s.NewSecretInformer(k8sClient, notificationConfigNamespace, secretName) 94 configMapInformer := k8s.NewConfigMapInformer(k8sClient, notificationConfigNamespace, configMapName) 95 apiFactory := api.NewFactory(settings.GetFactorySettings(argocdService, secretName, configMapName, selfServiceNotificationEnabled), namespace, secretInformer, configMapInformer) 96 97 res := ¬ificationController{ 98 secretInformer: secretInformer, 99 configMapInformer: configMapInformer, 100 appInformer: appInformer, 101 appProjInformer: appProjInformer, 102 } 103 skipProcessingOpt := controller.WithSkipProcessing(func(obj metav1.Object) (bool, string) { 104 app, ok := (obj).(*unstructured.Unstructured) 105 if !ok { 106 return false, "" 107 } 108 if checkAppNotInAdditionalNamespaces(app, namespace, applicationNamespaces) { 109 return true, "app is not in one of the application-namespaces, nor the notification controller namespace" 110 } 111 return !isAppSyncStatusRefreshed(app, log.WithField("app", obj.GetName())), "sync status out of date" 112 }) 113 metricsRegistryOpt := controller.WithMetricsRegistry(registry) 114 alterDestinationsOpt := controller.WithAlterDestinations(res.alterDestinations) 115 116 if !selfServiceNotificationEnabled { 117 res.ctrl = controller.NewController(namespaceableAppClient, appInformer, apiFactory, 118 skipProcessingOpt, 119 metricsRegistryOpt, 120 alterDestinationsOpt) 121 } else { 122 res.ctrl = controller.NewControllerWithNamespaceSupport(namespaceableAppClient, appInformer, apiFactory, 123 skipProcessingOpt, 124 metricsRegistryOpt, 125 alterDestinationsOpt) 126 } 127 return res 128 } 129 130 // Check if app is not in the namespace where the controller is in, and also app is not in one of the applicationNamespaces 131 func checkAppNotInAdditionalNamespaces(app *unstructured.Unstructured, namespace string, applicationNamespaces []string) bool { 132 return namespace != app.GetNamespace() && !glob.MatchStringInList(applicationNamespaces, app.GetNamespace(), glob.REGEXP) 133 } 134 135 func (c *notificationController) alterDestinations(obj metav1.Object, destinations services.Destinations, cfg api.Config) services.Destinations { 136 app, ok := (obj).(*unstructured.Unstructured) 137 if !ok { 138 return destinations 139 } 140 141 if proj := getAppProj(app, c.appProjInformer); proj != nil { 142 destinations.Merge(subscriptions.NewAnnotations(proj.GetAnnotations()).GetDestinations(cfg.DefaultTriggers, cfg.ServiceDefaultTriggers)) 143 destinations.Merge(settings.GetLegacyDestinations(proj.GetAnnotations(), cfg.DefaultTriggers, cfg.ServiceDefaultTriggers)) 144 } 145 return destinations 146 } 147 148 func newInformer(resClient dynamic.ResourceInterface, controllerNamespace string, applicationNamespaces []string, selector string) cache.SharedIndexInformer { 149 informer := cache.NewSharedIndexInformer( 150 &cache.ListWatch{ 151 ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { 152 // We are only interested in apps that exist in namespaces the 153 // user wants to be enabled. 154 options.LabelSelector = selector 155 appList, err := resClient.List(context.TODO(), options) 156 if err != nil { 157 return nil, fmt.Errorf("failed to list applications: %w", err) 158 } 159 newItems := []unstructured.Unstructured{} 160 for _, res := range appList.Items { 161 if controllerNamespace == res.GetNamespace() || glob.MatchStringInList(applicationNamespaces, res.GetNamespace(), glob.REGEXP) { 162 newItems = append(newItems, res) 163 } 164 } 165 appList.Items = newItems 166 return appList, nil 167 }, 168 WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { 169 options.LabelSelector = selector 170 return resClient.Watch(context.TODO(), options) 171 }, 172 }, 173 &unstructured.Unstructured{}, 174 resyncPeriod, 175 cache.Indexers{ 176 cache.NamespaceIndex: cache.MetaNamespaceIndexFunc, 177 }, 178 ) 179 return informer 180 } 181 182 func (c *notificationController) Init(ctx context.Context) error { 183 // resolve certificates using injected "argocd-tls-certs-cm" ConfigMap 184 httputil.SetCertResolver(argocert.GetCertificateForConnect) 185 186 go c.appInformer.Run(ctx.Done()) 187 go c.appProjInformer.Run(ctx.Done()) 188 go c.secretInformer.Run(ctx.Done()) 189 go c.configMapInformer.Run(ctx.Done()) 190 191 if !cache.WaitForCacheSync(ctx.Done(), c.appInformer.HasSynced, c.appProjInformer.HasSynced, c.secretInformer.HasSynced, c.configMapInformer.HasSynced) { 192 return errors.New("timed out waiting for caches to sync") 193 } 194 return nil 195 } 196 197 func (c *notificationController) Run(ctx context.Context, processors int) { 198 c.ctrl.Run(processors, ctx.Done()) 199 } 200 201 func getAppProj(app *unstructured.Unstructured, appProjInformer cache.SharedIndexInformer) *unstructured.Unstructured { 202 projName, ok, err := unstructured.NestedString(app.Object, "spec", "project") 203 if !ok || err != nil { 204 return nil 205 } 206 projObj, ok, err := appProjInformer.GetIndexer().GetByKey(fmt.Sprintf("%s/%s", app.GetNamespace(), projName)) 207 if !ok || err != nil { 208 return nil 209 } 210 proj, ok := projObj.(*unstructured.Unstructured) 211 if !ok { 212 return nil 213 } 214 if proj.GetAnnotations() == nil { 215 proj.SetAnnotations(map[string]string{}) 216 } 217 return proj 218 } 219 220 // Checks if the application SyncStatus has been refreshed by Argo CD after an operation has completed 221 func isAppSyncStatusRefreshed(app *unstructured.Unstructured, logEntry *log.Entry) bool { 222 _, ok, err := unstructured.NestedMap(app.Object, "status", "operationState") 223 if !ok || err != nil { 224 logEntry.Debug("No OperationState found, SyncStatus is assumed to be up-to-date") 225 return true 226 } 227 228 phase, ok, err := unstructured.NestedString(app.Object, "status", "operationState", "phase") 229 if !ok || err != nil { 230 logEntry.Debug("No OperationPhase found, SyncStatus is assumed to be up-to-date") 231 return true 232 } 233 switch phase { 234 case "Failed", "Error", "Succeeded": 235 finishedAtRaw, ok, err := unstructured.NestedString(app.Object, "status", "operationState", "finishedAt") 236 if !ok || err != nil { 237 logEntry.Debugf("No FinishedAt found for completed phase '%s', SyncStatus is assumed to be out-of-date", phase) 238 return false 239 } 240 finishedAt, err := time.Parse(time.RFC3339, finishedAtRaw) 241 if err != nil { 242 logEntry.Warnf("Failed to parse FinishedAt '%s'", finishedAtRaw) 243 return false 244 } 245 var reconciledAt, observedAt time.Time 246 reconciledAtRaw, ok, err := unstructured.NestedString(app.Object, "status", "reconciledAt") 247 if ok && err == nil { 248 reconciledAt, _ = time.Parse(time.RFC3339, reconciledAtRaw) 249 } 250 observedAtRaw, ok, err := unstructured.NestedString(app.Object, "status", "observedAt") 251 if ok && err == nil { 252 observedAt, _ = time.Parse(time.RFC3339, observedAtRaw) 253 } 254 if finishedAt.After(reconciledAt) && finishedAt.After(observedAt) { 255 logEntry.Debugf("SyncStatus out-of-date (FinishedAt=%v, ReconciledAt=%v, Observed=%v", finishedAt, reconciledAt, observedAt) 256 return false 257 } 258 logEntry.Debugf("SyncStatus up-to-date (FinishedAt=%v, ReconciledAt=%v, Observed=%v", finishedAt, reconciledAt, observedAt) 259 default: 260 logEntry.Debugf("Found phase '%s', SyncStatus is assumed to be up-to-date", phase) 261 } 262 263 return true 264 }