916 lines
26 KiB
Go
916 lines
26 KiB
Go
package clientconfig
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"reflect"
|
|
"strings"
|
|
|
|
"github.com/gophercloud/gophercloud"
|
|
"github.com/gophercloud/gophercloud/openstack"
|
|
"github.com/gophercloud/utils/env"
|
|
|
|
yaml "gopkg.in/yaml.v2"
|
|
)
|
|
|
|
// AuthType respresents a valid method of authentication.
|
|
type AuthType string
|
|
|
|
const (
|
|
// AuthPassword defines an unknown version of the password
|
|
AuthPassword AuthType = "password"
|
|
// AuthToken defined an unknown version of the token
|
|
AuthToken AuthType = "token"
|
|
|
|
// AuthV2Password defines version 2 of the password
|
|
AuthV2Password AuthType = "v2password"
|
|
// AuthV2Token defines version 2 of the token
|
|
AuthV2Token AuthType = "v2token"
|
|
|
|
// AuthV3Password defines version 3 of the password
|
|
AuthV3Password AuthType = "v3password"
|
|
// AuthV3Token defines version 3 of the token
|
|
AuthV3Token AuthType = "v3token"
|
|
|
|
// AuthV3ApplicationCredential defines version 3 of the application credential
|
|
AuthV3ApplicationCredential AuthType = "v3applicationcredential"
|
|
)
|
|
|
|
// ClientOpts represents options to customize the way a client is
|
|
// configured.
|
|
type ClientOpts struct {
|
|
// Cloud is the cloud entry in clouds.yaml to use.
|
|
Cloud string
|
|
|
|
// EnvPrefix allows a custom environment variable prefix to be used.
|
|
EnvPrefix string
|
|
|
|
// AuthType specifies the type of authentication to use.
|
|
// By default, this is "password".
|
|
AuthType AuthType
|
|
|
|
// AuthInfo defines the authentication information needed to
|
|
// authenticate to a cloud when clouds.yaml isn't used.
|
|
AuthInfo *AuthInfo
|
|
|
|
// RegionName is the region to create a Service Client in.
|
|
// This will override a region in clouds.yaml or can be used
|
|
// when authenticating directly with AuthInfo.
|
|
RegionName string
|
|
|
|
// EndpointType specifies whether to use the public, internal, or
|
|
// admin endpoint of a service.
|
|
EndpointType string
|
|
|
|
// HTTPClient provides the ability customize the ProviderClient's
|
|
// internal HTTP client.
|
|
HTTPClient *http.Client
|
|
|
|
// YAMLOpts provides the ability to pass a customized set
|
|
// of options and methods for loading the YAML file.
|
|
// It takes a YAMLOptsBuilder interface that is defined
|
|
// in this file. This is optional and the default behavior
|
|
// is to call the local LoadCloudsYAML functions defined
|
|
// in this file.
|
|
YAMLOpts YAMLOptsBuilder
|
|
}
|
|
|
|
// YAMLOptsBuilder defines an interface for customization when
|
|
// loading a clouds.yaml file.
|
|
type YAMLOptsBuilder interface {
|
|
LoadCloudsYAML() (map[string]Cloud, error)
|
|
LoadSecureCloudsYAML() (map[string]Cloud, error)
|
|
LoadPublicCloudsYAML() (map[string]Cloud, error)
|
|
}
|
|
|
|
// YAMLOpts represents options and methods to load a clouds.yaml file.
|
|
type YAMLOpts struct {
|
|
// By default, no options are specified.
|
|
}
|
|
|
|
// LoadCloudsYAML defines how to load a clouds.yaml file.
|
|
// By default, this calls the local LoadCloudsYAML function.
|
|
func (opts YAMLOpts) LoadCloudsYAML() (map[string]Cloud, error) {
|
|
return LoadCloudsYAML()
|
|
}
|
|
|
|
// LoadSecureCloudsYAML defines how to load a secure.yaml file.
|
|
// By default, this calls the local LoadSecureCloudsYAML function.
|
|
func (opts YAMLOpts) LoadSecureCloudsYAML() (map[string]Cloud, error) {
|
|
return LoadSecureCloudsYAML()
|
|
}
|
|
|
|
// LoadPublicCloudsYAML defines how to load a public-secure.yaml file.
|
|
// By default, this calls the local LoadPublicCloudsYAML function.
|
|
func (opts YAMLOpts) LoadPublicCloudsYAML() (map[string]Cloud, error) {
|
|
return LoadPublicCloudsYAML()
|
|
}
|
|
|
|
// LoadCloudsYAML will load a clouds.yaml file and return the full config.
|
|
// This is called by the YAMLOpts method. Calling this function directly
|
|
// is supported for now but has only been retained for backwards
|
|
// compatibility from before YAMLOpts was defined. This may be removed in
|
|
// the future.
|
|
func LoadCloudsYAML() (map[string]Cloud, error) {
|
|
_, content, err := FindAndReadCloudsYAML()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var clouds Clouds
|
|
err = yaml.Unmarshal(content, &clouds)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal yaml: %v", err)
|
|
}
|
|
|
|
return clouds.Clouds, nil
|
|
}
|
|
|
|
// LoadSecureCloudsYAML will load a secure.yaml file and return the full config.
|
|
// This is called by the YAMLOpts method. Calling this function directly
|
|
// is supported for now but has only been retained for backwards
|
|
// compatibility from before YAMLOpts was defined. This may be removed in
|
|
// the future.
|
|
func LoadSecureCloudsYAML() (map[string]Cloud, error) {
|
|
var secureClouds Clouds
|
|
|
|
_, content, err := FindAndReadSecureCloudsYAML()
|
|
if err != nil {
|
|
if err.Error() == "no secure.yaml file found" {
|
|
// secure.yaml is optional so just ignore read error
|
|
return secureClouds.Clouds, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
err = yaml.Unmarshal(content, &secureClouds)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal yaml: %v", err)
|
|
}
|
|
|
|
return secureClouds.Clouds, nil
|
|
}
|
|
|
|
// LoadPublicCloudsYAML will load a public-clouds.yaml file and return the full config.
|
|
// This is called by the YAMLOpts method. Calling this function directly
|
|
// is supported for now but has only been retained for backwards
|
|
// compatibility from before YAMLOpts was defined. This may be removed in
|
|
// the future.
|
|
func LoadPublicCloudsYAML() (map[string]Cloud, error) {
|
|
var publicClouds PublicClouds
|
|
|
|
_, content, err := FindAndReadPublicCloudsYAML()
|
|
if err != nil {
|
|
if err.Error() == "no clouds-public.yaml file found" {
|
|
// clouds-public.yaml is optional so just ignore read error
|
|
return publicClouds.Clouds, nil
|
|
}
|
|
|
|
return nil, err
|
|
}
|
|
|
|
err = yaml.Unmarshal(content, &publicClouds)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal yaml: %v", err)
|
|
}
|
|
|
|
return publicClouds.Clouds, nil
|
|
}
|
|
|
|
// GetCloudFromYAML will return a cloud entry from a clouds.yaml file.
|
|
func GetCloudFromYAML(opts *ClientOpts) (*Cloud, error) {
|
|
if opts.YAMLOpts == nil {
|
|
opts.YAMLOpts = new(YAMLOpts)
|
|
}
|
|
|
|
yamlOpts := opts.YAMLOpts
|
|
|
|
clouds, err := yamlOpts.LoadCloudsYAML()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to load clouds.yaml: %s", err)
|
|
}
|
|
|
|
// Determine which cloud to use.
|
|
// First see if a cloud name was explicitly set in opts.
|
|
var cloudName string
|
|
if opts != nil && opts.Cloud != "" {
|
|
cloudName = opts.Cloud
|
|
}
|
|
|
|
// Next see if a cloud name was specified as an environment variable.
|
|
// This is supposed to override an explicit opts setting.
|
|
envPrefix := "OS_"
|
|
if opts.EnvPrefix != "" {
|
|
envPrefix = opts.EnvPrefix
|
|
}
|
|
|
|
if v := env.Getenv(envPrefix + "CLOUD"); v != "" {
|
|
cloudName = v
|
|
}
|
|
|
|
var cloud *Cloud
|
|
if cloudName != "" {
|
|
v, ok := clouds[cloudName]
|
|
if !ok {
|
|
return nil, fmt.Errorf("cloud %s does not exist in clouds.yaml", cloudName)
|
|
}
|
|
cloud = &v
|
|
}
|
|
|
|
// If a cloud was not specified, and clouds only contains
|
|
// a single entry, use that entry.
|
|
if cloudName == "" && len(clouds) == 1 {
|
|
for _, v := range clouds {
|
|
cloud = &v
|
|
}
|
|
}
|
|
|
|
if cloud != nil {
|
|
// A profile points to a public cloud entry.
|
|
// If one was specified, load a list of public clouds
|
|
// and then merge the information with the current cloud data.
|
|
profileName := defaultIfEmpty(cloud.Profile, cloud.Cloud)
|
|
|
|
if profileName != "" {
|
|
publicClouds, err := yamlOpts.LoadPublicCloudsYAML()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to load clouds-public.yaml: %s", err)
|
|
}
|
|
|
|
publicCloud, ok := publicClouds[profileName]
|
|
if !ok {
|
|
return nil, fmt.Errorf("cloud %s does not exist in clouds-public.yaml", profileName)
|
|
}
|
|
|
|
cloud, err = mergeClouds(cloud, publicCloud)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Could not merge information from clouds.yaml and clouds-public.yaml for cloud %s", profileName)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Next, load a secure clouds file and see if a cloud entry
|
|
// can be found or merged.
|
|
secureClouds, err := yamlOpts.LoadSecureCloudsYAML()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to load secure.yaml: %s", err)
|
|
}
|
|
|
|
if secureClouds != nil {
|
|
// If no entry was found in clouds.yaml, no cloud name was specified,
|
|
// and only one secureCloud entry exists, use that as the cloud entry.
|
|
if cloud == nil && cloudName == "" && len(secureClouds) == 1 {
|
|
for _, v := range secureClouds {
|
|
cloud = &v
|
|
}
|
|
}
|
|
|
|
// Otherwise, see if the provided cloud name exists in the secure yaml file.
|
|
secureCloud, ok := secureClouds[cloudName]
|
|
if !ok && cloud == nil {
|
|
// cloud == nil serves two purposes here:
|
|
// if no entry in clouds.yaml was found and
|
|
// if a single-entry secureCloud wasn't used.
|
|
// At this point, no entry could be determined at all.
|
|
return nil, fmt.Errorf("Could not find cloud %s", cloudName)
|
|
}
|
|
|
|
// If secureCloud has content and it differs from the cloud entry,
|
|
// merge the two together.
|
|
if !reflect.DeepEqual((Cloud{}), secureCloud) && !reflect.DeepEqual(cloud, secureCloud) {
|
|
cloud, err = mergeClouds(secureCloud, cloud)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to merge information from clouds.yaml and secure.yaml")
|
|
}
|
|
}
|
|
}
|
|
|
|
// As an extra precaution, do one final check to see if cloud is nil.
|
|
// We shouldn't reach this point, though.
|
|
if cloud == nil {
|
|
return nil, fmt.Errorf("Could not find cloud %s", cloudName)
|
|
}
|
|
|
|
// Default is to verify SSL API requests
|
|
if cloud.Verify == nil {
|
|
iTrue := true
|
|
cloud.Verify = &iTrue
|
|
}
|
|
|
|
// TODO: this is where reading vendor files should go be considered when not found in
|
|
// clouds-public.yml
|
|
// https://github.com/openstack/openstacksdk/tree/master/openstack/config/vendors
|
|
|
|
// Both Interface and EndpointType are valid settings in clouds.yaml,
|
|
// but we want to standardize on EndpointType for simplicity.
|
|
//
|
|
// If only Interface was set, we copy that to EndpointType to use as the setting.
|
|
// But in all other cases, EndpointType is used and Interface is cleared.
|
|
if cloud.Interface != "" && cloud.EndpointType == "" {
|
|
cloud.EndpointType = cloud.Interface
|
|
}
|
|
|
|
cloud.Interface = ""
|
|
|
|
return cloud, nil
|
|
}
|
|
|
|
// AuthOptions creates a gophercloud.AuthOptions structure with the
|
|
// settings found in a specific cloud entry of a clouds.yaml file or
|
|
// based on authentication settings given in ClientOpts.
|
|
//
|
|
// This attempts to be a single point of entry for all OpenStack authentication.
|
|
//
|
|
// See http://docs.openstack.org/developer/os-client-config and
|
|
// https://github.com/openstack/os-client-config/blob/master/os_client_config/config.py.
|
|
func AuthOptions(opts *ClientOpts) (*gophercloud.AuthOptions, error) {
|
|
cloud := new(Cloud)
|
|
|
|
// If no opts were passed in, create an empty ClientOpts.
|
|
if opts == nil {
|
|
opts = new(ClientOpts)
|
|
}
|
|
|
|
// Determine if a clouds.yaml entry should be retrieved.
|
|
// Start by figuring out the cloud name.
|
|
// First check if one was explicitly specified in opts.
|
|
var cloudName string
|
|
if opts.Cloud != "" {
|
|
cloudName = opts.Cloud
|
|
}
|
|
|
|
// Next see if a cloud name was specified as an environment variable.
|
|
envPrefix := "OS_"
|
|
if opts.EnvPrefix != "" {
|
|
envPrefix = opts.EnvPrefix
|
|
}
|
|
|
|
if v := env.Getenv(envPrefix + "CLOUD"); v != "" {
|
|
cloudName = v
|
|
}
|
|
|
|
// If a cloud name was determined, try to look it up in clouds.yaml.
|
|
if cloudName != "" {
|
|
// Get the requested cloud.
|
|
var err error
|
|
cloud, err = GetCloudFromYAML(opts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// If cloud.AuthInfo is nil, then no cloud was specified.
|
|
if cloud.AuthInfo == nil {
|
|
// If opts.Auth is not nil, then try using the auth settings from it.
|
|
if opts.AuthInfo != nil {
|
|
cloud.AuthInfo = opts.AuthInfo
|
|
}
|
|
|
|
// If cloud.AuthInfo is still nil, then set it to an empty Auth struct
|
|
// and rely on environment variables to do the authentication.
|
|
if cloud.AuthInfo == nil {
|
|
cloud.AuthInfo = new(AuthInfo)
|
|
}
|
|
}
|
|
|
|
identityAPI := determineIdentityAPI(cloud, opts)
|
|
switch identityAPI {
|
|
case "2.0", "2":
|
|
return v2auth(cloud, opts)
|
|
case "3":
|
|
return v3auth(cloud, opts)
|
|
}
|
|
|
|
return nil, fmt.Errorf("Unable to build AuthOptions")
|
|
}
|
|
|
|
func determineIdentityAPI(cloud *Cloud, opts *ClientOpts) string {
|
|
var identityAPI string
|
|
if cloud.IdentityAPIVersion != "" {
|
|
identityAPI = cloud.IdentityAPIVersion
|
|
}
|
|
|
|
envPrefix := "OS_"
|
|
if opts != nil && opts.EnvPrefix != "" {
|
|
envPrefix = opts.EnvPrefix
|
|
}
|
|
|
|
if v := env.Getenv(envPrefix + "IDENTITY_API_VERSION"); v != "" {
|
|
identityAPI = v
|
|
}
|
|
|
|
if identityAPI == "" {
|
|
if cloud.AuthInfo != nil {
|
|
if strings.Contains(cloud.AuthInfo.AuthURL, "v2.0") {
|
|
identityAPI = "2.0"
|
|
}
|
|
|
|
if strings.Contains(cloud.AuthInfo.AuthURL, "v3") {
|
|
identityAPI = "3"
|
|
}
|
|
}
|
|
}
|
|
|
|
if identityAPI == "" {
|
|
switch cloud.AuthType {
|
|
case AuthV2Password:
|
|
identityAPI = "2.0"
|
|
case AuthV2Token:
|
|
identityAPI = "2.0"
|
|
case AuthV3Password:
|
|
identityAPI = "3"
|
|
case AuthV3Token:
|
|
identityAPI = "3"
|
|
case AuthV3ApplicationCredential:
|
|
identityAPI = "3"
|
|
}
|
|
}
|
|
|
|
// If an Identity API version could not be determined,
|
|
// default to v3.
|
|
if identityAPI == "" {
|
|
identityAPI = "3"
|
|
}
|
|
|
|
return identityAPI
|
|
}
|
|
|
|
// v2auth creates a v2-compatible gophercloud.AuthOptions struct.
|
|
func v2auth(cloud *Cloud, opts *ClientOpts) (*gophercloud.AuthOptions, error) {
|
|
// Environment variable overrides.
|
|
envPrefix := "OS_"
|
|
if opts != nil && opts.EnvPrefix != "" {
|
|
envPrefix = opts.EnvPrefix
|
|
}
|
|
|
|
if cloud.AuthInfo.AuthURL == "" {
|
|
if v := env.Getenv(envPrefix + "AUTH_URL"); v != "" {
|
|
cloud.AuthInfo.AuthURL = v
|
|
}
|
|
}
|
|
|
|
if cloud.AuthInfo.Token == "" {
|
|
if v := env.Getenv(envPrefix + "TOKEN"); v != "" {
|
|
cloud.AuthInfo.Token = v
|
|
}
|
|
|
|
if v := env.Getenv(envPrefix + "AUTH_TOKEN"); v != "" {
|
|
cloud.AuthInfo.Token = v
|
|
}
|
|
}
|
|
|
|
if cloud.AuthInfo.Username == "" {
|
|
if v := env.Getenv(envPrefix + "USERNAME"); v != "" {
|
|
cloud.AuthInfo.Username = v
|
|
}
|
|
}
|
|
|
|
if cloud.AuthInfo.Password == "" {
|
|
if v := env.Getenv(envPrefix + "PASSWORD"); v != "" {
|
|
cloud.AuthInfo.Password = v
|
|
}
|
|
}
|
|
|
|
if cloud.AuthInfo.ProjectID == "" {
|
|
if v := env.Getenv(envPrefix + "TENANT_ID"); v != "" {
|
|
cloud.AuthInfo.ProjectID = v
|
|
}
|
|
|
|
if v := env.Getenv(envPrefix + "PROJECT_ID"); v != "" {
|
|
cloud.AuthInfo.ProjectID = v
|
|
}
|
|
}
|
|
|
|
if cloud.AuthInfo.ProjectName == "" {
|
|
if v := env.Getenv(envPrefix + "TENANT_NAME"); v != "" {
|
|
cloud.AuthInfo.ProjectName = v
|
|
}
|
|
|
|
if v := env.Getenv(envPrefix + "PROJECT_NAME"); v != "" {
|
|
cloud.AuthInfo.ProjectName = v
|
|
}
|
|
}
|
|
|
|
ao := &gophercloud.AuthOptions{
|
|
IdentityEndpoint: cloud.AuthInfo.AuthURL,
|
|
TokenID: cloud.AuthInfo.Token,
|
|
Username: cloud.AuthInfo.Username,
|
|
Password: cloud.AuthInfo.Password,
|
|
TenantID: cloud.AuthInfo.ProjectID,
|
|
TenantName: cloud.AuthInfo.ProjectName,
|
|
}
|
|
|
|
return ao, nil
|
|
}
|
|
|
|
// v3auth creates a v3-compatible gophercloud.AuthOptions struct.
|
|
func v3auth(cloud *Cloud, opts *ClientOpts) (*gophercloud.AuthOptions, error) {
|
|
// Environment variable overrides.
|
|
envPrefix := "OS_"
|
|
if opts != nil && opts.EnvPrefix != "" {
|
|
envPrefix = opts.EnvPrefix
|
|
}
|
|
|
|
if cloud.AuthInfo.AuthURL == "" {
|
|
if v := env.Getenv(envPrefix + "AUTH_URL"); v != "" {
|
|
cloud.AuthInfo.AuthURL = v
|
|
}
|
|
}
|
|
|
|
if cloud.AuthInfo.Token == "" {
|
|
if v := env.Getenv(envPrefix + "TOKEN"); v != "" {
|
|
cloud.AuthInfo.Token = v
|
|
}
|
|
|
|
if v := env.Getenv(envPrefix + "AUTH_TOKEN"); v != "" {
|
|
cloud.AuthInfo.Token = v
|
|
}
|
|
}
|
|
|
|
if cloud.AuthInfo.Username == "" {
|
|
if v := env.Getenv(envPrefix + "USERNAME"); v != "" {
|
|
cloud.AuthInfo.Username = v
|
|
}
|
|
}
|
|
|
|
if cloud.AuthInfo.UserID == "" {
|
|
if v := env.Getenv(envPrefix + "USER_ID"); v != "" {
|
|
cloud.AuthInfo.UserID = v
|
|
}
|
|
}
|
|
|
|
if cloud.AuthInfo.Password == "" {
|
|
if v := env.Getenv(envPrefix + "PASSWORD"); v != "" {
|
|
cloud.AuthInfo.Password = v
|
|
}
|
|
}
|
|
|
|
if cloud.AuthInfo.ProjectID == "" {
|
|
if v := env.Getenv(envPrefix + "TENANT_ID"); v != "" {
|
|
cloud.AuthInfo.ProjectID = v
|
|
}
|
|
|
|
if v := env.Getenv(envPrefix + "PROJECT_ID"); v != "" {
|
|
cloud.AuthInfo.ProjectID = v
|
|
}
|
|
}
|
|
|
|
if cloud.AuthInfo.ProjectName == "" {
|
|
if v := env.Getenv(envPrefix + "TENANT_NAME"); v != "" {
|
|
cloud.AuthInfo.ProjectName = v
|
|
}
|
|
|
|
if v := env.Getenv(envPrefix + "PROJECT_NAME"); v != "" {
|
|
cloud.AuthInfo.ProjectName = v
|
|
}
|
|
}
|
|
|
|
if cloud.AuthInfo.DomainID == "" {
|
|
if v := env.Getenv(envPrefix + "DOMAIN_ID"); v != "" {
|
|
cloud.AuthInfo.DomainID = v
|
|
}
|
|
}
|
|
|
|
if cloud.AuthInfo.DomainName == "" {
|
|
if v := env.Getenv(envPrefix + "DOMAIN_NAME"); v != "" {
|
|
cloud.AuthInfo.DomainName = v
|
|
}
|
|
}
|
|
|
|
if cloud.AuthInfo.DefaultDomain == "" {
|
|
if v := env.Getenv(envPrefix + "DEFAULT_DOMAIN"); v != "" {
|
|
cloud.AuthInfo.DefaultDomain = v
|
|
}
|
|
}
|
|
|
|
if cloud.AuthInfo.ProjectDomainID == "" {
|
|
if v := env.Getenv(envPrefix + "PROJECT_DOMAIN_ID"); v != "" {
|
|
cloud.AuthInfo.ProjectDomainID = v
|
|
}
|
|
}
|
|
|
|
if cloud.AuthInfo.ProjectDomainName == "" {
|
|
if v := env.Getenv(envPrefix + "PROJECT_DOMAIN_NAME"); v != "" {
|
|
cloud.AuthInfo.ProjectDomainName = v
|
|
}
|
|
}
|
|
|
|
if cloud.AuthInfo.UserDomainID == "" {
|
|
if v := env.Getenv(envPrefix + "USER_DOMAIN_ID"); v != "" {
|
|
cloud.AuthInfo.UserDomainID = v
|
|
}
|
|
}
|
|
|
|
if cloud.AuthInfo.UserDomainName == "" {
|
|
if v := env.Getenv(envPrefix + "USER_DOMAIN_NAME"); v != "" {
|
|
cloud.AuthInfo.UserDomainName = v
|
|
}
|
|
}
|
|
|
|
if cloud.AuthInfo.ApplicationCredentialID == "" {
|
|
if v := env.Getenv(envPrefix + "APPLICATION_CREDENTIAL_ID"); v != "" {
|
|
cloud.AuthInfo.ApplicationCredentialID = v
|
|
}
|
|
}
|
|
|
|
if cloud.AuthInfo.ApplicationCredentialName == "" {
|
|
if v := env.Getenv(envPrefix + "APPLICATION_CREDENTIAL_NAME"); v != "" {
|
|
cloud.AuthInfo.ApplicationCredentialName = v
|
|
}
|
|
}
|
|
|
|
if cloud.AuthInfo.ApplicationCredentialSecret == "" {
|
|
if v := env.Getenv(envPrefix + "APPLICATION_CREDENTIAL_SECRET"); v != "" {
|
|
cloud.AuthInfo.ApplicationCredentialSecret = v
|
|
}
|
|
}
|
|
|
|
// Build a scope and try to do it correctly.
|
|
// https://github.com/openstack/os-client-config/blob/master/os_client_config/config.py#L595
|
|
scope := new(gophercloud.AuthScope)
|
|
|
|
// Application credentials don't support scope
|
|
if isApplicationCredential(cloud.AuthInfo) {
|
|
// If Domain* is set, but UserDomain* or ProjectDomain* aren't,
|
|
// then use Domain* as the default setting.
|
|
cloud = setDomainIfNeeded(cloud)
|
|
} else {
|
|
if !isProjectScoped(cloud.AuthInfo) {
|
|
if cloud.AuthInfo.DomainID != "" {
|
|
scope.DomainID = cloud.AuthInfo.DomainID
|
|
} else if cloud.AuthInfo.DomainName != "" {
|
|
scope.DomainName = cloud.AuthInfo.DomainName
|
|
}
|
|
} else {
|
|
// If Domain* is set, but UserDomain* or ProjectDomain* aren't,
|
|
// then use Domain* as the default setting.
|
|
cloud = setDomainIfNeeded(cloud)
|
|
|
|
if cloud.AuthInfo.ProjectID != "" {
|
|
scope.ProjectID = cloud.AuthInfo.ProjectID
|
|
} else {
|
|
scope.ProjectName = cloud.AuthInfo.ProjectName
|
|
scope.DomainID = cloud.AuthInfo.ProjectDomainID
|
|
scope.DomainName = cloud.AuthInfo.ProjectDomainName
|
|
}
|
|
}
|
|
}
|
|
|
|
ao := &gophercloud.AuthOptions{
|
|
Scope: scope,
|
|
IdentityEndpoint: cloud.AuthInfo.AuthURL,
|
|
TokenID: cloud.AuthInfo.Token,
|
|
Username: cloud.AuthInfo.Username,
|
|
UserID: cloud.AuthInfo.UserID,
|
|
Password: cloud.AuthInfo.Password,
|
|
TenantID: cloud.AuthInfo.ProjectID,
|
|
TenantName: cloud.AuthInfo.ProjectName,
|
|
DomainID: cloud.AuthInfo.UserDomainID,
|
|
DomainName: cloud.AuthInfo.UserDomainName,
|
|
ApplicationCredentialID: cloud.AuthInfo.ApplicationCredentialID,
|
|
ApplicationCredentialName: cloud.AuthInfo.ApplicationCredentialName,
|
|
ApplicationCredentialSecret: cloud.AuthInfo.ApplicationCredentialSecret,
|
|
}
|
|
|
|
// If an auth_type of "token" was specified, then make sure
|
|
// Gophercloud properly authenticates with a token. This involves
|
|
// unsetting a few other auth options. The reason this is done
|
|
// here is to wait until all auth settings (both in clouds.yaml
|
|
// and via environment variables) are set and then unset them.
|
|
if strings.Contains(string(cloud.AuthType), "token") || ao.TokenID != "" {
|
|
ao.Username = ""
|
|
ao.Password = ""
|
|
ao.UserID = ""
|
|
ao.DomainID = ""
|
|
ao.DomainName = ""
|
|
}
|
|
|
|
// Check for absolute minimum requirements.
|
|
if ao.IdentityEndpoint == "" {
|
|
err := gophercloud.ErrMissingInput{Argument: "auth_url"}
|
|
return nil, err
|
|
}
|
|
|
|
return ao, nil
|
|
}
|
|
|
|
// AuthenticatedClient is a convenience function to get a new provider client
|
|
// based on a clouds.yaml entry.
|
|
func AuthenticatedClient(opts *ClientOpts) (*gophercloud.ProviderClient, error) {
|
|
ao, err := AuthOptions(opts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return openstack.AuthenticatedClient(*ao)
|
|
}
|
|
|
|
// NewServiceClient is a convenience function to get a new service client.
|
|
func NewServiceClient(service string, opts *ClientOpts) (*gophercloud.ServiceClient, error) {
|
|
cloud := new(Cloud)
|
|
|
|
// If no opts were passed in, create an empty ClientOpts.
|
|
if opts == nil {
|
|
opts = new(ClientOpts)
|
|
}
|
|
|
|
// Determine if a clouds.yaml entry should be retrieved.
|
|
// Start by figuring out the cloud name.
|
|
// First check if one was explicitly specified in opts.
|
|
var cloudName string
|
|
if opts.Cloud != "" {
|
|
cloudName = opts.Cloud
|
|
}
|
|
|
|
// Next see if a cloud name was specified as an environment variable.
|
|
envPrefix := "OS_"
|
|
if opts.EnvPrefix != "" {
|
|
envPrefix = opts.EnvPrefix
|
|
}
|
|
|
|
if v := env.Getenv(envPrefix + "CLOUD"); v != "" {
|
|
cloudName = v
|
|
}
|
|
|
|
// If a cloud name was determined, try to look it up in clouds.yaml.
|
|
if cloudName != "" {
|
|
// Get the requested cloud.
|
|
var err error
|
|
cloud, err = GetCloudFromYAML(opts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Get a Provider Client
|
|
pClient, err := AuthenticatedClient(opts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// If an HTTPClient was specified, use it.
|
|
if opts.HTTPClient != nil {
|
|
pClient.HTTPClient = *opts.HTTPClient
|
|
}
|
|
|
|
// Determine the region to use.
|
|
// First, check if the REGION_NAME environment variable is set.
|
|
var region string
|
|
if v := env.Getenv(envPrefix + "REGION_NAME"); v != "" {
|
|
region = v
|
|
}
|
|
|
|
// Next, check if the cloud entry sets a region.
|
|
if v := cloud.RegionName; v != "" {
|
|
region = v
|
|
}
|
|
|
|
// Finally, see if one was specified in the ClientOpts.
|
|
// If so, this takes precedence.
|
|
if v := opts.RegionName; v != "" {
|
|
region = v
|
|
}
|
|
|
|
// Determine the endpoint type to use.
|
|
// First, check if the OS_INTERFACE environment variable is set.
|
|
var endpointType string
|
|
if v := env.Getenv(envPrefix + "INTERFACE"); v != "" {
|
|
endpointType = v
|
|
}
|
|
|
|
// Next, check if the cloud entry sets an endpoint type.
|
|
if v := cloud.EndpointType; v != "" {
|
|
endpointType = v
|
|
}
|
|
|
|
// Finally, see if one was specified in the ClientOpts.
|
|
// If so, this takes precedence.
|
|
if v := opts.EndpointType; v != "" {
|
|
endpointType = v
|
|
}
|
|
|
|
eo := gophercloud.EndpointOpts{
|
|
Region: region,
|
|
Availability: GetEndpointType(endpointType),
|
|
}
|
|
|
|
switch service {
|
|
case "clustering":
|
|
return openstack.NewClusteringV1(pClient, eo)
|
|
case "compute":
|
|
return openstack.NewComputeV2(pClient, eo)
|
|
case "container":
|
|
return openstack.NewContainerV1(pClient, eo)
|
|
case "container-infra":
|
|
return openstack.NewContainerInfraV1(pClient, eo)
|
|
case "database":
|
|
return openstack.NewDBV1(pClient, eo)
|
|
case "dns":
|
|
return openstack.NewDNSV2(pClient, eo)
|
|
case "identity":
|
|
identityVersion := "3"
|
|
if v := cloud.IdentityAPIVersion; v != "" {
|
|
identityVersion = v
|
|
}
|
|
|
|
switch identityVersion {
|
|
case "v2", "2", "2.0":
|
|
return openstack.NewIdentityV2(pClient, eo)
|
|
case "v3", "3":
|
|
return openstack.NewIdentityV3(pClient, eo)
|
|
default:
|
|
return nil, fmt.Errorf("invalid identity API version")
|
|
}
|
|
case "image":
|
|
return openstack.NewImageServiceV2(pClient, eo)
|
|
case "load-balancer":
|
|
return openstack.NewLoadBalancerV2(pClient, eo)
|
|
case "network":
|
|
return openstack.NewNetworkV2(pClient, eo)
|
|
case "object-store":
|
|
return openstack.NewObjectStorageV1(pClient, eo)
|
|
case "orchestration":
|
|
return openstack.NewOrchestrationV1(pClient, eo)
|
|
case "sharev2":
|
|
return openstack.NewSharedFileSystemV2(pClient, eo)
|
|
case "volume":
|
|
volumeVersion := "2"
|
|
if v := cloud.VolumeAPIVersion; v != "" {
|
|
volumeVersion = v
|
|
}
|
|
|
|
switch volumeVersion {
|
|
case "v1", "1":
|
|
return openstack.NewBlockStorageV1(pClient, eo)
|
|
case "v2", "2":
|
|
return openstack.NewBlockStorageV2(pClient, eo)
|
|
case "v3", "3":
|
|
return openstack.NewBlockStorageV3(pClient, eo)
|
|
default:
|
|
return nil, fmt.Errorf("invalid volume API version")
|
|
}
|
|
}
|
|
|
|
return nil, fmt.Errorf("unable to create a service client for %s", service)
|
|
}
|
|
|
|
// isProjectScoped determines if an auth struct is project scoped.
|
|
func isProjectScoped(authInfo *AuthInfo) bool {
|
|
if authInfo.ProjectID == "" && authInfo.ProjectName == "" {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// setDomainIfNeeded will set a DomainID and DomainName
|
|
// to ProjectDomain* and UserDomain* if not already set.
|
|
func setDomainIfNeeded(cloud *Cloud) *Cloud {
|
|
if cloud.AuthInfo.DomainID != "" {
|
|
if cloud.AuthInfo.UserDomainID == "" {
|
|
cloud.AuthInfo.UserDomainID = cloud.AuthInfo.DomainID
|
|
}
|
|
|
|
if cloud.AuthInfo.ProjectDomainID == "" {
|
|
cloud.AuthInfo.ProjectDomainID = cloud.AuthInfo.DomainID
|
|
}
|
|
|
|
cloud.AuthInfo.DomainID = ""
|
|
}
|
|
|
|
if cloud.AuthInfo.DomainName != "" {
|
|
if cloud.AuthInfo.UserDomainName == "" {
|
|
cloud.AuthInfo.UserDomainName = cloud.AuthInfo.DomainName
|
|
}
|
|
|
|
if cloud.AuthInfo.ProjectDomainName == "" {
|
|
cloud.AuthInfo.ProjectDomainName = cloud.AuthInfo.DomainName
|
|
}
|
|
|
|
cloud.AuthInfo.DomainName = ""
|
|
}
|
|
|
|
// If Domain fields are still not set, and if DefaultDomain has a value,
|
|
// set UserDomainID and ProjectDomainID to DefaultDomain.
|
|
// https://github.com/openstack/osc-lib/blob/86129e6f88289ef14bfaa3f7c9cdfbea8d9fc944/osc_lib/cli/client_config.py#L117-L146
|
|
if cloud.AuthInfo.DefaultDomain != "" {
|
|
if cloud.AuthInfo.UserDomainName == "" && cloud.AuthInfo.UserDomainID == "" {
|
|
cloud.AuthInfo.UserDomainID = cloud.AuthInfo.DefaultDomain
|
|
}
|
|
|
|
if cloud.AuthInfo.ProjectDomainName == "" && cloud.AuthInfo.ProjectDomainID == "" {
|
|
cloud.AuthInfo.ProjectDomainID = cloud.AuthInfo.DefaultDomain
|
|
}
|
|
}
|
|
|
|
return cloud
|
|
}
|
|
|
|
// isApplicationCredential determines if an application credential is used to auth.
|
|
func isApplicationCredential(authInfo *AuthInfo) bool {
|
|
if authInfo.ApplicationCredentialID == "" && authInfo.ApplicationCredentialName == "" && authInfo.ApplicationCredentialSecret == "" {
|
|
return false
|
|
}
|
|
return true
|
|
}
|