diff --git a/cluster-autoscaler/README.md b/cluster-autoscaler/README.md index 078b76890020..e2f1a45f2417 100644 --- a/cluster-autoscaler/README.md +++ b/cluster-autoscaler/README.md @@ -19,6 +19,7 @@ You should also take a look at the notes and "gotchas" for your specific cloud p * [BaiduCloud](./cloudprovider/baiducloud/README.md) * [CloudStack](./cloudprovider/cloudstack/README.md) * [HuaweiCloud](./cloudprovider/huaweicloud/README.md) +* [Hetzner](./cloudprovider/hetzner/README.md) * [Packet](./cloudprovider/packet/README.md#notes) * [IonosCloud](./cloudprovider/ionoscloud/README.md) * [OVHcloud](./cloudprovider/ovhcloud/README.md) @@ -155,3 +156,4 @@ Supported cloud providers: * Packet https://github.com/kubernetes/autoscaler/blob/master/cluster-autoscaler/cloudprovider/packet/README.md * OVHcloud https://github.com/kubernetes/autoscaler/blob/master/cluster-autoscaler/cloudprovider/ovhcloud/README.md * Linode https://github.com/kubernetes/autoscaler/blob/master/cluster-autoscaler/cloudprovider/linode/README.md +* Hetzner https://github.com/kubernetes/autoscaler/blob/master/cluster-autoscaler/cloudprovider/hetzner/README.md diff --git a/cluster-autoscaler/cloudprovider/builder/builder_all.go b/cluster-autoscaler/cloudprovider/builder/builder_all.go index a58a10afff67..00770e744a4c 100644 --- a/cluster-autoscaler/cloudprovider/builder/builder_all.go +++ b/cluster-autoscaler/cloudprovider/builder/builder_all.go @@ -1,4 +1,4 @@ -// +build !gce,!aws,!azure,!kubemark,!alicloud,!magnum,!digitalocean,!clusterapi,!huaweicloud,!ionoscloud,!linode +// +build !gce,!aws,!azure,!kubemark,!alicloud,!magnum,!digitalocean,!clusterapi,!huaweicloud,!ionoscloud,!linode,!hetzner /* Copyright 2018 The Kubernetes Authors. @@ -10,7 +10,7 @@ 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, +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. @@ -50,6 +50,7 @@ var AvailableCloudProviders = []string{ cloudprovider.DigitalOceanProviderName, cloudprovider.ExoscaleProviderName, cloudprovider.HuaweicloudProviderName, + cloudprovider.HetznerProviderName, cloudprovider.OVHcloudProviderName, cloudprovider.ClusterAPIProiverName, cloudprovider.IonoscloudProviderName, @@ -83,6 +84,8 @@ func buildCloudProvider(opts config.AutoscalingOptions, do cloudprovider.NodeGro return huaweicloud.BuildHuaweiCloud(opts, do, rl) case cloudprovider.OVHcloudProviderName: return ovhcloud.BuildOVHcloud(opts, do, rl) + case cloudprovider.HetznerProviderName: + return hetzner.BuildHetzner(opts, do, rl) case packet.ProviderName: return packet.BuildPacket(opts, do, rl) case cloudprovider.ClusterAPIProiverName: diff --git a/cluster-autoscaler/cloudprovider/builder/builder_hetzner.go b/cluster-autoscaler/cloudprovider/builder/builder_hetzner.go new file mode 100644 index 000000000000..8c54b01952d6 --- /dev/null +++ b/cluster-autoscaler/cloudprovider/builder/builder_hetzner.go @@ -0,0 +1,42 @@ +// +build hetzner + +/* +Copyright 2020 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 builder + +import ( + "k8s.io/autoscaler/cluster-autoscaler/cloudprovider" + "k8s.io/autoscaler/cluster-autoscaler/cloudprovider/hetzner" + "k8s.io/autoscaler/cluster-autoscaler/config" +) + +// AvailableCloudProviders supported by the Hetzner cloud provider builder. +var AvailableCloudProviders = []string{ + cloudprovider.HetznerProviderName, +} + +// DefaultCloudProvider is Hetzner. +const DefaultCloudProvider = cloudprovider.HetznerProviderName + +func buildCloudProvider(opts config.AutoscalingOptions, do cloudprovider.NodeGroupDiscoveryOptions, rl *cloudprovider.ResourceLimiter) cloudprovider.CloudProvider { + switch opts.CloudProviderName { + case cloudprovider.HetznerProviderName: + return hetzner.BuildHetzner(opts, do, rl) + } + + return nil +} diff --git a/cluster-autoscaler/cloudprovider/cloud_provider.go b/cluster-autoscaler/cloudprovider/cloud_provider.go index 9a6e0a86b0a1..fb09cfc4d0a1 100644 --- a/cluster-autoscaler/cloudprovider/cloud_provider.go +++ b/cluster-autoscaler/cloudprovider/cloud_provider.go @@ -46,6 +46,8 @@ const ( ExoscaleProviderName = "exoscale" // GceProviderName gets the provider name of gce GceProviderName = "gce" + // HetznerProviderName gets the provider name of hetzner + HetznerProviderName = "hetzner" // MagnumProviderName gets the provider name of magnum MagnumProviderName = "magnum" // KubemarkProviderName gets the provider name of kubemark diff --git a/cluster-autoscaler/cloudprovider/hetzner/OWNERS b/cluster-autoscaler/cloudprovider/hetzner/OWNERS new file mode 100644 index 000000000000..fb32465f61a2 --- /dev/null +++ b/cluster-autoscaler/cloudprovider/hetzner/OWNERS @@ -0,0 +1,4 @@ +approvers: +- Fgruntjes +reviewers: +- Fgruntjes diff --git a/cluster-autoscaler/cloudprovider/hetzner/README.md b/cluster-autoscaler/cloudprovider/hetzner/README.md new file mode 100644 index 000000000000..1a6758f06b9b --- /dev/null +++ b/cluster-autoscaler/cloudprovider/hetzner/README.md @@ -0,0 +1,42 @@ +# Cluster Autoscaler for Hetzner Cloud + +The cluster autoscaler for Hetzner Cloud scales worker nodes. + +# Configuration + +`HCLOUD_TOKEN` Required Hetzner Cloud token. +`HCLOUD_CLOUD_INIT` Base64 encoded Cloud Init yaml with commands to join the cluster +`HCLOUD_IMAGE` Defaults to `ubuntu-20.04`, @see https://docs.hetzner.cloud/#images + +Node groups must be defined with the `--nodes=::::` flag. +Multiple flags will create multiple node pools. For example: +``` +--nodes=1:10:CPX51:FSN1:pool1 +--nodes=1:10:CPX51:NBG1:pool2 +--nodes=1:10:CX41:NBG1:pool3 +``` + +# Development + +Make sure you're inside the root path of the [autoscaler +repository](https://github.com/kubernetes/autoscaler) + +1.) Build the `cluster-autoscaler` binary: + + +``` +make build-in-docker +``` + +2.) Build the docker image: + +``` +docker build -t hetzner/cluster-autoscaler:dev . +``` + + +3.) Push the docker image to Docker hub: + +``` +docker push hetzner/cluster-autoscaler:dev +``` diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/action.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/action.go new file mode 100644 index 000000000000..79a7e1f846e8 --- /dev/null +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/action.go @@ -0,0 +1,226 @@ +/* +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/url" + "time" + + "k8s.io/autoscaler/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema" +) + +// Action represents an action in the Hetzner Cloud. +type Action struct { + ID int + Status ActionStatus + Command string + Progress int + Started time.Time + Finished time.Time + ErrorCode string + ErrorMessage string + Resources []*ActionResource +} + +// ActionStatus represents an action's status. +type ActionStatus string + +// List of action statuses. +const ( + ActionStatusRunning ActionStatus = "running" + ActionStatusSuccess ActionStatus = "success" + ActionStatusError ActionStatus = "error" +) + +// ActionResource references other resources from an action. +type ActionResource struct { + ID int + Type ActionResourceType +} + +// ActionResourceType represents an action's resource reference type. +type ActionResourceType string + +// List of action resource reference types. +const ( + ActionResourceTypeServer ActionResourceType = "server" + ActionResourceTypeImage ActionResourceType = "image" + ActionResourceTypeISO ActionResourceType = "iso" + ActionResourceTypeFloatingIP ActionResourceType = "floating_ip" + ActionResourceTypeVolume ActionResourceType = "volume" +) + +// ActionError is the error of an action. +type ActionError struct { + Code string + Message string +} + +func (e ActionError) Error() string { + return fmt.Sprintf("%s (%s)", e.Message, e.Code) +} + +func (a *Action) Error() error { + if a.ErrorCode != "" && a.ErrorMessage != "" { + return ActionError{ + Code: a.ErrorCode, + Message: a.ErrorMessage, + } + } + return nil +} + +// ActionClient is a client for the actions API. +type ActionClient struct { + client *Client +} + +// GetByID retrieves an action by its ID. If the action does not exist, nil is returned. +func (c *ActionClient) GetByID(ctx context.Context, id int) (*Action, *Response, error) { + req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/actions/%d", id), nil) + if err != nil { + return nil, nil, err + } + + var body schema.ActionGetResponse + resp, err := c.client.Do(req, &body) + if err != nil { + if IsError(err, ErrorCodeNotFound) { + return nil, resp, nil + } + return nil, nil, err + } + return ActionFromSchema(body.Action), resp, nil +} + +// ActionListOpts specifies options for listing actions. +type ActionListOpts struct { + ListOpts + Status []ActionStatus + Sort []string +} + +func (l ActionListOpts) values() url.Values { + vals := l.ListOpts.values() + for _, status := range l.Status { + vals.Add("status", string(status)) + } + for _, sort := range l.Sort { + vals.Add("sort", sort) + } + return vals +} + +// List returns a list of actions for a specific page. +// +// Please note that filters specified in opts are not taken into account +// when their value corresponds to their zero value or when they are empty. +func (c *ActionClient) List(ctx context.Context, opts ActionListOpts) ([]*Action, *Response, error) { + path := "/actions?" + opts.values().Encode() + req, err := c.client.NewRequest(ctx, "GET", path, nil) + if err != nil { + return nil, nil, err + } + + var body schema.ActionListResponse + resp, err := c.client.Do(req, &body) + if err != nil { + return nil, nil, err + } + actions := make([]*Action, 0, len(body.Actions)) + for _, i := range body.Actions { + actions = append(actions, ActionFromSchema(i)) + } + return actions, resp, nil +} + +// All returns all actions. +func (c *ActionClient) All(ctx context.Context) ([]*Action, error) { + allActions := []*Action{} + + opts := ActionListOpts{} + opts.PerPage = 50 + + _, err := c.client.all(func(page int) (*Response, error) { + opts.Page = page + actions, resp, err := c.List(ctx, opts) + if err != nil { + return resp, err + } + allActions = append(allActions, actions...) + return resp, nil + }) + if err != nil { + return nil, err + } + + return allActions, nil +} + +// WatchProgress watches the action's progress until it completes with success or error. +func (c *ActionClient) WatchProgress(ctx context.Context, action *Action) (<-chan int, <-chan error) { + errCh := make(chan error, 1) + progressCh := make(chan int) + + go func() { + defer close(errCh) + defer close(progressCh) + + ticker := time.NewTicker(c.client.pollInterval) + sendProgress := func(p int) { + select { + case progressCh <- p: + break + default: + break + } + } + + for { + select { + case <-ctx.Done(): + errCh <- ctx.Err() + return + case <-ticker.C: + break + } + + a, _, err := c.GetByID(ctx, action.ID) + if err != nil { + errCh <- err + return + } + + switch a.Status { + case ActionStatusRunning: + sendProgress(a.Progress) + break + case ActionStatusSuccess: + sendProgress(100) + errCh <- nil + return + case ActionStatusError: + errCh <- a.Error() + return + } + } + }() + + return progressCh, errCh +} diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/certificate.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/certificate.go new file mode 100644 index 000000000000..990e698a3716 --- /dev/null +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/certificate.go @@ -0,0 +1,262 @@ +/* +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" +) + +// Certificate represents an certificate in the Hetzner Cloud. +type Certificate struct { + ID int + Name string + Labels map[string]string + Certificate string + Created time.Time + NotValidBefore time.Time + NotValidAfter time.Time + DomainNames []string + Fingerprint string +} + +// CertificateClient is a client for the Certificates API. +type CertificateClient struct { + client *Client +} + +// GetByID retrieves a Certificate by its ID. If the Certificate does not exist, nil is returned. +func (c *CertificateClient) GetByID(ctx context.Context, id int) (*Certificate, *Response, error) { + req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/certificates/%d", id), nil) + if err != nil { + return nil, nil, err + } + + var body schema.CertificateGetResponse + resp, err := c.client.Do(req, &body) + if err != nil { + if IsError(err, ErrorCodeNotFound) { + return nil, resp, nil + } + return nil, nil, err + } + return CertificateFromSchema(body.Certificate), resp, nil +} + +// GetByName retrieves a Certificate by its name. If the Certificate does not exist, nil is returned. +func (c *CertificateClient) GetByName(ctx context.Context, name string) (*Certificate, *Response, error) { + if name == "" { + return nil, nil, nil + } + Certificate, response, err := c.List(ctx, CertificateListOpts{Name: name}) + if len(Certificate) == 0 { + return nil, response, err + } + return Certificate[0], response, err +} + +// Get retrieves a Certificate by its ID if the input can be parsed as an integer, otherwise it +// retrieves a Certificate by its name. If the Certificate does not exist, nil is returned. +func (c *CertificateClient) Get(ctx context.Context, idOrName string) (*Certificate, *Response, error) { + if id, err := strconv.Atoi(idOrName); err == nil { + return c.GetByID(ctx, int(id)) + } + return c.GetByName(ctx, idOrName) +} + +// CertificateListOpts specifies options for listing Certificates. +type CertificateListOpts struct { + ListOpts + Name string +} + +func (l CertificateListOpts) values() url.Values { + vals := l.ListOpts.values() + if l.Name != "" { + vals.Add("name", l.Name) + } + return vals +} + +// List returns a list of Certificates 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 *CertificateClient) List(ctx context.Context, opts CertificateListOpts) ([]*Certificate, *Response, error) { + path := "/certificates?" + opts.values().Encode() + req, err := c.client.NewRequest(ctx, "GET", path, nil) + if err != nil { + return nil, nil, err + } + + var body schema.CertificateListResponse + resp, err := c.client.Do(req, &body) + if err != nil { + return nil, nil, err + } + Certificates := make([]*Certificate, 0, len(body.Certificates)) + for _, s := range body.Certificates { + Certificates = append(Certificates, CertificateFromSchema(s)) + } + return Certificates, resp, nil +} + +// All returns all Certificates. +func (c *CertificateClient) All(ctx context.Context) ([]*Certificate, error) { + allCertificates := []*Certificate{} + + opts := CertificateListOpts{} + opts.PerPage = 50 + + _, err := c.client.all(func(page int) (*Response, error) { + opts.Page = page + Certificate, resp, err := c.List(ctx, opts) + if err != nil { + return resp, err + } + allCertificates = append(allCertificates, Certificate...) + return resp, nil + }) + if err != nil { + return nil, err + } + + return allCertificates, nil +} + +// AllWithOpts returns all Certificates for the given options. +func (c *CertificateClient) AllWithOpts(ctx context.Context, opts CertificateListOpts) ([]*Certificate, error) { + var allCertificates []*Certificate + + _, err := c.client.all(func(page int) (*Response, error) { + opts.Page = page + Certificates, resp, err := c.List(ctx, opts) + if err != nil { + return resp, err + } + allCertificates = append(allCertificates, Certificates...) + return resp, nil + }) + if err != nil { + return nil, err + } + + return allCertificates, nil +} + +// CertificateCreateOpts specifies options for creating a new Certificate. +type CertificateCreateOpts struct { + Name string + Certificate string + PrivateKey string + Labels map[string]string +} + +// Validate checks if options are valid. +func (o CertificateCreateOpts) Validate() error { + if o.Name == "" { + return errors.New("missing name") + } + if o.Certificate == "" { + return errors.New("missing certificate") + } + if o.PrivateKey == "" { + return errors.New("missing private key") + } + return nil +} + +// Create creates a new certificate. +func (c *CertificateClient) Create(ctx context.Context, opts CertificateCreateOpts) (*Certificate, *Response, error) { + if err := opts.Validate(); err != nil { + return nil, nil, err + } + reqBody := schema.CertificateCreateRequest{ + Name: opts.Name, + Certificate: opts.Certificate, + PrivateKey: opts.PrivateKey, + } + if opts.Labels != nil { + reqBody.Labels = &opts.Labels + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + req, err := c.client.NewRequest(ctx, "POST", "/certificates", bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + respBody := schema.CertificateCreateResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return CertificateFromSchema(respBody.Certificate), resp, nil +} + +// CertificateUpdateOpts specifies options for updating a Certificate. +type CertificateUpdateOpts struct { + Name string + Labels map[string]string +} + +// Update updates a Certificate. +func (c *CertificateClient) Update(ctx context.Context, certificate *Certificate, opts CertificateUpdateOpts) (*Certificate, *Response, error) { + reqBody := schema.CertificateUpdateRequest{} + 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("/certificates/%d", certificate.ID) + req, err := c.client.NewRequest(ctx, "PUT", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + respBody := schema.CertificateUpdateResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return CertificateFromSchema(respBody.Certificate), resp, nil +} + +// Delete deletes a certificate. +func (c *CertificateClient) Delete(ctx context.Context, certificate *Certificate) (*Response, error) { + req, err := c.client.NewRequest(ctx, "DELETE", fmt.Sprintf("/certificates/%d", certificate.ID), nil) + if err != nil { + return nil, err + } + return c.client.Do(req, nil) +} diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/client.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/client.go new file mode 100644 index 000000000000..d7bbd8e9c71a --- /dev/null +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/client.go @@ -0,0 +1,410 @@ +/* +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" + "fmt" + "io" + "io/ioutil" + "math" + "net/http" + "net/http/httputil" + "net/url" + "strconv" + "strings" + "time" + + "k8s.io/autoscaler/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema" +) + +// Endpoint is the base URL of the API. +const Endpoint = "https://api.hetzner.cloud/v1" + +// UserAgent is the value for the library part of the User-Agent header +// that is sent with each request. +const UserAgent = "hcloud-go/" + Version + +// A BackoffFunc returns the duration to wait before performing the +// next retry. The retries argument specifies how many retries have +// already been performed. When called for the first time, retries is 0. +type BackoffFunc func(retries int) time.Duration + +// ConstantBackoff returns a BackoffFunc which backs off for +// constant duration d. +func ConstantBackoff(d time.Duration) BackoffFunc { + return func(_ int) time.Duration { + return d + } +} + +// ExponentialBackoff returns a BackoffFunc which implements an exponential +// backoff using 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 + } +} + +// 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 + + Action ActionClient + Certificate CertificateClient + Datacenter DatacenterClient + FloatingIP FloatingIPClient + Image ImageClient + ISO ISOClient + LoadBalancer LoadBalancerClient + LoadBalancerType LoadBalancerTypeClient + Location LocationClient + Network NetworkClient + Pricing PricingClient + Server ServerClient + ServerType ServerTypeClient + SSHKey SSHKeyClient + Volume VolumeClient +} + +// A ClientOption is used to configure a Client. +type ClientOption func(*Client) + +// WithEndpoint configures a Client to use the specified API endpoint. +func WithEndpoint(endpoint string) ClientOption { + return func(client *Client) { + client.endpoint = strings.TrimRight(endpoint, "/") + } +} + +// WithToken configures a Client to use the specified token for authentication. +func WithToken(token string) ClientOption { + return func(client *Client) { + client.token = token + } +} + +// WithPollInterval configures a Client to use the specified interval when polling +// from the API. +func WithPollInterval(pollInterval time.Duration) ClientOption { + return func(client *Client) { + client.pollInterval = pollInterval + } +} + +// WithBackoffFunc configures a Client to use the specified backoff function. +func WithBackoffFunc(f BackoffFunc) ClientOption { + return func(client *Client) { + client.backoffFunc = f + } +} + +// WithApplication configures a Client with the given application name and +// application version. The version may be blank. Programs are encouraged +// to at least set an application name. +func WithApplication(name, version string) ClientOption { + return func(client *Client) { + client.applicationName = name + client.applicationVersion = version + } +} + +// WithDebugWriter configures a Client to print debug information to the given +// writer. To, for example, print debug information on stderr, set it to os.Stderr. +func WithDebugWriter(debugWriter io.Writer) ClientOption { + return func(client *Client) { + client.debugWriter = debugWriter + } +} + +// WithHTTPClient configures a Client to perform HTTP requests with httpClient. +func WithHTTPClient(httpClient *http.Client) ClientOption { + return func(client *Client) { + client.httpClient = httpClient + } +} + +// NewClient creates a new client. +func NewClient(options ...ClientOption) *Client { + client := &Client{ + endpoint: Endpoint, + httpClient: &http.Client{}, + backoffFunc: ExponentialBackoff(2, 500*time.Millisecond), + pollInterval: 500 * time.Millisecond, + } + + for _, option := range options { + option(client) + } + + client.buildUserAgent() + + client.Action = ActionClient{client: client} + client.Datacenter = DatacenterClient{client: client} + client.FloatingIP = FloatingIPClient{client: client} + client.Image = ImageClient{client: client} + client.ISO = ISOClient{client: client} + client.Location = LocationClient{client: client} + client.Network = NetworkClient{client: client} + client.Pricing = PricingClient{client: client} + client.Server = ServerClient{client: client} + client.ServerType = ServerTypeClient{client: client} + client.SSHKey = SSHKeyClient{client: client} + client.Volume = VolumeClient{client: client} + client.LoadBalancer = LoadBalancerClient{client: client} + client.LoadBalancerType = LoadBalancerTypeClient{client: client} + client.Certificate = CertificateClient{client: client} + + return client +} + +// NewRequest creates an HTTP request against the API. The returned request +// is assigned with ctx and has all necessary headers set (auth, user agent, etc.). +func (c *Client) NewRequest(ctx context.Context, method, path string, body io.Reader) (*http.Request, error) { + url := c.endpoint + path + req, err := http.NewRequest(method, url, body) + if err != nil { + return nil, err + } + req.Header.Set("User-Agent", c.userAgent) + if c.token != "" { + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.token)) + } + if body != nil { + req.Header.Set("Content-Type", "application/json") + } + req = req.WithContext(ctx) + return req, nil +} + +// Do performs an HTTP request against the API. +func (c *Client) Do(r *http.Request, v interface{}) (*Response, error) { + var retries int + var body []byte + var err error + if r.ContentLength > 0 { + body, err = ioutil.ReadAll(r.Body) + if err != nil { + r.Body.Close() + return nil, err + } + r.Body.Close() + } + for { + if r.ContentLength > 0 { + r.Body = ioutil.NopCloser(bytes.NewReader(body)) + } + + if c.debugWriter != nil { + // To get the response body we need to read it before the request was actually send. https://github.com/golang/go/issues/29792 + dumpReq, err := httputil.DumpRequestOut(r, true) + if err != nil { + return nil, err + } + fmt.Fprintf(c.debugWriter, "--- Request:\n%s\n\n", dumpReq) + } + + resp, err := c.httpClient.Do(r) + if err != nil { + return nil, err + } + response := &Response{Response: resp} + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + resp.Body.Close() + return response, err + } + resp.Body.Close() + resp.Body = ioutil.NopCloser(bytes.NewReader(body)) + + if c.debugWriter != nil { + dumpResp, err := httputil.DumpResponse(resp, true) + if err != nil { + return nil, err + } + fmt.Fprintf(c.debugWriter, "--- Response:\n%s\n\n", dumpResp) + } + + if err = response.readMeta(body); err != nil { + return response, fmt.Errorf("hcloud: error reading response meta data: %s", err) + } + + if resp.StatusCode >= 400 && resp.StatusCode <= 599 { + err = errorFromResponse(resp, body) + if err == nil { + err = fmt.Errorf("hcloud: server responded with status code %d", resp.StatusCode) + } else { + if isRetryable(err) { + c.backoff(retries) + retries++ + continue + } + } + return response, err + } + if v != nil { + if w, ok := v.(io.Writer); ok { + _, err = io.Copy(w, bytes.NewReader(body)) + } else { + err = json.Unmarshal(body, v) + } + } + + return response, err + } +} + +func isRetryable(error error) bool { + err, ok := error.(Error) + if !ok { + return false + } + return err.Code == ErrorCodeRateLimitExceeded || err.Code == ErrorCodeConflict +} + +func (c *Client) backoff(retries int) { + time.Sleep(c.backoffFunc(retries)) +} + +func (c *Client) all(f func(int) (*Response, error)) (*Response, error) { + var ( + page = 1 + ) + for { + resp, err := f(page) + if err != nil { + return nil, err + } + if resp.Meta.Pagination == nil || resp.Meta.Pagination.NextPage == 0 { + return resp, nil + } + page = resp.Meta.Pagination.NextPage + } +} + +func (c *Client) buildUserAgent() { + switch { + case c.applicationName != "" && c.applicationVersion != "": + c.userAgent = c.applicationName + "/" + c.applicationVersion + " " + UserAgent + case c.applicationName != "" && c.applicationVersion == "": + c.userAgent = c.applicationName + " " + UserAgent + default: + c.userAgent = UserAgent + } +} + +func errorFromResponse(resp *http.Response, body []byte) error { + if !strings.HasPrefix(resp.Header.Get("Content-Type"), "application/json") { + return nil + } + + var respBody schema.ErrorResponse + if err := json.Unmarshal(body, &respBody); err != nil { + return nil + } + if respBody.Error.Code == "" && respBody.Error.Message == "" { + return nil + } + return ErrorFromSchema(respBody.Error) +} + +// Response represents a response from the API. It embeds http.Response. +type Response struct { + *http.Response + Meta Meta +} + +func (r *Response) readMeta(body []byte) error { + if h := r.Header.Get("RateLimit-Limit"); h != "" { + r.Meta.Ratelimit.Limit, _ = strconv.Atoi(h) + } + if h := r.Header.Get("RateLimit-Remaining"); h != "" { + r.Meta.Ratelimit.Remaining, _ = strconv.Atoi(h) + } + if h := r.Header.Get("RateLimit-Reset"); h != "" { + if ts, err := strconv.ParseInt(h, 10, 64); err == nil { + r.Meta.Ratelimit.Reset = time.Unix(ts, 0) + } + } + + if strings.HasPrefix(r.Header.Get("Content-Type"), "application/json") { + var s schema.MetaResponse + if err := json.Unmarshal(body, &s); err != nil { + return err + } + if s.Meta.Pagination != nil { + p := PaginationFromSchema(*s.Meta.Pagination) + r.Meta.Pagination = &p + } + } + + return nil +} + +// Meta represents meta information included in an API response. +type Meta struct { + Pagination *Pagination + Ratelimit Ratelimit +} + +// Pagination represents pagination meta information. +type Pagination struct { + Page int + PerPage int + PreviousPage int + NextPage int + LastPage int + TotalEntries int +} + +// Ratelimit represents ratelimit information. +type Ratelimit struct { + Limit int + Remaining int + Reset time.Time +} + +// ListOpts specifies options for listing resources. +type ListOpts struct { + Page int // Page (starting at 1) + PerPage int // Items per page (0 means default) + LabelSelector string // Label selector for filtering by labels +} + +func (l ListOpts) values() url.Values { + vals := url.Values{} + if l.Page > 0 { + vals.Add("page", strconv.Itoa(l.Page)) + } + if l.PerPage > 0 { + vals.Add("per_page", strconv.Itoa(l.PerPage)) + } + if len(l.LabelSelector) > 0 { + vals.Add("label_selector", l.LabelSelector) + } + return vals +} diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/datacenter.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/datacenter.go new file mode 100644 index 000000000000..9c3c8f583c12 --- /dev/null +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/datacenter.go @@ -0,0 +1,145 @@ +/* +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/url" + "strconv" + + "k8s.io/autoscaler/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema" +) + +// Datacenter represents a datacenter in the Hetzner Cloud. +type Datacenter struct { + ID int + Name string + Description string + Location *Location + ServerTypes DatacenterServerTypes +} + +// DatacenterServerTypes represents the server types available and supported in a datacenter. +type DatacenterServerTypes struct { + Supported []*ServerType + Available []*ServerType +} + +// DatacenterClient is a client for the datacenter API. +type DatacenterClient struct { + client *Client +} + +// GetByID retrieves a datacenter by its ID. If the datacenter does not exist, nil is returned. +func (c *DatacenterClient) GetByID(ctx context.Context, id int) (*Datacenter, *Response, error) { + req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/datacenters/%d", id), nil) + if err != nil { + return nil, nil, err + } + + var body schema.DatacenterGetResponse + resp, err := c.client.Do(req, &body) + if err != nil { + if IsError(err, ErrorCodeNotFound) { + return nil, resp, nil + } + return nil, resp, err + } + return DatacenterFromSchema(body.Datacenter), resp, nil +} + +// GetByName retrieves an datacenter by its name. If the datacenter does not exist, nil is returned. +func (c *DatacenterClient) GetByName(ctx context.Context, name string) (*Datacenter, *Response, error) { + if name == "" { + return nil, nil, nil + } + datacenters, response, err := c.List(ctx, DatacenterListOpts{Name: name}) + if len(datacenters) == 0 { + return nil, response, err + } + return datacenters[0], response, err +} + +// Get retrieves a datacenter by its ID if the input can be parsed as an integer, otherwise it +// retrieves a datacenter by its name. If the datacenter does not exist, nil is returned. +func (c *DatacenterClient) Get(ctx context.Context, idOrName string) (*Datacenter, *Response, error) { + if id, err := strconv.Atoi(idOrName); err == nil { + return c.GetByID(ctx, int(id)) + } + return c.GetByName(ctx, idOrName) +} + +// DatacenterListOpts specifies options for listing datacenters. +type DatacenterListOpts struct { + ListOpts + Name string +} + +func (l DatacenterListOpts) values() url.Values { + vals := l.ListOpts.values() + if l.Name != "" { + vals.Add("name", l.Name) + } + return vals +} + +// List returns a list of datacenters 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 *DatacenterClient) List(ctx context.Context, opts DatacenterListOpts) ([]*Datacenter, *Response, error) { + path := "/datacenters?" + opts.values().Encode() + req, err := c.client.NewRequest(ctx, "GET", path, nil) + if err != nil { + return nil, nil, err + } + + var body schema.DatacenterListResponse + resp, err := c.client.Do(req, &body) + if err != nil { + return nil, nil, err + } + datacenters := make([]*Datacenter, 0, len(body.Datacenters)) + for _, i := range body.Datacenters { + datacenters = append(datacenters, DatacenterFromSchema(i)) + } + return datacenters, resp, nil +} + +// All returns all datacenters. +func (c *DatacenterClient) All(ctx context.Context) ([]*Datacenter, error) { + allDatacenters := []*Datacenter{} + + opts := DatacenterListOpts{} + opts.PerPage = 50 + + _, err := c.client.all(func(page int) (*Response, error) { + opts.Page = page + datacenters, resp, err := c.List(ctx, opts) + if err != nil { + return resp, err + } + allDatacenters = append(allDatacenters, datacenters...) + return resp, nil + }) + if err != nil { + return nil, err + } + + return allDatacenters, nil +} diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/error.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/error.go new file mode 100644 index 000000000000..0b684dc9bd21 --- /dev/null +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/error.go @@ -0,0 +1,78 @@ +/* +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 "fmt" + +// ErrorCode represents an error code returned from the API. +type ErrorCode string + +// Error codes returned from the API. +const ( + ErrorCodeServiceError ErrorCode = "service_error" // Generic service error + ErrorCodeRateLimitExceeded ErrorCode = "rate_limit_exceeded" // Rate limit exceeded + ErrorCodeUnknownError ErrorCode = "unknown_error" // Unknown error + ErrorCodeNotFound ErrorCode = "not_found" // Resource not found + ErrorCodeInvalidInput ErrorCode = "invalid_input" // Validation error + ErrorCodeForbidden ErrorCode = "forbidden" // Insufficient permissions + ErrorCodeJSONError ErrorCode = "json_error" // Invalid JSON in request + ErrorCodeLocked ErrorCode = "locked" // Item is locked (Another action is running) + ErrorCodeResourceLimitExceeded ErrorCode = "resource_limit_exceeded" // Resource limit exceeded + ErrorCodeResourceUnavailable ErrorCode = "resource_unavailable" // Resource currently unavailable + ErrorCodeUniquenessError ErrorCode = "uniqueness_error" // One or more fields must be unique + ErrorCodeProtected ErrorCode = "protected" // The actions you are trying is protected + ErrorCodeMaintenance ErrorCode = "maintenance" // Cannot perform operation due to maintenance + ErrorCodeConflict ErrorCode = "conflict" // The resource has changed during the request, please retry + ErrorCodeServerAlreadyAttached ErrorCode = "server_already_attached" // The server is already attached to the resource + + // Deprecated error codes + + // The actual value of this error code is limit_reached. The new error code + // rate_limit_exceeded for ratelimiting was introduced before Hetzner Cloud + // launched into the public. To make clients using the old error code still + // work as expected, we set the value of the old error code to that of the + // new error code. + ErrorCodeLimitReached = ErrorCodeRateLimitExceeded +) + +// Error is an error returned from the API. +type Error struct { + Code ErrorCode + Message string + Details interface{} +} + +func (e Error) Error() string { + return fmt.Sprintf("%s (%s)", e.Message, e.Code) +} + +// ErrorDetailsInvalidInput contains the details of an 'invalid_input' error. +type ErrorDetailsInvalidInput struct { + Fields []ErrorDetailsInvalidInputField +} + +// ErrorDetailsInvalidInputField contains the validation errors reported on a field. +type ErrorDetailsInvalidInputField struct { + Name string + Messages []string +} + +// IsError returns whether err is an API error with the given error code. +func IsError(err error, code ErrorCode) bool { + apiErr, ok := err.(Error) + return ok && apiErr.Code == code +} diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/floating_ip.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/floating_ip.go new file mode 100644 index 000000000000..a8ef89f7efe7 --- /dev/null +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/floating_ip.go @@ -0,0 +1,394 @@ +/* +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" + "net/url" + "strconv" + "time" + + "k8s.io/autoscaler/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema" +) + +// FloatingIP represents a Floating IP in the Hetzner Cloud. +type FloatingIP struct { + ID int + Description string + Created time.Time + IP net.IP + Network *net.IPNet + Type FloatingIPType + Server *Server + DNSPtr map[string]string + HomeLocation *Location + Blocked bool + Protection FloatingIPProtection + Labels map[string]string + Name string +} + +// DNSPtrForIP returns the reverse DNS pointer of the IP address. +func (f *FloatingIP) DNSPtrForIP(ip net.IP) string { + return f.DNSPtr[ip.String()] +} + +// FloatingIPProtection represents the protection level of a Floating IP. +type FloatingIPProtection struct { + Delete bool +} + +// FloatingIPType represents the type of a Floating IP. +type FloatingIPType string + +// Floating IP types. +const ( + FloatingIPTypeIPv4 FloatingIPType = "ipv4" + FloatingIPTypeIPv6 FloatingIPType = "ipv6" +) + +// FloatingIPClient is a client for the Floating IP API. +type FloatingIPClient struct { + client *Client +} + +// GetByID retrieves a Floating IP by its ID. If the Floating IP does not exist, +// nil is returned. +func (c *FloatingIPClient) GetByID(ctx context.Context, id int) (*FloatingIP, *Response, error) { + req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/floating_ips/%d", id), nil) + if err != nil { + return nil, nil, err + } + + var body schema.FloatingIPGetResponse + resp, err := c.client.Do(req, &body) + if err != nil { + if IsError(err, ErrorCodeNotFound) { + return nil, resp, nil + } + return nil, resp, err + } + return FloatingIPFromSchema(body.FloatingIP), resp, nil +} + +// GetByName retrieves a Floating IP by its name. If the Floating IP does not exist, nil is returned. +func (c *FloatingIPClient) GetByName(ctx context.Context, name string) (*FloatingIP, *Response, error) { + if name == "" { + return nil, nil, nil + } + floatingIPs, response, err := c.List(ctx, FloatingIPListOpts{Name: name}) + if len(floatingIPs) == 0 { + return nil, response, err + } + return floatingIPs[0], response, err +} + +// Get retrieves a Floating IP by its ID if the input can be parsed as an integer, otherwise it +// retrieves a Floating IP by its name. If the Floating IP does not exist, nil is returned. +func (c *FloatingIPClient) Get(ctx context.Context, idOrName string) (*FloatingIP, *Response, error) { + if id, err := strconv.Atoi(idOrName); err == nil { + return c.GetByID(ctx, int(id)) + } + return c.GetByName(ctx, idOrName) +} + +// FloatingIPListOpts specifies options for listing Floating IPs. +type FloatingIPListOpts struct { + ListOpts + Name string +} + +func (l FloatingIPListOpts) values() url.Values { + vals := l.ListOpts.values() + if l.Name != "" { + vals.Add("name", l.Name) + } + return vals +} + +// List returns a list of Floating IPs 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 *FloatingIPClient) List(ctx context.Context, opts FloatingIPListOpts) ([]*FloatingIP, *Response, error) { + path := "/floating_ips?" + opts.values().Encode() + req, err := c.client.NewRequest(ctx, "GET", path, nil) + if err != nil { + return nil, nil, err + } + + var body schema.FloatingIPListResponse + resp, err := c.client.Do(req, &body) + if err != nil { + return nil, nil, err + } + floatingIPs := make([]*FloatingIP, 0, len(body.FloatingIPs)) + for _, s := range body.FloatingIPs { + floatingIPs = append(floatingIPs, FloatingIPFromSchema(s)) + } + return floatingIPs, resp, nil +} + +// All returns all Floating IPs. +func (c *FloatingIPClient) All(ctx context.Context) ([]*FloatingIP, error) { + return c.AllWithOpts(ctx, FloatingIPListOpts{ListOpts: ListOpts{PerPage: 50}}) +} + +// AllWithOpts returns all Floating IPs for the given options. +func (c *FloatingIPClient) AllWithOpts(ctx context.Context, opts FloatingIPListOpts) ([]*FloatingIP, error) { + allFloatingIPs := []*FloatingIP{} + + _, err := c.client.all(func(page int) (*Response, error) { + opts.Page = page + floatingIPs, resp, err := c.List(ctx, opts) + if err != nil { + return resp, err + } + allFloatingIPs = append(allFloatingIPs, floatingIPs...) + return resp, nil + }) + if err != nil { + return nil, err + } + + return allFloatingIPs, nil +} + +// FloatingIPCreateOpts specifies options for creating a Floating IP. +type FloatingIPCreateOpts struct { + Type FloatingIPType + HomeLocation *Location + Server *Server + Description *string + Name *string + Labels map[string]string +} + +// Validate checks if options are valid. +func (o FloatingIPCreateOpts) Validate() error { + switch o.Type { + case FloatingIPTypeIPv4, FloatingIPTypeIPv6: + break + default: + return errors.New("missing or invalid type") + } + if o.HomeLocation == nil && o.Server == nil { + return errors.New("one of home location or server is required") + } + return nil +} + +// FloatingIPCreateResult is the result of creating a Floating IP. +type FloatingIPCreateResult struct { + FloatingIP *FloatingIP + Action *Action +} + +// Create creates a Floating IP. +func (c *FloatingIPClient) Create(ctx context.Context, opts FloatingIPCreateOpts) (FloatingIPCreateResult, *Response, error) { + if err := opts.Validate(); err != nil { + return FloatingIPCreateResult{}, nil, err + } + + reqBody := schema.FloatingIPCreateRequest{ + Type: string(opts.Type), + Description: opts.Description, + Name: opts.Name, + } + if opts.HomeLocation != nil { + reqBody.HomeLocation = String(opts.HomeLocation.Name) + } + if opts.Server != nil { + reqBody.Server = Int(opts.Server.ID) + } + if opts.Labels != nil { + reqBody.Labels = &opts.Labels + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return FloatingIPCreateResult{}, nil, err + } + + req, err := c.client.NewRequest(ctx, "POST", "/floating_ips", bytes.NewReader(reqBodyData)) + if err != nil { + return FloatingIPCreateResult{}, nil, err + } + + var respBody schema.FloatingIPCreateResponse + resp, err := c.client.Do(req, &respBody) + if err != nil { + return FloatingIPCreateResult{}, resp, err + } + var action *Action + if respBody.Action != nil { + action = ActionFromSchema(*respBody.Action) + } + return FloatingIPCreateResult{ + FloatingIP: FloatingIPFromSchema(respBody.FloatingIP), + Action: action, + }, resp, nil +} + +// Delete deletes a Floating IP. +func (c *FloatingIPClient) Delete(ctx context.Context, floatingIP *FloatingIP) (*Response, error) { + req, err := c.client.NewRequest(ctx, "DELETE", fmt.Sprintf("/floating_ips/%d", floatingIP.ID), nil) + if err != nil { + return nil, err + } + return c.client.Do(req, nil) +} + +// FloatingIPUpdateOpts specifies options for updating a Floating IP. +type FloatingIPUpdateOpts struct { + Description string + Labels map[string]string + Name string +} + +// Update updates a Floating IP. +func (c *FloatingIPClient) Update(ctx context.Context, floatingIP *FloatingIP, opts FloatingIPUpdateOpts) (*FloatingIP, *Response, error) { + reqBody := schema.FloatingIPUpdateRequest{ + Description: opts.Description, + 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("/floating_ips/%d", floatingIP.ID) + req, err := c.client.NewRequest(ctx, "PUT", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + respBody := schema.FloatingIPUpdateResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return FloatingIPFromSchema(respBody.FloatingIP), resp, nil +} + +// Assign assigns a Floating IP to a server. +func (c *FloatingIPClient) Assign(ctx context.Context, floatingIP *FloatingIP, server *Server) (*Action, *Response, error) { + reqBody := schema.FloatingIPActionAssignRequest{ + Server: server.ID, + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/floating_ips/%d/actions/assign", floatingIP.ID) + req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + var respBody schema.FloatingIPActionAssignResponse + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, nil +} + +// Unassign unassigns a Floating IP from the currently assigned server. +func (c *FloatingIPClient) Unassign(ctx context.Context, floatingIP *FloatingIP) (*Action, *Response, error) { + var reqBody schema.FloatingIPActionUnassignRequest + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/floating_ips/%d/actions/unassign", floatingIP.ID) + req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + var respBody schema.FloatingIPActionUnassignResponse + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, nil +} + +// 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, + } + 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 +} + +// FloatingIPChangeProtectionOpts specifies options for changing the resource protection level of a Floating IP. +type FloatingIPChangeProtectionOpts struct { + Delete *bool +} + +// ChangeProtection changes the resource protection level of a Floating IP. +func (c *FloatingIPClient) ChangeProtection(ctx context.Context, floatingIP *FloatingIP, opts FloatingIPChangeProtectionOpts) (*Action, *Response, error) { + reqBody := schema.FloatingIPActionChangeProtectionRequest{ + Delete: opts.Delete, + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/floating_ips/%d/actions/change_protection", floatingIP.ID) + req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + respBody := schema.FloatingIPActionChangeProtectionResponse{} + 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/hcloud-go/hcloud/hcloud.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/hcloud.go new file mode 100644 index 000000000000..23b5d874cc73 --- /dev/null +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/hcloud.go @@ -0,0 +1,21 @@ +/* +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 is a library for the Hetzner Cloud API. +package hcloud + +// Version is the library's version following Semantic Versioning. +const Version = "1.22.0" diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/helper.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/helper.go new file mode 100644 index 000000000000..537607883e46 --- /dev/null +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/helper.go @@ -0,0 +1,34 @@ +/* +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 "time" + +// String returns a pointer to the passed string s. +func String(s string) *string { return &s } + +// Int returns a pointer to the passed integer i. +func Int(i int) *int { return &i } + +// Bool returns a pointer to the passed bool b. +func Bool(b bool) *bool { return &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 } diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/image.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/image.go new file mode 100644 index 000000000000..a20bd5596eb3 --- /dev/null +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/image.go @@ -0,0 +1,284 @@ +/* +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" + "fmt" + "net/url" + "strconv" + "time" + + "k8s.io/autoscaler/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema" +) + +// Image represents an Image in the Hetzner Cloud. +type Image struct { + ID int + Name string + Type ImageType + Status ImageStatus + Description string + ImageSize float32 + DiskSize float32 + Created time.Time + CreatedFrom *Server + BoundTo *Server + RapidDeploy bool + + OSFlavor string + OSVersion string + + Protection ImageProtection + Deprecated time.Time // The zero value denotes the image is not deprecated. + Labels map[string]string +} + +// IsDeprecated returns whether the image is deprecated. +func (image *Image) IsDeprecated() bool { + return !image.Deprecated.IsZero() +} + +// ImageProtection represents the protection level of an image. +type ImageProtection struct { + Delete bool +} + +// ImageType specifies the type of an image. +type ImageType string + +const ( + // ImageTypeSnapshot represents a snapshot image. + ImageTypeSnapshot ImageType = "snapshot" + // ImageTypeBackup represents a backup image. + ImageTypeBackup ImageType = "backup" + // ImageTypeSystem represents a system image. + ImageTypeSystem ImageType = "system" +) + +// ImageStatus specifies the status of an image. +type ImageStatus string + +const ( + // ImageStatusCreating is the status when an image is being created. + ImageStatusCreating ImageStatus = "creating" + // ImageStatusAvailable is the status when an image is available. + ImageStatusAvailable ImageStatus = "available" +) + +// ImageClient is a client for the image API. +type ImageClient struct { + client *Client +} + +// GetByID retrieves an image by its ID. If the image does not exist, nil is returned. +func (c *ImageClient) GetByID(ctx context.Context, id int) (*Image, *Response, error) { + req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/images/%d", id), nil) + if err != nil { + return nil, nil, err + } + + var body schema.ImageGetResponse + resp, err := c.client.Do(req, &body) + if err != nil { + if IsError(err, ErrorCodeNotFound) { + return nil, resp, nil + } + return nil, nil, err + } + return ImageFromSchema(body.Image), resp, nil +} + +// GetByName retrieves an image by its name. If the image does not exist, nil is returned. +func (c *ImageClient) GetByName(ctx context.Context, name string) (*Image, *Response, error) { + if name == "" { + return nil, nil, nil + } + images, response, err := c.List(ctx, ImageListOpts{Name: name}) + 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. +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.GetByName(ctx, idOrName) +} + +// ImageListOpts specifies options for listing images. +type ImageListOpts struct { + ListOpts + Type []ImageType + BoundTo *Server + Name string + Sort []string + Status []ImageStatus + IncludeDeprecated bool +} + +func (l ImageListOpts) values() url.Values { + vals := l.ListOpts.values() + for _, typ := range l.Type { + vals.Add("type", string(typ)) + } + if l.BoundTo != nil { + vals.Add("bound_to", strconv.Itoa(l.BoundTo.ID)) + } + if l.Name != "" { + vals.Add("name", l.Name) + } + if l.IncludeDeprecated { + vals.Add("include_deprecated", strconv.FormatBool(l.IncludeDeprecated)) + } + for _, sort := range l.Sort { + vals.Add("sort", sort) + } + for _, status := range l.Status { + vals.Add("status", string(status)) + } + return vals +} + +// List returns a list of images 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 *ImageClient) List(ctx context.Context, opts ImageListOpts) ([]*Image, *Response, error) { + path := "/images?" + opts.values().Encode() + req, err := c.client.NewRequest(ctx, "GET", path, nil) + if err != nil { + return nil, nil, err + } + + var body schema.ImageListResponse + resp, err := c.client.Do(req, &body) + if err != nil { + return nil, nil, err + } + images := make([]*Image, 0, len(body.Images)) + for _, i := range body.Images { + images = append(images, ImageFromSchema(i)) + } + return images, resp, nil +} + +// All returns all images. +func (c *ImageClient) All(ctx context.Context) ([]*Image, error) { + return c.AllWithOpts(ctx, ImageListOpts{ListOpts: ListOpts{PerPage: 50}}) +} + +// AllWithOpts returns all images for the given options. +func (c *ImageClient) AllWithOpts(ctx context.Context, opts ImageListOpts) ([]*Image, error) { + allImages := []*Image{} + + _, err := c.client.all(func(page int) (*Response, error) { + opts.Page = page + images, resp, err := c.List(ctx, opts) + if err != nil { + return resp, err + } + allImages = append(allImages, images...) + return resp, nil + }) + if err != nil { + return nil, err + } + + return allImages, nil +} + +// Delete deletes an image. +func (c *ImageClient) Delete(ctx context.Context, image *Image) (*Response, error) { + req, err := c.client.NewRequest(ctx, "DELETE", fmt.Sprintf("/images/%d", image.ID), nil) + if err != nil { + return nil, err + } + return c.client.Do(req, nil) +} + +// ImageUpdateOpts specifies options for updating an image. +type ImageUpdateOpts struct { + Description *string + Type ImageType + Labels map[string]string +} + +// Update updates an image. +func (c *ImageClient) Update(ctx context.Context, image *Image, opts ImageUpdateOpts) (*Image, *Response, error) { + reqBody := schema.ImageUpdateRequest{ + Description: opts.Description, + } + if opts.Type != "" { + reqBody.Type = String(string(opts.Type)) + } + if opts.Labels != nil { + reqBody.Labels = &opts.Labels + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/images/%d", image.ID) + req, err := c.client.NewRequest(ctx, "PUT", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + respBody := schema.ImageUpdateResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ImageFromSchema(respBody.Image), resp, nil +} + +// ImageChangeProtectionOpts specifies options for changing the resource protection level of an image. +type ImageChangeProtectionOpts struct { + Delete *bool +} + +// ChangeProtection changes the resource protection level of an image. +func (c *ImageClient) ChangeProtection(ctx context.Context, image *Image, opts ImageChangeProtectionOpts) (*Action, *Response, error) { + reqBody := schema.ImageActionChangeProtectionRequest{ + Delete: opts.Delete, + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/images/%d/actions/change_protection", image.ID) + req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + respBody := schema.ImageActionChangeProtectionResponse{} + 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/hcloud-go/hcloud/iso.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/iso.go new file mode 100644 index 000000000000..4932e44c3304 --- /dev/null +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/iso.go @@ -0,0 +1,155 @@ +/* +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/url" + "strconv" + "time" + + "k8s.io/autoscaler/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema" +) + +// ISO represents an ISO image in the Hetzner Cloud. +type ISO struct { + ID int + Name string + Description string + Type ISOType + Deprecated time.Time +} + +// IsDeprecated returns true if the ISO is deprecated +func (iso *ISO) IsDeprecated() bool { + return !iso.Deprecated.IsZero() +} + +// ISOType specifies the type of an ISO image. +type ISOType string + +const ( + // ISOTypePublic is the type of a public ISO image. + ISOTypePublic ISOType = "public" + + // ISOTypePrivate is the type of a private ISO image. + ISOTypePrivate ISOType = "private" +) + +// ISOClient is a client for the ISO API. +type ISOClient struct { + client *Client +} + +// GetByID retrieves an ISO by its ID. +func (c *ISOClient) GetByID(ctx context.Context, id int) (*ISO, *Response, error) { + req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/isos/%d", id), nil) + if err != nil { + return nil, nil, err + } + + var body schema.ISOGetResponse + resp, err := c.client.Do(req, &body) + if err != nil { + if IsError(err, ErrorCodeNotFound) { + return nil, resp, nil + } + return nil, resp, err + } + return ISOFromSchema(body.ISO), resp, nil +} + +// GetByName retrieves an ISO by its name. +func (c *ISOClient) GetByName(ctx context.Context, name string) (*ISO, *Response, error) { + if name == "" { + return nil, nil, nil + } + isos, response, err := c.List(ctx, ISOListOpts{Name: name}) + if len(isos) == 0 { + return nil, response, err + } + return isos[0], response, err +} + +// Get retrieves an ISO by its ID if the input can be parsed as an integer, otherwise it retrieves an ISO by its name. +func (c *ISOClient) Get(ctx context.Context, idOrName string) (*ISO, *Response, error) { + if id, err := strconv.Atoi(idOrName); err == nil { + return c.GetByID(ctx, int(id)) + } + return c.GetByName(ctx, idOrName) +} + +// ISOListOpts specifies options for listing isos. +type ISOListOpts struct { + ListOpts + Name string +} + +func (l ISOListOpts) values() url.Values { + vals := l.ListOpts.values() + if l.Name != "" { + vals.Add("name", l.Name) + } + return vals +} + +// List returns a list of ISOs 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 *ISOClient) List(ctx context.Context, opts ISOListOpts) ([]*ISO, *Response, error) { + path := "/isos?" + opts.values().Encode() + req, err := c.client.NewRequest(ctx, "GET", path, nil) + if err != nil { + return nil, nil, err + } + + var body schema.ISOListResponse + resp, err := c.client.Do(req, &body) + if err != nil { + return nil, nil, err + } + isos := make([]*ISO, 0, len(body.ISOs)) + for _, i := range body.ISOs { + isos = append(isos, ISOFromSchema(i)) + } + return isos, resp, nil +} + +// All returns all ISOs. +func (c *ISOClient) All(ctx context.Context) ([]*ISO, error) { + allISOs := []*ISO{} + + opts := ISOListOpts{} + opts.PerPage = 50 + + _, err := c.client.all(func(page int) (*Response, error) { + opts.Page = page + isos, resp, err := c.List(ctx, opts) + if err != nil { + return resp, err + } + allISOs = append(allISOs, isos...) + return resp, nil + }) + if err != nil { + return nil, err + } + + return allISOs, nil +} diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/load_balancer.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/load_balancer.go new file mode 100644 index 000000000000..3e25feb9f61f --- /dev/null +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/load_balancer.go @@ -0,0 +1,949 @@ +/* +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" + "fmt" + "net" + "net/url" + "strconv" + "time" + + "k8s.io/autoscaler/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema" +) + +// LoadBalancer represents a Load Balancer in the Hetzner Cloud. +type LoadBalancer struct { + ID int + Name string + PublicNet LoadBalancerPublicNet + PrivateNet []LoadBalancerPrivateNet + Location *Location + LoadBalancerType *LoadBalancerType + Algorithm LoadBalancerAlgorithm + Services []LoadBalancerService + Targets []LoadBalancerTarget + Protection LoadBalancerProtection + Labels map[string]string + Created time.Time + IncludedTraffic uint64 + OutgoingTraffic uint64 + IngoingTraffic uint64 +} + +// LoadBalancerPublicNet represents a Load Balancer's public network. +type LoadBalancerPublicNet struct { + Enabled bool + IPv4 LoadBalancerPublicNetIPv4 + IPv6 LoadBalancerPublicNetIPv6 +} + +// LoadBalancerPublicNetIPv4 represents a Load Balancer's public IPv4 address. +type LoadBalancerPublicNetIPv4 struct { + IP net.IP +} + +// LoadBalancerPublicNetIPv6 represents a Load Balancer's public IPv6 address. +type LoadBalancerPublicNetIPv6 struct { + IP net.IP +} + +// LoadBalancerPrivateNet represents a Load Balancer's private network. +type LoadBalancerPrivateNet struct { + Network *Network + IP net.IP +} + +// LoadBalancerService represents a Load Balancer service. +type LoadBalancerService struct { + Protocol LoadBalancerServiceProtocol + ListenPort int + DestinationPort int + Proxyprotocol bool + HTTP LoadBalancerServiceHTTP + HealthCheck LoadBalancerServiceHealthCheck +} + +// LoadBalancerServiceHTTP stores configuration for a service using the HTTP protocol. +type LoadBalancerServiceHTTP struct { + CookieName string + CookieLifetime time.Duration + Certificates []*Certificate + RedirectHTTP bool + StickySessions bool +} + +// LoadBalancerServiceHealthCheck stores configuration for a service health check. +type LoadBalancerServiceHealthCheck struct { + Protocol LoadBalancerServiceProtocol + Port int + Interval time.Duration + Timeout time.Duration + Retries int + HTTP *LoadBalancerServiceHealthCheckHTTP +} + +// LoadBalancerServiceHealthCheckHTTP stores configuration for a service health check +// using the HTTP protocol. +type LoadBalancerServiceHealthCheckHTTP struct { + Domain string + Path string + Response string + StatusCodes []string + TLS bool +} + +// LoadBalancerAlgorithmType specifies the algorithm type a Load Balancer +// uses for distributing requests. +type LoadBalancerAlgorithmType string + +const ( + // LoadBalancerAlgorithmTypeRoundRobin is an algorithm which distributes + // requests to targets in a round robin fashion. + LoadBalancerAlgorithmTypeRoundRobin LoadBalancerAlgorithmType = "round_robin" + // LoadBalancerAlgorithmTypeLeastConnections is an algorithm which distributes + // requests to targets with the least number of connections. + LoadBalancerAlgorithmTypeLeastConnections LoadBalancerAlgorithmType = "least_connections" +) + +// LoadBalancerAlgorithm configures the algorithm a Load Balancer uses +// for distributing requests. +type LoadBalancerAlgorithm struct { + Type LoadBalancerAlgorithmType +} + +// LoadBalancerTargetType specifies the type of a Load Balancer target. +type LoadBalancerTargetType string + +const ( + // LoadBalancerTargetTypeServer is a target type which points to a specific + // server. + LoadBalancerTargetTypeServer LoadBalancerTargetType = "server" + + // LoadBalancerTargetTypeLabelSelector is a target type which selects the + // servers a Load Balancer points to using labels assigned to the servers. + LoadBalancerTargetTypeLabelSelector LoadBalancerTargetType = "label_selector" + + // LoadBalancerTargetTypeIP is a target type which points to an IP. + LoadBalancerTargetTypeIP LoadBalancerTargetType = "ip" +) + +// LoadBalancerServiceProtocol specifies the protocol of a Load Balancer service. +type LoadBalancerServiceProtocol string + +const ( + // LoadBalancerServiceProtocolTCP specifies a TCP service. + LoadBalancerServiceProtocolTCP LoadBalancerServiceProtocol = "tcp" + // LoadBalancerServiceProtocolHTTP specifies an HTTP service. + LoadBalancerServiceProtocolHTTP LoadBalancerServiceProtocol = "http" + // LoadBalancerServiceProtocolHTTPS specifies an HTTPS service. + LoadBalancerServiceProtocolHTTPS LoadBalancerServiceProtocol = "https" +) + +// LoadBalancerTarget represents a Load Balancer target. +type LoadBalancerTarget struct { + Type LoadBalancerTargetType + Server *LoadBalancerTargetServer + LabelSelector *LoadBalancerTargetLabelSelector + IP *LoadBalancerTargetIP + HealthStatus []LoadBalancerTargetHealthStatus + Targets []LoadBalancerTarget + UsePrivateIP bool +} + +// LoadBalancerTargetServer configures a Load Balancer target +// pointing at a specific server. +type LoadBalancerTargetServer struct { + Server *Server +} + +// LoadBalancerTargetLabelSelector configures a Load Balancer target pointing +// at the servers matching the selector. This includes the target pointing at +// nothing, if no servers match the Selector. +type LoadBalancerTargetLabelSelector struct { + Selector string +} + +// LoadBalancerTargetIP configures a Load Balancer target pointing to a Hetzner +// Online IP address. +type LoadBalancerTargetIP struct { + IP string +} + +// LoadBalancerTargetHealthStatusStatus describes a target's health status. +type LoadBalancerTargetHealthStatusStatus string + +const ( + // LoadBalancerTargetHealthStatusStatusUnknown denotes that the health status is unknown. + LoadBalancerTargetHealthStatusStatusUnknown LoadBalancerTargetHealthStatusStatus = "unknown" + // LoadBalancerTargetHealthStatusStatusHealthy denotes a healthy target. + LoadBalancerTargetHealthStatusStatusHealthy LoadBalancerTargetHealthStatusStatus = "healthy" + // LoadBalancerTargetHealthStatusStatusUnhealthy denotes an unhealthy target. + LoadBalancerTargetHealthStatusStatusUnhealthy LoadBalancerTargetHealthStatusStatus = "unhealthy" +) + +// LoadBalancerTargetHealthStatus describes a target's health for a specific service. +type LoadBalancerTargetHealthStatus struct { + ListenPort int + Status LoadBalancerTargetHealthStatusStatus +} + +// LoadBalancerProtection represents the protection level of a Load Balancer. +type LoadBalancerProtection struct { + Delete bool +} + +// LoadBalancerClient is a client for the Load Balancers API. +type LoadBalancerClient struct { + client *Client +} + +// GetByID retrieves a Load Balancer by its ID. If the Load Balancer does not exist, nil is returned. +func (c *LoadBalancerClient) GetByID(ctx context.Context, id int) (*LoadBalancer, *Response, error) { + req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/load_balancers/%d", id), nil) + if err != nil { + return nil, nil, err + } + + var body schema.LoadBalancerGetResponse + resp, err := c.client.Do(req, &body) + if err != nil { + if IsError(err, ErrorCodeNotFound) { + return nil, resp, nil + } + return nil, nil, err + } + return LoadBalancerFromSchema(body.LoadBalancer), resp, nil +} + +// GetByName retrieves a Load Balancer by its name. If the Load Balancer does not exist, nil is returned. +func (c *LoadBalancerClient) GetByName(ctx context.Context, name string) (*LoadBalancer, *Response, error) { + if name == "" { + return nil, nil, nil + } + LoadBalancer, response, err := c.List(ctx, LoadBalancerListOpts{Name: name}) + if len(LoadBalancer) == 0 { + return nil, response, err + } + return LoadBalancer[0], response, err +} + +// Get retrieves a Load Balancer by its ID if the input can be parsed as an integer, otherwise it +// retrieves a Load Balancer by its name. If the Load Balancer does not exist, nil is returned. +func (c *LoadBalancerClient) Get(ctx context.Context, idOrName string) (*LoadBalancer, *Response, error) { + if id, err := strconv.Atoi(idOrName); err == nil { + return c.GetByID(ctx, int(id)) + } + return c.GetByName(ctx, idOrName) +} + +// LoadBalancerListOpts specifies options for listing Load Balancers. +type LoadBalancerListOpts struct { + ListOpts + Name string +} + +func (l LoadBalancerListOpts) values() url.Values { + vals := l.ListOpts.values() + if l.Name != "" { + vals.Add("name", l.Name) + } + return vals +} + +// List returns a list of Load Balancers 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 *LoadBalancerClient) List(ctx context.Context, opts LoadBalancerListOpts) ([]*LoadBalancer, *Response, error) { + path := "/load_balancers?" + opts.values().Encode() + req, err := c.client.NewRequest(ctx, "GET", path, nil) + if err != nil { + return nil, nil, err + } + + var body schema.LoadBalancerListResponse + resp, err := c.client.Do(req, &body) + if err != nil { + return nil, nil, err + } + LoadBalancers := make([]*LoadBalancer, 0, len(body.LoadBalancers)) + for _, s := range body.LoadBalancers { + LoadBalancers = append(LoadBalancers, LoadBalancerFromSchema(s)) + } + return LoadBalancers, resp, nil +} + +// All returns all Load Balancers. +func (c *LoadBalancerClient) All(ctx context.Context) ([]*LoadBalancer, error) { + allLoadBalancer := []*LoadBalancer{} + + opts := LoadBalancerListOpts{} + opts.PerPage = 50 + + _, err := c.client.all(func(page int) (*Response, error) { + opts.Page = page + LoadBalancer, resp, err := c.List(ctx, opts) + if err != nil { + return resp, err + } + allLoadBalancer = append(allLoadBalancer, LoadBalancer...) + return resp, nil + }) + if err != nil { + return nil, err + } + + return allLoadBalancer, nil +} + +// AllWithOpts returns all Load Balancers for the given options. +func (c *LoadBalancerClient) AllWithOpts(ctx context.Context, opts LoadBalancerListOpts) ([]*LoadBalancer, error) { + var allLoadBalancers []*LoadBalancer + + _, err := c.client.all(func(page int) (*Response, error) { + opts.Page = page + LoadBalancers, resp, err := c.List(ctx, opts) + if err != nil { + return resp, err + } + allLoadBalancers = append(allLoadBalancers, LoadBalancers...) + return resp, nil + }) + if err != nil { + return nil, err + } + + return allLoadBalancers, nil +} + +// LoadBalancerUpdateOpts specifies options for updating a Load Balancer. +type LoadBalancerUpdateOpts struct { + Name string + Labels map[string]string +} + +// Update updates a Load Balancer. +func (c *LoadBalancerClient) Update(ctx context.Context, loadBalancer *LoadBalancer, opts LoadBalancerUpdateOpts) (*LoadBalancer, *Response, error) { + reqBody := schema.LoadBalancerUpdateRequest{} + 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("/load_balancers/%d", loadBalancer.ID) + req, err := c.client.NewRequest(ctx, "PUT", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + respBody := schema.LoadBalancerUpdateResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return LoadBalancerFromSchema(respBody.LoadBalancer), resp, nil +} + +// LoadBalancerCreateOpts specifies options for creating a new Load Balancer. +type LoadBalancerCreateOpts struct { + Name string + LoadBalancerType *LoadBalancerType + Algorithm *LoadBalancerAlgorithm + Location *Location + NetworkZone NetworkZone + Labels map[string]string + Targets []LoadBalancerCreateOptsTarget + Services []LoadBalancerCreateOptsService + PublicInterface *bool + Network *Network +} + +// LoadBalancerCreateOptsTarget holds options for specifying a target +// when creating a new Load Balancer. +type LoadBalancerCreateOptsTarget struct { + Type LoadBalancerTargetType + Server LoadBalancerCreateOptsTargetServer + LabelSelector LoadBalancerCreateOptsTargetLabelSelector + IP LoadBalancerCreateOptsTargetIP + UsePrivateIP *bool +} + +// LoadBalancerCreateOptsTargetServer holds options for specifying a server target +// when creating a new Load Balancer. +type LoadBalancerCreateOptsTargetServer struct { + Server *Server +} + +// LoadBalancerCreateOptsTargetLabelSelector holds options for specifying a label selector target +// when creating a new Load Balancer. +type LoadBalancerCreateOptsTargetLabelSelector struct { + Selector string +} + +// LoadBalancerCreateOptsTargetIP holds options for specifying an IP target +// when creating a new Load Balancer. +type LoadBalancerCreateOptsTargetIP struct { + IP string +} + +// LoadBalancerCreateOptsService holds options for specifying a service +// when creating a new Load Balancer. +type LoadBalancerCreateOptsService struct { + Protocol LoadBalancerServiceProtocol + ListenPort *int + DestinationPort *int + Proxyprotocol *bool + HTTP *LoadBalancerCreateOptsServiceHTTP + HealthCheck *LoadBalancerCreateOptsServiceHealthCheck +} + +// LoadBalancerCreateOptsServiceHTTP holds options for specifying an HTTP service +// when creating a new Load Balancer. +type LoadBalancerCreateOptsServiceHTTP struct { + CookieName *string + CookieLifetime *time.Duration + Certificates []*Certificate + RedirectHTTP *bool + StickySessions *bool +} + +// LoadBalancerCreateOptsServiceHealthCheck holds options for specifying a service +// health check when creating a new Load Balancer. +type LoadBalancerCreateOptsServiceHealthCheck struct { + Protocol LoadBalancerServiceProtocol + Port *int + Interval *time.Duration + Timeout *time.Duration + Retries *int + HTTP *LoadBalancerCreateOptsServiceHealthCheckHTTP +} + +// LoadBalancerCreateOptsServiceHealthCheckHTTP holds options for specifying a service +// HTTP health check when creating a new Load Balancer. +type LoadBalancerCreateOptsServiceHealthCheckHTTP struct { + Domain *string + Path *string + Response *string + StatusCodes []string + TLS *bool +} + +// LoadBalancerCreateResult is the result of a create Load Balancer call. +type LoadBalancerCreateResult struct { + LoadBalancer *LoadBalancer + Action *Action +} + +// Create creates a new Load Balancer. +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 + } + req, err := c.client.NewRequest(ctx, "POST", "/load_balancers", bytes.NewReader(reqBodyData)) + if err != nil { + return LoadBalancerCreateResult{}, nil, err + } + + respBody := schema.LoadBalancerCreateResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return LoadBalancerCreateResult{}, resp, err + } + return LoadBalancerCreateResult{ + LoadBalancer: LoadBalancerFromSchema(respBody.LoadBalancer), + Action: ActionFromSchema(respBody.Action), + }, resp, nil +} + +// Delete deletes a Load Balancer. +func (c *LoadBalancerClient) Delete(ctx context.Context, loadBalancer *LoadBalancer) (*Response, error) { + req, err := c.client.NewRequest(ctx, "DELETE", fmt.Sprintf("/load_balancers/%d", loadBalancer.ID), nil) + if err != nil { + return nil, err + } + return c.client.Do(req, nil) +} + +func (c *LoadBalancerClient) addTarget(ctx context.Context, loadBalancer *LoadBalancer, reqBody schema.LoadBalancerActionAddTargetRequest) (*Action, *Response, error) { + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/load_balancers/%d/actions/add_target", loadBalancer.ID) + req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + var respBody schema.LoadBalancerActionAddTargetResponse + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, nil +} + +func (c *LoadBalancerClient) removeTarget(ctx context.Context, loadBalancer *LoadBalancer, reqBody schema.LoadBalancerActionRemoveTargetRequest) (*Action, *Response, error) { + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/load_balancers/%d/actions/remove_target", loadBalancer.ID) + req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + var respBody schema.LoadBalancerActionRemoveTargetResponse + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, nil +} + +// LoadBalancerAddServerTargetOpts specifies options for adding a server target +// to a Load Balancer. +type LoadBalancerAddServerTargetOpts struct { + Server *Server + UsePrivateIP *bool +} + +// AddServerTarget adds a server target to a Load Balancer. +func (c *LoadBalancerClient) AddServerTarget(ctx context.Context, loadBalancer *LoadBalancer, opts LoadBalancerAddServerTargetOpts) (*Action, *Response, error) { + reqBody := schema.LoadBalancerActionAddTargetRequest{ + Type: string(LoadBalancerTargetTypeServer), + Server: &schema.LoadBalancerActionAddTargetRequestServer{ + ID: opts.Server.ID, + }, + UsePrivateIP: opts.UsePrivateIP, + } + return c.addTarget(ctx, loadBalancer, reqBody) +} + +// RemoveServerTarget removes a server target from a Load Balancer. +func (c *LoadBalancerClient) RemoveServerTarget(ctx context.Context, loadBalancer *LoadBalancer, server *Server) (*Action, *Response, error) { + reqBody := schema.LoadBalancerActionRemoveTargetRequest{ + Type: string(LoadBalancerTargetTypeServer), + Server: &schema.LoadBalancerActionRemoveTargetRequestServer{ + ID: server.ID, + }, + } + return c.removeTarget(ctx, loadBalancer, reqBody) +} + +// LoadBalancerAddLabelSelectorTargetOpts specifies options for adding a label selector target +// to a Load Balancer. +type LoadBalancerAddLabelSelectorTargetOpts struct { + Selector string + UsePrivateIP *bool +} + +// AddLabelSelectorTarget adds a label selector target to a Load Balancer. +func (c *LoadBalancerClient) AddLabelSelectorTarget(ctx context.Context, loadBalancer *LoadBalancer, opts LoadBalancerAddLabelSelectorTargetOpts) (*Action, *Response, error) { + reqBody := schema.LoadBalancerActionAddTargetRequest{ + Type: string(LoadBalancerTargetTypeLabelSelector), + LabelSelector: &schema.LoadBalancerActionAddTargetRequestLabelSelector{ + Selector: opts.Selector, + }, + UsePrivateIP: opts.UsePrivateIP, + } + return c.addTarget(ctx, loadBalancer, reqBody) +} + +// RemoveLabelSelectorTarget removes a label selector target from a Load Balancer. +func (c *LoadBalancerClient) RemoveLabelSelectorTarget(ctx context.Context, loadBalancer *LoadBalancer, labelSelector string) (*Action, *Response, error) { + reqBody := schema.LoadBalancerActionRemoveTargetRequest{ + Type: string(LoadBalancerTargetTypeLabelSelector), + LabelSelector: &schema.LoadBalancerActionRemoveTargetRequestLabelSelector{ + Selector: labelSelector, + }, + } + return c.removeTarget(ctx, loadBalancer, reqBody) +} + +// LoadBalancerAddIPTargetOpts specifies options for adding an IP target to a +// Load Balancer. +type LoadBalancerAddIPTargetOpts struct { + IP net.IP +} + +// AddIPTarget adds an IP target to a Load Balancer. +func (c *LoadBalancerClient) AddIPTarget(ctx context.Context, loadBalancer *LoadBalancer, opts LoadBalancerAddIPTargetOpts) (*Action, *Response, error) { + reqBody := schema.LoadBalancerActionAddTargetRequest{ + Type: string(LoadBalancerTargetTypeIP), + IP: &schema.LoadBalancerActionAddTargetRequestIP{IP: opts.IP.String()}, + } + return c.addTarget(ctx, loadBalancer, reqBody) +} + +// RemoveIPTarget removes an IP target from a Load Balancer. +func (c *LoadBalancerClient) RemoveIPTarget(ctx context.Context, loadBalancer *LoadBalancer, ip net.IP) (*Action, *Response, error) { + reqBody := schema.LoadBalancerActionRemoveTargetRequest{ + Type: string(LoadBalancerTargetTypeIP), + IP: &schema.LoadBalancerActionRemoveTargetRequestIP{ + IP: ip.String(), + }, + } + return c.removeTarget(ctx, loadBalancer, reqBody) +} + +// LoadBalancerAddServiceOpts specifies options for adding a service to a Load Balancer. +type LoadBalancerAddServiceOpts struct { + Protocol LoadBalancerServiceProtocol + ListenPort *int + DestinationPort *int + Proxyprotocol *bool + HTTP *LoadBalancerAddServiceOptsHTTP + HealthCheck *LoadBalancerAddServiceOptsHealthCheck +} + +// LoadBalancerAddServiceOptsHTTP holds options for specifying an HTTP service +// when adding a service to a Load Balancer. +type LoadBalancerAddServiceOptsHTTP struct { + CookieName *string + CookieLifetime *time.Duration + Certificates []*Certificate + RedirectHTTP *bool + StickySessions *bool +} + +// LoadBalancerAddServiceOptsHealthCheck holds options for specifying an health check +// when adding a service to a Load Balancer. +type LoadBalancerAddServiceOptsHealthCheck struct { + Protocol LoadBalancerServiceProtocol + Port *int + Interval *time.Duration + Timeout *time.Duration + Retries *int + HTTP *LoadBalancerAddServiceOptsHealthCheckHTTP +} + +// LoadBalancerAddServiceOptsHealthCheckHTTP holds options for specifying an +// HTTP health check when adding a service to a Load Balancer. +type LoadBalancerAddServiceOptsHealthCheckHTTP struct { + Domain *string + Path *string + Response *string + StatusCodes []string + TLS *bool +} + +// AddService adds a service to a Load Balancer. +func (c *LoadBalancerClient) AddService(ctx context.Context, loadBalancer *LoadBalancer, opts LoadBalancerAddServiceOpts) (*Action, *Response, error) { + reqBody := loadBalancerAddServiceOptsToSchema(opts) + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/load_balancers/%d/actions/add_service", loadBalancer.ID) + req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + var respBody schema.LoadBalancerActionAddServiceResponse + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, nil +} + +// LoadBalancerUpdateServiceOpts specifies options for updating a service. +type LoadBalancerUpdateServiceOpts struct { + Protocol LoadBalancerServiceProtocol + DestinationPort *int + Proxyprotocol *bool + HTTP *LoadBalancerUpdateServiceOptsHTTP + HealthCheck *LoadBalancerUpdateServiceOptsHealthCheck +} + +// LoadBalancerUpdateServiceOptsHTTP specifies options for updating an HTTP(S) service. +type LoadBalancerUpdateServiceOptsHTTP struct { + CookieName *string + CookieLifetime *time.Duration + Certificates []*Certificate + RedirectHTTP *bool + StickySessions *bool +} + +// LoadBalancerUpdateServiceOptsHealthCheck specifies options for updating +// a service's health check. +type LoadBalancerUpdateServiceOptsHealthCheck struct { + Protocol LoadBalancerServiceProtocol + Port *int + Interval *time.Duration + Timeout *time.Duration + Retries *int + HTTP *LoadBalancerUpdateServiceOptsHealthCheckHTTP +} + +// LoadBalancerUpdateServiceOptsHealthCheckHTTP specifies options for updating +// the HTTP-specific settings of a service's health check. +type LoadBalancerUpdateServiceOptsHealthCheckHTTP struct { + Domain *string + Path *string + Response *string + StatusCodes []string + TLS *bool +} + +// UpdateService updates a Load Balancer service. +func (c *LoadBalancerClient) UpdateService(ctx context.Context, loadBalancer *LoadBalancer, listenPort int, opts LoadBalancerUpdateServiceOpts) (*Action, *Response, error) { + reqBody := loadBalancerUpdateServiceOptsToSchema(opts) + reqBody.ListenPort = listenPort + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/load_balancers/%d/actions/update_service", loadBalancer.ID) + req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + var respBody schema.LoadBalancerActionUpdateServiceResponse + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, nil +} + +// DeleteService deletes a Load Balancer service. +func (c *LoadBalancerClient) DeleteService(ctx context.Context, loadBalancer *LoadBalancer, listenPort int) (*Action, *Response, error) { + reqBody := schema.LoadBalancerDeleteServiceRequest{ + ListenPort: listenPort, + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/load_balancers/%d/actions/delete_service", loadBalancer.ID) + req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + var respBody schema.LoadBalancerDeleteServiceResponse + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, nil +} + +// LoadBalancerChangeProtectionOpts specifies options for changing the resource protection level of a Load Balancer. +type LoadBalancerChangeProtectionOpts struct { + Delete *bool +} + +// ChangeProtection changes the resource protection level of a Load Balancer. +func (c *LoadBalancerClient) ChangeProtection(ctx context.Context, loadBalancer *LoadBalancer, opts LoadBalancerChangeProtectionOpts) (*Action, *Response, error) { + reqBody := schema.LoadBalancerActionChangeProtectionRequest{ + Delete: opts.Delete, + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/load_balancers/%d/actions/change_protection", loadBalancer.ID) + req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + respBody := schema.LoadBalancerActionChangeProtectionResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, err +} + +// LoadBalancerChangeAlgorithmOpts specifies options for changing the algorithm of a Load Balancer. +type LoadBalancerChangeAlgorithmOpts struct { + Type LoadBalancerAlgorithmType +} + +// ChangeAlgorithm changes the algorithm of a Load Balancer. +func (c *LoadBalancerClient) ChangeAlgorithm(ctx context.Context, loadBalancer *LoadBalancer, opts LoadBalancerChangeAlgorithmOpts) (*Action, *Response, error) { + reqBody := schema.LoadBalancerActionChangeAlgorithmRequest{ + Type: string(opts.Type), + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/load_balancers/%d/actions/change_algorithm", loadBalancer.ID) + req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + respBody := schema.LoadBalancerActionChangeAlgorithmResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, err +} + +// LoadBalancerAttachToNetworkOpts specifies options for attaching a Load Balancer to a network. +type LoadBalancerAttachToNetworkOpts struct { + Network *Network + IP net.IP +} + +// AttachToNetwork attaches a Load Balancer to a network. +func (c *LoadBalancerClient) AttachToNetwork(ctx context.Context, loadBalancer *LoadBalancer, opts LoadBalancerAttachToNetworkOpts) (*Action, *Response, error) { + reqBody := schema.LoadBalancerActionAttachToNetworkRequest{ + Network: opts.Network.ID, + } + if opts.IP != nil { + reqBody.IP = String(opts.IP.String()) + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/load_balancers/%d/actions/attach_to_network", loadBalancer.ID) + req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + respBody := schema.LoadBalancerActionAttachToNetworkResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, err +} + +// LoadBalancerDetachFromNetworkOpts specifies options for detaching a Load Balancer from a network. +type LoadBalancerDetachFromNetworkOpts struct { + Network *Network +} + +// DetachFromNetwork detaches a Load Balancer from a network. +func (c *LoadBalancerClient) DetachFromNetwork(ctx context.Context, loadBalancer *LoadBalancer, opts LoadBalancerDetachFromNetworkOpts) (*Action, *Response, error) { + reqBody := schema.LoadBalancerActionDetachFromNetworkRequest{ + Network: opts.Network.ID, + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/load_balancers/%d/actions/detach_from_network", loadBalancer.ID) + req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + respBody := schema.LoadBalancerActionDetachFromNetworkResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, err +} + +// EnablePublicInterface enables the Load Balancer's public network interface. +func (c *LoadBalancerClient) EnablePublicInterface(ctx context.Context, loadBalancer *LoadBalancer) (*Action, *Response, error) { + path := fmt.Sprintf("/load_balancers/%d/actions/enable_public_interface", loadBalancer.ID) + req, err := c.client.NewRequest(ctx, "POST", path, nil) + if err != nil { + return nil, nil, err + } + respBody := schema.LoadBalancerActionEnablePublicInterfaceResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, err +} + +// DisablePublicInterface disables the Load Balancer's public network interface. +func (c *LoadBalancerClient) DisablePublicInterface(ctx context.Context, loadBalancer *LoadBalancer) (*Action, *Response, error) { + path := fmt.Sprintf("/load_balancers/%d/actions/disable_public_interface", loadBalancer.ID) + req, err := c.client.NewRequest(ctx, "POST", path, nil) + if err != nil { + return nil, nil, err + } + respBody := schema.LoadBalancerActionDisablePublicInterfaceResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, err +} + +// LoadBalancerChangeTypeOpts specifies options for changing a Load Balancer's type. +type LoadBalancerChangeTypeOpts struct { + LoadBalancerType *LoadBalancerType // new Load Balancer type +} + +// ChangeType changes a Load Balancer's type. +func (c *LoadBalancerClient) ChangeType(ctx context.Context, loadBalancer *LoadBalancer, opts LoadBalancerChangeTypeOpts) (*Action, *Response, error) { + reqBody := schema.LoadBalancerActionChangeTypeRequest{} + if opts.LoadBalancerType.ID != 0 { + reqBody.LoadBalancerType = opts.LoadBalancerType.ID + } else { + reqBody.LoadBalancerType = opts.LoadBalancerType.Name + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/load_balancers/%d/actions/change_type", loadBalancer.ID) + req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + respBody := schema.LoadBalancerActionChangeTypeResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, nil +} diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/load_balancer_type.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/load_balancer_type.go new file mode 100644 index 000000000000..d3bae99873ad --- /dev/null +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/load_balancer_type.go @@ -0,0 +1,142 @@ +/* +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/url" + "strconv" + + "k8s.io/autoscaler/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema" +) + +// LoadBalancerType represents a LoadBalancer type in the Hetzner Cloud. +type LoadBalancerType struct { + ID int + Name string + Description string + MaxConnections int + MaxServices int + MaxTargets int + MaxAssignedCertificates int + Pricings []LoadBalancerTypeLocationPricing +} + +// LoadBalancerTypeClient is a client for the Load Balancer types API. +type LoadBalancerTypeClient struct { + client *Client +} + +// GetByID retrieves a Load Balancer type by its ID. If the Load Balancer type does not exist, nil is returned. +func (c *LoadBalancerTypeClient) GetByID(ctx context.Context, id int) (*LoadBalancerType, *Response, error) { + req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/load_balancer_types/%d", id), nil) + if err != nil { + return nil, nil, err + } + + var body schema.LoadBalancerTypeGetResponse + resp, err := c.client.Do(req, &body) + if err != nil { + if IsError(err, ErrorCodeNotFound) { + return nil, resp, nil + } + return nil, nil, err + } + return LoadBalancerTypeFromSchema(body.LoadBalancerType), resp, nil +} + +// GetByName retrieves a Load Balancer type by its name. If the Load Balancer type does not exist, nil is returned. +func (c *LoadBalancerTypeClient) GetByName(ctx context.Context, name string) (*LoadBalancerType, *Response, error) { + if name == "" { + return nil, nil, nil + } + LoadBalancerTypes, response, err := c.List(ctx, LoadBalancerTypeListOpts{Name: name}) + if len(LoadBalancerTypes) == 0 { + return nil, response, err + } + return LoadBalancerTypes[0], response, err +} + +// Get retrieves a Load Balancer type by its ID if the input can be parsed as an integer, otherwise it +// retrieves a Load Balancer type by its name. If the Load Balancer type does not exist, nil is returned. +func (c *LoadBalancerTypeClient) Get(ctx context.Context, idOrName string) (*LoadBalancerType, *Response, error) { + if id, err := strconv.Atoi(idOrName); err == nil { + return c.GetByID(ctx, int(id)) + } + return c.GetByName(ctx, idOrName) +} + +// LoadBalancerTypeListOpts specifies options for listing Load Balancer types. +type LoadBalancerTypeListOpts struct { + ListOpts + Name string +} + +func (l LoadBalancerTypeListOpts) values() url.Values { + vals := l.ListOpts.values() + if l.Name != "" { + vals.Add("name", l.Name) + } + return vals +} + +// List returns a list of Load Balancer types 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 *LoadBalancerTypeClient) List(ctx context.Context, opts LoadBalancerTypeListOpts) ([]*LoadBalancerType, *Response, error) { + path := "/load_balancer_types?" + opts.values().Encode() + req, err := c.client.NewRequest(ctx, "GET", path, nil) + if err != nil { + return nil, nil, err + } + + var body schema.LoadBalancerTypeListResponse + resp, err := c.client.Do(req, &body) + if err != nil { + return nil, nil, err + } + LoadBalancerTypes := make([]*LoadBalancerType, 0, len(body.LoadBalancerTypes)) + for _, s := range body.LoadBalancerTypes { + LoadBalancerTypes = append(LoadBalancerTypes, LoadBalancerTypeFromSchema(s)) + } + return LoadBalancerTypes, resp, nil +} + +// All returns all Load Balancer types. +func (c *LoadBalancerTypeClient) All(ctx context.Context) ([]*LoadBalancerType, error) { + allLoadBalancerTypes := []*LoadBalancerType{} + + opts := LoadBalancerTypeListOpts{} + opts.PerPage = 50 + + _, err := c.client.all(func(page int) (*Response, error) { + opts.Page = page + LoadBalancerTypes, resp, err := c.List(ctx, opts) + if err != nil { + return resp, err + } + allLoadBalancerTypes = append(allLoadBalancerTypes, LoadBalancerTypes...) + return resp, nil + }) + if err != nil { + return nil, err + } + + return allLoadBalancerTypes, nil +} diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/location.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/location.go new file mode 100644 index 000000000000..e767455baea6 --- /dev/null +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/location.go @@ -0,0 +1,142 @@ +/* +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/url" + "strconv" + + "k8s.io/autoscaler/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema" +) + +// Location represents a location in the Hetzner Cloud. +type Location struct { + ID int + Name string + Description string + Country string + City string + Latitude float64 + Longitude float64 + NetworkZone NetworkZone +} + +// LocationClient is a client for the location API. +type LocationClient struct { + client *Client +} + +// GetByID retrieves a location by its ID. If the location does not exist, nil is returned. +func (c *LocationClient) GetByID(ctx context.Context, id int) (*Location, *Response, error) { + req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/locations/%d", id), nil) + if err != nil { + return nil, nil, err + } + + var body schema.LocationGetResponse + resp, err := c.client.Do(req, &body) + if err != nil { + if IsError(err, ErrorCodeNotFound) { + return nil, resp, nil + } + return nil, resp, err + } + return LocationFromSchema(body.Location), resp, nil +} + +// GetByName retrieves an location by its name. If the location does not exist, nil is returned. +func (c *LocationClient) GetByName(ctx context.Context, name string) (*Location, *Response, error) { + if name == "" { + return nil, nil, nil + } + locations, response, err := c.List(ctx, LocationListOpts{Name: name}) + if len(locations) == 0 { + return nil, response, err + } + return locations[0], response, err +} + +// Get retrieves a location by its ID if the input can be parsed as an integer, otherwise it +// retrieves a location by its name. If the location does not exist, nil is returned. +func (c *LocationClient) Get(ctx context.Context, idOrName string) (*Location, *Response, error) { + if id, err := strconv.Atoi(idOrName); err == nil { + return c.GetByID(ctx, int(id)) + } + return c.GetByName(ctx, idOrName) +} + +// LocationListOpts specifies options for listing location. +type LocationListOpts struct { + ListOpts + Name string +} + +func (l LocationListOpts) values() url.Values { + vals := l.ListOpts.values() + if l.Name != "" { + vals.Add("name", l.Name) + } + return vals +} + +// List returns a list of locations 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 *LocationClient) List(ctx context.Context, opts LocationListOpts) ([]*Location, *Response, error) { + path := "/locations?" + opts.values().Encode() + req, err := c.client.NewRequest(ctx, "GET", path, nil) + if err != nil { + return nil, nil, err + } + + var body schema.LocationListResponse + resp, err := c.client.Do(req, &body) + if err != nil { + return nil, nil, err + } + locations := make([]*Location, 0, len(body.Locations)) + for _, i := range body.Locations { + locations = append(locations, LocationFromSchema(i)) + } + return locations, resp, nil +} + +// All returns all locations. +func (c *LocationClient) All(ctx context.Context) ([]*Location, error) { + allLocations := []*Location{} + + opts := LocationListOpts{} + opts.PerPage = 50 + + _, err := c.client.all(func(page int) (*Response, error) { + opts.Page = page + locations, resp, err := c.List(ctx, opts) + if err != nil { + return resp, err + } + allLocations = append(allLocations, locations...) + return resp, nil + }) + if err != nil { + return nil, err + } + + return allLocations, nil +} diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/network.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/network.go new file mode 100644 index 000000000000..7b9876a0fd78 --- /dev/null +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/network.go @@ -0,0 +1,470 @@ +/* +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" + "net/url" + "strconv" + "time" + + "k8s.io/autoscaler/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema" +) + +// NetworkZone specifies a network zone. +type NetworkZone string + +// List of available Network Zones. +const ( + NetworkZoneEUCentral NetworkZone = "eu-central" +) + +// NetworkSubnetType specifies a type of a subnet. +type NetworkSubnetType string + +// List of available network subnet types. +const ( + NetworkSubnetTypeCloud NetworkSubnetType = "cloud" + NetworkSubnetTypeServer NetworkSubnetType = "server" +) + +// Network represents a network in the Hetzner Cloud. +type Network struct { + ID int + Name string + Created time.Time + IPRange *net.IPNet + Subnets []NetworkSubnet + Routes []NetworkRoute + Servers []*Server + Protection NetworkProtection + Labels map[string]string +} + +// NetworkSubnet represents a subnet of a network in the Hetzner Cloud. +type NetworkSubnet struct { + Type NetworkSubnetType + IPRange *net.IPNet + NetworkZone NetworkZone + Gateway net.IP +} + +// NetworkRoute represents a route of a network. +type NetworkRoute struct { + Destination *net.IPNet + Gateway net.IP +} + +// NetworkProtection represents the protection level of a network. +type NetworkProtection struct { + Delete bool +} + +// NetworkClient is a client for the network API. +type NetworkClient struct { + client *Client +} + +// GetByID retrieves a network by its ID. If the network does not exist, nil is returned. +func (c *NetworkClient) GetByID(ctx context.Context, id int) (*Network, *Response, error) { + req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/networks/%d", id), nil) + if err != nil { + return nil, nil, err + } + + var body schema.NetworkGetResponse + resp, err := c.client.Do(req, &body) + if err != nil { + if IsError(err, ErrorCodeNotFound) { + return nil, resp, nil + } + return nil, nil, err + } + return NetworkFromSchema(body.Network), resp, nil +} + +// GetByName retrieves a network by its name. If the network does not exist, nil is returned. +func (c *NetworkClient) GetByName(ctx context.Context, name string) (*Network, *Response, error) { + if name == "" { + return nil, nil, nil + } + Networks, response, err := c.List(ctx, NetworkListOpts{Name: name}) + if len(Networks) == 0 { + return nil, response, err + } + return Networks[0], response, err +} + +// Get retrieves a network by its ID if the input can be parsed as an integer, otherwise it +// retrieves a network by its name. If the network does not exist, nil is returned. +func (c *NetworkClient) Get(ctx context.Context, idOrName string) (*Network, *Response, error) { + if id, err := strconv.Atoi(idOrName); err == nil { + return c.GetByID(ctx, int(id)) + } + return c.GetByName(ctx, idOrName) +} + +// NetworkListOpts specifies options for listing networks. +type NetworkListOpts struct { + ListOpts + Name string +} + +func (l NetworkListOpts) values() url.Values { + vals := l.ListOpts.values() + if l.Name != "" { + vals.Add("name", l.Name) + } + return vals +} + +// List returns a list of networks 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 *NetworkClient) List(ctx context.Context, opts NetworkListOpts) ([]*Network, *Response, error) { + path := "/networks?" + opts.values().Encode() + req, err := c.client.NewRequest(ctx, "GET", path, nil) + if err != nil { + return nil, nil, err + } + + var body schema.NetworkListResponse + resp, err := c.client.Do(req, &body) + if err != nil { + return nil, nil, err + } + Networks := make([]*Network, 0, len(body.Networks)) + for _, s := range body.Networks { + Networks = append(Networks, NetworkFromSchema(s)) + } + return Networks, resp, nil +} + +// All returns all networks. +func (c *NetworkClient) All(ctx context.Context) ([]*Network, error) { + return c.AllWithOpts(ctx, NetworkListOpts{ListOpts: ListOpts{PerPage: 50}}) +} + +// AllWithOpts returns all networks for the given options. +func (c *NetworkClient) AllWithOpts(ctx context.Context, opts NetworkListOpts) ([]*Network, error) { + var allNetworks []*Network + + _, err := c.client.all(func(page int) (*Response, error) { + opts.Page = page + Networks, resp, err := c.List(ctx, opts) + if err != nil { + return resp, err + } + allNetworks = append(allNetworks, Networks...) + return resp, nil + }) + if err != nil { + return nil, err + } + + return allNetworks, nil +} + +// Delete deletes a network. +func (c *NetworkClient) Delete(ctx context.Context, network *Network) (*Response, error) { + req, err := c.client.NewRequest(ctx, "DELETE", fmt.Sprintf("/networks/%d", network.ID), nil) + if err != nil { + return nil, err + } + return c.client.Do(req, nil) +} + +// NetworkUpdateOpts specifies options for updating a network. +type NetworkUpdateOpts struct { + Name string + Labels map[string]string +} + +// Update updates a network. +func (c *NetworkClient) Update(ctx context.Context, network *Network, opts NetworkUpdateOpts) (*Network, *Response, error) { + reqBody := schema.NetworkUpdateRequest{ + 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("/networks/%d", network.ID) + req, err := c.client.NewRequest(ctx, "PUT", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + respBody := schema.NetworkUpdateResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return NetworkFromSchema(respBody.Network), resp, nil +} + +// NetworkCreateOpts specifies options for creating a new network. +type NetworkCreateOpts struct { + Name string + IPRange *net.IPNet + Subnets []NetworkSubnet + Routes []NetworkRoute + Labels map[string]string +} + +// Validate checks if options are valid. +func (o NetworkCreateOpts) Validate() error { + if o.Name == "" { + return errors.New("missing name") + } + if o.IPRange == nil || o.IPRange.String() == "" { + return errors.New("missing IP range") + } + return nil +} + +// Create creates a new network. +func (c *NetworkClient) Create(ctx context.Context, opts NetworkCreateOpts) (*Network, *Response, error) { + if err := opts.Validate(); err != nil { + return nil, nil, err + } + reqBody := schema.NetworkCreateRequest{ + Name: opts.Name, + IPRange: opts.IPRange.String(), + } + for _, subnet := range opts.Subnets { + reqBody.Subnets = append(reqBody.Subnets, schema.NetworkSubnet{ + Type: string(subnet.Type), + IPRange: subnet.IPRange.String(), + NetworkZone: string(subnet.NetworkZone), + }) + } + for _, route := range opts.Routes { + reqBody.Routes = append(reqBody.Routes, schema.NetworkRoute{ + Destination: route.Destination.String(), + Gateway: route.Gateway.String(), + }) + } + if opts.Labels != nil { + reqBody.Labels = &opts.Labels + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + req, err := c.client.NewRequest(ctx, "POST", "/networks", bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + respBody := schema.NetworkCreateResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return NetworkFromSchema(respBody.Network), resp, nil +} + +// NetworkChangeIPRangeOpts specifies options for changing the IP range of a network. +type NetworkChangeIPRangeOpts struct { + IPRange *net.IPNet +} + +// ChangeIPRange changes the IP range of a network. +func (c *NetworkClient) ChangeIPRange(ctx context.Context, network *Network, opts NetworkChangeIPRangeOpts) (*Action, *Response, error) { + reqBody := schema.NetworkActionChangeIPRangeRequest{ + IPRange: opts.IPRange.String(), + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/networks/%d/actions/change_ip_range", network.ID) + req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + respBody := schema.NetworkActionChangeIPRangeResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, nil +} + +// NetworkAddSubnetOpts specifies options for adding a subnet to a network. +type NetworkAddSubnetOpts struct { + Subnet NetworkSubnet +} + +// AddSubnet adds a subnet to a network. +func (c *NetworkClient) AddSubnet(ctx context.Context, network *Network, opts NetworkAddSubnetOpts) (*Action, *Response, error) { + reqBody := schema.NetworkActionAddSubnetRequest{ + Type: string(opts.Subnet.Type), + NetworkZone: string(opts.Subnet.NetworkZone), + } + if opts.Subnet.IPRange != nil { + reqBody.IPRange = opts.Subnet.IPRange.String() + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/networks/%d/actions/add_subnet", network.ID) + req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + respBody := schema.NetworkActionAddSubnetResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, nil +} + +// NetworkDeleteSubnetOpts specifies options for deleting a subnet from a network. +type NetworkDeleteSubnetOpts struct { + Subnet NetworkSubnet +} + +// DeleteSubnet deletes a subnet from a network. +func (c *NetworkClient) DeleteSubnet(ctx context.Context, network *Network, opts NetworkDeleteSubnetOpts) (*Action, *Response, error) { + reqBody := schema.NetworkActionDeleteSubnetRequest{ + IPRange: opts.Subnet.IPRange.String(), + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/networks/%d/actions/delete_subnet", network.ID) + req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + respBody := schema.NetworkActionDeleteSubnetResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, nil +} + +// NetworkAddRouteOpts specifies options for adding a route to a network. +type NetworkAddRouteOpts struct { + Route NetworkRoute +} + +// AddRoute adds a route to a network. +func (c *NetworkClient) AddRoute(ctx context.Context, network *Network, opts NetworkAddRouteOpts) (*Action, *Response, error) { + reqBody := schema.NetworkActionAddRouteRequest{ + Destination: opts.Route.Destination.String(), + Gateway: opts.Route.Gateway.String(), + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/networks/%d/actions/add_route", network.ID) + req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + respBody := schema.NetworkActionAddSubnetResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, nil +} + +// NetworkDeleteRouteOpts specifies options for deleting a route from a network. +type NetworkDeleteRouteOpts struct { + Route NetworkRoute +} + +// DeleteRoute deletes a route from a network. +func (c *NetworkClient) DeleteRoute(ctx context.Context, network *Network, opts NetworkDeleteRouteOpts) (*Action, *Response, error) { + reqBody := schema.NetworkActionDeleteRouteRequest{ + Destination: opts.Route.Destination.String(), + Gateway: opts.Route.Gateway.String(), + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/networks/%d/actions/delete_route", network.ID) + req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + respBody := schema.NetworkActionDeleteSubnetResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, nil +} + +// NetworkChangeProtectionOpts specifies options for changing the resource protection level of a network. +type NetworkChangeProtectionOpts struct { + Delete *bool +} + +// ChangeProtection changes the resource protection level of a network. +func (c *NetworkClient) ChangeProtection(ctx context.Context, network *Network, opts NetworkChangeProtectionOpts) (*Action, *Response, error) { + reqBody := schema.NetworkActionChangeProtectionRequest{ + Delete: opts.Delete, + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/networks/%d/actions/change_protection", network.ID) + req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + respBody := schema.NetworkActionChangeProtectionResponse{} + 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/hcloud-go/hcloud/pricing.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/pricing.go new file mode 100644 index 000000000000..e8a2fff35031 --- /dev/null +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/pricing.go @@ -0,0 +1,111 @@ +/* +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" + + "k8s.io/autoscaler/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema" +) + +// Pricing specifies pricing information for various resources. +type Pricing struct { + Image ImagePricing + FloatingIP FloatingIPPricing + Traffic TrafficPricing + ServerBackup ServerBackupPricing + ServerTypes []ServerTypePricing + LoadBalancerTypes []LoadBalancerTypePricing +} + +// Price represents a price. Net amount, gross amount, as well as VAT rate are +// specified as strings and it is the user's responsibility to convert them to +// appropriate types for calculations. +type Price struct { + Currency string + VATRate string + Net string + Gross string +} + +// ImagePricing provides pricing information for imaegs. +type ImagePricing struct { + PerGBMonth Price +} + +// FloatingIPPricing provides pricing information for Floating IPs. +type FloatingIPPricing struct { + Monthly Price +} + +// TrafficPricing provides pricing information for traffic. +type TrafficPricing struct { + PerTB Price +} + +// ServerBackupPricing provides pricing information for server backups. +type ServerBackupPricing struct { + Percentage string +} + +// ServerTypePricing provides pricing information for a server type. +type ServerTypePricing struct { + ServerType *ServerType + Pricings []ServerTypeLocationPricing +} + +// ServerTypeLocationPricing provides pricing information for a server type +// at a location. +type ServerTypeLocationPricing struct { + Location *Location + Hourly Price + Monthly Price +} + +// LoadBalancerTypePricing provides pricing information for a Load Balancer type. +type LoadBalancerTypePricing struct { + LoadBalancerType *LoadBalancerType + Pricings []LoadBalancerTypeLocationPricing +} + +// LoadBalancerTypeLocationPricing provides pricing information for a Load Balancer type +// at a location. +type LoadBalancerTypeLocationPricing struct { + Location *Location + Hourly Price + Monthly Price +} + +// PricingClient is a client for the pricing API. +type PricingClient struct { + client *Client +} + +// Get retrieves pricing information. +func (c *PricingClient) Get(ctx context.Context) (Pricing, *Response, error) { + req, err := c.client.NewRequest(ctx, "GET", "/pricing", nil) + if err != nil { + return Pricing{}, nil, err + } + + var body schema.PricingGetResponse + resp, err := c.client.Do(req, &body) + if err != nil { + return Pricing{}, nil, err + } + return PricingFromSchema(body.Pricing), resp, nil +} diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema.go new file mode 100644 index 000000000000..f238aea35a3e --- /dev/null +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema.go @@ -0,0 +1,908 @@ +/* +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 ( + "net" + "strconv" + "time" + + "k8s.io/autoscaler/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema" +) + +// This file provides converter functions to convert models in the +// schema package to models in the hcloud package and vice versa. + +// ActionFromSchema converts a schema.Action to an Action. +func ActionFromSchema(s schema.Action) *Action { + action := &Action{ + ID: s.ID, + Status: ActionStatus(s.Status), + Command: s.Command, + Progress: s.Progress, + Started: s.Started, + Resources: []*ActionResource{}, + } + if s.Finished != nil { + action.Finished = *s.Finished + } + if s.Error != nil { + action.ErrorCode = s.Error.Code + action.ErrorMessage = s.Error.Message + } + for _, r := range s.Resources { + action.Resources = append(action.Resources, &ActionResource{ + ID: r.ID, + Type: ActionResourceType(r.Type), + }) + } + return action +} + +// ActionsFromSchema converts a slice of schema.Action to a slice of Action. +func ActionsFromSchema(s []schema.Action) []*Action { + var actions []*Action + for _, a := range s { + actions = append(actions, ActionFromSchema(a)) + } + return actions +} + +// FloatingIPFromSchema converts a schema.FloatingIP to a FloatingIP. +func FloatingIPFromSchema(s schema.FloatingIP) *FloatingIP { + f := &FloatingIP{ + ID: s.ID, + Type: FloatingIPType(s.Type), + HomeLocation: LocationFromSchema(s.HomeLocation), + Created: s.Created, + Blocked: s.Blocked, + Protection: FloatingIPProtection{ + Delete: s.Protection.Delete, + }, + Name: s.Name, + } + if s.Description != nil { + f.Description = *s.Description + } + if s.Server != nil { + f.Server = &Server{ID: *s.Server} + } + if f.Type == FloatingIPTypeIPv4 { + f.IP = net.ParseIP(s.IP) + } else { + f.IP, f.Network, _ = net.ParseCIDR(s.IP) + } + f.DNSPtr = map[string]string{} + for _, entry := range s.DNSPtr { + f.DNSPtr[entry.IP] = entry.DNSPtr + } + f.Labels = map[string]string{} + for key, value := range s.Labels { + f.Labels[key] = value + } + return f +} + +// ISOFromSchema converts a schema.ISO to an ISO. +func ISOFromSchema(s schema.ISO) *ISO { + return &ISO{ + ID: s.ID, + Name: s.Name, + Description: s.Description, + Type: ISOType(s.Type), + Deprecated: s.Deprecated, + } +} + +// LocationFromSchema converts a schema.Location to a Location. +func LocationFromSchema(s schema.Location) *Location { + return &Location{ + ID: s.ID, + Name: s.Name, + Description: s.Description, + Country: s.Country, + City: s.City, + Latitude: s.Latitude, + Longitude: s.Longitude, + NetworkZone: NetworkZone(s.NetworkZone), + } +} + +// DatacenterFromSchema converts a schema.Datacenter to a Datacenter. +func DatacenterFromSchema(s schema.Datacenter) *Datacenter { + d := &Datacenter{ + ID: s.ID, + Name: s.Name, + Description: s.Description, + Location: LocationFromSchema(s.Location), + ServerTypes: DatacenterServerTypes{ + Available: []*ServerType{}, + Supported: []*ServerType{}, + }, + } + for _, t := range s.ServerTypes.Available { + d.ServerTypes.Available = append(d.ServerTypes.Available, &ServerType{ID: t}) + } + for _, t := range s.ServerTypes.Supported { + d.ServerTypes.Supported = append(d.ServerTypes.Supported, &ServerType{ID: t}) + } + return d +} + +// ServerFromSchema converts a schema.Server to a Server. +func ServerFromSchema(s schema.Server) *Server { + server := &Server{ + ID: s.ID, + Name: s.Name, + Status: ServerStatus(s.Status), + Created: s.Created, + PublicNet: ServerPublicNetFromSchema(s.PublicNet), + ServerType: ServerTypeFromSchema(s.ServerType), + IncludedTraffic: s.IncludedTraffic, + RescueEnabled: s.RescueEnabled, + Datacenter: DatacenterFromSchema(s.Datacenter), + Locked: s.Locked, + PrimaryDiskSize: s.PrimaryDiskSize, + Protection: ServerProtection{ + Delete: s.Protection.Delete, + Rebuild: s.Protection.Rebuild, + }, + } + if s.Image != nil { + server.Image = ImageFromSchema(*s.Image) + } + if s.BackupWindow != nil { + server.BackupWindow = *s.BackupWindow + } + if s.OutgoingTraffic != nil { + server.OutgoingTraffic = *s.OutgoingTraffic + } + if s.IngoingTraffic != nil { + server.IngoingTraffic = *s.IngoingTraffic + } + if s.ISO != nil { + server.ISO = ISOFromSchema(*s.ISO) + } + server.Labels = map[string]string{} + for key, value := range s.Labels { + server.Labels[key] = value + } + for _, id := range s.Volumes { + server.Volumes = append(server.Volumes, &Volume{ID: id}) + } + for _, privNet := range s.PrivateNet { + server.PrivateNet = append(server.PrivateNet, ServerPrivateNetFromSchema(privNet)) + } + return server +} + +// ServerPublicNetFromSchema converts a schema.ServerPublicNet to a ServerPublicNet. +func ServerPublicNetFromSchema(s schema.ServerPublicNet) ServerPublicNet { + publicNet := ServerPublicNet{ + IPv4: ServerPublicNetIPv4FromSchema(s.IPv4), + IPv6: ServerPublicNetIPv6FromSchema(s.IPv6), + } + for _, id := range s.FloatingIPs { + publicNet.FloatingIPs = append(publicNet.FloatingIPs, &FloatingIP{ID: id}) + } + return publicNet +} + +// ServerPublicNetIPv4FromSchema converts a schema.ServerPublicNetIPv4 to +// a ServerPublicNetIPv4. +func ServerPublicNetIPv4FromSchema(s schema.ServerPublicNetIPv4) ServerPublicNetIPv4 { + return ServerPublicNetIPv4{ + IP: net.ParseIP(s.IP), + Blocked: s.Blocked, + DNSPtr: s.DNSPtr, + } +} + +// ServerPublicNetIPv6FromSchema converts a schema.ServerPublicNetIPv6 to +// a ServerPublicNetIPv6. +func ServerPublicNetIPv6FromSchema(s schema.ServerPublicNetIPv6) ServerPublicNetIPv6 { + ipv6 := ServerPublicNetIPv6{ + Blocked: s.Blocked, + DNSPtr: map[string]string{}, + } + ipv6.IP, ipv6.Network, _ = net.ParseCIDR(s.IP) + + for _, dnsPtr := range s.DNSPtr { + ipv6.DNSPtr[dnsPtr.IP] = dnsPtr.DNSPtr + } + return ipv6 +} + +// ServerPrivateNetFromSchema converts a schema.ServerPrivateNet to a ServerPrivateNet. +func ServerPrivateNetFromSchema(s schema.ServerPrivateNet) ServerPrivateNet { + n := ServerPrivateNet{ + Network: &Network{ID: s.Network}, + IP: net.ParseIP(s.IP), + MACAddress: s.MACAddress, + } + for _, ip := range s.AliasIPs { + n.Aliases = append(n.Aliases, net.ParseIP(ip)) + } + return n +} + +// 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), + } + for _, price := range s.Prices { + st.Pricings = append(st.Pricings, ServerTypeLocationPricing{ + Location: &Location{Name: price.Location}, + Hourly: Price{ + Net: price.PriceHourly.Net, + Gross: price.PriceHourly.Gross, + }, + Monthly: Price{ + Net: price.PriceMonthly.Net, + Gross: price.PriceMonthly.Gross, + }, + }) + } + return st +} + +// SSHKeyFromSchema converts a schema.SSHKey to a SSHKey. +func SSHKeyFromSchema(s schema.SSHKey) *SSHKey { + sshKey := &SSHKey{ + ID: s.ID, + Name: s.Name, + Fingerprint: s.Fingerprint, + PublicKey: s.PublicKey, + Created: s.Created, + } + sshKey.Labels = map[string]string{} + for key, value := range s.Labels { + sshKey.Labels[key] = value + } + return 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, + Protection: ImageProtection{ + Delete: s.Protection.Delete, + }, + Deprecated: s.Deprecated, + } + if s.Name != nil { + i.Name = *s.Name + } + if s.ImageSize != nil { + i.ImageSize = *s.ImageSize + } + if s.OSVersion != nil { + i.OSVersion = *s.OSVersion + } + if s.CreatedFrom != nil { + i.CreatedFrom = &Server{ + ID: s.CreatedFrom.ID, + Name: s.CreatedFrom.Name, + } + } + if s.BoundTo != nil { + i.BoundTo = &Server{ + ID: *s.BoundTo, + } + } + i.Labels = map[string]string{} + for key, value := range s.Labels { + i.Labels[key] = value + } + return i +} + +// VolumeFromSchema converts a schema.Volume to a Volume. +func VolumeFromSchema(s schema.Volume) *Volume { + v := &Volume{ + ID: s.ID, + Name: s.Name, + Location: LocationFromSchema(s.Location), + Size: s.Size, + Status: VolumeStatus(s.Status), + LinuxDevice: s.LinuxDevice, + Protection: VolumeProtection{ + Delete: s.Protection.Delete, + }, + Created: s.Created, + } + if s.Server != nil { + v.Server = &Server{ID: *s.Server} + } + v.Labels = map[string]string{} + for key, value := range s.Labels { + v.Labels[key] = value + } + return v +} + +// NetworkFromSchema converts a schema.Network to a Network. +func NetworkFromSchema(s schema.Network) *Network { + n := &Network{ + ID: s.ID, + Name: s.Name, + Created: s.Created, + Protection: NetworkProtection{ + Delete: s.Protection.Delete, + }, + Labels: map[string]string{}, + } + + _, n.IPRange, _ = net.ParseCIDR(s.IPRange) + + for _, subnet := range s.Subnets { + n.Subnets = append(n.Subnets, NetworkSubnetFromSchema(subnet)) + } + for _, route := range s.Routes { + n.Routes = append(n.Routes, NetworkRouteFromSchema(route)) + } + for _, serverID := range s.Servers { + n.Servers = append(n.Servers, &Server{ID: serverID}) + } + for key, value := range s.Labels { + n.Labels[key] = value + } + + return n +} + +// NetworkSubnetFromSchema converts a schema.NetworkSubnet to a NetworkSubnet. +func NetworkSubnetFromSchema(s schema.NetworkSubnet) NetworkSubnet { + sn := NetworkSubnet{ + Type: NetworkSubnetType(s.Type), + NetworkZone: NetworkZone(s.NetworkZone), + Gateway: net.ParseIP(s.Gateway), + } + _, sn.IPRange, _ = net.ParseCIDR(s.IPRange) + return sn +} + +// NetworkRouteFromSchema converts a schema.NetworkRoute to a NetworkRoute. +func NetworkRouteFromSchema(s schema.NetworkRoute) NetworkRoute { + r := NetworkRoute{ + Gateway: net.ParseIP(s.Gateway), + } + _, r.Destination, _ = net.ParseCIDR(s.Destination) + return r +} + +// LoadBalancerTypeFromSchema converts a schema.LoadBalancerType to a LoadBalancerType. +func LoadBalancerTypeFromSchema(s schema.LoadBalancerType) *LoadBalancerType { + lt := &LoadBalancerType{ + ID: s.ID, + Name: s.Name, + Description: s.Description, + MaxConnections: s.MaxConnections, + MaxServices: s.MaxServices, + MaxTargets: s.MaxTargets, + MaxAssignedCertificates: s.MaxAssignedCertificates, + } + for _, price := range s.Prices { + lt.Pricings = append(lt.Pricings, LoadBalancerTypeLocationPricing{ + Location: &Location{Name: price.Location}, + Hourly: Price{ + Net: price.PriceHourly.Net, + Gross: price.PriceHourly.Gross, + }, + Monthly: Price{ + Net: price.PriceMonthly.Net, + Gross: price.PriceMonthly.Gross, + }, + }) + } + return lt +} + +// LoadBalancerFromSchema converts a schema.LoadBalancer to a LoadBalancer. +func LoadBalancerFromSchema(s schema.LoadBalancer) *LoadBalancer { + l := &LoadBalancer{ + ID: s.ID, + Name: s.Name, + PublicNet: LoadBalancerPublicNet{ + Enabled: s.PublicNet.Enabled, + IPv4: LoadBalancerPublicNetIPv4{ + IP: net.ParseIP(s.PublicNet.IPv4.IP), + }, + IPv6: LoadBalancerPublicNetIPv6{ + IP: net.ParseIP(s.PublicNet.IPv6.IP), + }, + }, + Location: LocationFromSchema(s.Location), + LoadBalancerType: LoadBalancerTypeFromSchema(s.LoadBalancerType), + Algorithm: LoadBalancerAlgorithm{Type: LoadBalancerAlgorithmType(s.Algorithm.Type)}, + Protection: LoadBalancerProtection{ + Delete: s.Protection.Delete, + }, + Labels: map[string]string{}, + Created: s.Created, + IncludedTraffic: s.IncludedTraffic, + } + for _, privateNet := range s.PrivateNet { + l.PrivateNet = append(l.PrivateNet, LoadBalancerPrivateNet{ + Network: &Network{ID: privateNet.Network}, + IP: net.ParseIP(privateNet.IP), + }) + } + if s.OutgoingTraffic != nil { + l.OutgoingTraffic = *s.OutgoingTraffic + } + if s.IngoingTraffic != nil { + l.IngoingTraffic = *s.IngoingTraffic + } + for _, service := range s.Services { + l.Services = append(l.Services, LoadBalancerServiceFromSchema(service)) + } + for _, target := range s.Targets { + l.Targets = append(l.Targets, LoadBalancerTargetFromSchema(target)) + } + for key, value := range s.Labels { + l.Labels[key] = value + } + return l +} + +// LoadBalancerServiceFromSchema converts a schema.LoadBalancerService to a LoadBalancerService. +func LoadBalancerServiceFromSchema(s schema.LoadBalancerService) LoadBalancerService { + ls := LoadBalancerService{ + Protocol: LoadBalancerServiceProtocol(s.Protocol), + ListenPort: s.ListenPort, + DestinationPort: s.DestinationPort, + Proxyprotocol: s.Proxyprotocol, + HealthCheck: LoadBalancerServiceHealthCheckFromSchema(s.HealthCheck), + } + if s.HTTP != nil { + ls.HTTP = LoadBalancerServiceHTTP{ + CookieName: s.HTTP.CookieName, + CookieLifetime: time.Duration(s.HTTP.CookieLifetime) * time.Second, + RedirectHTTP: s.HTTP.RedirectHTTP, + StickySessions: s.HTTP.StickySessions, + } + for _, certificateID := range s.HTTP.Certificates { + ls.HTTP.Certificates = append(ls.HTTP.Certificates, &Certificate{ID: certificateID}) + } + } + return ls +} + +// LoadBalancerServiceHealthCheckFromSchema converts a schema.LoadBalancerServiceHealthCheck to a LoadBalancerServiceHealthCheck. +func LoadBalancerServiceHealthCheckFromSchema(s *schema.LoadBalancerServiceHealthCheck) LoadBalancerServiceHealthCheck { + lsh := LoadBalancerServiceHealthCheck{ + Protocol: LoadBalancerServiceProtocol(s.Protocol), + Port: s.Port, + Interval: time.Duration(s.Interval) * time.Second, + Retries: s.Retries, + Timeout: time.Duration(s.Timeout) * time.Second, + } + if s.HTTP != nil { + lsh.HTTP = &LoadBalancerServiceHealthCheckHTTP{ + Domain: s.HTTP.Domain, + Path: s.HTTP.Path, + Response: s.HTTP.Response, + StatusCodes: s.HTTP.StatusCodes, + TLS: s.HTTP.TLS, + } + } + return lsh +} + +// LoadBalancerTargetFromSchema converts a schema.LoadBalancerTarget to a LoadBalancerTarget. +func LoadBalancerTargetFromSchema(s schema.LoadBalancerTarget) LoadBalancerTarget { + lt := LoadBalancerTarget{ + Type: LoadBalancerTargetType(s.Type), + UsePrivateIP: s.UsePrivateIP, + } + if s.Server != nil { + lt.Server = &LoadBalancerTargetServer{ + Server: &Server{ID: s.Server.ID}, + } + } + if s.LabelSelector != nil { + lt.LabelSelector = &LoadBalancerTargetLabelSelector{ + Selector: s.LabelSelector.Selector, + } + } + if s.IP != nil { + lt.IP = &LoadBalancerTargetIP{IP: s.IP.IP} + } + + for _, healthStatus := range s.HealthStatus { + lt.HealthStatus = append(lt.HealthStatus, LoadBalancerTargetHealthStatusFromSchema(healthStatus)) + } + for _, target := range s.Targets { + lt.Targets = append(lt.Targets, LoadBalancerTargetFromSchema(target)) + } + return lt +} + +// LoadBalancerTargetHealthStatusFromSchema converts a schema.LoadBalancerTarget to a LoadBalancerTarget. +func LoadBalancerTargetHealthStatusFromSchema(s schema.LoadBalancerTargetHealthStatus) LoadBalancerTargetHealthStatus { + return LoadBalancerTargetHealthStatus{ + ListenPort: s.ListenPort, + Status: LoadBalancerTargetHealthStatusStatus(s.Status), + } +} + +// CertificateFromSchema converts a schema.Certificate to a Certificate. +func CertificateFromSchema(s schema.Certificate) *Certificate { + c := &Certificate{ + ID: s.ID, + Name: s.Name, + Certificate: s.Certificate, + Created: s.Created, + NotValidBefore: s.NotValidBefore, + NotValidAfter: s.NotValidAfter, + DomainNames: s.DomainNames, + Fingerprint: s.Fingerprint, + } + if len(s.Labels) > 0 { + c.Labels = make(map[string]string) + } + for key, value := range s.Labels { + c.Labels[key] = value + } + return c +} + +// PaginationFromSchema converts a schema.MetaPagination to a Pagination. +func PaginationFromSchema(s schema.MetaPagination) Pagination { + return Pagination{ + Page: s.Page, + PerPage: s.PerPage, + PreviousPage: s.PreviousPage, + NextPage: s.NextPage, + LastPage: s.LastPage, + TotalEntries: s.TotalEntries, + } +} + +// ErrorFromSchema converts a schema.Error to an Error. +func ErrorFromSchema(s schema.Error) Error { + e := Error{ + Code: ErrorCode(s.Code), + Message: s.Message, + } + + switch d := s.Details.(type) { + case schema.ErrorDetailsInvalidInput: + details := ErrorDetailsInvalidInput{ + Fields: []ErrorDetailsInvalidInputField{}, + } + for _, field := range d.Fields { + details.Fields = append(details.Fields, ErrorDetailsInvalidInputField{ + Name: field.Name, + Messages: field.Messages, + }) + } + e.Details = details + } + return e +} + +// PricingFromSchema converts a schema.Pricing to a Pricing. +func PricingFromSchema(s schema.Pricing) Pricing { + p := Pricing{ + Image: ImagePricing{ + PerGBMonth: Price{ + Currency: s.Currency, + VATRate: s.VATRate, + Net: s.Image.PricePerGBMonth.Net, + Gross: s.Image.PricePerGBMonth.Gross, + }, + }, + FloatingIP: FloatingIPPricing{ + Monthly: Price{ + Currency: s.Currency, + VATRate: s.VATRate, + Net: s.FloatingIP.PriceMonthly.Net, + Gross: s.FloatingIP.PriceMonthly.Gross, + }, + }, + Traffic: TrafficPricing{ + PerTB: Price{ + Currency: s.Currency, + VATRate: s.VATRate, + Net: s.Traffic.PricePerTB.Net, + Gross: s.Traffic.PricePerTB.Gross, + }, + }, + ServerBackup: ServerBackupPricing{ + Percentage: s.ServerBackup.Percentage, + }, + } + for _, serverType := range s.ServerTypes { + var pricings []ServerTypeLocationPricing + for _, price := range serverType.Prices { + pricings = append(pricings, ServerTypeLocationPricing{ + Location: &Location{Name: price.Location}, + Hourly: Price{ + Currency: s.Currency, + VATRate: s.VATRate, + Net: price.PriceHourly.Net, + Gross: price.PriceHourly.Gross, + }, + Monthly: Price{ + Currency: s.Currency, + VATRate: s.VATRate, + Net: price.PriceMonthly.Net, + Gross: price.PriceMonthly.Gross, + }, + }) + } + p.ServerTypes = append(p.ServerTypes, ServerTypePricing{ + ServerType: &ServerType{ + ID: serverType.ID, + Name: serverType.Name, + }, + Pricings: pricings, + }) + } + for _, loadBalancerType := range s.LoadBalancerTypes { + var pricings []LoadBalancerTypeLocationPricing + for _, price := range loadBalancerType.Prices { + pricings = append(pricings, LoadBalancerTypeLocationPricing{ + Location: &Location{Name: price.Location}, + Hourly: Price{ + Currency: s.Currency, + VATRate: s.VATRate, + Net: price.PriceHourly.Net, + Gross: price.PriceHourly.Gross, + }, + Monthly: Price{ + Currency: s.Currency, + VATRate: s.VATRate, + Net: price.PriceMonthly.Net, + Gross: price.PriceMonthly.Gross, + }, + }) + } + p.LoadBalancerTypes = append(p.LoadBalancerTypes, LoadBalancerTypePricing{ + LoadBalancerType: &LoadBalancerType{ + ID: loadBalancerType.ID, + Name: loadBalancerType.Name, + }, + Pricings: pricings, + }) + } + return p +} + +func loadBalancerCreateOptsToSchema(opts LoadBalancerCreateOpts) schema.LoadBalancerCreateRequest { + req := schema.LoadBalancerCreateRequest{ + Name: opts.Name, + PublicInterface: opts.PublicInterface, + } + if opts.Algorithm != nil { + req.Algorithm = &schema.LoadBalancerCreateRequestAlgorithm{ + Type: string(opts.Algorithm.Type), + } + } + if opts.LoadBalancerType.ID != 0 { + req.LoadBalancerType = opts.LoadBalancerType.ID + } else if opts.LoadBalancerType.Name != "" { + req.LoadBalancerType = opts.LoadBalancerType.Name + } + if opts.Location != nil { + if opts.Location.ID != 0 { + req.Location = String(strconv.Itoa(opts.Location.ID)) + } else { + req.Location = String(opts.Location.Name) + } + } + if opts.NetworkZone != "" { + req.NetworkZone = String(string(opts.NetworkZone)) + } + if opts.Labels != nil { + req.Labels = &opts.Labels + } + if opts.Network != nil { + req.Network = Int(opts.Network.ID) + } + for _, target := range opts.Targets { + schemaTarget := schema.LoadBalancerCreateRequestTarget{} + switch target.Type { + case LoadBalancerTargetTypeServer: + schemaTarget.Type = string(LoadBalancerTargetTypeServer) + schemaTarget.Server = &schema.LoadBalancerCreateRequestTargetServer{ID: target.Server.Server.ID} + case LoadBalancerTargetTypeLabelSelector: + schemaTarget.Type = string(LoadBalancerTargetTypeLabelSelector) + schemaTarget.LabelSelector = &schema.LoadBalancerCreateRequestTargetLabelSelector{Selector: target.LabelSelector.Selector} + case LoadBalancerTargetTypeIP: + schemaTarget.Type = string(LoadBalancerTargetTypeIP) + schemaTarget.IP = &schema.LoadBalancerCreateRequestTargetIP{IP: target.IP.IP} + } + req.Targets = append(req.Targets, schemaTarget) + } + for _, service := range opts.Services { + schemaService := schema.LoadBalancerCreateRequestService{ + Protocol: string(service.Protocol), + ListenPort: service.ListenPort, + DestinationPort: service.DestinationPort, + Proxyprotocol: service.Proxyprotocol, + } + if service.HTTP != nil { + schemaService.HTTP = &schema.LoadBalancerCreateRequestServiceHTTP{ + RedirectHTTP: service.HTTP.RedirectHTTP, + StickySessions: service.HTTP.StickySessions, + CookieName: service.HTTP.CookieName, + } + if sec := service.HTTP.CookieLifetime.Seconds(); sec != 0 { + schemaService.HTTP.CookieLifetime = Int(int(sec)) + } + if service.HTTP.Certificates != nil { + certificates := []int{} + for _, certificate := range service.HTTP.Certificates { + certificates = append(certificates, certificate.ID) + } + schemaService.HTTP.Certificates = &certificates + } + } + if service.HealthCheck != nil { + schemaHealthCheck := &schema.LoadBalancerCreateRequestServiceHealthCheck{ + Protocol: string(service.HealthCheck.Protocol), + Port: service.HealthCheck.Port, + Retries: service.HealthCheck.Retries, + } + if service.HealthCheck.Interval != nil { + schemaHealthCheck.Interval = Int(int(service.HealthCheck.Interval.Seconds())) + } + if service.HealthCheck.Timeout != nil { + schemaHealthCheck.Timeout = Int(int(service.HealthCheck.Timeout.Seconds())) + } + if service.HealthCheck.HTTP != nil { + schemaHealthCheckHTTP := &schema.LoadBalancerCreateRequestServiceHealthCheckHTTP{ + Domain: service.HealthCheck.HTTP.Domain, + Path: service.HealthCheck.HTTP.Path, + Response: service.HealthCheck.HTTP.Response, + TLS: service.HealthCheck.HTTP.TLS, + } + if service.HealthCheck.HTTP.StatusCodes != nil { + schemaHealthCheckHTTP.StatusCodes = &service.HealthCheck.HTTP.StatusCodes + } + schemaHealthCheck.HTTP = schemaHealthCheckHTTP + } + schemaService.HealthCheck = schemaHealthCheck + } + req.Services = append(req.Services, schemaService) + } + return req +} + +func loadBalancerAddServiceOptsToSchema(opts LoadBalancerAddServiceOpts) schema.LoadBalancerActionAddServiceRequest { + req := schema.LoadBalancerActionAddServiceRequest{ + Protocol: string(opts.Protocol), + ListenPort: opts.ListenPort, + DestinationPort: opts.DestinationPort, + Proxyprotocol: opts.Proxyprotocol, + } + if opts.HTTP != nil { + req.HTTP = &schema.LoadBalancerActionAddServiceRequestHTTP{ + CookieName: opts.HTTP.CookieName, + RedirectHTTP: opts.HTTP.RedirectHTTP, + StickySessions: opts.HTTP.StickySessions, + } + if opts.HTTP.CookieLifetime != nil { + req.HTTP.CookieLifetime = Int(int(opts.HTTP.CookieLifetime.Seconds())) + } + if opts.HTTP.Certificates != nil { + certificates := []int{} + for _, certificate := range opts.HTTP.Certificates { + certificates = append(certificates, certificate.ID) + } + req.HTTP.Certificates = &certificates + } + } + if opts.HealthCheck != nil { + req.HealthCheck = &schema.LoadBalancerActionAddServiceRequestHealthCheck{ + Protocol: string(opts.HealthCheck.Protocol), + Port: opts.HealthCheck.Port, + Retries: opts.HealthCheck.Retries, + } + if opts.HealthCheck.Interval != nil { + req.HealthCheck.Interval = Int(int(opts.HealthCheck.Interval.Seconds())) + } + if opts.HealthCheck.Timeout != nil { + req.HealthCheck.Timeout = Int(int(opts.HealthCheck.Timeout.Seconds())) + } + if opts.HealthCheck.HTTP != nil { + req.HealthCheck.HTTP = &schema.LoadBalancerActionAddServiceRequestHealthCheckHTTP{ + Domain: opts.HealthCheck.HTTP.Domain, + Path: opts.HealthCheck.HTTP.Path, + Response: opts.HealthCheck.HTTP.Response, + TLS: opts.HealthCheck.HTTP.TLS, + } + if opts.HealthCheck.HTTP.StatusCodes != nil { + req.HealthCheck.HTTP.StatusCodes = &opts.HealthCheck.HTTP.StatusCodes + } + } + } + return req +} + +func loadBalancerUpdateServiceOptsToSchema(opts LoadBalancerUpdateServiceOpts) schema.LoadBalancerActionUpdateServiceRequest { + req := schema.LoadBalancerActionUpdateServiceRequest{ + DestinationPort: opts.DestinationPort, + Proxyprotocol: opts.Proxyprotocol, + } + if opts.Protocol != "" { + req.Protocol = String(string(opts.Protocol)) + } + if opts.HTTP != nil { + req.HTTP = &schema.LoadBalancerActionUpdateServiceRequestHTTP{ + CookieName: opts.HTTP.CookieName, + RedirectHTTP: opts.HTTP.RedirectHTTP, + StickySessions: opts.HTTP.StickySessions, + } + if opts.HTTP.CookieLifetime != nil { + req.HTTP.CookieLifetime = Int(int(opts.HTTP.CookieLifetime.Seconds())) + } + if opts.HTTP.Certificates != nil { + certificates := []int{} + for _, certificate := range opts.HTTP.Certificates { + certificates = append(certificates, certificate.ID) + } + req.HTTP.Certificates = &certificates + } + } + if opts.HealthCheck != nil { + req.HealthCheck = &schema.LoadBalancerActionUpdateServiceRequestHealthCheck{ + Port: opts.HealthCheck.Port, + Retries: opts.HealthCheck.Retries, + } + if opts.HealthCheck.Interval != nil { + req.HealthCheck.Interval = Int(int(opts.HealthCheck.Interval.Seconds())) + } + if opts.HealthCheck.Timeout != nil { + req.HealthCheck.Timeout = Int(int(opts.HealthCheck.Timeout.Seconds())) + } + if opts.HealthCheck.Protocol != "" { + req.HealthCheck.Protocol = String(string(opts.HealthCheck.Protocol)) + } + if opts.HealthCheck.HTTP != nil { + req.HealthCheck.HTTP = &schema.LoadBalancerActionUpdateServiceRequestHealthCheckHTTP{ + Domain: opts.HealthCheck.HTTP.Domain, + Path: opts.HealthCheck.HTTP.Path, + Response: opts.HealthCheck.HTTP.Response, + TLS: opts.HealthCheck.HTTP.TLS, + } + if opts.HealthCheck.HTTP.StatusCodes != nil { + req.HealthCheck.HTTP.StatusCodes = &opts.HealthCheck.HTTP.StatusCodes + } + } + } + return req +} diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/action.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/action.go new file mode 100644 index 000000000000..1de4764ef29a --- /dev/null +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/action.go @@ -0,0 +1,55 @@ +/* +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" + +// Action defines the schema of an action. +type Action struct { + ID int `json:"id"` + Status string `json:"status"` + Command string `json:"command"` + Progress int `json:"progress"` + Started time.Time `json:"started"` + Finished *time.Time `json:"finished"` + Error *ActionError `json:"error"` + Resources []ActionResourceReference `json:"resources"` +} + +// ActionResourceReference defines the schema of an action resource reference. +type ActionResourceReference struct { + ID int `json:"id"` + Type string `json:"type"` +} + +// ActionError defines the schema of an error embedded +// in an action. +type ActionError struct { + Code string `json:"code"` + Message string `json:"message"` +} + +// ActionGetResponse is the schema of the response when +// retrieving a single action. +type ActionGetResponse struct { + Action Action `json:"action"` +} + +// ActionListResponse defines the schema of the response when listing actions. +type ActionListResponse struct { + Actions []Action `json:"actions"` +} diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/certificate.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/certificate.go new file mode 100644 index 000000000000..447962969cd4 --- /dev/null +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/certificate.go @@ -0,0 +1,68 @@ +/* +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" + +// Certificate defines the schema of an certificate. +type Certificate struct { + ID int `json:"id"` + Name string `json:"name"` + Labels map[string]string `json:"labels"` + Certificate string `json:"certificate"` + Created time.Time `json:"created"` + NotValidBefore time.Time `json:"not_valid_before"` + NotValidAfter time.Time `json:"not_valid_after"` + DomainNames []string `json:"domain_names"` + Fingerprint string `json:"fingerprint"` +} + +// CertificateListResponse defines the schema of the response when +// listing Certificates. +type CertificateListResponse struct { + Certificates []Certificate `json:"certificates"` +} + +// CertificateGetResponse defines the schema of the response when +// retrieving a single Certificate. +type CertificateGetResponse struct { + Certificate Certificate `json:"certificate"` +} + +// CertificateCreateRequest defines the schema of the request to create a certificate. +type CertificateCreateRequest struct { + Name string `json:"name"` + Certificate string `json:"certificate"` + PrivateKey string `json:"private_key"` + Labels *map[string]string `json:"labels,omitempty"` +} + +// CertificateCreateResponse defines the schema of the response when creating a certificate. +type CertificateCreateResponse struct { + Certificate Certificate `json:"certificate"` +} + +// CertificateUpdateRequest defines the schema of the request to update a certificate. +type CertificateUpdateRequest struct { + Name *string `json:"name,omitempty"` + Labels *map[string]string `json:"labels,omitempty"` +} + +// CertificateUpdateResponse defines the schema of the response when updating a certificate. +type CertificateUpdateResponse struct { + Certificate Certificate `json:"certificate"` +} diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/datacenter.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/datacenter.go new file mode 100644 index 000000000000..3e939e72002b --- /dev/null +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/datacenter.go @@ -0,0 +1,39 @@ +/* +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 + +// Datacenter defines the schema of a datacenter. +type Datacenter struct { + ID int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Location Location `json:"location"` + ServerTypes struct { + Supported []int `json:"supported"` + Available []int `json:"available"` + } `json:"server_types"` +} + +// DatacenterGetResponse defines the schema of the response when retrieving a single datacenter. +type DatacenterGetResponse struct { + Datacenter Datacenter `json:"datacenter"` +} + +// DatacenterListResponse defines the schema of the response when listing datacenters. +type DatacenterListResponse struct { + Datacenters []Datacenter `json:"datacenters"` +} diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/error.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/error.go new file mode 100644 index 000000000000..bf02bd06db31 --- /dev/null +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/error.go @@ -0,0 +1,59 @@ +/* +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 "encoding/json" + +// Error represents the schema of an error response. +type Error struct { + Code string `json:"code"` + Message string `json:"message"` + DetailsRaw json.RawMessage `json:"details"` + Details interface{} +} + +// UnmarshalJSON overrides default json unmarshalling. +func (e *Error) UnmarshalJSON(data []byte) (err error) { + type Alias Error + alias := (*Alias)(e) + if err = json.Unmarshal(data, alias); err != nil { + return + } + switch e.Code { + case "invalid_input": + details := ErrorDetailsInvalidInput{} + if err = json.Unmarshal(e.DetailsRaw, &details); err != nil { + return + } + alias.Details = details + } + return +} + +// ErrorResponse defines the schema of a response containing an error. +type ErrorResponse struct { + Error Error `json:"error"` +} + +// ErrorDetailsInvalidInput defines the schema of the Details field +// of an error with code 'invalid_input'. +type ErrorDetailsInvalidInput struct { + Fields []struct { + Name string `json:"name"` + Messages []string `json:"messages"` + } `json:"fields"` +} diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/floating_ip.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/floating_ip.go new file mode 100644 index 000000000000..d734e3664d0a --- /dev/null +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/floating_ip.go @@ -0,0 +1,134 @@ +/* +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" + +// FloatingIP defines the schema of a Floating IP. +type FloatingIP struct { + ID int `json:"id"` + Description *string `json:"description"` + Created time.Time `json:"created"` + IP string `json:"ip"` + Type string `json:"type"` + Server *int `json:"server"` + DNSPtr []FloatingIPDNSPtr `json:"dns_ptr"` + HomeLocation Location `json:"home_location"` + Blocked bool `json:"blocked"` + Protection FloatingIPProtection `json:"protection"` + Labels map[string]string `json:"labels"` + Name string `json:"name"` +} + +// FloatingIPProtection represents the protection level of a Floating IP. +type FloatingIPProtection struct { + Delete bool `json:"delete"` +} + +// FloatingIPDNSPtr contains reverse DNS information for a +// IPv4 or IPv6 Floating IP. +type FloatingIPDNSPtr struct { + IP string `json:"ip"` + DNSPtr string `json:"dns_ptr"` +} + +// FloatingIPGetResponse defines the schema of the response when +// retrieving a single Floating IP. +type FloatingIPGetResponse struct { + FloatingIP FloatingIP `json:"floating_ip"` +} + +// FloatingIPUpdateRequest defines the schema of the request to update a Floating IP. +type FloatingIPUpdateRequest struct { + Description string `json:"description,omitempty"` + Labels *map[string]string `json:"labels,omitempty"` + Name string `json:"name,omitempty"` +} + +// FloatingIPUpdateResponse defines the schema of the response when updating a Floating IP. +type FloatingIPUpdateResponse struct { + FloatingIP FloatingIP `json:"floating_ip"` +} + +// FloatingIPListResponse defines the schema of the response when +// listing Floating IPs. +type FloatingIPListResponse struct { + FloatingIPs []FloatingIP `json:"floating_ips"` +} + +// FloatingIPCreateRequest defines the schema of the request to +// create a Floating IP. +type FloatingIPCreateRequest struct { + Type string `json:"type"` + HomeLocation *string `json:"home_location,omitempty"` + Server *int `json:"server,omitempty"` + Description *string `json:"description,omitempty"` + Labels *map[string]string `json:"labels,omitempty"` + Name *string `json:"name,omitempty"` +} + +// FloatingIPCreateResponse defines the schema of the response +// when creating a Floating IP. +type FloatingIPCreateResponse struct { + FloatingIP FloatingIP `json:"floating_ip"` + Action *Action `json:"action"` +} + +// FloatingIPActionAssignRequest defines the schema of the request to +// create an assign Floating IP action. +type FloatingIPActionAssignRequest struct { + Server int `json:"server"` +} + +// FloatingIPActionAssignResponse defines the schema of the response when +// creating an assign action. +type FloatingIPActionAssignResponse struct { + Action Action `json:"action"` +} + +// FloatingIPActionUnassignRequest defines the schema of the request to +// create an unassign Floating IP action. +type FloatingIPActionUnassignRequest struct{} + +// FloatingIPActionUnassignResponse defines the schema of the response when +// creating an unassign action. +type FloatingIPActionUnassignResponse struct { + Action Action `json:"action"` +} + +// FloatingIPActionChangeDNSPtrRequest defines the schema for the request to +// change a Floating IP's reverse DNS pointer. +type FloatingIPActionChangeDNSPtrRequest struct { + IP string `json:"ip"` + DNSPtr *string `json:"dns_ptr"` +} + +// FloatingIPActionChangeDNSPtrResponse defines the schema of the response when +// creating a change_dns_ptr Floating IP action. +type FloatingIPActionChangeDNSPtrResponse struct { + Action Action `json:"action"` +} + +// FloatingIPActionChangeProtectionRequest defines the schema of the request to change the resource protection of a Floating IP. +type FloatingIPActionChangeProtectionRequest struct { + Delete *bool `json:"delete,omitempty"` +} + +// FloatingIPActionChangeProtectionResponse defines the schema of the response when changing the resource protection of a Floating IP. +type FloatingIPActionChangeProtectionResponse struct { + Action Action `json:"action"` +} diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/image.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/image.go new file mode 100644 index 000000000000..989c6ade995a --- /dev/null +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/image.go @@ -0,0 +1,84 @@ +/* +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" + +// 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"` + Labels map[string]string `json:"labels"` +} + +// ImageProtection represents the protection level of a image. +type ImageProtection struct { + Delete bool `json:"delete"` +} + +// ImageCreatedFrom defines the schema of the images created from reference. +type ImageCreatedFrom struct { + ID int `json:"id"` + Name string `json:"name"` +} + +// ImageGetResponse defines the schema of the response when +// retrieving a single image. +type ImageGetResponse struct { + Image Image `json:"image"` +} + +// ImageListResponse defines the schema of the response when +// listing images. +type ImageListResponse struct { + Images []Image `json:"images"` +} + +// ImageUpdateRequest defines the schema of the request to update an image. +type ImageUpdateRequest struct { + Description *string `json:"description,omitempty"` + Type *string `json:"type,omitempty"` + Labels *map[string]string `json:"labels,omitempty"` +} + +// ImageUpdateResponse defines the schema of the response when updating an image. +type ImageUpdateResponse struct { + Image Image `json:"image"` +} + +// ImageActionChangeProtectionRequest defines the schema of the request to change the resource protection of an image. +type ImageActionChangeProtectionRequest struct { + Delete *bool `json:"delete,omitempty"` +} + +// ImageActionChangeProtectionResponse defines the schema of the response when changing the resource protection of an image. +type ImageActionChangeProtectionResponse struct { + Action Action `json:"action"` +} diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/iso.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/iso.go new file mode 100644 index 000000000000..764f0fc26391 --- /dev/null +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/iso.go @@ -0,0 +1,38 @@ +/* +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" + +// 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"` +} + +// ISOGetResponse defines the schema of the response when retrieving a single ISO. +type ISOGetResponse struct { + ISO ISO `json:"iso"` +} + +// ISOListResponse defines the schema of the response when listing ISOs. +type ISOListResponse struct { + ISOs []ISO `json:"isos"` +} 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 new file mode 100644 index 000000000000..0dc559906635 --- /dev/null +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/load_balancer.go @@ -0,0 +1,402 @@ +/* +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 LoadBalancer struct { + ID int `json:"id"` + Name string `json:"name"` + PublicNet LoadBalancerPublicNet `json:"public_net"` + PrivateNet []LoadBalancerPrivateNet `json:"private_net"` + Location Location `json:"location"` + LoadBalancerType LoadBalancerType `json:"load_balancer_type"` + Protection LoadBalancerProtection `json:"protection"` + Labels map[string]string `json:"labels"` + Created time.Time `json:"created"` + Services []LoadBalancerService `json:"services"` + Targets []LoadBalancerTarget `json:"targets"` + Algorithm LoadBalancerAlgorithm `json:"algorithm"` + IncludedTraffic uint64 `json:"included_traffic"` + OutgoingTraffic *uint64 `json:"outgoing_traffic"` + IngoingTraffic *uint64 `json:"ingoing_traffic"` +} + +type LoadBalancerPublicNet struct { + Enabled bool `json:"enabled"` + IPv4 LoadBalancerPublicNetIPv4 `json:"ipv4"` + IPv6 LoadBalancerPublicNetIPv6 `json:"ipv6"` +} + +type LoadBalancerPublicNetIPv4 struct { + IP string `json:"ip"` +} + +type LoadBalancerPublicNetIPv6 struct { + IP string `json:"ip"` +} + +type LoadBalancerPrivateNet struct { + Network int `json:"network"` + IP string `json:"ip"` +} + +type LoadBalancerAlgorithm struct { + Type string `json:"type"` +} + +type LoadBalancerProtection struct { + Delete bool `json:"delete"` +} + +type LoadBalancerService struct { + Protocol string `json:"protocol"` + ListenPort int `json:"listen_port"` + DestinationPort int `json:"destination_port"` + Proxyprotocol bool `json:"proxyprotocol"` + HTTP *LoadBalancerServiceHTTP `json:"http"` + HealthCheck *LoadBalancerServiceHealthCheck `json:"health_check"` +} + +type LoadBalancerServiceHTTP struct { + CookieName string `json:"cookie_name"` + CookieLifetime int `json:"cookie_lifetime"` + Certificates []int `json:"certificates"` + RedirectHTTP bool `json:"redirect_http"` + StickySessions bool `json:"sticky_sessions"` +} + +type LoadBalancerServiceHealthCheck struct { + Protocol string `json:"protocol"` + Port int `json:"port"` + Interval int `json:"interval"` + Timeout int `json:"timeout"` + Retries int `json:"retries"` + HTTP *LoadBalancerServiceHealthCheckHTTP `json:"http"` +} + +type LoadBalancerServiceHealthCheckHTTP struct { + Domain string `json:"domain"` + Path string `json:"path"` + Response string `json:"response"` + StatusCodes []string `json:"status_codes"` + TLS bool `json:"tls"` +} + +type LoadBalancerTarget struct { + Type string `json:"type"` + Server *LoadBalancerTargetServer `json:"server"` + LabelSelector *LoadBalancerTargetLabelSelector `json:"label_selector"` + IP *LoadBalancerTargetIP `json:"ip"` + HealthStatus []LoadBalancerTargetHealthStatus `json:"health_status"` + UsePrivateIP bool `json:"use_private_ip"` + Targets []LoadBalancerTarget `json:"targets,omitempty"` +} + +type LoadBalancerTargetHealthStatus struct { + ListenPort int `json:"listen_port"` + Status string `json:"status"` +} + +type LoadBalancerTargetServer struct { + ID int `json:"id"` +} + +type LoadBalancerTargetLabelSelector struct { + Selector string `json:"selector"` +} + +type LoadBalancerTargetIP struct { + IP string `json:"ip"` +} + +type LoadBalancerListResponse struct { + LoadBalancers []LoadBalancer `json:"load_balancers"` +} + +type LoadBalancerGetResponse struct { + LoadBalancer LoadBalancer `json:"load_balancer"` +} + +type LoadBalancerActionAddTargetRequest struct { + Type string `json:"type"` + Server *LoadBalancerActionAddTargetRequestServer `json:"server,omitempty"` + LabelSelector *LoadBalancerActionAddTargetRequestLabelSelector `json:"label_selector,omitempty"` + IP *LoadBalancerActionAddTargetRequestIP `json:"ip,omitempty"` + UsePrivateIP *bool `json:"use_private_ip,omitempty"` +} + +type LoadBalancerActionAddTargetRequestServer struct { + ID int `json:"id"` +} + +type LoadBalancerActionAddTargetRequestLabelSelector struct { + Selector string `json:"selector"` +} + +type LoadBalancerActionAddTargetRequestIP struct { + IP string `json:"ip"` +} + +type LoadBalancerActionAddTargetResponse struct { + Action Action `json:"action"` +} + +type LoadBalancerActionRemoveTargetRequest struct { + Type string `json:"type"` + Server *LoadBalancerActionRemoveTargetRequestServer `json:"server,omitempty"` + LabelSelector *LoadBalancerActionRemoveTargetRequestLabelSelector `json:"label_selector,omitempty"` + IP *LoadBalancerActionRemoveTargetRequestIP `json:"ip,omitempty"` +} + +type LoadBalancerActionRemoveTargetRequestServer struct { + ID int `json:"id"` +} + +type LoadBalancerActionRemoveTargetRequestLabelSelector struct { + Selector string `json:"selector"` +} + +type LoadBalancerActionRemoveTargetRequestIP struct { + IP string `json:"ip"` +} + +type LoadBalancerActionRemoveTargetResponse struct { + Action Action `json:"action"` +} + +type LoadBalancerActionAddServiceRequest struct { + Protocol string `json:"protocol"` + ListenPort *int `json:"listen_port,omitempty"` + DestinationPort *int `json:"destination_port,omitempty"` + Proxyprotocol *bool `json:"proxyprotocol,omitempty"` + HTTP *LoadBalancerActionAddServiceRequestHTTP `json:"http,omitempty"` + HealthCheck *LoadBalancerActionAddServiceRequestHealthCheck `json:"health_check,omitempty"` +} + +type LoadBalancerActionAddServiceRequestHTTP struct { + CookieName *string `json:"cookie_name,omitempty"` + CookieLifetime *int `json:"cookie_lifetime,omitempty"` + Certificates *[]int `json:"certificates,omitempty"` + RedirectHTTP *bool `json:"redirect_http,omitempty"` + StickySessions *bool `json:"sticky_sessions,omitempty"` +} + +type LoadBalancerActionAddServiceRequestHealthCheck struct { + Protocol string `json:"protocol"` + Port *int `json:"port,omitempty"` + Interval *int `json:"interval,omitempty"` + Timeout *int `json:"timeout,omitempty"` + Retries *int `json:"retries,omitempty"` + HTTP *LoadBalancerActionAddServiceRequestHealthCheckHTTP `json:"http,omitempty"` +} + +type LoadBalancerActionAddServiceRequestHealthCheckHTTP struct { + Domain *string `json:"domain,omitempty"` + Path *string `json:"path,omitempty"` + Response *string `json:"response,omitempty"` + StatusCodes *[]string `json:"status_codes,omitempty"` + TLS *bool `json:"tls,omitempty"` +} + +type LoadBalancerActionAddServiceResponse struct { + Action Action `json:"action"` +} + +type LoadBalancerActionUpdateServiceRequest struct { + ListenPort int `json:"listen_port"` + Protocol *string `json:"protocol,omitempty"` + DestinationPort *int `json:"destination_port,omitempty"` + Proxyprotocol *bool `json:"proxyprotocol,omitempty"` + HTTP *LoadBalancerActionUpdateServiceRequestHTTP `json:"http,omitempty"` + HealthCheck *LoadBalancerActionUpdateServiceRequestHealthCheck `json:"health_check,omitempty"` +} + +type LoadBalancerActionUpdateServiceRequestHTTP struct { + CookieName *string `json:"cookie_name,omitempty"` + CookieLifetime *int `json:"cookie_lifetime,omitempty"` + Certificates *[]int `json:"certificates,omitempty"` + RedirectHTTP *bool `json:"redirect_http,omitempty"` + StickySessions *bool `json:"sticky_sessions,omitempty"` +} + +type LoadBalancerActionUpdateServiceRequestHealthCheck struct { + Protocol *string `json:"protocol,omitempty"` + Port *int `json:"port,omitempty"` + Interval *int `json:"interval,omitempty"` + Timeout *int `json:"timeout,omitempty"` + Retries *int `json:"retries,omitempty"` + HTTP *LoadBalancerActionUpdateServiceRequestHealthCheckHTTP `json:"http,omitempty"` +} + +type LoadBalancerActionUpdateServiceRequestHealthCheckHTTP struct { + Domain *string `json:"domain,omitempty"` + Path *string `json:"path,omitempty"` + Response *string `json:"response,omitempty"` + StatusCodes *[]string `json:"status_codes,omitempty"` + TLS *bool `json:"tls,omitempty"` +} + +type LoadBalancerActionUpdateServiceResponse struct { + Action Action `json:"action"` +} + +type LoadBalancerDeleteServiceRequest struct { + ListenPort int `json:"listen_port"` +} + +type LoadBalancerDeleteServiceResponse struct { + Action Action `json:"action"` +} + +type LoadBalancerCreateRequest struct { + Name string `json:"name"` + LoadBalancerType interface{} `json:"load_balancer_type"` // int or string + Algorithm *LoadBalancerCreateRequestAlgorithm `json:"algorithm,omitempty"` + Location *string `json:"location,omitempty"` + NetworkZone *string `json:"network_zone,omitempty"` + Labels *map[string]string `json:"labels,omitempty"` + Targets []LoadBalancerCreateRequestTarget `json:"targets,omitempty"` + Services []LoadBalancerCreateRequestService `json:"services,omitempty"` + PublicInterface *bool `json:"public_interface,omitempty"` + Network *int `json:"network,omitempty"` +} + +type LoadBalancerCreateRequestAlgorithm struct { + Type string `json:"type"` +} + +type LoadBalancerCreateRequestTarget struct { + Type string `json:"type"` + Server *LoadBalancerCreateRequestTargetServer `json:"server,omitempty"` + LabelSelector *LoadBalancerCreateRequestTargetLabelSelector `json:"label_selector,omitempty"` + IP *LoadBalancerCreateRequestTargetIP `json:"ip,omitempty"` + UsePrivateIP *bool `json:"use_private_ip,omitempty"` +} + +type LoadBalancerCreateRequestTargetServer struct { + ID int `json:"id"` +} + +type LoadBalancerCreateRequestTargetLabelSelector struct { + Selector string `json:"selector"` +} + +type LoadBalancerCreateRequestTargetIP struct { + IP string `json:"ip"` +} + +type LoadBalancerCreateRequestService struct { + Protocol string `json:"protocol"` + ListenPort *int `json:"listen_port,omitempty"` + DestinationPort *int `json:"destination_port,omitempty"` + Proxyprotocol *bool `json:"proxyprotocol,omitempty"` + HTTP *LoadBalancerCreateRequestServiceHTTP `json:"http,omitempty"` + HealthCheck *LoadBalancerCreateRequestServiceHealthCheck `json:"health_check,omitempty"` +} + +type LoadBalancerCreateRequestServiceHTTP struct { + CookieName *string `json:"cookie_name,omitempty"` + CookieLifetime *int `json:"cookie_lifetime,omitempty"` + Certificates *[]int `json:"certificates,omitempty"` + RedirectHTTP *bool `json:"redirect_http,omitempty"` + StickySessions *bool `json:"sticky_sessions,omitempty"` +} + +type LoadBalancerCreateRequestServiceHealthCheck struct { + Protocol string `json:"protocol"` + Port *int `json:"port,omitempty"` + Interval *int `json:"interval,omitempty"` + Timeout *int `json:"timeout,omitempty"` + Retries *int `json:"retries,omitempty"` + HTTP *LoadBalancerCreateRequestServiceHealthCheckHTTP `json:"http,omitempty"` +} + +type LoadBalancerCreateRequestServiceHealthCheckHTTP struct { + Domain *string `json:"domain,omitempty"` + Path *string `json:"path,omitempty"` + Response *string `json:"response,omitempty"` + StatusCodes *[]string `json:"status_codes,omitempty"` + TLS *bool `json:"tls,omitempty"` +} + +type LoadBalancerCreateResponse struct { + LoadBalancer LoadBalancer `json:"load_balancer"` + Action Action `json:"action"` +} + +type LoadBalancerActionChangeProtectionRequest struct { + Delete *bool `json:"delete,omitempty"` +} + +type LoadBalancerActionChangeProtectionResponse struct { + Action Action `json:"action"` +} + +type LoadBalancerUpdateRequest struct { + Name *string `json:"name,omitempty"` + Labels *map[string]string `json:"labels,omitempty"` +} + +type LoadBalancerUpdateResponse struct { + LoadBalancer LoadBalancer `json:"load_balancer"` +} + +type LoadBalancerActionChangeAlgorithmRequest struct { + Type string `json:"type"` +} + +type LoadBalancerActionChangeAlgorithmResponse struct { + Action Action `json:"action"` +} + +type LoadBalancerActionAttachToNetworkRequest struct { + Network int `json:"network"` + IP *string `json:"ip,omitempty"` +} + +type LoadBalancerActionAttachToNetworkResponse struct { + Action Action `json:"action"` +} + +type LoadBalancerActionDetachFromNetworkRequest struct { + Network int `json:"network"` +} + +type LoadBalancerActionDetachFromNetworkResponse struct { + Action Action `json:"action"` +} + +type LoadBalancerActionEnablePublicInterfaceRequest struct{} + +type LoadBalancerActionEnablePublicInterfaceResponse struct { + Action Action `json:"action"` +} + +type LoadBalancerActionDisablePublicInterfaceRequest struct{} + +type LoadBalancerActionDisablePublicInterfaceResponse struct { + Action Action `json:"action"` +} + +type LoadBalancerActionChangeTypeRequest struct { + LoadBalancerType interface{} `json:"load_balancer_type"` // int or string +} + +type LoadBalancerActionChangeTypeResponse struct { + Action Action `json:"action"` +} diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/load_balancer_type.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/load_balancer_type.go new file mode 100644 index 000000000000..815273841508 --- /dev/null +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/load_balancer_type.go @@ -0,0 +1,41 @@ +/* +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 + +// LoadBalancerType defines the schema of a LoadBalancer type. +type LoadBalancerType struct { + ID int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + MaxConnections int `json:"max_connections"` + MaxServices int `json:"max_services"` + MaxTargets int `json:"max_targets"` + MaxAssignedCertificates int `json:"max_assigned_certificates"` + Prices []PricingLoadBalancerTypePrice `json:"prices"` +} + +// LoadBalancerTypeListResponse defines the schema of the response when +// listing LoadBalancer types. +type LoadBalancerTypeListResponse struct { + LoadBalancerTypes []LoadBalancerType `json:"load_balancer_types"` +} + +// LoadBalancerTypeGetResponse defines the schema of the response when +// retrieving a single LoadBalancer type. +type LoadBalancerTypeGetResponse struct { + LoadBalancerType LoadBalancerType `json:"load_balancer_type"` +} diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/location.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/location.go new file mode 100644 index 000000000000..16ea709c8436 --- /dev/null +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/location.go @@ -0,0 +1,39 @@ +/* +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 + +// Location defines the schema of a location. +type Location struct { + ID int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Country string `json:"country"` + City string `json:"city"` + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` + NetworkZone string `json:"network_zone"` +} + +// LocationGetResponse defines the schema of the response when retrieving a single location. +type LocationGetResponse struct { + Location Location `json:"location"` +} + +// LocationListResponse defines the schema of the response when listing locations. +type LocationListResponse struct { + Locations []Location `json:"locations"` +} diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/meta.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/meta.go new file mode 100644 index 000000000000..e527a2f45a22 --- /dev/null +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/meta.go @@ -0,0 +1,39 @@ +/* +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 + +// Meta defines the schema of meta information which may be included +// in responses. +type Meta struct { + Pagination *MetaPagination `json:"pagination"` +} + +// MetaPagination defines the schema of pagination information. +type MetaPagination struct { + Page int `json:"page"` + PerPage int `json:"per_page"` + PreviousPage int `json:"previous_page"` + NextPage int `json:"next_page"` + LastPage int `json:"last_page"` + TotalEntries int `json:"total_entries"` +} + +// MetaResponse defines the schema of a response containing +// meta information. +type MetaResponse struct { + Meta Meta `json:"meta"` +} diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/network.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/network.go new file mode 100644 index 000000000000..f473f228723e --- /dev/null +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/network.go @@ -0,0 +1,166 @@ +/* +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" + +// Network defines the schema of a network. +type Network struct { + ID int `json:"id"` + Name string `json:"name"` + Created time.Time `json:"created"` + IPRange string `json:"ip_range"` + Subnets []NetworkSubnet `json:"subnets"` + Routes []NetworkRoute `json:"routes"` + Servers []int `json:"servers"` + Protection NetworkProtection `json:"protection"` + Labels map[string]string `json:"labels"` +} + +// NetworkSubnet represents a subnet of a network. +type NetworkSubnet struct { + Type string `json:"type"` + IPRange string `json:"ip_range"` + NetworkZone string `json:"network_zone"` + Gateway string `json:"gateway"` +} + +// NetworkRoute represents a route of a network. +type NetworkRoute struct { + Destination string `json:"destination"` + Gateway string `json:"gateway"` +} + +// NetworkProtection represents the protection level of a network. +type NetworkProtection struct { + Delete bool `json:"delete"` +} + +// NetworkUpdateRequest defines the schema of the request to update a network. +type NetworkUpdateRequest struct { + Name string `json:"name,omitempty"` + Labels *map[string]string `json:"labels,omitempty"` +} + +// NetworkUpdateResponse defines the schema of the response when updating a network. +type NetworkUpdateResponse struct { + Network Network `json:"network"` +} + +// NetworkListResponse defines the schema of the response when +// listing networks. +type NetworkListResponse struct { + Networks []Network `json:"networks"` +} + +// NetworkGetResponse defines the schema of the response when +// retrieving a single network. +type NetworkGetResponse struct { + Network Network `json:"network"` +} + +// NetworkCreateRequest defines the schema of the request to create a network. +type NetworkCreateRequest struct { + Name string `json:"name"` + IPRange string `json:"ip_range"` + Subnets []NetworkSubnet `json:"subnets,omitempty"` + Routes []NetworkRoute `json:"routes,omitempty"` + Labels *map[string]string `json:"labels,omitempty"` +} + +// NetworkCreateResponse defines the schema of the response when +// creating a network. +type NetworkCreateResponse struct { + Network Network `json:"network"` +} + +// NetworkActionChangeIPRangeRequest defines the schema of the request to +// change the IP range of a network. +type NetworkActionChangeIPRangeRequest struct { + IPRange string `json:"ip_range"` +} + +// NetworkActionChangeIPRangeResponse defines the schema of the response when +// changing the IP range of a network. +type NetworkActionChangeIPRangeResponse struct { + Action Action `json:"action"` +} + +// NetworkActionAddSubnetRequest defines the schema of the request to +// add a subnet to a network. +type NetworkActionAddSubnetRequest struct { + Type string `json:"type"` + IPRange string `json:"ip_range,omitempty"` + NetworkZone string `json:"network_zone"` + Gateway string `json:"gateway"` +} + +// NetworkActionAddSubnetResponse defines the schema of the response when +// adding a subnet to a network. +type NetworkActionAddSubnetResponse struct { + Action Action `json:"action"` +} + +// NetworkActionDeleteSubnetRequest defines the schema of the request to +// delete a subnet from a network. +type NetworkActionDeleteSubnetRequest struct { + IPRange string `json:"ip_range"` +} + +// NetworkActionDeleteSubnetResponse defines the schema of the response when +// deleting a subnet from a network. +type NetworkActionDeleteSubnetResponse struct { + Action Action `json:"action"` +} + +// NetworkActionAddRouteRequest defines the schema of the request to +// add a route to a network. +type NetworkActionAddRouteRequest struct { + Destination string `json:"destination"` + Gateway string `json:"gateway"` +} + +// NetworkActionAddRouteResponse defines the schema of the response when +// adding a route to a network. +type NetworkActionAddRouteResponse struct { + Action Action `json:"action"` +} + +// NetworkActionDeleteRouteRequest defines the schema of the request to +// delete a route from a network. +type NetworkActionDeleteRouteRequest struct { + Destination string `json:"destination"` + Gateway string `json:"gateway"` +} + +// NetworkActionDeleteRouteResponse defines the schema of the response when +// deleting a route from a network. +type NetworkActionDeleteRouteResponse struct { + Action Action `json:"action"` +} + +// NetworkActionChangeProtectionRequest defines the schema of the request to +// change the resource protection of a network. +type NetworkActionChangeProtectionRequest struct { + Delete *bool `json:"delete,omitempty"` +} + +// NetworkActionChangeProtectionResponse defines the schema of the response when +// changing the resource protection of a network. +type NetworkActionChangeProtectionResponse struct { + Action Action `json:"action"` +} diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/pricing.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/pricing.go new file mode 100644 index 000000000000..248e352b023d --- /dev/null +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/pricing.go @@ -0,0 +1,90 @@ +/* +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 + +// Pricing defines the schema for pricing information. +type Pricing struct { + Currency string `json:"currency"` + VATRate string `json:"vat_rate"` + Image PricingImage `json:"image"` + FloatingIP PricingFloatingIP `json:"floating_ip"` + Traffic PricingTraffic `json:"traffic"` + ServerBackup PricingServerBackup `json:"server_backup"` + ServerTypes []PricingServerType `json:"server_types"` + LoadBalancerTypes []PricingLoadBalancerType `json:"load_balancer_types"` +} + +// Price defines the schema of a single price with net and gross amount. +type Price struct { + Net string `json:"net"` + Gross string `json:"gross"` +} + +// PricingImage defines the schema of pricing information for an image. +type PricingImage struct { + PricePerGBMonth Price `json:"price_per_gb_month"` +} + +// PricingFloatingIP defines the schema of pricing information for a Floating IP. +type PricingFloatingIP struct { + PriceMonthly Price `json:"price_monthly"` +} + +// PricingTraffic defines the schema of pricing information for traffic. +type PricingTraffic struct { + PricePerTB Price `json:"price_per_tb"` +} + +// PricingServerBackup defines the schema of pricing information for server backups. +type PricingServerBackup struct { + Percentage string `json:"percentage"` +} + +// PricingServerType defines the schema of pricing information for a server type. +type PricingServerType struct { + ID int `json:"id"` + Name string `json:"name"` + Prices []PricingServerTypePrice `json:"prices"` +} + +// PricingServerTypePrice defines the schema of pricing information for a server +// type at a location. +type PricingServerTypePrice struct { + Location string `json:"location"` + PriceHourly Price `json:"price_hourly"` + PriceMonthly Price `json:"price_monthly"` +} + +// PricingLoadBalancerType defines the schema of pricing information for a Load Balancer type. +type PricingLoadBalancerType struct { + ID int `json:"id"` + Name string `json:"name"` + Prices []PricingLoadBalancerTypePrice `json:"prices"` +} + +// PricingLoadBalancerTypePrice defines the schema of pricing information for a Load Balancer +// type at a location. +type PricingLoadBalancerTypePrice struct { + Location string `json:"location"` + PriceHourly Price `json:"price_hourly"` + PriceMonthly Price `json:"price_monthly"` +} + +// PricingGetResponse defines the schema of the response when retrieving pricing information. +type PricingGetResponse struct { + Pricing Pricing `json:"pricing"` +} diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/server.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/server.go new file mode 100644 index 000000000000..649f2ce7e033 --- /dev/null +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/server.go @@ -0,0 +1,383 @@ +/* +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" + +// Server defines the schema of a server. +type Server struct { + ID int `json:"id"` + Name string `json:"name"` + Status string `json:"status"` + Created time.Time `json:"created"` + PublicNet ServerPublicNet `json:"public_net"` + PrivateNet []ServerPrivateNet `json:"private_net"` + ServerType ServerType `json:"server_type"` + IncludedTraffic uint64 `json:"included_traffic"` + OutgoingTraffic *uint64 `json:"outgoing_traffic"` + IngoingTraffic *uint64 `json:"ingoing_traffic"` + BackupWindow *string `json:"backup_window"` + RescueEnabled bool `json:"rescue_enabled"` + ISO *ISO `json:"iso"` + Locked bool `json:"locked"` + Datacenter Datacenter `json:"datacenter"` + Image *Image `json:"image"` + Protection ServerProtection `json:"protection"` + Labels map[string]string `json:"labels"` + Volumes []int `json:"volumes"` + PrimaryDiskSize int `json:"primary_disk_size"` +} + +// ServerProtection defines the schema of a server's resource protection. +type ServerProtection struct { + Delete bool `json:"delete"` + Rebuild bool `json:"rebuild"` +} + +// ServerPublicNet defines the schema of a server's +// public network information. +type ServerPublicNet struct { + IPv4 ServerPublicNetIPv4 `json:"ipv4"` + IPv6 ServerPublicNetIPv6 `json:"ipv6"` + FloatingIPs []int `json:"floating_ips"` +} + +// ServerPublicNetIPv4 defines the schema of a server's public +// network information for an IPv4. +type ServerPublicNetIPv4 struct { + IP string `json:"ip"` + Blocked bool `json:"blocked"` + DNSPtr string `json:"dns_ptr"` +} + +// ServerPublicNetIPv6 defines the schema of a server's public +// network information for an IPv6. +type ServerPublicNetIPv6 struct { + IP string `json:"ip"` + Blocked bool `json:"blocked"` + DNSPtr []ServerPublicNetIPv6DNSPtr `json:"dns_ptr"` +} + +// ServerPublicNetIPv6DNSPtr defines the schema of a server's +// public network information for an IPv6 reverse DNS. +type ServerPublicNetIPv6DNSPtr struct { + IP string `json:"ip"` + DNSPtr string `json:"dns_ptr"` +} + +// ServerPrivateNet defines the schema of a server's private network information. +type ServerPrivateNet struct { + Network int `json:"network"` + IP string `json:"ip"` + AliasIPs []string `json:"alias_ips"` + MACAddress string `json:"mac_address"` +} + +// ServerGetResponse defines the schema of the response when +// retrieving a single server. +type ServerGetResponse struct { + Server Server `json:"server"` +} + +// ServerListResponse defines the schema of the response when +// listing servers. +type ServerListResponse struct { + Servers []Server `json:"servers"` +} + +// ServerCreateRequest defines the schema for the request to +// create a server. +type ServerCreateRequest struct { + Name string `json:"name"` + ServerType interface{} `json:"server_type"` // int or string + Image interface{} `json:"image"` // int or string + SSHKeys []int `json:"ssh_keys,omitempty"` + Location string `json:"location,omitempty"` + Datacenter string `json:"datacenter,omitempty"` + UserData string `json:"user_data,omitempty"` + StartAfterCreate *bool `json:"start_after_create,omitempty"` + Labels *map[string]string `json:"labels,omitempty"` + Automount *bool `json:"automount,omitempty"` + Volumes []int `json:"volumes,omitempty"` + Networks []int `json:"networks,omitempty"` +} + +// ServerCreateResponse defines the schema of the response when +// creating a server. +type ServerCreateResponse struct { + Server Server `json:"server"` + Action Action `json:"action"` + RootPassword *string `json:"root_password"` + NextActions []Action `json:"next_actions"` +} + +// ServerUpdateRequest defines the schema of the request to update a server. +type ServerUpdateRequest struct { + Name string `json:"name,omitempty"` + Labels *map[string]string `json:"labels,omitempty"` +} + +// ServerUpdateResponse defines the schema of the response when updating a server. +type ServerUpdateResponse struct { + Server Server `json:"server"` +} + +// ServerActionPoweronRequest defines the schema for the request to +// create a poweron server action. +type ServerActionPoweronRequest struct{} + +// ServerActionPoweronResponse defines the schema of the response when +// creating a poweron server action. +type ServerActionPoweronResponse struct { + Action Action `json:"action"` +} + +// ServerActionPoweroffRequest defines the schema for the request to +// create a poweroff server action. +type ServerActionPoweroffRequest struct{} + +// ServerActionPoweroffResponse defines the schema of the response when +// creating a poweroff server action. +type ServerActionPoweroffResponse struct { + Action Action `json:"action"` +} + +// ServerActionRebootRequest defines the schema for the request to +// create a reboot server action. +type ServerActionRebootRequest struct{} + +// ServerActionRebootResponse defines the schema of the response when +// creating a reboot server action. +type ServerActionRebootResponse struct { + Action Action `json:"action"` +} + +// ServerActionResetRequest defines the schema for the request to +// create a reset server action. +type ServerActionResetRequest struct{} + +// ServerActionResetResponse defines the schema of the response when +// creating a reset server action. +type ServerActionResetResponse struct { + Action Action `json:"action"` +} + +// ServerActionShutdownRequest defines the schema for the request to +// create a shutdown server action. +type ServerActionShutdownRequest struct{} + +// ServerActionShutdownResponse defines the schema of the response when +// creating a shutdown server action. +type ServerActionShutdownResponse struct { + Action Action `json:"action"` +} + +// ServerActionResetPasswordRequest defines the schema for the request to +// create a reset_password server action. +type ServerActionResetPasswordRequest struct{} + +// ServerActionResetPasswordResponse defines the schema of the response when +// creating a reset_password server action. +type ServerActionResetPasswordResponse struct { + Action Action `json:"action"` + RootPassword string `json:"root_password"` +} + +// ServerActionCreateImageRequest defines the schema for the request to +// create a create_image server action. +type ServerActionCreateImageRequest struct { + Type *string `json:"type"` + Description *string `json:"description"` + Labels *map[string]string `json:"labels,omitempty"` +} + +// ServerActionCreateImageResponse defines the schema of the response when +// creating a create_image server action. +type ServerActionCreateImageResponse struct { + Action Action `json:"action"` + Image Image `json:"image"` +} + +// ServerActionEnableRescueRequest defines the schema for the request to +// create a enable_rescue server action. +type ServerActionEnableRescueRequest struct { + Type *string `json:"type,omitempty"` + SSHKeys []int `json:"ssh_keys,omitempty"` +} + +// ServerActionEnableRescueResponse defines the schema of the response when +// creating a enable_rescue server action. +type ServerActionEnableRescueResponse struct { + Action Action `json:"action"` + RootPassword string `json:"root_password"` +} + +// ServerActionDisableRescueRequest defines the schema for the request to +// create a disable_rescue server action. +type ServerActionDisableRescueRequest struct{} + +// ServerActionDisableRescueResponse defines the schema of the response when +// creating a disable_rescue server action. +type ServerActionDisableRescueResponse struct { + Action Action `json:"action"` +} + +// ServerActionRebuildRequest defines the schema for the request to +// rebuild a server. +type ServerActionRebuildRequest struct { + Image interface{} `json:"image"` // int or string +} + +// ServerActionRebuildResponse defines the schema of the response when +// creating a rebuild server action. +type ServerActionRebuildResponse struct { + Action Action `json:"action"` +} + +// ServerActionAttachISORequest defines the schema for the request to +// attach an ISO to a server. +type ServerActionAttachISORequest struct { + ISO interface{} `json:"iso"` // int or string +} + +// ServerActionAttachISOResponse defines the schema of the response when +// creating a attach_iso server action. +type ServerActionAttachISOResponse struct { + Action Action `json:"action"` +} + +// ServerActionDetachISORequest defines the schema for the request to +// detach an ISO from a server. +type ServerActionDetachISORequest struct{} + +// ServerActionDetachISOResponse defines the schema of the response when +// creating a detach_iso server action. +type ServerActionDetachISOResponse struct { + Action Action `json:"action"` +} + +// ServerActionEnableBackupRequest defines the schema for the request to +// enable backup for a server. +type ServerActionEnableBackupRequest struct { + BackupWindow *string `json:"backup_window,omitempty"` +} + +// ServerActionEnableBackupResponse defines the schema of the response when +// creating a enable_backup server action. +type ServerActionEnableBackupResponse struct { + Action Action `json:"action"` +} + +// ServerActionDisableBackupRequest defines the schema for the request to +// disable backup for a server. +type ServerActionDisableBackupRequest struct{} + +// ServerActionDisableBackupResponse defines the schema of the response when +// creating a disable_backup server action. +type ServerActionDisableBackupResponse struct { + Action Action `json:"action"` +} + +// ServerActionChangeTypeRequest defines the schema for the request to +// change a server's type. +type ServerActionChangeTypeRequest struct { + ServerType interface{} `json:"server_type"` // int or string + UpgradeDisk bool `json:"upgrade_disk"` +} + +// ServerActionChangeTypeResponse defines the schema of the response when +// creating a change_type server action. +type ServerActionChangeTypeResponse struct { + Action Action `json:"action"` +} + +// ServerActionChangeDNSPtrRequest defines the schema for the request to +// change a server's reverse DNS pointer. +type ServerActionChangeDNSPtrRequest struct { + IP string `json:"ip"` + DNSPtr *string `json:"dns_ptr"` +} + +// ServerActionChangeDNSPtrResponse defines the schema of the response when +// creating a change_dns_ptr server action. +type ServerActionChangeDNSPtrResponse struct { + Action Action `json:"action"` +} + +// ServerActionChangeProtectionRequest defines the schema of the request to +// change the resource protection of a server. +type ServerActionChangeProtectionRequest struct { + Rebuild *bool `json:"rebuild,omitempty"` + Delete *bool `json:"delete,omitempty"` +} + +// ServerActionChangeProtectionResponse defines the schema of the response when +// changing the resource protection of a server. +type ServerActionChangeProtectionResponse struct { + Action Action `json:"action"` +} + +// ServerActionRequestConsoleRequest defines the schema of the request to +// request a WebSocket VNC console. +type ServerActionRequestConsoleRequest struct{} + +// ServerActionRequestConsoleResponse defines the schema of the response when +// requesting a WebSocket VNC console. +type ServerActionRequestConsoleResponse struct { + Action Action `json:"action"` + WSSURL string `json:"wss_url"` + Password string `json:"password"` +} + +// ServerActionAttachToNetworkRequest defines the schema for the request to +// attach a network to a server. +type ServerActionAttachToNetworkRequest struct { + Network int `json:"network"` + IP *string `json:"ip,omitempty"` + AliasIPs []*string `json:"alias_ips,omitempty"` +} + +// ServerActionAttachToNetworkResponse defines the schema of the response when +// creating an attach_to_network server action. +type ServerActionAttachToNetworkResponse struct { + Action Action `json:"action"` +} + +// ServerActionDetachFromNetworkRequest defines the schema for the request to +// detach a network from a server. +type ServerActionDetachFromNetworkRequest struct { + Network int `json:"network"` +} + +// ServerActionDetachFromNetworkResponse defines the schema of the response when +// creating a detach_from_network server action. +type ServerActionDetachFromNetworkResponse struct { + Action Action `json:"action"` +} + +// ServerActionChangeAliasIPsRequest defines the schema for the request to +// change a server's alias IPs in a network. +type ServerActionChangeAliasIPsRequest struct { + Network int `json:"network"` + AliasIPs []string `json:"alias_ips"` +} + +// ServerActionChangeAliasIPsResponse defines the schema of the response when +// creating an change_alias_ips server action. +type ServerActionChangeAliasIPsResponse struct { + Action Action `json:"action"` +} 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 new file mode 100644 index 000000000000..75be7ad7bba0 --- /dev/null +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/server_type.go @@ -0,0 +1,42 @@ +/* +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 + +// 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"` +} + +// ServerTypeListResponse defines the schema of the response when +// listing server types. +type ServerTypeListResponse struct { + ServerTypes []ServerType `json:"server_types"` +} + +// ServerTypeGetResponse defines the schema of the response when +// retrieving a single server type. +type ServerTypeGetResponse struct { + ServerType ServerType `json:"server_type"` +} diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/ssh_key.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/ssh_key.go new file mode 100644 index 000000000000..b061e0d06541 --- /dev/null +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/ssh_key.go @@ -0,0 +1,66 @@ +/* +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" + +// SSHKey defines the schema of a SSH key. +type SSHKey struct { + ID int `json:"id"` + Name string `json:"name"` + Fingerprint string `json:"fingerprint"` + PublicKey string `json:"public_key"` + Labels map[string]string `json:"labels"` + Created time.Time `json:"created"` +} + +// SSHKeyCreateRequest defines the schema of the request +// to create a SSH key. +type SSHKeyCreateRequest struct { + Name string `json:"name"` + PublicKey string `json:"public_key"` + Labels *map[string]string `json:"labels,omitempty"` +} + +// SSHKeyCreateResponse defines the schema of the response +// when creating a SSH key. +type SSHKeyCreateResponse struct { + SSHKey SSHKey `json:"ssh_key"` +} + +// SSHKeyListResponse defines the schema of the response +// when listing SSH keys. +type SSHKeyListResponse struct { + SSHKeys []SSHKey `json:"ssh_keys"` +} + +// SSHKeyGetResponse defines the schema of the response +// when retrieving a single SSH key. +type SSHKeyGetResponse struct { + SSHKey SSHKey `json:"ssh_key"` +} + +// SSHKeyUpdateRequest defines the schema of the request to update a SSH key. +type SSHKeyUpdateRequest struct { + Name string `json:"name,omitempty"` + Labels *map[string]string `json:"labels,omitempty"` +} + +// SSHKeyUpdateResponse defines the schema of the response when updating a SSH key. +type SSHKeyUpdateResponse struct { + SSHKey SSHKey `json:"ssh_key"` +} diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/volume.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/volume.go new file mode 100644 index 000000000000..4cef9aef5ef5 --- /dev/null +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema/volume.go @@ -0,0 +1,126 @@ +/* +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" + +// Volume defines the schema of a volume. +type Volume struct { + ID int `json:"id"` + Name string `json:"name"` + Server *int `json:"server"` + Status string `json:"status"` + Location Location `json:"location"` + Size int `json:"size"` + Protection VolumeProtection `json:"protection"` + Labels map[string]string `json:"labels"` + LinuxDevice string `json:"linux_device"` + Created time.Time `json:"created"` +} + +// VolumeCreateRequest defines the schema of the request +// to create a volume. +type VolumeCreateRequest struct { + Name string `json:"name"` + Size int `json:"size"` + Server *int `json:"server,omitempty"` + Location interface{} `json:"location,omitempty"` // int, string, or nil + Labels *map[string]string `json:"labels,omitempty"` + Automount *bool `json:"automount,omitempty"` + Format *string `json:"format,omitempty"` +} + +// VolumeCreateResponse defines the schema of the response +// when creating a volume. +type VolumeCreateResponse struct { + Volume Volume `json:"volume"` + Action *Action `json:"action"` + NextActions []Action `json:"next_actions"` +} + +// VolumeListResponse defines the schema of the response +// when listing volumes. +type VolumeListResponse struct { + Volumes []Volume `json:"volumes"` +} + +// VolumeGetResponse defines the schema of the response +// when retrieving a single volume. +type VolumeGetResponse struct { + Volume Volume `json:"volume"` +} + +// VolumeUpdateRequest defines the schema of the request to update a volume. +type VolumeUpdateRequest struct { + Name string `json:"name,omitempty"` + Labels *map[string]string `json:"labels,omitempty"` +} + +// VolumeUpdateResponse defines the schema of the response when updating a volume. +type VolumeUpdateResponse struct { + Volume Volume `json:"volume"` +} + +// VolumeProtection defines the schema of a volume's resource protection. +type VolumeProtection struct { + Delete bool `json:"delete"` +} + +// VolumeActionChangeProtectionRequest defines the schema of the request to +// change the resource protection of a volume. +type VolumeActionChangeProtectionRequest struct { + Delete *bool `json:"delete,omitempty"` +} + +// VolumeActionChangeProtectionResponse defines the schema of the response when +// changing the resource protection of a volume. +type VolumeActionChangeProtectionResponse struct { + Action Action `json:"action"` +} + +// VolumeActionAttachVolumeRequest defines the schema of the request to +// attach a volume to a server. +type VolumeActionAttachVolumeRequest struct { + Server int `json:"server"` + Automount *bool `json:"automount,omitempty"` +} + +// VolumeActionAttachVolumeResponse defines the schema of the response when +// attaching a volume to a server. +type VolumeActionAttachVolumeResponse struct { + Action Action `json:"action"` +} + +// VolumeActionDetachVolumeRequest defines the schema of the request to +// create an detach volume action. +type VolumeActionDetachVolumeRequest struct{} + +// VolumeActionDetachVolumeResponse defines the schema of the response when +// creating an detach volume action. +type VolumeActionDetachVolumeResponse struct { + Action Action `json:"action"` +} + +// VolumeActionResizeVolumeRequest defines the schema of the request to resize a volume. +type VolumeActionResizeVolumeRequest struct { + Size int `json:"size"` +} + +// VolumeActionResizeVolumeResponse defines the schema of the response when resizing a volume. +type VolumeActionResizeVolumeResponse 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 new file mode 100644 index 000000000000..f7bbe06d6593 --- /dev/null +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/server.go @@ -0,0 +1,970 @@ +/* +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" + "net/url" + "strconv" + "time" + + "k8s.io/autoscaler/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema" +) + +// Server represents a server in the Hetzner Cloud. +type Server struct { + ID int + Name string + Status ServerStatus + Created time.Time + PublicNet ServerPublicNet + PrivateNet []ServerPrivateNet + ServerType *ServerType + Datacenter *Datacenter + IncludedTraffic uint64 + OutgoingTraffic uint64 + IngoingTraffic uint64 + BackupWindow string + RescueEnabled bool + Locked bool + ISO *ISO + Image *Image + Protection ServerProtection + Labels map[string]string + Volumes []*Volume + PrimaryDiskSize int +} + +// ServerProtection represents the protection level of a server. +type ServerProtection struct { + Delete, Rebuild bool +} + +// ServerStatus specifies a server's status. +type ServerStatus string + +const ( + // ServerStatusInitializing is the status when a server is initializing. + ServerStatusInitializing ServerStatus = "initializing" + + // ServerStatusOff is the status when a server is off. + ServerStatusOff ServerStatus = "off" + + // ServerStatusRunning is the status when a server is running. + ServerStatusRunning ServerStatus = "running" + + // ServerStatusStarting is the status when a server is being started. + ServerStatusStarting ServerStatus = "starting" + + // ServerStatusStopping is the status when a server is being stopped. + ServerStatusStopping ServerStatus = "stopping" + + // ServerStatusMigrating is the status when a server is being migrated. + ServerStatusMigrating ServerStatus = "migrating" + + // ServerStatusRebuilding is the status when a server is being rebuilt. + ServerStatusRebuilding ServerStatus = "rebuilding" + + // ServerStatusDeleting is the status when a server is being deleted. + ServerStatusDeleting ServerStatus = "deleting" + + // ServerStatusUnknown is the status when a server's state is unknown. + ServerStatusUnknown ServerStatus = "unknown" +) + +// ServerPublicNet represents a server's public network. +type ServerPublicNet struct { + IPv4 ServerPublicNetIPv4 + IPv6 ServerPublicNetIPv6 + FloatingIPs []*FloatingIP +} + +// ServerPublicNetIPv4 represents a server's public IPv4 address. +type ServerPublicNetIPv4 struct { + IP net.IP + Blocked bool + DNSPtr string +} + +// ServerPublicNetIPv6 represents a server's public IPv6 network and address. +type ServerPublicNetIPv6 struct { + IP net.IP + Network *net.IPNet + Blocked bool + DNSPtr map[string]string +} + +// ServerPrivateNet defines the schema of a server's private network information. +type ServerPrivateNet struct { + Network *Network + IP net.IP + Aliases []net.IP + MACAddress string +} + +// DNSPtrForIP returns the reverse dns pointer of the ip address. +func (s *ServerPublicNetIPv6) DNSPtrForIP(ip net.IP) string { + return s.DNSPtr[ip.String()] +} + +// ServerRescueType represents rescue types. +type ServerRescueType string + +// List of rescue types. +const ( + ServerRescueTypeLinux32 ServerRescueType = "linux32" + ServerRescueTypeLinux64 ServerRescueType = "linux64" + ServerRescueTypeFreeBSD64 ServerRescueType = "freebsd64" +) + +// ServerClient is a client for the servers API. +type ServerClient struct { + client *Client +} + +// GetByID retrieves a server by its ID. If the server does not exist, nil is returned. +func (c *ServerClient) GetByID(ctx context.Context, id int) (*Server, *Response, error) { + req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/servers/%d", id), nil) + if err != nil { + return nil, nil, err + } + + var body schema.ServerGetResponse + resp, err := c.client.Do(req, &body) + if err != nil { + if IsError(err, ErrorCodeNotFound) { + return nil, resp, nil + } + return nil, nil, err + } + return ServerFromSchema(body.Server), resp, nil +} + +// GetByName retrieves a server by its name. If the server does not exist, nil is returned. +func (c *ServerClient) GetByName(ctx context.Context, name string) (*Server, *Response, error) { + if name == "" { + return nil, nil, nil + } + servers, response, err := c.List(ctx, ServerListOpts{Name: name}) + if len(servers) == 0 { + return nil, response, err + } + return servers[0], response, err +} + +// Get retrieves a server by its ID if the input can be parsed as an integer, otherwise it +// retrieves a server by its name. If the server does not exist, nil is returned. +func (c *ServerClient) Get(ctx context.Context, idOrName string) (*Server, *Response, error) { + if id, err := strconv.Atoi(idOrName); err == nil { + return c.GetByID(ctx, int(id)) + } + return c.GetByName(ctx, idOrName) +} + +// ServerListOpts specifies options for listing servers. +type ServerListOpts struct { + ListOpts + Name string + Status []ServerStatus +} + +func (l ServerListOpts) values() url.Values { + vals := l.ListOpts.values() + if l.Name != "" { + vals.Add("name", l.Name) + } + for _, status := range l.Status { + vals.Add("status", string(status)) + } + return vals +} + +// List returns a list of servers 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 *ServerClient) List(ctx context.Context, opts ServerListOpts) ([]*Server, *Response, error) { + path := "/servers?" + opts.values().Encode() + req, err := c.client.NewRequest(ctx, "GET", path, nil) + if err != nil { + return nil, nil, err + } + + var body schema.ServerListResponse + resp, err := c.client.Do(req, &body) + if err != nil { + return nil, nil, err + } + servers := make([]*Server, 0, len(body.Servers)) + for _, s := range body.Servers { + servers = append(servers, ServerFromSchema(s)) + } + return servers, resp, nil +} + +// All returns all servers. +func (c *ServerClient) All(ctx context.Context) ([]*Server, error) { + return c.AllWithOpts(ctx, ServerListOpts{ListOpts: ListOpts{PerPage: 50}}) +} + +// AllWithOpts returns all servers for the given options. +func (c *ServerClient) AllWithOpts(ctx context.Context, opts ServerListOpts) ([]*Server, error) { + allServers := []*Server{} + + _, err := c.client.all(func(page int) (*Response, error) { + opts.Page = page + servers, resp, err := c.List(ctx, opts) + if err != nil { + return resp, err + } + allServers = append(allServers, servers...) + return resp, nil + }) + if err != nil { + return nil, err + } + + return allServers, nil +} + +// ServerCreateOpts specifies options for creating a new server. +type ServerCreateOpts struct { + Name string + ServerType *ServerType + Image *Image + SSHKeys []*SSHKey + Location *Location + Datacenter *Datacenter + UserData string + StartAfterCreate *bool + Labels map[string]string + Automount *bool + Volumes []*Volume + Networks []*Network +} + +// Validate checks if options are valid. +func (o ServerCreateOpts) Validate() error { + if o.Name == "" { + return errors.New("missing name") + } + if o.ServerType == nil || (o.ServerType.ID == 0 && o.ServerType.Name == "") { + return errors.New("missing server type") + } + if o.Image == nil || (o.Image.ID == 0 && o.Image.Name == "") { + return errors.New("missing image") + } + if o.Location != nil && o.Datacenter != nil { + return errors.New("location and datacenter are mutually exclusive") + } + return nil +} + +// ServerCreateResult is the result of a create server call. +type ServerCreateResult struct { + Server *Server + Action *Action + RootPassword string + NextActions []*Action +} + +// Create creates a new server. +func (c *ServerClient) Create(ctx context.Context, opts ServerCreateOpts) (ServerCreateResult, *Response, error) { + if err := opts.Validate(); err != nil { + return ServerCreateResult{}, nil, err + } + + var reqBody schema.ServerCreateRequest + reqBody.UserData = opts.UserData + reqBody.Name = opts.Name + reqBody.Automount = opts.Automount + reqBody.StartAfterCreate = opts.StartAfterCreate + if opts.ServerType.ID != 0 { + reqBody.ServerType = opts.ServerType.ID + } else if opts.ServerType.Name != "" { + reqBody.ServerType = opts.ServerType.Name + } + if opts.Image.ID != 0 { + reqBody.Image = opts.Image.ID + } else if opts.Image.Name != "" { + reqBody.Image = opts.Image.Name + } + if opts.Labels != nil { + reqBody.Labels = &opts.Labels + } + for _, sshKey := range opts.SSHKeys { + reqBody.SSHKeys = append(reqBody.SSHKeys, sshKey.ID) + } + for _, volume := range opts.Volumes { + reqBody.Volumes = append(reqBody.Volumes, volume.ID) + } + for _, network := range opts.Networks { + reqBody.Networks = append(reqBody.Networks, network.ID) + } + + if opts.Location != nil { + if opts.Location.ID != 0 { + reqBody.Location = strconv.Itoa(opts.Location.ID) + } else { + reqBody.Location = opts.Location.Name + } + } + if opts.Datacenter != nil { + if opts.Datacenter.ID != 0 { + reqBody.Datacenter = strconv.Itoa(opts.Datacenter.ID) + } else { + reqBody.Datacenter = opts.Datacenter.Name + } + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return ServerCreateResult{}, nil, err + } + + req, err := c.client.NewRequest(ctx, "POST", "/servers", bytes.NewReader(reqBodyData)) + if err != nil { + return ServerCreateResult{}, nil, err + } + + var respBody schema.ServerCreateResponse + resp, err := c.client.Do(req, &respBody) + if err != nil { + return ServerCreateResult{}, resp, err + } + result := ServerCreateResult{ + Server: ServerFromSchema(respBody.Server), + Action: ActionFromSchema(respBody.Action), + NextActions: ActionsFromSchema(respBody.NextActions), + } + if respBody.RootPassword != nil { + result.RootPassword = *respBody.RootPassword + } + return result, resp, nil +} + +// Delete deletes a server. +func (c *ServerClient) Delete(ctx context.Context, server *Server) (*Response, error) { + req, err := c.client.NewRequest(ctx, "DELETE", fmt.Sprintf("/servers/%d", server.ID), nil) + if err != nil { + return nil, err + } + return c.client.Do(req, nil) +} + +// ServerUpdateOpts specifies options for updating a server. +type ServerUpdateOpts struct { + Name string + Labels map[string]string +} + +// Update updates a server. +func (c *ServerClient) Update(ctx context.Context, server *Server, opts ServerUpdateOpts) (*Server, *Response, error) { + reqBody := schema.ServerUpdateRequest{ + 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("/servers/%d", server.ID) + req, err := c.client.NewRequest(ctx, "PUT", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + respBody := schema.ServerUpdateResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ServerFromSchema(respBody.Server), resp, nil +} + +// Poweron starts a server. +func (c *ServerClient) Poweron(ctx context.Context, server *Server) (*Action, *Response, error) { + path := fmt.Sprintf("/servers/%d/actions/poweron", server.ID) + req, err := c.client.NewRequest(ctx, "POST", path, nil) + if err != nil { + return nil, nil, err + } + + respBody := schema.ServerActionPoweronResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, nil +} + +// Reboot reboots a server. +func (c *ServerClient) Reboot(ctx context.Context, server *Server) (*Action, *Response, error) { + path := fmt.Sprintf("/servers/%d/actions/reboot", server.ID) + req, err := c.client.NewRequest(ctx, "POST", path, nil) + if err != nil { + return nil, nil, err + } + + respBody := schema.ServerActionRebootResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, nil +} + +// Reset resets a server. +func (c *ServerClient) Reset(ctx context.Context, server *Server) (*Action, *Response, error) { + path := fmt.Sprintf("/servers/%d/actions/reset", server.ID) + req, err := c.client.NewRequest(ctx, "POST", path, nil) + if err != nil { + return nil, nil, err + } + + respBody := schema.ServerActionResetResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, nil +} + +// Shutdown shuts down a server. +func (c *ServerClient) Shutdown(ctx context.Context, server *Server) (*Action, *Response, error) { + path := fmt.Sprintf("/servers/%d/actions/shutdown", server.ID) + req, err := c.client.NewRequest(ctx, "POST", path, nil) + if err != nil { + return nil, nil, err + } + + respBody := schema.ServerActionShutdownResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, nil +} + +// Poweroff stops a server. +func (c *ServerClient) Poweroff(ctx context.Context, server *Server) (*Action, *Response, error) { + path := fmt.Sprintf("/servers/%d/actions/poweroff", server.ID) + req, err := c.client.NewRequest(ctx, "POST", path, nil) + if err != nil { + return nil, nil, err + } + + respBody := schema.ServerActionPoweroffResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, nil +} + +// ServerResetPasswordResult is the result of resetting a server's password. +type ServerResetPasswordResult struct { + Action *Action + RootPassword string +} + +// ResetPassword resets a server's password. +func (c *ServerClient) ResetPassword(ctx context.Context, server *Server) (ServerResetPasswordResult, *Response, error) { + path := fmt.Sprintf("/servers/%d/actions/reset_password", server.ID) + req, err := c.client.NewRequest(ctx, "POST", path, nil) + if err != nil { + return ServerResetPasswordResult{}, nil, err + } + + respBody := schema.ServerActionResetPasswordResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return ServerResetPasswordResult{}, resp, err + } + return ServerResetPasswordResult{ + Action: ActionFromSchema(respBody.Action), + RootPassword: respBody.RootPassword, + }, resp, nil +} + +// ServerCreateImageOpts specifies options for creating an image from a server. +type ServerCreateImageOpts struct { + Type ImageType + Description *string + Labels map[string]string +} + +// Validate checks if options are valid. +func (o ServerCreateImageOpts) Validate() error { + switch o.Type { + case ImageTypeSnapshot, ImageTypeBackup: + break + case "": + break + default: + return errors.New("invalid type") + } + + return nil +} + +// ServerCreateImageResult is the result of creating an image from a server. +type ServerCreateImageResult struct { + Action *Action + Image *Image +} + +// CreateImage creates an image from a server. +func (c *ServerClient) CreateImage(ctx context.Context, server *Server, opts *ServerCreateImageOpts) (ServerCreateImageResult, *Response, error) { + var reqBody schema.ServerActionCreateImageRequest + if opts != nil { + if err := opts.Validate(); err != nil { + return ServerCreateImageResult{}, nil, fmt.Errorf("invalid options: %s", err) + } + if opts.Description != nil { + reqBody.Description = opts.Description + } + if opts.Type != "" { + reqBody.Type = String(string(opts.Type)) + } + if opts.Labels != nil { + reqBody.Labels = &opts.Labels + } + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return ServerCreateImageResult{}, nil, err + } + + path := fmt.Sprintf("/servers/%d/actions/create_image", server.ID) + req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) + if err != nil { + return ServerCreateImageResult{}, nil, err + } + + respBody := schema.ServerActionCreateImageResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return ServerCreateImageResult{}, resp, err + } + return ServerCreateImageResult{ + Action: ActionFromSchema(respBody.Action), + Image: ImageFromSchema(respBody.Image), + }, resp, nil +} + +// ServerEnableRescueOpts specifies options for enabling rescue mode for a server. +type ServerEnableRescueOpts struct { + Type ServerRescueType + SSHKeys []*SSHKey +} + +// ServerEnableRescueResult is the result of enabling rescue mode for a server. +type ServerEnableRescueResult struct { + Action *Action + RootPassword string +} + +// 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)), + } + for _, sshKey := range opts.SSHKeys { + reqBody.SSHKeys = append(reqBody.SSHKeys, sshKey.ID) + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return ServerEnableRescueResult{}, nil, err + } + + path := fmt.Sprintf("/servers/%d/actions/enable_rescue", server.ID) + req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) + if err != nil { + return ServerEnableRescueResult{}, nil, err + } + + respBody := schema.ServerActionEnableRescueResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return ServerEnableRescueResult{}, resp, err + } + result := ServerEnableRescueResult{ + Action: ActionFromSchema(respBody.Action), + RootPassword: respBody.RootPassword, + } + return result, resp, nil +} + +// DisableRescue disables rescue mode for a server. +func (c *ServerClient) DisableRescue(ctx context.Context, server *Server) (*Action, *Response, error) { + path := fmt.Sprintf("/servers/%d/actions/disable_rescue", server.ID) + req, err := c.client.NewRequest(ctx, "POST", path, nil) + if err != nil { + return nil, nil, err + } + + respBody := schema.ServerActionDisableRescueResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, nil +} + +// ServerRebuildOpts specifies options for rebuilding a server. +type ServerRebuildOpts struct { + Image *Image +} + +// Rebuild rebuilds a server. +func (c *ServerClient) Rebuild(ctx context.Context, server *Server, opts ServerRebuildOpts) (*Action, *Response, error) { + reqBody := schema.ServerActionRebuildRequest{} + if opts.Image.ID != 0 { + reqBody.Image = opts.Image.ID + } else { + reqBody.Image = opts.Image.Name + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, 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 + } + + respBody := schema.ServerActionRebuildResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, nil +} + +// AttachISO attaches an ISO to a server. +func (c *ServerClient) AttachISO(ctx context.Context, server *Server, iso *ISO) (*Action, *Response, error) { + reqBody := schema.ServerActionAttachISORequest{} + if iso.ID != 0 { + reqBody.ISO = iso.ID + } else { + reqBody.ISO = iso.Name + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/servers/%d/actions/attach_iso", server.ID) + req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + respBody := schema.ServerActionAttachISOResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, nil +} + +// DetachISO detaches the currently attached ISO from a server. +func (c *ServerClient) DetachISO(ctx context.Context, server *Server) (*Action, *Response, error) { + path := fmt.Sprintf("/servers/%d/actions/detach_iso", server.ID) + req, err := c.client.NewRequest(ctx, "POST", path, nil) + if err != nil { + return nil, nil, err + } + + respBody := schema.ServerActionDetachISOResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, nil +} + +// EnableBackup enables backup for a server. Pass in an empty backup window to let the +// API pick a window for you. See the API documentation at docs.hetzner.cloud for a list +// of valid backup windows. +func (c *ServerClient) EnableBackup(ctx context.Context, server *Server, window string) (*Action, *Response, error) { + reqBody := schema.ServerActionEnableBackupRequest{} + if window != "" { + reqBody.BackupWindow = String(window) + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/servers/%d/actions/enable_backup", server.ID) + req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + respBody := schema.ServerActionEnableBackupResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, nil +} + +// DisableBackup disables backup for a server. +func (c *ServerClient) DisableBackup(ctx context.Context, server *Server) (*Action, *Response, error) { + path := fmt.Sprintf("/servers/%d/actions/disable_backup", server.ID) + req, err := c.client.NewRequest(ctx, "POST", path, nil) + if err != nil { + return nil, nil, err + } + + respBody := schema.ServerActionDisableBackupResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, nil +} + +// ServerChangeTypeOpts specifies options for changing a server's type. +type ServerChangeTypeOpts struct { + ServerType *ServerType // new server type + UpgradeDisk bool // whether disk should be upgraded +} + +// ChangeType changes a server's type. +func (c *ServerClient) ChangeType(ctx context.Context, server *Server, opts ServerChangeTypeOpts) (*Action, *Response, error) { + reqBody := schema.ServerActionChangeTypeRequest{ + UpgradeDisk: opts.UpgradeDisk, + } + if opts.ServerType.ID != 0 { + reqBody.ServerType = opts.ServerType.ID + } else { + reqBody.ServerType = opts.ServerType.Name + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/servers/%d/actions/change_type", server.ID) + req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + respBody := schema.ServerActionChangeTypeResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, nil +} + +// 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 + } + + respBody := schema.ServerActionChangeDNSPtrResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, nil +} + +// ServerChangeProtectionOpts specifies options for changing the resource protection level of a server. +type ServerChangeProtectionOpts struct { + Rebuild *bool + Delete *bool +} + +// ChangeProtection changes the resource protection level of a server. +func (c *ServerClient) ChangeProtection(ctx context.Context, server *Server, opts ServerChangeProtectionOpts) (*Action, *Response, error) { + reqBody := schema.ServerActionChangeProtectionRequest{ + Rebuild: opts.Rebuild, + Delete: opts.Delete, + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/servers/%d/actions/change_protection", server.ID) + req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + respBody := schema.ServerActionChangeProtectionResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, err +} + +// ServerRequestConsoleResult is the result of requesting a WebSocket VNC console. +type ServerRequestConsoleResult struct { + Action *Action + WSSURL string + Password string +} + +// RequestConsole requests a WebSocket VNC console. +func (c *ServerClient) RequestConsole(ctx context.Context, server *Server) (ServerRequestConsoleResult, *Response, error) { + path := fmt.Sprintf("/servers/%d/actions/request_console", server.ID) + req, err := c.client.NewRequest(ctx, "POST", path, nil) + if err != nil { + return ServerRequestConsoleResult{}, nil, err + } + + respBody := schema.ServerActionRequestConsoleResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return ServerRequestConsoleResult{}, resp, err + } + return ServerRequestConsoleResult{ + Action: ActionFromSchema(respBody.Action), + WSSURL: respBody.WSSURL, + Password: respBody.Password, + }, resp, nil +} + +// ServerAttachToNetworkOpts specifies options for attaching a server to a network. +type ServerAttachToNetworkOpts struct { + Network *Network + IP net.IP + AliasIPs []net.IP +} + +// AttachToNetwork attaches a server to a network. +func (c *ServerClient) AttachToNetwork(ctx context.Context, server *Server, opts ServerAttachToNetworkOpts) (*Action, *Response, error) { + reqBody := schema.ServerActionAttachToNetworkRequest{ + Network: opts.Network.ID, + } + if opts.IP != nil { + reqBody.IP = String(opts.IP.String()) + } + for _, aliasIP := range opts.AliasIPs { + reqBody.AliasIPs = append(reqBody.AliasIPs, String(aliasIP.String())) + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/servers/%d/actions/attach_to_network", server.ID) + req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + respBody := schema.ServerActionAttachToNetworkResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, err +} + +// ServerDetachFromNetworkOpts specifies options for detaching a server from a network. +type ServerDetachFromNetworkOpts struct { + Network *Network +} + +// DetachFromNetwork detaches a server from a network. +func (c *ServerClient) DetachFromNetwork(ctx context.Context, server *Server, opts ServerDetachFromNetworkOpts) (*Action, *Response, error) { + reqBody := schema.ServerActionDetachFromNetworkRequest{ + Network: opts.Network.ID, + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/servers/%d/actions/detach_from_network", server.ID) + req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + respBody := schema.ServerActionDetachFromNetworkResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, err +} + +// ServerChangeAliasIPsOpts specifies options for changing the alias ips of an already attached network. +type ServerChangeAliasIPsOpts struct { + Network *Network + AliasIPs []net.IP +} + +// ChangeAliasIPs changes a server's alias IPs in a network. +func (c *ServerClient) ChangeAliasIPs(ctx context.Context, server *Server, opts ServerChangeAliasIPsOpts) (*Action, *Response, error) { + reqBody := schema.ServerActionChangeAliasIPsRequest{ + Network: opts.Network.ID, + AliasIPs: []string{}, + } + for _, aliasIP := range opts.AliasIPs { + reqBody.AliasIPs = append(reqBody.AliasIPs, aliasIP.String()) + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + path := fmt.Sprintf("/servers/%d/actions/change_alias_ips", server.ID) + req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + respBody := schema.ServerActionDetachFromNetworkResponse{} + 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/hcloud-go/hcloud/server_type.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/server_type.go new file mode 100644 index 000000000000..b32b124aa5d2 --- /dev/null +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/server_type.go @@ -0,0 +1,165 @@ +/* +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/url" + "strconv" + + "k8s.io/autoscaler/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/schema" +) + +// 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 +} + +// StorageType specifies the type of storage. +type StorageType string + +const ( + // StorageTypeLocal is the type for local storage. + StorageTypeLocal StorageType = "local" + + // StorageTypeCeph is the type for remote storage. + StorageTypeCeph StorageType = "ceph" +) + +// CPUType specifies the type of the CPU. +type CPUType string + +const ( + // CPUTypeShared is the type for shared CPU. + CPUTypeShared CPUType = "shared" + + //CPUTypeDedicated is the type for dedicated CPU. + CPUTypeDedicated CPUType = "dedicated" +) + +// ServerTypeClient is a client for the server types API. +type ServerTypeClient struct { + client *Client +} + +// GetByID retrieves a server type by its ID. If the server type does not exist, nil is returned. +func (c *ServerTypeClient) GetByID(ctx context.Context, id int) (*ServerType, *Response, error) { + req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/server_types/%d", id), nil) + if err != nil { + return nil, nil, err + } + + var body schema.ServerTypeGetResponse + resp, err := c.client.Do(req, &body) + if err != nil { + if IsError(err, ErrorCodeNotFound) { + return nil, resp, nil + } + return nil, nil, err + } + return ServerTypeFromSchema(body.ServerType), resp, nil +} + +// GetByName retrieves a server type by its name. If the server type does not exist, nil is returned. +func (c *ServerTypeClient) GetByName(ctx context.Context, name string) (*ServerType, *Response, error) { + if name == "" { + return nil, nil, nil + } + serverTypes, response, err := c.List(ctx, ServerTypeListOpts{Name: name}) + if len(serverTypes) == 0 { + return nil, response, err + } + return serverTypes[0], response, err +} + +// Get retrieves a server type by its ID if the input can be parsed as an integer, otherwise it +// retrieves a server type by its name. If the server type does not exist, nil is returned. +func (c *ServerTypeClient) Get(ctx context.Context, idOrName string) (*ServerType, *Response, error) { + if id, err := strconv.Atoi(idOrName); err == nil { + return c.GetByID(ctx, int(id)) + } + return c.GetByName(ctx, idOrName) +} + +// ServerTypeListOpts specifies options for listing server types. +type ServerTypeListOpts struct { + ListOpts + Name string +} + +func (l ServerTypeListOpts) values() url.Values { + vals := l.ListOpts.values() + if l.Name != "" { + vals.Add("name", l.Name) + } + return vals +} + +// List returns a list of server types 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 *ServerTypeClient) List(ctx context.Context, opts ServerTypeListOpts) ([]*ServerType, *Response, error) { + path := "/server_types?" + opts.values().Encode() + req, err := c.client.NewRequest(ctx, "GET", path, nil) + if err != nil { + return nil, nil, err + } + + var body schema.ServerTypeListResponse + resp, err := c.client.Do(req, &body) + if err != nil { + return nil, nil, err + } + serverTypes := make([]*ServerType, 0, len(body.ServerTypes)) + for _, s := range body.ServerTypes { + serverTypes = append(serverTypes, ServerTypeFromSchema(s)) + } + return serverTypes, resp, nil +} + +// All returns all server types. +func (c *ServerTypeClient) All(ctx context.Context) ([]*ServerType, error) { + allServerTypes := []*ServerType{} + + opts := ServerTypeListOpts{} + opts.PerPage = 50 + + _, err := c.client.all(func(page int) (*Response, error) { + opts.Page = page + serverTypes, resp, err := c.List(ctx, opts) + if err != nil { + return resp, err + } + allServerTypes = append(allServerTypes, serverTypes...) + return resp, nil + }) + if err != nil { + return nil, err + } + + return allServerTypes, nil +} diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/ssh_key.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/ssh_key.go new file mode 100644 index 000000000000..ecba5da65e80 --- /dev/null +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/ssh_key.go @@ -0,0 +1,249 @@ +/* +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" +) + +// SSHKey represents a SSH key in the Hetzner Cloud. +type SSHKey struct { + ID int + Name string + Fingerprint string + PublicKey string + Labels map[string]string + Created time.Time +} + +// SSHKeyClient is a client for the SSH keys API. +type SSHKeyClient struct { + client *Client +} + +// GetByID retrieves a SSH key by its ID. If the SSH key does not exist, nil is returned. +func (c *SSHKeyClient) GetByID(ctx context.Context, id int) (*SSHKey, *Response, error) { + req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/ssh_keys/%d", id), nil) + if err != nil { + return nil, nil, err + } + + var body schema.SSHKeyGetResponse + resp, err := c.client.Do(req, &body) + if err != nil { + if IsError(err, ErrorCodeNotFound) { + return nil, resp, nil + } + return nil, nil, err + } + return SSHKeyFromSchema(body.SSHKey), resp, nil +} + +// GetByName retrieves a SSH key by its name. If the SSH key does not exist, nil is returned. +func (c *SSHKeyClient) GetByName(ctx context.Context, name string) (*SSHKey, *Response, error) { + if name == "" { + return nil, nil, nil + } + sshKeys, response, err := c.List(ctx, SSHKeyListOpts{Name: name}) + if len(sshKeys) == 0 { + return nil, response, err + } + return sshKeys[0], response, err +} + +// GetByFingerprint retreives a SSH key by its fingerprint. If the SSH key does not exist, nil is returned. +func (c *SSHKeyClient) GetByFingerprint(ctx context.Context, fingerprint string) (*SSHKey, *Response, error) { + sshKeys, response, err := c.List(ctx, SSHKeyListOpts{Fingerprint: fingerprint}) + if len(sshKeys) == 0 { + return nil, response, err + } + return sshKeys[0], response, err +} + +// Get retrieves a SSH key by its ID if the input can be parsed as an integer, otherwise it +// retrieves a SSH key by its name. If the SSH key does not exist, nil is returned. +func (c *SSHKeyClient) Get(ctx context.Context, idOrName string) (*SSHKey, *Response, error) { + if id, err := strconv.Atoi(idOrName); err == nil { + return c.GetByID(ctx, int(id)) + } + return c.GetByName(ctx, idOrName) +} + +// SSHKeyListOpts specifies options for listing SSH keys. +type SSHKeyListOpts struct { + ListOpts + Name string + Fingerprint string +} + +func (l SSHKeyListOpts) values() url.Values { + vals := l.ListOpts.values() + if l.Name != "" { + vals.Add("name", l.Name) + } + if l.Fingerprint != "" { + vals.Add("fingerprint", l.Fingerprint) + } + return vals +} + +// List returns a list of SSH keys 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 *SSHKeyClient) List(ctx context.Context, opts SSHKeyListOpts) ([]*SSHKey, *Response, error) { + path := "/ssh_keys?" + opts.values().Encode() + req, err := c.client.NewRequest(ctx, "GET", path, nil) + if err != nil { + return nil, nil, err + } + + var body schema.SSHKeyListResponse + resp, err := c.client.Do(req, &body) + if err != nil { + return nil, nil, err + } + sshKeys := make([]*SSHKey, 0, len(body.SSHKeys)) + for _, s := range body.SSHKeys { + sshKeys = append(sshKeys, SSHKeyFromSchema(s)) + } + return sshKeys, resp, nil +} + +// All returns all SSH keys. +func (c *SSHKeyClient) All(ctx context.Context) ([]*SSHKey, error) { + return c.AllWithOpts(ctx, SSHKeyListOpts{ListOpts: ListOpts{PerPage: 50}}) +} + +// AllWithOpts returns all SSH keys with the given options. +func (c *SSHKeyClient) AllWithOpts(ctx context.Context, opts SSHKeyListOpts) ([]*SSHKey, error) { + allSSHKeys := []*SSHKey{} + + _, err := c.client.all(func(page int) (*Response, error) { + opts.Page = page + sshKeys, resp, err := c.List(ctx, opts) + if err != nil { + return resp, err + } + allSSHKeys = append(allSSHKeys, sshKeys...) + return resp, nil + }) + if err != nil { + return nil, err + } + + return allSSHKeys, nil +} + +// SSHKeyCreateOpts specifies parameters for creating a SSH key. +type SSHKeyCreateOpts struct { + Name string + PublicKey string + Labels map[string]string +} + +// Validate checks if options are valid. +func (o SSHKeyCreateOpts) Validate() error { + if o.Name == "" { + return errors.New("missing name") + } + if o.PublicKey == "" { + return errors.New("missing public key") + } + return nil +} + +// Create creates a new SSH key with the given options. +func (c *SSHKeyClient) Create(ctx context.Context, opts SSHKeyCreateOpts) (*SSHKey, *Response, error) { + if err := opts.Validate(); err != nil { + return nil, nil, err + } + reqBody := schema.SSHKeyCreateRequest{ + Name: opts.Name, + PublicKey: opts.PublicKey, + } + if opts.Labels != nil { + reqBody.Labels = &opts.Labels + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + req, err := c.client.NewRequest(ctx, "POST", "/ssh_keys", bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + var respBody schema.SSHKeyCreateResponse + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return SSHKeyFromSchema(respBody.SSHKey), resp, nil +} + +// Delete deletes a SSH key. +func (c *SSHKeyClient) Delete(ctx context.Context, sshKey *SSHKey) (*Response, error) { + req, err := c.client.NewRequest(ctx, "DELETE", fmt.Sprintf("/ssh_keys/%d", sshKey.ID), nil) + if err != nil { + return nil, err + } + return c.client.Do(req, nil) +} + +// SSHKeyUpdateOpts specifies options for updating a SSH key. +type SSHKeyUpdateOpts struct { + Name string + Labels map[string]string +} + +// Update updates a SSH key. +func (c *SSHKeyClient) Update(ctx context.Context, sshKey *SSHKey, opts SSHKeyUpdateOpts) (*SSHKey, *Response, error) { + reqBody := schema.SSHKeyUpdateRequest{ + 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("/ssh_keys/%d", sshKey.ID) + req, err := c.client.NewRequest(ctx, "PUT", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + respBody := schema.SSHKeyUpdateResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return SSHKeyFromSchema(respBody.SSHKey), resp, nil +} diff --git a/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/volume.go b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/volume.go new file mode 100644 index 000000000000..5fc01882f72e --- /dev/null +++ b/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/volume.go @@ -0,0 +1,415 @@ +/* +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" +) + +// Volume represents a volume in the Hetzner Cloud. +type Volume struct { + ID int + Name string + Status VolumeStatus + Server *Server + Location *Location + Size int + Protection VolumeProtection + Labels map[string]string + LinuxDevice string + Created time.Time +} + +// VolumeProtection represents the protection level of a volume. +type VolumeProtection struct { + Delete bool +} + +// VolumeClient is a client for the volume API. +type VolumeClient struct { + client *Client +} + +// VolumeStatus specifies a volume's status. +type VolumeStatus string + +const ( + // VolumeStatusCreating is the status when a volume is being created. + VolumeStatusCreating VolumeStatus = "creating" + + // VolumeStatusAvailable is the status when a volume is available. + VolumeStatusAvailable VolumeStatus = "available" +) + +// GetByID retrieves a volume by its ID. If the volume does not exist, nil is returned. +func (c *VolumeClient) GetByID(ctx context.Context, id int) (*Volume, *Response, error) { + req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/volumes/%d", id), nil) + if err != nil { + return nil, nil, err + } + + var body schema.VolumeGetResponse + resp, err := c.client.Do(req, &body) + if err != nil { + if IsError(err, ErrorCodeNotFound) { + return nil, resp, nil + } + return nil, nil, err + } + return VolumeFromSchema(body.Volume), resp, nil +} + +// GetByName retrieves a volume by its name. If the volume does not exist, nil is returned. +func (c *VolumeClient) GetByName(ctx context.Context, name string) (*Volume, *Response, error) { + if name == "" { + return nil, nil, nil + } + volumes, response, err := c.List(ctx, VolumeListOpts{Name: name}) + if len(volumes) == 0 { + return nil, response, err + } + return volumes[0], response, err +} + +// Get retrieves a volume by its ID if the input can be parsed as an integer, otherwise it +// retrieves a volume by its name. If the volume does not exist, nil is returned. +func (c *VolumeClient) Get(ctx context.Context, idOrName string) (*Volume, *Response, error) { + if id, err := strconv.Atoi(idOrName); err == nil { + return c.GetByID(ctx, int(id)) + } + return c.GetByName(ctx, idOrName) +} + +// VolumeListOpts specifies options for listing volumes. +type VolumeListOpts struct { + ListOpts + Name string + Status []VolumeStatus +} + +func (l VolumeListOpts) values() url.Values { + vals := l.ListOpts.values() + if l.Name != "" { + vals.Add("name", l.Name) + } + for _, status := range l.Status { + vals.Add("status", string(status)) + } + return vals +} + +// List returns a list of volumes 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 *VolumeClient) List(ctx context.Context, opts VolumeListOpts) ([]*Volume, *Response, error) { + path := "/volumes?" + opts.values().Encode() + req, err := c.client.NewRequest(ctx, "GET", path, nil) + if err != nil { + return nil, nil, err + } + + var body schema.VolumeListResponse + resp, err := c.client.Do(req, &body) + if err != nil { + return nil, nil, err + } + volumes := make([]*Volume, 0, len(body.Volumes)) + for _, s := range body.Volumes { + volumes = append(volumes, VolumeFromSchema(s)) + } + return volumes, resp, nil +} + +// All returns all volumes. +func (c *VolumeClient) All(ctx context.Context) ([]*Volume, error) { + return c.AllWithOpts(ctx, VolumeListOpts{ListOpts: ListOpts{PerPage: 50}}) +} + +// AllWithOpts returns all volumes with the given options. +func (c *VolumeClient) AllWithOpts(ctx context.Context, opts VolumeListOpts) ([]*Volume, error) { + allVolumes := []*Volume{} + + _, err := c.client.all(func(page int) (*Response, error) { + opts.Page = page + volumes, resp, err := c.List(ctx, opts) + if err != nil { + return resp, err + } + allVolumes = append(allVolumes, volumes...) + return resp, nil + }) + if err != nil { + return nil, err + } + + return allVolumes, nil +} + +// VolumeCreateOpts specifies parameters for creating a volume. +type VolumeCreateOpts struct { + Name string + Size int + Server *Server + Location *Location + Labels map[string]string + Automount *bool + Format *string +} + +// Validate checks if options are valid. +func (o VolumeCreateOpts) Validate() error { + if o.Name == "" { + return errors.New("missing name") + } + if o.Size <= 0 { + return errors.New("size must be greater than 0") + } + if o.Server == nil && o.Location == nil { + return errors.New("one of server or location must be provided") + } + if o.Server != nil && o.Location != nil { + return errors.New("only one of server or location must be provided") + } + if o.Server == nil && (o.Automount != nil && *o.Automount) { + return errors.New("server must be provided when automount is true") + } + return nil +} + +// VolumeCreateResult is the result of creating a volume. +type VolumeCreateResult struct { + Volume *Volume + Action *Action + NextActions []*Action +} + +// Create creates a new volume with the given options. +func (c *VolumeClient) Create(ctx context.Context, opts VolumeCreateOpts) (VolumeCreateResult, *Response, error) { + if err := opts.Validate(); err != nil { + return VolumeCreateResult{}, nil, err + } + reqBody := schema.VolumeCreateRequest{ + Name: opts.Name, + Size: opts.Size, + Automount: opts.Automount, + Format: opts.Format, + } + if opts.Labels != nil { + reqBody.Labels = &opts.Labels + } + if opts.Server != nil { + reqBody.Server = Int(opts.Server.ID) + } + if opts.Location != nil { + if opts.Location.ID != 0 { + reqBody.Location = opts.Location.ID + } else { + reqBody.Location = opts.Location.Name + } + } + + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return VolumeCreateResult{}, nil, err + } + + req, err := c.client.NewRequest(ctx, "POST", "/volumes", bytes.NewReader(reqBodyData)) + if err != nil { + return VolumeCreateResult{}, nil, err + } + + var respBody schema.VolumeCreateResponse + resp, err := c.client.Do(req, &respBody) + if err != nil { + return VolumeCreateResult{}, resp, err + } + + var action *Action + if respBody.Action != nil { + action = ActionFromSchema(*respBody.Action) + } + + return VolumeCreateResult{ + Volume: VolumeFromSchema(respBody.Volume), + Action: action, + NextActions: ActionsFromSchema(respBody.NextActions), + }, resp, nil +} + +// Delete deletes a volume. +func (c *VolumeClient) Delete(ctx context.Context, volume *Volume) (*Response, error) { + req, err := c.client.NewRequest(ctx, "DELETE", fmt.Sprintf("/volumes/%d", volume.ID), nil) + if err != nil { + return nil, err + } + return c.client.Do(req, nil) +} + +// VolumeUpdateOpts specifies options for updating a volume. +type VolumeUpdateOpts struct { + Name string + Labels map[string]string +} + +// Update updates a volume. +func (c *VolumeClient) Update(ctx context.Context, volume *Volume, opts VolumeUpdateOpts) (*Volume, *Response, error) { + reqBody := schema.VolumeUpdateRequest{ + 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("/volumes/%d", volume.ID) + req, err := c.client.NewRequest(ctx, "PUT", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + respBody := schema.VolumeUpdateResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return VolumeFromSchema(respBody.Volume), resp, nil +} + +// VolumeAttachOpts specifies options for attaching a volume. +type VolumeAttachOpts struct { + Server *Server + Automount *bool +} + +// AttachWithOpts attaches a volume to a server. +func (c *VolumeClient) AttachWithOpts(ctx context.Context, volume *Volume, opts VolumeAttachOpts) (*Action, *Response, error) { + reqBody := schema.VolumeActionAttachVolumeRequest{ + Server: opts.Server.ID, + Automount: opts.Automount, + } + + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/volumes/%d/actions/attach", volume.ID) + req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + var respBody schema.VolumeActionAttachVolumeResponse + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, nil +} + +// Attach attaches a volume to a server. +func (c *VolumeClient) Attach(ctx context.Context, volume *Volume, server *Server) (*Action, *Response, error) { + return c.AttachWithOpts(ctx, volume, VolumeAttachOpts{Server: server}) +} + +// Detach detaches a volume from a server. +func (c *VolumeClient) Detach(ctx context.Context, volume *Volume) (*Action, *Response, error) { + var reqBody schema.VolumeActionDetachVolumeRequest + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/volumes/%d/actions/detach", volume.ID) + req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + var respBody schema.VolumeActionDetachVolumeResponse + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, nil +} + +// VolumeChangeProtectionOpts specifies options for changing the resource protection level of a volume. +type VolumeChangeProtectionOpts struct { + Delete *bool +} + +// ChangeProtection changes the resource protection level of a volume. +func (c *VolumeClient) ChangeProtection(ctx context.Context, volume *Volume, opts VolumeChangeProtectionOpts) (*Action, *Response, error) { + reqBody := schema.VolumeActionChangeProtectionRequest{ + Delete: opts.Delete, + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/volumes/%d/actions/change_protection", volume.ID) + req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + respBody := schema.VolumeActionChangeProtectionResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, err +} + +// Resize changes the size of a volume. +func (c *VolumeClient) Resize(ctx context.Context, volume *Volume, size int) (*Action, *Response, error) { + reqBody := schema.VolumeActionResizeVolumeRequest{ + Size: size, + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/volumes/%d/actions/resize", volume.ID) + req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + respBody := schema.VolumeActionResizeVolumeResponse{} + 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_cloud_provider.go b/cluster-autoscaler/cloudprovider/hetzner/hetzner_cloud_provider.go new file mode 100644 index 000000000000..c4c1b3d5eebe --- /dev/null +++ b/cluster-autoscaler/cloudprovider/hetzner/hetzner_cloud_provider.go @@ -0,0 +1,242 @@ +/* +Copyright 2019 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 hetzner + +import ( + "fmt" + apiv1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/autoscaler/cluster-autoscaler/cloudprovider" + "k8s.io/autoscaler/cluster-autoscaler/config" + "k8s.io/autoscaler/cluster-autoscaler/utils/errors" + "k8s.io/klog/v2" + "regexp" + "strconv" + "strings" + "sync" + "time" +) + +var _ cloudprovider.CloudProvider = (*HetznerCloudProvider)(nil) + +const ( + // GPULabel is the label added to nodes with GPU resource. + GPULabel = hcloudLabelNamespace + "/gpu-node" + providerIDPrefix = "hcloud://" + nodeGroupLabel = hcloudLabelNamespace + "/node-group" + hcloudLabelNamespace = "hcloud" + drainingNodePoolId = "draining-node-pool" + serverCreateTimeout = 1 * time.Minute + serverRegisterTimeout = 10 * time.Minute + defaultPodAmountsLimit = 110 +) + +// HetznerCloudProvider implements CloudProvider interface. +type HetznerCloudProvider struct { + manager *hetznerManager + resourceLimiter *cloudprovider.ResourceLimiter +} + +// Name returns name of the cloud provider. +func (d *HetznerCloudProvider) Name() string { + return cloudprovider.HetznerProviderName +} + +// NodeGroups returns all node groups configured for this cloud provider. +func (d *HetznerCloudProvider) NodeGroups() []cloudprovider.NodeGroup { + groups := make([]cloudprovider.NodeGroup, 0, len(d.manager.nodeGroups)) + for groupId := range d.manager.nodeGroups { + groups = append(groups, d.manager.nodeGroups[groupId]) + } + return groups +} + +// NodeGroupForNode returns the node group for the given node, nil if the node +// should not be processed by cluster autoscaler, or non-nil error if such +// occurred. Must be implemented. +func (d *HetznerCloudProvider) NodeGroupForNode(node *apiv1.Node) (cloudprovider.NodeGroup, error) { + server, err := d.manager.serverForNode(node) + if err != nil { + return nil, fmt.Errorf("failed to check if server %s exists error: %v", node.Spec.ProviderID, err) + } + + var groupId string + if server == nil { + klog.V(3).Infof("failed to find hcloud server for node %s", node.Name) + nodeGroupId, exists := node.Labels[nodeGroupLabel] + if !exists { + return nil, nil + } + groupId = nodeGroupId + } else { + serverGroupId, exists := server.Labels[nodeGroupLabel] + groupId = serverGroupId + if !exists { + return nil, nil + } + } + + group, exists := d.manager.nodeGroups[groupId] + if !exists { + return nil, nil + } + + return group, nil +} + +// Pricing returns pricing model for this cloud provider or error if not +// available. Implementation optional. +func (d *HetznerCloudProvider) Pricing() (cloudprovider.PricingModel, errors.AutoscalerError) { + return nil, cloudprovider.ErrNotImplemented +} + +// GetAvailableMachineTypes get all machine types that can be requested from +// the cloud provider. Implementation optional. +func (d *HetznerCloudProvider) GetAvailableMachineTypes() ([]string, error) { + serverTypes, err := d.manager.client.ServerType.All(d.manager.apiCallContext) + if err != nil { + return nil, err + } + + types := make([]string, len(serverTypes)) + for _, server := range serverTypes { + types = append(types, server.Name) + } + + return types, nil +} + +// NewNodeGroup builds a theoretical node group based on the node definition +// provided. The node group is not automatically created on the cloud provider +// side. The node group is not returned by NodeGroups() until it is created. +// Implementation optional. +func (d *HetznerCloudProvider) NewNodeGroup( + machineType string, + labels map[string]string, + systemLabels map[string]string, + taints []apiv1.Taint, + extraResources map[string]resource.Quantity, +) (cloudprovider.NodeGroup, error) { + return nil, cloudprovider.ErrNotImplemented +} + +// GetResourceLimiter returns struct containing limits (max, min) for +// resources (cores, memory etc.). +func (d *HetznerCloudProvider) GetResourceLimiter() (*cloudprovider.ResourceLimiter, error) { + return d.resourceLimiter, nil +} + +// GPULabel returns the label added to nodes with GPU resource. +func (d *HetznerCloudProvider) GPULabel() string { + return GPULabel +} + +// GetAvailableGPUTypes return all available GPU types cloud provider supports. +func (d *HetznerCloudProvider) GetAvailableGPUTypes() map[string]struct{} { + return nil +} + +// Cleanup cleans up open resources before the cloud provider is destroyed, +// i.e. go routines etc. +func (d *HetznerCloudProvider) Cleanup() error { + return nil +} + +// Refresh is called before every main loop and can be used to dynamically +// update cloud provider state. In particular the list of node groups returned +// by NodeGroups() can change as a result of CloudProvider.Refresh(). +func (d *HetznerCloudProvider) Refresh() error { + for _, group := range d.manager.nodeGroups { + group.resetTargetSize(0) + } + return nil +} + +// BuildHetzner builds the Hetzner cloud provider. +func BuildHetzner(_ config.AutoscalingOptions, do cloudprovider.NodeGroupDiscoveryOptions, rl *cloudprovider.ResourceLimiter) cloudprovider.CloudProvider { + manager, err := newManager() + if err != nil { + klog.Fatalf("Failed to create Hetzner manager: %v", err) + } + + provider, err := newHetznerCloudProvider(manager, rl) + if err != nil { + klog.Fatalf("Failed to create Hetzner cloud provider: %v", err) + } + + validNodePoolName := regexp.MustCompile(`^[a-z0-9A-Z]+[a-z0-9A-Z\-\.\_]*[a-z0-9A-Z]+$|^[a-z0-9A-Z]{1}$`) + clusterUpdateLock := sync.Mutex{} + + for _, nodegroupSpec := range do.NodeGroupSpecs { + spec, err := createNodePoolSpec(nodegroupSpec) + if err != nil { + klog.Fatalf("Failed to parse pool spec `%s` provider: %v", nodegroupSpec, err) + } + + validNodePoolName.MatchString(spec.name) + servers, err := manager.allServers(spec.name) + if err != nil { + klog.Fatalf("Failed to get servers for for node pool %s error: %v", nodegroupSpec, err) + } + + manager.nodeGroups[spec.name] = &hetznerNodeGroup{ + manager: manager, + id: spec.name, + minSize: spec.minSize, + maxSize: spec.maxSize, + instanceType: strings.ToLower(spec.instanceType), + region: strings.ToLower(spec.region), + targetSize: len(servers), + clusterUpdateMutex: &clusterUpdateLock, + } + } + + return provider +} + +func createNodePoolSpec(groupSpec string) (*hetznerNodeGroupSpec, error) { + tokens := strings.SplitN(groupSpec, ":", 5) + if len(tokens) != 5 { + return nil, fmt.Errorf("expected format `::::` got %s", groupSpec) + } + + definition := hetznerNodeGroupSpec{ + instanceType: tokens[2], + region: tokens[3], + name: tokens[4], + } + if size, err := strconv.Atoi(tokens[0]); err == nil { + definition.minSize = size + } else { + return nil, fmt.Errorf("failed to set min size: %s, expected integer", tokens[0]) + } + + if size, err := strconv.Atoi(tokens[1]); err == nil { + definition.maxSize = size + } else { + return nil, fmt.Errorf("failed to set max size: %s, expected integer", tokens[1]) + } + + return &definition, nil +} + +func newHetznerCloudProvider(manager *hetznerManager, rl *cloudprovider.ResourceLimiter) (*HetznerCloudProvider, error) { + return &HetznerCloudProvider{ + manager: manager, + resourceLimiter: rl, + }, nil +} diff --git a/cluster-autoscaler/cloudprovider/hetzner/hetzner_manager.go b/cluster-autoscaler/cloudprovider/hetzner/hetzner_manager.go new file mode 100644 index 000000000000..cf6a9818221d --- /dev/null +++ b/cluster-autoscaler/cloudprovider/hetzner/hetzner_manager.go @@ -0,0 +1,145 @@ +/* +Copyright 2019 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 hetzner + +import ( + "context" + "encoding/base64" + "errors" + "fmt" + apiv1 "k8s.io/api/core/v1" + "k8s.io/autoscaler/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud" + "os" + "strings" +) + +var ( + version = "dev" +) + +// hetznerManager handles Hetzner communication and data caching of +// node groups +type hetznerManager struct { + client *hcloud.Client + nodeGroups map[string]*hetznerNodeGroup + apiCallContext context.Context + cloudInit string + image string +} + +func newManager() (*hetznerManager, error) { + token := os.Getenv("HCLOUD_TOKEN") + if token == "" { + return nil, errors.New("`HCLOUD_TOKEN` is not specified") + } + + cloudInitBase64 := os.Getenv("HCLOUD_CLOUD_INIT") + if cloudInitBase64 == "" { + return nil, errors.New("`HCLOUD_CLOUD_INIT` is not specified") + } + + image := os.Getenv("HCLOUD_IMAGE") + if image == "" { + image = "ubuntu-20.04" + } + + client := hcloud.NewClient(hcloud.WithToken(token)) + ctx := context.Background() + cloudInit, err := base64.StdEncoding.DecodeString(cloudInitBase64) + if err != nil { + return nil, fmt.Errorf("failed to parse cloud init error: %s", err) + } + + m := &hetznerManager{ + client: client, + nodeGroups: make(map[string]*hetznerNodeGroup), + cloudInit: string(cloudInit), + image: image, + apiCallContext: ctx, + } + + m.nodeGroups[drainingNodePoolId] = &hetznerNodeGroup{ + manager: m, + instanceType: "cx11", + region: "fsn1", + targetSize: 0, + maxSize: 0, + minSize: 0, + id: drainingNodePoolId, + } + + return m, nil +} + +// Refresh refreshes the cache holding the nodegroups. This is called by the CA +// based on the `--scan-interval`. By default it's 10 seconds. +func (m *hetznerManager) Refresh() error { + return nil +} + +func (m *hetznerManager) allServers(nodeGroup string) ([]*hcloud.Server, error) { + listOptions := hcloud.ListOpts{ + PerPage: 50, + LabelSelector: nodeGroupLabel + "=" + nodeGroup, + } + + requestOptions := hcloud.ServerListOpts{ListOpts: listOptions} + servers, err := m.client.Server.AllWithOpts(m.apiCallContext, requestOptions) + if err != nil { + return nil, fmt.Errorf("failed to get servers for hcloud: %v", err) + } + + return servers, nil +} + +func (m *hetznerManager) deleteByNode(node *apiv1.Node) error { + server, err := m.serverForNode(node) + if err != nil { + return fmt.Errorf("failed to delete node %s error: %v", node.Name, err) + } + + if server == nil { + return fmt.Errorf("failed to delete node %s server not found", node.Name) + } + + return m.deleteServer(server) +} + +func (m *hetznerManager) deleteServer(server *hcloud.Server) error { + _, err := m.client.Server.Delete(m.apiCallContext, server) + return err +} + +func (m *hetznerManager) addNodeToDrainingPool(node *apiv1.Node) (*hetznerNodeGroup, error) { + m.nodeGroups[drainingNodePoolId].targetSize += 1 + return m.nodeGroups[drainingNodePoolId], nil +} + +func (m *hetznerManager) serverForNode(node *apiv1.Node) (*hcloud.Server, error) { + var nodeIdOrName string + if node.Spec.ProviderID != "" { + nodeIdOrName = strings.TrimPrefix(node.Spec.ProviderID, providerIDPrefix) + } else { + nodeIdOrName = node.Name + } + + server, _, err := m.client.Server.Get(m.apiCallContext, nodeIdOrName) + if err != nil { + return nil, fmt.Errorf("failed to get servers for node %s error: %v", node.Name, err) + } + return server, nil +} diff --git a/cluster-autoscaler/cloudprovider/hetzner/hetzner_node_group.go b/cluster-autoscaler/cloudprovider/hetzner/hetzner_node_group.go new file mode 100644 index 000000000000..addcf9e199e5 --- /dev/null +++ b/cluster-autoscaler/cloudprovider/hetzner/hetzner_node_group.go @@ -0,0 +1,410 @@ +/* +Copyright 2019 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 hetzner + +import ( + "fmt" + apiv1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/autoscaler/cluster-autoscaler/cloudprovider" + "k8s.io/autoscaler/cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud" + "k8s.io/klog/v2" + schedulerframework "k8s.io/kubernetes/pkg/scheduler/framework/v1alpha1" + "math/rand" + "strconv" + "sync" + "time" +) + +// hetznerNodeGroup implements cloudprovider.NodeGroup interface. hetznerNodeGroup contains +// configuration info and functions to control a set of nodes that have the +// same capacity and set of labels. +type hetznerNodeGroup struct { + id string + manager *hetznerManager + minSize int + maxSize int + targetSize int + region string + instanceType string + + clusterUpdateMutex *sync.Mutex +} + +type hetznerNodeGroupSpec struct { + name string + minSize int + maxSize int + region string + instanceType string +} + +// MaxSize returns maximum size of the node group. +func (n *hetznerNodeGroup) MaxSize() int { + return n.maxSize +} + +// MinSize returns minimum size of the node group. +func (n *hetznerNodeGroup) MinSize() int { + return n.minSize +} + +// TargetSize returns the current target size of the node group. It is possible +// that the number of nodes in Kubernetes is different at the moment but should +// be equal to Size() once everything stabilizes (new nodes finish startup and +// registration or removed nodes are deleted completely). Implementation +// required. +func (n *hetznerNodeGroup) TargetSize() (int, error) { + return n.targetSize, nil +} + +// IncreaseSize increases the size of the node group. To delete a node you need +// to explicitly name it and use DeleteNode. This function should wait until +// node group size is updated. Implementation required. +func (n *hetznerNodeGroup) IncreaseSize(delta int) error { + if delta <= 0 { + return fmt.Errorf("delta must be positive, have: %d", delta) + } + + targetSize := n.targetSize + delta + if targetSize > n.MaxSize() { + return fmt.Errorf("size increase is too large. current: %d desired: %d max: %d", n.targetSize, targetSize, n.MaxSize()) + } + + klog.V(4).Infof("Scaling Instance Pool %s to %d", n.id, targetSize) + + n.clusterUpdateMutex.Lock() + defer n.clusterUpdateMutex.Unlock() + + available, err := serverTypeAvailable(n.manager, n.instanceType, n.region) + if err != nil { + return fmt.Errorf("failed to check if type %s is available in region %s error: %v", n.instanceType, n.region, err) + } + if !available { + return fmt.Errorf("server type %s not available in region %s", n.instanceType, n.region) + } + + waitGroup := sync.WaitGroup{} + for i := 0; i < delta; i++ { + waitGroup.Add(1) + go func() { + defer waitGroup.Done() + err := createServer(n) + if err != nil { + targetSize-- + klog.Errorf("failed to create error: %v", err) + } + }() + } + waitGroup.Wait() + + n.targetSize = targetSize + + return nil +} + +// DeleteNodes deletes nodes from this node group (and also increasing the size +// of the node group with that). Error is returned either on failure or if the +// given node doesn't belong to this node group. This function should wait +// until node group size is updated. Implementation required. +func (n *hetznerNodeGroup) DeleteNodes(nodes []*apiv1.Node) error { + n.clusterUpdateMutex.Lock() + defer n.clusterUpdateMutex.Unlock() + + targetSize := n.targetSize - len(nodes) + if targetSize < n.MinSize() { + return fmt.Errorf("size decrease is too large. current: %d desired: %d min: %d", n.targetSize, targetSize, n.MinSize()) + } + + waitGroup := sync.WaitGroup{} + + for _, node := range nodes { + waitGroup.Add(1) + go func(node *apiv1.Node) { + klog.Infof("Evicting server %s", node.Name) + + err := n.manager.deleteByNode(node) + if err != nil { + klog.Errorf("failed to delete server ID %d error: %v", node.Name, err) + } + + waitGroup.Done() + }(node) + } + waitGroup.Wait() + + n.resetTargetSize(-len(nodes)) + + return nil +} + +// DecreaseTargetSize decreases the target size of the node group. This function +// doesn't permit to delete any existing node and can be used only to reduce the +// request for new nodes that have not been yet fulfilled. Delta should be negative. +// It is assumed that cloud provider will not delete the existing nodes when there +// is an option to just decrease the target. Implementation required. +func (n *hetznerNodeGroup) DecreaseTargetSize(delta int) error { + n.targetSize = n.targetSize + delta + return nil +} + +// Id returns an unique identifier of the node group. +func (n *hetznerNodeGroup) Id() string { + return n.id +} + +// Debug returns a string containing all information regarding this node group. +func (n *hetznerNodeGroup) Debug() string { + return fmt.Sprintf("cluster ID: %s (min:%d max:%d)", n.Id(), n.MinSize(), n.MaxSize()) +} + +// Nodes returns a list of all nodes that belong to this node group. It is +// required that Instance objects returned by this method have Id field set. +// Other fields are optional. +func (n *hetznerNodeGroup) Nodes() ([]cloudprovider.Instance, error) { + listOptions := hcloud.ListOpts{ + PerPage: 50, + LabelSelector: nodeGroupLabel + "=" + n.id, + } + requestOptions := hcloud.ServerListOpts{ListOpts: listOptions} + servers, err := n.manager.client.Server.AllWithOpts(n.manager.apiCallContext, requestOptions) + if err != nil { + return nil, fmt.Errorf("failed to get servers for hcloud: %v", err) + } + + instances := make([]cloudprovider.Instance, 0, len(servers)) + for _, vm := range servers { + instances = append(instances, toInstance(vm)) + } + + return instances, nil +} + +// TemplateNodeInfo returns a schedulerframework.NodeInfo structure of an empty +// (as if just started) node. This will be used in scale-up simulations to +// predict what would a new node look like if a node group was expanded. The +// returned NodeInfo is expected to have a fully populated Node object, with +// all of the labels, capacity and allocatable information as well as all pods +// that are started on the node by default, using manifest (most likely only +// kube-proxy). Implementation optional. +func (n *hetznerNodeGroup) TemplateNodeInfo() (*schedulerframework.NodeInfo, error) { + resourceList, err := getMachineTypeResourceList(n.manager, n.instanceType) + if err != nil { + return nil, fmt.Errorf("failed to create resource list for node group %s error: %v", n.id, err) + } + + node := apiv1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: newNodeName(n), + Labels: map[string]string{}, + }, + Status: apiv1.NodeStatus{ + Capacity: resourceList, + Conditions: cloudprovider.BuildReadyConditions(), + }, + } + node.Status.Allocatable = node.Status.Capacity + node.Labels = cloudprovider.JoinStringMaps(node.Labels, buildNodeGroupLabels(n)) + node.Status.Conditions = cloudprovider.BuildReadyConditions() + + nodeInfo := schedulerframework.NewNodeInfo(cloudprovider.BuildKubeProxy(n.id)) + err = nodeInfo.SetNode(&node) + if err != nil { + return nil, fmt.Errorf("could not create node info for node group %s error: %v", n.id, err) + } + + return nodeInfo, nil +} + +// Exist checks if the node group really exists on the cloud provider side. +// Allows to tell the theoretical node group from the real one. Implementation +// required. +func (n *hetznerNodeGroup) Exist() bool { + _, exists := n.manager.nodeGroups[n.id] + return exists +} + +// Create creates the node group on the cloud provider side. Implementation +// optional. +func (n *hetznerNodeGroup) Create() (cloudprovider.NodeGroup, error) { + n.manager.nodeGroups[n.id] = n + + return n, cloudprovider.ErrNotImplemented +} + +// Delete deletes the node group on the cloud provider side. This will be +// executed only for autoprovisioned node groups, once their size drops to 0. +// Implementation optional. +func (n *hetznerNodeGroup) Delete() error { + // We do not use actual node groups but all nodes within the Hcloud project are labeled with a group + return nil +} + +// Autoprovisioned returns true if the node group is autoprovisioned. An +// autoprovisioned group was created by CA and can be deleted when scaled to 0. +func (n *hetznerNodeGroup) Autoprovisioned() bool { + // All groups are auto provisioned + return false +} + +func toInstance(vm *hcloud.Server) cloudprovider.Instance { + return cloudprovider.Instance{ + Id: toProviderID(vm.ID), + Status: toInstanceStatus(vm.Status), + } +} + +func toProviderID(nodeID int) string { + return fmt.Sprintf("%s%d", providerIDPrefix, nodeID) +} + +func toInstanceStatus(status hcloud.ServerStatus) *cloudprovider.InstanceStatus { + if status == "" { + return nil + } + + st := &cloudprovider.InstanceStatus{} + switch status { + case hcloud.ServerStatusInitializing: + case hcloud.ServerStatusStarting: + st.State = cloudprovider.InstanceCreating + case hcloud.ServerStatusRunning: + st.State = cloudprovider.InstanceRunning + case hcloud.ServerStatusOff: + case hcloud.ServerStatusDeleting: + case hcloud.ServerStatusStopping: + st.State = cloudprovider.InstanceDeleting + default: + st.ErrorInfo = &cloudprovider.InstanceErrorInfo{ + ErrorClass: cloudprovider.OtherErrorClass, + ErrorCode: "no-code-hcloud", + ErrorMessage: "error", + } + } + + return st +} + +func newNodeName(n *hetznerNodeGroup) string { + return fmt.Sprintf("%s-%d", n.id, rand.Int63()) +} + +func buildNodeGroupLabels(n *hetznerNodeGroup) map[string]string { + return map[string]string{ + apiv1.LabelInstanceType: n.instanceType, + apiv1.LabelZoneRegionStable: n.region, + nodeGroupLabel: n.id, + } +} + +func getMachineTypeResourceList(m *hetznerManager, instanceType string) (apiv1.ResourceList, error) { + typeInfo, _, err := m.client.ServerType.Get(m.apiCallContext, instanceType) + if err != nil || typeInfo == nil { + return nil, fmt.Errorf("failed to get machine type %s info error: %v", instanceType, err) + } + + return apiv1.ResourceList{ + // TODO somehow determine the actual pods that will be running + apiv1.ResourcePods: *resource.NewQuantity(defaultPodAmountsLimit, resource.DecimalSI), + apiv1.ResourceCPU: *resource.NewQuantity(int64(typeInfo.Cores), resource.DecimalSI), + apiv1.ResourceMemory: *resource.NewQuantity(int64(typeInfo.Memory*1024*1024*1024), resource.DecimalSI), + apiv1.ResourceStorage: *resource.NewQuantity(int64(typeInfo.Disk*1024*1024*1024), resource.DecimalSI), + }, nil +} + +func serverTypeAvailable(manager *hetznerManager, instanceType string, region string) (bool, error) { + serverType, _, err := manager.client.ServerType.Get(manager.apiCallContext, instanceType) + if err != nil { + return false, err + } + + for _, price := range serverType.Pricings { + if price.Location.Name == region { + return true, nil + } + } + + return false, nil +} + +func createServer(n *hetznerNodeGroup) error { + StartAfterCreate := true + serverCreateResult, _, err := n.manager.client.Server.Create(n.manager.apiCallContext, hcloud.ServerCreateOpts{ + Name: newNodeName(n), + UserData: n.manager.cloudInit, + Location: &hcloud.Location{Name: n.region}, + ServerType: &hcloud.ServerType{Name: n.instanceType}, + Image: &hcloud.Image{Name: n.manager.image}, + StartAfterCreate: &StartAfterCreate, + Labels: map[string]string{ + nodeGroupLabel: n.id, + }, + }) + + if err != nil { + return fmt.Errorf("could not create server type %s in region %s", n.instanceType, n.region) + } + + server := serverCreateResult.Server + err = waitForServerStatus(n.manager, server, hcloud.ServerStatusRunning) + if err != nil { + _ = n.manager.deleteServer(server) + return fmt.Errorf("failed to start server %s error: %v", server.Name, err) + } + + return nil +} + +func waitForServerStatus(m *hetznerManager, server *hcloud.Server, status hcloud.ServerStatus) error { + errorResult := make(chan error) + + go func() { + for { + serverResponse, _, err := m.client.Server.Get(m.apiCallContext, strconv.Itoa(server.ID)) + if err != nil { + errorResult <- fmt.Errorf("failed to get server %s status error: %v", server.Name, err) + return + } + + if serverResponse.Status == status { + errorResult <- nil + return + } + + time.Sleep(1 * time.Second) + } + }() + + select { + case res := <-errorResult: + return res + case <-time.After(serverCreateTimeout): + return fmt.Errorf("waiting for server %s status %s timeout", server.Name, status) + } +} + +func (n *hetznerNodeGroup) resetTargetSize(expectedDelta int) { + servers, err := n.manager.allServers(n.id) + if err != nil { + klog.Errorf("failed to set node pool %s size, using delta %d error: %v", n.id, expectedDelta, err) + n.targetSize = n.targetSize - expectedDelta + } else { + klog.Infof("Set node group %s size from %d to %d, expected delta %d", n.id, n.targetSize, len(servers), expectedDelta) + n.targetSize = len(servers) + } +} diff --git a/hack/verify-spelling.sh b/hack/verify-spelling.sh index 442f66910419..148c60dd60dc 100755 --- a/hack/verify-spelling.sh +++ b/hack/verify-spelling.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/usr/bin/env bash # Copyright 2018 The Kubernetes Authors. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,10 +17,10 @@ set -o errexit set -o nounset set -o pipefail -DIR=$(dirname $0) +DIR=$(dirname $0) # Install tools we need go install ${DIR}/../../../github.com/client9/misspell/cmd/misspell # Spell checking -git ls-files --full-name | grep -v -e vendor | grep -v cluster-autoscaler/cloudprovider/magnum/gophercloud| grep -v cluster-autoscaler/cloudprovider/huaweicloud/huaweicloud-sdk-go-v3 | grep -v cluster-autoscaler/cloudprovider/digitalocean/godo | xargs misspell -error -o stderr +git ls-files --full-name | grep -v -e vendor | grep -v cluster-autoscaler/cloudprovider/magnum/gophercloud| grep -v cluster-autoscaler/cloudprovider/huaweicloud/huaweicloud-sdk-go-v3 | grep -v cluster-autoscaler/cloudprovider/digitalocean/godo | grep -v cluster-autoscaler/cloudprovider/hetzner/hcloud-go | xargs misspell -error -o stderr