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:
commit
2bd5bca721
69 changed files with 1506 additions and 490 deletions
12
Makefile
12
Makefile
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
//go:build !ignore_autogenerated
|
||||
// +build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
//go:build !ignore_autogenerated
|
||||
// +build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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])
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
30
pkg/engine/response/response_test.go
Normal file
30
pkg/engine/response/response_test.go
Normal 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)
|
||||
}
|
97
pkg/engine/response/status.go
Normal file
97
pkg/engine/response/status.go
Normal 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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
69
pkg/testrunner/scenario_test.go
Normal file
69
pkg/testrunner/scenario_test.go
Normal 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")
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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"}`
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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...")
|
||||
|
|
|
@ -17,5 +17,5 @@ expected:
|
|||
rules:
|
||||
- name: pEP
|
||||
type: Mutation
|
||||
success: true
|
||||
status: pass
|
||||
message: successfully process JSON patches
|
||||
|
|
|
@ -16,5 +16,5 @@ expected:
|
|||
rules:
|
||||
- name: disable-servicelink-and-token
|
||||
type: Mutation
|
||||
success: true
|
||||
status: pass
|
||||
message: successfully processed strategic merge patch
|
|
@ -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
|
|
@ -18,4 +18,4 @@ expected:
|
|||
- name: validate-default-proc-mount
|
||||
type: Validation
|
||||
message: "validation rule 'validate-default-proc-mount' passed."
|
||||
success: true
|
||||
status: pass
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -18,4 +18,4 @@ expected:
|
|||
- name: validate-volumes-whitelist
|
||||
type: Validation
|
||||
message: "validation rule 'validate-volumes-whitelist' anyPattern[2] passed."
|
||||
success: true
|
||||
status: pass
|
|
@ -20,5 +20,5 @@ expected:
|
|||
rules:
|
||||
- name: default-deny-ingress
|
||||
type: Generation
|
||||
success: true
|
||||
status: pass
|
||||
message: created resource NetworkPolicy/devtest/default-deny-ingress
|
||||
|
|
|
@ -20,7 +20,7 @@ expected:
|
|||
rules:
|
||||
- name: generate-resourcequota
|
||||
type: Generation
|
||||
success: true
|
||||
status: pass
|
||||
- name: generate-limitrange
|
||||
type: Generation
|
||||
success: true
|
||||
status: pass
|
||||
|
|
|
@ -17,5 +17,5 @@ expected:
|
|||
rules:
|
||||
- name: annotate-empty-dir
|
||||
type: Mutation
|
||||
success: true
|
||||
status: pass
|
||||
message: "successfully processed strategic merge patch"
|
|
@ -17,5 +17,5 @@ expected:
|
|||
rules:
|
||||
- name: annotate-host-path
|
||||
type: Mutation
|
||||
success: true
|
||||
status: pass
|
||||
message: "successfully processed strategic merge patch"
|
|
@ -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
|
||||
|
|
|
@ -16,4 +16,4 @@ expected:
|
|||
rules:
|
||||
- name: validate-hostPath
|
||||
type: Validation
|
||||
success: true
|
||||
status: pass
|
|
@ -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
|
|
@ -16,4 +16,4 @@ expected:
|
|||
rules:
|
||||
- name: validate-hostPID-hostIPC
|
||||
type: Validation
|
||||
success: false
|
||||
status: fail
|
|
@ -16,7 +16,7 @@ expected:
|
|||
rules:
|
||||
- name: validate-privileged
|
||||
type: Validation
|
||||
success: false
|
||||
status: fail
|
||||
- name: validate-allowPrivilegeEscalation
|
||||
type: Validation
|
||||
success: false
|
||||
status: fail
|
|
@ -17,4 +17,4 @@ expected:
|
|||
rules:
|
||||
- name: validate-sysctls
|
||||
type: Validation
|
||||
success: false
|
||||
status: fail
|
|
@ -16,4 +16,4 @@ expected:
|
|||
rules:
|
||||
- name: validate-automountServiceAccountToken
|
||||
type: Validation
|
||||
success: true
|
||||
status: pass
|
|
@ -16,4 +16,4 @@ expected:
|
|||
rules:
|
||||
- name: validate-ingress
|
||||
type: Validation
|
||||
success: true
|
||||
status: pass
|
|
@ -16,4 +16,4 @@ expected:
|
|||
rules:
|
||||
- name: validate-ingress
|
||||
type: Validation
|
||||
success: false
|
||||
status: fail
|
Loading…
Reference in a new issue