1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2025-03-31 03:45:17 +00:00

refactor: clean engine api package (#6156)

* refactor: introduce engine api package

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* status

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* refactor: clean engine api package

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* cleanup

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

---------

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>
This commit is contained in:
Charles-Edouard Brétéché 2023-01-30 15:49:44 +01:00 committed by GitHub
parent 47b4a177dd
commit 892b8f921d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 1253 additions and 418 deletions

View file

@ -0,0 +1,130 @@
package api
import (
"reflect"
kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
utils "github.com/kyverno/kyverno/pkg/utils/match"
"github.com/kyverno/kyverno/pkg/utils/wildcard"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
// EngineResponse engine response to the action
type EngineResponse struct {
// PatchedResource is the resource patched with the engine action changes
PatchedResource unstructured.Unstructured
// Policy is the original policy
Policy kyvernov1.PolicyInterface
// PolicyResponse contains the engine policy response
PolicyResponse PolicyResponse
// NamespaceLabels given by policy context
NamespaceLabels map[string]string
}
// IsOneOf checks if any rule has status in a given list
func (er EngineResponse) IsOneOf(status ...RuleStatus) bool {
for _, r := range er.PolicyResponse.Rules {
if r.HasStatus(status...) {
return true
}
}
return false
}
// IsSuccessful checks if any rule has failed or produced an error during execution
func (er EngineResponse) IsSuccessful() bool {
return !er.IsOneOf(RuleStatusFail, RuleStatusError)
}
// IsSkipped checks if any rule has skipped resource or not.
func (er EngineResponse) IsSkipped() bool {
return er.IsOneOf(RuleStatusSkip)
}
// IsFailed checks if any rule created a policy violation
func (er EngineResponse) IsFailed() bool {
return er.IsOneOf(RuleStatusFail)
}
// IsError checks if any rule resulted in a processing error
func (er EngineResponse) IsError() bool {
return er.IsOneOf(RuleStatusError)
}
// IsEmpty checks if any rule results are present
func (er EngineResponse) IsEmpty() bool {
return len(er.PolicyResponse.Rules) == 0
}
// isNil checks if rule is an empty rule
func (er EngineResponse) IsNil() bool {
return reflect.DeepEqual(er, EngineResponse{})
}
// GetPatches returns all the patches joined
func (er EngineResponse) GetPatches() [][]byte {
var patches [][]byte
for _, r := range er.PolicyResponse.Rules {
if r.Patches != nil {
patches = append(patches, r.Patches...)
}
}
return patches
}
// GetFailedRules returns failed rules
func (er EngineResponse) GetFailedRules() []string {
return er.getRules(func(rule RuleResponse) bool { return rule.HasStatus(RuleStatusFail, RuleStatusError) })
}
// GetSuccessRules returns success rules
func (er EngineResponse) GetSuccessRules() []string {
return er.getRules(func(rule RuleResponse) bool { return rule.HasStatus(RuleStatusPass) })
}
// GetResourceSpec returns resourceSpec of er
func (er EngineResponse) GetResourceSpec() ResourceSpec {
return ResourceSpec{
Kind: er.PatchedResource.GetKind(),
APIVersion: er.PatchedResource.GetAPIVersion(),
Namespace: er.PatchedResource.GetNamespace(),
Name: er.PatchedResource.GetName(),
UID: string(er.PatchedResource.GetUID()),
}
}
func (er EngineResponse) getRules(predicate func(RuleResponse) bool) []string {
var rules []string
for _, r := range er.PolicyResponse.Rules {
if predicate(r) {
rules = append(rules, r.Name)
}
}
return rules
}
func (er *EngineResponse) GetValidationFailureAction() kyvernov1.ValidationFailureAction {
for _, v := range er.PolicyResponse.ValidationFailureActionOverrides {
if !v.Action.IsValid() {
continue
}
if v.Namespaces == nil {
hasPass, err := utils.CheckSelector(v.NamespaceSelector, er.NamespaceLabels)
if err == nil && hasPass {
return v.Action
}
}
for _, ns := range v.Namespaces {
if wildcard.Match(ns, er.PatchedResource.GetNamespace()) {
if v.NamespaceSelector == nil {
return v.Action
}
hasPass, err := utils.CheckSelector(v.NamespaceSelector, er.NamespaceLabels)
if err == nil && hasPass {
return v.Action
}
}
}
}
return er.PolicyResponse.ValidationFailureAction
}

View file

@ -0,0 +1,721 @@
package api
import (
"reflect"
"testing"
kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
func TestEngineResponse_IsEmpty(t *testing.T) {
type fields struct {
PatchedResource unstructured.Unstructured
Policy kyvernov1.PolicyInterface
PolicyResponse PolicyResponse
NamespaceLabels map[string]string
}
tests := []struct {
name string
fields fields
want bool
}{{
want: true,
}, {
fields: fields{
PolicyResponse: PolicyResponse{
Rules: []RuleResponse{{}},
},
},
want: false,
}, {
fields: fields{
NamespaceLabels: map[string]string{
"a": "b",
},
},
want: true,
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
er := EngineResponse{
PatchedResource: tt.fields.PatchedResource,
Policy: tt.fields.Policy,
PolicyResponse: tt.fields.PolicyResponse,
NamespaceLabels: tt.fields.NamespaceLabels,
}
if got := er.IsEmpty(); got != tt.want {
t.Errorf("EngineResponse.IsEmpty() = %v, want %v", got, tt.want)
}
})
}
}
func TestEngineResponse_IsNil(t *testing.T) {
type fields struct {
PatchedResource unstructured.Unstructured
Policy kyvernov1.PolicyInterface
PolicyResponse PolicyResponse
NamespaceLabels map[string]string
}
tests := []struct {
name string
fields fields
want bool
}{{
want: true,
}, {
fields: fields{
PolicyResponse: PolicyResponse{
Rules: []RuleResponse{{}},
},
},
want: false,
}, {
fields: fields{
NamespaceLabels: map[string]string{
"a": "b",
},
},
want: false,
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
er := EngineResponse{
PatchedResource: tt.fields.PatchedResource,
Policy: tt.fields.Policy,
PolicyResponse: tt.fields.PolicyResponse,
NamespaceLabels: tt.fields.NamespaceLabels,
}
if got := er.IsNil(); got != tt.want {
t.Errorf("EngineResponse.IsNil() = %v, want %v", got, tt.want)
}
})
}
}
func TestEngineResponse_IsOneOf(t *testing.T) {
type fields struct {
PatchedResource unstructured.Unstructured
Policy kyvernov1.PolicyInterface
PolicyResponse PolicyResponse
NamespaceLabels map[string]string
}
type args struct {
status []RuleStatus
}
tests := []struct {
name string
fields fields
args args
want bool
}{{
fields: fields{
PolicyResponse: PolicyResponse{
Rules: []RuleResponse{{
Status: RuleStatusFail,
}},
},
},
want: false,
}, {
fields: fields{
PolicyResponse: PolicyResponse{
Rules: []RuleResponse{{
Status: RuleStatusFail,
}},
},
},
args: args{
status: []RuleStatus{RuleStatusFail},
},
want: true,
}, {
fields: fields{
PolicyResponse: PolicyResponse{
Rules: []RuleResponse{{
Status: RuleStatusFail,
}},
},
},
args: args{
status: []RuleStatus{RuleStatusFail, RuleStatusPass},
},
want: true,
}, {
fields: fields{
PolicyResponse: PolicyResponse{
Rules: []RuleResponse{{
Status: RuleStatusFail,
}},
},
},
args: args{
status: []RuleStatus{RuleStatusPass},
},
want: false,
}, {
fields: fields{
PolicyResponse: PolicyResponse{
Rules: []RuleResponse{{
Status: RuleStatusFail,
}},
},
},
args: args{
status: []RuleStatus{},
},
want: false,
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
er := EngineResponse{
PatchedResource: tt.fields.PatchedResource,
Policy: tt.fields.Policy,
PolicyResponse: tt.fields.PolicyResponse,
NamespaceLabels: tt.fields.NamespaceLabels,
}
if got := er.IsOneOf(tt.args.status...); got != tt.want {
t.Errorf("EngineResponse.IsOneOf() = %v, want %v", got, tt.want)
}
})
}
}
func TestEngineResponse_IsSuccessful(t *testing.T) {
type fields struct {
PatchedResource unstructured.Unstructured
Policy kyvernov1.PolicyInterface
PolicyResponse PolicyResponse
NamespaceLabels map[string]string
}
tests := []struct {
name string
fields fields
want bool
}{{
want: true,
}, {
fields: fields{
PolicyResponse: PolicyResponse{
Rules: []RuleResponse{{
Status: RuleStatusPass,
}},
},
},
want: true,
}, {
fields: fields{
PolicyResponse: PolicyResponse{
Rules: []RuleResponse{{
Status: RuleStatusFail,
}},
},
},
want: false,
}, {
fields: fields{
PolicyResponse: PolicyResponse{
Rules: []RuleResponse{{
Status: RuleStatusWarn,
}},
},
},
want: true,
}, {
fields: fields{
PolicyResponse: PolicyResponse{
Rules: []RuleResponse{{
Status: RuleStatusError,
}},
},
},
want: false,
}, {
fields: fields{
PolicyResponse: PolicyResponse{
Rules: []RuleResponse{{
Status: RuleStatusSkip,
}},
},
},
want: true,
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
er := EngineResponse{
PatchedResource: tt.fields.PatchedResource,
Policy: tt.fields.Policy,
PolicyResponse: tt.fields.PolicyResponse,
NamespaceLabels: tt.fields.NamespaceLabels,
}
if got := er.IsSuccessful(); got != tt.want {
t.Errorf("EngineResponse.IsSuccessful() = %v, want %v", got, tt.want)
}
})
}
}
func TestEngineResponse_IsSkipped(t *testing.T) {
type fields struct {
PatchedResource unstructured.Unstructured
Policy kyvernov1.PolicyInterface
PolicyResponse PolicyResponse
NamespaceLabels map[string]string
}
tests := []struct {
name string
fields fields
want bool
}{{
want: false,
}, {
fields: fields{
PolicyResponse: PolicyResponse{
Rules: []RuleResponse{{
Status: RuleStatusPass,
}},
},
},
want: false,
}, {
fields: fields{
PolicyResponse: PolicyResponse{
Rules: []RuleResponse{{
Status: RuleStatusFail,
}},
},
},
want: false,
}, {
fields: fields{
PolicyResponse: PolicyResponse{
Rules: []RuleResponse{{
Status: RuleStatusWarn,
}},
},
},
want: false,
}, {
fields: fields{
PolicyResponse: PolicyResponse{
Rules: []RuleResponse{{
Status: RuleStatusError,
}},
},
},
want: false,
}, {
fields: fields{
PolicyResponse: PolicyResponse{
Rules: []RuleResponse{{
Status: RuleStatusSkip,
}},
},
},
want: true,
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
er := EngineResponse{
PatchedResource: tt.fields.PatchedResource,
Policy: tt.fields.Policy,
PolicyResponse: tt.fields.PolicyResponse,
NamespaceLabels: tt.fields.NamespaceLabels,
}
if got := er.IsSkipped(); got != tt.want {
t.Errorf("EngineResponse.IsSkipped() = %v, want %v", got, tt.want)
}
})
}
}
func TestEngineResponse_IsFailed(t *testing.T) {
type fields struct {
PatchedResource unstructured.Unstructured
Policy kyvernov1.PolicyInterface
PolicyResponse PolicyResponse
NamespaceLabels map[string]string
}
tests := []struct {
name string
fields fields
want bool
}{{
want: false,
}, {
fields: fields{
PolicyResponse: PolicyResponse{
Rules: []RuleResponse{{
Status: RuleStatusPass,
}},
},
},
want: false,
}, {
fields: fields{
PolicyResponse: PolicyResponse{
Rules: []RuleResponse{{
Status: RuleStatusFail,
}},
},
},
want: true,
}, {
fields: fields{
PolicyResponse: PolicyResponse{
Rules: []RuleResponse{{
Status: RuleStatusWarn,
}},
},
},
want: false,
}, {
fields: fields{
PolicyResponse: PolicyResponse{
Rules: []RuleResponse{{
Status: RuleStatusError,
}},
},
},
want: false,
}, {
fields: fields{
PolicyResponse: PolicyResponse{
Rules: []RuleResponse{{
Status: RuleStatusSkip,
}},
},
},
want: false,
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
er := EngineResponse{
PatchedResource: tt.fields.PatchedResource,
Policy: tt.fields.Policy,
PolicyResponse: tt.fields.PolicyResponse,
NamespaceLabels: tt.fields.NamespaceLabels,
}
if got := er.IsFailed(); got != tt.want {
t.Errorf("EngineResponse.IsFailed() = %v, want %v", got, tt.want)
}
})
}
}
func TestEngineResponse_IsError(t *testing.T) {
type fields struct {
PatchedResource unstructured.Unstructured
Policy kyvernov1.PolicyInterface
PolicyResponse PolicyResponse
NamespaceLabels map[string]string
}
tests := []struct {
name string
fields fields
want bool
}{{
want: false,
}, {
fields: fields{
PolicyResponse: PolicyResponse{
Rules: []RuleResponse{{
Status: RuleStatusPass,
}},
},
},
want: false,
}, {
fields: fields{
PolicyResponse: PolicyResponse{
Rules: []RuleResponse{{
Status: RuleStatusFail,
}},
},
},
want: false,
}, {
fields: fields{
PolicyResponse: PolicyResponse{
Rules: []RuleResponse{{
Status: RuleStatusWarn,
}},
},
},
want: false,
}, {
fields: fields{
PolicyResponse: PolicyResponse{
Rules: []RuleResponse{{
Status: RuleStatusError,
}},
},
},
want: true,
}, {
fields: fields{
PolicyResponse: PolicyResponse{
Rules: []RuleResponse{{
Status: RuleStatusSkip,
}},
},
},
want: false,
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
er := EngineResponse{
PatchedResource: tt.fields.PatchedResource,
Policy: tt.fields.Policy,
PolicyResponse: tt.fields.PolicyResponse,
NamespaceLabels: tt.fields.NamespaceLabels,
}
if got := er.IsError(); got != tt.want {
t.Errorf("EngineResponse.IsError() = %v, want %v", got, tt.want)
}
})
}
}
func TestEngineResponse_GetFailedRules(t *testing.T) {
type fields struct {
PatchedResource unstructured.Unstructured
Policy kyvernov1.PolicyInterface
PolicyResponse PolicyResponse
NamespaceLabels map[string]string
}
tests := []struct {
name string
fields fields
want []string
}{{
fields: fields{
PolicyResponse: PolicyResponse{
Rules: []RuleResponse{{
Name: "skip",
Status: RuleStatusSkip,
}},
},
},
}, {
fields: fields{
PolicyResponse: PolicyResponse{
Rules: []RuleResponse{{
Name: "warn",
Status: RuleStatusWarn,
}},
},
},
}, {
fields: fields{
PolicyResponse: PolicyResponse{
Rules: []RuleResponse{{
Name: "pass",
Status: RuleStatusPass,
}},
},
},
}, {
fields: fields{
PolicyResponse: PolicyResponse{
Rules: []RuleResponse{{
Name: "fail",
Status: RuleStatusFail,
}},
},
},
want: []string{"fail"},
}, {
fields: fields{
PolicyResponse: PolicyResponse{
Rules: []RuleResponse{{
Name: "fail-1",
Status: RuleStatusFail,
}, {
Name: "fail-2",
Status: RuleStatusFail,
}},
},
},
want: []string{"fail-1", "fail-2"},
}, {
fields: fields{
PolicyResponse: PolicyResponse{
Rules: []RuleResponse{{
Name: "fail-1",
Status: RuleStatusFail,
}, {
Name: "error-1",
Status: RuleStatusError,
}},
},
},
want: []string{"fail-1", "error-1"},
}, {
fields: fields{
PolicyResponse: PolicyResponse{
Rules: []RuleResponse{{
Name: "error-1",
Status: RuleStatusError,
}, {
Name: "error-2",
Status: RuleStatusError,
}},
},
},
want: []string{"error-1", "error-2"},
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
er := EngineResponse{
PatchedResource: tt.fields.PatchedResource,
Policy: tt.fields.Policy,
PolicyResponse: tt.fields.PolicyResponse,
NamespaceLabels: tt.fields.NamespaceLabels,
}
if got := er.GetFailedRules(); !reflect.DeepEqual(got, tt.want) {
t.Errorf("EngineResponse.GetFailedRules() = %v, want %v", got, tt.want)
}
})
}
}
func TestEngineResponse_GetSuccessRules(t *testing.T) {
type fields struct {
PatchedResource unstructured.Unstructured
Policy kyvernov1.PolicyInterface
PolicyResponse PolicyResponse
NamespaceLabels map[string]string
}
tests := []struct {
name string
fields fields
want []string
}{{
fields: fields{
PolicyResponse: PolicyResponse{
Rules: []RuleResponse{{
Name: "skip",
Status: RuleStatusSkip,
}},
},
},
}, {
fields: fields{
PolicyResponse: PolicyResponse{
Rules: []RuleResponse{{
Name: "warn",
Status: RuleStatusWarn,
}},
},
},
}, {
fields: fields{
PolicyResponse: PolicyResponse{
Rules: []RuleResponse{{
Name: "pass-1",
Status: RuleStatusPass,
}, {
Name: "pass-2",
Status: RuleStatusPass,
}},
},
},
want: []string{"pass-1", "pass-2"},
}, {
fields: fields{
PolicyResponse: PolicyResponse{
Rules: []RuleResponse{{
Name: "pass",
Status: RuleStatusPass,
}},
},
},
want: []string{"pass"},
}, {
fields: fields{
PolicyResponse: PolicyResponse{
Rules: []RuleResponse{{
Name: "pass",
Status: RuleStatusPass,
}, {
Name: "fail",
Status: RuleStatusFail,
}},
},
},
want: []string{"pass"},
}, {
fields: fields{
PolicyResponse: PolicyResponse{
Rules: []RuleResponse{{
Name: "pass",
Status: RuleStatusPass,
}, {
Name: "skip",
Status: RuleStatusSkip,
}},
},
},
want: []string{"pass"},
}, {
fields: fields{
PolicyResponse: PolicyResponse{
Rules: []RuleResponse{{
Name: "fail",
Status: RuleStatusFail,
}},
},
},
}, {
fields: fields{
PolicyResponse: PolicyResponse{
Rules: []RuleResponse{{
Name: "fail-1",
Status: RuleStatusFail,
}, {
Name: "fail-2",
Status: RuleStatusFail,
}},
},
},
}, {
fields: fields{
PolicyResponse: PolicyResponse{
Rules: []RuleResponse{{
Name: "fail-1",
Status: RuleStatusFail,
}, {
Name: "error-1",
Status: RuleStatusError,
}},
},
},
}, {
fields: fields{
PolicyResponse: PolicyResponse{
Rules: []RuleResponse{{
Name: "error-1",
Status: RuleStatusError,
}, {
Name: "error-2",
Status: RuleStatusError,
}},
},
},
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
er := EngineResponse{
PatchedResource: tt.fields.PatchedResource,
Policy: tt.fields.Policy,
PolicyResponse: tt.fields.PolicyResponse,
NamespaceLabels: tt.fields.NamespaceLabels,
}
if got := er.GetSuccessRules(); !reflect.DeepEqual(got, tt.want) {
t.Errorf("EngineResponse.GetSuccessRules() = %v, want %v", got, tt.want)
}
})
}
}

