-
Notifications
You must be signed in to change notification settings - Fork 14
/
cache.go
204 lines (167 loc) · 7.16 KB
/
cache.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
package v3
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"sync"
"github.com/nutanix-cloud-native/prism-go-client"
"github.com/nutanix-cloud-native/prism-go-client/environment/types"
)
type clientCacheMap map[string]*Client
var (
// ErrorClientNotFound is returned when the client is not found in the cache
ErrorClientNotFound = errors.New("client not found in client cache")
// ErrorPrismAddressNotSet is returned when the address is not set for Nutanix Prism Central
ErrorPrismAddressNotSet = errors.New("address not set for Nutanix Prism Central")
// ErrorPrismPortNotSet is returned when the port is not set for Nutanix Prism Central
ErrorPrismPortNotSet = errors.New("port not set for Nutanix Prism Central")
// ErrorPrismUsernameNotSet is returned when the username is not set for Nutanix Prism Central
ErrorPrismUsernameNotSet = errors.New("username not set for Nutanix Prism Central")
// ErrorPrismPasswordNotSet is returned when the password is not set for Nutanix Prism Central
ErrorPrismPasswordNotSet = errors.New("password not set for Nutanix Prism Central")
)
// ClientCache is a cache for prism clients
type ClientCache struct {
cache clientCacheMap
validationHashes map[string]string
mtx sync.RWMutex
useSessionAuth bool
}
// CacheOpts is a functional option for the ClientCache
type CacheOpts func(*ClientCache)
// WithSessionAuth sets the session auth for the ClientCache
// If sessionAuth is true, the client will use session auth instead of basic auth for authentication of requests
// If sessionAuth is false, the client will use basic auth for authentication of requests
func WithSessionAuth(sessionAuth bool) CacheOpts {
return func(c *ClientCache) {
c.useSessionAuth = sessionAuth
}
}
// NewClientCache returns a new ClientCache
func NewClientCache(opts ...CacheOpts) *ClientCache {
cache := &ClientCache{
cache: make(clientCacheMap),
validationHashes: make(map[string]string),
mtx: sync.RWMutex{},
}
for _, opt := range opts {
opt(cache)
}
return cache
}
// CachedClientParams define the interface that needs to be implemented by an object that will be used to create
// a cached client.
type CachedClientParams interface {
// ManagementEndpoint returns the struct containing all information needed to construct a new client
// and is used to calculate the validation hash for the client for the purpose of cache invalidation.
// The validation hash is calculated based on the serialized version of the ManagementEndpoint.
ManagementEndpoint() types.ManagementEndpoint
// Key returns a unique key for the client that is used to store the client in the cache
Key() string
}
// GetOrCreate returns the client for the given client name and endpoint.
// - If the client is not found in the cache, it creates a new client, adds it to the cache, and returns it
// - If the client is found in the cache, it validates whether the client is still valid by comparing validation hashes
// - If the client is found in the cache and the validation hash is the same, it returns the client
// - If the client is found in the cache and the validation hash is different, it regenerates the client, updates the cache, and returns the client
// func (c *ClientCache) GetOrCreate(clientName string, endpoint types.ManagementEndpoint, opts ...ClientOption) (*Client, error) {
func (c *ClientCache) GetOrCreate(cachedClientParams CachedClientParams, opts ...ClientOption) (*Client, error) {
currentValidationHash, err := validationHashFromEndpoint(cachedClientParams.ManagementEndpoint())
if err != nil {
return nil, fmt.Errorf("failed to calculate validation hash for cachedClientParams with key %s: %w", cachedClientParams.Key(), err)
}
client, validationHash, err := c.get(cachedClientParams.Key())
if err != nil {
if !errors.Is(err, ErrorClientNotFound) {
return nil, fmt.Errorf("failed to get client with key %s from cache: %w", cachedClientParams.Key(), err)
}
}
if validationHash == currentValidationHash {
// validation hash is the same, return the client
return client, nil
}
// validation hash is different, regenerate the client
c.Delete(cachedClientParams)
credentials := prismgoclient.Credentials{
URL: cachedClientParams.ManagementEndpoint().Address.Host,
Endpoint: cachedClientParams.ManagementEndpoint().Address.Host,
Insecure: cachedClientParams.ManagementEndpoint().Insecure,
Username: cachedClientParams.ManagementEndpoint().ApiCredentials.Username,
Password: cachedClientParams.ManagementEndpoint().ApiCredentials.Password,
SessionAuth: c.useSessionAuth,
}
setDefaultsForCredentials(&credentials)
if err := validateCredentials(credentials); err != nil {
return nil, fmt.Errorf("failed to validate credentials for cachedClientParams with key %s: %w", cachedClientParams.Key(), err)
}
if cachedClientParams.ManagementEndpoint().AdditionalTrustBundle != "" {
opts = append(opts, WithPEMEncodedCertBundle([]byte(cachedClientParams.ManagementEndpoint().AdditionalTrustBundle)))
}
client, err = NewV3Client(credentials, opts...)
if err != nil {
return nil, fmt.Errorf("failed to create client for cachedClientParams with key %s: %w", cachedClientParams.Key(), err)
}
c.set(cachedClientParams.Key(), currentValidationHash, client)
return client, nil
}
func validationHashFromEndpoint(endpoint types.ManagementEndpoint) (string, error) {
// Note: this will only work reliably as long as types.ManagementEndpoint is predictably serializable i.e. does
// not contain a map. Due to randomized ordering of map keys in Go, we would constantly invalidate caches
// if the ManagementEndpoint has a map.
serializedEndpoint, err := json.Marshal(endpoint)
if err != nil {
return "", err
}
hasher := sha256.New()
hasher.Write(serializedEndpoint)
hashedBytes := hasher.Sum(nil)
currentValidationHash := hex.EncodeToString(hashedBytes)
return currentValidationHash, nil
}
func setDefaultsForCredentials(credentials *prismgoclient.Credentials) {
if credentials.Port == "" {
credentials.Port = "9440"
}
if credentials.URL == "" {
credentials.URL = fmt.Sprintf("%s:%s", credentials.Endpoint, credentials.Port)
}
}
func validateCredentials(credentials prismgoclient.Credentials) error {
if credentials.Username == "" {
return ErrorPrismUsernameNotSet
}
if credentials.Password == "" {
return ErrorPrismPasswordNotSet
}
return nil
}
// Get returns the client and the validation hash for the given client name
func (c *ClientCache) get(clientName string) (*Client, string, error) {
c.mtx.RLock()
defer c.mtx.RUnlock()
clnt, ok := c.cache[clientName]
if !ok {
return nil, "", ErrorClientNotFound
}
validationHash, ok := c.validationHashes[clientName]
if !ok {
return clnt, "", nil
}
return clnt, validationHash, nil
}
// Set adds the client to the cache
func (c *ClientCache) set(clientName string, validationHash string, client *Client) {
c.mtx.Lock()
defer c.mtx.Unlock()
c.cache[clientName] = client
c.validationHashes[clientName] = validationHash
}
// Delete removes the client from the cache
func (c *ClientCache) Delete(params CachedClientParams) {
c.mtx.Lock()
defer c.mtx.Unlock()
delete(c.cache, params.Key())
delete(c.validationHashes, params.Key())
}