Skip to content

Commit 003ab22

Browse files
committed
Switch to avast/retry-go for HTTP retry
The hashicorp dependencies are MPL licensed and therefore not allowed in the CNCF. This means we now use the `avast/retry-go` implementation to achieve the same goal. Signed-off-by: Sascha Grunert <[email protected]>
1 parent 6e8d290 commit 003ab22

File tree

5 files changed

+100
-59
lines changed

5 files changed

+100
-59
lines changed

go.mod

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,11 @@ require (
5656
cloud.google.com/go/pubsub v1.45.3
5757
github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d
5858
github.com/DATA-DOG/go-sqlmock v1.5.2
59+
github.com/avast/retry-go/v4 v4.6.0
5960
github.com/cyberphone/json-canonicalization v0.0.0-20220623050100-57a0ce2678a7
6061
github.com/go-redis/redismock/v9 v9.2.0
6162
github.com/go-sql-driver/mysql v1.8.1
6263
github.com/golang/mock v1.6.0
63-
github.com/hashicorp/go-cleanhttp v0.5.2
64-
github.com/hashicorp/go-retryablehttp v0.7.7
6564
github.com/jmoiron/sqlx v1.4.0
6665
github.com/redis/go-redis/v9 v9.7.0
6766
github.com/sassoftware/relic/v7 v7.6.2
@@ -132,7 +131,9 @@ require (
132131
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect
133132
github.com/google/s2a-go v0.1.9 // indirect
134133
github.com/hashicorp/errwrap v1.1.0 // indirect
134+
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
135135
github.com/hashicorp/go-multierror v1.1.1 // indirect
136+
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
136137
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
137138
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.7 // indirect
138139
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPp
6464
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
6565
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
6666
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
67+
github.com/avast/retry-go/v4 v4.6.0 h1:K9xNA+KeB8HHc2aWFuLb25Offp+0iVRXEvFx8IinRJA=
68+
github.com/avast/retry-go/v4 v4.6.0/go.mod h1:gvWlPhBVsvBbLkVGDg/KwvBv0bEkCOLRRSHKIr2PyOE=
6769
github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU=
6870
github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
6971
github.com/aws/aws-sdk-go-v2 v1.32.8 h1:cZV+NUS/eGxKXMtmyhtYPJ7Z4YLoI/V8bkTdRZfYhGo=

pkg/client/options.go

Lines changed: 67 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,16 @@
1515
package client
1616

1717
import (
18+
"crypto/tls"
19+
"errors"
20+
"fmt"
1821
"net/http"
22+
"net/url"
23+
"regexp"
24+
"strings"
1925
"time"
2026

21-
"github.com/hashicorp/go-retryablehttp"
27+
"github.com/avast/retry-go/v4"
2228
)
2329

2430
// Option is a functional option for customizing static signatures.
@@ -30,7 +36,6 @@ type options struct {
3036
RetryWaitMin time.Duration
3137
RetryWaitMax time.Duration
3238
InsecureTLS bool
33-
Logger interface{}
3439
NoDisableKeepalives bool
3540
Headers map[string][]string
3641
}
@@ -81,14 +86,9 @@ func WithRetryWaitMax(t time.Duration) Option {
8186
}
8287
}
8388

84-
// WithLogger sets the logger; it must implement either retryablehttp.Logger or retryablehttp.LeveledLogger; if not, this will not take effect.
85-
func WithLogger(logger interface{}) Option {
86-
return func(o *options) {
87-
switch logger.(type) {
88-
case retryablehttp.Logger, retryablehttp.LeveledLogger:
89-
o.Logger = logger
90-
}
91-
}
89+
// WithLogger sets the logger; this method is deprecated and will not take any effect.
90+
func WithLogger(interface{}) Option {
91+
return func(*options) {}
9292
}
9393

9494
// WithInsecureTLS disables TLS verification.
@@ -113,33 +113,75 @@ func WithHeaders(h map[string][]string) Option {
113113
}
114114

115115
type roundTripper struct {
116-
http.RoundTripper
117-
UserAgent string
118-
Headers map[string][]string
116+
inner http.RoundTripper
117+
*options
119118
}
120119

121120
// RoundTrip implements `http.RoundTripper`
122-
func (rt *roundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
123-
req.Header.Set("User-Agent", rt.UserAgent)
124-
for k, v := range rt.Headers {
121+
func (rt *roundTripper) RoundTrip(req *http.Request) (res *http.Response, err error) {
122+
req.Header.Set("User-Agent", rt.options.UserAgent)
123+
for k, v := range rt.options.Headers {
125124
for _, h := range v {
126125
req.Header.Add(k, h)
127126
}
128127
}
129-
return rt.RoundTripper.RoundTrip(req)
128+
129+
err = retry.Do(func() (err error) {
130+
res, err = rt.inner.RoundTrip(req)
131+
if retryErr := shouldRetry(res, err); retryErr != nil {
132+
return retryErr
133+
}
134+
return nil
135+
},
136+
retry.Attempts(rt.options.RetryCount),
137+
retry.Delay(rt.options.RetryWaitMin),
138+
retry.MaxDelay(rt.options.RetryWaitMax),
139+
)
140+
141+
return res, err
130142
}
131143

132-
func createRoundTripper(inner http.RoundTripper, o *options) http.RoundTripper {
144+
var tooManyRedirectyRe = regexp.MustCompile(`stopped after \d+ redirects\z`)
145+
146+
func shouldRetry(resp *http.Response, err error) error {
147+
if err != nil {
148+
urlErr := &url.Error{}
149+
150+
// Filter well known URL errors
151+
if errors.As(err, &urlErr) {
152+
certVerificationErr := &tls.CertificateVerificationError{}
153+
154+
if tooManyRedirectyRe.MatchString(urlErr.Error()) ||
155+
strings.Contains(urlErr.Error(), "unsupported protocol scheme") ||
156+
strings.Contains(urlErr.Error(), "invalid header") ||
157+
strings.Contains(urlErr.Error(), "certificate is not trusted") ||
158+
errors.As(urlErr.Err, &certVerificationErr) {
159+
return nil
160+
}
161+
}
162+
163+
// Retry any other errror
164+
return err
165+
}
166+
167+
if resp.StatusCode == http.StatusTooManyRequests {
168+
return fmt.Errorf("retry %d: %s", resp.StatusCode, resp.Status)
169+
}
170+
171+
if resp.StatusCode == 0 || (resp.StatusCode >= 500 &&
172+
resp.StatusCode != http.StatusNotImplemented) {
173+
return fmt.Errorf("retry unexpected HTTP status %d: %s", resp.StatusCode, resp.Status)
174+
}
175+
176+
return nil
177+
}
178+
179+
func wrapRoundTripper(inner http.RoundTripper, o *options) http.RoundTripper {
133180
if inner == nil {
134181
inner = http.DefaultTransport
135182
}
136-
if o.UserAgent == "" && o.Headers == nil {
137-
// There's nothing to do...
138-
return inner
139-
}
140183
return &roundTripper{
141-
RoundTripper: inner,
142-
UserAgent: o.UserAgent,
143-
Headers: o.Headers,
184+
inner: inner,
185+
options: o,
144186
}
145187
}

pkg/client/options_test.go

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,14 @@
1515
package client
1616

1717
import (
18-
"log"
1918
"net/http"
20-
"os"
2119
"testing"
2220

2321
"github.com/google/go-cmp/cmp"
22+
"go.uber.org/zap"
2423
)
2524

2625
func TestMakeOptions(t *testing.T) {
27-
customLogger := log.New(os.Stdout, "", log.LstdFlags)
28-
2926
tests := []struct {
3027
desc string
3128

@@ -42,10 +39,6 @@ func TestMakeOptions(t *testing.T) {
4239
desc: "WithRetryCount",
4340
opts: []Option{WithRetryCount(2)},
4441
want: &options{UserAgent: "", RetryCount: 2},
45-
}, {
46-
desc: "WithLogger",
47-
opts: []Option{WithLogger(customLogger)},
48-
want: &options{UserAgent: "", RetryCount: DefaultRetryCount, Logger: customLogger},
4942
}, {
5043
desc: "WithLoggerNil",
5144
opts: []Option{WithLogger(nil)},
@@ -62,7 +55,9 @@ func TestMakeOptions(t *testing.T) {
6255
for _, tc := range tests {
6356
t.Run(tc.desc, func(t *testing.T) {
6457
got := makeOptions(tc.opts...)
65-
if d := cmp.Diff(tc.want, got, cmp.Comparer(func(a, b *log.Logger) bool { return a == b })); d != "" {
58+
if d := cmp.Diff(tc.want, got, cmp.Comparer(func(a, b zap.SugaredLogger) bool {
59+
return a == b
60+
})); d != "" {
6661
t.Errorf("makeOptions(%v) returned unexpected result (-want +got): %s", tc.desc, d)
6762
}
6863
})
@@ -82,11 +77,11 @@ func (m *mockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error)
8277
return m.resp, m.err
8378
}
8479

85-
func TestCreateRoundTripper(t *testing.T) {
80+
func TestWrapRoundTripper(t *testing.T) {
8681
t.Run("always returns non-nil", func(t *testing.T) {
87-
got := createRoundTripper(nil, &options{})
82+
got := wrapRoundTripper(nil, &options{})
8883
if got == nil {
89-
t.Errorf("createRoundTripper() should never return a nil `http.RoundTripper`")
84+
t.Errorf("wrapRoundTripper() should never return a nil `http.RoundTripper`")
9085
}
9186
})
9287

@@ -104,7 +99,7 @@ func TestCreateRoundTripper(t *testing.T) {
10499
expectedUserAgent := "test UserAgent"
105100

106101
m := &mockRoundTripper{}
107-
rt := createRoundTripper(m, &options{
102+
rt := wrapRoundTripper(m, &options{
108103
UserAgent: expectedUserAgent,
109104
})
110105
m.resp = testResp

pkg/client/rekor_client.go

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,15 @@ package client
1616

1717
import (
1818
"crypto/tls"
19+
"net"
1920
"net/http"
2021
"net/url"
22+
"time"
2123

2224
"github.com/go-openapi/runtime"
2325
httptransport "github.com/go-openapi/runtime/client"
2426
"github.com/go-openapi/strfmt"
2527

26-
"github.com/hashicorp/go-cleanhttp"
27-
retryablehttp "github.com/hashicorp/go-retryablehttp"
2828
"github.com/sigstore/rekor/pkg/generated/client"
2929
"github.com/sigstore/rekor/pkg/util"
3030
)
@@ -36,25 +36,26 @@ func GetRekorClient(rekorServerURL string, opts ...Option) (*client.Rekor, error
3636
}
3737
o := makeOptions(opts...)
3838

39-
retryableClient := retryablehttp.NewClient()
40-
defaultTransport := cleanhttp.DefaultTransport()
41-
if o.NoDisableKeepalives {
42-
defaultTransport.DisableKeepAlives = false
39+
transport := &http.Transport{
40+
Proxy: http.ProxyFromEnvironment,
41+
DialContext: (&net.Dialer{
42+
Timeout: 30 * time.Second,
43+
KeepAlive: 30 * time.Second,
44+
DualStack: true,
45+
}).DialContext,
46+
MaxIdleConns: 100,
47+
IdleConnTimeout: 90 * time.Second,
48+
TLSHandshakeTimeout: 10 * time.Second,
49+
ExpectContinueTimeout: 1 * time.Second,
50+
ForceAttemptHTTP2: true,
51+
MaxIdleConnsPerHost: -1,
52+
DisableKeepAlives: !o.NoDisableKeepalives,
53+
TLSClientConfig: &tls.Config{InsecureSkipVerify: o.InsecureTLS}, //nolint:gosec
4354
}
44-
if o.InsecureTLS {
45-
/* #nosec G402 */
46-
defaultTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
47-
}
48-
retryableClient.HTTPClient = &http.Client{
49-
Transport: defaultTransport,
50-
}
51-
retryableClient.RetryMax = int(o.RetryCount)
52-
retryableClient.RetryWaitMin = o.RetryWaitMin
53-
retryableClient.RetryWaitMax = o.RetryWaitMax
54-
retryableClient.Logger = o.Logger
5555

56-
httpClient := retryableClient.StandardClient()
57-
httpClient.Transport = createRoundTripper(httpClient.Transport, o)
56+
httpClient := &http.Client{
57+
Transport: wrapRoundTripper(transport, o),
58+
}
5859

5960
// sanitize path
6061
if url.Path == "" {

0 commit comments

Comments
 (0)