Skip to content

Commit 0672649

Browse files
Merge pull request #73 from JonasProgrammer/feat/placement-group
feat: add placement groups
2 parents 57a0e93 + f32f790 commit 0672649

File tree

4 files changed

+317
-60
lines changed

4 files changed

+317
-60
lines changed

README.md

Lines changed: 15 additions & 11 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.4.0/docker-machine-driver-hetzner_3.4.0_linux_amd64.tar.gz
19-
$ tar -xvf docker-machine-driver-hetzner_3.4.0_linux_amd64.tar.gz
18+
$ wget https://github.com/JonasProgrammer/docker-machine-driver-hetzner/releases/download/3.5.0/docker-machine-driver-hetzner_3.5.0_linux_amd64.tar.gz
19+
$ tar -xvf docker-machine-driver-hetzner_3.5.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
@@ -106,6 +106,8 @@ $ docker-machine create \
106106
- `--hetzner-firewalls`: Firewall IDs or names which should be applied on the server
107107
- `--hetzner-server-label`: `key=value` pairs of additional metadata to assign to the server.
108108
- `--hetzner-key-label`: `key=value` pairs of additional metadata to assign to SSH key (only applies if newly creadted).
109+
- `--hetzner-placement-group`: Add to a placement group by name or ID; a spread-group will be created on demand if it does not exist
110+
- `--hetzner-auto-spread`: Add to a `docker-machine` provided `spread` group (mutually exclusive with `--hetzner-placement-group`)
109111

110112
#### Existing SSH keys
111113

@@ -126,21 +128,23 @@ was used during creation.
126128

127129
| CLI option | Environment variable | Default |
128130
| ----------------------------------- | --------------------------------- | -------------------------- |
129-
| **`--hetzner-api-token`** | `HETZNER_API_TOKEN` | - |
131+
| **`--hetzner-api-token`** | `HETZNER_API_TOKEN` | |
130132
| `--hetzner-image` | `HETZNER_IMAGE` | `ubuntu-18.04` |
131-
| `--hetzner-image-id` | `HETZNER_IMAGE_ID` | - |
133+
| `--hetzner-image-id` | `HETZNER_IMAGE_ID` | |
132134
| `--hetzner-server-type` | `HETZNER_TYPE` | `cx11` |
133-
| `--hetzner-server-location` | `HETZNER_LOCATION` | - *(let Hetzner choose)* |
134-
| `--hetzner-existing-key-path` | `HETZNER_EXISTING_KEY_PATH` | - *(generate new keypair)* |
135+
| `--hetzner-server-location` | `HETZNER_LOCATION` | *(let Hetzner choose)* |
136+
| `--hetzner-existing-key-path` | `HETZNER_EXISTING_KEY_PATH` | *(generate new keypair)* |
135137
| `--hetzner-existing-key-id` | `HETZNER_EXISTING_KEY_ID` | 0 *(upload new key)* |
136-
| `--hetzner-additional-key` | `HETZNER_ADDITIONAL_KEYS` | - |
137-
| `--hetzner-user-data` | `HETZNER_USER_DATA` | - |
138-
| `--hetzner-networks` | `HETZNER_NETWORKS` | - |
139-
| `--hetzner-firewalls` | `HETZNER_FIREWALLS` | - |
140-
| `--hetzner-volumes` | `HETZNER_VOLUMES` | - |
138+
| `--hetzner-additional-key` | `HETZNER_ADDITIONAL_KEYS` | |
139+
| `--hetzner-user-data` | `HETZNER_USER_DATA` | |
140+
| `--hetzner-networks` | `HETZNER_NETWORKS` | |
141+
| `--hetzner-firewalls` | `HETZNER_FIREWALLS` | |
142+
| `--hetzner-volumes` | `HETZNER_VOLUMES` | |
141143
| `--hetzner-use-private-network` | `HETZNER_USE_PRIVATE_NETWORK` | false |
142144
| `--hetzner-server-label` | (inoperative) | `[]` |
143145
| `--hetzner-key-label` | (inoperative) | `[]` |
146+
| `--hetzner-placement-group` | `HETZNER_PLACEMENT_GROUP` | |
147+
| `--hetzner-auto-spread` | `HETZNER_AUTO_SPREAD` | false |
144148

