Skip to content

Conversation

@olucasfreitas
Copy link
Contributor

Details

This PR improves the UX when listing resources by introducing a new --hide-empty-columns flag that automatically hides columns containing no data across all rows, making the output cleaner and more readable.

The flag has been added to the following rosa list commands:

  • accessrequests
  • accountroles
  • addon
  • breakglasscredential
  • cluster
  • dnsdomains
  • gates
  • iamserviceaccounts
  • idp
  • ingress
  • instancetypes
  • ocmroles
  • oidcconfig
  • operatorroles
  • region
  • service
  • upgrade

Note: The following commands do NOT have this flag:

  • list machinepools - Already has built-in auto-hide empty columns logic
  • list version, list userroles, list user, list rhRegion, list tuningconfigs, list oidcprovider, list kubeletconfig, list externalauthprovider - These maintain their original behavior

Default Behavior (All Columns Shown)

  1. Run any supported list command without the flag:

    ./rosa list clusters
  2. You should see all columns displayed, including empty ones:

    ID          NAME        STATE  TOPOLOGY  REGION      PROVIDER  VERSION
    1a2b3c4d    my-cluster  ready            us-east-1   aws       4.14.0
    5e6f7g8h    test-rosa   ready            us-west-2   aws       4.13.5
    

Using --hide-empty-columns Flag

  1. Run the same command with the flag:

    ./rosa list clusters --hide-empty-columns
  2. Empty columns (like TOPOLOGY in this example) should be automatically hidden:

    ID          NAME        STATE  REGION      PROVIDER  VERSION
    1a2b3c4d    my-cluster  ready  us-east-1   aws       4.14.0
    5e6f7g8h    test-rosa   ready  us-west-2   aws       4.13.5
    

New behavior of the '--all'

  1. The --all flag shows the 3 additional columns (AZ TYPE, WIN-LI ENABLED, DEDICATED HOST) but still hides empty columns:

    ./rosa list machinepools --cluster <cluster-name> --all

    Example output:

    ID       AUTOSCALING  REPLICAS  INSTANCE TYPE  SPOT INSTANCES  DISK SIZE  AZ TYPE   WIN-LI ENABLED  DEDICATED HOST
    workers  No           3         m5.xlarge      No              default    Standard  No              No
    

Ticket

Closes [OCM-1520]

@openshift-ci openshift-ci bot requested review from ciaranRoche and gdbranco August 21, 2025 01:57
@openshift-ci
Copy link
Contributor

openshift-ci bot commented Aug 21, 2025

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: olucasfreitas
Once this PR has been reviewed and has the lgtm label, please assign gdbranco for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@codecov
Copy link

codecov bot commented Aug 21, 2025

Codecov Report

❌ Patch coverage is 26.70807% with 354 lines in your changes missing coverage. Please review.
✅ Project coverage is 27.76%. Comparing base (8f44abd) to head (fc6c0ed).
⚠️ Report is 97 commits behind head on master.

Files with missing lines Patch % Lines
pkg/output/table_filter.go 50.00% 47 Missing and 3 partials ⚠️
cmd/list/addon/cmd.go 3.03% 32 Missing ⚠️
cmd/list/operatorroles/cmd.go 3.57% 27 Missing ⚠️
cmd/list/gates/cmd.go 3.70% 26 Missing ⚠️
pkg/machinepool/machinepool.go 46.93% 25 Missing and 1 partial ⚠️
cmd/list/idp/cmd.go 4.16% 23 Missing ⚠️
cmd/list/ocmroles/cmd.go 5.00% 19 Missing ⚠️
cmd/list/ingress/cmd.go 5.26% 18 Missing ⚠️
cmd/list/service/cmd.go 5.55% 17 Missing ⚠️
cmd/list/cluster/cmd.go 6.25% 15 Missing ⚠️
... and 10 more
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #2983      +/-   ##
==========================================
+ Coverage   27.37%   27.76%   +0.39%     
==========================================
  Files         306      317      +11     
  Lines       34350    35291     +941     
==========================================
+ Hits         9403     9799     +396     
- Misses      24301    24832     +531     
- Partials      646      660      +14     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Contributor

@hunterkepley hunterkepley left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall looks much better than the original write, left some comments- will wait for you to make changes before doing review on the pkg/output files introduced since they may change

@olucasfreitas
Copy link
Contributor Author

@hunterkepley addressed all the comments in the new commit

