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 } } } 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. diff --git a/openstack/endpoint_location.go b/openstack/endpoint_location.go index 14cff0d755..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. @@ -31,34 +43,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,42 +79,52 @@ 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) + + requestedVersion := extractServiceTypeVersion(opts.Type) + 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.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) + // 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) + } + + 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) + } } } } } - // 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 }