rebase: update K8s packages to v0.32.1

Update K8s packages in go.mod to v0.32.1

Signed-off-by: Praveen M <m.praveen@ibm.com>
This commit is contained in:
Praveen M
2025-01-16 09:41:46 +05:30
committed by mergify[bot]
parent 5aef21ea4e
commit 7eb99fc6c9
2442 changed files with 273386 additions and 47788 deletions

View File

@ -63,7 +63,7 @@ func ReadAdmissionConfiguration(pluginNames []string, configFilePath string, con
if err != nil {
return nil, fmt.Errorf("unable to read admission control configuration from %q [%v]", configFilePath, err)
}
codecs := serializer.NewCodecFactory(configScheme)
codecs := serializer.NewCodecFactory(configScheme, serializer.EnableStrict)
decoder := codecs.UniversalDecoder()
decodedObj, err := runtime.Decode(decoder, data)
// we were able to decode the file successfully

View File

@ -207,7 +207,7 @@ func newAdmissionMetrics() *AdmissionMetrics {
Namespace: namespace,
Subsystem: subsystem,
Name: "webhook_fail_open_count",
Help: "Admission webhook fail open count, identified by name and broken out for each admission type (validating or mutating).",
Help: "Admission webhook fail open count, identified by name and broken out for each admission type (validating or admit).",
StabilityLevel: metrics.ALPHA,
},
[]string{"name", "type"})
@ -217,7 +217,7 @@ func newAdmissionMetrics() *AdmissionMetrics {
Namespace: namespace,
Subsystem: subsystem,
Name: "webhook_request_total",
Help: "Admission webhook request total, identified by name and broken out for each admission type (validating or mutating) and operation. Additional labels specify whether the request was rejected or not and an HTTP status code. Codes greater than 600 are truncated to 600, to keep the metrics cardinality bounded.",
Help: "Admission webhook request total, identified by name and broken out for each admission type (validating or admit) and operation. Additional labels specify whether the request was rejected or not and an HTTP status code. Codes greater than 600 are truncated to 600, to keep the metrics cardinality bounded.",
StabilityLevel: metrics.ALPHA,
},
[]string{"name", "type", "operation", "code", "rejected"})

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package validating
package authorizer
import (
"context"
@ -39,7 +39,10 @@ type cachingAuthorizer struct {
decisions map[string]authzResult
}
func newCachingAuthorizer(in authorizer.Authorizer) authorizer.Authorizer {
// NewCachingAuthorizer returns an authorizer that caches decisions for the duration
// of the authorizers use. Intended to be used for short-lived operations such as
// the handling of a request in the admission chain, and then discarded.
func NewCachingAuthorizer(in authorizer.Authorizer) authorizer.Authorizer {
return &cachingAuthorizer{
authorizer: in,
decisions: make(map[string]authzResult),

View File

@ -0,0 +1,190 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cel
import (
"context"
"fmt"
"github.com/google/cel-go/interpreter"
"math"
"time"
admissionv1 "k8s.io/api/admission/v1"
v1 "k8s.io/api/core/v1"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/cel"
"k8s.io/apiserver/pkg/cel/library"
)
// newActivation creates an activation for CEL admission plugins from the given request, admission chain and
// variable binding information.
func newActivation(compositionCtx CompositionContext, versionedAttr *admission.VersionedAttributes, request *admissionv1.AdmissionRequest, inputs OptionalVariableBindings, namespace *v1.Namespace) (*evaluationActivation, error) {
oldObjectVal, err := objectToResolveVal(versionedAttr.VersionedOldObject)
if err != nil {
return nil, fmt.Errorf("failed to prepare oldObject variable for evaluation: %w", err)
}
objectVal, err := objectToResolveVal(versionedAttr.VersionedObject)
if err != nil {
return nil, fmt.Errorf("failed to prepare object variable for evaluation: %w", err)
}
var paramsVal, authorizerVal, requestResourceAuthorizerVal any
if inputs.VersionedParams != nil {
paramsVal, err = objectToResolveVal(inputs.VersionedParams)
if err != nil {
return nil, fmt.Errorf("failed to prepare params variable for evaluation: %w", err)
}
}
if inputs.Authorizer != nil {
authorizerVal = library.NewAuthorizerVal(versionedAttr.GetUserInfo(), inputs.Authorizer)
requestResourceAuthorizerVal = library.NewResourceAuthorizerVal(versionedAttr.GetUserInfo(), inputs.Authorizer, versionedAttr)
}
requestVal, err := convertObjectToUnstructured(request)
if err != nil {
return nil, fmt.Errorf("failed to prepare request variable for evaluation: %w", err)
}
namespaceVal, err := objectToResolveVal(namespace)
if err != nil {
return nil, fmt.Errorf("failed to prepare namespace variable for evaluation: %w", err)
}
va := &evaluationActivation{
object: objectVal,
oldObject: oldObjectVal,
params: paramsVal,
request: requestVal.Object,
namespace: namespaceVal,
authorizer: authorizerVal,
requestResourceAuthorizer: requestResourceAuthorizerVal,
}
// composition is an optional feature that only applies for ValidatingAdmissionPolicy and MutatingAdmissionPolicy.
if compositionCtx != nil {
va.variables = compositionCtx.Variables(va)
}
return va, nil
}
type evaluationActivation struct {
object, oldObject, params, request, namespace, authorizer, requestResourceAuthorizer, variables interface{}
}
// ResolveName returns a value from the activation by qualified name, or false if the name
// could not be found.
func (a *evaluationActivation) ResolveName(name string) (interface{}, bool) {
switch name {
case ObjectVarName:
return a.object, true
case OldObjectVarName:
return a.oldObject, true
case ParamsVarName:
return a.params, true // params may be null
case RequestVarName:
return a.request, true
case NamespaceVarName:
return a.namespace, true
case AuthorizerVarName:
return a.authorizer, a.authorizer != nil
case RequestResourceAuthorizerVarName:
return a.requestResourceAuthorizer, a.requestResourceAuthorizer != nil
case VariableVarName: // variables always present
return a.variables, true
default:
return nil, false
}
}
// Parent returns the parent of the current activation, may be nil.
// If non-nil, the parent will be searched during resolve calls.
func (a *evaluationActivation) Parent() interpreter.Activation {
return nil
}
// Evaluate runs a compiled CEL admission plugin expression using the provided activation and CEL
// runtime cost budget.
func (a *evaluationActivation) Evaluate(ctx context.Context, compositionCtx CompositionContext, compilationResult CompilationResult, remainingBudget int64) (EvaluationResult, int64, error) {
var evaluation = EvaluationResult{}
if compilationResult.ExpressionAccessor == nil { // in case of placeholder
return evaluation, remainingBudget, nil
}
evaluation.ExpressionAccessor = compilationResult.ExpressionAccessor
if compilationResult.Error != nil {
evaluation.Error = &cel.Error{
Type: cel.ErrorTypeInvalid,
Detail: fmt.Sprintf("compilation error: %v", compilationResult.Error),
Cause: compilationResult.Error,
}
return evaluation, remainingBudget, nil
}
if compilationResult.Program == nil {
evaluation.Error = &cel.Error{
Type: cel.ErrorTypeInternal,
Detail: "unexpected internal error compiling expression",
}
return evaluation, remainingBudget, nil
}
t1 := time.Now()
evalResult, evalDetails, err := compilationResult.Program.ContextEval(ctx, a)
// budget may be spent due to lazy evaluation of composited variables
if compositionCtx != nil {
compositionCost := compositionCtx.GetAndResetCost()
if compositionCost > remainingBudget {
return evaluation, -1, &cel.Error{
Type: cel.ErrorTypeInvalid,
Detail: "validation failed due to running out of cost budget, no further validation rules will be run",
Cause: cel.ErrOutOfBudget,
}
}
remainingBudget -= compositionCost
}
elapsed := time.Since(t1)
evaluation.Elapsed = elapsed
if evalDetails == nil {
return evaluation, -1, &cel.Error{
Type: cel.ErrorTypeInternal,
Detail: fmt.Sprintf("runtime cost could not be calculated for expression: %v, no further expression will be run", compilationResult.ExpressionAccessor.GetExpression()),
}
} else {
rtCost := evalDetails.ActualCost()
if rtCost == nil {
return evaluation, -1, &cel.Error{
Type: cel.ErrorTypeInvalid,
Detail: fmt.Sprintf("runtime cost could not be calculated for expression: %v, no further expression will be run", compilationResult.ExpressionAccessor.GetExpression()),
Cause: cel.ErrOutOfBudget,
}
} else {
if *rtCost > math.MaxInt64 || int64(*rtCost) > remainingBudget {
return evaluation, -1, &cel.Error{
Type: cel.ErrorTypeInvalid,
Detail: "validation failed due to running out of cost budget, no further validation rules will be run",
Cause: cel.ErrOutOfBudget,
}
}
remainingBudget -= int64(*rtCost)
}
}
if err != nil {
evaluation.Error = &cel.Error{
Type: cel.ErrorTypeInvalid,
Detail: fmt.Sprintf("expression '%v' resulted in error: %v", compilationResult.ExpressionAccessor.GetExpression(), err),
}
} else {
evaluation.EvalResult = evalResult
}
return evaluation, remainingBudget, nil
}

View File

@ -24,8 +24,10 @@ import (
"k8s.io/apimachinery/pkg/util/version"
celconfig "k8s.io/apiserver/pkg/apis/cel"
apiservercel "k8s.io/apiserver/pkg/cel"
"k8s.io/apiserver/pkg/cel/common"
"k8s.io/apiserver/pkg/cel/environment"
"k8s.io/apiserver/pkg/cel/library"
"k8s.io/apiserver/pkg/cel/mutation"
)
const (
@ -186,7 +188,7 @@ func (c compiler) CompileCELExpression(expressionAccessor ExpressionAccessor, op
found := false
returnTypes := expressionAccessor.ReturnTypes()
for _, returnType := range returnTypes {
if ast.OutputType() == returnType || cel.AnyType == returnType {
if ast.OutputType().IsExactType(returnType) || cel.AnyType.IsExactType(returnType) {
found = true
break
}
@ -194,9 +196,9 @@ func (c compiler) CompileCELExpression(expressionAccessor ExpressionAccessor, op
if !found {
var reason string
if len(returnTypes) == 1 {
reason = fmt.Sprintf("must evaluate to %v", returnTypes[0].String())
reason = fmt.Sprintf("must evaluate to %v but got %v", returnTypes[0].String(), ast.OutputType().String())
} else {
reason = fmt.Sprintf("must evaluate to one of %v", returnTypes)
reason = fmt.Sprintf("must evaluate to one of %v but got %v", returnTypes, ast.OutputType().String())
}
return resultError(reason, apiservercel.ErrorTypeInvalid, nil)
@ -226,46 +228,78 @@ func mustBuildEnvs(baseEnv *environment.EnvSet) variableDeclEnvs {
envs := make(variableDeclEnvs, 8) // since the number of variable combinations is small, pre-build a environment for each
for _, hasParams := range []bool{false, true} {
for _, hasAuthorizer := range []bool{false, true} {
var err error
for _, strictCost := range []bool{false, true} {
var envOpts []cel.EnvOption
if hasParams {
envOpts = append(envOpts, cel.Variable(ParamsVarName, cel.DynType))
}
if hasAuthorizer {
envOpts = append(envOpts,
cel.Variable(AuthorizerVarName, library.AuthorizerType),
cel.Variable(RequestResourceAuthorizerVarName, library.ResourceCheckType))
}
envOpts = append(envOpts,
cel.Variable(ObjectVarName, cel.DynType),
cel.Variable(OldObjectVarName, cel.DynType),
cel.Variable(NamespaceVarName, namespaceType.CelType()),
cel.Variable(RequestVarName, requestType.CelType()))
extended, err := baseEnv.Extend(
environment.VersionedOptions{
// Feature epoch was actually 1.26, but we artificially set it to 1.0 because these
// options should always be present.
IntroducedVersion: version.MajorMinor(1, 0),
EnvOptions: envOpts,
DeclTypes: []*apiservercel.DeclType{
namespaceType,
requestType,
},
},
)
decl := OptionalVariableDeclarations{HasParams: hasParams, HasAuthorizer: hasAuthorizer, StrictCost: strictCost}
envs[decl], err = createEnvForOpts(baseEnv, namespaceType, requestType, decl)
if err != nil {
panic(fmt.Sprintf("environment misconfigured: %v", err))
panic(err)
}
if strictCost {
extended, err = extended.Extend(environment.StrictCostOpt)
if err != nil {
panic(fmt.Sprintf("environment misconfigured: %v", err))
}
}
envs[OptionalVariableDeclarations{HasParams: hasParams, HasAuthorizer: hasAuthorizer, StrictCost: strictCost}] = extended
}
// We only need this ObjectTypes where strict cost is true
decl := OptionalVariableDeclarations{HasParams: hasParams, HasAuthorizer: hasAuthorizer, StrictCost: true, HasPatchTypes: true}
envs[decl], err = createEnvForOpts(baseEnv, namespaceType, requestType, decl)
if err != nil {
panic(err)
}
}
}
return envs
}
func createEnvForOpts(baseEnv *environment.EnvSet, namespaceType *apiservercel.DeclType, requestType *apiservercel.DeclType, opts OptionalVariableDeclarations) (*environment.EnvSet, error) {
var envOpts []cel.EnvOption
envOpts = append(envOpts,
cel.Variable(ObjectVarName, cel.DynType),
cel.Variable(OldObjectVarName, cel.DynType),
cel.Variable(NamespaceVarName, namespaceType.CelType()),
cel.Variable(RequestVarName, requestType.CelType()))
if opts.HasParams {
envOpts = append(envOpts, cel.Variable(ParamsVarName, cel.DynType))
}
if opts.HasAuthorizer {
envOpts = append(envOpts,
cel.Variable(AuthorizerVarName, library.AuthorizerType),
cel.Variable(RequestResourceAuthorizerVarName, library.ResourceCheckType))
}
extended, err := baseEnv.Extend(
environment.VersionedOptions{
// Feature epoch was actually 1.26, but we artificially set it to 1.0 because these
// options should always be present.
IntroducedVersion: version.MajorMinor(1, 0),
EnvOptions: envOpts,
DeclTypes: []*apiservercel.DeclType{
namespaceType,
requestType,
},
},
)
if err != nil {
return nil, fmt.Errorf("environment misconfigured: %w", err)
}
if opts.StrictCost {
extended, err = extended.Extend(environment.StrictCostOpt)
if err != nil {
return nil, fmt.Errorf("environment misconfigured: %w", err)
}
}
if opts.HasPatchTypes {
extended, err = extended.Extend(hasPatchTypes)
if err != nil {
return nil, fmt.Errorf("environment misconfigured: %w", err)
}
}
return extended, nil
}
var hasPatchTypes = environment.VersionedOptions{
// Feature epoch was actually 1.32, but we artificially set it to 1.0 because these
// options should always be present.
IntroducedVersion: version.MajorMinor(1, 0),
EnvOptions: []cel.EnvOption{
common.ResolverEnvOption(&mutation.DynamicTypeResolver{}),
environment.UnversionedLib(library.JSONPatch), // for jsonPatch.escape() function
},
}

View File

@ -36,15 +36,27 @@ import (
const VariablesTypeName = "kubernetes.variables"
// CompositedCompiler compiles expressions with variable composition.
type CompositedCompiler struct {
Compiler
FilterCompiler
ConditionCompiler
MutatingCompiler
CompositionEnv *CompositionEnv
}
type CompositedFilter struct {
Filter
// CompositedConditionEvaluator provides evaluation of a condition expression with variable composition.
// The expressions must return a boolean.
type CompositedConditionEvaluator struct {
ConditionEvaluator
compositionEnv *CompositionEnv
}
// CompositedEvaluator provides evaluation of a single expression with variable composition.
// The types that may returned by the expression is determined at compilation time.
type CompositedEvaluator struct {
MutatingEvaluator
compositionEnv *CompositionEnv
}
@ -64,11 +76,13 @@ func NewCompositedCompilerFromTemplate(context *CompositionEnv) *CompositedCompi
CompiledVariables: map[string]CompilationResult{},
}
compiler := NewCompiler(context.EnvSet)
filterCompiler := NewFilterCompiler(context.EnvSet)
conditionCompiler := &conditionCompiler{compiler}
mutation := &mutatingCompiler{compiler}
return &CompositedCompiler{
Compiler: compiler,
FilterCompiler: filterCompiler,
CompositionEnv: context,
Compiler: compiler,
ConditionCompiler: conditionCompiler,
MutatingCompiler: mutation,
CompositionEnv: context,
}
}
@ -85,11 +99,20 @@ func (c *CompositedCompiler) CompileAndStoreVariable(variable NamedExpressionAcc
return result
}
func (c *CompositedCompiler) Compile(expressions []ExpressionAccessor, optionalDecls OptionalVariableDeclarations, envType environment.Type) Filter {
filter := c.FilterCompiler.Compile(expressions, optionalDecls, envType)
return &CompositedFilter{
Filter: filter,
compositionEnv: c.CompositionEnv,
func (c *CompositedCompiler) CompileCondition(expressions []ExpressionAccessor, optionalDecls OptionalVariableDeclarations, envType environment.Type) ConditionEvaluator {
condition := c.ConditionCompiler.CompileCondition(expressions, optionalDecls, envType)
return &CompositedConditionEvaluator{
ConditionEvaluator: condition,
compositionEnv: c.CompositionEnv,
}
}
// CompileEvaluator compiles an mutatingEvaluator for the given expression, options and environment.
func (c *CompositedCompiler) CompileMutatingEvaluator(expression ExpressionAccessor, optionalDecls OptionalVariableDeclarations, envType environment.Type) MutatingEvaluator {
mutation := c.MutatingCompiler.CompileMutatingEvaluator(expression, optionalDecls, envType)
return &CompositedEvaluator{
MutatingEvaluator: mutation,
compositionEnv: c.CompositionEnv,
}
}
@ -160,9 +183,9 @@ func (c *compositionContext) Variables(activation any) ref.Val {
return lazyMap
}
func (f *CompositedFilter) ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *v1.AdmissionRequest, optionalVars OptionalVariableBindings, namespace *corev1.Namespace, runtimeCELCostBudget int64) ([]EvaluationResult, int64, error) {
func (f *CompositedConditionEvaluator) ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *v1.AdmissionRequest, optionalVars OptionalVariableBindings, namespace *corev1.Namespace, runtimeCELCostBudget int64) ([]EvaluationResult, int64, error) {
ctx = f.compositionEnv.CreateContext(ctx)
return f.Filter.ForInput(ctx, versionedAttr, request, optionalVars, namespace, runtimeCELCostBudget)
return f.ConditionEvaluator.ForInput(ctx, versionedAttr, request, optionalVars, namespace, runtimeCELCostBudget)
}
func (c *compositionContext) reportCost(cost int64) {

View File

@ -0,0 +1,216 @@
/*
Copyright 2022 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cel
import (
"context"
"reflect"
admissionv1 "k8s.io/api/admission/v1"
authenticationv1 "k8s.io/api/authentication/v1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/cel/environment"
)
// conditionCompiler implement the interface ConditionCompiler.
type conditionCompiler struct {
compiler Compiler
}
func NewConditionCompiler(env *environment.EnvSet) ConditionCompiler {
return &conditionCompiler{compiler: NewCompiler(env)}
}
// CompileCondition compiles the cel expressions defined in the ExpressionAccessors into a ConditionEvaluator
func (c *conditionCompiler) CompileCondition(expressionAccessors []ExpressionAccessor, options OptionalVariableDeclarations, mode environment.Type) ConditionEvaluator {
compilationResults := make([]CompilationResult, len(expressionAccessors))
for i, expressionAccessor := range expressionAccessors {
if expressionAccessor == nil {
continue
}
compilationResults[i] = c.compiler.CompileCELExpression(expressionAccessor, options, mode)
}
return NewCondition(compilationResults)
}
// condition implements the ConditionEvaluator interface
type condition struct {
compilationResults []CompilationResult
}
func NewCondition(compilationResults []CompilationResult) ConditionEvaluator {
return &condition{
compilationResults,
}
}
func convertObjectToUnstructured(obj interface{}) (*unstructured.Unstructured, error) {
if obj == nil || reflect.ValueOf(obj).IsNil() {
return &unstructured.Unstructured{Object: nil}, nil
}
ret, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
if err != nil {
return nil, err
}
return &unstructured.Unstructured{Object: ret}, nil
}
func objectToResolveVal(r runtime.Object) (interface{}, error) {
if r == nil || reflect.ValueOf(r).IsNil() {
return nil, nil
}
v, err := convertObjectToUnstructured(r)
if err != nil {
return nil, err
}
return v.Object, nil
}
// ForInput evaluates the compiled CEL expressions converting them into CELEvaluations
// errors per evaluation are returned on the Evaluation object
// runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input.
func (c *condition) ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *admissionv1.AdmissionRequest, inputs OptionalVariableBindings, namespace *v1.Namespace, runtimeCELCostBudget int64) ([]EvaluationResult, int64, error) {
// TODO: replace unstructured with ref.Val for CEL variables when native type support is available
evaluations := make([]EvaluationResult, len(c.compilationResults))
var err error
// if this activation supports composition, we will need the compositionCtx. It may be nil.
compositionCtx, _ := ctx.(CompositionContext)
activation, err := newActivation(compositionCtx, versionedAttr, request, inputs, namespace)
if err != nil {
return nil, -1, err
}
remainingBudget := runtimeCELCostBudget
for i, compilationResult := range c.compilationResults {
evaluations[i], remainingBudget, err = activation.Evaluate(ctx, compositionCtx, compilationResult, remainingBudget)
if err != nil {
return nil, -1, err
}
}
return evaluations, remainingBudget, nil
}
// TODO: to reuse https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/request/admissionreview.go#L154
func CreateAdmissionRequest(attr admission.Attributes, equivalentGVR metav1.GroupVersionResource, equivalentKind metav1.GroupVersionKind) *admissionv1.AdmissionRequest {
// Attempting to use same logic as webhook for constructing resource
// GVK, GVR, subresource
// Use the GVK, GVR that the matcher decided was equivalent to that of the request
// https://github.com/kubernetes/kubernetes/blob/90c362b3430bcbbf8f245fadbcd521dab39f1d7c/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/webhook.go#L182-L210
gvk := equivalentKind
gvr := equivalentGVR
subresource := attr.GetSubresource()
requestGVK := attr.GetKind()
requestGVR := attr.GetResource()
requestSubResource := attr.GetSubresource()
aUserInfo := attr.GetUserInfo()
var userInfo authenticationv1.UserInfo
if aUserInfo != nil {
userInfo = authenticationv1.UserInfo{
Extra: make(map[string]authenticationv1.ExtraValue),
Groups: aUserInfo.GetGroups(),
UID: aUserInfo.GetUID(),
Username: aUserInfo.GetName(),
}
// Convert the extra information in the user object
for key, val := range aUserInfo.GetExtra() {
userInfo.Extra[key] = authenticationv1.ExtraValue(val)
}
}
dryRun := attr.IsDryRun()
return &admissionv1.AdmissionRequest{
Kind: metav1.GroupVersionKind{
Group: gvk.Group,
Kind: gvk.Kind,
Version: gvk.Version,
},
Resource: metav1.GroupVersionResource{
Group: gvr.Group,
Resource: gvr.Resource,
Version: gvr.Version,
},
SubResource: subresource,
RequestKind: &metav1.GroupVersionKind{
Group: requestGVK.Group,
Kind: requestGVK.Kind,
Version: requestGVK.Version,
},
RequestResource: &metav1.GroupVersionResource{
Group: requestGVR.Group,
Resource: requestGVR.Resource,
Version: requestGVR.Version,
},
RequestSubResource: requestSubResource,
Name: attr.GetName(),
Namespace: attr.GetNamespace(),
Operation: admissionv1.Operation(attr.GetOperation()),
UserInfo: userInfo,
// Leave Object and OldObject unset since we don't provide access to them via request
DryRun: &dryRun,
Options: runtime.RawExtension{
Object: attr.GetOperationOptions(),
},
}
}
// CreateNamespaceObject creates a Namespace object that is suitable for the CEL evaluation.
// If the namespace is nil, CreateNamespaceObject returns nil
func CreateNamespaceObject(namespace *v1.Namespace) *v1.Namespace {
if namespace == nil {
return nil
}
return &v1.Namespace{
Status: namespace.Status,
Spec: namespace.Spec,
ObjectMeta: metav1.ObjectMeta{
Name: namespace.Name,
GenerateName: namespace.GenerateName,
Namespace: namespace.Namespace,
UID: namespace.UID,
ResourceVersion: namespace.ResourceVersion,
Generation: namespace.Generation,
CreationTimestamp: namespace.CreationTimestamp,
DeletionTimestamp: namespace.DeletionTimestamp,
DeletionGracePeriodSeconds: namespace.DeletionGracePeriodSeconds,
Labels: namespace.Labels,
Annotations: namespace.Annotations,
Finalizers: namespace.Finalizers,
},
}
}
// CompilationErrors returns a list of all the errors from the compilation of the mutatingEvaluator
func (c *condition) CompilationErrors() []error {
compilationErrors := []error{}
for _, result := range c.compilationResults {
if result.Error != nil {
compilationErrors = append(compilationErrors, result.Error)
}
}
return compilationErrors
}

View File

@ -1,361 +0,0 @@
/*
Copyright 2022 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cel
import (
"context"
"fmt"
"math"
"reflect"
"time"
"github.com/google/cel-go/interpreter"
admissionv1 "k8s.io/api/admission/v1"
authenticationv1 "k8s.io/api/authentication/v1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/cel"
"k8s.io/apiserver/pkg/cel/environment"
"k8s.io/apiserver/pkg/cel/library"
)
// filterCompiler implement the interface FilterCompiler.
type filterCompiler struct {
compiler Compiler
}
func NewFilterCompiler(env *environment.EnvSet) FilterCompiler {
return &filterCompiler{compiler: NewCompiler(env)}
}
type evaluationActivation struct {
object, oldObject, params, request, namespace, authorizer, requestResourceAuthorizer, variables interface{}
}
// ResolveName returns a value from the activation by qualified name, or false if the name
// could not be found.
func (a *evaluationActivation) ResolveName(name string) (interface{}, bool) {
switch name {
case ObjectVarName:
return a.object, true
case OldObjectVarName:
return a.oldObject, true
case ParamsVarName:
return a.params, true // params may be null
case RequestVarName:
return a.request, true
case NamespaceVarName:
return a.namespace, true
case AuthorizerVarName:
return a.authorizer, a.authorizer != nil
case RequestResourceAuthorizerVarName:
return a.requestResourceAuthorizer, a.requestResourceAuthorizer != nil
case VariableVarName: // variables always present
return a.variables, true
default:
return nil, false
}
}
// Parent returns the parent of the current activation, may be nil.
// If non-nil, the parent will be searched during resolve calls.
func (a *evaluationActivation) Parent() interpreter.Activation {
return nil
}
// Compile compiles the cel expressions defined in the ExpressionAccessors into a Filter
func (c *filterCompiler) Compile(expressionAccessors []ExpressionAccessor, options OptionalVariableDeclarations, mode environment.Type) Filter {
compilationResults := make([]CompilationResult, len(expressionAccessors))
for i, expressionAccessor := range expressionAccessors {
if expressionAccessor == nil {
continue
}
compilationResults[i] = c.compiler.CompileCELExpression(expressionAccessor, options, mode)
}
return NewFilter(compilationResults)
}
// filter implements the Filter interface
type filter struct {
compilationResults []CompilationResult
}
func NewFilter(compilationResults []CompilationResult) Filter {
return &filter{
compilationResults,
}
}
func convertObjectToUnstructured(obj interface{}) (*unstructured.Unstructured, error) {
if obj == nil || reflect.ValueOf(obj).IsNil() {
return &unstructured.Unstructured{Object: nil}, nil
}
ret, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
if err != nil {
return nil, err
}
return &unstructured.Unstructured{Object: ret}, nil
}
func objectToResolveVal(r runtime.Object) (interface{}, error) {
if r == nil || reflect.ValueOf(r).IsNil() {
return nil, nil
}
v, err := convertObjectToUnstructured(r)
if err != nil {
return nil, err
}
return v.Object, nil
}
// ForInput evaluates the compiled CEL expressions converting them into CELEvaluations
// errors per evaluation are returned on the Evaluation object
// runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input.
func (f *filter) ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *admissionv1.AdmissionRequest, inputs OptionalVariableBindings, namespace *v1.Namespace, runtimeCELCostBudget int64) ([]EvaluationResult, int64, error) {
// TODO: replace unstructured with ref.Val for CEL variables when native type support is available
evaluations := make([]EvaluationResult, len(f.compilationResults))
var err error
oldObjectVal, err := objectToResolveVal(versionedAttr.VersionedOldObject)
if err != nil {
return nil, -1, err
}
objectVal, err := objectToResolveVal(versionedAttr.VersionedObject)
if err != nil {
return nil, -1, err
}
var paramsVal, authorizerVal, requestResourceAuthorizerVal any
if inputs.VersionedParams != nil {
paramsVal, err = objectToResolveVal(inputs.VersionedParams)
if err != nil {
return nil, -1, err
}
}
if inputs.Authorizer != nil {
authorizerVal = library.NewAuthorizerVal(versionedAttr.GetUserInfo(), inputs.Authorizer)
requestResourceAuthorizerVal = library.NewResourceAuthorizerVal(versionedAttr.GetUserInfo(), inputs.Authorizer, versionedAttr)
}
requestVal, err := convertObjectToUnstructured(request)
if err != nil {
return nil, -1, err
}
namespaceVal, err := objectToResolveVal(namespace)
if err != nil {
return nil, -1, err
}
va := &evaluationActivation{
object: objectVal,
oldObject: oldObjectVal,
params: paramsVal,
request: requestVal.Object,
namespace: namespaceVal,
authorizer: authorizerVal,
requestResourceAuthorizer: requestResourceAuthorizerVal,
}
// composition is an optional feature that only applies for ValidatingAdmissionPolicy.
// check if the context allows composition
var compositionCtx CompositionContext
var ok bool
if compositionCtx, ok = ctx.(CompositionContext); ok {
va.variables = compositionCtx.Variables(va)
}
remainingBudget := runtimeCELCostBudget
for i, compilationResult := range f.compilationResults {
var evaluation = &evaluations[i]
if compilationResult.ExpressionAccessor == nil { // in case of placeholder
continue
}
evaluation.ExpressionAccessor = compilationResult.ExpressionAccessor
if compilationResult.Error != nil {
evaluation.Error = &cel.Error{
Type: cel.ErrorTypeInvalid,
Detail: fmt.Sprintf("compilation error: %v", compilationResult.Error),
Cause: compilationResult.Error,
}
continue
}
if compilationResult.Program == nil {
evaluation.Error = &cel.Error{
Type: cel.ErrorTypeInternal,
Detail: fmt.Sprintf("unexpected internal error compiling expression"),
}
continue
}
t1 := time.Now()
evalResult, evalDetails, err := compilationResult.Program.ContextEval(ctx, va)
// budget may be spent due to lazy evaluation of composited variables
if compositionCtx != nil {
compositionCost := compositionCtx.GetAndResetCost()
if compositionCost > remainingBudget {
return nil, -1, &cel.Error{
Type: cel.ErrorTypeInvalid,
Detail: fmt.Sprintf("validation failed due to running out of cost budget, no further validation rules will be run"),
Cause: cel.ErrOutOfBudget,
}
}
remainingBudget -= compositionCost
}
elapsed := time.Since(t1)
evaluation.Elapsed = elapsed
if evalDetails == nil {
return nil, -1, &cel.Error{
Type: cel.ErrorTypeInternal,
Detail: fmt.Sprintf("runtime cost could not be calculated for expression: %v, no further expression will be run", compilationResult.ExpressionAccessor.GetExpression()),
}
} else {
rtCost := evalDetails.ActualCost()
if rtCost == nil {
return nil, -1, &cel.Error{
Type: cel.ErrorTypeInvalid,
Detail: fmt.Sprintf("runtime cost could not be calculated for expression: %v, no further expression will be run", compilationResult.ExpressionAccessor.GetExpression()),
Cause: cel.ErrOutOfBudget,
}
} else {
if *rtCost > math.MaxInt64 || int64(*rtCost) > remainingBudget {
return nil, -1, &cel.Error{
Type: cel.ErrorTypeInvalid,
Detail: fmt.Sprintf("validation failed due to running out of cost budget, no further validation rules will be run"),
Cause: cel.ErrOutOfBudget,
}
}
remainingBudget -= int64(*rtCost)
}
}
if err != nil {
evaluation.Error = &cel.Error{
Type: cel.ErrorTypeInvalid,
Detail: fmt.Sprintf("expression '%v' resulted in error: %v", compilationResult.ExpressionAccessor.GetExpression(), err),
}
} else {
evaluation.EvalResult = evalResult
}
}
return evaluations, remainingBudget, nil
}
// TODO: to reuse https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/request/admissionreview.go#L154
func CreateAdmissionRequest(attr admission.Attributes, equivalentGVR metav1.GroupVersionResource, equivalentKind metav1.GroupVersionKind) *admissionv1.AdmissionRequest {
// Attempting to use same logic as webhook for constructing resource
// GVK, GVR, subresource
// Use the GVK, GVR that the matcher decided was equivalent to that of the request
// https://github.com/kubernetes/kubernetes/blob/90c362b3430bcbbf8f245fadbcd521dab39f1d7c/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/webhook.go#L182-L210
gvk := equivalentKind
gvr := equivalentGVR
subresource := attr.GetSubresource()
requestGVK := attr.GetKind()
requestGVR := attr.GetResource()
requestSubResource := attr.GetSubresource()
aUserInfo := attr.GetUserInfo()
var userInfo authenticationv1.UserInfo
if aUserInfo != nil {
userInfo = authenticationv1.UserInfo{
Extra: make(map[string]authenticationv1.ExtraValue),
Groups: aUserInfo.GetGroups(),
UID: aUserInfo.GetUID(),
Username: aUserInfo.GetName(),
}
// Convert the extra information in the user object
for key, val := range aUserInfo.GetExtra() {
userInfo.Extra[key] = authenticationv1.ExtraValue(val)
}
}
dryRun := attr.IsDryRun()
return &admissionv1.AdmissionRequest{
Kind: metav1.GroupVersionKind{
Group: gvk.Group,
Kind: gvk.Kind,
Version: gvk.Version,
},
Resource: metav1.GroupVersionResource{
Group: gvr.Group,
Resource: gvr.Resource,
Version: gvr.Version,
},
SubResource: subresource,
RequestKind: &metav1.GroupVersionKind{
Group: requestGVK.Group,
Kind: requestGVK.Kind,
Version: requestGVK.Version,
},
RequestResource: &metav1.GroupVersionResource{
Group: requestGVR.Group,
Resource: requestGVR.Resource,
Version: requestGVR.Version,
},
RequestSubResource: requestSubResource,
Name: attr.GetName(),
Namespace: attr.GetNamespace(),
Operation: admissionv1.Operation(attr.GetOperation()),
UserInfo: userInfo,
// Leave Object and OldObject unset since we don't provide access to them via request
DryRun: &dryRun,
Options: runtime.RawExtension{
Object: attr.GetOperationOptions(),
},
}
}
// CreateNamespaceObject creates a Namespace object that is suitable for the CEL evaluation.
// If the namespace is nil, CreateNamespaceObject returns nil
func CreateNamespaceObject(namespace *v1.Namespace) *v1.Namespace {
if namespace == nil {
return nil
}
return &v1.Namespace{
Status: namespace.Status,
Spec: namespace.Spec,
ObjectMeta: metav1.ObjectMeta{
Name: namespace.Name,
GenerateName: namespace.GenerateName,
Namespace: namespace.Namespace,
UID: namespace.UID,
ResourceVersion: namespace.ResourceVersion,
Generation: namespace.Generation,
CreationTimestamp: namespace.CreationTimestamp,
DeletionTimestamp: namespace.DeletionTimestamp,
DeletionGracePeriodSeconds: namespace.DeletionGracePeriodSeconds,
Labels: namespace.Labels,
Annotations: namespace.Annotations,
Finalizers: namespace.Finalizers,
},
}
}
// CompilationErrors returns a list of all the errors from the compilation of the evaluator
func (e *filter) CompilationErrors() []error {
compilationErrors := []error{}
for _, result := range e.compilationResults {
if result.Error != nil {
compilationErrors = append(compilationErrors, result.Error)
}
}
return compilationErrors
}

View File

@ -63,12 +63,15 @@ type OptionalVariableDeclarations struct {
HasAuthorizer bool
// StrictCost specifies if the CEL cost limitation is strict for extended libraries as well as native libraries.
StrictCost bool
// HasPatchTypes specifies if JSONPatch, Object, Object.metadata and similar types are available in CEL. These can be used
// to initialize the typed objects in CEL required to create patches.
HasPatchTypes bool
}
// FilterCompiler contains a function to assist with converting types and values to/from CEL-typed values.
type FilterCompiler interface {
// Compile is used for the cel expression compilation
Compile(expressions []ExpressionAccessor, optionalDecls OptionalVariableDeclarations, envType environment.Type) Filter
// ConditionCompiler contains a function to assist with converting types and values to/from CEL-typed values.
type ConditionCompiler interface {
// CompileCondition is used for the cel expression compilation
CompileCondition(expressions []ExpressionAccessor, optionalDecls OptionalVariableDeclarations, envType environment.Type) ConditionEvaluator
}
// OptionalVariableBindings provides expression bindings for optional CEL variables.
@ -82,16 +85,38 @@ type OptionalVariableBindings struct {
Authorizer authorizer.Authorizer
}
// Filter contains a function to evaluate compiled CEL-typed values
// ConditionEvaluator contains the result of compiling a CEL expression
// that evaluates to a condition. This is used both for validation and pre-conditions.
// It expects the inbound object to already have been converted to the version expected
// by the underlying CEL code (which is indicated by the match criteria of a policy definition).
// versionedParams may be nil.
type Filter interface {
type ConditionEvaluator interface {
// ForInput converts compiled CEL-typed values into evaluated CEL-typed value.
// runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input.
// If cost budget is calculated, the filter should return the remaining budget.
// If cost budget is calculated, the condition should return the remaining budget.
ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *v1.AdmissionRequest, optionalVars OptionalVariableBindings, namespace *corev1.Namespace, runtimeCELCostBudget int64) ([]EvaluationResult, int64, error)
// CompilationErrors returns a list of errors from the compilation of the evaluator
// CompilationErrors returns a list of errors from the compilation of the mutatingEvaluator
CompilationErrors() []error
}
// MutatingCompiler contains a function to assist with converting types and values to/from CEL-typed values.
type MutatingCompiler interface {
// CompileMutatingEvaluator is used for the cel expression compilation
CompileMutatingEvaluator(expression ExpressionAccessor, optionalDecls OptionalVariableDeclarations, envType environment.Type) MutatingEvaluator
}
// MutatingEvaluator contains the result of compiling a CEL expression
// that evaluates to a mutation.
// It expects the inbound object to already have been converted to the version expected
// by the underlying CEL code (which is indicated by the match criteria of a policy definition).
// versionedParams may be nil.
type MutatingEvaluator interface {
// ForInput converts compiled CEL-typed values into a CEL-typed value representing a mutation.
// runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input.
// If cost budget is calculated, the condition should return the remaining budget.
ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *v1.AdmissionRequest, optionalVars OptionalVariableBindings, namespace *corev1.Namespace, runtimeCELCostBudget int64) (EvaluationResult, int64, error)
// CompilationErrors returns a list of errors from the compilation of the mutatingEvaluator
CompilationErrors() []error
}

View File

@ -0,0 +1,73 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cel
import (
"context"
admissionv1 "k8s.io/api/admission/v1"
v1 "k8s.io/api/core/v1"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/cel/environment"
)
// mutatingCompiler provides a MutatingCompiler implementation.
type mutatingCompiler struct {
compiler Compiler
}
// CompileMutatingEvaluator compiles a CEL expression for admission plugins and returns an MutatingEvaluator for executing the
// compiled CEL expression.
func (p *mutatingCompiler) CompileMutatingEvaluator(expressionAccessor ExpressionAccessor, options OptionalVariableDeclarations, mode environment.Type) MutatingEvaluator {
compilationResult := p.compiler.CompileCELExpression(expressionAccessor, options, mode)
return NewMutatingEvaluator(compilationResult)
}
type mutatingEvaluator struct {
compilationResult CompilationResult
}
func NewMutatingEvaluator(compilationResult CompilationResult) MutatingEvaluator {
return &mutatingEvaluator{compilationResult}
}
// ForInput evaluates the compiled CEL expression and returns an evaluation result
// errors per evaluation are returned in the evaluation result
// runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input.
func (p *mutatingEvaluator) ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *admissionv1.AdmissionRequest, inputs OptionalVariableBindings, namespace *v1.Namespace, runtimeCELCostBudget int64) (EvaluationResult, int64, error) {
// if this activation supports composition, we will need the compositionCtx. It may be nil.
compositionCtx, _ := ctx.(CompositionContext)
activation, err := newActivation(compositionCtx, versionedAttr, request, inputs, namespace)
if err != nil {
return EvaluationResult{}, -1, err
}
evaluation, remainingBudget, err := activation.Evaluate(ctx, compositionCtx, p.compilationResult, runtimeCELCostBudget)
if err != nil {
return evaluation, -1, err
}
return evaluation, remainingBudget, nil
}
// CompilationErrors returns a list of all the errors from the compilation of the mutatingEvaluator
func (p *mutatingEvaluator) CompilationErrors() (compilationErrors []error) {
if p.compilationResult.Error != nil {
return []error{p.compilationResult.Error}
}
return nil
}

View File

@ -26,6 +26,7 @@ type PolicyAccessor interface {
GetNamespace() string
GetParamKind() *v1.ParamKind
GetMatchConstraints() *v1.MatchResources
GetFailurePolicy() *v1.FailurePolicyType
}
type BindingAccessor interface {

View File

@ -49,6 +49,9 @@ type Source[H Hook] interface {
// Dispatcher dispatches evaluates an admission request against the currently
// active hooks returned by the source.
type Dispatcher[H Hook] interface {
// Start the dispatcher. This method should be called only once at startup.
Start(ctx context.Context) error
// Dispatch a request to the policies. Dispatcher may choose not to
// call a hook, either because the rules of the hook does not match, or
// the namespaceSelector or the objectSelector of the hook does not

View File

@ -36,8 +36,9 @@ import (
)
// H is the Hook type generated by the source and consumed by the dispatcher.
// !TODO: Just pass in a Plugin[H] with accessors to all this information
type sourceFactory[H any] func(informers.SharedInformerFactory, kubernetes.Interface, dynamic.Interface, meta.RESTMapper) Source[H]
type dispatcherFactory[H any] func(authorizer.Authorizer, *matching.Matcher) Dispatcher[H]
type dispatcherFactory[H any] func(authorizer.Authorizer, *matching.Matcher, kubernetes.Interface) Dispatcher[H]
// admissionResources is the list of resources related to CEL-based admission
// features.
@ -170,7 +171,7 @@ func (c *Plugin[H]) ValidateInitialization() error {
}
c.source = c.sourceFactory(c.informerFactory, c.client, c.dynamicClient, c.restMapper)
c.dispatcher = c.dispatcherFactory(c.authorizer, c.matcher)
c.dispatcher = c.dispatcherFactory(c.authorizer, c.matcher, c.client)
pluginContext, pluginContextCancel := context.WithCancel(context.Background())
go func() {
@ -181,10 +182,15 @@ func (c *Plugin[H]) ValidateInitialization() error {
go func() {
err := c.source.Run(pluginContext)
if err != nil && !errors.Is(err, context.Canceled) {
utilruntime.HandleError(fmt.Errorf("policy source context unexpectedly closed: %v", err))
utilruntime.HandleError(fmt.Errorf("policy source context unexpectedly closed: %w", err))
}
}()
err := c.dispatcher.Start(pluginContext)
if err != nil && !errors.Is(err, context.Canceled) {
utilruntime.HandleError(fmt.Errorf("policy dispatcher context unexpectedly closed: %w", err))
}
c.SetReadyFunc(func() bool {
return namespaceInformer.Informer().HasSynced() && c.source.HasSynced()
})

View File

@ -36,7 +36,7 @@ import (
"k8s.io/client-go/tools/cache"
)
// A policy invocation is a single policy-binding-param tuple from a Policy Hook
// PolicyInvocation is a single policy-binding-param tuple from a Policy Hook
// in the context of a specific request. The params have already been resolved
// and any error in configuration or setting up the invocation is stored in
// the Error field.
@ -62,10 +62,6 @@ type PolicyInvocation[P runtime.Object, B runtime.Object, E Evaluator] struct {
// Params fetched by the binding to use to evaluate the policy
Param runtime.Object
// Error is set if there was an error with the policy or binding or its
// params, etc
Error error
}
// dispatcherDelegate is called during a request with a pre-filtered list
@ -76,7 +72,7 @@ type PolicyInvocation[P runtime.Object, B runtime.Object, E Evaluator] struct {
//
// The delegate provides the "validation" or "mutation" aspect of dispatcher functionality
// (in contrast to generic.PolicyDispatcher which only selects active policies and params)
type dispatcherDelegate[P, B runtime.Object, E Evaluator] func(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces, versionedAttributes webhookgeneric.VersionedAttributeAccessor, invocations []PolicyInvocation[P, B, E]) error
type dispatcherDelegate[P, B runtime.Object, E Evaluator] func(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces, versionedAttributes webhookgeneric.VersionedAttributeAccessor, invocations []PolicyInvocation[P, B, E]) ([]PolicyError, *apierrors.StatusError)
type policyDispatcher[P runtime.Object, B runtime.Object, E Evaluator] struct {
newPolicyAccessor func(P) PolicyAccessor
@ -104,7 +100,10 @@ func NewPolicyDispatcher[P runtime.Object, B runtime.Object, E Evaluator](
// request. It then resolves all params and creates an Invocation for each
// matching policy-binding-param tuple. The delegate is then called with the
// list of tuples.
//
func (d *policyDispatcher[P, B, E]) Start(ctx context.Context) error {
return nil
}
// Note: MatchConditions expressions are not evaluated here. The dispatcher delegate
// is expected to ignore the result of any policies whose match conditions dont pass.
// This may be possible to refactor so matchconditions are checked here instead.
@ -117,29 +116,33 @@ func (d *policyDispatcher[P, B, E]) Dispatch(ctx context.Context, a admission.At
objectInterfaces: o,
}
var policyErrors []PolicyError
addConfigError := func(err error, definition PolicyAccessor, binding BindingAccessor) {
var message error
if binding == nil {
message = fmt.Errorf("failed to configure policy: %w", err)
} else {
message = fmt.Errorf("failed to configure binding: %w", err)
}
policyErrors = append(policyErrors, PolicyError{
Policy: definition,
Binding: binding,
Message: message,
})
}
for _, hook := range hooks {
policyAccessor := d.newPolicyAccessor(hook.Policy)
matches, matchGVR, matchGVK, err := d.matcher.DefinitionMatches(a, o, policyAccessor)
if err != nil {
// There was an error evaluating if this policy matches anything.
utilruntime.HandleError(err)
relevantHooks = append(relevantHooks, PolicyInvocation[P, B, E]{
Policy: hook.Policy,
Error: err,
})
addConfigError(err, policyAccessor, nil)
continue
} else if !matches {
continue
} else if hook.ConfigurationError != nil {
// The policy matches but there is a configuration error with the
// policy itself
relevantHooks = append(relevantHooks, PolicyInvocation[P, B, E]{
Policy: hook.Policy,
Error: hook.ConfigurationError,
Resource: matchGVR,
Kind: matchGVK,
})
utilruntime.HandleError(hook.ConfigurationError)
addConfigError(hook.ConfigurationError, policyAccessor, nil)
continue
}
@ -148,19 +151,22 @@ func (d *policyDispatcher[P, B, E]) Dispatch(ctx context.Context, a admission.At
matches, err = d.matcher.BindingMatches(a, o, bindingAccessor)
if err != nil {
// There was an error evaluating if this binding matches anything.
utilruntime.HandleError(err)
relevantHooks = append(relevantHooks, PolicyInvocation[P, B, E]{
Policy: hook.Policy,
Binding: binding,
Error: err,
Resource: matchGVR,
Kind: matchGVK,
})
addConfigError(err, policyAccessor, bindingAccessor)
continue
} else if !matches {
continue
}
// here the binding matches.
// VersionedAttr result will be cached and reused later during parallel
// hook calls.
if _, err = versionedAttrAccessor.VersionedAttribute(matchGVK); err != nil {
// VersionedAttr result will be cached and reused later during parallel
// hook calls.
addConfigError(err, policyAccessor, nil)
continue
}
// Collect params for this binding
params, err := CollectParams(
policyAccessor.GetParamKind(),
@ -171,14 +177,7 @@ func (d *policyDispatcher[P, B, E]) Dispatch(ctx context.Context, a admission.At
)
if err != nil {
// There was an error collecting params for this binding.
utilruntime.HandleError(err)
relevantHooks = append(relevantHooks, PolicyInvocation[P, B, E]{
Policy: hook.Policy,
Binding: binding,
Error: err,
Resource: matchGVR,
Kind: matchGVK,
})
addConfigError(err, policyAccessor, bindingAccessor)
continue
}
@ -194,23 +193,72 @@ func (d *policyDispatcher[P, B, E]) Dispatch(ctx context.Context, a admission.At
Evaluator: hook.Evaluator,
})
}
}
}
// VersionedAttr result will be cached and reused later during parallel
// hook calls
_, err = versionedAttrAccessor.VersionedAttribute(matchGVK)
if err != nil {
return apierrors.NewInternalError(err)
}
if len(relevantHooks) > 0 {
extraPolicyErrors, statusError := d.delegate(ctx, a, o, versionedAttrAccessor, relevantHooks)
if statusError != nil {
return statusError
}
policyErrors = append(policyErrors, extraPolicyErrors...)
}
var filteredErrors []PolicyError
for _, e := range policyErrors {
// we always default the FailurePolicy if it is unset and validate it in API level
var policy v1.FailurePolicyType
if fp := e.Policy.GetFailurePolicy(); fp == nil {
policy = v1.Fail
} else {
policy = *fp
}
switch policy {
case v1.Ignore:
// TODO: add metrics for ignored error here
continue
case v1.Fail:
filteredErrors = append(filteredErrors, e)
default:
filteredErrors = append(filteredErrors, e)
}
}
if len(relevantHooks) == 0 {
// no matching hooks
return nil
if len(filteredErrors) > 0 {
forbiddenErr := admission.NewForbidden(a, fmt.Errorf("admission request denied by policy"))
// The forbiddenErr is always a StatusError.
var err *apierrors.StatusError
if !errors.As(forbiddenErr, &err) {
// Should never happen.
return apierrors.NewInternalError(fmt.Errorf("failed to create status error"))
}
err.ErrStatus.Message = ""
for _, policyError := range filteredErrors {
message := policyError.Error()
// If this is the first denied decision, use its message and reason
// for the status error message.
if err.ErrStatus.Message == "" {
err.ErrStatus.Message = message
if policyError.Reason != "" {
err.ErrStatus.Reason = policyError.Reason
}
}
// Add the denied decision's message to the status error's details
err.ErrStatus.Details.Causes = append(
err.ErrStatus.Details.Causes,
metav1.StatusCause{Message: message})
}
return err
}
return d.delegate(ctx, a, o, versionedAttrAccessor, relevantHooks)
return nil
}
// Returns params to use to evaluate a policy-binding with given param
@ -352,3 +400,18 @@ func (v *versionedAttributeAccessor) VersionedAttribute(gvk schema.GroupVersionK
v.versionedAttrs[gvk] = versionedAttr
return versionedAttr, nil
}
type PolicyError struct {
Policy PolicyAccessor
Binding BindingAccessor
Message error
Reason metav1.StatusReason
}
func (c PolicyError) Error() string {
if c.Binding != nil {
return fmt.Sprintf("policy '%s' with binding '%s' denied request: %s", c.Policy.GetName(), c.Binding.GetName(), c.Message.Error())
}
return fmt.Sprintf("policy %q denied request: %s", c.Policy.GetName(), c.Message.Error())
}

View File

@ -41,6 +41,13 @@ import (
"k8s.io/klog/v2"
)
// Interval for refreshing policies.
// TODO: Consider reducing this to a shorter duration or replacing this entirely
// with checks that detect when a policy change took effect.
const policyRefreshIntervalDefault = 1 * time.Second
var policyRefreshInterval = policyRefreshIntervalDefault
type policySource[P runtime.Object, B runtime.Object, E Evaluator] struct {
ctx context.Context
policyInformer generic.Informer[P]
@ -122,6 +129,15 @@ func NewPolicySource[P runtime.Object, B runtime.Object, E Evaluator](
return res
}
// SetPolicyRefreshIntervalForTests allows the refresh interval to be overridden during tests.
// This should only be called from tests.
func SetPolicyRefreshIntervalForTests(interval time.Duration) func() {
policyRefreshInterval = interval
return func() {
policyRefreshInterval = policyRefreshIntervalDefault
}
}
func (s *policySource[P, B, E]) Run(ctx context.Context) error {
if s.ctx != nil {
return fmt.Errorf("policy source already running")
@ -178,7 +194,7 @@ func (s *policySource[P, B, E]) Run(ctx context.Context) error {
// and needs to be recompiled
go func() {
// Loop every 1 second until context is cancelled, refreshing policies
wait.Until(s.refreshPolicies, 1*time.Second, ctx.Done())
wait.Until(s.refreshPolicies, policyRefreshInterval, ctx.Done())
}()
<-ctx.Done()

View File

@ -45,7 +45,6 @@ import (
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/initializer"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/features"
)
// PolicyTestContext is everything you need to unit test a policy plugin
@ -196,18 +195,6 @@ func NewPolicyTestContext[P, B runtime.Object, E Evaluator](
plugin.SetEnabled(true)
featureGate := featuregate.NewFeatureGate()
err = featureGate.Add(map[featuregate.Feature]featuregate.FeatureSpec{
//!TODO: move this to validating specific tests
features.ValidatingAdmissionPolicy: {
Default: true, PreRelease: featuregate.Beta}})
if err != nil {
return nil, nil, err
}
err = featureGate.SetFromMap(map[string]bool{string(features.ValidatingAdmissionPolicy): true})
if err != nil {
return nil, nil, err
}
testContext, testCancel := context.WithCancel(context.Background())
genericInitializer := initializer.New(
nativeClient,

View File

@ -0,0 +1,144 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package mutating
import (
v1 "k8s.io/api/admissionregistration/v1"
"k8s.io/api/admissionregistration/v1alpha1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apiserver/pkg/admission/plugin/policy/generic"
)
func NewMutatingAdmissionPolicyAccessor(obj *Policy) generic.PolicyAccessor {
return &mutatingAdmissionPolicyAccessor{
Policy: obj,
}
}
func NewMutatingAdmissionPolicyBindingAccessor(obj *PolicyBinding) generic.BindingAccessor {
return &mutatingAdmissionPolicyBindingAccessor{
PolicyBinding: obj,
}
}
type mutatingAdmissionPolicyAccessor struct {
*Policy
}
func (v *mutatingAdmissionPolicyAccessor) GetNamespace() string {
return v.Namespace
}
func (v *mutatingAdmissionPolicyAccessor) GetName() string {
return v.Name
}
func (v *mutatingAdmissionPolicyAccessor) GetParamKind() *v1.ParamKind {
pk := v.Spec.ParamKind
if pk == nil {
return nil
}
return &v1.ParamKind{
APIVersion: pk.APIVersion,
Kind: pk.Kind,
}
}
func (v *mutatingAdmissionPolicyAccessor) GetMatchConstraints() *v1.MatchResources {
return convertV1alpha1ResourceRulesToV1(v.Spec.MatchConstraints)
}
func (v *mutatingAdmissionPolicyAccessor) GetFailurePolicy() *v1.FailurePolicyType {
return toV1FailurePolicy(v.Spec.FailurePolicy)
}
func toV1FailurePolicy(failurePolicy *v1alpha1.FailurePolicyType) *v1.FailurePolicyType {
if failurePolicy == nil {
return nil
}
fp := v1.FailurePolicyType(*failurePolicy)
return &fp
}
type mutatingAdmissionPolicyBindingAccessor struct {
*PolicyBinding
}
func (v *mutatingAdmissionPolicyBindingAccessor) GetNamespace() string {
return v.Namespace
}
func (v *mutatingAdmissionPolicyBindingAccessor) GetName() string {
return v.Name
}
func (v *mutatingAdmissionPolicyBindingAccessor) GetPolicyName() types.NamespacedName {
return types.NamespacedName{
Namespace: "",
Name: v.Spec.PolicyName,
}
}
func (v *mutatingAdmissionPolicyBindingAccessor) GetMatchResources() *v1.MatchResources {
return convertV1alpha1ResourceRulesToV1(v.Spec.MatchResources)
}
func (v *mutatingAdmissionPolicyBindingAccessor) GetParamRef() *v1.ParamRef {
if v.Spec.ParamRef == nil {
return nil
}
var nfa *v1.ParameterNotFoundActionType
if v.Spec.ParamRef.ParameterNotFoundAction != nil {
nfa = new(v1.ParameterNotFoundActionType)
*nfa = v1.ParameterNotFoundActionType(*v.Spec.ParamRef.ParameterNotFoundAction)
}
return &v1.ParamRef{
Name: v.Spec.ParamRef.Name,
Namespace: v.Spec.ParamRef.Namespace,
Selector: v.Spec.ParamRef.Selector,
ParameterNotFoundAction: nfa,
}
}
func convertV1alpha1ResourceRulesToV1(mc *v1alpha1.MatchResources) *v1.MatchResources {
if mc == nil {
return nil
}
var res v1.MatchResources
res.NamespaceSelector = mc.NamespaceSelector
res.ObjectSelector = mc.ObjectSelector
for _, ex := range mc.ExcludeResourceRules {
res.ExcludeResourceRules = append(res.ExcludeResourceRules, v1.NamedRuleWithOperations{
ResourceNames: ex.ResourceNames,
RuleWithOperations: ex.RuleWithOperations,
})
}
for _, ex := range mc.ResourceRules {
res.ResourceRules = append(res.ResourceRules, v1.NamedRuleWithOperations{
ResourceNames: ex.ResourceNames,
RuleWithOperations: ex.RuleWithOperations,
})
}
if mc.MatchPolicy != nil {
mp := v1.MatchPolicyType(*mc.MatchPolicy)
res.MatchPolicy = &mp
}
return &res
}

View File

@ -0,0 +1,81 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package mutating
import (
"fmt"
"k8s.io/api/admissionregistration/v1alpha1"
plugincel "k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch"
"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions"
apiservercel "k8s.io/apiserver/pkg/cel"
"k8s.io/apiserver/pkg/cel/environment"
)
// compilePolicy compiles the policy into a PolicyEvaluator
// any error is stored and delayed until invocation.
//
// Each individual mutation is compiled into MutationEvaluationFunc and
// returned is a PolicyEvaluator in the same order as the mutations appeared in the policy.
func compilePolicy(policy *Policy) PolicyEvaluator {
opts := plugincel.OptionalVariableDeclarations{HasParams: policy.Spec.ParamKind != nil, StrictCost: true, HasAuthorizer: true}
compiler, err := plugincel.NewCompositedCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true))
if err != nil {
return PolicyEvaluator{Error: &apiservercel.Error{
Type: apiservercel.ErrorTypeInternal,
Detail: fmt.Sprintf("failed to initialize CEL compiler: %v", err),
}}
}
// Compile and store variables
compiler.CompileAndStoreVariables(convertv1alpha1Variables(policy.Spec.Variables), opts, environment.StoredExpressions)
// Compile matchers
var matcher matchconditions.Matcher = nil
matchConditions := policy.Spec.MatchConditions
if len(matchConditions) > 0 {
matchExpressionAccessors := make([]plugincel.ExpressionAccessor, len(matchConditions))
for i := range matchConditions {
matchExpressionAccessors[i] = (*matchconditions.MatchCondition)(&matchConditions[i])
}
matcher = matchconditions.NewMatcher(compiler.CompileCondition(matchExpressionAccessors, opts, environment.StoredExpressions), toV1FailurePolicy(policy.Spec.FailurePolicy), "policy", "validate", policy.Name)
}
// Compiler patchers
var patchers []patch.Patcher
patchOptions := opts
patchOptions.HasPatchTypes = true
for _, m := range policy.Spec.Mutations {
switch m.PatchType {
case v1alpha1.PatchTypeJSONPatch:
if m.JSONPatch != nil {
accessor := &patch.JSONPatchCondition{Expression: m.JSONPatch.Expression}
compileResult := compiler.CompileMutatingEvaluator(accessor, patchOptions, environment.StoredExpressions)
patchers = append(patchers, patch.NewJSONPatcher(compileResult))
}
case v1alpha1.PatchTypeApplyConfiguration:
if m.ApplyConfiguration != nil {
accessor := &patch.ApplyConfigurationCondition{Expression: m.ApplyConfiguration.Expression}
compileResult := compiler.CompileMutatingEvaluator(accessor, patchOptions, environment.StoredExpressions)
patchers = append(patchers, patch.NewApplyConfigurationPatcher(compileResult))
}
}
}
return PolicyEvaluator{Matcher: matcher, Mutators: patchers, CompositionEnv: compiler.CompositionEnv}
}

View File

@ -0,0 +1,295 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package mutating
import (
"context"
"errors"
"fmt"
"k8s.io/api/admissionregistration/v1alpha1"
v1 "k8s.io/api/core/v1"
apiequality "k8s.io/apimachinery/pkg/api/equality"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apiserver/pkg/admission"
admissionauthorizer "k8s.io/apiserver/pkg/admission/plugin/authorizer"
"k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/admission/plugin/policy/generic"
"k8s.io/apiserver/pkg/admission/plugin/policy/matching"
"k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch"
webhookgeneric "k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
celconfig "k8s.io/apiserver/pkg/apis/cel"
"k8s.io/apiserver/pkg/authorization/authorizer"
)
func NewDispatcher(a authorizer.Authorizer, m *matching.Matcher, tcm patch.TypeConverterManager) generic.Dispatcher[PolicyHook] {
res := &dispatcher{
matcher: m,
authz: a,
typeConverterManager: tcm,
}
res.Dispatcher = generic.NewPolicyDispatcher[*Policy, *PolicyBinding, PolicyEvaluator](
NewMutatingAdmissionPolicyAccessor,
NewMutatingAdmissionPolicyBindingAccessor,
m,
res.dispatchInvocations,
)
return res
}
type dispatcher struct {
matcher *matching.Matcher
authz authorizer.Authorizer
typeConverterManager patch.TypeConverterManager
generic.Dispatcher[PolicyHook]
}
func (d *dispatcher) Start(ctx context.Context) error {
go d.typeConverterManager.Run(ctx)
return d.Dispatcher.Start(ctx)
}
func (d *dispatcher) dispatchInvocations(
ctx context.Context,
a admission.Attributes,
o admission.ObjectInterfaces,
versionedAttributes webhookgeneric.VersionedAttributeAccessor,
invocations []generic.PolicyInvocation[*Policy, *PolicyBinding, PolicyEvaluator],
) ([]generic.PolicyError, *k8serrors.StatusError) {
var lastVersionedAttr *admission.VersionedAttributes
reinvokeCtx := a.GetReinvocationContext()
var policyReinvokeCtx *policyReinvokeContext
if v := reinvokeCtx.Value(PluginName); v != nil {
policyReinvokeCtx = v.(*policyReinvokeContext)
} else {
policyReinvokeCtx = &policyReinvokeContext{}
reinvokeCtx.SetValue(PluginName, policyReinvokeCtx)
}
if reinvokeCtx.IsReinvoke() && policyReinvokeCtx.IsOutputChangedSinceLastPolicyInvocation(a.GetObject()) {
// If the object has changed, we know the in-tree plugin re-invocations have mutated the object,
// and we need to reinvoke all eligible policies.
policyReinvokeCtx.RequireReinvokingPreviouslyInvokedPlugins()
}
defer func() {
policyReinvokeCtx.SetLastPolicyInvocationOutput(a.GetObject())
}()
var policyErrors []generic.PolicyError
addConfigError := func(err error, invocation generic.PolicyInvocation[*Policy, *PolicyBinding, PolicyEvaluator], reason metav1.StatusReason) {
policyErrors = append(policyErrors, generic.PolicyError{
Message: err,
Policy: NewMutatingAdmissionPolicyAccessor(invocation.Policy),
Binding: NewMutatingAdmissionPolicyBindingAccessor(invocation.Binding),
Reason: reason,
})
}
// There is at least one invocation to invoke. Make sure we have a namespace
// object if the incoming object is not cluster scoped to pass into the evaluator.
var namespace *v1.Namespace
var err error
namespaceName := a.GetNamespace()
// Special case, the namespace object has the namespace of itself (maybe a bug).
// unset it if the incoming object is a namespace
if gvk := a.GetKind(); gvk.Kind == "Namespace" && gvk.Version == "v1" && gvk.Group == "" {
namespaceName = ""
}
// if it is cluster scoped, namespaceName will be empty
// Otherwise, get the Namespace resource.
if namespaceName != "" {
namespace, err = d.matcher.GetNamespace(namespaceName)
if err != nil {
return nil, k8serrors.NewNotFound(schema.GroupResource{Group: "", Resource: "namespaces"}, namespaceName)
}
}
authz := admissionauthorizer.NewCachingAuthorizer(d.authz)
// Should loop through invocations, handling possible error and invoking
// evaluator to apply patch, also should handle re-invocations
for _, invocation := range invocations {
if invocation.Evaluator.CompositionEnv != nil {
ctx = invocation.Evaluator.CompositionEnv.CreateContext(ctx)
}
if len(invocation.Evaluator.Mutators) != len(invocation.Policy.Spec.Mutations) {
// This would be a bug. The compiler should always return exactly as
// many evaluators as there are mutations
return nil, k8serrors.NewInternalError(fmt.Errorf("expected %v compiled evaluators for policy %v, got %v",
len(invocation.Policy.Spec.Mutations), invocation.Policy.Name, len(invocation.Evaluator.Mutators)))
}
versionedAttr, err := versionedAttributes.VersionedAttribute(invocation.Kind)
if err != nil {
// This should never happen, we pre-warm versoined attribute
// accessors before starting the dispatcher
return nil, k8serrors.NewInternalError(err)
}
if invocation.Evaluator.Matcher != nil {
matchResults := invocation.Evaluator.Matcher.Match(ctx, versionedAttr, invocation.Param, authz)
if matchResults.Error != nil {
addConfigError(matchResults.Error, invocation, metav1.StatusReasonInvalid)
continue
}
// if preconditions are not met, then skip mutations
if !matchResults.Matches {
continue
}
}
invocationKey, invocationKeyErr := keyFor(invocation)
if invocationKeyErr != nil {
// This should never happen. It occurs if there is a programming
// error causing the Param not to be a valid object.
return nil, k8serrors.NewInternalError(invocationKeyErr)
}
if reinvokeCtx.IsReinvoke() && !policyReinvokeCtx.ShouldReinvoke(invocationKey) {
continue
}
objectBeforeMutations := versionedAttr.VersionedObject
// Mutations for a single invocation of a MutatingAdmissionPolicy are evaluated
// in order.
for mutationIndex := range invocation.Policy.Spec.Mutations {
lastVersionedAttr = versionedAttr
if versionedAttr.VersionedObject == nil { // Do not call patchers if there is no object to patch.
continue
}
patcher := invocation.Evaluator.Mutators[mutationIndex]
optionalVariables := cel.OptionalVariableBindings{VersionedParams: invocation.Param, Authorizer: authz}
err = d.dispatchOne(ctx, patcher, o, versionedAttr, namespace, invocation.Resource, optionalVariables)
if err != nil {
var statusError *k8serrors.StatusError
if errors.As(err, &statusError) {
return nil, statusError
}
addConfigError(err, invocation, metav1.StatusReasonInvalid)
continue
}
}
if !apiequality.Semantic.DeepEqual(objectBeforeMutations, versionedAttr.VersionedObject) {
// The mutation has changed the object. Prepare to reinvoke all previous mutations that are eligible for re-invocation.
policyReinvokeCtx.RequireReinvokingPreviouslyInvokedPlugins()
reinvokeCtx.SetShouldReinvoke()
}
if invocation.Policy.Spec.ReinvocationPolicy == v1alpha1.IfNeededReinvocationPolicy {
policyReinvokeCtx.AddReinvocablePolicyToPreviouslyInvoked(invocationKey)
}
}
if lastVersionedAttr != nil && lastVersionedAttr.VersionedObject != nil && lastVersionedAttr.Dirty {
policyReinvokeCtx.RequireReinvokingPreviouslyInvokedPlugins()
reinvokeCtx.SetShouldReinvoke()
if err := o.GetObjectConvertor().Convert(lastVersionedAttr.VersionedObject, lastVersionedAttr.Attributes.GetObject(), nil); err != nil {
return nil, k8serrors.NewInternalError(fmt.Errorf("failed to convert object: %w", err))
}
}
return policyErrors, nil
}
func (d *dispatcher) dispatchOne(
ctx context.Context,
patcher patch.Patcher,
o admission.ObjectInterfaces,
versionedAttributes *admission.VersionedAttributes,
namespace *v1.Namespace,
resource schema.GroupVersionResource,
optionalVariables cel.OptionalVariableBindings,
) (err error) {
if patcher == nil {
// internal error. this should not happen
return k8serrors.NewInternalError(fmt.Errorf("policy evaluator is nil"))
}
// Find type converter for the invoked Group-Version.
typeConverter := d.typeConverterManager.GetTypeConverter(versionedAttributes.VersionedKind)
if typeConverter == nil {
// This can happen if the request is for a resource whose schema
// has not been registered with the type converter manager.
return k8serrors.NewServiceUnavailable(fmt.Sprintf("Resource kind %s not found. There can be a delay between when CustomResourceDefinitions are created and when they are available.", versionedAttributes.VersionedKind))
}
patchRequest := patch.Request{
MatchedResource: resource,
VersionedAttributes: versionedAttributes,
ObjectInterfaces: o,
OptionalVariables: optionalVariables,
Namespace: namespace,
TypeConverter: typeConverter,
}
newVersionedObject, err := patcher.Patch(ctx, patchRequest, celconfig.RuntimeCELCostBudget)
if err != nil {
return err
}
switch versionedAttributes.VersionedObject.(type) {
case *unstructured.Unstructured:
// No conversion needed before defaulting for the patch object if the admitted object is unstructured.
default:
// Before defaulting, if the admitted object is a typed object, convert unstructured patch result back to a typed object.
newVersionedObject, err = o.GetObjectConvertor().ConvertToVersion(newVersionedObject, versionedAttributes.GetKind().GroupVersion())
if err != nil {
return err
}
}
o.GetObjectDefaulter().Default(newVersionedObject)
versionedAttributes.Dirty = true
versionedAttributes.VersionedObject = newVersionedObject
return nil
}
func keyFor(invocation generic.PolicyInvocation[*Policy, *PolicyBinding, PolicyEvaluator]) (key, error) {
var paramUID types.NamespacedName
if invocation.Param != nil {
paramAccessor, err := meta.Accessor(invocation.Param)
if err != nil {
// This should never happen, as the param should have been validated
// before being passed to the plugin.
return key{}, err
}
paramUID = types.NamespacedName{
Name: paramAccessor.GetName(),
Namespace: paramAccessor.GetNamespace(),
}
}
return key{
PolicyUID: types.NamespacedName{
Name: invocation.Policy.GetName(),
Namespace: invocation.Policy.GetNamespace(),
},
BindingUID: types.NamespacedName{
Name: invocation.Binding.GetName(),
Namespace: invocation.Binding.GetNamespace(),
},
ParamUID: paramUID,
}, nil
}

View File

@ -0,0 +1,45 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package patch
import (
"context"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/managedfields"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/cel"
)
// Patcher provides a patch function to perform a mutation to an object in the admission chain.
type Patcher interface {
// Patch returns a copy of the object in the request, modified to change specified by the patch.
// The original object in the request MUST NOT be modified in-place.
Patch(ctx context.Context, request Request, runtimeCELCostBudget int64) (runtime.Object, error)
}
// Request defines the arguments required by a patcher.
type Request struct {
MatchedResource schema.GroupVersionResource
VersionedAttributes *admission.VersionedAttributes
ObjectInterfaces admission.ObjectInterfaces
OptionalVariables cel.OptionalVariableBindings
Namespace *v1.Namespace
TypeConverter managedfields.TypeConverter
}

View File

@ -0,0 +1,192 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package patch
import (
"context"
gojson "encoding/json"
"errors"
"fmt"
celgo "github.com/google/cel-go/cel"
"reflect"
"strconv"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/traits"
"google.golang.org/protobuf/types/known/structpb"
jsonpatch "gopkg.in/evanphx/json-patch.v4"
admissionv1 "k8s.io/api/admission/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer/json"
plugincel "k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/cel/mutation"
"k8s.io/apiserver/pkg/cel/mutation/dynamic"
pointer "k8s.io/utils/ptr"
)
// JSONPatchCondition contains the inputs needed to compile and evaluate a cel expression
// that returns a JSON patch value.
type JSONPatchCondition struct {
Expression string
}
var _ plugincel.ExpressionAccessor = &JSONPatchCondition{}
func (v *JSONPatchCondition) GetExpression() string {
return v.Expression
}
func (v *JSONPatchCondition) ReturnTypes() []*celgo.Type {
return []*celgo.Type{celgo.ListType(jsonPatchType)}
}
var jsonPatchType = types.NewObjectType("JSONPatch")
// NewJSONPatcher creates a patcher that performs a JSON Patch mutation.
func NewJSONPatcher(patchEvaluator plugincel.MutatingEvaluator) Patcher {
return &jsonPatcher{patchEvaluator}
}
type jsonPatcher struct {
PatchEvaluator plugincel.MutatingEvaluator
}
func (e *jsonPatcher) Patch(ctx context.Context, r Request, runtimeCELCostBudget int64) (runtime.Object, error) {
admissionRequest := plugincel.CreateAdmissionRequest(
r.VersionedAttributes.Attributes,
metav1.GroupVersionResource(r.MatchedResource),
metav1.GroupVersionKind(r.VersionedAttributes.VersionedKind))
compileErrors := e.PatchEvaluator.CompilationErrors()
if len(compileErrors) > 0 {
return nil, errors.Join(compileErrors...)
}
patchObj, _, err := e.evaluatePatchExpression(ctx, e.PatchEvaluator, runtimeCELCostBudget, r, admissionRequest)
if err != nil {
return nil, err
}
o := r.ObjectInterfaces
jsonSerializer := json.NewSerializerWithOptions(json.DefaultMetaFactory, o.GetObjectCreater(), o.GetObjectTyper(), json.SerializerOptions{Pretty: false, Strict: true})
objJS, err := runtime.Encode(jsonSerializer, r.VersionedAttributes.VersionedObject)
if err != nil {
return nil, fmt.Errorf("failed to create JSON patch: %w", err)
}
patchedJS, err := patchObj.Apply(objJS)
if err != nil {
if errors.Is(err, jsonpatch.ErrTestFailed) {
// If a json patch fails a test operation, the patch must not be applied
return r.VersionedAttributes.VersionedObject, nil
}
return nil, fmt.Errorf("JSON Patch: %w", err)
}
var newVersionedObject runtime.Object
if _, ok := r.VersionedAttributes.VersionedObject.(*unstructured.Unstructured); ok {
newVersionedObject = &unstructured.Unstructured{}
} else {
newVersionedObject, err = o.GetObjectCreater().New(r.VersionedAttributes.VersionedKind)
if err != nil {
return nil, apierrors.NewInternalError(err)
}
}
if newVersionedObject, _, err = jsonSerializer.Decode(patchedJS, nil, newVersionedObject); err != nil {
return nil, apierrors.NewInternalError(err)
}
return newVersionedObject, nil
}
func (e *jsonPatcher) evaluatePatchExpression(ctx context.Context, patchEvaluator plugincel.MutatingEvaluator, remainingBudget int64, r Request, admissionRequest *admissionv1.AdmissionRequest) (jsonpatch.Patch, int64, error) {
var err error
var eval plugincel.EvaluationResult
eval, remainingBudget, err = patchEvaluator.ForInput(ctx, r.VersionedAttributes, admissionRequest, r.OptionalVariables, r.Namespace, remainingBudget)
if err != nil {
return nil, -1, err
}
if eval.Error != nil {
return nil, -1, eval.Error
}
refVal := eval.EvalResult
// the return type can be any valid CEL value.
// Scalars, maps and lists are used to set the value when the path points to a field of that type.
// ObjectVal is used when the path points to a struct. A map like "{"field1": 1, "fieldX": bool}" is not
// possible in Kubernetes CEL because maps and lists may not have mixed types.
iter, ok := refVal.(traits.Lister)
if !ok {
// Should never happen since compiler checks return type.
return nil, -1, fmt.Errorf("type mismatch: JSONPatchType.expression should evaluate to array")
}
result := jsonpatch.Patch{}
for it := iter.Iterator(); it.HasNext() == types.True; {
v := it.Next()
patchObj, err := v.ConvertToNative(reflect.TypeOf(&mutation.JSONPatchVal{}))
if err != nil {
// Should never happen since return type is checked by compiler.
return nil, -1, fmt.Errorf("type mismatch: JSONPatchType.expression should evaluate to array of JSONPatch: %w", err)
}
op, ok := patchObj.(*mutation.JSONPatchVal)
if !ok {
// Should never happen since return type is checked by compiler.
return nil, -1, fmt.Errorf("type mismatch: JSONPatchType.expression should evaluate to array of JSONPatch, got element of %T", patchObj)
}
// Construct a JSON Patch from the evaluated CEL expression
resultOp := jsonpatch.Operation{}
resultOp["op"] = pointer.To(gojson.RawMessage(strconv.Quote(op.Op)))
resultOp["path"] = pointer.To(gojson.RawMessage(strconv.Quote(op.Path)))
if len(op.From) > 0 {
resultOp["from"] = pointer.To(gojson.RawMessage(strconv.Quote(op.From)))
}
if op.Val != nil {
if objVal, ok := op.Val.(*dynamic.ObjectVal); ok {
// TODO: Object initializers are insufficiently type checked.
// In the interim, we use this sanity check to detect type mismatches
// between field names and Object initializers. For example,
// "Object.spec{ selector: Object.spec.wrong{}}" is detected as a mismatch.
// Before beta, attaching full type information both to Object initializers and
// the "object" and "oldObject" variables is needed. This will allow CEL to
// perform comprehensive runtime type checking.
err := objVal.CheckTypeNamesMatchFieldPathNames()
if err != nil {
return nil, -1, fmt.Errorf("type mismatch: %w", err)
}
}
// CEL data literals representing arbitrary JSON values can be serialized to JSON for use in
// JSON Patch if first converted to pb.Value.
v, err := op.Val.ConvertToNative(reflect.TypeOf(&structpb.Value{}))
if err != nil {
return nil, -1, fmt.Errorf("JSONPath valueExpression evaluated to a type that could not marshal to JSON: %w", err)
}
b, err := gojson.Marshal(v)
if err != nil {
return nil, -1, fmt.Errorf("JSONPath valueExpression evaluated to a type that could not marshal to JSON: %w", err)
}
resultOp["value"] = pointer.To[gojson.RawMessage](b)
}
result = append(result, resultOp)
}
return result, remainingBudget, nil
}

View File

@ -0,0 +1,217 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package patch
import (
"context"
"errors"
"fmt"
celgo "github.com/google/cel-go/cel"
celtypes "github.com/google/cel-go/common/types"
"strings"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
"sigs.k8s.io/structured-merge-diff/v4/schema"
"sigs.k8s.io/structured-merge-diff/v4/typed"
"sigs.k8s.io/structured-merge-diff/v4/value"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/managedfields"
plugincel "k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/cel/mutation/dynamic"
)
// ApplyConfigurationCondition contains the inputs needed to compile and evaluate a cel expression
// that returns an apply configuration
type ApplyConfigurationCondition struct {
Expression string
}
var _ plugincel.ExpressionAccessor = &ApplyConfigurationCondition{}
func (v *ApplyConfigurationCondition) GetExpression() string {
return v.Expression
}
func (v *ApplyConfigurationCondition) ReturnTypes() []*celgo.Type {
return []*celgo.Type{applyConfigObjectType}
}
var applyConfigObjectType = celtypes.NewObjectType("Object")
// NewApplyConfigurationPatcher creates a patcher that performs an applyConfiguration mutation.
func NewApplyConfigurationPatcher(expressionEvaluator plugincel.MutatingEvaluator) Patcher {
return &applyConfigPatcher{expressionEvaluator: expressionEvaluator}
}
type applyConfigPatcher struct {
expressionEvaluator plugincel.MutatingEvaluator
}
func (e *applyConfigPatcher) Patch(ctx context.Context, r Request, runtimeCELCostBudget int64) (runtime.Object, error) {
admissionRequest := plugincel.CreateAdmissionRequest(
r.VersionedAttributes.Attributes,
metav1.GroupVersionResource(r.MatchedResource),
metav1.GroupVersionKind(r.VersionedAttributes.VersionedKind))
compileErrors := e.expressionEvaluator.CompilationErrors()
if len(compileErrors) > 0 {
return nil, errors.Join(compileErrors...)
}
eval, _, err := e.expressionEvaluator.ForInput(ctx, r.VersionedAttributes, admissionRequest, r.OptionalVariables, r.Namespace, runtimeCELCostBudget)
if err != nil {
return nil, err
}
if eval.Error != nil {
return nil, eval.Error
}
v := eval.EvalResult
// The compiler ensures that the return type is an ObjectVal with type name of "Object".
objVal, ok := v.(*dynamic.ObjectVal)
if !ok {
// Should not happen since the compiler type checks the return type.
return nil, fmt.Errorf("unsupported return type from ApplyConfiguration expression: %v", v.Type())
}
// TODO: Object initializers are insufficiently type checked.
// In the interim, we use this sanity check to detect type mismatches
// between field names and Object initializers. For example,
// "Object.spec{ selector: Object.spec.wrong{}}" is detected as a mismatch.
// Before beta, attaching full type information both to Object initializers and
// the "object" and "oldObject" variables is needed. This will allow CEL to
// perform comprehensive runtime type checking.
err = objVal.CheckTypeNamesMatchFieldPathNames()
if err != nil {
return nil, fmt.Errorf("type mismatch: %w", err)
}
value, ok := objVal.Value().(map[string]any)
if !ok {
return nil, fmt.Errorf("invalid return type: %T", v)
}
patchObject := unstructured.Unstructured{Object: value}
patchObject.SetGroupVersionKind(r.VersionedAttributes.VersionedObject.GetObjectKind().GroupVersionKind())
patched, err := ApplyStructuredMergeDiff(r.TypeConverter, r.VersionedAttributes.VersionedObject, &patchObject)
if err != nil {
return nil, fmt.Errorf("error applying patch: %w", err)
}
return patched, nil
}
// ApplyStructuredMergeDiff applies a structured merge diff to an object and returns a copy of the object
// with the patch applied.
func ApplyStructuredMergeDiff(
typeConverter managedfields.TypeConverter,
originalObject runtime.Object,
patch *unstructured.Unstructured,
) (runtime.Object, error) {
if patch.GroupVersionKind() != originalObject.GetObjectKind().GroupVersionKind() {
return nil, fmt.Errorf("patch (%v) and original object (%v) are not of the same gvk", patch.GroupVersionKind().String(), originalObject.GetObjectKind().GroupVersionKind().String())
} else if typeConverter == nil {
return nil, fmt.Errorf("type converter must not be nil")
}
patchObjTyped, err := typeConverter.ObjectToTyped(patch)
if err != nil {
return nil, fmt.Errorf("failed to convert patch object to typed object: %w", err)
}
err = validatePatch(patchObjTyped)
if err != nil {
return nil, fmt.Errorf("invalid ApplyConfiguration: %w", err)
}
liveObjTyped, err := typeConverter.ObjectToTyped(originalObject)
if err != nil {
return nil, fmt.Errorf("failed to convert original object to typed object: %w", err)
}
newObjTyped, err := liveObjTyped.Merge(patchObjTyped)
if err != nil {
return nil, fmt.Errorf("failed to merge patch: %w", err)
}
// Our mutating admission policy sets the fields but does not track ownership.
// Newly introduced fields in the patch won't be tracked by a field manager
// (so if the original object is updated again but the mutating policy is
// not active, the fields will be dropped).
//
// This necessarily means that changes to an object by a mutating policy
// are only preserved if the policy was active at the time of the change.
// (If the policy is not active, the changes may be dropped.)
newObj, err := typeConverter.TypedToObject(newObjTyped)
if err != nil {
return nil, fmt.Errorf("failed to convert typed object to object: %w", err)
}
return newObj, nil
}
// validatePatch searches an apply configuration for any arrays, maps or structs elements that are atomic and returns
// an error if any are found.
// This prevents accidental removal of fields that can occur when the user intends to modify some
// fields in an atomic type, not realizing that all fields not explicitly set in the new value
// of the atomic will be removed.
func validatePatch(v *typed.TypedValue) error {
atomics := findAtomics(nil, v.Schema(), v.TypeRef(), v.AsValue())
if len(atomics) > 0 {
return fmt.Errorf("may not mutate atomic arrays, maps or structs: %v", strings.Join(atomics, ", "))
}
return nil
}
// findAtomics returns field paths for any atomic arrays, maps or structs found when traversing the given value.
func findAtomics(path []fieldpath.PathElement, s *schema.Schema, tr schema.TypeRef, v value.Value) (atomics []string) {
if a, ok := s.Resolve(tr); ok { // Validation pass happens before this and checks that all schemas can be resolved
if v.IsMap() && a.Map != nil {
if a.Map.ElementRelationship == schema.Atomic {
atomics = append(atomics, pathString(path))
}
v.AsMap().Iterate(func(key string, val value.Value) bool {
pe := fieldpath.PathElement{FieldName: &key}
if sf, ok := a.Map.FindField(key); ok {
tr = sf.Type
atomics = append(atomics, findAtomics(append(path, pe), s, tr, val)...)
}
return true
})
}
if v.IsList() && a.List != nil {
if a.List.ElementRelationship == schema.Atomic {
atomics = append(atomics, pathString(path))
}
list := v.AsList()
for i := 0; i < list.Length(); i++ {
pe := fieldpath.PathElement{Index: &i}
atomics = append(atomics, findAtomics(append(path, pe), s, a.List.ElementType, list.At(i))...)
}
}
}
return atomics
}
func pathString(path []fieldpath.PathElement) string {
sb := strings.Builder{}
for _, p := range path {
sb.WriteString(p.String())
}
return sb.String()
}

View File

@ -0,0 +1,187 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package patch
import (
"context"
"encoding/json"
"fmt"
"strings"
"sync"
"time"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/managedfields"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/openapi"
"k8s.io/kube-openapi/pkg/spec3"
)
type TypeConverterManager interface {
// GetTypeConverter returns a type converter for the given GVK
GetTypeConverter(gvk schema.GroupVersionKind) managedfields.TypeConverter
Run(ctx context.Context)
}
func NewTypeConverterManager(
staticTypeConverter managedfields.TypeConverter,
openapiClient openapi.Client,
) TypeConverterManager {
return &typeConverterManager{
staticTypeConverter: staticTypeConverter,
openapiClient: openapiClient,
typeConverterMap: make(map[schema.GroupVersion]typeConverterCacheEntry),
lastFetchedPaths: make(map[schema.GroupVersion]openapi.GroupVersion),
}
}
type typeConverterCacheEntry struct {
typeConverter managedfields.TypeConverter
entry openapi.GroupVersion
}
// typeConverterManager helps us make sure we have an up to date schema and
// type converter for our openapi models. It should be connfigured to use a
// static type converter for natively typed schemas, and fetches the schema
// for CRDs/other over the network on demand (trying to reduce network calls where necessary)
type typeConverterManager struct {
// schemaCache is used to cache the schema for a given GVK
staticTypeConverter managedfields.TypeConverter
// discoveryClient is used to fetch the schema for a given GVK
openapiClient openapi.Client
lock sync.RWMutex
typeConverterMap map[schema.GroupVersion]typeConverterCacheEntry
lastFetchedPaths map[schema.GroupVersion]openapi.GroupVersion
}
func (t *typeConverterManager) Run(ctx context.Context) {
// Loop every 5s refershing the OpenAPI schema list to know which
// schemas have been invalidated. This should use e-tags under the hood
_ = wait.PollUntilContextCancel(ctx, 5*time.Second, true, func(_ context.Context) (done bool, err error) {
paths, err := t.openapiClient.Paths()
if err != nil {
utilruntime.HandleError(fmt.Errorf("failed to fetch openapi paths: %w", err))
return false, nil
}
// The /openapi/v3 endpoint contains a list of paths whose ServerRelativeURL
// value changes every time the schema is updated. So we poll /openapi/v3
// to get the "version number" for each schema, and invalidate our cache
// if the version number has changed since we pulled it.
parsedPaths := make(map[schema.GroupVersion]openapi.GroupVersion, len(paths))
for path, entry := range paths {
if !strings.HasPrefix(path, "apis/") && !strings.HasPrefix(path, "api/") {
continue
}
path = strings.TrimPrefix(path, "apis/")
path = strings.TrimPrefix(path, "api/")
gv, err := schema.ParseGroupVersion(path)
if err != nil {
utilruntime.HandleError(fmt.Errorf("failed to parse group version %q: %w", path, err))
return false, nil
}
parsedPaths[gv] = entry
}
t.lock.Lock()
defer t.lock.Unlock()
t.lastFetchedPaths = parsedPaths
return false, nil
})
}
func (t *typeConverterManager) GetTypeConverter(gvk schema.GroupVersionKind) managedfields.TypeConverter {
// Check to see if the static type converter handles this GVK
if t.staticTypeConverter != nil {
//!TODO: Add ability to check existence to type converter
// working around for now but seeing if getting a typed version of an
// empty object returns error
stub := &unstructured.Unstructured{}
stub.SetGroupVersionKind(gvk)
if _, err := t.staticTypeConverter.ObjectToTyped(stub); err == nil {
return t.staticTypeConverter
}
}
gv := gvk.GroupVersion()
existing, entry, err := func() (managedfields.TypeConverter, openapi.GroupVersion, error) {
t.lock.RLock()
defer t.lock.RUnlock()
// If schema is not supported by static type converter, ask discovery
// for the schema
entry, ok := t.lastFetchedPaths[gv]
if !ok {
// If we can't get the schema, we can't do anything
return nil, nil, fmt.Errorf("no schema for %v", gvk)
}
// If the entry schema has not changed, used the same type converter
if existing, ok := t.typeConverterMap[gv]; ok && existing.entry.ServerRelativeURL() == entry.ServerRelativeURL() {
// If we have a type converter for this GVK, return it
return existing.typeConverter, existing.entry, nil
}
return nil, entry, nil
}()
if err != nil {
utilruntime.HandleError(err)
return nil
} else if existing != nil {
return existing
}
schBytes, err := entry.Schema(runtime.ContentTypeJSON)
if err != nil {
utilruntime.HandleError(fmt.Errorf("failed to get schema for %v: %w", gvk, err))
return nil
}
var sch spec3.OpenAPI
if err := json.Unmarshal(schBytes, &sch); err != nil {
utilruntime.HandleError(fmt.Errorf("failed to unmarshal schema for %v: %w", gvk, err))
return nil
}
// The schema has changed, or there is no entry for it, generate
// a new type converter for this GV
tc, err := managedfields.NewTypeConverter(sch.Components.Schemas, false)
if err != nil {
utilruntime.HandleError(fmt.Errorf("failed to create type converter for %v: %w", gvk, err))
return nil
}
t.lock.Lock()
defer t.lock.Unlock()
t.typeConverterMap[gv] = typeConverterCacheEntry{
typeConverter: tc,
entry: entry,
}
return tc
}

View File

@ -0,0 +1,151 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package mutating
import (
"context"
celgo "github.com/google/cel-go/cel"
"io"
"k8s.io/api/admissionregistration/v1alpha1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/managedfields"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/admission/plugin/policy/generic"
"k8s.io/apiserver/pkg/admission/plugin/policy/matching"
"k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch"
"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/features"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
"k8s.io/component-base/featuregate"
)
const (
// PluginName indicates the name of admission plug-in
PluginName = "MutatingAdmissionPolicy"
)
// Register registers a plugin
func Register(plugins *admission.Plugins) {
plugins.Register(PluginName, func(configFile io.Reader) (admission.Interface, error) {
return NewPlugin(configFile), nil
})
}
type Policy = v1alpha1.MutatingAdmissionPolicy
type PolicyBinding = v1alpha1.MutatingAdmissionPolicyBinding
type PolicyMutation = v1alpha1.Mutation
type PolicyHook = generic.PolicyHook[*Policy, *PolicyBinding, PolicyEvaluator]
type Mutator struct {
}
type MutationEvaluationFunc func(
ctx context.Context,
matchedResource schema.GroupVersionResource,
versionedAttr *admission.VersionedAttributes,
o admission.ObjectInterfaces,
versionedParams runtime.Object,
namespace *corev1.Namespace,
typeConverter managedfields.TypeConverter,
runtimeCELCostBudget int64,
authorizer authorizer.Authorizer,
) (runtime.Object, error)
type PolicyEvaluator struct {
Matcher matchconditions.Matcher
Mutators []patch.Patcher
CompositionEnv *cel.CompositionEnv
Error error
}
// Plugin is an implementation of admission.Interface.
type Plugin struct {
*generic.Plugin[PolicyHook]
}
var _ admission.Interface = &Plugin{}
var _ admission.MutationInterface = &Plugin{}
// NewPlugin returns a generic admission webhook plugin.
func NewPlugin(_ io.Reader) *Plugin {
// There is no request body to mutate for DELETE, so this plugin never handles that operation.
handler := admission.NewHandler(admission.Create, admission.Update, admission.Connect)
res := &Plugin{}
res.Plugin = generic.NewPlugin(
handler,
func(f informers.SharedInformerFactory, client kubernetes.Interface, dynamicClient dynamic.Interface, restMapper meta.RESTMapper) generic.Source[PolicyHook] {
return generic.NewPolicySource(
f.Admissionregistration().V1alpha1().MutatingAdmissionPolicies().Informer(),
f.Admissionregistration().V1alpha1().MutatingAdmissionPolicyBindings().Informer(),
NewMutatingAdmissionPolicyAccessor,
NewMutatingAdmissionPolicyBindingAccessor,
compilePolicy,
//!TODO: Create a way to share param informers between
// mutating/validating plugins
f,
dynamicClient,
restMapper,
)
},
func(a authorizer.Authorizer, m *matching.Matcher, client kubernetes.Interface) generic.Dispatcher[PolicyHook] {
return NewDispatcher(a, m, patch.NewTypeConverterManager(nil, client.Discovery().OpenAPIV3()))
},
)
return res
}
// Admit makes an admission decision based on the request attributes.
func (a *Plugin) Admit(ctx context.Context, attr admission.Attributes, o admission.ObjectInterfaces) error {
return a.Plugin.Dispatch(ctx, attr, o)
}
func (a *Plugin) InspectFeatureGates(featureGates featuregate.FeatureGate) {
a.Plugin.SetEnabled(featureGates.Enabled(features.MutatingAdmissionPolicy))
}
// Variable is a named expression for composition.
type Variable struct {
Name string
Expression string
}
func (v *Variable) GetExpression() string {
return v.Expression
}
func (v *Variable) ReturnTypes() []*celgo.Type {
return []*celgo.Type{celgo.AnyType, celgo.DynType}
}
func (v *Variable) GetName() string {
return v.Name
}
func convertv1alpha1Variables(variables []v1alpha1.Variable) []cel.NamedExpressionAccessor {
namedExpressions := make([]cel.NamedExpressionAccessor, len(variables))
for i, variable := range variables {
namedExpressions[i] = &Variable{Name: variable.Name, Expression: variable.Expression}
}
return namedExpressions
}

View File

@ -0,0 +1,76 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package mutating
import (
apiequality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/sets"
)
type key struct {
PolicyUID types.NamespacedName
BindingUID types.NamespacedName
ParamUID types.NamespacedName
MutationIndex int
}
type policyReinvokeContext struct {
// lastPolicyOutput holds the result of the last Policy admission plugin call
lastPolicyOutput runtime.Object
// previouslyInvokedReinvocablePolicys holds the set of policies that have been invoked and
// should be reinvoked if a later mutation occurs
previouslyInvokedReinvocablePolicies sets.Set[key]
// reinvokePolicies holds the set of Policies that should be reinvoked
reinvokePolicies sets.Set[key]
}
func (rc *policyReinvokeContext) ShouldReinvoke(policy key) bool {
return rc.reinvokePolicies.Has(policy)
}
func (rc *policyReinvokeContext) IsOutputChangedSinceLastPolicyInvocation(object runtime.Object) bool {
return !apiequality.Semantic.DeepEqual(rc.lastPolicyOutput, object)
}
func (rc *policyReinvokeContext) SetLastPolicyInvocationOutput(object runtime.Object) {
if object == nil {
rc.lastPolicyOutput = nil
return
}
rc.lastPolicyOutput = object.DeepCopyObject()
}
func (rc *policyReinvokeContext) AddReinvocablePolicyToPreviouslyInvoked(policy key) {
if rc.previouslyInvokedReinvocablePolicies == nil {
rc.previouslyInvokedReinvocablePolicies = sets.New[key]()
}
rc.previouslyInvokedReinvocablePolicies.Insert(policy)
}
func (rc *policyReinvokeContext) RequireReinvokingPreviouslyInvokedPlugins() {
if len(rc.previouslyInvokedReinvocablePolicies) > 0 {
if rc.reinvokePolicies == nil {
rc.reinvokePolicies = sets.New[key]()
}
for s := range rc.previouslyInvokedReinvocablePolicies {
rc.reinvokePolicies.Insert(s)
}
rc.previouslyInvokedReinvocablePolicies = sets.New[key]()
}
}

View File

@ -54,6 +54,10 @@ func (v *validatingAdmissionPolicyAccessor) GetMatchConstraints() *v1.MatchResou
return v.Spec.MatchConstraints
}
func (v *validatingAdmissionPolicyAccessor) GetFailurePolicy() *v1.FailurePolicyType {
return v.Spec.FailurePolicy
}
type validatingAdmissionPolicyBindingAccessor struct {
*v1.ValidatingAdmissionPolicyBinding
}

View File

@ -30,6 +30,7 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
utiljson "k8s.io/apimachinery/pkg/util/json"
"k8s.io/apiserver/pkg/admission"
admissionauthorizer "k8s.io/apiserver/pkg/admission/plugin/authorizer"
"k8s.io/apiserver/pkg/admission/plugin/policy/generic"
celmetrics "k8s.io/apiserver/pkg/admission/plugin/policy/validating/metrics"
celconfig "k8s.io/apiserver/pkg/apis/cel"
@ -63,6 +64,10 @@ type policyDecisionWithMetadata struct {
Binding *admissionregistrationv1.ValidatingAdmissionPolicyBinding
}
func (c *dispatcher) Start(ctx context.Context) error {
return nil
}
// Dispatch implements generic.Dispatcher.
func (c *dispatcher) Dispatch(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces, hooks []PolicyHook) error {
@ -109,7 +114,7 @@ func (c *dispatcher) Dispatch(ctx context.Context, a admission.Attributes, o adm
}
}
authz := newCachingAuthorizer(c.authz)
authz := admissionauthorizer.NewCachingAuthorizer(c.authz)
for _, hook := range hooks {
// versionedAttributes will be set to non-nil inside of the loop, but

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package cel
package metrics
import (
"errors"

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package cel
package metrics
import (
"context"

View File

@ -36,7 +36,6 @@ import (
"k8s.io/client-go/dynamic"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
"k8s.io/component-base/featuregate"
)
const (
@ -93,13 +92,12 @@ type Plugin struct {
var _ admission.Interface = &Plugin{}
var _ admission.ValidationInterface = &Plugin{}
var _ initializer.WantsFeatures = &Plugin{}
var _ initializer.WantsExcludedAdmissionResources = &Plugin{}
func NewPlugin(_ io.Reader) *Plugin {
handler := admission.NewHandler(admission.Connect, admission.Create, admission.Delete, admission.Update)
return &Plugin{
p := &Plugin{
Plugin: generic.NewPlugin(
handler,
func(f informers.SharedInformerFactory, client kubernetes.Interface, dynamicClient dynamic.Interface, restMapper meta.RESTMapper) generic.Source[PolicyHook] {
@ -114,11 +112,13 @@ func NewPlugin(_ io.Reader) *Plugin {
restMapper,
)
},
func(a authorizer.Authorizer, m *matching.Matcher) generic.Dispatcher[PolicyHook] {
func(a authorizer.Authorizer, m *matching.Matcher, client kubernetes.Interface) generic.Dispatcher[PolicyHook] {
return NewDispatcher(a, generic.NewPolicyMatcher(m))
},
),
}
p.SetEnabled(true)
return p
}
// Validate makes an admission decision based on the request attributes.
@ -126,10 +126,6 @@ func (a *Plugin) Validate(ctx context.Context, attr admission.Attributes, o admi
return a.Plugin.Dispatch(ctx, attr, o)
}
func (a *Plugin) InspectFeatureGates(featureGates featuregate.FeatureGate) {
a.Plugin.SetEnabled(featureGates.Enabled(features.ValidatingAdmissionPolicy))
}
func compilePolicy(policy *Policy) Validator {
hasParam := false
if policy.Spec.ParamKind != nil {
@ -155,13 +151,13 @@ func compilePolicy(policy *Policy) Validator {
for i := range matchConditions {
matchExpressionAccessors[i] = (*matchconditions.MatchCondition)(&matchConditions[i])
}
matcher = matchconditions.NewMatcher(filterCompiler.Compile(matchExpressionAccessors, optionalVars, environment.StoredExpressions), failurePolicy, "policy", "validate", policy.Name)
matcher = matchconditions.NewMatcher(filterCompiler.CompileCondition(matchExpressionAccessors, optionalVars, environment.StoredExpressions), failurePolicy, "policy", "validate", policy.Name)
}
res := NewValidator(
filterCompiler.Compile(convertv1Validations(policy.Spec.Validations), optionalVars, environment.StoredExpressions),
filterCompiler.CompileCondition(convertv1Validations(policy.Spec.Validations), optionalVars, environment.StoredExpressions),
matcher,
filterCompiler.Compile(convertv1AuditAnnotations(policy.Spec.AuditAnnotations), optionalVars, environment.StoredExpressions),
filterCompiler.Compile(convertv1MessageExpressions(policy.Spec.Validations), expressionOptionalVars, environment.StoredExpressions),
filterCompiler.CompileCondition(convertv1AuditAnnotations(policy.Spec.AuditAnnotations), optionalVars, environment.StoredExpressions),
filterCompiler.CompileCondition(convertv1MessageExpressions(policy.Spec.Validations), expressionOptionalVars, environment.StoredExpressions),
failurePolicy,
)

View File

@ -41,13 +41,13 @@ import (
// validator implements the Validator interface
type validator struct {
celMatcher matchconditions.Matcher
validationFilter cel.Filter
auditAnnotationFilter cel.Filter
messageFilter cel.Filter
validationFilter cel.ConditionEvaluator
auditAnnotationFilter cel.ConditionEvaluator
messageFilter cel.ConditionEvaluator
failPolicy *v1.FailurePolicyType
}
func NewValidator(validationFilter cel.Filter, celMatcher matchconditions.Matcher, auditAnnotationFilter, messageFilter cel.Filter, failPolicy *v1.FailurePolicyType) Validator {
func NewValidator(validationFilter cel.ConditionEvaluator, celMatcher matchconditions.Matcher, auditAnnotationFilter, messageFilter cel.ConditionEvaluator, failPolicy *v1.FailurePolicyType) Validator {
return &validator{
celMatcher: celMatcher,
validationFilter: validationFilter,
@ -122,6 +122,7 @@ func (v *validator) Validate(ctx context.Context, matchedResource schema.GroupVe
messageResults, _, err := v.messageFilter.ForInput(ctx, versionedAttr, admissionRequest, expressionOptionalVars, ns, remainingBudget)
for i, evalResult := range evalResults {
var decision = &decisions[i]
decision.Elapsed = evalResult.Elapsed
// TODO: move this to generics
validation, ok := evalResult.ExpressionAccessor.(*ValidationCondition)
if !ok {
@ -146,6 +147,7 @@ func (v *validator) Validate(ctx context.Context, matchedResource schema.GroupVe
decision.Message = fmt.Sprintf("failed messageExpression: %s", err)
} else if evalResult.EvalResult != celtypes.True {
decision.Action = ActionDeny
decision.Evaluation = EvalDeny
if validation.Reason == nil {
decision.Reason = metav1.StatusReasonInvalid
} else {
@ -210,6 +212,7 @@ func (v *validator) Validate(ctx context.Context, matchedResource schema.GroupVe
continue
}
var auditAnnotationResult = &auditAnnotationResults[i]
auditAnnotationResult.Elapsed = evalResult.Elapsed
// TODO: move this to generics
validation, ok := evalResult.ExpressionAccessor.(*AuditAnnotationCondition)
if !ok {

View File

@ -50,7 +50,7 @@ type WebhookAccessor interface {
GetRESTClient(clientManager *webhookutil.ClientManager) (*rest.RESTClient, error)
// GetCompiledMatcher gets the compiled matcher object
GetCompiledMatcher(compiler cel.FilterCompiler) matchconditions.Matcher
GetCompiledMatcher(compiler cel.ConditionCompiler) matchconditions.Matcher
// GetName gets the webhook Name field. Note that the name is scoped to the webhook
// configuration and does not provide a globally unique identity, if a unique identity is
@ -132,7 +132,7 @@ func (m *mutatingWebhookAccessor) GetType() string {
return "admit"
}
func (m *mutatingWebhookAccessor) GetCompiledMatcher(compiler cel.FilterCompiler) matchconditions.Matcher {
func (m *mutatingWebhookAccessor) GetCompiledMatcher(compiler cel.ConditionCompiler) matchconditions.Matcher {
m.compileMatcher.Do(func() {
expressions := make([]cel.ExpressionAccessor, len(m.MutatingWebhook.MatchConditions))
for i, matchCondition := range m.MutatingWebhook.MatchConditions {
@ -145,7 +145,7 @@ func (m *mutatingWebhookAccessor) GetCompiledMatcher(compiler cel.FilterCompiler
if utilfeature.DefaultFeatureGate.Enabled(features.StrictCostEnforcementForWebhooks) {
strictCost = true
}
m.compiledMatcher = matchconditions.NewMatcher(compiler.Compile(
m.compiledMatcher = matchconditions.NewMatcher(compiler.CompileCondition(
expressions,
cel.OptionalVariableDeclarations{
HasParams: false,
@ -265,7 +265,7 @@ func (v *validatingWebhookAccessor) GetRESTClient(clientManager *webhookutil.Cli
return v.client, v.clientErr
}
func (v *validatingWebhookAccessor) GetCompiledMatcher(compiler cel.FilterCompiler) matchconditions.Matcher {
func (v *validatingWebhookAccessor) GetCompiledMatcher(compiler cel.ConditionCompiler) matchconditions.Matcher {
v.compileMatcher.Do(func() {
expressions := make([]cel.ExpressionAccessor, len(v.ValidatingWebhook.MatchConditions))
for i, matchCondition := range v.ValidatingWebhook.MatchConditions {
@ -278,7 +278,7 @@ func (v *validatingWebhookAccessor) GetCompiledMatcher(compiler cel.FilterCompil
if utilfeature.DefaultFeatureGate.Enabled(features.StrictCostEnforcementForWebhooks) {
strictCost = true
}
v.compiledMatcher = matchconditions.NewMatcher(compiler.Compile(
v.compiledMatcher = matchconditions.NewMatcher(compiler.CompileCondition(
expressions,
cel.OptionalVariableDeclarations{
HasParams: false,

View File

@ -21,6 +21,9 @@ import (
"fmt"
"io"
"k8s.io/apiserver/pkg/cel/environment"
"k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/klog/v2"
admissionv1 "k8s.io/api/admission/v1"
@ -38,9 +41,6 @@ import (
"k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/object"
"k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/rules"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/cel/environment"
"k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature"
webhookutil "k8s.io/apiserver/pkg/util/webhook"
"k8s.io/client-go/informers"
clientset "k8s.io/client-go/kubernetes"
@ -57,7 +57,7 @@ type Webhook struct {
namespaceMatcher *namespace.Matcher
objectMatcher *object.Matcher
dispatcher Dispatcher
filterCompiler cel.FilterCompiler
filterCompiler cel.ConditionCompiler
authorizer authorizer.Authorizer
}
@ -102,7 +102,7 @@ func NewWebhook(handler *admission.Handler, configFile io.Reader, sourceFactory
namespaceMatcher: &namespace.Matcher{},
objectMatcher: &object.Matcher{},
dispatcher: dispatcherFactory(&cm),
filterCompiler: cel.NewFilterCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), utilfeature.DefaultFeatureGate.Enabled(features.StrictCostEnforcementForWebhooks))),
filterCompiler: cel.NewConditionCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), utilfeature.DefaultFeatureGate.Enabled(features.StrictCostEnforcementForWebhooks))),
}, nil
}

View File

@ -54,14 +54,14 @@ var _ Matcher = &matcher{}
// matcher evaluates compiled cel expressions and determines if they match the given request or not
type matcher struct {
filter celplugin.Filter
filter celplugin.ConditionEvaluator
failPolicy v1.FailurePolicyType
matcherType string
matcherKind string
objectName string
}
func NewMatcher(filter celplugin.Filter, failPolicy *v1.FailurePolicyType, matcherKind, matcherType, objectName string) Matcher {
func NewMatcher(filter celplugin.ConditionEvaluator, failPolicy *v1.FailurePolicyType, matcherKind, matcherType, objectName string) Matcher {
var f v1.FailurePolicyType
if failPolicy == nil {
f = v1.Fail

View File

@ -190,7 +190,7 @@ func (a *mutatingDispatcher) Dispatch(ctx context.Context, attr admission.Attrib
admissionmetrics.Metrics.ObserveWebhook(ctx, hook.Name, time.Since(t), rejected, versionedAttr.Attributes, "admit", 200)
}
if changed {
// Patch had changed the object. Prepare to reinvoke all previous webhooks that are eligible for re-invocation.
// Patch had changed the object. Prepare to reinvoke all previous mutations that are eligible for re-invocation.
webhookReinvokeCtx.RequireReinvokingPreviouslyInvokedPlugins()
reinvokeCtx.SetShouldReinvoke()
}
@ -348,7 +348,7 @@ func (a *mutatingDispatcher) callAttrMutatingHook(ctx context.Context, h *admiss
}
var patchedJS []byte
jsonSerializer := json.NewSerializer(json.DefaultMetaFactory, o.GetObjectCreater(), o.GetObjectTyper(), false)
jsonSerializer := json.NewSerializerWithOptions(json.DefaultMetaFactory, o.GetObjectCreater(), o.GetObjectTyper(), json.SerializerOptions{})
switch result.PatchType {
// VerifyAdmissionResponse normalizes to v1 patch types, regardless of the AdmissionReview version used
case admissionv1.PatchTypeJSONPatch:

View File

@ -121,7 +121,7 @@ func (r *Matcher) resource() bool {
func IsExemptAdmissionConfigurationResource(attr admission.Attributes) bool {
gvk := attr.GetKind()
if gvk.Group == "admissionregistration.k8s.io" {
if gvk.Kind == "ValidatingWebhookConfiguration" || gvk.Kind == "MutatingWebhookConfiguration" || gvk.Kind == "ValidatingAdmissionPolicy" || gvk.Kind == "ValidatingAdmissionPolicyBinding" {
if gvk.Kind == "ValidatingWebhookConfiguration" || gvk.Kind == "MutatingWebhookConfiguration" || gvk.Kind == "ValidatingAdmissionPolicy" || gvk.Kind == "ValidatingAdmissionPolicyBinding" || gvk.Kind == "MutatingAdmissionPolicy" || gvk.Kind == "MutatingAdmissionPolicyBinding" {
return true
}
}

View File

@ -401,6 +401,13 @@ type WebhookMatchCondition struct {
// If version specified by subjectAccessReviewVersion in the request variable is v1beta1,
// the contents would be converted to the v1 version before evaluating the CEL expression.
//
// - 'resourceAttributes' describes information for a resource access request and is unset for non-resource requests. e.g. has(request.resourceAttributes) && request.resourceAttributes.namespace == 'default'
// - 'nonResourceAttributes' describes information for a non-resource access request and is unset for resource requests. e.g. has(request.nonResourceAttributes) && request.nonResourceAttributes.path == '/healthz'.
// - 'user' is the user to test for. e.g. request.user == 'alice'
// - 'groups' is the groups to test for. e.g. ('group1' in request.groups)
// - 'extra' corresponds to the user.Info.GetExtra() method from the authenticator.
// - 'uid' is the information about the requesting user. e.g. request.uid == '1'
//
// Documentation on CEL: https://kubernetes.io/docs/reference/using-api/cel/
Expression string
}

View File

@ -48,3 +48,12 @@ func SetDefaults_KMSConfiguration(obj *KMSConfiguration) {
obj.CacheSize = &defaultCacheSize
}
}
func SetDefaults_WebhookConfiguration(obj *WebhookConfiguration) {
if obj.AuthorizedTTL.Duration == 0 {
obj.AuthorizedTTL.Duration = 5 * time.Minute
}
if obj.UnauthorizedTTL.Duration == 0 {
obj.UnauthorizedTTL.Duration = 30 * time.Second
}
}

View File

@ -47,6 +47,7 @@ func init() {
func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(SchemeGroupVersion,
&AdmissionConfiguration{},
&AuthorizationConfiguration{},
&EncryptionConfiguration{},
)
// also register into the v1 group as EncryptionConfig (due to a docs bug)

View File

@ -48,3 +48,129 @@ type AdmissionPluginConfiguration struct {
// +optional
Configuration *runtime.Unknown `json:"configuration"`
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type AuthorizationConfiguration struct {
metav1.TypeMeta
// Authorizers is an ordered list of authorizers to
// authorize requests against.
// This is similar to the --authorization-modes kube-apiserver flag
// Must be at least one.
Authorizers []AuthorizerConfiguration `json:"authorizers"`
}
const (
TypeWebhook AuthorizerType = "Webhook"
FailurePolicyNoOpinion string = "NoOpinion"
FailurePolicyDeny string = "Deny"
AuthorizationWebhookConnectionInfoTypeKubeConfigFile string = "KubeConfigFile"
AuthorizationWebhookConnectionInfoTypeInCluster string = "InClusterConfig"
)
type AuthorizerType string
type AuthorizerConfiguration struct {
// Type refers to the type of the authorizer
// "Webhook" is supported in the generic API server
// Other API servers may support additional authorizer
// types like Node, RBAC, ABAC, etc.
Type string `json:"type"`
// Name used to describe the webhook
// This is explicitly used in monitoring machinery for metrics
// Note: Names must be DNS1123 labels like `myauthorizername` or
// subdomains like `myauthorizer.example.domain`
// Required, with no default
Name string `json:"name"`
// Webhook defines the configuration for a Webhook authorizer
// Must be defined when Type=Webhook
// Must not be defined when Type!=Webhook
Webhook *WebhookConfiguration `json:"webhook,omitempty"`
}
type WebhookConfiguration struct {
// The duration to cache 'authorized' responses from the webhook
// authorizer.
// Same as setting `--authorization-webhook-cache-authorized-ttl` flag
// Default: 5m0s
AuthorizedTTL metav1.Duration `json:"authorizedTTL"`
// The duration to cache 'unauthorized' responses from the webhook
// authorizer.
// Same as setting `--authorization-webhook-cache-unauthorized-ttl` flag
// Default: 30s
UnauthorizedTTL metav1.Duration `json:"unauthorizedTTL"`
// Timeout for the webhook request
// Maximum allowed value is 30s.
// Required, no default value.
Timeout metav1.Duration `json:"timeout"`
// The API version of the authorization.k8s.io SubjectAccessReview to
// send to and expect from the webhook.
// Same as setting `--authorization-webhook-version` flag
// Valid values: v1beta1, v1
// Required, no default value
SubjectAccessReviewVersion string `json:"subjectAccessReviewVersion"`
// MatchConditionSubjectAccessReviewVersion specifies the SubjectAccessReview
// version the CEL expressions are evaluated against
// Valid values: v1
// Required, no default value
MatchConditionSubjectAccessReviewVersion string `json:"matchConditionSubjectAccessReviewVersion"`
// Controls the authorization decision when a webhook request fails to
// complete or returns a malformed response or errors evaluating
// matchConditions.
// Valid values:
// - NoOpinion: continue to subsequent authorizers to see if one of
// them allows the request
// - Deny: reject the request without consulting subsequent authorizers
// Required, with no default.
FailurePolicy string `json:"failurePolicy"`
// ConnectionInfo defines how we talk to the webhook
ConnectionInfo WebhookConnectionInfo `json:"connectionInfo"`
// matchConditions is a list of conditions that must be met for a request to be sent to this
// webhook. An empty list of matchConditions matches all requests.
// There are a maximum of 64 match conditions allowed.
//
// The exact matching logic is (in order):
// 1. If at least one matchCondition evaluates to FALSE, then the webhook is skipped.
// 2. If ALL matchConditions evaluate to TRUE, then the webhook is called.
// 3. If at least one matchCondition evaluates to an error (but none are FALSE):
// - If failurePolicy=Deny, then the webhook rejects the request
// - If failurePolicy=NoOpinion, then the error is ignored and the webhook is skipped
MatchConditions []WebhookMatchCondition `json:"matchConditions"`
}
type WebhookConnectionInfo struct {
// Controls how the webhook should communicate with the server.
// Valid values:
// - KubeConfigFile: use the file specified in kubeConfigFile to locate the
// server.
// - InClusterConfig: use the in-cluster configuration to call the
// SubjectAccessReview API hosted by kube-apiserver. This mode is not
// allowed for kube-apiserver.
Type string `json:"type"`
// Path to KubeConfigFile for connection info
// Required, if connectionInfo.Type is KubeConfig
KubeConfigFile *string `json:"kubeConfigFile"`
}
type WebhookMatchCondition struct {
// expression represents the expression which will be evaluated by CEL. Must evaluate to bool.
// CEL expressions have access to the contents of the SubjectAccessReview in v1 version.
// If version specified by subjectAccessReviewVersion in the request variable is v1beta1,
// the contents would be converted to the v1 version before evaluating the CEL expression.
//
// - 'resourceAttributes' describes information for a resource access request and is unset for non-resource requests. e.g. has(request.resourceAttributes) && request.resourceAttributes.namespace == 'default'
// - 'nonResourceAttributes' describes information for a non-resource access request and is unset for resource requests. e.g. has(request.nonResourceAttributes) && request.nonResourceAttributes.path == '/healthz'.
// - 'user' is the user to test for. e.g. request.user == 'alice'
// - 'groups' is the groups to test for. e.g. ('group1' in request.groups)
// - 'extra' corresponds to the user.Info.GetExtra() method from the authenticator.
// - 'uid' is the information about the requesting user. e.g. request.uid == '1'
//
// Documentation on CEL: https://kubernetes.io/docs/reference/using-api/cel/
Expression string `json:"expression"`
}

View File

@ -67,6 +67,26 @@ func RegisterConversions(s *runtime.Scheme) error {
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*AuthorizationConfiguration)(nil), (*apiserver.AuthorizationConfiguration)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1_AuthorizationConfiguration_To_apiserver_AuthorizationConfiguration(a.(*AuthorizationConfiguration), b.(*apiserver.AuthorizationConfiguration), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*apiserver.AuthorizationConfiguration)(nil), (*AuthorizationConfiguration)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_apiserver_AuthorizationConfiguration_To_v1_AuthorizationConfiguration(a.(*apiserver.AuthorizationConfiguration), b.(*AuthorizationConfiguration), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*AuthorizerConfiguration)(nil), (*apiserver.AuthorizerConfiguration)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1_AuthorizerConfiguration_To_apiserver_AuthorizerConfiguration(a.(*AuthorizerConfiguration), b.(*apiserver.AuthorizerConfiguration), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*apiserver.AuthorizerConfiguration)(nil), (*AuthorizerConfiguration)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_apiserver_AuthorizerConfiguration_To_v1_AuthorizerConfiguration(a.(*apiserver.AuthorizerConfiguration), b.(*AuthorizerConfiguration), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*EncryptionConfiguration)(nil), (*apiserver.EncryptionConfiguration)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1_EncryptionConfiguration_To_apiserver_EncryptionConfiguration(a.(*EncryptionConfiguration), b.(*apiserver.EncryptionConfiguration), scope)
}); err != nil {
@ -137,6 +157,36 @@ func RegisterConversions(s *runtime.Scheme) error {
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*WebhookConfiguration)(nil), (*apiserver.WebhookConfiguration)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1_WebhookConfiguration_To_apiserver_WebhookConfiguration(a.(*WebhookConfiguration), b.(*apiserver.WebhookConfiguration), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*apiserver.WebhookConfiguration)(nil), (*WebhookConfiguration)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_apiserver_WebhookConfiguration_To_v1_WebhookConfiguration(a.(*apiserver.WebhookConfiguration), b.(*WebhookConfiguration), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*WebhookConnectionInfo)(nil), (*apiserver.WebhookConnectionInfo)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1_WebhookConnectionInfo_To_apiserver_WebhookConnectionInfo(a.(*WebhookConnectionInfo), b.(*apiserver.WebhookConnectionInfo), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*apiserver.WebhookConnectionInfo)(nil), (*WebhookConnectionInfo)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_apiserver_WebhookConnectionInfo_To_v1_WebhookConnectionInfo(a.(*apiserver.WebhookConnectionInfo), b.(*WebhookConnectionInfo), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*WebhookMatchCondition)(nil), (*apiserver.WebhookMatchCondition)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1_WebhookMatchCondition_To_apiserver_WebhookMatchCondition(a.(*WebhookMatchCondition), b.(*apiserver.WebhookMatchCondition), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*apiserver.WebhookMatchCondition)(nil), (*WebhookMatchCondition)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_apiserver_WebhookMatchCondition_To_v1_WebhookMatchCondition(a.(*apiserver.WebhookMatchCondition), b.(*WebhookMatchCondition), scope)
}); err != nil {
return err
}
return nil
}
@ -204,6 +254,50 @@ func Convert_apiserver_AdmissionPluginConfiguration_To_v1_AdmissionPluginConfigu
return autoConvert_apiserver_AdmissionPluginConfiguration_To_v1_AdmissionPluginConfiguration(in, out, s)
}
func autoConvert_v1_AuthorizationConfiguration_To_apiserver_AuthorizationConfiguration(in *AuthorizationConfiguration, out *apiserver.AuthorizationConfiguration, s conversion.Scope) error {
out.Authorizers = *(*[]apiserver.AuthorizerConfiguration)(unsafe.Pointer(&in.Authorizers))
return nil
}
// Convert_v1_AuthorizationConfiguration_To_apiserver_AuthorizationConfiguration is an autogenerated conversion function.
func Convert_v1_AuthorizationConfiguration_To_apiserver_AuthorizationConfiguration(in *AuthorizationConfiguration, out *apiserver.AuthorizationConfiguration, s conversion.Scope) error {
return autoConvert_v1_AuthorizationConfiguration_To_apiserver_AuthorizationConfiguration(in, out, s)
}
func autoConvert_apiserver_AuthorizationConfiguration_To_v1_AuthorizationConfiguration(in *apiserver.AuthorizationConfiguration, out *AuthorizationConfiguration, s conversion.Scope) error {
out.Authorizers = *(*[]AuthorizerConfiguration)(unsafe.Pointer(&in.Authorizers))
return nil
}
// Convert_apiserver_AuthorizationConfiguration_To_v1_AuthorizationConfiguration is an autogenerated conversion function.
func Convert_apiserver_AuthorizationConfiguration_To_v1_AuthorizationConfiguration(in *apiserver.AuthorizationConfiguration, out *AuthorizationConfiguration, s conversion.Scope) error {
return autoConvert_apiserver_AuthorizationConfiguration_To_v1_AuthorizationConfiguration(in, out, s)
}
func autoConvert_v1_AuthorizerConfiguration_To_apiserver_AuthorizerConfiguration(in *AuthorizerConfiguration, out *apiserver.AuthorizerConfiguration, s conversion.Scope) error {
out.Type = apiserver.AuthorizerType(in.Type)
out.Name = in.Name
out.Webhook = (*apiserver.WebhookConfiguration)(unsafe.Pointer(in.Webhook))
return nil
}
// Convert_v1_AuthorizerConfiguration_To_apiserver_AuthorizerConfiguration is an autogenerated conversion function.
func Convert_v1_AuthorizerConfiguration_To_apiserver_AuthorizerConfiguration(in *AuthorizerConfiguration, out *apiserver.AuthorizerConfiguration, s conversion.Scope) error {
return autoConvert_v1_AuthorizerConfiguration_To_apiserver_AuthorizerConfiguration(in, out, s)
}
func autoConvert_apiserver_AuthorizerConfiguration_To_v1_AuthorizerConfiguration(in *apiserver.AuthorizerConfiguration, out *AuthorizerConfiguration, s conversion.Scope) error {
out.Type = string(in.Type)
out.Name = in.Name
out.Webhook = (*WebhookConfiguration)(unsafe.Pointer(in.Webhook))
return nil
}
// Convert_apiserver_AuthorizerConfiguration_To_v1_AuthorizerConfiguration is an autogenerated conversion function.
func Convert_apiserver_AuthorizerConfiguration_To_v1_AuthorizerConfiguration(in *apiserver.AuthorizerConfiguration, out *AuthorizerConfiguration, s conversion.Scope) error {
return autoConvert_apiserver_AuthorizerConfiguration_To_v1_AuthorizerConfiguration(in, out, s)
}
func autoConvert_v1_EncryptionConfiguration_To_apiserver_EncryptionConfiguration(in *EncryptionConfiguration, out *apiserver.EncryptionConfiguration, s conversion.Scope) error {
out.Resources = *(*[]apiserver.ResourceConfiguration)(unsafe.Pointer(&in.Resources))
return nil
@ -361,3 +455,83 @@ func autoConvert_apiserver_SecretboxConfiguration_To_v1_SecretboxConfiguration(i
func Convert_apiserver_SecretboxConfiguration_To_v1_SecretboxConfiguration(in *apiserver.SecretboxConfiguration, out *SecretboxConfiguration, s conversion.Scope) error {
return autoConvert_apiserver_SecretboxConfiguration_To_v1_SecretboxConfiguration(in, out, s)
}
func autoConvert_v1_WebhookConfiguration_To_apiserver_WebhookConfiguration(in *WebhookConfiguration, out *apiserver.WebhookConfiguration, s conversion.Scope) error {
out.AuthorizedTTL = in.AuthorizedTTL
out.UnauthorizedTTL = in.UnauthorizedTTL
out.Timeout = in.Timeout
out.SubjectAccessReviewVersion = in.SubjectAccessReviewVersion
out.MatchConditionSubjectAccessReviewVersion = in.MatchConditionSubjectAccessReviewVersion
out.FailurePolicy = in.FailurePolicy
if err := Convert_v1_WebhookConnectionInfo_To_apiserver_WebhookConnectionInfo(&in.ConnectionInfo, &out.ConnectionInfo, s); err != nil {
return err
}
out.MatchConditions = *(*[]apiserver.WebhookMatchCondition)(unsafe.Pointer(&in.MatchConditions))
return nil
}
// Convert_v1_WebhookConfiguration_To_apiserver_WebhookConfiguration is an autogenerated conversion function.
func Convert_v1_WebhookConfiguration_To_apiserver_WebhookConfiguration(in *WebhookConfiguration, out *apiserver.WebhookConfiguration, s conversion.Scope) error {
return autoConvert_v1_WebhookConfiguration_To_apiserver_WebhookConfiguration(in, out, s)
}
func autoConvert_apiserver_WebhookConfiguration_To_v1_WebhookConfiguration(in *apiserver.WebhookConfiguration, out *WebhookConfiguration, s conversion.Scope) error {
out.AuthorizedTTL = in.AuthorizedTTL
out.UnauthorizedTTL = in.UnauthorizedTTL
out.Timeout = in.Timeout
out.SubjectAccessReviewVersion = in.SubjectAccessReviewVersion
out.MatchConditionSubjectAccessReviewVersion = in.MatchConditionSubjectAccessReviewVersion
out.FailurePolicy = in.FailurePolicy
if err := Convert_apiserver_WebhookConnectionInfo_To_v1_WebhookConnectionInfo(&in.ConnectionInfo, &out.ConnectionInfo, s); err != nil {
return err
}
out.MatchConditions = *(*[]WebhookMatchCondition)(unsafe.Pointer(&in.MatchConditions))
return nil
}
// Convert_apiserver_WebhookConfiguration_To_v1_WebhookConfiguration is an autogenerated conversion function.
func Convert_apiserver_WebhookConfiguration_To_v1_WebhookConfiguration(in *apiserver.WebhookConfiguration, out *WebhookConfiguration, s conversion.Scope) error {
return autoConvert_apiserver_WebhookConfiguration_To_v1_WebhookConfiguration(in, out, s)
}
func autoConvert_v1_WebhookConnectionInfo_To_apiserver_WebhookConnectionInfo(in *WebhookConnectionInfo, out *apiserver.WebhookConnectionInfo, s conversion.Scope) error {
out.Type = in.Type
out.KubeConfigFile = (*string)(unsafe.Pointer(in.KubeConfigFile))
return nil
}
// Convert_v1_WebhookConnectionInfo_To_apiserver_WebhookConnectionInfo is an autogenerated conversion function.
func Convert_v1_WebhookConnectionInfo_To_apiserver_WebhookConnectionInfo(in *WebhookConnectionInfo, out *apiserver.WebhookConnectionInfo, s conversion.Scope) error {
return autoConvert_v1_WebhookConnectionInfo_To_apiserver_WebhookConnectionInfo(in, out, s)
}
func autoConvert_apiserver_WebhookConnectionInfo_To_v1_WebhookConnectionInfo(in *apiserver.WebhookConnectionInfo, out *WebhookConnectionInfo, s conversion.Scope) error {
out.Type = in.Type
out.KubeConfigFile = (*string)(unsafe.Pointer(in.KubeConfigFile))
return nil
}
// Convert_apiserver_WebhookConnectionInfo_To_v1_WebhookConnectionInfo is an autogenerated conversion function.
func Convert_apiserver_WebhookConnectionInfo_To_v1_WebhookConnectionInfo(in *apiserver.WebhookConnectionInfo, out *WebhookConnectionInfo, s conversion.Scope) error {
return autoConvert_apiserver_WebhookConnectionInfo_To_v1_WebhookConnectionInfo(in, out, s)
}
func autoConvert_v1_WebhookMatchCondition_To_apiserver_WebhookMatchCondition(in *WebhookMatchCondition, out *apiserver.WebhookMatchCondition, s conversion.Scope) error {
out.Expression = in.Expression
return nil
}
// Convert_v1_WebhookMatchCondition_To_apiserver_WebhookMatchCondition is an autogenerated conversion function.
func Convert_v1_WebhookMatchCondition_To_apiserver_WebhookMatchCondition(in *WebhookMatchCondition, out *apiserver.WebhookMatchCondition, s conversion.Scope) error {
return autoConvert_v1_WebhookMatchCondition_To_apiserver_WebhookMatchCondition(in, out, s)
}
func autoConvert_apiserver_WebhookMatchCondition_To_v1_WebhookMatchCondition(in *apiserver.WebhookMatchCondition, out *WebhookMatchCondition, s conversion.Scope) error {
out.Expression = in.Expression
return nil
}
// Convert_apiserver_WebhookMatchCondition_To_v1_WebhookMatchCondition is an autogenerated conversion function.
func Convert_apiserver_WebhookMatchCondition_To_v1_WebhookMatchCondition(in *apiserver.WebhookMatchCondition, out *WebhookMatchCondition, s conversion.Scope) error {
return autoConvert_apiserver_WebhookMatchCondition_To_v1_WebhookMatchCondition(in, out, s)
}

View File

@ -100,6 +100,59 @@ func (in *AdmissionPluginConfiguration) DeepCopy() *AdmissionPluginConfiguration
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AuthorizationConfiguration) DeepCopyInto(out *AuthorizationConfiguration) {
*out = *in
out.TypeMeta = in.TypeMeta
if in.Authorizers != nil {
in, out := &in.Authorizers, &out.Authorizers
*out = make([]AuthorizerConfiguration, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthorizationConfiguration.
func (in *AuthorizationConfiguration) DeepCopy() *AuthorizationConfiguration {
if in == nil {
return nil
}
out := new(AuthorizationConfiguration)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *AuthorizationConfiguration) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AuthorizerConfiguration) DeepCopyInto(out *AuthorizerConfiguration) {
*out = *in
if in.Webhook != nil {
in, out := &in.Webhook, &out.Webhook
*out = new(WebhookConfiguration)
(*in).DeepCopyInto(*out)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthorizerConfiguration.
func (in *AuthorizerConfiguration) DeepCopy() *AuthorizerConfiguration {
if in == nil {
return nil
}
out := new(AuthorizerConfiguration)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *EncryptionConfiguration) DeepCopyInto(out *EncryptionConfiguration) {
*out = *in
@ -279,3 +332,65 @@ func (in *SecretboxConfiguration) DeepCopy() *SecretboxConfiguration {
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *WebhookConfiguration) DeepCopyInto(out *WebhookConfiguration) {
*out = *in
out.AuthorizedTTL = in.AuthorizedTTL
out.UnauthorizedTTL = in.UnauthorizedTTL
out.Timeout = in.Timeout
in.ConnectionInfo.DeepCopyInto(&out.ConnectionInfo)
if in.MatchConditions != nil {
in, out := &in.MatchConditions, &out.MatchConditions
*out = make([]WebhookMatchCondition, len(*in))
copy(*out, *in)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebhookConfiguration.
func (in *WebhookConfiguration) DeepCopy() *WebhookConfiguration {
if in == nil {
return nil
}
out := new(WebhookConfiguration)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *WebhookConnectionInfo) DeepCopyInto(out *WebhookConnectionInfo) {
*out = *in
if in.KubeConfigFile != nil {
in, out := &in.KubeConfigFile, &out.KubeConfigFile
*out = new(string)
**out = **in
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebhookConnectionInfo.
func (in *WebhookConnectionInfo) DeepCopy() *WebhookConnectionInfo {
if in == nil {
return nil
}
out := new(WebhookConnectionInfo)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *WebhookMatchCondition) DeepCopyInto(out *WebhookMatchCondition) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebhookMatchCondition.
func (in *WebhookMatchCondition) DeepCopy() *WebhookMatchCondition {
if in == nil {
return nil
}
out := new(WebhookMatchCondition)
in.DeepCopyInto(out)
return out
}

View File

@ -29,10 +29,20 @@ import (
// Public to allow building arbitrary schemes.
// All generated defaulters are covering - they call all nested defaulters.
func RegisterDefaults(scheme *runtime.Scheme) error {
scheme.AddTypeDefaultingFunc(&AuthorizationConfiguration{}, func(obj interface{}) { SetObjectDefaults_AuthorizationConfiguration(obj.(*AuthorizationConfiguration)) })
scheme.AddTypeDefaultingFunc(&EncryptionConfiguration{}, func(obj interface{}) { SetObjectDefaults_EncryptionConfiguration(obj.(*EncryptionConfiguration)) })
return nil
}
func SetObjectDefaults_AuthorizationConfiguration(in *AuthorizationConfiguration) {
for i := range in.Authorizers {
a := &in.Authorizers[i]
if a.Webhook != nil {
SetDefaults_WebhookConfiguration(a.Webhook)
}
}
}
func SetObjectDefaults_EncryptionConfiguration(in *EncryptionConfiguration) {
for i := range in.Resources {
a := &in.Resources[i]

View File

@ -615,6 +615,13 @@ type WebhookMatchCondition struct {
// If version specified by subjectAccessReviewVersion in the request variable is v1beta1,
// the contents would be converted to the v1 version before evaluating the CEL expression.
//
// - 'resourceAttributes' describes information for a resource access request and is unset for non-resource requests. e.g. has(request.resourceAttributes) && request.resourceAttributes.namespace == 'default'
// - 'nonResourceAttributes' describes information for a non-resource access request and is unset for resource requests. e.g. has(request.nonResourceAttributes) && request.nonResourceAttributes.path == '/healthz'.
// - 'user' is the user to test for. e.g. request.user == 'alice'
// - 'groups' is the groups to test for. e.g. ('group1' in request.groups)
// - 'extra' corresponds to the user.Info.GetExtra() method from the authenticator.
// - 'uid' is the information about the requesting user. e.g. request.uid == '1'
//
// Documentation on CEL: https://kubernetes.io/docs/reference/using-api/cel/
Expression string `json:"expression"`
}

View File

@ -586,6 +586,13 @@ type WebhookMatchCondition struct {
// If version specified by subjectAccessReviewVersion in the request variable is v1beta1,
// the contents would be converted to the v1 version before evaluating the CEL expression.
//
// - 'resourceAttributes' describes information for a resource access request and is unset for non-resource requests. e.g. has(request.resourceAttributes) && request.resourceAttributes.namespace == 'default'
// - 'nonResourceAttributes' describes information for a non-resource access request and is unset for resource requests. e.g. has(request.nonResourceAttributes) && request.nonResourceAttributes.path == '/healthz'.
// - 'user' is the user to test for. e.g. request.user == 'alice'
// - 'groups' is the groups to test for. e.g. ('group1' in request.groups)
// - 'extra' corresponds to the user.Info.GetExtra() method from the authenticator.
// - 'uid' is the information about the requesting user. e.g. request.uid == '1'
//
// Documentation on CEL: https://kubernetes.io/docs/reference/using-api/cel/
Expression string `json:"expression"`
}

View File

@ -38,14 +38,13 @@ import (
authenticationcel "k8s.io/apiserver/pkg/authentication/cel"
authorizationcel "k8s.io/apiserver/pkg/authorization/cel"
"k8s.io/apiserver/pkg/cel"
"k8s.io/apiserver/pkg/cel/environment"
"k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/client-go/util/cert"
)
// ValidateAuthenticationConfiguration validates a given AuthenticationConfiguration.
func ValidateAuthenticationConfiguration(c *api.AuthenticationConfiguration, disallowedIssuers []string) field.ErrorList {
func ValidateAuthenticationConfiguration(compiler authenticationcel.Compiler, c *api.AuthenticationConfiguration, disallowedIssuers []string) field.ErrorList {
root := field.NewPath("jwt")
var allErrs field.ErrorList
@ -62,7 +61,7 @@ func ValidateAuthenticationConfiguration(c *api.AuthenticationConfiguration, dis
seenDiscoveryURLs := sets.New[string]()
for i, a := range c.JWT {
fldPath := root.Index(i)
_, errs := validateJWTAuthenticator(a, fldPath, sets.New(disallowedIssuers...), utilfeature.DefaultFeatureGate.Enabled(features.StructuredAuthenticationConfiguration))
_, errs := validateJWTAuthenticator(compiler, a, fldPath, sets.New(disallowedIssuers...), utilfeature.DefaultFeatureGate.Enabled(features.StructuredAuthenticationConfiguration))
allErrs = append(allErrs, errs...)
if seenIssuers.Has(a.Issuer.URL) {
@ -93,18 +92,16 @@ func ValidateAuthenticationConfiguration(c *api.AuthenticationConfiguration, dis
// CompileAndValidateJWTAuthenticator validates a given JWTAuthenticator and returns a CELMapper with the compiled
// CEL expressions for claim mappings and validation rules.
// This is exported for use in oidc package.
func CompileAndValidateJWTAuthenticator(authenticator api.JWTAuthenticator, disallowedIssuers []string) (authenticationcel.CELMapper, field.ErrorList) {
return validateJWTAuthenticator(authenticator, nil, sets.New(disallowedIssuers...), utilfeature.DefaultFeatureGate.Enabled(features.StructuredAuthenticationConfiguration))
func CompileAndValidateJWTAuthenticator(compiler authenticationcel.Compiler, authenticator api.JWTAuthenticator, disallowedIssuers []string) (authenticationcel.CELMapper, field.ErrorList) {
return validateJWTAuthenticator(compiler, authenticator, nil, sets.New(disallowedIssuers...), utilfeature.DefaultFeatureGate.Enabled(features.StructuredAuthenticationConfiguration))
}
func validateJWTAuthenticator(authenticator api.JWTAuthenticator, fldPath *field.Path, disallowedIssuers sets.Set[string], structuredAuthnFeatureEnabled bool) (authenticationcel.CELMapper, field.ErrorList) {
func validateJWTAuthenticator(compiler authenticationcel.Compiler, authenticator api.JWTAuthenticator, fldPath *field.Path, disallowedIssuers sets.Set[string], structuredAuthnFeatureEnabled bool) (authenticationcel.CELMapper, field.ErrorList) {
var allErrs field.ErrorList
// strictCost is set to true which enables the strict cost for CEL validation.
compiler := authenticationcel.NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true))
state := &validationState{}
allErrs = append(allErrs, validateIssuer(authenticator.Issuer, disallowedIssuers, fldPath.Child("issuer"))...)
allErrs = append(allErrs, validateIssuer(authenticator.Issuer, disallowedIssuers, fldPath.Child("issuer"), structuredAuthnFeatureEnabled)...)
allErrs = append(allErrs, validateClaimValidationRules(compiler, state, authenticator.ClaimValidationRules, fldPath.Child("claimValidationRules"), structuredAuthnFeatureEnabled)...)
allErrs = append(allErrs, validateClaimMappings(compiler, state, authenticator.ClaimMappings, fldPath.Child("claimMappings"), structuredAuthnFeatureEnabled)...)
allErrs = append(allErrs, validateUserValidationRules(compiler, state, authenticator.UserValidationRules, fldPath.Child("userValidationRules"), structuredAuthnFeatureEnabled)...)
@ -118,12 +115,12 @@ type validationState struct {
usesEmailVerifiedClaim bool
}
func validateIssuer(issuer api.Issuer, disallowedIssuers sets.Set[string], fldPath *field.Path) field.ErrorList {
func validateIssuer(issuer api.Issuer, disallowedIssuers sets.Set[string], fldPath *field.Path, structuredAuthnFeatureEnabled bool) field.ErrorList {
var allErrs field.ErrorList
allErrs = append(allErrs, validateIssuerURL(issuer.URL, disallowedIssuers, fldPath.Child("url"))...)
allErrs = append(allErrs, validateIssuerDiscoveryURL(issuer.URL, issuer.DiscoveryURL, fldPath.Child("discoveryURL"))...)
allErrs = append(allErrs, validateAudiences(issuer.Audiences, issuer.AudienceMatchPolicy, fldPath.Child("audiences"), fldPath.Child("audienceMatchPolicy"))...)
allErrs = append(allErrs, validateIssuerDiscoveryURL(issuer.URL, issuer.DiscoveryURL, fldPath.Child("discoveryURL"), structuredAuthnFeatureEnabled)...)
allErrs = append(allErrs, validateAudiences(issuer.Audiences, issuer.AudienceMatchPolicy, fldPath.Child("audiences"), fldPath.Child("audienceMatchPolicy"), structuredAuthnFeatureEnabled)...)
allErrs = append(allErrs, validateCertificateAuthority(issuer.CertificateAuthority, fldPath.Child("certificateAuthority"))...)
return allErrs
@ -137,13 +134,17 @@ func validateIssuerURL(issuerURL string, disallowedIssuers sets.Set[string], fld
return validateURL(issuerURL, disallowedIssuers, fldPath)
}
func validateIssuerDiscoveryURL(issuerURL, issuerDiscoveryURL string, fldPath *field.Path) field.ErrorList {
func validateIssuerDiscoveryURL(issuerURL, issuerDiscoveryURL string, fldPath *field.Path, structuredAuthnFeatureEnabled bool) field.ErrorList {
var allErrs field.ErrorList
if len(issuerDiscoveryURL) == 0 {
return nil
}
if !structuredAuthnFeatureEnabled {
allErrs = append(allErrs, field.Invalid(fldPath, issuerDiscoveryURL, "discoveryURL is not supported when StructuredAuthenticationConfiguration feature gate is disabled"))
}
if len(issuerURL) > 0 && strings.TrimRight(issuerURL, "/") == strings.TrimRight(issuerDiscoveryURL, "/") {
allErrs = append(allErrs, field.Invalid(fldPath, issuerDiscoveryURL, "discoveryURL must be different from URL"))
}
@ -181,7 +182,7 @@ func validateURL(issuerURL string, disallowedIssuers sets.Set[string], fldPath *
return allErrs
}
func validateAudiences(audiences []string, audienceMatchPolicy api.AudienceMatchPolicyType, fldPath, audienceMatchPolicyFldPath *field.Path) field.ErrorList {
func validateAudiences(audiences []string, audienceMatchPolicy api.AudienceMatchPolicyType, fldPath, audienceMatchPolicyFldPath *field.Path, structuredAuthnFeatureEnabled bool) field.ErrorList {
var allErrs field.ErrorList
if len(audiences) == 0 {
@ -189,6 +190,10 @@ func validateAudiences(audiences []string, audienceMatchPolicy api.AudienceMatch
return allErrs
}
if len(audiences) > 1 && !structuredAuthnFeatureEnabled {
allErrs = append(allErrs, field.Invalid(fldPath, audiences, "multiple audiences are not supported when StructuredAuthenticationConfiguration feature gate is disabled"))
}
seenAudiences := sets.NewString()
for i, audience := range audiences {
fldPath := fldPath.Index(i)
@ -347,6 +352,11 @@ func validateClaimMappings(compiler authenticationcel.Compiler, state *validatio
if mapping.Key != strings.ToLower(mapping.Key) {
allErrs = append(allErrs, field.Invalid(fldPath.Child("key"), mapping.Key, "key must be lowercase"))
}
if isKubernetesDomainPrefix(mapping.Key) {
allErrs = append(allErrs, field.Invalid(fldPath.Child("key"), mapping.Key, "k8s.io, kubernetes.io and their subdomains are reserved for Kubernetes use"))
}
if seenExtraKeys.Has(mapping.Key) {
allErrs = append(allErrs, field.Duplicate(fldPath.Child("key"), mapping.Key))
continue
@ -386,6 +396,24 @@ func validateClaimMappings(compiler authenticationcel.Compiler, state *validatio
return allErrs
}
func isKubernetesDomainPrefix(key string) bool {
domainPrefix := getDomainPrefix(key)
if domainPrefix == "kubernetes.io" || strings.HasSuffix(domainPrefix, ".kubernetes.io") {
return true
}
if domainPrefix == "k8s.io" || strings.HasSuffix(domainPrefix, ".k8s.io") {
return true
}
return false
}
func getDomainPrefix(key string) string {
if parts := strings.SplitN(key, "/", 2); len(parts) == 2 {
return parts[0]
}
return ""
}
func usesEmailClaim(ast *celgo.Ast) bool {
return hasSelectExp(ast.Expr(), "claims", "email")
}
@ -585,7 +613,7 @@ func compileUserCELExpression(compiler authenticationcel.Compiler, expression au
}
// ValidateAuthorizationConfiguration validates a given AuthorizationConfiguration.
func ValidateAuthorizationConfiguration(fldPath *field.Path, c *api.AuthorizationConfiguration, knownTypes sets.String, repeatableTypes sets.String) field.ErrorList {
func ValidateAuthorizationConfiguration(compiler authorizationcel.Compiler, fldPath *field.Path, c *api.AuthorizationConfiguration, knownTypes sets.Set[string], repeatableTypes sets.Set[string]) field.ErrorList {
allErrs := field.ErrorList{}
if len(c.Authorizers) == 0 {
@ -602,7 +630,7 @@ func ValidateAuthorizationConfiguration(fldPath *field.Path, c *api.Authorizatio
continue
}
if !knownTypes.Has(aType) {
allErrs = append(allErrs, field.NotSupported(fldPath.Child("type"), aType, knownTypes.List()))
allErrs = append(allErrs, field.NotSupported(fldPath.Child("type"), aType, sets.List(knownTypes)))
continue
}
if seenAuthorizerTypes.Has(aType) && !repeatableTypes.Has(aType) {
@ -626,7 +654,7 @@ func ValidateAuthorizationConfiguration(fldPath *field.Path, c *api.Authorizatio
allErrs = append(allErrs, field.Required(fldPath.Child("webhook"), "required when type=Webhook"))
continue
}
allErrs = append(allErrs, ValidateWebhookConfiguration(fldPath, a.Webhook)...)
allErrs = append(allErrs, ValidateWebhookConfiguration(compiler, fldPath, a.Webhook)...)
default:
if a.Webhook != nil {
allErrs = append(allErrs, field.Invalid(fldPath.Child("webhook"), "non-null", "may only be specified when type=Webhook"))
@ -637,7 +665,7 @@ func ValidateAuthorizationConfiguration(fldPath *field.Path, c *api.Authorizatio
return allErrs
}
func ValidateWebhookConfiguration(fldPath *field.Path, c *api.WebhookConfiguration) field.ErrorList {
func ValidateWebhookConfiguration(compiler authorizationcel.Compiler, fldPath *field.Path, c *api.WebhookConfiguration) field.ErrorList {
allErrs := field.ErrorList{}
if c.Timeout.Duration == 0 {
@ -709,7 +737,7 @@ func ValidateWebhookConfiguration(fldPath *field.Path, c *api.WebhookConfigurati
allErrs = append(allErrs, field.NotSupported(fldPath.Child("connectionInfo", "type"), c.ConnectionInfo, []string{api.AuthorizationWebhookConnectionInfoTypeInCluster, api.AuthorizationWebhookConnectionInfoTypeKubeConfigFile}))
}
_, errs := compileMatchConditions(c.MatchConditions, fldPath, utilfeature.DefaultFeatureGate.Enabled(features.StructuredAuthorizationConfiguration))
_, errs := compileMatchConditions(compiler, c.MatchConditions, fldPath, utilfeature.DefaultFeatureGate.Enabled(features.StructuredAuthorizationConfiguration))
allErrs = append(allErrs, errs...)
return allErrs
@ -717,11 +745,11 @@ func ValidateWebhookConfiguration(fldPath *field.Path, c *api.WebhookConfigurati
// ValidateAndCompileMatchConditions validates a given webhook's matchConditions.
// This is exported for use in authz package.
func ValidateAndCompileMatchConditions(matchConditions []api.WebhookMatchCondition) (*authorizationcel.CELMatcher, field.ErrorList) {
return compileMatchConditions(matchConditions, nil, utilfeature.DefaultFeatureGate.Enabled(features.StructuredAuthorizationConfiguration))
func ValidateAndCompileMatchConditions(compiler authorizationcel.Compiler, matchConditions []api.WebhookMatchCondition) (*authorizationcel.CELMatcher, field.ErrorList) {
return compileMatchConditions(compiler, matchConditions, nil, utilfeature.DefaultFeatureGate.Enabled(features.StructuredAuthorizationConfiguration))
}
func compileMatchConditions(matchConditions []api.WebhookMatchCondition, fldPath *field.Path, structuredAuthzFeatureEnabled bool) (*authorizationcel.CELMatcher, field.ErrorList) {
func compileMatchConditions(compiler authorizationcel.Compiler, matchConditions []api.WebhookMatchCondition, fldPath *field.Path, structuredAuthzFeatureEnabled bool) (*authorizationcel.CELMatcher, field.ErrorList) {
var allErrs field.ErrorList
// should fail when match conditions are used without feature enabled
if len(matchConditions) > 0 && !structuredAuthzFeatureEnabled {
@ -732,8 +760,6 @@ func compileMatchConditions(matchConditions []api.WebhookMatchCondition, fldPath
return nil, allErrs
}
// strictCost is set to true which enables the strict cost for CEL validation.
compiler := authorizationcel.NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true))
seenExpressions := sets.NewString()
var compilationResults []authorizationcel.CompilationResult
var usesFieldSelector, usesLabelSelector bool

View File

@ -77,6 +77,7 @@ func (c DelegatingAuthenticatorConfig) New() (authenticator.Request, *spec.Secur
c.RequestHeaderConfig.CAContentProvider.VerifyOptions,
c.RequestHeaderConfig.AllowedClientNames,
c.RequestHeaderConfig.UsernameHeaders,
c.RequestHeaderConfig.UIDHeaders,
c.RequestHeaderConfig.GroupHeaders,
c.RequestHeaderConfig.ExtraHeaderPrefixes,
)

View File

@ -24,6 +24,8 @@ import (
type RequestHeaderConfig struct {
// UsernameHeaders are the headers to check (in order, case-insensitively) for an identity. The first header with a value wins.
UsernameHeaders headerrequest.StringSliceProvider
// UsernameHeaders are the headers to check (in order, case-insensitively) for an identity UID. The first header with a value wins.
UIDHeaders headerrequest.StringSliceProvider
// GroupHeaders are the headers to check (case-insensitively) for a group names. All values will be used.
GroupHeaders headerrequest.StringSliceProvider
// ExtraHeaderPrefixes are the head prefixes to check (case-insentively) for filling in

View File

@ -39,6 +39,12 @@ type compiler struct {
varEnvs map[string]*environment.EnvSet
}
// NewDefaultCompiler returns a new Compiler following the default compatibility version.
// Note: the compiler construction depends on feature gates and the compatibility version to be initialized.
func NewDefaultCompiler() Compiler {
return NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true))
}
// NewCompiler returns a new Compiler.
func NewCompiler(env *environment.EnvSet) Compiler {
return &compiler{

View File

@ -53,6 +53,9 @@ type requestHeaderAuthRequestHandler struct {
// nameHeaders are the headers to check (in order, case-insensitively) for an identity. The first header with a value wins.
nameHeaders StringSliceProvider
// nameHeaders are the headers to check (in order, case-insensitively) for an identity UID. The first header with a value wins.
uidHeaders StringSliceProvider
// groupHeaders are the headers to check (case-insensitively) for group membership. All values of all headers will be added.
groupHeaders StringSliceProvider
@ -61,11 +64,15 @@ type requestHeaderAuthRequestHandler struct {
extraHeaderPrefixes StringSliceProvider
}
func New(nameHeaders, groupHeaders, extraHeaderPrefixes []string) (authenticator.Request, error) {
func New(nameHeaders, uidHeaders, groupHeaders, extraHeaderPrefixes []string) (authenticator.Request, error) {
trimmedNameHeaders, err := trimHeaders(nameHeaders...)
if err != nil {
return nil, err
}
trimmedUIDHeaders, err := trimHeaders(uidHeaders...)
if err != nil {
return nil, err
}
trimmedGroupHeaders, err := trimHeaders(groupHeaders...)
if err != nil {
return nil, err
@ -77,14 +84,16 @@ func New(nameHeaders, groupHeaders, extraHeaderPrefixes []string) (authenticator
return NewDynamic(
StaticStringSlice(trimmedNameHeaders),
StaticStringSlice(trimmedUIDHeaders),
StaticStringSlice(trimmedGroupHeaders),
StaticStringSlice(trimmedExtraHeaderPrefixes),
), nil
}
func NewDynamic(nameHeaders, groupHeaders, extraHeaderPrefixes StringSliceProvider) authenticator.Request {
func NewDynamic(nameHeaders, uidHeaders, groupHeaders, extraHeaderPrefixes StringSliceProvider) authenticator.Request {
return &requestHeaderAuthRequestHandler{
nameHeaders: nameHeaders,
uidHeaders: uidHeaders,
groupHeaders: groupHeaders,
extraHeaderPrefixes: extraHeaderPrefixes,
}
@ -103,8 +112,8 @@ func trimHeaders(headerNames ...string) ([]string, error) {
return ret, nil
}
func NewDynamicVerifyOptionsSecure(verifyOptionFn x509request.VerifyOptionFunc, proxyClientNames, nameHeaders, groupHeaders, extraHeaderPrefixes StringSliceProvider) authenticator.Request {
headerAuthenticator := NewDynamic(nameHeaders, groupHeaders, extraHeaderPrefixes)
func NewDynamicVerifyOptionsSecure(verifyOptionFn x509request.VerifyOptionFunc, proxyClientNames, nameHeaders, uidHeaders, groupHeaders, extraHeaderPrefixes StringSliceProvider) authenticator.Request {
headerAuthenticator := NewDynamic(nameHeaders, uidHeaders, groupHeaders, extraHeaderPrefixes)
return x509request.NewDynamicCAVerifier(verifyOptionFn, headerAuthenticator, proxyClientNames)
}
@ -114,25 +123,30 @@ func (a *requestHeaderAuthRequestHandler) AuthenticateRequest(req *http.Request)
if len(name) == 0 {
return nil, false, nil
}
uid := headerValue(req.Header, a.uidHeaders.Value())
groups := allHeaderValues(req.Header, a.groupHeaders.Value())
extra := newExtra(req.Header, a.extraHeaderPrefixes.Value())
// clear headers used for authentication
ClearAuthenticationHeaders(req.Header, a.nameHeaders, a.groupHeaders, a.extraHeaderPrefixes)
ClearAuthenticationHeaders(req.Header, a.nameHeaders, a.uidHeaders, a.groupHeaders, a.extraHeaderPrefixes)
return &authenticator.Response{
User: &user.DefaultInfo{
Name: name,
UID: uid,
Groups: groups,
Extra: extra,
},
}, true, nil
}
func ClearAuthenticationHeaders(h http.Header, nameHeaders, groupHeaders, extraHeaderPrefixes StringSliceProvider) {
func ClearAuthenticationHeaders(h http.Header, nameHeaders, uidHeaders, groupHeaders, extraHeaderPrefixes StringSliceProvider) {
for _, headerName := range nameHeaders.Value() {
h.Del(headerName)
}
for _, headerName := range uidHeaders.Value() {
h.Del(headerName)
}
for _, headerName := range groupHeaders.Value() {
h.Del(headerName)
}

View File

@ -45,6 +45,7 @@ const (
// RequestHeaderAuthRequestProvider a provider that knows how to dynamically fill parts of RequestHeaderConfig struct
type RequestHeaderAuthRequestProvider interface {
UsernameHeaders() []string
UIDHeaders() []string
GroupHeaders() []string
ExtraHeaderPrefixes() []string
AllowedClientNames() []string
@ -54,6 +55,7 @@ var _ RequestHeaderAuthRequestProvider = &RequestHeaderAuthRequestController{}
type requestHeaderBundle struct {
UsernameHeaders []string
UIDHeaders []string
GroupHeaders []string
ExtraHeaderPrefixes []string
AllowedClientNames []string
@ -80,6 +82,7 @@ type RequestHeaderAuthRequestController struct {
exportedRequestHeaderBundle atomic.Value
usernameHeadersKey string
uidHeadersKey string
groupHeadersKey string
extraHeaderPrefixesKey string
allowedClientNamesKey string
@ -90,7 +93,7 @@ func NewRequestHeaderAuthRequestController(
cmName string,
cmNamespace string,
client kubernetes.Interface,
usernameHeadersKey, groupHeadersKey, extraHeaderPrefixesKey, allowedClientNamesKey string) *RequestHeaderAuthRequestController {
usernameHeadersKey, uidHeadersKey, groupHeadersKey, extraHeaderPrefixesKey, allowedClientNamesKey string) *RequestHeaderAuthRequestController {
c := &RequestHeaderAuthRequestController{
name: "RequestHeaderAuthRequestController",
@ -100,6 +103,7 @@ func NewRequestHeaderAuthRequestController(
configmapNamespace: cmNamespace,
usernameHeadersKey: usernameHeadersKey,
uidHeadersKey: uidHeadersKey,
groupHeadersKey: groupHeadersKey,
extraHeaderPrefixesKey: extraHeaderPrefixesKey,
allowedClientNamesKey: allowedClientNamesKey,
@ -152,6 +156,10 @@ func (c *RequestHeaderAuthRequestController) UsernameHeaders() []string {
return c.loadRequestHeaderFor(c.usernameHeadersKey)
}
func (c *RequestHeaderAuthRequestController) UIDHeaders() []string {
return c.loadRequestHeaderFor(c.uidHeadersKey)
}
func (c *RequestHeaderAuthRequestController) GroupHeaders() []string {
return c.loadRequestHeaderFor(c.groupHeadersKey)
}
@ -278,6 +286,11 @@ func (c *RequestHeaderAuthRequestController) getRequestHeaderBundleFromConfigMap
return nil, err
}
uidHeaderCurrentValue, err := deserializeStrings(cm.Data[c.uidHeadersKey])
if err != nil {
return nil, err
}
groupHeadersCurrentValue, err := deserializeStrings(cm.Data[c.groupHeadersKey])
if err != nil {
return nil, err
@ -296,6 +309,7 @@ func (c *RequestHeaderAuthRequestController) getRequestHeaderBundleFromConfigMap
return &requestHeaderBundle{
UsernameHeaders: usernameHeaderCurrentValue,
UIDHeaders: uidHeaderCurrentValue,
GroupHeaders: groupHeadersCurrentValue,
ExtraHeaderPrefixes: extraHeaderPrefixesCurrentValue,
AllowedClientNames: allowedClientNamesCurrentValue,
@ -312,6 +326,8 @@ func (c *RequestHeaderAuthRequestController) loadRequestHeaderFor(key string) []
switch key {
case c.usernameHeadersKey:
return headerBundle.UsernameHeaders
case c.uidHeadersKey:
return headerBundle.UIDHeaders
case c.groupHeadersKey:
return headerBundle.GroupHeaders
case c.extraHeaderPrefixesKey:

View File

@ -17,6 +17,7 @@ limitations under the License.
package x509
import (
"crypto/sha256"
"crypto/x509"
"crypto/x509/pkix"
"encoding/hex"
@ -276,10 +277,17 @@ var CommonNameUserConversion = UserConversionFunc(func(chain []*x509.Certificate
if len(chain[0].Subject.CommonName) == 0 {
return nil, false, nil
}
fp := sha256.Sum256(chain[0].Raw)
id := "X509SHA256=" + hex.EncodeToString(fp[:])
return &authenticator.Response{
User: &user.DefaultInfo{
Name: chain[0].Subject.CommonName,
Groups: chain[0].Subject.Organization,
Extra: map[string][]string{
user.CredentialIDKey: {id},
},
},
}, true, nil
})

View File

@ -17,18 +17,12 @@ limitations under the License.
package serviceaccount
import (
"context"
"fmt"
"strings"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apiserver/pkg/authentication/user"
v1core "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/klog/v2"
)
const (
@ -36,9 +30,6 @@ const (
ServiceAccountUsernameSeparator = ":"
ServiceAccountGroupPrefix = "system:serviceaccounts:"
AllServiceAccountsGroup = "system:serviceaccounts"
// CredentialIDKey is the key used in a user's "extra" to specify the unique
// identifier for this identity document).
CredentialIDKey = "authentication.kubernetes.io/credential-id"
// IssuedCredentialIDAuditAnnotationKey is the annotation key used in the audit event that is persisted to the
// '/token' endpoint for service accounts.
// This annotation indicates the generated credential identifier for the service account token being issued.
@ -156,7 +147,7 @@ func (sa *ServiceAccountInfo) UserInfo() user.Info {
if info.Extra == nil {
info.Extra = make(map[string][]string)
}
info.Extra[CredentialIDKey] = []string{sa.CredentialID}
info.Extra[user.CredentialIDKey] = []string{sa.CredentialID}
}
if sa.NodeName != "" {
if info.Extra == nil {
@ -172,15 +163,6 @@ func (sa *ServiceAccountInfo) UserInfo() user.Info {
return info
}
// CredentialIDForJTI converts a given JTI string into a credential identifier for use in a
// users 'extra' info.
func CredentialIDForJTI(jti string) string {
if len(jti) == 0 {
return ""
}
return "JTI=" + jti
}
// IsServiceAccountToken returns true if the secret is a valid api token for the service account
func IsServiceAccountToken(secret *v1.Secret, sa *v1.ServiceAccount) bool {
if secret.Type != v1.SecretTypeServiceAccountToken {
@ -200,29 +182,3 @@ func IsServiceAccountToken(secret *v1.Secret, sa *v1.ServiceAccount) bool {
return true
}
func GetOrCreateServiceAccount(coreClient v1core.CoreV1Interface, namespace, name string) (*v1.ServiceAccount, error) {
sa, err := coreClient.ServiceAccounts(namespace).Get(context.TODO(), name, metav1.GetOptions{})
if err == nil {
return sa, nil
}
if !apierrors.IsNotFound(err) {
return nil, err
}
// Create the namespace if we can't verify it exists.
// Tolerate errors, since we don't know whether this component has namespace creation permissions.
if _, err := coreClient.Namespaces().Get(context.TODO(), namespace, metav1.GetOptions{}); apierrors.IsNotFound(err) {
if _, err = coreClient.Namespaces().Create(context.TODO(), &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}}, metav1.CreateOptions{}); err != nil && !apierrors.IsAlreadyExists(err) {
klog.Warningf("create non-exist namespace %s failed:%v", namespace, err)
}
}
// Create the service account
sa, err = coreClient.ServiceAccounts(namespace).Create(context.TODO(), &v1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: name}}, metav1.CreateOptions{})
if apierrors.IsAlreadyExists(err) {
// If we're racing to init and someone else already created it, re-fetch
return coreClient.ServiceAccounts(namespace).Get(context.TODO(), name, metav1.GetOptions{})
}
return sa, err
}

View File

@ -66,8 +66,8 @@ func (i *DefaultInfo) GetExtra() map[string][]string {
return i.Extra
}
// well-known user and group names
const (
// well-known user and group names
SystemPrivilegedGroup = "system:masters"
NodesGroup = "system:nodes"
MonitoringGroup = "system:monitoring"
@ -81,4 +81,8 @@ const (
KubeProxy = "system:kube-proxy"
KubeControllerManager = "system:kube-controller-manager"
KubeScheduler = "system:kube-scheduler"
// CredentialIDKey is the key used in a user's "extra" to specify the unique
// identifier for this identity document).
CredentialIDKey = "authentication.kubernetes.io/credential-id"
)

View File

@ -92,7 +92,7 @@ func (f AuthorizerFunc) Authorize(ctx context.Context, a Attributes) (Decision,
// RuleResolver provides a mechanism for resolving the list of rules that apply to a given user within a namespace.
type RuleResolver interface {
// RulesFor get the list of cluster wide rules, the list of rules in the specific namespace, incomplete status and errors.
RulesFor(user user.Info, namespace string) ([]ResourceRuleInfo, []NonResourceRuleInfo, bool, error)
RulesFor(ctx context.Context, user user.Info, namespace string) ([]ResourceRuleInfo, []NonResourceRuleInfo, bool, error)
}
// RequestAttributesGetter provides a function that extracts Attributes from an http.Request

View File

@ -33,7 +33,7 @@ func (alwaysAllowAuthorizer) Authorize(ctx context.Context, a authorizer.Attribu
return authorizer.DecisionAllow, "", nil
}
func (alwaysAllowAuthorizer) RulesFor(user user.Info, namespace string) ([]authorizer.ResourceRuleInfo, []authorizer.NonResourceRuleInfo, bool, error) {
func (alwaysAllowAuthorizer) RulesFor(ctx context.Context, user user.Info, namespace string) ([]authorizer.ResourceRuleInfo, []authorizer.NonResourceRuleInfo, bool, error) {
return []authorizer.ResourceRuleInfo{
&authorizer.DefaultResourceRuleInfo{
Verbs: []string{"*"},
@ -61,7 +61,7 @@ func (alwaysDenyAuthorizer) Authorize(ctx context.Context, a authorizer.Attribut
return authorizer.DecisionNoOpinion, "Everything is forbidden.", nil
}
func (alwaysDenyAuthorizer) RulesFor(user user.Info, namespace string) ([]authorizer.ResourceRuleInfo, []authorizer.NonResourceRuleInfo, bool, error) {
func (alwaysDenyAuthorizer) RulesFor(ctx context.Context, user user.Info, namespace string) ([]authorizer.ResourceRuleInfo, []authorizer.NonResourceRuleInfo, bool, error) {
return []authorizer.ResourceRuleInfo{}, []authorizer.NonResourceRuleInfo{}, false, nil
}

View File

@ -22,6 +22,7 @@ import (
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apiserver/pkg/authorization/authorizer"
authorizationcel "k8s.io/apiserver/pkg/authorization/cel"
"k8s.io/apiserver/plugin/pkg/authorizer/webhook"
authorizationclient "k8s.io/client-go/kubernetes/typed/authorization/v1"
)
@ -31,6 +32,9 @@ import (
type DelegatingAuthorizerConfig struct {
SubjectAccessReviewClient authorizationclient.AuthorizationV1Interface
// Compiler is the CEL compiler to use for evaluating policies. If nil, a default compiler will be used.
Compiler authorizationcel.Compiler
// AllowCacheTTL is the length of time that a successful authorization response will be cached
AllowCacheTTL time.Duration
@ -48,6 +52,10 @@ func (c DelegatingAuthorizerConfig) New() (authorizer.Authorizer, error) {
if c.WebhookRetryBackoff == nil {
return nil, errors.New("retry backoff parameters for delegating authorization webhook has not been specified")
}
compiler := c.Compiler
if compiler == nil {
compiler = authorizationcel.NewDefaultCompiler()
}
return webhook.NewFromInterface(
c.SubjectAccessReviewClient,
@ -56,5 +64,6 @@ func (c DelegatingAuthorizerConfig) New() (authorizer.Authorizer, error) {
*c.WebhookRetryBackoff,
authorizer.DecisionNoOpinion,
NewDelegatingAuthorizerMetrics(),
compiler,
)
}

View File

@ -65,6 +65,12 @@ type compiler struct {
envSet *environment.EnvSet
}
// NewDefaultCompiler returns a new Compiler following the default compatibility version.
// Note: the compiler construction depends on feature gates and the compatibility version to be initialized.
func NewDefaultCompiler() Compiler {
return NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true))
}
// NewCompiler returns a new Compiler.
func NewCompiler(env *environment.EnvSet) Compiler {
return &compiler{

View File

@ -77,7 +77,7 @@ func NewRuleResolvers(authorizationHandlers ...authorizer.RuleResolver) authoriz
}
// RulesFor against a chain of authorizer.RuleResolver objects and returns nil if successful and returns error if unsuccessful
func (authzHandler unionAuthzRulesHandler) RulesFor(user user.Info, namespace string) ([]authorizer.ResourceRuleInfo, []authorizer.NonResourceRuleInfo, bool, error) {
func (authzHandler unionAuthzRulesHandler) RulesFor(ctx context.Context, user user.Info, namespace string) ([]authorizer.ResourceRuleInfo, []authorizer.NonResourceRuleInfo, bool, error) {
var (
errList []error
resourceRulesList []authorizer.ResourceRuleInfo
@ -86,7 +86,7 @@ func (authzHandler unionAuthzRulesHandler) RulesFor(user user.Info, namespace st
incompleteStatus := false
for _, currAuthzHandler := range authzHandler {
resourceRules, nonResourceRules, incomplete, err := currAuthzHandler.RulesFor(user, namespace)
resourceRules, nonResourceRules, incomplete, err := currAuthzHandler.RulesFor(ctx, user, namespace)
if incomplete {
incompleteStatus = true

127
vendor/k8s.io/apiserver/pkg/cel/common/typeprovider.go generated vendored Normal file
View File

@ -0,0 +1,127 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package common
import (
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
)
// TypeResolver resolves a type by a given name.
type TypeResolver interface {
// Resolve resolves the type by its name.
// This function returns false if the name does not refer to a known object type.
Resolve(name string) (ResolvedType, bool)
}
// ResolvedType refers an object type that can be looked up for its fields.
type ResolvedType interface {
ref.Type
Type() *types.Type
// Field finds the field by the field name, or false if the field is not known.
// This function directly return a FieldType that is known to CEL to be more customizable.
Field(name string) (*types.FieldType, bool)
// FieldNames returns the field names associated with the type, if the type
// is found.
FieldNames() ([]string, bool)
// Val creates an instance for the ResolvedType, given its fields and their values.
Val(fields map[string]ref.Val) ref.Val
}
// ResolverTypeProvider delegates type resolution first to the TypeResolver and then
// to the underlying types.Provider for types not resolved by the TypeResolver.
type ResolverTypeProvider struct {
typeResolver TypeResolver
underlyingTypeProvider types.Provider
}
var _ types.Provider = (*ResolverTypeProvider)(nil)
// FindStructType returns the Type give a qualified type name, by looking it up with
// the DynamicTypeResolver and translating it to CEL Type.
// If the type is not known to the DynamicTypeResolver, the lookup falls back to the underlying
// ResolverTypeProvider instead.
func (p *ResolverTypeProvider) FindStructType(structType string) (*types.Type, bool) {
t, ok := p.typeResolver.Resolve(structType)
if ok {
return types.NewTypeTypeWithParam(t.Type()), true
}
return p.underlyingTypeProvider.FindStructType(structType)
}
// FindStructFieldNames returns the field names associated with the type, if the type
// is found.
func (p *ResolverTypeProvider) FindStructFieldNames(structType string) ([]string, bool) {
t, ok := p.typeResolver.Resolve(structType)
if ok {
return t.FieldNames()
}
return p.underlyingTypeProvider.FindStructFieldNames(structType)
}
// FindStructFieldType returns the field type for a checked type value.
// Returns false if the field could not be found.
func (p *ResolverTypeProvider) FindStructFieldType(structType, fieldName string) (*types.FieldType, bool) {
t, ok := p.typeResolver.Resolve(structType)
if ok {
return t.Field(fieldName)
}
return p.underlyingTypeProvider.FindStructFieldType(structType, fieldName)
}
// NewValue creates a new type value from a qualified name and map of fields.
func (p *ResolverTypeProvider) NewValue(structType string, fields map[string]ref.Val) ref.Val {
t, ok := p.typeResolver.Resolve(structType)
if ok {
return t.Val(fields)
}
return p.underlyingTypeProvider.NewValue(structType, fields)
}
func (p *ResolverTypeProvider) EnumValue(enumName string) ref.Val {
return p.underlyingTypeProvider.EnumValue(enumName)
}
func (p *ResolverTypeProvider) FindIdent(identName string) (ref.Val, bool) {
return p.underlyingTypeProvider.FindIdent(identName)
}
// ResolverEnvOption creates the ResolverTypeProvider with a given DynamicTypeResolver,
// and also returns the CEL ResolverEnvOption to apply it to the env.
func ResolverEnvOption(resolver TypeResolver) cel.EnvOption {
_, envOpt := NewResolverTypeProviderAndEnvOption(resolver)
return envOpt
}
// NewResolverTypeProviderAndEnvOption creates the ResolverTypeProvider with a given DynamicTypeResolver,
// and also returns the CEL ResolverEnvOption to apply it to the env.
func NewResolverTypeProviderAndEnvOption(resolver TypeResolver) (*ResolverTypeProvider, cel.EnvOption) {
tp := &ResolverTypeProvider{typeResolver: resolver}
var envOption cel.EnvOption = func(e *cel.Env) (*cel.Env, error) {
// wrap the existing type provider (acquired from the env)
// and set new type provider for the env.
tp.underlyingTypeProvider = e.CELTypeProvider()
typeProviderOption := cel.CustomTypeProvider(tp)
return typeProviderOption(e)
}
return tp, envOption
}

View File

@ -33,7 +33,8 @@ import (
"k8s.io/apiserver/pkg/cel/library"
genericfeatures "k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature"
utilversion "k8s.io/apiserver/pkg/util/version"
"k8s.io/component-base/featuregate"
utilversion "k8s.io/component-base/version"
)
// DefaultCompatibilityVersion returns a default compatibility version for use with EnvSet
@ -49,7 +50,7 @@ import (
// A default version number equal to the current Kubernetes major.minor version
// indicates fast forward CEL features that can be used when rollback is no longer needed.
func DefaultCompatibilityVersion() *version.Version {
effectiveVer := utilversion.DefaultComponentGlobalsRegistry.EffectiveVersionFor(utilversion.DefaultKubeComponent)
effectiveVer := featuregate.DefaultComponentGlobalsRegistry.EffectiveVersionFor(featuregate.DefaultKubeComponent)
if effectiveVer == nil {
effectiveVer = utilversion.DefaultKubeEffectiveVersion()
}
@ -71,9 +72,9 @@ var baseOptsWithoutStrictCost = []VersionedOptions{
cel.EagerlyValidateDeclarations(true),
cel.DefaultUTCTimeZone(true),
library.URLs(),
library.Regex(),
library.Lists(),
UnversionedLib(library.URLs),
UnversionedLib(library.Regex),
UnversionedLib(library.Lists),
// cel-go v0.17.7 change the cost of has() from 0 to 1, but also provided the CostEstimatorOptions option to preserve the old behavior, so we enabled it at the same time we bumped our cel version to v0.17.7.
// Since it is a regression fix, we apply it uniformly to all code use v0.17.7.
@ -91,7 +92,7 @@ var baseOptsWithoutStrictCost = []VersionedOptions{
{
IntroducedVersion: version.MajorMinor(1, 27),
EnvOptions: []cel.EnvOption{
library.Authz(),
UnversionedLib(library.Authz),
},
},
{
@ -99,7 +100,7 @@ var baseOptsWithoutStrictCost = []VersionedOptions{
EnvOptions: []cel.EnvOption{
cel.CrossTypeNumericComparisons(true),
cel.OptionalTypes(),
library.Quantity(),
UnversionedLib(library.Quantity),
},
},
// add the new validator in 1.29
@ -138,15 +139,15 @@ var baseOptsWithoutStrictCost = []VersionedOptions{
{
IntroducedVersion: version.MajorMinor(1, 30),
EnvOptions: []cel.EnvOption{
library.IP(),
library.CIDR(),
UnversionedLib(library.IP),
UnversionedLib(library.CIDR),
},
},
// Format Library
{
IntroducedVersion: version.MajorMinor(1, 31),
EnvOptions: []cel.EnvOption{
library.Format(),
UnversionedLib(library.Format),
},
},
// Authz selectors
@ -165,7 +166,14 @@ var baseOptsWithoutStrictCost = []VersionedOptions{
return enabled
},
EnvOptions: []cel.EnvOption{
library.AuthzSelectors(),
UnversionedLib(library.AuthzSelectors),
},
},
// Two variable comprehensions
{
IntroducedVersion: version.MajorMinor(1, 32),
EnvOptions: []cel.EnvOption{
UnversionedLib(ext.TwoVarComprehensions),
},
},
}
@ -191,6 +199,19 @@ var StrictCostOpt = VersionedOptions{
},
}
// cacheBaseEnvs controls whether calls to MustBaseEnvSet are cached.
// Defaults to true, may be disabled by calling DisableBaseEnvSetCachingForTests.
var cacheBaseEnvs = true
// DisableBaseEnvSetCachingForTests clears and disables base env caching.
// This is only intended for unit tests exercising MustBaseEnvSet directly with different enablement options.
// It does not clear other initialization paths that may cache results of calling MustBaseEnvSet.
func DisableBaseEnvSetCachingForTests() {
cacheBaseEnvs = false
baseEnvs.Clear()
baseEnvsWithOption.Clear()
}
// MustBaseEnvSet returns the common CEL base environments for Kubernetes for Version, or panics
// if the version is nil, or does not have major and minor components.
//
@ -216,7 +237,9 @@ func MustBaseEnvSet(ver *version.Version, strictCost bool) *EnvSet {
}
entry, _, _ = baseEnvsSingleflight.Do(key, func() (interface{}, error) {
entry := mustNewEnvSet(ver, baseOpts)
baseEnvs.Store(key, entry)
if cacheBaseEnvs {
baseEnvs.Store(key, entry)
}
return entry, nil
})
} else {
@ -225,7 +248,9 @@ func MustBaseEnvSet(ver *version.Version, strictCost bool) *EnvSet {
}
entry, _, _ = baseEnvsWithOptionSingleflight.Do(key, func() (interface{}, error) {
entry := mustNewEnvSet(ver, baseOptsWithoutStrictCost)
baseEnvsWithOption.Store(key, entry)
if cacheBaseEnvs {
baseEnvsWithOption.Store(key, entry)
}
return entry, nil
})
}
@ -239,3 +264,20 @@ var (
baseEnvsSingleflight = &singleflight.Group{}
baseEnvsWithOptionSingleflight = &singleflight.Group{}
)
// UnversionedLib wraps library initialization calls like ext.Sets() or library.IP()
// to force compilation errors if the call evolves to include a varadic variable option.
//
// This provides automatic detection of a problem that is hard to catch in review--
// If a CEL library used in Kubernetes is unversioned and then become versioned, and we
// fail to set a desired version, the libraries defaults to the latest version, changing
// CEL environment without controlled rollout, bypassing the entire purpose of the base
// environment.
//
// If usages of this function fail to compile: add version=1 argument to all call sites
// that fail compilation while removing the UnversionedLib wrapper. Next, review
// the changes in the library present in higher versions and, if needed, use VersionedOptions to
// the base environment to roll out to a newer version safely.
func UnversionedLib(initializer func() cel.EnvOption) cel.EnvOption {
return initializer()
}

View File

@ -41,11 +41,11 @@ type Format struct {
MaxRegexSize int
}
func (d *Format) ConvertToNative(typeDesc reflect.Type) (interface{}, error) {
func (d Format) ConvertToNative(typeDesc reflect.Type) (interface{}, error) {
return nil, fmt.Errorf("type conversion error from 'Format' to '%v'", typeDesc)
}
func (d *Format) ConvertToType(typeVal ref.Type) ref.Val {
func (d Format) ConvertToType(typeVal ref.Type) ref.Val {
switch typeVal {
case FormatType:
return d
@ -56,18 +56,18 @@ func (d *Format) ConvertToType(typeVal ref.Type) ref.Val {
}
}
func (d *Format) Equal(other ref.Val) ref.Val {
otherDur, ok := other.(*Format)
func (d Format) Equal(other ref.Val) ref.Val {
otherDur, ok := other.(Format)
if !ok {
return types.MaybeNoSuchOverloadErr(other)
}
return types.Bool(d.Name == otherDur.Name)
}
func (d *Format) Type() ref.Type {
func (d Format) Type() ref.Type {
return FormatType
}
func (d *Format) Value() interface{} {
func (d Format) Value() interface{} {
return d
}

View File

@ -232,7 +232,20 @@ var authzLib = &authz{}
type authz struct{}
func (*authz) LibraryName() string {
return "k8s.authz"
return "kubernetes.authz"
}
func (*authz) Types() []*cel.Type {
return []*cel.Type{
AuthorizerType,
PathCheckType,
GroupCheckType,
ResourceCheckType,
DecisionType}
}
func (*authz) declarations() map[string][]cel.FunctionOpt {
return authzLibraryDecls
}
var authzLibraryDecls = map[string][]cel.FunctionOpt{
@ -324,7 +337,15 @@ var authzSelectorsLib = &authzSelectors{}
type authzSelectors struct{}
func (*authzSelectors) LibraryName() string {
return "k8s.authzSelectors"
return "kubernetes.authzSelectors"
}
func (*authzSelectors) Types() []*cel.Type {
return []*cel.Type{ResourceCheckType}
}
func (*authzSelectors) declarations() map[string][]cel.FunctionOpt {
return authzSelectorsLibraryDecls
}
var authzSelectorsLibraryDecls = map[string][]cel.FunctionOpt{

View File

@ -109,7 +109,15 @@ var cidrsLib = &cidrs{}
type cidrs struct{}
func (*cidrs) LibraryName() string {
return "net.cidr"
return "kubernetes.net.cidr"
}
func (*cidrs) declarations() map[string][]cel.FunctionOpt {
return cidrLibraryDecls
}
func (*cidrs) Types() []*cel.Type {
return []*cel.Type{apiservercel.CIDRType, apiservercel.IPType}
}
var cidrLibraryDecls = map[string][]cel.FunctionOpt{

View File

@ -18,14 +18,13 @@ package library
import (
"fmt"
"math"
"github.com/google/cel-go/checker"
"github.com/google/cel-go/common"
"github.com/google/cel-go/common/ast"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"github.com/google/cel-go/common/types/traits"
"math"
"k8s.io/apiserver/pkg/cel"
)
@ -36,16 +35,25 @@ var panicOnUnknown = false
// builtInFunctions is a list of functions used in cost tests that are not handled by CostEstimator.
var knownUnhandledFunctions = map[string]bool{
"uint": true,
"duration": true,
"bytes": true,
"timestamp": true,
"value": true,
"_==_": true,
"_&&_": true,
"_>_": true,
"!_": true,
"strings.quote": true,
"@not_strictly_false": true,
"uint": true,
"duration": true,
"bytes": true,
"cel.@mapInsert": true,
"timestamp": true,
"strings.quote": true,
"value": true,
"_==_": true,
"_&&_": true,
"_||_": true,
"_>_": true,
"_>=_": true,
"_<_": true,
"_<=_": true,
"!_": true,
"_?_:_": true,
"_+_": true,
"_-_": true,
}
// CostEstimator implements CEL's interpretable.ActualCostEstimator and checker.CostEstimator.
@ -98,7 +106,7 @@ func (l *CostEstimator) CallCost(function, overloadId string, args []ref.Val, re
cost += traversalCost(args[0]) // these O(n) operations all cost roughly the cost of a single traversal
}
return &cost
case "url", "lowerAscii", "upperAscii", "substring", "trim":
case "url", "lowerAscii", "upperAscii", "substring", "trim", "jsonpatch.escapeKey":
if len(args) >= 1 {
cost := uint64(math.Ceil(float64(actualSize(args[0])) * common.StringTraversalCostFactor))
return &cost
@ -201,7 +209,7 @@ func (l *CostEstimator) CallCost(function, overloadId string, args []ref.Val, re
}
case "validate":
if len(args) >= 2 {
format, isFormat := args[0].Value().(*cel.Format)
format, isFormat := args[0].Value().(cel.Format)
if isFormat {
strSize := actualSize(args[1])
@ -235,6 +243,26 @@ func (l *CostEstimator) CallCost(function, overloadId string, args []ref.Val, re
// url accessors
cost := uint64(1)
return &cost
case "_==_":
if len(args) == 2 {
unitCost := uint64(1)
lhs := args[0]
switch lhs.(type) {
case *cel.Quantity, cel.Quantity,
*cel.IP, cel.IP,
*cel.CIDR, cel.CIDR,
*cel.Format, cel.Format, // Formats have a small max size. Format takes pointer receiver.
*cel.URL, cel.URL, // TODO: Computing the actual cost is expensive, and changing this would be a breaking change
*cel.Semver, cel.Semver,
*authorizerVal, authorizerVal, *pathCheckVal, pathCheckVal, *groupCheckVal, groupCheckVal,
*resourceCheckVal, resourceCheckVal, *decisionVal, decisionVal:
return &unitCost
default:
if panicOnUnknown && lhs.Type() != nil && isRegisteredType(lhs.Type().TypeName()) {
panic(fmt.Errorf("CallCost: unhandled equality for Kubernetes type %T", lhs))
}
}
}
}
if panicOnUnknown && !knownUnhandledFunctions[function] {
panic(fmt.Errorf("CallCost: unhandled function %q or args %v", function, args))
@ -275,10 +303,10 @@ func (l *CostEstimator) EstimateCallCost(function, overloadId string, target *ch
return &checker.CallEstimate{CostEstimate: l.sizeEstimate(*target).MultiplyByCostFactor(common.StringTraversalCostFactor)}
}
}
case "url":
case "url", "jsonpatch.escapeKey":
if len(args) == 1 {
sz := l.sizeEstimate(args[0])
return &checker.CallEstimate{CostEstimate: sz.MultiplyByCostFactor(common.StringTraversalCostFactor)}
return &checker.CallEstimate{CostEstimate: sz.MultiplyByCostFactor(common.StringTraversalCostFactor), ResultSize: &sz}
}
case "lowerAscii", "upperAscii", "substring", "trim":
if target != nil {
@ -475,6 +503,40 @@ func (l *CostEstimator) EstimateCallCost(function, overloadId string, target *ch
case "getScheme", "getHostname", "getHost", "getPort", "getEscapedPath", "getQuery":
// url accessors
return &checker.CallEstimate{CostEstimate: checker.CostEstimate{Min: 1, Max: 1}}
case "_==_":
if len(args) == 2 {
lhs := args[0]
rhs := args[1]
if lhs.Type().Equal(rhs.Type()) == types.True {
t := lhs.Type()
if t.Kind() == types.OpaqueKind {
switch t.TypeName() {
case cel.IPType.TypeName(), cel.CIDRType.TypeName():
return &checker.CallEstimate{CostEstimate: checker.CostEstimate{Min: 1, Max: 1}}
}
}
if t.Kind() == types.StructKind {
switch t {
case cel.QuantityType, AuthorizerType, PathCheckType, // O(1) cost equality checks
GroupCheckType, ResourceCheckType, DecisionType, cel.SemverType:
return &checker.CallEstimate{CostEstimate: checker.CostEstimate{Min: 1, Max: 1}}
case cel.FormatType:
return &checker.CallEstimate{CostEstimate: checker.CostEstimate{Min: 1, Max: cel.MaxFormatSize}.MultiplyByCostFactor(common.StringTraversalCostFactor)}
case cel.URLType:
size := checker.SizeEstimate{Min: 1, Max: 1}
rhSize := rhs.ComputedSize()
lhSize := rhs.ComputedSize()
if rhSize != nil && lhSize != nil {
size = rhSize.Union(*lhSize)
}
return &checker.CallEstimate{CostEstimate: checker.CostEstimate{Min: 1, Max: size.Max}.MultiplyByCostFactor(common.StringTraversalCostFactor)}
}
}
if panicOnUnknown && isRegisteredType(t.TypeName()) {
panic(fmt.Errorf("EstimateCallCost: unhandled equality for Kubernetes type %v", t))
}
}
}
}
if panicOnUnknown && !knownUnhandledFunctions[function] {
panic(fmt.Errorf("EstimateCallCost: unhandled function %q, target %v, args %v", function, target, args))

View File

@ -25,6 +25,7 @@ import (
"github.com/google/cel-go/common/decls"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation"
"k8s.io/apimachinery/pkg/util/validation"
apiservercel "k8s.io/apiserver/pkg/cel"
@ -90,7 +91,15 @@ var formatLib = &format{}
type format struct{}
func (*format) LibraryName() string {
return "format"
return "kubernetes.format"
}
func (*format) Types() []*cel.Type {
return []*cel.Type{apiservercel.FormatType}
}
func (*format) declarations() map[string][]cel.FunctionOpt {
return formatLibraryDecls
}
func ZeroArgumentFunctionBinding(binding func() ref.Val) decls.OverloadOpt {
@ -124,7 +133,7 @@ func (*format) ProgramOptions() []cel.ProgramOption {
return []cel.ProgramOption{}
}
var ConstantFormats map[string]*apiservercel.Format = map[string]*apiservercel.Format{
var ConstantFormats = map[string]apiservercel.Format{
"dns1123Label": {
Name: "DNS1123Label",
ValidateFunc: func(s string) []string { return apimachineryvalidation.NameIsDNSLabel(s, false) },
@ -252,7 +261,7 @@ var formatLibraryDecls = map[string][]cel.FunctionOpt{
}
func formatValidate(arg1, arg2 ref.Val) ref.Val {
f, ok := arg1.Value().(*apiservercel.Format)
f, ok := arg1.Value().(apiservercel.Format)
if !ok {
return types.MaybeNoSuchOverloadErr(arg1)
}

View File

@ -132,7 +132,15 @@ var ipLib = &ip{}
type ip struct{}
func (*ip) LibraryName() string {
return "net.ip"
return "kubernetes.net.ip"
}
func (*ip) declarations() map[string][]cel.FunctionOpt {
return ipLibraryDecls
}
func (*ip) Types() []*cel.Type {
return []*cel.Type{apiservercel.IPType}
}
var ipLibraryDecls = map[string][]cel.FunctionOpt{

89
vendor/k8s.io/apiserver/pkg/cel/library/jsonpatch.go generated vendored Normal file
View File

@ -0,0 +1,89 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package library
import (
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"strings"
)
// JSONPatch provides a CEL function library extension of JSONPatch functions.
//
// jsonpatch.escapeKey
//
// Escapes a string for use as a JSONPatch path key.
//
// jsonpatch.escapeKey(<string>) <string>
//
// Examples:
//
// "/metadata/labels/" + jsonpatch.escapeKey('k8s.io/my~label') // returns "/metadata/labels/k8s.io~1my~0label"
func JSONPatch() cel.EnvOption {
return cel.Lib(jsonPatchLib)
}
var jsonPatchLib = &jsonPatch{}
type jsonPatch struct{}
func (*jsonPatch) LibraryName() string {
return "kubernetes.jsonpatch"
}
func (*jsonPatch) declarations() map[string][]cel.FunctionOpt {
return jsonPatchLibraryDecls
}
func (*jsonPatch) Types() []*cel.Type {
return []*cel.Type{}
}
var jsonPatchLibraryDecls = map[string][]cel.FunctionOpt{
"jsonpatch.escapeKey": {
cel.Overload("string_jsonpatch_escapeKey_string", []*cel.Type{cel.StringType}, cel.StringType,
cel.UnaryBinding(escape)),
},
}
func (*jsonPatch) CompileOptions() []cel.EnvOption {
var options []cel.EnvOption
for name, overloads := range jsonPatchLibraryDecls {
options = append(options, cel.Function(name, overloads...))
}
return options
}
func (*jsonPatch) ProgramOptions() []cel.ProgramOption {
return []cel.ProgramOption{}
}
var jsonPatchReplacer = strings.NewReplacer("/", "~1", "~", "~0")
func escapeKey(k string) string {
return jsonPatchReplacer.Replace(k)
}
func escape(arg ref.Val) ref.Val {
s, ok := arg.Value().(string)
if !ok {
return types.MaybeNoSuchOverloadErr(arg)
}
escaped := escapeKey(s)
return types.String(escaped)
}

61
vendor/k8s.io/apiserver/pkg/cel/library/libraries.go generated vendored Normal file
View File

@ -0,0 +1,61 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package library
import (
"github.com/google/cel-go/cel"
)
// Library represents a CEL library used by kubernetes.
type Library interface {
// SingletonLibrary provides the library name and ensures the library can be safely registered into environments.
cel.SingletonLibrary
// Types provides all custom types introduced by the library.
Types() []*cel.Type
// declarations returns all function declarations provided by the library.
declarations() map[string][]cel.FunctionOpt
}
// KnownLibraries returns all libraries used in Kubernetes.
func KnownLibraries() []Library {
return []Library{
authzLib,
authzSelectorsLib,
listsLib,
regexLib,
urlsLib,
quantityLib,
ipLib,
cidrsLib,
formatLib,
semverLib,
jsonPatchLib,
}
}
func isRegisteredType(typeName string) bool {
for _, lib := range KnownLibraries() {
for _, rt := range lib.Types() {
if rt.TypeName() == typeName {
return true
}
}
}
return false
}

View File

@ -96,7 +96,15 @@ var listsLib = &lists{}
type lists struct{}
func (*lists) LibraryName() string {
return "k8s.lists"
return "kubernetes.lists"
}
func (*lists) Types() []*cel.Type {
return []*cel.Type{}
}
func (*lists) declarations() map[string][]cel.FunctionOpt {
return listsLibraryDecls
}
var paramA = cel.TypeParamType("A")

View File

@ -143,7 +143,15 @@ var quantityLib = &quantity{}
type quantity struct{}
func (*quantity) LibraryName() string {
return "k8s.quantity"
return "kubernetes.quantity"
}
func (*quantity) Types() []*cel.Type {
return []*cel.Type{apiservercel.QuantityType}
}
func (*quantity) declarations() map[string][]cel.FunctionOpt {
return quantityLibraryDecls
}
var quantityLibraryDecls = map[string][]cel.FunctionOpt{

View File

@ -52,7 +52,15 @@ var regexLib = &regex{}
type regex struct{}
func (*regex) LibraryName() string {
return "k8s.regex"
return "kubernetes.regex"
}
func (*regex) Types() []*cel.Type {
return []*cel.Type{}
}
func (*regex) declarations() map[string][]cel.FunctionOpt {
return regexLibraryDecls
}
var regexLibraryDecls = map[string][]cel.FunctionOpt{

247
vendor/k8s.io/apiserver/pkg/cel/library/semverlib.go generated vendored Normal file
View File

@ -0,0 +1,247 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package library
import (
"github.com/blang/semver/v4"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
apiservercel "k8s.io/apiserver/pkg/cel"
)
// Semver provides a CEL function library extension for [semver.Version].
//
// semver
//
// Converts a string to a semantic version or results in an error if the string is not a valid semantic version. Refer
// to semver.org documentation for information on accepted patterns.
//
// semver(<string>) <Semver>
//
// Examples:
//
// semver('1.0.0') // returns a Semver
// semver('0.1.0-alpha.1') // returns a Semver
// semver('200K') // error
// semver('Three') // error
// semver('Mi') // error
//
// isSemver
//
// Returns true if a string is a valid Semver. isSemver returns true if and
// only if semver does not result in error.
//
// isSemver( <string>) <bool>
//
// Examples:
//
// isSemver('1.0.0') // returns true
// isSemver('v1.0') // returns true (tolerant parsing)
// isSemver('hello') // returns false
//
// Conversion to Scalars:
//
// - major/minor/patch: return the major version number as int64.
//
// <Semver>.major() <int>
//
// Examples:
//
// semver("1.2.3").major() // returns 1
//
// Comparisons
//
// - isGreaterThan: Returns true if and only if the receiver is greater than the operand
//
// - isLessThan: Returns true if and only if the receiver is less than the operand
//
// - compareTo: Compares receiver to operand and returns 0 if they are equal, 1 if the receiver is greater, or -1 if the receiver is less than the operand
//
//
// <Semver>.isLessThan(<semver>) <bool>
// <Semver>.isGreaterThan(<semver>) <bool>
// <Semver>.compareTo(<semver>) <int>
//
// Examples:
//
// semver("1.2.3").compareTo(semver("1.2.3")) // returns 0
// semver("1.2.3").compareTo(semver("2.0.0")) // returns -1
// semver("1.2.3").compareTo(semver("0.1.2")) // returns 1
func SemverLib() cel.EnvOption {
return cel.Lib(semverLib)
}
var semverLib = &semverLibType{}
type semverLibType struct{}
func (*semverLibType) LibraryName() string {
return "kubernetes.Semver"
}
func (*semverLibType) Types() []*cel.Type {
return []*cel.Type{apiservercel.SemverType}
}
func (*semverLibType) declarations() map[string][]cel.FunctionOpt {
return map[string][]cel.FunctionOpt{
"semver": {
cel.Overload("string_to_semver", []*cel.Type{cel.StringType}, apiservercel.SemverType, cel.UnaryBinding((stringToSemver))),
},
"isSemver": {
cel.Overload("is_semver_string", []*cel.Type{cel.StringType}, cel.BoolType, cel.UnaryBinding(isSemver)),
},
"isGreaterThan": {
cel.MemberOverload("semver_is_greater_than", []*cel.Type{apiservercel.SemverType, apiservercel.SemverType}, cel.BoolType, cel.BinaryBinding(semverIsGreaterThan)),
},
"isLessThan": {
cel.MemberOverload("semver_is_less_than", []*cel.Type{apiservercel.SemverType, apiservercel.SemverType}, cel.BoolType, cel.BinaryBinding(semverIsLessThan)),
},
"compareTo": {
cel.MemberOverload("semver_compare_to", []*cel.Type{apiservercel.SemverType, apiservercel.SemverType}, cel.IntType, cel.BinaryBinding(semverCompareTo)),
},
"major": {
cel.MemberOverload("semver_major", []*cel.Type{apiservercel.SemverType}, cel.IntType, cel.UnaryBinding(semverMajor)),
},
"minor": {
cel.MemberOverload("semver_minor", []*cel.Type{apiservercel.SemverType}, cel.IntType, cel.UnaryBinding(semverMinor)),
},
"patch": {
cel.MemberOverload("semver_patch", []*cel.Type{apiservercel.SemverType}, cel.IntType, cel.UnaryBinding(semverPatch)),
},
}
}
func (s *semverLibType) CompileOptions() []cel.EnvOption {
// Defined in this function to avoid an initialization order problem.
semverLibraryDecls := s.declarations()
options := make([]cel.EnvOption, 0, len(semverLibraryDecls))
for name, overloads := range semverLibraryDecls {
options = append(options, cel.Function(name, overloads...))
}
return options
}
func (*semverLibType) ProgramOptions() []cel.ProgramOption {
return []cel.ProgramOption{}
}
func isSemver(arg ref.Val) ref.Val {
str, ok := arg.Value().(string)
if !ok {
return types.MaybeNoSuchOverloadErr(arg)
}
// Using semver/v4 here is okay because this function isn't
// used to validate the Kubernetes API. In the CEL base library
// we would have to use the regular expression from
// pkg/apis/resource/structured/namedresources/validation/validation.go.
_, err := semver.Parse(str)
if err != nil {
return types.Bool(false)
}
return types.Bool(true)
}
func stringToSemver(arg ref.Val) ref.Val {
str, ok := arg.Value().(string)
if !ok {
return types.MaybeNoSuchOverloadErr(arg)
}
// Using semver/v4 here is okay because this function isn't
// used to validate the Kubernetes API. In the CEL base library
// we would have to use the regular expression from
// pkg/apis/resource/structured/namedresources/validation/validation.go
// first before parsing.
v, err := semver.Parse(str)
if err != nil {
return types.WrapErr(err)
}
return apiservercel.Semver{Version: v}
}
func semverMajor(arg ref.Val) ref.Val {
v, ok := arg.Value().(semver.Version)
if !ok {
return types.MaybeNoSuchOverloadErr(arg)
}
return types.Int(v.Major)
}
func semverMinor(arg ref.Val) ref.Val {
v, ok := arg.Value().(semver.Version)
if !ok {
return types.MaybeNoSuchOverloadErr(arg)
}
return types.Int(v.Minor)
}
func semverPatch(arg ref.Val) ref.Val {
v, ok := arg.Value().(semver.Version)
if !ok {
return types.MaybeNoSuchOverloadErr(arg)
}
return types.Int(v.Patch)
}
func semverIsGreaterThan(arg ref.Val, other ref.Val) ref.Val {
v, ok := arg.Value().(semver.Version)
if !ok {
return types.MaybeNoSuchOverloadErr(arg)
}
v2, ok := other.Value().(semver.Version)
if !ok {
return types.MaybeNoSuchOverloadErr(arg)
}
return types.Bool(v.Compare(v2) == 1)
}
func semverIsLessThan(arg ref.Val, other ref.Val) ref.Val {
v, ok := arg.Value().(semver.Version)
if !ok {
return types.MaybeNoSuchOverloadErr(arg)
}
v2, ok := other.Value().(semver.Version)
if !ok {
return types.MaybeNoSuchOverloadErr(arg)
}
return types.Bool(v.Compare(v2) == -1)
}
func semverCompareTo(arg ref.Val, other ref.Val) ref.Val {
v, ok := arg.Value().(semver.Version)
if !ok {
return types.MaybeNoSuchOverloadErr(arg)
}
v2, ok := other.Value().(semver.Version)
if !ok {
return types.MaybeNoSuchOverloadErr(arg)
}
return types.Int(v.Compare(v2))
}

View File

@ -38,7 +38,7 @@ type testLib struct {
}
func (*testLib) LibraryName() string {
return "k8s.test"
return "kubernetes.test"
}
type TestOption func(*testLib) *testLib

View File

@ -113,7 +113,15 @@ var urlsLib = &urls{}
type urls struct{}
func (*urls) LibraryName() string {
return "k8s.urls"
return "kubernetes.urls"
}
func (*urls) Types() []*cel.Type {
return []*cel.Type{apiservercel.URLType}
}
func (*urls) declarations() map[string][]cel.FunctionOpt {
return urlLibraryDecls
}
var urlLibraryDecls = map[string][]cel.FunctionOpt{

View File

@ -48,5 +48,7 @@ const (
// MinNumberSize is the length of literal 0
MinNumberSize = 1
// MaxFormatSize is the maximum size we allow for format strings
MaxFormatSize = 64
MaxNameFormatRegexSize = 128
)

View File

@ -0,0 +1,249 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dynamic
import (
"errors"
"fmt"
"reflect"
"strings"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"github.com/google/cel-go/common/types/traits"
"google.golang.org/protobuf/types/known/structpb"
)
// ObjectType is the implementation of the Object type for use when compiling
// CEL expressions without schema information about the object.
// This is to provide CEL expressions with access to Object{} types constructors.
type ObjectType struct {
objectType *types.Type
}
func (o *ObjectType) HasTrait(trait int) bool {
return o.objectType.HasTrait(trait)
}
// TypeName returns the name of this ObjectType.
func (o *ObjectType) TypeName() string {
return o.objectType.TypeName()
}
// Val returns an instance given the fields.
func (o *ObjectType) Val(fields map[string]ref.Val) ref.Val {
return NewObjectVal(o.objectType, fields)
}
func (o *ObjectType) Type() *types.Type {
return o.objectType
}
// Field looks up the field by name.
// This is the unstructured version that allows any name as the field name.
// The returned field is of DynType type.
func (o *ObjectType) Field(name string) (*types.FieldType, bool) {
return &types.FieldType{
// for unstructured, we do not check for its type,
// use DynType for all fields.
Type: types.DynType,
IsSet: func(target any) bool {
if m, ok := target.(map[string]any); ok {
_, isSet := m[name]
return isSet
}
return false
},
GetFrom: func(target any) (any, error) {
if m, ok := target.(map[string]any); ok {
return m[name], nil
}
return nil, fmt.Errorf("cannot get field %q", name)
},
}, true
}
func (o *ObjectType) FieldNames() ([]string, bool) {
return nil, true // Field names are not known for dynamic types. All field names are allowed.
}
// NewObjectType creates a ObjectType by the given field name.
func NewObjectType(name string) *ObjectType {
return &ObjectType{
objectType: types.NewObjectType(name),
}
}
// ObjectVal is the CEL Val for an object that is constructed via the Object{} in
// CEL expressions without schema information about the object.
type ObjectVal struct {
objectType *types.Type
fields map[string]ref.Val
}
// NewObjectVal creates an ObjectVal by its ResolvedType and its fields.
func NewObjectVal(objectType *types.Type, fields map[string]ref.Val) *ObjectVal {
return &ObjectVal{
objectType: objectType,
fields: fields,
}
}
var _ ref.Val = (*ObjectVal)(nil)
var _ traits.Zeroer = (*ObjectVal)(nil)
// ConvertToNative converts the object to map[string]any.
// All nested lists are converted into []any native type.
//
// It returns an error if the target type is not map[string]any,
// or any recursive conversion fails.
func (v *ObjectVal) ConvertToNative(typeDesc reflect.Type) (any, error) {
result := make(map[string]any, len(v.fields))
for k, v := range v.fields {
converted, err := convertField(v)
if err != nil {
return nil, fmt.Errorf("fail to convert field %q: %w", k, err)
}
result[k] = converted
}
if typeDesc == reflect.TypeOf(result) {
return result, nil
}
// CEL's builtin data literal values all support conversion to structpb.Value, which
// can then be serialized to JSON. This is convenient for CEL expressions that return
// an arbitrary JSON value, such as our MutatingAdmissionPolicy JSON Patch valueExpression
// field, so we support the conversion here, for Object data literals, as well.
if typeDesc == reflect.TypeOf(&structpb.Value{}) {
return structpb.NewStruct(result)
}
return nil, fmt.Errorf("unable to convert to %v", typeDesc)
}
// ConvertToType supports type conversions between CEL value types supported by the expression language.
func (v *ObjectVal) ConvertToType(typeValue ref.Type) ref.Val {
if v.objectType.TypeName() == typeValue.TypeName() {
return v
}
if typeValue == types.TypeType {
return types.NewTypeTypeWithParam(v.objectType)
}
return types.NewErr("unsupported conversion into %v", typeValue)
}
// Equal returns true if the `other` value has the same type and content as the implementing struct.
func (v *ObjectVal) Equal(other ref.Val) ref.Val {
if rhs, ok := other.(*ObjectVal); ok {
if v.objectType.Equal(rhs.objectType) != types.True {
return types.False
}
return types.Bool(reflect.DeepEqual(v.fields, rhs.fields))
}
return types.False
}
// Type returns the TypeValue of the value.
func (v *ObjectVal) Type() ref.Type {
return types.NewObjectType(v.objectType.TypeName())
}
// Value returns its value as a map[string]any.
func (v *ObjectVal) Value() any {
var result any
var object map[string]any
result, err := v.ConvertToNative(reflect.TypeOf(object))
if err != nil {
return types.WrapErr(err)
}
return result
}
// CheckTypeNamesMatchFieldPathNames transitively checks the CEL object type names of this ObjectVal. Returns all
// found type name mismatch errors.
// Children ObjectVal types under <field> or this ObjectVal
// must have type names of the form "<ObjectVal.TypeName>.<field>", children of that type must have type names of the
// form "<ObjectVal.TypeName>.<field>.<field>" and so on.
// Intermediate maps and lists are unnamed and ignored.
func (v *ObjectVal) CheckTypeNamesMatchFieldPathNames() error {
return errors.Join(typeCheck(v, []string{v.Type().TypeName()})...)
}
func typeCheck(v ref.Val, typeNamePath []string) []error {
var errs []error
if ov, ok := v.(*ObjectVal); ok {
tn := ov.objectType.TypeName()
if strings.Join(typeNamePath, ".") != tn {
errs = append(errs, fmt.Errorf("unexpected type name %q, expected %q, which matches field name path from root Object type", tn, strings.Join(typeNamePath, ".")))
}
for k, f := range ov.fields {
errs = append(errs, typeCheck(f, append(typeNamePath, k))...)
}
}
value := v.Value()
if listOfVal, ok := value.([]ref.Val); ok {
for _, v := range listOfVal {
errs = append(errs, typeCheck(v, typeNamePath)...)
}
}
if mapOfVal, ok := value.(map[ref.Val]ref.Val); ok {
for _, v := range mapOfVal {
errs = append(errs, typeCheck(v, typeNamePath)...)
}
}
return errs
}
// IsZeroValue indicates whether the object is the zero value for the type.
// For the ObjectVal, it is zero value if and only if the fields map is empty.
func (v *ObjectVal) IsZeroValue() bool {
return len(v.fields) == 0
}
// convertField converts a referred ref.Val to its expected type.
// For objects, the expected type is map[string]any
// For lists, the expected type is []any
// For maps, the expected type is map[string]any
// For anything else, it is converted via value.Value()
//
// It will return an error if the request type is a map but the key
// is not a string.
func convertField(value ref.Val) (any, error) {
// special handling for lists, where the elements are converted with Value() instead of ConvertToNative
// to allow them to become native value of any type.
if listOfVal, ok := value.Value().([]ref.Val); ok {
var result []any
for _, v := range listOfVal {
result = append(result, v.Value())
}
return result, nil
}
// unstructured maps, as seen in annotations
// map keys must be strings
if mapOfVal, ok := value.Value().(map[ref.Val]ref.Val); ok {
result := make(map[string]any, len(mapOfVal))
for k, v := range mapOfVal {
stringKey, ok := k.Value().(string)
if !ok {
return nil, fmt.Errorf("map key %q is of type %T, not string", k, k)
}
result[stringKey] = v.Value()
}
return result, nil
}
return value.Value(), nil
}

185
vendor/k8s.io/apiserver/pkg/cel/mutation/jsonpatch.go generated vendored Normal file
View File

@ -0,0 +1,185 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package mutation
import (
"fmt"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"reflect"
)
var jsonPatchType = types.NewObjectType(JSONPatchTypeName)
var (
jsonPatchOp = "op"
jsonPatchPath = "path"
jsonPatchFrom = "from"
jsonPatchValue = "value"
)
// JSONPatchType and JSONPatchVal are defined entirely from scratch here because JSONPatchVal
// has a dynamic 'value' field which can not be defined with an OpenAPI schema,
// preventing us from using DeclType and UnstructuredToVal.
// JSONPatchType provides a CEL type for "JSONPatch" operations.
type JSONPatchType struct{}
func (r *JSONPatchType) HasTrait(trait int) bool {
return jsonPatchType.HasTrait(trait)
}
// TypeName returns the name of this ObjectType.
func (r *JSONPatchType) TypeName() string {
return jsonPatchType.TypeName()
}
// Val returns an instance given the fields.
func (r *JSONPatchType) Val(fields map[string]ref.Val) ref.Val {
result := &JSONPatchVal{}
for name, value := range fields {
switch name {
case jsonPatchOp:
if s, ok := value.Value().(string); ok {
result.Op = s
} else {
return types.NewErr("unexpected type %T for JSONPatchType 'op' field", value.Value())
}
case jsonPatchPath:
if s, ok := value.Value().(string); ok {
result.Path = s
} else {
return types.NewErr("unexpected type %T for JSONPatchType 'path' field", value.Value())
}
case jsonPatchFrom:
if s, ok := value.Value().(string); ok {
result.From = s
} else {
return types.NewErr("unexpected type %T for JSONPatchType 'from' field", value.Value())
}
case jsonPatchValue:
result.Val = value
default:
return types.NewErr("unexpected JSONPatchType field: %s", name)
}
}
return result
}
func (r *JSONPatchType) Type() *types.Type {
return jsonPatchType
}
func (r *JSONPatchType) Field(name string) (*types.FieldType, bool) {
var fieldType *types.Type
switch name {
case jsonPatchOp, jsonPatchFrom, jsonPatchPath:
fieldType = cel.StringType
case jsonPatchValue:
fieldType = types.DynType
}
return &types.FieldType{
Type: fieldType,
}, true
}
func (r *JSONPatchType) FieldNames() ([]string, bool) {
return []string{jsonPatchOp, jsonPatchFrom, jsonPatchPath, jsonPatchValue}, true
}
// JSONPatchVal is the ref.Val for a JSONPatch.
type JSONPatchVal struct {
Op, From, Path string
Val ref.Val
}
func (p *JSONPatchVal) ConvertToNative(typeDesc reflect.Type) (any, error) {
if typeDesc == reflect.TypeOf(&JSONPatchVal{}) {
return p, nil
}
return nil, fmt.Errorf("cannot convert to native type: %v", typeDesc)
}
func (p *JSONPatchVal) ConvertToType(typeValue ref.Type) ref.Val {
if typeValue == jsonPatchType {
return p
} else if typeValue == types.TypeType {
return types.NewTypeTypeWithParam(jsonPatchType)
}
return types.NewErr("unsupported type: %s", typeValue.TypeName())
}
func (p *JSONPatchVal) Equal(other ref.Val) ref.Val {
if o, ok := other.(*JSONPatchVal); ok && p != nil && o != nil {
if p.Op != o.Op || p.From != o.From || p.Path != o.Path {
return types.False
}
if (p.Val == nil) != (o.Val == nil) {
return types.False
}
if p.Val == nil {
return types.True
}
return p.Val.Equal(o.Val)
}
return types.False
}
func (p *JSONPatchVal) Get(index ref.Val) ref.Val {
if name, ok := index.Value().(string); ok {
switch name {
case jsonPatchOp:
return types.String(p.Op)
case jsonPatchPath:
return types.String(p.Path)
case jsonPatchFrom:
return types.String(p.From)
case jsonPatchValue:
return p.Val
default:
}
}
return types.NewErr("unsupported indexer: %s", index)
}
func (p *JSONPatchVal) IsSet(field ref.Val) ref.Val {
if name, ok := field.Value().(string); ok {
switch name {
case jsonPatchOp:
return types.Bool(len(p.Op) > 0)
case jsonPatchPath:
return types.Bool(len(p.Path) > 0)
case jsonPatchFrom:
return types.Bool(len(p.From) > 0)
case jsonPatchValue:
return types.Bool(p.Val != nil)
}
}
return types.NewErr("unsupported field: %s", field)
}
func (p *JSONPatchVal) Type() ref.Type {
return jsonPatchType
}
func (p *JSONPatchVal) Value() any {
return p
}
var _ ref.Val = &JSONPatchVal{}

View File

@ -0,0 +1,47 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package mutation
import (
"strings"
"k8s.io/apiserver/pkg/cel/common"
"k8s.io/apiserver/pkg/cel/mutation/dynamic"
)
// ObjectTypeName is the name of Object types that are used to declare the types of
// Kubernetes objects in CEL dynamically using the naming scheme "Object.<fieldName>...<fieldName>".
// For example "Object.spec.containers" is the type of the spec.containers field of the object in scope.
const ObjectTypeName = "Object"
// JSONPatchTypeName is the name of the JSONPatch type. This type is typically used to create JSON patches
// in CEL expressions.
const JSONPatchTypeName = "JSONPatch"
// DynamicTypeResolver resolves the Object and JSONPatch types when compiling
// CEL expressions without schema information about the object.
type DynamicTypeResolver struct{}
func (r *DynamicTypeResolver) Resolve(name string) (common.ResolvedType, bool) {
if name == JSONPatchTypeName {
return &JSONPatchType{}, true
}
if name == ObjectTypeName || strings.HasPrefix(name, ObjectTypeName+".") {
return dynamic.NewObjectType(name), true
}
return nil, false
}

73
vendor/k8s.io/apiserver/pkg/cel/semver.go generated vendored Normal file
View File

@ -0,0 +1,73 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cel
import (
"fmt"
"reflect"
"github.com/blang/semver/v4"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
)
var (
SemverType = cel.ObjectType("kubernetes.Semver")
)
// Semver provdes a CEL representation of a [semver.Version].
type Semver struct {
semver.Version
}
func (v Semver) ConvertToNative(typeDesc reflect.Type) (interface{}, error) {
if reflect.TypeOf(v.Version).AssignableTo(typeDesc) {
return v.Version, nil
}
if reflect.TypeOf("").AssignableTo(typeDesc) {
return v.Version.String(), nil
}
return nil, fmt.Errorf("type conversion error from 'Semver' to '%v'", typeDesc)
}
func (v Semver) ConvertToType(typeVal ref.Type) ref.Val {
switch typeVal {
case SemverType:
return v
case types.TypeType:
return SemverType
default:
return types.NewErr("type conversion error from '%s' to '%s'", SemverType, typeVal)
}
}
func (v Semver) Equal(other ref.Val) ref.Val {
otherDur, ok := other.(Semver)
if !ok {
return types.MaybeNoSuchOverloadErr(other)
}
return types.Bool(v.Version.EQ(otherDur.Version))
}
func (v Semver) Type() ref.Type {
return SemverType
}
func (v Semver) Value() interface{} {
return v.Version
}

View File

@ -429,7 +429,7 @@ func (rt *DeclTypeProvider) FindStructType(typeName string) (*types.Type, bool)
declType, found := rt.findDeclType(typeName)
if found {
expT := declType.CelType()
return expT, found
return types.NewTypeTypeWithParam(expT), found
}
return rt.typeProvider.FindStructType(typeName)
}

View File

@ -17,6 +17,7 @@ limitations under the License.
package discovery
import (
"context"
"net/http"
"sync"
@ -33,12 +34,21 @@ import (
// GroupManager is an interface that allows dynamic mutation of the existing webservice to handle
// API groups being added or removed.
type GroupManager interface {
GroupLister
AddGroup(apiGroup metav1.APIGroup)
RemoveGroup(groupName string)
ServeHTTP(resp http.ResponseWriter, req *http.Request)
WebService() *restful.WebService
}
// GroupLister knows how to list APIGroups for discovery.
type GroupLister interface {
// Groups returns APIGroups for discovery, filling in ServerAddressByClientCIDRs
// based on data in req.
Groups(ctx context.Context, req *http.Request) ([]metav1.APIGroup, error)
}
// rootAPIsHandler creates a webservice serving api group discovery.
// The list of APIGroups may change while the server is running because additional resources
// are registered or removed. It is not safe to cache the values.
@ -94,24 +104,40 @@ func (s *rootAPIsHandler) RemoveGroup(groupName string) {
}
}
func (s *rootAPIsHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
func (s *rootAPIsHandler) Groups(ctx context.Context, req *http.Request) ([]metav1.APIGroup, error) {
s.lock.RLock()
defer s.lock.RUnlock()
return s.groupsLocked(ctx, req), nil
}
// groupsLocked returns the APIGroupList discovery information for this handler.
// The caller must hold the lock before invoking this method to avoid data races.
func (s *rootAPIsHandler) groupsLocked(ctx context.Context, req *http.Request) []metav1.APIGroup {
clientIP := utilnet.GetClientIP(req)
serverCIDR := s.addresses.ServerAddressByClientCIDRs(clientIP)
orderedGroups := []metav1.APIGroup{}
for _, groupName := range s.apiGroupNames {
orderedGroups = append(orderedGroups, s.apiGroups[groupName])
}
clientIP := utilnet.GetClientIP(req)
serverCIDR := s.addresses.ServerAddressByClientCIDRs(clientIP)
groups := make([]metav1.APIGroup, len(orderedGroups))
for i := range orderedGroups {
groups[i] = orderedGroups[i]
groups[i].ServerAddressByClientCIDRs = serverCIDR
}
responsewriters.WriteObjectNegotiated(s.serializer, negotiation.DefaultEndpointRestrictions, schema.GroupVersion{}, resp, req, http.StatusOK, &metav1.APIGroupList{Groups: groups}, false)
return groups
}
func (s *rootAPIsHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
s.lock.RLock()
defer s.lock.RUnlock()
groupList := metav1.APIGroupList{Groups: s.groupsLocked(req.Context(), req)}
responsewriters.WriteObjectNegotiated(s.serializer, negotiation.DefaultEndpointRestrictions, schema.GroupVersion{}, resp, req, http.StatusOK, &groupList, false)
}
func (s *rootAPIsHandler) restfulHandle(req *restful.Request, resp *restful.Response) {

View File

@ -54,6 +54,7 @@ func withAuthentication(handler http.Handler, auth authenticator.Request, failed
}
standardRequestHeaderConfig := &authenticatorfactory.RequestHeaderConfig{
UsernameHeaders: headerrequest.StaticStringSlice{"X-Remote-User"},
UIDHeaders: headerrequest.StaticStringSlice{"X-Remote-Uid"},
GroupHeaders: headerrequest.StaticStringSlice{"X-Remote-Group"},
ExtraHeaderPrefixes: headerrequest.StaticStringSlice{"X-Remote-Extra-"},
}
@ -90,6 +91,7 @@ func withAuthentication(handler http.Handler, auth authenticator.Request, failed
headerrequest.ClearAuthenticationHeaders(
req.Header,
standardRequestHeaderConfig.UsernameHeaders,
standardRequestHeaderConfig.UIDHeaders,
standardRequestHeaderConfig.GroupHeaders,
standardRequestHeaderConfig.ExtraHeaderPrefixes,
)
@ -99,6 +101,7 @@ func withAuthentication(handler http.Handler, auth authenticator.Request, failed
headerrequest.ClearAuthenticationHeaders(
req.Header,
requestHeaderConfig.UsernameHeaders,
requestHeaderConfig.UIDHeaders,
requestHeaderConfig.GroupHeaders,
requestHeaderConfig.ExtraHeaderPrefixes,
)

View File

@ -28,7 +28,6 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apimachinery/pkg/util/managedfields"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/endpoints/discovery"
@ -71,9 +70,6 @@ type APIGroupVersion struct {
// version (for when the inevitable meta/v2 group emerges).
MetaGroupVersion *schema.GroupVersion
// RootScopedKinds are the root scoped kinds for the primary GroupVersion
RootScopedKinds sets.String
// Serializer is used to determine how to convert responses from API methods into bytes to send over
// the wire.
Serializer runtime.NegotiatedSerializer

View File

@ -55,6 +55,7 @@ func createHandler(r rest.NamedCreater, scope *RequestScope, admit admission.Int
ctx := req.Context()
// For performance tracking purposes.
ctx, span := tracing.Start(ctx, "Create", traceFields(req)...)
req = req.WithContext(ctx)
defer span.End(500 * time.Millisecond)
namespace, name, err := scope.Namer.Name(req)

View File

@ -30,19 +30,27 @@ import (
metainternalversionvalidation "k8s.io/apimachinery/pkg/apis/meta/internalversion/validation"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/validation"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/audit"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/endpoints/handlers/finisher"
requestmetrics "k8s.io/apiserver/pkg/endpoints/handlers/metrics"
"k8s.io/apiserver/pkg/endpoints/handlers/negotiation"
"k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/features"
"k8s.io/apiserver/pkg/registry/rest"
"k8s.io/apiserver/pkg/util/apihelpers"
"k8s.io/apiserver/pkg/util/dryrun"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/component-base/tracing"
"k8s.io/klog/v2"
"k8s.io/utils/ptr"
)
// DeleteResource returns a function that will handle a resource deletion
@ -52,6 +60,7 @@ func DeleteResource(r rest.GracefulDeleter, allowsOptions bool, scope *RequestSc
ctx := req.Context()
// For performance tracking purposes.
ctx, span := tracing.Start(ctx, "Delete", traceFields(req)...)
req = req.WithContext(ctx)
defer span.End(500 * time.Millisecond)
namespace, name, err := scope.Namer.Name(req)
@ -84,7 +93,7 @@ func DeleteResource(r rest.GracefulDeleter, allowsOptions bool, scope *RequestSc
}
span.AddEvent("limitedReadBody succeeded", attribute.Int("len", len(body)))
if len(body) > 0 {
s, err := negotiation.NegotiateInputSerializer(req, false, metainternalversionscheme.Codecs)
s, err := negotiation.NegotiateInputSerializer(req, false, apihelpers.GetMetaInternalVersionCodecs())
if err != nil {
scope.err(err, w, req)
return
@ -92,7 +101,7 @@ func DeleteResource(r rest.GracefulDeleter, allowsOptions bool, scope *RequestSc
// For backwards compatibility, we need to allow existing clients to submit per group DeleteOptions
// It is also allowed to pass a body with meta.k8s.io/v1.DeleteOptions
defaultGVK := scope.MetaGroupVersion.WithKind("DeleteOptions")
obj, gvk, err := metainternalversionscheme.Codecs.DecoderToVersion(s.Serializer, defaultGVK.GroupVersion()).Decode(body, &defaultGVK, options)
obj, gvk, err := apihelpers.GetMetaInternalVersionCodecs().DecoderToVersion(s.Serializer, defaultGVK.GroupVersion()).Decode(body, &defaultGVK, options)
if err != nil {
scope.err(err, w, req)
return
@ -104,7 +113,7 @@ func DeleteResource(r rest.GracefulDeleter, allowsOptions bool, scope *RequestSc
span.AddEvent("Decoded delete options")
objGV := gvk.GroupVersion()
audit.LogRequestObject(req.Context(), obj, objGV, scope.Resource, scope.Subresource, metainternalversionscheme.Codecs)
audit.LogRequestObject(req.Context(), obj, objGV, scope.Resource, scope.Subresource, apihelpers.GetMetaInternalVersionCodecs())
span.AddEvent("Recorded the audit event")
} else {
if err := metainternalversionscheme.ParameterCodec.DecodeParameters(req.URL.Query(), scope.MetaGroupVersion, options); err != nil {
@ -114,6 +123,9 @@ func DeleteResource(r rest.GracefulDeleter, allowsOptions bool, scope *RequestSc
}
}
}
if !utilfeature.DefaultFeatureGate.Enabled(features.AllowUnsafeMalformedObjectDeletion) && options != nil {
options.IgnoreStoreReadErrorWithClusterBreakingPotential = nil
}
if errs := validation.ValidateDeleteOptions(options); len(errs) > 0 {
err := errors.NewInvalid(schema.GroupKind{Group: metav1.GroupName, Kind: "DeleteOptions"}, "", errs)
scope.err(err, w, req)
@ -121,10 +133,36 @@ func DeleteResource(r rest.GracefulDeleter, allowsOptions bool, scope *RequestSc
}
options.TypeMeta.SetGroupVersionKind(metav1.SchemeGroupVersion.WithKind("DeleteOptions"))
span.AddEvent("About to delete object from database")
wasDeleted := true
userInfo, _ := request.UserFrom(ctx)
staticAdmissionAttrs := admission.NewAttributesRecord(nil, nil, scope.Kind, namespace, name, scope.Resource, scope.Subresource, admission.Delete, options, dryrun.IsDryRun(options.DryRun), userInfo)
if utilfeature.DefaultFeatureGate.Enabled(features.AllowUnsafeMalformedObjectDeletion) {
if options != nil && ptr.Deref(options.IgnoreStoreReadErrorWithClusterBreakingPotential, false) {
// let's make sure that the audit will reflect that this delete request
// was tried with ignoreStoreReadErrorWithClusterBreakingPotential enabled
audit.AddAuditAnnotation(ctx, "apiserver.k8s.io/unsafe-delete-ignore-read-error", "")
p, ok := r.(rest.CorruptObjectDeleterProvider)
if !ok || p.GetCorruptObjDeleter() == nil {
// this is a developer error
scope.err(errors.NewInternalError(fmt.Errorf("no unsafe deleter provided, can not honor ignoreStoreReadErrorWithClusterBreakingPotential")), w, req)
return
}
if scope.Authorizer == nil {
scope.err(errors.NewInternalError(fmt.Errorf("no authorizer provided, unable to authorize unsafe delete")), w, req)
return
}
if err := authorizeUnsafeDelete(ctx, staticAdmissionAttrs, scope.Authorizer); err != nil {
scope.err(err, w, req)
return
}
r = p.GetCorruptObjDeleter()
}
}
span.AddEvent("About to delete object from database")
wasDeleted := true
result, err := finisher.FinishRequest(ctx, func() (runtime.Object, error) {
obj, deleted, err := r.Delete(ctx, name, rest.AdmissionToValidateObjectDeleteFunc(admit, staticAdmissionAttrs, scope), options)
wasDeleted = deleted
@ -172,6 +210,7 @@ func DeleteCollection(r rest.CollectionDeleter, checkBody bool, scope *RequestSc
return func(w http.ResponseWriter, req *http.Request) {
ctx := req.Context()
ctx, span := tracing.Start(ctx, "Delete", traceFields(req)...)
req = req.WithContext(ctx)
defer span.End(500 * time.Millisecond)
namespace, err := scope.Namer.Namespace(req)
@ -229,7 +268,7 @@ func DeleteCollection(r rest.CollectionDeleter, checkBody bool, scope *RequestSc
}
span.AddEvent("limitedReadBody succeeded", attribute.Int("len", len(body)))
if len(body) > 0 {
s, err := negotiation.NegotiateInputSerializer(req, false, metainternalversionscheme.Codecs)
s, err := negotiation.NegotiateInputSerializer(req, false, apihelpers.GetMetaInternalVersionCodecs())
if err != nil {
scope.err(err, w, req)
return
@ -237,7 +276,7 @@ func DeleteCollection(r rest.CollectionDeleter, checkBody bool, scope *RequestSc
// For backwards compatibility, we need to allow existing clients to submit per group DeleteOptions
// It is also allowed to pass a body with meta.k8s.io/v1.DeleteOptions
defaultGVK := scope.MetaGroupVersion.WithKind("DeleteOptions")
obj, gvk, err := metainternalversionscheme.Codecs.DecoderToVersion(s.Serializer, defaultGVK.GroupVersion()).Decode(body, &defaultGVK, options)
obj, gvk, err := apihelpers.GetMetaInternalVersionCodecs().DecoderToVersion(s.Serializer, defaultGVK.GroupVersion()).Decode(body, &defaultGVK, options)
if err != nil {
scope.err(err, w, req)
return
@ -248,7 +287,7 @@ func DeleteCollection(r rest.CollectionDeleter, checkBody bool, scope *RequestSc
}
objGV := gvk.GroupVersion()
audit.LogRequestObject(req.Context(), obj, objGV, scope.Resource, scope.Subresource, metainternalversionscheme.Codecs)
audit.LogRequestObject(req.Context(), obj, objGV, scope.Resource, scope.Subresource, apihelpers.GetMetaInternalVersionCodecs())
} else {
if err := metainternalversionscheme.ParameterCodec.DecodeParameters(req.URL.Query(), scope.MetaGroupVersion, options); err != nil {
err = errors.NewBadRequest(err.Error())
@ -257,11 +296,26 @@ func DeleteCollection(r rest.CollectionDeleter, checkBody bool, scope *RequestSc
}
}
}
if !utilfeature.DefaultFeatureGate.Enabled(features.AllowUnsafeMalformedObjectDeletion) && options != nil {
options.IgnoreStoreReadErrorWithClusterBreakingPotential = nil
}
if errs := validation.ValidateDeleteOptions(options); len(errs) > 0 {
err := errors.NewInvalid(schema.GroupKind{Group: metav1.GroupName, Kind: "DeleteOptions"}, "", errs)
scope.err(err, w, req)
return
}
if utilfeature.DefaultFeatureGate.Enabled(features.AllowUnsafeMalformedObjectDeletion) {
if options != nil && ptr.Deref(options.IgnoreStoreReadErrorWithClusterBreakingPotential, false) {
fieldErrList := field.ErrorList{
field.Invalid(field.NewPath("ignoreStoreReadErrorWithClusterBreakingPotential"), true, "is not allowed with DELETECOLLECTION, try again after removing the option"),
}
err := errors.NewInvalid(schema.GroupKind{Group: metav1.GroupName, Kind: "DeleteOptions"}, "", fieldErrList)
scope.err(err, w, req)
return
}
}
options.TypeMeta.SetGroupVersionKind(metav1.SchemeGroupVersion.WithKind("DeleteOptions"))
admit = admission.WithAudit(admit)
@ -292,3 +346,77 @@ func DeleteCollection(r rest.CollectionDeleter, checkBody bool, scope *RequestSc
transformResponseObject(ctx, scope, req, w, http.StatusOK, outputMediaType, result)
}
}
// authorizeUnsafeDelete ensures that the user has permission to do
// 'unsafe-delete-ignore-read-errors' on the resource being deleted when
// ignoreStoreReadErrorWithClusterBreakingPotential is enabled
func authorizeUnsafeDelete(ctx context.Context, attr admission.Attributes, authz authorizer.Authorizer) (err error) {
if attr.GetOperation() != admission.Delete || attr.GetOperationOptions() == nil {
return nil
}
options, ok := attr.GetOperationOptions().(*metav1.DeleteOptions)
if !ok {
return errors.NewInternalError(fmt.Errorf("expected an option of type: %T, but got: %T", &metav1.DeleteOptions{}, attr.GetOperationOptions()))
}
if !ptr.Deref(options.IgnoreStoreReadErrorWithClusterBreakingPotential, false) {
return nil
}
requestInfo, found := request.RequestInfoFrom(ctx)
if !found {
return admission.NewForbidden(attr, fmt.Errorf("no RequestInfo found in the context"))
}
if !requestInfo.IsResourceRequest || len(attr.GetSubresource()) > 0 {
return admission.NewForbidden(attr, fmt.Errorf("ignoreStoreReadErrorWithClusterBreakingPotential delete option is not allowed on a subresource or non-resource request"))
}
// if we are here, IgnoreStoreReadErrorWithClusterBreakingPotential
// is set to true in the delete options, the user must have permission
// to do 'unsafe-delete-ignore-read-errors' on the given resource.
record := authorizer.AttributesRecord{
User: attr.GetUserInfo(),
Verb: "unsafe-delete-ignore-read-errors",
Namespace: attr.GetNamespace(),
Name: attr.GetName(),
APIGroup: attr.GetResource().Group,
APIVersion: attr.GetResource().Version,
Resource: attr.GetResource().Resource,
ResourceRequest: true,
}
// TODO: can't use ResourceAttributesFrom from k8s.io/kubernetes/pkg/registry/authorization/util
// due to prevent staging --> k8s.io/kubernetes dep issue
if utilfeature.DefaultFeatureGate.Enabled(features.AuthorizeWithSelectors) {
if len(requestInfo.FieldSelector) > 0 {
fieldSelector, err := fields.ParseSelector(requestInfo.FieldSelector)
if err != nil {
record.FieldSelectorRequirements, record.FieldSelectorParsingErr = nil, err
} else {
if requirements := fieldSelector.Requirements(); len(requirements) > 0 {
record.FieldSelectorRequirements, record.FieldSelectorParsingErr = fieldSelector.Requirements(), nil
}
}
}
if len(requestInfo.LabelSelector) > 0 {
labelSelector, err := labels.Parse(requestInfo.LabelSelector)
if err != nil {
record.LabelSelectorRequirements, record.LabelSelectorParsingErr = nil, err
} else {
if requirements, _ /*selectable*/ := labelSelector.Requirements(); len(requirements) > 0 {
record.LabelSelectorRequirements, record.LabelSelectorParsingErr = requirements, nil
}
}
}
}
decision, reason, err := authz.Authorize(ctx, record)
if err != nil {
err = fmt.Errorf("error while checking permission for %q, %w", record.Verb, err)
klog.FromContext(ctx).V(1).Error(err, "failed to authorize")
return admission.NewForbidden(attr, err)
}
if decision == authorizer.DecisionAllow {
return nil
}
return admission.NewForbidden(attr, fmt.Errorf("not permitted to do %q, reason: %s", record.Verb, reason))
}

View File

@ -45,6 +45,7 @@ import (
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/component-base/tracing"
"k8s.io/klog/v2"
"k8s.io/utils/ptr"
)
// getterFunc performs a get request with the given context and object name. The request
@ -57,6 +58,7 @@ func getResourceHandler(scope *RequestScope, getter getterFunc) http.HandlerFunc
return func(w http.ResponseWriter, req *http.Request) {
ctx := req.Context()
ctx, span := tracing.Start(ctx, "Get", traceFields(req)...)
req = req.WithContext(ctx)
defer span.End(500 * time.Millisecond)
namespace, name, err := scope.Namer.Name(req)
@ -171,6 +173,7 @@ func ListResource(r rest.Lister, rw rest.Watcher, scope *RequestScope, forceWatc
ctx := req.Context()
// For performance tracking purposes.
ctx, span := tracing.Start(ctx, "List", traceFields(req)...)
req = req.WithContext(ctx)
namespace, err := scope.Namer.Namespace(req)
if err != nil {
@ -185,15 +188,8 @@ func ListResource(r rest.Lister, rw rest.Watcher, scope *RequestScope, forceWatc
if err != nil {
hasName = false
}
ctx = request.WithNamespace(ctx, namespace)
outputMediaType, _, err := negotiation.NegotiateOutputMediaType(req, scope.Serializer, scope)
if err != nil {
scope.err(err, w, req)
return
}
opts := metainternalversion.ListOptions{}
if err := metainternalversionscheme.ParameterCodec.DecodeParameters(req.URL.Query(), scope.MetaGroupVersion, &opts); err != nil {
err = errors.NewBadRequest(err.Error())
@ -208,6 +204,17 @@ func ListResource(r rest.Lister, rw rest.Watcher, scope *RequestScope, forceWatc
return
}
var restrictions negotiation.EndpointRestrictions
restrictions = scope
if isListWatchRequest(opts) {
restrictions = &watchListEndpointRestrictions{scope}
}
outputMediaType, _, err := negotiation.NegotiateOutputMediaType(req, scope.Serializer, restrictions)
if err != nil {
scope.err(err, w, req)
return
}
// transform fields
// TODO: DecodeParametersInto should do this.
if opts.FieldSelector != nil {
@ -258,6 +265,16 @@ func ListResource(r rest.Lister, rw rest.Watcher, scope *RequestScope, forceWatc
if timeout == 0 && minRequestTimeout > 0 {
timeout = time.Duration(float64(minRequestTimeout) * (rand.Float64() + 1.0))
}
var emptyVersionedList runtime.Object
if isListWatchRequest(opts) {
emptyVersionedList, err = scope.Convertor.ConvertToVersion(r.NewList(), scope.Kind.GroupVersion())
if err != nil {
scope.err(errors.NewInternalError(err), w, req)
return
}
}
klog.V(3).InfoS("Starting watch", "path", req.URL.Path, "resourceVersion", opts.ResourceVersion, "labels", opts.LabelSelector, "fields", opts.FieldSelector, "timeout", timeout)
ctx, cancel := context.WithTimeout(ctx, timeout)
defer func() { cancel() }()
@ -266,7 +283,7 @@ func ListResource(r rest.Lister, rw rest.Watcher, scope *RequestScope, forceWatc
scope.err(err, w, req)
return
}
handler, err := serveWatchHandler(watcher, scope, outputMediaType, req, w, timeout, metrics.CleanListScope(ctx, &opts))
handler, err := serveWatchHandler(watcher, scope, outputMediaType, req, w, timeout, metrics.CleanListScope(ctx, &opts), emptyVersionedList)
if err != nil {
scope.err(err, w, req)
return
@ -307,3 +324,18 @@ func ListResource(r rest.Lister, rw rest.Watcher, scope *RequestScope, forceWatc
transformResponseObject(ctx, scope, req, w, http.StatusOK, outputMediaType, result)
}
}
type watchListEndpointRestrictions struct {
negotiation.EndpointRestrictions
}
func (e *watchListEndpointRestrictions) AllowsMediaTypeTransform(mimeType, mimeSubType string, target *schema.GroupVersionKind) bool {
if target != nil && target.Kind == "Table" {
return false
}
return e.EndpointRestrictions.AllowsMediaTypeTransform(mimeType, mimeSubType, target)
}
func isListWatchRequest(opts metainternalversion.ListOptions) bool {
return utilfeature.DefaultFeatureGate.Enabled(features.WatchList) && ptr.Deref(opts.SendInitialEvents, false) && opts.AllowWatchBookmarks
}

View File

@ -26,6 +26,8 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature"
)
// MediaTypesForSerializer returns a list of media and stream media types for the server.
@ -33,6 +35,10 @@ func MediaTypesForSerializer(ns runtime.NegotiatedSerializer) (mediaTypes, strea
for _, info := range ns.SupportedMediaTypes() {
mediaTypes = append(mediaTypes, info.MediaType)
if info.StreamSerializer != nil {
if utilfeature.DefaultFeatureGate.Enabled(features.CBORServingAndStorage) && info.MediaType == runtime.ContentTypeCBOR {
streamMediaTypes = append(streamMediaTypes, runtime.ContentTypeCBORSequence)
continue
}
// stream=watch is the existing mime-type parameter for watch
streamMediaTypes = append(streamMediaTypes, info.MediaType+";stream=watch")
}

View File

@ -35,9 +35,11 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/validation"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
cbor "k8s.io/apimachinery/pkg/runtime/serializer/cbor/direct"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/managedfields"
"k8s.io/apimachinery/pkg/util/mergepatch"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/strategicpatch"
"k8s.io/apimachinery/pkg/util/validation/field"
@ -50,8 +52,10 @@ import (
requestmetrics "k8s.io/apiserver/pkg/endpoints/handlers/metrics"
"k8s.io/apiserver/pkg/endpoints/handlers/negotiation"
"k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/features"
"k8s.io/apiserver/pkg/registry/rest"
"k8s.io/apiserver/pkg/util/dryrun"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/component-base/tracing"
)
@ -66,6 +70,7 @@ func PatchResource(r rest.Patcher, scope *RequestScope, admit admission.Interfac
ctx := req.Context()
// For performance tracking purposes.
ctx, span := tracing.Start(ctx, "Patch", traceFields(req)...)
req = req.WithContext(ctx)
defer span.End(500 * time.Millisecond)
// Do this first, otherwise name extraction can fail for unrecognized content types
@ -128,10 +133,25 @@ func PatchResource(r rest.Patcher, scope *RequestScope, admit admission.Interfac
audit.LogRequestPatch(req.Context(), patchBytes)
span.AddEvent("Recorded the audit event")
baseContentType := runtime.ContentTypeJSON
if patchType == types.ApplyPatchType {
var baseContentType string
switch patchType {
case types.ApplyYAMLPatchType:
baseContentType = runtime.ContentTypeYAML
case types.ApplyCBORPatchType:
if !utilfeature.DefaultFeatureGate.Enabled(features.CBORServingAndStorage) {
// This request should have already been rejected by the
// Content-Type allowlist check. Return 500 because assumptions are
// already broken and the feature is not GA.
utilruntime.HandleErrorWithContext(req.Context(), nil, "The patch content-type allowlist check should have made this unreachable.")
scope.err(errors.NewInternalError(errors.NewInternalError(fmt.Errorf("unexpected patch type: %v", patchType))), w, req)
return
}
baseContentType = runtime.ContentTypeCBOR
default:
baseContentType = runtime.ContentTypeJSON
}
s, ok := runtime.SerializerInfoForMediaType(scope.Serializer.SupportedMediaTypes(), baseContentType)
if !ok {
scope.err(fmt.Errorf("no serializer defined for %v", baseContentType), w, req)
@ -451,6 +471,20 @@ func (p *smpPatcher) createNewObject(_ context.Context) (runtime.Object, error)
return nil, errors.NewNotFound(p.resource.GroupResource(), p.name)
}
func newApplyPatcher(p *patcher, fieldManager *managedfields.FieldManager, unmarshalFn, unmarshalStrictFn func([]byte, interface{}) error) *applyPatcher {
return &applyPatcher{
fieldManager: fieldManager,
patch: p.patchBytes,
options: p.options,
creater: p.creater,
kind: p.kind,
userAgent: p.userAgent,
validationDirective: p.validationDirective,
unmarshalFn: unmarshalFn,
unmarshalStrictFn: unmarshalStrictFn,
}
}
type applyPatcher struct {
patch []byte
options *metav1.PatchOptions
@ -459,6 +493,8 @@ type applyPatcher struct {
fieldManager *managedfields.FieldManager
userAgent string
validationDirective string
unmarshalFn func(data []byte, v interface{}) error
unmarshalStrictFn func(data []byte, v interface{}) error
}
func (p *applyPatcher) applyPatchToCurrentObject(requestContext context.Context, obj runtime.Object) (runtime.Object, error) {
@ -471,7 +507,7 @@ func (p *applyPatcher) applyPatchToCurrentObject(requestContext context.Context,
}
patchObj := &unstructured.Unstructured{Object: map[string]interface{}{}}
if err := yaml.Unmarshal(p.patch, &patchObj.Object); err != nil {
if err := p.unmarshalFn(p.patch, &patchObj.Object); err != nil {
return nil, errors.NewBadRequest(fmt.Sprintf("error decoding YAML: %v", err))
}
@ -483,7 +519,7 @@ func (p *applyPatcher) applyPatchToCurrentObject(requestContext context.Context,
// TODO: spawn something to track deciding whether a fieldValidation=Strict
// fatal error should return before an error from the apply operation
if p.validationDirective == metav1.FieldValidationStrict || p.validationDirective == metav1.FieldValidationWarn {
if err := yaml.UnmarshalStrict(p.patch, &map[string]interface{}{}); err != nil {
if err := p.unmarshalStrictFn(p.patch, &map[string]interface{}{}); err != nil {
if p.validationDirective == metav1.FieldValidationStrict {
return nil, errors.NewBadRequest(fmt.Sprintf("error strict decoding YAML: %v", err))
}
@ -633,16 +669,21 @@ func (p *patcher) patchResource(ctx context.Context, scope *RequestScope) (runti
fieldManager: scope.FieldManager,
}
// this case is unreachable if ServerSideApply is not enabled because we will have already rejected the content type
case types.ApplyPatchType:
p.mechanism = &applyPatcher{
fieldManager: scope.FieldManager,
patch: p.patchBytes,
options: p.options,
creater: p.creater,
kind: p.kind,
userAgent: p.userAgent,
validationDirective: p.validationDirective,
case types.ApplyYAMLPatchType:
p.mechanism = newApplyPatcher(p, scope.FieldManager, yaml.Unmarshal, yaml.UnmarshalStrict)
p.forceAllowCreate = true
case types.ApplyCBORPatchType:
if !utilfeature.DefaultFeatureGate.Enabled(features.CBORServingAndStorage) {
utilruntime.HandleErrorWithContext(context.TODO(), nil, "CBOR apply requests should be rejected before reaching this point unless the feature gate is enabled.")
return nil, false, fmt.Errorf("%v: unimplemented patch type", p.patchType)
}
// The strict and non-strict funcs are the same here because any CBOR map with
// duplicate keys is invalid and always rejected outright regardless of strictness
// mode, and unknown field errors can't occur in practice because the type of the
// destination value for unmarshaling an apply configuration is always
// "unstructured".
p.mechanism = newApplyPatcher(p, scope.FieldManager, cbor.Unmarshal, cbor.Unmarshal)
p.forceAllowCreate = true
default:
return nil, false, fmt.Errorf("%v: unimplemented patch type", p.patchType)
@ -669,7 +710,7 @@ func (p *patcher) patchResource(ctx context.Context, scope *RequestScope) (runti
result, err := requestFunc()
// If the object wasn't committed to storage because it's serialized size was too large,
// it is safe to remove managedFields (which can be large) and try again.
if isTooLargeError(err) && p.patchType != types.ApplyPatchType {
if isTooLargeError(err) && p.patchType != types.ApplyYAMLPatchType && p.patchType != types.ApplyCBORPatchType {
if _, accessorErr := meta.Accessor(p.restPatcher.New()); accessorErr == nil {
p.updatedObjectInfo = rest.DefaultUpdatedObjectInfo(nil,
p.applyPatch,

View File

@ -18,6 +18,7 @@ package handlers
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io"
@ -38,8 +39,9 @@ import (
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
"k8s.io/apiserver/pkg/endpoints/metrics"
endpointsrequest "k8s.io/apiserver/pkg/endpoints/request"
klog "k8s.io/klog/v2"
"k8s.io/apiserver/pkg/storage"
"k8s.io/apiserver/pkg/util/apihelpers"
"k8s.io/klog/v2"
)
// watchEmbeddedEncoder performs encoding of the embedded object.
@ -147,6 +149,8 @@ type watchEncoder struct {
encoder runtime.Encoder
framer io.Writer
watchListTransformerFn watchListTransformerFunction
buffer runtime.Splice
eventBuffer runtime.Splice
@ -154,15 +158,16 @@ type watchEncoder struct {
identifiers map[watch.EventType]runtime.Identifier
}
func newWatchEncoder(ctx context.Context, kind schema.GroupVersionKind, embeddedEncoder runtime.Encoder, encoder runtime.Encoder, framer io.Writer) *watchEncoder {
func newWatchEncoder(ctx context.Context, kind schema.GroupVersionKind, embeddedEncoder runtime.Encoder, encoder runtime.Encoder, framer io.Writer, watchListTransformerFn watchListTransformerFunction) *watchEncoder {
return &watchEncoder{
ctx: ctx,
kind: kind,
embeddedEncoder: embeddedEncoder,
encoder: encoder,
framer: framer,
buffer: runtime.NewSpliceBuffer(),
eventBuffer: runtime.NewSpliceBuffer(),
ctx: ctx,
kind: kind,
embeddedEncoder: embeddedEncoder,
encoder: encoder,
framer: framer,
watchListTransformerFn: watchListTransformerFn,
buffer: runtime.NewSpliceBuffer(),
eventBuffer: runtime.NewSpliceBuffer(),
}
}
@ -174,6 +179,12 @@ func (e *watchEncoder) Encode(event watch.Event) error {
encodeFunc := func(obj runtime.Object, w io.Writer) error {
return e.doEncode(obj, event, w)
}
if event.Type == watch.Bookmark {
// Bookmark objects are small, and we don't yet support serialization for them.
// Additionally, we need to additionally transform them to support watch-list feature
event = e.watchListTransformerFn(event)
return encodeFunc(event.Object, e.framer)
}
if co, ok := event.Object.(runtime.CacheableObject); ok {
return co.CacheEncode(e.identifier(event.Type), encodeFunc, e.framer)
}
@ -270,7 +281,7 @@ func doTransformObject(ctx context.Context, obj runtime.Object, opts interface{}
return asTable(ctx, obj, options, scope, target.GroupVersion())
default:
accepted, _ := negotiation.MediaTypesForSerializer(metainternalversionscheme.Codecs)
accepted, _ := negotiation.MediaTypesForSerializer(apihelpers.GetMetaInternalVersionCodecs())
err := negotiation.NewNotAcceptableError(accepted)
return nil, err
}
@ -304,7 +315,7 @@ func targetEncodingForTransform(scope *RequestScope, mediaType negotiation.Media
case target == nil:
case (target.Kind == "PartialObjectMetadata" || target.Kind == "PartialObjectMetadataList" || target.Kind == "Table") &&
(target.GroupVersion() == metav1beta1.SchemeGroupVersion || target.GroupVersion() == metav1.SchemeGroupVersion):
return *target, metainternalversionscheme.Codecs, true
return *target, apihelpers.GetMetaInternalVersionCodecs(), true
}
return scope.Kind, scope.Serializer, false
}
@ -479,3 +490,94 @@ func asPartialObjectMetadataList(result runtime.Object, groupVersion schema.Grou
return nil, newNotAcceptableError(fmt.Sprintf("no PartialObjectMetadataList exists in group version %s", groupVersion))
}
}
// watchListTransformerFunction an optional function
// applied to watchlist bookmark events that transforms
// the embedded object before sending it to a client.
type watchListTransformerFunction func(watch.Event) watch.Event
// watchListTransformer performs transformation of
// a special watchList bookmark event.
//
// The bookmark is annotated with InitialEventsListBlueprintAnnotationKey
// and contains an empty, versioned list that we must encode in the requested format
// (e.g., protobuf, JSON, CBOR) and then store as a base64-encoded string.
type watchListTransformer struct {
initialEventsListBlueprint runtime.Object
targetGVK *schema.GroupVersionKind
negotiatedEncoder runtime.Encoder
buffer runtime.Splice
}
// createWatchListTransformerIfRequested returns a transformer function for watchlist bookmark event.
func newWatchListTransformer(initialEventsListBlueprint runtime.Object, targetGVK *schema.GroupVersionKind, negotiatedEncoder runtime.Encoder) *watchListTransformer {
return &watchListTransformer{
initialEventsListBlueprint: initialEventsListBlueprint,
targetGVK: targetGVK,
negotiatedEncoder: negotiatedEncoder,
buffer: runtime.NewSpliceBuffer(),
}
}
func (e *watchListTransformer) transform(event watch.Event) watch.Event {
if e.initialEventsListBlueprint == nil {
return event
}
hasAnnotation, err := storage.HasInitialEventsEndBookmarkAnnotation(event.Object)
if err != nil {
return newWatchEventErrorFor(err)
}
if !hasAnnotation {
return event
}
if err = e.encodeInitialEventsListBlueprint(event.Object); err != nil {
return newWatchEventErrorFor(err)
}
return event
}
func (e *watchListTransformer) encodeInitialEventsListBlueprint(object runtime.Object) error {
initialEventsListBlueprint, err := e.transformInitialEventsListBlueprint()
if err != nil {
return err
}
defer e.buffer.Reset()
if err = e.negotiatedEncoder.Encode(initialEventsListBlueprint, e.buffer); err != nil {
return err
}
encodedInitialEventsListBlueprint := e.buffer.Bytes()
// the storage layer creates a deep copy of the obj before modifying it.
// since the object has the annotation, we can modify it directly.
objectMeta, err := meta.Accessor(object)
if err != nil {
return err
}
annotations := objectMeta.GetAnnotations()
annotations[metav1.InitialEventsListBlueprintAnnotationKey] = base64.StdEncoding.EncodeToString(encodedInitialEventsListBlueprint)
objectMeta.SetAnnotations(annotations)
return nil
}
func (e *watchListTransformer) transformInitialEventsListBlueprint() (runtime.Object, error) {
if e.targetGVK != nil && e.targetGVK.Kind == "PartialObjectMetadata" {
return asPartialObjectMetadataList(e.initialEventsListBlueprint, e.targetGVK.GroupVersion())
}
return e.initialEventsListBlueprint, nil
}
func newWatchEventErrorFor(err error) watch.Event {
return watch.Event{
Type: watch.Error,
Object: &metav1.Status{
Status: metav1.StatusFailure,
Message: err.Error(),
Reason: metav1.StatusReasonInternalError,
Code: http.StatusInternalServerError,
},
}
}

View File

@ -34,18 +34,24 @@ var sanitizer = strings.NewReplacer(`&`, "&amp;", `<`, "&lt;", `>`, "&gt;")
// Forbidden renders a simple forbidden error
func Forbidden(ctx context.Context, attributes authorizer.Attributes, w http.ResponseWriter, req *http.Request, reason string, s runtime.NegotiatedSerializer) {
msg := sanitizer.Replace(forbiddenMessage(attributes))
w.Header().Set("X-Content-Type-Options", "nosniff")
var errMsg string
if len(reason) == 0 {
errMsg = fmt.Sprintf("%s", msg)
} else {
errMsg = fmt.Sprintf("%s: %s", msg, reason)
}
gv := schema.GroupVersion{Group: attributes.GetAPIGroup(), Version: attributes.GetAPIVersion()}
ErrorNegotiated(ForbiddenStatusError(attributes, reason), s, gv, w, req)
}
func ForbiddenStatusError(attributes authorizer.Attributes, reason string) *apierrors.StatusError {
msg := sanitizer.Replace(forbiddenMessage(attributes))
var errMsg error
if len(reason) == 0 {
errMsg = fmt.Errorf("%s", msg)
} else {
errMsg = fmt.Errorf("%s: %s", msg, reason)
}
gr := schema.GroupResource{Group: attributes.GetAPIGroup(), Resource: attributes.GetResource()}
ErrorNegotiated(apierrors.NewForbidden(gr, attributes.GetName(), fmt.Errorf(errMsg)), s, gv, w, req)
return apierrors.NewForbidden(gr, attributes.GetName(), errMsg)
}
func forbiddenMessage(attributes authorizer.Attributes) string {

View File

@ -98,6 +98,7 @@ func SerializeObject(mediaType string, encoder runtime.Encoder, hw http.Response
attribute.String("protocol", req.Proto),
attribute.String("mediaType", mediaType),
attribute.String("encoder", string(encoder.Identifier())))
req = req.WithContext(ctx)
defer span.End(5 * time.Second)
w := &deferredResponseWriter{
@ -284,7 +285,12 @@ func WriteObjectNegotiated(s runtime.NegotiatedSerializer, restrictions negotiat
audit.LogResponseObject(req.Context(), object, gv, s)
encoder := s.EncoderForVersion(serializer.Serializer, gv)
var encoder runtime.Encoder
if utilfeature.DefaultFeatureGate.Enabled(features.CBORServingAndStorage) {
encoder = s.EncoderForVersion(runtime.UseNondeterministicEncoding(serializer.Serializer), gv)
} else {
encoder = s.EncoderForVersion(serializer.Serializer, gv)
}
request.TrackSerializeResponseObjectLatency(req.Context(), func() {
if listGVKInContentType {
SerializeObject(generateMediaTypeWithGVK(serializer.MediaType, mediaType.Convert), encoder, w, req, statusCode, object)

View File

@ -39,6 +39,7 @@ import (
"k8s.io/apiserver/pkg/endpoints/handlers/finisher"
requestmetrics "k8s.io/apiserver/pkg/endpoints/handlers/metrics"
"k8s.io/apiserver/pkg/endpoints/handlers/negotiation"
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
"k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/rest"
"k8s.io/apiserver/pkg/util/dryrun"
@ -52,6 +53,7 @@ func UpdateResource(r rest.Updater, scope *RequestScope, admit admission.Interfa
ctx := req.Context()
// For performance tracking purposes.
ctx, span := tracing.Start(ctx, "Update", traceFields(req)...)
req = req.WithContext(ctx)
defer span.End(500 * time.Millisecond)
namespace, name, err := scope.Namer.Name(req)
@ -275,13 +277,7 @@ func withAuthorization(validate rest.ValidateObjectFunc, a authorizer.Authorizer
}
// The user is not authorized to perform this action, so we need to build the error response
gr := schema.GroupResource{
Group: attributes.GetAPIGroup(),
Resource: attributes.GetResource(),
}
name := attributes.GetName()
err := fmt.Errorf("%v", authorizerReason)
return errors.NewForbidden(gr, name, err)
return responsewriters.ForbiddenStatusError(attributes, authorizerReason)
}
}

View File

@ -64,7 +64,7 @@ func (w *realTimeoutFactory) TimeoutCh() (<-chan time.Time, func() bool) {
// serveWatchHandler returns a handle to serve a watch response.
// TODO: the functionality in this method and in WatchServer.Serve is not cleanly decoupled.
func serveWatchHandler(watcher watch.Interface, scope *RequestScope, mediaTypeOptions negotiation.MediaTypeOptions, req *http.Request, w http.ResponseWriter, timeout time.Duration, metricsScope string) (http.Handler, error) {
func serveWatchHandler(watcher watch.Interface, scope *RequestScope, mediaTypeOptions negotiation.MediaTypeOptions, req *http.Request, w http.ResponseWriter, timeout time.Duration, metricsScope string, initialEventsListBlueprint runtime.Object) (http.Handler, error) {
options, err := optionsForTransform(mediaTypeOptions, req)
if err != nil {
return nil, err
@ -76,40 +76,62 @@ func serveWatchHandler(watcher watch.Interface, scope *RequestScope, mediaTypeOp
return nil, err
}
framer := serializer.StreamSerializer.Framer
streamSerializer := serializer.StreamSerializer.Serializer
encoder := scope.Serializer.EncoderForVersion(streamSerializer, scope.Kind.GroupVersion())
var encoder runtime.Encoder
if utilfeature.DefaultFeatureGate.Enabled(features.CBORServingAndStorage) {
encoder = scope.Serializer.EncoderForVersion(runtime.UseNondeterministicEncoding(serializer.StreamSerializer.Serializer), scope.Kind.GroupVersion())
} else {
encoder = scope.Serializer.EncoderForVersion(serializer.StreamSerializer.Serializer, scope.Kind.GroupVersion())
}
useTextFraming := serializer.EncodesAsText
if framer == nil {
return nil, fmt.Errorf("no framer defined for %q available for embedded encoding", serializer.MediaType)
}
// TODO: next step, get back mediaTypeOptions from negotiate and return the exact value here
mediaType := serializer.MediaType
if mediaType != runtime.ContentTypeJSON {
switch mediaType {
case runtime.ContentTypeJSON:
// as-is
case runtime.ContentTypeCBOR:
// If a client indicated it accepts application/cbor (exactly one data item) on a
// watch request, set the conformant application/cbor-seq media type the watch
// response. RFC 9110 allows an origin server to deviate from the indicated
// preference rather than send a 406 (Not Acceptable) response (see
// https://www.rfc-editor.org/rfc/rfc9110.html#section-12.1-5).
mediaType = runtime.ContentTypeCBORSequence
default:
mediaType += ";stream=watch"
}
ctx := req.Context()
// locate the appropriate embedded encoder based on the transform
var embeddedEncoder runtime.Encoder
var negotiatedEncoder runtime.Encoder
contentKind, contentSerializer, transform := targetEncodingForTransform(scope, mediaTypeOptions, req)
if transform {
info, ok := runtime.SerializerInfoForMediaType(contentSerializer.SupportedMediaTypes(), serializer.MediaType)
if !ok {
return nil, fmt.Errorf("no encoder for %q exists in the requested target %#v", serializer.MediaType, contentSerializer)
}
embeddedEncoder = contentSerializer.EncoderForVersion(info.Serializer, contentKind.GroupVersion())
if utilfeature.DefaultFeatureGate.Enabled(features.CBORServingAndStorage) {
negotiatedEncoder = contentSerializer.EncoderForVersion(runtime.UseNondeterministicEncoding(info.Serializer), contentKind.GroupVersion())
} else {
negotiatedEncoder = contentSerializer.EncoderForVersion(info.Serializer, contentKind.GroupVersion())
}
} else {
embeddedEncoder = scope.Serializer.EncoderForVersion(serializer.Serializer, contentKind.GroupVersion())
if utilfeature.DefaultFeatureGate.Enabled(features.CBORServingAndStorage) {
negotiatedEncoder = scope.Serializer.EncoderForVersion(runtime.UseNondeterministicEncoding(serializer.Serializer), contentKind.GroupVersion())
} else {
negotiatedEncoder = scope.Serializer.EncoderForVersion(serializer.Serializer, contentKind.GroupVersion())
}
}
var memoryAllocator runtime.MemoryAllocator
if encoderWithAllocator, supportsAllocator := embeddedEncoder.(runtime.EncoderWithAllocator); supportsAllocator {
if encoderWithAllocator, supportsAllocator := negotiatedEncoder.(runtime.EncoderWithAllocator); supportsAllocator {
// don't put the allocator inside the embeddedEncodeFn as that would allocate memory on every call.
// instead, we allocate the buffer for the entire watch session and release it when we close the connection.
memoryAllocator = runtime.AllocatorPool.Get().(*runtime.Allocator)
embeddedEncoder = runtime.NewEncoderWithAllocator(encoderWithAllocator, memoryAllocator)
negotiatedEncoder = runtime.NewEncoderWithAllocator(encoderWithAllocator, memoryAllocator)
}
var tableOptions *metav1.TableOptions
if options != nil {
@ -119,7 +141,7 @@ func serveWatchHandler(watcher watch.Interface, scope *RequestScope, mediaTypeOp
return nil, fmt.Errorf("unexpected options type: %T", options)
}
}
embeddedEncoder = newWatchEmbeddedEncoder(ctx, embeddedEncoder, mediaTypeOptions.Convert, tableOptions, scope)
embeddedEncoder := newWatchEmbeddedEncoder(ctx, negotiatedEncoder, mediaTypeOptions.Convert, tableOptions, scope)
if encoderWithAllocator, supportsAllocator := encoder.(runtime.EncoderWithAllocator); supportsAllocator {
if memoryAllocator == nil {
@ -145,6 +167,8 @@ func serveWatchHandler(watcher watch.Interface, scope *RequestScope, mediaTypeOp
Encoder: encoder,
EmbeddedEncoder: embeddedEncoder,
watchListTransformerFn: newWatchListTransformer(initialEventsListBlueprint, mediaTypeOptions.Convert, negotiatedEncoder).transform,
MemoryAllocator: memoryAllocator,
TimeoutFactory: &realTimeoutFactory{timeout},
ServerShuttingDownCh: serverShuttingDownCh,
@ -174,6 +198,10 @@ type WatchServer struct {
Encoder runtime.Encoder
// used to encode the nested object in the watch stream
EmbeddedEncoder runtime.Encoder
// watchListTransformerFn a function applied
// to watchlist bookmark events that transforms
// the embedded object before sending it to a client.
watchListTransformerFn watchListTransformerFunction
MemoryAllocator runtime.MemoryAllocator
TimeoutFactory TimeoutFactory
@ -219,7 +247,7 @@ func (s *WatchServer) HandleHTTP(w http.ResponseWriter, req *http.Request) {
flusher.Flush()
kind := s.Scope.Kind
watchEncoder := newWatchEncoder(req.Context(), kind, s.EmbeddedEncoder, s.Encoder, framer)
watchEncoder := newWatchEncoder(req.Context(), kind, s.EmbeddedEncoder, s.Encoder, framer, s.watchListTransformerFn)
ch := s.Watching.ResultChan()
done := req.Context().Done()
@ -288,7 +316,7 @@ func (s *WatchServer) HandleWS(ws *websocket.Conn) {
framer := newWebsocketFramer(ws, s.UseTextFraming)
kind := s.Scope.Kind
watchEncoder := newWatchEncoder(context.TODO(), kind, s.EmbeddedEncoder, s.Encoder, framer)
watchEncoder := newWatchEncoder(context.TODO(), kind, s.EmbeddedEncoder, s.Encoder, framer, s.watchListTransformerFn)
ch := s.Watching.ResultChan()
for {

View File

@ -685,9 +685,27 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
reqScope.MetaGroupVersion = *a.group.MetaGroupVersion
}
var resetFields map[fieldpath.APIVersion]*fieldpath.Set
if resetFieldsStrategy, isResetFieldsStrategy := storage.(rest.ResetFieldsStrategy); isResetFieldsStrategy {
resetFields = resetFieldsStrategy.GetResetFields()
// Strategies may ignore changes to some fields by resetting the field values.
//
// For instance, spec resource strategies should reset the status, and status subresource
// strategies should reset the spec.
//
// Strategies that reset fields must report to the field manager which fields are
// reset by implementing either the ResetFieldsStrategy or the ResetFieldsFilterStrategy
// interface.
//
// For subresources that provide write access to only specific nested fields
// fieldpath.NewPatternFilter can help create a filter to reset all other fields.
var resetFieldsFilter map[fieldpath.APIVersion]fieldpath.Filter
resetFieldsStrategy, isResetFieldsStrategy := storage.(rest.ResetFieldsStrategy)
if isResetFieldsStrategy {
resetFieldsFilter = fieldpath.NewExcludeFilterSetMap(resetFieldsStrategy.GetResetFields())
}
if resetFieldsStrategy, isResetFieldsFilterStrategy := storage.(rest.ResetFieldsFilterStrategy); isResetFieldsFilterStrategy {
if isResetFieldsStrategy {
return nil, nil, fmt.Errorf("may not implement both ResetFieldsStrategy and ResetFieldsFilterStrategy")
}
resetFieldsFilter = resetFieldsStrategy.GetResetFieldsFilter()
}
reqScope.FieldManager, err = managedfields.NewDefaultFieldManager(
@ -698,7 +716,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
fqKindToRegister,
reqScope.HubGroupVersion,
subresource,
resetFields,
resetFieldsFilter,
)
if err != nil {
return nil, nil, fmt.Errorf("failed to create field manager: %v", err)
@ -875,7 +893,10 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
string(types.JSONPatchType),
string(types.MergePatchType),
string(types.StrategicMergePatchType),
string(types.ApplyPatchType),
string(types.ApplyYAMLPatchType),
}
if utilfeature.DefaultFeatureGate.Enabled(features.CBORServingAndStorage) {
supportedTypes = append(supportedTypes, string(types.ApplyCBORPatchType))
}
handler := metrics.InstrumentRouteFunc(action.Verb, group, version, resource, subresource, requestScope, metrics.APIServerComponent, deprecated, removedRelease, restfulPatchResource(patcher, reqScope, admit, supportedTypes))
handler = utilwarning.AddWarningsHandler(handler, warnings)
@ -1195,6 +1216,8 @@ func typeToJSON(typeName string) string {
return "string"
case "v1.IncludeObjectPolicy", "*v1.IncludeObjectPolicy":
return "string"
case "*string":
return "string"
// TODO: Fix these when go-restful supports a way to specify an array query param:
// https://github.com/emicklei/go-restful/issues/225

View File

@ -416,6 +416,33 @@ func Reset() {
}
}
// ResetLabelAllowLists resets the label allow lists for all metrics.
// NOTE: This is only used for testing.
func ResetLabelAllowLists() {
for _, metric := range metrics {
if counterVec, ok := metric.(*compbasemetrics.CounterVec); ok {
counterVec.ResetLabelAllowLists()
continue
}
if gaugeVec, ok := metric.(*compbasemetrics.GaugeVec); ok {
gaugeVec.ResetLabelAllowLists()
continue
}
if histogramVec, ok := metric.(*compbasemetrics.HistogramVec); ok {
histogramVec.ResetLabelAllowLists()
continue
}
if summaryVec, ok := metric.(*compbasemetrics.SummaryVec); ok {
summaryVec.ResetLabelAllowLists()
continue
}
if timingHistogramVec, ok := metric.(*compbasemetrics.TimingHistogramVec); ok {
timingHistogramVec.ResetLabelAllowLists()
continue
}
}
}
// UpdateInflightRequestMetrics reports concurrency metrics classified by
// mutating vs Readonly.
func UpdateInflightRequestMetrics(phase string, nonmutating, mutating int) {

View File

@ -18,6 +18,7 @@ package features
import (
"k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/version"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/component-base/featuregate"
)
@ -26,7 +27,6 @@ const (
// Every feature gate should add method here following this template:
//
// // owner: @username
// // alpha: v1.4
// MyFeature featuregate.Feature = "MyFeature"
//
// Feature gates should be listed in alphabetical, case-sensitive
@ -35,8 +35,6 @@ const (
// across the file.
// owner: @ivelichkovich, @tallclair
// alpha: v1.27
// beta: v1.28
// stable: v1.30
// kep: https://kep.k8s.io/3716
//
@ -44,8 +42,6 @@ const (
AdmissionWebhookMatchConditions featuregate.Feature = "AdmissionWebhookMatchConditions"
// owner: @jefftree @alexzielenski
// alpha: v1.26
// beta: v1.27
// stable: v1.30
//
// Enables an single HTTP endpoint /discovery/<version> which supports native HTTP
@ -54,14 +50,20 @@ const (
// owner: @vinayakankugoyal
// kep: https://kep.k8s.io/4633
// alpha: v1.31
//
// Allows us to enable anonymous auth for only certain apiserver endpoints.
AnonymousAuthConfigurableEndpoints featuregate.Feature = "AnonymousAuthConfigurableEndpoints"
// owner: @stlaz @tkashem @dgrisonnet
// kep: https://kep.k8s.io/3926
//
// Enables the cluster admin to identify resources that fail to
// decrypt or fail to be decoded into an object, and introduces
// a new delete option to allow deletion of such corrupt
// resources using the Kubernetes API only.
AllowUnsafeMalformedObjectDeletion featuregate.Feature = "AllowUnsafeMalformedObjectDeletion"
// owner: @smarterclayton
// alpha: v1.8
// beta: v1.9
// stable: 1.29
//
// Allow API clients to retrieve resource lists in chunks rather than
@ -69,63 +71,53 @@ const (
APIListChunking featuregate.Feature = "APIListChunking"
// owner: @ilackams
// alpha: v1.7
// beta: v1.16
//
// Enables compression of REST responses (GET and LIST only)
APIResponseCompression featuregate.Feature = "APIResponseCompression"
// owner: @roycaihw
// alpha: v1.20
//
// Assigns each kube-apiserver an ID in a cluster.
APIServerIdentity featuregate.Feature = "APIServerIdentity"
// owner: @dashpole
// alpha: v1.22
// beta: v1.27
//
// Add support for distributed tracing in the API Server
APIServerTracing featuregate.Feature = "APIServerTracing"
// owner: @linxiulei
// beta: v1.30
//
// Enables serving watch requests in separate goroutines.
APIServingWithRoutine featuregate.Feature = "APIServingWithRoutine"
// owner: @deads2k
// kep: https://kep.k8s.io/4601
// alpha: v1.31
//
// Allows authorization to use field and label selectors.
AuthorizeWithSelectors featuregate.Feature = "AuthorizeWithSelectors"
// owner: @benluddy
// kep: https://kep.k8s.io/4222
//
// Enables CBOR as a supported encoding for requests and responses, and as the
// preferred storage encoding for custom resources.
CBORServingAndStorage featuregate.Feature = "CBORServingAndStorage"
// owner: @serathius
//
// Replaces watch cache hashmap implementation with a btree based one, bringing performance improvements.
BtreeWatchCache featuregate.Feature = "BtreeWatchCache"
// owner: @serathius
// beta: v1.31
// Enables concurrent watch object decoding to avoid starving watch cache when conversion webhook is installed.
ConcurrentWatchObjectDecode featuregate.Feature = "ConcurrentWatchObjectDecode"
// owner: @cici37 @jpbetz
// kep: http://kep.k8s.io/3488
// alpha: v1.26
// beta: v1.28
// stable: v1.30
//
// Note: the feature gate can be removed in 1.32
// Enables expression validation in Admission Control
ValidatingAdmissionPolicy featuregate.Feature = "ValidatingAdmissionPolicy"
// owner: @jefftree
// kep: https://kep.k8s.io/4355
// alpha: v1.31
//
// Enables coordinated leader election in the API server
CoordinatedLeaderElection featuregate.Feature = "CoordinatedLeaderElection"
// alpha: v1.20
// beta: v1.21
// GA: v1.24
//
// Allows for updating watchcache resource version with progress notify events.
EfficientWatchResumption featuregate.Feature = "EfficientWatchResumption"
@ -137,80 +129,46 @@ const (
// Enables KMS v1 API for encryption at rest.
KMSv1 featuregate.Feature = "KMSv1"
// owner: @aramase
// kep: https://kep.k8s.io/3299
// alpha: v1.25
// beta: v1.27
// stable: v1.29
//
// Enables KMS v2 API for encryption at rest.
KMSv2 featuregate.Feature = "KMSv2"
// owner: @enj
// kep: https://kep.k8s.io/3299
// beta: v1.28
// stable: v1.29
//
// Enables the use of derived encryption keys with KMS v2.
KMSv2KDF featuregate.Feature = "KMSv2KDF"
// owner: @alexzielenski, @cici37, @jiahuif
// owner: @alexzielenski, @cici37, @jiahuif, @jpbetz
// kep: https://kep.k8s.io/3962
// alpha: v1.30
//
// Enables the MutatingAdmissionPolicy in Admission Chain
MutatingAdmissionPolicy featuregate.Feature = "MutatingAdmissionPolicy"
// owner: @jiahuif
// kep: https://kep.k8s.io/2887
// alpha: v1.23
// beta: v1.24
//
// Enables populating "enum" field of OpenAPI schemas
// in the spec returned from kube-apiserver.
OpenAPIEnums featuregate.Feature = "OpenAPIEnums"
// owner: @caesarxuchao
// alpha: v1.15
// beta: v1.16
// stable: 1.29
//
// Allow apiservers to show a count of remaining items in the response
// to a chunking list request.
RemainingItemCount featuregate.Feature = "RemainingItemCount"
// owner: @stlaz
//
// Enable kube-apiserver to accept UIDs via request header authentication.
// This will also make the kube-apiserver's API aggregator add UIDs via standard
// headers when forwarding requests to the servers serving the aggregated API.
RemoteRequestHeaderUID featuregate.Feature = "RemoteRequestHeaderUID"
// owner: @wojtek-t
// beta: v1.31
//
// Enables resilient watchcache initialization to avoid controlplane
// overload.
ResilientWatchCacheInitialization featuregate.Feature = "ResilientWatchCacheInitialization"
// owner: @serathius
// beta: v1.30
//
// Allow watch cache to create a watch on a dedicated RPC.
// This prevents watch cache from being starved by other watches.
SeparateCacheWatchRPC featuregate.Feature = "SeparateCacheWatchRPC"
// owner: @apelisse, @lavalamp
// alpha: v1.14
// beta: v1.16
// stable: v1.22
//
// Server-side apply. Merging happens on the server.
ServerSideApply featuregate.Feature = "ServerSideApply"
// owner: @kevindelgado
// kep: https://kep.k8s.io/2885
// alpha: v1.23
// beta: v1.24
//
// Enables server-side field validation.
ServerSideFieldValidation featuregate.Feature = "ServerSideFieldValidation"
// owner: @enj
// beta: v1.29
//
// Enables http2 DOS mitigations for unauthenticated clients.
//
@ -228,13 +186,11 @@ const (
UnauthenticatedHTTP2DOSMitigation featuregate.Feature = "UnauthenticatedHTTP2DOSMitigation"
// owner: @jpbetz
// alpha: v1.30
// Resource create requests using generateName are retried automatically by the apiserver
// if the generated name conflicts with an existing resource name, up to a maximum number of 7 retries.
RetryGenerateName featuregate.Feature = "RetryGenerateName"
// owner: @cici37
// alpha: v1.30
//
// StrictCostEnforcementForVAP is used to apply strict CEL cost validation for ValidatingAdmissionPolicy.
// It will be set to off by default for certain time of period to prevent the impact on the existing users.
@ -243,7 +199,6 @@ const (
StrictCostEnforcementForVAP featuregate.Feature = "StrictCostEnforcementForVAP"
// owner: @cici37
// alpha: v1.30
//
// StrictCostEnforcementForWebhooks is used to apply strict CEL cost validation for matchConditions in Webhooks.
// It will be set to off by default for certain time of period to prevent the impact on the existing users.
@ -252,14 +207,11 @@ const (
StrictCostEnforcementForWebhooks featuregate.Feature = "StrictCostEnforcementForWebhooks"
// owner: @caesarxuchao @roycaihw
// alpha: v1.20
//
// Enable the storage version API.
StorageVersionAPI featuregate.Feature = "StorageVersionAPI"
// owner: @caesarxuchao
// alpha: v1.14
// beta: v1.15
//
// Allow apiservers to expose the storage version hash in the discovery
// document.
@ -267,69 +219,41 @@ const (
// owner: @aramase, @enj, @nabokihms
// kep: https://kep.k8s.io/3331
// alpha: v1.29
// beta: v1.30
//
// Enables Structured Authentication Configuration
StructuredAuthenticationConfiguration featuregate.Feature = "StructuredAuthenticationConfiguration"
// owner: @palnabarun
// kep: https://kep.k8s.io/3221
// alpha: v1.29
// beta: v1.30
//
// Enables Structured Authorization Configuration
StructuredAuthorizationConfiguration featuregate.Feature = "StructuredAuthorizationConfiguration"
// owner: @wojtek-t
// alpha: v1.15
// beta: v1.16
// GA: v1.17
//
// Enables support for watch bookmark events.
WatchBookmark featuregate.Feature = "WatchBookmark"
// owner: @wojtek-t
// beta: v1.31
//
// Enables post-start-hook for storage readiness
WatchCacheInitializationPostStartHook featuregate.Feature = "WatchCacheInitializationPostStartHook"
// owner: @serathius
// beta: 1.30
// Enables watches without resourceVersion to be served from storage.
// Used to prevent https://github.com/kubernetes/kubernetes/issues/123072 until etcd fixes the issue.
WatchFromStorageWithoutResourceVersion featuregate.Feature = "WatchFromStorageWithoutResourceVersion"
// owner: @vinaykul
// kep: http://kep.k8s.io/1287
// alpha: v1.27
//
// Enables In-Place Pod Vertical Scaling
InPlacePodVerticalScaling featuregate.Feature = "InPlacePodVerticalScaling"
// owner: @p0lyn0mial
// alpha: v1.27
//
// Allow the API server to stream individual items instead of chunking
WatchList featuregate.Feature = "WatchList"
// owner: @serathius
// kep: http://kep.k8s.io/2340
// alpha: v1.28
// beta: v1.31
//
// Allow the API server to serve consistent lists from cache
ConsistentListFromCache featuregate.Feature = "ConsistentListFromCache"
// owner: @tkashem
// beta: v1.29
// GA: v1.30
//
// Allow Priority & Fairness in the API server to use a zero value for
// the 'nominalConcurrencyShares' field of the 'limited' section of a
// priority level.
ZeroLimitedNominalConcurrencyShares featuregate.Feature = "ZeroLimitedNominalConcurrencyShares"
)
func init() {
@ -340,89 +264,181 @@ func init() {
// defaultVersionedKubernetesFeatureGates consists of all known Kubernetes-specific feature keys with VersionedSpecs.
// To add a new feature, define a key for it above and add it here. The features will be
// available throughout Kubernetes binaries.
//
// Entries are alphabetized and separated from each other with blank lines to avoid sweeping gofmt changes
// when adding or removing one entry.
var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate.VersionedSpecs{
// Example:
// EmulationVersion: {
// {Version: version.MustParse("1.30"), Default: false, PreRelease: featuregate.Alpha},
// },
AdmissionWebhookMatchConditions: {
{Version: version.MustParse("1.27"), Default: false, PreRelease: featuregate.Alpha},
{Version: version.MustParse("1.28"), Default: true, PreRelease: featuregate.Beta},
{Version: version.MustParse("1.30"), Default: true, PreRelease: featuregate.GA, LockToDefault: true},
},
AggregatedDiscoveryEndpoint: {
{Version: version.MustParse("1.26"), Default: false, PreRelease: featuregate.Alpha},
{Version: version.MustParse("1.27"), Default: true, PreRelease: featuregate.Beta},
{Version: version.MustParse("1.30"), Default: true, PreRelease: featuregate.GA, LockToDefault: true},
},
AllowUnsafeMalformedObjectDeletion: {
{Version: version.MustParse("1.32"), Default: false, PreRelease: featuregate.Alpha},
},
AnonymousAuthConfigurableEndpoints: {
{Version: version.MustParse("1.31"), Default: false, PreRelease: featuregate.Alpha},
{Version: version.MustParse("1.32"), Default: true, PreRelease: featuregate.Beta},
},
APIListChunking: {
{Version: version.MustParse("1.8"), Default: false, PreRelease: featuregate.Alpha},
{Version: version.MustParse("1.9"), Default: true, PreRelease: featuregate.Beta},
{Version: version.MustParse("1.29"), Default: true, PreRelease: featuregate.GA, LockToDefault: true},
},
APIResponseCompression: {
{Version: version.MustParse("1.8"), Default: false, PreRelease: featuregate.Alpha},
{Version: version.MustParse("1.16"), Default: true, PreRelease: featuregate.Beta},
},
APIServerIdentity: {
{Version: version.MustParse("1.20"), Default: false, PreRelease: featuregate.Alpha},
{Version: version.MustParse("1.26"), Default: true, PreRelease: featuregate.Beta},
},
APIServerTracing: {
{Version: version.MustParse("1.22"), Default: false, PreRelease: featuregate.Alpha},
{Version: version.MustParse("1.27"), Default: true, PreRelease: featuregate.Beta},
},
APIServingWithRoutine: {
{Version: version.MustParse("1.30"), Default: false, PreRelease: featuregate.Alpha},
},
BtreeWatchCache: {
{Version: version.MustParse("1.32"), Default: true, PreRelease: featuregate.Beta},
},
AuthorizeWithSelectors: {
{Version: version.MustParse("1.31"), Default: false, PreRelease: featuregate.Alpha},
{Version: version.MustParse("1.32"), Default: true, PreRelease: featuregate.Beta},
},
CBORServingAndStorage: {
{Version: version.MustParse("1.32"), Default: false, PreRelease: featuregate.Alpha},
},
ConcurrentWatchObjectDecode: {
{Version: version.MustParse("1.31"), Default: false, PreRelease: featuregate.Beta},
},
ConsistentListFromCache: {
{Version: version.MustParse("1.28"), Default: false, PreRelease: featuregate.Alpha},
{Version: version.MustParse("1.31"), Default: true, PreRelease: featuregate.Beta},
},
CoordinatedLeaderElection: {
{Version: version.MustParse("1.31"), Default: false, PreRelease: featuregate.Alpha},
},
EfficientWatchResumption: {
{Version: version.MustParse("1.20"), Default: false, PreRelease: featuregate.Alpha},
{Version: version.MustParse("1.21"), Default: true, PreRelease: featuregate.Beta},
{Version: version.MustParse("1.24"), Default: true, PreRelease: featuregate.GA, LockToDefault: true},
},
KMSv1: {
{Version: version.MustParse("1.28"), Default: true, PreRelease: featuregate.Deprecated},
{Version: version.MustParse("1.29"), Default: false, PreRelease: featuregate.Deprecated},
},
MutatingAdmissionPolicy: {
{Version: version.MustParse("1.32"), Default: false, PreRelease: featuregate.Alpha},
},
OpenAPIEnums: {
{Version: version.MustParse("1.23"), Default: false, PreRelease: featuregate.Alpha},
{Version: version.MustParse("1.24"), Default: true, PreRelease: featuregate.Beta},
},
RemainingItemCount: {
{Version: version.MustParse("1.15"), Default: false, PreRelease: featuregate.Alpha},
{Version: version.MustParse("1.16"), Default: true, PreRelease: featuregate.Beta},
{Version: version.MustParse("1.29"), Default: true, PreRelease: featuregate.GA, LockToDefault: true},
},
RemoteRequestHeaderUID: {
{Version: version.MustParse("1.32"), Default: false, PreRelease: featuregate.Alpha},
},
ResilientWatchCacheInitialization: {
{Version: version.MustParse("1.31"), Default: true, PreRelease: featuregate.Beta},
},
RetryGenerateName: {
{Version: version.MustParse("1.30"), Default: false, PreRelease: featuregate.Alpha},
{Version: version.MustParse("1.31"), Default: true, PreRelease: featuregate.Beta},
{Version: version.MustParse("1.32"), Default: true, LockToDefault: true, PreRelease: featuregate.GA},
},
SeparateCacheWatchRPC: {
{Version: version.MustParse("1.28"), Default: true, PreRelease: featuregate.Beta},
},
StorageVersionAPI: {
{Version: version.MustParse("1.20"), Default: false, PreRelease: featuregate.Alpha},
},
StorageVersionHash: {
{Version: version.MustParse("1.14"), Default: false, PreRelease: featuregate.Alpha},
{Version: version.MustParse("1.15"), Default: true, PreRelease: featuregate.Beta},
},
StrictCostEnforcementForVAP: {
{Version: version.MustParse("1.30"), Default: false, PreRelease: featuregate.Beta},
{Version: version.MustParse("1.32"), Default: true, PreRelease: featuregate.GA, LockToDefault: true},
},
StrictCostEnforcementForWebhooks: {
{Version: version.MustParse("1.30"), Default: false, PreRelease: featuregate.Beta},
{Version: version.MustParse("1.32"), Default: true, PreRelease: featuregate.GA, LockToDefault: true},
},
StructuredAuthenticationConfiguration: {
{Version: version.MustParse("1.29"), Default: false, PreRelease: featuregate.Alpha},
{Version: version.MustParse("1.30"), Default: true, PreRelease: featuregate.Beta},
},
StructuredAuthorizationConfiguration: {
{Version: version.MustParse("1.29"), Default: false, PreRelease: featuregate.Alpha},
{Version: version.MustParse("1.30"), Default: true, PreRelease: featuregate.Beta},
{Version: version.MustParse("1.32"), Default: true, PreRelease: featuregate.GA, LockToDefault: true},
},
UnauthenticatedHTTP2DOSMitigation: {
{Version: version.MustParse("1.25"), Default: false, PreRelease: featuregate.Beta},
{Version: version.MustParse("1.29"), Default: true, PreRelease: featuregate.Beta},
},
WatchBookmark: {
{Version: version.MustParse("1.15"), Default: false, PreRelease: featuregate.Alpha},
{Version: version.MustParse("1.16"), Default: true, PreRelease: featuregate.Beta},
{Version: version.MustParse("1.17"), Default: true, PreRelease: featuregate.GA, LockToDefault: true},
},
WatchCacheInitializationPostStartHook: {
{Version: version.MustParse("1.31"), Default: false, PreRelease: featuregate.Beta},
},
WatchFromStorageWithoutResourceVersion: {
{Version: version.MustParse("1.27"), Default: false, PreRelease: featuregate.Beta},
},
WatchList: {
{Version: version.MustParse("1.27"), Default: false, PreRelease: featuregate.Alpha},
{Version: version.MustParse("1.32"), Default: true, PreRelease: featuregate.Beta},
},
}
// defaultKubernetesFeatureGates consists of all known Kubernetes-specific feature keys.
// To add a new feature, define a key for it above and add it here. The features will be
// available throughout Kubernetes binaries.
var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{
AnonymousAuthConfigurableEndpoints: {Default: false, PreRelease: featuregate.Alpha},
AggregatedDiscoveryEndpoint: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.33
AdmissionWebhookMatchConditions: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.33
APIListChunking: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.32
APIResponseCompression: {Default: true, PreRelease: featuregate.Beta},
APIServerIdentity: {Default: true, PreRelease: featuregate.Beta},
APIServerTracing: {Default: true, PreRelease: featuregate.Beta},
APIServingWithRoutine: {Default: false, PreRelease: featuregate.Alpha},
AuthorizeWithSelectors: {Default: false, PreRelease: featuregate.Alpha},
ConcurrentWatchObjectDecode: {Default: false, PreRelease: featuregate.Beta},
ValidatingAdmissionPolicy: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.32
CoordinatedLeaderElection: {Default: false, PreRelease: featuregate.Alpha},
EfficientWatchResumption: {Default: true, PreRelease: featuregate.GA, LockToDefault: true},
KMSv1: {Default: false, PreRelease: featuregate.Deprecated},
KMSv2: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.31
KMSv2KDF: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.31
OpenAPIEnums: {Default: true, PreRelease: featuregate.Beta},
RemainingItemCount: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.32
ResilientWatchCacheInitialization: {Default: true, PreRelease: featuregate.Beta},
RetryGenerateName: {Default: true, PreRelease: featuregate.Beta},
SeparateCacheWatchRPC: {Default: true, PreRelease: featuregate.Beta},
ServerSideApply: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.29
ServerSideFieldValidation: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.29
StorageVersionAPI: {Default: false, PreRelease: featuregate.Alpha},
StorageVersionHash: {Default: true, PreRelease: featuregate.Beta},
StrictCostEnforcementForVAP: {Default: false, PreRelease: featuregate.Beta},
StrictCostEnforcementForWebhooks: {Default: false, PreRelease: featuregate.Beta},
StructuredAuthenticationConfiguration: {Default: true, PreRelease: featuregate.Beta},
StructuredAuthorizationConfiguration: {Default: true, PreRelease: featuregate.Beta},
UnauthenticatedHTTP2DOSMitigation: {Default: true, PreRelease: featuregate.Beta},
WatchBookmark: {Default: true, PreRelease: featuregate.GA, LockToDefault: true},
WatchCacheInitializationPostStartHook: {Default: false, PreRelease: featuregate.Beta},
WatchFromStorageWithoutResourceVersion: {Default: false, PreRelease: featuregate.Beta},
InPlacePodVerticalScaling: {Default: false, PreRelease: featuregate.Alpha},
WatchList: {Default: false, PreRelease: featuregate.Alpha},
ConsistentListFromCache: {Default: true, PreRelease: featuregate.Beta},
ZeroLimitedNominalConcurrencyShares: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.32
}
// defaultKubernetesFeatureGates consists of legacy unversioned Kubernetes-specific feature keys.
// Please do not add to this struct and use defaultVersionedKubernetesFeatureGates instead.
var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{}

View File

@ -13,6 +13,7 @@ reviewers:
- saad-ali
- janetkuo
- pwittrock
- ncdc
- dims
- enj
emeritus_reviewers:
- ncdc

View File

@ -0,0 +1,122 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package registry
import (
"context"
"errors"
"strings"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/validation/field"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/rest"
"k8s.io/apiserver/pkg/storage"
storeerr "k8s.io/apiserver/pkg/storage/errors"
"k8s.io/klog/v2"
"k8s.io/utils/ptr"
)
// the corrupt object deleter has the same interface as rest.GracefulDeleter
var _ rest.GracefulDeleter = &corruptObjectDeleter{}
// NewCorruptObjectDeleter returns a deleter that can perform unsafe deletion
// of corrupt objects, it makes an attempt to perform a normal deletion flow
// first, and if the normal deletion flow fails with a corrupt object error
// then it performs the unsafe delete of the object.
//
// NOTE: it skips precondition checks, finalizer constraints, and any
// post deletion hook defined in 'AfterDelete' of the registry.
//
// WARNING: This may break the cluster if the resource being deleted has dependencies.
func NewCorruptObjectDeleter(store *Store) rest.GracefulDeleter {
return &corruptObjectDeleter{store: store}
}
// corruptObjectDeleter implements unsafe object deletion flow
type corruptObjectDeleter struct {
store *Store
}
// Delete performs an unsafe deletion of the given resource from the storage.
//
// NOTE: This function should NEVER be used for any normal deletion
// flow, it is exclusively used when the user enables
// 'IgnoreStoreReadErrorWithClusterBreakingPotential' in the delete options.
func (d *corruptObjectDeleter) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, opts *metav1.DeleteOptions) (runtime.Object, bool, error) {
if opts == nil || !ptr.Deref[bool](opts.IgnoreStoreReadErrorWithClusterBreakingPotential, false) {
// this is a developer error, we should never be here, since the unsafe
// deleter is wired in the rest layer only when the option is enabled
return nil, false, apierrors.NewInternalError(errors.New("initialization error, expected normal deletion flow to be used"))
}
key, err := d.store.KeyFunc(ctx, name)
if err != nil {
return nil, false, err
}
obj := d.store.NewFunc()
qualifiedResource := d.store.qualifiedResourceFromContext(ctx)
// use the storage implementation directly, bypass the dryRun layer
storageBackend := d.store.Storage.Storage
// we leave ResourceVersion as empty in the GetOptions so the
// object is retrieved from the underlying storage directly
err = storageBackend.Get(ctx, key, storage.GetOptions{}, obj)
if err == nil || !storage.IsCorruptObject(err) {
// TODO: The Invalid error should have a field for Resource.
// After that field is added, we should fill the Resource and
// leave the Kind field empty. See the discussion in #18526.
qualifiedKind := schema.GroupKind{Group: qualifiedResource.Group, Kind: qualifiedResource.Resource}
fieldErrList := field.ErrorList{
field.Invalid(field.NewPath("ignoreStoreReadErrorWithClusterBreakingPotential"), true, "is exclusively used to delete corrupt object(s), try again by removing this option"),
}
return nil, false, apierrors.NewInvalid(qualifiedKind, name, fieldErrList)
}
// try normal deletion anyway, it is expected to fail
obj, deleted, err := d.store.Delete(ctx, name, deleteValidation, opts)
if err == nil {
return obj, deleted, err
}
// TODO: unfortunately we can't do storage.IsCorruptObject(err),
// conversion to API error drops the inner error chain
if !strings.Contains(err.Error(), "corrupt object") {
return obj, deleted, err
}
// TODO: at this instant, some actor may have a) managed to recreate this
// object by doing a delete+create, or b) the underlying error has resolved
// since the last time we checked, and the object is readable now.
klog.FromContext(ctx).V(1).Info("Going to perform unsafe object deletion", "object", klog.KRef(genericapirequest.NamespaceValue(ctx), name))
out := d.store.NewFunc()
storageOpts := storage.DeleteOptions{IgnoreStoreReadError: true}
// dropping preconditions, and keeping the admission
if err := storageBackend.Delete(ctx, key, out, nil, storage.ValidateObjectFunc(deleteValidation), nil, storageOpts); err != nil {
if storage.IsNotFound(err) {
// the DELETE succeeded, but we don't have the object since it's
// not retrievable from the storage, so we send a nil object
return nil, false, nil
}
return nil, false, storeerr.InterpretDeleteError(err, qualifiedResource, name)
}
// the DELETE succeeded, but we don't have the object sine it's
// not retrievable from the storage, so we send a nil objct
return nil, true, nil
}

View File

@ -46,7 +46,7 @@ func (s *DryRunnableStorage) Create(ctx context.Context, key string, obj, out ru
return s.Storage.Create(ctx, key, obj, out, ttl)
}
func (s *DryRunnableStorage) Delete(ctx context.Context, key string, out runtime.Object, preconditions *storage.Preconditions, deleteValidation storage.ValidateObjectFunc, dryRun bool, cachedExistingObject runtime.Object) error {
func (s *DryRunnableStorage) Delete(ctx context.Context, key string, out runtime.Object, preconditions *storage.Preconditions, deleteValidation storage.ValidateObjectFunc, dryRun bool, cachedExistingObject runtime.Object, opts storage.DeleteOptions) error {
if dryRun {
if err := s.Storage.Get(ctx, key, storage.GetOptions{}, out); err != nil {
return err
@ -56,7 +56,7 @@ func (s *DryRunnableStorage) Delete(ctx context.Context, key string, out runtime
}
return deleteValidation(ctx, out)
}
return s.Storage.Delete(ctx, key, out, preconditions, deleteValidation, cachedExistingObject)
return s.Storage.Delete(ctx, key, out, preconditions, deleteValidation, cachedExistingObject, opts)
}
func (s *DryRunnableStorage) Watch(ctx context.Context, key string, opts storage.ListOptions) (watch.Interface, error) {

Some files were not shown because too many files have changed in this diff Show More