pkg/apis/ndbcontroller/v1/validation.go (176 lines of code) (raw):
// Copyright (c) 2021, 2022, Oracle and/or its affiliates.
//
// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/
package v1
import (
"errors"
"fmt"
"math"
"reflect"
"strings"
"github.com/mysql/ndb-operator/pkg/constants"
"github.com/mysql/ndb-operator/pkg/ndbconfig/configparser"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/apimachinery/pkg/util/validation"
"k8s.io/apimachinery/pkg/util/validation/field"
)
// List of all config parameters that are not allowed in
// .Spec.DataNode.Config. and any additional details to be appended to the error.
var disallowedConfigParams = map[string]string{
// NoOfReplicas is already set via .spec.redundancyLevel
"noofreplicas": "Specify it via .spec.redundancyLevel.", // NoOfReplicas
// Disallow any parameter that specifies machine/port
// configurations as that will be taken care by the operator.
"nodeid": "", // NodeId
"hostname": "", // HostName
"serverport": "", // ServerPort
"executeoncomputer": "", // ExecuteOnComputer
"nodegroup": "", // NodeGroup
"portnumber": "", // Mgmd's PortNumber
// Disallow DataDir config as that will be handled by the operator
"datadir": "", // DataDir
}
func validateConfigParams(config map[string]*intstr.IntOrString, specPath *field.Path) (errList field.ErrorList) {
for configKey := range config {
if details, exists := disallowedConfigParams[strings.ToLower(configKey)]; exists {
msg := fmt.Sprintf("config param %q is not allowed in %s. ", configKey, specPath.String())
if details == "" {
msg += "It will be configured automatically by the Ndb Operator based on the spec."
} else {
msg += details
}
errList = append(errList, field.Forbidden(specPath.Child(configKey), msg))
}
}
return errList
}
// HasValidSpec validates the spec of the NdbCluster object
func (nc *NdbCluster) HasValidSpec() (bool, field.ErrorList) {
spec := nc.Spec
var errList field.ErrorList
specPath := field.NewPath("spec")
mysqldPath := specPath.Child("mysqlNode")
dataNodePath := specPath.Child("dataNode")
managementNodePath := specPath.Child("managementNode")
dataNodeCount := spec.DataNode.NodeCount
mysqlServerCount := nc.GetMySQLServerMaxNodeCount()
managementNodeCount := nc.GetManagementNodeCount()
numOfFreeApiSlots := spec.FreeAPISlots + 1
// check if number of data nodes is a multiple of redundancy
if math.Mod(float64(dataNodeCount), float64(spec.RedundancyLevel)) != 0 {
msg := fmt.Sprintf(
"spec.dataNode.nodeCount should be a multiple of the spec.redundancyLevel(=%d)", spec.RedundancyLevel)
errList = append(errList, field.Invalid(dataNodePath.Child("nodeCount"), dataNodeCount, msg))
}
// check if total number of nodes are not more than the allowed maximum
total := managementNodeCount + dataNodeCount + mysqlServerCount + numOfFreeApiSlots
if total > constants.MaxNumberOfNodes {
invalidValue := fmt.Sprintf(
"%d (= %d management, %d data, %d mysql nodes and %d free API nodes)",
total, managementNodeCount, dataNodeCount, mysqlServerCount, numOfFreeApiSlots)
msg := fmt.Sprintf(
"Total number of MySQL Cluster nodes should not exceed the allowed maximum of %d", constants.MaxNumberOfNodes)
errList = append(errList, field.Invalid(field.NewPath("Total Nodes"), invalidValue, msg))
}
// check if there are any disallowed config params in dataNode's Configuration.
if err := validateConfigParams(nc.Spec.DataNode.Config, dataNodePath.Child("config")); err != nil {
errList = append(errList, err...)
}
// check if there are any disallowed config params in managementNode Config.
if nc.Spec.ManagementNode != nil {
if err := validateConfigParams(nc.Spec.ManagementNode.Config, managementNodePath.Child("config")); err != nil {
errList = append(errList, err...)
}
}
// check if the MySQL root password secret name has the expected format
var rootPasswordSecret string
if spec.MysqlNode != nil {
mysqldSpec := spec.MysqlNode
// check if the MySQL root password secret name has the expected format
rootPasswordSecret = mysqldSpec.RootPasswordSecretName
if rootPasswordSecret != "" {
errs := validation.IsDNS1123Subdomain(rootPasswordSecret)
// append errors, if any, to errList
for _, err := range errs {
errList = append(errList,
field.Invalid(mysqldPath.Child("rootPasswordSecretName"), rootPasswordSecret, err))
}
}
// check if maxNodeCount is less than nodeCount
if mysqldSpec.MaxNodeCount != 0 &&
mysqldSpec.MaxNodeCount < mysqldSpec.NodeCount {
msg := fmt.Sprintf(
"spec.mysqlNode.maxNodeCount cannot be less than spec.mysqlNode.nodeCount(=%d)", mysqldSpec.NodeCount)
errList = append(errList,
field.Invalid(mysqldPath.Child("maxNodeCount"), mysqldSpec.MaxNodeCount, msg))
}
}
// check if any passed my.cnf has proper format
myCnfString := nc.GetMySQLCnf()
if len(myCnfString) > 0 {
myCnf, err := configparser.ParseString(myCnfString)
if err != nil {
// error parsing the cnf
errList = append(errList,
field.Invalid(mysqldPath.Child("myCnf"), myCnfString, err.Error()))
} else {
// accept only one mysqld section in the cnf
if len(myCnf) != 1 ||
myCnf.GetNumberOfSections("mysqld") != 1 {
errList = append(errList,
field.Invalid(mysqldPath.Child("myCnf"),
myCnfString, "spec.mysqlNode.myCnf can have only one mysqld section"))
}
}
}
return errList == nil, errList
}
func cannotUpdateFieldError(specPath *field.Path, newValue interface{}) *field.Error {
return field.Invalid(specPath, newValue,
fmt.Sprintf("%s cannot be updated once NdbCluster has been created", specPath.String()))
}
// validateNdbPodSpecResources verifies that the Resources field of the NdbPodSpec has not changes
func validateNdbPodSpecResources(specPath *field.Path, oldNdbPodSpec, newNdbPodSpec *NdbClusterPodSpec) *field.Error {
var oldResources, newResources *corev1.ResourceRequirements
if oldNdbPodSpec != nil {
oldResources = oldNdbPodSpec.Resources
}
if newNdbPodSpec != nil {
newResources = newNdbPodSpec.Resources
}
if !reflect.DeepEqual(oldResources, newResources) {
resourcesPath := specPath.Child("resources")
return field.Forbidden(resourcesPath,
fmt.Sprintf("%s cannot be updated once NdbCluster has been created", resourcesPath.String()))
}
return nil
}
func (nc *NdbCluster) IsValidSpecUpdate(newNc *NdbCluster) (bool, field.ErrorList) {
var errList field.ErrorList
specPath := field.NewPath("spec")
managementNodePath := specPath.Child("managementNode")
dataNodePath := specPath.Child("dataNode")
mysqldPath := specPath.Child("mysqlNode")
if nc.Spec.RedundancyLevel == 1 {
// MySQL Cluster replica = 1 => updating MySQL config via
// rolling restart is not possible. Disallow any spec update.
errList = append(errList,
field.InternalError(specPath,
errors.New("operator cannot handle any spec update to a MySQL Cluster whose replica is 1")))
return false, errList
}
// Do not allow decreasing Spec.DataNode.NodeCount
if nc.Spec.DataNode.NodeCount > newNc.Spec.DataNode.NodeCount {
errList = append(errList,
field.Invalid(dataNodePath.Child("nodeCount"), newNc.Spec.DataNode.NodeCount,
"spec.dataNode.nodeCount cannot be reduced once MySQL Cluster has been started"))
}
// Do not allow updating Spec.RedundancyLevel
if nc.Spec.RedundancyLevel != newNc.Spec.RedundancyLevel {
errList = append(errList,
cannotUpdateFieldError(specPath.Child("redundancyLevel"), newNc.Spec.RedundancyLevel))
}
// Do not allow updating Resource field of various ndbPodSpecs
if nc.Spec.ManagementNode != nil {
if err := validateNdbPodSpecResources(
managementNodePath.Child("ndbPodSpec"),
nc.Spec.ManagementNode.NdbPodSpec, newNc.Spec.ManagementNode.NdbPodSpec); err != nil {
errList = append(errList, err)
}
}
if err := validateNdbPodSpecResources(
dataNodePath.Child("ndbPodSpec"), nc.Spec.DataNode.NdbPodSpec, newNc.Spec.DataNode.NdbPodSpec); err != nil {
errList = append(errList, err)
}
if nc.GetMySQLServerNodeCount() != 0 &&
newNc.GetMySQLServerNodeCount() != 0 {
if err := validateNdbPodSpecResources(
mysqldPath.Child("ndbPodSpec"), nc.Spec.MysqlNode.NdbPodSpec, newNc.Spec.MysqlNode.NdbPodSpec); err != nil {
errList = append(errList, err)
}
}
if nc.GetMySQLServerConnectionPoolSize() > newNc.GetMySQLServerConnectionPoolSize() {
// Do not allow reducing connection pool size as that leads to chaos when reserving nodeIds
errList = append(errList,
field.Invalid(mysqldPath.Child("connectionPoolSize"),
newNc.GetMySQLServerConnectionPoolSize(),
"connectionPoolSize cannot be reduced once MySQL Cluster has been started"))
}
// Check if the new NdbCluster valid is spec
if isValid, specErrList := newNc.HasValidSpec(); !isValid {
errList = append(errList, specErrList...)
}
return errList == nil, errList
}