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