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

feat: add cleanupPolicy validation code (#5279)

* validate the cleanupPolicy

Signed-off-by: Nikhil Sharma <nikhilsharma230303@gmail.com>

* add validation for DELETE permission for cleanupPolicy

Signed-off-by: Nikhil Sharma <nikhilsharma230303@gmail.com>

* add separate binary for cleanupPolicy

Signed-off-by: Nikhil Sharma <nikhilsharma230303@gmail.com>

* fix linter issues

Signed-off-by: Nikhil Sharma <nikhilsharma230303@gmail.com>

Signed-off-by: Nikhil Sharma <nikhilsharma230303@gmail.com>
Co-authored-by: Charles-Edouard Brétéché <charled.breteche@gmail.com>
This commit is contained in:
Nikhil Sharma 2022-11-14 15:13:32 +05:30 committed by GitHub
parent 2b4ff1ef6d
commit d44dc97990
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 574 additions and 0 deletions

View file

@ -0,0 +1,18 @@
package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation/field"
)
// CleanupPolicyInterface abstracts the concrete policy type (Policy vs ClusterPolicy)
// +kubebuilder:object:generate=false
type CleanupPolicyInterface interface {
metav1.Object
GetSpec() *CleanupPolicySpec
GetStatus() *CleanupPolicyStatus
Validate(sets.String) field.ErrorList
GetKind() string
GetSchedule() string
}

View file

