From e986a9bcb706ea332f9fb33e78fcdef7d8d2c569 Mon Sep 17 00:00:00 2001 From: phm07 <22707808+phm07@users.noreply.github.com> Date: Fri, 17 Jul 2026 11:23:18 +0200 Subject: [PATCH 1/2] feat: add api command --- docs/reference/manual/hcloud.md | 1 + docs/reference/manual/hcloud_api.md | 36 +++++++++++++++ internal/cli/root.go | 2 + internal/cmd/api/api.go | 71 +++++++++++++++++++++++++++++ internal/hcapi2/client.go | 13 ++++++ internal/hcapi2/mock/client.go | 13 +++++- 6 files changed, 134 insertions(+), 2 deletions(-) create mode 100644 docs/reference/manual/hcloud_api.md create mode 100644 internal/cmd/api/api.go diff --git a/docs/reference/manual/hcloud.md b/docs/reference/manual/hcloud.md index d0a7a5395..4ff8b5b5c 100644 --- a/docs/reference/manual/hcloud.md +++ b/docs/reference/manual/hcloud.md @@ -25,6 +25,7 @@ A command-line interface for Hetzner Cloud ### SEE ALSO * [hcloud all](hcloud_all.md) - Commands that apply to all resources +* [hcloud api](hcloud_api.md) - Make API call * [hcloud certificate](hcloud_certificate.md) - Manage Certificates * [hcloud completion](hcloud_completion.md) - Output shell completion code for the specified shell * [hcloud config](hcloud_config.md) - Manage configuration diff --git a/docs/reference/manual/hcloud_api.md b/docs/reference/manual/hcloud_api.md new file mode 100644 index 000000000..088499bcc --- /dev/null +++ b/docs/reference/manual/hcloud_api.md @@ -0,0 +1,36 @@ +## hcloud api + +Make API call + +``` +hcloud api [options] +``` + +### Options + +``` + -d, --data string HTTP request body content (use - to read from stdin) + -h, --help help for api + -X, --method string HTTP method to use for API call (default "GET") + -v, --value stringToString key=value pairs to pass as query parameters (default []) +``` + +### Options inherited from parent commands + +``` + --config string Config file path (default "~/.config/hcloud/cli.toml") + --context string Currently active context + --debug Enable debug output + --debug-file string File to write debug output to + --endpoint string Hetzner Cloud API endpoint (default "https://api.hetzner.cloud/v1") + --hetzner-endpoint string Hetzner API endpoint (default "https://api.hetzner.com/v1") + --http-timeout duration Timeout for HTTP requests (default 0 = no timeout) + --no-experimental-warnings If true, experimental warnings are not shown + --poll-interval duration Interval at which to poll information, for example action progress (default 500ms) + --quiet If true, only print error messages +``` + +### SEE ALSO + +* [hcloud](hcloud.md) - Hetzner Cloud CLI + diff --git a/internal/cli/root.go b/internal/cli/root.go index d28bc2d29..02de98776 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -6,6 +6,7 @@ import ( "github.com/spf13/cobra" "github.com/hetznercloud/cli/internal/cmd/all" + "github.com/hetznercloud/cli/internal/cmd/api" "github.com/hetznercloud/cli/internal/cmd/certificate" "github.com/hetznercloud/cli/internal/cmd/completion" configCmd "github.com/hetznercloud/cli/internal/cmd/config" @@ -74,6 +75,7 @@ func NewRootCommand(s state.State) *cobra.Command { completion.NewCommand(s), context.NewCommand(s), configCmd.NewCommand(s), + api.ApiCmd.CobraCommand(s), ) cmd.PersistentFlags().AddFlagSet(s.Config().FlagSet()) diff --git a/internal/cmd/api/api.go b/internal/cmd/api/api.go new file mode 100644 index 000000000..bf297cc1a --- /dev/null +++ b/internal/cmd/api/api.go @@ -0,0 +1,71 @@ +package api + +import ( + _ "embed" + "encoding/json" + "io" + "net/url" + "os" + "strings" + + "github.com/spf13/cobra" + + "github.com/hetznercloud/cli/internal/cmd/base" + "github.com/hetznercloud/cli/internal/hcapi2" + "github.com/hetznercloud/cli/internal/state" +) + +var ApiCmd = base.Cmd{ + BaseCobraCommand: func(client hcapi2.Client) *cobra.Command { + cmd := &cobra.Command{ + Use: "api [options] ", + Short: "Make API call", + } + + cmd.Flags().StringP("method", "X", "GET", "HTTP method to use for API call") + cmd.Flags().StringP("data", "d", "", "HTTP request body content (use - to read from stdin)") + cmd.Flags().StringToStringP("value", "v", nil, "key=value pairs to pass as query parameters") + return cmd + }, + Run: func(s state.State, cmd *cobra.Command, args []string) error { + path := args[0] + method, _ := cmd.Flags().GetString("method") + data, _ := cmd.Flags().GetString("data") + values, _ := cmd.Flags().GetStringToString("value") + + var body io.Reader + switch data { + case "": + body = nil + case "-": + body = os.Stdin + default: + body = strings.NewReader(data) + } + + u, err := url.Parse(path) + if err != nil { + return err + } + + params := u.Query() + for k, v := range values { + params.Set(k, v) + } + u.RawQuery = params.Encode() + + req, err := s.Client().NewRequest(s, method, u.String(), body) + if err != nil { + return err + } + + var respBody json.RawMessage + _, err = s.Client().Do(req, &respBody) + if err != nil { + return err + } + + cmd.Print(string(respBody)) + return nil + }, +} diff --git a/internal/hcapi2/client.go b/internal/hcapi2/client.go index f8d9346db..cc79ba00f 100644 --- a/internal/hcapi2/client.go +++ b/internal/hcapi2/client.go @@ -1,6 +1,9 @@ package hcapi2 import ( + "context" + "io" + "net/http" "sync" "github.com/hetznercloud/hcloud-go/v2/hcloud" @@ -31,6 +34,8 @@ type Client interface { StorageBoxType() StorageBoxTypeClient Zone() ZoneClient WithOpts(...hcloud.ClientOption) + NewRequest(ctx context.Context, method, path string, body io.Reader) (*http.Request, error) + Do(req *http.Request, v any) (*hcloud.Response, error) } type clientCache struct { @@ -82,6 +87,14 @@ func (c *client) WithOpts(opts ...hcloud.ClientOption) { c.update() } +func (c *client) NewRequest(ctx context.Context, method, path string, body io.Reader) (*http.Request, error) { + return c.client.NewRequest(ctx, method, path, body) +} + +func (c *client) Do(req *http.Request, v any) (*hcloud.Response, error) { + return c.client.Do(req, v) +} + func (c *client) update() { c.client = hcloud.NewClient(c.opts...) c.cache = clientCache{} diff --git a/internal/hcapi2/mock/client.go b/internal/hcapi2/mock/client.go index 1e3aa9f0b..7d57559b5 100644 --- a/internal/hcapi2/mock/client.go +++ b/internal/hcapi2/mock/client.go @@ -1,10 +1,13 @@ package mock import ( + "context" + "io" + "net/http" + "go.uber.org/mock/gomock" "github.com/hetznercloud/cli/internal/hcapi2" - "github.com/hetznercloud/cli/internal/state/config" "github.com/hetznercloud/hcloud-go/v2/hcloud" ) @@ -157,6 +160,12 @@ func (*Client) WithOpts(_ ...hcloud.ClientOption) { // no-op } -func (*Client) FromConfig(_ config.Config) { +func (*Client) NewRequest(_ context.Context, _, _ string, _ io.Reader) (*http.Request, error) { + // no-op + return nil, nil +} + +func (*Client) Do(_ *http.Request, _ any) (*hcloud.Response, error) { // no-op + return nil, nil } From 95eaa367b9e4b7961bc8fadf0b1a1ee00017ed23 Mon Sep 17 00:00:00 2001 From: phm07 <22707808+phm07@users.noreply.github.com> Date: Fri, 17 Jul 2026 13:39:04 +0200 Subject: [PATCH 2/2] fix lint --- internal/cli/root.go | 2 +- internal/cmd/api/api.go | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/internal/cli/root.go b/internal/cli/root.go index 02de98776..2adb7cf1e 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -75,7 +75,7 @@ func NewRootCommand(s state.State) *cobra.Command { completion.NewCommand(s), context.NewCommand(s), configCmd.NewCommand(s), - api.ApiCmd.CobraCommand(s), + api.APICmd.CobraCommand(s), ) cmd.PersistentFlags().AddFlagSet(s.Config().FlagSet()) diff --git a/internal/cmd/api/api.go b/internal/cmd/api/api.go index bf297cc1a..bff3ecadc 100644 --- a/internal/cmd/api/api.go +++ b/internal/cmd/api/api.go @@ -1,7 +1,6 @@ package api import ( - _ "embed" "encoding/json" "io" "net/url" @@ -15,8 +14,8 @@ import ( "github.com/hetznercloud/cli/internal/state" ) -var ApiCmd = base.Cmd{ - BaseCobraCommand: func(client hcapi2.Client) *cobra.Command { +var APICmd = base.Cmd{ + BaseCobraCommand: func(_ hcapi2.Client) *cobra.Command { cmd := &cobra.Command{ Use: "api [options] ", Short: "Make API call",