diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/action.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/action.go index 12b4721e3c7a..ecf9c5c7b46e 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/action.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/action.go @@ -72,25 +72,12 @@ func (a *Action) Error() error { // ActionClient is a client for the actions API. type ActionClient struct { - client *Client + action *ResourceActionClient } // GetByID retrieves an action by its ID. If the action does not exist, nil is returned. func (c *ActionClient) GetByID(ctx context.Context, id int64) (*Action, *Response, error) { - req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/actions/%d", id), nil) - if err != nil { - return nil, nil, err - } - - var body schema.ActionGetResponse - resp, err := c.client.Do(req, &body) - if err != nil { - if IsError(err, ErrorCodeNotFound) { - return nil, resp, nil - } - return nil, nil, err - } - return ActionFromSchema(body.Action), resp, nil + return c.action.GetByID(ctx, id) } // ActionListOpts specifies options for listing actions. @@ -120,47 +107,17 @@ func (l ActionListOpts) values() url.Values { // Please note that filters specified in opts are not taken into account // when their value corresponds to their zero value or when they are empty. func (c *ActionClient) List(ctx context.Context, opts ActionListOpts) ([]*Action, *Response, error) { - path := "/actions?" + opts.values().Encode() - req, err := c.client.NewRequest(ctx, "GET", path, nil) - if err != nil { - return nil, nil, err - } - - var body schema.ActionListResponse - resp, err := c.client.Do(req, &body) - if err != nil { - return nil, nil, err - } - actions := make([]*Action, 0, len(body.Actions)) - for _, i := range body.Actions { - actions = append(actions, ActionFromSchema(i)) - } - return actions, resp, nil + return c.action.List(ctx, opts) } // All returns all actions. func (c *ActionClient) All(ctx context.Context) ([]*Action, error) { - return c.AllWithOpts(ctx, ActionListOpts{ListOpts: ListOpts{PerPage: 50}}) + return c.action.All(ctx, ActionListOpts{ListOpts: ListOpts{PerPage: 50}}) } // AllWithOpts returns all actions for the given options. func (c *ActionClient) AllWithOpts(ctx context.Context, opts ActionListOpts) ([]*Action, error) { - var allActions []*Action - - err := c.client.all(func(page int) (*Response, error) { - opts.Page = page - actions, resp, err := c.List(ctx, opts) - if err != nil { - return resp, err - } - allActions = append(allActions, actions...) - return resp, nil - }) - if err != nil { - return nil, err - } - - return allActions, nil + return c.action.All(ctx, opts) } // WatchOverallProgress watches several actions' progress until they complete @@ -189,20 +146,21 @@ func (c *ActionClient) WatchOverallProgress(ctx context.Context, actions []*Acti defer close(errCh) defer close(progressCh) - successIDs := make([]int64, 0, len(actions)) + completedIDs := make([]int64, 0, len(actions)) watchIDs := make(map[int64]struct{}, len(actions)) for _, action := range actions { watchIDs[action.ID] = struct{}{} } retries := 0 + previousProgress := 0 for { select { case <-ctx.Done(): errCh <- ctx.Err() return - case <-time.After(c.client.pollBackoffFunc(retries)): + case <-time.After(c.action.client.pollBackoffFunc(retries)): retries++ } @@ -216,21 +174,35 @@ func (c *ActionClient) WatchOverallProgress(ctx context.Context, actions []*Acti errCh <- err return } + if len(as) == 0 { + // No actions returned for the provided IDs, they do not exist in the API. + // We need to catch and fail early for this, otherwise the loop will continue + // indefinitely. + errCh <- fmt.Errorf("failed to wait for actions: remaining actions (%v) are not returned from API", opts.ID) + return + } + progress := 0 for _, a := range as { switch a.Status { case ActionStatusRunning: - continue + progress += a.Progress case ActionStatusSuccess: delete(watchIDs, a.ID) - successIDs = append(successIDs, a.ID) - sendProgress(progressCh, int(float64(len(actions)-len(successIDs))/float64(len(actions))*100)) + completedIDs = append(completedIDs, a.ID) case ActionStatusError: delete(watchIDs, a.ID) + completedIDs = append(completedIDs, a.ID) errCh <- fmt.Errorf("action %d failed: %w", a.ID, a.Error()) } } + progress += len(completedIDs) * 100 + if progress != 0 && progress != previousProgress { + sendProgress(progressCh, progress/len(actions)) + previousProgress = progress + } + if len(watchIDs) == 0 { return } @@ -273,7 +245,7 @@ func (c *ActionClient) WatchProgress(ctx context.Context, action *Action) (<-cha case <-ctx.Done(): errCh <- ctx.Err() return - case <-time.After(c.client.pollBackoffFunc(retries)): + case <-time.After(c.action.client.pollBackoffFunc(retries)): retries++ } @@ -282,6 +254,10 @@ func (c *ActionClient) WatchProgress(ctx context.Context, action *Action) (<-cha errCh <- err return } + if a == nil { + errCh <- fmt.Errorf("failed to wait for action %d: action not returned from API", action.ID) + return + } switch a.Status { case ActionStatusRunning: @@ -300,6 +276,7 @@ func (c *ActionClient) WatchProgress(ctx context.Context, action *Action) (<-cha return progressCh, errCh } +// sendProgress allows the user to only read from the error channel and ignore any progress updates. func sendProgress(progressCh chan int, p int) { select { case progressCh <- p: @@ -308,3 +285,82 @@ func sendProgress(progressCh chan int, p int) { break } } + +// ResourceActionClient is a client for the actions API exposed by the resource. +type ResourceActionClient struct { + resource string + client *Client +} + +func (c *ResourceActionClient) getBaseURL() string { + if c.resource == "" { + return "" + } + + return "/" + c.resource +} + +// GetByID retrieves an action by its ID. If the action does not exist, nil is returned. +func (c *ResourceActionClient) GetByID(ctx context.Context, id int64) (*Action, *Response, error) { + req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("%s/actions/%d", c.getBaseURL(), id), nil) + if err != nil { + return nil, nil, err + } + + var body schema.ActionGetResponse + resp, err := c.client.Do(req, &body) + if err != nil { + if IsError(err, ErrorCodeNotFound) { + return nil, resp, nil + } + return nil, nil, err + } + return ActionFromSchema(body.Action), resp, nil +} + +// List returns a list of actions for a specific page. +// +// Please note that filters specified in opts are not taken into account +// when their value corresponds to their zero value or when they are empty. +func (c *ResourceActionClient) List(ctx context.Context, opts ActionListOpts) ([]*Action, *Response, error) { + req, err := c.client.NewRequest( + ctx, + "GET", + fmt.Sprintf("%s/actions?%s", c.getBaseURL(), opts.values().Encode()), + nil, + ) + if err != nil { + return nil, nil, err + } + + var body schema.ActionListResponse + resp, err := c.client.Do(req, &body) + if err != nil { + return nil, nil, err + } + actions := make([]*Action, 0, len(body.Actions)) + for _, i := range body.Actions { + actions = append(actions, ActionFromSchema(i)) + } + return actions, resp, nil +} + +// All returns all actions for the given options. +func (c *ResourceActionClient) All(ctx context.Context, opts ActionListOpts) ([]*Action, error) { + allActions := []*Action{} + + err := c.client.all(func(page int) (*Response, error) { + opts.Page = page + actions, resp, err := c.List(ctx, opts) + if err != nil { + return resp, err + } + allActions = append(allActions, actions...) + return resp, nil + }) + if err != nil { + return nil, err + } + + return allActions, nil +} diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/certificate.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/certificate.go index 9a5b26f869f2..e6ffa50cb62a 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/certificate.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/certificate.go @@ -93,6 +93,7 @@ type CertificateCreateResult struct { // CertificateClient is a client for the Certificates API. type CertificateClient struct { client *Client + Action *ResourceActionClient } // GetByID retrieves a Certificate by its ID. If the Certificate does not exist, nil is returned. @@ -182,7 +183,7 @@ func (c *CertificateClient) All(ctx context.Context) ([]*Certificate, error) { // AllWithOpts returns all Certificates for the given options. func (c *CertificateClient) AllWithOpts(ctx context.Context, opts CertificateListOpts) ([]*Certificate, error) { - var allCertificates []*Certificate + allCertificates := []*Certificate{} err := c.client.all(func(page int) (*Response, error) { opts.Page = page diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/client.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/client.go index 56983ea84d76..6fd9f29914b3 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/client.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/client.go @@ -125,7 +125,7 @@ func WithPollInterval(pollInterval time.Duration) ClientOption { // function when polling from the API. func WithPollBackoffFunc(f BackoffFunc) ClientOption { return func(client *Client) { - client.backoffFunc = f + client.pollBackoffFunc = f } } @@ -189,25 +189,25 @@ func NewClient(options ...ClientOption) *Client { client.httpClient.Transport = i.InstrumentedRoundTripper() } - client.Action = ActionClient{client: client} + client.Action = ActionClient{action: &ResourceActionClient{client: client}} client.Datacenter = DatacenterClient{client: client} - client.FloatingIP = FloatingIPClient{client: client} - client.Image = ImageClient{client: client} + client.FloatingIP = FloatingIPClient{client: client, Action: &ResourceActionClient{client: client, resource: "floating_ips"}} + client.Image = ImageClient{client: client, Action: &ResourceActionClient{client: client, resource: "images"}} client.ISO = ISOClient{client: client} client.Location = LocationClient{client: client} - client.Network = NetworkClient{client: client} + client.Network = NetworkClient{client: client, Action: &ResourceActionClient{client: client, resource: "networks"}} client.Pricing = PricingClient{client: client} - client.Server = ServerClient{client: client} + client.Server = ServerClient{client: client, Action: &ResourceActionClient{client: client, resource: "servers"}} client.ServerType = ServerTypeClient{client: client} client.SSHKey = SSHKeyClient{client: client} - client.Volume = VolumeClient{client: client} - client.LoadBalancer = LoadBalancerClient{client: client} + client.Volume = VolumeClient{client: client, Action: &ResourceActionClient{client: client, resource: "volumes"}} + client.LoadBalancer = LoadBalancerClient{client: client, Action: &ResourceActionClient{client: client, resource: "load_balancers"}} client.LoadBalancerType = LoadBalancerTypeClient{client: client} - client.Certificate = CertificateClient{client: client} - client.Firewall = FirewallClient{client: client} + client.Certificate = CertificateClient{client: client, Action: &ResourceActionClient{client: client, resource: "certificates"}} + client.Firewall = FirewallClient{client: client, Action: &ResourceActionClient{client: client, resource: "firewalls"}} client.PlacementGroup = PlacementGroupClient{client: client} client.RDNS = RDNSClient{client: client} - client.PrimaryIP = PrimaryIPClient{client: client} + client.PrimaryIP = PrimaryIPClient{client: client, Action: &ResourceActionClient{client: client, resource: "primary_ips"}} return client } @@ -290,7 +290,7 @@ func (c *Client) Do(r *http.Request, v interface{}) (*Response, error) { err = errorFromResponse(resp, body) if err == nil { err = fmt.Errorf("hcloud: server responded with status code %d", resp.StatusCode) - } else if isConflict(err) { + } else if IsError(err, ErrorCodeConflict) { c.backoff(retries) retries++ continue @@ -309,14 +309,6 @@ func (c *Client) Do(r *http.Request, v interface{}) (*Response, error) { } } -func isConflict(error error) bool { - err, ok := error.(Error) - if !ok { - return false - } - return err.Code == ErrorCodeConflict -} - func (c *Client) backoff(retries int) { time.Sleep(c.backoffFunc(retries)) } diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/datacenter.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/datacenter.go index cc473845e632..6a1debd4d94c 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/datacenter.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/datacenter.go @@ -116,7 +116,7 @@ func (c *DatacenterClient) All(ctx context.Context) ([]*Datacenter, error) { // AllWithOpts returns all datacenters for the given options. func (c *DatacenterClient) AllWithOpts(ctx context.Context, opts DatacenterListOpts) ([]*Datacenter, error) { - var allDatacenters []*Datacenter + allDatacenters := []*Datacenter{} err := c.client.all(func(page int) (*Response, error) { opts.Page = page diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/error.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/error.go index ff04d07b229a..ac689d110486 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/error.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/error.go @@ -16,6 +16,7 @@ const ( ErrorCodeNotFound ErrorCode = "not_found" // Resource not found ErrorCodeInvalidInput ErrorCode = "invalid_input" // Validation error ErrorCodeForbidden ErrorCode = "forbidden" // Insufficient permissions + ErrorCodeUnauthorized ErrorCode = "unauthorized" // Request was made with an invalid or unknown token ErrorCodeJSONError ErrorCode = "json_error" // Invalid JSON in request ErrorCodeLocked ErrorCode = "locked" // Item is locked (Another action is running) ErrorCodeResourceLimitExceeded ErrorCode = "resource_limit_exceeded" // Resource limit exceeded @@ -29,6 +30,7 @@ const ( ErrorUnsupportedError ErrorCode = "unsupported_error" // The given resource does not support this // Server related error codes. + ErrorCodeInvalidServerType ErrorCode = "invalid_server_type" // The server type does not fit for the given server or is deprecated ErrorCodeServerNotStopped ErrorCode = "server_not_stopped" // The action requires a stopped server ErrorCodeNetworksOverlap ErrorCode = "networks_overlap" // The network IP range overlaps with one of the server networks @@ -36,6 +38,7 @@ const ( ErrorCodeServerAlreadyAttached ErrorCode = "server_already_attached" // The server is already attached to the resource // Load Balancer related error codes. + ErrorCodeIPNotOwned ErrorCode = "ip_not_owned" // The IP you are trying to add as a target is not owned by the Project owner ErrorCodeSourcePortAlreadyUsed ErrorCode = "source_port_already_used" // The source port you are trying to add is already in use ErrorCodeCloudResourceIPNotAllowed ErrorCode = "cloud_resource_ip_not_allowed" // The IP you are trying to add as a target belongs to a Hetzner Cloud resource @@ -47,15 +50,18 @@ const ( ErrorCodeLoadBalancerNotAttachedToNetwork ErrorCode = "load_balancer_not_attached_to_network" // The Load Balancer is not attached to a network // Network related error codes. + ErrorCodeIPNotAvailable ErrorCode = "ip_not_available" // The provided Network IP is not available ErrorCodeNoSubnetAvailable ErrorCode = "no_subnet_available" // No Subnet or IP is available for the Load Balancer/Server within the network ErrorCodeVSwitchAlreadyUsed ErrorCode = "vswitch_id_already_used" // The given Robot vSwitch ID is already registered in another network // Volume related error codes. + ErrorCodeNoSpaceLeftInLocation ErrorCode = "no_space_left_in_location" // There is no volume space left in the given location ErrorCodeVolumeAlreadyAttached ErrorCode = "volume_already_attached" // Volume is already attached to a server, detach first // Firewall related error codes. + ErrorCodeFirewallAlreadyApplied ErrorCode = "firewall_already_applied" // Firewall was already applied on resource ErrorCodeFirewallAlreadyRemoved ErrorCode = "firewall_already_removed" // Firewall was already removed from the resource ErrorCodeIncompatibleNetworkType ErrorCode = "incompatible_network_type" // The Network type is incompatible for the given resource @@ -64,6 +70,7 @@ const ( ErrorCodeFirewallResourceNotFound ErrorCode = "firewall_resource_not_found" // Resource a firewall should be attached to / detached from not found // Certificate related error codes. + ErrorCodeCAARecordDoesNotAllowCA ErrorCode = "caa_record_does_not_allow_ca" // CAA record does not allow certificate authority ErrorCodeCADNSValidationFailed ErrorCode = "ca_dns_validation_failed" // Certificate Authority: DNS validation failed ErrorCodeCATooManyAuthorizationsFailedRecently ErrorCode = "ca_too_many_authorizations_failed_recently" // Certificate Authority: Too many authorizations failed recently @@ -72,12 +79,13 @@ const ( ErrorCodeCloudNotVerifyDomainDelegatedToZone ErrorCode = "could_not_verify_domain_delegated_to_zone" // Could not verify domain delegated to zone ErrorCodeDNSZoneNotFound ErrorCode = "dns_zone_not_found" // DNS zone not found - // Deprecated error codes - // The actual value of this error code is limit_reached. The new error code - // rate_limit_exceeded for ratelimiting was introduced before Hetzner Cloud - // launched into the public. To make clients using the old error code still - // work as expected, we set the value of the old error code to that of the - // new error code. + // Deprecated error codes. + + // Deprecated: The actual value of this error code is limit_reached. The + // new error code rate_limit_exceeded for rate limiting was introduced + // before Hetzner Cloud launched into the public. To make clients using the + // old error code still work as expected, we set the value of the old error + // code to that of the new error code. ErrorCodeLimitReached = ErrorCodeRateLimitExceeded ) diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/firewall.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/firewall.go index 968d2045909a..138fdebfdea3 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/firewall.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/firewall.go @@ -91,6 +91,7 @@ type FirewallResourceLabelSelector struct { // FirewallClient is a client for the Firewalls API. type FirewallClient struct { client *Client + Action *ResourceActionClient } // GetByID retrieves a Firewall by its ID. If the Firewall does not exist, nil is returned. @@ -180,7 +181,7 @@ func (c *FirewallClient) All(ctx context.Context) ([]*Firewall, error) { // AllWithOpts returns all Firewalls for the given options. func (c *FirewallClient) AllWithOpts(ctx context.Context, opts FirewallListOpts) ([]*Firewall, error) { - var allFirewalls []*Firewall + allFirewalls := []*Firewall{} err := c.client.all(func(page int) (*Response, error) { opts.Page = page diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/floating_ip.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/floating_ip.go index 7e2dc5cad952..569576c0209f 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/floating_ip.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/floating_ip.go @@ -91,6 +91,7 @@ func (f *FloatingIP) GetDNSPtrForIP(ip net.IP) (string, error) { // FloatingIPClient is a client for the Floating IP API. type FloatingIPClient struct { client *Client + Action *ResourceActionClient } // GetByID retrieves a Floating IP by its ID. If the Floating IP does not exist, @@ -181,7 +182,7 @@ func (c *FloatingIPClient) All(ctx context.Context) ([]*FloatingIP, error) { // AllWithOpts returns all Floating IPs for the given options. func (c *FloatingIPClient) AllWithOpts(ctx context.Context, opts FloatingIPListOpts) ([]*FloatingIP, error) { - var allFloatingIPs []*FloatingIP + allFloatingIPs := []*FloatingIP{} err := c.client.all(func(page int) (*Response, error) { opts.Page = page diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/hcloud.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/hcloud.go index 0131c0d9230e..3d31d3251dae 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/hcloud.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/hcloud.go @@ -2,4 +2,4 @@ package hcloud // Version is the library's version following Semantic Versioning. -const Version = "2.0.0" // x-release-please-version +const Version = "2.4.0" // x-release-please-version diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/image.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/image.go index f79489728769..1da240170ec4 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/image.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/image.go @@ -78,6 +78,7 @@ const ( // ImageClient is a client for the image API. type ImageClient struct { client *Client + Action *ResourceActionClient } // GetByID retrieves an image by its ID. If the image does not exist, nil is returned. diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/internal/instrumentation/metrics.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/internal/instrumentation/metrics.go index 69a7165ba868..aa57c7107c0a 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/internal/instrumentation/metrics.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/internal/instrumentation/metrics.go @@ -23,29 +23,36 @@ func New(subsystemIdentifier string, instrumentationRegistry *prometheus.Registr // InstrumentedRoundTripper returns an instrumented round tripper. func (i *Instrumenter) InstrumentedRoundTripper() http.RoundTripper { - inFlightRequestsGauge := prometheus.NewGauge(prometheus.GaugeOpts{ - Name: fmt.Sprintf("hcloud_%s_in_flight_requests", i.subsystemIdentifier), - Help: fmt.Sprintf("A gauge of in-flight requests to the hcloud %s.", i.subsystemIdentifier), - }) - - requestsPerEndpointCounter := prometheus.NewCounterVec( - prometheus.CounterOpts{ - Name: fmt.Sprintf("hcloud_%s_requests_total", i.subsystemIdentifier), - Help: fmt.Sprintf("A counter for requests to the hcloud %s per endpoint.", i.subsystemIdentifier), - }, - []string{"code", "method", "api_endpoint"}, + inFlightRequestsGauge := registerOrReuse( + i.instrumentationRegistry, + prometheus.NewGauge(prometheus.GaugeOpts{ + Name: fmt.Sprintf("hcloud_%s_in_flight_requests", i.subsystemIdentifier), + Help: fmt.Sprintf("A gauge of in-flight requests to the hcloud %s.", i.subsystemIdentifier), + }), ) - requestLatencyHistogram := prometheus.NewHistogramVec( - prometheus.HistogramOpts{ - Name: fmt.Sprintf("hcloud_%s_request_duration_seconds", i.subsystemIdentifier), - Help: fmt.Sprintf("A histogram of request latencies to the hcloud %s .", i.subsystemIdentifier), - Buckets: prometheus.DefBuckets, - }, - []string{"method"}, + requestsPerEndpointCounter := registerOrReuse( + i.instrumentationRegistry, + prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: fmt.Sprintf("hcloud_%s_requests_total", i.subsystemIdentifier), + Help: fmt.Sprintf("A counter for requests to the hcloud %s per endpoint.", i.subsystemIdentifier), + }, + []string{"code", "method", "api_endpoint"}, + ), ) - i.instrumentationRegistry.MustRegister(requestsPerEndpointCounter, requestLatencyHistogram, inFlightRequestsGauge) + requestLatencyHistogram := registerOrReuse( + i.instrumentationRegistry, + prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: fmt.Sprintf("hcloud_%s_request_duration_seconds", i.subsystemIdentifier), + Help: fmt.Sprintf("A histogram of request latencies to the hcloud %s .", i.subsystemIdentifier), + Buckets: prometheus.DefBuckets, + }, + []string{"method"}, + ), + ) return promhttp.InstrumentRoundTripperInFlight(inFlightRequestsGauge, promhttp.InstrumentRoundTripperDuration(requestLatencyHistogram, @@ -74,6 +81,27 @@ func (i *Instrumenter) instrumentRoundTripperEndpoint(counter *prometheus.Counte } } +// registerOrReuse will try to register the passed Collector, but in case a conflicting collector was already registered, +// it will instead return that collector. Make sure to always use the collector return by this method. +// Similar to [Registry.MustRegister] it will panic if any other error occurs. +func registerOrReuse[C prometheus.Collector](registry *prometheus.Registry, collector C) C { + err := registry.Register(collector) + if err != nil { + // If we get a AlreadyRegisteredError we can return the existing collector + if are, ok := err.(prometheus.AlreadyRegisteredError); ok { + if existingCollector, ok := are.ExistingCollector.(C); ok { + collector = existingCollector + } else { + panic("received incompatible existing collector") + } + } else { + panic(err) + } + } + + return collector +} + func preparePathForLabel(path string) string { path = strings.ToLower(path) diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/iso.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/iso.go index 70807e38fc18..896a3ecdbb91 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/iso.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/iso.go @@ -17,12 +17,9 @@ type ISO struct { Description string Type ISOType Architecture *Architecture - Deprecated time.Time -} - -// IsDeprecated returns true if the ISO is deprecated. -func (iso *ISO) IsDeprecated() bool { - return !iso.Deprecated.IsZero() + // Deprecated: Use [ISO.Deprecation] instead. + Deprecated time.Time + DeprecatableResource } // ISOType specifies the type of an ISO image. @@ -143,7 +140,7 @@ func (c *ISOClient) All(ctx context.Context) ([]*ISO, error) { // AllWithOpts returns all ISOs for the given options. func (c *ISOClient) AllWithOpts(ctx context.Context, opts ISOListOpts) ([]*ISO, error) { - allISOs := make([]*ISO, 0) + allISOs := []*ISO{} err := c.client.all(func(page int) (*Response, error) { opts.Page = page diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/load_balancer.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/load_balancer.go index 020312f19592..471eb947ad9c 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/load_balancer.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/load_balancer.go @@ -238,6 +238,7 @@ func (lb *LoadBalancer) GetDNSPtrForIP(ip net.IP) (string, error) { // LoadBalancerClient is a client for the Load Balancers API. type LoadBalancerClient struct { client *Client + Action *ResourceActionClient } // GetByID retrieves a Load Balancer by its ID. If the Load Balancer does not exist, nil is returned. @@ -327,7 +328,7 @@ func (c *LoadBalancerClient) All(ctx context.Context) ([]*LoadBalancer, error) { // AllWithOpts returns all Load Balancers for the given options. func (c *LoadBalancerClient) AllWithOpts(ctx context.Context, opts LoadBalancerListOpts) ([]*LoadBalancer, error) { - var allLoadBalancers []*LoadBalancer + allLoadBalancers := []*LoadBalancer{} err := c.client.all(func(page int) (*Response, error) { opts.Page = page diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/load_balancer_type.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/load_balancer_type.go index 2aa7289ac4af..2281c1afee4f 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/load_balancer_type.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/load_balancer_type.go @@ -113,7 +113,7 @@ func (c *LoadBalancerTypeClient) All(ctx context.Context) ([]*LoadBalancerType, // AllWithOpts returns all Load Balancer types for the given options. func (c *LoadBalancerTypeClient) AllWithOpts(ctx context.Context, opts LoadBalancerTypeListOpts) ([]*LoadBalancerType, error) { - var allLoadBalancerTypes []*LoadBalancerType + allLoadBalancerTypes := []*LoadBalancerType{} err := c.client.all(func(page int) (*Response, error) { opts.Page = page diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/location.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/location.go index 58105e820bf4..c4d751775034 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/location.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/location.go @@ -113,7 +113,7 @@ func (c *LocationClient) All(ctx context.Context) ([]*Location, error) { // AllWithOpts returns all locations for the given options. func (c *LocationClient) AllWithOpts(ctx context.Context, opts LocationListOpts) ([]*Location, error) { - var allLocations []*Location + allLocations := []*Location{} err := c.client.all(func(page int) (*Response, error) { opts.Page = page diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/metadata/client.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/metadata/client.go index 8b31bfdd8b17..984e3e510b83 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/metadata/client.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/metadata/client.go @@ -7,6 +7,7 @@ import ( "net/http" "strconv" "strings" + "time" "github.com/prometheus/client_golang/prometheus" @@ -18,46 +19,57 @@ const Endpoint = "http://169.254.169.254/hetzner/v1/metadata" // Client is a client for the Hetzner Cloud Server Metadata Endpoints. type Client struct { endpoint string + timeout time.Duration httpClient *http.Client instrumentationRegistry *prometheus.Registry } -// A ClientOption is used to configure a Client. +// A ClientOption is used to configure a [Client]. type ClientOption func(*Client) -// WithEndpoint configures a Client to use the specified Metadata API endpoint. +// WithEndpoint configures a [Client] to use the specified Metadata API endpoint. func WithEndpoint(endpoint string) ClientOption { return func(client *Client) { client.endpoint = strings.TrimRight(endpoint, "/") } } -// WithHTTPClient configures a Client to perform HTTP requests with httpClient. +// WithHTTPClient configures a [Client] to perform HTTP requests with httpClient. func WithHTTPClient(httpClient *http.Client) ClientOption { return func(client *Client) { client.httpClient = httpClient } } -// WithInstrumentation configures a Client to collect metrics about the performed HTTP requests. +// WithInstrumentation configures a [Client] to collect metrics about the performed HTTP requests. func WithInstrumentation(registry *prometheus.Registry) ClientOption { return func(client *Client) { client.instrumentationRegistry = registry } } -// NewClient creates a new client. +// WithTimeout specifies a time limit for requests made by this [Client]. Defaults to 5 seconds. +func WithTimeout(timeout time.Duration) ClientOption { + return func(client *Client) { + client.timeout = timeout + } +} + +// NewClient creates a new [Client] with the options applied. func NewClient(options ...ClientOption) *Client { client := &Client{ endpoint: Endpoint, httpClient: &http.Client{}, + timeout: 5 * time.Second, } for _, option := range options { option(client) } + client.httpClient.Timeout = client.timeout + if client.instrumentationRegistry != nil { i := instrumentation.New("metadata", client.instrumentationRegistry) client.httpClient.Transport = i.InstrumentedRoundTripper() @@ -65,8 +77,7 @@ func NewClient(options ...ClientOption) *Client { return client } -// NewRequest creates an HTTP request against the API. The returned request -// is assigned with ctx and has all necessary headers set (auth, user agent, etc.). +// get executes an HTTP request against the API. func (c *Client) get(path string) (string, error) { url := c.endpoint + path resp, err := c.httpClient.Get(url) diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/network.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/network.go index ccce14208e30..d0452bc93ce4 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/network.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/network.go @@ -73,6 +73,7 @@ type NetworkProtection struct { // NetworkClient is a client for the network API. type NetworkClient struct { client *Client + Action *ResourceActionClient } // GetByID retrieves a network by its ID. If the network does not exist, nil is returned. @@ -162,7 +163,7 @@ func (c *NetworkClient) All(ctx context.Context) ([]*Network, error) { // AllWithOpts returns all networks for the given options. func (c *NetworkClient) AllWithOpts(ctx context.Context, opts NetworkListOpts) ([]*Network, error) { - var allNetworks []*Network + allNetworks := []*Network{} err := c.client.all(func(page int) (*Response, error) { opts.Page = page diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/placement_group.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/placement_group.go index 3355c182e0cb..6907ad4495cb 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/placement_group.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/placement_group.go @@ -133,7 +133,7 @@ func (c *PlacementGroupClient) All(ctx context.Context) ([]*PlacementGroup, erro // AllWithOpts returns all PlacementGroups for the given options. func (c *PlacementGroupClient) AllWithOpts(ctx context.Context, opts PlacementGroupListOpts) ([]*PlacementGroup, error) { - var allPlacementGroups []*PlacementGroup + allPlacementGroups := []*PlacementGroup{} err := c.client.all(func(page int) (*Response, error) { opts.Page = page diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/primary_ip.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/primary_ip.go index 8e43b3a3dc1c..48cc4edde194 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/primary_ip.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/primary_ip.go @@ -160,6 +160,7 @@ type PrimaryIPChangeProtectionResult struct { // PrimaryIPClient is a client for the Primary IP API. type PrimaryIPClient struct { client *Client + Action *ResourceActionClient } // GetByID retrieves a Primary IP by its ID. If the Primary IP does not exist, nil is returned. @@ -265,7 +266,7 @@ func (c *PrimaryIPClient) All(ctx context.Context) ([]*PrimaryIP, error) { // AllWithOpts returns all Primary IPs for the given options. func (c *PrimaryIPClient) AllWithOpts(ctx context.Context, opts PrimaryIPListOpts) ([]*PrimaryIP, error) { - var allPrimaryIPs []*PrimaryIP + allPrimaryIPs := []*PrimaryIP{} err := c.client.all(func(page int) (*Response, error) { opts.Page = page diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema.go index d03317db8ad3..59f938f91801 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema.go @@ -124,6 +124,9 @@ func ISOFromSchema(s schema.ISO) *ISO { Description: s.Description, Type: ISOType(s.Type), Deprecated: s.Deprecated, + DeprecatableResource: DeprecatableResource{ + DeprecationFromSchema(s.Deprecation), + }, } if s.Architecture != nil { iso.Architecture = Ptr(Architecture(*s.Architecture)) @@ -970,7 +973,7 @@ func loadBalancerCreateOptsToSchema(opts LoadBalancerCreateOpts) schema.LoadBala TLS: service.HealthCheck.HTTP.TLS, } if service.HealthCheck.HTTP.StatusCodes != nil { - schemaHealthCheckHTTP.StatusCodes = &service.HealthCheck.HTTP.StatusCodes + schemaHealthCheckHTTP.StatusCodes = &service.HealthCheck.HTTP.StatusCodes //nolint:gosec // does not result in bug } schemaHealthCheck.HTTP = schemaHealthCheckHTTP } diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/iso.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/iso.go index 4f89dd04627e..0fcb1a6c9451 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/iso.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/iso.go @@ -10,6 +10,7 @@ type ISO struct { Type string `json:"type"` Architecture *string `json:"architecture"` Deprecated time.Time `json:"deprecated"` + DeprecatableResource } // ISOGetResponse defines the schema of the response when retrieving a single ISO. diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/server.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/server.go index 9e2071f3b780..ff6e23444a33 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/server.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/server.go @@ -191,6 +191,7 @@ func (s *Server) GetDNSPtrForIP(ip net.IP) (string, error) { // ServerClient is a client for the servers API. type ServerClient struct { client *Client + Action *ResourceActionClient } // GetByID retrieves a server by its ID. If the server does not exist, nil is returned. diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/server_type.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/server_type.go index aebaebf3c55b..9640a2ac5482 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/server_type.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/server_type.go @@ -140,7 +140,7 @@ func (c *ServerTypeClient) All(ctx context.Context) ([]*ServerType, error) { // AllWithOpts returns all server types for the given options. func (c *ServerTypeClient) AllWithOpts(ctx context.Context, opts ServerTypeListOpts) ([]*ServerType, error) { - var allServerTypes []*ServerType + allServerTypes := []*ServerType{} err := c.client.all(func(page int) (*Response, error) { opts.Page = page diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/volume.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/volume.go index 025907d401ab..754c89efb972 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/volume.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/volume.go @@ -35,6 +35,7 @@ type VolumeProtection struct { // VolumeClient is a client for the volume API. type VolumeClient struct { client *Client + Action *ResourceActionClient } // VolumeStatus specifies a volume's status.