@olucasfreitas olucasfreitas force-pushed the OCM-1520 branch 2 times, most recently from ef825a0 to eb6702d Compare August 27, 2025 21:45
Copy link
Contributor

@hunterkepley hunterkepley left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some more comments, looking good 🙂 Noticed a couple of improvements which could be made to lessen the LoC committed for this effort, as well as a couple Go things that would be useful

@olucasfreitas olucasfreitas force-pushed the OCM-1520 branch 2 times, most recently from 2f270f0 to bdf62a6 Compare August 28, 2025 20:55
@olucasfreitas
Copy link
Contributor Author

@hunterkepley new comments addressed on the new commit

Copy link
Contributor

@hunterkepley hunterkepley left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some suggestions on the implementation of BuildTable as well as the edge case

@hunterkepley
Copy link
Contributor

hunterkepley commented Sep 5, 2025

Here's a Go Playground showing the solution I came up with for a list of separators: https://go.dev/play/p/2qUevkuSUUz

We can pass the table data- including the new string separator list, allowing the custom format we require for some commands -into this and it will build a usable output to pass into a tabwriter, with less LoC

@olucasfreitas
Copy link
Contributor Author

/retest

@olucasfreitas
Copy link
Contributor Author

/test coverage

Copy link
Contributor

@hunterkepley hunterkepley left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some more comments, will do a more in-depth review after these are addressed since it may change up other things im worried about

Comment on lines 135 to 156
if len(row) == 0 {
continue
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious, is there a case where we would expect the length of a row to be 0 in the middle of a table?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for what i understood here, empty rows can occur in real-world data

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you provide an example?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Data Import with Blank Lines

// CSV parsing or API responses might produce:
tableData := [][]string{
    {"ID", "Name", "Status"},
    {"1", "Alice", "Active"},
    {},  // Blank line in source data
    {"2", "Bob", "Pending"},
}

Important Distinction

// These are different:
emptyRow := []string{}        // Length 0 - gets skipped ✓
blankRow := []string{"", "", ""}  // Length 3 - displays with spacing

Copy link
Contributor

@hunterkepley hunterkepley Sep 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey Lucas, thanks for explaining the logic here;

I was more-so asking for a 'real world' example, such as one coming from the ROSA CLI. My thoughts are, that this may not be possible for our tables in the ROSA CLI, just by design. We add data generally when data exists, so there should be at least one value in each row (for loops over resource lists add data by nature causes this)

BUT- im asking for examples because I want to see if I might be wrong here, and if there's a list command which can produce empty rows

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example, if we list access requests - we use the API to fetch a list of the cluster's access requests. Then we loop through those

This pattern is pretty common across all list commands, where we fetch data before listing, since that data is generally not available locally on the user's machine

Hopefully that makes senes, LMK if you have questions

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, now I got it, I'm gonna run through some commands a send you the data here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, I ran through the commands, and I stand corrected, no cases in where this can happen showed up for me, thanks for the explanation here, I addressed the changes on this commit e397f65

Comment on lines 122 to 146
separators := make([]string, maxCols)
for i := range separators {
separators[i] = separator
}

BuildTableWithSeparators(writer, separators, tableData)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#2983 (comment)

In this comment I talked about having support for separators which have different separators at different parts of the table. For example, val1\tval2\t\tval3

It seems like you have support for it now, but did not actually support it, instead wrapping it in a function which makes a copy of a single separator that was passed in

If we could make that functional, and get the original separators back in the command which have unique separator values per column, that would be great

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example, list machinepools

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Improved this on the last commit

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@olucasfreitas im afraid this was virtually unchanged from the last commit

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worked on this on this commit 3f5390f

@hunterkepley
Copy link
Contributor

@olucasfreitas
#2983 (comment)

What is MOCK MODE? I don't see it in this PR or in the CLI? 😄

@olucasfreitas
Copy link
Contributor Author

@hunterkepley the mock mode was something I added only for testing, to activate the mock responses on the command

@hunterkepley
Copy link
Contributor

the mock mode was something I added only for testing, to activate the mock responses on the command

@olucasfreitas You made an entire mock mode only for testing this feature?

@olucasfreitas
Copy link
Contributor Author

@hunterkepley was actually pretty simple, not an ENTIRE mock mode as you maybe thought

@olucasfreitas
Copy link
Contributor Author

/retest

Copy link
Contributor

@hunterkepley hunterkepley left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems there were a missed comment or 2 from the last round of review, id like to address those first before going to deep on another (but I did add some comments)

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"+
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are still not taking into account different separators within a single table

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worked on this on this commit 3f5390f

return true
}

