Skip to content
Commits on Source (6)
......@@ -15,8 +15,8 @@ You can find sources and pre-compiled binaries [here](https://github.com/JonasPr
```bash
# Download the binary (this example downloads the binary for linux amd64)
$ wget https://github.com/JonasProgrammer/docker-machine-driver-hetzner/releases/download/3.3.0/docker-machine-driver-hetzner_3.3.0_linux_amd64.tar.gz
$ tar -xvf docker-machine-driver-hetzner_3.3.0_linux_amd64.tar.gz
$ wget https://github.com/JonasProgrammer/docker-machine-driver-hetzner/releases/download/3.4.0/docker-machine-driver-hetzner_3.4.0_linux_amd64.tar.gz
$ tar -xvf docker-machine-driver-hetzner_3.4.0_linux_amd64.tar.gz
# Make it executable and copy the binary in a directory accessible with your $PATH
$ chmod +x docker-machine-driver-hetzner
......@@ -105,6 +105,7 @@ $ docker-machine create \
- `--hetzner-use-private-network`: Use private network
- `--hetzner-firewalls`: Firewall IDs or names which should be applied on the server
- `--hetzner-server-label`: `key=value` pairs of additional metadata to assign to the server.
- `--hetzner-key-label`: `key=value` pairs of additional metadata to assign to SSH key (only applies if newly creadted).
#### Existing SSH keys
......@@ -138,8 +139,8 @@ was used during creation.
| `--hetzner-firewalls` | `HETZNER_FIREWALLS` | - |
| `--hetzner-volumes` | `HETZNER_VOLUMES` | - |
| `--hetzner-use-private-network` | `HETZNER_USE_PRIVATE_NETWORK` | false |
| `--hetzner-server-label` | `HETZNER_SERVER_LABELS` | `[]` |
| `--hetzner-server-label` | (inoperative) | `[]` |
| `--hetzner-key-label` | (inoperative) | `[]` |
## Building from source
......
......@@ -20,6 +20,7 @@ import (
"golang.org/x/crypto/ssh"
)
// Driver contains hetzner-specific data to implement [drivers.Driver]
type Driver struct {
*drivers.BaseDriver
......@@ -44,6 +45,7 @@ type Driver struct {
firewalls []string
cachedServer *hcloud.Server
serverLabels map[string]string
keyLabels map[string]string
additionalKeys []string
AdditionalKeyIDs []int
......@@ -68,8 +70,10 @@ const (
flagFirewalls = "hetzner-firewalls"
flagAdditionalKeys = "hetzner-additional-key"
flagServerLabel = "hetzner-server-label"
flagKeyLabel = "hetzner-key-label"
)
// NewDriver initializes a new driver instance; see [drivers.Driver.NewDriver]
func NewDriver() *Driver {
return &Driver{
Image: defaultImage,
......@@ -83,10 +87,12 @@ func NewDriver() *Driver {
}
}
// DriverName returns the hard-coded string "hetzner"; see [drivers.Driver.DriverName]
func (d *Driver) DriverName() string {
return "hetzner"
}
// GetCreateFlags retrieves additional driver-specific arguments; see [drivers.Driver.GetCreateFlags]
func (d *Driver) GetCreateFlags() []mcnflag.Flag {
return []mcnflag.Flag{
mcnflag.StringFlag{
......@@ -171,9 +177,17 @@ func (d *Driver) GetCreateFlags() []mcnflag.Flag {
Usage: "Key value pairs of additional labels to assign to the server",
Value: []string{},
},
mcnflag.StringSliceFlag{
EnvVar: "HETZNER_KEY_LABELS",
Name: flagKeyLabel,
Usage: "Key value pairs of additional labels to assign to the SSH key",
Value: []string{},
},
}
}
// SetConfigFromFlags handles additional driver arguments as retrieved by [Driver.GetCreateFlags];
// see [drivers.Driver.SetConfigFromFlags]
func (d *Driver) SetConfigFromFlags(opts drivers.DriverOptions) error {
d.AccessToken = opts.String(flagAPIToken)
d.Image = opts.String(flagImage)
......@@ -217,9 +231,18 @@ func (d *Driver) setLabelsFromFlags(opts drivers.DriverOptions) error {
}
d.serverLabels[split[0]] = split[1]
}
d.keyLabels = make(map[string]string)
for _, label := range opts.StringSlice(flagKeyLabel) {
split := strings.SplitN(label, "=", 2)
if len(split) != 2 {
return errors.Errorf("key label %v is not in key=value format", label)
}
d.keyLabels[split[0]] = split[1]
}
return nil
}
// PreCreateCheck validates the Driver data is in a valid state for creation; see [drivers.Driver.PreCreateCheck]
func (d *Driver) PreCreateCheck() error {
if d.IsExistingKey {
if d.originalKey == "" {
......@@ -267,188 +290,253 @@ func (d *Driver) PreCreateCheck() error {
return nil
}
// Create actually creates the hetzner-cloud server; see [drivers.Driver.Create]
func (d *Driver) Create() error {
if d.originalKey != "" {
log.Debugf("Copying SSH key...")
if err := d.copySSHKeyPair(d.originalKey); err != nil {
return errors.Wrap(err, "could not copy ssh key pair")
}
} else {
log.Debugf("Generating SSH key...")
if err := mcnssh.GenerateSSHKey(d.GetSSHKeyPath()); err != nil {
return errors.Wrap(err, "could not generate ssh key")
}
err := d.prepareLocalKey()
if err != nil {
return err
}
defer d.destroyDanglingKeys()
if d.KeyID == 0 {
log.Infof("Creating SSH key...")
err = d.createRemoteKeys()
if err != nil {
return err
}
buf, err := ioutil.ReadFile(d.GetSSHKeyPath() + ".pub")
if err != nil {
return errors.Wrap(err, "could not read ssh public key")
}
log.Infof("Creating Hetzner server...")
key, err := d.getRemoteKeyWithSameFingerprint(buf)
if err != nil {
return errors.Wrap(err, "error retrieving potentially existing key")
}
if key == nil {
log.Infof("SSH key not found in Hetzner. Uploading...")
srvopts, err := d.makeCreateServerOptions()
if err != nil {
return err
}
srv, _, err := d.getClient().Server.Create(context.Background(), *srvopts)
if err != nil {
return errors.Wrap(err, "could not create server")
}
log.Infof(" -> Creating server %s[%d] in %s[%d]", srv.Server.Name, srv.Server.ID, srv.Action.Command, srv.Action.ID)
if err = d.waitForAction(srv.Action); err != nil {
return errors.Wrap(err, "could not wait for action")
}
d.ServerID = srv.Server.ID
log.Infof(" -> Server %s[%d]: Waiting to come up...", srv.Server.Name, srv.Server.ID)
err = d.waitForRunningServer()
if err != nil {
return err
}
err = d.configureNetworkAccess(srv)
if err != nil {
return err
}
log.Infof(" -> Server %s[%d] ready. Ip %s", srv.Server.Name, srv.Server.ID, d.IPAddress)
// Successful creation, so no keys dangle anymore
d.danglingKeys = nil
return nil
}
key, err = d.makeKey(d.GetMachineName(), string(buf))
func (d *Driver) configureNetworkAccess(srv hcloud.ServerCreateResult) error {
if d.UsePrivateNetwork {
for {
// we need to wait until network is attached
log.Infof("Wait until private network attached ...")
server, _, err := d.getClient().Server.GetByID(context.Background(), srv.Server.ID)
if err != nil {
return err
return errors.Wrapf(err, "could not get newly created server [%d]", srv.Server.ID)
}
} else {
d.IsExistingKey = true
log.Debugf("SSH key found in Hetzner. ID: %d", key.ID)
if server.PrivateNet != nil {
d.IPAddress = server.PrivateNet[0].IP.String()
break
}
time.Sleep(1 * time.Second)
}
d.KeyID = key.ID
} else {
log.Infof("Using public network ...")
d.IPAddress = srv.Server.PublicNet.IPv4.IP.String()
}
for i, pubkey := range d.additionalKeys {
key, err := d.getRemoteKeyWithSameFingerprint([]byte(pubkey))
return nil
}
func (d *Driver) waitForRunningServer() error {
for {
srvstate, err := d.GetState()
if err != nil {
return errors.Wrapf(err, "error checking for existing key for %v", pubkey)
return errors.Wrap(err, "could not get state")
}
if key == nil {
log.Infof("Creating new key for %v...", pubkey)
key, err = d.makeKey(fmt.Sprintf("%v-additional-%d", d.GetMachineName(), i), pubkey)
if err != nil {
return errors.Wrapf(err, "error creating new key for %v", pubkey)
}
log.Infof(" -> Created %v", key.ID)
d.AdditionalKeyIDs = append(d.AdditionalKeyIDs, key.ID)
} else {
log.Infof("Using existing key (%v) %v", key.ID, key.Name)
if srvstate == state.Running {
break
}
d.cachedAdditionalKeys = append(d.cachedAdditionalKeys, key)
time.Sleep(1 * time.Second)
}
return nil
}
log.Infof("Creating Hetzner server...")
func (d *Driver) makeCreateServerOptions() (*hcloud.ServerCreateOpts, error) {
srvopts := hcloud.ServerCreateOpts{
Name: d.GetMachineName(),
UserData: d.userData,
Labels: d.serverLabels,
}
networks, err := d.createNetworks()
if err != nil {
return nil, err
}
srvopts.Networks = networks
firewalls, err := d.createFirewalls()
if err != nil {
return nil, err
}
srvopts.Firewalls = firewalls
volumes, err := d.createVolumes()
if err != nil {
return nil, err
}
srvopts.Volumes = volumes
if srvopts.Location, err = d.getLocation(); err != nil {
return nil, errors.Wrap(err, "could not get location")
}
if srvopts.ServerType, err = d.getType(); err != nil {
return nil, errors.Wrap(err, "could not get type")
}
if srvopts.Image, err = d.getImage(); err != nil {
return nil, errors.Wrap(err, "could not get image")
}
key, err := d.getKey()
if err != nil {
return nil, errors.Wrap(err, "could not get ssh key")
}
srvopts.SSHKeys = append(d.cachedAdditionalKeys, key)
return &srvopts, nil
}
func (d *Driver) createNetworks() ([]*hcloud.Network, error) {
networks := []*hcloud.Network{}
for _, networkIDorName := range d.networks {
network, _, err := d.getClient().Network.Get(context.Background(), networkIDorName)
if err != nil {
return errors.Wrap(err, "could not get network by ID or name")
return nil, errors.Wrap(err, "could not get network by ID or name")
}
if network == nil {
return errors.Errorf("network '%s' not found", networkIDorName)
return nil, errors.Errorf("network '%s' not found", networkIDorName)
}
networks = append(networks, network)
}
srvopts.Networks = networks
return networks, nil
}
func (d *Driver) createFirewalls() ([]*hcloud.ServerCreateFirewall, error) {
firewalls := []*hcloud.ServerCreateFirewall{}
for _, firewallIDorName := range d.firewalls {
firewall, _, err := d.getClient().Firewall.Get(context.Background(), firewallIDorName)
if err != nil {
return errors.Wrap(err, "could not get firewall by ID or name")
return nil, errors.Wrap(err, "could not get firewall by ID or name")
}
if firewall == nil {
return errors.Errorf("firewall '%s' not found", firewallIDorName)
return nil, errors.Errorf("firewall '%s' not found", firewallIDorName)
}
firewalls = append(firewalls, &hcloud.ServerCreateFirewall{Firewall: *firewall})
}
srvopts.Firewalls = firewalls
return firewalls, nil
}
func (d *Driver) createVolumes() ([]*hcloud.Volume, error) {
volumes := []*hcloud.Volume{}
for _, volumeIDorName := range d.volumes {
volume, _, err := d.getClient().Volume.Get(context.Background(), volumeIDorName)
if err != nil {
return errors.Wrap(err, "could not get volume by ID or name")
return nil, errors.Wrap(err, "could not get volume by ID or name")
}
if volume == nil {
return errors.Errorf("volume '%s' not found", volumeIDorName)
return nil, errors.Errorf("volume '%s' not found", volumeIDorName)
}
volumes = append(volumes, volume)
}
srvopts.Volumes = volumes
var err error
if srvopts.Location, err = d.getLocation(); err != nil {
return errors.Wrap(err, "could not get location")
}
if srvopts.ServerType, err = d.getType(); err != nil {
return errors.Wrap(err, "could not get type")
}
if srvopts.Image, err = d.getImage(); err != nil {
return errors.Wrap(err, "could not get image")
}
key, err := d.getKey()
if err != nil {
return errors.Wrap(err, "could not get ssh key")
}
srvopts.SSHKeys = append(d.cachedAdditionalKeys, key)
srv, _, err := d.getClient().Server.Create(context.Background(), srvopts)
if err != nil {
return errors.Wrap(err, "could not create server")
}
return volumes, nil
}
log.Infof(" -> Creating server %s[%d] in %s[%d]", srv.Server.Name, srv.Server.ID, srv.Action.Command, srv.Action.ID)
if err = d.waitForAction(srv.Action); err != nil {
return errors.Wrap(err, "could not wait for action")
}
func (d *Driver) createRemoteKeys() error {
if d.KeyID == 0 {
log.Infof("Creating SSH key...")
d.ServerID = srv.Server.ID
log.Infof(" -> Server %s[%d]: Waiting to come up...", srv.Server.Name, srv.Server.ID)
buf, err := ioutil.ReadFile(d.GetSSHKeyPath() + ".pub")
if err != nil {
return errors.Wrap(err, "could not read ssh public key")
}
for {
srvstate, err := d.GetState()
key, err := d.getRemoteKeyWithSameFingerprint(buf)
if err != nil {
return errors.Wrap(err, "could not get state")
return errors.Wrap(err, "error retrieving potentially existing key")
}
if key == nil {
log.Infof("SSH key not found in Hetzner. Uploading...")
if srvstate == state.Running {
break
key, err = d.makeKey(d.GetMachineName(), string(buf), d.keyLabels)
if err != nil {
return err
}
} else {
d.IsExistingKey = true
log.Debugf("SSH key found in Hetzner. ID: %d", key.ID)
}
time.Sleep(1 * time.Second)
d.KeyID = key.ID
}
for i, pubkey := range d.additionalKeys {
key, err := d.getRemoteKeyWithSameFingerprint([]byte(pubkey))
if err != nil {
return errors.Wrapf(err, "error checking for existing key for %v", pubkey)
}
if key == nil {
log.Infof("Creating new key for %v...", pubkey)
key, err = d.makeKey(fmt.Sprintf("%v-additional-%d", d.GetMachineName(), i), pubkey, d.keyLabels)
if d.UsePrivateNetwork {
for {
// we need to wait until network is attached
log.Infof("Wait until private network attached ...")
server, _, err := d.getClient().Server.GetByID(context.Background(), srv.Server.ID)
if err != nil {
return errors.Wrapf(err, "could not get newly created server [%d]", srv.Server.ID)
}
if server.PrivateNet != nil {
d.IPAddress = server.PrivateNet[0].IP.String()
break
return errors.Wrapf(err, "error creating new key for %v", pubkey)
}
time.Sleep(1 * time.Second)
log.Infof(" -> Created %v", key.ID)
d.AdditionalKeyIDs = append(d.AdditionalKeyIDs, key.ID)
} else {
log.Infof("Using existing key (%v) %v", key.ID, key.Name)
}
} else {
log.Infof("Using public network ...")
d.IPAddress = srv.Server.PublicNet.IPv4.IP.String()
}
log.Infof(" -> Server %s[%d] ready. Ip %s", srv.Server.Name, srv.Server.ID, d.IPAddress)
// Successful creation, so no keys dangle anymore
d.danglingKeys = nil
d.cachedAdditionalKeys = append(d.cachedAdditionalKeys, key)
}
return nil
}
func (d *Driver) prepareLocalKey() error {
if d.originalKey != "" {
log.Debugf("Copying SSH key...")
if err := d.copySSHKeyPair(d.originalKey); err != nil {
return errors.Wrap(err, "could not copy ssh key pair")
}
} else {
log.Debugf("Generating SSH key...")
if err := mcnssh.GenerateSSHKey(d.GetSSHKeyPath()); err != nil {
return errors.Wrap(err, "could not generate ssh key")
}
}
return nil
}
// Creates a new key for the machine and appends it to the dangling key list
func (d *Driver) makeKey(name string, pubkey string) (*hcloud.SSHKey, error) {
func (d *Driver) makeKey(name string, pubkey string, labels map[string]string) (*hcloud.SSHKey, error) {
keyopts := hcloud.SSHKeyCreateOpts{
Name: name,
PublicKey: pubkey,
Labels: labels,
}
key, _, err := d.getClient().SSHKey.Create(context.Background(), keyopts)
......@@ -471,10 +559,12 @@ func (d *Driver) destroyDanglingKeys() {
}
}
// GetSSHHostname retrieves the SSH host to connect to the machine; see [drivers.Driver.GetSSHHostname]
func (d *Driver) GetSSHHostname() (string, error) {
return d.GetIP()
}
// GetURL retrieves the URL of the docker daemon on the machine; see [drivers.Driver.GetURL]
func (d *Driver) GetURL() (string, error) {
if err := drivers.MustBeRunning(d); err != nil {
return "", errors.Wrap(err, "could not execute drivers.MustBeRunning")
......@@ -488,6 +578,7 @@ func (d *Driver) GetURL() (string, error) {
return fmt.Sprintf("tcp://%s", net.JoinHostPort(ip, "2376")), nil
}
// GetState retrieves the state the machine is currently in; see [drivers.Driver.GetState]
func (d *Driver) GetState() (state.State, error) {
srv, _, err := d.getClient().Server.GetByID(context.Background(), d.ServerID)
if err != nil {
......@@ -508,6 +599,7 @@ func (d *Driver) GetState() (state.State, error) {
return state.None, nil
}
// Remove deletes the hetzner server and additional resources created during creation; see [drivers.Driver.Remove]
func (d *Driver) Remove() error {
if d.ServerID != 0 {
srv, err := d.getServerHandle()
......@@ -562,6 +654,7 @@ func (d *Driver) Remove() error {
return nil
}
// Restart instructs the hetzner cloud server to reboot; see [drivers.Driver.Restart]
func (d *Driver) Restart() error {
srv, err := d.getServerHandle()
if err != nil {
......@@ -581,6 +674,7 @@ func (d *Driver) Restart() error {
return d.waitForAction(act)
}
// Start instructs the hetzner cloud server to power up; see [drivers.Driver.Start]
func (d *Driver) Start() error {
srv, err := d.getServerHandle()
if err != nil {
......@@ -600,6 +694,7 @@ func (d *Driver) Start() error {
return d.waitForAction(act)
}
// Stop instructs the hetzner cloud server to shut down; see [drivers.Driver.Stop]
func (d *Driver) Stop() error {
srv, err := d.getServerHandle()
if err != nil {
......@@ -619,6 +714,7 @@ func (d *Driver) Stop() error {
return d.waitForAction(act)
}
// Kill forcefully shuts down the hetzner cloud server; see [drivers.Driver.Kill]
func (d *Driver) Kill() error {
srv, err := d.getServerHandle()
if err != nil {
......@@ -721,19 +817,19 @@ func (d *Driver) getKey() (*hcloud.SSHKey, error) {
return stype, nil
}
func (d *Driver) getRemoteKeyWithSameFingerprint(pubkey_byte []byte) (*hcloud.SSHKey, error) {
pubkey, _, _, _, err := ssh.ParseAuthorizedKey(pubkey_byte)
func (d *Driver) getRemoteKeyWithSameFingerprint(publicKeyBytes []byte) (*hcloud.SSHKey, error) {
publicKey, _, _, _, err := ssh.ParseAuthorizedKey(publicKeyBytes)
if err != nil {
return nil, errors.Wrap(err, "could not parse ssh public key")
}
fp := ssh.FingerprintLegacyMD5(pubkey)
fp := ssh.FingerprintLegacyMD5(publicKey)
remotekey, _, err := d.getClient().SSHKey.GetByFingerprint(context.Background(), fp)
remoteKey, _, err := d.getClient().SSHKey.GetByFingerprint(context.Background(), fp)
if err != nil {
return remotekey, errors.Wrap(err, "could not get sshkey by fingerprint")
return remoteKey, errors.Wrap(err, "could not get sshkey by fingerprint")
}
return remotekey, nil
return remoteKey, nil
}
func (d *Driver) getServerHandle() (*hcloud.Server, error) {
......