@@ -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.
3162func 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