requests.go raw

   1  package clientconfig
   2  
   3  import (
   4  	"errors"
   5  	"fmt"
   6  	"net/http"
   7  	"os"
   8  	"reflect"
   9  	"strings"
  10  
  11  	"github.com/gophercloud/gophercloud"
  12  	"github.com/gophercloud/gophercloud/openstack"
  13  	"github.com/gophercloud/utils/env"
  14  	"github.com/gophercloud/utils/gnocchi"
  15  	"github.com/gophercloud/utils/internal"
  16  
  17  	"github.com/hashicorp/go-uuid"
  18  	yaml "gopkg.in/yaml.v2"
  19  )
  20  
  21  // AuthType respresents a valid method of authentication.
  22  type AuthType string
  23  
  24  const (
  25  	// AuthPassword defines an unknown version of the password
  26  	AuthPassword AuthType = "password"
  27  	// AuthToken defined an unknown version of the token
  28  	AuthToken AuthType = "token"
  29  
  30  	// AuthV2Password defines version 2 of the password
  31  	AuthV2Password AuthType = "v2password"
  32  	// AuthV2Token defines version 2 of the token
  33  	AuthV2Token AuthType = "v2token"
  34  
  35  	// AuthV3Password defines version 3 of the password
  36  	AuthV3Password AuthType = "v3password"
  37  	// AuthV3Token defines version 3 of the token
  38  	AuthV3Token AuthType = "v3token"
  39  
  40  	// AuthV3ApplicationCredential defines version 3 of the application credential
  41  	AuthV3ApplicationCredential AuthType = "v3applicationcredential"
  42  )
  43  
  44  // ClientOpts represents options to customize the way a client is
  45  // configured.
  46  type ClientOpts struct {
  47  	// Cloud is the cloud entry in clouds.yaml to use.
  48  	Cloud string
  49  
  50  	// EnvPrefix allows a custom environment variable prefix to be used.
  51  	EnvPrefix string
  52  
  53  	// AuthType specifies the type of authentication to use.
  54  	// By default, this is "password".
  55  	AuthType AuthType
  56  
  57  	// AuthInfo defines the authentication information needed to
  58  	// authenticate to a cloud when clouds.yaml isn't used.
  59  	AuthInfo *AuthInfo
  60  
  61  	// RegionName is the region to create a Service Client in.
  62  	// This will override a region in clouds.yaml or can be used
  63  	// when authenticating directly with AuthInfo.
  64  	RegionName string
  65  
  66  	// EndpointType specifies whether to use the public, internal, or
  67  	// admin endpoint of a service.
  68  	EndpointType string
  69  
  70  	// HTTPClient provides the ability customize the ProviderClient's
  71  	// internal HTTP client.
  72  	HTTPClient *http.Client
  73  
  74  	// YAMLOpts provides the ability to pass a customized set
  75  	// of options and methods for loading the YAML file.
  76  	// It takes a YAMLOptsBuilder interface that is defined
  77  	// in this file. This is optional and the default behavior
  78  	// is to call the local LoadCloudsYAML functions defined
  79  	// in this file.
  80  	YAMLOpts YAMLOptsBuilder
  81  }
  82  
  83  // YAMLOptsBuilder defines an interface for customization when
  84  // loading a clouds.yaml file.
  85  type YAMLOptsBuilder interface {
  86  	LoadCloudsYAML() (map[string]Cloud, error)
  87  	LoadSecureCloudsYAML() (map[string]Cloud, error)
  88  	LoadPublicCloudsYAML() (map[string]Cloud, error)
  89  }
  90  
  91  // YAMLOpts represents options and methods to load a clouds.yaml file.
  92  type YAMLOpts struct {
  93  	// By default, no options are specified.
  94  }
  95  
  96  // LoadCloudsYAML defines how to load a clouds.yaml file.
  97  // By default, this calls the local LoadCloudsYAML function.
  98  func (opts YAMLOpts) LoadCloudsYAML() (map[string]Cloud, error) {
  99  	return LoadCloudsYAML()
 100  }
 101  
 102  // LoadSecureCloudsYAML defines how to load a secure.yaml file.
 103  // By default, this calls the local LoadSecureCloudsYAML function.
 104  func (opts YAMLOpts) LoadSecureCloudsYAML() (map[string]Cloud, error) {
 105  	return LoadSecureCloudsYAML()
 106  }
 107  
 108  // LoadPublicCloudsYAML defines how to load a public-secure.yaml file.
 109  // By default, this calls the local LoadPublicCloudsYAML function.
 110  func (opts YAMLOpts) LoadPublicCloudsYAML() (map[string]Cloud, error) {
 111  	return LoadPublicCloudsYAML()
 112  }
 113  
 114  // LoadCloudsYAML will load a clouds.yaml file and return the full config.
 115  // This is called by the YAMLOpts method. Calling this function directly
 116  // is supported for now but has only been retained for backwards
 117  // compatibility from before YAMLOpts was defined. This may be removed in
 118  // the future.
 119  func LoadCloudsYAML() (map[string]Cloud, error) {
 120  	_, content, err := FindAndReadCloudsYAML()
 121  	if err != nil {
 122  		return nil, err
 123  	}
 124  
 125  	var clouds Clouds
 126  	err = yaml.Unmarshal(content, &clouds)
 127  	if err != nil {
 128  		return nil, fmt.Errorf("failed to unmarshal yaml: %w", err)
 129  	}
 130  
 131  	return clouds.Clouds, nil
 132  }
 133  
 134  // LoadSecureCloudsYAML will load a secure.yaml file and return the full config.
 135  // This is called by the YAMLOpts method. Calling this function directly
 136  // is supported for now but has only been retained for backwards
 137  // compatibility from before YAMLOpts was defined. This may be removed in
 138  // the future.
 139  func LoadSecureCloudsYAML() (map[string]Cloud, error) {
 140  	var secureClouds Clouds
 141  
 142  	_, content, err := FindAndReadSecureCloudsYAML()
 143  	if err != nil {
 144  		if errors.Is(err, os.ErrNotExist) {
 145  			// secure.yaml is optional so just ignore read error
 146  			return secureClouds.Clouds, nil
 147  		}
 148  		return nil, err
 149  	}
 150  
 151  	err = yaml.Unmarshal(content, &secureClouds)
 152  	if err != nil {
 153  		return nil, fmt.Errorf("failed to unmarshal yaml: %w", err)
 154  	}
 155  
 156  	return secureClouds.Clouds, nil
 157  }
 158  
 159  // LoadPublicCloudsYAML will load a public-clouds.yaml file and return the full config.
 160  // This is called by the YAMLOpts method. Calling this function directly
 161  // is supported for now but has only been retained for backwards
 162  // compatibility from before YAMLOpts was defined. This may be removed in
 163  // the future.
 164  func LoadPublicCloudsYAML() (map[string]Cloud, error) {
 165  	var publicClouds PublicClouds
 166  
 167  	_, content, err := FindAndReadPublicCloudsYAML()
 168  	if err != nil {
 169  		if errors.Is(err, os.ErrNotExist) {
 170  			// clouds-public.yaml is optional so just ignore read error
 171  			return publicClouds.Clouds, nil
 172  		}
 173  
 174  		return nil, err
 175  	}
 176  
 177  	err = yaml.Unmarshal(content, &publicClouds)
 178  	if err != nil {
 179  		return nil, fmt.Errorf("failed to unmarshal yaml: %w", err)
 180  	}
 181  
 182  	return publicClouds.Clouds, nil
 183  }
 184  
 185  // GetCloudFromYAML will return a cloud entry from a clouds.yaml file.
 186  func GetCloudFromYAML(opts *ClientOpts) (*Cloud, error) {
 187  	if opts == nil {
 188  		opts = new(ClientOpts)
 189  	}
 190  
 191  	if opts.YAMLOpts == nil {
 192  		opts.YAMLOpts = new(YAMLOpts)
 193  	}
 194  
 195  	yamlOpts := opts.YAMLOpts
 196  
 197  	clouds, err := yamlOpts.LoadCloudsYAML()
 198  	if err != nil {
 199  		return nil, fmt.Errorf("unable to load clouds.yaml: %w", err)
 200  	}
 201  
 202  	// Determine which cloud to use.
 203  	// First see if a cloud name was explicitly set in opts.
 204  	var cloudName string
 205  	if opts.Cloud != "" {
 206  		cloudName = opts.Cloud
 207  	} else {
 208  		// If not, see if a cloud name was specified as an environment variable.
 209  		envPrefix := "OS_"
 210  		if opts.EnvPrefix != "" {
 211  			envPrefix = opts.EnvPrefix
 212  		}
 213  
 214  		if v := env.Getenv(envPrefix + "CLOUD"); v != "" {
 215  			cloudName = v
 216  		}
 217  	}
 218  
 219  	var cloud *Cloud
 220  	if cloudName != "" {
 221  		v, ok := clouds[cloudName]
 222  		if !ok {
 223  			return nil, fmt.Errorf("cloud %s does not exist in clouds.yaml", cloudName)
 224  		}
 225  		cloud = &v
 226  	}
 227  
 228  	// If a cloud was not specified, and clouds only contains
 229  	// a single entry, use that entry.
 230  	if cloudName == "" && len(clouds) == 1 {
 231  		for _, v := range clouds {
 232  			cloud = &v
 233  		}
 234  	}
 235  
 236  	if cloud != nil {
 237  		// A profile points to a public cloud entry.
 238  		// If one was specified, load a list of public clouds
 239  		// and then merge the information with the current cloud data.
 240  		profileName := defaultIfEmpty(cloud.Profile, cloud.Cloud)
 241  
 242  		if profileName != "" {
 243  			publicClouds, err := yamlOpts.LoadPublicCloudsYAML()
 244  			if err != nil {
 245  				return nil, fmt.Errorf("unable to load clouds-public.yaml: %w", err)
 246  			}
 247  
 248  			publicCloud, ok := publicClouds[profileName]
 249  			if !ok {
 250  				return nil, fmt.Errorf("cloud %s does not exist in clouds-public.yaml", profileName)
 251  			}
 252  
 253  			cloud, err = mergeClouds(cloud, publicCloud)
 254  			if err != nil {
 255  				return nil, fmt.Errorf("Could not merge information from clouds.yaml and clouds-public.yaml for cloud %s", profileName)
 256  			}
 257  		}
 258  	}
 259  
 260  	// Next, load a secure clouds file and see if a cloud entry
 261  	// can be found or merged.
 262  	secureClouds, err := yamlOpts.LoadSecureCloudsYAML()
 263  	if err != nil {
 264  		return nil, fmt.Errorf("unable to load secure.yaml: %w", err)
 265  	}
 266  
 267  	if secureClouds != nil {
 268  		// If no entry was found in clouds.yaml, no cloud name was specified,
 269  		// and only one secureCloud entry exists, use that as the cloud entry.
 270  		if cloud == nil && cloudName == "" && len(secureClouds) == 1 {
 271  			for _, v := range secureClouds {
 272  				cloud = &v
 273  			}
 274  		}
 275  
 276  		// Otherwise, see if the provided cloud name exists in the secure yaml file.
 277  		secureCloud, ok := secureClouds[cloudName]
 278  		if !ok && cloud == nil {
 279  			// cloud == nil serves two purposes here:
 280  			// if no entry in clouds.yaml was found and
 281  			// if a single-entry secureCloud wasn't used.
 282  			// At this point, no entry could be determined at all.
 283  			return nil, fmt.Errorf("Could not find cloud %s", cloudName)
 284  		}
 285  
 286  		// If secureCloud has content and it differs from the cloud entry,
 287  		// merge the two together.
 288  		if !reflect.DeepEqual((Cloud{}), secureCloud) && !reflect.DeepEqual(cloud, secureCloud) {
 289  			cloud, err = mergeClouds(secureCloud, cloud)
 290  			if err != nil {
 291  				return nil, fmt.Errorf("unable to merge information from clouds.yaml and secure.yaml")
 292  			}
 293  		}
 294  	}
 295  
 296  	// As an extra precaution, do one final check to see if cloud is nil.
 297  	// We shouldn't reach this point, though.
 298  	if cloud == nil {
 299  		return nil, fmt.Errorf("Could not find cloud %s", cloudName)
 300  	}
 301  
 302  	// Default is to verify SSL API requests
 303  	if cloud.Verify == nil {
 304  		iTrue := true
 305  		cloud.Verify = &iTrue
 306  	}
 307  
 308  	// merging per-region value overrides
 309  	if opts.RegionName != "" {
 310  		for _, v := range cloud.Regions {
 311  			if opts.RegionName == v.Name {
 312  				cloud, err = mergeClouds(v.Values, cloud)
 313  				break
 314  			}
 315  		}
 316  	}
 317  
 318  	// TODO: this is where reading vendor files should go be considered when not found in
 319  	// clouds-public.yml
 320  	// https://github.com/openstack/openstacksdk/tree/master/openstack/config/vendors
 321  
 322  	// Both Interface and EndpointType are valid settings in clouds.yaml,
 323  	// but we want to standardize on EndpointType for simplicity.
 324  	//
 325  	// If only Interface was set, we copy that to EndpointType to use as the setting.
 326  	// But in all other cases, EndpointType is used and Interface is cleared.
 327  	if cloud.Interface != "" && cloud.EndpointType == "" {
 328  		cloud.EndpointType = cloud.Interface
 329  	}
 330  
 331  	cloud.Interface = ""
 332  
 333  	return cloud, nil
 334  }
 335  
 336  // AuthOptions creates a gophercloud.AuthOptions structure with the
 337  // settings found in a specific cloud entry of a clouds.yaml file or
 338  // based on authentication settings given in ClientOpts.
 339  //
 340  // This attempts to be a single point of entry for all OpenStack authentication.
 341  //
 342  // See http://docs.openstack.org/developer/os-client-config and
 343  // https://github.com/openstack/os-client-config/blob/master/os_client_config/config.py.
 344  func AuthOptions(opts *ClientOpts) (*gophercloud.AuthOptions, error) {
 345  	cloud := new(Cloud)
 346  
 347  	// If no opts were passed in, create an empty ClientOpts.
 348  	if opts == nil {
 349  		opts = new(ClientOpts)
 350  	}
 351  
 352  	// Determine if a clouds.yaml entry should be retrieved.
 353  	// Start by figuring out the cloud name.
 354  	// First check if one was explicitly specified in opts.
 355  	var cloudName string
 356  	if opts.Cloud != "" {
 357  		cloudName = opts.Cloud
 358  	} else {
 359  		// If not, see if a cloud name was specified as an environment
 360  		// variable.
 361  		envPrefix := "OS_"
 362  		if opts.EnvPrefix != "" {
 363  			envPrefix = opts.EnvPrefix
 364  		}
 365  
 366  		if v := env.Getenv(envPrefix + "CLOUD"); v != "" {
 367  			cloudName = v
 368  		}
 369  	}
 370  
 371  	// If a cloud name was determined, try to look it up in clouds.yaml.
 372  	if cloudName != "" {
 373  		// Get the requested cloud.
 374  		var err error
 375  		cloud, err = GetCloudFromYAML(opts)
 376  		if err != nil {
 377  			return nil, err
 378  		}
 379  	}
 380  
 381  	// If cloud.AuthInfo is nil, then no cloud was specified.
 382  	if cloud.AuthInfo == nil {
 383  		// If opts.AuthInfo is not nil, then try using the auth settings from it.
 384  		if opts.AuthInfo != nil {
 385  			cloud.AuthInfo = opts.AuthInfo
 386  		}
 387  
 388  		// If cloud.AuthInfo is still nil, then set it to an empty Auth struct
 389  		// and rely on environment variables to do the authentication.
 390  		if cloud.AuthInfo == nil {
 391  			cloud.AuthInfo = new(AuthInfo)
 392  		}
 393  	}
 394  
 395  	identityAPI := determineIdentityAPI(cloud, opts)
 396  	switch identityAPI {
 397  	case "2.0", "2":
 398  		return v2auth(cloud, opts)
 399  	case "3":
 400  		return v3auth(cloud, opts)
 401  	}
 402  
 403  	return nil, fmt.Errorf("Unable to build AuthOptions")
 404  }
 405  
 406  func determineIdentityAPI(cloud *Cloud, opts *ClientOpts) string {
 407  	var identityAPI string
 408  	if cloud.IdentityAPIVersion != "" {
 409  		identityAPI = cloud.IdentityAPIVersion
 410  	}
 411  
 412  	envPrefix := "OS_"
 413  	if opts != nil && opts.EnvPrefix != "" {
 414  		envPrefix = opts.EnvPrefix
 415  	}
 416  
 417  	if v := env.Getenv(envPrefix + "IDENTITY_API_VERSION"); v != "" {
 418  		identityAPI = v
 419  	}
 420  
 421  	if identityAPI == "" {
 422  		if cloud.AuthInfo != nil {
 423  			if strings.Contains(cloud.AuthInfo.AuthURL, "v2.0") {
 424  				identityAPI = "2.0"
 425  			}
 426  
 427  			if strings.Contains(cloud.AuthInfo.AuthURL, "v3") {
 428  				identityAPI = "3"
 429  			}
 430  		}
 431  	}
 432  
 433  	if identityAPI == "" {
 434  		switch cloud.AuthType {
 435  		case AuthV2Password:
 436  			identityAPI = "2.0"
 437  		case AuthV2Token:
 438  			identityAPI = "2.0"
 439  		case AuthV3Password:
 440  			identityAPI = "3"
 441  		case AuthV3Token:
 442  			identityAPI = "3"
 443  		case AuthV3ApplicationCredential:
 444  			identityAPI = "3"
 445  		}
 446  	}
 447  
 448  	// If an Identity API version could not be determined,
 449  	// default to v3.
 450  	if identityAPI == "" {
 451  		identityAPI = "3"
 452  	}
 453  
 454  	return identityAPI
 455  }
 456  
 457  // v2auth creates a v2-compatible gophercloud.AuthOptions struct.
 458  func v2auth(cloud *Cloud, opts *ClientOpts) (*gophercloud.AuthOptions, error) {
 459  	// Environment variable overrides.
 460  	envPrefix := "OS_"
 461  	if opts != nil && opts.EnvPrefix != "" {
 462  		envPrefix = opts.EnvPrefix
 463  	}
 464  
 465  	if cloud.AuthInfo.AuthURL == "" {
 466  		if v := env.Getenv(envPrefix + "AUTH_URL"); v != "" {
 467  			cloud.AuthInfo.AuthURL = v
 468  		}
 469  	}
 470  
 471  	if cloud.AuthInfo.Token == "" {
 472  		if v := env.Getenv(envPrefix + "TOKEN"); v != "" {
 473  			cloud.AuthInfo.Token = v
 474  		}
 475  
 476  		if v := env.Getenv(envPrefix + "AUTH_TOKEN"); v != "" {
 477  			cloud.AuthInfo.Token = v
 478  		}
 479  	}
 480  
 481  	if cloud.AuthInfo.Username == "" {
 482  		if v := env.Getenv(envPrefix + "USERNAME"); v != "" {
 483  			cloud.AuthInfo.Username = v
 484  		}
 485  	}
 486  
 487  	if cloud.AuthInfo.Password == "" {
 488  		if v := env.Getenv(envPrefix + "PASSWORD"); v != "" {
 489  			cloud.AuthInfo.Password = v
 490  		}
 491  	}
 492  
 493  	if cloud.AuthInfo.ProjectID == "" {
 494  		if v := env.Getenv(envPrefix + "TENANT_ID"); v != "" {
 495  			cloud.AuthInfo.ProjectID = v
 496  		}
 497  
 498  		if v := env.Getenv(envPrefix + "PROJECT_ID"); v != "" {
 499  			cloud.AuthInfo.ProjectID = v
 500  		}
 501  	}
 502  
 503  	if cloud.AuthInfo.ProjectName == "" {
 504  		if v := env.Getenv(envPrefix + "TENANT_NAME"); v != "" {
 505  			cloud.AuthInfo.ProjectName = v
 506  		}
 507  
 508  		if v := env.Getenv(envPrefix + "PROJECT_NAME"); v != "" {
 509  			cloud.AuthInfo.ProjectName = v
 510  		}
 511  	}
 512  
 513  	ao := &gophercloud.AuthOptions{
 514  		IdentityEndpoint: cloud.AuthInfo.AuthURL,
 515  		TokenID:          cloud.AuthInfo.Token,
 516  		Username:         cloud.AuthInfo.Username,
 517  		Password:         cloud.AuthInfo.Password,
 518  		TenantID:         cloud.AuthInfo.ProjectID,
 519  		TenantName:       cloud.AuthInfo.ProjectName,
 520  		AllowReauth:      cloud.AuthInfo.AllowReauth,
 521  	}
 522  
 523  	return ao, nil
 524  }
 525  
 526  // v3auth creates a v3-compatible gophercloud.AuthOptions struct.
 527  func v3auth(cloud *Cloud, opts *ClientOpts) (*gophercloud.AuthOptions, error) {
 528  	// Environment variable overrides.
 529  	envPrefix := "OS_"
 530  	if opts != nil && opts.EnvPrefix != "" {
 531  		envPrefix = opts.EnvPrefix
 532  	}
 533  
 534  	if cloud.AuthInfo.AuthURL == "" {
 535  		if v := env.Getenv(envPrefix + "AUTH_URL"); v != "" {
 536  			cloud.AuthInfo.AuthURL = v
 537  		}
 538  	}
 539  
 540  	if cloud.AuthInfo.Token == "" {
 541  		if v := env.Getenv(envPrefix + "TOKEN"); v != "" {
 542  			cloud.AuthInfo.Token = v
 543  		}
 544  
 545  		if v := env.Getenv(envPrefix + "AUTH_TOKEN"); v != "" {
 546  			cloud.AuthInfo.Token = v
 547  		}
 548  	}
 549  
 550  	if cloud.AuthInfo.Username == "" {
 551  		if v := env.Getenv(envPrefix + "USERNAME"); v != "" {
 552  			cloud.AuthInfo.Username = v
 553  		}
 554  	}
 555  
 556  	if cloud.AuthInfo.UserID == "" {
 557  		if v := env.Getenv(envPrefix + "USER_ID"); v != "" {
 558  			cloud.AuthInfo.UserID = v
 559  		}
 560  	}
 561  
 562  	if cloud.AuthInfo.Password == "" {
 563  		if v := env.Getenv(envPrefix + "PASSWORD"); v != "" {
 564  			cloud.AuthInfo.Password = v
 565  		}
 566  	}
 567  
 568  	if cloud.AuthInfo.ProjectID == "" {
 569  		if v := env.Getenv(envPrefix + "TENANT_ID"); v != "" {
 570  			cloud.AuthInfo.ProjectID = v
 571  		}
 572  
 573  		if v := env.Getenv(envPrefix + "PROJECT_ID"); v != "" {
 574  			cloud.AuthInfo.ProjectID = v
 575  		}
 576  	}
 577  
 578  	if cloud.AuthInfo.ProjectName == "" {
 579  		if v := env.Getenv(envPrefix + "TENANT_NAME"); v != "" {
 580  			cloud.AuthInfo.ProjectName = v
 581  		}
 582  
 583  		if v := env.Getenv(envPrefix + "PROJECT_NAME"); v != "" {
 584  			cloud.AuthInfo.ProjectName = v
 585  		}
 586  	}
 587  
 588  	if cloud.AuthInfo.DomainID == "" {
 589  		if v := env.Getenv(envPrefix + "DOMAIN_ID"); v != "" {
 590  			cloud.AuthInfo.DomainID = v
 591  		}
 592  	}
 593  
 594  	if cloud.AuthInfo.DomainName == "" {
 595  		if v := env.Getenv(envPrefix + "DOMAIN_NAME"); v != "" {
 596  			cloud.AuthInfo.DomainName = v
 597  		}
 598  	}
 599  
 600  	if cloud.AuthInfo.DefaultDomain == "" {
 601  		if v := env.Getenv(envPrefix + "DEFAULT_DOMAIN"); v != "" {
 602  			cloud.AuthInfo.DefaultDomain = v
 603  		}
 604  	}
 605  
 606  	if cloud.AuthInfo.ProjectDomainID == "" {
 607  		if v := env.Getenv(envPrefix + "PROJECT_DOMAIN_ID"); v != "" {
 608  			cloud.AuthInfo.ProjectDomainID = v
 609  		}
 610  	}
 611  
 612  	if cloud.AuthInfo.ProjectDomainName == "" {
 613  		if v := env.Getenv(envPrefix + "PROJECT_DOMAIN_NAME"); v != "" {
 614  			cloud.AuthInfo.ProjectDomainName = v
 615  		}
 616  	}
 617  
 618  	if cloud.AuthInfo.UserDomainID == "" {
 619  		if v := env.Getenv(envPrefix + "USER_DOMAIN_ID"); v != "" {
 620  			cloud.AuthInfo.UserDomainID = v
 621  		}
 622  	}
 623  
 624  	if cloud.AuthInfo.UserDomainName == "" {
 625  		if v := env.Getenv(envPrefix + "USER_DOMAIN_NAME"); v != "" {
 626  			cloud.AuthInfo.UserDomainName = v
 627  		}
 628  	}
 629  
 630  	if cloud.AuthInfo.ApplicationCredentialID == "" {
 631  		if v := env.Getenv(envPrefix + "APPLICATION_CREDENTIAL_ID"); v != "" {
 632  			cloud.AuthInfo.ApplicationCredentialID = v
 633  		}
 634  	}
 635  
 636  	if cloud.AuthInfo.ApplicationCredentialName == "" {
 637  		if v := env.Getenv(envPrefix + "APPLICATION_CREDENTIAL_NAME"); v != "" {
 638  			cloud.AuthInfo.ApplicationCredentialName = v
 639  		}
 640  	}
 641  
 642  	if cloud.AuthInfo.ApplicationCredentialSecret == "" {
 643  		if v := env.Getenv(envPrefix + "APPLICATION_CREDENTIAL_SECRET"); v != "" {
 644  			cloud.AuthInfo.ApplicationCredentialSecret = v
 645  		}
 646  	}
 647  
 648  	if cloud.AuthInfo.SystemScope == "" {
 649  		if v := env.Getenv(envPrefix + "SYSTEM_SCOPE"); v != "" {
 650  			cloud.AuthInfo.SystemScope = v
 651  		}
 652  	}
 653  
 654  	// Build a scope and try to do it correctly.
 655  	// https://github.com/openstack/os-client-config/blob/master/os_client_config/config.py#L595
 656  	scope := new(gophercloud.AuthScope)
 657  
 658  	// Application credentials don't support scope
 659  	if isApplicationCredential(cloud.AuthInfo) {
 660  		// If Domain* is set, but UserDomain* or ProjectDomain* aren't,
 661  		// then use Domain* as the default setting.
 662  		cloud = setDomainIfNeeded(cloud)
 663  	} else {
 664  		if !isProjectScoped(cloud.AuthInfo) {
 665  			if cloud.AuthInfo.DomainID != "" {
 666  				scope.DomainID = cloud.AuthInfo.DomainID
 667  			} else if cloud.AuthInfo.DomainName != "" {
 668  				scope.DomainName = cloud.AuthInfo.DomainName
 669  			}
 670  			if cloud.AuthInfo.SystemScope != "" {
 671  				scope.System = true
 672  			}
 673  		} else {
 674  			// If Domain* is set, but UserDomain* or ProjectDomain* aren't,
 675  			// then use Domain* as the default setting.
 676  			cloud = setDomainIfNeeded(cloud)
 677  
 678  			if cloud.AuthInfo.ProjectID != "" {
 679  				scope.ProjectID = cloud.AuthInfo.ProjectID
 680  			} else {
 681  				scope.ProjectName = cloud.AuthInfo.ProjectName
 682  				scope.DomainID = cloud.AuthInfo.ProjectDomainID
 683  				scope.DomainName = cloud.AuthInfo.ProjectDomainName
 684  			}
 685  		}
 686  	}
 687  
 688  	ao := &gophercloud.AuthOptions{
 689  		Scope:                       scope,
 690  		IdentityEndpoint:            cloud.AuthInfo.AuthURL,
 691  		TokenID:                     cloud.AuthInfo.Token,
 692  		Username:                    cloud.AuthInfo.Username,
 693  		UserID:                      cloud.AuthInfo.UserID,
 694  		Password:                    cloud.AuthInfo.Password,
 695  		TenantID:                    cloud.AuthInfo.ProjectID,
 696  		TenantName:                  cloud.AuthInfo.ProjectName,
 697  		DomainID:                    cloud.AuthInfo.UserDomainID,
 698  		DomainName:                  cloud.AuthInfo.UserDomainName,
 699  		ApplicationCredentialID:     cloud.AuthInfo.ApplicationCredentialID,
 700  		ApplicationCredentialName:   cloud.AuthInfo.ApplicationCredentialName,
 701  		ApplicationCredentialSecret: cloud.AuthInfo.ApplicationCredentialSecret,
 702  		AllowReauth:                 cloud.AuthInfo.AllowReauth,
 703  	}
 704  
 705  	// If an auth_type of "token" was specified, then make sure
 706  	// Gophercloud properly authenticates with a token. This involves
 707  	// unsetting a few other auth options. The reason this is done
 708  	// here is to wait until all auth settings (both in clouds.yaml
 709  	// and via environment variables) are set and then unset them.
 710  	if strings.Contains(string(cloud.AuthType), "token") || ao.TokenID != "" {
 711  		ao.Username = ""
 712  		ao.Password = ""
 713  		ao.UserID = ""
 714  		ao.DomainID = ""
 715  		ao.DomainName = ""
 716  	}
 717  
 718  	// Check for absolute minimum requirements.
 719  	if ao.IdentityEndpoint == "" {
 720  		err := gophercloud.ErrMissingInput{Argument: "auth_url"}
 721  		return nil, err
 722  	}
 723  
 724  	return ao, nil
 725  }
 726  
 727  // AuthenticatedClient is a convenience function to get a new provider client
 728  // based on a clouds.yaml entry.
 729  func AuthenticatedClient(opts *ClientOpts) (*gophercloud.ProviderClient, error) {
 730  	ao, err := AuthOptions(opts)
 731  	if err != nil {
 732  		return nil, err
 733  	}
 734  
 735  	return openstack.AuthenticatedClient(*ao)
 736  }
 737  
 738  // NewServiceClient is a convenience function to get a new service client.
 739  func NewServiceClient(service string, opts *ClientOpts) (*gophercloud.ServiceClient, error) {
 740  	cloud := new(Cloud)
 741  
 742  	// If no opts were passed in, create an empty ClientOpts.
 743  	if opts == nil {
 744  		opts = new(ClientOpts)
 745  	}
 746  
 747  	// Determine if a clouds.yaml entry should be retrieved.
 748  	// Start by figuring out the cloud name.
 749  	// First check if one was explicitly specified in opts.
 750  	var cloudName string
 751  	if opts.Cloud != "" {
 752  		cloudName = opts.Cloud
 753  	}
 754  
 755  	// Next see if a cloud name was specified as an environment variable.
 756  	envPrefix := "OS_"
 757  	if opts.EnvPrefix != "" {
 758  		envPrefix = opts.EnvPrefix
 759  	}
 760  
 761  	if v := env.Getenv(envPrefix + "CLOUD"); v != "" {
 762  		cloudName = v
 763  	}
 764  
 765  	// If a cloud name was determined, try to look it up in clouds.yaml.
 766  	if cloudName != "" {
 767  		// Get the requested cloud.
 768  		var err error
 769  		cloud, err = GetCloudFromYAML(opts)
 770  		if err != nil {
 771  			return nil, err
 772  		}
 773  	}
 774  
 775  	// Check if a custom CA cert was provided.
 776  	// First, check if the CACERT environment variable is set.
 777  	var caCertPath string
 778  	if v := env.Getenv(envPrefix + "CACERT"); v != "" {
 779  		caCertPath = v
 780  	}
 781  	// Next, check if the cloud entry sets a CA cert.
 782  	if v := cloud.CACertFile; v != "" {
 783  		caCertPath = v
 784  	}
 785  
 786  	// Check if a custom client cert was provided.
 787  	// First, check if the CERT environment variable is set.
 788  	var clientCertPath string
 789  	if v := env.Getenv(envPrefix + "CERT"); v != "" {
 790  		clientCertPath = v
 791  	}
 792  	// Next, check if the cloud entry sets a client cert.
 793  	if v := cloud.ClientCertFile; v != "" {
 794  		clientCertPath = v
 795  	}
 796  
 797  	// Check if a custom client key was provided.
 798  	// First, check if the KEY environment variable is set.
 799  	var clientKeyPath string
 800  	if v := env.Getenv(envPrefix + "KEY"); v != "" {
 801  		clientKeyPath = v
 802  	}
 803  	// Next, check if the cloud entry sets a client key.
 804  	if v := cloud.ClientKeyFile; v != "" {
 805  		clientKeyPath = v
 806  	}
 807  
 808  	// Define whether or not SSL API requests should be verified.
 809  	var insecurePtr *bool
 810  	if cloud.Verify != nil {
 811  		// Here we take the boolean pointer negation.
 812  		insecure := !*cloud.Verify
 813  		insecurePtr = &insecure
 814  	}
 815  
 816  	tlsConfig, err := internal.PrepareTLSConfig(caCertPath, clientCertPath, clientKeyPath, insecurePtr)
 817  	if err != nil {
 818  		return nil, err
 819  	}
 820  
 821  	// Get a Provider Client
 822  	ao, err := AuthOptions(opts)
 823  	if err != nil {
 824  		return nil, err
 825  	}
 826  	pClient, err := openstack.NewClient(ao.IdentityEndpoint)
 827  	if err != nil {
 828  		return nil, err
 829  	}
 830  
 831  	// If an HTTPClient was specified, use it.
 832  	if opts.HTTPClient != nil {
 833  		pClient.HTTPClient = *opts.HTTPClient
 834  	} else {
 835  		// Otherwise create a new HTTP client with the generated TLS config.
 836  		transport := http.DefaultTransport.(*http.Transport).Clone()
 837  		transport.TLSClientConfig = tlsConfig
 838  		pClient.HTTPClient = http.Client{Transport: transport}
 839  	}
 840  
 841  	err = openstack.Authenticate(pClient, *ao)
 842  	if err != nil {
 843  		return nil, err
 844  	}
 845  
 846  	// Determine the region to use.
 847  	// First, check if the REGION_NAME environment variable is set.
 848  	var region string
 849  	if v := env.Getenv(envPrefix + "REGION_NAME"); v != "" {
 850  		region = v
 851  	}
 852  
 853  	// Next, check if the cloud entry sets a region.
 854  	if v := cloud.RegionName; v != "" {
 855  		region = v
 856  	}
 857  
 858  	// Finally, see if one was specified in the ClientOpts.
 859  	// If so, this takes precedence.
 860  	if v := opts.RegionName; v != "" {
 861  		region = v
 862  	}
 863  
 864  	// Determine the endpoint type to use.
 865  	// First, check if the OS_INTERFACE environment variable is set.
 866  	var endpointType string
 867  	if v := env.Getenv(envPrefix + "INTERFACE"); v != "" {
 868  		endpointType = v
 869  	}
 870  
 871  	// Next, check if the cloud entry sets an endpoint type.
 872  	if v := cloud.EndpointType; v != "" {
 873  		endpointType = v
 874  	}
 875  
 876  	// Finally, see if one was specified in the ClientOpts.
 877  	// If so, this takes precedence.
 878  	if v := opts.EndpointType; v != "" {
 879  		endpointType = v
 880  	}
 881  
 882  	eo := gophercloud.EndpointOpts{
 883  		Region:       region,
 884  		Availability: GetEndpointType(endpointType),
 885  	}
 886  
 887  	switch service {
 888  	case "baremetal":
 889  		return openstack.NewBareMetalV1(pClient, eo)
 890  	case "baremetal-introspection":
 891  		return openstack.NewBareMetalIntrospectionV1(pClient, eo)
 892  	case "clustering":
 893  		return openstack.NewClusteringV1(pClient, eo)
 894  	case "compute":
 895  		return openstack.NewComputeV2(pClient, eo)
 896  	case "container":
 897  		return openstack.NewContainerV1(pClient, eo)
 898  	case "container-infra":
 899  		return openstack.NewContainerInfraV1(pClient, eo)
 900  	case "database":
 901  		return openstack.NewDBV1(pClient, eo)
 902  	case "dns":
 903  		return openstack.NewDNSV2(pClient, eo)
 904  	case "gnocchi":
 905  		return gnocchi.NewGnocchiV1(pClient, eo)
 906  	case "identity":
 907  		identityVersion := "3"
 908  		if v := cloud.IdentityAPIVersion; v != "" {
 909  			identityVersion = v
 910  		}
 911  
 912  		switch identityVersion {
 913  		case "v2", "2", "2.0":
 914  			return openstack.NewIdentityV2(pClient, eo)
 915  		case "v3", "3":
 916  			return openstack.NewIdentityV3(pClient, eo)
 917  		default:
 918  			return nil, fmt.Errorf("invalid identity API version")
 919  		}
 920  	case "image":
 921  		return openstack.NewImageServiceV2(pClient, eo)
 922  	case "key-manager":
 923  		return openstack.NewKeyManagerV1(pClient, eo)
 924  	case "load-balancer":
 925  		return openstack.NewLoadBalancerV2(pClient, eo)
 926  	case "messaging":
 927  		clientID, err := uuid.GenerateUUID()
 928  		if err != nil {
 929  			return nil, fmt.Errorf("failed to generate UUID: %w", err)
 930  		}
 931  		return openstack.NewMessagingV2(pClient, clientID, eo)
 932  	case "network":
 933  		return openstack.NewNetworkV2(pClient, eo)
 934  	case "object-store":
 935  		return openstack.NewObjectStorageV1(pClient, eo)
 936  	case "orchestration":
 937  		return openstack.NewOrchestrationV1(pClient, eo)
 938  	case "placement":
 939  		return openstack.NewPlacementV1(pClient, eo)
 940  	case "sharev2":
 941  		return openstack.NewSharedFileSystemV2(pClient, eo)
 942  	case "volume":
 943  		volumeVersion := "3"
 944  		if v := cloud.VolumeAPIVersion; v != "" {
 945  			volumeVersion = v
 946  		}
 947  
 948  		switch volumeVersion {
 949  		case "v1", "1":
 950  			return openstack.NewBlockStorageV1(pClient, eo)
 951  		case "v2", "2":
 952  			return openstack.NewBlockStorageV2(pClient, eo)
 953  		case "v3", "3":
 954  			return openstack.NewBlockStorageV3(pClient, eo)
 955  		default:
 956  			return nil, fmt.Errorf("invalid volume API version")
 957  		}
 958  	case "workflowv2":
 959  		return openstack.NewWorkflowV2(pClient, eo)
 960  	}
 961  
 962  	return nil, fmt.Errorf("unable to create a service client for %s", service)
 963  }
 964  
 965  // isProjectScoped determines if an auth struct is project scoped.
 966  func isProjectScoped(authInfo *AuthInfo) bool {
 967  	if authInfo.ProjectID == "" && authInfo.ProjectName == "" {
 968  		return false
 969  	}
 970  
 971  	return true
 972  }
 973  
 974  // setDomainIfNeeded will set a DomainID and DomainName
 975  // to ProjectDomain* and UserDomain* if not already set.
 976  func setDomainIfNeeded(cloud *Cloud) *Cloud {
 977  	if cloud.AuthInfo.DomainID != "" {
 978  		if cloud.AuthInfo.UserDomainID == "" {
 979  			cloud.AuthInfo.UserDomainID = cloud.AuthInfo.DomainID
 980  		}
 981  
 982  		if cloud.AuthInfo.ProjectDomainID == "" {
 983  			cloud.AuthInfo.ProjectDomainID = cloud.AuthInfo.DomainID
 984  		}
 985  
 986  		cloud.AuthInfo.DomainID = ""
 987  	}
 988  
 989  	if cloud.AuthInfo.DomainName != "" {
 990  		if cloud.AuthInfo.UserDomainName == "" {
 991  			cloud.AuthInfo.UserDomainName = cloud.AuthInfo.DomainName
 992  		}
 993  
 994  		if cloud.AuthInfo.ProjectDomainName == "" {
 995  			cloud.AuthInfo.ProjectDomainName = cloud.AuthInfo.DomainName
 996  		}
 997  
 998  		cloud.AuthInfo.DomainName = ""
 999  	}
1000  
1001  	// If Domain fields are still not set, and if DefaultDomain has a value,
1002  	// set UserDomainID and ProjectDomainID to DefaultDomain.
1003  	// https://github.com/openstack/osc-lib/blob/86129e6f88289ef14bfaa3f7c9cdfbea8d9fc944/osc_lib/cli/client_config.py#L117-L146
1004  	if cloud.AuthInfo.DefaultDomain != "" {
1005  		if cloud.AuthInfo.UserDomainName == "" && cloud.AuthInfo.UserDomainID == "" {
1006  			cloud.AuthInfo.UserDomainID = cloud.AuthInfo.DefaultDomain
1007  		}
1008  
1009  		if cloud.AuthInfo.ProjectDomainName == "" && cloud.AuthInfo.ProjectDomainID == "" {
1010  			cloud.AuthInfo.ProjectDomainID = cloud.AuthInfo.DefaultDomain
1011  		}
1012  	}
1013  
1014  	return cloud
1015  }
1016  
1017  // isApplicationCredential determines if an application credential is used to auth.
1018  func isApplicationCredential(authInfo *AuthInfo) bool {
1019  	if authInfo.ApplicationCredentialID == "" && authInfo.ApplicationCredentialName == "" && authInfo.ApplicationCredentialSecret == "" {
1020  		return false
1021  	}
1022  	return true
1023  }
1024