Skip to content

Commit

Permalink
Add a Network validation section for APIVIP sanity checks
Browse files Browse the repository at this point in the history
Almost all sections within the image definition go through sanity checks
before actually creating a new image. However, EIB misses a dedicated
section to test the Network part of the Kubernetes configuration.

Add a validateNetwork function to in order to make sure that the specified
APIVIP value is well-formed and valid, regardless of wheather it is in
IPv4 or IPv6 format.

Signed-off-by: Marco Chiappero <[email protected]>
  • Loading branch information
mchiappero committed Aug 5, 2024
1 parent 6f2902f commit 5bfaf6b
Show file tree
Hide file tree
Showing 2 changed files with 172 additions and 24 deletions.
42 changes: 36 additions & 6 deletions pkg/image/validation/kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"net/url"
"net/netip"
"os"
"path/filepath"
"slices"
Expand Down Expand Up @@ -33,6 +34,7 @@ func validateKubernetes(ctx *image.Context) []FailedValidation {
return failures
}

failures = append(failures, validateNetwork(&def.Kubernetes)...)
failures = append(failures, validateNodes(&def.Kubernetes)...)
failures = append(failures, validateManifestURLs(&def.Kubernetes)...)
failures = append(failures, validateHelm(&def.Kubernetes, combustion.HelmValuesPath(ctx), combustion.HelmCertsPath(ctx))...)
Expand All @@ -44,21 +46,49 @@ func isKubernetesDefined(k8s *image.Kubernetes) bool {
return k8s.Version != ""
}

func validateNodes(k8s *image.Kubernetes) []FailedValidation {
func validateNetwork(k8s *image.Kubernetes) []FailedValidation {
var failures []FailedValidation
var vipAddress = k8s.Network.APIVIP

if vipAddress == "" {
numNodes := len(k8s.Nodes)
// Single node cluster, apiVIP is optional, check only multi-node
if numNodes > 1 {
failures = append(failures, FailedValidation{
UserMessage: "The 'apiVIP' field is required in the 'network' section when defining entries under 'nodes'.",
})
}

numNodes := len(k8s.Nodes)
if numNodes <= 1 {
// Single node cluster, node configurations are not required
return failures
}

ip, err := netip.ParseAddr(vipAddress)
if err != nil {
msg := fmt.Sprintf("Invalid cluster API address ('%s')", strings.ToLower(vipAddress))
failures = append(failures, FailedValidation{
UserMessage: msg,
})
}

if k8s.Network.APIVIP == "" {
if ip.IsUnspecified() || ip.IsMulticast() {
msg := fmt.Sprintf("Invalid cluster API address (%s)", ip.String())
failures = append(failures, FailedValidation{
UserMessage: "The 'apiVIP' field is required in the 'network' section when defining entries under 'nodes'.",
UserMessage: msg,
})
}

return failures
}

func validateNodes(k8s *image.Kubernetes) []FailedValidation {
var failures []FailedValidation

numNodes := len(k8s.Nodes)
if numNodes <= 1 {
// Single node cluster, node configurations are not required
return failures
}

var nodeTypes []string
var nodeNames []string
var initialisers []*image.Node
Expand Down
154 changes: 136 additions & 18 deletions pkg/image/validation/kubernetes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,142 @@ func TestIsKubernetesDefined(t *testing.T) {
assert.False(t, result)
}

func TestValidateNetwork(t *testing.T) {
var validV4Network = validNetwork

tests := map[string]struct {
K8s image.Kubernetes
ExpectedFailedMessages []string
}{
`valid v4 VIP`: {
K8s: image.Kubernetes{
Network: validV4Network,
},
},
`valid v6 VIP`: {
K8s: image.Kubernetes{
Network: image.Network{
APIHost: "host.com",
APIVIP: "FEC0::1",
},
},
},
`valid single node - no network config`: {
K8s: image.Kubernetes{
Network: image.Network{},
Nodes: []image.Node{
{
Hostname: "host",
Type: image.KubernetesNodeTypeServer,
},
},
},
},
`invalid multi-node - no network config`: {
K8s: image.Kubernetes{
Network: image.Network{},
Nodes: []image.Node{
{
Hostname: "host1",
Type: image.KubernetesNodeTypeServer,
},
{
Hostname: "host2",
Type: image.KubernetesNodeTypeAgent,
},
},
},
ExpectedFailedMessages: []string{
"The 'apiVIP' field is required in the 'network' section when defining entries under 'nodes'.",
},
},
`malformed v4 VIP`: {
K8s: image.Kubernetes{
Network: image.Network{
APIHost: "host.com",
APIVIP: "192.168.1",
},
},
ExpectedFailedMessages: []string{
"Invalid cluster API address ('192.168.1')",
},
},
`malformed v6 VIP`: {
K8s: image.Kubernetes{
Network: image.Network{
APIHost: "host.com",
APIVIP: "FEC0::1::1",
},
},
ExpectedFailedMessages: []string{
"Invalid cluster API address ('fec0::1::1')",
},
},
`undefined v4 VIP`: {
K8s: image.Kubernetes{
Network: image.Network{
APIHost: "host.com",
APIVIP: "0.0.0.0",
},
},
ExpectedFailedMessages: []string{
"Invalid cluster API address (0.0.0.0)",
},
},
`undefined v6 VIP`: {
K8s: image.Kubernetes{
Network: image.Network{
APIHost: "host.com",
APIVIP: "::",
},
},
ExpectedFailedMessages: []string{
"Invalid cluster API address (::)",
},
},
`multicast v4 VIP`: {
K8s: image.Kubernetes{
Network: image.Network{
APIHost: "host.com",
APIVIP: "224.224.224.224",
},
},
ExpectedFailedMessages: []string{
"Invalid cluster API address (224.224.224.224)",
},
},
`multicast v6 VIP`: {
K8s: image.Kubernetes{
Network: image.Network{
APIHost: "host.com",
APIVIP: "FF01::1",
},
},
ExpectedFailedMessages: []string{
"Invalid cluster API address (ff01::1)",
},
},
}

for name, test := range tests {
t.Run(name, func(t *testing.T) {
k := test.K8s
failures := validateNetwork(&k)
assert.Len(t, failures, len(test.ExpectedFailedMessages))

var foundMessages []string
for _, foundValidation := range failures {
foundMessages = append(foundMessages, foundValidation.UserMessage)
}

for _, expectedMessage := range test.ExpectedFailedMessages {
assert.Contains(t, foundMessages, expectedMessage)
}

})
}
}

func TestValidateNodes(t *testing.T) {
tests := map[string]struct {
K8s image.Kubernetes
Expand All @@ -166,24 +302,6 @@ func TestValidateNodes(t *testing.T) {
Nodes: []image.Node{},
},
},
`with nodes - no network config`: {
K8s: image.Kubernetes{
Network: image.Network{},
Nodes: []image.Node{
{
Hostname: "host1",
Type: image.KubernetesNodeTypeServer,
},
{
Hostname: "host2",
Type: image.KubernetesNodeTypeAgent,
},
},
},
ExpectedFailedMessages: []string{
"The 'apiVIP' field is required in the 'network' section when defining entries under 'nodes'.",
},
},
`no hostname`: {
K8s: image.Kubernetes{
Network: validNetwork,
Expand Down

0 comments on commit 5bfaf6b

Please sign in to comment.