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

store context values as single JSON document using merge patches.

This commit is contained in:
shivkumar dudhani 2019-12-17 16:06:13 -08:00
parent c4da72ad3e
commit 38987d50c3
12 changed files with 190 additions and 106 deletions

View file

@ -2,16 +2,21 @@ package context
import (
"encoding/json"
"fmt"
"sync"
jsonpatch "github.com/evanphx/json-patch"
"github.com/golang/glog"
authenticationv1 "k8s.io/api/authentication/v1"
)
//Interface ... normal functions
type Interface interface {
Add(key string, data []byte) error
Remove(key string) error
// merges the json with context
AddJSON(dataRaw []byte) error
// merges resource json under request.object
AddResource(dataRaw []byte) error
// merges userInfo json under request.userInfo
AddUserInfo(userInfo authenticationv1.UserInfo) error
EvalInterface
}
@ -22,47 +27,98 @@ type EvalInterface interface {
//Context stores the data resources as JSON
type Context struct {
mu sync.RWMutex
data map[string]interface{}
mu sync.RWMutex
// data map[string]interface{}
jsonRaw []byte
}
//NewContext returns a new context
func NewContext() *Context {
ctx := Context{
data: map[string]interface{}{},
// data: map[string]interface{}{},
jsonRaw: []byte(`{}`), // empty json struct
}
return &ctx
}
//Add adds resource with the key
// we always overwrite the resoruce if already present
func (ctx *Context) Add(key string, resource []byte) error {
// AddJSON merges json data
func (ctx *Context) AddJSON(dataRaw []byte) error {
var err error
ctx.mu.Lock()
defer ctx.mu.Unlock()
// insert/update
// umarshall before adding
var data interface{}
if err := json.Unmarshal(resource, &data); err != nil {
glog.V(4).Infof("failed to unmarshall resource in context: %v", err)
fmt.Println(err)
// merge json
ctx.jsonRaw, err = jsonpatch.MergePatch(ctx.jsonRaw, dataRaw)
if err != nil {
glog.V(4).Infof("failed to merge JSON data: %v", err)
return err
}
ctx.data[key] = data
return nil
}
//Remove removes resource with given key
func (ctx *Context) Remove(key string) error {
ctx.mu.Lock()
defer ctx.mu.Unlock()
_, ok := ctx.data[key]
if ok {
delete(ctx.data, key)
return nil
// //Add adds resource with the key
// // we always overwrite the resoruce if already present
// func (ctx *Context) Add(key string, resource []byte) error {
// ctx.mu.Lock()
// defer ctx.mu.Unlock()
// // insert/update
// // umarshall before adding
// var data interface{}
// if err := json.Unmarshal(resource, &data); err != nil {
// glog.V(4).Infof("failed to unmarshall resource in context: %v", err)
// fmt.Println(err)
// return err
// }
// ctx.data[key] = data
// return nil
// }
// func (ctx *Context) getData() interface{} {
// return ctx.data
// }
//Add data at path: request.object
func (ctx *Context) AddResource(dataRaw []byte) error {
// unmarshall the resource struct
var data interface{}
if err := json.Unmarshal(dataRaw, &data); err != nil {
glog.V(4).Infof("failed to unmarshall the context data: %v", err)
return err
}
return fmt.Errorf("no resource with key %s", key)
modifiedResource := struct {
Request interface{} `json:"request"`
}{
Request: struct {
Object interface{} `json:"object"`
}{
Object: data,
},
}
objRaw, err := json.Marshal(modifiedResource)
if err != nil {
glog.V(4).Infof("failed to marshall the updated context data")
return err
}
return ctx.AddJSON(objRaw)
}
func (ctx *Context) getData() interface{} {
return ctx.data
func (ctx *Context) AddUserInfo(userInfo authenticationv1.UserInfo) error {
modifiedResource := struct {
Request interface{} `json:"request"`
}{
Request: struct {
UserInfo interface{} `json:"userInfo"`
}{
UserInfo: userInfo,
},
}
objRaw, err := json.Marshal(modifiedResource)
if err != nil {
glog.V(4).Infof("failed to marshall the updated context data")
return err
}
return ctx.AddJSON(objRaw)
}

View file

@ -3,9 +3,11 @@ package context
import (
"reflect"
"testing"
authenticationv1 "k8s.io/api/authentication/v1"
)
func Test_Add(t *testing.T) {
func Test_addResourceAndUserContext(t *testing.T) {
rawResource := []byte(`
{
"apiVersion": "v1",
@ -40,47 +42,40 @@ func Test_Add(t *testing.T) {
}
`)
expectedResult := "my-namespace"
userInfo := authenticationv1.UserInfo{
Username: "admin",
UID: "014fbff9a07c",
}
var err error
var expectedResult string
ctx := NewContext()
ctx.Add("resource", rawResource)
query := "resource.metadata.labels.namespace"
result, err := ctx.Query(query)
ctx.AddResource(rawResource)
result, err := ctx.Query("request.object.apiVersion")
if err != nil {
t.Error(err)
}
t.Log(expectedResult)
t.Log(result)
if !reflect.DeepEqual(expectedResult, result) {
t.Error("exected result does not match")
}
}
func Test_AddUser(t *testing.T) {
rawUser := []byte(`
{
"userInfo": {
"username": "admin",
"uid": "014fbff9a07c",
"groups": ["system:authenticated","my-admin-group"],
"extra": {
"some-key":["some-value1", "some-value2"]
}
}
}
`)
expectedResult := "admin"
var err error
ctx := NewContext()
ctx.Add("user", rawUser)
query := "user.userInfo.username"
result, err := ctx.Query(query)
if err != nil {
t.Error(err)
}
t.Log(expectedResult)
expectedResult = "v1"
t.Log(result)
if !reflect.DeepEqual(expectedResult, result) {
t.Error("exected result does not match")
}
ctx.AddUserInfo(userInfo)
result, err = ctx.Query("request.object.apiVersion")
if err != nil {
t.Error(err)
}
expectedResult = "v1"
t.Log(result)
if !reflect.DeepEqual(expectedResult, result) {
t.Error("exected result does not match")
}
result, err = ctx.Query("request.userInfo.username")
if err != nil {
t.Error(err)
}
expectedResult = "admin"
t.Log(result)
if !reflect.DeepEqual(expectedResult, result) {
t.Error("exected result does not match")

View file

@ -1,11 +1,31 @@
package context
import (
"encoding/json"
"github.com/golang/glog"
"github.com/jmespath/go-jmespath"
)
//Query searches for query in the context
// //Query searches for query in the context
// func (ctx *Context) Query(query string) (interface{}, error) {
// var emptyResult interface{}
// // compile the query
// queryPath, err := jmespath.Compile(query)
// if err != nil {
// glog.V(4).Infof("incorrect query %s: %v", query, err)
// return emptyResult, err
// }
// // search
// result, err := queryPath.Search(ctx.getData())
// if err != nil {
// glog.V(4).Infof("failed to search query %s: %v", query, err)
// return emptyResult, err
// }
// return result, nil
// }
//Query ...
func (ctx *Context) Query(query string) (interface{}, error) {
var emptyResult interface{}
// compile the query
@ -14,9 +34,17 @@ func (ctx *Context) Query(query string) (interface{}, error) {
glog.V(4).Infof("incorrect query %s: %v", query, err)
return emptyResult, err
}
// search
result, err := queryPath.Search(ctx.getData())
ctx.mu.RLock()
defer ctx.mu.RUnlock()
var data interface{}
if err := json.Unmarshal(ctx.jsonRaw, &data); err != nil {
glog.V(4).Infof("failed to unmarshall context")
return emptyResult, err
}
result, err := queryPath.Search(data)
if err != nil {
glog.V(4).Infof("failed to search query %s: %v", query, err)
return emptyResult, err

View file

@ -33,7 +33,7 @@ func Test_VariableSubstitutionOverlay(t *testing.T) {
"overlay": {
"metadata": {
"labels": {
"appname": "{{resource.metadata.name}}"
"appname": "{{request.object.metadata.name}}"
}
}
}
@ -70,8 +70,8 @@ func Test_VariableSubstitutionOverlay(t *testing.T) {
resourceUnstructured, err := ConvertToUnstructured(rawResource)
assert.NilError(t, err)
ctx := context.NewContext()
ctx.Add("resource", rawResource)
value, err := ctx.Query("resource.metadata.name")
ctx.AddResource(rawResource)
value, err := ctx.Query("request.object.metadata.name")
t.Log(value)
if err != nil {
t.Error(err)

View file

@ -1,11 +1,11 @@
package engine
import (
client "github.com/nirmata/kyverno/pkg/dclient"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1"
client "github.com/nirmata/kyverno/pkg/dclient"
"github.com/nirmata/kyverno/pkg/engine/context"
authenticationv1 "k8s.io/api/authentication/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"github.com/nirmata/kyverno/pkg/engine/context"
)
// PolicyContext contains the contexts for engine to process
@ -20,7 +20,7 @@ type PolicyContext struct {
// Dynamic client - used by generate
Client *client.Client
// Contexts to store resources
Context *context.Context
Context context.EvalInterface
}
// RequestInfo contains permission info carried in an admission request

View file

@ -25,7 +25,7 @@ func Test_variableSubstitutionValue(t *testing.T) {
patternMap := []byte(`
{
"spec": {
"name": "{{resource.metadata.name}}"
"name": "{{request.object.metadata.name}}"
}
}
`)
@ -38,7 +38,7 @@ func Test_variableSubstitutionValue(t *testing.T) {
// context
ctx := context.NewContext()
ctx.Add("resource", resourceRaw)
ctx.AddResource(resourceRaw)
value := SubstituteVariables(ctx, pattern)
resultRaw, err := json.Marshal(value)
if err != nil {
@ -66,7 +66,7 @@ func Test_variableSubstitutionValueOperatorNotEqual(t *testing.T) {
patternMap := []byte(`
{
"spec": {
"name": "!{{resource.metadata.name}}"
"name": "!{{request.object.metadata.name}}"
}
}
`)
@ -78,7 +78,7 @@ func Test_variableSubstitutionValueOperatorNotEqual(t *testing.T) {
// context
ctx := context.NewContext()
ctx.Add("resource", resourceRaw)
ctx.AddResource(resourceRaw)
value := SubstituteVariables(ctx, pattern)
resultRaw, err := json.Marshal(value)
if err != nil {
@ -108,7 +108,7 @@ func Test_variableSubstitutionValueFail(t *testing.T) {
patternMap := []byte(`
{
"spec": {
"name": "{{resource.metadata.name1}}"
"name": "{{request.object.metadata.name1}}"
}
}
`)
@ -120,7 +120,7 @@ func Test_variableSubstitutionValueFail(t *testing.T) {
// context
ctx := context.NewContext()
ctx.Add("resource", resourceRaw)
ctx.AddResource(resourceRaw)
value := SubstituteVariables(ctx, pattern)
resultRaw, err := json.Marshal(value)
if err != nil {
@ -155,7 +155,7 @@ func Test_variableSubstitutionObject(t *testing.T) {
patternMap := []byte(`
{
"spec": {
"variable": "{{resource.spec.variable}}"
"variable": "{{request.object.spec.variable}}"
}
}
`)
@ -167,7 +167,7 @@ func Test_variableSubstitutionObject(t *testing.T) {
// context
ctx := context.NewContext()
ctx.Add("resource", resourceRaw)
ctx.AddResource(resourceRaw)
value := SubstituteVariables(ctx, pattern)
resultRaw, err := json.Marshal(value)
if err != nil {
@ -202,7 +202,7 @@ func Test_variableSubstitutionObjectOperatorNotEqualFail(t *testing.T) {
patternMap := []byte(`
{
"spec": {
"variable": "!{{resource.spec.variable}}"
"variable": "!{{request.object.spec.variable}}"
}
}
`)
@ -215,7 +215,7 @@ func Test_variableSubstitutionObjectOperatorNotEqualFail(t *testing.T) {
// context
ctx := context.NewContext()
ctx.Add("resource", resourceRaw)
ctx.AddResource(resourceRaw)
value := SubstituteVariables(ctx, pattern)
resultRaw, err := json.Marshal(value)
if err != nil {
@ -250,8 +250,8 @@ func Test_variableSubstitutionMultipleObject(t *testing.T) {
patternMap := []byte(`
{
"spec": {
"var": "{{resource.spec.variable.varNested.var1}}",
"variable": "{{resource.spec.variable}}"
"var": "{{request.object.spec.variable.varNested.var1}}",
"variable": "{{request.object.spec.variable}}"
}
}
`)
@ -264,7 +264,7 @@ func Test_variableSubstitutionMultipleObject(t *testing.T) {
// context
ctx := context.NewContext()
ctx.Add("resource", resourceRaw)
ctx.AddResource(resourceRaw)
value := SubstituteVariables(ctx, pattern)
resultRaw, err := json.Marshal(value)
if err != nil {

View file

@ -225,7 +225,7 @@ func applyPolicy(client *client.Client, resource unstructured.Unstructured, p ky
}()
// build context
ctx := context.NewContext()
ctx.Add("resource", transformResource(resource))
ctx.AddResource(transformResource(resource))
policyContext := engine.PolicyContext{
NewResource: resource,

View file

@ -9,6 +9,7 @@ import (
"github.com/golang/glog"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1"
"github.com/nirmata/kyverno/pkg/engine"
"github.com/nirmata/kyverno/pkg/engine/context"
"github.com/nirmata/kyverno/pkg/engine/response"
"github.com/nirmata/kyverno/pkg/utils"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@ -58,9 +59,12 @@ func applyPolicy(policy kyverno.ClusterPolicy, resource unstructured.Unstructure
var engineResponses []response.EngineResponse
var engineResponse response.EngineResponse
var err error
// build context
ctx := context.NewContext()
ctx.AddResource(transformResource(resource))
//MUTATION
engineResponse, err = mutation(policy, resource, policyStatus)
engineResponse, err = mutation(policy, resource, policyStatus, ctx)
engineResponses = append(engineResponses, engineResponse)
if err != nil {
glog.Errorf("unable to process mutation rules: %v", err)
@ -70,7 +74,7 @@ func applyPolicy(policy kyverno.ClusterPolicy, resource unstructured.Unstructure
sendStat(false)
//VALIDATION
engineResponse = engine.Validate(engine.PolicyContext{Policy: policy, NewResource: resource})
engineResponse = engine.Validate(engine.PolicyContext{Policy: policy, Context: ctx, NewResource: resource})
engineResponses = append(engineResponses, engineResponse)
// gather stats
gatherStat(policy.Name, engineResponse.PolicyResponse)
@ -80,8 +84,9 @@ func applyPolicy(policy kyverno.ClusterPolicy, resource unstructured.Unstructure
//TODO: GENERATION
return engineResponses
}
func mutation(policy kyverno.ClusterPolicy, resource unstructured.Unstructured, policyStatus PolicyStatusInterface) (response.EngineResponse, error) {
engineResponse := engine.Mutate(engine.PolicyContext{Policy: policy, NewResource: resource})
func mutation(policy kyverno.ClusterPolicy, resource unstructured.Unstructured, policyStatus PolicyStatusInterface, ctx context.EvalInterface) (response.EngineResponse, error) {
engineResponse := engine.Mutate(engine.PolicyContext{Policy: policy, NewResource: resource, Context: ctx})
if !engineResponse.IsSuccesful() {
glog.V(4).Infof("mutation had errors reporting them")
return engineResponse, nil

View file

@ -3,7 +3,9 @@ package policy
import (
"fmt"
"github.com/golang/glog"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
)
@ -21,3 +23,12 @@ func buildPolicyLabel(policyName string) (labels.Selector, error) {
}
return policySelector, nil
}
func transformResource(resource unstructured.Unstructured) []byte {
data, err := resource.MarshalJSON()
if err != nil {
glog.Errorf("failed to marshall resource %v: %v", resource, err)
return nil
}
return data
}

View file

@ -3,7 +3,6 @@ package webhooks
import (
"fmt"
"strings"
"encoding/json"
"github.com/golang/glog"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1"
@ -11,7 +10,6 @@ import (
"github.com/nirmata/kyverno/pkg/engine/response"
"k8s.io/api/admission/v1beta1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
authenticationv1 "k8s.io/api/authentication/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
)
@ -154,12 +152,3 @@ func convertToUnstructured(data []byte) (*unstructured.Unstructured, error) {
}
return resource, nil
}
func transformUser(userInfo authenticationv1.UserInfo) []byte {
data, err := json.Marshal(userInfo)
if err != nil {
glog.Errorf("failed to marshall resource %v: %v", userInfo, err)
return nil
}
return data
}

View file

@ -66,8 +66,8 @@ func (ws *WebhookServer) HandleMutation(request *v1beta1.AdmissionRequest, polic
// build context
ctx := context.NewContext()
// load incoming resource into the context
ctx.Add("resource", request.Object.Raw)
ctx.Add("user", transformUser(request.UserInfo))
ctx.AddResource(request.Object.Raw)
ctx.AddUserInfo(request.UserInfo)
policyContext := engine.PolicyContext{
NewResource: *resource,

View file

@ -62,8 +62,8 @@ func (ws *WebhookServer) HandleValidation(request *v1beta1.AdmissionRequest, pol
// build context
ctx := context.NewContext()
// load incoming resource into the context
ctx.Add("resource", request.Object.Raw)
ctx.Add("user", transformUser(request.UserInfo))
ctx.AddResource(request.Object.Raw)
ctx.AddUserInfo(request.UserInfo)
policyContext := engine.PolicyContext{
NewResource: newR,