github.com/argoproj/argo-cd/v3@v3.2.1/cmd/argocd/commands/admin/app.go (about)

     1  package admin
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	stderrors "errors"
     7  	"fmt"
     8  	"net/http"
     9  	"os"
    10  	"sort"
    11  	"time"
    12  
    13  	"github.com/argoproj/gitops-engine/pkg/health"
    14  	"github.com/argoproj/gitops-engine/pkg/utils/kube"
    15  	"github.com/spf13/cobra"
    16  	corev1 "k8s.io/api/core/v1"
    17  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    18  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    19  	"k8s.io/apimachinery/pkg/util/runtime"
    20  	"k8s.io/client-go/kubernetes"
    21  	kubecache "k8s.io/client-go/tools/cache"
    22  	"k8s.io/client-go/tools/clientcmd"
    23  	"sigs.k8s.io/yaml"
    24  
    25  	cmdutil "github.com/argoproj/argo-cd/v3/cmd/util"
    26  	"github.com/argoproj/argo-cd/v3/common"
    27  	"github.com/argoproj/argo-cd/v3/controller"
    28  	"github.com/argoproj/argo-cd/v3/controller/cache"
    29  	"github.com/argoproj/argo-cd/v3/controller/metrics"
    30  	"github.com/argoproj/argo-cd/v3/controller/sharding"
    31  	argocdclient "github.com/argoproj/argo-cd/v3/pkg/apiclient"
    32  	"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
    33  	appclientset "github.com/argoproj/argo-cd/v3/pkg/client/clientset/versioned"
    34  	appinformers "github.com/argoproj/argo-cd/v3/pkg/client/informers/externalversions"
    35  	reposerverclient "github.com/argoproj/argo-cd/v3/reposerver/apiclient"
    36  	"github.com/argoproj/argo-cd/v3/util/argo"
    37  	"github.com/argoproj/argo-cd/v3/util/argo/normalizers"
    38  	cacheutil "github.com/argoproj/argo-cd/v3/util/cache"
    39  	appstatecache "github.com/argoproj/argo-cd/v3/util/cache/appstate"
    40  	"github.com/argoproj/argo-cd/v3/util/cli"
    41  	"github.com/argoproj/argo-cd/v3/util/config"
    42  	"github.com/argoproj/argo-cd/v3/util/db"
    43  	"github.com/argoproj/argo-cd/v3/util/errors"
    44  	utilio "github.com/argoproj/argo-cd/v3/util/io"
    45  	kubeutil "github.com/argoproj/argo-cd/v3/util/kube"
    46  	"github.com/argoproj/argo-cd/v3/util/settings"
    47  )
    48  
    49  func NewAppCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
    50  	command := &cobra.Command{
    51  		Use:   "app",
    52  		Short: "Manage applications configuration",
    53  		Example: `
    54  # Compare results of two reconciliations and print diff
    55  argocd admin app diff-reconcile-results APPNAME [flags]
    56  
    57  # Generate declarative config for an application
    58  argocd admin app generate-spec APPNAME
    59  
    60  # Reconcile all applications and store reconciliation summary in the specified file
    61  argocd admin app get-reconcile-results APPNAME
    62  `,
    63  		Run: func(c *cobra.Command, args []string) {
    64  			c.HelpFunc()(c, args)
    65  		},
    66  	}
    67  
    68  	command.AddCommand(NewGenAppSpecCommand())
    69  	command.AddCommand(NewReconcileCommand(clientOpts))
    70  	command.AddCommand(NewDiffReconcileResults())
    71  	return command
    72  }
    73  
    74  // NewGenAppSpecCommand generates declarative configuration file for given application
    75  func NewGenAppSpecCommand() *cobra.Command {
    76  	var (
    77  		appOpts      cmdutil.AppOptions
    78  		fileURL      string
    79  		appName      string
    80  		labels       []string
    81  		outputFormat string
    82  		annotations  []string
    83  		inline       bool
    84  		setFinalizer bool
    85  	)
    86  	command := &cobra.Command{
    87  		Use:   "generate-spec APPNAME",
    88  		Short: "Generate declarative config for an application",
    89  		Example: `
    90  	# Generate declarative config for a directory app
    91  	argocd admin app generate-spec guestbook --repo https://github.com/argoproj/argocd-example-apps.git --path guestbook --dest-namespace default --dest-server https://kubernetes.default.svc --directory-recurse
    92  
    93  	# Generate declarative config for a Jsonnet app
    94  	argocd admin app generate-spec jsonnet-guestbook --repo https://github.com/argoproj/argocd-example-apps.git --path jsonnet-guestbook --dest-namespace default --dest-server https://kubernetes.default.svc --jsonnet-ext-str replicas=2
    95  
    96  	# Generate declarative config for a Helm app
    97  	argocd admin app generate-spec helm-guestbook --repo https://github.com/argoproj/argocd-example-apps.git --path helm-guestbook --dest-namespace default --dest-server https://kubernetes.default.svc --helm-set replicaCount=2
    98  
    99  	# Generate declarative config for a Helm app from a Helm repo
   100  	argocd admin app generate-spec nginx-ingress --repo https://charts.helm.sh/stable --helm-chart nginx-ingress --revision 1.24.3 --dest-namespace default --dest-server https://kubernetes.default.svc
   101  
   102  	# Generate declarative config for a Kustomize app
   103  	argocd admin app generate-spec kustomize-guestbook --repo https://github.com/argoproj/argocd-example-apps.git --path kustomize-guestbook --dest-namespace default --dest-server https://kubernetes.default.svc --kustomize-image quay.io/argoprojlabs/argocd-e2e-container:0.1
   104  
   105  	# Generate declarative config for a app using a custom tool:
   106  	argocd admin app generate-spec kasane --repo https://github.com/argoproj/argocd-example-apps.git --path plugins/kasane --dest-namespace default --dest-server https://kubernetes.default.svc --config-management-plugin kasane
   107  `,
   108  		Run: func(c *cobra.Command, args []string) {
   109  			apps, err := cmdutil.ConstructApps(fileURL, appName, labels, annotations, args, appOpts, c.Flags())
   110  			errors.CheckError(err)
   111  			if len(apps) > 1 {
   112  				errors.CheckError(stderrors.New("failed to generate spec, more than one application is not supported"))
   113  			}
   114  			app := apps[0]
   115  			if app.Name == "" {
   116  				c.HelpFunc()(c, args)
   117  				os.Exit(1)
   118  			}
   119  			if setFinalizer {
   120  				app.Finalizers = append(app.Finalizers, v1alpha1.ResourcesFinalizerName)
   121  			}
   122  			out, closer, err := getOutWriter(inline, fileURL)
   123  			errors.CheckError(err)
   124  			defer utilio.Close(closer)
   125  
   126  			errors.CheckError(PrintResources(outputFormat, out, app))
   127  		},
   128  	}
   129  	command.Flags().StringVar(&appName, "name", "", "A name for the app, ignored if a file is set (DEPRECATED)")
   130  	command.Flags().StringVarP(&fileURL, "file", "f", "", "Filename or URL to Kubernetes manifests for the app")
   131  	command.Flags().StringArrayVarP(&labels, "label", "l", []string{}, "Labels to apply to the app")
   132  	command.Flags().StringArrayVarP(&annotations, "annotations", "", []string{}, "Set metadata annotations (e.g. example=value)")
   133  	command.Flags().StringVarP(&outputFormat, "output", "o", "yaml", "Output format. One of: json|yaml")
   134  	command.Flags().BoolVarP(&inline, "inline", "i", false, "If set then generated resource is written back to the file specified in --file flag")
   135  	command.Flags().BoolVar(&setFinalizer, "set-finalizer", false, "Sets deletion finalizer on the application, application resources will be cascaded on deletion")
   136  
   137  	// Only complete files with appropriate extension.
   138  	err := command.Flags().SetAnnotation("file", cobra.BashCompFilenameExt, []string{"json", "yaml", "yml"})
   139  	errors.CheckError(err)
   140  
   141  	cmdutil.AddAppFlags(command, &appOpts)
   142  	return command
   143  }
   144  
   145  type appReconcileResult struct {
   146  	Name       string                          `json:"name"`
   147  	Health     health.HealthStatusCode         `json:"health"`
   148  	Sync       *v1alpha1.SyncStatus            `json:"sync"`
   149  	Conditions []v1alpha1.ApplicationCondition `json:"conditions"`
   150  }
   151  
   152  type reconcileResults struct {
   153  	Applications []appReconcileResult `json:"applications"`
   154  }
   155  
   156  func (r *reconcileResults) getAppsMap() map[string]appReconcileResult {
   157  	res := map[string]appReconcileResult{}
   158  	for i := range r.Applications {
   159  		res[r.Applications[i].Name] = r.Applications[i]
   160  	}
   161  	return res
   162  }
   163  
   164  func printLine(format string, a ...any) {
   165  	_, _ = fmt.Printf(format+"\n", a...)
   166  }
   167  
   168  func NewDiffReconcileResults() *cobra.Command {
   169  	command := &cobra.Command{
   170  		Use:   "diff-reconcile-results PATH1 PATH2",
   171  		Short: "Compare results of two reconciliations and print diff.",
   172  		Run: func(c *cobra.Command, args []string) {
   173  			if len(args) != 2 {
   174  				c.HelpFunc()(c, args)
   175  				os.Exit(1)
   176  			}
   177  
   178  			path1 := args[0]
   179  			path2 := args[1]
   180  			var res1 reconcileResults
   181  			var res2 reconcileResults
   182  			errors.CheckError(config.UnmarshalLocalFile(path1, &res1))
   183  			errors.CheckError(config.UnmarshalLocalFile(path2, &res2))
   184  			errors.CheckError(diffReconcileResults(res1, res2))
   185  		},
   186  	}
   187  
   188  	return command
   189  }
   190  
   191  func toUnstructured(val any) (*unstructured.Unstructured, error) {
   192  	data, err := json.Marshal(val)
   193  	if err != nil {
   194  		return nil, fmt.Errorf("error while marhsalling value: %w", err)
   195  	}
   196  	res := make(map[string]any)
   197  	err = json.Unmarshal(data, &res)
   198  	if err != nil {
   199  		return nil, fmt.Errorf("error while unmarhsalling data: %w", err)
   200  	}
   201  	return &unstructured.Unstructured{Object: res}, nil
   202  }
   203  
   204  type diffPair struct {
   205  	name   string
   206  	first  *unstructured.Unstructured
   207  	second *unstructured.Unstructured
   208  }
   209  
   210  func diffReconcileResults(res1 reconcileResults, res2 reconcileResults) error {
   211  	var pairs []diffPair
   212  	resMap1 := res1.getAppsMap()
   213  	resMap2 := res2.getAppsMap()
   214  	for k, v := range resMap1 {
   215  		firstUn, err := toUnstructured(v)
   216  		if err != nil {
   217  			return fmt.Errorf("error converting first resource to unstructured: %w", err)
   218  		}
   219  		var secondUn *unstructured.Unstructured
   220  		second, ok := resMap2[k]
   221  		if ok {
   222  			secondUn, err = toUnstructured(second)
   223  			if err != nil {
   224  				return fmt.Errorf("error converting second resource to unstructured: %w", err)
   225  			}
   226  			delete(resMap2, k)
   227  		}
   228  		pairs = append(pairs, diffPair{name: k, first: firstUn, second: secondUn})
   229  	}
   230  	for k, v := range resMap2 {
   231  		secondUn, err := toUnstructured(v)
   232  		if err != nil {
   233  			return fmt.Errorf("error converting second resource of second map to unstructure: %w", err)
   234  		}
   235  		pairs = append(pairs, diffPair{name: k, first: nil, second: secondUn})
   236  	}
   237  	sort.Slice(pairs, func(i, j int) bool {
   238  		return pairs[i].name < pairs[j].name
   239  	})
   240  	for _, item := range pairs {
   241  		printLine(item.name)
   242  		_ = cli.PrintDiff(item.name, item.first, item.second)
   243  	}
   244  
   245  	return nil
   246  }
   247  
   248  func NewReconcileCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
   249  	var (
   250  		clientConfig         clientcmd.ClientConfig
   251  		selector             string
   252  		repoServerAddress    string
   253  		outputFormat         string
   254  		refresh              bool
   255  		serverSideDiff       bool
   256  		ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts
   257  	)
   258  
   259  	command := &cobra.Command{
   260  		Use:   "get-reconcile-results PATH",
   261  		Short: "Reconcile all applications and stores reconciliation summary in the specified file.",
   262  		Run: func(c *cobra.Command, args []string) {
   263  			ctx := c.Context()
   264  
   265  			// get rid of logging error handler
   266  			runtime.ErrorHandlers = runtime.ErrorHandlers[1:]
   267  
   268  			if len(args) != 1 {
   269  				c.HelpFunc()(c, args)
   270  				os.Exit(1)
   271  			}
   272  			outputPath := args[0]
   273  
   274  			errors.CheckError(os.Setenv(v1alpha1.EnvVarFakeInClusterConfig, "true"))
   275  			cfg, err := clientConfig.ClientConfig()
   276  			errors.CheckError(err)
   277  			namespace, _, err := clientConfig.Namespace()
   278  			errors.CheckError(err)
   279  
   280  			var result []appReconcileResult
   281  			if refresh {
   282  				appClientset := appclientset.NewForConfigOrDie(cfg)
   283  				kubeClientset := kubernetes.NewForConfigOrDie(cfg)
   284  				if repoServerAddress == "" {
   285  					printLine("Repo server is not provided, trying to port-forward to argocd-repo-server pod.")
   286  					overrides := clientcmd.ConfigOverrides{}
   287  					repoServerName := clientOpts.RepoServerName
   288  					repoServerServiceLabelSelector := common.LabelKeyComponentRepoServer + "=" + common.LabelValueComponentRepoServer
   289  					repoServerServices, err := kubeClientset.CoreV1().Services(namespace).List(context.Background(), metav1.ListOptions{LabelSelector: repoServerServiceLabelSelector})
   290  					errors.CheckError(err)
   291  					if len(repoServerServices.Items) > 0 {
   292  						if repoServerServicelabel, ok := repoServerServices.Items[0].Labels[common.LabelKeyAppName]; ok && repoServerServicelabel != "" {
   293  							repoServerName = repoServerServicelabel
   294  						}
   295  					}
   296  					repoServerPodLabelSelector := common.LabelKeyAppName + "=" + repoServerName
   297  					repoServerPort, err := kubeutil.PortForward(8081, namespace, &overrides, repoServerPodLabelSelector)
   298  					errors.CheckError(err)
   299  					repoServerAddress = fmt.Sprintf("localhost:%d", repoServerPort)
   300  				}
   301  				repoServerClient := reposerverclient.NewRepoServerClientset(repoServerAddress, 60, reposerverclient.TLSConfiguration{DisableTLS: false, StrictValidation: false})
   302  				result, err = reconcileApplications(ctx, kubeClientset, appClientset, namespace, repoServerClient, selector, newLiveStateCache, serverSideDiff, ignoreNormalizerOpts)
   303  				errors.CheckError(err)
   304  			} else {
   305  				appClientset := appclientset.NewForConfigOrDie(cfg)
   306  				result, err = getReconcileResults(ctx, appClientset, namespace, selector)
   307  			}
   308  
   309  			errors.CheckError(saveToFile(err, outputFormat, reconcileResults{Applications: result}, outputPath))
   310  		},
   311  	}
   312  	clientConfig = cli.AddKubectlFlagsToCmd(command)
   313  	command.Flags().StringVar(&repoServerAddress, "repo-server", "", "Repo server address.")
   314  	command.Flags().StringVar(&selector, "l", "", "Label selector")
   315  	command.Flags().StringVar(&outputFormat, "o", "yaml", "Output format (yaml|json)")
   316  	command.Flags().BoolVar(&refresh, "refresh", false, "If set to true then recalculates apps reconciliation")
   317  	command.Flags().BoolVar(&serverSideDiff, "server-side-diff", false, "If set to \"true\" will use server-side diff while comparing resources. Default (\"false\")")
   318  	command.Flags().DurationVar(&ignoreNormalizerOpts.JQExecutionTimeout, "ignore-normalizer-jq-execution-timeout", normalizers.DefaultJQExecutionTimeout, "Set ignore normalizer JQ execution timeout")
   319  	return command
   320  }
   321  
   322  func saveToFile(err error, outputFormat string, result reconcileResults, outputPath string) error {
   323  	errors.CheckError(err)
   324  	var data []byte
   325  	switch outputFormat {
   326  	case "yaml":
   327  		if data, err = yaml.Marshal(result); err != nil {
   328  			return fmt.Errorf("error marshalling yaml: %w", err)
   329  		}
   330  	case "json":
   331  		if data, err = json.Marshal(result); err != nil {
   332  			return fmt.Errorf("error marshalling json: %w", err)
   333  		}
   334  	default:
   335  		return fmt.Errorf("format %s is not supported", outputFormat)
   336  	}
   337  
   338  	return os.WriteFile(outputPath, data, 0o644)
   339  }
   340  
   341  func getReconcileResults(ctx context.Context, appClientset appclientset.Interface, namespace string, selector string) ([]appReconcileResult, error) {
   342  	appsList, err := appClientset.ArgoprojV1alpha1().Applications(namespace).List(ctx, metav1.ListOptions{LabelSelector: selector})
   343  	if err != nil {
   344  		return nil, fmt.Errorf("error listing namespaced apps: %w", err)
   345  	}
   346  
   347  	var items []appReconcileResult
   348  	for _, app := range appsList.Items {
   349  		items = append(items, appReconcileResult{
   350  			Name:       app.Name,
   351  			Conditions: app.Status.Conditions,
   352  			Health:     app.Status.Health.Status,
   353  			Sync:       &app.Status.Sync,
   354  		})
   355  	}
   356  	return items, nil
   357  }
   358  
   359  func reconcileApplications(
   360  	ctx context.Context,
   361  	kubeClientset kubernetes.Interface,
   362  	appClientset appclientset.Interface,
   363  	namespace string,
   364  	repoServerClient reposerverclient.Clientset,
   365  	selector string,
   366  	createLiveStateCache func(argoDB db.ArgoDB, appInformer kubecache.SharedIndexInformer, settingsMgr *settings.SettingsManager, server *metrics.MetricsServer) cache.LiveStateCache,
   367  	serverSideDiff bool,
   368  	ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts,
   369  ) ([]appReconcileResult, error) {
   370  	settingsMgr := settings.NewSettingsManager(ctx, kubeClientset, namespace)
   371  	argoDB := db.NewDB(namespace, settingsMgr, kubeClientset)
   372  	appInformerFactory := appinformers.NewSharedInformerFactoryWithOptions(
   373  		appClientset,
   374  		1*time.Hour,
   375  		appinformers.WithNamespace(namespace),
   376  		appinformers.WithTweakListOptions(func(_ *metav1.ListOptions) {}),
   377  	)
   378  
   379  	appInformer := appInformerFactory.Argoproj().V1alpha1().Applications().Informer()
   380  	projInformer := appInformerFactory.Argoproj().V1alpha1().AppProjects().Informer()
   381  	go appInformer.Run(ctx.Done())
   382  	go projInformer.Run(ctx.Done())
   383  	if !kubecache.WaitForCacheSync(ctx.Done(), appInformer.HasSynced, projInformer.HasSynced) {
   384  		return nil, stderrors.New("failed to sync cache")
   385  	}
   386  
   387  	appLister := appInformerFactory.Argoproj().V1alpha1().Applications().Lister()
   388  	projLister := appInformerFactory.Argoproj().V1alpha1().AppProjects().Lister()
   389  	server, err := metrics.NewMetricsServer("", appLister, func(_ any) bool {
   390  		return true
   391  	}, func(_ *http.Request) error {
   392  		return nil
   393  	}, []string{}, []string{}, argoDB)
   394  	if err != nil {
   395  		return nil, fmt.Errorf("error starting new metrics server: %w", err)
   396  	}
   397  	stateCache := createLiveStateCache(argoDB, appInformer, settingsMgr, server)
   398  	if err := stateCache.Init(); err != nil {
   399  		return nil, fmt.Errorf("error initializing state cache: %w", err)
   400  	}
   401  
   402  	cache := appstatecache.NewCache(
   403  		cacheutil.NewCache(cacheutil.NewInMemoryCache(1*time.Minute)),
   404  		1*time.Minute,
   405  	)
   406  
   407  	appStateManager := controller.NewAppStateManager(
   408  		argoDB,
   409  		appClientset,
   410  		repoServerClient,
   411  		namespace,
   412  		kubeutil.NewKubectl(),
   413  		func(_ string) (kube.CleanupFunc, error) {
   414  			return func() {}, nil
   415  		},
   416  		settingsMgr,
   417  		stateCache,
   418  		server,
   419  		cache,
   420  		time.Second,
   421  		argo.NewResourceTracking(),
   422  		false,
   423  		0,
   424  		serverSideDiff,
   425  		ignoreNormalizerOpts,
   426  	)
   427  
   428  	appsList, err := appClientset.ArgoprojV1alpha1().Applications(namespace).List(ctx, metav1.ListOptions{LabelSelector: selector})
   429  	if err != nil {
   430  		return nil, fmt.Errorf("error listing namespaced apps: %w", err)
   431  	}
   432  
   433  	sort.Slice(appsList.Items, func(i, j int) bool {
   434  		return appsList.Items[i].Spec.Destination.Server < appsList.Items[j].Spec.Destination.Server
   435  	})
   436  
   437  	var items []appReconcileResult
   438  	prevServer := ""
   439  	for _, app := range appsList.Items {
   440  		destCluster, err := argo.GetDestinationCluster(ctx, app.Spec.Destination, argoDB)
   441  		if err != nil {
   442  			return nil, fmt.Errorf("error getting destination cluster: %w", err)
   443  		}
   444  
   445  		if prevServer != destCluster.Server {
   446  			if prevServer != "" {
   447  				if clusterCache, err := stateCache.GetClusterCache(destCluster); err == nil {
   448  					clusterCache.Invalidate()
   449  				}
   450  			}
   451  			printLine("Reconciling apps of %s", destCluster.Server)
   452  			prevServer = destCluster.Server
   453  		}
   454  		printLine(app.Name)
   455  
   456  		proj, err := projLister.AppProjects(namespace).Get(app.Spec.Project)
   457  		if err != nil {
   458  			return nil, fmt.Errorf("error getting namespaced project: %w", err)
   459  		}
   460  
   461  		sources := make([]v1alpha1.ApplicationSource, 0)
   462  		revisions := make([]string, 0)
   463  		sources = append(sources, app.Spec.GetSource())
   464  		revisions = append(revisions, app.Spec.GetSource().TargetRevision)
   465  
   466  		res, err := appStateManager.CompareAppState(&app, proj, revisions, sources, false, false, nil, false)
   467  		if err != nil {
   468  			return nil, fmt.Errorf("error comparing app states: %w", err)
   469  		}
   470  		items = append(items, appReconcileResult{
   471  			Name:       app.Name,
   472  			Conditions: app.Status.Conditions,
   473  			Health:     res.GetHealthStatus(),
   474  			Sync:       res.GetSyncStatus(),
   475  		})
   476  	}
   477  	return items, nil
   478  }
   479  
   480  func newLiveStateCache(argoDB db.ArgoDB, appInformer kubecache.SharedIndexInformer, settingsMgr *settings.SettingsManager, server *metrics.MetricsServer) cache.LiveStateCache {
   481  	return cache.NewLiveStateCache(argoDB, appInformer, settingsMgr, server, func(_ map[string]bool, _ corev1.ObjectReference) {}, &sharding.ClusterSharding{}, argo.NewResourceTracking())
   482  }