Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ require (
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect
golang.org/x/net v0.56.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sync v0.21.0 // indirect
golang.org/x/sync v0.21.0
golang.org/x/sys v0.46.0 // indirect
golang.org/x/term v0.44.0 // indirect
golang.org/x/text v0.38.0 // indirect
Expand Down
89 changes: 79 additions & 10 deletions internal/openstack/service_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,83 @@ import (
"os"
"os/exec"
"strings"
"sync"

"github.com/gophercloud/gophercloud/v2"
"github.com/gophercloud/utils/v2/openstack/clientconfig"
"golang.org/x/sync/singleflight"
)

// GetServiceClient returns an gophercloud ServiceClient for the given serviceType.
// providerCache caches ProviderClients keyed by a string derived from the
// authInfo, so multiple GetServiceClient calls with the same credentials
// only authenticate against Keystone once.
var (
providerCacheMu sync.Mutex
providerCache = map[string]*gophercloud.ProviderClient{}
providerGroup singleflight.Group
)

func cacheKey(authInfo *clientconfig.AuthInfo) string {
if authInfo == nil {
return ""
}
// Include all fields that affect the resulting Keystone token and catalog
// so a future auth context differing only in user, domain, or AuthURL
// doesn't silently reuse the wrong cached provider.
return strings.Join([]string{
authInfo.AuthURL,
authInfo.ProjectName,
authInfo.ProjectDomainName,
authInfo.Username,
authInfo.UserDomainName,
}, "\x00")
}

// GetServiceClient returns a ServiceClient for the given serviceType.
// Providers are cached per auth context so Keystone is only hit once per
// unique set of credentials across all SetupWithManager calls. Concurrent
// callers with the same key share a single in-flight request via singleflight,
// preventing duplicate Keystone round-trips on startup.
func GetServiceClient(ctx context.Context, serviceType string, authInfo *clientconfig.AuthInfo) (*gophercloud.ServiceClient, error) {
key := cacheKey(authInfo)

providerCacheMu.Lock()
provider, ok := providerCache[key]
providerCacheMu.Unlock()

if !ok {
v, err, _ := providerGroup.Do(key, func() (any, error) {
// Re-check under the group: another goroutine may have populated
// the cache while we were waiting for the singleflight slot.
providerCacheMu.Lock()
if p, hit := providerCache[key]; hit {
providerCacheMu.Unlock()
return p, nil
}
providerCacheMu.Unlock()

p, err := NewProviderClient(ctx, authInfo)
if err != nil {
return nil, err
}
providerCacheMu.Lock()
providerCache[key] = p
providerCacheMu.Unlock()
return p, nil
})
if err != nil {
return nil, err
}
provider = v.(*gophercloud.ProviderClient)
}

return ServiceClientFromProvider(provider, serviceType)
}

// NewProviderClient authenticates against OpenStack and returns a ProviderClient
// that can be reused across multiple service clients via ServiceClientFromProvider,
// avoiding repeated Keystone round-trips and catalog deserialisations.
func NewProviderClient(ctx context.Context, authInfo *clientconfig.AuthInfo) (*gophercloud.ProviderClient, error) {
if authInfo == nil {
authInfo = &clientconfig.AuthInfo{}
}
Expand All @@ -46,19 +116,18 @@ func GetServiceClient(ctx context.Context, serviceType string, authInfo *clientc

var clientOpts clientconfig.ClientOpts
clientOpts.AuthInfo = authInfo
provider, err := clientconfig.AuthenticatedClient(ctx, &clientOpts)
if err != nil {
return nil, err
}
return clientconfig.AuthenticatedClient(ctx, &clientOpts)
}

// ServiceClientFromProvider returns a ServiceClient for the given serviceType
// using an already-authenticated ProviderClient.
func ServiceClientFromProvider(provider *gophercloud.ProviderClient, serviceType string) (*gophercloud.ServiceClient, error) {
eo := gophercloud.EndpointOpts{}
eo.ApplyDefaults(serviceType)

// Override endpoint?
var url string
if url, err = provider.EndpointLocator(eo); err != nil {
url, err := provider.EndpointLocator(eo)
if err != nil {
return nil, err
}

return &gophercloud.ServiceClient{
ProviderClient: provider,
Endpoint: url,
Expand Down
Loading