diff --git a/cmd/argocd/commands/app.go b/cmd/argocd/commands/app.go index bcf0b5a378d64..2559c702b5ddc 100644 --- a/cmd/argocd/commands/app.go +++ b/cmd/argocd/commands/app.go @@ -259,6 +259,54 @@ func hasAppChanged(appReq, appRes *argoappv1.Application, upsert bool) bool { return true } +func parentChildDetails(appIf application.ApplicationServiceClient, ctx context.Context, appName string, appNs string) (map[string]argoappv1.ResourceNode, map[string][]string, map[string]struct{}) { + + mapUidToNode := make(map[string]argoappv1.ResourceNode) + mapParentToChild := make(map[string][]string) + parentNode := make(map[string]struct{}) + + resourceTree, err := appIf.ResourceTree(ctx, &application.ResourcesQuery{Name: &appName, AppNamespace: &appNs, ApplicationName: &appName}) + errors.CheckError(err) + + for _, node := range resourceTree.Nodes { + + mapUidToNode[node.UID] = node + + if len(node.ParentRefs) > 0 { + _, ok := mapParentToChild[node.ParentRefs[0].UID] + if !ok { + var temp []string + mapParentToChild[node.ParentRefs[0].UID] = temp + } + mapParentToChild[node.ParentRefs[0].UID] = append(mapParentToChild[node.ParentRefs[0].UID], node.UID) + } else { + parentNode[node.UID] = struct{}{} + } + + } + return mapUidToNode, mapParentToChild, parentNode +} + +func printHeader(acdClient argocdclient.Client, app *argoappv1.Application, ctx context.Context, windows *argoappv1.SyncWindows, showOperation bool, showParams bool) { + aURL := appURL(ctx, acdClient, app.Name) + printAppSummaryTable(app, aURL, windows) + + if len(app.Status.Conditions) > 0 { + fmt.Println() + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + printAppConditions(w, app) + _ = w.Flush() + fmt.Println() + } + if showOperation && app.Status.OperationState != nil { + fmt.Println() + printOperationResult(app.Status.OperationState) + } + if showParams { + printParams(app) + } +} + // NewApplicationGetCommand returns a new instance of an `argocd app get` command func NewApplicationGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command { var ( @@ -268,12 +316,13 @@ func NewApplicationGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Com showParams bool showOperation bool ) + var command = &cobra.Command{ Use: "get APPNAME", Short: "Get application details", Run: func(c *cobra.Command, args []string) { ctx := c.Context() - + output, _ = c.Flags().GetString("output") if len(args) == 0 { c.HelpFunc()(c, args) os.Exit(1) @@ -283,11 +332,13 @@ func NewApplicationGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Com defer argoio.Close(conn) appName, appNs := argo.ParseFromQualifiedName(args[0], "") + app, err := appIf.Get(ctx, &application.ApplicationQuery{ Name: &appName, Refresh: getRefreshType(refresh, hardRefresh), AppNamespace: &appNs, }) + errors.CheckError(err) pConn, projIf := headless.NewClientOrDie(clientOpts, c).NewProjectClientOrDie() @@ -302,35 +353,41 @@ func NewApplicationGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Com err := PrintResource(app, output) errors.CheckError(err) case "wide", "": - aURL := appURL(ctx, acdClient, app.Name) - printAppSummaryTable(app, aURL, windows) - - if len(app.Status.Conditions) > 0 { + printHeader(acdClient, app, ctx, windows, showOperation, showParams) + if len(app.Status.Resources) > 0 { fmt.Println() w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) - printAppConditions(w, app) + printAppResources(w, app) _ = w.Flush() - fmt.Println() } - if showOperation && app.Status.OperationState != nil { + case "tree": + printHeader(acdClient, app, ctx, windows, showOperation, showParams) + mapUidToNode, mapParentToChild, parentNode := parentChildDetails(appIf, ctx, appName, appNs) + mapNodeNameToResourceState := make(map[string]*resourceState) + for _, res := range getResourceStates(app, nil) { + mapNodeNameToResourceState[res.Kind+"/"+res.Name] = res + } + if len(mapUidToNode) > 0 { fmt.Println() - printOperationResult(app.Status.OperationState) + printTreeView(mapUidToNode, mapParentToChild, parentNode, mapNodeNameToResourceState) } - if showParams { - printParams(app) + case "tree=detailed": + printHeader(acdClient, app, ctx, windows, showOperation, showParams) + mapUidToNode, mapParentToChild, parentNode := parentChildDetails(appIf, ctx, appName, appNs) + mapNodeNameToResourceState := make(map[string]*resourceState) + for _, res := range getResourceStates(app, nil) { + mapNodeNameToResourceState[res.Kind+"/"+res.Name] = res } - if len(app.Status.Resources) > 0 { + if len(mapUidToNode) > 0 { fmt.Println() - w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) - printAppResources(w, app) - _ = w.Flush() + printTreeViewDetailed(mapUidToNode, mapParentToChild, parentNode, mapNodeNameToResourceState) } default: errors.CheckError(fmt.Errorf("unknown output format: %s", output)) } }, } - command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide") + command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide|tree") command.Flags().BoolVar(&showOperation, "show-operation", false, "Show application operation") command.Flags().BoolVar(&showParams, "show-params", false, "Show application parameters and overrides") command.Flags().BoolVar(&refresh, "refresh", false, "Refresh application data when retrieving") @@ -1521,6 +1578,24 @@ func printAppResources(w io.Writer, app *argoappv1.Application) { } } +func printTreeView(nodeMapping map[string]argoappv1.ResourceNode, parentChildMapping map[string][]string, parentNodes map[string]struct{}, mapNodeNameToResourceState map[string]*resourceState) { + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + _, _ = fmt.Fprintf(w, "KIND/NAME\tSTATUS\tHEALTH\tMESSAGE\n") + for uid := range parentNodes { + treeViewAppGet("", nodeMapping, parentChildMapping, nodeMapping[uid], mapNodeNameToResourceState, w) + } + _ = w.Flush() +} + +func printTreeViewDetailed(nodeMapping map[string]argoappv1.ResourceNode, parentChildMapping map[string][]string, parentNodes map[string]struct{}, mapNodeNameToResourceState map[string]*resourceState) { + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + fmt.Fprintf(w, "KIND/NAME\tSTATUS\tHEALTH\tAGE\tMESSAGE\tREASON\n") + for uid := range parentNodes { + detailedTreeViewAppGet("", nodeMapping, parentChildMapping, nodeMapping[uid], mapNodeNameToResourceState, w) + } + _ = w.Flush() +} + // NewApplicationSyncCommand returns a new instance of an `argocd app sync` command func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command { var ( diff --git a/cmd/argocd/commands/app_resource_test.go b/cmd/argocd/commands/app_resource_test.go index 2c94ad7a0f418..5846065141e15 100644 --- a/cmd/argocd/commands/app_resource_test.go +++ b/cmd/argocd/commands/app_resource_test.go @@ -1,13 +1,93 @@ package commands import ( + "bytes" "testing" + "text/tabwriter" "github.com/stretchr/testify/assert" "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" ) +func TestPrintTreeViewAppResources(t *testing.T) { + var nodes [3]v1alpha1.ResourceNode + nodes[0].ResourceRef = v1alpha1.ResourceRef{Group: "", Version: "v1", Kind: "Pod", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo-5dcd5457d5-6trpt", UID: "92c3a5fe-d13e-4ae2-b8ec-c10dd3543b28"} + nodes[0].ParentRefs = []v1alpha1.ResourceRef{{Group: "apps", Version: "v1", Kind: "ReplicaSet", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo-5dcd5457d5", UID: "75c30dce-1b66-414f-a86c-573a74be0f40"}} + nodes[1].ResourceRef = v1alpha1.ResourceRef{Group: "apps", Version: "v1", Kind: "ReplicaSet", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo-5dcd5457d5", UID: "75c30dce-1b66-414f-a86c-573a74be0f40"} + nodes[1].ParentRefs = []v1alpha1.ResourceRef{{Group: "argoproj.io", Version: "", Kind: "Rollout", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo", UID: "87f3aab0-f634-4b2c-959a-7ddd30675ed0"}} + nodes[2].ResourceRef = v1alpha1.ResourceRef{Group: "argoproj.io", Version: "", Kind: "Rollout", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo", UID: "87f3aab0-f634-4b2c-959a-7ddd30675ed0"} + var nodeMapping = make(map[string]v1alpha1.ResourceNode) + var mapParentToChild = make(map[string][]string) + var parentNode = make(map[string]struct{}) + for _, node := range nodes { + nodeMapping[node.UID] = node + if len(node.ParentRefs) > 0 { + _, ok := mapParentToChild[node.ParentRefs[0].UID] + if !ok { + var temp []string + mapParentToChild[node.ParentRefs[0].UID] = temp + } + mapParentToChild[node.ParentRefs[0].UID] = append(mapParentToChild[node.ParentRefs[0].UID], node.UID) + } else { + parentNode[node.UID] = struct{}{} + } + } + buf := &bytes.Buffer{} + w := tabwriter.NewWriter(buf, 0, 0, 2, ' ', 0) + + printTreeViewAppResourcesNotOrphaned(nodeMapping, mapParentToChild, parentNode, false, false, w) + if err := w.Flush(); err != nil { + t.Fatal(err) + } + output := buf.String() + + assert.Contains(t, output, "Rollout") + assert.Contains(t, output, "argoproj.io") +} + +func TestPrintTreeViewDetailedAppResources(t *testing.T) { + var nodes [3]v1alpha1.ResourceNode + nodes[0].ResourceRef = v1alpha1.ResourceRef{Group: "", Version: "v1", Kind: "Pod", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo-5dcd5457d5-6trpt", UID: "92c3a5fe-d13e-4ae2-b8ec-c10dd3543b28"} + nodes[0].ParentRefs = []v1alpha1.ResourceRef{{Group: "apps", Version: "v1", Kind: "ReplicaSet", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo-5dcd5457d5", UID: "75c30dce-1b66-414f-a86c-573a74be0f40"}} + nodes[1].ResourceRef = v1alpha1.ResourceRef{Group: "apps", Version: "v1", Kind: "ReplicaSet", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo-5dcd5457d5", UID: "75c30dce-1b66-414f-a86c-573a74be0f40"} + nodes[1].ParentRefs = []v1alpha1.ResourceRef{{Group: "argoproj.io", Version: "", Kind: "Rollout", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo", UID: "87f3aab0-f634-4b2c-959a-7ddd30675ed0"}} + nodes[2].ResourceRef = v1alpha1.ResourceRef{Group: "argoproj.io", Version: "", Kind: "Rollout", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo", UID: "87f3aab0-f634-4b2c-959a-7ddd30675ed0"} + nodes[2].Health = &v1alpha1.HealthStatus{ + Status: "Degraded", + Message: "Readiness Gate failed", + } + + var nodeMapping = make(map[string]v1alpha1.ResourceNode) + var mapParentToChild = make(map[string][]string) + var parentNode = make(map[string]struct{}) + for _, node := range nodes { + nodeMapping[node.UID] = node + if len(node.ParentRefs) > 0 { + _, ok := mapParentToChild[node.ParentRefs[0].UID] + if !ok { + var temp []string + mapParentToChild[node.ParentRefs[0].UID] = temp + } + mapParentToChild[node.ParentRefs[0].UID] = append(mapParentToChild[node.ParentRefs[0].UID], node.UID) + } else { + parentNode[node.UID] = struct{}{} + } + } + buf := &bytes.Buffer{} + w := tabwriter.NewWriter(buf, 0, 0, 2, ' ', 0) + + printDetailedTreeViewAppResourcesNotOrphaned(nodeMapping, mapParentToChild, parentNode, false, false, w) + if err := w.Flush(); err != nil { + t.Fatal(err) + } + output := buf.String() + + assert.Contains(t, output, "Rollout") + assert.Contains(t, output, "Degraded") + assert.Contains(t, output, "Readiness Gate failed") +} + func TestPrintResourcesTree(t *testing.T) { tree := v1alpha1.ApplicationTree{ Nodes: []v1alpha1.ResourceNode{ @@ -32,7 +112,7 @@ func TestPrintResourcesTree(t *testing.T) { }, } output, _ := captureOutput(func() error { - printResources(true, false, &tree) + printResources(true, false, &tree, "") return nil }) diff --git a/cmd/argocd/commands/app_resources.go b/cmd/argocd/commands/app_resources.go index 60ba6efff406e..f7c8ecdbe12bf 100644 --- a/cmd/argocd/commands/app_resources.go +++ b/cmd/argocd/commands/app_resources.go @@ -4,9 +4,8 @@ import ( "fmt" "os" - "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" - "github.com/argoproj/argo-cd/v2/cmd/util" + "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -149,34 +148,114 @@ func NewApplicationDeleteResourceCommand(clientOpts *argocdclient.ClientOptions) return command } -func printResources(listAll bool, orphaned bool, appResourceTree *v1alpha1.ApplicationTree) { - w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) - headers := []interface{}{"GROUP", "KIND", "NAMESPACE", "NAME", "ORPHANED"} - fmtStr := "%s\t%s\t%s\t%s\t%s\n" - _, _ = fmt.Fprintf(w, fmtStr, headers...) - if !orphaned || listAll { - for _, res := range appResourceTree.Nodes { - if len(res.ParentRefs) == 0 { - _, _ = fmt.Fprintf(w, fmtStr, res.Group, res.Kind, res.Namespace, res.Name, "No") +func parentChildInfo(nodes []v1alpha1.ResourceNode) (map[string]v1alpha1.ResourceNode, map[string][]string, map[string]struct{}) { + mapUidToNode := make(map[string]v1alpha1.ResourceNode) + mapParentToChild := make(map[string][]string) + parentNode := make(map[string]struct{}) + + for _, node := range nodes { + mapUidToNode[node.UID] = node + + if len(node.ParentRefs) > 0 { + _, ok := mapParentToChild[node.ParentRefs[0].UID] + if !ok { + var temp []string + mapParentToChild[node.ParentRefs[0].UID] = temp } + mapParentToChild[node.ParentRefs[0].UID] = append(mapParentToChild[node.ParentRefs[0].UID], node.UID) + } else { + parentNode[node.UID] = struct{}{} } } - if orphaned || listAll { - for _, res := range appResourceTree.OrphanedNodes { - _, _ = fmt.Fprintf(w, fmtStr, res.Group, res.Kind, res.Namespace, res.Name, "Yes") + return mapUidToNode, mapParentToChild, parentNode +} + +func printDetailedTreeViewAppResourcesNotOrphaned(nodeMapping map[string]v1alpha1.ResourceNode, parentChildMapping map[string][]string, parentNodes map[string]struct{}, orphaned bool, listAll bool, w *tabwriter.Writer) { + for uid := range parentNodes { + detailedTreeViewAppResourcesNotOrphaned("", nodeMapping, parentChildMapping, nodeMapping[uid], w) + } + +} + +func printDetailedTreeViewAppResourcesOrphaned(nodeMapping map[string]v1alpha1.ResourceNode, parentChildMapping map[string][]string, parentNodes map[string]struct{}, orphaned bool, listAll bool, w *tabwriter.Writer) { + for uid := range parentNodes { + detailedTreeViewAppResourcesOrphaned("", nodeMapping, parentChildMapping, nodeMapping[uid], w) + } +} + +func printTreeViewAppResourcesNotOrphaned(nodeMapping map[string]v1alpha1.ResourceNode, parentChildMapping map[string][]string, parentNodes map[string]struct{}, orphaned bool, listAll bool, w *tabwriter.Writer) { + for uid := range parentNodes { + treeViewAppResourcesNotOrphaned("", nodeMapping, parentChildMapping, nodeMapping[uid], w) + } + +} + +func printTreeViewAppResourcesOrphaned(nodeMapping map[string]v1alpha1.ResourceNode, parentChildMapping map[string][]string, parentNodes map[string]struct{}, orphaned bool, listAll bool, w *tabwriter.Writer) { + for uid := range parentNodes { + treeViewAppResourcesOrphaned("", nodeMapping, parentChildMapping, nodeMapping[uid], w) + } +} + +func printResources(listAll bool, orphaned bool, appResourceTree *v1alpha1.ApplicationTree, output string) { + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + if output == "tree=detailed" { + fmt.Fprintf(w, "GROUP\tKIND\tNAMESPACE\tNAME\tORPHANED\tAGE\tHEALTH\tREASON\n") + + if !orphaned || listAll { + mapUidToNode, mapParentToChild, parentNode := parentChildInfo(appResourceTree.Nodes) + printDetailedTreeViewAppResourcesNotOrphaned(mapUidToNode, mapParentToChild, parentNode, orphaned, listAll, w) + } + + if orphaned || listAll { + mapUidToNode, mapParentToChild, parentNode := parentChildInfo(appResourceTree.OrphanedNodes) + printDetailedTreeViewAppResourcesOrphaned(mapUidToNode, mapParentToChild, parentNode, orphaned, listAll, w) + } + + } else if output == "tree" { + fmt.Fprintf(w, "GROUP\tKIND\tNAMESPACE\tNAME\tORPHANED\n") + + if !orphaned || listAll { + mapUidToNode, mapParentToChild, parentNode := parentChildInfo(appResourceTree.Nodes) + printTreeViewAppResourcesNotOrphaned(mapUidToNode, mapParentToChild, parentNode, orphaned, listAll, w) + } + + if orphaned || listAll { + mapUidToNode, mapParentToChild, parentNode := parentChildInfo(appResourceTree.OrphanedNodes) + printTreeViewAppResourcesOrphaned(mapUidToNode, mapParentToChild, parentNode, orphaned, listAll, w) + } + + } else { + + headers := []interface{}{"GROUP", "KIND", "NAMESPACE", "NAME", "ORPHANED"} + fmtStr := "%s\t%s\t%s\t%s\t%s\n" + _, _ = fmt.Fprintf(w, fmtStr, headers...) + if !orphaned || listAll { + for _, res := range appResourceTree.Nodes { + if len(res.ParentRefs) == 0 { + _, _ = fmt.Fprintf(w, fmtStr, res.Group, res.Kind, res.Namespace, res.Name, "No") + } + } } + if orphaned || listAll { + for _, res := range appResourceTree.OrphanedNodes { + _, _ = fmt.Fprintf(w, fmtStr, res.Group, res.Kind, res.Namespace, res.Name, "Yes") + } + } + } _ = w.Flush() + } func NewApplicationListResourcesCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command { var orphaned bool + var output string var command = &cobra.Command{ Use: "resources APPNAME", Short: "List resource of application", Run: func(c *cobra.Command, args []string) { ctx := c.Context() - + output, _ = c.Flags().GetString("output") if len(args) != 1 { c.HelpFunc()(c, args) os.Exit(1) @@ -190,9 +269,10 @@ func NewApplicationListResourcesCommand(clientOpts *argocdclient.ClientOptions) AppNamespace: &appNs, }) errors.CheckError(err) - printResources(listAll, orphaned, appResourceTree) + printResources(listAll, orphaned, appResourceTree, output) }, } command.Flags().BoolVar(&orphaned, "orphaned", false, "Lists only orphaned resources") + command.Flags().StringVar(&output, "output", "", "Provides the tree view of the resources") return command } diff --git a/cmd/argocd/commands/app_test.go b/cmd/argocd/commands/app_test.go index 0880fdc1c1ae5..68983560999c8 100644 --- a/cmd/argocd/commands/app_test.go +++ b/cmd/argocd/commands/app_test.go @@ -115,6 +115,86 @@ func TestFindRevisionHistoryWithoutPassedId(t *testing.T) { } +func TestPrintTreeViewAppGet(t *testing.T) { + var nodes [3]v1alpha1.ResourceNode + nodes[0].ResourceRef = v1alpha1.ResourceRef{Group: "", Version: "v1", Kind: "Pod", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo-5dcd5457d5-6trpt", UID: "92c3a5fe-d13e-4ae2-b8ec-c10dd3543b28"} + nodes[0].ParentRefs = []v1alpha1.ResourceRef{{Group: "apps", Version: "v1", Kind: "ReplicaSet", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo-5dcd5457d5", UID: "75c30dce-1b66-414f-a86c-573a74be0f40"}} + nodes[1].ResourceRef = v1alpha1.ResourceRef{Group: "apps", Version: "v1", Kind: "ReplicaSet", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo-5dcd5457d5", UID: "75c30dce-1b66-414f-a86c-573a74be0f40"} + nodes[1].ParentRefs = []v1alpha1.ResourceRef{{Group: "argoproj.io", Version: "", Kind: "Rollout", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo", UID: "87f3aab0-f634-4b2c-959a-7ddd30675ed0"}} + nodes[2].ResourceRef = v1alpha1.ResourceRef{Group: "argoproj.io", Version: "", Kind: "Rollout", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo", UID: "87f3aab0-f634-4b2c-959a-7ddd30675ed0"} + + var nodeMapping = make(map[string]v1alpha1.ResourceNode) + var mapParentToChild = make(map[string][]string) + var parentNode = make(map[string]struct{}) + + for _, node := range nodes { + nodeMapping[node.UID] = node + + if len(node.ParentRefs) > 0 { + _, ok := mapParentToChild[node.ParentRefs[0].UID] + if !ok { + var temp []string + mapParentToChild[node.ParentRefs[0].UID] = temp + } + mapParentToChild[node.ParentRefs[0].UID] = append(mapParentToChild[node.ParentRefs[0].UID], node.UID) + } else { + parentNode[node.UID] = struct{}{} + } + } + + output, _ := captureOutput(func() error { + printTreeView(nodeMapping, mapParentToChild, parentNode, nil) + return nil + }) + + assert.Contains(t, output, "Pod") + assert.Contains(t, output, "ReplicaSet") + assert.Contains(t, output, "Rollout") + assert.Contains(t, output, "numalogic-rollout-demo-5dcd5457d5-6trpt") +} + +func TestPrintTreeViewDetailedAppGet(t *testing.T) { + var nodes [3]v1alpha1.ResourceNode + nodes[0].ResourceRef = v1alpha1.ResourceRef{Group: "", Version: "v1", Kind: "Pod", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo-5dcd5457d5-6trpt", UID: "92c3a5fe-d13e-4ae2-b8ec-c10dd3543b28"} + nodes[0].Health = &v1alpha1.HealthStatus{Status: "Degraded", Message: "Readiness Gate failed"} + nodes[0].ParentRefs = []v1alpha1.ResourceRef{{Group: "apps", Version: "v1", Kind: "ReplicaSet", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo-5dcd5457d5", UID: "75c30dce-1b66-414f-a86c-573a74be0f40"}} + nodes[1].ResourceRef = v1alpha1.ResourceRef{Group: "apps", Version: "v1", Kind: "ReplicaSet", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo-5dcd5457d5", UID: "75c30dce-1b66-414f-a86c-573a74be0f40"} + nodes[1].ParentRefs = []v1alpha1.ResourceRef{{Group: "argoproj.io", Version: "", Kind: "Rollout", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo", UID: "87f3aab0-f634-4b2c-959a-7ddd30675ed0"}} + nodes[2].ResourceRef = v1alpha1.ResourceRef{Group: "argoproj.io", Version: "", Kind: "Rollout", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo", UID: "87f3aab0-f634-4b2c-959a-7ddd30675ed0"} + + var nodeMapping = make(map[string]v1alpha1.ResourceNode) + var mapParentToChild = make(map[string][]string) + var parentNode = make(map[string]struct{}) + + for _, node := range nodes { + nodeMapping[node.UID] = node + + if len(node.ParentRefs) > 0 { + _, ok := mapParentToChild[node.ParentRefs[0].UID] + if !ok { + var temp []string + mapParentToChild[node.ParentRefs[0].UID] = temp + } + mapParentToChild[node.ParentRefs[0].UID] = append(mapParentToChild[node.ParentRefs[0].UID], node.UID) + } else { + parentNode[node.UID] = struct{}{} + } + } + + output, _ := captureOutput(func() error { + printTreeViewDetailed(nodeMapping, mapParentToChild, parentNode, nil) + return nil + }) + + assert.Contains(t, output, "Pod") + assert.Contains(t, output, "ReplicaSet") + assert.Contains(t, output, "Rollout") + assert.Contains(t, output, "numalogic-rollout-demo-5dcd5457d5-6trpt") + assert.Contains(t, output, "Degraded") + assert.Contains(t, output, "Readiness Gate failed") + +} + func TestDefaultWaitOptions(t *testing.T) { watch := watchOpts{ sync: false, diff --git a/cmd/argocd/commands/tree.go b/cmd/argocd/commands/tree.go new file mode 100644 index 0000000000000..d46e5375df130 --- /dev/null +++ b/cmd/argocd/commands/tree.go @@ -0,0 +1,167 @@ +package commands + +import ( + "fmt" + "strings" + "text/tabwriter" + "time" + + "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" + "github.com/argoproj/gitops-engine/pkg/health" + "k8s.io/apimachinery/pkg/util/duration" +) + +const ( + firstElemPrefix = `├─` + lastElemPrefix = `└─` + indent = " " + pipe = `│ ` +) + +func extractHealthStatusAndReason(node v1alpha1.ResourceNode) (healthStatus health.HealthStatusCode, reason string) { + if node.Health != nil { + healthStatus = node.Health.Status + reason = node.Health.Message + } + return +} + +func treeViewAppGet(prefix string, uidToNodeMap map[string]v1alpha1.ResourceNode, parentToChildMap map[string][]string, parent v1alpha1.ResourceNode, mapNodeNameToResourceState map[string]*resourceState, w *tabwriter.Writer) { + if mapNodeNameToResourceState[parent.Kind+"/"+parent.Name] != nil { + value := mapNodeNameToResourceState[parent.Kind+"/"+parent.Name] + _, _ = fmt.Fprintf(w, "%s%s\t%s\t%s\t%s\n", printPrefix(prefix), parent.Kind+"/"+value.Name, value.Status, value.Health, value.Message) + } else { + _, _ = fmt.Fprintf(w, "%s%s\t%s\t%s\t%s\n", printPrefix(prefix), parent.Kind+"/"+parent.Name, "", "", "") + } + chs := parentToChildMap[parent.UID] + for i, childUid := range chs { + var p string + switch i { + case len(chs) - 1: + p = prefix + lastElemPrefix + default: + p = prefix + firstElemPrefix + } + treeViewAppGet(p, uidToNodeMap, parentToChildMap, uidToNodeMap[childUid], mapNodeNameToResourceState, w) + } + +} + +func detailedTreeViewAppGet(prefix string, uidToNodeMap map[string]v1alpha1.ResourceNode, parentChildMap map[string][]string, parent v1alpha1.ResourceNode, mapNodeNameToResourceState map[string]*resourceState, w *tabwriter.Writer) { + healthStatus, reason := extractHealthStatusAndReason(parent) + var age = "" + if parent.CreatedAt != nil { + age = duration.HumanDuration(time.Since(parent.CreatedAt.Time)) + } + + if mapNodeNameToResourceState[parent.Kind+"/"+parent.Name] != nil { + value := mapNodeNameToResourceState[parent.Kind+"/"+parent.Name] + _, _ = fmt.Fprintf(w, "%s%s\t%s\t%s\t%s\t%s\t%s\n", printPrefix(prefix), parent.Kind+"/"+value.Name, value.Status, value.Health, age, value.Message, reason) + } else { + _, _ = fmt.Fprintf(w, "%s%s\t%s\t%s\t%s\t%s\t%s\n", printPrefix(prefix), parent.Kind+"/"+parent.Name, "", healthStatus, age, "", reason) + + } + chs := parentChildMap[parent.UID] + for i, child := range chs { + var p string + switch i { + case len(chs) - 1: + p = prefix + lastElemPrefix + default: + p = prefix + firstElemPrefix + } + detailedTreeViewAppGet(p, uidToNodeMap, parentChildMap, uidToNodeMap[child], mapNodeNameToResourceState, w) + } +} + +func treeViewAppResourcesNotOrphaned(prefix string, uidToNodeMap map[string]v1alpha1.ResourceNode, parentChildMap map[string][]string, parent v1alpha1.ResourceNode, w *tabwriter.Writer) { + if len(parent.ParentRefs) == 0 { + _, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", parent.Group, parent.Kind, parent.Namespace, parent.Name, "No") + } + chs := parentChildMap[parent.UID] + for i, child := range chs { + var p string + switch i { + case len(chs) - 1: + p = prefix + lastElemPrefix + default: + p = prefix + firstElemPrefix + } + treeViewAppResourcesNotOrphaned(p, uidToNodeMap, parentChildMap, uidToNodeMap[child], w) + } +} + +func treeViewAppResourcesOrphaned(prefix string, uidToNodeMap map[string]v1alpha1.ResourceNode, parentChildMap map[string][]string, parent v1alpha1.ResourceNode, w *tabwriter.Writer) { + _, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", parent.Group, parent.Kind, parent.Namespace, parent.Name, "Yes") + chs := parentChildMap[parent.UID] + for i, child := range chs { + var p string + switch i { + case len(chs) - 1: + p = prefix + lastElemPrefix + default: + p = prefix + firstElemPrefix + } + treeViewAppResourcesOrphaned(p, uidToNodeMap, parentChildMap, uidToNodeMap[child], w) + } +} + +func detailedTreeViewAppResourcesNotOrphaned(prefix string, uidToNodeMap map[string]v1alpha1.ResourceNode, parentChildMap map[string][]string, parent v1alpha1.ResourceNode, w *tabwriter.Writer) { + + if len(parent.ParentRefs) == 0 { + healthStatus, reason := extractHealthStatusAndReason(parent) + var age = "" + if parent.CreatedAt != nil { + age = duration.HumanDuration(time.Since(parent.CreatedAt.Time)) + } + _, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", parent.Group, parent.Kind, parent.Namespace, parent.Name, "No", age, healthStatus, reason) + } + chs := parentChildMap[parent.UID] + for i, child := range chs { + var p string + switch i { + case len(chs) - 1: + p = prefix + lastElemPrefix + default: + p = prefix + firstElemPrefix + } + detailedTreeViewAppResourcesNotOrphaned(p, uidToNodeMap, parentChildMap, uidToNodeMap[child], w) + } +} + +func detailedTreeViewAppResourcesOrphaned(prefix string, uidToNodeMap map[string]v1alpha1.ResourceNode, parentChildMap map[string][]string, parent v1alpha1.ResourceNode, w *tabwriter.Writer) { + healthStatus, reason := extractHealthStatusAndReason(parent) + var age = "" + if parent.CreatedAt != nil { + age = duration.HumanDuration(time.Since(parent.CreatedAt.Time)) + } + _, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", parent.Group, parent.Kind, parent.Namespace, parent.Name, "Yes", age, healthStatus, reason) + + chs := parentChildMap[parent.UID] + for i, child := range chs { + var p string + switch i { + case len(chs) - 1: + p = prefix + lastElemPrefix + default: + p = prefix + firstElemPrefix + } + detailedTreeViewAppResourcesOrphaned(p, uidToNodeMap, parentChildMap, uidToNodeMap[child], w) + } +} + +func printPrefix(p string) string { + + if strings.HasSuffix(p, firstElemPrefix) { + p = strings.Replace(p, firstElemPrefix, pipe, strings.Count(p, firstElemPrefix)-1) + } else { + p = strings.ReplaceAll(p, firstElemPrefix, pipe) + } + + if strings.HasSuffix(p, lastElemPrefix) { + p = strings.Replace(p, lastElemPrefix, strings.Repeat(" ", len([]rune(lastElemPrefix))), strings.Count(p, lastElemPrefix)-1) + } else { + p = strings.ReplaceAll(p, lastElemPrefix, strings.Repeat(" ", len([]rune(lastElemPrefix)))) + } + return p +} diff --git a/cmd/argocd/commands/tree_test.go b/cmd/argocd/commands/tree_test.go new file mode 100644 index 0000000000000..91ffb9b963d01 --- /dev/null +++ b/cmd/argocd/commands/tree_test.go @@ -0,0 +1,216 @@ +package commands + +import ( + "bytes" + "testing" + "text/tabwriter" + + "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" + "github.com/stretchr/testify/assert" +) + +func TestTreeViewAppGet(t *testing.T) { + var parent v1alpha1.ResourceNode + parent.ResourceRef = v1alpha1.ResourceRef{Group: "argoproj.io", Version: "", Kind: "Rollout", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo", UID: "87f3aab0-f634-4b2c-959a-7ddd30675ed0"} + objs := make(map[string]v1alpha1.ResourceNode) + objs["87f3aab0-f634-4b2c-959a-7ddd30675ed0"] = parent + var child v1alpha1.ResourceNode + child.ResourceRef = v1alpha1.ResourceRef{Group: "apps", Version: "v1", Kind: "ReplicaSet", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo-5dcd5457d5", UID: "75c30dce-1b66-414f-a86c-573a74be0f40"} + child.ParentRefs = []v1alpha1.ResourceRef{{Group: "argoproj.io", Version: "", Kind: "Rollout", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo", UID: "87f3aab0-f634-4b2c-959a-7ddd30675ed0"}} + + objs["75c30dce-1b66-414f-a86c-573a74be0f40"] = child + + childMapping := make(map[string][]string) + childMapping["87f3aab0-f634-4b2c-959a-7ddd30675ed0"] = []string{"75c30dce-1b66-414f-a86c-573a74be0f40"} + + stateMap := make(map[string]*resourceState) + stateMap["Rollout/numalogic-rollout-demo"] = &resourceState{ + Status: "Running", + Health: "Healthy", + Hook: "", + Message: "No Issues", + Name: "sandbox-rollout-numalogic-demo", + Kind: "Rollout", + Group: "argoproj.io", + } + + buf := &bytes.Buffer{} + w := tabwriter.NewWriter(buf, 0, 0, 2, ' ', 0) + treeViewAppGet("", objs, childMapping, parent, stateMap, w) + if err := w.Flush(); err != nil { + t.Fatal(err) + } + output := buf.String() + assert.Contains(t, output, "ReplicaSet") + assert.Contains(t, output, "Rollout") + assert.Contains(t, output, "numalogic-rollout") + assert.Contains(t, output, "Healthy") + assert.Contains(t, output, "No Issues") +} + +func TestTreeViewDetailedAppGet(t *testing.T) { + var parent v1alpha1.ResourceNode + parent.ResourceRef = v1alpha1.ResourceRef{Group: "argoproj.io", Version: "", Kind: "Rollout", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo", UID: "87f3aab0-f634-4b2c-959a-7ddd30675ed0"} + objs := make(map[string]v1alpha1.ResourceNode) + objs["87f3aab0-f634-4b2c-959a-7ddd30675ed0"] = parent + var child v1alpha1.ResourceNode + child.ResourceRef = v1alpha1.ResourceRef{Group: "apps", Version: "v1", Kind: "ReplicaSet", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo-5dcd5457d5", UID: "75c30dce-1b66-414f-a86c-573a74be0f40"} + child.ParentRefs = []v1alpha1.ResourceRef{{Group: "argoproj.io", Version: "", Kind: "Rollout", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo", UID: "87f3aab0-f634-4b2c-959a-7ddd30675ed0"}} + child.Health = &v1alpha1.HealthStatus{Status: "Degraded", Message: "Readiness Gate failed"} + objs["75c30dce-1b66-414f-a86c-573a74be0f40"] = child + + childMapping := make(map[string][]string) + childMapping["87f3aab0-f634-4b2c-959a-7ddd30675ed0"] = []string{"75c30dce-1b66-414f-a86c-573a74be0f40"} + + stateMap := make(map[string]*resourceState) + stateMap["Rollout/numalogic-rollout-demo"] = &resourceState{ + Status: "Running", + Health: "Healthy", + Hook: "", + Message: "No Issues", + Name: "sandbox-rollout-numalogic-demo", + Kind: "Rollout", + Group: "argoproj.io", + } + + buf := &bytes.Buffer{} + w := tabwriter.NewWriter(buf, 0, 0, 2, ' ', 0) + detailedTreeViewAppGet("", objs, childMapping, parent, stateMap, w) + if err := w.Flush(); err != nil { + t.Fatal(err) + } + + output := buf.String() + + assert.Contains(t, output, "ReplicaSet") + assert.Contains(t, output, "Rollout") + assert.Contains(t, output, "numalogic-rollout") + assert.Contains(t, output, "Healthy") + assert.Contains(t, output, "No Issues") + assert.Contains(t, output, "Degraded") + assert.Contains(t, output, "Readiness Gate failed") +} + +func TestTreeViewAppResources(t *testing.T) { + var parent v1alpha1.ResourceNode + parent.ResourceRef = v1alpha1.ResourceRef{Group: "argoproj.io", Version: "", Kind: "Rollout", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo", UID: "87f3aab0-f634-4b2c-959a-7ddd30675ed0"} + objs := make(map[string]v1alpha1.ResourceNode) + objs["87f3aab0-f634-4b2c-959a-7ddd30675ed0"] = parent + var child v1alpha1.ResourceNode + child.ResourceRef = v1alpha1.ResourceRef{Group: "apps", Version: "v1", Kind: "ReplicaSet", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo-5dcd5457d5", UID: "75c30dce-1b66-414f-a86c-573a74be0f40"} + child.ParentRefs = []v1alpha1.ResourceRef{{Group: "argoproj.io", Version: "", Kind: "Rollout", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo", UID: "87f3aab0-f634-4b2c-959a-7ddd30675ed0"}} + + objs["75c30dce-1b66-414f-a86c-573a74be0f40"] = child + + childMapping := make(map[string][]string) + childMapping["87f3aab0-f634-4b2c-959a-7ddd30675ed0"] = []string{"75c30dce-1b66-414f-a86c-573a74be0f40"} + + buf := &bytes.Buffer{} + w := tabwriter.NewWriter(buf, 0, 0, 2, ' ', 0) + + treeViewAppResourcesNotOrphaned("", objs, childMapping, parent, w) + + var orphan v1alpha1.ResourceNode + orphan.ResourceRef = v1alpha1.ResourceRef{Group: "apps", Version: "v1", Kind: "ReplicaSet", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo-5dcdnk457d5", UID: "75c30dce-1b66-41hf-a86c-573a74be0f40"} + objsOrphan := make(map[string]v1alpha1.ResourceNode) + objsOrphan["75c30dce-1b66-41hf-a86c-573a74be0f40"] = orphan + orphanchildMapping := make(map[string][]string) + orphanParent := orphan + + treeViewAppResourcesOrphaned("", objsOrphan, orphanchildMapping, orphanParent, w) + if err := w.Flush(); err != nil { + t.Fatal(err) + } + output := buf.String() + + assert.Contains(t, output, "ReplicaSet") + assert.Contains(t, output, "Rollout") + assert.Contains(t, output, "numalogic-rollout") + assert.Contains(t, output, "argoproj.io") + assert.Contains(t, output, "No") + assert.Contains(t, output, "Yes") + assert.Contains(t, output, "numalogic-rollout-demo-5dcdnk457d5") +} + +func TestTreeViewDetailedAppResources(t *testing.T) { + var parent v1alpha1.ResourceNode + parent.ResourceRef = v1alpha1.ResourceRef{Group: "argoproj.io", Version: "", Kind: "Rollout", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo", UID: "87f3aab0-f634-4b2c-959a-7ddd30675ed0"} + objs := make(map[string]v1alpha1.ResourceNode) + objs["87f3aab0-f634-4b2c-959a-7ddd30675ed0"] = parent + var child v1alpha1.ResourceNode + child.ResourceRef = v1alpha1.ResourceRef{Group: "apps", Version: "v1", Kind: "ReplicaSet", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo-5dcd5457d5", UID: "75c30dce-1b66-414f-a86c-573a74be0f40"} + child.ParentRefs = []v1alpha1.ResourceRef{{Group: "argoproj.io", Version: "", Kind: "Rollout", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo", UID: "87f3aab0-f634-4b2c-959a-7ddd30675ed0"}} + objs["75c30dce-1b66-414f-a86c-573a74be0f40"] = child + childMapping := make(map[string][]string) + childMapping["87f3aab0-f634-4b2c-959a-7ddd30675ed0"] = []string{"75c30dce-1b66-414f-a86c-573a74be0f40"} + buf := &bytes.Buffer{} + w := tabwriter.NewWriter(buf, 0, 0, 2, ' ', 0) + detailedTreeViewAppResourcesNotOrphaned("", objs, childMapping, parent, w) + var orphan v1alpha1.ResourceNode + orphan.ResourceRef = v1alpha1.ResourceRef{Group: "apps", Version: "v1", Kind: "ReplicaSet", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo-5dcdnk457d5", UID: "75c30dce-1b66-41hf-a86c-573a74be0f40"} + orphan.Health = &v1alpha1.HealthStatus{ + Status: "Degraded", + Message: "Readiness Gate failed", + } + objsOrphan := make(map[string]v1alpha1.ResourceNode) + objsOrphan["75c30dce-1b66-41hf-a86c-573a74be0f40"] = orphan + + orphanchildMapping := make(map[string][]string) + orphanParent := orphan + detailedTreeViewAppResourcesOrphaned("", objsOrphan, orphanchildMapping, orphanParent, w) + if err := w.Flush(); err != nil { + t.Fatal(err) + } + output := buf.String() + + assert.Contains(t, output, "ReplicaSet") + assert.Contains(t, output, "Rollout") + assert.Contains(t, output, "numalogic-rollout") + assert.Contains(t, output, "argoproj.io") + assert.Contains(t, output, "No") + assert.Contains(t, output, "Yes") + assert.Contains(t, output, "numalogic-rollout-demo-5dcdnk457d5") + assert.Contains(t, output, "Degraded") + assert.Contains(t, output, "Readiness Gate failed") +} + +func TestPrintPrefix(t *testing.T) { + tests := []struct { + input string + expected string + name string + }{ + { + input: "", + expected: "", + name: "empty string", + }, + { + input: firstElemPrefix, + expected: firstElemPrefix, + name: "only first element prefix", + }, + { + input: lastElemPrefix, + expected: lastElemPrefix, + name: "only last element prefix", + }, + { + input: firstElemPrefix + firstElemPrefix, + expected: pipe + firstElemPrefix, + name: "double first element prefix", + }, + { + input: firstElemPrefix + lastElemPrefix, + expected: pipe + lastElemPrefix, + name: "first then last element prefix", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got := printPrefix(test.input) + assert.Equal(t, test.expected, got) + }) + } +} diff --git a/docs/user-guide/commands/argocd_app_get.md b/docs/user-guide/commands/argocd_app_get.md index ac027526072bd..1269386486294 100644 --- a/docs/user-guide/commands/argocd_app_get.md +++ b/docs/user-guide/commands/argocd_app_get.md @@ -11,7 +11,7 @@ argocd app get APPNAME [flags] ``` --hard-refresh Refresh application data as well as target manifests cache -h, --help help for get - -o, --output string Output format. One of: json|yaml|wide (default "wide") + -o, --output string Output format. One of: json|yaml|wide|tree (default "wide") --refresh Refresh application data when retrieving --show-operation Show application operation --show-params Show application parameters and overrides diff --git a/docs/user-guide/commands/argocd_app_resources.md b/docs/user-guide/commands/argocd_app_resources.md index 936fe61229897..bfb698178f1ea 100644 --- a/docs/user-guide/commands/argocd_app_resources.md +++ b/docs/user-guide/commands/argocd_app_resources.md @@ -9,8 +9,9 @@ argocd app resources APPNAME [flags] ### Options ``` - -h, --help help for resources - --orphaned Lists only orphaned resources + -h, --help help for resources + --orphaned Lists only orphaned resources + --output string Provides the tree view of the resources ``` ### Options inherited from parent commands