Skip to content

Commit 0dab647

Browse files
authored
feat: add IPv6-only clusters (#752)
1 parent 7af4d69 commit 0dab647

File tree

16 files changed

+223
-23
lines changed

16 files changed

+223
-23
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ require (
2020
github.com/spf13/cobra v1.7.0
2121
github.com/spf13/viper v1.16.0
2222
github.com/stretchr/testify v1.8.4
23+
go4.org/netipx v0.0.0-20230728184502-ec4c8b891b28
2324
golang.org/x/oauth2 v0.12.0
2425
golang.org/x/sync v0.3.0
2526
google.golang.org/api v0.143.0

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,8 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
446446
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
447447
go.uber.org/zap v1.25.0 h1:4Hvk6GtkucQ790dqmj7l1eEnRdKm3k3ZUrUMS2d5+5c=
448448
go.uber.org/zap v1.25.0/go.mod h1:JIAUzQIH94IC4fOJQm7gMmBJP5k7wQfdcnYdPoEXJYk=
449+
go4.org/netipx v0.0.0-20230728184502-ec4c8b891b28 h1:zLxFnORHDFTSkJPawMU7LzsuGQJ4MUFS653jJHpORow=
450+
go4.org/netipx v0.0.0-20230728184502-ec4c8b891b28/go.mod h1:TQvodOM+hJTioNQJilmLXu08JNb8i+ccq418+KWu1/Y=
449451
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
450452
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
451453
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=

internal/cmd/ktf/environments.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ func init() { //nolint:gochecknoinits
4949
// cluster configurations
5050
environmentsCreateCmd.PersistentFlags().String("kubernetes-version", "", "which kubernetes version to use (default: latest for driver)")
5151
environmentsCreateCmd.PersistentFlags().Bool("cni-calico", false, "use Calico for cluster CNI instead of the default CNI")
52+
environmentsCreateCmd.PersistentFlags().Bool("ipv6-only", false, "only use IPv6")
5253

5354
// addon configurations
5455
environmentsCreateCmd.PersistentFlags().StringArray("addon", nil, "name of an addon to deploy to the testing environment's cluster")
@@ -86,6 +87,10 @@ var environmentsCreateCmd = &cobra.Command{
8687
useCalicoCNI, err := cmd.PersistentFlags().GetBool("cni-calico")
8788
cobra.CheckErr(err)
8889

90+
// check if IPv6 was requested
91+
useIPv6Only, err := cmd.PersistentFlags().GetBool("ipv6-only")
92+
cobra.CheckErr(err)
93+
8994
// setup the new environment
9095
builder := environments.NewBuilder()
9196
if !useGeneratedName {
@@ -94,6 +99,9 @@ var environmentsCreateCmd = &cobra.Command{
9499
if useCalicoCNI {
95100
builder = builder.WithCalicoCNI()
96101
}
102+
if useIPv6Only {
103+
builder = builder.WithIPv6Only()
104+
}
97105
if kubernetesVersion != "" {
98106
version, err := semver.Parse(strings.TrimPrefix(kubernetesVersion, "v"))
99107
cobra.CheckErr(err)

pkg/clusters/addons/kong/addon.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,16 @@ func (a *Addon) Deploy(ctx context.Context, cluster clusters.Cluster) error {
266266
)
267267
}
268268

269+
if cluster.IPFamily() == clusters.IPv6 {
270+
a.deployArgs = append(a.deployArgs,
271+
"--set", "proxy.address=[::]",
272+
"--set", "admin.address=[::1]",
273+
"--set", "status.address=[::]",
274+
"--set", "cluster.address=[::]",
275+
"--set", "ingressController.admissionWebhook.address=[::]",
276+
)
277+
}
278+
269279
// if the ingress controller is disabled flag it in the chart and don't install any CRDs
270280
if a.ingressControllerDisabled {
271281
a.deployArgs = append(a.deployArgs,

pkg/clusters/addons/metallb/metallb.go

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ import (
77
"fmt"
88
"io"
99
"net"
10+
"net/netip"
1011
"os"
1112
"time"
1213

14+
"go4.org/netipx"
1315
corev1 "k8s.io/api/core/v1"
1416
"k8s.io/apimachinery/pkg/api/errors"
1517
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -26,7 +28,6 @@ import (
2628
"github.com/kong/kubernetes-testing-framework/pkg/clusters/types/kind"
2729
"github.com/kong/kubernetes-testing-framework/pkg/utils/docker"
2830
"github.com/kong/kubernetes-testing-framework/pkg/utils/kubernetes/kubectl"
29-
"github.com/kong/kubernetes-testing-framework/pkg/utils/networking"
3031
)
3132

3233
// -----------------------------------------------------------------------------
@@ -141,10 +142,8 @@ func (a *addon) DumpDiagnostics(context.Context, clusters.Cluster) (map[string][
141142
// -----------------------------------------------------------------------------
142143

143144
var (
144-
defaultStartIP = net.ParseIP("0.0.0.100")
145-
defaultEndIP = net.ParseIP("0.0.0.250")
146-
metalManifest = "https://github.com/metallb/metallb/config/native?ref=v0.13.11&timeout=2m"
147-
secretKeyLen = 128
145+
metalManifest = "https://github.com/metallb/metallb/config/native?ref=v0.13.11&timeout=2m"
146+
secretKeyLen = 128
148147
)
149148

150149
// -----------------------------------------------------------------------------
@@ -201,11 +200,15 @@ func deployMetallbForKindCluster(ctx context.Context, cluster clusters.Cluster,
201200

202201
func createIPAddressPool(ctx context.Context, cluster clusters.Cluster, dockerNetwork string) error {
203202
// get an IP range for the docker container network to use for MetalLB
204-
network, err := docker.GetDockerContainerIPNetwork(docker.GetKindContainerID(cluster.Name()), dockerNetwork)
203+
// this returns addresses based on the _Docker network_ the cluster runs on, not the cluster itself. this may,
204+
// for example, return IPv4 addresses even for an IPv6-only cluster. although unsupported addresses will be listed
205+
// in the IPAddressPool, speaker will not actually assign them if they are not compatible with the cluster network.
206+
network, network6, err := docker.GetDockerContainerIPNetwork(docker.GetKindContainerID(cluster.Name()), dockerNetwork)
205207
if err != nil {
206208
return err
207209
}
208210
ipStart, ipEnd := getIPRangeForMetallb(*network)
211+
ip6Start, ip6End := getIPRangeForMetallb(*network6)
209212

210213
dynamicClient, err := dynamic.NewForConfig(cluster.Config())
211214
if err != nil {
@@ -228,7 +231,8 @@ func createIPAddressPool(ctx context.Context, cluster clusters.Cluster, dockerNe
228231
},
229232
"spec": map[string]interface{}{
230233
"addresses": []string{
231-
networking.GetIPRangeStr(ipStart, ipEnd),
234+
fmt.Sprintf("%s-%s", ipStart, ipEnd),
235+
fmt.Sprintf("%s-%s", ip6Start, ip6End),
232236
},
233237
},
234238
},
@@ -297,15 +301,24 @@ func createL2Advertisement(ctx context.Context, cluster clusters.Cluster) error
297301
return nil
298302
}
299303

304+
// TODO use netip throughout. this converts because old public APIs used net/ip instead of net/netip
305+
300306
// getIPRangeForMetallb provides a range of IP addresses to use for MetalLB given an IPv4 Network
301307
//
302-
// TODO: Just choosing specific default IPs for now, need to check range validity and dynamically assign IPs.
308+
// TODO: this just chooses the upper half of the Docker network (minus the network and broadcast addresses for the
309+
// chosen subnet), although those IPs may be in use. Speaker will happily assign those, but they won't work.
310+
// In practice this doesn't appear to cause many problems, since the IPs are normally not in use by KIND components
311+
// (it appears to assign starting from the bottom of the Docker net)
303312
//
304313
// See: https://github.com/Kong/kubernetes-testing-framework/issues/24
305-
func getIPRangeForMetallb(network net.IPNet) (startIP, endIP net.IP) {
306-
startIP = networking.ConvertUint32ToIPv4(networking.ConvertIPv4ToUint32(network.IP) | networking.ConvertIPv4ToUint32(defaultStartIP))
307-
endIP = networking.ConvertUint32ToIPv4(networking.ConvertIPv4ToUint32(network.IP) | networking.ConvertIPv4ToUint32(defaultEndIP))
308-
return
314+
func getIPRangeForMetallb(network net.IPNet) (startIP, endIP netip.Addr) {
315+
// we trust that this is a valid prefix here because we already checked it in docker.GetDockerContainerIPNetwork
316+
prefix := netip.MustParsePrefix(network.String())
317+
half := prefix.Bits() + 1
318+
wholeRange := netipx.RangeOfPrefix(prefix)
319+
upperHalfPrefix := netip.PrefixFrom(wholeRange.To(), half).Masked()
320+
halfRange := netipx.RangeOfPrefix(upperHalfPrefix)
321+
return halfRange.From().Next(), halfRange.To().Prev()
309322
}
310323

311324
// TODO: needs to be replaced with non-kubectl, just used this originally for speed.

pkg/clusters/addons/metallb/metallb_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,17 @@ import (
55
"testing"
66

77
"github.com/stretchr/testify/assert"
8-
9-
"github.com/kong/kubernetes-testing-framework/pkg/utils/networking"
108
)
119

1210
func TestHelperFunctions(t *testing.T) {
1311
network := net.IPNet{
1412
IP: net.IPv4(192, 168, 1, 0),
15-
Mask: net.IPv4Mask(0, 0, 0, 255),
13+
Mask: net.IPv4Mask(255, 255, 255, 0),
1614
}
15+
// this should choose the upper half of the input network, minus network and broadcast addresses
16+
// since we start with 192.168.1.0/24, we should get 192.168.1.128/25. the complete range is
17+
// 192.168.1.128-192.168.1.255, and the returned range is thus 192.168.1.129-192.168.1.254.
1718
ip1, ip2 := getIPRangeForMetallb(network)
18-
assert.Equal(t, ip1.String(), net.IPv4(192, 168, 1, 100).String())
19-
assert.Equal(t, ip2.String(), net.IPv4(192, 168, 1, 250).String())
20-
assert.Equal(t, networking.GetIPRangeStr(ip1, ip2), "192.168.1.100-192.168.1.250")
19+
assert.Equal(t, net.IPv4(192, 168, 1, 129).String(), ip1.String())
20+
assert.Equal(t, net.IPv4(192, 168, 1, 254).String(), ip2.String())
2121
}

pkg/clusters/cluster.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,17 @@ import (
1515
// Type indicates the type of Kubernetes cluster (e.g. Kind, GKE, e.t.c.)
1616
type Type string
1717

18+
type IPFamily string
19+
20+
const (
21+
// IPv4 indicates a Cluster that supports only IPv4 networking.
22+
IPv4 IPFamily = "ipv4"
23+
// IPv6 indicates a Cluster that supports only IPv6 networking.
24+
IPv6 IPFamily = "ipv6"
25+
// Dual indicates a Cluster that supports both IPv4 and IPv6 networking.
26+
Dual IPFamily = "dual"
27+
)
28+
1829
// Cluster objects represent a running Kubernetes cluster.
1930
type Cluster interface {
2031
// Name indicates the unique name of the running cluster.
@@ -51,6 +62,9 @@ type Cluster interface {
5162
// of said directory and an error.
5263
// It uses the provided meta string allow for diagnostics identification.
5364
DumpDiagnostics(ctx context.Context, meta string) (string, error)
65+
66+
// IPFamily returns the cluster's IP networking capabilities.
67+
IPFamily() IPFamily
5468
}
5569

5670
type Builder interface {

pkg/clusters/types/gke/builder.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,8 @@ func (b *Builder) Build(ctx context.Context) (clusters.Cluster, error) {
229229
cfg: restCFG,
230230
addons: make(clusters.Addons),
231231
l: &sync.RWMutex{},
232+
// we simply set this directly for GKE as we lack the ability to create other types of cluster
233+
ipFamily: clusters.IPv4,
232234
}
233235

234236
if err := utils.ClusterInitHooks(ctx, cluster); err != nil {

pkg/clusters/types/gke/cluster.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ type Cluster struct {
3636
cfg *rest.Config
3737
addons clusters.Addons
3838
l *sync.RWMutex
39+
ipFamily clusters.IPFamily
3940
}
4041

4142
// NewFromExistingWithEnv provides a new clusters.Cluster backed by an existing GKE cluster,
@@ -278,3 +279,7 @@ func (c *Cluster) DumpDiagnostics(ctx context.Context, meta string) (string, err
278279

279280
return outDir, err
280281
}
282+
283+
func (c *Cluster) IPFamily() clusters.IPFamily {
284+
return c.ipFamily
285+
}

pkg/clusters/types/kind/builder.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ type Builder struct {
2525
configPath *string
2626
configReader io.Reader
2727
calicoCNI bool
28+
ipv6Only bool
2829
}
2930

3031
// NewBuilder provides a new *Builder object.
@@ -74,6 +75,12 @@ func (b *Builder) WithCalicoCNI() *Builder {
7475
return b
7576
}
7677

78+
// WithIPv6Only configures KIND to only use IPv6.
79+
func (b *Builder) WithIPv6Only() *Builder {
80+
b.ipv6Only = true
81+
return b
82+
}
83+
7784
// Build creates and configures clients for a Kind-based Kubernetes clusters.Cluster.
7885
func (b *Builder) Build(ctx context.Context) (clusters.Cluster, error) {
7986
deployArgs := make([]string, 0)
@@ -92,6 +99,12 @@ func (b *Builder) Build(ctx context.Context) (clusters.Cluster, error) {
9299
deployArgs = append(deployArgs, "--wait", "1s")
93100
}
94101

102+
if b.ipv6Only {
103+
if err := b.useIPv6Only(); err != nil {
104+
return nil, fmt.Errorf("failed configuring IPv6-only networking: %w", err)
105+
}
106+
}
107+
95108
var stdin io.Reader
96109
if b.configPath != nil {
97110
deployArgs = append(deployArgs, "--config", *b.configPath)
@@ -116,13 +129,19 @@ func (b *Builder) Build(ctx context.Context) (clusters.Cluster, error) {
116129
return nil, err
117130
}
118131

132+
ipFamily := clusters.IPv4
133+
if b.ipv6Only {
134+
ipFamily = clusters.IPv6
135+
}
136+
119137
cluster := &Cluster{
120138
name: b.Name,
121139
client: kc,
122140
cfg: cfg,
123141
addons: make(clusters.Addons),
124142
deployArgs: deployArgs,
125143
l: &sync.RWMutex{},
144+
ipFamily: ipFamily,
126145
}
127146

128147
if b.calicoCNI {

0 commit comments

Comments
 (0)