blob: 7bebc4dc6c43d8fc167a5bb5cc277350baa19468 [file] [log] [blame]
// Copyright 2017 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 flex exposes gaemiddleware Environments for AppEngine's Flex
// environment.
package flex
import (
"context"
"net/http"
"strings"
"go.chromium.org/luci/common/data/caching/lru"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/common/tsmon/monitor"
"go.chromium.org/luci/common/tsmon/target"
"go.chromium.org/luci/config/appengine/gaeconfig"
"go.chromium.org/luci/server/auth"
"go.chromium.org/luci/server/auth/authdb"
"go.chromium.org/luci/server/pprof"
"go.chromium.org/luci/server/router"
"go.chromium.org/luci/server/tsmon"
authClient "go.chromium.org/luci/appengine/gaeauth/client"
gaeauth "go.chromium.org/luci/appengine/gaeauth/server"
"go.chromium.org/luci/appengine/gaeauth/server/gaesigner"
"go.chromium.org/luci/appengine/gaemiddleware"
gaetsmon "go.chromium.org/luci/appengine/tsmon"
"go.chromium.org/luci/gae/impl/cloud"
"go.chromium.org/luci/gae/service/datastore"
"go.chromium.org/luci/gae/service/info"
"cloud.google.com/go/compute/metadata"
)
var (
// ListeningAddr is a address to bind the listening socket to.
ListeningAddr string
// globalFlex is the global luci/gae cloud Flex services definition.
globalFlex *cloud.Flex
// globalFlexConfig is a process-wide Flex environment configuration.
globalFlexConfig *cloud.Config
// globalAuthConfig is configuration of the server/auth library.
//
// It specifies concrete GAE-based implementations for various interfaces
// used by the library.
//
// It is indirectly stateful (since NewDBCache returns a stateful object that
// keeps AuthDB cache in local memory), and thus it's defined as a long living
// global variable.
//
// Used in prod contexts only.
globalAuthConfig = auth.Config{
DBProvider: authdb.NewDBCache(gaeauth.GetAuthDB),
Signer: gaesigner.Signer{},
AccessTokenProvider: authClient.GetAccessToken,
AnonymousTransport: func(context.Context) http.RoundTripper { return http.DefaultTransport },
IsDevMode: !metadata.OnGCE(),
}
// globalTsMonState holds configuration and state related to time series
// monitoring.
//
// TODO(vadimsh): We can flush asynchronously on Flex. Unfortunately, the
// request context is canceled as soon as the request ends, killing all
// outliving goroutines. We either need a way to detach it, or run the flush
// using the global context (the logging is moot in this case, since we'll
// have no trace ID for logs).
globalTsMonState = &tsmon.State{
CustomMonitor: tsmonDebugMonitor(),
Target: func(ctx context.Context) target.Task {
return target.Task{
DataCenter: "appengine",
ServiceName: info.AppID(ctx),
JobName: info.ModuleName(ctx),
HostName: strings.SplitN(info.VersionID(ctx), ".", 2)[0],
}
},
InstanceID: info.InstanceID,
TaskNumAllocator: gaetsmon.DatastoreTaskNumAllocator{},
FlushInMiddleware: true,
}
)
// tsmonDebugMonitor returns debug monitor when running on dev server.
func tsmonDebugMonitor() monitor.Monitor {
if !metadata.OnGCE() {
return monitor.NewDebugMonitor("")
}
return nil
}
func init() {
if metadata.OnGCE() {
ListeningAddr = ":8080" // assume real Flex
} else {
ListeningAddr = "127.0.0.1:8080" // dev environment
}
}
// ReadOnlyFlex is an Environment designed for cooperative Flex support
// environments.
var ReadOnlyFlex = gaemiddleware.Environment{
MemcacheAvailable: false,
DSReadOnly: true,
DSReadOnlyPredicate: func(k *datastore.Key) (isRO bool) {
// HACK(vadimsh): This is needed to allow tsmon middleware to bypass
// read-only filter on the datastore. It needs writable access to the
// datastore for DatastoreTaskNumAllocator to work. It doesn't rely on
// dscache, and thus it is safe to mutate datastore from Flex side.
return k.Namespace() != gaetsmon.DatastoreNamespace
},
Prepare: func(ctx context.Context) {
globalFlex = &cloud.Flex{
Cache: lru.New(65535),
}
var err error
if globalFlexConfig, err = globalFlex.Configure(ctx); err != nil {
panic(errors.Annotate(err, "could not create Flex config").Err())
}
},
WithInitialRequest: func(ctx context.Context, req *http.Request) context.Context {
// Install our Cloud services.
if globalFlexConfig == nil {
// This can happen when Prepare fails.
panic("global Flex config is not initialized")
}
return globalFlexConfig.Use(ctx, globalFlex.Request(ctx, req))
},
WithConfig: gaeconfig.Use,
WithAuth: func(ctx context.Context) context.Context {
return auth.Initialize(ctx, &globalAuthConfig)
},
ExtraMiddleware: router.NewMiddlewareChain(
flexFoundationMiddleware,
globalTsMonState.Middleware,
),
ExtraHandlers: func(r *router.Router, base router.MiddlewareChain) {
gaeauth.InstallHandlers(r, base)
pprof.InstallHandlers(r, base)
// Install a handler for basic health checking. We respond with HTTP 200 to
// indicate that we're always healthy.
r.GET("/_ah/health", router.MiddlewareChain{},
func(c *router.Context) { c.Writer.WriteHeader(http.StatusOK) })
},
}
func flexFoundationMiddleware(c *router.Context, next router.Handler) {
sr := cloud.ScopedRequestHandler{
CapturePanics: true,
}
sr.Handle(c.Context, c.Writer, func(ctx context.Context, rw http.ResponseWriter) {
c.Context = ctx
c.Writer = rw
next(c)
})
}
// WithGlobal returns a Context that is not attached to a specific request.
func WithGlobal(ctx context.Context) context.Context {
return ReadOnlyFlex.With(ctx, &http.Request{})
}