Skip to content

Commit 36a6a64

Browse files
authored
Merge pull request kubernetes#95427 from roycaihw/fix/tls-transport-cache
TLS transport cache: don't cache transports for incomparable configs
2 parents 28b46be + fac48d2 commit 36a6a64

File tree

4 files changed

+183
-49
lines changed

4 files changed

+183
-49
lines changed

staging/src/k8s.io/client-go/transport/cache.go

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -47,35 +47,34 @@ type tlsCacheKey struct {
4747
keyData string
4848
certFile string
4949
keyFile string
50-
getCert string
5150
serverName string
5251
nextProtos string
53-
dial string
5452
disableCompression bool
55-
proxy string
5653
}
5754

5855
func (t tlsCacheKey) String() string {
5956
keyText := "<none>"
6057
if len(t.keyData) > 0 {
6158
keyText = "<redacted>"
6259
}
63-
return fmt.Sprintf("insecure:%v, caData:%#v, certData:%#v, keyData:%s, getCert: %s, serverName:%s, dial:%s disableCompression:%t, proxy: %s", t.insecure, t.caData, t.certData, keyText, t.getCert, t.serverName, t.dial, t.disableCompression, t.proxy)
60+
return fmt.Sprintf("insecure:%v, caData:%#v, certData:%#v, keyData:%s, serverName:%s, disableCompression:%t", t.insecure, t.caData, t.certData, keyText, t.serverName, t.disableCompression)
6461
}
6562

