Skip to content

Commit

Permalink
engine
Browse files Browse the repository at this point in the history
Signed-off-by: Charles-Edouard Brétéché <[email protected]>
  • Loading branch information
eddycharly committed Sep 28, 2023
1 parent f4c1c19 commit fda7c15
Show file tree
Hide file tree
Showing 12 changed files with 246 additions and 43 deletions.
35 changes: 13 additions & 22 deletions pkg/commands/root.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
package commands

import (
"errors"
"fmt"

"github.com/eddycharly/tf-kyverno/pkg/engine"
"github.com/eddycharly/tf-kyverno/pkg/plan"
"github.com/eddycharly/tf-kyverno/pkg/policy"
tfengine "github.com/eddycharly/tf-kyverno/pkg/tf-engine"
"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/output/pluralize"
"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
Expand All @@ -30,27 +29,19 @@ func (c *command) Run(cmd *cobra.Command, _ []string) error {
if err != nil {
return err
}
resources, ok, err := unstructured.NestedSlice(plan, "planned_values", "root_module", "resources")
if err != nil {
return err
}
if !ok {
return errors.New("failed to find resources in the plan")
}
fmt.Fprintln(out, "-", len(resources), pluralize.Pluralize(len(resources), "resource", "resources"), "loaded")
fmt.Fprintln(out, "-", len(plan.Resources), pluralize.Pluralize(len(plan.Resources), "resource", "resources"), "loaded")
fmt.Fprintln(out, "Running ...")
// TODO
for _, resource := range resources {
resourceName, _, _ := unstructured.NestedString(resource.(map[string]interface{}), "address")
for _, policy := range policies {
for _, rule := range policy.Spec.Rules {
match, exclude := engine.MatchExclude(rule.MatchResources, rule.ExcludeResources, resource)
if match && !exclude {
fmt.Fprintln(out, "-", policy.Name, rule.Name, "matches", resourceName, "(", "match", match, ",", "exclude", exclude, ")")
} else {
fmt.Fprintln(out, "-", policy.Name, rule.Name, "doesn't match", resourceName, "(", "match", match, ",", "exclude", exclude, ")")
}
}
e := tfengine.New()
responses := e.Run(tfengine.TfEngineRequest{
Plan: plan,
Policies: policies,
})
for _, response := range responses {
resourceName, _, _ := unstructured.NestedString(response.Resource.(map[string]interface{}), "address")
if response.Error == nil {
fmt.Fprintln(out, "-", response.Policy.Name, "/", response.Rule.Name, "/", resourceName, "PASSED")
} else {
fmt.Fprintln(out, "-", response.Policy.Name, "/", response.Rule.Name, "/", resourceName, "FAILED:", response.Error)
}
}
fmt.Fprintln(out, "Done")
Expand Down
19 changes: 19 additions & 0 deletions pkg/engine/blocks/constant/constant.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package constant

import (
"github.com/eddycharly/tf-kyverno/pkg/engine"
)

type constant[TREQUEST any, TRESPONSE any] struct {
responses []TRESPONSE
}

func (b *constant[TREQUEST, TRESPONSE]) Run(_ TREQUEST) []TRESPONSE {
return b.responses
}

func New[TREQUEST any, TRESPONSE any](responses ...TRESPONSE) engine.Engine[TREQUEST, TRESPONSE] {
return &constant[TREQUEST, TRESPONSE]{
responses: responses,
}
}
19 changes: 19 additions & 0 deletions pkg/engine/blocks/function/function.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package function

import (
"github.com/eddycharly/tf-kyverno/pkg/engine"
)

type function[TREQUEST any, TRESPONSE any] struct {
function func(TREQUEST) TRESPONSE
}

func (b *function[TREQUEST, TRESPONSE]) Run(request TREQUEST) []TRESPONSE {
return []TRESPONSE{b.function(request)}
}

func New[TREQUEST any, TRESPONSE any](f func(TREQUEST) TRESPONSE) engine.Engine[TREQUEST, TRESPONSE] {
return &function[TREQUEST, TRESPONSE]{
function: f,
}
}
25 changes: 25 additions & 0 deletions pkg/engine/blocks/loop/loop.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package loop

import (
"github.com/eddycharly/tf-kyverno/pkg/engine"
)

type loop[TPARENT any, TCHILD any, TRESPONSE any] struct {
inner engine.Engine[TCHILD, TRESPONSE]
looper func(TPARENT) []TCHILD
}

func (b *loop[TPARENT, TCHILD, TRESPONSE]) Run(parent TPARENT) []TRESPONSE {
var responses []TRESPONSE
for _, child := range b.looper(parent) {
responses = append(responses, b.inner.Run(child)...)
}
return responses
}

func New[TPARENT any, TCHILD any, TRESPONSE any](inner engine.Engine[TCHILD, TRESPONSE], looper func(TPARENT) []TCHILD) engine.Engine[TPARENT, TRESPONSE] {
return &loop[TPARENT, TCHILD, TRESPONSE]{
inner: inner,
looper: looper,
}
}
15 changes: 15 additions & 0 deletions pkg/engine/blocks/null/null.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package null

import (
"github.com/eddycharly/tf-kyverno/pkg/engine"
)

type null[TREQUEST any, TRESPONSE any] struct{}

func (b *null[TREQUEST, TRESPONSE]) Run(_ TREQUEST) []TRESPONSE {
return nil
}

func New[TREQUEST any, TRESPONSE any]() engine.Engine[TREQUEST, TRESPONSE] {
return &null[TREQUEST, TRESPONSE]{}
}
24 changes: 24 additions & 0 deletions pkg/engine/blocks/predicate/predicate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package predicate

import (
"github.com/eddycharly/tf-kyverno/pkg/engine"
)

type predicate[TREQUEST any, TRESPONSE any] struct {
inner engine.Engine[TREQUEST, TRESPONSE]
predicate func(TREQUEST) bool
}

func (b *predicate[TREQUEST, TRESPONSE]) Run(request TREQUEST) []TRESPONSE {
if !b.predicate(request) {
return nil
}
return b.inner.Run(request)
}

func New[TREQUEST any, TRESPONSE any](inner engine.Engine[TREQUEST, TRESPONSE], condition func(TREQUEST) bool) engine.Engine[TREQUEST, TRESPONSE] {
return &predicate[TREQUEST, TRESPONSE]{
inner: inner,
predicate: condition,
}
}
28 changes: 28 additions & 0 deletions pkg/engine/builder/builder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package builder

import (
"github.com/eddycharly/tf-kyverno/pkg/engine"
"github.com/eddycharly/tf-kyverno/pkg/engine/blocks/constant"
"github.com/eddycharly/tf-kyverno/pkg/engine/blocks/function"
"github.com/eddycharly/tf-kyverno/pkg/engine/blocks/predicate"
)

type Engine[TREQUEST any, TRESPONSE any] struct {
engine.Engine[TREQUEST, TRESPONSE]
}

func new[TREQUEST any, TRESPONSE any](engine engine.Engine[TREQUEST, TRESPONSE]) Engine[TREQUEST, TRESPONSE] {
return Engine[TREQUEST, TRESPONSE]{engine}
}

func Constant[TREQUEST any, TRESPONSE any](responses ...TRESPONSE) Engine[TREQUEST, TRESPONSE] {
return new(constant.New[TREQUEST](responses...))
}

func (inner Engine[TREQUEST, TRESPONSE]) Predicate(condition func(TREQUEST) bool) Engine[TREQUEST, TRESPONSE] {
return new(predicate.New(inner, condition))
}

func Function[TREQUEST any, TRESPONSE any](f func(TREQUEST) TRESPONSE) Engine[TREQUEST, TRESPONSE] {
return new(function.New(f))
}
5 changes: 5 additions & 0 deletions pkg/engine/engine.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package engine

type Engine[TREQUEST any, TRESPONSE any] interface {
Run(TREQUEST) []TRESPONSE
}
26 changes: 11 additions & 15 deletions pkg/engine/match.go → pkg/match/match.go
Original file line number Diff line number Diff line change
@@ -1,51 +1,47 @@
package engine
package match

import (
"reflect"

"github.com/eddycharly/tf-kyverno/pkg/apis/v1alpha1"
)

func MatchExclude(include *v1alpha1.MatchResources, exclude *v1alpha1.MatchResources, actual interface{}) (bool, bool) {
return match(include, actual), match(exclude, actual)
}

func match(match *v1alpha1.MatchResources, actual interface{}) bool {
func MatchResources(match *v1alpha1.MatchResources, actual interface{}) bool {
if match == nil || (len(match.Any) == 0 && len(match.All) == 0) {
return false
}
if len(match.Any) != 0 {
if !matchAny(match.Any, actual) {
if !MatchAny(match.Any, actual) {
return false
}
}
if len(match.All) != 0 {
if !matchAll(match.All, actual) {
if !MatchAll(match.All, actual) {
return false
}
}
return true
}

func matchAny(filters v1alpha1.ResourceFilters, actual interface{}) bool {
func MatchAny(filters v1alpha1.ResourceFilters, actual interface{}) bool {
for _, filter := range filters {
if matchResource(filter.Resource, actual) {
if Match(filter.Resource, actual) {
return true
}
}
return false
}

func matchAll(filters v1alpha1.ResourceFilters, actual interface{}) bool {
func MatchAll(filters v1alpha1.ResourceFilters, actual interface{}) bool {
for _, filter := range filters {
if !matchResource(filter.Resource, actual) {
if !Match(filter.Resource, actual) {
return false
}
}
return true
}

func matchResource(expected, actual interface{}) bool {
func Match(expected, actual interface{}) bool {
if reflect.TypeOf(expected) != reflect.TypeOf(actual) {
return false
}
Expand All @@ -58,7 +54,7 @@ func matchResource(expected, actual interface{}) bool {
return false
}
for i := 0; i < reflect.ValueOf(expected).Len(); i++ {
if !matchResource(reflect.ValueOf(expected).Index(i).Interface(), reflect.ValueOf(actual).Index(i).Interface()) {
if !Match(reflect.ValueOf(expected).Index(i).Interface(), reflect.ValueOf(actual).Index(i).Interface()) {
return false
}
}
Expand All @@ -70,7 +66,7 @@ func matchResource(expected, actual interface{}) bool {
if !actualValue.IsValid() {
return false
}
if !matchResource(iter.Value().Interface(), actualValue.Interface()) {
if !Match(iter.Value().Interface(), actualValue.Interface()) {
return false
}
}
Expand Down
22 changes: 20 additions & 2 deletions pkg/plan/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,19 @@ package plan

import (
"encoding/json"
"errors"
"os"
"path/filepath"

"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)

func Load(path string) (map[string]interface{}, error) {
type Plan struct {
Plan map[string]interface{}
Resources []interface{}
}

func Load(path string) (*Plan, error) {
content, err := os.ReadFile(filepath.Clean(path))
if err != nil {
return nil, err
Expand All @@ -15,5 +23,15 @@ func Load(path string) (map[string]interface{}, error) {
if err := json.Unmarshal(content, &plan); err != nil {
return nil, err
}
return plan, nil
resources, ok, err := unstructured.NestedSlice(plan, "planned_values", "root_module", "resources")
if err != nil {
return nil, err
}
if !ok {
return nil, errors.New("failed to find resources in the plan")
}
return &Plan{
Plan: plan,
Resources: resources,
}, nil
}
63 changes: 63 additions & 0 deletions pkg/tf-engine/tf-engine.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package tfengine

import (
"errors"

"github.com/eddycharly/tf-kyverno/pkg/apis/v1alpha1"
"github.com/eddycharly/tf-kyverno/pkg/engine"
"github.com/eddycharly/tf-kyverno/pkg/engine/blocks/loop"
"github.com/eddycharly/tf-kyverno/pkg/engine/builder"
"github.com/eddycharly/tf-kyverno/pkg/match"
"github.com/eddycharly/tf-kyverno/pkg/plan"
)

type TfEngineRequest struct {
Plan *plan.Plan
Policies []*v1alpha1.Policy
}

type TfEngineResponse struct {
Policy *v1alpha1.Policy
Rule *v1alpha1.Rule
Resource interface{}
Error error
}

func New() engine.Engine[TfEngineRequest, TfEngineResponse] {
type request struct {
policy *v1alpha1.Policy
rule *v1alpha1.Rule
resource interface{}
}
looper := func(r TfEngineRequest) []request {
var requests []request
for _, resource := range r.Plan.Resources {
for _, policy := range r.Policies {
for _, rule := range policy.Spec.Rules {
requests = append(requests, request{
policy: policy,
rule: &rule,
resource: resource,
})
}
}
}
return requests
}
inner := builder.
Function(func(r request) TfEngineResponse {
response := TfEngineResponse{
Policy: r.policy,
Rule: r.rule,
Resource: r.resource,
}
if !match.Match(r.rule.Validation.Pattern, r.resource) {
response.Error = errors.New(r.rule.Validation.Message)
}
return response
}).
Predicate(func(r request) bool { return !match.MatchResources(r.rule.ExcludeResources, r.resource) }).
Predicate(func(r request) bool { return match.MatchResources(r.rule.MatchResources, r.resource) })
// TODO: we can't use the builder package for loops :(
return loop.New(inner, looper)
}
8 changes: 4 additions & 4 deletions policy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ spec:
any:
- resource:
type: aws_s3_bucket
exclude:
any:
- resource:
name: example
# exclude:
# any:
# - resource:
# name: example
validate:
message: 'A team tag is required for all S3 buckets'
pattern:
Expand Down

0 comments on commit fda7c15

Please sign in to comment.