diff --git a/pkg/common/common.go b/pkg/common/common.go index 3a73bdb731..b46f35a4de 100644 --- a/pkg/common/common.go +++ b/pkg/common/common.go @@ -2,6 +2,7 @@ package common import ( "encoding/json" + "fmt" "strings" "github.com/go-logr/logr" @@ -77,3 +78,33 @@ func GetKindFromGVK(str string) (apiVersion string, kind string) { } return splitString[0] + "/" + splitString[1], splitString[2] } + +func VariableToJSON(key, value string) []byte { + var subString string + splitBySlash := strings.Split(key, "\"") + if len(splitBySlash) > 1 { + subString = splitBySlash[1] + } + + startString := "" + endString := "" + lenOfVariableString := 0 + addedSlashString := false + for _, k := range strings.Split(splitBySlash[0], ".") { + if k != "" { + startString += fmt.Sprintf(`{"%s":`, k) + endString += `}` + lenOfVariableString = lenOfVariableString + len(k) + 1 + if lenOfVariableString >= len(splitBySlash[0]) && len(splitBySlash) > 1 && !addedSlashString { + startString += fmt.Sprintf(`{"%s":`, subString) + endString += `}` + addedSlashString = true + } + } + } + + midString := fmt.Sprintf(`"%s"`, value) + finalString := startString + midString + endString + var jsonData = []byte(finalString) + return jsonData +} diff --git a/pkg/engine/generation.go b/pkg/engine/generation.go index ee9fc72c06..8d29ce6fb3 100644 --- a/pkg/engine/generation.go +++ b/pkg/engine/generation.go @@ -87,7 +87,7 @@ func filterRule(rule kyverno.Rule, policyContext *PolicyContext) *response.RuleR policyContext.JSONContext.Checkpoint() defer policyContext.JSONContext.Restore() - if err := LoadContext(logger, rule.Context, resCache, policyContext); err != nil { + if err := LoadContext(logger, rule.Context, resCache, policyContext, rule.Name); err != nil { logger.V(4).Info("cannot add external data to the context", "reason", err.Error()) return nil } diff --git a/pkg/engine/jsonContext.go b/pkg/engine/jsonContext.go index 7c5c8b08a1..5429cd88aa 100644 --- a/pkg/engine/jsonContext.go +++ b/pkg/engine/jsonContext.go @@ -9,40 +9,55 @@ import ( "github.com/go-logr/logr" kyverno "github.com/kyverno/kyverno/pkg/api/kyverno/v1" + pkgcommon "github.com/kyverno/kyverno/pkg/common" "github.com/kyverno/kyverno/pkg/engine/context" jmespath "github.com/kyverno/kyverno/pkg/engine/jmespath" "github.com/kyverno/kyverno/pkg/engine/variables" + "github.com/kyverno/kyverno/pkg/kyverno/store" "github.com/kyverno/kyverno/pkg/resourcecache" "k8s.io/client-go/dynamic/dynamiclister" ) // LoadContext - Fetches and adds external data to the Context. -func LoadContext(logger logr.Logger, contextEntries []kyverno.ContextEntry, resCache resourcecache.ResourceCache, ctx *PolicyContext) error { +func LoadContext(logger logr.Logger, contextEntries []kyverno.ContextEntry, resCache resourcecache.ResourceCache, ctx *PolicyContext, ruleName string) error { if len(contextEntries) == 0 { return nil } - // get GVR Cache for "configmaps" - // can get cache for other resources if the informers are enabled in resource cache - gvrC, ok := resCache.GetGVRCache("ConfigMap") - if !ok { - return errors.New("configmaps GVR Cache not found") - } + policyName := ctx.Policy.Name + if store.GetMock() { + rule := store.GetPolicyRuleFromContext(policyName, ruleName) + variables := rule.Values - lister := gvrC.Lister() - - for _, entry := range contextEntries { - if entry.ConfigMap != nil { - if err := loadConfigMap(logger, entry, lister, ctx.JSONContext); err != nil { - return err - } - } else if entry.APICall != nil { - if err := loadAPIData(logger, entry, ctx); err != nil { + for key, value := range variables { + jsonData := pkgcommon.VariableToJSON(key, value) + if err := ctx.JSONContext.AddJSON(jsonData); err != nil { return err } } - } + } else { + // get GVR Cache for "configmaps" + // can get cache for other resources if the informers are enabled in resource cache + gvrC, ok := resCache.GetGVRCache("ConfigMap") + if !ok { + return errors.New("configmaps GVR Cache not found") + } + + lister := gvrC.Lister() + + for _, entry := range contextEntries { + if entry.ConfigMap != nil { + if err := loadConfigMap(logger, entry, lister, ctx.JSONContext); err != nil { + return err + } + } else if entry.APICall != nil { + if err := loadAPIData(logger, entry, ctx); err != nil { + return err + } + } + } + } return nil } diff --git a/pkg/engine/mutation.go b/pkg/engine/mutation.go index 6b44cf130e..1bdbdb531e 100644 --- a/pkg/engine/mutation.go +++ b/pkg/engine/mutation.go @@ -72,7 +72,7 @@ func Mutate(policyContext *PolicyContext) (resp *response.EngineResponse) { logger.V(3).Info("matched mutate rule") policyContext.JSONContext.Restore() - if err := LoadContext(logger, rule.Context, resCache, policyContext); err != nil { + if err := LoadContext(logger, rule.Context, resCache, policyContext, rule.Name); err != nil { logger.Error(err, "failed to load context") continue } diff --git a/pkg/engine/mutation_test.go b/pkg/engine/mutation_test.go index 6d7f5bdb66..92a5d42e25 100644 --- a/pkg/engine/mutation_test.go +++ b/pkg/engine/mutation_test.go @@ -8,6 +8,7 @@ import ( kyverno "github.com/kyverno/kyverno/pkg/api/kyverno/v1" "github.com/kyverno/kyverno/pkg/engine/context" "github.com/kyverno/kyverno/pkg/engine/utils" + "github.com/kyverno/kyverno/pkg/kyverno/store" "gotest.tools/assert" ) @@ -164,3 +165,104 @@ func Test_variableSubstitutionPathNotExist(t *testing.T) { t.Log(er.PolicyResponse.Rules[0].Message) assert.Equal(t, er.PolicyResponse.Rules[0].Message, expectedErrorStr) } + +func Test_variableSubstitutionCLI(t *testing.T) { + resourceRaw := []byte(`{ + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "name": "nginx-config-test" + }, + "spec": { + "containers": [ + { + "image": "nginx:latest", + "name": "test-nginx" + } + ] + } + }`) + + policyraw := []byte(`{ + "apiVersion": "kyverno.io/v1", + "kind": "ClusterPolicy", + "metadata": { + "name": "cm-variable-example" + }, + "spec": { + "rules": [ + { + "name": "example-configmap-lookup", + "context": [ + { + "name": "dictionary", + "configMap": { + "name": "mycmap", + "namespace": "default" + } + } + ], + "match": { + "resources": { + "kinds": [ + "Pod" + ] + } + }, + "mutate": { + "patchStrategicMerge": { + "metadata": { + "labels": { + "my-environment-name": "{{dictionary.data.env}}" + } + } + } + } + } + ] + } + }`) + + configMapVariableContext := store.Context{ + Policies: []store.Policy{ + { + Name: "cm-variable-example", + Rules: []store.Rule{ + { + Name: "example-configmap-lookup", + Values: map[string]string{ + "dictionary.data.env": "dev1", + }, + }, + }, + }, + }, + } + + expectedPatch := []byte(`{"op":"add","path":"/metadata/labels","value":{"my-environment-name":"dev1"}}`) + + store.SetContext(configMapVariableContext) + store.SetMock(true) + var policy kyverno.ClusterPolicy + err := json.Unmarshal(policyraw, &policy) + assert.NilError(t, err) + 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 := Mutate(policyContext) + t.Log(string(expectedPatch)) + t.Log(string(er.PolicyResponse.Rules[0].Patches[0])) + if !reflect.DeepEqual(expectedPatch, er.PolicyResponse.Rules[0].Patches[0]) { + t.Error("patches dont match") + } +} diff --git a/pkg/engine/validation.go b/pkg/engine/validation.go index 353d9e4e59..5fa35e4b04 100644 --- a/pkg/engine/validation.go +++ b/pkg/engine/validation.go @@ -97,7 +97,7 @@ func validateResource(log logr.Logger, ctx *PolicyContext) *response.EngineRespo } ctx.JSONContext.Restore() - if err := LoadContext(log, rule.Context, ctx.ResourceCache, ctx); err != nil { + if err := LoadContext(log, rule.Context, ctx.ResourceCache, ctx, rule.Name); err != nil { log.Error(err, "failed to load context") continue } diff --git a/pkg/engine/validation_test.go b/pkg/engine/validation_test.go index 9a0b34b0ab..0af0d06482 100644 --- a/pkg/engine/validation_test.go +++ b/pkg/engine/validation_test.go @@ -7,6 +7,7 @@ import ( kyverno "github.com/kyverno/kyverno/pkg/api/kyverno/v1" "github.com/kyverno/kyverno/pkg/engine/context" "github.com/kyverno/kyverno/pkg/engine/utils" + "github.com/kyverno/kyverno/pkg/kyverno/store" utils2 "github.com/kyverno/kyverno/pkg/utils" "gotest.tools/assert" "k8s.io/api/admission/v1beta1" @@ -2141,3 +2142,103 @@ func executeTest(t *testing.T, err error, test testCase) { t.Errorf("Testcase has failed, policy: %v", policy.Name) } } + +func TestValidate_context_variable_substitution_CLI(t *testing.T) { + rawPolicy := []byte(`{ + "apiVersion": "kyverno.io/v1", + "kind": "ClusterPolicy", + "metadata": { + "name": "restrict-pod-count" + }, + "spec": { + "validationFailureAction": "enforce", + "background": false, + "rules": [ + { + "name": "restrict-pod-count", + "match": { + "resources": { + "kinds": [ + "Pod" + ] + } + }, + "context": [ + { + "name": "podcounts", + "apiCall": { + "urlPath": "/api/v1/pods", + "jmesPath": "items[?spec.nodeName=='minikube'] | length(@)" + } + } + ], + "validate": { + "message": "restrict pod counts to be no more than 10 on node minikube", + "deny": { + "conditions": [ + { + "key": "{{ podcounts }}", + "operator": "GreaterThanOrEquals", + "value": 10 + } + ] + } + } + } + ] + } + } + `) + + rawResource := []byte(` + { + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "name": "nginx-config-test" + }, + "spec": { + "containers": [ + { + "image": "nginx:latest", + "name": "test-nginx" + } + ] + } + } + `) + + configMapVariableContext := store.Context{ + Policies: []store.Policy{ + { + Name: "restrict-pod-count", + Rules: []store.Rule{ + { + Name: "restrict-pod-count", + Values: map[string]string{ + "podcounts": "12", + }, + }, + }, + }, + }, + } + + store.SetContext(configMapVariableContext) + store.SetMock(true) + + var policy kyverno.ClusterPolicy + err := json.Unmarshal(rawPolicy, &policy) + assert.NilError(t, err) + + resourceUnstructured, err := utils.ConvertToUnstructured(rawResource) + assert.NilError(t, err) + msgs := []string{ + "restrict pod counts to be no more than 10 on node minikube", + } + 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()) +} diff --git a/pkg/generate/generate.go b/pkg/generate/generate.go index aaa8db345b..44f5a57c05 100644 --- a/pkg/generate/generate.go +++ b/pkg/generate/generate.go @@ -221,7 +221,7 @@ func (c *Controller) applyGeneratePolicy(log logr.Logger, policyContext *engine. } // add configmap json data to context - if err := engine.LoadContext(log, rule.Context, resCache, policyContext); err != nil { + if err := engine.LoadContext(log, rule.Context, resCache, policyContext, rule.Name); err != nil { log.Error(err, "cannot add configmaps to context") return nil, err } diff --git a/pkg/kyverno/apply/command.go b/pkg/kyverno/apply/command.go index ecd8033a71..5f9358d5a3 100644 --- a/pkg/kyverno/apply/command.go +++ b/pkg/kyverno/apply/command.go @@ -15,6 +15,7 @@ import ( "github.com/kyverno/kyverno/pkg/engine/response" "github.com/kyverno/kyverno/pkg/kyverno/common" sanitizederror "github.com/kyverno/kyverno/pkg/kyverno/sanitizedError" + "github.com/kyverno/kyverno/pkg/kyverno/store" "github.com/kyverno/kyverno/pkg/openapi" policy2 "github.com/kyverno/kyverno/pkg/policy" "github.com/spf13/cobra" @@ -148,6 +149,7 @@ func Command() *cobra.Command { func applyCommandHelper(resourcePaths []string, cluster bool, policyReport bool, mutateLogPath string, variablesString string, valuesFile string, namespace string, policyPaths []string, stdin bool) (validateEngineResponses []*response.EngineResponse, rc *resultCounts, resources []*unstructured.Unstructured, skippedPolicies []SkippedPolicy, err error) { + store.SetMock(true) kubernetesConfig := genericclioptions.NewConfigFlags(true) fs := memfs.New() @@ -286,7 +288,7 @@ func applyCommandHelper(resourcePaths []string, cluster bool, policyReport bool, thisPolicyResourceValues[k] = v } - if len(common.PolicyHasVariables(*policy)) > 0 && len(thisPolicyResourceValues) == 0 { + if len(common.PolicyHasVariables(*policy)) > 0 && len(thisPolicyResourceValues) == 0 && len(store.GetContext().Policies) == 0 { return validateEngineResponses, rc, resources, skippedPolicies, sanitizederror.NewWithError(fmt.Sprintf("policy %s have variables. pass the values for the variables using set/values_file flag", policy.Name), err) } diff --git a/pkg/kyverno/common/common.go b/pkg/kyverno/common/common.go index cd7b6f5fe4..62f09775bc 100644 --- a/pkg/kyverno/common/common.go +++ b/pkg/kyverno/common/common.go @@ -16,11 +16,13 @@ import ( "github.com/go-git/go-billy/v5" "github.com/go-logr/logr" v1 "github.com/kyverno/kyverno/pkg/api/kyverno/v1" + pkgcommon "github.com/kyverno/kyverno/pkg/common" client "github.com/kyverno/kyverno/pkg/dclient" "github.com/kyverno/kyverno/pkg/engine" "github.com/kyverno/kyverno/pkg/engine/context" "github.com/kyverno/kyverno/pkg/engine/response" sanitizederror "github.com/kyverno/kyverno/pkg/kyverno/sanitizedError" + "github.com/kyverno/kyverno/pkg/kyverno/store" "github.com/kyverno/kyverno/pkg/policymutation" "github.com/kyverno/kyverno/pkg/utils" ut "github.com/kyverno/kyverno/pkg/utils" @@ -32,15 +34,15 @@ import ( ) // GetPolicies - Extracting the policies from multiple YAML - -type Resource struct { - Name string `json:"name"` - Values map[string]string `json:"values"` -} - type Policy struct { Name string `json:"name"` Resources []Resource `json:"resources"` + Rules []Rule `json:"rules"` +} + +type Rule struct { + Name string `json:"name"` + Values map[string]string `json:"values"` } type Values struct { @@ -48,6 +50,11 @@ type Values struct { NamespaceSelectors []NamespaceSelector `json:"namespaceSelector"` } +type Resource struct { + Name string `json:"name"` + Values map[string]string `json:"values"` +} + type NamespaceSelector struct { Name string `json:"name"` Labels map[string]string `json:"labels"` @@ -305,9 +312,9 @@ func RemoveDuplicateVariables(matches [][]string) string { return variableStr } -// GetVariable - get the variables from console/file func GetVariable(variablesString, valuesFile string, fs billy.Filesystem, isGit bool, policyresoucePath string) (map[string]string, map[string]map[string]Resource, map[string]map[string]string, error) { - valuesMap := make(map[string]map[string]Resource) + valuesMapResource := make(map[string]map[string]Resource) + valuesMapRule := make(map[string]map[string]Rule) namespaceSelectorMap := make(map[string]map[string]string) variables := make(map[string]string) var yamlFile []byte @@ -331,25 +338,33 @@ func GetVariable(variablesString, valuesFile string, fs billy.Filesystem, isGit } if err != nil { - return variables, valuesMap, namespaceSelectorMap, sanitizederror.NewWithError("unable to read yaml", err) + return variables, valuesMapResource, namespaceSelectorMap, sanitizederror.NewWithError("unable to read yaml", err) } valuesBytes, err := yaml.ToJSON(yamlFile) if err != nil { - return variables, valuesMap, namespaceSelectorMap, sanitizederror.NewWithError("failed to convert json", err) + return variables, valuesMapResource, namespaceSelectorMap, sanitizederror.NewWithError("failed to convert json", err) } values := &Values{} if err := json.Unmarshal(valuesBytes, values); err != nil { - return variables, valuesMap, namespaceSelectorMap, sanitizederror.NewWithError("failed to decode yaml", err) + return variables, valuesMapResource, namespaceSelectorMap, sanitizederror.NewWithError("failed to decode yaml", err) } for _, p := range values.Policies { - pmap := make(map[string]Resource) + resourceMap := make(map[string]Resource) for _, r := range p.Resources { - pmap[r.Name] = r + resourceMap[r.Name] = r + } + valuesMapResource[p.Name] = resourceMap + + if p.Rules != nil { + ruleMap := make(map[string]Rule) + for _, r := range p.Rules { + ruleMap[r.Name] = r + } + valuesMapRule[p.Name] = ruleMap } - valuesMap[p.Name] = pmap } for _, n := range values.NamespaceSelectors { @@ -357,7 +372,26 @@ func GetVariable(variablesString, valuesFile string, fs billy.Filesystem, isGit } } - return variables, valuesMap, namespaceSelectorMap, nil + storePolices := make([]store.Policy, 0) + for policyName, ruleMap := range valuesMapRule { + storeRules := make([]store.Rule, 0) + for _, rule := range ruleMap { + storeRules = append(storeRules, store.Rule{ + Name: rule.Name, + Values: rule.Values, + }) + } + storePolices = append(storePolices, store.Policy{ + Name: policyName, + Rules: storeRules, + }) + } + + store.SetContext(store.Context{ + Policies: storePolices, + }) + + return variables, valuesMapResource, namespaceSelectorMap, nil } // MutatePolices - function to apply mutation on policies @@ -409,32 +443,7 @@ func ApplyPolicyOnResource(policy *v1.ClusterPolicy, resource *unstructured.Unst ctx := context.NewContext() for key, value := range variables { - var subString string - splitBySlash := strings.Split(key, "\"") - if len(splitBySlash) > 1 { - subString = splitBySlash[1] - } - - startString := "" - endString := "" - lenOfVariableString := 0 - addedSlashString := false - for _, k := range strings.Split(splitBySlash[0], ".") { - if k != "" { - startString += fmt.Sprintf(`{"%s":`, k) - endString += `}` - lenOfVariableString = lenOfVariableString + len(k) + 1 - if lenOfVariableString >= len(splitBySlash[0]) && len(splitBySlash) > 1 && addedSlashString == false { - startString += fmt.Sprintf(`{"%s":`, subString) - endString += `}` - addedSlashString = true - } - } - } - - midString := fmt.Sprintf(`"%s"`, value) - finalString := startString + midString + endString - var jsonData = []byte(finalString) + jsonData := pkgcommon.VariableToJSON(key, value) ctx.AddJSON(jsonData) } diff --git a/pkg/kyverno/store/store.go b/pkg/kyverno/store/store.go new file mode 100644 index 0000000000..878585f81e --- /dev/null +++ b/pkg/kyverno/store/store.go @@ -0,0 +1,56 @@ +package store + +var Mock bool +var ContextVar Context + +func SetMock(mock bool) { + Mock = mock +} + +func GetMock() bool { + return Mock +} + +func SetContext(context Context) { + ContextVar = context +} + +func GetContext() Context { + return ContextVar +} + +func GetPolicyFromContext(policyName string) *Policy { + for _, policy := range ContextVar.Policies { + if policy.Name == policyName { + return &policy + } + } + return nil +} + +func GetPolicyRuleFromContext(policyName string, ruleName string) *Rule { + for _, policy := range ContextVar.Policies { + if policy.Name == policyName { + for _, rule := range policy.Rules { + if rule.Name == ruleName { + return &rule + } + } + } + } + return nil +} + +type Context struct { + Policies []Policy `json:"policies"` +} + +type Policy struct { + Name string `json:"name"` + Rules []Rule `json:"rules"` +} + +type Rule struct { + Name string `json:"name"` + Values map[string]string `json:"values"` +}