From 5fef7c19f1e82252a9fb86a7946d7235a79c189a Mon Sep 17 00:00:00 2001 From: normanre Date: Sun, 1 Feb 2026 04:03:41 +0100 Subject: [PATCH 1/3] Added OptionStaticIp to set an static ip endpoint in config --- .gitignore | 1 + internal/state/config/options.go | 9 +++++++++ internal/state/state.go | 13 +++++++++++++ 3 files changed, 23 insertions(+) diff --git a/.gitignore b/.gitignore index d96ff5264..af98bedbd 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ /cmd/hcloud/hcloud hcloud_cli.p12 coverage.txt +/.idea \ No newline at end of file diff --git a/internal/state/config/options.go b/internal/state/config/options.go index 7f9c21d69..932038d03 100644 --- a/internal/state/config/options.go +++ b/internal/state/config/options.go @@ -115,6 +115,15 @@ var ( &overrides{configKey: "active_context"}, ) + OptionStaticIp = newOpt( + "static-ip", + "Use static ip for Hetzner API endpoint", + "", + DefaultPreferenceFlags, + nil, + nil, + ) + OptionEndpoint = newOpt( "endpoint", "Hetzner Cloud API endpoint", diff --git a/internal/state/state.go b/internal/state/state.go index c46f0b1b0..1e5094208 100644 --- a/internal/state/state.go +++ b/internal/state/state.go @@ -4,6 +4,8 @@ import ( "context" "fmt" "io" + "net" + "net/http" "os" "strings" @@ -128,5 +130,16 @@ func (c *state) newClient() (hcapi2.Client, error) { })) } + if staticIp, err := config.OptionStaticIp.Get(c.config); err == nil && staticIp != "" { + tr := &http.Transport{DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + split := strings.Split(addr, ":") + return net.Dial(network, fmt.Sprintf("%s:%s", staticIp, split[1])) + }} + client := &http.Client{Transport: tr} + opts = append(opts, hcloud.WithHTTPClient(client)) + } else if err != nil { + return nil, err + } + return hcapi2.NewClient(opts...), nil } From 80911aed66f78b6203e7ab65ac41da218b62aac4 Mon Sep 17 00:00:00 2001 From: normanre Date: Sun, 1 Feb 2026 04:38:18 +0100 Subject: [PATCH 2/3] Added OptionRootCA to set an custom root CA in config --- internal/state/config/options.go | 11 +++++++- internal/state/state.go | 43 ++++++++++++++++++++++++++------ 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/internal/state/config/options.go b/internal/state/config/options.go index 932038d03..cbf3e2983 100644 --- a/internal/state/config/options.go +++ b/internal/state/config/options.go @@ -117,7 +117,16 @@ var ( OptionStaticIp = newOpt( "static-ip", - "Use static ip for Hetzner API endpoint", + "Set a static ip for Hetzner API endpoint", + "", + DefaultPreferenceFlags, + nil, + nil, + ) + + OptionRootCA = newOpt( + "root-ca", + "Set a custom root CA for Hetzner API endpoint", "", DefaultPreferenceFlags, nil, diff --git a/internal/state/state.go b/internal/state/state.go index 1e5094208..bd631e6d4 100644 --- a/internal/state/state.go +++ b/internal/state/state.go @@ -2,6 +2,8 @@ package state import ( "context" + "crypto/tls" + "crypto/x509" "fmt" "io" "net" @@ -130,15 +132,42 @@ func (c *state) newClient() (hcapi2.Client, error) { })) } - if staticIp, err := config.OptionStaticIp.Get(c.config); err == nil && staticIp != "" { - tr := &http.Transport{DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { - split := strings.Split(addr, ":") - return net.Dial(network, fmt.Sprintf("%s:%s", staticIp, split[1])) - }} + staticIp, err := config.OptionStaticIp.Get(c.config) + if err != nil { + return nil, err + } + + rootCA, err := config.OptionRootCA.Get(c.config) + if err != nil { + return nil, err + } + + if staticIp != "" || rootCA != "" { + tr := &http.Transport{} + + if staticIp != "" { + tr.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { + split := strings.Split(addr, ":") + return net.Dial(network, fmt.Sprintf("%s:%s", staticIp, split[1])) + } + } + + if rootCA != "" { + certs := x509.NewCertPool() + pemData, err := os.ReadFile(rootCA) + if err != nil { + fmt.Printf("Error reading root CA certificate %s:\n%s\n", rootCA, err) + } + if err == nil { + certs.AppendCertsFromPEM(pemData) + tr.TLSClientConfig = &tls.Config{ + RootCAs: certs, + } + } + } + client := &http.Client{Transport: tr} opts = append(opts, hcloud.WithHTTPClient(client)) - } else if err != nil { - return nil, err } return hcapi2.NewClient(opts...), nil From 8bc91d98f58c9fc90fe496c15edab7a949a7d6c7 Mon Sep 17 00:00:00 2001 From: normanre Date: Sun, 1 Feb 2026 04:48:57 +0100 Subject: [PATCH 3/3] Show root CA error onyl with debug enabled --- internal/state/state.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/internal/state/state.go b/internal/state/state.go index bd631e6d4..6c64ab833 100644 --- a/internal/state/state.go +++ b/internal/state/state.go @@ -92,13 +92,14 @@ func (c *state) newClient() (hcapi2.Client, error) { return nil, err } + var debugWriter io.Writer + if debug { filePath, err := config.OptionDebugFile.Get(c.config) if err != nil { return nil, err } - var debugWriter io.Writer if filePath == "" { debugWriter = os.Stderr } else { @@ -155,8 +156,8 @@ func (c *state) newClient() (hcapi2.Client, error) { if rootCA != "" { certs := x509.NewCertPool() pemData, err := os.ReadFile(rootCA) - if err != nil { - fmt.Printf("Error reading root CA certificate %s:\n%s\n", rootCA, err) + if err != nil && debug { + fmt.Fprintf(debugWriter, "Error reading root CA certificate %s:\n%s\n", rootCA, err) } if err == nil { certs.AppendCertsFromPEM(pemData)