Skip to content

Commit c1352da

Browse files
authored
Merge pull request #8297 from pat-s/feat/imagesForArch-nodepool
Hetzner(feat): add option to set nodepool-specific image IDs
2 parents 172a22c + 008a3b9 commit c1352da

File tree

4 files changed

+200
-10
lines changed

4 files changed

+200
-10
lines changed

cluster-autoscaler/cloudprovider/hetzner/README.md

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,16 @@ The cluster autoscaler for Hetzner Cloud scales worker nodes.
1010

1111
`HCLOUD_IMAGE` Defaults to `ubuntu-20.04`, @see https://docs.hetzner.cloud/#images. You can also use an image ID here (e.g. `15512617`), or a label selector associated with a custom snapshot (e.g. `customized_ubuntu=true`). The most recent snapshot will be used in the latter case.
1212

13-
`HCLOUD_CLUSTER_CONFIG` This is the new format replacing
14-
* `HCLOUD_CLOUD_INIT`
15-
* `HCLOUD_IMAGE`
16-
13+
`HCLOUD_CLUSTER_CONFIG` This is the new format replacing
14+
* `HCLOUD_CLOUD_INIT`
15+
* `HCLOUD_IMAGE`
16+
1717
Base64 encoded JSON according to the following structure
1818

1919
```json
2020
{
2121
"imagesForArch": { // These should be the same format as HCLOUD_IMAGE
22-
"arm64": "",
22+
"arm64": "",
2323
"amd64": ""
2424
},
2525
"nodeConfigs": {
@@ -28,7 +28,7 @@ The cluster autoscaler for Hetzner Cloud scales worker nodes.
2828
"labels": {
2929
"node.kubernetes.io/role": "autoscaler-node"
3030
},
31-
"taints":
31+
"taints":
3232
[
3333
{
3434
"key": "node.kubernetes.io/role",
@@ -47,6 +47,13 @@ Can be useful when you have many different node pools and run into issues of the
4747

4848
**NOTE**: In contrast to `HCLOUD_CLUSTER_CONFIG`, this file is not base64 encoded.
4949

50+
The global `imagesForArch` configuration can be overridden on a per-nodepool basis by adding an `imagesForArch` field to individual nodepool configurations.
51+
52+
The image selection logic works as follows:
53+
54+
1. If a nodepool has its own `imagesForArch` configuration, it will be used for that specific nodepool
55+
1. If a nodepool doesn't have `imagesForArch` configured, the global `imagesForArch` configuration will be used as a fallback
56+
1. If neither is configured, the legacy `HCLOUD_IMAGE` environment variable will be used
5057

5158
`HCLOUD_NETWORK` Default empty , The id or name of the network that is used in the cluster , @see https://docs.hetzner.cloud/#networks
5259

@@ -105,5 +112,5 @@ git add hcloud-go/
105112

106113
## Debugging
107114

108-
To enable debug logging, set the log level of the autoscaler to at least level 5 via cli flag: `--v=5`
109-
The logs will include all requests and responses made towards the Hetzner API including headers and body.
115+
To enable debug logging, set the log level of the autoscaler to at least level 5 via cli flag: `--v=5`
116+
The logs will include all requests and responses made towards the Hetzner API including headers and body.

cluster-autoscaler/cloudprovider/hetzner/hetzner_manager.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ type NodeConfig struct {
7777
PlacementGroup string
7878
Taints []apiv1.Taint
7979
Labels map[string]string
80+
ImagesForArch *ImageList
8081
}
8182

8283
// LegacyConfig holds the configuration in the legacy format

cluster-autoscaler/cloudprovider/hetzner/hetzner_node_group.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -528,12 +528,20 @@ func findImage(n *hetznerNodeGroup, serverType *hcloud.ServerType) (*hcloud.Imag
528528
// Select correct image based on server type architecture
529529
imageName := n.manager.clusterConfig.LegacyConfig.ImageName
530530
if n.manager.clusterConfig.IsUsingNewFormat {
531+
// Check for nodepool-specific images first, then fall back to global images
532+
var imagesForArch *ImageList
533+
if nodeConfig, exists := n.manager.clusterConfig.NodeConfigs[n.id]; exists && nodeConfig.ImagesForArch != nil {
534+
imagesForArch = nodeConfig.ImagesForArch
535+
} else {
536+
imagesForArch = &n.manager.clusterConfig.ImagesForArch
537+
}
538+
531539
if serverType.Architecture == hcloud.ArchitectureARM {
532-
imageName = n.manager.clusterConfig.ImagesForArch.Arm64
540+
imageName = imagesForArch.Arm64
533541
}
534542

535543
if serverType.Architecture == hcloud.ArchitectureX86 {
536-
imageName = n.manager.clusterConfig.ImagesForArch.Amd64
544+
imageName = imagesForArch.Amd64
537545
}
538546
}
539547

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/*
2+
Copyright 2019 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package hetzner
18+
19+
import (
20+
"testing"
21+
22+
"github.com/stretchr/testify/assert"
23+
"github.com/stretchr/testify/require"
24+
)
25+
26+
func TestFindImageWithPerNodepoolConfig(t *testing.T) {
27+
// Test case 1: Nodepool with specific imagesForArch should use those images
28+
t.Run("nodepool with specific imagesForArch", func(t *testing.T) {
29+
manager := &hetznerManager{
30+
clusterConfig: &ClusterConfig{
31+
IsUsingNewFormat: true,
32+
ImagesForArch: ImageList{
33+
Arm64: "global-arm64-image",
34+
Amd64: "global-amd64-image",
35+
},
36+
NodeConfigs: map[string]*NodeConfig{
37+
"pool1": {
38+
ImagesForArch: &ImageList{
39+
Arm64: "pool1-arm64-image",
40+
Amd64: "pool1-amd64-image",
41+
},
42+
},
43+
},
44+
},
45+
}
46+
47+
nodeGroup := &hetznerNodeGroup{
48+
id: "pool1",
49+
manager: manager,
50+
}
51+
52+
// This would normally call the actual API, but we're just testing the logic
53+
// The actual image selection logic is in findImage function
54+
// For this test, we'll verify the configuration is set up correctly
55+
nodeConfig, exists := manager.clusterConfig.NodeConfigs[nodeGroup.id]
56+
require.True(t, exists)
57+
require.NotNil(t, nodeConfig.ImagesForArch)
58+
assert.Equal(t, "pool1-arm64-image", nodeConfig.ImagesForArch.Arm64)
59+
assert.Equal(t, "pool1-amd64-image", nodeConfig.ImagesForArch.Amd64)
60+
})
61+
62+
// Test case 2: Nodepool without specific imagesForArch should fall back to global
63+
t.Run("nodepool without specific imagesForArch", func(t *testing.T) {
64+
manager := &hetznerManager{
65+
clusterConfig: &ClusterConfig{
66+
IsUsingNewFormat: true,
67+
ImagesForArch: ImageList{
68+
Arm64: "global-arm64-image",
69+
Amd64: "global-amd64-image",
70+
},
71+
NodeConfigs: map[string]*NodeConfig{
72+
"pool2": {
73+
// No ImagesForArch specified
74+
},
75+
},
76+
},
77+
}
78+
79+
nodeGroup := &hetznerNodeGroup{
80+
id: "pool2",
81+
manager: manager,
82+
}
83+
84+
nodeConfig, exists := manager.clusterConfig.NodeConfigs[nodeGroup.id]
85+
require.True(t, exists)
86+
assert.Nil(t, nodeConfig.ImagesForArch)
87+
assert.Equal(t, "global-arm64-image", manager.clusterConfig.ImagesForArch.Arm64)
88+
assert.Equal(t, "global-amd64-image", manager.clusterConfig.ImagesForArch.Amd64)
89+
})
90+
91+
// Test case 3: Nodepool with nil ImagesForArch should fall back to global
92+
t.Run("nodepool with nil imagesForArch", func(t *testing.T) {
93+
manager := &hetznerManager{
94+
clusterConfig: &ClusterConfig{
95+
IsUsingNewFormat: true,
96+
ImagesForArch: ImageList{
97+
Arm64: "global-arm64-image",
98+
Amd64: "global-amd64-image",
99+
},
100+
NodeConfigs: map[string]*NodeConfig{
101+
"pool3": {
102+
ImagesForArch: nil, // Explicitly nil
103+
},
104+
},
105+
},
106+
}
107+
108+
nodeGroup := &hetznerNodeGroup{
109+
id: "pool3",
110+
manager: manager,
111+
}
112+
113+
nodeConfig, exists := manager.clusterConfig.NodeConfigs[nodeGroup.id]
114+
require.True(t, exists)
115+
assert.Nil(t, nodeConfig.ImagesForArch)
116+
assert.Equal(t, "global-arm64-image", manager.clusterConfig.ImagesForArch.Arm64)
117+
assert.Equal(t, "global-amd64-image", manager.clusterConfig.ImagesForArch.Amd64)
118+
})
119+
}
120+
121+
func TestImageSelectionLogic(t *testing.T) {
122+
// Test the image selection logic that would be used in findImage function
123+
t.Run("image selection logic", func(t *testing.T) {
124+
manager := &hetznerManager{
125+
clusterConfig: &ClusterConfig{
126+
IsUsingNewFormat: true,
127+
ImagesForArch: ImageList{
128+
Arm64: "global-arm64-image",
129+
Amd64: "global-amd64-image",
130+
},
131+
NodeConfigs: map[string]*NodeConfig{
132+
"pool1": {
133+
ImagesForArch: &ImageList{
134+
Arm64: "pool1-arm64-image",
135+
Amd64: "pool1-amd64-image",
136+
},
137+
},
138+
"pool2": {
139+
// No ImagesForArch specified
140+
},
141+
},
142+
},
143+
}
144+
145+
// Test pool1 (has specific imagesForArch)
146+
nodeConfig, exists := manager.clusterConfig.NodeConfigs["pool1"]
147+
require.True(t, exists)
148+
require.NotNil(t, nodeConfig.ImagesForArch)
149+
150+
var imagesForArch *ImageList
151+
if nodeConfig.ImagesForArch != nil {
152+
imagesForArch = nodeConfig.ImagesForArch
153+
} else {
154+
imagesForArch = &manager.clusterConfig.ImagesForArch
155+
}
156+
157+
assert.Equal(t, "pool1-arm64-image", imagesForArch.Arm64)
158+
assert.Equal(t, "pool1-amd64-image", imagesForArch.Amd64)
159+
160+
// Test pool2 (no specific imagesForArch, should use global)
161+
nodeConfig, exists = manager.clusterConfig.NodeConfigs["pool2"]
162+
require.True(t, exists)
163+
assert.Nil(t, nodeConfig.ImagesForArch)
164+
165+
if nodeConfig.ImagesForArch != nil {
166+
imagesForArch = nodeConfig.ImagesForArch
167+
} else {
168+
imagesForArch = &manager.clusterConfig.ImagesForArch
169+
}
170+
171+
assert.Equal(t, "global-arm64-image", imagesForArch.Arm64)
172+
assert.Equal(t, "global-amd64-image", imagesForArch.Amd64)
173+
})
174+
}

0 commit comments

Comments
 (0)