func validateNamingScheme(
	cr *kdv1.KubeDirectorCluster,
	appCR *kdv1.KubeDirectorApp,
	patches []clusterPatchSpec,
) []clusterPatchSpec {
	if cr.Spec.NamingScheme == nil {
		namingScheme := shared.GetDefaultNamingScheme()
		cr.Spec.NamingScheme = &namingScheme
		patches = append(
			patches,
			clusterPatchSpec{
				Op:   "add",
				Path: "/spec/namingScheme",
				Value: clusterPatchValue{
					ValueStr: cr.Spec.NamingScheme,
				},
			},
		)
	}
	return patches
}
If the naming scheme is unspecified, use a global constant for the naming scheme.
package validator
import (
	"context"
	"crypto/tls"
	"encoding/json"
	"fmt"
	"net/http"
	"strconv"
	"strings"
	"time"
	kdv1 "github.com/bluek8s/kubedirector/pkg/apis/kubedirector/v1beta1"
	"github.com/bluek8s/kubedirector/pkg/catalog"
	"github.com/bluek8s/kubedirector/pkg/controller/kubedirectorcluster"
	"github.com/bluek8s/kubedirector/pkg/observer"
	"github.com/bluek8s/kubedirector/pkg/secretkeys"
	"github.com/bluek8s/kubedirector/pkg/shared"
	"k8s.io/api/admission/v1beta1"
	v1 "k8s.io/api/authentication/v1"
	sar "k8s.io/api/authorization/v1"
	"k8s.io/apimachinery/pkg/api/equality"
	"k8s.io/apimachinery/pkg/api/resource"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/util/validation/field"
)
type secretValidateResult int
const maxKDMembers = 1000
const (
	secretIsValid secretValidateResult = iota
	secretPrefixNotMatched
	secretNotFound
)
type clusterPatchSpec struct {
	Op    string            `json:"op"`
	Path  string            `json:"path"`
	Value clusterPatchValue `json:"value"`
}
type clusterPatchValue struct {
	ValueInt           *int32
	ValueStr           *string
	ValueClusterStatus *kdv1.KubeDirectorClusterStatus
	ValueKDSecret      *kdv1.KDSecret
	ValueSecretKey     *kdv1.SecretKey
	ValueDict          *dictValue
}
func (obj clusterPatchValue) MarshalJSON() ([]byte, error) {
	if obj.ValueInt != nil {
		return json.Marshal(obj.ValueInt)
	}
	if obj.ValueKDSecret != nil {
		return json.Marshal(obj.ValueKDSecret)
	}
	if obj.ValueClusterStatus != nil {
		return json.Marshal(obj.ValueClusterStatus)
	}
	if obj.ValueSecretKey != nil {
		return json.Marshal(obj.ValueSecretKey)
	}
	if obj.ValueDict != nil {
		return json.Marshal(obj.ValueDict)
	}
	return json.Marshal(obj.ValueStr)
}
func validateSpecChange(
	cr *kdv1.KubeDirectorCluster,
	prevCr *kdv1.KubeDirectorCluster,
	valErrors []string,
	patches []clusterPatchSpec,
) ([]string, []clusterPatchSpec) {
	if cr.Status == nil {
		valErrors = append(
			valErrors,
			multipleSpecChange,
		)
		return valErrors, patches
	}
	for _, roleStatus := range cr.Status.Roles {
		for _, memberStatus := range roleStatus.Members {
			if len(memberStatus.StateDetail.PendingNotifyCmds) != 0 {
				valErrors = append(
					valErrors,
					pendingNotifies,
				)
				return valErrors, patches
			}
		}
	}
	stringStateModified := string(kubedirectorcluster.ClusterSpecModified)
	if cr.Status.State == stringStateModified {
		valErrors = append(
			valErrors,
			multipleSpecChange,
		)
		return valErrors, patches
	}
	newState := stringStateModified
	patches = append(
		patches,
		clusterPatchSpec{
			Op:   "replace",
			Path: "/status/state",
			Value: clusterPatchValue{
				ValueStr: &newState,
			},
		},
	)
	return valErrors, patches
}
func validateCardinality(
	cr *kdv1.KubeDirectorCluster,
	appCR *kdv1.KubeDirectorApp,
	valErrors []string,
	patches []clusterPatchSpec,
) ([]string, []clusterPatchSpec) {
	anyError := false
	totalMembers := int32(0)
	numRoles := len(cr.Spec.Roles)
	rolesPath := field.NewPath("spec", "roles")
	for i := 0; i < numRoles; i++ {
		role := &(cr.Spec.Roles[i])
		appRole := catalog.GetRoleFromID(appCR, role.Name)
		if appRole == nil {
			continue
		}
		cardinality, isScaleOut := catalog.GetRoleCardinality(appRole)
		if role.Members != nil {
			var invalidMemberCount = false
			if isScaleOut {
				if *(role.Members) < cardinality {
					invalidMemberCount = true
				}
			} else {
				if *(role.Members) != cardinality {
					invalidMemberCount = true
				}
			}
			if invalidMemberCount {
				anyError = true
				valErrors = append(
					valErrors,
					fmt.Sprintf(
						invalidCardinality,
						role.Name,
						*(role.Members),
						appRole.Cardinality,
					),
				)
			}
		} else {
			role.Members = &cardinality
			patches = append(
				patches,
				clusterPatchSpec{
					Op:   "add",
					Path: "/spec/roles/" + strconv.Itoa(i) + "/members",
					Value: clusterPatchValue{
						ValueInt: role.Members,
					},
				},
			)
		}
		totalMembers += *role.Members
		if totalMembers > maxKDMembers {
			anyError = true
			valErrors = append(
				valErrors,
				fmt.Sprint(
					maxMemberLimit,
					maxKDMembers,
				),
			)
			break
		}
		var anyLabelAnnError bool
		valErrors, anyLabelAnnError = validateLabelsAndAnnotations(
			rolesPath.Index(i),
			role.PodLabels,
			role.PodAnnotations,
			role.ServiceLabels,
			role.ServiceAnnotations,
			valErrors,
		)
		anyError = anyError || anyLabelAnnError
	}
	if anyError {
		var emptyPatchList []clusterPatchSpec
		return valErrors, emptyPatchList
	}
	return valErrors, patches
}
func validateClusterRoles(
	cr *kdv1.KubeDirectorCluster,
	appCR *kdv1.KubeDirectorApp,
	valErrors []string,
) []string {
	var configuredRoles []string
	allRoles := catalog.GetAllRoleIDs(appCR)
	roleSeen := make(map[string]bool)
	uniqueRoles := true
	for _, role := range cr.Spec.Roles {
		if shared.StringInList(role.Name, allRoles) {
			configuredRoles = append(configuredRoles, role.Name)
		} else {
			invalidRoleMsg := fmt.Sprintf(
				invalidRole,
				role.Name,
				appCR.Name,
				strings.Join(allRoles, ","),
			)
			valErrors = append(valErrors, invalidRoleMsg)
		}
		if _, ok := roleSeen[role.Name]; ok {
			uniqueRoles = false
		}
		roleSeen[role.Name] = true
	}
	if !uniqueRoles {
		valErrors = append(valErrors, nonUniqueRoleID)
	}
	for _, activeRole := range catalog.GetSelectedRoleIDs(appCR) {
		if !shared.StringInList(activeRole, configuredRoles) {
			role := catalog.GetRoleFromID(appCR, activeRole)
			if role != nil {
				validMin, _ := catalog.GetRoleCardinality(role)
				if validMin != 0 {
					unconfiguredRoleMsg := fmt.Sprintf(
						unconfiguredRole,
						activeRole,
						appCR.Name,
					)
					valErrors = append(valErrors, unconfiguredRoleMsg)
				}
			}
		}
	}
	return valErrors
}
func validateGeneralClusterChanges(
	cr *kdv1.KubeDirectorCluster,
	prevCr *kdv1.KubeDirectorCluster,
	valErrors []string,
) []string {
	if cr.Spec.AppID != prevCr.Spec.AppID {
		appModifiedMsg := fmt.Sprintf(
			modifiedProperty,
			"app",
		)
		valErrors = append(valErrors, appModifiedMsg)
	}
	appCatalogMatch := true
	if cr.Spec.AppCatalog != nil {
		if prevCr.Spec.AppCatalog != nil {
			appCatalogMatch = (*(cr.Spec.AppCatalog) == *(prevCr.Spec.AppCatalog))
		} else {
			appCatalogMatch = false
		}
	} else {
		appCatalogMatch = (prevCr.Spec.AppCatalog == nil)
	}
	if !appCatalogMatch {
		appCatalogModifiedMsg := fmt.Sprintf(
			modifiedProperty,
			"appCatalog",
		)
		valErrors = append(valErrors, appCatalogModifiedMsg)
	}
	return valErrors
}
func validateRoleChanges(
	cr *kdv1.KubeDirectorCluster,
	prevCr *kdv1.KubeDirectorCluster,
	valErrors []string,
) []string {
	prevRoles := make(map[string]*kdv1.Role)
	numPrevRoles := len(prevCr.Spec.Roles)
	for i := 0; i < numPrevRoles; i++ {
		p := &(prevCr.Spec.Roles[i])
		prevRoles[p.Name] = p
	}
	prevRoleHasStatus := make(map[string]bool)
	if prevCr.Status != nil {
		for _, s := range prevCr.Status.Roles {
			prevRoleHasStatus[s.Name] = true
		}
	}
	numRoles := len(cr.Spec.Roles)
	for i := 0; i < numRoles; i++ {
		role := &(cr.Spec.Roles[i])
		if _, ok := prevRoleHasStatus[role.Name]; !ok {
			continue
		}
		prevRole, hasPrevRole := prevRoles[role.Name]
		if !hasPrevRole {
			roleModifiedMsg := fmt.Sprintf(
				modifiedRole,
				role.Name,
			)
			valErrors = append(valErrors, roleModifiedMsg)
			continue
		}
		compareRole := *role
		compareRole.Members = prevRole.Members
		if !equality.Semantic.DeepEqual(&compareRole, prevRole) {
			roleModifiedMsg := fmt.Sprintf(
				modifiedRole,
				role.Name,
			)
			valErrors = append(valErrors, roleModifiedMsg)
		}
	}
	return valErrors
}
func validateRoleStorageClass(
	cr *kdv1.KubeDirectorCluster,
	valErrors []string,
	patches []clusterPatchSpec,
) ([]string, []clusterPatchSpec) {
	var validateDefault = false
	var missingDefault = false
	globalStorageClass := shared.GetDefaultStorageClass()
	numRoles := len(cr.Spec.Roles)
	for i := 0; i < numRoles; i++ {
		role := &(cr.Spec.Roles[i])
		if role.Storage == nil {
			continue
		}
		storageSize, err := resource.ParseQuantity(role.Storage.Size)
		if err != nil {
			valErrors = append(
				valErrors,
				fmt.Sprintf(
					invalidStorageDef,
					role.Name,
				),
			)
			break
		}
		if storageSize.Sign() != 1 {
			valErrors = append(
				valErrors,
				fmt.Sprintf(
					invalidStorageSize,
					role.Name,
				),
			)
			break
		}
		storageClass := role.Storage.StorageClass
		if storageClass != nil {
			_, scErr := observer.GetStorageClass(*storageClass)
			if scErr != nil {
				valErrors = append(
					valErrors,
					fmt.Sprintf(
						invalidRoleStorageClass,
						*storageClass,
						role.Name,
					),
				)
			}
			continue
		}
		if len(globalStorageClass) > 0 {
			validateDefault = true
			role.Storage.StorageClass = &globalStorageClass
		} else {
			scK8sDefault, _ := observer.GetDefaultStorageClass()
			if scK8sDefault == nil {
				missingDefault = true
				continue
			}
			role.Storage.StorageClass = &(scK8sDefault.Name)
		}
		if !missingDefault {
			patches = append(
				patches,
				clusterPatchSpec{
					Op:   "add",
					Path: "/spec/roles/" + strconv.Itoa(i) + "/storage/storageClassName",
					Value: clusterPatchValue{
						ValueStr: role.Storage.StorageClass,
					},
				},
			)
		}
	}
	if missingDefault {
		valErrors = append(
			valErrors,
			noDefaultStorageClass,
		)
	} else if validateDefault {
		_, err := observer.GetStorageClass(globalStorageClass)
		if err != nil {
			valErrors = append(
				valErrors,
				fmt.Sprintf(
					badDefaultStorageClass,
					globalStorageClass,
				),
			)
		}
	}
	return valErrors, patches
}
func validateRoleServiceAccount(
	cr *kdv1.KubeDirectorCluster,
	valErrs []string,
	userInfo v1.UserInfo,
) []string {
	numRoles := len(cr.Spec.Roles)
	for i := 0; i < numRoles; i++ {
		role := &(cr.Spec.Roles[i])
		if role.ServiceAccountName == "" {
			continue
		}
		_, erro := observer.GetServiceAccount(cr.Namespace, role.ServiceAccountName)
		if erro != nil {
			valErrs = append(valErrs,
				"service account "+role.ServiceAccountName+" requested by role "+role.Name+" does not exist")
			continue
		}
		xtra := make(map[string]sar.ExtraValue)
		for k, v := range userInfo.Extra {
			xtra[k] = sar.ExtraValue(v)
		}
		sar := &sar.SubjectAccessReview{
			TypeMeta: metav1.TypeMeta{
				Kind:       "SubjectAccessReview",
				APIVersion: "authorization.k8s.io/v1",
			},
			Spec: sar.SubjectAccessReviewSpec{
				ResourceAttributes: &sar.ResourceAttributes{
					Namespace: cr.Namespace,
					Verb:      "get",
					Resource:  "ServiceAccount",
					Name:      role.ServiceAccountName,
				},
				User:   userInfo.Username,
				Groups: userInfo.Groups,
				UID:    userInfo.UID,
				Extra:  xtra,
			},
		}
		err := shared.Create(context.TODO(), sar)
		if err != nil {
			valErrs = append(valErrs, err.Error())
		} else {
			if sar.Status.Denied {
				valErrs = append(valErrs, sar.Status.Reason)
			}
		}
	}
	return valErrs
}
func validateApp(
	cr *kdv1.KubeDirectorCluster,
	patches []clusterPatchSpec,
) (*kdv1.KubeDirectorApp, []clusterPatchSpec, string) {
	appCR, err := catalog.FindApp(cr)
	if err != nil {
		return nil, patches,
			fmt.Sprintf(invalidAppMessage, cr.Spec.AppID)
	}
	if cr.Spec.AppCatalog != nil {
		return appCR, patches, ""
	}
	var appCatalog string
	if appCR.Namespace == cr.Namespace {
		appCatalog = shared.AppCatalogLocal
	} else {
		appCatalog = shared.AppCatalogSystem
	}
	patches = append(
		patches,
		clusterPatchSpec{
			Op:   "add",
			Path: "/spec/appCatalog",
			Value: clusterPatchValue{
				ValueStr: &appCatalog,
			},
		},
	)
	return appCR, patches, ""
}
func validateMinResources(
	cr *kdv1.KubeDirectorCluster,
	appCR *kdv1.KubeDirectorApp,
	valErrors []string,
) []string {
	numRoles := len(cr.Spec.Roles)
	for i := 0; i < numRoles; i++ {
		role := &(cr.Spec.Roles[i])
		appRole := catalog.GetRoleFromID(appCR, role.Name)
		if appRole == nil {
			continue
		}
		minResources := catalog.GetRoleMinResources(appRole)
		if minResources == nil {
			continue
		}
		logError := func(
			resName string,
			resValue string,
			roleName string,
			expValue string,
			valErrors []string) []string {
			return append(
				valErrors,
				fmt.Sprintf(
					invalidResource,
					resName,
					resValue,
					roleName,
					expValue,
				),
			)
		}
		for resKey, resVal := range *minResources {
			if resVal.IsZero() {
				continue
			}
			if limit, ok := role.Resources.Requests[resKey]; ok {
				if limit.Value() < resVal.Value() {
					valErrors = logError(resKey.String(), limit.String(), role.Name, resVal.String(), valErrors)
				}
			} else {
				valErrors = logError(resKey.String(), "0", role.Name, resVal.String(), valErrors)
			}
		}
	}
	return valErrors
}
func validateFileInjections(
	cr *kdv1.KubeDirectorCluster,
	valErrors []string,
	patches []clusterPatchSpec,
) ([]string, []clusterPatchSpec) {
	numRoles := len(cr.Spec.Roles)
	for i := 0; i < numRoles; i++ {
		role := &(cr.Spec.Roles[i])
		if len(role.FileInjections) == 0 {
			continue
		}
		numInjections := len(role.FileInjections)
		for j := 0; j < numInjections; j++ {
			fileInjection := role.FileInjections[j]
			srcURL := fileInjection.SrcURL
			tr := &http.Transport{
				TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
			}
			client := &http.Client{Transport: tr, Timeout: 15 * time.Second}
			_, headErr := client.Head(srcURL)
			if headErr != nil {
				valErrors = append(
					valErrors,
					fmt.Sprintf(
						invalidSrcURL,
						srcURL,
						role.Name,
						headErr,
					),
				)
				continue
			}
		}
	}
	return valErrors, patches
}
func validateSecrets(
	cr *kdv1.KubeDirectorCluster,
	valErrors []string,
	patches []clusterPatchSpec,
) ([]string, []clusterPatchSpec) {
	requiredNamePrefix := shared.GetRequiredSecretPrefix()
	validateFunc := func(
		secretName string,
	) secretValidateResult {
		if strings.HasPrefix(secretName, requiredNamePrefix) {
			_, fetchErr := observer.GetSecret(
				cr.Namespace,
				secretName,
			)
			if fetchErr != nil {
				return secretNotFound
			}
		} else {
			return secretPrefixNotMatched
		}
		return secretIsValid
	}
	defaultSecret := cr.Spec.DefaultSecret
	if defaultSecret != nil {
		defaultSecretValidateResult := validateFunc(defaultSecret.Name)
		if defaultSecretValidateResult == secretPrefixNotMatched {
			valErrors = append(
				valErrors,
				fmt.Sprintf(
					invalidDefaultSecretPrefix,
					defaultSecret.Name,
					requiredNamePrefix,
				),
			)
			return valErrors, patches
		}
		if defaultSecretValidateResult == secretNotFound {
			valErrors = append(
				valErrors,
				fmt.Sprintf(
					invalidDefaultSecret,
					defaultSecret.Name,
					cr.Namespace,
				),
			)
			return valErrors, patches
		}
	}
	numRoles := len(cr.Spec.Roles)
	for i := 0; i < numRoles; i++ {
		role := &(cr.Spec.Roles[i])
		if role.Secret != nil {
			secretValidateResult := validateFunc(role.Secret.Name)
			if secretValidateResult == secretPrefixNotMatched {
				valErrors = append(
					valErrors,
					fmt.Sprintf(
						invalidSecretPrefix,
						role.Secret.Name,
						role.Name,
						requiredNamePrefix,
					),
				)
				continue
			}
			if secretValidateResult == secretNotFound {
				valErrors = append(
					valErrors,
					fmt.Sprintf(
						invalidSecret,
						role.Secret.Name,
						role.Name,
						requiredNamePrefix,
					),
				)
				continue
			}
		}
		if role.Secret == nil && cr.Spec.DefaultSecret != nil {
			patches = append(
				patches,
				clusterPatchSpec{
					Op:   "add",
					Path: "/spec/roles/" + strconv.Itoa(i) + "/secret",
					Value: clusterPatchValue{
						ValueKDSecret: defaultSecret,
					},
				},
			)
		}
	}
	return valErrors, patches
}
func encryptSecretKeys(
	cr *kdv1.KubeDirectorCluster,
	prevCr *kdv1.KubeDirectorCluster,
	valErrors []string,
	patches []clusterPatchSpec,
) ([]string, []clusterPatchSpec) {
	for roleIndex, role := range cr.Spec.Roles {
		prevEncryptedValues := map[string]string{}
		for _, prevRole := range prevCr.Spec.Roles {
			if prevRole.Name == role.Name {
				for _, secretKey := range prevRole.SecretKeys {
					prevEncryptedValues[secretKey.Name] = secretKey.EncryptedValue
				}
			}
		}
		for secretKeyIndex, secretKey := range role.SecretKeys {
			if secretKey.Value == "" && secretKey.EncryptedValue != "" {
				if secretKey.EncryptedValue != prevEncryptedValues[secretKey.Name] {
					valErrors = append(valErrors,
						fmt.Sprintf(forbiddenManualSecretKeyEncryptedValuePlacement, secretKey.Name),
					)
				}
				continue
			}
			encryptedValue, err := secretkeys.Encrypt(secretKey.Value)
			if err != nil {
				validatorLog.Error(err, "cannot encrypt secret key",
					"namespace", cr.Namespace, "kdcluster", cr.Name,
					"role", role.Name,
					"secret key", secretKeyIndex)
				valErrors = append(valErrors,
					fmt.Sprintf(failedSecretKeyEncryption, secretKey.Name),
				)
				continue
			}
			patches = append(patches, clusterPatchSpec{
				Op:   "replace",
				Path: "/spec/roles/" + strconv.Itoa(roleIndex) + "/secretKeys/" + strconv.Itoa(secretKeyIndex),
				Value: clusterPatchValue{
					ValueSecretKey: &kdv1.SecretKey{
						Name:           secretKey.Name,
						EncryptedValue: encryptedValue,
					},
				},
			})
		}
	}
	return valErrors, patches
}
func addServiceType(
	cr *kdv1.KubeDirectorCluster,
	valErrors []string,
	patches []clusterPatchSpec,
) ([]string, []clusterPatchSpec) {
	if cr.Spec.ServiceType != nil {
		return valErrors, patches
	}
	serviceType := shared.GetDefaultServiceType()
	cr.Spec.ServiceType = &serviceType
	patches = append(
		patches,
		clusterPatchSpec{
			Op:   "add",
			Path: "/spec/serviceType",
			Value: clusterPatchValue{
				ValueStr: cr.Spec.ServiceType,
			},
		},
	)
	return valErrors, patches
}