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 }