# Developing hcloud-cli

## Installing tools

We recommend installing tools using [mise](https://github.com/jdx/mise). Once mise is installed and activated
in your env, run `mise install` in this repository to install all required tools. Tools will then be automatically
added to your `PATH` when entering the directory.

## 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:

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

## Build

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:

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

## Conventions

### Command usage

Command usage descriptions (namely `cobra.Command.Use`) should follow the [docopt](http://docopt.org/) syntax.

Additionally:
- Optional flags should be the first in the usage string.
  - If there is more than one optional flag, you can use `[options]` to abbreviate.
- Required flags should always be documented explicitly and before the positional arguments, but after the 
  optional flags.

### 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
```

### Command validation

Please use the `util.Validate` method to make sure the command is validated against its usage string during runtime.
This can be done by setting the `Args` field of the `cobra.Command` struct to `util.Validate`:

```go
cmd := &cobra.Command{
   Use:   "my-command [options]",
   Args:  util.Validate,
   Run: func(cmd *cobra.Command, args []string) {
      // ...
   },
}
```

If you are using base commands like `base.Cmd` or `base.ListCmd` etc., this is already done for you.

**Note:** Your command usage needs to follow the [docopt](http://docopt.org/) syntax for this to work.
If your command uses optional positional arguments or other complicated usage that necessitates to disable
argument count checking, you use `util.ValidateLenient` instead. It will not throw an error if there are
more arguments than expected.

### Generated files

Generated files (that are created by running `go generate`) should be prefixed with `zz_`. This is to group them
together in the file list and to easily identify them as generated files. Also, it allows the CI to check if the
generated files are up-to-date.

### Experimental Features

When adding an experimental feature, make sure to mark it as such by using the `base.ExperimentalWrapper` function.

Example:

```go
var (
  ExperimentalProduct = ExperimentalWrapper("Product", "in beta", "https://docs.hetzner.cloud/changelog#new-product")
)

func (c) CobraCommand(s state.State) *cobra.Command {
  cmd := &cobra.Command{
    Use:     "command",
    Short:   "My experimental command",
    Long:    "This is an experimental command.",
    PreRunE: s.EnsureToken,
  }

  cmd.Run = func(cmd *cobra.Command, _ []string) {}

  return ExperimentalProduct(s, cmd)
}
```

It should contain the experimental product name, the maturity level and a link to the relevant changelog or documentation.
