From e23f4c3ef6386bdd33fa639329ef52ec472bb0fc Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 16 Apr 2025 13:00:26 +0100 Subject: [PATCH 1/5] trivial: Simplify endpoint filtering We do not need to check the validity of the provided opts more than once so don't. We can also simplify our handling of multiple endpoints. Signed-off-by: Stephen Finucane --- openstack/endpoint_location.go | 81 +++++++++++++++------------------- 1 file changed, 36 insertions(+), 45 deletions(-) diff --git a/openstack/endpoint_location.go b/openstack/endpoint_location.go index 14cff0d755..324b549a6a 100644 --- a/openstack/endpoint_location.go +++ b/openstack/endpoint_location.go @@ -31,34 +31,29 @@ func V2EndpointURL(catalog *tokens2.ServiceCatalog, opts gophercloud.EndpointOpt } } - // If multiple endpoints were found, use the first result - // and disregard the other endpoints. - // - // This behavior matches the Python library. See GH-1764. - if len(endpoints) > 1 { - endpoints = endpoints[0:1] + // Report an error if there were no matching endpoints. + if len(endpoints) == 0 { + err := &gophercloud.ErrEndpointNotFound{} + return "", err } // Extract the appropriate URL from the matching Endpoint. - for _, endpoint := range endpoints { - switch opts.Availability { - case gophercloud.AvailabilityPublic: - return gophercloud.NormalizeURL(endpoint.PublicURL), nil - case gophercloud.AvailabilityInternal: - return gophercloud.NormalizeURL(endpoint.InternalURL), nil - case gophercloud.AvailabilityAdmin: - return gophercloud.NormalizeURL(endpoint.AdminURL), nil - default: - err := &ErrInvalidAvailabilityProvided{} - err.Argument = "Availability" - err.Value = opts.Availability - return "", err - } + // + // If multiple endpoints were found, use the first result and disregard the other endpoints. + // This behavior matches the Python library. See GH-1764. + switch opts.Availability { + case gophercloud.AvailabilityPublic: + return gophercloud.NormalizeURL(endpoints[0].PublicURL), nil + case gophercloud.AvailabilityInternal: + return gophercloud.NormalizeURL(endpoints[0].InternalURL), nil + case gophercloud.AvailabilityAdmin: + return gophercloud.NormalizeURL(endpoints[0].AdminURL), nil + default: + err := &ErrInvalidAvailabilityProvided{} + err.Argument = "Availability" + err.Value = opts.Availability + return "", err } - - // Report an error if there were no matching endpoints. - err := &gophercloud.ErrEndpointNotFound{} - return "", err } /* @@ -72,20 +67,21 @@ will also often need to specify a Name and/or a Region depending on what's available on your OpenStack deployment. */ func V3EndpointURL(catalog *tokens3.ServiceCatalog, opts gophercloud.EndpointOpts) (string, error) { + if opts.Availability != gophercloud.AvailabilityAdmin && + opts.Availability != gophercloud.AvailabilityPublic && + opts.Availability != gophercloud.AvailabilityInternal { + err := &ErrInvalidAvailabilityProvided{} + err.Argument = "Availability" + err.Value = opts.Availability + return "", err + } + // Extract Endpoints from the catalog entries that match the requested Type, Interface, // Name if provided, and Region if provided. var endpoints = make([]tokens3.Endpoint, 0, 1) for _, entry := range catalog.Entries { if (slices.Contains(opts.Types(), entry.Type)) && (opts.Name == "" || entry.Name == opts.Name) { for _, endpoint := range entry.Endpoints { - if opts.Availability != gophercloud.AvailabilityAdmin && - opts.Availability != gophercloud.AvailabilityPublic && - opts.Availability != gophercloud.AvailabilityInternal { - err := &ErrInvalidAvailabilityProvided{} - err.Argument = "Availability" - err.Value = opts.Availability - return "", err - } if (opts.Availability == gophercloud.Availability(endpoint.Interface)) && (opts.Region == "" || endpoint.Region == opts.Region || endpoint.RegionID == opts.Region) { endpoints = append(endpoints, endpoint) @@ -94,20 +90,15 @@ func V3EndpointURL(catalog *tokens3.ServiceCatalog, opts gophercloud.EndpointOpt } } - // If multiple endpoints were found, use the first result - // and disregard the other endpoints. - // - // This behavior matches the Python library. See GH-1764. - if len(endpoints) > 1 { - endpoints = endpoints[0:1] + // Report an error if there were no matching endpoints. + if len(endpoints) == 0 { + err := &gophercloud.ErrEndpointNotFound{} + return "", err } // Extract the URL from the matching Endpoint. - for _, endpoint := range endpoints { - return gophercloud.NormalizeURL(endpoint.URL), nil - } - - // Report an error if there were no matching endpoints. - err := &gophercloud.ErrEndpointNotFound{} - return "", err + // + // If multiple endpoints were found, use the first result and disregard the other endpoints. + // This behavior matches the Python library. See GH-1764. + return gophercloud.NormalizeURL(endpoints[0].URL), nil } From b9332f6fbf66a374620e923d4d03ca7413d4231b Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 17 Apr 2025 13:14:42 +0100 Subject: [PATCH 2/5] Stop overwriting provided service type As noted inline, this allows us to prefer what the user (or we) requested until such a time as this is no longer necessary. Signed-off-by: Stephen Finucane --- endpoint_search.go | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/endpoint_search.go b/endpoint_search.go index 8818e769b8..e234c549c2 100644 --- a/endpoint_search.go +++ b/endpoint_search.go @@ -118,10 +118,21 @@ func (eo *EndpointOpts) ApplyDefaults(t string) { // TODO(stephenfin): This should probably be an error in v3 for t, aliases := range ServiceTypeAliases { if slices.Contains(aliases, eo.Type) { - // we intentionally override the service type, even if it - // was explicitly requested by the user - eo.Type = t - eo.Aliases = aliases + // we pretend the alias is the official service type and the + // official service type is an alias which allows us to prefer + // what the user requested + // TODO(stephenfin): We should stop doing this once we have + // proper version discovery + // https://github.com/gophercloud/gophercloud/issues/3349 + altAliases := []string{t} + for _, alias := range aliases { + if alias == eo.Type { + continue + } + altAliases = append(altAliases, alias) + } + eo.Aliases = altAliases + continue } } } From edf3f178a04525a5bccdb3c904842c0f6ce8ca16 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 17 Apr 2025 13:25:11 +0100 Subject: [PATCH 3/5] Sort discovered endpoints in order of service types Currently, we iterate through endpoints first, comparing them to the request service type and its aliases. This means that endpoints which use an alias can end up being selected ahead of endpoints that use the requested type. Invert this logic so that we prefer endpoints that match our requested type ahead of those that match an alias of the requested type. Signed-off-by: Stephen Finucane --- openstack/endpoint_location.go | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/openstack/endpoint_location.go b/openstack/endpoint_location.go index 324b549a6a..97c9217b43 100644 --- a/openstack/endpoint_location.go +++ b/openstack/endpoint_location.go @@ -79,12 +79,20 @@ func V3EndpointURL(catalog *tokens3.ServiceCatalog, opts gophercloud.EndpointOpt // Extract Endpoints from the catalog entries that match the requested Type, Interface, // Name if provided, and Region if provided. var endpoints = make([]tokens3.Endpoint, 0, 1) + + entriesByType := map[string][]tokens3.CatalogEntry{} for _, entry := range catalog.Entries { - if (slices.Contains(opts.Types(), entry.Type)) && (opts.Name == "" || entry.Name == opts.Name) { - for _, endpoint := range entry.Endpoints { - if (opts.Availability == gophercloud.Availability(endpoint.Interface)) && - (opts.Region == "" || endpoint.Region == opts.Region || endpoint.RegionID == opts.Region) { - endpoints = append(endpoints, endpoint) + entriesByType[entry.Type] = append(entriesByType[entry.Type], entry) + } + + for _, serviceType := range opts.Types() { + if entries, ok := entriesByType[serviceType]; ok { + for _, entry := range entries { + for _, endpoint := range entry.Endpoints { + if (opts.Availability == gophercloud.Availability(endpoint.Interface)) && + (opts.Region == "" || endpoint.Region == opts.Region || endpoint.RegionID == opts.Region) { + endpoints = append(endpoints, endpoint) + } } } } From e8760563329c87c535997649687a0ca5066a749a Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 17 Apr 2025 13:50:04 +0100 Subject: [PATCH 4/5] Compare versioned service type aliases If we requested volumev2 then we obviously don't want to match on volumev3. Signed-off-by: Stephen Finucane --- openstack/endpoint_location.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/openstack/endpoint_location.go b/openstack/endpoint_location.go index 97c9217b43..652197760d 100644 --- a/openstack/endpoint_location.go +++ b/openstack/endpoint_location.go @@ -1,6 +1,7 @@ package openstack import ( + "regexp" "slices" "github.com/gophercloud/gophercloud/v2" @@ -8,6 +9,17 @@ import ( tokens3 "github.com/gophercloud/gophercloud/v2/openstack/identity/v3/tokens" ) +func extractServiceTypeVersion(serviceType string) string { + versionedServiceTypeAliasRegexp := regexp.MustCompile(`^.*v(\d)$`) + + matches := versionedServiceTypeAliasRegexp.FindAllStringSubmatch(serviceType, 1) + if matches != nil { + // no point converting to an int + return matches[0][1] + } + return "" +} + /* V2EndpointURL discovers the endpoint URL for a specific service from a ServiceCatalog acquired during the v2 identity service. @@ -80,8 +92,14 @@ func V3EndpointURL(catalog *tokens3.ServiceCatalog, opts gophercloud.EndpointOpt // Name if provided, and Region if provided. var endpoints = make([]tokens3.Endpoint, 0, 1) + requestedVersion := extractServiceTypeVersion(opts.Type) entriesByType := map[string][]tokens3.CatalogEntry{} for _, entry := range catalog.Entries { + // If we explicitly requested e.g. volumev3 and the endpoint is using volumev2, ignore + actualVersion := extractServiceTypeVersion(entry.Type) + if requestedVersion != "" && actualVersion != "" && requestedVersion != actualVersion { + continue + } entriesByType[entry.Type] = append(entriesByType[entry.Type], entry) } From f710413a6c7330cbbfbd9e2f5bd09addf9c736e7 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 17 Apr 2025 13:16:12 +0100 Subject: [PATCH 5/5] Prefer versioned service type aliases This is a partial revert of e3947338f69b8b52bd494f09388152febc49c6aa. Prefer e.g. volumev3 or sharev2 to block-storage or shared-file-systems as a temporary measure while we work on versioned API discovery. Couple with prior changes, this means if we e.g. have two catalog entries with services types of 'volumev2' and 'volumev3' (in that order!) and we request a service with 'volumev3', we will return the latter catalog entry ahead of the former. Signed-off-by: Stephen Finucane --- openstack/client.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openstack/client.go b/openstack/client.go index 122a3ee699..6b06b70d40 100644 --- a/openstack/client.go +++ b/openstack/client.go @@ -404,17 +404,17 @@ func NewBlockStorageV1(client *gophercloud.ProviderClient, eo gophercloud.Endpoi // NewBlockStorageV2 creates a ServiceClient that may be used to access the v2 // block storage service. func NewBlockStorageV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { - return initClientOpts(client, eo, "block-storage") + return initClientOpts(client, eo, "volumev2") } // NewBlockStorageV3 creates a ServiceClient that may be used to access the v3 block storage service. func NewBlockStorageV3(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { - return initClientOpts(client, eo, "block-storage") + return initClientOpts(client, eo, "volumev3") } // NewSharedFileSystemV2 creates a ServiceClient that may be used to access the v2 shared file system service. func NewSharedFileSystemV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { - return initClientOpts(client, eo, "shared-file-system") + return initClientOpts(client, eo, "sharev2") } // NewOrchestrationV1 creates a ServiceClient that may be used to access the v1 @@ -485,7 +485,7 @@ func NewContainerInfraV1(client *gophercloud.ProviderClient, eo gophercloud.Endp // NewWorkflowV2 creates a ServiceClient that may be used with the v2 workflow management package. func NewWorkflowV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { - return initClientOpts(client, eo, "workflow") + return initClientOpts(client, eo, "workflowv2") } // NewPlacementV1 creates a ServiceClient that may be used with the placement package.