sigs.k8s.io/cluster-api@v1.7.1/internal/topology/variables/clusterclass_variable_validation.go (about) 1 /* 2 Copyright 2021 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package variables 18 19 import ( 20 "context" 21 "fmt" 22 "strings" 23 24 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" 25 structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema" 26 structuraldefaulting "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/defaulting" 27 "k8s.io/apiextensions-apiserver/pkg/apiserver/validation" 28 apivalidation "k8s.io/apimachinery/pkg/api/validation" 29 metav1validation "k8s.io/apimachinery/pkg/apis/meta/v1/validation" 30 "k8s.io/apimachinery/pkg/util/sets" 31 "k8s.io/apimachinery/pkg/util/validation/field" 32 33 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 34 ) 35 36 const ( 37 // builtinsName is the name of the builtin variable. 38 builtinsName = "builtin" 39 ) 40 41 // ValidateClusterClassVariables validates clusterClassVariable. 42 func ValidateClusterClassVariables(ctx context.Context, clusterClassVariables []clusterv1.ClusterClassVariable, fldPath *field.Path) field.ErrorList { 43 var allErrs field.ErrorList 44 45 allErrs = append(allErrs, validateClusterClassVariableNamesUnique(clusterClassVariables, fldPath)...) 46 47 for i := range clusterClassVariables { 48 allErrs = append(allErrs, validateClusterClassVariable(ctx, &clusterClassVariables[i], fldPath.Index(i))...) 49 } 50 51 return allErrs 52 } 53 54 // validateClusterClassVariableNamesUnique validates that ClusterClass variable names are unique. 55 func validateClusterClassVariableNamesUnique(clusterClassVariables []clusterv1.ClusterClassVariable, pathPrefix *field.Path) field.ErrorList { 56 var allErrs field.ErrorList 57 58 variableNames := sets.Set[string]{} 59 for i, clusterClassVariable := range clusterClassVariables { 60 if variableNames.Has(clusterClassVariable.Name) { 61 allErrs = append(allErrs, 62 field.Invalid( 63 pathPrefix.Index(i).Child("name"), 64 clusterClassVariable.Name, 65 fmt.Sprintf("variable name must be unique. Variable with name %q is defined more than once", clusterClassVariable.Name), 66 ), 67 ) 68 } 69 variableNames.Insert(clusterClassVariable.Name) 70 } 71 72 return allErrs 73 } 74 75 // validateClusterClassVariable validates a ClusterClassVariable. 76 func validateClusterClassVariable(ctx context.Context, variable *clusterv1.ClusterClassVariable, fldPath *field.Path) field.ErrorList { 77 allErrs := field.ErrorList{} 78 79 // Validate variable name. 80 allErrs = append(allErrs, validateClusterClassVariableName(variable.Name, fldPath.Child("name"))...) 81 82 // Validate variable metadata. 83 allErrs = append(allErrs, validateClusterClassVariableMetadata(variable.Metadata, fldPath.Child("metadata"))...) 84 85 // Validate schema. 86 allErrs = append(allErrs, validateRootSchema(ctx, variable, fldPath.Child("schema", "openAPIV3Schema"))...) 87 88 return allErrs 89 } 90 91 // validateClusterClassVariableName validates a variable name. 92 func validateClusterClassVariableName(variableName string, fldPath *field.Path) field.ErrorList { 93 allErrs := field.ErrorList{} 94 95 if variableName == "" { 96 allErrs = append(allErrs, field.Required(fldPath, "variable name must be defined")) 97 } 98 99 if variableName == builtinsName { 100 allErrs = append(allErrs, field.Invalid(fldPath, variableName, fmt.Sprintf("%q is a reserved variable name", builtinsName))) 101 } 102 if strings.Contains(variableName, ".") { 103 allErrs = append(allErrs, field.Invalid(fldPath, variableName, "variable name cannot contain \".\"")) // TODO: consider if to restrict variable names to RFC 1123 104 } 105 106 return allErrs 107 } 108 109 // validateClusterClassVariableMetadata validates a variable metadata. 110 func validateClusterClassVariableMetadata(metadata clusterv1.ClusterClassVariableMetadata, fldPath *field.Path) field.ErrorList { 111 allErrs := metav1validation.ValidateLabels( 112 metadata.Labels, 113 fldPath.Child("labels"), 114 ) 115 allErrs = append(allErrs, apivalidation.ValidateAnnotations( 116 metadata.Annotations, 117 fldPath.Child("annotations"), 118 )...) 119 return allErrs 120 } 121 122 var validVariableTypes = sets.Set[string]{}.Insert("object", "array", "string", "number", "integer", "boolean") 123 124 // validateRootSchema validates the schema. 125 func validateRootSchema(ctx context.Context, clusterClassVariable *clusterv1.ClusterClassVariable, fldPath *field.Path) field.ErrorList { 126 var allErrs field.ErrorList 127 128 apiExtensionsSchema, allErrs := convertToAPIExtensionsJSONSchemaProps(&clusterClassVariable.Schema.OpenAPIV3Schema, field.NewPath("schema")) 129 if len(allErrs) > 0 { 130 return field.ErrorList{field.InternalError(fldPath, 131 fmt.Errorf("failed to convert schema definition for variable %q; ClusterClass should be checked: %v", clusterClassVariable.Name, allErrs))} // TODO: consider if to add ClusterClass name 132 } 133 134 // Validate structural schema. 135 // Note: structural schema only allows `type: object` on the root level, so we wrap the schema with: 136 // type: object 137 // properties: 138 // variableSchema: <variable-schema> 139 wrappedSchema := &apiextensions.JSONSchemaProps{ 140 Type: "object", 141 Properties: map[string]apiextensions.JSONSchemaProps{ 142 "variableSchema": *apiExtensionsSchema, 143 }, 144 } 145 146 // Get the structural schema for the variable. 147 ss, err := structuralschema.NewStructural(wrappedSchema) 148 if err != nil { 149 return append(allErrs, field.Invalid(fldPath.Child("schema"), "", err.Error())) 150 } 151 152 // Validate the schema. 153 if validationErrors := structuralschema.ValidateStructural(fldPath.Child("schema"), ss); len(validationErrors) > 0 { 154 return append(allErrs, validationErrors...) 155 } 156 157 // Validate defaults in the structural schema. 158 validationErrors, err := structuraldefaulting.ValidateDefaults(ctx, fldPath.Child("schema"), ss, true, true) 159 if err != nil { 160 return append(allErrs, field.Invalid(fldPath.Child("schema"), "", err.Error())) 161 } 162 if len(validationErrors) > 0 { 163 return append(allErrs, validationErrors...) 164 } 165 166 // If the structural schema is valid, ensure a schema validator can be constructed. 167 if _, _, err := validation.NewSchemaValidator(apiExtensionsSchema); err != nil { 168 return append(allErrs, field.Invalid(fldPath, "", fmt.Sprintf("failed to build validator: %v", err))) 169 } 170 171 allErrs = append(allErrs, validateSchema(apiExtensionsSchema, fldPath)...) 172 return allErrs 173 } 174 175 func validateSchema(schema *apiextensions.JSONSchemaProps, fldPath *field.Path) field.ErrorList { 176 var allErrs field.ErrorList 177 178 // Validate that type is one of the validVariableTypes. 179 switch { 180 case schema.Type == "": 181 return field.ErrorList{field.Required(fldPath.Child("type"), "type cannot be empty")} 182 case !validVariableTypes.Has(schema.Type): 183 return field.ErrorList{field.NotSupported(fldPath.Child("type"), schema.Type, sets.List(validVariableTypes))} 184 } 185 186 // If the structural schema is valid, ensure a schema validator can be constructed. 187 validator, _, err := validation.NewSchemaValidator(schema) 188 if err != nil { 189 return append(allErrs, field.Invalid(fldPath, "", fmt.Sprintf("failed to build validator: %v", err))) 190 } 191 192 if schema.Example != nil { 193 if errs := validation.ValidateCustomResource(fldPath, *schema.Example, validator); len(errs) > 0 { 194 allErrs = append(allErrs, field.Invalid(fldPath.Child("example"), schema.Example, fmt.Sprintf("invalid value in example: %v", errs))) 195 } 196 } 197 198 for i, enum := range schema.Enum { 199 if enum != nil { 200 if errs := validation.ValidateCustomResource(fldPath, enum, validator); len(errs) > 0 { 201 allErrs = append(allErrs, field.Invalid(fldPath.Child("enum").Index(i), enum, fmt.Sprintf("invalid value in enum: %v", errs))) 202 } 203 } 204 } 205 206 if schema.AdditionalProperties != nil { 207 if len(schema.Properties) > 0 { 208 allErrs = append(allErrs, field.Forbidden(fldPath.Child("additionalProperties"), "additionalProperties and properties are mutual exclusive")) 209 } 210 allErrs = append(allErrs, validateSchema(schema.AdditionalProperties.Schema, fldPath.Child("additionalProperties"))...) 211 } 212 213 for propertyName, propertySchema := range schema.Properties { 214 p := propertySchema 215 allErrs = append(allErrs, validateSchema(&p, fldPath.Child("properties").Key(propertyName))...) 216 } 217 218 if schema.Items != nil { 219 allErrs = append(allErrs, validateSchema(schema.Items.Schema, fldPath.Child("items"))...) 220 } 221 222 return allErrs 223 }