mirror of
https://github.com/hatchet-dev/hatchet.git
synced 2025-12-30 21:29:44 -06:00
* feat: webhook update * feat: add headers to cel env * fix: header casing * feat: wire up edits * fix: updates * fix: finish wiring up updates * fix: handle save on enter * fix: lint * feat: add slack and discord * feat: initial slack setup * fix: get slack working * fix: rm discord for now * fix: lint * chore: gen * fix: explicit save button * feat: add link to CEL docs * feat: add callout for reaching out to support * feat: docs * refactor: challenge * fix: naming * fix: return * fix: resp codes * fix: webhooks beta flag * fix: rm discord * fix: docs
400 lines
9.6 KiB
Go
400 lines
9.6 KiB
Go
package cel
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"fmt"
|
|
|
|
"github.com/google/cel-go/cel"
|
|
"github.com/google/cel-go/checker/decls"
|
|
"github.com/google/cel-go/common/types"
|
|
"github.com/google/cel-go/common/types/ref"
|
|
"github.com/google/cel-go/ext"
|
|
|
|
"github.com/hatchet-dev/hatchet/pkg/repository/postgres/dbsqlc"
|
|
"github.com/hatchet-dev/hatchet/pkg/repository/v1/sqlcv1"
|
|
|
|
expr "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
|
|
)
|
|
|
|
type CELParser struct {
|
|
workflowStrEnv *cel.Env
|
|
stepRunEnv *cel.Env
|
|
eventEnv *cel.Env
|
|
incomingWebhookEnv *cel.Env
|
|
}
|
|
|
|
var checksumDecl = decls.NewFunction("checksum",
|
|
decls.NewOverload("checksum_string",
|
|
[]*expr.Type{decls.String},
|
|
decls.String,
|
|
),
|
|
)
|
|
|
|
var checksum = cel.Function("checksum",
|
|
cel.MemberOverload(
|
|
"checksum_string_impl",
|
|
[]*cel.Type{cel.StringType},
|
|
cel.StringType,
|
|
cel.FunctionBinding(func(args ...ref.Val) ref.Val {
|
|
if len(args) != 1 {
|
|
return types.NewErr("checksum requires 1 argument")
|
|
}
|
|
str, ok := args[0].(types.String)
|
|
if !ok {
|
|
return types.NewErr("argument must be a string")
|
|
}
|
|
hash := sha256.Sum256([]byte(str))
|
|
return types.String(fmt.Sprintf("%x", hash))
|
|
})),
|
|
)
|
|
|
|
func NewCELParser() *CELParser {
|
|
workflowStrEnv, _ := cel.NewEnv(
|
|
cel.Declarations(
|
|
decls.NewVar("input", decls.NewMapType(decls.String, decls.Dyn)),
|
|
decls.NewVar("additional_metadata", decls.NewMapType(decls.String, decls.Dyn)),
|
|
decls.NewVar("workflow_run_id", decls.String),
|
|
checksumDecl,
|
|
),
|
|
checksum,
|
|
ext.Strings(),
|
|
)
|
|
|
|
stepRunEnv, _ := cel.NewEnv(
|
|
cel.Declarations(
|
|
decls.NewVar("input", decls.NewMapType(decls.String, decls.Dyn)),
|
|
decls.NewVar("additional_metadata", decls.NewMapType(decls.String, decls.Dyn)),
|
|
decls.NewVar("parents", decls.NewMapType(decls.String, decls.NewMapType(decls.String, decls.Dyn))),
|
|
decls.NewVar("workflow_run_id", decls.String),
|
|
checksumDecl,
|
|
),
|
|
checksum,
|
|
ext.Strings(),
|
|
)
|
|
|
|
eventEnv, _ := cel.NewEnv(
|
|
cel.Declarations(
|
|
decls.NewVar("input", decls.NewMapType(decls.String, decls.Dyn)),
|
|
decls.NewVar("additional_metadata", decls.NewMapType(decls.String, decls.Dyn)),
|
|
decls.NewVar("payload", decls.NewMapType(decls.String, decls.Dyn)),
|
|
decls.NewVar("event_id", decls.String),
|
|
decls.NewVar("event_key", decls.String),
|
|
checksumDecl,
|
|
),
|
|
ext.Strings(),
|
|
)
|
|
|
|
incomingWebhookEnv, _ := cel.NewEnv(
|
|
cel.Declarations(
|
|
decls.NewVar("input", decls.NewMapType(decls.String, decls.Dyn)),
|
|
decls.NewVar("headers", decls.NewMapType(decls.String, decls.String)),
|
|
checksumDecl,
|
|
),
|
|
)
|
|
|
|
return &CELParser{
|
|
workflowStrEnv: workflowStrEnv,
|
|
stepRunEnv: stepRunEnv,
|
|
eventEnv: eventEnv,
|
|
incomingWebhookEnv: incomingWebhookEnv,
|
|
}
|
|
}
|
|
|
|
type Input map[string]interface{}
|
|
|
|
type InputOpts func(Input)
|
|
|
|
func WithInput(input map[string]interface{}) InputOpts {
|
|
return func(w Input) {
|
|
w["input"] = input
|
|
}
|
|
}
|
|
|
|
func WithHeaders(headers map[string]string) InputOpts {
|
|
return func(w Input) {
|
|
w["headers"] = headers
|
|
}
|
|
}
|
|
|
|
func WithParents(parents any) InputOpts {
|
|
return func(w Input) {
|
|
w["parents"] = parents
|
|
}
|
|
}
|
|
|
|
func WithAdditionalMetadata(metadata map[string]interface{}) InputOpts {
|
|
return func(w Input) {
|
|
w["additional_metadata"] = metadata
|
|
}
|
|
}
|
|
|
|
func WithWorkflowRunID(workflowRunID string) InputOpts {
|
|
return func(w Input) {
|
|
w["workflow_run_id"] = workflowRunID
|
|
}
|
|
}
|
|
|
|
func WithPayload(payload map[string]interface{}) InputOpts {
|
|
return func(w Input) {
|
|
w["payload"] = payload
|
|
}
|
|
}
|
|
|
|
func WithEventID(eventID string) InputOpts {
|
|
return func(w Input) {
|
|
w["event_id"] = eventID
|
|
}
|
|
}
|
|
|
|
func WithEventKey(key string) InputOpts {
|
|
return func(w Input) {
|
|
w["event_key"] = key
|
|
}
|
|
}
|
|
|
|
func NewInput(opts ...InputOpts) Input {
|
|
res := make(map[string]interface{})
|
|
|
|
for _, opt := range opts {
|
|
opt(res)
|
|
}
|
|
|
|
return res
|
|
}
|
|
|
|
func (p *CELParser) ParseWorkflowString(workflowExp string) (cel.Program, error) {
|
|
ast, issues := p.workflowStrEnv.Compile(workflowExp)
|
|
|
|
if issues != nil && issues.Err() != nil {
|
|
return nil, issues.Err()
|
|
}
|
|
|
|
return p.workflowStrEnv.Program(ast)
|
|
}
|
|
|
|
func (p *CELParser) ParseAndEvalWorkflowString(workflowExp string, in Input) (string, error) {
|
|
prg, err := p.ParseWorkflowString(workflowExp)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
var inMap map[string]interface{} = in
|
|
|
|
out, _, err := prg.Eval(inMap)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// Switch on the type of the output.
|
|
switch out.Type() {
|
|
case types.StringType:
|
|
return out.Value().(string), nil
|
|
default:
|
|
return "", fmt.Errorf("output must evaluate to a string: got %s", out.Type().TypeName())
|
|
}
|
|
}
|
|
|
|
type StepRunOutType string
|
|
|
|
const (
|
|
StepRunOutTypeString StepRunOutType = "string"
|
|
StepRunOutTypeInt StepRunOutType = "int"
|
|
)
|
|
|
|
type StepRunOut struct {
|
|
String *string
|
|
Int *int
|
|
Type StepRunOutType
|
|
}
|
|
|
|
func (p *CELParser) ParseStepRun(stepRunExpr string) (cel.Program, error) {
|
|
ast, issues := p.stepRunEnv.Compile(stepRunExpr)
|
|
|
|
if issues != nil && issues.Err() != nil {
|
|
return nil, issues.Err()
|
|
}
|
|
|
|
return p.stepRunEnv.Program(ast)
|
|
}
|
|
|
|
func (p *CELParser) ParseAndEvalStepRun(stepRunExpr string, in Input) (*StepRunOut, error) {
|
|
prg, err := p.ParseWorkflowString(stepRunExpr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var inMap map[string]interface{} = in
|
|
|
|
out, _, err := prg.Eval(inMap)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
res := &StepRunOut{}
|
|
|
|
switch out.Type() {
|
|
case cel.StringType:
|
|
str := out.Value().(string)
|
|
res.String = &str
|
|
res.Type = StepRunOutTypeString
|
|
case cel.IntType:
|
|
i := int(out.Value().(int64))
|
|
res.Int = &i
|
|
res.Type = StepRunOutTypeInt
|
|
case cel.DoubleType:
|
|
i := int(out.Value().(float64))
|
|
res.Int = &i
|
|
res.Type = StepRunOutTypeInt
|
|
default:
|
|
return nil, fmt.Errorf("output must evaluate to a string or integer: got %s", out.Type().TypeName())
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
func (p *CELParser) CheckStepRunOutAgainstKnown(out *StepRunOut, knownType dbsqlc.StepExpressionKind) error {
|
|
switch knownType {
|
|
case dbsqlc.StepExpressionKindDYNAMICRATELIMITKEY:
|
|
if out.String == nil {
|
|
prefix := "expected string output for dynamic rate limit key"
|
|
|
|
if out.Int != nil {
|
|
return fmt.Errorf("%s, got int", prefix)
|
|
}
|
|
|
|
return fmt.Errorf("%s, got unknown type", prefix)
|
|
}
|
|
case dbsqlc.StepExpressionKindDYNAMICRATELIMITVALUE:
|
|
if out.Int == nil {
|
|
prefix := "expected int output for dynamic rate limit value"
|
|
|
|
if out.String != nil {
|
|
return fmt.Errorf("%s, got string", prefix)
|
|
}
|
|
|
|
return fmt.Errorf("%s, got unknown type", prefix)
|
|
}
|
|
case dbsqlc.StepExpressionKindDYNAMICRATELIMITWINDOW:
|
|
if out.String == nil {
|
|
prefix := "expected string output for dynamic rate limit window"
|
|
|
|
if out.Int != nil {
|
|
return fmt.Errorf("%s, got int", prefix)
|
|
}
|
|
|
|
return fmt.Errorf("%s, got unknown type", prefix)
|
|
}
|
|
case dbsqlc.StepExpressionKindDYNAMICRATELIMITUNITS:
|
|
if out.Int == nil {
|
|
prefix := "expected int output for dynamic rate limit units"
|
|
|
|
if out.String != nil {
|
|
return fmt.Errorf("%s, got string", prefix)
|
|
}
|
|
|
|
return fmt.Errorf("%s, got unknown type", prefix)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *CELParser) CheckStepRunOutAgainstKnownV1(out *StepRunOut, knownType sqlcv1.StepExpressionKind) error {
|
|
switch knownType {
|
|
case sqlcv1.StepExpressionKindDYNAMICRATELIMITKEY:
|
|
if out.String == nil {
|
|
prefix := "expected string output for dynamic rate limit key"
|
|
|
|
if out.Int != nil {
|
|
return fmt.Errorf("%s, got int", prefix)
|
|
}
|
|
|
|
return fmt.Errorf("%s, got unknown type", prefix)
|
|
}
|
|
case sqlcv1.StepExpressionKindDYNAMICRATELIMITVALUE:
|
|
if out.Int == nil {
|
|
prefix := "expected int output for dynamic rate limit value"
|
|
|
|
if out.String != nil {
|
|
return fmt.Errorf("%s, got string", prefix)
|
|
}
|
|
|
|
return fmt.Errorf("%s, got unknown type", prefix)
|
|
}
|
|
case sqlcv1.StepExpressionKindDYNAMICRATELIMITWINDOW:
|
|
if out.String == nil {
|
|
prefix := "expected string output for dynamic rate limit window"
|
|
|
|
if out.Int != nil {
|
|
return fmt.Errorf("%s, got int", prefix)
|
|
}
|
|
|
|
return fmt.Errorf("%s, got unknown type", prefix)
|
|
}
|
|
case sqlcv1.StepExpressionKindDYNAMICRATELIMITUNITS:
|
|
if out.Int == nil {
|
|
prefix := "expected int output for dynamic rate limit units"
|
|
|
|
if out.String != nil {
|
|
return fmt.Errorf("%s, got string", prefix)
|
|
}
|
|
|
|
return fmt.Errorf("%s, got unknown type", prefix)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *CELParser) EvaluateEventExpression(expr string, input Input) (bool, error) {
|
|
ast, issues := p.eventEnv.Compile(expr)
|
|
|
|
if issues != nil && issues.Err() != nil {
|
|
return false, fmt.Errorf("failed to compile expression: %w", issues.Err())
|
|
}
|
|
|
|
program, err := p.eventEnv.Program(ast)
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to create program: %w", err)
|
|
}
|
|
|
|
var inMap map[string]interface{} = input
|
|
|
|
out, _, err := program.Eval(inMap)
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to evaluate expression: %w", err)
|
|
}
|
|
|
|
if out.Type() != types.BoolType {
|
|
return false, fmt.Errorf("expression did not evaluate to a boolean: got %s", out.Type().TypeName())
|
|
}
|
|
|
|
return out.Value().(bool), nil
|
|
}
|
|
|
|
func (p *CELParser) EvaluateIncomingWebhookExpression(expr string, input Input) (string, error) {
|
|
ast, issues := p.incomingWebhookEnv.Compile(expr)
|
|
|
|
if issues != nil && issues.Err() != nil {
|
|
return "", fmt.Errorf("failed to compile expression: %w", issues.Err())
|
|
}
|
|
|
|
program, err := p.incomingWebhookEnv.Program(ast)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to create program: %w", err)
|
|
}
|
|
|
|
var inMap map[string]interface{} = input
|
|
|
|
out, _, err := program.Eval(inMap)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to evaluate expression: %w", err)
|
|
}
|
|
|
|
if out.Type() != types.StringType {
|
|
return "", fmt.Errorf("expression did not evaluate to a string: got %s", out.Type().TypeName())
|
|
}
|
|
|
|
return out.Value().(string), nil
|
|
}
|