terraform/vendor/github.com/gophercloud/utils/openstack/clientconfig/requests.go

805 lines
22 KiB
Go

package clientconfig
import (
"fmt"
"os"
"reflect"
"strings"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack"
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
}
// LoadCloudsYAML will load a clouds.yaml file and return the full config.
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.
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.
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) {
clouds, err := 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 := os.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
}
}
var cloudIsInCloudsYaml bool
if cloud == nil {
// not an immediate error as it might still be defined in secure.yaml
cloudIsInCloudsYaml = false
} else {
cloudIsInCloudsYaml = true
}
publicClouds, err := LoadPublicCloudsYAML()
if err != nil {
return nil, fmt.Errorf("unable to load clouds-public.yaml: %s", err)
}
var profileName = defaultIfEmpty(cloud.Profile, cloud.Cloud)
if profileName != "" {
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)
}
}
secureClouds, err := 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 !cloudIsInCloudsYaml && cloudName == "" && len(secureClouds) == 1 {
for _, v := range secureClouds {
cloud = &v
}
}
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")
}
}
}
// 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
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 := os.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 := os.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 := os.Getenv(envPrefix + "AUTH_URL"); v != "" {
cloud.AuthInfo.AuthURL = v
}
}
if cloud.AuthInfo.Token == "" {
if v := os.Getenv(envPrefix + "TOKEN"); v != "" {
cloud.AuthInfo.Token = v
}
if v := os.Getenv(envPrefix + "AUTH_TOKEN"); v != "" {
cloud.AuthInfo.Token = v
}
}
if cloud.AuthInfo.Username == "" {
if v := os.Getenv(envPrefix + "USERNAME"); v != "" {
cloud.AuthInfo.Username = v
}
}
if cloud.AuthInfo.Password == "" {
if v := os.Getenv(envPrefix + "PASSWORD"); v != "" {
cloud.AuthInfo.Password = v
}
}
if cloud.AuthInfo.ProjectID == "" {
if v := os.Getenv(envPrefix + "TENANT_ID"); v != "" {
cloud.AuthInfo.ProjectID = v
}
if v := os.Getenv(envPrefix + "PROJECT_ID"); v != "" {
cloud.AuthInfo.ProjectID = v
}
}
if cloud.AuthInfo.ProjectName == "" {
if v := os.Getenv(envPrefix + "TENANT_NAME"); v != "" {
cloud.AuthInfo.ProjectName = v
}
if v := os.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 := os.Getenv(envPrefix + "AUTH_URL"); v != "" {
cloud.AuthInfo.AuthURL = v
}
}
if cloud.AuthInfo.Token == "" {
if v := os.Getenv(envPrefix + "TOKEN"); v != "" {
cloud.AuthInfo.Token = v
}
if v := os.Getenv(envPrefix + "AUTH_TOKEN"); v != "" {
cloud.AuthInfo.Token = v
}
}
if cloud.AuthInfo.Username == "" {
if v := os.Getenv(envPrefix + "USERNAME"); v != "" {
cloud.AuthInfo.Username = v
}
}
if cloud.AuthInfo.UserID == "" {
if v := os.Getenv(envPrefix + "USER_ID"); v != "" {
cloud.AuthInfo.UserID = v
}
}
if cloud.AuthInfo.Password == "" {
if v := os.Getenv(envPrefix + "PASSWORD"); v != "" {
cloud.AuthInfo.Password = v
}
}
if cloud.AuthInfo.ProjectID == "" {
if v := os.Getenv(envPrefix + "TENANT_ID"); v != "" {
cloud.AuthInfo.ProjectID = v
}
if v := os.Getenv(envPrefix + "PROJECT_ID"); v != "" {
cloud.AuthInfo.ProjectID = v
}
}
if cloud.AuthInfo.ProjectName == "" {
if v := os.Getenv(envPrefix + "TENANT_NAME"); v != "" {
cloud.AuthInfo.ProjectName = v
}
if v := os.Getenv(envPrefix + "PROJECT_NAME"); v != "" {
cloud.AuthInfo.ProjectName = v
}
}
if cloud.AuthInfo.DomainID == "" {
if v := os.Getenv(envPrefix + "DOMAIN_ID"); v != "" {
cloud.AuthInfo.DomainID = v
}
}
if cloud.AuthInfo.DomainName == "" {
if v := os.Getenv(envPrefix + "DOMAIN_NAME"); v != "" {
cloud.AuthInfo.DomainName = v
}
}
if cloud.AuthInfo.DefaultDomain == "" {
if v := os.Getenv(envPrefix + "DEFAULT_DOMAIN"); v != "" {
cloud.AuthInfo.DefaultDomain = v
}
}
if cloud.AuthInfo.ProjectDomainID == "" {
if v := os.Getenv(envPrefix + "PROJECT_DOMAIN_ID"); v != "" {
cloud.AuthInfo.ProjectDomainID = v
}
}
if cloud.AuthInfo.ProjectDomainName == "" {
if v := os.Getenv(envPrefix + "PROJECT_DOMAIN_NAME"); v != "" {
cloud.AuthInfo.ProjectDomainName = v
}
}
if cloud.AuthInfo.UserDomainID == "" {
if v := os.Getenv(envPrefix + "USER_DOMAIN_ID"); v != "" {
cloud.AuthInfo.UserDomainID = v
}
}
if cloud.AuthInfo.UserDomainName == "" {
if v := os.Getenv(envPrefix + "USER_DOMAIN_NAME"); v != "" {
cloud.AuthInfo.UserDomainName = v
}
}
if cloud.AuthInfo.ApplicationCredentialID == "" {
if v := os.Getenv(envPrefix + "APPLICATION_CREDENTIAL_ID"); v != "" {
cloud.AuthInfo.ApplicationCredentialID = v
}
}
if cloud.AuthInfo.ApplicationCredentialName == "" {
if v := os.Getenv(envPrefix + "APPLICATION_CREDENTIAL_NAME"); v != "" {
cloud.AuthInfo.ApplicationCredentialName = v
}
}
if cloud.AuthInfo.ApplicationCredentialSecret == "" {
if v := os.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 := os.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
}
// Determine the region to use.
// First, check if the REGION_NAME environment variable is set.
var region string
if v := os.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
}
eo := gophercloud.EndpointOpts{
Region: region,
}
switch service {
case "clustering":
return openstack.NewClusteringV1(pClient, eo)
case "compute":
return openstack.NewComputeV2(pClient, eo)
case "container":
return openstack.NewContainerV1(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
}