From 6525fe3500c51ecb5303ba704d32b1eceb25ead4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20T=C3=B6lle?= Date: Thu, 23 Mar 2023 13:46:48 +0100 Subject: [PATCH 1/3] chore(deps): update vendored hcloud-go to 1.42.0 --- .../hetzner/hcloud-go/hcloud/action.go | 53 +++++++++++--- .../metrics_test.go => architecture.go} | 37 +++------- .../hetzner/hcloud-go/hcloud/certificate.go | 4 +- .../hetzner/hcloud-go/hcloud/client.go | 55 +++++++++----- .../hetzner/hcloud-go/hcloud/error.go | 12 ++-- .../hetzner/hcloud-go/hcloud/floating_ip.go | 6 +- .../hetzner/hcloud-go/hcloud/hcloud.go | 2 +- .../hetzner/hcloud-go/hcloud/helper.go | 24 +++++-- .../hetzner/hcloud-go/hcloud/image.go | 44 ++++++++++-- .../internal/instrumentation/metrics.go | 7 +- .../hetzner/hcloud-go/hcloud/iso.go | 25 +++++-- .../hetzner/hcloud-go/hcloud/labels.go | 2 +- .../hetzner/hcloud-go/hcloud/load_balancer.go | 3 +- .../hcloud-go/hcloud/metadata/client.go | 20 +++--- .../hetzner/hcloud-go/hcloud/network.go | 1 + .../hcloud-go/hcloud/placement_group.go | 8 +-- .../hetzner/hcloud-go/hcloud/pricing.go | 5 +- .../hetzner/hcloud-go/hcloud/primary_ip.go | 34 ++++----- .../hetzner/hcloud-go/hcloud/rdns.go | 2 +- .../hetzner/hcloud-go/hcloud/schema.go | 72 ++++++++++--------- .../hetzner/hcloud-go/hcloud/schema/image.go | 35 ++++----- .../hetzner/hcloud-go/hcloud/schema/iso.go | 11 +-- .../hcloud-go/hcloud/schema/pricing.go | 7 +- .../hcloud-go/hcloud/schema/primary_ip.go | 8 ++- .../hetzner/hcloud-go/hcloud/schema/server.go | 9 ++- .../hcloud-go/hcloud/schema/server_type.go | 19 ++--- .../hetzner/hcloud-go/hcloud/server.go | 72 +++++++++++++++---- .../hetzner/hcloud-go/hcloud/server_type.go | 19 ++--- .../hetzner/hcloud-go/hcloud/volume.go | 2 +- 29 files changed, 381 insertions(+), 217 deletions(-) rename cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/{internal/instrumentation/metrics_test.go => architecture.go} (52%) diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/action.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/action.go index f6d25227115b..9502739db527 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/action.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/action.go @@ -197,7 +197,24 @@ func (c *ActionClient) AllWithOpts(ctx context.Context, opts ActionListOpts) ([] return allActions, nil } -// WatchOverallProgress watches several actions' progress until they complete with success or error. +// WatchOverallProgress watches several actions' progress until they complete +// with success or error. This watching happens in a goroutine and updates are +// provided through the two returned channels: +// +// - The first channel receives percentage updates of the progress, based on +// the number of completed versus total watched actions. The return value +// is an int between 0 and 100. +// - The second channel returned receives errors for actions that did not +// complete successfully, as well as any errors that happened while +// querying the API. +// +// By default the method keeps watching until all actions have finished +// processing. If you want to be able to cancel the method or configure a +// timeout, use the [context.Context]. Once the method has stopped watching, +// both returned channels are closed. +// +// WatchOverallProgress uses the [WithPollBackoffFunc] of the [Client] to wait +// until sending the next request. func (c *ActionClient) WatchOverallProgress(ctx context.Context, actions []*Action) (<-chan int, <-chan error) { errCh := make(chan error, len(actions)) progressCh := make(chan int) @@ -212,15 +229,15 @@ func (c *ActionClient) WatchOverallProgress(ctx context.Context, actions []*Acti watchIDs[action.ID] = struct{}{} } - ticker := time.NewTicker(c.client.pollInterval) - defer ticker.Stop() + retries := 0 + for { select { case <-ctx.Done(): errCh <- ctx.Err() return - case <-ticker.C: - break + case <-time.After(c.client.pollBackoffFunc(retries)): + retries++ } opts := ActionListOpts{} @@ -257,7 +274,24 @@ func (c *ActionClient) WatchOverallProgress(ctx context.Context, actions []*Acti return progressCh, errCh } -// WatchProgress watches one action's progress until it completes with success or error. +// WatchProgress watches one action's progress until it completes with success +// or error. This watching happens in a goroutine and updates are provided +// through the two returned channels: +// +// - The first channel receives percentage updates of the progress, based on +// the progress percentage indicated by the API. The return value is an int +// between 0 and 100. +// - The second channel receives any errors that happened while querying the +// API, as well as the error of the action if it did not complete +// successfully, or nil if it did. +// +// By default the method keeps watching until the action has finished +// processing. If you want to be able to cancel the method or configure a +// timeout, use the [context.Context]. Once the method has stopped watching, +// both returned channels are closed. +// +// WatchProgress uses the [WithPollBackoffFunc] of the [Client] to wait until +// sending the next request. func (c *ActionClient) WatchProgress(ctx context.Context, action *Action) (<-chan int, <-chan error) { errCh := make(chan error, 1) progressCh := make(chan int) @@ -266,16 +300,15 @@ func (c *ActionClient) WatchProgress(ctx context.Context, action *Action) (<-cha defer close(errCh) defer close(progressCh) - ticker := time.NewTicker(c.client.pollInterval) - defer ticker.Stop() + retries := 0 for { select { case <-ctx.Done(): errCh <- ctx.Err() return - case <-ticker.C: - break + case <-time.After(c.client.pollBackoffFunc(retries)): + retries++ } a, _, err := c.GetByID(ctx, action.ID) diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/internal/instrumentation/metrics_test.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/architecture.go similarity index 52% rename from cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/internal/instrumentation/metrics_test.go rename to cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/architecture.go index a8651f2e5234..1a2c173becbb 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/internal/instrumentation/metrics_test.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/architecture.go @@ -14,32 +14,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -package instrumentation +package hcloud -import "testing" +// Architecture specifies the architecture of the CPU. +type Architecture string -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) - } - }) - } -} +const ( + // ArchitectureX86 is the architecture for Intel/AMD x86 CPUs. + ArchitectureX86 Architecture = "x86" + + // ArchitectureARM is the architecture for ARM CPUs. + ArchitectureARM Architecture = "arm" +) diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/certificate.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/certificate.go index 67ce802641c4..c95b4d1f709e 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/certificate.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/certificate.go @@ -47,10 +47,10 @@ const ( CertificateStatusTypePending CertificateStatusType = "pending" CertificateStatusTypeFailed CertificateStatusType = "failed" - // only in issuance + // only in issuance. CertificateStatusTypeCompleted CertificateStatusType = "completed" - // only in renewal + // only in renewal. CertificateStatusTypeScheduled CertificateStatusType = "scheduled" CertificateStatusTypeUnavailable CertificateStatusType = "unavailable" ) diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/client.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/client.go index c36cf605cb9b..9ad4482ddce5 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/client.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/client.go @@ -23,7 +23,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "math" "net/http" "net/http/httputil" @@ -34,6 +33,7 @@ import ( "github.com/prometheus/client_golang/prometheus" "golang.org/x/net/http/httpguts" + "k8s.io/autoscaler/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/internal/instrumentation" "k8s.io/autoscaler/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema" ) @@ -59,7 +59,10 @@ func ConstantBackoff(d time.Duration) BackoffFunc { } // ExponentialBackoff returns a BackoffFunc which implements an exponential -// backoff using the formula: b^retries * d +// backoff. +// It uses the formula: +// +// b^retries * d func ExponentialBackoff(b float64, d time.Duration) BackoffFunc { return func(retries int) time.Duration { return time.Duration(math.Pow(b, float64(retries))) * d @@ -71,8 +74,8 @@ type Client struct { endpoint string token string tokenValid bool - pollInterval time.Duration backoffFunc BackoffFunc + pollBackoffFunc BackoffFunc httpClient *http.Client applicationName string applicationVersion string @@ -119,15 +122,31 @@ func WithToken(token string) ClientOption { } } -// WithPollInterval configures a Client to use the specified interval when polling -// from the API. +// WithPollInterval configures a Client to use the specified interval when +// polling from the API. +// +// Deprecated: Setting the poll interval is deprecated, you can now configure +// [WithPollBackoffFunc] with a [ConstantBackoff] to get the same results. To +// migrate your code, replace your usage like this: +// +// // before +// hcloud.WithPollInterval(2 * time.Second) +// // now +// hcloud.WithPollBackoffFunc(hcloud.ConstantBackoff(2 * time.Second)) func WithPollInterval(pollInterval time.Duration) ClientOption { + return WithPollBackoffFunc(ConstantBackoff(pollInterval)) +} + +// WithPollBackoffFunc configures a Client to use the specified backoff +// function when polling from the API. +func WithPollBackoffFunc(f BackoffFunc) ClientOption { return func(client *Client) { - client.pollInterval = pollInterval + client.backoffFunc = f } } // WithBackoffFunc configures a Client to use the specified backoff function. +// The backoff function is used for retrying HTTP requests. func WithBackoffFunc(f BackoffFunc) ClientOption { return func(client *Client) { client.backoffFunc = f @@ -169,11 +188,11 @@ func WithInstrumentation(registry *prometheus.Registry) ClientOption { // NewClient creates a new client. func NewClient(options ...ClientOption) *Client { client := &Client{ - endpoint: Endpoint, - tokenValid: true, - httpClient: &http.Client{}, - backoffFunc: ExponentialBackoff(2, 500*time.Millisecond), - pollInterval: 500 * time.Millisecond, + endpoint: Endpoint, + tokenValid: true, + httpClient: &http.Client{}, + backoffFunc: ExponentialBackoff(2, 500*time.Millisecond), + pollBackoffFunc: ConstantBackoff(500 * time.Millisecond), } for _, option := range options { @@ -238,7 +257,7 @@ func (c *Client) Do(r *http.Request, v interface{}) (*Response, error) { var body []byte var err error if r.ContentLength > 0 { - body, err = ioutil.ReadAll(r.Body) + body, err = io.ReadAll(r.Body) if err != nil { r.Body.Close() return nil, err @@ -247,7 +266,7 @@ func (c *Client) Do(r *http.Request, v interface{}) (*Response, error) { } for { if r.ContentLength > 0 { - r.Body = ioutil.NopCloser(bytes.NewReader(body)) + r.Body = io.NopCloser(bytes.NewReader(body)) } if c.debugWriter != nil { @@ -263,13 +282,13 @@ func (c *Client) Do(r *http.Request, v interface{}) (*Response, error) { return nil, err } response := &Response{Response: resp} - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { resp.Body.Close() return response, err } resp.Body.Close() - resp.Body = ioutil.NopCloser(bytes.NewReader(body)) + resp.Body = io.NopCloser(bytes.NewReader(body)) if c.debugWriter != nil { dumpResp, err := httputil.DumpResponse(resp, true) @@ -287,7 +306,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 isRetryable(err) { + } else if isConflict(err) { c.backoff(retries) retries++ continue @@ -306,12 +325,12 @@ func (c *Client) Do(r *http.Request, v interface{}) (*Response, error) { } } -func isRetryable(error error) bool { +func isConflict(error error) bool { err, ok := error.(Error) if !ok { return false } - return err.Code == ErrorCodeRateLimitExceeded || err.Code == ErrorCodeConflict + return err.Code == ErrorCodeConflict } func (c *Client) backoff(retries int) { diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/error.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/error.go index 6f983bede7d6..e77398ec7c6c 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/error.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/error.go @@ -44,14 +44,14 @@ const ( ErrorCodeResourceLocked ErrorCode = "resource_locked" // The resource is locked. The caller should contact support ErrorUnsupportedError ErrorCode = "unsupported_error" // The given resource does not support this - // Server related error codes + // 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 ErrorCodePlacementError ErrorCode = "placement_error" // An error during the placement occurred ErrorCodeServerAlreadyAttached ErrorCode = "server_already_attached" // The server is already attached to the resource - // Load Balancer related error codes + // 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 @@ -62,16 +62,16 @@ const ( ErrorCodeTargetsWithoutUsePrivateIP ErrorCode = "targets_without_use_private_ip" // The Load Balancer has targets that use the public IP instead of the private IP ErrorCodeLoadBalancerNotAttachedToNetwork ErrorCode = "load_balancer_not_attached_to_network" // The Load Balancer is not attached to a network - // Network related error codes + // 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 + // 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 + // 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 @@ -79,7 +79,7 @@ const ( 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 + // 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 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 8be0915ef5ac..73074805efc8 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/floating_ip.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/floating_ip.go @@ -48,7 +48,7 @@ type FloatingIP struct { } // DNSPtrForIP returns the reverse DNS pointer of the IP address. -// Deprecated: Use GetDNSPtrForIP instead +// Deprecated: Use GetDNSPtrForIP instead. func (f *FloatingIP) DNSPtrForIP(ip net.IP) string { return f.DNSPtr[ip.String()] } @@ -257,10 +257,10 @@ func (c *FloatingIPClient) Create(ctx context.Context, opts FloatingIPCreateOpts Name: opts.Name, } if opts.HomeLocation != nil { - reqBody.HomeLocation = String(opts.HomeLocation.Name) + reqBody.HomeLocation = Ptr(opts.HomeLocation.Name) } if opts.Server != nil { - reqBody.Server = Int(opts.Server.ID) + reqBody.Server = Ptr(opts.Server.ID) } if opts.Labels != nil { reqBody.Labels = &opts.Labels diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/hcloud.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/hcloud.go index 332ff7496742..331a8a89c5af 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.35.0" +const Version = "1.42.0" // x-release-please-version diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/helper.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/helper.go index 537607883e46..93af1ee7a409 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/helper.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/helper.go @@ -18,17 +18,27 @@ package hcloud import "time" +// Ptr returns a pointer to p. +func Ptr[T any](p T) *T { + return &p +} + // String returns a pointer to the passed string s. -func String(s string) *string { return &s } +// +// Deprecated: Use [Ptr] instead. +func String(s string) *string { return Ptr(s) } // Int returns a pointer to the passed integer i. -func Int(i int) *int { return &i } +// +// Deprecated: Use [Ptr] instead. +func Int(i int) *int { return Ptr(i) } // Bool returns a pointer to the passed bool b. -func Bool(b bool) *bool { return &b } +// +// Deprecated: Use [Ptr] instead. +func Bool(b bool) *bool { return Ptr(b) } // Duration returns a pointer to the passed time.Duration d. -func Duration(d time.Duration) *time.Duration { return &d } - -func intSlice(is []int) *[]int { return &is } -func stringSlice(ss []string) *[]string { return &ss } +// +// Deprecated: Use [Ptr] instead. +func Duration(d time.Duration) *time.Duration { return Ptr(d) } diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/image.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/image.go index 920f094f8a16..0e7593c59975 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/image.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/image.go @@ -42,8 +42,9 @@ type Image struct { BoundTo *Server RapidDeploy bool - OSFlavor string - OSVersion string + OSFlavor string + OSVersion string + Architecture Architecture Protection ImageProtection Deprecated time.Time // The zero value denotes the image is not deprecated. @@ -114,6 +115,8 @@ func (c *ImageClient) GetByID(ctx context.Context, id int) (*Image, *Response, e } // GetByName retrieves an image by its name. If the image does not exist, nil is returned. +// +// Deprecated: Use [ImageClient.GetByNameAndArchitecture] instead. func (c *ImageClient) GetByName(ctx context.Context, name string) (*Image, *Response, error) { if name == "" { return nil, nil, nil @@ -125,15 +128,44 @@ func (c *ImageClient) GetByName(ctx context.Context, name string) (*Image, *Resp return images[0], response, err } +// GetByNameAndArchitecture retrieves an image by its name and architecture. If the image does not exist, +// nil is returned. +// In contrast to [ImageClient.Get], this method also returns deprecated images. Depending on your needs you should +// check for this in your calling method. +func (c *ImageClient) GetByNameAndArchitecture(ctx context.Context, name string, architecture Architecture) (*Image, *Response, error) { + if name == "" { + return nil, nil, nil + } + images, response, err := c.List(ctx, ImageListOpts{Name: name, Architecture: []Architecture{architecture}, IncludeDeprecated: true}) + if len(images) == 0 { + return nil, response, err + } + return images[0], response, err +} + // Get retrieves an image by its ID if the input can be parsed as an integer, otherwise it // retrieves an image by its name. If the image does not exist, nil is returned. +// +// Deprecated: Use [ImageClient.GetForArchitecture] instead. func (c *ImageClient) Get(ctx context.Context, idOrName string) (*Image, *Response, error) { if id, err := strconv.Atoi(idOrName); err == nil { - return c.GetByID(ctx, int(id)) + return c.GetByID(ctx, id) } return c.GetByName(ctx, idOrName) } +// GetForArchitecture retrieves an image by its ID if the input can be parsed as an integer, otherwise it +// retrieves an image by its name and architecture. If the image does not exist, nil is returned. +// +// In contrast to [ImageClient.Get], this method also returns deprecated images. Depending on your needs you should +// check for this in your calling method. +func (c *ImageClient) GetForArchitecture(ctx context.Context, idOrName string, architecture Architecture) (*Image, *Response, error) { + if id, err := strconv.Atoi(idOrName); err == nil { + return c.GetByID(ctx, id) + } + return c.GetByNameAndArchitecture(ctx, idOrName, architecture) +} + // ImageListOpts specifies options for listing images. type ImageListOpts struct { ListOpts @@ -143,6 +175,7 @@ type ImageListOpts struct { Sort []string Status []ImageStatus IncludeDeprecated bool + Architecture []Architecture } func (l ImageListOpts) values() url.Values { @@ -165,6 +198,9 @@ func (l ImageListOpts) values() url.Values { for _, status := range l.Status { vals.Add("status", string(status)) } + for _, arch := range l.Architecture { + vals.Add("architecture", string(arch)) + } return vals } @@ -238,7 +274,7 @@ func (c *ImageClient) Update(ctx context.Context, image *Image, opts ImageUpdate Description: opts.Description, } if opts.Type != "" { - reqBody.Type = String(string(opts.Type)) + reqBody.Type = Ptr(string(opts.Type)) } if opts.Labels != nil { reqBody.Labels = &opts.Labels 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 844ba87083fc..14af92844aed 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 @@ -32,7 +32,7 @@ type Instrumenter struct { instrumentationRegistry *prometheus.Registry } -// New creates a new Instrumenter. The subsystemIdentifier will be used as part of the metric names (e.g. hcloud__requests_total) +// 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} } @@ -74,8 +74,10 @@ func (i *Instrumenter) InstrumentedRoundTripper() http.RoundTripper { // 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 +// +// /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) @@ -83,6 +85,7 @@ func (i *Instrumenter) instrumentRoundTripperEndpoint(counter *prometheus.Counte statusCode := strconv.Itoa(resp.StatusCode) counter.WithLabelValues(statusCode, strings.ToLower(resp.Request.Method), preparePathForLabel(resp.Request.URL.Path)).Inc() } + return resp, err } } diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/iso.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/iso.go index 1fece1e559ff..a7d0fec23950 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/iso.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/iso.go @@ -28,14 +28,15 @@ import ( // ISO represents an ISO image in the Hetzner Cloud. type ISO struct { - ID int - Name string - Description string - Type ISOType - Deprecated time.Time + ID int + Name string + Description string + Type ISOType + Architecture *Architecture + Deprecated time.Time } -// IsDeprecated returns true if the ISO is deprecated +// IsDeprecated returns true if the ISO is deprecated. func (iso *ISO) IsDeprecated() bool { return !iso.Deprecated.IsZero() } @@ -99,6 +100,12 @@ type ISOListOpts struct { ListOpts Name string Sort []string + // Architecture filters the ISOs by Architecture. Note that custom ISOs do not have any architecture set, and you + // must use IncludeWildcardArchitecture to include them. + Architecture []Architecture + // IncludeWildcardArchitecture must be set to also return custom ISOs that have no architecture set, if you are + // also setting the Architecture field. + IncludeWildcardArchitecture bool } func (l ISOListOpts) values() url.Values { @@ -109,6 +116,12 @@ func (l ISOListOpts) values() url.Values { for _, sort := range l.Sort { vals.Add("sort", sort) } + for _, arch := range l.Architecture { + vals.Add("architecture", string(arch)) + } + if l.IncludeWildcardArchitecture { + vals.Add("include_architecture_wildcard", "true") + } return vals } diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/labels.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/labels.go index a8c3b4fe81ab..e22000a8fa67 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/labels.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/labels.go @@ -7,7 +7,7 @@ import ( var keyRegexp = regexp.MustCompile( `^([a-z0-9A-Z]((?:[\-_.]|[a-z0-9A-Z]){0,253}[a-z0-9A-Z])?/)?[a-z0-9A-Z]((?:[\-_.]|[a-z0-9A-Z]|){0,62}[a-z0-9A-Z])?$`) -var valueRegexp = regexp.MustCompile(`^([a-z0-9A-Z](?:[\-_.]|[a-z0-9A-Z]){0,62})?[a-z0-9A-Z]$`) +var valueRegexp = regexp.MustCompile(`^(([a-z0-9A-Z](?:[\-_.]|[a-z0-9A-Z]){0,62})?[a-z0-9A-Z]$|$)`) func ValidateResourceLabels(labels map[string]interface{}) (bool, error) { for k, v := range labels { 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 e17d1aeef411..24f4e8852bd0 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/load_balancer.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/load_balancer.go @@ -507,7 +507,6 @@ type LoadBalancerCreateResult struct { func (c *LoadBalancerClient) Create(ctx context.Context, opts LoadBalancerCreateOpts) (LoadBalancerCreateResult, *Response, error) { reqBody := loadBalancerCreateOptsToSchema(opts) reqBodyData, err := json.Marshal(reqBody) - if err != nil { return LoadBalancerCreateResult{}, nil, err } @@ -881,7 +880,7 @@ func (c *LoadBalancerClient) AttachToNetwork(ctx context.Context, loadBalancer * Network: opts.Network.ID, } if opts.IP != nil { - reqBody.IP = String(opts.IP.String()) + reqBody.IP = Ptr(opts.IP.String()) } reqBodyData, err := json.Marshal(reqBody) if err != nil { 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 f3f66531c26d..ff835f5d393f 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/metadata/client.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/metadata/client.go @@ -18,7 +18,7 @@ package metadata import ( "fmt" - "io/ioutil" + "io" "net" "net/http" "strconv" @@ -90,7 +90,7 @@ func (c *Client) get(path string) (string, error) { return "", err } defer resp.Body.Close() - bodyBytes, err := ioutil.ReadAll(resp.Body) + bodyBytes, err := io.ReadAll(resp.Body) if err != nil { return "", err } @@ -102,7 +102,7 @@ func (c *Client) get(path string) (string, error) { } // 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 +// 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 { @@ -114,12 +114,12 @@ func (c *Client) IsHcloudServer() bool { return false } -// Hostname returns the hostname of the server that did the request to the Metadata server +// 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 +// 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 { @@ -128,7 +128,7 @@ func (c *Client) InstanceID() (int, error) { return strconv.Atoi(resp) } -// PublicIPv4 returns the Public IPv4 of the server that did the request to the Metadata server +// 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 { @@ -137,18 +137,18 @@ func (c *Client) PublicIPv4() (net.IP, error) { return net.ParseIP(resp), nil } -// Region returns the Network Zone of the server that did the request to the Metadata server +// 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 +// 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) +// 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 e95c92c6141d..77a8c65ac614 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/network.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/network.go @@ -37,6 +37,7 @@ type NetworkZone string const ( NetworkZoneEUCentral NetworkZone = "eu-central" NetworkZoneUSEast NetworkZone = "us-east" + NetworkZoneUSWest NetworkZone = "us-west" ) // 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 index b8c7fff361ff..508db93e6271 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/placement_group.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/placement_group.go @@ -39,11 +39,11 @@ type PlacementGroup struct { Type PlacementGroupType } -// PlacementGroupType specifies the type of a Placement Group +// PlacementGroupType specifies the type of a Placement Group. type PlacementGroupType string const ( - // PlacementGroupTypeSpread spreads all servers in the group on different vhosts + // PlacementGroupTypeSpread spreads all servers in the group on different vhosts. PlacementGroupTypeSpread PlacementGroupType = "spread" ) @@ -174,7 +174,7 @@ type PlacementGroupCreateOpts struct { Type PlacementGroupType } -// Validate checks if options are valid +// Validate checks if options are valid. func (o PlacementGroupCreateOpts) Validate() error { if o.Name == "" { return errors.New("missing name") @@ -188,7 +188,7 @@ type PlacementGroupCreateResult struct { Action *Action } -// Create creates a new PlacementGroup +// 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 diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/pricing.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/pricing.go index e047c38ccc03..f08b02435dc2 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/pricing.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/pricing.go @@ -72,12 +72,13 @@ type FloatingIPTypePricing struct { // PrimaryIPTypePricing defines the schema of pricing information for a primary IP // type at a datacenter. type PrimaryIPTypePricing struct { - Datacenter string + Datacenter string // Deprecated: the API does not return pricing for the individual DCs anymore + Location string Hourly PrimaryIPPrice Monthly PrimaryIPPrice } -// PrimaryIPTypePricing provides pricing information for PrimaryIPs +// PrimaryIPTypePricing provides pricing information for PrimaryIPs. type PrimaryIPPricing struct { Type string Pricings []PrimaryIPTypePricing 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 c519001e1824..aa34cff651c6 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/primary_ip.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/primary_ip.go @@ -13,7 +13,7 @@ import ( "k8s.io/autoscaler/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema" ) -// PrimaryIP defines a Primary IP +// PrimaryIP defines a Primary IP. type PrimaryIP struct { ID int IP net.IP @@ -90,12 +90,6 @@ type PrimaryIPUpdateOpts struct { Name string `json:"name,omitempty"` } -// PrimaryIPUpdateResult defines the response -// when updating a Primary IP. -type PrimaryIPUpdateResult struct { - PrimaryIP PrimaryIP `json:"primary_ip"` -} - // PrimaryIPAssignOpts defines the request to // assign a Primary IP to an assignee (usually a server). type PrimaryIPAssignOpts struct { @@ -111,7 +105,7 @@ type PrimaryIPAssignResult struct { } // PrimaryIPChangeDNSPtrOpts defines the request to -// change a DNS PTR entry from a Primary IP +// change a DNS PTR entry from a Primary IP. type PrimaryIPChangeDNSPtrOpts struct { ID int DNSPtr string `json:"dns_ptr"` @@ -125,19 +119,19 @@ type PrimaryIPChangeDNSPtrResult struct { } // PrimaryIPChangeProtectionOpts defines the request to -// change protection configuration of a Primary IP +// change protection configuration of a Primary IP. type PrimaryIPChangeProtectionOpts struct { ID int Delete bool `json:"delete"` } // PrimaryIPChangeProtectionResult defines the response -// when changing a protection of a PrimaryIP +// when changing a protection of a PrimaryIP. type PrimaryIPChangeProtectionResult struct { Action schema.Action `json:"action"` } -// PrimaryIPClient is a client for the Primary IP API +// PrimaryIPClient is a client for the Primary IP API. type PrimaryIPClient struct { client *Client } @@ -240,10 +234,12 @@ func (c *PrimaryIPClient) List(ctx context.Context, opts PrimaryIPListOpts) ([]* // All returns all Primary IPs. func (c *PrimaryIPClient) All(ctx context.Context) ([]*PrimaryIP, error) { - allPrimaryIPs := []*PrimaryIP{} + return c.AllWithOpts(ctx, PrimaryIPListOpts{ListOpts: ListOpts{PerPage: 50}}) +} - opts := PrimaryIPListOpts{} - opts.PerPage = 50 +// AllWithOpts returns all Primary IPs for the given options. +func (c *PrimaryIPClient) AllWithOpts(ctx context.Context, opts PrimaryIPListOpts) ([]*PrimaryIP, error) { + var allPrimaryIPs []*PrimaryIP err := c.client.all(func(page int) (*Response, error) { opts.Page = page @@ -311,15 +307,15 @@ func (c *PrimaryIPClient) Update(ctx context.Context, primaryIP *PrimaryIP, reqB return nil, nil, err } - respBody := PrimaryIPUpdateResult{} + var respBody schema.PrimaryIPUpdateResult resp, err := c.client.Do(req, &respBody) if err != nil { return nil, resp, err } - return &respBody.PrimaryIP, resp, nil + return PrimaryIPFromSchema(respBody.PrimaryIP), resp, nil } -// Assign a Primary IP to a resource +// Assign a Primary IP to a resource. func (c *PrimaryIPClient) Assign(ctx context.Context, opts PrimaryIPAssignOpts) (*Action, *Response, error) { reqBodyData, err := json.Marshal(opts) if err != nil { @@ -340,7 +336,7 @@ func (c *PrimaryIPClient) Assign(ctx context.Context, opts PrimaryIPAssignOpts) return ActionFromSchema(respBody.Action), resp, nil } -// Unassign a Primary IP from a resource +// Unassign a Primary IP from a resource. func (c *PrimaryIPClient) Unassign(ctx context.Context, id int) (*Action, *Response, error) { path := fmt.Sprintf("/primary_ips/%d/actions/unassign", id) req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader([]byte{})) @@ -356,7 +352,7 @@ func (c *PrimaryIPClient) Unassign(ctx context.Context, id int) (*Action, *Respo return ActionFromSchema(respBody.Action), resp, nil } -// ChangeDNSPtr Change the reverse DNS from a Primary IP +// ChangeDNSPtr Change the reverse DNS from a Primary IP. func (c *PrimaryIPClient) ChangeDNSPtr(ctx context.Context, opts PrimaryIPChangeDNSPtrOpts) (*Action, *Response, error) { reqBodyData, err := json.Marshal(opts) if err != nil { diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/rdns.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/rdns.go index d7cbf0b0db6b..daf9b4c1c8f9 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/rdns.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/rdns.go @@ -23,7 +23,7 @@ import ( ) // RDNSSupporter defines functions to change and lookup reverse dns entries. -// currently implemented by Server, FloatingIP and LoadBalancer +// 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. diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema.go index ba528204b655..7f45cd294ce9 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema.go @@ -134,13 +134,17 @@ func PrimaryIPFromSchema(s schema.PrimaryIP) *PrimaryIP { // ISOFromSchema converts a schema.ISO to an ISO. func ISOFromSchema(s schema.ISO) *ISO { - return &ISO{ + iso := &ISO{ ID: s.ID, Name: s.Name, Description: s.Description, Type: ISOType(s.Type), Deprecated: s.Deprecated, } + if s.Architecture != nil { + iso.Architecture = Ptr(Architecture(*s.Architecture)) + } + return iso } // LocationFromSchema converts a schema.Location to a Location. @@ -290,14 +294,15 @@ func ServerPrivateNetFromSchema(s schema.ServerPrivateNet) ServerPrivateNet { // ServerTypeFromSchema converts a schema.ServerType to a ServerType. func ServerTypeFromSchema(s schema.ServerType) *ServerType { st := &ServerType{ - ID: s.ID, - Name: s.Name, - Description: s.Description, - Cores: s.Cores, - Memory: s.Memory, - Disk: s.Disk, - StorageType: StorageType(s.StorageType), - CPUType: CPUType(s.CPUType), + ID: s.ID, + Name: s.Name, + Description: s.Description, + Cores: s.Cores, + Memory: s.Memory, + Disk: s.Disk, + StorageType: StorageType(s.StorageType), + CPUType: CPUType(s.CPUType), + Architecture: Architecture(s.Architecture), } for _, price := range s.Prices { st.Pricings = append(st.Pricings, ServerTypeLocationPricing{ @@ -334,14 +339,15 @@ func SSHKeyFromSchema(s schema.SSHKey) *SSHKey { // ImageFromSchema converts a schema.Image to an Image. func ImageFromSchema(s schema.Image) *Image { i := &Image{ - ID: s.ID, - Type: ImageType(s.Type), - Status: ImageStatus(s.Status), - Description: s.Description, - DiskSize: s.DiskSize, - Created: s.Created, - RapidDeploy: s.RapidDeploy, - OSFlavor: s.OSFlavor, + ID: s.ID, + Type: ImageType(s.Type), + Status: ImageStatus(s.Status), + Description: s.Description, + DiskSize: s.DiskSize, + Created: s.Created, + RapidDeploy: s.RapidDeploy, + OSFlavor: s.OSFlavor, + Architecture: Architecture(s.Architecture), Protection: ImageProtection{ Delete: s.Protection.Delete, }, @@ -737,7 +743,7 @@ func PricingFromSchema(s schema.Pricing) Pricing { var pricings []PrimaryIPTypePricing for _, price := range primaryIPType.Prices { p := PrimaryIPTypePricing{ - Datacenter: price.Datacenter, + Location: price.Location, Monthly: PrimaryIPPrice{ Net: price.PriceMonthly.Net, Gross: price.PriceMonthly.Gross, @@ -897,19 +903,19 @@ func loadBalancerCreateOptsToSchema(opts LoadBalancerCreateOpts) schema.LoadBala } if opts.Location != nil { if opts.Location.ID != 0 { - req.Location = String(strconv.Itoa(opts.Location.ID)) + req.Location = Ptr(strconv.Itoa(opts.Location.ID)) } else { - req.Location = String(opts.Location.Name) + req.Location = Ptr(opts.Location.Name) } } if opts.NetworkZone != "" { - req.NetworkZone = String(string(opts.NetworkZone)) + req.NetworkZone = Ptr(string(opts.NetworkZone)) } if opts.Labels != nil { req.Labels = &opts.Labels } if opts.Network != nil { - req.Network = Int(opts.Network.ID) + req.Network = Ptr(opts.Network.ID) } for _, target := range opts.Targets { schemaTarget := schema.LoadBalancerCreateRequestTarget{ @@ -943,7 +949,7 @@ func loadBalancerCreateOptsToSchema(opts LoadBalancerCreateOpts) schema.LoadBala } if service.HTTP.CookieLifetime != nil { if sec := service.HTTP.CookieLifetime.Seconds(); sec != 0 { - schemaService.HTTP.CookieLifetime = Int(int(sec)) + schemaService.HTTP.CookieLifetime = Ptr(int(sec)) } } if service.HTTP.Certificates != nil { @@ -961,10 +967,10 @@ func loadBalancerCreateOptsToSchema(opts LoadBalancerCreateOpts) schema.LoadBala Retries: service.HealthCheck.Retries, } if service.HealthCheck.Interval != nil { - schemaHealthCheck.Interval = Int(int(service.HealthCheck.Interval.Seconds())) + schemaHealthCheck.Interval = Ptr(int(service.HealthCheck.Interval.Seconds())) } if service.HealthCheck.Timeout != nil { - schemaHealthCheck.Timeout = Int(int(service.HealthCheck.Timeout.Seconds())) + schemaHealthCheck.Timeout = Ptr(int(service.HealthCheck.Timeout.Seconds())) } if service.HealthCheck.HTTP != nil { schemaHealthCheckHTTP := &schema.LoadBalancerCreateRequestServiceHealthCheckHTTP{ @@ -999,7 +1005,7 @@ func loadBalancerAddServiceOptsToSchema(opts LoadBalancerAddServiceOpts) schema. StickySessions: opts.HTTP.StickySessions, } if opts.HTTP.CookieLifetime != nil { - req.HTTP.CookieLifetime = Int(int(opts.HTTP.CookieLifetime.Seconds())) + req.HTTP.CookieLifetime = Ptr(int(opts.HTTP.CookieLifetime.Seconds())) } if opts.HTTP.Certificates != nil { certificates := []int{} @@ -1016,10 +1022,10 @@ func loadBalancerAddServiceOptsToSchema(opts LoadBalancerAddServiceOpts) schema. Retries: opts.HealthCheck.Retries, } if opts.HealthCheck.Interval != nil { - req.HealthCheck.Interval = Int(int(opts.HealthCheck.Interval.Seconds())) + req.HealthCheck.Interval = Ptr(int(opts.HealthCheck.Interval.Seconds())) } if opts.HealthCheck.Timeout != nil { - req.HealthCheck.Timeout = Int(int(opts.HealthCheck.Timeout.Seconds())) + req.HealthCheck.Timeout = Ptr(int(opts.HealthCheck.Timeout.Seconds())) } if opts.HealthCheck.HTTP != nil { req.HealthCheck.HTTP = &schema.LoadBalancerActionAddServiceRequestHealthCheckHTTP{ @@ -1042,7 +1048,7 @@ func loadBalancerUpdateServiceOptsToSchema(opts LoadBalancerUpdateServiceOpts) s Proxyprotocol: opts.Proxyprotocol, } if opts.Protocol != "" { - req.Protocol = String(string(opts.Protocol)) + req.Protocol = Ptr(string(opts.Protocol)) } if opts.HTTP != nil { req.HTTP = &schema.LoadBalancerActionUpdateServiceRequestHTTP{ @@ -1051,7 +1057,7 @@ func loadBalancerUpdateServiceOptsToSchema(opts LoadBalancerUpdateServiceOpts) s StickySessions: opts.HTTP.StickySessions, } if opts.HTTP.CookieLifetime != nil { - req.HTTP.CookieLifetime = Int(int(opts.HTTP.CookieLifetime.Seconds())) + req.HTTP.CookieLifetime = Ptr(int(opts.HTTP.CookieLifetime.Seconds())) } if opts.HTTP.Certificates != nil { certificates := []int{} @@ -1067,13 +1073,13 @@ func loadBalancerUpdateServiceOptsToSchema(opts LoadBalancerUpdateServiceOpts) s Retries: opts.HealthCheck.Retries, } if opts.HealthCheck.Interval != nil { - req.HealthCheck.Interval = Int(int(opts.HealthCheck.Interval.Seconds())) + req.HealthCheck.Interval = Ptr(int(opts.HealthCheck.Interval.Seconds())) } if opts.HealthCheck.Timeout != nil { - req.HealthCheck.Timeout = Int(int(opts.HealthCheck.Timeout.Seconds())) + req.HealthCheck.Timeout = Ptr(int(opts.HealthCheck.Timeout.Seconds())) } if opts.HealthCheck.Protocol != "" { - req.HealthCheck.Protocol = String(string(opts.HealthCheck.Protocol)) + req.HealthCheck.Protocol = Ptr(string(opts.HealthCheck.Protocol)) } if opts.HealthCheck.HTTP != nil { req.HealthCheck.HTTP = &schema.LoadBalancerActionUpdateServiceRequestHealthCheckHTTP{ diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/image.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/image.go index 99d1e7aed197..0c773787b8b3 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/image.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/image.go @@ -20,23 +20,24 @@ import "time" // Image defines the schema of an image. type Image struct { - ID int `json:"id"` - Status string `json:"status"` - Type string `json:"type"` - Name *string `json:"name"` - Description string `json:"description"` - ImageSize *float32 `json:"image_size"` - DiskSize float32 `json:"disk_size"` - Created time.Time `json:"created"` - CreatedFrom *ImageCreatedFrom `json:"created_from"` - BoundTo *int `json:"bound_to"` - OSFlavor string `json:"os_flavor"` - OSVersion *string `json:"os_version"` - RapidDeploy bool `json:"rapid_deploy"` - Protection ImageProtection `json:"protection"` - Deprecated time.Time `json:"deprecated"` - Deleted time.Time `json:"deleted"` - Labels map[string]string `json:"labels"` + ID int `json:"id"` + Status string `json:"status"` + Type string `json:"type"` + Name *string `json:"name"` + Description string `json:"description"` + ImageSize *float32 `json:"image_size"` + DiskSize float32 `json:"disk_size"` + Created time.Time `json:"created"` + CreatedFrom *ImageCreatedFrom `json:"created_from"` + BoundTo *int `json:"bound_to"` + OSFlavor string `json:"os_flavor"` + OSVersion *string `json:"os_version"` + Architecture string `json:"architecture"` + RapidDeploy bool `json:"rapid_deploy"` + Protection ImageProtection `json:"protection"` + Deprecated time.Time `json:"deprecated"` + Deleted time.Time `json:"deleted"` + Labels map[string]string `json:"labels"` } // ImageProtection represents the protection level of a image. 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 764f0fc26391..943865712f81 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/iso.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/iso.go @@ -20,11 +20,12 @@ import "time" // ISO defines the schema of an ISO image. type ISO struct { - ID int `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - Type string `json:"type"` - Deprecated time.Time `json:"deprecated"` + ID int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Type string `json:"type"` + Architecture *string `json:"architecture"` + Deprecated time.Time `json:"deprecated"` } // ISOGetResponse defines the schema of the response when retrieving a single ISO. 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 76c08e29b3e2..8fc50eb32e84 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/pricing.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/pricing.go @@ -110,15 +110,16 @@ type PricingGetResponse struct { Pricing Pricing `json:"pricing"` } -// PricingPrimaryIPTypePrice defines the schema of pricing information for a primary IP +// PricingPrimaryIPTypePrice defines the schema of pricing information for a primary IP. // type at a datacenter. type PricingPrimaryIPTypePrice struct { - Datacenter string `json:"datacenter"` + Datacenter string `json:"datacenter"` // Deprecated: the API does not return pricing for the individual DCs anymore + Location string `json:"location"` PriceHourly Price `json:"price_hourly"` PriceMonthly Price `json:"price_monthly"` } -// PricingPrimaryIP define the schema of pricing information for a primary IP at a datacenter +// PricingPrimaryIP define the schema of pricing information for a primary IP at a datacenter. type PricingPrimaryIP struct { Type string `json:"type"` Prices []PricingPrimaryIPTypePrice `json:"prices"` diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/primary_ip.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/primary_ip.go index b21e28b4a8b4..d232a732d195 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/primary_ip.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/primary_ip.go @@ -2,7 +2,7 @@ package schema import "time" -// PrimaryIP defines a Primary IP +// PrimaryIP defines a Primary IP. type PrimaryIP struct { ID int `json:"id"` IP string `json:"ip"` @@ -47,3 +47,9 @@ type PrimaryIPGetResult struct { type PrimaryIPListResult struct { PrimaryIPs []PrimaryIP `json:"primary_ips"` } + +// PrimaryIPUpdateResult defines the response +// when updating a Primary IP. +type PrimaryIPUpdateResult struct { + PrimaryIP PrimaryIP `json:"primary_ip"` +} 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 4fc11a3ca02e..3949ebfbe03e 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/server.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/server.go @@ -152,6 +152,12 @@ type ServerCreateResponse struct { NextActions []Action `json:"next_actions"` } +// ServerDeleteResponse defines the schema of the response when +// deleting a server. +type ServerDeleteResponse struct { + Action Action `json:"action"` +} + // ServerUpdateRequest defines the schema of the request to update a server. type ServerUpdateRequest struct { Name string `json:"name,omitempty"` @@ -272,7 +278,8 @@ type ServerActionRebuildRequest struct { // ServerActionRebuildResponse defines the schema of the response when // creating a rebuild server action. type ServerActionRebuildResponse struct { - Action Action `json:"action"` + Action Action `json:"action"` + RootPassword *string `json:"root_password"` } // ServerActionAttachISORequest defines the schema for the request to diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/server_type.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/server_type.go index 75be7ad7bba0..e125d905f65e 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/server_type.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/server_type.go @@ -18,15 +18,16 @@ package schema // ServerType defines the schema of a server type. type ServerType struct { - ID int `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - Cores int `json:"cores"` - Memory float32 `json:"memory"` - Disk int `json:"disk"` - StorageType string `json:"storage_type"` - CPUType string `json:"cpu_type"` - Prices []PricingServerTypePrice `json:"prices"` + ID int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Cores int `json:"cores"` + Memory float32 `json:"memory"` + Disk int `json:"disk"` + StorageType string `json:"storage_type"` + CPUType string `json:"cpu_type"` + Architecture string `json:"architecture"` + Prices []PricingServerTypePrice `json:"prices"` } // ServerTypeListResponse defines the schema of the response when diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/server.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/server.go index f06f7974565f..40b228afab14 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/server.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/server.go @@ -364,8 +364,9 @@ func (o ServerCreateOpts) Validate() error { return errors.New("location and datacenter are mutually exclusive") } if o.PublicNet != nil { - if !o.PublicNet.EnableIPv4 && !o.PublicNet.EnableIPv6 && len(o.Networks) == 0 { - return errors.New("missing networks when EnableIPv4 and EnableIPv6 is false") + if !o.PublicNet.EnableIPv4 && !o.PublicNet.EnableIPv6 && + len(o.Networks) == 0 && (o.StartAfterCreate == nil || *o.StartAfterCreate) { + return errors.New("missing networks or StartAfterCreate == false when EnableIPv4 and EnableIPv6 is false") } } return nil @@ -473,13 +474,35 @@ func (c *ServerClient) Create(ctx context.Context, opts ServerCreateOpts) (Serve return result, resp, nil } +// ServerDeleteResult is the result of a delete server call. +type ServerDeleteResult struct { + Action *Action +} + // Delete deletes a server. +// +// Deprecated: Use [ServerClient.DeleteWithResult] instead. func (c *ServerClient) Delete(ctx context.Context, server *Server) (*Response, error) { + _, resp, err := c.DeleteWithResult(ctx, server) + return resp, err +} + +// DeleteWithResult deletes a server and returns the parsed response containing the action. +func (c *ServerClient) DeleteWithResult(ctx context.Context, server *Server) (*ServerDeleteResult, *Response, error) { req, err := c.client.NewRequest(ctx, "DELETE", fmt.Sprintf("/servers/%d", server.ID), nil) if err != nil { - return nil, err + return &ServerDeleteResult{}, nil, err } - return c.client.Do(req, nil) + + var respBody schema.ServerDeleteResponse + resp, err := c.client.Do(req, &respBody) + if err != nil { + return &ServerDeleteResult{}, resp, err + } + + return &ServerDeleteResult{ + Action: ActionFromSchema(respBody.Action), + }, resp, nil } // ServerUpdateOpts specifies options for updating a server. @@ -658,7 +681,7 @@ func (c *ServerClient) CreateImage(ctx context.Context, server *Server, opts *Se reqBody.Description = opts.Description } if opts.Type != "" { - reqBody.Type = String(string(opts.Type)) + reqBody.Type = Ptr(string(opts.Type)) } if opts.Labels != nil { reqBody.Labels = &opts.Labels @@ -701,7 +724,7 @@ type ServerEnableRescueResult struct { // EnableRescue enables rescue mode for a server. func (c *ServerClient) EnableRescue(ctx context.Context, server *Server, opts ServerEnableRescueOpts) (ServerEnableRescueResult, *Response, error) { reqBody := schema.ServerActionEnableRescueRequest{ - Type: String(string(opts.Type)), + Type: Ptr(string(opts.Type)), } for _, sshKey := range opts.SSHKeys { reqBody.SSHKeys = append(reqBody.SSHKeys, sshKey.ID) @@ -750,8 +773,23 @@ type ServerRebuildOpts struct { Image *Image } +// ServerRebuildResult is the result of a create server call. +type ServerRebuildResult struct { + Action *Action + RootPassword string +} + // Rebuild rebuilds a server. +// +// Deprecated: Use [ServerClient.RebuildWithResult] instead. func (c *ServerClient) Rebuild(ctx context.Context, server *Server, opts ServerRebuildOpts) (*Action, *Response, error) { + result, resp, err := c.RebuildWithResult(ctx, server, opts) + + return result.Action, resp, err +} + +// RebuildWithResult rebuilds a server. +func (c *ServerClient) RebuildWithResult(ctx context.Context, server *Server, opts ServerRebuildOpts) (ServerRebuildResult, *Response, error) { reqBody := schema.ServerActionRebuildRequest{} if opts.Image.ID != 0 { reqBody.Image = opts.Image.ID @@ -760,21 +798,29 @@ func (c *ServerClient) Rebuild(ctx context.Context, server *Server, opts ServerR } reqBodyData, err := json.Marshal(reqBody) if err != nil { - return nil, nil, err + return ServerRebuildResult{}, nil, err } path := fmt.Sprintf("/servers/%d/actions/rebuild", server.ID) req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) if err != nil { - return nil, nil, err + return ServerRebuildResult{}, nil, err } respBody := schema.ServerActionRebuildResponse{} resp, err := c.client.Do(req, &respBody) if err != nil { - return nil, resp, err + return ServerRebuildResult{}, resp, err } - return ActionFromSchema(respBody.Action), resp, nil + + result := ServerRebuildResult{ + Action: ActionFromSchema(respBody.Action), + } + if respBody.RootPassword != nil { + result.RootPassword = *respBody.RootPassword + } + + return result, resp, nil } // AttachISO attaches an ISO to a server. @@ -826,7 +872,7 @@ func (c *ServerClient) DetachISO(ctx context.Context, server *Server) (*Action, func (c *ServerClient) EnableBackup(ctx context.Context, server *Server, window string) (*Action, *Response, error) { reqBody := schema.ServerActionEnableBackupRequest{} if window != "" { - reqBody.BackupWindow = String(window) + reqBody.BackupWindow = Ptr(window) } reqBodyData, err := json.Marshal(reqBody) if err != nil { @@ -979,10 +1025,10 @@ func (c *ServerClient) AttachToNetwork(ctx context.Context, server *Server, opts Network: opts.Network.ID, } if opts.IP != nil { - reqBody.IP = String(opts.IP.String()) + reqBody.IP = Ptr(opts.IP.String()) } for _, aliasIP := range opts.AliasIPs { - reqBody.AliasIPs = append(reqBody.AliasIPs, String(aliasIP.String())) + reqBody.AliasIPs = append(reqBody.AliasIPs, Ptr(aliasIP.String())) } reqBodyData, err := json.Marshal(reqBody) if err != nil { 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 2f4ff1f04303..3b45140066f4 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/server_type.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/server_type.go @@ -27,15 +27,16 @@ import ( // ServerType represents a server type in the Hetzner Cloud. type ServerType struct { - ID int - Name string - Description string - Cores int - Memory float32 - Disk int - StorageType StorageType - CPUType CPUType - Pricings []ServerTypeLocationPricing + ID int + Name string + Description string + Cores int + Memory float32 + Disk int + StorageType StorageType + CPUType CPUType + Architecture Architecture + Pricings []ServerTypeLocationPricing } // StorageType specifies the type of storage. diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/volume.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/volume.go index 4b3426c7823f..9b4ebd63ca1b 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/volume.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/volume.go @@ -226,7 +226,7 @@ func (c *VolumeClient) Create(ctx context.Context, opts VolumeCreateOpts) (Volum reqBody.Labels = &opts.Labels } if opts.Server != nil { - reqBody.Server = Int(opts.Server.ID) + reqBody.Server = Ptr(opts.Server.ID) } if opts.Location != nil { if opts.Location.ID != 0 { From c3399dcbc1dc02dcd8c8f76f312fb3c9689952e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20T=C3=B6lle?= Date: Thu, 23 Mar 2023 14:44:37 +0100 Subject: [PATCH 2/3] feat: predict LabelArchStable for new hetzner nodes --- .../hetzner/hetzner_node_group.go | 35 ++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/cluster-autoscaler/cloudprovider/hetzner/hetzner_node_group.go b/cluster-autoscaler/cloudprovider/hetzner/hetzner_node_group.go index e644f87c9d36..bde051c95923 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hetzner_node_group.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hetzner_node_group.go @@ -231,9 +231,14 @@ func (n *hetznerNodeGroup) TemplateNodeInfo() (*schedulerframework.NodeInfo, err }, } node.Status.Allocatable = node.Status.Capacity - node.Labels = cloudprovider.JoinStringMaps(node.Labels, buildNodeGroupLabels(n)) node.Status.Conditions = cloudprovider.BuildReadyConditions() + nodeGroupLabels, err := buildNodeGroupLabels(n) + if err != nil { + return nil, err + } + node.Labels = cloudprovider.JoinStringMaps(node.Labels, nodeGroupLabels) + nodeInfo := schedulerframework.NewNodeInfo(cloudprovider.BuildKubeProxy(n.id)) nodeInfo.SetNode(&node) @@ -313,13 +318,19 @@ func newNodeName(n *hetznerNodeGroup) string { return fmt.Sprintf("%s-%x", n.id, rand.Int63()) } -func buildNodeGroupLabels(n *hetznerNodeGroup) map[string]string { +func buildNodeGroupLabels(n *hetznerNodeGroup) (map[string]string, error) { + archLabel, err := instanceTypeArch(n.manager, n.instanceType) + if err != nil { + return nil, err + } + return map[string]string{ apiv1.LabelInstanceType: n.instanceType, - apiv1.LabelZoneRegionStable: n.region, + apiv1.LabelTopologyRegion: n.region, + apiv1.LabelArchStable: archLabel, "csi.hetzner.cloud/location": n.region, nodeGroupLabel: n.id, - } + }, nil } func getMachineTypeResourceList(m *hetznerManager, instanceType string) (apiv1.ResourceList, error) { @@ -352,6 +363,22 @@ func serverTypeAvailable(manager *hetznerManager, instanceType string, region st return false, nil } +func instanceTypeArch(manager *hetznerManager, instanceType string) (string, error) { + serverType, err := manager.cachedServerType.getServerType(instanceType) + if err != nil { + return "", err + } + + switch serverType.Architecture { + case hcloud.ArchitectureARM: + return "arm64", nil + case hcloud.ArchitectureX86: + return "amd64", nil + default: + return "amd64", nil + } +} + func createServer(n *hetznerNodeGroup) error { StartAfterCreate := true opts := hcloud.ServerCreateOpts{ From 6e94d1a84a9583f5909aa0e168b4cfbe8b98ccb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20T=C3=B6lle?= Date: Thu, 23 Mar 2023 15:45:52 +0100 Subject: [PATCH 3/3] feat: select correct image by ARCH --- .../cloudprovider/hetzner/hetzner_manager.go | 29 +--------- .../hetzner/hetzner_node_group.go | 54 ++++++++++++++++++- 2 files changed, 54 insertions(+), 29 deletions(-) diff --git a/cluster-autoscaler/cloudprovider/hetzner/hetzner_manager.go b/cluster-autoscaler/cloudprovider/hetzner/hetzner_manager.go index 7c7e47925723..7c2f45b3d462 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hetzner_manager.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hetzner_manager.go @@ -46,7 +46,7 @@ type hetznerManager struct { nodeGroups map[string]*hetznerNodeGroup apiCallContext context.Context cloudInit string - image *hcloud.Image + image string sshKey *hcloud.SSHKey network *hcloud.Network firewall *hcloud.Firewall @@ -103,31 +103,6 @@ func newManager() (*hetznerManager, error) { } } - // Search for an image ID corresponding to the supplied HCLOUD_IMAGE env - // variable. This value can either be an image ID itself (an int), a name - // (e.g. "ubuntu-20.04"), or a label selector associated with an image - // snapshot. In the latter case it will use the most recent snapshot. - image, _, err := client.Image.Get(ctx, imageName) - if err != nil { - return nil, fmt.Errorf("unable to find image %s: %v", imageName, err) - } - if image == nil { - images, err := client.Image.AllWithOpts(ctx, hcloud.ImageListOpts{ - Type: []hcloud.ImageType{hcloud.ImageTypeSnapshot}, - Status: []hcloud.ImageStatus{hcloud.ImageStatusAvailable}, - Sort: []string{"created:desc"}, - ListOpts: hcloud.ListOpts{ - LabelSelector: imageName, - }, - }) - - if err != nil || len(images) == 0 { - return nil, fmt.Errorf("unable to find image %s: %v", imageName, err) - } - - image = images[0] - } - var sshKey *hcloud.SSHKey sshKeyName := os.Getenv("HCLOUD_SSH_KEY") if sshKeyName != "" { @@ -166,7 +141,7 @@ func newManager() (*hetznerManager, error) { client: client, nodeGroups: make(map[string]*hetznerNodeGroup), cloudInit: string(cloudInit), - image: image, + image: imageName, sshKey: sshKey, network: network, firewall: firewall, diff --git a/cluster-autoscaler/cloudprovider/hetzner/hetzner_node_group.go b/cluster-autoscaler/cloudprovider/hetzner/hetzner_node_group.go index bde051c95923..05cf317ac016 100644 --- a/cluster-autoscaler/cloudprovider/hetzner/hetzner_node_group.go +++ b/cluster-autoscaler/cloudprovider/hetzner/hetzner_node_group.go @@ -17,8 +17,10 @@ limitations under the License. package hetzner import ( + "context" "fmt" "math/rand" + "strings" "sync" "time" @@ -380,13 +382,23 @@ func instanceTypeArch(manager *hetznerManager, instanceType string) (string, err } func createServer(n *hetznerNodeGroup) error { + serverType, err := n.manager.cachedServerType.getServerType(n.instanceType) + if err != nil { + return err + } + + image, err := findImage(n, serverType) + if err != nil { + return err + } + StartAfterCreate := true opts := hcloud.ServerCreateOpts{ Name: newNodeName(n), UserData: n.manager.cloudInit, Location: &hcloud.Location{Name: n.region}, - ServerType: &hcloud.ServerType{Name: n.instanceType}, - Image: n.manager.image, + ServerType: serverType, + Image: image, StartAfterCreate: &StartAfterCreate, Labels: map[string]string{ nodeGroupLabel: n.id, @@ -423,6 +435,44 @@ func createServer(n *hetznerNodeGroup) error { return nil } +// findImage searches for an image ID corresponding to the supplied +// HCLOUD_IMAGE env variable. This value can either be an image ID itself (an +// int), a name (e.g. "ubuntu-20.04"), or a label selector associated with an +// image snapshot. In the latter case it will use the most recent snapshot. +// It also verifies that the returned image has a compatible architecture with +// server. +func findImage(n *hetznerNodeGroup, serverType *hcloud.ServerType) (*hcloud.Image, error) { + // Select correct image based on server type architecture + image, _, err := n.manager.client.Image.GetForArchitecture(context.TODO(), n.manager.image, serverType.Architecture) + if err != nil { + // Keep looking for label if image was not found by id or name + if !strings.HasPrefix(err.Error(), "image not found") { + return nil, err + } + } + + if image != nil { + return image, nil + } + + // Look for snapshot with label + images, err := n.manager.client.Image.AllWithOpts(context.TODO(), hcloud.ImageListOpts{ + Type: []hcloud.ImageType{hcloud.ImageTypeSnapshot}, + Status: []hcloud.ImageStatus{hcloud.ImageStatusAvailable}, + Sort: []string{"created:desc"}, + Architecture: []hcloud.Architecture{serverType.Architecture}, + ListOpts: hcloud.ListOpts{ + LabelSelector: n.manager.image, + }, + }) + + if err != nil || len(images) == 0 { + return nil, fmt.Errorf("unable to find image %s with architecture %s: %v", n.manager.image, serverType.Architecture, err) + } + + return images[0], nil +} + func waitForServerAction(m *hetznerManager, serverName string, action *hcloud.Action) error { // The implementation of the Hetzner Cloud action client's WatchProgress // method may be a little puzzling. The following comment thus explains how