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 := &notificationController{
    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  }