This repository was archived by the owner on Jun 6, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 362
feat: add custom key for cel expression support #961
Merged
+337
−22
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
b6bb02f
feat: add custom key for cel expression support
82410b0
test: add tests
559eb29
test: update CEL policy example
8ea4d74
test: fix word
91f7629
test: update CEL policy example
31782b7
fix: goimports
175987a
fix: lint
1aa323a
fix: lint
f4ce267
fix: rename var
5e42045
fix: pr comments
1f64bd8
fix: remove messae printing
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
apiVersion: v1 | ||
policies: | ||
- name: CEL_policy | ||
isDefault: true | ||
rules: | ||
- identifier: CUSTOM_DEPLOYMENT_BILLING_LABEL_EXISTS | ||
messageOnFailure: "workloads labels should contain billing label" | ||
- identifier: CUSTOM_SECRET_ENVIRONMENT_LABEL_EXISTS | ||
messageOnFailure: "secret labels should contain environment label" | ||
|
||
|
||
customRules: | ||
- identifier: CUSTOM_WORKLOADS_BILLING_LABEL_EXISTS | ||
name: Ensure Workloads has billing label [CUSTOM RULE] | ||
defaultMessageOnFailure: workloads labels should contain billing label | ||
schema: | ||
# constraint schema | ||
if: | ||
properties: | ||
kind: | ||
type: string | ||
enum: | ||
- Deployment | ||
- Pod | ||
then: | ||
CELDefinition: | ||
- expression: "object.kind != 'Deployment' || (has(object.metadata.labels) && has(object.metadata.labels.billing))" | ||
message: "deployment labels should contain billing label" | ||
- expression: "object.kind != 'Pod' || (has(object.metadata.labels) && has(object.metadata.labels.billing))" | ||
message: "pod labels should contain billing label" | ||
- identifier: CUSTOM_SECRET_ENVIRONMENT_LABEL_EXISTS | ||
name: Ensure Secret has environment label [CUSTOM RULE] | ||
defaultMessageOnFailure: secret labels should contain environment label | ||
schema: | ||
# constraint schema | ||
if: | ||
properties: | ||
kind: | ||
type: string | ||
enum: | ||
- Secret | ||
then: | ||
CELDefinition: | ||
- expression: "has(object.metadata.labels) && has(object.metadata.labels.environment)" | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
132 changes: 132 additions & 0 deletions
132
pkg/jsonSchemaValidator/extensions/customKeyCELDefinition.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
// This file defines a custom key to implement the logic for cel rule: | ||
|
||
package jsonSchemaValidator | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
|
||
"github.com/google/cel-go/cel" | ||
"github.com/santhosh-tekuri/jsonschema/v5" | ||
) | ||
|
||
const CELDefinitionCustomKey = "CELDefinition" | ||
|
||
type CustomKeyCELDefinitionCompiler struct{} | ||
|
||
type CustomKeyCELDefinitionSchema []interface{} | ||
|
||
var CustomKeyCELRule = jsonschema.MustCompileString("customKeyCELDefinition.json", `{ | ||
"properties" : { | ||
"CELDefinition": { | ||
"type": "array" | ||
} | ||
} | ||
}`) | ||
|
||
func (CustomKeyCELDefinitionCompiler) Compile(ctx jsonschema.CompilerContext, m map[string]interface{}) (jsonschema.ExtSchema, error) { | ||
if customKeyCELRule, ok := m[CELDefinitionCustomKey]; ok { | ||
customKeyCELRuleObj, validObject := customKeyCELRule.([]interface{}) | ||
if !validObject { | ||
return nil, fmt.Errorf("CELDefinition must be an array") | ||
} | ||
|
||
CELDefinitionSchema, err := convertCustomKeyCELDefinitionSchemaToCELDefinitionSchema(customKeyCELRuleObj) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
if len(CELDefinitionSchema.CELExpressions) == 0 { | ||
return nil, fmt.Errorf("CELDefinition can't be empty") | ||
} | ||
|
||
return CustomKeyCELDefinitionSchema(customKeyCELRuleObj), nil | ||
} | ||
return nil, nil | ||
} | ||
|
||
func (customKeyCELDefinitionSchema CustomKeyCELDefinitionSchema) Validate(ctx jsonschema.ValidationContext, dataValue interface{}) error { | ||
CELDefinitionSchema, err := convertCustomKeyCELDefinitionSchemaToCELDefinitionSchema(customKeyCELDefinitionSchema) | ||
if err != nil { | ||
return ctx.Error(CustomKeyValidationErrorKeyPath, err.Error()) | ||
} | ||
// wrap dataValue (the resource that should be validated) inside a struct with parent object key | ||
resourceWithParentKey := make(map[string]interface{}) | ||
resourceWithParentKey["object"] = dataValue | ||
|
||
// prepare CEL env inputs - in our case the only input is the resource that should be validated | ||
inputs, err := getCELEnvInputs(resourceWithParentKey) | ||
if err != nil { | ||
return ctx.Error(CustomKeyValidationErrorKeyPath, err.Error()) | ||
} | ||
|
||
env, err := cel.NewEnv(inputs...) | ||
if err != nil { | ||
return ctx.Error(CustomKeyValidationErrorKeyPath, err.Error()) | ||
} | ||
|
||
for _, celExpression := range CELDefinitionSchema.CELExpressions { | ||
ast, issues := env.Compile(celExpression.Expression) | ||
if issues != nil && issues.Err() != nil { | ||
return ctx.Error(CustomKeyValidationErrorKeyPath, "cel expression compile error: %s", issues.Err()) | ||
} | ||
|
||
prg, err := env.Program(ast) | ||
if err != nil { | ||
return ctx.Error(CustomKeyValidationErrorKeyPath, "cel program construction error: %s", err) | ||
} | ||
|
||
res1, _, err := prg.Eval(resourceWithParentKey) | ||
if err != nil { | ||
return ctx.Error(CustomKeyValidationErrorKeyPath, "cel evaluation error: %s", err) | ||
} | ||
|
||
if res1.Type().TypeName() != "bool" { | ||
return ctx.Error(CustomKeyValidationErrorKeyPath, "cel expression needs to return a boolean") | ||
} | ||
|
||
celReturnValue, ok := res1.Value().(bool) | ||
if !ok { | ||
return ctx.Error(CustomKeyValidationErrorKeyPath, "cel expression needs to return a boolean") | ||
} | ||
if !celReturnValue { | ||
return ctx.Error(CELDefinitionCustomKey, "values in data value %v do not match", dataValue) | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
type CELExpression struct { | ||
Expression string `json:"expression"` | ||
} | ||
|
||
type CELDefinition struct { | ||
CELExpressions []CELExpression | ||
} | ||
|
||
func convertCustomKeyCELDefinitionSchemaToCELDefinitionSchema(CELDefinitionSchema CustomKeyCELDefinitionSchema) (*CELDefinition, error) { | ||
var CELDefinition CELDefinition | ||
for _, CELExpressionFromSchema := range CELDefinitionSchema { | ||
var CELExpression CELExpression | ||
b, err := json.Marshal(CELExpressionFromSchema) | ||
if err != nil { | ||
return nil, fmt.Errorf("CELExpression failed to marshal to json, %s", err.Error()) | ||
} | ||
err = json.Unmarshal(b, &CELExpression) | ||
if err != nil { | ||
return nil, fmt.Errorf("CELExpression must be an object of type CELExpression %s", err.Error()) | ||
} | ||
CELDefinition.CELExpressions = append(CELDefinition.CELExpressions, CELExpression) | ||
} | ||
|
||
return &CELDefinition, nil | ||
} | ||
|
||
func getCELEnvInputs(dataValue map[string]interface{}) ([]cel.EnvOption, error) { | ||
inputVars := make([]cel.EnvOption, 0, len(dataValue)) | ||
for input := range dataValue { | ||
inputVars = append(inputVars, cel.Variable(input, cel.DynType)) | ||
} | ||
return inputVars, nil | ||
} |
8 changes: 8 additions & 0 deletions
8
pkg/jsonSchemaValidator/test_fixtures/invalid-cel-definition-expression.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
{ | ||
"CELDefinition": [ | ||
{ | ||
"expression": "hassss(object.metadata.labels) && has(object.metadata.labels.billing)", | ||
"message": "deployment labels should contain billing label" | ||
} | ||
] | ||
} |
8 changes: 8 additions & 0 deletions
8
pkg/jsonSchemaValidator/test_fixtures/invalid-cel-definition.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
{ | ||
"CELDefinition": [ | ||
{ | ||
"expression": 1, | ||
"message": "deployment labels should contain billing label" | ||
} | ||
] | ||
} |
File renamed without changes.
File renamed without changes.
8 changes: 8 additions & 0 deletions
8
pkg/jsonSchemaValidator/test_fixtures/valid-cel-definition.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
{ | ||
"CELDefinition": [ | ||
{ | ||
"expression": "has(object.metadata.labels) && has(object.metadata.labels.billing)", | ||
"message": "deployment labels should contain billing label" | ||
} | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why
Marshal
and thenUnmarshal
? if you just need a type conversion then use Go's type conversion. Am I missing something?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok let's see, what is your suggestion?