View file

@ -0,0 +1,28 @@
package api
import (
kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type ValidationFailureActionOverride struct {
Action kyvernov1.ValidationFailureAction
Namespaces []string
NamespaceSelector *metav1.LabelSelector
}
// PolicyResponse policy application response
type PolicyResponse struct {
// Policy contains policy details
Policy PolicySpec
// Resource contains resource details
Resource ResourceSpec
// PolicyStats contains policy statistics
PolicyStats
// Rules contains policy rules responses
Rules []RuleResponse
// ValidationFailureAction audit (default) or enforce
ValidationFailureAction kyvernov1.ValidationFailureAction
// ValidationFailureActionOverrides overrides
ValidationFailureActionOverrides []ValidationFailureActionOverride
}

View file

@ -0,0 +1,7 @@
package api
// PolicySpec policy
type PolicySpec struct {
Name string
Namespace string
}

View file

@ -0,0 +1,15 @@
package api
// ResourceSpec resource action applied on
type ResourceSpec struct {
Kind string
APIVersion string
Namespace string
Name string
UID string
}
// String implements Stringer interface
func (rs ResourceSpec) String() string {
return rs.Kind + "/" + rs.Namespace + "/" + rs.Name
}

View file

@ -0,0 +1,45 @@
package api
import "testing"
func TestResourceSpec_GetKey(t *testing.T) {
type fields struct {
Kind string
APIVersion string
Namespace string
Name string
UID string
}
tests := []struct {
name string
fields fields
want string
}{{
fields: fields{
Kind: "Pod",
Namespace: "test",
Name: "nignx",
},
want: "Pod/test/nignx",
}, {
fields: fields{
Kind: "ClusterRole",
Name: "admin",
},
want: "ClusterRole//admin",
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
rs := ResourceSpec{
Kind: tt.fields.Kind,
APIVersion: tt.fields.APIVersion,
Namespace: tt.fields.Namespace,
Name: tt.fields.Name,
UID: tt.fields.UID,
}
if got := rs.String(); got != tt.want {
t.Errorf("ResourceSpec.GetKey() = %v, want %v", got, tt.want)
}
})
}
}

