github.com/iter8-tools/iter8@v1.1.2/base/notify.go (about) 1 package base 2 3 import ( 4 "bytes" 5 "errors" 6 "io" 7 "net/http" 8 "strings" 9 10 "time" 11 12 "github.com/iter8-tools/iter8/base/log" 13 ) 14 15 // notifyInputs is the input to the notify task 16 type notifyInputs struct { 17 // URL is the URL of the notification hook 18 URL string `json:"url" yaml:"url"` 19 20 // Method is the HTTP method that needs to be used 21 Method string `json:"method,omitempty" yaml:"method,omitempty"` 22 23 // Params is the set of HTTP parameters that need to be sent 24 Params map[string]string `json:"params,omitempty" yaml:"params,omitempty"` 25 26 // Headers is the set of HTTP headers that need to be sent 27 Headers map[string]string `json:"headers,omitempty" yaml:"headers,omitempty"` 28 29 // URL is the URL of the request payload template that should be used 30 PayloadTemplateURL string `json:"payloadTemplateURL,omitempty" yaml:"payloadTemplateURL,omitempty"` 31 32 // SoftFailure indicates the task and experiment should not fail if the task 33 // cannot successfully send a request to the notification hook 34 SoftFailure bool `json:"softFailure" yaml:"softFailure"` 35 } 36 37 const ( 38 // NotifyTaskName is the task name 39 NotifyTaskName = "notify" 40 ) 41 42 // notifyTask sends notifications 43 type notifyTask struct { 44 TaskMeta 45 With notifyInputs `json:"with" yaml:"with"` 46 } 47 48 // Summary is the data that is given to the payload template 49 // Summary is a subset of the data contained in Experiment 50 type Summary struct { 51 // Timestamp is when the summary was created 52 // For example: 2022-08-09 15:10:36.569745 -0400 EDT m=+12.599643189 53 TimeStamp string `json:"timeStamp" yaml:"timeStamp"` 54 55 // Completed is whether or not the experiment has completed 56 Completed bool `json:"completed" yaml:"completed"` 57 58 // NoTaskFailures is whether or not the experiment had any tasks that failed 59 NoTaskFailures bool `json:"noTaskFailures" yaml:"noTaskFailures"` 60 61 // NumTasks is the number of tasks in the experiment 62 NumTasks int `json:"numTasks" yaml:"numTasks"` 63 64 // NumCompletedTasks is the number of completed tasks in the experiment 65 NumCompletedTasks int `json:"numCompletedTasks" yaml:"numCompletedTasks"` 66 67 // Experiment is the experiment struct 68 Experiment *Experiment `json:"experiment" yaml:"experiment"` 69 } 70 71 // getSummary gets the values for the payload template 72 func getSummary(exp *Experiment) map[string]Summary { 73 return map[string]Summary{ 74 "Summary": { 75 TimeStamp: time.Now().String(), 76 Completed: exp.Completed(), 77 NoTaskFailures: exp.NoFailure(), 78 NumTasks: len(exp.Spec), 79 NumCompletedTasks: exp.Result.NumCompletedTasks, 80 Experiment: &Experiment{ 81 Metadata: ExperimentMetadata{ 82 Name: exp.Metadata.Name, 83 Namespace: exp.Metadata.Namespace, 84 }, 85 }, 86 }, 87 } 88 } 89 90 // getPayload fetches the payload template from the PayloadTemplateURL and 91 // executes it with values from getSummary() 92 func (t *notifyTask) getPayload(exp *Experiment) (string, error) { 93 if t.With.PayloadTemplateURL != "" { 94 template, err := getTextTemplateFromURL(t.With.PayloadTemplateURL) 95 if err != nil { 96 return "", err 97 } 98 99 values := getSummary(exp) 100 101 // get the metrics spec 102 var buf bytes.Buffer 103 err = template.Execute(&buf, values) 104 if err != nil { 105 log.Logger.Error("could not execute payload template") 106 return "", err 107 } 108 109 return buf.String(), nil 110 } 111 112 return "", nil 113 } 114 115 // initializeDefaults sets default values 116 func (t *notifyTask) initializeDefaults() { 117 // set default HTTP method 118 if t.With.Method == "" { 119 if t.With.PayloadTemplateURL != "" { 120 t.With.Method = http.MethodPost 121 } else { 122 t.With.Method = http.MethodGet 123 } 124 } 125 } 126 127 // validate task inputs 128 func (t *notifyTask) validateInputs() error { 129 if t.With.URL == "" { 130 return errors.New("no URL was provided for notify task") 131 } 132 133 return nil 134 } 135 136 // run executes this task 137 func (t *notifyTask) run(exp *Experiment) error { 138 // validate inputs 139 err := t.validateInputs() 140 if err != nil { 141 return err 142 } 143 144 // initialize defaults 145 t.initializeDefaults() 146 147 var requestBody io.Reader 148 149 log.Logger.Debug("method: ", t.With.Method, " URL: ", t.With.URL) 150 151 if t.With.PayloadTemplateURL != "" { 152 payload, err := t.getPayload(exp) 153 if err != nil { 154 log.Logger.Error("could not get payload") 155 return err 156 } 157 158 log.Logger.Debug("add payload: ", string(payload)) 159 160 requestBody = strings.NewReader(payload) 161 } 162 163 // create a new HTTP request 164 req, err := http.NewRequest(t.With.Method, t.With.URL, requestBody) 165 if err != nil { 166 log.Logger.Error("could not create HTTP request for notify task: ", err) 167 168 if t.With.SoftFailure { 169 return nil 170 } 171 return err 172 } 173 174 // iterate through headers 175 for headerName, headerValue := range t.With.Headers { 176 req.Header.Add(headerName, headerValue) 177 log.Logger.Debug("add header: ", headerName, ", value: ", headerValue) 178 } 179 180 // add query params 181 q := req.URL.Query() 182 for key, value := range t.With.Params { 183 q.Add(key, value) 184 log.Logger.Debug("add param: ", key, ", value: ", value) 185 } 186 req.URL.RawQuery = q.Encode() 187 188 // send request 189 client := &http.Client{} 190 resp, err := client.Do(req) 191 if err != nil { 192 log.Logger.Error("could not send HTTP request for notify task: ", err) 193 194 if t.With.SoftFailure { 195 return nil 196 } 197 return err 198 } 199 defer func() { 200 _ = resp.Body.Close() 201 }() 202 203 if !t.With.SoftFailure && (resp.StatusCode < 200 || resp.StatusCode > 299) { 204 return errors.New("did not receive successful status code for notify task") 205 } 206 207 // read response responseBody 208 responseBody, err := io.ReadAll(resp.Body) 209 if err != nil { 210 log.Logger.Error("could not read response body from notification request", err) 211 212 return nil 213 } 214 215 log.Logger.Debug("response body: ", string(responseBody)) 216 217 return nil 218 }