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:
parent
1ebd2c99f2
commit
e0e6074afc
9 changed files with 161 additions and 39 deletions
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 }} }}"}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue