package evaluate import ( "errors" "fmt" "math" "reflect" "strconv" "time" "github.com/antonmedv/expr" "github.com/antonmedv/expr/file" "github.com/sirupsen/logrus" "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" ) func EvaluateResult(result any, metric v1alpha1.Metric, logCtx logrus.Entry) (v1alpha1.AnalysisPhase, error) { successCondition := false failCondition := false var err error if metric.SuccessCondition != "" { successCondition, err = EvalCondition(result, metric.SuccessCondition) if err != nil { return v1alpha1.AnalysisPhaseError, err } } if metric.FailureCondition != "" { failCondition, err = EvalCondition(result, metric.FailureCondition) if err != nil { return v1alpha1.AnalysisPhaseError, err } } switch { case metric.SuccessCondition == "" && metric.FailureCondition == "": //Always return success unless there is an error return v1alpha1.AnalysisPhaseSuccessful, nil case metric.SuccessCondition != "" && metric.FailureCondition == "": // Without a failure condition, a measurement is considered a failure if the measurement's success condition is not true failCondition = !successCondition case metric.SuccessCondition == "" && metric.FailureCondition != "": // Without a success condition, a measurement is considered a successful if the measurement's failure condition is not true successCondition = !failCondition } if failCondition { return v1alpha1.AnalysisPhaseFailed, nil } if !failCondition && !successCondition { return v1alpha1.AnalysisPhaseInconclusive, nil } // If we reach this code path, failCondition is false and successCondition is true return v1alpha1.AnalysisPhaseSuccessful, nil } func EvalTime(expression string) (time.Time, error) { var err error env := map[string]any{ "isNaN": math.IsNaN, "isInf": isInf, } unwrapFileErr := func(e error) error { if fileErr, ok := err.(*file.Error); ok { e = errors.New(fileErr.Message) } return e } program, err := expr.Compile(expression, expr.Env(env)) if err != nil { return time.Time{}, unwrapFileErr(err) } output, err := expr.Run(program, env) if err != nil { return time.Time{}, unwrapFileErr(err) } switch val := output.(type) { case time.Time: return val, nil default: return time.Time{}, fmt.Errorf("expected time.Time, but got %T", val) } } // EvalCondition evaluates the condition with the resultValue as an input func EvalCondition(resultValue any, condition string) (bool, error) { var err error env := map[string]any{ "result": valueFromPointer(resultValue), "asInt": asInt, "asFloat": asFloat, "isNaN": math.IsNaN, "isInf": isInf, "isNil": isNilFunc(resultValue), "default": defaultFunc(resultValue), } unwrapFileErr := func(e error) error { if fileErr, ok := err.(*file.Error); ok { e = errors.New(fileErr.Message) } return e } program, err := expr.Compile(condition, expr.Env(env)) if err != nil { return false, unwrapFileErr(err) } output, err := expr.Run(program, env) if err != nil { return false, unwrapFileErr(err) } switch val := output.(type) { case bool: return val, nil default: return false, fmt.Errorf("expected bool, but got %T", val) } } func isInf(f float64) bool { return math.IsInf(f, 0) } func asInt(in any) int64 { switch i := in.(type) { case float64: return int64(i) case float32: return int64(i) case int64: return i case int32: return int64(i) case int16: return int64(i) case int8: return int64(i) case int: return int64(i) case uint64: return int64(i) case uint32: return int64(i) case uint16: return int64(i) case uint8: return int64(i) case uint: return int64(i) case string: inAsInt, err := strconv.ParseInt(i, 10, 64) if err == nil { return inAsInt } panic(err) } panic(fmt.Sprintf("asInt() not supported on %v %v", reflect.TypeOf(in), in)) } func asFloat(in any) float64 { switch i := in.(type) { case float64: return i case float32: return float64(i) case int64: return float64(i) case int32: return float64(i) case int16: return float64(i) case int8: return float64(i) case int: return float64(i) case uint64: return float64(i) case uint32: return float64(i) case uint16: return float64(i) case uint8: return float64(i) case uint: return float64(i) case string: inAsFloat, err := strconv.ParseFloat(i, 64) if err == nil { return inAsFloat } panic(err) } panic(fmt.Sprintf("asFloat() not supported on %v %v", reflect.TypeOf(in), in)) } // Check whether two slices of type string are equal or not. func Equal(a, b []string) bool { if len(a) != len(b) { return false } left := make(map[string]bool) for _, x := range a { left[x] = true } for _, x := range b { if !left[x] { return false } } return true } func defaultFunc(resultValue any) func(any, any) any { return func(_ any, defaultValue any) any { if isNil(resultValue) { return defaultValue } return valueFromPointer(resultValue) } } func isNilFunc(resultValue any) func(any) bool { return func(_ any) bool { return isNil(resultValue) } } // isNil is courtesy of: https://gist.github.com/mangatmodi/06946f937cbff24788fa1d9f94b6b138 func isNil(in any) (out bool) { if in == nil { out = true return } switch reflect.TypeOf(in).Kind() { case reflect.Ptr, reflect.Map, reflect.Array, reflect.Chan, reflect.Slice: out = reflect.ValueOf(in).IsNil() } return } // valueFromPointer allows pointers to be passed in from the provider, but then extracts the value from // the pointer if the pointer is not nil, else returns nil func valueFromPointer(in any) (out any) { if isNil(in) { return } if reflect.TypeOf(in).Kind() != reflect.Ptr { out = in return } out = reflect.ValueOf(in).Elem().Interface() return }