1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2025-03-06 07:57:07 +00:00

Merge branch 'main' into test_cli

This commit is contained in:
vyankyGH 2021-02-02 18:57:05 +05:30 committed by GitHub
commit 27f9b4747a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 875 additions and 119 deletions

View file

@ -68,9 +68,11 @@ Parameter | Description | Default
`createSelfSignedCert` | generate a self signed cert and certificate authority. Kyverno defaults to using kube-controller-manager CA-signed certificate or existing cert secret if false. | `false`
`config.existingConfig` | existing Kubernetes configmap to use for the resource filters configuration | `nil`
`config.resourceFilters` | list of filter of resource types to be skipped by kyverno policy engine. See [documentation](https://github.com/kyverno/kyverno/blob/master/documentation/installation.md#filter-kubernetes-resources-that-admission-webhook-should-not-process) for details | `["[Event,*,*]","[*,kube-system,*]","[*,kube-public,*]","[*,kube-node-lease,*]","[Node,*,*]","[APIService,*,*]","[TokenReview,*,*]","[SubjectAccessReview,*,*]","[*,kyverno,*]"]`
`dnsPolicy` | Sets the DNS Policy which determines the manner in which DNS resolution happens across the cluster. For further reference, see [the official docs](https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/#pod-s-dns-policy) | `ClusterFirst`
`extraArgs` | list of extra arguments to give the binary | `[]`
`fullnameOverride` | override the expanded name of the chart | `nil`
`generatecontrollerExtraResources` | extra resource type Kyverno is allowed to generate | `[]`
`hostNetwork` | Use the host network's namespace. Set it to `true` when dealing with a custom CNI over Amazon EKS | `false`
`image.pullPolicy` | Image pull policy | `IfNotPresent`
`image.pullSecrets` | Specify image pull secrets | `[]` (does not add image pull secrets to deployed pods)
`image.repository` | Image repository | `ghcr.io/kyverno/kyverno`

View file

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

View file

@ -37,6 +37,12 @@ spec:
{{- if .Values.priorityClassName }}
priorityClassName: {{ .Values.priorityClassName | quote }}
{{- end }}
{{- if .Values.hostNetwork }}
hostNetwork: {{ .Values.hostNetwork }}
{{- end }}
{{- if .Values.dnsPolicy }}
dnsPolicy: {{ .Values.dnsPolicy }}
{{- end }}
initContainers:
- name: kyverno-pre
image: {{ .Values.initImage.repository }}:{{ default .Chart.AppVersion (default .Values.image.tag .Values.initImage.tag) }}

View file

@ -42,6 +42,16 @@ affinity: {}
nodeSelector: {}
tolerations: []
# change hostNetwork to true when you want the kyverno's pod to share its host's network namespace
# useful for situations like when you end up dealing with a custom CNI over Amazon EKS
# update the 'dnsPolicy' accordingly as well to suit the host network mode
hostNetwork: false
# dnsPolicy determines the manner in which DNS resolution happens in the cluster
# in case of hostNetwork: true, usually, the dnsPolicy is suitable to be "ClusterFirstWithHostNet"
# for further reference: https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/#pod-s-dns-policy
dnsPolicy: "ClusterFirst"
extraArgs: []
# - --webhooktimeout=4

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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)
// - 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
}

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

View file

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

View file

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

View file

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

View file

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

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())
}
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

View file

@ -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"
@ -427,16 +429,84 @@ func validateResources(rule kyverno.Rule) (string, error) {
// matched resources
if path, err := validateMatchedResourceDescription(rule.MatchResources.ResourceDescription); err != nil {
return fmt.Sprintf("resources.%s", path), err
return fmt.Sprintf("match.resources.%s", path), err
}
// exclude resources
if path, err := validateExcludeResourceDescription(rule.ExcludeResources.ResourceDescription); err != nil {
return fmt.Sprintf("resources.%s", path), err
return fmt.Sprintf("exclude.resources.%s", path), err
}
//validating the values present under validation.preconditions, if they exist
if rule.Conditions != nil {
if path, err := validateConditions(rule.Conditions, "preconditions"); err != nil {
return fmt.Sprintf("validate.%s", path), err
}
}
// validating the values present under validation.deny.conditions, if they exist
if rule.Validation.Deny != nil {
if path, err := validateConditions(rule.Validation.Deny.Conditions, "conditions"); err != nil {
return fmt.Sprintf("validate.deny.%s", path), err
}
}
return "", nil
}
// validateConditions validates all the 'conditions' or 'preconditions' of a rule depending on the corresponding 'condition.key'.
// As of now, it is validating the 'value' field whether it contains the only allowed set of values or not when 'condition.key' is {{request.operation}}
func validateConditions(conditions []kyverno.Condition, schemaKey string) (string, error) {
// []kyverno.Condition can only exist under either 'conditions' or 'preconditions' key of the policy schema
if schemaKey != "conditions" && schemaKey != "preconditions" {
return fmt.Sprintf(schemaKey), fmt.Errorf("wrong schema key found for validating the conditions. Conditions can only occur under 'preconditions' or 'conditions' key in the policy schema")
}
for i, condition := range conditions {
if path, err := validateConditionValues(condition); err != nil {
return fmt.Sprintf("%s[%d].%s", schemaKey, i, path), err
}
}
return "", nil
}
// ValidateUniqueRuleName checks if the rule names are unique across a policy
// validateConditionValues validates whether all the values under the 'value' field of a 'conditions' field
// are apt with respect to the provided 'condition.key'
func validateConditionValues(c kyverno.Condition) (string, error) {
switch strings.ReplaceAll(c.Key.(string), " ", "") {
case "{{request.operation}}":
return validateConditionValuesKeyRequestOperation(c)
default:
return "", nil
}
}
// validateConditionValuesKeyRequestOperation validates whether all the values under the 'value' field of a 'conditions' field
// are one of ["CREATE", "UPDATE", "DELETE", "CONNECT"] when 'condition.key' is {{request.operation}}
func validateConditionValuesKeyRequestOperation(c kyverno.Condition) (string, error) {
valuesAllowed := map[string]bool{
"CREATE": true,
"UPDATE": true,
"DELETE": true,
"CONNECT": true,
}
switch reflect.TypeOf(c.Value).Kind() {
case reflect.String:
if !valuesAllowed[c.Value.(string)] {
return fmt.Sprintf("value: %s", c.Value.(string)), fmt.Errorf("unknown value '%s' found under the 'value' field. Only the following values are allowed: [CREATE, UPDATE, DELETE, CONNECT]", c.Value.(string))
}
case reflect.Slice:
values := reflect.ValueOf(c.Value)
for i := 0; i < values.Len(); i++ {
value := values.Index(i).Interface().(string)
if !valuesAllowed[value] {
return fmt.Sprintf("value[%d]", i), fmt.Errorf("unknown value '%s' found under the 'value' field. Only the following values are allowed: [CREATE, UPDATE, DELETE, CONNECT]", value)
}
}
default:
return fmt.Sprintf("value"), fmt.Errorf("'value' field found to be of the type %v. The provided value/values are expected to be either in the form of a string or list", reflect.TypeOf(c.Value).Kind())
}
return "", nil
}
// validateUniqueRuleName checks if the rule names are unique across a policy
func validateUniqueRuleName(p kyverno.ClusterPolicy) (string, error) {
var ruleNames []string
@ -481,14 +551,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)
}
}

