diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 729a91a6..2803f703 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 +``` diff --git a/cmd/hcloud/main.go b/cmd/hcloud/main.go index 2998caed..1d98ebeb 100644 --- a/cmd/hcloud/main.go +++ b/cmd/hcloud/main.go @@ -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) } diff --git a/internal/cmd/firewall/firewall.go b/internal/cmd/firewall/firewall.go index 45b6cd42..a41be725 100644 --- a/internal/cmd/firewall/firewall.go +++ b/internal/cmd/firewall/firewall.go @@ -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 } diff --git a/internal/cmd/floatingip/floatingip.go b/internal/cmd/floatingip/floatingip.go index 1d0dd0b0..c9e8b36c 100644 --- a/internal/cmd/floatingip/floatingip.go +++ b/internal/cmd/floatingip/floatingip.go @@ -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 } diff --git a/internal/cmd/image/image.go b/internal/cmd/image/image.go index e4c0a3bd..cad8cf5d 100644 --- a/internal/cmd/image/image.go +++ b/internal/cmd/image/image.go @@ -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 } diff --git a/internal/cmd/loadbalancer/load_balancer.go b/internal/cmd/loadbalancer/load_balancer.go index 2ed342a5..12a06463 100644 --- a/internal/cmd/loadbalancer/load_balancer.go +++ b/internal/cmd/loadbalancer/load_balancer.go @@ -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), ) diff --git a/internal/cmd/network/network.go b/internal/cmd/network/network.go index 014b227c..cf9a23c2 100644 --- a/internal/cmd/network/network.go +++ b/internal/cmd/network/network.go @@ -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 } diff --git a/internal/cmd/primaryip/primaryip.go b/internal/cmd/primaryip/primaryip.go index 601b9372..3af9eee2 100644 --- a/internal/cmd/primaryip/primaryip.go +++ b/internal/cmd/primaryip/primaryip.go @@ -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 } diff --git a/internal/cmd/server/server.go b/internal/cmd/server/server.go index a51cc13b..c44d8465 100644 --- a/internal/cmd/server/server.go +++ b/internal/cmd/server/server.go @@ -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 } diff --git a/internal/cmd/util/util.go b/internal/cmd/util/util.go index 08af39a4..dd62a127 100644 --- a/internal/cmd/util/util.go +++ b/internal/cmd/util/util.go @@ -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...) +} diff --git a/internal/cmd/volume/volume.go b/internal/cmd/volume/volume.go index 7eb82909..b6a74569 100644 --- a/internal/cmd/volume/volume.go +++ b/internal/cmd/volume/volume.go @@ -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 }