Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cloudprovider hetzner #3640

Closed
wants to merge 21 commits into from
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
9628240
Partial implementation of Hetzner cloudprovider
Fgruntjes Oct 21, 2020
4f0d0a6
Updating vendor against git@github.com:kubernetes/kubernetes.git:3eb9…
Fgruntjes Oct 21, 2020
67a7b6b
Fixed invalid stringformat modifier
Fgruntjes Oct 21, 2020
49c4e5d
further implementation of hetzner cloud manager
Fgruntjes Oct 23, 2020
28a933f
less logging when server is not found
Fgruntjes Oct 23, 2020
a4fd478
Revert "Updating vendor against git@github.com:kubernetes/kubernetes.…
Fgruntjes Oct 23, 2020
3a86a44
Moved hcloud client from vendor to cloud provider folder
Fgruntjes Oct 23, 2020
5c4bdaf
Ensured the provider ID prefix is equal to the one used by the hcloud…
Fgruntjes Oct 23, 2020
9b5698a
Fixed setting target size
Fgruntjes Oct 23, 2020
ac6e633
Fixed with incorrect auto provisioned group
Fgruntjes Oct 23, 2020
ee4c915
Added OWNERS file
Fgruntjes Oct 23, 2020
5c94282
Added hcloud-go package to ignored linting files
Fgruntjes Oct 23, 2020
4bbc71b
Removed patching external id in node, this is the job of the cluster
Fgruntjes Oct 26, 2020
c07fd86
Added readme reference to hetzner provider and removed the fake node …
Fgruntjes Oct 29, 2020
798c470
Added debug logging when scale groups get refreshed
Fgruntjes Nov 9, 2020
23b156e
fixed hetzner provider pod template to allow scaling to zero
Fgruntjes Nov 9, 2020
a78d75a
Code style fixes
Fgruntjes Nov 9, 2020
5e449db
Removed Pieter from code owners for hetzner cloud provider to simplif…
Fgruntjes Nov 9, 2020
34cdf12
Update cluster-autoscaler/cloudprovider/builder/builder_hetzner.go
Fgruntjes Jan 11, 2021
3b23956
Update cluster-autoscaler/cloudprovider/hetzner/hetzner_cloud_provide…
Fgruntjes Jan 11, 2021
7e50426
Update cluster-autoscaler/cloudprovider/hetzner/hetzner_cloud_provide…
Fgruntjes Jan 11, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions cluster-autoscaler/cloudprovider/builder/builder_all.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"k8s.io/autoscaler/cluster-autoscaler/cloudprovider/digitalocean"
"k8s.io/autoscaler/cluster-autoscaler/cloudprovider/exoscale"
"k8s.io/autoscaler/cluster-autoscaler/cloudprovider/gce"
"k8s.io/autoscaler/cluster-autoscaler/cloudprovider/hetzner"
"k8s.io/autoscaler/cluster-autoscaler/cloudprovider/huaweicloud"
"k8s.io/autoscaler/cluster-autoscaler/cloudprovider/magnum"
"k8s.io/autoscaler/cluster-autoscaler/cloudprovider/packet"
Expand All @@ -45,6 +46,7 @@ var AvailableCloudProviders = []string{
cloudprovider.DigitalOceanProviderName,
cloudprovider.ExoscaleProviderName,
cloudprovider.HuaweicloudProviderName,
cloudprovider.HetznerProviderName,
clusterapi.ProviderName,
}

Expand All @@ -71,6 +73,8 @@ func buildCloudProvider(opts config.AutoscalingOptions, do cloudprovider.NodeGro
return magnum.BuildMagnum(opts, do, rl)
case cloudprovider.HuaweicloudProviderName:
return huaweicloud.BuildHuaweiCloud(opts, do, rl)
case cloudprovider.HetznerProviderName:
return hetzner.BuildHetzner(opts, do, rl)
case packet.ProviderName:
return packet.BuildPacket(opts, do, rl)
case clusterapi.ProviderName:
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 exoscale
Fgruntjes marked this conversation as resolved.
Show resolved Hide resolved

/*
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 @@ -41,6 +41,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
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
```
236 changes: 236 additions & 0 deletions cluster-autoscaler/cloudprovider/hetzner/hetzner_cloud_provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
/*
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"
)

var _ cloudprovider.CloudProvider = (*HetznerCloudProvider)(nil)

const (
// GPULabel is the label added to nodes with GPU resource.
GPULabel = hcloudLabelNamespace + "/gpu-node"
providerIDPrefix = "hetzner-autoscale://"
nodeGroupLabel = hcloudLabelNamespace + "/node-group"
hcloudLabelNamespace = "k8s.hcloud"
drainingNodePoolId = "draining-node-pool"
)

// 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)
Fgruntjes marked this conversation as resolved.
Show resolved Hide resolved
for i := range d.manager.nodeGroups {
groups = append(groups, d.manager.nodeGroups[i])
Fgruntjes marked this conversation as resolved.
Show resolved Hide resolved
}
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)
}

if server == nil {
klog.V(3).Infof("failed to find hcloud server for node %s", node.Name)
return nil, nil
}

groupId, exists := server.Labels[nodeGroupLabel]
if !exists {
klog.Warningf("server %s does not contain nodegroup label %s, draining node", node.Name, nodeGroupLabel)
return addDrainingNodeGroup(d.manager, node)
}

group, exists := d.manager.nodeGroups[groupId]
if !exists {
klog.Warningf("nodegroup %s not configured for server %s, draining node", groupId, node.Name)
return addDrainingNodeGroup(d.manager, node)
}

return group, nil
}

func addDrainingNodeGroup(manager *hetznerManager, node *apiv1.Node) (cloudprovider.NodeGroup, error) {
klog.Warningf("server %s within not configured node, creating temp group with target size 0", node.Name, nodeGroupLabel)
return manager.addNodeToDrainingPool(node)
}

// 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
Fgruntjes marked this conversation as resolved.
Show resolved Hide resolved
}

// 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 {
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 `<min-servers>:<max-servers>:<machine-type>:<region>:<name>` 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
}
Loading