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 }