Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add CLI option --format to disconnect command #108

Merged
merged 3 commits into from
Sep 4, 2024

Conversation

jirihnidek
Copy link
Contributor

  • It is possible to print output of disconnect command in machine-readable format. The --format requires argument and it could be only "json" ATM. It follows practice of status command.
  • Refactored code a little to share some code with status command.

@subpop
Copy link
Collaborator

subpop commented May 2, 2024

I like the use of the Before function handler. That's a good idea to validate flags and user input!

@jirihnidek
Copy link
Contributor Author

BTW: I would like to add --format CLI option to connect command too, but I would like to do some refactoring of main.go first. This file is just too big and I would like to split it into connect.go and disconnect.go. Each command should be in separate file.

@subpop
Copy link
Collaborator

subpop commented May 6, 2024

disconnect, connect, status all could take advantage of a --format flag. This duplication leads me to believe there should be a global/top-level --format flag that all subcommands can use. From within the Action of a subcommand, flags defined on the parent are accessed from within the cli.Context using their name like any other flag. For example, consider the following program:

package main

import (
	"fmt"
	"log"
	"os"

	"github.com/urfave/cli/v2"
)

func main() {
	app := cli.NewApp()
	app.Name = "rhc"

	app.Flags = []cli.Flag{
		&cli.StringFlag{
			Name: "format",
		},
	}
	app.Commands = []*cli.Command{
		{
			Name: "connect",
			Flags: []cli.Flag{
				&cli.StringFlag{
					Name: "username",
				},
			},
			Action: func(ctx *cli.Context) error {
				fmt.Printf("username: %v\n", ctx.String("username"))
				fmt.Printf("format: %v\n", ctx.String("format"))
				return nil
			},
		},
	}

	if err := app.Run(os.Args); err != nil {
		log.Fatal(err)
	}
}

When invoked like so: rhc --format hello connect, it outputs:

format: hello

However, when invoked like so: rhc connect --format hello, it will error. The flags defined in a parent command must immediately follow the command on which they are defined.

Have you considered adding --format as a top-level flag?

@jirihnidek
Copy link
Contributor Author

I agree that it would be good to have --format json as top-level flag, but I would like to have support for machine-readable output for commands. Should I implement --format json for connect command as part of this PR?

What bothers me is a fact that we will break compatibility with rhc status --format json.

TBH: I don't like the behavior of github.com/urfave/cli that you have to use flags of parent command immediately after main command. This is something unusual.

There is alfa version (v3) of github.com/urfave/cli (we still use v2) ... I will try it if the behavior is still the same in v3.

@jirihnidek
Copy link
Contributor Author

jirihnidek commented May 7, 2024

The v3 support persistent flags introduces in this PR: urfave/cli#1568

The code looks like this:

package main

import (
	"context"
	"fmt"
	"github.com/urfave/cli/v3"
	"log"
	"os"
)

func main() {
	cmd := cli.Command{
		Name: "rhc",
		Commands: []*cli.Command{
			{
				Name: "connect",
				Flags: []cli.Flag{
					&cli.StringFlag{
						Name: "username",
					},
				},
				Action: func(ctx context.Context, command *cli.Command) error {
					fmt.Printf("username: %s\n", command.String("username"))
					fmt.Printf("format: %s\n", command.String("format"))
					return nil
				},
			},
		},
		Flags: []cli.Flag{
			&cli.StringFlag{
				Name:       "format",
				Persistent: true,
			},
		},
	}

	err := cmd.Run(context.Background(), os.Args)
	if err != nil {
		log.Fatal(err)
	}
}

If a flag is marked as persistent, then you can use global flag after sub-command like this:

jhnidek@thinkpad-p1:~/tmp/go_cli$ ./main --format json connect --username pepa
username: pepa
format: json
jhnidek@thinkpad-p1:~/tmp/go_cli$ ./main connect --username pepa --format json
username: pepa
format: json
jhnidek@thinkpad-p1:~/tmp/go_cli$ ./main --format xml connect --username pepa --format json
username: pepa
format: json

Unfortunately this feature hasn't been back-ported to v2.

@subpop
Copy link
Collaborator

subpop commented May 8, 2024

Oh, that's exactly what we should use. Let's proceed with separate --format flags for each subcommand, and when we update to cli/v3, we can refactor the code to use a single, persistent flag. That way we can support --format for the subcommands that need them and maintain compatibility with our CLI when we do eventually refactor.

@subpop subpop self-requested a review May 8, 2024 16:27
main.go Outdated Show resolved Hide resolved
main.go Outdated
Comment on lines 459 to 460
// disconnectAction tries to stop rhscd service, disconnect from Red Hat Insights, and finally
// it unregister system from Red Hat Subscription Management
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
// disconnectAction tries to stop rhscd service, disconnect from Red Hat Insights, and finally
// it unregister system from Red Hat Subscription Management
// disconnectAction tries to stop rhcd service, disconnect from Red Hat Insights, and
// finally it unregisters system from Red Hat Subscription Management

