Repository: gophercloud/gophercloud

Issue:
core: endpoint locator doesn't use active context to resolve microversions
The following func uses the context, provided during the initialization, which causes `context canceled` when the original context is done.

https://github.com/gophercloud/gophercloud/blob/c3046e033334224f7732c220b552ecb56f040f86/openstack/client.go#L286-L288

Caught by https://github.com/terraform-provider-openstack/terraform-provider-openstack/pull/1899

proper func signature should be:

```go
        client.EndpointLocator = func(ctx context.Context, opts gophercloud.EndpointOpts) (string, error) {
                return V3Endpoint(ctx, client, catalog, opts)
        }
```

cc @stephenfin 

Discussion:

Solution patch:
diff --git a/README.md b/README.md
index 69a53bf59..594e9af3f 100644
--- a/README.md
+++ b/README.md
@@ -96,9 +96,9 @@ func main() {
 
 	// Use the ProviderClient and the endpoint options fetched from
 	// `clouds.yaml` to build a service client: a compute client in this
-	// case. Note that the contructor does not accept a `context.Context`:
-	// no further call to the OpenStack API is needed at this stage.
-	computeClient, err := openstack.NewComputeV2(providerClient, endpointOptions)
+	// case. Note that the contructor does accept a `context.Context`
+	// to resolve supported API microversions.
+	computeClient, err := openstack.NewComputeV2(ctx, providerClient, endpointOptions)
 	if err != nil {
 		panic(err)
 	}
@@ -135,7 +135,7 @@ func main() {
 		panic(err)
 	}
 
-	computeClient, err := openstack.NewComputeV2(providerClient, gophercloud.EndpointOpts{
+	computeClient, err := openstack.NewComputeV2(ctx, providerClient, gophercloud.EndpointOpts{
 		Region: os.Getenv("OS_REGION_NAME"),
 	})
 	if err != nil {
@@ -173,7 +173,7 @@ func main() {
 		panic(err)
 	}
 
-	computeClient, err := openstack.NewComputeV2(providerClient, gophercloud.EndpointOpts{
+	computeClient, err := openstack.NewComputeV2(ctx, providerClient, gophercloud.EndpointOpts{
 		Region: "RegionName",
 	})
 	if err != nil {
diff --git a/docs/MICROVERSIONS.md b/docs/MICROVERSIONS.md
index 2054c9a6e..6aa88e8a7 100644
--- a/docs/MICROVERSIONS.md
+++ b/docs/MICROVERSIONS.md
@@ -26,7 +26,7 @@ More information can be found here:
 You can set a specific microversion on a Service Client by doing the following:
 
 ```go
-client, err := openstack.NewComputeV2(providerClient, nil)
+client, err := openstack.NewComputeV2(context.TODO(), providerClient, nil)
 client.Microversion = "2.52"
 ```
 
diff --git a/endpoint_search.go b/endpoint_search.go
index 737a8f761..e594dcb28 100644
--- a/endpoint_search.go
+++ b/endpoint_search.go
@@ -1,6 +1,9 @@
 package gophercloud
 
-import "slices"
+import (
+	"context"
+	"slices"
+)
 
 // Availability indicates to whom a specific service endpoint is accessible:
 // the internet at large, internal networks only, or only to administrators.
@@ -100,7 +103,7 @@ It provides an implementation that locates a single endpoint from a service
 catalog for a specific ProviderClient based on user-provided EndpointOpts. The
 provider then uses it to discover related ServiceClients.
 */
-type EndpointLocator func(EndpointOpts) (string, error)
+type EndpointLocator func(context.Context, EndpointOpts) (string, error)
 
 // ApplyDefaults is an internal method to be used by provider implementations.
 //
diff --git a/internal/acceptance/clients/clients.go b/internal/acceptance/clients/clients.go
index 6c3639148..8f9b8de35 100644
--- a/internal/acceptance/clients/clients.go
+++ b/internal/acceptance/clients/clients.go
@@ -147,7 +147,7 @@ func NewBlockStorageV1Client() (*gophercloud.ServiceClient, error) {
 
 	client = configureDebug(client)
 
-	return openstack.NewBlockStorageV1(client, gophercloud.EndpointOpts{
+	return openstack.NewBlockStorageV1(context.TODO(), client, gophercloud.EndpointOpts{
 		Region: os.Getenv("OS_REGION_NAME"),
 	})
 }
@@ -168,7 +168,7 @@ func NewBlockStorageV2Client() (*gophercloud.ServiceClient, error) {
 
 	client = configureDebug(client)
 
-	return openstack.NewBlockStorageV2(client, gophercloud.EndpointOpts{
+	return openstack.NewBlockStorageV2(context.TODO(), client, gophercloud.EndpointOpts{
 		Region: os.Getenv("OS_REGION_NAME"),
 	})
 }
@@ -189,7 +189,7 @@ func NewBlockStorageV3Client() (*gophercloud.ServiceClient, error) {
 
 	client = configureDebug(client)
 
-	return openstack.NewBlockStorageV3(client, gophercloud.EndpointOpts{
+	return openstack.NewBlockStorageV3(context.TODO(), client, gophercloud.EndpointOpts{
 		Region: os.Getenv("OS_REGION_NAME"),
 	})
 }
@@ -248,7 +248,7 @@ func NewComputeV2Client() (*gophercloud.ServiceClient, error) {
 
 	client = configureDebug(client)
 
-	return openstack.NewComputeV2(client, gophercloud.EndpointOpts{
+	return openstack.NewComputeV2(context.TODO(), client, gophercloud.EndpointOpts{
 		Region: os.Getenv("OS_REGION_NAME"),
 	})
 }
@@ -269,7 +269,7 @@ func NewBareMetalV1Client() (*gophercloud.ServiceClient, error) {
 
 	client = configureDebug(client)
 
-	return openstack.NewBareMetalV1(client, gophercloud.EndpointOpts{
+	return openstack.NewBareMetalV1(context.TODO(), client, gophercloud.EndpointOpts{
 		Region: os.Getenv("OS_REGION_NAME"),
 	})
 }
@@ -310,7 +310,7 @@ func NewBareMetalIntrospectionV1Client() (*gophercloud.ServiceClient, error) {
 
 	client = configureDebug(client)
 
-	return openstack.NewBareMetalIntrospectionV1(client, gophercloud.EndpointOpts{
+	return openstack.NewBareMetalIntrospectionV1(context.TODO(), client, gophercloud.EndpointOpts{
 		Region: os.Getenv("OS_REGION_NAME"),
 	})
 }
@@ -331,7 +331,7 @@ func NewDBV1Client() (*gophercloud.ServiceClient, error) {
 
 	client = configureDebug(client)
 
-	return openstack.NewDBV1(client, gophercloud.EndpointOpts{
+	return openstack.NewDBV1(context.TODO(), client, gophercloud.EndpointOpts{
 		Region: os.Getenv("OS_REGION_NAME"),
 	})
 }
@@ -352,7 +352,7 @@ func NewDNSV2Client() (*gophercloud.ServiceClient, error) {
 
 	client = configureDebug(client)
 
-	return openstack.NewDNSV2(client, gophercloud.EndpointOpts{
+	return openstack.NewDNSV2(context.TODO(), client, gophercloud.EndpointOpts{
 		Region: os.Getenv("OS_REGION_NAME"),
 	})
 }
@@ -373,7 +373,7 @@ func NewIdentityV2Client() (*gophercloud.ServiceClient, error) {
 
 	client = configureDebug(client)
 
-	return openstack.NewIdentityV2(client, gophercloud.EndpointOpts{
+	return openstack.NewIdentityV2(context.TODO(), client, gophercloud.EndpointOpts{
 		Region: os.Getenv("OS_REGION_NAME"),
 	})
 }
@@ -394,7 +394,7 @@ func NewIdentityV2AdminClient() (*gophercloud.ServiceClient, error) {
 
 	client = configureDebug(client)
 
-	return openstack.NewIdentityV2(client, gophercloud.EndpointOpts{
+	return openstack.NewIdentityV2(context.TODO(), client, gophercloud.EndpointOpts{
 		Region:       os.Getenv("OS_REGION_NAME"),
 		Availability: gophercloud.AvailabilityAdmin,
 	})
@@ -416,7 +416,7 @@ func NewIdentityV2UnauthenticatedClient() (*gophercloud.ServiceClient, error) {
 
 	client = configureDebug(client)
 
-	return openstack.NewIdentityV2(client, gophercloud.EndpointOpts{})
+	return openstack.NewIdentityV2(context.TODO(), client, gophercloud.EndpointOpts{})
 }
 
 // NewIdentityV3Client returns a *ServiceClient for making calls
@@ -435,7 +435,7 @@ func NewIdentityV3Client() (*gophercloud.ServiceClient, error) {
 
 	client = configureDebug(client)
 
-	return openstack.NewIdentityV3(client, gophercloud.EndpointOpts{
+	return openstack.NewIdentityV3(context.TODO(), client, gophercloud.EndpointOpts{
 		Region: os.Getenv("OS_REGION_NAME"),
 	})
 }
@@ -456,7 +456,7 @@ func NewIdentityV3UnauthenticatedClient() (*gophercloud.ServiceClient, error) {
 
 	client = configureDebug(client)
 
-	return openstack.NewIdentityV3(client, gophercloud.EndpointOpts{})
+	return openstack.NewIdentityV3(context.TODO(), client, gophercloud.EndpointOpts{})
 }
 
 // NewImageV2Client returns a *ServiceClient for making calls to the
@@ -475,7 +475,7 @@ func NewImageV2Client() (*gophercloud.ServiceClient, error) {
 
 	client = configureDebug(client)
 
-	return openstack.NewImageV2(client, gophercloud.EndpointOpts{
+	return openstack.NewImageV2(context.TODO(), client, gophercloud.EndpointOpts{
 		Region: os.Getenv("OS_REGION_NAME"),
 	})
 }
@@ -496,7 +496,7 @@ func NewNetworkV2Client() (*gophercloud.ServiceClient, error) {
 
 	client = configureDebug(client)
 
-	return openstack.NewNetworkV2(client, gophercloud.EndpointOpts{
+	return openstack.NewNetworkV2(context.TODO(), client, gophercloud.EndpointOpts{
 		Region: os.Getenv("OS_REGION_NAME"),
 	})
 }
@@ -517,7 +517,7 @@ func NewObjectStorageV1Client() (*gophercloud.ServiceClient, error) {
 
 	client = configureDebug(client)
 
-	return openstack.NewObjectStorageV1(client, gophercloud.EndpointOpts{
+	return openstack.NewObjectStorageV1(context.TODO(), client, gophercloud.EndpointOpts{
 		Region: os.Getenv("OS_REGION_NAME"),
 	})
 }
@@ -538,7 +538,7 @@ func NewSharedFileSystemV2Client() (*gophercloud.ServiceClient, error) {
 
 	client = configureDebug(client)
 
-	return openstack.NewSharedFileSystemV2(client, gophercloud.EndpointOpts{
+	return openstack.NewSharedFileSystemV2(context.TODO(), client, gophercloud.EndpointOpts{
 		Region: os.Getenv("OS_REGION_NAME"),
 	})
 }
@@ -559,7 +559,7 @@ func NewLoadBalancerV2Client() (*gophercloud.ServiceClient, error) {
 
 	client = configureDebug(client)
 
-	return openstack.NewLoadBalancerV2(client, gophercloud.EndpointOpts{
+	return openstack.NewLoadBalancerV2(context.TODO(), client, gophercloud.EndpointOpts{
 		Region: os.Getenv("OS_REGION_NAME"),
 	})
 }
@@ -580,7 +580,7 @@ func NewMessagingV2Client(clientID string) (*gophercloud.ServiceClient, error) {
 
 	client = configureDebug(client)
 
-	return openstack.NewMessagingV2(client, clientID, gophercloud.EndpointOpts{
+	return openstack.NewMessagingV2(context.TODO(), client, clientID, gophercloud.EndpointOpts{
 		Region: os.Getenv("OS_REGION_NAME"),
 	})
 }
@@ -601,7 +601,7 @@ func NewContainerV1Client() (*gophercloud.ServiceClient, error) {
 
 	client = configureDebug(client)
 
-	return openstack.NewContainerV1(client, gophercloud.EndpointOpts{
+	return openstack.NewContainerV1(context.TODO(), client, gophercloud.EndpointOpts{
 		Region: os.Getenv("OS_REGION_NAME"),
 	})
 }
@@ -622,7 +622,7 @@ func NewKeyManagerV1Client() (*gophercloud.ServiceClient, error) {
 
 	client = configureDebug(client)
 
-	return openstack.NewKeyManagerV1(client, gophercloud.EndpointOpts{
+	return openstack.NewKeyManagerV1(context.TODO(), client, gophercloud.EndpointOpts{
 		Region: os.Getenv("OS_REGION_NAME"),
 	})
 }
@@ -657,7 +657,7 @@ func NewContainerInfraV1Client() (*gophercloud.ServiceClient, error) {
 
 	client = configureDebug(client)
 
-	return openstack.NewContainerInfraV1(client, gophercloud.EndpointOpts{
+	return openstack.NewContainerInfraV1(context.TODO(), client, gophercloud.EndpointOpts{
 		Region: os.Getenv("OS_REGION_NAME"),
 	})
 }
@@ -678,7 +678,7 @@ func NewWorkflowV2Client() (*gophercloud.ServiceClient, error) {
 
 	client = configureDebug(client)
 
-	return openstack.NewWorkflowV2(client, gophercloud.EndpointOpts{
+	return openstack.NewWorkflowV2(context.TODO(), client, gophercloud.EndpointOpts{
 		Region: os.Getenv("OS_REGION_NAME"),
 	})
 }
@@ -699,7 +699,7 @@ func NewOrchestrationV1Client() (*gophercloud.ServiceClient, error) {
 
 	client = configureDebug(client)
 
-	return openstack.NewOrchestrationV1(client, gophercloud.EndpointOpts{
+	return openstack.NewOrchestrationV1(context.TODO(), client, gophercloud.EndpointOpts{
 		Region: os.Getenv("OS_REGION_NAME"),
 	})
 }
@@ -720,7 +720,7 @@ func NewPlacementV1Client() (*gophercloud.ServiceClient, error) {
 
 	client = configureDebug(client)
 
-	return openstack.NewPlacementV1(client, gophercloud.EndpointOpts{
+	return openstack.NewPlacementV1(context.TODO(), client, gophercloud.EndpointOpts{
 		Region: os.Getenv("OS_REGION_NAME"),
 	})
 }
diff --git a/openstack/client.go b/openstack/client.go
index ff2bfd57e..2c81b27f3 100644
--- a/openstack/client.go
+++ b/openstack/client.go
@@ -36,7 +36,7 @@ const (
 //
 //	ao, err := openstack.AuthOptionsFromEnv()
 //	provider, err := openstack.NewClient(ao.IdentityEndpoint)
-//	client, err := openstack.NewIdentityV3(provider, gophercloud.EndpointOpts{})
+//	client, err := openstack.NewIdentityV3(ctx, provider, gophercloud.EndpointOpts{})
 func NewClient(endpoint string) (*gophercloud.ProviderClient, error) {
 	base, err := utils.BaseEndpoint(endpoint)
 	if err != nil {
@@ -69,7 +69,7 @@ func NewClient(endpoint string) (*gophercloud.ProviderClient, error) {
 //
 //	ao, err := openstack.AuthOptionsFromEnv()
 //	provider, err := openstack.AuthenticatedClient(ctx, ao)
-//	client, err := openstack.NewNetworkV2(provider, gophercloud.EndpointOpts{
+//	client, err := openstack.NewNetworkV2(ctx, provider, gophercloud.EndpointOpts{
 //		Region: os.Getenv("OS_REGION_NAME"),
 //	})
 func AuthenticatedClient(ctx context.Context, options gophercloud.AuthOptions) (*gophercloud.ProviderClient, error) {
@@ -121,7 +121,7 @@ type v2TokenNoReauth struct {
 func (v2TokenNoReauth) CanReauth() bool { return false }
 
 func v2auth(ctx context.Context, client *gophercloud.ProviderClient, endpoint string, options tokens2.AuthOptionsBuilder, eo gophercloud.EndpointOpts) error {
-	v2Client, err := NewIdentityV2(client, eo)
+	v2Client, err := NewIdentityV2(ctx, client, eo)
 	if err != nil {
 		return err
 	}
@@ -162,7 +162,7 @@ func v2auth(ctx context.Context, client *gophercloud.ProviderClient, endpoint st
 			return nil
 		}
 	}
-	client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) {
+	client.EndpointLocator = func(ctx context.Context, opts gophercloud.EndpointOpts) (string, error) {
 		return V2Endpoint(ctx, client, catalog, opts)
 	}
 
@@ -176,7 +176,7 @@ func AuthenticateV3(ctx context.Context, client *gophercloud.ProviderClient, opt
 
 func v3auth(ctx context.Context, client *gophercloud.ProviderClient, endpoint string, opts tokens3.AuthOptionsBuilder, eo gophercloud.EndpointOpts) error {
 	// Override the generated service endpoint with the one returned by the version endpoint.
-	v3Client, err := NewIdentityV3(client, eo)
+	v3Client, err := NewIdentityV3(ctx, client, eo)
 	if err != nil {
 		return err
 	}
@@ -283,7 +283,7 @@ func v3auth(ctx context.Context, client *gophercloud.ProviderClient, endpoint st
 			return nil
 		}
 	}
-	client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) {
+	client.EndpointLocator = func(ctx context.Context, opts gophercloud.EndpointOpts) (string, error) {
 		return V3Endpoint(ctx, client, catalog, opts)
 	}
 
@@ -292,13 +292,13 @@ func v3auth(ctx context.Context, client *gophercloud.ProviderClient, endpoint st
 
 // NewIdentityV2 creates a ServiceClient that may be used to interact with the
 // v2 identity service.
-func NewIdentityV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+func NewIdentityV2(ctx context.Context, client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
 	endpoint := client.IdentityBase + "v2.0/"
 	clientType := "identity"
 	var err error
 	if !reflect.DeepEqual(eo, gophercloud.EndpointOpts{}) {
 		eo.ApplyDefaults(clientType)
-		endpoint, err = client.EndpointLocator(eo)
+		endpoint, err = client.EndpointLocator(ctx, eo)
 		if err != nil {
 			return nil, err
 		}
@@ -313,13 +313,13 @@ func NewIdentityV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOp
 
 // NewIdentityV3 creates a ServiceClient that may be used to access the v3
 // identity service.
-func NewIdentityV3(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+func NewIdentityV3(ctx context.Context, client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
 	endpoint := client.IdentityBase + "v3/"
 	clientType := "identity"
 	var err error
 	if !reflect.DeepEqual(eo, gophercloud.EndpointOpts{}) {
 		eo.ApplyDefaults(clientType)
-		endpoint, err = client.EndpointLocator(eo)
+		endpoint, err = client.EndpointLocator(ctx, eo)
 		if err != nil {
 			return nil, err
 		}
@@ -346,7 +346,7 @@ func NewIdentityV3(client *gophercloud.ProviderClient, eo gophercloud.EndpointOp
 }
 
 // TODO(stephenfin): Allow passing aliases to all New${SERVICE}V${VERSION} methods in v3
-func initClientOpts(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts, clientType string, version int) (*gophercloud.ServiceClient, error) {
+func initClientOpts(ctx context.Context, client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts, clientType string, version int) (*gophercloud.ServiceClient, error) {
 	sc := new(gophercloud.ServiceClient)
 
 	eo.ApplyDefaults(clientType)
@@ -355,7 +355,7 @@ func initClientOpts(client *gophercloud.ProviderClient, eo gophercloud.EndpointO
 	}
 	eo.Version = version
 
-	url, err := client.EndpointLocator(eo)
+	url, err := client.EndpointLocator(ctx, eo)
 	if err != nil {
 		return sc, err
 	}
@@ -368,8 +368,8 @@ func initClientOpts(client *gophercloud.ProviderClient, eo gophercloud.EndpointO
 
 // NewBareMetalV1 creates a ServiceClient that may be used with the v1
 // bare metal package.
-func NewBareMetalV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
-	sc, err := initClientOpts(client, eo, "baremetal", 1)
+func NewBareMetalV1(ctx context.Context, client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+	sc, err := initClientOpts(ctx, client, eo, "baremetal", 1)
 	if !strings.HasSuffix(strings.TrimSuffix(sc.Endpoint, "/"), "v1") {
 		sc.ResourceBase = sc.Endpoint + "v1/"
 	}
@@ -378,26 +378,26 @@ func NewBareMetalV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointO
 
 // NewBareMetalIntrospectionV1 creates a ServiceClient that may be used with the v1
 // bare metal introspection package.
-func NewBareMetalIntrospectionV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
-	return initClientOpts(client, eo, "baremetal-introspection", 1)
+func NewBareMetalIntrospectionV1(ctx context.Context, client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+	return initClientOpts(ctx, client, eo, "baremetal-introspection", 1)
 }
 
 // NewObjectStorageV1 creates a ServiceClient that may be used with the v1
 // object storage package.
-func NewObjectStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
-	return initClientOpts(client, eo, "object-store", 1)
+func NewObjectStorageV1(ctx context.Context, client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+	return initClientOpts(ctx, client, eo, "object-store", 1)
 }
 
 // NewComputeV2 creates a ServiceClient that may be used with the v2 compute
 // package.
-func NewComputeV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
-	return initClientOpts(client, eo, "compute", 2)
+func NewComputeV2(ctx context.Context, client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+	return initClientOpts(ctx, client, eo, "compute", 2)
 }
 
 // NewNetworkV2 creates a ServiceClient that may be used with the v2 network
 // package.
-func NewNetworkV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
-	sc, err := initClientOpts(client, eo, "network", 2)
+func NewNetworkV2(ctx context.Context, client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+	sc, err := initClientOpts(ctx, client, eo, "network", 2)
 	sc.ResourceBase = sc.Endpoint + "v2.0/"
 	return sc, err
 }
@@ -405,57 +405,57 @@ func NewNetworkV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpt
 // TODO(stephenfin): Remove this in v3. We no longer support the V1 Block Storage service.
 // NewBlockStorageV1 creates a ServiceClient that may be used to access the v1
 // block storage service.
-func NewBlockStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
-	return initClientOpts(client, eo, "volume", 1)
+func NewBlockStorageV1(ctx context.Context, client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+	return initClientOpts(ctx, client, eo, "volume", 1)
 }
 
 // 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", 2)
+func NewBlockStorageV2(ctx context.Context, client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+	return initClientOpts(ctx, client, eo, "block-storage", 2)
 }
 
 // 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", 3)
+func NewBlockStorageV3(ctx context.Context, client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+	return initClientOpts(ctx, client, eo, "block-storage", 3)
 }
 
 // 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", 2)
+func NewSharedFileSystemV2(ctx context.Context, client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+	return initClientOpts(ctx, client, eo, "shared-file-system", 2)
 }
 
 // NewOrchestrationV1 creates a ServiceClient that may be used to access the v1
 // orchestration service.
-func NewOrchestrationV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
-	return initClientOpts(client, eo, "orchestration", 1)
+func NewOrchestrationV1(ctx context.Context, client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+	return initClientOpts(ctx, client, eo, "orchestration", 1)
 }
 
 // NewDBV1 creates a ServiceClient that may be used to access the v1 DB service.
-func NewDBV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
-	return initClientOpts(client, eo, "database", 1)
+func NewDBV1(ctx context.Context, client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+	return initClientOpts(ctx, client, eo, "database", 1)
 }
 
 // NewDNSV2 creates a ServiceClient that may be used to access the v2 DNS
 // service.
-func NewDNSV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
-	sc, err := initClientOpts(client, eo, "dns", 2)
+func NewDNSV2(ctx context.Context, client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+	sc, err := initClientOpts(ctx, client, eo, "dns", 2)
 	sc.ResourceBase = sc.Endpoint + "v2/"
 	return sc, err
 }
 
 // NewImageV2 creates a ServiceClient that may be used to access the v2 image
 // service.
-func NewImageV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
-	sc, err := initClientOpts(client, eo, "image", 2)
+func NewImageV2(ctx context.Context, client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+	sc, err := initClientOpts(ctx, client, eo, "image", 2)
 	sc.ResourceBase = sc.Endpoint + "v2/"
 	return sc, err
 }
 
 // NewLoadBalancerV2 creates a ServiceClient that may be used to access the v2
 // load balancer service.
-func NewLoadBalancerV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
-	sc, err := initClientOpts(client, eo, "load-balancer", 2)
+func NewLoadBalancerV2(ctx context.Context, client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+	sc, err := initClientOpts(ctx, client, eo, "load-balancer", 2)
 
 	// Fixes edge case having an OpenStack lb endpoint with trailing version number.
 	endpoint := strings.ReplaceAll(sc.Endpoint, "v2.0/", "")
@@ -466,37 +466,37 @@ func NewLoadBalancerV2(client *gophercloud.ProviderClient, eo gophercloud.Endpoi
 
 // NewMessagingV2 creates a ServiceClient that may be used with the v2 messaging
 // service.
-func NewMessagingV2(client *gophercloud.ProviderClient, clientID string, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
-	sc, err := initClientOpts(client, eo, "message", 2)
+func NewMessagingV2(ctx context.Context, client *gophercloud.ProviderClient, clientID string, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+	sc, err := initClientOpts(ctx, client, eo, "message", 2)
 	sc.MoreHeaders = map[string]string{"Client-ID": clientID}
 	return sc, err
 }
 
 // NewContainerV1 creates a ServiceClient that may be used with v1 container package
-func NewContainerV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
-	return initClientOpts(client, eo, "application-container", 1)
+func NewContainerV1(ctx context.Context, client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+	return initClientOpts(ctx, client, eo, "application-container", 1)
 }
 
 // NewKeyManagerV1 creates a ServiceClient that may be used with the v1 key
 // manager service.
-func NewKeyManagerV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
-	sc, err := initClientOpts(client, eo, "key-manager", 1)
+func NewKeyManagerV1(ctx context.Context, client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+	sc, err := initClientOpts(ctx, client, eo, "key-manager", 1)
 	sc.ResourceBase = sc.Endpoint + "v1/"
 	return sc, err
 }
 
 // NewContainerInfraV1 creates a ServiceClient that may be used with the v1 container infra management
 // package.
-func NewContainerInfraV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
-	return initClientOpts(client, eo, "container-infrastructure-management", 1)
+func NewContainerInfraV1(ctx context.Context, client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+	return initClientOpts(ctx, client, eo, "container-infrastructure-management", 1)
 }
 
 // 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", 2)
+func NewWorkflowV2(ctx context.Context, client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+	return initClientOpts(ctx, client, eo, "workflow", 2)
 }
 
 // NewPlacementV1 creates a ServiceClient that may be used with the placement package.
-func NewPlacementV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
-	return initClientOpts(client, eo, "placement", 1)
+func NewPlacementV1(ctx context.Context, client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+	return initClientOpts(ctx, client, eo, "placement", 1)
 }
diff --git a/openstack/common/extensions/doc.go b/openstack/common/extensions/doc.go
index e07dd0f8f..ca402ee47 100644
--- a/openstack/common/extensions/doc.go
+++ b/openstack/common/extensions/doc.go
@@ -22,7 +22,7 @@ Example of Retrieving Compute Extensions
 
 	ao, err := openstack.AuthOptionsFromEnv()
 	provider, err := openstack.AuthenticatedClient(context.TODO(), ao)
-	computeClient, err := openstack.NewComputeV2(provider, gophercloud.EndpointOpts{
+	computeClient, err := openstack.NewComputeV2(context.TODO(), provider, gophercloud.EndpointOpts{
 		Region: os.Getenv("OS_REGION_NAME"),
 	})
 
@@ -37,7 +37,7 @@ Example of Retrieving Network Extensions
 
 	ao, err := openstack.AuthOptionsFromEnv()
 	provider, err := openstack.AuthenticatedClient(context.TODO(), ao)
-	networkClient, err := openstack.NewNetworkV2(provider, gophercloud.EndpointOpts{
+	networkClient, err := openstack.NewNetworkV2(context.TODO(), provider, gophercloud.EndpointOpts{
 		Region: os.Getenv("OS_REGION_NAME"),
 	})
 
diff --git a/openstack/compute/v2/remoteconsoles/doc.go b/openstack/compute/v2/remoteconsoles/doc.go
index 65dc9b91d..d30787bb5 100644
--- a/openstack/compute/v2/remoteconsoles/doc.go
+++ b/openstack/compute/v2/remoteconsoles/doc.go
@@ -6,7 +6,7 @@ that API.
 
 Example of Creating a new RemoteConsole
 
-	computeClient, err := openstack.NewComputeV2(providerClient, endpointOptions)
+	computeClient, err := openstack.NewComputeV2(context.TODO(), providerClient, endpointOptions)
 	computeClient.Microversion = "2.6"
 
 	createOpts := remoteconsoles.CreateOpts{
diff --git a/openstack/compute/v2/servers/requests.go b/openstack/compute/v2/servers/requests.go
index b74b98768..6754d0a93 100644
--- a/openstack/compute/v2/servers/requests.go
+++ b/openstack/compute/v2/servers/requests.go
@@ -654,6 +654,12 @@ type UpdateOpts struct {
 
 	// AccessIPv6 provides a new IPv6 address for the instance.
 	AccessIPv6 *string `json:"accessIPv6,omitempty"`
+
+	// Hostname changes the hostname of the server.
+	// Requires microversion 2.90 or later.
+	// Note: This information is published via the metadata service and requires
+	// application such as cloud-init to propagate it through to the instance.
+	Hostname *string `json:"hostname,omitempty"`
 }
 
 // ToServerUpdateMap formats an UpdateOpts structure into a request body.
diff --git a/openstack/config/clouds/clouds.go b/openstack/config/clouds/clouds.go
index 146605809..eae6f38c8 100644
--- a/openstack/config/clouds/clouds.go
+++ b/openstack/config/clouds/clouds.go
@@ -13,7 +13,7 @@
 //		panic(err)
 //	}
 //
-//	networkClient, err := openstack.NewNetworkV2(providerClient, eo)
+//	networkClient, err := openstack.NewNetworkV2(ctx, providerClient, eo)
 //	if err != nil {
 //		panic(err)
 //	}
diff --git a/openstack/containerinfra/v1/nodegroups/doc.go b/openstack/containerinfra/v1/nodegroups/doc.go
index fb8073d67..824e7f30a 100644
--- a/openstack/containerinfra/v1/nodegroups/doc.go
+++ b/openstack/containerinfra/v1/nodegroups/doc.go
@@ -16,7 +16,7 @@ Create a client to use:
 	    panic(err)
 	}
 
-	client, err := openstack.NewContainerInfraV1(provider, gophercloud.EndpointOpts{Region: os.Getenv("OS_REGION_NAME")})
+	client, err := openstack.NewContainerInfraV1(context.TODO(), provider, gophercloud.EndpointOpts{Region: os.Getenv("OS_REGION_NAME")})
 	if err != nil {
 	    panic(err)
 	}
diff --git a/openstack/doc.go b/openstack/doc.go
index 4d89fb5d0..538b93f76 100644
--- a/openstack/doc.go
+++ b/openstack/doc.go
@@ -7,7 +7,7 @@ Example of Creating a Service Client
 
 	ao, err := openstack.AuthOptionsFromEnv()
 	provider, err := openstack.AuthenticatedClient(context.TODO(), ao)
-	client, err := openstack.NewNetworkV2(provider, gophercloud.EndpointOpts{
+	client, err := openstack.NewNetworkV2(context.TODO(), provider, gophercloud.EndpointOpts{
 		Region: os.Getenv("OS_REGION_NAME"),
 	})
 */
diff --git a/openstack/orchestration/v1/stacks/doc.go b/openstack/orchestration/v1/stacks/doc.go
index 772c0f445..ddf54d20a 100644
--- a/openstack/orchestration/v1/stacks/doc.go
+++ b/openstack/orchestration/v1/stacks/doc.go
@@ -20,7 +20,7 @@ import (
 
 Example of Preparing Orchestration client:
 
-	client, err := openstack.NewOrchestrationV1(provider,  gophercloud.EndpointOpts{Region: "RegionOne"})
+	client, err := openstack.NewOrchestrationV1(context.TODO(), provider, gophercloud.EndpointOpts{Region: "RegionOne"})
 
 Example of List Stack:
 



Test patch:
diff --git a/internal/acceptance/openstack/client_test.go b/internal/acceptance/openstack/client_test.go
index 3b252a285..ccef68855 100644
--- a/internal/acceptance/openstack/client_test.go
+++ b/internal/acceptance/openstack/client_test.go
@@ -37,7 +37,7 @@ func TestAuthenticatedClient(t *testing.T) {
 	t.Logf("Client successfully acquired a token: %v", client.TokenID)
 
 	// Find the compute service in the service catalog.
-	compute, err := openstack.NewComputeV2(client, gophercloud.EndpointOpts{
+	compute, err := openstack.NewComputeV2(context.TODO(), client, gophercloud.EndpointOpts{
 		Region: os.Getenv("OS_REGION_NAME"),
 	})
 	if err != nil {
@@ -128,7 +128,7 @@ func TestReauth(t *testing.T) {
 	}
 
 	t.Logf("Creating a compute client")
-	_, err = openstack.NewComputeV2(provider, gophercloud.EndpointOpts{
+	_, err = openstack.NewComputeV2(context.TODO(), provider, gophercloud.EndpointOpts{
 		Region: os.Getenv("OS_REGION_NAME"),
 	})
 	if err != nil {
@@ -145,7 +145,7 @@ func TestReauth(t *testing.T) {
 	}
 
 	t.Logf("Creating a compute client")
-	_, err = openstack.NewComputeV2(provider, gophercloud.EndpointOpts{
+	_, err = openstack.NewComputeV2(context.TODO(), provider, gophercloud.EndpointOpts{
 		Region: os.Getenv("OS_REGION_NAME"),
 	})
 	if err != nil {
diff --git a/internal/acceptance/openstack/compute/v2/servers_test.go b/internal/acceptance/openstack/compute/v2/servers_test.go
index 7f769e6e9..cebeb43e5 100644
--- a/internal/acceptance/openstack/compute/v2/servers_test.go
+++ b/internal/acceptance/openstack/compute/v2/servers_test.go
@@ -631,3 +631,40 @@ func TestServerNoNetworkCreateDestroy(t *testing.T) {
 		t.Fatalf("Instance must not be a member of specified network")
 	}
 }
+
+func TestServersUpdateHostname(t *testing.T) {
+	clients.RequireLong(t)
+
+	client, err := clients.NewComputeV2Client()
+	th.AssertNoErr(t, err)
+	client.Microversion = "2.90"
+
+	server, err := CreateMicroversionServer(t, client)
+	th.AssertNoErr(t, err)
+	defer DeleteServer(t, client, server)
+
+	alternateHostname := tools.RandomString("ACPTTEST", 16)
+	for alternateHostname == *server.Hostname {
+		alternateHostname = tools.RandomString("ACPTTEST", 16)
+	}
+
+	t.Logf("Attempting to change the server's hostname to %s.", alternateHostname)
+
+	updateOpts := servers.UpdateOpts{
+		Hostname: &alternateHostname,
+	}
+
+	updated, err := servers.Update(context.TODO(), client, server.ID, updateOpts).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, server.ID, updated.ID)
+
+	err = tools.WaitFor(func(ctx context.Context) (bool, error) {
+		latest, err := servers.Get(ctx, client, updated.ID).Extract()
+		if err != nil {
+			return false, err
+		}
+		return *latest.Hostname == alternateHostname, nil
+	})
+	th.AssertNoErr(t, err)
+}
diff --git a/internal/acceptance/openstack/identity/v3/reauth_test.go b/internal/acceptance/openstack/identity/v3/reauth_test.go
index 2936ffce1..f598f21a9 100644
--- a/internal/acceptance/openstack/identity/v3/reauth_test.go
+++ b/internal/acceptance/openstack/identity/v3/reauth_test.go
@@ -27,7 +27,7 @@ func TestReauthAuthResultDeadlock(t *testing.T) {
 
 	provider.SetToken("this is not a valid token")
 
-	client, err := openstack.NewIdentityV3(provider, gophercloud.EndpointOpts{})
+	client, err := openstack.NewIdentityV3(context.TODO(), provider, gophercloud.EndpointOpts{})
 	th.AssertNoErr(t, err)
 	pages, err := projects.List(client, nil).AllPages(context.TODO())
 	th.AssertNoErr(t, err)
diff --git a/openstack/compute/v2/servers/testing/fixtures_test.go b/openstack/compute/v2/servers/testing/fixtures_test.go
index 7d7239d2f..5c5153887 100644
--- a/openstack/compute/v2/servers/testing/fixtures_test.go
+++ b/openstack/compute/v2/servers/testing/fixtures_test.go
@@ -1338,3 +1338,17 @@ func HandleServerWithTagsCreationSuccessfully(t *testing.T, fakeServer th.FakeSe
 		fmt.Fprint(w, SingleServerWithTagsBody)
 	})
 }
+
+// HandleServerHostnameUpdateSuccessfully sets up the test server to respond to a server update
+// request changing the hostname.
+func HandleServerHostnameUpdateSuccessfully(t *testing.T, fakeServer th.FakeServer) {
+	fakeServer.Mux.HandleFunc("/servers/1234asdf", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestJSONRequest(t, r, `{ "server": { "hostname": "new-hostname" } }`)
+
+		fmt.Fprint(w, SingleServerBody)
+	})
+}
diff --git a/openstack/compute/v2/servers/testing/requests_test.go b/openstack/compute/v2/servers/testing/requests_test.go
index 16a16530f..96cb77f14 100644
--- a/openstack/compute/v2/servers/testing/requests_test.go
+++ b/openstack/compute/v2/servers/testing/requests_test.go
@@ -1183,3 +1183,17 @@ func TestCreateServerWithHypervisorHostname(t *testing.T) {
 	th.AssertNoErr(t, err)
 	th.CheckJSONEquals(t, expected, actual)
 }
+
+func TestUpdateServerHostname(t *testing.T) {
+	fakeServer := th.SetupHTTP()
+	defer fakeServer.Teardown()
+	HandleServerHostnameUpdateSuccessfully(t, fakeServer)
+
+	client := client.ServiceClient(fakeServer)
+	actual, err := servers.Update(context.TODO(), client, "1234asdf", servers.UpdateOpts{Hostname: ptr.To("new-hostname")}).Extract()
+	if err != nil {
+		t.Fatalf("Unexpected Update error: %v", err)
+	}
+
+	th.CheckDeepEquals(t, ServerDerp, *actual)
+}
diff --git a/openstack/testing/client_test.go b/openstack/testing/client_test.go
index 214c68ccd..e0a5e4cc0 100644
--- a/openstack/testing/client_test.go
+++ b/openstack/testing/client_test.go
@@ -286,7 +286,7 @@ func TestIdentityAdminV3Client(t *testing.T) {
 	}
 	pc, err := openstack.AuthenticatedClient(context.TODO(), options)
 	th.AssertNoErr(t, err)
-	sc, err := openstack.NewIdentityV3(pc, gophercloud.EndpointOpts{
+	sc, err := openstack.NewIdentityV3(context.TODO(), pc, gophercloud.EndpointOpts{
 		Availability: gophercloud.AvailabilityAdmin,
 	})
 	th.AssertNoErr(t, err)