Skip to content

Commit

Permalink
Add plural IP fields to EndpointSpec
Browse files Browse the repository at this point in the history
...that mirror the singular HealthCheckIP, PrivateIP and PublicIP
fields to support dual-stack addresses. The singular fields are
deprecated and remain to support backwards compatibility and migration
with prior versions. Going forward only the plural fields will be used.
Get*IP/Add*IP methods were added to EndpointSpec that handle the singular
fields. On Get for IPv4, if the plural field doesn't contain an IPv4
address then retrieve the singular field. On Set for IPv4 also set
the corresponding singular field.

Signed-off-by: Tom Pantelis <[email protected]>
  • Loading branch information
tpantelis authored and skitt committed Jan 17, 2025
1 parent fe74afa commit 2c0e7b5
Show file tree
Hide file tree
Showing 6 changed files with 378 additions and 34 deletions.
58 changes: 58 additions & 0 deletions pkg/apis/submariner.io/v1/endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/pkg/errors"
"github.com/submariner-io/admiral/pkg/resource"
"k8s.io/apimachinery/pkg/api/equality"
k8snet "k8s.io/utils/net"
)

func (ep *EndpointSpec) GetBackendPort(configName string, defaultValue int32) (int32, error) {
Expand Down Expand Up @@ -102,3 +103,60 @@ func (ep *EndpointSpec) hasSameBackendConfig(other *EndpointSpec) bool {

return equality.Semantic.DeepEqual(ep.BackendConfig, other.BackendConfig)
}

func getIPFrom(family k8snet.IPFamily, ips []string, ipv4Fallback string) string {
for _, ip := range ips {
if k8snet.IPFamilyOfString(ip) == family {
return ip
}
}

if family == k8snet.IPv4 {
return ipv4Fallback
}

return ""
}

func setIP(ips []string, ipv4Fallback, newIP string) ([]string, string) {
family := k8snet.IPFamilyOfString(newIP)

if family == k8snet.IPv4 {
ipv4Fallback = newIP
}

for i := range ips {
if k8snet.IPFamilyOfString(ips[i]) == family {
ips[i] = newIP
return ips, ipv4Fallback
}
}

ips = append(ips, newIP)

return ips, ipv4Fallback
}

func (ep *EndpointSpec) GetHealthCheckIP(family k8snet.IPFamily) string {
return getIPFrom(family, ep.HealthCheckIPs, ep.HealthCheckIP)
}

func (ep *EndpointSpec) SetHealthCheckIP(ip string) {
ep.HealthCheckIPs, ep.HealthCheckIP = setIP(ep.HealthCheckIPs, ep.HealthCheckIP, ip)
}

func (ep *EndpointSpec) GetPublicIP(family k8snet.IPFamily) string {
return getIPFrom(family, ep.PublicIPs, ep.PublicIP)
}

func (ep *EndpointSpec) SetPublicIP(ip string) {
ep.PublicIPs, ep.PublicIP = setIP(ep.PublicIPs, ep.PublicIP, ip)
}

func (ep *EndpointSpec) GetPrivateIP(family k8snet.IPFamily) string {
return getIPFrom(family, ep.PrivateIPs, ep.PrivateIP)
}

func (ep *EndpointSpec) SetPrivateIP(ip string) {
ep.PrivateIPs, ep.PrivateIP = setIP(ep.PrivateIPs, ep.PrivateIP, ip)
}
229 changes: 229 additions & 0 deletions pkg/apis/submariner.io/v1/endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,23 @@ import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
v1 "github.com/submariner-io/submariner/pkg/apis/submariner.io/v1"
k8snet "k8s.io/utils/net"
)

const (
ipV4Addr = "1.2.3.4"
ipV6Addr = "2001:db8:3333:4444:5555:6666:7777:8888"
)

var _ = Describe("EndpointSpec", func() {
Context("GenerateName", testGenerateName)
Context("Equals", testEquals)
Context("GetHealthCheckIP", testGetHealthCheckIP)
Context("SetHealthCheckIP", testSetHealthCheckIP)
Context("GetPublicIP", testGetPublicIP)
Context("SetPublicIP", testSetPublicIP)
Context("GetPrivateIP", testGetPrivateIP)
Context("SetPrivateIP", testSetPrivateIP)
})

func testGenerateName() {
Expand Down Expand Up @@ -138,3 +150,220 @@ func testEquals() {
})
})
}

