Skip to content

Commit d85741c

Browse files
committed
fix(k8s): join host and port instead of concatenating
An IPv6 address concatenated with ":port" is ambiguous and unusable in a URL. This matters more now that node resolution prefers the ExternalIP on the node object, which is IPv6 on a dual-stack or IPv6 cluster. net.JoinHostPort brackets IPv6 and leaves IPv4 and hostnames unchanged.
1 parent 11346e3 commit d85741c

2 files changed

Lines changed: 46 additions & 4 deletions

File tree

modules/k8s/service.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ package k8s
33
import (
44
"context"
55
"fmt"
6+
"net"
67
"net/url"
8+
"strconv"
79
"strings"
810
"time"
911

@@ -154,7 +156,7 @@ func GetServiceEndpointContextE(t testing.TestingT, ctx context.Context, options
154156
switch service.Spec.Type {
155157
case corev1.ServiceTypeClusterIP:
156158

157-
return fmt.Sprintf("%s:%d", service.Spec.ClusterIP, servicePort), nil
159+
return net.JoinHostPort(service.Spec.ClusterIP, strconv.Itoa(servicePort)), nil
158160
case corev1.ServiceTypeNodePort:
159161
return findEndpointForNodePortServiceContext(t, ctx, options, service, int32(servicePort))
160162
case corev1.ServiceTypeExternalName:
@@ -176,10 +178,10 @@ func GetServiceEndpointContextE(t testing.TestingT, ctx context.Context, options
176178
}
177179

178180
if ingress[0].Hostname == "" {
179-
return fmt.Sprintf("%s:%d", ingress[0].IP, servicePort), nil
181+
return net.JoinHostPort(ingress[0].IP, strconv.Itoa(servicePort)), nil
180182
}
181183

182-
return fmt.Sprintf("%s:%d", ingress[0].Hostname, servicePort), nil
184+
return net.JoinHostPort(ingress[0].Hostname, strconv.Itoa(servicePort)), nil
183185
default:
184186
return "", NewUnknownServiceTypeError(service)
185187
}
@@ -210,7 +212,7 @@ func findEndpointForNodePortServiceContext(
210212
return "", err
211213
}
212214

213-
return fmt.Sprintf("%s:%d", nodeHostname, nodePort), nil
215+
return net.JoinHostPort(nodeHostname, strconv.FormatInt(int64(nodePort), 10)), nil
214216
}
215217

216218
// FindNodePortContextE returns the allocated NodePort for the given servicePort from the service definition.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package k8s_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/gruntwork-io/terratest/modules/k8s/v2"
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
corev1 "k8s.io/api/core/v1"
10+
)
11+
12+
// TestGetServiceEndpointBracketsIPv6 pins that endpoints are host/port joined rather than concatenated. An IPv6
13+
// address concatenated with ":port" is ambiguous and unusable in a URL, and this matters more now that node
14+
// resolution prefers the ExternalIP recorded on the node, which is IPv6 on a dual-stack or IPv6 cluster.
15+
func TestGetServiceEndpointBracketsIPv6(t *testing.T) {
16+
t.Parallel()
17+
18+
testCases := []struct {
19+
name string
20+
clusterIP string
21+
expected string
22+
}{
23+
{"ipv4 is unchanged", "10.0.0.1", "10.0.0.1:80"},
24+
{"ipv6 is bracketed", "2600:1f18:abcd::1", "[2600:1f18:abcd::1]:80"},
25+
}
26+
27+
for _, testCase := range testCases {
28+
t.Run(testCase.name, func(t *testing.T) {
29+
t.Parallel()
30+
31+
service := &corev1.Service{
32+
Spec: corev1.ServiceSpec{Type: corev1.ServiceTypeClusterIP, ClusterIP: testCase.clusterIP},
33+
}
34+
35+
endpoint, err := k8s.GetServiceEndpointContextE(t, t.Context(), nil, service, 80)
36+
require.NoError(t, err)
37+
assert.Equal(t, testCase.expected, endpoint)
38+
})
39+
}
40+
}

0 commit comments

Comments
 (0)