main.go Show resolved Hide resolved
main.go Outdated Show resolved Hide resolved
main.go Outdated Show resolved Hide resolved
main.go Outdated Show resolved Hide resolved
Copy link
Collaborator

@subpop subpop left a comment

Choose a reason for hiding this comment

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

I noticed a significant amount of conditional checks on the value of uiSettings.isMachineReadable, and some data structure transformation between DisconnectResult and error. I recommend looking into a way to make DisconnectResult conform to the error interface. I think that could result in reducing a lot of the conditional checks, and might streamline some of the code paths that are trying to transform a DisconnectResult into and error.

You might even consider making a generic Result interface type that DisconnectResult can implement. This could be expanded to other command results such as status (with a StatusResult) and connect (with a ConnectResult.

@jirihnidek
Copy link
Contributor Author

jirihnidek commented May 9, 2024

I agree that there is too many checks of uiSettings.isMachineReadable. I would like to simplify this. Problem is that we try to print some progress (spinner, progress messaged, colors, etc.), when --format CLI is not used. For this reason we just cannot defer printing results in all cases.

The DisconnectResult can also contain many error messages from stopping yggdrasil service, insights-client, rhsm.service. Thus, I still do not understand how implementing func (err DisconnectResult) Error() interface could help to simplify the code. The DisconnectResult is never converted to error.

main.go Outdated Show resolved Hide resolved
main.go Outdated Show resolved Hide resolved
main.go Show resolved Hide resolved
@jirihnidek jirihnidek force-pushed the jhnidek/disconnect_json_output branch from 2fe764b to bda0cad Compare May 13, 2024 09:17
@jirihnidek
Copy link
Contributor Author

jirihnidek commented May 13, 2024

Refactored code a little bit. I hope that it is better now.

@jirihnidek jirihnidek force-pushed the jhnidek/disconnect_json_output branch from bda0cad to 0f31209 Compare May 14, 2024 08:05
@jirihnidek jirihnidek requested a review from subpop June 13, 2024 14:47
@jirihnidek
Copy link
Contributor Author

@subpop Hi, can you please review this PR again?

Copy link
Collaborator

@subpop subpop left a comment

Choose a reason for hiding this comment

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

Looks good over all. I just have a couple very minor questions.

main.go Outdated
if uiSettings.isMachineReadable {
defer func(disconnectResult *DisconnectResult) {
// When exit code is zero, then print machine-readable output
// When exit code has non-zero value, then disconnectResult is returned as a error
Copy link
Collaborator

Choose a reason for hiding this comment

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

This comment doesn't see to align with the behavior of the code. Is it left over
from some previous code? This is one of the pitfalls of using comments to
describe the algorithm of the code.

main.go Outdated
// change returned error to CLI exit error to be able to set exit code to
// a non-zero value
if err != nil {
panic(fmt.Errorf("unable to print status as %s document: %s", format, err.Error()))
Copy link
Collaborator

Choose a reason for hiding this comment

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

Typically, it's not necessary to explicitly call Error() on an error type. This
should be sufficient.

Suggested change
panic(fmt.Errorf("unable to print status as %s document: %s", format, err.Error()))
panic(fmt.Errorf("unable to print status as %v document: %v", format, err))

main.go Outdated
}(&disconnectResult)
}

disconnectResult.exitCode = 0
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this necessary? The zero value of an int is 0 already.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Refactored code to not duplicate code. Only Error() interface is used now.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Nice refactor! I like the change and use of Error interface.

@jirihnidek jirihnidek force-pushed the jhnidek/disconnect_json_output branch from 0f31209 to ebea78b Compare August 21, 2024 13:22
@subpop
Copy link
Collaborator

subpop commented Aug 26, 2024

There appear to be some valid lint errors related to printf format strings.

main.go Outdated

// rhcPrintf is method for printing human-readable output. It suppresses output, when
// machine-readable format is used.
func rhcPrintf(format string, a ...interface{}) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

The name of this function doesn't indicate its purpose clearly to me. Could we
call it something like interactivePrintf?

* It is possible to print output of disconnect command in
  machine-readable format. The --format requires argument
  and it could be only "json" ATM. It follows practice of
  status command.
* Refactored code a little to share some code with status
  command.
* When --format json is used, then use only Error() interface
  for printing disconnect result
@jirihnidek jirihnidek force-pushed the jhnidek/disconnect_json_output branch from ebea78b to d79ca95 Compare September 2, 2024 09:01
* Fix some code style issues and make code linter more happy
@jirihnidek jirihnidek force-pushed the jhnidek/disconnect_json_output branch from d79ca95 to 1b53bd3 Compare September 3, 2024 12:03
Copy link
Collaborator

@subpop subpop left a comment

Choose a reason for hiding this comment

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

OK. This looks good to me!

@subpop subpop merged commit 87c37da into main Sep 4, 2024
23 of 24 checks passed
@subpop subpop deleted the jhnidek/disconnect_json_output branch September 4, 2024 13:48
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