Skip to content

Commit ee9e720

Browse files
authored
🌱 Support hrobot:// ProviderID of upstream ccm. (#72)
1 parent ebfea76 commit ee9e720

8 files changed

Lines changed: 266 additions & 64 deletions

File tree

README.md

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,14 @@ The Hetzner Cloud controller manager seamlessly integrates your Kubernetes clust
99
1010
## About the Fork
1111

12-
In the long run, we (Syself) would like to switch to the [upstream ccm](https://github.com/syself/hetzner-cloud-controller-manager/) again.
12+
We plan to support the [upstream hcloud
13+
ccm](https://github.com/hetznercloud/hcloud-cloud-controller-manager/) in
14+
[CAPH](https://github.com/syself/cluster-api-provider-hetzner/). After that, this fork is no longer
15+
needed.
16+
17+
## About the Fork (old)
18+
19+
In the long run, we (Syself) would like to switch to the [upstream ccm](https://github.com/hetznercloud/hcloud-cloud-controller-manager/) again.
1320

1421
A lot of changes were made in the upstream fork, and we don't plan to merge them into our fork.
1522

@@ -62,10 +69,16 @@ See [CAPH docs](https://syself.com/docs/caph/topics/baremetal/creating-workload-
6269

6370
## Usage
6471

65-
We recommend to mount the secret `hetzner` as volume and make it available for the container as `/etc/hetzner-secret`.
66-
Then the credentials are automatically reloaded, when the secret changes.
67-
When you use hot-reloading, the secret keys must be named `hcloud` (or `token`, for upstream hcloud-ccm compatibility), `robot-user`, and `robot-password`.
68-
You see an example in the [ccm helm chart](https://github.com/syself/charts/tree/main/charts/ccm-hetzner)
72+
We recommend to mount the secret `hetzner` as volume and make it available for the container as
73+
`/etc/hetzner-secret`. Then the credentials are automatically reloaded, when the secret changes.
74+
75+
When you use hot-reloading, the secret keys must be named `hcloud` (or `token`, for upstream
76+
hcloud-ccm compatibility), `robot-user`, and `robot-password`. You see an example in the [ccm helm
77+
chart](https://github.com/syself/charts/tree/main/charts/ccm-hetzner)
78+
79+
For bare-metal nodes without an existing ProviderID, you can switch to `hrobot://<id>` via
80+
`--use-hrobot-provider-id-for-baremetal` (or
81+
`HCLOUD_USE_HROBOT_PROVIDER_ID_FOR_BAREMETAL=true`).
6982

7083
## Env Variables
7184

@@ -75,6 +88,9 @@ CACHE_TIMEOUT: Timeout of the Robot API Cache. See [ParseDuration](https://pkg.g
7588

7689
HCLOUD_ENDPOINT: Defaults to `https://api.hetzner.cloud/v1`
7790

91+
HCLOUD_USE_HROBOT_PROVIDER_ID_FOR_BAREMETAL: When set to `true`, newly initialized bare-metal
92+
nodes use `hrobot://<id>` instead of `hcloud://bm-<id>`.
93+
7894
Additional Env Variables are defined at the top of [cloud.go](https://github.com/syself/hetzner-cloud-controller-manager/blob/master/hcloud/cloud.go)
7995

8096
Deprecated (use mounted secret instead):

hcloud/cloud.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ const (
6565
hcloudLoadBalancersUsePrivateIP = "HCLOUD_LOAD_BALANCERS_USE_PRIVATE_IP"
6666
hcloudLoadBalancersDisableIPv6 = "HCLOUD_LOAD_BALANCERS_DISABLE_IPV6"
6767
hcloudMetricsEnabledENVVar = "HCLOUD_METRICS_ENABLED"
68+
UseHrobotProviderIDForBaremetalEnvVar = "HCLOUD_USE_HROBOT_PROVIDER_ID_FOR_BAREMETAL"
6869
hcloudMetricsAddress = ":8233"
6970
providerName = "hcloud"
7071
hostNamePrefixRobot = "bm-"
@@ -247,6 +248,10 @@ func newCloud(_ io.Reader) (cloudprovider.Interface, error) {
247248
if err != nil {
248249
return nil, fmt.Errorf("%s: %w", op, err)
249250
}
251+
useHrobotProviderIDForBaremetal, err := getEnvBool(UseHrobotProviderIDForBaremetalEnvVar)
252+
if err != nil {
253+
return nil, fmt.Errorf("%s: %w", op, err)
254+
}
250255

251256
credentialsDir := credentials.GetDirectory(rootDir)
252257
_, err = os.Stat(credentialsDir)
@@ -261,7 +266,7 @@ func newCloud(_ io.Reader) (cloudprovider.Interface, error) {
261266
return &cloud{
262267
hcloudClient: hcloudClient,
263268
robotClient: robotClient,
264-
instances: newInstances(hcloudClient, robotClient, instancesAddressFamily, networkID),
269+
instances: newInstances(hcloudClient, robotClient, instancesAddressFamily, networkID, useHrobotProviderIDForBaremetal),
265270
loadBalancer: loadBalancers,
266271
routes: nil,
267272
networkID: networkID,

hcloud/cloud_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,7 @@ func Test_updateHcloudCredentials(t *testing.T) {
436436
},
437437
})
438438
})
439-
i := newInstances(hcloudClient, nil, AddressFamilyIPv4, 0)
439+
i := newInstances(hcloudClient, nil, AddressFamilyIPv4, 0, false)
440440

441441
node := &corev1.Node{
442442
Spec: corev1.NodeSpec{ProviderID: "hcloud://1"},

hcloud/instances.go

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,16 +40,29 @@ const (
4040
)
4141

4242
type instances struct {
43-
client *hcloud.Client
44-
robotClient robotclient.Client
45-
addressFamily addressFamily
46-
networkID int64
43+
client *hcloud.Client
44+
robotClient robotclient.Client
45+
addressFamily addressFamily
46+
networkID int64
47+
useHrobotProviderID bool
4748
}
4849

4950
var errServerNotFound = fmt.Errorf("server not found")
5051

51-
func newInstances(client *hcloud.Client, robotClient robotclient.Client, addressFamily addressFamily, networkID int64) *instances {
52-
return &instances{client, robotClient, addressFamily, networkID}
52+
func newInstances(
53+
client *hcloud.Client,
54+
robotClient robotclient.Client,
55+
addressFamily addressFamily,
56+
networkID int64,
57+
useHrobotProviderID bool,
58+
) *instances {
59+
return &instances{
60+
client: client,
61+
robotClient: robotClient,
62+
addressFamily: addressFamily,
63+
networkID: networkID,
64+
useHrobotProviderID: useHrobotProviderID,
65+
}
5366
}
5467

5568
// lookupServer attempts to locate the corresponding hcloud.Server or models.Server (robot server) for a given v1.Node.
@@ -161,8 +174,13 @@ func (i *instances) InstanceMetadata(ctx context.Context, node *corev1.Node) (me
161174
return nil, fmt.Errorf("failed to get instance metadata: no matching bare metal server found for node '%s': %w",
162175
node.Name, errServerNotFound)
163176
}
177+
providerID := providerid.GetBaremetalProviderID(node, bmServer.ServerNumber, i.useHrobotProviderID)
164178
return &cloudprovider.InstanceMetadata{
165-
ProviderID: providerid.LegacyFromRobotServerNumber(bmServer.ServerNumber),
179+
// By default this returns the legacy format "hcloud://bm-NNNN". When the
180+
// --use-hrobot-provider-id-for-baremetal flag is set, it returns "hrobot://NNNN".
181+
// We will use the upstream hcloud-ccm in the future. Related PR for caph:
182+
// https://github.com/syself/cluster-api-provider-hetzner/pull/1703
183+
ProviderID: providerID,
166184
InstanceType: getInstanceTypeOfRobotServer(bmServer),
167185
NodeAddresses: robotNodeAddresses(i.addressFamily, bmServer),
168186
Zone: getZoneOfRobotServer(bmServer),

hcloud/instances_test.go

Lines changed: 113 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ func TestInstances_InstanceExists(t *testing.T) {
8989
})
9090
})
9191

92-
instances := newInstances(env.Client, env.RobotClient, AddressFamilyIPv4, 0)
92+
instances := newInstances(env.Client, env.RobotClient, AddressFamilyIPv4, 0, false)
9393

9494
tests := []struct {
9595
name string
@@ -111,6 +111,15 @@ func TestInstances_InstanceExists(t *testing.T) {
111111
Spec: corev1.NodeSpec{ProviderID: "hcloud://bm-321"},
112112
},
113113
expected: true,
114+
}, {
115+
name: "existing robot server by id (hrobot)",
116+
node: &corev1.Node{
117+
ObjectMeta: metav1.ObjectMeta{
118+
Name: "bm-server1",
119+
},
120+
Spec: corev1.NodeSpec{ProviderID: "hrobot://321"},
121+
},
122+
expected: true,
114123
}, {
115124
name: "missing server by id",
116125
node: &corev1.Node{
@@ -126,6 +135,15 @@ func TestInstances_InstanceExists(t *testing.T) {
126135
Spec: corev1.NodeSpec{ProviderID: "hcloud://bm-322"},
127136
},
128137
expected: false,
138+
}, {
139+
name: "missing robot server by id (hrobot)",
140+
node: &corev1.Node{
141+
ObjectMeta: metav1.ObjectMeta{
142+
Name: "bm-server2",
143+
},
144+
Spec: corev1.NodeSpec{ProviderID: "hrobot://322"},
145+
},
146+
expected: false,
129147
}, {
130148
name: "existing server by name",
131149
node: &corev1.Node{
@@ -206,7 +224,7 @@ func TestInstances_InstanceExistsRobotServerCreatedAfterCacheFill(t *testing.T)
206224
t.Fatalf("Unexpected error creating cached robot client: %v", err)
207225
}
208226

209-
instances := newInstances(env.Client, robotClient, AddressFamilyIPv4, 0)
227+
instances := newInstances(env.Client, robotClient, AddressFamilyIPv4, 0, false)
210228

211229
// Warm the cache while bm-new does not exist yet.
212230
exists, err := instances.InstanceExists(context.TODO(), &corev1.Node{
@@ -278,7 +296,7 @@ func TestInstances_InstanceExistsRobotServerRepeatedMissingNameSkipsSecondForceR
278296
t.Fatalf("Unexpected error creating cached robot client: %v", err)
279297
}
280298

281-
instances := newInstances(env.Client, robotClient, AddressFamilyIPv4, 0)
299+
instances := newInstances(env.Client, robotClient, AddressFamilyIPv4, 0, false)
282300
node := &corev1.Node{
283301
ObjectMeta: metav1.ObjectMeta{Name: "bm-missing"},
284302
}
@@ -348,7 +366,7 @@ func TestInstances_InstanceShutdown(t *testing.T) {
348366
})
349367
})
350368

351-
instances := newInstances(env.Client, env.RobotClient, AddressFamilyIPv4, 0)
369+
instances := newInstances(env.Client, env.RobotClient, AddressFamilyIPv4, 0, false)
352370

353371
tests := []struct {
354372
name string
@@ -376,6 +394,15 @@ func TestInstances_InstanceShutdown(t *testing.T) {
376394
},
377395
},
378396
expected: false,
397+
}, {
398+
name: "bm server (hrobot)",
399+
node: &corev1.Node{
400+
Spec: corev1.NodeSpec{ProviderID: "hrobot://321"},
401+
ObjectMeta: metav1.ObjectMeta{
402+
Name: "bm-server1",
403+
},
404+
},
405+
expected: false,
379406
},
380407
}
381408

@@ -416,7 +443,7 @@ func TestInstances_InstanceMetadata(t *testing.T) {
416443
})
417444
})
418445

419-
instances := newInstances(env.Client, env.RobotClient, AddressFamilyIPv4, 0)
446+
instances := newInstances(env.Client, env.RobotClient, AddressFamilyIPv4, 0, false)
420447

421448
metadata, err := instances.InstanceMetadata(context.TODO(), &corev1.Node{
422449
Spec: corev1.NodeSpec{ProviderID: "hcloud://1"},
@@ -457,7 +484,7 @@ func TestInstances_InstanceMetadataRobotServer(t *testing.T) {
457484
})
458485
})
459486

460-
instances := newInstances(env.Client, env.RobotClient, AddressFamilyIPv4, 0)
487+
instances := newInstances(env.Client, env.RobotClient, AddressFamilyIPv4, 0, false)
461488

462489
metadata, err := instances.InstanceMetadata(context.TODO(), &corev1.Node{
463490
ObjectMeta: metav1.ObjectMeta{
@@ -485,6 +512,86 @@ func TestInstances_InstanceMetadataRobotServer(t *testing.T) {
485512
}
486513
}
487514

515+
func TestInstances_InstanceMetadataRobotServerUsesHrobotProviderIDFlag(t *testing.T) {
516+
env := newTestEnv()
517+
defer env.Teardown()
518+
env.Mux.HandleFunc("/robot/server", func(w http.ResponseWriter, _ *http.Request) {
519+
json.NewEncoder(w).Encode([]models.ServerResponse{
520+
{
521+
Server: models.Server{
522+
ServerIP: "123.123.123.123",
523+
ServerIPv6Net: "2a01:f48:111:4221::",
524+
ServerNumber: 321,
525+
Product: "bm-product 1",
526+
Name: "bm-server1",
527+
Dc: "NBG1-DC1",
528+
},
529+
},
530+
})
531+
})
532+
533+
instances := newInstances(env.Client, env.RobotClient, AddressFamilyIPv4, 0, true)
534+
535+
metadata, err := instances.InstanceMetadata(context.TODO(), &corev1.Node{
536+
ObjectMeta: metav1.ObjectMeta{
537+
Name: "bm-server1",
538+
},
539+
})
540+
if err != nil {
541+
t.Fatalf("Unexpected error: %v", err)
542+
}
543+
544+
expectedMetadata := &cloudprovider.InstanceMetadata{
545+
ProviderID: "hrobot://321",
546+
InstanceType: "bm-product-1",
547+
NodeAddresses: []corev1.NodeAddress{
548+
{Type: corev1.NodeHostName, Address: "bm-server1"},
549+
{Type: corev1.NodeExternalIP, Address: "123.123.123.123"},
550+
},
551+
Zone: "nbg1",
552+
Region: "eu-central",
553+
}
554+
555+
if !reflect.DeepEqual(metadata, expectedMetadata) {
556+
t.Fatalf("Expected metadata %+v but got %+v", *expectedMetadata, *metadata)
557+
}
558+
}
559+
560+
func TestInstances_InstanceMetadataRobotServerKeepsExistingProviderIDWhenFlagIsEnabled(t *testing.T) {
561+
env := newTestEnv()
562+
defer env.Teardown()
563+
env.Mux.HandleFunc("/robot/server/321", func(w http.ResponseWriter, _ *http.Request) {
564+
json.NewEncoder(w).Encode(models.ServerResponse{
565+
Server: models.Server{
566+
ServerIP: "123.123.123.123",
567+
ServerIPv6Net: "2a01:f48:111:4221::",
568+
ServerNumber: 321,
569+
Product: "bm-product 1",
570+
Name: "bm-server1",
571+
Dc: "NBG1-DC1",
572+
},
573+
})
574+
})
575+
576+
instances := newInstances(env.Client, env.RobotClient, AddressFamilyIPv4, 0, true)
577+
578+
metadata, err := instances.InstanceMetadata(context.TODO(), &corev1.Node{
579+
ObjectMeta: metav1.ObjectMeta{
580+
Name: "bm-server1",
581+
},
582+
// Existing nodes must keep their ProviderID even when the new format is
583+
// enabled, otherwise migrations would silently rewrite node identity.
584+
Spec: corev1.NodeSpec{ProviderID: "hcloud://bm-321"},
585+
})
586+
if err != nil {
587+
t.Fatalf("Unexpected error: %v", err)
588+
}
589+
590+
if metadata.ProviderID != "hcloud://bm-321" {
591+
t.Fatalf("expected existing provider id to be kept, got %q", metadata.ProviderID)
592+
}
593+
}
594+
488595
func TestNodeAddresses(t *testing.T) {
489596
tests := []struct {
490597
name string

internal/providerid/providerid.go

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"fmt"
55
"strconv"
66
"strings"
7+
8+
corev1 "k8s.io/api/core/v1"
79
)
810

911
const (
@@ -17,6 +19,8 @@ const (
1719
//
1820
// It MUST not be changed, otherwise existing nodes will not be recognized anymore.
1921
prefixRobotLegacy = "hcloud://bm-"
22+
23+
prefixRobotNew = "hrobot://"
2024
)
2125

2226
type UnkownPrefixError struct {
@@ -25,9 +29,10 @@ type UnkownPrefixError struct {
2529

2630
func (e *UnkownPrefixError) Error() string {
2731
return fmt.Sprintf(
28-
"Provider ID does not have one of the the expected prefixes (%s, %s): %s",
32+
"Provider ID does not have one of the the expected prefixes (%s, %s, %s): %s",
2933
prefixCloud,
3034
prefixRobotLegacy,
35+
prefixRobotNew,
3136
e.ProviderID,
3237
)
3338
}
@@ -40,6 +45,9 @@ func (e *UnkownPrefixError) Error() string {
4045
func ToServerID(providerID string) (id int64, isCloudServer bool, err error) {
4146
idString := ""
4247
switch {
48+
case strings.HasPrefix(providerID, prefixRobotNew):
49+
idString = strings.ReplaceAll(providerID, prefixRobotNew, "")
50+
4351
case strings.HasPrefix(providerID, prefixRobotLegacy):
4452
// This case needs to be before [prefixCloud], as [prefixCloud] is a superset of [prefixRobotLegacy]
4553
idString = strings.ReplaceAll(providerID, prefixRobotLegacy, "")
@@ -68,7 +76,14 @@ func FromCloudServerID(serverID int64) string {
6876
return fmt.Sprintf("%s%d", prefixCloud, serverID)
6977
}
7078

71-
// LegacyFromRobotServerNumber generates the canonical ProviderID for a Robot Server.
72-
func LegacyFromRobotServerNumber(serverNumber int) string {
79+
// GetBaremetalProviderID creates a ProviderID for baremetal servers. Depending on the
80+
// configuration, it is either hcloud://bm-NNNN or hrobot://NNNN.
81+
func GetBaremetalProviderID(node *corev1.Node, serverNumber int, useHrobotProviderID bool) string {
82+
if node.Spec.ProviderID != "" {
83+
return node.Spec.ProviderID
84+
}
85+
if useHrobotProviderID {
86+
return fmt.Sprintf("%s%d", prefixRobotNew, serverNumber)
87+
}
7388
return fmt.Sprintf("%s%d", prefixRobotLegacy, serverNumber)
7489
}

0 commit comments

Comments
 (0)