func testGetIP(ipsSetter func(*v1.EndpointSpec, []string, string), ipsGetter func(*v1.EndpointSpec, k8snet.IPFamily) string) {
var (
spec *v1.EndpointSpec
legacyIPv4IP string
ips []string
)

BeforeEach(func() {
legacyIPv4IP = ""
ips = []string{}
})

JustBeforeEach(func() {
spec = &v1.EndpointSpec{}
ipsSetter(spec, ips, legacyIPv4IP)
})

Context("IPv4", func() {
When("an IPv4 address is present", func() {
BeforeEach(func() {
ips = []string{ipV6Addr, ipV4Addr}
})

It("should return the address", func() {
Expect(ipsGetter(spec, k8snet.IPv4)).To(Equal(ipV4Addr))
})
})

When("an IPv4 address is not present and the legacy IPv4 address is set", func() {
BeforeEach(func() {
ips = []string{ipV6Addr}
legacyIPv4IP = ipV4Addr
})

It("should return the legacy address", func() {
Expect(ipsGetter(spec, k8snet.IPv4)).To(Equal(ipV4Addr))
})
})

When("an IPv4 address is not present and the legacy IPv4 address is not set", func() {
It("should return empty string", func() {
Expect(ipsGetter(spec, k8snet.IPv4)).To(BeEmpty())
})
})
})

Context("IPv6", func() {
When("an IPv6 address is present", func() {
BeforeEach(func() {
ips = []string{ipV4Addr, ipV6Addr}
})

It("should return the address", func() {
Expect(ipsGetter(spec, k8snet.IPv6)).To(Equal(ipV6Addr))
})
})

When("an IPv6 address is not present", func() {
BeforeEach(func() {
ips = []string{ipV4Addr}
})

It("should return empty string", func() {
Expect(ipsGetter(spec, k8snet.IPv6)).To(BeEmpty())
})
})
})
}

func testSetIP(initIPs func(*v1.EndpointSpec, []string), ipsSetter func(*v1.EndpointSpec, string),
ipsGetter func(*v1.EndpointSpec) ([]string, string),
) {
var (
spec *v1.EndpointSpec
ipToSet string
initialIPs []string
)

BeforeEach(func() {
spec = &v1.EndpointSpec{}
initialIPs = []string{}
ipToSet = ""
})

JustBeforeEach(func() {
initIPs(spec, initialIPs)
ipsSetter(spec, ipToSet)
})

verifyIPs := func(ips []string, legacyV4 string) {
actualIPs, actualLegacy := ipsGetter(spec)
Expect(actualIPs).To(Equal(ips))
Expect(actualLegacy).To(Equal(legacyV4))
}

Context("IPv4", func() {
BeforeEach(func() {
ipToSet = ipV4Addr
})

When("no addresses are present", func() {
It("should add the new address", func() {
verifyIPs([]string{ipToSet}, ipToSet)
})
})

When("no IPv4 address is present", func() {
BeforeEach(func() {
initialIPs = []string{ipV6Addr}
})

It("should add the new address", func() {
verifyIPs([]string{ipV6Addr, ipToSet}, ipToSet)
})
})

When("an IPv4 address is already present", func() {
BeforeEach(func() {
initialIPs = []string{"11.22.33.44"}
})

It("should update address", func() {
verifyIPs([]string{ipToSet}, ipToSet)
})
})
})

Context("IPv6", func() {
BeforeEach(func() {
ipToSet = ipV6Addr
})

When("no addresses are present", func() {
It("should add the new address", func() {
verifyIPs([]string{ipToSet}, "")
})
})

When("no IPv6 address is present", func() {
BeforeEach(func() {
initialIPs = []string{ipV4Addr}
})

It("should add the new address", func() {
verifyIPs([]string{ipV4Addr, ipToSet}, "")
})
})

When("an IPv6 address is already present", func() {
BeforeEach(func() {
initialIPs = []string{"1234:cb9:3333:4444:5555:6666:7777:8888"}
})

It("should update address", func() {
verifyIPs([]string{ipToSet}, "")
})
})
})
}

func testGetHealthCheckIP() {
testGetIP(func(s *v1.EndpointSpec, ips []string, ipv4IP string) {
s.HealthCheckIPs = ips
s.HealthCheckIP = ipv4IP
}, func(s *v1.EndpointSpec, family k8snet.IPFamily) string {
return s.GetHealthCheckIP(family)
})
}

func testSetHealthCheckIP() {
testSetIP(func(s *v1.EndpointSpec, ips []string) {
s.HealthCheckIPs = ips
}, func(s *v1.EndpointSpec, ip string) {
s.SetHealthCheckIP(ip)
}, func(s *v1.EndpointSpec) ([]string, string) {
return s.HealthCheckIPs, s.HealthCheckIP
})
}