View file

@ -1,287 +0,0 @@
package api
import (
"fmt"
"reflect"
"time"
kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
pssutils "github.com/kyverno/kyverno/pkg/pss/utils"
utils "github.com/kyverno/kyverno/pkg/utils/match"
"github.com/kyverno/kyverno/pkg/utils/wildcard"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/pod-security-admission/api"
)
// EngineResponse engine response to the action
type EngineResponse struct {
// Resource patched with the engine action changes
PatchedResource unstructured.Unstructured
// Original policy
Policy kyvernov1.PolicyInterface
// Policy Response
PolicyResponse PolicyResponse
// NamespaceLabels given by policy context
NamespaceLabels map[string]string
}
// PolicyResponse policy application response
type PolicyResponse struct {
// policy details
Policy PolicySpec `json:"policy"`
// resource details
Resource ResourceSpec `json:"resource"`
// policy statistics
PolicyStats `json:",inline"`
// rule response
Rules []RuleResponse `json:"rules"`
// ValidationFailureAction: audit (default) or enforce
ValidationFailureAction kyvernov1.ValidationFailureAction
ValidationFailureActionOverrides []ValidationFailureActionOverride
}
// PolicySpec policy
type PolicySpec struct {
Name string `json:"name"`
Namespace string `json:"namespace"`
}
// ResourceSpec resource action applied on
type ResourceSpec struct {
Kind string `json:"kind"`
APIVersion string `json:"apiVersion"`
Namespace string `json:"namespace"`
Name string `json:"name"`
// UID is not used to build the unique identifier
// optional
UID string `json:"uid"`
}
// GetKey returns the key
func (rs ResourceSpec) GetKey() string {
return rs.Kind + "/" + rs.Namespace + "/" + rs.Name
}
// PolicyStats stores statistics for the single policy application
type PolicyStats struct {
// time required to process the policy rules on a resource
ProcessingTime time.Duration `json:"processingTime"`
// Count of rules that were applied successfully
RulesAppliedCount int `json:"rulesAppliedCount"`
// Count of rules that with execution errors
RulesErrorCount int `json:"rulesErrorCount"`
// Timestamp of the instant the Policy was triggered
PolicyExecutionTimestamp int64 `json:"policyExecutionTimestamp"`
}
type RuleType string
const (
// Mutation type for mutation rule
Mutation RuleType = "Mutation"
// Validation type for validation rule
Validation RuleType = "Validation"
// Generation type for generation rule
Generation RuleType = "Generation"
// ImageVerify type for image verification
ImageVerify RuleType = "ImageVerify"
)
// RuleResponse details for each rule application
type RuleResponse struct {
// rule name specified in policy
Name string `json:"name"`
// rule type (Mutation,Generation,Validation) for Kyverno Policy
Type RuleType `json:"type"`
// message response from the rule application
Message string `json:"message"`
// JSON patches, for mutation rules
Patches [][]byte `json:"patches,omitempty"`
// Resource generated by the generate rules of a policy
GeneratedResource unstructured.Unstructured `json:"generatedResource,omitempty"`
// rule status
Status RuleStatus `json:"status"`
// statistics
RuleStats `json:",inline"`
// PatchedTarget is the patched resource for mutate.targets
PatchedTarget *unstructured.Unstructured
// PatchedTargetSubresourceName is the name of the subresource which is patched, empty if the resource patched is
// not a subresource.
PatchedTargetSubresourceName string
// PatchedTargetParentResourceGVR is the GVR of the parent resource of the PatchedTarget. This is only populated
// when PatchedTarget is a subresource.
PatchedTargetParentResourceGVR metav1.GroupVersionResource
// PodSecurityChecks contains pod security checks (only if this is a pod security rule)
PodSecurityChecks *PodSecurityChecks
}
type PodSecurityChecks struct {
Level api.Level
Version string
Checks []pssutils.PSSCheckResult
}
// ToString ...
func (rr RuleResponse) ToString() string {
return fmt.Sprintf("rule %s (%s): %v", rr.Name, rr.Type, rr.Message)
}
// RuleStats stores the statistics for the single rule application
type RuleStats struct {
// time required to apply the rule on the resource
ProcessingTime time.Duration `json:"processingTime"`
// Timestamp of the instant the rule got triggered
RuleExecutionTimestamp int64 `json:"ruleExecutionTimestamp"`
}
// IsSuccessful checks if any rule has failed or produced an error during execution
func (er EngineResponse) IsSuccessful() bool {
for _, r := range er.PolicyResponse.Rules {
if r.Status == RuleStatusFail || r.Status == RuleStatusError {
return false
}
}
return true
}
// IsSkipped checks if any rule has skipped resource or not.
func (er EngineResponse) IsSkipped() bool {
for _, r := range er.PolicyResponse.Rules {
if r.Status == RuleStatusSkip {
return true
}
}
return false
}
// IsFailed checks if any rule created a policy violation
func (er EngineResponse) IsFailed() bool {
for _, r := range er.PolicyResponse.Rules {
if r.Status == RuleStatusFail {
return true
}
}
return false
}
// IsError checks if any rule resulted in a processing error
func (er EngineResponse) IsError() bool {
for _, r := range er.PolicyResponse.Rules {
if r.Status == RuleStatusError {
return true
}
}
return false
}
// IsEmpty checks if any rule results are present
func (er EngineResponse) IsEmpty() bool {
return len(er.PolicyResponse.Rules) == 0
}
// isNil checks if rule is an empty rule
func (er EngineResponse) IsNil() bool {
return reflect.DeepEqual(er, EngineResponse{})
}
// GetPatches returns all the patches joined
func (er EngineResponse) GetPatches() [][]byte {
var patches [][]byte
for _, r := range er.PolicyResponse.Rules {
if r.Patches != nil {
patches = append(patches, r.Patches...)
}
}
return patches
}
// GetFailedRules returns failed rules
func (er EngineResponse) GetFailedRules() []string {
return er.getRules(func(status RuleStatus) bool { return status == RuleStatusFail || status == RuleStatusError })
}
// GetSuccessRules returns success rules
func (er EngineResponse) GetSuccessRules() []string {
return er.getRules(func(status RuleStatus) bool { return status == RuleStatusPass })
}
// GetResourceSpec returns resourceSpec of er
func (er EngineResponse) GetResourceSpec() ResourceSpec {
return ResourceSpec{
Kind: er.PatchedResource.GetKind(),
APIVersion: er.PatchedResource.GetAPIVersion(),
Namespace: er.PatchedResource.GetNamespace(),
Name: er.PatchedResource.GetName(),
UID: string(er.PatchedResource.GetUID()),
}
}
func (er EngineResponse) getRules(predicate func(RuleStatus) bool) []string {
var rules []string
for _, r := range er.PolicyResponse.Rules {
if predicate(r.Status) {
rules = append(rules, r.Name)
}
}
return rules
}
func (er *EngineResponse) GetValidationFailureAction() kyvernov1.ValidationFailureAction {
for _, v := range er.PolicyResponse.ValidationFailureActionOverrides {
if !v.Action.IsValid() {
continue
}
if v.Namespaces == nil {
hasPass, err := utils.CheckSelector(v.NamespaceSelector, er.NamespaceLabels)
if err == nil && hasPass {
return v.Action
}
}
for _, ns := range v.Namespaces {
if wildcard.Match(ns, er.PatchedResource.GetNamespace()) {
if v.NamespaceSelector == nil {
return v.Action
}
hasPass, err := utils.CheckSelector(v.NamespaceSelector, er.NamespaceLabels)
if err == nil && hasPass {
return v.Action
}
}
}
}
return er.PolicyResponse.ValidationFailureAction
}
type ValidationFailureActionOverride struct {
Action kyvernov1.ValidationFailureAction `json:"action"`
Namespaces []string `json:"namespaces,omitempty"`
NamespaceSelector *metav1.LabelSelector `json:"namespaceSelector,omitempty" yaml:"namespaceSelector,omitempty"`
}

