@@ -20,9 +20,12 @@ import (
2020 "k8s.io/autoscaler/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/internal/instrumentation"
2121)
2222
23- // Endpoint is the base URL of the API.
23+ // Endpoint is the base URL of the Cloud API.
2424const Endpoint = "https://api.hetzner.cloud/v1"
2525
26+ // Endpoint is the base URL of the Hetzner API.
27+ const HetznerEndpoint = "https://api.hetzner.com/v1"
28+
2629// UserAgent is the value for the library part of the User-Agent header
2730// that is sent with each request.
2831const UserAgent = "hcloud-go/" + Version
@@ -84,6 +87,7 @@ func ExponentialBackoffWithOpts(opts ExponentialBackoffOpts) BackoffFunc {
8487// Client is a client for the Hetzner Cloud API.
8588type Client struct {
8689 endpoint string
90+ hetznerEndpoint string
8791 token string
8892 tokenValid bool
8993 retryBackoffFunc BackoffFunc
@@ -111,11 +115,14 @@ type Client struct {
111115 Pricing PricingClient
112116 Server ServerClient
113117 ServerType ServerTypeClient
118+ StorageBox StorageBoxClient
114119 SSHKey SSHKeyClient
115120 Volume VolumeClient
116121 PlacementGroup PlacementGroupClient
117122 RDNS RDNSClient
118123 PrimaryIP PrimaryIPClient
124+ StorageBoxType StorageBoxTypeClient
125+ Zone ZoneClient
119126}
120127
121128// A ClientOption is used to configure a Client.
@@ -128,6 +135,16 @@ func WithEndpoint(endpoint string) ClientOption {
128135 }
129136}
130137
138+ // WithHetznerEndpoint configures a Client to use the specified Hetzner API endpoint.
139+ //
140+ // Experimental: This option is experimental, breaking changes may occur within minor releases.
141+ // See https://docs.hetzner.cloud/changelog#2025-06-25-new-api-for-storage-boxes for more details.
142+ func WithHetznerEndpoint (endpoint string ) ClientOption {
143+ return func (client * Client ) {
144+ client .hetznerEndpoint = strings .TrimRight (endpoint , "/" )
145+ }
146+ }
147+
131148// WithToken configures a Client to use the specified token for authentication.
132149func WithToken (token string ) ClientOption {
133150 return func (client * Client ) {
@@ -245,9 +262,10 @@ func WithInstrumentation(registry prometheus.Registerer) ClientOption {
245262// NewClient creates a new client.
246263func NewClient (options ... ClientOption ) * Client {
247264 client := & Client {
248- endpoint : Endpoint ,
249- tokenValid : true ,
250- httpClient : & http.Client {},
265+ endpoint : Endpoint ,
266+ hetznerEndpoint : HetznerEndpoint ,
267+ tokenValid : true ,
268+ httpClient : & http.Client {},
251269
252270 retryBackoffFunc : ExponentialBackoffWithOpts (ExponentialBackoffOpts {
253271 Base : time .Second ,
@@ -272,25 +290,39 @@ func NewClient(options ...ClientOption) *Client {
272290
273291 client .handler = assembleHandlerChain (client )
274292
275- client .Action = ActionClient {action : & ResourceActionClient {client : client }}
293+ // Cloud API
294+ client .Action = ActionClient {action : & ResourceActionClient [noopResource ]{client : client }}
276295 client .Datacenter = DatacenterClient {client : client }
277- client .FloatingIP = FloatingIPClient {client : client , Action : & ResourceActionClient {client : client , resource : "floating_ips" }}
278- client .Image = ImageClient {client : client , Action : & ResourceActionClient {client : client , resource : "images" }}
296+ client .FloatingIP = FloatingIPClient {client : client , Action : & ResourceActionClient [ * FloatingIP ] {client : client , resource : "floating_ips" }}
297+ client .Image = ImageClient {client : client , Action : & ResourceActionClient [ * Image ] {client : client , resource : "images" }}
279298 client .ISO = ISOClient {client : client }
280299 client .Location = LocationClient {client : client }
281- client .Network = NetworkClient {client : client , Action : & ResourceActionClient {client : client , resource : "networks" }}
300+ client .Network = NetworkClient {client : client , Action : & ResourceActionClient [ * Network ] {client : client , resource : "networks" }}
282301 client .Pricing = PricingClient {client : client }
283- client .Server = ServerClient {client : client , Action : & ResourceActionClient {client : client , resource : "servers" }}
302+ client .Server = ServerClient {client : client , Action : & ResourceActionClient [ * Server ] {client : client , resource : "servers" }}
284303 client .ServerType = ServerTypeClient {client : client }
285304 client .SSHKey = SSHKeyClient {client : client }
286- client .Volume = VolumeClient {client : client , Action : & ResourceActionClient {client : client , resource : "volumes" }}
287- client .LoadBalancer = LoadBalancerClient {client : client , Action : & ResourceActionClient {client : client , resource : "load_balancers" }}
305+ client .Volume = VolumeClient {client : client , Action : & ResourceActionClient [ * Volume ] {client : client , resource : "volumes" }}
306+ client .LoadBalancer = LoadBalancerClient {client : client , Action : & ResourceActionClient [ * LoadBalancer ] {client : client , resource : "load_balancers" }}
288307 client .LoadBalancerType = LoadBalancerTypeClient {client : client }
289- client .Certificate = CertificateClient {client : client , Action : & ResourceActionClient {client : client , resource : "certificates" }}
290- client .Firewall = FirewallClient {client : client , Action : & ResourceActionClient {client : client , resource : "firewalls" }}
308+ client .Certificate = CertificateClient {client : client , Action : & ResourceActionClient [ * Certificate ] {client : client , resource : "certificates" }}
309+ client .Firewall = FirewallClient {client : client , Action : & ResourceActionClient [ * Firewall ] {client : client , resource : "firewalls" }}
291310 client .PlacementGroup = PlacementGroupClient {client : client }
292311 client .RDNS = RDNSClient {client : client }
293- client .PrimaryIP = PrimaryIPClient {client : client , Action : & ResourceActionClient {client : client , resource : "primary_ips" }}
312+ client .PrimaryIP = PrimaryIPClient {client : client , Action : & ResourceActionClient [* PrimaryIP ]{client : client , resource : "primary_ips" }}
313+ client .Zone = ZoneClient {client : client , Action : & ResourceActionClient [* Zone ]{client : client , resource : "zones" }}
314+
315+ // Hetzner API
316+
317+ // Shallow copy of the client and overwrite of the API endpoint.
318+ // We have two "base clients" because the endpoint is only added to the requests URL 3 layers deep, and we want to avoid passing this info through all the layers. By embedding it in the client, we can easily select which "base client" is used for each "resource client".
319+ // We create a shallow copy so the handler chain and prometheus registry are the same values and it is transparent to the user.
320+ hetznerClient := new (Client )
321+ * hetznerClient = * client
322+ hetznerClient .endpoint = hetznerClient .hetznerEndpoint
323+
324+ client .StorageBox = StorageBoxClient {client : hetznerClient , Action : & ResourceActionClient [* StorageBox ]{client : hetznerClient , resource : "storage_boxes" }}
325+ client .StorageBoxType = StorageBoxTypeClient {client : hetznerClient }
294326
295327 return client
296328}
@@ -299,23 +331,22 @@ func NewClient(options ...ClientOption) *Client {
299331// is assigned with ctx and has all necessary headers set (auth, user agent, etc.).
300332func (c * Client ) NewRequest (ctx context.Context , method , path string , body io.Reader ) (* http.Request , error ) {
301333 url := c .endpoint + path
302- req , err := http .NewRequest ( method , url , body )
334+ req , err := http .NewRequestWithContext ( ctx , method , url , body )
303335 if err != nil {
304336 return nil , err
305337 }
306338 req .Header .Set ("User-Agent" , c .userAgent )
307339 req .Header .Set ("Accept" , "application/json" )
308340
309341 if ! c .tokenValid {
310- return nil , errors .New ("Authorization token contains invalid characters" )
342+ return nil , errors .New ("authorization token contains invalid characters" )
311343 } else if c .token != "" {
312344 req .Header .Set ("Authorization" , fmt .Sprintf ("Bearer %s" , c .token ))
313345 }
314346
315347 if body != nil {
316348 req .Header .Set ("Content-Type" , "application/json" )
317349 }
318- req = req .WithContext (ctx )
319350 return req , nil
320351}
321352
@@ -356,7 +387,7 @@ type Response struct {
356387func (r * Response ) populateBody () error {
357388 // Read full response body and save it for later use
358389 body , err := io .ReadAll (r .Body )
359- r .Body .Close ()
390+ _ = r .Body .Close ()
360391 if err != nil {
361392 return err
362393 }
0 commit comments