github.com/maxgio92/test-infra@v0.1.0/kubetest/boskos/client/client.go (about)

     1  /*
     2  Copyright 2017 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 client
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"encoding/json"
    23  	"errors"
    24  	"fmt"
    25  	"io"
    26  	"net"
    27  	"net/http"
    28  	"net/url"
    29  	"os"
    30  	"strings"
    31  	"sync"
    32  	"syscall"
    33  	"time"
    34  
    35  	"github.com/google/uuid"
    36  	"github.com/hashicorp/go-multierror"
    37  	"github.com/sirupsen/logrus"
    38  	utilerrors "k8s.io/apimachinery/pkg/util/errors"
    39  
    40  	"github.com/maxgio92/test-infra/kubetest/boskos/common"
    41  	"github.com/maxgio92/test-infra/kubetest/boskos/storage"
    42  	"github.com/maxgio92/test-infra/prow/config/secret"
    43  )
    44  
    45  var (
    46  	// ErrNotFound is returned by Acquire() when no resources are available.
    47  	ErrNotFound = errors.New("resources not found")
    48  	// ErrAlreadyInUse is returned by Acquire when resources are already being requested.
    49  	ErrAlreadyInUse = errors.New("resources already used by another user")
    50  	// ErrContextRequired is returned by AcquireWait and AcquireByStateWait when
    51  	// they are invoked with a nil context.
    52  	ErrContextRequired = errors.New("context required")
    53  	// ErrTypeNotFound is returned when the requested resource type (rtype) does not exist.
    54  	// For this error to be returned, you must set DistinguishNotFoundVsTypeNotFound to true.
    55  	ErrTypeNotFound = errors.New("resource type not found")
    56  )
    57  
    58  // Client defines the public Boskos client object
    59  type Client struct {
    60  	// Dialer is the net.Dialer used to establish connections to the remote
    61  	// boskos endpoint.
    62  	Dialer DialerWithRetry
    63  	// DistinguishNotFoundVsTypeNotFound, if set, will make it possible to distinguish between
    64  	// ErrNotFound and ErrTypeNotFound. For backwards-compatibility, this flag is off by
    65  	// default.
    66  	DistinguishNotFoundVsTypeNotFound bool
    67  
    68  	// http is the http.Client used to interact with the boskos REST API
    69  	http http.Client
    70  
    71  	owner       string
    72  	url         string
    73  	username    string
    74  	getPassword func() []byte
    75  	lock        sync.Mutex
    76  
    77  	storage storage.PersistenceLayer
    78  }
    79  
    80  // NewClient creates a Boskos client for the specified URL and resource owner.
    81  //
    82  // Clients created with this function default to retrying failed connection
    83  // attempts three times with a ten second pause between each attempt.
    84  func NewClient(owner string, urlString, username, passwordFile string) (*Client, error) {
    85  
    86  	if (username == "") != (passwordFile == "") {
    87  		return nil, fmt.Errorf("username and passwordFile must be specified together")
    88  	}
    89  
    90  	var getPassword func() []byte
    91  	if passwordFile != "" {
    92  		u, err := url.Parse(urlString)
    93  		if err != nil {
    94  			return nil, err
    95  		}
    96  		if u.Scheme != "https" {
    97  			// returning error here would make the tests hard
    98  			// we print out a warning message here instead
    99  			fmt.Printf("[WARNING] should NOT use password without enabling TLS: '%s'\n", urlString)
   100  		}
   101  
   102  		if err := secret.Add(passwordFile); err != nil {
   103  			logrus.WithError(err).Fatal("Failed to start secrets agent")
   104  		}
   105  		getPassword = secret.GetTokenGenerator(passwordFile)
   106  	}
   107  
   108  	return NewClientWithPasswordGetter(owner, urlString, username, getPassword)
   109  }
   110  
   111  // NewClientWithPasswordGetter creates a Boskos client for the specified URL and resource owner.
   112  //
   113  // Clients created with this function default to retrying failed connection
   114  // attempts three times with a ten second pause between each attempt.
   115  func NewClientWithPasswordGetter(owner string, urlString, username string, passwordGetter func() []byte) (*Client, error) {
   116  	client := &Client{
   117  		url:         urlString,
   118  		username:    username,
   119  		getPassword: passwordGetter,
   120  		owner:       owner,
   121  		storage:     storage.NewMemoryStorage(),
   122  	}
   123  
   124  	// Configure the dialer to attempt three additional times to establish
   125  	// a connection after a failed dial attempt. The dialer should wait 10
   126  	// seconds between each attempt.
   127  	client.Dialer.RetryCount = 3
   128  	client.Dialer.RetrySleep = time.Second * 10
   129  
   130  	// Configure the dialer and HTTP client transport to mimic the configuration
   131  	// of the http.DefaultTransport with the exception that the Dialer's Dial
   132  	// and DialContext functions are assigned to the client transport.
   133  	//
   134  	// See https://golang.org/pkg/net/http/#RoundTripper for the values
   135  	// values used for the http.DefaultTransport.
   136  	client.Dialer.Timeout = 30 * time.Second
   137  	client.Dialer.KeepAlive = 30 * time.Second
   138  	client.Dialer.DualStack = true
   139  	client.http.Transport = &http.Transport{
   140  		Proxy:                 http.ProxyFromEnvironment,
   141  		Dial:                  client.Dialer.Dial,
   142  		DialContext:           client.Dialer.DialContext,
   143  		MaxIdleConns:          100,
   144  		IdleConnTimeout:       90 * time.Second,
   145  		TLSHandshakeTimeout:   10 * time.Second,
   146  		ExpectContinueTimeout: 1 * time.Second,
   147  	}
   148  
   149  	return client, nil
   150  }
   151  
   152  // public method
   153  
   154  // Acquire asks boskos for a resource of certain type in certain state, and set the resource to dest state.
   155  // Returns the resource on success.
   156  func (c *Client) Acquire(rtype, state, dest string) (*common.Resource, error) {
   157  	return c.AcquireWithPriority(rtype, state, dest, "")
   158  }
   159  
   160  // AcquireWithPriority asks boskos for a resource of certain type in certain state, and set the resource to dest state.
   161  // Returns the resource on success.
   162  // Boskos Priority are FIFO.
   163  func (c *Client) AcquireWithPriority(rtype, state, dest, requestID string) (*common.Resource, error) {
   164  	r, err := c.acquire(rtype, state, dest, requestID)
   165  	if err != nil {
   166  		return nil, err
   167  	}
   168  	c.lock.Lock()
   169  	defer c.lock.Unlock()
   170  	if r != nil {
   171  		c.storage.Add(*r)
   172  	}
   173  
   174  	return r, nil
   175  }
   176  
   177  // AcquireWait blocks until Acquire returns the specified resource or the
   178  // provided context is cancelled or its deadline exceeded.
   179  func (c *Client) AcquireWait(ctx context.Context, rtype, state, dest string) (*common.Resource, error) {
   180  	// request with FIFO priority
   181  	requestID := uuid.New().String()
   182  	return c.AcquireWaitWithPriority(ctx, rtype, state, dest, requestID)
   183  }
   184  
   185  // AcquireWaitWithPriority blocks until Acquire returns the specified resource or the
   186  // provided context is cancelled or its deadline exceeded. This allows you to pass in a request priority.
   187  // Boskos Priority are FIFO.
   188  func (c *Client) AcquireWaitWithPriority(ctx context.Context, rtype, state, dest, requestID string) (*common.Resource, error) {
   189  	if ctx == nil {
   190  		return nil, ErrContextRequired
   191  	}
   192  	// Try to acquire the resource until available or the context is
   193  	// cancelled or its deadline exceeded.
   194  	for {
   195  		r, err := c.AcquireWithPriority(rtype, state, dest, requestID)
   196  		if err != nil {
   197  			if err == ErrAlreadyInUse || err == ErrNotFound {
   198  				select {
   199  				case <-ctx.Done():
   200  					return nil, err
   201  				case <-time.After(3 * time.Second):
   202  					continue
   203  				}
   204  			}
   205  			return nil, err
   206  		}
   207  		return r, nil
   208  	}
   209  }
   210  
   211  // AcquireByState asks boskos for a resources of certain type, and set the resource to dest state.
   212  // Returns a list of resources on success.
   213  func (c *Client) AcquireByState(state, dest string, names []string) ([]common.Resource, error) {
   214  	resources, err := c.acquireByState(state, dest, names)
   215  	if err != nil {
   216  		return nil, err
   217  	}
   218  	c.lock.Lock()
   219  	defer c.lock.Unlock()
   220  	for _, r := range resources {
   221  		c.storage.Add(r)
   222  	}
   223  	return resources, nil
   224  }
   225  
   226  // AcquireByStateWait blocks until AcquireByState returns the specified
   227  // resource(s) or the provided context is cancelled or its deadline
   228  // exceeded.
   229  func (c *Client) AcquireByStateWait(ctx context.Context, state, dest string, names []string) ([]common.Resource, error) {
   230  	if ctx == nil {
   231  		return nil, ErrContextRequired
   232  	}
   233  	// Try to acquire the resource(s) until available or the context is
   234  	// cancelled or its deadline exceeded.
   235  	for {
   236  		r, err := c.AcquireByState(state, dest, names)
   237  		if err != nil {
   238  			if err == ErrAlreadyInUse || err == ErrNotFound {
   239  				select {
   240  				case <-ctx.Done():
   241  					return nil, err
   242  				case <-time.After(3 * time.Second):
   243  					continue
   244  				}
   245  			}
   246  			return nil, err
   247  		}
   248  		return r, nil
   249  	}
   250  }
   251  
   252  // ReleaseAll returns all resources hold by the client back to boskos and set them to dest state.
   253  func (c *Client) ReleaseAll(dest string) error {
   254  	c.lock.Lock()
   255  	defer c.lock.Unlock()
   256  	resources, err := c.storage.List()
   257  	if err != nil {
   258  		return err
   259  	}
   260  	if len(resources) == 0 {
   261  		return fmt.Errorf("no holding resource")
   262  	}
   263  	var allErrors error
   264  	for _, r := range resources {
   265  		c.storage.Delete(r.Name)
   266  		err := c.Release(r.Name, dest)
   267  		if err != nil {
   268  			allErrors = multierror.Append(allErrors, err)
   269  		}
   270  	}
   271  	return allErrors
   272  }
   273  
   274  // ReleaseOne returns one of owned resources back to boskos and set it to dest state.
   275  func (c *Client) ReleaseOne(name, dest string) error {
   276  	c.lock.Lock()
   277  	defer c.lock.Unlock()
   278  
   279  	if _, err := c.storage.Get(name); err != nil {
   280  		return fmt.Errorf("no resource name %v", name)
   281  	}
   282  	c.storage.Delete(name)
   283  	if err := c.Release(name, dest); err != nil {
   284  		return err
   285  	}
   286  	return nil
   287  }
   288  
   289  // UpdateAll signals update for all resources hold by the client.
   290  func (c *Client) UpdateAll(state string) error {
   291  	c.lock.Lock()
   292  	defer c.lock.Unlock()
   293  
   294  	resources, err := c.storage.List()
   295  	if err != nil {
   296  		return err
   297  	}
   298  	if len(resources) == 0 {
   299  		return fmt.Errorf("no holding resource")
   300  	}
   301  	var allErrors error
   302  	for _, r := range resources {
   303  		if err := c.Update(r.Name, state, nil); err != nil {
   304  			allErrors = multierror.Append(allErrors, err)
   305  			continue
   306  		}
   307  		if err := c.updateLocalResource(r, state, nil); err != nil {
   308  			allErrors = multierror.Append(allErrors, err)
   309  		}
   310  	}
   311  	return allErrors
   312  }
   313  
   314  // SyncAll signals update for all resources hold by the client.
   315  func (c *Client) SyncAll() error {
   316  	c.lock.Lock()
   317  	defer c.lock.Unlock()
   318  
   319  	resources, err := c.storage.List()
   320  	if err != nil {
   321  		return err
   322  	}
   323  	if len(resources) == 0 {
   324  		logrus.Info("no resource to sync")
   325  		return nil
   326  	}
   327  	var allErrors error
   328  	for _, r := range resources {
   329  		if err := c.Update(r.Name, r.State, nil); err != nil {
   330  			allErrors = multierror.Append(allErrors, err)
   331  			continue
   332  		}
   333  		if _, err := c.storage.Update(r); err != nil {
   334  			allErrors = multierror.Append(allErrors, err)
   335  		}
   336  	}
   337  	return allErrors
   338  }
   339  
   340  // UpdateOne signals update for one of the resources hold by the client.
   341  func (c *Client) UpdateOne(name, state string, userData *common.UserData) error {
   342  	c.lock.Lock()
   343  	defer c.lock.Unlock()
   344  
   345  	r, err := c.storage.Get(name)
   346  	if err != nil {
   347  		return fmt.Errorf("no resource name %v", name)
   348  	}
   349  	if err := c.Update(r.Name, state, userData); err != nil {
   350  		return err
   351  	}
   352  	return c.updateLocalResource(r, state, userData)
   353  }
   354  
   355  // Reset will scan all boskos resources of type, in state, last updated before expire, and set them to dest state.
   356  // Returns a map of {resourceName:owner} for further actions.
   357  func (c *Client) Reset(rtype, state string, expire time.Duration, dest string) (map[string]string, error) {
   358  	return c.reset(rtype, state, expire, dest)
   359  }
   360  
   361  // Metric will query current metric for target resource type.
   362  // Return a common.Metric object on success.
   363  func (c *Client) Metric(rtype string) (common.Metric, error) {
   364  	return c.metric(rtype)
   365  }
   366  
   367  // HasResource tells if current client holds any resources
   368  func (c *Client) HasResource() bool {
   369  	resources, _ := c.storage.List()
   370  	return len(resources) > 0
   371  }
   372  
   373  // private methods
   374  
   375  func (c *Client) updateLocalResource(res common.Resource, state string, data *common.UserData) error {
   376  	res.State = state
   377  	if res.UserData == nil {
   378  		res.UserData = data
   379  	} else {
   380  		res.UserData.Update(data)
   381  	}
   382  	_, err := c.storage.Update(res)
   383  	return err
   384  }
   385  
   386  func (c *Client) acquire(rtype, state, dest, requestID string) (*common.Resource, error) {
   387  	values := url.Values{}
   388  	values.Set("type", rtype)
   389  	values.Set("state", state)
   390  	values.Set("owner", c.owner)
   391  	values.Set("dest", dest)
   392  	if requestID != "" {
   393  		values.Set("request_id", requestID)
   394  	}
   395  
   396  	res := common.Resource{}
   397  
   398  	work := func(retriedErrs *[]error) (bool, error) {
   399  		resp, err := c.httpPost("/acquire", values, "", nil)
   400  		if err != nil {
   401  			// Swallow the error so we can retry
   402  			*retriedErrs = append(*retriedErrs, err)
   403  			return false, nil
   404  		}
   405  		defer resp.Body.Close()
   406  
   407  		switch resp.StatusCode {
   408  		case http.StatusOK:
   409  			body, err := io.ReadAll(resp.Body)
   410  			if err != nil {
   411  				return false, err
   412  			}
   413  
   414  			err = json.Unmarshal(body, &res)
   415  			if err != nil {
   416  				return false, err
   417  			}
   418  			if res.Name == "" {
   419  				return false, fmt.Errorf("unable to parse resource")
   420  			}
   421  			return true, nil
   422  		case http.StatusUnauthorized:
   423  			return false, ErrAlreadyInUse
   424  		case http.StatusNotFound:
   425  			// The only way to distinguish between all reasources being busy and a request for a non-existent
   426  			// resource type is to check the text of the accompanying error message.
   427  			if c.DistinguishNotFoundVsTypeNotFound {
   428  				if bytes, err := io.ReadAll(resp.Body); err == nil {
   429  					errorMsg := string(bytes)
   430  					if strings.Contains(errorMsg, common.ResourceTypeNotFoundMessage(rtype)) {
   431  						return false, ErrTypeNotFound
   432  					}
   433  				}
   434  			}
   435  			return false, ErrNotFound
   436  		default:
   437  			*retriedErrs = append(*retriedErrs, fmt.Errorf("status %s, status code %v", resp.Status, resp.StatusCode))
   438  			// Swallow it so we can retry
   439  			return false, nil
   440  		}
   441  	}
   442  
   443  	return &res, retry(work)
   444  }
   445  
   446  func (c *Client) acquireByState(state, dest string, names []string) ([]common.Resource, error) {
   447  	values := url.Values{}
   448  	values.Set("state", state)
   449  	values.Set("dest", dest)
   450  	values.Set("names", strings.Join(names, ","))
   451  	values.Set("owner", c.owner)
   452  	var resources []common.Resource
   453  
   454  	work := func(retriedErrs *[]error) (bool, error) {
   455  		resp, err := c.httpPost("/acquirebystate", values, "", nil)
   456  		if err != nil {
   457  			*retriedErrs = append(*retriedErrs, err)
   458  			return false, nil
   459  		}
   460  		defer resp.Body.Close()
   461  
   462  		switch resp.StatusCode {
   463  		case http.StatusOK:
   464  			if err := json.NewDecoder(resp.Body).Decode(&resources); err != nil {
   465  				return false, err
   466  			}
   467  			return true, nil
   468  		case http.StatusUnauthorized:
   469  			return false, ErrAlreadyInUse
   470  		case http.StatusNotFound:
   471  			return false, ErrNotFound
   472  		default:
   473  			*retriedErrs = append(*retriedErrs, fmt.Errorf("status %s, status code %v", resp.Status, resp.StatusCode))
   474  			return false, nil
   475  		}
   476  	}
   477  
   478  	return resources, retry(work)
   479  }
   480  
   481  // Release a lease for a resource and set its state to the destination state
   482  func (c *Client) Release(name, dest string) error {
   483  	values := url.Values{}
   484  	values.Set("name", name)
   485  	values.Set("dest", dest)
   486  	values.Set("owner", c.owner)
   487  
   488  	work := func(retriedErrs *[]error) (bool, error) {
   489  		resp, err := c.httpPost("/release", values, "", nil)
   490  		if err != nil {
   491  			*retriedErrs = append(*retriedErrs, err)
   492  			return false, nil
   493  		}
   494  		defer resp.Body.Close()
   495  
   496  		if resp.StatusCode != http.StatusOK {
   497  			*retriedErrs = append(*retriedErrs, fmt.Errorf("status %s, statusCode %v releasing %s", resp.Status, resp.StatusCode, name))
   498  			return false, nil
   499  		}
   500  		return true, nil
   501  	}
   502  
   503  	return retry(work)
   504  }
   505  
   506  // Update a resource on the server, setting the state and user data
   507  func (c *Client) Update(name, state string, userData *common.UserData) error {
   508  	var bodyData *bytes.Buffer
   509  	if userData != nil {
   510  		bodyData = new(bytes.Buffer)
   511  		err := json.NewEncoder(bodyData).Encode(userData)
   512  		if err != nil {
   513  			return err
   514  		}
   515  	}
   516  	values := url.Values{}
   517  	values.Set("name", name)
   518  	values.Set("owner", c.owner)
   519  	values.Set("state", state)
   520  
   521  	work := func(retriedErrs *[]error) (bool, error) {
   522  		// As the body is an io.Reader and hence its content
   523  		// can only be read once, we have to copy it for every request we make
   524  		var body io.Reader
   525  		if bodyData != nil {
   526  			body = bytes.NewReader(bodyData.Bytes())
   527  		}
   528  		resp, err := c.httpPost("/update", values, "application/json", body)
   529  		if err != nil {
   530  			*retriedErrs = append(*retriedErrs, err)
   531  			return false, nil
   532  		}
   533  		defer resp.Body.Close()
   534  
   535  		if resp.StatusCode != http.StatusOK {
   536  			*retriedErrs = append(*retriedErrs, fmt.Errorf("status %s, status code %v updating %s", resp.Status, resp.StatusCode, name))
   537  			return false, nil
   538  		}
   539  		return true, nil
   540  	}
   541  
   542  	return retry(work)
   543  }
   544  
   545  func (c *Client) reset(rtype, state string, expire time.Duration, dest string) (map[string]string, error) {
   546  	rmap := make(map[string]string)
   547  	values := url.Values{}
   548  	values.Set("type", rtype)
   549  	values.Set("state", state)
   550  	values.Set("expire", expire.String())
   551  	values.Set("dest", dest)
   552  
   553  	work := func(retriedErrs *[]error) (bool, error) {
   554  		resp, err := c.httpPost("/reset", values, "", nil)
   555  		if err != nil {
   556  			*retriedErrs = append(*retriedErrs, err)
   557  			return false, nil
   558  		}
   559  		defer resp.Body.Close()
   560  
   561  		if resp.StatusCode == http.StatusOK {
   562  			body, err := io.ReadAll(resp.Body)
   563  			if err != nil {
   564  				return false, err
   565  			}
   566  
   567  			err = json.Unmarshal(body, &rmap)
   568  			return true, err
   569  		}
   570  		*retriedErrs = append(*retriedErrs, fmt.Errorf("status %s, status code %v", resp.Status, resp.StatusCode))
   571  		return false, nil
   572  
   573  	}
   574  
   575  	return rmap, retry(work)
   576  }
   577  
   578  func (c *Client) metric(rtype string) (common.Metric, error) {
   579  	var metric common.Metric
   580  	values := url.Values{}
   581  	values.Set("type", rtype)
   582  
   583  	work := func(retriedErrs *[]error) (bool, error) {
   584  		resp, err := c.httpGet("/metric", values)
   585  		if err != nil {
   586  			*retriedErrs = append(*retriedErrs, err)
   587  			return false, nil
   588  		}
   589  		defer resp.Body.Close()
   590  
   591  		if resp.StatusCode != http.StatusOK {
   592  			*retriedErrs = append(*retriedErrs, fmt.Errorf("status %s, status code %v", resp.Status, resp.StatusCode))
   593  			return false, nil
   594  		}
   595  
   596  		body, err := io.ReadAll(resp.Body)
   597  		if err != nil {
   598  			return false, err
   599  		}
   600  
   601  		return true, json.Unmarshal(body, &metric)
   602  	}
   603  
   604  	return metric, retry(work)
   605  }
   606  
   607  func (c *Client) httpGet(action string, values url.Values) (*http.Response, error) {
   608  	u, _ := url.ParseRequestURI(c.url)
   609  	u.Path = action
   610  	u.RawQuery = values.Encode()
   611  	req, err := http.NewRequest(http.MethodGet, u.String(), nil)
   612  	if err != nil {
   613  		return nil, err
   614  	}
   615  	if c.username != "" && c.getPassword != nil {
   616  		req.SetBasicAuth(c.username, string(c.getPassword()))
   617  	}
   618  	return c.http.Do(req)
   619  }
   620  
   621  func (c *Client) httpPost(action string, values url.Values, contentType string, body io.Reader) (*http.Response, error) {
   622  	u, _ := url.ParseRequestURI(c.url)
   623  	u.Path = action
   624  	u.RawQuery = values.Encode()
   625  	req, err := http.NewRequest(http.MethodPost, u.String(), body)
   626  	if err != nil {
   627  		return nil, err
   628  	}
   629  	if c.username != "" && c.getPassword != nil {
   630  		req.SetBasicAuth(c.username, string(c.getPassword()))
   631  	}
   632  	if contentType != "" {
   633  		req.Header.Set("Content-Type", contentType)
   634  	}
   635  	return c.http.Do(req)
   636  }
   637  
   638  // DialerWithRetry is a composite version of the net.Dialer that retries
   639  // connection attempts.
   640  type DialerWithRetry struct {
   641  	net.Dialer
   642  
   643  	// RetryCount is the number of times to retry a connection attempt.
   644  	RetryCount uint
   645  
   646  	// RetrySleep is the length of time to pause between retry attempts.
   647  	RetrySleep time.Duration
   648  }
   649  
   650  // Dial connects to the address on the named network.
   651  func (d *DialerWithRetry) Dial(network, address string) (net.Conn, error) {
   652  	return d.DialContext(context.Background(), network, address)
   653  }
   654  
   655  // DialContext connects to the address on the named network using the provided context.
   656  func (d *DialerWithRetry) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
   657  	// Always bump the retry count by 1 in order to equal the actual number of
   658  	// attempts. For example, if a retry count of 2 is specified, the intent
   659  	// is for three attempts -- the initial attempt with two retries in case
   660  	// the initial attempt times out.
   661  	count := d.RetryCount + 1
   662  	sleep := d.RetrySleep
   663  	i := uint(0)
   664  	for {
   665  		conn, err := d.Dialer.DialContext(ctx, network, address)
   666  		if err != nil {
   667  			if isDialErrorRetriable(err) {
   668  				if i < count-1 {
   669  					select {
   670  					case <-time.After(sleep):
   671  						i++
   672  						continue
   673  					case <-ctx.Done():
   674  						return nil, err
   675  					}
   676  				}
   677  			}
   678  			return nil, err
   679  		}
   680  		return conn, nil
   681  	}
   682  }
   683  
   684  // isDialErrorRetriable determines whether or not a dialer should retry
   685  // a failed connection attempt by examining the connection error to see
   686  // if it is one of the following error types:
   687  //   - Timeout
   688  //   - Temporary
   689  //   - ECONNREFUSED
   690  //   - ECONNRESET
   691  func isDialErrorRetriable(err error) bool {
   692  	opErr, isOpErr := err.(*net.OpError)
   693  	if !isOpErr {
   694  		return false
   695  	}
   696  	if opErr.Timeout() || opErr.Temporary() {
   697  		return true
   698  	}
   699  	sysErr, isSysErr := opErr.Err.(*os.SyscallError)
   700  	if !isSysErr {
   701  		return false
   702  	}
   703  	switch sysErr.Err {
   704  	case syscall.ECONNREFUSED, syscall.ECONNRESET:
   705  		return true
   706  	}
   707  	return false
   708  }
   709  
   710  // workFunc describes retrieable work. It should
   711  // * Return an error for non-recoverable errors
   712  // * Write retriable errors into `retriedErrs` and return with false, nil
   713  // * Return with true, nil on success
   714  type workFunc func(retriedErrs *[]error) (bool, error)
   715  
   716  // SleepFunc is called when requests are retried. This may be replaced in tests.
   717  var SleepFunc = time.Sleep
   718  
   719  func retry(work workFunc) error {
   720  	var retriedErrs []error
   721  
   722  	maxAttempts := 4
   723  	for i := 1; i <= maxAttempts; i++ {
   724  		success, err := work(&retriedErrs)
   725  		if err != nil {
   726  			return err
   727  		}
   728  		if success {
   729  			return nil
   730  		}
   731  		if i == maxAttempts {
   732  			break
   733  		}
   734  
   735  		SleepFunc(time.Duration(i*i) * time.Second)
   736  	}
   737  
   738  	return utilerrors.NewAggregate(retriedErrs)
   739  }