-
Notifications
You must be signed in to change notification settings - Fork 4
/
type_expressions.go
188 lines (147 loc) · 5.78 KB
/
type_expressions.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
/*
Copyright 2021-2023 ICS-FORTH.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package v1alpha1
import (
"regexp"
"strings"
"text/template"
"github.com/Knetic/govaluate"
"github.com/Masterminds/sprig/v3"
"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/util/json"
)
// ConditionalExpr is a source of information about whether the state of the workflow after a given time is correct or not.
// This is needed because some scenarios may run in infinite-horizons.
type ConditionalExpr struct {
// Metrics set a Grafana alert that will be triggered once the condition is met.
// Parsing:
// Grafana URL: http://grafana/d/A2EjFbsMk/ycsb-services?editPanel=86
// metrics: A2EjFbsMk/86/Average (Panel/Dashboard/Metric)
//
// +optional
// +nullable
Metrics ExprMetrics `json:"metrics,omitempty"`
// State describe the runtime condition that should be met after the action has been executed
// Shall be defined using .Lifecycle() methods. The methods account only jobs that are managed by the object.
// +optional
// +nullable
State ExprState `json:"state,omitempty"`
}
func (in *ConditionalExpr) IsZero() bool {
return in == nil || *in == (ConditionalExpr{})
}
func (in *ConditionalExpr) HasMetricsExpr() bool {
return in != nil && in.Metrics != ""
}
func (in *ConditionalExpr) HasStateExpr() bool {
return in != nil && in.State != ""
}
/*
Validate State Expressions
*/
// +kubebuilder:object:generate=false
var sprigFuncMap = sprig.TxtFuncMap() // a singleton for better performance
type ExprState string
// Evaluate will evaluate the expression using the golang's templates enriched with the spring func map.
func (expr ExprState) Evaluate(state interface{}) (string, error) {
if expr == "" || state == nil {
return "", nil
}
// Parse the expression
t, err := template.New("").Funcs(sprigFuncMap).Option("missingkey=error").Parse(string(expr))
if err != nil {
return "", errors.Wrapf(err, "parsing error")
}
// Access the state fields and substitute the output.
var out strings.Builder
// pretty retarded way to support lower-case macros e.g, {{.inputs.parameters.}}
// The StateAggregationFunctions is an exception as need the param to be in the form {{.NumSuccessfulJobs}}.
if _, ok := state.(StateAggregationFunctions); !ok {
var lowercase map[string]interface{}
tmp, err := json.Marshal(state)
if err != nil {
return "", errors.Wrapf(err, "unable to create lowercase version (Marshal)")
}
if err := json.Unmarshal(tmp, &lowercase); err != nil {
return "", errors.Wrapf(err, "unable to create lowercase version (Unmarshal)")
}
state = lowercase
}
if err := t.Execute(&out, state); err != nil {
return "", errors.Wrapf(err, "malformed inputs. Available: %v", state)
}
return out.String(), nil
}
// GoValuate wraps the Evaluate function to the GoValuate expressions.
func (expr ExprState) GoValuate(state interface{}) (bool, error) {
if expr == "" {
return true, nil
}
out, err := expr.Evaluate(state)
if err != nil {
return false, errors.Wrapf(err, "dereference error")
}
expression, err := govaluate.NewEvaluableExpression(out)
if err != nil {
return false, errors.Wrapf(err, "invalid expression '%s'", expr)
}
// The following loop converts govaluate variables (which we don't use), into strings. This
// allows us to have expressions like: "foo != bar" without requiring foo and bar to be quoted.
tokens := expression.Tokens()
for i, tok := range tokens {
switch tok.Kind {
case govaluate.VARIABLE:
tok.Kind = govaluate.STRING
default:
continue
}
tokens[i] = tok
}
expression, err = govaluate.NewEvaluableExpressionFromTokens(tokens)
if err != nil {
return false, errors.Wrapf(err, "failed to parse expression '%s'", expr)
}
result, err := expression.Evaluate(nil)
if err != nil {
return false, errors.Wrapf(err, "failed to evaluate expresion '%s'", expr)
}
boolRes, ok := result.(bool)
if !ok {
return false, errors.Errorf("expected boolean evaluation for '%s'. Got %v", expr, result)
}
return boolRes, nil
}
/*
Validate Metrics Expressions
*/
// +kubebuilder:object:generate=false
// ExprMetricsValidator expressions evaluated with https://regex101.com/r/bjPwQK/1
var ExprMetricsValidator = regexp.MustCompile(`(?m)^(?P<reducer>\w+)\(\)\s+of\s+query\((?P<dashboardUID>\w+)\/(?P<panelID>\d+)\/(?P<metric>.+),\s+(?P<from>\w+),\s+(?P<to>\w+)\)\s+is\s+(?P<evaluator>\w+)\((?P<params>-*\d*[\.,\s]*\d*\w*)\)\s*(for\s+\((?P<for>\w+)\))*\s*(every\((?P<every>\w+)\))*\s*$`)
type ExprMetrics string
func (query ExprMetrics) Parse() ([]string, error) {
matches := ExprMetricsValidator.FindStringSubmatch(string(query))
if len(matches) == 0 {
return nil, errors.Errorf(`erroneous query '%s'.
Examples:
- 'avg() of query(wpFnYRwGk/2/bitrate, 15m, now) is below(14)'
- 'avg() of query(wpFnYRwGk/2/bitrate, 15m, now) is below(0.4)'
- 'avg() of query(wpFnYRwGk/2/bitrate, 15m, now) is novalue()'
- 'avg() of query(wpFnYRwGk/2/bitrate, 15m, now) is withinrange(4, 88)'
- 'avg() of query(wpFnYRwGk/2/bitrate, 15m, now) is withinrange(4, 88) for (1m)'
- 'avg() of query(wpFnYRwGk/2/bitrate, 15m, now) is withinrange(4, 88) for (1m) every(1m)'
- 'avg() of query(summary/152/tx-avg, 1m, now) is below(5000)'
- 'avg() of query(summary/152/tx-avg, 1m, now) is below(-5000)'
Prepare your expressions at: https://regex101.com/r/8JrgyI/1`, query)
}
return matches, nil
}