Matchconditions admission webhooks alpha implementation for kep-3716 (#116261)
* api changes adding match conditions * feature gate and registry strategy to drop fields * matchConditions logic for admission webhooks * feedback * update test * import order * bears.com * update fail policy ignore behavior * update docs and matcher to hold fail policy as non-pointer * update matcher error aggregation, fix early fail failpolicy ignore, update docs * final cleanup * openapi gen Kubernetes-commit: 5e5b3029f3bbfc93c3569f07ad300a5c6057fc58
This commit is contained in:
parent
b841df9c51
commit
05d2078e68
8
go.mod
8
go.mod
|
|
@ -42,9 +42,9 @@ require (
|
||||||
google.golang.org/protobuf v1.28.1
|
google.golang.org/protobuf v1.28.1
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||||
gopkg.in/square/go-jose.v2 v2.6.0
|
gopkg.in/square/go-jose.v2 v2.6.0
|
||||||
k8s.io/api v0.0.0-20230315055832-c79498c4e63b
|
k8s.io/api v0.0.0-20230315055831-abe66f57fdb1
|
||||||
k8s.io/apimachinery v0.0.0-20230315054728-8d1258da8f38
|
k8s.io/apimachinery v0.0.0-20230315054728-8d1258da8f38
|
||||||
k8s.io/client-go v0.0.0-20230315061852-9ff2627505a4
|
k8s.io/client-go v0.0.0-20230315061912-38589731da69
|
||||||
k8s.io/component-base v0.0.0-20230315065615-6b9bb8ecc3d0
|
k8s.io/component-base v0.0.0-20230315065615-6b9bb8ecc3d0
|
||||||
k8s.io/klog/v2 v2.90.1
|
k8s.io/klog/v2 v2.90.1
|
||||||
k8s.io/kms v0.0.0-20230315071541-54e6d3479bfc
|
k8s.io/kms v0.0.0-20230315071541-54e6d3479bfc
|
||||||
|
|
@ -124,9 +124,9 @@ require (
|
||||||
)
|
)
|
||||||
|
|
||||||
replace (
|
replace (
|
||||||
k8s.io/api => k8s.io/api v0.0.0-20230315055832-c79498c4e63b
|
k8s.io/api => k8s.io/api v0.0.0-20230315032826-0b4c449988b1
|
||||||
k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20230315054728-8d1258da8f38
|
k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20230315054728-8d1258da8f38
|
||||||
k8s.io/client-go => k8s.io/client-go v0.0.0-20230315061852-9ff2627505a4
|
k8s.io/client-go => k8s.io/client-go v0.0.0-20230315061912-38589731da69
|
||||||
k8s.io/component-base => k8s.io/component-base v0.0.0-20230315065615-6b9bb8ecc3d0
|
k8s.io/component-base => k8s.io/component-base v0.0.0-20230315065615-6b9bb8ecc3d0
|
||||||
k8s.io/kms => k8s.io/kms v0.0.0-20230315071541-54e6d3479bfc
|
k8s.io/kms => k8s.io/kms v0.0.0-20230315071541-54e6d3479bfc
|
||||||
)
|
)
|
||||||
|
|
|
||||||
8
go.sum
8
go.sum
|
|
@ -878,12 +878,12 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
|
||||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||||
k8s.io/api v0.0.0-20230315055832-c79498c4e63b h1:+XtheLjvEgurlWvtf13tutNwmW6WUHGezbiapc4s6EM=
|
k8s.io/api v0.0.0-20230315032826-0b4c449988b1 h1:wlCdY1kqV0RkfnfRr4mEZ3fGJ1VvLelr5Q2vCnCICIo=
|
||||||
k8s.io/api v0.0.0-20230315055832-c79498c4e63b/go.mod h1:aZ6MBt4NMLXSxkSKFkoDaP4hTutnZIvH5dCSpOis9g4=
|
k8s.io/api v0.0.0-20230315032826-0b4c449988b1/go.mod h1:aZ6MBt4NMLXSxkSKFkoDaP4hTutnZIvH5dCSpOis9g4=
|
||||||
k8s.io/apimachinery v0.0.0-20230315054728-8d1258da8f38 h1:n1qDRCTPAXwyXYg7eSpWDO9FdW79lwAQ9dAr1vETpn4=
|
k8s.io/apimachinery v0.0.0-20230315054728-8d1258da8f38 h1:n1qDRCTPAXwyXYg7eSpWDO9FdW79lwAQ9dAr1vETpn4=
|
||||||
k8s.io/apimachinery v0.0.0-20230315054728-8d1258da8f38/go.mod h1:5ikh59fK3AJ287GUvpUsryoMFtH9zj/ARfWCo3AyXTM=
|
k8s.io/apimachinery v0.0.0-20230315054728-8d1258da8f38/go.mod h1:5ikh59fK3AJ287GUvpUsryoMFtH9zj/ARfWCo3AyXTM=
|
||||||
k8s.io/client-go v0.0.0-20230315061852-9ff2627505a4 h1:mjPYL7mzIzB9PHI4ttnYkGY5zat6ObsndQNh7Gx12OQ=
|
k8s.io/client-go v0.0.0-20230315061912-38589731da69 h1:LnTXY9Akksk/aAmbebKIaC0doqk1aKbZQA3OoAd0BB0=
|
||||||
k8s.io/client-go v0.0.0-20230315061852-9ff2627505a4/go.mod h1:daDXfDBtiJdqKrqqFL0WtQ6hHzsDxP1lCEdTt3+kijU=
|
k8s.io/client-go v0.0.0-20230315061912-38589731da69/go.mod h1:b0alWGtfu+BI7XZwwdOHJIsr7aDjKf3ANThw8Sr+tw8=
|
||||||
k8s.io/component-base v0.0.0-20230315065615-6b9bb8ecc3d0 h1:IjneP02MOB07PIP9+PQjKrOIZEZ5T7umR+GIZkU4h0U=
|
k8s.io/component-base v0.0.0-20230315065615-6b9bb8ecc3d0 h1:IjneP02MOB07PIP9+PQjKrOIZEZ5T7umR+GIZkU4h0U=
|
||||||
k8s.io/component-base v0.0.0-20230315065615-6b9bb8ecc3d0/go.mod h1:kTuptveA6tUMLMKnaq4AbIAAk7IcdhwkbljAV3JZRpM=
|
k8s.io/component-base v0.0.0-20230315065615-6b9bb8ecc3d0/go.mod h1:kTuptveA6tUMLMKnaq4AbIAAk7IcdhwkbljAV3JZRpM=
|
||||||
k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw=
|
k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw=
|
||||||
|
|
|
||||||
|
|
@ -112,12 +112,13 @@ func (p pluginHandlerWithMetrics) Validate(ctx context.Context, a admission.Attr
|
||||||
|
|
||||||
// AdmissionMetrics instruments admission with prometheus metrics.
|
// AdmissionMetrics instruments admission with prometheus metrics.
|
||||||
type AdmissionMetrics struct {
|
type AdmissionMetrics struct {
|
||||||
step *metricSet
|
step *metricSet
|
||||||
controller *metricSet
|
controller *metricSet
|
||||||
webhook *metricSet
|
webhook *metricSet
|
||||||
webhookRejection *metrics.CounterVec
|
webhookRejection *metrics.CounterVec
|
||||||
webhookFailOpen *metrics.CounterVec
|
webhookFailOpen *metrics.CounterVec
|
||||||
webhookRequest *metrics.CounterVec
|
webhookRequest *metrics.CounterVec
|
||||||
|
matchConditionEvalErrors *metrics.CounterVec
|
||||||
}
|
}
|
||||||
|
|
||||||
// newAdmissionMetrics create a new AdmissionMetrics, configured with default metric names.
|
// newAdmissionMetrics create a new AdmissionMetrics, configured with default metric names.
|
||||||
|
|
@ -217,13 +218,24 @@ func newAdmissionMetrics() *AdmissionMetrics {
|
||||||
},
|
},
|
||||||
[]string{"name", "type", "operation", "code", "rejected"})
|
[]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()
|
step.mustRegister()
|
||||||
controller.mustRegister()
|
controller.mustRegister()
|
||||||
webhook.mustRegister()
|
webhook.mustRegister()
|
||||||
legacyregistry.MustRegister(webhookRejection)
|
legacyregistry.MustRegister(webhookRejection)
|
||||||
legacyregistry.MustRegister(webhookFailOpen)
|
legacyregistry.MustRegister(webhookFailOpen)
|
||||||
legacyregistry.MustRegister(webhookRequest)
|
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() {
|
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()
|
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 {
|
type metricSet struct {
|
||||||
latencies *metrics.HistogramVec
|
latencies *metrics.HistogramVec
|
||||||
latenciesSummary *metrics.SummaryVec
|
latenciesSummary *metrics.SummaryVec
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,6 @@ import (
|
||||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ ExpressionAccessor = &MatchCondition{}
|
|
||||||
|
|
||||||
type ExpressionAccessor interface {
|
type ExpressionAccessor interface {
|
||||||
GetExpression() string
|
GetExpression() string
|
||||||
ReturnTypes() []*cel.Type
|
ReturnTypes() []*cel.Type
|
||||||
|
|
@ -44,19 +42,6 @@ type EvaluationResult struct {
|
||||||
Error error
|
Error error
|
||||||
}
|
}
|
||||||
|
|
||||||
// MatchCondition contains the inputs needed to compile, evaluate and match a cel expression
|
|
||||||
type MatchCondition struct {
|
|
||||||
Expression string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *MatchCondition) GetExpression() string {
|
|
||||||
return v.Expression
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *MatchCondition) ReturnTypes() []*cel.Type {
|
|
||||||
return []*cel.Type{cel.BoolType}
|
|
||||||
}
|
|
||||||
|
|
||||||
// OptionalVariableDeclarations declares which optional CEL variables
|
// OptionalVariableDeclarations declares which optional CEL variables
|
||||||
// are declared for an expression.
|
// are declared for an expression.
|
||||||
type OptionalVariableDeclarations struct {
|
type OptionalVariableDeclarations struct {
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,6 @@ import (
|
||||||
|
|
||||||
celtypes "github.com/google/cel-go/common/types"
|
celtypes "github.com/google/cel-go/common/types"
|
||||||
|
|
||||||
"k8s.io/klog/v2"
|
|
||||||
|
|
||||||
v1 "k8s.io/api/admissionregistration/v1"
|
v1 "k8s.io/api/admissionregistration/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
|
@ -33,6 +31,7 @@ import (
|
||||||
celconfig "k8s.io/apiserver/pkg/apis/cel"
|
celconfig "k8s.io/apiserver/pkg/apis/cel"
|
||||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
apiservercel "k8s.io/apiserver/pkg/cel"
|
apiservercel "k8s.io/apiserver/pkg/cel"
|
||||||
|
"k8s.io/klog/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// validator implements the Validator interface
|
// validator implements the Validator interface
|
||||||
|
|
|
||||||
|
|
@ -19,11 +19,15 @@ package webhook
|
||||||
import (
|
import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"k8s.io/api/admissionregistration/v1"
|
v1 "k8s.io/api/admissionregistration/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/labels"
|
"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/namespace"
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/object"
|
"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"
|
webhookutil "k8s.io/apiserver/pkg/util/webhook"
|
||||||
"k8s.io/client-go/rest"
|
"k8s.io/client-go/rest"
|
||||||
)
|
)
|
||||||
|
|
@ -44,6 +48,9 @@ type WebhookAccessor interface {
|
||||||
// GetRESTClient gets the webhook client
|
// GetRESTClient gets the webhook client
|
||||||
GetRESTClient(clientManager *webhookutil.ClientManager) (*rest.RESTClient, error)
|
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
|
// 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
|
// configuration and does not provide a globally unique identity, if a unique identity is
|
||||||
// needed, use GetUID.
|
// needed, use GetUID.
|
||||||
|
|
@ -67,6 +74,9 @@ type WebhookAccessor interface {
|
||||||
// GetAdmissionReviewVersions gets the webhook AdmissionReviewVersions field.
|
// GetAdmissionReviewVersions gets the webhook AdmissionReviewVersions field.
|
||||||
GetAdmissionReviewVersions() []string
|
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 if the accessor contains a MutatingWebhook, returns it and true, else returns false.
|
||||||
GetMutatingWebhook() (*v1.MutatingWebhook, bool)
|
GetMutatingWebhook() (*v1.MutatingWebhook, bool)
|
||||||
// GetValidatingWebhook if the accessor contains a ValidatingWebhook, returns it and true, else returns false.
|
// GetValidatingWebhook if the accessor contains a ValidatingWebhook, returns it and true, else returns false.
|
||||||
|
|
@ -94,6 +104,9 @@ type mutatingWebhookAccessor struct {
|
||||||
initClient sync.Once
|
initClient sync.Once
|
||||||
client *rest.RESTClient
|
client *rest.RESTClient
|
||||||
clientErr error
|
clientErr error
|
||||||
|
|
||||||
|
compileMatcher sync.Once
|
||||||
|
compiledMatcher matchconditions.Matcher
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mutatingWebhookAccessor) GetUID() string {
|
func (m *mutatingWebhookAccessor) GetUID() string {
|
||||||
|
|
@ -111,6 +124,28 @@ func (m *mutatingWebhookAccessor) GetRESTClient(clientManager *webhookutil.Clien
|
||||||
return m.client, m.clientErr
|
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) {
|
func (m *mutatingWebhookAccessor) GetParsedNamespaceSelector() (labels.Selector, error) {
|
||||||
m.initNamespaceSelector.Do(func() {
|
m.initNamespaceSelector.Do(func() {
|
||||||
m.namespaceSelector, m.namespaceSelectorErr = metav1.LabelSelectorAsSelector(m.NamespaceSelector)
|
m.namespaceSelector, m.namespaceSelectorErr = metav1.LabelSelectorAsSelector(m.NamespaceSelector)
|
||||||
|
|
@ -165,6 +200,10 @@ func (m *mutatingWebhookAccessor) GetAdmissionReviewVersions() []string {
|
||||||
return m.AdmissionReviewVersions
|
return m.AdmissionReviewVersions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *mutatingWebhookAccessor) GetMatchConditions() []v1.MatchCondition {
|
||||||
|
return m.MatchConditions
|
||||||
|
}
|
||||||
|
|
||||||
func (m *mutatingWebhookAccessor) GetMutatingWebhook() (*v1.MutatingWebhook, bool) {
|
func (m *mutatingWebhookAccessor) GetMutatingWebhook() (*v1.MutatingWebhook, bool) {
|
||||||
return m.MutatingWebhook, true
|
return m.MutatingWebhook, true
|
||||||
}
|
}
|
||||||
|
|
@ -194,6 +233,9 @@ type validatingWebhookAccessor struct {
|
||||||
initClient sync.Once
|
initClient sync.Once
|
||||||
client *rest.RESTClient
|
client *rest.RESTClient
|
||||||
clientErr error
|
clientErr error
|
||||||
|
|
||||||
|
compileMatcher sync.Once
|
||||||
|
compiledMatcher matchconditions.Matcher
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *validatingWebhookAccessor) GetUID() string {
|
func (v *validatingWebhookAccessor) GetUID() string {
|
||||||
|
|
@ -211,6 +253,27 @@ func (v *validatingWebhookAccessor) GetRESTClient(clientManager *webhookutil.Cli
|
||||||
return v.client, v.clientErr
|
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) {
|
func (v *validatingWebhookAccessor) GetParsedNamespaceSelector() (labels.Selector, error) {
|
||||||
v.initNamespaceSelector.Do(func() {
|
v.initNamespaceSelector.Do(func() {
|
||||||
v.namespaceSelector, v.namespaceSelectorErr = metav1.LabelSelectorAsSelector(v.NamespaceSelector)
|
v.namespaceSelector, v.namespaceSelectorErr = metav1.LabelSelectorAsSelector(v.NamespaceSelector)
|
||||||
|
|
@ -265,6 +328,10 @@ func (v *validatingWebhookAccessor) GetAdmissionReviewVersions() []string {
|
||||||
return v.AdmissionReviewVersions
|
return v.AdmissionReviewVersions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *validatingWebhookAccessor) GetMatchConditions() []v1.MatchCondition {
|
||||||
|
return v.MatchConditions
|
||||||
|
}
|
||||||
|
|
||||||
func (v *validatingWebhookAccessor) GetMutatingWebhook() (*v1.MutatingWebhook, bool) {
|
func (v *validatingWebhookAccessor) GetMutatingWebhook() (*v1.MutatingWebhook, bool) {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,7 @@ func TestMutatingWebhookAccessor(t *testing.T) {
|
||||||
SideEffects: accessor.GetSideEffects(),
|
SideEffects: accessor.GetSideEffects(),
|
||||||
TimeoutSeconds: accessor.GetTimeoutSeconds(),
|
TimeoutSeconds: accessor.GetTimeoutSeconds(),
|
||||||
AdmissionReviewVersions: accessor.GetAdmissionReviewVersions(),
|
AdmissionReviewVersions: accessor.GetAdmissionReviewVersions(),
|
||||||
|
MatchConditions: accessor.GetMatchConditions(),
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(orig, copy) {
|
if !reflect.DeepEqual(orig, copy) {
|
||||||
t.Errorf("expected mutatingWebhook to round trip through WebhookAccessor, diff:\n%s", diff.ObjectReflectDiff(orig, copy))
|
t.Errorf("expected mutatingWebhook to round trip through WebhookAccessor, diff:\n%s", diff.ObjectReflectDiff(orig, copy))
|
||||||
|
|
@ -102,6 +103,7 @@ func TestValidatingWebhookAccessor(t *testing.T) {
|
||||||
SideEffects: accessor.GetSideEffects(),
|
SideEffects: accessor.GetSideEffects(),
|
||||||
TimeoutSeconds: accessor.GetTimeoutSeconds(),
|
TimeoutSeconds: accessor.GetTimeoutSeconds(),
|
||||||
AdmissionReviewVersions: accessor.GetAdmissionReviewVersions(),
|
AdmissionReviewVersions: accessor.GetAdmissionReviewVersions(),
|
||||||
|
MatchConditions: accessor.GetMatchConditions(),
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(orig, copy) {
|
if !reflect.DeepEqual(orig, copy) {
|
||||||
t.Errorf("expected validatingWebhook to round trip through WebhookAccessor, diff:\n%s", diff.ObjectReflectDiff(orig, copy))
|
t.Errorf("expected validatingWebhook to round trip through WebhookAccessor, diff:\n%s", diff.ObjectReflectDiff(orig, copy))
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,10 @@ import (
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/webhook"
|
"k8s.io/apiserver/pkg/admission/plugin/webhook"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type VersionedAttributeAccessor interface {
|
||||||
|
VersionedAttribute(gvk schema.GroupVersionKind) (*admission.VersionedAttributes, error)
|
||||||
|
}
|
||||||
|
|
||||||
// Source can list dynamic webhook plugins.
|
// Source can list dynamic webhook plugins.
|
||||||
type Source interface {
|
type Source interface {
|
||||||
Webhooks() []webhook.WebhookAccessor
|
Webhooks() []webhook.WebhookAccessor
|
||||||
|
|
|
||||||
|
|
@ -23,19 +23,22 @@ import (
|
||||||
|
|
||||||
admissionv1 "k8s.io/api/admission/v1"
|
admissionv1 "k8s.io/api/admission/v1"
|
||||||
admissionv1beta1 "k8s.io/api/admission/v1beta1"
|
admissionv1beta1 "k8s.io/api/admission/v1beta1"
|
||||||
"k8s.io/api/admissionregistration/v1"
|
v1 "k8s.io/api/admissionregistration/v1"
|
||||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/apiserver/pkg/admission"
|
"k8s.io/apiserver/pkg/admission"
|
||||||
genericadmissioninit "k8s.io/apiserver/pkg/admission/initializer"
|
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"
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/config"
|
"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/namespace"
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/object"
|
"k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/object"
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/rules"
|
"k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/rules"
|
||||||
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
webhookutil "k8s.io/apiserver/pkg/util/webhook"
|
webhookutil "k8s.io/apiserver/pkg/util/webhook"
|
||||||
"k8s.io/client-go/informers"
|
"k8s.io/client-go/informers"
|
||||||
clientset "k8s.io/client-go/kubernetes"
|
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.
|
// 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
|
namespaceMatcher *namespace.Matcher
|
||||||
objectMatcher *object.Matcher
|
objectMatcher *object.Matcher
|
||||||
dispatcher Dispatcher
|
dispatcher Dispatcher
|
||||||
|
filterCompiler cel.FilterCompiler
|
||||||
|
authorizer authorizer.Authorizer
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
@ -92,6 +97,7 @@ func NewWebhook(handler *admission.Handler, configFile io.Reader, sourceFactory
|
||||||
namespaceMatcher: &namespace.Matcher{},
|
namespaceMatcher: &namespace.Matcher{},
|
||||||
objectMatcher: &object.Matcher{},
|
objectMatcher: &object.Matcher{},
|
||||||
dispatcher: dispatcherFactory(&cm),
|
dispatcher: dispatcherFactory(&cm),
|
||||||
|
filterCompiler: cel.NewFilterCompiler(),
|
||||||
}, nil
|
}, 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.
|
// ValidateInitialization implements the InitializationValidator interface.
|
||||||
func (a *Webhook) ValidateInitialization() error {
|
func (a *Webhook) ValidateInitialization() error {
|
||||||
if a.hookSource == nil {
|
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,
|
// 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.
|
// 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)
|
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.
|
// 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 {
|
if !matches && matchNsErr == nil {
|
||||||
|
|
@ -207,6 +217,25 @@ func (a *Webhook) ShouldCallHook(h webhook.WebhookAccessor, attr admission.Attri
|
||||||
return nil, matchObjErr
|
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
|
return invocation, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,21 +17,26 @@ limitations under the License.
|
||||||
package generic
|
package generic
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
v1 "k8s.io/api/admissionregistration/v1"
|
v1 "k8s.io/api/admissionregistration/v1"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
"k8s.io/apimachinery/pkg/api/errors"
|
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/labels"
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/apiserver/pkg/admission"
|
"k8s.io/apiserver/pkg/admission"
|
||||||
|
"k8s.io/apiserver/pkg/admission/plugin/cel"
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/webhook"
|
"k8s.io/apiserver/pkg/admission/plugin/webhook"
|
||||||
|
"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/namespace"
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/object"
|
"k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/object"
|
||||||
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
)
|
)
|
||||||
|
|
||||||
func gvr(group, version, resource string) schema.GroupVersionResource {
|
func gvr(group, version, resource string) schema.GroupVersionResource {
|
||||||
|
|
@ -42,8 +47,55 @@ func gvk(group, version, kind string) schema.GroupVersionKind {
|
||||||
return schema.GroupVersionKind{Group: group, Version: version, Kind: kind}
|
return schema.GroupVersionKind{Group: group, Version: version, Kind: kind}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _ matchconditions.Matcher = &fakeMatcher{}
|
||||||
|
|
||||||
|
type fakeMatcher struct {
|
||||||
|
throwError error
|
||||||
|
matchResult bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeMatcher) Match(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object) matchconditions.MatchResult {
|
||||||
|
if f.throwError != nil {
|
||||||
|
return matchconditions.MatchResult{
|
||||||
|
Matches: true,
|
||||||
|
FailedConditionName: "",
|
||||||
|
Error: f.throwError,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matchconditions.MatchResult{
|
||||||
|
Matches: f.matchResult,
|
||||||
|
FailedConditionName: "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ webhook.WebhookAccessor = &fakeWebhookAccessor{}
|
||||||
|
|
||||||
|
type fakeWebhookAccessor struct {
|
||||||
|
webhook.WebhookAccessor
|
||||||
|
throwError error
|
||||||
|
matchResult bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeWebhookAccessor) GetCompiledMatcher(compiler cel.FilterCompiler, authorizer authorizer.Authorizer) matchconditions.Matcher {
|
||||||
|
return &fakeMatcher{
|
||||||
|
throwError: f.throwError,
|
||||||
|
matchResult: f.matchResult,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ VersionedAttributeAccessor = &fakeVersionedAttributeAccessor{}
|
||||||
|
|
||||||
|
type fakeVersionedAttributeAccessor struct{}
|
||||||
|
|
||||||
|
func (v *fakeVersionedAttributeAccessor) VersionedAttribute(gvk schema.GroupVersionKind) (*admission.VersionedAttributes, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
func TestShouldCallHook(t *testing.T) {
|
func TestShouldCallHook(t *testing.T) {
|
||||||
a := &Webhook{namespaceMatcher: &namespace.Matcher{}, objectMatcher: &object.Matcher{}}
|
a := &Webhook{
|
||||||
|
namespaceMatcher: &namespace.Matcher{},
|
||||||
|
objectMatcher: &object.Matcher{},
|
||||||
|
}
|
||||||
|
|
||||||
allScopes := v1.AllScopes
|
allScopes := v1.AllScopes
|
||||||
exactMatch := v1.Exact
|
exactMatch := v1.Exact
|
||||||
|
|
@ -83,12 +135,15 @@ func TestShouldCallHook(t *testing.T) {
|
||||||
expectCallResource schema.GroupVersionResource
|
expectCallResource schema.GroupVersionResource
|
||||||
expectCallSubresource string
|
expectCallSubresource string
|
||||||
expectCallKind schema.GroupVersionKind
|
expectCallKind schema.GroupVersionKind
|
||||||
|
matchError error
|
||||||
|
matchResult bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "no rules (just write)",
|
name: "no rules (just write)",
|
||||||
webhook: &v1.ValidatingWebhook{NamespaceSelector: &metav1.LabelSelector{}, Rules: []v1.RuleWithOperations{}},
|
webhook: &v1.ValidatingWebhook{NamespaceSelector: &metav1.LabelSelector{}, Rules: []v1.RuleWithOperations{}},
|
||||||
attrs: admission.NewAttributesRecord(nil, nil, gvk("apps", "v1", "Deployment"), "ns", "name", gvr("apps", "v1", "deployments"), "", admission.Create, &metav1.CreateOptions{}, false, nil),
|
attrs: admission.NewAttributesRecord(nil, nil, gvk("apps", "v1", "Deployment"), "ns", "name", gvr("apps", "v1", "deployments"), "", admission.Create, &metav1.CreateOptions{}, false, nil),
|
||||||
expectCall: false,
|
expectCall: false,
|
||||||
|
matchResult: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid kind lookup",
|
name: "invalid kind lookup",
|
||||||
|
|
@ -100,9 +155,10 @@ func TestShouldCallHook(t *testing.T) {
|
||||||
Operations: []v1.OperationType{"*"},
|
Operations: []v1.OperationType{"*"},
|
||||||
Rule: v1.Rule{APIGroups: []string{"example.com"}, APIVersions: []string{"v1"}, Resources: []string{"widgets"}, Scope: &allScopes},
|
Rule: v1.Rule{APIGroups: []string{"example.com"}, APIVersions: []string{"v1"}, Resources: []string{"widgets"}, Scope: &allScopes},
|
||||||
}}},
|
}}},
|
||||||
attrs: admission.NewAttributesRecord(nil, nil, gvk("example.com", "v2", "Widget"), "ns", "name", gvr("example.com", "v2", "widgets"), "", admission.Create, &metav1.CreateOptions{}, false, nil),
|
attrs: admission.NewAttributesRecord(nil, nil, gvk("example.com", "v2", "Widget"), "ns", "name", gvr("example.com", "v2", "widgets"), "", admission.Create, &metav1.CreateOptions{}, false, nil),
|
||||||
expectCall: false,
|
expectCall: false,
|
||||||
expectErr: "unknown kind",
|
expectErr: "unknown kind",
|
||||||
|
matchResult: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "wildcard rule, match as requested",
|
name: "wildcard rule, match as requested",
|
||||||
|
|
@ -118,6 +174,7 @@ func TestShouldCallHook(t *testing.T) {
|
||||||
expectCallKind: gvk("apps", "v1", "Deployment"),
|
expectCallKind: gvk("apps", "v1", "Deployment"),
|
||||||
expectCallResource: gvr("apps", "v1", "deployments"),
|
expectCallResource: gvr("apps", "v1", "deployments"),
|
||||||
expectCallSubresource: "",
|
expectCallSubresource: "",
|
||||||
|
matchResult: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "specific rules, prefer exact match",
|
name: "specific rules, prefer exact match",
|
||||||
|
|
@ -139,6 +196,7 @@ func TestShouldCallHook(t *testing.T) {
|
||||||
expectCallKind: gvk("apps", "v1", "Deployment"),
|
expectCallKind: gvk("apps", "v1", "Deployment"),
|
||||||
expectCallResource: gvr("apps", "v1", "deployments"),
|
expectCallResource: gvr("apps", "v1", "deployments"),
|
||||||
expectCallSubresource: "",
|
expectCallSubresource: "",
|
||||||
|
matchResult: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "specific rules, match miss",
|
name: "specific rules, match miss",
|
||||||
|
|
@ -152,8 +210,9 @@ func TestShouldCallHook(t *testing.T) {
|
||||||
Operations: []v1.OperationType{"*"},
|
Operations: []v1.OperationType{"*"},
|
||||||
Rule: v1.Rule{APIGroups: []string{"apps"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments"}, Scope: &allScopes},
|
Rule: v1.Rule{APIGroups: []string{"apps"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments"}, Scope: &allScopes},
|
||||||
}}},
|
}}},
|
||||||
attrs: admission.NewAttributesRecord(nil, nil, gvk("apps", "v1", "Deployment"), "ns", "name", gvr("apps", "v1", "deployments"), "", admission.Create, &metav1.CreateOptions{}, false, nil),
|
attrs: admission.NewAttributesRecord(nil, nil, gvk("apps", "v1", "Deployment"), "ns", "name", gvr("apps", "v1", "deployments"), "", admission.Create, &metav1.CreateOptions{}, false, nil),
|
||||||
expectCall: false,
|
expectCall: false,
|
||||||
|
matchResult: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "specific rules, exact match miss",
|
name: "specific rules, exact match miss",
|
||||||
|
|
@ -168,8 +227,9 @@ func TestShouldCallHook(t *testing.T) {
|
||||||
Operations: []v1.OperationType{"*"},
|
Operations: []v1.OperationType{"*"},
|
||||||
Rule: v1.Rule{APIGroups: []string{"apps"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments"}, Scope: &allScopes},
|
Rule: v1.Rule{APIGroups: []string{"apps"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments"}, Scope: &allScopes},
|
||||||
}}},
|
}}},
|
||||||
attrs: admission.NewAttributesRecord(nil, nil, gvk("apps", "v1", "Deployment"), "ns", "name", gvr("apps", "v1", "deployments"), "", admission.Create, &metav1.CreateOptions{}, false, nil),
|
attrs: admission.NewAttributesRecord(nil, nil, gvk("apps", "v1", "Deployment"), "ns", "name", gvr("apps", "v1", "deployments"), "", admission.Create, &metav1.CreateOptions{}, false, nil),
|
||||||
expectCall: false,
|
expectCall: false,
|
||||||
|
matchResult: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "specific rules, equivalent match, prefer extensions",
|
name: "specific rules, equivalent match, prefer extensions",
|
||||||
|
|
@ -189,6 +249,7 @@ func TestShouldCallHook(t *testing.T) {
|
||||||
expectCallKind: gvk("extensions", "v1beta1", "Deployment"),
|
expectCallKind: gvk("extensions", "v1beta1", "Deployment"),
|
||||||
expectCallResource: gvr("extensions", "v1beta1", "deployments"),
|
expectCallResource: gvr("extensions", "v1beta1", "deployments"),
|
||||||
expectCallSubresource: "",
|
expectCallSubresource: "",
|
||||||
|
matchResult: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "specific rules, equivalent match, prefer apps",
|
name: "specific rules, equivalent match, prefer apps",
|
||||||
|
|
@ -208,6 +269,7 @@ func TestShouldCallHook(t *testing.T) {
|
||||||
expectCallKind: gvk("apps", "v1beta1", "Deployment"),
|
expectCallKind: gvk("apps", "v1beta1", "Deployment"),
|
||||||
expectCallResource: gvr("apps", "v1beta1", "deployments"),
|
expectCallResource: gvr("apps", "v1beta1", "deployments"),
|
||||||
expectCallSubresource: "",
|
expectCallSubresource: "",
|
||||||
|
matchResult: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
@ -230,6 +292,7 @@ func TestShouldCallHook(t *testing.T) {
|
||||||
expectCallKind: gvk("autoscaling", "v1", "Scale"),
|
expectCallKind: gvk("autoscaling", "v1", "Scale"),
|
||||||
expectCallResource: gvr("apps", "v1", "deployments"),
|
expectCallResource: gvr("apps", "v1", "deployments"),
|
||||||
expectCallSubresource: "scale",
|
expectCallSubresource: "scale",
|
||||||
|
matchResult: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "specific rules, subresource match miss",
|
name: "specific rules, subresource match miss",
|
||||||
|
|
@ -243,8 +306,9 @@ func TestShouldCallHook(t *testing.T) {
|
||||||
Operations: []v1.OperationType{"*"},
|
Operations: []v1.OperationType{"*"},
|
||||||
Rule: v1.Rule{APIGroups: []string{"apps"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes},
|
Rule: v1.Rule{APIGroups: []string{"apps"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes},
|
||||||
}}},
|
}}},
|
||||||
attrs: admission.NewAttributesRecord(nil, nil, gvk("autoscaling", "v1", "Scale"), "ns", "name", gvr("apps", "v1", "deployments"), "scale", admission.Create, &metav1.CreateOptions{}, false, nil),
|
attrs: admission.NewAttributesRecord(nil, nil, gvk("autoscaling", "v1", "Scale"), "ns", "name", gvr("apps", "v1", "deployments"), "scale", admission.Create, &metav1.CreateOptions{}, false, nil),
|
||||||
expectCall: false,
|
expectCall: false,
|
||||||
|
matchResult: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "specific rules, subresource exact match miss",
|
name: "specific rules, subresource exact match miss",
|
||||||
|
|
@ -259,8 +323,9 @@ func TestShouldCallHook(t *testing.T) {
|
||||||
Operations: []v1.OperationType{"*"},
|
Operations: []v1.OperationType{"*"},
|
||||||
Rule: v1.Rule{APIGroups: []string{"apps"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes},
|
Rule: v1.Rule{APIGroups: []string{"apps"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes},
|
||||||
}}},
|
}}},
|
||||||
attrs: admission.NewAttributesRecord(nil, nil, gvk("autoscaling", "v1", "Scale"), "ns", "name", gvr("apps", "v1", "deployments"), "scale", admission.Create, &metav1.CreateOptions{}, false, nil),
|
attrs: admission.NewAttributesRecord(nil, nil, gvk("autoscaling", "v1", "Scale"), "ns", "name", gvr("apps", "v1", "deployments"), "scale", admission.Create, &metav1.CreateOptions{}, false, nil),
|
||||||
expectCall: false,
|
expectCall: false,
|
||||||
|
matchResult: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "specific rules, subresource equivalent match, prefer extensions",
|
name: "specific rules, subresource equivalent match, prefer extensions",
|
||||||
|
|
@ -280,6 +345,7 @@ func TestShouldCallHook(t *testing.T) {
|
||||||
expectCallKind: gvk("extensions", "v1beta1", "Scale"),
|
expectCallKind: gvk("extensions", "v1beta1", "Scale"),
|
||||||
expectCallResource: gvr("extensions", "v1beta1", "deployments"),
|
expectCallResource: gvr("extensions", "v1beta1", "deployments"),
|
||||||
expectCallSubresource: "scale",
|
expectCallSubresource: "scale",
|
||||||
|
matchResult: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "specific rules, subresource equivalent match, prefer apps",
|
name: "specific rules, subresource equivalent match, prefer apps",
|
||||||
|
|
@ -299,12 +365,86 @@ func TestShouldCallHook(t *testing.T) {
|
||||||
expectCallKind: gvk("apps", "v1beta1", "Scale"),
|
expectCallKind: gvk("apps", "v1beta1", "Scale"),
|
||||||
expectCallResource: gvr("apps", "v1beta1", "deployments"),
|
expectCallResource: gvr("apps", "v1beta1", "deployments"),
|
||||||
expectCallSubresource: "scale",
|
expectCallSubresource: "scale",
|
||||||
|
matchResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wildcard rule, match conditions also match",
|
||||||
|
webhook: &v1.ValidatingWebhook{
|
||||||
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
|
Rules: []v1.RuleWithOperations{{
|
||||||
|
Operations: []v1.OperationType{"*"},
|
||||||
|
Rule: v1.Rule{APIGroups: []string{"*"}, APIVersions: []string{"*"}, Resources: []string{"*"}, Scope: &allScopes},
|
||||||
|
}},
|
||||||
|
MatchConditions: []v1.MatchCondition{
|
||||||
|
{
|
||||||
|
Name: "test1",
|
||||||
|
Expression: "test expression",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
attrs: admission.NewAttributesRecord(nil, nil, gvk("apps", "v1", "Deployment"), "ns", "name", gvr("apps", "v1", "deployments"), "", admission.Create, &metav1.CreateOptions{}, false, nil),
|
||||||
|
expectCall: true,
|
||||||
|
expectCallKind: gvk("apps", "v1", "Deployment"),
|
||||||
|
expectCallResource: gvr("apps", "v1", "deployments"),
|
||||||
|
expectCallSubresource: "",
|
||||||
|
matchResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wildcard rule, match conditions do not match",
|
||||||
|
webhook: &v1.ValidatingWebhook{
|
||||||
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
|
Rules: []v1.RuleWithOperations{{
|
||||||
|
Operations: []v1.OperationType{"*"},
|
||||||
|
Rule: v1.Rule{APIGroups: []string{"*"}, APIVersions: []string{"*"}, Resources: []string{"*"}, Scope: &allScopes},
|
||||||
|
}},
|
||||||
|
MatchConditions: []v1.MatchCondition{
|
||||||
|
{
|
||||||
|
Name: "test1",
|
||||||
|
Expression: "test expression",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
attrs: admission.NewAttributesRecord(nil, nil, gvk("apps", "v1", "Deployment"), "ns", "name", gvr("apps", "v1", "deployments"), "", admission.Create, &metav1.CreateOptions{}, false, nil),
|
||||||
|
expectCall: false,
|
||||||
|
matchResult: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wildcard rule, match conditions error",
|
||||||
|
webhook: &v1.ValidatingWebhook{
|
||||||
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
|
Rules: []v1.RuleWithOperations{{
|
||||||
|
Operations: []v1.OperationType{"*"},
|
||||||
|
Rule: v1.Rule{APIGroups: []string{"*"}, APIVersions: []string{"*"}, Resources: []string{"*"}, Scope: &allScopes},
|
||||||
|
}},
|
||||||
|
MatchConditions: []v1.MatchCondition{
|
||||||
|
{
|
||||||
|
Name: "test1",
|
||||||
|
Expression: "test expression",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
attrs: admission.NewAttributesRecord(nil, nil, gvk("apps", "v1", "Deployment"), "ns", "name", gvr("apps", "v1", "deployments"), "", admission.Create, &metav1.CreateOptions{}, false, nil),
|
||||||
|
expectCall: false,
|
||||||
|
expectErr: "deployments.apps \"name\" is forbidden: test error",
|
||||||
|
expectCallKind: gvk("apps", "v1", "Deployment"),
|
||||||
|
expectCallResource: gvr("apps", "v1", "deployments"),
|
||||||
|
expectCallSubresource: "",
|
||||||
|
matchError: errors.New("test error"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, testcase := range testcases {
|
for i, testcase := range testcases {
|
||||||
t.Run(testcase.name, func(t *testing.T) {
|
t.Run(testcase.name, func(t *testing.T) {
|
||||||
invocation, err := a.ShouldCallHook(webhook.NewValidatingWebhookAccessor(fmt.Sprintf("webhook-%d", i), fmt.Sprintf("webhook-cfg-%d", i), testcase.webhook), testcase.attrs, interfaces)
|
fakeWebhook := &fakeWebhookAccessor{
|
||||||
|
WebhookAccessor: webhook.NewValidatingWebhookAccessor(fmt.Sprintf("webhook-%d", i), fmt.Sprintf("webhook-cfg-%d", i), testcase.webhook),
|
||||||
|
matchResult: testcase.matchResult,
|
||||||
|
throwError: testcase.matchError,
|
||||||
|
}
|
||||||
|
|
||||||
|
invocation, err := a.ShouldCallHook(context.TODO(), fakeWebhook, testcase.attrs, interfaces, &fakeVersionedAttributeAccessor{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if len(testcase.expectErr) == 0 {
|
if len(testcase.expectErr) == 0 {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
|
@ -353,7 +493,7 @@ func (f fakeNamespaceLister) Get(name string) (*corev1.Namespace, error) {
|
||||||
if ok {
|
if ok {
|
||||||
return ns, nil
|
return ns, nil
|
||||||
}
|
}
|
||||||
return nil, errors.NewNotFound(corev1.Resource("namespaces"), name)
|
return nil, k8serrors.NewNotFound(corev1.Resource("namespaces"), name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkShouldCallHookWithComplexSelector(b *testing.B) {
|
func BenchmarkShouldCallHookWithComplexSelector(b *testing.B) {
|
||||||
|
|
@ -415,13 +555,16 @@ func BenchmarkShouldCallHookWithComplexSelector(b *testing.B) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
wbAccessor := webhook.NewValidatingWebhookAccessor("webhook", "webhook-cfg", wb)
|
wbAccessor := &fakeWebhookAccessor{
|
||||||
|
WebhookAccessor: webhook.NewValidatingWebhookAccessor("webhook", "webhook-cfg", wb),
|
||||||
|
matchResult: true,
|
||||||
|
}
|
||||||
attrs := admission.NewAttributesRecord(nil, nil, gvk("autoscaling", "v1", "Scale"), "ns", "name", gvr("apps", "v1", "deployments"), "scale", admission.Create, &metav1.CreateOptions{}, false, nil)
|
attrs := admission.NewAttributesRecord(nil, nil, gvk("autoscaling", "v1", "Scale"), "ns", "name", gvr("apps", "v1", "deployments"), "scale", admission.Create, &metav1.CreateOptions{}, false, nil)
|
||||||
interfaces := &admission.RuntimeObjectInterfaces{EquivalentResourceMapper: mapper}
|
interfaces := &admission.RuntimeObjectInterfaces{EquivalentResourceMapper: mapper}
|
||||||
a := &Webhook{namespaceMatcher: &namespace.Matcher{NamespaceLister: namespaceLister}, objectMatcher: &object.Matcher{}}
|
a := &Webhook{namespaceMatcher: &namespace.Matcher{NamespaceLister: namespaceLister}, objectMatcher: &object.Matcher{}}
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
a.ShouldCallHook(wbAccessor, attrs, interfaces)
|
a.ShouldCallHook(context.TODO(), wbAccessor, attrs, interfaces, nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -483,13 +626,16 @@ func BenchmarkShouldCallHookWithComplexRule(b *testing.B) {
|
||||||
wb.Rules = append(wb.Rules, rule)
|
wb.Rules = append(wb.Rules, rule)
|
||||||
}
|
}
|
||||||
|
|
||||||
wbAccessor := webhook.NewValidatingWebhookAccessor("webhook", "webhook-cfg", wb)
|
wbAccessor := &fakeWebhookAccessor{
|
||||||
|
WebhookAccessor: webhook.NewValidatingWebhookAccessor("webhook", "webhook-cfg", wb),
|
||||||
|
matchResult: true,
|
||||||
|
}
|
||||||
attrs := admission.NewAttributesRecord(nil, nil, gvk("autoscaling", "v1", "Scale"), "ns", "name", gvr("apps", "v1", "deployments"), "scale", admission.Create, &metav1.CreateOptions{}, false, nil)
|
attrs := admission.NewAttributesRecord(nil, nil, gvk("autoscaling", "v1", "Scale"), "ns", "name", gvr("apps", "v1", "deployments"), "scale", admission.Create, &metav1.CreateOptions{}, false, nil)
|
||||||
interfaces := &admission.RuntimeObjectInterfaces{EquivalentResourceMapper: mapper}
|
interfaces := &admission.RuntimeObjectInterfaces{EquivalentResourceMapper: mapper}
|
||||||
a := &Webhook{namespaceMatcher: &namespace.Matcher{NamespaceLister: namespaceLister}, objectMatcher: &object.Matcher{}}
|
a := &Webhook{namespaceMatcher: &namespace.Matcher{NamespaceLister: namespaceLister}, objectMatcher: &object.Matcher{}}
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
a.ShouldCallHook(wbAccessor, attrs, interfaces)
|
a.ShouldCallHook(context.TODO(), wbAccessor, attrs, interfaces, &fakeVersionedAttributeAccessor{})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -556,12 +702,15 @@ func BenchmarkShouldCallHookWithComplexSelectorAndRule(b *testing.B) {
|
||||||
wb.Rules = append(wb.Rules, rule)
|
wb.Rules = append(wb.Rules, rule)
|
||||||
}
|
}
|
||||||
|
|
||||||
wbAccessor := webhook.NewValidatingWebhookAccessor("webhook", "webhook-cfg", wb)
|
wbAccessor := &fakeWebhookAccessor{
|
||||||
|
WebhookAccessor: webhook.NewValidatingWebhookAccessor("webhook", "webhook-cfg", wb),
|
||||||
|
matchResult: true,
|
||||||
|
}
|
||||||
attrs := admission.NewAttributesRecord(nil, nil, gvk("autoscaling", "v1", "Scale"), "ns", "name", gvr("apps", "v1", "deployments"), "scale", admission.Create, &metav1.CreateOptions{}, false, nil)
|
attrs := admission.NewAttributesRecord(nil, nil, gvk("autoscaling", "v1", "Scale"), "ns", "name", gvr("apps", "v1", "deployments"), "scale", admission.Create, &metav1.CreateOptions{}, false, nil)
|
||||||
interfaces := &admission.RuntimeObjectInterfaces{EquivalentResourceMapper: mapper}
|
interfaces := &admission.RuntimeObjectInterfaces{EquivalentResourceMapper: mapper}
|
||||||
a := &Webhook{namespaceMatcher: &namespace.Matcher{NamespaceLister: namespaceLister}, objectMatcher: &object.Matcher{}}
|
a := &Webhook{namespaceMatcher: &namespace.Matcher{NamespaceLister: namespaceLister}, objectMatcher: &object.Matcher{}}
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
a.ShouldCallHook(wbAccessor, attrs, interfaces)
|
a.ShouldCallHook(context.TODO(), wbAccessor, attrs, interfaces, nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,360 @@
|
||||||
|
/*
|
||||||
|
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"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
v1 "k8s.io/api/admissionregistration/v1"
|
||||||
|
|
||||||
|
celtypes "github.com/google/cel-go/common/types"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
admissionv1 "k8s.io/api/admission/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/apiserver/pkg/admission"
|
||||||
|
"k8s.io/apiserver/pkg/admission/plugin/cel"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ cel.Filter = &fakeCelFilter{}
|
||||||
|
|
||||||
|
type fakeCelFilter struct {
|
||||||
|
evaluations []cel.EvaluationResult
|
||||||
|
throwError bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeCelFilter) ForInput(context.Context, *admission.VersionedAttributes, *admissionv1.AdmissionRequest, cel.OptionalVariableBindings, int64) ([]cel.EvaluationResult, int64, error) {
|
||||||
|
if f.throwError {
|
||||||
|
return nil, 0, errors.New("test error")
|
||||||
|
}
|
||||||
|
return f.evaluations, 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeCelFilter) CompilationErrors() []error {
|
||||||
|
return []error{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMatch(t *testing.T) {
|
||||||
|
fakeAttr := admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{}, "default", "foo", schema.GroupVersionResource{}, "", admission.Create, nil, false, nil)
|
||||||
|
fakeVersionedAttr, _ := admission.NewVersionedAttributes(fakeAttr, schema.GroupVersionKind{}, nil)
|
||||||
|
fail := v1.Fail
|
||||||
|
ignore := v1.Ignore
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
evaluations []cel.EvaluationResult
|
||||||
|
throwError bool
|
||||||
|
shouldMatch bool
|
||||||
|
returnedName string
|
||||||
|
failPolicy *v1.FailurePolicyType
|
||||||
|
expectError string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "test single matches",
|
||||||
|
evaluations: []cel.EvaluationResult{
|
||||||
|
{
|
||||||
|
EvalResult: celtypes.True,
|
||||||
|
ExpressionAccessor: &MatchCondition{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
shouldMatch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test multiple match",
|
||||||
|
evaluations: []cel.EvaluationResult{
|
||||||
|
{
|
||||||
|
EvalResult: celtypes.True,
|
||||||
|
ExpressionAccessor: &MatchCondition{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
EvalResult: celtypes.True,
|
||||||
|
ExpressionAccessor: &MatchCondition{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
shouldMatch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test empty evals",
|
||||||
|
evaluations: []cel.EvaluationResult{},
|
||||||
|
shouldMatch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test single no match",
|
||||||
|
evaluations: []cel.EvaluationResult{
|
||||||
|
{
|
||||||
|
EvalResult: celtypes.False,
|
||||||
|
ExpressionAccessor: &MatchCondition{
|
||||||
|
Name: "test1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
shouldMatch: false,
|
||||||
|
returnedName: "test1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test multiple no match",
|
||||||
|
evaluations: []cel.EvaluationResult{
|
||||||
|
{
|
||||||
|
EvalResult: celtypes.False,
|
||||||
|
ExpressionAccessor: &MatchCondition{
|
||||||
|
Name: "test1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
EvalResult: celtypes.False,
|
||||||
|
ExpressionAccessor: &MatchCondition{
|
||||||
|
Name: "test2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
shouldMatch: false,
|
||||||
|
returnedName: "test1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test mixed with no match first",
|
||||||
|
evaluations: []cel.EvaluationResult{
|
||||||
|
{
|
||||||
|
EvalResult: celtypes.False,
|
||||||
|
ExpressionAccessor: &MatchCondition{
|
||||||
|
Name: "test1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
EvalResult: celtypes.True,
|
||||||
|
ExpressionAccessor: &MatchCondition{
|
||||||
|
Name: "test2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
shouldMatch: false,
|
||||||
|
returnedName: "test1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test mixed with no match last",
|
||||||
|
evaluations: []cel.EvaluationResult{
|
||||||
|
{
|
||||||
|
EvalResult: celtypes.True,
|
||||||
|
ExpressionAccessor: &MatchCondition{
|
||||||
|
Name: "test2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
EvalResult: celtypes.False,
|
||||||
|
ExpressionAccessor: &MatchCondition{
|
||||||
|
Name: "test1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
shouldMatch: false,
|
||||||
|
returnedName: "test1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test mixed with no match middle",
|
||||||
|
evaluations: []cel.EvaluationResult{
|
||||||
|
{
|
||||||
|
EvalResult: celtypes.True,
|
||||||
|
ExpressionAccessor: &MatchCondition{
|
||||||
|
Name: "test2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
EvalResult: celtypes.False,
|
||||||
|
ExpressionAccessor: &MatchCondition{
|
||||||
|
Name: "test1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
EvalResult: celtypes.True,
|
||||||
|
ExpressionAccessor: &MatchCondition{
|
||||||
|
Name: "test2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
shouldMatch: false,
|
||||||
|
returnedName: "test1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test error, no fail policy",
|
||||||
|
evaluations: []cel.EvaluationResult{
|
||||||
|
{
|
||||||
|
EvalResult: celtypes.True,
|
||||||
|
ExpressionAccessor: &MatchCondition{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
shouldMatch: true,
|
||||||
|
throwError: true,
|
||||||
|
expectError: "test error",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test error, fail policy fail",
|
||||||
|
evaluations: []cel.EvaluationResult{
|
||||||
|
{
|
||||||
|
EvalResult: celtypes.True,
|
||||||
|
ExpressionAccessor: &MatchCondition{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
failPolicy: &fail,
|
||||||
|
shouldMatch: true,
|
||||||
|
throwError: true,
|
||||||
|
expectError: "test error",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test error, fail policy ignore",
|
||||||
|
evaluations: []cel.EvaluationResult{
|
||||||
|
{
|
||||||
|
EvalResult: celtypes.True,
|
||||||
|
ExpressionAccessor: &MatchCondition{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
failPolicy: &ignore,
|
||||||
|
shouldMatch: false,
|
||||||
|
throwError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test mix of true, errors and false",
|
||||||
|
evaluations: []cel.EvaluationResult{
|
||||||
|
{
|
||||||
|
EvalResult: celtypes.True,
|
||||||
|
ExpressionAccessor: &MatchCondition{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Error: errors.New("test error"),
|
||||||
|
ExpressionAccessor: &MatchCondition{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
EvalResult: celtypes.False,
|
||||||
|
ExpressionAccessor: &MatchCondition{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
EvalResult: celtypes.True,
|
||||||
|
ExpressionAccessor: &MatchCondition{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Error: errors.New("test error"),
|
||||||
|
ExpressionAccessor: &MatchCondition{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
shouldMatch: false,
|
||||||
|
throwError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test mix of true, errors and fail policy not set",
|
||||||
|
evaluations: []cel.EvaluationResult{
|
||||||
|
{
|
||||||
|
EvalResult: celtypes.True,
|
||||||
|
ExpressionAccessor: &MatchCondition{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Error: errors.New("test error"),
|
||||||
|
ExpressionAccessor: &MatchCondition{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
EvalResult: celtypes.True,
|
||||||
|
ExpressionAccessor: &MatchCondition{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Error: errors.New("test error"),
|
||||||
|
ExpressionAccessor: &MatchCondition{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
shouldMatch: false,
|
||||||
|
throwError: false,
|
||||||
|
expectError: "test error",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test mix of true, errors and fail policy fail",
|
||||||
|
evaluations: []cel.EvaluationResult{
|
||||||
|
{
|
||||||
|
EvalResult: celtypes.True,
|
||||||
|
ExpressionAccessor: &MatchCondition{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Error: errors.New("test error"),
|
||||||
|
ExpressionAccessor: &MatchCondition{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
EvalResult: celtypes.True,
|
||||||
|
ExpressionAccessor: &MatchCondition{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Error: errors.New("test error"),
|
||||||
|
ExpressionAccessor: &MatchCondition{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
failPolicy: &fail,
|
||||||
|
shouldMatch: false,
|
||||||
|
throwError: false,
|
||||||
|
expectError: "test error",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test mix of true, errors and fail policy ignore",
|
||||||
|
evaluations: []cel.EvaluationResult{
|
||||||
|
{
|
||||||
|
EvalResult: celtypes.True,
|
||||||
|
ExpressionAccessor: &MatchCondition{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Error: errors.New("test error"),
|
||||||
|
ExpressionAccessor: &MatchCondition{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
EvalResult: celtypes.True,
|
||||||
|
ExpressionAccessor: &MatchCondition{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Error: errors.New("test error"),
|
||||||
|
ExpressionAccessor: &MatchCondition{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
failPolicy: &ignore,
|
||||||
|
shouldMatch: false,
|
||||||
|
throwError: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
m := NewMatcher(&fakeCelFilter{
|
||||||
|
evaluations: tc.evaluations,
|
||||||
|
throwError: tc.throwError,
|
||||||
|
}, nil, tc.failPolicy, "test", "testhook")
|
||||||
|
ctx := context.TODO()
|
||||||
|
matchResult := m.Match(ctx, fakeVersionedAttr, nil)
|
||||||
|
|
||||||
|
if matchResult.Error != nil {
|
||||||
|
if len(tc.expectError) == 0 {
|
||||||
|
t.Fatal(matchResult.Error)
|
||||||
|
}
|
||||||
|
if !strings.Contains(matchResult.Error.Error(), tc.expectError) {
|
||||||
|
t.Fatalf("expected error containing %q, got %s", tc.expectError, matchResult.Error.Error())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
} else if len(tc.expectError) > 0 {
|
||||||
|
t.Fatal("expected error but did not get one")
|
||||||
|
}
|
||||||
|
if len(tc.expectError) > 0 && matchResult.Error == nil {
|
||||||
|
t.Errorf("expected error thrown when filter errors")
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Equal(t, tc.shouldMatch, matchResult.Matches)
|
||||||
|
require.Equal(t, tc.returnedName, matchResult.FailedConditionName)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -26,14 +26,13 @@ import (
|
||||||
jsonpatch "github.com/evanphx/json-patch"
|
jsonpatch "github.com/evanphx/json-patch"
|
||||||
"go.opentelemetry.io/otel/attribute"
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
|
||||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
|
||||||
"k8s.io/klog/v2"
|
|
||||||
|
|
||||||
admissionv1 "k8s.io/api/admission/v1"
|
admissionv1 "k8s.io/api/admission/v1"
|
||||||
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
|
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
|
||||||
|
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/apimachinery/pkg/runtime/serializer/json"
|
"k8s.io/apimachinery/pkg/runtime/serializer/json"
|
||||||
utiljson "k8s.io/apimachinery/pkg/util/json"
|
utiljson "k8s.io/apimachinery/pkg/util/json"
|
||||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||||
|
|
@ -48,6 +47,7 @@ import (
|
||||||
webhookutil "k8s.io/apiserver/pkg/util/webhook"
|
webhookutil "k8s.io/apiserver/pkg/util/webhook"
|
||||||
"k8s.io/apiserver/pkg/warning"
|
"k8s.io/apiserver/pkg/warning"
|
||||||
"k8s.io/component-base/tracing"
|
"k8s.io/component-base/tracing"
|
||||||
|
"k8s.io/klog/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
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{}
|
var _ generic.Dispatcher = &mutatingDispatcher{}
|
||||||
|
|
||||||
func (a *mutatingDispatcher) Dispatch(ctx context.Context, attr admission.Attributes, o admission.ObjectInterfaces, hooks []webhook.WebhookAccessor) error {
|
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() {
|
defer func() {
|
||||||
webhookReinvokeCtx.SetLastWebhookInvocationOutput(attr.GetObject())
|
webhookReinvokeCtx.SetLastWebhookInvocationOutput(attr.GetObject())
|
||||||
}()
|
}()
|
||||||
var versionedAttr *admission.VersionedAttributes
|
v := &versionedAttributeAccessor{
|
||||||
|
attr: attr,
|
||||||
|
objectInterfaces: o,
|
||||||
|
}
|
||||||
for i, hook := range hooks {
|
for i, hook := range hooks {
|
||||||
attrForCheck := attr
|
attrForCheck := attr
|
||||||
if versionedAttr != nil {
|
if v.versionedAttr != nil {
|
||||||
attrForCheck = versionedAttr
|
attrForCheck = v.versionedAttr
|
||||||
}
|
}
|
||||||
invocation, statusErr := a.plugin.ShouldCallHook(hook, attrForCheck, o)
|
|
||||||
|
invocation, statusErr := a.plugin.ShouldCallHook(ctx, hook, attrForCheck, o, v)
|
||||||
if statusErr != nil {
|
if statusErr != nil {
|
||||||
return statusErr
|
return statusErr
|
||||||
}
|
}
|
||||||
if invocation == nil {
|
if invocation == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
hook, ok := invocation.Webhook.GetMutatingWebhook()
|
hook, ok := invocation.Webhook.GetMutatingWebhook()
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("mutating webhook dispatch requires v1.MutatingWebhook, but got %T", hook)
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if versionedAttr == nil {
|
versionedAttr, err := v.VersionedAttribute(invocation.Kind)
|
||||||
// First webhook, create versioned attributes
|
if err != nil {
|
||||||
var err error
|
return apierrors.NewInternalError(err)
|
||||||
if versionedAttr, err = admission.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 := admission.ConvertVersionedAttributes(versionedAttr, invocation.Kind, o); err != nil {
|
|
||||||
return apierrors.NewInternalError(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
t := time.Now()
|
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
|
// convert versionedAttr.VersionedObject to the internal version in the underlying admission.Attributes
|
||||||
if versionedAttr != nil && versionedAttr.VersionedObject != nil && versionedAttr.Dirty {
|
if v.versionedAttr != nil && v.versionedAttr.VersionedObject != nil && v.versionedAttr.Dirty {
|
||||||
return o.GetObjectConvertor().Convert(versionedAttr.VersionedObject, versionedAttr.Attributes.GetObject(), nil)
|
return o.GetObjectConvertor().Convert(v.versionedAttr.VersionedObject, v.versionedAttr.Attributes.GetObject(), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -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{}
|
var _ generic.Dispatcher = &validatingDispatcher{}
|
||||||
|
|
||||||
func (d *validatingDispatcher) Dispatch(ctx context.Context, attr admission.Attributes, o admission.ObjectInterfaces, hooks []webhook.WebhookAccessor) error {
|
func (d *validatingDispatcher) Dispatch(ctx context.Context, attr admission.Attributes, o admission.ObjectInterfaces, hooks []webhook.WebhookAccessor) error {
|
||||||
var relevantHooks []*generic.WebhookInvocation
|
var relevantHooks []*generic.WebhookInvocation
|
||||||
// Construct all the versions we need to call our webhooks
|
// Construct all the versions we need to call our webhooks
|
||||||
versionedAttrs := map[schema.GroupVersionKind]*admission.VersionedAttributes{}
|
versionedAttrAccessor := &versionedAttributeAccessor{
|
||||||
|
versionedAttrs: map[schema.GroupVersionKind]*admission.VersionedAttributes{},
|
||||||
|
attr: attr,
|
||||||
|
objectInterfaces: o,
|
||||||
|
}
|
||||||
for _, hook := range hooks {
|
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 {
|
if statusError != nil {
|
||||||
return statusError
|
return statusError
|
||||||
}
|
}
|
||||||
if invocation == nil {
|
if invocation == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
relevantHooks = append(relevantHooks, invocation)
|
relevantHooks = append(relevantHooks, invocation)
|
||||||
// If we already have this version, continue
|
// VersionedAttr result will be cached and reused later during parallel webhook calls
|
||||||
if _, ok := versionedAttrs[invocation.Kind]; ok {
|
_, err := versionedAttrAccessor.VersionedAttribute(invocation.Kind)
|
||||||
continue
|
|
||||||
}
|
|
||||||
versionedAttr, err := admission.NewVersionedAttributes(attr, invocation.Kind, o)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return apierrors.NewInternalError(err)
|
return apierrors.NewInternalError(err)
|
||||||
}
|
}
|
||||||
versionedAttrs[invocation.Kind] = versionedAttr
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(relevantHooks) == 0 {
|
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) {
|
go func(invocation *generic.WebhookInvocation, idx int) {
|
||||||
ignoreClientCallFailures := false
|
ignoreClientCallFailures := false
|
||||||
hookName := "unknown"
|
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
|
// 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.
|
// that is used by the second defer to report errors. The recovery and error reporting must be done first.
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,11 @@ const (
|
||||||
// current RuntimeCELCostBudget gives roughly 1 seconds for the validation
|
// current RuntimeCELCostBudget gives roughly 1 seconds for the validation
|
||||||
RuntimeCELCostBudget = 10000000
|
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
|
// CheckFrequency configures the number of iterations within a comprehension to evaluate
|
||||||
// before checking whether the function evaluation has been interrupted
|
// before checking whether the function evaluation has been interrupted
|
||||||
CheckFrequency = 100
|
CheckFrequency = 100
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,13 @@ const (
|
||||||
// of code conflicts because changes are more likely to be scattered
|
// of code conflicts because changes are more likely to be scattered
|
||||||
// across the file.
|
// across the file.
|
||||||
|
|
||||||
|
// owner: @ivelichkovich, @tallclair
|
||||||
|
// alpha: v1.27
|
||||||
|
// kep: https://kep.k8s.io/3716
|
||||||
|
//
|
||||||
|
// Enables usage of MatchConditions fields to use CEL expressions for matching on admission webhooks
|
||||||
|
AdmissionWebhookMatchConditions featuregate.Feature = "AdmissionWebhookMatchConditions"
|
||||||
|
|
||||||
// owner: @jefftree @alexzielenski
|
// owner: @jefftree @alexzielenski
|
||||||
// alpha: v1.26
|
// alpha: v1.26
|
||||||
//
|
//
|
||||||
|
|
@ -223,8 +230,11 @@ func init() {
|
||||||
// To add a new feature, define a key for it above and add it here. The features will be
|
// To add a new feature, define a key for it above and add it here. The features will be
|
||||||
// available throughout Kubernetes binaries.
|
// available throughout Kubernetes binaries.
|
||||||
var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{
|
var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{
|
||||||
|
|
||||||
AggregatedDiscoveryEndpoint: {Default: true, PreRelease: featuregate.Beta},
|
AggregatedDiscoveryEndpoint: {Default: true, PreRelease: featuregate.Beta},
|
||||||
|
|
||||||
|
AdmissionWebhookMatchConditions: {Default: false, PreRelease: featuregate.Alpha},
|
||||||
|
|
||||||
APIListChunking: {Default: true, PreRelease: featuregate.Beta},
|
APIListChunking: {Default: true, PreRelease: featuregate.Beta},
|
||||||
|
|
||||||
APIPriorityAndFairness: {Default: true, PreRelease: featuregate.Beta},
|
APIPriorityAndFairness: {Default: true, PreRelease: featuregate.Beta},
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue