appengine: implement AppEngineTokenSource for 2nd gen runtimes

Go 1.11 on App Engine standard is a "second generation" runtime, and
second generation runtimes do not set the appengine build tag.
appengine_hook.go was behind the appengine build tag, meaning that
AppEngineTokenSource panicked on the go111 runtime, saying,
"AppEngineTokenSource can only be used on App Engine."

The second gen runtimes should use ComputeTokenSource, which is also
what flex does [1]. This commit does two things to remedy the situation:

1. Put the pre-existing implementation of AppEngineTokenSource behind
   the appengine build tag since it only works on first gen App Engine
   runtimes. This leaves first gen behavior unchanged.
2. Add a new implementation of AppEngineTokenSource and tag it
   !appengine. This implementation will therefore be used by second gen
   App Engine standard runtimes and App Engine flexible. It delegates
   to ComputeTokenSource.

The new AppEngineTokenSource implementation emits a log message
informing the user that AppEngineTokenSource is deprecated for second
gen runtimes and flex, instructing them to use DefaultTokenSource or
ComputeTokenSource instead. The documentation is updated to say the
same.

In this way users will not break when upgrading from Go 1.9 to Go 1.11
on App Engine but they will be nudged toward the world where App Engine
runtimes have less special behavior.

findDefaultCredentials still calls AppEngineTokenSource for first gen
runtimes and ComputeTokenSource for flex.

Fixes #334

Test: I deployed an app that uses AppEngineTokenSource to Go 1.9 and
      Go 1.11 on App Engine standard and to Go 1.11 on App Engine
      flexible and it worked in all cases. Also verified that the log
      message is present on go111 and flex.

[1] DefaultTokenSource did use ComputeTokenSource for flex but
AppEngineTokenSource did not. AppEngineTokenSource is supported on flex,
in the sense that it doesn't panic when used on flex in the way it does
when used outside App Engine. However, AppEngineTokenSource makes an API
call internally that isn't supported by default on flex, which emits a
log instructing the user to enable the compat runtime. The compat
runtimes are deprecated and deploys are blocked. This is a bad
experience. This commit has the side effect of fixing this.

Change-Id: Iab63547b410535db60dcf204782d5b6b599a4e0c
GitHub-Last-Rev: 5779afb167
GitHub-Pull-Request: golang/oauth2#341
Reviewed-on: https://go-review.googlesource.com/c/146177
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
This commit is contained in:
Michael Traver 2018-10-31 18:52:15 +00:00 committed by Brad Fitzpatrick
parent 8527f56f71
commit 5a69e67f3f
8 changed files with 136 additions and 101 deletions

View File

