1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2025-03-29 02:45:06 +00:00

feat: improve webhooks rules generation (#11419)

* feat: improve webhooks rules generation

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* iterate per rule

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* reduce rules

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* rework default operations

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* consider subresource

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* aggregate operations

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* sort rules

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* ephemeralcontainers

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* operations

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* aggregation

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* operations type

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* generate rules

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* nits

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* generate

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* all operations

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* collector changes

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* account for exclusions

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* unit tests

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* fix exclusions when no operations specified

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* unit tests

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

---------

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>
Co-authored-by: shuting <shuting@nirmata.com>
This commit is contained in:
Charles-Edouard Brétéché 2024-10-21 14:56:21 +02:00 committed by GitHub
parent 50006a3e66
commit 3580034fa1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 918 additions and 1157 deletions

View file

@ -33,7 +33,6 @@ import (
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/sets"
admissionregistrationv1informers "k8s.io/client-go/informers/admissionregistration/v1"
appsv1informers "k8s.io/client-go/informers/apps/v1"
@ -60,10 +59,6 @@ const (
IdleDeadline = tickerInterval * 10
maxRetries = 10
tickerInterval = 10 * time.Second
webhookCreate = "CREATE"
webhookUpdate = "UPDATE"
webhookDelete = "DELETE"
webhookConnect = "CONNECT"
)
var (
@ -82,6 +77,12 @@ var (
APIGroups: []string{"coordination.k8s.io"},
APIVersions: []string{"v1"},
}
createUpdateDelete = []kyvernov1.AdmissionOperation{kyvernov1.Create, kyvernov1.Update, kyvernov1.Delete}
allOperations = []kyvernov1.AdmissionOperation{kyvernov1.Create, kyvernov1.Update, kyvernov1.Delete, kyvernov1.Connect}
defaultOperations = map[bool][]kyvernov1.AdmissionOperation{
true: allOperations,
false: {kyvernov1.Create, kyvernov1.Update},
}
)
type controller struct {
@ -821,14 +822,12 @@ func (c *controller) buildResourceMutatingWebhookConfiguration(ctx context.Conte
ObjectMeta: objectMeta(config.MutatingWebhookConfigurationName, cfg.GetWebhookAnnotations(), cfg.GetWebhookLabels(), c.buildOwner()...),
Webhooks: []admissionregistrationv1.MutatingWebhook{},
}
var mapResourceToOpnType map[string][]admissionregistrationv1.OperationType
if c.watchdogCheck() {
webhookCfg := config.WebhookConfig{}
webhookCfgs := cfg.GetWebhooks()
if len(webhookCfgs) > 0 {
webhookCfg = webhookCfgs[0]
}
ignoreWebhook := newWebhook(c.defaultTimeout, ignore, cfg.GetMatchConditions())
failWebhook := newWebhook(c.defaultTimeout, fail, cfg.GetMatchConditions())
policies, err := c.getAllPolicies()
@ -842,40 +841,36 @@ func (c *controller) buildResourceMutatingWebhookConfiguration(ctx context.Conte
spec := p.GetSpec()
if spec.HasMutateStandard() || spec.HasVerifyImages() {
if spec.CustomWebhookMatchConditions() {
fineGrainedIgnore := newWebhookPerPolicy(c.defaultTimeout, ignore, cfg.GetMatchConditions(), p)
fineGrainedFail := newWebhookPerPolicy(c.defaultTimeout, fail, cfg.GetMatchConditions(), p)
if spec.GetFailurePolicy(ctx) == kyvernov1.Ignore {
fineGrainedIgnore := newWebhookPerPolicy(c.defaultTimeout, ignore, cfg.GetMatchConditions(), p)
c.mergeWebhook(fineGrainedIgnore, p, false)
fineGrainedIgnoreList = append(fineGrainedIgnoreList, fineGrainedIgnore)
} else {
fineGrainedFail := newWebhookPerPolicy(c.defaultTimeout, fail, cfg.GetMatchConditions(), p)
c.mergeWebhook(fineGrainedFail, p, false)
fineGrainedFailList = append(fineGrainedFailList, fineGrainedFail)
}
continue
}
if spec.GetFailurePolicy(ctx) == kyvernov1.Ignore {
c.mergeWebhook(ignoreWebhook, p, false)
} else {
c.mergeWebhook(failWebhook, p, false)
if spec.GetFailurePolicy(ctx) == kyvernov1.Ignore {
c.mergeWebhook(ignoreWebhook, p, false)
} else {
c.mergeWebhook(failWebhook, p, false)
}
}
rules := p.GetSpec().Rules
mapResourceToOpnType = addOpnForMutatingWebhookConf(rules, mapResourceToOpnType)
}
}
}
webhooks := []*webhook{ignoreWebhook, failWebhook}
webhooks = append(webhooks, fineGrainedIgnoreList...)
webhooks = append(webhooks, fineGrainedFailList...)
result.Webhooks = c.buildResourceMutatingWebhookRules(caBundle, webhookCfg, &noneOnDryRun, webhooks, mapResourceToOpnType)
result.Webhooks = c.buildResourceMutatingWebhookRules(caBundle, webhookCfg, &noneOnDryRun, webhooks)
} else {
c.recordPolicyState(config.MutatingWebhookConfigurationName)
}
return &result, nil
}
func (c *controller) buildResourceMutatingWebhookRules(caBundle []byte, webhookCfg config.WebhookConfig, sideEffects *admissionregistrationv1.SideEffectClass, webhooks []*webhook, mapResourceToOpnType map[string][]admissionregistrationv1.OperationType) []admissionregistrationv1.MutatingWebhook {
func (c *controller) buildResourceMutatingWebhookRules(caBundle []byte, webhookCfg config.WebhookConfig, sideEffects *admissionregistrationv1.SideEffectClass, webhooks []*webhook) []admissionregistrationv1.MutatingWebhook {
var mutatingWebhooks []admissionregistrationv1.MutatingWebhook //nolint:prealloc
objectSelector := webhookCfg.ObjectSelector
if objectSelector == nil {
@ -893,7 +888,7 @@ func (c *controller) buildResourceMutatingWebhookRules(caBundle []byte, webhookC
admissionregistrationv1.MutatingWebhook{
Name: name,
ClientConfig: c.clientConfig(caBundle, path),
Rules: webhook.buildRulesWithOperations(mapResourceToOpnType, []admissionregistrationv1.OperationType{"CREATE", "UPDATE"}),
Rules: webhook.buildRulesWithOperations(),
FailurePolicy: &failurePolicy,
SideEffects: sideEffects,
AdmissionReviewVersions: []string{"v1"},
@ -963,42 +958,11 @@ func (c *controller) buildDefaultResourceValidatingWebhookConfiguration(_ contex
nil
}
func addOpnForMutatingWebhookConf(rules []kyvernov1.Rule, mapResourceToOpnType map[string][]admissionregistrationv1.OperationType) map[string][]admissionregistrationv1.OperationType {
var mapResourceToOpn map[string]map[string]bool
for _, r := range rules {
if r.HasMutate() || r.HasVerifyImages() {
var resources []string
operationStatusMap := getOperationStatusMap()
operationStatusMap = computeOperationsForMutatingWebhookConf(r, operationStatusMap)
resources = computeResourcesOfRule(r)
for _, r := range resources {
mapResourceToOpn, mapResourceToOpnType = appendResource(r, mapResourceToOpn, operationStatusMap, mapResourceToOpnType)
}
}
}
return mapResourceToOpnType
}
func addOpnForValidatingWebhookConf(rules []kyvernov1.Rule, mapResourceToOpnType map[string][]admissionregistrationv1.OperationType) map[string][]admissionregistrationv1.OperationType {
var mapResourceToOpn map[string]map[string]bool
for _, r := range rules {
var resources []string
operationStatusMap := getOperationStatusMap()
operationStatusMap = computeOperationsForValidatingWebhookConf(r, operationStatusMap)
resources = computeResourcesOfRule(r)
for _, r := range resources {
mapResourceToOpn, mapResourceToOpnType = appendResource(r, mapResourceToOpn, operationStatusMap, mapResourceToOpnType)
}
}
return mapResourceToOpnType
}
func (c *controller) buildResourceValidatingWebhookConfiguration(ctx context.Context, cfg config.Configuration, caBundle []byte) (*admissionregistrationv1.ValidatingWebhookConfiguration, error) {
result := admissionregistrationv1.ValidatingWebhookConfiguration{
ObjectMeta: objectMeta(config.ValidatingWebhookConfigurationName, cfg.GetWebhookAnnotations(), cfg.GetWebhookLabels(), c.buildOwner()...),
Webhooks: []admissionregistrationv1.ValidatingWebhook{},
}
var mapResourceToOpnType map[string][]admissionregistrationv1.OperationType
if c.watchdogCheck() {
webhookCfg := config.WebhookConfig{}
webhookCfgs := cfg.GetWebhooks()
@ -1020,45 +984,40 @@ func (c *controller) buildResourceValidatingWebhookConfiguration(ctx context.Con
spec := p.GetSpec()
if spec.HasValidate() || spec.HasGenerate() || spec.HasMutateExisting() || spec.HasVerifyImageChecks() || spec.HasVerifyManifests() {
if spec.CustomWebhookMatchConditions() {
fineGrainedIgnore := newWebhookPerPolicy(c.defaultTimeout, ignore, cfg.GetMatchConditions(), p)
fineGrainedFail := newWebhookPerPolicy(c.defaultTimeout, fail, cfg.GetMatchConditions(), p)
if spec.GetFailurePolicy(ctx) == kyvernov1.Ignore {
fineGrainedIgnore := newWebhookPerPolicy(c.defaultTimeout, ignore, cfg.GetMatchConditions(), p)
c.mergeWebhook(fineGrainedIgnore, p, true)
fineGrainedIgnoreList = append(fineGrainedIgnoreList, fineGrainedIgnore)
} else {
fineGrainedFail := newWebhookPerPolicy(c.defaultTimeout, fail, cfg.GetMatchConditions(), p)
c.mergeWebhook(fineGrainedFail, p, true)
fineGrainedFailList = append(fineGrainedFailList, fineGrainedFail)
}
continue
}
if spec.GetFailurePolicy(ctx) == kyvernov1.Ignore {
c.mergeWebhook(ignoreWebhook, p, true)
} else {
c.mergeWebhook(failWebhook, p, true)
if spec.GetFailurePolicy(ctx) == kyvernov1.Ignore {
c.mergeWebhook(ignoreWebhook, p, true)
} else {
c.mergeWebhook(failWebhook, p, true)
}
}
}
}
rules := p.GetSpec().Rules
mapResourceToOpnType = addOpnForValidatingWebhookConf(rules, mapResourceToOpnType)
}
sideEffects := &none
if c.admissionReports {
sideEffects = &noneOnDryRun
}
webhooks := []*webhook{ignoreWebhook, failWebhook}
webhooks = append(webhooks, fineGrainedIgnoreList...)
webhooks = append(webhooks, fineGrainedFailList...)
result.Webhooks = c.buildResourceValidatingWebhookRules(caBundle, webhookCfg, sideEffects, webhooks, mapResourceToOpnType)
result.Webhooks = c.buildResourceValidatingWebhookRules(caBundle, webhookCfg, sideEffects, webhooks)
} else {
c.recordPolicyState(config.MutatingWebhookConfigurationName)
}
return &result, nil
}
func (c *controller) buildResourceValidatingWebhookRules(caBundle []byte, webhookCfg config.WebhookConfig, sideEffects *admissionregistrationv1.SideEffectClass, webhooks []*webhook, mapResourceToOpnType map[string][]admissionregistrationv1.OperationType) []admissionregistrationv1.ValidatingWebhook {
func (c *controller) buildResourceValidatingWebhookRules(caBundle []byte, webhookCfg config.WebhookConfig, sideEffects *admissionregistrationv1.SideEffectClass, webhooks []*webhook) []admissionregistrationv1.ValidatingWebhook {
var validatingWebhooks []admissionregistrationv1.ValidatingWebhook //nolint:prealloc
objectSelector := webhookCfg.ObjectSelector
if objectSelector == nil {
@ -1076,7 +1035,7 @@ func (c *controller) buildResourceValidatingWebhookRules(caBundle []byte, webhoo
admissionregistrationv1.ValidatingWebhook{
Name: name,
ClientConfig: c.clientConfig(caBundle, path),
Rules: webhook.buildRulesWithOperations(mapResourceToOpnType, []admissionregistrationv1.OperationType{"CREATE", "UPDATE", "DELETE", "CONNECT"}),
Rules: webhook.buildRulesWithOperations(),
FailurePolicy: &failurePolicy,
SideEffects: sideEffects,
AdmissionReviewVersions: []string{"v1"},
@ -1114,64 +1073,101 @@ func (c *controller) getLease() (*coordinationv1.Lease, error) {
return c.leaseLister.Leases(config.KyvernoNamespace()).Get("kyverno-health")
}
// GroupVersionResourceScope adds the resource scope to the GVR
type GroupVersionResourceScope struct {
schema.GroupVersionResource
Scope admissionregistrationv1.ScopeType
type groupVersionResourceSubresourceScope struct {
group string
version string
resource string
subresource string
scope admissionregistrationv1.ScopeType
}
// String puts / between group/version/resource and scope
func (gvs GroupVersionResourceScope) String() string {
return gvs.GroupVersion().String() + "/" + gvs.Resource + "/" + string(gvs.Scope)
type webhookConfig map[string]sets.Set[kyvernov1.AdmissionOperation]
func (w webhookConfig) add(kind string, ops ...kyvernov1.AdmissionOperation) {
if len(ops) != 0 {
if w[kind] == nil {
w[kind] = sets.New[kyvernov1.AdmissionOperation]()
}
w[kind].Insert(ops...)
}
}
func (w webhookConfig) merge(other webhookConfig) {
for key, value := range other {
if w[key] == nil {
w[key] = value
} else {
w[key] = w[key].Union(value)
}
}
}
// mergeWebhook merges the matching kinds of the policy to webhook.rule
func (c *controller) mergeWebhook(dst *webhook, policy kyvernov1.PolicyInterface, updateValidate bool) {
var matchedGVK []string
matchedGVK = append(matchedGVK, autogen.Default.GetAutogenKinds(policy)...)
for _, rule := range policy.GetSpec().Rules {
// matching kinds in generate policies need to be added to both webhook
matched := webhookConfig{}
for _, rule := range autogen.Default.ComputeRules(policy, "") {
// matching kinds in generate policies need to be added to both webhooks
if rule.HasGenerate() {
matchedGVK = append(matchedGVK, rule.MatchResources.GetKinds()...)
// all four operations including CONNECT are needed for generate.
// for example https://kyverno.io/policies/other/audit-event-on-exec/audit-event-on-exec/
matched.merge(collectResourceDescriptions(rule, allOperations...))
for _, g := range rule.Generation.ForEachGeneration {
if g.GeneratePattern.ResourceSpec.Kind != "" {
matchedGVK = append(matchedGVK, g.GeneratePattern.ResourceSpec.Kind)
matched.add(g.GeneratePattern.ResourceSpec.Kind, createUpdateDelete...)
} else {
matchedGVK = append(matchedGVK, g.GeneratePattern.CloneList.Kinds...)
for _, kind := range g.GeneratePattern.CloneList.Kinds {
matched.add(kind, createUpdateDelete...)
}
}
}
if rule.Generation.ResourceSpec.Kind != "" {
matchedGVK = append(matchedGVK, rule.Generation.ResourceSpec.Kind)
matched.add(rule.Generation.ResourceSpec.Kind, createUpdateDelete...)
} else {
matchedGVK = append(matchedGVK, rule.Generation.CloneList.Kinds...)
continue
for _, kind := range rule.Generation.CloneList.Kinds {
matched.add(kind, createUpdateDelete...)
}
}
}
if (updateValidate && rule.HasValidate() || rule.HasVerifyImageChecks()) ||
} else if (updateValidate && rule.HasValidate() || rule.HasVerifyImageChecks()) ||
(updateValidate && rule.HasMutateExisting()) ||
(!updateValidate && rule.HasMutateStandard()) ||
(!updateValidate && rule.HasVerifyImages()) || (!updateValidate && rule.HasVerifyManifests()) {
matchedGVK = append(matchedGVK, rule.MatchResources.GetKinds()...)
matched.merge(collectResourceDescriptions(rule, defaultOperations[updateValidate]...))
}
}
var gvrsList []GroupVersionResourceScope
for _, gvk := range matchedGVK {
for kind, ops := range matched {
var gvrsList []groupVersionResourceSubresourceScope
// NOTE: webhook stores GVR in its rules while policy stores GVK in its rules definition
group, version, kind, subresource := kubeutils.ParseKindSelector(gvk)
group, version, kind, subresource := kubeutils.ParseKindSelector(kind)
// if kind or group is `*` we use the scope of the policy
policyScope := admissionregistrationv1.AllScopes
if policy.IsNamespaced() {
policyScope = admissionregistrationv1.NamespacedScope
}
// if kind is `*` no need to lookup resources
if kind == "*" && subresource == "*" {
gvrsList = append(gvrsList, GroupVersionResourceScope{GroupVersionResource: schema.GroupVersionResource{Group: group, Version: version, Resource: "*/*"}, Scope: policyScope})
gvrsList = append(gvrsList, groupVersionResourceSubresourceScope{
group: group,
version: version,
resource: kind,
subresource: subresource,
scope: policyScope,
})
} else if kind == "*" && subresource == "" {
gvrsList = append(gvrsList, GroupVersionResourceScope{GroupVersionResource: schema.GroupVersionResource{Group: group, Version: version, Resource: "*"}, Scope: policyScope})
gvrsList = append(gvrsList, groupVersionResourceSubresourceScope{
group: group,
version: version,
resource: kind,
subresource: subresource,
scope: policyScope,
})
} else if kind == "*" && subresource != "" {
gvrsList = append(gvrsList, GroupVersionResourceScope{GroupVersionResource: schema.GroupVersionResource{Group: group, Version: version, Resource: "*/" + subresource}, Scope: policyScope})
gvrsList = append(gvrsList, groupVersionResourceSubresourceScope{
group: group,
version: version,
resource: kind,
subresource: subresource,
scope: policyScope,
})
} else {
gvrss, err := c.discoveryClient.FindResources(group, version, kind, subresource)
if err != nil {
@ -1183,14 +1179,19 @@ func (c *controller) mergeWebhook(dst *webhook, policy kyvernov1.PolicyInterface
if resource.Namespaced {
resourceScope = admissionregistrationv1.NamespacedScope
}
gvrsList = append(gvrsList, GroupVersionResourceScope{GroupVersionResource: gvrs.GroupVersion.WithResource(gvrs.ResourceSubresource()), Scope: resourceScope})
gvrsList = append(gvrsList, groupVersionResourceSubresourceScope{
group: gvrs.GroupVersion.Group,
version: gvrs.GroupVersion.Version,
resource: gvrs.Resource,
subresource: gvrs.SubResource,
scope: resourceScope,
})
}
}
for _, gvrs := range gvrsList {
dst.set(gvrs.group, gvrs.version, gvrs.resource, gvrs.subresource, gvrs.scope, ops.UnsortedList()...)
}
}
for _, gvrs := range gvrsList {
dst.set(gvrs)
}
spec := policy.GetSpec()
webhookTimeoutSeconds := spec.GetWebhookTimeoutSeconds()
if webhookTimeoutSeconds != nil {

View file

@ -1,443 +0,0 @@
package webhook
import (
"cmp"
"reflect"
"slices"
"sort"
"testing"
kyverno "github.com/kyverno/kyverno/api/kyverno/v1"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
)
func TestAddOperationsForValidatingWebhookConfMultiplePolicies(t *testing.T) {
testCases := []struct {
name string
policies []kyverno.ClusterPolicy
expectedResult map[string][]admissionregistrationv1.OperationType
}{
{
name: "test-1",
policies: []kyverno.ClusterPolicy{
{
Spec: kyverno.Spec{
Rules: []kyverno.Rule{
{
MatchResources: kyverno.MatchResources{
ResourceDescription: kyverno.ResourceDescription{
Kinds: []string{"ConfigMap"},
},
},
},
},
},
},
{
Spec: kyverno.Spec{
Rules: []kyverno.Rule{
{
MatchResources: kyverno.MatchResources{
ResourceDescription: kyverno.ResourceDescription{
Kinds: []string{"ConfigMap"},
Operations: []kyverno.AdmissionOperation{"DELETE"},
},
},
},
},
},
},
},
expectedResult: map[string][]admissionregistrationv1.OperationType{
"ConfigMap": {"CREATE", "UPDATE", "DELETE", "CONNECT"},
},
}, {
name: "test-2",
policies: []kyverno.ClusterPolicy{
{
Spec: kyverno.Spec{
Rules: []kyverno.Rule{
{
MatchResources: kyverno.MatchResources{
ResourceDescription: kyverno.ResourceDescription{
Kinds: []string{"Role"},
Operations: []kyverno.AdmissionOperation{"DELETE"},
},
},
},
},
},
},
{
Spec: kyverno.Spec{
Rules: []kyverno.Rule{
{
MatchResources: kyverno.MatchResources{
ResourceDescription: kyverno.ResourceDescription{
Kinds: []string{"Secrets"},
Operations: []kyverno.AdmissionOperation{"CONNECT"},
},
},
},
},
},
},
},
expectedResult: map[string][]admissionregistrationv1.OperationType{
"Role": {"DELETE"},
"Secrets": {"CONNECT"},
},
},
}
var mapResourceToOpnType map[string][]admissionregistrationv1.OperationType
for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {
for _, p := range test.policies {
mapResourceToOpnType = addOpnForValidatingWebhookConf(p.GetSpec().Rules, mapResourceToOpnType)
}
for key, expectedValue := range test.expectedResult {
slices.SortFunc(expectedValue, func(a, b admissionregistrationv1.OperationType) int {
return cmp.Compare(a, b)
})
value := mapResourceToOpnType[key]
slices.SortFunc(value, func(a, b admissionregistrationv1.OperationType) int {
return cmp.Compare(a, b)
})
if !reflect.DeepEqual(expectedValue, value) {
t.Errorf("key: %v, expected %v, but got %v", key, expectedValue, value)
}
}
})
}
}
func TestAddOperationsForValidatingWebhookConf(t *testing.T) {
testCases := []struct {
name string
rules []kyverno.Rule
expectedResult map[string][]admissionregistrationv1.OperationType
}{
{
name: "Test Case 1",
rules: []kyverno.Rule{
{
MatchResources: kyverno.MatchResources{
ResourceDescription: kyverno.ResourceDescription{
Kinds: []string{"ConfigMap"},
Operations: []kyverno.AdmissionOperation{"CREATE"},
},
},
},
},
expectedResult: map[string][]admissionregistrationv1.OperationType{
"ConfigMap": {"CREATE"},
},
},
{
name: "Test Case 2",
rules: []kyverno.Rule{
{
MatchResources: kyverno.MatchResources{
ResourceDescription: kyverno.ResourceDescription{
Kinds: []string{"ConfigMap"},
},
},
ExcludeResources: &kyverno.MatchResources{
ResourceDescription: kyverno.ResourceDescription{
Operations: []kyverno.AdmissionOperation{"DELETE", "CONNECT", "CREATE"},
},
},
},
},
expectedResult: map[string][]admissionregistrationv1.OperationType{
"ConfigMap": {"UPDATE"},
},
},
{
name: "Test Case 3",
rules: []kyverno.Rule{
{
MatchResources: kyverno.MatchResources{
ResourceDescription: kyverno.ResourceDescription{
Kinds: []string{"ConfigMap"},
Operations: []kyverno.AdmissionOperation{"CREATE"},
},
},
},
{
MatchResources: kyverno.MatchResources{
ResourceDescription: kyverno.ResourceDescription{
Kinds: []string{"ConfigMap"},
},
},
},
},
expectedResult: map[string][]admissionregistrationv1.OperationType{
"ConfigMap": {"CREATE", "UPDATE", "DELETE", "CONNECT"},
},
},
{
name: "Test Case 4",
rules: []kyverno.Rule{
{
MatchResources: kyverno.MatchResources{
ResourceDescription: kyverno.ResourceDescription{
Kinds: []string{"ConfigMap"},
Operations: []kyverno.AdmissionOperation{"CREATE"},
},
},
},
{
MatchResources: kyverno.MatchResources{
ResourceDescription: kyverno.ResourceDescription{
Kinds: []string{"ConfigMap"},
Operations: []kyverno.AdmissionOperation{"UPDATE"},
},
},
},
},
expectedResult: map[string][]admissionregistrationv1.OperationType{
"ConfigMap": {"CREATE", "UPDATE"},
},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
var result map[string][]admissionregistrationv1.OperationType
var mapResourceToOpnType map[string][]admissionregistrationv1.OperationType
result = addOpnForValidatingWebhookConf(testCase.rules, mapResourceToOpnType)
for key, expectedValue := range testCase.expectedResult {
slices.SortFunc(expectedValue, func(a, b admissionregistrationv1.OperationType) int {
return cmp.Compare(a, b)
})
value := result[key]
slices.SortFunc(value, func(a, b admissionregistrationv1.OperationType) int {
return cmp.Compare(a, b)
})
if !reflect.DeepEqual(expectedValue, value) {
t.Errorf("key: %v, expected %v, but got %v", key, expectedValue, value)
}
}
})
}
}
func TestAddOperationsForMutatingtingWebhookConf(t *testing.T) {
testCases := []struct {
name string
rules []kyverno.Rule
expectedResult map[string][]admissionregistrationv1.OperationType
}{
{
name: "Test Case 1",
rules: []kyverno.Rule{
{
Mutation: &kyverno.Mutation{
PatchesJSON6902: "add",
},
MatchResources: kyverno.MatchResources{
ResourceDescription: kyverno.ResourceDescription{
Kinds: []string{"ConfigMap"},
Operations: []kyverno.AdmissionOperation{"CREATE"},
},
},
},
},
expectedResult: map[string][]admissionregistrationv1.OperationType{
"ConfigMap": {"CREATE"},
},
},
{
name: "Test Case 2",
rules: []kyverno.Rule{
{
Mutation: &kyverno.Mutation{
PatchesJSON6902: "add",
},
MatchResources: kyverno.MatchResources{
ResourceDescription: kyverno.ResourceDescription{
Kinds: []string{"Secret"},
},
},
ExcludeResources: &kyverno.MatchResources{
ResourceDescription: kyverno.ResourceDescription{
Operations: []kyverno.AdmissionOperation{"UPDATE"},
},
},
},
},
expectedResult: map[string][]admissionregistrationv1.OperationType{
"Secret": {"CREATE"},
},
},
{
name: "Test Case 3",
rules: []kyverno.Rule{
{
Mutation: &kyverno.Mutation{
PatchesJSON6902: "add",
},
MatchResources: kyverno.MatchResources{
ResourceDescription: kyverno.ResourceDescription{
Kinds: []string{"Secret"},
Operations: []kyverno.AdmissionOperation{"CREATE"},
},
},
},
{
Mutation: &kyverno.Mutation{
PatchesJSON6902: "add",
},
MatchResources: kyverno.MatchResources{
ResourceDescription: kyverno.ResourceDescription{
Kinds: []string{"Secret"},
Operations: []kyverno.AdmissionOperation{"UPDATE"},
},
},
},
},
expectedResult: map[string][]admissionregistrationv1.OperationType{
"Secret": {"CREATE", "UPDATE"},
},
},
{
name: "Test Case 4",
rules: []kyverno.Rule{
{
Mutation: &kyverno.Mutation{
PatchesJSON6902: "add",
},
MatchResources: kyverno.MatchResources{
ResourceDescription: kyverno.ResourceDescription{
Kinds: []string{"Secret"},
Operations: []kyverno.AdmissionOperation{"CREATE"},
},
},
},
{
Mutation: &kyverno.Mutation{
PatchesJSON6902: "add",
},
MatchResources: kyverno.MatchResources{
ResourceDescription: kyverno.ResourceDescription{
Kinds: []string{"Secret"},
},
},
},
},
expectedResult: map[string][]admissionregistrationv1.OperationType{
"Secret": {"CREATE", "UPDATE"},
},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
var result map[string][]admissionregistrationv1.OperationType
var mapResourceToOpnType map[string][]admissionregistrationv1.OperationType
result = addOpnForMutatingWebhookConf(testCase.rules, mapResourceToOpnType)
for key, expectedValue := range testCase.expectedResult {
slices.SortFunc(expectedValue, func(a, b admissionregistrationv1.OperationType) int {
return cmp.Compare(a, b)
})
value := result[key]
slices.SortFunc(value, func(a, b admissionregistrationv1.OperationType) int {
return cmp.Compare(a, b)
})
if !reflect.DeepEqual(expectedValue, value) {
t.Errorf("key: %v, expected %v, but got %v", key, expectedValue, value)
}
}
})
}
}
func TestAddOperationsForMutatingtingWebhookConfMultiplePolicies(t *testing.T) {
testCases := []struct {
name string
policies []kyverno.ClusterPolicy
expectedResult map[string][]admissionregistrationv1.OperationType
}{
{
name: "test-1",
policies: []kyverno.ClusterPolicy{
{
Spec: kyverno.Spec{
Rules: []kyverno.Rule{
{
Mutation: &kyverno.Mutation{
RawPatchStrategicMerge: &apiextensionsv1.JSON{Raw: []byte(`"nodeSelector": {<"public-ip-type": "elastic"}, +"priorityClassName": "elastic-ip-required"`)},
},
MatchResources: kyverno.MatchResources{
ResourceDescription: kyverno.ResourceDescription{
Kinds: []string{"Pod"},
},
},
},
},
},
},
{
Spec: kyverno.Spec{
Rules: []kyverno.Rule{
{
Generation: &kyverno.Generation{},
MatchResources: kyverno.MatchResources{
ResourceDescription: kyverno.ResourceDescription{
Kinds: []string{"Deployments", "StatefulSet", "DaemonSet", "Job"},
},
},
},
},
},
},
},
expectedResult: map[string][]admissionregistrationv1.OperationType{
"Pod": {"CREATE", "UPDATE"},
},
},
}
var mapResourceToOpnType map[string][]admissionregistrationv1.OperationType
for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {
for _, p := range test.policies {
mapResourceToOpnType = addOpnForMutatingWebhookConf(p.GetSpec().Rules, mapResourceToOpnType)
}
if !compareMaps(mapResourceToOpnType, test.expectedResult) {
t.Errorf("Expected %v, but got %v", test.expectedResult, mapResourceToOpnType)
}
})
}
}
func compareMaps(a, b map[string][]admissionregistrationv1.OperationType) bool {
if len(a) != len(b) {
return false
}
for key, aValue := range a {
bValue, ok := b[key]
if !ok {
return false
}
sort.Slice(aValue, func(i, j int) bool {
return cmp.Compare(aValue[i], aValue[j]) < 0
})
sort.Slice(bValue, func(i, j int) bool {
return cmp.Compare(bValue[i], bValue[j]) < 0
})
if !reflect.DeepEqual(aValue, bValue) {
return false
}
}
return true
}

View file

@ -2,229 +2,84 @@ package webhook
import (
"cmp"
"reflect"
"slices"
"strings"
"github.com/kyverno/kyverno/api/kyverno"
kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
"github.com/kyverno/kyverno/pkg/config"
"golang.org/x/exp/maps"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/sets"
objectmeta "k8s.io/client-go/tools/cache"
"k8s.io/utils/ptr"
)
// webhook is the instance that aggregates the GVK of existing policies
// based on group, kind, scopeType, failurePolicy and webhookTimeout
// a fine-grained webhook is created per policy with a unique path
type webhook struct {
// policyMeta is set for fine-grained webhooks
policyMeta objectmeta.ObjectName
maxWebhookTimeout int32
failurePolicy admissionregistrationv1.FailurePolicyType
rules map[groupVersionScope]sets.Set[string]
matchConditions []admissionregistrationv1.MatchCondition
}
// groupVersionScope contains the GV and scopeType of a resource
type groupVersionScope struct {
schema.GroupVersion
scopeType admissionregistrationv1.ScopeType
}
// String puts / between group/version and scope
func (gvs groupVersionScope) String() string {
return gvs.GroupVersion.String() + "/" + string(gvs.scopeType)
}
func newWebhook(timeout int32, failurePolicy admissionregistrationv1.FailurePolicyType, matchConditions []admissionregistrationv1.MatchCondition) *webhook {
return &webhook{
maxWebhookTimeout: timeout,
failurePolicy: failurePolicy,
rules: map[groupVersionScope]sets.Set[string]{},
matchConditions: matchConditions,
func collectResourceDescriptions(rule kyvernov1.Rule, defaultOps ...kyvernov1.AdmissionOperation) webhookConfig {
out := map[string]sets.Set[kyvernov1.AdmissionOperation]{}
for _, kind := range rule.MatchResources.ResourceDescription.Kinds {
if out[kind] == nil {
out[kind] = sets.New[kyvernov1.AdmissionOperation]()
}
ops := rule.MatchResources.ResourceDescription.Operations
if len(ops) == 0 {
ops = defaultOps
}
out[kind].Insert(ops...)
}
}
func findKeyContainingSubstring(m map[string][]admissionregistrationv1.OperationType, substring string, defaultOpn []admissionregistrationv1.OperationType) []admissionregistrationv1.OperationType {
for key, value := range m {
if key == "Pod/exec" || strings.Contains(strings.ToLower(key), strings.ToLower(substring)) || strings.Contains(strings.ToLower(substring), strings.ToLower(key)) {
return value
for _, value := range rule.MatchResources.All {
for _, kind := range value.Kinds {
if out[kind] == nil {
out[kind] = sets.New[kyvernov1.AdmissionOperation]()
}
ops := value.Operations
if len(ops) == 0 {
ops = defaultOps
}
out[kind].Insert(ops...)
}
}
return defaultOpn
}
func newWebhookPerPolicy(timeout int32, failurePolicy admissionregistrationv1.FailurePolicyType, matchConditions []admissionregistrationv1.MatchCondition, policy kyvernov1.PolicyInterface) *webhook {
webhook := newWebhook(timeout, failurePolicy, matchConditions)
webhook.policyMeta = objectmeta.ObjectName{
Namespace: policy.GetNamespace(),
Name: policy.GetName(),
}
if policy.GetSpec().CustomWebhookMatchConditions() {
webhook.matchConditions = policy.GetSpec().GetMatchConditions()
}
return webhook
}
func (wh *webhook) buildRulesWithOperations(final map[string][]admissionregistrationv1.OperationType, defaultOpn []admissionregistrationv1.OperationType) []admissionregistrationv1.RuleWithOperations {
rules := make([]admissionregistrationv1.RuleWithOperations, 0, len(wh.rules))
for gv, resources := range wh.rules {
ruleforset := make([]admissionregistrationv1.RuleWithOperations, 0, len(resources))
for res := range resources {
resource := sets.New(res)
// if we have pods, we add pods/ephemeralcontainers by default
if (gv.Group == "" || gv.Group == "*") && (gv.Version == "v1" || gv.Version == "*") && (resource.Has("pods") || resource.Has("*")) {
resource.Insert("pods/ephemeralcontainers")
for _, value := range rule.MatchResources.Any {
for _, kind := range value.Kinds {
if out[kind] == nil {
out[kind] = sets.New[kyvernov1.AdmissionOperation]()
}
operations := findKeyContainingSubstring(final, res, defaultOpn)
if len(operations) == 0 {
ops := value.Operations
if len(ops) == 0 {
ops = defaultOps
}
out[kind].Insert(ops...)
}
}
// we consider only `exclude.any` elements and only if `kinds` is empty or if there's a corresponding kind in the match statement
// nothing else than `kinds` and `operations` must be set
if rule.ExcludeResources != nil {
for _, value := range rule.ExcludeResources.Any {
if !value.UserInfo.IsEmpty() {
continue
}
slices.SortFunc(operations, func(a, b admissionregistrationv1.OperationType) int {
return cmp.Compare(a, b)
})
var added bool
ruleforset, added = appendResourceInRule(resource, operations, ruleforset)
if !added {
ruleforset = append(ruleforset, admissionregistrationv1.RuleWithOperations{
Rule: admissionregistrationv1.Rule{
APIGroups: []string{gv.Group},
APIVersions: []string{gv.Version},
Resources: sets.List(resource),
Scope: ptr.To(gv.scopeType),
},
Operations: operations,
})
if value.Name != "" ||
len(value.Names) != 0 ||
len(value.Namespaces) != 0 ||
len(value.Annotations) != 0 ||
value.Selector != nil ||
value.NamespaceSelector != nil {
continue
}
}
rules = append(rules, ruleforset...)
}
for _, rule := range rules {
slices.Sort(rule.Resources)
}
less := func(a []string, b []string) (int, bool) {
if x := cmp.Compare(len(a), len(b)); x != 0 {
return x, true
}
for i := range a {
if x := cmp.Compare(a[i], b[i]); x != 0 {
return x, true
kinds := value.Kinds
if len(kinds) == 0 {
kinds = maps.Keys(out)
}
}
return 0, false
}
slices.SortFunc(rules, func(a admissionregistrationv1.RuleWithOperations, b admissionregistrationv1.RuleWithOperations) int {
if x, match := less(a.APIGroups, b.APIGroups); match {
return x
}
if x, match := less(a.APIVersions, b.APIVersions); match {
return x
}
if x, match := less(a.Resources, b.Resources); match {
return x
}
if x := strings.Compare(string(*a.Scope), string(*b.Scope)); x != 0 {
return x
}
return 0
})
return rules
}
func appendResourceInRule(resource sets.Set[string], operations []admissionregistrationv1.OperationType, ruleforset []admissionregistrationv1.RuleWithOperations) ([]admissionregistrationv1.RuleWithOperations, bool) {
for i, rule := range ruleforset {
if reflect.DeepEqual(rule.Operations, operations) {
ruleforset[i].Rule.Resources = append(rule.Rule.Resources, sets.List(resource)...)
return ruleforset, true
}
}
return ruleforset, false
}
func scanResourceFilterForResources(resFilter kyvernov1.ResourceFilters) []string {
var resources []string
for _, rf := range resFilter {
if rf.ResourceDescription.Kinds != nil {
resources = append(resources, rf.ResourceDescription.Kinds...)
}
}
return resources
}
func scanResourceFilter(resFilter kyvernov1.ResourceFilters, operationStatusMap map[string]bool) (bool, map[string]bool) {
opFound := false
for _, rf := range resFilter {
if rf.ResourceDescription.Operations != nil {
for _, o := range rf.ResourceDescription.Operations {
opFound = true
operationStatusMap[string(o)] = true
ops := value.Operations
if len(ops) == 0 {
// if only kind was specified, clear all operations
ops = allOperations
}
for _, kind := range kinds {
if out[kind] != nil {
out[kind] = out[kind].Delete(ops...)
}
}
}
}
return opFound, operationStatusMap
}
func scanResourceFilterForExclude(resFilter kyvernov1.ResourceFilters, operationStatusMap map[string]bool) (bool, map[string]bool) {
opFound := false
for _, rf := range resFilter {
if rf.ResourceDescription.Operations != nil {
for _, o := range rf.ResourceDescription.Operations {
opFound = true
operationStatusMap[string(o)] = false
}
}
}
return opFound, operationStatusMap
}
func (wh *webhook) set(gvrs GroupVersionResourceScope) {
gvs := groupVersionScope{
GroupVersion: gvrs.GroupVersion(),
scopeType: gvrs.Scope,
}
// check if the resource contains wildcard and is already added as all scope
// in that case, we do not need to add it again as namespaced scope
if (gvrs.Resource == "*" || gvrs.Group == "*") && gvs.scopeType == admissionregistrationv1.NamespacedScope {
allScopeResource := groupVersionScope{
GroupVersion: gvs.GroupVersion,
scopeType: admissionregistrationv1.AllScopes,
}
resources := wh.rules[allScopeResource]
if resources != nil {
// explicitly do nothing as the resource is already added as all scope
return
}
}
// check if the resource is already added
resources := wh.rules[gvs]
if resources == nil {
wh.rules[gvs] = sets.New(gvrs.Resource)
} else {
resources.Insert(gvrs.Resource)
}
}
func (wh *webhook) isEmpty() bool {
return len(wh.rules) == 0
}
func (wh *webhook) key(separator string) string {
p := wh.policyMeta
if p.Namespace != "" {
return p.Namespace + separator + p.Name
}
return p.Name
return out
}
func objectMeta(name string, annotations map[string]string, labels map[string]string, owner ...metav1.OwnerReference) metav1.ObjectMeta {
@ -242,163 +97,6 @@ func objectMeta(name string, annotations map[string]string, labels map[string]st
}
}
func computeOperationsForValidatingWebhookConf(r kyvernov1.Rule, operationStatusMap map[string]bool) map[string]bool {
var opFound bool
opFoundCount := 0
if len(r.MatchResources.Any) != 0 {
opFound, operationStatusMap = scanResourceFilter(r.MatchResources.Any, operationStatusMap)
opFoundCount = opFoundCountIncrement(opFound, opFoundCount)
}
if len(r.MatchResources.All) != 0 {
opFound, operationStatusMap = scanResourceFilter(r.MatchResources.All, operationStatusMap)
opFoundCount = opFoundCountIncrement(opFound, opFoundCount)
}
if r.MatchResources.ResourceDescription.Operations != nil {
for _, o := range r.MatchResources.ResourceDescription.Operations {
opFound = true
operationStatusMap[string(o)] = true
opFoundCount = opFoundCountIncrement(opFound, opFoundCount)
}
}
if !opFound {
operationStatusMap[webhookCreate] = true
operationStatusMap[webhookUpdate] = true
operationStatusMap[webhookConnect] = true
operationStatusMap[webhookDelete] = true
}
if r.ExcludeResources != nil {
if r.ExcludeResources.ResourceDescription.Operations != nil {
for _, o := range r.ExcludeResources.ResourceDescription.Operations {
operationStatusMap[string(o)] = false
}
}
if len(r.ExcludeResources.Any) != 0 {
_, operationStatusMap = scanResourceFilterForExclude(r.ExcludeResources.Any, operationStatusMap)
}
if len(r.ExcludeResources.All) != 0 {
_, operationStatusMap = scanResourceFilterForExclude(r.ExcludeResources.All, operationStatusMap)
}
}
return operationStatusMap
}
func opFoundCountIncrement(opFound bool, opFoundCount int) int {
if opFound {
opFoundCount++
}
return opFoundCount
}
func computeOperationsForMutatingWebhookConf(r kyvernov1.Rule, operationStatusMap map[string]bool) map[string]bool {
if r.HasMutate() || r.HasVerifyImages() {
var opFound bool
opFoundCount := 0
if len(r.MatchResources.Any) != 0 {
opFound, operationStatusMap = scanResourceFilter(r.MatchResources.Any, operationStatusMap)
opFoundCount = opFoundCountIncrement(opFound, opFoundCount)
}
if len(r.MatchResources.All) != 0 {
opFound, operationStatusMap = scanResourceFilter(r.MatchResources.All, operationStatusMap)
opFoundCount = opFoundCountIncrement(opFound, opFoundCount)
}
if r.MatchResources.ResourceDescription.Operations != nil {
for _, o := range r.MatchResources.ResourceDescription.Operations {
opFound = true
operationStatusMap[string(o)] = true
opFoundCount = opFoundCountIncrement(opFound, opFoundCount)
}
}
if opFoundCount == 0 {
operationStatusMap[webhookCreate] = true
operationStatusMap[webhookUpdate] = true
}
if r.ExcludeResources != nil {
if r.ExcludeResources.ResourceDescription.Operations != nil {
for _, o := range r.ExcludeResources.ResourceDescription.Operations {
operationStatusMap[string(o)] = false
}
}
if len(r.ExcludeResources.Any) != 0 {
_, operationStatusMap = scanResourceFilterForExclude(r.ExcludeResources.Any, operationStatusMap)
}
if len(r.ExcludeResources.All) != 0 {
_, operationStatusMap = scanResourceFilterForExclude(r.ExcludeResources.All, operationStatusMap)
}
}
}
return operationStatusMap
}
func mergeOperations(operationStatusMap map[string]bool, currentOps []admissionregistrationv1.OperationType) []admissionregistrationv1.OperationType {
operationReq := make([]admissionregistrationv1.OperationType, 0, 4)
for k, v := range operationStatusMap {
if v {
var oper admissionregistrationv1.OperationType = admissionregistrationv1.OperationType(k)
operationReq = append(operationReq, oper)
}
}
result := sets.New(currentOps...).Insert(operationReq...)
return sets.List(result)
}
func getOperationStatusMap() map[string]bool {
operationStatusMap := make(map[string]bool)
operationStatusMap[webhookCreate] = false
operationStatusMap[webhookUpdate] = false
operationStatusMap[webhookDelete] = false
operationStatusMap[webhookConnect] = false
return operationStatusMap
}
func appendResource(r string, mapResourceToOpn map[string]map[string]bool, opnStatusMap map[string]bool, mapResourceToOpnType map[string][]admissionregistrationv1.OperationType) (map[string]map[string]bool, map[string][]admissionregistrationv1.OperationType) {
if _, exists := mapResourceToOpn[r]; exists {
opnStatMap1 := opnStatusMap
opnStatMap2 := mapResourceToOpn[r]
for opn := range opnStatusMap {
if opnStatMap1[opn] || opnStatMap2[opn] {
opnStatusMap[opn] = true
}
}
mapResourceToOpn[r] = opnStatusMap
mapResourceToOpnType[r] = mergeOperations(opnStatusMap, mapResourceToOpnType[r])
} else {
if mapResourceToOpn == nil {
mapResourceToOpn = make(map[string]map[string]bool)
}
mapResourceToOpn[r] = opnStatusMap
if mapResourceToOpnType == nil {
mapResourceToOpnType = make(map[string][]admissionregistrationv1.OperationType)
}
mapResourceToOpnType[r] = mergeOperations(opnStatusMap, mapResourceToOpnType[r])
}
return mapResourceToOpn, mapResourceToOpnType
}
func computeResourcesOfRule(r kyvernov1.Rule) []string {
var resources []string
if len(r.MatchResources.Any) != 0 {
resources = scanResourceFilterForResources(r.MatchResources.Any)
}
if len(r.MatchResources.All) != 0 {
resources = scanResourceFilterForResources(r.MatchResources.Any)
}
if r.MatchResources.ResourceDescription.Kinds != nil {
resources = append(resources, r.MatchResources.ResourceDescription.Kinds...)
}
if r.ExcludeResources != nil {
if len(r.ExcludeResources.Any) != 0 {
resources = scanResourceFilterForResources(r.MatchResources.Any)
}
if len(r.ExcludeResources.All) != 0 {
resources = scanResourceFilterForResources(r.MatchResources.Any)
}
if r.ExcludeResources.ResourceDescription.Kinds != nil {
resources = append(resources, r.ExcludeResources.ResourceDescription.Kinds...)
}
}
return resources
}
func setRuleCount(rules []kyvernov1.Rule, status *kyvernov1.PolicyStatus) {
validateCount, generateCount, mutateCount, verifyImagesCount := 0, 0, 0, 0
for _, rule := range rules {
@ -444,3 +142,15 @@ func webhookNameAndPath(wh webhook, baseName, basePath string) (name string, pat
}
return name, path
}
func less[T cmp.Ordered](a []T, b []T) int {
if x := cmp.Compare(len(a), len(b)); x != 0 {
return x
}
for i := range a {
if x := cmp.Compare(a[i], b[i]); x != 0 {
return x
}
}
return 0
}

View file

@ -2,16 +2,13 @@ package webhook
import (
"encoding/json"
"reflect"
"sort"
"testing"
kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
"github.com/kyverno/kyverno/pkg/autogen"
"gotest.tools/assert"
autogenv1 "github.com/kyverno/kyverno/pkg/autogen/v1"
"github.com/stretchr/testify/assert"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/utils/ptr"
)
@ -20,10 +17,7 @@ func Test_webhook_isEmpty(t *testing.T) {
empty := newWebhook(DefaultWebhookTimeout, admissionregistrationv1.Ignore, []admissionregistrationv1.MatchCondition{})
assert.Equal(t, empty.isEmpty(), true)
notEmpty := newWebhook(DefaultWebhookTimeout, admissionregistrationv1.Ignore, []admissionregistrationv1.MatchCondition{})
notEmpty.set(GroupVersionResourceScope{
GroupVersionResource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"},
Scope: admissionregistrationv1.NamespacedScope,
})
notEmpty.set("", "v1", "pods", "", admissionregistrationv1.NamespacedScope, kyvernov1.Create)
assert.Equal(t, notEmpty.isEmpty(), false)
}
@ -155,9 +149,9 @@ var policy = `
func Test_RuleCount(t *testing.T) {
var cpol kyvernov1.ClusterPolicy
err := json.Unmarshal([]byte(policy), &cpol)
assert.NilError(t, err)
assert.NoError(t, err)
status := cpol.GetStatus()
rules := autogen.Default.ComputeRules(&cpol, "")
rules := autogenv1.ComputeRules(&cpol, "")
setRuleCount(rules, status)
assert.Equal(t, status.RuleCount.Validate, 0)
assert.Equal(t, status.RuleCount.Generate, 0)
@ -165,259 +159,479 @@ func Test_RuleCount(t *testing.T) {
assert.Equal(t, status.RuleCount.VerifyImages, 2)
}
func TestMergeOprations(t *testing.T) {
testCases := []struct {
name string
inputMap map[string]bool
expectedResult []admissionregistrationv1.OperationType
}{
{
name: "Test Case 1",
inputMap: map[string]bool{
webhookCreate: true,
webhookUpdate: false,
webhookDelete: true,
},
expectedResult: []admissionregistrationv1.OperationType{webhookCreate, webhookDelete},
},
{
name: "Test Case 2",
inputMap: map[string]bool{
webhookCreate: false,
webhookUpdate: false,
webhookDelete: false,
webhookConnect: true,
},
expectedResult: []admissionregistrationv1.OperationType{webhookConnect},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
result := mergeOperations(testCase.inputMap, []admissionregistrationv1.OperationType{})
sort.Slice(result, func(i, j int) bool {
return result[i] < result[j]
})
sort.Slice(testCase.expectedResult, func(i, j int) bool {
return testCase.expectedResult[i] < testCase.expectedResult[j]
})
if !reflect.DeepEqual(result, testCase.expectedResult) {
t.Errorf("Expected %v, but got %v", testCase.expectedResult, result)
}
})
}
}
func TestComputeOperationsForMutatingWebhookConf(t *testing.T) {
testCases := []struct {
name string
rules []kyvernov1.Rule
expectedResult map[string]bool
}{
{
name: "Test Case 1",
rules: []kyvernov1.Rule{
{
Mutation: &kyvernov1.Mutation{
PatchesJSON6902: "add",
},
MatchResources: kyvernov1.MatchResources{
ResourceDescription: kyvernov1.ResourceDescription{
Operations: []kyvernov1.AdmissionOperation{webhookCreate},
},
},
},
},
expectedResult: map[string]bool{
webhookCreate: true,
},
},
{
name: "Test Case 2",
rules: []kyvernov1.Rule{
{
Mutation: &kyvernov1.Mutation{
PatchesJSON6902: "add",
},
MatchResources: kyvernov1.MatchResources{},
ExcludeResources: &kyvernov1.MatchResources{},
},
{
Mutation: &kyvernov1.Mutation{
PatchesJSON6902: "add",
},
MatchResources: kyvernov1.MatchResources{},
ExcludeResources: &kyvernov1.MatchResources{},
},
},
expectedResult: map[string]bool{
webhookCreate: true,
webhookUpdate: true,
},
},
{
name: "Test Case 2",
rules: []kyvernov1.Rule{
{
Mutation: &kyvernov1.Mutation{
PatchesJSON6902: "add",
},
MatchResources: kyvernov1.MatchResources{},
ExcludeResources: &kyvernov1.MatchResources{
ResourceDescription: kyvernov1.ResourceDescription{
Operations: []kyvernov1.AdmissionOperation{webhookCreate},
},
},
},
},
expectedResult: map[string]bool{
webhookCreate: false,
webhookUpdate: true,
},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
var result map[string]bool
for _, r := range testCase.rules {
result = computeOperationsForMutatingWebhookConf(r, make(map[string]bool))
}
if !reflect.DeepEqual(result, testCase.expectedResult) {
t.Errorf("Expected %v, but got %v", testCase.expectedResult, result)
}
})
}
}
func TestComputeOperationsForValidatingWebhookConf(t *testing.T) {
testCases := []struct {
name string
rules []kyvernov1.Rule
expectedResult map[string]bool
}{
{
name: "Test Case 1",
rules: []kyvernov1.Rule{
{
MatchResources: kyvernov1.MatchResources{
ResourceDescription: kyvernov1.ResourceDescription{
Operations: []kyvernov1.AdmissionOperation{webhookCreate},
},
},
},
},
expectedResult: map[string]bool{
webhookCreate: true,
},
},
{
name: "Test Case 2",
rules: []kyvernov1.Rule{
{
MatchResources: kyvernov1.MatchResources{},
ExcludeResources: &kyvernov1.MatchResources{},
},
},
expectedResult: map[string]bool{
webhookCreate: true,
webhookUpdate: true,
webhookConnect: true,
webhookDelete: true,
},
},
{
name: "Test Case 3",
rules: []kyvernov1.Rule{
{
MatchResources: kyvernov1.MatchResources{
ResourceDescription: kyvernov1.ResourceDescription{
Operations: []kyvernov1.AdmissionOperation{webhookCreate, webhookUpdate},
},
},
ExcludeResources: &kyvernov1.MatchResources{
ResourceDescription: kyvernov1.ResourceDescription{
Operations: []kyvernov1.AdmissionOperation{webhookDelete},
},
},
},
},
expectedResult: map[string]bool{
webhookCreate: true,
webhookUpdate: true,
webhookDelete: false,
},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
var result map[string]bool
for _, r := range testCase.rules {
result = computeOperationsForValidatingWebhookConf(r, make(map[string]bool))
}
if !reflect.DeepEqual(result, testCase.expectedResult) {
t.Errorf("Expected %v, but got %v", testCase.expectedResult, result)
}
})
}
}
func TestBuildRulesWithOperations(t *testing.T) {
testCases := []struct {
name string
rules map[groupVersionScope]sets.Set[string]
mapResourceToOpnType map[string][]admissionregistrationv1.OperationType
expectedResult []admissionregistrationv1.RuleWithOperations
}{
{
name: "Test Case 1",
rules: map[groupVersionScope]sets.Set[string]{
groupVersionScope{
GroupVersion: corev1.SchemeGroupVersion,
scopeType: admissionregistrationv1.NamespacedScope,
}: {
"pods": sets.Empty{},
"configmaps": sets.Empty{},
},
name string
rules sets.Set[ruleEntry]
expectedResult []admissionregistrationv1.RuleWithOperations
}{{
rules: sets.New[ruleEntry](
ruleEntry{"", "v1", "configmaps", "", admissionregistrationv1.NamespacedScope, kyvernov1.Create},
ruleEntry{"", "v1", "pods", "", admissionregistrationv1.NamespacedScope, kyvernov1.Create},
),
expectedResult: []admissionregistrationv1.RuleWithOperations{{
Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.Create},
Rule: admissionregistrationv1.Rule{
APIGroups: []string{""},
APIVersions: []string{"v1"},
Resources: []string{"configmaps", "pods", "pods/ephemeralcontainers"},
Scope: ptr.To(admissionregistrationv1.NamespacedScope),
},
mapResourceToOpnType: map[string][]admissionregistrationv1.OperationType{
"Pod": {webhookCreate, webhookUpdate},
"ConfigMaps": {webhookCreate},
}},
}, {
rules: sets.New[ruleEntry](
ruleEntry{"", "v1", "configmaps", "", admissionregistrationv1.NamespacedScope, kyvernov1.Create},
ruleEntry{"", "v1", "pods", "", admissionregistrationv1.NamespacedScope, kyvernov1.Create},
ruleEntry{"", "v1", "pods", "", admissionregistrationv1.NamespacedScope, kyvernov1.Update},
),
expectedResult: []admissionregistrationv1.RuleWithOperations{{
Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.Create},
Rule: admissionregistrationv1.Rule{
APIGroups: []string{""},
APIVersions: []string{"v1"},
Resources: []string{"configmaps"},
Scope: ptr.To(admissionregistrationv1.NamespacedScope),
},
expectedResult: []admissionregistrationv1.RuleWithOperations{
{
Operations: []admissionregistrationv1.OperationType{webhookCreate},
Rule: admissionregistrationv1.Rule{
APIGroups: []string{""},
APIVersions: []string{"v1"},
Resources: []string{"configmaps"},
Scope: ptr.To(admissionregistrationv1.NamespacedScope),
},
}, {
Operations: []admissionregistrationv1.OperationType{webhookCreate, webhookUpdate},
Rule: admissionregistrationv1.Rule{
APIGroups: []string{""},
APIVersions: []string{"v1"},
Resources: []string{"pods", "pods/ephemeralcontainers"},
Scope: ptr.To(admissionregistrationv1.NamespacedScope),
},
},
}, {
Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.Create, admissionregistrationv1.Update},
Rule: admissionregistrationv1.Rule{
APIGroups: []string{""},
APIVersions: []string{"v1"},
Resources: []string{"pods", "pods/ephemeralcontainers"},
Scope: ptr.To(admissionregistrationv1.NamespacedScope),
},
},
}
}},
}}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
wh := &webhook{
rules: testCase.rules,
}
result := wh.buildRulesWithOperations(testCase.mapResourceToOpnType, []admissionregistrationv1.OperationType{webhookCreate, webhookUpdate})
if !reflect.DeepEqual(result, testCase.expectedResult) {
t.Errorf("Expected %v, but got %v", testCase.expectedResult, result)
}
result := wh.buildRulesWithOperations()
assert.Equal(t, testCase.expectedResult, result)
})
}
}
func Test_less(t *testing.T) {
tests := []struct {
name string
do func() int
want int
}{{
do: func() int {
return less([]int{0}, []int{0, 0})
},
want: -1,
}, {
do: func() int {
return less([]int{0, 0}, []int{0})
},
want: 1,
}, {
do: func() int {
return less([]int{0}, []int{1})
},
want: -1,
}, {
do: func() int {
return less([]int{1}, []int{0})
},
want: 1,
}, {
do: func() int {
return less([]int{0, 0}, []int{0, 0})
},
want: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.do()
assert.Equal(t, tt.want, got)
})
}
}
func Test_collectResourceDescriptions(t *testing.T) {
tests := []struct {
name string
rule kyvernov1.Rule
defaultOps []kyvernov1.AdmissionOperation
want webhookConfig
}{{
name: "empty",
rule: kyvernov1.Rule{},
defaultOps: allOperations,
want: webhookConfig{},
}, {
name: "match any - default ops",
rule: kyvernov1.Rule{
MatchResources: kyvernov1.MatchResources{
Any: kyvernov1.ResourceFilters{{
ResourceDescription: kyvernov1.ResourceDescription{
Kinds: []string{"ConfigMap"},
},
}},
},
},
defaultOps: []kyvernov1.AdmissionOperation{kyvernov1.Create, kyvernov1.Update},
want: webhookConfig{
"ConfigMap": sets.New(kyvernov1.Create, kyvernov1.Update),
},
}, {
name: "match any - ops",
rule: kyvernov1.Rule{
MatchResources: kyvernov1.MatchResources{
Any: kyvernov1.ResourceFilters{{
ResourceDescription: kyvernov1.ResourceDescription{
Kinds: []string{"ConfigMap"},
Operations: []kyvernov1.AdmissionOperation{kyvernov1.Create, kyvernov1.Update},
},
}},
},
},
defaultOps: allOperations,
want: webhookConfig{
"ConfigMap": sets.New(kyvernov1.Create, kyvernov1.Update),
},
}, {
name: "match any - multiple",
rule: kyvernov1.Rule{
MatchResources: kyvernov1.MatchResources{
Any: kyvernov1.ResourceFilters{{
ResourceDescription: kyvernov1.ResourceDescription{
Kinds: []string{"ConfigMap"},
Operations: []kyvernov1.AdmissionOperation{kyvernov1.Create},
},
}, {
ResourceDescription: kyvernov1.ResourceDescription{
Kinds: []string{
"Secret",
},
Operations: []kyvernov1.AdmissionOperation{kyvernov1.Update},
},
}},
},
},
defaultOps: allOperations,
want: webhookConfig{
"ConfigMap": sets.New(kyvernov1.Create),
"Secret": sets.New(kyvernov1.Update),
},
}, {
name: "match all - default ops",
rule: kyvernov1.Rule{
MatchResources: kyvernov1.MatchResources{
All: kyvernov1.ResourceFilters{{
ResourceDescription: kyvernov1.ResourceDescription{
Kinds: []string{"ConfigMap"},
},
}},
},
},
defaultOps: []kyvernov1.AdmissionOperation{kyvernov1.Create, kyvernov1.Update},
want: webhookConfig{
"ConfigMap": sets.New(kyvernov1.Create, kyvernov1.Update),
},
}, {
name: "match any - ops",
rule: kyvernov1.Rule{
MatchResources: kyvernov1.MatchResources{
All: kyvernov1.ResourceFilters{{
ResourceDescription: kyvernov1.ResourceDescription{
Kinds: []string{"ConfigMap"},
Operations: []kyvernov1.AdmissionOperation{kyvernov1.Create, kyvernov1.Update},
},
}},
},
},
defaultOps: allOperations,
want: webhookConfig{
"ConfigMap": sets.New(kyvernov1.Create, kyvernov1.Update),
},
}, {
name: "match all - multiple",
rule: kyvernov1.Rule{
MatchResources: kyvernov1.MatchResources{
Any: kyvernov1.ResourceFilters{{
ResourceDescription: kyvernov1.ResourceDescription{
Kinds: []string{"ConfigMap"},
Operations: []kyvernov1.AdmissionOperation{kyvernov1.Create},
},
}, {
ResourceDescription: kyvernov1.ResourceDescription{
Kinds: []string{"Secret"},
Operations: []kyvernov1.AdmissionOperation{kyvernov1.Update},
},
}},
},
},
defaultOps: allOperations,
want: webhookConfig{
"ConfigMap": sets.New(kyvernov1.Create),
"Secret": sets.New(kyvernov1.Update),
},
}, {
name: "exclude - no ops",
rule: kyvernov1.Rule{
MatchResources: kyvernov1.MatchResources{
Any: kyvernov1.ResourceFilters{{
ResourceDescription: kyvernov1.ResourceDescription{
Kinds: []string{"ConfigMap"},
Operations: allOperations,
},
}, {
ResourceDescription: kyvernov1.ResourceDescription{
Kinds: []string{"Secret"},
Operations: []kyvernov1.AdmissionOperation{kyvernov1.Update},
},
}},
},
ExcludeResources: &kyvernov1.MatchResources{
Any: kyvernov1.ResourceFilters{{
ResourceDescription: kyvernov1.ResourceDescription{
Kinds: []string{"ConfigMap"},
},
}},
},
},
defaultOps: []kyvernov1.AdmissionOperation{kyvernov1.Create, kyvernov1.Update},
want: webhookConfig{
"ConfigMap": sets.New[kyvernov1.AdmissionOperation](),
"Secret": sets.New(kyvernov1.Update),
},
}, {
name: "exclude - ops",
rule: kyvernov1.Rule{
MatchResources: kyvernov1.MatchResources{
Any: kyvernov1.ResourceFilters{{
ResourceDescription: kyvernov1.ResourceDescription{
Kinds: []string{"ConfigMap"},
Operations: allOperations,
},
}, {
ResourceDescription: kyvernov1.ResourceDescription{
Kinds: []string{"Secret"},
Operations: []kyvernov1.AdmissionOperation{kyvernov1.Update},
},
}},
},
ExcludeResources: &kyvernov1.MatchResources{
Any: kyvernov1.ResourceFilters{{
ResourceDescription: kyvernov1.ResourceDescription{
Kinds: []string{"ConfigMap"},
Operations: []kyvernov1.AdmissionOperation{kyvernov1.Connect},
},
}},
},
},
defaultOps: []kyvernov1.AdmissionOperation{kyvernov1.Create, kyvernov1.Update},
want: webhookConfig{
"ConfigMap": sets.New(kyvernov1.Create, kyvernov1.Update, kyvernov1.Delete),
"Secret": sets.New(kyvernov1.Update),
},
}, {
name: "exclude - with annotations",
rule: kyvernov1.Rule{
MatchResources: kyvernov1.MatchResources{
Any: kyvernov1.ResourceFilters{{
ResourceDescription: kyvernov1.ResourceDescription{
Kinds: []string{"ConfigMap"},
Operations: allOperations,
},
}, {
ResourceDescription: kyvernov1.ResourceDescription{
Kinds: []string{"Secret"},
Operations: []kyvernov1.AdmissionOperation{kyvernov1.Update},
},
}},
},
ExcludeResources: &kyvernov1.MatchResources{
Any: kyvernov1.ResourceFilters{{
ResourceDescription: kyvernov1.ResourceDescription{
Kinds: []string{"ConfigMap"},
Annotations: map[string]string{
"foo": "bar",
},
Operations: []kyvernov1.AdmissionOperation{kyvernov1.Connect},
},
}},
},
},
defaultOps: []kyvernov1.AdmissionOperation{kyvernov1.Create, kyvernov1.Update},
want: webhookConfig{
"ConfigMap": sets.New(allOperations...),
"Secret": sets.New(kyvernov1.Update),
},
}, {
name: "exclude - with name",
rule: kyvernov1.Rule{
MatchResources: kyvernov1.MatchResources{
Any: kyvernov1.ResourceFilters{{
ResourceDescription: kyvernov1.ResourceDescription{
Kinds: []string{"ConfigMap"},
Operations: allOperations,
},
}, {
ResourceDescription: kyvernov1.ResourceDescription{
Kinds: []string{"Secret"},
Operations: []kyvernov1.AdmissionOperation{kyvernov1.Update},
},
}},
},
ExcludeResources: &kyvernov1.MatchResources{
Any: kyvernov1.ResourceFilters{{
ResourceDescription: kyvernov1.ResourceDescription{
Kinds: []string{"ConfigMap"},
Name: "foo",
Operations: []kyvernov1.AdmissionOperation{kyvernov1.Connect},
},
}},
},
},
defaultOps: []kyvernov1.AdmissionOperation{kyvernov1.Create, kyvernov1.Update},
want: webhookConfig{
"ConfigMap": sets.New(allOperations...),
"Secret": sets.New(kyvernov1.Update),
},
}, {
name: "exclude - with names",
rule: kyvernov1.Rule{
MatchResources: kyvernov1.MatchResources{
Any: kyvernov1.ResourceFilters{{
ResourceDescription: kyvernov1.ResourceDescription{
Kinds: []string{"ConfigMap"},
Operations: allOperations,
},
}, {
ResourceDescription: kyvernov1.ResourceDescription{
Kinds: []string{"Secret"},
Operations: []kyvernov1.AdmissionOperation{kyvernov1.Update},
},
}},
},
ExcludeResources: &kyvernov1.MatchResources{
Any: kyvernov1.ResourceFilters{{
ResourceDescription: kyvernov1.ResourceDescription{
Kinds: []string{"ConfigMap"},
Names: []string{"foo"},
Operations: []kyvernov1.AdmissionOperation{kyvernov1.Connect},
},
}},
},
},
defaultOps: []kyvernov1.AdmissionOperation{kyvernov1.Create, kyvernov1.Update},
want: webhookConfig{
"ConfigMap": sets.New(allOperations...),
"Secret": sets.New(kyvernov1.Update),
},
}, {
name: "exclude - with namespaces",
rule: kyvernov1.Rule{
MatchResources: kyvernov1.MatchResources{
Any: kyvernov1.ResourceFilters{{
ResourceDescription: kyvernov1.ResourceDescription{
Kinds: []string{"ConfigMap"},
Operations: allOperations,
},
}, {
ResourceDescription: kyvernov1.ResourceDescription{
Kinds: []string{"Secret"},
Operations: []kyvernov1.AdmissionOperation{kyvernov1.Update},
},
}},
},
ExcludeResources: &kyvernov1.MatchResources{
Any: kyvernov1.ResourceFilters{{
ResourceDescription: kyvernov1.ResourceDescription{
Kinds: []string{"ConfigMap"},
Namespaces: []string{"foo"},
Operations: []kyvernov1.AdmissionOperation{kyvernov1.Connect},
},
}},
},
},
defaultOps: []kyvernov1.AdmissionOperation{kyvernov1.Create, kyvernov1.Update},
want: webhookConfig{
"ConfigMap": sets.New(allOperations...),
"Secret": sets.New(kyvernov1.Update),
},
}, {
name: "exclude - with selector",
rule: kyvernov1.Rule{
MatchResources: kyvernov1.MatchResources{
Any: kyvernov1.ResourceFilters{{
ResourceDescription: kyvernov1.ResourceDescription{
Kinds: []string{"ConfigMap"},
Operations: allOperations,
},
}, {
ResourceDescription: kyvernov1.ResourceDescription{
Kinds: []string{"Secret"},
Operations: []kyvernov1.AdmissionOperation{kyvernov1.Update},
},
}},
},
ExcludeResources: &kyvernov1.MatchResources{
Any: kyvernov1.ResourceFilters{{
ResourceDescription: kyvernov1.ResourceDescription{
Kinds: []string{"ConfigMap"},
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"foo": "bar",
},
},
Operations: []kyvernov1.AdmissionOperation{kyvernov1.Connect},
},
}},
},
},
defaultOps: []kyvernov1.AdmissionOperation{kyvernov1.Create, kyvernov1.Update},
want: webhookConfig{
"ConfigMap": sets.New(allOperations...),
"Secret": sets.New(kyvernov1.Update),
},
}, {
name: "exclude - with ns selector",
rule: kyvernov1.Rule{
MatchResources: kyvernov1.MatchResources{
Any: kyvernov1.ResourceFilters{{
ResourceDescription: kyvernov1.ResourceDescription{
Kinds: []string{"ConfigMap"},
Operations: allOperations,
},
}, {
ResourceDescription: kyvernov1.ResourceDescription{
Kinds: []string{"Secret"},
Operations: []kyvernov1.AdmissionOperation{kyvernov1.Update},
},
}},
},
ExcludeResources: &kyvernov1.MatchResources{
Any: kyvernov1.ResourceFilters{{
ResourceDescription: kyvernov1.ResourceDescription{
Kinds: []string{"ConfigMap"},
NamespaceSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"foo": "bar",
},
},
Operations: []kyvernov1.AdmissionOperation{kyvernov1.Connect},
},
}},
},
},
defaultOps: []kyvernov1.AdmissionOperation{kyvernov1.Create, kyvernov1.Update},
want: webhookConfig{
"ConfigMap": sets.New(allOperations...),
"Secret": sets.New(kyvernov1.Update),
},
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := collectResourceDescriptions(tt.rule, tt.defaultOps...)
assert.Equal(t, tt.want, got)
})
}
}

View file

@ -0,0 +1,279 @@
package webhook
import (
"slices"
"strings"
kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
"k8s.io/apimachinery/pkg/util/sets"
objectmeta "k8s.io/client-go/tools/cache"
"k8s.io/utils/ptr"
)
// webhook is the instance that aggregates the GVK of existing policies
// based on group, kind, scopeType, failurePolicy and webhookTimeout
// a fine-grained webhook is created per policy with a unique path
type webhook struct {
// policyMeta is set for fine-grained webhooks
policyMeta objectmeta.ObjectName
maxWebhookTimeout int32
failurePolicy admissionregistrationv1.FailurePolicyType
rules sets.Set[ruleEntry]
matchConditions []admissionregistrationv1.MatchCondition
}
type ruleEntry struct {
group string
version string
resource string
subresource string
scope admissionregistrationv1.ScopeType
operation kyvernov1.AdmissionOperation
}
type groupVersionScope struct {
group string
version string
scope admissionregistrationv1.ScopeType
}
type resourceOperations struct {
create bool
update bool
delete bool
connect bool
}
func (r resourceOperations) operations() []admissionregistrationv1.OperationType {
// if r.create && r.update && r.delete && r.connect {
// return []admissionregistrationv1.OperationType{admissionregistrationv1.OperationAll}
// }
var ops []admissionregistrationv1.OperationType
if r.create {
ops = append(ops, admissionregistrationv1.Create)
}
if r.update {
ops = append(ops, admissionregistrationv1.Update)
}
if r.delete {
ops = append(ops, admissionregistrationv1.Delete)
}
if r.connect {
ops = append(ops, admissionregistrationv1.Connect)
}
return ops
}
func newWebhook(timeout int32, failurePolicy admissionregistrationv1.FailurePolicyType, matchConditions []admissionregistrationv1.MatchCondition) *webhook {
return &webhook{
maxWebhookTimeout: timeout,
failurePolicy: failurePolicy,
rules: sets.New[ruleEntry](),
matchConditions: matchConditions,
}
}
func newWebhookPerPolicy(timeout int32, failurePolicy admissionregistrationv1.FailurePolicyType, matchConditions []admissionregistrationv1.MatchCondition, policy kyvernov1.PolicyInterface) *webhook {
webhook := newWebhook(timeout, failurePolicy, matchConditions)
webhook.policyMeta = objectmeta.ObjectName{
Namespace: policy.GetNamespace(),
Name: policy.GetName(),
}
if policy.GetSpec().CustomWebhookMatchConditions() {
webhook.matchConditions = policy.GetSpec().GetMatchConditions()
}
return webhook
}
func (wh *webhook) hasRule(
group, version, resource, subresource string,
scope admissionregistrationv1.ScopeType,
operation kyvernov1.AdmissionOperation,
) bool {
var scopes []admissionregistrationv1.ScopeType
if scope == admissionregistrationv1.AllScopes {
scopes = []admissionregistrationv1.ScopeType{scope}
} else {
scopes = []admissionregistrationv1.ScopeType{scope, admissionregistrationv1.AllScopes}
}
var groups, versions []string
if group == "*" {
groups = []string{group}
} else {
groups = []string{group, "*"}
}
if version == "*" {
versions = []string{version}
} else {
versions = []string{version, "*"}
}
type resourceAndSub struct {
resource, sub string
}
var resources []resourceAndSub
// */* -> */*
// pods/* -> pods/*, */*
// */scale -> */scale, */*
// pods/scale -> pods/scale, pods/*, */scale, */*
// * -> *, */*
// pods -> pods, *, */* (but not pods/*)
if subresource == "" {
if resource == "*" {
resources = []resourceAndSub{{"*", ""}, {"*", "*"}}
} else {
resources = []resourceAndSub{{resource, ""}, {"*", ""}, {"*", "*"}}
}
} else if subresource == "*" {
if resource == "*" {
resources = []resourceAndSub{{"*", "*"}}
} else {
resources = []resourceAndSub{{resource, "*"}, {"*", "*"}}
}
} else {
if resource == "*" {
resources = []resourceAndSub{{"*", subresource}, {"*", "*"}}
} else {
resources = []resourceAndSub{{resource, subresource}, {resource, "*"}, {"*", subresource}, {"*", "*"}}
}
}
for _, _scope := range scopes {
for _, _group := range groups {
for _, _version := range versions {
for _, _resource := range resources {
if _scope != scope || _group != group || _version != version || _resource.resource != resource || _resource.sub != subresource {
test := ruleEntry{
group: _group,
version: _version,
resource: _resource.resource,
subresource: _resource.sub,
scope: _scope,
operation: operation,
}
if wh.rules.Has(test) {
return true
}
}
}
}
}
}
return false
}
func (wh *webhook) buildRulesWithOperations() []admissionregistrationv1.RuleWithOperations {
rules := map[groupVersionScope]map[string]resourceOperations{}
// keep only the relevant rules and map operations by [group, version, scope] first, then by [resource]
for rule := range wh.rules {
if !wh.hasRule(rule.group, rule.version, rule.resource, rule.subresource, rule.scope, rule.operation) {
key := groupVersionScope{rule.group, rule.version, rule.scope}
gvs := rules[key]
if gvs == nil {
gvs = map[string]resourceOperations{}
rules[key] = gvs
}
resource := rule.resource
if rule.subresource != "" {
resource = rule.resource + "/" + rule.subresource
}
ops := gvs[resource]
switch rule.operation {
case kyvernov1.Create:
ops.create = true
case kyvernov1.Update:
ops.update = true
case kyvernov1.Delete:
ops.delete = true
case kyvernov1.Connect:
ops.connect = true
}
gvs[resource] = ops
}
}
// build rules
out := make([]admissionregistrationv1.RuleWithOperations, 0, len(rules))
for gvs, resources := range rules {
// invert the resources map
opsResources := map[resourceOperations]sets.Set[string]{}
for resource, ops := range resources {
r := opsResources[ops]
if r == nil {
r = sets.New[string]()
}
opsResources[ops] = r.Insert(resource)
}
for ops, resources := range opsResources {
// if we have pods, we add pods/ephemeralcontainers by default
if (gvs.group == "" || gvs.group == "*") && (gvs.version == "v1" || gvs.version == "*") && (resources.Has("pods") || resources.Has("*")) {
resources = resources.Insert("pods/ephemeralcontainers")
}
out = append(out, admissionregistrationv1.RuleWithOperations{
Rule: admissionregistrationv1.Rule{
APIGroups: []string{gvs.group},
APIVersions: []string{gvs.version},
Resources: resources.UnsortedList(),
Scope: ptr.To(gvs.scope),
},
Operations: ops.operations(),
})
}
}
// sort rules
for _, rule := range out {
slices.Sort(rule.APIGroups)
slices.Sort(rule.APIVersions)
slices.Sort(rule.Resources)
slices.Sort(rule.Operations)
}
slices.SortFunc(out, func(a admissionregistrationv1.RuleWithOperations, b admissionregistrationv1.RuleWithOperations) int {
if x := less(a.APIGroups, b.APIGroups); x != 0 {
return x
}
if x := less(a.APIVersions, b.APIVersions); x != 0 {
return x
}
if x := less(a.Resources, b.Resources); x != 0 {
return x
}
if x := less(a.Operations, b.Operations); x != 0 {
return x
}
if x := strings.Compare(string(*a.Scope), string(*b.Scope)); x != 0 {
return x
}
return 0
})
return out
}
func (wh *webhook) set(
group string,
version string,
resource string,
subresource string,
scope admissionregistrationv1.ScopeType,
operations ...kyvernov1.AdmissionOperation,
) {
for _, operation := range operations {
wh.rules.Insert(ruleEntry{
group: group,
version: version,
resource: resource,
subresource: subresource,
scope: scope,
operation: operation,
})
}
}
func (wh *webhook) isEmpty() bool {
return len(wh.rules) == 0
}
func (wh *webhook) key(separator string) string {
p := wh.policyMeta
if p.Namespace != "" {
return p.Namespace + separator + p.Name
}
return p.Name
}