Skip to content

Commit

Permalink
add hetzner cloud (#65)
Browse files Browse the repository at this point in the history
* add hetzner cloud

* remove floatingip related code & fix get server
  • Loading branch information
mrIncompetent committed Feb 5, 2018
1 parent 7a0b294 commit 1717402
Show file tree
Hide file tree
Showing 9 changed files with 329 additions and 15 deletions.
11 changes: 10 additions & 1 deletion Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Gopkg.toml
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,7 @@ required = ["k8s.io/code-generator/cmd/client-gen"]
[[constraint]]
name = "github.com/prometheus/client_golang"
version = "v0.8.0"

[[constraint]]
name = "github.com/hetznercloud/hcloud-go"
version = "v1.3.0"
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
# Features
## What works
- Kubernetes v1.8.5 and v1.9.0
- Creation of worker nodes on AWS, Digitalocean and Openstack
- Creation of worker nodes on AWS, Digitalocean, Openstack and Hetzner cloud
- Using Ubuntu & Coreos ContainerLinux distributions
- Using Ubuntu with [CRI-O](https://github.com/kubernetes-incubator/cri-o) container runtime instead of Docker

Expand Down
10 changes: 10 additions & 0 deletions docs/cloud-provider.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,13 @@ region: ""
# the name of the network to use
network: ""
```

## Hetzner cloud

### machine.spec.providerConfig.cloudProviderSpec
```yaml
token: "<< HETZNER_API_TOKEN >>"
serverType: "cx11"
datacenter: ""
location: "fsn1"
```
14 changes: 1 addition & 13 deletions docs/operating-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,7 @@
| AWS |||
| Openstack |||
| Digitalocean |||

### Features

| | Ubuntu | Container Linux |
|---|---|---|
| `machine.spec.versions.containerRuntime` | x* | x** |
| `machine.spec.versions.kubelet` ||*** |

```
* currently the latest version from https://download.docker.com/linux/ubuntu will be installed
** whatever comes with container linux will be used
*** as we use the kubelet-wrapper, the version needs to be a tag from https://quay.io/repository/coreos/hyperkube?tag=latest&tab=tags
```
| Hetzner || x |

## Configuring a operating system

Expand Down
25 changes: 25 additions & 0 deletions examples/machine-hetzner.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
apiVersion: "machine.k8s.io/v1alpha1"
kind: Machine
metadata:
name: machine1
spec:
metadata:
name: node1
providerConfig:
sshPublicKeys:
- "<< YOUR_PUBLIC_KEY >>"
cloudProvider: "hetzner"
cloudProviderSpec:
token: "<< HETZNER_API_TOKEN >>"
serverType: "cx11"
datacenter: ""
location: "fsn1"
operatingSystem: "ubuntu"
operatingSystemSpec:
distUpgradeOnBoot: false
roles:
- "Node"
versions:
kubelet: "1.9.2"
containerRuntime:
name: "cri-o"
2 changes: 2 additions & 0 deletions pkg/cloudprovider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/kubermatic/machine-controller/pkg/cloudprovider/cloud"
"github.com/kubermatic/machine-controller/pkg/cloudprovider/provider/aws"
"github.com/kubermatic/machine-controller/pkg/cloudprovider/provider/digitalocean"
"github.com/kubermatic/machine-controller/pkg/cloudprovider/provider/hetzner"
"github.com/kubermatic/machine-controller/pkg/cloudprovider/provider/openstack"
"github.com/kubermatic/machine-controller/pkg/providerconfig"
machinessh "github.com/kubermatic/machine-controller/pkg/ssh"
Expand All @@ -19,6 +20,7 @@ var (
providerconfig.CloudProviderDigitalocean: func(key *machinessh.PrivateKey) cloud.Provider { return digitalocean.New(key) },
providerconfig.CloudProviderAWS: func(key *machinessh.PrivateKey) cloud.Provider { return aws.New(key) },
providerconfig.CloudProviderOpenstack: func(key *machinessh.PrivateKey) cloud.Provider { return openstack.New(key) },
providerconfig.CloudProviderHetzner: func(key *machinessh.PrivateKey) cloud.Provider { return hetzner.New(key) },
}
)

Expand Down
275 changes: 275 additions & 0 deletions pkg/cloudprovider/provider/hetzner/provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
package hetzner

import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"strconv"
"sync"

"github.com/golang/glog"
"github.com/hetznercloud/hcloud-go/hcloud"
"golang.org/x/crypto/ssh"

"k8s.io/apimachinery/pkg/runtime"

"github.com/kubermatic/machine-controller/pkg/cloudprovider/cloud"
cloudprovidererrors "github.com/kubermatic/machine-controller/pkg/cloudprovider/errors"
"github.com/kubermatic/machine-controller/pkg/cloudprovider/instance"
"github.com/kubermatic/machine-controller/pkg/machines/v1alpha1"
"github.com/kubermatic/machine-controller/pkg/providerconfig"
machinessh "github.com/kubermatic/machine-controller/pkg/ssh"
)

type provider struct {
privateKey *machinessh.PrivateKey
}

// New returns a digitalocean provider
func New(privateKey *machinessh.PrivateKey) cloud.Provider {
return &provider{privateKey: privateKey}
}

type Config struct {
Token string `json:"token"`
ServerType string `json:"serverType"`
Datacenter string `json:"datacenter"`
Location string `json:"location"`
}

// Protects creation of public key
var publicKeyCreationLock = &sync.Mutex{}

func getNameForOS(os providerconfig.OperatingSystem) (string, error) {
switch os {
case providerconfig.OperatingSystemUbuntu:
return "ubuntu-16.04", nil
}
return "", providerconfig.ErrOSNotSupported
}

func getClient(token string) *hcloud.Client {
return hcloud.NewClient(hcloud.WithToken(token))
}

func getConfig(s runtime.RawExtension) (*Config, *providerconfig.Config, error) {
pconfig := providerconfig.Config{}
err := json.Unmarshal(s.Raw, &pconfig)
if err != nil {
return nil, nil, err
}
c := Config{}
err = json.Unmarshal(pconfig.CloudProviderSpec.Raw, &c)
return &c, &pconfig, err
}

func (p *provider) Validate(spec v1alpha1.MachineSpec) error {
c, pc, err := getConfig(spec.ProviderConfig)
if err != nil {
return fmt.Errorf("failed to parse config: %v", err)
}

if c.Token == "" {
return errors.New("token is missing")
}

_, err = getNameForOS(pc.OperatingSystem)
if err != nil {
return fmt.Errorf("invalid/not supported operating system specified %q: %v", pc.OperatingSystem, err)
}

ctx := context.TODO()
client := getClient(c.Token)

if c.Location != "" && c.Datacenter != "" {
return fmt.Errorf("location and datacenter must not be set at the same time")
}

if c.Location != "" {
if _, _, err = client.Location.Get(ctx, c.Location); err != nil {
return fmt.Errorf("failed to get location: %v", err)
}
}

if c.Datacenter != "" {
if _, _, err = client.Datacenter.Get(ctx, c.Datacenter); err != nil {
return fmt.Errorf("failed to get datacenter: %v", err)
}
}

if _, _, err = client.ServerType.Get(ctx, c.ServerType); err != nil {
return fmt.Errorf("failed to get server type: %v", err)
}

return nil
}

func ensureSSHKeysExist(ctx context.Context, client hcloud.SSHKeyClient, key *machinessh.PrivateKey) (*hcloud.SSHKey, error) {
publicKeyCreationLock.Lock()
defer publicKeyCreationLock.Unlock()

publicKey := key.PublicKey()
pk, err := ssh.NewPublicKey(&publicKey)
if err != nil {
return nil, fmt.Errorf("failed to parse publickey: %v", err)
}

fingerprint := ssh.FingerprintLegacyMD5(pk)
keys, err := client.All(ctx)
for _, key := range keys {
if key.Fingerprint == fingerprint {
return key, nil
}
}

hkey, res, err := client.Create(ctx, hcloud.SSHKeyCreateOpts{
Name: key.Name(),
PublicKey: string(ssh.MarshalAuthorizedKey(pk)),
})
if err != nil {
return nil, fmt.Errorf("failed to create ssh public key on hetzner cloud: %v", err)
}
if res.StatusCode != http.StatusCreated {
return nil, fmt.Errorf("failed to create ssh public key on hetzner cloud. invalid statuscode returned: %d", res.StatusCode)
}

return hkey, nil
}

func (p *provider) Create(machine *v1alpha1.Machine, userdata string) (instance.Instance, error) {
c, pc, err := getConfig(machine.Spec.ProviderConfig)
if err != nil {
return nil, fmt.Errorf("failed to parse config: %v", err)
}

ctx := context.TODO()
client := getClient(c.Token)

key, err := ensureSSHKeysExist(ctx, client.SSHKey, p.privateKey)
if err != nil {
return nil, fmt.Errorf("failed ensure that the ssh key '%s' exists: %v", p.privateKey.Name(), err)
}

imageName, err := getNameForOS(pc.OperatingSystem)
if err != nil {
return nil, fmt.Errorf("invalid operating system specified %q: %v", pc.OperatingSystem, err)
}

serverCreateOpts := hcloud.ServerCreateOpts{
Name: machine.Spec.Name,
UserData: userdata,
SSHKeys: []*hcloud.SSHKey{key},
}

if c.Datacenter != "" {
serverCreateOpts.Datacenter, _, err = client.Datacenter.Get(ctx, c.Datacenter)
if err != nil {
return nil, fmt.Errorf("failed to get datacenter: %v", err)
}
}

if c.Location != "" {
serverCreateOpts.Location, _, err = client.Location.Get(ctx, c.Location)
if err != nil {
return nil, fmt.Errorf("failed to get location: %v", err)
}
}

serverCreateOpts.Image, _, err = client.Image.Get(ctx, imageName)
if err != nil {
return nil, fmt.Errorf("failed to get image: %v", err)
}

serverCreateOpts.ServerType, _, err = client.ServerType.Get(ctx, c.ServerType)
if err != nil {
return nil, fmt.Errorf("failed to get server type: %v", err)
}

serverCreateRes, res, err := client.Server.Create(ctx, serverCreateOpts)
if err != nil {
return nil, fmt.Errorf("failed to create server: %v", err)
}
if res.StatusCode != http.StatusCreated {
return nil, fmt.Errorf("failed to create server invalid status code returned. expected=%d got %d", http.StatusCreated, res.StatusCode)
}

return &hetznerServer{server: serverCreateRes.Server}, nil
}

func (p *provider) Delete(machine *v1alpha1.Machine) error {
c, _, err := getConfig(machine.Spec.ProviderConfig)
if err != nil {
return fmt.Errorf("failed to parse config: %v", err)
}

ctx := context.TODO()
client := getClient(c.Token)
i, err := p.Get(machine)
if err != nil {
if err == cloudprovidererrors.ErrInstanceNotFound {
glog.V(4).Info("instance already deleted")
return nil
}
return err
}

res, err := client.Server.Delete(ctx, i.(*hetznerServer).server)
if err != nil {
return fmt.Errorf("failed to delete the server: %v", err)
}
if res.StatusCode != http.StatusOK && res.StatusCode != http.StatusNotFound {
return fmt.Errorf("invalid status code returned. expected=%d got=%d", http.StatusOK, res.StatusCode)
}
return err
}

func (p *provider) AddDefaults(spec v1alpha1.MachineSpec) (v1alpha1.MachineSpec, bool, error) {
return spec, false, nil
}

func (p *provider) Get(machine *v1alpha1.Machine) (instance.Instance, error) {
c, _, err := getConfig(machine.Spec.ProviderConfig)
if err != nil {
return nil, fmt.Errorf("failed to parse config: %v", err)
}

ctx := context.TODO()
client := getClient(c.Token)

server, _, err := client.Server.Get(ctx, machine.Spec.Name)
if err != nil {
return nil, err
}
if server == nil {
return nil, cloudprovidererrors.ErrInstanceNotFound
}

return &hetznerServer{server: server}, nil
}

func (p *provider) GetCloudConfig(spec v1alpha1.MachineSpec) (config string, name string, err error) {
return "", "", nil
}

type hetznerServer struct {
server *hcloud.Server
}

func (s *hetznerServer) Name() string {
return s.server.Name
}

func (s *hetznerServer) ID() string {
return strconv.Itoa(s.server.ID)
}

func (s *hetznerServer) Addresses() []string {
var addresses []string
for _, fips := range s.server.PublicNet.FloatingIPs {
addresses = append(addresses, fips.IP.String())
}

return append(addresses, s.server.PublicNet.IPv4.IP.String(), s.server.PublicNet.IPv6.IP.String())
}

0 comments on commit 1717402

Please sign in to comment.