Skip to content

Commit

Permalink
Implement support for custom internal IP mapping via HCLOUD_INSTANCES…
Browse files Browse the repository at this point in the history
…_INTERNAL_IP_MAP

This update adds the ability to specify custom internal IP addresses for Hetzner Cloud and Robot Instances through the `HCLOUD_INSTANCES_INTERNAL_IP_MAP` environment variable.
  • Loading branch information
elohmeier committed Jul 9, 2024
1 parent c1bf8f7 commit d72734f
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 33 deletions.
2 changes: 1 addition & 1 deletion hcloud/cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ func (c *cloud) Instances() (cloudprovider.Instances, bool) {
}

func (c *cloud) InstancesV2() (cloudprovider.InstancesV2, bool) {
return newInstances(c.client, c.robotClient, c.recorder, c.cfg.Instance.AddressFamily, c.networkID), true
return newInstances(c.client, c.robotClient, c.recorder, c.cfg.Instance.AddressFamily, c.networkID, c.cfg.Instance.InternalIpMap), true
}

func (c *cloud) Zones() (cloudprovider.Zones, bool) {
Expand Down
38 changes: 28 additions & 10 deletions hcloud/instances.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,16 @@ type instances struct {
recorder record.EventRecorder
addressFamily config.AddressFamily
networkID int64
internalIpMap map[string]string

Check failure on line 42 in hcloud/instances.go

View workflow job for this annotation

GitHub Actions / Lint

var-naming: struct field internalIpMap should be internalIPMap (revive)
}

var (
errServerNotFound = errors.New("server not found")
errMissingRobotClient = errors.New("no robot client configured, make sure to enable Robot support in the configuration")
)

func newInstances(client *hcloud.Client, robotClient robot.Client, recorder record.EventRecorder, addressFamily config.AddressFamily, networkID int64) *instances {
return &instances{client, robotClient, recorder, addressFamily, networkID}
func newInstances(client *hcloud.Client, robotClient robot.Client, recorder record.EventRecorder, addressFamily config.AddressFamily, networkID int64, internalIpMap map[string]string) *instances {

Check failure on line 50 in hcloud/instances.go

View workflow job for this annotation

GitHub Actions / Lint

var-naming: func parameter internalIpMap should be internalIPMap (revive)
return &instances{client, robotClient, recorder, addressFamily, networkID, internalIpMap}
}

// lookupServer attempts to locate the corresponding [*hcloud.Server] or [*hrobotmodels.Server] for a given [*corev1.Node].
Expand Down Expand Up @@ -174,15 +175,15 @@ func (i *instances) InstanceMetadata(ctx context.Context, node *corev1.Node) (*c
op, node.Name, errServerNotFound)
}

metadata, err := server.Metadata(i.addressFamily, i.networkID)
metadata, err := server.Metadata(i.addressFamily, i.networkID, i.internalIpMap)
if err != nil {
return nil, fmt.Errorf("%s: %w", op, err)
}

return metadata, nil
}

func hcloudNodeAddresses(addressFamily config.AddressFamily, networkID int64, server *hcloud.Server) []corev1.NodeAddress {
func hcloudNodeAddresses(addressFamily config.AddressFamily, networkID int64, server *hcloud.Server, internalIpMap map[string]string) []corev1.NodeAddress {

Check failure on line 186 in hcloud/instances.go

View workflow job for this annotation

GitHub Actions / Lint

var-naming: func parameter internalIpMap should be internalIPMap (revive)
var addresses []corev1.NodeAddress
addresses = append(
addresses,
Expand Down Expand Up @@ -222,10 +223,19 @@ func hcloudNodeAddresses(addressFamily config.AddressFamily, networkID int64, se
}
}
}

// Add internal IP from the map if a mapping exists
if internalIP, ok := internalIpMap[server.Name]; ok {
addresses = append(
addresses,
corev1.NodeAddress{Type: corev1.NodeInternalIP, Address: internalIP},
)
}

return addresses
}

