Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pkg/asset/installconfig/vsphere/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func Validate(ic *types.InstallConfig) error {
return errors.New(field.Required(field.NewPath("platform", "vsphere"), "vSphere validation requires a vSphere platform configuration").Error())
}

allErrs = append(allErrs, validation.ValidatePlatform(ic.Platform.VSphere, field.NewPath("platform").Child("vsphere"))...)
allErrs = append(allErrs, validation.ValidatePlatform(ic.Platform.VSphere, ic.Networking, field.NewPath("platform").Child("vsphere"))...)

return allErrs.ToAggregate()
}
Expand Down
4 changes: 3 additions & 1 deletion pkg/types/validation/installconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,9 @@ func validatePlatform(platform *types.Platform, fldPath *field.Path, network *ty
})
}
if platform.VSphere != nil {
validate(vsphere.Name, platform.VSphere, func(f *field.Path) field.ErrorList { return vspherevalidation.ValidatePlatform(platform.VSphere, f) })
validate(vsphere.Name, platform.VSphere, func(f *field.Path) field.ErrorList {
return vspherevalidation.ValidatePlatform(platform.VSphere, c.Networking, f)
})
}
if platform.BareMetal != nil {
validate(baremetal.Name, platform.BareMetal, func(f *field.Path) field.ErrorList {
Expand Down
61 changes: 48 additions & 13 deletions pkg/types/vsphere/validation/platform.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@ package validation

import (
"fmt"
"net"
"strings"

"k8s.io/apimachinery/pkg/util/validation/field"

"github.com/openshift/installer/pkg/types"
"github.com/openshift/installer/pkg/types/vsphere"
"github.com/openshift/installer/pkg/validate"
)

// ValidatePlatform checks that the specified platform is valid.
func ValidatePlatform(p *vsphere.Platform, fldPath *field.Path) field.ErrorList {
func ValidatePlatform(p *vsphere.Platform, network *types.Networking, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
if len(p.VCenter) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("vCenter"), "must specify the name of the vCenter"))
Expand All @@ -37,7 +39,7 @@ func ValidatePlatform(p *vsphere.Platform, fldPath *field.Path) field.ErrorList

// If all VIPs are empty, skip IP validation. All VIPs are required to be defined together.
if strings.Join([]string{p.APIVIP, p.IngressVIP}, "") != "" {
allErrs = append(allErrs, validateVIPs(p, fldPath)...)
allErrs = append(allErrs, validateVIPs(p, network, fldPath)...)
}

// folder is optional, but if provided should pass validation
Expand All @@ -60,30 +62,63 @@ func ValidateForProvisioning(p *vsphere.Platform, fldPath *field.Path) field.Err
allErrs = append(allErrs, field.Required(fldPath.Child("network"), "must specify the network"))
}

allErrs = append(allErrs, validateVIPs(p, fldPath)...)
if len(p.APIVIP) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("apiVIP"), "must specify a VIP for API"))
}

if len(p.IngressVIP) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("ingressVIP"), "must specify a VIP for Ingress"))
}
return allErrs
}

// ipInNetwork return true if the given ip is within one of the machine networks.
func ipInNetwork(vipIP net.IP, machineNetwork []types.MachineNetworkEntry) bool {
for _, machine := range machineNetwork {
if machine.CIDR.Contains(vipIP) {
return true
}
}
return false
}