@ -5,85 +5,34 @@
package google package google
import ( import (
"sort"
"strings"
"sync"
"time" "time"
"golang.org/x/net/context" "golang.org/x/net/context"
"golang.org/x/oauth2" "golang.org/x/oauth2"
) )
// appengineFlex is set at init time by appengineflex_hook.go. If true, we are on App Engine Flex. // Set at init time by appengine_gen1.go. If nil, we're not on App Engine standard first generation (<= Go 1.9) or App Engine flexible.
var appengineFlex bool
// Set at init time by appengine_hook.go. If nil, we're not on App Engine.
var appengineTokenFunc func(c context.Context, scopes ...string) (token string, expiry time.Time, err error) var appengineTokenFunc func(c context.Context, scopes ...string) (token string, expiry time.Time, err error)
// Set at init time by appengine_hook.go. If nil, we're not on App Engine. // Set at init time by appengine_gen1.go. If nil, we're not on App Engine standard first generation (<= Go 1.9) or App Engine flexible.
var appengineAppIDFunc func(c context.Context) string var appengineAppIDFunc func(c context.Context) string
// AppEngineTokenSource returns a token source that fetches tokens // AppEngineTokenSource returns a token source that fetches tokens from either
// issued to the current App Engine application's service account. // the current application's service account or from the metadata server,
// If you are implementing a 3-legged OAuth 2.0 flow on App Engine // depending on the App Engine environment. See below for environment-specific
// that involves user accounts, see oauth2.Config instead. // details. If you are implementing a 3-legged OAuth 2.0 flow on App Engine that
// involves user accounts, see oauth2.Config instead.
// //
// The provided context must have come from appengine.NewContext. // First generation App Engine runtimes (<= Go 1.9):
// AppEngineTokenSource returns a token source that fetches tokens issued to the
// current App Engine application's service account. The provided context must have
// come from appengine.NewContext.
//
// Second generation App Engine runtimes (>= Go 1.11) and App Engine flexible:
// AppEngineTokenSource is DEPRECATED on second generation runtimes and on the
// flexible environment. It delegates to ComputeTokenSource, and the provided
// context and scopes are not used. Please use DefaultTokenSource (or ComputeTokenSource,
// which DefaultTokenSource will use in this case) instead.
func AppEngineTokenSource(ctx context.Context, scope ...string) oauth2.TokenSource { func AppEngineTokenSource(ctx context.Context, scope ...string) oauth2.TokenSource {
if appengineTokenFunc == nil { return appEngineTokenSource(ctx, scope...)
panic("google: AppEngineTokenSource can only be used on App Engine.")
}
scopes := append([]string{}, scope...)
sort.Strings(scopes)
return &appEngineTokenSource{
ctx: ctx,
scopes: scopes,
key: strings.Join(scopes, " "),
}
}
// aeTokens helps the fetched tokens to be reused until their expiration.
var (
aeTokensMu sync.Mutex
aeTokens = make(map[string]*tokenLock) // key is space-separated scopes
)
type tokenLock struct {
mu sync.Mutex // guards t; held while fetching or updating t
t *oauth2.Token
}
type appEngineTokenSource struct {
ctx context.Context
scopes []string
key string // to aeTokens map; space-separated scopes
}
func (ts *appEngineTokenSource) Token() (*oauth2.Token, error) {
if appengineTokenFunc == nil {
panic("google: AppEngineTokenSource can only be used on App Engine.")
}
aeTokensMu.Lock()
tok, ok := aeTokens[ts.key]
if !ok {
tok = &tokenLock{}
aeTokens[ts.key] = tok
}
aeTokensMu.Unlock()
tok.mu.Lock()
defer tok.mu.Unlock()
if tok.t.Valid() {
return tok.t, nil
}
access, exp, err := appengineTokenFunc(ts.ctx, ts.scopes...)
if err != nil {
return nil, err
}
tok.t = &oauth2.Token{
AccessToken: access,
Expiry: exp,
}
return tok.t, nil
} }

77
google/appengine_gen1.go Normal file
View File

@ -0,0 +1,77 @@
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build appengine
// This file applies to App Engine first generation runtimes (<= Go 1.9).
package google
import (
"sort"
"strings"
"sync"
"golang.org/x/net/context"
"golang.org/x/oauth2"
"google.golang.org/appengine"
)
func init() {
appengineTokenFunc = appengine.AccessToken
appengineAppIDFunc = appengine.AppID
}
// See comment on AppEngineTokenSource in appengine.go.
func appEngineTokenSource(ctx context.Context, scope ...string) oauth2.TokenSource {
scopes := append([]string{}, scope...)
sort.Strings(scopes)
return &gaeTokenSource{
ctx: ctx,
scopes: scopes,
key: strings.Join(scopes, " "),
}
}
// aeTokens helps the fetched tokens to be reused until their expiration.
var (
aeTokensMu sync.Mutex
aeTokens = make(map[string]*tokenLock) // key is space-separated scopes
)
type tokenLock struct {
mu sync.Mutex // guards t; held while fetching or updating t
t *oauth2.Token
}
type gaeTokenSource struct {
ctx context.Context
scopes []string
key string // to aeTokens map; space-separated scopes
}
func (ts *gaeTokenSource) Token() (*oauth2.Token, error) {
aeTokensMu.Lock()
tok, ok := aeTokens[ts.key]
if !ok {
tok = &tokenLock{}
aeTokens[ts.key] = tok
}
aeTokensMu.Unlock()
tok.mu.Lock()
defer tok.mu.Unlock()
if tok.t.Valid() {
return tok.t, nil
}
access, exp, err := appengineTokenFunc(ts.ctx, ts.scopes...)
if err != nil {
return nil, err
}
tok.t = &oauth2.Token{
AccessToken: access,
Expiry: exp,
}
return tok.t, nil
}

View File

@ -0,0 +1,27 @@
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build !appengine
// This file applies to App Engine second generation runtimes (>= Go 1.11) and App Engine flexible.
package google
import (
"log"
"sync"
"golang.org/x/net/context"
"golang.org/x/oauth2"
)
var logOnce sync.Once // only spam about deprecation once
// See comment on AppEngineTokenSource in appengine.go.
func appEngineTokenSource(ctx context.Context, scope ...string) oauth2.TokenSource {
logOnce.Do(func() {
log.Print("google: AppEngineTokenSource is deprecated on App Engine standard second generation runtimes (>= Go 1.11) and App Engine flexible. Please use DefaultTokenSource or ComputeTokenSource.")
})
return ComputeTokenSource("")
}