@ -0,0 +1,158 @@
package v1alpha1
import (
"encoding/json"
"fmt"
"testing"
"gotest.tools/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/validation/field"
)
func Test_CleanupPolicy_Name(t *testing.T) {
subject := CleanupPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: "this-is-a-way-too-long-policy-name-that-should-trigger-an-error-when-calling-the-policy-validation-method",
},
Spec: CleanupPolicySpec{
Schedule: "* * * * *",
},
}
errs := subject.Validate(nil)
assert.Assert(t, len(errs) == 1)
assert.Equal(t, errs[0].Field, "metadata.name")
assert.Equal(t, errs[0].Type, field.ErrorTypeTooLong)
assert.Equal(t, errs[0].Detail, "must have at most 63 bytes")
assert.Equal(t, errs[0].Error(), "metadata.name: Too long: must have at most 63 bytes")
}
func Test_CleanupPolicy_Schedule(t *testing.T) {
subject := CleanupPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: "test-policy",
},
Spec: CleanupPolicySpec{
Schedule: "schedule-not-in-proper-cron-format",
},
}
errs := subject.Validate(nil)
assert.Assert(t, len(errs) == 1)
assert.Equal(t, errs[0].Field, "spec.schedule")
assert.Equal(t, errs[0].Type, field.ErrorTypeInvalid)
assert.Equal(t, errs[0].Detail, "schedule spec in the cleanupPolicy is not in proper cron format")
assert.Equal(t, errs[0].Error(), fmt.Sprintf(`spec.schedule: Invalid value: "%s": schedule spec in the cleanupPolicy is not in proper cron format`, subject.Spec.Schedule))
}
func Test_ClusterCleanupPolicy_Name(t *testing.T) {
subject := ClusterCleanupPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: "this-is-a-way-too-long-policy-name-that-should-trigger-an-error-when-calling-the-policy-validation-method",
},
Spec: CleanupPolicySpec{
Schedule: "* * * * *",
},
}
errs := subject.Validate(nil)
assert.Assert(t, len(errs) == 1)
assert.Equal(t, errs[0].Field, "metadata.name")
assert.Equal(t, errs[0].Type, field.ErrorTypeTooLong)
assert.Equal(t, errs[0].Detail, "must have at most 63 bytes")
assert.Equal(t, errs[0].Error(), "metadata.name: Too long: must have at most 63 bytes")
}
func Test_ClusterCleanupPolicy_Schedule(t *testing.T) {
subject := ClusterCleanupPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: "test-policy",
},
Spec: CleanupPolicySpec{
Schedule: "schedule-not-in-proper-cron-format",
},
}
errs := subject.Validate(nil)
assert.Assert(t, len(errs) == 1)
assert.Equal(t, errs[0].Field, "spec.schedule")
assert.Equal(t, errs[0].Type, field.ErrorTypeInvalid)
assert.Equal(t, errs[0].Detail, "schedule spec in the cleanupPolicy is not in proper cron format")
assert.Equal(t, errs[0].Error(), fmt.Sprintf(`spec.schedule: Invalid value: "%s": schedule spec in the cleanupPolicy is not in proper cron format`, subject.Spec.Schedule))
}
func Test_doesMatchExcludeConflict(t *testing.T) {
path := field.NewPath("dummy")
testcases := []struct {
description string
policySpec []byte
errors func(r *CleanupPolicySpec) field.ErrorList
}{
{
description: "Same match and exclude",
policySpec: []byte(`{"match":{"resources":{"kinds":["Pod","Namespace"],"name":"something","namespaces":["something","something1"],"selector":{"matchLabels":{"memory":"high"},"matchExpressions":[{"key":"tier","operator":"In","values":["database"]}]}},"subjects":[{"name":"something","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something","something1"],"roles":["something","something1"]},"exclude":{"resources":{"kinds":["Pod","Namespace"],"name":"something","namespaces":["something","something1"],"selector":{"matchLabels":{"memory":"high"},"matchExpressions":[{"key":"tier","operator":"In","values":["database"]}]}},"subjects":[{"name":"something","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something","something1"],"roles":["something","something1"]}, "schedule": "* * * * *"}`),
errors: func(r *CleanupPolicySpec) (errs field.ErrorList) {
return append(errs, field.Invalid(path, r, "CleanupPolicy is matching an empty set"))
},
},
{
description: "Failed to exclude kind",
policySpec: []byte(`{"match":{"resources":{"kinds":["Pod","Namespace"],"name":"something","namespaces":["something","something1"],"selector":{"matchLabels":{"memory":"high"},"matchExpressions":[{"key":"tier","operator":"In","values":["database"]}]}},"subjects":[{"name":"something","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something","something1"],"roles":["something","something1"]},"exclude":{"resources":{"kinds":["Namespace"],"name":"something","namespaces":["something","something1"],"selector":{"matchLabels":{"memory":"high"},"matchExpressions":[{"key":"tier","operator":"In","values":["database"]}]}},"subjects":[{"name":"something","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something","something1"],"roles":["something","something1"]}, "schedule": "* * * * *"}`),
},
{
description: "Failed to exclude name",
policySpec: []byte(`{"match":{"resources":{"kinds":["Pod","Namespace"],"name":"something","namespaces":["something","something1"],"selector":{"matchLabels":{"memory":"high"},"matchExpressions":[{"key":"tier","operator":"In","values":["database"]}]}},"subjects":[{"name":"something","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something","something1"],"roles":["something","something1"]},"exclude":{"resources":{"kinds":["Pod","Namespace"],"name":"something-*","namespaces":["something","something1"],"selector":{"matchLabels":{"memory":"high"},"matchExpressions":[{"key":"tier","operator":"In","values":["database"]}]}},"subjects":[{"name":"something","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something","something1"],"roles":["something","something1"]}, "schedule": "* * * * *"}`),
},
{
description: "Failed to exclude namespace",
policySpec: []byte(`{"match":{"resources":{"kinds":["Pod","Namespace"],"name":"something","namespaces":["something","something1"],"selector":{"matchLabels":{"memory":"high"},"matchExpressions":[{"key":"tier","operator":"In","values":["database"]}]}},"subjects":[{"name":"something","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something","something1"],"roles":["something","something1"]},"exclude":{"resources":{"kinds":["Pod","Namespace"],"name":"something","namespaces":["something3","something1"],"selector":{"matchLabels":{"memory":"high"},"matchExpressions":[{"key":"tier","operator":"In","values":["database"]}]}},"subjects":[{"name":"something","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something","something1"],"roles":["something","something1"]}, "schedule": "* * * * *"}`),
},
{
description: "Failed to exclude labels",
policySpec: []byte(`{"match":{"resources":{"kinds":["Pod","Namespace"],"name":"something","namespaces":["something","something1"],"selector":{"matchLabels":{"memory":"high"},"matchExpressions":[{"key":"tier","operator":"In","values":["database"]}]}},"subjects":[{"name":"something","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something","something1"],"roles":["something","something1"]},"exclude":{"resources":{"kinds":["Pod","Namespace"],"name":"something","namespaces":["something","something1"],"selector":{"matchLabels":{"memory":"higha"},"matchExpressions":[{"key":"tier","operator":"In","values":["database"]}]}},"subjects":[{"name":"something","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something","something1"],"roles":["something","something1"]}, "schedule": "* * * * *"}`),
},
{
description: "Failed to exclude expression",
policySpec: []byte(`{"match":{"resources":{"kinds":["Pod","Namespace"],"name":"something","namespaces":["something","something1"],"selector":{"matchLabels":{"memory":"high"},"matchExpressions":[{"key":"tier","operator":"In","values":["database"]}]}},"subjects":[{"name":"something","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something","something1"],"roles":["something","something1"]},"exclude":{"resources":{"kinds":["Pod","Namespace"],"name":"something","namespaces":["something","something1"],"selector":{"matchLabels":{"memory":"high"},"matchExpressions":[{"key":"tier","operator":"In","values":["databases"]}]}},"subjects":[{"name":"something","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something","something1"],"roles":["something","something1"]}, "schedule": "* * * * *"}`),
},
{
description: "Failed to exclude subjects",
policySpec: []byte(`{"match":{"resources":{"kinds":["Pod","Namespace"],"name":"something","namespaces":["something","something1"],"selector":{"matchLabels":{"memory":"high"},"matchExpressions":[{"key":"tier","operator":"In","values":["database"]}]}},"subjects":[{"name":"something","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something","something1"],"roles":["something","something1"]},"exclude":{"resources":{"kinds":["Pod","Namespace"],"name":"something","namespaces":["something","something1"],"selector":{"matchLabels":{"memory":"high"},"matchExpressions":[{"key":"tier","operator":"In","values":["database"]}]}},"subjects":[{"name":"something2","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something","something1"],"roles":["something","something1"]}, "schedule": "* * * * *"}`),
},
{
description: "Failed to exclude clusterroles",
policySpec: []byte(`{"match":{"resources":{"kinds":["Pod","Namespace"],"name":"something","namespaces":["something","something1"],"selector":{"matchLabels":{"memory":"high"},"matchExpressions":[{"key":"tier","operator":"In","values":["database"]}]}},"subjects":[{"name":"something","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something","something1"],"roles":["something","something1"]},"exclude":{"resources":{"kinds":["Pod","Namespace"],"name":"something","namespaces":["something","something1"],"selector":{"matchLabels":{"memory":"high"},"matchExpressions":[{"key":"tier","operator":"In","values":["database"]}]}},"subjects":[{"name":"something","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something3","something1"],"roles":["something","something1"]}, "schedule": "* * * * *"}`),
},
{
description: "Failed to exclude roles",
policySpec: []byte(`{"match":{"resources":{"kinds":["Pod","Namespace"],"name":"something","namespaces":["something","something1"],"selector":{"matchLabels":{"memory":"high"},"matchExpressions":[{"key":"tier","operator":"In","values":["database"]}]}},"subjects":[{"name":"something","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something","something1"],"roles":["something","something1"]},"exclude":{"resources":{"kinds":["Pod","Namespace"],"name":"something","namespaces":["something","something1"],"selector":{"matchLabels":{"memory":"high"},"matchExpressions":[{"key":"tier","operator":"In","values":["database"]}]}},"subjects":[{"name":"something","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something","something1"],"roles":["something3","something1"]}, "schedule": "* * * * *"}`),
},
{
description: "simple",
policySpec: []byte(`{"match":{"resources":{"kinds":["Pod","Namespace"],"name":"something","namespaces":["something","something1"]}},"exclude":{"resources":{"kinds":["Pod","Namespace","Job"],"name":"some*","namespaces":["something","something1","something2"]}}, "schedule": "* * * * *"}`),
errors: func(r *CleanupPolicySpec) (errs field.ErrorList) {
return append(errs, field.Invalid(path, r, "CleanupPolicy is matching an empty set"))
},
},
{
description: "simple - fail",
policySpec: []byte(`{"match":{"resources":{"kinds":["Pod","Namespace"],"name":"somxething","namespaces":["something","something1"]}},"exclude":{"resources":{"kinds":["Pod","Namespace","Job"],"name":"some*","namespaces":["something","something1","something2"]}}, "schedule": "* * * * *"}`),
},
{
description: "empty case",
policySpec: []byte(`{"match":{"resources":{"selector":{"matchLabels":{"allow-deletes":"false"}}}},"exclude":{"clusterRoles":["random"]},"validate":{"message":"Deleting {{request.object.kind}}/{{request.object.metadata.name}} is not allowed","deny":{"conditions":{"all":[{"key":"{{request.operation}}","operator":"Equal","value":"DELETE"}]}}}, "schedule": "* * * * *"}`),
},
}
for _, testcase := range testcases {
var policySpec CleanupPolicySpec
err := json.Unmarshal(testcase.policySpec, &policySpec)
assert.NilError(t, err)
errs := policySpec.ValidateMatchExcludeConflict(path)
var expectedErrs field.ErrorList
if testcase.errors != nil {
expectedErrs = testcase.errors(&policySpec)
}
assert.Equal(t, len(errs), len(expectedErrs))
for i := range errs {
fmt.Println(i)
assert.Equal(t, errs[i].Error(), expectedErrs[i].Error())
}
}
}