func robotNodeAddresses(addressFamily config.AddressFamily, server *hrobotmodels.Server) []corev1.NodeAddress {
func robotNodeAddresses(addressFamily config.AddressFamily, server *hrobotmodels.Server, internalIpMap map[string]string) []corev1.NodeAddress {

Check failure on line 238 in hcloud/instances.go

View workflow job for this annotation

GitHub Actions / Lint

var-naming: func parameter internalIpMap should be internalIPMap (revive)
var addresses []corev1.NodeAddress
addresses = append(
addresses,
Expand All @@ -250,12 +260,20 @@ func robotNodeAddresses(addressFamily config.AddressFamily, server *hrobotmodels
)
}

// Add internal IP from the map if a mapping exists
if internalIP, ok := internalIpMap[server.Name]; ok {
addresses = append(
addresses,
corev1.NodeAddress{Type: corev1.NodeInternalIP, Address: internalIP},
)
}

return addresses
}

type genericServer interface {
IsShutdown() (bool, error)
Metadata(addressFamily config.AddressFamily, networkID int64) (*cloudprovider.InstanceMetadata, error)
Metadata(addressFamily config.AddressFamily, networkID int64, internalIpMap map[string]string) (*cloudprovider.InstanceMetadata, error)

Check failure on line 276 in hcloud/instances.go

View workflow job for this annotation

GitHub Actions / Lint

var-naming: interface method parameter internalIpMap should be internalIPMap (revive)
}

type hcloudServer struct {
Expand All @@ -266,11 +284,11 @@ func (s hcloudServer) IsShutdown() (bool, error) {
return s.Status == hcloud.ServerStatusOff, nil
}

func (s hcloudServer) Metadata(addressFamily config.AddressFamily, networkID int64) (*cloudprovider.InstanceMetadata, error) {
func (s hcloudServer) Metadata(addressFamily config.AddressFamily, networkID int64, internalIpMap map[string]string) (*cloudprovider.InstanceMetadata, error) {

Check failure on line 287 in hcloud/instances.go

View workflow job for this annotation

GitHub Actions / Lint

var-naming: method parameter internalIpMap should be internalIPMap (revive)
return &cloudprovider.InstanceMetadata{
ProviderID: providerid.FromCloudServerID(s.ID),
InstanceType: s.ServerType.Name,
NodeAddresses: hcloudNodeAddresses(addressFamily, networkID, s.Server),
NodeAddresses: hcloudNodeAddresses(addressFamily, networkID, s.Server, internalIpMap),
Zone: s.Datacenter.Name,
Region: s.Datacenter.Location.Name,
}, nil
Expand All @@ -292,11 +310,11 @@ func (s robotServer) IsShutdown() (bool, error) {
return resetStatus.OperatingStatus == "shut off", nil
}

func (s robotServer) Metadata(addressFamily config.AddressFamily, _ int64) (*cloudprovider.InstanceMetadata, error) {
func (s robotServer) Metadata(addressFamily config.AddressFamily, _ int64, internalIpMap map[string]string) (*cloudprovider.InstanceMetadata, error) {

Check failure on line 313 in hcloud/instances.go

View workflow job for this annotation

GitHub Actions / Lint

var-naming: method parameter internalIpMap should be internalIPMap (revive)
return &cloudprovider.InstanceMetadata{
ProviderID: providerid.FromRobotServerNumber(s.ServerNumber),
InstanceType: getInstanceTypeOfRobotServer(s.Server),
NodeAddresses: robotNodeAddresses(addressFamily, s.Server),
NodeAddresses: robotNodeAddresses(addressFamily, s.Server, internalIpMap),
Zone: getZoneOfRobotServer(s.Server),
Region: getRegionOfRobotServer(s.Server),
}, nil
Expand Down
12 changes: 6 additions & 6 deletions hcloud/instances_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ func TestInstances_InstanceExists(t *testing.T) {
})
})

instances := newInstances(env.Client, env.RobotClient, env.Recorder, config.AddressFamilyIPv4, 0)
instances := newInstances(env.Client, env.RobotClient, env.Recorder, config.AddressFamilyIPv4, 0, map[string]string{})

tests := []struct {
name string
Expand Down Expand Up @@ -209,7 +209,7 @@ func TestInstances_InstanceShutdown(t *testing.T) {
})
})

instances := newInstances(env.Client, env.RobotClient, env.Recorder, config.AddressFamilyIPv4, 0)
instances := newInstances(env.Client, env.RobotClient, env.Recorder, config.AddressFamilyIPv4, 0, map[string]string{})
env.Mux.HandleFunc("/robot/server/3", func(w http.ResponseWriter, _ *http.Request) {
json.NewEncoder(w).Encode(hrobotmodels.ServerResponse{
Server: hrobotmodels.Server{
Expand Down Expand Up @@ -343,7 +343,7 @@ func TestInstances_InstanceMetadata(t *testing.T) {
})
})

instances := newInstances(env.Client, env.RobotClient, env.Recorder, config.AddressFamilyIPv4, 0)
instances := newInstances(env.Client, env.RobotClient, env.Recorder, config.AddressFamilyIPv4, 0, map[string]string{})

metadata, err := instances.InstanceMetadata(context.TODO(), &corev1.Node{
Spec: corev1.NodeSpec{ProviderID: "hcloud://1"},
Expand Down Expand Up @@ -384,7 +384,7 @@ func TestInstances_InstanceMetadataRobotServer(t *testing.T) {
})
})

instances := newInstances(env.Client, env.RobotClient, env.Recorder, config.AddressFamilyIPv4, 0)
instances := newInstances(env.Client, env.RobotClient, env.Recorder, config.AddressFamilyIPv4, 0, map[string]string{})

metadata, err := instances.InstanceMetadata(context.TODO(), &corev1.Node{
ObjectMeta: metav1.ObjectMeta{
Expand Down Expand Up @@ -575,7 +575,7 @@ func TestNodeAddresses(t *testing.T) {

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
addresses := hcloudNodeAddresses(test.addressFamily, test.privateNetwork, test.server)
addresses := hcloudNodeAddresses(test.addressFamily, test.privateNetwork, test.server, map[string]string{})

if !reflect.DeepEqual(addresses, test.expected) {
t.Fatalf("Expected addresses %+v but got %+v", test.expected, addresses)
Expand Down Expand Up @@ -636,7 +636,7 @@ func TestNodeAddressesRobotServer(t *testing.T) {

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
addresses := robotNodeAddresses(test.addressFamily, test.server)
addresses := robotNodeAddresses(test.addressFamily, test.server, map[string]string{})

if !reflect.DeepEqual(addresses, test.expected) {
t.Fatalf("%s: expected addresses %+v but got %+v", test.name, test.expected, addresses)
Expand Down
26 changes: 26 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const (
robotRateLimitWaitTime = "ROBOT_RATE_LIMIT_WAIT_TIME"

hcloudInstancesAddressFamily = "HCLOUD_INSTANCES_ADDRESS_FAMILY"
hcloudInstancesInternalIpMap = "HCLOUD_INSTANCES_INTERNAL_IP_MAP"

Check failure on line 25 in internal/config/config.go

View workflow job for this annotation

GitHub Actions / Lint

var-naming: const hcloudInstancesInternalIpMap should be hcloudInstancesInternalIPMap (revive)

// Disable the "master/server is attached to the network" check against the metadata service.
hcloudNetworkDisableAttachedCheck = "HCLOUD_NETWORK_DISABLE_ATTACHED_CHECK"
Expand Down Expand Up @@ -67,6 +68,7 @@ const (

type InstanceConfiguration struct {
AddressFamily AddressFamily
InternalIpMap map[string]string

Check failure on line 71 in internal/config/config.go

View workflow job for this annotation

GitHub Actions / Lint

var-naming: struct field InternalIpMap should be InternalIPMap (revive)
}

type LoadBalancerConfiguration struct {
Expand Down Expand Up @@ -179,6 +181,11 @@ func Read() (HCCMConfiguration, error) {
cfg.Instance.AddressFamily = AddressFamilyIPv4
}

cfg.Instance.InternalIpMap, err = parseInternalIpMap(os.Getenv(hcloudInstancesInternalIpMap))
if err != nil {
errs = append(errs, err)
}

cfg.LoadBalancer.Enabled, err = getEnvBool(hcloudLoadBalancersEnabled, true)
if err != nil {
errs = append(errs, err)
Expand Down Expand Up @@ -288,3 +295,22 @@ func getEnvDuration(key string) (time.Duration, error) {

return b, nil
}

// parseInternalIpMap parses the HCLOUD_INSTANCES_INTERNAL_IP_MAP environment variable into a map.
func parseInternalIpMap(value string) (map[string]string, error) {

Check failure on line 300 in internal/config/config.go

View workflow job for this annotation

GitHub Actions / Lint

var-naming: func parseInternalIpMap should be parseInternalIPMap (revive)
ipMap := make(map[string]string)
if value == "" {
return ipMap, nil
}

pairs := strings.Split(value, ",")
for _, pair := range pairs {
kv := strings.Split(pair, "=")
if len(kv) != 2 {
return nil, fmt.Errorf("invalid format for %s: %s", hcloudInstancesInternalIpMap, pair)
}
ipMap[kv[0]] = kv[1]
}

return ipMap, nil
}
32 changes: 16 additions & 16 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func TestRead(t *testing.T) {
want: HCCMConfiguration{
Robot: RobotConfiguration{CacheTimeout: 5 * time.Minute},
Metrics: MetricsConfiguration{Enabled: true, Address: ":8233"},
Instance: InstanceConfiguration{AddressFamily: AddressFamilyIPv4},
Instance: InstanceConfiguration{AddressFamily: AddressFamilyIPv4, InternalIpMap: map[string]string{}},
LoadBalancer: LoadBalancerConfiguration{Enabled: true},
},
wantErr: nil,
Expand All @@ -40,7 +40,7 @@ func TestRead(t *testing.T) {
HCloudClient: HCloudClientConfiguration{Token: "jr5g7ZHpPptyhJzZyHw2Pqu4g9gTqDvEceYpngPf79jN_NOT_VALID_dzhepnahq"},
Robot: RobotConfiguration{CacheTimeout: 5 * time.Minute},
Metrics: MetricsConfiguration{Enabled: true, Address: ":8233"},
Instance: InstanceConfiguration{AddressFamily: AddressFamilyIPv4},
Instance: InstanceConfiguration{AddressFamily: AddressFamilyIPv4, InternalIpMap: map[string]string{}},
Network: NetworkConfiguration{
NameOrID: "foobar",
},
Expand Down Expand Up @@ -112,7 +112,7 @@ failed to read ROBOT_PASSWORD_FILE: open /tmp/hetzner-password: no such file or
},
Robot: RobotConfiguration{CacheTimeout: 5 * time.Minute},
Metrics: MetricsConfiguration{Enabled: true, Address: ":8233"},
Instance: InstanceConfiguration{AddressFamily: AddressFamilyIPv4},
Instance: InstanceConfiguration{AddressFamily: AddressFamilyIPv4, InternalIpMap: map[string]string{}},
LoadBalancer: LoadBalancerConfiguration{Enabled: true},
},
wantErr: nil,
Expand All @@ -135,7 +135,7 @@ failed to read ROBOT_PASSWORD_FILE: open /tmp/hetzner-password: no such file or
RateLimitWaitTime: 5 * time.Minute,
},
Metrics: MetricsConfiguration{Enabled: true, Address: ":8233"},
Instance: InstanceConfiguration{AddressFamily: AddressFamilyIPv4},
Instance: InstanceConfiguration{AddressFamily: AddressFamilyIPv4, InternalIpMap: map[string]string{}},
LoadBalancer: LoadBalancerConfiguration{Enabled: true},
},
wantErr: nil,
Expand All @@ -148,7 +148,7 @@ failed to read ROBOT_PASSWORD_FILE: open /tmp/hetzner-password: no such file or
want: HCCMConfiguration{
Robot: RobotConfiguration{CacheTimeout: 5 * time.Minute},
Metrics: MetricsConfiguration{Enabled: true, Address: ":8233"},
Instance: InstanceConfiguration{AddressFamily: AddressFamilyIPv6},
Instance: InstanceConfiguration{AddressFamily: AddressFamilyIPv6, InternalIpMap: map[string]string{}},
LoadBalancer: LoadBalancerConfiguration{Enabled: true},
},
wantErr: nil,
Expand All @@ -162,7 +162,7 @@ failed to read ROBOT_PASSWORD_FILE: open /tmp/hetzner-password: no such file or
want: HCCMConfiguration{
Robot: RobotConfiguration{CacheTimeout: 5 * time.Minute},
Metrics: MetricsConfiguration{Enabled: true, Address: ":8233"},
Instance: InstanceConfiguration{AddressFamily: AddressFamilyIPv4},
Instance: InstanceConfiguration{AddressFamily: AddressFamilyIPv4, InternalIpMap: map[string]string{}},
LoadBalancer: LoadBalancerConfiguration{Enabled: true},
Network: NetworkConfiguration{
NameOrID: "foobar",
Expand All @@ -181,7 +181,7 @@ failed to read ROBOT_PASSWORD_FILE: open /tmp/hetzner-password: no such file or
want: HCCMConfiguration{
Robot: RobotConfiguration{CacheTimeout: 5 * time.Minute},
Metrics: MetricsConfiguration{Enabled: true, Address: ":8233"},
Instance: InstanceConfiguration{AddressFamily: AddressFamilyIPv4},
Instance: InstanceConfiguration{AddressFamily: AddressFamilyIPv4, InternalIpMap: map[string]string{}},
LoadBalancer: LoadBalancerConfiguration{Enabled: true},
Network: NetworkConfiguration{
NameOrID: "foobar",
Expand All @@ -203,7 +203,7 @@ failed to read ROBOT_PASSWORD_FILE: open /tmp/hetzner-password: no such file or
want: HCCMConfiguration{
Robot: RobotConfiguration{CacheTimeout: 5 * time.Minute},
Metrics: MetricsConfiguration{Enabled: true, Address: ":8233"},
Instance: InstanceConfiguration{AddressFamily: AddressFamilyIPv4},
Instance: InstanceConfiguration{AddressFamily: AddressFamilyIPv4, InternalIpMap: map[string]string{}},
LoadBalancer: LoadBalancerConfiguration{
Enabled: false,
Location: "nbg1",
Expand Down Expand Up @@ -289,7 +289,7 @@ func TestHCCMConfiguration_Validate(t *testing.T) {
name: "minimal",
fields: fields{
HCloudClient: HCloudClientConfiguration{Token: "jr5g7ZHpPptyhJzZyHw2Pqu4g9gTqDvEceYpngPf79jN_NOT_VALID_dzhepnahq"},
Instance: InstanceConfiguration{AddressFamily: AddressFamilyIPv4},
Instance: InstanceConfiguration{AddressFamily: AddressFamilyIPv4, InternalIpMap: map[string]string{}},
},
wantErr: nil,
},
Expand All @@ -299,7 +299,7 @@ func TestHCCMConfiguration_Validate(t *testing.T) {
fields: fields{
HCloudClient: HCloudClientConfiguration{Token: "jr5g7ZHpPptyhJzZyHw2Pqu4g9gTqDvEceYpngPf79jN_NOT_VALID_dzhepnahq"},
Metrics: MetricsConfiguration{Enabled: true, Address: ":8233"},
Instance: InstanceConfiguration{AddressFamily: AddressFamilyIPv4},
Instance: InstanceConfiguration{AddressFamily: AddressFamilyIPv4, InternalIpMap: map[string]string{}},
Network: NetworkConfiguration{
NameOrID: "foobar",
},
Expand All @@ -313,31 +313,31 @@ func TestHCCMConfiguration_Validate(t *testing.T) {
name: "token missing",
fields: fields{
HCloudClient: HCloudClientConfiguration{Token: ""},
Instance: InstanceConfiguration{AddressFamily: AddressFamilyIPv4},
Instance: InstanceConfiguration{AddressFamily: AddressFamilyIPv4, InternalIpMap: map[string]string{}},
},
wantErr: errors.New("environment variable \"HCLOUD_TOKEN\" is required"),
},
{
name: "token invalid length",
fields: fields{
HCloudClient: HCloudClientConfiguration{Token: "abc"},
Instance: InstanceConfiguration{AddressFamily: AddressFamilyIPv4},
Instance: InstanceConfiguration{AddressFamily: AddressFamilyIPv4, InternalIpMap: map[string]string{}},
},
wantErr: errors.New("entered token is invalid (must be exactly 64 characters long)"),
},
{
name: "address family invalid",
fields: fields{
HCloudClient: HCloudClientConfiguration{Token: "jr5g7ZHpPptyhJzZyHw2Pqu4g9gTqDvEceYpngPf79jN_NOT_VALID_dzhepnahq"},
Instance: InstanceConfiguration{AddressFamily: AddressFamily("foobar")},
Instance: InstanceConfiguration{AddressFamily: AddressFamily("foobar"), InternalIpMap: map[string]string{}},
},
wantErr: errors.New("invalid value for \"HCLOUD_INSTANCES_ADDRESS_FAMILY\", expect one of: ipv4,ipv6,dualstack"),
},
{
name: "LB location and network zone set",
fields: fields{
HCloudClient: HCloudClientConfiguration{Token: "jr5g7ZHpPptyhJzZyHw2Pqu4g9gTqDvEceYpngPf79jN_NOT_VALID_dzhepnahq"},
Instance: InstanceConfiguration{AddressFamily: AddressFamilyIPv4},
Instance: InstanceConfiguration{AddressFamily: AddressFamilyIPv4, InternalIpMap: map[string]string{}},
LoadBalancer: LoadBalancerConfiguration{
Location: "nbg1",
NetworkZone: "eu-central",
Expand All @@ -349,7 +349,7 @@ func TestHCCMConfiguration_Validate(t *testing.T) {
name: "robot enabled but missing credentials",
fields: fields{
HCloudClient: HCloudClientConfiguration{Token: "jr5g7ZHpPptyhJzZyHw2Pqu4g9gTqDvEceYpngPf79jN_NOT_VALID_dzhepnahq"},
Instance: InstanceConfiguration{AddressFamily: AddressFamilyIPv4},
Instance: InstanceConfiguration{AddressFamily: AddressFamilyIPv4, InternalIpMap: map[string]string{}},

Robot: RobotConfiguration{
Enabled: true,
Expand All @@ -363,7 +363,7 @@ environment variable "ROBOT_PASSWORD" is required if Robot support is enabled`),
fields: fields{

HCloudClient: HCloudClientConfiguration{Token: "jr5g7ZHpPptyhJzZyHw2Pqu4g9gTqDvEceYpngPf79jN_NOT_VALID_dzhepnahq"},
Instance: InstanceConfiguration{AddressFamily: AddressFamilyIPv4},
Instance: InstanceConfiguration{AddressFamily: AddressFamilyIPv4, InternalIpMap: map[string]string{}},
Route: RouteConfiguration{Enabled: true},
Robot: RobotConfiguration{
Enabled: true,
Expand Down

0 comments on commit d72734f

Please sign in to comment.