// validateVIPs checks that all required VIPs are provided and are valid IP addresses.
func validateVIPs(p *vsphere.Platform, fldPath *field.Path) field.ErrorList {
func validateVIPs(p *vsphere.Platform, network *types.Networking, fldPath *field.Path) field.ErrorList {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if network == nil {
   // add an error and return. This should not happen, so let's not make the function more complex with nested ifs to cover the case.
}

// deal with the happy scenario first
if p.APIVIP != "" {
    ip := net.ParseIP(p.APIVIP)
    if ip != nil {
        if !validateIPInCIDR(ip, network.MachineNetwork) {
            // add error
        }
    } else {
        // add error
    }
} else {
    // add error
}

// repeat for IngressVIP

allErrs := field.ErrorList{}

if len(p.APIVIP) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("apiVIP"), "must specify a VIP for the API"))
} else if err := validate.IP(p.APIVIP); err != nil {
allErrs = append(allErrs, field.Invalid(fldPath.Child("apiVIP"), p.APIVIP, err.Error()))
if network == nil {
return append(allErrs, field.Invalid(field.NewPath("networking"), network, "must specify the machine networks"))
}

if len(p.IngressVIP) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("ingressVIP"), "must specify a VIP for Ingress"))
} else if err := validate.IP(p.IngressVIP); err != nil {
allErrs = append(allErrs, field.Invalid(fldPath.Child("ingressVIP"), p.IngressVIP, err.Error()))
if len(p.APIVIP) != 0 {
ip := net.ParseIP(p.APIVIP)
if ip != nil {
if !ipInNetwork(ip, network.MachineNetwork) {
allErrs = append(allErrs, field.Invalid(fldPath.Child("apiVIP"), p.APIVIP, "must be contained within one of the machine networks"))
}
} else {
allErrs = append(allErrs, field.Invalid(fldPath.Child("apiVIP"), p.APIVIP, "not a valid IP address"))
}
} else {
allErrs = append(allErrs, field.Required(fldPath.Child("apiVIP"), "must specify a VIP for both API and Ingress VIPs when specifying either"))
}

if len(p.IngressVIP) != 0 {
ip := net.ParseIP(p.IngressVIP)
if ip != nil {
if !ipInNetwork(ip, network.MachineNetwork) {
allErrs = append(allErrs, field.Invalid(fldPath.Child("ingressVIP"), p.IngressVIP, "must be contained within one of the machine networks"))
}
} else {
allErrs = append(allErrs, field.Invalid(fldPath.Child("ingressVIP"), p.IngressVIP, "not a valid IP address"))
}
} else {
allErrs = append(allErrs, field.Required(fldPath.Child("ingressVIP"), "must specify a VIP for both API and Ingress VIPs when specifying either"))
}

if len(p.APIVIP) != 0 && len(p.IngressVIP) != 0 && p.APIVIP == p.IngressVIP {
allErrs = append(allErrs, field.Invalid(fldPath.Child("apiVIP"), p.APIVIP, "IPs for both API and Ingress should not be the same."))
}

return allErrs
}

Expand Down
85 changes: 78 additions & 7 deletions pkg/types/vsphere/validation/platform_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package validation

import (
"fmt"
"testing"

"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/util/validation/field"

"github.com/openshift/installer/pkg/ipnet"
"github.com/openshift/installer/pkg/types"
"github.com/openshift/installer/pkg/types/vsphere"
)

Expand All @@ -16,18 +19,31 @@ func validPlatform() *vsphere.Platform {
Password: "test-password",
Datacenter: "test-datacenter",
DefaultDatastore: "test-datastore",
APIVIP: "192.168.111.2",
IngressVIP: "192.168.111.3",
}
}

func validNetwork() *types.Networking {
n := types.Networking{}
cidr := ipnet.MustParseCIDR("192.168.111.1/24")
n.MachineNetwork = append(n.MachineNetwork, types.MachineNetworkEntry{
CIDR: *cidr,
})
return &n
}