func testGetPublicIP() {
testGetIP(func(s *v1.EndpointSpec, ips []string, ipv4IP string) {
s.PublicIPs = ips
s.PublicIP = ipv4IP
}, func(s *v1.EndpointSpec, family k8snet.IPFamily) string {
return s.GetPublicIP(family)
})
}

func testSetPublicIP() {
testSetIP(func(s *v1.EndpointSpec, ips []string) {
s.PublicIPs = ips
}, func(s *v1.EndpointSpec, ip string) {
s.SetPublicIP(ip)
}, func(s *v1.EndpointSpec) ([]string, string) {
return s.PublicIPs, s.PublicIP
})
}

func testGetPrivateIP() {
testGetIP(func(s *v1.EndpointSpec, ips []string, ipv4IP string) {
s.PrivateIPs = ips
s.PrivateIP = ipv4IP
}, func(s *v1.EndpointSpec, family k8snet.IPFamily) string {
return s.GetPrivateIP(family)
})
}

func testSetPrivateIP() {
testSetIP(func(s *v1.EndpointSpec, ips []string) {
s.PrivateIPs = ips
}, func(s *v1.EndpointSpec, ip string) {
s.SetPrivateIP(ip)
}, func(s *v1.EndpointSpec) ([]string, string) {
return s.PrivateIPs, s.PrivateIP
})
}
36 changes: 17 additions & 19 deletions pkg/apis/submariner.io/v1/string_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,25 +26,23 @@ import (
v1 "github.com/submariner-io/submariner/pkg/apis/submariner.io/v1"
)

const expectedString = `{"metadata":{"creationTimestamp":null},"spec":{"cluster_id":"cluster-id","cable_name":` +
`"cable-1","hostname":"","subnets":["10.0.0.0/24","172.0.0.0/24"],"private_ip":"1.1.1.1",` +
`"public_ip":"","nat_enabled":false,"backend":""}}`

var _ = Describe("API v1", func() {
When("Endpoint String representation called", func() {
It("Should return a human readable string", func() {
endpoint := v1.Endpoint{
Spec: v1.EndpointSpec{
ClusterID: "cluster-id",
Subnets: []string{"10.0.0.0/24", "172.0.0.0/24"},
CableName: "cable-1",
PublicIP: "",
PrivateIP: "1.1.1.1",
},
}

Expect(endpoint.String()).To(Equal(expectedString))
})
var _ = Describe("Endpoint String", func() {
It("should return a human readable string", func() {
str := (&v1.Endpoint{
Spec: v1.EndpointSpec{
ClusterID: "east",
Subnets: []string{"10.0.0.0/24"},
CableName: "cable-1",
PublicIPs: []string{"1.1.1.1"},
PrivateIPs: []string{"2.2.2.2"},
},
}).String()

Expect(str).To(ContainSubstring("east"))
Expect(str).To(ContainSubstring("10.0.0.0/24"))
Expect(str).To(ContainSubstring("cable-1"))
Expect(str).To(ContainSubstring("1.1.1.1"))
Expect(str).To(ContainSubstring("2.2.2.2"))
})
})

Expand Down
21 changes: 16 additions & 5 deletions pkg/apis/submariner.io/v1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,22 @@ type EndpointSpec struct {
ClusterID string `json:"cluster_id"`
CableName string `json:"cable_name"`
// +optional
HealthCheckIP string `json:"healthCheckIP,omitempty"`
Hostname string `json:"hostname"`
Subnets []string `json:"subnets"`
PrivateIP string `json:"private_ip"`
PublicIP string `json:"public_ip"`
HealthCheckIP string `json:"healthCheckIP,omitempty"`
// +kubebuilder:validation:MaxItems:=2
// +optional
HealthCheckIPs []string `json:"healthCheckIPs,omitempty"`
Hostname string `json:"hostname"`
Subnets []string `json:"subnets"`
// +optional
PrivateIP string `json:"private_ip,omitempty"`
// +kubebuilder:validation:MaxItems:=2
// +optional
PrivateIPs []string `json:"privateIPs,omitempty"`
// +optional
PublicIP string `json:"public_ip,omitempty"`
// +kubebuilder:validation:MaxItems:=2
// +optional
PublicIPs []string `json:"publicIPs,omitempty"`
NATEnabled bool `json:"nat_enabled"`
Backend string `json:"backend"`
BackendConfig map[string]string `json:"backend_config,omitempty"`
Expand Down
Loading

0 comments on commit 2c0e7b5

Please sign in to comment.