-
Notifications
You must be signed in to change notification settings - Fork 34
/
notify.go
218 lines (174 loc) · 5.51 KB
/
notify.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
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
package base
import (
"bytes"
"errors"
"io"
"net/http"
"strings"
"time"
"github.com/iter8-tools/iter8/base/log"
)
// notifyInputs is the input to the notify task
type notifyInputs struct {
// URL is the URL of the notification hook
URL string `json:"url" yaml:"url"`
// Method is the HTTP method that needs to be used
Method string `json:"method,omitempty" yaml:"method,omitempty"`
// Params is the set of HTTP parameters that need to be sent
Params map[string]string `json:"params,omitempty" yaml:"params,omitempty"`
// Headers is the set of HTTP headers that need to be sent
Headers map[string]string `json:"headers,omitempty" yaml:"headers,omitempty"`
// URL is the URL of the request payload template that should be used
PayloadTemplateURL string `json:"payloadTemplateURL,omitempty" yaml:"payloadTemplateURL,omitempty"`
// SoftFailure indicates the task and experiment should not fail if the task
// cannot successfully send a request to the notification hook
SoftFailure bool `json:"softFailure" yaml:"softFailure"`
}
const (
// NotifyTaskName is the task name
NotifyTaskName = "notify"
)
// notifyTask sends notifications
type notifyTask struct {
TaskMeta
With notifyInputs `json:"with" yaml:"with"`
}
// Summary is the data that is given to the payload template
// Summary is a subset of the data contained in Experiment
type Summary struct {
// Timestamp is when the summary was created
// For example: 2022-08-09 15:10:36.569745 -0400 EDT m=+12.599643189
TimeStamp string `json:"timeStamp" yaml:"timeStamp"`
// Completed is whether or not the experiment has completed
Completed bool `json:"completed" yaml:"completed"`
// NoTaskFailures is whether or not the experiment had any tasks that failed
NoTaskFailures bool `json:"noTaskFailures" yaml:"noTaskFailures"`
// NumTasks is the number of tasks in the experiment
NumTasks int `json:"numTasks" yaml:"numTasks"`
// NumCompletedTasks is the number of completed tasks in the experiment
NumCompletedTasks int `json:"numCompletedTasks" yaml:"numCompletedTasks"`
// Experiment is the experiment struct
Experiment *Experiment `json:"experiment" yaml:"experiment"`
}
// getSummary gets the values for the payload template
func getSummary(exp *Experiment) map[string]Summary {
return map[string]Summary{
"Summary": {
TimeStamp: time.Now().String(),
Completed: exp.Completed(),
NoTaskFailures: exp.NoFailure(),
NumTasks: len(exp.Spec),
NumCompletedTasks: exp.Result.NumCompletedTasks,
Experiment: &Experiment{
Metadata: ExperimentMetadata{
Name: exp.Metadata.Name,
Namespace: exp.Metadata.Namespace,
},
},
},
}
}
// getPayload fetches the payload template from the PayloadTemplateURL and
// executes it with values from getSummary()
func (t *notifyTask) getPayload(exp *Experiment) (string, error) {
if t.With.PayloadTemplateURL != "" {
template, err := getTextTemplateFromURL(t.With.PayloadTemplateURL)
if err != nil {
return "", err
}
values := getSummary(exp)
// get the metrics spec
var buf bytes.Buffer
err = template.Execute(&buf, values)
if err != nil {
log.Logger.Error("could not execute payload template")
return "", err
}
return buf.String(), nil
}
return "", nil
}
// initializeDefaults sets default values
func (t *notifyTask) initializeDefaults() {
// set default HTTP method
if t.With.Method == "" {
if t.With.PayloadTemplateURL != "" {
t.With.Method = http.MethodPost
} else {
t.With.Method = http.MethodGet
}
}
}
// validate task inputs
func (t *notifyTask) validateInputs() error {
if t.With.URL == "" {
return errors.New("no URL was provided for notify task")
}
return nil
}
// run executes this task
func (t *notifyTask) run(exp *Experiment) error {
// validate inputs
err := t.validateInputs()
if err != nil {
return err
}
// initialize defaults
t.initializeDefaults()
var requestBody io.Reader
log.Logger.Debug("method: ", t.With.Method, " URL: ", t.With.URL)
if t.With.PayloadTemplateURL != "" {
payload, err := t.getPayload(exp)
if err != nil {
log.Logger.Error("could not get payload")
return err
}
log.Logger.Debug("add payload: ", string(payload))
requestBody = strings.NewReader(payload)
}
// create a new HTTP request
req, err := http.NewRequest(t.With.Method, t.With.URL, requestBody)
if err != nil {
log.Logger.Error("could not create HTTP request for notify task: ", err)
if t.With.SoftFailure {
return nil
}
return err
}
// iterate through headers
for headerName, headerValue := range t.With.Headers {
req.Header.Add(headerName, headerValue)
log.Logger.Debug("add header: ", headerName, ", value: ", headerValue)
}
// add query params
q := req.URL.Query()
for key, value := range t.With.Params {
q.Add(key, value)
log.Logger.Debug("add param: ", key, ", value: ", value)
}
req.URL.RawQuery = q.Encode()
// send request
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
log.Logger.Error("could not send HTTP request for notify task: ", err)
if t.With.SoftFailure {
return nil
}
return err
}
defer func() {
_ = resp.Body.Close()
}()
if !t.With.SoftFailure && (resp.StatusCode < 200 || resp.StatusCode > 299) {
return errors.New("did not receive successful status code for notify task")
}
// read response responseBody
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
log.Logger.Error("could not read response body from notification request", err)
return nil
}
log.Logger.Debug("response body: ", string(responseBody))
return nil
}