View file

@ -1,31 +0,0 @@
package api
import (
"testing"
"gopkg.in/yaml.v2"
"gotest.tools/assert"
)
var sourceYAML = `
policy:
name: disallow-bind-mounts
resource:
kind: Pod
apiVersion: v1
name: image-with-hostpath
rules:
- name: validate-hostPath
type: Validation
status: fail
`
func Test_parse_yaml(t *testing.T) {
var pr PolicyResponse
if err := yaml.Unmarshal([]byte(sourceYAML), &pr); err != nil {
t.Errorf("failed to parse YAML: %v", err)
return
}
assert.Equal(t, 1, len(pr.Rules))
assert.Equal(t, RuleStatusFail, pr.Rules[0].Status)
}

View file

@ -0,0 +1,61 @@
package api
import (
"fmt"
pssutils "github.com/kyverno/kyverno/pkg/pss/utils"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/pod-security-admission/api"
)
// PodSecurityChecks details about pod securty checks
type PodSecurityChecks struct {
// Level is the pod security level
Level api.Level
// Version is the pod security version
Version string
// Checks contains check result details
Checks []pssutils.PSSCheckResult
}
// RuleResponse details for each rule application
type RuleResponse struct {
// Name is the rule name specified in policy
Name string
// Type is the rule type (Mutation,Generation,Validation) for Kyverno Policy
Type RuleType
// Message is the message response from the rule application
Message string
// Patches are JSON patches, for mutation rules
Patches [][]byte
// GeneratedResource is the generated by the generate rules of a policy
GeneratedResource unstructured.Unstructured
// Status rule status
Status RuleStatus
// ExecutionStats statistics
ExecutionStats
// PatchedTarget is the patched resource for mutate.targets
PatchedTarget *unstructured.Unstructured
// PatchedTargetSubresourceName is the name of the subresource which is patched, empty if the resource patched is not a subresource.
PatchedTargetSubresourceName string
// PatchedTargetParentResourceGVR is the GVR of the parent resource of the PatchedTarget. This is only populated when PatchedTarget is a subresource.
PatchedTargetParentResourceGVR metav1.GroupVersionResource
// PodSecurityChecks contains pod security checks (only if this is a pod security rule)
PodSecurityChecks *PodSecurityChecks
}
// HasStatus checks if rule status is in a given list
func (r RuleResponse) HasStatus(status ...RuleStatus) bool {
for _, s := range status {
if r.Status == s {
return true
}
}
return false
}
// String implements Stringer interface
func (r RuleResponse) String() string {
return fmt.Sprintf("rule %s (%s): %v", r.Name, r.Type, r.Message)
}

