Skip to content

Commit

Permalink
Implement Hetzner Cloud Provider (code by Fgruntjes)
Browse files Browse the repository at this point in the history
  • Loading branch information
Fgruntjes authored and LKaemmerling committed Mar 11, 2021
1 parent bb3b24c commit 908638b
Show file tree
Hide file tree
Showing 47 changed files with 9,298 additions and 5 deletions.
2 changes: 2 additions & 0 deletions cluster-autoscaler/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -155,4 +156,5 @@ 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
* Cluster API https://github.com/kubernetes/autoscaler/blob/master/cluster-autoscaler/cloudprovider/clusterapi/README.md
7 changes: 5 additions & 2 deletions cluster-autoscaler/cloudprovider/builder/builder_all.go
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -50,6 +50,7 @@ var AvailableCloudProviders = []string{
cloudprovider.DigitalOceanProviderName,
cloudprovider.ExoscaleProviderName,
cloudprovider.HuaweicloudProviderName,
cloudprovider.HetznerProviderName,
cloudprovider.OVHcloudProviderName,
cloudprovider.ClusterAPIProiverName,
cloudprovider.IonoscloudProviderName,
Expand Down Expand Up @@ -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:
Expand Down
42 changes: 42 additions & 0 deletions cluster-autoscaler/cloudprovider/builder/builder_hetzner.go
Original file line number Diff line number Diff line change
@@ -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
}
2 changes: 2 additions & 0 deletions cluster-autoscaler/cloudprovider/cloud_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions cluster-autoscaler/cloudprovider/hetzner/OWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
approvers:
- Fgruntjes
reviewers:
- Fgruntjes
42 changes: 42 additions & 0 deletions cluster-autoscaler/cloudprovider/hetzner/README.md
Original file line number Diff line number Diff line change
@@ -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=<min-servers>:<max-servers>:<instance-type>:<region>:<name>` 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
```
226 changes: 226 additions & 0 deletions cluster-autoscaler/cloudprovider/hetzner/hcloud-go/hcloud/action.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 908638b

Please sign in to comment.