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