diff --git a/pkg/engine/context/context.go b/pkg/engine/context/context.go index 89b6d27a4c..3e983d9e41 100644 --- a/pkg/engine/context/context.go +++ b/pkg/engine/context/context.go @@ -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) } diff --git a/pkg/engine/context/context_test.go b/pkg/engine/context/context_test.go index f511895d39..f07ee97c1c 100644 --- a/pkg/engine/context/context_test.go +++ b/pkg/engine/context/context_test.go @@ -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") diff --git a/pkg/engine/context/evaluate.go b/pkg/engine/context/evaluate.go index 67ba133dc2..adc0d23828 100644 --- a/pkg/engine/context/evaluate.go +++ b/pkg/engine/context/evaluate.go @@ -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 diff --git a/pkg/engine/mutation_test.go b/pkg/engine/mutation_test.go index 79b3d3c0a3..9912117ba2 100644 --- a/pkg/engine/mutation_test.go +++ b/pkg/engine/mutation_test.go @@ -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) diff --git a/pkg/engine/policyContext.go b/pkg/engine/policyContext.go index 54aab3be4c..34f7f23cb9 100644 --- a/pkg/engine/policyContext.go +++ b/pkg/engine/policyContext.go @@ -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 diff --git a/pkg/engine/variables/variables_test.go b/pkg/engine/variables/variables_test.go index 12abef53b4..9e183f8e5f 100644 --- a/pkg/engine/variables/variables_test.go +++ b/pkg/engine/variables/variables_test.go @@ -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 { diff --git a/pkg/namespace/generation.go b/pkg/namespace/generation.go index b8b446e881..35f4cd5ea5 100644 --- a/pkg/namespace/generation.go +++ b/pkg/namespace/generation.go @@ -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, diff --git a/pkg/policy/apply.go b/pkg/policy/apply.go index 8b1fa74368..cbe06a670c 100644 --- a/pkg/policy/apply.go +++ b/pkg/policy/apply.go @@ -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 diff --git a/pkg/policy/common.go b/pkg/policy/common.go index 7d504c4de0..b4d6155abc 100644 --- a/pkg/policy/common.go +++ b/pkg/policy/common.go @@ -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 +} diff --git a/pkg/webhooks/common.go b/pkg/webhooks/common.go index ec4d35a3d9..6ca60cbccf 100644 --- a/pkg/webhooks/common.go +++ b/pkg/webhooks/common.go @@ -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 -} diff --git a/pkg/webhooks/mutation.go b/pkg/webhooks/mutation.go index af44ee7959..275c2a86aa 100644 --- a/pkg/webhooks/mutation.go +++ b/pkg/webhooks/mutation.go @@ -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, diff --git a/pkg/webhooks/validation.go b/pkg/webhooks/validation.go index e2d5f3f879..710c87c585 100644 --- a/pkg/webhooks/validation.go +++ b/pkg/webhooks/validation.go @@ -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,