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

merge foreach and add attestation checks

Signed-off-by: Jim Bugwadia <jim@nirmata.com>
This commit is contained in:
Jim Bugwadia 2021-10-02 14:24:06 -07:00
commit 2bd5bca721
69 changed files with 1506 additions and 490 deletions

View file

@ -148,21 +148,25 @@ create-e2e-infrastruture:
## variables
BIN_DIR := $(GOPATH)/bin
GO_ACC := $(BIN_DIR)/go-acc
GO_ACC := $(BIN_DIR)/go-acc@latest
CODE_COVERAGE_FILE:= coverage
CODE_COVERAGE_FILE_TXT := $(CODE_COVERAGE_FILE).txt
CODE_COVERAGE_FILE_HTML := $(CODE_COVERAGE_FILE).html
## targets
$(GO_ACC):
@echo " downloading testing tools"
go get -v github.com/ory/go-acc
@echo " installing testing tools"
go install -v github.com/ory/go-acc@latest
$(eval export PATH=$(GO_ACC):$(PATH))
# go test provides code coverage per packages only.
# go-acc merges the result for pks so that it be used by
# go tool cover for reporting
test: test-unit test-e2e test-cmd
test: test-clean test-unit test-e2e test-cmd
test-clean:
@echo " cleaning test cache"
go clean -testcache ./...
# go get downloads and installs the binary

View file

