clusterloader2/api/validation.go (238 lines of code) (raw):
/*
Copyright 2021 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package api
import (
"fmt"
"os"
"k8s.io/apimachinery/pkg/util/validation"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/perf-tests/clusterloader2/pkg/errors"
)
// ConfigValidator contains metadata for config validation.
type ConfigValidator struct {
configDir string
config *Config
}
// NewConfigValidator creates a new ConfigValidator object
func NewConfigValidator(configDir string, config *Config) *ConfigValidator {
return &ConfigValidator{
configDir: configDir,
config: config,
}
}
// Validate checks and verifies the configuration parameters.
func (v *ConfigValidator) Validate() *errors.ErrorList {
allErrs := field.ErrorList{}
c := v.config
// TODO(#1696): Clean up after removing automanagedNamespaces
if c.AutomanagedNamespaces < 0 {
allErrs = append(allErrs, field.Invalid(field.NewPath("automanagedNamespaces"), c.AutomanagedNamespaces, "must be non-negative"))
}
allErrs = append(allErrs, v.validateNamespace(&c.Namespace, field.NewPath("namespace"))...)
for i := range c.TuningSets {
allErrs = append(allErrs, v.validateTuningSet(c.TuningSets[i], field.NewPath("tuningSets").Index(i))...)
}
// validate steps
if len(c.Steps) == 0 {
allErrs = append(allErrs, field.Invalid(field.NewPath("steps"), 0, "cannot be empty"))
}
for i := range c.Steps {
allErrs = append(allErrs, v.validateStep(c.Steps[i], field.NewPath("steps").Index(i))...)
}
// Ensure that the tuning set referenced in a phase has been declared
allErrs = append(allErrs, v.handleTuningSetInPhase()...)
if len(allErrs) == 0 {
return nil
}
return v.toErrorList(allErrs)
}
func (v *ConfigValidator) toErrorList(fldErrors field.ErrorList) *errors.ErrorList {
errList := errors.NewErrorList()
for _, fldError := range fldErrors {
errList.Append(fldError)
}
return errList
}
func (v *ConfigValidator) validateNamespace(ns *NamespaceConfig, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
if ns.Number <= 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("number"), ns.Number, "must be positive"))
}
return allErrs
}
func (v *ConfigValidator) validateStep(s *Step, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
for i := range s.Phases {
allErrs = append(allErrs, v.validatePhase(s.Phases[i], fldPath.Child("phases").Index(i))...)
}
for i := range s.Measurements {
allErrs = append(allErrs, v.validateMeasurement(s.Measurements[i], fldPath.Child("measurements").Index(i))...)
}
isPhasesEmpty := len(s.Phases) == 0
isMeasurementsEmpty := len(s.Measurements) == 0
if isPhasesEmpty && isMeasurementsEmpty {
allErrs = append(allErrs, field.Invalid(fldPath.Child("measurements"), s.Measurements, "can't be empty with empty phases"))
}
if !isPhasesEmpty && !isMeasurementsEmpty {
allErrs = append(allErrs, field.Invalid(fldPath.Child("measurements"), s.Measurements, "can't be non-empty with non-empty phases"))
}
return allErrs
}
func (v *ConfigValidator) handleTuningSetInPhase() field.ErrorList {
allErrs := field.ErrorList{}
c := v.config
for i := range c.Steps {
allErrs = append(allErrs, validateTuningSetInPhase(c.Steps[i], c.TuningSets, field.NewPath("steps").Index(i))...)
}
return allErrs
}
func validateTuningSetInPhase(s *Step, ts []*TuningSet, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
for i := range s.Phases {
if s.Phases[i].TuningSet != "" {
found := false
for j := range ts {
if s.Phases[i].TuningSet == ts[j].Name {
found = true
break
}
}
if found == false {
allErrs = append(allErrs, field.Invalid(fldPath.Child("phases").Index(i).Child("tuningSet"), s.Phases[i].TuningSet, "tuning set referenced has not been declared"))
}
}
}
return allErrs
}
func (v *ConfigValidator) validatePhase(p *Phase, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
if p.NamespaceRange != nil {
allErrs = append(allErrs, v.validateNamespaceRange(p.NamespaceRange, fldPath.Child("namespaceRange"))...)
}
if p.ReplicasPerNamespace < 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("replicasPerNamespace"), p.ReplicasPerNamespace, "must be non-negative"))
}
for i := range p.ObjectBundle {
allErrs = append(allErrs, v.validateObject(p.ObjectBundle[i], fldPath.Child("objectBundle").Index(i))...)
}
return allErrs
}
func (v *ConfigValidator) validateObject(o *Object, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
errs := validation.IsDNS1123Subdomain(o.Basename)
for i := range errs {
allErrs = append(allErrs, field.Invalid(fldPath.Child("basename"), o.Basename, errs[i]))
}
if !v.fileExists(o.ObjectTemplatePath) {
allErrs = append(allErrs, field.Invalid(fldPath.Child("objectTemplatePath"), o.ObjectTemplatePath, "file must exist"))
}
return allErrs
}
func (v *ConfigValidator) validateNamespaceRange(nr *NamespaceRange, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
if nr.Min < 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("min"), nr.Min, "must be non-negative"))
}
if nr.Max < nr.Min {
allErrs = append(allErrs, field.Invalid(fldPath.Child("max"), nr.Max, "must be greater than min"))
}
return allErrs
}
func (v *ConfigValidator) validateTuningSet(ts *TuningSet, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
tuningSetsNumber := 0
if ts.QPSLoad != nil {
tuningSetsNumber++
allErrs = append(allErrs, v.validateQPSLoad(ts.QPSLoad, fldPath.Child("qpsLoad"))...)
}
if ts.RandomizedLoad != nil {
tuningSetsNumber++
allErrs = append(allErrs, v.validateRandomizedLoad(ts.RandomizedLoad, fldPath.Child("randomizedLoad"))...)
}
if ts.SteppedLoad != nil {
tuningSetsNumber++
allErrs = append(allErrs, v.validateSteppedLoad(ts.SteppedLoad, fldPath.Child("steppedLoad"))...)
}
if ts.TimeLimitedLoad != nil {
tuningSetsNumber++
allErrs = append(allErrs, v.validateTimeLimitedLoad(ts.TimeLimitedLoad, fldPath.Child("timeLimitedLoad"))...)
}
if ts.RandomizedTimeLimitedLoad != nil {
tuningSetsNumber++
allErrs = append(allErrs, v.validateRandomizedTimeLimitedLoad(ts.RandomizedTimeLimitedLoad, fldPath.Child("randomizedTimeLimitedLoad"))...)
}
if ts.ParallelismLimitedLoad != nil {
tuningSetsNumber++
allErrs = append(allErrs, v.validateParallelismTimeLimitedLoad(ts.ParallelismLimitedLoad, fldPath.Child("parallelismLimitedLoad"))...)
}
if ts.GlobalQPSLoad != nil {
tuningSetsNumber++
allErrs = append(allErrs, v.validateGlobalQPSLoad(ts.GlobalQPSLoad, fldPath.Child("globalQPSLoad"))...)
}
if tuningSetsNumber != 1 {
allErrs = append(allErrs, field.Forbidden(fldPath, "must specify exactly 1 tuning set type"))
}
return allErrs
}
func (v *ConfigValidator) validateMeasurement(m *Measurement, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
if len(m.Instances) != 0 && m.Identifier != "" {
allErrs = append(allErrs, field.Invalid(fldPath.Child("Identifier"), m.Identifier, " cannot be non empty when Instances specified"))
}
if len(m.Instances) == 0 && m.Identifier == "" {
allErrs = append(allErrs, field.Invalid(fldPath.Child("Identifier"), m.Identifier, " and Instances cannot be both empty"))
}
return allErrs
}
func (v *ConfigValidator) validateQPSLoad(ql *QPSLoad, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
if ql.QPS <= 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("qps"), ql.QPS, "must have positive value"))
}
return allErrs
}
func (v *ConfigValidator) validateRandomizedLoad(rl *RandomizedLoad, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
if rl.AverageQPS <= 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("averageQps"), rl.AverageQPS, "must have positive value"))
}
return allErrs
}
func (v *ConfigValidator) validateSteppedLoad(sl *SteppedLoad, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
if sl.BurstSize <= 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("burstSize"), sl.BurstSize, "must have positive value"))
}
return allErrs
}
func (v *ConfigValidator) validateTimeLimitedLoad(tll *TimeLimitedLoad, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
if tll.TimeLimit <= 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("timeLimit"), tll.TimeLimit, "must have positive value"))
}
return allErrs
}
func (v *ConfigValidator) validateRandomizedTimeLimitedLoad(rtl *RandomizedTimeLimitedLoad, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
if rtl.TimeLimit <= 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("timeLimit"), rtl.TimeLimit, "must have positive value"))
}
return allErrs
}
func (v *ConfigValidator) validateParallelismTimeLimitedLoad(ptl *ParallelismLimitedLoad, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
if ptl.ParallelismLimit <= 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("parallelismLimit"), ptl.ParallelismLimit, "must have positive value"))
}
return allErrs
}
func (v *ConfigValidator) validateGlobalQPSLoad(gl *GlobalQPSLoad, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
if gl.QPS <= 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("qps"), gl.QPS, "must have positive value"))
}
if gl.Burst <= 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("burst"), gl.Burst, "must have positive value"))
}
return allErrs
}
func (v *ConfigValidator) fileExists(path string) bool {
cwd, _ := os.Getwd()
_, err := os.Stat(fmt.Sprintf("%s/%s/%s", cwd, v.configDir, path))
if os.IsNotExist(err) {
// If relative path didn't work, we also try absolute path.
_, err = os.Stat(fmt.Sprintf("%s/%s", v.configDir, path))
}
return !os.IsNotExist(err)
}