Skip to content

Commit 234f717

Browse files
authored
Merge pull request #13080 from k8s-infra-cherrypick-robot/cherry-pick-13075-to-release-1.12
[release-1.12] 🌱 Add httpClientCache to runtime client
2 parents c3a9f07 + 8a79fc8 commit 234f717

File tree

2 files changed

+194
-37
lines changed

2 files changed

+194
-37
lines changed

internal/runtime/client/client.go

Lines changed: 98 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import (
5555
runtimemetrics "sigs.k8s.io/cluster-api/internal/runtime/metrics"
5656
runtimeregistry "sigs.k8s.io/cluster-api/internal/runtime/registry"
5757
"sigs.k8s.io/cluster-api/util"
58+
"sigs.k8s.io/cluster-api/util/cache"
5859
)
5960

6061
type errCallingExtensionHandler error
@@ -73,22 +74,52 @@ type Options struct {
7374
// New returns a new Client.
7475
func New(options Options) runtimeclient.Client {
7576
return &client{
76-
certFile: options.CertFile,
77-
keyFile: options.KeyFile,
78-
catalog: options.Catalog,
79-
registry: options.Registry,
80-
client: options.Client,
77+
certFile: options.CertFile,
78+
keyFile: options.KeyFile,
79+
catalog: options.Catalog,
80+
registry: options.Registry,
81+
client: options.Client,
82+
httpClientsCache: cache.New[httpClientEntry](24 * time.Hour),
8183
}
8284
}
8385

8486
var _ runtimeclient.Client = &client{}
8587

8688
type client struct {
87-
certFile string
88-
keyFile string
89-
catalog *runtimecatalog.Catalog
90-
registry runtimeregistry.ExtensionRegistry
91-
client ctrlclient.Client
89+
certFile string
90+
keyFile string
91+
catalog *runtimecatalog.Catalog
92+
registry runtimeregistry.ExtensionRegistry
93+
client ctrlclient.Client
94+
httpClientsCache cache.Cache[httpClientEntry]
95+
}
96+
97+
type httpClientEntry struct {
98+
// Note: caData and hostName are the variable parts in the TLSConfig
99+
// for an http.Client that is used to call runtime extensions.
100+
caData []byte
101+
hostName string
102+
103+
client *http.Client
104+
}
105+
106+
func newHTTPClientEntry(hostName string, caData []byte, client *http.Client) httpClientEntry {
107+
return httpClientEntry{
108+
hostName: hostName,
109+
caData: caData,
110+
client: client,
111+
}
112+
}
113+
114+
func newHTTPClientEntryKey(hostName string, caData []byte) string {
115+
return httpClientEntry{
116+
hostName: hostName,
117+
caData: caData,
118+
}.Key()
119+
}
120+
121+
func (r httpClientEntry) Key() string {
122+
return fmt.Sprintf("%s/%s", r.hostName, string(r.caData))
92123
}
93124

94125
func (c *client) WarmUp(extensionConfigList *runtimev1.ExtensionConfigList) error {
@@ -108,16 +139,20 @@ func (c *client) Discover(ctx context.Context, extensionConfig *runtimev1.Extens
108139
return nil, errors.Wrapf(err, "failed to discover extension %q: failed to compute GVH of hook", extensionConfig.Name)
109140
}
110141

142+
httpClient, err := c.getHTTPClient(extensionConfig.Spec.ClientConfig)
143+
if err != nil {
144+
return nil, errors.Wrapf(err, "failed to discover extension %q: failed to get http client", extensionConfig.Name)
145+
}
146+
111147
request := &runtimehooksv1.DiscoveryRequest{}
112148
response := &runtimehooksv1.DiscoveryResponse{}
113149
opts := &httpCallOptions{
114-
certFile: c.certFile,
115-
keyFile: c.keyFile,
116150
catalog: c.catalog,
117151
config: extensionConfig.Spec.ClientConfig,
118152
registrationGVH: hookGVH,
119153
hookGVH: hookGVH,
120154
timeout: defaultDiscoveryTimeout,
155+
httpClient: httpClient,
121156
}
122157
if err := httpCall(ctx, request, response, opts); err != nil {
123158
return nil, errors.Wrapf(err, "failed to discover extension %q", extensionConfig.Name)
@@ -363,15 +398,19 @@ func (c *client) CallExtension(ctx context.Context, hook runtimecatalog.Hook, fo
363398
}
364399
}
365400

401+
httpClient, err := c.getHTTPClient(registration.ClientConfig)
402+
if err != nil {
403+
return errors.Wrapf(err, "failed to call extension handler %q: failed to get http client", name)
404+
}
405+
366406
httpOpts := &httpCallOptions{
367-
certFile: c.certFile,
368-
keyFile: c.keyFile,
369407
catalog: c.catalog,
370408
config: registration.ClientConfig,
371409
registrationGVH: registration.GroupVersionHook,
372410
hookGVH: hookGVH,
373411
name: strings.TrimSuffix(registration.Name, "."+registration.ExtensionConfigName),
374412
timeout: timeoutDuration,
413+
httpClient: httpClient,
375414
}
376415
err = httpCall(ctx, request, response, httpOpts)
377416
if err != nil {
@@ -413,6 +452,48 @@ func (c *client) CallExtension(ctx context.Context, hook runtimecatalog.Hook, fo
413452
return nil
414453
}
415454

455+
func (c *client) getHTTPClient(config runtimev1.ClientConfig) (*http.Client, error) {
456+
// Note: we are passing an empty gvh and "" as name because the only relevant part of the url
457+
// for this function is the Hostname, which derives from config (ghv and name are appended to the path).
458+
extensionURL, err := urlForExtension(config, runtimecatalog.GroupVersionHook{}, "")
459+
if err != nil {
460+
return nil, err
461+
}
462+
463+
if cacheEntry, ok := c.httpClientsCache.Has(newHTTPClientEntryKey(extensionURL.Hostname(), config.CABundle)); ok {
464+
return cacheEntry.client, nil
465+
}
466+
467+
httpClient, err := createHTTPClient(c.certFile, c.keyFile, config.CABundle, extensionURL.Hostname())
468+
if err != nil {
469+
return nil, err
470+
}
471+
472+
c.httpClientsCache.Add(newHTTPClientEntry(extensionURL.Hostname(), config.CABundle, httpClient))
473+
return httpClient, nil
474+
}
475+
476+
func createHTTPClient(certFile, keyFile string, caData []byte, hostName string) (*http.Client, error) {
477+
httpClient := &http.Client{}
478+
tlsConfig, err := transport.TLSConfigFor(&transport.Config{
479+
TLS: transport.TLSConfig{
480+
CertFile: certFile,
481+
KeyFile: keyFile,
482+
CAData: caData,
483+
ServerName: hostName,
484+
},
485+
})
486+
if err != nil {
487+
return nil, errors.Wrap(err, "failed to create tls config")
488+
}
489+
490+
// This also adds http2
491+
httpClient.Transport = utilnet.SetTransportDefaults(&http.Transport{
492+
TLSClientConfig: tlsConfig,
493+
})
494+
return httpClient, nil
495+
}
496+
416497
// cloneAndAddSettings creates a new request object and adds settings to it.
417498
func cloneAndAddSettings(request runtimehooksv1.RequestObject, registrationSettings map[string]string) runtimehooksv1.RequestObject {
418499
// Merge the settings from registration with the settings in the request.
@@ -431,14 +512,13 @@ func cloneAndAddSettings(request runtimehooksv1.RequestObject, registrationSetti
431512
}
432513

433514
type httpCallOptions struct {
434-
certFile string
435-
keyFile string
436515
catalog *runtimecatalog.Catalog
437516
config runtimev1.ClientConfig
438517
registrationGVH runtimecatalog.GroupVersionHook
439518
hookGVH runtimecatalog.GroupVersionHook
440519
name string
441520
timeout time.Duration
521+
httpClient *http.Client
442522
}
443523

444524
func httpCall(ctx context.Context, request, response runtime.Object, opts *httpCallOptions) error {
@@ -517,27 +597,8 @@ func httpCall(ctx context.Context, request, response runtime.Object, opts *httpC
517597
return errors.Wrap(err, "http call failed: failed to create http request")
518598
}
519599

520-
// Use client-go's transport.TLSConfigureFor to ensure good defaults for tls
521-
client := &http.Client{}
522-
defer client.CloseIdleConnections()
523-
524-
tlsConfig, err := transport.TLSConfigFor(&transport.Config{
525-
TLS: transport.TLSConfig{
526-
CertFile: opts.certFile,
527-
KeyFile: opts.keyFile,
528-
CAData: opts.config.CABundle,
529-
ServerName: extensionURL.Hostname(),
530-
},
531-
})
532-
if err != nil {
533-
return errors.Wrap(err, "http call failed: failed to create tls config")
534-
}
535-
// This also adds http2
536-
client.Transport = utilnet.SetTransportDefaults(&http.Transport{
537-
TLSClientConfig: tlsConfig,
538-
})
539-
540-
resp, err := client.Do(httpRequest)
600+
// Call the extension.
601+
resp, err := opts.httpClient.Do(httpRequest)
541602

542603
// Create http request metric.
543604
defer func() {

internal/runtime/client/client_test.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"fmt"
2525
"net/http"
2626
"net/http/httptest"
27+
"net/url"
2728
"os"
2829
"path/filepath"
2930
"regexp"
@@ -203,6 +204,15 @@ func TestClient_httpCall(t *testing.T) {
203204
// set url to srv for in tt.opts
204205
tt.opts.config.URL = srv.URL
205206
tt.opts.config.CABundle = testcerts.CACert
207+
208+
// set httpClient in tt.opts
209+
// Note: cert and key file are not necessary, because in this test the server do not requires client authentication with certificates signed by a given CA.
210+
u, err := url.Parse(srv.URL)
211+
g.Expect(err).ToNot(HaveOccurred())
212+
213+
httpClient, err := createHTTPClient("", "", testcerts.CACert, u.Hostname())
214+
g.Expect(err).ToNot(HaveOccurred())
215+
tt.opts.httpClient = httpClient
206216
}
207217

208218
err := httpCall(context.TODO(), tt.request, tt.response, tt.opts)
@@ -934,6 +944,92 @@ func TestClient_CallExtensionWithClientAuthentication(t *testing.T) {
934944
g.Expect(serverCallCount).To(Equal(1))
935945
}
936946

947+
func TestClient_GetHttpClient(t *testing.T) {
948+
g := NewWithT(t)
949+
950+
extension1 := runtimev1.ExtensionConfig{
951+
ObjectMeta: metav1.ObjectMeta{
952+
Name: "extension1",
953+
ResourceVersion: "15",
954+
},
955+
Spec: runtimev1.ExtensionConfigSpec{
956+
ClientConfig: runtimev1.ClientConfig{
957+
URL: "https://serverA.example.com/",
958+
CABundle: testcerts.CACert,
959+
},
960+
},
961+
}
962+
963+
extension2 := runtimev1.ExtensionConfig{
964+
ObjectMeta: metav1.ObjectMeta{
965+
Name: "extension2",
966+
ResourceVersion: "36",
967+
},
968+
Spec: runtimev1.ExtensionConfigSpec{
969+
ClientConfig: runtimev1.ClientConfig{
970+
URL: "https://serverA.example.com/",
971+
CABundle: testcerts.CACert,
972+
},
973+
},
974+
}
975+
976+
extension3 := runtimev1.ExtensionConfig{
977+
ObjectMeta: metav1.ObjectMeta{
978+
Name: "extension3",
979+
ResourceVersion: "54",
980+
},
981+
Spec: runtimev1.ExtensionConfigSpec{
982+
ClientConfig: runtimev1.ClientConfig{
983+
URL: "https://serverB.example.com/",
984+
CABundle: testcerts.CACert, // in a real example also CA should be different, but the host name is already enough to require a different client.
985+
},
986+
},
987+
}
988+
989+
c := New(Options{})
990+
991+
internalClient := c.(*client)
992+
g.Expect(internalClient.httpClientsCache.Len()).To(Equal(0))
993+
994+
// Get http client for extension 1
995+
gotClientExtension1, err := internalClient.getHTTPClient(extension1.Spec.ClientConfig)
996+
g.Expect(err).ToNot(HaveOccurred())
997+
g.Expect(gotClientExtension1).ToNot(BeNil())
998+
999+
// Check http client cache have only one item
1000+
g.Expect(internalClient.httpClientsCache.Len()).To(Equal(1))
1001+
_, ok := internalClient.httpClientsCache.Has(newHTTPClientEntryKey("serverA.example.com", extension1.Spec.ClientConfig.CABundle))
1002+
g.Expect(ok).To(BeTrue())
1003+
1004+
// Check http client cache is used for the same extension
1005+
gotClientExtension1Again, err := internalClient.getHTTPClient(extension1.Spec.ClientConfig)
1006+
g.Expect(err).ToNot(HaveOccurred())
1007+
g.Expect(gotClientExtension1Again).To(Equal(gotClientExtension1))
1008+
1009+
// Get http client for extension 2, same server
1010+
gotClientExtension2, err := internalClient.getHTTPClient(extension2.Spec.ClientConfig)
1011+
g.Expect(err).ToNot(HaveOccurred())
1012+
g.Expect(gotClientExtension2).ToNot(BeNil())
1013+
g.Expect(gotClientExtension2).To(Equal(gotClientExtension1))
1014+
1015+
// Check http client cache have two items
1016+
g.Expect(internalClient.httpClientsCache.Len()).To(Equal(1))
1017+
_, ok = internalClient.httpClientsCache.Has(newHTTPClientEntryKey("serverA.example.com", extension2.Spec.ClientConfig.CABundle))
1018+
g.Expect(ok).To(BeTrue())
1019+
1020+
// Get http client for extension 3, another server
1021+
gotClientExtension3, err := internalClient.getHTTPClient(extension3.Spec.ClientConfig)
1022+
g.Expect(err).ToNot(HaveOccurred())
1023+
g.Expect(gotClientExtension3).ToNot(BeNil())
1024+
1025+
// Check http client cache have two items
1026+
g.Expect(internalClient.httpClientsCache.Len()).To(Equal(2))
1027+
_, ok = internalClient.httpClientsCache.Has(newHTTPClientEntryKey("serverA.example.com", extension1.Spec.ClientConfig.CABundle))
1028+
g.Expect(ok).To(BeTrue())
1029+
_, ok = internalClient.httpClientsCache.Has(newHTTPClientEntryKey("serverB.example.com", extension2.Spec.ClientConfig.CABundle))
1030+
g.Expect(ok).To(BeTrue())
1031+
}
1032+
9371033
func cacheKeyFunc(extensionName, extensionConfigResourceVersion string, request runtimehooksv1.RequestObject) string {
9381034
// Note: extensionName is identical to the value of the name parameter passed into CallExtension.
9391035
s := fmt.Sprintf("%s-%s", extensionName, extensionConfigResourceVersion)

0 commit comments

Comments
 (0)