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..2adb7cf1e 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..bff3ecadc --- /dev/null +++ b/internal/cmd/api/api.go @@ -0,0 +1,70 @@ +package api + +import ( + "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(_ 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 }