From 9198a4fa7da0f431b5a144d6d4ddea512ad5780a Mon Sep 17 00:00:00 2001 From: Enno Richter Date: Tue, 9 Jul 2024 10:09:48 +0200 Subject: [PATCH] Implement support for custom internal IP mapping via HCLOUD_INSTANCES_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. --- hcloud/cloud.go | 2 +- hcloud/instances.go | 38 +++++++++++++++++++++++++--------- hcloud/instances_test.go | 12 +++++------ internal/config/config.go | 26 +++++++++++++++++++++++ internal/config/config_test.go | 32 ++++++++++++++-------------- 5 files changed, 77 insertions(+), 33 deletions(-) diff --git a/hcloud/cloud.go b/hcloud/cloud.go index c2cef9a0e..85315a6ef 100644 --- a/hcloud/cloud.go +++ b/hcloud/cloud.go @@ -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) { diff --git a/hcloud/instances.go b/hcloud/instances.go index fa32023de..2cb693878 100644 --- a/hcloud/instances.go +++ b/hcloud/instances.go @@ -43,6 +43,7 @@ type instances struct { recorder record.EventRecorder addressFamily config.AddressFamily networkID int64 + internalIpMap map[string]string } var ( @@ -50,8 +51,8 @@ var ( 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 { + return &instances{client, robotClient, recorder, addressFamily, networkID, internalIpMap} } // lookupServer attempts to locate the corresponding [*hcloud.Server] or [*hrobotmodels.Server] for a given [*corev1.Node]. @@ -178,7 +179,7 @@ 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) } @@ -186,7 +187,7 @@ func (i *instances) InstanceMetadata(ctx context.Context, node *corev1.Node) (*c 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 { var addresses []corev1.NodeAddress addresses = append( addresses, @@ -226,10 +227,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 { var addresses []corev1.NodeAddress addresses = append( addresses, @@ -254,12 +264,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) } type hcloudServer struct { @@ -270,11 +288,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) { 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, AdditionalLabels: map[string]string{ @@ -299,11 +317,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) { 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), AdditionalLabels: map[string]string{ diff --git a/hcloud/instances_test.go b/hcloud/instances_test.go index ec4d12b97..476553491 100644 --- a/hcloud/instances_test.go +++ b/hcloud/instances_test.go @@ -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 @@ -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{ @@ -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"}, @@ -387,7 +387,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{ @@ -581,7 +581,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) @@ -642,7 +642,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) diff --git a/internal/config/config.go b/internal/config/config.go index 81c93e9ac..0d93114e6 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -23,6 +23,7 @@ const ( robotRateLimitWaitTime = "ROBOT_RATE_LIMIT_WAIT_TIME" hcloudInstancesAddressFamily = "HCLOUD_INSTANCES_ADDRESS_FAMILY" + hcloudInstancesInternalIpMap = "HCLOUD_INSTANCES_INTERNAL_IP_MAP" // Disable the "master/server is attached to the network" check against the metadata service. hcloudNetworkDisableAttachedCheck = "HCLOUD_NETWORK_DISABLE_ATTACHED_CHECK" @@ -68,6 +69,7 @@ const ( type InstanceConfiguration struct { AddressFamily AddressFamily + InternalIpMap map[string]string } type LoadBalancerConfiguration struct { @@ -154,6 +156,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) @@ -263,3 +270,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) { + 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 +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go index ac8d6f978..ab1c714a4 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -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, @@ -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", }, @@ -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, @@ -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, @@ -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, @@ -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", @@ -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", @@ -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", @@ -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, }, @@ -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", }, @@ -313,7 +313,7 @@ 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"), }, @@ -321,7 +321,7 @@ func TestHCCMConfiguration_Validate(t *testing.T) { 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)"), }, @@ -329,7 +329,7 @@ func TestHCCMConfiguration_Validate(t *testing.T) { 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"), }, @@ -337,7 +337,7 @@ func TestHCCMConfiguration_Validate(t *testing.T) { 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", @@ -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, @@ -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,