1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2025-01-20 18:52:16 +00:00
kyverno/pkg/controllers/webhook/utils_webhook.go
Charles-Edouard Brétéché 3580034fa1
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>
2024-10-21 12:56:21 +00:00

279 lines
7.8 KiB
Go

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
}