blob: be09152b9f9708b267a81c9b9477bd975248661b [file] [log] [blame]
// Copyright 2020 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 main is the main entry point for the app.
package main
import (
"context"
"fmt"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"go.chromium.org/luci/common/data/rand/mathrand"
"go.chromium.org/luci/common/logging"
"go.chromium.org/luci/common/proto/access"
"go.chromium.org/luci/grpc/prpc"
"go.chromium.org/luci/server"
"go.chromium.org/luci/server/gaeemulation"
"go.chromium.org/luci/server/module"
"go.chromium.org/luci/server/router"
"go.chromium.org/luci/server/tq"
// Enable datastore transactional tasks support.
_ "go.chromium.org/luci/server/tq/txn/datastore"
"go.chromium.org/luci/buildbucket/appengine/rpc"
pb "go.chromium.org/luci/buildbucket/proto"
)
// isBeefy returns whether the request was intended for the beefy service.
func isBeefy(req *http.Request) bool {
return strings.Contains(req.Host, "beefy")
}
// isDev returns whether the request was intended for the dev instance.
func isDev(req *http.Request) bool {
return strings.HasSuffix(req.Host, "-dev.appspot.com")
}
func main() {
mods := []module.Module{
gaeemulation.NewModuleFromFlags(),
tq.NewModuleFromFlags(),
}
server.Main(nil, mods, func(srv *server.Server) error {
// Proxy buildbucket.v2.Builds pRPC requests back to the Python
// service in order to achieve a programmatic traffic split.
// Because of the way dispatch routes work, requests are proxied
// to a copy of the Python service hosted at a different path.
// TODO(crbug/1042991): Remove the proxy once the go service handles all traffic.
pythonURL, err := url.Parse(fmt.Sprintf("https://default-dot-%s.appspot.com/python", srv.Options.CloudProject))
if err != nil {
panic(err)
}
beefyURL, err := url.Parse(fmt.Sprintf("https://beefy-dot-%s.appspot.com/python", srv.Options.CloudProject))
if err != nil {
panic(err)
}
prx := httputil.NewSingleHostReverseProxy(pythonURL)
prx.Director = func(req *http.Request) {
target := pythonURL
if isBeefy(req) {
target = beefyURL
}
// According to net.Request documentation, setting Host is unnecessary
// because URL.Host is supposed to be used for outbound requests.
// However, on GAE, it seems that req.Host is incorrectly used.
req.Host = target.Host
req.URL.Scheme = target.Scheme
req.URL.Host = target.Host
req.URL.Path = fmt.Sprintf("%s%s", target.Path, req.URL.Path)
}
// makeOverride returns a prpc.Override which allows the given percentage of requests
// through to this service, proxying the remainder to Python.
makeOverride := func(prodPct, devPct int) func(*router.Context) bool {
return func(ctx *router.Context) bool {
// TODO(crbug/1090540): remove env k-v
ctx.Context = context.WithValue(ctx.Context, "env", "Prod")
pct := prodPct
if isDev(ctx.Request) {
pct = devPct
// TODO(crbug/1090540): remove env k-v
ctx.Context = context.WithValue(ctx.Context, "env", "Dev")
}
switch val := ctx.Request.Header.Get("Should-Proxy"); val {
case "true":
pct = 0
logging.Debugf(ctx.Context, "request demanded to be proxied")
case "false":
pct = 100
logging.Debugf(ctx.Context, "request demanded not to be proxied")
}
if mathrand.Intn(ctx.Context, 100) < pct {
return false
}
target := pythonURL
if isBeefy(ctx.Request) {
target = beefyURL
}
logging.Debugf(ctx.Context, "proxying request to %s", target)
prx.ServeHTTP(ctx.Writer, ctx.Request)
return true
}
}
srv.PRPC.AccessControl = prpc.AllowOriginAll
access.RegisterAccessServer(srv.PRPC, &access.UnimplementedAccessServer{})
pb.RegisterBuildsServer(srv.PRPC, rpc.NewBuilds())
pb.RegisterBuildersServer(srv.PRPC, rpc.NewBuilders())
// TODO(crbug/1082369): Remove this workaround once field masks can be decoded.
srv.PRPC.HackFixFieldMasksForJSON = true
// makeOverride(prod % -> Go, dev % -> Go).
srv.PRPC.RegisterOverride("buildbucket.v2.Builds", "CancelBuild", makeOverride(0, 0))
srv.PRPC.RegisterOverride("buildbucket.v2.Builds", "ScheduleBuild", makeOverride(0, 0))
return nil
})
}