github.com/opentelekomcloud/gophertelekomcloud@v0.9.3/openstack/loader.go (about)

     1  package openstack
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"log"
     8  	"os"
     9  	"path/filepath"
    10  	"strings"
    11  
    12  	"gopkg.in/yaml.v2"
    13  
    14  	"github.com/opentelekomcloud/gophertelekomcloud"
    15  	"github.com/opentelekomcloud/gophertelekomcloud/openstack/utils"
    16  )
    17  
    18  const (
    19  	defaultEnvVarKey = "envvars"
    20  	defaultPrefix    = "OS_"
    21  
    22  	DefaultProfileName = "otc"
    23  	regionPlaceHolder  = "{region_name}"
    24  )
    25  
    26  var (
    27  	yamlSuffixes = []string{".yaml", ".yml"}
    28  	jsonSuffixes = []string{".json"}
    29  
    30  	configFiles = fileList("clouds")
    31  	secureFiles = fileList("secure")
    32  	vendorFiles = fileList("clouds-public")
    33  
    34  	OTCVendorConfig = &VendorConfig{
    35  		Clouds: map[string]Cloud{
    36  			DefaultProfileName: {
    37  				AuthInfo: AuthInfo{
    38  					AuthURL: fmt.Sprintf("https://iam.%s.otc.t-systems.com/v3", regionPlaceHolder),
    39  				},
    40  				Regions:            []string{"eu-de", "eu-nl"},
    41  				EndpointType:       "public",
    42  				IdentityAPIVersion: "3",
    43  			},
    44  		},
    45  	}
    46  )
    47  
    48  func configSearchPath() []string {
    49  	home, _ := os.UserHomeDir()
    50  	cwd, _ := os.Getwd()
    51  	userConfigDir, _ := filepath.Abs(filepath.Join(home, ".config/openstack"))
    52  	unixConfigDir, _ := filepath.Abs("/etc/openstack")
    53  	return []string{
    54  		cwd,
    55  		userConfigDir,
    56  		unixConfigDir,
    57  	}
    58  }
    59  
    60  func fileList(name string) []string {
    61  	paths := configSearchPath()
    62  	var suffixes []string
    63  	suffixes = append(suffixes, yamlSuffixes...)
    64  	suffixes = append(suffixes, jsonSuffixes...)
    65  	size := len(suffixes) * len(paths)
    66  	files := make([]string, size)
    67  	i := 0
    68  	for _, path := range paths {
    69  		for _, suffix := range suffixes {
    70  			files[i] = filepath.Join(path, name+suffix)
    71  			i++
    72  		}
    73  	}
    74  	return files
    75  }
    76  
    77  type Env struct {
    78  	// prefix of the invironment, `OS_` in most cases
    79  	prefix string
    80  	// cloud containins all information about used cloud
    81  	cloud *Cloud
    82  	// unstable make Env ignore lazy cloud loading and
    83  	// refresh it every time it's requested
    84  	unstable bool
    85  }
    86  
    87  // NewEnv create new <prefixed> Env loader, lazy by default
    88  func NewEnv(prefix string, lazy ...bool) *Env {
    89  	if prefix != "" && !strings.HasSuffix(prefix, "_") {
    90  		prefix += "_"
    91  	}
    92  	unstable := false
    93  	if len(lazy) > 0 {
    94  		unstable = !lazy[0]
    95  	}
    96  	return &Env{prefix: prefix, unstable: unstable}
    97  }
    98  
    99  func (e *Env) Prefix() string {
   100  	return e.prefix
   101  }
   102  
   103  func (e *Env) cloudFromEnv() *Cloud {
   104  	authOpts, _ := AuthOptionsFromEnv(e)
   105  	verify := true
   106  	if v := e.GetEnv("INSECURE"); v != "" {
   107  		verify = v != "1" && v != "true"
   108  	}
   109  	aws := NewEnv("AWS_")
   110  	access := aws.GetEnv("ACCESS_KEY_ID")
   111  	if access == "" {
   112  		access = e.GetEnv("ACCESS_KEY", "ACCESS_KEY_ID", "AK")
   113  	}
   114  	secret := aws.GetEnv("ACCESS_SECRET_KEY")
   115  	if secret == "" {
   116  		secret = e.GetEnv("SECRET_KEY", "ACCESS_KEY_SECRET", "SK")
   117  	}
   118  	security := aws.GetEnv("SECURITY_TOKEN")
   119  	if security == "" {
   120  		security = e.GetEnv("SECURITY_TOKEN", "AKSK_SECURITY_TOKEN", "ST")
   121  	}
   122  	region := e.GetEnv("REGION_NAME", "REGION_ID")
   123  	if region == "" {
   124  		region = utils.GetRegion(authOpts)
   125  	}
   126  
   127  	cloud := &Cloud{
   128  		Cloud:   e.GetEnv("CLOUD"),
   129  		Profile: e.GetEnv("PROFILE"),
   130  		AuthInfo: AuthInfo{
   131  			AuthURL:           authOpts.IdentityEndpoint,
   132  			Token:             authOpts.TokenID,
   133  			Username:          authOpts.Username,
   134  			UserID:            authOpts.UserID,
   135  			Password:          authOpts.Password,
   136  			Passcode:          authOpts.Passcode,
   137  			ProjectName:       authOpts.TenantName,
   138  			ProjectID:         authOpts.TenantID,
   139  			UserDomainName:    e.GetEnv("USER_DOMAIN_NAME"),
   140  			UserDomainID:      e.GetEnv("USER_DOMAIN_ID"),
   141  			ProjectDomainName: e.GetEnv("PROJECT_DOMAIN_NAME"),
   142  			ProjectDomainID:   e.GetEnv("PROJECT_DOMAIN_ID"),
   143  			DomainName:        authOpts.DomainName,
   144  			DomainID:          authOpts.DomainID,
   145  			DefaultDomain:     e.GetEnv("DEFAULT_DOMAIN"),
   146  			AccessKey:         access,
   147  			SecretKey:         secret,
   148  			SecurityToken:     security,
   149  			AgencyName:        authOpts.AgencyName,
   150  			AgencyDomainName:  authOpts.AgencyDomainName,
   151  			DelegatedProject:  authOpts.DelegatedProject,
   152  		},
   153  		AuthType:           AuthType(e.GetEnv("AUTH_TYPE")),
   154  		RegionName:         region,
   155  		EndpointType:       e.GetEnv("ENDPOINT_TYPE"),
   156  		Interface:          e.GetEnv("INTERFACE"),
   157  		IdentityAPIVersion: e.GetEnv("IDENTITY_API_VERSION"),
   158  		VolumeAPIVersion:   e.GetEnv("VOLUME_API_VERSION"),
   159  		Verify:             &verify,
   160  		CACertFile:         e.GetEnv("CA_CERT", "CA_CERT_FILE"),
   161  		ClientCertFile:     e.GetEnv("CLIENT_CERT", "CLIENT_CERT_FILE"),
   162  		ClientKeyFile:      e.GetEnv("CLIENT_KEY", "CLIENT_KEY_FILE"),
   163  	}
   164  	return cloud
   165  }
   166  
   167  // GetEnv returns first non-empty value of given environment variables
   168  func (e *Env) GetEnv(keys ...string) string {
   169  	for _, key := range keys {
   170  		if value := os.Getenv(e.prefix + key); value != "" {
   171  			return value
   172  		}
   173  	}
   174  	return ""
   175  }
   176  
   177  // VendorConfig represents a collection of PublicCloud entries in clouds-public.yaml file.
   178  // The format of the clouds-public.yml is documented at
   179  // https://docs.openstack.org/python-openstackclient/latest/configuration/
   180  type VendorConfig struct {
   181  	Clouds map[string]Cloud `yaml:"public-clouds" json:"public-clouds"`
   182  }
   183  
   184  // Config represents a collection of Cloud entries in a clouds.yaml file.
   185  // The format of clouds.yaml is documented at
   186  // https://docs.openstack.org/os-client-config/latest/user/configuration.html.
   187  type Config struct {
   188  	DefaultCloud string           `yaml:"-" json:"-"`
   189  	Clouds       map[string]Cloud `yaml:"clouds" json:"clouds"`
   190  }
   191  
   192  func NewConfig() *Config {
   193  	return &Config{
   194  		Clouds: map[string]Cloud{},
   195  	}
   196  }
   197  
   198  // AuthType represents a valid method of authentication: `password`, `token`, `aksk` or `agency`
   199  type AuthType string
   200  
   201  // AuthInfo represents the auth section of a cloud entry
   202  type AuthInfo struct {
   203  	// AuthURL is the keystone/identity endpoint URL.
   204  	AuthURL string `yaml:"auth_url,omitempty" json:"auth_url,omitempty"`
   205  
   206  	// Token is a pre-generated authentication token.
   207  	Token string `yaml:"token,omitempty" json:"token,omitempty"`
   208  
   209  	// Username is the username of the user.
   210  	Username string `yaml:"username,omitempty" json:"username,omitempty"`
   211  
   212  	// UserID is the unique ID of a user.
   213  	UserID string `yaml:"user_id,omitempty" json:"user_id,omitempty"`
   214  
   215  	// Password is the password of the user.
   216  	Password string `yaml:"password,omitempty" json:"password,omitempty"`
   217  
   218  	// Passcode for MFA.
   219  	Passcode string `yaml:"-" json:"-"`
   220  
   221  	// ProjectName is the common/human-readable name of a project.
   222  	// Users can be scoped to a project.
   223  	// ProjectName on its own is not enough to ensure a unique scope. It must
   224  	// also be combined with either a ProjectDomainName or ProjectDomainID.
   225  	// ProjectName cannot be combined with ProjectID in a scope.
   226  	ProjectName string `yaml:"project_name,omitempty" json:"project_name,omitempty"`
   227  
   228  	// ProjectID is the unique ID of a project.
   229  	// It can be used to scope a user to a specific project.
   230  	ProjectID string `yaml:"project_id,omitempty" json:"project_id,omitempty"`
   231  
   232  	// UserDomainName is the name of the domain where a user resides.
   233  	// It is used to identify the source domain of a user.
   234  	UserDomainName string `yaml:"user_domain_name,omitempty" json:"user_domain_name,omitempty"`
   235  
   236  	// UserDomainID is the unique ID of the domain where a user resides.
   237  	// It is used to identify the source domain of a user.
   238  	UserDomainID string `yaml:"user_domain_id,omitempty" json:"user_domain_id,omitempty"`
   239  
   240  	// ProjectDomainName is the name of the domain where a project resides.
   241  	// It is used to identify the source domain of a project.
   242  	// ProjectDomainName can be used in addition to a ProjectName when scoping
   243  	// a user to a specific project.
   244  	ProjectDomainName string `yaml:"project_domain_name,omitempty" json:"project_domain_name,omitempty"`
   245  
   246  	// ProjectDomainID is the name of the domain where a project resides.
   247  	// It is used to identify the source domain of a project.
   248  	// ProjectDomainID can be used in addition to a ProjectName when scoping
   249  	// a user to a specific project.
   250  	ProjectDomainID string `yaml:"project_domain_id,omitempty" json:"project_domain_id,omitempty"`
   251  
   252  	// DomainName is the name of a domain which can be used to identify the
   253  	// source domain of either a user or a project.
   254  	// If UserDomainName and ProjectDomainName are not specified, then DomainName
   255  	// is used as a default choice.
   256  	// It can also be used be used to specify a domain-only scope.
   257  	DomainName string `yaml:"domain_name,omitempty" json:"domain_name,omitempty"`
   258  
   259  	// DomainID is the unique ID of a domain which can be used to identify the
   260  	// source domain of either a user or a project.
   261  	// If UserDomainID and ProjectDomainID are not specified, then DomainID is
   262  	// used as a default choice.
   263  	// It can also be used be used to specify a domain-only scope.
   264  	DomainID string `yaml:"domain_id,omitempty" json:"domain_id,omitempty"`
   265  
   266  	// DefaultDomain is the domain ID to fall back on if no other domain has
   267  	// been specified and a domain is required for scope.
   268  	DefaultDomain string `yaml:"default_domain,omitempty" json:"default_domain,omitempty"`
   269  
   270  	// AK/SK auth means
   271  	AccessKey     string `yaml:"ak,omitempty" json:"ak,omitempty"`
   272  	SecretKey     string `yaml:"sk,omitempty" json:"sk,omitempty"`
   273  	SecurityToken string `yaml:"security_token,omitempty" json:"security_token,omitempty"`
   274  
   275  	// OTC Agency config
   276  	AgencyName string `yaml:"target_agency_name,omitempty" json:"agency_name,omitempty"`
   277  	// AgencyDomainName is the name of domain who created the agency
   278  	AgencyDomainName string `yaml:"target_domain_id,omitempty" json:"target_domain_id,omitempty"`
   279  	// DelegatedProject is the name of delegated project
   280  	DelegatedProject string `yaml:"target_project_name,omitempty" json:"target_project_name,omitempty"`
   281  }
   282  
   283  // Cloud represents an entry in a clouds.yaml/public-clouds.yaml/secure.yaml file.
   284  type Cloud struct {
   285  	Cloud      string   `yaml:"cloud,omitempty" json:"cloud,omitempty"`
   286  	Profile    string   `yaml:"profile,omitempty" json:"profile,omitempty"`
   287  	AuthType   AuthType `yaml:"auth_type,omitempty" json:"auth_type,omitempty"`
   288  	AuthInfo   AuthInfo `yaml:"auth,omitempty" json:"auth,omitempty"`
   289  	RegionName string   `yaml:"region_name,omitempty" json:"region_name,omitempty"`
   290  	Regions    []string `yaml:"regions,omitempty" json:"regions,omitempty"`
   291  
   292  	// EndpointType and Interface both specify whether to use the public, internal,
   293  	// or admin interface of a service. They should be considered synonymous, but
   294  	// EndpointType will take precedence when both are specified.
   295  	EndpointType string `yaml:"endpoint_type,omitempty" json:"endpoint_type,omitempty"`
   296  	Interface    string `yaml:"interface,omitempty" json:"interface,omitempty"`
   297  
   298  	// API Version overrides.
   299  	IdentityAPIVersion string `yaml:"identity_api_version,omitempty" json:"identity_api_version,omitempty"`
   300  	VolumeAPIVersion   string `yaml:"volume_api_version,omitempty" json:"volume_api_version,omitempty"`
   301  
   302  	// Verify whether or not SSL API requests should be verified.
   303  	Verify *bool `yaml:"verify,omitempty" json:"verify,omitempty"`
   304  
   305  	// CACertFile a path to a CA Cert bundle that can be used as part of
   306  	// verifying SSL API requests.
   307  	CACertFile string `yaml:"cacert,omitempty" json:"cacert,omitempty"`
   308  
   309  	// ClientCertFile a path to a client certificate to use as part of the SSL
   310  	// transaction.
   311  	ClientCertFile string `yaml:"cert,omitempty" json:"cert,omitempty"`
   312  
   313  	// ClientKeyFile a path to a client key to use as part of the SSL
   314  	// transaction.
   315  	ClientKeyFile string `yaml:"key,omitempty" json:"key,omitempty"`
   316  }
   317  
   318  func (c *Cloud) computeRegion() {
   319  	if c.RegionName != "" {
   320  		return
   321  	}
   322  	name := c.AuthInfo.ProjectName
   323  	if name == "" {
   324  		name = c.AuthInfo.DelegatedProject
   325  	}
   326  	c.RegionName = strings.Split(name, "_")[0]
   327  }
   328  
   329  func (c *Cloud) computeAuthURL() error {
   330  	// Auth URL depends on provided region
   331  	if url := c.AuthInfo.AuthURL; strings.Contains(url, regionPlaceHolder) {
   332  		if c.RegionName == "" {
   333  			return fmt.Errorf("region placeholder found in `AuthURL` (%s), but no region provided", url)
   334  		}
   335  		c.AuthInfo.AuthURL = strings.ReplaceAll(url, regionPlaceHolder, c.RegionName)
   336  	}
   337  	return nil
   338  }
   339  
   340  func loadFile(path string) ([]byte, error) {
   341  	file, err := os.Open(path)
   342  	if err != nil {
   343  		return nil, err
   344  	}
   345  	defer func() { _ = file.Close() }()
   346  	data, err := ioutil.ReadAll(file)
   347  	if err != nil {
   348  		return nil, err
   349  	}
   350  	return data, nil
   351  }
   352  
   353  func loadCloudFile(path string) (*Config, error) {
   354  	data, err := loadFile(path)
   355  	if err != nil {
   356  		return nil, err
   357  	}
   358  	clouds := NewConfig()
   359  	if err := yaml.Unmarshal(data, clouds); err != nil {
   360  		return nil, err
   361  	}
   362  	return clouds, err
   363  }
   364  
   365  func loadVendorFile(path string) (*VendorConfig, error) {
   366  	data, err := loadFile(path)
   367  	if err != nil {
   368  		return nil, err
   369  	}
   370  	clouds := new(VendorConfig)
   371  	if err := yaml.Unmarshal(data, clouds); err != nil {
   372  		return nil, err
   373  	}
   374  	return clouds, err
   375  }
   376  
   377  func mergeWithVendor(config *Config, vendor *VendorConfig) (*Config, error) {
   378  	for k, cloud := range config.Clouds {
   379  		profile := cloud.Profile
   380  		if profile == "" {
   381  			profile = cloud.Cloud
   382  		}
   383  		if profile == "" {
   384  			continue
   385  		}
   386  		if v, ok := vendor.Clouds[profile]; ok {
   387  			merged, err := mergeClouds(&cloud, &v)
   388  			if err != nil {
   389  				log.Printf("error during merge with vendor file: %s", err)
   390  				return config, err
   391  			}
   392  			config.Clouds[k] = *merged
   393  		}
   394  	}
   395  	return config, nil
   396  }
   397  
   398  func mergeCloudConfigs(config, fallback *Config) (*Config, error) {
   399  	resultClouds := &Config{
   400  		Clouds: map[string]Cloud{},
   401  	}
   402  	for profile, cfg := range config.Clouds {
   403  		if fallback, ok := fallback.Clouds[profile]; ok {
   404  			cld, err := mergeClouds(cfg, fallback)
   405  			if err != nil {
   406  				return nil, err
   407  			}
   408  			resultClouds.Clouds[profile] = *cld
   409  		} else {
   410  			resultClouds.Clouds[profile] = cfg
   411  		}
   412  	}
   413  	return resultClouds, nil
   414  }
   415  
   416  func selectExisting(files []string) string {
   417  	for _, file := range files {
   418  		if _, err := os.Stat(file); err == nil {
   419  			return file
   420  		}
   421  	}
   422  	return ""
   423  }
   424  
   425  // mergeClouds merges two Config recursively (the AuthInfo also gets merged).
   426  // In case both Config define a value, the value in the 'cloud' cloud takes precedence
   427  func mergeClouds(cloud, fallback interface{}) (*Cloud, error) {
   428  	overrideJson, err := json.Marshal(fallback)
   429  	if err != nil {
   430  		return nil, err
   431  	}
   432  	cloudJson, err := json.Marshal(cloud)
   433  	if err != nil {
   434  		return nil, err
   435  	}
   436  	var fallbackInterface interface{}
   437  	err = json.Unmarshal(overrideJson, &fallbackInterface)
   438  	if err != nil {
   439  		return nil, err
   440  	}
   441  	var cloudInterface interface{}
   442  	err = json.Unmarshal(cloudJson, &cloudInterface)
   443  	if err != nil {
   444  		return nil, err
   445  	}
   446  	var mergedCloud Cloud
   447  	mergedInterface := utils.MergeInterfaces(cloudInterface, fallbackInterface)
   448  	mergedJson, err := json.Marshal(mergedInterface)
   449  	if err != nil {
   450  		return nil, err
   451  	}
   452  	if err := json.Unmarshal(mergedJson, &mergedCloud); err != nil {
   453  		return nil, err
   454  	}
   455  	return &mergedCloud, nil
   456  }
   457  
   458  func mergeWithSecure(cloudConfig *Config, securePath string) *Config {
   459  	s, err := loadCloudFile(securePath)
   460  	if err != nil {
   461  		log.Printf("Failed to load %s as secure config", securePath)
   462  		return cloudConfig
   463  	}
   464  	cc, err := mergeCloudConfigs(cloudConfig, s)
   465  	if err != nil {
   466  		log.Printf("Failed to merge %s into cloud config", securePath)
   467  		return cloudConfig
   468  	}
   469  	return cc
   470  }
   471  
   472  func mergeWithVendors(cloudConfig *Config, vendorPath string) *Config {
   473  	v, err := loadVendorFile(vendorPath)
   474  	if err != nil {
   475  		log.Printf("Failed to load %s as vendor config", vendorPath)
   476  		return cloudConfig
   477  	}
   478  	cc, err := mergeWithVendor(cloudConfig, v)
   479  	if err != nil {
   480  		log.Printf("Failed to merge %s into vendor config", vendorPath)
   481  		return cloudConfig
   482  	}
   483  	return cc
   484  }
   485  
   486  // Cloud get cloud merged from configuration and env variables
   487  // if `cloudName` is not empty, explicit cloud name will be used instead
   488  // defined in `OS_CLOUD` environment variable
   489  func (e *Env) Cloud(name ...string) (*Cloud, error) {
   490  	cloudName := ""
   491  	if len(name) > 0 {
   492  		cloudName = name[0]
   493  		e.unstable = true // previously loaded cloud can be different
   494  	}
   495  	if e.cloud == nil || e.unstable {
   496  		config, err := e.loadOpenstackConfig()
   497  		if err != nil {
   498  			return nil, fmt.Errorf("failed to load clouds configuration: %s", err)
   499  		}
   500  		if cloudName == "" {
   501  			cloudName = config.DefaultCloud
   502  		}
   503  		cloud, err := mergeClouds(
   504  			config.Clouds[cloudName],
   505  			e.cloudFromEnv(),
   506  		)
   507  		if err != nil {
   508  			return nil, fmt.Errorf("failed to merge cloud %s with Env vars: %s", config.DefaultCloud, err)
   509  		}
   510  		cloud.Cloud = cloudName // override value read from environment
   511  		cloud.computeRegion()
   512  		if err := cloud.computeAuthURL(); err != nil {
   513  			return nil, err
   514  		}
   515  		e.cloud = cloud
   516  	}
   517  	return e.cloud, nil
   518  
   519  }
   520  
   521  // LoadCloudConfig utilize all existing cloud configurations to create cloud configuration:
   522  // env variables, clouds.yaml, secure.yaml, clouds-public.yaml
   523  func (e *Env) loadOpenstackConfig() (*Config, error) {
   524  	var (
   525  		configs = make([]string, len(configFiles))
   526  		secure  = make([]string, len(secureFiles))
   527  		vendors = make([]string, len(vendorFiles))
   528  	)
   529  	copy(configs, configFiles)
   530  	copy(secure, secureFiles)
   531  	copy(vendors, vendorFiles)
   532  
   533  	// find config files
   534  	if c := e.GetEnv("CLIENT_CONFIG_FILE"); c != "" {
   535  		configs = utils.PrependString(c, configs)
   536  	}
   537  	configPath := selectExisting(configs)
   538  
   539  	if s := e.GetEnv("CLIENT_SECURE_FILE"); s != "" {
   540  		secure = utils.PrependString(s, secure)
   541  	}
   542  	securePath := selectExisting(secure)
   543  
   544  	if v := e.GetEnv("CLIENT_VENDOR_FILE"); v != "" {
   545  		vendors = utils.PrependString(v, vendors)
   546  	}
   547  	vendorPath := selectExisting(vendors)
   548  
   549  	cloudConfig := NewConfig()
   550  
   551  	// load clouds.yaml
   552  	if configPath != "" {
   553  		c, err := loadCloudFile(configPath)
   554  		if err != nil {
   555  			log.Printf("Failed to load %s as cloud config", securePath)
   556  		}
   557  		if c.Clouds != nil {
   558  			cloudConfig = c
   559  		}
   560  	}
   561  
   562  	// merge with secure.yaml
   563  	if securePath != "" {
   564  		cloudConfig = mergeWithSecure(cloudConfig, securePath)
   565  	}
   566  
   567  	// append cloud from envvars
   568  	envVarKey := e.GetEnv("CLOUD_NAME")
   569  	if envVarKey == "" {
   570  		envVarKey = defaultEnvVarKey
   571  	}
   572  	if _, ok := cloudConfig.Clouds[envVarKey]; ok {
   573  		return nil, fmt.Errorf("%sCLOUD_NAME=`%s` duplicates cloud defined in file", e.prefix, envVarKey)
   574  	}
   575  	cloudConfig.Clouds[envVarKey] = *NewEnv(envVarKey).cloudFromEnv()
   576  
   577  	cloudName := e.GetEnv("CLOUD")
   578  	if cloudName == "" && len(cloudConfig.Clouds) == 1 {
   579  		for k := range cloudConfig.Clouds {
   580  			cloudName = k
   581  		}
   582  	}
   583  	cloudConfig.DefaultCloud = cloudName
   584  
   585  	// merge with clouds-public.yaml
   586  	var err error
   587  	if vendorPath != "" {
   588  		cloudConfig = mergeWithVendors(cloudConfig, vendorPath)
   589  	} else {
   590  		cloudConfig, err = mergeWithVendor(cloudConfig, OTCVendorConfig)
   591  	}
   592  	return cloudConfig, err
   593  }
   594  
   595  func getAuthType(val AuthType) AuthType {
   596  	explicitTypes := []string{"token", "password", "aksk"}
   597  	for _, opt := range explicitTypes {
   598  		if strings.Contains(string(val), opt) {
   599  			return AuthType(opt)
   600  		}
   601  	}
   602  	return val
   603  }
   604  
   605  // AuthOptionsFromInfo builds auth options from auth info and type. Returns either AuthOptions or AKSKAuthOptions
   606  func AuthOptionsFromInfo(authInfo *AuthInfo, authType AuthType) (golangsdk.AuthOptionsProvider, error) {
   607  	// project scope
   608  	if authInfo.ProjectID != "" || authInfo.ProjectName != "" {
   609  		if authInfo.ProjectDomainName != "" {
   610  			authInfo.DomainName = authInfo.ProjectDomainName
   611  		}
   612  		if authInfo.ProjectDomainID != "" {
   613  			authInfo.ProjectID = authInfo.ProjectDomainID
   614  		}
   615  	}
   616  	// user scope
   617  	if authInfo.Username != "" || authInfo.UserID != "" {
   618  		if authInfo.UserDomainName != "" {
   619  			authInfo.DomainName = authInfo.UserDomainName
   620  		}
   621  		if authInfo.UserDomainID != "" {
   622  			authInfo.ProjectID = authInfo.UserDomainID
   623  		}
   624  	}
   625  
   626  	ao := golangsdk.AuthOptions{
   627  		IdentityEndpoint: authInfo.AuthURL,
   628  		TokenID:          authInfo.Token,
   629  		Username:         authInfo.Username,
   630  		UserID:           authInfo.UserID,
   631  		Password:         authInfo.Password,
   632  		DomainID:         authInfo.DomainID,
   633  		DomainName:       authInfo.DomainName,
   634  		TenantID:         authInfo.ProjectID,
   635  		TenantName:       authInfo.ProjectName,
   636  		Passcode:         authInfo.Passcode,
   637  	}
   638  
   639  	explicitAuthType := getAuthType(authType)
   640  
   641  	// If an auth_type of "token" was specified, then make sure
   642  	// Gophercloud properly authenticates with a token. This involves
   643  	// unsetting a few other auth options. The reason this is done
   644  	// here is to wait until all auth settings (both in clouds.yaml
   645  	// and via environment variables) are set and then unset them.
   646  	if explicitAuthType == "token" || explicitAuthType == "aksk" {
   647  		ao.Username = ""
   648  		ao.Password = ""
   649  		ao.UserID = ""
   650  		ao.DomainID = ""
   651  		ao.DomainName = ""
   652  	}
   653  
   654  	// Check for absolute minimum requirements.
   655  	if ao.IdentityEndpoint == "" {
   656  		err := golangsdk.ErrMissingInput{Argument: "auth_url"}
   657  		return nil, err
   658  	}
   659  	if explicitAuthType == "aksk" || (explicitAuthType == "" && authInfo.AccessKey != "") {
   660  		return golangsdk.AKSKAuthOptions{
   661  			IdentityEndpoint: ao.IdentityEndpoint,
   662  			ProjectId:        ao.TenantID,
   663  			ProjectName:      ao.TenantName,
   664  			Domain:           ao.DomainName,
   665  			DomainID:         ao.DomainID,
   666  			AccessKey:        authInfo.AccessKey,
   667  			SecretKey:        authInfo.SecretKey,
   668  			AgencyName:       ao.AgencyName,
   669  			AgencyDomainName: ao.AgencyDomainName,
   670  			DelegatedProject: ao.DelegatedProject,
   671  		}, nil
   672  	}
   673  	return ao, nil
   674  }
   675  
   676  // AuthenticatedClient create new client based on used Env prefix
   677  // this uses LoadOpenstackConfig inside
   678  func (e *Env) AuthenticatedClient(cloudName ...string) (*golangsdk.ProviderClient, error) {
   679  	cloud, err := e.Cloud(cloudName...)
   680  	if err != nil {
   681  		return nil, err
   682  	}
   683  	return AuthenticatedClientFromCloud(cloud)
   684  }
   685  
   686  // AuthenticatedClientFromCloud create new authenticated client for given cloud config
   687  func AuthenticatedClientFromCloud(cloud *Cloud) (*golangsdk.ProviderClient, error) {
   688  	opts, err := AuthOptionsFromInfo(&cloud.AuthInfo, cloud.AuthType)
   689  	if err != nil {
   690  		return nil, fmt.Errorf("failed to convert AuthInfo to AuthOptsBuilder with Env vars: %s", err)
   691  	}
   692  	client, err := AuthenticatedClient(opts)
   693  	if err != nil {
   694  		return nil, fmt.Errorf("failed to authenticate client: %s", err)
   695  	}
   696  	return client, nil
   697  }