From 97e93742fae2b860dab96d35a68a67393bf8e11e Mon Sep 17 00:00:00 2001 From: Sergey Shevchenko Date: Fri, 18 Mar 2022 15:59:27 +0400 Subject: [PATCH] [ca]: Upgrade hcloud-go to 1.33.1 Signed-off-by: Sergey Shevchenko --- .../hetzner/hcloud-go/hcloud/client.go | 38 ++- .../hetzner/hcloud-go/hcloud/error.go | 32 ++- .../hetzner/hcloud-go/hcloud/firewall.go | 1 + .../hetzner/hcloud-go/hcloud/floating_ip.go | 62 +++-- .../hetzner/hcloud-go/hcloud/hcloud.go | 2 +- .../internal/instrumentation/metrics.go | 102 +++++++ .../internal/instrumentation/metrics_test.go | 45 +++ .../hetzner/hcloud-go/hcloud/load_balancer.go | 54 +++- .../hcloud-go/hcloud/metadata/client.go | 149 ++++++++++ .../hetzner/hcloud-go/hcloud/network.go | 1 + .../hcloud-go/hcloud/placement_group.go | 259 ++++++++++++++++++ .../hetzner/hcloud-go/hcloud/pricing.go | 14 + .../hetzner/hcloud-go/hcloud/rdns.go | 62 +++++ .../hetzner/hcloud-go/hcloud/schema.go | 64 ++++- .../hcloud-go/hcloud/schema/firewall.go | 1 + .../hcloud-go/hcloud/schema/load_balancer.go | 19 +- .../hcloud/schema/placement_group.go | 56 ++++ .../hcloud-go/hcloud/schema/pricing.go | 14 + .../hetzner/hcloud-go/hcloud/schema/server.go | 20 ++ .../hetzner/hcloud-go/hcloud/server.go | 104 +++++-- .../cloudprovider/hetzner/hetzner_manager.go | 3 +- 21 files changed, 1033 insertions(+), 69 deletions(-) create mode 100644 cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/internal/instrumentation/metrics.go create mode 100644 cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/internal/instrumentation/metrics_test.go create mode 100644 cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/metadata/client.go create mode 100644 cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/placement_group.go create mode 100644 cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/rdns.go create mode 100644 cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/placement_group.go diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/client.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/client.go index 9af15d51721e..b85cd4617ed8 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/client.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/client.go @@ -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" ) @@ -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 @@ -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. @@ -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{ @@ -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} @@ -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 } diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/error.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/error.go index be81fbb0bf2b..49407e574801 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/error.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/error.go @@ -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 @@ -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 @@ -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()) +} diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/firewall.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/firewall.go index ca9889ff9a65..b69b734c385c 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/firewall.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/firewall.go @@ -47,6 +47,7 @@ type FirewallRule struct { DestinationIPs []net.IPNet Protocol FirewallRuleProtocol Port *string + Description *string } // FirewallRuleDirection specifies the direction of a Firewall rule. 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 37db55192629..ad86d7c242f2 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/floating_ip.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/floating_ip.go @@ -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()] } @@ -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 @@ -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. diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/hcloud.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/hcloud.go index 879f39b22463..ac2d5cb28e38 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/hcloud.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/hcloud.go @@ -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" 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 new file mode 100644 index 000000000000..844ba87083fc --- /dev/null +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/internal/instrumentation/metrics.go @@ -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__requests_total) + instrumentationRegistry *prometheus.Registry +} + +// New creates a new Instrumenter. The subsystemIdentifier will be used as part of the metric names (e.g. hcloud__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) +} diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/internal/instrumentation/metrics_test.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/internal/instrumentation/metrics_test.go new file mode 100644 index 000000000000..a8651f2e5234 --- /dev/null +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/internal/instrumentation/metrics_test.go @@ -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) + } + }) + } +} 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 0cdc4b223cf6..c6eadb717729 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/load_balancer.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/load_balancer.go @@ -58,12 +58,14 @@ type LoadBalancerPublicNet struct { // LoadBalancerPublicNetIPv4 represents a Load Balancer's public IPv4 address. type LoadBalancerPublicNetIPv4 struct { - IP net.IP + IP net.IP + DNSPtr string } // LoadBalancerPublicNetIPv6 represents a Load Balancer's public IPv6 address. type LoadBalancerPublicNetIPv6 struct { - IP net.IP + IP net.IP + DNSPtr string } // LoadBalancerPrivateNet represents a Load Balancer's private network. @@ -211,6 +213,44 @@ type LoadBalancerProtection struct { Delete bool } +// 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 (lb *LoadBalancer) changeDNSPtr(ctx context.Context, client *Client, ip net.IP, ptr *string) (*Action, *Response, error) { + reqBody := schema.LoadBalancerActionChangeDNSPtrRequest{ + IP: ip.String(), + DNSPtr: ptr, + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/load_balancers/%d/actions/change_dns_ptr", lb.ID) + req, err := client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + respBody := schema.LoadBalancerActionChangeDNSPtrResponse{} + 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 (lb *LoadBalancer) GetDNSPtrForIP(ip net.IP) (string, error) { + if net.IP.Equal(lb.PublicNet.IPv4.IP, ip) { + return lb.PublicNet.IPv4.DNSPtr, nil + } else if net.IP.Equal(lb.PublicNet.IPv6.IP, ip) { + return lb.PublicNet.IPv6.DNSPtr, nil + } + + return "", DNSNotFoundError{ip} +} + // LoadBalancerClient is a client for the Load Balancers API. type LoadBalancerClient struct { client *Client @@ -1040,3 +1080,13 @@ func (c *LoadBalancerClient) GetMetrics( } return ms, resp, nil } + +// ChangeDNSPtr changes or resets the reverse DNS pointer for a Load Balancer. +// Pass a nil ptr to reset the reverse DNS pointer to its default value. +func (c *LoadBalancerClient) ChangeDNSPtr(ctx context.Context, lb *LoadBalancer, ip string, ptr *string) (*Action, *Response, error) { + netIP := net.ParseIP(ip) + if netIP == nil { + return nil, nil, InvalidIPError{ip} + } + return lb.changeDNSPtr(ctx, c.client, net.ParseIP(ip), ptr) +} diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/metadata/client.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/metadata/client.go new file mode 100644 index 000000000000..e96577f76e1d --- /dev/null +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/metadata/client.go @@ -0,0 +1,149 @@ +/* +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 metadata + +import ( + "io/ioutil" + "net" + "net/http" + "strconv" + "strings" + + "github.com/prometheus/client_golang/prometheus" + + "k8s.io/autoscaler/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/internal/instrumentation" +) + +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 + + httpClient *http.Client + instrumentationRegistry *prometheus.Registry +} + +// A ClientOption is used to configure a Client. +type ClientOption func(*Client) + +// 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. +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. +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{ + endpoint: Endpoint, + httpClient: &http.Client{}, + } + + for _, option := range options { + option(client) + } + + if client.instrumentationRegistry != nil { + i := instrumentation.New("metadata", client.instrumentationRegistry) + client.httpClient.Transport = i.InstrumentedRoundTripper() + } + 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.). +func (c *Client) get(path string) (string, error) { + url := c.endpoint + path + resp, err := c.httpClient.Get(url) + if err != nil { + return "", err + } + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", err + } + resp.Body.Close() + return string(body), nil +} + +// IsHcloudServer checks if the currently called server is a hcloud server by calling a metadata endpoint +// if the endpoint answers with a non-empty value this method returns true, otherwise false +func (c *Client) IsHcloudServer() bool { + hostname, err := c.Hostname() + if err != nil { + return false + } + if len(hostname) > 0 { + return true + } + return false +} + +// Hostname returns the hostname of the server that did the request to the Metadata server +func (c *Client) Hostname() (string, error) { + return c.get("/hostname") +} + +// InstanceID returns the ID of the server that did the request to the Metadata server +func (c *Client) InstanceID() (int, error) { + resp, err := c.get("/instance-id") + if err != nil { + return 0, err + } + return strconv.Atoi(resp) +} + +// PublicIPv4 returns the Public IPv4 of the server that did the request to the Metadata server +func (c *Client) PublicIPv4() (net.IP, error) { + resp, err := c.get("/public-ipv4") + if err != nil { + return nil, err + } + return net.ParseIP(resp), nil +} + +// Region returns the Network Zone of the server that did the request to the Metadata server +func (c *Client) Region() (string, error) { + return c.get("/region") +} + +// AvailabilityZone returns the datacenter of the server that did the request to the Metadata server +func (c *Client) AvailabilityZone() (string, error) { + return c.get("/availability-zone") +} + +// PrivateNetworks returns details about the private networks the server is attached to +// Returns YAML (unparsed) +func (c *Client) PrivateNetworks() (string, error) { + return c.get("/private-networks") +} diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/network.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/network.go index fa8baff5c67a..25bc67713ad3 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/network.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/network.go @@ -36,6 +36,7 @@ type NetworkZone string // List of available Network Zones. const ( NetworkZoneEUCentral NetworkZone = "eu-central" + NetworkZoneUSEast NetworkZone = "us-east" ) // NetworkSubnetType specifies a type of a subnet. diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/placement_group.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/placement_group.go new file mode 100644 index 000000000000..9dbe6b87371f --- /dev/null +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/placement_group.go @@ -0,0 +1,259 @@ +/* +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 hcloud + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "net/url" + "strconv" + "time" + + "k8s.io/autoscaler/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema" +) + +// PlacementGroup represents a Placement Group in the Hetzner Cloud. +type PlacementGroup struct { + ID int + Name string + Labels map[string]string + Created time.Time + Servers []int + Type PlacementGroupType +} + +// PlacementGroupType specifies the type of a Placement Group +type PlacementGroupType string + +const ( + // PlacementGroupTypeSpread spreads all servers in the group on different vhosts + PlacementGroupTypeSpread PlacementGroupType = "spread" +) + +// PlacementGroupClient is a client for the Placement Groups API. +type PlacementGroupClient struct { + client *Client +} + +// GetByID retrieves a PlacementGroup by its ID. If the PlacementGroup does not exist, nil is returned. +func (c *PlacementGroupClient) GetByID(ctx context.Context, id int) (*PlacementGroup, *Response, error) { + req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/placement_groups/%d", id), nil) + if err != nil { + return nil, nil, err + } + + var body schema.PlacementGroupGetResponse + resp, err := c.client.Do(req, &body) + if err != nil { + if IsError(err, ErrorCodeNotFound) { + return nil, resp, nil + } + return nil, nil, err + } + return PlacementGroupFromSchema(body.PlacementGroup), resp, nil +} + +// GetByName retrieves a PlacementGroup by its name. If the PlacementGroup does not exist, nil is returned. +func (c *PlacementGroupClient) GetByName(ctx context.Context, name string) (*PlacementGroup, *Response, error) { + if name == "" { + return nil, nil, nil + } + placementGroups, response, err := c.List(ctx, PlacementGroupListOpts{Name: name}) + if len(placementGroups) == 0 { + return nil, response, err + } + return placementGroups[0], response, err +} + +// Get retrieves a PlacementGroup by its ID if the input can be parsed as an integer, otherwise it +// retrieves a PlacementGroup by its name. If the PlacementGroup does not exist, nil is returned. +func (c *PlacementGroupClient) Get(ctx context.Context, idOrName string) (*PlacementGroup, *Response, error) { + if id, err := strconv.Atoi(idOrName); err == nil { + return c.GetByID(ctx, int(id)) + } + return c.GetByName(ctx, idOrName) +} + +// PlacementGroupListOpts specifies options for listing PlacementGroup. +type PlacementGroupListOpts struct { + ListOpts + Name string + Type PlacementGroupType +} + +func (l PlacementGroupListOpts) values() url.Values { + vals := l.ListOpts.values() + if l.Name != "" { + vals.Add("name", l.Name) + } + if l.Type != "" { + vals.Add("type", string(l.Type)) + } + return vals +} + +// List returns a list of PlacementGroups 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 *PlacementGroupClient) List(ctx context.Context, opts PlacementGroupListOpts) ([]*PlacementGroup, *Response, error) { + path := "/placement_groups?" + opts.values().Encode() + req, err := c.client.NewRequest(ctx, "GET", path, nil) + if err != nil { + return nil, nil, err + } + + var body schema.PlacementGroupListResponse + resp, err := c.client.Do(req, &body) + if err != nil { + return nil, nil, err + } + placementGroups := make([]*PlacementGroup, 0, len(body.PlacementGroups)) + for _, g := range body.PlacementGroups { + placementGroups = append(placementGroups, PlacementGroupFromSchema(g)) + } + return placementGroups, resp, nil +} + +// All returns all PlacementGroups. +func (c *PlacementGroupClient) All(ctx context.Context) ([]*PlacementGroup, error) { + opts := PlacementGroupListOpts{ + ListOpts: ListOpts{ + PerPage: 50, + }, + } + + return c.AllWithOpts(ctx, opts) +} + +// AllWithOpts returns all PlacementGroups for the given options. +func (c *PlacementGroupClient) AllWithOpts(ctx context.Context, opts PlacementGroupListOpts) ([]*PlacementGroup, error) { + var allPlacementGroups []*PlacementGroup + + err := c.client.all(func(page int) (*Response, error) { + opts.Page = page + placementGroups, resp, err := c.List(ctx, opts) + if err != nil { + return resp, err + } + allPlacementGroups = append(allPlacementGroups, placementGroups...) + return resp, nil + }) + if err != nil { + return nil, err + } + + return allPlacementGroups, nil +} + +// PlacementGroupCreateOpts specifies options for creating a new PlacementGroup. +type PlacementGroupCreateOpts struct { + Name string + Labels map[string]string + Type PlacementGroupType +} + +// Validate checks if options are valid +func (o PlacementGroupCreateOpts) Validate() error { + if o.Name == "" { + return errors.New("missing name") + } + return nil +} + +// PlacementGroupCreateResult is the result of a create PlacementGroup call. +type PlacementGroupCreateResult struct { + PlacementGroup *PlacementGroup + Action *Action +} + +// Create creates a new PlacementGroup +func (c *PlacementGroupClient) Create(ctx context.Context, opts PlacementGroupCreateOpts) (PlacementGroupCreateResult, *Response, error) { + if err := opts.Validate(); err != nil { + return PlacementGroupCreateResult{}, nil, err + } + reqBody := placementGroupCreateOptsToSchema(opts) + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return PlacementGroupCreateResult{}, nil, err + } + req, err := c.client.NewRequest(ctx, "POST", "/placement_groups", bytes.NewReader(reqBodyData)) + if err != nil { + return PlacementGroupCreateResult{}, nil, err + } + + respBody := schema.PlacementGroupCreateResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return PlacementGroupCreateResult{}, nil, err + } + result := PlacementGroupCreateResult{ + PlacementGroup: PlacementGroupFromSchema(respBody.PlacementGroup), + } + if respBody.Action != nil { + result.Action = ActionFromSchema(*respBody.Action) + } + + return result, resp, nil +} + +// PlacementGroupUpdateOpts specifies options for updating a PlacementGroup. +type PlacementGroupUpdateOpts struct { + Name string + Labels map[string]string +} + +// Update updates a PlacementGroup. +func (c *PlacementGroupClient) Update(ctx context.Context, placementGroup *PlacementGroup, opts PlacementGroupUpdateOpts) (*PlacementGroup, *Response, error) { + reqBody := schema.PlacementGroupUpdateRequest{} + if opts.Name != "" { + reqBody.Name = &opts.Name + } + if opts.Labels != nil { + reqBody.Labels = &opts.Labels + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/placement_groups/%d", placementGroup.ID) + req, err := c.client.NewRequest(ctx, "PUT", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + respBody := schema.PlacementGroupUpdateResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + + return PlacementGroupFromSchema(respBody.PlacementGroup), resp, nil +} + +// Delete deletes a PlacementGroup. +func (c *PlacementGroupClient) Delete(ctx context.Context, placementGroup *PlacementGroup) (*Response, error) { + req, err := c.client.NewRequest(ctx, "DELETE", fmt.Sprintf("/placement_groups/%d", placementGroup.ID), nil) + if err != nil { + return nil, err + } + return c.client.Do(req, nil) +} diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/pricing.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/pricing.go index 6325bd8330ee..ad2bcbe3bfd4 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/pricing.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/pricing.go @@ -26,6 +26,7 @@ import ( type Pricing struct { Image ImagePricing FloatingIP FloatingIPPricing + FloatingIPs []FloatingIPTypePricing Traffic TrafficPricing ServerBackup ServerBackupPricing ServerTypes []ServerTypePricing @@ -53,6 +54,19 @@ type FloatingIPPricing struct { Monthly Price } +// FloatingIPTypePricing provides pricing information for Floating IPs per Type. +type FloatingIPTypePricing struct { + Type FloatingIPType + Pricings []FloatingIPTypeLocationPricing +} + +// FloatingIPTypeLocationPricing provides pricing information for a Floating IP type +// at a location. +type FloatingIPTypeLocationPricing struct { + Location *Location + Monthly Price +} + // TrafficPricing provides pricing information for traffic. type TrafficPricing struct { PerTB Price diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/rdns.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/rdns.go new file mode 100644 index 000000000000..d7cbf0b0db6b --- /dev/null +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/rdns.go @@ -0,0 +1,62 @@ +/* +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 hcloud + +import ( + "context" + "fmt" + "net" +) + +// RDNSSupporter defines functions to change and lookup reverse dns entries. +// currently implemented by Server, FloatingIP and LoadBalancer +type RDNSSupporter interface { + // 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. + changeDNSPtr(ctx context.Context, client *Client, ip net.IP, ptr *string) (*Action, *Response, error) + // 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. + GetDNSPtrForIP(ip net.IP) (string, error) +} + +// RDNSClient simplifys the handling objects which support reverse dns entries. +type RDNSClient struct { + client *Client +} + +// 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 (c *RDNSClient) ChangeDNSPtr(ctx context.Context, rdns RDNSSupporter, ip net.IP, ptr *string) (*Action, *Response, error) { + return rdns.changeDNSPtr(ctx, c.client, ip, ptr) +} + +// SupportsRDNS checks if the object supports reverse dns functions. +func SupportsRDNS(i interface{}) bool { + _, ok := i.(RDNSSupporter) + return ok +} + +// RDNSLookup searches for the dns assigned to the given IP address. +// It returns an error if the object does not support reverse dns or if there is no dns set for the given IP address. +func RDNSLookup(i interface{}, ip net.IP) (string, error) { + rdns, ok := i.(RDNSSupporter) + if !ok { + return "", fmt.Errorf("%+v does not support RDNS", i) + } + + return rdns.GetDNSPtrForIP(ip) +} diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema.go index 641b762607b1..2baa4202b7ce 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema.go @@ -188,6 +188,9 @@ func ServerFromSchema(s schema.Server) *Server { for _, privNet := range s.PrivateNet { server.PrivateNet = append(server.PrivateNet, ServerPrivateNetFromSchema(privNet)) } + if s.PlacementGroup != nil { + server.PlacementGroup = PlacementGroupFromSchema(*s.PlacementGroup) + } return server } @@ -446,10 +449,12 @@ func LoadBalancerFromSchema(s schema.LoadBalancer) *LoadBalancer { PublicNet: LoadBalancerPublicNet{ Enabled: s.PublicNet.Enabled, IPv4: LoadBalancerPublicNetIPv4{ - IP: net.ParseIP(s.PublicNet.IPv4.IP), + IP: net.ParseIP(s.PublicNet.IPv4.IP), + DNSPtr: s.PublicNet.IPv4.DNSPtr, }, IPv6: LoadBalancerPublicNetIPv6{ - IP: net.ParseIP(s.PublicNet.IPv6.IP), + IP: net.ParseIP(s.PublicNet.IPv6.IP), + DNSPtr: s.PublicNet.IPv6.DNSPtr, }, }, Location: LocationFromSchema(s.Location), @@ -676,6 +681,22 @@ func PricingFromSchema(s schema.Pricing) Pricing { }, }, } + for _, floatingIPType := range s.FloatingIPs { + var pricings []FloatingIPTypeLocationPricing + for _, price := range floatingIPType.Prices { + p := FloatingIPTypeLocationPricing{ + Location: &Location{Name: price.Location}, + Monthly: Price{ + Currency: s.Currency, + VATRate: s.VATRate, + Net: price.PriceMonthly.Net, + Gross: price.PriceMonthly.Gross, + }, + } + pricings = append(pricings, p) + } + p.FloatingIPs = append(p.FloatingIPs, FloatingIPTypePricing{Type: FloatingIPType(floatingIPType.Type), Pricings: pricings}) + } for _, serverType := range s.ServerTypes { var pricings []ServerTypeLocationPricing for _, price := range serverType.Prices { @@ -775,11 +796,36 @@ func FirewallFromSchema(s schema.Firewall) *Firewall { DestinationIPs: destinationIPs, Protocol: FirewallRuleProtocol(rule.Protocol), Port: rule.Port, + Description: rule.Description, }) } return f } +// PlacementGroupFromSchema converts a schema.PlacementGroup to a PlacementGroup. +func PlacementGroupFromSchema(s schema.PlacementGroup) *PlacementGroup { + g := &PlacementGroup{ + ID: s.ID, + Name: s.Name, + Labels: s.Labels, + Created: s.Created, + Servers: s.Servers, + Type: PlacementGroupType(s.Type), + } + return g +} + +func placementGroupCreateOptsToSchema(opts PlacementGroupCreateOpts) schema.PlacementGroupCreateRequest { + req := schema.PlacementGroupCreateRequest{ + Name: opts.Name, + Type: string(opts.Type), + } + if opts.Labels != nil { + req.Labels = &opts.Labels + } + return req +} + func loadBalancerCreateOptsToSchema(opts LoadBalancerCreateOpts) schema.LoadBalancerCreateRequest { req := schema.LoadBalancerCreateRequest{ Name: opts.Name, @@ -997,9 +1043,10 @@ func firewallCreateOptsToSchema(opts FirewallCreateOpts) schema.FirewallCreateRe } for _, rule := range opts.Rules { schemaRule := schema.FirewallRule{ - Direction: string(rule.Direction), - Protocol: string(rule.Protocol), - Port: rule.Port, + Direction: string(rule.Direction), + Protocol: string(rule.Protocol), + Port: rule.Port, + Description: rule.Description, } switch rule.Direction { case FirewallRuleDirectionOut: @@ -1037,9 +1084,10 @@ func firewallSetRulesOptsToSchema(opts FirewallSetRulesOpts) schema.FirewallActi req := schema.FirewallActionSetRulesRequest{Rules: []schema.FirewallRule{}} for _, rule := range opts.Rules { schemaRule := schema.FirewallRule{ - Direction: string(rule.Direction), - Protocol: string(rule.Protocol), - Port: rule.Port, + Direction: string(rule.Direction), + Protocol: string(rule.Protocol), + Port: rule.Port, + Description: rule.Description, } switch rule.Direction { case FirewallRuleDirectionOut: diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/firewall.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/firewall.go index da94e9f7e997..6c32bb05bf43 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/firewall.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/firewall.go @@ -35,6 +35,7 @@ type FirewallRule struct { DestinationIPs []string `json:"destination_ips,omitempty"` Protocol string `json:"protocol"` Port *string `json:"port,omitempty"` + Description *string `json:"description,omitempty"` } // FirewallListResponse defines the schema of the response when listing Firewalls. diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/load_balancer.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/load_balancer.go index bf3a2525819e..f50b2b592939 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/load_balancer.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/load_balancer.go @@ -43,11 +43,13 @@ type LoadBalancerPublicNet struct { } type LoadBalancerPublicNetIPv4 struct { - IP string `json:"ip"` + IP string `json:"ip"` + DNSPtr string `json:"dns_ptr"` } type LoadBalancerPublicNetIPv6 struct { - IP string `json:"ip"` + IP string `json:"ip"` + DNSPtr string `json:"dns_ptr"` } type LoadBalancerPrivateNet struct { @@ -417,3 +419,16 @@ type LoadBalancerGetMetricsResponse struct { type LoadBalancerTimeSeriesVals struct { Values []interface{} `json:"values"` } + +// LoadBalancerActionChangeDNSPtrRequest defines the schema for the request to +// change a Load Balancer reverse DNS pointer. +type LoadBalancerActionChangeDNSPtrRequest struct { + IP string `json:"ip"` + DNSPtr *string `json:"dns_ptr"` +} + +// LoadBalancerActionChangeDNSPtrResponse defines the schema of the response when +// creating a change_dns_ptr Floating IP action. +type LoadBalancerActionChangeDNSPtrResponse struct { + Action Action `json:"action"` +} diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/placement_group.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/placement_group.go new file mode 100644 index 000000000000..a13a8d5b4f31 --- /dev/null +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/placement_group.go @@ -0,0 +1,56 @@ +/* +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 schema + +import "time" + +type PlacementGroup struct { + ID int `json:"id"` + Name string `json:"name"` + Labels map[string]string `json:"labels"` + Created time.Time `json:"created"` + Servers []int `json:"servers"` + Type string `json:"type"` +} + +type PlacementGroupListResponse struct { + PlacementGroups []PlacementGroup `json:"placement_groups"` +} + +type PlacementGroupGetResponse struct { + PlacementGroup PlacementGroup `json:"placement_group"` +} + +type PlacementGroupCreateRequest struct { + Name string `json:"name"` + Labels *map[string]string `json:"labels,omitempty"` + Type string `json:"type"` +} + +type PlacementGroupCreateResponse struct { + PlacementGroup PlacementGroup `json:"placement_group"` + Action *Action `json:"action"` +} + +type PlacementGroupUpdateRequest struct { + Name *string `json:"name,omitempty"` + Labels *map[string]string `json:"labels,omitempty"` +} + +type PlacementGroupUpdateResponse struct { + PlacementGroup PlacementGroup `json:"placement_group"` +} diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/pricing.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/pricing.go index 3cb8c099ee11..5e8b80c09630 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/pricing.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/pricing.go @@ -22,6 +22,7 @@ type Pricing struct { VATRate string `json:"vat_rate"` Image PricingImage `json:"image"` FloatingIP PricingFloatingIP `json:"floating_ip"` + FloatingIPs []PricingFloatingIPType `json:"floating_ips"` Traffic PricingTraffic `json:"traffic"` ServerBackup PricingServerBackup `json:"server_backup"` ServerTypes []PricingServerType `json:"server_types"` @@ -45,6 +46,19 @@ type PricingFloatingIP struct { PriceMonthly Price `json:"price_monthly"` } +// PricingFloatingIPType defines the schema of pricing information for a Floating IP per type. +type PricingFloatingIPType struct { + Type string `json:"type"` + Prices []PricingFloatingIPTypePrice `json:"prices"` +} + +// PricingFloatingIPTypePrice defines the schema of pricing information for a Floating IP +// type at a location. +type PricingFloatingIPTypePrice struct { + Location string `json:"location"` + PriceMonthly Price `json:"price_monthly"` +} + // PricingTraffic defines the schema of pricing information for traffic. type PricingTraffic struct { PricePerTB Price `json:"price_per_tb"` diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/server.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/server.go index d04be6319fc2..2ac327381512 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/server.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/server.go @@ -40,6 +40,7 @@ type Server struct { Labels map[string]string `json:"labels"` Volumes []int `json:"volumes"` PrimaryDiskSize int `json:"primary_disk_size"` + PlacementGroup *PlacementGroup `json:"placement_group"` } // ServerProtection defines the schema of a server's resource protection. @@ -123,6 +124,7 @@ type ServerCreateRequest struct { Volumes []int `json:"volumes,omitempty"` Networks []int `json:"networks,omitempty"` Firewalls []ServerCreateFirewalls `json:"firewalls,omitempty"` + PlacementGroup int `json:"placement_group,omitempty"` } // ServerCreateFirewall defines which Firewalls to apply when creating a Server. @@ -411,3 +413,21 @@ type ServerGetMetricsResponse struct { type ServerTimeSeriesVals struct { Values []interface{} `json:"values"` } + +// ServerActionAddToPlacementGroupRequest defines the schema for the request to +// add a server to a placement group. +type ServerActionAddToPlacementGroupRequest struct { + PlacementGroup int `json:"placement_group"` +} + +// ServerActionAddToPlacementGroupResponse defines the schema of the response when +// creating an add_to_placement_group server action. +type ServerActionAddToPlacementGroupResponse struct { + Action Action `json:"action"` +} + +// ServerActionRemoveFromPlacementGroupResponse defines the schema of the response when +// creating a remove_from_placement_group server action. +type ServerActionRemoveFromPlacementGroupResponse struct { + Action Action `json:"action"` +} diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/server.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/server.go index 77984fd63d3c..79783fd94325 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/server.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/server.go @@ -53,6 +53,7 @@ type Server struct { Labels map[string]string Volumes []*Volume PrimaryDiskSize int + PlacementGroup *PlacementGroup } // ServerProtection represents the protection level of a server. @@ -156,6 +157,44 @@ const ( ServerRescueTypeFreeBSD64 ServerRescueType = "freebsd64" ) +// 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 (s *Server) changeDNSPtr(ctx context.Context, client *Client, ip net.IP, ptr *string) (*Action, *Response, error) { + reqBody := schema.ServerActionChangeDNSPtrRequest{ + IP: ip.String(), + DNSPtr: ptr, + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/servers/%d/actions/change_dns_ptr", s.ID) + req, err := client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + respBody := schema.ServerActionChangeDNSPtrResponse{} + 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 (s *Server) GetDNSPtrForIP(ip net.IP) (string, error) { + if net.IP.Equal(s.PublicNet.IPv4.IP, ip) { + return s.PublicNet.IPv4.DNSPtr, nil + } else if dns, ok := s.PublicNet.IPv6.DNSPtr[ip.String()]; ok { + return dns, nil + } + + return "", DNSNotFoundError{ip} +} + // ServerClient is a client for the servers API. type ServerClient struct { client *Client @@ -281,6 +320,7 @@ type ServerCreateOpts struct { Volumes []*Volume Networks []*Network Firewalls []*ServerCreateFirewall + PlacementGroup *PlacementGroup } // ServerCreateFirewall defines which Firewalls to apply when creating a Server. @@ -365,6 +405,9 @@ func (c *ServerClient) Create(ctx context.Context, opts ServerCreateOpts) (Serve reqBody.Datacenter = opts.Datacenter.Name } } + if opts.PlacementGroup != nil { + reqBody.PlacementGroup = opts.PlacementGroup.ID + } reqBodyData, err := json.Marshal(reqBody) if err != nil { return ServerCreateResult{}, nil, err @@ -819,27 +862,11 @@ func (c *ServerClient) ChangeType(ctx context.Context, server *Server, opts Serv // ChangeDNSPtr changes or resets the reverse DNS pointer for a server IP address. // Pass a nil ptr to reset the reverse DNS pointer to its default value. func (c *ServerClient) ChangeDNSPtr(ctx context.Context, server *Server, ip string, ptr *string) (*Action, *Response, error) { - reqBody := schema.ServerActionChangeDNSPtrRequest{ - IP: ip, - DNSPtr: ptr, - } - reqBodyData, err := json.Marshal(reqBody) - if err != nil { - return nil, nil, err - } - - path := fmt.Sprintf("/servers/%d/actions/change_dns_ptr", server.ID) - req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) - if err != nil { - return nil, nil, err + netIP := net.ParseIP(ip) + if netIP == nil { + return nil, nil, InvalidIPError{ip} } - - respBody := schema.ServerActionChangeDNSPtrResponse{} - resp, err := c.client.Do(req, &respBody) - if err != nil { - return nil, resp, err - } - return ActionFromSchema(respBody.Action), resp, nil + return server.changeDNSPtr(ctx, c.client, net.ParseIP(ip), ptr) } // ServerChangeProtectionOpts specifies options for changing the resource protection level of a server. @@ -1086,3 +1113,40 @@ func (c *ServerClient) GetMetrics(ctx context.Context, server *Server, opts Serv } return ms, resp, nil } + +func (c *ServerClient) AddToPlacementGroup(ctx context.Context, server *Server, placementGroup *PlacementGroup) (*Action, *Response, error) { + reqBody := schema.ServerActionAddToPlacementGroupRequest{ + PlacementGroup: placementGroup.ID, + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + path := fmt.Sprintf("/servers/%d/actions/add_to_placement_group", server.ID) + req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + respBody := schema.ServerActionAddToPlacementGroupResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, err +} + +func (c *ServerClient) RemoveFromPlacementGroup(ctx context.Context, server *Server) (*Action, *Response, error) { + path := fmt.Sprintf("/servers/%d/actions/remove_from_placement_group", server.ID) + req, err := c.client.NewRequest(ctx, "POST", path, nil) + if err != nil { + return nil, nil, err + } + + respBody := schema.ServerActionRemoveFromPlacementGroupResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, err +} diff --git a/cluster-autoscaler/cloudprovider/hetzner/hetzner_manager.go b/cluster-autoscaler/cloudprovider/hetzner/hetzner_manager.go index aee95efb31f2..9cbf373df73b 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hetzner_manager.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hetzner_manager.go @@ -27,6 +27,7 @@ import ( "time" apiv1 "k8s.io/api/core/v1" + "k8s.io/autoscaler/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud" ) @@ -137,7 +138,7 @@ func newManager() (*hetznerManager, error) { image: image, sshKey: sshKey, network: network, - firewall: firewall, + firewall: firewall, createTimeout: createTimeout, apiCallContext: ctx, }