@@ -36,7 +36,7 @@ type Driver struct {
36
36
cachedKey * hcloud.SSHKey
37
37
IsExistingKey bool
38
38
originalKey string
39
- danglingKeys [] * hcloud. SSHKey
39
+ dangling [] func ()
40
40
ServerID int
41
41
userData string
42
42
volumes []string
@@ -46,6 +46,8 @@ type Driver struct {
46
46
cachedServer * hcloud.Server
47
47
serverLabels map [string ]string
48
48
keyLabels map [string ]string
49
+ placementGroup string
50
+ cachedPGrp * hcloud.PlacementGroup
49
51
50
52
additionalKeys []string
51
53
AdditionalKeyIDs []int
@@ -71,6 +73,14 @@ const (
71
73
flagAdditionalKeys = "hetzner-additional-key"
72
74
flagServerLabel = "hetzner-server-label"
73
75
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"
74
84
)
75
85
76
86
// NewDriver initializes a new driver instance; see [drivers.Driver.NewDriver]
@@ -79,7 +89,6 @@ func NewDriver() *Driver {
79
89
Image : defaultImage ,
80
90
Type : defaultType ,
81
91
IsExistingKey : false ,
82
- danglingKeys : []* hcloud.SSHKey {},
83
92
BaseDriver : & drivers.BaseDriver {
84
93
SSHUser : drivers .DefaultSSHUser ,
85
94
SSHPort : drivers .DefaultSSHPort ,
@@ -183,6 +192,17 @@ func (d *Driver) GetCreateFlags() []mcnflag.Flag {
183
192
Usage : "Key value pairs of additional labels to assign to the SSH key" ,
184
193
Value : []string {},
185
194
},
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
+ },
186
206
}
187
207
}
188
208
@@ -204,6 +224,14 @@ func (d *Driver) SetConfigFromFlags(opts drivers.DriverOptions) error {
204
224
d .firewalls = opts .StringSlice (flagFirewalls )
205
225
d .additionalKeys = opts .StringSlice (flagAdditionalKeys )
206
226
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
+
207
235
err := d .setLabelsFromFlags (opts )
208
236
if err != nil {
209
237
return err
@@ -283,6 +311,10 @@ func (d *Driver) PreCreateCheck() error {
283
311
return errors .Wrap (err , "could not get location" )
284
312
}
285
313
314
+ if _ , err := d .getPlacementGroup (); err != nil {
315
+ return fmt .Errorf ("could not create placement group: %w" , err )
316
+ }
317
+
286
318
if d .UsePrivateNetwork && len (d .networks ) == 0 {
287
319
return errors .Errorf ("No private network attached." )
288
320
}
@@ -297,7 +329,7 @@ func (d *Driver) Create() error {
297
329
return err
298
330
}
299
331
300
- defer d .destroyDanglingKeys ()
332
+ defer d .destroyDangling ()
301
333
err = d .createRemoteKeys ()
302
334
if err != nil {
303
335
return err
@@ -335,7 +367,7 @@ func (d *Driver) Create() error {
335
367
336
368
log .Infof (" -> Server %s[%d] ready. Ip %s" , srv .Server .Name , srv .Server .ID , d .IPAddress )
337
369
// Successful creation, so no keys dangle anymore
338
- d .danglingKeys = nil
370
+ d .dangling = nil
339
371
340
372
return nil
341
373
}
@@ -379,10 +411,16 @@ func (d *Driver) waitForRunningServer() error {
379
411
}
380
412
381
413
func (d * Driver ) makeCreateServerOptions () (* hcloud.ServerCreateOpts , error ) {
414
+ pgrp , err := d .getPlacementGroup ()
415
+ if err != nil {
416
+ return nil , err
417
+ }
418
+
382
419
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 ,
386
424
}
387
425
388
426
networks , err := d .createNetworks ()
@@ -546,16 +584,19 @@ func (d *Driver) makeKey(name string, pubkey string, labels map[string]string) (
546
584
return nil , errors .Errorf ("key upload did not return an error, but key was nil" )
547
585
}
548
586
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
+
550
594
return key , nil
551
595
}
552
596
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 ()
559
600
}
560
601
}
561
602
@@ -615,25 +656,31 @@ func (d *Driver) Remove() error {
615
656
if _ , err := d .getClient ().Server .Delete (context .Background (), srv ); err != nil {
616
657
return errors .Wrap (err , "could not delete server" )
617
658
}
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
+ }
618
664
}
619
665
}
620
666
621
- // Failing to remove these is just a soft error
667
+ // failure to remove a key is not ha hard error
622
668
for i , id := range d .AdditionalKeyIDs {
623
669
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 )
627
673
} else if key == nil {
628
674
log .Warnf (" -> -> %d no longer exists" , id )
629
675
}
630
676
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 )
634
680
}
635
681
}
636
682
683
+ // failure to remove a server-specific key is a hard error
637
684
if ! d .IsExistingKey && d .KeyID != 0 {
638
685
key , err := d .getKey ()
639
686
if err != nil {
@@ -870,3 +917,101 @@ func (d *Driver) waitForAction(a *hcloud.Action) error {
870
917
}
871
918
return nil
872
919
}
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