6663
func (c *tlsTransportCache) get(config *Config) (http.RoundTripper, error) {
67-
key, err := tlsConfigKey(config)
64+
key, canCache, err := tlsConfigKey(config)
6865
if err != nil {
6966
return nil, err
7067
}
7168

72-
// Ensure we only create a single transport for the given TLS options
73-
c.mu.Lock()
74-
defer c.mu.Unlock()
69+
if canCache {
70+
// Ensure we only create a single transport for the given TLS options
71+
c.mu.Lock()
72+
defer c.mu.Unlock()
7573

76-
// See if we already have a custom transport for this config
77-
if t, ok := c.transports[key]; ok {
78-
return t, nil
74+
// See if we already have a custom transport for this config
75+
if t, ok := c.transports[key]; ok {
76+
return t, nil
77+
}
7978
}
8079

8180
// Get the TLS options for this client config
@@ -110,33 +109,41 @@ func (c *tlsTransportCache) get(config *Config) (http.RoundTripper, error) {
110109
proxy = config.Proxy
111110
}
112111

113-
// Cache a single transport for these options
114-
c.transports[key] = utilnet.SetTransportDefaults(&http.Transport{
112+
transport := utilnet.SetTransportDefaults(&http.Transport{
115113
Proxy: proxy,
116114
TLSHandshakeTimeout: 10 * time.Second,
117115
TLSClientConfig: tlsConfig,
118116
MaxIdleConnsPerHost: idleConnsPerHost,
119117
DialContext: dial,
120118
DisableCompression: config.DisableCompression,
121119
})
122-
return c.transports[key], nil
120+
121+
if canCache {
122+
// Cache a single transport for these options
123+
c.transports[key] = transport
124+
}
125+
126+
return transport, nil
123127
}
124128

125129
// tlsConfigKey returns a unique key for tls.Config objects returned from TLSConfigFor
126-
func tlsConfigKey(c *Config) (tlsCacheKey, error) {
130+
func tlsConfigKey(c *Config) (tlsCacheKey, bool, error) {
127131
// Make sure ca/key/cert content is loaded
128132
if err := loadTLSFiles(c); err != nil {
129-
return tlsCacheKey{}, err
133+
return tlsCacheKey{}, false, err
130134
}
135+
136+
if c.TLS.GetCert != nil || c.Dial != nil || c.Proxy != nil {
137+
// cannot determine equality for functions
138+
return tlsCacheKey{}, false, nil
139+
}
140+
131141
k := tlsCacheKey{
132142
insecure: c.TLS.Insecure,
133143
caData: string(c.TLS.CAData),
134-
getCert: fmt.Sprintf("%p", c.TLS.GetCert),
135144
serverName: c.TLS.ServerName,
136145
nextProtos: strings.Join(c.TLS.NextProtos, ","),
137-
dial: fmt.Sprintf("%p", c.Dial),
138146
disableCompression: c.DisableCompression,
139-
proxy: fmt.Sprintf("%p", c.Proxy),
140147
}
141148

142149
if c.TLS.ReloadTLSFiles {
@@ -147,5 +154,5 @@ func tlsConfigKey(c *Config) (tlsCacheKey, error) {
147154
k.keyData = string(c.TLS.KeyData)
148155
}
149156

150-
return k, nil
157+
return k, true, nil
151158
}

staging/src/k8s.io/client-go/transport/cache_test.go

Lines changed: 20 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import (
2121
"crypto/tls"
2222
"net"
2323
"net/http"
24-
"net/url"
2524
"testing"
2625
)
2726

@@ -37,16 +36,24 @@ func TestTLSConfigKey(t *testing.T) {
3736
}
3837
for nameA, valueA := range identicalConfigurations {
3938
for nameB, valueB := range identicalConfigurations {
40-
keyA, err := tlsConfigKey(valueA)
39+
keyA, canCache, err := tlsConfigKey(valueA)
4140
if err != nil {
4241
t.Errorf("Unexpected error for %q: %v", nameA, err)
4342
continue
4443
}
45-
keyB, err := tlsConfigKey(valueB)
44+
if !canCache {
45+
t.Errorf("Unexpected canCache=false")
46+
continue
47+
}
48+
keyB, canCache, err := tlsConfigKey(valueB)
4649
if err != nil {
4750
t.Errorf("Unexpected error for %q: %v", nameB, err)
4851
continue
4952
}
53+
if !canCache {
54+
t.Errorf("Unexpected canCache=false")
55+
continue
56+
}
5057
if keyA != keyB {
5158
t.Errorf("Expected identical cache keys for %q and %q, got:\n\t%s\n\t%s", nameA, nameB, keyA, keyB)
5259
continue
@@ -132,12 +139,12 @@ func TestTLSConfigKey(t *testing.T) {
132139
}
133140
for nameA, valueA := range uniqueConfigurations {
134141
for nameB, valueB := range uniqueConfigurations {
135-
keyA, err := tlsConfigKey(valueA)
142+
keyA, canCacheA, err := tlsConfigKey(valueA)
136143
if err != nil {
137144
t.Errorf("Unexpected error for %q: %v", nameA, err)
138145
continue
139146
}
140-
keyB, err := tlsConfigKey(valueB)
147+
keyB, canCacheB, err := tlsConfigKey(valueB)
141148
if err != nil {
142149
t.Errorf("Unexpected error for %q: %v", nameB, err)
143150
continue
@@ -148,33 +155,18 @@ func TestTLSConfigKey(t *testing.T) {
148155
if keyA != keyB {
149156
t.Errorf("Expected identical cache keys for %q and %q, got:\n\t%s\n\t%s", nameA, nameB, keyA, keyB)
150157
}
158+
if canCacheA != canCacheB {
159+
t.Errorf("Expected identical canCache %q and %q, got:\n\t%v\n\t%v", nameA, nameB, canCacheA, canCacheB)
160+
}
151161
continue
152162
}
153163

154-
if keyA == keyB {
155-
t.Errorf("Expected unique cache keys for %q and %q, got:\n\t%s\n\t%s", nameA, nameB, keyA, keyB)
156-
continue
164+
if canCacheA && canCacheB {
165+
if keyA == keyB {
166+
t.Errorf("Expected unique cache keys for %q and %q, got:\n\t%s\n\t%s", nameA, nameB, keyA, keyB)
167+
continue
168+
}
157169
}
158170
}
159171
}
160172
}
161-
162-
func TestTLSConfigKeyFuncPtr(t *testing.T) {
163-
keys := make(map[tlsCacheKey]struct{})
164-
makeKey := func(p func(*http.Request) (*url.URL, error)) tlsCacheKey {
165-
key, err := tlsConfigKey(&Config{Proxy: p})
166-
if err != nil {
167-
t.Fatalf("Unexpected error creating cache key: %v", err)
168-
}
169-
return key
170-
}
171-
172-
keys[makeKey(http.ProxyFromEnvironment)] = struct{}{}
173-
keys[makeKey(http.ProxyFromEnvironment)] = struct{}{}
174-
keys[makeKey(http.ProxyURL(nil))] = struct{}{}
175-
keys[makeKey(nil)] = struct{}{}
176-
177-
if got, want := len(keys), 3; got != want {
178-
t.Fatalf("Unexpected number of keys: got=%d want=%d", got, want)
179-
}
180-
}

staging/src/k8s.io/kube-aggregator/pkg/controllers/status/available_controller.go

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,53 @@ type AvailableConditionController struct {
8686
cache map[string]map[string][]string
8787
// this lock protects operations on the above cache
8888
cacheLock sync.RWMutex
89+
90+
// TLS config with customized dialer cannot be cached by the client-go
91+
// tlsTransportCache. Use a local cache here to reduce the chance of
92+
// the controller spamming idle connections with short-lived transports.
93+
// NOTE: the cache works because we assume that the transports constructed
94+
// by the controller only vary on the dynamic cert/key.
95+
tlsCache *tlsTransportCache
96+
}
97+
98+
type tlsTransportCache struct {
99+
mu sync.Mutex
100+
transports map[tlsCacheKey]http.RoundTripper
101+
}
102+
103+
func (c *tlsTransportCache) get(config *rest.Config) (http.RoundTripper, error) {
104+
// If the available controller doesn't customzie the dialer (and we know from
105+
// the code that the controller doesn't customzie other functions i.e. Proxy
106+
// and GetCert (ExecProvider)), the config is cacheable by the client-go TLS
107+
// transport cache. Let's skip the local cache and depend on the client-go cache.
108+
if config.Dial == nil {
109+
return rest.TransportFor(config)
110+
}
111+
c.mu.Lock()
112+
defer c.mu.Unlock()
113+
// See if we already have a custom transport for this config
114+
key := tlsConfigKey(config)
115+
if t, ok := c.transports[key]; ok {
116+
return t, nil
117+
}
118+
restTransport, err := rest.TransportFor(config)
119+
if err != nil {
120+
return nil, err
121+
}
122+
c.transports[key] = restTransport
123+
return restTransport, nil
124+
}
125+
126+
type tlsCacheKey struct {
127+
certData string
128+
keyData string
129+
}
130+
131+
func tlsConfigKey(c *rest.Config) tlsCacheKey {
132+
return tlsCacheKey{
133+
certData: string(c.TLSClientConfig.CertData),
134+
keyData: string(c.TLSClientConfig.KeyData),
135+
}
89136
}
90137

91138
// NewAvailableConditionController returns a new AvailableConditionController.
@@ -115,6 +162,7 @@ func NewAvailableConditionController(
115162
workqueue.NewItemExponentialFailureRateLimiter(5*time.Millisecond, 30*time.Second),
116163
"AvailableConditionController"),
117164
proxyCurrentCertKeyContent: proxyCurrentCertKeyContent,
165+
tlsCache: &tlsTransportCache{transports: make(map[tlsCacheKey]http.RoundTripper)},
118166
}
119167

120168
if egressSelector != nil {
@@ -185,7 +233,12 @@ func (c *AvailableConditionController) sync(key string) error {
185233
if c.dialContext != nil {
186234
restConfig.Dial = c.dialContext
187235
}
188-
restTransport, err := rest.TransportFor(restConfig)
236+
// TLS config with customized dialer cannot be cached by the client-go
237+
// tlsTransportCache. Use a local cache here to reduce the chance of
238+
// the controller spamming idle connections with short-lived transports.
239+
// NOTE: the cache works because we assume that the transports constructed
240+
// by the controller only vary on the dynamic cert/key.
241+
restTransport, err := c.tlsCache.get(restConfig)
189242
if err != nil {
190243
return err
191244
}

staging/src/k8s.io/kube-aggregator/pkg/controllers/status/available_controller_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package apiserver
1818

1919
import (
2020
"fmt"
21+
"net"
2122
"net/http"
2223
"net/http/httptest"
2324
"net/url"
@@ -133,6 +134,7 @@ func setupAPIServices(apiServices []*apiregistration.APIService) (*AvailableCond
133134
// the maximum disruption time to a minimum, but it does prevent hot loops.
134135
workqueue.NewItemExponentialFailureRateLimiter(5*time.Millisecond, 30*time.Second),
135136
"AvailableConditionController"),
137+
tlsCache: &tlsTransportCache{transports: make(map[tlsCacheKey]http.RoundTripper)},
136138
}
137139
for _, svc := range apiServices {
138140
c.addAPIService(svc)
@@ -202,6 +204,55 @@ func TestBuildCache(t *testing.T) {
202204
})
203205
}
204206
}
207+
208+
func TestTLSCache(t *testing.T) {
209+
apiServices := []*apiregistration.APIService{newRemoteAPIService("remote.group")}
210+
services := []*v1.Service{newService("foo", "bar", testServicePort, testServicePortName)}
211+
c, _ := setupAPIServices(apiServices)
212+
// TLS configs with customized dialers are uncacheable by the client-go
213+
// TLS transport cache. The local cache will be used.
214+
c.dialContext = (&net.Dialer{
215+
Timeout: 30 * time.Second,
216+
KeepAlive: 30 * time.Second,
217+
}).DialContext
218+
for _, svc := range services {
219+
c.addService(svc)
220+
}
221+
tests := []struct {
222+
name string
223+
proxyCurrentCertKeyContent certKeyFunc
224+
expectedCacheSize int
225+
}{
226+
{
227+
name: "nil certKeyFunc",
228+
expectedCacheSize: 1,
229+
},
230+
{
231+
name: "empty certKeyFunc",
232+
proxyCurrentCertKeyContent: func() ([]byte, []byte) { return emptyCert(), emptyCert() },
233+
// the tlsCacheKey is the same, reuse existing transport
234+
expectedCacheSize: 1,
235+
},
236+
{
237+
name: "different certKeyFunc",
238+
proxyCurrentCertKeyContent: testCertKeyFunc,
239+
// the tlsCacheKey is different, create a new transport
240+
expectedCacheSize: 2,
241+
},
242+
}
243+
for _, tc := range tests {
244+
t.Run(tc.name, func(t *testing.T) {
245+
c.proxyCurrentCertKeyContent = tc.proxyCurrentCertKeyContent
246+
for _, apiService := range apiServices {
247+
c.sync(apiService.Name)
248+
}
249+
if len(c.tlsCache.transports) != tc.expectedCacheSize {
250+
t.Fatalf("%v cache size expected %v, got %v", tc.name, tc.expectedCacheSize, len(c.tlsCache.transports))
251+
}
252+
})
253+
}
254+
}
255+
205256
func TestSync(t *testing.T) {
206257
tests := []struct {
207258
name string
@@ -356,6 +407,7 @@ func TestSync(t *testing.T) {
356407
endpointsLister: v1listers.NewEndpointsLister(endpointsIndexer),
357408
serviceResolver: &fakeServiceResolver{url: testServer.URL},
358409
proxyCurrentCertKeyContent: func() ([]byte, []byte) { return emptyCert(), emptyCert() },
410+
tlsCache: &tlsTransportCache{transports: make(map[tlsCacheKey]http.RoundTripper)},
359411
}
360412
c.sync(tc.apiServiceName)
361413

@@ -420,3 +472,33 @@ func TestUpdateAPIServiceStatus(t *testing.T) {
420472
func emptyCert() []byte {
421473
return []byte{}
422474
}
475+
476+
func testCertKeyFunc() ([]byte, []byte) {
477+
return []byte(`-----BEGIN CERTIFICATE-----
478+
MIICBDCCAW2gAwIBAgIJAPgVBh+4xbGoMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV
479+
BAMMEHdlYmhvb2tfdGVzdHNfY2EwIBcNMTcwNzI4MjMxNTI4WhgPMjI5MTA1MTMy
480+
MzE1MjhaMB8xHTAbBgNVBAMMFHdlYmhvb2tfdGVzdHNfY2xpZW50MIGfMA0GCSqG
481+
SIb3DQEBAQUAA4GNADCBiQKBgQDkGXXSm6Yun5o3Jlmx45rItcQ2pmnoDk4eZfl0
482+
rmPa674s2pfYo3KywkXQ1Fp3BC8GUgzPLSfJ8xXya9Lg1Wo8sHrDln0iRg5HXxGu
483+
uFNhRBvj2S0sIff0ZG/IatB9I6WXVOUYuQj6+A0CdULNj1vBqH9+7uWbLZ6lrD4b
484+
a44x/wIDAQABo0owSDAJBgNVHRMEAjAAMAsGA1UdDwQEAwIF4DAdBgNVHSUEFjAU
485+
BggrBgEFBQcDAgYIKwYBBQUHAwEwDwYDVR0RBAgwBocEfwAAATANBgkqhkiG9w0B
486+
AQsFAAOBgQCpN27uh/LjUVCaBK7Noko25iih/JSSoWzlvc8CaipvSPofNWyGx3Vu
487+
OdcSwNGYX/pp4ZoAzFij/Y5u0vKTVLkWXATeTMVmlPvhmpYjj9gPkCSY6j/SiKlY
488+
kGy0xr+0M5UQkMBcfIh9oAp9um1fZHVWAJAGP/ikZgkcUey0LmBn8w==
489+
-----END CERTIFICATE-----`), []byte(`-----BEGIN RSA PRIVATE KEY-----
490+
MIICWwIBAAKBgQDkGXXSm6Yun5o3Jlmx45rItcQ2pmnoDk4eZfl0rmPa674s2pfY
491+
o3KywkXQ1Fp3BC8GUgzPLSfJ8xXya9Lg1Wo8sHrDln0iRg5HXxGuuFNhRBvj2S0s
492+
Iff0ZG/IatB9I6WXVOUYuQj6+A0CdULNj1vBqH9+7uWbLZ6lrD4ba44x/wIDAQAB
493+
AoGAZbWwowvCq1GBq4vPPRI3h739Uz0bRl1ymf1woYXNguXRtCB4yyH+2BTmmrrF
494+
6AIWkePuUEdbUaKyK5nGu3iOWM+/i6NP3kopQANtbAYJ2ray3kwvFlhqyn1bxX4n
495+
gl/Cbdw1If4zrDrB66y8mYDsjzK7n/gFaDNcY4GArjvOXKkCQQD9Lgv+WD73y4RP
496+
yS+cRarlEeLLWVsX/pg2oEBLM50jsdUnrLSW071MjBgP37oOXzqynF9SoDbP2Y5C
497+
x+aGux9LAkEA5qPlQPv0cv8Wc3qTI+LixZ/86PPHKWnOnwaHm3b9vQjZAkuVQg3n
498+
Wgg9YDmPM87t3UFH7ZbDihUreUxwr9ZjnQJAZ9Z95shMsxbOYmbSVxafu6m1Sc+R
499+
M+sghK7/D5jQpzYlhUspGf8n0YBX0hLhXUmjamQGGH5LXL4Owcb4/mM6twJAEVio
500+
SF/qva9jv+GrKVrKFXT374lOJFY53Qn/rvifEtWUhLCslCA5kzLlctRBafMZPrfH
501+
Mh5RrJP1BhVysDbenQJASGcc+DiF7rB6K++ZGyC11E2AP29DcZ0pgPESSV7npOGg
502+
+NqPRZNVCSZOiVmNuejZqmwKhZNGZnBFx1Y+ChAAgw==
503+
-----END RSA PRIVATE KEY-----`)
504+
}

0 commit comments

Comments
 (0)