Skip to content

Commit f41032b

Browse files
committed
deps(hetzner): update hcloud-go to v2.37.0
1 parent d411518 commit f41032b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+5106
-146
lines changed

cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/action.go

Lines changed: 70 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -65,19 +65,26 @@ func (e ActionError) Action() *Action {
6565
}
6666

6767
func (e ActionError) Error() string {
68-
action := e.Action()
69-
if action != nil {
68+
if e.action != nil {
7069
// For easier debugging, the error string contains the Action ID.
71-
return fmt.Sprintf("%s (%s, %d)", e.Message, e.Code, action.ID)
70+
return fmt.Sprintf("%s (%s, %d)", e.Message, e.Code, e.action.ID)
7271
}
7372
return fmt.Sprintf("%s (%s)", e.Message, e.Code)
7473
}
7574

7675
func (a *Action) Error() error {
77-
if a.ErrorCode != "" && a.ErrorMessage != "" {
76+
if a.Status == ActionStatusError || a.ErrorCode != "" {
77+
code := a.ErrorCode
78+
if code == "" {
79+
code = "<unknown>"
80+
}
81+
message := a.ErrorMessage
82+
if message == "" {
83+
message = "Unknown error"
84+
}
7885
return ActionError{
79-
Code: a.ErrorCode,
80-
Message: a.ErrorMessage,
86+
Code: code,
87+
Message: message,
8188
action: a,
8289
}
8390
}
@@ -86,7 +93,7 @@ func (a *Action) Error() error {
8693

8794
// ActionClient is a client for the actions API.
8895
type ActionClient struct {
89-
action *ResourceActionClient
96+
action *ResourceActionClient[noopResource]
9097
}
9198

9299
// GetByID retrieves an action by its ID. If the action does not exist, nil is returned.
@@ -116,7 +123,7 @@ func (l ActionListOpts) values() url.Values {
116123
return vals
117124
}
118125

119-
// List returns a list of actions for a specific page.
126+
// List returns a paginated list of actions.
120127
//
121128
// Please note that filters specified in opts are not taken into account
122129
// when their value corresponds to their zero value or when they are empty.
@@ -128,23 +135,27 @@ func (c *ActionClient) List(ctx context.Context, opts ActionListOpts) ([]*Action
128135
//
129136
// Deprecated: It is required to pass in a list of IDs since 30 January 2025. Please use [ActionClient.AllWithOpts] instead.
130137
func (c *ActionClient) All(ctx context.Context) ([]*Action, error) {
131-
return c.action.All(ctx, ActionListOpts{ListOpts: ListOpts{PerPage: 50}})
138+
return c.action.All(ctx, ActionListOpts{})
132139
}
133140

134141
// AllWithOpts returns all actions for the given options.
135142
//
136143
// It is required to set [ActionListOpts.ID]. Any other fields set in the opts are ignored.
137144
func (c *ActionClient) AllWithOpts(ctx context.Context, opts ActionListOpts) ([]*Action, error) {
145+
if opts.ListOpts.PerPage == 0 {
146+
// Do not send unused per page param
147+
opts.ListOpts.PerPage = -1
148+
}
138149
return c.action.All(ctx, opts)
139150
}
140151

141152
// ResourceActionClient is a client for the actions API exposed by the resource.
142-
type ResourceActionClient struct {
153+
type ResourceActionClient[R actionSupporter] struct {
143154
resource string
144155
client *Client
145156
}
146157

147-
func (c *ResourceActionClient) getBaseURL() string {
158+
func (c *ResourceActionClient[R]) getBaseURL() string {
148159
if c.resource == "" {
149160
return ""
150161
}
@@ -153,7 +164,7 @@ func (c *ResourceActionClient) getBaseURL() string {
153164
}
154165

155166
// GetByID retrieves an action by its ID. If the action does not exist, nil is returned.
156-
func (c *ResourceActionClient) GetByID(ctx context.Context, id int64) (*Action, *Response, error) {
167+
func (c *ResourceActionClient[R]) GetByID(ctx context.Context, id int64) (*Action, *Response, error) {
157168
opPath := c.getBaseURL() + "/actions/%d"
158169
ctx = ctxutil.SetOpPath(ctx, opPath)
159170

@@ -169,11 +180,11 @@ func (c *ResourceActionClient) GetByID(ctx context.Context, id int64) (*Action,
169180
return ActionFromSchema(respBody.Action), resp, nil
170181
}
171182

172-
// List returns a list of actions for a specific page.
183+
// List returns a paginated list of actions.
173184
//
174185
// Please note that filters specified in opts are not taken into account
175186
// when their value corresponds to their zero value or when they are empty.
176-
func (c *ResourceActionClient) List(ctx context.Context, opts ActionListOpts) ([]*Action, *Response, error) {
187+
func (c *ResourceActionClient[R]) List(ctx context.Context, opts ActionListOpts) ([]*Action, *Response, error) {
177188
opPath := c.getBaseURL() + "/actions?%s"
178189
ctx = ctxutil.SetOpPath(ctx, opPath)
179190

@@ -188,9 +199,53 @@ func (c *ResourceActionClient) List(ctx context.Context, opts ActionListOpts) ([
188199
}
189200

190201
// All returns all actions for the given options.
191-
func (c *ResourceActionClient) All(ctx context.Context, opts ActionListOpts) ([]*Action, error) {
202+
func (c *ResourceActionClient[R]) All(ctx context.Context, opts ActionListOpts) ([]*Action, error) {
203+
if opts.ListOpts.PerPage == 0 {
204+
opts.ListOpts.PerPage = 50
205+
}
192206
return iterPages(func(page int) ([]*Action, *Response, error) {
193207
opts.Page = page
194208
return c.List(ctx, opts)
195209
})
196210
}
211+
212+
type actionSupporter interface {
213+
pathID() (string, error)
214+
}
215+
216+
// noopResource is used by the [ActionClient] to satisfy its underlying
217+
// [ResourceActionClient] generic type.
218+
type noopResource struct{}
219+
220+
func (noopResource) pathID() (string, error) { return "", nil }
221+
222+
// ListFor returns a paginated list of actions for the given Resource.
223+
//
224+
// Please note that filters specified in opts are not taken into account
225+
// when their value corresponds to their zero value or when they are empty.
226+
func (c *ResourceActionClient[R]) ListFor(ctx context.Context, resource R, opts ActionListOpts) ([]*Action, *Response, error) {
227+
opPath := c.getBaseURL() + "/%s/actions?%s"
228+
ctx = ctxutil.SetOpPath(ctx, opPath)
229+
230+
id, err := resource.pathID()
231+
if err != nil {
232+
return nil, nil, invalidArgument("resource", resource, err)
233+
}
234+
235+
reqPath := fmt.Sprintf(opPath, id, opts.values().Encode())
236+
237+
respBody, resp, err := getRequest[schema.ActionListResponse](ctx, c.client, reqPath)
238+
if err != nil {
239+
return nil, resp, err
240+
}
241+
242+
return allFromSchemaFunc(respBody.Actions, ActionFromSchema), resp, nil
243+
}
244+
245+
// AllFor returns all actions for the given Resource.
246+
func (c *ResourceActionClient[R]) AllFor(ctx context.Context, resource R, opts ActionListOpts) ([]*Action, error) {
247+
return iterPages(func(page int) ([]*Action, *Response, error) {
248+
opts.Page = page
249+
return c.ListFor(ctx, resource, opts)
250+
})
251+
}

cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/action_waiter.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,11 @@ func (c *ActionClient) WaitForFunc(ctx context.Context, handleUpdate func(update
3939
}
4040

4141
retries := 0
42-
for {
43-
if len(running) == 0 {
44-
break
45-
}
42+
for len(running) != 0 {
4643

4744
select {
4845
case <-ctx.Done():
49-
return ctx.Err()
46+
return fmt.Errorf("%w: remaining running actions: %v", ctx.Err(), slices.Collect(maps.Keys(running)))
5047
case <-time.After(c.action.client.pollBackoffFunc(retries)):
5148
retries++
5249
}

cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/certificate.go

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66
"net/url"
7+
"strconv"
78
"time"
89

910
"k8s.io/autoscaler/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/exp/ctxutil"
@@ -61,8 +62,8 @@ type CertificateStatus struct {
6162
// IsFailed returns true if either the Issuance or the Renewal of a certificate
6263
// failed. In this case the FailureReason field details the nature of the
6364
// failure.
64-
func (st *CertificateStatus) IsFailed() bool {
65-
return st.Issuance == CertificateStatusTypeFailed || st.Renewal == CertificateStatusTypeFailed
65+
func (o *CertificateStatus) IsFailed() bool {
66+
return o.Issuance == CertificateStatusTypeFailed || o.Renewal == CertificateStatusTypeFailed
6667
}
6768

6869
// Certificate represents a certificate in the Hetzner Cloud.
@@ -81,6 +82,13 @@ type Certificate struct {
8182
UsedBy []CertificateUsedByRef
8283
}
8384

85+
func (o *Certificate) pathID() (string, error) {
86+
if o.ID == 0 {
87+
return "", missingField(o, "ID")
88+
}
89+
return strconv.FormatInt(o.ID, 10), nil
90+
}
91+
8492
// CertificateCreateResult is the result of creating a certificate.
8593
type CertificateCreateResult struct {
8694
Certificate *Certificate
@@ -90,7 +98,7 @@ type CertificateCreateResult struct {
9098
// CertificateClient is a client for the Certificates API.
9199
type CertificateClient struct {
92100
client *Client
93-
Action *ResourceActionClient
101+
Action *ResourceActionClient[*Certificate]
94102
}
95103

96104
// GetByID retrieves a Certificate by its ID. If the Certificate does not exist, nil is returned.
@@ -161,11 +169,14 @@ func (c *CertificateClient) List(ctx context.Context, opts CertificateListOpts)
161169

162170
// All returns all Certificates.
163171
func (c *CertificateClient) All(ctx context.Context) ([]*Certificate, error) {
164-
return c.AllWithOpts(ctx, CertificateListOpts{ListOpts: ListOpts{PerPage: 50}})
172+
return c.AllWithOpts(ctx, CertificateListOpts{})
165173
}
166174

167175
// AllWithOpts returns all Certificates for the given options.
168176
func (c *CertificateClient) AllWithOpts(ctx context.Context, opts CertificateListOpts) ([]*Certificate, error) {
177+
if opts.ListOpts.PerPage == 0 {
178+
opts.ListOpts.PerPage = 50
179+
}
169180
return iterPages(func(page int) ([]*Certificate, *Response, error) {
170181
opts.Page = page
171182
return c.List(ctx, opts)
@@ -219,7 +230,7 @@ func (o CertificateCreateOpts) validateUploaded() error {
219230
// Create returns an error for certificates of any other type. Use
220231
// CreateCertificate to create such certificates.
221232
func (c *CertificateClient) Create(ctx context.Context, opts CertificateCreateOpts) (*Certificate, *Response, error) {
222-
if !(opts.Type == "" || opts.Type == CertificateTypeUploaded) {
233+
if opts.Type != "" && opts.Type != CertificateTypeUploaded {
223234
return nil, nil, invalidFieldValue(opts, "Type", opts.Type)
224235
}
225236
result, resp, err := c.CreateCertificate(ctx, opts)

cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/client.go

Lines changed: 49 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
2424
const 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.
2831
const UserAgent = "hcloud-go/" + Version
@@ -84,6 +87,7 @@ func ExponentialBackoffWithOpts(opts ExponentialBackoffOpts) BackoffFunc {
8487
// Client is a client for the Hetzner Cloud API.
8588
type 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.
132149
func 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.
246263
func 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.).
300332
func (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 {
356387
func (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

Comments
 (0)