From 796f684a8857abf618c2f07749e79862a0f3805b Mon Sep 17 00:00:00 2001 From: Yash Mehrotra Date: Wed, 23 Aug 2023 19:36:19 +0530 Subject: [PATCH] feat: add k8s health support for cel --- {cel => celext}/cel_test.go | 43 +++++++++++++++++------ celext/celfuncs.go | 69 +++++++++++++++++++++++++++++++++++++ template.go | 18 ++-------- 3 files changed, 103 insertions(+), 27 deletions(-) rename {cel => celext}/cel_test.go (61%) create mode 100644 celext/celfuncs.go diff --git a/cel/cel_test.go b/celext/cel_test.go similarity index 61% rename from cel/cel_test.go rename to celext/cel_test.go index e941d2bf6..114286f53 100644 --- a/cel/cel_test.go +++ b/celext/cel_test.go @@ -1,9 +1,10 @@ -package cel +package celext import ( + "fmt" "testing" - "github.com/flanksource/gomplate/v3/funcs" + "github.com/flanksource/gomplate/v3/k8s" "github.com/google/cel-go/cel" "github.com/stretchr/testify/assert" ) @@ -14,8 +15,8 @@ func panIf(err error) { } } -func executeTemplate(t *testing.T, i int, input string, output any) { - env, err := cel.NewEnv(funcs.CelEnvOption...) +func executeTemplate(t *testing.T, i int, input string, output any, environment map[string]any) { + env, err := cel.NewEnv(GetCelEnv(environment)...) panIf(err) ast, issues := env.Compile(input) @@ -23,13 +24,13 @@ func executeTemplate(t *testing.T, i int, input string, output any) { panIf(err) } - prg, err := env.Program(ast) + prg, err := env.Program(ast, cel.Globals(environment)) panIf(err) - out, _, err := prg.Eval(map[string]any{}) + out, _, err := prg.Eval(environment) panIf(err) - assert.EqualValues(t, out.Value(), output) + assert.EqualValues(t, output, out.Value(), fmt.Sprintf("Test:%d failed", i+1)) } func TestCelNamespace(t *testing.T) { @@ -44,7 +45,7 @@ func TestCelNamespace(t *testing.T) { } for i, td := range testData { - executeTemplate(t, i, td.Input, td.Output) + executeTemplate(t, i, td.Input, td.Output, nil) } } @@ -59,7 +60,7 @@ func TestCelMultipleReturns(t *testing.T) { } for i, td := range testData { - executeTemplate(t, i, td.Input, td.Outputs) + executeTemplate(t, i, td.Input, td.Outputs, nil) } } @@ -74,7 +75,7 @@ func TestCelVariadic(t *testing.T) { } for i, td := range testData { - executeTemplate(t, i, td.Input, td.Output) + executeTemplate(t, i, td.Input, td.Output, nil) } } @@ -87,6 +88,26 @@ func TestCelSliceReturn(t *testing.T) { } for i, td := range testData { - executeTemplate(t, i, td.Input, td.Output) + executeTemplate(t, i, td.Input, td.Output, nil) + } +} + +func TestCelK8s(t *testing.T) { + testData := []struct { + Input string + Output any + }{ + {Input: `k8s.is_healthy(healthy_obj)`, Output: true}, + {Input: `k8s.is_healthy(unhealthy_obj)`, Output: false}, + {Input: `k8s.health(healthy_obj).status`, Output: "Healthy"}, + {Input: `k8s.health(unhealthy_obj).message`, Output: "Back-off 40s restarting failed container=main pod=my-pod_argocd(63674389-f613-11e8-a057-fe5f49266390)"}, + } + + for i, td := range testData { + environment := map[string]any{ + "healthy_obj": k8s.TestHealthy, + "unhealthy_obj": k8s.TestUnhealthy, + } + executeTemplate(t, i, td.Input, td.Output, environment) } } diff --git a/celext/celfuncs.go b/celext/celfuncs.go new file mode 100644 index 000000000..0c6a41f7d --- /dev/null +++ b/celext/celfuncs.go @@ -0,0 +1,69 @@ +package celext + +import ( + "encoding/json" + + "github.com/flanksource/gomplate/v3/funcs" + "github.com/flanksource/gomplate/v3/k8s" + pkgStrings "github.com/flanksource/gomplate/v3/strings" + "github.com/google/cel-go/cel" + "github.com/google/cel-go/common/types" + "github.com/google/cel-go/common/types/ref" + "github.com/google/cel-go/ext" +) + +func GetCelEnv(environment map[string]any) []cel.EnvOption { + var opts []cel.EnvOption + + // Generated functions + opts = append(opts, funcs.CelEnvOption...) + + opts = append(opts, pkgStrings.CelEnvOption...) + + // load other cel-go extensions that aren't available by default + extensions := []cel.EnvOption{ext.Math(), ext.Encoders(), ext.Strings(), ext.Sets(), ext.Lists()} + opts = append(opts, extensions...) + + // Load input as variables + for k := range environment { + opts = append(opts, cel.Variable(k, cel.AnyType)) + } + + opts = append(opts, []cel.EnvOption{k8sHealth(), k8sIsHealthy()}...) + return opts +} + +func k8sHealth() cel.EnvOption { + return cel.Function("k8s.health", + cel.Overload("k8s.health_any", + []*cel.Type{cel.AnyType}, + cel.AnyType, + cel.UnaryBinding(func(obj ref.Val) ref.Val { + jsonObj, _ := toJSON(k8s.GetHealth(obj.Value())) + return types.NewDynamicMap(types.DefaultTypeAdapter, jsonObj) + }), + ), + ) +} + +func k8sIsHealthy() cel.EnvOption { + return cel.Function("k8s.is_healthy", + cel.Overload("k8s.is_healthy_any", + []*cel.Type{cel.AnyType}, + cel.StringType, + cel.UnaryBinding(func(obj ref.Val) ref.Val { + return types.Bool(k8s.GetHealth(obj.Value()).OK) + }), + ), + ) +} + +func toJSON(v any) (map[string]any, error) { + var jsonObj map[string]any + b, err := json.Marshal(v) + if err != nil { + return jsonObj, err + } + err = json.Unmarshal(b, &jsonObj) + return jsonObj, err +} diff --git a/template.go b/template.go index bdccb056f..0459efc64 100644 --- a/template.go +++ b/template.go @@ -9,12 +9,10 @@ import ( "strings" gotemplate "text/template" - "github.com/flanksource/gomplate/v3/funcs" + "github.com/flanksource/gomplate/v3/celext" _ "github.com/flanksource/gomplate/v3/js" - pkgStrings "github.com/flanksource/gomplate/v3/strings" "github.com/flanksource/mapstructure" "github.com/google/cel-go/cel" - "github.com/google/cel-go/ext" "github.com/robertkrimen/otto" "github.com/robertkrimen/otto/registry" _ "github.com/robertkrimen/otto/underscore" @@ -66,7 +64,6 @@ func RunTemplate(environment map[string]any, template Template) (string, error) if err != nil { return "", err } - data, err := serialize(environment) if err != nil { return "", err @@ -81,18 +78,7 @@ func RunTemplate(environment map[string]any, template Template) (string, error) // cel-go if template.Expression != "" { - var opts = funcs.CelEnvOption - opts = append(opts, pkgStrings.CelEnvOption...) - - // load other cel-go extensions that aren't available by default - extensions := []cel.EnvOption{ext.Math(), ext.Encoders(), ext.Strings(), ext.Sets(), ext.Lists()} - opts = append(opts, extensions...) - - for k := range environment { - opts = append(opts, cel.Variable(k, cel.AnyType)) - } - - env, err := cel.NewEnv(opts...) + env, err := cel.NewEnv(celext.GetCelEnv(environment)...) if err != nil { return "", err }