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:
parent
50006a3e66
commit
3580034fa1
5 changed files with 918 additions and 1157 deletions
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
279
pkg/controllers/webhook/utils_webhook.go
Normal file
279
pkg/controllers/webhook/utils_webhook.go
Normal 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
|
||||
}
|
Loading…
Add table
Reference in a new issue