View File

@ -1,14 +0,0 @@
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build appengine appenginevm
package google
import "google.golang.org/appengine"
func init() {
appengineTokenFunc = appengine.AccessToken
appengineAppIDFunc = appengine.AppID
}

View File

@ -1,11 +0,0 @@
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build appenginevm
package google
func init() {
appengineFlex = true // Flex doesn't support appengine.AccessToken; depend on metadata server.
}

View File

@ -59,15 +59,18 @@ func findDefaultCredentials(ctx context.Context, scopes []string) (*DefaultCrede
return nil, fmt.Errorf("google: error getting credentials using well-known file (%v): %v", filename, err) return nil, fmt.Errorf("google: error getting credentials using well-known file (%v): %v", filename, err)
} }
// Third, if we're on Google App Engine use those credentials. // Third, if we're on a Google App Engine standard first generation runtime (<= Go 1.9)
if appengineTokenFunc != nil && !appengineFlex { // use those credentials. App Engine standard second generation runtimes (>= Go 1.11)
// and App Engine flexible use ComputeTokenSource and the metadata server.
if appengineTokenFunc != nil {
return &DefaultCredentials{ return &DefaultCredentials{
ProjectID: appengineAppIDFunc(ctx), ProjectID: appengineAppIDFunc(ctx),
TokenSource: AppEngineTokenSource(ctx, scopes...), TokenSource: AppEngineTokenSource(ctx, scopes...),
}, nil }, nil
} }
// Fourth, if we're on Google Compute Engine use the metadata server. // Fourth, if we're on Google Compute Engine, an App Engine standard second generation runtime,
// or App Engine flexible, use the metadata server.
if metadata.OnGCE() { if metadata.OnGCE() {
id, _ := metadata.ProjectID() id, _ := metadata.ProjectID()
return &DefaultCredentials{ return &DefaultCredentials{

View File

@ -40,8 +40,10 @@ type DefaultCredentials = Credentials
// 2. A JSON file in a location known to the gcloud command-line tool. // 2. A JSON file in a location known to the gcloud command-line tool.
// On Windows, this is %APPDATA%/gcloud/application_default_credentials.json. // On Windows, this is %APPDATA%/gcloud/application_default_credentials.json.
// On other systems, $HOME/.config/gcloud/application_default_credentials.json. // On other systems, $HOME/.config/gcloud/application_default_credentials.json.
// 3. On Google App Engine it uses the appengine.AccessToken function. // 3. On Google App Engine standard first generation runtimes (<= Go 1.9) it uses
// 4. On Google Compute Engine and Google App Engine Managed VMs, it fetches // the appengine.AccessToken function.
// 4. On Google Compute Engine, Google App Engine standard second generation runtimes
// (>= Go 1.11), and Google App Engine flexible environment, it fetches
// credentials from the metadata server. // credentials from the metadata server.
// (In this final case any provided scopes are ignored.) // (In this final case any provided scopes are ignored.)
func FindDefaultCredentials(ctx context.Context, scopes ...string) (*Credentials, error) { func FindDefaultCredentials(ctx context.Context, scopes ...string) (*Credentials, error) {

View File

@ -35,8 +35,10 @@ type DefaultCredentials struct {
// 2. A JSON file in a location known to the gcloud command-line tool. // 2. A JSON file in a location known to the gcloud command-line tool.
// On Windows, this is %APPDATA%/gcloud/application_default_credentials.json. // On Windows, this is %APPDATA%/gcloud/application_default_credentials.json.
// On other systems, $HOME/.config/gcloud/application_default_credentials.json. // On other systems, $HOME/.config/gcloud/application_default_credentials.json.
// 3. On Google App Engine it uses the appengine.AccessToken function. // 3. On Google App Engine standard first generation runtimes (<= Go 1.9) it uses
// 4. On Google Compute Engine and Google App Engine Managed VMs, it fetches // the appengine.AccessToken function.
// 4. On Google Compute Engine, Google App Engine standard second generation runtimes
// (>= Go 1.11), and Google App Engine flexible environment, it fetches
// credentials from the metadata server. // credentials from the metadata server.
// (In this final case any provided scopes are ignored.) // (In this final case any provided scopes are ignored.)
func FindDefaultCredentials(ctx context.Context, scopes ...string) (*DefaultCredentials, error) { func FindDefaultCredentials(ctx context.Context, scopes ...string) (*DefaultCredentials, error) {