@@ -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,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+ }
0 commit comments