View file

@ -17,8 +17,15 @@ limitations under the License.
package v1alpha1 package v1alpha1
import ( import (
"encoding/json"
"reflect"
kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1" kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
"github.com/kyverno/kyverno/pkg/utils/wildcard"
"github.com/robfig/cron"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation/field"
) )
// +genclient // +genclient
@ -41,6 +48,32 @@ type CleanupPolicy struct {
Status CleanupPolicyStatus `json:"status,omitempty"` Status CleanupPolicyStatus `json:"status,omitempty"`
} }
// GetSpec returns the policy spec
func (p *CleanupPolicy) GetSpec() *CleanupPolicySpec {
return &p.Spec
}
// GetStatus returns the policy status
func (p *CleanupPolicy) GetStatus() *CleanupPolicyStatus {
return &p.Status
}
// GetSchedule returns the schedule from the policy spec
func (p *CleanupPolicy) GetSchedule() string {
return p.Spec.Schedule
}
func (p *CleanupPolicy) GetKind() string {
return p.Kind
}
// Validate implements programmatic validation
func (p *CleanupPolicy) Validate(clusterResources sets.String) (errs field.ErrorList) {
errs = append(errs, kyvernov1.ValidatePolicyName(field.NewPath("metadata").Child("name"), p.Name)...)
errs = append(errs, p.Spec.Validate(field.NewPath("spec"), clusterResources, true)...)
return errs
}
// +kubebuilder:object:root=true // +kubebuilder:object:root=true
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
@ -72,6 +105,32 @@ type ClusterCleanupPolicy struct {
Status CleanupPolicyStatus `json:"status,omitempty"` Status CleanupPolicyStatus `json:"status,omitempty"`
} }
// GetSpec returns the policy spec
func (p *ClusterCleanupPolicy) GetSpec() *CleanupPolicySpec {
return &p.Spec
}
// GetStatus returns the policy status
func (p *ClusterCleanupPolicy) GetStatus() *CleanupPolicyStatus {
return &p.Status
}
// GetSchedule returns the schedule from the policy spec
func (p *ClusterCleanupPolicy) GetSchedule() string {
return p.Spec.Schedule
}
func (p *ClusterCleanupPolicy) GetKind() string {
return p.Kind
}
// Validate implements programmatic validation
func (p *ClusterCleanupPolicy) Validate(clusterResources sets.String) (errs field.ErrorList) {
errs = append(errs, kyvernov1.ValidatePolicyName(field.NewPath("metadata").Child("name"), p.Name)...)
errs = append(errs, p.Spec.Validate(field.NewPath("spec"), clusterResources, false)...)
return errs
}
// +kubebuilder:object:root=true // +kubebuilder:object:root=true
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
@ -109,3 +168,185 @@ type CleanupPolicySpec struct {
type CleanupPolicyStatus struct { type CleanupPolicyStatus struct {
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"`
} }
// Validate implements programmatic validation
func (p *CleanupPolicySpec) Validate(path *field.Path, clusterResources sets.String, namespaced bool) (errs field.ErrorList) {
errs = append(errs, ValidateSchedule(path.Child("schedule"), p.Schedule)...)
errs = append(errs, p.MatchResources.Validate(path.Child("match"), namespaced, clusterResources)...)
errs = append(errs, p.ExcludeResources.Validate(path.Child("exclude"), namespaced, clusterResources)...)
errs = append(errs, p.ValidateMatchExcludeConflict(path)...)
return errs
}
// ValidateSchedule validates whether the schedule specified is in proper cron format or not.
func ValidateSchedule(path *field.Path, schedule string) (errs field.ErrorList) {
if _, err := cron.ParseStandard(schedule); err != nil {
errs = append(errs, field.Invalid(path, schedule, "schedule spec in the cleanupPolicy is not in proper cron format"))
}
return errs
}
// ValidateMatchExcludeConflict checks if the resultant of match and exclude block is not an empty set
func (spec *CleanupPolicySpec) ValidateMatchExcludeConflict(path *field.Path) (errs field.ErrorList) {
if len(spec.ExcludeResources.All) > 0 || len(spec.MatchResources.All) > 0 {
return errs
}
// if both have any then no resource should be common
if len(spec.MatchResources.Any) > 0 && len(spec.ExcludeResources.Any) > 0 {
for _, rmr := range spec.MatchResources.Any {
for _, rer := range spec.ExcludeResources.Any {
if reflect.DeepEqual(rmr, rer) {
return append(errs, field.Invalid(path, spec, "CleanupPolicy is matching an empty set"))
}
}
}
return errs
}
if reflect.DeepEqual(spec.ExcludeResources, kyvernov1.MatchResources{}) {
return errs
}
excludeRoles := sets.NewString(spec.ExcludeResources.Roles...)
excludeClusterRoles := sets.NewString(spec.ExcludeResources.ClusterRoles...)
excludeKinds := sets.NewString(spec.ExcludeResources.Kinds...)
excludeNamespaces := sets.NewString(spec.ExcludeResources.Namespaces...)
excludeSubjects := sets.NewString()
for _, subject := range spec.ExcludeResources.Subjects {
subjectRaw, _ := json.Marshal(subject)
excludeSubjects.Insert(string(subjectRaw))
}
excludeSelectorMatchExpressions := sets.NewString()
if spec.ExcludeResources.Selector != nil {
for _, matchExpression := range spec.ExcludeResources.Selector.MatchExpressions {
matchExpressionRaw, _ := json.Marshal(matchExpression)
excludeSelectorMatchExpressions.Insert(string(matchExpressionRaw))
}
}
excludeNamespaceSelectorMatchExpressions := sets.NewString()
if spec.ExcludeResources.NamespaceSelector != nil {
for _, matchExpression := range spec.ExcludeResources.NamespaceSelector.MatchExpressions {
matchExpressionRaw, _ := json.Marshal(matchExpression)
excludeNamespaceSelectorMatchExpressions.Insert(string(matchExpressionRaw))
}
}
if len(excludeRoles) > 0 {
if len(spec.MatchResources.Roles) == 0 || !excludeRoles.HasAll(spec.MatchResources.Roles...) {
return errs
}
}
if len(excludeClusterRoles) > 0 {
if len(spec.MatchResources.ClusterRoles) == 0 || !excludeClusterRoles.HasAll(spec.MatchResources.ClusterRoles...) {
return errs
}
}
if len(excludeSubjects) > 0 {
if len(spec.MatchResources.Subjects) == 0 {
return errs
}
for _, subject := range spec.MatchResources.UserInfo.Subjects {
subjectRaw, _ := json.Marshal(subject)
if !excludeSubjects.Has(string(subjectRaw)) {
return errs
}
}
}
if spec.ExcludeResources.Name != "" {
if !wildcard.Match(spec.ExcludeResources.Name, spec.MatchResources.Name) {
return errs
}
}
if len(spec.ExcludeResources.Names) > 0 {
excludeSlice := spec.ExcludeResources.Names
matchSlice := spec.MatchResources.Names
// if exclude block has something and match doesn't it means we
// have a non empty set
if len(spec.MatchResources.Names) == 0 {
return errs
}
// if *any* name in match and exclude conflicts
// we want user to fix that
for _, matchName := range matchSlice {
for _, excludeName := range excludeSlice {
if wildcard.Match(excludeName, matchName) {
return append(errs, field.Invalid(path, spec, "CleanupPolicy is matching an empty set"))
}
}
}
return errs
}
if len(excludeNamespaces) > 0 {
if len(spec.MatchResources.Namespaces) == 0 || !excludeNamespaces.HasAll(spec.MatchResources.Namespaces...) {
return errs
}
}
if len(excludeKinds) > 0 {
if len(spec.MatchResources.Kinds) == 0 || !excludeKinds.HasAll(spec.MatchResources.Kinds...) {
return errs
}
}
if spec.MatchResources.Selector != nil && spec.ExcludeResources.Selector != nil {
if len(excludeSelectorMatchExpressions) > 0 {
if len(spec.MatchResources.Selector.MatchExpressions) == 0 {
return errs
}
for _, matchExpression := range spec.MatchResources.Selector.MatchExpressions {
matchExpressionRaw, _ := json.Marshal(matchExpression)
if !excludeSelectorMatchExpressions.Has(string(matchExpressionRaw)) {
return errs
}
}
}
if len(spec.ExcludeResources.Selector.MatchLabels) > 0 {
if len(spec.MatchResources.Selector.MatchLabels) == 0 {
return errs
}
for label, value := range spec.MatchResources.Selector.MatchLabels {
if spec.ExcludeResources.Selector.MatchLabels[label] != value {
return errs
}
}
}
}
if spec.MatchResources.NamespaceSelector != nil && spec.ExcludeResources.NamespaceSelector != nil {
if len(excludeNamespaceSelectorMatchExpressions) > 0 {
if len(spec.MatchResources.NamespaceSelector.MatchExpressions) == 0 {
return errs
}
for _, matchExpression := range spec.MatchResources.NamespaceSelector.MatchExpressions {
matchExpressionRaw, _ := json.Marshal(matchExpression)
if !excludeNamespaceSelectorMatchExpressions.Has(string(matchExpressionRaw)) {
return errs
}
}
}
if len(spec.ExcludeResources.NamespaceSelector.MatchLabels) > 0 {
if len(spec.MatchResources.NamespaceSelector.MatchLabels) == 0 {
return errs
}
for label, value := range spec.MatchResources.NamespaceSelector.MatchLabels {
if spec.ExcludeResources.NamespaceSelector.MatchLabels[label] != value {
return errs
}
}
}
}
if (spec.MatchResources.Selector == nil && spec.ExcludeResources.Selector != nil) ||
(spec.MatchResources.Selector != nil && spec.ExcludeResources.Selector == nil) {
return errs
}
if (spec.MatchResources.NamespaceSelector == nil && spec.ExcludeResources.NamespaceSelector != nil) ||
(spec.MatchResources.NamespaceSelector != nil && spec.ExcludeResources.NamespaceSelector == nil) {
return errs
}
if spec.MatchResources.Annotations != nil && spec.ExcludeResources.Annotations != nil {
if !(reflect.DeepEqual(spec.MatchResources.Annotations, spec.ExcludeResources.Annotations)) {
return errs
}
}
if (spec.MatchResources.Annotations == nil && spec.ExcludeResources.Annotations != nil) ||
(spec.MatchResources.Annotations != nil && spec.ExcludeResources.Annotations == nil) {
return errs
}
return append(errs, field.Invalid(path, spec, "CleanupPolicy is matching an empty set"))
}

View file

@ -0,0 +1,5 @@
package logger
import "github.com/kyverno/kyverno/pkg/logging"
var Logger = logging.WithName("cleanupwebhooks")

1
cmd/cleanup/main.go Normal file
View file

@ -0,0 +1 @@
package main

View file

@ -0,0 +1,45 @@
package utils
import (
"encoding/json"
"fmt"
kyvernov1alpha1 "github.com/kyverno/kyverno/api/kyverno/v1alpha1"
admissionv1 "k8s.io/api/admission/v1"
)
func UnmarshalCleanupPolicy(kind string, raw []byte) (kyvernov1alpha1.CleanupPolicyInterface, error) {
var policy kyvernov1alpha1.CleanupPolicyInterface
if kind == "CleanupPolicy" {
if err := json.Unmarshal(raw, &policy); err != nil {
return policy, err
}
return policy, nil
}
return policy, fmt.Errorf("admission request does not contain a cleanuppolicy")
}
func GetCleanupPolicy(request *admissionv1.AdmissionRequest) (kyvernov1alpha1.CleanupPolicyInterface, error) {
return UnmarshalCleanupPolicy(request.Kind.Kind, request.Object.Raw)
}
func GetCleanupPolicies(request *admissionv1.AdmissionRequest) (kyvernov1alpha1.CleanupPolicyInterface, kyvernov1alpha1.CleanupPolicyInterface, error) {
var emptypolicy kyvernov1alpha1.CleanupPolicyInterface
policy, err := UnmarshalCleanupPolicy(request.Kind.Kind, request.Object.Raw)
if err != nil {
return policy, emptypolicy, err
}
if request.Operation == admissionv1.Update {
oldPolicy, err := UnmarshalCleanupPolicy(request.Kind.Kind, request.OldObject.Raw)
return policy, oldPolicy, err
}
return policy, emptypolicy, nil
}
func GetResourceName(request *admissionv1.AdmissionRequest) string {
resourceName := request.Kind.Kind + "/" + request.Name
if request.Namespace != "" {
resourceName = request.Namespace + "/" + resourceName
}
return resourceName
}

View file

@ -0,0 +1,98 @@
package validate
import (
"fmt"
"github.com/go-logr/logr"
kyvernov1alpha1 "github.com/kyverno/kyverno/api/kyverno/v1alpha1"
"github.com/kyverno/kyverno/cmd/cleanup/logger"
"github.com/kyverno/kyverno/pkg/clients/dclient"
"github.com/kyverno/kyverno/pkg/engine/variables"
"github.com/kyverno/kyverno/pkg/logging"
"github.com/kyverno/kyverno/pkg/openapi"
"github.com/kyverno/kyverno/pkg/policy/generate"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/client-go/discovery"
)
// Cleanup provides implementation to validate permission for using DELETE operation by CleanupPolicy
type Cleanup struct {
// rule to hold CleanupPolicy specifications
spec kyvernov1alpha1.CleanupPolicySpec
// authCheck to check access for operations
authCheck generate.Operations
// logger
log logr.Logger
}
// NewCleanup returns a new instance of Cleanup validation checker
func NewCleanup(client dclient.Interface, cleanup kyvernov1alpha1.CleanupPolicySpec, log logr.Logger) *Cleanup {
c := Cleanup{
spec: cleanup,
authCheck: generate.NewAuth(client, log),
log: log,
}
return &c
}
// canIDelete returns a error if kyverno cannot perform operations
func (c *Cleanup) CanIDelete(kind, namespace string) error {
// Skip if there is variable defined
authCheck := c.authCheck
if !variables.IsVariable(kind) && !variables.IsVariable(namespace) {
// DELETE
ok, err := authCheck.CanIDelete(kind, namespace)
if err != nil {
// machinery error
return err
}
if !ok {
return fmt.Errorf("kyverno does not have permissions to 'delete' resource %s/%s. Update permissions in ClusterRole", kind, namespace)
}
} else {
c.log.V(4).Info("name & namespace uses variables, so cannot be resolved. Skipping Auth Checks.")
}
return nil
}
// Validate checks the policy and rules declarations for required configurations
func ValidateCleanupPolicy(cleanuppolicy kyvernov1alpha1.CleanupPolicyInterface, client dclient.Interface, mock bool, openApiManager openapi.Manager) error {
namespace := cleanuppolicy.GetNamespace()
var res []*metav1.APIResourceList
clusterResources := sets.NewString()
// Get all the cluster type kind supported by cluster
res, err := discovery.ServerPreferredResources(client.Discovery().DiscoveryInterface())
if err != nil {
if discovery.IsGroupDiscoveryFailedError(err) {
err := err.(*discovery.ErrGroupDiscoveryFailed)
for gv, err := range err.Groups {
logger.Logger.Error(err, "failed to list api resources", "group", gv)
}
} else {
return err
}
}
for _, resList := range res {
for _, r := range resList.APIResources {
if !r.Namespaced {
clusterResources.Insert(r.Kind)
}
}
}
if errs := cleanuppolicy.Validate(clusterResources); len(errs) != 0 {
return errs.ToAggregate()
}
for kind := range clusterResources {
checker := NewCleanup(client, *cleanuppolicy.GetSpec(), logging.GlobalLogger())
if err := checker.CanIDelete(kind, namespace); err != nil {
return fmt.Errorf("cannot delete kind %s in namespace %s", kind, namespace)
}
}
return nil
}

View file

@ -4247,6 +4247,11 @@ CleanupPolicyStatus
</tbody> </tbody>
</table> </table>
<hr /> <hr />
<h3 id="kyverno.io/v1alpha1.CleanupPolicyInterface">CleanupPolicyInterface
</h3>
<p>
<p>CleanupPolicyInterface abstracts the concrete policy type (Policy vs ClusterPolicy)</p>
</p>
<h3 id="kyverno.io/v1alpha1.CleanupPolicySpec">CleanupPolicySpec <h3 id="kyverno.io/v1alpha1.CleanupPolicySpec">CleanupPolicySpec
</h3> </h3>
<p> <p>

1
go.mod
View file

@ -33,6 +33,7 @@ require (
github.com/onsi/gomega v1.22.1 github.com/onsi/gomega v1.22.1
github.com/orcaman/concurrent-map/v2 v2.0.0 github.com/orcaman/concurrent-map/v2 v2.0.0
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/robfig/cron v1.2.0
github.com/sigstore/cosign v1.13.0 github.com/sigstore/cosign v1.13.0
github.com/sigstore/k8s-manifest-sigstore v0.4.2 github.com/sigstore/k8s-manifest-sigstore v0.4.2
github.com/sigstore/sigstore v1.4.4 github.com/sigstore/sigstore v1.4.4

2
go.sum
View file

@ -1812,6 +1812,8 @@ github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5X
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=