Skip to content

Commit 7feded3

Browse files
authored
Merge pull request #8570 from tloesch/feature/cluster-autoscaler-hetzner-ip-range-config
feat(hetzner): add IP range configuration for private network
2 parents e031a9a + b6b4d19 commit 7feded3

File tree

4 files changed

+89
-7
lines changed

4 files changed

+89
-7
lines changed

cluster-autoscaler/cloudprovider/hetzner/README.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ The cluster autoscaler for Hetzner Cloud scales worker nodes.
2222
"arm64": "",
2323
"amd64": ""
2424
},
25+
"defaultSubnetIPRange": "10.0.0.0/16", // Optional, if not set the hetzner cloud default will be used - make sure this subnet exists within you private network and to use the cidr notation
2526
"nodeConfigs": {
2627
"pool1": { // This equals the pool name. Required for each pool that you have
2728
"cloudInit": "", // HCLOUD_CLOUD_INIT make sure it isn't base64 encoded twice ;]
@@ -35,7 +36,8 @@ The cluster autoscaler for Hetzner Cloud scales worker nodes.
3536
"value": "autoscaler-node",
3637
"effect": "NoExecute"
3738
}
38-
]
39+
],
40+
"subnetIPRange": "10.0.0.0/24" // Optional, if not set the defaultSubnetIPRange will be used - make sure this subnet exists within you private network and to use the cidr notation
3941
}
4042
}
4143
}
@@ -55,6 +57,15 @@ The image selection logic works as follows:
5557
1. If a nodepool doesn't have `imagesForArch` configured, the global `imagesForArch` configuration will be used as a fallback
5658
1. If neither is configured, the legacy `HCLOUD_IMAGE` environment variable will be used
5759

60+
61+
The `defaultSubnetIPRange` and `subnetIPRange` configuration can be used to place nodes within a specific IP range.
62+
This only applies to private networks. Make sure that the subnet exists within your private network.
63+
If you do not set this value, the default setting from Hetzner Cloud will be used.
64+
65+
The global `defaultSubnetIPRange` can be overridden on a per-nodepool basis by adding a `subnetIPRange` field to individual nodepool configurations.
66+
67+
68+
5869
`HCLOUD_NETWORK` Default empty , The id or name of the network that is used in the cluster , @see https://docs.hetzner.cloud/#networks
5970

6071
`HCLOUD_FIREWALL` Default empty , The id or name of the firewall that is used in the cluster , @see https://docs.hetzner.cloud/#firewalls

