diff --git a/charts/kyverno/crds/crds.yaml b/charts/kyverno/crds/crds.yaml index f5bf6f3418..fecda32cfc 100644 --- a/charts/kyverno/crds/crds.yaml +++ b/charts/kyverno/crds/crds.yaml @@ -50,17 +50,34 @@ spec: context: description: Context defines variables and data sources that can be used during rule execution. items: - description: ContextEntry adds variables and data sources to a rule Context + description: ContextEntry adds variables and data sources to a rule Context. Either a ConfigMap reference or a APILookup must be provided. properties: + apiCall: + description: APICall is an API server request to retrieve data + properties: + jmesPath: + description: JMESPath is an optional JSON Match Expression that can be used to transform the JSON response from the API server. + type: string + urlPath: + description: URLPath is the URL path to be used in the HTTP GET request + type: string + required: + - urlPath + type: object configMap: - description: ConfigMapReference refers to a ConfigMap + description: ConfigMap is the ConfigMap reference. properties: name: + description: Name is the ConfigMap name. type: string namespace: + description: Namespace is the ConfigMap namespace. type: string + required: + - name type: object name: + description: Name is the variable name. type: string type: object type: array @@ -1152,17 +1169,34 @@ spec: context: description: Context defines variables and data sources that can be used during rule execution. items: - description: ContextEntry adds variables and data sources to a rule Context + description: ContextEntry adds variables and data sources to a rule Context. Either a ConfigMap reference or a APILookup must be provided. properties: + apiCall: + description: APICall is an API server request to retrieve data + properties: + jmesPath: + description: JMESPath is an optional JSON Match Expression that can be used to transform the JSON response from the API server. + type: string + urlPath: + description: URLPath is the URL path to be used in the HTTP GET request + type: string + required: + - urlPath + type: object configMap: - description: ConfigMapReference refers to a ConfigMap + description: ConfigMap is the ConfigMap reference. properties: name: + description: Name is the ConfigMap name. type: string namespace: + description: Namespace is the ConfigMap namespace. type: string + required: + - name type: object name: + description: Name is the variable name. type: string type: object type: array diff --git a/cmd/kyverno/main.go b/cmd/kyverno/main.go index 0826e587b9..d39a8c2c08 100755 --- a/cmd/kyverno/main.go +++ b/cmd/kyverno/main.go @@ -277,6 +277,7 @@ func main() { log.Log.WithName("ValidateAuditHandler"), configData, rCache, + client, ) // Configure certificates diff --git a/definitions/crds/kyverno.io_clusterpolicies.yaml b/definitions/crds/kyverno.io_clusterpolicies.yaml index 853ad21d58..1ca9779944 100644 --- a/definitions/crds/kyverno.io_clusterpolicies.yaml +++ b/definitions/crds/kyverno.io_clusterpolicies.yaml @@ -67,17 +67,39 @@ spec: can be used during rule execution. items: description: ContextEntry adds variables and data sources - to a rule Context + to a rule Context. Either a ConfigMap reference or a APILookup + must be provided. properties: + apiCall: + description: APICall is an API server request to retrieve + data + properties: + jmesPath: + description: JMESPath is an optional JSON Match Expression + that can be used to transform the JSON response + from the API server. + type: string + urlPath: + description: URLPath is the URL path to be used in + the HTTP GET request + type: string + required: + - urlPath + type: object configMap: - description: ConfigMapReference refers to a ConfigMap + description: ConfigMap is the ConfigMap reference. properties: name: + description: Name is the ConfigMap name. type: string namespace: + description: Namespace is the ConfigMap namespace. type: string + required: + - name type: object name: + description: Name is the variable name. type: string type: object type: array diff --git a/definitions/crds/kyverno.io_policies.yaml b/definitions/crds/kyverno.io_policies.yaml index ab0948973b..cb15f6b3e1 100644 --- a/definitions/crds/kyverno.io_policies.yaml +++ b/definitions/crds/kyverno.io_policies.yaml @@ -68,17 +68,39 @@ spec: can be used during rule execution. items: description: ContextEntry adds variables and data sources - to a rule Context + to a rule Context. Either a ConfigMap reference or a APILookup + must be provided. properties: + apiCall: + description: APICall is an API server request to retrieve + data + properties: + jmesPath: + description: JMESPath is an optional JSON Match Expression + that can be used to transform the JSON response + from the API server. + type: string + urlPath: + description: URLPath is the URL path to be used in + the HTTP GET request + type: string + required: + - urlPath + type: object configMap: - description: ConfigMapReference refers to a ConfigMap + description: ConfigMap is the ConfigMap reference. properties: name: + description: Name is the ConfigMap name. type: string namespace: + description: Namespace is the ConfigMap namespace. type: string + required: + - name type: object name: + description: Name is the variable name. type: string type: object type: array diff --git a/definitions/install.yaml b/definitions/install.yaml index 0633bcc706..7989ac4626 100644 --- a/definitions/install.yaml +++ b/definitions/install.yaml @@ -55,17 +55,34 @@ spec: context: description: Context defines variables and data sources that can be used during rule execution. items: - description: ContextEntry adds variables and data sources to a rule Context + description: ContextEntry adds variables and data sources to a rule Context. Either a ConfigMap reference or a APILookup must be provided. properties: + apiCall: + description: APICall is an API server request to retrieve data + properties: + jmesPath: + description: JMESPath is an optional JSON Match Expression that can be used to transform the JSON response from the API server. + type: string + urlPath: + description: URLPath is the URL path to be used in the HTTP GET request + type: string + required: + - urlPath + type: object configMap: - description: ConfigMapReference refers to a ConfigMap + description: ConfigMap is the ConfigMap reference. properties: name: + description: Name is the ConfigMap name. type: string namespace: + description: Namespace is the ConfigMap namespace. type: string + required: + - name type: object name: + description: Name is the variable name. type: string type: object type: array @@ -1157,17 +1174,34 @@ spec: context: description: Context defines variables and data sources that can be used during rule execution. items: - description: ContextEntry adds variables and data sources to a rule Context + description: ContextEntry adds variables and data sources to a rule Context. Either a ConfigMap reference or a APILookup must be provided. properties: + apiCall: + description: APICall is an API server request to retrieve data + properties: + jmesPath: + description: JMESPath is an optional JSON Match Expression that can be used to transform the JSON response from the API server. + type: string + urlPath: + description: URLPath is the URL path to be used in the HTTP GET request + type: string + required: + - urlPath + type: object configMap: - description: ConfigMapReference refers to a ConfigMap + description: ConfigMap is the ConfigMap reference. properties: name: + description: Name is the ConfigMap name. type: string namespace: + description: Namespace is the ConfigMap namespace. type: string + required: + - name type: object name: + description: Name is the variable name. type: string type: object type: array diff --git a/definitions/install_debug.yaml b/definitions/install_debug.yaml index bebf52231f..2dc7da84ff 100755 --- a/definitions/install_debug.yaml +++ b/definitions/install_debug.yaml @@ -55,17 +55,34 @@ spec: context: description: Context defines variables and data sources that can be used during rule execution. items: - description: ContextEntry adds variables and data sources to a rule Context + description: ContextEntry adds variables and data sources to a rule Context. Either a ConfigMap reference or a APILookup must be provided. properties: + apiCall: + description: APICall is an API server request to retrieve data + properties: + jmesPath: + description: JMESPath is an optional JSON Match Expression that can be used to transform the JSON response from the API server. + type: string + urlPath: + description: URLPath is the URL path to be used in the HTTP GET request + type: string + required: + - urlPath + type: object configMap: - description: ConfigMapReference refers to a ConfigMap + description: ConfigMap is the ConfigMap reference. properties: name: + description: Name is the ConfigMap name. type: string namespace: + description: Namespace is the ConfigMap namespace. type: string + required: + - name type: object name: + description: Name is the variable name. type: string type: object type: array @@ -1157,17 +1174,34 @@ spec: context: description: Context defines variables and data sources that can be used during rule execution. items: - description: ContextEntry adds variables and data sources to a rule Context + description: ContextEntry adds variables and data sources to a rule Context. Either a ConfigMap reference or a APILookup must be provided. properties: + apiCall: + description: APICall is an API server request to retrieve data + properties: + jmesPath: + description: JMESPath is an optional JSON Match Expression that can be used to transform the JSON response from the API server. + type: string + urlPath: + description: URLPath is the URL path to be used in the HTTP GET request + type: string + required: + - urlPath + type: object configMap: - description: ConfigMapReference refers to a ConfigMap + description: ConfigMap is the ConfigMap reference. properties: name: + description: Name is the ConfigMap name. type: string namespace: + description: Namespace is the ConfigMap namespace. type: string + required: + - name type: object name: + description: Name is the variable name. type: string type: object type: array diff --git a/pkg/api/kyverno/v1/policy_types.go b/pkg/api/kyverno/v1/policy_types.go index 1b3bf05928..78308cab28 100755 --- a/pkg/api/kyverno/v1/policy_types.go +++ b/pkg/api/kyverno/v1/policy_types.go @@ -98,18 +98,43 @@ type Rule struct { Generation Generation `json:"generate,omitempty" yaml:"generate,omitempty"` } -// ContextEntry adds variables and data sources to a rule Context +// ContextEntry adds variables and data sources to a rule Context. Either a +// ConfigMap reference or a APILookup must be provided. type ContextEntry struct { - Name string `json:"name,omitempty" yaml:"name,omitempty"` + + // Name is the variable name. + Name string `json:"name,omitempty" yaml:"name,omitempty"` + + // ConfigMap is the ConfigMap reference. ConfigMap *ConfigMapReference `json:"configMap,omitempty" yaml:"configMap,omitempty"` + + // APICall is an API server request to retrieve data + APICall *APICall `json:"apiCall,omitempty" yaml:"apiCall,omitempty"` } // ConfigMapReference refers to a ConfigMap type ConfigMapReference struct { - Name string `json:"name,omitempty" yaml:"name,omitempty"` + + // Name is the ConfigMap name. + Name string `json:"name" yaml:"name"` + + // Namespace is the ConfigMap namespace. Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"` } +// APICall contains an API server URL path used to perform an HTTP GET request +// and an optional JMESPath to transform the retrieved data. +type APICall struct { + + // URLPath is the URL path to be used in the HTTP GET request + URLPath string `json:"urlPath" yaml:"urlPath"` + + // JMESPath is an optional JSON Match Expression that can be used to + // transform the JSON response from the API server. + // +optional + JMESPath string `json:"jmesPath,omitempty" yaml:"jmesPath,omitempty"` +} + // Condition defines variable-based conditional criteria for rule execution. type Condition struct { // Key is the context entry (using JMESPath) for conditional rule evaluation. diff --git a/pkg/dclient/client.go b/pkg/dclient/client.go index f26b85a0cf..255b6a53ca 100644 --- a/pkg/dclient/client.go +++ b/pkg/dclient/client.go @@ -55,8 +55,13 @@ func NewClient(config *rest.Config, resync time.Duration, stopCh <-chan struct{} kclient: kclient, log: log.WithName("dclient"), } + // Set discovery client - discoveryClient := ServerPreferredResources{cachedClient: memory.NewMemCacheClient(kclient.Discovery()), log: client.log} + discoveryClient := &ServerPreferredResources{ + cachedClient: memory.NewMemCacheClient(kclient.Discovery()), + log: client.log, + } + // client will invalidate registered resources cache every x seconds, // As there is no way to identify if the registered resource is available or not // we will be invalidating the local cache, so the next request get a fresh cache @@ -121,8 +126,8 @@ func (c *Client) getGroupVersionMapper(apiVersion string, kind string) schema.Gr gvr, _ := c.DiscoveryClient.GetGVRFromKind(kind) return gvr } - return c.DiscoveryClient.GetGVRFromAPIVersionKind(apiVersion, kind) + return c.DiscoveryClient.GetGVRFromAPIVersionKind(apiVersion, kind) } // GetResource returns the resource in unstructured/json format @@ -347,15 +352,19 @@ func (c ServerPreferredResources) findResource(apiVersion string, kind string) ( } for _, serverResource := range serverResources { - if apiVersion != "" && serverResource.GroupVersion != apiVersion { continue } for _, resource := range serverResource.APIResources { + if strings.Contains(resource.Name, "/") { + // skip the sub-resources like deployment/status + continue + } - // skip the resource names with "/", to avoid comparison with subresources - if resource.Kind == kind && !strings.Contains(resource.Name, "/") { + // match kind or names (e.g. Namespace, namespaces, namespace) + // to allow matching API paths (e.g. /api/v1/namespaces). + if resource.Kind == kind || resource.Name == kind || resource.SingularName == kind { gv, err := schema.ParseGroupVersion(serverResource.GroupVersion) if err != nil { c.log.Error(err, "failed to parse groupVersion", "groupVersion", serverResource.GroupVersion) diff --git a/pkg/engine/apiPath.go b/pkg/engine/apiPath.go new file mode 100644 index 0000000000..cca6ae47c6 --- /dev/null +++ b/pkg/engine/apiPath.go @@ -0,0 +1,123 @@ +package engine + +import ( + "fmt" + "strings" +) + +type APIPath struct { + Root string + Group string + Version string + ResourceType string + Name string + Namespace string +} + +// NewAPIPath validates and parses an API path. +// See: https://kubernetes.io/docs/reference/using-api/api-concepts/ +func NewAPIPath(path string) (*APIPath, error) { + trimmedPath := strings.Trim(path, "/ ") + paths := strings.Split(trimmedPath, "/") + + if len(paths) < 3 || len(paths) > 7 { + return nil, fmt.Errorf("invalid path length %s", path) + } + + if paths[0] != "api" && paths[0] != "apis" { + return nil, fmt.Errorf("urlPath must start with /api or /apis") + } + + if paths[0] == "api" && paths[1] != "v1" { + return nil, fmt.Errorf("expected urlPath to start with /api/v1/") + } + + if paths[0] == "api" { + if len(paths) == 3 { + return &APIPath{ + Root: paths[0], + Group: paths[1], + ResourceType: paths[2], + }, nil + } + + if len(paths) == 4 { + return &APIPath{ + Root: paths[0], + Group: paths[1], + ResourceType: paths[2], + Name: paths[3], + }, nil + } + + return nil, fmt.Errorf("invalid /api/v1 path %s", path) + } + + // /apis/GROUP/VERSION/RESOURCETYPE/ + if len(paths) == 4 { + return &APIPath{ + Root: paths[0], + Group: paths[1], + Version: paths[2], + ResourceType: paths[3], + }, nil + } + + // /apis/GROUP/VERSION/RESOURCETYPE/NAME + if len(paths) == 5 { + return &APIPath{ + Root: paths[0], + Group: paths[1], + Version: paths[2], + ResourceType: paths[3], + Name: paths[4], + }, nil + } + + // /apis/GROUP/VERSION/namespaces/NAMESPACE/RESOURCETYPE + if len(paths) == 6 { + return &APIPath{ + Root: paths[0], + Group: paths[1], + Version: paths[2], + Namespace: paths[4], + ResourceType: paths[5], + }, nil + } + + // /apis/GROUP/VERSION/namespaces/NAMESPACE/RESOURCETYPE/NAME + if len(paths) == 7 { + return &APIPath{ + Root: paths[0], + Group: paths[1], + Version: paths[2], + Namespace: paths[4], + ResourceType: paths[5], + Name: paths[6], + }, nil + } + + return nil, fmt.Errorf("invalid /apis path %s", path) +} + +func (a *APIPath) String() string { + var paths []string + if a.Namespace != "" { + if a.Name == "" { + paths = []string{a.Root, a.Group, a.Version, a.ResourceType, "namespaces", a.Namespace} + } else { + paths = []string{a.Root, a.Group, a.Version, a.ResourceType, "namespaces", a.Namespace, a.Name} + } + } else { + if a.Name != "" { + paths = []string{a.Root, a.Group, a.Version, a.ResourceType, a.Name} + } else { + paths = []string{a.Root, a.Group, a.Version, a.ResourceType} + } + } + + + result := "/" + strings.Join(paths, "/") + result = strings.ReplaceAll(result, "//", "/") + return result +} diff --git a/pkg/engine/apiPath_test.go b/pkg/engine/apiPath_test.go new file mode 100644 index 0000000000..306e443c6f --- /dev/null +++ b/pkg/engine/apiPath_test.go @@ -0,0 +1,24 @@ +package engine + +import ( + "testing" +) + +func Test_Paths(t *testing.T) { + f := func(path, expected string) { + p, err := NewAPIPath(path) + if err != nil { + t.Error(err) + return + } + + if p.String() != expected { + t.Errorf("expected %s got %s", expected, p.String()) + } + } + + f("/api/v1/namespace/{{ request.namespace }}", "/api/v1/namespace/{{ request.namespace }}") + f("/api/v1/namespace/{{ request.namespace }}/", "/api/v1/namespace/{{ request.namespace }}") + f("/api/v1/namespace/{{ request.namespace }}/ ", "/api/v1/namespace/{{ request.namespace }}") + f(" /api/v1/namespace/{{ request.namespace }}", "/api/v1/namespace/{{ request.namespace }}") +} diff --git a/pkg/engine/generation.go b/pkg/engine/generation.go index 5e619d0682..5c3a52bff3 100644 --- a/pkg/engine/generation.go +++ b/pkg/engine/generation.go @@ -12,11 +12,11 @@ import ( // 1. validate variables to be substitute in the general ruleInfo (match,exclude,condition) // - the caller has to check the ruleResponse to determine whether the path exist // 2. returns the list of rules that are applicable on this policy and resource, if 1 succeed -func Generate(policyContext PolicyContext) (resp *response.EngineResponse) { +func Generate(policyContext *PolicyContext) (resp *response.EngineResponse) { return filterRules(policyContext) } -func filterRules(policyContext PolicyContext) *response.EngineResponse { +func filterRules(policyContext *PolicyContext) *response.EngineResponse { kind := policyContext.NewResource.GetKind() name := policyContext.NewResource.GetName() namespace := policyContext.NewResource.GetNamespace() @@ -46,7 +46,7 @@ func filterRules(policyContext PolicyContext) *response.EngineResponse { return resp } -func filterRule(rule kyverno.Rule, policyContext PolicyContext) *response.RuleResponse { +func filterRule(rule kyverno.Rule, policyContext *PolicyContext) *response.RuleResponse { if !rule.HasGenerate() { return nil } @@ -59,7 +59,6 @@ func filterRule(rule kyverno.Rule, policyContext PolicyContext) *response.RuleRe admissionInfo := policyContext.AdmissionInfo ctx := policyContext.JSONContext resCache := policyContext.ResourceCache - jsonContext := policyContext.JSONContext excludeGroupRole := policyContext.ExcludeGroupRole logger := log.Log.WithName("Generate").WithValues("policy", policy.Name, @@ -83,7 +82,7 @@ func filterRule(rule kyverno.Rule, policyContext PolicyContext) *response.RuleRe } // add configmap json data to context - if err := AddResourceToContext(logger, rule.Context, resCache, jsonContext); err != nil { + if err := LoadContext(logger, rule.Context, resCache, policyContext); err != nil { logger.V(4).Info("cannot add configmaps to context", "reason", err.Error()) return nil } diff --git a/pkg/engine/jsonContext.go b/pkg/engine/jsonContext.go new file mode 100644 index 0000000000..6dc929d259 --- /dev/null +++ b/pkg/engine/jsonContext.go @@ -0,0 +1,190 @@ +package engine + +import ( + "encoding/json" + "errors" + "fmt" + "github.com/go-logr/logr" + "github.com/jmespath/go-jmespath" + kyverno "github.com/kyverno/kyverno/pkg/api/kyverno/v1" + "github.com/kyverno/kyverno/pkg/engine/context" + "github.com/kyverno/kyverno/pkg/resourcecache" + "k8s.io/apimachinery/pkg/runtime" + "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 { + 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") + } + + lister := gvrC.Lister() + + for _, entry := range contextEntries { + if entry.ConfigMap != nil { + if err := loadConfigMap(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 +} + +func loadAPIData(logger logr.Logger, entry kyverno.ContextEntry, ctx *PolicyContext) error { + jsonData, err := fetchAPIData(entry, ctx) + if err != nil { + return err + } + + if entry.APICall.JMESPath == "" { + err = ctx.JSONContext.AddJSON(jsonData) + if err != nil { + return fmt.Errorf("failed to add resource data to context: contextEntry: %v, error: %v", entry, err) + } + + return nil + } + + results, err := applyJMESPath(entry.APICall.JMESPath, jsonData) + if err != nil { + return fmt.Errorf("failed to apply JMESPath for context entry %v: %v", entry, err) + } + + contextNamedData := make(map[string]interface{}) + contextNamedData[entry.Name] = results + contextData, err := json.Marshal(contextNamedData) + if err != nil { + return fmt.Errorf("failed to marshall data %v for context entry %v: %v", contextNamedData, entry, err) + } + + err = ctx.JSONContext.AddJSON(contextData) + if err != nil { + return fmt.Errorf("failed to add JMESPath (%s) results to context, error: %v", entry.APICall.JMESPath, err) + } + + logger.Info("added APICall context entry", "data", contextNamedData) + return nil +} + +func applyJMESPath(jmesPath string, jsonData []byte) (interface{}, error) { + jp, err := jmespath.Compile(jmesPath) + if err != nil { + return nil, fmt.Errorf("failed to compile JMESPath: %s, error: %v", jmesPath, err) + } + + var data interface{} + err = json.Unmarshal(jsonData, &data) + if err != nil { + return nil, fmt.Errorf("failed to unmarshall JSON: %s, error: %v", string(jsonData), err) + } + + return jp.Search(data) +} + +func fetchAPIData(entry kyverno.ContextEntry, ctx *PolicyContext) ([]byte, error) { + if entry.APICall == nil { + return nil, fmt.Errorf("missing APICall in context entry %v", entry) + } + + p, err := NewAPIPath(entry.APICall.URLPath) + if err != nil { + return nil, fmt.Errorf("failed to build API path for %v: %v", entry, err) + } + + var jsonData []byte + if p.Name != "" { + jsonData, err = loadResource(ctx, p) + if err != nil { + return nil, fmt.Errorf("failed to add resource with urlPath: %s: %v", p, err) + } + + } else { + jsonData, err = loadResourceList(ctx, p) + if err != nil { + return nil, fmt.Errorf("failed to add resource list with urlPath: %s, error: %v", p, err) + } + } + + return jsonData, nil +} + +func loadResourceList(ctx *PolicyContext, p *APIPath) ([]byte, error) { + l, err := ctx.Client.ListResource(p.Version, p.ResourceType, p.Namespace, nil) + if err != nil { + return nil, err + } + + return l.MarshalJSON() +} + +func loadResource(ctx *PolicyContext, p *APIPath) ([]byte, error) { + if ctx.Client == nil { + return nil, fmt.Errorf("API client is not available") + } + + r, err := ctx.Client.GetResource(p.Version, p.ResourceType, p.Namespace, p.Name) + if err != nil { + return nil, err + } + + return r.MarshalJSON() +} + +func loadConfigMap(entry kyverno.ContextEntry, lister dynamiclister.Lister, ctx *context.Context) error { + data, err := fetchConfigMap(entry, lister) + if err != nil { + return fmt.Errorf("failed to retrieve config map for context entry %v: %v", entry, err) + } + + err = ctx.AddJSON(data) + if err != nil { + return fmt.Errorf("failed to add config map for context entry %v: %v", entry, err) + } + + return nil +} + +func fetchConfigMap(entry kyverno.ContextEntry, lister dynamiclister.Lister) ([]byte, error) { + contextData := make(map[string]interface{}) + name := entry.ConfigMap.Name + namespace := entry.ConfigMap.Namespace + if namespace == "" { + namespace = "default" + } + + key := fmt.Sprintf("%s/%s", namespace, name) + obj, err := lister.Get(key) + if err != nil { + return nil, fmt.Errorf("failed to read configmap %s/%s from cache: %v", namespace, name, err) + } + + unstructuredObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) + if err != nil { + return nil, fmt.Errorf("failed to convert configmap %s/%s: %v", namespace, name, err) + } + + // extract configmap data + contextData["data"] = unstructuredObj["data"] + contextData["metadata"] = unstructuredObj["metadata"] + contextNamedData := make(map[string]interface{}) + contextNamedData[entry.Name] = contextData + data, err := json.Marshal(contextNamedData) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal configmap %s/%s: %v", namespace, name, err) + } + + return data, nil +} diff --git a/pkg/engine/mutation.go b/pkg/engine/mutation.go index e5cd7f3da6..cd3571fe16 100644 --- a/pkg/engine/mutation.go +++ b/pkg/engine/mutation.go @@ -30,7 +30,6 @@ func Mutate(policyContext *PolicyContext) (resp *response.EngineResponse) { ctx := policyContext.JSONContext resCache := policyContext.ResourceCache - jsonContext := policyContext.JSONContext logger := log.Log.WithName("EngineMutate").WithValues("policy", policy.Name, "kind", patchedResource.GetKind(), "namespace", patchedResource.GetNamespace(), "name", patchedResource.GetName()) @@ -68,7 +67,7 @@ func Mutate(policyContext *PolicyContext) (resp *response.EngineResponse) { logger.V(3).Info("matched mutate rule") // add configmap json data to context - if err := AddResourceToContext(logger, rule.Context, resCache, jsonContext); err != nil { + if err := LoadContext(logger, rule.Context, resCache, policyContext); err != nil { logger.V(2).Info("failed to add configmaps to context", "reason", err.Error()) continue } diff --git a/pkg/engine/utils.go b/pkg/engine/utils.go index f9df354bb2..0cfdaa2325 100644 --- a/pkg/engine/utils.go +++ b/pkg/engine/utils.go @@ -1,18 +1,14 @@ package engine import ( - "encoding/json" "errors" "fmt" "reflect" "strings" "time" - "github.com/go-logr/logr" kyverno "github.com/kyverno/kyverno/pkg/api/kyverno/v1" - "github.com/kyverno/kyverno/pkg/engine/context" "github.com/kyverno/kyverno/pkg/engine/wildcards" - "github.com/kyverno/kyverno/pkg/resourcecache" "github.com/kyverno/kyverno/pkg/utils" "github.com/minio/minio/pkg/wildcard" authenticationv1 "k8s.io/api/authentication/v1" @@ -20,7 +16,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/log" ) @@ -269,17 +264,18 @@ func MatchesResourceDescription(resourceRef unstructured.Unstructured, ruleRef k return nil } + func copyConditions(original []kyverno.Condition) []kyverno.Condition { if original == nil || len(original) == 0 { return []kyverno.Condition{} } - var copy []kyverno.Condition + var copies []kyverno.Condition for _, condition := range original { - copy = append(copy, *condition.DeepCopy()) + copies = append(copies, *condition.DeepCopy()) } - return copy + return copies } // excludeResource checks if the resource has ownerRef set @@ -310,58 +306,3 @@ func ManagedPodResource(policy kyverno.ClusterPolicy, resource unstructured.Unst return false } - -// AddResourceToContext - Add the Configmap JSON to Context. -// it will read configmaps (can be extended to get other type of resource like secrets, namespace etc) -// from the informer cache and add the configmap data to context -func AddResourceToContext(logger logr.Logger, contextEntries []kyverno.ContextEntry, resCache resourcecache.ResourceCache, ctx *context.Context) error { - if len(contextEntries) == 0 { - return nil - } - - gvrC, ok := resCache.GetGVRCache("ConfigMap") - if ok { - lister := gvrC.Lister() - for _, context := range contextEntries { - contextData := make(map[string]interface{}) - name := context.ConfigMap.Name - namespace := context.ConfigMap.Namespace - if namespace == "" { - namespace = "default" - } - - key := fmt.Sprintf("%s/%s", namespace, name) - obj, err := lister.Get(key) - if err != nil { - logger.Error(err, fmt.Sprintf("failed to read configmap %s/%s from cache", namespace, name)) - continue - } - - unstructuredObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) - if err != nil { - logger.Error(err, "failed to convert context runtime object to unstructured") - continue - } - - // extract configmap data - contextData["data"] = unstructuredObj["data"] - contextData["metadata"] = unstructuredObj["metadata"] - contextNamedData := make(map[string]interface{}) - contextNamedData[context.Name] = contextData - jdata, err := json.Marshal(contextNamedData) - if err != nil { - logger.Error(err, "failed to unmarshal context data") - continue - } - - // add data to context - err = ctx.AddJSON(jdata) - if err != nil { - logger.Error(err, "failed to load context json") - continue - } - } - return nil - } - return errors.New("configmaps GVR Cache not found") -} diff --git a/pkg/engine/validation.go b/pkg/engine/validation.go index 5d608268a7..58a42917ed 100644 --- a/pkg/engine/validation.go +++ b/pkg/engine/validation.go @@ -98,7 +98,7 @@ func validateResource(log logr.Logger, ctx *PolicyContext) *response.EngineRespo log = log.WithValues("rule", rule.Name) // add configmap json data to context - if err := AddResourceToContext(log, rule.Context, ctx.ResourceCache, ctx.JSONContext); err != nil { + if err := LoadContext(log, rule.Context, ctx.ResourceCache, ctx); err != nil { log.V(2).Info("failed to add configmaps to context", "reason", err.Error()) continue } diff --git a/pkg/generate/generate.go b/pkg/generate/generate.go index 8708d3d1bf..9dcae9f135 100644 --- a/pkg/generate/generate.go +++ b/pkg/generate/generate.go @@ -121,7 +121,7 @@ func (c *Controller) applyGenerate(resource unstructured.Unstructured, gr kyvern return nil, err } - policyContext := engine.PolicyContext{ + policyContext := &engine.PolicyContext{ NewResource: resource, Policy: *policyObj, AdmissionInfo: gr.Spec.Context.UserRequestInfo, @@ -179,7 +179,7 @@ func updateStatus(statusControl StatusControlInterface, gr kyverno.GenerateReque return statusControl.Success(gr, genResources) } -func (c *Controller) applyGeneratePolicy(log logr.Logger, policyContext engine.PolicyContext, gr kyverno.GenerateRequest, applicableRules []string) (genResources []kyverno.ResourceSpec, err error) { +func (c *Controller) applyGeneratePolicy(log logr.Logger, policyContext *engine.PolicyContext, gr kyverno.GenerateRequest, applicableRules []string) (genResources []kyverno.ResourceSpec, err error) { // Get the response as the actions to be performed on the resource // - - substitute values policy := policyContext.Policy @@ -212,7 +212,7 @@ func (c *Controller) applyGeneratePolicy(log logr.Logger, policyContext engine.P } // add configmap json data to context - if err := engine.AddResourceToContext(log, rule.Context, resCache, jsonContext); err != nil { + if err := engine.LoadContext(log, rule.Context, resCache, policyContext); err != nil { log.Info("cannot add configmaps to context", "reason", err.Error()) return nil, err } diff --git a/pkg/kyverno/apply/command.go b/pkg/kyverno/apply/command.go index 8934862f20..34efcec10c 100644 --- a/pkg/kyverno/apply/command.go +++ b/pkg/kyverno/apply/command.go @@ -528,7 +528,8 @@ func applyPolicyOnResource(policy *v1.ClusterPolicy, resource *unstructured.Unst } if policyHasGenerate { - generateResponse := engine.Generate(engine.PolicyContext{Policy: *policy, NewResource: *resource}) + ctx := &engine.PolicyContext{Policy: *policy, NewResource: *resource} + generateResponse := engine.Generate(ctx) engineResponses = append(engineResponses, generateResponse) if len(generateResponse.PolicyResponse.Rules) > 0 { log.Log.V(3).Info("generate resource is valid", "policy", policy.Name, "resource", resPath) diff --git a/pkg/policy/apply.go b/pkg/policy/apply.go index 603391264e..21d5c44316 100644 --- a/pkg/policy/apply.go +++ b/pkg/policy/apply.go @@ -10,6 +10,7 @@ import ( jsonpatch "github.com/evanphx/json-patch" "github.com/go-logr/logr" kyverno "github.com/kyverno/kyverno/pkg/api/kyverno/v1" + 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" @@ -20,7 +21,9 @@ import ( // applyPolicy applies policy on a resource func applyPolicy(policy kyverno.ClusterPolicy, resource unstructured.Unstructured, - logger logr.Logger, excludeGroupRole []string, resCache resourcecache.ResourceCache) (responses []*response.EngineResponse) { + logger logr.Logger, excludeGroupRole []string, resCache resourcecache.ResourceCache, + client *client.Client) (responses []*response.EngineResponse) { + startTime := time.Now() defer func() { name := resource.GetKind() + "/" + resource.GetName() @@ -47,7 +50,16 @@ func applyPolicy(policy kyverno.ClusterPolicy, resource unstructured.Unstructure logger.Error(err, "failed to process mutation rule") } - engineResponseValidation = engine.Validate(&engine.PolicyContext{Policy: policy, NewResource: resource, ExcludeGroupRole: excludeGroupRole, ResourceCache: resCache, JSONContext: ctx}) + policyCtx := &engine.PolicyContext{ + Policy: policy, + NewResource: resource, + ExcludeGroupRole: excludeGroupRole, + ResourceCache: resCache, + JSONContext: ctx, + Client: client, + } + + engineResponseValidation = engine.Validate(policyCtx) engineResponses = append(engineResponses, mergeRuleRespose(engineResponseMutation, engineResponseValidation)) return engineResponses diff --git a/pkg/policy/common/common.go b/pkg/policy/common/validate_pattern.go similarity index 100% rename from pkg/policy/common/common.go rename to pkg/policy/common/validate_pattern.go diff --git a/pkg/policy/existing.go b/pkg/policy/existing.go index 869b85a45d..d2e8ce1d09 100644 --- a/pkg/policy/existing.go +++ b/pkg/policy/existing.go @@ -84,7 +84,7 @@ func (pc *PolicyController) applyPolicy(policy *kyverno.ClusterPolicy, resource logger.V(4).Info("policy and resource already processed", "policyResourceVersion", policy.ResourceVersion, "resourceResourceVersion", resource.GetResourceVersion(), "kind", resource.GetKind(), "namespace", resource.GetNamespace(), "name", resource.GetName()) } - engineResponse := applyPolicy(*policy, resource, logger, pc.configHandler.GetExcludeGroupRole(), pc.resCache) + engineResponse := applyPolicy(*policy, resource, logger, pc.configHandler.GetExcludeGroupRole(), pc.resCache, pc.client) engineResponses = append(engineResponses, engineResponse...) // post-processing, register the resource as processed diff --git a/pkg/policy/validate.go b/pkg/policy/validate.go index 2741f90b1b..2f6fc0e982 100644 --- a/pkg/policy/validate.go +++ b/pkg/policy/validate.go @@ -4,12 +4,14 @@ import ( "encoding/json" "errors" "fmt" + "github.com/jmespath/go-jmespath" + "github.com/kyverno/kyverno/pkg/engine" + "github.com/kyverno/kyverno/pkg/kyverno/common" "reflect" "strings" kyverno "github.com/kyverno/kyverno/pkg/api/kyverno/v1" dclient "github.com/kyverno/kyverno/pkg/dclient" - "github.com/kyverno/kyverno/pkg/kyverno/common" "github.com/kyverno/kyverno/pkg/openapi" "github.com/kyverno/kyverno/pkg/utils" "github.com/minio/minio/pkg/wildcard" @@ -481,14 +483,59 @@ func validateRuleContext(rule kyverno.Rule) error { return fmt.Errorf("a name is required for context entries") } + var err error if entry.ConfigMap != nil { - if entry.ConfigMap.Name == "" { - return fmt.Errorf("a name is required for configMap context entry") - } + err = validateConfigMap(entry) + } else if entry.APICall != nil { + err = validateAPICall(entry) + } else { + return fmt.Errorf("a configMap or apiCall is required for context entries") + } - if entry.ConfigMap.Namespace == "" { - return fmt.Errorf("a namespace is required for configMap context entry") - } + if err != nil { + return err + } + } + + return nil +} + +func validateConfigMap(entry kyverno.ContextEntry) error { + if entry.ConfigMap == nil { + return fmt.Errorf("configMap is empty") + } + + if entry.APICall != nil { + return fmt.Errorf("both configMap and apiCall are not allowed in a context entry") + } + + 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 kyverno.ContextEntry) error { + if entry.APICall == nil { + return fmt.Errorf("apiCall is empty") + } + + if entry.ConfigMap != nil { + return fmt.Errorf("both configMap and apiCall are not allowed in a context entry") + } + + if _, err := engine.NewAPIPath(entry.APICall.URLPath); err != nil { + return err + } + + if 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) } } diff --git a/pkg/testrunner/scenario.go b/pkg/testrunner/scenario.go index 569df78a6d..8e41f3c0ac 100644 --- a/pkg/testrunner/scenario.go +++ b/pkg/testrunner/scenario.go @@ -150,7 +150,7 @@ func runTestCase(t *testing.T, tc scaseT) bool { if err := createNamespace(client, resource); err != nil { t.Error(err) } else { - policyContext := engine.PolicyContext{ + policyContext := &engine.PolicyContext{ NewResource: *resource, Policy: *policy, Client: client, diff --git a/pkg/webhooks/generation.go b/pkg/webhooks/generation.go index f07b8d317d..0f1ef8f70c 100644 --- a/pkg/webhooks/generation.go +++ b/pkg/webhooks/generation.go @@ -42,7 +42,7 @@ func (ws *WebhookServer) HandleGenerate(request *v1beta1.AdmissionRequest, polic logger.Error(err, "failed to extract resource") } - policyContext := engine.PolicyContext{ + policyContext := &engine.PolicyContext{ NewResource: new, OldResource: old, AdmissionInfo: userRequestInfo, diff --git a/pkg/webhooks/server.go b/pkg/webhooks/server.go index 0bb88012fb..a1197c973d 100644 --- a/pkg/webhooks/server.go +++ b/pkg/webhooks/server.go @@ -458,7 +458,7 @@ func (ws *WebhookServer) resourceValidation(request *v1beta1.AdmissionRequest) * logger.Error(err, "failed to load service account in context") } - ok, msg := HandleValidation(request, policies, nil, ctx, userRequestInfo, ws.statusListener, ws.eventGen, ws.prGenerator, ws.log, ws.configHandler, ws.resCache) + ok, msg := HandleValidation(request, policies, nil, ctx, userRequestInfo, ws.statusListener, ws.eventGen, ws.prGenerator, ws.log, ws.configHandler, ws.resCache, ws.client) if !ok { logger.Info("admission request denied") return &v1beta1.AdmissionResponse{ diff --git a/pkg/webhooks/validate_audit.go b/pkg/webhooks/validate_audit.go index 71abf57d9d..00fdec6c99 100644 --- a/pkg/webhooks/validate_audit.go +++ b/pkg/webhooks/validate_audit.go @@ -1,12 +1,12 @@ package webhooks import ( + client "github.com/kyverno/kyverno/pkg/dclient" "strings" "time" "github.com/go-logr/logr" v1 "github.com/kyverno/kyverno/pkg/api/kyverno/v1" - kyvernoclient "github.com/kyverno/kyverno/pkg/client/clientset/versioned" "github.com/kyverno/kyverno/pkg/config" enginectx "github.com/kyverno/kyverno/pkg/engine/context" "github.com/kyverno/kyverno/pkg/event" @@ -41,7 +41,7 @@ type AuditHandler interface { } type auditHandler struct { - client *kyvernoclient.Clientset + client *client.Client queue workqueue.RateLimitingInterface pCache policycache.Interface eventGen event.Interface @@ -67,7 +67,8 @@ func NewValidateAuditHandler(pCache policycache.Interface, crbInformer rbacinformer.ClusterRoleBindingInformer, log logr.Logger, dynamicConfig config.Interface, - resCache resourcecache.ResourceCache) AuditHandler { + resCache resourcecache.ResourceCache, + client *client.Client) AuditHandler { return &auditHandler{ pCache: pCache, @@ -82,6 +83,7 @@ func NewValidateAuditHandler(pCache policycache.Interface, prGenerator: prGenerator, configHandler: dynamicConfig, resCache: resCache, + client: client, } } @@ -173,7 +175,7 @@ func (h *auditHandler) process(request *v1beta1.AdmissionRequest) error { return errors.Wrap(err, "failed to load service account in context") } - HandleValidation(request, policies, nil, ctx, userRequestInfo, h.statusListener, h.eventGen, h.prGenerator, logger, h.configHandler, h.resCache) + HandleValidation(request, policies, nil, ctx, userRequestInfo, h.statusListener, h.eventGen, h.prGenerator, logger, h.configHandler, h.resCache, h.client) return nil } diff --git a/pkg/webhooks/validation.go b/pkg/webhooks/validation.go index addcbf259f..29f63f7d4d 100644 --- a/pkg/webhooks/validation.go +++ b/pkg/webhooks/validation.go @@ -1,6 +1,7 @@ package webhooks import ( + client "github.com/kyverno/kyverno/pkg/dclient" "reflect" "sort" "time" @@ -36,7 +37,8 @@ func HandleValidation( prGenerator policyreport.GeneratorInterface, log logr.Logger, dynamicConfig config.Interface, - resCache resourcecache.ResourceCache) (bool, string) { + resCache resourcecache.ResourceCache, + client *client.Client) (bool, string) { if len(policies) == 0 { return true, "" @@ -76,6 +78,7 @@ func HandleValidation( ExcludeResourceFunc: dynamicConfig.ToFilter, ResourceCache: resCache, JSONContext: ctx, + Client: client, } var engineResponses []*response.EngineResponse