mirror of
https://github.com/hatchet-dev/hatchet.git
synced 2026-01-05 16:19:43 -06:00
* feat: add table, wire up partitioning * feat: wire failures into the OLAP db from rabbit * feat: bubble failures up to controller * fix: naming * fix: hack around enum type * fix: typo Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix: typos * fix: migration name * feat: log debug failure * feat: pub message from debug endpoint to log failure * fix: error handling * fix: use ingestor * fix: olap suffix * fix: pass source through * fix: dont log ingest failure * fix: rm debug as enum opt * chore: gen * Feat: Webhooks (#1978) * feat: migration + go gen * feat: non unique source name * feat: api types * fix: rm cruft * feat: initial api for webhooks * feat: handle encryption of incoming keys * fix: nil pointer errors * fix: import * feat: add endpoint for incoming webhooks * fix: naming * feat: start wiring up basic auth * feat: wire up cel event parsing * feat: implement authentication * fix: hack for plain text content * feat: add source to enum * feat: add source name enum * feat: db source name enum fix * fix: use source name enums * feat: nest sources * feat: first pass at stripe * fix: clean up source name passing * fix: use unique name for webhook * feat: populator test * fix: null values * fix: ordering * fix: rm unnecessary index * fix: validation * feat: validation on create * fix: lint * fix: naming * feat: wire triggering webhook name through to events table * feat: cleanup + python gen + e2e test for basic auth * feat: query to insert webhook validation errors * refactor: auth handler * fix: naming * refactor: validation errors, part II * feat: wire up writes through olap * fix: linting, fallthrough case * fix: validation * feat: tests for failure cases for basic auth * feat: expand tests * fix: correctly return 404 out of task getter * chore: generated stuff * fix: rm cruft * fix: longer sleep * debug: print name + events to logs * feat: limit to N * feat: add limit env var * debug: ci test * fix: apply namespaces to keys * fix: namespacing, part ii * fix: sdk config * fix: handle prefixing * feat: handle partitioning logic * chore: gen * feat: add webhook limit * feat: wire up limits * fix: gen * fix: reverse order of generic fallthrough * fix: comment for potential unexpected behavior * fix: add check constraints, improve error handling * chore: gen * chore: gen * fix: improve naming * feat: scaffold webhooks page * feat: sidebar * feat: first pass at page * feat: improve feedback on UI * feat: initial work on create modal * feat: change default to basic * fix: openapi spec discriminated union * fix: go side * feat: start wiring up placeholders for stripe and github * feat: pre-populated fields for Stripe + Github * feat: add name section * feat: copy improvements, show URL * feat: UI cleanup * fix: check if tenant populator errors * feat: add comments * chore: gen again * fix: default name * fix: styling * fix: improve stripe header processing * feat: docs, part 1 * fix: lint * fix: migration order * feat: implement rate limit per-webhook * feat: comment * feat: clean up docs * chore: gen * fix: migration versions * fix: olap naming * fix: partitions * chore: gen * feat: store webhook cel eval failures properly * fix: pk order * fix: auth tweaks, move fetches out of populator * fix: pgtype.Text instead of string pointer * chore: gen --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
393 lines
9.4 KiB
Go
393 lines
9.4 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)),
|
|
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 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.eventEnv.Compile(expr)
|
|
|
|
if issues != nil && issues.Err() != nil {
|
|
return "", fmt.Errorf("failed to compile expression: %w", issues.Err())
|
|
}
|
|
|
|
program, err := p.eventEnv.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
|
|
}
|