package patch import ( "encoding/json" "fmt" "github.com/go-logr/logr" "github.com/kyverno/kyverno/pkg/engine/anchor" "github.com/kyverno/kyverno/pkg/engine/validate" "sigs.k8s.io/kustomize/kyaml/yaml" ) type ConditionError struct { errorChain error } func (ce ConditionError) Error() string { return fmt.Sprintf("condition failed: %s", ce.errorChain.Error()) } func NewConditionError(err error) error { return ConditionError{err} } type GlobalConditionError struct { errorChain error } func (ce GlobalConditionError) Error() string { return fmt.Sprintf("global condition failed: %s", ce.errorChain.Error()) } func NewGlobalConditionError(err error) error { return GlobalConditionError{err} } // preProcessPattern - Dynamically preProcess the yaml // 1> For conditional anchor remove anchors from the pattern. // 2> For Adding anchors remove anchor tags. // The whole yaml is structured as a pointer tree. // https://godoc.org/gopkg.in/yaml.v3#Node // A single Node contains Tag to identify it as MappingNode (map[string]interface{}), Sequence ([]interface{}), ScalarNode (string, int, float bool etc.) // A parent node having MappingNode keeps the data as , inside it's Content field and Tag field as "!!map". // A parent node having Sequence keeps the data as array of Node inside Content field and a Tag field as "!!seq". // https://github.com/kubernetes-sigs/kustomize/blob/master/kyaml/yaml/rnode.go func PreProcessPattern(logger logr.Logger, pattern, resource *yaml.RNode) error { err := preProcessRecursive(logger, pattern, resource) if err != nil { return err } return deleteConditionElements(pattern) } func preProcessRecursive(logger logr.Logger, pattern, resource *yaml.RNode) error { switch pattern.YNode().Kind { case yaml.MappingNode: return walkMap(logger, pattern, resource) case yaml.SequenceNode: return walkList(logger, pattern, resource) } return nil } func walkMap(logger logr.Logger, pattern, resource *yaml.RNode) error { if _, err := handleAddIfNotPresentAnchor(pattern, resource); err != nil { return fmt.Errorf("failed to process addIfNotPresent anchor: %w", err) } if err := validateConditions(logger, pattern, resource); err != nil { return err // do not wrap condition errors } isNotAnchor := func(a anchor.Anchor) bool { return !hasAnchor(a) } nonAnchors, err := nonAnchorKeys(pattern, isNotAnchor) if err != nil { return err } for _, field := range nonAnchors { var resourceValue *yaml.RNode if resource != nil && resource.Field(field) != nil { resourceValue = resource.Field(field).Value } err := preProcessRecursive(logger, pattern.Field(field).Value, resourceValue) if err != nil { return err } } return nil } func walkList(logger logr.Logger, pattern, resource *yaml.RNode) error { elements, err := pattern.Elements() if err != nil { return err } if len(elements) == 0 { return nil } if elements[0].YNode().Kind == yaml.MappingNode { return processListOfMaps(logger, pattern, resource) } return nil } // processListOfMaps - process arrays // in many cases like containers, volumes kustomize uses name field to match resource for processing // If any conditional anchor match resource field and if the pattern doesn't contain "name" field and // resource contains "name" field, then copy the name field from resource to pattern. func processListOfMaps(logger logr.Logger, pattern, resource *yaml.RNode) error { patternElements, err := pattern.Elements() if err != nil { return err } resourceElements, err := resource.Elements() if err != nil { return err } for _, patternElement := range patternElements { // If pattern has conditions, look for matching elements and process them hasAnyAnchor := hasAnchors(patternElement, hasAnchor) hasGlobalConditions := hasAnchors(patternElement, anchor.IsGlobal) if hasAnyAnchor { anyGlobalConditionPassed := false var lastGlobalAnchorError error = nil patternElementCopy := patternElement.Copy() for _, resourceElement := range resourceElements { if err := preProcessRecursive(logger, patternElementCopy, resourceElement); err != nil { logger.V(3).Info("anchor mismatch", "reason", err.Error()) if isConditionError(err) { continue } if isGlobalConditionError(err) { lastGlobalAnchorError = err continue } return err } if hasGlobalConditions { // global anchor has passed, there is no need to return an error anyGlobalConditionPassed = true } else { if err := handlePatternName(pattern, patternElementCopy, resourceElement); err != nil { return fmt.Errorf("failed to update name in pattern: %w", err) } } } if resource == nil { if err := preProcessRecursive(logger, patternElementCopy, resource); err != nil { logger.V(3).Info("anchor mismatch", "reason", err.Error()) if isConditionError(err) { continue } return err } if hasGlobalConditions { // global anchor has passed, there is no need to return an error anyGlobalConditionPassed = true } } if !anyGlobalConditionPassed && lastGlobalAnchorError != nil { return lastGlobalAnchorError } } } return nil } func handlePatternName(pattern, patternElement, resourceElement *yaml.RNode) error { // If condition is satisfied, create new pattern list element based on patternElement // but related with current resource element by name. // Resource element must have name. Without name kustomize won't be able to update this element. // In case if element does not have name, skip it. resourceElementName := resourceElement.Field("name") if resourceElementName.IsNilOrEmpty() { return nil } newNode := patternElement.Copy() empty, err := deleteAnchors(newNode, true, false) if err != nil { return err } // Do not add an empty element to the patch if empty { return nil } err = newNode.PipeE(yaml.SetField("name", resourceElementName.Value)) if err != nil { return err } err = pattern.PipeE(yaml.Append(newNode.YNode())) if err != nil { return err } return nil } func isConditionError(err error) bool { _, ok := err.(ConditionError) return ok } func isGlobalConditionError(err error) bool { _, ok := err.(GlobalConditionError) return ok } // validateConditions checks all conditions from current map. // If at least one condition fails, return error. // If caller handles list of maps and gets an error, it must skip element. // If caller handles list of maps and gets GlobalConditionError, it must skip entire rule. // If caller handles map, it must stop processing and skip entire rule. func validateConditions(logger logr.Logger, pattern, resource *yaml.RNode) error { var err error err = validateConditionsInternal(logger, pattern, resource, anchor.IsGlobal) if err != nil { return NewGlobalConditionError(err) } err = validateConditionsInternal(logger, pattern, resource, anchor.IsCondition) if err != nil { return NewConditionError(err) } return nil } // handleAddIfNotPresentAnchor handles adding anchors. // Remove anchor from pattern, if field already exists. // Remove anchor wrapping from key, if field does not exist in the resource. func handleAddIfNotPresentAnchor(pattern, resource *yaml.RNode) (int, error) { anchors, err := filterKeys(pattern, anchor.IsAddIfNotPresent) if err != nil { return 0, err } for _, a := range anchors { key := a.Key() if resource != nil && resource.Field(key) != nil { // Resource already has this field. // Delete the field with addIfNotPresent anchor from patch. err = pattern.PipeE(yaml.Clear(a.String())) if err != nil { return 0, err } } else { // Remove anchor tags from patch field key. renameField(a.String(), key, pattern) } } return len(anchors), nil } func filterKeys(pattern *yaml.RNode, condition func(anchor.Anchor) bool) ([]anchor.Anchor, error) { if !isMappingNode(pattern) { return nil, nil } fields, err := pattern.Fields() if err != nil { return nil, err } var anchors []anchor.Anchor for _, field := range fields { if a := anchor.Parse(field); a != nil && condition(a) { anchors = append(anchors, a) } } return anchors, nil } func nonAnchorKeys(pattern *yaml.RNode, condition func(anchor.Anchor) bool) ([]string, error) { if !isMappingNode(pattern) { return nil, nil } fields, err := pattern.Fields() if err != nil { return nil, err } var keys []string for _, field := range fields { if a := anchor.Parse(field); a == nil || condition(a) { keys = append(keys, field) } } return keys, nil } func isMappingNode(node *yaml.RNode) bool { return yaml.ErrorIfInvalid(node, yaml.MappingNode) == nil } func hasAnchor(a anchor.Anchor) bool { return anchor.ContainsCondition(a) || anchor.IsAddIfNotPresent(a) } func hasAnchors(pattern *yaml.RNode, isAnchor func(anchor.Anchor) bool) bool { ynode := pattern.YNode() //nolint:ifshort if ynode.Kind == yaml.MappingNode { fields, err := pattern.Fields() if err != nil { return false } for _, key := range fields { if a := anchor.Parse(key); a != nil && isAnchor(a) { return true } patternNode := pattern.Field(key) if !patternNode.IsNilOrEmpty() { if hasAnchors(patternNode.Value, isAnchor) { return true } } } } else if ynode.Kind == yaml.ScalarNode { return anchor.ContainsCondition(anchor.Parse(ynode.Value)) } else if ynode.Kind == yaml.SequenceNode { elements, _ := pattern.Elements() for _, e := range elements { if hasAnchors(e, isAnchor) { return true } } return false } return false } func renameField(name, newName string, pattern *yaml.RNode) { field := pattern.Field(name) if field == nil { return } field.Key.YNode().Value = newName } func convertRNodeToInterface(document *yaml.RNode) (interface{}, error) { if document.YNode().Kind == yaml.ScalarNode { return document.YNode().Value, nil } rawDocument, err := document.MarshalJSON() if err != nil { return nil, err } var documentInterface interface{} err = json.Unmarshal(rawDocument, &documentInterface) if err != nil { return nil, err } return documentInterface, nil } func checkCondition(logger logr.Logger, pattern *yaml.RNode, resource *yaml.RNode) error { patternInterface, err := convertRNodeToInterface(pattern) if err != nil { return err } resourceInterface, err := convertRNodeToInterface(resource) if err != nil { return err } err = validate.MatchPattern(logger, resourceInterface, patternInterface) if err != nil { return err } return nil } func deleteConditionElements(pattern *yaml.RNode) error { fields, err := pattern.Fields() if err != nil { return err } for _, field := range fields { deleteScalar := anchor.ContainsCondition(anchor.Parse(field)) canDelete, err := deleteAnchors(pattern.Field(field).Value, deleteScalar, false) if err != nil { return err } if canDelete { err = pattern.PipeE(yaml.Clear(field)) if err != nil { return err } } } return nil } // deleteAnchors deletes all the anchors and returns true, // if this node must be deleted from patch. // Node is considered to be deleted, if there were only // anchors elements. After anchors elements are removed, // A patch with nil values which could cause // unnecessary resource elements deletion. func deleteAnchors(node *yaml.RNode, deleteScalar, traverseMappingNodes bool) (bool, error) { switch node.YNode().Kind { case yaml.MappingNode: return deleteAnchorsInMap(node, traverseMappingNodes) case yaml.SequenceNode: return deleteAnchorsInList(node, traverseMappingNodes) case yaml.ScalarNode: return deleteScalar, nil } return false, nil } func deleteAnchorsInMap(node *yaml.RNode, traverseMappingNodes bool) (bool, error) { anchors, err := filterKeys(node, anchor.ContainsCondition) if err != nil { return false, err } // remove all conditional anchors with no child nodes first anchorsExist := false for _, a := range anchors { field := node.Field(a.String()) shouldDelete, err := deleteAnchors(field.Value, true, traverseMappingNodes) if err != nil { return false, err } if shouldDelete { if err := node.PipeE(yaml.Clear(a.String())); err != nil { return false, err } } else { anchorsExist = true } } if anchorsExist { if err := stripAnchorsFromNode(node, ""); err != nil { return false, fmt.Errorf("failed to remove anchor tags: %w", err) } } fields, err := node.Fields() if err != nil { return false, err } needToDelete := true for _, field := range fields { canDelete, err := deleteAnchors(node.Field(field).Value, false, traverseMappingNodes) if err != nil { return false, err } if canDelete { err = node.PipeE(yaml.Clear(field)) if err != nil { return false, err } } else { // If we have at least one element without anchor, // then we don't need to delete this element. needToDelete = false } } return needToDelete, nil } // stripAnchorFromNode strips one or more anchor fields from the node. // If key is "" all anchor fields are stripped. Otherwise, only the matching // field is stripped. func stripAnchorsFromNode(node *yaml.RNode, key string) error { anchors, err := filterKeys(node, anchor.ContainsCondition) if err != nil { return err } for _, a := range anchors { k := a.Key() if key == "" || k == key { renameField(a.String(), k, node) } } return nil } func deleteAnchorsInList(node *yaml.RNode, traverseMappingNodes bool) (bool, error) { elements, err := node.Elements() if err != nil { return false, err } wasEmpty := len(elements) == 0 for i, element := range elements { if hasAnchors(element, hasAnchor) { shouldDelete := true if traverseMappingNodes && isMappingNode(element) { shouldDelete, err = deleteAnchors(element, true, traverseMappingNodes) if err != nil { return false, fmt.Errorf("failed to delete anchors: %w", err) } } if shouldDelete { deleteListElement(node, i) } } else { // This element also could have some conditions // inside sub-arrays. Delete them too. canDelete, err := deleteAnchors(element, false, traverseMappingNodes) if err != nil { return false, fmt.Errorf("failed to delete anchors: %w", err) } if canDelete { deleteListElement(node, i) } } } elements, err = node.Elements() if err != nil { return false, err } if len(elements) == 0 && !wasEmpty { return true, nil } return false, nil } func deleteListElement(list *yaml.RNode, i int) { content := list.YNode().Content list.YNode().Content = append(content[:i], content[i+1:]...) } func validateConditionsInternal(logger logr.Logger, pattern, resource *yaml.RNode, filter func(anchor.Anchor) bool) error { anchors, err := filterKeys(pattern, filter) if err != nil { return err } for _, a := range anchors { conditionKey := a.Key() if resource == nil || resource.Field(conditionKey) == nil { return fmt.Errorf("could not found \"%s\" key in the resource", conditionKey) } patternValue := pattern.Field(a.String()).Value resourceValue := resource.Field(conditionKey).Value if count, err := handleAddIfNotPresentAnchor(patternValue, resourceValue); err != nil { return err } else if count > 0 { continue } if err := checkCondition(logger, patternValue, resourceValue); err != nil { return err } } return nil }