Skip to content

Commit

Permalink
validation: add host validations
Browse files Browse the repository at this point in the history
baremetal.Host is validated using go-playground/validator module and a custom defined rule for duplicates
  • Loading branch information
andfasano committed Mar 19, 2020
1 parent 584a282 commit ec00344
Show file tree
Hide file tree
Showing 3 changed files with 309 additions and 7 deletions.
10 changes: 5 additions & 5 deletions pkg/types/baremetal/platform.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,18 @@ import (

// BMC stores the information about a baremetal host's management controller.
type BMC struct {
Username string `json:"username"`
Password string `json:"password"`
Address string `json:"address"`
Username string `json:"username" validate:"required"`
Password string `json:"password" validate:"required"`
Address string `json:"address" validate:"required,uniqueField"`
DisableCertificateVerification bool `json:"disableCertificateVerification"`
}

// Host stores all the configuration data for a baremetal host.
type Host struct {
Name string `json:"name,omitempty"`
Name string `json:"name,omitempty" validate:"required,uniqueField"`
BMC BMC `json:"bmc"`
Role string `json:"role"`
BootMACAddress string `json:"bootMACAddress"`
BootMACAddress string `json:"bootMACAddress" validate:"required,uniqueField"`
HardwareProfile string `json:"hardwareProfile"`
}

Expand Down
58 changes: 57 additions & 1 deletion pkg/types/baremetal/validation/platform.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ import (
"fmt"
"net"
"net/url"
"reflect"
"strings"

"github.com/go-playground/validator/v10"

"github.com/openshift/installer/pkg/types"
"github.com/openshift/installer/pkg/types/baremetal"
"github.com/openshift/installer/pkg/validate"
"k8s.io/apimachinery/pkg/util/validation/field"
"strings"
)

// dynamicValidator is a function that validates certain fields in the platform.
Expand Down Expand Up @@ -61,6 +64,57 @@ func validateOSImageURI(uri string) error {
return nil
}

// validateHosts checks that hosts have all required fields set with appropriate values
func validateHosts(hosts []*baremetal.Host, fldPath *field.Path) field.ErrorList {
hostErrs := field.ErrorList{}

values := make(map[string]map[interface{}]struct{})

//Initialize a new validator and register a custom validation rule for the tag `uniqueField`
validate := validator.New()
validate.RegisterValidation("uniqueField", func(fl validator.FieldLevel) bool {
valueFound := false
fieldName := fl.Parent().Type().Name() + "." + fl.FieldName()
fieldValue := fl.Field().Interface()

if fl.Field().Type().Comparable() {
if _, present := values[fieldName]; !present {
values[fieldName] = make(map[interface{}]struct{})
}

fieldValues := values[fieldName]
if _, valueFound = fieldValues[fieldValue]; !valueFound {
fieldValues[fieldValue] = struct{}{}
}
} else {
panic(fmt.Sprintf("Cannot apply validation rule 'uniqueField' on field %s", fl.FieldName()))
}

return !valueFound
})

//Apply validations and translate errors
fldPath = fldPath.Child("hosts")

for idx, host := range hosts {
err := validate.Struct(host)
if err != nil {
hostType := reflect.TypeOf(hosts).Elem().Elem().Name()
for _, err := range err.(validator.ValidationErrors) {
childName := fldPath.Index(idx).Child(err.Namespace()[len(hostType)+1:])
switch err.Tag() {
case "required":
hostErrs = append(hostErrs, field.Required(childName, "missing "+err.Field()))
case "uniqueField":
hostErrs = append(hostErrs, field.Duplicate(childName, err.Value()))
}
}
}
}

return hostErrs
}

// ValidatePlatform checks that the specified platform is valid.
func ValidatePlatform(p *baremetal.Platform, n *types.Networking, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
Expand Down Expand Up @@ -156,6 +210,8 @@ func ValidatePlatform(p *baremetal.Platform, n *types.Networking, fldPath *field
}
}

allErrs = append(allErrs, validateHosts(p.Hosts, fldPath)...)

for _, validator := range dynamicValidators {
allErrs = append(allErrs, validator(p, fldPath)...)
}
Expand Down
248 changes: 247 additions & 1 deletion pkg/types/baremetal/validation/platform_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -481,11 +481,257 @@ func TestValidatePlatform(t *testing.T) {
network: network,
expected: "Invalid value: \"192.168.128.1\": \"192.168.128.1\" is not in the provisioning network",
},
{
name: "duplicate_bmc_address",
platform: &baremetal.Platform{
APIVIP: "192.168.111.2",
DNSVIP: "192.168.111.3",
IngressVIP: "192.168.111.4",
Hosts: []*baremetal.Host{
{
Name: "host1",
BootMACAddress: "CA:FE:CA:FE:00:00",
BMC: baremetal.BMC{
Username: "root",
Password: "password",
Address: "ipmi://192.168.111.1",
},
},
{
Name: "host2",
BootMACAddress: "CA:FE:CA:FE:00:01",
BMC: baremetal.BMC{
Username: "root",
Password: "password",
Address: "ipmi://192.168.111.1",
},
},
},
LibvirtURI: "qemu://system",
ProvisioningNetworkCIDR: ipnet.MustParseCIDR("172.22.0.0/24"),
ClusterProvisioningIP: "172.22.0.3",
BootstrapProvisioningIP: "172.22.0.2",
ExternalBridge: "br0",
ProvisioningBridge: "br1",
ProvisioningNetworkInterface: "ens3",
},
network: network,
expected: "baremetal.hosts\\[1\\].BMC.Address: Duplicate value: \"ipmi://192.168.111.1\"",
},
{
name: "bmc_address_required",
platform: &baremetal.Platform{
APIVIP: "192.168.111.2",
DNSVIP: "192.168.111.3",
IngressVIP: "192.168.111.4",
Hosts: []*baremetal.Host{
{
Name: "host1",
BootMACAddress: "CA:FE:CA:FE:00:00",
BMC: baremetal.BMC{
Username: "root",
Password: "password",
},
},
},
LibvirtURI: "qemu://system",
ProvisioningNetworkCIDR: ipnet.MustParseCIDR("172.22.0.0/24"),
ClusterProvisioningIP: "172.22.0.3",
BootstrapProvisioningIP: "172.22.0.2",
ExternalBridge: "br0",
ProvisioningBridge: "br1",
ProvisioningNetworkInterface: "ens3",
},
network: network,
expected: "baremetal.hosts\\[0\\].BMC.Address: Required value: missing Address",
},
{
name: "bmc_username_required",
platform: &baremetal.Platform{
APIVIP: "192.168.111.2",
DNSVIP: "192.168.111.3",
IngressVIP: "192.168.111.4",
Hosts: []*baremetal.Host{
{
Name: "host1",
BootMACAddress: "CA:FE:CA:FE:00:00",
BMC: baremetal.BMC{
Password: "password",
Address: "ipmi://192.168.111.1",
},
},
},
LibvirtURI: "qemu://system",
ProvisioningNetworkCIDR: ipnet.MustParseCIDR("172.22.0.0/24"),
ClusterProvisioningIP: "172.22.0.3",
BootstrapProvisioningIP: "172.22.0.2",
ExternalBridge: "br0",
ProvisioningBridge: "br1",
ProvisioningNetworkInterface: "ens3",
},
network: network,
expected: "baremetal.hosts\\[0\\].BMC.Username: Required value: missing Username",
},
{
name: "bmc_password_required",
platform: &baremetal.Platform{
APIVIP: "192.168.111.2",
DNSVIP: "192.168.111.3",
IngressVIP: "192.168.111.4",
Hosts: []*baremetal.Host{
{
Name: "host1",
BootMACAddress: "CA:FE:CA:FE:00:00",
BMC: baremetal.BMC{
Username: "root",
Address: "ipmi://192.168.111.1",
},
},
},
LibvirtURI: "qemu://system",
ProvisioningNetworkCIDR: ipnet.MustParseCIDR("172.22.0.0/24"),
ClusterProvisioningIP: "172.22.0.3",
BootstrapProvisioningIP: "172.22.0.2",
ExternalBridge: "br0",
ProvisioningBridge: "br1",
ProvisioningNetworkInterface: "ens3",
},
network: network,
expected: "baremetal.hosts\\[0\\].BMC.Password: Required value: missing Password",
},
{
name: "duplicate_host_name",
platform: &baremetal.Platform{
APIVIP: "192.168.111.2",
DNSVIP: "192.168.111.3",
IngressVIP: "192.168.111.4",
Hosts: []*baremetal.Host{
{
Name: "host1",
BootMACAddress: "CA:FE:CA:FE:00:00",
BMC: baremetal.BMC{
Username: "root",
Password: "password",
Address: "ipmi://192.168.111.1",
},
},
{
Name: "host1",
BootMACAddress: "CA:FE:CA:FE:00:01",
BMC: baremetal.BMC{
Username: "root",
Password: "password",
Address: "ipmi://192.168.111.2",
},
},
},
LibvirtURI: "qemu://system",
ProvisioningNetworkCIDR: ipnet.MustParseCIDR("172.22.0.0/24"),
ClusterProvisioningIP: "172.22.0.3",
BootstrapProvisioningIP: "172.22.0.2",
ExternalBridge: "br0",
ProvisioningBridge: "br1",
ProvisioningNetworkInterface: "ens3",
},
network: network,
expected: "baremetal.hosts\\[1\\].Name: Duplicate value: \"host1\"",
},
{
name: "duplicate_host_mac",
platform: &baremetal.Platform{
APIVIP: "192.168.111.2",
DNSVIP: "192.168.111.3",
IngressVIP: "192.168.111.4",
Hosts: []*baremetal.Host{
{
Name: "host1",
BootMACAddress: "CA:FE:CA:FE:CA:FE",
BMC: baremetal.BMC{
Username: "root",
Password: "password",
Address: "ipmi://192.168.111.1",
},
},
{
Name: "host2",
BootMACAddress: "CA:FE:CA:FE:CA:FE",
BMC: baremetal.BMC{
Username: "root",
Password: "password",
Address: "ipmi://192.168.111.2",
},
},
},
LibvirtURI: "qemu://system",
ProvisioningNetworkCIDR: ipnet.MustParseCIDR("172.22.0.0/24"),
ClusterProvisioningIP: "172.22.0.3",
BootstrapProvisioningIP: "172.22.0.2",
ExternalBridge: "br0",
ProvisioningBridge: "br1",
ProvisioningNetworkInterface: "ens3",
},
network: network,
expected: "baremetal.hosts\\[1\\].BootMACAddress: Duplicate value: \"CA:FE:CA:FE:CA:FE\"",
},
{
name: "missing_name",
platform: &baremetal.Platform{
APIVIP: "192.168.111.2",
DNSVIP: "192.168.111.3",
IngressVIP: "192.168.111.4",
Hosts: []*baremetal.Host{
{
BootMACAddress: "CA:FE:CA:FE:CA:FE",
BMC: baremetal.BMC{
Username: "root",
Password: "password",
Address: "ipmi://192.168.111.1",
},
},
},
LibvirtURI: "qemu://system",
ProvisioningNetworkCIDR: ipnet.MustParseCIDR("172.22.0.0/24"),
ClusterProvisioningIP: "172.22.0.3",
BootstrapProvisioningIP: "172.22.0.2",
ExternalBridge: "br0",
ProvisioningBridge: "br1",
ProvisioningNetworkInterface: "ens3",
},
network: network,
expected: "baremetal.hosts\\[0\\].Name: Required value: missing Name",
},
{
name: "missing_mac",
platform: &baremetal.Platform{
APIVIP: "192.168.111.2",
DNSVIP: "192.168.111.3",
IngressVIP: "192.168.111.4",
Hosts: []*baremetal.Host{
{
Name: "host1",
BMC: baremetal.BMC{
Username: "root",
Password: "password",
Address: "ipmi://192.168.111.1",
},
},
},
LibvirtURI: "qemu://system",
ProvisioningNetworkCIDR: ipnet.MustParseCIDR("172.22.0.0/24"),
ClusterProvisioningIP: "172.22.0.3",
BootstrapProvisioningIP: "172.22.0.2",
ExternalBridge: "br0",
ProvisioningBridge: "br1",
ProvisioningNetworkInterface: "ens3",
},
network: network,
expected: "baremetal.hosts\\[0\\].BootMACAddress: Required value: missing BootMACAddress",
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
err := ValidatePlatform(tc.platform, tc.network, field.NewPath("test-path")).ToAggregate()
err := ValidatePlatform(tc.platform, tc.network, field.NewPath("baremetal")).ToAggregate()
if tc.expected == "" {
assert.NoError(t, err)
} else {
Expand Down

0 comments on commit ec00344

Please sign in to comment.