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 }