Skip to content

Commit 64015e6

Browse files
fabriziopandinik8s-infra-cherrypick-robot
authored andcommitted
Add httpClientCache to runtime client
1 parent 25d56dd commit 64015e6

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
@@ -52,6 +52,7 @@ import (
5252
runtimemetrics "sigs.k8s.io/cluster-api/internal/runtime/metrics"
5353
runtimeregistry "sigs.k8s.io/cluster-api/internal/runtime/registry"
5454
"sigs.k8s.io/cluster-api/util"
55+
"sigs.k8s.io/cluster-api/util/cache"
5556
)
5657

5758
type errCallingExtensionHandler error
@@ -70,22 +71,52 @@ type Options struct {
7071
// New returns a new Client.
7172
func New(options Options) runtimeclient.Client {
7273
return &client{
73-
certFile: options.CertFile,
74-
keyFile: options.KeyFile,
75-
catalog: options.Catalog,
76-
registry: options.Registry,
77-
client: options.Client,
74+
certFile: options.CertFile,
75+
keyFile: options.KeyFile,
76+
catalog: options.Catalog,
77+
registry: options.Registry,
78+
client: options.Client,
79+
httpClientsCache: cache.New[httpClientEntry](24 * time.Hour),
7880
}
7981
}
8082

8183
var _ runtimeclient.Client = &client{}
8284

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

91122
func (c *client) WarmUp(extensionConfigList *runtimev1.ExtensionConfigList) error {
@@ -105,16 +136,20 @@ func (c *client) Discover(ctx context.Context, extensionConfig *runtimev1.Extens
105136
return nil, errors.Wrapf(err, "failed to discover extension %q: failed to compute GVH of hook", extensionConfig.Name)
106137
}
107138

139+
httpClient, err := c.getHTTPClient(extensionConfig.Spec.ClientConfig)
140+
if err != nil {
141+
return nil, errors.Wrapf(err, "failed to discover extension %q: failed to get http client", extensionConfig.Name)
142+
}
143+
108144
request := &runtimehooksv1.DiscoveryRequest{}
109145
response := &runtimehooksv1.DiscoveryResponse{}
110146
opts := &httpCallOptions{
111-
certFile: c.certFile,
112-
keyFile: c.keyFile,
113147
catalog: c.catalog,
114148
config: extensionConfig.Spec.ClientConfig,
115149
registrationGVH: hookGVH,
116150
hookGVH: hookGVH,
117151
timeout: defaultDiscoveryTimeout,
152+
httpClient: httpClient,
118153
}
119154
if err := httpCall(ctx, request, response, opts); err != nil {
120155
return nil, errors.Wrapf(err, "failed to discover extension %q", extensionConfig.Name)
@@ -336,15 +371,19 @@ func (c *client) CallExtension(ctx context.Context, hook runtimecatalog.Hook, fo
336371
}
337372
}
338373

374+
httpClient, err := c.getHTTPClient(registration.ClientConfig)
375+
if err != nil {
376+
return errors.Wrapf(err, "failed to call extension handler %q: failed to get http client", name)
377+
}
378+
339379
httpOpts := &httpCallOptions{
340-
certFile: c.certFile,
341-
keyFile: c.keyFile,
342380
catalog: c.catalog,
343381
config: registration.ClientConfig,
344382
registrationGVH: registration.GroupVersionHook,
345383
hookGVH: hookGVH,
346384
name: strings.TrimSuffix(registration.Name, "."+registration.ExtensionConfigName),
347385
timeout: timeoutDuration,
386+
httpClient: httpClient,
348387
}
349388
err = httpCall(ctx, request, response, httpOpts)
350389
if err != nil {
@@ -388,6 +427,48 @@ func (c *client) CallExtension(ctx context.Context, hook runtimecatalog.Hook, fo
388427
return nil
389428
}
390429

430+
func (c *client) getHTTPClient(config runtimev1.ClientConfig) (*http.Client, error) {
431+
// Note: we are passing an empty gvh and "" as name because the only relevant part of the url
432+
// for this function is the Hostname, which derives from config (ghv and name are appended to the path).
433+
extensionURL, err := urlForExtension(config, runtimecatalog.GroupVersionHook{}, "")
434+
if err != nil {
435+
return nil, err
436+
}
437+
438+
if cacheEntry, ok := c.httpClientsCache.Has(newHTTPClientEntryKey(extensionURL.Hostname(), config.CABundle)); ok {
439+
return cacheEntry.client, nil
440+
}
441+
442+
httpClient, err := createHTTPClient(c.certFile, c.keyFile, config.CABundle, extensionURL.Hostname())
443+
if err != nil {
444+
return nil, err
445+
}
446+
447+
c.httpClientsCache.Add(newHTTPClientEntry(extensionURL.Hostname(), config.CABundle, httpClient))
448+
return httpClient, nil
449+
}
450+
451+
func createHTTPClient(certFile, keyFile string, caData []byte, hostName string) (*http.Client, error) {
452+
httpClient := &http.Client{}
453+
tlsConfig, err := transport.TLSConfigFor(&transport.Config{
454+
TLS: transport.TLSConfig{
455+
CertFile: certFile,
456+
KeyFile: keyFile,
457+
CAData: caData,
458+
ServerName: hostName,
459+
},
460+
})
461+
if err != nil {
462+
return nil, errors.Wrap(err, "failed to create tls config")
463+
}
464+
465+
// This also adds http2
466+
httpClient.Transport = utilnet.SetTransportDefaults(&http.Transport{
467+
TLSClientConfig: tlsConfig,
468+
})
469+
return httpClient, nil
470+
}
471+
391472
// cloneAndAddSettings creates a new request object and adds settings to it.
392473
func cloneAndAddSettings(request runtimehooksv1.RequestObject, registrationSettings map[string]string) runtimehooksv1.RequestObject {
393474
// Merge the settings from registration with the settings in the request.
@@ -406,14 +487,13 @@ func cloneAndAddSettings(request runtimehooksv1.RequestObject, registrationSetti
406487
}
407488

408489
type httpCallOptions struct {
409-
certFile string
410-
keyFile string
411490
catalog *runtimecatalog.Catalog
412491
config runtimev1.ClientConfig
413492
registrationGVH runtimecatalog.GroupVersionHook
414493
hookGVH runtimecatalog.GroupVersionHook
415494
name string
416495
timeout time.Duration
496+
httpClient *http.Client
417497
}
418498

419499
func httpCall(ctx context.Context, request, response runtime.Object, opts *httpCallOptions) error {
@@ -492,27 +572,8 @@ func httpCall(ctx context.Context, request, response runtime.Object, opts *httpC
492572
return errors.Wrap(err, "http call failed: failed to create http request")
493573
}
494574

495-
// Use client-go's transport.TLSConfigureFor to ensure good defaults for tls
496-
client := &http.Client{}
497-
defer client.CloseIdleConnections()
498-
499-
tlsConfig, err := transport.TLSConfigFor(&transport.Config{
500-
TLS: transport.TLSConfig{
501-
CertFile: opts.certFile,
502-
KeyFile: opts.keyFile,
503-
CAData: opts.config.CABundle,
504-
ServerName: extensionURL.Hostname(),
505-
},
506-
})
507-
if err != nil {
508-
return errors.Wrap(err, "http call failed: failed to create tls config")
509-
}
510-
// This also adds http2
511-
client.Transport = utilnet.SetTransportDefaults(&http.Transport{
512-
TLSClientConfig: tlsConfig,
513-
})
514-
515-
resp, err := client.Do(httpRequest)
575+
// Call the extension.
576+
resp, err := opts.httpClient.Do(httpRequest)
516577

517578
// Create http request metric.
518579
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"
@@ -201,6 +202,15 @@ func TestClient_httpCall(t *testing.T) {
201202
// set url to srv for in tt.opts
202203
tt.opts.config.URL = ptr.To(srv.URL)
203204
tt.opts.config.CABundle = testcerts.CACert
205+
206+
// set httpClient in tt.opts
207+
// 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.
208+
u, err := url.Parse(srv.URL)
209+
g.Expect(err).ToNot(HaveOccurred())
210+
211+
httpClient, err := createHTTPClient("", "", testcerts.CACert, u.Hostname())
212+
g.Expect(err).ToNot(HaveOccurred())
213+
tt.opts.httpClient = httpClient
204214
}
205215

206216
err := httpCall(context.TODO(), tt.request, tt.response, tt.opts)
@@ -941,6 +951,92 @@ func TestClient_CallExtensionWithClientAuthentication(t *testing.T) {
941951
g.Expect(serverCallCount).To(Equal(1))
942952
}
943953

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

0 commit comments

Comments
 (0)