Skip to content

Commit

Permalink
[ca]: Upgrade hcloud-go to 1.33.1
Browse files Browse the repository at this point in the history
Signed-off-by: Sergey Shevchenko <[email protected]>
  • Loading branch information
sergeyshevch committed Mar 18, 2022
1 parent 1c6a71e commit 97e9374
Show file tree
Hide file tree
Showing 21 changed files with 1,033 additions and 69 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ import (
"strings"
"time"

"k8s.io/autoscaler/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/internal/instrumentation"

"github.com/prometheus/client_golang/prometheus"

"k8s.io/autoscaler/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema"
)

Expand Down Expand Up @@ -64,15 +68,16 @@ func ExponentialBackoff(b float64, d time.Duration) BackoffFunc {

// Client is a client for the Hetzner Cloud API.
type Client struct {
endpoint string
token string
pollInterval time.Duration
backoffFunc BackoffFunc
httpClient *http.Client
applicationName string
applicationVersion string
userAgent string
debugWriter io.Writer
endpoint string
token string
pollInterval time.Duration
backoffFunc BackoffFunc
httpClient *http.Client
applicationName string
applicationVersion string
userAgent string
debugWriter io.Writer
instrumentationRegistry *prometheus.Registry

Action ActionClient
Certificate CertificateClient
Expand All @@ -90,6 +95,8 @@ type Client struct {
ServerType ServerTypeClient
SSHKey SSHKeyClient
Volume VolumeClient
PlacementGroup PlacementGroupClient
RDNS RDNSClient
}

// A ClientOption is used to configure a Client.
Expand Down Expand Up @@ -149,6 +156,13 @@ func WithHTTPClient(httpClient *http.Client) ClientOption {
}
}

// 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.
func NewClient(options ...ClientOption) *Client {
client := &Client{
Expand All @@ -163,6 +177,10 @@ func NewClient(options ...ClientOption) *Client {
}

client.buildUserAgent()
if client.instrumentationRegistry != nil {
i := instrumentation.New("api", client.instrumentationRegistry)
client.httpClient.Transport = i.InstrumentedRoundTripper()
}

client.Action = ActionClient{client: client}
client.Datacenter = DatacenterClient{client: client}
Expand All @@ -180,6 +198,8 @@ func NewClient(options ...ClientOption) *Client {
client.LoadBalancerType = LoadBalancerTypeClient{client: client}
client.Certificate = CertificateClient{client: client}
client.Firewall = FirewallClient{client: client}
client.PlacementGroup = PlacementGroupClient{client: client}
client.RDNS = RDNSClient{client: client}

return client
}
Expand Down
32 changes: 26 additions & 6 deletions cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ limitations under the License.

package hcloud

import "fmt"
import (
"fmt"
"net"
)

// ErrorCode represents an error code returned from the API.
type ErrorCode string
Expand Down Expand Up @@ -68,11 +71,12 @@ const (
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
ErrorCodeResourceInUse ErrorCode = "resource_in_use" // Firewall must not be in use to be deleted
ErrorCodeServerAlreadyAdded ErrorCode = "server_already_added" // Server added more than one time to resource
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
ErrorCodeResourceInUse ErrorCode = "resource_in_use" // Firewall must not be in use to be deleted
ErrorCodeServerAlreadyAdded ErrorCode = "server_already_added" // Server added more than one time to resource
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
Expand Down Expand Up @@ -119,3 +123,19 @@ func IsError(err error, code ErrorCode) bool {
apiErr, ok := err.(Error)
return ok && apiErr.Code == code
}

type InvalidIPError struct {
IP string
}

func (e InvalidIPError) Error() string {
return fmt.Sprintf("could not parse ip address %s", e.IP)
}

type DNSNotFoundError struct {
IP net.IP
}

func (e DNSNotFoundError) Error() string {
return fmt.Sprintf("dns for ip %s not found", e.IP.String())
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ type FirewallRule struct {
DestinationIPs []net.IPNet
Protocol FirewallRuleProtocol
Port *string
Description *string
}

// FirewallRuleDirection specifies the direction of a Firewall rule.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ type FloatingIP struct {
}

// DNSPtrForIP returns the reverse DNS pointer of the IP address.
// Deprecated: Use GetDNSPtrForIP instead
func (f *FloatingIP) DNSPtrForIP(ip net.IP) string {
return f.DNSPtr[ip.String()]
}
Expand All @@ -66,6 +67,43 @@ const (
FloatingIPTypeIPv6 FloatingIPType = "ipv6"
)

// changeDNSPtr changes or resets the reverse DNS pointer for a IP address.
// Pass a nil ptr to reset the reverse DNS pointer to its default value.
func (f *FloatingIP) changeDNSPtr(ctx context.Context, client *Client, ip net.IP, ptr *string) (*Action, *Response, error) {
reqBody := schema.FloatingIPActionChangeDNSPtrRequest{
IP: ip.String(),
DNSPtr: ptr,
}
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return nil, nil, err
}

path := fmt.Sprintf("/floating_ips/%d/actions/change_dns_ptr", f.ID)
req, err := client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
if err != nil {
return nil, nil, err
}

respBody := schema.FloatingIPActionChangeDNSPtrResponse{}
resp, err := client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return ActionFromSchema(respBody.Action), resp, nil
}

// GetDNSPtrForIP searches for the dns assigned to the given IP address.
// It returns an error if there is no dns set for the given IP address.
func (f *FloatingIP) GetDNSPtrForIP(ip net.IP) (string, error) {
dns, ok := f.DNSPtr[ip.String()]
if !ok {
return "", DNSNotFoundError{ip}
}

return dns, nil
}

// FloatingIPClient is a client for the Floating IP API.
type FloatingIPClient struct {
client *Client
Expand Down Expand Up @@ -341,27 +379,11 @@ func (c *FloatingIPClient) Unassign(ctx context.Context, floatingIP *FloatingIP)
// ChangeDNSPtr changes or resets the reverse DNS pointer for a Floating IP address.
// Pass a nil ptr to reset the reverse DNS pointer to its default value.
func (c *FloatingIPClient) ChangeDNSPtr(ctx context.Context, floatingIP *FloatingIP, ip string, ptr *string) (*Action, *Response, error) {
reqBody := schema.FloatingIPActionChangeDNSPtrRequest{
IP: ip,
DNSPtr: ptr,
netIP := net.ParseIP(ip)
if netIP == nil {
return nil, nil, InvalidIPError{ip}
}
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return nil, nil, err
}

path := fmt.Sprintf("/floating_ips/%d/actions/change_dns_ptr", floatingIP.ID)
req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
if err != nil {
return nil, nil, err
}

respBody := schema.FloatingIPActionChangeDNSPtrResponse{}
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return ActionFromSchema(respBody.Action), resp, nil
return floatingIP.changeDNSPtr(ctx, c.client, net.ParseIP(ip), ptr)
}

// FloatingIPChangeProtectionOpts specifies options for changing the resource protection level of a Floating IP.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ limitations under the License.
package hcloud

// Version is the library's version following Semantic Versioning.
const Version = "1.27.0"
const Version = "1.32.0"
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package instrumentation

import (
"fmt"
"net/http"
"regexp"
"strconv"
"strings"

"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)

type Instrumenter struct {
subsystemIdentifier string // will be used as part of the metric name (hcloud_<identifier>_requests_total)
instrumentationRegistry *prometheus.Registry
}

// New creates a new Instrumenter. The subsystemIdentifier will be used as part of the metric names (e.g. hcloud_<identifier>_requests_total)
func New(subsystemIdentifier string, instrumentationRegistry *prometheus.Registry) *Instrumenter {
return &Instrumenter{subsystemIdentifier: subsystemIdentifier, instrumentationRegistry: instrumentationRegistry}
}

// 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"},
)

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"},
)

i.instrumentationRegistry.MustRegister(requestsPerEndpointCounter, requestLatencyHistogram, inFlightRequestsGauge)

return promhttp.InstrumentRoundTripperInFlight(inFlightRequestsGauge,
promhttp.InstrumentRoundTripperDuration(requestLatencyHistogram,
i.instrumentRoundTripperEndpoint(requestsPerEndpointCounter,
http.DefaultTransport,
),
),
)
}

// instrumentRoundTripperEndpoint implements a hcloud specific round tripper to count requests per API endpoint
// numeric IDs are removed from the URI Path.
// Sample:
// /volumes/1234/actions/attach --> /volumes/actions/attach
func (i *Instrumenter) instrumentRoundTripperEndpoint(counter *prometheus.CounterVec, next http.RoundTripper) promhttp.RoundTripperFunc {
return func(r *http.Request) (*http.Response, error) {
resp, err := next.RoundTrip(r)
if err == nil {
statusCode := strconv.Itoa(resp.StatusCode)
counter.WithLabelValues(statusCode, strings.ToLower(resp.Request.Method), preparePathForLabel(resp.Request.URL.Path)).Inc()
}
return resp, err
}
}

func preparePathForLabel(path string) string {
path = strings.ToLower(path)

// replace all numbers and chars that are not a-z, / or _
reg := regexp.MustCompile("[^a-z/_]+")
path = reg.ReplaceAllString(path, "")

// replace all artifacts of number replacement (//)
path = strings.ReplaceAll(path, "//", "/")

// replace the /v/ that indicated the API version
return strings.Replace(path, "/v/", "/", 1)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package instrumentation

import "testing"

func Test_preparePath(t *testing.T) {
tests := []struct {
name string
path string
want string
}{
{
"simple test",
"/v1/volumes/123456",
"/volumes/",
},
{
"simple test",
"/v1/volumes/123456/actions/attach",
"/volumes/actions/attach",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := preparePathForLabel(tt.path); got != tt.want {
t.Errorf("preparePathForLabel() = %v, want %v", got, tt.want)
}
})
}
}
Loading

0 comments on commit 97e9374

Please sign in to comment.