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  }