diff --git a/apis/chainlink/v1alpha1/node_validation.go b/apis/chainlink/v1alpha1/node_validation.go new file mode 100644 index 00000000..d2c14a9f --- /dev/null +++ b/apis/chainlink/v1alpha1/node_validation.go @@ -0,0 +1,58 @@ +package v1alpha1 + +import ( + "fmt" + "github.com/kotalco/kotal/apis/shared" + "github.com/kotalco/kotal/helpers/kerrors" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func ValidateCreate(n *Node) []*kerrors.KErrors { + var allErrors []*kerrors.KErrors + + nodelog.Info("validate create", "name", n.Name) + + allErrors = append(allErrors, shared.ValidateCreate(&n.Spec.Resources)...) + + if len(allErrors) == 0 { + return nil + } + + return allErrors +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func ValidateUpdate(n *Node, old runtime.Object) (errors []*kerrors.KErrors) { + oldNode := old.(*Node) + + nodelog.Info("validate update", "name", n.Name) + + errors = append(errors, shared.ValidateUpdate(&n.Spec.Resources, &oldNode.Spec.Resources)...) + + if oldNode.Spec.EthereumChainId != n.Spec.EthereumChainId { + err := field.Invalid(field.NewPath("spec").Child("ethereumChainId"), fmt.Sprintf("%d", n.Spec.EthereumChainId), "field is immutable") + customErr := kerrors.New(*err) + customErr.ChildField = "ethereumChainId" + customErr.CustomMsg = err.Detail + errors = append(errors, customErr) + } + + if oldNode.Spec.LinkContractAddress != n.Spec.LinkContractAddress { + err := field.Invalid(field.NewPath("spec").Child("linkContractAddress"), n.Spec.LinkContractAddress, "field is immutable") + customErr := kerrors.New(*err) + customErr.ChildField = "linkContractAddress" + customErr.CustomMsg = err.Detail + errors = append(errors, customErr) + } + + return +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func ValidateDelete(n *Node) []*kerrors.KErrors { + nodelog.Info("validate delete", "name", n.Name) + return nil +} diff --git a/apis/chainlink/v1alpha1/node_validation_webhook.go b/apis/chainlink/v1alpha1/node_validation_webhook.go index d27c4c99..7af75168 100644 --- a/apis/chainlink/v1alpha1/node_validation_webhook.go +++ b/apis/chainlink/v1alpha1/node_validation_webhook.go @@ -1,8 +1,6 @@ package v1alpha1 import ( - "fmt" - apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -20,14 +18,16 @@ func (r *Node) ValidateCreate() error { nodelog.Info("validate create", "name", r.Name) - allErrors = append(allErrors, r.Spec.Resources.ValidateCreate()...) + errList := ValidateCreate(r) + for _, v := range errList { + allErrors = append(allErrors, &v.FieldErr) + } if len(allErrors) == 0 { return nil } return apierrors.NewInvalid(schema.GroupKind{}, r.Name, allErrors) - } // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type @@ -37,20 +37,9 @@ func (r *Node) ValidateUpdate(old runtime.Object) error { nodelog.Info("validate update", "name", r.Name) - allErrors = append(allErrors, r.Spec.Resources.ValidateUpdate(&oldNode.Spec.Resources)...) - - if oldNode.Spec.EthereumChainId != r.Spec.EthereumChainId { - err := field.Invalid(field.NewPath("spec").Child("ethereumChainId"), fmt.Sprintf("%d", r.Spec.EthereumChainId), "field is immutable") - allErrors = append(allErrors, err) - } - - if oldNode.Spec.LinkContractAddress != r.Spec.LinkContractAddress { - err := field.Invalid(field.NewPath("spec").Child("linkContractAddress"), r.Spec.LinkContractAddress, "field is immutable") - allErrors = append(allErrors, err) - } - - if len(allErrors) == 0 { - return nil + errList := ValidateUpdate(r, oldNode) + for _, v := range errList { + allErrors = append(allErrors, &v.FieldErr) } return apierrors.NewInvalid(schema.GroupKind{}, r.Name, allErrors) diff --git a/apis/shared/resources.go b/apis/shared/resources.go index 15fc4f05..b64933d1 100644 --- a/apis/shared/resources.go +++ b/apis/shared/resources.go @@ -1,9 +1,6 @@ package shared import ( - "fmt" - - "k8s.io/apimachinery/pkg/api/resource" "k8s.io/apimachinery/pkg/util/validation/field" ) @@ -31,69 +28,27 @@ type Resources struct { // validate is the shared validation logic func (r *Resources) validate() (errors field.ErrorList) { - cpu := r.CPU - cpuLimit := r.CPULimit - - if cpu != cpuLimit { - // validate cpuLimit can't be less than cpu request - cpuQuantity := resource.MustParse(cpu) - cpuLimitQuantity := resource.MustParse(cpuLimit) - if cpuLimitQuantity.Cmp(cpuQuantity) == -1 { - msg := fmt.Sprintf("must be greater than or equal to cpu %s", string(cpu)) - err := field.Invalid(field.NewPath("spec").Child("resources").Child("cpuLimit"), cpuLimit, msg) - errors = append(errors, err) - } - } - - memory := r.Memory - memoryLimit := r.MemoryLimit - memoryQuantity := resource.MustParse(memory) - memoryLimitQuantity := resource.MustParse(memoryLimit) - - // validate memory limit must be greater than memory - if memoryLimitQuantity.Cmp(memoryQuantity) != 1 { - msg := fmt.Sprintf("must be greater than memory %s", string(memory)) - err := field.Invalid(field.NewPath("spec").Child("resources").Child("memoryLimit"), memoryLimit, msg) - errors = append(errors, err) + customErrs := validate(r) + for _, v := range customErrs { + errors = append(errors, &v.FieldErr) } - return } // ValidateCreate validates resources during creation func (r *Resources) ValidateCreate() (errors field.ErrorList) { - errors = append(errors, r.validate()...) + customErrs := ValidateCreate(r) + for _, v := range customErrs { + errors = append(errors, &v.FieldErr) + } return } // ValidateUpdate validates resources during update func (r *Resources) ValidateUpdate(oldResources *Resources) (errors field.ErrorList) { - - oldStorage := oldResources.Storage - oldStorageClass := oldResources.StorageClass - - errors = append(errors, r.validate()...) - - // requested storage can't be decreased - if oldStorage != r.Storage { - - oldStorageQuantity := resource.MustParse(oldStorage) - newStorageQuantity := resource.MustParse(r.Storage) - - if newStorageQuantity.Cmp(oldStorageQuantity) == -1 { - msg := fmt.Sprintf("must be greater than or equal to old storage %s", oldStorage) - err := field.Invalid(field.NewPath("spec").Child("resources").Child("storage"), r.Storage, msg) - errors = append(errors, err) - } - - } - - // storage class is immutable - if oldStorageClass != nil && r.StorageClass != nil && *oldStorageClass != *r.StorageClass { - msg := "field is immutable" - err := field.Invalid(field.NewPath("spec").Child("resources").Child("storageClass"), *r.StorageClass, msg) - errors = append(errors, err) + customErrs := ValidateUpdate(r, oldResources) + for _, v := range customErrs { + errors = append(errors, &v.FieldErr) } - return } diff --git a/apis/shared/resources_validation.go b/apis/shared/resources_validation.go new file mode 100644 index 00000000..96175b25 --- /dev/null +++ b/apis/shared/resources_validation.go @@ -0,0 +1,90 @@ +package shared + +import ( + "fmt" + "github.com/kotalco/kotal/helpers/kerrors" + + "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +// validate is the shared validation logic +func validate(r *Resources) (errors []*kerrors.KErrors) { + cpu := r.CPU + cpuLimit := r.CPULimit + + if cpu != cpuLimit { + // validate cpuLimit can't be less than cpu request + cpuQuantity := resource.MustParse(cpu) + cpuLimitQuantity := resource.MustParse(cpuLimit) + if cpuLimitQuantity.Cmp(cpuQuantity) == -1 { + msg := fmt.Sprintf("must be greater than or equal to cpu %s", string(cpu)) + err := field.Invalid(field.NewPath("spec").Child("resources").Child("cpuLimit"), cpuLimit, msg) + customErr := kerrors.New(*err) + customErr.ChildField = "cpuLimit" + customErr.CustomMsg = msg + errors = append(errors, customErr) + } + } + + memory := r.Memory + memoryLimit := r.MemoryLimit + memoryQuantity := resource.MustParse(memory) + memoryLimitQuantity := resource.MustParse(memoryLimit) + + // validate memory limit must be greater than memory + if memoryLimitQuantity.Cmp(memoryQuantity) != 1 { + msg := fmt.Sprintf("must be greater than memory %s", string(memory)) + err := field.Invalid(field.NewPath("spec").Child("resources").Child("memoryLimit"), memoryLimit, msg) + customErr := kerrors.New(*err) + customErr.ChildField = "memoryLimit" + customErr.CustomMsg = msg + errors = append(errors, customErr) + } + + return +} + +// ValidateCreate validates resources during creation +func ValidateCreate(r *Resources) (errors []*kerrors.KErrors) { + errors = append(errors, validate(r)...) + return +} + +// ValidateUpdate validates resources during update +func ValidateUpdate(r *Resources, oldResources *Resources) (errors []*kerrors.KErrors) { + + oldStorage := oldResources.Storage + oldStorageClass := oldResources.StorageClass + + errors = append(errors, validate(r)...) + + // requested storage can't be decreased + if oldStorage != r.Storage { + + oldStorageQuantity := resource.MustParse(oldStorage) + newStorageQuantity := resource.MustParse(r.Storage) + + if newStorageQuantity.Cmp(oldStorageQuantity) == -1 { + msg := fmt.Sprintf("must be greater than or equal to old storage %s", oldStorage) + err := field.Invalid(field.NewPath("spec").Child("resources").Child("storage"), r.Storage, msg) + customErr := kerrors.New(*err) + customErr.ChildField = "storage" + customErr.CustomMsg = msg + errors = append(errors, customErr) + } + + } + + // storage class is immutable + if oldStorageClass != nil && r.StorageClass != nil && *oldStorageClass != *r.StorageClass { + msg := "field is immutable" + err := field.Invalid(field.NewPath("spec").Child("resources").Child("storageClass"), *r.StorageClass, msg) + customErr := kerrors.New(*err) + customErr.ChildField = "storage" + customErr.CustomMsg = msg + errors = append(errors, customErr) + } + + return +} diff --git a/helpers/kerrors/error.go b/helpers/kerrors/error.go new file mode 100644 index 00000000..ea05321d --- /dev/null +++ b/helpers/kerrors/error.go @@ -0,0 +1,15 @@ +package kerrors + +import "k8s.io/apimachinery/pkg/util/validation/field" + +type KErrors struct { + FieldErr field.Error + ChildField string + CustomMsg string +} + +func New(err field.Error) *KErrors { + return &KErrors{ + FieldErr: err, + } +}