Skip to content

Commit 43a2445

Browse files
yannhcarlossg
andauthored
Retry (#282)
* fix: use hashicorp/go-retryablehttp to retry failed schema downloads --------- Co-authored-by: Carlos Sanchez <[email protected]>
1 parent 706cd56 commit 43a2445

File tree

21 files changed

+2131
-48
lines changed

21 files changed

+2131
-48
lines changed

go.mod

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,8 @@ require (
66
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1
77
sigs.k8s.io/yaml v1.4.0
88
)
9+
10+
require (
11+
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
12+
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
13+
)

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
22
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
3+
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
4+
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
5+
github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
6+
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
37
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4=
48
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY=
59
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=

pkg/registry/http.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"os"
1111
"time"
1212

13+
retryablehttp "github.com/hashicorp/go-retryablehttp"
1314
"github.com/yannh/kubeconform/pkg/cache"
1415
)
1516

@@ -51,8 +52,14 @@ func newHTTPRegistry(schemaPathTemplate string, cacheFolder string, strict bool,
5152
filecache = cache.NewOnDiskCache(cacheFolder)
5253
}
5354

55+
// retriable http client
56+
retryClient := retryablehttp.NewClient()
57+
retryClient.RetryMax = 2
58+
retryClient.HTTPClient = &http.Client{Transport: reghttp}
59+
retryClient.Logger = nil
60+
5461
return &SchemaRegistry{
55-
c: &http.Client{Transport: reghttp},
62+
c: retryClient.StandardClient(),
5663
schemaPathTemplate: schemaPathTemplate,
5764
cache: filecache,
5865
strict: strict,

pkg/registry/http_test.go

Lines changed: 110 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -3,114 +3,177 @@ package registry
33
import (
44
"bytes"
55
"fmt"
6-
"io"
6+
"math/rand"
77
"net/http"
8-
"strings"
98
"testing"
9+
"time"
1010
)
1111

1212
type mockHTTPGetter struct {
13-
httpGet func(string) (*http.Response, error)
13+
callNumber int
14+
httpGet func(mockHTTPGetter, string) (*http.Response, error)
1415
}
1516

16-
func newMockHTTPGetter(f func(string) (*http.Response, error)) *mockHTTPGetter {
17+
func newMockHTTPGetter(f func(mockHTTPGetter, string) (*http.Response, error)) *mockHTTPGetter {
1718
return &mockHTTPGetter{
18-
httpGet: f,
19+
callNumber: 0,
20+
httpGet: f,
1921
}
2022
}
21-
func (m mockHTTPGetter) Get(url string) (resp *http.Response, err error) {
22-
return m.httpGet(url)
23+
func (m *mockHTTPGetter) Get(url string) (resp *http.Response, err error) {
24+
m.callNumber = m.callNumber + 1
25+
return m.httpGet(*m, url)
2326
}
2427

2528
func TestDownloadSchema(t *testing.T) {
29+
callCounts := map[string]int{}
30+
31+
// http server to simulate different responses
32+
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
33+
var s int
34+
callCounts[r.URL.Path]++
35+
callCount := callCounts[r.URL.Path]
36+
37+
switch r.URL.Path {
38+
case "/404":
39+
s = http.StatusNotFound
40+
case "/500":
41+
s = http.StatusInternalServerError
42+
case "/503":
43+
if callCount < 2 {
44+
s = http.StatusServiceUnavailable
45+
} else {
46+
s = http.StatusOK // Should succeed on 3rd try
47+
}
48+
49+
case "/simulate-reset":
50+
if callCount < 2 {
51+
if hj, ok := w.(http.Hijacker); ok {
52+
conn, _, err := hj.Hijack()
53+
if err != nil {
54+
fmt.Printf("Hijacking failed: %v\n", err)
55+
return
56+
}
57+
conn.Close() // Close the connection to simulate a reset
58+
}
59+
return
60+
}
61+
s = http.StatusOK // Should succeed on third try
62+
63+
default:
64+
s = http.StatusOK
65+
}
66+
67+
w.WriteHeader(s)
68+
w.Write([]byte(http.StatusText(s)))
69+
})
70+
71+
port := fmt.Sprint(rand.Intn(1000) + 9000) // random port
72+
server := &http.Server{Addr: "127.0.0.1:" + port}
73+
url := fmt.Sprintf("http://localhost:%s", port)
74+
75+
go func() {
76+
if err := server.ListenAndServe(); err != nil {
77+
fmt.Printf("Failed to start server: %v\n", err)
78+
}
79+
}()
80+
defer server.Shutdown(nil)
81+
82+
// Wait for the server to start
83+
for i := 0; i < 20; i++ {
84+
if _, err := http.Get(url); err == nil {
85+
break
86+
}
87+
88+
if i == 19 {
89+
t.Error("http server did not start")
90+
return
91+
}
92+
93+
time.Sleep(50 * time.Millisecond)
94+
}
95+
2696
for _, testCase := range []struct {
2797
name string
28-
c httpGetter
2998
schemaPathTemplate string
3099
strict bool
31100
resourceKind, resourceAPIVersion, k8sversion string
32101
expect []byte
33102
expectErr error
34103
}{
35104
{
36-
"error when downloading",
37-
newMockHTTPGetter(func(url string) (resp *http.Response, err error) {
38-
return nil, fmt.Errorf("failed downloading from registry")
39-
}),
40-
"http://kubernetesjson.dev",
105+
"retry connection reset by peer",
106+
fmt.Sprintf("%s/simulate-reset", url),
41107
true,
42108
"Deployment",
43109
"v1",
44110
"1.18.0",
111+
[]byte(http.StatusText(http.StatusOK)),
45112
nil,
46-
fmt.Errorf("failed downloading schema at http://kubernetesjson.dev: failed downloading from registry"),
47113
},
48114
{
49115
"getting 404",
50-
newMockHTTPGetter(func(url string) (resp *http.Response, err error) {
51-
return &http.Response{
52-
StatusCode: http.StatusNotFound,
53-
Body: io.NopCloser(strings.NewReader("http response mock body")),
54-
}, nil
55-
}),
56-
"http://kubernetesjson.dev",
116+
fmt.Sprintf("%s/404", url),
57117
true,
58118
"Deployment",
59119
"v1",
60120
"1.18.0",
61121
nil,
62-
fmt.Errorf("could not find schema at http://kubernetesjson.dev"),
122+
fmt.Errorf("could not find schema at %s/404", url),
63123
},
64124
{
65-
"getting 503",
66-
newMockHTTPGetter(func(url string) (resp *http.Response, err error) {
67-
return &http.Response{
68-
StatusCode: http.StatusServiceUnavailable,
69-
Body: io.NopCloser(strings.NewReader("http response mock body")),
70-
}, nil
71-
}),
72-
"http://kubernetesjson.dev",
125+
"getting 500",
126+
fmt.Sprintf("%s/500", url),
73127
true,
74128
"Deployment",
75129
"v1",
76130
"1.18.0",
77131
nil,
78-
fmt.Errorf("error while downloading schema at http://kubernetesjson.dev - received HTTP status 503"),
132+
fmt.Errorf("failed downloading schema at %s/500: Get \"%s/500\": GET %s/500 giving up after 3 attempt(s)", url, url, url),
133+
},
134+
{
135+
"retry 503",
136+
fmt.Sprintf("%s/503", url),
137+
true,
138+
"Deployment",
139+
"v1",
140+
"1.18.0",
141+
[]byte(http.StatusText(http.StatusOK)),
142+
nil,
79143
},
80144
{
81145
"200",
82-
newMockHTTPGetter(func(url string) (resp *http.Response, err error) {
83-
return &http.Response{
84-
StatusCode: http.StatusOK,
85-
Body: io.NopCloser(strings.NewReader("http response mock body")),
86-
}, nil
87-
}),
88-
"http://kubernetesjson.dev",
146+
url,
89147
true,
90148
"Deployment",
91149
"v1",
92150
"1.18.0",
93-
[]byte("http response mock body"),
151+
[]byte(http.StatusText(http.StatusOK)),
94152
nil,
95153
},
96154
} {
97-
reg := SchemaRegistry{
98-
c: testCase.c,
99-
schemaPathTemplate: testCase.schemaPathTemplate,
100-
strict: testCase.strict,
155+
callCounts = map[string]int{} // Reinitialise counters
156+
157+
reg, err := newHTTPRegistry(testCase.schemaPathTemplate, "", testCase.strict, true, true)
158+
if err != nil {
159+
t.Errorf("during test '%s': failed to create registry: %s", testCase.name, err)
160+
continue
101161
}
102162

103163
_, res, err := reg.DownloadSchema(testCase.resourceKind, testCase.resourceAPIVersion, testCase.k8sversion)
104164
if err == nil || testCase.expectErr == nil {
105-
if err != testCase.expectErr {
106-
t.Errorf("during test '%s': expected error, got:\n%s\n%s\n", testCase.name, testCase.expectErr, err)
165+
if err == nil && testCase.expectErr != nil {
166+
t.Errorf("during test '%s': expected error\n%s, got nil", testCase.name, testCase.expectErr)
167+
}
168+
if err != nil && testCase.expectErr == nil {
169+
t.Errorf("during test '%s': expected no error, got\n%s\n", testCase.name, err)
107170
}
108171
} else if err.Error() != testCase.expectErr.Error() {
109-
t.Errorf("during test '%s': expected error, got:\n%s\n%s\n", testCase.name, testCase.expectErr, err)
172+
t.Errorf("during test '%s': expected error\n%s, got:\n%s\n", testCase.name, testCase.expectErr, err)
110173
}
111174

112175
if !bytes.Equal(res, testCase.expect) {
113-
t.Errorf("during test '%s': expected %s, got %s", testCase.name, testCase.expect, res)
176+
t.Errorf("during test '%s': expected '%s', got '%s'", testCase.name, testCase.expect, res)
114177
}
115178
}
116179

0 commit comments

Comments
 (0)