cluster-autoscaler/cloudprovider/hetzner/hetzner_cloud_provider.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ import (
2020
"context"
2121
"errors"
2222
"fmt"
23+
"net"
2324
"regexp"
25+
"slices"
2426
"strconv"
2527
"strings"
2628
"sync"
@@ -198,6 +200,17 @@ func BuildHetzner(_ config.AutoscalingOptions, do cloudprovider.NodeGroupDiscove
198200
klog.Fatalf("No cluster config present provider: %v", err)
199201
}
200202

203+
var defaultSubnetIPRange *net.IPNet
204+
if manager.clusterConfig.IsUsingNewFormat && manager.network != nil && manager.clusterConfig.DefaultSubnetIPRange != "" {
205+
_, defaultSubnetIPRange, err = net.ParseCIDR(manager.clusterConfig.DefaultSubnetIPRange)
206+
if err != nil {
207+
klog.Fatalf("failed to parse default subnet ip range %s: %s", manager.clusterConfig.DefaultSubnetIPRange, err)
208+
}
209+
if !isIpRangeInNetwork(defaultSubnetIPRange, manager.network) {
210+
klog.Fatalf("default subnet ip range %s is not part of network %s", manager.clusterConfig.DefaultSubnetIPRange, manager.network.Name)
211+
}
212+
}
213+
201214
validNodePoolName := regexp.MustCompile(`^[a-z0-9A-Z]+[a-z0-9A-Z\-\.\_]*[a-z0-9A-Z]+$|^[a-z0-9A-Z]{1}$`)
202215
clusterUpdateLock := sync.Mutex{}
203216
placementGroupTotals := make(map[string]int)
@@ -214,6 +227,7 @@ func BuildHetzner(_ config.AutoscalingOptions, do cloudprovider.NodeGroupDiscove
214227
}
215228

216229
var placementGroup *hcloud.PlacementGroup
230+
var subnetIPRange *net.IPNet
217231
if manager.clusterConfig.IsUsingNewFormat {
218232
_, ok := manager.clusterConfig.NodeConfigs[spec.name]
219233
if !ok {
@@ -232,6 +246,20 @@ func BuildHetzner(_ config.AutoscalingOptions, do cloudprovider.NodeGroupDiscove
232246
}
233247
placementGroupTotals[placementGroup.Name] += spec.maxSize
234248
}
249+
if manager.network != nil {
250+
if manager.clusterConfig.NodeConfigs[spec.name].SubnetIPRange != "" {
251+
_, subnetIPRange, err = net.ParseCIDR(manager.clusterConfig.NodeConfigs[spec.name].SubnetIPRange)
252+
if err != nil {
253+
klog.Fatalf("failed to parse subnet ip range %s for node group %s: %s", manager.clusterConfig.NodeConfigs[spec.name].SubnetIPRange, spec.name, err)
254+
}
255+
if !isIpRangeInNetwork(subnetIPRange, manager.network) {
256+
klog.Fatalf("subnet ip range %s for node group %s is not part of network %s", manager.clusterConfig.NodeConfigs[spec.name].SubnetIPRange, spec.name, manager.network.Name)
257+
}
258+
} else {
259+
subnetIPRange = defaultSubnetIPRange
260+
}
261+
}
262+
235263
}
236264

237265
manager.nodeGroups[spec.name] = &hetznerNodeGroup{
@@ -244,6 +272,7 @@ func BuildHetzner(_ config.AutoscalingOptions, do cloudprovider.NodeGroupDiscove
244272
targetSize: len(servers),
245273
clusterUpdateMutex: &clusterUpdateLock,
246274
placementGroup: placementGroup,
275+
subnetIPRange: subnetIPRange,
247276
}
248277
}
249278

@@ -262,6 +291,15 @@ func BuildHetzner(_ config.AutoscalingOptions, do cloudprovider.NodeGroupDiscove
262291
return provider
263292
}
264293

294+
func isIpRangeInNetwork(ipRange *net.IPNet, network *hcloud.Network) bool {
295+
return slices.ContainsFunc(network.Subnets, func(subnet hcloud.NetworkSubnet) bool {
296+
if ipRange == nil || subnet.IPRange == nil {
297+
return false
298+
}
299+
return subnet.IPRange.IP.Equal(ipRange.IP) && len(subnet.IPRange.Mask) == len(ipRange.Mask)
300+
})
301+
}
302+
265303
func getPlacementGroup(manager *hetznerManager, placementGroupRef string) (*hcloud.PlacementGroup, error) {
266304
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
267305
defer cancel()

cluster-autoscaler/cloudprovider/hetzner/hetzner_manager.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,11 @@ type hetznerManager struct {
5959

6060
// ClusterConfig holds the configuration for all the nodepools
6161
type ClusterConfig struct {
62-
ImagesForArch ImageList
63-
NodeConfigs map[string]*NodeConfig
64-
IsUsingNewFormat bool
65-
LegacyConfig LegacyConfig
62+
ImagesForArch ImageList
63+
NodeConfigs map[string]*NodeConfig
64+
IsUsingNewFormat bool
65+
LegacyConfig LegacyConfig
66+
DefaultSubnetIPRange string
6667
}
6768

6869
// ImageList holds the image id/names for the different architectures
@@ -78,6 +79,7 @@ type NodeConfig struct {
7879
Taints []apiv1.Taint
7980
Labels map[string]string
8081
ImagesForArch *ImageList
82+
SubnetIPRange string
8183
}
8284

8385
// LegacyConfig holds the configuration in the legacy format

cluster-autoscaler/cloudprovider/hetzner/hetzner_node_group.go

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"fmt"
2323
"maps"
2424
"math/rand"
25+
"net"
2526
"strings"
2627
"sync"
2728

@@ -49,6 +50,7 @@ type hetznerNodeGroup struct {
4950

5051
clusterUpdateMutex *sync.Mutex
5152
placementGroup *hcloud.PlacementGroup
53+
subnetIPRange *net.IPNet
5254
}
5355

5456
type hetznerNodeGroupSpec struct {
@@ -472,7 +474,8 @@ func createServer(n *hetznerNodeGroup) error {
472474
cloudInit = n.manager.clusterConfig.NodeConfigs[n.id].CloudInit
473475
}
474476

475-
StartAfterCreate := true
477+
// dont start the server if we need to attach the server to a private subnet network
478+
StartAfterCreate := n.manager.network != nil && n.subnetIPRange == nil
476479
opts := hcloud.ServerCreateOpts{
477480
Name: newNodeName(n),
478481
UserData: cloudInit,
@@ -492,7 +495,7 @@ func createServer(n *hetznerNodeGroup) error {
492495
if n.manager.sshKey != nil {
493496
opts.SSHKeys = []*hcloud.SSHKey{n.manager.sshKey}
494497
}
495-
if n.manager.network != nil {
498+
if n.manager.network != nil && n.subnetIPRange == nil {
496499
opts.Networks = []*hcloud.Network{n.manager.network}
497500
}
498501
if n.manager.firewall != nil {
@@ -516,6 +519,34 @@ func createServer(n *hetznerNodeGroup) error {
516519
return fmt.Errorf("failed to start server %s error: %v", server.Name, err)
517520
}
518521

522+
if n.manager.network != nil && n.subnetIPRange != nil {
523+
// Attach server to private network with subnetIPRange
524+
attachAction, _, err := n.manager.client.Server.AttachToNetwork(ctx, server, hcloud.ServerAttachToNetworkOpts{
525+
Network: n.manager.network,
526+
IPRange: n.subnetIPRange,
527+
})
528+
if err != nil {
529+
_ = n.manager.deleteServer(server)
530+
return fmt.Errorf("failed to attach server %s to network %s with IP range %s error: %v", server.Name, n.manager.network.Name, n.subnetIPRange.String(), err)
531+
}
532+
if err = n.manager.client.Action.WaitFor(ctx, attachAction); err != nil {
533+
_ = n.manager.deleteServer(server)
534+
return fmt.Errorf("failed waiting for network action for server %s error: %v", server.Name, err)
535+
}
536+
}
537+
538+
if !StartAfterCreate {
539+
powerOnAction, _, err := n.manager.client.Server.Poweron(ctx, server)
540+
if err != nil {
541+
_ = n.manager.deleteServer(server)
542+
return fmt.Errorf("failed to power on server %s error: %v", server.Name, err)
543+
}
544+
if err = n.manager.client.Action.WaitFor(ctx, powerOnAction); err != nil {
545+
_ = n.manager.deleteServer(server)
546+
return fmt.Errorf("failed waiting for power on action for server %s error: %v", server.Name, err)
547+
}
548+
}
549+
519550
return nil
520551
}
521552

0 commit comments

Comments
 (0)