From bdf62a62d0fa3fff03520fe271788f5466c26576 Mon Sep 17 00:00:00 2001 From: lufreita Date: Wed, 20 Aug 2025 22:51:44 -0300 Subject: [PATCH] OCM-1520 | feat: Add --hide-empty-columns flag to rosa list commands --- cmd/list/accessrequests/cmd.go | 47 ++++-- cmd/list/accessrequests/cmd_test.go | 2 - cmd/list/accountroles/cmd.go | 30 ++-- cmd/list/addon/cmd.go | 56 +++++-- cmd/list/breakglasscredential/cmd.go | 27 +++- cmd/list/cluster/cmd.go | 34 ++-- cmd/list/dnsdomains/cmd.go | 30 ++-- cmd/list/gates/cmd.go | 56 +++++-- cmd/list/iamserviceaccounts/cmd.go | 21 ++- cmd/list/idp/cmd.go | 46 ++++-- cmd/list/ingress/cmd.go | 37 +++-- cmd/list/instancetypes/cmd.go | 34 ++-- cmd/list/machinepool/cmd.go | 1 + cmd/list/machinepool/cmd_test.go | 37 +++-- cmd/list/ocmroles/cmd.go | 36 +++-- cmd/list/oidcconfig/cmd.go | 34 +++- cmd/list/operatorroles/cmd.go | 61 +++++-- cmd/list/region/cmd.go | 33 ++-- cmd/list/service/cmd.go | 30 +++- cmd/list/upgrade/cmd.go | 27 +++- .../rosa/list/access-request/command_args.yml | 1 + .../rosa/list/account-roles/command_args.yml | 1 + .../rosa/list/addons/command_args.yml | 1 + .../break-glass-credentials/command_args.yml | 1 + .../rosa/list/clusters/command_args.yml | 1 + .../rosa/list/dns-domain/command_args.yml | 1 + .../rosa/list/gates/command_args.yml | 1 + .../list/iamserviceaccounts/command_args.yml | 1 + .../rosa/list/idps/command_args.yml | 1 + .../rosa/list/ingresses/command_args.yml | 1 + .../rosa/list/machinepools/command_args.yml | 1 + .../list/managed-services/command_args.yml | 1 + .../rosa/list/ocm-roles/command_args.yml | 1 + .../rosa/list/oidc-config/command_args.yml | 1 + .../rosa/list/operator-roles/command_args.yml | 1 + .../rosa/list/regions/command_args.yml | 1 + .../rosa/list/upgrades/command_args.yml | 1 + pkg/machinepool/machinepool.go | 149 ++++++++++++------ pkg/machinepool/machinepool_test.go | 122 ++++++++------ pkg/output/hide_empty_columns.go | 18 +++ pkg/output/table_filter.go | 88 +++++++++++ 41 files changed, 791 insertions(+), 282 deletions(-) create mode 100644 pkg/output/hide_empty_columns.go create mode 100644 pkg/output/table_filter.go diff --git a/cmd/list/accessrequests/cmd.go b/cmd/list/accessrequests/cmd.go index 7d40f027ff..43b8231634 100644 --- a/cmd/list/accessrequests/cmd.go +++ b/cmd/list/accessrequests/cmd.go @@ -2,7 +2,6 @@ package accessrequests import ( "context" - "fmt" "os" "text/tabwriter" "time" @@ -38,6 +37,7 @@ func NewListAccessRequestsCommand() *cobra.Command { } output.AddFlag(cmd) + output.AddHideEmptyColumnsFlag(cmd) ocm.AddOptionalClusterFlag(cmd) return cmd } @@ -67,12 +67,34 @@ func ListAccessRequestsRunner() rosa.CommandRunner { } return nil } + + headers := []string{"STATE", "ID", "CLUSTER ID", "UPDATED AT"} + + var tableData [][]string + for _, accessRequest := range accessRequests { + row := []string{ + string(accessRequest.Status().State()), + accessRequest.ID(), + accessRequest.ClusterId(), + accessRequest.UpdatedAt().UTC().Format(time.UnixDate), + } + tableData = append(tableData, row) + } + + if output.ShouldHideEmptyColumns() { + tableData = output.RemoveEmptyColumns(headers, tableData) + } else { + tableData = append([][]string{headers}, tableData...) + } + writer := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0) - output, hasPending, pendingId := getAccessRequestsOutput(clusterId, accessRequests) - fmt.Fprint(writer, output) + output.BuildTable(writer, "\t", tableData) if err := writer.Flush(); err != nil { return err } + + hasPending, pendingId := checkForPendingRequests(clusterId, accessRequests) + if hasPending { r.Reporter.Infof("Run the following command to approve or deny the Access Request:\n\n"+ " rosa create decision --access-request %s --decision Approved\n"+ @@ -84,23 +106,22 @@ func ListAccessRequestsRunner() rosa.CommandRunner { } } -func getAccessRequestsOutput(clusterId string, accessRequests []*v1.AccessRequest) (string, bool, string) { - output := "STATE\tID\tCLUSTER ID\tUPDATED AT\n" +func checkForPendingRequests(clusterId string, accessRequests []*v1.AccessRequest) (bool, string) { hasPending := false id := "" for _, accessRequest := range accessRequests { - if accessRequest.Status().State() == v1.AccessRequestStatePending { + if accessRequest.Status().State() == v1.AccessRequestStatePending || + accessRequest.Status().State() == v1.AccessRequestStateApproved { hasPending = true if clusterId != "" { id = accessRequest.ID() } + // Once we find the first pending/approved request for the cluster, we can break + // since we only need one ID for the suggestion message + if clusterId != "" && hasPending { + break + } } - output += fmt.Sprintf("%s\t%s\t%s\t%s\n", - accessRequest.Status().State(), - accessRequest.ID(), - accessRequest.ClusterId(), - accessRequest.UpdatedAt().Format(time.UnixDate)) } - - return output, hasPending, id + return hasPending, id } diff --git a/cmd/list/accessrequests/cmd_test.go b/cmd/list/accessrequests/cmd_test.go index f7a2bb7ba7..c1679fead2 100644 --- a/cmd/list/accessrequests/cmd_test.go +++ b/cmd/list/accessrequests/cmd_test.go @@ -2,7 +2,6 @@ package accessrequests import ( "context" - "fmt" "net/http" "time" @@ -126,7 +125,6 @@ var _ = Describe("rosa attach policy", func() { Expect(err).NotTo(HaveOccurred()) stdOut, _ := t.StdOutReader.Read() - fmt.Println(stdOut) Expect(stdOut).To(Equal(accessRequestsOutput)) }) diff --git a/cmd/list/accountroles/cmd.go b/cmd/list/accountroles/cmd.go index 75ffc89659..1369c9f51b 100644 --- a/cmd/list/accountroles/cmd.go +++ b/cmd/list/accountroles/cmd.go @@ -17,7 +17,6 @@ limitations under the License. package accountroles import ( - "fmt" "os" "text/tabwriter" "time" @@ -55,6 +54,7 @@ func init() { "List only account-roles that are associated with the given version.", ) output.AddFlag(Cmd) + output.AddHideEmptyColumnsFlag(Cmd) } func run(_ *cobra.Command, _ []string) { @@ -108,23 +108,35 @@ func run(_ *cobra.Command, _ []string) { os.Exit(0) } - // Create the writer that will be used to print the tabulated results: - writer := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) - fmt.Fprintf(writer, "ROLE NAME\tROLE TYPE\tROLE ARN\tOPENSHIFT VERSION\tAWS Managed\n") + headers := []string{"ROLE NAME", "ROLE TYPE", "ROLE ARN", "OPENSHIFT VERSION", "AWS Managed"} + + var tableData [][]string for _, accountRole := range accountRoles { awsManaged := "No" if accountRole.ManagedPolicy { awsManaged = "Yes" } - fmt.Fprintf( - writer, - "%s\t%s\t%s\t%s\t%s\n", + row := []string{ accountRole.RoleName, accountRole.RoleType, accountRole.RoleARN, accountRole.Version, awsManaged, - ) + } + tableData = append(tableData, row) + } + + if output.ShouldHideEmptyColumns() { + tableData = output.RemoveEmptyColumns(headers, tableData) + } else { + tableData = append([][]string{headers}, tableData...) } - writer.Flush() + + writer := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + output.BuildTable(writer, "\t", tableData) + if err := writer.Flush(); err != nil { + _ = r.Reporter.Errorf("Failed to flush output: %v", err) + os.Exit(1) + } + } diff --git a/cmd/list/addon/cmd.go b/cmd/list/addon/cmd.go index bdac62cf2f..31d4691c40 100644 --- a/cmd/list/addon/cmd.go +++ b/cmd/list/addon/cmd.go @@ -17,7 +17,6 @@ limitations under the License. package addon import ( - "fmt" "os" "text/tabwriter" @@ -56,6 +55,7 @@ func init() { ) output.AddFlag(Cmd) + output.AddHideEmptyColumnsFlag(Cmd) } // When no specific cluster id is provided by the user, this function lists all available AddOns @@ -81,18 +81,34 @@ func listAllAddOns(r *rosa.Runtime) { os.Exit(0) } - // Create the writer that will be used to print the tabulated results: - writer := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) - fmt.Fprintf(writer, "ID\t\tNAME\t\tAVAILABILITY\n") + headers := []string{"ID", "NAME", "AVAILABILITY"} + var tableData [][]string for _, addOnResource := range addOnResources { availability := "unavailable" if addOnResource.Available { availability = "available" } - fmt.Fprintf(writer, "%s\t\t%s\t\t%s\n", addOnResource.AddOn.ID(), addOnResource.AddOn.Name(), availability) + row := []string{ + addOnResource.AddOn.ID(), + addOnResource.AddOn.Name(), + availability, + } + tableData = append(tableData, row) + } + + if output.ShouldHideEmptyColumns() { + tableData = output.RemoveEmptyColumns(headers, tableData) + } else { + tableData = append([][]string{headers}, tableData...) } - writer.Flush() + writer := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + output.BuildTable(writer, "\t\t", tableData) + + if err := writer.Flush(); err != nil { + _ = r.Reporter.Errorf("Failed to flush output: %v", err) + os.Exit(1) + } os.Exit(0) } @@ -142,13 +158,31 @@ func listClusterAddOns(clusterKey string, r *rosa.Runtime) { os.Exit(0) } - // Create the writer that will be used to print the tabulated results: - writer := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) - fmt.Fprintf(writer, "ID\t\tNAME\t\tSTATE\n") + headers := []string{"ID", "NAME", "STATE"} + + var tableData [][]string for _, clusterAddOn := range clusterAddOns { - fmt.Fprintf(writer, "%s\t\t%s\t\t%s\n", clusterAddOn.ID, clusterAddOn.Name, clusterAddOn.State) + row := []string{ + clusterAddOn.ID, + clusterAddOn.Name, + clusterAddOn.State, + } + tableData = append(tableData, row) + } + + if output.ShouldHideEmptyColumns() { + tableData = output.RemoveEmptyColumns(headers, tableData) + } else { + tableData = append([][]string{headers}, tableData...) + } + + writer := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + output.BuildTable(writer, "\t\t", tableData) + + if err := writer.Flush(); err != nil { + _ = r.Reporter.Errorf("Failed to flush output: %v", err) + os.Exit(1) } - writer.Flush() } func run(_ *cobra.Command, _ []string) { diff --git a/cmd/list/breakglasscredential/cmd.go b/cmd/list/breakglasscredential/cmd.go index d4b0ef9a5f..00fb8c8b57 100644 --- a/cmd/list/breakglasscredential/cmd.go +++ b/cmd/list/breakglasscredential/cmd.go @@ -27,6 +27,7 @@ var Cmd = &cobra.Command{ func init() { ocm.AddClusterFlag(Cmd) output.AddFlag(Cmd) + output.AddHideEmptyColumnsFlag(Cmd) } func run(cmd *cobra.Command, _ []string) { @@ -69,18 +70,28 @@ func runWithRuntime(r *rosa.Runtime, cmd *cobra.Command) error { return nil } - // Create the writer that will be used to print the tabulated results: - writer := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0) - - fmt.Fprintf(writer, "ID\tUSERNAME\tSTATUS\n") + headers := []string{"ID", "USERNAME", "STATUS"} + var tableData [][]string for _, credential := range breakGlassCredentials { - fmt.Fprintf(writer, "%s\t%s\t%s\n", + row := []string{ credential.ID(), credential.Username(), - credential.Status(), - ) + string(credential.Status()), + } + tableData = append(tableData, row) + } + + if output.ShouldHideEmptyColumns() { + tableData = output.RemoveEmptyColumns(headers, tableData) + } else { + tableData = append([][]string{headers}, tableData...) } - writer.Flush() + writer := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0) + output.BuildTable(writer, "\t", tableData) + + if err := writer.Flush(); err != nil { + return err + } return nil } diff --git a/cmd/list/cluster/cmd.go b/cmd/list/cluster/cmd.go index a47a3e9da5..7ea2d1bfb8 100644 --- a/cmd/list/cluster/cmd.go +++ b/cmd/list/cluster/cmd.go @@ -17,7 +17,6 @@ limitations under the License. package cluster import ( - "fmt" "os" "text/tabwriter" @@ -52,6 +51,7 @@ func init() { flags.SortFlags = false output.AddFlag(Cmd) + output.AddHideEmptyColumnsFlag(Cmd) flags.BoolVarP(&args.listAll, "all", "a", false, "List all clusters across different AWS "+ "accounts under the same Red Hat organization") flags.StringVar(&args.accountRoleArn, "account-role-arn", "", "List all clusters "+ @@ -107,25 +107,37 @@ func run(_ *cobra.Command, _ []string) { os.Exit(0) } - // Create the writer that will be used to print the tabulated results: - writer := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) - fmt.Fprintf(writer, "ID\tNAME\tSTATE\tTOPOLOGY\n") + headers := []string{"ID", "NAME", "STATE", "TOPOLOGY"} + var tableData [][]string for _, cluster := range clusters { - typeOutput := "Classic" + typeOutput := "" if cluster.AWS() != nil && cluster.AWS().STS() != nil && cluster.AWS().STS().Enabled() { typeOutput = "Classic (STS)" } if cluster.Hypershift().Enabled() { typeOutput = "Hosted CP" } - fmt.Fprintf( - writer, - "%s\t%s\t%s\t%s\n", + + row := []string{ cluster.ID(), cluster.Name(), - cluster.State(), + string(cluster.State()), typeOutput, - ) + } + tableData = append(tableData, row) + } + + if output.ShouldHideEmptyColumns() { + tableData = output.RemoveEmptyColumns(headers, tableData) + } else { + tableData = append([][]string{headers}, tableData...) + } + + writer := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + output.BuildTable(writer, "\t", tableData) + + if err := writer.Flush(); err != nil { + _ = r.Reporter.Errorf("Failed to flush output: %v", err) + os.Exit(1) } - writer.Flush() } diff --git a/cmd/list/dnsdomains/cmd.go b/cmd/list/dnsdomains/cmd.go index 1cf80742fb..c9795e49be 100644 --- a/cmd/list/dnsdomains/cmd.go +++ b/cmd/list/dnsdomains/cmd.go @@ -17,7 +17,6 @@ limitations under the License. package dnsdomains import ( - "fmt" "os" "text/tabwriter" "time" @@ -64,6 +63,7 @@ func init() { ) output.AddFlag(Cmd) + output.AddHideEmptyColumnsFlag(Cmd) } func run(_ *cobra.Command, _ []string) { @@ -99,24 +99,36 @@ func run(_ *cobra.Command, _ []string) { os.Exit(0) } - // Create the writer that will be used to print the tabulated results: - writer := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0) - - fmt.Fprintf(writer, "ID\tCLUSTER ID\tRESERVED TIME\tUSER DEFINED\tARCHITECTURE\n") + headers := []string{"ID", "CLUSTER ID", "RESERVED TIME", "USER DEFINED", "ARCHITECTURE"} + var tableData [][]string for _, dnsdomain := range dnsDomains { userDefined := "No" if dnsdomain.UserDefined() { userDefined = "Yes" } - fmt.Fprintf(writer, "%s\t%s\t%s\t%s\t%s\n", + row := []string{ dnsdomain.ID(), dnsdomain.Cluster().ID(), dnsdomain.ReservedAtTimestamp().Format(time.RFC3339), userDefined, - dnsdomain.ClusterArch(), - ) + string(dnsdomain.ClusterArch()), + } + tableData = append(tableData, row) + } + + if output.ShouldHideEmptyColumns() { + tableData = output.RemoveEmptyColumns(headers, tableData) + } else { + tableData = append([][]string{headers}, tableData...) + } + + writer := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0) + output.BuildTable(writer, "\t", tableData) + + if err := writer.Flush(); err != nil { + _ = r.Reporter.Errorf("Failed to flush output: %v", err) + os.Exit(1) } - writer.Flush() } func filterByClusterArch(domains []*v1.DNSDomain, arch v1.ClusterArchitecture) []*v1.DNSDomain { diff --git a/cmd/list/gates/cmd.go b/cmd/list/gates/cmd.go index 18570402b5..13e66fc318 100644 --- a/cmd/list/gates/cmd.go +++ b/cmd/list/gates/cmd.go @@ -86,6 +86,7 @@ func init() { Cmd.MarkFlagRequired("version") output.AddFlag(Cmd) + output.AddHideEmptyColumnsFlag(Cmd) } const ( @@ -183,33 +184,62 @@ func run(_ *cobra.Command, _ []string) { os.Exit(0) } - // Create the writer that will be used to print the tabulated results: cols, _ := consolesize.GetConsoleSize() descriptionSize := float64(cols) * 0.30 - writer := tabwriter.NewWriter(os.Stdout, 0, 8, 2, ' ', 0) - fmt.Fprintln(writer, "Gate Description\tSTS\tOCP Version\tDocumentation URL\t") + + headers := []string{"Gate Description", "STS", "OCP Version", "Documentation URL"} + var tableData [][]string for _, gate := range versionGates { wrappedDescription := wordWrap(strings.TrimSuffix(gate.Description(), "\n"), int(descriptionSize)) + lines := strings.Split(wrappedDescription, "\n") - for i, line := range strings.Split(wrappedDescription, "\n") { + for i, line := range lines { if i == 0 { - fmt.Fprintf(writer, - "%s\t%t\t%s\t%s\t\n", + // First line has all data + row := []string{ line, - gate.STSOnly(), + fmt.Sprintf("%t", gate.STSOnly()), gate.VersionRawIDPrefix(), gate.DocumentationURL(), - ) + } + tableData = append(tableData, row) } else { - fmt.Fprintf(writer, - "%s\t \t \t \t\n", - line, - ) + // Continuation lines have description only + row := []string{line, "", "", ""} + tableData = append(tableData, row) } } } - writer.Flush() + + var finalTableData [][]string + + if output.ShouldHideEmptyColumns() { + finalTableData = output.RemoveEmptyColumns(headers, tableData) + } else { + finalTableData = append([][]string{headers}, tableData...) + } + + writer := tabwriter.NewWriter(os.Stdout, 0, 8, 2, ' ', 0) + + headerLine := strings.Join(finalTableData[0], "\t") + "\t" + fmt.Fprintln(writer, headerLine) + + for i := 1; i < len(finalTableData); i++ { + row := finalTableData[i] + if output.ShouldHideEmptyColumns() || len(row) < 2 || row[1] != "" { + // Normal row or when hiding empty columns + fmt.Fprintf(writer, "%s\t\n", strings.Join(row, "\t")) + } else { + // Continuation row - preserve spacing (only when not hiding empty columns) + fmt.Fprintf(writer, "%s\t \t \t \t\n", row[0]) + } + } + + if err := writer.Flush(); err != nil { + _ = r.Reporter.Errorf("Failed to flush output: %v", err) + os.Exit(1) + } } func parseMajorMinor(version string) (string, error) { diff --git a/cmd/list/iamserviceaccounts/cmd.go b/cmd/list/iamserviceaccounts/cmd.go index 7510de5b9f..ae415b0c1c 100644 --- a/cmd/list/iamserviceaccounts/cmd.go +++ b/cmd/list/iamserviceaccounts/cmd.go @@ -63,6 +63,7 @@ func NewListIamServiceAccountsCommand() *cobra.Command { ) output.AddFlag(cmd) + output.AddHideEmptyColumnsFlag(cmd) return cmd } @@ -130,25 +131,33 @@ func ListIamServiceAccountsRunner(userOptions *ListIamServiceAccountsUserOptions return nil } - // Print table - writer := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) - fmt.Fprintln(writer, "NAME\tARN\tCLUSTER\tNAMESPACE\tSERVICE ACCOUNT\tCREATED") - + headers := []string{"NAME", "ARN", "CLUSTER", "NAMESPACE", "SERVICE ACCOUNT", "CREATED"} + var tableData [][]string for _, role := range serviceAccountRoles { created := "" if role.CreatedDate != nil { created = role.CreatedDate.Format("2006-01-02 15:04:05") } - fmt.Fprintf(writer, "%s\t%s\t%s\t%s\t%s\t%s\n", + row := []string{ role.RoleName, role.ARN, role.Cluster, role.Namespace, role.ServiceAccount, created, - ) + } + tableData = append(tableData, row) } + if output.ShouldHideEmptyColumns() { + tableData = output.RemoveEmptyColumns(headers, tableData) + } else { + tableData = append([][]string{headers}, tableData...) + } + + writer := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + output.BuildTable(writer, "\t", tableData) + return writer.Flush() } } diff --git a/cmd/list/idp/cmd.go b/cmd/list/idp/cmd.go index 76d8bdac48..fa923f09b3 100644 --- a/cmd/list/idp/cmd.go +++ b/cmd/list/idp/cmd.go @@ -17,7 +17,6 @@ limitations under the License. package idp import ( - "fmt" "os" "text/tabwriter" @@ -43,6 +42,7 @@ var Cmd = &cobra.Command{ func init() { ocm.AddClusterFlag(Cmd) output.AddFlag(Cmd) + output.AddHideEmptyColumnsFlag(Cmd) } func run(_ *cobra.Command, _ []string) { @@ -85,19 +85,41 @@ func run(_ *cobra.Command, _ []string) { os.Exit(0) } - // Create the writer that will be used to print the tabulated results: - writer := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) - if len(idps) == 1 && !ocm.HasAuthURLSupport(idps[0]) { - fmt.Fprintf(writer, "NAME\t\tTYPE\n") - } else { - fmt.Fprintf(writer, "NAME\t\tTYPE\t\tAUTH URL\n") + includeAuthURL := !(len(idps) == 1 && !ocm.HasAuthURLSupport(idps[0])) + + headers := []string{"NAME", "TYPE"} + if includeAuthURL { + headers = append(headers, "AUTH URL") } + + var tableData [][]string for _, idp := range idps { - oauthURL, err := ocm.GetOAuthURL(cluster, idp) - if err != nil { - r.Reporter.Warnf("Error building OAuth URL for %s: %v", idp.Name(), err) + row := []string{ + idp.Name(), + ocm.IdentityProviderType(idp), + } + + if includeAuthURL { + oauthURL, err := ocm.GetOAuthURL(cluster, idp) + if err != nil { + r.Reporter.Warnf("Error building OAuth URL for %s: %v", idp.Name(), err) + } + row = append(row, oauthURL) } - fmt.Fprintf(writer, "%s\t\t%s\t\t%s\n", idp.Name(), ocm.IdentityProviderType(idp), oauthURL) + tableData = append(tableData, row) + } + + if output.ShouldHideEmptyColumns() { + tableData = output.RemoveEmptyColumns(headers, tableData) + } else { + tableData = append([][]string{headers}, tableData...) + } + + writer := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + output.BuildTable(writer, "\t\t", tableData) + + if err := writer.Flush(); err != nil { + _ = r.Reporter.Errorf("Failed to flush output: %v", err) + os.Exit(1) } - writer.Flush() } diff --git a/cmd/list/ingress/cmd.go b/cmd/list/ingress/cmd.go index 719d82f4b8..21883a285f 100644 --- a/cmd/list/ingress/cmd.go +++ b/cmd/list/ingress/cmd.go @@ -45,6 +45,7 @@ var Cmd = &cobra.Command{ func init() { ocm.AddClusterFlag(Cmd) output.AddFlag(Cmd) + output.AddHideEmptyColumnsFlag(Cmd) } func run(_ *cobra.Command, _ []string) { @@ -82,25 +83,37 @@ func run(_ *cobra.Command, _ []string) { os.Exit(0) } - // Create the writer that will be used to print the tabulated results: - writer := tabwriter.NewWriter(os.Stdout, 2, 4, 2, ' ', 0) - - fmt.Fprintf(writer, "ID\tAPPLICATION ROUTER\tPRIVATE\tDEFAULT\tROUTE SELECTORS\tLB-TYPE"+ - "\tEXCLUDED NAMESPACE\tWILDCARD POLICY\tNAMESPACE OWNERSHIP\n") + headers := []string{"ID", "APPLICATION ROUTER", "PRIVATE", "DEFAULT", "ROUTE SELECTORS", + "LB-TYPE", "EXCLUDED NAMESPACE", "WILDCARD POLICY", "NAMESPACE OWNERSHIP"} + var tableData [][]string for _, ingress := range ingresses { - fmt.Fprintf(writer, "%s\thttps://%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", + row := []string{ ingress.ID(), - ingress.DNSName(), + fmt.Sprintf("https://%s", ingress.DNSName()), isPrivate(ingress.Listening()), isDefault(ingress), printRouteSelectors(ingress), - ingress.LoadBalancerType(), + string(ingress.LoadBalancerType()), helper.SliceToSortedString(ingress.ExcludedNamespaces()), - ingress.RouteWildcardPolicy(), - ingress.RouteNamespaceOwnershipPolicy(), - ) + string(ingress.RouteWildcardPolicy()), + string(ingress.RouteNamespaceOwnershipPolicy()), + } + tableData = append(tableData, row) + } + + if output.ShouldHideEmptyColumns() { + tableData = output.RemoveEmptyColumns(headers, tableData) + } else { + tableData = append([][]string{headers}, tableData...) + } + + writer := tabwriter.NewWriter(os.Stdout, 2, 4, 2, ' ', 0) + output.BuildTable(writer, "\t", tableData) + + if err := writer.Flush(); err != nil { + _ = r.Reporter.Errorf("Failed to flush output: %v", err) + os.Exit(1) } - writer.Flush() } func isPrivate(listeningMethod cmv1.ListeningMethod) string { diff --git a/cmd/list/instancetypes/cmd.go b/cmd/list/instancetypes/cmd.go index 235d317439..3341fed144 100644 --- a/cmd/list/instancetypes/cmd.go +++ b/cmd/list/instancetypes/cmd.go @@ -48,6 +48,7 @@ func makeCmd() *cobra.Command { Args: cobra.NoArgs, } + output.AddHideEmptyColumnsFlag(cmd) return cmd } @@ -172,23 +173,34 @@ func runWithRuntime(r *rosa.Runtime, cmd *cobra.Command) error { return fmt.Errorf("There are no machine types supported for your account. Contact Red Hat support.") } - // Create the writer that will be used to print the tabulated results: - writer := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) - fmt.Fprintf(writer, "ID\tCATEGORY\tCPU_CORES\tMEMORY\n") - + headers := []string{"ID", "CATEGORY", "CPU_CORES", "MEMORY"} + var tableData [][]string for _, machine := range machineTypes.Items { if !machine.Available { continue } availableMachine := machine.MachineType - fmt.Fprintf(writer, - "%s\t%s\t%d\t%s\n", - availableMachine.ID(), availableMachine.Category(), int(availableMachine.CPU().Value()), - ByteCountIEC(int(availableMachine.Memory().Value()), - availableMachine.Memory().Unit()), - ) + row := []string{ + availableMachine.ID(), + string(availableMachine.Category()), + fmt.Sprintf("%d", int(availableMachine.CPU().Value())), + ByteCountIEC(int(availableMachine.Memory().Value()), availableMachine.Memory().Unit()), + } + tableData = append(tableData, row) + } + + if output.ShouldHideEmptyColumns() { + tableData = output.RemoveEmptyColumns(headers, tableData) + } else { + tableData = append([][]string{headers}, tableData...) + } + + writer := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + output.BuildTable(writer, "\t", tableData) + + if err := writer.Flush(); err != nil { + return err } - writer.Flush() return nil } diff --git a/cmd/list/machinepool/cmd.go b/cmd/list/machinepool/cmd.go index 0394cae3c6..23b62b02f4 100644 --- a/cmd/list/machinepool/cmd.go +++ b/cmd/list/machinepool/cmd.go @@ -87,6 +87,7 @@ func NewListMachinePoolCommand() *cobra.Command { ) output.AddFlag(cmd) + output.AddHideEmptyColumnsFlag(cmd) ocm.AddClusterFlag(cmd) return cmd } diff --git a/cmd/list/machinepool/cmd_test.go b/cmd/list/machinepool/cmd_test.go index 5d98a88e93..1a03a7d547 100644 --- a/cmd/list/machinepool/cmd_test.go +++ b/cmd/list/machinepool/cmd_test.go @@ -18,20 +18,20 @@ import ( const ( nodePoolName = "nodepool85" clusterId = "24vf9iitg3p6tlml88iml6j6mu095mh8" - singleNodePoolOutput = "ID AUTOSCALING REPLICAS INSTANCE TYPE LABELS TAINTS AVAILABILITY ZONE SUBNET DISK SIZE VERSION AUTOREPAIR \n" + - "nodepool85 No /0 m5.xlarge us-east-1a default 4.12.24 No \n" + singleNodePoolOutput = "ID AUTOSCALING REPLICAS INSTANCE TYPE LABELS TAINTS AVAILABILITY ZONE SUBNET DISK SIZE VERSION AUTOREPAIR\n" + + "nodepool85 No /0 m5.xlarge us-east-1a default 4.12.24 No\n" - singleMachinePoolOutput = "ID AUTOSCALING REPLICAS INSTANCE TYPE AVAILABILITY ZONES SPOT INSTANCES DISK SIZE\n" + - "nodepool85 No 0 m5.xlarge us-east-1a, us-east-1b, us-east-1c Yes (max $5) default\n" + singleMachinePoolOutput = "ID AUTOSCALING REPLICAS INSTANCE TYPE LABELS TAINTS AVAILABILITY ZONES SUBNETS SPOT INSTANCES DISK SIZE SG IDS\n" + + "nodepool85 No 0 m5.xlarge us-east-1a, us-east-1b, us-east-1c Yes (max $5) default \n" - multipleMachinePoolOutput = "ID AUTOSCALING REPLICAS INSTANCE TYPE LABELS TAINTS AVAILABILITY ZONES SPOT INSTANCES DISK SIZE\n" + - "nodepool85 No 0 m5.xlarge us-east-1a, us-east-1b, us-east-1c Yes (max $5) default\n" + - "nodepool852 No 0 m5.xlarge test=label us-east-1a, us-east-1b, us-east-1c Yes (max $5) default\n" + - "nodepool853 Yes 1-100 m5.xlarge test=label test=taint: us-east-1a, us-east-1b, us-east-1c Yes (max $5) default\n" + multipleMachinePoolOutput = "ID AUTOSCALING REPLICAS INSTANCE TYPE LABELS TAINTS AVAILABILITY ZONES SUBNETS SPOT INSTANCES DISK SIZE SG IDS\n" + + "nodepool85 No 0 m5.xlarge us-east-1a, us-east-1b, us-east-1c Yes (max $5) default \n" + + "nodepool852 No 0 m5.xlarge test=label us-east-1a, us-east-1b, us-east-1c Yes (max $5) default \n" + + "nodepool853 Yes 1-100 m5.xlarge test=label test=taint: us-east-1a, us-east-1b, us-east-1c Yes (max $5) default \n" - multipleNodePoolsOutput = "ID AUTOSCALING REPLICAS INSTANCE TYPE LABELS TAINTS AVAILABILITY ZONE SUBNET DISK SIZE VERSION AUTOREPAIR \n" + - "nodepool85 No /0 m5.xlarge us-east-1a default 4.12.24 No \n" + - "nodepool852 Yes /100-1000 m5.xlarge test=label us-east-1a default 4.12.24 No \n" + multipleNodePoolsOutput = "ID AUTOSCALING REPLICAS INSTANCE TYPE LABELS TAINTS AVAILABILITY ZONE SUBNET DISK SIZE VERSION AUTOREPAIR\n" + + "nodepool85 No /0 m5.xlarge us-east-1a default 4.12.24 No\n" + + "nodepool852 Yes /100-1000 m5.xlarge test=label us-east-1a default 4.12.24 No\n" ) var _ = Describe("List machine pool", func() { @@ -144,17 +144,20 @@ var _ = Describe("List machine pool", func() { Expect(err).ToNot(HaveOccurred()) err = cmd.Flag("all").Value.Set("true") Expect(err).ToNot(HaveOccurred()) + err = cmd.Flag("hide-empty-columns").Value.Set("true") + Expect(err).ToNot(HaveOccurred()) err = runner(context.Background(), t.RosaRuntime, cmd, []string{}) Expect(err).ToNot(HaveOccurred()) stdout, err := t.StdOutReader.Read() Expect(err).ToNot(HaveOccurred()) - // With --all, should include columns like SUBNETS and SG IDS even if empty - Expect(stdout).To(ContainSubstring("SUBNETS")) - Expect(stdout).To(ContainSubstring("SG IDS")) + // With --all, should show the 3 special columns (empty columns are still hidden) Expect(stdout).To(ContainSubstring("AZ TYPE")) Expect(stdout).To(ContainSubstring("WIN-LI ENABLED")) Expect(stdout).To(ContainSubstring("DEDICATED HOST")) + // Empty columns should NOT be shown even with --all + Expect(stdout).ToNot(ContainSubstring("SUBNETS")) + Expect(stdout).ToNot(ContainSubstring("SG IDS")) }) It("Shows AZ TYPE column when --az-type flag is used", func() { @@ -299,7 +302,7 @@ var _ = Describe("List machine pool", func() { Expect(stdout).To(ContainSubstring("sg-1, sg-2")) }) - It("Hides empty columns by default", func() { + It("Hides empty columns when --hide-empty-columns is used", func() { t.ApiServer.AppendHandlers(RespondWithJSON(http.StatusOK, classicClusterReady)) t.ApiServer.AppendHandlers(RespondWithJSON(http.StatusOK, mpResponse)) runner := ListMachinePoolRunner() @@ -308,12 +311,14 @@ var _ = Describe("List machine pool", func() { cmd := NewListMachinePoolCommand() err = cmd.Flag("cluster").Value.Set(clusterId) Expect(err).ToNot(HaveOccurred()) + err = cmd.Flag("hide-empty-columns").Value.Set("true") + Expect(err).ToNot(HaveOccurred()) err = runner(context.Background(), t.RosaRuntime, cmd, []string{}) Expect(err).ToNot(HaveOccurred()) stdout, err := t.StdOutReader.Read() Expect(err).ToNot(HaveOccurred()) - // Should not include empty columns like SUBNETS and SG IDS by default + // Should not include empty columns like SUBNETS and SG IDS when flag is set Expect(stdout).ToNot(ContainSubstring("SUBNETS")) Expect(stdout).ToNot(ContainSubstring("SG IDS")) Expect(stdout).ToNot(ContainSubstring("LABELS")) diff --git a/cmd/list/ocmroles/cmd.go b/cmd/list/ocmroles/cmd.go index 1336777fab..c03326063f 100644 --- a/cmd/list/ocmroles/cmd.go +++ b/cmd/list/ocmroles/cmd.go @@ -44,6 +44,7 @@ rosa list ocm-roles`, func init() { output.AddFlag(Cmd) + output.AddHideEmptyColumnsFlag(Cmd) } func run(_ *cobra.Command, _ []string) { @@ -84,20 +85,37 @@ func run(_ *cobra.Command, _ []string) { os.Exit(0) } - // Create the writer that will be used to print the tabulated results: - writer := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) - fmt.Fprint(writer, "ROLE NAME\tROLE ARN\tLINKED\tADMIN\tAWS Managed\n") + headers := []string{"ROLE NAME", "ROLE ARN", "LINKED", "ADMIN", "AWS Managed"} + var tableData [][]string for _, ocmRole := range ocmRoles { - var awsManaged string + awsManaged := "No" if ocmRole.ManagedPolicy { awsManaged = "Yes" - } else { - awsManaged = "No" } - fmt.Fprintf(writer, "%s\t%s\t%s\t%s\t%s\n", ocmRole.RoleName, ocmRole.RoleARN, ocmRole.Linked, ocmRole.Admin, - awsManaged) + row := []string{ + ocmRole.RoleName, + ocmRole.RoleARN, + ocmRole.Linked, + ocmRole.Admin, + awsManaged, + } + tableData = append(tableData, row) } - writer.Flush() + + if output.ShouldHideEmptyColumns() { + tableData = output.RemoveEmptyColumns(headers, tableData) + } else { + tableData = append([][]string{headers}, tableData...) + } + + writer := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + output.BuildTable(writer, "\t", tableData) + + if err := writer.Flush(); err != nil { + _ = r.Reporter.Errorf("Failed to flush output: %v", err) + os.Exit(1) + } + } func listOCMRoles(r *rosa.Runtime) ([]aws.Role, error) { diff --git a/cmd/list/oidcconfig/cmd.go b/cmd/list/oidcconfig/cmd.go index a81111382e..889e94474f 100644 --- a/cmd/list/oidcconfig/cmd.go +++ b/cmd/list/oidcconfig/cmd.go @@ -40,6 +40,7 @@ var Cmd = &cobra.Command{ func init() { output.AddFlag(Cmd) + output.AddHideEmptyColumnsFlag(Cmd) } func run(_ *cobra.Command, _ []string) { @@ -68,17 +69,36 @@ func run(_ *cobra.Command, _ []string) { os.Exit(0) } - // Create the writer that will be used to print the tabulated results: - writer := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0) + // Define headers once + headers := []string{"ID", "MANAGED", "ISSUER URL", "SECRET ARN"} - fmt.Fprintf(writer, "ID\tMANAGED\tISSUER URL\tSECRET ARN\n") + // Prepare table data + var tableData [][]string for _, oidcConfig := range oidcConfigs { - fmt.Fprintf(writer, "%s\t%v\t%s\t%s\n", + row := []string{ oidcConfig.ID(), - oidcConfig.Managed(), + fmt.Sprintf("%v", oidcConfig.Managed()), oidcConfig.IssuerUrl(), oidcConfig.SecretArn(), - ) + } + tableData = append(tableData, row) + } + + // Process headers and data if hiding empty columns + if output.ShouldHideEmptyColumns() { + tableData = output.RemoveEmptyColumns(headers, tableData) + } else { + tableData = append([][]string{headers}, tableData...) + } + + // Print the table + writer := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0) + output.BuildTable(writer, "\t", tableData) + + // Check for flush errors + if err := writer.Flush(); err != nil { + _ = r.Reporter.Errorf("Failed to flush output: %v", err) + os.Exit(1) } - writer.Flush() + } diff --git a/cmd/list/operatorroles/cmd.go b/cmd/list/operatorroles/cmd.go index 1bddb2ab19..cc842ff856 100644 --- a/cmd/list/operatorroles/cmd.go +++ b/cmd/list/operatorroles/cmd.go @@ -74,6 +74,7 @@ func init() { interactive.AddFlag(flags) ocm.AddOptionalClusterFlag(Cmd) output.AddFlag(Cmd) + output.AddHideEmptyColumnsFlag(Cmd) } func run(cmd *cobra.Command, _ []string) { @@ -165,16 +166,34 @@ func run(cmd *cobra.Command, _ []string) { } } if args.prefix == "" { - fmt.Fprintf(writer, "ROLE PREFIX\tAMOUNT IN BUNDLE\n") + // Define headers once + headers := []string{"ROLE PREFIX", "AMOUNT IN BUNDLE"} + + // Prepare table data + var tableData [][]string for _, key := range prefixes { - fmt.Fprintf( - writer, - "%s\t%d\n", + row := []string{ key, - len(operatorsMap[key]), - ) + fmt.Sprintf("%d", len(operatorsMap[key])), + } + tableData = append(tableData, row) + } + + // Process headers and data if hiding empty columns + if output.ShouldHideEmptyColumns() { + tableData = output.RemoveEmptyColumns(headers, tableData) + } else { + tableData = append([][]string{headers}, tableData...) + } + + // Print the table + output.BuildTable(writer, "\t", tableData) + + // Check for flush errors + if err := writer.Flush(); err != nil { + _ = r.Reporter.Errorf("Failed to flush output: %v", err) + os.Exit(1) } - writer.Flush() if !interactive.Enabled() { os.Exit(0) } @@ -210,8 +229,9 @@ func run(cmd *cobra.Command, _ []string) { os.Exit(1) } - fmt.Fprintf(writer, "OPERATOR NAME\tOPERATOR NAMESPACE\tROLE NAME\t"+ - "ROLE ARN\tCLUSTER ID\tVERSION\tPOLICIES\tAWS Managed\tIN USE\n") + headers := []string{"OPERATOR NAME", "OPERATOR NAMESPACE", "ROLE NAME", "ROLE ARN", + "CLUSTER ID", "VERSION", "POLICIES", "AWS Managed", "IN USE"} + var tableData [][]string for _, operatorRole := range operatorsMap[args.prefix] { awsManaged := "No" inUse := "No" @@ -221,20 +241,31 @@ func run(cmd *cobra.Command, _ []string) { if hasClusterUsingOperatorRolesPrefix { inUse = "Yes" } - fmt.Fprintf( - writer, - "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", + row := []string{ operatorRole.OperatorName, operatorRole.OperatorNamespace, operatorRole.RoleName, operatorRole.RoleARN, operatorRole.ClusterID, operatorRole.Version, - operatorRole.AttachedPolicies, + output.PrintStringSlice(operatorRole.AttachedPolicies), awsManaged, inUse, - ) + } + tableData = append(tableData, row) + } + + if output.ShouldHideEmptyColumns() { + tableData = output.RemoveEmptyColumns(headers, tableData) + } else { + tableData = append([][]string{headers}, tableData...) + } + + output.BuildTable(writer, "\t", tableData) + + if err := writer.Flush(); err != nil { + _ = r.Reporter.Errorf("Failed to flush output: %v", err) + os.Exit(1) } - writer.Flush() } } diff --git a/cmd/list/region/cmd.go b/cmd/list/region/cmd.go index 8701be1752..f9e1225b3e 100644 --- a/cmd/list/region/cmd.go +++ b/cmd/list/region/cmd.go @@ -80,6 +80,7 @@ func init() { ) output.AddFlag(Cmd) + output.AddHideEmptyColumnsFlag(Cmd) } func run(cmd *cobra.Command, _ []string) { @@ -153,19 +154,29 @@ func run(cmd *cobra.Command, _ []string) { os.Exit(1) } - // Create the writer that will be used to print the tabulated results: - writer := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) - headerFormat := "ID\t\tNAME\t\tMULTI-AZ SUPPORT\t\tHOSTED-CP SUPPORT\n" - fmt.Fprint(writer, headerFormat) - + headers := []string{"ID", "NAME", "MULTI-AZ SUPPORT", "HOSTED-CP SUPPORT"} + var tableData [][]string for _, region := range availableRegions { - fmt.Fprintf(writer, - "%s\t\t%s\t\t%t\t\t%t\n", + row := []string{ region.ID(), region.DisplayName(), - region.SupportsMultiAZ(), - region.SupportsHypershift(), - ) + fmt.Sprintf("%t", region.SupportsMultiAZ()), + fmt.Sprintf("%t", region.SupportsHypershift()), + } + tableData = append(tableData, row) + } + + if output.ShouldHideEmptyColumns() { + tableData = output.RemoveEmptyColumns(headers, tableData) + } else { + tableData = append([][]string{headers}, tableData...) + } + + writer := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + output.BuildTable(writer, "\t\t", tableData) + + if err := writer.Flush(); err != nil { + _ = r.Reporter.Errorf("Failed to flush output: %v", err) + os.Exit(1) } - writer.Flush() } diff --git a/cmd/list/service/cmd.go b/cmd/list/service/cmd.go index 3fc69372b5..5bba138dce 100644 --- a/cmd/list/service/cmd.go +++ b/cmd/list/service/cmd.go @@ -17,7 +17,6 @@ limitations under the License. package service import ( - "fmt" "os" "text/tabwriter" @@ -47,6 +46,7 @@ func init() { flags.SortFlags = false output.AddFlag(Cmd) + output.AddHideEmptyColumnsFlag(Cmd) } func run(cmd *cobra.Command, argv []string) { @@ -77,12 +77,30 @@ func run(cmd *cobra.Command, argv []string) { os.Exit(0) } - writer := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) - fmt.Fprintf(writer, "SERVICE_ID\tSERVICE\tSERVICE_STATE\tCLUSTER_NAME\n") + headers := []string{"SERVICE_ID", "SERVICE", "SERVICE_STATE", "CLUSTER_NAME"} + var tableData [][]string servicesList.Each(func(srv *msv1.ManagedService) bool { - fmt.Fprintf(writer, "%s\t%s\t%s\t%s\n", - srv.ID(), srv.Service(), srv.ServiceState(), srv.Cluster().Name()) + row := []string{ + srv.ID(), + srv.Service(), + srv.ServiceState(), + srv.Cluster().Name(), + } + tableData = append(tableData, row) return true }) - writer.Flush() + + if output.ShouldHideEmptyColumns() { + tableData = output.RemoveEmptyColumns(headers, tableData) + } else { + tableData = append([][]string{headers}, tableData...) + } + + writer := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + output.BuildTable(writer, "\t", tableData) + + if err := writer.Flush(); err != nil { + _ = r.Reporter.Errorf("Failed to flush output: %v", err) + os.Exit(1) + } } diff --git a/cmd/list/upgrade/cmd.go b/cmd/list/upgrade/cmd.go index c97e7f8699..fac9d51ef3 100644 --- a/cmd/list/upgrade/cmd.go +++ b/cmd/list/upgrade/cmd.go @@ -61,6 +61,7 @@ func init() { confirm.AddFlag(flags) output.AddFlag(Cmd) + output.AddHideEmptyColumnsFlag(Cmd) } func run(cmd *cobra.Command, _ []string) { @@ -155,9 +156,8 @@ func runWithRuntime(r *rosa.Runtime, _ *cobra.Command) error { } } - // Create the writer that will be used to print the tabulated results: - writer := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) - fmt.Fprintf(writer, "VERSION\tNOTES\n") + headers := []string{"VERSION", "NOTES"} + var tableData [][]string for i, availableUpgrade := range availableUpgrades { notes := make([]string, 0) if i == 0 || availableUpgrade == latestRev { @@ -181,9 +181,26 @@ func runWithRuntime(r *rosa.Runtime, _ *cobra.Command) error { } } } - fmt.Fprintf(writer, "%s\t%s\n", availableUpgrade, strings.Join(notes, " - ")) + + row := []string{ + availableUpgrade, + strings.Join(notes, " - "), + } + tableData = append(tableData, row) + } + + if output.ShouldHideEmptyColumns() { + tableData = output.RemoveEmptyColumns(headers, tableData) + } else { + tableData = append([][]string{headers}, tableData...) + } + + writer := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + output.BuildTable(writer, "\t", tableData) + + if err := writer.Flush(); err != nil { + return err } - writer.Flush() return nil } diff --git a/cmd/rosa/structure_test/command_args/rosa/list/access-request/command_args.yml b/cmd/rosa/structure_test/command_args/rosa/list/access-request/command_args.yml index 4f9c473396..18fdcf854f 100644 --- a/cmd/rosa/structure_test/command_args/rosa/list/access-request/command_args.yml +++ b/cmd/rosa/structure_test/command_args/rosa/list/access-request/command_args.yml @@ -1,4 +1,5 @@ - name: cluster +- name: hide-empty-columns - name: output - name: profile - name: region diff --git a/cmd/rosa/structure_test/command_args/rosa/list/account-roles/command_args.yml b/cmd/rosa/structure_test/command_args/rosa/list/account-roles/command_args.yml index 8e3a64c545..0c46dd7202 100644 --- a/cmd/rosa/structure_test/command_args/rosa/list/account-roles/command_args.yml +++ b/cmd/rosa/structure_test/command_args/rosa/list/account-roles/command_args.yml @@ -1,4 +1,5 @@ - name: version - name: output +- name: hide-empty-columns - name: profile - name: region diff --git a/cmd/rosa/structure_test/command_args/rosa/list/addons/command_args.yml b/cmd/rosa/structure_test/command_args/rosa/list/addons/command_args.yml index 4f9c473396..18fdcf854f 100644 --- a/cmd/rosa/structure_test/command_args/rosa/list/addons/command_args.yml +++ b/cmd/rosa/structure_test/command_args/rosa/list/addons/command_args.yml @@ -1,4 +1,5 @@ - name: cluster +- name: hide-empty-columns - name: output - name: profile - name: region diff --git a/cmd/rosa/structure_test/command_args/rosa/list/break-glass-credentials/command_args.yml b/cmd/rosa/structure_test/command_args/rosa/list/break-glass-credentials/command_args.yml index 4f9c473396..18fdcf854f 100644 --- a/cmd/rosa/structure_test/command_args/rosa/list/break-glass-credentials/command_args.yml +++ b/cmd/rosa/structure_test/command_args/rosa/list/break-glass-credentials/command_args.yml @@ -1,4 +1,5 @@ - name: cluster +- name: hide-empty-columns - name: output - name: profile - name: region diff --git a/cmd/rosa/structure_test/command_args/rosa/list/clusters/command_args.yml b/cmd/rosa/structure_test/command_args/rosa/list/clusters/command_args.yml index 0f997f24ca..bdc351bcf3 100644 --- a/cmd/rosa/structure_test/command_args/rosa/list/clusters/command_args.yml +++ b/cmd/rosa/structure_test/command_args/rosa/list/clusters/command_args.yml @@ -1,5 +1,6 @@ - name: output - name: all - name: account-role-arn +- name: hide-empty-columns - name: profile - name: region diff --git a/cmd/rosa/structure_test/command_args/rosa/list/dns-domain/command_args.yml b/cmd/rosa/structure_test/command_args/rosa/list/dns-domain/command_args.yml index 28f0165a19..f0672a4411 100644 --- a/cmd/rosa/structure_test/command_args/rosa/list/dns-domain/command_args.yml +++ b/cmd/rosa/structure_test/command_args/rosa/list/dns-domain/command_args.yml @@ -1,4 +1,5 @@ - name: all +- name: hide-empty-columns - name: hosted-cp - name: output - name: profile diff --git a/cmd/rosa/structure_test/command_args/rosa/list/gates/command_args.yml b/cmd/rosa/structure_test/command_args/rosa/list/gates/command_args.yml index b84588d44c..a9fa2dd709 100644 --- a/cmd/rosa/structure_test/command_args/rosa/list/gates/command_args.yml +++ b/cmd/rosa/structure_test/command_args/rosa/list/gates/command_args.yml @@ -1,5 +1,6 @@ - name: cluster - name: gate +- name: hide-empty-columns - name: output - name: profile - name: region diff --git a/cmd/rosa/structure_test/command_args/rosa/list/iamserviceaccounts/command_args.yml b/cmd/rosa/structure_test/command_args/rosa/list/iamserviceaccounts/command_args.yml index c7cbc7f958..1f4eebbaff 100644 --- a/cmd/rosa/structure_test/command_args/rosa/list/iamserviceaccounts/command_args.yml +++ b/cmd/rosa/structure_test/command_args/rosa/list/iamserviceaccounts/command_args.yml @@ -1,4 +1,5 @@ - name: cluster +- name: hide-empty-columns - name: namespace - name: output - name: profile diff --git a/cmd/rosa/structure_test/command_args/rosa/list/idps/command_args.yml b/cmd/rosa/structure_test/command_args/rosa/list/idps/command_args.yml index 4f9c473396..18fdcf854f 100644 --- a/cmd/rosa/structure_test/command_args/rosa/list/idps/command_args.yml +++ b/cmd/rosa/structure_test/command_args/rosa/list/idps/command_args.yml @@ -1,4 +1,5 @@ - name: cluster +- name: hide-empty-columns - name: output - name: profile - name: region diff --git a/cmd/rosa/structure_test/command_args/rosa/list/ingresses/command_args.yml b/cmd/rosa/structure_test/command_args/rosa/list/ingresses/command_args.yml index 4f9c473396..18fdcf854f 100644 --- a/cmd/rosa/structure_test/command_args/rosa/list/ingresses/command_args.yml +++ b/cmd/rosa/structure_test/command_args/rosa/list/ingresses/command_args.yml @@ -1,4 +1,5 @@ - name: cluster +- name: hide-empty-columns - name: output - name: profile - name: region diff --git a/cmd/rosa/structure_test/command_args/rosa/list/machinepools/command_args.yml b/cmd/rosa/structure_test/command_args/rosa/list/machinepools/command_args.yml index 223beaf763..a9d0498e05 100644 --- a/cmd/rosa/structure_test/command_args/rosa/list/machinepools/command_args.yml +++ b/cmd/rosa/structure_test/command_args/rosa/list/machinepools/command_args.yml @@ -6,3 +6,4 @@ - name: profile - name: region - name: win-li +- name: hide-empty-columns diff --git a/cmd/rosa/structure_test/command_args/rosa/list/managed-services/command_args.yml b/cmd/rosa/structure_test/command_args/rosa/list/managed-services/command_args.yml index 39943b9c86..f5df6743e0 100644 --- a/cmd/rosa/structure_test/command_args/rosa/list/managed-services/command_args.yml +++ b/cmd/rosa/structure_test/command_args/rosa/list/managed-services/command_args.yml @@ -1,3 +1,4 @@ - name: output +- name: hide-empty-columns - name: profile - name: region diff --git a/cmd/rosa/structure_test/command_args/rosa/list/ocm-roles/command_args.yml b/cmd/rosa/structure_test/command_args/rosa/list/ocm-roles/command_args.yml index 39943b9c86..0ae2bf107b 100644 --- a/cmd/rosa/structure_test/command_args/rosa/list/ocm-roles/command_args.yml +++ b/cmd/rosa/structure_test/command_args/rosa/list/ocm-roles/command_args.yml @@ -1,3 +1,4 @@ +- name: hide-empty-columns - name: output - name: profile - name: region diff --git a/cmd/rosa/structure_test/command_args/rosa/list/oidc-config/command_args.yml b/cmd/rosa/structure_test/command_args/rosa/list/oidc-config/command_args.yml index 39943b9c86..0ae2bf107b 100644 --- a/cmd/rosa/structure_test/command_args/rosa/list/oidc-config/command_args.yml +++ b/cmd/rosa/structure_test/command_args/rosa/list/oidc-config/command_args.yml @@ -1,3 +1,4 @@ +- name: hide-empty-columns - name: output - name: profile - name: region diff --git a/cmd/rosa/structure_test/command_args/rosa/list/operator-roles/command_args.yml b/cmd/rosa/structure_test/command_args/rosa/list/operator-roles/command_args.yml index aeb2bc7375..1ce4a1a641 100644 --- a/cmd/rosa/structure_test/command_args/rosa/list/operator-roles/command_args.yml +++ b/cmd/rosa/structure_test/command_args/rosa/list/operator-roles/command_args.yml @@ -3,5 +3,6 @@ - name: interactive - name: cluster - name: output +- name: hide-empty-columns - name: profile - name: region diff --git a/cmd/rosa/structure_test/command_args/rosa/list/regions/command_args.yml b/cmd/rosa/structure_test/command_args/rosa/list/regions/command_args.yml index c1ce33860a..cd0cb6fa76 100644 --- a/cmd/rosa/structure_test/command_args/rosa/list/regions/command_args.yml +++ b/cmd/rosa/structure_test/command_args/rosa/list/regions/command_args.yml @@ -1,4 +1,5 @@ - name: external-id +- name: hide-empty-columns - name: hosted-cp - name: multi-az - name: output diff --git a/cmd/rosa/structure_test/command_args/rosa/list/upgrades/command_args.yml b/cmd/rosa/structure_test/command_args/rosa/list/upgrades/command_args.yml index 7d93b48ab7..2d7c5d5fc0 100644 --- a/cmd/rosa/structure_test/command_args/rosa/list/upgrades/command_args.yml +++ b/cmd/rosa/structure_test/command_args/rosa/list/upgrades/command_args.yml @@ -2,5 +2,6 @@ - name: machinepool - name: "yes" - name: output +- name: hide-empty-columns - name: profile - name: region diff --git a/pkg/machinepool/machinepool.go b/pkg/machinepool/machinepool.go index b155f1c1e1..45a8ac7be7 100644 --- a/pkg/machinepool/machinepool.go +++ b/pkg/machinepool/machinepool.go @@ -1061,12 +1061,39 @@ func (m *machinePool) ListMachinePools(r *rosa.Runtime, clusterKey string, clust // Create the writer that will be used to print the tabulated results: writer := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) - finalStringToOutput := getMachinePoolsString(r, machinePools, args) + // Get headers and data + headers, tableData := getMachinePoolsData(r, machinePools, args) if isHypershift { - finalStringToOutput = getNodePoolsString(nodePools) + headers, tableData = getNodePoolsData(nodePools) + } + + // Only hide empty columns if the flag is set + if output.ShouldHideEmptyColumns() { + // Find which columns are special (controlled by flags) + specialColumns := make(map[int]bool) + if !isHypershift { + for i, header := range headers { + if (header == "AZ TYPE" && (args.ShowAZType || args.ShowAll)) || + (header == "WIN-LI ENABLED" && (args.ShowWindowsLI || args.ShowAll)) || + (header == "DEDICATED HOST" && (args.ShowDedicated || args.ShowAll)) { + specialColumns[i] = true + } + } + } + + // Remove empty columns but preserve special columns when their flags are set + headers, tableData = removeEmptyColumnsExceptSpecial(headers, tableData, specialColumns) + } + + // Print the table + fmt.Fprintf(writer, "%s\n", strings.Join(headers, "\t")) + for _, row := range tableData { + fmt.Fprintf(writer, "%s\n", strings.Join(row, "\t")) + } + + if err := writer.Flush(); err != nil { + return err } - fmt.Fprint(writer, finalStringToOutput) - writer.Flush() return nil } @@ -1235,11 +1262,11 @@ func appendUpgradesIfExist(scheduledUpgrade *cmv1.NodePoolUpgradePolicy, output return output } -func getMachinePoolsString( +func getMachinePoolsData( runtime *rosa.Runtime, machinePools []*cmv1.MachinePool, args ListMachinePoolArgs, -) string { +) ([]string, [][]string) { type columnDefinition struct { header string isVisible bool @@ -1267,63 +1294,41 @@ func getMachinePoolsString( {"DEDICATED HOST", args.ShowDedicated || args.ShowAll, func(mp *cmv1.MachinePool) string { return isDedicatedHost(mp, runtime) }}, } - var visibleColumnHeaders []string - var visibleColumnData [][]string - numPools := len(machinePools) - + var headers []string + var tableData [][]string for _, column := range allColumnDefinitions { - if !column.isVisible { - continue + if column.isVisible { + headers = append(headers, column.header) } + } - columnValues := make([]string, numPools) - hasNonEmptyValue := false - - for i, pool := range machinePools { - if pool == nil { - columnValues[i] = "-" + for _, pool := range machinePools { + var row []string + for _, column := range allColumnDefinitions { + if !column.isVisible { continue } - value := column.extractData(pool) - columnValues[i] = value - if value != "" && value != "-" { - hasNonEmptyValue = true - } - } - - if args.ShowAll || hasNonEmptyValue || numPools == 0 { - visibleColumnHeaders = append(visibleColumnHeaders, column.header) - visibleColumnData = append(visibleColumnData, columnValues) - } - } - - var tableBuilder strings.Builder - - // Write header - if len(visibleColumnHeaders) > 0 { - tableBuilder.WriteString(strings.Join(visibleColumnHeaders, "\t") + "\n") - } - - // Write data rows - for rowIndex := range numPools { - for colIndex, columnValues := range visibleColumnData { - if colIndex > 0 { - tableBuilder.WriteString("\t") + if pool == nil { + row = append(row, "") + } else { + value := column.extractData(pool) + row = append(row, value) } - tableBuilder.WriteString(columnValues[rowIndex]) } - tableBuilder.WriteString("\n") + tableData = append(tableData, row) } - return tableBuilder.String() + return headers, tableData } -func getNodePoolsString(nodePools []*cmv1.NodePool) string { - outputString := "ID\tAUTOSCALING\tREPLICAS\t" + - "INSTANCE TYPE\tLABELS\t\tTAINTS\t\tAVAILABILITY ZONE\tSUBNET\tDISK SIZE\tVERSION\tAUTOREPAIR\t\n" +func getNodePoolsData(nodePools []*cmv1.NodePool) ([]string, [][]string) { + headers := []string{"ID", "AUTOSCALING", "REPLICAS", "INSTANCE TYPE", "LABELS", "TAINTS", + "AVAILABILITY ZONE", "SUBNET", "DISK SIZE", "VERSION", "AUTOREPAIR"} + + var tableData [][]string for _, nodePool := range nodePools { - outputString += fmt.Sprintf("%s\t%s\t%s\t%s\t%s\t\t%s\t\t%s\t%s\t%s\t%s\t%s\t\n", + row := []string{ nodePool.ID(), ocmOutput.PrintNodePoolAutoscaling(nodePool.Autoscaling()), ocmOutput.PrintNodePoolReplicasShort( @@ -1338,9 +1343,49 @@ func getNodePoolsString(nodePools []*cmv1.NodePool) string { ocmOutput.PrintNodePoolDiskSize(nodePool.AWSNodePool()), ocmOutput.PrintNodePoolVersion(nodePool.Version()), ocmOutput.PrintNodePoolAutorepair(nodePool.AutoRepair()), - ) + } + tableData = append(tableData, row) } - return outputString + + return headers, tableData +} + +// removeEmptyColumnsExceptSpecial removes empty columns except those marked as special +// (i.e., columns that are controlled by explicit flags and should be shown even if empty) +func removeEmptyColumnsExceptSpecial(headers []string, tableData [][]string, specialColumns map[int]bool) ([]string, [][]string) { + var newHeaders []string + var columnsToKeep []int + + for i, header := range headers { + // Keep special columns regardless of whether they're empty + if specialColumns[i] { + newHeaders = append(newHeaders, header) + columnsToKeep = append(columnsToKeep, i) + continue + } + + // For non-special columns, only keep if they have data + if !output.CheckIfColumnIsEmpty(i, tableData) { + newHeaders = append(newHeaders, header) + columnsToKeep = append(columnsToKeep, i) + } + } + + // Build new table data with only the kept columns + var newTableData [][]string + for _, row := range tableData { + var newRow []string + for _, colIdx := range columnsToKeep { + if colIdx < len(row) { + newRow = append(newRow, row[colIdx]) + } else { + newRow = append(newRow, "") + } + } + newTableData = append(newTableData, newRow) + } + + return newHeaders, newTableData } func (m *machinePool) EditMachinePool(cmd *cobra.Command, machinePoolId string, clusterKey string, diff --git a/pkg/machinepool/machinepool_test.go b/pkg/machinepool/machinepool_test.go index 6dfda9f67c..8aa2c84f89 100644 --- a/pkg/machinepool/machinepool_test.go +++ b/pkg/machinepool/machinepool_test.go @@ -7,6 +7,7 @@ import ( "net/http" "os" "reflect" + "strings" "time" "go.uber.org/mock/gomock" @@ -35,6 +36,23 @@ import ( var policyBuilder cmv1.NodePoolUpgradePolicyBuilder var date time.Time +// Helper function to convert headers and tableData back to string format for testing +func formatTableString(headers []string, tableData [][]string) string { + var result strings.Builder + + // Add headers + result.WriteString(strings.Join(headers, "\t")) + result.WriteString("\n") + + // Add data rows + for _, row := range tableData { + result.WriteString(strings.Join(row, "\t")) + result.WriteString("\n") + } + + return result.String() +} + var _ = Describe("Machinepool and nodepool", func() { var ( mockClient *mock.MockClient @@ -189,16 +207,16 @@ var _ = Describe("Machinepool and nodepool", func() { Subnet("sn").Version(cmv1.NewVersion().ID("1")).AutoRepair(false))) cluster, err := clusterBuilder.Build() Expect(err).ToNot(HaveOccurred()) - out := getNodePoolsString(cluster.NodePools().Slice()) - Expect(err).ToNot(HaveOccurred()) - Expect(out).To(Equal(fmt.Sprintf("ID\tAUTOSCALING\tREPLICAS\t"+ - "INSTANCE TYPE\tLABELS\t\tTAINTS\t\tAVAILABILITY ZONE\tSUBNET\tDISK SIZE\tVERSION\tAUTOREPAIR\t\n"+ - "%s\t%s\t%s\t%s\t%s\t\t%s\t\t%s\t%s\t%s\t%s\t%s\t\n", + headers, tableData := getNodePoolsData(cluster.NodePools().Slice()) + out := formatTableString(headers, tableData) + + expectedOutput := fmt.Sprintf("ID\tAUTOSCALING\tREPLICAS\tINSTANCE TYPE\tLABELS\tTAINTS\tAVAILABILITY ZONE\tSUBNET\tDISK SIZE\tVERSION\tAUTOREPAIR\n"+ + "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", cluster.NodePools().Get(0).ID(), ocmOutput.PrintNodePoolAutoscaling(cluster.NodePools().Get(0).Autoscaling()), ocmOutput.PrintNodePoolReplicasShort( ocmOutput.PrintNodePoolCurrentReplicas(cluster.NodePools().Get(0).Status()), - ocmOutput.PrintNodePoolReplicas(cluster.NodePools().Get(0).Autoscaling(), + ocmOutput.PrintNodePoolReplicasInline(cluster.NodePools().Get(0).Autoscaling(), cluster.NodePools().Get(0).Replicas()), ), ocmOutput.PrintNodePoolInstanceType(cluster.NodePools().Get(0).AWSNodePool()), @@ -208,7 +226,8 @@ var _ = Describe("Machinepool and nodepool", func() { cluster.NodePools().Get(0).Subnet(), ocmOutput.PrintNodePoolDiskSize(cluster.NodePools().Get(0).AWSNodePool()), ocmOutput.PrintNodePoolVersion(cluster.NodePools().Get(0).Version()), - ocmOutput.PrintNodePoolAutorepair(cluster.NodePools().Get(0).AutoRepair())))) + ocmOutput.PrintNodePoolAutorepair(cluster.NodePools().Get(0).AutoRepair())) + Expect(out).To(Equal(expectedOutput)) }) It("Test appendUpgradesIfExist", func() { policy, err := policyBuilder.Build() @@ -350,10 +369,11 @@ var _ = Describe("Machinepool and nodepool", func() { r := &rosa.Runtime{} args := ListMachinePoolArgs{} - out := getMachinePoolsString(r, cluster.MachinePools().Slice(), args) + headers, tableData := getMachinePoolsData(r, cluster.MachinePools().Slice(), args) + out := formatTableString(headers, tableData) - expectedOutput := "ID\tAUTOSCALING\tREPLICAS\tINSTANCE TYPE\tTAINTS\tSUBNETS\tSPOT INSTANCES\tDISK SIZE\n" + - "mp-1\tNo\t3\tm5.large\ttest-key=test-value:\tsubnet-1, subnet-2\tNo\tdefault\n" + expectedOutput := "ID\tAUTOSCALING\tREPLICAS\tINSTANCE TYPE\tLABELS\tTAINTS\tAVAILABILITY ZONES\tSUBNETS\tSPOT INSTANCES\tDISK SIZE\tSG IDS\n" + + "mp-1\tNo\t3\tm5.large\t\ttest-key=test-value:\t\tsubnet-1, subnet-2\tNo\tdefault\t\n" Expect(out).To(Equal(expectedOutput)) }) @@ -365,7 +385,8 @@ var _ = Describe("Machinepool and nodepool", func() { r := &rosa.Runtime{} args := ListMachinePoolArgs{} - out := getMachinePoolsString(r, cluster.MachinePools().Slice(), args) + headers, tableData := getMachinePoolsData(r, cluster.MachinePools().Slice(), args) + out := formatTableString(headers, tableData) // When there are no machine pools, only headers are returned expectedOutput := "ID\tAUTOSCALING\tREPLICAS\tINSTANCE TYPE\tLABELS\tTAINTS\tAVAILABILITY ZONES\tSUBNETS\tSPOT INSTANCES\tDISK SIZE\tSG IDS\n" @@ -373,7 +394,7 @@ var _ = Describe("Machinepool and nodepool", func() { Expect(out).To(Equal(expectedOutput)) }) - It("Test printMachinePools with showAll flag", func() { + It("Test printMachinePools with --all flag", func() { clusterBuilder := cmv1.NewCluster().ID("test").State(cmv1.ClusterStateReady). MachinePools(cmv1.NewMachinePoolList(). Items(cmv1.NewMachinePool().ID("mp-1").Replicas(3). @@ -383,11 +404,16 @@ var _ = Describe("Machinepool and nodepool", func() { r := &rosa.Runtime{} args := ListMachinePoolArgs{ShowAll: true} - out := getMachinePoolsString(r, cluster.MachinePools().Slice(), args) + headers, tableData := getMachinePoolsData(r, cluster.MachinePools().Slice(), args) - expectedOutput := "ID\tAUTOSCALING\tREPLICAS\tINSTANCE TYPE\tLABELS\tTAINTS\tAVAILABILITY ZONES\tSUBNETS\tSPOT INSTANCES\tDISK SIZE\tSG IDS\tAZ TYPE\tWIN-LI ENABLED\tDEDICATED HOST\n" + - "mp-1\tNo\t3\tm5.large\t\t\t\t\tNo\tdefault\t\tN/A\tNo\tNo\n" - Expect(out).To(Equal(expectedOutput)) + // When ShowAll is true, all three special columns should be present + expectedHeaders := []string{"ID", "AUTOSCALING", "REPLICAS", "INSTANCE TYPE", "LABELS", "TAINTS", "AVAILABILITY ZONES", "SUBNETS", "SPOT INSTANCES", "DISK SIZE", "SG IDS", "AZ TYPE", "WIN-LI ENABLED", "DEDICATED HOST"} + Expect(headers).To(Equal(expectedHeaders)) + + // Check the data row + expectedRow := []string{"mp-1", "No", "3", "m5.large", "", "", "", "", "No", "default", "", "N/A", "No", "No"} + Expect(tableData).To(HaveLen(1)) + Expect(tableData[0]).To(Equal(expectedRow)) }) It("Test printMachinePools with showAZType flag", func() { @@ -400,10 +426,11 @@ var _ = Describe("Machinepool and nodepool", func() { r := &rosa.Runtime{} args := ListMachinePoolArgs{ShowAZType: true} - out := getMachinePoolsString(r, cluster.MachinePools().Slice(), args) + headers, tableData := getMachinePoolsData(r, cluster.MachinePools().Slice(), args) + out := formatTableString(headers, tableData) - expectedOutput := "ID\tAUTOSCALING\tREPLICAS\tINSTANCE TYPE\tSPOT INSTANCES\tDISK SIZE\tAZ TYPE\n" + - "mp-1\tNo\t3\tm5.large\tNo\tdefault\tN/A\n" + expectedOutput := "ID\tAUTOSCALING\tREPLICAS\tINSTANCE TYPE\tLABELS\tTAINTS\tAVAILABILITY ZONES\tSUBNETS\tSPOT INSTANCES\tDISK SIZE\tSG IDS\tAZ TYPE\n" + + "mp-1\tNo\t3\tm5.large\t\t\t\t\tNo\tdefault\t\tN/A\n" Expect(out).To(Equal(expectedOutput)) }) @@ -417,10 +444,11 @@ var _ = Describe("Machinepool and nodepool", func() { r := &rosa.Runtime{} args := ListMachinePoolArgs{ShowDedicated: true} - out := getMachinePoolsString(r, cluster.MachinePools().Slice(), args) + headers, tableData := getMachinePoolsData(r, cluster.MachinePools().Slice(), args) + out := formatTableString(headers, tableData) - expectedOutput := "ID\tAUTOSCALING\tREPLICAS\tINSTANCE TYPE\tSPOT INSTANCES\tDISK SIZE\tDEDICATED HOST\n" + - "mp-1\tNo\t3\tm5.large\tNo\tdefault\tNo\n" + expectedOutput := "ID\tAUTOSCALING\tREPLICAS\tINSTANCE TYPE\tLABELS\tTAINTS\tAVAILABILITY ZONES\tSUBNETS\tSPOT INSTANCES\tDISK SIZE\tSG IDS\tDEDICATED HOST\n" + + "mp-1\tNo\t3\tm5.large\t\t\t\t\tNo\tdefault\t\tNo\n" Expect(out).To(Equal(expectedOutput)) }) @@ -434,10 +462,11 @@ var _ = Describe("Machinepool and nodepool", func() { r := &rosa.Runtime{} args := ListMachinePoolArgs{ShowWindowsLI: true} - out := getMachinePoolsString(r, cluster.MachinePools().Slice(), args) + headers, tableData := getMachinePoolsData(r, cluster.MachinePools().Slice(), args) + out := formatTableString(headers, tableData) - expectedOutput := "ID\tAUTOSCALING\tREPLICAS\tINSTANCE TYPE\tSPOT INSTANCES\tDISK SIZE\tWIN-LI ENABLED\n" + - "mp-1\tNo\t3\tm5.large\tNo\tdefault\tNo\n" + expectedOutput := "ID\tAUTOSCALING\tREPLICAS\tINSTANCE TYPE\tLABELS\tTAINTS\tAVAILABILITY ZONES\tSUBNETS\tSPOT INSTANCES\tDISK SIZE\tSG IDS\tWIN-LI ENABLED\n" + + "mp-1\tNo\t3\tm5.large\t\t\t\t\tNo\tdefault\t\tNo\n" Expect(out).To(Equal(expectedOutput)) }) @@ -451,10 +480,11 @@ var _ = Describe("Machinepool and nodepool", func() { r := &rosa.Runtime{} args := ListMachinePoolArgs{ShowAZType: true, ShowDedicated: true} - out := getMachinePoolsString(r, cluster.MachinePools().Slice(), args) + headers, tableData := getMachinePoolsData(r, cluster.MachinePools().Slice(), args) + out := formatTableString(headers, tableData) - expectedOutput := "ID\tAUTOSCALING\tREPLICAS\tINSTANCE TYPE\tSPOT INSTANCES\tDISK SIZE\tAZ TYPE\tDEDICATED HOST\n" + - "mp-1\tNo\t3\tm5.large\tNo\tdefault\tN/A\tNo\n" + expectedOutput := "ID\tAUTOSCALING\tREPLICAS\tINSTANCE TYPE\tLABELS\tTAINTS\tAVAILABILITY ZONES\tSUBNETS\tSPOT INSTANCES\tDISK SIZE\tSG IDS\tAZ TYPE\tDEDICATED HOST\n" + + "mp-1\tNo\t3\tm5.large\t\t\t\t\tNo\tdefault\t\tN/A\tNo\n" Expect(out).To(Equal(expectedOutput)) }) @@ -470,10 +500,11 @@ var _ = Describe("Machinepool and nodepool", func() { r := &rosa.Runtime{} args := ListMachinePoolArgs{} - out := getMachinePoolsString(r, cluster.MachinePools().Slice(), args) + headers, tableData := getMachinePoolsData(r, cluster.MachinePools().Slice(), args) + out := formatTableString(headers, tableData) - expectedOutput := "ID\tAUTOSCALING\tREPLICAS\tINSTANCE TYPE\tSPOT INSTANCES\tDISK SIZE\n" + - "mp-autoscale\tYes\t2-10\tm5.xlarge\tNo\tdefault\n" + expectedOutput := "ID\tAUTOSCALING\tREPLICAS\tINSTANCE TYPE\tLABELS\tTAINTS\tAVAILABILITY ZONES\tSUBNETS\tSPOT INSTANCES\tDISK SIZE\tSG IDS\n" + + "mp-autoscale\tYes\t2-10\tm5.xlarge\t\t\t\t\tNo\tdefault\t\n" Expect(out).To(Equal(expectedOutput)) }) @@ -492,12 +523,13 @@ var _ = Describe("Machinepool and nodepool", func() { r := &rosa.Runtime{} args := ListMachinePoolArgs{} - out := getMachinePoolsString(r, cluster.MachinePools().Slice(), args) + headers, tableData := getMachinePoolsData(r, cluster.MachinePools().Slice(), args) + out := formatTableString(headers, tableData) - expectedOutput := "ID\tAUTOSCALING\tREPLICAS\tINSTANCE TYPE\tSPOT INSTANCES\tDISK SIZE\n" + - "mp-1\tNo\t3\tm5.large\tNo\tdefault\n" + - "mp-2\tYes\t1-5\tc5.xlarge\tNo\tdefault\n" + - "mp-3\tNo\t1\tt3.medium\tNo\tdefault\n" + expectedOutput := "ID\tAUTOSCALING\tREPLICAS\tINSTANCE TYPE\tLABELS\tTAINTS\tAVAILABILITY ZONES\tSUBNETS\tSPOT INSTANCES\tDISK SIZE\tSG IDS\n" + + "mp-1\tNo\t3\tm5.large\t\t\t\t\tNo\tdefault\t\n" + + "mp-2\tYes\t1-5\tc5.xlarge\t\t\t\t\tNo\tdefault\t\n" + + "mp-3\tNo\t1\tt3.medium\t\t\t\t\tNo\tdefault\t\n" Expect(out).To(Equal(expectedOutput)) }) @@ -516,11 +548,12 @@ var _ = Describe("Machinepool and nodepool", func() { r := &rosa.Runtime{} args := ListMachinePoolArgs{} - out := getMachinePoolsString(r, cluster.MachinePools().Slice(), args) + headers, tableData := getMachinePoolsData(r, cluster.MachinePools().Slice(), args) + out := formatTableString(headers, tableData) - expectedOutput := "ID\tAUTOSCALING\tREPLICAS\tINSTANCE TYPE\tAVAILABILITY ZONES\tSUBNETS\tSPOT INSTANCES\tDISK SIZE\n" + - "mp-minimal\tNo\t1\tt3.small\t\t\tNo\tdefault\n" + - "mp-complete\tNo\t3\tm5.large\tus-east-1a\tsubnet-1\tNo\tdefault\n" + expectedOutput := "ID\tAUTOSCALING\tREPLICAS\tINSTANCE TYPE\tLABELS\tTAINTS\tAVAILABILITY ZONES\tSUBNETS\tSPOT INSTANCES\tDISK SIZE\tSG IDS\n" + + "mp-minimal\tNo\t1\tt3.small\t\t\t\t\tNo\tdefault\t\n" + + "mp-complete\tNo\t3\tm5.large\t\t\tus-east-1a\tsubnet-1\tNo\tdefault\t\n" Expect(out).To(Equal(expectedOutput)) }) @@ -534,11 +567,12 @@ var _ = Describe("Machinepool and nodepool", func() { r := &rosa.Runtime{} args := ListMachinePoolArgs{} - out := getMachinePoolsString(r, cluster.MachinePools().Slice(), args) + headers, tableData := getMachinePoolsData(r, cluster.MachinePools().Slice(), args) + out := formatTableString(headers, tableData) - // Only columns with actual data should be included - expectedOutput := "ID\tAUTOSCALING\tREPLICAS\tINSTANCE TYPE\tSPOT INSTANCES\tDISK SIZE\n" + - "mp-1\tNo\t2\tm5.medium\tNo\tdefault\n" + // All regular columns are shown even if empty when not using --hide-empty-columns + expectedOutput := "ID\tAUTOSCALING\tREPLICAS\tINSTANCE TYPE\tLABELS\tTAINTS\tAVAILABILITY ZONES\tSUBNETS\tSPOT INSTANCES\tDISK SIZE\tSG IDS\n" + + "mp-1\tNo\t2\tm5.medium\t\t\t\t\tNo\tdefault\t\n" Expect(out).To(Equal(expectedOutput)) }) diff --git a/pkg/output/hide_empty_columns.go b/pkg/output/hide_empty_columns.go new file mode 100644 index 0000000000..30bd19fa09 --- /dev/null +++ b/pkg/output/hide_empty_columns.go @@ -0,0 +1,18 @@ +package output + +import "github.com/spf13/cobra" + +var hideEmptyColumnsFlag bool = false + +func AddHideEmptyColumnsFlag(cmd *cobra.Command) { + cmd.PersistentFlags().BoolVar( + &hideEmptyColumnsFlag, + "hide-empty-columns", + false, + "Hide columns that contain no data", + ) +} + +func ShouldHideEmptyColumns() bool { + return hideEmptyColumnsFlag +} diff --git a/pkg/output/table_filter.go b/pkg/output/table_filter.go new file mode 100644 index 0000000000..504b3a955a --- /dev/null +++ b/pkg/output/table_filter.go @@ -0,0 +1,88 @@ +package output + +import ( + "fmt" + "text/tabwriter" +) + +func CheckIfColumnIsEmpty(columnIdx int, tableData [][]string) bool { + for _, row := range tableData { + if columnIdx < len(row) && row[columnIdx] != "" { + return false + } + } + return true +} + +func RemoveEmptyColumns(headers []string, tableData [][]string) [][]string { + if len(tableData) == 0 { + return [][]string{headers} + } + + var newHeaders []string + var columnsToKeep []int + + for i, header := range headers { + if !CheckIfColumnIsEmpty(i, tableData) { + newHeaders = append(newHeaders, header) + columnsToKeep = append(columnsToKeep, i) + } + } + + var result [][]string + result = append(result, newHeaders) + + for _, row := range tableData { + var newRow []string + for _, colIdx := range columnsToKeep { + if colIdx < len(row) { + newRow = append(newRow, row[colIdx]) + } else { + newRow = append(newRow, "") + } + } + result = append(result, newRow) + } + + return result +} + +// BuildTable writes table data to a tabwriter with the specified separator +// The first row in tableData is treated as headers. +// Dynamically builds format strings like "%s\t%s\t%s\n" based on column count. +func BuildTable(writer *tabwriter.Writer, separator string, tableData [][]string) { + for _, row := range tableData { + if len(row) == 0 { + continue + } + + // Build format string dynamically based on number of columns + formatString := buildFormatString(len(row), separator) + + // Convert []string to []interface{} for fmt.Fprintf + args := make([]any, len(row)) + for i, v := range row { + args[i] = v + } + + fmt.Fprintf(writer, formatString, args...) + } +} + +// buildFormatString creates a format string with the appropriate number of %s placeholders +func buildFormatString(columnCount int, separator string) string { + if columnCount == 0 { + return "\n" + } + + format := "" + for i := range columnCount { + format += "%s" + if i < columnCount-1 { + format += separator + } + } + format += "\n" + + return format +}