1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2025-03-31 03:45:17 +00:00

api server lookups (#1514)

* initial commit for api server lookups

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* initial commit for API server lookups

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* Enhancing dockerfiles (multi-stage) of kyverno components and adding non-root user to the docker images (#1495)

* Dockerfile refactored

Signed-off-by: Raj Babu Das <mail.rajdas@gmail.com>

* Adding non-root commands to docker images and enhanced the dockerfiles

Signed-off-by: Raj Babu Das <mail.rajdas@gmail.com>

* changing base image to scratch

Signed-off-by: Raj Babu Das <mail.rajdas@gmail.com>

* Minor typo fix

Signed-off-by: Raj Babu Das <mail.rajdas@gmail.com>

* changing dockerfiles to use /etc/passwd to use non-root user'

Signed-off-by: Raj Babu Das <mail.rajdas@gmail.com>

* minor typo

Signed-off-by: Raj Babu Das <mail.rajdas@gmail.com>

* minor typo

Signed-off-by: Raj Babu Das <mail.rajdas@gmail.com>
Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* revert cli image name (#1507)

Signed-off-by: Raj Babu Das <mail.rajdas@gmail.com>
Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* Refactor resourceCache; Reduce throttling requests (background controller) (#1500)

* skip sending API request for filtered resource

* fix PR comment

Signed-off-by: Shuting Zhao <shutting06@gmail.com>

* fixes https://github.com/kyverno/kyverno/issues/1490

Signed-off-by: Shuting Zhao <shutting06@gmail.com>

* fix bug - namespace is not returned properly

Signed-off-by: Shuting Zhao <shutting06@gmail.com>

* reduce throttling - list resource using lister

* refactor resource cache

* fix test

Signed-off-by: Shuting Zhao <shutting06@gmail.com>

* fix label selector

Signed-off-by: Shuting Zhao <shutting06@gmail.com>

* fix build failure

Signed-off-by: Shuting Zhao <shutting06@gmail.com>
Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* fix merge issues

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* fix unit test

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* add nil check for API client

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

Co-authored-by: Raj Babu Das <mail.rajdas@gmail.com>
Co-authored-by: shuting <shutting06@gmail.com>
This commit is contained in:
Jim Bugwadia 2021-02-01 12:59:13 -08:00 committed by GitHub
parent c692263177
commit e8e3b93a5f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 639 additions and 117 deletions

View file

@ -50,17 +50,34 @@ spec:
context: context:
description: Context defines variables and data sources that can be used during rule execution. description: Context defines variables and data sources that can be used during rule execution.
items: 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: 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: configMap:
description: ConfigMapReference refers to a ConfigMap description: ConfigMap is the ConfigMap reference.
properties: properties:
name: name:
description: Name is the ConfigMap name.
type: string type: string
namespace: namespace:
description: Namespace is the ConfigMap namespace.
type: string type: string
required:
- name
type: object type: object
name: name:
description: Name is the variable name.
type: string type: string
type: object type: object
type: array type: array
@ -1152,17 +1169,34 @@ spec:
context: context:
description: Context defines variables and data sources that can be used during rule execution. description: Context defines variables and data sources that can be used during rule execution.
items: 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: 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: configMap:
description: ConfigMapReference refers to a ConfigMap description: ConfigMap is the ConfigMap reference.
properties: properties:
name: name:
description: Name is the ConfigMap name.
type: string type: string
namespace: namespace:
description: Namespace is the ConfigMap namespace.
type: string type: string
required:
- name
type: object type: object
name: name:
description: Name is the variable name.
type: string type: string
type: object type: object
type: array type: array

View file

@ -277,6 +277,7 @@ func main() {
log.Log.WithName("ValidateAuditHandler"), log.Log.WithName("ValidateAuditHandler"),
configData, configData,
rCache, rCache,
client,
) )
// Configure certificates // Configure certificates

View file

@ -67,17 +67,39 @@ spec:
can be used during rule execution. can be used during rule execution.
items: items:
description: ContextEntry adds variables and data sources 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: 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: configMap:
description: ConfigMapReference refers to a ConfigMap description: ConfigMap is the ConfigMap reference.
properties: properties:
name: name:
description: Name is the ConfigMap name.
type: string type: string
namespace: namespace:
description: Namespace is the ConfigMap namespace.
type: string type: string
required:
- name
type: object type: object
name: name:
description: Name is the variable name.
type: string type: string
type: object type: object
type: array type: array

View file

@ -68,17 +68,39 @@ spec:
can be used during rule execution. can be used during rule execution.
items: items:
description: ContextEntry adds variables and data sources 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: 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: configMap:
description: ConfigMapReference refers to a ConfigMap description: ConfigMap is the ConfigMap reference.
properties: properties:
name: name:
description: Name is the ConfigMap name.
type: string type: string
namespace: namespace:
description: Namespace is the ConfigMap namespace.
type: string type: string
required:
- name
type: object type: object
name: name:
description: Name is the variable name.
type: string type: string
type: object type: object
type: array type: array

View file

@ -55,17 +55,34 @@ spec:
context: context:
description: Context defines variables and data sources that can be used during rule execution. description: Context defines variables and data sources that can be used during rule execution.
items: 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: 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: configMap:
description: ConfigMapReference refers to a ConfigMap description: ConfigMap is the ConfigMap reference.
properties: properties:
name: name:
description: Name is the ConfigMap name.
type: string type: string
namespace: namespace:
description: Namespace is the ConfigMap namespace.
type: string type: string
required:
- name
type: object type: object
name: name:
description: Name is the variable name.
type: string type: string
type: object type: object
type: array type: array
@ -1157,17 +1174,34 @@ spec:
context: context:
description: Context defines variables and data sources that can be used during rule execution. description: Context defines variables and data sources that can be used during rule execution.
items: 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: 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: configMap:
description: ConfigMapReference refers to a ConfigMap description: ConfigMap is the ConfigMap reference.
properties: properties:
name: name:
description: Name is the ConfigMap name.
type: string type: string
namespace: namespace:
description: Namespace is the ConfigMap namespace.
type: string type: string
required:
- name
type: object type: object
name: name:
description: Name is the variable name.
type: string type: string
type: object type: object
type: array type: array

View file

@ -55,17 +55,34 @@ spec:
context: context:
description: Context defines variables and data sources that can be used during rule execution. description: Context defines variables and data sources that can be used during rule execution.
items: 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: 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: configMap:
description: ConfigMapReference refers to a ConfigMap description: ConfigMap is the ConfigMap reference.
properties: properties:
name: name:
description: Name is the ConfigMap name.
type: string type: string
namespace: namespace:
description: Namespace is the ConfigMap namespace.
type: string type: string
required:
- name
type: object type: object
name: name:
description: Name is the variable name.
type: string type: string
type: object type: object
type: array type: array
@ -1157,17 +1174,34 @@ spec:
context: context:
description: Context defines variables and data sources that can be used during rule execution. description: Context defines variables and data sources that can be used during rule execution.
items: 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: 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: configMap:
description: ConfigMapReference refers to a ConfigMap description: ConfigMap is the ConfigMap reference.
properties: properties:
name: name:
description: Name is the ConfigMap name.
type: string type: string
namespace: namespace:
description: Namespace is the ConfigMap namespace.
type: string type: string
required:
- name
type: object type: object
name: name:
description: Name is the variable name.
type: string type: string
type: object type: object
type: array type: array

View file

@ -98,18 +98,43 @@ type Rule struct {
Generation Generation `json:"generate,omitempty" yaml:"generate,omitempty"` 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 { 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"` 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 // ConfigMapReference refers to a ConfigMap
type ConfigMapReference struct { 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"` 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. // Condition defines variable-based conditional criteria for rule execution.
type Condition struct { type Condition struct {
// Key is the context entry (using JMESPath) for conditional rule evaluation. // Key is the context entry (using JMESPath) for conditional rule evaluation.

View file

@ -55,8 +55,13 @@ func NewClient(config *rest.Config, resync time.Duration, stopCh <-chan struct{}
kclient: kclient, kclient: kclient,
log: log.WithName("dclient"), log: log.WithName("dclient"),
} }
// Set discovery client // 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, // client will invalidate registered resources cache every x seconds,
// As there is no way to identify if the registered resource is available or not // 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 // 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) gvr, _ := c.DiscoveryClient.GetGVRFromKind(kind)
return gvr return gvr
} }
return c.DiscoveryClient.GetGVRFromAPIVersionKind(apiVersion, kind)
return c.DiscoveryClient.GetGVRFromAPIVersionKind(apiVersion, kind)
} }
// GetResource returns the resource in unstructured/json format // GetResource returns the resource in unstructured/json format
@ -347,15 +352,19 @@ func (c ServerPreferredResources) findResource(apiVersion string, kind string) (
} }
for _, serverResource := range serverResources { for _, serverResource := range serverResources {
if apiVersion != "" && serverResource.GroupVersion != apiVersion { if apiVersion != "" && serverResource.GroupVersion != apiVersion {
continue continue
} }
for _, resource := range serverResource.APIResources { 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 // match kind or names (e.g. Namespace, namespaces, namespace)
if resource.Kind == kind && !strings.Contains(resource.Name, "/") { // 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) gv, err := schema.ParseGroupVersion(serverResource.GroupVersion)
if err != nil { if err != nil {
c.log.Error(err, "failed to parse groupVersion", "groupVersion", serverResource.GroupVersion) c.log.Error(err, "failed to parse groupVersion", "groupVersion", serverResource.GroupVersion)

123
pkg/engine/apiPath.go Normal file
View file

@ -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
}

View file

@ -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 }}")
}

View file

@ -12,11 +12,11 @@ import (
// 1. validate variables to be substitute in the general ruleInfo (match,exclude,condition) // 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 // - 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 // 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) return filterRules(policyContext)
} }
func filterRules(policyContext PolicyContext) *response.EngineResponse { func filterRules(policyContext *PolicyContext) *response.EngineResponse {
kind := policyContext.NewResource.GetKind() kind := policyContext.NewResource.GetKind()
name := policyContext.NewResource.GetName() name := policyContext.NewResource.GetName()
namespace := policyContext.NewResource.GetNamespace() namespace := policyContext.NewResource.GetNamespace()
@ -46,7 +46,7 @@ func filterRules(policyContext PolicyContext) *response.EngineResponse {
return resp return resp
} }
func filterRule(rule kyverno.Rule, policyContext PolicyContext) *response.RuleResponse { func filterRule(rule kyverno.Rule, policyContext *PolicyContext) *response.RuleResponse {
if !rule.HasGenerate() { if !rule.HasGenerate() {
return nil return nil
} }
@ -59,7 +59,6 @@ func filterRule(rule kyverno.Rule, policyContext PolicyContext) *response.RuleRe
admissionInfo := policyContext.AdmissionInfo admissionInfo := policyContext.AdmissionInfo
ctx := policyContext.JSONContext ctx := policyContext.JSONContext
resCache := policyContext.ResourceCache resCache := policyContext.ResourceCache
jsonContext := policyContext.JSONContext
excludeGroupRole := policyContext.ExcludeGroupRole excludeGroupRole := policyContext.ExcludeGroupRole
logger := log.Log.WithName("Generate").WithValues("policy", policy.Name, 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 // 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()) logger.V(4).Info("cannot add configmaps to context", "reason", err.Error())
return nil return nil
} }

190
pkg/engine/jsonContext.go Normal file
View file

@ -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
}

View file

@ -30,7 +30,6 @@ func Mutate(policyContext *PolicyContext) (resp *response.EngineResponse) {
ctx := policyContext.JSONContext ctx := policyContext.JSONContext
resCache := policyContext.ResourceCache resCache := policyContext.ResourceCache
jsonContext := policyContext.JSONContext
logger := log.Log.WithName("EngineMutate").WithValues("policy", policy.Name, "kind", patchedResource.GetKind(), logger := log.Log.WithName("EngineMutate").WithValues("policy", policy.Name, "kind", patchedResource.GetKind(),
"namespace", patchedResource.GetNamespace(), "name", patchedResource.GetName()) "namespace", patchedResource.GetNamespace(), "name", patchedResource.GetName())
@ -68,7 +67,7 @@ func Mutate(policyContext *PolicyContext) (resp *response.EngineResponse) {
logger.V(3).Info("matched mutate rule") logger.V(3).Info("matched mutate rule")
// add configmap json data to context // 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()) logger.V(2).Info("failed to add configmaps to context", "reason", err.Error())
continue continue
} }

View file

@ -1,18 +1,14 @@
package engine package engine
import ( import (
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"reflect" "reflect"
"strings" "strings"
"time" "time"
"github.com/go-logr/logr"
kyverno "github.com/kyverno/kyverno/pkg/api/kyverno/v1" 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/engine/wildcards"
"github.com/kyverno/kyverno/pkg/resourcecache"
"github.com/kyverno/kyverno/pkg/utils" "github.com/kyverno/kyverno/pkg/utils"
"github.com/minio/minio/pkg/wildcard" "github.com/minio/minio/pkg/wildcard"
authenticationv1 "k8s.io/api/authentication/v1" authenticationv1 "k8s.io/api/authentication/v1"
@ -20,7 +16,6 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log"
) )
@ -269,17 +264,18 @@ func MatchesResourceDescription(resourceRef unstructured.Unstructured, ruleRef k
return nil return nil
} }
func copyConditions(original []kyverno.Condition) []kyverno.Condition { func copyConditions(original []kyverno.Condition) []kyverno.Condition {
if original == nil || len(original) == 0 { if original == nil || len(original) == 0 {
return []kyverno.Condition{} return []kyverno.Condition{}
} }
var copy []kyverno.Condition var copies []kyverno.Condition
for _, condition := range original { 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 // excludeResource checks if the resource has ownerRef set
@ -310,58 +306,3 @@ func ManagedPodResource(policy kyverno.ClusterPolicy, resource unstructured.Unst
return false 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")
}

View file

@ -98,7 +98,7 @@ func validateResource(log logr.Logger, ctx *PolicyContext) *response.EngineRespo
log = log.WithValues("rule", rule.Name) log = log.WithValues("rule", rule.Name)
// add configmap json data to context // 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()) log.V(2).Info("failed to add configmaps to context", "reason", err.Error())
continue continue
} }

View file

@ -121,7 +121,7 @@ func (c *Controller) applyGenerate(resource unstructured.Unstructured, gr kyvern
return nil, err return nil, err
} }
policyContext := engine.PolicyContext{ policyContext := &engine.PolicyContext{
NewResource: resource, NewResource: resource,
Policy: *policyObj, Policy: *policyObj,
AdmissionInfo: gr.Spec.Context.UserRequestInfo, AdmissionInfo: gr.Spec.Context.UserRequestInfo,
@ -179,7 +179,7 @@ func updateStatus(statusControl StatusControlInterface, gr kyverno.GenerateReque
return statusControl.Success(gr, genResources) 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 // Get the response as the actions to be performed on the resource
// - - substitute values // - - substitute values
policy := policyContext.Policy policy := policyContext.Policy
@ -212,7 +212,7 @@ func (c *Controller) applyGeneratePolicy(log logr.Logger, policyContext engine.P
} }
// add configmap json data to context // 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()) log.Info("cannot add configmaps to context", "reason", err.Error())
return nil, err return nil, err
} }

View file

@ -528,7 +528,8 @@ func applyPolicyOnResource(policy *v1.ClusterPolicy, resource *unstructured.Unst
} }
if policyHasGenerate { 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) engineResponses = append(engineResponses, generateResponse)
if len(generateResponse.PolicyResponse.Rules) > 0 { if len(generateResponse.PolicyResponse.Rules) > 0 {
log.Log.V(3).Info("generate resource is valid", "policy", policy.Name, "resource", resPath) log.Log.V(3).Info("generate resource is valid", "policy", policy.Name, "resource", resPath)

View file

@ -10,6 +10,7 @@ import (
jsonpatch "github.com/evanphx/json-patch" jsonpatch "github.com/evanphx/json-patch"
"github.com/go-logr/logr" "github.com/go-logr/logr"
kyverno "github.com/kyverno/kyverno/pkg/api/kyverno/v1" 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"
"github.com/kyverno/kyverno/pkg/engine/context" "github.com/kyverno/kyverno/pkg/engine/context"
"github.com/kyverno/kyverno/pkg/engine/response" "github.com/kyverno/kyverno/pkg/engine/response"
@ -20,7 +21,9 @@ import (
// applyPolicy applies policy on a resource // applyPolicy applies policy on a resource
func applyPolicy(policy kyverno.ClusterPolicy, resource unstructured.Unstructured, 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() startTime := time.Now()
defer func() { defer func() {
name := resource.GetKind() + "/" + resource.GetName() 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") 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)) engineResponses = append(engineResponses, mergeRuleRespose(engineResponseMutation, engineResponseValidation))
return engineResponses return engineResponses

View file

@ -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()) 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...) engineResponses = append(engineResponses, engineResponse...)
// post-processing, register the resource as processed // post-processing, register the resource as processed

View file

@ -4,12 +4,14 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"github.com/jmespath/go-jmespath"
"github.com/kyverno/kyverno/pkg/engine"
"github.com/kyverno/kyverno/pkg/kyverno/common"
"reflect" "reflect"
"strings" "strings"
kyverno "github.com/kyverno/kyverno/pkg/api/kyverno/v1" kyverno "github.com/kyverno/kyverno/pkg/api/kyverno/v1"
dclient "github.com/kyverno/kyverno/pkg/dclient" 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/openapi"
"github.com/kyverno/kyverno/pkg/utils" "github.com/kyverno/kyverno/pkg/utils"
"github.com/minio/minio/pkg/wildcard" "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") return fmt.Errorf("a name is required for context entries")
} }
var err error
if entry.ConfigMap != nil { if entry.ConfigMap != nil {
if entry.ConfigMap.Name == "" { err = validateConfigMap(entry)
return fmt.Errorf("a name is required for configMap context 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 == "" { if err != nil {
return fmt.Errorf("a namespace is required for configMap context entry") 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)
} }
} }

View file

@ -150,7 +150,7 @@ func runTestCase(t *testing.T, tc scaseT) bool {
if err := createNamespace(client, resource); err != nil { if err := createNamespace(client, resource); err != nil {
t.Error(err) t.Error(err)
} else { } else {
policyContext := engine.PolicyContext{ policyContext := &engine.PolicyContext{
NewResource: *resource, NewResource: *resource,
Policy: *policy, Policy: *policy,
Client: client, Client: client,

View file

@ -42,7 +42,7 @@ func (ws *WebhookServer) HandleGenerate(request *v1beta1.AdmissionRequest, polic
logger.Error(err, "failed to extract resource") logger.Error(err, "failed to extract resource")
} }
policyContext := engine.PolicyContext{ policyContext := &engine.PolicyContext{
NewResource: new, NewResource: new,
OldResource: old, OldResource: old,
AdmissionInfo: userRequestInfo, AdmissionInfo: userRequestInfo,

View file

@ -458,7 +458,7 @@ func (ws *WebhookServer) resourceValidation(request *v1beta1.AdmissionRequest) *
logger.Error(err, "failed to load service account in context") 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 { if !ok {
logger.Info("admission request denied") logger.Info("admission request denied")
return &v1beta1.AdmissionResponse{ return &v1beta1.AdmissionResponse{

View file

@ -1,12 +1,12 @@
package webhooks package webhooks
import ( import (
client "github.com/kyverno/kyverno/pkg/dclient"
"strings" "strings"
"time" "time"
"github.com/go-logr/logr" "github.com/go-logr/logr"
v1 "github.com/kyverno/kyverno/pkg/api/kyverno/v1" v1 "github.com/kyverno/kyverno/pkg/api/kyverno/v1"
kyvernoclient "github.com/kyverno/kyverno/pkg/client/clientset/versioned"
"github.com/kyverno/kyverno/pkg/config" "github.com/kyverno/kyverno/pkg/config"
enginectx "github.com/kyverno/kyverno/pkg/engine/context" enginectx "github.com/kyverno/kyverno/pkg/engine/context"
"github.com/kyverno/kyverno/pkg/event" "github.com/kyverno/kyverno/pkg/event"
@ -41,7 +41,7 @@ type AuditHandler interface {
} }
type auditHandler struct { type auditHandler struct {
client *kyvernoclient.Clientset client *client.Client
queue workqueue.RateLimitingInterface queue workqueue.RateLimitingInterface
pCache policycache.Interface pCache policycache.Interface
eventGen event.Interface eventGen event.Interface
@ -67,7 +67,8 @@ func NewValidateAuditHandler(pCache policycache.Interface,
crbInformer rbacinformer.ClusterRoleBindingInformer, crbInformer rbacinformer.ClusterRoleBindingInformer,
log logr.Logger, log logr.Logger,
dynamicConfig config.Interface, dynamicConfig config.Interface,
resCache resourcecache.ResourceCache) AuditHandler { resCache resourcecache.ResourceCache,
client *client.Client) AuditHandler {
return &auditHandler{ return &auditHandler{
pCache: pCache, pCache: pCache,
@ -82,6 +83,7 @@ func NewValidateAuditHandler(pCache policycache.Interface,
prGenerator: prGenerator, prGenerator: prGenerator,
configHandler: dynamicConfig, configHandler: dynamicConfig,
resCache: resCache, 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") 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 return nil
} }

View file

@ -1,6 +1,7 @@
package webhooks package webhooks
import ( import (
client "github.com/kyverno/kyverno/pkg/dclient"
"reflect" "reflect"
"sort" "sort"
"time" "time"
@ -36,7 +37,8 @@ func HandleValidation(
prGenerator policyreport.GeneratorInterface, prGenerator policyreport.GeneratorInterface,
log logr.Logger, log logr.Logger,
dynamicConfig config.Interface, dynamicConfig config.Interface,
resCache resourcecache.ResourceCache) (bool, string) { resCache resourcecache.ResourceCache,
client *client.Client) (bool, string) {
if len(policies) == 0 { if len(policies) == 0 {
return true, "" return true, ""
@ -76,6 +78,7 @@ func HandleValidation(
ExcludeResourceFunc: dynamicConfig.ToFilter, ExcludeResourceFunc: dynamicConfig.ToFilter,
ResourceCache: resCache, ResourceCache: resCache,
JSONContext: ctx, JSONContext: ctx,
Client: client,
} }
var engineResponses []*response.EngineResponse var engineResponses []*response.EngineResponse