View file

@ -0,0 +1,159 @@
package api
import (
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
func TestRuleResponse_String(t *testing.T) {
type fields struct {
Name string
Type RuleType
Message string
Patches [][]byte
GeneratedResource unstructured.Unstructured
Status RuleStatus
ExecutionStats ExecutionStats
PatchedTarget *unstructured.Unstructured
PatchedTargetSubresourceName string
PatchedTargetParentResourceGVR metav1.GroupVersionResource
PodSecurityChecks *PodSecurityChecks
}
tests := []struct {
name string
fields fields
want string
}{{
fields: fields{
Name: "test-mutation",
Type: Mutation,
Message: "message",
},
want: "rule test-mutation (Mutation): message",
}, {
fields: fields{
Name: "test-validation",
Type: Validation,
Message: "message",
},
want: "rule test-validation (Validation): message",
}, {
fields: fields{
Name: "test-generation",
Type: Generation,
Message: "message",
},
want: "rule test-generation (Generation): message",
}, {
fields: fields{
Name: "test-image-verify",
Type: ImageVerify,
Message: "message",
},
want: "rule test-image-verify (ImageVerify): message",
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
rr := RuleResponse{
Name: tt.fields.Name,
Type: tt.fields.Type,
Message: tt.fields.Message,
Patches: tt.fields.Patches,
GeneratedResource: tt.fields.GeneratedResource,
Status: tt.fields.Status,
ExecutionStats: tt.fields.ExecutionStats,
PatchedTarget: tt.fields.PatchedTarget,
PatchedTargetSubresourceName: tt.fields.PatchedTargetSubresourceName,
PatchedTargetParentResourceGVR: tt.fields.PatchedTargetParentResourceGVR,
PodSecurityChecks: tt.fields.PodSecurityChecks,
}
if got := rr.String(); got != tt.want {
t.Errorf("RuleResponse.ToString() = %v, want %v", got, tt.want)
}
})
}
}
func TestRuleResponse_HasStatus(t *testing.T) {
type fields struct {
Name string
Type RuleType
Message string
Patches [][]byte
GeneratedResource unstructured.Unstructured
Status RuleStatus
ExecutionStats ExecutionStats
PatchedTarget *unstructured.Unstructured
PatchedTargetSubresourceName string
PatchedTargetParentResourceGVR metav1.GroupVersionResource
PodSecurityChecks *PodSecurityChecks
}
type args struct {
status []RuleStatus
}
tests := []struct {
name string
fields fields
args args
want bool
}{{
fields: fields{
Status: RuleStatusFail,
},
args: args{
status: []RuleStatus{RuleStatusFail},
},
want: true,
}, {
fields: fields{
Status: RuleStatusFail,
},
args: args{
status: []RuleStatus{RuleStatusError},
},
want: false,
}, {
fields: fields{
Status: RuleStatusFail,
},
args: args{
status: []RuleStatus{RuleStatusError, RuleStatusPass, RuleStatusFail},
},
want: true,
}, {
fields: fields{
Status: RuleStatusFail,
},
args: args{
status: []RuleStatus{},
},
want: false,
}, {
fields: fields{
Status: RuleStatusFail,
},
want: false,
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := RuleResponse{
Name: tt.fields.Name,
Type: tt.fields.Type,
Message: tt.fields.Message,
Patches: tt.fields.Patches,
GeneratedResource: tt.fields.GeneratedResource,
Status: tt.fields.Status,
ExecutionStats: tt.fields.ExecutionStats,
PatchedTarget: tt.fields.PatchedTarget,
PatchedTargetSubresourceName: tt.fields.PatchedTargetSubresourceName,
PatchedTargetParentResourceGVR: tt.fields.PatchedTargetParentResourceGVR,
PodSecurityChecks: tt.fields.PodSecurityChecks,
}
if got := r.HasStatus(tt.args.status...); got != tt.want {
t.Errorf("RuleResponse.HasStatus() = %v, want %v", got, tt.want)
}
})
}
}

