Skip to content

Commit

Permalink
feat: group subcommands in command help (#675)
Browse files Browse the repository at this point in the history
This PR adds subcommand groups, which make the help output more
structured and easier to read. Conventions on which groups to add and
how to name them is also added.

---------

Co-authored-by: pauhull <22707808+pauhull@users.noreply.github.com>
phm07 and phm07 authored Feb 1, 2024

Verified

This commit was signed with the committer’s verified signature.
EVODelavega Elias Van Ootegem
1 parent e8f3620 commit 0cb271f
Showing 11 changed files with 288 additions and 71 deletions.
110 changes: 107 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,130 @@
# Developing hcloud-cli

## Generated files

This repository contains generated files, mainly for testing purposes. These files are generated by running

```sh
go generate ./...
```

in the root directory of this repository. Make sure to keep generated files up-to-date
when making changes to the code.

## Unit tests
Unit tests are located in the `internal` directory. Run them with

Unit tests are located in the `internal` directory. Run them with:

```sh
go test ./...
```

## Build
To build the binary, run

To build the binary, run:

```sh
go build -o hcloud-cli ./cmd/hcloud
```

To include version information in the resulting binary and build for all targets, use GoReleaser:
To include version information in the resulting binary and build for all targets, use GoReleaser:

```sh
goreleaser --snapshot --skip-publish --rm-dist
```

## Conventions

### Subcommand groups

Cobra offers the functionality to group subcommands. The conventions on when and how to group commands are as follows:

1. Use the following Categories:
- **General** (CRUD operations such as `create`, `delete`, `describe`, `list`, `update` + Label
commands `add-`/`remove-label`)
- Groups based on actions (e.g. `enable`, `disable`, `attach`, `detach`), for example `enable-`/`disable-protection`
or `poweron`/`poweroff`/`shutdown`/`reset`/`reboot`
- **Additional commands** that don't fit into the other categories (`Group.ID` is empty). These should be
utility commands, for example ones that perform client-side actions.
2. Groups are only needed if more than the "General" group commands are present.
3. `Group.ID` formatting:
1. If the `ID` is a noun, it should be singular.
2. If the `ID` is a verb, it should be in the infinitive form.
3. The `ID` should be in kebab-case.
4. `Group.Title` formatting:
1. Should be the `ID` but capitalized and with spaces instead of dashes.
2. If a single resource is managed, the `Title` should be singular. Otherwise, it should be plural.

Example: Multiple network can be attached to server -> `Networks`, but only one ISO can be attached to a server -> `ISO`

Here is how to create a group:

```go
// General
util.AddGroup(cmd, "general", "General",
SomeGeneralCommand.CobraCommand(s),
// ...
)

// Additional commands
cmd.AddCommand(
SomeUtilCommand.CobraCommand(s),
// ...
)
```

Example of the `hcloud server` command groups:

```
General:
add-label Add a label to a server
change-type Change type of a server
create Create a server
create-image Create an image from a server
delete Delete a server
describe Describe a server
list List Servers
rebuild Rebuild a server
remove-label Remove a label from a server
update Update a Server
Protection:
disable-protection Disable resource protection for a server
enable-protection Enable resource protection for a server
Rescue:
disable-rescue Disable rescue for a server
enable-rescue Enable rescue for a server
Power/Reboot:
poweroff Poweroff a server
poweron Poweron a server
reboot Reboot a server
reset Reset a server
shutdown Shutdown a server
Networks:
attach-to-network Attach a server to a network
change-alias-ips Change a server's alias IPs in a network
detach-from-network Detach a server from a network
ISO:
attach-iso Attach an ISO to a server
detach-iso Detach an ISO from a server
Placement Groups:
add-to-placement-group Add a server to a placement group
remove-from-placement-group Removes a server from a placement group
Backup:
disable-backup Disable backup for a server
enable-backup Enable backup for a server
Additional Commands:
ip Print a server's IP address
metrics [ALPHA] Metrics from a Server
request-console Request a WebSocket VNC console for a server
reset-password Reset the root password of a server
set-rdns Change reverse DNS of a Server
ssh Spawn an SSH connection for the server
```
13 changes: 9 additions & 4 deletions cmd/hcloud/main.go
Original file line number Diff line number Diff line change
@@ -23,6 +23,7 @@ import (
"github.com/hetznercloud/cli/internal/cmd/server"
"github.com/hetznercloud/cli/internal/cmd/servertype"
"github.com/hetznercloud/cli/internal/cmd/sshkey"
"github.com/hetznercloud/cli/internal/cmd/util"
"github.com/hetznercloud/cli/internal/cmd/version"
"github.com/hetznercloud/cli/internal/cmd/volume"
"github.com/hetznercloud/cli/internal/state"
@@ -52,16 +53,14 @@ func main() {
}

rootCommand := cli.NewRootCommand(s)
rootCommand.AddCommand(

util.AddGroup(rootCommand, "resource", "Resources",
all.NewCommand(s),
floatingip.NewCommand(s),
image.NewCommand(s),
server.NewCommand(s),
sshkey.NewCommand(s),
version.NewCommand(s),
completion.NewCommand(s),
servertype.NewCommand(s),
context.NewCommand(s),
datacenter.NewCommand(s),
location.NewCommand(s),
iso.NewCommand(s),
@@ -75,6 +74,12 @@ func main() {
primaryip.NewCommand(s),
)

rootCommand.AddCommand(
version.NewCommand(s),
completion.NewCommand(s),
context.NewCommand(s),
)

if err := rootCommand.Execute(); err != nil {
log.Fatalln(err)
}
16 changes: 12 additions & 4 deletions internal/cmd/firewall/firewall.go
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@ package firewall
import (
"github.com/spf13/cobra"

"github.com/hetznercloud/cli/internal/cmd/util"
"github.com/hetznercloud/cli/internal/state"
)

@@ -14,19 +15,26 @@ func NewCommand(s state.State) *cobra.Command {
TraverseChildren: true,
DisableFlagsInUseLine: true,
}
cmd.AddCommand(

util.AddGroup(cmd, "general", "General",
ListCmd.CobraCommand(s),
DescribeCmd.CobraCommand(s),
CreateCmd.CobraCommand(s),
DeleteCmd.CobraCommand(s),
UpdateCmd.CobraCommand(s),
LabelCmds.AddCobraCommand(s),
LabelCmds.RemoveCobraCommand(s),
)

util.AddGroup(cmd, "rule", "Rules",
ReplaceRulesCmd.CobraCommand(s),
DeleteCmd.CobraCommand(s),
AddRuleCmd.CobraCommand(s),
DeleteRuleCmd.CobraCommand(s),
)

util.AddGroup(cmd, "resource", "Resources",
ApplyToResourceCmd.CobraCommand(s),
RemoveFromResourceCmd.CobraCommand(s),
LabelCmds.AddCobraCommand(s),
LabelCmds.RemoveCobraCommand(s),
)
return cmd
}
25 changes: 17 additions & 8 deletions internal/cmd/floatingip/floatingip.go
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@ package floatingip
import (
"github.com/spf13/cobra"

"github.com/hetznercloud/cli/internal/cmd/util"
"github.com/hetznercloud/cli/internal/state"
)

@@ -14,19 +15,27 @@ func NewCommand(s state.State) *cobra.Command {
TraverseChildren: true,
DisableFlagsInUseLine: true,
}
cmd.AddCommand(
UpdateCmd.CobraCommand(s),

util.AddGroup(cmd, "general", "General",
ListCmd.CobraCommand(s),
CreateCmd.CobraCommand(s),
DescribeCmd.CobraCommand(s),
AssignCmd.CobraCommand(s),
UnassignCmd.CobraCommand(s),
CreateCmd.CobraCommand(s),
DeleteCmd.CobraCommand(s),
EnableProtectionCmd.CobraCommand(s),
DisableProtectionCmd.CobraCommand(s),
UpdateCmd.CobraCommand(s),
LabelCmds.AddCobraCommand(s),
LabelCmds.RemoveCobraCommand(s),
SetRDNSCmd.CobraCommand(s),
)

util.AddGroup(cmd, "protection", "Protection",
EnableProtectionCmd.CobraCommand(s),
DisableProtectionCmd.CobraCommand(s),
)

util.AddGroup(cmd, "assign", "Assign",
AssignCmd.CobraCommand(s),
UnassignCmd.CobraCommand(s),
)

cmd.AddCommand(SetRDNSCmd.CobraCommand(s))
return cmd
}
13 changes: 9 additions & 4 deletions internal/cmd/image/image.go
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@ package image
import (
"github.com/spf13/cobra"

"github.com/hetznercloud/cli/internal/cmd/util"
"github.com/hetznercloud/cli/internal/state"
)

@@ -14,15 +15,19 @@ func NewCommand(s state.State) *cobra.Command {
TraverseChildren: true,
DisableFlagsInUseLine: true,
}
cmd.AddCommand(

util.AddGroup(cmd, "general", "General",
ListCmd.CobraCommand(s),
DeleteCmd.CobraCommand(s),
DescribeCmd.CobraCommand(s),
DeleteCmd.CobraCommand(s),
UpdateCmd.CobraCommand(s),
EnableProtectionCmd.CobraCommand(s),
DisableProtectionCmd.CobraCommand(s),
LabelCmds.AddCobraCommand(s),
LabelCmds.RemoveCobraCommand(s),
)

util.AddGroup(cmd, "protection", "Protection",
EnableProtectionCmd.CobraCommand(s),
DisableProtectionCmd.CobraCommand(s),
)
return cmd
}
34 changes: 27 additions & 7 deletions internal/cmd/loadbalancer/load_balancer.go
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@ package loadbalancer
import (
"github.com/spf13/cobra"

"github.com/hetznercloud/cli/internal/cmd/util"
"github.com/hetznercloud/cli/internal/state"
)

@@ -15,27 +16,46 @@ func NewCommand(s state.State) *cobra.Command {
TraverseChildren: true,
DisableFlagsInUseLine: true,
}
cmd.AddCommand(
CreateCmd.CobraCommand(s),

util.AddGroup(cmd, "general", "General",
ListCmd.CobraCommand(s),
CreateCmd.CobraCommand(s),
DescribeCmd.CobraCommand(s),
DeleteCmd.CobraCommand(s),
UpdateCmd.CobraCommand(s),
LabelCmds.AddCobraCommand(s),
LabelCmds.RemoveCobraCommand(s),
ChangeAlgorithmCmd.CobraCommand(s),
ChangeTypeCmd.CobraCommand(s),
)

util.AddGroup(cmd, "protection", "Protection",
EnableProtectionCmd.CobraCommand(s),
DisableProtectionCmd.CobraCommand(s),
)

util.AddGroup(cmd, "target", "Targets",
AddTargetCmd.CobraCommand(s),
RemoveTargetCmd.CobraCommand(s),
ChangeAlgorithmCmd.CobraCommand(s),
)

util.AddGroup(cmd, "service", "Services",
AddServiceCmd.CobraCommand(s),
UpdateServiceCmd.CobraCommand(s),
DeleteServiceCmd.CobraCommand(s),
AddServiceCmd.CobraCommand(s),
EnableProtectionCmd.CobraCommand(s),
DisableProtectionCmd.CobraCommand(s),
)

util.AddGroup(cmd, "network", "Network",
AttachToNetworkCmd.CobraCommand(s),
DetachFromNetworkCmd.CobraCommand(s),
)

util.AddGroup(cmd, "public-interface", "Public Interface",
EnablePublicInterfaceCmd.CobraCommand(s),
DisablePublicInterfaceCmd.CobraCommand(s),
ChangeTypeCmd.CobraCommand(s),
)

cmd.AddCommand(
MetricsCmd.CobraCommand(s),
SetRDNSCmd.CobraCommand(s),
)
25 changes: 18 additions & 7 deletions internal/cmd/network/network.go
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@ package network
import (
"github.com/spf13/cobra"

"github.com/hetznercloud/cli/internal/cmd/util"
"github.com/hetznercloud/cli/internal/state"
)

@@ -14,22 +15,32 @@ func NewCommand(s state.State) *cobra.Command {
TraverseChildren: true,
DisableFlagsInUseLine: true,
}
cmd.AddCommand(

util.AddGroup(cmd, "general", "General",
ListCmd.CobraCommand(s),
DescribeCmd.CobraCommand(s),
DeleteCmd.CobraCommand(s),
CreateCmd.CobraCommand(s),
UpdateCmd.CobraCommand(s),
DeleteCmd.CobraCommand(s),
ChangeIPRangeCmd.CobraCommand(s),
AddRouteCmd.CobraCommand(s),
RemoveRouteCmd.CobraCommand(s),
AddSubnetCmd.CobraCommand(s),
RemoveSubnetCmd.CobraCommand(s),
LabelCmds.AddCobraCommand(s),
LabelCmds.RemoveCobraCommand(s),
ChangeIPRangeCmd.CobraCommand(s),
)

util.AddGroup(cmd, "protection", "Protection",
EnableProtectionCmd.CobraCommand(s),
DisableProtectionCmd.CobraCommand(s),
)

util.AddGroup(cmd, "route", "Routes",
AddRouteCmd.CobraCommand(s),
RemoveRouteCmd.CobraCommand(s),
ExposeRoutesToVSwitchCmd.CobraCommand(s),
)

util.AddGroup(cmd, "subnet", "Subnets",
AddSubnetCmd.CobraCommand(s),
RemoveSubnetCmd.CobraCommand(s),
)
return cmd
}
23 changes: 16 additions & 7 deletions internal/cmd/primaryip/primaryip.go
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@ package primaryip
import (
"github.com/spf13/cobra"

"github.com/hetznercloud/cli/internal/cmd/util"
"github.com/hetznercloud/cli/internal/state"
)

@@ -14,19 +15,27 @@ func NewCommand(s state.State) *cobra.Command {
TraverseChildren: true,
DisableFlagsInUseLine: true,
}
cmd.AddCommand(

util.AddGroup(cmd, "general", "General",
ListCmd.CobraCommand(s),
DescribeCmd.CobraCommand(s),
DeleteCmd.CobraCommand(s),
CreateCmd.CobraCommand(s),
UpdateCmd.CobraCommand(s),
DeleteCmd.CobraCommand(s),
AssignCmd.CobraCommand(s),
UnAssignCmd.CobraCommand(s),
SetRDNSCmd.CobraCommand(s),
EnableProtectionCmd.CobraCommand(s),
DisableProtectionCmd.CobraCommand(s),
LabelCmds.AddCobraCommand(s),
LabelCmds.RemoveCobraCommand(s),
)

util.AddGroup(cmd, "protection", "Protection",
EnableProtectionCmd.CobraCommand(s),
DisableProtectionCmd.CobraCommand(s),
)

util.AddGroup(cmd, "assign", "Assign",
AssignCmd.CobraCommand(s),
UnAssignCmd.CobraCommand(s),
)

cmd.AddCommand(SetRDNSCmd.CobraCommand(s))
return cmd
}
66 changes: 46 additions & 20 deletions internal/cmd/server/server.go
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@ package server
import (
"github.com/spf13/cobra"

"github.com/hetznercloud/cli/internal/cmd/util"
"github.com/hetznercloud/cli/internal/state"
)

@@ -14,41 +15,66 @@ func NewCommand(s state.State) *cobra.Command {
TraverseChildren: true,
DisableFlagsInUseLine: true,
}
cmd.AddCommand(

util.AddGroup(cmd, "general", "General",
ListCmd.CobraCommand(s),
DescribeCmd.CobraCommand(s),
CreateCmd.CobraCommand(s),
DeleteCmd.CobraCommand(s),
RebootCmd.CobraCommand(s),
PoweronCmd.CobraCommand(s),
PoweroffCmd.CobraCommand(s),
ResetCmd.CobraCommand(s),
ShutdownCmd.CobraCommand(s),
CreateImageCmd.CobraCommand(s),
ResetPasswordCmd.CobraCommand(s),
EnableRescueCmd.CobraCommand(s),
DisableRescueCmd.CobraCommand(s),
AttachISOCmd.CobraCommand(s),
DetachISOCmd.CobraCommand(s),
UpdateCmd.CobraCommand(s),
CreateImageCmd.CobraCommand(s),
ChangeTypeCmd.CobraCommand(s),
RebuildCmd.CobraCommand(s),
EnableBackupCmd.CobraCommand(s),
DisableBackupCmd.CobraCommand(s),
EnableProtectionCmd.CobraCommand(s),
DisableProtectionCmd.CobraCommand(s),
SSHCmd.CobraCommand(s),
LabelCmds.AddCobraCommand(s),
LabelCmds.RemoveCobraCommand(s),
SetRDNSCmd.CobraCommand(s),
)

util.AddGroup(cmd, "protection", "Protection",
EnableProtectionCmd.CobraCommand(s),
DisableProtectionCmd.CobraCommand(s),
)

util.AddGroup(cmd, "rescue", "Rescue",
EnableRescueCmd.CobraCommand(s),
DisableRescueCmd.CobraCommand(s),
)

util.AddGroup(cmd, "power", "Power/Reboot",
PoweronCmd.CobraCommand(s),
PoweroffCmd.CobraCommand(s),
RebootCmd.CobraCommand(s),
ShutdownCmd.CobraCommand(s),
ResetCmd.CobraCommand(s),
)

util.AddGroup(cmd, "network", "Networks",
AttachToNetworkCmd.CobraCommand(s),
DetachFromNetworkCmd.CobraCommand(s),
ChangeAliasIPsCmd.CobraCommand(s),
)

util.AddGroup(cmd, "iso", "ISO",
AttachISOCmd.CobraCommand(s),
DetachISOCmd.CobraCommand(s),
)

util.AddGroup(cmd, "placement-group", "Placement Groups",
AddToPlacementGroupCmd.CobraCommand(s),
RemoveFromPlacementGroupCmd.CobraCommand(s),
)

util.AddGroup(cmd, "backup", "Backup",
EnableBackupCmd.CobraCommand(s),
DisableBackupCmd.CobraCommand(s),
)

cmd.AddCommand(
SSHCmd.CobraCommand(s),
IPCmd.CobraCommand(s),
RequestConsoleCmd.CobraCommand(s),
ResetPasswordCmd.CobraCommand(s),
MetricsCmd.CobraCommand(s),
AddToPlacementGroupCmd.CobraCommand(s),
RemoveFromPlacementGroupCmd.CobraCommand(s),
SetRDNSCmd.CobraCommand(s),
)
return cmd
}
12 changes: 12 additions & 0 deletions internal/cmd/util/util.go
Original file line number Diff line number Diff line change
@@ -192,3 +192,15 @@ func ValidateRequiredFlags(flags *pflag.FlagSet, names ...string) error {
}
return nil
}

// AddGroup adds a group to the passed command and adds the passed commands to the group.
func AddGroup(cmd *cobra.Command, id string, title string, groupCmds ...*cobra.Command) {
if !strings.HasSuffix(title, ":") {
title += ":"
}
cmd.AddGroup(&cobra.Group{ID: id, Title: title})
for _, groupCmd := range groupCmds {
groupCmd.GroupID = id
}
cmd.AddCommand(groupCmds...)
}
22 changes: 15 additions & 7 deletions internal/cmd/volume/volume.go
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@ package volume
import (
"github.com/spf13/cobra"

"github.com/hetznercloud/cli/internal/cmd/util"
"github.com/hetznercloud/cli/internal/state"
)

@@ -14,19 +15,26 @@ func NewCommand(s state.State) *cobra.Command {
TraverseChildren: true,
DisableFlagsInUseLine: true,
}
cmd.AddCommand(

util.AddGroup(cmd, "general", "General",
ListCmd.CobraCommand(s),
DescribeCmd.CobraCommand(s),
DeleteCmd.CobraCommand(s),
CreateCmd.CobraCommand(s),
UpdateCmd.CobraCommand(s),
DeleteCmd.CobraCommand(s),
DescribeCmd.CobraCommand(s),
AttachCmd.CobraCommand(s),
DetachCmd.CobraCommand(s),
LabelCmds.AddCobraCommand(s),
LabelCmds.RemoveCobraCommand(s),
ResizeCmd.CobraCommand(s),
)

util.AddGroup(cmd, "protection", "Protection",
EnableProtectionCmd.CobraCommand(s),
DisableProtectionCmd.CobraCommand(s),
LabelCmds.AddCobraCommand(s),
LabelCmds.RemoveCobraCommand(s),
)

util.AddGroup(cmd, "attach", "Attach",
AttachCmd.CobraCommand(s),
DetachCmd.CobraCommand(s),
)
return cmd
}

0 comments on commit 0cb271f

Please sign in to comment.