145149
## Building from source
146150

driver.go

Lines changed: 166 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ type Driver struct {
3636
cachedKey *hcloud.SSHKey
3737
IsExistingKey bool
3838
originalKey string
39-
danglingKeys []*hcloud.SSHKey
39+
dangling []func()
4040
ServerID int
4141
userData string
4242
volumes []string
@@ -46,6 +46,8 @@ type Driver struct {
4646
cachedServer *hcloud.Server
4747
serverLabels map[string]string
4848
keyLabels map[string]string
49+
placementGroup string
50+
cachedPGrp *hcloud.PlacementGroup
4951

5052
additionalKeys []string
5153
AdditionalKeyIDs []int
@@ -71,6 +73,14 @@ const (
7173
flagAdditionalKeys = "hetzner-additional-key"
7274
flagServerLabel = "hetzner-server-label"
7375
flagKeyLabel = "hetzner-key-label"
76+
flagPlacementGroup = "hetzner-placement-group"
77+
flagAutoSpread = "hetzner-auto-spread"
78+
79+
labelNamespace = "docker-machine"
80+
labelAutoSpreadPg = "auto-spread"
81+
labelAutoCreated = "auto-created"
82+
83+
autoSpreadPgName = "__auto_spread"
7484
)
7585

7686
// NewDriver initializes a new driver instance; see [drivers.Driver.NewDriver]
@@ -79,7 +89,6 @@ func NewDriver() *Driver {
7989
Image: defaultImage,
8090
Type: defaultType,
8191
IsExistingKey: false,
82-
danglingKeys: []*hcloud.SSHKey{},
8392
BaseDriver: &drivers.BaseDriver{
8493
SSHUser: drivers.DefaultSSHUser,
8594
SSHPort: drivers.DefaultSSHPort,
@@ -183,6 +192,17 @@ func (d *Driver) GetCreateFlags() []mcnflag.Flag {
183192
Usage: "Key value pairs of additional labels to assign to the SSH key",
184193
Value: []string{},
185194
},
195+
mcnflag.StringFlag{
196+
EnvVar: "HETZNER_PLACEMENT_GROUP",
197+
Name: flagPlacementGroup,
198+
Usage: "Placement group ID or name to add the server to; will be created if it does not exist",
199+
Value: "",
200+
},
201+
mcnflag.BoolFlag{
202+
EnvVar: "HETZNER_AUTO_SPREAD",
203+
Name: flagAutoSpread,
204+
Usage: "Auto-spread on a docker-machine-specific default placement group",
205+
},
186206
}
187207
}
188208

@@ -204,6 +224,14 @@ func (d *Driver) SetConfigFromFlags(opts drivers.DriverOptions) error {
204224
d.firewalls = opts.StringSlice(flagFirewalls)
205225
d.additionalKeys = opts.StringSlice(flagAdditionalKeys)
206226

227+
d.placementGroup = opts.String(flagPlacementGroup)
228+
if opts.Bool(flagAutoSpread) {
229+
if d.placementGroup != "" {
230+
return errors.Errorf(flagAutoSpread + " and " + flagPlacementGroup + " are mutually exclusive")
231+
}
232+
d.placementGroup = autoSpreadPgName
233+
}
234+
207235
err := d.setLabelsFromFlags(opts)
208236
if err != nil {
209237
return err
@@ -283,6 +311,10 @@ func (d *Driver) PreCreateCheck() error {
283311
return errors.Wrap(err, "could not get location")
284312
}
285313

314+
if _, err := d.getPlacementGroup(); err != nil {
315+
return fmt.Errorf("could not create placement group: %w", err)
316+
}
317+
286318
if d.UsePrivateNetwork && len(d.networks) == 0 {
287319
return errors.Errorf("No private network attached.")
288320
}
@@ -297,7 +329,7 @@ func (d *Driver) Create() error {
297329
return err
298330
}
299331

300-
defer d.destroyDanglingKeys()
332+
defer d.destroyDangling()
301333
err = d.createRemoteKeys()
302334
if err != nil {
303335
return err
@@ -335,7 +367,7 @@ func (d *Driver) Create() error {
335367

336368
log.Infof(" -> Server %s[%d] ready. Ip %s", srv.Server.Name, srv.Server.ID, d.IPAddress)
337369
// Successful creation, so no keys dangle anymore
338-
d.danglingKeys = nil
370+
d.dangling = nil
339371

340372
return nil
341373
}
@@ -379,10 +411,16 @@ func (d *Driver) waitForRunningServer() error {
379411
}
380412

381413
func (d *Driver) makeCreateServerOptions() (*hcloud.ServerCreateOpts, error) {
414+
pgrp, err := d.getPlacementGroup()
415+
if err != nil {
416+
return nil, err
417+
}
418+
382419
srvopts := hcloud.ServerCreateOpts{
383-
Name: d.GetMachineName(),
384-
UserData: d.userData,
385-
Labels: d.serverLabels,
420+
Name: d.GetMachineName(),
421+
UserData: d.userData,
422+
Labels: d.serverLabels,
423+
PlacementGroup: pgrp,
386424
}
387425

388426
networks, err := d.createNetworks()
@@ -546,16 +584,19 @@ func (d *Driver) makeKey(name string, pubkey string, labels map[string]string) (
546584
return nil, errors.Errorf("key upload did not return an error, but key was nil")
547585
}
548586

549-
d.danglingKeys = append(d.danglingKeys, key)
587+
d.dangling = append(d.dangling, func() {
588+
_, err := d.getClient().SSHKey.Delete(context.Background(), key)
589+
if err != nil {
590+
log.Error(fmt.Errorf("could not delete ssh key: %w", err))
591+
}
592+
})
593+
550594
return key, nil
551595
}
552596

553-
func (d *Driver) destroyDanglingKeys() {
554-
for _, key := range d.danglingKeys {
555-
if _, err := d.getClient().SSHKey.Delete(context.Background(), key); err != nil {
556-
log.Errorf("could not delete ssh key: %v", err)
557-
return
558-
}
597+
func (d *Driver) destroyDangling() {
598+
for _, destructor := range d.dangling {
599+
destructor()
559600
}
560601
}
561602

@@ -615,25 +656,31 @@ func (d *Driver) Remove() error {
615656
if _, err := d.getClient().Server.Delete(context.Background(), srv); err != nil {
616657
return errors.Wrap(err, "could not delete server")
617658
}
659+
660+
// failure to remove a placement group is not a hard error
661+
if softErr := d.removeEmptyServerPlacementGroup(srv); softErr != nil {
662+
log.Error(softErr)
663+
}
618664
}
619665
}
620666

621-
// Failing to remove these is just a soft error
667+
// failure to remove a key is not ha hard error
622668
for i, id := range d.AdditionalKeyIDs {
623669
log.Infof(" -> Destroying additional key #%d (%d)", i, id)
624-
key, _, err := d.getClient().SSHKey.GetByID(context.Background(), id)
625-
if err != nil {
626-
log.Warnf(" -> -> could not retrieve key %v", err)
670+
key, _, softErr := d.getClient().SSHKey.GetByID(context.Background(), id)
671+
if softErr != nil {
672+
log.Warnf(" -> -> could not retrieve key %v", softErr)
627673
} else if key == nil {
628674
log.Warnf(" -> -> %d no longer exists", id)
629675
}
630676

631-
_, err = d.getClient().SSHKey.Delete(context.Background(), key)
632-
if err != nil {
633-
log.Warnf(" -> -> could not remove key: %v", err)
677+
_, softErr = d.getClient().SSHKey.Delete(context.Background(), key)
678+
if softErr != nil {
679+
log.Warnf(" -> -> could not remove key: %v", softErr)
634680
}
635681
}
636682

683+
// failure to remove a server-specific key is a hard error
637684
if !d.IsExistingKey && d.KeyID != 0 {
638685
key, err := d.getKey()
639686
if err != nil {
@@ -870,3 +917,101 @@ func (d *Driver) waitForAction(a *hcloud.Action) error {
870917
}
871918
return nil
872919
}
920+
921+
func (d *Driver) labelName(name string) string {
922+
return labelNamespace + "/" + name
923+
}
924+
925+
func (d *Driver) getAutoPlacementGroup() (*hcloud.PlacementGroup, error) {
926+
res, err := d.getClient().PlacementGroup.AllWithOpts(context.Background(), hcloud.PlacementGroupListOpts{
927+
ListOpts: hcloud.ListOpts{LabelSelector: d.labelName(labelAutoSpreadPg)},
928+
})
929+
930+
if err != nil {
931+
return nil, err
932+
}
933+
934+
if len(res) != 0 {
935+
return res[0], nil
936+
}
937+
938+
grp, err := d.makePlacementGroup("Docker-Machine auto spread", map[string]string{
939+
d.labelName(labelAutoSpreadPg): "true",
940+
d.labelName(labelAutoCreated): "true",
941+
})
942+
943+
return grp, err
944+
}
945+
946+
func (d *Driver) makePlacementGroup(name string, labels map[string]string) (*hcloud.PlacementGroup, error) {
947+
grp, _, err := d.getClient().PlacementGroup.Create(context.Background(), hcloud.PlacementGroupCreateOpts{
948+
Name: name,
949+
Labels: labels,
950+
Type: "spread",
951+
})
952+
953+
if grp.PlacementGroup != nil {
954+
d.dangling = append(d.dangling, func() {
955+
_, err := d.getClient().PlacementGroup.Delete(context.Background(), grp.PlacementGroup)
956+
if err != nil {
957+
log.Errorf("could not delete placement group: %v", err)
958+
}
959+
})
960+
}
961+
962+
if err != nil {
963+
return nil, fmt.Errorf("could not create placement group: %w", err)
964+
}
965+
966+
return grp.PlacementGroup, nil
967+
}
968+
969+
func (d *Driver) getPlacementGroup() (*hcloud.PlacementGroup, error) {
970+
if d.placementGroup == "" {
971+
return nil, nil
972+
} else if d.cachedPGrp != nil {
973+
return d.cachedPGrp, nil
974+
}
975+
976+
name := d.placementGroup
977+
if name == autoSpreadPgName {
978+
grp, err := d.getAutoPlacementGroup()
979+
d.cachedPGrp = grp
980+
return grp, err
981+
} else {
982+
client := d.getClient().PlacementGroup
983+
grp, _, err := client.Get(context.Background(), name)
984+
if err != nil {
985+
return nil, fmt.Errorf("could not get placement group: %w", err)
986+
}
987+
988+
if grp != nil {
989+
return grp, nil
990+
}
991+
992+
return d.makePlacementGroup(name, map[string]string{d.labelName(labelAutoCreated): "true"})
993+
}
994+
}
995+
996+
func (d *Driver) removeEmptyServerPlacementGroup(srv *hcloud.Server) error {
997+
pg := srv.PlacementGroup
998+
if pg == nil {
999+
return nil
1000+
}
1001+
1002+
if len(pg.Servers) > 1 {
1003+
log.Debugf("more than 1 servers in group, ignoring %v", pg)
1004+
return nil
1005+
}
1006+
1007+
if auto, exists := pg.Labels[d.labelName(labelAutoCreated)]; exists && auto == "true" {
1008+
_, err := d.getClient().PlacementGroup.Delete(context.Background(), pg)
1009+
if err != nil {
1010+
return fmt.Errorf("could not remove placement group: %w", err)
1011+
}
1012+
return nil
1013+
} else {
1014+
log.Debugf("group not auto-created, ignoring: %v", pg)
1015+
return nil
1016+
}
1017+
}

go.mod

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,8 @@ require (
66
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect
77
github.com/docker/docker v0.0.0-20181018193557-f7e5154f37a4 // indirect
88
github.com/docker/machine v0.16.2
9-
github.com/hetznercloud/hcloud-go v1.24.0
10-
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
11-
github.com/pkg/errors v0.8.1
12-
github.com/sirupsen/logrus v1.4.2 // indirect
13-
github.com/stretchr/testify v1.3.0 // indirect
14-
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4
15-
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb // indirect
9+
github.com/hetznercloud/hcloud-go v1.32.0
10+
github.com/pkg/errors v0.9.1
11+
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
1612
gotest.tools v2.2.0+incompatible // indirect
1713
)

0 commit comments

Comments
 (0)