Skip to content

Wrapper around help text #11

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

Merged
merged 3 commits into from
Oct 8, 2018
Merged

Wrapper around help text #11

merged 3 commits into from
Oct 8, 2018

Conversation

yorinasub17
Copy link
Contributor

This adds the wrapped help text printer to gruntwork-cli and a function to create a new cli.App that takes advantage of it.

Sample output:

Main help

Original

NAME:
   houston - A CLI tool for interacting with Gruntwork Houston that you can use to authenticate to AWS on the CLI and to SSH to your EC2 Instances.

USAGE:
   houston [global options] command [command options] [arguments...]

VERSION:
   snapshot

COMMANDS:
     exec       Execute a command with temporary AWS credentials obtained by logging into Gruntwork Houston
     ssh        Connect to an EC2 instance via SSH with your public key.
     configure  Configure houston CLI options.
     profiles   Show a status of all the defined profiles.
     start      Start the background HTTP server process so it can cache your AWS credentials. Note: the exec command starts the HTTP server process automatically.
     stop       Stop the background HTTP server process.
     status     Show the status of the background HTTP server process.
     help, h    Shows a list of commands or help for one command

GLOBAL OPTIONS:
   --help, -h     show help
   --version, -v  print the version

New:

Usage: houston [--help] [--version] command [options] [args]

A CLI tool for interacting with Gruntwork Houston that you can use to
authenticate to AWS on the CLI and to SSH to your EC2 Instances.

Commands:

   exec       Execute a command with temporary AWS credentials obtained by
              logging into Gruntwork Houston
   ssh        Connect to an EC2 instance via SSH with your public key.
   configure  Configure houston CLI options.
   profiles   Show a status of all the defined profiles.
   start      Start the background HTTP server process so it can cache your
              AWS credentials. Note: the exec command starts the HTTP server
              process automatically.
   stop       Stop the background HTTP server process.
   status     Show the status of the background HTTP server process.
   help, h    Shows a list of commands or help for one command

Command help

Original:

NAME:
   houston exec - Execute a command with temporary AWS credentials obtained by logging into Gruntwork Houston

USAGE:
   houston exec [OPTIONS] <profile> -- <command>

DESCRIPTION:
   The exec command makes it easier to use CLI tools that need AWS credentials, such as aws, terraform, and packer. Here's how it works:

   1. The first time you run this command for a <profile>, it will open your web browser and have you login to your Identity Provider (i.e., Google, ADFS, Okta).
   2. After login, the Identity Provider will redirect you to the Gruntwork Houston web console, where you will pick the AWS IAM Role you want to use.
   3. Gruntwork Houston will fetch temporary AWS credentials for this IAM Role and POST them back to houston CLI running on your computer.
   4. The houston CLI will set those credentials as the appropriate environment variables and execute <command>.
   5. The houston CLI will cache those credentials in memory, so all subsequent commands will execute without going through the login flow (until those credentials expire).

EXAMPLES:

   houston exec dev -- aws s3 ls
   houston exec prod -- terraform apply
   houston exec stage -- packer build server.json

OPTIONS:
   --config value, -c value  The configuration file for houston (default: "/Users/yoriy/.houston/houston.yml")
   --port value              The TCP port the http server is running on (default: 41170)

New:

Usage: houston exec [options] <profile> -- <command>

The exec command makes it easier to use CLI tools that need AWS credentials,
such as aws, terraform, and packer. Here's how it works:

  1. The first time you run this command for a <profile>, it will open your web
  browser and have you login to your Identity Provider (i.e., Google, ADFS,
  Okta).
  2. After login, the Identity Provider will redirect you to the Gruntwork
  Houston web console, where you will pick the AWS IAM Role you want to use.
  3. Gruntwork Houston will fetch temporary AWS credentials for this IAM Role
  and POST them back to houston CLI running on your computer.
  4. The houston CLI will set those credentials as the appropriate environment
  variables and execute <command>.
  5. The houston CLI will cache those credentials in memory, so all subsequent
  commands will execute without going through the login flow (until those
  credentials expire).