View file

@ -251,6 +251,155 @@ func Test_Validate_ResourceDescription_MatchedValid(t *testing.T) {
assert.NilError(t, err)
}
func Test_Validate_DenyConditions_KeyRequestOperation_Empty(t *testing.T) {
denyConditions := []byte(`[]`)
var dcs []kyverno.Condition
err := json.Unmarshal(denyConditions, &dcs)
assert.NilError(t, err)
_, err = validateConditions(dcs, "conditions")
assert.NilError(t, err)
}
func Test_Validate_Preconditions_KeyRequestOperation_Empty(t *testing.T) {
preConditions := []byte(`[]`)
var pcs []kyverno.Condition
err := json.Unmarshal(preConditions, &pcs)
assert.NilError(t, err)
_, err = validateConditions(pcs, "preconditions")
assert.NilError(t, err)
}
func Test_Validate_DenyConditionsValuesString_KeyRequestOperation_ExpectedValue(t *testing.T) {
denyConditions := []byte(`
[
{
"key":"{{request.operation}}",
"operator":"Equals",
"value":"DELETE"
},
{
"key":"{{request.operation}}",
"operator":"NotEquals",
"value":"CREATE"
},
{
"key":"{{request.operation}}",
"operator":"NotEquals",
"value":"CONNECT"
},
{
"key":"{{ request.operation }}",
"operator":"NotEquals",
"value":"UPDATE"
},
{
"key":"{{lbServiceCount}}",
"operator":"Equals",
"value":"2"
}
]
`)
var dcs []kyverno.Condition
err := json.Unmarshal(denyConditions, &dcs)
assert.NilError(t, err)
_, err = validateConditions(dcs, "conditions")
assert.NilError(t, err)
}
func Test_Validate_PreconditionsValuesString_KeyRequestOperation_UnknownValue(t *testing.T) {
preConditions := []byte(`
[
{
"key":"{{request.operation}}",
"operator":"Equals",
"value":"foobar"
},
{
"key": "{{request.operation}}",
"operator": "NotEquals",
"value": "CREATE"
}
]
`)
var pcs []kyverno.Condition
err := json.Unmarshal(preConditions, &pcs)
assert.NilError(t, err)
_, err = validateConditions(pcs, "preconditions")
assert.Assert(t, err != nil)
}
func Test_Validate_DenyConditionsValuesList_KeyRequestOperation_ExpectedItem(t *testing.T) {
denyConditions := []byte(`
[
{
"key":"{{request.operation}}",
"operator":"Equals",
"value": [
"CREATE",
"DELETE",
"CONNECT"
]
},
{
"key":"{{request.operation}}",
"operator":"NotEquals",
"value": [
"UPDATE"
]
},
{
"key": "{{lbServiceCount}}",
"operator": "Equals",
"value": "2"
}
]
`)
var dcs []kyverno.Condition
err := json.Unmarshal(denyConditions, &dcs)
assert.NilError(t, err)
_, err = validateConditions(dcs, "conditions")
assert.NilError(t, err)
}
func Test_Validate_PreconditionsValuesList_KeyRequestOperation_UnknownItem(t *testing.T) {
preConditions := []byte(`
[
{
"key":"{{request.operation}}",
"operator":"Equals",
"value": [
"foobar",
"CREATE"
]
},
{
"key":"{{request.operation}}",
"operator":"NotEquals",
"value": [
"foobar"
]
}
]
`)
var pcs []kyverno.Condition
err := json.Unmarshal(preConditions, &pcs)
assert.NilError(t, err)
_, err = validateConditions(pcs, "preconditions")
assert.Assert(t, err != nil)
}
func Test_Validate_ResourceDescription_MissingKindsOnExclude(t *testing.T) {
var err error
excludeResourcedescirption := []byte(`

View file

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

View file

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

View file

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

View file

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

View file

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