mirror of
https://github.com/kyverno/kyverno.git
synced 2025-01-20 18:52:16 +00:00
3580034fa1
* 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>
279 lines
7.8 KiB
Go
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
|
|
}
|