Skip to content

Commit 8c9c3cf

Browse files
authored
Merge pull request #1 from rejoshed/feature/client-caching
Use client cache to reduce the number of client connections
2 parents 69dd5bd + a4f3dd6 commit 8c9c3cf

File tree

7 files changed

+182
-14
lines changed

7 files changed

+182
-14
lines changed

config/rbac/role.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ metadata:
66
creationTimestamp: null
77
name: manager-role
88
rules:
9+
- apiGroups:
10+
- ""
11+
resources:
12+
- configmaps
13+
verbs:
14+
- get
15+
- list
16+
- watch
917
- apiGroups:
1018
- ""
1119
resources:

controllers/cloudstackcluster_controller.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import (
3939
// RBAC permissions used in all reconcilers. Events and Secrets.
4040
// "" empty string as the api group indicates core kubernetes objects. "*" indicates all objects.
4141
// +kubebuilder:rbac:groups="",resources=secrets;,verbs=get;list;watch
42+
// +kubebuilder:rbac:groups="",resources=configmaps;,verbs=get;list;watch
4243
// +kubebuilder:rbac:groups="",resources=events,verbs=get;list;watch;create;update;patch
4344

4445
// RBAC permissions for CloudStackCluster.

controllers/utils/failuredomains.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,12 @@ func (c *CloudClientImplementation) AsFailureDomainUser(fdSpec *infrav1.CloudSta
135135
return ctrl.Result{}, errors.Wrapf(err, "getting ACSEndpoint secret with ref: %v", fdSpec.ACSEndpoint)
136136
}
137137

138+
clientConfig := &corev1.ConfigMap{}
139+
key = client.ObjectKey{Name: cloud.ClientConfigMapName, Namespace: cloud.ClientConfigMapNamespace}
140+
_ = c.K8sClient.Get(c.RequestCtx, key, clientConfig)
141+
138142
var err error
139-
if c.CSClient, err = cloud.NewClientFromK8sSecret(endpointCredentials); err != nil {
143+
if c.CSClient, err = cloud.NewClientFromK8sSecret(endpointCredentials, clientConfig); err != nil {
140144
return ctrl.Result{}, errors.Wrapf(err, "parsing ACSEndpoint secret with ref: %v", fdSpec.ACSEndpoint)
141145
}
142146

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module sigs.k8s.io/cluster-api-provider-cloudstack
33
go 1.16
44

55
require (
6+
github.com/ReneKroon/ttlcache v1.7.0 // indirect
67
github.com/apache/cloudstack-go/v2 v2.13.0
78
github.com/go-logr/logr v1.2.3
89
github.com/golang/mock v1.6.0

go.sum

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMo
7070
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
7171
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
7272
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
73+
github.com/ReneKroon/ttlcache v1.7.0 h1:8BkjFfrzVFXyrqnMtezAaJ6AHPSsVV10m6w28N/Fgkk=
74+
github.com/ReneKroon/ttlcache v1.7.0/go.mod h1:8BGGzdumrIjWxdRx8zpK6L3oGMWvIXdvB2GD1cfvd+I=
7375
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
7476
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
7577
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
@@ -600,6 +602,7 @@ go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5/go.mod h1:nmDLcffg48OtT/PSW0H
600602
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
601603
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
602604
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
605+
go.uber.org/goleak v0.10.0/go.mod h1:VCZuO8V8mFPlL0F5J5GK1rtHV3DrFcQ1R8ryq7FK0aI=
603606
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
604607
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
605608
go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA=

pkg/cloud/client.go

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,18 @@ package cloud
1818

1919
import (
2020
"bytes"
21+
"fmt"
2122
"io"
2223
"os"
24+
"sync"
25+
"time"
2326

2427
corev1 "k8s.io/api/core/v1"
2528

2629
"gopkg.in/yaml.v3"
2730
"sigs.k8s.io/cluster-api-provider-cloudstack/pkg/metrics"
2831

32+
"github.com/ReneKroon/ttlcache"
2933
"github.com/apache/cloudstack-go/v2/cloudstack"
3034
"github.com/pkg/errors"
3135
)
@@ -66,6 +70,14 @@ type SecretConfig struct {
6670
StringData Config `yaml:"stringData"`
6771
}
6872

73+
var clientCache *ttlcache.Cache
74+
var cacheMutex sync.Mutex
75+
76+
const ClientConfigMapName = "capc-client-config"
77+
const ClientConfigMapNamespace = "capc-system"
78+
const ClientCacheTTLKey = "client-cache-ttl"
79+
const DefaultClientCacheTTL = time.Duration(1 * time.Hour)
80+
6981
// UnmarshalAllSecretConfigs parses a yaml document for each secret.
7082
func UnmarshalAllSecretConfigs(in []byte, out *[]SecretConfig) error {
7183
r := bytes.NewReader(in)
@@ -84,7 +96,8 @@ func UnmarshalAllSecretConfigs(in []byte, out *[]SecretConfig) error {
8496
return nil
8597
}
8698

87-
func NewClientFromK8sSecret(endpointSecret *corev1.Secret) (Client, error) {
99+
// NewClientFromK8sSecret returns a client from a k8s secret
100+
func NewClientFromK8sSecret(endpointSecret *corev1.Secret, clientConfig *corev1.ConfigMap) (Client, error) {
88101
endpointSecretStrings := map[string]string{}
89102
for k, v := range endpointSecret.Data {
90103
endpointSecretStrings[k] = string(v)
@@ -93,19 +106,19 @@ func NewClientFromK8sSecret(endpointSecret *corev1.Secret) (Client, error) {
93106
if err != nil {
94107
return nil, err
95108
}
96-
return NewClientFromBytesConfig(bytes)
109+
return NewClientFromBytesConfig(bytes, clientConfig)
97110
}
98111

99112
// NewClientFromBytesConfig returns a client from a bytes array that unmarshals to a yaml config.
100-
func NewClientFromBytesConfig(conf []byte) (Client, error) {
113+
func NewClientFromBytesConfig(conf []byte, clientConfig *corev1.ConfigMap) (Client, error) {
101114
r := bytes.NewReader(conf)
102115
dec := yaml.NewDecoder(r)
103116
var config Config
104117
if err := dec.Decode(&config); err != nil {
105118
return nil, err
106119
}
107120

108-
return NewClientFromConf(config)
121+
return NewClientFromConf(config, clientConfig)
109122
}
110123

111124
// NewClientFromYamlPath returns a client from a yaml config at path.
@@ -129,11 +142,23 @@ func NewClientFromYamlPath(confPath string, secretName string) (Client, error) {
129142
return nil, errors.Errorf("config with secret name %s not found", secretName)
130143
}
131144

132-
return NewClientFromConf(conf)
145+
return NewClientFromConf(conf, nil)
133146
}
134147

135-
// Creates a new Cloud Client form a map of strings to strings.
136-
func NewClientFromConf(conf Config) (Client, error) {
148+
// NewClientFromConf creates a new Cloud Client form a map of strings to strings.
149+
func NewClientFromConf(conf Config, clientConfig *corev1.ConfigMap) (Client, error) {
150+
cacheMutex.Lock()
151+
defer cacheMutex.Unlock()
152+
153+
if clientCache == nil {
154+
clientCache = newClientCache(clientConfig)
155+
}
156+
157+
clientCacheKey := generateClientCacheKey(conf)
158+
if client, exists := clientCache.Get(clientCacheKey); exists {
159+
return client.(Client), nil
160+
}
161+
137162
verifySSL := true
138163
if conf.VerifySSL == "false" {
139164
verifySSL = false
@@ -146,6 +171,8 @@ func NewClientFromConf(conf Config) (Client, error) {
146171
c.cs = cloudstack.NewAsyncClient(conf.APIUrl, conf.APIKey, conf.SecretKey, verifySSL)
147172
c.csAsync = cloudstack.NewClient(conf.APIUrl, conf.APIKey, conf.SecretKey, verifySSL)
148173
c.customMetrics = metrics.NewCustomMetrics()
174+
clientCache.Set(clientCacheKey, c)
175+
149176
return c, nil
150177
}
151178

@@ -163,11 +190,38 @@ func (c *client) NewClientInDomainAndAccount(domain string, account string) (Cli
163190
c.config.APIKey = user.APIKey
164191
c.config.SecretKey = user.SecretKey
165192

166-
return NewClientFromConf(c.config)
193+
return NewClientFromConf(c.config, nil)
167194
}
168195

169-
// Create a client from a CloudStack-Go API client. Mostly used for testing.
196+
// NewClientFromCSAPIClient creates a client from a CloudStack-Go API client. Mostly used for testing.
170197
func NewClientFromCSAPIClient(cs *cloudstack.CloudStackClient) Client {
171198
c := &client{cs: cs, csAsync: cs, customMetrics: metrics.NewCustomMetrics()}
172199
return c
173200
}
201+
202+
// generateClientCacheKey generates a cache key from a Config
203+
func generateClientCacheKey(conf Config) string {
204+
return fmt.Sprintf("%+v", conf)
205+
}
206+
207+
// newClientCache returns a new instance of client cache
208+
func newClientCache(clientConfig *corev1.ConfigMap) *ttlcache.Cache {
209+
clientCache := ttlcache.NewCache()
210+
clientCache.SetTTL(GetClientCacheTTL(clientConfig))
211+
clientCache.SkipTtlExtensionOnHit(false)
212+
return clientCache
213+
}
214+
215+
// GetClientCacheTTL returns a client cache TTL duration from the passed config map
216+
func GetClientCacheTTL(clientConfig *corev1.ConfigMap) time.Duration {
217+
var cacheTTL time.Duration
218+
if clientConfig != nil {
219+
if ttl, exists := clientConfig.Data[ClientCacheTTLKey]; exists {
220+
cacheTTL, _ = time.ParseDuration(ttl)
221+
}
222+
}
223+
if cacheTTL == 0 {
224+
cacheTTL = DefaultClientCacheTTL
225+
}
226+
return cacheTTL
227+
}

pkg/cloud/client_test.go

Lines changed: 101 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ package cloud_test
1818

1919
import (
2020
"os"
21+
"time"
22+
23+
corev1 "k8s.io/api/core/v1"
2124

2225
. "github.com/onsi/ginkgo/v2"
2326
. "github.com/onsi/gomega"
@@ -36,17 +39,18 @@ var _ = Describe("Instance", func() {
3639
var ()
3740

3841
BeforeEach(func() {
39-
// This test fixture is useful for development, but the actual method of parsing is confinded to the client's
40-
// new client method. The parsing used here is more of a schema, and we don't need to test another library's
41-
// abilities to parse said schema.
42-
Skip("Dev test suite.")
4342
})
4443

4544
AfterEach(func() {
4645
})
4746

4847
Context("When fetching a YAML config.", func() {
4948
It("Handles the positive case.", func() {
49+
// This test fixture is useful for development, but the actual method of parsing is confinded to the client's
50+
// new client method. The parsing used here is more of a schema, and we don't need to test another library's
51+
// abilities to parse said schema.
52+
Skip("Dev test suite.")
53+
5054
// Create a real cloud client.
5155
var connectionErr error
5256
_, connectionErr = helpers.NewCSClient()
@@ -56,4 +60,97 @@ var _ = Describe("Instance", func() {
5660
Ω(connectionErr).ShouldNot(HaveOccurred())
5761
})
5862
})
63+
64+
Context("GetClientCacheTTL", func() {
65+
It("Returns the default TTL when a nil is passed", func() {
66+
result := cloud.GetClientCacheTTL(nil)
67+
Ω(result).Should(Equal(cloud.DefaultClientCacheTTL))
68+
})
69+
70+
It("Returns the default TTL when an empty config map is passed", func() {
71+
clientConfig := &corev1.ConfigMap{}
72+
result := cloud.GetClientCacheTTL(clientConfig)
73+
Ω(result).Should(Equal(cloud.DefaultClientCacheTTL))
74+
})
75+
76+
It("Returns the default TTL when the TTL key does not exist", func() {
77+
clientConfig := &corev1.ConfigMap{}
78+
clientConfig.Data = map[string]string{}
79+
clientConfig.Data[cloud.ClientCacheTTLKey+"XXXX"] = "1m5s"
80+
result := cloud.GetClientCacheTTL(clientConfig)
81+
Ω(result).Should(Equal(cloud.DefaultClientCacheTTL))
82+
})
83+
84+
It("Returns the default TTL when the TTL value is invalid", func() {
85+
clientConfig := &corev1.ConfigMap{}
86+
clientConfig.Data = map[string]string{}
87+
clientConfig.Data[cloud.ClientCacheTTLKey] = "5mXXX"
88+
result := cloud.GetClientCacheTTL(clientConfig)
89+
Ω(result).Should(Equal(cloud.DefaultClientCacheTTL))
90+
})
91+
92+
It("Returns the TTL from the input clientConfig map", func() {
93+
clientConfig := &corev1.ConfigMap{}
94+
clientConfig.Data = map[string]string{}
95+
clientConfig.Data[cloud.ClientCacheTTLKey] = "5m10s"
96+
expected, _ := time.ParseDuration("5m10s")
97+
result := cloud.GetClientCacheTTL(clientConfig)
98+
Ω(result).Should(Equal(expected))
99+
})
100+
})
101+
102+
Context("NewClientFromConf", func() {
103+
clientConfig := &corev1.ConfigMap{}
104+
105+
BeforeEach(func() {
106+
clientConfig.Data = map[string]string{}
107+
clientConfig.Data[cloud.ClientCacheTTLKey] = "100ms"
108+
})
109+
110+
It("Returns a new client", func() {
111+
config := cloud.Config{
112+
APIUrl: "http://1.1.1.1",
113+
}
114+
result, err := cloud.NewClientFromConf(config, clientConfig)
115+
Ω(err).ShouldNot(HaveOccurred())
116+
Ω(result).ShouldNot(BeNil())
117+
})
118+
119+
It("Returns a new client for a different config", func() {
120+
config1 := cloud.Config{
121+
APIUrl: "http://2.2.2.2",
122+
}
123+
config2 := cloud.Config{
124+
APIUrl: "http://3.3.3.3",
125+
}
126+
result1, _ := cloud.NewClientFromConf(config1, clientConfig)
127+
result2, _ := cloud.NewClientFromConf(config2, clientConfig)
128+
Ω(result1).ShouldNot(Equal(result2))
129+
})
130+
131+
It("Returns a cached client for the same config", func() {
132+
config1 := cloud.Config{
133+
APIUrl: "http://4.4.4.4",
134+
}
135+
config2 := cloud.Config{
136+
APIUrl: "http://4.4.4.4",
137+
}
138+
result1, _ := cloud.NewClientFromConf(config1, clientConfig)
139+
result2, _ := cloud.NewClientFromConf(config2, clientConfig)
140+
Ω(result1).Should(Equal(result2))
141+
})
142+
143+
It("Returns a new client after cache expiration", func() {
144+
config1 := cloud.Config{
145+
APIUrl: "http://5.5.5.5",
146+
}
147+
config2 := cloud.Config{
148+
APIUrl: "http://5.5.5.5",
149+
}
150+
result1, _ := cloud.NewClientFromConf(config1, clientConfig)
151+
time.Sleep(150 * time.Millisecond)
152+
result2, _ := cloud.NewClientFromConf(config2, clientConfig)
153+
Ω(result1).ShouldNot(Equal(result2))
154+
})
155+
})
59156
})

0 commit comments

Comments
 (0)