diff --git a/driver.go b/driver.go index 39a0cb8bf65a9aeecba92cdbf9b22cd2b1514d9d..4868d1e434c1aa86ba6c5779ea7e7f7e5cd7d77a 100644 --- a/driver.go +++ b/driver.go @@ -20,6 +20,7 @@ import ( "golang.org/x/crypto/ssh" ) +// Driver contains hetzner-specific data to implement [drivers.Driver] type Driver struct { *drivers.BaseDriver @@ -70,6 +71,7 @@ const ( flagServerLabel = "hetzner-server-label" ) +// NewDriver initializes a new driver instance; see [drivers.Driver.NewDriver] func NewDriver() *Driver { return &Driver{ Image: defaultImage, @@ -83,10 +85,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{ @@ -174,6 +178,8 @@ func (d *Driver) GetCreateFlags() []mcnflag.Flag { } } +// 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) @@ -220,6 +226,7 @@ func (d *Driver) setLabelsFromFlags(opts drivers.DriverOptions) error { 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,180 +274,244 @@ 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 + } - key, err = d.makeKey(d.GetMachineName(), string(buf)) + 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 +} + +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)) + 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) - 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 } @@ -471,10 +542,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 +561,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 +582,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 +637,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 +657,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 +677,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 +697,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 +800,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) {