Examples:

   houston exec dev -- aws s3 ls
   houston exec prod -- terraform apply
   houston exec stage -- packer build server.json

Options:

   --config value, -c value  The configuration file for houston (default:
                             "/Users/yoriy/.houston/houston.yml")
   --port value              The TCP port the http server is running on (default:
                             41170)

Copy link
Member

@brikis98 brikis98 left a comment

Choose a reason for hiding this comment

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

Very nice! 👍


const HELP_TEXT_LINE_WIDTH = 80

const CLI_APP_HELP_TEMPLATE = `Usage: {{.HelpName}} {{range $index, $option := .VisibleFlags}}[{{$option.GetName | PrefixedFirstFlagName}}] {{end}}{{if .Commands}}command [options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[args]{{end}}{{if .Description}}
Copy link
Member

Choose a reason for hiding this comment

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

What is different about these templates than the default urfave ones? May be worth mentioning in a comment.

// text := "one two three"
// out := SplitKeepDelimiter(re, text)
// out == ["one ", "two ", "three"]
func SplitKeepDelimiter(re *regexp.Regexp, str string) (out []string) {
Copy link
Member

Choose a reason for hiding this comment

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

Why declare out in the return value?

Copy link
Member

Choose a reason for hiding this comment

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

Also, does SplitAfterN not do the same thing?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why declare out in the return value?

No strong reason, just that I noticed a few places in sample go code preferring to declare in return value if you are just going to var out. Is it generally preferable to declare return vars in the function?

Also, does SplitAfterN not do the same thing?

strings.SplitAfter does do the same thing, but it requires a specific substring. Here, we need to split on regex because the separator is volatile (at least one whitespace char) and so there is no exact match string that can be used.

Copy link
Member

Choose a reason for hiding this comment

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

Is it generally preferable to declare return vars in the function?

I don't know what "idiomatic" Go code does (TBH, I'm not sure it's mature enough of a language for that to be well defined), but I find it easier to read and reason about the code if all variable declarations are in the body of the method, rather than some there and some in the return value.

strings.SplitAfter does do the same thing, but it requires a specific substring. Here, we need to split on regex because the separator is volatile (at least one whitespace char) and so there is no exact match string that can be used.

Ah, gotcha.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Renamed this func to RegexpSplitAfter to make it clearer that this is the regex version of strings.SplitAfter.

}

// Wrap text to line width, while preserving any indentation
func TabAwareWrapText(text string, lineWidth int, tab string) string {
Copy link
Member

Choose a reason for hiding this comment

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

Add some examples to the comments to provide context on what this function is doing

func NewApp() *cli.App {
cli.HelpPrinter = WrappedHelpPrinter
cli.AppHelpTemplate = CLI_APP_HELP_TEMPLATE
cli.CommandHelpTemplate = CLI_COMMAND_HELP_TEMPLATE
Copy link
Member

Choose a reason for hiding this comment

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

Hm, in some of our apps, we overwrite these help templates. E.g., Terragrunt: https://github.com/gruntwork-io/terragrunt/blob/master/cli/cli_app.go#L151 (note, we have not yet migrated Terragrunt to use gruntwork-cli under the hood, so it's just an example). Will the wrapped help text work for these apps?

Copy link
Contributor Author

@yorinasub17 yorinasub17 Oct 7, 2018

Choose a reason for hiding this comment

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

That should work. Actually, these are global variables in urfave/cli that are read in as part of the help command, so a later overwrites will take precedence. You just have to override after the NewApp call, as opposed to before:

cli := entrypoint.NewAll()
cli.AppHelpTemplate = HELP_TEMPLATE

That said, it doesn't handle the elastic tabstop output table so for that example the output is not so nice (pasted below). Once I switched the template to raw tabs, it looked better so may not be worth generalizing.

No tabs:

DESCRIPTION:
   terragrunt - Terragrunt is a thin wrapper for Terraform that provides extra
   tools for working with multiple
   Terraform modules, remote state, and locking. For documentation, see
   https://github.com/gruntwork-io/terragrunt/.

USAGE:
   terragrunt <COMMAND>

COMMANDS:
   plan-all             Display the plans of a 'stack' by running 'terragrunt
   plan' in each subfolder
   apply-all            Apply a 'stack' by running 'terragrunt apply' in each
   subfolder
   output-all           Display the outputs of a 'stack' by running 'terragrunt
   output' in each subfolder
   destroy-all          Destroy a 'stack' by running 'terragrunt destroy' in
   each subfolder
   validate-all         Validate 'stack' by running 'terragrunt validate' in
   each subfolder
   *                    Terragrunt forwards all other commands directly to
   Terraform

GLOBAL OPTIONS:
   terragrunt-config                    Path to the Terragrunt config file.
   Default is terraform.tfvars.
   terragrunt-tfpath                    Path to the Terraform binary. Default is
   terraform (on PATH).
   terragrunt-no-auto-init              Don't automatically run 'terraform init'
   during other terragrunt commands. You must run 'terragrunt init' manually.
   terragrunt-no-auto-retry             Don't automatically re-run command in
   case of transient errors.
   terragrunt-non-interactive           Assume "yes" for all prompts.
   terragrunt-working-dir               The path to the Terraform templates.
   Default is current directory.
   terragrunt-download-dir              The path where to download Terraform
   code. Default is .terragrunt-cache in the working directory.
   terragrunt-source                    Download Terraform configurations from
   the specified source into a temporary folder, and run Terraform in that
   temporary folder.
   terragrunt-source-update             Delete the contents of the temporary
   folder to clear out any old, cached source code before downloading new source
   code into it.
   terragrunt-iam-role                 Assume the specified IAM role
                                       before executing Terraform.
                                       Can also be set via the
                                       TERRAGRUNT_IAM_ROLE
                                       environment variable.
   terragrunt-ignore-dependency-errors  *-all commands continue processing
   components even if a dependency fails.
   terragrunt-exclude-dir               Unix-style glob of directories to
   exclude when running *-all commands

VERSION:


AUTHOR(S):
   Gruntwork <www.gruntwork.io>

with tabs

DESCRIPTION:                                                                                                                                         [65/7627]
   terragrunt - Terragrunt is a thin wrapper for Terraform that provides extra
   tools for working with multiple
   Terraform modules, remote state, and locking. For documentation, see
   https://github.com/gruntwork-io/terragrunt/.

USAGE:
   terragrunt <COMMAND>

COMMANDS:
   plan-all      Display the plans of a 'stack' by running 'terragrunt plan'
                 in each subfolder
   apply-all     Apply a 'stack' by running 'terragrunt apply' in each
                 subfolder
   output-all    Display the outputs of a 'stack' by running 'terragrunt
                 output' in each subfolder
   destroy-all   Destroy a 'stack' by running 'terragrunt destroy' in each
                 subfolder
   validate-all  Validate 'stack' by running 'terragrunt validate' in each
                 subfolder
   *             Terragrunt forwards all other commands directly to Terraform

GLOBAL OPTIONS:
   terragrunt-config                    Path to the Terragrunt config file. Default is
                                        terraform.tfvars.
   terragrunt-tfpath                    Path to the Terraform binary. Default is terraform
                                        (on PATH).
   terragrunt-no-auto-init              Don't automatically run 'terraform init'
                                        during other terragrunt commands. You must run
                                        'terragrunt init' manually.
   terragrunt-no-auto-retry             Don't automatically re-run command in case of
                                        transient errors.
   terragrunt-non-interactive           Assume "yes" for all prompts.
   terragrunt-working-dir               The path to the Terraform templates. Default is
                                        current directory.
   terragrunt-download-dir              The path where to download Terraform code.
                                        Default is .terragrunt-cache in the working
                                        directory.
   terragrunt-source                    Download Terraform configurations from the specified
                                        source into a temporary folder, and run Terraform in
                                        that temporary folder.
   terragrunt-source-update             Delete the contents of the temporary folder
                                        to clear out any old, cached source code
                                        before downloading new source code into it.
   terragrunt-iam-role                  Assume the specified IAM role before executing
                                        Terraform. Can also be set via the
                                        TERRAGRUNT_IAM_ROLE environment variable.
   terragrunt-ignore-dependency-errors  *-all commands continue processing
                                        components even if a dependency
                                        fails.
   terragrunt-exclude-dir               Unix-style glob of directories to exclude when
                                        running *-all commands

VERSION:


AUTHOR(S):
   Gruntwork <www.gruntwork.io>

// char.
tabLength := 8
currentLineLength := TabAwareStringLength(wrapped, tabLength)
for _, word := range words[1:] {
Copy link
Member

Choose a reason for hiding this comment

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

In general, this sort of code can be easier to read and maintain if written recursively:

func TabAwareWrapText(words []string, lineWidth int, tab string, outputLinesSoFar []string): string {
  if len(words) == 0 {
    return strings.Join(outputLinesSoFar, "\n")
  }

  head = words[0]
  tail = words[1:]

  if lineLengthExceeded(outputLinesSoFar, head, lineWidth) {
    return TabAwareWrapText(tail, lineWidth, tab, addWordOnNewLine(outputLinesSoFar, head))
  } else {
    return TabAwareWrapText(tail, lineWidth, tab, addWordToExistingLine(outputLinesSoFar, head))
  }
}

Not sure it's worth refactoring in this case, as (a) Go isn't exactly a functional programming language and (b) I'm not sure it does tail call optimization, which may be a problem with long strings.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agree. I was going to write recursively, but then I learned that the maintainers of Go isn't too keen on supporting tail call optimization so opted not to. Sad :(

Copy link
Member

Choose a reason for hiding this comment

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

Gah. Stupid Go.

}
}

var wrapTextTests = []struct {
Copy link
Member

Choose a reason for hiding this comment

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

Probably a good idea to add some real-world help text (with many lines) as tests, such as the ones in the PR description.

@yorinasub17
Copy link
Contributor Author

yorinasub17 commented Oct 7, 2018

UPDATE:

  • Comments describing difference of these templates vs the default ones
  • Add a text that came out of urfave/cli into unit test
  • No more return type declared variables
  • TabAwareWrapText => IndentAwareWrapText (the latter is a more accurate name)
  • Comment with example of what IndentAwareWrapText is doing
  • Make linewidth a non const so that it can be overridden.
  • SplitKeepDelimiter => RegexpSplitAfter

TODO:

  • Unit test for the actual printer (the main func) checking for
    • Using the new templates as opposed to the default
    • Overriding linewidth
    • Real world example

@yorinasub17
Copy link
Contributor Author

Ok added some unit tests with real world examples for the overall printers. @brikis98 Ready for final review before merge!

Copy link
Member

@brikis98 brikis98 left a comment

Choose a reason for hiding this comment

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

This looks great. Nice work!

app := cli.NewApp()
// Create a new CLI app. This will return a urfave/cli App with some
// common initialization.
app := entrypoint.NewApp()
Copy link
Member

Choose a reason for hiding this comment

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

Make sure to call out this change in the release notes!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just to clarify, you mean https://github.com/gruntwork-io/gruntwork-cli/releases and newsletter and not a special file in here that tracks changes? Also, this should be a minor release (0.2.0) because it is a backwards compatible feature release right?

Copy link
Member

Choose a reason for hiding this comment

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

Yes on all counts!


`

const EXPECTED_EXEC_CMD_HELP_OUT_120_LINES = `Usage: houston exec [options] <profile> -- <command>
Copy link
Member

Choose a reason for hiding this comment

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

Nice tests 👍

@yorinasub17
Copy link
Contributor Author

Ok merging and doing a release! Thanks!

@yorinasub17 yorinasub17 merged commit a972613 into master Oct 8, 2018
@yorinasub17 yorinasub17 deleted the yori-help-utils branch October 8, 2018 22:23
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