View file

@ -0,0 +1,21 @@
package api
// RuleStatus represents the status of rule execution
type RuleStatus string
const (
// RuleStatusPass indicates that the resources meets the policy rule requirements
RuleStatusPass RuleStatus = "pass"
// RuleStatusFail indicates that the resource does not meet the policy rule requirements
RuleStatusFail RuleStatus = "fail"
// RuleStatusWarn indicates that the resource does not meet the policy rule requirements, but the policy is not scored
RuleStatusWarn RuleStatus = "warning"
// RuleStatusError indicates that the policy rule could not be evaluated due to a processing error, for
// example when a variable cannot be resolved in the policy rule definition. Note that variables
// that cannot be resolved in preconditions are replaced with empty values to allow existence
// checks.
RuleStatusError RuleStatus = "error"
// RuleStatusSkip indicates that the policy rule was not selected based on user inputs or applicability, for example
// when preconditions are not met, or when conditional or global anchors are not satistied.
RuleStatusSkip RuleStatus = "skip"
)

View file

@ -0,0 +1,15 @@
package api
// RuleType represents the type of a rule
type RuleType string
const (
// Mutation type for mutation rule
Mutation RuleType = "Mutation"
// Validation type for validation rule
Validation RuleType = "Validation"
// Generation type for generation rule
Generation RuleType = "Generation"
// ImageVerify type for image verification
ImageVerify RuleType = "ImageVerify"
)

