blob: bbc99e0830ac67d84cc9c21d8a31bbdaba06fdb4 [file] [log] [blame]
// Copyright 2015 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package prod
import (
"fmt"
"net/http"
"net/http/cookiejar"
"net/url"
"strings"
"go.chromium.org/gae/service/urlfetch"
"golang.org/x/net/context"
gOAuth "golang.org/x/oauth2/google"
"google.golang.org/appengine"
"google.golang.org/appengine/remote_api"
)
// RemoteAPIScopes is the set of OAuth2 scopes needed for Remote API access.
var RemoteAPIScopes = []string{
"https://www.googleapis.com/auth/appengine.apis",
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/cloud.platform",
}
type key int
var (
prodStateKey = "contains the current *prodState"
probeCacheKey = "contains the current *infoProbeCache"
)
// getAEContext retrieves the raw "google.golang.org/appengine" compatible
// Context.
//
// This is an independent Context chain from `c`. In an attempt to maintain user
// expectations, the deadline of `c` is transferred to the returned Context,
// RPCs. Cancelation is not transferred.
func getAEContext(c context.Context) context.Context {
ps := getProdState(c)
return ps.context(c)
}
func setupAECtx(c, aeCtx context.Context) context.Context {
c = withProdState(c, prodState{
ctx: aeCtx,
noTxnCtx: aeCtx,
})
return useModule(useMail(useUser(useURLFetch(useRDS(useMC(useTQ(useGI(useLogging(c)))))))))
}
// Use adds production implementations for all the gae services to the
// context. The implementations are all backed by the real appengine SDK
// functionality.
//
// The services added are:
// - github.com/luci-go/common/logging
// - go.chromium.org/gae/service/datastore
// - go.chromium.org/gae/service/info
// - go.chromium.org/gae/service/mail
// - go.chromium.org/gae/service/memcache
// - go.chromium.org/gae/service/module
// - go.chromium.org/gae/service/taskqueue
// - go.chromium.org/gae/service/urlfetch
// - go.chromium.org/gae/service/user
//
// These can be retrieved with the <service>.Get functions.
//
// It is important to note that this DOES NOT install the AppEngine SDK into the
// supplied Context. In general, using the raw AppEngine SDK to access a service
// that is covered by luci/gae is dangerous, leading to a number of potential
// pitfalls including inconsistent transaction management and data corruption.
//
// Users who wish to access the raw AppEngine SDK must derive their own
// AppEngine Context at their own risk.
func Use(c context.Context, r *http.Request) context.Context {
return setupAECtx(c, appengine.NewContext(r))
}
// UseRemote is the same as Use, except that it lets you attach a context to
// a remote host using the Remote API feature. See the docs for the
// prerequisites.
//
// docs: https://cloud.google.com/appengine/docs/go/tools/remoteapi
//
// inOutCtx will be replaced with the new, derived context, if err is nil,
// otherwise it's unchanged and continues to be safe-to-use.
//
// If client is nil, this will use create a new client, and will try to be
// clever about it:
// * If you're creating a remote context FROM AppEngine, this will use
// urlfetch.Transport. This can be used to allow app-to-app remote_api
// control.
//
// * If host starts with "localhost", this will create a regular http.Client
// with a cookiejar, and call the _ah/login API to log in as an admin with
// the user "admin@example.com".
//
// * Otherwise, it will create a Google OAuth2 client with the following scopes:
// - "https://www.googleapis.com/auth/appengine.apis"
// - "https://www.googleapis.com/auth/userinfo.email"
// - "https://www.googleapis.com/auth/cloud.platform"
//
// It is important to note that this DOES NOT install the AppEngine SDK into the
// supplied Context. See the warning in Use for more information.
func UseRemote(inOutCtx *context.Context, host string, client *http.Client) (err error) {
if client == nil {
aeCtx := getAEContext(*inOutCtx)
if strings.HasPrefix(host, "localhost") {
transp := http.DefaultTransport
if aeCtx != nil {
transp = urlfetch.Get(*inOutCtx)
}
client = &http.Client{Transport: transp}
client.Jar, err = cookiejar.New(nil)
if err != nil {
return
}
u := fmt.Sprintf("http://%s/_ah/login?%s", host, url.Values{
"email": {"admin@example.com"},
"admin": {"True"},
"action": {"Login"},
}.Encode())
var rsp *http.Response
rsp, err = client.Get(u)
if err != nil {
return
}
defer rsp.Body.Close()
} else {
if aeCtx == nil {
aeCtx = context.Background()
}
client, err = gOAuth.DefaultClient(aeCtx, RemoteAPIScopes...)
if err != nil {
return
}
}
}
aeCtx, err := remote_api.NewRemoteContext(host, client)
if err != nil {
return
}
*inOutCtx = setupAECtx(*inOutCtx, aeCtx)
return nil
}
// prodState is the current production state.
type prodState struct {
// ctx is the current derived GAE context.
ctx context.Context
// noTxnCtx is a Context maintained alongside ctx. When a transaction is
// entered, ctx will be updated, but noTxnCtx will not, allowing extra-
// transactional Context access.
noTxnCtx context.Context
// inTxn if true if this is in a transaction, false otherwise.
inTxn bool
}
func getProdState(c context.Context) prodState {
if v := c.Value(&prodStateKey).(*prodState); v != nil {
return *v
}
return prodState{}
}
func withProdState(c context.Context, ps prodState) context.Context {
return context.WithValue(c, &prodStateKey, &ps)
}
// context returns the current AppEngine-bound Context. Prior to returning,
// the deadline from "c" (if any) is applied.
//
// Note that this does not (currently) apply any other Done state or propagate
// cancellation from "c".
//
// Tracking at:
// https://go.chromium.org/gae/issues/59
func (ps *prodState) context(c context.Context) context.Context {
aeCtx := ps.ctx
if aeCtx == nil {
return nil
}
if deadline, ok := c.Deadline(); ok {
aeCtx, _ = context.WithDeadline(aeCtx, deadline)
}
return aeCtx
}