rebase: update all k8s packages to 0.27.2

Signed-off-by: Niels de Vos <ndevos@ibm.com>
This commit is contained in:
Niels de Vos
2023-06-01 18:58:10 +02:00
committed by mergify[bot]
parent 07b05616a0
commit 2551a0b05f
618 changed files with 42944 additions and 16168 deletions

View File

@ -109,3 +109,15 @@ func (m *ValidatingAdmissionPolicyMetrics) ObserveRejection(ctx context.Context,
m.policyCheck.WithContext(ctx).WithLabelValues(policy, binding, "deny", state).Inc()
m.policyLatency.WithContext(ctx).WithLabelValues(policy, binding, "deny", state).Observe(elapsed.Seconds())
}
// ObserveAudit observes a policy validation audit annotation was published for a validation failure.
func (m *ValidatingAdmissionPolicyMetrics) ObserveAudit(ctx context.Context, elapsed time.Duration, policy, binding, state string) {
m.policyCheck.WithContext(ctx).WithLabelValues(policy, binding, "audit", state).Inc()
m.policyLatency.WithContext(ctx).WithLabelValues(policy, binding, "audit", state).Observe(elapsed.Seconds())
}
// ObserveWarn observes a policy validation warning was published for a validation failure.
func (m *ValidatingAdmissionPolicyMetrics) ObserveWarn(ctx context.Context, elapsed time.Duration, policy, binding, state string) {
m.policyCheck.WithContext(ctx).WithLabelValues(policy, binding, "warn", state).Inc()
m.policyLatency.WithContext(ctx).WithLabelValues(policy, binding, "warn", state).Observe(elapsed.Seconds())
}

View File

@ -19,7 +19,6 @@ package configuration
import (
"fmt"
"sort"
"sync/atomic"
"k8s.io/api/admissionregistration/v1"
"k8s.io/apimachinery/pkg/labels"
@ -29,18 +28,14 @@ import (
"k8s.io/client-go/informers"
admissionregistrationlisters "k8s.io/client-go/listers/admissionregistration/v1"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/tools/cache/synctrack"
)
// mutatingWebhookConfigurationManager collects the mutating webhook objects so that they can be called.
type mutatingWebhookConfigurationManager struct {
configuration *atomic.Value
lister admissionregistrationlisters.MutatingWebhookConfigurationLister
hasSynced func() bool
// initialConfigurationSynced tracks if
// the existing webhook configs have been synced (honored) by the
// manager at startup-- the informer has synced and either has no items
// or has finished executing updateConfiguration() once.
initialConfigurationSynced *atomic.Bool
lister admissionregistrationlisters.MutatingWebhookConfigurationLister
hasSynced func() bool
lazy synctrack.Lazy[[]webhook.WebhookAccessor]
}
var _ generic.Source = &mutatingWebhookConfigurationManager{}
@ -48,62 +43,39 @@ var _ generic.Source = &mutatingWebhookConfigurationManager{}
func NewMutatingWebhookConfigurationManager(f informers.SharedInformerFactory) generic.Source {
informer := f.Admissionregistration().V1().MutatingWebhookConfigurations()
manager := &mutatingWebhookConfigurationManager{
configuration: &atomic.Value{},
lister: informer.Lister(),
hasSynced: informer.Informer().HasSynced,
initialConfigurationSynced: &atomic.Bool{},
lister: informer.Lister(),
}
manager.lazy.Evaluate = manager.getConfiguration
// Start with an empty list
manager.configuration.Store([]webhook.WebhookAccessor{})
manager.initialConfigurationSynced.Store(false)
// On any change, rebuild the config
informer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(_ interface{}) { manager.updateConfiguration() },
UpdateFunc: func(_, _ interface{}) { manager.updateConfiguration() },
DeleteFunc: func(_ interface{}) { manager.updateConfiguration() },
handle, _ := informer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(_ interface{}) { manager.lazy.Notify() },
UpdateFunc: func(_, _ interface{}) { manager.lazy.Notify() },
DeleteFunc: func(_ interface{}) { manager.lazy.Notify() },
})
manager.hasSynced = handle.HasSynced
return manager
}
// Webhooks returns the merged MutatingWebhookConfiguration.
func (m *mutatingWebhookConfigurationManager) Webhooks() []webhook.WebhookAccessor {
return m.configuration.Load().([]webhook.WebhookAccessor)
out, err := m.lazy.Get()
if err != nil {
utilruntime.HandleError(fmt.Errorf("error getting webhook configuration: %v", err))
}
return out
}
// HasSynced returns true when the manager is synced with existing webhookconfig
// objects at startup-- which means the informer is synced and either has no items
// or updateConfiguration() has completed.
func (m *mutatingWebhookConfigurationManager) HasSynced() bool {
if !m.hasSynced() {
return false
}
if m.initialConfigurationSynced.Load() {
// the informer has synced and configuration has been updated
return true
}
if configurations, err := m.lister.List(labels.Everything()); err == nil && len(configurations) == 0 {
// the empty list we initially stored is valid to use.
// Setting initialConfigurationSynced to true, so subsequent checks
// would be able to take the fast path on the atomic boolean in a
// cluster without any admission webhooks configured.
m.initialConfigurationSynced.Store(true)
// the informer has synced and we don't have any items
return true
}
return false
}
// HasSynced returns true if the initial set of mutating webhook configurations
// has been loaded.
func (m *mutatingWebhookConfigurationManager) HasSynced() bool { return m.hasSynced() }
func (m *mutatingWebhookConfigurationManager) updateConfiguration() {
func (m *mutatingWebhookConfigurationManager) getConfiguration() ([]webhook.WebhookAccessor, error) {
configurations, err := m.lister.List(labels.Everything())
if err != nil {
utilruntime.HandleError(fmt.Errorf("error updating configuration: %v", err))
return
return []webhook.WebhookAccessor{}, err
}
m.configuration.Store(mergeMutatingWebhookConfigurations(configurations))
m.initialConfigurationSynced.Store(true)
return mergeMutatingWebhookConfigurations(configurations), nil
}
func mergeMutatingWebhookConfigurations(configurations []*v1.MutatingWebhookConfiguration) []webhook.WebhookAccessor {

View File

@ -19,7 +19,6 @@ package configuration
import (
"fmt"
"sort"
"sync/atomic"
"k8s.io/api/admissionregistration/v1"
"k8s.io/apimachinery/pkg/labels"
@ -29,18 +28,14 @@ import (
"k8s.io/client-go/informers"
admissionregistrationlisters "k8s.io/client-go/listers/admissionregistration/v1"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/tools/cache/synctrack"
)
// validatingWebhookConfigurationManager collects the validating webhook objects so that they can be called.
type validatingWebhookConfigurationManager struct {
configuration *atomic.Value
lister admissionregistrationlisters.ValidatingWebhookConfigurationLister
hasSynced func() bool
// initialConfigurationSynced tracks if
// the existing webhook configs have been synced (honored) by the
// manager at startup-- the informer has synced and either has no items
// or has finished executing updateConfiguration() once.
initialConfigurationSynced *atomic.Bool
lister admissionregistrationlisters.ValidatingWebhookConfigurationLister
hasSynced func() bool
lazy synctrack.Lazy[[]webhook.WebhookAccessor]
}
var _ generic.Source = &validatingWebhookConfigurationManager{}
@ -48,63 +43,39 @@ var _ generic.Source = &validatingWebhookConfigurationManager{}
func NewValidatingWebhookConfigurationManager(f informers.SharedInformerFactory) generic.Source {
informer := f.Admissionregistration().V1().ValidatingWebhookConfigurations()
manager := &validatingWebhookConfigurationManager{
configuration: &atomic.Value{},
lister: informer.Lister(),
hasSynced: informer.Informer().HasSynced,
initialConfigurationSynced: &atomic.Bool{},
lister: informer.Lister(),
}
manager.lazy.Evaluate = manager.getConfiguration
// Start with an empty list
manager.configuration.Store([]webhook.WebhookAccessor{})
manager.initialConfigurationSynced.Store(false)
// On any change, rebuild the config
informer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(_ interface{}) { manager.updateConfiguration() },
UpdateFunc: func(_, _ interface{}) { manager.updateConfiguration() },
DeleteFunc: func(_ interface{}) { manager.updateConfiguration() },
handle, _ := informer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(_ interface{}) { manager.lazy.Notify() },
UpdateFunc: func(_, _ interface{}) { manager.lazy.Notify() },
DeleteFunc: func(_ interface{}) { manager.lazy.Notify() },
})
manager.hasSynced = handle.HasSynced
return manager
}
// Webhooks returns the merged ValidatingWebhookConfiguration.
func (v *validatingWebhookConfigurationManager) Webhooks() []webhook.WebhookAccessor {
return v.configuration.Load().([]webhook.WebhookAccessor)
out, err := v.lazy.Get()
if err != nil {
utilruntime.HandleError(fmt.Errorf("error getting webhook configuration: %v", err))
}
return out
}
// HasSynced returns true when the manager is synced with existing webhookconfig
// objects at startup-- which means the informer is synced and either has no items
// or updateConfiguration() has completed.
func (v *validatingWebhookConfigurationManager) HasSynced() bool {
if !v.hasSynced() {
return false
}
if v.initialConfigurationSynced.Load() {
// the informer has synced and configuration has been updated
return true
}
if configurations, err := v.lister.List(labels.Everything()); err == nil && len(configurations) == 0 {
// the empty list we initially stored is valid to use.
// Setting initialConfigurationSynced to true, so subsequent checks
// would be able to take the fast path on the atomic boolean in a
// cluster without any admission webhooks configured.
v.initialConfigurationSynced.Store(true)
// the informer has synced and we don't have any items
return true
}
return false
// HasSynced returns true if the initial set of mutating webhook configurations
// has been loaded.
func (v *validatingWebhookConfigurationManager) HasSynced() bool { return v.hasSynced() }
}
func (v *validatingWebhookConfigurationManager) updateConfiguration() {
func (v *validatingWebhookConfigurationManager) getConfiguration() ([]webhook.WebhookAccessor, error) {
configurations, err := v.lister.List(labels.Everything())
if err != nil {
utilruntime.HandleError(fmt.Errorf("error updating configuration: %v", err))
return
return []webhook.WebhookAccessor{}, err
}
v.configuration.Store(mergeValidatingWebhookConfigurations(configurations))
v.initialConfigurationSynced.Store(true)
return mergeValidatingWebhookConfigurations(configurations), nil
}
func mergeValidatingWebhookConfigurations(configurations []*v1.ValidatingWebhookConfiguration) []webhook.WebhookAccessor {

View File

@ -14,16 +14,40 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package generic
package admission
import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
)
// VersionedAttributes is a wrapper around the original admission attributes, adding versioned
// variants of the object and old object.
type VersionedAttributes struct {
// Attributes holds the original admission attributes
Attributes
// VersionedOldObject holds Attributes.OldObject (if non-nil), converted to VersionedKind.
// It must never be mutated.
VersionedOldObject runtime.Object
// VersionedObject holds Attributes.Object (if non-nil), converted to VersionedKind.
// If mutated, Dirty must be set to true by the mutator.
VersionedObject runtime.Object
// VersionedKind holds the fully qualified kind
VersionedKind schema.GroupVersionKind
// Dirty indicates VersionedObject has been modified since being converted from Attributes.Object
Dirty bool
}
// GetObject overrides the Attributes.GetObject()
func (v *VersionedAttributes) GetObject() runtime.Object {
if v.VersionedObject != nil {
return v.VersionedObject
}
return v.Attributes.GetObject()
}
// ConvertToGVK converts object to the desired gvk.
func ConvertToGVK(obj runtime.Object, gvk schema.GroupVersionKind, o admission.ObjectInterfaces) (runtime.Object, error) {
func ConvertToGVK(obj runtime.Object, gvk schema.GroupVersionKind, o ObjectInterfaces) (runtime.Object, error) {
// Unlike other resources, custom resources do not have internal version, so
// if obj is a custom resource, it should not need conversion.
if obj.GetObjectKind().GroupVersionKind() == gvk {
@ -43,7 +67,7 @@ func ConvertToGVK(obj runtime.Object, gvk schema.GroupVersionKind, o admission.O
}
// NewVersionedAttributes returns versioned attributes with the old and new object (if non-nil) converted to the requested kind
func NewVersionedAttributes(attr admission.Attributes, gvk schema.GroupVersionKind, o admission.ObjectInterfaces) (*VersionedAttributes, error) {
func NewVersionedAttributes(attr Attributes, gvk schema.GroupVersionKind, o ObjectInterfaces) (*VersionedAttributes, error) {
// convert the old and new objects to the requested version
versionedAttr := &VersionedAttributes{
Attributes: attr,
@ -72,7 +96,7 @@ func NewVersionedAttributes(attr admission.Attributes, gvk schema.GroupVersionKi
// * attr.VersionedObject is used as the source for the new object if Dirty=true (and is round-tripped through attr.Attributes.Object, clearing Dirty in the process)
// * attr.Attributes.Object is used as the source for the new object if Dirty=false
// * attr.Attributes.OldObject is used as the source for the old object
func ConvertVersionedAttributes(attr *VersionedAttributes, gvk schema.GroupVersionKind, o admission.ObjectInterfaces) error {
func ConvertVersionedAttributes(attr *VersionedAttributes, gvk schema.GroupVersionKind, o ObjectInterfaces) error {
// we already have the desired kind, we're done
if attr.VersionedKind == gvk {
return nil

View File

@ -20,6 +20,7 @@ import (
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/cel/openapi/resolver"
quota "k8s.io/apiserver/pkg/quota/v1"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/informers"
@ -81,3 +82,10 @@ type WantsRESTMapper interface {
SetRESTMapper(meta.RESTMapper)
admission.InitializationValidator
}
// WantsSchemaResolver defines a function which sets the SchemaResolver for
// an admission plugin that needs it.
type WantsSchemaResolver interface {
SetSchemaResolver(resolver resolver.SchemaResolver)
admission.InitializationValidator
}

View File

@ -112,12 +112,13 @@ func (p pluginHandlerWithMetrics) Validate(ctx context.Context, a admission.Attr
// AdmissionMetrics instruments admission with prometheus metrics.
type AdmissionMetrics struct {
step *metricSet
controller *metricSet
webhook *metricSet
webhookRejection *metrics.CounterVec
webhookFailOpen *metrics.CounterVec
webhookRequest *metrics.CounterVec
step *metricSet
controller *metricSet
webhook *metricSet
webhookRejection *metrics.CounterVec
webhookFailOpen *metrics.CounterVec
webhookRequest *metrics.CounterVec
matchConditionEvalErrors *metrics.CounterVec
}
// newAdmissionMetrics create a new AdmissionMetrics, configured with default metric names.
@ -178,7 +179,7 @@ func newAdmissionMetrics() *AdmissionMetrics {
Subsystem: subsystem,
Name: "webhook_admission_duration_seconds",
Help: "Admission webhook latency histogram in seconds, identified by name and broken out for each operation and API resource and type (validate or admit).",
Buckets: []float64{0.005, 0.025, 0.1, 0.5, 1.0, 2.5},
Buckets: []float64{0.005, 0.025, 0.1, 0.5, 1.0, 2.5, 10, 25},
StabilityLevel: metrics.STABLE,
},
[]string{"name", "type", "operation", "rejected"},
@ -217,13 +218,24 @@ func newAdmissionMetrics() *AdmissionMetrics {
},
[]string{"name", "type", "operation", "code", "rejected"})
matchConditionEvalError := metrics.NewCounterVec(
&metrics.CounterOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "admission_match_condition_evaluation_errors_total",
Help: "Admission match condition evaluation errors count, identified by name of resource containing the match condition and broken out for each admission type (validating or mutating).",
StabilityLevel: metrics.ALPHA,
},
[]string{"name", "type"})
step.mustRegister()
controller.mustRegister()
webhook.mustRegister()
legacyregistry.MustRegister(webhookRejection)
legacyregistry.MustRegister(webhookFailOpen)
legacyregistry.MustRegister(webhookRequest)
return &AdmissionMetrics{step: step, controller: controller, webhook: webhook, webhookRejection: webhookRejection, webhookFailOpen: webhookFailOpen, webhookRequest: webhookRequest}
legacyregistry.MustRegister(matchConditionEvalError)
return &AdmissionMetrics{step: step, controller: controller, webhook: webhook, webhookRejection: webhookRejection, webhookFailOpen: webhookFailOpen, webhookRequest: webhookRequest, matchConditionEvalErrors: matchConditionEvalError}
}
func (m *AdmissionMetrics) reset() {
@ -267,6 +279,11 @@ func (m *AdmissionMetrics) ObserveWebhookFailOpen(ctx context.Context, name, ste
m.webhookFailOpen.WithContext(ctx).WithLabelValues(name, stepType).Inc()
}
// ObserveMatchConditionEvalError records validating or mutating webhook that are not called due to match conditions
func (m *AdmissionMetrics) ObserveMatchConditionEvalError(ctx context.Context, name, stepType string) {
m.matchConditionEvalErrors.WithContext(ctx).WithLabelValues(name, stepType).Inc()
}
type metricSet struct {
latencies *metrics.HistogramVec
latenciesSummary *metrics.SummaryVec

View File

@ -0,0 +1,10 @@
# See the OWNERS docs at https://go.k8s.io/owners
approvers:
- jpbetz
- cici37
- alexzielenski
reviewers:
- jpbetz
- cici37
- alexzielenski

View File

@ -14,9 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package validatingadmissionpolicy
package cel
import (
"fmt"
celconfig "k8s.io/apiserver/pkg/apis/cel"
"sync"
"github.com/google/cel-go/cel"
@ -26,43 +28,33 @@ import (
)
const (
ObjectVarName = "object"
OldObjectVarName = "oldObject"
ParamsVarName = "params"
RequestVarName = "request"
checkFrequency = 100
ObjectVarName = "object"
OldObjectVarName = "oldObject"
ParamsVarName = "params"
RequestVarName = "request"
AuthorizerVarName = "authorizer"
RequestResourceAuthorizerVarName = "authorizer.requestResource"
)
type envs struct {
noParams *cel.Env
withParams *cel.Env
}
var (
initEnvsOnce sync.Once
initEnvs *envs
initEnvs envs
initEnvsErr error
)
func getEnvs() (*envs, error) {
func getEnvs() (envs, error) {
initEnvsOnce.Do(func() {
base, err := buildBaseEnv()
requiredVarsEnv, err := buildRequiredVarsEnv()
if err != nil {
initEnvsErr = err
return
}
noParams, err := buildNoParamsEnv(base)
initEnvs, err = buildWithOptionalVarsEnvs(requiredVarsEnv)
if err != nil {
initEnvsErr = err
return
}
withParams, err := buildWithParamsEnv(noParams)
if err != nil {
initEnvsErr = err
return
}
initEnvs = &envs{noParams: noParams, withParams: withParams}
})
return initEnvs, initEnvsErr
}
@ -81,11 +73,15 @@ func buildBaseEnv() (*cel.Env, error) {
return cel.NewEnv(opts...)
}
func buildNoParamsEnv(baseEnv *cel.Env) (*cel.Env, error) {
func buildRequiredVarsEnv() (*cel.Env, error) {
baseEnv, err := buildBaseEnv()
if err != nil {
return nil, err
}
var propDecls []cel.EnvOption
reg := apiservercel.NewRegistry(baseEnv)
requestType := buildRequestType()
requestType := BuildRequestType()
rt, err := apiservercel.NewRuleTypes(requestType.TypeName(), requestType, reg)
if err != nil {
return nil, err
@ -109,15 +105,40 @@ func buildNoParamsEnv(baseEnv *cel.Env) (*cel.Env, error) {
return env, nil
}
func buildWithParamsEnv(noParams *cel.Env) (*cel.Env, error) {
return noParams.Extend(cel.Variable(ParamsVarName, cel.DynType))
type envs map[OptionalVariableDeclarations]*cel.Env
func buildEnvWithVars(baseVarsEnv *cel.Env, options OptionalVariableDeclarations) (*cel.Env, error) {
var opts []cel.EnvOption
if options.HasParams {
opts = append(opts, cel.Variable(ParamsVarName, cel.DynType))
}
if options.HasAuthorizer {
opts = append(opts, cel.Variable(AuthorizerVarName, library.AuthorizerType))
opts = append(opts, cel.Variable(RequestResourceAuthorizerVarName, library.ResourceCheckType))
}
return baseVarsEnv.Extend(opts...)
}
// buildRequestType generates a DeclType for AdmissionRequest. This may be replaced with a utility that
func buildWithOptionalVarsEnvs(requiredVarsEnv *cel.Env) (envs, error) {
envs := make(envs, 4) // 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} {
opts := OptionalVariableDeclarations{HasParams: hasParams, HasAuthorizer: hasAuthorizer}
env, err := buildEnvWithVars(requiredVarsEnv, opts)
if err != nil {
return nil, err
}
envs[opts] = env
}
}
return envs, nil
}
// BuildRequestType generates a DeclType for AdmissionRequest. This may be replaced with a utility that
// converts the native type definition to apiservercel.DeclType once such a utility becomes available.
// The 'uid' field is omitted since it is not needed for in-process admission review.
// The 'object' and 'oldObject' fields are omitted since they are exposed as root level CEL variables.
func buildRequestType() *apiservercel.DeclType {
func BuildRequestType() *apiservercel.DeclType {
field := func(name string, declType *apiservercel.DeclType, required bool) *apiservercel.DeclField {
return apiservercel.NewDeclField(name, declType, required, nil, nil)
}
@ -160,14 +181,16 @@ func buildRequestType() *apiservercel.DeclType {
))
}
// CompilationResult represents a compiled ValidatingAdmissionPolicy validation expression.
// CompilationResult represents a compiled validations expression.
type CompilationResult struct {
Program cel.Program
Error *apiservercel.Error
Program cel.Program
Error *apiservercel.Error
ExpressionAccessor ExpressionAccessor
}
// CompileValidatingPolicyExpression returns a compiled vaalidating policy CEL expression.
func CompileValidatingPolicyExpression(validationExpression string, hasParams bool) CompilationResult {
// CompileCELExpression returns a compiled CEL expression.
// perCallLimit was added for testing purpose only. Callers should always use const PerCallLimit from k8s.io/apiserver/pkg/apis/cel/config.go as input.
func CompileCELExpression(expressionAccessor ExpressionAccessor, optionalVars OptionalVariableDeclarations, perCallLimit uint64) CompilationResult {
var env *cel.Env
envs, err := getEnvs()
if err != nil {
@ -176,29 +199,52 @@ func CompileValidatingPolicyExpression(validationExpression string, hasParams bo
Type: apiservercel.ErrorTypeInternal,
Detail: "compiler initialization failed: " + err.Error(),
},
ExpressionAccessor: expressionAccessor,
}
}
if hasParams {
env = envs.withParams
} else {
env = envs.noParams
env, ok := envs[optionalVars]
if !ok {
return CompilationResult{
Error: &apiservercel.Error{
Type: apiservercel.ErrorTypeInvalid,
Detail: fmt.Sprintf("compiler initialization failed: failed to load environment for %v", optionalVars),
},
ExpressionAccessor: expressionAccessor,
}
}
ast, issues := env.Compile(validationExpression)
ast, issues := env.Compile(expressionAccessor.GetExpression())
if issues != nil {
return CompilationResult{
Error: &apiservercel.Error{
Type: apiservercel.ErrorTypeInvalid,
Detail: "compilation failed: " + issues.String(),
},
ExpressionAccessor: expressionAccessor,
}
}
if ast.OutputType() != cel.BoolType {
found := false
returnTypes := expressionAccessor.ReturnTypes()
for _, returnType := range returnTypes {
if ast.OutputType() == returnType {
found = true
break
}
}
if !found {
var reason string
if len(returnTypes) == 1 {
reason = fmt.Sprintf("must evaluate to %v", returnTypes[0].String())
} else {
reason = fmt.Sprintf("must evaluate to one of %v", returnTypes)
}
return CompilationResult{
Error: &apiservercel.Error{
Type: apiservercel.ErrorTypeInvalid,
Detail: "cel expression must evaluate to a bool",
Detail: reason,
},
ExpressionAccessor: expressionAccessor,
}
}
@ -210,12 +256,14 @@ func CompileValidatingPolicyExpression(validationExpression string, hasParams bo
Type: apiservercel.ErrorTypeInternal,
Detail: "unexpected compilation error: " + err.Error(),
},
ExpressionAccessor: expressionAccessor,
}
}
prog, err := env.Program(ast,
cel.EvalOptions(cel.OptOptimize),
cel.EvalOptions(cel.OptOptimize, cel.OptTrackCost),
cel.OptimizeRegex(library.ExtensionLibRegexOptimizations...),
cel.InterruptCheckFrequency(checkFrequency),
cel.InterruptCheckFrequency(celconfig.CheckFrequency),
cel.CostLimit(perCallLimit),
)
if err != nil {
return CompilationResult{
@ -223,9 +271,11 @@ func CompileValidatingPolicyExpression(validationExpression string, hasParams bo
Type: apiservercel.ErrorTypeInvalid,
Detail: "program instantiation failed: " + err.Error(),
},
ExpressionAccessor: expressionAccessor,
}
}
return CompilationResult{
Program: prog,
Program: prog,
ExpressionAccessor: expressionAccessor,
}
}

View File

@ -0,0 +1,296 @@
/*
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"
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/library"
)
// filterCompiler implement the interface FilterCompiler.
type filterCompiler struct {
}
func NewFilterCompiler() FilterCompiler {
return &filterCompiler{}
}
type evaluationActivation struct {
object, oldObject, params, request, authorizer, requestResourceAuthorizer 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 AuthorizerVarName:
return a.authorizer, a.authorizer != nil
case RequestResourceAuthorizerVarName:
return a.requestResourceAuthorizer, a.requestResourceAuthorizer != nil
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, perCallLimit uint64) Filter {
compilationResults := make([]CompilationResult, len(expressionAccessors))
for i, expressionAccessor := range expressionAccessors {
if expressionAccessor == nil {
continue
}
compilationResults[i] = CompileCELExpression(expressionAccessor, options, perCallLimit)
}
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, 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
}
va := &evaluationActivation{
object: objectVal,
oldObject: oldObjectVal,
params: paramsVal,
request: requestVal.Object,
authorizer: authorizerVal,
requestResourceAuthorizer: requestResourceAuthorizerVal,
}
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),
}
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)
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()),
}
} 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"),
}
}
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) *admissionv1.AdmissionRequest {
// FIXME: how to get resource GVK, GVR and subresource?
gvk := attr.GetKind()
gvr := attr.GetResource()
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(),
},
}
}
// 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

@ -0,0 +1,87 @@
/*
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"
"time"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types/ref"
v1 "k8s.io/api/admission/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/authorization/authorizer"
)
type ExpressionAccessor interface {
GetExpression() string
ReturnTypes() []*cel.Type
}
// EvaluationResult contains the minimal required fields and metadata of a cel evaluation
type EvaluationResult struct {
EvalResult ref.Val
ExpressionAccessor ExpressionAccessor
Elapsed time.Duration
Error error
}
// OptionalVariableDeclarations declares which optional CEL variables
// are declared for an expression.
type OptionalVariableDeclarations struct {
// HasParams specifies if the "params" variable is declared.
// The "params" variable may still be bound to "null" when declared.
HasParams bool
// HasAuthorizer specifies if the"authorizer" and "authorizer.requestResource"
// variables are declared. When declared, the authorizer variables are
// expected to be non-null.
HasAuthorizer 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
// perCallLimit was added for testing purpose only. Callers should always use const PerCallLimit from k8s.io/apiserver/pkg/apis/cel/config.go as input.
Compile(expressions []ExpressionAccessor, optionalDecls OptionalVariableDeclarations, perCallLimit uint64) Filter
}
// OptionalVariableBindings provides expression bindings for optional CEL variables.
type OptionalVariableBindings struct {
// VersionedParams provides the "params" variable binding. This variable binding may
// be set to nil even when OptionalVariableDeclarations.HashParams is set to true.
VersionedParams runtime.Object
// Authorizer provides the authorizer used for the "authorizer" and
// "authorizer.requestResource" variable bindings. If the expression was compiled with
// OptionalVariableDeclarations.HasAuthorizer set to true this must be non-nil.
Authorizer authorizer.Authorizer
}
// Filter contains a function to evaluate compiled CEL-typed values
// 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 {
// 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.
ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *v1.AdmissionRequest, optionalVars OptionalVariableBindings, runtimeCELCostBudget int64) ([]EvaluationResult, int64, error)
// CompilationErrors returns a list of errors from the compilation of the evaluator
CompilationErrors() []error
}

View File

@ -23,6 +23,8 @@ import (
"io"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/cel/openapi/resolver"
"k8s.io/apiserver/pkg/features"
"k8s.io/client-go/dynamic"
"k8s.io/component-base/featuregate"
@ -71,6 +73,8 @@ type celAdmissionPlugin struct {
restMapper meta.RESTMapper
dynamicClient dynamic.Interface
stopCh <-chan struct{}
authorizer authorizer.Authorizer
schemaResolver resolver.SchemaResolver
}
var _ initializer.WantsExternalKubeInformerFactory = &celAdmissionPlugin{}
@ -78,7 +82,8 @@ var _ initializer.WantsExternalKubeClientSet = &celAdmissionPlugin{}
var _ initializer.WantsRESTMapper = &celAdmissionPlugin{}
var _ initializer.WantsDynamicClient = &celAdmissionPlugin{}
var _ initializer.WantsDrainedNotification = &celAdmissionPlugin{}
var _ initializer.WantsAuthorizer = &celAdmissionPlugin{}
var _ initializer.WantsSchemaResolver = &celAdmissionPlugin{}
var _ admission.InitializationValidator = &celAdmissionPlugin{}
var _ admission.ValidationInterface = &celAdmissionPlugin{}
@ -108,6 +113,14 @@ func (c *celAdmissionPlugin) SetDrainedNotification(stopCh <-chan struct{}) {
c.stopCh = stopCh
}
func (c *celAdmissionPlugin) SetAuthorizer(authorizer authorizer.Authorizer) {
c.authorizer = authorizer
}
func (c *celAdmissionPlugin) SetSchemaResolver(resolver resolver.SchemaResolver) {
c.schemaResolver = resolver
}
func (c *celAdmissionPlugin) InspectFeatureGates(featureGates featuregate.FeatureGate) {
if featureGates.Enabled(features.ValidatingAdmissionPolicy) {
c.enabled = true
@ -138,7 +151,10 @@ func (c *celAdmissionPlugin) ValidateInitialization() error {
if c.stopCh == nil {
return errors.New("missing stop channel")
}
c.evaluator = NewAdmissionController(c.informerFactory, c.client, c.restMapper, c.dynamicClient)
if c.authorizer == nil {
return errors.New("missing authorizer")
}
c.evaluator = NewAdmissionController(c.informerFactory, c.client, c.restMapper, c.schemaResolver /* (optional) */, c.dynamicClient, c.authorizer)
if err := c.evaluator.ValidateInitialization(); err != nil {
return err
}

View File

@ -20,25 +20,35 @@ import (
"context"
"errors"
"fmt"
"strings"
"sync"
"sync/atomic"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/matching"
"k8s.io/klog/v2"
"k8s.io/api/admissionregistration/v1alpha1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
utiljson "k8s.io/apimachinery/pkg/util/json"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apiserver/pkg/admission"
celmetrics "k8s.io/apiserver/pkg/admission/cel"
"k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/internal/generic"
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/matching"
celconfig "k8s.io/apiserver/pkg/apis/cel"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/cel/openapi/resolver"
"k8s.io/apiserver/pkg/warning"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/cache"
)
var _ CELPolicyEvaluator = &celAdmissionController{}
@ -46,44 +56,32 @@ var _ CELPolicyEvaluator = &celAdmissionController{}
// celAdmissionController is the top-level controller for admission control using CEL
// it is responsible for watching policy definitions, bindings, and config param CRDs
type celAdmissionController struct {
// Context under which the controller runs
runningContext context.Context
// Controller which manages book-keeping for the cluster's dynamic policy
// information.
policyController *policyController
policyDefinitionsController generic.Controller[*v1alpha1.ValidatingAdmissionPolicy]
policyBindingController generic.Controller[*v1alpha1.ValidatingAdmissionPolicyBinding]
// atomic []policyData
// list of every known policy definition, and all informatoin required to
// validate its bindings against an object.
// A snapshot of the current policy configuration is synced with this field
// asynchronously
definitions atomic.Value
}
// dynamicclient used to create informers to watch the param crd types
dynamicClient dynamic.Interface
restMapper meta.RESTMapper
// Everything someone might need to validate a single ValidatingPolicyDefinition
// against all of its registered bindings.
type policyData struct {
definitionInfo
paramController generic.Controller[runtime.Object]
bindings []bindingInfo
}
// Provided to the policy's Compile function as an injected dependency to
// assist with compiling its expressions to CEL
validatorCompiler ValidatorCompiler
// Lock which protects:
// - definitionInfo
// - bindingInfos
// - paramCRDControllers
// - definitionsToBindings
// All other fields should be assumed constant
mutex sync.RWMutex
// controller and metadata
paramsCRDControllers map[v1alpha1.ParamKind]*paramInfo
// Index for each definition namespace/name, contains all binding
// namespace/names known to exist for that definition
definitionInfo map[namespacedName]*definitionInfo
// Index for each bindings namespace/name. Contains compiled templates
// for the binding depending on the policy/param combination.
bindingInfos map[namespacedName]*bindingInfo
// Map from namespace/name of a definition to a set of namespace/name
// of bindings which depend on it.
// All keys must have at least one dependent binding
// All binding names MUST exist as a key bindingInfos
definitionsToBindings map[namespacedName]sets.Set[namespacedName]
// contains the cel PolicyDecisions along with the ValidatingAdmissionPolicy and ValidatingAdmissionPolicyBinding
// that determined the decision
type policyDecisionWithMetadata struct {
PolicyDecision
Definition *v1alpha1.ValidatingAdmissionPolicy
Binding *v1alpha1.ValidatingAdmissionPolicyBinding
}
// namespaceName is used as a key in definitionInfo and bindingInfos
@ -104,7 +102,7 @@ type definitionInfo struct {
type bindingInfo struct {
// Compiled CEL expression turned into an validator
validator atomic.Pointer[Validator]
validator Validator
// Last value seen by this controller to be used in policy enforcement
// May not be nil
@ -113,7 +111,7 @@ type bindingInfo struct {
type paramInfo struct {
// Controller which is watching this param CRD
controller generic.Controller[*unstructured.Unstructured]
controller generic.Controller[runtime.Object]
// Function to call to stop the informer and clean up the controller
stop func()
@ -127,67 +125,54 @@ func NewAdmissionController(
informerFactory informers.SharedInformerFactory,
client kubernetes.Interface,
restMapper meta.RESTMapper,
schemaResolver resolver.SchemaResolver,
dynamicClient dynamic.Interface,
authz authorizer.Authorizer,
) CELPolicyEvaluator {
matcher := matching.NewMatcher(informerFactory.Core().V1().Namespaces().Lister(), client)
validatorCompiler := &CELValidatorCompiler{
Matcher: matcher,
var typeChecker *TypeChecker
if schemaResolver != nil {
typeChecker = &TypeChecker{schemaResolver: schemaResolver, restMapper: restMapper}
}
c := &celAdmissionController{
definitionInfo: make(map[namespacedName]*definitionInfo),
bindingInfos: make(map[namespacedName]*bindingInfo),
paramsCRDControllers: make(map[v1alpha1.ParamKind]*paramInfo),
definitionsToBindings: make(map[namespacedName]sets.Set[namespacedName]),
dynamicClient: dynamicClient,
validatorCompiler: validatorCompiler,
restMapper: restMapper,
return &celAdmissionController{
definitions: atomic.Value{},
policyController: newPolicyController(
restMapper,
client,
dynamicClient,
typeChecker,
cel.NewFilterCompiler(),
NewMatcher(matching.NewMatcher(informerFactory.Core().V1().Namespaces().Lister(), client)),
generic.NewInformer[*v1alpha1.ValidatingAdmissionPolicy](
informerFactory.Admissionregistration().V1alpha1().ValidatingAdmissionPolicies().Informer()),
generic.NewInformer[*v1alpha1.ValidatingAdmissionPolicyBinding](
informerFactory.Admissionregistration().V1alpha1().ValidatingAdmissionPolicyBindings().Informer()),
authz,
),
}
c.policyDefinitionsController = generic.NewController(
generic.NewInformer[*v1alpha1.ValidatingAdmissionPolicy](
informerFactory.Admissionregistration().V1alpha1().ValidatingAdmissionPolicies().Informer()),
c.reconcilePolicyDefinition,
generic.ControllerOptions{
Workers: 1,
Name: "cel-policy-definitions",
},
)
c.policyBindingController = generic.NewController(
generic.NewInformer[*v1alpha1.ValidatingAdmissionPolicyBinding](
informerFactory.Admissionregistration().V1alpha1().ValidatingAdmissionPolicyBindings().Informer()),
c.reconcilePolicyBinding,
generic.ControllerOptions{
Workers: 1,
Name: "cel-policy-bindings",
},
)
return c
}
func (c *celAdmissionController) Run(stopCh <-chan struct{}) {
if c.runningContext != nil {
return
}
ctx, cancel := context.WithCancel(context.Background())
c.runningContext = ctx
defer func() {
c.runningContext = nil
}()
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
c.policyDefinitionsController.Run(ctx)
c.policyController.Run(ctx)
}()
wg.Add(1)
go func() {
defer wg.Done()
c.policyBindingController.Run(ctx)
// Wait indefinitely until policies/bindings are listed & handled before
// allowing policies to be refreshed
if !cache.WaitForNamedCacheSync("cel-admission-controller", ctx.Done(), c.policyController.HasSynced) {
return
}
// Loop every 1 second until context is cancelled, refreshing policies
wait.Until(c.refreshPolicies, 1*time.Second, ctx.Done())
}()
<-stopCh
@ -195,13 +180,16 @@ func (c *celAdmissionController) Run(stopCh <-chan struct{}) {
wg.Wait()
}
const maxAuditAnnotationValueLength = 10 * 1024
func (c *celAdmissionController) Validate(
ctx context.Context,
a admission.Attributes,
o admission.ObjectInterfaces,
) (err error) {
c.mutex.RLock()
defer c.mutex.RUnlock()
if !c.HasSynced() {
return admission.NewForbidden(a, fmt.Errorf("not yet ready to handle request"))
}
var deniedDecisions []policyDecisionWithMetadata
@ -227,27 +215,29 @@ func (c *celAdmissionController) Validate(
message = fmt.Errorf("failed to configure binding: %w", err).Error()
}
deniedDecisions = append(deniedDecisions, policyDecisionWithMetadata{
policyDecision: policyDecision{
action: actionDeny,
message: message,
PolicyDecision: PolicyDecision{
Action: ActionDeny,
Message: message,
},
definition: definition,
binding: binding,
Definition: definition,
Binding: binding,
})
default:
deniedDecisions = append(deniedDecisions, policyDecisionWithMetadata{
policyDecision: policyDecision{
action: actionDeny,
message: fmt.Errorf("unrecognized failure policy: '%v'", policy).Error(),
PolicyDecision: PolicyDecision{
Action: ActionDeny,
Message: fmt.Errorf("unrecognized failure policy: '%v'", policy).Error(),
},
definition: definition,
binding: binding,
Definition: definition,
Binding: binding,
})
}
}
for definitionNamespacedName, definitionInfo := range c.definitionInfo {
policyDatas := c.definitions.Load().([]policyData)
for _, definitionInfo := range policyDatas {
definition := definitionInfo.lastReconciledValue
matches, matchKind, err := c.validatorCompiler.DefinitionMatches(a, o, definition)
matches, matchKind, err := c.policyController.matcher.DefinitionMatches(a, o, definition)
if err != nil {
// Configuration error.
addConfigError(err, definition, nil)
@ -262,17 +252,12 @@ func (c *celAdmissionController) Validate(
continue
}
dependentBindings := c.definitionsToBindings[definitionNamespacedName]
if len(dependentBindings) == 0 {
continue
}
for namespacedBindingName := range dependentBindings {
auditAnnotationCollector := newAuditAnnotationCollector()
for _, bindingInfo := range definitionInfo.bindings {
// If the key is inside dependentBindings, there is guaranteed to
// be a bindingInfo for it
bindingInfo := c.bindingInfos[namespacedBindingName]
binding := bindingInfo.lastReconciledValue
matches, err := c.validatorCompiler.BindingMatches(a, o, binding)
matches, err := c.policyController.matcher.BindingMatches(a, o, binding)
if err != nil {
// Configuration error.
addConfigError(err, definition, binding)
@ -282,18 +267,21 @@ func (c *celAdmissionController) Validate(
continue
}
var param *unstructured.Unstructured
var param runtime.Object
// versionedAttributes will be set to non-nil inside of the loop, but
// is scoped outside of the param loop so we only convert once. We defer
// conversion so that it is only performed when we know a policy matches,
// saving the cost of converting non-matching requests.
var versionedAttr *admission.VersionedAttributes
// If definition has paramKind, paramRef is required in binding.
// If definition has no paramKind, paramRef set in binding will be ignored.
paramKind := definition.Spec.ParamKind
paramRef := binding.Spec.ParamRef
if paramKind != nil && paramRef != nil {
// Find the params referred by the binding by looking its name up
// in our informer for its CRD
paramInfo, ok := c.paramsCRDControllers[*paramKind]
if !ok {
paramController := definitionInfo.paramController
if paramController == nil {
addConfigError(fmt.Errorf("paramKind kind `%v` not known",
paramKind.String()), definition, binding)
continue
@ -302,18 +290,19 @@ func (c *celAdmissionController) Validate(
// If the param informer for this admission policy has not yet
// had time to perform an initial listing, don't attempt to use
// it.
//!TOOD(alexzielenski): add a wait for a very short amount of
// time for the cache to sync
if !paramInfo.controller.HasSynced() {
timeoutCtx, cancel := context.WithTimeout(c.policyController.context, 1*time.Second)
defer cancel()
if !cache.WaitForCacheSync(timeoutCtx.Done(), paramController.HasSynced) {
addConfigError(fmt.Errorf("paramKind kind `%v` not yet synced to use for admission",
paramKind.String()), definition, binding)
continue
}
if len(paramRef.Namespace) == 0 {
param, err = paramInfo.controller.Informer().Get(paramRef.Name)
param, err = paramController.Informer().Get(paramRef.Name)
} else {
param, err = paramInfo.controller.Informer().Namespaced(paramRef.Namespace).Get(paramRef.Name)
param, err = paramController.Informer().Namespaced(paramRef.Namespace).Get(paramRef.Name)
}
if err != nil {
@ -336,16 +325,17 @@ func (c *celAdmissionController) Validate(
}
}
validator := bindingInfo.validator.Load()
if validator == nil {
// Compile policy definition using binding
newValidator := c.validatorCompiler.Compile(definition)
validator = &newValidator
bindingInfo.validator.Store(validator)
if versionedAttr == nil {
va, err := admission.NewVersionedAttributes(a, matchKind, o)
if err != nil {
wrappedErr := fmt.Errorf("failed to convert object version: %w", err)
addConfigError(wrappedErr, definition, binding)
continue
}
versionedAttr = va
}
decisions, err := (*validator).Validate(a, o, param, matchKind)
validationResult := bindingInfo.validator.Validate(ctx, versionedAttr, param, celconfig.RuntimeCELCostBudget)
if err != nil {
// runtime error. Apply failure policy
wrappedError := fmt.Errorf("failed to evaluate CEL expression: %w", err)
@ -353,38 +343,77 @@ func (c *celAdmissionController) Validate(
continue
}
for _, decision := range decisions {
switch decision.action {
case actionAdmit:
if decision.evaluation == evalError {
celmetrics.Metrics.ObserveAdmissionWithError(ctx, decision.elapsed, definition.Name, binding.Name, "active")
for i, decision := range validationResult.Decisions {
switch decision.Action {
case ActionAdmit:
if decision.Evaluation == EvalError {
celmetrics.Metrics.ObserveAdmissionWithError(ctx, decision.Elapsed, definition.Name, binding.Name, "active")
}
case ActionDeny:
for _, action := range binding.Spec.ValidationActions {
switch action {
case v1alpha1.Deny:
deniedDecisions = append(deniedDecisions, policyDecisionWithMetadata{
Definition: definition,
Binding: binding,
PolicyDecision: decision,
})
celmetrics.Metrics.ObserveRejection(ctx, decision.Elapsed, definition.Name, binding.Name, "active")
case v1alpha1.Audit:
c.publishValidationFailureAnnotation(binding, i, decision, versionedAttr)
celmetrics.Metrics.ObserveAudit(ctx, decision.Elapsed, definition.Name, binding.Name, "active")
case v1alpha1.Warn:
warning.AddWarning(ctx, "", fmt.Sprintf("Validation failed for ValidatingAdmissionPolicy '%s' with binding '%s': %s", definition.Name, binding.Name, decision.Message))
celmetrics.Metrics.ObserveWarn(ctx, decision.Elapsed, definition.Name, binding.Name, "active")
}
}
case actionDeny:
deniedDecisions = append(deniedDecisions, policyDecisionWithMetadata{
definition: definition,
binding: binding,
policyDecision: decision,
})
celmetrics.Metrics.ObserveRejection(ctx, decision.elapsed, definition.Name, binding.Name, "active")
default:
return fmt.Errorf("unrecognized evaluation decision '%s' for ValidatingAdmissionPolicyBinding '%s' with ValidatingAdmissionPolicy '%s'",
decision.action, binding.Name, definition.Name)
decision.Action, binding.Name, definition.Name)
}
}
for _, auditAnnotation := range validationResult.AuditAnnotations {
switch auditAnnotation.Action {
case AuditAnnotationActionPublish:
value := auditAnnotation.Value
if len(auditAnnotation.Value) > maxAuditAnnotationValueLength {
value = value[:maxAuditAnnotationValueLength]
}
auditAnnotationCollector.add(auditAnnotation.Key, value)
case AuditAnnotationActionError:
// When failurePolicy=fail, audit annotation errors result in deny
deniedDecisions = append(deniedDecisions, policyDecisionWithMetadata{
Definition: definition,
Binding: binding,
PolicyDecision: PolicyDecision{
Action: ActionDeny,
Evaluation: EvalError,
Message: auditAnnotation.Error,
Elapsed: auditAnnotation.Elapsed,
},
})
celmetrics.Metrics.ObserveRejection(ctx, auditAnnotation.Elapsed, definition.Name, binding.Name, "active")
case AuditAnnotationActionExclude: // skip it
default:
return fmt.Errorf("unsupported AuditAnnotation Action: %s", auditAnnotation.Action)
}
}
}
auditAnnotationCollector.publish(definition.Name, a)
}
if len(deniedDecisions) > 0 {
// TODO: refactor admission.NewForbidden so the name extraction is reusable but the code/reason is customizable
var message string
deniedDecision := deniedDecisions[0]
if deniedDecision.binding != nil {
message = fmt.Sprintf("ValidatingAdmissionPolicy '%s' with binding '%s' denied request: %s", deniedDecision.definition.Name, deniedDecision.binding.Name, deniedDecision.message)
if deniedDecision.Binding != nil {
message = fmt.Sprintf("ValidatingAdmissionPolicy '%s' with binding '%s' denied request: %s", deniedDecision.Definition.Name, deniedDecision.Binding.Name, deniedDecision.Message)
} else {
message = fmt.Sprintf("ValidatingAdmissionPolicy '%s' denied request: %s", deniedDecision.definition.Name, deniedDecision.message)
message = fmt.Sprintf("ValidatingAdmissionPolicy '%s' denied request: %s", deniedDecision.Definition.Name, deniedDecision.Message)
}
err := admission.NewForbidden(a, errors.New(message)).(*k8serrors.StatusError)
reason := deniedDecision.reason
reason := deniedDecision.Reason
if len(reason) == 0 {
reason = metav1.StatusReasonInvalid
}
@ -396,11 +425,78 @@ func (c *celAdmissionController) Validate(
return nil
}
func (c *celAdmissionController) publishValidationFailureAnnotation(binding *v1alpha1.ValidatingAdmissionPolicyBinding, expressionIndex int, decision PolicyDecision, attributes admission.Attributes) {
key := "validation.policy.admission.k8s.io/validation_failure"
// Marshal to a list of failures since, in the future, we may need to support multiple failures
valueJson, err := utiljson.Marshal([]validationFailureValue{{
ExpressionIndex: expressionIndex,
Message: decision.Message,
ValidationActions: binding.Spec.ValidationActions,
Binding: binding.Name,
Policy: binding.Spec.PolicyName,
}})
if err != nil {
klog.Warningf("Failed to set admission audit annotation %s for ValidatingAdmissionPolicy %s and ValidatingAdmissionPolicyBinding %s: %v", key, binding.Spec.PolicyName, binding.Name, err)
}
value := string(valueJson)
if err := attributes.AddAnnotation(key, value); err != nil {
klog.Warningf("Failed to set admission audit annotation %s to %s for ValidatingAdmissionPolicy %s and ValidatingAdmissionPolicyBinding %s: %v", key, value, binding.Spec.PolicyName, binding.Name, err)
}
}
func (c *celAdmissionController) HasSynced() bool {
return c.policyBindingController.HasSynced() &&
c.policyDefinitionsController.HasSynced()
return c.policyController.HasSynced() && c.definitions.Load() != nil
}
func (c *celAdmissionController) ValidateInitialization() error {
return c.validatorCompiler.ValidateInitialization()
return c.policyController.matcher.ValidateInitialization()
}
func (c *celAdmissionController) refreshPolicies() {
c.definitions.Store(c.policyController.latestPolicyData())
}
// validationFailureValue defines the JSON format of a "validation.policy.admission.k8s.io/validation_failure" audit
// annotation value.
type validationFailureValue struct {
Message string `json:"message"`
Policy string `json:"policy"`
Binding string `json:"binding"`
ExpressionIndex int `json:"expressionIndex"`
ValidationActions []v1alpha1.ValidationAction `json:"validationActions"`
}
type auditAnnotationCollector struct {
annotations map[string][]string
}
func newAuditAnnotationCollector() auditAnnotationCollector {
return auditAnnotationCollector{annotations: map[string][]string{}}
}
func (a auditAnnotationCollector) add(key, value string) {
// If multiple bindings produces the exact same key and value for an audit annotation,
// ignore the duplicates.
for _, v := range a.annotations[key] {
if v == value {
return
}
}
a.annotations[key] = append(a.annotations[key], value)
}
func (a auditAnnotationCollector) publish(policyName string, attributes admission.Attributes) {
for key, bindingAnnotations := range a.annotations {
var value string
if len(bindingAnnotations) == 1 {
value = bindingAnnotations[0]
} else {
// Multiple distinct values can exist when binding params are used in the valueExpression of an auditAnnotation.
// When this happens, the values are concatenated into a comma-separated list.
value = strings.Join(bindingAnnotations, ", ")
}
if err := attributes.AddAnnotation(policyName+"/"+key, value); err != nil {
klog.Warningf("Failed to set admission audit annotation %s to %s for ValidatingAdmissionPolicy %s: %v", key, value, policyName, err)
}
}
}

View File

@ -19,22 +19,177 @@ package validatingadmissionpolicy
import (
"context"
"fmt"
"sync"
"time"
v1 "k8s.io/api/admissionregistration/v1"
"k8s.io/api/admissionregistration/v1alpha1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
apiequality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/sets"
celmetrics "k8s.io/apiserver/pkg/admission/cel"
"k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/internal/generic"
"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions"
celconfig "k8s.io/apiserver/pkg/apis/cel"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/dynamic/dynamicinformer"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
k8sscheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/tools/cache"
)
func (c *celAdmissionController) reconcilePolicyDefinition(namespace, name string, definition *v1alpha1.ValidatingAdmissionPolicy) error {
type policyController struct {
once sync.Once
context context.Context
dynamicClient dynamic.Interface
restMapper meta.RESTMapper
policyDefinitionsController generic.Controller[*v1alpha1.ValidatingAdmissionPolicy]
policyBindingController generic.Controller[*v1alpha1.ValidatingAdmissionPolicyBinding]
// Provided to the policy's Compile function as an injected dependency to
// assist with compiling its expressions to CEL
filterCompiler cel.FilterCompiler
matcher Matcher
newValidator
// The TypeCheck checks the policy's expressions for type errors.
// Type of params is defined in policy.Spec.ParamsKind
// Types of object are calculated from policy.Spec.MatchingConstraints
typeChecker *TypeChecker
// Lock which protects:
// - cachedPolicies
// - paramCRDControllers
// - definitionInfo
// - bindingInfos
// - definitionsToBindings
// All other fields should be assumed constant
mutex sync.RWMutex
cachedPolicies []policyData
// controller and metadata
paramsCRDControllers map[v1alpha1.ParamKind]*paramInfo
// Index for each definition namespace/name, contains all binding
// namespace/names known to exist for that definition
definitionInfo map[namespacedName]*definitionInfo
// Index for each bindings namespace/name. Contains compiled templates
// for the binding depending on the policy/param combination.
bindingInfos map[namespacedName]*bindingInfo
// Map from namespace/name of a definition to a set of namespace/name
// of bindings which depend on it.
// All keys must have at least one dependent binding
// All binding names MUST exist as a key bindingInfos
definitionsToBindings map[namespacedName]sets.Set[namespacedName]
client kubernetes.Interface
authz authorizer.Authorizer
}
type newValidator func(validationFilter cel.Filter, celMatcher matchconditions.Matcher, auditAnnotationFilter, messageFilter cel.Filter, failurePolicy *v1.FailurePolicyType, authorizer authorizer.Authorizer) Validator
func newPolicyController(
restMapper meta.RESTMapper,
client kubernetes.Interface,
dynamicClient dynamic.Interface,
typeChecker *TypeChecker,
filterCompiler cel.FilterCompiler,
matcher Matcher,
policiesInformer generic.Informer[*v1alpha1.ValidatingAdmissionPolicy],
bindingsInformer generic.Informer[*v1alpha1.ValidatingAdmissionPolicyBinding],
authz authorizer.Authorizer,
) *policyController {
res := &policyController{}
*res = policyController{
filterCompiler: filterCompiler,
typeChecker: typeChecker,
definitionInfo: make(map[namespacedName]*definitionInfo),
bindingInfos: make(map[namespacedName]*bindingInfo),
paramsCRDControllers: make(map[v1alpha1.ParamKind]*paramInfo),
definitionsToBindings: make(map[namespacedName]sets.Set[namespacedName]),
matcher: matcher,
newValidator: NewValidator,
policyDefinitionsController: generic.NewController(
policiesInformer,
res.reconcilePolicyDefinition,
generic.ControllerOptions{
Workers: 1,
Name: "cel-policy-definitions",
},
),
policyBindingController: generic.NewController(
bindingsInformer,
res.reconcilePolicyBinding,
generic.ControllerOptions{
Workers: 1,
Name: "cel-policy-bindings",
},
),
restMapper: restMapper,
dynamicClient: dynamicClient,
client: client,
authz: authz,
}
return res
}
func (c *policyController) Run(ctx context.Context) {
// Only support being run once
c.once.Do(func() {
c.context = ctx
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
c.policyDefinitionsController.Run(ctx)
}()
wg.Add(1)
go func() {
defer wg.Done()
c.policyBindingController.Run(ctx)
}()
<-ctx.Done()
wg.Wait()
})
}
func (c *policyController) HasSynced() bool {
return c.policyDefinitionsController.HasSynced() && c.policyBindingController.HasSynced()
}
func (c *policyController) reconcilePolicyDefinition(namespace, name string, definition *v1alpha1.ValidatingAdmissionPolicy) error {
c.mutex.Lock()
defer c.mutex.Unlock()
err := c.reconcilePolicyDefinitionSpec(namespace, name, definition)
if err != nil {
return err
}
if c.typeChecker != nil {
err = c.reconcilePolicyStatus(namespace, name, definition)
}
return err
}
func (c *policyController) reconcilePolicyDefinitionSpec(namespace, name string, definition *v1alpha1.ValidatingAdmissionPolicy) error {
c.cachedPolicies = nil // invalidate cachedPolicies
// Namespace for policydefinition is empty.
nn := getNamespaceName(namespace, name)
@ -46,6 +201,12 @@ func (c *celAdmissionController) reconcilePolicyDefinition(namespace, name strin
celmetrics.Metrics.ObserveDefinition(context.TODO(), "active", "deny")
}
// Skip reconcile if the spec of the definition is unchanged
if info.lastReconciledValue != nil && definition != nil &&
apiequality.Semantic.DeepEqual(info.lastReconciledValue.Spec, definition.Spec) {
return nil
}
var paramSource *v1alpha1.ParamKind
if definition != nil {
paramSource = definition.Spec.ParamKind
@ -75,7 +236,7 @@ func (c *celAdmissionController) reconcilePolicyDefinition(namespace, name strin
// definition has changed.
for key := range c.definitionsToBindings[nn] {
bindingInfo := c.bindingInfos[key]
bindingInfo.validator.Store(nil)
bindingInfo.validator = nil
c.bindingInfos[key] = bindingInfo
}
@ -125,20 +286,77 @@ func (c *celAdmissionController) reconcilePolicyDefinition(namespace, name strin
info.dependentDefinitions.Insert(nn)
} else {
instanceContext, instanceCancel := context.WithCancel(c.runningContext)
instanceContext, instanceCancel := context.WithCancel(c.context)
// Watch for new instances of this policy
informer := dynamicinformer.NewFilteredDynamicInformer(
c.dynamicClient,
paramsGVR.Resource,
corev1.NamespaceAll,
30*time.Second,
cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc},
nil,
)
var informer cache.SharedIndexInformer
// Informer Factory is optional
if c.client != nil {
// Create temporary informer factory
// Cannot use the k8s shared informer factory for dynamic params informer.
// Would leak unnecessary informers when we are done since we would have to
// call informerFactory.Start() with a longer-lived stopCh than necessary.
// SharedInformerFactory does not support temporary usage.
dynamicFactory := informers.NewSharedInformerFactory(c.client, 10*time.Minute)
// Look for a typed informer. If it does not exist
genericInformer, err := dynamicFactory.ForResource(paramsGVR.Resource)
// Ignore error. We fallback to dynamic informer if there is no
// typed informer
if err != nil {
informer = nil
} else {
informer = genericInformer.Informer()
// Set transformer on the informer to workaround inconsistency
// where typed objects have TypeMeta wiped out but dynamic
// objects keep kind/apiVersion fields
informer.SetTransform(func(i interface{}) (interface{}, error) {
// Ensure param is populated with its GVK for consistency
// (CRD dynamic informer always returns objects with kind/apiversion,
// but native types do not include populated TypeMeta.
if param := i.(runtime.Object); param != nil {
if param.GetObjectKind().GroupVersionKind().Empty() {
// https://github.com/kubernetes/client-go/issues/413#issue-324586398
gvks, _, _ := k8sscheme.Scheme.ObjectKinds(param)
for _, gvk := range gvks {
if len(gvk.Kind) == 0 {
continue
}
if len(gvk.Version) == 0 || gvk.Version == runtime.APIVersionInternal {
continue
}
param.GetObjectKind().SetGroupVersionKind(gvk)
break
}
}
}
return i, nil
})
}
}
if informer == nil {
// Dynamic JSON informer fallback.
// Cannot use shared dynamic informer since it would be impossible
// to clean CRD informers properly with multiple dependents
// (cannot start ahead of time, and cannot track dependencies via stopCh)
informer = dynamicinformer.NewFilteredDynamicInformer(
c.dynamicClient,
paramsGVR.Resource,
corev1.NamespaceAll,
// Use same interval as is used for k8s typed sharedInformerFactory
// https://github.com/kubernetes/kubernetes/blob/7e0923899fed622efbc8679cca6b000d43633e38/cmd/kube-apiserver/app/server.go#L430
10*time.Minute,
cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc},
nil,
).Informer()
}
controller := generic.NewController(
generic.NewInformer[*unstructured.Unstructured](informer.Informer()),
generic.NewInformer[runtime.Object](informer),
c.reconcileParams,
generic.ControllerOptions{
Workers: 1,
@ -152,17 +370,19 @@ func (c *celAdmissionController) reconcilePolicyDefinition(namespace, name strin
dependentDefinitions: sets.New(nn),
}
go informer.Informer().Run(instanceContext.Done())
go controller.Run(instanceContext)
go informer.Run(instanceContext.Done())
}
return nil
}
func (c *celAdmissionController) reconcilePolicyBinding(namespace, name string, binding *v1alpha1.ValidatingAdmissionPolicyBinding) error {
func (c *policyController) reconcilePolicyBinding(namespace, name string, binding *v1alpha1.ValidatingAdmissionPolicyBinding) error {
c.mutex.Lock()
defer c.mutex.Unlock()
c.cachedPolicies = nil // invalidate cachedPolicies
// Namespace for PolicyBinding is empty. In the future a namespaced binding
// may be added
// https://github.com/kubernetes/enhancements/blob/bf5c3c81ea2081d60c1dc7c832faa98479e06209/keps/sig-api-machinery/3488-cel-admission-control/README.md?plain=1#L1042
@ -173,6 +393,12 @@ func (c *celAdmissionController) reconcilePolicyBinding(namespace, name string,
c.bindingInfos[nn] = info
}
// Skip if the spec of the binding is unchanged.
if info.lastReconciledValue != nil && binding != nil &&
apiequality.Semantic.DeepEqual(info.lastReconciledValue.Spec, binding.Spec) {
return nil
}
var oldNamespacedDefinitionName namespacedName
if info.lastReconciledValue != nil {
// All validating policies are cluster-scoped so have empty namespace
@ -212,12 +438,36 @@ func (c *celAdmissionController) reconcilePolicyBinding(namespace, name string,
}
// Remove compiled template for old binding
info.validator.Store(nil)
info.validator = nil
info.lastReconciledValue = binding
return nil
}
func (c *celAdmissionController) reconcileParams(namespace, name string, params *unstructured.Unstructured) error {
func (c *policyController) reconcilePolicyStatus(namespace, name string, definition *v1alpha1.ValidatingAdmissionPolicy) error {
if definition != nil && definition.Status.ObservedGeneration < definition.Generation {
st := c.calculatePolicyStatus(definition)
newDefinition := definition.DeepCopy()
newDefinition.Status = *st
_, err := c.client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().UpdateStatus(c.context, newDefinition, metav1.UpdateOptions{})
if err != nil {
// ignore error when the controller is not able to
// mutate the definition, and to avoid infinite requeue.
utilruntime.HandleError(err)
}
}
return nil
}
func (c *policyController) calculatePolicyStatus(definition *v1alpha1.ValidatingAdmissionPolicy) *v1alpha1.ValidatingAdmissionPolicyStatus {
expressionWarnings := c.typeChecker.Check(definition)
// modifying a deepcopy of the original status, preserving unrelated existing data
status := definition.Status.DeepCopy()
status.ObservedGeneration = definition.Generation
status.TypeChecking = &v1alpha1.TypeChecking{ExpressionWarnings: expressionWarnings}
return status
}
func (c *policyController) reconcileParams(namespace, name string, params runtime.Object) error {
// Do nothing.
// When we add informational type checking we will need to compile in the
// reconcile loops instead of lazily so we can add compiler errors / type
@ -225,6 +475,127 @@ func (c *celAdmissionController) reconcileParams(namespace, name string, params
return nil
}
// Fetches the latest set of policy data or recalculates it if it has changed
// since it was last fetched
func (c *policyController) latestPolicyData() []policyData {
existing := func() []policyData {
c.mutex.RLock()
defer c.mutex.RUnlock()
return c.cachedPolicies
}()
if existing != nil {
return existing
}
c.mutex.Lock()
defer c.mutex.Unlock()
var res []policyData
for definitionNN, definitionInfo := range c.definitionInfo {
var bindingInfos []bindingInfo
for bindingNN := range c.definitionsToBindings[definitionNN] {
bindingInfo := c.bindingInfos[bindingNN]
if bindingInfo.validator == nil && definitionInfo.configurationError == nil {
hasParam := false
if definitionInfo.lastReconciledValue.Spec.ParamKind != nil {
hasParam = true
}
optionalVars := cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: true}
expressionOptionalVars := cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: false}
failurePolicy := convertv1alpha1FailurePolicyTypeTov1FailurePolicyType(definitionInfo.lastReconciledValue.Spec.FailurePolicy)
var matcher matchconditions.Matcher = nil
matchConditions := definitionInfo.lastReconciledValue.Spec.MatchConditions
if len(matchConditions) > 0 {
matchExpressionAccessors := make([]cel.ExpressionAccessor, len(matchConditions))
for i := range matchConditions {
matchExpressionAccessors[i] = (*matchconditions.MatchCondition)(&matchConditions[i])
}
matcher = matchconditions.NewMatcher(c.filterCompiler.Compile(matchExpressionAccessors, optionalVars, celconfig.PerCallLimit), c.authz, failurePolicy, "validatingadmissionpolicy", definitionInfo.lastReconciledValue.Name)
}
bindingInfo.validator = c.newValidator(
c.filterCompiler.Compile(convertv1alpha1Validations(definitionInfo.lastReconciledValue.Spec.Validations), optionalVars, celconfig.PerCallLimit),
matcher,
c.filterCompiler.Compile(convertv1alpha1AuditAnnotations(definitionInfo.lastReconciledValue.Spec.AuditAnnotations), optionalVars, celconfig.PerCallLimit),
c.filterCompiler.Compile(convertV1Alpha1MessageExpressions(definitionInfo.lastReconciledValue.Spec.Validations), expressionOptionalVars, celconfig.PerCallLimit),
failurePolicy,
c.authz,
)
}
bindingInfos = append(bindingInfos, *bindingInfo)
}
var paramController generic.Controller[runtime.Object]
if paramKind := definitionInfo.lastReconciledValue.Spec.ParamKind; paramKind != nil {
if info, ok := c.paramsCRDControllers[*paramKind]; ok {
paramController = info.controller
}
}
res = append(res, policyData{
definitionInfo: *definitionInfo,
paramController: paramController,
bindings: bindingInfos,
})
}
c.cachedPolicies = res
return res
}
func convertv1alpha1FailurePolicyTypeTov1FailurePolicyType(policyType *v1alpha1.FailurePolicyType) *v1.FailurePolicyType {
if policyType == nil {
return nil
}
var v1FailPolicy v1.FailurePolicyType
if *policyType == v1alpha1.Fail {
v1FailPolicy = v1.Fail
} else if *policyType == v1alpha1.Ignore {
v1FailPolicy = v1.Ignore
}
return &v1FailPolicy
}
func convertv1alpha1Validations(inputValidations []v1alpha1.Validation) []cel.ExpressionAccessor {
celExpressionAccessor := make([]cel.ExpressionAccessor, len(inputValidations))
for i, validation := range inputValidations {
validation := ValidationCondition{
Expression: validation.Expression,
Message: validation.Message,
Reason: validation.Reason,
}
celExpressionAccessor[i] = &validation
}
return celExpressionAccessor
}
func convertV1Alpha1MessageExpressions(inputValidations []v1alpha1.Validation) []cel.ExpressionAccessor {
celExpressionAccessor := make([]cel.ExpressionAccessor, len(inputValidations))
for i, validation := range inputValidations {
if validation.MessageExpression != "" {
condition := MessageExpressionCondition{
MessageExpression: validation.MessageExpression,
}
celExpressionAccessor[i] = &condition
}
}
return celExpressionAccessor
}
func convertv1alpha1AuditAnnotations(inputValidations []v1alpha1.AuditAnnotation) []cel.ExpressionAccessor {
celExpressionAccessor := make([]cel.ExpressionAccessor, len(inputValidations))
for i, validation := range inputValidations {
validation := AuditAnnotationCondition{
Key: validation.Key,
ValueExpression: validation.ValueExpression,
}
celExpressionAccessor[i] = &validation
}
return celExpressionAccessor
}
func getNamespaceName(namespace, name string) namespacedName {
return namespacedName{
namespace: namespace,

View File

@ -18,6 +18,7 @@ package validatingadmissionpolicy
import (
"context"
"k8s.io/apiserver/pkg/admission"
)

View File

@ -17,34 +17,73 @@ limitations under the License.
package validatingadmissionpolicy
import (
"context"
celgo "github.com/google/cel-go/cel"
"k8s.io/api/admissionregistration/v1alpha1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/cel"
)
// Validator defines the func used to validate the cel expressions
// matchKind provides the GroupVersionKind that the object should be
// validated by CEL expressions as.
type Validator interface {
Validate(a admission.Attributes, o admission.ObjectInterfaces, versionedParams runtime.Object, matchKind schema.GroupVersionKind) ([]policyDecision, error)
var _ cel.ExpressionAccessor = &ValidationCondition{}
// ValidationCondition contains the inputs needed to compile, evaluate and validate a cel expression
type ValidationCondition struct {
Expression string
Message string
Reason *metav1.StatusReason
}
// ValidatorCompiler is Dependency Injected into the PolicyDefinition's `Compile`
// function to assist with converting types and values to/from CEL-typed values.
type ValidatorCompiler interface {
func (v *ValidationCondition) GetExpression() string {
return v.Expression
}
func (v *ValidationCondition) ReturnTypes() []*celgo.Type {
return []*celgo.Type{celgo.BoolType}
}
// AuditAnnotationCondition contains the inputs needed to compile, evaluate and publish a cel audit annotation
type AuditAnnotationCondition struct {
Key string
ValueExpression string
}
func (v *AuditAnnotationCondition) GetExpression() string {
return v.ValueExpression
}
func (v *AuditAnnotationCondition) ReturnTypes() []*celgo.Type {
return []*celgo.Type{celgo.StringType, celgo.NullType}
}
// Matcher is used for matching ValidatingAdmissionPolicy and ValidatingAdmissionPolicyBinding to attributes
type Matcher interface {
admission.InitializationValidator
// Matches says whether this policy definition matches the provided admission
// DefinitionMatches says whether this policy definition matches the provided admission
// resource request
DefinitionMatches(a admission.Attributes, o admission.ObjectInterfaces, definition *v1alpha1.ValidatingAdmissionPolicy) (bool, schema.GroupVersionKind, error)
// Matches says whether this policy definition matches the provided admission
// BindingMatches says whether this policy definition matches the provided admission
// resource request
BindingMatches(a admission.Attributes, o admission.ObjectInterfaces, definition *v1alpha1.ValidatingAdmissionPolicyBinding) (bool, error)
// Compile is used for the cel expression compilation
Compile(
policy *v1alpha1.ValidatingAdmissionPolicy,
) Validator
}
// ValidateResult defines the result of a Validator.Validate operation.
type ValidateResult struct {
// Decisions specifies the outcome of the validation as well as the details about the decision.
Decisions []PolicyDecision
// AuditAnnotations specifies the audit annotations that should be recorded for the validation.
AuditAnnotations []PolicyAuditAnnotation
}
// Validator is contains logic for converting ValidationEvaluation to PolicyDecisions
type Validator interface {
// Validate is used to take cel evaluations and convert into decisions
// runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input.
Validate(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) ValidateResult
}

View File

@ -21,6 +21,7 @@ import (
"errors"
"fmt"
"sync"
"sync/atomic"
"time"
kerrors "k8s.io/apimachinery/pkg/api/errors"
@ -30,6 +31,7 @@ import (
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/tools/cache/synctrack"
"k8s.io/client-go/util/workqueue"
"k8s.io/klog/v2"
)
@ -45,6 +47,11 @@ type controller[T runtime.Object] struct {
reconciler func(namespace, name string, newObj T) error
options ControllerOptions
// must hold a func() bool or nil
notificationsDelivered atomic.Value
hasProcessed synctrack.AsyncTracker[string]
}
type ControllerOptions struct {
@ -69,12 +76,20 @@ func NewController[T runtime.Object](
options.Name = fmt.Sprintf("%T-controller", *new(T))
}
return &controller[T]{
c := &controller[T]{
options: options,
informer: informer,
reconciler: reconciler,
queue: nil,
}
c.hasProcessed.UpstreamHasSynced = func() bool {
f := c.notificationsDelivered.Load()
if f == nil {
return false
}
return f.(func() bool)()
}
return c
}
// Runs the controller and returns an error explaining why running was stopped.
@ -92,20 +107,22 @@ func (c *controller[T]) Run(ctx context.Context) error {
// would never shut down the workqueue
defer c.queue.ShutDown()
enqueue := func(obj interface{}) {
enqueue := func(obj interface{}, isInInitialList bool) {
var key string
var err error
if key, err = cache.DeletionHandlingMetaNamespaceKeyFunc(obj); err != nil {
utilruntime.HandleError(err)
return
}
if isInInitialList {
c.hasProcessed.Start(key)
}
c.queue.Add(key)
}
registration, err := c.informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
enqueue(obj)
},
registration, err := c.informer.AddEventHandler(cache.ResourceEventHandlerDetailedFuncs{
AddFunc: enqueue,
UpdateFunc: func(oldObj, newObj interface{}) {
oldMeta, err1 := meta.Accessor(oldObj)
newMeta, err2 := meta.Accessor(newObj)
@ -126,11 +143,11 @@ func (c *controller[T]) Run(ctx context.Context) error {
return
}
enqueue(newObj)
enqueue(newObj, false)
},
DeleteFunc: func(obj interface{}) {
// Enqueue
enqueue(obj)
enqueue(obj, false)
},
})
@ -139,9 +156,12 @@ func (c *controller[T]) Run(ctx context.Context) error {
return err
}
c.notificationsDelivered.Store(registration.HasSynced)
// Make sure event handler is removed from informer in case return early from
// an error
defer func() {
c.notificationsDelivered.Store(func() bool { return false })
// Remove event handler and Handle Error here. Error should only be raised
// for improper usage of event handler API.
if err := c.informer.RemoveEventHandler(registration); err != nil {
@ -166,8 +186,8 @@ func (c *controller[T]) Run(ctx context.Context) error {
for i := uint(0); i < c.options.Workers; i++ {
waitGroup.Add(1)
go func() {
defer waitGroup.Done()
wait.Until(c.runWorker, time.Second, ctx.Done())
waitGroup.Done()
}()
}
@ -188,7 +208,7 @@ func (c *controller[T]) Run(ctx context.Context) error {
}
func (c *controller[T]) HasSynced() bool {
return c.informer.HasSynced()
return c.hasProcessed.HasSynced()
}
func (c *controller[T]) runWorker() {
@ -220,6 +240,7 @@ func (c *controller[T]) runWorker() {
// but the key is invalid so there is no point in doing that)
return fmt.Errorf("expected string in workqueue but got %#v", obj)
}
defer c.hasProcessed.Finished(key)
if err := c.reconcile(key); err != nil {
// Put the item back on the workqueue to handle any transient errors.

View File

@ -0,0 +1,78 @@
/*
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 validatingadmissionpolicy
import (
"k8s.io/api/admissionregistration/v1alpha1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/matching"
)
var _ matching.MatchCriteria = &matchCriteria{}
type matchCriteria struct {
constraints *v1alpha1.MatchResources
}
// GetParsedNamespaceSelector returns the converted LabelSelector which implements labels.Selector
func (m *matchCriteria) GetParsedNamespaceSelector() (labels.Selector, error) {
return metav1.LabelSelectorAsSelector(m.constraints.NamespaceSelector)
}
// GetParsedObjectSelector returns the converted LabelSelector which implements labels.Selector
func (m *matchCriteria) GetParsedObjectSelector() (labels.Selector, error) {
return metav1.LabelSelectorAsSelector(m.constraints.ObjectSelector)
}
// GetMatchResources returns the matchConstraints
func (m *matchCriteria) GetMatchResources() v1alpha1.MatchResources {
return *m.constraints
}
type matcher struct {
Matcher *matching.Matcher
}
func NewMatcher(m *matching.Matcher) Matcher {
return &matcher{
Matcher: m,
}
}
// ValidateInitialization checks if Matcher is initialized.
func (c *matcher) ValidateInitialization() error {
return c.Matcher.ValidateInitialization()
}
// DefinitionMatches returns whether this ValidatingAdmissionPolicy matches the provided admission resource request
func (c *matcher) DefinitionMatches(a admission.Attributes, o admission.ObjectInterfaces, definition *v1alpha1.ValidatingAdmissionPolicy) (bool, schema.GroupVersionKind, error) {
criteria := matchCriteria{constraints: definition.Spec.MatchConstraints}
return c.Matcher.Matches(a, o, &criteria)
}
// BindingMatches returns whether this ValidatingAdmissionPolicyBinding matches the provided admission resource request
func (c *matcher) BindingMatches(a admission.Attributes, o admission.ObjectInterfaces, binding *v1alpha1.ValidatingAdmissionPolicyBinding) (bool, error) {
if binding.Spec.MatchResources == nil {
return true, nil
}
criteria := matchCriteria{constraints: binding.Spec.MatchResources}
isMatch, _, err := c.Matcher.Matches(a, o, &criteria)
return isMatch, err
}

View File

@ -0,0 +1,36 @@
/*
Copyright 2023 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 validatingadmissionpolicy
import (
celgo "github.com/google/cel-go/cel"
"k8s.io/apiserver/pkg/admission/plugin/cel"
)
var _ cel.ExpressionAccessor = (*MessageExpressionCondition)(nil)
type MessageExpressionCondition struct {
MessageExpression string
}
func (m *MessageExpressionCondition) GetExpression() string {
return m.MessageExpression
}
func (m *MessageExpressionCondition) ReturnTypes() []*celgo.Type {
return []*celgo.Type{celgo.StringType}
}

View File

@ -20,37 +20,54 @@ import (
"net/http"
"time"
"k8s.io/api/admissionregistration/v1alpha1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type policyDecisionAction string
type PolicyDecisionAction string
const (
actionAdmit policyDecisionAction = "admit"
actionDeny policyDecisionAction = "deny"
ActionAdmit PolicyDecisionAction = "admit"
ActionDeny PolicyDecisionAction = "deny"
)
type policyDecisionEvaluation string
type PolicyDecisionEvaluation string
const (
evalAdmit policyDecisionEvaluation = "admit"
evalError policyDecisionEvaluation = "error"
evalDeny policyDecisionEvaluation = "deny"
EvalAdmit PolicyDecisionEvaluation = "admit"
EvalError PolicyDecisionEvaluation = "error"
EvalDeny PolicyDecisionEvaluation = "deny"
)
type policyDecision struct {
action policyDecisionAction
evaluation policyDecisionEvaluation
message string
reason metav1.StatusReason
elapsed time.Duration
// PolicyDecision contains the action determined from a cel evaluation along with metadata such as message, reason and duration
type PolicyDecision struct {
Action PolicyDecisionAction
Evaluation PolicyDecisionEvaluation
Message string
Reason metav1.StatusReason
Elapsed time.Duration
}
type policyDecisionWithMetadata struct {
policyDecision
definition *v1alpha1.ValidatingAdmissionPolicy
binding *v1alpha1.ValidatingAdmissionPolicyBinding
type PolicyAuditAnnotationAction string
const (
// AuditAnnotationActionPublish indicates that the audit annotation should be
// published with the audit event.
AuditAnnotationActionPublish PolicyAuditAnnotationAction = "publish"
// AuditAnnotationActionError indicates that the valueExpression resulted
// in an error.
AuditAnnotationActionError PolicyAuditAnnotationAction = "error"
// AuditAnnotationActionExclude indicates that the audit annotation should be excluded
// because the valueExpression evaluated to null, or because FailurePolicy is Ignore
// and the expression failed with a parse error, type check error, or runtime error.
AuditAnnotationActionExclude PolicyAuditAnnotationAction = "exclude"
)
type PolicyAuditAnnotation struct {
Key string
Value string
Elapsed time.Duration
Action PolicyAuditAnnotationAction
Error string
}
func reasonToCode(r metav1.StatusReason) int32 {

View File

@ -0,0 +1,435 @@
/*
Copyright 2023 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 validatingadmissionpolicy
import (
"errors"
"fmt"
"sort"
"strings"
"sync"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types/ref"
"k8s.io/api/admissionregistration/v1alpha1"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation/field"
plugincel "k8s.io/apiserver/pkg/admission/plugin/cel"
apiservercel "k8s.io/apiserver/pkg/cel"
"k8s.io/apiserver/pkg/cel/common"
"k8s.io/apiserver/pkg/cel/library"
"k8s.io/apiserver/pkg/cel/openapi"
"k8s.io/apiserver/pkg/cel/openapi/resolver"
"k8s.io/klog/v2"
)
const maxTypesToCheck = 10
type TypeChecker struct {
schemaResolver resolver.SchemaResolver
restMapper meta.RESTMapper
}
type typeOverwrite struct {
object *apiservercel.DeclType
params *apiservercel.DeclType
}
// typeCheckingResult holds the issues found during type checking, any returned
// error, and the gvk that the type checking is performed against.
type typeCheckingResult struct {
gvk schema.GroupVersionKind
issues *cel.Issues
err error
}
// Check preforms the type check against the given policy, and format the result
// as []ExpressionWarning that is ready to be set in policy.Status
// The result is nil if type checking returns no warning.
// The policy object is NOT mutated. The caller should update Status accordingly
func (c *TypeChecker) Check(policy *v1alpha1.ValidatingAdmissionPolicy) []v1alpha1.ExpressionWarning {
exps := make([]string, 0, len(policy.Spec.Validations))
// check main validation expressions, located in spec.validations[*]
fieldRef := field.NewPath("spec", "validations")
for _, v := range policy.Spec.Validations {
exps = append(exps, v.Expression)
}
msgs := c.CheckExpressions(exps, policy.Spec.ParamKind != nil, policy)
var results []v1alpha1.ExpressionWarning // intentionally not setting capacity
for i, msg := range msgs {
if msg != "" {
results = append(results, v1alpha1.ExpressionWarning{
FieldRef: fieldRef.Index(i).Child("expression").String(),
Warning: msg,
})
}
}
return results
}
// CheckExpressions checks a set of compiled CEL programs against the GVKs defined in
// policy.Spec.MatchConstraints
// The result is a human-readable form that describe which expressions
// violate what types at what place. The indexes of the return []string
// matches these of the input expressions.
// TODO: It is much more useful to have machine-readable output and let the
// client format it. That requires an update to the KEP, probably in coming
// releases.
func (c *TypeChecker) CheckExpressions(expressions []string, hasParams bool, policy *v1alpha1.ValidatingAdmissionPolicy) []string {
var allWarnings []string
allGvks := c.typesToCheck(policy)
gvks := make([]schema.GroupVersionKind, 0, len(allGvks))
schemas := make([]common.Schema, 0, len(allGvks))
for _, gvk := range allGvks {
s, err := c.schemaResolver.ResolveSchema(gvk)
if err != nil {
// type checking errors MUST NOT alter the behavior of the policy
// even if an error occurs.
if !errors.Is(err, resolver.ErrSchemaNotFound) {
// Anything except ErrSchemaNotFound is an internal error
klog.ErrorS(err, "internal error: schema resolution failure", "gvk", gvk)
}
// skip if an unrecoverable error occurs.
continue
}
gvks = append(gvks, gvk)
schemas = append(schemas, &openapi.Schema{Schema: s})
}
paramsType := c.paramsType(policy)
paramsDeclType, err := c.declType(paramsType)
if err != nil {
if !errors.Is(err, resolver.ErrSchemaNotFound) {
klog.V(2).ErrorS(err, "cannot resolve schema for params", "gvk", paramsType)
}
paramsDeclType = nil
}
for _, exp := range expressions {
var results []typeCheckingResult
for i, gvk := range gvks {
s := schemas[i]
issues, err := c.checkExpression(exp, hasParams, typeOverwrite{
object: common.SchemaDeclType(s, true),
params: paramsDeclType,
})
// save even if no issues are found, for the sake of formatting.
results = append(results, typeCheckingResult{
gvk: gvk,
issues: issues,
err: err,
})
}
allWarnings = append(allWarnings, c.formatWarning(results))
}
return allWarnings
}
// formatWarning converts the resulting issues and possible error during
// type checking into a human-readable string
func (c *TypeChecker) formatWarning(results []typeCheckingResult) string {
var sb strings.Builder
for _, result := range results {
if result.issues == nil && result.err == nil {
continue
}
if result.err != nil {
sb.WriteString(fmt.Sprintf("%v: type checking error: %v\n", result.gvk, result.err))
} else {
sb.WriteString(fmt.Sprintf("%v: %s\n", result.gvk, result.issues))
}
}
return strings.TrimSuffix(sb.String(), "\n")
}
func (c *TypeChecker) declType(gvk schema.GroupVersionKind) (*apiservercel.DeclType, error) {
if gvk.Empty() {
return nil, nil
}
s, err := c.schemaResolver.ResolveSchema(gvk)
if err != nil {
return nil, err
}
return common.SchemaDeclType(&openapi.Schema{Schema: s}, true), nil
}
func (c *TypeChecker) paramsType(policy *v1alpha1.ValidatingAdmissionPolicy) schema.GroupVersionKind {
if policy.Spec.ParamKind == nil {
return schema.GroupVersionKind{}
}
gv, err := schema.ParseGroupVersion(policy.Spec.ParamKind.APIVersion)
if err != nil {
return schema.GroupVersionKind{}
}
return gv.WithKind(policy.Spec.ParamKind.Kind)
}
func (c *TypeChecker) checkExpression(expression string, hasParams bool, types typeOverwrite) (*cel.Issues, error) {
env, err := buildEnv(hasParams, types)
if err != nil {
return nil, err
}
// We cannot reuse an AST that is parsed by another env, so reparse it here.
// Compile = Parse + Check, we especially want the results of Check.
//
// Paradoxically, we discard the type-checked result and let the admission
// controller use the dynamic typed program.
// This is a compromise that is defined in the KEP. We can revisit this
// decision and expect a change with limited size.
_, issues := env.Compile(expression)
return issues, nil
}
// typesToCheck extracts a list of GVKs that needs type checking from the policy
// the result is sorted in the order of Group, Version, and Kind
func (c *TypeChecker) typesToCheck(p *v1alpha1.ValidatingAdmissionPolicy) []schema.GroupVersionKind {
gvks := sets.New[schema.GroupVersionKind]()
if p.Spec.MatchConstraints == nil || len(p.Spec.MatchConstraints.ResourceRules) == 0 {
return nil
}
for _, rule := range p.Spec.MatchConstraints.ResourceRules {
groups := extractGroups(&rule.Rule)
if len(groups) == 0 {
continue
}
versions := extractVersions(&rule.Rule)
if len(versions) == 0 {
continue
}
resources := extractResources(&rule.Rule)
if len(resources) == 0 {
continue
}
// sort GVRs so that the loop below provides
// consistent results.
sort.Strings(groups)
sort.Strings(versions)
sort.Strings(resources)
count := 0
for _, group := range groups {
for _, version := range versions {
for _, resource := range resources {
gvr := schema.GroupVersionResource{
Group: group,
Version: version,
Resource: resource,
}
resolved, err := c.restMapper.KindsFor(gvr)
if err != nil {
continue
}
for _, r := range resolved {
if !r.Empty() {
gvks.Insert(r)
count++
// early return if maximum number of types are already
// collected
if count == maxTypesToCheck {
if gvks.Len() == 0 {
return nil
}
return sortGVKList(gvks.UnsortedList())
}
}
}
}
}
}
}
if gvks.Len() == 0 {
return nil
}
return sortGVKList(gvks.UnsortedList())
}
func extractGroups(rule *v1alpha1.Rule) []string {
groups := make([]string, 0, len(rule.APIGroups))
for _, group := range rule.APIGroups {
// give up if wildcard
if strings.ContainsAny(group, "*") {
return nil
}
groups = append(groups, group)
}
return groups
}
func extractVersions(rule *v1alpha1.Rule) []string {
versions := make([]string, 0, len(rule.APIVersions))
for _, version := range rule.APIVersions {
if strings.ContainsAny(version, "*") {
return nil
}
versions = append(versions, version)
}
return versions
}
func extractResources(rule *v1alpha1.Rule) []string {
resources := make([]string, 0, len(rule.Resources))
for _, resource := range rule.Resources {
// skip wildcard and subresources
if strings.ContainsAny(resource, "*/") {
continue
}
resources = append(resources, resource)
}
return resources
}
// sortGVKList sorts the list by Group, Version, and Kind
// returns the list itself.
func sortGVKList(list []schema.GroupVersionKind) []schema.GroupVersionKind {
sort.Slice(list, func(i, j int) bool {
if g := strings.Compare(list[i].Group, list[j].Group); g != 0 {
return g < 0
}
if v := strings.Compare(list[i].Version, list[j].Version); v != 0 {
return v < 0
}
return strings.Compare(list[i].Kind, list[j].Kind) < 0
})
return list
}
func buildEnv(hasParams bool, types typeOverwrite) (*cel.Env, error) {
baseEnv, err := getBaseEnv()
if err != nil {
return nil, err
}
reg := apiservercel.NewRegistry(baseEnv)
requestType := plugincel.BuildRequestType()
var varOpts []cel.EnvOption
var rts []*apiservercel.RuleTypes
// request, hand-crafted type
rt, opts, err := createRuleTypesAndOptions(reg, requestType, plugincel.RequestVarName)
if err != nil {
return nil, err
}
rts = append(rts, rt)
varOpts = append(varOpts, opts...)
// object and oldObject, same type, type(s) resolved from constraints
rt, opts, err = createRuleTypesAndOptions(reg, types.object, plugincel.ObjectVarName, plugincel.OldObjectVarName)
if err != nil {
return nil, err
}
rts = append(rts, rt)
varOpts = append(varOpts, opts...)
// params, defined by ParamKind
if hasParams {
rt, opts, err := createRuleTypesAndOptions(reg, types.params, plugincel.ParamsVarName)
if err != nil {
return nil, err
}
rts = append(rts, rt)
varOpts = append(varOpts, opts...)
}
opts, err = ruleTypesOpts(rts, baseEnv.TypeProvider())
if err != nil {
return nil, err
}
opts = append(opts, varOpts...) // add variables after ruleTypes.
env, err := baseEnv.Extend(opts...)
if err != nil {
return nil, err
}
return env, nil
}
// createRuleTypeAndOptions creates the cel RuleTypes and a slice of EnvOption
// that can be used for creating a CEL env containing variables of declType.
// declType can be nil, in which case the variables will be of DynType.
func createRuleTypesAndOptions(registry *apiservercel.Registry, declType *apiservercel.DeclType, variables ...string) (*apiservercel.RuleTypes, []cel.EnvOption, error) {
opts := make([]cel.EnvOption, 0, len(variables))
// untyped, use DynType
if declType == nil {
for _, v := range variables {
opts = append(opts, cel.Variable(v, cel.DynType))
}
return nil, opts, nil
}
// create a RuleType for the given type
rt, err := apiservercel.NewRuleTypes(declType.TypeName(), declType, registry)
if err != nil {
return nil, nil, err
}
if rt == nil {
return nil, nil, nil
}
for _, v := range variables {
opts = append(opts, cel.Variable(v, declType.CelType()))
}
return rt, opts, nil
}
func ruleTypesOpts(ruleTypes []*apiservercel.RuleTypes, underlyingTypeProvider ref.TypeProvider) ([]cel.EnvOption, error) {
var providers []ref.TypeProvider // may be unused, too small to matter
var adapters []ref.TypeAdapter
for _, rt := range ruleTypes {
if rt != nil {
withTP, err := rt.WithTypeProvider(underlyingTypeProvider)
if err != nil {
return nil, err
}
providers = append(providers, withTP)
adapters = append(adapters, withTP)
}
}
var tp ref.TypeProvider
var ta ref.TypeAdapter
switch len(providers) {
case 0:
return nil, nil
case 1:
tp = providers[0]
ta = adapters[0]
default:
tp = &apiservercel.CompositedTypeProvider{Providers: providers}
ta = &apiservercel.CompositedTypeAdapter{Adapters: adapters}
}
return []cel.EnvOption{cel.CustomTypeProvider(tp), cel.CustomTypeAdapter(ta)}, nil
}
func getBaseEnv() (*cel.Env, error) {
typeCheckingBaseEnvInit.Do(func() {
var opts []cel.EnvOption
opts = append(opts, cel.HomogeneousAggregateLiterals())
// Validate function declarations once during base env initialization,
// so they don't need to be evaluated each time a CEL rule is compiled.
// This is a relatively expensive operation.
opts = append(opts, cel.EagerlyValidateDeclarations(true), cel.DefaultUTCTimeZone(true))
opts = append(opts, library.ExtensionLibs...)
typeCheckingBaseEnv, typeCheckingBaseEnvError = cel.NewEnv(opts...)
})
return typeCheckingBaseEnv, typeCheckingBaseEnvError
}
var typeCheckingBaseEnv *cel.Env
var typeCheckingBaseEnvError error
var typeCheckingBaseEnvInit sync.Once

View File

@ -17,302 +17,232 @@ limitations under the License.
package validatingadmissionpolicy
import (
"context"
"fmt"
"reflect"
"strings"
"time"
celtypes "github.com/google/cel-go/common/types"
"github.com/google/cel-go/interpreter"
admissionv1 "k8s.io/api/admission/v1"
"k8s.io/api/admissionregistration/v1alpha1"
authenticationv1 "k8s.io/api/authentication/v1"
v1 "k8s.io/api/admissionregistration/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/matching"
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
"k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions"
celconfig "k8s.io/apiserver/pkg/apis/cel"
"k8s.io/apiserver/pkg/authorization/authorizer"
apiservercel "k8s.io/apiserver/pkg/cel"
"k8s.io/klog/v2"
)
var _ ValidatorCompiler = &CELValidatorCompiler{}
var _ matching.MatchCriteria = &matchCriteria{}
type matchCriteria struct {
constraints *v1alpha1.MatchResources
// validator implements the Validator interface
type validator struct {
celMatcher matchconditions.Matcher
validationFilter cel.Filter
auditAnnotationFilter cel.Filter
messageFilter cel.Filter
failPolicy *v1.FailurePolicyType
authorizer authorizer.Authorizer
}
// GetParsedNamespaceSelector returns the converted LabelSelector which implements labels.Selector
func (m *matchCriteria) GetParsedNamespaceSelector() (labels.Selector, error) {
return metav1.LabelSelectorAsSelector(m.constraints.NamespaceSelector)
}
// GetParsedObjectSelector returns the converted LabelSelector which implements labels.Selector
func (m *matchCriteria) GetParsedObjectSelector() (labels.Selector, error) {
return metav1.LabelSelectorAsSelector(m.constraints.ObjectSelector)
}
// GetMatchResources returns the matchConstraints
func (m *matchCriteria) GetMatchResources() v1alpha1.MatchResources {
return *m.constraints
}
// CELValidatorCompiler implement the interface ValidatorCompiler.
type CELValidatorCompiler struct {
Matcher *matching.Matcher
}
// DefinitionMatches returns whether this ValidatingAdmissionPolicy matches the provided admission resource request
func (c *CELValidatorCompiler) DefinitionMatches(a admission.Attributes, o admission.ObjectInterfaces, definition *v1alpha1.ValidatingAdmissionPolicy) (bool, schema.GroupVersionKind, error) {
criteria := matchCriteria{constraints: definition.Spec.MatchConstraints}
return c.Matcher.Matches(a, o, &criteria)
}
// BindingMatches returns whether this ValidatingAdmissionPolicyBinding matches the provided admission resource request
func (c *CELValidatorCompiler) BindingMatches(a admission.Attributes, o admission.ObjectInterfaces, binding *v1alpha1.ValidatingAdmissionPolicyBinding) (bool, error) {
if binding.Spec.MatchResources == nil {
return true, nil
}
criteria := matchCriteria{constraints: binding.Spec.MatchResources}
isMatch, _, err := c.Matcher.Matches(a, o, &criteria)
return isMatch, err
}
// ValidateInitialization checks if Matcher is initialized.
func (c *CELValidatorCompiler) ValidateInitialization() error {
return c.Matcher.ValidateInitialization()
}
type validationActivation struct {
object, oldObject, params, request interface{}
}
// ResolveName returns a value from the activation by qualified name, or false if the name
// could not be found.
func (a *validationActivation) 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
case RequestVarName:
return a.request, true
default:
return nil, false
func NewValidator(validationFilter cel.Filter, celMatcher matchconditions.Matcher, auditAnnotationFilter, messageFilter cel.Filter, failPolicy *v1.FailurePolicyType, authorizer authorizer.Authorizer) Validator {
return &validator{
celMatcher: celMatcher,
validationFilter: validationFilter,
auditAnnotationFilter: auditAnnotationFilter,
messageFilter: messageFilter,
failPolicy: failPolicy,
authorizer: authorizer,
}
}
// Parent returns the parent of the current activation, may be nil.
// If non-nil, the parent will be searched during resolve calls.
func (a *validationActivation) Parent() interpreter.Activation {
return nil
func policyDecisionActionForError(f v1.FailurePolicyType) PolicyDecisionAction {
if f == v1.Ignore {
return ActionAdmit
}
return ActionDeny
}
// Compile compiles the cel expression defined in ValidatingAdmissionPolicy
func (c *CELValidatorCompiler) Compile(p *v1alpha1.ValidatingAdmissionPolicy) Validator {
if len(p.Spec.Validations) == 0 {
return nil
func auditAnnotationEvaluationForError(f v1.FailurePolicyType) PolicyAuditAnnotationAction {
if f == v1.Ignore {
return AuditAnnotationActionExclude
}
hasParam := false
if p.Spec.ParamKind != nil {
hasParam = true
}
compilationResults := make([]CompilationResult, len(p.Spec.Validations))
for i, validation := range p.Spec.Validations {
compilationResults[i] = CompileValidatingPolicyExpression(validation.Expression, hasParam)
}
return &CELValidator{policy: p, compilationResults: compilationResults}
return AuditAnnotationActionError
}
// CELValidator implements the Validator interface
type CELValidator struct {
policy *v1alpha1.ValidatingAdmissionPolicy
compilationResults []CompilationResult
}
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
}
func policyDecisionActionForError(f v1alpha1.FailurePolicyType) policyDecisionAction {
if f == v1alpha1.Ignore {
return actionAdmit
}
return actionDeny
}
// Validate validates all cel expressions in Validator and returns a PolicyDecision for each CEL expression or returns an error.
// An error will be returned if failed to convert the object/oldObject/params/request to unstructured.
// Each PolicyDecision will have a decision and a message.
// policyDecision.message will be empty if the decision is allowed and no error met.
func (v *CELValidator) Validate(a admission.Attributes, o admission.ObjectInterfaces, versionedParams runtime.Object, matchKind schema.GroupVersionKind) ([]policyDecision, error) {
// TODO: replace unstructured with ref.Val for CEL variables when native type support is available
decisions := make([]policyDecision, len(v.compilationResults))
var err error
versionedAttr, err := generic.NewVersionedAttributes(a, matchKind, o)
if err != nil {
return nil, err
}
oldObjectVal, err := objectToResolveVal(versionedAttr.VersionedOldObject)
if err != nil {
return nil, err
}
objectVal, err := objectToResolveVal(versionedAttr.VersionedObject)
if err != nil {
return nil, err
}
paramsVal, err := objectToResolveVal(versionedParams)
if err != nil {
return nil, err
}
request := createAdmissionRequest(versionedAttr.Attributes)
requestVal, err := convertObjectToUnstructured(request)
if err != nil {
return nil, err
}
va := &validationActivation{
object: objectVal,
oldObject: oldObjectVal,
params: paramsVal,
request: requestVal.Object,
}
var f v1alpha1.FailurePolicyType
if v.policy.Spec.FailurePolicy == nil {
f = v1alpha1.Fail
// Validate takes a list of Evaluation and a failure policy and converts them into actionable PolicyDecisions
// 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 (v *validator) Validate(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) ValidateResult {
var f v1.FailurePolicyType
if v.failPolicy == nil {
f = v1.Fail
} else {
f = *v.policy.Spec.FailurePolicy
f = *v.failPolicy
}
for i, compilationResult := range v.compilationResults {
validation := v.policy.Spec.Validations[i]
if v.celMatcher != nil {
matchResults := v.celMatcher.Match(ctx, versionedAttr, versionedParams)
if matchResults.Error != nil {
return ValidateResult{
Decisions: []PolicyDecision{
{
Action: policyDecisionActionForError(f),
Evaluation: EvalError,
Message: matchResults.Error.Error(),
},
},
}
}
var policyDecision = &decisions[i]
// if preconditions are not met, then do not return any validations
if !matchResults.Matches {
return ValidateResult{}
}
}
if compilationResult.Error != nil {
policyDecision.action = policyDecisionActionForError(f)
policyDecision.evaluation = evalError
policyDecision.message = fmt.Sprintf("compilation error: %v", compilationResult.Error)
optionalVars := cel.OptionalVariableBindings{VersionedParams: versionedParams, Authorizer: v.authorizer}
expressionOptionalVars := cel.OptionalVariableBindings{VersionedParams: versionedParams}
admissionRequest := cel.CreateAdmissionRequest(versionedAttr.Attributes)
evalResults, remainingBudget, err := v.validationFilter.ForInput(ctx, versionedAttr, admissionRequest, optionalVars, runtimeCELCostBudget)
if err != nil {
return ValidateResult{
Decisions: []PolicyDecision{
{
Action: policyDecisionActionForError(f),
Evaluation: EvalError,
Message: err.Error(),
},
},
}
}
decisions := make([]PolicyDecision, len(evalResults))
messageResults, _, err := v.messageFilter.ForInput(ctx, versionedAttr, admissionRequest, expressionOptionalVars, remainingBudget)
for i, evalResult := range evalResults {
var decision = &decisions[i]
// TODO: move this to generics
validation, ok := evalResult.ExpressionAccessor.(*ValidationCondition)
if !ok {
klog.Error("Invalid type conversion to ValidationCondition")
decision.Action = policyDecisionActionForError(f)
decision.Evaluation = EvalError
decision.Message = "Invalid type sent to validator, expected ValidationCondition"
continue
}
if compilationResult.Program == nil {
policyDecision.action = policyDecisionActionForError(f)
policyDecision.evaluation = evalError
policyDecision.message = "unexpected internal error compiling expression"
continue
var messageResult *cel.EvaluationResult
var messageError *apiservercel.Error
if len(messageResults) > i {
messageResult = &messageResults[i]
}
t1 := time.Now()
evalResult, _, err := compilationResult.Program.Eval(va)
elapsed := time.Since(t1)
policyDecision.elapsed = elapsed
if err != nil {
policyDecision.action = policyDecisionActionForError(f)
policyDecision.evaluation = evalError
policyDecision.message = fmt.Sprintf("expression '%v' resulted in error: %v", v.policy.Spec.Validations[i].Expression, err)
} else if evalResult != celtypes.True {
policyDecision.action = actionDeny
messageError, _ = err.(*apiservercel.Error)
if evalResult.Error != nil {
decision.Action = policyDecisionActionForError(f)
decision.Evaluation = EvalError
decision.Message = evalResult.Error.Error()
} else if messageError != nil &&
(messageError.Type == apiservercel.ErrorTypeInternal ||
(messageError.Type == apiservercel.ErrorTypeInvalid &&
strings.HasPrefix(messageError.Detail, "validation failed due to running out of cost budget"))) {
decision.Action = policyDecisionActionForError(f)
decision.Evaluation = EvalError
decision.Message = fmt.Sprintf("failed messageExpression: %s", err)
} else if evalResult.EvalResult != celtypes.True {
decision.Action = ActionDeny
if validation.Reason == nil {
policyDecision.reason = metav1.StatusReasonInvalid
decision.Reason = metav1.StatusReasonInvalid
} else {
policyDecision.reason = *validation.Reason
decision.Reason = *validation.Reason
}
if len(validation.Message) > 0 {
policyDecision.message = strings.TrimSpace(validation.Message)
} else {
policyDecision.message = fmt.Sprintf("failed expression: %v", strings.TrimSpace(validation.Expression))
// decide the failure message
var message string
// attempt to set message with messageExpression result
if messageResult != nil && messageResult.Error == nil && messageResult.EvalResult != nil {
// also fallback if the eval result is non-string (including null) or
// whitespaces.
if message, ok = messageResult.EvalResult.Value().(string); ok {
message = strings.TrimSpace(message)
// deny excessively long message from EvalResult
if len(message) > celconfig.MaxEvaluatedMessageExpressionSizeBytes {
klog.V(2).InfoS("excessively long message denied", "message", message)
message = ""
}
// deny message that contains newlines
if strings.ContainsAny(message, "\n") {
klog.V(2).InfoS("multi-line message denied", "message", message)
message = ""
}
}
}
if messageResult != nil && messageResult.Error != nil {
// log any error with messageExpression
klog.V(2).ErrorS(messageResult.Error, "error while evaluating messageExpression")
}
// fallback to set message to the custom message
if message == "" && len(validation.Message) > 0 {
message = strings.TrimSpace(validation.Message)
}
// fallback to use the expression to compose a message
if message == "" {
message = fmt.Sprintf("failed expression: %v", strings.TrimSpace(validation.Expression))
}
decision.Message = message
} else {
policyDecision.action = actionAdmit
policyDecision.evaluation = evalAdmit
decision.Action = ActionAdmit
decision.Evaluation = EvalAdmit
}
}
return decisions, nil
}
func createAdmissionRequest(attr admission.Attributes) *admissionv1.AdmissionRequest {
// FIXME: how to get resource GVK, GVR and subresource?
gvk := attr.GetKind()
gvr := attr.GetResource()
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(),
},
}
options := cel.OptionalVariableBindings{VersionedParams: versionedParams}
auditAnnotationEvalResults, _, err := v.auditAnnotationFilter.ForInput(ctx, versionedAttr, cel.CreateAdmissionRequest(versionedAttr.Attributes), options, runtimeCELCostBudget)
if err != nil {
return ValidateResult{
Decisions: []PolicyDecision{
{
Action: policyDecisionActionForError(f),
Evaluation: EvalError,
Message: err.Error(),
},
},
}
}
auditAnnotationResults := make([]PolicyAuditAnnotation, len(auditAnnotationEvalResults))
for i, evalResult := range auditAnnotationEvalResults {
if evalResult.ExpressionAccessor == nil {
continue
}
var auditAnnotationResult = &auditAnnotationResults[i]
// TODO: move this to generics
validation, ok := evalResult.ExpressionAccessor.(*AuditAnnotationCondition)
if !ok {
klog.Error("Invalid type conversion to AuditAnnotationCondition")
auditAnnotationResult.Action = auditAnnotationEvaluationForError(f)
auditAnnotationResult.Error = fmt.Sprintf("Invalid type sent to validator, expected AuditAnnotationCondition but got %T", evalResult.ExpressionAccessor)
continue
}
auditAnnotationResult.Key = validation.Key
if evalResult.Error != nil {
auditAnnotationResult.Action = auditAnnotationEvaluationForError(f)
auditAnnotationResult.Error = evalResult.Error.Error()
} else {
switch evalResult.EvalResult.Type() {
case celtypes.StringType:
value := strings.TrimSpace(evalResult.EvalResult.Value().(string))
if len(value) == 0 {
auditAnnotationResult.Action = AuditAnnotationActionExclude
} else {
auditAnnotationResult.Action = AuditAnnotationActionPublish
auditAnnotationResult.Value = value
}
case celtypes.NullType:
auditAnnotationResult.Action = AuditAnnotationActionExclude
default:
auditAnnotationResult.Action = AuditAnnotationActionError
auditAnnotationResult.Error = fmt.Sprintf("valueExpression '%v' resulted in unsupported return type: %v. "+
"Return type must be either string or null.", validation.ValueExpression, evalResult.EvalResult.Type())
}
}
}
return ValidateResult{Decisions: decisions, AuditAnnotations: auditAnnotationResults}
}

View File

@ -19,11 +19,15 @@ package webhook
import (
"sync"
"k8s.io/api/admissionregistration/v1"
v1 "k8s.io/api/admissionregistration/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions"
"k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/namespace"
"k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/object"
celconfig "k8s.io/apiserver/pkg/apis/cel"
"k8s.io/apiserver/pkg/authorization/authorizer"
webhookutil "k8s.io/apiserver/pkg/util/webhook"
"k8s.io/client-go/rest"
)
@ -44,6 +48,9 @@ type WebhookAccessor interface {
// GetRESTClient gets the webhook client
GetRESTClient(clientManager *webhookutil.ClientManager) (*rest.RESTClient, error)
// GetCompiledMatcher gets the compiled matcher object
GetCompiledMatcher(compiler cel.FilterCompiler, authorizer authorizer.Authorizer) 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
// needed, use GetUID.
@ -67,6 +74,9 @@ type WebhookAccessor interface {
// GetAdmissionReviewVersions gets the webhook AdmissionReviewVersions field.
GetAdmissionReviewVersions() []string
// GetMatchConditions gets the webhook match conditions field.
GetMatchConditions() []v1.MatchCondition
// GetMutatingWebhook if the accessor contains a MutatingWebhook, returns it and true, else returns false.
GetMutatingWebhook() (*v1.MutatingWebhook, bool)
// GetValidatingWebhook if the accessor contains a ValidatingWebhook, returns it and true, else returns false.
@ -94,6 +104,9 @@ type mutatingWebhookAccessor struct {
initClient sync.Once
client *rest.RESTClient
clientErr error
compileMatcher sync.Once
compiledMatcher matchconditions.Matcher
}
func (m *mutatingWebhookAccessor) GetUID() string {
@ -111,6 +124,28 @@ func (m *mutatingWebhookAccessor) GetRESTClient(clientManager *webhookutil.Clien
return m.client, m.clientErr
}
// TODO: graduation to beta: resolve the fact that we rebuild ALL items whenever ANY config changes in NewMutatingWebhookConfigurationManager and NewValidatingWebhookConfigurationManager ... now that we're doing CEL compilation, we probably want to avoid that
func (m *mutatingWebhookAccessor) GetCompiledMatcher(compiler cel.FilterCompiler, authorizer authorizer.Authorizer) matchconditions.Matcher {
m.compileMatcher.Do(func() {
expressions := make([]cel.ExpressionAccessor, len(m.MutatingWebhook.MatchConditions))
for i, matchCondition := range m.MutatingWebhook.MatchConditions {
expressions[i] = &matchconditions.MatchCondition{
Name: matchCondition.Name,
Expression: matchCondition.Expression,
}
}
m.compiledMatcher = matchconditions.NewMatcher(compiler.Compile(
expressions,
cel.OptionalVariableDeclarations{
HasParams: false,
HasAuthorizer: true,
},
celconfig.PerCallLimit,
), authorizer, m.FailurePolicy, "validating", m.Name)
})
return m.compiledMatcher
}
func (m *mutatingWebhookAccessor) GetParsedNamespaceSelector() (labels.Selector, error) {
m.initNamespaceSelector.Do(func() {
m.namespaceSelector, m.namespaceSelectorErr = metav1.LabelSelectorAsSelector(m.NamespaceSelector)
@ -165,6 +200,10 @@ func (m *mutatingWebhookAccessor) GetAdmissionReviewVersions() []string {
return m.AdmissionReviewVersions
}
func (m *mutatingWebhookAccessor) GetMatchConditions() []v1.MatchCondition {
return m.MatchConditions
}
func (m *mutatingWebhookAccessor) GetMutatingWebhook() (*v1.MutatingWebhook, bool) {
return m.MutatingWebhook, true
}
@ -194,6 +233,9 @@ type validatingWebhookAccessor struct {
initClient sync.Once
client *rest.RESTClient
clientErr error
compileMatcher sync.Once
compiledMatcher matchconditions.Matcher
}
func (v *validatingWebhookAccessor) GetUID() string {
@ -211,6 +253,27 @@ func (v *validatingWebhookAccessor) GetRESTClient(clientManager *webhookutil.Cli
return v.client, v.clientErr
}
func (v *validatingWebhookAccessor) GetCompiledMatcher(compiler cel.FilterCompiler, authorizer authorizer.Authorizer) matchconditions.Matcher {
v.compileMatcher.Do(func() {
expressions := make([]cel.ExpressionAccessor, len(v.ValidatingWebhook.MatchConditions))
for i, matchCondition := range v.ValidatingWebhook.MatchConditions {
expressions[i] = &matchconditions.MatchCondition{
Name: matchCondition.Name,
Expression: matchCondition.Expression,
}
}
v.compiledMatcher = matchconditions.NewMatcher(compiler.Compile(
expressions,
cel.OptionalVariableDeclarations{
HasParams: false,
HasAuthorizer: true,
},
celconfig.PerCallLimit,
), authorizer, v.FailurePolicy, "validating", v.Name)
})
return v.compiledMatcher
}
func (v *validatingWebhookAccessor) GetParsedNamespaceSelector() (labels.Selector, error) {
v.initNamespaceSelector.Do(func() {
v.namespaceSelector, v.namespaceSelectorErr = metav1.LabelSelectorAsSelector(v.NamespaceSelector)
@ -265,6 +328,10 @@ func (v *validatingWebhookAccessor) GetAdmissionReviewVersions() []string {
return v.AdmissionReviewVersions
}
func (v *validatingWebhookAccessor) GetMatchConditions() []v1.MatchCondition {
return v.MatchConditions
}
func (v *validatingWebhookAccessor) GetMutatingWebhook() (*v1.MutatingWebhook, bool) {
return nil, false
}

View File

@ -19,43 +19,21 @@ package generic
import (
"context"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/webhook"
)
type VersionedAttributeAccessor interface {
VersionedAttribute(gvk schema.GroupVersionKind) (*admission.VersionedAttributes, error)
}
// Source can list dynamic webhook plugins.
type Source interface {
Webhooks() []webhook.WebhookAccessor
HasSynced() bool
}
// VersionedAttributes is a wrapper around the original admission attributes, adding versioned
// variants of the object and old object.
type VersionedAttributes struct {
// Attributes holds the original admission attributes
admission.Attributes
// VersionedOldObject holds Attributes.OldObject (if non-nil), converted to VersionedKind.
// It must never be mutated.
VersionedOldObject runtime.Object
// VersionedObject holds Attributes.Object (if non-nil), converted to VersionedKind.
// If mutated, Dirty must be set to true by the mutator.
VersionedObject runtime.Object
// VersionedKind holds the fully qualified kind
VersionedKind schema.GroupVersionKind
// Dirty indicates VersionedObject has been modified since being converted from Attributes.Object
Dirty bool
}
// GetObject overrides the Attributes.GetObject()
func (v *VersionedAttributes) GetObject() runtime.Object {
if v.VersionedObject != nil {
return v.VersionedObject
}
return v.Attributes.GetObject()
}
// WebhookInvocation describes how to call a webhook, including the resource and subresource the webhook registered for,
// and the kind that should be sent to the webhook.
type WebhookInvocation struct {

View File

@ -23,19 +23,22 @@ import (
admissionv1 "k8s.io/api/admission/v1"
admissionv1beta1 "k8s.io/api/admission/v1beta1"
"k8s.io/api/admissionregistration/v1"
v1 "k8s.io/api/admissionregistration/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
genericadmissioninit "k8s.io/apiserver/pkg/admission/initializer"
"k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/admission/plugin/webhook"
"k8s.io/apiserver/pkg/admission/plugin/webhook/config"
"k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/namespace"
"k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/object"
"k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/rules"
"k8s.io/apiserver/pkg/authorization/authorizer"
webhookutil "k8s.io/apiserver/pkg/util/webhook"
"k8s.io/client-go/informers"
clientset "k8s.io/client-go/kubernetes"
"k8s.io/klog/v2"
)
// Webhook is an abstract admission plugin with all the infrastructure to define Admit or Validate on-top.
@ -49,6 +52,8 @@ type Webhook struct {
namespaceMatcher *namespace.Matcher
objectMatcher *object.Matcher
dispatcher Dispatcher
filterCompiler cel.FilterCompiler
authorizer authorizer.Authorizer
}
var (
@ -92,6 +97,7 @@ func NewWebhook(handler *admission.Handler, configFile io.Reader, sourceFactory
namespaceMatcher: &namespace.Matcher{},
objectMatcher: &object.Matcher{},
dispatcher: dispatcherFactory(&cm),
filterCompiler: cel.NewFilterCompiler(),
}, nil
}
@ -124,6 +130,10 @@ func (a *Webhook) SetExternalKubeInformerFactory(f informers.SharedInformerFacto
})
}
func (a *Webhook) SetAuthorizer(authorizer authorizer.Authorizer) {
a.authorizer = authorizer
}
// ValidateInitialization implements the InitializationValidator interface.
func (a *Webhook) ValidateInitialization() error {
if a.hookSource == nil {
@ -140,7 +150,7 @@ func (a *Webhook) ValidateInitialization() error {
// ShouldCallHook returns invocation details if the webhook should be called, nil if the webhook should not be called,
// or an error if an error was encountered during evaluation.
func (a *Webhook) ShouldCallHook(h webhook.WebhookAccessor, attr admission.Attributes, o admission.ObjectInterfaces) (*WebhookInvocation, *apierrors.StatusError) {
func (a *Webhook) ShouldCallHook(ctx context.Context, h webhook.WebhookAccessor, attr admission.Attributes, o admission.ObjectInterfaces, v VersionedAttributeAccessor) (*WebhookInvocation, *apierrors.StatusError) {
matches, matchNsErr := a.namespaceMatcher.MatchNamespaceSelector(h, attr)
// Should not return an error here for webhooks which do not apply to the request, even if err is an unexpected scenario.
if !matches && matchNsErr == nil {
@ -207,6 +217,25 @@ func (a *Webhook) ShouldCallHook(h webhook.WebhookAccessor, attr admission.Attri
return nil, matchObjErr
}
matchConditions := h.GetMatchConditions()
if len(matchConditions) > 0 {
versionedAttr, err := v.VersionedAttribute(invocation.Kind)
if err != nil {
return nil, apierrors.NewInternalError(err)
}
matcher := h.GetCompiledMatcher(a.filterCompiler, a.authorizer)
matchResult := matcher.Match(ctx, versionedAttr, nil)
if matchResult.Error != nil {
klog.Warningf("Failed evaluating match conditions, failing closed %v: %v", h.GetName(), matchResult.Error)
return nil, apierrors.NewForbidden(attr.GetResource().GroupResource(), attr.GetName(), matchResult.Error)
} else if !matchResult.Matches {
// if no match, always skip webhook
return nil, nil
}
}
return invocation, nil
}

View File

@ -0,0 +1,36 @@
/*
Copyright 2023 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 matchconditions
import (
"context"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/admission"
)
type MatchResult struct {
Matches bool
Error error
FailedConditionName string
}
// Matcher contains logic for converting Evaluations to bool of matches or does not match
type Matcher interface {
// Match is used to take cel evaluations and convert into decisions
Match(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object) MatchResult
}

View File

@ -0,0 +1,139 @@
/*
Copyright 2023 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 matchconditions
import (
"context"
"errors"
"fmt"
"github.com/google/cel-go/cel"
celtypes "github.com/google/cel-go/common/types"
v1 "k8s.io/api/admissionregistration/v1"
"k8s.io/apimachinery/pkg/runtime"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apiserver/pkg/admission"
admissionmetrics "k8s.io/apiserver/pkg/admission/metrics"
celplugin "k8s.io/apiserver/pkg/admission/plugin/cel"
celconfig "k8s.io/apiserver/pkg/apis/cel"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/klog/v2"
)
var _ celplugin.ExpressionAccessor = &MatchCondition{}
// MatchCondition contains the inputs needed to compile, evaluate and match a cel expression
type MatchCondition v1.MatchCondition
func (v *MatchCondition) GetExpression() string {
return v.Expression
}
func (v *MatchCondition) ReturnTypes() []*cel.Type {
return []*cel.Type{cel.BoolType}
}
var _ Matcher = &matcher{}
// matcher evaluates compiled cel expressions and determines if they match the given request or not
type matcher struct {
filter celplugin.Filter
authorizer authorizer.Authorizer
failPolicy v1.FailurePolicyType
matcherType string
objectName string
}
func NewMatcher(filter celplugin.Filter, authorizer authorizer.Authorizer, failPolicy *v1.FailurePolicyType, matcherType, objectName string) Matcher {
var f v1.FailurePolicyType
if failPolicy == nil {
f = v1.Fail
} else {
f = *failPolicy
}
return &matcher{
filter: filter,
authorizer: authorizer,
failPolicy: f,
matcherType: matcherType,
objectName: objectName,
}
}
func (m *matcher) Match(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object) MatchResult {
evalResults, _, err := m.filter.ForInput(ctx, versionedAttr, celplugin.CreateAdmissionRequest(versionedAttr.Attributes), celplugin.OptionalVariableBindings{
VersionedParams: versionedParams,
Authorizer: m.authorizer,
}, celconfig.RuntimeCELCostBudgetMatchConditions)
if err != nil {
// filter returning error is unexpected and not an evaluation error so not incrementing metric here
if m.failPolicy == v1.Fail {
return MatchResult{
Error: err,
}
} else if m.failPolicy == v1.Ignore {
return MatchResult{
Matches: false,
}
}
//TODO: add default so that if in future we add different failure types it doesn't fall through
}
errorList := []error{}
for _, evalResult := range evalResults {
matchCondition, ok := evalResult.ExpressionAccessor.(*MatchCondition)
if !ok {
// This shouldnt happen, but if it does treat same as eval error
klog.Error("Invalid type conversion to MatchCondition")
errorList = append(errorList, errors.New(fmt.Sprintf("internal error converting ExpressionAccessor to MatchCondition")))
continue
}
if evalResult.Error != nil {
errorList = append(errorList, evalResult.Error)
//TODO: what's the best way to handle this metric since its reused by VAP for match conditions
admissionmetrics.Metrics.ObserveMatchConditionEvalError(ctx, m.objectName, m.matcherType)
}
if evalResult.EvalResult == celtypes.False {
// If any condition false, skip calling webhook always
return MatchResult{
Matches: false,
FailedConditionName: matchCondition.Name,
}
}
}
if len(errorList) > 0 {
// If mix of true and eval errors then resort to fail policy
if m.failPolicy == v1.Fail {
// mix of true and errors with fail policy fail should fail request without calling webhook
err = utilerrors.NewAggregate(errorList)
return MatchResult{
Error: err,
}
} else if m.failPolicy == v1.Ignore {
// if fail policy ignore then skip call to webhook
return MatchResult{
Matches: false,
}
}
}
// if no results eval to false, return matches true with list of any errors encountered
return MatchResult{
Matches: true,
}
}

View File

@ -26,14 +26,13 @@ import (
jsonpatch "github.com/evanphx/json-patch"
"go.opentelemetry.io/otel/attribute"
apiequality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/klog/v2"
admissionv1 "k8s.io/api/admission/v1"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
apiequality "k8s.io/apimachinery/pkg/api/equality"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer/json"
utiljson "k8s.io/apimachinery/pkg/util/json"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
@ -48,6 +47,7 @@ import (
webhookutil "k8s.io/apiserver/pkg/util/webhook"
"k8s.io/apiserver/pkg/warning"
"k8s.io/component-base/tracing"
"k8s.io/klog/v2"
)
const (
@ -75,6 +75,30 @@ func newMutatingDispatcher(p *Plugin) func(cm *webhookutil.ClientManager) generi
}
}
var _ generic.VersionedAttributeAccessor = &versionedAttributeAccessor{}
type versionedAttributeAccessor struct {
versionedAttr *admission.VersionedAttributes
attr admission.Attributes
objectInterfaces admission.ObjectInterfaces
}
func (v *versionedAttributeAccessor) VersionedAttribute(gvk schema.GroupVersionKind) (*admission.VersionedAttributes, error) {
if v.versionedAttr == nil {
// First call, create versioned attributes
var err error
if v.versionedAttr, err = admission.NewVersionedAttributes(v.attr, gvk, v.objectInterfaces); err != nil {
return nil, apierrors.NewInternalError(err)
}
} else {
// Subsequent call, convert existing versioned attributes to the requested version
if err := admission.ConvertVersionedAttributes(v.versionedAttr, gvk, v.objectInterfaces); err != nil {
return nil, apierrors.NewInternalError(err)
}
}
return v.versionedAttr, nil
}
var _ generic.Dispatcher = &mutatingDispatcher{}
func (a *mutatingDispatcher) Dispatch(ctx context.Context, attr admission.Attributes, o admission.ObjectInterfaces, hooks []webhook.WebhookAccessor) error {
@ -95,19 +119,24 @@ func (a *mutatingDispatcher) Dispatch(ctx context.Context, attr admission.Attrib
defer func() {
webhookReinvokeCtx.SetLastWebhookInvocationOutput(attr.GetObject())
}()
var versionedAttr *generic.VersionedAttributes
v := &versionedAttributeAccessor{
attr: attr,
objectInterfaces: o,
}
for i, hook := range hooks {
attrForCheck := attr
if versionedAttr != nil {
attrForCheck = versionedAttr
if v.versionedAttr != nil {
attrForCheck = v.versionedAttr
}
invocation, statusErr := a.plugin.ShouldCallHook(hook, attrForCheck, o)
invocation, statusErr := a.plugin.ShouldCallHook(ctx, hook, attrForCheck, o, v)
if statusErr != nil {
return statusErr
}
if invocation == nil {
continue
}
hook, ok := invocation.Webhook.GetMutatingWebhook()
if !ok {
return fmt.Errorf("mutating webhook dispatch requires v1.MutatingWebhook, but got %T", hook)
@ -121,17 +150,9 @@ func (a *mutatingDispatcher) Dispatch(ctx context.Context, attr admission.Attrib
continue
}
if versionedAttr == nil {
// First webhook, create versioned attributes
var err error
if versionedAttr, err = generic.NewVersionedAttributes(attr, invocation.Kind, o); err != nil {
return apierrors.NewInternalError(err)
}
} else {
// Subsequent webhook, convert existing versioned attributes to this webhook's version
if err := generic.ConvertVersionedAttributes(versionedAttr, invocation.Kind, o); err != nil {
return apierrors.NewInternalError(err)
}
versionedAttr, err := v.VersionedAttribute(invocation.Kind)
if err != nil {
return apierrors.NewInternalError(err)
}
t := time.Now()
@ -203,8 +224,8 @@ func (a *mutatingDispatcher) Dispatch(ctx context.Context, attr admission.Attrib
}
// convert versionedAttr.VersionedObject to the internal version in the underlying admission.Attributes
if versionedAttr != nil && versionedAttr.VersionedObject != nil && versionedAttr.Dirty {
return o.GetObjectConvertor().Convert(versionedAttr.VersionedObject, versionedAttr.Attributes.GetObject(), nil)
if v.versionedAttr != nil && v.versionedAttr.VersionedObject != nil && v.versionedAttr.Dirty {
return o.GetObjectConvertor().Convert(v.versionedAttr.VersionedObject, v.versionedAttr.Attributes.GetObject(), nil)
}
return nil
@ -212,7 +233,7 @@ func (a *mutatingDispatcher) Dispatch(ctx context.Context, attr admission.Attrib
// note that callAttrMutatingHook updates attr
func (a *mutatingDispatcher) callAttrMutatingHook(ctx context.Context, h *admissionregistrationv1.MutatingWebhook, invocation *generic.WebhookInvocation, attr *generic.VersionedAttributes, annotator *webhookAnnotator, o admission.ObjectInterfaces, round, idx int) (bool, error) {
func (a *mutatingDispatcher) callAttrMutatingHook(ctx context.Context, h *admissionregistrationv1.MutatingWebhook, invocation *generic.WebhookInvocation, attr *admission.VersionedAttributes, annotator *webhookAnnotator, o admission.ObjectInterfaces, round, idx int) (bool, error) {
configurationName := invocation.Webhook.GetConfigurationName()
changed := false
defer func() { annotator.addMutationAnnotation(changed) }()
@ -363,7 +384,7 @@ func (a *mutatingDispatcher) callAttrMutatingHook(ctx context.Context, h *admiss
}
type webhookAnnotator struct {
attr *generic.VersionedAttributes
attr *admission.VersionedAttributes
failedOpenAnnotationKey string
patchAnnotationKey string
mutationAnnotationKey string
@ -371,7 +392,7 @@ type webhookAnnotator struct {
configuration string
}
func newWebhookAnnotator(attr *generic.VersionedAttributes, round, idx int, webhook, configuration string) *webhookAnnotator {
func newWebhookAnnotator(attr *admission.VersionedAttributes, round, idx int, webhook, configuration string) *webhookAnnotator {
return &webhookAnnotator{
attr: attr,
failedOpenAnnotationKey: fmt.Sprintf("%sround_%d_index_%d", MutationAuditAnnotationFailedOpenKeyPrefix, round, idx),

View File

@ -116,7 +116,7 @@ func (m *Matcher) MatchNamespaceSelector(p NamespaceSelectorProvider, attr admis
if !ok {
return false, apierrors.NewInternalError(err)
}
return false, &apierrors.StatusError{status.Status()}
return false, &apierrors.StatusError{ErrStatus: status.Status()}
}
if err != nil {
return false, apierrors.NewInternalError(err)

View File

@ -26,6 +26,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/uuid"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
)
@ -130,7 +131,7 @@ func VerifyAdmissionResponse(uid types.UID, mutating bool, review runtime.Object
// CreateAdmissionObjects returns the unique request uid, the AdmissionReview object to send the webhook and to decode the response into,
// or an error if the webhook does not support receiving any of the admission review versions we know to send
func CreateAdmissionObjects(versionedAttributes *generic.VersionedAttributes, invocation *generic.WebhookInvocation) (uid types.UID, request, response runtime.Object, err error) {
func CreateAdmissionObjects(versionedAttributes *admission.VersionedAttributes, invocation *generic.WebhookInvocation) (uid types.UID, request, response runtime.Object, err error) {
for _, version := range invocation.Webhook.GetAdmissionReviewVersions() {
switch version {
case admissionv1.SchemeGroupVersion.Version:
@ -151,7 +152,7 @@ func CreateAdmissionObjects(versionedAttributes *generic.VersionedAttributes, in
}
// CreateV1AdmissionReview creates an AdmissionReview for the provided admission.Attributes
func CreateV1AdmissionReview(uid types.UID, versionedAttributes *generic.VersionedAttributes, invocation *generic.WebhookInvocation) *admissionv1.AdmissionReview {
func CreateV1AdmissionReview(uid types.UID, versionedAttributes *admission.VersionedAttributes, invocation *generic.WebhookInvocation) *admissionv1.AdmissionReview {
attr := versionedAttributes.Attributes
gvk := invocation.Kind
gvr := invocation.Resource
@ -217,7 +218,7 @@ func CreateV1AdmissionReview(uid types.UID, versionedAttributes *generic.Version
}
// CreateV1beta1AdmissionReview creates an AdmissionReview for the provided admission.Attributes
func CreateV1beta1AdmissionReview(uid types.UID, versionedAttributes *generic.VersionedAttributes, invocation *generic.WebhookInvocation) *admissionv1beta1.AdmissionReview {
func CreateV1beta1AdmissionReview(uid types.UID, versionedAttributes *admission.VersionedAttributes, invocation *generic.WebhookInvocation) *admissionv1beta1.AdmissionReview {
attr := versionedAttributes.Attributes
gvk := invocation.Kind
gvr := invocation.Resource

View File

@ -62,30 +62,51 @@ func newValidatingDispatcher(p *Plugin) func(cm *webhookutil.ClientManager) gene
}
}
var _ generic.VersionedAttributeAccessor = &versionedAttributeAccessor{}
type versionedAttributeAccessor struct {
versionedAttrs map[schema.GroupVersionKind]*admission.VersionedAttributes
attr admission.Attributes
objectInterfaces admission.ObjectInterfaces
}
func (v *versionedAttributeAccessor) VersionedAttribute(gvk schema.GroupVersionKind) (*admission.VersionedAttributes, error) {
if val, ok := v.versionedAttrs[gvk]; ok {
return val, nil
}
versionedAttr, err := admission.NewVersionedAttributes(v.attr, gvk, v.objectInterfaces)
if err != nil {
return nil, err
}
v.versionedAttrs[gvk] = versionedAttr
return versionedAttr, nil
}
var _ generic.Dispatcher = &validatingDispatcher{}
func (d *validatingDispatcher) Dispatch(ctx context.Context, attr admission.Attributes, o admission.ObjectInterfaces, hooks []webhook.WebhookAccessor) error {
var relevantHooks []*generic.WebhookInvocation
// Construct all the versions we need to call our webhooks
versionedAttrs := map[schema.GroupVersionKind]*generic.VersionedAttributes{}
versionedAttrAccessor := &versionedAttributeAccessor{
versionedAttrs: map[schema.GroupVersionKind]*admission.VersionedAttributes{},
attr: attr,
objectInterfaces: o,
}
for _, hook := range hooks {
invocation, statusError := d.plugin.ShouldCallHook(hook, attr, o)
invocation, statusError := d.plugin.ShouldCallHook(ctx, hook, attr, o, versionedAttrAccessor)
if statusError != nil {
return statusError
}
if invocation == nil {
continue
}
relevantHooks = append(relevantHooks, invocation)
// If we already have this version, continue
if _, ok := versionedAttrs[invocation.Kind]; ok {
continue
}
versionedAttr, err := generic.NewVersionedAttributes(attr, invocation.Kind, o)
// VersionedAttr result will be cached and reused later during parallel webhook calls
_, err := versionedAttrAccessor.VersionedAttribute(invocation.Kind)
if err != nil {
return apierrors.NewInternalError(err)
}
versionedAttrs[invocation.Kind] = versionedAttr
}
if len(relevantHooks) == 0 {
@ -108,7 +129,7 @@ func (d *validatingDispatcher) Dispatch(ctx context.Context, attr admission.Attr
go func(invocation *generic.WebhookInvocation, idx int) {
ignoreClientCallFailures := false
hookName := "unknown"
versionedAttr := versionedAttrs[invocation.Kind]
versionedAttr := versionedAttrAccessor.versionedAttrs[invocation.Kind]
// The ordering of these two defers is critical. The wg.Done will release the parent go func to close the errCh
// that is used by the second defer to report errors. The recovery and error reporting must be done first.
defer wg.Done()
@ -215,7 +236,7 @@ func (d *validatingDispatcher) Dispatch(ctx context.Context, attr admission.Attr
return errs[0]
}
func (d *validatingDispatcher) callHook(ctx context.Context, h *v1.ValidatingWebhook, invocation *generic.WebhookInvocation, attr *generic.VersionedAttributes) error {
func (d *validatingDispatcher) callHook(ctx context.Context, h *v1.ValidatingWebhook, invocation *generic.WebhookInvocation, attr *admission.VersionedAttributes) error {
if attr.Attributes.IsDryRun() {
if h.SideEffects == nil {
return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("Webhook SideEffects is nil"), Status: apierrors.NewBadRequest("Webhook SideEffects is nil")}

View File

@ -19,6 +19,7 @@ package apiserver
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
tracingapi "k8s.io/component-base/tracing/api/v1"
)
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
@ -153,16 +154,6 @@ type TLSConfig struct {
type TracingConfiguration struct {
metav1.TypeMeta
// +optional
// Endpoint of the collector that's running on the control-plane node.
// The APIServer uses the egressType ControlPlane when sending data to the collector.
// The syntax is defined in https://github.com/grpc/grpc/blob/master/doc/naming.md.
// Defaults to the otlp grpc default, localhost:4317
// The connection is insecure, and does not currently support TLS.
Endpoint *string
// +optional
// SamplingRatePerMillion is the number of samples to collect per million spans.
// Defaults to 0.
SamplingRatePerMillion *int32
// Embed the component config tracing configuration struct
tracingapi.TracingConfiguration
}

View File

@ -19,6 +19,7 @@ package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
tracingapi "k8s.io/component-base/tracing/api/v1"
)
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
@ -154,16 +155,6 @@ type TLSConfig struct {
type TracingConfiguration struct {
metav1.TypeMeta `json:",inline"`
// +optional
// Endpoint of the collector that's running on the control-plane node.
// The APIServer uses the egressType ControlPlane when sending data to the collector.
// The syntax is defined in https://github.com/grpc/grpc/blob/master/doc/naming.md.
// Defaults to the otlpgrpc default, localhost:4317
// The connection is insecure, and does not support TLS.
Endpoint *string `json:"endpoint,omitempty" protobuf:"bytes,1,opt,name=endpoint"`
// +optional
// SamplingRatePerMillion is the number of samples to collect per million spans.
// Defaults to 0.
SamplingRatePerMillion *int32 `json:"samplingRatePerMillion,omitempty" protobuf:"varint,2,opt,name=samplingRatePerMillion"`
// Embed the component config tracing configuration struct
tracingapi.TracingConfiguration `json:",inline"`
}

View File

@ -313,8 +313,7 @@ func Convert_apiserver_TLSConfig_To_v1alpha1_TLSConfig(in *apiserver.TLSConfig,
}
func autoConvert_v1alpha1_TracingConfiguration_To_apiserver_TracingConfiguration(in *TracingConfiguration, out *apiserver.TracingConfiguration, s conversion.Scope) error {
out.Endpoint = (*string)(unsafe.Pointer(in.Endpoint))
out.SamplingRatePerMillion = (*int32)(unsafe.Pointer(in.SamplingRatePerMillion))
out.TracingConfiguration = in.TracingConfiguration
return nil
}
@ -324,8 +323,7 @@ func Convert_v1alpha1_TracingConfiguration_To_apiserver_TracingConfiguration(in
}
func autoConvert_apiserver_TracingConfiguration_To_v1alpha1_TracingConfiguration(in *apiserver.TracingConfiguration, out *TracingConfiguration, s conversion.Scope) error {
out.Endpoint = (*string)(unsafe.Pointer(in.Endpoint))
out.SamplingRatePerMillion = (*int32)(unsafe.Pointer(in.SamplingRatePerMillion))
out.TracingConfiguration = in.TracingConfiguration
return nil
}

View File

@ -189,16 +189,7 @@ func (in *TLSConfig) DeepCopy() *TLSConfig {
func (in *TracingConfiguration) DeepCopyInto(out *TracingConfiguration) {
*out = *in
out.TypeMeta = in.TypeMeta
if in.Endpoint != nil {
in, out := &in.Endpoint, &out.Endpoint
*out = new(string)
**out = **in
}
if in.SamplingRatePerMillion != nil {
in, out := &in.SamplingRatePerMillion, &out.SamplingRatePerMillion
*out = new(int32)
**out = **in
}
in.TracingConfiguration.DeepCopyInto(&out.TracingConfiguration)
return
}

View File

@ -23,10 +23,14 @@ import (
)
const GroupName = "apiserver.k8s.io"
const ConfigGroupName = "apiserver.config.k8s.io"
// SchemeGroupVersion is group version used to register these objects
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1beta1"}
// ConfigSchemeGroupVersion is group version used to register these objects
var ConfigSchemeGroupVersion = schema.GroupVersion{Group: ConfigGroupName, Version: "v1beta1"}
var (
// TODO: move SchemeBuilder with zz_generated.deepcopy.go to k8s.io/api.
// localSchemeBuilder and AddToScheme will stay in k8s.io/kubernetes.
@ -47,6 +51,9 @@ func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(SchemeGroupVersion,
&EgressSelectorConfiguration{},
)
scheme.AddKnownTypes(ConfigSchemeGroupVersion,
&TracingConfiguration{},
)
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
return nil
}

View File

@ -18,6 +18,7 @@ package v1beta1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
tracingapi "k8s.io/component-base/tracing/api/v1"
)
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
@ -118,3 +119,13 @@ type TLSConfig struct {
// +optional
ClientCert string `json:"clientCert,omitempty"`
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// TracingConfiguration provides versioned configuration for tracing clients.
type TracingConfiguration struct {
metav1.TypeMeta `json:",inline"`
// Embed the component config tracing configuration struct
tracingapi.TracingConfiguration `json:",inline"`
}

View File

@ -81,6 +81,16 @@ func RegisterConversions(s *runtime.Scheme) error {
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*TracingConfiguration)(nil), (*apiserver.TracingConfiguration)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1beta1_TracingConfiguration_To_apiserver_TracingConfiguration(a.(*TracingConfiguration), b.(*apiserver.TracingConfiguration), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*apiserver.TracingConfiguration)(nil), (*TracingConfiguration)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_apiserver_TracingConfiguration_To_v1beta1_TracingConfiguration(a.(*apiserver.TracingConfiguration), b.(*TracingConfiguration), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*Transport)(nil), (*apiserver.Transport)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1beta1_Transport_To_apiserver_Transport(a.(*Transport), b.(*apiserver.Transport), scope)
}); err != nil {
@ -238,6 +248,26 @@ func Convert_apiserver_TLSConfig_To_v1beta1_TLSConfig(in *apiserver.TLSConfig, o
return autoConvert_apiserver_TLSConfig_To_v1beta1_TLSConfig(in, out, s)
}
func autoConvert_v1beta1_TracingConfiguration_To_apiserver_TracingConfiguration(in *TracingConfiguration, out *apiserver.TracingConfiguration, s conversion.Scope) error {
out.TracingConfiguration = in.TracingConfiguration
return nil
}
// Convert_v1beta1_TracingConfiguration_To_apiserver_TracingConfiguration is an autogenerated conversion function.
func Convert_v1beta1_TracingConfiguration_To_apiserver_TracingConfiguration(in *TracingConfiguration, out *apiserver.TracingConfiguration, s conversion.Scope) error {
return autoConvert_v1beta1_TracingConfiguration_To_apiserver_TracingConfiguration(in, out, s)
}
func autoConvert_apiserver_TracingConfiguration_To_v1beta1_TracingConfiguration(in *apiserver.TracingConfiguration, out *TracingConfiguration, s conversion.Scope) error {
out.TracingConfiguration = in.TracingConfiguration
return nil
}
// Convert_apiserver_TracingConfiguration_To_v1beta1_TracingConfiguration is an autogenerated conversion function.
func Convert_apiserver_TracingConfiguration_To_v1beta1_TracingConfiguration(in *apiserver.TracingConfiguration, out *TracingConfiguration, s conversion.Scope) error {
return autoConvert_apiserver_TracingConfiguration_To_v1beta1_TracingConfiguration(in, out, s)
}
func autoConvert_v1beta1_Transport_To_apiserver_Transport(in *Transport, out *apiserver.Transport, s conversion.Scope) error {
out.TCP = (*apiserver.TCPTransport)(unsafe.Pointer(in.TCP))
out.UDS = (*apiserver.UDSTransport)(unsafe.Pointer(in.UDS))

View File

@ -132,6 +132,32 @@ func (in *TLSConfig) DeepCopy() *TLSConfig {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TracingConfiguration) DeepCopyInto(out *TracingConfiguration) {
*out = *in
out.TypeMeta = in.TypeMeta
in.TracingConfiguration.DeepCopyInto(&out.TracingConfiguration)
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TracingConfiguration.
func (in *TracingConfiguration) DeepCopy() *TracingConfiguration {
if in == nil {
return nil
}
out := new(TracingConfiguration)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *TracingConfiguration) 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 *Transport) DeepCopyInto(out *Transport) {
*out = *in

View File

@ -189,16 +189,7 @@ func (in *TLSConfig) DeepCopy() *TLSConfig {
func (in *TracingConfiguration) DeepCopyInto(out *TracingConfiguration) {
*out = *in
out.TypeMeta = in.TypeMeta
if in.Endpoint != nil {
in, out := &in.Endpoint, &out.Endpoint
*out = new(string)
**out = **in
}
if in.SamplingRatePerMillion != nil {
in, out := &in.SamplingRatePerMillion, &out.SamplingRatePerMillion
*out = new(int32)
**out = **in
}
in.TracingConfiguration.DeepCopyInto(&out.TracingConfiguration)
return
}

45
vendor/k8s.io/apiserver/pkg/apis/cel/config.go generated vendored Normal file
View File

@ -0,0 +1,45 @@
/*
Copyright 2023 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
const (
// PerCallLimit specify the actual cost limit per CEL validation call
// current PerCallLimit gives roughly 0.1 second for each expression validation call
PerCallLimit = 1000000
// RuntimeCELCostBudget is the overall cost budget for runtime CEL validation cost per ValidatingAdmissionPolicyBinding or CustomResource
// current RuntimeCELCostBudget gives roughly 1 seconds for the validation
RuntimeCELCostBudget = 10000000
// RuntimeCELCostBudgetMatchConditions is the overall cost budget for runtime CEL validation cost on matchConditions per object with matchConditions
// this is per webhook for validatingwebhookconfigurations and mutatingwebhookconfigurations or per ValidatingAdmissionPolicyBinding
// current RuntimeCELCostBudgetMatchConditions gives roughly 1/4 seconds for the validation
RuntimeCELCostBudgetMatchConditions = 2500000
// CheckFrequency configures the number of iterations within a comprehension to evaluate
// before checking whether the function evaluation has been interrupted
CheckFrequency = 100
// MaxRequestSizeBytes is the maximum size of a request to the API server
// TODO(DangerOnTheRanger): wire in MaxRequestBodyBytes from apiserver/pkg/server/options/server_run_options.go to make this configurable
// Note that even if server_run_options.go becomes configurable in the future, this cost constant should be fixed and it should be the max allowed request size for the server
MaxRequestSizeBytes = int64(3 * 1024 * 1024)
// MaxEvaluatedMessageExpressionSizeBytes represents the largest-allowable string generated
// by a messageExpression field
MaxEvaluatedMessageExpressionSizeBytes = 5 * 1024
)

View File

@ -24,7 +24,49 @@ import (
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// EncryptionConfiguration stores the complete configuration for encryption providers.
/*
EncryptionConfiguration stores the complete configuration for encryption providers.
It also allows the use of wildcards to specify the resources that should be encrypted.
Use '*.<group>' to encrypt all resources within a group or '*.*' to encrypt all resources.
'*.' can be used to encrypt all resource in the core group. '*.*' will encrypt all
resources, even custom resources that are added after API server start.
Use of wildcards that overlap within the same resource list or across multiple
entries are not allowed since part of the configuration would be ineffective.
Resource lists are processed in order, with earlier lists taking precedence.
Example:
kind: EncryptionConfiguration
apiVersion: apiserver.config.k8s.io/v1
resources:
- resources:
- events
providers:
- identity: {} # do not encrypt events even though *.* is specified below
- resources:
- secrets
- configmaps
- pandas.awesome.bears.example
providers:
- aescbc:
keys:
- name: key1
secret: c2VjcmV0IGlzIHNlY3VyZQ==
- resources:
- '*.apps'
providers:
- aescbc:
keys:
- name: key2
secret: c2VjcmV0IGlzIHNlY3VyZSwgb3IgaXMgaXQ/Cg==
- resources:
- '*.*'
providers:
- aescbc:
keys:
- name: key3
secret: c2VjcmV0IGlzIHNlY3VyZSwgSSB0aGluaw==
*/
type EncryptionConfiguration struct {
metav1.TypeMeta
// resources is a list containing resources, and their corresponding encryption providers.
@ -33,10 +75,14 @@ type EncryptionConfiguration struct {
// ResourceConfiguration stores per resource configuration.
type ResourceConfiguration struct {
// resources is a list of kubernetes resources which have to be encrypted.
// resources is a list of kubernetes resources which have to be encrypted. The resource names are derived from `resource` or `resource.group` of the group/version/resource.
// eg: pandas.awesome.bears.example is a custom resource with 'group': awesome.bears.example, 'resource': pandas.
// Use '*.*' to encrypt all resources and '*.<group>' to encrypt all resources in a specific group.
// eg: '*.awesome.bears.example' will encrypt all resources in the group 'awesome.bears.example'.
// eg: '*.' will encrypt all resources in the core group (such as pods, configmaps, etc).
Resources []string
// providers is a list of transformers to be used for reading and writing the resources to disk.
// eg: aesgcm, aescbc, secretbox, identity.
// eg: aesgcm, aescbc, secretbox, identity, kms.
Providers []ProviderConfiguration
}
@ -92,7 +138,7 @@ type KMSConfiguration struct {
// name is the name of the KMS plugin to be used.
Name string
// cachesize is the maximum number of secrets which are cached in memory. The default value is 1000.
// Set to a negative value to disable caching.
// Set to a negative value to disable caching. This field is only allowed for KMS v1 providers.
// +optional
CacheSize *int32
// endpoint is the gRPC server listening address, for example "unix:///var/run/kms-provider.sock".

View File

@ -39,11 +39,12 @@ func SetDefaults_KMSConfiguration(obj *KMSConfiguration) {
obj.Timeout = defaultTimeout
}
if obj.CacheSize == nil {
obj.CacheSize = &defaultCacheSize
}
if obj.APIVersion == "" {
obj.APIVersion = defaultAPIVersion
}
// cacheSize is relevant only for kms v1
if obj.CacheSize == nil && obj.APIVersion == "v1" {
obj.CacheSize = &defaultCacheSize
}
}

View File

@ -24,7 +24,49 @@ import (
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// EncryptionConfiguration stores the complete configuration for encryption providers.
/*
EncryptionConfiguration stores the complete configuration for encryption providers.
It also allows the use of wildcards to specify the resources that should be encrypted.
Use '*.<group>' to encrypt all resources within a group or '*.*' to encrypt all resources.
'*.' can be used to encrypt all resource in the core group. '*.*' will encrypt all
resources, even custom resources that are added after API server start.
Use of wildcards that overlap within the same resource list or across multiple
entries are not allowed since part of the configuration would be ineffective.
Resource lists are processed in order, with earlier lists taking precedence.
Example:
kind: EncryptionConfiguration
apiVersion: apiserver.config.k8s.io/v1
resources:
- resources:
- events
providers:
- identity: {} # do not encrypt events even though *.* is specified below
- resources:
- secrets
- configmaps
- pandas.awesome.bears.example
providers:
- aescbc:
keys:
- name: key1
secret: c2VjcmV0IGlzIHNlY3VyZQ==
- resources:
- '*.apps'
providers:
- aescbc:
keys:
- name: key2
secret: c2VjcmV0IGlzIHNlY3VyZSwgb3IgaXMgaXQ/Cg==
- resources:
- '*.*'
providers:
- aescbc:
keys:
- name: key3
secret: c2VjcmV0IGlzIHNlY3VyZSwgSSB0aGluaw==
*/
type EncryptionConfiguration struct {
metav1.TypeMeta
// resources is a list containing resources, and their corresponding encryption providers.
@ -33,10 +75,14 @@ type EncryptionConfiguration struct {
// ResourceConfiguration stores per resource configuration.
type ResourceConfiguration struct {
// resources is a list of kubernetes resources which have to be encrypted.
// resources is a list of kubernetes resources which have to be encrypted. The resource names are derived from `resource` or `resource.group` of the group/version/resource.
// eg: pandas.awesome.bears.example is a custom resource with 'group': awesome.bears.example, 'resource': pandas.
// Use '*.*' to encrypt all resources and '*.<group>' to encrypt all resources in a specific group.
// eg: '*.awesome.bears.example' will encrypt all resources in the group 'awesome.bears.example'.
// eg: '*.' will encrypt all resources in the core group (such as pods, configmaps, etc).
Resources []string `json:"resources"`
// providers is a list of transformers to be used for reading and writing the resources to disk.
// eg: aesgcm, aescbc, secretbox, identity.
// eg: aesgcm, aescbc, secretbox, identity, kms.
Providers []ProviderConfiguration `json:"providers"`
}
@ -92,7 +138,7 @@ type KMSConfiguration struct {
// name is the name of the KMS plugin to be used.
Name string `json:"name"`
// cachesize is the maximum number of secrets which are cached in memory. The default value is 1000.
// Set to a negative value to disable caching.
// Set to a negative value to disable caching. This field is only allowed for KMS v1 providers.
// +optional
CacheSize *int32 `json:"cachesize,omitempty"`
// endpoint is the gRPC server listening address, for example "unix:///var/run/kms-provider.sock".

View File

@ -23,6 +23,7 @@ import (
"net/url"
"strings"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apiserver/pkg/apis/config"
@ -34,7 +35,7 @@ const (
unsupportedSchemeErrFmt = "unsupported scheme %q for KMS provider, only unix is supported"
unsupportedKMSAPIVersionErrFmt = "unsupported apiVersion %s for KMS provider, only v1 and v2 are supported"
atLeastOneRequiredErrFmt = "at least one %s is required"
invalidURLErrFmt = "invalid endpoint for kms provider, error: parse %s: net/url: invalid control character in URL"
invalidURLErrFmt = "invalid endpoint for kms provider, error: %v"
mandatoryFieldErrFmt = "%s is a mandatory field for a %s"
base64EncodingErr = "secrets must be base64 encoded"
zeroOrNegativeErrFmt = "%s should be a positive value"
@ -42,6 +43,14 @@ const (
encryptionConfigNilErr = "EncryptionConfiguration can't be nil"
invalidKMSConfigNameErrFmt = "invalid KMS provider name %s, must not contain ':'"
duplicateKMSConfigNameErrFmt = "duplicate KMS provider name %s, names must be unique"
eventsGroupErr = "'*.events.k8s.io' objects are stored using the 'events' API group in etcd. Use 'events' instead in the config file"
extensionsGroupErr = "'extensions' group has been removed and cannot be used for encryption"
starResourceErr = "use '*.' to encrypt all the resources from core API group or *.* to encrypt all resources"
overlapErr = "using overlapping resources such as 'secrets' and '*.' in the same resource list is not allowed as they will be masked"
nonRESTAPIResourceErr = "resources which do not have REST API/s cannot be encrypted"
resourceNameErr = "resource name should not contain capital letters"
resourceAcrossGroupErr = "encrypting the same resource across groups is not supported"
duplicateResourceErr = "the same resource cannot be specified multiple times"
)
var (
@ -59,7 +68,7 @@ func ValidateEncryptionConfiguration(c *config.EncryptionConfiguration, reload b
allErrs := field.ErrorList{}
if c == nil {
allErrs = append(allErrs, field.Required(root, "EncryptionConfiguration can't be nil"))
allErrs = append(allErrs, field.Required(root, encryptionConfigNilErr))
return allErrs
}
@ -78,6 +87,9 @@ func ValidateEncryptionConfiguration(c *config.EncryptionConfiguration, reload b
allErrs = append(allErrs, field.Required(r, fmt.Sprintf(atLeastOneRequiredErrFmt, r)))
}
allErrs = append(allErrs, validateResourceOverlap(conf.Resources, r)...)
allErrs = append(allErrs, validateResourceNames(conf.Resources, r)...)
if len(conf.Providers) == 0 {
allErrs = append(allErrs, field.Required(p, fmt.Sprintf(atLeastOneRequiredErrFmt, p)))
}
@ -103,6 +115,175 @@ func ValidateEncryptionConfiguration(c *config.EncryptionConfiguration, reload b
return allErrs
}
var anyGroupAnyResource = schema.GroupResource{
Group: "*",
Resource: "*",
}
func validateResourceOverlap(resources []string, fieldPath *field.Path) field.ErrorList {
if len(resources) < 2 { // cannot have overlap with a single resource
return nil
}
var allErrs field.ErrorList
r := make([]schema.GroupResource, 0, len(resources))
for _, resource := range resources {
r = append(r, schema.ParseGroupResource(resource))
}
var hasOverlap, hasDuplicate bool
for i, r1 := range r {
for j, r2 := range r {
if i == j {
continue
}
if r1 == r2 && !hasDuplicate {
hasDuplicate = true
continue
}
if hasOverlap {
continue
}
if r1 == anyGroupAnyResource {
hasOverlap = true
continue
}
if r1.Group != r2.Group {
continue
}
if r1.Resource == "*" || r2.Resource == "*" {
hasOverlap = true
continue
}
}
}
if hasDuplicate {
allErrs = append(
allErrs,
field.Invalid(
fieldPath,
resources,
duplicateResourceErr,
),
)
}
if hasOverlap {
allErrs = append(
allErrs,
field.Invalid(
fieldPath,
resources,
overlapErr,
),
)
}
return allErrs
}
func validateResourceNames(resources []string, fieldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
for j, res := range resources {
jj := fieldPath.Index(j)
// check if resource name has capital letters
if hasCapital(res) {
allErrs = append(
allErrs,
field.Invalid(
jj,
resources[j],
resourceNameErr,
),
)
continue
}
// check if resource is '*'
if res == "*" {
allErrs = append(
allErrs,
field.Invalid(
jj,
resources[j],
starResourceErr,
),
)
continue
}
// check if resource is:
// 'apiserveripinfo' OR
// 'serviceipallocations' OR
// 'servicenodeportallocations' OR
if res == "apiserveripinfo" ||
res == "serviceipallocations" ||
res == "servicenodeportallocations" {
allErrs = append(
allErrs,
field.Invalid(
jj,
resources[j],
nonRESTAPIResourceErr,
),
)
continue
}
// check if group is 'events.k8s.io'
gr := schema.ParseGroupResource(res)
if gr.Group == "events.k8s.io" {
allErrs = append(
allErrs,
field.Invalid(
jj,
resources[j],
eventsGroupErr,
),
)
continue
}
// check if group is 'extensions'
if gr.Group == "extensions" {
allErrs = append(
allErrs,
field.Invalid(
jj,
resources[j],
extensionsGroupErr,
),
)
continue
}
// disallow resource.* as encrypting the same resource across groups does not make sense
if gr.Group == "*" && gr.Resource != "*" {
allErrs = append(
allErrs,
field.Invalid(
jj,
resources[j],
resourceAcrossGroupErr,
),
)
continue
}
}
return allErrs
}
func validateSingleProvider(provider config.ProviderConfiguration, fieldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
found := 0
@ -195,7 +376,13 @@ func validateKMSConfiguration(c *config.KMSConfiguration, fieldPath *field.Path,
func validateKMSCacheSize(c *config.KMSConfiguration, fieldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
if *c.CacheSize == 0 {
// In defaulting, we set the cache size to the default value only when API version is v1.
// So, for v2 API version, we expect the cache size field to be nil.
if c.APIVersion != "v1" && c.CacheSize != nil {
allErrs = append(allErrs, field.Invalid(fieldPath, *c.CacheSize, "cachesize is not supported in v2"))
}
if c.APIVersion == "v1" && *c.CacheSize == 0 {
allErrs = append(allErrs, field.Invalid(fieldPath, *c.CacheSize, fmt.Sprintf(nonZeroErrFmt, "cachesize")))
}
@ -219,7 +406,7 @@ func validateKMSEndpoint(c *config.KMSConfiguration, fieldPath *field.Path) fiel
u, err := url.Parse(c.Endpoint)
if err != nil {
return append(allErrs, field.Invalid(fieldPath, c.Endpoint, fmt.Sprintf("invalid endpoint for kms provider, error: %v", err)))
return append(allErrs, field.Invalid(fieldPath, c.Endpoint, fmt.Sprintf(invalidURLErrFmt, err)))
}
if u.Scheme != "unix" {
@ -259,3 +446,7 @@ func validateKMSConfigName(c *config.KMSConfiguration, fieldPath *field.Path, km
return allErrs
}
func hasCapital(input string) bool {
return strings.ToLower(input) != input
}

View File

@ -25,6 +25,9 @@ import (
// a given request. PolicyRuleEvaluator evaluates the audit policy against the
// authorizer attributes and returns a RequestAuditConfig that applies to the request.
type RequestAuditConfig struct {
// Level at which the request is being audited at
Level audit.Level
// OmitStages is the stages that need to be omitted from being audited.
OmitStages []audit.Stage
@ -33,21 +36,10 @@ type RequestAuditConfig struct {
OmitManagedFields bool
}
// RequestAuditConfigWithLevel includes Level at which the request is being audited.
// PolicyRuleEvaluator evaluates the audit configuration for a request
// against the authorizer attributes and returns an RequestAuditConfigWithLevel
// that applies to the request.
type RequestAuditConfigWithLevel struct {
RequestAuditConfig
// Level at which the request is being audited at
Level audit.Level
}
// PolicyRuleEvaluator exposes methods for evaluating the policy rules.
type PolicyRuleEvaluator interface {
// EvaluatePolicyRule evaluates the audit policy of the apiserver against
// the given authorizer attributes and returns the audit configuration that
// is applicable to the given equest.
EvaluatePolicyRule(authorizer.Attributes) RequestAuditConfigWithLevel
EvaluatePolicyRule(authorizer.Attributes) RequestAuditConfig
}

View File

@ -61,25 +61,21 @@ type policyRuleEvaluator struct {
audit.Policy
}
func (p *policyRuleEvaluator) EvaluatePolicyRule(attrs authorizer.Attributes) auditinternal.RequestAuditConfigWithLevel {
func (p *policyRuleEvaluator) EvaluatePolicyRule(attrs authorizer.Attributes) auditinternal.RequestAuditConfig {
for _, rule := range p.Rules {
if ruleMatches(&rule, attrs) {
return auditinternal.RequestAuditConfigWithLevel{
Level: rule.Level,
RequestAuditConfig: auditinternal.RequestAuditConfig{
OmitStages: rule.OmitStages,
OmitManagedFields: isOmitManagedFields(&rule, p.OmitManagedFields),
},
return auditinternal.RequestAuditConfig{
Level: rule.Level,
OmitStages: rule.OmitStages,
OmitManagedFields: isOmitManagedFields(&rule, p.OmitManagedFields),
}
}
}
return auditinternal.RequestAuditConfigWithLevel{
Level: DefaultAuditLevel,
RequestAuditConfig: auditinternal.RequestAuditConfig{
OmitStages: p.OmitStages,
OmitManagedFields: p.OmitManagedFields,
},
return auditinternal.RequestAuditConfig{
Level: DefaultAuditLevel,
OmitStages: p.OmitStages,
OmitManagedFields: p.OmitManagedFields,
}
}
@ -235,11 +231,9 @@ type fakePolicyRuleEvaluator struct {
stage []audit.Stage
}
func (f *fakePolicyRuleEvaluator) EvaluatePolicyRule(_ authorizer.Attributes) auditinternal.RequestAuditConfigWithLevel {
return auditinternal.RequestAuditConfigWithLevel{
Level: f.level,
RequestAuditConfig: auditinternal.RequestAuditConfig{
OmitStages: f.stage,
},
func (f *fakePolicyRuleEvaluator) EvaluatePolicyRule(_ authorizer.Attributes) auditinternal.RequestAuditConfig {
return auditinternal.RequestAuditConfig{
Level: f.level,
OmitStages: f.stage,
}
}

View File

@ -163,17 +163,7 @@ func (a *requestHeaderAuthRequestHandler) AuthenticateRequest(req *http.Request)
extra := newExtra(req.Header, a.extraHeaderPrefixes.Value())
// clear headers used for authentication
for _, headerName := range a.nameHeaders.Value() {
req.Header.Del(headerName)
}
for _, headerName := range a.groupHeaders.Value() {
req.Header.Del(headerName)
}
for k := range extra {
for _, prefix := range a.extraHeaderPrefixes.Value() {
req.Header.Del(prefix + k)
}
}
ClearAuthenticationHeaders(req.Header, a.nameHeaders, a.groupHeaders, a.extraHeaderPrefixes)
return &authenticator.Response{
User: &user.DefaultInfo{
@ -184,6 +174,26 @@ func (a *requestHeaderAuthRequestHandler) AuthenticateRequest(req *http.Request)
}, true, nil
}
func ClearAuthenticationHeaders(h http.Header, nameHeaders, groupHeaders, extraHeaderPrefixes StringSliceProvider) {
for _, headerName := range nameHeaders.Value() {
h.Del(headerName)
}
for _, headerName := range groupHeaders.Value() {
h.Del(headerName)
}
for _, prefix := range extraHeaderPrefixes.Value() {
for k := range h {
if hasPrefixIgnoreCase(k, prefix) {
delete(h, k) // we have the raw key so avoid relying on canonicalization
}
}
}
}
func hasPrefixIgnoreCase(s, prefix string) bool {
return len(s) >= len(prefix) && strings.EqualFold(s[:len(prefix)], prefix)
}
func headerValue(h http.Header, headerNames []string) string {
for _, headerName := range headerNames {
headerValue := h.Get(headerName)
@ -226,7 +236,7 @@ func newExtra(h http.Header, headerPrefixes []string) map[string][]string {
// we have to iterate over prefixes first in order to have proper ordering inside the value slices
for _, prefix := range headerPrefixes {
for headerName, vv := range h {
if !strings.HasPrefix(strings.ToLower(headerName), strings.ToLower(prefix)) {
if !hasPrefixIgnoreCase(headerName, prefix) {
continue
}

View File

@ -277,12 +277,24 @@ func writeLength(w io.Writer, b []byte, length int) {
// toBytes performs unholy acts to avoid allocations
func toBytes(s string) []byte {
return *(*[]byte)(unsafe.Pointer(&s))
// unsafe.StringData is unspecified for the empty string, so we provide a strict interpretation
if len(s) == 0 {
return nil
}
// Copied from go 1.20.1 os.File.WriteString
// https://github.com/golang/go/blob/202a1a57064127c3f19d96df57b9f9586145e21c/src/os/file.go#L246
return unsafe.Slice(unsafe.StringData(s), len(s))
}
// toString performs unholy acts to avoid allocations
func toString(b []byte) string {
return *(*string)(unsafe.Pointer(&b))
// unsafe.SliceData relies on cap whereas we want to rely on len
if len(b) == 0 {
return ""
}
// Copied from go 1.20.1 strings.Builder.String
// https://github.com/golang/go/blob/202a1a57064127c3f19d96df57b9f9586145e21c/src/strings/builder.go#L48
return unsafe.String(unsafe.SliceData(b), len(b))
}
// simple recorder that only appends warning

11
vendor/k8s.io/apiserver/pkg/cel/OWNERS generated vendored Normal file
View File

@ -0,0 +1,11 @@
# See the OWNERS docs at https://go.k8s.io/owners
# Kubernetes CEL library authors and maintainers
approvers:
- jpbetz
- cici37
- jiahuif
reviewers:
- jpbetz
- cici37
- jiahuif

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

@ -0,0 +1,81 @@
/*
Copyright 2023 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
// Schema is the adapted type for an OpenAPI schema that CEL uses.
// This schema does not cover all OpenAPI fields but only these CEL requires
// are exposed as getters.
type Schema interface {
// Type returns the OpenAPI type.
// Multiple types are not supported. It should return
// empty string if no type is specified.
Type() string
// Format returns the OpenAPI format. May be empty
Format() string
// Items returns the OpenAPI items. or nil of this field does not exist or
// contains no schema.
Items() Schema
// Properties returns the OpenAPI properties, or nil if this field does not
// exist.
// The values of the returned map are of the adapted type.
Properties() map[string]Schema
// AdditionalProperties returns the OpenAPI additional properties field,
// or nil if this field does not exist.
AdditionalProperties() SchemaOrBool
// Default returns the OpenAPI default field, or nil if this field does not exist.
Default() any
Validations
KubeExtensions
// WithTypeAndObjectMeta returns a schema that has the type and object meta set.
// the type includes "kind", "apiVersion" field
// the "metadata" field requires "name" and "generateName" to be set
// The original schema must not be mutated. Make a copy if necessary.
WithTypeAndObjectMeta() Schema
}
// Validations contains OpenAPI validation that the CEL library uses.
type Validations interface {
MaxItems() *int64
MaxLength() *int64
MaxProperties() *int64
Required() []string
Enum() []any
Nullable() bool
}
// KubeExtensions contains Kubernetes-specific extensions to the OpenAPI schema.
type KubeExtensions interface {
IsXIntOrString() bool
IsXEmbeddedResource() bool
IsXPreserveUnknownFields() bool
XListType() string
XListMapKeys() []string
}
// SchemaOrBool contains either a schema or a boolean indicating if the object
// can contain any fields.
type SchemaOrBool interface {
Schema() Schema
Allows() bool
}

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

@ -0,0 +1,177 @@
/*
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 common
import (
"fmt"
"strings"
)
// MapList provides a "lookup by key" operation for lists (arrays) with x-kubernetes-list-type=map.
type MapList interface {
// Get returns the first element having given key, for all
// x-kubernetes-list-map-keys, to the provided object. If the provided object isn't itself a valid MapList element,
// get returns nil.
Get(interface{}) interface{}
}
type keyStrategy interface {
// CompositeKeyFor returns a composite key for the provided object, if possible, and a
// boolean that indicates whether or not a key could be generated for the provided object.
CompositeKeyFor(map[string]interface{}) (interface{}, bool)
}
// singleKeyStrategy is a cheaper strategy for associative lists that have exactly one key.
type singleKeyStrategy struct {
key string
}
// CompositeKeyFor directly returns the value of the single key to
// use as a composite key.
func (ks *singleKeyStrategy) CompositeKeyFor(obj map[string]interface{}) (interface{}, bool) {
v, ok := obj[ks.key]
if !ok {
return nil, false
}
switch v.(type) {
case bool, float64, int64, string:
return v, true
default:
return nil, false // non-scalar
}
}
// multiKeyStrategy computes a composite key of all key values.
type multiKeyStrategy struct {
sts Schema
}
// CompositeKeyFor returns a composite key computed from the values of all
// keys.
func (ks *multiKeyStrategy) CompositeKeyFor(obj map[string]interface{}) (interface{}, bool) {
const keyDelimiter = "\x00" // 0 byte should never appear in the composite key except as delimiter
var delimited strings.Builder
for _, key := range ks.sts.XListMapKeys() {
v, ok := obj[key]
if !ok {
return nil, false
}
switch v.(type) {
case bool:
fmt.Fprintf(&delimited, keyDelimiter+"%t", v)
case float64:
fmt.Fprintf(&delimited, keyDelimiter+"%f", v)
case int64:
fmt.Fprintf(&delimited, keyDelimiter+"%d", v)
case string:
fmt.Fprintf(&delimited, keyDelimiter+"%q", v)
default:
return nil, false // values must be scalars
}
}
return delimited.String(), true
}
// emptyMapList is a MapList containing no elements.
type emptyMapList struct{}
func (emptyMapList) Get(interface{}) interface{} {
return nil
}
type mapListImpl struct {
sts Schema
ks keyStrategy
// keyedItems contains all lazily keyed map items
keyedItems map[interface{}]interface{}
// unkeyedItems contains all map items that have not yet been keyed
unkeyedItems []interface{}
}
func (a *mapListImpl) Get(obj interface{}) interface{} {
mobj, ok := obj.(map[string]interface{})
if !ok {
return nil
}
key, ok := a.ks.CompositeKeyFor(mobj)
if !ok {
return nil
}
if match, ok := a.keyedItems[key]; ok {
return match
}
// keep keying items until we either find a match or run out of unkeyed items
for len(a.unkeyedItems) > 0 {
// dequeue an unkeyed item
item := a.unkeyedItems[0]
a.unkeyedItems = a.unkeyedItems[1:]
// key the item
mitem, ok := item.(map[string]interface{})
if !ok {
continue
}
itemKey, ok := a.ks.CompositeKeyFor(mitem)
if !ok {
continue
}
if _, exists := a.keyedItems[itemKey]; !exists {
a.keyedItems[itemKey] = mitem
}
// if it matches, short-circuit
if itemKey == key {
return mitem
}
}
return nil
}
func makeKeyStrategy(sts Schema) keyStrategy {
listMapKeys := sts.XListMapKeys()
if len(listMapKeys) == 1 {
key := listMapKeys[0]
return &singleKeyStrategy{
key: key,
}
}
return &multiKeyStrategy{
sts: sts,
}
}
// MakeMapList returns a queryable interface over the provided x-kubernetes-list-type=map
// keyedItems. If the provided schema is _not_ an array with x-kubernetes-list-type=map, returns an
// empty mapList.
func MakeMapList(sts Schema, items []interface{}) (rv MapList) {
if sts.Type() != "array" || sts.XListType() != "map" || len(sts.XListMapKeys()) == 0 || len(items) == 0 {
return emptyMapList{}
}
ks := makeKeyStrategy(sts)
return &mapListImpl{
sts: sts,
ks: ks,
keyedItems: map[interface{}]interface{}{},
unkeyedItems: items,
}
}

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

@ -0,0 +1,257 @@
/*
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 common
import (
"time"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
apiservercel "k8s.io/apiserver/pkg/cel"
"k8s.io/kube-openapi/pkg/validation/spec"
)
const maxRequestSizeBytes = apiservercel.DefaultMaxRequestSizeBytes
// SchemaDeclType converts the structural schema to a CEL declaration, or returns nil if the
// structural schema should not be exposed in CEL expressions.
// Set isResourceRoot to true for the root of a custom resource or embedded resource.
//
// Schemas with XPreserveUnknownFields not exposed unless they are objects. Array and "maps" schemas
// are not exposed if their items or additionalProperties schemas are not exposed. Object Properties are not exposed
// if their schema is not exposed.
//
// The CEL declaration for objects with XPreserveUnknownFields does not expose unknown fields.
func SchemaDeclType(s Schema, isResourceRoot bool) *apiservercel.DeclType {
if s == nil {
return nil
}
if s.IsXIntOrString() {
// schemas using XIntOrString are not required to have a type.
// intOrStringType represents the x-kubernetes-int-or-string union type in CEL expressions.
// In CEL, the type is represented as dynamic value, which can be thought of as a union type of all types.
// All type checking for XIntOrString is deferred to runtime, so all access to values of this type must
// be guarded with a type check, e.g.:
//
// To require that the string representation be a percentage:
// `type(intOrStringField) == string && intOrStringField.matches(r'(\d+(\.\d+)?%)')`
// To validate requirements on both the int and string representation:
// `type(intOrStringField) == int ? intOrStringField < 5 : double(intOrStringField.replace('%', '')) < 0.5
//
dyn := apiservercel.NewSimpleTypeWithMinSize("dyn", cel.DynType, nil, 1) // smallest value for a serialized x-kubernetes-int-or-string is 0
// handle x-kubernetes-int-or-string by returning the max length/min serialized size of the largest possible string
dyn.MaxElements = maxRequestSizeBytes - 2
return dyn
}
// We ignore XPreserveUnknownFields since we don't support validation rules on
// data that we don't have schema information for.
if isResourceRoot {
// 'apiVersion', 'kind', 'metadata.name' and 'metadata.generateName' are always accessible to validator rules
// at the root of resources, even if not specified in the schema.
// This includes the root of a custom resource and the root of XEmbeddedResource objects.
s = s.WithTypeAndObjectMeta()
}
switch s.Type() {
case "array":
if s.Items() != nil {
itemsType := SchemaDeclType(s.Items(), s.Items().IsXEmbeddedResource())
if itemsType == nil {
return nil
}
var maxItems int64
if s.MaxItems() != nil {
maxItems = zeroIfNegative(*s.MaxItems())
} else {
maxItems = estimateMaxArrayItemsFromMinSize(itemsType.MinSerializedSize)
}
return apiservercel.NewListType(itemsType, maxItems)
}
return nil
case "object":
if s.AdditionalProperties() != nil && s.AdditionalProperties().Schema() != nil {
propsType := SchemaDeclType(s.AdditionalProperties().Schema(), s.AdditionalProperties().Schema().IsXEmbeddedResource())
if propsType != nil {
var maxProperties int64
if s.MaxProperties() != nil {
maxProperties = zeroIfNegative(*s.MaxProperties())
} else {
maxProperties = estimateMaxAdditionalPropertiesFromMinSize(propsType.MinSerializedSize)
}
return apiservercel.NewMapType(apiservercel.StringType, propsType, maxProperties)
}
return nil
}
fields := make(map[string]*apiservercel.DeclField, len(s.Properties()))
required := map[string]bool{}
if s.Required() != nil {
for _, f := range s.Required() {
required[f] = true
}
}
// an object will always be serialized at least as {}, so account for that
minSerializedSize := int64(2)
for name, prop := range s.Properties() {
var enumValues []interface{}
if prop.Enum() != nil {
for _, e := range prop.Enum() {
enumValues = append(enumValues, e)
}
}
if fieldType := SchemaDeclType(prop, prop.IsXEmbeddedResource()); fieldType != nil {
if propName, ok := apiservercel.Escape(name); ok {
fields[propName] = apiservercel.NewDeclField(propName, fieldType, required[name], enumValues, prop.Default())
}
// the min serialized size for an object is 2 (for {}) plus the min size of all its required
// properties
// only include required properties without a default value; default values are filled in
// server-side
if required[name] && prop.Default() == nil {
minSerializedSize += int64(len(name)) + fieldType.MinSerializedSize + 4
}
}
}
objType := apiservercel.NewObjectType("object", fields)
objType.MinSerializedSize = minSerializedSize
return objType
case "string":
switch s.Format() {
case "byte":
byteWithMaxLength := apiservercel.NewSimpleTypeWithMinSize("bytes", cel.BytesType, types.Bytes([]byte{}), apiservercel.MinStringSize)
if s.MaxLength() != nil {
byteWithMaxLength.MaxElements = zeroIfNegative(*s.MaxLength())
} else {
byteWithMaxLength.MaxElements = estimateMaxStringLengthPerRequest(s)
}
return byteWithMaxLength
case "duration":
durationWithMaxLength := apiservercel.NewSimpleTypeWithMinSize("duration", cel.DurationType, types.Duration{Duration: time.Duration(0)}, int64(apiservercel.MinDurationSizeJSON))
durationWithMaxLength.MaxElements = estimateMaxStringLengthPerRequest(s)
return durationWithMaxLength
case "date":
timestampWithMaxLength := apiservercel.NewSimpleTypeWithMinSize("timestamp", cel.TimestampType, types.Timestamp{Time: time.Time{}}, int64(apiservercel.JSONDateSize))
timestampWithMaxLength.MaxElements = estimateMaxStringLengthPerRequest(s)
return timestampWithMaxLength
case "date-time":
timestampWithMaxLength := apiservercel.NewSimpleTypeWithMinSize("timestamp", cel.TimestampType, types.Timestamp{Time: time.Time{}}, int64(apiservercel.MinDatetimeSizeJSON))
timestampWithMaxLength.MaxElements = estimateMaxStringLengthPerRequest(s)
return timestampWithMaxLength
}
strWithMaxLength := apiservercel.NewSimpleTypeWithMinSize("string", cel.StringType, types.String(""), apiservercel.MinStringSize)
if s.MaxLength() != nil {
// multiply the user-provided max length by 4 in the case of an otherwise-untyped string
// we do this because the OpenAPIv3 spec indicates that maxLength is specified in runes/code points,
// but we need to reason about length for things like request size, so we use bytes in this code (and an individual
// unicode code point can be up to 4 bytes long)
strWithMaxLength.MaxElements = zeroIfNegative(*s.MaxLength()) * 4
} else {
strWithMaxLength.MaxElements = estimateMaxStringLengthPerRequest(s)
}
return strWithMaxLength
case "boolean":
return apiservercel.BoolType
case "number":
return apiservercel.DoubleType
case "integer":
return apiservercel.IntType
}
return nil
}
func zeroIfNegative(v int64) int64 {
if v < 0 {
return 0
}
return v
}
// WithTypeAndObjectMeta ensures the kind, apiVersion and
// metadata.name and metadata.generateName properties are specified, making a shallow copy of the provided schema if needed.
func WithTypeAndObjectMeta(s *spec.Schema) *spec.Schema {
if s.Properties != nil &&
s.Properties["kind"].Type.Contains("string") &&
s.Properties["apiVersion"].Type.Contains("string") &&
s.Properties["metadata"].Type.Contains("object") &&
s.Properties["metadata"].Properties != nil &&
s.Properties["metadata"].Properties["name"].Type.Contains("string") &&
s.Properties["metadata"].Properties["generateName"].Type.Contains("string") {
return s
}
result := *s
props := make(map[string]spec.Schema, len(s.Properties))
for k, prop := range s.Properties {
props[k] = prop
}
stringType := spec.StringProperty()
props["kind"] = *stringType
props["apiVersion"] = *stringType
props["metadata"] = spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"name": *stringType,
"generateName": *stringType,
},
},
}
result.Properties = props
return &result
}
// estimateMaxStringLengthPerRequest estimates the maximum string length (in characters)
// of a string compatible with the format requirements in the provided schema.
// must only be called on schemas of type "string" or x-kubernetes-int-or-string: true
func estimateMaxStringLengthPerRequest(s Schema) int64 {
if s.IsXIntOrString() {
return maxRequestSizeBytes - 2
}
switch s.Format() {
case "duration":
return apiservercel.MaxDurationSizeJSON
case "date":
return apiservercel.JSONDateSize
case "date-time":
return apiservercel.MaxDatetimeSizeJSON
default:
// subtract 2 to account for ""
return maxRequestSizeBytes - 2
}
}
// estimateMaxArrayItemsPerRequest estimates the maximum number of array items with
// the provided minimum serialized size that can fit into a single request.
func estimateMaxArrayItemsFromMinSize(minSize int64) int64 {
// subtract 2 to account for [ and ]
return (maxRequestSizeBytes - 2) / (minSize + 1)
}
// estimateMaxAdditionalPropertiesPerRequest estimates the maximum number of additional properties
// with the provided minimum serialized size that can fit into a single request.
func estimateMaxAdditionalPropertiesFromMinSize(minSize int64) int64 {
// 2 bytes for key + "" + colon + comma + smallest possible value, realistically the actual keys
// will all vary in length
keyValuePairSize := minSize + 6
// subtract 2 to account for { and }
return (maxRequestSizeBytes - 2) / keyValuePairSize
}

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

@ -0,0 +1,717 @@
/*
Copyright 2021 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 (
"fmt"
"reflect"
"sync"
"time"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"github.com/google/cel-go/common/types/traits"
"k8s.io/kube-openapi/pkg/validation/strfmt"
"k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apiserver/pkg/cel"
)
// UnstructuredToVal converts a Kubernetes unstructured data element to a CEL Val.
// The root schema of custom resource schema is expected contain type meta and object meta schemas.
// If Embedded resources do not contain type meta and object meta schemas, they will be added automatically.
func UnstructuredToVal(unstructured interface{}, schema Schema) ref.Val {
if unstructured == nil {
if schema.Nullable() {
return types.NullValue
}
return types.NewErr("invalid data, got null for schema with nullable=false")
}
if schema.IsXIntOrString() {
switch v := unstructured.(type) {
case string:
return types.String(v)
case int:
return types.Int(v)
case int32:
return types.Int(v)
case int64:
return types.Int(v)
}
return types.NewErr("invalid data, expected XIntOrString value to be either a string or integer")
}
if schema.Type() == "object" {
m, ok := unstructured.(map[string]interface{})
if !ok {
return types.NewErr("invalid data, expected a map for the provided schema with type=object")
}
if schema.IsXEmbeddedResource() || schema.Properties() != nil {
if schema.IsXEmbeddedResource() {
schema = schema.WithTypeAndObjectMeta()
}
return &unstructuredMap{
value: m,
schema: schema,
propSchema: func(key string) (Schema, bool) {
if schema, ok := schema.Properties()[key]; ok {
return schema, true
}
return nil, false
},
}
}
if schema.AdditionalProperties() != nil && schema.AdditionalProperties().Schema() != nil {
return &unstructuredMap{
value: m,
schema: schema,
propSchema: func(key string) (Schema, bool) {
return schema.AdditionalProperties().Schema(), true
},
}
}
// A object with x-kubernetes-preserve-unknown-fields but no properties or additionalProperties is treated
// as an empty object.
if schema.IsXPreserveUnknownFields() {
return &unstructuredMap{
value: m,
schema: schema,
propSchema: func(key string) (Schema, bool) {
return nil, false
},
}
}
return types.NewErr("invalid object type, expected either Properties or AdditionalProperties with Allows=true and non-empty Schema")
}
if schema.Type() == "array" {
l, ok := unstructured.([]interface{})
if !ok {
return types.NewErr("invalid data, expected an array for the provided schema with type=array")
}
if schema.Items() == nil {
return types.NewErr("invalid array type, expected Items with a non-empty Schema")
}
typedList := unstructuredList{elements: l, itemsSchema: schema.Items()}
listType := schema.XListType()
if listType != "" {
switch listType {
case "map":
mapKeys := schema.XListMapKeys()
return &unstructuredMapList{unstructuredList: typedList, escapedKeyProps: escapeKeyProps(mapKeys)}
case "set":
return &unstructuredSetList{unstructuredList: typedList}
case "atomic":
return &typedList
default:
return types.NewErr("invalid x-kubernetes-list-type, expected 'map', 'set' or 'atomic' but got %s", listType)
}
}
return &typedList
}
if schema.Type() == "string" {
str, ok := unstructured.(string)
if !ok {
return types.NewErr("invalid data, expected string, got %T", unstructured)
}
switch schema.Format() {
case "duration":
d, err := strfmt.ParseDuration(str)
if err != nil {
return types.NewErr("Invalid duration %s: %v", str, err)
}
return types.Duration{Duration: d}
case "date":
d, err := time.Parse(strfmt.RFC3339FullDate, str) // strfmt uses this format for OpenAPIv3 value validation
if err != nil {
return types.NewErr("Invalid date formatted string %s: %v", str, err)
}
return types.Timestamp{Time: d}
case "date-time":
d, err := strfmt.ParseDateTime(str)
if err != nil {
return types.NewErr("Invalid date-time formatted string %s: %v", str, err)
}
return types.Timestamp{Time: time.Time(d)}
case "byte":
base64 := strfmt.Base64{}
err := base64.UnmarshalText([]byte(str))
if err != nil {
return types.NewErr("Invalid byte formatted string %s: %v", str, err)
}
return types.Bytes(base64)
}
return types.String(str)
}
if schema.Type() == "number" {
switch v := unstructured.(type) {
// float representations of whole numbers (e.g. 1.0, 0.0) can convert to int representations (e.g. 1, 0) in yaml
// to json translation, and then get parsed as int64s
case int:
return types.Double(v)
case int32:
return types.Double(v)
case int64:
return types.Double(v)
case float32:
return types.Double(v)
case float64:
return types.Double(v)
default:
return types.NewErr("invalid data, expected float, got %T", unstructured)
}
}
if schema.Type() == "integer" {
switch v := unstructured.(type) {
case int:
return types.Int(v)
case int32:
return types.Int(v)
case int64:
return types.Int(v)
default:
return types.NewErr("invalid data, expected int, got %T", unstructured)
}
}
if schema.Type() == "boolean" {
b, ok := unstructured.(bool)
if !ok {
return types.NewErr("invalid data, expected bool, got %T", unstructured)
}
return types.Bool(b)
}
if schema.IsXPreserveUnknownFields() {
return &unknownPreserved{u: unstructured}
}
return types.NewErr("invalid type, expected object, array, number, integer, boolean or string, or no type with x-kubernetes-int-or-string or x-kubernetes-preserve-unknown-fields is true, got %s", schema.Type())
}
// unknownPreserved represents unknown data preserved in custom resources via x-kubernetes-preserve-unknown-fields.
// It preserves the data at runtime without assuming it is of any particular type and supports only equality checking.
// unknownPreserved should be used only for values are not directly accessible in CEL expressions, i.e. for data
// where there is no corresponding CEL type declaration.
type unknownPreserved struct {
u interface{}
}
func (t *unknownPreserved) ConvertToNative(refType reflect.Type) (interface{}, error) {
return nil, fmt.Errorf("type conversion to '%s' not supported for values preserved by x-kubernetes-preserve-unknown-fields", refType)
}
func (t *unknownPreserved) ConvertToType(typeValue ref.Type) ref.Val {
return types.NewErr("type conversion to '%s' not supported for values preserved by x-kubernetes-preserve-unknown-fields", typeValue.TypeName())
}
func (t *unknownPreserved) Equal(other ref.Val) ref.Val {
return types.Bool(equality.Semantic.DeepEqual(t.u, other.Value()))
}
func (t *unknownPreserved) Type() ref.Type {
return types.UnknownType
}
func (t *unknownPreserved) Value() interface{} {
return t.u // used by Equal checks
}
// unstructuredMapList represents an unstructured data instance of an OpenAPI array with x-kubernetes-list-type=map.
type unstructuredMapList struct {
unstructuredList
escapedKeyProps []string
sync.Once // for for lazy load of mapOfList since it is only needed if Equals is called
mapOfList map[interface{}]interface{}
}
func (t *unstructuredMapList) getMap() map[interface{}]interface{} {
t.Do(func() {
t.mapOfList = make(map[interface{}]interface{}, len(t.elements))
for _, e := range t.elements {
t.mapOfList[t.toMapKey(e)] = e
}
})
return t.mapOfList
}
// toMapKey returns a valid golang map key for the given element of the map list.
// element must be a valid map list entry where all map key props are scalar types (which are comparable in go
// and valid for use in a golang map key).
func (t *unstructuredMapList) toMapKey(element interface{}) interface{} {
eObj, ok := element.(map[string]interface{})
if !ok {
return types.NewErr("unexpected data format for element of array with x-kubernetes-list-type=map: %T", element)
}
// Arrays are comparable in go and may be used as map keys, but maps and slices are not.
// So we can special case small numbers of key props as arrays and fall back to serialization
// for larger numbers of key props
if len(t.escapedKeyProps) == 1 {
return eObj[t.escapedKeyProps[0]]
}
if len(t.escapedKeyProps) == 2 {
return [2]interface{}{eObj[t.escapedKeyProps[0]], eObj[t.escapedKeyProps[1]]}
}
if len(t.escapedKeyProps) == 3 {
return [3]interface{}{eObj[t.escapedKeyProps[0]], eObj[t.escapedKeyProps[1]], eObj[t.escapedKeyProps[2]]}
}
key := make([]interface{}, len(t.escapedKeyProps))
for i, kf := range t.escapedKeyProps {
key[i] = eObj[kf]
}
return fmt.Sprintf("%v", key)
}
// Equal on a map list ignores list element order.
func (t *unstructuredMapList) Equal(other ref.Val) ref.Val {
oMapList, ok := other.(traits.Lister)
if !ok {
return types.MaybeNoSuchOverloadErr(other)
}
sz := types.Int(len(t.elements))
if sz != oMapList.Size() {
return types.False
}
tMap := t.getMap()
for it := oMapList.Iterator(); it.HasNext() == types.True; {
v := it.Next()
k := t.toMapKey(v.Value())
tVal, ok := tMap[k]
if !ok {
return types.False
}
eq := UnstructuredToVal(tVal, t.itemsSchema).Equal(v)
if eq != types.True {
return eq // either false or error
}
}
return types.True
}
// Add for a map list `X + Y` performs a merge where the array positions of all keys in `X` are preserved but the values
// are overwritten by values in `Y` when the key sets of `X` and `Y` intersect. Elements in `Y` with
// non-intersecting keys are appended, retaining their partial order.
func (t *unstructuredMapList) Add(other ref.Val) ref.Val {
oMapList, ok := other.(traits.Lister)
if !ok {
return types.MaybeNoSuchOverloadErr(other)
}
elements := make([]interface{}, len(t.elements))
keyToIdx := map[interface{}]int{}
for i, e := range t.elements {
k := t.toMapKey(e)
keyToIdx[k] = i
elements[i] = e
}
for it := oMapList.Iterator(); it.HasNext() == types.True; {
v := it.Next().Value()
k := t.toMapKey(v)
if overwritePosition, ok := keyToIdx[k]; ok {
elements[overwritePosition] = v
} else {
elements = append(elements, v)
}
}
return &unstructuredMapList{
unstructuredList: unstructuredList{elements: elements, itemsSchema: t.itemsSchema},
escapedKeyProps: t.escapedKeyProps,
}
}
// escapeKeyProps returns identifiers with Escape applied to each.
// Identifiers that cannot be escaped are left as-is. They are inaccessible to CEL programs but are
// are still needed internally to perform equality checks.
func escapeKeyProps(idents []string) []string {
result := make([]string, len(idents))
for i, prop := range idents {
if escaped, ok := cel.Escape(prop); ok {
result[i] = escaped
} else {
result[i] = prop
}
}
return result
}
// unstructuredSetList represents an unstructured data instance of an OpenAPI array with x-kubernetes-list-type=set.
type unstructuredSetList struct {
unstructuredList
escapedKeyProps []string
sync.Once // for for lazy load of setOfList since it is only needed if Equals is called
set map[interface{}]struct{}
}
func (t *unstructuredSetList) getSet() map[interface{}]struct{} {
// sets are only allowed to contain scalar elements, which are comparable in go, and can safely be used as
// golang map keys
t.Do(func() {
t.set = make(map[interface{}]struct{}, len(t.elements))
for _, e := range t.elements {
t.set[e] = struct{}{}
}
})
return t.set
}
// Equal on a map list ignores list element order.
func (t *unstructuredSetList) Equal(other ref.Val) ref.Val {
oSetList, ok := other.(traits.Lister)
if !ok {
return types.MaybeNoSuchOverloadErr(other)
}
sz := types.Int(len(t.elements))
if sz != oSetList.Size() {
return types.False
}
tSet := t.getSet()
for it := oSetList.Iterator(); it.HasNext() == types.True; {
next := it.Next().Value()
_, ok := tSet[next]
if !ok {
return types.False
}
}
return types.True
}
// Add for a set list `X + Y` performs a union where the array positions of all elements in `X` are preserved and
// non-intersecting elements in `Y` are appended, retaining their partial order.
func (t *unstructuredSetList) Add(other ref.Val) ref.Val {
oSetList, ok := other.(traits.Lister)
if !ok {
return types.MaybeNoSuchOverloadErr(other)
}
elements := t.elements
set := t.getSet()
for it := oSetList.Iterator(); it.HasNext() == types.True; {
next := it.Next().Value()
if _, ok := set[next]; !ok {
set[next] = struct{}{}
elements = append(elements, next)
}
}
return &unstructuredSetList{
unstructuredList: unstructuredList{elements: elements, itemsSchema: t.itemsSchema},
escapedKeyProps: t.escapedKeyProps,
}
}
// unstructuredList represents an unstructured data instance of an OpenAPI array with x-kubernetes-list-type=atomic (the default).
type unstructuredList struct {
elements []interface{}
itemsSchema Schema
}
var _ = traits.Lister(&unstructuredList{})
func (t *unstructuredList) ConvertToNative(typeDesc reflect.Type) (interface{}, error) {
switch typeDesc.Kind() {
case reflect.Slice:
switch t.itemsSchema.Type() {
// Workaround for https://github.com/kubernetes/kubernetes/issues/117590 until we
// resolve the desired behavior in cel-go via https://github.com/google/cel-go/issues/688
case "string":
var result []string
for _, e := range t.elements {
s, ok := e.(string)
if !ok {
return nil, fmt.Errorf("expected all elements to be of type string, but got %T", e)
}
result = append(result, s)
}
return result, nil
default:
return t.elements, nil
}
}
return nil, fmt.Errorf("type conversion error from '%s' to '%s'", t.Type(), typeDesc)
}
func (t *unstructuredList) ConvertToType(typeValue ref.Type) ref.Val {
switch typeValue {
case types.ListType:
return t
case types.TypeType:
return types.ListType
}
return types.NewErr("type conversion error from '%s' to '%s'", t.Type(), typeValue.TypeName())
}
func (t *unstructuredList) Equal(other ref.Val) ref.Val {
oList, ok := other.(traits.Lister)
if !ok {
return types.MaybeNoSuchOverloadErr(other)
}
sz := types.Int(len(t.elements))
if sz != oList.Size() {
return types.False
}
for i := types.Int(0); i < sz; i++ {
eq := t.Get(i).Equal(oList.Get(i))
if eq != types.True {
return eq // either false or error
}
}
return types.True
}
func (t *unstructuredList) Type() ref.Type {
return types.ListType
}
func (t *unstructuredList) Value() interface{} {
return t.elements
}
func (t *unstructuredList) Add(other ref.Val) ref.Val {
oList, ok := other.(traits.Lister)
if !ok {
return types.MaybeNoSuchOverloadErr(other)
}
elements := t.elements
for it := oList.Iterator(); it.HasNext() == types.True; {
next := it.Next().Value()
elements = append(elements, next)
}
return &unstructuredList{elements: elements, itemsSchema: t.itemsSchema}
}
func (t *unstructuredList) Contains(val ref.Val) ref.Val {
if types.IsUnknownOrError(val) {
return val
}
var err ref.Val
sz := len(t.elements)
for i := 0; i < sz; i++ {
elem := UnstructuredToVal(t.elements[i], t.itemsSchema)
cmp := elem.Equal(val)
b, ok := cmp.(types.Bool)
if !ok && err == nil {
err = types.MaybeNoSuchOverloadErr(cmp)
}
if b == types.True {
return types.True
}
}
if err != nil {
return err
}
return types.False
}
func (t *unstructuredList) Get(idx ref.Val) ref.Val {
iv, isInt := idx.(types.Int)
if !isInt {
return types.ValOrErr(idx, "unsupported index: %v", idx)
}
i := int(iv)
if i < 0 || i >= len(t.elements) {
return types.NewErr("index out of bounds: %v", idx)
}
return UnstructuredToVal(t.elements[i], t.itemsSchema)
}
func (t *unstructuredList) Iterator() traits.Iterator {
items := make([]ref.Val, len(t.elements))
for i, item := range t.elements {
itemCopy := item
items[i] = UnstructuredToVal(itemCopy, t.itemsSchema)
}
return &listIterator{unstructuredList: t, items: items}
}
type listIterator struct {
*unstructuredList
items []ref.Val
idx int
}
func (it *listIterator) HasNext() ref.Val {
return types.Bool(it.idx < len(it.items))
}
func (it *listIterator) Next() ref.Val {
item := it.items[it.idx]
it.idx++
return item
}
func (t *unstructuredList) Size() ref.Val {
return types.Int(len(t.elements))
}
// unstructuredMap represented an unstructured data instance of an OpenAPI object.
type unstructuredMap struct {
value map[string]interface{}
schema Schema
// propSchema finds the schema to use for a particular map key.
propSchema func(key string) (Schema, bool)
}
var _ = traits.Mapper(&unstructuredMap{})
func (t *unstructuredMap) ConvertToNative(typeDesc reflect.Type) (interface{}, error) {
switch typeDesc.Kind() {
case reflect.Map:
return t.value, nil
}
return nil, fmt.Errorf("type conversion error from '%s' to '%s'", t.Type(), typeDesc)
}
func (t *unstructuredMap) ConvertToType(typeValue ref.Type) ref.Val {
switch typeValue {
case types.MapType:
return t
case types.TypeType:
return types.MapType
}
return types.NewErr("type conversion error from '%s' to '%s'", t.Type(), typeValue.TypeName())
}
func (t *unstructuredMap) Equal(other ref.Val) ref.Val {
oMap, isMap := other.(traits.Mapper)
if !isMap {
return types.MaybeNoSuchOverloadErr(other)
}
if t.Size() != oMap.Size() {
return types.False
}
for key, value := range t.value {
if propSchema, ok := t.propSchema(key); ok {
ov, found := oMap.Find(types.String(key))
if !found {
return types.False
}
v := UnstructuredToVal(value, propSchema)
vEq := v.Equal(ov)
if vEq != types.True {
return vEq // either false or error
}
} else {
// Must be an object with properties.
// Since we've encountered an unknown field, fallback to unstructured equality checking.
ouMap, ok := other.(*unstructuredMap)
if !ok {
// The compiler ensures equality is against the same type of object, so this should be unreachable
return types.MaybeNoSuchOverloadErr(other)
}
if oValue, ok := ouMap.value[key]; ok {
if !equality.Semantic.DeepEqual(value, oValue) {
return types.False
}
}
}
}
return types.True
}
func (t *unstructuredMap) Type() ref.Type {
return types.MapType
}
func (t *unstructuredMap) Value() interface{} {
return t.value
}
func (t *unstructuredMap) Contains(key ref.Val) ref.Val {
v, found := t.Find(key)
if v != nil && types.IsUnknownOrError(v) {
return v
}
return types.Bool(found)
}
func (t *unstructuredMap) Get(key ref.Val) ref.Val {
v, found := t.Find(key)
if found {
return v
}
return types.ValOrErr(key, "no such key: %v", key)
}
func (t *unstructuredMap) Iterator() traits.Iterator {
isObject := t.schema.Properties() != nil
keys := make([]ref.Val, len(t.value))
i := 0
for k := range t.value {
if _, ok := t.propSchema(k); ok {
mapKey := k
if isObject {
if escaped, ok := cel.Escape(k); ok {
mapKey = escaped
}
}
keys[i] = types.String(mapKey)
i++
}
}
return &mapIterator{unstructuredMap: t, keys: keys}
}
type mapIterator struct {
*unstructuredMap
keys []ref.Val
idx int
}
func (it *mapIterator) HasNext() ref.Val {
return types.Bool(it.idx < len(it.keys))
}
func (it *mapIterator) Next() ref.Val {
key := it.keys[it.idx]
it.idx++
return key
}
func (t *unstructuredMap) Size() ref.Val {
return types.Int(len(t.value))
}
func (t *unstructuredMap) Find(key ref.Val) (ref.Val, bool) {
isObject := t.schema.Properties() != nil
keyStr, ok := key.(types.String)
if !ok {
return types.MaybeNoSuchOverloadErr(key), true
}
k := keyStr.Value().(string)
if isObject {
k, ok = cel.Unescape(k)
if !ok {
return nil, false
}
}
if v, ok := t.value[k]; ok {
// If this is an object with properties, not an object with additionalProperties,
// then null valued nullable fields are treated the same as absent optional fields.
if isObject && v == nil {
return nil, false
}
if propSchema, ok := t.propSchema(k); ok {
return UnstructuredToVal(v, propSchema), true
}
}
return nil, false
}

119
vendor/k8s.io/apiserver/pkg/cel/composited.go generated vendored Normal file
View File

@ -0,0 +1,119 @@
/*
Copyright 2023 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 (
"github.com/google/cel-go/common/types/ref"
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
)
var _ ref.TypeProvider = (*CompositedTypeProvider)(nil)
var _ ref.TypeAdapter = (*CompositedTypeAdapter)(nil)
// CompositedTypeProvider is the provider that tries each of the underlying
// providers in order, and returns result of the first successful attempt.
type CompositedTypeProvider struct {
// Providers contains the underlying type providers.
// If Providers is empty, the CompositedTypeProvider becomes no-op provider.
Providers []ref.TypeProvider
}
// EnumValue finds out the numeric value of the given enum name.
// The result comes from first provider that returns non-nil.
func (c *CompositedTypeProvider) EnumValue(enumName string) ref.Val {
for _, p := range c.Providers {
val := p.EnumValue(enumName)
if val != nil {
return val
}
}
return nil
}
// FindIdent takes a qualified identifier name and returns a Value if one
// exists. The result comes from first provider that returns non-nil.
func (c *CompositedTypeProvider) FindIdent(identName string) (ref.Val, bool) {
for _, p := range c.Providers {
val, ok := p.FindIdent(identName)
if ok {
return val, ok
}
}
return nil, false
}
// FindType finds the Type given a qualified type name, or return false
// if none of the providers finds the type.
// If any of the providers find the type, the first provider that returns true
// will be the result.
func (c *CompositedTypeProvider) FindType(typeName string) (*exprpb.Type, bool) {
for _, p := range c.Providers {
typ, ok := p.FindType(typeName)
if ok {
return typ, ok
}
}
return nil, false
}
// FindFieldType returns the field type for a checked type value. Returns
// false if none of the providers can find the type.
// If multiple providers can find the field, the result is taken from
// the first that does.
func (c *CompositedTypeProvider) FindFieldType(messageType string, fieldName string) (*ref.FieldType, bool) {
for _, p := range c.Providers {
ft, ok := p.FindFieldType(messageType, fieldName)
if ok {
return ft, ok
}
}
return nil, false
}
// NewValue creates a new type value from a qualified name and map of field
// name to value.
// If multiple providers can create the new type, the first that returns
// non-nil will decide the result.
func (c *CompositedTypeProvider) NewValue(typeName string, fields map[string]ref.Val) ref.Val {
for _, p := range c.Providers {
v := p.NewValue(typeName, fields)
if v != nil {
return v
}
}
return nil
}
// CompositedTypeAdapter is the adapter that tries each of the underlying
// type adapter in order until the first successfully conversion.
type CompositedTypeAdapter struct {
// Adapters contains underlying type adapters.
// If Adapters is empty, the CompositedTypeAdapter becomes a no-op adapter.
Adapters []ref.TypeAdapter
}
// NativeToValue takes the value and convert it into a ref.Val
// The result comes from the first TypeAdapter that returns non-nil.
func (c *CompositedTypeAdapter) NativeToValue(value interface{}) ref.Val {
for _, a := range c.Adapters {
v := a.NativeToValue(value)
if v != nil {
return v
}
}
return nil
}

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

@ -0,0 +1,580 @@
/*
Copyright 2023 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 (
"context"
"fmt"
"reflect"
"strings"
"github.com/google/cel-go/cel"
"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/runtime/schema"
"k8s.io/apiserver/pkg/authentication/serviceaccount"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer"
)
// Authz provides a CEL function library extension for performing authorization checks.
// Note that authorization checks are only supported for CEL expression fields in the API
// where an 'authorizer' variable is provided to the CEL expression. See the
// documentation of API fields where CEL expressions are used to learn if the 'authorizer'
// variable is provided.
//
// path
//
// Returns a PathCheck configured to check authorization for a non-resource request
// path (e.g. /healthz). If path is an empty string, an error is returned.
// Note that the leading '/' is not required.
//
// <Authorizer>.path(<string>) <PathCheck>
//
// Examples:
//
// authorizer.path('/healthz') // returns a PathCheck for the '/healthz' API path
// authorizer.path('') // results in "path must not be empty" error
// authorizer.path(' ') // results in "path must not be empty" error
//
// group
//
// Returns a GroupCheck configured to check authorization for the API resources for
// a particular API group.
// Note that authorization checks are only supported for CEL expression fields in the API
// where an 'authorizer' variable is provided to the CEL expression. Check the
// documentation of API fields where CEL expressions are used to learn if the 'authorizer'
// variable is provided.
//
// <Authorizer>.group(<string>) <GroupCheck>
//
// Examples:
//
// authorizer.group('apps') // returns a GroupCheck for the 'apps' API group
// authorizer.group('') // returns a GroupCheck for the core API group
// authorizer.group('example.com') // returns a GroupCheck for the custom resources in the 'example.com' API group
//
// serviceAccount
//
// Returns an Authorizer configured to check authorization for the provided service account namespace and name.
// If the name is not a valid DNS subdomain string (as defined by RFC 1123), an error is returned.
// If the namespace is not a valid DNS label (as defined by RFC 1123), an error is returned.
//
// <Authorizer>.serviceAccount(<string>, <string>) <Authorizer>
//
// Examples:
//
// authorizer.serviceAccount('default', 'myserviceaccount') // returns an Authorizer for the service account with namespace 'default' and name 'myserviceaccount'
// authorizer.serviceAccount('not@a#valid!namespace', 'validname') // returns an error
// authorizer.serviceAccount('valid.example.com', 'invalid@*name') // returns an error
//
// resource
//
// Returns a ResourceCheck configured to check authorization for a particular API resource.
// Note that the provided resource string should be a lower case plural name of a Kubernetes API resource.
//
// <GroupCheck>.resource(<string>) <ResourceCheck>
//
// Examples:
//
// authorizer.group('apps').resource('deployments') // returns a ResourceCheck for the 'deployments' resources in the 'apps' group.
// authorizer.group('').resource('pods') // returns a ResourceCheck for the 'pods' resources in the core group.
// authorizer.group('apps').resource('') // results in "resource must not be empty" error
// authorizer.group('apps').resource(' ') // results in "resource must not be empty" error
//
// subresource
//
// Returns a ResourceCheck configured to check authorization for a particular subresource of an API resource.
// If subresource is set to "", the subresource field of this ResourceCheck is considered unset.
//
// <ResourceCheck>.subresource(<string>) <ResourceCheck>
//
// Examples:
//
// authorizer.group('').resource('pods').subresource('status') // returns a ResourceCheck the 'status' subresource of 'pods'
// authorizer.group('apps').resource('deployments').subresource('scale') // returns a ResourceCheck the 'scale' subresource of 'deployments'
// authorizer.group('example.com').resource('widgets').subresource('scale') // returns a ResourceCheck for the 'scale' subresource of the 'widgets' custom resource
// authorizer.group('example.com').resource('widgets').subresource('') // returns a ResourceCheck for the 'widgets' resource.
//
// namespace
//
// Returns a ResourceCheck configured to check authorization for a particular namespace.
// For cluster scoped resources, namespace() does not need to be called; namespace defaults
// to "", which is the correct namespace value to use to check cluster scoped resources.
// If namespace is set to "", the ResourceCheck will check authorization for the cluster scope.
//
// <ResourceCheck>.namespace(<string>) <ResourceCheck>
//
// Examples:
//
// authorizer.group('apps').resource('deployments').namespace('test') // returns a ResourceCheck for 'deployments' in the 'test' namespace
// authorizer.group('').resource('pods').namespace('default') // returns a ResourceCheck for 'pods' in the 'default' namespace
// authorizer.group('').resource('widgets').namespace('') // returns a ResourceCheck for 'widgets' in the cluster scope
//
// name
//
// Returns a ResourceCheck configured to check authorization for a particular resource name.
// If name is set to "", the name field of this ResourceCheck is considered unset.
//
// <ResourceCheck>.name(<name>) <ResourceCheck>
//
// Examples:
//
// authorizer.group('apps').resource('deployments').namespace('test').name('backend') // returns a ResourceCheck for the 'backend' 'deployments' resource in the 'test' namespace
// authorizer.group('apps').resource('deployments').namespace('test').name('') // returns a ResourceCheck for the 'deployments' resource in the 'test' namespace
//
// check
//
// For PathCheck, checks if the principal (user or service account) that sent the request is authorized for the HTTP request verb of the path.
// For ResourceCheck, checks if the principal (user or service account) that sent the request is authorized for the API verb and the configured authorization checks of the ResourceCheck.
// The check operation can be expensive, particularly in clusters using the webhook authorization mode.
//
// <PathCheck>.check(<check>) <Decision>
// <ResourceCheck>.check(<check>) <Decision>
//
// Examples:
//
// authorizer.group('').resource('pods').namespace('default').check('create') // Checks if the principal (user or service account) is authorized create pods in the 'default' namespace.
// authorizer.path('/healthz').check('get') // Checks if the principal (user or service account) is authorized to make HTTP GET requests to the /healthz API path.
//
// allowed
//
// Returns true if the authorizer's decision for the check is "allow". Note that if the authorizer's decision is
// "no opinion", that the 'allowed' function will return false.
//
// <Decision>.allowed() <bool>
//
// Examples:
//
// authorizer.group('').resource('pods').namespace('default').check('create').allowed() // Returns true if the principal (user or service account) is allowed create pods in the 'default' namespace.
// authorizer.path('/healthz').check('get').allowed() // Returns true if the principal (user or service account) is allowed to make HTTP GET requests to the /healthz API path.
//
// reason
//
// Returns a string reason for the authorization decision
//
// <Decision>.reason() <string>
//
// Examples:
//
// authorizer.path('/healthz').check('GET').reason()
func Authz() cel.EnvOption {
return cel.Lib(authzLib)
}
var authzLib = &authz{}
type authz struct{}
var authzLibraryDecls = map[string][]cel.FunctionOpt{
"path": {
cel.MemberOverload("authorizer_path", []*cel.Type{AuthorizerType, cel.StringType}, PathCheckType,
cel.BinaryBinding(authorizerPath))},
"group": {
cel.MemberOverload("authorizer_group", []*cel.Type{AuthorizerType, cel.StringType}, GroupCheckType,
cel.BinaryBinding(authorizerGroup))},
"serviceAccount": {
cel.MemberOverload("authorizer_serviceaccount", []*cel.Type{AuthorizerType, cel.StringType, cel.StringType}, AuthorizerType,
cel.FunctionBinding(authorizerServiceAccount))},
"resource": {
cel.MemberOverload("groupcheck_resource", []*cel.Type{GroupCheckType, cel.StringType}, ResourceCheckType,
cel.BinaryBinding(groupCheckResource))},
"subresource": {
cel.MemberOverload("resourcecheck_subresource", []*cel.Type{ResourceCheckType, cel.StringType}, ResourceCheckType,
cel.BinaryBinding(resourceCheckSubresource))},
"namespace": {
cel.MemberOverload("resourcecheck_namespace", []*cel.Type{ResourceCheckType, cel.StringType}, ResourceCheckType,
cel.BinaryBinding(resourceCheckNamespace))},
"name": {
cel.MemberOverload("resourcecheck_name", []*cel.Type{ResourceCheckType, cel.StringType}, ResourceCheckType,
cel.BinaryBinding(resourceCheckName))},
"check": {
cel.MemberOverload("pathcheck_check", []*cel.Type{PathCheckType, cel.StringType}, DecisionType,
cel.BinaryBinding(pathCheckCheck)),
cel.MemberOverload("resourcecheck_check", []*cel.Type{ResourceCheckType, cel.StringType}, DecisionType,
cel.BinaryBinding(resourceCheckCheck))},
"allowed": {
cel.MemberOverload("decision_allowed", []*cel.Type{DecisionType}, cel.BoolType,
cel.UnaryBinding(decisionAllowed))},
"reason": {
cel.MemberOverload("decision_reason", []*cel.Type{DecisionType}, cel.StringType,
cel.UnaryBinding(decisionReason))},
}
func (*authz) CompileOptions() []cel.EnvOption {
options := make([]cel.EnvOption, 0, len(authzLibraryDecls))
for name, overloads := range authzLibraryDecls {
options = append(options, cel.Function(name, overloads...))
}
return options
}
func (*authz) ProgramOptions() []cel.ProgramOption {
return []cel.ProgramOption{}
}
func authorizerPath(arg1, arg2 ref.Val) ref.Val {
authz, ok := arg1.(authorizerVal)
if !ok {
return types.MaybeNoSuchOverloadErr(arg1)
}
path, ok := arg2.Value().(string)
if !ok {
return types.MaybeNoSuchOverloadErr(arg1)
}
if len(strings.TrimSpace(path)) == 0 {
return types.NewErr("path must not be empty")
}
return authz.pathCheck(path)
}
func authorizerGroup(arg1, arg2 ref.Val) ref.Val {
authz, ok := arg1.(authorizerVal)
if !ok {
return types.MaybeNoSuchOverloadErr(arg1)
}
group, ok := arg2.Value().(string)
if !ok {
return types.MaybeNoSuchOverloadErr(arg1)
}
return authz.groupCheck(group)
}
func authorizerServiceAccount(args ...ref.Val) ref.Val {
argn := len(args)
if argn != 3 {
return types.NoSuchOverloadErr()
}
authz, ok := args[0].(authorizerVal)
if !ok {
return types.MaybeNoSuchOverloadErr(args[0])
}
namespace, ok := args[1].Value().(string)
if !ok {
return types.MaybeNoSuchOverloadErr(args[1])
}
name, ok := args[2].Value().(string)
if !ok {
return types.MaybeNoSuchOverloadErr(args[2])
}
if errors := apimachineryvalidation.ValidateServiceAccountName(name, false); len(errors) > 0 {
return types.NewErr("Invalid service account name")
}
if errors := apimachineryvalidation.ValidateNamespaceName(namespace, false); len(errors) > 0 {
return types.NewErr("Invalid service account namespace")
}
return authz.serviceAccount(namespace, name)
}
func groupCheckResource(arg1, arg2 ref.Val) ref.Val {
groupCheck, ok := arg1.(groupCheckVal)
if !ok {
return types.MaybeNoSuchOverloadErr(arg1)
}
resource, ok := arg2.Value().(string)
if !ok {
return types.MaybeNoSuchOverloadErr(arg1)
}
if len(strings.TrimSpace(resource)) == 0 {
return types.NewErr("resource must not be empty")
}
return groupCheck.resourceCheck(resource)
}
func resourceCheckSubresource(arg1, arg2 ref.Val) ref.Val {
resourceCheck, ok := arg1.(resourceCheckVal)
if !ok {
return types.MaybeNoSuchOverloadErr(arg1)
}
subresource, ok := arg2.Value().(string)
if !ok {
return types.MaybeNoSuchOverloadErr(arg1)
}
result := resourceCheck
result.subresource = subresource
return result
}
func resourceCheckNamespace(arg1, arg2 ref.Val) ref.Val {
resourceCheck, ok := arg1.(resourceCheckVal)
if !ok {
return types.MaybeNoSuchOverloadErr(arg1)
}
namespace, ok := arg2.Value().(string)
if !ok {
return types.MaybeNoSuchOverloadErr(arg1)
}
result := resourceCheck
result.namespace = namespace
return result
}
func resourceCheckName(arg1, arg2 ref.Val) ref.Val {
resourceCheck, ok := arg1.(resourceCheckVal)
if !ok {
return types.MaybeNoSuchOverloadErr(arg1)
}
name, ok := arg2.Value().(string)
if !ok {
return types.MaybeNoSuchOverloadErr(arg1)
}
result := resourceCheck
result.name = name
return result
}
func pathCheckCheck(arg1, arg2 ref.Val) ref.Val {
pathCheck, ok := arg1.(pathCheckVal)
if !ok {
return types.MaybeNoSuchOverloadErr(arg1)
}
httpRequestVerb, ok := arg2.Value().(string)
if !ok {
return types.MaybeNoSuchOverloadErr(arg1)
}
return pathCheck.Authorize(context.TODO(), httpRequestVerb)
}
func resourceCheckCheck(arg1, arg2 ref.Val) ref.Val {
resourceCheck, ok := arg1.(resourceCheckVal)
if !ok {
return types.MaybeNoSuchOverloadErr(arg1)
}
apiVerb, ok := arg2.Value().(string)
if !ok {
return types.MaybeNoSuchOverloadErr(arg1)
}
return resourceCheck.Authorize(context.TODO(), apiVerb)
}
func decisionAllowed(arg ref.Val) ref.Val {
decision, ok := arg.(decisionVal)
if !ok {
return types.MaybeNoSuchOverloadErr(arg)
}
return types.Bool(decision.authDecision == authorizer.DecisionAllow)
}
func decisionReason(arg ref.Val) ref.Val {
decision, ok := arg.(decisionVal)
if !ok {
return types.MaybeNoSuchOverloadErr(arg)
}
return types.String(decision.reason)
}
var (
AuthorizerType = cel.ObjectType("kubernetes.authorization.Authorizer")
PathCheckType = cel.ObjectType("kubernetes.authorization.PathCheck")
GroupCheckType = cel.ObjectType("kubernetes.authorization.GroupCheck")
ResourceCheckType = cel.ObjectType("kubernetes.authorization.ResourceCheck")
DecisionType = cel.ObjectType("kubernetes.authorization.Decision")
)
// Resource represents an API resource
type Resource interface {
// GetName returns the name of the object as presented in the request. On a CREATE operation, the client
// may omit name and rely on the server to generate the name. If that is the case, this method will return
// the empty string
GetName() string
// GetNamespace is the namespace associated with the request (if any)
GetNamespace() string
// GetResource is the name of the resource being requested. This is not the kind. For example: pods
GetResource() schema.GroupVersionResource
// GetSubresource is the name of the subresource being requested. This is a different resource, scoped to the parent resource, but it may have a different kind.
// For instance, /pods has the resource "pods" and the kind "Pod", while /pods/foo/status has the resource "pods", the sub resource "status", and the kind "Pod"
// (because status operates on pods). The binding resource for a pod though may be /pods/foo/binding, which has resource "pods", subresource "binding", and kind "Binding".
GetSubresource() string
}
func NewAuthorizerVal(userInfo user.Info, authorizer authorizer.Authorizer) ref.Val {
return authorizerVal{receiverOnlyObjectVal: receiverOnlyVal(AuthorizerType), userInfo: userInfo, authAuthorizer: authorizer}
}
func NewResourceAuthorizerVal(userInfo user.Info, authorizer authorizer.Authorizer, requestResource Resource) ref.Val {
a := authorizerVal{receiverOnlyObjectVal: receiverOnlyVal(AuthorizerType), userInfo: userInfo, authAuthorizer: authorizer}
resource := requestResource.GetResource()
g := a.groupCheck(resource.Group)
r := g.resourceCheck(resource.Resource)
r.subresource = requestResource.GetSubresource()
r.namespace = requestResource.GetNamespace()
r.name = requestResource.GetName()
return r
}
type authorizerVal struct {
receiverOnlyObjectVal
userInfo user.Info
authAuthorizer authorizer.Authorizer
}
func (a authorizerVal) pathCheck(path string) pathCheckVal {
return pathCheckVal{receiverOnlyObjectVal: receiverOnlyVal(PathCheckType), authorizer: a, path: path}
}
func (a authorizerVal) groupCheck(group string) groupCheckVal {
return groupCheckVal{receiverOnlyObjectVal: receiverOnlyVal(GroupCheckType), authorizer: a, group: group}
}
func (a authorizerVal) serviceAccount(namespace, name string) authorizerVal {
sa := &serviceaccount.ServiceAccountInfo{Name: name, Namespace: namespace}
return authorizerVal{
receiverOnlyObjectVal: receiverOnlyVal(AuthorizerType),
userInfo: sa.UserInfo(),
authAuthorizer: a.authAuthorizer,
}
}
type pathCheckVal struct {
receiverOnlyObjectVal
authorizer authorizerVal
path string
}
func (a pathCheckVal) Authorize(ctx context.Context, verb string) ref.Val {
attr := &authorizer.AttributesRecord{
Path: a.path,
Verb: verb,
User: a.authorizer.userInfo,
}
decision, reason, err := a.authorizer.authAuthorizer.Authorize(ctx, attr)
if err != nil {
return types.NewErr("error in authorization check: %v", err)
}
return newDecision(decision, reason)
}
type groupCheckVal struct {
receiverOnlyObjectVal
authorizer authorizerVal
group string
}
func (g groupCheckVal) resourceCheck(resource string) resourceCheckVal {
return resourceCheckVal{receiverOnlyObjectVal: receiverOnlyVal(ResourceCheckType), groupCheck: g, resource: resource}
}
type resourceCheckVal struct {
receiverOnlyObjectVal
groupCheck groupCheckVal
resource string
subresource string
namespace string
name string
}
func (a resourceCheckVal) Authorize(ctx context.Context, verb string) ref.Val {
attr := &authorizer.AttributesRecord{
ResourceRequest: true,
APIGroup: a.groupCheck.group,
APIVersion: "*",
Resource: a.resource,
Subresource: a.subresource,
Namespace: a.namespace,
Name: a.name,
Verb: verb,
User: a.groupCheck.authorizer.userInfo,
}
decision, reason, err := a.groupCheck.authorizer.authAuthorizer.Authorize(ctx, attr)
if err != nil {
return types.NewErr("error in authorization check: %v", err)
}
return newDecision(decision, reason)
}
func newDecision(authDecision authorizer.Decision, reason string) decisionVal {
return decisionVal{receiverOnlyObjectVal: receiverOnlyVal(DecisionType), authDecision: authDecision, reason: reason}
}
type decisionVal struct {
receiverOnlyObjectVal
authDecision authorizer.Decision
reason string
}
// receiverOnlyObjectVal provides an implementation of ref.Val for
// any object type that has receiver functions but does not expose any fields to
// CEL.
type receiverOnlyObjectVal struct {
typeValue *types.TypeValue
}
// receiverOnlyVal returns a receiverOnlyObjectVal for the given type.
func receiverOnlyVal(objectType *cel.Type) receiverOnlyObjectVal {
return receiverOnlyObjectVal{typeValue: types.NewTypeValue(objectType.String())}
}
// ConvertToNative implements ref.Val.ConvertToNative.
func (a receiverOnlyObjectVal) ConvertToNative(typeDesc reflect.Type) (any, error) {
return nil, fmt.Errorf("type conversion error from '%s' to '%v'", a.typeValue.String(), typeDesc)
}
// ConvertToType implements ref.Val.ConvertToType.
func (a receiverOnlyObjectVal) ConvertToType(typeVal ref.Type) ref.Val {
switch typeVal {
case a.typeValue:
return a
case types.TypeType:
return a.typeValue
}
return types.NewErr("type conversion error from '%s' to '%s'", a.typeValue, typeVal)
}
// Equal implements ref.Val.Equal.
func (a receiverOnlyObjectVal) Equal(other ref.Val) ref.Val {
o, ok := other.(receiverOnlyObjectVal)
if !ok {
return types.MaybeNoSuchOverloadErr(other)
}
return types.Bool(a == o)
}
// Type implements ref.Val.Type.
func (a receiverOnlyObjectVal) Type() ref.Type {
return a.typeValue
}
// Value implements ref.Val.Value.
func (a receiverOnlyObjectVal) Value() any {
return types.NoSuchOverloadErr()
}

View File

@ -36,6 +36,15 @@ type CostEstimator struct {
func (l *CostEstimator) CallCost(function, overloadId string, args []ref.Val, result ref.Val) *uint64 {
switch function {
case "check":
// An authorization check has a fixed cost
// This cost is set to allow for only two authorization checks per expression
cost := uint64(350000)
return &cost
case "serviceAccount", "path", "group", "resource", "subresource", "namespace", "name", "allowed", "denied", "reason":
// All authorization builder and accessor functions have a nominal cost
cost := uint64(1)
return &cost
case "isSorted", "sum", "max", "min", "indexOf", "lastIndexOf":
var cost uint64
if len(args) > 0 {
@ -78,6 +87,13 @@ func (l *CostEstimator) EstimateCallCost(function, overloadId string, target *ch
// WARNING: Any changes to this code impact API compatibility! The estimated cost is used to determine which CEL rules may be written to a
// CRD and any change (cost increases and cost decreases) are breaking.
switch function {
case "check":
// An authorization check has a fixed cost
// This cost is set to allow for only two authorization checks per expression
return &checker.CallEstimate{CostEstimate: checker.CostEstimate{Min: 350000, Max: 350000}}
case "serviceAccount", "path", "group", "resource", "subresource", "namespace", "name", "allowed", "denied", "reason":
// All authorization builder and accessor functions have a nominal cost
return &checker.CallEstimate{CostEstimate: checker.CostEstimate{Min: 1, Max: 1}}
case "isSorted", "sum", "max", "min", "indexOf", "lastIndexOf":
if target != nil {
// Charge 1 cost for comparing each element in the list
@ -94,7 +110,6 @@ func (l *CostEstimator) EstimateCallCost(function, overloadId string, target *ch
} else { // the target is a string, which is supported by indexOf and lastIndexOf
return &checker.CallEstimate{CostEstimate: l.sizeEstimate(*target).MultiplyByCostFactor(common.StringTraversalCostFactor)}
}
}
case "url":
if len(args) == 1 {

View File

@ -29,6 +29,7 @@ var k8sExtensionLibs = []cel.EnvOption{
URLs(),
Regex(),
Lists(),
Authz(),
}
var ExtensionLibRegexOptimizations = []*interpreter.RegexOptimization{FindRegexOptimization, FindAllRegexOptimization}

View File

@ -61,9 +61,9 @@ import (
//
// - getScheme: If absent in the URL, returns an empty string.
//
// - getHostname: IPv6 addresses are returned with braces, e.g. "[::1]". If absent in the URL, returns an empty string.
// - getHostname: IPv6 addresses are returned without braces, e.g. "::1". If absent in the URL, returns an empty string.
//
// - getHost: IPv6 addresses are returned without braces, e.g. "::1". If absent in the URL, returns an empty string.
// - getHost: IPv6 addresses are returned with braces, e.g. "[::1]". If absent in the URL, returns an empty string.
//
// - getEscapedPath: The string returned by getEscapedPath is URL escaped, e.g. "with space" becomes "with%20space".
// If absent in the URL, returns an empty string.

View File

@ -16,9 +16,11 @@ limitations under the License.
package cel
import celconfig "k8s.io/apiserver/pkg/apis/cel"
const (
// DefaultMaxRequestSizeBytes is the size of the largest request that will be accepted
DefaultMaxRequestSizeBytes = int64(3 * 1024 * 1024)
DefaultMaxRequestSizeBytes = celconfig.MaxRequestSizeBytes
// MaxDurationSizeJSON
// OpenAPI duration strings follow RFC 3339, section 5.6 - see the comment on maxDatetimeSizeJSON

147
vendor/k8s.io/apiserver/pkg/cel/openapi/adaptor.go generated vendored Normal file
View File

@ -0,0 +1,147 @@
/*
Copyright 2023 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 openapi
import (
"github.com/google/cel-go/common/types/ref"
apiservercel "k8s.io/apiserver/pkg/cel"
"k8s.io/apiserver/pkg/cel/common"
"k8s.io/kube-openapi/pkg/validation/spec"
)
var _ common.Schema = (*Schema)(nil)
var _ common.SchemaOrBool = (*SchemaOrBool)(nil)
type Schema struct {
Schema *spec.Schema
}
type SchemaOrBool struct {
SchemaOrBool *spec.SchemaOrBool
}
func (sb *SchemaOrBool) Schema() common.Schema {
return &Schema{Schema: sb.SchemaOrBool.Schema}
}
func (sb *SchemaOrBool) Allows() bool {
return sb.SchemaOrBool.Allows
}
func (s *Schema) Type() string {
if len(s.Schema.Type) == 0 {
return ""
}
return s.Schema.Type[0]
}
func (s *Schema) Format() string {
return s.Schema.Format
}
func (s *Schema) Items() common.Schema {
if s.Schema.Items == nil || s.Schema.Items.Schema == nil {
return nil
}
return &Schema{Schema: s.Schema.Items.Schema}
}
func (s *Schema) Properties() map[string]common.Schema {
if s.Schema.Properties == nil {
return nil
}
res := make(map[string]common.Schema, len(s.Schema.Properties))
for n, prop := range s.Schema.Properties {
// map value is unaddressable, create a shallow copy
// this is a shallow non-recursive copy
s := prop
res[n] = &Schema{Schema: &s}
}
return res
}
func (s *Schema) AdditionalProperties() common.SchemaOrBool {
if s.Schema.AdditionalProperties == nil {
return nil
}
return &SchemaOrBool{SchemaOrBool: s.Schema.AdditionalProperties}
}
func (s *Schema) Default() any {
return s.Schema.Default
}
func (s *Schema) MaxItems() *int64 {
return s.Schema.MaxItems
}
func (s *Schema) MaxLength() *int64 {
return s.Schema.MaxLength
}
func (s *Schema) MaxProperties() *int64 {
return s.Schema.MaxProperties
}
func (s *Schema) Required() []string {
return s.Schema.Required
}
func (s *Schema) Enum() []any {
return s.Schema.Enum
}
func (s *Schema) Nullable() bool {
return s.Schema.Nullable
}
func (s *Schema) IsXIntOrString() bool {
return isXIntOrString(s.Schema)
}
func (s *Schema) IsXEmbeddedResource() bool {
return isXEmbeddedResource(s.Schema)
}
func (s *Schema) IsXPreserveUnknownFields() bool {
return isXPreserveUnknownFields(s.Schema)
}
func (s *Schema) XListType() string {
return getXListType(s.Schema)
}
func (s *Schema) XListMapKeys() []string {
return getXListMapKeys(s.Schema)
}
func (s *Schema) WithTypeAndObjectMeta() common.Schema {
return &Schema{common.WithTypeAndObjectMeta(s.Schema)}
}
func UnstructuredToVal(unstructured any, schema *spec.Schema) ref.Val {
return common.UnstructuredToVal(unstructured, &Schema{schema})
}
func SchemaDeclType(s *spec.Schema, isResourceRoot bool) *apiservercel.DeclType {
return common.SchemaDeclType(&Schema{Schema: s}, isResourceRoot)
}
func MakeMapList(sts *spec.Schema, items []interface{}) (rv common.MapList) {
return common.MakeMapList(&Schema{Schema: sts}, items)
}

62
vendor/k8s.io/apiserver/pkg/cel/openapi/extensions.go generated vendored Normal file
View File

@ -0,0 +1,62 @@
/*
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 openapi
import (
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/kube-openapi/pkg/validation/spec"
)
var intOrStringFormat = intstr.IntOrString{}.OpenAPISchemaFormat()
func isExtension(schema *spec.Schema, key string) bool {
v, ok := schema.Extensions.GetBool(key)
return v && ok
}
func isXIntOrString(schema *spec.Schema) bool {
// built-in types have the Format while CRDs use extension
// both are valid, checking both
return schema.Format == intOrStringFormat || isExtension(schema, extIntOrString)
}
func isXEmbeddedResource(schema *spec.Schema) bool {
return isExtension(schema, extEmbeddedResource)
}
func isXPreserveUnknownFields(schema *spec.Schema) bool {
return isExtension(schema, extPreserveUnknownFields)
}
func getXListType(schema *spec.Schema) string {
s, _ := schema.Extensions.GetString(extListType)
return s
}
func getXListMapKeys(schema *spec.Schema) []string {
mapKeys, ok := schema.Extensions.GetStringSlice(extListMapKeys)
if !ok {
return nil
}
return mapKeys
}
const extIntOrString = "x-kubernetes-int-or-string"
const extEmbeddedResource = "x-kubernetes-embedded-resource"
const extPreserveUnknownFields = "x-kubernetes-preserve-unknown-fields"
const extListType = "x-kubernetes-list-type"
const extListMapKeys = "x-kubernetes-list-map-keys"

View File

@ -0,0 +1,115 @@
/*
Copyright 2023 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 resolver
import (
"fmt"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/endpoints/openapi"
"k8s.io/kube-openapi/pkg/common"
"k8s.io/kube-openapi/pkg/validation/spec"
)
// DefinitionsSchemaResolver resolves the schema of a built-in type
// by looking up the OpenAPI definitions.
type DefinitionsSchemaResolver struct {
defs map[string]common.OpenAPIDefinition
gvkToSchema map[schema.GroupVersionKind]*spec.Schema
}
// NewDefinitionsSchemaResolver creates a new DefinitionsSchemaResolver.
// An example working setup:
// scheme = "k8s.io/client-go/kubernetes/scheme".Scheme
// getDefinitions = "k8s.io/kubernetes/pkg/generated/openapi".GetOpenAPIDefinitions
func NewDefinitionsSchemaResolver(scheme *runtime.Scheme, getDefinitions common.GetOpenAPIDefinitions) *DefinitionsSchemaResolver {
gvkToSchema := make(map[schema.GroupVersionKind]*spec.Schema)
namer := openapi.NewDefinitionNamer(scheme)
defs := getDefinitions(func(path string) spec.Ref {
return spec.MustCreateRef(path)
})
for name, def := range defs {
_, e := namer.GetDefinitionName(name)
gvks := extensionsToGVKs(e)
s := def.Schema // map value not addressable, make copy
for _, gvk := range gvks {
gvkToSchema[gvk] = &s
}
}
return &DefinitionsSchemaResolver{
gvkToSchema: gvkToSchema,
defs: defs,
}
}
func (d *DefinitionsSchemaResolver) ResolveSchema(gvk schema.GroupVersionKind) (*spec.Schema, error) {
s, ok := d.gvkToSchema[gvk]
if !ok {
return nil, fmt.Errorf("cannot resolve %v: %w", gvk, ErrSchemaNotFound)
}
s, err := populateRefs(func(ref string) (*spec.Schema, bool) {
// find the schema by the ref string, and return a deep copy
def, ok := d.defs[ref]
if !ok {
return nil, false
}
s := def.Schema
return &s, true
}, s)
if err != nil {
return nil, err
}
return s, nil
}
func extensionsToGVKs(extensions spec.Extensions) []schema.GroupVersionKind {
gvksAny, ok := extensions[extGVK]
if !ok {
return nil
}
gvks, ok := gvksAny.([]any)
if !ok {
return nil
}
result := make([]schema.GroupVersionKind, 0, len(gvks))
for _, gvkAny := range gvks {
// type check the map and all fields
gvkMap, ok := gvkAny.(map[string]any)
if !ok {
return nil
}
g, ok := gvkMap["group"].(string)
if !ok {
return nil
}
v, ok := gvkMap["version"].(string)
if !ok {
return nil
}
k, ok := gvkMap["kind"].(string)
if !ok {
return nil
}
result = append(result, schema.GroupVersionKind{
Group: g,
Version: v,
Kind: k,
})
}
return result
}

View File

@ -0,0 +1,104 @@
/*
Copyright 2023 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 resolver
import (
"encoding/json"
"fmt"
"strings"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/discovery"
"k8s.io/kube-openapi/pkg/validation/spec"
)
// ClientDiscoveryResolver uses client-go discovery to resolve schemas at run time.
type ClientDiscoveryResolver struct {
Discovery discovery.DiscoveryInterface
}
var _ SchemaResolver = (*ClientDiscoveryResolver)(nil)
func (r *ClientDiscoveryResolver) ResolveSchema(gvk schema.GroupVersionKind) (*spec.Schema, error) {
p, err := r.Discovery.OpenAPIV3().Paths()
if err != nil {
return nil, err
}
resourcePath := resourcePathFromGV(gvk.GroupVersion())
c, ok := p[resourcePath]
if !ok {
return nil, fmt.Errorf("cannot resolve group version %q: %w", gvk.GroupVersion(), ErrSchemaNotFound)
}
b, err := c.Schema(runtime.ContentTypeJSON)
if err != nil {
return nil, err
}
resp := new(schemaResponse)
err = json.Unmarshal(b, resp)
if err != nil {
return nil, err
}
s, err := resolveType(resp, gvk)
if err != nil {
return nil, err
}
s, err = populateRefs(func(ref string) (*spec.Schema, bool) {
s, ok := resp.Components.Schemas[strings.TrimPrefix(ref, refPrefix)]
return s, ok
}, s)
if err != nil {
return nil, err
}
return s, nil
}
func resolveType(resp *schemaResponse, gvk schema.GroupVersionKind) (*spec.Schema, error) {
for _, s := range resp.Components.Schemas {
var gvks []schema.GroupVersionKind
err := s.Extensions.GetObject(extGVK, &gvks)
if err != nil {
return nil, err
}
for _, g := range gvks {
if g == gvk {
return s, nil
}
}
}
return nil, fmt.Errorf("cannot resolve group version kind %q: %w", gvk, ErrSchemaNotFound)
}
func resourcePathFromGV(gv schema.GroupVersion) string {
var resourcePath string
if len(gv.Group) == 0 {
resourcePath = fmt.Sprintf("api/%s", gv.Version)
} else {
resourcePath = fmt.Sprintf("apis/%s/%s", gv.Group, gv.Version)
}
return resourcePath
}
type schemaResponse struct {
Components struct {
Schemas map[string]*spec.Schema `json:"schemas"`
} `json:"components"`
}
const refPrefix = "#/components/schemas/"
const extGVK = "x-kubernetes-group-version-kind"

View File

@ -0,0 +1,100 @@
/*
Copyright 2023 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 resolver
import (
"fmt"
"k8s.io/kube-openapi/pkg/validation/spec"
)
// populateRefs recursively replaces Refs in the schema with the referred one.
// schemaOf is the callback to find the corresponding schema by the ref.
// This function will not mutate the original schema. If the schema needs to be
// mutated, a copy will be returned, otherwise it returns the original schema.
func populateRefs(schemaOf func(ref string) (*spec.Schema, bool), schema *spec.Schema) (*spec.Schema, error) {
result := *schema
changed := false
ref, isRef := refOf(schema)
if isRef {
// replace the whole schema with the referred one.
resolved, ok := schemaOf(ref)
if !ok {
return nil, fmt.Errorf("internal error: cannot resolve Ref %q: %w", ref, ErrSchemaNotFound)
}
result = *resolved
changed = true
}
// schema is an object, populate its properties and additionalProperties
props := make(map[string]spec.Schema, len(schema.Properties))
propsChanged := false
for name, prop := range result.Properties {
populated, err := populateRefs(schemaOf, &prop)
if err != nil {
return nil, err
}
if populated != &prop {
propsChanged = true
}
props[name] = *populated
}
if propsChanged {
changed = true
result.Properties = props
}
if result.AdditionalProperties != nil && result.AdditionalProperties.Schema != nil {
populated, err := populateRefs(schemaOf, result.AdditionalProperties.Schema)
if err != nil {
return nil, err
}
if populated != result.AdditionalProperties.Schema {
changed = true
result.AdditionalProperties.Schema = populated
}
}
// schema is a list, populate its items
if result.Items != nil && result.Items.Schema != nil {
populated, err := populateRefs(schemaOf, result.Items.Schema)
if err != nil {
return nil, err
}
if populated != result.Items.Schema {
changed = true
result.Items.Schema = populated
}
}
if changed {
return &result, nil
}
return schema, nil
}
func refOf(schema *spec.Schema) (string, bool) {
if schema.Ref.GetURL() != nil {
return schema.Ref.String(), true
}
// A Ref may be wrapped in allOf to preserve its description
// see https://github.com/kubernetes/kubernetes/issues/106387
// For kube-openapi, allOf is only used for wrapping a Ref.
for _, allOf := range schema.AllOf {
if ref, isRef := refOf(&allOf); isRef {
return ref, isRef
}
}
return "", false
}

View File

@ -0,0 +1,39 @@
/*
Copyright 2023 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 resolver
import (
"fmt"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/kube-openapi/pkg/validation/spec"
)
// SchemaResolver finds the OpenAPI schema for the given GroupVersionKind.
// This interface uses the type defined by k8s.io/kube-openapi
type SchemaResolver interface {
// ResolveSchema takes a GroupVersionKind (GVK) and returns the OpenAPI schema
// identified by the GVK.
// The function returns a non-nil error if the schema cannot be found or fail
// to resolve. The returned error wraps ErrSchemaNotFound if the resolution is
// attempted but the corresponding schema cannot be found.
ResolveSchema(gvk schema.GroupVersionKind) (*spec.Schema, error)
}
// ErrSchemaNotFound is wrapped and returned if the schema cannot be located
// by the resolver.
var ErrSchemaNotFound = fmt.Errorf("schema not found")

View File

@ -360,6 +360,23 @@ func (rt *RuleTypes) EnvOptions(tp ref.TypeProvider) ([]cel.EnvOption, error) {
if rt == nil {
return []cel.EnvOption{}, nil
}
rtWithTypes, err := rt.WithTypeProvider(tp)
if err != nil {
return nil, err
}
return []cel.EnvOption{
cel.CustomTypeProvider(rtWithTypes),
cel.CustomTypeAdapter(rtWithTypes),
cel.Variable("rule", rt.ruleSchemaDeclTypes.root.CelType()),
}, nil
}
// WithTypeProvider returns a new RuleTypes that sets the given TypeProvider
// If the original RuleTypes is nil, the returned RuleTypes is still nil.
func (rt *RuleTypes) WithTypeProvider(tp ref.TypeProvider) (*RuleTypes, error) {
if rt == nil {
return nil, nil
}
var ta ref.TypeAdapter = types.DefaultTypeAdapter
tpa, ok := tp.(ref.TypeAdapter)
if ok {
@ -382,11 +399,7 @@ func (rt *RuleTypes) EnvOptions(tp ref.TypeProvider) ([]cel.EnvOption, error) {
"type %s definition differs between CEL environment and rule", name)
}
}
return []cel.EnvOption{
cel.CustomTypeProvider(rtWithTypes),
cel.CustomTypeAdapter(rtWithTypes),
cel.Variable("rule", rt.ruleSchemaDeclTypes.root.CelType()),
}, nil
return rtWithTypes, nil
}
// FindType attempts to resolve the typeName provided from the rule's rule-schema, or if not

2
vendor/k8s.io/apiserver/pkg/endpoints/OWNERS generated vendored Normal file
View File

@ -0,0 +1,2 @@
approvers:
- apelisse

View File

@ -0,0 +1,5 @@
# See the OWNERS docs at https://go.k8s.io/owners
reviewers:
- alexzielenski
- jefftree

View File

@ -35,8 +35,7 @@ import (
// - Replies with 304 Not Modified, if If-None-Match header matches hash
//
// hash should be the value of calculateETag on object. If hash is empty, then
//
// the object is simply serialized without E-Tag functionality
// the object is simply serialized without E-Tag functionality
func ServeHTTPWithETag(
object runtime.Object,
hash string,
@ -55,7 +54,7 @@ func ServeHTTPWithETag(
// Otherwise, we delegate to the handler for actual content
//
// According to documentation, An Etag within an If-None-Match
// header will be enclosed within doule quotes:
// header will be enclosed within double quotes:
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match#directives
if clientCachedHash := req.Header.Get("If-None-Match"); quotedHash == clientCachedHash {
w.WriteHeader(http.StatusNotModified)

View File

@ -169,3 +169,7 @@ func (f *recorderResourceManager) WebService() *restful.WebService {
func (f *recorderResourceManager) ServeHTTP(http.ResponseWriter, *http.Request) {
panic("unimplemented")
}
func (f *recorderResourceManager) WithSource(source Source) ResourceManager {
panic("unimplemented")
}

View File

@ -26,6 +26,7 @@ import (
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/version"
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
"k8s.io/apiserver/pkg/endpoints/metrics"
"sync/atomic"
@ -35,6 +36,15 @@ import (
"k8s.io/klog/v2"
)
type Source uint
// The GroupVersion from the lowest Source takes precedence
const (
AggregatorSource Source = 0
BuiltinSource Source = 100
CRDSource Source = 200
)
// This handler serves the /apis endpoint for an aggregated list of
// api resources indexed by their group version.
type ResourceManager interface {
@ -64,19 +74,67 @@ type ResourceManager interface {
// Thread-Safe
SetGroups([]apidiscoveryv2beta1.APIGroupDiscovery)
// Returns the same resource manager using a different source
// The source is used to decide how to de-duplicate groups.
// The group from the least-numbered source is used
WithSource(source Source) ResourceManager
http.Handler
}
type resourceManager struct {
source Source
*resourceDiscoveryManager
}
func (rm resourceManager) AddGroupVersion(groupName string, value apidiscoveryv2beta1.APIVersionDiscovery) {
rm.resourceDiscoveryManager.AddGroupVersion(rm.source, groupName, value)
}
func (rm resourceManager) SetGroupVersionPriority(gv metav1.GroupVersion, grouppriority, versionpriority int) {
rm.resourceDiscoveryManager.SetGroupVersionPriority(rm.source, gv, grouppriority, versionpriority)
}
func (rm resourceManager) RemoveGroup(groupName string) {
rm.resourceDiscoveryManager.RemoveGroup(rm.source, groupName)
}
func (rm resourceManager) RemoveGroupVersion(gv metav1.GroupVersion) {
rm.resourceDiscoveryManager.RemoveGroupVersion(rm.source, gv)
}
func (rm resourceManager) SetGroups(groups []apidiscoveryv2beta1.APIGroupDiscovery) {
rm.resourceDiscoveryManager.SetGroups(rm.source, groups)
}
func (rm resourceManager) WithSource(source Source) ResourceManager {
return resourceManager{
source: source,
resourceDiscoveryManager: rm.resourceDiscoveryManager,
}
}
type groupKey struct {
name string
// Source identifies where this group came from and dictates which group
// among duplicates is chosen to be used for discovery.
source Source
}
type groupVersionKey struct {
metav1.GroupVersion
source Source
}
type resourceDiscoveryManager struct {
serializer runtime.NegotiatedSerializer
// cache is an atomic pointer to avoid the use of locks
cache atomic.Pointer[cachedGroupList]
serveHTTPFunc http.HandlerFunc
// Writes protected by the lock.
// List of all apigroups & resources indexed by the resource manager
lock sync.RWMutex
apiGroups map[string]*apidiscoveryv2beta1.APIGroupDiscovery
versionPriorities map[metav1.GroupVersion]priorityInfo
apiGroups map[groupKey]*apidiscoveryv2beta1.APIGroupDiscovery
versionPriorities map[groupVersionKey]priorityInfo
}
type priorityInfo struct {
@ -84,25 +142,46 @@ type priorityInfo struct {
VersionPriority int
}
func NewResourceManager() ResourceManager {
func NewResourceManager(path string) ResourceManager {
scheme := runtime.NewScheme()
codecs := serializer.NewCodecFactory(scheme)
utilruntime.Must(apidiscoveryv2beta1.AddToScheme(scheme))
return &resourceDiscoveryManager{serializer: codecs, versionPriorities: make(map[metav1.GroupVersion]priorityInfo)}
rdm := &resourceDiscoveryManager{
serializer: codecs,
versionPriorities: make(map[groupVersionKey]priorityInfo),
}
rdm.serveHTTPFunc = metrics.InstrumentHandlerFunc("GET",
/* group = */ "",
/* version = */ "",
/* resource = */ "",
/* subresource = */ path,
/* scope = */ "",
/* component = */ metrics.APIServerComponent,
/* deprecated */ false,
/* removedRelease */ "",
rdm.serveHTTP)
return resourceManager{
source: BuiltinSource,
resourceDiscoveryManager: rdm,
}
}
func (rdm *resourceDiscoveryManager) SetGroupVersionPriority(gv metav1.GroupVersion, groupPriorityMinimum, versionPriority int) {
func (rdm *resourceDiscoveryManager) SetGroupVersionPriority(source Source, gv metav1.GroupVersion, groupPriorityMinimum, versionPriority int) {
rdm.lock.Lock()
defer rdm.lock.Unlock()
rdm.versionPriorities[gv] = priorityInfo{
key := groupVersionKey{
GroupVersion: gv,
source: source,
}
rdm.versionPriorities[key] = priorityInfo{
GroupPriorityMinimum: groupPriorityMinimum,
VersionPriority: versionPriority,
}
rdm.cache.Store(nil)
}
func (rdm *resourceDiscoveryManager) SetGroups(groups []apidiscoveryv2beta1.APIGroupDiscovery) {
func (rdm *resourceDiscoveryManager) SetGroups(source Source, groups []apidiscoveryv2beta1.APIGroupDiscovery) {
rdm.lock.Lock()
defer rdm.lock.Unlock()
@ -111,13 +190,17 @@ func (rdm *resourceDiscoveryManager) SetGroups(groups []apidiscoveryv2beta1.APIG
for _, group := range groups {
for _, version := range group.Versions {
rdm.addGroupVersionLocked(group.Name, version)
rdm.addGroupVersionLocked(source, group.Name, version)
}
}
// Filter unused out priority entries
for gv := range rdm.versionPriorities {
entry, exists := rdm.apiGroups[gv.Group]
key := groupKey{
source: source,
name: gv.Group,
}
entry, exists := rdm.apiGroups[key]
if !exists {
delete(rdm.versionPriorities, gv)
continue
@ -138,21 +221,26 @@ func (rdm *resourceDiscoveryManager) SetGroups(groups []apidiscoveryv2beta1.APIG
}
}
func (rdm *resourceDiscoveryManager) AddGroupVersion(groupName string, value apidiscoveryv2beta1.APIVersionDiscovery) {
func (rdm *resourceDiscoveryManager) AddGroupVersion(source Source, groupName string, value apidiscoveryv2beta1.APIVersionDiscovery) {
rdm.lock.Lock()
defer rdm.lock.Unlock()
rdm.addGroupVersionLocked(groupName, value)
rdm.addGroupVersionLocked(source, groupName, value)
}
func (rdm *resourceDiscoveryManager) addGroupVersionLocked(groupName string, value apidiscoveryv2beta1.APIVersionDiscovery) {
func (rdm *resourceDiscoveryManager) addGroupVersionLocked(source Source, groupName string, value apidiscoveryv2beta1.APIVersionDiscovery) {
klog.Infof("Adding GroupVersion %s %s to ResourceManager", groupName, value.Version)
if rdm.apiGroups == nil {
rdm.apiGroups = make(map[string]*apidiscoveryv2beta1.APIGroupDiscovery)
rdm.apiGroups = make(map[groupKey]*apidiscoveryv2beta1.APIGroupDiscovery)
}
if existing, groupExists := rdm.apiGroups[groupName]; groupExists {
key := groupKey{
source: source,
name: groupName,
}
if existing, groupExists := rdm.apiGroups[key]; groupExists {
// If this version already exists, replace it
versionExists := false
@ -165,6 +253,7 @@ func (rdm *resourceDiscoveryManager) addGroupVersionLocked(groupName string, val
if reflect.DeepEqual(existing.Versions[i], value) {
return
}
existing.Versions[i] = value
versionExists = true
break
@ -182,12 +271,16 @@ func (rdm *resourceDiscoveryManager) addGroupVersionLocked(groupName string, val
},
Versions: []apidiscoveryv2beta1.APIVersionDiscovery{value},
}
rdm.apiGroups[groupName] = group
rdm.apiGroups[key] = group
}
gv := metav1.GroupVersion{Group: groupName, Version: value.Version}
if _, ok := rdm.versionPriorities[gv]; !ok {
rdm.versionPriorities[gv] = priorityInfo{
gvKey := groupVersionKey{
GroupVersion: gv,
source: source,
}
if _, ok := rdm.versionPriorities[gvKey]; !ok {
rdm.versionPriorities[gvKey] = priorityInfo{
GroupPriorityMinimum: 1000,
VersionPriority: 15,
}
@ -197,10 +290,16 @@ func (rdm *resourceDiscoveryManager) addGroupVersionLocked(groupName string, val
rdm.cache.Store(nil)
}
func (rdm *resourceDiscoveryManager) RemoveGroupVersion(apiGroup metav1.GroupVersion) {
func (rdm *resourceDiscoveryManager) RemoveGroupVersion(source Source, apiGroup metav1.GroupVersion) {
rdm.lock.Lock()
defer rdm.lock.Unlock()
group, exists := rdm.apiGroups[apiGroup.Group]
key := groupKey{
source: source,
name: apiGroup.Group,
}
group, exists := rdm.apiGroups[key]
if !exists {
return
}
@ -218,23 +317,33 @@ func (rdm *resourceDiscoveryManager) RemoveGroupVersion(apiGroup metav1.GroupVer
return
}
delete(rdm.versionPriorities, apiGroup)
gvKey := groupVersionKey{
GroupVersion: apiGroup,
source: source,
}
delete(rdm.versionPriorities, gvKey)
if len(group.Versions) == 0 {
delete(rdm.apiGroups, group.Name)
delete(rdm.apiGroups, key)
}
// Reset response document so it is recreated lazily
rdm.cache.Store(nil)
}
func (rdm *resourceDiscoveryManager) RemoveGroup(groupName string) {
func (rdm *resourceDiscoveryManager) RemoveGroup(source Source, groupName string) {
rdm.lock.Lock()
defer rdm.lock.Unlock()
delete(rdm.apiGroups, groupName)
key := groupKey{
source: source,
name: groupName,
}
delete(rdm.apiGroups, key)
for k := range rdm.versionPriorities {
if k.Group == groupName {
if k.Group == groupName && k.source == source {
delete(rdm.versionPriorities, k)
}
}
@ -246,19 +355,66 @@ func (rdm *resourceDiscoveryManager) RemoveGroup(groupName string) {
// Prepares the api group list for serving by converting them from map into
// list and sorting them according to insertion order
func (rdm *resourceDiscoveryManager) calculateAPIGroupsLocked() []apidiscoveryv2beta1.APIGroupDiscovery {
regenerationCounter.Inc()
// Re-order the apiGroups by their priority.
groups := []apidiscoveryv2beta1.APIGroupDiscovery{}
for _, group := range rdm.apiGroups {
copied := *group.DeepCopy()
groupsToUse := map[string]apidiscoveryv2beta1.APIGroupDiscovery{}
sourcesUsed := map[metav1.GroupVersion]Source{}
for key, group := range rdm.apiGroups {
if existing, ok := groupsToUse[key.name]; ok {
for _, v := range group.Versions {
gv := metav1.GroupVersion{Group: key.name, Version: v.Version}
// Skip groupversions we've already seen before. Only DefaultSource
// takes precedence
if usedSource, seen := sourcesUsed[gv]; seen && key.source >= usedSource {
continue
} else if seen {
// Find the index of the duplicate version and replace
for i := 0; i < len(existing.Versions); i++ {
if existing.Versions[i].Version == v.Version {
existing.Versions[i] = v
break
}
}
} else {
// New group-version, just append
existing.Versions = append(existing.Versions, v)
}
sourcesUsed[gv] = key.source
groupsToUse[key.name] = existing
}
// Check to see if we have overlapping versions. If we do, take the one
// with highest source precedence
} else {
groupsToUse[key.name] = *group.DeepCopy()
for _, v := range group.Versions {
gv := metav1.GroupVersion{Group: key.name, Version: v.Version}
sourcesUsed[gv] = key.source
}
}
}
for _, group := range groupsToUse {
// Re-order versions based on their priority. Use kube-aware string
// comparison as a tie breaker
sort.SliceStable(copied.Versions, func(i, j int) bool {
iVersion := copied.Versions[i].Version
jVersion := copied.Versions[j].Version
sort.SliceStable(group.Versions, func(i, j int) bool {
iVersion := group.Versions[i].Version
jVersion := group.Versions[j].Version
iPriority := rdm.versionPriorities[metav1.GroupVersion{Group: group.Name, Version: iVersion}].VersionPriority
jPriority := rdm.versionPriorities[metav1.GroupVersion{Group: group.Name, Version: jVersion}].VersionPriority
iGV := metav1.GroupVersion{Group: group.Name, Version: iVersion}
jGV := metav1.GroupVersion{Group: group.Name, Version: jVersion}
iSource := sourcesUsed[iGV]
jSource := sourcesUsed[jGV]
iPriority := rdm.versionPriorities[groupVersionKey{iGV, iSource}].VersionPriority
jPriority := rdm.versionPriorities[groupVersionKey{jGV, jSource}].VersionPriority
// Sort by version string comparator if priority is equal
if iPriority == jPriority {
@ -269,13 +425,16 @@ func (rdm *resourceDiscoveryManager) calculateAPIGroupsLocked() []apidiscoveryv2
return iPriority > jPriority
})
groups = append(groups, *copied.DeepCopy())
groups = append(groups, group)
}
// For each group, determine the highest minimum group priority and use that
priorities := map[string]int{}
for gv, info := range rdm.versionPriorities {
if source := sourcesUsed[gv.GroupVersion]; source != gv.source {
continue
}
if existing, exists := priorities[gv.Group]; exists {
if existing < info.GroupPriorityMinimum {
priorities[gv.Group] = info.GroupPriorityMinimum
@ -338,6 +497,10 @@ type cachedGroupList struct {
}
func (rdm *resourceDiscoveryManager) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
rdm.serveHTTPFunc(resp, req)
}
func (rdm *resourceDiscoveryManager) serveHTTP(resp http.ResponseWriter, req *http.Request) {
cache := rdm.fetchFromCache()
response := cache.cachedResponse
etag := cache.cachedResponseETag

View File

@ -0,0 +1,36 @@
/*
Copyright 2023 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 aggregated
import (
"k8s.io/component-base/metrics"
"k8s.io/component-base/metrics/legacyregistry"
)
var (
regenerationCounter = metrics.NewCounter(
&metrics.CounterOpts{
Name: "aggregator_discovery_aggregation_count_total",
Help: "Counter of number of times discovery was aggregated",
StabilityLevel: metrics.ALPHA,
},
)
)
func init() {
legacyregistry.MustRegister(regenerationCounter)
}

View File

@ -27,9 +27,6 @@ import (
// this function.
func StorageVersionHash(group, version, kind string) string {
gvk := group + "/" + version + "/" + kind
if gvk == "" {
return ""
}
bytes := sha256.Sum256([]byte(gvk))
// Assuming there are N kinds in the cluster, and the hash is X-byte long,
// the chance of colliding hash P(N,X) approximates to 1-e^(-(N^2)/2^(8X+1)).

View File

@ -133,10 +133,10 @@ func evaluatePolicyAndCreateAuditEvent(req *http.Request, policy audit.PolicyRul
return ac, fmt.Errorf("failed to GetAuthorizerAttributes: %v", err)
}
ls := policy.EvaluatePolicyRule(attribs)
audit.ObservePolicyLevel(ctx, ls.Level)
ac.RequestAuditConfig = ls.RequestAuditConfig
if ls.Level == auditinternal.LevelNone {
rac := policy.EvaluatePolicyRule(attribs)
audit.ObservePolicyLevel(ctx, rac.Level)
ac.RequestAuditConfig = rac
if rac.Level == auditinternal.LevelNone {
// Don't audit.
return ac, nil
}
@ -145,7 +145,7 @@ func evaluatePolicyAndCreateAuditEvent(req *http.Request, policy audit.PolicyRul
if !ok {
requestReceivedTimestamp = time.Now()
}
ev, err := audit.NewEventFromRequest(req, requestReceivedTimestamp, ls.Level, attribs)
ev, err := audit.NewEventFromRequest(req, requestReceivedTimestamp, rac.Level, attribs)
if err != nil {
return nil, fmt.Errorf("failed to complete audit event from request: %v", err)
}

View File

@ -27,6 +27,8 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/authenticatorfactory"
"k8s.io/apiserver/pkg/authentication/request/headerrequest"
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/klog/v2"
@ -38,15 +40,20 @@ type recordMetrics func(context.Context, *authenticator.Response, bool, error, a
// stores any such user found onto the provided context for the request. If authentication fails or returns an error
// the failed handler is used. On success, "Authorization" header is removed from the request and handler
// is invoked to serve the request.
func WithAuthentication(handler http.Handler, auth authenticator.Request, failed http.Handler, apiAuds authenticator.Audiences) http.Handler {
return withAuthentication(handler, auth, failed, apiAuds, recordAuthMetrics)
func WithAuthentication(handler http.Handler, auth authenticator.Request, failed http.Handler, apiAuds authenticator.Audiences, requestHeaderConfig *authenticatorfactory.RequestHeaderConfig) http.Handler {
return withAuthentication(handler, auth, failed, apiAuds, requestHeaderConfig, recordAuthMetrics)
}
func withAuthentication(handler http.Handler, auth authenticator.Request, failed http.Handler, apiAuds authenticator.Audiences, metrics recordMetrics) http.Handler {
func withAuthentication(handler http.Handler, auth authenticator.Request, failed http.Handler, apiAuds authenticator.Audiences, requestHeaderConfig *authenticatorfactory.RequestHeaderConfig, metrics recordMetrics) http.Handler {
if auth == nil {
klog.Warning("Authentication is disabled")
return handler
}
standardRequestHeaderConfig := &authenticatorfactory.RequestHeaderConfig{
UsernameHeaders: headerrequest.StaticStringSlice{"X-Remote-User"},
GroupHeaders: headerrequest.StaticStringSlice{"X-Remote-Group"},
ExtraHeaderPrefixes: headerrequest.StaticStringSlice{"X-Remote-Extra-"},
}
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
authenticationStart := time.Now()
@ -76,6 +83,24 @@ func withAuthentication(handler http.Handler, auth authenticator.Request, failed
// authorization header is not required anymore in case of a successful authentication.
req.Header.Del("Authorization")
// delete standard front proxy headers
headerrequest.ClearAuthenticationHeaders(
req.Header,
standardRequestHeaderConfig.UsernameHeaders,
standardRequestHeaderConfig.GroupHeaders,
standardRequestHeaderConfig.ExtraHeaderPrefixes,
)
// also delete any custom front proxy headers
if requestHeaderConfig != nil {
headerrequest.ClearAuthenticationHeaders(
req.Header,
requestHeaderConfig.UsernameHeaders,
requestHeaderConfig.GroupHeaders,
requestHeaderConfig.ExtraHeaderPrefixes,
)
}
req = req.WithContext(genericapirequest.WithUser(req.Context(), resp.User))
handler.ServeHTTP(w, req)
})

View File

@ -70,7 +70,7 @@ func WithAuthorization(handler http.Handler, a authorizer.Authorizer, s runtime.
return
}
klog.V(4).InfoS("Forbidden", "URI", req.RequestURI, "Reason", reason)
klog.V(4).InfoS("Forbidden", "URI", req.RequestURI, "reason", reason)
audit.AddAuditAnnotations(ctx,
decisionAnnotationKey, decisionForbid,
reasonAnnotationKey, reason)

View File

@ -109,14 +109,14 @@ func WithImpersonation(handler http.Handler, a authorizer.Authorizer, s runtime.
actingAsAttributes.Resource = "uids"
default:
klog.V(4).InfoS("unknown impersonation request type", "Request", impersonationRequest)
klog.V(4).InfoS("unknown impersonation request type", "request", impersonationRequest)
responsewriters.Forbidden(ctx, actingAsAttributes, w, req, fmt.Sprintf("unknown impersonation request type: %v", impersonationRequest), s)
return
}
decision, reason, err := a.Authorize(ctx, actingAsAttributes)
if err != nil || decision != authorizer.DecisionAllow {
klog.V(4).InfoS("Forbidden", "URI", req.RequestURI, "Reason", reason, "Error", err)
klog.V(4).InfoS("Forbidden", "URI", req.RequestURI, "reason", reason, "err", err)
responsewriters.Forbidden(ctx, actingAsAttributes, w, req, reason, s)
return
}

View File

@ -100,13 +100,13 @@ func WithStorageVersionPrecondition(handler http.Handler, svm storageversion.Man
}
// If the resource's StorageVersion is not in the to-be-updated list, let it pass.
// Non-persisted resources are not in the to-be-updated list, so they will pass.
gr := schema.GroupResource{requestInfo.APIGroup, requestInfo.Resource}
gr := schema.GroupResource{Group: requestInfo.APIGroup, Resource: requestInfo.Resource}
if !svm.PendingUpdate(gr) {
handler.ServeHTTP(w, req)
return
}
gv := schema.GroupVersion{requestInfo.APIGroup, requestInfo.APIVersion}
gv := schema.GroupVersion{Group: requestInfo.APIGroup, Version: requestInfo.APIVersion}
responsewriters.ErrorNegotiated(apierrors.NewServiceUnavailable(fmt.Sprintf("wait for storage version registration to complete for resource: %v, last seen error: %v", gr, svm.LastUpdateError(gr))), s, gv, w, req)
})
}

View File

@ -27,14 +27,13 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"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"
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager"
"k8s.io/apiserver/pkg/registry/rest"
"k8s.io/apiserver/pkg/storageversion"
openapiproto "k8s.io/kube-openapi/pkg/util/proto"
)
// ConvertabilityChecker indicates what versions a GroupKind is available in.
@ -82,7 +81,7 @@ type APIGroupVersion struct {
Defaulter runtime.ObjectDefaulter
Namer runtime.Namer
UnsafeConvertor runtime.ObjectConvertor
TypeConverter fieldmanager.TypeConverter
TypeConverter managedfields.TypeConverter
EquivalentResourceRegistry runtime.EquivalentResourceRegistry
@ -95,9 +94,6 @@ type APIGroupVersion struct {
MinRequestTimeout time.Duration
// OpenAPIModels exposes the OpenAPI models to each individual handler.
OpenAPIModels openapiproto.Models
// The limit on the request body size that would be accepted and decoded in a write request.
// 0 means no limit.
MaxRequestBodyBytes int64

View File

@ -162,8 +162,13 @@ func createHandler(r rest.NamedCreater, scope *RequestScope, admit admission.Int
userInfo, _ := request.UserFrom(ctx)
if objectMeta, err := meta.Accessor(obj); err == nil {
// Wipe fields which cannot take user-provided values
rest.WipeObjectMetaSystemFields(objectMeta)
preserveObjectMetaSystemFields := false
if c, ok := r.(rest.SubresourceObjectMetaPreserver); ok && len(scope.Subresource) > 0 {
preserveObjectMetaSystemFields = c.PreserveRequestObjectMetaSystemFieldsOnSubresourceCreate()
}
if !preserveObjectMetaSystemFields {
rest.WipeObjectMetaSystemFields(objectMeta)
}
// ensure namespace on the object is correct, or error if a conflicting namespace was set in the object
if err := rest.EnsureObjectNamespaceMatchesRequestNamespace(rest.ExpectedNamespaceForResource(namespace, scope.Resource), objectMeta); err != nil {

View File

@ -38,8 +38,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"
)
@ -196,7 +198,8 @@ func DeleteCollection(r rest.CollectionDeleter, checkBody bool, scope *RequestSc
return
}
if errs := metainternalversionvalidation.ValidateListOptions(&listOptions); len(errs) > 0 {
metainternalversion.SetListOptionsDefaults(&listOptions, utilfeature.DefaultFeatureGate.Enabled(features.WatchList))
if errs := metainternalversionvalidation.ValidateListOptions(&listOptions, utilfeature.DefaultFeatureGate.Enabled(features.WatchList)); len(errs) > 0 {
err := errors.NewInvalid(schema.GroupKind{Group: metav1.GroupName, Kind: "ListOptions"}, "", errs)
scope.err(err, w, req)
return

View File

@ -21,6 +21,7 @@ import (
"fmt"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/util/managedfields"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/warning"
)
@ -70,7 +71,7 @@ func (admit *managedFieldsValidatingAdmissionController) Admit(ctx context.Conte
return err
}
managedFieldsAfterAdmission := objectMeta.GetManagedFields()
if _, err := DecodeManagedFields(managedFieldsAfterAdmission); err != nil {
if err := managedfields.ValidateManagedFields(managedFieldsAfterAdmission); err != nil {
objectMeta.SetManagedFields(managedFieldsBeforeAdmission)
warning.AddWarning(ctx, "",
fmt.Sprintf(InvalidManagedFieldsAfterMutatingAdmissionWarningFormat,

View File

@ -1,75 +0,0 @@
/*
Copyright 2019 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 fieldmanager
import (
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal"
)
type buildManagerInfoManager struct {
fieldManager Manager
groupVersion schema.GroupVersion
subresource string
}
var _ Manager = &buildManagerInfoManager{}
// NewBuildManagerInfoManager creates a new Manager that converts the manager name into a unique identifier
// combining operation and version for update requests, and just operation for apply requests.
func NewBuildManagerInfoManager(f Manager, gv schema.GroupVersion, subresource string) Manager {
return &buildManagerInfoManager{
fieldManager: f,
groupVersion: gv,
subresource: subresource,
}
}
// Update implements Manager.
func (f *buildManagerInfoManager) Update(liveObj, newObj runtime.Object, managed Managed, manager string) (runtime.Object, Managed, error) {
manager, err := f.buildManagerInfo(manager, metav1.ManagedFieldsOperationUpdate)
if err != nil {
return nil, nil, fmt.Errorf("failed to build manager identifier: %v", err)
}
return f.fieldManager.Update(liveObj, newObj, managed, manager)
}
// Apply implements Manager.
func (f *buildManagerInfoManager) Apply(liveObj, appliedObj runtime.Object, managed Managed, manager string, force bool) (runtime.Object, Managed, error) {
manager, err := f.buildManagerInfo(manager, metav1.ManagedFieldsOperationApply)
if err != nil {
return nil, nil, fmt.Errorf("failed to build manager identifier: %v", err)
}
return f.fieldManager.Apply(liveObj, appliedObj, managed, manager, force)
}
func (f *buildManagerInfoManager) buildManagerInfo(prefix string, operation metav1.ManagedFieldsOperationType) (string, error) {
managerInfo := metav1.ManagedFieldsEntry{
Manager: prefix,
Operation: operation,
APIVersion: f.groupVersion.String(),
Subresource: f.subresource,
}
if managerInfo.Manager == "" {
managerInfo.Manager = "unknown"
}
return internal.BuildManagerIdentifier(&managerInfo)
}

View File

@ -1,134 +0,0 @@
/*
Copyright 2019 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 fieldmanager
import (
"fmt"
"sort"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
)
type capManagersManager struct {
fieldManager Manager
maxUpdateManagers int
oldUpdatesManagerName string
}
var _ Manager = &capManagersManager{}
// NewCapManagersManager creates a new wrapped FieldManager which ensures that the number of managers from updates
// does not exceed maxUpdateManagers, by merging some of the oldest entries on each update.
func NewCapManagersManager(fieldManager Manager, maxUpdateManagers int) Manager {
return &capManagersManager{
fieldManager: fieldManager,
maxUpdateManagers: maxUpdateManagers,
oldUpdatesManagerName: "ancient-changes",
}
}
// Update implements Manager.
func (f *capManagersManager) Update(liveObj, newObj runtime.Object, managed Managed, manager string) (runtime.Object, Managed, error) {
object, managed, err := f.fieldManager.Update(liveObj, newObj, managed, manager)
if err != nil {
return object, managed, err
}
if managed, err = f.capUpdateManagers(managed); err != nil {
return nil, nil, fmt.Errorf("failed to cap update managers: %v", err)
}
return object, managed, nil
}
// Apply implements Manager.
func (f *capManagersManager) Apply(liveObj, appliedObj runtime.Object, managed Managed, fieldManager string, force bool) (runtime.Object, Managed, error) {
return f.fieldManager.Apply(liveObj, appliedObj, managed, fieldManager, force)
}
// capUpdateManagers merges a number of the oldest update entries into versioned buckets,
// such that the number of entries from updates does not exceed f.maxUpdateManagers.
func (f *capManagersManager) capUpdateManagers(managed Managed) (newManaged Managed, err error) {
// Gather all entries from updates
updaters := []string{}
for manager, fields := range managed.Fields() {
if !fields.Applied() {
updaters = append(updaters, manager)
}
}
if len(updaters) <= f.maxUpdateManagers {
return managed, nil
}
// If we have more than the maximum, sort the update entries by time, oldest first.
sort.Slice(updaters, func(i, j int) bool {
iTime, jTime, iSeconds, jSeconds := managed.Times()[updaters[i]], managed.Times()[updaters[j]], int64(0), int64(0)
if iTime != nil {
iSeconds = iTime.Unix()
}
if jTime != nil {
jSeconds = jTime.Unix()
}
if iSeconds != jSeconds {
return iSeconds < jSeconds
}
return updaters[i] < updaters[j]
})
// Merge the oldest updaters with versioned bucket managers until the number of updaters is under the cap
versionToFirstManager := map[string]string{}
for i, length := 0, len(updaters); i < len(updaters) && length > f.maxUpdateManagers; i++ {
manager := updaters[i]
vs := managed.Fields()[manager]
time := managed.Times()[manager]
version := string(vs.APIVersion())
// Create a new manager identifier for the versioned bucket entry.
// The version for this manager comes from the version of the update being merged into the bucket.
bucket, err := internal.BuildManagerIdentifier(&metav1.ManagedFieldsEntry{
Manager: f.oldUpdatesManagerName,
Operation: metav1.ManagedFieldsOperationUpdate,
APIVersion: version,
})
if err != nil {
return managed, fmt.Errorf("failed to create bucket manager for version %v: %v", version, err)
}
// Merge the fieldets if this is not the first time the version was seen.
// Otherwise just record the manager name in versionToFirstManager
if first, ok := versionToFirstManager[version]; ok {
// If the bucket doesn't exists yet, create one.
if _, ok := managed.Fields()[bucket]; !ok {
s := managed.Fields()[first]
delete(managed.Fields(), first)
managed.Fields()[bucket] = s
}
managed.Fields()[bucket] = fieldpath.NewVersionedSet(vs.Set().Union(managed.Fields()[bucket].Set()), vs.APIVersion(), vs.Applied())
delete(managed.Fields(), manager)
length--
// Use the time from the update being merged into the bucket, since it is more recent.
managed.Times()[bucket] = time
} else {
versionToFirstManager[version] = manager
}
}
return managed, nil
}

View File

@ -1,264 +0,0 @@
/*
Copyright 2018 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 fieldmanager
import (
"fmt"
"reflect"
"time"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal"
"k8s.io/klog/v2"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
"sigs.k8s.io/structured-merge-diff/v4/merge"
)
// DefaultMaxUpdateManagers defines the default maximum retained number of managedFields entries from updates
// if the number of update managers exceeds this, the oldest entries will be merged until the number is below the maximum.
// TODO(jennybuckley): Determine if this is really the best value. Ideally we wouldn't unnecessarily merge too many entries.
const DefaultMaxUpdateManagers int = 10
// DefaultTrackOnCreateProbability defines the default probability that the field management of an object
// starts being tracked from the object's creation, instead of from the first time the object is applied to.
const DefaultTrackOnCreateProbability float32 = 1
var atMostEverySecond = internal.NewAtMostEvery(time.Second)
// Managed groups a fieldpath.ManagedFields together with the timestamps associated with each operation.
type Managed interface {
// Fields gets the fieldpath.ManagedFields.
Fields() fieldpath.ManagedFields
// Times gets the timestamps associated with each operation.
Times() map[string]*metav1.Time
}
// Manager updates the managed fields and merges applied configurations.
type Manager interface {
// Update is used when the object has already been merged (non-apply
// use-case), and simply updates the managed fields in the output
// object.
// * `liveObj` is not mutated by this function
// * `newObj` may be mutated by this function
// Returns the new object with managedFields removed, and the object's new
// proposed managedFields separately.
Update(liveObj, newObj runtime.Object, managed Managed, manager string) (runtime.Object, Managed, error)
// Apply is used when server-side apply is called, as it merges the
// object and updates the managed fields.
// * `liveObj` is not mutated by this function
// * `newObj` may be mutated by this function
// Returns the new object with managedFields removed, and the object's new
// proposed managedFields separately.
Apply(liveObj, appliedObj runtime.Object, managed Managed, fieldManager string, force bool) (runtime.Object, Managed, error)
}
// FieldManager updates the managed fields and merge applied
// configurations.
type FieldManager struct {
fieldManager Manager
subresource string
}
// NewFieldManager creates a new FieldManager that decodes, manages, then re-encodes managedFields
// on update and apply requests.
func NewFieldManager(f Manager, subresource string) *FieldManager {
return &FieldManager{fieldManager: f, subresource: subresource}
}
// NewDefaultFieldManager creates a new FieldManager that merges apply requests
// and update managed fields for other types of requests.
func NewDefaultFieldManager(typeConverter TypeConverter, objectConverter runtime.ObjectConvertor, objectDefaulter runtime.ObjectDefaulter, objectCreater runtime.ObjectCreater, kind schema.GroupVersionKind, hub schema.GroupVersion, subresource string, resetFields map[fieldpath.APIVersion]*fieldpath.Set) (*FieldManager, error) {
f, err := NewStructuredMergeManager(typeConverter, objectConverter, objectDefaulter, kind.GroupVersion(), hub, resetFields)
if err != nil {
return nil, fmt.Errorf("failed to create field manager: %v", err)
}
return newDefaultFieldManager(f, typeConverter, objectConverter, objectCreater, kind, subresource), nil
}
// NewDefaultCRDFieldManager creates a new FieldManager specifically for
// CRDs. This allows for the possibility of fields which are not defined
// in models, as well as having no models defined at all.
func NewDefaultCRDFieldManager(typeConverter TypeConverter, objectConverter runtime.ObjectConvertor, objectDefaulter runtime.ObjectDefaulter, objectCreater runtime.ObjectCreater, kind schema.GroupVersionKind, hub schema.GroupVersion, subresource string, resetFields map[fieldpath.APIVersion]*fieldpath.Set) (_ *FieldManager, err error) {
f, err := NewCRDStructuredMergeManager(typeConverter, objectConverter, objectDefaulter, kind.GroupVersion(), hub, resetFields)
if err != nil {
return nil, fmt.Errorf("failed to create field manager: %v", err)
}
return newDefaultFieldManager(f, typeConverter, objectConverter, objectCreater, kind, subresource), nil
}
// newDefaultFieldManager is a helper function which wraps a Manager with certain default logic.
func newDefaultFieldManager(f Manager, typeConverter TypeConverter, objectConverter runtime.ObjectConvertor, objectCreater runtime.ObjectCreater, kind schema.GroupVersionKind, subresource string) *FieldManager {
return NewFieldManager(
NewLastAppliedUpdater(
NewLastAppliedManager(
NewProbabilisticSkipNonAppliedManager(
NewCapManagersManager(
NewBuildManagerInfoManager(
NewManagedFieldsUpdater(
NewStripMetaManager(f),
), kind.GroupVersion(), subresource,
), DefaultMaxUpdateManagers,
), objectCreater, kind, DefaultTrackOnCreateProbability,
), typeConverter, objectConverter, kind.GroupVersion()),
), subresource,
)
}
// DecodeManagedFields converts ManagedFields from the wire format (api format)
// to the format used by sigs.k8s.io/structured-merge-diff
func DecodeManagedFields(encodedManagedFields []metav1.ManagedFieldsEntry) (Managed, error) {
return internal.DecodeManagedFields(encodedManagedFields)
}
func decodeLiveOrNew(liveObj, newObj runtime.Object, ignoreManagedFieldsFromRequestObject bool) (Managed, error) {
liveAccessor, err := meta.Accessor(liveObj)
if err != nil {
return nil, err
}
// We take the managedFields of the live object in case the request tries to
// manually set managedFields via a subresource.
if ignoreManagedFieldsFromRequestObject {
return emptyManagedFieldsOnErr(DecodeManagedFields(liveAccessor.GetManagedFields()))
}
// If the object doesn't have metadata, we should just return without trying to
// set the managedFields at all, so creates/updates/patches will work normally.
newAccessor, err := meta.Accessor(newObj)
if err != nil {
return nil, err
}
if isResetManagedFields(newAccessor.GetManagedFields()) {
return internal.NewEmptyManaged(), nil
}
// If the managed field is empty or we failed to decode it,
// let's try the live object. This is to prevent clients who
// don't understand managedFields from deleting it accidentally.
managed, err := DecodeManagedFields(newAccessor.GetManagedFields())
if err != nil || len(managed.Fields()) == 0 {
return emptyManagedFieldsOnErr(DecodeManagedFields(liveAccessor.GetManagedFields()))
}
return managed, nil
}
func emptyManagedFieldsOnErr(managed Managed, err error) (Managed, error) {
if err != nil {
return internal.NewEmptyManaged(), nil
}
return managed, nil
}
// Update is used when the object has already been merged (non-apply
// use-case), and simply updates the managed fields in the output
// object.
func (f *FieldManager) Update(liveObj, newObj runtime.Object, manager string) (object runtime.Object, err error) {
// First try to decode the managed fields provided in the update,
// This is necessary to allow directly updating managed fields.
isSubresource := f.subresource != ""
managed, err := decodeLiveOrNew(liveObj, newObj, isSubresource)
if err != nil {
return newObj, nil
}
internal.RemoveObjectManagedFields(newObj)
if object, managed, err = f.fieldManager.Update(liveObj, newObj, managed, manager); err != nil {
return nil, err
}
if err = internal.EncodeObjectManagedFields(object, managed); err != nil {
return nil, fmt.Errorf("failed to encode managed fields: %v", err)
}
return object, nil
}
// UpdateNoErrors is the same as Update, but it will not return
// errors. If an error happens, the object is returned with
// managedFields cleared.
func (f *FieldManager) UpdateNoErrors(liveObj, newObj runtime.Object, manager string) runtime.Object {
obj, err := f.Update(liveObj, newObj, manager)
if err != nil {
atMostEverySecond.Do(func() {
ns, name := "unknown", "unknown"
if accessor, err := meta.Accessor(newObj); err == nil {
ns = accessor.GetNamespace()
name = accessor.GetName()
}
klog.ErrorS(err, "[SHOULD NOT HAPPEN] failed to update managedFields", "VersionKind",
newObj.GetObjectKind().GroupVersionKind(), "namespace", ns, "name", name)
})
// Explicitly remove managedFields on failure, so that
// we can't have garbage in it.
internal.RemoveObjectManagedFields(newObj)
return newObj
}
return obj
}
// Returns true if the managedFields indicate that the user is trying to
// reset the managedFields, i.e. if the list is non-nil but empty, or if
// the list has one empty item.
func isResetManagedFields(managedFields []metav1.ManagedFieldsEntry) bool {
if len(managedFields) == 0 {
return managedFields != nil
}
if len(managedFields) == 1 {
return reflect.DeepEqual(managedFields[0], metav1.ManagedFieldsEntry{})
}
return false
}
// Apply is used when server-side apply is called, as it merges the
// object and updates the managed fields.
func (f *FieldManager) Apply(liveObj, appliedObj runtime.Object, manager string, force bool) (object runtime.Object, err error) {
// If the object doesn't have metadata, apply isn't allowed.
accessor, err := meta.Accessor(liveObj)
if err != nil {
return nil, fmt.Errorf("couldn't get accessor: %v", err)
}
// Decode the managed fields in the live object, since it isn't allowed in the patch.
managed, err := DecodeManagedFields(accessor.GetManagedFields())
if err != nil {
return nil, fmt.Errorf("failed to decode managed fields: %v", err)
}
object, managed, err = f.fieldManager.Apply(liveObj, appliedObj, managed, manager, force)
if err != nil {
if conflicts, ok := err.(merge.Conflicts); ok {
return nil, internal.NewConflictError(conflicts)
}
return nil, err
}
if err = internal.EncodeObjectManagedFields(object, managed); err != nil {
return nil, fmt.Errorf("failed to encode managed fields: %v", err)
}
return object, nil
}

View File

@ -1,60 +0,0 @@
/*
Copyright 2020 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 internal
import (
"sync"
"time"
)
// AtMostEvery will never run the method more than once every specified
// duration.
type AtMostEvery struct {
delay time.Duration
lastCall time.Time
mutex sync.Mutex
}
// NewAtMostEvery creates a new AtMostEvery, that will run the method at
// most every given duration.
func NewAtMostEvery(delay time.Duration) *AtMostEvery {
return &AtMostEvery{
delay: delay,
}
}
// updateLastCall returns true if the lastCall time has been updated,
// false if it was too early.
func (s *AtMostEvery) updateLastCall() bool {
s.mutex.Lock()
defer s.mutex.Unlock()
if time.Since(s.lastCall) < s.delay {
return false
}
s.lastCall = time.Now()
return true
}
// Do will run the method if enough time has passed, and return true.
// Otherwise, it does nothing and returns false.
func (s *AtMostEvery) Do(fn func()) bool {
if !s.updateLastCall() {
return false
}
fn()
return true
}

View File

@ -1,89 +0,0 @@
/*
Copyright 2019 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 internal
import (
"encoding/json"
"fmt"
"sort"
"strings"
"time"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
"sigs.k8s.io/structured-merge-diff/v4/merge"
)
// NewConflictError returns an error including details on the requests apply conflicts
func NewConflictError(conflicts merge.Conflicts) *errors.StatusError {
causes := []metav1.StatusCause{}
for _, conflict := range conflicts {
causes = append(causes, metav1.StatusCause{
Type: metav1.CauseTypeFieldManagerConflict,
Message: fmt.Sprintf("conflict with %v", printManager(conflict.Manager)),
Field: conflict.Path.String(),
})
}
return errors.NewApplyConflict(causes, getConflictMessage(conflicts))
}
func getConflictMessage(conflicts merge.Conflicts) string {
if len(conflicts) == 1 {
return fmt.Sprintf("Apply failed with 1 conflict: conflict with %v: %v", printManager(conflicts[0].Manager), conflicts[0].Path)
}
m := map[string][]fieldpath.Path{}
for _, conflict := range conflicts {
m[conflict.Manager] = append(m[conflict.Manager], conflict.Path)
}
uniqueManagers := []string{}
for manager := range m {
uniqueManagers = append(uniqueManagers, manager)
}
// Print conflicts by sorted managers.
sort.Strings(uniqueManagers)
messages := []string{}
for _, manager := range uniqueManagers {
messages = append(messages, fmt.Sprintf("conflicts with %v:", printManager(manager)))
for _, path := range m[manager] {
messages = append(messages, fmt.Sprintf("- %v", path))
}
}
return fmt.Sprintf("Apply failed with %d conflicts: %s", len(conflicts), strings.Join(messages, "\n"))
}
func printManager(manager string) string {
encodedManager := &metav1.ManagedFieldsEntry{}
if err := json.Unmarshal([]byte(manager), encodedManager); err != nil {
return fmt.Sprintf("%q", manager)
}
managerStr := fmt.Sprintf("%q", encodedManager.Manager)
if encodedManager.Subresource != "" {
managerStr = fmt.Sprintf("%s with subresource %q", managerStr, encodedManager.Subresource)
}
if encodedManager.Operation == metav1.ManagedFieldsOperationUpdate {
if encodedManager.Time == nil {
return fmt.Sprintf("%s using %v", managerStr, encodedManager.APIVersion)
}
return fmt.Sprintf("%s using %v at %v", managerStr, encodedManager.APIVersion, encodedManager.Time.UTC().Format(time.RFC3339))
}
return managerStr
}

View File

@ -1,47 +0,0 @@
/*
Copyright 2018 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 internal
import (
"bytes"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
)
// EmptyFields represents a set with no paths
// It looks like metav1.Fields{Raw: []byte("{}")}
var EmptyFields = func() metav1.FieldsV1 {
f, err := SetToFields(*fieldpath.NewSet())
if err != nil {
panic("should never happen")
}
return f
}()
// FieldsToSet creates a set paths from an input trie of fields
func FieldsToSet(f metav1.FieldsV1) (s fieldpath.Set, err error) {
err = s.FromJSON(bytes.NewReader(f.Raw))
return s, err
}
// SetToFields creates a trie of fields from an input set of paths
func SetToFields(s fieldpath.Set) (f metav1.FieldsV1, err error) {
f.Raw, err = s.ToJSON()
return f, err
}

View File

@ -1,248 +0,0 @@
/*
Copyright 2018 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 internal
import (
"encoding/json"
"fmt"
"sort"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
)
// ManagedInterface groups a fieldpath.ManagedFields together with the timestamps associated with each operation.
type ManagedInterface interface {
// Fields gets the fieldpath.ManagedFields.
Fields() fieldpath.ManagedFields
// Times gets the timestamps associated with each operation.
Times() map[string]*metav1.Time
}
type managedStruct struct {
fields fieldpath.ManagedFields
times map[string]*metav1.Time
}
var _ ManagedInterface = &managedStruct{}
// Fields implements ManagedInterface.
func (m *managedStruct) Fields() fieldpath.ManagedFields {
return m.fields
}
// Times implements ManagedInterface.
func (m *managedStruct) Times() map[string]*metav1.Time {
return m.times
}
// NewEmptyManaged creates an empty ManagedInterface.
func NewEmptyManaged() ManagedInterface {
return NewManaged(fieldpath.ManagedFields{}, map[string]*metav1.Time{})
}
// NewManaged creates a ManagedInterface from a fieldpath.ManagedFields and the timestamps associated with each operation.
func NewManaged(f fieldpath.ManagedFields, t map[string]*metav1.Time) ManagedInterface {
return &managedStruct{
fields: f,
times: t,
}
}
// RemoveObjectManagedFields removes the ManagedFields from the object
// before we merge so that it doesn't appear in the ManagedFields
// recursively.
func RemoveObjectManagedFields(obj runtime.Object) {
accessor, err := meta.Accessor(obj)
if err != nil {
panic(fmt.Sprintf("couldn't get accessor: %v", err))
}
accessor.SetManagedFields(nil)
}
// EncodeObjectManagedFields converts and stores the fieldpathManagedFields into the objects ManagedFields
func EncodeObjectManagedFields(obj runtime.Object, managed ManagedInterface) error {
accessor, err := meta.Accessor(obj)
if err != nil {
panic(fmt.Sprintf("couldn't get accessor: %v", err))
}
encodedManagedFields, err := encodeManagedFields(managed)
if err != nil {
return fmt.Errorf("failed to convert back managed fields to API: %v", err)
}
accessor.SetManagedFields(encodedManagedFields)
return nil
}
// DecodeManagedFields converts ManagedFields from the wire format (api format)
// to the format used by sigs.k8s.io/structured-merge-diff
func DecodeManagedFields(encodedManagedFields []metav1.ManagedFieldsEntry) (ManagedInterface, error) {
managed := managedStruct{}
managed.fields = make(fieldpath.ManagedFields, len(encodedManagedFields))
managed.times = make(map[string]*metav1.Time, len(encodedManagedFields))
for i, encodedVersionedSet := range encodedManagedFields {
switch encodedVersionedSet.Operation {
case metav1.ManagedFieldsOperationApply, metav1.ManagedFieldsOperationUpdate:
default:
return nil, fmt.Errorf("operation must be `Apply` or `Update`")
}
if len(encodedVersionedSet.APIVersion) < 1 {
return nil, fmt.Errorf("apiVersion must not be empty")
}
switch encodedVersionedSet.FieldsType {
case "FieldsV1":
// Valid case.
case "":
return nil, fmt.Errorf("missing fieldsType in managed fields entry %d", i)
default:
return nil, fmt.Errorf("invalid fieldsType %q in managed fields entry %d", encodedVersionedSet.FieldsType, i)
}
manager, err := BuildManagerIdentifier(&encodedVersionedSet)
if err != nil {
return nil, fmt.Errorf("error decoding manager from %v: %v", encodedVersionedSet, err)
}
managed.fields[manager], err = decodeVersionedSet(&encodedVersionedSet)
if err != nil {
return nil, fmt.Errorf("error decoding versioned set from %v: %v", encodedVersionedSet, err)
}
managed.times[manager] = encodedVersionedSet.Time
}
return &managed, nil
}
// BuildManagerIdentifier creates a manager identifier string from a ManagedFieldsEntry
func BuildManagerIdentifier(encodedManager *metav1.ManagedFieldsEntry) (manager string, err error) {
encodedManagerCopy := *encodedManager
// Never include fields type in the manager identifier
encodedManagerCopy.FieldsType = ""
// Never include the fields in the manager identifier
encodedManagerCopy.FieldsV1 = nil
// Never include the time in the manager identifier
encodedManagerCopy.Time = nil
// For appliers, don't include the APIVersion in the manager identifier,
// so it will always have the same manager identifier each time it applied.
if encodedManager.Operation == metav1.ManagedFieldsOperationApply {
encodedManagerCopy.APIVersion = ""
}
// Use the remaining fields to build the manager identifier
b, err := json.Marshal(&encodedManagerCopy)
if err != nil {
return "", fmt.Errorf("error marshalling manager identifier: %v", err)
}
return string(b), nil
}
func decodeVersionedSet(encodedVersionedSet *metav1.ManagedFieldsEntry) (versionedSet fieldpath.VersionedSet, err error) {
fields := EmptyFields
if encodedVersionedSet.FieldsV1 != nil {
fields = *encodedVersionedSet.FieldsV1
}
set, err := FieldsToSet(fields)
if err != nil {
return nil, fmt.Errorf("error decoding set: %v", err)
}
return fieldpath.NewVersionedSet(&set, fieldpath.APIVersion(encodedVersionedSet.APIVersion), encodedVersionedSet.Operation == metav1.ManagedFieldsOperationApply), nil
}
// encodeManagedFields converts ManagedFields from the format used by
// sigs.k8s.io/structured-merge-diff to the wire format (api format)
func encodeManagedFields(managed ManagedInterface) (encodedManagedFields []metav1.ManagedFieldsEntry, err error) {
if len(managed.Fields()) == 0 {
return nil, nil
}
encodedManagedFields = []metav1.ManagedFieldsEntry{}
for manager := range managed.Fields() {
versionedSet := managed.Fields()[manager]
v, err := encodeManagerVersionedSet(manager, versionedSet)
if err != nil {
return nil, fmt.Errorf("error encoding versioned set for %v: %v", manager, err)
}
if t, ok := managed.Times()[manager]; ok {
v.Time = t
}
encodedManagedFields = append(encodedManagedFields, *v)
}
return sortEncodedManagedFields(encodedManagedFields)
}
func sortEncodedManagedFields(encodedManagedFields []metav1.ManagedFieldsEntry) (sortedManagedFields []metav1.ManagedFieldsEntry, err error) {
sort.Slice(encodedManagedFields, func(i, j int) bool {
p, q := encodedManagedFields[i], encodedManagedFields[j]
if p.Operation != q.Operation {
return p.Operation < q.Operation
}
pSeconds, qSeconds := int64(0), int64(0)
if p.Time != nil {
pSeconds = p.Time.Unix()
}
if q.Time != nil {
qSeconds = q.Time.Unix()
}
if pSeconds != qSeconds {
return pSeconds < qSeconds
}
if p.Manager != q.Manager {
return p.Manager < q.Manager
}
if p.APIVersion != q.APIVersion {
return p.APIVersion < q.APIVersion
}
return p.Subresource < q.Subresource
})
return encodedManagedFields, nil
}
func encodeManagerVersionedSet(manager string, versionedSet fieldpath.VersionedSet) (encodedVersionedSet *metav1.ManagedFieldsEntry, err error) {
encodedVersionedSet = &metav1.ManagedFieldsEntry{}
// Get as many fields as we can from the manager identifier
err = json.Unmarshal([]byte(manager), encodedVersionedSet)
if err != nil {
return nil, fmt.Errorf("error unmarshalling manager identifier %v: %v", manager, err)
}
// Get the APIVersion, Operation, and Fields from the VersionedSet
encodedVersionedSet.APIVersion = string(versionedSet.APIVersion())
if versionedSet.Applied() {
encodedVersionedSet.Operation = metav1.ManagedFieldsOperationApply
}
encodedVersionedSet.FieldsType = "FieldsV1"
fields, err := SetToFields(*versionedSet.Set())
if err != nil {
return nil, fmt.Errorf("error encoding set: %v", err)
}
encodedVersionedSet.FieldsV1 = &fields
return encodedVersionedSet, nil
}

View File

@ -1,140 +0,0 @@
/*
Copyright 2018 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 internal
import (
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
"sigs.k8s.io/structured-merge-diff/v4/value"
)
const (
// Field indicates that the content of this path element is a field's name
Field = "f"
// Value indicates that the content of this path element is a field's value
Value = "v"
// Index indicates that the content of this path element is an index in an array
Index = "i"
// Key indicates that the content of this path element is a key value map
Key = "k"
// Separator separates the type of a path element from the contents
Separator = ":"
)
// NewPathElement parses a serialized path element
func NewPathElement(s string) (fieldpath.PathElement, error) {
split := strings.SplitN(s, Separator, 2)
if len(split) < 2 {
return fieldpath.PathElement{}, fmt.Errorf("missing colon: %v", s)
}
switch split[0] {
case Field:
return fieldpath.PathElement{
FieldName: &split[1],
}, nil
case Value:
val, err := value.FromJSON([]byte(split[1]))
if err != nil {
return fieldpath.PathElement{}, err
}
return fieldpath.PathElement{
Value: &val,
}, nil
case Index:
i, err := strconv.Atoi(split[1])
if err != nil {
return fieldpath.PathElement{}, err
}
return fieldpath.PathElement{
Index: &i,
}, nil
case Key:
kv := map[string]json.RawMessage{}
err := json.Unmarshal([]byte(split[1]), &kv)
if err != nil {
return fieldpath.PathElement{}, err
}
fields := value.FieldList{}
for k, v := range kv {
b, err := json.Marshal(v)
if err != nil {
return fieldpath.PathElement{}, err
}
val, err := value.FromJSON(b)
if err != nil {
return fieldpath.PathElement{}, err
}
fields = append(fields, value.Field{
Name: k,
Value: val,
})
}
return fieldpath.PathElement{
Key: &fields,
}, nil
default:
// Ignore unknown key types
return fieldpath.PathElement{}, nil
}
}
// PathElementString serializes a path element
func PathElementString(pe fieldpath.PathElement) (string, error) {
switch {
case pe.FieldName != nil:
return Field + Separator + *pe.FieldName, nil
case pe.Key != nil:
kv := map[string]json.RawMessage{}
for _, k := range *pe.Key {
b, err := value.ToJSON(k.Value)
if err != nil {
return "", err
}
m := json.RawMessage{}
err = json.Unmarshal(b, &m)
if err != nil {
return "", err
}
kv[k.Name] = m
}
b, err := json.Marshal(kv)
if err != nil {
return "", err
}
return Key + ":" + string(b), nil
case pe.Value != nil:
b, err := value.ToJSON(*pe.Value)
if err != nil {
return "", err
}
return Value + ":" + string(b), nil
case pe.Index != nil:
return Index + ":" + strconv.Itoa(*pe.Index), nil
default:
return "", errors.New("Invalid type of path element")
}
}

View File

@ -1,172 +0,0 @@
/*
Copyright 2020 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 fieldmanager
import (
"encoding/json"
"fmt"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
"sigs.k8s.io/structured-merge-diff/v4/merge"
)
type lastAppliedManager struct {
fieldManager Manager
typeConverter TypeConverter
objectConverter runtime.ObjectConvertor
groupVersion schema.GroupVersion
}
var _ Manager = &lastAppliedManager{}
// NewLastAppliedManager converts the client-side apply annotation to
// server-side apply managed fields
func NewLastAppliedManager(fieldManager Manager, typeConverter TypeConverter, objectConverter runtime.ObjectConvertor, groupVersion schema.GroupVersion) Manager {
return &lastAppliedManager{
fieldManager: fieldManager,
typeConverter: typeConverter,
objectConverter: objectConverter,
groupVersion: groupVersion,
}
}
// Update implements Manager.
func (f *lastAppliedManager) Update(liveObj, newObj runtime.Object, managed Managed, manager string) (runtime.Object, Managed, error) {
return f.fieldManager.Update(liveObj, newObj, managed, manager)
}
// Apply will consider the last-applied annotation
// for upgrading an object managed by client-side apply to server-side apply
// without conflicts.
func (f *lastAppliedManager) Apply(liveObj, newObj runtime.Object, managed Managed, manager string, force bool) (runtime.Object, Managed, error) {
newLiveObj, newManaged, newErr := f.fieldManager.Apply(liveObj, newObj, managed, manager, force)
// Upgrade the client-side apply annotation only from kubectl server-side-apply.
// To opt-out of this behavior, users may specify a different field manager.
if manager != "kubectl" {
return newLiveObj, newManaged, newErr
}
// Check if we have conflicts
if newErr == nil {
return newLiveObj, newManaged, newErr
}
conflicts, ok := newErr.(merge.Conflicts)
if !ok {
return newLiveObj, newManaged, newErr
}
conflictSet := conflictsToSet(conflicts)
// Check if conflicts are allowed due to client-side apply,
// and if so, then force apply
allowedConflictSet, err := f.allowedConflictsFromLastApplied(liveObj)
if err != nil {
return newLiveObj, newManaged, newErr
}
if !conflictSet.Difference(allowedConflictSet).Empty() {
newConflicts := conflictsDifference(conflicts, allowedConflictSet)
return newLiveObj, newManaged, newConflicts
}
return f.fieldManager.Apply(liveObj, newObj, managed, manager, true)
}
func (f *lastAppliedManager) allowedConflictsFromLastApplied(liveObj runtime.Object) (*fieldpath.Set, error) {
var accessor, err = meta.Accessor(liveObj)
if err != nil {
panic(fmt.Sprintf("couldn't get accessor: %v", err))
}
// If there is no client-side apply annotation, then there is nothing to do
var annotations = accessor.GetAnnotations()
if annotations == nil {
return nil, fmt.Errorf("no last applied annotation")
}
var lastApplied, ok = annotations[corev1.LastAppliedConfigAnnotation]
if !ok || lastApplied == "" {
return nil, fmt.Errorf("no last applied annotation")
}
liveObjVersioned, err := f.objectConverter.ConvertToVersion(liveObj, f.groupVersion)
if err != nil {
return nil, fmt.Errorf("failed to convert live obj to versioned: %v", err)
}
liveObjTyped, err := f.typeConverter.ObjectToTyped(liveObjVersioned)
if err != nil {
return nil, fmt.Errorf("failed to convert live obj to typed: %v", err)
}
var lastAppliedObj = &unstructured.Unstructured{Object: map[string]interface{}{}}
err = json.Unmarshal([]byte(lastApplied), lastAppliedObj)
if err != nil {
return nil, fmt.Errorf("failed to decode last applied obj: %v in '%s'", err, lastApplied)
}
if lastAppliedObj.GetAPIVersion() != f.groupVersion.String() {
return nil, fmt.Errorf("expected version of last applied to match live object '%s', but got '%s': %v", f.groupVersion.String(), lastAppliedObj.GetAPIVersion(), err)
}
lastAppliedObjTyped, err := f.typeConverter.ObjectToTyped(lastAppliedObj)
if err != nil {
return nil, fmt.Errorf("failed to convert last applied to typed: %v", err)
}
lastAppliedObjFieldSet, err := lastAppliedObjTyped.ToFieldSet()
if err != nil {
return nil, fmt.Errorf("failed to create fieldset for last applied object: %v", err)
}
comparison, err := lastAppliedObjTyped.Compare(liveObjTyped)
if err != nil {
return nil, fmt.Errorf("failed to compare last applied object and live object: %v", err)
}
// Remove fields in last applied that are different, added, or missing in
// the live object.
// Because last-applied fields don't match the live object fields,
// then we don't own these fields.
lastAppliedObjFieldSet = lastAppliedObjFieldSet.
Difference(comparison.Modified).
Difference(comparison.Added).
Difference(comparison.Removed)
return lastAppliedObjFieldSet, nil
}
// TODO: replace with merge.Conflicts.ToSet()
func conflictsToSet(conflicts merge.Conflicts) *fieldpath.Set {
conflictSet := fieldpath.NewSet()
for _, conflict := range []merge.Conflict(conflicts) {
conflictSet.Insert(conflict.Path)
}
return conflictSet
}
func conflictsDifference(conflicts merge.Conflicts, s *fieldpath.Set) merge.Conflicts {
newConflicts := []merge.Conflict{}
for _, conflict := range []merge.Conflict(conflicts) {
if !s.Has(conflict.Path) {
newConflicts = append(newConflicts, conflict)
}
}
return newConflicts
}

View File

@ -1,121 +0,0 @@
/*
Copyright 2020 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 fieldmanager
import (
"fmt"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
)
type lastAppliedUpdater struct {
fieldManager Manager
}
var _ Manager = &lastAppliedUpdater{}
// NewLastAppliedUpdater sets the client-side apply annotation up to date with
// server-side apply managed fields
func NewLastAppliedUpdater(fieldManager Manager) Manager {
return &lastAppliedUpdater{
fieldManager: fieldManager,
}
}
// Update implements Manager.
func (f *lastAppliedUpdater) Update(liveObj, newObj runtime.Object, managed Managed, manager string) (runtime.Object, Managed, error) {
return f.fieldManager.Update(liveObj, newObj, managed, manager)
}
// server-side apply managed fields
func (f *lastAppliedUpdater) Apply(liveObj, newObj runtime.Object, managed Managed, manager string, force bool) (runtime.Object, Managed, error) {
liveObj, managed, err := f.fieldManager.Apply(liveObj, newObj, managed, manager, force)
if err != nil {
return liveObj, managed, err
}
// Sync the client-side apply annotation only from kubectl server-side apply.
// To opt-out of this behavior, users may specify a different field manager.
//
// If the client-side apply annotation doesn't exist,
// then continue because we have no annotation to update
if manager == "kubectl" && hasLastApplied(liveObj) {
lastAppliedValue, err := buildLastApplied(newObj)
if err != nil {
return nil, nil, fmt.Errorf("failed to build last-applied annotation: %v", err)
}
err = setLastApplied(liveObj, lastAppliedValue)
if err != nil {
return nil, nil, fmt.Errorf("failed to set last-applied annotation: %v", err)
}
}
return liveObj, managed, err
}
func hasLastApplied(obj runtime.Object) bool {
var accessor, err = meta.Accessor(obj)
if err != nil {
panic(fmt.Sprintf("couldn't get accessor: %v", err))
}
var annotations = accessor.GetAnnotations()
if annotations == nil {
return false
}
lastApplied, ok := annotations[corev1.LastAppliedConfigAnnotation]
return ok && len(lastApplied) > 0
}
func setLastApplied(obj runtime.Object, value string) error {
accessor, err := meta.Accessor(obj)
if err != nil {
panic(fmt.Sprintf("couldn't get accessor: %v", err))
}
var annotations = accessor.GetAnnotations()
if annotations == nil {
annotations = map[string]string{}
}
annotations[corev1.LastAppliedConfigAnnotation] = value
if err := apimachineryvalidation.ValidateAnnotationsSize(annotations); err != nil {
delete(annotations, corev1.LastAppliedConfigAnnotation)
}
accessor.SetAnnotations(annotations)
return nil
}
func buildLastApplied(obj runtime.Object) (string, error) {
obj = obj.DeepCopyObject()
var accessor, err = meta.Accessor(obj)
if err != nil {
panic(fmt.Sprintf("couldn't get accessor: %v", err))
}
// Remove the annotation from the object before encoding the object
var annotations = accessor.GetAnnotations()
delete(annotations, corev1.LastAppliedConfigAnnotation)
accessor.SetAnnotations(annotations)
lastApplied, err := runtime.Encode(unstructured.UnstructuredJSONScheme, obj)
if err != nil {
return "", fmt.Errorf("couldn't encode object into last applied annotation: %v", err)
}
return string(lastApplied), nil
}

View File

@ -1,83 +0,0 @@
/*
Copyright 2020 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 fieldmanager
import (
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
)
type managedFieldsUpdater struct {
fieldManager Manager
}
var _ Manager = &managedFieldsUpdater{}
// NewManagedFieldsUpdater is responsible for updating the managedfields
// in the object, updating the time of the operation as necessary. For
// updates, it uses a hard-coded manager to detect if things have
// changed, and swaps back the correct manager after the operation is
// done.
func NewManagedFieldsUpdater(fieldManager Manager) Manager {
return &managedFieldsUpdater{
fieldManager: fieldManager,
}
}
// Update implements Manager.
func (f *managedFieldsUpdater) Update(liveObj, newObj runtime.Object, managed Managed, manager string) (runtime.Object, Managed, error) {
self := "current-operation"
object, managed, err := f.fieldManager.Update(liveObj, newObj, managed, self)
if err != nil {
return object, managed, err
}
// If the current operation took any fields from anything, it means the object changed,
// so update the timestamp of the managedFieldsEntry and merge with any previous updates from the same manager
if vs, ok := managed.Fields()[self]; ok {
delete(managed.Fields(), self)
if previous, ok := managed.Fields()[manager]; ok {
managed.Fields()[manager] = fieldpath.NewVersionedSet(vs.Set().Union(previous.Set()), vs.APIVersion(), vs.Applied())
} else {
managed.Fields()[manager] = vs
}
managed.Times()[manager] = &metav1.Time{Time: time.Now().UTC()}
}
return object, managed, nil
}
// Apply implements Manager.
func (f *managedFieldsUpdater) Apply(liveObj, appliedObj runtime.Object, managed Managed, fieldManager string, force bool) (runtime.Object, Managed, error) {
object, managed, err := f.fieldManager.Apply(liveObj, appliedObj, managed, fieldManager, force)
if err != nil {
return object, managed, err
}
if object != nil {
managed.Times()[fieldManager] = &metav1.Time{Time: time.Now().UTC()}
} else {
object = liveObj.DeepCopyObject()
internal.RemoveObjectManagedFields(object)
}
return object, managed, nil
}

View File

@ -1,174 +0,0 @@
/*
Copyright 2021 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 fieldmanager
import (
"fmt"
"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/apiserver/pkg/endpoints/handlers/fieldmanager/internal"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
)
var (
scaleGroupVersion = schema.GroupVersion{Group: "autoscaling", Version: "v1"}
replicasPathInScale = fieldpath.MakePathOrDie("spec", "replicas")
)
// ResourcePathMappings maps a group/version to its replicas path. The
// assumption is that all the paths correspond to leaf fields.
type ResourcePathMappings map[string]fieldpath.Path
// ScaleHandler manages the conversion of managed fields between a main
// resource and the scale subresource
type ScaleHandler struct {
parentEntries []metav1.ManagedFieldsEntry
groupVersion schema.GroupVersion
mappings ResourcePathMappings
}
// NewScaleHandler creates a new ScaleHandler
func NewScaleHandler(parentEntries []metav1.ManagedFieldsEntry, groupVersion schema.GroupVersion, mappings ResourcePathMappings) *ScaleHandler {
return &ScaleHandler{
parentEntries: parentEntries,
groupVersion: groupVersion,
mappings: mappings,
}
}
// ToSubresource filter the managed fields of the main resource and convert
// them so that they can be handled by scale.
// For the managed fields that have a replicas path it performs two changes:
// 1. APIVersion is changed to the APIVersion of the scale subresource
// 2. Replicas path of the main resource is transformed to the replicas path of
// the scale subresource
func (h *ScaleHandler) ToSubresource() ([]metav1.ManagedFieldsEntry, error) {
managed, err := DecodeManagedFields(h.parentEntries)
if err != nil {
return nil, err
}
f := fieldpath.ManagedFields{}
t := map[string]*metav1.Time{}
for manager, versionedSet := range managed.Fields() {
path, ok := h.mappings[string(versionedSet.APIVersion())]
// Skip the entry if the APIVersion is unknown
if !ok || path == nil {
continue
}
if versionedSet.Set().Has(path) {
newVersionedSet := fieldpath.NewVersionedSet(
fieldpath.NewSet(replicasPathInScale),
fieldpath.APIVersion(scaleGroupVersion.String()),
versionedSet.Applied(),
)
f[manager] = newVersionedSet
t[manager] = managed.Times()[manager]
}
}
return managedFieldsEntries(internal.NewManaged(f, t))
}
// ToParent merges `scaleEntries` with the entries of the main resource and
// transforms them accordingly
func (h *ScaleHandler) ToParent(scaleEntries []metav1.ManagedFieldsEntry) ([]metav1.ManagedFieldsEntry, error) {
decodedParentEntries, err := DecodeManagedFields(h.parentEntries)
if err != nil {
return nil, err
}
parentFields := decodedParentEntries.Fields()
decodedScaleEntries, err := DecodeManagedFields(scaleEntries)
if err != nil {
return nil, err
}
scaleFields := decodedScaleEntries.Fields()
f := fieldpath.ManagedFields{}
t := map[string]*metav1.Time{}
for manager, versionedSet := range parentFields {
// Get the main resource "replicas" path
path, ok := h.mappings[string(versionedSet.APIVersion())]
// Drop the entry if the APIVersion is unknown.
if !ok {
continue
}
// If the parent entry does not have the replicas path or it is nil, just
// keep it as it is. The path is nil for Custom Resources without scale
// subresource.
if path == nil || !versionedSet.Set().Has(path) {
f[manager] = versionedSet
t[manager] = decodedParentEntries.Times()[manager]
continue
}
if _, ok := scaleFields[manager]; !ok {
// "Steal" the replicas path from the main resource entry
newSet := versionedSet.Set().Difference(fieldpath.NewSet(path))
if !newSet.Empty() {
newVersionedSet := fieldpath.NewVersionedSet(
newSet,
versionedSet.APIVersion(),
versionedSet.Applied(),
)
f[manager] = newVersionedSet
t[manager] = decodedParentEntries.Times()[manager]
}
} else {
// Field wasn't stolen, let's keep the entry as it is.
f[manager] = versionedSet
t[manager] = decodedParentEntries.Times()[manager]
delete(scaleFields, manager)
}
}
for manager, versionedSet := range scaleFields {
if !versionedSet.Set().Has(replicasPathInScale) {
continue
}
newVersionedSet := fieldpath.NewVersionedSet(
fieldpath.NewSet(h.mappings[h.groupVersion.String()]),
fieldpath.APIVersion(h.groupVersion.String()),
versionedSet.Applied(),
)
f[manager] = newVersionedSet
t[manager] = decodedParentEntries.Times()[manager]
}
return managedFieldsEntries(internal.NewManaged(f, t))
}
func managedFieldsEntries(entries internal.ManagedInterface) ([]metav1.ManagedFieldsEntry, error) {
obj := &unstructured.Unstructured{Object: map[string]interface{}{}}
if err := internal.EncodeObjectManagedFields(obj, entries); err != nil {
return nil, err
}
accessor, err := meta.Accessor(obj)
if err != nil {
panic(fmt.Sprintf("couldn't get accessor: %v", err))
}
return accessor.GetManagedFields(), nil
}

View File

@ -1,91 +0,0 @@
/*
Copyright 2019 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 fieldmanager
import (
"fmt"
"math/rand"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
type skipNonAppliedManager struct {
fieldManager Manager
objectCreater runtime.ObjectCreater
gvk schema.GroupVersionKind
beforeApplyManagerName string
probability float32
}
var _ Manager = &skipNonAppliedManager{}
// NewSkipNonAppliedManager creates a new wrapped FieldManager that only starts tracking managers after the first apply.
func NewSkipNonAppliedManager(fieldManager Manager, objectCreater runtime.ObjectCreater, gvk schema.GroupVersionKind) Manager {
return NewProbabilisticSkipNonAppliedManager(fieldManager, objectCreater, gvk, 0.0)
}
// NewProbabilisticSkipNonAppliedManager creates a new wrapped FieldManager that starts tracking managers after the first apply,
// or starts tracking on create with p probability.
func NewProbabilisticSkipNonAppliedManager(fieldManager Manager, objectCreater runtime.ObjectCreater, gvk schema.GroupVersionKind, p float32) Manager {
return &skipNonAppliedManager{
fieldManager: fieldManager,
objectCreater: objectCreater,
gvk: gvk,
beforeApplyManagerName: "before-first-apply",
probability: p,
}
}
// Update implements Manager.
func (f *skipNonAppliedManager) Update(liveObj, newObj runtime.Object, managed Managed, manager string) (runtime.Object, Managed, error) {
accessor, err := meta.Accessor(liveObj)
if err != nil {
return newObj, managed, nil
}
// If managed fields is empty, we need to determine whether to skip tracking managed fields.
if len(managed.Fields()) == 0 {
// Check if the operation is a create, by checking whether lastObj's UID is empty.
// If the operation is create, P(tracking managed fields) = f.probability
// If the operation is update, skip tracking managed fields, since we already know managed fields is empty.
if len(accessor.GetUID()) == 0 {
if f.probability <= rand.Float32() {
return newObj, managed, nil
}
} else {
return newObj, managed, nil
}
}
return f.fieldManager.Update(liveObj, newObj, managed, manager)
}
// Apply implements Manager.
func (f *skipNonAppliedManager) Apply(liveObj, appliedObj runtime.Object, managed Managed, fieldManager string, force bool) (runtime.Object, Managed, error) {
if len(managed.Fields()) == 0 {
emptyObj, err := f.objectCreater.New(f.gvk)
if err != nil {
return nil, nil, fmt.Errorf("failed to create empty object of type %v: %v", f.gvk, err)
}
liveObj, managed, err = f.fieldManager.Update(emptyObj, liveObj, managed, f.beforeApplyManagerName)
if err != nil {
return nil, nil, fmt.Errorf("failed to create manager for existing fields: %v", err)
}
}
return f.fieldManager.Apply(liveObj, appliedObj, managed, fieldManager, force)
}

View File

@ -1,90 +0,0 @@
/*
Copyright 2019 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 fieldmanager
import (
"fmt"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
)
type stripMetaManager struct {
fieldManager Manager
// stripSet is the list of fields that should never be part of a mangedFields.
stripSet *fieldpath.Set
}
var _ Manager = &stripMetaManager{}
// NewStripMetaManager creates a new Manager that strips metadata and typemeta fields from the manager's fieldset.
func NewStripMetaManager(fieldManager Manager) Manager {
return &stripMetaManager{
fieldManager: fieldManager,
stripSet: fieldpath.NewSet(
fieldpath.MakePathOrDie("apiVersion"),
fieldpath.MakePathOrDie("kind"),
fieldpath.MakePathOrDie("metadata"),
fieldpath.MakePathOrDie("metadata", "name"),
fieldpath.MakePathOrDie("metadata", "namespace"),
fieldpath.MakePathOrDie("metadata", "creationTimestamp"),
fieldpath.MakePathOrDie("metadata", "selfLink"),
fieldpath.MakePathOrDie("metadata", "uid"),
fieldpath.MakePathOrDie("metadata", "clusterName"),
fieldpath.MakePathOrDie("metadata", "generation"),
fieldpath.MakePathOrDie("metadata", "managedFields"),
fieldpath.MakePathOrDie("metadata", "resourceVersion"),
),
}
}
// Update implements Manager.
func (f *stripMetaManager) Update(liveObj, newObj runtime.Object, managed Managed, manager string) (runtime.Object, Managed, error) {
newObj, managed, err := f.fieldManager.Update(liveObj, newObj, managed, manager)
if err != nil {
return nil, nil, err
}
f.stripFields(managed.Fields(), manager)
return newObj, managed, nil
}
// Apply implements Manager.
func (f *stripMetaManager) Apply(liveObj, appliedObj runtime.Object, managed Managed, manager string, force bool) (runtime.Object, Managed, error) {
newObj, managed, err := f.fieldManager.Apply(liveObj, appliedObj, managed, manager, force)
if err != nil {
return nil, nil, err
}
f.stripFields(managed.Fields(), manager)
return newObj, managed, nil
}
// stripFields removes a predefined set of paths found in typed from managed
func (f *stripMetaManager) stripFields(managed fieldpath.ManagedFields, manager string) {
vs, ok := managed[manager]
if ok {
if vs == nil {
panic(fmt.Sprintf("Found unexpected nil manager which should never happen: %s", manager))
}
newSet := vs.Set().Difference(f.stripSet)
if newSet.Empty() {
delete(managed, manager)
} else {
managed[manager] = fieldpath.NewVersionedSet(newSet, vs.APIVersion(), vs.Applied())
}
}
}

View File

@ -1,184 +0,0 @@
/*
Copyright 2019 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 fieldmanager
import (
"fmt"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
"sigs.k8s.io/structured-merge-diff/v4/merge"
)
type structuredMergeManager struct {
typeConverter TypeConverter
objectConverter runtime.ObjectConvertor
objectDefaulter runtime.ObjectDefaulter
groupVersion schema.GroupVersion
hubVersion schema.GroupVersion
updater merge.Updater
}
var _ Manager = &structuredMergeManager{}
// NewStructuredMergeManager creates a new Manager that merges apply requests
// and update managed fields for other types of requests.
func NewStructuredMergeManager(typeConverter TypeConverter, objectConverter runtime.ObjectConvertor, objectDefaulter runtime.ObjectDefaulter, gv schema.GroupVersion, hub schema.GroupVersion, resetFields map[fieldpath.APIVersion]*fieldpath.Set) (Manager, error) {
return &structuredMergeManager{
typeConverter: typeConverter,
objectConverter: objectConverter,
objectDefaulter: objectDefaulter,
groupVersion: gv,
hubVersion: hub,
updater: merge.Updater{
Converter: newVersionConverter(typeConverter, objectConverter, hub), // This is the converter provided to SMD from k8s
IgnoredFields: resetFields,
},
}, nil
}
// NewCRDStructuredMergeManager creates a new Manager specifically for
// CRDs. This allows for the possibility of fields which are not defined
// in models, as well as having no models defined at all.
func NewCRDStructuredMergeManager(typeConverter TypeConverter, objectConverter runtime.ObjectConvertor, objectDefaulter runtime.ObjectDefaulter, gv schema.GroupVersion, hub schema.GroupVersion, resetFields map[fieldpath.APIVersion]*fieldpath.Set) (_ Manager, err error) {
return &structuredMergeManager{
typeConverter: typeConverter,
objectConverter: objectConverter,
objectDefaulter: objectDefaulter,
groupVersion: gv,
hubVersion: hub,
updater: merge.Updater{
Converter: newCRDVersionConverter(typeConverter, objectConverter, hub),
IgnoredFields: resetFields,
},
}, nil
}
func objectGVKNN(obj runtime.Object) string {
name := "<unknown>"
namespace := "<unknown>"
if accessor, err := meta.Accessor(obj); err == nil {
name = accessor.GetName()
namespace = accessor.GetNamespace()
}
return fmt.Sprintf("%v/%v; %v", namespace, name, obj.GetObjectKind().GroupVersionKind())
}
// Update implements Manager.
func (f *structuredMergeManager) Update(liveObj, newObj runtime.Object, managed Managed, manager string) (runtime.Object, Managed, error) {
newObjVersioned, err := f.toVersioned(newObj)
if err != nil {
return nil, nil, fmt.Errorf("failed to convert new object (%v) to proper version (%v): %v", objectGVKNN(newObj), f.groupVersion, err)
}
liveObjVersioned, err := f.toVersioned(liveObj)
if err != nil {
return nil, nil, fmt.Errorf("failed to convert live object (%v) to proper version: %v", objectGVKNN(liveObj), err)
}
newObjTyped, err := f.typeConverter.ObjectToTyped(newObjVersioned)
if err != nil {
return nil, nil, fmt.Errorf("failed to convert new object (%v) to smd typed: %v", objectGVKNN(newObjVersioned), err)
}
liveObjTyped, err := f.typeConverter.ObjectToTyped(liveObjVersioned)
if err != nil {
return nil, nil, fmt.Errorf("failed to convert live object (%v) to smd typed: %v", objectGVKNN(liveObjVersioned), err)
}
apiVersion := fieldpath.APIVersion(f.groupVersion.String())
// TODO(apelisse) use the first return value when unions are implemented
_, managedFields, err := f.updater.Update(liveObjTyped, newObjTyped, apiVersion, managed.Fields(), manager)
if err != nil {
return nil, nil, fmt.Errorf("failed to update ManagedFields (%v): %v", objectGVKNN(newObjVersioned), err)
}
managed = internal.NewManaged(managedFields, managed.Times())
return newObj, managed, nil
}
// Apply implements Manager.
func (f *structuredMergeManager) Apply(liveObj, patchObj runtime.Object, managed Managed, manager string, force bool) (runtime.Object, Managed, error) {
// Check that the patch object has the same version as the live object
if patchVersion := patchObj.GetObjectKind().GroupVersionKind().GroupVersion(); patchVersion != f.groupVersion {
return nil, nil,
errors.NewBadRequest(
fmt.Sprintf("Incorrect version specified in apply patch. "+
"Specified patch version: %s, expected: %s",
patchVersion, f.groupVersion))
}
patchObjMeta, err := meta.Accessor(patchObj)
if err != nil {
return nil, nil, fmt.Errorf("couldn't get accessor: %v", err)
}
if patchObjMeta.GetManagedFields() != nil {
return nil, nil, errors.NewBadRequest("metadata.managedFields must be nil")
}
liveObjVersioned, err := f.toVersioned(liveObj)
if err != nil {
return nil, nil, fmt.Errorf("failed to convert live object (%v) to proper version: %v", objectGVKNN(liveObj), err)
}
patchObjTyped, err := f.typeConverter.ObjectToTyped(patchObj)
if err != nil {
return nil, nil, fmt.Errorf("failed to create typed patch object (%v): %v", objectGVKNN(patchObj), err)
}
liveObjTyped, err := f.typeConverter.ObjectToTyped(liveObjVersioned)
if err != nil {
return nil, nil, fmt.Errorf("failed to create typed live object (%v): %v", objectGVKNN(liveObjVersioned), err)
}
apiVersion := fieldpath.APIVersion(f.groupVersion.String())
newObjTyped, managedFields, err := f.updater.Apply(liveObjTyped, patchObjTyped, apiVersion, managed.Fields(), manager, force)
if err != nil {
return nil, nil, err
}
managed = internal.NewManaged(managedFields, managed.Times())
if newObjTyped == nil {
return nil, managed, nil
}
newObj, err := f.typeConverter.TypedToObject(newObjTyped)
if err != nil {
return nil, nil, fmt.Errorf("failed to convert new typed object (%v) to object: %v", objectGVKNN(patchObj), err)
}
newObjVersioned, err := f.toVersioned(newObj)
if err != nil {
return nil, nil, fmt.Errorf("failed to convert new object (%v) to proper version: %v", objectGVKNN(patchObj), err)
}
f.objectDefaulter.Default(newObjVersioned)
newObjUnversioned, err := f.toUnversioned(newObjVersioned)
if err != nil {
return nil, nil, fmt.Errorf("failed to convert to unversioned (%v): %v", objectGVKNN(patchObj), err)
}
return newObjUnversioned, managed, nil
}
func (f *structuredMergeManager) toVersioned(obj runtime.Object) (runtime.Object, error) {
return f.objectConverter.ConvertToVersion(obj, f.groupVersion)
}
func (f *structuredMergeManager) toUnversioned(obj runtime.Object) (runtime.Object, error) {
return f.objectConverter.ConvertToVersion(obj, f.hubVersion)
}

View File

@ -1,130 +0,0 @@
/*
Copyright 2018 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 fieldmanager
import (
"fmt"
"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"
"k8s.io/kube-openapi/pkg/util/proto"
"sigs.k8s.io/structured-merge-diff/v4/typed"
"sigs.k8s.io/structured-merge-diff/v4/value"
)
// TypeConverter allows you to convert from runtime.Object to
// typed.TypedValue and the other way around.
type TypeConverter interface {
ObjectToTyped(runtime.Object) (*typed.TypedValue, error)
TypedToObject(*typed.TypedValue) (runtime.Object, error)
}
// DeducedTypeConverter is a TypeConverter for CRDs that don't have a
// schema. It does implement the same interface though (and create the
// same types of objects), so that everything can still work the same.
// CRDs are merged with all their fields being "atomic" (lists
// included).
//
// Note that this is not going to be sufficient for converting to/from
// CRDs that have a schema defined (we don't support that schema yet).
// TODO(jennybuckley): Use the schema provided by a CRD if it exists.
type DeducedTypeConverter struct{}
var _ TypeConverter = DeducedTypeConverter{}
// ObjectToTyped converts an object into a TypedValue with a "deduced type".
func (DeducedTypeConverter) ObjectToTyped(obj runtime.Object) (*typed.TypedValue, error) {
switch o := obj.(type) {
case *unstructured.Unstructured:
return typed.DeducedParseableType.FromUnstructured(o.UnstructuredContent())
default:
return typed.DeducedParseableType.FromStructured(obj)
}
}
// TypedToObject transforms the typed value into a runtime.Object. That
// is not specific to deduced type.
func (DeducedTypeConverter) TypedToObject(value *typed.TypedValue) (runtime.Object, error) {
return valueToObject(value.AsValue())
}
type typeConverter struct {
parser *managedfields.GvkParser
}
var _ TypeConverter = &typeConverter{}
// NewTypeConverter builds a TypeConverter from a proto.Models. This
// will automatically find the proper version of the object, and the
// corresponding schema information.
func NewTypeConverter(models proto.Models, preserveUnknownFields bool) (TypeConverter, error) {
parser, err := managedfields.NewGVKParser(models, preserveUnknownFields)
if err != nil {
return nil, err
}
return &typeConverter{parser: parser}, nil
}
func (c *typeConverter) ObjectToTyped(obj runtime.Object) (*typed.TypedValue, error) {
gvk := obj.GetObjectKind().GroupVersionKind()
t := c.parser.Type(gvk)
if t == nil {
return nil, newNoCorrespondingTypeError(gvk)
}
switch o := obj.(type) {
case *unstructured.Unstructured:
return t.FromUnstructured(o.UnstructuredContent())
default:
return t.FromStructured(obj)
}
}
func (c *typeConverter) TypedToObject(value *typed.TypedValue) (runtime.Object, error) {
return valueToObject(value.AsValue())
}
func valueToObject(val value.Value) (runtime.Object, error) {
vu := val.Unstructured()
switch o := vu.(type) {
case map[string]interface{}:
return &unstructured.Unstructured{Object: o}, nil
default:
return nil, fmt.Errorf("failed to convert value to unstructured for type %T", vu)
}
}
type noCorrespondingTypeErr struct {
gvk schema.GroupVersionKind
}
func newNoCorrespondingTypeError(gvk schema.GroupVersionKind) error {
return &noCorrespondingTypeErr{gvk: gvk}
}
func (k *noCorrespondingTypeErr) Error() string {
return fmt.Sprintf("no corresponding type for %v", k.gvk)
}
func isNoCorrespondingTypeError(err error) bool {
if err == nil {
return false
}
_, ok := err.(*noCorrespondingTypeErr)
return ok
}

View File

@ -1,101 +0,0 @@
/*
Copyright 2018 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 fieldmanager
import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
"sigs.k8s.io/structured-merge-diff/v4/merge"
"sigs.k8s.io/structured-merge-diff/v4/typed"
)
// versionConverter is an implementation of
// sigs.k8s.io/structured-merge-diff/merge.Converter
type versionConverter struct {
typeConverter TypeConverter
objectConvertor runtime.ObjectConvertor
hubGetter func(from schema.GroupVersion) schema.GroupVersion
}
var _ merge.Converter = &versionConverter{}
// NewVersionConverter builds a VersionConverter from a TypeConverter and an ObjectConvertor.
func newVersionConverter(t TypeConverter, o runtime.ObjectConvertor, h schema.GroupVersion) merge.Converter {
return &versionConverter{
typeConverter: t,
objectConvertor: o,
hubGetter: func(from schema.GroupVersion) schema.GroupVersion {
return schema.GroupVersion{
Group: from.Group,
Version: h.Version,
}
},
}
}
// NewCRDVersionConverter builds a VersionConverter for CRDs from a TypeConverter and an ObjectConvertor.
func newCRDVersionConverter(t TypeConverter, o runtime.ObjectConvertor, h schema.GroupVersion) merge.Converter {
return &versionConverter{
typeConverter: t,
objectConvertor: o,
hubGetter: func(from schema.GroupVersion) schema.GroupVersion {
return h
},
}
}
// Convert implements sigs.k8s.io/structured-merge-diff/merge.Converter
func (v *versionConverter) Convert(object *typed.TypedValue, version fieldpath.APIVersion) (*typed.TypedValue, error) {
// Convert the smd typed value to a kubernetes object.
objectToConvert, err := v.typeConverter.TypedToObject(object)
if err != nil {
return object, err
}
// Parse the target groupVersion.
groupVersion, err := schema.ParseGroupVersion(string(version))
if err != nil {
return object, err
}
// If attempting to convert to the same version as we already have, just return it.
fromVersion := objectToConvert.GetObjectKind().GroupVersionKind().GroupVersion()
if fromVersion == groupVersion {
return object, nil
}
// Convert to internal
internalObject, err := v.objectConvertor.ConvertToVersion(objectToConvert, v.hubGetter(fromVersion))
if err != nil {
return object, err
}
// Convert the object into the target version
convertedObject, err := v.objectConvertor.ConvertToVersion(internalObject, groupVersion)
if err != nil {
return object, err
}
// Convert the object back to a smd typed value and return it.
return v.typeConverter.ObjectToTyped(convertedObject)
}
// IsMissingVersionError
func (v *versionConverter) IsMissingVersionError(err error) bool {
return runtime.IsNotRegisteredError(err) || isNoCorrespondingTypeError(err)
}

View File

@ -39,7 +39,9 @@ import (
"k8s.io/apiserver/pkg/endpoints/handlers/negotiation"
"k8s.io/apiserver/pkg/endpoints/metrics"
"k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/features"
"k8s.io/apiserver/pkg/registry/rest"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/component-base/tracing"
"k8s.io/klog/v2"
)
@ -198,7 +200,8 @@ func ListResource(r rest.Lister, rw rest.Watcher, scope *RequestScope, forceWatc
return
}
if errs := metainternalversionvalidation.ValidateListOptions(&opts); len(errs) > 0 {
metainternalversion.SetListOptionsDefaults(&opts, utilfeature.DefaultFeatureGate.Enabled(features.WatchList))
if errs := metainternalversionvalidation.ValidateListOptions(&opts, utilfeature.DefaultFeatureGate.Enabled(features.WatchList)); len(errs) > 0 {
err := errors.NewInvalid(schema.GroupKind{Group: metav1.GroupName, Kind: "ListOptions"}, "", errs)
scope.err(err, w, req)
return

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