sigs.k8s.io/cluster-api@v1.6.3/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 "k8s.io/apimachinery/pkg/util/sets" 29 "k8s.io/apimachinery/pkg/util/validation/field" 30 31 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 32 ) 33 34 const ( 35 // builtinsName is the name of the builtin variable. 36 builtinsName = "builtin" 37 ) 38 39 // ValidateClusterClassVariables validates clusterClassVariable. 40 func ValidateClusterClassVariables(ctx context.Context, clusterClassVariables []clusterv1.ClusterClassVariable, fldPath *field.Path) field.ErrorList { 41 var allErrs field.ErrorList 42 43 allErrs = append(allErrs, validateClusterClassVariableNamesUnique(clusterClassVariables, fldPath)...) 44 45 for i := range clusterClassVariables { 46 allErrs = append(allErrs, validateClusterClassVariable(ctx, &clusterClassVariables[i], fldPath.Index(i))...) 47 } 48 49 return allErrs 50 } 51 52 // validateClusterClassVariableNamesUnique validates that ClusterClass variable names are unique. 53 func validateClusterClassVariableNamesUnique(clusterClassVariables []clusterv1.ClusterClassVariable, pathPrefix *field.Path) field.ErrorList { 54 var allErrs field.ErrorList 55 56 variableNames := sets.Set[string]{} 57 for i, clusterClassVariable := range clusterClassVariables { 58 if variableNames.Has(clusterClassVariable.Name) { 59 allErrs = append(allErrs, 60 field.Invalid( 61 pathPrefix.Index(i).Child("name"), 62 clusterClassVariable.Name, 63 fmt.Sprintf("variable name must be unique. Variable with name %q is defined more than once", clusterClassVariable.Name), 64 ), 65 ) 66 } 67 variableNames.Insert(clusterClassVariable.Name) 68 } 69 70 return allErrs 71 } 72 73 // validateClusterClassVariable validates a ClusterClassVariable. 74 func validateClusterClassVariable(ctx context.Context, variable *clusterv1.ClusterClassVariable, fldPath *field.Path) field.ErrorList { 75 allErrs := field.ErrorList{} 76 77 // Validate variable name. 78 allErrs = append(allErrs, validateClusterClassVariableName(variable.Name, fldPath.Child("name"))...) 79 80 // Validate schema. 81 allErrs = append(allErrs, validateRootSchema(ctx, variable, fldPath.Child("schema", "openAPIV3Schema"))...) 82 83 return allErrs 84 } 85 86 // validateClusterClassVariableName validates a variable name. 87 func validateClusterClassVariableName(variableName string, fldPath *field.Path) field.ErrorList { 88 allErrs := field.ErrorList{} 89 90 if variableName == "" { 91 allErrs = append(allErrs, field.Required(fldPath, "variable name must be defined")) 92 } 93 94 if variableName == builtinsName { 95 allErrs = append(allErrs, field.Invalid(fldPath, variableName, fmt.Sprintf("%q is a reserved variable name", builtinsName))) 96 } 97 if strings.Contains(variableName, ".") { 98 allErrs = append(allErrs, field.Invalid(fldPath, variableName, "variable name cannot contain \".\"")) // TODO: consider if to restrict variable names to RFC 1123 99 } 100 101 return allErrs 102 } 103 104 var validVariableTypes = sets.Set[string]{}.Insert("object", "array", "string", "number", "integer", "boolean") 105 106 // validateRootSchema validates the schema. 107 func validateRootSchema(ctx context.Context, clusterClassVariable *clusterv1.ClusterClassVariable, fldPath *field.Path) field.ErrorList { 108 var allErrs field.ErrorList 109 110 apiExtensionsSchema, allErrs := convertToAPIExtensionsJSONSchemaProps(&clusterClassVariable.Schema.OpenAPIV3Schema, field.NewPath("schema")) 111 if len(allErrs) > 0 { 112 return field.ErrorList{field.InternalError(fldPath, 113 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 114 } 115 116 // Validate structural schema. 117 // Note: structural schema only allows `type: object` on the root level, so we wrap the schema with: 118 // type: object 119 // properties: 120 // variableSchema: <variable-schema> 121 wrappedSchema := &apiextensions.JSONSchemaProps{ 122 Type: "object", 123 Properties: map[string]apiextensions.JSONSchemaProps{ 124 "variableSchema": *apiExtensionsSchema, 125 }, 126 } 127 128 // Get the structural schema for the variable. 129 ss, err := structuralschema.NewStructural(wrappedSchema) 130 if err != nil { 131 return append(allErrs, field.Invalid(fldPath.Child("schema"), "", err.Error())) 132 } 133 134 // Validate the schema. 135 if validationErrors := structuralschema.ValidateStructural(fldPath.Child("schema"), ss); len(validationErrors) > 0 { 136 return append(allErrs, validationErrors...) 137 } 138 139 // Validate defaults in the structural schema. 140 validationErrors, err := structuraldefaulting.ValidateDefaults(ctx, fldPath.Child("schema"), ss, true, true) 141 if err != nil { 142 return append(allErrs, field.Invalid(fldPath.Child("schema"), "", err.Error())) 143 } 144 if len(validationErrors) > 0 { 145 return append(allErrs, validationErrors...) 146 } 147 148 // If the structural schema is valid, ensure a schema validator can be constructed. 149 if _, _, err := validation.NewSchemaValidator(apiExtensionsSchema); err != nil { 150 return append(allErrs, field.Invalid(fldPath, "", fmt.Sprintf("failed to build validator: %v", err))) 151 } 152 153 allErrs = append(allErrs, validateSchema(apiExtensionsSchema, fldPath)...) 154 return allErrs 155 } 156 157 func validateSchema(schema *apiextensions.JSONSchemaProps, fldPath *field.Path) field.ErrorList { 158 var allErrs field.ErrorList 159 160 // Validate that type is one of the validVariableTypes. 161 switch { 162 case schema.Type == "": 163 return field.ErrorList{field.Required(fldPath.Child("type"), "type cannot be empty")} 164 case !validVariableTypes.Has(schema.Type): 165 return field.ErrorList{field.NotSupported(fldPath.Child("type"), schema.Type, sets.List(validVariableTypes))} 166 } 167 168 // If the structural schema is valid, ensure a schema validator can be constructed. 169 validator, _, err := validation.NewSchemaValidator(schema) 170 if err != nil { 171 return append(allErrs, field.Invalid(fldPath, "", fmt.Sprintf("failed to build validator: %v", err))) 172 } 173 174 if schema.Example != nil { 175 if errs := validation.ValidateCustomResource(fldPath, *schema.Example, validator); len(errs) > 0 { 176 allErrs = append(allErrs, field.Invalid(fldPath.Child("example"), schema.Example, fmt.Sprintf("invalid value in example: %v", errs))) 177 } 178 } 179 180 for i, enum := range schema.Enum { 181 if enum != nil { 182 if errs := validation.ValidateCustomResource(fldPath, enum, validator); len(errs) > 0 { 183 allErrs = append(allErrs, field.Invalid(fldPath.Child("enum").Index(i), enum, fmt.Sprintf("invalid value in enum: %v", errs))) 184 } 185 } 186 } 187 188 if schema.AdditionalProperties != nil { 189 if len(schema.Properties) > 0 { 190 allErrs = append(allErrs, field.Forbidden(fldPath.Child("additionalProperties"), "additionalProperties and properties are mutual exclusive")) 191 } 192 allErrs = append(allErrs, validateSchema(schema.AdditionalProperties.Schema, fldPath.Child("additionalProperties"))...) 193 } 194 195 for propertyName, propertySchema := range schema.Properties { 196 p := propertySchema 197 allErrs = append(allErrs, validateSchema(&p, fldPath.Child("properties").Key(propertyName))...) 198 } 199 200 if schema.Items != nil { 201 allErrs = append(allErrs, validateSchema(schema.Items.Schema, fldPath.Child("items"))...) 202 } 203 204 return allErrs 205 }