Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit c71524b

Browse files
committedOct 21, 2020
Making it possible to specify connect and header timeouts on registry backends
With unit tests. Signed-off-by: Jean Rouge <[email protected]>
1 parent e435b83 commit c71524b

File tree

6 files changed

+163
-9
lines changed

6 files changed

+163
-9
lines changed
 

‎lib/backend/registrybackend/blobclient.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ type BlobClient struct {
6363
// NewBlobClient creates a new BlobClient.
6464
func NewBlobClient(config Config) (*BlobClient, error) {
6565
config = config.applyDefaults()
66-
authenticator, err := security.NewAuthenticator(config.Address, config.Security)
66+
authenticator, err := config.Authenticator()
6767
if err != nil {
6868
return nil, fmt.Errorf("cannot create tag client authenticator: %s", err)
6969
}

‎lib/backend/registrybackend/blobclient_test.go

+60
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ import (
1919
"io"
2020
"net/http"
2121
"testing"
22+
"time"
2223

2324
"github.com/pressly/chi"
25+
"github.com/stretchr/testify/assert"
2426
"github.com/stretchr/testify/require"
2527
"github.com/uber/kraken/core"
2628
"github.com/uber/kraken/lib/backend/backenderrors"
@@ -125,3 +127,61 @@ func TestBlobDownloadFileNotFound(t *testing.T) {
125127
var b bytes.Buffer
126128
require.Equal(backenderrors.ErrBlobNotFound, client.Download(namespace, "data", &b))
127129
}
130+
131+
func TestBlobDownloadHeaderTimeout(t *testing.T) {
132+
require := require.New(t)
133+
134+
blob := randutil.Blob(32 * memsize.KB)
135+
namespace := core.NamespaceFixture()
136+
137+
r := chi.NewRouter()
138+
r.Get(fmt.Sprintf("/v2/%s/blobs/{blob}", namespace), func(w http.ResponseWriter, req *http.Request) {
139+
time.Sleep(time.Second)
140+
// ignoring errors here, as this will fail after we timeout below
141+
_, _ = io.Copy(w, bytes.NewReader(blob))
142+
})
143+
r.Head(fmt.Sprintf("/v2/%s/blobs/{blob}", namespace), func(w http.ResponseWriter, req *http.Request) {
144+
time.Sleep(time.Second)
145+
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(blob)))
146+
})
147+
addr, stop := testutil.StartServer(r)
148+
defer stop()
149+
150+
config := newTestConfig(addr)
151+
config.ResponseHeaderTimeout = 100 * time.Millisecond
152+
client, err := NewBlobClient(config)
153+
require.NoError(err)
154+
155+
_, err = client.Stat(namespace, "data")
156+
if assert.NotNil(t, err) {
157+
assert.Contains(t, err.Error(), "timeout awaiting response headers")
158+
}
159+
160+
var b bytes.Buffer
161+
err = client.Download(namespace, "data", &b)
162+
if assert.NotNil(t, err) {
163+
assert.Contains(t, err.Error(), "timeout awaiting response headers")
164+
}
165+
}
166+
167+
// FIXME: debugging failing tests on Travis
168+
//func TestBlobDownloadConnectTimeout(t *testing.T) {
169+
// require := require.New(t)
170+
//
171+
// // unroutable address, courtesy of https://stackoverflow.com/a/904609/4867444
172+
// config := newTestConfig("10.255.255.1")
173+
// config.ConnectTimeout = 100 * time.Millisecond
174+
// client, err := NewBlobClient(config)
175+
// require.NoError(err)
176+
//
177+
// _, err = client.Stat("dummynamespace", "data")
178+
// if assert.NotNil(t, err) {
179+
// assert.Contains(t, err.Error(), "i/o timeout")
180+
// }
181+
//
182+
// var b bytes.Buffer
183+
// err = client.Download("dummynamespace", "data", &b)
184+
// if assert.NotNil(t, err) {
185+
// assert.Contains(t, err.Error(), "i/o timeout")
186+
// }
187+
//}

‎lib/backend/registrybackend/config.go

+27-3
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,22 @@
1414
package registrybackend
1515

1616
import (
17+
"net"
18+
"net/http"
1719
"time"
1820

1921
"github.com/uber/kraken/lib/backend/registrybackend/security"
2022
)
2123

2224
// Config defines the registry address, timeout and security options.
2325
type Config struct {
24-
Address string `yaml:"address"`
25-
Timeout time.Duration `yaml:"timeout"`
26-
Security security.Config `yaml:"security"`
26+
Address string `yaml:"address"`
27+
Timeout time.Duration `yaml:"timeout"`
28+
// ConnectTimeout limits the time spent establishing the TCP connection (if a new one is needed).
29+
ConnectTimeout time.Duration `yaml:"connect_timeout"`
30+
// ResponseHeaderTimeout limits the time spent reading the headers of the response.
31+
ResponseHeaderTimeout time.Duration `yaml:"response_header_timeout"`
32+
Security security.Config `yaml:"security"`
2733
}
2834

2935
// Set default configuration
@@ -33,3 +39,21 @@ func (c Config) applyDefaults() Config {
3339
}
3440
return c
3541
}
42+
43+
func (c Config) Authenticator() (security.Authenticator, error) {
44+
transport := http.DefaultTransport.(*http.Transport).Clone()
45+
46+
if c.ConnectTimeout != 0 {
47+
dialer := &net.Dialer{
48+
Timeout: c.ConnectTimeout,
49+
KeepAlive: 30 * time.Second,
50+
}
51+
transport.DialContext = dialer.DialContext
52+
}
53+
54+
if c.ResponseHeaderTimeout != 0 {
55+
transport.ResponseHeaderTimeout = c.ResponseHeaderTimeout
56+
}
57+
58+
return security.NewAuthenticator(c.Address, c.Security, transport)
59+
}

