@@ -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
381413func (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+ }
0 commit comments