23
pkg/engine/api/stats.go Normal file
View file

@ -0,0 +1,23 @@
package api
import (
"time"
)
// ExecutionStats stores the statistics for the single policy/rule application
type ExecutionStats struct {
// ProcessingTime is the time required to apply the policy/rule on the resource
ProcessingTime time.Duration
// Timestamp of the instant the policy/rule got triggered
Timestamp int64
}
// PolicyStats stores statistics for the single policy application
type PolicyStats struct {
// ExecutionStats policy execution stats
ExecutionStats
// RulesAppliedCount is the count of rules that were applied successfully
RulesAppliedCount int
// RulesErrorCount is the count of rules that with execution errors
RulesErrorCount int
}

View file

@ -1,76 +0,0 @@
package api
// RuleStatus represents the status of rule execution
type RuleStatus string
// RuleStatusPass is used to report the result of processing a rule.
const (
// RuleStatusPass indicates that the resources meets the policy rule requirements
RuleStatusPass RuleStatus = "pass"
// RuleStatusFail indicates that the resource does not meet the policy rule requirements
RuleStatusFail RuleStatus = "fail"
// RuleStatusWarn indicates that the resource does not meet the policy rule requirements, but the policy is not scored
RuleStatusWarn RuleStatus = "warning"
// RuleStatusError indicates that the policy rule could not be evaluated due to a processing error, for
// example when a variable cannot be resolved in the policy rule definition. Note that variables
// that cannot be resolved in preconditions are replaced with empty values to allow existence
// checks.
RuleStatusError RuleStatus = "error"
// RuleStatusSkip indicates that the policy rule was not selected based on user inputs or applicability, for example
// when preconditions are not met, or when conditional or global anchors are not satistied.
RuleStatusSkip RuleStatus = "skip"
)
// // String implements Stringer interface
// func (s RuleStatus) String() string {
// return toString[s]
// }
// // MarshalJSON marshals the enum as a quoted json string
// func (s *RuleStatus) MarshalJSON() ([]byte, error) {
// var b strings.Builder
// fmt.Fprintf(&b, "\"%s\"", toString[*s])
// return []byte(b.String()), nil
// }
// // UnmarshalJSON unmarshals a quoted json string to the enum value
// func (s *RuleStatus) UnmarshalJSON(b []byte) error {
// var strVal string
// err := json.Unmarshal(b, &strVal)
// if err != nil {
// return err
// }
// statusVal, err := getRuleStatus(strVal)
// if err != nil {
// return err
// }
// *s = *statusVal
// return nil
// }
// func getRuleStatus(s string) (*RuleStatus, error) {
// for k, v := range toID {
// if s == k {
// return &v, nil
// }
// }
// return nil, fmt.Errorf("invalid status: %s", s)
// }
// func (s *RuleStatus) UnmarshalYAML(unmarshal func(interface{}) error) error {
// var str string
// if err := unmarshal(&str); err != nil {
// return err
// }
// statusVal, err := getRuleStatus(str)
// if err != nil {
// return err
// }
// *s = *statusVal
// return nil
// }

View file

