Merge pull request #7300 from kmbulebu/openstack_app_creds

OpenStack: Support Application Credential Authentication
This commit is contained in:
Megan Marsh 2019-03-05 13:11:51 -08:00 committed by GitHub
commit 5aeab4ec06
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 369 additions and 142 deletions

View File

@ -16,22 +16,25 @@ import (
// AccessConfig is for common configuration related to openstack access
type AccessConfig struct {
Username string `mapstructure:"username"`
UserID string `mapstructure:"user_id"`
Password string `mapstructure:"password"`
IdentityEndpoint string `mapstructure:"identity_endpoint"`
TenantID string `mapstructure:"tenant_id"`
TenantName string `mapstructure:"tenant_name"`
DomainID string `mapstructure:"domain_id"`
DomainName string `mapstructure:"domain_name"`
Insecure bool `mapstructure:"insecure"`
Region string `mapstructure:"region"`
EndpointType string `mapstructure:"endpoint_type"`
CACertFile string `mapstructure:"cacert"`
ClientCertFile string `mapstructure:"cert"`
ClientKeyFile string `mapstructure:"key"`
Token string `mapstructure:"token"`
Cloud string `mapstructure:"cloud"`
Username string `mapstructure:"username"`
UserID string `mapstructure:"user_id"`
Password string `mapstructure:"password"`
IdentityEndpoint string `mapstructure:"identity_endpoint"`
TenantID string `mapstructure:"tenant_id"`
TenantName string `mapstructure:"tenant_name"`
DomainID string `mapstructure:"domain_id"`
DomainName string `mapstructure:"domain_name"`
Insecure bool `mapstructure:"insecure"`
Region string `mapstructure:"region"`
EndpointType string `mapstructure:"endpoint_type"`
CACertFile string `mapstructure:"cacert"`
ClientCertFile string `mapstructure:"cert"`
ClientKeyFile string `mapstructure:"key"`
Token string `mapstructure:"token"`
ApplicationCredentialName string `mapstructure:"application_credential_name"`
ApplicationCredentialID string `mapstructure:"application_credential_id"`
ApplicationCredentialSecret string `mapstructure:"application_credential_secret"`
Cloud string `mapstructure:"cloud"`
osClient *gophercloud.ProviderClient
}
@ -126,6 +129,9 @@ func (c *AccessConfig) Prepare(ctx *interpolate.Context) []error {
{&c.DomainID, &ao.DomainID},
{&c.DomainName, &ao.DomainName},
{&c.Token, &ao.TokenID},
{&c.ApplicationCredentialName, &ao.ApplicationCredentialName},
{&c.ApplicationCredentialID, &ao.ApplicationCredentialID},
{&c.ApplicationCredentialSecret, &ao.ApplicationCredentialSecret},
}
for _, s := range overrides {
if *s.From != "" {

4
go.mod
View File

@ -64,8 +64,8 @@ require (
github.com/google/go-querystring v0.0.0-20151028211038-2a60fc2ba6c1 // indirect
github.com/google/shlex v0.0.0-20150127133951-6f45313302b9
github.com/google/uuid v0.0.0-20171129191014-dec09d789f3d
github.com/gophercloud/gophercloud v0.0.0-20180815020510-83835c772d1a
github.com/gophercloud/utils v0.0.0-20180806215700-d6e28a8b3199
github.com/gophercloud/gophercloud v0.0.0-20180903124057-ea7289ebdf06
github.com/gophercloud/utils v0.0.0-20190124192022-a5c25e7a53a6
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect
github.com/gorilla/websocket v0.0.0-20170319172727-a91eba7f9777 // indirect
github.com/gotestyourself/gotestyourself v2.2.0+incompatible // indirect

8
go.sum
View File

@ -143,10 +143,10 @@ github.com/google/shlex v0.0.0-20150127133951-6f45313302b9 h1:JM174NTeGNJ2m/oLH3
github.com/google/shlex v0.0.0-20150127133951-6f45313302b9/go.mod h1:RpwtwJQFrIEPstU94h88MWPXP2ektJZ8cZ0YntAmXiE=
github.com/google/uuid v0.0.0-20171129191014-dec09d789f3d h1:rXQlD9GXkjA/PQZhmEaF/8Pj/sJfdZJK7GJG0gkS8I0=
github.com/google/uuid v0.0.0-20171129191014-dec09d789f3d/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gophercloud/gophercloud v0.0.0-20180815020510-83835c772d1a h1:BYGFl3ozKqWP2FnV4hyr8pNvBBLvyoREM4H6Un75wQ4=
github.com/gophercloud/gophercloud v0.0.0-20180815020510-83835c772d1a/go.mod h1:3WdhXV3rUYy9p6AUW8d94kr+HS62Y4VL9mBnFxsD8q4=
github.com/gophercloud/utils v0.0.0-20180806215700-d6e28a8b3199 h1:mmwryCmmFkCxL3t5r6syrbk1eyP6tP9q/whDdAiM9Mw=
github.com/gophercloud/utils v0.0.0-20180806215700-d6e28a8b3199/go.mod h1:wjDF8z83zTeg5eMLml5EBSlAhbF7G8DobyI1YsMuyzw=
github.com/gophercloud/gophercloud v0.0.0-20180903124057-ea7289ebdf06 h1:m7Rt/8En7PLrM7PQpykdZBPKUdgZWN6MwiA/ChVIoxs=
github.com/gophercloud/gophercloud v0.0.0-20180903124057-ea7289ebdf06/go.mod h1:3WdhXV3rUYy9p6AUW8d94kr+HS62Y4VL9mBnFxsD8q4=
github.com/gophercloud/utils v0.0.0-20190124192022-a5c25e7a53a6 h1:Cw/B8Bu7Rryomxf7bjc8zNfIyLgjxsDd91n0eGRWpuo=
github.com/gophercloud/utils v0.0.0-20190124192022-a5c25e7a53a6/go.mod h1:wjDF8z83zTeg5eMLml5EBSlAhbF7G8DobyI1YsMuyzw=
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg=
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v0.0.0-20170319172727-a91eba7f9777 h1:JIM+OacoOJRU30xpjMf8sulYqjr0ViA3WDrTX6j/yDI=

View File

@ -140,7 +140,7 @@ See the [contributing guide](./.github/CONTRIBUTING.md).
## Help and feedback
If you're struggling with something or have spotted a potential bug, feel free
to submit an issue to our [bug tracker](/issues).
to submit an issue to our [bug tracker](https://github.com/gophercloud/gophercloud/issues).
## Thank You

View File

@ -84,6 +84,12 @@ type AuthOptions struct {
// Scope determines the scoping of the authentication request.
Scope *AuthScope `json:"-"`
// Authentication through Application Credentials requires supplying name, project and secret
// For project we can use TenantID
ApplicationCredentialID string `json:"-"`
ApplicationCredentialName string `json:"-"`
ApplicationCredentialSecret string `json:"-"`
}
// AuthScope allows a created token to be limited to a specific domain or project.
@ -142,7 +148,7 @@ func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[s
type userReq struct {
ID *string `json:"id,omitempty"`
Name *string `json:"name,omitempty"`
Password string `json:"password"`
Password string `json:"password,omitempty"`
Domain *domainReq `json:"domain,omitempty"`
}
@ -154,10 +160,18 @@ func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[s
ID string `json:"id"`
}
type applicationCredentialReq struct {
ID *string `json:"id,omitempty"`
Name *string `json:"name,omitempty"`
User *userReq `json:"user,omitempty"`
Secret *string `json:"secret,omitempty"`
}
type identityReq struct {
Methods []string `json:"methods"`
Password *passwordReq `json:"password,omitempty"`
Token *tokenReq `json:"token,omitempty"`
Methods []string `json:"methods"`
Password *passwordReq `json:"password,omitempty"`
Token *tokenReq `json:"token,omitempty"`
ApplicationCredential *applicationCredentialReq `json:"application_credential,omitempty"`
}
type authReq struct {
@ -171,6 +185,7 @@ func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[s
// Populate the request structure based on the provided arguments. Create and return an error
// if insufficient or incompatible information is present.
var req request
var userRequest userReq
if opts.Password == "" {
if opts.TokenID != "" {
@ -194,8 +209,49 @@ func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[s
req.Auth.Identity.Token = &tokenReq{
ID: opts.TokenID,
}
} else if opts.ApplicationCredentialID != "" {
// Configure the request for ApplicationCredentialID authentication.
// https://github.com/openstack/keystoneauth/blob/stable/rocky/keystoneauth1/identity/v3/application_credential.py#L48-L67
// There are three kinds of possible application_credential requests
// 1. application_credential id + secret
// 2. application_credential name + secret + user_id
// 3. application_credential name + secret + username + domain_id / domain_name
if opts.ApplicationCredentialSecret == "" {
return nil, ErrAppCredMissingSecret{}
}
req.Auth.Identity.Methods = []string{"application_credential"}
req.Auth.Identity.ApplicationCredential = &applicationCredentialReq{
ID: &opts.ApplicationCredentialID,
Secret: &opts.ApplicationCredentialSecret,
}
} else if opts.ApplicationCredentialName != "" {
if opts.ApplicationCredentialSecret == "" {
return nil, ErrAppCredMissingSecret{}
}
// make sure that only one of DomainName or DomainID were provided
if opts.DomainID == "" && opts.DomainName == "" {
return nil, ErrDomainIDOrDomainName{}
}
req.Auth.Identity.Methods = []string{"application_credential"}
if opts.DomainID != "" {
userRequest = userReq{
Name: &opts.Username,
Domain: &domainReq{ID: &opts.DomainID},
}
} else if opts.DomainName != "" {
userRequest = userReq{
Name: &opts.Username,
Domain: &domainReq{Name: &opts.DomainName},
}
}
req.Auth.Identity.ApplicationCredential = &applicationCredentialReq{
Name: &opts.ApplicationCredentialName,
User: &userRequest,
Secret: &opts.ApplicationCredentialSecret,
}
} else {
// If no password or token ID are available, authentication can't continue.
// If no password or token ID or ApplicationCredential are available, authentication can't continue.
return nil, ErrMissingPassword{}
}
} else {

View File

@ -451,3 +451,10 @@ type ErrScopeEmpty struct{ BaseError }
func (e ErrScopeEmpty) Error() string {
return "You must provide either a Project or Domain in a Scope"
}
// ErrAppCredMissingSecret indicates that no Application Credential Secret was provided with Application Credential ID or Name
type ErrAppCredMissingSecret struct{ BaseError }
func (e ErrAppCredMissingSecret) Error() string {
return "You must provide an Application Credential Secret"
}

View File

@ -295,7 +295,11 @@ func (client *ProviderClient) Request(method, url string, options *RequestOpts)
seeker.Seek(0, 0)
}
}
// make a new call to request with a nil reauth func in order to avoid infinite loop
reauthFunc := client.ReauthFunc
client.ReauthFunc = nil
resp, err = client.Request(method, url, options)
client.ReauthFunc = reauthFunc
if err != nil {
switch err.(type) {
case *ErrUnexpectedResponseCode:
@ -378,7 +382,7 @@ func defaultOkCodes(method string) []int {
case method == "PUT":
return []int{201, 202}
case method == "PATCH":
return []int{200, 204}
return []int{200, 202, 204}
case method == "DELETE":
return []int{202, 204}
}

View File

@ -89,23 +89,45 @@ func (r Result) extractIntoPtr(to interface{}, label string) error {
if typeOfV.Kind() == reflect.Struct {
if typeOfV.NumField() > 0 && typeOfV.Field(0).Anonymous {
newSlice := reflect.MakeSlice(reflect.SliceOf(typeOfV), 0, 0)
newType := reflect.New(typeOfV).Elem()
for _, v := range m[label].([]interface{}) {
// For each iteration of the slice, we create a new struct.
// This is to work around a bug where elements of a slice
// are reused and not overwritten when the same copy of the
// struct is used:
//
// https://github.com/golang/go/issues/21092
// https://github.com/golang/go/issues/24155
// https://play.golang.org/p/NHo3ywlPZli
newType := reflect.New(typeOfV).Elem()
b, err := json.Marshal(v)
if err != nil {
return err
}
// This is needed for structs with an UnmarshalJSON method.
// Technically this is just unmarshalling the response into
// a struct that is never used, but it's good enough to
// trigger the UnmarshalJSON method.
for i := 0; i < newType.NumField(); i++ {
s := newType.Field(i).Addr().Interface()
err = json.NewDecoder(bytes.NewReader(b)).Decode(s)
// Unmarshal is used rather than NewDecoder to also work
// around the above-mentioned bug.
err = json.Unmarshal(b, s)
if err != nil {
return err
}
}
newSlice = reflect.Append(newSlice, newType)
}
// "to" should now be properly modeled to receive the
// JSON response body and unmarshal into all the correct
// fields of the struct or composed extension struct
// at the end of this method.
toValue.Set(newSlice)
}
}
@ -366,6 +388,27 @@ func (jt *JSONRFC3339ZNoT) UnmarshalJSON(data []byte) error {
return nil
}
// RFC3339ZNoTNoZ is another time format used in Zun (Containers Service).
const RFC3339ZNoTNoZ = "2006-01-02 15:04:05"
type JSONRFC3339ZNoTNoZ time.Time
func (jt *JSONRFC3339ZNoTNoZ) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
if s == "" {
return nil
}
t, err := time.Parse(RFC3339ZNoTNoZ, s)
if err != nil {
return err
}
*jt = JSONRFC3339ZNoTNoZ(t)
return nil
}
/*
Link is an internal type to be used in packages of collection resources that are
paginated in a certain way.

View File

@ -9,7 +9,7 @@ import (
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack"
"gopkg.in/yaml.v2"
yaml "gopkg.in/yaml.v2"
)
// AuthType respresents a valid method of authentication.
@ -30,6 +30,9 @@ const (
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
@ -333,6 +336,8 @@ func determineIdentityAPI(cloud *Cloud, opts *ClientOpts) string {
identityAPI = "3"
case AuthV3Token:
identityAPI = "3"
case AuthV3ApplicationCredential:
identityAPI = "3"
}
}
@ -353,40 +358,52 @@ func v2auth(cloud *Cloud, opts *ClientOpts) (*gophercloud.AuthOptions, error) {
envPrefix = opts.EnvPrefix
}
if v := os.Getenv(envPrefix + "AUTH_URL"); v != "" {
cloud.AuthInfo.AuthURL = v
if cloud.AuthInfo.AuthURL == "" {
if v := os.Getenv(envPrefix + "AUTH_URL"); v != "" {
cloud.AuthInfo.AuthURL = v
}
}
if v := os.Getenv(envPrefix + "TOKEN"); v != "" {
cloud.AuthInfo.Token = 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 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 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 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 v := os.Getenv(envPrefix + "TENANT_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_ID"); v != "" {
cloud.AuthInfo.ProjectID = v
}
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 v := os.Getenv(envPrefix + "PROJECT_NAME"); v != "" {
cloud.AuthInfo.ProjectName = v
}
}
ao := &gophercloud.AuthOptions{
@ -409,109 +426,161 @@ func v3auth(cloud *Cloud, opts *ClientOpts) (*gophercloud.AuthOptions, error) {
envPrefix = opts.EnvPrefix
}
if v := os.Getenv(envPrefix + "AUTH_URL"); v != "" {
cloud.AuthInfo.AuthURL = v
if cloud.AuthInfo.AuthURL == "" {
if v := os.Getenv(envPrefix + "AUTH_URL"); v != "" {
cloud.AuthInfo.AuthURL = v
}
}
if v := os.Getenv(envPrefix + "TOKEN"); v != "" {
cloud.AuthInfo.Token = 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 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 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 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 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 v := os.Getenv(envPrefix + "TENANT_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 v := os.Getenv(envPrefix + "PROJECT_ID"); v != "" {
cloud.AuthInfo.ProjectID = v
if cloud.AuthInfo.DomainID == "" {
if v := os.Getenv(envPrefix + "DOMAIN_ID"); v != "" {
cloud.AuthInfo.DomainID = v
}
}
if v := os.Getenv(envPrefix + "TENANT_NAME"); v != "" {
cloud.AuthInfo.ProjectName = v
if cloud.AuthInfo.DomainName == "" {
if v := os.Getenv(envPrefix + "DOMAIN_NAME"); v != "" {
cloud.AuthInfo.DomainName = v
}
}
if v := os.Getenv(envPrefix + "PROJECT_NAME"); v != "" {
cloud.AuthInfo.ProjectName = v
if cloud.AuthInfo.DefaultDomain == "" {
if v := os.Getenv(envPrefix + "DEFAULT_DOMAIN"); v != "" {
cloud.AuthInfo.DefaultDomain = v
}
}
if v := os.Getenv(envPrefix + "DOMAIN_ID"); v != "" {
cloud.AuthInfo.DomainID = v
if cloud.AuthInfo.ProjectDomainID == "" {
if v := os.Getenv(envPrefix + "PROJECT_DOMAIN_ID"); v != "" {
cloud.AuthInfo.ProjectDomainID = v
}
}
if v := os.Getenv(envPrefix + "DOMAIN_NAME"); v != "" {
cloud.AuthInfo.DomainName = v
if cloud.AuthInfo.ProjectDomainName == "" {
if v := os.Getenv(envPrefix + "PROJECT_DOMAIN_NAME"); v != "" {
cloud.AuthInfo.ProjectDomainName = v
}
}
if v := os.Getenv(envPrefix + "DEFAULT_DOMAIN"); v != "" {
cloud.AuthInfo.DefaultDomain = v
if cloud.AuthInfo.UserDomainID == "" {
if v := os.Getenv(envPrefix + "USER_DOMAIN_ID"); v != "" {
cloud.AuthInfo.UserDomainID = v
}
}
if v := os.Getenv(envPrefix + "PROJECT_DOMAIN_ID"); v != "" {
cloud.AuthInfo.ProjectDomainID = v
if cloud.AuthInfo.UserDomainName == "" {
if v := os.Getenv(envPrefix + "USER_DOMAIN_NAME"); v != "" {
cloud.AuthInfo.UserDomainName = v
}
}
if v := os.Getenv(envPrefix + "PROJECT_DOMAIN_NAME"); v != "" {
cloud.AuthInfo.ProjectDomainName = v
if cloud.AuthInfo.ApplicationCredentialID == "" {
if v := os.Getenv(envPrefix + "APPLICATION_CREDENTIAL_ID"); v != "" {
cloud.AuthInfo.ApplicationCredentialID = v
}
}
if v := os.Getenv(envPrefix + "USER_DOMAIN_ID"); v != "" {
cloud.AuthInfo.UserDomainID = v
if cloud.AuthInfo.ApplicationCredentialName == "" {
if v := os.Getenv(envPrefix + "APPLICATION_CREDENTIAL_NAME"); v != "" {
cloud.AuthInfo.ApplicationCredentialName = v
}
}
if v := os.Getenv(envPrefix + "USER_DOMAIN_NAME"); v != "" {
cloud.AuthInfo.UserDomainName = 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)
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
// Application credentials don't support scope
if !isApplicationCredential(cloud.AuthInfo) {
if !isProjectScoped(cloud.AuthInfo) {
if cloud.AuthInfo.DomainID != "" {
scope.DomainID = cloud.AuthInfo.DomainID
} else if cloud.AuthInfo.DomainName != "" {
scope.DomainName = cloud.AuthInfo.DomainName
}
} else {
scope.ProjectName = cloud.AuthInfo.ProjectName
scope.DomainID = cloud.AuthInfo.ProjectDomainID
scope.DomainName = cloud.AuthInfo.ProjectDomainName
// 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,
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
@ -591,24 +660,23 @@ func NewServiceClient(service string, opts *ClientOpts) (*gophercloud.ServiceCli
}
// Determine the region to use.
// First, see if the cloud entry has one.
// 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
}
// Next, see if one was specified in the ClientOpts.
// Finally, see if one was specified in the ClientOpts.
// If so, this takes precedence.
if v := opts.RegionName; v != "" {
region = v
}
// Finally, see if there's an environment variable.
// This should always override prior settings.
if v := os.Getenv(envPrefix + "REGION_NAME"); v != "" {
region = v
}
eo := gophercloud.EndpointOpts{
Region: region,
}
@ -722,3 +790,11 @@ func setDomainIfNeeded(cloud *Cloud) *Cloud {
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
}

View File

@ -61,6 +61,15 @@ type AuthInfo struct {
// Password is the password of the user.
Password string `yaml:"password"`
// Application Credential ID to login with.
ApplicationCredentialID string `yaml:"application_credential_id"`
// Application Credential name to login with.
ApplicationCredentialName string `yaml:"application_credential_name"`
// Application Credential secret to login with.
ApplicationCredentialSecret string `yaml:"application_credential_secret"`
// ProjectName is the common/human-readable name of a project.
// Users can be scoped to a project.
// ProjectName on its own is not enough to ensure a unique scope. It must

View File

@ -128,16 +128,13 @@ func findAndReadYAML(yamlFile string) ([]byte, error) {
}
// unix user config directory: ~/.config/openstack.
currentUser, err := user.Current()
if err != nil {
return nil, fmt.Errorf("unable to get current user: %s", err)
}
homeDir := currentUser.HomeDir
if homeDir != "" {
filename := filepath.Join(homeDir, ".config/openstack/"+yamlFile)
if ok := fileExists(filename); ok {
return ioutil.ReadFile(filename)
if currentUser, err := user.Current(); err == nil {
homeDir := currentUser.HomeDir
if homeDir != "" {
filename := filepath.Join(homeDir, ".config/openstack/"+yamlFile)
if ok := fileExists(filename); ok {
return ioutil.ReadFile(filename)
}
}
}

18
vendor/vendor.json vendored
View File

@ -811,10 +811,10 @@
"revisionTime": "2017-11-29T19:10:14Z"
},
{
"checksumSHA1": "qduT9GZUhXc00XoHEwLx16Xn9gM=",
"checksumSHA1": "90HfuOdlP9yK6xUnJCAnCrL73DQ=",
"path": "github.com/gophercloud/gophercloud",
"revision": "7112fcd50da4ea27e8d4d499b30f04eea143bec2",
"revisionTime": "2018-05-31T02:06:30Z"
"revision": "ea7289ebdf06687b792c087e2516317579d3003b",
"revisionTime": "2018-09-03T13:40:57Z"
},
{
"checksumSHA1": "b7g9TcU1OmW7e2UySYeOAmcfHpY=",
@ -961,10 +961,16 @@
"revisionTime": "2018-05-31T02:06:30Z"
},
{
"checksumSHA1": "BYHuEArNKnTCbp/LTCwQSlaIY4Y=",
"checksumSHA1": "df+06zNEC3V7qgnTaVLtH0uktmI=",
"path": "github.com/gophercloud/utils",
"revision": "a5c25e7a53a63b89622852e35d7200c85f7cbe56",
"revisionTime": "2019-01-24T19:20:22Z"
},
{
"checksumSHA1": "+lG+bluykADYk0Zzq8sdh7KIyxY=",
"path": "github.com/gophercloud/utils/openstack/clientconfig",
"revision": "d6e28a8b3199a79da5e74e3dde1eb878ff525f1a",
"revisionTime": "2018-08-06T21:57:00Z"
"revision": "a5c25e7a53a63b89622852e35d7200c85f7cbe56",
"revisionTime": "2019-01-24T19:20:22Z"
},
{
"checksumSHA1": "xSmii71kfQASGNG2C8ttmHx9KTE=",

View File

@ -78,15 +78,28 @@ builder.
- `username` or `user_id` (string) - The username or id used to connect to
the OpenStack service. If not specified, Packer will use the environment
variable `OS_USERNAME` or `OS_USERID`, if set. This is not required if
using access token instead of password or if using `cloud.yaml`.
using access token or application credential instead of password, or if using
`cloud.yaml`.
- `password` (string) - The password used to connect to the OpenStack
service. If not specified, Packer will use the environment variables
`OS_PASSWORD`, if set. This is not required if using access token instead
of password or if using `cloud.yaml`.
`OS_PASSWORD`, if set. This is not required if using access token or
application credential instead of password, or if using `cloud.yaml`.
### Optional:
- `application_credential_id` (string) - The application credential id to
use with application credential based authorization. Packer will use the
environment variable `OS_APPLICATION_CREDENTIAL_ID`, if set.
- `application_credential_name` (string) - The application credential name to
use with application credential based authorization. Packer will use the
environment variable `OS_APPLICATION_CREDENTIAL_NAME`, if set.
- `application_credential_secret` (string) - The application credential secret
to use with application credential based authorization. Packer will use the
environment variable `OS_APPLICATION_CREDENTIAL_SECRET`, if set.
- `availability_zone` (string) - The availability zone to launch the server
in. If this isn't specified, the default enforced by your OpenStack cluster
will be used. This may be required for some OpenStack clusters.
@ -432,3 +445,13 @@ Or use the following environment variables:
- `OS_AUTH_URL`
- `OS_TOKEN`
- One of `OS_TENANT_NAME` or `OS_TENANT_ID`
### Authorize Using Application Credential
To authorize with an application credential, only `identity_endpoint`,
`application_credential_id`, and `application_credential_secret` are needed.
Or use the following environment variables:
- `OS_AUTH_URL`
- `OS_APPLICATION_CREDENTIAL_ID`
- `OS_APPLICATION_CREDENTIAL_SECRET`