k8s.io/apiserver@v0.31.1/pkg/admission/plugin/policy/validating/typechecking.go (about) 1 /* 2 Copyright 2023 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 validating 18 19 import ( 20 "errors" 21 "fmt" 22 "sort" 23 "strings" 24 "time" 25 26 "github.com/google/cel-go/cel" 27 28 "k8s.io/api/admissionregistration/v1" 29 "k8s.io/apimachinery/pkg/api/meta" 30 "k8s.io/apimachinery/pkg/runtime/schema" 31 utilruntime "k8s.io/apimachinery/pkg/util/runtime" 32 "k8s.io/apimachinery/pkg/util/sets" 33 "k8s.io/apimachinery/pkg/util/validation/field" 34 "k8s.io/apimachinery/pkg/util/version" 35 plugincel "k8s.io/apiserver/pkg/admission/plugin/cel" 36 apiservercel "k8s.io/apiserver/pkg/cel" 37 "k8s.io/apiserver/pkg/cel/common" 38 "k8s.io/apiserver/pkg/cel/environment" 39 "k8s.io/apiserver/pkg/cel/library" 40 "k8s.io/apiserver/pkg/cel/openapi" 41 "k8s.io/apiserver/pkg/cel/openapi/resolver" 42 "k8s.io/apiserver/pkg/features" 43 utilfeature "k8s.io/apiserver/pkg/util/feature" 44 "k8s.io/klog/v2" 45 ) 46 47 const maxTypesToCheck = 10 48 49 type TypeChecker struct { 50 SchemaResolver resolver.SchemaResolver 51 RestMapper meta.RESTMapper 52 } 53 54 // TypeCheckingContext holds information about the policy being type-checked. 55 // The struct is opaque to the caller. 56 type TypeCheckingContext struct { 57 gvks []schema.GroupVersionKind 58 declTypes []*apiservercel.DeclType 59 paramGVK schema.GroupVersionKind 60 paramDeclType *apiservercel.DeclType 61 62 variables []v1.Variable 63 } 64 65 type typeOverwrite struct { 66 object *apiservercel.DeclType 67 params *apiservercel.DeclType 68 } 69 70 // TypeCheckingResult holds the issues found during type checking, any returned 71 // error, and the gvk that the type checking is performed against. 72 type TypeCheckingResult struct { 73 // GVK is the associated GVK 74 GVK schema.GroupVersionKind 75 // Issues contain machine-readable information about the typechecking result. 76 Issues error 77 // Err is the possible error that was encounter during type checking. 78 Err error 79 } 80 81 // TypeCheckingResults is a collection of TypeCheckingResult 82 type TypeCheckingResults []*TypeCheckingResult 83 84 func (rs TypeCheckingResults) String() string { 85 var messages []string 86 for _, r := range rs { 87 message := r.String() 88 if message != "" { 89 messages = append(messages, message) 90 } 91 } 92 return strings.Join(messages, "\n") 93 } 94 95 // String converts the result to human-readable form as a string. 96 func (r *TypeCheckingResult) String() string { 97 if r.Issues == nil && r.Err == nil { 98 return "" 99 } 100 if r.Err != nil { 101 return fmt.Sprintf("%v: type checking error: %v\n", r.GVK, r.Err) 102 } 103 return fmt.Sprintf("%v: %s\n", r.GVK, r.Issues) 104 } 105 106 // Check preforms the type check against the given policy, and format the result 107 // as []ExpressionWarning that is ready to be set in policy.Status 108 // The result is nil if type checking returns no warning. 109 // The policy object is NOT mutated. The caller should update Status accordingly 110 func (c *TypeChecker) Check(policy *v1.ValidatingAdmissionPolicy) []v1.ExpressionWarning { 111 ctx := c.CreateContext(policy) 112 113 // warnings to return, note that the capacity is optimistically set to zero 114 var warnings []v1.ExpressionWarning // intentionally not setting capacity 115 116 // check main validation expressions and their message expressions, located in spec.validations[*] 117 fieldRef := field.NewPath("spec", "validations") 118 for i, v := range policy.Spec.Validations { 119 results := c.CheckExpression(ctx, v.Expression) 120 if len(results) != 0 { 121 warnings = append(warnings, v1.ExpressionWarning{ 122 FieldRef: fieldRef.Index(i).Child("expression").String(), 123 Warning: results.String(), 124 }) 125 } 126 // Note that MessageExpression is optional 127 if v.MessageExpression == "" { 128 continue 129 } 130 results = c.CheckExpression(ctx, v.MessageExpression) 131 if len(results) != 0 { 132 warnings = append(warnings, v1.ExpressionWarning{ 133 FieldRef: fieldRef.Index(i).Child("messageExpression").String(), 134 Warning: results.String(), 135 }) 136 } 137 } 138 139 return warnings 140 } 141 142 // CreateContext resolves all types and their schemas from a policy definition and creates the context. 143 func (c *TypeChecker) CreateContext(policy *v1.ValidatingAdmissionPolicy) *TypeCheckingContext { 144 ctx := new(TypeCheckingContext) 145 allGvks := c.typesToCheck(policy) 146 gvks := make([]schema.GroupVersionKind, 0, len(allGvks)) 147 declTypes := make([]*apiservercel.DeclType, 0, len(allGvks)) 148 for _, gvk := range allGvks { 149 declType, err := c.declType(gvk) 150 if err != nil { 151 // type checking errors MUST NOT alter the behavior of the policy 152 // even if an error occurs. 153 if !errors.Is(err, resolver.ErrSchemaNotFound) { 154 // Anything except ErrSchemaNotFound is an internal error 155 klog.V(2).ErrorS(err, "internal error: schema resolution failure", "gvk", gvk) 156 } 157 // skip for not found or internal error 158 continue 159 } 160 gvks = append(gvks, gvk) 161 declTypes = append(declTypes, declType) 162 } 163 ctx.gvks = gvks 164 ctx.declTypes = declTypes 165 166 paramsGVK := c.paramsGVK(policy) // maybe empty, correctly handled 167 paramsDeclType, err := c.declType(paramsGVK) 168 if err != nil { 169 if !errors.Is(err, resolver.ErrSchemaNotFound) { 170 klog.V(2).ErrorS(err, "internal error: cannot resolve schema for params", "gvk", paramsGVK) 171 } 172 paramsDeclType = nil 173 } 174 ctx.paramGVK = paramsGVK 175 ctx.paramDeclType = paramsDeclType 176 ctx.variables = policy.Spec.Variables 177 return ctx 178 } 179 180 func (c *TypeChecker) compiler(ctx *TypeCheckingContext, typeOverwrite typeOverwrite) (*plugincel.CompositedCompiler, error) { 181 envSet, err := buildEnvSet( 182 /* hasParams */ ctx.paramDeclType != nil, 183 /* hasAuthorizer */ true, 184 typeOverwrite) 185 if err != nil { 186 return nil, err 187 } 188 env, err := plugincel.NewCompositionEnv(plugincel.VariablesTypeName, envSet) 189 if err != nil { 190 return nil, err 191 } 192 compiler := &plugincel.CompositedCompiler{ 193 Compiler: &typeCheckingCompiler{typeOverwrite: typeOverwrite, compositionEnv: env}, 194 CompositionEnv: env, 195 } 196 return compiler, nil 197 } 198 199 // CheckExpression type checks a single expression, given the context 200 func (c *TypeChecker) CheckExpression(ctx *TypeCheckingContext, expression string) TypeCheckingResults { 201 var results TypeCheckingResults 202 for i, gvk := range ctx.gvks { 203 declType := ctx.declTypes[i] 204 compiler, err := c.compiler(ctx, typeOverwrite{ 205 object: declType, 206 params: ctx.paramDeclType, 207 }) 208 if err != nil { 209 utilruntime.HandleError(err) 210 continue 211 } 212 options := plugincel.OptionalVariableDeclarations{ 213 HasParams: ctx.paramDeclType != nil, 214 HasAuthorizer: true, 215 StrictCost: utilfeature.DefaultFeatureGate.Enabled(features.StrictCostEnforcementForVAP), 216 } 217 compiler.CompileAndStoreVariables(convertv1beta1Variables(ctx.variables), options, environment.StoredExpressions) 218 result := compiler.CompileCELExpression(celExpression(expression), options, environment.StoredExpressions) 219 if err := result.Error; err != nil { 220 typeCheckingResult := &TypeCheckingResult{GVK: gvk} 221 if err.Type == apiservercel.ErrorTypeInvalid { 222 typeCheckingResult.Issues = err 223 } else { 224 typeCheckingResult.Err = err 225 } 226 results = append(results, typeCheckingResult) 227 } 228 } 229 return results 230 } 231 232 type celExpression string 233 234 func (c celExpression) GetExpression() string { 235 return string(c) 236 } 237 238 func (c celExpression) ReturnTypes() []*cel.Type { 239 return []*cel.Type{cel.AnyType} 240 } 241 func generateUniqueTypeName(kind string) string { 242 return fmt.Sprintf("%s%d", kind, time.Now().Nanosecond()) 243 } 244 245 func (c *TypeChecker) declType(gvk schema.GroupVersionKind) (*apiservercel.DeclType, error) { 246 if gvk.Empty() { 247 return nil, nil 248 } 249 s, err := c.SchemaResolver.ResolveSchema(gvk) 250 if err != nil { 251 return nil, err 252 } 253 return common.SchemaDeclType(&openapi.Schema{Schema: s}, true).MaybeAssignTypeName(generateUniqueTypeName(gvk.Kind)), nil 254 } 255 256 func (c *TypeChecker) paramsGVK(policy *v1.ValidatingAdmissionPolicy) schema.GroupVersionKind { 257 if policy.Spec.ParamKind == nil { 258 return schema.GroupVersionKind{} 259 } 260 gv, err := schema.ParseGroupVersion(policy.Spec.ParamKind.APIVersion) 261 if err != nil { 262 return schema.GroupVersionKind{} 263 } 264 return gv.WithKind(policy.Spec.ParamKind.Kind) 265 } 266 267 // typesToCheck extracts a list of GVKs that needs type checking from the policy 268 // the result is sorted in the order of Group, Version, and Kind 269 func (c *TypeChecker) typesToCheck(p *v1.ValidatingAdmissionPolicy) []schema.GroupVersionKind { 270 gvks := sets.New[schema.GroupVersionKind]() 271 if p.Spec.MatchConstraints == nil || len(p.Spec.MatchConstraints.ResourceRules) == 0 { 272 return nil 273 } 274 restMapperRefreshAttempted := false // at most once per policy, refresh RESTMapper and retry resolution. 275 for _, rule := range p.Spec.MatchConstraints.ResourceRules { 276 groups := extractGroups(&rule.Rule) 277 if len(groups) == 0 { 278 continue 279 } 280 versions := extractVersions(&rule.Rule) 281 if len(versions) == 0 { 282 continue 283 } 284 resources := extractResources(&rule.Rule) 285 if len(resources) == 0 { 286 continue 287 } 288 // sort GVRs so that the loop below provides 289 // consistent results. 290 sort.Strings(groups) 291 sort.Strings(versions) 292 sort.Strings(resources) 293 count := 0 294 for _, group := range groups { 295 for _, version := range versions { 296 for _, resource := range resources { 297 gvr := schema.GroupVersionResource{ 298 Group: group, 299 Version: version, 300 Resource: resource, 301 } 302 resolved, err := c.RestMapper.KindsFor(gvr) 303 if err != nil { 304 if restMapperRefreshAttempted { 305 // RESTMapper refresh happens at most once per policy 306 continue 307 } 308 c.tryRefreshRESTMapper() 309 restMapperRefreshAttempted = true 310 resolved, err = c.RestMapper.KindsFor(gvr) 311 if err != nil { 312 continue 313 } 314 } 315 for _, r := range resolved { 316 if !r.Empty() { 317 gvks.Insert(r) 318 count++ 319 // early return if maximum number of types are already 320 // collected 321 if count == maxTypesToCheck { 322 if gvks.Len() == 0 { 323 return nil 324 } 325 return sortGVKList(gvks.UnsortedList()) 326 } 327 } 328 } 329 } 330 } 331 } 332 } 333 if gvks.Len() == 0 { 334 return nil 335 } 336 return sortGVKList(gvks.UnsortedList()) 337 } 338 339 func extractGroups(rule *v1.Rule) []string { 340 groups := make([]string, 0, len(rule.APIGroups)) 341 for _, group := range rule.APIGroups { 342 // give up if wildcard 343 if strings.ContainsAny(group, "*") { 344 return nil 345 } 346 groups = append(groups, group) 347 } 348 return groups 349 } 350 351 func extractVersions(rule *v1.Rule) []string { 352 versions := make([]string, 0, len(rule.APIVersions)) 353 for _, version := range rule.APIVersions { 354 if strings.ContainsAny(version, "*") { 355 return nil 356 } 357 versions = append(versions, version) 358 } 359 return versions 360 } 361 362 func extractResources(rule *v1.Rule) []string { 363 resources := make([]string, 0, len(rule.Resources)) 364 for _, resource := range rule.Resources { 365 // skip wildcard and subresources 366 if strings.ContainsAny(resource, "*/") { 367 continue 368 } 369 resources = append(resources, resource) 370 } 371 return resources 372 } 373 374 // sortGVKList sorts the list by Group, Version, and Kind 375 // returns the list itself. 376 func sortGVKList(list []schema.GroupVersionKind) []schema.GroupVersionKind { 377 sort.Slice(list, func(i, j int) bool { 378 if g := strings.Compare(list[i].Group, list[j].Group); g != 0 { 379 return g < 0 380 } 381 if v := strings.Compare(list[i].Version, list[j].Version); v != 0 { 382 return v < 0 383 } 384 return strings.Compare(list[i].Kind, list[j].Kind) < 0 385 }) 386 return list 387 } 388 389 // tryRefreshRESTMapper refreshes the RESTMapper if it supports refreshing. 390 func (c *TypeChecker) tryRefreshRESTMapper() { 391 if r, ok := c.RestMapper.(meta.ResettableRESTMapper); ok { 392 r.Reset() 393 } 394 } 395 396 func buildEnvSet(hasParams bool, hasAuthorizer bool, types typeOverwrite) (*environment.EnvSet, error) { 397 baseEnv := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), utilfeature.DefaultFeatureGate.Enabled(features.StrictCostEnforcementForVAP)) 398 requestType := plugincel.BuildRequestType() 399 namespaceType := plugincel.BuildNamespaceType() 400 401 var varOpts []cel.EnvOption 402 var declTypes []*apiservercel.DeclType 403 404 // namespace, hand-crafted type 405 declTypes = append(declTypes, namespaceType) 406 varOpts = append(varOpts, createVariableOpts(namespaceType, plugincel.NamespaceVarName)...) 407 408 // request, hand-crafted type 409 declTypes = append(declTypes, requestType) 410 varOpts = append(varOpts, createVariableOpts(requestType, plugincel.RequestVarName)...) 411 412 // object and oldObject, same type, type(s) resolved from constraints 413 declTypes = append(declTypes, types.object) 414 varOpts = append(varOpts, createVariableOpts(types.object, plugincel.ObjectVarName, plugincel.OldObjectVarName)...) 415 416 // params, defined by ParamKind 417 if hasParams && types.params != nil { 418 declTypes = append(declTypes, types.params) 419 varOpts = append(varOpts, createVariableOpts(types.params, plugincel.ParamsVarName)...) 420 } 421 422 // authorizer, implicitly available to all expressions of a policy 423 if hasAuthorizer { 424 // we only need its structure but not the variable itself 425 varOpts = append(varOpts, cel.Variable("authorizer", library.AuthorizerType)) 426 } 427 428 return baseEnv.Extend( 429 environment.VersionedOptions{ 430 // Feature epoch was actually 1.26, but we artificially set it to 1.0 because these 431 // options should always be present. 432 IntroducedVersion: version.MajorMinor(1, 0), 433 EnvOptions: varOpts, 434 DeclTypes: declTypes, 435 }, 436 ) 437 } 438 439 // createVariableOpts creates a slice of EnvOption 440 // that can be used for creating a CEL env containing variables of declType. 441 // declType can be nil, in which case the variables will be of DynType. 442 func createVariableOpts(declType *apiservercel.DeclType, variables ...string) []cel.EnvOption { 443 opts := make([]cel.EnvOption, 0, len(variables)) 444 t := cel.DynType 445 if declType != nil { 446 t = declType.CelType() 447 } 448 for _, v := range variables { 449 opts = append(opts, cel.Variable(v, t)) 450 } 451 return opts 452 } 453 454 type typeCheckingCompiler struct { 455 compositionEnv *plugincel.CompositionEnv 456 typeOverwrite typeOverwrite 457 } 458 459 // CompileCELExpression compiles the given expression. 460 // The implementation is the same as that of staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/compile.go 461 // except that: 462 // - object, oldObject, and params are typed instead of Dyn 463 // - compiler does not enforce the output type 464 // - the compiler does not initialize the program 465 func (c *typeCheckingCompiler) CompileCELExpression(expressionAccessor plugincel.ExpressionAccessor, options plugincel.OptionalVariableDeclarations, mode environment.Type) plugincel.CompilationResult { 466 resultError := func(errorString string, errType apiservercel.ErrorType) plugincel.CompilationResult { 467 return plugincel.CompilationResult{ 468 Error: &apiservercel.Error{ 469 Type: errType, 470 Detail: errorString, 471 }, 472 ExpressionAccessor: expressionAccessor, 473 } 474 } 475 env, err := c.compositionEnv.Env(mode) 476 if err != nil { 477 return resultError(fmt.Sprintf("fail to build env: %v", err), apiservercel.ErrorTypeInternal) 478 } 479 ast, issues := env.Compile(expressionAccessor.GetExpression()) 480 if issues != nil { 481 return resultError(issues.String(), apiservercel.ErrorTypeInvalid) 482 } 483 // type checker does not require the program, however the type must still be set. 484 return plugincel.CompilationResult{ 485 OutputType: ast.OutputType(), 486 } 487 } 488 489 var _ plugincel.Compiler = (*typeCheckingCompiler)(nil)