...

Source file src/sigs.k8s.io/release-utils/http/agent.go

Documentation: sigs.k8s.io/release-utils/http

     1  /*
     2  Copyright 2021 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package http
    18  
    19  import (
    20  	"bytes"
    21  	"fmt"
    22  	"io"
    23  	"math"
    24  	"net/http"
    25  	"time"
    26  
    27  	"github.com/sirupsen/logrus"
    28  )
    29  
    30  const (
    31  	defaultPostContentType = "application/octet-stream"
    32  )
    33  
    34  //go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -generate
    35  //go:generate /usr/bin/env bash -c "cat ../scripts/boilerplate/boilerplate.generatego.txt httpfakes/fake_agent_implementation.go > httpfakes/_fake_agent_implementation.go && mv httpfakes/_fake_agent_implementation.go httpfakes/fake_agent_implementation.go"
    36  
    37  // Agent is an http agent
    38  type Agent struct {
    39  	options *agentOptions
    40  	AgentImplementation
    41  }
    42  
    43  // AgentImplementation is the actual implementation of the http calls
    44  //
    45  //counterfeiter:generate . AgentImplementation
    46  type AgentImplementation interface {
    47  	SendPostRequest(*http.Client, string, []byte, string) (*http.Response, error)
    48  	SendGetRequest(*http.Client, string) (*http.Response, error)
    49  }
    50  
    51  type defaultAgentImplementation struct{}
    52  
    53  // agentOptions has the configurable bits of the agent
    54  type agentOptions struct {
    55  	FailOnHTTPError bool          // Set to true to fail on HTTP Status > 299
    56  	Retries         uint          // Number of times to retry when errors happen
    57  	Timeout         time.Duration // Timeout when fetching URLs
    58  	MaxWaitTime     time.Duration // Max waiting time when backing off
    59  	PostContentType string        // Content type to send when posting data
    60  }
    61  
    62  // String returns a string representation of the options
    63  func (ao *agentOptions) String() string {
    64  	return fmt.Sprintf(
    65  		"HTTP.Agent options: Timeout: %d - Retries: %d - FailOnHTTPError: %+v",
    66  		ao.Timeout, ao.Retries, ao.FailOnHTTPError,
    67  	)
    68  }
    69  
    70  var defaultAgentOptions = &agentOptions{
    71  	FailOnHTTPError: true,
    72  	Retries:         3,
    73  	Timeout:         3 * time.Second,
    74  	MaxWaitTime:     60 * time.Second,
    75  	PostContentType: defaultPostContentType,
    76  }
    77  
    78  // NewAgent return a new agent with default options
    79  func NewAgent() *Agent {
    80  	return &Agent{
    81  		AgentImplementation: &defaultAgentImplementation{},
    82  		options:             defaultAgentOptions,
    83  	}
    84  }
    85  
    86  // SetImplementation sets the agent implementation
    87  func (a *Agent) SetImplementation(impl AgentImplementation) {
    88  	a.AgentImplementation = impl
    89  }
    90  
    91  // WithTimeout sets the agent timeout
    92  func (a *Agent) WithTimeout(timeout time.Duration) *Agent {
    93  	a.options.Timeout = timeout
    94  	return a
    95  }
    96  
    97  // WithRetries sets the number of times we'll attempt to fetch the URL
    98  func (a *Agent) WithRetries(retries uint) *Agent {
    99  	a.options.Retries = retries
   100  	return a
   101  }
   102  
   103  // WithFailOnHTTPError determines if the agent fails on HTTP errors (HTTP status not in 200s)
   104  func (a *Agent) WithFailOnHTTPError(flag bool) *Agent {
   105  	a.options.FailOnHTTPError = flag
   106  	return a
   107  }
   108  
   109  // Client return an net/http client preconfigured with the agent options
   110  func (a *Agent) Client() *http.Client {
   111  	return &http.Client{
   112  		Timeout: a.options.Timeout,
   113  	}
   114  }
   115  
   116  // Get returns the body a a GET request
   117  func (a *Agent) Get(url string) (content []byte, err error) {
   118  	request, err := a.GetRequest(url)
   119  	if err != nil {
   120  		return nil, fmt.Errorf("getting GET request: %w", err)
   121  	}
   122  	defer request.Body.Close()
   123  
   124  	return a.readResponse(request)
   125  }
   126  
   127  // GetRequest sends a GET request to a URL and returns the request and response
   128  func (a *Agent) GetRequest(url string) (response *http.Response, err error) {
   129  	logrus.Debugf("Sending GET request to %s", url)
   130  	try := 0
   131  	for {
   132  		response, err = a.AgentImplementation.SendGetRequest(a.Client(), url)
   133  		try++
   134  		if err == nil || try >= int(a.options.Retries) {
   135  			return response, err
   136  		}
   137  		// Do exponential backoff...
   138  		waitTime := math.Pow(2, float64(try))
   139  		//  ... but wait no more than 1 min
   140  		if waitTime > 60 {
   141  			waitTime = a.options.MaxWaitTime.Seconds()
   142  		}
   143  		logrus.Errorf(
   144  			"Error getting URL (will retry %d more times in %.0f secs): %s",
   145  			int(a.options.Retries)-try, waitTime, err.Error(),
   146  		)
   147  		time.Sleep(time.Duration(waitTime) * time.Second)
   148  	}
   149  }
   150  
   151  // Post returns the body of a POST request
   152  func (a *Agent) Post(url string, postData []byte) (content []byte, err error) {
   153  	response, err := a.PostRequest(url, postData)
   154  	if err != nil {
   155  		return nil, fmt.Errorf("getting post request: %w", err)
   156  	}
   157  	defer response.Body.Close()
   158  
   159  	return a.readResponse(response)
   160  }
   161  
   162  // PostRequest sends the postData in a POST request to a URL and returns the request object
   163  func (a *Agent) PostRequest(url string, postData []byte) (response *http.Response, err error) {
   164  	logrus.Debugf("Sending POST request to %s", url)
   165  	try := 0
   166  	for {
   167  		response, err = a.AgentImplementation.SendPostRequest(a.Client(), url, postData, a.options.PostContentType)
   168  		try++
   169  		if err == nil || try >= int(a.options.Retries) {
   170  			return response, err
   171  		}
   172  		// Do exponential backoff...
   173  		waitTime := math.Pow(2, float64(try))
   174  		//  ... but wait no more than 1 min
   175  		if waitTime > 60 {
   176  			waitTime = a.options.MaxWaitTime.Seconds()
   177  		}
   178  		logrus.Errorf(
   179  			"Error getting URL (will retry %d more times in %.0f secs): %s",
   180  			int(a.options.Retries)-try, waitTime, err.Error(),
   181  		)
   182  		time.Sleep(time.Duration(waitTime) * time.Second)
   183  	}
   184  }
   185  
   186  // SendPostRequest sends the actual HTTP post to the server
   187  func (impl *defaultAgentImplementation) SendPostRequest(
   188  	client *http.Client, url string, postData []byte, contentType string,
   189  ) (response *http.Response, err error) {
   190  	if contentType == "" {
   191  		contentType = defaultPostContentType
   192  	}
   193  	response, err = client.Post(url, contentType, bytes.NewBuffer(postData))
   194  	if err != nil {
   195  		return response, fmt.Errorf("posting data to %s: %w", url, err)
   196  	}
   197  	return response, nil
   198  }
   199  
   200  // SendGetRequest performs the actual request
   201  func (impl *defaultAgentImplementation) SendGetRequest(client *http.Client, url string) (
   202  	response *http.Response, err error,
   203  ) {
   204  	response, err = client.Get(url)
   205  	if err != nil {
   206  		return response, fmt.Errorf("getting %s: %w", url, err)
   207  	}
   208  
   209  	return response, nil
   210  }
   211  
   212  // readResponse read an dinterpret the http request
   213  func (a *Agent) readResponse(response *http.Response) (body []byte, err error) {
   214  	// Read the response body
   215  	defer response.Body.Close()
   216  	body, err = io.ReadAll(response.Body)
   217  	if err != nil {
   218  		return nil, fmt.Errorf(
   219  			"reading the response body from %s: %w",
   220  			response.Request.URL, err,
   221  		)
   222  	}
   223  
   224  	// Check the https response code
   225  	if response.StatusCode < 200 || response.StatusCode >= 300 {
   226  		if a.options.FailOnHTTPError {
   227  			return nil, fmt.Errorf(
   228  				"HTTP error %s for %s", response.Status, response.Request.URL,
   229  			)
   230  		}
   231  		logrus.Warnf("Got HTTP error but FailOnHTTPError not set: %s", response.Status)
   232  	}
   233  	return body, err
   234  }
   235  

View as plain text