1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2025-03-30 19:35:06 +00:00

add validation; add 'element' to context

Signed-off-by: Jim Bugwadia <jim@nirmata.com>
This commit is contained in:
Jim Bugwadia 2021-10-02 16:53:02 -07:00
parent 1ebd2c99f2
commit e0e6074afc
9 changed files with 161 additions and 39 deletions

View file

@ -99,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 {

View file

@ -20,6 +20,9 @@ type PolicyContext struct {
// OldResource is the prior resource for an update, or nil
OldResource unstructured.Unstructured
// Element is set when the context is used for processing a foreach loop
Element unstructured.Unstructured
// AdmissionInfo contains the admission request information
AdmissionInfo kyverno.RequestInfo

View file

@ -37,7 +37,7 @@ func MatchPattern(logger logr.Logger, resource, pattern interface{}) error {
// 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("skipping resource as anchor does not apply", "msg", ac.AnchorError.Error())
return &PatternError{nil, "", true}
return &PatternError{err, "", true}
}
// check if an anchor defined in the policy rule is missing in the resource
@ -49,7 +49,7 @@ func MatchPattern(logger logr.Logger, resource, pattern interface{}) error {
return &PatternError{err, elemPath, false}
}
return &PatternError{nil, "", false}
return nil
}
// validateResourceElement detects the element type (map, array, nil, string, int, bool, float)

View file

@ -278,13 +278,17 @@ func addElementToContext(ctx *PolicyContext, e interface{}) error {
return err
}
jsonData := map[string]interface{}{
"element": data,
}
if err := ctx.JSONContext.AddJSONObject(jsonData); err != nil {
return errors.Wrapf(err, "failed to add element (%v) to JSON context", e)
}
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)
}
ctx.Element = u
return nil
}
@ -375,12 +379,17 @@ func (v *validator) getDenyMessage(deny bool) string {
}
func (v *validator) validateResourceWithRule() *response.RuleResponse {
if reflect.DeepEqual(v.ctx.OldResource, unstructured.Unstructured{}) {
if !isEmptyUnstructured(&v.ctx.Element) {
resp := v.validatePatterns(v.ctx.Element)
return resp
}
if !isEmptyUnstructured(&v.ctx.OldResource) {
resp := v.validatePatterns(v.ctx.NewResource)
return resp
}
if reflect.DeepEqual(v.ctx.NewResource, unstructured.Unstructured{}) {
if isEmptyUnstructured(&v.ctx.NewResource) {
v.log.V(3).Info("skipping validation on deleted resource")
return nil
}
@ -395,6 +404,18 @@ func (v *validator) validateResourceWithRule() *response.RuleResponse {
return newResp
}
func isEmptyUnstructured(u *unstructured.Unstructured) bool {
if u == nil {
return true
}
if reflect.DeepEqual(*u, unstructured.Unstructured{}) {
return true
}
return false
}
// matches checks if either the new or old resource satisfies the filter conditions defined in the rule
func matches(logger logr.Logger, rule kyverno.Rule, ctx *PolicyContext) bool {
err := MatchesResourceDescription(ctx.NewResource, rule, ctx.AdmissionInfo, ctx.ExcludeGroupRole, ctx.NamespaceLabels)
@ -525,9 +546,9 @@ func (v *validator) buildErrorMessage(err error, path string) string {
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)
msgRaw, sErr := variables.SubstituteAll(v.log, v.ctx.JSONContext, v.rule.Validation.Message)
if sErr != nil {
v.log.Info("failed to substitute variables in message: %v", sErr)
}
msg := msgRaw.(string)

View file

@ -2514,7 +2514,7 @@ func Test_foreach_container_deny_fail(t *testing.T) {
"list": "request.object.spec.template.spec.containers",
"deny": {
"conditions": [
{"key": "{{ regex_match('{{request.object.image}}', 'docker.io') }}", "operator": "Equals", "value": false}
{"key": "{{ regex_match('{{element.image}}', 'docker.io') }}", "operator": "Equals", "value": false}
]
}
}
@ -2550,7 +2550,7 @@ func Test_foreach_container_deny_success(t *testing.T) {
"list": "request.object.spec.template.spec.containers",
"deny": {
"conditions": [
{"key": "{{ regex_match('{{request.object.image}}', 'docker.io') }}", "operator": "Equals", "value": false}
{"key": "{{ regex_match('{{element.image}}', 'docker.io') }}", "operator": "Equals", "value": false}
]
}
}
@ -2623,14 +2623,14 @@ func Test_foreach_context_preconditions(t *testing.T) {
"context": [{"name": "img", "configMap": {"name": "mycmap", "namespace": "default"}}],
"preconditions": { "all": [
{
"key": "{{request.object.name}}",
"key": "{{element.name}}",
"operator": "In",
"value": ["podvalid"]
}
]},
"deny": {
"conditions": [
{"key": "{{ request.object.image }}", "operator": "NotEquals", "value": "{{ img.data.{{ request.object.name }} }}"}
{"key": "{{ element.image }}", "operator": "NotEquals", "value": "{{ img.data.{{ element.name }} }}"}
]
}
}
@ -2687,14 +2687,14 @@ func Test_foreach_context_preconditions_fail(t *testing.T) {
"context": [{"name": "img", "configMap": {"name": "mycmap", "namespace": "default"}}],
"preconditions": { "all": [
{
"key": "{{request.object.name}}",
"key": "{{element.name}}",
"operator": "In",
"value": ["podvalid", "podinvalid"]
}
]},
"deny": {
"conditions": [
{"key": "{{ request.object.image }}", "operator": "NotEquals", "value": "{{ img.data.{{ request.object.name }} }}"}
{"key": "{{ element.image }}", "operator": "NotEquals", "value": "{{ img.data.{{ element.name }} }}"}
]
}
}

View file

@ -270,6 +270,8 @@ func substituteVariablesIfAny(log logr.Logger, ctx context.EvalInterface, vr Var
return data.Element, nil
}
isDeleteRequest := isDeleteRequest(ctx)
vars := RegexVariables.FindAllString(value, -1)
for len(vars) > 0 {
originalPattern := value
@ -281,8 +283,7 @@ func substituteVariablesIfAny(log logr.Logger, ctx context.EvalInterface, vr Var
variable = strings.Replace(variable, "@", fmt.Sprintf("request.object.%s", getJMESPath(data.Path)), -1)
}
operation, err := ctx.Query("request.operation")
if err == nil && operation == "DELETE" {
if isDeleteRequest {
variable = strings.ReplaceAll(variable, "request.object", "request.oldObject")
}
@ -318,6 +319,15 @@ func substituteVariablesIfAny(log logr.Logger, ctx context.EvalInterface, vr Var
})
}
func isDeleteRequest(ctx context.EvalInterface) bool {
operation, err := ctx.Query("request.operation")
if err == nil && operation == "DELETE" {
return true
}
return false
}
// getJMESPath converts path to JMES format
func getJMESPath(rawPath string) string {
tokens := strings.Split(rawPath, "/")[3:] // skip empty element and two non-resource (like mutate.overlay)

View file

@ -21,7 +21,11 @@ type Validation interface {
// - Mutate
// - Validation
// - Generate
func validateActions(idx int, rule kyverno.Rule, client *dclient.Client, mock bool) error {
func validateActions(idx int, rule *kyverno.Rule, client *dclient.Client, mock bool) error {
if rule == nil {
return nil
}
var checker Validation
// Mutate
@ -34,7 +38,7 @@ func validateActions(idx int, rule kyverno.Rule, client *dclient.Client, mock bo
// Validate
if rule.HasValidate() {
checker = validate.NewValidateFactory(rule.Validation)
checker = validate.NewValidateFactory(&rule.Validation)
if path, err := checker.Validate(); err != nil {
return fmt.Errorf("path: spec.rules[%d].validate.%s.: %v", idx, path, err)
}

View file

@ -148,7 +148,7 @@ func Validate(policy *kyverno.ClusterPolicy, client *dclient.Client, mock bool,
// - Mutate
// - Validate
// - Generate
if err := validateActions(i, rule, client, mock); err != nil {
if err := validateActions(i, &rule, client, mock); err != nil {
return err
}

View file

@ -2,42 +2,43 @@ package validate
import (
"fmt"
"strings"
kyverno "github.com/kyverno/kyverno/pkg/api/kyverno/v1"
commonAnchors "github.com/kyverno/kyverno/pkg/engine/anchor/common"
"github.com/kyverno/kyverno/pkg/policy/common"
)
// Validate provides implementation to validate 'validate' rule
// Validate validates a 'validate' rule
type Validate struct {
// rule to hold 'validate' rule specifications
rule kyverno.Validation
rule *kyverno.Validation
}
//NewValidateFactory returns a new instance of Mutate validation checker
func NewValidateFactory(rule kyverno.Validation) *Validate {
func NewValidateFactory(rule *kyverno.Validation) *Validate {
m := Validate{
rule: rule,
}
return &m
}
//Validate validates the 'validate' rule
func (v *Validate) Validate() (string, error) {
rule := v.rule
if err := v.validateOverlayPattern(); err != nil {
if err := v.validateElements(); err != nil {
// no need to proceed ahead
return "", err
}
if rule.Pattern != nil {
if path, err := common.ValidatePattern(rule.Pattern, "/", []commonAnchors.IsAnchor{commonAnchors.IsConditionAnchor, commonAnchors.IsExistenceAnchor, commonAnchors.IsEqualityAnchor, commonAnchors.IsNegationAnchor, commonAnchors.IsGlobalAnchor}); err != nil {
if v.rule.Pattern != nil {
if path, err := common.ValidatePattern(v.rule.Pattern, "/", []commonAnchors.IsAnchor{commonAnchors.IsConditionAnchor, commonAnchors.IsExistenceAnchor, commonAnchors.IsEqualityAnchor, commonAnchors.IsNegationAnchor, commonAnchors.IsGlobalAnchor}); err != nil {
return fmt.Sprintf("pattern.%s", path), err
}
}
if rule.AnyPattern != nil {
anyPattern, err := rule.DeserializeAnyPattern()
if v.rule.AnyPattern != nil {
anyPattern, err := v.rule.DeserializeAnyPattern()
if err != nil {
return "anyPattern", fmt.Errorf("failed to deserialize anyPattern, expect array: %v", err)
}
@ -47,19 +48,92 @@ func (v *Validate) Validate() (string, error) {
}
}
}
if v.rule.ForEachValidation != nil {
if err := v.validateForEach(v.rule.ForEachValidation); err != nil {
return "", err
}
}
return "", nil
}
// validateOverlayPattern checks one of pattern/anyPattern must exist
func (v *Validate) validateOverlayPattern() error {
rule := v.rule
if rule.Pattern == nil && rule.AnyPattern == nil && rule.Deny == nil {
return fmt.Errorf("pattern, anyPattern or deny must be specified")
func (v *Validate) validateElements() error {
count := validationElemCount(v.rule)
if count == 0 {
return fmt.Errorf("one of pattern, anyPattern, deny, foreach must be specified")
}
if rule.Pattern != nil && rule.AnyPattern != nil {
return fmt.Errorf("only one operation allowed per validation rule(pattern or anyPattern)")
if count > 1 {
return fmt.Errorf("only one of pattern, anyPattern, deny, foreach can be specified")
}
return nil
}
func validationElemCount(v *kyverno.Validation) int {
if v == nil {
return 0
}
count := 0
if v.Pattern != nil {
count++
}
if v.AnyPattern != nil {
count++
}
if v.Deny != nil {
count++
}
if v.ForEachValidation != nil {
count++
}
return count
}
func (v *Validate) validateForEach(foreach *kyverno.ForEachValidation) error {
if foreach.List == "" {
return fmt.Errorf("foreach.list is required")
}
if !strings.HasPrefix(foreach.List, "request.object") {
return fmt.Errorf("foreach.list must start with 'request.object' e.g. 'request.object.spec.containers'.")
}
count := foreachElemCount(foreach)
if count == 0 {
return fmt.Errorf("one of pattern, anyPattern, deny must be specified")
}
if count > 1 {
return fmt.Errorf("only one of pattern, anyPattern, deny can be specified")
}
return nil
}
func foreachElemCount(foreach *kyverno.ForEachValidation) int {
if foreach == nil {
return 0
}
count := 0
if foreach.Pattern != nil {
count++
}
if foreach.AnyPattern != nil {
count++
}
if foreach.Deny != nil {
count++
}
return count
}