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

     1  package commands
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"os"
     7  	"reflect"
     8  	"text/tabwriter"
     9  
    10  	"github.com/mattn/go-isatty"
    11  	"github.com/spf13/cobra"
    12  	"google.golang.org/grpc/codes"
    13  
    14  	"github.com/argoproj/argo-cd/v3/cmd/argocd/commands/admin"
    15  	"github.com/argoproj/argo-cd/v3/cmd/argocd/commands/headless"
    16  	"github.com/argoproj/argo-cd/v3/cmd/argocd/commands/utils"
    17  	cmdutil "github.com/argoproj/argo-cd/v3/cmd/util"
    18  	argocdclient "github.com/argoproj/argo-cd/v3/pkg/apiclient"
    19  	"github.com/argoproj/argo-cd/v3/pkg/apiclient/applicationset"
    20  	arogappsetv1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
    21  	"github.com/argoproj/argo-cd/v3/util/argo"
    22  	"github.com/argoproj/argo-cd/v3/util/errors"
    23  	"github.com/argoproj/argo-cd/v3/util/grpc"
    24  	utilio "github.com/argoproj/argo-cd/v3/util/io"
    25  	"github.com/argoproj/argo-cd/v3/util/templates"
    26  )
    27  
    28  var appSetExample = templates.Examples(`
    29  	# Get an ApplicationSet.
    30  	argocd appset get APPSETNAME
    31  
    32  	# List all the ApplicationSets
    33  	argocd appset list
    34  
    35  	# Create an ApplicationSet from a YAML stored in a file or at given URL
    36  	argocd appset create <filename or URL> (<filename or URL>...)
    37  
    38  	# Delete an ApplicationSet
    39  	argocd appset delete APPSETNAME (APPSETNAME...)
    40  	`)
    41  
    42  // NewAppSetCommand returns a new instance of an `argocd appset` command
    43  func NewAppSetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
    44  	command := &cobra.Command{
    45  		Use:     "appset",
    46  		Short:   "Manage ApplicationSets",
    47  		Example: appSetExample,
    48  		Run: func(c *cobra.Command, args []string) {
    49  			c.HelpFunc()(c, args)
    50  			os.Exit(1)
    51  		},
    52  	}
    53  	command.AddCommand(NewApplicationSetGetCommand(clientOpts))
    54  	command.AddCommand(NewApplicationSetCreateCommand(clientOpts))
    55  	command.AddCommand(NewApplicationSetListCommand(clientOpts))
    56  	command.AddCommand(NewApplicationSetDeleteCommand(clientOpts))
    57  	command.AddCommand(NewApplicationSetGenerateCommand(clientOpts))
    58  	return command
    59  }
    60  
    61  // NewApplicationSetGetCommand returns a new instance of an `argocd appset get` command
    62  func NewApplicationSetGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
    63  	var (
    64  		output     string
    65  		showParams bool
    66  	)
    67  	command := &cobra.Command{
    68  		Use:   "get APPSETNAME",
    69  		Short: "Get ApplicationSet details",
    70  		Example: templates.Examples(`
    71  	# Get ApplicationSets
    72  	argocd appset get APPSETNAME
    73  		`),
    74  		Run: func(c *cobra.Command, args []string) {
    75  			ctx := c.Context()
    76  
    77  			if len(args) == 0 {
    78  				c.HelpFunc()(c, args)
    79  				os.Exit(1)
    80  			}
    81  			acdClient := headless.NewClientOrDie(clientOpts, c)
    82  			conn, appIf := acdClient.NewApplicationSetClientOrDie()
    83  			defer utilio.Close(conn)
    84  
    85  			appSetName, appSetNs := argo.ParseFromQualifiedName(args[0], "")
    86  
    87  			appSet, err := appIf.Get(ctx, &applicationset.ApplicationSetGetQuery{Name: appSetName, AppsetNamespace: appSetNs})
    88  			errors.CheckError(err)
    89  
    90  			switch output {
    91  			case "yaml", "json":
    92  				err := PrintResource(appSet, output)
    93  				errors.CheckError(err)
    94  			case "wide", "":
    95  				printAppSetSummaryTable(appSet)
    96  				if len(appSet.Status.Conditions) > 0 {
    97  					fmt.Println()
    98  					w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
    99  					printAppSetConditions(w, appSet)
   100  					_ = w.Flush()
   101  					fmt.Println()
   102  				}
   103  				if showParams {
   104  					printHelmParams(appSet.Spec.Template.Spec.GetSource().Helm)
   105  				}
   106  			default:
   107  				errors.CheckError(fmt.Errorf("unknown output format: %s", output))
   108  			}
   109  		},
   110  	}
   111  	command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide")
   112  	command.Flags().BoolVar(&showParams, "show-params", false, "Show ApplicationSet parameters and overrides")
   113  	return command
   114  }
   115  
   116  // NewApplicationSetCreateCommand returns a new instance of an `argocd appset create` command
   117  func NewApplicationSetCreateCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
   118  	var output string
   119  	var upsert, dryRun bool
   120  	command := &cobra.Command{
   121  		Use:   "create",
   122  		Short: "Create one or more ApplicationSets",
   123  		Example: templates.Examples(`
   124  	# Create ApplicationSets
   125  	argocd appset create <filename or URL> (<filename or URL>...)
   126  
   127  	# Dry-run AppSet creation to see what applications would be managed
   128  	argocd appset create --dry-run <filename or URL> -o json | jq -r '.status.resources[].name' 
   129  		`),
   130  		Run: func(c *cobra.Command, args []string) {
   131  			ctx := c.Context()
   132  
   133  			if len(args) == 0 {
   134  				c.HelpFunc()(c, args)
   135  				os.Exit(1)
   136  			}
   137  			argocdClient := headless.NewClientOrDie(clientOpts, c)
   138  			fileURL := args[0]
   139  			appsets, err := cmdutil.ConstructApplicationSet(fileURL)
   140  			errors.CheckError(err)
   141  
   142  			if len(appsets) == 0 {
   143  				fmt.Printf("No ApplicationSets found while parsing the input file")
   144  				os.Exit(1)
   145  			}
   146  
   147  			for _, appset := range appsets {
   148  				if appset.Name == "" {
   149  					errors.Fatal(errors.ErrorGeneric, fmt.Sprintf("Error creating ApplicationSet %s. ApplicationSet does not have Name field set", appset))
   150  				}
   151  
   152  				conn, appIf := argocdClient.NewApplicationSetClientOrDie()
   153  				defer utilio.Close(conn)
   154  
   155  				// Get app before creating to see if it is being updated or no change
   156  				existing, err := appIf.Get(ctx, &applicationset.ApplicationSetGetQuery{Name: appset.Name, AppsetNamespace: appset.Namespace})
   157  				if grpc.UnwrapGRPCStatus(err).Code() != codes.NotFound {
   158  					errors.CheckError(err)
   159  				}
   160  
   161  				appSetCreateRequest := applicationset.ApplicationSetCreateRequest{
   162  					Applicationset: appset,
   163  					Upsert:         upsert,
   164  					DryRun:         dryRun,
   165  				}
   166  				created, err := appIf.Create(ctx, &appSetCreateRequest)
   167  				errors.CheckError(err)
   168  
   169  				dryRunMsg := ""
   170  				if dryRun {
   171  					dryRunMsg = " (dry-run)"
   172  				}
   173  
   174  				var action string
   175  				switch {
   176  				case existing == nil:
   177  					action = "created"
   178  				case !hasAppSetChanged(existing, created, upsert):
   179  					action = "unchanged"
   180  				default:
   181  					action = "updated"
   182  				}
   183  
   184  				c.PrintErrf("ApplicationSet '%s' %s%s\n", created.Name, action, dryRunMsg)
   185  
   186  				switch output {
   187  				case "yaml", "json":
   188  					err := PrintResource(created, output)
   189  					errors.CheckError(err)
   190  				case "wide", "":
   191  					printAppSetSummaryTable(created)
   192  
   193  					if len(created.Status.Conditions) > 0 {
   194  						fmt.Println()
   195  						w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
   196  						printAppSetConditions(w, created)
   197  						_ = w.Flush()
   198  						fmt.Println()
   199  					}
   200  				default:
   201  					errors.CheckError(fmt.Errorf("unknown output format: %s", output))
   202  				}
   203  			}
   204  		},
   205  	}
   206  	command.Flags().BoolVar(&upsert, "upsert", false, "Allows to override ApplicationSet with the same name even if supplied ApplicationSet spec is different from existing spec")
   207  	command.Flags().BoolVar(&dryRun, "dry-run", false, "Allows to evaluate the ApplicationSet template on the server to get a preview of the applications that would be created")
   208  	command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide")
   209  	return command
   210  }
   211  
   212  // NewApplicationSetGenerateCommand returns a new instance of an `argocd appset generate` command
   213  func NewApplicationSetGenerateCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
   214  	var output string
   215  	command := &cobra.Command{
   216  		Use:   "generate",
   217  		Short: "Generate apps of ApplicationSet rendered templates",
   218  		Example: templates.Examples(`
   219  	# Generate apps of ApplicationSet rendered templates
   220  	argocd appset generate <filename or URL> (<filename or URL>...)
   221  `),
   222  		Run: func(c *cobra.Command, args []string) {
   223  			ctx := c.Context()
   224  
   225  			if len(args) == 0 {
   226  				c.HelpFunc()(c, args)
   227  				os.Exit(1)
   228  			}
   229  			argocdClient := headless.NewClientOrDie(clientOpts, c)
   230  			fileURL := args[0]
   231  			appsets, err := cmdutil.ConstructApplicationSet(fileURL)
   232  			errors.CheckError(err)
   233  
   234  			if len(appsets) != 1 {
   235  				fmt.Printf("Input file must contain one ApplicationSet")
   236  				os.Exit(1)
   237  			}
   238  			appset := appsets[0]
   239  			if appset.Name == "" {
   240  				errors.Fatal(errors.ErrorGeneric, fmt.Sprintf("Error generating apps for ApplicationSet %s. ApplicationSet does not have Name field set", appset))
   241  			}
   242  
   243  			conn, appIf := argocdClient.NewApplicationSetClientOrDie()
   244  			defer utilio.Close(conn)
   245  
   246  			req := applicationset.ApplicationSetGenerateRequest{
   247  				ApplicationSet: appset,
   248  			}
   249  			resp, err := appIf.Generate(ctx, &req)
   250  			errors.CheckError(err)
   251  
   252  			var appsList []arogappsetv1.Application
   253  			for i := range resp.Applications {
   254  				appsList = append(appsList, *resp.Applications[i])
   255  			}
   256  
   257  			switch output {
   258  			case "yaml", "json":
   259  				var resources []any
   260  				for i := range appsList {
   261  					app := appsList[i]
   262  					// backfill api version and kind because k8s client always return empty values for these fields
   263  					app.APIVersion = arogappsetv1.ApplicationSchemaGroupVersionKind.GroupVersion().String()
   264  					app.Kind = arogappsetv1.ApplicationSchemaGroupVersionKind.Kind
   265  					resources = append(resources, app)
   266  				}
   267  
   268  				cobra.CheckErr(admin.PrintResources(output, os.Stdout, resources...))
   269  			case "wide", "":
   270  				printApplicationTable(appsList, &output)
   271  			default:
   272  				errors.CheckError(fmt.Errorf("unknown output format: %s", output))
   273  			}
   274  		},
   275  	}
   276  	command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide")
   277  	return command
   278  }
   279  
   280  // NewApplicationSetListCommand returns a new instance of an `argocd appset list` command
   281  func NewApplicationSetListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
   282  	var (
   283  		output          string
   284  		selector        string
   285  		projects        []string
   286  		appSetNamespace string
   287  	)
   288  	command := &cobra.Command{
   289  		Use:   "list",
   290  		Short: "List ApplicationSets",
   291  		Example: templates.Examples(`
   292  	# List all ApplicationSets
   293  	argocd appset list
   294  		`),
   295  		Run: func(c *cobra.Command, _ []string) {
   296  			ctx := c.Context()
   297  
   298  			conn, appIf := headless.NewClientOrDie(clientOpts, c).NewApplicationSetClientOrDie()
   299  			defer utilio.Close(conn)
   300  			appsets, err := appIf.List(ctx, &applicationset.ApplicationSetListQuery{Selector: selector, Projects: projects, AppsetNamespace: appSetNamespace})
   301  			errors.CheckError(err)
   302  
   303  			appsetList := appsets.Items
   304  
   305  			switch output {
   306  			case "yaml", "json":
   307  				err := PrintResourceList(appsetList, output, false)
   308  				errors.CheckError(err)
   309  			case "name":
   310  				printApplicationSetNames(appsetList)
   311  			case "wide", "":
   312  				printApplicationSetTable(appsetList, &output)
   313  			default:
   314  				errors.CheckError(fmt.Errorf("unknown output format: %s", output))
   315  			}
   316  		},
   317  	}
   318  	command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: wide|name|json|yaml")
   319  	command.Flags().StringVarP(&selector, "selector", "l", "", "List applicationsets by label")
   320  	command.Flags().StringArrayVarP(&projects, "project", "p", []string{}, "Filter by project name")
   321  	command.Flags().StringVarP(&appSetNamespace, "appset-namespace", "N", "", "Only list applicationsets in namespace")
   322  
   323  	return command
   324  }
   325  
   326  // NewApplicationSetDeleteCommand returns a new instance of an `argocd appset delete` command
   327  func NewApplicationSetDeleteCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
   328  	var noPrompt bool
   329  	command := &cobra.Command{
   330  		Use:   "delete",
   331  		Short: "Delete one or more ApplicationSets",
   332  		Example: templates.Examples(`
   333  	# Delete an applicationset
   334  	argocd appset delete APPSETNAME (APPSETNAME...)
   335  		`),
   336  		Run: func(c *cobra.Command, args []string) {
   337  			ctx := c.Context()
   338  
   339  			if len(args) == 0 {
   340  				c.HelpFunc()(c, args)
   341  				os.Exit(1)
   342  			}
   343  			conn, appIf := headless.NewClientOrDie(clientOpts, c).NewApplicationSetClientOrDie()
   344  			defer utilio.Close(conn)
   345  			isTerminal := isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd())
   346  			numOfApps := len(args)
   347  			promptFlag := c.Flag("yes")
   348  			if promptFlag.Changed && promptFlag.Value.String() == "true" {
   349  				noPrompt = true
   350  			}
   351  
   352  			var (
   353  				confirmAll = false
   354  				confirm    = false
   355  			)
   356  
   357  			// This is for backward compatibility,
   358  			// before we showed the prompts only when condition isTerminal && !noPrompt is true
   359  			promptUtil := utils.NewPrompt(isTerminal && !noPrompt)
   360  
   361  			for _, appSetQualifiedName := range args {
   362  				appSetName, appSetNs := argo.ParseFromQualifiedName(appSetQualifiedName, "")
   363  
   364  				appsetDeleteReq := applicationset.ApplicationSetDeleteRequest{
   365  					Name:            appSetName,
   366  					AppsetNamespace: appSetNs,
   367  				}
   368  				messageForSingle := "Are you sure you want to delete '" + appSetQualifiedName + "' and all its Applications? [y/n] "
   369  				messageForAll := "Are you sure you want to delete '" + appSetQualifiedName + "' and all its Applications? [y/n/a] where 'a' is to delete all specified ApplicationSets and their Applications without prompting"
   370  				if !confirmAll {
   371  					confirm, confirmAll = promptUtil.ConfirmBaseOnCount(messageForSingle, messageForAll, numOfApps)
   372  				}
   373  				if confirm || confirmAll {
   374  					_, err := appIf.Delete(ctx, &appsetDeleteReq)
   375  					errors.CheckError(err)
   376  					fmt.Printf("applicationset '%s' deleted\n", appSetQualifiedName)
   377  				} else {
   378  					fmt.Println("The command to delete '" + appSetQualifiedName + "' was cancelled.")
   379  				}
   380  			}
   381  		},
   382  	}
   383  	command.Flags().BoolVarP(&noPrompt, "yes", "y", false, "Turn off prompting to confirm cascaded deletion of Application resources")
   384  	return command
   385  }
   386  
   387  // Print simple list of application names
   388  func printApplicationSetNames(apps []arogappsetv1.ApplicationSet) {
   389  	for _, app := range apps {
   390  		fmt.Println(app.QualifiedName())
   391  	}
   392  }
   393  
   394  // Print table of application data
   395  func printApplicationSetTable(apps []arogappsetv1.ApplicationSet, output *string) {
   396  	w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
   397  	var fmtStr string
   398  	headers := []any{"NAME", "PROJECT", "SYNCPOLICY", "CONDITIONS"}
   399  	if *output == "wide" {
   400  		fmtStr = "%s\t%s\t%s\t%s\t%s\t%s\t%s\n"
   401  		headers = append(headers, "REPO", "PATH", "TARGET")
   402  	} else {
   403  		fmtStr = "%s\t%s\t%s\t%s\n"
   404  	}
   405  	_, _ = fmt.Fprintf(w, fmtStr, headers...)
   406  	for _, app := range apps {
   407  		conditions := make([]arogappsetv1.ApplicationSetCondition, 0)
   408  		for _, condition := range app.Status.Conditions {
   409  			if condition.Status == arogappsetv1.ApplicationSetConditionStatusTrue {
   410  				conditions = append(conditions, condition)
   411  			}
   412  		}
   413  		vals := []any{
   414  			app.QualifiedName(),
   415  			app.Spec.Template.Spec.Project,
   416  			app.Spec.SyncPolicy,
   417  			conditions,
   418  		}
   419  		if *output == "wide" {
   420  			vals = append(vals, app.Spec.Template.Spec.GetSource().RepoURL, app.Spec.Template.Spec.GetSource().Path, app.Spec.Template.Spec.GetSource().TargetRevision)
   421  		}
   422  		_, _ = fmt.Fprintf(w, fmtStr, vals...)
   423  	}
   424  	_ = w.Flush()
   425  }
   426  
   427  func getServerForAppSet(appSet *arogappsetv1.ApplicationSet) string {
   428  	if appSet.Spec.Template.Spec.Destination.Server == "" {
   429  		return appSet.Spec.Template.Spec.Destination.Name
   430  	}
   431  
   432  	return appSet.Spec.Template.Spec.Destination.Server
   433  }
   434  
   435  func printAppSetSummaryTable(appSet *arogappsetv1.ApplicationSet) {
   436  	fmt.Printf(printOpFmtStr, "Name:", appSet.QualifiedName())
   437  	fmt.Printf(printOpFmtStr, "Project:", appSet.Spec.Template.Spec.GetProject())
   438  	fmt.Printf(printOpFmtStr, "Server:", getServerForAppSet(appSet))
   439  	fmt.Printf(printOpFmtStr, "Namespace:", appSet.Spec.Template.Spec.Destination.Namespace)
   440  	if !appSet.Spec.Template.Spec.HasMultipleSources() {
   441  		fmt.Println("Source:")
   442  	} else {
   443  		fmt.Println("Sources:")
   444  	}
   445  
   446  	// if no source has been defined, print the default value for a source
   447  	if len(appSet.Spec.Template.Spec.GetSources()) == 0 {
   448  		src := appSet.Spec.Template.Spec.GetSource()
   449  		printAppSourceDetails(&src)
   450  	} else {
   451  		// otherwise range over the sources and print each source details
   452  		for _, source := range appSet.Spec.Template.Spec.GetSources() {
   453  			printAppSourceDetails(&source)
   454  		}
   455  	}
   456  
   457  	var (
   458  		syncPolicyStr string
   459  		syncPolicy    = appSet.Spec.Template.Spec.SyncPolicy
   460  	)
   461  	if syncPolicy != nil && syncPolicy.IsAutomatedSyncEnabled() {
   462  		syncPolicyStr = "Automated"
   463  		if syncPolicy.Automated.Prune {
   464  			syncPolicyStr += " (Prune)"
   465  		}
   466  	} else {
   467  		syncPolicyStr = "<none>"
   468  	}
   469  	fmt.Printf(printOpFmtStr, "SyncPolicy:", syncPolicyStr)
   470  }
   471  
   472  func printAppSetConditions(w io.Writer, appSet *arogappsetv1.ApplicationSet) {
   473  	_, _ = fmt.Fprintf(w, "CONDITION\tSTATUS\tMESSAGE\tLAST TRANSITION\n")
   474  	for _, item := range appSet.Status.Conditions {
   475  		_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", item.Type, item.Status, item.Message, item.LastTransitionTime)
   476  	}
   477  }
   478  
   479  func hasAppSetChanged(appReq, appRes *arogappsetv1.ApplicationSet, upsert bool) bool {
   480  	// upsert==false, no change occurred from create command
   481  	if !upsert {
   482  		return false
   483  	}
   484  
   485  	// Server will return nils for empty labels, annotations, finalizers
   486  	if len(appReq.Labels) == 0 {
   487  		appReq.Labels = nil
   488  	}
   489  	if len(appReq.Annotations) == 0 {
   490  		appReq.Annotations = nil
   491  	}
   492  	if len(appReq.Finalizers) == 0 {
   493  		appReq.Finalizers = nil
   494  	}
   495  
   496  	if reflect.DeepEqual(appRes.Spec, appReq.Spec) &&
   497  		reflect.DeepEqual(appRes.Labels, appReq.Labels) &&
   498  		reflect.DeepEqual(appRes.Annotations, appReq.Annotations) &&
   499  		reflect.DeepEqual(appRes.Finalizers, appReq.Finalizers) {
   500  		return false
   501  	}
   502  
   503  	return true
   504  }