func TestValidatePlatform(t *testing.T) {
cases := []struct {
name string
platform *vsphere.Platform
networking *types.Networking
expectedError string
}{
{
name: "minimal",
platform: validPlatform(),
name: "minimal",
platform: validPlatform(),
networking: validNetwork(),
},
{
name: "missing vCenter name",
Expand All @@ -36,6 +52,7 @@ func TestValidatePlatform(t *testing.T) {
p.VCenter = ""
return p
}(),
networking: validNetwork(),
expectedError: `^test-path\.vCenter: Required value: must specify the name of the vCenter$`,
},
{
Expand All @@ -45,6 +62,7 @@ func TestValidatePlatform(t *testing.T) {
p.Username = ""
return p
}(),
networking: validNetwork(),
expectedError: `^test-path\.username: Required value: must specify the username$`,
},
{
Expand All @@ -54,6 +72,7 @@ func TestValidatePlatform(t *testing.T) {
p.Password = ""
return p
}(),
networking: validNetwork(),
expectedError: `^test-path\.password: Required value: must specify the password$`,
},
{
Expand All @@ -63,6 +82,7 @@ func TestValidatePlatform(t *testing.T) {
p.Datacenter = ""
return p
}(),
networking: validNetwork(),
expectedError: `^test-path\.datacenter: Required value: must specify the datacenter$`,
},
{
Expand All @@ -72,6 +92,7 @@ func TestValidatePlatform(t *testing.T) {
p.DefaultDatastore = ""
return p
}(),
networking: validNetwork(),
expectedError: `^test-path\.defaultDatastore: Required value: must specify the default datastore$`,
},
{
Expand All @@ -82,6 +103,7 @@ func TestValidatePlatform(t *testing.T) {
p.IngressVIP = "192.168.111.3"
return p
}(),
networking: validNetwork(),
// expectedError: `^test-path\.apiVIP: Invalid value: "": "" is not a valid IP`,
},
{
Expand All @@ -92,7 +114,8 @@ func TestValidatePlatform(t *testing.T) {
p.IngressVIP = "192.168.111.3"
return p
}(),
expectedError: `^test-path\.apiVIP: Required value: must specify a VIP for the API`,
networking: validNetwork(),
expectedError: `^test-path\.apiVIP: Required value: must specify a VIP for both API and Ingress VIPs when specifying either`,
},
{
name: "missing Ingress VIP",
Expand All @@ -102,7 +125,8 @@ func TestValidatePlatform(t *testing.T) {
p.IngressVIP = ""
return p
}(),
expectedError: `^test-path\.ingressVIP: Required value: must specify a VIP for Ingress`,
networking: validNetwork(),
expectedError: `^test-path\.ingressVIP: Required value: must specify a VIP for both API and Ingress VIPs when specifying either`,
},
{
name: "Invalid API VIP",
Expand All @@ -112,7 +136,8 @@ func TestValidatePlatform(t *testing.T) {
p.IngressVIP = "192.168.111.2"
return p
}(),
expectedError: `^test-path.apiVIP: Invalid value: "192.168.111": "192.168.111" is not a valid IP`,
networking: validNetwork(),
expectedError: `^test-path.apiVIP: Invalid value: "192.168.111": not a valid IP address$`,
},
{
name: "Invalid Ingress VIP",
Expand All @@ -122,7 +147,8 @@ func TestValidatePlatform(t *testing.T) {
p.IngressVIP = "192.168.111"
return p
}(),
expectedError: `^test-path.ingressVIP: Invalid value: "192.168.111": "192.168.111" is not a valid IP`,
networking: validNetwork(),
expectedError: `^test-path.ingressVIP: Invalid value: "192.168.111": not a valid IP address$`,
},
{
name: "Same API and Ingress VIP",
Expand All @@ -132,6 +158,7 @@ func TestValidatePlatform(t *testing.T) {
p.IngressVIP = "192.168.111.1"
return p
}(),
networking: validNetwork(),
expectedError: `^test-path.apiVIP: Invalid value: "192.168.111.1": IPs for both API and Ingress should not be the same`,
},
{
Expand All @@ -141,6 +168,7 @@ func TestValidatePlatform(t *testing.T) {
p.VCenter = "tEsT-vCenter"
return p
}(),
networking: validNetwork(),
expectedError: `^test-path\.vCenter: Invalid value: "tEsT-vCenter": must be the domain name or IP address of the vCenter`,
},
{
Expand All @@ -150,12 +178,55 @@ func TestValidatePlatform(t *testing.T) {
p.VCenter = "https://test-center"
return p
}(),
networking: validNetwork(),
expectedError: `^test-path\.vCenter: Invalid value: "https://test-center": must be the domain name or IP address of the vCenter$`,
},
{
name: "APIVIP not in machine CIDR",
platform: func() *vsphere.Platform {
p := validPlatform()
p.APIVIP = "192.168.1.1"
p.IngressVIP = "192.168.0.1"
return p
}(),
networking: func() *types.Networking {
p := types.Networking{}
value, err := ipnet.ParseCIDR("192.168.0.0/24")
if err != nil {
fmt.Println(err)
}
p.MachineNetwork = append(p.MachineNetwork, types.MachineNetworkEntry{
CIDR: *value,
})
return &p
}(),
expectedError: `^test-path.apiVIP: Invalid value: "192.168.1.1": must be contained within one of the machine networks$`,
},
{
name: "IngressVIP not in machine CIDR",
platform: func() *vsphere.Platform {
p := validPlatform()
p.APIVIP = "192.168.0.1"
p.IngressVIP = "192.168.1.1"
return p
}(),
networking: func() *types.Networking {
p := types.Networking{}
value, err := ipnet.ParseCIDR("192.168.0.0/24")
if err != nil {
fmt.Println(err)
}
p.MachineNetwork = append(p.MachineNetwork, types.MachineNetworkEntry{
CIDR: *value,
})
return &p
}(),
expectedError: `^test-path.ingressVIP: Invalid value: "192.168.1.1": must be contained within one of the machine networks$`,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
err := ValidatePlatform(tc.platform, field.NewPath("test-path")).ToAggregate()
err := ValidatePlatform(tc.platform, tc.networking, field.NewPath("test-path")).ToAggregate()
if tc.expectedError == "" {
assert.NoError(t, err)
} else {
Expand Down