‎lib/backend/registrybackend/security/security.go

+3-4
Original file line numberDiff line numberDiff line change
@@ -72,17 +72,16 @@ type authenticator struct {
7272
// address, TLS, and credentials configuration. It supports both basic auth and
7373
// token based authentication challenges. If TLS is disabled, no authentication
7474
// is attempted.
75-
func NewAuthenticator(address string, config Config) (Authenticator, error) {
76-
rt := http.DefaultTransport.(*http.Transport).Clone()
75+
func NewAuthenticator(address string, config Config, transport *http.Transport) (Authenticator, error) {
7776
tlsClientConfig, err := config.TLS.BuildClient()
7877
if err != nil {
7978
return nil, fmt.Errorf("build tls config for %q: %s", address, err)
8079
}
81-
rt.TLSClientConfig = tlsClientConfig
80+
transport.TLSClientConfig = tlsClientConfig
8281
return &authenticator{
8382
address: address,
8483
config: config,
85-
roundTripper: rt,
84+
roundTripper: transport,
8685
credentialStore: newCredentialStore(address, config),
8786
challengeManager: challenge.NewSimpleManager(),
8887
}, nil

‎lib/backend/registrybackend/tagclient.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ type TagClient struct {
6464
// NewTagClient creates a new TagClient.
6565
func NewTagClient(config Config) (*TagClient, error) {
6666
config = config.applyDefaults()
67-
authenticator, err := security.NewAuthenticator(config.Address, config.Security)
67+
authenticator, err := config.Authenticator()
6868
if err != nil {
6969
return nil, fmt.Errorf("cannot create tag client authenticator: %s", err)
7070
}

‎lib/backend/registrybackend/tagclient_test.go

+71
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@ import (
2020
"net/http"
2121
"strings"
2222
"testing"
23+
"time"
2324

2425
"github.com/pressly/chi"
26+
"github.com/stretchr/testify/assert"
2527
"github.com/stretchr/testify/require"
2628
"github.com/uber/kraken/core"
2729
"github.com/uber/kraken/lib/backend/backenderrors"
@@ -97,3 +99,72 @@ func TestTagDownloadFileNotFound(t *testing.T) {
9799
var b bytes.Buffer
98100
require.Equal(backenderrors.ErrBlobNotFound, client.Download(tag, tag, &b))
99101
}
102+
103+
func TestTagDownloadHeaderTimeout(t *testing.T) {
104+
require := require.New(t)
105+
106+
imageConfig := core.NewBlobFixture()
107+
layer1 := core.NewBlobFixture()
108+
layer2 := core.NewBlobFixture()
109+
digest, manifest := dockerutil.ManifestFixture(
110+
imageConfig.Digest, layer1.Digest, layer2.Digest)
111+
112+
tag := core.TagFixture()
113+
namespace := strings.Split(tag, ":")[0]
114+
115+
r := chi.NewRouter()
116+
r.Get(fmt.Sprintf("/v2/%s/manifests/{tag}", namespace), func(w http.ResponseWriter, req *http.Request) {
117+
time.Sleep(time.Second)
118+
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(manifest)))
119+
w.Header().Set("Docker-Content-Digest", digest.String())
120+
_, err := io.Copy(w, bytes.NewReader(manifest))
121+
require.NoError(err)
122+
})
123+
r.Head(fmt.Sprintf("/v2/%s/manifests/{tag}", namespace), func(w http.ResponseWriter, req *http.Request) {
124+
time.Sleep(time.Second)
125+
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(manifest)))
126+
w.Header().Set("Docker-Content-Digest", digest.String())
127+
_, err := io.Copy(w, bytes.NewReader(manifest))
128+
require.NoError(err)
129+
})
130+
addr, stop := testutil.StartServer(r)
131+
defer stop()
132+
133+
config := newTestConfig(addr)
134+
config.ResponseHeaderTimeout = 100 * time.Millisecond
135+
client, err := NewTagClient(config)
136+
require.NoError(err)
137+
138+
_, err = client.Stat(tag, tag)
139+
if assert.NotNil(t, err) {
140+
assert.Contains(t, err.Error(), "timeout awaiting response headers")
141+
}
142+
143+
var b bytes.Buffer
144+
err = client.Download(tag, tag, &b)
145+
if assert.NotNil(t, err) {
146+
assert.Contains(t, err.Error(), "timeout awaiting response headers")
147+
}
148+
}
149+
150+
// FIXME: debugging failing tests on Travis
151+
//func TestTagDownloadConnectTimeout(t *testing.T) {
152+
// require := require.New(t)
153+
//
154+
// // unroutable address, courtesy of https://stackoverflow.com/a/904609/4867444
155+
// config := newTestConfig("10.255.255.1")
156+
// config.ConnectTimeout = 100 * time.Millisecond
157+
// client, err := NewTagClient(config)
158+
// require.NoError(err)
159+
//
160+
// _, err = client.Stat("dummynamespace", "image:tag")
161+
// if assert.NotNil(t, err) {
162+
// assert.Contains(t, err.Error(), "i/o timeout")
163+
// }
164+
//
165+
// var b bytes.Buffer
166+
// err = client.Download("dummynamespace", "image:tag", &b)
167+
// if assert.NotNil(t, err) {
168+
// assert.Contains(t, err.Error(), "i/o timeout")
169+
// }
170+
//}

0 commit comments

Comments
 (0)
Please sign in to comment.