@ -35,7 +35,9 @@ func filterRules(rclient registryclient.Client, policyContext *PolicyContext, st
Namespace: policyContext.policy.GetNamespace(),
},
PolicyStats: engineapi.PolicyStats{
PolicyExecutionTimestamp: startTime.Unix(),
ExecutionStats: engineapi.ExecutionStats{
Timestamp: startTime.Unix(),
},
},
Resource: engineapi.ResourceSpec{
Kind: kind,
@ -106,9 +108,9 @@ func filterRule(rclient registryclient.Client, rule kyvernov1.Rule, policyContex
Name: rule.Name,
Type: ruleType,
Status: engineapi.RuleStatusFail,
RuleStats: engineapi.RuleStats{
ProcessingTime: time.Since(startTime),
RuleExecutionTimestamp: startTime.Unix(),
ExecutionStats: engineapi.ExecutionStats{
ProcessingTime: time.Since(startTime),
Timestamp: startTime.Unix(),
},
}
}
@ -147,9 +149,9 @@ func filterRule(rclient registryclient.Client, rule kyvernov1.Rule, policyContex
Name: ruleCopy.Name,
Type: ruleType,
Status: engineapi.RuleStatusSkip,
RuleStats: engineapi.RuleStats{
ProcessingTime: time.Since(startTime),
RuleExecutionTimestamp: startTime.Unix(),
ExecutionStats: engineapi.ExecutionStats{
ProcessingTime: time.Since(startTime),
Timestamp: startTime.Unix(),
},
}
}
@ -159,9 +161,9 @@ func filterRule(rclient registryclient.Client, rule kyvernov1.Rule, policyContex
Name: ruleCopy.Name,
Type: ruleType,
Status: engineapi.RuleStatusPass,
RuleStats: engineapi.RuleStats{
ProcessingTime: time.Since(startTime),
RuleExecutionTimestamp: startTime.Unix(),
ExecutionStats: engineapi.ExecutionStats{
ProcessingTime: time.Since(startTime),
Timestamp: startTime.Unix(),
},
}
}

View file

@ -34,7 +34,9 @@ func filterGenerateRules(rclient registryclient.Client, policyContext *PolicyCon
Namespace: pNamespace,
},
PolicyStats: engineapi.PolicyStats{
PolicyExecutionTimestamp: startTime.Unix(),
ExecutionStats: engineapi.ExecutionStats{
Timestamp: startTime.Unix(),
},
},
Resource: engineapi.ResourceSpec{
Kind: kind,

View file

@ -19,9 +19,9 @@ func ProcessPatchJSON6902(ruleName string, patchesJSON6902 []byte, resource unst
resp.Name = ruleName
resp.Type = engineapi.Mutation
defer func() {
resp.RuleStats.ProcessingTime = time.Since(startTime)
resp.RuleStats.RuleExecutionTimestamp = startTime.Unix()
logger.V(4).Info("applied JSON6902 patch", "processingTime", resp.RuleStats.ProcessingTime.String())
resp.ExecutionStats.ProcessingTime = time.Since(startTime)
resp.ExecutionStats.Timestamp = startTime.Unix()
logger.V(4).Info("applied JSON6902 patch", "processingTime", resp.ExecutionStats.ProcessingTime.String())
}()
resourceRaw, err := resource.MarshalJSON()

View file

@ -23,9 +23,9 @@ func ProcessStrategicMergePatch(ruleName string, overlay interface{}, resource u
resp.Type = engineapi.Mutation
defer func() {
resp.RuleStats.ProcessingTime = time.Since(startTime)
resp.RuleStats.RuleExecutionTimestamp = startTime.Unix()
logger.V(4).Info("finished applying strategicMerge patch", "processingTime", resp.RuleStats.ProcessingTime.String())
resp.ExecutionStats.ProcessingTime = time.Since(startTime)
resp.ExecutionStats.Timestamp = startTime.Unix()
logger.V(4).Info("finished applying strategicMerge patch", "processingTime", resp.ExecutionStats.ProcessingTime.String())
}()
overlayBytes, err := json.Marshal(overlay)

View file

@ -375,6 +375,6 @@ func endMutateResultResponse(logger logr.Logger, resp *engineapi.EngineResponse,
}
resp.PolicyResponse.ProcessingTime = time.Since(startTime)
resp.PolicyResponse.PolicyExecutionTimestamp = startTime.Unix()
resp.PolicyResponse.Timestamp = startTime.Unix()
logger.V(5).Info("finished processing policy", "processingTime", resp.PolicyResponse.ProcessingTime.String(), "mutationRulesApplied", resp.PolicyResponse.RulesAppliedCount)
}

View file

@ -95,7 +95,7 @@ func buildResponse(ctx *PolicyContext, resp *engineapi.EngineResponse, startTime
}
resp.PolicyResponse.ProcessingTime = time.Since(startTime)
resp.PolicyResponse.PolicyExecutionTimestamp = startTime.Unix()
resp.PolicyResponse.Timestamp = startTime.Unix()
}
func validateResource(ctx context.Context, log logr.Logger, rclient registryclient.Client, enginectx *PolicyContext, cfg config.Configuration) *engineapi.EngineResponse {
@ -175,9 +175,9 @@ func processValidationRule(ctx context.Context, log logr.Logger, rclient registr
}
func addRuleResponse(log logr.Logger, resp *engineapi.EngineResponse, ruleResp *engineapi.RuleResponse, startTime time.Time) {
ruleResp.RuleStats.ProcessingTime = time.Since(startTime)
ruleResp.RuleStats.RuleExecutionTimestamp = startTime.Unix()
log.V(4).Info("finished processing rule", "processingTime", ruleResp.RuleStats.ProcessingTime.String())
ruleResp.ExecutionStats.ProcessingTime = time.Since(startTime)
ruleResp.ExecutionStats.Timestamp = startTime.Unix()
log.V(4).Info("finished processing rule", "processingTime", ruleResp.ExecutionStats.ProcessingTime.String())
if ruleResp.Status == engineapi.RuleStatusPass || ruleResp.Status == engineapi.RuleStatusFail {
incrementAppliedCount(resp)

View file

@ -58,7 +58,7 @@ func ProcessEngineResponse(ctx context.Context, m metrics.MetricsConfigManager,
default:
ruleResult = metrics.Fail
}
ruleExecutionLatencyInSeconds := float64(rule.RuleStats.ProcessingTime) / float64(1000*1000*1000)
ruleExecutionLatencyInSeconds := float64(rule.ExecutionStats.ProcessingTime) / float64(1000*1000*1000)
registerPolicyExecutionDurationMetric(
ctx,
m,

View file

@ -17,7 +17,7 @@ func GetErrorMsg(engineReponses []*engineapi.EngineResponse) string {
str = append(str, fmt.Sprintf("failed policy %s:", er.PolicyResponse.Policy.Name))
for _, rule := range er.PolicyResponse.Rules {
if rule.Status != engineapi.RuleStatusPass {
str = append(str, rule.ToString())
str = append(str, rule.String())
}
}
}