1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2024-12-14 11:57:48 +00:00

fix: Add subresources support to policy exceptions (#5839)

Signed-off-by: Vyom-Yadav <jackhammervyom@gmail.com>

Signed-off-by: Vyom-Yadav <jackhammervyom@gmail.com>
Co-authored-by: shuting <shuting@nirmata.com>
This commit is contained in:
Vyom Yadav 2023-01-05 11:53:44 +05:30 committed by GitHub
parent 40836ff6fc
commit 9d2deb0568
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 275 additions and 136 deletions

View file

@ -11,7 +11,7 @@ import (
"github.com/kyverno/kyverno/pkg/config"
enginecontext "github.com/kyverno/kyverno/pkg/engine/context"
controllerutils "github.com/kyverno/kyverno/pkg/utils/controller"
match "github.com/kyverno/kyverno/pkg/utils/match"
"github.com/kyverno/kyverno/pkg/utils/match"
"go.uber.org/multierr"
"k8s.io/apimachinery/pkg/util/sets"
corev1listers "k8s.io/client-go/listers/core/v1"
@ -94,13 +94,13 @@ func (h *handlers) executePolicy(ctx context.Context, logger logr.Logger, policy
debug.Info("resource namespace didn't match policy namespace", "result", err)
}
// match resource with match/exclude clause
matched := match.CheckMatchesResources(resource, spec.MatchResources, nsLabels)
matched := match.CheckMatchesResources(resource, spec.MatchResources, nsLabels, nil, "")
if matched != nil {
debug.Info("resource/match didn't match", "result", matched)
continue
}
if spec.ExcludeResources != nil {
excluded := match.CheckMatchesResources(resource, *spec.ExcludeResources, nsLabels)
excluded := match.CheckMatchesResources(resource, *spec.ExcludeResources, nsLabels, nil, "")
if excluded == nil {
debug.Info("resource/exclude matched")
continue

View file

@ -70,8 +70,12 @@ func filterRule(rclient registryclient.Client, rule kyvernov1.Rule, policyContex
}
logger := logging.WithName("exception")
kindsInPolicy := append(rule.MatchResources.GetKinds(), rule.ExcludeResources.GetKinds()...)
subresourceGVKToAPIResource := GetSubresourceGVKToAPIResourceMap(kindsInPolicy, policyContext)
// check if there is a corresponding policy exception
ruleResp := hasPolicyExceptions(policyContext, &rule, logger)
ruleResp := hasPolicyExceptions(policyContext, &rule, subresourceGVKToAPIResource, logger)
if ruleResp != nil {
return ruleResp
}
@ -94,9 +98,6 @@ func filterRule(rclient registryclient.Client, rule kyvernov1.Rule, policyContex
logger = logging.WithName(string(ruleType)).WithValues("policy", policy.GetName(),
"kind", newResource.GetKind(), "namespace", newResource.GetNamespace(), "name", newResource.GetName())
kindsInPolicy := append(rule.MatchResources.GetKinds(), rule.ExcludeResources.GetKinds()...)
subresourceGVKToAPIResource := GetSubresourceGVKToAPIResourceMap(kindsInPolicy, policyContext)
if err := MatchesResourceDescription(subresourceGVKToAPIResource, newResource, rule, admissionInfo, excludeGroupRole, namespaceLabels, "", policyContext.subresource); err != nil {
if ruleType == response.Generation {
// if the oldResource matched, return "false" to delete GR for it

View file

@ -106,12 +106,15 @@ func VerifyAndPatchImages(
return
}
if !matches(logger, rule, policyContext) {
kindsInPolicy := append(rule.MatchResources.GetKinds(), rule.ExcludeResources.GetKinds()...)
subresourceGVKToAPIResource := GetSubresourceGVKToAPIResourceMap(kindsInPolicy, policyContext)
if !matches(logger, rule, policyContext, subresourceGVKToAPIResource) {
return
}
// check if there is a corresponding policy exception
ruleResp := hasPolicyExceptions(policyContext, rule, logger)
ruleResp := hasPolicyExceptions(policyContext, rule, subresourceGVKToAPIResource, logger)
if ruleResp != nil {
resp.PolicyResponse.Rules = append(resp.PolicyResponse.Rules, *ruleResp)
return

View file

@ -73,7 +73,7 @@ func Mutate(ctx context.Context, rclient registryclient.Client, policyContext *P
}
// check if there is a corresponding policy exception
ruleResp := hasPolicyExceptions(policyContext, &computeRules[i], logger)
ruleResp := hasPolicyExceptions(policyContext, &computeRules[i], subresourceGVKToAPIResource, logger)
if ruleResp != nil {
resp.PolicyResponse.Rules = append(resp.PolicyResponse.Rules, *ruleResp)
return

View file

@ -17,19 +17,16 @@ import (
"github.com/kyverno/kyverno/pkg/engine/wildcards"
"github.com/kyverno/kyverno/pkg/logging"
datautils "github.com/kyverno/kyverno/pkg/utils/data"
kubeutils "github.com/kyverno/kyverno/pkg/utils/kube"
matched "github.com/kyverno/kyverno/pkg/utils/match"
"github.com/kyverno/kyverno/pkg/utils/wildcard"
"github.com/pkg/errors"
"golang.org/x/exp/slices"
"golang.org/x/text/cases"
"golang.org/x/text/language"
authenticationv1 "k8s.io/api/authentication/v1"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime/schema"
)
// EngineStats stores in the statistics for a single application of resource
@ -40,32 +37,6 @@ type EngineStats struct {
RulesAppliedCount int
}
func checkKind(subresourceGVKToAPIResource map[string]*metav1.APIResource, kinds []string, gvk schema.GroupVersionKind, subresourceInAdmnReview string) bool {
title := cases.Title(language.Und, cases.NoLower)
result := false
for _, k := range kinds {
if k != "*" {
gv, kind := kubeutils.GetKindFromGVK(k)
apiResource, ok := subresourceGVKToAPIResource[k]
if ok {
result = apiResource.Group == gvk.Group && (apiResource.Version == gvk.Version || strings.Contains(gv, "*")) && apiResource.Kind == gvk.Kind
} else { // if the kind is not found in the subresourceGVKToAPIResource, then it is not a subresource
result = title.String(kind) == gvk.Kind && subresourceInAdmnReview == ""
if gv != "" {
result = result && kubeutils.GroupVersionMatches(gv, gvk.GroupVersion().String())
}
}
} else {
result = true
}
if result {
break
}
}
return result
}
func checkName(name, resourceName string) bool {
return wildcard.Match(name, resourceName)
}
@ -145,7 +116,7 @@ func doesResourceMatchConditionBlock(subresourceGVKToAPIResource map[string]*met
var errs []error
if len(conditionBlock.Kinds) > 0 {
if !checkKind(subresourceGVKToAPIResource, conditionBlock.Kinds, resource.GroupVersionKind(), subresourceInAdmnReview) {
if !matched.CheckKind(subresourceGVKToAPIResource, conditionBlock.Kinds, resource.GroupVersionKind(), subresourceInAdmnReview) {
errs = append(errs, fmt.Errorf("kind does not match %v", conditionBlock.Kinds))
}
}

View file

@ -4,8 +4,6 @@ import (
"encoding/json"
"testing"
"k8s.io/apimachinery/pkg/runtime/schema"
v1 "github.com/kyverno/kyverno/api/kyverno/v1"
"github.com/kyverno/kyverno/api/kyverno/v1beta1"
"github.com/kyverno/kyverno/pkg/autogen"
@ -2469,68 +2467,3 @@ func TestManagedPodResource(t *testing.T) {
assert.Equal(t, res, tc.expectedResult, "test %d/%s failed, expect %v, got %v", i+1, tc.name, tc.expectedResult, res)
}
}
func Test_checkKind(t *testing.T) {
subresourceGVKToAPIResource := make(map[string]*metav1.APIResource)
match := checkKind(subresourceGVKToAPIResource, []string{"*"}, schema.GroupVersionKind{Kind: "Deployment", Group: "", Version: "v1"}, "")
assert.Equal(t, match, true)
match = checkKind(subresourceGVKToAPIResource, []string{"Pod"}, schema.GroupVersionKind{Kind: "Pod", Group: "", Version: "v1"}, "")
assert.Equal(t, match, true)
match = checkKind(subresourceGVKToAPIResource, []string{"v1/Pod"}, schema.GroupVersionKind{Kind: "Pod", Group: "", Version: "v1"}, "")
assert.Equal(t, match, true)
match = checkKind(subresourceGVKToAPIResource, []string{"tekton.dev/v1beta1/TaskRun"}, schema.GroupVersionKind{Kind: "TaskRun", Group: "tekton.dev", Version: "v1beta1"}, "")
assert.Equal(t, match, true)
match = checkKind(subresourceGVKToAPIResource, []string{"tekton.dev/*/TaskRun"}, schema.GroupVersionKind{Kind: "TaskRun", Group: "tekton.dev", Version: "v1alpha1"}, "")
assert.Equal(t, match, true)
// Though both 'pods', 'pods/status' have same kind i.e. 'Pod' but they are different resources, 'subresourceInAdmnReview' is used in determining that.
match = checkKind(subresourceGVKToAPIResource, []string{"v1/Pod"}, schema.GroupVersionKind{Kind: "Pod", Group: "", Version: "v1"}, "status")
assert.Equal(t, match, false)
// Though both 'pods', 'pods/status' have same kind i.e. 'Pod' but they are different resources, 'subresourceInAdmnReview' is used in determining that.
match = checkKind(subresourceGVKToAPIResource, []string{"v1/Pod"}, schema.GroupVersionKind{Kind: "Pod", Group: "", Version: "v1"}, "ephemeralcontainers")
assert.Equal(t, match, false)
subresourceGVKToAPIResource["networking.k8s.io/v1/NetworkPolicy/status"] = &metav1.APIResource{
Name: "networkpolicies/status",
SingularName: "",
Namespaced: true,
Kind: "NetworkPolicy",
Group: "networking.k8s.io",
Version: "v1",
}
subresourceGVKToAPIResource["v1/Pod.status"] = &metav1.APIResource{
Name: "pods/status",
SingularName: "",
Namespaced: true,
Kind: "Pod",
Group: "",
Version: "v1",
}
subresourceGVKToAPIResource["*/Pod.eviction"] = &metav1.APIResource{
Name: "pods/eviction",
SingularName: "",
Namespaced: true,
Kind: "Eviction",
Group: "policy",
Version: "v1",
}
match = checkKind(subresourceGVKToAPIResource, []string{"networking.k8s.io/v1/NetworkPolicy/status"}, schema.GroupVersionKind{Kind: "NetworkPolicy", Group: "networking.k8s.io", Version: "v1"}, "status")
assert.Equal(t, match, true)
match = checkKind(subresourceGVKToAPIResource, []string{"v1/Pod.status"}, schema.GroupVersionKind{Kind: "Pod", Group: "", Version: "v1"}, "status")
assert.Equal(t, match, true)
match = checkKind(subresourceGVKToAPIResource, []string{"*/Pod.eviction"}, schema.GroupVersionKind{Kind: "Eviction", Group: "policy", Version: "v1"}, "eviction")
assert.Equal(t, match, true)
match = checkKind(subresourceGVKToAPIResource, []string{"v1alpha1/Pod.eviction"}, schema.GroupVersionKind{Kind: "Eviction", Group: "policy", Version: "v1"}, "eviction")
assert.Equal(t, match, false)
}

View file

@ -133,11 +133,14 @@ func validateResource(ctx context.Context, log logr.Logger, rclient registryclie
return nil
}
log = log.WithValues("rule", rule.Name)
if !matches(log, rule, enginectx) {
kindsInPolicy := append(rule.MatchResources.GetKinds(), rule.ExcludeResources.GetKinds()...)
subresourceGVKToAPIResource := GetSubresourceGVKToAPIResourceMap(kindsInPolicy, enginectx)
if !matches(log, rule, enginectx, subresourceGVKToAPIResource) {
return nil
}
// check if there is a corresponding policy exception
ruleResp := hasPolicyExceptions(enginectx, rule, log)
ruleResp := hasPolicyExceptions(enginectx, rule, subresourceGVKToAPIResource, log)
if ruleResp != nil {
return ruleResp
}
@ -573,10 +576,7 @@ func isEmptyUnstructured(u *unstructured.Unstructured) bool {
}
// matches checks if either the new or old resource satisfies the filter conditions defined in the rule
func matches(logger logr.Logger, rule *kyvernov1.Rule, ctx *PolicyContext) bool {
kindsInPolicy := append(rule.MatchResources.GetKinds(), rule.ExcludeResources.GetKinds()...)
subresourceGVKToAPIResource := GetSubresourceGVKToAPIResourceMap(kindsInPolicy, ctx)
func matches(logger logr.Logger, rule *kyvernov1.Rule, ctx *PolicyContext, subresourceGVKToAPIResource map[string]*metav1.APIResource) bool {
err := MatchesResourceDescription(subresourceGVKToAPIResource, ctx.newResource, *rule, ctx.admissionInfo, ctx.excludeGroupRole, ctx.namespaceLabels, "", ctx.subresource)
if err == nil {
return true
@ -775,13 +775,13 @@ func (v *validator) substituteDeny() error {
}
// matchesException checks if an exception applies to the resource being admitted
func matchesException(policyContext *PolicyContext, rule *kyvernov1.Rule) (*kyvernov2alpha1.PolicyException, error) {
func matchesException(policyContext *PolicyContext, rule *kyvernov1.Rule, subresourceGVKToAPIResource map[string]*metav1.APIResource) (*kyvernov2alpha1.PolicyException, error) {
candidates, err := policyContext.FindExceptions(rule.Name)
if err != nil {
return nil, err
}
for _, candidate := range candidates {
err := matched.CheckMatchesResources(policyContext.newResource, candidate.Spec.Match, policyContext.namespaceLabels)
err := matched.CheckMatchesResources(policyContext.newResource, candidate.Spec.Match, policyContext.namespaceLabels, subresourceGVKToAPIResource, policyContext.subresource)
// if there's no error it means a match
if err == nil {
return candidate, nil
@ -792,9 +792,9 @@ func matchesException(policyContext *PolicyContext, rule *kyvernov1.Rule) (*kyve
// hasPolicyExceptions returns nil when there are no matching exceptions.
// A rule response is returned when an exception is matched, or there is an error.
func hasPolicyExceptions(ctx *PolicyContext, rule *kyvernov1.Rule, log logr.Logger) *response.RuleResponse {
func hasPolicyExceptions(ctx *PolicyContext, rule *kyvernov1.Rule, subresourceGVKToAPIResource map[string]*metav1.APIResource, log logr.Logger) *response.RuleResponse {
// if matches, check if there is a corresponding policy exception
exception, err := matchesException(ctx, rule)
exception, err := matchesException(ctx, rule, subresourceGVKToAPIResource)
// if we found an exception
if err == nil && exception != nil {
key, err := cache.MetaNamespaceKeyFunc(exception)

View file

@ -8,6 +8,7 @@ import (
kyvernov2beta1 "github.com/kyverno/kyverno/api/kyverno/v2beta1"
"github.com/kyverno/kyverno/pkg/engine/wildcards"
"github.com/kyverno/kyverno/pkg/logging"
kubeutils "github.com/kyverno/kyverno/pkg/utils/kube"
"github.com/kyverno/kyverno/pkg/utils/wildcard"
"go.uber.org/multierr"
"golang.org/x/text/cases"
@ -32,6 +33,8 @@ func CheckMatchesResources(
resource unstructured.Unstructured,
statement kyvernov2beta1.MatchResources,
namespaceLabels map[string]string,
subresourceGVKToAPIResource map[string]*metav1.APIResource,
subresourceInAdmnReview string,
// policyNamespace string,
) error {
var errs []error
@ -45,6 +48,8 @@ func CheckMatchesResources(
rmr,
resource,
namespaceLabels,
subresourceGVKToAPIResource,
subresourceInAdmnReview,
)) == 0 {
oneMatched = true
break
@ -62,6 +67,8 @@ func CheckMatchesResources(
rmr,
resource,
namespaceLabels,
subresourceGVKToAPIResource,
subresourceInAdmnReview,
)...,
)
}
@ -73,6 +80,8 @@ func checkResourceFilter(
statement kyvernov1.ResourceFilter,
resource unstructured.Unstructured,
namespaceLabels map[string]string,
subresourceGVKToAPIResource map[string]*metav1.APIResource,
subresourceInAdmnReview string,
) []error {
var errs []error
// checking if the block is empty
@ -84,6 +93,8 @@ func checkResourceFilter(
statement.ResourceDescription,
resource,
namespaceLabels,
subresourceGVKToAPIResource,
subresourceInAdmnReview,
)
errs = append(errs, matchErrs...)
return errs
@ -93,10 +104,12 @@ func checkResourceDescription(
conditionBlock kyvernov1.ResourceDescription,
resource unstructured.Unstructured,
namespaceLabels map[string]string,
subresourceGVKToAPIResource map[string]*metav1.APIResource,
subresourceInAdmnReview string,
) []error {
var errs []error
if len(conditionBlock.Kinds) > 0 {
if !checkKind(conditionBlock.Kinds, resource.GetKind(), resource.GroupVersionKind()) {
if !CheckKind(subresourceGVKToAPIResource, conditionBlock.Kinds, resource.GroupVersionKind(), subresourceInAdmnReview) {
errs = append(errs, fmt.Errorf("kind does not match %v", conditionBlock.Kinds))
}
}
@ -154,29 +167,30 @@ func checkResourceDescription(
return errs
}
func checkKind(kinds []string, resourceKind string, gvk schema.GroupVersionKind) bool {
func CheckKind(subresourceGVKToAPIResource map[string]*metav1.APIResource, kinds []string, gvk schema.GroupVersionKind, subresourceInAdmnReview string) bool {
title := cases.Title(language.Und, cases.NoLower)
result := false
for _, k := range kinds {
parts := strings.Split(k, "/")
if len(parts) == 1 {
if k == "*" || resourceKind == title.String(k) {
return true
if k != "*" {
gv, kind := kubeutils.GetKindFromGVK(k)
apiResource, ok := subresourceGVKToAPIResource[k]
if ok {
result = apiResource.Group == gvk.Group && (apiResource.Version == gvk.Version || strings.Contains(gv, "*")) && apiResource.Kind == gvk.Kind
} else { // if the kind is not found in the subresourceGVKToAPIResource, then it is not a subresource
result = title.String(kind) == gvk.Kind && subresourceInAdmnReview == ""
if gv != "" {
result = result && kubeutils.GroupVersionMatches(gv, gvk.GroupVersion().String())
}
}
} else {
result = true
}
if len(parts) == 2 {
kindParts := strings.SplitN(parts[1], ".", 2)
if gvk.Kind == title.String(kindParts[0]) && gvk.Version == parts[0] {
return true
}
}
if len(parts) == 3 || len(parts) == 4 {
kindParts := strings.SplitN(parts[2], ".", 2)
if gvk.Group == parts[0] && (gvk.Version == parts[1] || parts[1] == "*") && gvk.Kind == title.String(kindParts[0]) {
return true
}
if result {
break
}
}
return false
return result
}
func checkName(name, resourceName string) bool {

View file

@ -0,0 +1,74 @@
package match
import (
"testing"
"gotest.tools/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
)
func Test_CheckKind(t *testing.T) {
subresourceGVKToAPIResource := make(map[string]*metav1.APIResource)
match := CheckKind(subresourceGVKToAPIResource, []string{"*"}, schema.GroupVersionKind{Kind: "Deployment", Group: "", Version: "v1"}, "")
assert.Equal(t, match, true)
match = CheckKind(subresourceGVKToAPIResource, []string{"Pod"}, schema.GroupVersionKind{Kind: "Pod", Group: "", Version: "v1"}, "")
assert.Equal(t, match, true)
match = CheckKind(subresourceGVKToAPIResource, []string{"v1/Pod"}, schema.GroupVersionKind{Kind: "Pod", Group: "", Version: "v1"}, "")
assert.Equal(t, match, true)
match = CheckKind(subresourceGVKToAPIResource, []string{"tekton.dev/v1beta1/TaskRun"}, schema.GroupVersionKind{Kind: "TaskRun", Group: "tekton.dev", Version: "v1beta1"}, "")
assert.Equal(t, match, true)
match = CheckKind(subresourceGVKToAPIResource, []string{"tekton.dev/*/TaskRun"}, schema.GroupVersionKind{Kind: "TaskRun", Group: "tekton.dev", Version: "v1alpha1"}, "")
assert.Equal(t, match, true)
// Though both 'pods', 'pods/status' have same kind i.e. 'Pod' but they are different resources, 'subresourceInAdmnReview' is used in determining that.
match = CheckKind(subresourceGVKToAPIResource, []string{"v1/Pod"}, schema.GroupVersionKind{Kind: "Pod", Group: "", Version: "v1"}, "status")
assert.Equal(t, match, false)
// Though both 'pods', 'pods/status' have same kind i.e. 'Pod' but they are different resources, 'subresourceInAdmnReview' is used in determining that.
match = CheckKind(subresourceGVKToAPIResource, []string{"v1/Pod"}, schema.GroupVersionKind{Kind: "Pod", Group: "", Version: "v1"}, "ephemeralcontainers")
assert.Equal(t, match, false)
subresourceGVKToAPIResource["networking.k8s.io/v1/NetworkPolicy/status"] = &metav1.APIResource{
Name: "networkpolicies/status",
SingularName: "",
Namespaced: true,
Kind: "NetworkPolicy",
Group: "networking.k8s.io",
Version: "v1",
}
subresourceGVKToAPIResource["v1/Pod.status"] = &metav1.APIResource{
Name: "pods/status",
SingularName: "",
Namespaced: true,
Kind: "Pod",
Group: "",
Version: "v1",
}
subresourceGVKToAPIResource["*/Pod.eviction"] = &metav1.APIResource{
Name: "pods/eviction",
SingularName: "",
Namespaced: true,
Kind: "Eviction",
Group: "policy",
Version: "v1",
}
match = CheckKind(subresourceGVKToAPIResource, []string{"networking.k8s.io/v1/NetworkPolicy/status"}, schema.GroupVersionKind{Kind: "NetworkPolicy", Group: "networking.k8s.io", Version: "v1"}, "status")
assert.Equal(t, match, true)
match = CheckKind(subresourceGVKToAPIResource, []string{"v1/Pod.status"}, schema.GroupVersionKind{Kind: "Pod", Group: "", Version: "v1"}, "status")
assert.Equal(t, match, true)
match = CheckKind(subresourceGVKToAPIResource, []string{"*/Pod.eviction"}, schema.GroupVersionKind{Kind: "Eviction", Group: "policy", Version: "v1"}, "eviction")
assert.Equal(t, match, true)
match = CheckKind(subresourceGVKToAPIResource, []string{"v1alpha1/Pod.eviction"}, schema.GroupVersionKind{Kind: "Eviction", Group: "policy", Version: "v1"}, "eviction")
assert.Equal(t, match, false)
}

View file

@ -0,0 +1,37 @@
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: nginx-test-scaling-policy
status:
conditions:
- reason: Succeeded
status: "True"
type: Ready
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: nginx-test
name: nginx-test
namespace: test-validate
status:
replicas: 2
---
apiVersion: kyverno.io/v2alpha1
kind: PolicyException
metadata:
name: allow-scaling-nginx-test
namespace: test-validate
spec:
exceptions:
- policyName: nginx-test-scaling-policy
ruleNames:
- validate-nginx-test
match:
any:
- resources:
kinds:
- Deployment/scale
names:
- nginx-test

View file

@ -0,0 +1,67 @@
apiVersion: v1
kind: Namespace
metadata:
name: test-validate
---
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: nginx-test-scaling-policy
spec:
background: false
failurePolicy: Fail
rules:
- match:
resources:
kinds:
- "Deployment/scale"
names:
- nginx-test
namespaces:
- test-validate
name: validate-nginx-test
validate:
message: 'nginx-test needs to have 2 replicas'
pattern:
spec:
replicas: 2
validationFailureAction: Enforce
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: nginx-test
name: nginx-test
namespace: test-validate
spec:
replicas: 2
selector:
matchLabels:
app: nginx-test
template:
metadata:
labels:
app: nginx-test
spec:
containers:
- image: nginx
name: nginx
---
apiVersion: kyverno.io/v2alpha1
kind: PolicyException
metadata:
name: allow-scaling-nginx-test
namespace: test-validate
spec:
exceptions:
- policyName: nginx-test-scaling-policy
ruleNames:
- validate-nginx-test
match:
any:
- resources:
kinds:
- Deployment/scale
names:
- nginx-test

View file

@ -0,0 +1,12 @@
apiVersion: kuttl.dev/v1beta1
kind: TestStep
commands:
- script: |
if kubectl scale deployment nginx-test --replicas=1 -n test-validate 2>&1 | grep -q 'validation error: nginx-test needs to have 2 replicas'
then
echo "Test failed. Resource was blocked from scaling."
exit 1
else
echo "Tested succeeded. Resource was allowed to scale."
exit 0
fi

View file

@ -0,0 +1,7 @@
apiVersion: kuttl.dev/v1beta1
kind: TestStep
commands:
- command: kubectl delete deploy nginx-test -n test-validate --force --wait=true --ignore-not-found=true
- command: kubectl delete polex allow-scaling-nginx-test -n test-validate --force --wait=true --ignore-not-found=true
- command: kubectl delete cpol nginx-test-scaling-policy -n test-validate --force --wait=true --ignore-not-found=true
- command: kubectl delete ns test-validate --force --ignore-not-found=true

View file

@ -0,0 +1,20 @@
# ## Description
This test validates that a policy blocking scaling using `Deployment/scale` resource can be bypassed using `PolicyException`.
## Expected Behavior
The `Deployment` is scaled.
## Steps
### Test Steps
1. Create a `ClusterPolicy` that matches on `Deployment/scale` and blocks scaling the `Deployment`.
2. Create a `Deployment` with the number of replicas allowed in the policy.
3. Create a `PolicyException` for the above mentioned policy.
4. Validate that the `Deployment` is scaled.
## Reference Issue(s)
https://github.com/kyverno/kyverno/issues/5804