@ -423,6 +423,8 @@ type Validation struct {
// +optional
Message string `json:"message,omitempty" yaml:"message,omitempty"`
ForEachValidation *ForEachValidation `json:"foreach,omitempty" yaml:"foreach,omitempty"`
// Pattern specifies an overlay-style pattern used to check resources.
// +kubebuilder:validation:XPreserveUnknownFields
// +optional
@ -449,6 +451,39 @@ type Deny struct {
AnyAllConditions apiextensions.JSON `json:"conditions,omitempty" yaml:"conditions,omitempty"`
}
type ForEachValidation struct {
// List specifies a JMESPath expression that results in one or more elements
// to which the validation logic is applied.
List string `json:"list,omitempty" yaml:"list,omitempty"`
// Context defines variables and data sources that can be used during rule execution.
// +optional
Context []ContextEntry `json:"context,omitempty" yaml:"context,omitempty"`
// Preconditions are used to determine if a policy rule should be applied by evaluating a
// set of conditions. The declaration can contain nested `any` or `all` statements.
// See: https://kyverno.io/docs/writing-policies/preconditions/
// +kubebuilder:validation:XPreserveUnknownFields
// +optional
AnyAllConditions *AnyAllConditions `json:"preconditions,omitempty" yaml:"preconditions,omitempty"`
// Pattern specifies an overlay-style pattern used to check resources.
// +kubebuilder:validation:XPreserveUnknownFields
// +optional
Pattern apiextensions.JSON `json:"pattern,omitempty" yaml:"pattern,omitempty"`
// AnyPattern specifies list of validation patterns. At least one of the patterns
// must be satisfied for the validation rule to succeed.
// +kubebuilder:validation:XPreserveUnknownFields
// +optional
AnyPattern apiextensions.JSON `json:"anyPattern,omitempty" yaml:"anyPattern,omitempty"`
// Deny defines conditions used to pass or fail a validation rule.
// +optional
Deny *Deny `json:"deny,omitempty" yaml:"deny,omitempty"`
}
// ImageVerification validates that images that match the specified pattern
// are signed with the supplied public key. Once the image is verified it is
// mutated to include the SHA digest retrieved during the registration.

View file

@ -1,3 +1,4 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
/*

View file

@ -1,3 +1,4 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
/*

View file

@ -52,7 +52,7 @@ func VerifySignature(imageRef string, key []byte, repository string, log logr.Lo
}
cosignOpts := &cosign.CheckOpts{
RootCerts: fulcio.GetRoots(),
RootCerts: fulcio.GetRoots(),
Annotations: map[string]interface{}{},
SigVerifier: pubKey,
RegistryClientOpts: []remote.Option{
@ -139,7 +139,7 @@ func decodeAttestations(attestations []cosign.SignedPayload) (map[string]interfa
decodedAttestations := make([]map[string]interface{}, len(attestations))
for _, a := range attestations {
for _, a := range attestations {
payload := a.Payload
data, err := base64.StdEncoding.DecodeString(string(payload))
if err != nil {

View file

@ -16,7 +16,7 @@ func IsConditionalAnchorError(msg string) bool {
return false
}
// IsGlobalAnchorError checks if error message has conditional anchor error string
// IsGlobalAnchorError checks if error message has global anchor error string
func IsGlobalAnchorError(msg string) bool {
return strings.Contains(msg, GlobalAnchorErrMsg)
}

View file

@ -1,5 +1,7 @@
package common
import "encoding/json"
// CopyMap creates a full copy of the target map
func CopyMap(m map[string]interface{}) map[string]interface{} {
mapCopy := make(map[string]interface{})
@ -17,3 +19,22 @@ func CopySlice(s []interface{}) []interface{} {
return sliceCopy
}
func ToMap(data interface{}) (map[string]interface{}, error) {
if m, ok := data.(map[string]interface{}); ok {
return m, nil
}
b, err := json.Marshal(data)
if err != nil {
return nil, err
}
mapData := make(map[string]interface{})
err = json.Unmarshal(b, &mapData)
if err != nil {
return nil, err
}
return mapData, nil
}

View file

@ -53,21 +53,22 @@ type EvalInterface interface {
//Context stores the data resources as JSON
type Context struct {
mutex sync.RWMutex
jsonRaw []byte
jsonRawCheckpoint []byte
builtInVars []string
images *Images
log logr.Logger
mutex sync.RWMutex
jsonRaw []byte
jsonRawCheckpoints [][]byte
builtInVars []string
images *Images
log logr.Logger
}
//NewContext returns a new context
// builtInVars is the list of known variables (e.g. serviceAccountName)
func NewContext(builtInVars ...string) *Context {
ctx := Context{
jsonRaw: []byte(`{}`), // empty json struct
builtInVars: builtInVars,
log: log.Log.WithName("context"),
jsonRaw: []byte(`{}`), // empty json struct
builtInVars: builtInVars,
log: log.Log.WithName("context"),
jsonRawCheckpoints: make([][]byte, 0),
}
return &ctx
@ -98,6 +99,16 @@ func (ctx *Context) AddJSON(dataRaw []byte) error {
return nil
}
// AddJSON merges json data
func (ctx *Context) AddJSONObject(jsonData interface{}) error {
jsonBytes, err := json.Marshal(jsonData)
if err != nil {
return err
}
return ctx.AddJSON(jsonBytes)
}
// AddRequest adds an admission request to context
func (ctx *Context) AddRequest(request *v1beta1.AdmissionRequest) error {
modifiedResource := struct {
@ -187,6 +198,7 @@ func (ctx *Context) AddResourceAsObject(data interface{}) error {
ctx.log.Error(err, "failed to marshal the resource")
return err
}
return ctx.AddJSON(objRaw)
}
@ -306,28 +318,45 @@ func (ctx *Context) ImageInfo() *Images {
return ctx.images
}
// Checkpoint creates a copy of the internal state.
// Prior checkpoints will be overridden.
// Checkpoint creates a copy of the current internal state and
// pushes it into a stack of stored states.
func (ctx *Context) Checkpoint() {
ctx.mutex.Lock()
defer ctx.mutex.Unlock()
ctx.jsonRawCheckpoint = make([]byte, len(ctx.jsonRaw))
copy(ctx.jsonRawCheckpoint, ctx.jsonRaw)
jsonRawCheckpoint := make([]byte, len(ctx.jsonRaw))
copy(jsonRawCheckpoint, ctx.jsonRaw)
ctx.jsonRawCheckpoints = append(ctx.jsonRawCheckpoints, jsonRawCheckpoint)
}
// Restore restores internal state from a prior checkpoint, if one exists.
// If a prior checkpoint does not exist, the state will not be changed.
// Restore sets the internal state to the last checkpoint, and removes the checkpoint.
func (ctx *Context) Restore() {
ctx.reset(true)
}
// Reset sets the internal state to the last checkpoint, but does not remove the checkpoint.
func (ctx *Context) Reset() {
ctx.reset(false)
}
func (ctx *Context) reset(remove bool) {
ctx.mutex.Lock()
defer ctx.mutex.Unlock()
if ctx.jsonRawCheckpoint == nil || len(ctx.jsonRawCheckpoint) == 0 {
if len(ctx.jsonRawCheckpoints) == 0 {
return
}
ctx.jsonRaw = make([]byte, len(ctx.jsonRawCheckpoint))
copy(ctx.jsonRaw, ctx.jsonRawCheckpoint)
n := len(ctx.jsonRawCheckpoints) - 1
jsonRawCheckpoint := ctx.jsonRawCheckpoints[n]
ctx.jsonRaw = make([]byte, len(jsonRawCheckpoint))
copy(ctx.jsonRaw, jsonRawCheckpoint)
if remove {
ctx.jsonRawCheckpoints = ctx.jsonRawCheckpoints[:n]
}
}
// AddBuiltInVars adds given pattern to the builtInVars

View file

@ -115,6 +115,6 @@ func Test_addResourceAndUserContext(t *testing.T) {
expectedResult = "nirmata"
t.Log(result)
if !reflect.DeepEqual(expectedResult, result) {
t.Error("exected result does not match")
t.Error("expected result does not match")
}
}

View file

@ -83,7 +83,7 @@ func ForceMutate(ctx context.EvalInterface, policy kyverno.ClusterPolicy, resour
if rule.Mutation.Patches != nil {
var resp response.RuleResponse
resp, resource = mutate.ProcessPatches(logger.WithValues("rule", rule.Name), rule.Name, rule.Mutation, resource)
if !resp.Success {
if resp.Status != response.RuleStatusPass {
return unstructured.Unstructured{}, fmt.Errorf(resp.Message)
}
}
@ -91,7 +91,7 @@ func ForceMutate(ctx context.EvalInterface, policy kyverno.ClusterPolicy, resour
if rule.Mutation.PatchStrategicMerge != nil {
var resp response.RuleResponse
resp, resource = mutate.ProcessStrategicMergePatch(rule.Name, rule.Mutation.PatchStrategicMerge, resource, logger.WithValues("rule", rule.Name))
if !resp.Success {
if resp.Status != response.RuleStatusPass {
return unstructured.Unstructured{}, fmt.Errorf(resp.Message)
}
}
@ -104,11 +104,10 @@ func ForceMutate(ctx context.EvalInterface, policy kyverno.ClusterPolicy, resour
}
resp, resource = mutate.ProcessPatchJSON6902(rule.Name, jsonPatches, resource, logger.WithValues("rule", rule.Name))
if !resp.Success {
if resp.Status != response.RuleStatusPass {
return unstructured.Unstructured{}, fmt.Errorf(resp.Message)
}
}
}
return resource, nil

View file

@ -80,9 +80,9 @@ func filterRule(rule kyverno.Rule, policyContext *PolicyContext) *response.RuleR
// if the oldResource matched, return "false" to delete GR for it
if err = MatchesResourceDescription(oldResource, rule, admissionInfo, excludeGroupRole, namespaceLabels); err == nil {
return &response.RuleResponse{
Name: rule.Name,
Type: "Generation",
Success: false,
Name: rule.Name,
Type: "Generation",
Status: response.RuleStatusFail,
RuleStats: response.RuleStats{
ProcessingTime: time.Since(startTime),
RuleExecutionTimestamp: startTime.Unix(),
@ -123,9 +123,9 @@ func filterRule(rule kyverno.Rule, policyContext *PolicyContext) *response.RuleR
// build rule Response
return &response.RuleResponse{
Name: ruleCopy.Name,
Type: "Generation",
Success: true,
Name: ruleCopy.Name,
Type: "Generation",
Status: response.RuleStatusPass,
RuleStats: response.RuleStats{
ProcessingTime: time.Since(startTime),
RuleExecutionTimestamp: startTime.Unix(),

View file

@ -3,6 +3,8 @@ package engine
import (
"encoding/json"
"fmt"
"github.com/kyverno/kyverno/pkg/engine/variables"
"github.com/pkg/errors"
"time"
"github.com/go-logr/logr"
@ -35,7 +37,7 @@ func VerifyAndPatchImages(policyContext *PolicyContext) (resp *response.EngineRe
startTime := time.Now()
defer func() {
buildResponse(logger, policyContext, resp, startTime)
buildResponse(policyContext, resp, startTime)
logger.V(4).Info("finished policy processing", "processingTime", resp.PolicyResponse.ProcessingTime.String(), "rulesApplied", resp.PolicyResponse.RulesAppliedCount)
}()
@ -54,11 +56,11 @@ func VerifyAndPatchImages(policyContext *PolicyContext) (resp *response.EngineRe
policyContext.JSONContext.Restore()
iv := &imageVerifier {
logger: logger,
iv := &imageVerifier{
logger: logger,
policyContext: policyContext,
rule: &rule,
resp: resp,
rule: &rule,
resp: resp,
}
for _, imageVerify := range rule.VerifyImages {
@ -71,10 +73,10 @@ func VerifyAndPatchImages(policyContext *PolicyContext) (resp *response.EngineRe
}
type imageVerifier struct {
logger logr.Logger
logger logr.Logger
policyContext *PolicyContext
rule *v1.Rule
resp *response.EngineResponse
rule *v1.Rule
resp *response.EngineResponse
}
func (iv *imageVerifier) verify(imageVerify *v1.ImageVerification, images map[string]*context.ImageInfo) {
@ -100,11 +102,11 @@ func (iv *imageVerifier) verify(imageVerify *v1.ImageVerification, images map[st
if len(imageVerify.Attestations) == 0 {
var digest string
ruleResp, digest = iv.verifySignature(repository, key, imageInfo)
if ruleResp.Success {
if ruleResp.Status == response.RuleStatusPass {
iv.patchDigest(imageInfo, digest, ruleResp)
}
} else {
ruleResp = iv.attestImage(repository, key, imageInfo)
ruleResp = iv.attestImage(repository, key, imageInfo, imageVerify.Attestations)
}
iv.resp.PolicyResponse.Rules = append(iv.resp.PolicyResponse.Rules, *ruleResp)
@ -134,12 +136,12 @@ func (iv *imageVerifier) verifySignature(repository, key string, imageInfo *cont
digest, err := cosign.VerifySignature(image, []byte(key), repository, iv.logger)
if err != nil {
iv.logger.Info("failed to verify image signature", "image", image, "error", err, "duration", time.Since(start).Seconds())
ruleResp.Success = false
ruleResp.Status = response.RuleStatusFail
ruleResp.Message = fmt.Sprintf("image signature verification failed for %s: %v", image, err)
return ruleResp, ""
}
ruleResp.Success = true
ruleResp.Status = response.RuleStatusPass
ruleResp.Message = fmt.Sprintf("image %s verified", image)
iv.logger.V(3).Info("verified image", "image", image, "digest", digest, "duration", time.Since(start).Seconds())
return ruleResp, digest
@ -165,28 +167,44 @@ func makeAddDigestPatch(imageInfo *context.ImageInfo, digest string) ([]byte, er
return json.Marshal(patch)
}
func (iv *imageVerifier) attestImage(repository, key string, imageInfo *context.ImageInfo) *response.RuleResponse {
func (iv *imageVerifier) attestImage(repository, key string, imageInfo *context.ImageInfo, attestationChecks []*v1.AnyAllConditions) *response.RuleResponse {
image := imageInfo.String()
ruleResp := &response.RuleResponse{
Name: iv.rule.Name,
Type: utils.Validation.String(),
}
start := time.Now()
inTotoAttestation, err := cosign.FetchAttestations(image, []byte(key), repository)
attestations, err := cosign.FetchAttestations(image, []byte(key), repository)
if err != nil {
iv.logger.Info("failed to verify image attestations", "image", image, "error", err, "duration", time.Since(start).Seconds())
ruleResp.Success = false
ruleResp.Message = fmt.Sprintf("image attestation failed for %s: %v", image, err)
return ruleResp
iv.logger.Info("failed to fetch attestations", "image", image, "error", err, "duration", time.Since(start).Seconds())
return ruleError(iv.rule, fmt.Sprintf("failed to fetch attestations for %s", image), err)
}
iv.logger.Info("received attestation", "in-toto-attestation", inTotoAttestation)
iv.logger.Info("received attestation", "attestations", attestations)
iv.policyContext.JSONContext.Checkpoint()
defer iv.policyContext.JSONContext.Restore()
if err := iv.policyContext.JSONContext.AddJSONObject(attestations); err != nil {
return ruleError(iv.rule, fmt.Sprintf("failed to add attestations to the context %v", attestations), err)
}
// add to context
passed, err := iv.checkConditions(attestationChecks)
if err != nil {
return ruleError(iv.rule, "failed to check attestation", err)
}
// process any / all conditions
return ruleResp
if !passed {
return ruleResponse(iv.rule, "attestation checks failed", response.RuleStatusFail)
}
return ruleResponse(iv.rule, "attestation checks passed", response.RuleStatusPass)
}
func (iv *imageVerifier) checkConditions(attestationChecks []*v1.AnyAllConditions) (bool, error) {
conditions, err := variables.SubstituteAllInConditions(iv.logger, iv.policyContext.JSONContext, attestationChecks)
if err != nil {
return false, errors.Wrapf(err, "failed to substitute variables in conditions")
}
pass := variables.EvaluateConditions(iv.logger, iv.policyContext.JSONContext, conditions)
return pass, nil
}

View file

@ -101,7 +101,7 @@ func (h patchesJSON6902Handler) Handle() (resp response.RuleResponse, patchedRes
patchesJSON6902, err := convertPatchesToJSON(h.mutation.PatchesJSON6902)
if err != nil {
resp.Success = false
resp.Status = response.RuleStatusFail
h.logger.Error(err, "error in type conversion")
resp.Message = err.Error()
return resp, h.patchedResource

View file

@ -38,25 +38,25 @@ func ProcessOverlay(log logr.Logger, ruleName string, overlay interface{}, resou
case conditionNotPresent:
logger.V(3).Info("skip applying rule", "reason", "conditionNotPresent")
resp.Success = true
resp.Status = response.RuleStatusPass
return resp, resource
case conditionFailure:
logger.V(3).Info("skip applying rule", "reason", "conditionFailure")
//TODO: send zero response and not consider this as applied?
resp.Success = true
resp.Status = response.RuleStatusPass
resp.Message = overlayerr.ErrorMsg()
return resp, resource
case overlayFailure:
logger.Info("failed to process overlay")
resp.Success = false
resp.Status = response.RuleStatusFail
resp.Message = fmt.Sprintf("failed to process overlay: %v", overlayerr.ErrorMsg())
return resp, resource
default:
logger.Info("failed to process overlay")
resp.Success = false
resp.Status = response.RuleStatusFail
resp.Message = fmt.Sprintf("Unknown type of error: %v", overlayerr.Error())
return resp, resource
}
@ -64,14 +64,14 @@ func ProcessOverlay(log logr.Logger, ruleName string, overlay interface{}, resou
logger.V(4).Info("processing overlay rule", "patches", len(patches))
if len(patches) == 0 {
resp.Success = true
resp.Status = response.RuleStatusPass
return resp, resource
}
// convert to RAW
resourceRaw, err := resource.MarshalJSON()
if err != nil {
resp.Success = false
resp.Status = response.RuleStatusFail
logger.Error(err, "failed to marshal resource")
resp.Message = fmt.Sprintf("failed to process JSON patches: %v", err)
return resp, resource
@ -82,7 +82,7 @@ func ProcessOverlay(log logr.Logger, ruleName string, overlay interface{}, resou
patchResource, err = utils.ApplyPatches(resourceRaw, patches)
if err != nil {
msg := fmt.Sprintf("failed to apply JSON patches: %v", err)
resp.Success = false
resp.Status = response.RuleStatusFail
resp.Message = msg
return resp, resource
}
@ -91,13 +91,13 @@ func ProcessOverlay(log logr.Logger, ruleName string, overlay interface{}, resou
err = patchedResource.UnmarshalJSON(patchResource)
if err != nil {
logger.Error(err, "failed to unmarshal resource")
resp.Success = false
resp.Status = response.RuleStatusFail
resp.Message = fmt.Sprintf("failed to process JSON patches: %v", err)
return resp, resource
}
// rule application successfully
resp.Success = true
resp.Status = response.RuleStatusPass
resp.Message = fmt.Sprintf("successfully processed overlay")
resp.Patches = patches

View file

@ -27,7 +27,7 @@ func ProcessPatchJSON6902(ruleName string, patchesJSON6902 []byte, resource unst
resourceRaw, err := resource.MarshalJSON()
if err != nil {
resp.Success = false
resp.Status = response.RuleStatusFail
logger.Error(err, "failed to marshal resource")
resp.Message = fmt.Sprintf("failed to marshal resource: %v", err)
return resp, resource
@ -35,7 +35,7 @@ func ProcessPatchJSON6902(ruleName string, patchesJSON6902 []byte, resource unst
patchedResourceRaw, err := applyPatchesWithOptions(resourceRaw, patchesJSON6902)
if err != nil {
resp.Success = false
resp.Status = response.RuleStatusFail
logger.Error(err, "unable to apply RFC 6902 patches")
resp.Message = fmt.Sprintf("unable to apply RFC 6902 patches: %v", err)
return resp, resource
@ -43,7 +43,7 @@ func ProcessPatchJSON6902(ruleName string, patchesJSON6902 []byte, resource unst
patchesBytes, err := generatePatches(resourceRaw, patchedResourceRaw)
if err != nil {
resp.Success = false
resp.Status = response.RuleStatusFail
logger.Error(err, "unable generate patch bytes from base and patched document, apply patchesJSON6902 directly")
resp.Message = fmt.Sprintf("unable generate patch bytes from base and patched document, apply patchesJSON6902 directly: %v", err)
return resp, resource
@ -56,12 +56,12 @@ func ProcessPatchJSON6902(ruleName string, patchesJSON6902 []byte, resource unst
err = patchedResource.UnmarshalJSON(patchedResourceRaw)
if err != nil {
logger.Error(err, "failed to unmarshal resource")
resp.Success = false
resp.Status = response.RuleStatusFail
resp.Message = fmt.Sprintf("failed to unmarshal resource: %v", err)
return resp, resource
}
resp.Success = true
resp.Status = response.RuleStatusPass
resp.Message = fmt.Sprintf("successfully process JSON6902 patches")
resp.Patches = patchesBytes
return resp, patchedResource

View file

@ -2,6 +2,7 @@ package mutate
import (
"fmt"
"github.com/kyverno/kyverno/pkg/engine/response"
"testing"
"github.com/ghodss/yaml"
@ -50,7 +51,7 @@ func TestTypeConversion(t *testing.T) {
assert.Nil(t, err)
// apply patches
resp, _ := ProcessPatchJSON6902("type-conversion", jsonPatches, resource, log.Log)
if !assert.Equal(t, true, resp.Success) {
if !assert.Equal(t, response.RuleStatusPass, resp.Status) {
t.Fatal(resp.Message)
}

View file

@ -35,7 +35,7 @@ func ProcessPatches(log logr.Logger, ruleName string, mutation kyverno.Mutation,
// convert to RAW
resourceRaw, err := resource.MarshalJSON()
if err != nil {
resp.Success = false
resp.Status = response.RuleStatusFail
logger.Error(err, "failed to marshal resource")
resp.Message = fmt.Sprintf("failed to process JSON patches: %v", err)
return resp, resource
@ -67,7 +67,7 @@ func ProcessPatches(log logr.Logger, ruleName string, mutation kyverno.Mutation,
// error while processing JSON patches
if len(errs) > 0 {
resp.Success = false
resp.Status = response.RuleStatusFail
resp.Message = fmt.Sprintf("failed to process JSON patches: %v", func() string {
var str []string
for _, err := range errs {
@ -80,13 +80,13 @@ func ProcessPatches(log logr.Logger, ruleName string, mutation kyverno.Mutation,
err = patchedResource.UnmarshalJSON(resourceRaw)
if err != nil {
logger.Error(err, "failed to unmmarshal resource")
resp.Success = false
resp.Status = response.RuleStatusFail
resp.Message = fmt.Sprintf("failed to process JSON patches: %v", err)
return resp, resource
}
// JSON patches processed successfully
resp.Success = true
resp.Status = response.RuleStatusPass
resp.Message = fmt.Sprintf("successfully process JSON patches")
resp.Patches = patches
return resp, patchedResource

View file

@ -1,6 +1,7 @@
package mutate
import (
"github.com/kyverno/kyverno/pkg/engine/response"
"testing"
"gotest.tools/assert"
@ -43,7 +44,7 @@ func TestProcessPatches_EmptyPatches(t *testing.T) {
t.Error(err)
}
rr, _ := ProcessPatches(log.Log, "", emptyRule.Mutation, *resourceUnstructured)
assert.Check(t, rr.Success)
assert.Equal(t, rr.Status, response.RuleStatusPass)
assert.Assert(t, len(rr.Patches) == 0)
}
@ -72,14 +73,14 @@ func makeRuleWithPatches(patches []types.Patch) types.Rule {
func TestProcessPatches_EmptyDocument(t *testing.T) {
rule := makeRuleWithPatch(makeAddIsMutatedLabelPatch())
rr, _ := ProcessPatches(log.Log, rule.Name, rule.Mutation, unstructured.Unstructured{})
assert.Assert(t, !rr.Success)
assert.Equal(t, rr.Status, response.RuleStatusFail)
assert.Assert(t, len(rr.Patches) == 0)
}
func TestProcessPatches_AllEmpty(t *testing.T) {
emptyRule := types.Rule{}
rr, _ := ProcessPatches(log.Log, "", emptyRule.Mutation, unstructured.Unstructured{})
assert.Check(t, !rr.Success)
assert.Equal(t, rr.Status, response.RuleStatusFail)
assert.Assert(t, len(rr.Patches) == 0)
}
@ -92,7 +93,7 @@ func TestProcessPatches_AddPathDoesntExist(t *testing.T) {
t.Error(err)
}
rr, _ := ProcessPatches(log.Log, rule.Name, rule.Mutation, *resourceUnstructured)
assert.Check(t, !rr.Success)
assert.Equal(t, rr.Status, response.RuleStatusFail)
assert.Assert(t, len(rr.Patches) == 0)
}
@ -104,7 +105,7 @@ func TestProcessPatches_RemovePathDoesntExist(t *testing.T) {
t.Error(err)
}
rr, _ := ProcessPatches(log.Log, rule.Name, rule.Mutation, *resourceUnstructured)
assert.Check(t, rr.Success)
assert.Equal(t, rr.Status, response.RuleStatusPass)
assert.Assert(t, len(rr.Patches) == 0)
}
@ -117,7 +118,7 @@ func TestProcessPatches_AddAndRemovePathsDontExist_EmptyResult(t *testing.T) {
t.Error(err)
}
rr, _ := ProcessPatches(log.Log, rule.Name, rule.Mutation, *resourceUnstructured)
assert.Check(t, !rr.Success)
assert.Equal(t, rr.Status, response.RuleStatusFail)
assert.Assert(t, len(rr.Patches) == 0)
}
@ -131,7 +132,7 @@ func TestProcessPatches_AddAndRemovePathsDontExist_ContinueOnError_NotEmptyResul
t.Error(err)
}
rr, _ := ProcessPatches(log.Log, rule.Name, rule.Mutation, *resourceUnstructured)
assert.Check(t, rr.Success)
assert.Equal(t, rr.Status, response.RuleStatusPass)
assert.Assert(t, len(rr.Patches) != 0)
assertEqStringAndData(t, `{"path":"/metadata/labels/label3","op":"add","value":"label3Value"}`, rr.Patches[0])
}
@ -144,7 +145,7 @@ func TestProcessPatches_RemovePathDoesntExist_EmptyResult(t *testing.T) {
t.Error(err)
}
rr, _ := ProcessPatches(log.Log, rule.Name, rule.Mutation, *resourceUnstructured)
assert.Check(t, rr.Success)
assert.Equal(t, rr.Status, response.RuleStatusPass)
assert.Assert(t, len(rr.Patches) == 0)
}
@ -157,7 +158,7 @@ func TestProcessPatches_RemovePathDoesntExist_NotEmptyResult(t *testing.T) {
t.Error(err)
}
rr, _ := ProcessPatches(log.Log, rule.Name, rule.Mutation, *resourceUnstructured)
assert.Check(t, rr.Success)
assert.Equal(t, rr.Status, response.RuleStatusPass)
assert.Assert(t, len(rr.Patches) == 1)
assertEqStringAndData(t, `{"path":"/metadata/labels/label2","op":"add","value":"label2Value"}`, rr.Patches[0])
}

View file

@ -31,7 +31,7 @@ func ProcessStrategicMergePatch(ruleName string, overlay interface{}, resource u
overlayBytes, err := json.Marshal(overlay)
if err != nil {
resp.Success = false
resp.Status = response.RuleStatusFail
logger.Error(err, "failed to marshal resource")
resp.Message = fmt.Sprintf("failed to process patchStrategicMerge: %v", err)
return resp, resource
@ -39,7 +39,7 @@ func ProcessStrategicMergePatch(ruleName string, overlay interface{}, resource u
base, err := json.Marshal(resource.Object)
if err != nil {
resp.Success = false
resp.Status = response.RuleStatusFail
logger.Error(err, "failed to marshal resource")
resp.Message = fmt.Sprintf("failed to process patchStrategicMerge: %v", err)
return resp, resource
@ -48,7 +48,7 @@ func ProcessStrategicMergePatch(ruleName string, overlay interface{}, resource u
if err != nil {
log.Error(err, "failed to apply patchStrategicMerge")
msg := fmt.Sprintf("failed to apply patchStrategicMerge: %v", err)
resp.Success = false
resp.Status = response.RuleStatusFail
resp.Message = msg
return resp, resource
}
@ -56,7 +56,7 @@ func ProcessStrategicMergePatch(ruleName string, overlay interface{}, resource u
err = patchedResource.UnmarshalJSON(patchedBytes)
if err != nil {
logger.Error(err, "failed to unmarshal resource")
resp.Success = false
resp.Status = response.RuleStatusFail
resp.Message = fmt.Sprintf("failed to process patchStrategicMerge: %v", err)
return resp, resource
}
@ -66,7 +66,7 @@ func ProcessStrategicMergePatch(ruleName string, overlay interface{}, resource u
jsonPatches, err := generatePatches(base, patchedBytes)
if err != nil {
msg := fmt.Sprintf("failed to generated JSON patches from patched resource: %v", err.Error())
resp.Success = false
resp.Status = response.RuleStatusFail
log.Info(msg)
resp.Message = msg
return resp, patchedResource
@ -76,7 +76,7 @@ func ProcessStrategicMergePatch(ruleName string, overlay interface{}, resource u
log.V(5).Info("generated patch", "patch", string(p))
}
resp.Success = true
resp.Status = response.RuleStatusPass
resp.Patches = jsonPatches
resp.Message = "successfully processed strategic merge patch"
return resp, patchedResource

View file

@ -335,9 +335,12 @@ func checkCondition(logger logr.Logger, pattern *yaml.RNode, resource *yaml.RNod
return err
}
_, err = validate.ValidateResourceWithPattern(logger, resourceInterface, patternInterface)
err = validate.MatchPattern(logger, resourceInterface, patternInterface)
if err != nil {
return err
}
return err
return nil
}
func deleteConditionsFromNestedMaps(pattern *yaml.RNode) (bool, error) {

View file

@ -913,7 +913,7 @@ func Test_CheckConditionAnchor_Matches(t *testing.T) {
resource := yaml.MustParse(string(resourceRaw))
err := checkCondition(log.Log, pattern, resource)
assert.NilError(t, err)
assert.Equal(t, err, nil)
}
func Test_CheckConditionAnchor_DoesNotMatch(t *testing.T) {

View file

@ -75,13 +75,13 @@ func Mutate(policyContext *PolicyContext) (resp *response.EngineResponse) {
// Restore() is meant for restoring context loaded from external lookup (APIServer & ConfigMap)
// while we need to keep updated resource in the JSON context as rules can be chained
resource, err := policyContext.JSONContext.Query("request.object")
policyContext.JSONContext.Restore()
policyContext.JSONContext.Reset()
if err == nil && resource != nil {
if err := ctx.AddResourceAsObject(resource.(map[string]interface{})); err != nil {
logger.WithName("RestoreContext").Error(err, "unable to update resource object")
}
} else {
logger.WithName("RestoreContext").Error(err, "failed to quey resource object")
logger.WithName("RestoreContext").Error(err, "failed to query resource object")
}
if err := LoadContext(logger, rule.Context, resCache, policyContext, rule.Name); err != nil {
@ -117,8 +117,8 @@ func Mutate(policyContext *PolicyContext) (resp *response.EngineResponse) {
ruleResp := response.RuleResponse{
Name: ruleCopy.Name,
Type: utils.Validation.String(),
Message: fmt.Sprintf("variable substitution failed for rule %s: %s", ruleCopy.Name, err.Error()),
Success: true,
Message: fmt.Sprintf("variable substitution failed: %s", err.Error()),
Status: response.RuleStatusPass,
}
incrementAppliedCount(resp)
@ -131,7 +131,7 @@ func Mutate(policyContext *PolicyContext) (resp *response.EngineResponse) {
mutation := ruleCopy.Mutation.DeepCopy()
mutateHandler := mutate.CreateMutateHandler(ruleCopy.Name, mutation, patchedResource, ctx, logger)
ruleResponse, patchedResource = mutateHandler.Handle()
if ruleResponse.Success {
if ruleResponse.Status == response.RuleStatusPass {
// - overlay pattern does not match the resource conditions
if ruleResponse.Patches == nil {
continue

View file

@ -157,7 +157,7 @@ func Test_variableSubstitutionPathNotExist(t *testing.T) {
JSONContext: ctx,
NewResource: *resourceUnstructured}
er := Mutate(policyContext)
expectedErrorStr := "variable substitution failed for rule test-path-not-exist: Unknown key \"name1\" in path"
expectedErrorStr := "variable substitution failed: Unknown key \"name1\" in path"
assert.Equal(t, er.PolicyResponse.Rules[0].Message, expectedErrorStr)
}

View file

@ -40,3 +40,18 @@ type PolicyContext struct {
// NamespaceLabels stores the label of namespace to be processed by namespace selector
NamespaceLabels map[string]string
}
func (pc *PolicyContext) Copy() *PolicyContext {
return &PolicyContext{
Policy: pc.Policy,
NewResource: pc.NewResource,
OldResource: pc.OldResource,
AdmissionInfo: pc.AdmissionInfo,
Client: pc.Client,
ExcludeGroupRole: pc.ExcludeGroupRole,
ExcludeResourceFunc: pc.ExcludeResourceFunc,
ResourceCache: pc.ResourceCache,
JSONContext: pc.JSONContext,
NamespaceLabels: pc.NamespaceLabels,
}
}

View file

@ -54,26 +54,38 @@ func (rs ResourceSpec) GetKey() string {
//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"`
}
//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 string `json:"type"`
// message response from the rule application
Message string `json:"message"`
// JSON patches, for mutation rules
Patches [][]byte `json:"patches,omitempty"`
// success/fail
Success bool `json:"success"`
// rule status
Status RuleStatus `json:"status"`
// statistics
RuleStats `json:",inline"`
}
@ -94,20 +106,22 @@ type RuleStats struct {
//IsSuccessful checks if any rule has failed or not
func (er EngineResponse) IsSuccessful() bool {
for _, r := range er.PolicyResponse.Rules {
if !r.Success {
if r.Status != RuleStatusPass {
return false
}
}
return true
}
//IsFailed checks if any rule has succeeded or not
func (er EngineResponse) IsFailed() bool {
for _, r := range er.PolicyResponse.Rules {
if r.Success {
if r.Status == RuleStatusPass {
return false
}
}
return true
}
@ -125,12 +139,12 @@ func (er EngineResponse) GetPatches() [][]byte {
//GetFailedRules returns failed rules
func (er EngineResponse) GetFailedRules() []string {
return er.getRules(false)
return er.getRules(RuleStatusFail)
}
//GetSuccessRules returns success rules
func (er EngineResponse) GetSuccessRules() []string {
return er.getRules(true)
return er.getRules(RuleStatusPass)
}
// GetResourceSpec returns resourceSpec of er
@ -144,10 +158,10 @@ func (er EngineResponse) GetResourceSpec() ResourceSpec {
}
}
func (er EngineResponse) getRules(success bool) []string {
func (er EngineResponse) getRules(status RuleStatus) []string {
var rules []string
for _, r := range er.PolicyResponse.Rules {
if r.Success == success {
if r.Status == status {
rules = append(rules, r.Name)
}
}

View file

@ -0,0 +1,30 @@
package response
import (
"gopkg.in/yaml.v2"
"gotest.tools/assert"
"testing"
)
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,97 @@
package response
import (
"encoding/json"
"fmt"
"strings"
)
// RuleStatus represents the status of rule execution
type RuleStatus int
// RuleStatusPass is used to report the result of processing a rule.
const (
// RuleStatusPass indicates that the resources meets the policy rule requirements
RuleStatusPass RuleStatus = iota
// Fail indicates that the resource does not meet the policy rule requirements
RuleStatusFail
// Warn indicates that the the resource does not meet the policy rule requirements, but the policy is not scored
RuleStatusWarn
// Error 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
// Skip 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
)
func (s *RuleStatus) String() string {
return toString[*s]
}
var toString = map[RuleStatus]string{
RuleStatusPass: "pass",
RuleStatusFail: "fail",
RuleStatusWarn: "warning",
RuleStatusError: "error",
RuleStatusSkip: "skip",
}
var toID = map[string]RuleStatus{
"pass": RuleStatusPass,
"fail": RuleStatusFail,
"warning": RuleStatusWarn,
"error": RuleStatusError,
"skip": RuleStatusSkip,
}
// 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 (v *RuleStatus) UnmarshalYAML(unmarshal func(interface{}) error) error {
var s string
if err := unmarshal(&s); err != nil {
return err
}
statusVal, err := getRuleStatus(s)
if err != nil {
return err
}
*v = *statusVal
return nil
}

View file

@ -392,7 +392,8 @@ func transformConditions(original apiextensions.JSON) (interface{}, error) {
case []kyverno.Condition: // backwards compatibility
return copyOldConditions(typedValue), nil
}
return nil, fmt.Errorf("wrongfully configured data")
return nil, fmt.Errorf("invalid preconditions")
}
// excludeResource checks if the resource has ownerRef set

View file

@ -12,24 +12,43 @@ import (
"github.com/kyverno/kyverno/pkg/engine/wildcards"
)
// ValidateResourceWithPattern is a start of element-by-element validation process
type PatternError struct {
Err error
Path string
Skip bool
}
func (e *PatternError) Error() string {
if e.Err == nil {
return ""
}
return e.Err.Error()
}
// MatchPattern is a start of element-by-element pattern validation process.
// It assumes that validation is started from root, so "/" is passed
func ValidateResourceWithPattern(logger logr.Logger, resource, pattern interface{}) (string, error) {
func MatchPattern(logger logr.Logger, resource, pattern interface{}) error {
// newAnchorMap - to check anchor key has values
ac := common.NewAnchorMap()
elemPath, err := validateResourceElement(logger, resource, pattern, pattern, "/", ac)
if err != nil {
// if conditional or global anchors report errors, the rule does not apply to the resource
if common.IsConditionalAnchorError(err.Error()) || common.IsGlobalAnchorError(err.Error()) {
logger.V(3).Info(ac.AnchorError.Message)
return "", nil
logger.V(3).Info("skipping resource as anchor does not apply", "msg", ac.AnchorError.Error())
return &PatternError{nil, "", true}
}
if !ac.IsAnchorError() {
return elemPath, err
// check if an anchor defined in the policy rule is missing in the resource
if ac.IsAnchorError() {
logger.V(3).Info("missing anchor in resource")
return &PatternError{err, "", false}
}
return &PatternError{err, elemPath, false}
}
return "", nil
return &PatternError{nil, "", false}
}
// validateResourceElement detects the element type (map, array, nil, string, int, bool, float)
@ -44,7 +63,7 @@ func validateResourceElement(log logr.Logger, resourceElement, patternElement, o
log.V(4).Info("Pattern and resource have different structures.", "path", path, "expected", fmt.Sprintf("%T", patternElement), "current", fmt.Sprintf("%T", resourceElement))
return path, fmt.Errorf("Pattern and resource have different structures. Path: %s. Expected %T, found %T", path, patternElement, resourceElement)
}
// CheckAnchorInResource - check anchor anchor key exists in resource and update the AnchorKey fields.
// CheckAnchorInResource - check anchor key exists in resource and update the AnchorKey fields.
ac.CheckAnchorInResource(typedPatternElement, typedResourceElement)
return validateMap(log, typedResourceElement, typedPatternElement, originPattern, path, ac)
// array

View file

@ -1590,19 +1590,53 @@ func TestConditionalAnchorWithMultiplePatterns(t *testing.T) {
}
for _, testCase := range testCases {
var pattern, resource interface{}
err := json.Unmarshal(testCase.pattern, &pattern)
assert.NilError(t, err)
err = json.Unmarshal(testCase.resource, &resource)
assert.NilError(t, err)
_, err = ValidateResourceWithPattern(log.Log, resource, pattern)
if testCase.nilErr {
assert.NilError(t, err, fmt.Sprintf("\ntest: %s\npattern: %s\nresource: %s\n", testCase.name, pattern, resource))
} else {
assert.Assert(t,
err != nil,
fmt.Sprintf("\ntest: %s\npattern: %s\nresource: %s\nmsg: %v", testCase.name, pattern, resource, err))
}
testMatchPattern(t, testCase)
}
}
func Test_global_anchor(t *testing.T) {
testCases := []struct {
name string
pattern []byte
resource []byte
nilErr bool
}{
{
name: "check global anchor_skip",
pattern: []byte(`{"spec": {"containers": [{"name": "*","<(image)": "*:latest","imagePullPolicy": "!Always"}]}}`),
resource: []byte(`{"spec": {"containers": [{"name": "nginx","image": "nginx:v1", "imagePullPolicy": "Always"}]}}`),
nilErr: true,
},
{
name: "check global anchor_apply",
pattern: []byte(`{"spec": {"containers": [{"name": "*","<(image)": "*:latest","imagePullPolicy": "!Always"}]}}`),
resource: []byte(`{"spec": {"containers": [{"name": "nginx","image": "nginx:latest", "imagePullPolicy": "Always"}]}}`),
nilErr: false,
},
}
testMatchPattern(t, testCases[0])
testMatchPattern(t, testCases[1])
}
func testMatchPattern(t *testing.T, testCase struct {
name string
pattern []byte
resource []byte
nilErr bool
}) {
var pattern, resource interface{}
err := json.Unmarshal(testCase.pattern, &pattern)
assert.NilError(t, err)
err = json.Unmarshal(testCase.resource, &resource)
assert.NilError(t, err)
err, _ = MatchPattern(log.Log, resource, pattern)
if testCase.nilErr {
assert.NilError(t, err, fmt.Sprintf("\ntest: %s\npattern: %s\nresource: %s\n", testCase.name, pattern, resource))
} else {
assert.Assert(t,
err != nil,
fmt.Sprintf("\ntest: %s\npattern: %s\nresource: %s\nmsg: %v", testCase.name, pattern, resource, err))
}
}

View file

@ -1,7 +1,11 @@
package engine
import (
"encoding/json"
"fmt"
"github.com/kyverno/kyverno/pkg/engine/common"
"github.com/pkg/errors"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
"reflect"
"strings"
"time"
@ -9,8 +13,6 @@ import (
"github.com/go-logr/logr"
gojmespath "github.com/jmespath/go-jmespath"
kyverno "github.com/kyverno/kyverno/pkg/api/kyverno/v1"
"github.com/kyverno/kyverno/pkg/engine/common"
"github.com/kyverno/kyverno/pkg/engine/context"
"github.com/kyverno/kyverno/pkg/engine/response"
"github.com/kyverno/kyverno/pkg/engine/utils"
"github.com/kyverno/kyverno/pkg/engine/validate"
@ -27,7 +29,7 @@ func Validate(policyContext *PolicyContext) (resp *response.EngineResponse) {
logger := buildLogger(policyContext)
logger.V(4).Info("start policy processing", "startTime", startTime)
defer func() {
buildResponse(logger, policyContext, resp, startTime)
buildResponse(policyContext, resp, startTime)
logger.V(4).Info("finished policy processing", "processingTime", resp.PolicyResponse.ProcessingTime.String(), "validationRulesApplied", resp.PolicyResponse.RulesAppliedCount)
}()
@ -46,14 +48,14 @@ func buildLogger(ctx *PolicyContext) logr.Logger {
return logger
}
func buildResponse(logger logr.Logger, ctx *PolicyContext, resp *response.EngineResponse, startTime time.Time) {
func buildResponse(ctx *PolicyContext, resp *response.EngineResponse, startTime time.Time) {
if reflect.DeepEqual(resp, response.EngineResponse{}) {
return
}
if reflect.DeepEqual(resp.PatchedResource, unstructured.Unstructured{}) {
// for delete requests patched resource will be oldResource since newResource is empty
var resource unstructured.Unstructured = ctx.NewResource
var resource = ctx.NewResource
if reflect.DeepEqual(ctx.NewResource, unstructured.Unstructured{}) {
resource = ctx.OldResource
}
@ -76,10 +78,14 @@ func incrementAppliedCount(resp *response.EngineResponse) {
resp.PolicyResponse.RulesAppliedCount++
}
func incrementErrorCount(resp *response.EngineResponse) {
resp.PolicyResponse.RulesErrorCount++
}
func validateResource(log logr.Logger, ctx *PolicyContext) *response.EngineResponse {
resp := &response.EngineResponse{}
if ManagedPodResource(ctx.Policy, ctx.NewResource) {
log.V(5).Info("skip policy as direct changes to pods managed by workload controllers are not allowed", "policy", ctx.Policy.GetName())
log.V(5).Info("skip validation of pods managed by workload controllers", "policy", ctx.Policy.GetName())
return resp
}
@ -87,113 +93,306 @@ func validateResource(log logr.Logger, ctx *PolicyContext) *response.EngineRespo
defer ctx.JSONContext.Restore()
for _, rule := range ctx.Policy.Spec.Rules {
var err error
if !rule.HasValidate() {
continue
}
log = log.WithValues("rule", rule.Name)
if !matches(log, rule, ctx) {
continue
}
ctx.JSONContext.Restore()
if err := LoadContext(log, rule.Context, ctx.ResourceCache, ctx, rule.Name); err != nil {
if _, ok := err.(gojmespath.NotFoundError); ok {
log.V(3).Info("failed to load context", "reason", err.Error())
} else {
log.Error(err, "failed to load context")
}
continue
}
log.V(3).Info("matched validate rule")
ctx.JSONContext.Reset()
startTime := time.Now()
ruleCopy := rule.DeepCopy()
ruleCopy.AnyAllConditions, err = variables.SubstituteAllInPreconditions(log, ctx.JSONContext, ruleCopy.AnyAllConditions)
if err != nil {
log.V(4).Info("failed to substitute vars in preconditions, skip current rule", "rule name", rule.Name)
return nil
}
preconditions, err := transformConditions(ruleCopy.AnyAllConditions)
if err != nil {
log.V(2).Info("wrongfully configured data", "reason", err.Error())
continue
}
// evaluate pre-conditions
if !variables.EvaluateConditions(log, ctx.JSONContext, preconditions) {
log.V(4).Info("resource fails the preconditions")
continue
}
if rule.Validation.Pattern != nil || rule.Validation.AnyPattern != nil {
if *ruleCopy, err = substituteAll(log, ctx, *ruleCopy, resp); err != nil {
continue
}
ruleResponse := validateResourceWithRule(log, ctx, *ruleCopy)
if ruleResponse != nil {
if !common.IsConditionalAnchorError(ruleResponse.Message) {
incrementAppliedCount(resp)
resp.PolicyResponse.Rules = append(resp.PolicyResponse.Rules, *ruleResponse)
}
}
} else if rule.Validation.Deny != nil {
ruleCopy.Validation.Deny.AnyAllConditions, err = variables.SubstituteAllInPreconditions(log, ctx.JSONContext, ruleCopy.Validation.Deny.AnyAllConditions)
if err != nil {
log.V(4).Info("failed to substitute vars in preconditions, skip current rule", "rule name", rule.Name)
continue
}
if *ruleCopy, err = substituteAll(log, ctx, *ruleCopy, resp); err != nil {
continue
}
denyConditions, err := transformConditions(ruleCopy.Validation.Deny.AnyAllConditions)
if err != nil {
log.V(2).Info("wrongfully configured data", "reason", err.Error())
continue
}
deny := variables.EvaluateConditions(log, ctx.JSONContext, denyConditions)
ruleResp := response.RuleResponse{
Name: ruleCopy.Name,
Type: utils.Validation.String(),
Message: ruleCopy.Validation.Message,
Success: !deny,
}
incrementAppliedCount(resp)
resp.PolicyResponse.Rules = append(resp.PolicyResponse.Rules, ruleResp)
ruleResp := processValidationRule(log, ctx, &rule)
if ruleResp != nil {
addRuleResponse(log, resp, ruleResp, startTime)
}
}
return resp
}
func validateResourceWithRule(log logr.Logger, ctx *PolicyContext, rule kyverno.Rule) (resp *response.RuleResponse) {
if reflect.DeepEqual(ctx.OldResource, unstructured.Unstructured{}) {
resp := validatePatterns(log, ctx.JSONContext, ctx.NewResource, rule)
return &resp
func processValidationRule(log logr.Logger, ctx *PolicyContext, rule *kyverno.Rule) *response.RuleResponse {
v := newValidator(log, ctx, rule)
if rule.Validation.ForEachValidation != nil {
return v.validateForEach()
}
if reflect.DeepEqual(ctx.NewResource, unstructured.Unstructured{}) {
log.V(3).Info("skipping validation on deleted resource")
return v.validate()
}
func addRuleResponse(log logr.Logger, resp *response.EngineResponse, ruleResp *response.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())
if ruleResp.Status == response.RuleStatusPass || ruleResp.Status == response.RuleStatusFail {
incrementAppliedCount(resp)
} else if ruleResp.Status == response.RuleStatusError {
incrementErrorCount(resp)
}
resp.PolicyResponse.Rules = append(resp.PolicyResponse.Rules, *ruleResp)
}
type validator struct {
log logr.Logger
ctx *PolicyContext
rule *kyverno.Rule
contextEntries []kyverno.ContextEntry
anyAllConditions apiextensions.JSON
pattern apiextensions.JSON
anyPattern apiextensions.JSON
deny *kyverno.Deny
}
func newValidator(log logr.Logger, ctx *PolicyContext, rule *kyverno.Rule) *validator {
ruleCopy := rule.DeepCopy()
return &validator{
log: log,
rule: ruleCopy,
ctx: ctx,
contextEntries: ruleCopy.Context,
anyAllConditions: ruleCopy.AnyAllConditions,
pattern: ruleCopy.Validation.Pattern,
anyPattern: ruleCopy.Validation.AnyPattern,
deny: ruleCopy.Validation.Deny,
}
}
func newForeachValidator(log logr.Logger, ctx *PolicyContext, rule *kyverno.Rule) *validator {
ruleCopy := rule.DeepCopy()
// Variable substitution expects JSON data, so we convert to a map
anyAllConditions, err := common.ToMap(ruleCopy.Validation.ForEachValidation.AnyAllConditions)
if err != nil {
log.Error(err, "failed to convert ruleCopy.Validation.ForEachValidation.AnyAllConditions")
}
return &validator{
log: log,
ctx: ctx,
rule: ruleCopy,
contextEntries: ruleCopy.Validation.ForEachValidation.Context,
anyAllConditions: anyAllConditions,
pattern: ruleCopy.Validation.ForEachValidation.Pattern,
anyPattern: ruleCopy.Validation.ForEachValidation.AnyPattern,
deny: ruleCopy.Validation.ForEachValidation.Deny,
}
}
func (v *validator) validate() *response.RuleResponse {
if err := v.loadContext(); err != nil {
return ruleError(v.rule, "failed to load context", err)
}
preconditionsPassed, err := v.checkPreconditions()
if err != nil {
return ruleError(v.rule, "failed to evaluate preconditions", err)
} else if !preconditionsPassed {
return ruleResponse(v.rule, "preconditions not met", response.RuleStatusSkip)
}
if v.pattern != nil || v.anyPattern != nil {
if err = v.substitutePatterns(); err != nil {
return ruleError(v.rule, "variable substitution failed", err)
}
ruleResponse := v.validateResourceWithRule()
return ruleResponse
} else if v.deny != nil {
ruleResponse := v.validateDeny()
return ruleResponse
}
v.log.Info("invalid validation rule: either patterns or deny conditions are expected")
return nil
}
func (v *validator) validateForEach() *response.RuleResponse {
if err := v.loadContext(); err != nil {
return ruleError(v.rule, "failed to load context", err)
}
preconditionsPassed, err := v.checkPreconditions()
if err != nil {
return ruleError(v.rule, "failed to evaluate preconditions", err)
} else if !preconditionsPassed {
return ruleResponse(v.rule, "preconditions not met", response.RuleStatusSkip)
}
foreach := v.rule.Validation.ForEachValidation
if foreach == nil {
return nil
}
oldResp := validatePatterns(log, ctx.JSONContext, ctx.OldResource, rule)
newResp := validatePatterns(log, ctx.JSONContext, ctx.NewResource, rule)
elements, err := v.evaluateList(foreach.List)
if err != nil {
msg := fmt.Sprintf("failed to evaluate list %s", foreach.List)
return ruleError(v.rule, msg, err)
}
v.ctx.JSONContext.Checkpoint()
defer v.ctx.JSONContext.Restore()
applyCount := 0
for _, e := range elements {
v.ctx.JSONContext.Reset()
ctx := v.ctx.Copy()
if err := addElementToContext(ctx, e); err != nil {
v.log.Error(err, "failed to add element to context")
return ruleError(v.rule, "failed to process foreach", err)
}
foreachValidator := newForeachValidator(v.log, ctx, v.rule)
r := foreachValidator.validate()
if r == nil {
v.log.Info("skipping rule due to empty result")
continue
} else if r.Status == response.RuleStatusSkip {
v.log.Info("skipping rule as preconditions were not met")
continue
} else if r.Status != response.RuleStatusPass {
msg := fmt.Sprintf("validation failed in foreach rule for %v", r.Message)
return ruleResponse(v.rule, msg, r.Status)
}
applyCount++
}
if applyCount == 0 {
return ruleResponse(v.rule, "rule skipped", response.RuleStatusSkip)
}
return ruleResponse(v.rule, "rule passed", response.RuleStatusPass)
}
func addElementToContext(ctx *PolicyContext, e interface{}) error {
data, err := common.ToMap(e)
if err != nil {
return err
}
u := unstructured.Unstructured{}
u.SetUnstructuredContent(data)
ctx.NewResource = u
if err := ctx.JSONContext.AddResourceAsObject(e); err != nil {
return errors.Wrapf(err, "failed to add resource (%v) to JSON context", e)
}
return nil
}
func (v *validator) evaluateList(jmesPath string) ([]interface{}, error) {
i, err := v.ctx.JSONContext.Query(jmesPath)
if err != nil {
return nil, err
}
l, ok := i.([]interface{})
if !ok {
return []interface{}{i}, nil
}
return l, nil
}
func (v *validator) loadContext() error {
if err := LoadContext(v.log, v.contextEntries, v.ctx.ResourceCache, v.ctx, v.rule.Name); err != nil {
if _, ok := err.(gojmespath.NotFoundError); ok {
v.log.V(3).Info("failed to load context", "reason", err.Error())
} else {
v.log.Error(err, "failed to load context")
}
return err
}
return nil
}
func (v *validator) checkPreconditions() (bool, error) {
preconditions, err := variables.SubstituteAllInPreconditions(v.log, v.ctx.JSONContext, v.anyAllConditions)
if err != nil {
return false, errors.Wrapf(err, "failed to substitute variables in preconditions")
}
typeConditions, err := transformConditions(preconditions)
if err != nil {
return false, errors.Wrapf(err, "failed to parse preconditions")
}
pass := variables.EvaluateConditions(v.log, v.ctx.JSONContext, typeConditions)
return pass, nil
}
func (v *validator) validateDeny() *response.RuleResponse {
anyAllCond := v.deny.AnyAllConditions
anyAllCond, err := variables.SubstituteAll(v.log, v.ctx.JSONContext, anyAllCond)
if err != nil {
return ruleError(v.rule, "failed to substitute variables in deny conditions", err)
}
if err = v.substituteDeny(); err != nil {
return ruleError(v.rule, "failed to substitute variables in rule", err)
}
denyConditions, err := transformConditions(anyAllCond)
if err != nil {
return ruleError(v.rule, "invalid deny conditions", err)
}
deny := variables.EvaluateConditions(v.log, v.ctx.JSONContext, denyConditions)
if deny {
return ruleResponse(v.rule, v.getDenyMessage(deny), response.RuleStatusFail)
}
return ruleResponse(v.rule, v.getDenyMessage(deny), response.RuleStatusPass)
}
func (v *validator) getDenyMessage(deny bool) string {
if !deny {
return fmt.Sprintf("validation rule '%s' passed.", v.rule.Name)
}
msg := v.rule.Validation.Message
if msg == "" {
return fmt.Sprintf("validation error: rule %s failed", v.rule.Name)
}
raw, err := variables.SubstituteAll(v.log, v.ctx.JSONContext, msg)
if err != nil {
return msg
}
return raw.(string)
}
func (v *validator) validateResourceWithRule() *response.RuleResponse {
if reflect.DeepEqual(v.ctx.OldResource, unstructured.Unstructured{}) {
resp := v.validatePatterns(v.ctx.NewResource)
return resp
}
if reflect.DeepEqual(v.ctx.NewResource, unstructured.Unstructured{}) {
v.log.V(3).Info("skipping validation on deleted resource")
return nil
}
oldResp := v.validatePatterns(v.ctx.OldResource)
newResp := v.validatePatterns(v.ctx.NewResource)
if isSameRuleResponse(oldResp, newResp) {
log.V(3).Info("skipping modified resource as validation results have not changed")
v.log.V(3).Info("skipping modified resource as validation results have not changed")
return nil
}
return &newResp
return newResp
}
// matches checks if either the new or old resource satisfies the filter conditions defined in the rule
@ -214,7 +413,7 @@ func matches(logger logr.Logger, rule kyverno.Rule, ctx *PolicyContext) bool {
return false
}
func isSameRuleResponse(r1 response.RuleResponse, r2 response.RuleResponse) bool {
func isSameRuleResponse(r1 *response.RuleResponse, r2 *response.RuleResponse) bool {
if r1.Name != r2.Name {
return false
}
@ -227,7 +426,7 @@ func isSameRuleResponse(r1 response.RuleResponse, r2 response.RuleResponse) bool
return false
}
if r1.Success != r2.Success {
if r1.Status != r2.Status {
return false
}
@ -235,57 +434,52 @@ func isSameRuleResponse(r1 response.RuleResponse, r2 response.RuleResponse) bool
}
// validatePatterns validate pattern and anyPattern
func validatePatterns(log logr.Logger, ctx context.EvalInterface, resource unstructured.Unstructured, rule kyverno.Rule) (resp response.RuleResponse) {
startTime := time.Now()
logger := log.WithValues("rule", rule.Name, "name", resource.GetName(), "kind", resource.GetKind())
logger.V(5).Info("start processing rule", "startTime", startTime)
resp.Name = rule.Name
resp.Type = utils.Validation.String()
defer func() {
resp.RuleStats.ProcessingTime = time.Since(startTime)
resp.RuleStats.RuleExecutionTimestamp = startTime.Unix()
logger.V(4).Info("finished processing rule", "processingTime", resp.RuleStats.ProcessingTime.String())
}()
func (v *validator) validatePatterns(resource unstructured.Unstructured) *response.RuleResponse {
if v.pattern != nil {
if err := validate.MatchPattern(v.log, resource.Object, v.pattern); err != nil {
validationRule := rule.Validation.DeepCopy()
if validationRule.Pattern != nil {
pattern := validationRule.Pattern
if pe, ok := err.(*validate.PatternError); ok {
v.log.V(3).Info("validation error", "path", pe.Path, "error", err.Error())
if pe.Path == "" {
return ruleResponse(v.rule, v.buildErrorMessage(err, ""), response.RuleStatusError)
}
if path, err := validate.ValidateResourceWithPattern(logger, resource.Object, pattern); err != nil {
logger.V(3).Info("validation failed", "path", path, "error", err.Error())
resp.Success = false
resp.Message = buildErrorMessage(rule, path)
return resp
return ruleResponse(v.rule, v.buildErrorMessage(err, pe.Path), response.RuleStatusFail)
}
}
logger.V(4).Info("successfully processed rule")
resp.Success = true
resp.Message = fmt.Sprintf("validation rule '%s' passed.", rule.Name)
return resp
v.log.V(4).Info("successfully processed rule")
msg := fmt.Sprintf("validation rule '%s' passed.", v.rule.Name)
return ruleResponse(v.rule, msg, response.RuleStatusPass)
}
if validationRule.AnyPattern != nil {
if v.anyPattern != nil {
var failedAnyPatternsErrors []error
var err error
anyPatterns, err := rule.Validation.DeserializeAnyPattern()
anyPatterns, err := deserializeAnyPattern(v.anyPattern)
if err != nil {
resp.Success = false
resp.Message = fmt.Sprintf("failed to deserialize anyPattern, expected type array: %v", err)
return resp
msg := fmt.Sprintf("failed to deserialize anyPattern, expected type array: %v", err)
return ruleResponse(v.rule, msg, response.RuleStatusError)
}
for idx, pattern := range anyPatterns {
path, err := validate.ValidateResourceWithPattern(logger, resource.Object, pattern)
err := validate.MatchPattern(v.log, resource.Object, pattern)
if err == nil {
resp.Success = true
resp.Message = fmt.Sprintf("validation rule '%s' anyPattern[%d] passed.", rule.Name, idx)
return resp
msg := fmt.Sprintf("validation rule '%s' anyPattern[%d] passed.", v.rule.Name, idx)
return ruleResponse(v.rule, msg, response.RuleStatusPass)
}
logger.V(4).Info("validation rule failed", "anyPattern[%d]", idx, "path", path)
patternErr := fmt.Errorf("Rule %s[%d] failed at path %s.", rule.Name, idx, path)
failedAnyPatternsErrors = append(failedAnyPatternsErrors, patternErr)
if pe, ok := err.(*validate.PatternError); ok {
v.log.V(3).Info("validation rule failed", "anyPattern[%d]", idx, "path", pe.Path)
if pe.Path == "" {
patternErr := fmt.Errorf("Rule %s[%d] failed: %s.", v.rule.Name, idx, err.Error())
failedAnyPatternsErrors = append(failedAnyPatternsErrors, patternErr)
} else {
patternErr := fmt.Errorf("Rule %s[%d] failed at path %s.", v.rule.Name, idx, pe.Path)
failedAnyPatternsErrors = append(failedAnyPatternsErrors, patternErr)
}
}
}
// Any Pattern validation errors
@ -295,30 +489,60 @@ func validatePatterns(log logr.Logger, ctx context.EvalInterface, resource unstr
errorStr = append(errorStr, err.Error())
}
log.V(4).Info(fmt.Sprintf("Validation rule '%s' failed. %s", rule.Name, errorStr))
resp.Success = false
resp.Message = buildAnyPatternErrorMessage(rule, errorStr)
return resp
v.log.V(4).Info(fmt.Sprintf("Validation rule '%s' failed. %s", v.rule.Name, errorStr))
msg := buildAnyPatternErrorMessage(v.rule, errorStr)
return ruleResponse(v.rule, msg, response.RuleStatusFail)
}
}
return resp
return ruleResponse(v.rule, v.rule.Validation.Message, response.RuleStatusPass)
}
func buildErrorMessage(rule kyverno.Rule, path string) string {
if rule.Validation.Message == "" {
return fmt.Sprintf("validation error: rule %s failed at path %s", rule.Name, path)
func deserializeAnyPattern(anyPattern apiextensions.JSON) ([]interface{}, error) {
if anyPattern == nil {
return nil, nil
}
if strings.HasSuffix(rule.Validation.Message, ".") {
return fmt.Sprintf("validation error: %s Rule %s failed at path %s", rule.Validation.Message, rule.Name, path)
ap, err := json.Marshal(anyPattern)
if err != nil {
return nil, err
}
return fmt.Sprintf("validation error: %s. Rule %s failed at path %s", rule.Validation.Message, rule.Name, path)
var res []interface{}
if err := json.Unmarshal(ap, &res); err != nil {
return nil, err
}
return res, nil
}
func buildAnyPatternErrorMessage(rule kyverno.Rule, errors []string) string {
func (v *validator) buildErrorMessage(err error, path string) string {
if v.rule.Validation.Message == "" {
if path != "" {
return fmt.Sprintf("validation error: rule %s failed at path %s", v.rule.Name, path)
}
return fmt.Sprintf("validation error: rule %s execution error: %s", v.rule.Name, err.Error())
}
msgRaw, err := variables.SubstituteAll(v.log, v.ctx.JSONContext, v.rule.Validation.Message)
if err != nil {
v.log.Info("failed to substitute variables in message: %v", err)
}
msg := msgRaw.(string)
if !strings.HasSuffix(msg, ".") {
msg = msg + "."
}
if path != "" {
return fmt.Sprintf("validation error: %s Rule %s failed at path %s", msg, v.rule.Name, path)
}
return fmt.Sprintf("validation error: %s Rule %s execution error: %s", msg, v.rule.Name, err.Error())
}
func buildAnyPatternErrorMessage(rule *kyverno.Rule, errors []string) string {
errStr := strings.Join(errors, " ")
if rule.Validation.Message == "" {
return fmt.Sprintf("validation error: %s", errStr)
@ -331,28 +555,54 @@ func buildAnyPatternErrorMessage(rule kyverno.Rule, errors []string) string {
return fmt.Sprintf("validation error: %s. %s", rule.Validation.Message, errStr)
}
func substituteAll(log logr.Logger, ctx *PolicyContext, rule kyverno.Rule, resp *response.EngineResponse) (kyverno.Rule, error) {
var err error
if rule, err = variables.SubstituteAllInRule(log, ctx.JSONContext, rule); err != nil {
ruleResp := response.RuleResponse{
Name: rule.Name,
Type: utils.Validation.String(),
Message: fmt.Sprintf("variable substitution failed for rule %s: %s", rule.Name, err.Error()),
Success: true,
func (v *validator) substitutePatterns() error {
if v.pattern != nil {
i, err := variables.SubstituteAll(v.log, v.ctx.JSONContext, v.pattern)
if err != nil {
return err
}
incrementAppliedCount(resp)
resp.PolicyResponse.Rules = append(resp.PolicyResponse.Rules, ruleResp)
switch err.(type) {
case gojmespath.NotFoundError:
log.V(2).Info("failed to substitute variables, skip current rule", "info", err.Error(), "rule name", rule.Name)
default:
log.Error(err, "failed to substitute variables, skip current rule", "rule name", rule.Name)
}
return rule, err
v.pattern = i.(apiextensions.JSON)
return nil
}
return rule, nil
if v.anyPattern != nil {
i, err := variables.SubstituteAll(v.log, v.ctx.JSONContext, v.anyPattern)
if err != nil {
return err
}
v.anyPattern = i.(apiextensions.JSON)
return nil
}
return nil
}
func (v *validator) substituteDeny() error {
if v.deny == nil {
return nil
}
i, err := variables.SubstituteAll(v.log, v.ctx.JSONContext, v.deny)
if err != nil {
return err
}
v.deny = i.(*kyverno.Deny)
return nil
}
func ruleError(rule *kyverno.Rule, msg string, err error) *response.RuleResponse {
msg = fmt.Sprintf("%s: %s", msg, err.Error())
return ruleResponse(rule, msg, response.RuleStatusError)
}
func ruleResponse(rule *kyverno.Rule, msg string, status response.RuleStatus) *response.RuleResponse {
return &response.RuleResponse{
Name: rule.Name,
Type: utils.Validation.String(),
Message: msg,
Status: status,
}
}

View file

@ -2,6 +2,7 @@ package engine
import (
"encoding/json"
"github.com/kyverno/kyverno/pkg/engine/response"
"testing"
kyverno "github.com/kyverno/kyverno/pkg/api/kyverno/v1"
@ -127,10 +128,12 @@ func TestValidate_image_tag_fail(t *testing.T) {
"validation rule 'validate-tag' passed.",
"validation error: imagePullPolicy 'Always' required with tag 'latest'. Rule validate-latest failed at path /spec/containers/0/imagePullPolicy/",
}
er := Validate(&PolicyContext{Policy: policy, NewResource: *resourceUnstructured, JSONContext: context.NewContext()})
for index, r := range er.PolicyResponse.Rules {
assert.Equal(t, r.Message, msgs[index])
}
assert.Assert(t, !er.IsSuccessful())
}
@ -1474,9 +1477,9 @@ func Test_VariableSubstitutionPathNotExistInPattern(t *testing.T) {
JSONContext: ctx,
NewResource: *resourceUnstructured}
er := Validate(policyContext)
assert.Assert(t, er.PolicyResponse.Rules[0].Success)
assert.Equal(t, er.PolicyResponse.Rules[0].Status, response.RuleStatusError)
assert.Equal(t, er.PolicyResponse.Rules[0].Message,
"variable substitution failed for rule test-path-not-exist: Unknown key \"name1\" in path")
"variable substitution failed: Unknown key \"name1\" in path")
}
func Test_VariableSubstitutionPathNotExistInAnyPattern_OnePatternStatisfiesButSubstitutionFails(t *testing.T) {
@ -1566,8 +1569,8 @@ func Test_VariableSubstitutionPathNotExistInAnyPattern_OnePatternStatisfiesButSu
JSONContext: ctx,
NewResource: *resourceUnstructured}
er := Validate(policyContext)
assert.Assert(t, er.PolicyResponse.Rules[0].Success)
assert.Equal(t, er.PolicyResponse.Rules[0].Message, "variable substitution failed for rule test-path-not-exist: Unknown key \"name1\" in path")
assert.Equal(t, er.PolicyResponse.Rules[0].Status, response.RuleStatusError)
assert.Equal(t, er.PolicyResponse.Rules[0].Message, "variable substitution failed: Unknown key \"name1\" in path")
}
func Test_VariableSubstitution_NotOperatorWithStringVariable(t *testing.T) {
@ -1625,7 +1628,7 @@ func Test_VariableSubstitution_NotOperatorWithStringVariable(t *testing.T) {
JSONContext: ctx,
NewResource: *resourceUnstructured}
er := Validate(policyContext)
assert.Assert(t, !er.PolicyResponse.Rules[0].Success)
assert.Equal(t, er.PolicyResponse.Rules[0].Status, response.RuleStatusFail)
assert.Equal(t, er.PolicyResponse.Rules[0].Message, "validation error: rule not-operator-with-variable-should-alway-fail-validation failed at path /spec/content/")
}
@ -1716,8 +1719,8 @@ func Test_VariableSubstitutionPathNotExistInAnyPattern_AllPathNotPresent(t *test
JSONContext: ctx,
NewResource: *resourceUnstructured}
er := Validate(policyContext)
assert.Assert(t, er.PolicyResponse.Rules[0].Success)
assert.Equal(t, er.PolicyResponse.Rules[0].Message, "variable substitution failed for rule test-path-not-exist: Unknown key \"name1\" in path")
assert.Equal(t, er.PolicyResponse.Rules[0].Status, response.RuleStatusError)
assert.Equal(t, er.PolicyResponse.Rules[0].Message, "variable substitution failed: Unknown key \"name1\" in path")
}
func Test_VariableSubstitutionPathNotExistInAnyPattern_AllPathPresent_NonePatternSatisfy(t *testing.T) {
@ -1808,7 +1811,7 @@ func Test_VariableSubstitutionPathNotExistInAnyPattern_AllPathPresent_NonePatter
NewResource: *resourceUnstructured}
er := Validate(policyContext)
assert.Assert(t, !er.PolicyResponse.Rules[0].Success)
assert.Equal(t, er.PolicyResponse.Rules[0].Status, response.RuleStatusFail)
assert.Equal(t, er.PolicyResponse.Rules[0].Message,
"validation error: Rule test-path-not-exist[0] failed at path /spec/template/spec/containers/0/name/. Rule test-path-not-exist[1] failed at path /spec/template/spec/containers/0/name/.")
}
@ -1912,41 +1915,41 @@ func Test_VariableSubstitutionValidate_VariablesInMessageAreResolved(t *testing.
JSONContext: ctx,
NewResource: *resourceUnstructured}
er := Validate(policyContext)
assert.Assert(t, !er.PolicyResponse.Rules[0].Success)
assert.Equal(t, er.PolicyResponse.Rules[0].Status, response.RuleStatusFail)
assert.Equal(t, er.PolicyResponse.Rules[0].Message, "The animal cow is not in the allowed list of animals.")
}
func Test_Flux_Kustomization_PathNotPresent(t *testing.T) {
tests := []struct {
name string
policyRaw []byte
resourceRaw []byte
expectedResult bool
expectedMessage string
name string
policyRaw []byte
resourceRaw []byte
expectedResults []response.RuleStatus
expectedMessages []string
}{
{
name: "path-not-present",
policyRaw: []byte(`{"apiVersion":"kyverno.io/v1","kind":"ClusterPolicy","metadata":{"name":"flux-multi-tenancy"},"spec":{"validationFailureAction":"enforce","rules":[{"name":"serviceAccountName","exclude":{"resources":{"namespaces":["flux-system"]}},"match":{"resources":{"kinds":["Kustomization","HelmRelease"]}},"validate":{"message":".spec.serviceAccountName is required","pattern":{"spec":{"serviceAccountName":"?*"}}}},{"name":"sourceRefNamespace","exclude":{"resources":{"namespaces":["flux-system"]}},"match":{"resources":{"kinds":["Kustomization","HelmRelease"]}},"validate":{"message":"spec.sourceRef.namespace must be the same as metadata.namespace","deny":{"conditions":[{"key":"{{request.object.spec.sourceRef.namespace}}","operator":"NotEquals","value":"{{request.object.metadata.namespace}}"}]}}}]}}`),
// referred variable path not present
resourceRaw: []byte(`{"apiVersion":"kustomize.toolkit.fluxcd.io/v1beta1","kind":"Kustomization","metadata":{"name":"dev-team","namespace":"apps"},"spec":{"serviceAccountName":"dev-team","interval":"5m","sourceRef":{"kind":"GitRepository","name":"dev-team"},"prune":true,"validation":"client"}}`),
expectedResult: false,
expectedMessage: "spec.sourceRef.namespace must be the same as metadata.namespace",
resourceRaw: []byte(`{"apiVersion":"kustomize.toolkit.fluxcd.io/v1beta1","kind":"Kustomization","metadata":{"name":"dev-team","namespace":"apps"},"spec":{"serviceAccountName":"dev-team","interval":"5m","sourceRef":{"kind":"GitRepository","name":"dev-team"},"prune":true,"validation":"client"}}`),
expectedResults: []response.RuleStatus{response.RuleStatusPass, response.RuleStatusError},
expectedMessages: []string{"validation rule 'serviceAccountName' passed.", "failed to substitute variables in deny conditions: Unknown key \"namespace\" in path"},
},
{
name: "resource-with-violation",
policyRaw: []byte(`{"apiVersion":"kyverno.io/v1","kind":"ClusterPolicy","metadata":{"name":"flux-multi-tenancy"},"spec":{"validationFailureAction":"enforce","rules":[{"name":"serviceAccountName","exclude":{"resources":{"namespaces":["flux-system"]}},"match":{"resources":{"kinds":["Kustomization","HelmRelease"]}},"validate":{"message":".spec.serviceAccountName is required","pattern":{"spec":{"serviceAccountName":"?*"}}}},{"name":"sourceRefNamespace","exclude":{"resources":{"namespaces":["flux-system"]}},"match":{"resources":{"kinds":["Kustomization","HelmRelease"]}},"validate":{"message":"spec.sourceRef.namespace {{request.object.spec.sourceRef.namespace}} must be the same as metadata.namespace {{request.object.metadata.namespace}}","deny":{"conditions":[{"key":"{{request.object.spec.sourceRef.namespace}}","operator":"NotEquals","value":"{{request.object.metadata.namespace}}"}]}}}]}}`),
// referred variable path present with different value
resourceRaw: []byte(`{"apiVersion":"kustomize.toolkit.fluxcd.io/v1beta1","kind":"Kustomization","metadata":{"name":"dev-team","namespace":"apps"},"spec":{"serviceAccountName":"dev-team","interval":"5m","sourceRef":{"kind":"GitRepository","name":"dev-team","namespace":"default"},"prune":true,"validation":"client"}}`),
expectedResult: false,
expectedMessage: "spec.sourceRef.namespace default must be the same as metadata.namespace apps",
resourceRaw: []byte(`{"apiVersion":"kustomize.toolkit.fluxcd.io/v1beta1","kind":"Kustomization","metadata":{"name":"dev-team","namespace":"apps"},"spec":{"serviceAccountName":"dev-team","interval":"5m","sourceRef":{"kind":"GitRepository","name":"dev-team","namespace":"default"},"prune":true,"validation":"client"}}`),
expectedResults: []response.RuleStatus{response.RuleStatusPass, response.RuleStatusFail},
expectedMessages: []string{"validation rule 'serviceAccountName' passed.", "spec.sourceRef.namespace default must be the same as metadata.namespace apps"},
},
{
name: "resource-comply",
policyRaw: []byte(`{"apiVersion":"kyverno.io/v1","kind":"ClusterPolicy","metadata":{"name":"flux-multi-tenancy"},"spec":{"validationFailureAction":"enforce","rules":[{"name":"serviceAccountName","exclude":{"resources":{"namespaces":["flux-system"]}},"match":{"resources":{"kinds":["Kustomization","HelmRelease"]}},"validate":{"message":".spec.serviceAccountName is required","pattern":{"spec":{"serviceAccountName":"?*"}}}},{"name":"sourceRefNamespace","exclude":{"resources":{"namespaces":["flux-system"]}},"match":{"resources":{"kinds":["Kustomization","HelmRelease"]}},"validate":{"message":"spec.sourceRef.namespace must be the same as metadata.namespace","deny":{"conditions":[{"key":"{{request.object.spec.sourceRef.namespace}}","operator":"NotEquals","value":"{{request.object.metadata.namespace}}"}]}}}]}}`),
// referred variable path present with same value - validate passes
resourceRaw: []byte(`{"apiVersion":"kustomize.toolkit.fluxcd.io/v1beta1","kind":"Kustomization","metadata":{"name":"dev-team","namespace":"apps"},"spec":{"serviceAccountName":"dev-team","interval":"5m","sourceRef":{"kind":"GitRepository","name":"dev-team","namespace":"apps"},"prune":true,"validation":"client"}}`),
expectedResult: true,
expectedMessage: "spec.sourceRef.namespace must be the same as metadata.namespace",
resourceRaw: []byte(`{"apiVersion":"kustomize.toolkit.fluxcd.io/v1beta1","kind":"Kustomization","metadata":{"name":"dev-team","namespace":"apps"},"spec":{"serviceAccountName":"dev-team","interval":"5m","sourceRef":{"kind":"GitRepository","name":"dev-team","namespace":"apps"},"prune":true,"validation":"client"}}`),
expectedResults: []response.RuleStatus{response.RuleStatusPass, response.RuleStatusPass},
expectedMessages: []string{"validation rule 'serviceAccountName' passed.", "validation rule 'sourceRefNamespace' passed."},
},
}
@ -1967,10 +1970,8 @@ func Test_Flux_Kustomization_PathNotPresent(t *testing.T) {
er := Validate(policyContext)
for i, rule := range er.PolicyResponse.Rules {
if rule.Name == "sourceRefNamespace" {
assert.Equal(t, er.PolicyResponse.Rules[i].Success, test.expectedResult)
assert.Equal(t, er.PolicyResponse.Rules[i].Message, test.expectedMessage, "\ntest %s failed\nexpected: %s\nactual: %s", test.name, test.expectedMessage, rule.Message)
}
assert.Equal(t, er.PolicyResponse.Rules[i].Status, test.expectedResults[i], "\ntest %s failed\nexpected: %s\nactual: %s", test.name, test.expectedResults[i].String(), er.PolicyResponse.Rules[i].Status.String())
assert.Equal(t, er.PolicyResponse.Rules[i].Message, test.expectedMessages[i], "\ntest %s failed\nexpected: %s\nactual: %s", test.name, test.expectedMessages[i], rule.Message)
}
}
}
@ -2417,3 +2418,329 @@ func Test_StringInDenyCondition(t *testing.T) {
er := Validate(&PolicyContext{Policy: policy, NewResource: *resourceUnstructured, JSONContext: ctx})
assert.Assert(t, er.IsSuccessful())
}
func Test_foreach_container_pass(t *testing.T) {
resourceRaw := []byte(`{
"apiVersion": "v1",
"kind": "Deployment",
"metadata": {"name": "test"},
"spec": { "template": { "spec": {
"containers": [
{"name": "pod1-valid", "image": "nginx/nginx:v1"},
{"name": "pod2-valid", "image": "nginx/nginx:v2"},
{"name": "pod3-valid", "image": "nginx/nginx:v3"}
]
}}}}`)
policyraw := []byte(`{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {"name": "test"},
"spec": {
"rules": [
{
"name": "test-path-not-exist",
"match": {"resources": { "kinds": [ "Deployment" ] } },
"validate": {
"foreach": {
"list": "request.object.spec.template.spec.containers",
"pattern": {
"name": "*-valid"
}
}
}}]}}`)
testForEach(t, policyraw, resourceRaw, "", response.RuleStatusPass)
}
func Test_foreach_container_fail(t *testing.T) {
resourceRaw := []byte(`{
"apiVersion": "v1",
"kind": "Deployment",
"metadata": {"name": "test"},
"spec": { "template": { "spec": {
"containers": [
{"name": "pod1-valid", "image": "nginx/nginx:v1"},
{"name": "pod2-invalid", "image": "nginx/nginx:v2"},
{"name": "pod3-valid", "image": "nginx/nginx:v3"}
]
}}}}`)
policyraw := []byte(`{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {"name": "test"},
"spec": {
"rules": [
{
"name": "test",
"match": {"resources": { "kinds": [ "Deployment" ] } },
"validate": {
"foreach": {
"list": "request.object.spec.template.spec.containers",
"pattern": {
"name": "*-valid"
}
}
}}]}}`)
testForEach(t, policyraw, resourceRaw, "", response.RuleStatusFail)
}
func Test_foreach_container_deny_fail(t *testing.T) {
resourceRaw := []byte(`{
"apiVersion": "v1",
"kind": "Deployment",
"metadata": {"name": "test"},
"spec": { "template": { "spec": {
"containers": [
{"name": "pod1-valid", "image": "nginx/nginx:v1"},
{"name": "pod2-invalid", "image": "docker.io/nginx/nginx:v2"},
{"name": "pod3-valid", "image": "nginx/nginx:v3"}
]
}}}}`)
policyraw := []byte(`{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {"name": "test"},
"spec": {
"rules": [
{
"name": "test",
"match": {"resources": { "kinds": [ "Deployment" ] } },
"validate": {
"foreach": {
"list": "request.object.spec.template.spec.containers",
"deny": {
"conditions": [
{"key": "{{ regex_match('{{request.object.image}}', 'docker.io') }}", "operator": "Equals", "value": false}
]
}
}
}}]}}`)
testForEach(t, policyraw, resourceRaw, "", response.RuleStatusFail)
}
func Test_foreach_container_deny_success(t *testing.T) {
resourceRaw := []byte(`{
"apiVersion": "v1",
"kind": "Deployment",
"metadata": {"name": "test"},
"spec": { "template": { "spec": {
"containers": [
{"name": "pod1-valid", "image": "nginx/nginx:v1"},
{"name": "pod2-invalid", "image": "nginx/nginx:v2"},
{"name": "pod3-valid", "image": "nginx/nginx:v3"}
]
}}}}`)
policyraw := []byte(`{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {"name": "test"},
"spec": {
"rules": [
{
"name": "test",
"match": {"resources": { "kinds": [ "Deployment" ] } },
"validate": {
"foreach": {
"list": "request.object.spec.template.spec.containers",
"deny": {
"conditions": [
{"key": "{{ regex_match('{{request.object.image}}', 'docker.io') }}", "operator": "Equals", "value": false}
]
}
}
}}]}}`)
testForEach(t, policyraw, resourceRaw, "", response.RuleStatusFail)
}
func Test_foreach_container_deny_error(t *testing.T) {
resourceRaw := []byte(`{
"apiVersion": "v1",
"kind": "Deployment",
"metadata": {"name": "test"},
"spec": { "template": { "spec": {
"containers": [
{"name": "pod1-valid", "image": "nginx/nginx:v1"},
{"name": "pod2-invalid", "image": "nginx/nginx:v2"},
{"name": "pod3-valid", "image": "nginx/nginx:v3"}
]
}}}}`)
policyraw := []byte(`{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {"name": "test"},
"spec": {
"rules": [
{
"name": "test",
"match": {"resources": { "kinds": [ "Deployment" ] } },
"validate": {
"foreach": {
"list": "request.object.spec.template.spec.containers",
"deny": {
"conditions": [
{"key": "{{ regex_match_INVALID('{{request.object.image}}', 'docker.io') }}", "operator": "Equals", "value": false}
]
}
}
}}]}}`)
testForEach(t, policyraw, resourceRaw, "", response.RuleStatusError)
}
func Test_foreach_context_preconditions(t *testing.T) {
resourceRaw := []byte(`{
"apiVersion": "v1",
"kind": "Deployment",
"metadata": {"name": "test"},
"spec": { "template": { "spec": {
"containers": [
{"name": "podvalid", "image": "nginx/nginx:v1"},
{"name": "podinvalid", "image": "nginx/nginx:v2"}
]
}}}}`)
policyraw := []byte(`{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {"name": "test"},
"spec": {
"rules": [
{
"name": "test",
"match": {"resources": { "kinds": [ "Deployment" ] } },
"validate": {
"foreach": {
"list": "request.object.spec.template.spec.containers",
"context": [{"name": "img", "configMap": {"name": "mycmap", "namespace": "default"}}],
"preconditions": { "all": [
{
"key": "{{request.object.name}}",
"operator": "In",
"value": ["podvalid"]
}
]},
"deny": {
"conditions": [
{"key": "{{ request.object.image }}", "operator": "NotEquals", "value": "{{ img.data.{{ request.object.name }} }}"}
]
}
}
}}]}}`)
configMapVariableContext := store.Context{
Policies: []store.Policy{
{
Name: "test",
Rules: []store.Rule{
{
Name: "test",
Values: map[string]string{
"img.data.podvalid": "nginx/nginx:v1",
"img.data.podinvalid": "nginx/nginx:v2",
},
},
},
},
},
}
store.SetContext(configMapVariableContext)
store.SetMock(true)
testForEach(t, policyraw, resourceRaw, "", response.RuleStatusPass)
}
func Test_foreach_context_preconditions_fail(t *testing.T) {
resourceRaw := []byte(`{
"apiVersion": "v1",
"kind": "Deployment",
"metadata": {"name": "test"},
"spec": { "template": { "spec": {
"containers": [
{"name": "podvalid", "image": "nginx/nginx:v1"},
{"name": "podinvalid", "image": "nginx/nginx:v2"}
]
}}}}`)
policyraw := []byte(`{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {"name": "test"},
"spec": {
"rules": [
{
"name": "test",
"match": {"resources": { "kinds": [ "Deployment" ] } },
"validate": {
"foreach": {
"list": "request.object.spec.template.spec.containers",
"context": [{"name": "img", "configMap": {"name": "mycmap", "namespace": "default"}}],
"preconditions": { "all": [
{
"key": "{{request.object.name}}",
"operator": "In",
"value": ["podvalid", "podinvalid"]
}
]},
"deny": {
"conditions": [
{"key": "{{ request.object.image }}", "operator": "NotEquals", "value": "{{ img.data.{{ request.object.name }} }}"}
]
}
}
}}]}}`)
configMapVariableContext := store.Context{
Policies: []store.Policy{
{
Name: "test",
Rules: []store.Rule{
{
Name: "test",
Values: map[string]string{
"img.data.podvalid": "nginx/nginx:v1",
"img.data.podinvalid": "nginx/nginx:v1",
},
},
},
},
},
}
store.SetContext(configMapVariableContext)
store.SetMock(true)
testForEach(t, policyraw, resourceRaw, "", response.RuleStatusFail)
}
func testForEach(t *testing.T, policyraw []byte, resourceRaw []byte, msg string, status response.RuleStatus) {
var policy kyverno.ClusterPolicy
assert.NilError(t, json.Unmarshal(policyraw, &policy))
resourceUnstructured, err := utils.ConvertToUnstructured(resourceRaw)
assert.NilError(t, err)
ctx := context.NewContext()
err = ctx.AddResource(resourceRaw)
assert.NilError(t, err)
policyContext := &PolicyContext{
Policy: policy,
JSONContext: ctx,
NewResource: *resourceUnstructured}
er := Validate(policyContext)
assert.Equal(t, er.PolicyResponse.Rules[0].Status, status)
if msg != "" {
assert.Equal(t, er.PolicyResponse.Rules[0].Message, msg)
}
}

View file

@ -43,22 +43,13 @@ func ReplaceAllVars(src string, repl func(string) string) string {
return RegexVariables.ReplaceAllStringFunc(src, repl)
}
func SubstituteAll(log logr.Logger, ctx context.EvalInterface, document interface{}) (_ interface{}, err error) {
document, err = substituteReferences(log, document)
if err != nil {
return kyverno.Rule{}, err
}
return substituteVars(log, ctx, document, DefaultVariableResolver)
}
func newPreconditionsVariableResolver(log logr.Logger) VariableResolver {
// PreconditionsVariableResolver is used to substitute vars in preconditions.
// It returns empty string if error occured during substitution
// It returns an empty string if an error occurs during the substitution.
return func(ctx context.EvalInterface, variable string) (interface{}, error) {
value, err := DefaultVariableResolver(ctx, variable)
if err != nil {
log.V(4).Info(fmt.Sprintf("Variable \"%s\" is not resolved in preconditions. Considering it as an empty string", variable))
log.V(4).Info(fmt.Sprintf("using empty string for unresolved variable \"%s\" in preconditions", variable))
return "", nil
}
@ -66,13 +57,110 @@ func newPreconditionsVariableResolver(log logr.Logger) VariableResolver {
}
}
// SubstituteAll substitutes variables and references in the document. The document must be JSON data
// i.e. string, []interface{}, map[string]interface{}
func SubstituteAll(log logr.Logger, ctx context.EvalInterface, document interface{}) (_ interface{}, err error) {
return substituteAll(log, ctx, document, DefaultVariableResolver)
}
func SubstituteAllInPreconditions(log logr.Logger, ctx context.EvalInterface, document interface{}) (_ interface{}, err error) {
document, err = substituteReferences(log, document)
return substituteAll(log, ctx, document, newPreconditionsVariableResolver(log))
}
func SubstituteAllInRule(log logr.Logger, ctx context.EvalInterface, typedRule kyverno.Rule) (_ kyverno.Rule, err error) {
var rule interface{}
rule, err = RuleToUntyped(typedRule)
if err != nil {
return typedRule, err
}
rule, err = SubstituteAll(log, ctx, rule)
if err != nil {
return typedRule, err
}
return UntypedToRule(rule)
}
func RuleToUntyped(rule kyverno.Rule) (interface{}, error) {
jsonRule, err := json.Marshal(rule)
if err != nil {
return nil, err
}
var untyped interface{}
err = json.Unmarshal(jsonRule, &untyped)
if err != nil {
return nil, err
}
return untyped, nil
}
func UntypedToRule(untyped interface{}) (kyverno.Rule, error) {
jsonRule, err := json.Marshal(untyped)
if err != nil {
return kyverno.Rule{}, err
}
return substituteVars(log, ctx, document, newPreconditionsVariableResolver(log))
var rule kyverno.Rule
err = json.Unmarshal(jsonRule, &rule)
if err != nil {
return kyverno.Rule{}, err
}
return rule, nil
}
func SubstituteAllInConditions(log logr.Logger, ctx context.EvalInterface, conditions []*kyverno.AnyAllConditions) ([]*kyverno.AnyAllConditions, error) {
c, err := ConditionsToJSONObject(conditions)
if err != nil {
return nil, err
}
i, err := SubstituteAll(log, ctx, c)
if err != nil {
return nil, err
}
return JSONObjectToConditions(i)
}
func ConditionsToJSONObject(conditions []*kyverno.AnyAllConditions) ([]map[string]interface{}, error){
bytes, err := json.Marshal(conditions)
if err != nil {
return nil, err
}
var m = []map[string]interface{}{}
if err := json.Unmarshal(bytes, &m); err != nil {
return nil, err
}
return m, nil
}
func JSONObjectToConditions(data interface{}) ([]*kyverno.AnyAllConditions, error) {
bytes, err := json.Marshal(data)
if err != nil {
return nil, err
}
var c []*kyverno.AnyAllConditions
if err := json.Unmarshal(bytes, &c); err != nil {
return nil, err
}
return c, nil
}
func substituteAll(log logr.Logger, ctx context.EvalInterface, document interface{}, resolver VariableResolver) (_ interface{}, err error) {
document, err = substituteReferences(log, document)
if err != nil {
return document, err
}
return substituteVars(log, ctx, document, resolver)
}
func SubstituteAllForceMutate(log logr.Logger, ctx context.EvalInterface, typedRule kyverno.Rule) (_ kyverno.Rule, err error) {
@ -100,8 +188,6 @@ func SubstituteAllForceMutate(log logr.Logger, ctx context.EvalInterface, typedR
return UntypedToRule(rule)
}
//SubstituteVars replaces the variables with the values defined in the context
// - if any variable is invalid or has nil value, it is considered as a failed variable substitution
func substituteVars(log logr.Logger, ctx context.EvalInterface, rule interface{}, vr VariableResolver) (interface{}, error) {
return jsonUtils.NewTraversal(rule, substituteVariablesIfAny(log, ctx, vr)).TraverseJSON()
}
@ -411,57 +497,6 @@ func getValueFromReference(fullDocument interface{}, path string) (interface{},
return element, nil
}
func SubstituteAllInRule(log logr.Logger, ctx context.EvalInterface, typedRule kyverno.Rule) (_ kyverno.Rule, err error) {
var rule interface{}
rule, err = RuleToUntyped(typedRule)
if err != nil {
return typedRule, err
}
rule, err = substituteReferences(log, rule)
if err != nil {
return typedRule, err
}
rule, err = substituteVars(log, ctx, rule, DefaultVariableResolver)
if err != nil {
return typedRule, err
}
return UntypedToRule(rule)
}
func RuleToUntyped(rule kyverno.Rule) (interface{}, error) {
jsonRule, err := json.Marshal(rule)
if err != nil {
return nil, err
}
var untyped interface{}
err = json.Unmarshal(jsonRule, &untyped)
if err != nil {
return nil, err
}
return untyped, nil
}
func UntypedToRule(untyped interface{}) (kyverno.Rule, error) {
jsonRule, err := json.Marshal(untyped)
if err != nil {
return kyverno.Rule{}, err
}
var rule kyverno.Rule
err = json.Unmarshal(jsonRule, &rule)
if err != nil {
return kyverno.Rule{}, err
}
return rule, nil
}
func replaceSubstituteVariables(document interface{}) interface{} {
rawDocument, err := json.Marshal(document)
if err != nil {

View file

@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/kyverno/kyverno/pkg/engine/response"
"reflect"
"strings"
"time"
@ -149,7 +150,7 @@ func (c *Controller) applyGenerate(resource unstructured.Unstructured, gr kyvern
var applicableRules []string
// Removing GR if rule is failed. Used when the generate condition failed but gr exist
for _, r := range engineResponse.PolicyResponse.Rules {
if !r.Success {
if r.Status != response.RuleStatusPass {
logger.V(4).Info("querying all generate requests")
selector := labels.SelectorFromSet(labels.Set(map[string]string{
"generate.kyverno.io/policy-name": engineResponse.PolicyResponse.Policy.Name,

View file

@ -86,7 +86,7 @@ var rawPolicy = []byte(`
}
`)
var rawEngRes = []byte(`{"PatchedResource":{"apiVersion":"v1","kind":"Pod","metadata":{"name":"nginx1","namespace":"default"},"spec":{"containers":[{"image":"nginx","imagePullPolicy":"IfNotPresent","name":"nginx","resources":{"limits":{"cpu":"200m","memory":"100Mi"},"requests":{"cpu":"100m","memory":"50Mi"}}}]}},"PolicyResponse":{"policy":{"name":"pod-requirements","namespace":""},"resource":{"kind":"Pod","apiVersion":"v1","namespace":"default","name":"nginx1","uid":""},"processingTime":974958,"rulesAppliedCount":2,"policyExecutionTimestamp":1630527712,"rules":[{"name":"pods-require-account","type":"Validation","message":"validation error: User pods must include an account for charging. Rule pods-require-account failed at path /metadata/labels/","success":false,"processingTime":28833,"ruleExecutionTimestamp":1630527712},{"name":"pods-require-limits","type":"Validation","message":"validation rule 'pods-require-limits' passed.","success":true,"processingTime":578625,"ruleExecutionTimestamp":1630527712}],"ValidationFailureAction":"audit"}}`)
var rawEngRes = []byte(`{"PatchedResource":{"apiVersion":"v1","kind":"Pod","metadata":{"name":"nginx1","namespace":"default"},"spec":{"containers":[{"image":"nginx","imagePullPolicy":"IfNotPresent","name":"nginx","resources":{"limits":{"cpu":"200m","memory":"100Mi"},"requests":{"cpu":"100m","memory":"50Mi"}}}]}},"PolicyResponse":{"policy":{"name":"pod-requirements","namespace":""},"resource":{"kind":"Pod","apiVersion":"v1","namespace":"default","name":"nginx1","uid":""},"processingTime":974958,"rulesAppliedCount":2,"policyExecutionTimestamp":1630527712,"rules":[{"name":"pods-require-account","type":"Validation","message":"validation error: User pods must include an account for charging. Rule pods-require-account failed at path /metadata/labels/","status":"fail","processingTime":28833,"ruleExecutionTimestamp":1630527712},{"name":"pods-require-limits","type":"Validation","message":"validation rule 'pods-require-limits' passed.","status":"pass","processingTime":578625,"ruleExecutionTimestamp":1630527712}],"ValidationFailureAction":"audit"}}`)
func Test_buildPolicyReports(t *testing.T) {
os.Setenv("POLICY-TYPE", common.PolicyReport)
@ -118,9 +118,9 @@ func Test_buildPolicyReports(t *testing.T) {
assert.Assert(t, report.GetName() == "policyreport-ns-default")
assert.Assert(t, report.GetKind() == "PolicyReport")
assert.Assert(t, len(report.UnstructuredContent()["results"].([]interface{})) == 2)
assert.Assert(t,
report.UnstructuredContent()["summary"].(map[string]interface{})[preport.StatusPass].(int64) == 1,
report.UnstructuredContent()["summary"].(map[string]interface{})[preport.StatusPass].(int64))
summary := report.UnstructuredContent()["summary"].(map[string]interface{})
assert.Assert(t, summary[preport.StatusPass].(int64) == 1, summary[preport.StatusPass].(int64))
}
}
}

View file

@ -768,7 +768,7 @@ func ProcessValidateEngineResponse(policy *v1.ClusterPolicy, validateResponse *r
Message: valResponseRule.Message,
}
if valResponseRule.Success {
if valResponseRule.Status == response.RuleStatusPass {
rc.Pass++
vrule.Check = report.StatusPass
} else {
@ -823,7 +823,7 @@ func processGenerateEngineResponse(policy *v1.ClusterPolicy, generateResponse *r
for i, genResponseRule := range generateResponse.PolicyResponse.Rules {
if policyRule.Name == genResponseRule.Name {
ruleFoundInEngineResponse = true
if genResponseRule.Success {
if genResponseRule.Status == response.RuleStatusPass {
rc.Pass++
} else {
if printCount < 1 {
@ -894,7 +894,7 @@ func processMutateEngineResponse(policy *v1.ClusterPolicy, mutateResponse *respo
for i, mutateResponseRule := range mutateResponse.PolicyResponse.Rules {
if policyRule.Name == mutateResponseRule.Name {
ruleFoundInEngineResponse = true
if mutateResponseRule.Success {
if mutateResponseRule.Status == response.RuleStatusPass {
rc.Pass++
printMutatedRes = true
} else {

View file

@ -84,7 +84,7 @@ func (pc PromConfig) ProcessEngineResponse(policy kyverno.ClusterPolicy, engineR
ruleName := rule.Name
ruleType := ParseRuleTypeFromEngineRuleResponse(rule)
ruleResult := metrics.Fail
if rule.Success {
if rule.Status == response.RuleStatusPass {
ruleResult = metrics.Pass
}

View file

@ -77,7 +77,7 @@ func (pc PromConfig) ProcessEngineResponse(policy kyverno.ClusterPolicy, engineR
ruleName := rule.Name
ruleType := ParseRuleTypeFromEngineRuleResponse(rule)
ruleResult := metrics.Fail
if rule.Success {
if rule.Status == response.RuleStatusPass {
ruleResult = metrics.Pass
}

View file

@ -128,7 +128,7 @@ func getFailedOverallRuleInfo(resource unstructured.Unstructured, engineResponse
if !jsonpatch.Equal(patchedResource, rawResource) {
log.V(4).Info("policy rule conditions not satisfied by resource", "rule", rule.Name)
engineResponse.PolicyResponse.Rules[index].Success = false
engineResponse.PolicyResponse.Rules[index].Status = response.RuleStatusFail
engineResponse.PolicyResponse.Rules[index].Message = fmt.Sprintf("mutation json patches not found at resource path %s", extractPatchPath(patches, log))
}
}

View file

@ -208,7 +208,7 @@ func generateFailEventsPerEr(log logr.Logger, er *response.EngineResponse) []eve
logger.V(4).Info("reporting fail results for policy")
for _, rule := range er.PolicyResponse.Rules {
if rule.Success {
if rule.Status != response.RuleStatusPass {
continue
}
// generate event on resource for each failed rule

View file

@ -264,7 +264,7 @@ func buildViolatedRules(er *response.EngineResponse) []kyverno.ViolatedRule {
Message: rule.Message,
}
vrule.Check = report.StatusFail
if rule.Success {
if rule.Status == response.RuleStatusPass {
vrule.Check = report.StatusPass
}
violatedRules = append(violatedRules, vrule)

View file

@ -3,9 +3,11 @@ package testrunner
import (
"bytes"
"encoding/json"
"github.com/stretchr/testify/assert"
"io/ioutil"
"os"
ospath "path"
"path/filepath"
"reflect"
"testing"
@ -14,83 +16,94 @@ import (
"github.com/kyverno/kyverno/pkg/engine"
"github.com/kyverno/kyverno/pkg/engine/context"
"github.com/kyverno/kyverno/pkg/engine/response"
"gopkg.in/yaml.v2"
"gopkg.in/yaml.v3"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
k8sRuntime "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
apiyaml "k8s.io/apimachinery/pkg/util/yaml"
"k8s.io/client-go/kubernetes/scheme"
"path"
"runtime"
)
type scenarioT struct {
testCases []scaseT
type Scenario struct {
TestCases []TestCase
}
//scase defines input and output for a case
type scaseT struct {
Input sInput `yaml:"input"`
Expected sExpected `yaml:"expected"`
//CaseT defines input and output for a case
type TestCase struct {
Input Input `yaml:"input"`
Expected Expected `yaml:"expected"`
}
//sInput defines input for a test scenario
type sInput struct {
//Input defines input for a test scenario
type Input struct {
Policy string `yaml:"policy"`
Resource string `yaml:"resource"`
LoadResources []string `yaml:"loadresources,omitempty"`
}
type sExpected struct {
Mutation sMutation `yaml:"mutation,omitempty"`
Validation sValidation `yaml:"validation,omitempty"`
Generation sGeneration `yaml:"generation,omitempty"`
type Expected struct {
Mutation Mutation `yaml:"mutation,omitempty"`
Validation Validation `yaml:"validation,omitempty"`
Generation Generation `yaml:"generation,omitempty"`
}
type sMutation struct {
type Mutation struct {
// path to the patched resource to be compared with
PatchedResource string `yaml:"patchedresource,omitempty"`
// expected response from the policy engine
PolicyResponse response.PolicyResponse `yaml:"policyresponse"`
}
type sValidation struct {
type Validation struct {
// expected response from the policy engine
PolicyResponse response.PolicyResponse `yaml:"policyresponse"`
}
type sGeneration struct {
type Generation struct {
// generated resources
GeneratedResources []kyverno.ResourceSpec `yaml:"generatedResources"`
// expected response from the policy engine
PolicyResponse response.PolicyResponse `yaml:"policyresponse"`
}
//getRelativePath expects a path relative to project and builds the complete path
func getRelativePath(path string) string {
gp := os.Getenv("GOPATH")
ap := ospath.Join(gp, projectPath)
return ospath.Join(ap, path)
// RootDir returns the kyverno project directory based on the location of the current file.
// It assumes that the project directory is 2 levels up. This means if this function is moved
// it may not work as expected.
func RootDir() string {
_, b, _, _ := runtime.Caller(0)
d := path.Join(path.Dir(b))
d = filepath.Dir(d)
return filepath.Dir(d)
}
func loadScenario(t *testing.T, path string) (*scenarioT, error) {
fileBytes, err := loadFile(t, path)
if err != nil {
return nil, err
}
//getRelativePath expects a path relative to project and builds the complete path
func getRelativePath(path string) string {
root := RootDir()
return ospath.Join(root, path)
}
var testCases []scaseT
func loadScenario(t *testing.T, path string) (*Scenario, error) {
fileBytes, err := loadFile(t, path)
assert.Nil(t, err)
var testCases []TestCase
// load test cases separated by '---'
// each test case defines an input & expected result
scenariosBytes := bytes.Split(fileBytes, []byte("---"))
for _, scenarioBytes := range scenariosBytes {
tc := scaseT{}
if err := yaml.Unmarshal([]byte(scenarioBytes), &tc); err != nil {
for _, testCaseBytes := range scenariosBytes {
var tc TestCase
if err := yaml.Unmarshal(testCaseBytes, &tc); err != nil {
t.Errorf("failed to decode test case YAML: %v", err)
continue
}
testCases = append(testCases, tc)
}
scenario := scenarioT{
testCases: testCases,
scenario := Scenario{
TestCases: testCases,
}
return &scenario, nil
@ -106,14 +119,14 @@ func loadFile(t *testing.T, path string) ([]byte, error) {
return ioutil.ReadFile(path)
}
func runScenario(t *testing.T, s *scenarioT) bool {
for _, tc := range s.testCases {
func runScenario(t *testing.T, s *Scenario) bool {
for _, tc := range s.TestCases {
runTestCase(t, tc)
}
return true
}
func runTestCase(t *testing.T, tc scaseT) bool {
func runTestCase(t *testing.T, tc TestCase) bool {
policy := loadPolicy(t, tc.Input.Policy)
if policy == nil {
t.Error("Policy not loaded")
@ -310,8 +323,8 @@ func compareRules(t *testing.T, rule response.RuleResponse, expectedRule respons
// }
// success
if rule.Success != expectedRule.Success {
t.Errorf("rule success: expected %t, received %t", expectedRule.Success, rule.Success)
if rule.Status != expectedRule.Status {
t.Errorf("rule status mismatch: expected %s, received %s", expectedRule.Status.String(), rule.Status.String())
}
}
@ -330,7 +343,7 @@ func loadPolicyResource(t *testing.T, file string) *unstructured.Unstructured {
}
func getClient(t *testing.T, files []string) *client.Client {
var objects []runtime.Object
var objects []k8sRuntime.Object
if files != nil {
for _, file := range files {
@ -338,7 +351,7 @@ func getClient(t *testing.T, files []string) *client.Client {
}
}
// create mock client
scheme := runtime.NewScheme()
scheme := k8sRuntime.NewScheme()
// mock client expects the resource to be as runtime.Object
c, err := client.NewMockClient(scheme, nil, objects...)
if err != nil {
@ -352,7 +365,7 @@ func getClient(t *testing.T, files []string) *client.Client {
return c
}
func getGVRForResources(objects []runtime.Object) []schema.GroupVersionResource {
func getGVRForResources(objects []k8sRuntime.Object) []schema.GroupVersionResource {
var gvrs []schema.GroupVersionResource
for _, obj := range objects {
gvk := obj.GetObjectKind().GroupVersionKind()
@ -380,7 +393,7 @@ func loadResource(t *testing.T, path string) []*unstructured.Unstructured {
continue
}
data, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&obj)
data, err := k8sRuntime.DefaultUnstructuredConverter.ToUnstructured(&obj)
if err != nil {
t.Logf("failed to unmarshall resource. %v", err)
continue
@ -392,8 +405,8 @@ func loadResource(t *testing.T, path string) []*unstructured.Unstructured {
return unstrResources
}
func loadObjects(t *testing.T, path string) []runtime.Object {
var resources []runtime.Object
func loadObjects(t *testing.T, path string) []k8sRuntime.Object {
var resources []k8sRuntime.Object
t.Logf("loading objects from %s", path)
data, err := loadFile(t, path)
if err != nil {

View file

@ -0,0 +1,69 @@
package testrunner
import (
"github.com/kyverno/kyverno/pkg/engine/response"
"gopkg.in/yaml.v3"
"gotest.tools/assert"
"io/ioutil"
"testing"
)
var sourceYAML = `
input:
policy: test/best_practices/disallow_bind_mounts.yaml
resource: test/resources/disallow_host_filesystem.yaml
expected:
validation:
policyresponse:
policy:
namespace: ''
name: disallow-bind-mounts
resource:
kind: Pod
apiVersion: v1
namespace: ''
name: image-with-hostpath
rules:
- name: validate-hostPath
type: Validation
status: fail
`
func Test_parse_yaml(t *testing.T) {
var s TestCase
if err := yaml.Unmarshal([]byte(sourceYAML), &s); err != nil {
t.Errorf("failed to parse YAML: %v", err)
return
}
assert.Equal(t, s.Expected.Validation.PolicyResponse.Policy.Name, "disallow-bind-mounts")
assert.Equal(t, 1, len(s.Expected.Validation.PolicyResponse.Rules), "invalid rule count")
assert.Equal(t, response.RuleStatusFail, s.Expected.Validation.PolicyResponse.Rules[0].Status, "invalid status")
}
func Test_parse_file(t *testing.T) {
s, err := loadScenario(t, "test/scenarios/samples/best_practices/disallow_bind_mounts_fail.yaml")
assert.NilError(t, err)
assert.Equal(t, 1, len(s.TestCases))
assert.Equal(t, s.TestCases[0].Expected.Validation.PolicyResponse.Policy.Name, "disallow-bind-mounts")
assert.Equal(t, 1, len(s.TestCases[0].Expected.Validation.PolicyResponse.Rules), "invalid rule count")
assert.Equal(t, response.RuleStatusFail, s.TestCases[0].Expected.Validation.PolicyResponse.Rules[0].Status, "invalid status")
}
func Test_parse_file2(t *testing.T) {
path := getRelativePath("test/scenarios/samples/best_practices/disallow_bind_mounts_fail.yaml")
data, err := ioutil.ReadFile(path)
assert.NilError(t, err)
strData := string(data)
var s TestCase
if err := yaml.Unmarshal([]byte(strData), &s); err != nil {
t.Errorf("failed to parse YAML: %v", err)
return
}
assert.Equal(t, s.Expected.Validation.PolicyResponse.Policy.Name, "disallow-bind-mounts")
assert.Equal(t, 1, len(s.Expected.Validation.PolicyResponse.Rules), "invalid rule count")
assert.Equal(t, response.RuleStatusFail, s.Expected.Validation.PolicyResponse.Rules[0].Status, "invalid status")
}

View file

@ -8,10 +8,6 @@ import (
"sigs.k8s.io/controller-runtime/pkg/log"
)
var (
projectPath = envOr("PROJECT_PATH", "src/github.com/kyverno/kyverno")
)
// LoadFile loads file in byte buffer
func LoadFile(path string) ([]byte, error) {
if _, err := os.Stat(path); os.IsNotExist(err) {

View file

@ -9,7 +9,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/log"
)
func newPolicyResponse(policy, rule string, patchesStr []string, success bool) response.PolicyResponse {
func newPolicyResponse(policy, rule string, patchesStr []string, status response.RuleStatus) response.PolicyResponse {
var patches [][]byte
for _, p := range patchesStr {
patches = append(patches, []byte(p))
@ -21,12 +21,13 @@ func newPolicyResponse(policy, rule string, patchesStr []string, success bool) r
{
Name: rule,
Patches: patches,
Success: success},
Status: status,
},
},
}
}
func newEngineResponse(policy, rule string, patchesStr []string, success bool, annotation map[string]interface{}) *response.EngineResponse {
func newEngineResponse(policy, rule string, patchesStr []string, status response.RuleStatus, annotation map[string]interface{}) *response.EngineResponse {
return &response.EngineResponse{
PatchedResource: unstructured.Unstructured{
Object: map[string]interface{}{
@ -35,13 +36,13 @@ func newEngineResponse(policy, rule string, patchesStr []string, success bool, a
},
},
},
PolicyResponse: newPolicyResponse(policy, rule, patchesStr, success),
PolicyResponse: newPolicyResponse(policy, rule, patchesStr, status),
}
}
func Test_empty_annotation(t *testing.T) {
patchStr := `{ "op": "replace", "path": "/spec/containers/0/imagePullPolicy", "value": "IfNotPresent" }`
engineResponse := newEngineResponse("mutate-container", "default-imagepullpolicy", []string{patchStr}, true, nil)
engineResponse := newEngineResponse("mutate-container", "default-imagepullpolicy", []string{patchStr}, response.RuleStatusPass, nil)
annPatches := generateAnnotationPatches([]*response.EngineResponse{engineResponse}, log.Log)
expectedPatches := `{"op":"add","path":"/metadata/annotations","value":{"policies.kyverno.io/last-applied-patches":"default-imagepullpolicy.mutate-container.kyverno.io: replaced /spec/containers/0/imagePullPolicy\n"}}`
@ -54,7 +55,7 @@ func Test_exist_annotation(t *testing.T) {
}
patchStr := `{ "op": "replace", "path": "/spec/containers/0/imagePullPolicy", "value": "IfNotPresent" }`
engineResponse := newEngineResponse("mutate-container", "default-imagepullpolicy", []string{patchStr}, true, annotation)
engineResponse := newEngineResponse("mutate-container", "default-imagepullpolicy", []string{patchStr}, response.RuleStatusPass, annotation)
annPatches := generateAnnotationPatches([]*response.EngineResponse{engineResponse}, log.Log)
expectedPatches := `{"op":"add","path":"/metadata/annotations/policies.kyverno.io~1last-applied-patches","value":"default-imagepullpolicy.mutate-container.kyverno.io: replaced /spec/containers/0/imagePullPolicy\n"}`
@ -67,7 +68,7 @@ func Test_exist_kyverno_annotation(t *testing.T) {
}
patchStr := `{ "op": "replace", "path": "/spec/containers/0/imagePullPolicy", "value": "IfNotPresent" }`
engineResponse := newEngineResponse("mutate-container", "default-imagepullpolicy", []string{patchStr}, true, annotation)
engineResponse := newEngineResponse("mutate-container", "default-imagepullpolicy", []string{patchStr}, response.RuleStatusPass, annotation)
annPatches := generateAnnotationPatches([]*response.EngineResponse{engineResponse}, log.Log)
expectedPatches := `{"op":"add","path":"/metadata/annotations/policies.kyverno.io~1last-applied-patches","value":"default-imagepullpolicy.mutate-container.kyverno.io: replaced /spec/containers/0/imagePullPolicy\n"}`
@ -78,10 +79,10 @@ func Test_annotation_nil_patch(t *testing.T) {
annotation := map[string]interface{}{
"policies.kyverno.patches": "old-annotation",
}
engineResponse := newEngineResponse("mutate-container", "default-imagepullpolicy", nil, true, annotation)
engineResponse := newEngineResponse("mutate-container", "default-imagepullpolicy", nil, response.RuleStatusPass, annotation)
annPatches := generateAnnotationPatches([]*response.EngineResponse{engineResponse}, log.Log)
assert.Assert(t, annPatches == nil)
engineResponseNew := newEngineResponse("mutate-container", "default-imagepullpolicy", []string{""}, true, annotation)
engineResponseNew := newEngineResponse("mutate-container", "default-imagepullpolicy", []string{""}, response.RuleStatusPass, annotation)
annPatchesNew := generateAnnotationPatches([]*response.EngineResponse{engineResponseNew}, log.Log)
assert.Assert(t, annPatchesNew == nil)
}
@ -91,7 +92,7 @@ func Test_annotation_failed_Patch(t *testing.T) {
"policies.kyverno.patches": "old-annotation",
}
engineResponse := newEngineResponse("mutate-container", "default-imagepullpolicy", nil, false, annotation)
engineResponse := newEngineResponse("mutate-container", "default-imagepullpolicy", nil, response.RuleStatusFail, annotation)
annPatches := generateAnnotationPatches([]*response.EngineResponse{engineResponse}, log.Log)
assert.Assert(t, annPatches == nil)
@ -102,7 +103,7 @@ func Test_exist_patches(t *testing.T) {
"policies.kyverno.io/patches": "present",
}
patchStr := `{ "op": "replace", "path": "/spec/containers/0/imagePullPolicy", "value": "IfNotPresent" }`
engineResponse := newEngineResponse("mutate-container", "default-imagepullpolicy", []string{patchStr}, true, annotation)
engineResponse := newEngineResponse("mutate-container", "default-imagepullpolicy", []string{patchStr}, response.RuleStatusPass, annotation)
annPatches := generateAnnotationPatches([]*response.EngineResponse{engineResponse}, log.Log)
expectedPatches1 := `{"op":"remove","path":"/metadata/annotations/policies.kyverno.io~1patches","value":null}`
expectedPatches2 := `{"op":"add","path":"/metadata/annotations/policies.kyverno.io~1last-applied-patches","value":"default-imagepullpolicy.mutate-container.kyverno.io: replaced /spec/containers/0/imagePullPolicy\n"}`

View file

@ -47,7 +47,7 @@ func getEnforceFailureErrorMsg(engineResponses []*response.EngineResponse) strin
if !er.IsSuccessful() && er.PolicyResponse.ValidationFailureAction == common.Enforce {
ruleToReason := make(map[string]string)
for _, rule := range er.PolicyResponse.Rules {
if !rule.Success {
if rule.Status != response.RuleStatusPass {
ruleToReason[rule.Name] = rule.Message
}
}
@ -72,7 +72,7 @@ func getErrorMsg(engineReponses []*response.EngineResponse) string {
resourceInfo = fmt.Sprintf("%s/%s/%s", er.PolicyResponse.Resource.Kind, er.PolicyResponse.Resource.Namespace, er.PolicyResponse.Resource.Name)
str = append(str, fmt.Sprintf("failed policy %s:", er.PolicyResponse.Policy.Name))
for _, rule := range er.PolicyResponse.Rules {
if !rule.Success {
if rule.Status != response.RuleStatusPass {
str = append(str, rule.ToString())
}
}

View file

@ -86,7 +86,7 @@ func (ws *WebhookServer) handleGenerate(
}
engineResponse := engine.Generate(policyContext)
for _, rule := range engineResponse.PolicyResponse.Rules {
if !rule.Success {
if rule.Status != response.RuleStatusPass {
ws.deleteGR(logger, engineResponse)
continue
}

View file

@ -216,7 +216,7 @@ func Test_Mutate(t *testing.T) {
Expect(err).NotTo(HaveOccurred())
By("Validating created resource with the expected pattern...")
_, err = validate.ValidateResourceWithPattern(log.Log, actual, expected)
err, _ = validate.MatchPattern(log.Log, actual, expected)
Expect(err).NotTo(HaveOccurred())
By("Deleting Cluster Policies...")

View file

@ -17,5 +17,5 @@ expected:
rules:
- name: pEP
type: Mutation
success: true
status: pass
message: successfully process JSON patches

View file

@ -16,5 +16,5 @@ expected:
rules:
- name: disable-servicelink-and-token
type: Mutation
success: true
status: pass
message: successfully processed strategic merge patch

View file

@ -17,7 +17,7 @@ expected:
rules:
- name: add-memory-limit
type: Mutation
success: true
status: pass
message: successfully processed strategic merge patch
validation:
policyresponse:
@ -33,4 +33,4 @@ expected:
- name: check-cpu-memory-limits
type: Validation
message: validation rule 'check-cpu-memory-limits' passed.
success: true
status: pass

View file

@ -18,4 +18,4 @@ expected:
- name: validate-default-proc-mount
type: Validation
message: "validation rule 'validate-default-proc-mount' passed."
success: true
status: pass

View file

@ -17,4 +17,4 @@ expected:
- name: prevent-mounting-default-serviceaccount
type: Validation
message: "validation error: Prevent mounting of default service account. Rule prevent-mounting-default-serviceaccount failed at path /spec/serviceAccountName/"
success: false
status: fail

View file

@ -17,8 +17,8 @@ expected:
- name: check-readinessProbe-exists
type: Validation
message: validation rule 'check-readinessProbe-exists' passed.
success: true
status: pass
- name: check-livenessProbe-exists
type: Validation
message: validation rule 'check-livenessProbe-exists' passed.
success: true
status: pass

View file

@ -17,4 +17,4 @@ expected:
- name: validate-selinux-options
type: Validation
message: "validation error: SELinux level is required. Rule validate-selinux-options failed at path /spec/containers/0/securityContext/seLinuxOptions/"
success: false
status: fail

View file

@ -18,4 +18,4 @@ expected:
- name: validate-volumes-whitelist
type: Validation
message: "validation rule 'validate-volumes-whitelist' anyPattern[2] passed."
success: true
status: pass

View file

@ -20,5 +20,5 @@ expected:
rules:
- name: default-deny-ingress
type: Generation
success: true
status: pass
message: created resource NetworkPolicy/devtest/default-deny-ingress

View file

@ -20,7 +20,7 @@ expected:
rules:
- name: generate-resourcequota
type: Generation
success: true
status: pass
- name: generate-limitrange
type: Generation
success: true
status: pass

View file

@ -17,5 +17,5 @@ expected:
rules:
- name: annotate-empty-dir
type: Mutation
success: true
status: pass
message: "successfully processed strategic merge patch"

View file

@ -17,5 +17,5 @@ expected:
rules:
- name: annotate-host-path
type: Mutation
success: true
status: pass
message: "successfully processed strategic merge patch"

View file

@ -15,5 +15,6 @@ expected:
name: image-with-hostpath
rules:
- name: validate-hostPath
message: "validation error: Host path volumes are not allowed. Rule validate-hostPath failed at path /spec/volumes/0/hostPath/"
type: Validation
success: false
status: fail

View file

@ -16,4 +16,4 @@ expected:
rules:
- name: validate-hostPath
type: Validation
success: true
status: pass

View file

@ -16,7 +16,7 @@ expected:
rules:
- name: validate-host-network
type: Validation
success: true
status: pass
- name: validate-host-port
type: Validation
success: false
status: fail

View file

@ -16,4 +16,4 @@ expected:
rules:
- name: validate-hostPID-hostIPC
type: Validation
success: false
status: fail

View file

@ -16,7 +16,7 @@ expected:
rules:
- name: validate-privileged
type: Validation
success: false
status: fail
- name: validate-allowPrivilegeEscalation
type: Validation
success: false
status: fail

View file

@ -17,4 +17,4 @@ expected:
rules:
- name: validate-sysctls
type: Validation
success: false
status: fail

View file

@ -16,4 +16,4 @@ expected:
rules:
- name: validate-automountServiceAccountToken
type: Validation
success: true
status: pass

View file

@ -16,4 +16,4 @@ expected:
rules:
- name: validate-ingress
type: Validation
success: true
status: pass

View file

@ -16,4 +16,4 @@ expected:
rules:
- name: validate-ingress
type: Validation
success: false
status: fail