github.com/argoproj/argo-cd/v3@v3.2.1/cmd/argocd-applicationset-controller/commands/applicationset_controller.go (about)

     1  package command
     2  
     3  import (
     4  	"fmt"
     5  	"math"
     6  	"net/http"
     7  	"os"
     8  	"runtime/debug"
     9  	"time"
    10  
    11  	"github.com/argoproj/pkg/v2/stats"
    12  	"k8s.io/apimachinery/pkg/runtime"
    13  	ctrl "sigs.k8s.io/controller-runtime"
    14  
    15  	"github.com/argoproj/argo-cd/v3/reposerver/apiclient"
    16  	logutils "github.com/argoproj/argo-cd/v3/util/log"
    17  	"github.com/argoproj/argo-cd/v3/util/profile"
    18  	"github.com/argoproj/argo-cd/v3/util/tls"
    19  
    20  	"github.com/argoproj/argo-cd/v3/applicationset/controllers"
    21  	"github.com/argoproj/argo-cd/v3/applicationset/generators"
    22  	"github.com/argoproj/argo-cd/v3/applicationset/utils"
    23  	"github.com/argoproj/argo-cd/v3/applicationset/webhook"
    24  	cmdutil "github.com/argoproj/argo-cd/v3/cmd/util"
    25  	"github.com/argoproj/argo-cd/v3/common"
    26  	"github.com/argoproj/argo-cd/v3/util/env"
    27  	"github.com/argoproj/argo-cd/v3/util/github_app"
    28  
    29  	log "github.com/sirupsen/logrus"
    30  	"github.com/spf13/cobra"
    31  	"k8s.io/client-go/dynamic"
    32  	"k8s.io/client-go/kubernetes"
    33  	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
    34  	_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
    35  	"k8s.io/client-go/tools/clientcmd"
    36  	ctrlcache "sigs.k8s.io/controller-runtime/pkg/cache"
    37  	ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
    38  	metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
    39  
    40  	appsetmetrics "github.com/argoproj/argo-cd/v3/applicationset/metrics"
    41  	"github.com/argoproj/argo-cd/v3/applicationset/services"
    42  	appv1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
    43  	"github.com/argoproj/argo-cd/v3/util/cli"
    44  	"github.com/argoproj/argo-cd/v3/util/db"
    45  	"github.com/argoproj/argo-cd/v3/util/errors"
    46  	argosettings "github.com/argoproj/argo-cd/v3/util/settings"
    47  )
    48  
    49  var gitSubmoduleEnabled = env.ParseBoolFromEnv(common.EnvGitSubmoduleEnabled, true)
    50  
    51  const (
    52  	cliName = common.ApplicationSetController
    53  )
    54  
    55  func NewCommand() *cobra.Command {
    56  	var (
    57  		clientConfig                 clientcmd.ClientConfig
    58  		metricsAddr                  string
    59  		probeBindAddr                string
    60  		webhookAddr                  string
    61  		enableLeaderElection         bool
    62  		applicationSetNamespaces     []string
    63  		argocdRepoServer             string
    64  		policy                       string
    65  		enablePolicyOverride         bool
    66  		debugLog                     bool
    67  		dryRun                       bool
    68  		enableProgressiveSyncs       bool
    69  		enableNewGitFileGlobbing     bool
    70  		repoServerPlaintext          bool
    71  		repoServerStrictTLS          bool
    72  		repoServerTimeoutSeconds     int
    73  		maxConcurrentReconciliations int
    74  		scmRootCAPath                string
    75  		allowedScmProviders          []string
    76  		globalPreservedAnnotations   []string
    77  		globalPreservedLabels        []string
    78  		enableGitHubAPIMetrics       bool
    79  		metricsAplicationsetLabels   []string
    80  		enableScmProviders           bool
    81  		webhookParallelism           int
    82  		tokenRefStrictMode           bool
    83  		maxResourcesStatusCount      int
    84  	)
    85  	scheme := runtime.NewScheme()
    86  	_ = clientgoscheme.AddToScheme(scheme)
    87  	_ = appv1alpha1.AddToScheme(scheme)
    88  	command := cobra.Command{
    89  		Use:               cliName,
    90  		Short:             "Starts Argo CD ApplicationSet controller",
    91  		DisableAutoGenTag: true,
    92  		RunE: func(c *cobra.Command, _ []string) error {
    93  			ctx := c.Context()
    94  
    95  			vers := common.GetVersion()
    96  			namespace, _, err := clientConfig.Namespace()
    97  			applicationSetNamespaces = append(applicationSetNamespaces, namespace)
    98  
    99  			errors.CheckError(err)
   100  			vers.LogStartupInfo(
   101  				"ArgoCD ApplicationSet Controller",
   102  				map[string]any{
   103  					"namespace": namespace,
   104  				},
   105  			)
   106  
   107  			cli.SetLogFormat(cmdutil.LogFormat)
   108  			cli.SetLogLevel(cmdutil.LogLevel)
   109  
   110  			ctrl.SetLogger(logutils.NewLogrusLogger(logutils.NewWithCurrentConfig()))
   111  
   112  			// Recover from panic and log the error using the configured logger instead of the default.
   113  			defer func() {
   114  				if r := recover(); r != nil {
   115  					log.WithField("trace", string(debug.Stack())).Fatal("Recovered from panic: ", r)
   116  				}
   117  			}()
   118  
   119  			restConfig, err := clientConfig.ClientConfig()
   120  			errors.CheckError(err)
   121  
   122  			restConfig.UserAgent = fmt.Sprintf("argocd-applicationset-controller/%s (%s)", vers.Version, vers.Platform)
   123  
   124  			policyObj, exists := utils.Policies[policy]
   125  			if !exists {
   126  				log.Error("Policy value can be: sync, create-only, create-update, create-delete, default value: sync")
   127  				os.Exit(1)
   128  			}
   129  
   130  			// By default, watch all namespaces
   131  			var watchedNamespace string
   132  			// If the applicationset-namespaces contains only one namespace it corresponds to the current namespace
   133  			if len(applicationSetNamespaces) == 1 {
   134  				watchedNamespace = (applicationSetNamespaces)[0]
   135  			} else if enableScmProviders && len(allowedScmProviders) == 0 {
   136  				log.Error("When enabling applicationset in any namespace using applicationset-namespaces, you must either set --enable-scm-providers=false or specify --allowed-scm-providers")
   137  				os.Exit(1)
   138  			}
   139  
   140  			var cacheOpt ctrlcache.Options
   141  
   142  			if watchedNamespace != "" {
   143  				cacheOpt = ctrlcache.Options{
   144  					DefaultNamespaces: map[string]ctrlcache.Config{
   145  						watchedNamespace: {},
   146  					},
   147  				}
   148  			}
   149  
   150  			cfg := ctrl.GetConfigOrDie()
   151  			err = appv1alpha1.SetK8SConfigDefaults(cfg)
   152  			if err != nil {
   153  				log.Error(err, "Unable to apply K8s REST config defaults")
   154  				os.Exit(1)
   155  			}
   156  
   157  			mgr, err := ctrl.NewManager(cfg, ctrl.Options{
   158  				Scheme: scheme,
   159  				Metrics: metricsserver.Options{
   160  					BindAddress: metricsAddr,
   161  				},
   162  				Cache:                  cacheOpt,
   163  				HealthProbeBindAddress: probeBindAddr,
   164  				LeaderElection:         enableLeaderElection,
   165  				LeaderElectionID:       "58ac56fa.applicationsets.argoproj.io",
   166  				Client: ctrlclient.Options{
   167  					DryRun: &dryRun,
   168  				},
   169  			})
   170  			if err != nil {
   171  				log.Error(err, "unable to start manager")
   172  				os.Exit(1)
   173  			}
   174  
   175  			pprofMux := http.NewServeMux()
   176  			profile.RegisterProfiler(pprofMux)
   177  			// This looks a little strange. Eg, not using ctrl.Options PprofBindAddress and then adding the pprof mux
   178  			// to the metrics server. However, it allows for the controller to dynamically expose the pprof endpoints
   179  			// and use the existing metrics server, the same pattern that the application controller and api-server follow.
   180  			if err = mgr.AddMetricsServerExtraHandler("/debug/pprof/", pprofMux); err != nil {
   181  				log.Error(err, "failed to register pprof handlers")
   182  			}
   183  			dynamicClient, err := dynamic.NewForConfig(mgr.GetConfig())
   184  			errors.CheckError(err)
   185  			k8sClient, err := kubernetes.NewForConfig(mgr.GetConfig())
   186  			errors.CheckError(err)
   187  
   188  			argoSettingsMgr := argosettings.NewSettingsManager(ctx, k8sClient, namespace)
   189  			argoCDDB := db.NewDB(namespace, argoSettingsMgr, k8sClient)
   190  
   191  			scmConfig := generators.NewSCMConfig(scmRootCAPath, allowedScmProviders, enableScmProviders, enableGitHubAPIMetrics, github_app.NewAuthCredentials(argoCDDB.(db.RepoCredsDB)), tokenRefStrictMode)
   192  
   193  			tlsConfig := apiclient.TLSConfiguration{
   194  				DisableTLS:       repoServerPlaintext,
   195  				StrictValidation: repoServerStrictTLS,
   196  			}
   197  
   198  			if !repoServerPlaintext && repoServerStrictTLS {
   199  				pool, err := tls.LoadX509CertPool(
   200  					env.StringFromEnv(common.EnvAppConfigPath, common.DefaultAppConfigPath)+"/reposerver/tls/tls.crt",
   201  					env.StringFromEnv(common.EnvAppConfigPath, common.DefaultAppConfigPath)+"/reposerver/tls/ca.crt",
   202  				)
   203  				errors.CheckError(err)
   204  				tlsConfig.Certificates = pool
   205  			}
   206  
   207  			repoClientset := apiclient.NewRepoServerClientset(argocdRepoServer, repoServerTimeoutSeconds, tlsConfig)
   208  			argoCDService := services.NewArgoCDService(argoCDDB, gitSubmoduleEnabled, repoClientset, enableNewGitFileGlobbing)
   209  
   210  			topLevelGenerators := generators.GetGenerators(ctx, mgr.GetClient(), k8sClient, namespace, argoCDService, dynamicClient, scmConfig)
   211  
   212  			// start a webhook server that listens to incoming webhook payloads
   213  			webhookHandler, err := webhook.NewWebhookHandler(webhookParallelism, argoSettingsMgr, mgr.GetClient(), topLevelGenerators)
   214  			if err != nil {
   215  				log.Error(err, "failed to create webhook handler")
   216  			}
   217  			if webhookHandler != nil {
   218  				startWebhookServer(webhookHandler, webhookAddr)
   219  			}
   220  
   221  			metrics := appsetmetrics.NewApplicationsetMetrics(
   222  				utils.NewAppsetLister(mgr.GetClient()),
   223  				metricsAplicationsetLabels,
   224  				func(appset *appv1alpha1.ApplicationSet) bool {
   225  					return utils.IsNamespaceAllowed(applicationSetNamespaces, appset.Namespace)
   226  				})
   227  
   228  			if err = (&controllers.ApplicationSetReconciler{
   229  				Generators:                 topLevelGenerators,
   230  				Client:                     mgr.GetClient(),
   231  				Scheme:                     mgr.GetScheme(),
   232  				Recorder:                   mgr.GetEventRecorderFor("applicationset-controller"),
   233  				Renderer:                   &utils.Render{},
   234  				Policy:                     policyObj,
   235  				EnablePolicyOverride:       enablePolicyOverride,
   236  				KubeClientset:              k8sClient,
   237  				ArgoDB:                     argoCDDB,
   238  				ArgoCDNamespace:            namespace,
   239  				ApplicationSetNamespaces:   applicationSetNamespaces,
   240  				EnableProgressiveSyncs:     enableProgressiveSyncs,
   241  				SCMRootCAPath:              scmRootCAPath,
   242  				GlobalPreservedAnnotations: globalPreservedAnnotations,
   243  				GlobalPreservedLabels:      globalPreservedLabels,
   244  				Metrics:                    &metrics,
   245  				MaxResourcesStatusCount:    maxResourcesStatusCount,
   246  			}).SetupWithManager(mgr, enableProgressiveSyncs, maxConcurrentReconciliations); err != nil {
   247  				log.Error(err, "unable to create controller", "controller", "ApplicationSet")
   248  				os.Exit(1)
   249  			}
   250  
   251  			stats.StartStatsTicker(10 * time.Minute)
   252  			log.Info("Starting manager")
   253  			if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
   254  				log.Error(err, "problem running manager")
   255  				os.Exit(1)
   256  			}
   257  			return nil
   258  		},
   259  	}
   260  	clientConfig = cli.AddKubectlFlagsToCmd(&command)
   261  	command.Flags().StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.")
   262  	command.Flags().StringVar(&probeBindAddr, "probe-addr", ":8081", "The address the probe endpoint binds to.")
   263  	command.Flags().StringVar(&webhookAddr, "webhook-addr", ":7000", "The address the webhook endpoint binds to.")
   264  	command.Flags().BoolVar(&enableLeaderElection, "enable-leader-election", env.ParseBoolFromEnv("ARGOCD_APPLICATIONSET_CONTROLLER_ENABLE_LEADER_ELECTION", false),
   265  		"Enable leader election for controller manager. "+
   266  			"Enabling this will ensure there is only one active controller manager.")
   267  	command.Flags().StringSliceVar(&applicationSetNamespaces, "applicationset-namespaces", env.StringsFromEnv("ARGOCD_APPLICATIONSET_CONTROLLER_NAMESPACES", []string{}, ","), "Argo CD applicationset namespaces")
   268  	command.Flags().StringVar(&argocdRepoServer, "argocd-repo-server", env.StringFromEnv("ARGOCD_APPLICATIONSET_CONTROLLER_REPO_SERVER", common.DefaultRepoServerAddr), "Argo CD repo server address")
   269  	command.Flags().StringVar(&policy, "policy", env.StringFromEnv("ARGOCD_APPLICATIONSET_CONTROLLER_POLICY", ""), "Modify how application is synced between the generator and the cluster. Default is '' (empty), which means AppSets default to 'sync', but they may override that default. Setting an explicit value prevents AppSet-level overrides, unless --allow-policy-override is enabled. Explicit options are: 'sync' (create & update & delete), 'create-only', 'create-update' (no deletion), 'create-delete' (no update)")
   270  	command.Flags().BoolVar(&enablePolicyOverride, "enable-policy-override", env.ParseBoolFromEnv("ARGOCD_APPLICATIONSET_CONTROLLER_ENABLE_POLICY_OVERRIDE", policy == ""), "For security reason if 'policy' is set, it is not possible to override it at applicationSet level. 'allow-policy-override' allows user to define their own policy")
   271  	command.Flags().BoolVar(&debugLog, "debug", env.ParseBoolFromEnv("ARGOCD_APPLICATIONSET_CONTROLLER_DEBUG", false), "Print debug logs. Takes precedence over loglevel")
   272  	command.Flags().StringVar(&cmdutil.LogFormat, "logformat", env.StringFromEnv("ARGOCD_APPLICATIONSET_CONTROLLER_LOGFORMAT", "json"), "Set the logging format. One of: json|text")
   273  	command.Flags().StringVar(&cmdutil.LogLevel, "loglevel", env.StringFromEnv("ARGOCD_APPLICATIONSET_CONTROLLER_LOGLEVEL", "info"), "Set the logging level. One of: debug|info|warn|error")
   274  	command.Flags().StringSliceVar(&allowedScmProviders, "allowed-scm-providers", env.StringsFromEnv("ARGOCD_APPLICATIONSET_CONTROLLER_ALLOWED_SCM_PROVIDERS", []string{}, ","), "The list of allowed custom SCM provider API URLs. This restriction does not apply to SCM or PR generators which do not accept a custom API URL. (Default: Empty = all)")
   275  	command.Flags().BoolVar(&enableScmProviders, "enable-scm-providers", env.ParseBoolFromEnv("ARGOCD_APPLICATIONSET_CONTROLLER_ENABLE_SCM_PROVIDERS", true), "Enable retrieving information from SCM providers, used by the SCM and PR generators (Default: true)")
   276  	command.Flags().BoolVar(&dryRun, "dry-run", env.ParseBoolFromEnv("ARGOCD_APPLICATIONSET_CONTROLLER_DRY_RUN", false), "Enable dry run mode")
   277  	command.Flags().BoolVar(&tokenRefStrictMode, "token-ref-strict-mode", env.ParseBoolFromEnv("ARGOCD_APPLICATIONSET_CONTROLLER_TOKENREF_STRICT_MODE", false), fmt.Sprintf("Set to true to require secrets referenced by SCM providers to have the %s=%s label set (Default: false)", common.LabelKeySecretType, common.LabelValueSecretTypeSCMCreds))
   278  	command.Flags().BoolVar(&enableProgressiveSyncs, "enable-progressive-syncs", env.ParseBoolFromEnv("ARGOCD_APPLICATIONSET_CONTROLLER_ENABLE_PROGRESSIVE_SYNCS", false), "Enable use of the experimental progressive syncs feature.")
   279  	command.Flags().BoolVar(&enableNewGitFileGlobbing, "enable-new-git-file-globbing", env.ParseBoolFromEnv("ARGOCD_APPLICATIONSET_CONTROLLER_ENABLE_NEW_GIT_FILE_GLOBBING", false), "Enable new globbing in Git files generator.")
   280  	command.Flags().BoolVar(&repoServerPlaintext, "repo-server-plaintext", env.ParseBoolFromEnv("ARGOCD_APPLICATIONSET_CONTROLLER_REPO_SERVER_PLAINTEXT", false), "Disable TLS on connections to repo server")
   281  	command.Flags().BoolVar(&repoServerStrictTLS, "repo-server-strict-tls", env.ParseBoolFromEnv("ARGOCD_APPLICATIONSET_CONTROLLER_REPO_SERVER_STRICT_TLS", false), "Whether to use strict validation of the TLS cert presented by the repo server")
   282  	command.Flags().IntVar(&repoServerTimeoutSeconds, "repo-server-timeout-seconds", env.ParseNumFromEnv("ARGOCD_APPLICATIONSET_CONTROLLER_REPO_SERVER_TIMEOUT_SECONDS", 60, 0, math.MaxInt64), "Repo server RPC call timeout seconds.")
   283  	command.Flags().IntVar(&maxConcurrentReconciliations, "concurrent-reconciliations", env.ParseNumFromEnv("ARGOCD_APPLICATIONSET_CONTROLLER_CONCURRENT_RECONCILIATIONS", 10, 1, math.MaxInt), "Max concurrent reconciliations limit for the controller")
   284  	command.Flags().StringVar(&scmRootCAPath, "scm-root-ca-path", env.StringFromEnv("ARGOCD_APPLICATIONSET_CONTROLLER_SCM_ROOT_CA_PATH", ""), "Provide Root CA Path for self-signed TLS Certificates")
   285  	command.Flags().StringSliceVar(&globalPreservedAnnotations, "preserved-annotations", env.StringsFromEnv("ARGOCD_APPLICATIONSET_CONTROLLER_GLOBAL_PRESERVED_ANNOTATIONS", []string{}, ","), "Sets global preserved field values for annotations")
   286  	command.Flags().StringSliceVar(&globalPreservedLabels, "preserved-labels", env.StringsFromEnv("ARGOCD_APPLICATIONSET_CONTROLLER_GLOBAL_PRESERVED_LABELS", []string{}, ","), "Sets global preserved field values for labels")
   287  	command.Flags().IntVar(&webhookParallelism, "webhook-parallelism-limit", env.ParseNumFromEnv("ARGOCD_APPLICATIONSET_CONTROLLER_WEBHOOK_PARALLELISM_LIMIT", 50, 1, 1000), "Number of webhook requests processed concurrently")
   288  	command.Flags().StringSliceVar(&metricsAplicationsetLabels, "metrics-applicationset-labels", []string{}, "List of Application labels that will be added to the argocd_applicationset_labels metric")
   289  	command.Flags().BoolVar(&enableGitHubAPIMetrics, "enable-github-api-metrics", env.ParseBoolFromEnv("ARGOCD_APPLICATIONSET_CONTROLLER_ENABLE_GITHUB_API_METRICS", false), "Enable GitHub API metrics for generators that use the GitHub API")
   290  	command.Flags().IntVar(&maxResourcesStatusCount, "max-resources-status-count", env.ParseNumFromEnv("ARGOCD_APPLICATIONSET_CONTROLLER_MAX_RESOURCES_STATUS_COUNT", 0, 0, math.MaxInt), "Max number of resources stored in appset status.")
   291  
   292  	return &command
   293  }
   294  
   295  func startWebhookServer(webhookHandler *webhook.WebhookHandler, webhookAddr string) {
   296  	mux := http.NewServeMux()
   297  	mux.HandleFunc("/api/webhook", webhookHandler.Handler)
   298  	go func() {
   299  		log.Infof("Starting webhook server %s", webhookAddr)
   300  		err := http.ListenAndServe(webhookAddr, mux)
   301  		if err != nil {
   302  			log.Error(err, "failed to start webhook server")
   303  			os.Exit(1)
   304  		}
   305  	}()
   306  }