Skip to content

Commit b0b6f49

Browse files
committed
Cache OpenStack ProviderClient per auth context
The seven controllers using the default credentials each call AuthenticatedClient from SetupWithManager, deserialising the full Keystone service catalog every time. With a large multi-region catalog this is wasteful regardless of whether it is a proximate OOM cause. Providers are now cached per auth context (AuthURL, project, user, and their domains) so Keystone is hit once for the default credentials and once for the test-project credentials.
1 parent 0e8e58c commit b0b6f49

2 files changed

Lines changed: 80 additions & 11 deletions

File tree

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ require (
8585
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect
8686
golang.org/x/net v0.55.0 // indirect
8787
golang.org/x/oauth2 v0.36.0 // indirect
88-
golang.org/x/sync v0.21.0 // indirect
88+
golang.org/x/sync v0.21.0
8989
golang.org/x/sys v0.46.0 // indirect
9090
golang.org/x/term v0.43.0 // indirect
9191
golang.org/x/text v0.38.0 // indirect

internal/openstack/service_client.go

Lines changed: 79 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,83 @@ import (
2222
"os"
2323
"os/exec"
2424
"strings"
25+
"sync"
2526

2627
"github.com/gophercloud/gophercloud/v2"
2728
"github.com/gophercloud/utils/v2/openstack/clientconfig"
29+
"golang.org/x/sync/singleflight"
2830
)
2931

30-
// GetServiceClient returns an gophercloud ServiceClient for the given serviceType.
32+
// providerCache caches ProviderClients keyed by a string derived from the
33+
// authInfo, so multiple GetServiceClient calls with the same credentials
34+
// only authenticate against Keystone once.
35+
var (
36+
providerCacheMu sync.Mutex
37+
providerCache = map[string]*gophercloud.ProviderClient{}
38+
providerGroup singleflight.Group
39+
)
40+
41+
func cacheKey(authInfo *clientconfig.AuthInfo) string {
42+
if authInfo == nil {
43+
return ""
44+
}
45+
// Include all fields that affect the resulting Keystone token and catalog
46+
// so a future auth context differing only in user, domain, or AuthURL
47+
// doesn't silently reuse the wrong cached provider.
48+
return strings.Join([]string{
49+
authInfo.AuthURL,
50+
authInfo.ProjectName,
51+
authInfo.ProjectDomainName,
52+
authInfo.Username,
53+
authInfo.UserDomainName,
54+
}, "\x00")
55+
}
56+
57+
// GetServiceClient returns a ServiceClient for the given serviceType.
58+
// Providers are cached per auth context so Keystone is only hit once per
59+
// unique set of credentials across all SetupWithManager calls. Concurrent
60+
// callers with the same key share a single in-flight request via singleflight,
61+
// preventing duplicate Keystone round-trips on startup.
3162
func GetServiceClient(ctx context.Context, serviceType string, authInfo *clientconfig.AuthInfo) (*gophercloud.ServiceClient, error) {
63+
key := cacheKey(authInfo)
64+
65+
providerCacheMu.Lock()
66+
provider, ok := providerCache[key]
67+
providerCacheMu.Unlock()
68+
69+
if !ok {
70+
v, err, _ := providerGroup.Do(key, func() (any, error) {
71+
// Re-check under the group: another goroutine may have populated
72+
// the cache while we were waiting for the singleflight slot.
73+
providerCacheMu.Lock()
74+
if p, hit := providerCache[key]; hit {
75+
providerCacheMu.Unlock()
76+
return p, nil
77+
}
78+
providerCacheMu.Unlock()
79+
80+
p, err := NewProviderClient(ctx, authInfo)
81+
if err != nil {
82+
return nil, err
83+
}
84+
providerCacheMu.Lock()
85+
providerCache[key] = p
86+
providerCacheMu.Unlock()
87+
return p, nil
88+
})
89+
if err != nil {
90+
return nil, err
91+
}
92+
provider = v.(*gophercloud.ProviderClient)
93+
}
94+
95+
return ServiceClientFromProvider(provider, serviceType)
96+
}
97+
98+
// NewProviderClient authenticates against OpenStack and returns a ProviderClient
99+
// that can be reused across multiple service clients via ServiceClientFromProvider,
100+
// avoiding repeated Keystone round-trips and catalog deserialisations.
101+
func NewProviderClient(ctx context.Context, authInfo *clientconfig.AuthInfo) (*gophercloud.ProviderClient, error) {
32102
if authInfo == nil {
33103
authInfo = &clientconfig.AuthInfo{}
34104
}
@@ -46,19 +116,18 @@ func GetServiceClient(ctx context.Context, serviceType string, authInfo *clientc
46116

47117
var clientOpts clientconfig.ClientOpts
48118
clientOpts.AuthInfo = authInfo
49-
provider, err := clientconfig.AuthenticatedClient(ctx, &clientOpts)
50-
if err != nil {
51-
return nil, err
52-
}
119+
return clientconfig.AuthenticatedClient(ctx, &clientOpts)
120+
}
121+
122+
// ServiceClientFromProvider returns a ServiceClient for the given serviceType
123+
// using an already-authenticated ProviderClient.
124+
func ServiceClientFromProvider(provider *gophercloud.ProviderClient, serviceType string) (*gophercloud.ServiceClient, error) {
53125
eo := gophercloud.EndpointOpts{}
54126
eo.ApplyDefaults(serviceType)
55-
56-
// Override endpoint?
57-
var url string
58-
if url, err = provider.EndpointLocator(eo); err != nil {
127+
url, err := provider.EndpointLocator(eo)
128+
if err != nil {
59129
return nil, err
60130
}
61-
62131
return &gophercloud.ServiceClient{
63132
ProviderClient: provider,
64133
Endpoint: url,

0 commit comments

Comments
 (0)