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

     1  package commands
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"os"
     7  	"strconv"
     8  	"strings"
     9  	"text/tabwriter"
    10  
    11  	"gopkg.in/yaml.v3"
    12  
    13  	"github.com/argoproj/argo-cd/v3/cmd/argocd/commands/utils"
    14  	"github.com/argoproj/argo-cd/v3/cmd/util"
    15  	"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
    16  
    17  	log "github.com/sirupsen/logrus"
    18  	"github.com/spf13/cobra"
    19  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    20  	"k8s.io/apimachinery/pkg/types"
    21  	"k8s.io/utils/ptr"
    22  
    23  	"github.com/argoproj/argo-cd/v3/cmd/argocd/commands/headless"
    24  	argocdclient "github.com/argoproj/argo-cd/v3/pkg/apiclient"
    25  	applicationpkg "github.com/argoproj/argo-cd/v3/pkg/apiclient/application"
    26  	"github.com/argoproj/argo-cd/v3/util/argo"
    27  	"github.com/argoproj/argo-cd/v3/util/errors"
    28  	utilio "github.com/argoproj/argo-cd/v3/util/io"
    29  )
    30  
    31  // NewApplicationGetResourceCommand returns a new instance of the `app get-resource` command
    32  func NewApplicationGetResourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
    33  	var (
    34  		resourceName      string
    35  		kind              string
    36  		project           string
    37  		filteredFields    []string
    38  		showManagedFields bool
    39  		output            string
    40  	)
    41  	command := &cobra.Command{
    42  		Use:   "get-resource APPNAME",
    43  		Short: "Get details about the live Kubernetes manifests of a resource in an application. The filter-fields flag can be used to only display fields you want to see.",
    44  		Example: `
    45    # Get a specific resource, Pod my-app-pod, in 'my-app' by name in wide format
    46      argocd app get-resource my-app --kind Pod --resource-name my-app-pod
    47  
    48    # Get a specific resource, Pod my-app-pod, in 'my-app' by name in yaml format
    49      argocd app get-resource my-app --kind Pod --resource-name my-app-pod -o yaml
    50  
    51    # Get a specific resource, Pod my-app-pod, in 'my-app' by name in json format
    52      argocd app get-resource my-app --kind Pod --resource-name my-app-pod -o json
    53  
    54    # Get details about all Pods in the application
    55      argocd app get-resource my-app --kind Pod
    56  
    57    # Get a specific resource with managed fields, Pod my-app-pod, in 'my-app' by name in wide format
    58      argocd app get-resource my-app --kind Pod --resource-name my-app-pod --show-managed-fields
    59  
    60    # Get the the details of a specific field in a resource in 'my-app' in the wide format
    61      argocd app get-resource my-app --kind Pod --filter-fields status.podIP
    62  
    63    # Get the details of multiple specific fields in a specific resource in 'my-app' in the wide format
    64      argocd app get-resource my-app --kind Pod --resource-name my-app-pod --filter-fields status.podIP,status.hostIP`,
    65  	}
    66  
    67  	command.Run = func(c *cobra.Command, args []string) {
    68  		ctx := c.Context()
    69  
    70  		if len(args) != 1 {
    71  			c.HelpFunc()(c, args)
    72  			os.Exit(1)
    73  		}
    74  
    75  		appName, appNs := argo.ParseFromQualifiedName(args[0], "")
    76  
    77  		conn, appIf := headless.NewClientOrDie(clientOpts, c).NewApplicationClientOrDie()
    78  		defer utilio.Close(conn)
    79  
    80  		tree, err := appIf.ResourceTree(ctx, &applicationpkg.ResourcesQuery{
    81  			ApplicationName: &appName,
    82  			AppNamespace:    &appNs,
    83  		})
    84  		errors.CheckError(err)
    85  
    86  		// Get manifests of resources
    87  		// If resource name is "" find all resources of that kind
    88  		var resources []unstructured.Unstructured
    89  		var fetchedStr string
    90  		for _, r := range tree.Nodes {
    91  			if (resourceName != "" && r.Name != resourceName) || r.Kind != kind {
    92  				continue
    93  			}
    94  			resource, err := appIf.GetResource(ctx, &applicationpkg.ApplicationResourceRequest{
    95  				Name:         &appName,
    96  				AppNamespace: &appNs,
    97  				Group:        &r.Group,
    98  				Kind:         &r.Kind,
    99  				Namespace:    &r.Namespace,
   100  				Project:      &project,
   101  				ResourceName: &r.Name,
   102  				Version:      &r.Version,
   103  			})
   104  			errors.CheckError(err)
   105  			manifest := resource.GetManifest()
   106  
   107  			var obj *unstructured.Unstructured
   108  			err = json.Unmarshal([]byte(manifest), &obj)
   109  			errors.CheckError(err)
   110  
   111  			if !showManagedFields {
   112  				unstructured.RemoveNestedField(obj.Object, "metadata", "managedFields")
   113  			}
   114  
   115  			if len(filteredFields) != 0 {
   116  				obj = filterFieldsFromObject(obj, filteredFields)
   117  			}
   118  
   119  			fetchedStr += obj.GetName() + ", "
   120  			resources = append(resources, *obj)
   121  		}
   122  		printManifests(&resources, len(filteredFields) > 0, resourceName == "", output)
   123  
   124  		if fetchedStr != "" {
   125  			fetchedStr = strings.TrimSuffix(fetchedStr, ", ")
   126  		}
   127  		log.Infof("Resources '%s' fetched", fetchedStr)
   128  	}
   129  
   130  	command.Flags().StringVar(&resourceName, "resource-name", "", "Name of resource, if none is included will output details of all resources with specified kind")
   131  	command.Flags().StringVar(&kind, "kind", "", "Kind of resource [REQUIRED]")
   132  	err := command.MarkFlagRequired("kind")
   133  	errors.CheckError(err)
   134  	command.Flags().StringVar(&project, "project", "", "Project of resource")
   135  	command.Flags().StringSliceVar(&filteredFields, "filter-fields", nil, "A comma separated list of fields to display, if not provided will output the entire manifest")
   136  	command.Flags().BoolVar(&showManagedFields, "show-managed-fields", false, "Show managed fields in the output manifest")
   137  	command.Flags().StringVarP(&output, "output", "o", "wide", "Format of the output, wide, yaml, or json")
   138  	return command
   139  }
   140  
   141  // filterFieldsFromObject creates a new unstructured object containing only the specified fields from the source object.
   142  func filterFieldsFromObject(obj *unstructured.Unstructured, filteredFields []string) *unstructured.Unstructured {
   143  	var filteredObj unstructured.Unstructured
   144  	filteredObj.Object = make(map[string]any)
   145  
   146  	for _, f := range filteredFields {
   147  		fields := strings.Split(f, ".")
   148  
   149  		value, exists, err := unstructured.NestedFieldCopy(obj.Object, fields...)
   150  		if exists {
   151  			errors.CheckError(err)
   152  			err = unstructured.SetNestedField(filteredObj.Object, value, fields...)
   153  			errors.CheckError(err)
   154  		} else {
   155  			// If doesn't exist assume its a nested inside a list of objects
   156  			value := extractNestedItem(obj.Object, fields, 0)
   157  			filteredObj.Object = value
   158  		}
   159  	}
   160  	filteredObj.SetName(obj.GetName())
   161  	return &filteredObj
   162  }
   163  
   164  // extractNestedItem recursively extracts an item that may be nested inside a list of objects.
   165  func extractNestedItem(obj map[string]any, fields []string, depth int) map[string]any {
   166  	if depth >= len(fields) {
   167  		return nil
   168  	}
   169  
   170  	value, exists, _ := unstructured.NestedFieldCopy(obj, fields[:depth+1]...)
   171  	list, ok := value.([]any)
   172  	if !exists || !ok {
   173  		return extractNestedItem(obj, fields, depth+1)
   174  	}
   175  
   176  	extractedItems := extractItemsFromList(list, fields[depth+1:])
   177  	if len(extractedItems) == 0 {
   178  		for _, e := range list {
   179  			if o, ok := e.(map[string]any); ok {
   180  				result := extractNestedItem(o, fields[depth+1:], 0)
   181  				extractedItems = append(extractedItems, result)
   182  			}
   183  		}
   184  	}
   185  
   186  	filteredObj := reconstructObject(extractedItems, fields, depth)
   187  	return filteredObj
   188  }
   189  
   190  // extractItemsFromList processes a list of objects and extracts specific fields from each item.
   191  func extractItemsFromList(list []any, fields []string) []any {
   192  	var extratedObjs []any
   193  	for _, e := range list {
   194  		extractedObj := make(map[string]any)
   195  		if o, ok := e.(map[string]any); ok {
   196  			value, exists, _ := unstructured.NestedFieldCopy(o, fields...)
   197  			if !exists {
   198  				continue
   199  			}
   200  			err := unstructured.SetNestedField(extractedObj, value, fields...)
   201  			errors.CheckError(err)
   202  			extratedObjs = append(extratedObjs, extractedObj)
   203  		}
   204  	}
   205  	return extratedObjs
   206  }
   207  
   208  // reconstructObject rebuilds the original object structure by placing extracted items back into their proper nested location.
   209  func reconstructObject(extracted []any, fields []string, depth int) map[string]any {
   210  	obj := make(map[string]any)
   211  	err := unstructured.SetNestedField(obj, extracted, fields[:depth+1]...)
   212  	errors.CheckError(err)
   213  	return obj
   214  }
   215  
   216  // printManifests outputs resource manifests in the specified format (wide, JSON, or YAML).
   217  func printManifests(objs *[]unstructured.Unstructured, filteredFields bool, showName bool, output string) {
   218  	w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
   219  	if showName {
   220  		fmt.Fprintf(w, "FIELD\tRESOURCE NAME\tVALUE\n")
   221  	} else {
   222  		fmt.Fprintf(w, "FIELD\tVALUE\n")
   223  	}
   224  
   225  	for i, o := range *objs {
   226  		if output == "json" || output == "yaml" {
   227  			var formattedManifest []byte
   228  			var err error
   229  			if output == "json" {
   230  				formattedManifest, err = json.MarshalIndent(o.Object, "", " ")
   231  			} else {
   232  				formattedManifest, err = yaml.Marshal(o.Object)
   233  			}
   234  			errors.CheckError(err)
   235  
   236  			fmt.Println(string(formattedManifest))
   237  			if len(*objs) > 1 && i != len(*objs)-1 {
   238  				fmt.Println("---")
   239  			}
   240  		} else {
   241  			name := o.GetName()
   242  			if filteredFields {
   243  				unstructured.RemoveNestedField(o.Object, "metadata", "name")
   244  			}
   245  
   246  			printManifestAsTable(w, name, showName, o.Object, "")
   247  		}
   248  	}
   249  
   250  	if output != "json" && output != "yaml" {
   251  		err := w.Flush()
   252  		errors.CheckError(err)
   253  	}
   254  }
   255  
   256  // printManifestAsTable recursively prints a manifest object as a tabular view with nested fields flattened.
   257  func printManifestAsTable(w *tabwriter.Writer, name string, showName bool, obj map[string]any, parentField string) {
   258  	for key, value := range obj {
   259  		field := parentField + key
   260  		switch v := value.(type) {
   261  		case map[string]any:
   262  			printManifestAsTable(w, name, showName, v, field+".")
   263  		case []any:
   264  			for i, e := range v {
   265  				index := "[" + strconv.Itoa(i) + "]"
   266  
   267  				if innerObj, ok := e.(map[string]any); ok {
   268  					printManifestAsTable(w, name, showName, innerObj, field+index+".")
   269  				} else {
   270  					if showName {
   271  						fmt.Fprintf(w, "%v\t%v\t%v\n", field+index, name, e)
   272  					} else {
   273  						fmt.Fprintf(w, "%v\t%v\n", field+index, e)
   274  					}
   275  				}
   276  			}
   277  		default:
   278  			if showName {
   279  				fmt.Fprintf(w, "%v\t%v\t%v\n", field, name, v)
   280  			} else {
   281  				fmt.Fprintf(w, "%v\t%v\n", field, v)
   282  			}
   283  		}
   284  	}
   285  }
   286  
   287  func NewApplicationPatchResourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
   288  	var (
   289  		patch        string
   290  		patchType    string
   291  		resourceName string
   292  		namespace    string
   293  		kind         string
   294  		group        string
   295  		all          bool
   296  		project      string
   297  	)
   298  	command := &cobra.Command{
   299  		Use:   "patch-resource APPNAME",
   300  		Short: "Patch resource in an application",
   301  	}
   302  
   303  	command.Flags().StringVar(&patch, "patch", "", "Patch")
   304  	err := command.MarkFlagRequired("patch")
   305  	errors.CheckError(err)
   306  	command.Flags().StringVar(&patchType, "patch-type", string(types.MergePatchType), "Which Patching strategy to use: 'application/json-patch+json', 'application/merge-patch+json', or 'application/strategic-merge-patch+json'. Defaults to 'application/merge-patch+json'")
   307  	command.Flags().StringVar(&resourceName, "resource-name", "", "Name of resource")
   308  	command.Flags().StringVar(&kind, "kind", "", "Kind")
   309  	err = command.MarkFlagRequired("kind")
   310  	errors.CheckError(err)
   311  	command.Flags().StringVar(&group, "group", "", "Group")
   312  	command.Flags().StringVar(&namespace, "namespace", "", "Namespace")
   313  	command.Flags().BoolVar(&all, "all", false, "Indicates whether to patch multiple matching of resources")
   314  	command.Flags().StringVar(&project, "project", "", `The name of the application's project - specifying this allows the command to report "not found" instead of "permission denied" if the app does not exist`)
   315  	command.Run = func(c *cobra.Command, args []string) {
   316  		ctx := c.Context()
   317  
   318  		if len(args) != 1 {
   319  			c.HelpFunc()(c, args)
   320  			os.Exit(1)
   321  		}
   322  		appName, appNs := argo.ParseFromQualifiedName(args[0], "")
   323  
   324  		conn, appIf := headless.NewClientOrDie(clientOpts, c).NewApplicationClientOrDie()
   325  		defer utilio.Close(conn)
   326  		resources, err := appIf.ManagedResources(ctx, &applicationpkg.ResourcesQuery{
   327  			ApplicationName: &appName,
   328  			AppNamespace:    &appNs,
   329  		})
   330  		errors.CheckError(err)
   331  		objectsToPatch, err := util.FilterResources(command.Flags().Changed("group"), resources.Items, group, kind, namespace, resourceName, all)
   332  		errors.CheckError(err)
   333  		for i := range objectsToPatch {
   334  			obj := objectsToPatch[i]
   335  			gvk := obj.GroupVersionKind()
   336  			_, err = appIf.PatchResource(ctx, &applicationpkg.ApplicationResourcePatchRequest{
   337  				Name:         &appName,
   338  				AppNamespace: &appNs,
   339  				Namespace:    ptr.To(obj.GetNamespace()),
   340  				ResourceName: ptr.To(obj.GetName()),
   341  				Version:      ptr.To(gvk.Version),
   342  				Group:        ptr.To(gvk.Group),
   343  				Kind:         ptr.To(gvk.Kind),
   344  				Patch:        ptr.To(patch),
   345  				PatchType:    ptr.To(patchType),
   346  				Project:      ptr.To(project),
   347  			})
   348  			errors.CheckError(err)
   349  			log.Infof("Resource '%s' patched", obj.GetName())
   350  		}
   351  	}
   352  
   353  	return command
   354  }
   355  
   356  func NewApplicationDeleteResourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
   357  	var (
   358  		resourceName string
   359  		namespace    string
   360  		kind         string
   361  		group        string
   362  		force        bool
   363  		orphan       bool
   364  		all          bool
   365  		project      string
   366  	)
   367  	command := &cobra.Command{
   368  		Use:   "delete-resource APPNAME",
   369  		Short: "Delete resource in an application",
   370  	}
   371  
   372  	command.Flags().StringVar(&resourceName, "resource-name", "", "Name of resource")
   373  	command.Flags().StringVar(&kind, "kind", "", "Kind")
   374  	err := command.MarkFlagRequired("kind")
   375  	errors.CheckError(err)
   376  	command.Flags().StringVar(&group, "group", "", "Group")
   377  	command.Flags().StringVar(&namespace, "namespace", "", "Namespace")
   378  	command.Flags().BoolVar(&force, "force", false, "Indicates whether to force delete the resource")
   379  	command.Flags().BoolVar(&orphan, "orphan", false, "Indicates whether to orphan the dependents of the deleted resource")
   380  	command.Flags().BoolVar(&all, "all", false, "Indicates whether to patch multiple matching of resources")
   381  	command.Flags().StringVar(&project, "project", "", `The name of the application's project - specifying this allows the command to report "not found" instead of "permission denied" if the app does not exist`)
   382  	command.Run = func(c *cobra.Command, args []string) {
   383  		ctx := c.Context()
   384  
   385  		if len(args) != 1 {
   386  			c.HelpFunc()(c, args)
   387  			os.Exit(1)
   388  		}
   389  		appName, appNs := argo.ParseFromQualifiedName(args[0], "")
   390  
   391  		conn, appIf := headless.NewClientOrDie(clientOpts, c).NewApplicationClientOrDie()
   392  		defer utilio.Close(conn)
   393  		resources, err := appIf.ManagedResources(ctx, &applicationpkg.ResourcesQuery{
   394  			ApplicationName: &appName,
   395  			AppNamespace:    &appNs,
   396  		})
   397  		errors.CheckError(err)
   398  		objectsToDelete, err := util.FilterResources(command.Flags().Changed("group"), resources.Items, group, kind, namespace, resourceName, all)
   399  		errors.CheckError(err)
   400  
   401  		promptUtil := utils.NewPrompt(clientOpts.PromptsEnabled)
   402  
   403  		for i := range objectsToDelete {
   404  			obj := objectsToDelete[i]
   405  			gvk := obj.GroupVersionKind()
   406  
   407  			canDelete := promptUtil.Confirm(fmt.Sprintf("Are you sure you want to delete %s/%s %s/%s ? [y/n]", gvk.Group, gvk.Kind, obj.GetNamespace(), obj.GetName()))
   408  			if canDelete {
   409  				_, err = appIf.DeleteResource(ctx, &applicationpkg.ApplicationResourceDeleteRequest{
   410  					Name:         &appName,
   411  					AppNamespace: &appNs,
   412  					Namespace:    ptr.To(obj.GetNamespace()),
   413  					ResourceName: ptr.To(obj.GetName()),
   414  					Version:      ptr.To(gvk.Version),
   415  					Group:        ptr.To(gvk.Group),
   416  					Kind:         ptr.To(gvk.Kind),
   417  					Force:        &force,
   418  					Orphan:       &orphan,
   419  					Project:      ptr.To(project),
   420  				})
   421  				errors.CheckError(err)
   422  				log.Infof("Resource '%s' deleted", obj.GetName())
   423  			} else {
   424  				fmt.Printf("The command to delete %s/%s %s/%s was cancelled.\n", gvk.Group, gvk.Kind, obj.GetNamespace(), obj.GetName())
   425  			}
   426  		}
   427  	}
   428  
   429  	return command
   430  }
   431  
   432  func parentChildInfo(nodes []v1alpha1.ResourceNode) (map[string]v1alpha1.ResourceNode, map[string][]string, map[string]struct{}) {
   433  	mapUIDToNode := make(map[string]v1alpha1.ResourceNode)
   434  	mapParentToChild := make(map[string][]string)
   435  	parentNode := make(map[string]struct{})
   436  
   437  	for _, node := range nodes {
   438  		mapUIDToNode[node.UID] = node
   439  
   440  		if len(node.ParentRefs) > 0 {
   441  			_, ok := mapParentToChild[node.ParentRefs[0].UID]
   442  			if !ok {
   443  				var temp []string
   444  				mapParentToChild[node.ParentRefs[0].UID] = temp
   445  			}
   446  			mapParentToChild[node.ParentRefs[0].UID] = append(mapParentToChild[node.ParentRefs[0].UID], node.UID)
   447  		} else {
   448  			parentNode[node.UID] = struct{}{}
   449  		}
   450  	}
   451  	return mapUIDToNode, mapParentToChild, parentNode
   452  }
   453  
   454  func printDetailedTreeViewAppResourcesNotOrphaned(nodeMapping map[string]v1alpha1.ResourceNode, parentChildMapping map[string][]string, parentNodes map[string]struct{}, w *tabwriter.Writer) {
   455  	for uid := range parentNodes {
   456  		detailedTreeViewAppResourcesNotOrphaned("", nodeMapping, parentChildMapping, nodeMapping[uid], w)
   457  	}
   458  }
   459  
   460  func printDetailedTreeViewAppResourcesOrphaned(nodeMapping map[string]v1alpha1.ResourceNode, parentChildMapping map[string][]string, parentNodes map[string]struct{}, w *tabwriter.Writer) {
   461  	for uid := range parentNodes {
   462  		detailedTreeViewAppResourcesOrphaned("", nodeMapping, parentChildMapping, nodeMapping[uid], w)
   463  	}
   464  }
   465  
   466  func printTreeViewAppResourcesNotOrphaned(nodeMapping map[string]v1alpha1.ResourceNode, parentChildMapping map[string][]string, parentNodes map[string]struct{}, w *tabwriter.Writer) {
   467  	for uid := range parentNodes {
   468  		treeViewAppResourcesNotOrphaned("", nodeMapping, parentChildMapping, nodeMapping[uid], w)
   469  	}
   470  }
   471  
   472  func printTreeViewAppResourcesOrphaned(nodeMapping map[string]v1alpha1.ResourceNode, parentChildMapping map[string][]string, parentNodes map[string]struct{}, w *tabwriter.Writer) {
   473  	for uid := range parentNodes {
   474  		treeViewAppResourcesOrphaned("", nodeMapping, parentChildMapping, nodeMapping[uid], w)
   475  	}
   476  }
   477  
   478  func printResources(listAll bool, orphaned bool, appResourceTree *v1alpha1.ApplicationTree, output string) {
   479  	w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
   480  	switch output {
   481  	case "tree=detailed":
   482  		fmt.Fprintf(w, "GROUP\tKIND\tNAMESPACE\tNAME\tORPHANED\tAGE\tHEALTH\tREASON\n")
   483  
   484  		if !orphaned || listAll {
   485  			mapUIDToNode, mapParentToChild, parentNode := parentChildInfo(appResourceTree.Nodes)
   486  			printDetailedTreeViewAppResourcesNotOrphaned(mapUIDToNode, mapParentToChild, parentNode, w)
   487  		}
   488  
   489  		if orphaned || listAll {
   490  			mapUIDToNode, mapParentToChild, parentNode := parentChildInfo(appResourceTree.OrphanedNodes)
   491  			printDetailedTreeViewAppResourcesOrphaned(mapUIDToNode, mapParentToChild, parentNode, w)
   492  		}
   493  	case "tree":
   494  		fmt.Fprintf(w, "GROUP\tKIND\tNAMESPACE\tNAME\tORPHANED\n")
   495  
   496  		if !orphaned || listAll {
   497  			mapUIDToNode, mapParentToChild, parentNode := parentChildInfo(appResourceTree.Nodes)
   498  			printTreeViewAppResourcesNotOrphaned(mapUIDToNode, mapParentToChild, parentNode, w)
   499  		}
   500  
   501  		if orphaned || listAll {
   502  			mapUIDToNode, mapParentToChild, parentNode := parentChildInfo(appResourceTree.OrphanedNodes)
   503  			printTreeViewAppResourcesOrphaned(mapUIDToNode, mapParentToChild, parentNode, w)
   504  		}
   505  	default:
   506  		headers := []any{"GROUP", "KIND", "NAMESPACE", "NAME", "ORPHANED"}
   507  		fmtStr := "%s\t%s\t%s\t%s\t%s\n"
   508  		_, _ = fmt.Fprintf(w, fmtStr, headers...)
   509  		if !orphaned || listAll {
   510  			for _, res := range appResourceTree.Nodes {
   511  				if len(res.ParentRefs) == 0 {
   512  					_, _ = fmt.Fprintf(w, fmtStr, res.Group, res.Kind, res.Namespace, res.Name, "No")
   513  				}
   514  			}
   515  		}
   516  		if orphaned || listAll {
   517  			for _, res := range appResourceTree.OrphanedNodes {
   518  				_, _ = fmt.Fprintf(w, fmtStr, res.Group, res.Kind, res.Namespace, res.Name, "Yes")
   519  			}
   520  		}
   521  	}
   522  	err := w.Flush()
   523  	errors.CheckError(err)
   524  }
   525  
   526  func NewApplicationListResourcesCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
   527  	var (
   528  		orphaned bool
   529  		output   string
   530  		project  string
   531  	)
   532  	command := &cobra.Command{
   533  		Use:   "resources APPNAME",
   534  		Short: "List resource of application",
   535  		Run: func(c *cobra.Command, args []string) {
   536  			ctx := c.Context()
   537  			if len(args) != 1 {
   538  				c.HelpFunc()(c, args)
   539  				os.Exit(1)
   540  			}
   541  			listAll := !c.Flag("orphaned").Changed
   542  			appName, appNs := argo.ParseFromQualifiedName(args[0], "")
   543  			conn, appIf := headless.NewClientOrDie(clientOpts, c).NewApplicationClientOrDie()
   544  			defer utilio.Close(conn)
   545  			appResourceTree, err := appIf.ResourceTree(ctx, &applicationpkg.ResourcesQuery{
   546  				ApplicationName: &appName,
   547  				AppNamespace:    &appNs,
   548  				Project:         &project,
   549  			})
   550  			errors.CheckError(err)
   551  			printResources(listAll, orphaned, appResourceTree, output)
   552  		},
   553  	}
   554  	command.Flags().BoolVar(&orphaned, "orphaned", false, "Lists only orphaned resources")
   555  	command.Flags().StringVar(&output, "output", "", "Provides the tree view of the resources")
   556  	command.Flags().StringVar(&project, "project", "", `The name of the application's project - specifying this allows the command to report "not found" instead of "permission denied" if the app does not exist`)
   557  	return command
   558  }