Skip to content
Commits on Source (2)
......@@ -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.12.2/docker-machine-driver-hetzner_3.12.2_linux_amd64.tar.gz
$ tar -xvf docker-machine-driver-hetzner_3.12.2_linux_amd64.tar.gz
$ wget https://github.com/JonasProgrammer/docker-machine-driver-hetzner/releases/download/3.13.0/docker-machine-driver-hetzner_3.13.0_linux_amd64.tar.gz
$ tar -xvf docker-machine-driver-hetzner_3.13.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
......@@ -91,7 +91,8 @@ $ docker-machine create \
## Options
- `--hetzner-api-token`: **required**. Your project-specific access token for the Hetzner Cloud API.
- `--hetzner-image`: The name of the Hetzner Cloud image to use, see [Images API](https://docs.hetzner.cloud/#resources-images-get) for how to get a list (defaults to `ubuntu-18.04`).
- `--hetzner-image`: The name (or ID) of the Hetzner Cloud image to use, see [Images API](https://docs.hetzner.cloud/#resources-images-get) for how to get a list (defaults to `ubuntu-18.04`).
- `--hetzner-image`: The architecture to use during image lookup, inferred from the server type if not explicitly given.
- `--hetzner-image-id`: The id of the Hetzner cloud image (or snapshot) to use, see [Images API](https://docs.hetzner.cloud/#resources-images-get) for how to get a list (mutually excludes `--hetzner-image`).
- `--hetzner-server-type`: The type of the Hetzner Cloud server, see [Server Types API](https://docs.hetzner.cloud/#resources-server-types-get) for how to get a list (defaults to `cx11`).
- `--hetzner-server-location`: The location to create the server in, see [Locations API](https://docs.hetzner.cloud/#resources-locations-get) for how to get a list.
......@@ -115,6 +116,15 @@ $ docker-machine create \
- `--hetzner-primary-ipv4/6`: Sets an existing primary IP (v4 or v6 respectively) for the server, as documented in [Networking](#networking)
- `--hetzner-wait-on-error`: Amount of seconds to wait on server creation failure (0/no wait by default)
#### Image selection
When `--hetzner-image-id` is passed, it will be used for lookup by ID as-is. No additional validation is performed, and it is mutually exclusive with
other `--hetzner-image*`-flags.
When `--hetzner-image` is passed, lookup will happen either by name or by ID as per Hetzner-supplied logic. The lookup mechanism will filter by image
architecture, which is usually inferred from the server type. One may explicitly specify it using `--hetzner-image-arch` in which case the user
supplied value will take precedence.
#### Existing SSH keys
When you specify the `--hetzner-existing-key-path` option, the driver will attempt to copy `(specified file name)`
......@@ -136,6 +146,7 @@ was used during creation.
|---------------------------------|-------------------------------| -------------------------- |
| **`--hetzner-api-token`** | `HETZNER_API_TOKEN` | |
| `--hetzner-image` | `HETZNER_IMAGE` | `ubuntu-18.04` |
| `--hetzner-image-arch` | `HETZNER_IMAGE_ARCH` | *(infer from server)* |
| `--hetzner-image-id` | `HETZNER_IMAGE_ID` | |
| `--hetzner-server-type` | `HETZNER_TYPE` | `cx11` |
| `--hetzner-server-location` | `HETZNER_LOCATION` | *(let Hetzner choose)* |
......
......@@ -26,6 +26,7 @@ type Driver struct {
AccessToken string
Image string
ImageID int
ImageArch hcloud.Architecture
cachedImage *hcloud.Image
Type string
cachedType *hcloud.ServerType
......@@ -69,6 +70,7 @@ const (
flagAPIToken = "hetzner-api-token"
flagImage = "hetzner-image"
flagImageID = "hetzner-image-id"
flagImageArch = "hetzner-image-arch"
flagType = "hetzner-server-type"
flagLocation = "hetzner-server-location"
flagExKeyID = "hetzner-existing-key-id"
......@@ -108,6 +110,8 @@ const (
legacyFlagUserDataFromFile = "hetzner-user-data-from-file"
legacyFlagDisablePublic4 = "hetzner-disable-public-4"
legacyFlagDisablePublic6 = "hetzner-disable-public-6"
emptyImageArchitecture = hcloud.Architecture("")
)
// NewDriver initializes a new driver instance; see [drivers.Driver.NewDriver]
......@@ -144,6 +148,11 @@ func (d *Driver) GetCreateFlags() []mcnflag.Flag {
Name: flagImageID,
Usage: "Image to use for server creation",
},
mcnflag.StringFlag{
EnvVar: "HETZNER_IMAGE_ARCH",
Name: flagImageArch,
Usage: "Image architecture for lookup to use for server creation",
},
mcnflag.StringFlag{
EnvVar: "HETZNER_TYPE",
Name: flagType,
......@@ -305,12 +314,16 @@ func (d *Driver) setConfigFromFlagsImpl(opts drivers.DriverOptions) error {
d.AccessToken = opts.String(flagAPIToken)
d.Image = opts.String(flagImage)
d.ImageID = opts.Int(flagImageID)
err := d.setImageArch(opts.String(flagImageArch))
if err != nil {
return err
}
d.Location = opts.String(flagLocation)
d.Type = opts.String(flagType)
d.KeyID = opts.Int(flagExKeyID)
d.IsExistingKey = d.KeyID != 0
d.originalKey = opts.String(flagExKeyPath)
err := d.setUserDataFlags(opts)
err = d.setUserDataFlags(opts)
if err != nil {
return err
}
......@@ -349,12 +362,45 @@ func (d *Driver) setConfigFromFlagsImpl(opts drivers.DriverOptions) error {
return d.flagFailure("hetzner requires --%v to be set", flagAPIToken)
}
if err = d.verifyImageFlags(); err != nil {
return err
}
if err = d.verifyNetworkFlags(); err != nil {
return err
}
instrumented(d)
return nil
}
func (d *Driver) setImageArch(arch string) error {
switch arch {
case "":
d.ImageArch = emptyImageArchitecture
case string(hcloud.ArchitectureARM):
d.ImageArch = hcloud.ArchitectureARM
case string(hcloud.ArchitectureX86):
d.ImageArch = hcloud.ArchitectureX86
default:
return errors.Errorf("unknown architecture %v", arch)
}
return nil
}
func (d *Driver) verifyImageFlags() error {
if d.ImageID != 0 && d.Image != "" && d.Image != defaultImage /* support legacy behaviour */ {
return d.flagFailure("--%v and --%v are mutually exclusive", flagImage, flagImageID)
} else if d.ImageID != 0 && d.ImageArch != "" {
return d.flagFailure("--%v and --%v are mutually exclusive", flagImageArch, flagImageID)
} else if d.ImageID == 0 && d.Image == "" {
d.Image = defaultImage
}
return nil
}
func (d *Driver) verifyNetworkFlags() error {
if d.DisablePublic4 && d.DisablePublic6 && !d.UsePrivateNetwork {
return d.flagFailure("--%v must be used if public networking is disabled (hint: implicitly set by --%v)",
flagUsePrivateNetwork, flagDisablePublic)
......@@ -367,9 +413,6 @@ func (d *Driver) setConfigFromFlagsImpl(opts drivers.DriverOptions) error {
if d.DisablePublic6 && d.PrimaryIPv6 != "" {
return d.flagFailure("--%v and --%v are mutually exclusive", flagPrimary6, flagDisablePublic6)
}
instrumented(d)
return nil
}
......@@ -437,35 +480,14 @@ func (d *Driver) setLabelsFromFlags(opts drivers.DriverOptions) error {
// 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 == "" {
return d.flagFailure("specifying an existing key ID requires the existing key path to be set as well")
}
key, err := d.getKey()
if err != nil {
return errors.Wrap(err, "could not get key")
}
buf, err := os.ReadFile(d.originalKey + ".pub")
if err != nil {
return errors.Wrap(err, "could not read public key")
}
// Will also parse `ssh-rsa w309jwf0e39jf asdf` public keys
pubk, _, _, _, err := ssh.ParseAuthorizedKey(buf)
if err != nil {
return errors.Wrap(err, "could not parse authorized key")
}
if key.Fingerprint != ssh.FingerprintLegacyMD5(pubk) &&
key.Fingerprint != ssh.FingerprintSHA256(pubk) {
return errors.Errorf("remote key %d does not match local key %s", d.KeyID, d.originalKey)
}
if err := d.setupExistingKey(); err != nil {
return err
}
if _, err := d.getType(); err != nil {
if serverType, err := d.getType(); err != nil {
return errors.Wrap(err, "could not get type")
} else if d.ImageArch != "" && serverType.Architecture != d.ImageArch {
log.Warnf("supplied architecture %v differs from server architecture %v", d.ImageArch, serverType.Architecture)
}
if _, err := d.getImage(); err != nil {
......@@ -495,6 +517,39 @@ func (d *Driver) PreCreateCheck() error {
return nil
}
func (d *Driver) setupExistingKey() error {
if !d.IsExistingKey {
return nil
}
if d.originalKey == "" {
return d.flagFailure("specifying an existing key ID requires the existing key path to be set as well")
}
key, err := d.getKey()
if err != nil {
return errors.Wrap(err, "could not get key")
}
buf, err := os.ReadFile(d.originalKey + ".pub")
if err != nil {
return errors.Wrap(err, "could not read public key")
}
// Will also parse `ssh-rsa w309jwf0e39jf asdf` public keys
pubk, _, _, _, err := ssh.ParseAuthorizedKey(buf)
if err != nil {
return errors.Wrap(err, "could not parse authorized key")
}
if key.Fingerprint != ssh.FingerprintLegacyMD5(pubk) &&
key.Fingerprint != ssh.FingerprintSHA256(pubk) {
return errors.Errorf("remote key %d does not match local key %s", d.KeyID, d.originalKey)
}
return nil
}
// Create actually creates the hetzner-cloud server; see [drivers.Driver.Create]
func (d *Driver) Create() error {
err := d.prepareLocalKey()
......@@ -872,26 +927,8 @@ func (d *Driver) GetState() (state.State, error) {
// 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()
if err != nil {
return errors.Wrap(err, "could not get server handle")
}
if srv == nil {
log.Infof(" -> Server does not exist anymore")
} else {
log.Infof(" -> Destroying server %s[%d] in...", srv.Name, srv.ID)
if _, err := d.getClient().Server.Delete(context.Background(), srv); err != nil {
return errors.Wrap(err, "could not delete server")
}
// failure to remove a placement group is not a hard error
if softErr := d.removeEmptyServerPlacementGroup(srv); softErr != nil {
log.Error(softErr)
}
}
if err := d.destroyServer(); err != nil {
return err
}
// failure to remove a key is not ha hard error
......@@ -931,6 +968,40 @@ func (d *Driver) Remove() error {
return nil
}
func (d *Driver) destroyServer() error {
if d.ServerID == 0 {
return nil
}
srv, err := d.getServerHandle()
if err != nil {
return errors.Wrap(err, "could not get server handle")
}
if srv == nil {
log.Infof(" -> Server does not exist anymore")
} else {
log.Infof(" -> Destroying server %s[%d] in...", srv.Name, srv.ID)
res, _, err := d.getClient().Server.DeleteWithResult(context.Background(), srv)
if err != nil {
return errors.Wrap(err, "could not delete server")
}
// failure to remove a placement group is not a hard error
if softErr := d.removeEmptyServerPlacementGroup(srv); softErr != nil {
log.Error(softErr)
}
// wait for the server to actually be deleted
if err = d.waitForAction(res.Action); err != nil {
return errors.Wrap(err, "could not wait for deletion")
}
}
return nil
}
// Restart instructs the hetzner cloud server to reboot; see [drivers.Driver.Restart]
func (d *Driver) Restart() error {
srv, err := d.getServerHandle()
......@@ -1071,7 +1142,12 @@ func (d *Driver) getImage() (*hcloud.Image, error) {
return image, errors.Wrap(err, fmt.Sprintf("could not get image by id %v", d.ImageID))
}
} else {
image, _, err = d.getClient().Image.GetByName(context.Background(), d.Image)
arch, err := d.getImageArchitectureForLookup()
if err != nil {
return nil, errors.Wrap(err, "could not determine image architecture")
}
image, _, err = d.getClient().Image.GetByNameAndArchitecture(context.Background(), d.Image, arch)
if err != nil {
return image, errors.Wrap(err, fmt.Sprintf("could not get image by name %v", d.Image))
}
......@@ -1081,6 +1157,19 @@ func (d *Driver) getImage() (*hcloud.Image, error) {
return instrumented(image), nil
}
func (d *Driver) getImageArchitectureForLookup() (hcloud.Architecture, error) {
if d.ImageArch != emptyImageArchitecture {
return d.ImageArch, nil
}
serverType, err := d.getType()
if err != nil {
return "", err
}
return serverType.Architecture, nil
}
func (d *Driver) getKey() (*hcloud.SSHKey, error) {
if d.cachedKey != nil {
return d.cachedKey, nil
......
......@@ -3,6 +3,7 @@ package main
import (
"github.com/docker/machine/commands/commandstest"
"github.com/docker/machine/libmachine/drivers"
"github.com/hetznercloud/hcloud-go/hcloud"
"os"
"strings"
"testing"
......@@ -207,6 +208,68 @@ func TestDisablePublic46Legacy(t *testing.T) {
}
}
func TestImageFlagExclusions(t *testing.T) {
// both id and name given
d := NewDriver()
err := d.setConfigFromFlagsImpl(makeFlags(map[string]interface{}{
flagImageID: 42,
flagImage: "answer",
}))
assertMutualExclusion(t, err, flagImageID, flagImage)
// both id and arch given
d = NewDriver()
err = d.setConfigFromFlagsImpl(makeFlags(map[string]interface{}{
flagImageID: 42,
flagImageArch: string(hcloud.ArchitectureX86),
}))
assertMutualExclusion(t, err, flagImageID, flagImageArch)
}
func TestImageArch(t *testing.T) {
// no explicit arch
d := NewDriver()
err := d.setConfigFromFlagsImpl(makeFlags(map[string]interface{}{
flagImage: "answer",
}))
if err != nil {
t.Fatalf("unexpected error, %v", err)
}
if d.ImageArch != emptyImageArchitecture {
t.Errorf("expected empty architecture, but got %v", d.ImageArch)
}
// existing architectures
testArchFlag(t, hcloud.ArchitectureARM)
testArchFlag(t, hcloud.ArchitectureX86)
// invalid
d = NewDriver()
err = d.setConfigFromFlagsImpl(makeFlags(map[string]interface{}{
flagImage: "answer",
flagImageArch: "hal9000",
}))
if err == nil {
t.Fatal("expected error, but invalid arch was accepted")
}
}
func testArchFlag(t *testing.T, arch hcloud.Architecture) {
d := NewDriver()
err := d.setConfigFromFlagsImpl(makeFlags(map[string]interface{}{
flagImage: "answer",
flagImageArch: string(arch),
}))
if err != nil {
t.Fatalf("unexpected error, %v", err)
}
if d.ImageArch != arch {
t.Errorf("expected %v architecture, but got %v", arch, d.ImageArch)
}
}
func assertMutualExclusion(t *testing.T, err error, flag1, flag2 string) {
if err == nil {
t.Errorf("expected mutually exclusive flags to fail, but no error was thrown: %v %v", flag1, flag2)
......
......@@ -4,31 +4,31 @@ go 1.18
require (
github.com/docker/machine v0.16.2
github.com/hetznercloud/hcloud-go v1.37.0
github.com/hetznercloud/hcloud-go v1.42.0
github.com/pkg/errors v0.9.1
golang.org/x/crypto v0.3.0
golang.org/x/crypto v0.8.0
)
replace github.com/codegangsta/cli v1.22.12 => github.com/urfave/cli v1.22.12
require (
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/codegangsta/cli v1.22.12 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/docker/docker v20.10.21+incompatible // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/moby/term v0.0.0-20221120202655-abb19827d345 // indirect
github.com/prometheus/client_golang v1.14.0 // indirect
github.com/moby/term v0.0.0-20221205130635-1aeaba878587 // indirect
github.com/prometheus/client_golang v1.15.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.9.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
golang.org/x/net v0.2.0 // indirect
golang.org/x/sys v0.2.0 // indirect
golang.org/x/term v0.2.0 // indirect
golang.org/x/text v0.4.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
golang.org/x/net v0.9.0 // indirect
golang.org/x/sys v0.7.0 // indirect
golang.org/x/term v0.7.0 // indirect
golang.org/x/text v0.9.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
)
This diff is collapsed.