1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2024-12-15 17:51:20 +00:00
kyverno/pkg/policy/validate.go
Charles-Edouard Brétéché 062bd798f0
test: add kuttl tests for policy kinds validation (#6547)
* test: add kuttl tests for policy kinds validation

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* fix

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* fix

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* fix

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

---------

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>
2023-03-13 18:24:11 +08:00

1449 lines
49 KiB
Go

package policy
import (
"context"
"crypto/md5" //nolint:gosec
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"reflect"
"regexp"
"sort"
"strings"
"github.com/distribution/distribution/reference"
jsonpatch "github.com/evanphx/json-patch/v5"
"github.com/jmespath/go-jmespath"
"github.com/jmoiron/jsonq"
kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/utils/common"
"github.com/kyverno/kyverno/pkg/autogen"
"github.com/kyverno/kyverno/pkg/background/generate"
"github.com/kyverno/kyverno/pkg/clients/dclient"
openapicontroller "github.com/kyverno/kyverno/pkg/controllers/openapi"
enginecontext "github.com/kyverno/kyverno/pkg/engine/context"
"github.com/kyverno/kyverno/pkg/engine/variables"
"github.com/kyverno/kyverno/pkg/engine/variables/regex"
"github.com/kyverno/kyverno/pkg/logging"
"github.com/kyverno/kyverno/pkg/openapi"
apiutils "github.com/kyverno/kyverno/pkg/utils/api"
kubeutils "github.com/kyverno/kyverno/pkg/utils/kube"
"github.com/kyverno/kyverno/pkg/utils/wildcard"
"golang.org/x/exp/slices"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apimachinery/pkg/util/yaml"
"k8s.io/client-go/discovery"
)
var allowedVariables = regexp.MustCompile(`request\.|serviceAccountName|serviceAccountNamespace|element|elementIndex|@|images\.|image\.|target\.|([a-z_0-9]+\()[^{}]`)
var allowedVariablesBackground = regexp.MustCompile(`request\.|element|elementIndex|@|images\.|image\.|target\.|([a-z_0-9]+\()[^{}]`)
// wildCardAllowedVariables represents regex for the allowed fields in wildcards
var wildCardAllowedVariables = regexp.MustCompile(`\{\{\s*(request\.|serviceAccountName|serviceAccountNamespace)[^{}]*\}\}`)
var errOperationForbidden = errors.New("variables are forbidden in the path of a JSONPatch")
// validateJSONPatchPathForForwardSlash checks for forward slash
func validateJSONPatchPathForForwardSlash(patch string) error {
// Replace all variables in PatchesJSON6902, all variable checks should have happened already.
// This prevents further checks from failing unexpectedly.
patch = variables.ReplaceAllVars(patch, func(s string) string { return "kyvernojsonpatchvariable" })
re, err := regexp.Compile("^/")
if err != nil {
return err
}
jsonPatch, err := yaml.ToJSON([]byte(patch))
if err != nil {
return err
}
decodedPatch, err := jsonpatch.DecodePatch(jsonPatch)
if err != nil {
return err
}
for _, operation := range decodedPatch {
path, err := operation.Path()
if err != nil {
return err
}
val := re.MatchString(path)
if !val {
return fmt.Errorf("%s", path)
}
}
return nil
}
func validateJSONPatch(patch string, ruleIdx int) error {
patch = variables.ReplaceAllVars(patch, func(s string) string { return "kyvernojsonpatchvariable" })
jsonPatch, err := yaml.ToJSON([]byte(patch))
if err != nil {
return err
}
decodedPatch, err := jsonpatch.DecodePatch(jsonPatch)
if err != nil {
return err
}
for _, operation := range decodedPatch {
op := operation.Kind()
if op != "add" && op != "remove" && op != "replace" {
return fmt.Errorf("unexpected kind: spec.rules[%d]: %s", ruleIdx, op)
}
v, _ := operation.ValueInterface()
if v != nil {
vs := fmt.Sprintf("%v", v)
if strings.ContainsAny(vs, `"`) || strings.ContainsAny(vs, `'`) {
return fmt.Errorf("missing quote around value: spec.rules[%d]: %s", ruleIdx, vs)
}
}
}
return nil
}
func checkValidationFailureAction(spec *kyvernov1.Spec) []string {
msg := "Validation failure actions enforce/audit are deprecated, use Enforce/Audit instead."
if spec.ValidationFailureAction == "enforce" || spec.ValidationFailureAction == "audit" {
return []string{msg}
}
for _, override := range spec.ValidationFailureActionOverrides {
if override.Action == "enforce" || override.Action == "audit" {
return []string{msg}
}
}
return nil
}
// Validate checks the policy and rules declarations for required configurations
func Validate(policy, oldPolicy kyvernov1.PolicyInterface, client dclient.Interface, mock bool, openApiManager openapi.Manager) ([]string, error) {
var warnings []string
namespaced := policy.IsNamespaced()
spec := policy.GetSpec()
background := spec.BackgroundProcessingEnabled()
mutateExistingOnPolicyUpdate := spec.GetMutateExistingOnPolicyUpdate()
if !mock {
openapicontroller.NewController(client, openApiManager).CheckSync(context.TODO())
}
warnings = append(warnings, checkValidationFailureAction(spec)...)
var errs field.ErrorList
specPath := field.NewPath("spec")
err := ValidateVariables(policy, background)
if err != nil {
return warnings, err
}
if mutateExistingOnPolicyUpdate {
err := ValidateOnPolicyUpdate(policy, mutateExistingOnPolicyUpdate)
if err != nil {
return warnings, err
}
}
var res []*metav1.APIResourceList
clusterResources := sets.New[string]()
if !mock && namespaced {
// Get all the cluster type kind supported by cluster
res, err = discovery.ServerPreferredResources(client.Discovery().DiscoveryInterface())
if err != nil {
if discovery.IsGroupDiscoveryFailedError(err) {
err := err.(*discovery.ErrGroupDiscoveryFailed)
for gv, err := range err.Groups {
logging.Error(err, "failed to list api resources", "group", gv)
}
} else {
return warnings, err
}
}
for _, resList := range res {
for _, r := range resList.APIResources {
if !r.Namespaced {
clusterResources.Insert(r.Kind)
}
}
}
}
if errs := policy.Validate(clusterResources); len(errs) != 0 {
return warnings, errs.ToAggregate()
}
if !namespaced {
err := validateNamespaces(spec, specPath.Child("validationFailureActionOverrides"))
if err != nil {
return warnings, err
}
}
rules := autogen.ComputeRules(policy)
rulesPath := specPath.Child("rules")
for i, rule := range rules {
rulePath := rulesPath.Index(i)
// check for forward slash
if err := validateJSONPatchPathForForwardSlash(rule.Mutation.PatchesJSON6902); err != nil {
return warnings, fmt.Errorf("path must begin with a forward slash: spec.rules[%d]: %s", i, err)
}
if err := validateJSONPatch(rule.Mutation.PatchesJSON6902, i); err != nil {
return warnings, fmt.Errorf("%s", err)
}
if jsonPatchOnPod(rule) {
msg := "Pods managed by workload controllers should not be directly mutated using policies. " +
"Use the autogen feature or write policies that match Pod controllers."
logging.V(1).Info(msg)
warnings = append(warnings, msg)
}
// validate resource description
if path, err := validateResources(rulePath, rule); err != nil {
return warnings, fmt.Errorf("path: spec.rules[%d].%s: %v", i, path, err)
}
err := validateElementInForEach(rule)
if err != nil {
return warnings, err
}
if err := validateRuleContext(rule); err != nil {
return warnings, fmt.Errorf("path: spec.rules[%d]: %v", i, err)
}
if err := validateRuleImageExtractorsJMESPath(rule); err != nil {
return warnings, fmt.Errorf("path: spec.rules[%d]: %v", i, err)
}
// If a rule's match block does not match any kind,
// we should only allow it to have metadata in its overlay
if len(rule.MatchResources.Any) > 0 {
for _, rmr := range rule.MatchResources.Any {
if len(rmr.Kinds) == 0 {
return warnings, validateMatchKindHelper(rule)
}
}
} else if len(rule.MatchResources.All) > 0 {
for _, rmr := range rule.MatchResources.All {
if len(rmr.Kinds) == 0 {
return warnings, validateMatchKindHelper(rule)
}
}
} else {
if len(rule.MatchResources.Kinds) == 0 {
return warnings, validateMatchKindHelper(rule)
}
}
if err := immutableGenerateFields(policy, oldPolicy); err != nil {
return warnings, err
}
// validate Cluster Resources in namespaced policy
// For namespaced policy, ClusterResource type field and values are not allowed in match and exclude
if namespaced {
return warnings, checkClusterResourceInMatchAndExclude(rule, clusterResources, policy.GetNamespace(), mock, res)
}
if err := validateActions(i, &rules[i], client, mock); err != nil {
return warnings, err
}
if rule.HasVerifyImages() {
verifyImagePath := rulePath.Child("verifyImages")
for index, i := range rule.VerifyImages {
errs = append(errs, i.Validate(verifyImagePath.Index(index))...)
}
if len(errs) != 0 {
return warnings, errs.ToAggregate()
}
}
kindsFromRule := rule.MatchResources.GetKinds()
resourceTypesMap := make(map[string]bool)
for _, kind := range kindsFromRule {
_, k := kubeutils.GetKindFromGVK(kind)
k, _ = kubeutils.SplitSubresource(k)
resourceTypesMap[k] = true
}
if len(resourceTypesMap) == 1 {
for k := range resourceTypesMap {
if k == "Pod" && podControllerAutoGenExclusion(policy) {
msg := "Policies that match Pods apply to all Pods including those created and managed by controllers " +
"excluded from autogen. Use preconditions to exclude the Pods managed by controllers which are " +
"excluded from autogen. Refer to https://kyverno.io/docs/writing-policies/autogen/ for details."
warnings = append(warnings, msg)
}
}
}
// Validate Kind with match resource kinds
match := rule.MatchResources
exclude := rule.ExcludeResources
for _, value := range match.Any {
wildcardErr := validateWildcard(value.ResourceDescription.Kinds, spec, rule)
if wildcardErr != nil {
return warnings, wildcardErr
}
if !slices.Contains(value.ResourceDescription.Kinds, "*") {
err := validateKinds(value.ResourceDescription.Kinds, mock, background, rule.HasValidate(), client)
if err != nil {
return warnings, fmt.Errorf("the kind defined in the any match resource is invalid: %w", err)
}
}
}
for _, value := range match.All {
wildcardErr := validateWildcard(value.ResourceDescription.Kinds, spec, rule)
if wildcardErr != nil {
return warnings, wildcardErr
}
if !slices.Contains(value.ResourceDescription.Kinds, "*") {
err := validateKinds(value.ResourceDescription.Kinds, mock, background, rule.HasValidate(), client)
if err != nil {
return warnings, fmt.Errorf("the kind defined in the all match resource is invalid: %w", err)
}
}
}
for _, value := range exclude.Any {
wildcardErr := validateWildcard(value.ResourceDescription.Kinds, spec, rule)
if wildcardErr != nil {
return warnings, wildcardErr
}
if !slices.Contains(value.ResourceDescription.Kinds, "*") {
err := validateKinds(value.ResourceDescription.Kinds, mock, background, rule.HasValidate(), client)
if err != nil {
return warnings, fmt.Errorf("the kind defined in the any exclude resource is invalid: %w", err)
}
}
}
for _, value := range exclude.All {
wildcardErr := validateWildcard(value.ResourceDescription.Kinds, spec, rule)
if wildcardErr != nil {
return warnings, wildcardErr
}
if !slices.Contains(value.ResourceDescription.Kinds, "*") {
err := validateKinds(value.ResourceDescription.Kinds, mock, background, rule.HasValidate(), client)
if err != nil {
return warnings, fmt.Errorf("the kind defined in the all exclude resource is invalid: %w", err)
}
}
}
if !slices.Contains(rule.MatchResources.Kinds, "*") {
err := validateKinds(rule.MatchResources.Kinds, mock, background, rule.HasValidate(), client)
if err != nil {
return warnings, fmt.Errorf("match resource kind is invalid: %w", err)
}
err = validateKinds(rule.ExcludeResources.Kinds, mock, background, rule.HasValidate(), client)
if err != nil {
return warnings, fmt.Errorf("exclude resource kind is invalid: %w", err)
}
} else {
wildcardErr := validateWildcard(rule.MatchResources.Kinds, spec, rule)
if wildcardErr != nil {
return warnings, wildcardErr
}
wildcardErr = validateWildcard(rule.ExcludeResources.Kinds, spec, rule)
if wildcardErr != nil {
return warnings, wildcardErr
}
}
// Validate string values in labels
if !isLabelAndAnnotationsString(rule) {
return warnings, fmt.Errorf("labels and annotations supports only string values, \"use double quotes around the non string values\"")
}
// add label to source mentioned in policy
if !mock && rule.Generation.Clone.Name != "" {
obj, err := client.GetResource(context.TODO(), "", rule.Generation.Kind, rule.Generation.Clone.Namespace, rule.Generation.Clone.Name)
if err != nil {
logging.Error(err, fmt.Sprintf("source resource %s/%s/%s not found.", rule.Generation.Kind, rule.Generation.Clone.Namespace, rule.Generation.Clone.Name))
continue
}
err = UpdateSourceResource(client, rule.Generation.Kind, rule.Generation.Clone.Namespace, policy.GetName(), obj)
if err != nil {
logging.Error(err, "failed to update source", "kind", obj.GetKind(), "name", obj.GetName(), "namespace", obj.GetNamespace())
continue
}
logging.V(4).Info("updated source", "kind", obj.GetKind(), "name", obj.GetName(), "namespace", obj.GetNamespace())
}
if !mock && len(rule.Generation.CloneList.Kinds) != 0 {
for _, kind := range rule.Generation.CloneList.Kinds {
apiVersion, kind := kubeutils.GetKindFromGVK(kind)
resources, err := client.ListResource(context.TODO(), apiVersion, kind, rule.Generation.CloneList.Namespace, rule.Generation.CloneList.Selector)
if err != nil {
logging.Error(err, fmt.Sprintf("failed to list resources %s/%s.", kind, rule.Generation.CloneList.Namespace))
continue
}
for _, rName := range resources.Items {
obj, err := client.GetResource(context.TODO(), apiVersion, kind, rule.Generation.CloneList.Namespace, rName.GetName())
if err != nil {
logging.Error(err, fmt.Sprintf("source resource %s/%s/%s not found.", kind, rule.Generation.Clone.Namespace, rule.Generation.Clone.Name))
continue
}
err = UpdateSourceResource(client, kind, rule.Generation.CloneList.Namespace, policy.GetName(), obj)
if err != nil {
logging.Error(err, "failed to update source", "kind", obj.GetKind(), "name", obj.GetName(), "namespace", obj.GetNamespace())
continue
}
}
}
}
matchKinds := match.GetKinds()
excludeKinds := exclude.GetKinds()
allKinds := make([]string, 0, len(matchKinds)+len(excludeKinds))
allKinds = append(allKinds, matchKinds...)
allKinds = append(allKinds, excludeKinds...)
if rule.HasValidate() {
validationJson, err := json.Marshal(rule.Validation)
if err != nil {
return nil, err
}
checkForScaleSubresource(validationJson, allKinds, &warnings)
checkForStatusSubresource(validationJson, allKinds, &warnings)
}
if rule.HasMutate() {
mutationJson, err := json.Marshal(rule.Mutation)
targets := rule.Mutation.Targets
for _, target := range targets {
allKinds = append(allKinds, target.GetKind())
}
if err != nil {
return nil, err
}
checkForScaleSubresource(mutationJson, allKinds, &warnings)
checkForStatusSubresource(mutationJson, allKinds, &warnings)
}
}
if !mock && (spec.SchemaValidation == nil || *spec.SchemaValidation) {
if err := openApiManager.ValidatePolicyMutation(policy); err != nil {
return warnings, err
}
}
return warnings, nil
}
func UpdateSourceResource(client dclient.Interface, kind, namespace string, policyName string, obj *unstructured.Unstructured) error {
updateSource := true
label := obj.GetLabels()
if len(label) == 0 {
label = make(map[string]string)
label[generate.LabelClonePolicyName] = policyName
} else {
if label[generate.LabelClonePolicyName] != "" {
policyNames := label[generate.LabelClonePolicyName]
if !strings.Contains(policyNames, policyName) {
policyNames = policyNames + "," + policyName
label[generate.LabelClonePolicyName] = policyNames
} else {
updateSource = false
}
} else {
label[generate.LabelClonePolicyName] = policyName
}
}
if updateSource {
logging.V(4).Info("updating existing clone source labels")
obj.SetLabels(label)
obj.SetResourceVersion("")
_, err := client.UpdateResource(context.TODO(), obj.GetAPIVersion(), kind, namespace, obj, false)
if err != nil {
logging.Error(err, "failed to update source", "kind", obj.GetKind(), "name", obj.GetName(), "namespace", obj.GetNamespace())
return err
}
logging.V(4).Info("updated source", "kind", obj.GetKind(), "name", obj.GetName(), "namespace", obj.GetNamespace())
}
return nil
}
func ValidateVariables(p kyvernov1.PolicyInterface, backgroundMode bool) error {
vars := hasVariables(p)
if backgroundMode {
if err := containsUserVariables(p, vars); err != nil {
return fmt.Errorf("only select variables are allowed in background mode. Set spec.background=false to disable background mode for this policy rule: %s ", err)
}
}
if err := hasInvalidVariables(p, backgroundMode); err != nil {
return fmt.Errorf("policy contains invalid variables: %s", err.Error())
}
return nil
}
// hasInvalidVariables - checks for unexpected variables in the policy
func hasInvalidVariables(policy kyvernov1.PolicyInterface, background bool) error {
for _, r := range autogen.ComputeRules(policy) {
ruleCopy := r.DeepCopy()
if err := ruleForbiddenSectionsHaveVariables(ruleCopy); err != nil {
return err
}
// skip variable checks on verifyImages.attestations, as variables in attestations are dynamic
for i, vi := range ruleCopy.VerifyImages {
for j := range vi.Attestations {
ruleCopy.VerifyImages[i].Attestations[j].Conditions = nil
}
}
ctx := buildContext(ruleCopy, background)
if _, err := variables.SubstituteAllInRule(logging.GlobalLogger(), ctx, *ruleCopy); !variables.CheckNotFoundErr(err) {
return fmt.Errorf("variable substitution failed for rule %s: %s", ruleCopy.Name, err.Error())
}
}
return nil
}
func ValidateOnPolicyUpdate(p kyvernov1.PolicyInterface, onPolicyUpdate bool) error {
vars := hasVariables(p)
if len(vars) == 0 {
return nil
}
if err := hasInvalidVariables(p, onPolicyUpdate); err != nil {
return fmt.Errorf("update event, policy contains invalid variables: %s", err.Error())
}
if err := containsUserVariables(p, vars); err != nil {
return fmt.Errorf("only select variables are allowed in on policy update. Set spec.mutateExistingOnPolicyUpdate=false to disable update policy mode for this policy rule: %s ", err)
}
return nil
}
// for now forbidden sections are match, exclude and
func ruleForbiddenSectionsHaveVariables(rule *kyvernov1.Rule) error {
var err error
err = jsonPatchPathHasVariables(rule.Mutation.PatchesJSON6902)
if err != nil && errors.Is(errOperationForbidden, err) {
return fmt.Errorf("rule \"%s\" should not have variables in patchesJSON6902 path section", rule.Name)
}
err = objectHasVariables(rule.ExcludeResources)
if err != nil {
return fmt.Errorf("rule \"%s\" should not have variables in exclude section", rule.Name)
}
err = objectHasVariables(rule.MatchResources)
if err != nil {
return fmt.Errorf("rule \"%s\" should not have variables in match section", rule.Name)
}
err = imageRefHasVariables(rule.VerifyImages)
if err != nil {
return fmt.Errorf("rule \"%s\" should not have variables in image reference section", rule.Name)
}
return nil
}
// hasVariables - check for variables in the policy
func hasVariables(policy kyvernov1.PolicyInterface) [][]string {
policyRaw, _ := json.Marshal(policy)
matches := regex.RegexVariables.FindAllStringSubmatch(string(policyRaw), -1)
return matches
}
func jsonPatchPathHasVariables(patch string) error {
jsonPatch, err := yaml.ToJSON([]byte(patch))
if err != nil {
return err
}
decodedPatch, err := jsonpatch.DecodePatch(jsonPatch)
if err != nil {
return err
}
for _, operation := range decodedPatch {
path, err := operation.Path()
if err != nil {
return err
}
vars := regex.RegexVariables.FindAllString(path, -1)
if len(vars) > 0 {
return errOperationForbidden
}
}
return nil
}
func objectHasVariables(object interface{}) error {
var err error
objectJSON, err := json.Marshal(object)
if err != nil {
return err
}
if len(common.RegexVariables.FindAllStringSubmatch(string(objectJSON), -1)) > 0 {
return fmt.Errorf("invalid variables")
}
return nil
}
func imageRefHasVariables(verifyImages []kyvernov1.ImageVerification) error {
for _, verifyImage := range verifyImages {
verifyImage = *verifyImage.Convert()
for _, imageRef := range verifyImage.ImageReferences {
matches := regex.RegexVariables.FindAllString(imageRef, -1)
if len(matches) > 0 {
return fmt.Errorf("variables are not allowed in image reference")
}
}
}
return nil
}
func buildContext(rule *kyvernov1.Rule, background bool) *enginecontext.MockContext {
re := getAllowedVariables(background)
ctx := enginecontext.NewMockContext(re)
addContextVariables(rule.Context, ctx)
for _, fe := range rule.Validation.ForEachValidation {
addContextVariables(fe.Context, ctx)
}
for _, fe := range rule.Mutation.ForEachMutation {
addContextVariables(fe.Context, ctx)
}
return ctx
}
func getAllowedVariables(background bool) *regexp.Regexp {
if background {
return allowedVariablesBackground
}
return allowedVariables
}
func addContextVariables(entries []kyvernov1.ContextEntry, ctx *enginecontext.MockContext) {
for _, contextEntry := range entries {
if contextEntry.APICall != nil || contextEntry.ImageRegistry != nil || contextEntry.Variable != nil {
ctx.AddVariable(contextEntry.Name + "*")
}
if contextEntry.ConfigMap != nil {
ctx.AddVariable(contextEntry.Name + ".data")
ctx.AddVariable(contextEntry.Name + ".metadata")
ctx.AddVariable(contextEntry.Name + ".data.*")
ctx.AddVariable(contextEntry.Name + ".metadata.*")
}
}
}
func validateElementInForEach(document apiextensions.JSON) error {
jsonByte, err := json.Marshal(document)
if err != nil {
return err
}
var jsonInterface interface{}
err = json.Unmarshal(jsonByte, &jsonInterface)
if err != nil {
return err
}
_, err = variables.ValidateElementInForEach(logging.GlobalLogger(), jsonInterface)
return err
}
func validateMatchKindHelper(rule kyvernov1.Rule) error {
if !ruleOnlyDealsWithResourceMetaData(rule) {
return fmt.Errorf("policy can only deal with the metadata field of the resource if" +
" the rule does not match any kind")
}
return fmt.Errorf("at least one element must be specified in a kind block, the kind attribute is mandatory when working with the resources element")
}
// isLabelAndAnnotationsString :- Validate if labels and annotations contains only string values
func isLabelAndAnnotationsString(rule kyvernov1.Rule) bool {
checkLabelAnnotation := func(metaKey map[string]interface{}) bool {
for mk := range metaKey {
if mk == "labels" {
labelKey, ok := metaKey[mk].(map[string]interface{})
if ok {
// range over labels
for _, val := range labelKey {
if reflect.TypeOf(val).String() != "string" {
return false
}
}
}
} else if mk == "annotations" {
annotationKey, ok := metaKey[mk].(map[string]interface{})
if ok {
// range over annotations
for _, val := range annotationKey {
if reflect.TypeOf(val).String() != "string" {
return false
}
}
}
}
}
return true
}
// checkMetadata - Verify if the labels and annotations contains string value inside metadata
checkMetadata := func(patternMap map[string]interface{}) bool {
for k := range patternMap {
if k == "metadata" {
metaKey, ok := patternMap[k].(map[string]interface{})
if ok {
// range over metadata
return checkLabelAnnotation(metaKey)
}
}
if k == "spec" {
metadata, _ := jsonq.NewQuery(patternMap).Object("spec", "template", "metadata")
return checkLabelAnnotation(metadata)
}
}
return true
}
if rule.HasValidate() {
if rule.Validation.ForEachValidation != nil {
for _, foreach := range rule.Validation.ForEachValidation {
patternMap, ok := foreach.GetPattern().(map[string]interface{})
if ok {
return checkMetadata(patternMap)
}
}
} else {
patternMap, ok := rule.Validation.GetPattern().(map[string]interface{})
if ok {
return checkMetadata(patternMap)
} else if rule.Validation.GetAnyPattern() != nil {
anyPatterns, err := rule.Validation.DeserializeAnyPattern()
if err != nil {
logging.Error(err, "failed to deserialize anyPattern, expect type array")
return false
}
for _, pattern := range anyPatterns {
patternMap, ok := pattern.(map[string]interface{})
if ok {
ret := checkMetadata(patternMap)
if !ret {
return ret
}
}
}
}
}
}
if rule.HasMutate() {
if rule.Mutation.ForEachMutation != nil {
for _, foreach := range rule.Mutation.ForEachMutation {
forEachStrategicMergeMap, ok := foreach.GetPatchStrategicMerge().(map[string]interface{})
if ok {
return checkMetadata(forEachStrategicMergeMap)
}
}
} else {
strategicMergeMap, ok := rule.Mutation.GetPatchStrategicMerge().(map[string]interface{})
if ok {
return checkMetadata(strategicMergeMap)
}
}
}
return true
}
func ruleOnlyDealsWithResourceMetaData(rule kyvernov1.Rule) bool {
patches, _ := rule.Mutation.GetPatchStrategicMerge().(map[string]interface{})
for k := range patches {
if k != "metadata" {
return false
}
}
if rule.Mutation.PatchesJSON6902 != "" {
bytes := []byte(rule.Mutation.PatchesJSON6902)
jp, _ := jsonpatch.DecodePatch(bytes)
for _, o := range jp {
path, _ := o.Path()
if !strings.HasPrefix(path, "/metadata") {
return false
}
}
}
patternMap, _ := rule.Validation.GetPattern().(map[string]interface{})
for k := range patternMap {
if k != "metadata" {
return false
}
}
anyPatterns, err := rule.Validation.DeserializeAnyPattern()
if err != nil {
logging.Error(err, "failed to deserialize anyPattern, expect type array")
return false
}
for _, pattern := range anyPatterns {
patternMap, _ := pattern.(map[string]interface{})
for k := range patternMap {
if k != "metadata" {
return false
}
}
}
return true
}
func validateResources(path *field.Path, rule kyvernov1.Rule) (string, error) {
// validate userInfo in match and exclude
if errs := rule.ExcludeResources.UserInfo.Validate(path.Child("exclude")); len(errs) != 0 {
return "exclude", errs.ToAggregate()
}
if (len(rule.MatchResources.Any) > 0 || len(rule.MatchResources.All) > 0) && !reflect.DeepEqual(rule.MatchResources.ResourceDescription, kyvernov1.ResourceDescription{}) {
return "match.", fmt.Errorf("can't specify any/all together with match resources")
}
if (len(rule.ExcludeResources.Any) > 0 || len(rule.ExcludeResources.All) > 0) && !reflect.DeepEqual(rule.ExcludeResources.ResourceDescription, kyvernov1.ResourceDescription{}) {
return "exclude.", fmt.Errorf("can't specify any/all together with exclude resources")
}
if len(rule.ExcludeResources.Any) > 0 && len(rule.ExcludeResources.All) > 0 {
return "match.", fmt.Errorf("can't specify any and all together")
}
if len(rule.MatchResources.Any) > 0 {
for _, rmr := range rule.MatchResources.Any {
// matched resources
if path, err := validateMatchedResourceDescription(rmr.ResourceDescription); err != nil {
return fmt.Sprintf("match.resources.%s", path), err
}
}
} else if len(rule.MatchResources.All) > 0 {
for _, rmr := range rule.MatchResources.All {
// matched resources
if path, err := validateMatchedResourceDescription(rmr.ResourceDescription); err != nil {
return fmt.Sprintf("match.resources.%s", path), err
}
}
} else {
// matched resources
if path, err := validateMatchedResourceDescription(rule.MatchResources.ResourceDescription); err != nil {
return fmt.Sprintf("match.resources.%s", path), err
}
}
// validating the values present under validate.preconditions, if they exist
if target := rule.GetAnyAllConditions(); target != nil {
if path, err := validateConditions(target, "preconditions"); err != nil {
return fmt.Sprintf("validate.%s", path), err
}
}
// validating the values present under validate.conditions, if they exist
if rule.Validation.Deny != nil {
if target := rule.Validation.Deny.GetAnyAllConditions(); target != nil {
if path, err := validateConditions(target, "conditions"); err != nil {
return fmt.Sprintf("validate.deny.%s", path), err
}
}
}
return "", nil
}
// validateConditions validates all the 'conditions' or 'preconditions' of a rule depending on the corresponding 'condition.key'.
// As of now, it is validating the 'value' field whether it contains the only allowed set of values or not when 'condition.key' is {{request.operation}}
// this is backwards compatible i.e. conditions can be provided in the old manner as well i.e. without 'any' or 'all'
func validateConditions(conditions apiextensions.JSON, schemaKey string) (string, error) {
// Conditions can only exist under some specific keys of the policy schema
allowedSchemaKeys := map[string]bool{
"preconditions": true,
"conditions": true,
}
if !allowedSchemaKeys[schemaKey] {
return schemaKey, fmt.Errorf("wrong schema key found for validating the conditions. Conditions can only occur under one of ['preconditions', 'conditions'] keys in the policy schema")
}
// conditions are currently in the form of []interface{}
kyvernoConditions, err := apiutils.ApiextensionsJsonToKyvernoConditions(conditions)
if err != nil {
return schemaKey, err
}
switch typedConditions := kyvernoConditions.(type) {
case kyvernov1.AnyAllConditions:
// validating the conditions under 'any', if there are any
if !reflect.DeepEqual(typedConditions, kyvernov1.AnyAllConditions{}) && typedConditions.AnyConditions != nil {
for i, condition := range typedConditions.AnyConditions {
if path, err := validateConditionValues(condition); err != nil {
return fmt.Sprintf("%s.any[%d].%s", schemaKey, i, path), err
}
}
}
// validating the conditions under 'all', if there are any
if !reflect.DeepEqual(typedConditions, kyvernov1.AnyAllConditions{}) && typedConditions.AllConditions != nil {
for i, condition := range typedConditions.AllConditions {
if path, err := validateConditionValues(condition); err != nil {
return fmt.Sprintf("%s.all[%d].%s", schemaKey, i, path), err
}
}
}
case []kyvernov1.Condition: // backwards compatibility
for i, condition := range typedConditions {
if path, err := validateConditionValues(condition); err != nil {
return fmt.Sprintf("%s[%d].%s", schemaKey, i, path), err
}
}
}
return "", nil
}
// validateConditionValues validates whether all the values under the 'value' field of a 'conditions' field
// are apt with respect to the provided 'condition.key'
func validateConditionValues(c kyvernov1.Condition) (string, error) {
k := c.GetKey()
v := c.GetValue()
if k == nil || v == nil || c.Operator == "" {
return "", fmt.Errorf("entered value of `key`, `value` or `operator` is missing or misspelled")
}
switch reflect.TypeOf(k).Kind() {
case reflect.String:
value, err := validateValuesKeyRequest(c)
return value, err
default:
return "", nil
}
}
func validateValuesKeyRequest(c kyvernov1.Condition) (string, error) {
k := c.GetKey()
switch strings.ReplaceAll(k.(string), " ", "") {
case "{{request.operation}}":
return validateConditionValuesKeyRequestOperation(c)
default:
return "", nil
}
}
// validateConditionValuesKeyRequestOperation validates whether all the values under the 'value' field of a 'conditions' field
// are one of ["CREATE", "UPDATE", "DELETE", "CONNECT"] when 'condition.key' is {{request.operation}}
func validateConditionValuesKeyRequestOperation(c kyvernov1.Condition) (string, error) {
valuesAllowed := map[string]bool{
"CREATE": true,
"UPDATE": true,
"DELETE": true,
"CONNECT": true,
}
v := c.GetValue()
switch reflect.TypeOf(v).Kind() {
case reflect.String:
valueStr := v.(string)
// allow templatized values like {{ config-map.data.sample-key }}
// because they might be actually pointing to a rightful value in the provided config-map
if len(valueStr) >= 4 && valueStr[:2] == "{{" && valueStr[len(valueStr)-2:] == "}}" {
return "", nil
}
if !valuesAllowed[valueStr] {
return fmt.Sprintf("value: %s", v.(string)), fmt.Errorf("unknown value '%s' found under the 'value' field. Only the following values are allowed: [CREATE, UPDATE, DELETE, CONNECT]", v.(string))
}
case reflect.Slice:
values := reflect.ValueOf(v)
for i := 0; i < values.Len(); i++ {
value := values.Index(i).Interface().(string)
if !valuesAllowed[value] {
return fmt.Sprintf("value[%d]", i), fmt.Errorf("unknown value '%s' found under the 'value' field. Only the following values are allowed: [CREATE, UPDATE, DELETE, CONNECT]", value)
}
}
default:
return "value", fmt.Errorf("'value' field found to be of the type %v. The provided value/values are expected to be either in the form of a string or list", reflect.TypeOf(v).Kind())
}
return "", nil
}
func validateRuleContext(rule kyvernov1.Rule) error {
if rule.Context == nil || len(rule.Context) == 0 {
return nil
}
for _, entry := range rule.Context {
if entry.Name == "" {
return fmt.Errorf("a name is required for context entries")
}
for _, v := range []string{"images", "request", "serviceAccountName", "serviceAccountNamespace", "element", "elementIndex"} {
if entry.Name == v || strings.HasPrefix(entry.Name, v+".") {
return fmt.Errorf("entry name %s is invalid as it conflicts with a pre-defined variable %s", entry.Name, v)
}
}
var err error
if entry.ConfigMap != nil && entry.APICall == nil && entry.ImageRegistry == nil && entry.Variable == nil {
err = validateConfigMap(entry)
} else if entry.ConfigMap == nil && entry.APICall != nil && entry.ImageRegistry == nil && entry.Variable == nil {
err = validateAPICall(entry)
} else if entry.ConfigMap == nil && entry.APICall == nil && entry.ImageRegistry != nil && entry.Variable == nil {
err = validateImageRegistry(entry)
} else if entry.ConfigMap == nil && entry.APICall == nil && entry.ImageRegistry == nil && entry.Variable != nil {
err = validateVariable(entry)
} else {
return fmt.Errorf("exactly one of configMap or apiCall or imageRegistry or variable is required for context entries")
}
if err != nil {
return err
}
}
return nil
}
// validateRuleImageExtractorsJMESPath ensures that the rule does not
// mutate image digests if it has an image extractor that uses a JMESPath.
func validateRuleImageExtractorsJMESPath(rule kyvernov1.Rule) error {
imageExtractorConfigs := rule.ImageExtractors
imageVerifications := rule.VerifyImages
if imageExtractorConfigs == nil || imageVerifications == nil {
return nil
}
anyMutateDigest := false
for _, imageVerification := range imageVerifications {
if imageVerification.MutateDigest {
anyMutateDigest = true
break
}
}
if !anyMutateDigest {
return nil
}
anyJMESPath := false
for _, imageExtractors := range imageExtractorConfigs {
for _, imageExtractor := range imageExtractors {
if imageExtractor.JMESPath != "" {
anyJMESPath = true
break
}
}
}
if anyJMESPath {
return fmt.Errorf("jmespath may not be used in an image extractor when mutating digests with verify images")
}
return nil
}
func validateVariable(entry kyvernov1.ContextEntry) error {
// If JMESPath contains variables, the validation will fail because it's not possible to infer which value
// will be inserted by the variable
// Skip validation if a variable is detected
jmesPath := variables.ReplaceAllVars(entry.Variable.JMESPath, func(s string) string { return "kyvernojmespathvariable" })
if !strings.Contains(jmesPath, "kyvernojmespathvariable") && entry.Variable.JMESPath != "" {
if _, err := jmespath.NewParser().Parse(entry.Variable.JMESPath); err != nil {
return fmt.Errorf("failed to parse JMESPath %s: %v", entry.Variable.JMESPath, err)
}
}
if entry.Variable.Value == nil && jmesPath == "" {
return fmt.Errorf("a variable must define a value or a jmesPath expression")
}
if entry.Variable.Default != nil && jmesPath == "" {
return fmt.Errorf("a variable must define a default value only when a jmesPath expression is defined")
}
return nil
}
func validateConfigMap(entry kyvernov1.ContextEntry) error {
if entry.ConfigMap.Name == "" {
return fmt.Errorf("a name is required for configMap context entry")
}
if entry.ConfigMap.Namespace == "" {
return fmt.Errorf("a namespace is required for configMap context entry")
}
return nil
}
func validateAPICall(entry kyvernov1.ContextEntry) error {
// If JMESPath contains variables, the validation will fail because it's not possible to infer which value
// will be inserted by the variable
// Skip validation if a variable is detected
jmesPath := variables.ReplaceAllVars(entry.APICall.JMESPath, func(s string) string { return "kyvernojmespathvariable" })
if !strings.Contains(jmesPath, "kyvernojmespathvariable") && entry.APICall.JMESPath != "" {
if _, err := jmespath.NewParser().Parse(entry.APICall.JMESPath); err != nil {
return fmt.Errorf("failed to parse JMESPath %s: %v", entry.APICall.JMESPath, err)
}
}
return nil
}
func validateImageRegistry(entry kyvernov1.ContextEntry) error {
if entry.ImageRegistry.Reference == "" {
return fmt.Errorf("a ref is required for imageRegistry context entry")
}
// Replace all variables to prevent validation failing on variable keys.
ref := variables.ReplaceAllVars(entry.ImageRegistry.Reference, func(s string) string { return "kyvernoimageref" })
// it's no use validating a reference that contains a variable
if !strings.Contains(ref, "kyvernoimageref") {
_, err := reference.Parse(ref)
if err != nil {
return fmt.Errorf("bad image: %s: %w", ref, err)
}
}
// If JMESPath contains variables, the validation will fail because it's not possible to infer which value
// will be inserted by the variable
// Skip validation if a variable is detected
jmesPath := variables.ReplaceAllVars(entry.ImageRegistry.JMESPath, func(s string) string { return "kyvernojmespathvariable" })
if !strings.Contains(jmesPath, "kyvernojmespathvariable") && entry.ImageRegistry.JMESPath != "" {
if _, err := jmespath.NewParser().Parse(entry.ImageRegistry.JMESPath); err != nil {
return fmt.Errorf("failed to parse JMESPath %s: %v", entry.ImageRegistry.JMESPath, err)
}
}
return nil
}
// validateResourceDescription checks if all necessary fields are present and have values. Also checks a Selector.
// field type is checked through openapi
// Returns error if
// - kinds is empty array in matched resource block, i.e. kinds: []
// - selector is invalid
func validateMatchedResourceDescription(rd kyvernov1.ResourceDescription) (string, error) {
if reflect.DeepEqual(rd, kyvernov1.ResourceDescription{}) {
return "", fmt.Errorf("match resources not specified")
}
return "", nil
}
// checkClusterResourceInMatchAndExclude returns false if namespaced ClusterPolicy contains cluster wide resources in
// Match and Exclude block
func checkClusterResourceInMatchAndExclude(rule kyvernov1.Rule, clusterResources sets.Set[string], policyNamespace string, mock bool, res []*metav1.APIResourceList) error {
if !mock {
// Check for generate policy
// - if resource to be generated is namespaced resource then the namespace field
// should be mentioned
// - if resource to be generated is non namespaced resource then the namespace field
// should not be mentioned
if rule.HasGenerate() {
generateResourceKind := rule.Generation.Kind
for _, resList := range res {
for _, r := range resList.APIResources {
if r.Kind == generateResourceKind {
if r.Namespaced {
if rule.Generation.Namespace == "" {
return fmt.Errorf("path: spec.rules[%v]: please mention the namespace to generate a namespaced resource", rule.Name)
}
if rule.Generation.Namespace != policyNamespace {
return fmt.Errorf("path: spec.rules[%v]: a namespaced policy cannot generate resources in other namespaces, expected: %v, received: %v", rule.Name, policyNamespace, rule.Generation.Namespace)
}
if rule.Generation.Clone.Name != "" {
if rule.Generation.Clone.Namespace != policyNamespace {
return fmt.Errorf("path: spec.rules[%v]: a namespaced policy cannot clone resources to or from other namespaces, expected: %v, received: %v", rule.Name, policyNamespace, rule.Generation.Clone.Namespace)
}
}
} else {
if rule.Generation.Namespace != "" {
return fmt.Errorf("path: spec.rules[%v]: do not mention the namespace to generate a non namespaced resource", rule.Name)
}
if policyNamespace != "" {
return fmt.Errorf("path: spec.rules[%v]: a namespaced policy cannot generate cluster-wide resources", rule.Name)
}
}
} else if len(rule.Generation.CloneList.Kinds) != 0 {
for _, kind := range rule.Generation.CloneList.Kinds {
_, splitkind := kubeutils.GetKindFromGVK(kind)
if r.Kind == splitkind {
if r.Namespaced {
if rule.Generation.CloneList.Namespace != policyNamespace {
return fmt.Errorf("path: spec.rules[%v]: a namespaced policy cannot clone resource in other namespace, expected: %v, received: %v", rule.Name, policyNamespace, rule.Generation.Namespace)
}
} else {
if policyNamespace != "" {
return fmt.Errorf("path: spec.rules[%v]: a namespaced policy cannot generate cluster-wide resources", rule.Name)
}
}
}
}
}
}
}
}
}
return nil
}
// jsonPatchOnPod checks if a rule applies JSON patches to Pod
func jsonPatchOnPod(rule kyvernov1.Rule) bool {
if !rule.HasMutate() {
return false
}
if slices.Contains(rule.MatchResources.Kinds, "Pod") && rule.Mutation.PatchesJSON6902 != "" {
return true
}
return false
}
func podControllerAutoGenExclusion(policy kyvernov1.PolicyInterface) bool {
annotations := policy.GetAnnotations()
val, ok := annotations[kyvernov1.PodControllersAnnotation]
if !ok || val == "none" {
return false
}
reorderVal := strings.Split(strings.ToLower(val), ",")
sort.Slice(reorderVal, func(i, j int) bool { return reorderVal[i] < reorderVal[j] })
if ok && !reflect.DeepEqual(reorderVal, []string{"cronjob", "daemonset", "deployment", "job", "statefulset"}) {
return true
}
return false
}
// validateWildcard check for an Match/Exclude block contains "*"
func validateWildcard(kinds []string, spec *kyvernov1.Spec, rule kyvernov1.Rule) error {
if slices.Contains(kinds, "*") && spec.BackgroundProcessingEnabled() {
return fmt.Errorf("wildcard policy not allowed in background mode. Set spec.background=false to disable background mode for this policy rule ")
}
if slices.Contains(kinds, "*") && len(kinds) > 1 {
return fmt.Errorf("wildard policy can not deal more than one kind")
}
if slices.Contains(kinds, "*") {
if rule.HasGenerate() || rule.HasVerifyImages() || rule.Validation.ForEachValidation != nil {
return fmt.Errorf("wildcard policy does not support rule type")
}
if rule.HasValidate() {
if rule.Validation.GetPattern() != nil || rule.Validation.GetAnyPattern() != nil {
if !ruleOnlyDealsWithResourceMetaData(rule) {
return fmt.Errorf("policy can only deal with the metadata field of the resource if" +
" the rule does not match any kind")
}
}
if rule.Validation.Deny != nil {
kyvernoConditions, _ := apiutils.ApiextensionsJsonToKyvernoConditions(rule.Validation.Deny.GetAnyAllConditions())
switch typedConditions := kyvernoConditions.(type) {
case []kyvernov1.Condition: // backwards compatibility
for _, condition := range typedConditions {
key := condition.GetKey()
if !strings.Contains(key.(string), "request.object.metadata.") && (!wildCardAllowedVariables.MatchString(key.(string)) || strings.Contains(key.(string), "request.object.spec")) {
return fmt.Errorf("policy can only deal with the metadata field of the resource if" +
" the rule does not match any kind")
}
}
}
}
}
if rule.HasMutate() {
if !ruleOnlyDealsWithResourceMetaData(rule) {
return fmt.Errorf("policy can only deal with the metadata field of the resource if" +
" the rule does not match any kind")
}
}
}
return nil
}
// validateKinds verifies if an API resource that matches 'kind' is valid kind
// and found in the cache, returns error if not found. It also returns an error if background scanning
// is enabled for a subresource.
func validateKinds(kinds []string, mock, backgroundScanningEnabled, isValidationPolicy bool, client dclient.Interface) error {
for _, k := range kinds {
if !mock {
group, version, kind, subresource := kubeutils.ParseKindSelector(k)
gvrs, err := client.Discovery().FindResources(group, version, kind, subresource)
if err != nil {
return fmt.Errorf("unable to convert GVK to GVR for kinds %s, err: %s", k, err)
}
if len(gvrs) == 0 {
return fmt.Errorf("unable to convert GVK to GVR for kinds %s", k)
}
if backgroundScanningEnabled {
for _, gvr := range gvrs {
if strings.Contains(gvr.Resource, "/") {
return fmt.Errorf("background scan enabled with subresource %s", subresource)
}
}
}
}
}
return nil
}
func validateWildcardsWithNamespaces(enforce, audit, enforceW, auditW []string) error {
pat, ns, notOk := wildcard.MatchPatterns(auditW, enforce...)
if notOk {
return fmt.Errorf("wildcard pattern '%s' matches with namespace '%s'", pat, ns)
}
pat, ns, notOk = wildcard.MatchPatterns(enforceW, audit...)
if notOk {
return fmt.Errorf("wildcard pattern '%s' matches with namespace '%s'", pat, ns)
}
pat1, pat2, notOk := wildcard.MatchPatterns(auditW, enforceW...)
if notOk {
return fmt.Errorf("wildcard pattern '%s' conflicts with the pattern '%s'", pat1, pat2)
}
pat1, pat2, notOk = wildcard.MatchPatterns(enforceW, auditW...)
if notOk {
return fmt.Errorf("wildcard pattern '%s' conflicts with the pattern '%s'", pat1, pat2)
}
return nil
}
func validateNamespaces(s *kyvernov1.Spec, path *field.Path) error {
action := map[string]sets.Set[string]{
"enforce": sets.New[string](),
"audit": sets.New[string](),
"enforceW": sets.New[string](),
"auditW": sets.New[string](),
}
for i, vfa := range s.ValidationFailureActionOverrides {
patternList, nsList := wildcard.SeperateWildcards(vfa.Namespaces)
if vfa.Action.Audit() {
if action["enforce"].HasAny(nsList...) {
return fmt.Errorf("conflicting namespaces found in path: %s: %s", path.Index(i).Child("namespaces").String(),
strings.Join(sets.List(action["enforce"].Intersection(sets.New(nsList...))), ", "))
}
action["auditW"].Insert(patternList...)
} else if vfa.Action.Enforce() {
if action["audit"].HasAny(nsList...) {
return fmt.Errorf("conflicting namespaces found in path: %s: %s", path.Index(i).Child("namespaces").String(),
strings.Join(sets.List(action["audit"].Intersection(sets.New(nsList...))), ", "))
}
action["enforceW"].Insert(patternList...)
}
action[strings.ToLower(string(vfa.Action))].Insert(nsList...)
err := validateWildcardsWithNamespaces(
sets.List(action["enforce"]),
sets.List(action["audit"]),
sets.List(action["enforceW"]),
sets.List(action["auditW"]),
)
if err != nil {
return fmt.Errorf("path: %s: %s", path.Index(i).Child("namespaces").String(), err.Error())
}
}
return nil
}
func checkForScaleSubresource(ruleTypeJson []byte, allKinds []string, warnings *[]string) {
if strings.Contains(string(ruleTypeJson), "replicas") {
for _, kind := range allKinds {
if strings.Contains(strings.ToLower(kind), "scale") {
return
}
}
msg := "You are matching on replicas but not including the scale subresource in the policy."
*warnings = append(*warnings, msg)
}
}
func checkForStatusSubresource(ruleTypeJson []byte, allKinds []string, warnings *[]string) {
if strings.Contains(string(ruleTypeJson), "status") {
for _, kind := range allKinds {
if strings.Contains(strings.ToLower(kind), "status") {
return
}
}
msg := "You are matching on status but not including the status subresource in the policy."
*warnings = append(*warnings, msg)
}
}
func immutableGenerateFields(new, old kyvernov1.PolicyInterface) error {
if new == nil || old == nil {
return nil
}
if !new.GetSpec().HasGenerate() {
return nil
}
oldRuleHashes, err := buildHashes(old.GetSpec().Rules)
if err != nil {
return err
}
newRuleHashes, err := buildHashes(new.GetSpec().Rules)
if err != nil {
return err
}
switch len(old.GetSpec().Rules) <= len(new.GetSpec().Rules) {
case true:
if newRuleHashes.IsSuperset(oldRuleHashes) {
return nil
} else {
return errors.New("change of immutable fields for a generate rule is disallowed")
}
case false:
if oldRuleHashes.IsSuperset(newRuleHashes) {
return nil
} else {
return errors.New("rule deletion - change of immutable fields for a generate rule is disallowed")
}
}
return nil
}
func resetMutableFields(rule kyvernov1.Rule) *kyvernov1.Rule {
new := new(kyvernov1.Rule)
rule.DeepCopyInto(new)
new.Generation.Synchronize = true
new.Generation.SetData(nil)
return new
}
func buildHashes(rules []kyvernov1.Rule) (sets.Set[string], error) {
ruleHashes := sets.New[string]()
for _, rule := range rules {
r := resetMutableFields(rule)
data, err := json.Marshal(r)
if err != nil {
return ruleHashes, fmt.Errorf("failed to create hash from the generate rule %v", err)
}
hash := md5.Sum(data) //nolint:gosec
ruleHashes.Insert(hex.EncodeToString(hash[:]))
}
return ruleHashes, nil
}