type ColumnFilterFunc func(columnIdx int) bool
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this introduced?

Copy link
Contributor Author

@olucasfreitas olucasfreitas Sep 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was created to support the new FilterColumns function, which needed a way to let callers specify custom logic for which columns to keep or remove. I did this for the flexibility, reusability and composability

Copy link
Contributor

@hunterkepley hunterkepley Sep 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool yeah that makes sense,

I did have a question about this:

	// Use FilterColumns with a predicate that keeps non-empty columns
	newHeaders, newTableData := FilterColumns(headers, tableData, func(columnIdx int) bool {
		return !CheckIfColumnIsEmpty(columnIdx, tableData)
	})

I see that this is used twice, to filter columns based on !CheckIfColumnIsEmpty

I think this is greatly useful if we had different types of filtering to be done, but in our case here I see we have only CheckIfColumnIsEmpty

My thoughts are, that we can get rid of the type here, and instead of passing a function into FilterColumns, we can directly call CheckIfColumnIsEmpty. WDYT?

OH- another note;

The current design has it so tableData is passed into both FilterColumns and the function being passed in.

	// Use FilterColumns with a predicate that keeps non-empty columns
	newHeaders, newTableData := FilterColumns(headers, tableData, func(columnIdx int) bool {
		return !CheckIfColumnIsEmpty(columnIdx, tableData)
	})

Looking at this again, we can see that. Sadly, this means this data (no matter how large or small) is tripled in memory every time this is called (once for the first duplication to pass into the function, and a second one for the CheckIfColumnIsEmpty func

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While the memory part may not seem like a big deal at first, we have to think about the fact that some tables can be very long. And we have the ability to control the pagination with the CLI commands usually

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My thoughts are, that we can get rid of the type here, and instead of passing a function into FilterColumns, we can directly call CheckIfColumnIsEmpty. WDYT?

I do agree with this, makes this much simpler to understand and the implementation cleaner too

While the memory part may not seem like a big deal at first, we have to think about the fact that some tables can be very long. And we have the ability to control the pagination with the CLI commands usually

Did not think of this, thanks for the explanation, will take a look into to fixing it on a new commit

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hopefully addressed on 75d64ee

Comment on lines 122 to 146
separators := make([]string, maxCols)
for i := range separators {
separators[i] = separator
}

BuildTableWithSeparators(writer, separators, tableData)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@olucasfreitas im afraid this was virtually unchanged from the last commit

Comment on lines 1064 to 1068
// Get headers and data
headers, tableData := getMachinePoolsData(r, machinePools, args)
if isHypershift {
finalStringToOutput = getNodePoolsString(nodePools)
headers, tableData = getNodePoolsData(nodePools)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the original functions (getMachinePoolsString and getNodePoolsString), we return a string such as

test1\ttest2\ttest3\nval1\tval2\tval3\n

Couldn't we convert this into a list and not change the original functions?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worked on this on this commit 3f5390f

@hunterkepley
Copy link
Contributor

Oh it looks like it may have squashed- be sure to check this page for replies on comments, the old ones won't show up on the Files changed tab if the code was changed during the squash 🙂

@olucasfreitas
Copy link
Contributor Author

/retest

1 similar comment
@olucasfreitas
Copy link
Contributor Author

/retest

Copy link
Contributor

@hunterkepley hunterkepley left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will continue review tomorrow- please feel free to go ahead and make the change I suggested a it will help out with my review

return headers, tableData
}

// Write header
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey I think we need to include unique separators for the nodepool output- you can see here that the original separator list has a mix of \t\t and \t

It seems support was added for it in the last commit but it was not implemented in the actual command yet (still using a single separator \t for this command for example)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hunterkepley adress this on the new commit, please take a look when you can

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@olucasfreitas Hey it looks like this wasn't changed- the BuildTableWithSeparators function is not called in the machinepool command still

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it looks like it's only used in tests

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I forgot to push the rest of the changes, please check it again

@openshift-ci
Copy link
Contributor

openshift-ci bot commented Oct 15, 2025

@olucasfreitas: all tests passed!

Full PR test history. Your PR dashboard.

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants