Skip to content

Commit 24f4902

Browse files
feat: add placement groups
- add placement groups using --hetzner-placement-group - auto-created pgs will be deleted with the last server within them - added --hetzner-auto-spread for a driver-managed placement group Closes: #72
1 parent 57a0e93 commit 24f4902

File tree

3 files changed

+294
-42
lines changed

3 files changed

+294
-42
lines changed

driver.go

Lines changed: 158 additions & 14 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,6 +656,11 @@ 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+
err = d.removeEmptyServerPlacementGroup(srv)
661+
if err != nil {
662+
log.Error(err) // not a hard failure
663+
}
618664
}
619665
}
620666

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

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)