Skip to content

Commit 6915fda

Browse files
feat: Implement support for image architecture (fixes #105, thanks @Knight1)
- switch to non-deprecated architecture-aware image lookup - use server type architecture by default - add --hetzner-image-arch for explicit setting - minor refactoring
1 parent 202612d commit 6915fda

File tree

3 files changed

+217
-54
lines changed

3 files changed

+217
-54
lines changed

README.md

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ You can find sources and pre-compiled binaries [here](https://github.com/JonasPr
1515

1616
```bash
1717
# Download the binary (this example downloads the binary for linux amd64)
18-
$ 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
19-
$ tar -xvf docker-machine-driver-hetzner_3.12.2_linux_amd64.tar.gz
18+
$ 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
19+
$ tar -xvf docker-machine-driver-hetzner_3.13.0_linux_amd64.tar.gz
2020

2121
# Make it executable and copy the binary in a directory accessible with your $PATH
2222
$ chmod +x docker-machine-driver-hetzner
@@ -91,7 +91,8 @@ $ docker-machine create \
9191
## Options
9292

9393
- `--hetzner-api-token`: **required**. Your project-specific access token for the Hetzner Cloud API.
94-
- `--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`).
94+
- `--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`).
95+
- `--hetzner-image`: The architecture to use during image lookup, inferred from the server type if not explicitly given.
9596
- `--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`).
9697
- `--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`).
9798
- `--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 \
115116
- `--hetzner-primary-ipv4/6`: Sets an existing primary IP (v4 or v6 respectively) for the server, as documented in [Networking](#networking)
116117
- `--hetzner-wait-on-error`: Amount of seconds to wait on server creation failure (0/no wait by default)
117118

119+
#### Image selection
120+
121+
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
122+
other `--hetzner-image*`-flags.
123+
124+
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
125+
architecture, which is usually inferred from the server type. One may explicitly specify it using `--hetzner-image-arch` in which case the user
126+
supplied value will take precedence.
127+
118128
#### Existing SSH keys
119129

120130
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.
136146
|---------------------------------|-------------------------------| -------------------------- |
137147
| **`--hetzner-api-token`** | `HETZNER_API_TOKEN` | |
138148
| `--hetzner-image` | `HETZNER_IMAGE` | `ubuntu-18.04` |
149+
| `--hetzner-image-arch` | `HETZNER_IMAGE_ARCH` | *(infer from server)* |
139150
| `--hetzner-image-id` | `HETZNER_IMAGE_ID` | |
140151
| `--hetzner-server-type` | `HETZNER_TYPE` | `cx11` |
141152
| `--hetzner-server-location` | `HETZNER_LOCATION` | *(let Hetzner choose)* |

driver.go

Lines changed: 140 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ type Driver struct {
2626
AccessToken string
2727
Image string
2828
ImageID int
29+
ImageArch hcloud.Architecture
2930
cachedImage *hcloud.Image
3031
Type string
3132
cachedType *hcloud.ServerType
@@ -69,6 +70,7 @@ const (
6970
flagAPIToken = "hetzner-api-token"
7071
flagImage = "hetzner-image"
7172
flagImageID = "hetzner-image-id"
73+
flagImageArch = "hetzner-image-arch"
7274
flagType = "hetzner-server-type"
7375
flagLocation = "hetzner-server-location"
7476
flagExKeyID = "hetzner-existing-key-id"
@@ -108,6 +110,8 @@ const (
108110
legacyFlagUserDataFromFile = "hetzner-user-data-from-file"
109111
legacyFlagDisablePublic4 = "hetzner-disable-public-4"
110112
legacyFlagDisablePublic6 = "hetzner-disable-public-6"
113+
114+
emptyImageArchitecture = hcloud.Architecture("")
111115
)
112116

113117
// NewDriver initializes a new driver instance; see [drivers.Driver.NewDriver]
@@ -144,6 +148,11 @@ func (d *Driver) GetCreateFlags() []mcnflag.Flag {
144148
Name: flagImageID,
145149
Usage: "Image to use for server creation",
146150
},
151+
mcnflag.StringFlag{
152+
EnvVar: "HETZNER_IMAGE_ARCH",
153+
Name: flagImageArch,
154+
Usage: "Image architecture for lookup to use for server creation",
155+
},
147156
mcnflag.StringFlag{
148157
EnvVar: "HETZNER_TYPE",
149158
Name: flagType,
@@ -305,12 +314,16 @@ func (d *Driver) setConfigFromFlagsImpl(opts drivers.DriverOptions) error {
305314
d.AccessToken = opts.String(flagAPIToken)
306315
d.Image = opts.String(flagImage)
307316
d.ImageID = opts.Int(flagImageID)
317+
err := d.setImageArch(opts.String(flagImageArch))
318+
if err != nil {
319+
return err
320+
}
308321
d.Location = opts.String(flagLocation)
309322
d.Type = opts.String(flagType)
310323
d.KeyID = opts.Int(flagExKeyID)
311324
d.IsExistingKey = d.KeyID != 0
312325
d.originalKey = opts.String(flagExKeyPath)
313-
err := d.setUserDataFlags(opts)
326+
err = d.setUserDataFlags(opts)
314327
if err != nil {
315328
return err
316329
}
@@ -349,12 +362,45 @@ func (d *Driver) setConfigFromFlagsImpl(opts drivers.DriverOptions) error {
349362
return d.flagFailure("hetzner requires --%v to be set", flagAPIToken)
350363
}
351364

365+
if err = d.verifyImageFlags(); err != nil {
366+
return err
367+
}
368+
369+
if err = d.verifyNetworkFlags(); err != nil {
370+
return err
371+
}
372+
373+
instrumented(d)
374+
375+
return nil
376+
}
377+
378+
func (d *Driver) setImageArch(arch string) error {
379+
switch arch {
380+
case "":
381+
d.ImageArch = emptyImageArchitecture
382+
case string(hcloud.ArchitectureARM):
383+
d.ImageArch = hcloud.ArchitectureARM
384+
case string(hcloud.ArchitectureX86):
385+
d.ImageArch = hcloud.ArchitectureX86
386+
default:
387+
return errors.Errorf("unknown architecture %v", arch)
388+
}
389+
return nil
390+
}
391+
392+
func (d *Driver) verifyImageFlags() error {
352393
if d.ImageID != 0 && d.Image != "" && d.Image != defaultImage /* support legacy behaviour */ {
353394
return d.flagFailure("--%v and --%v are mutually exclusive", flagImage, flagImageID)
395+
} else if d.ImageID != 0 && d.ImageArch != "" {
396+
return d.flagFailure("--%v and --%v are mutually exclusive", flagImageArch, flagImageID)
354397
} else if d.ImageID == 0 && d.Image == "" {
355398
d.Image = defaultImage
356399
}
400+
return nil
401+
}
357402

403+
func (d *Driver) verifyNetworkFlags() error {
358404
if d.DisablePublic4 && d.DisablePublic6 && !d.UsePrivateNetwork {
359405
return d.flagFailure("--%v must be used if public networking is disabled (hint: implicitly set by --%v)",
360406
flagUsePrivateNetwork, flagDisablePublic)
@@ -367,9 +413,6 @@ func (d *Driver) setConfigFromFlagsImpl(opts drivers.DriverOptions) error {
367413
if d.DisablePublic6 && d.PrimaryIPv6 != "" {
368414
return d.flagFailure("--%v and --%v are mutually exclusive", flagPrimary6, flagDisablePublic6)
369415
}
370-
371-
instrumented(d)
372-
373416
return nil
374417
}
375418

@@ -437,35 +480,14 @@ func (d *Driver) setLabelsFromFlags(opts drivers.DriverOptions) error {
437480

438481
// PreCreateCheck validates the Driver data is in a valid state for creation; see [drivers.Driver.PreCreateCheck]
439482
func (d *Driver) PreCreateCheck() error {
440-
if d.IsExistingKey {
441-
if d.originalKey == "" {
442-
return d.flagFailure("specifying an existing key ID requires the existing key path to be set as well")
443-
}
444-
445-
key, err := d.getKey()
446-
if err != nil {
447-
return errors.Wrap(err, "could not get key")
448-
}
449-
450-
buf, err := os.ReadFile(d.originalKey + ".pub")
451-
if err != nil {
452-
return errors.Wrap(err, "could not read public key")
453-
}
454-
455-
// Will also parse `ssh-rsa w309jwf0e39jf asdf` public keys
456-
pubk, _, _, _, err := ssh.ParseAuthorizedKey(buf)
457-
if err != nil {
458-
return errors.Wrap(err, "could not parse authorized key")
459-
}
460-
461-
if key.Fingerprint != ssh.FingerprintLegacyMD5(pubk) &&
462-
key.Fingerprint != ssh.FingerprintSHA256(pubk) {
463-
return errors.Errorf("remote key %d does not match local key %s", d.KeyID, d.originalKey)
464-
}
483+
if err := d.setupExistingKey(); err != nil {
484+
return err
465485
}
466486

467-
if _, err := d.getType(); err != nil {
487+
if serverType, err := d.getType(); err != nil {
468488
return errors.Wrap(err, "could not get type")
489+
} else if d.ImageArch != "" && serverType.Architecture != d.ImageArch {
490+
log.Warnf("supplied architecture %v differs from server architecture %v", d.ImageArch, serverType.Architecture)
469491
}
470492

471493
if _, err := d.getImage(); err != nil {
@@ -495,6 +517,39 @@ func (d *Driver) PreCreateCheck() error {
495517
return nil
496518
}
497519

520+
func (d *Driver) setupExistingKey() error {
521+
if !d.IsExistingKey {
522+
return nil
523+
}
524+
525+
if d.originalKey == "" {
526+
return d.flagFailure("specifying an existing key ID requires the existing key path to be set as well")
527+
}
528+
529+
key, err := d.getKey()
530+
if err != nil {
531+
return errors.Wrap(err, "could not get key")
532+
}
533+
534+
buf, err := os.ReadFile(d.originalKey + ".pub")
535+
if err != nil {
536+
return errors.Wrap(err, "could not read public key")
537+
}
538+
539+
// Will also parse `ssh-rsa w309jwf0e39jf asdf` public keys
540+
pubk, _, _, _, err := ssh.ParseAuthorizedKey(buf)
541+
if err != nil {
542+
return errors.Wrap(err, "could not parse authorized key")
543+
}
544+
545+
if key.Fingerprint != ssh.FingerprintLegacyMD5(pubk) &&
546+
key.Fingerprint != ssh.FingerprintSHA256(pubk) {
547+
return errors.Errorf("remote key %d does not match local key %s", d.KeyID, d.originalKey)
548+
}
549+
550+
return nil
551+
}
552+
498553
// Create actually creates the hetzner-cloud server; see [drivers.Driver.Create]
499554
func (d *Driver) Create() error {
500555
err := d.prepareLocalKey()
@@ -872,26 +927,8 @@ func (d *Driver) GetState() (state.State, error) {
872927

873928
// Remove deletes the hetzner server and additional resources created during creation; see [drivers.Driver.Remove]
874929
func (d *Driver) Remove() error {
875-
if d.ServerID != 0 {
876-
srv, err := d.getServerHandle()
877-
if err != nil {
878-
return errors.Wrap(err, "could not get server handle")
879-
}
880-
881-
if srv == nil {
882-
log.Infof(" -> Server does not exist anymore")
883-
} else {
884-
log.Infof(" -> Destroying server %s[%d] in...", srv.Name, srv.ID)
885-
886-
if _, err := d.getClient().Server.Delete(context.Background(), srv); err != nil {
887-
return errors.Wrap(err, "could not delete server")
888-
}
889-
890-
// failure to remove a placement group is not a hard error
891-
if softErr := d.removeEmptyServerPlacementGroup(srv); softErr != nil {
892-
log.Error(softErr)
893-
}
894-
}
930+
if err := d.destroyServer(); err != nil {
931+
return err
895932
}
896933

897934
// failure to remove a key is not ha hard error
@@ -931,6 +968,40 @@ func (d *Driver) Remove() error {
931968
return nil
932969
}
933970

971+
func (d *Driver) destroyServer() error {
972+
if d.ServerID == 0 {
973+
return nil
974+
}
975+
976+
srv, err := d.getServerHandle()
977+
if err != nil {
978+
return errors.Wrap(err, "could not get server handle")
979+
}
980+
981+
if srv == nil {
982+
log.Infof(" -> Server does not exist anymore")
983+
} else {
984+
log.Infof(" -> Destroying server %s[%d] in...", srv.Name, srv.ID)
985+
986+
res, _, err := d.getClient().Server.DeleteWithResult(context.Background(), srv)
987+
if err != nil {
988+
return errors.Wrap(err, "could not delete server")
989+
}
990+
991+
// failure to remove a placement group is not a hard error
992+
if softErr := d.removeEmptyServerPlacementGroup(srv); softErr != nil {
993+
log.Error(softErr)
994+
}
995+
996+
// wait for the server to actually be deleted
997+
if err = d.waitForAction(res.Action); err != nil {
998+
return errors.Wrap(err, "could not wait for deletion")
999+
}
1000+
}
1001+
1002+
return nil
1003+
}
1004+
9341005
// Restart instructs the hetzner cloud server to reboot; see [drivers.Driver.Restart]
9351006
func (d *Driver) Restart() error {
9361007
srv, err := d.getServerHandle()
@@ -1071,7 +1142,12 @@ func (d *Driver) getImage() (*hcloud.Image, error) {
10711142
return image, errors.Wrap(err, fmt.Sprintf("could not get image by id %v", d.ImageID))
10721143
}
10731144
} else {
1074-
image, _, err = d.getClient().Image.GetByName(context.Background(), d.Image)
1145+
arch, err := d.getImageArchitectureForLookup()
1146+
if err != nil {
1147+
return nil, errors.Wrap(err, "could not determine image architecture")
1148+
}
1149+
1150+
image, _, err = d.getClient().Image.GetByNameAndArchitecture(context.Background(), d.Image, arch)
10751151
if err != nil {
10761152
return image, errors.Wrap(err, fmt.Sprintf("could not get image by name %v", d.Image))
10771153
}
@@ -1081,6 +1157,19 @@ func (d *Driver) getImage() (*hcloud.Image, error) {
10811157
return instrumented(image), nil
10821158
}
10831159

1160+
func (d *Driver) getImageArchitectureForLookup() (hcloud.Architecture, error) {
1161+
if d.ImageArch != emptyImageArchitecture {
1162+
return d.ImageArch, nil
1163+
}
1164+
1165+
serverType, err := d.getType()
1166+
if err != nil {
1167+
return "", err
1168+
}
1169+
1170+
return serverType.Architecture, nil
1171+
}
1172+
10841173
func (d *Driver) getKey() (*hcloud.SSHKey, error) {
10851174
if d.cachedKey != nil {
10861175
return d.cachedKey, nil

0 commit comments

Comments
 (0)