go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/bisection/main.go (about) 1 // Copyright 2022 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package main implements the App Engine based HTTP server to handle request 16 // to LUCI Bisection 17 package main 18 19 import ( 20 "context" 21 "flag" 22 "fmt" 23 "net/http" 24 25 "github.com/golang/protobuf/proto" 26 // Store auth sessions in the datastore. 27 "google.golang.org/grpc/codes" 28 "google.golang.org/grpc/status" 29 30 "go.chromium.org/luci/auth/identity" 31 "go.chromium.org/luci/bisection/bqexporter" 32 "go.chromium.org/luci/bisection/compilefailureanalysis/cancelanalysis" 33 "go.chromium.org/luci/bisection/compilefailuredetection" 34 "go.chromium.org/luci/bisection/culpritaction/revertculprit" 35 "go.chromium.org/luci/bisection/culpritverification" 36 "go.chromium.org/luci/bisection/internal/config" 37 "go.chromium.org/luci/bisection/internal/lucianalysis" 38 "go.chromium.org/luci/bisection/metrics" 39 pb "go.chromium.org/luci/bisection/proto/v1" 40 "go.chromium.org/luci/bisection/pubsub" 41 "go.chromium.org/luci/bisection/server" 42 "go.chromium.org/luci/bisection/testfailureanalysis/bisection" 43 "go.chromium.org/luci/bisection/testfailuredetection" 44 "go.chromium.org/luci/bisection/throttle" 45 "go.chromium.org/luci/common/errors" 46 "go.chromium.org/luci/common/logging" 47 "go.chromium.org/luci/config/server/cfgmodule" 48 "go.chromium.org/luci/grpc/prpc" 49 luciserver "go.chromium.org/luci/server" 50 "go.chromium.org/luci/server/auth" 51 "go.chromium.org/luci/server/auth/openid" 52 "go.chromium.org/luci/server/cron" 53 "go.chromium.org/luci/server/gaeemulation" 54 "go.chromium.org/luci/server/module" 55 "go.chromium.org/luci/server/router" 56 "go.chromium.org/luci/server/secrets" 57 "go.chromium.org/luci/server/tq" 58 ) 59 60 const ( 61 ACCESS_GROUP = "luci-bisection-access" 62 ACCESS_GROUP_FOR_BOT = "luci-bisection-bot-access" 63 ) 64 65 func checkAPIAccess(ctx context.Context, methodName string, req proto.Message) (context.Context, error) { 66 switch yes, err := auth.IsMember(ctx, ACCESS_GROUP); { 67 case err != nil: 68 return nil, status.Errorf(codes.Internal, "error when checking group membership") 69 case !yes: 70 return nil, status.Errorf(codes.PermissionDenied, "%s does not have access to method %s of GoFindit", auth.CurrentIdentity(ctx), methodName) 71 default: 72 return ctx, nil 73 } 74 } 75 76 func checkBotAPIAccess(ctx context.Context, methodName string, req proto.Message) (context.Context, error) { 77 switch yes, err := auth.IsMember(ctx, ACCESS_GROUP_FOR_BOT); { 78 case err != nil: 79 return nil, status.Errorf(codes.Internal, "error when checking group membership for bot") 80 case !yes: 81 return nil, status.Errorf(codes.PermissionDenied, "%s does not have access to method %s of GoFindit", auth.CurrentIdentity(ctx), methodName) 82 default: 83 return ctx, nil 84 } 85 } 86 87 func main() { 88 modules := []module.Module{ 89 cfgmodule.NewModuleFromFlags(), 90 cron.NewModuleFromFlags(), 91 gaeemulation.NewModuleFromFlags(), 92 secrets.NewModuleFromFlags(), 93 tq.NewModuleFromFlags(), 94 } 95 luciAnalysisProject := "" 96 flag.StringVar( 97 &luciAnalysisProject, "luci-analysis-project", luciAnalysisProject, `the GCP project id of LUCI analysis.`, 98 ) 99 uiRedirectURL := "luci-milo-dev.appspot.com/ui/bisection" 100 flag.StringVar( 101 &uiRedirectURL, "ui-redirect-url", uiRedirectURL, `the redirect url for the frontend.`, 102 ) 103 104 luciserver.Main(nil, modules, func(srv *luciserver.Server) error { 105 // Redirect the frontend to Milo. 106 srv.Routes.NotFound(nil, func(ctx *router.Context) { 107 url := fmt.Sprintf("https://%s%s", uiRedirectURL, ctx.Request.URL.Path) 108 http.Redirect(ctx.Writer, ctx.Request, url, http.StatusFound) 109 }) 110 111 // Pubsub handler 112 pubsubMwc := router.NewMiddlewareChain( 113 auth.Authenticate(&openid.GoogleIDTokenAuthMethod{ 114 AudienceCheck: openid.AudienceMatchesHost, 115 }), 116 ) 117 pusherID := identity.Identity(fmt.Sprintf("user:buildbucket-pubsub@%s.iam.gserviceaccount.com", srv.Options.CloudProject)) 118 119 srv.Routes.POST("/_ah/push-handlers/buildbucket", pubsubMwc, func(ctx *router.Context) { 120 if got := auth.CurrentIdentity(ctx.Request.Context()); got != pusherID { 121 logging.Errorf(ctx.Request.Context(), "Expecting ID token of %q, got %q", pusherID, got) 122 ctx.Writer.WriteHeader(http.StatusForbidden) 123 } else { 124 pubsub.BuildbucketPubSubHandler(ctx) 125 } 126 }) 127 pg := &LUCIAnalysisProject{ 128 DefaultProject: luciAnalysisProject, 129 } 130 ac, err := lucianalysis.NewClient(srv.Context, srv.Options.CloudProject, pg.Project) 131 if err != nil { 132 return errors.Annotate(err, "creating analysis client").Err() 133 } 134 135 // Installs gRPC service. 136 pb.RegisterAnalysesServer(srv, &pb.DecoratedAnalyses{ 137 Service: &server.AnalysesServer{ 138 AnalysisClient: ac, 139 }, 140 Prelude: checkAPIAccess, 141 }) 142 143 // Installs gRPC service to communicate with recipes 144 pb.RegisterBotUpdatesServer(srv, &pb.DecoratedBotUpdates{ 145 Service: &server.BotUpdatesServer{}, 146 Prelude: checkBotAPIAccess, 147 }) 148 149 // Register pPRC servers. 150 srv.ConfigurePRPC(func(s *prpc.Server) { 151 // Allow cross-origin calls. 152 s.AccessControl = prpc.AllowOriginAll 153 // TODO(crbug/1082369): Remove this workaround once field masks can be decoded. 154 s.HackFixFieldMasksForJSON = true 155 }) 156 157 // GAE crons 158 cron.RegisterHandler("update-config", config.UpdateProjects) 159 cron.RegisterHandler("collect-global-metrics", metrics.CollectGlobalMetrics) 160 cron.RegisterHandler("throttle-bisection", throttle.CronHandler) 161 cron.RegisterHandler("export-test-analyses", bqexporter.ExportTestAnalyses) 162 cron.RegisterHandler("ensure-views", bqexporter.EnsureViews) 163 164 // Task queues 165 compilefailuredetection.RegisterTaskClass() 166 if err := revertculprit.RegisterTaskClass(srv, pg.Project); err != nil { 167 return errors.Annotate(err, "register revert culprit").Err() 168 } 169 cancelanalysis.RegisterTaskClass() 170 culpritverification.RegisterTaskClass() 171 if err := testfailuredetection.RegisterTaskClass(srv, pg.Project); err != nil { 172 return errors.Annotate(err, "register test failure detection").Err() 173 } 174 if err := bisection.RegisterTaskClass(srv, pg.Project); err != nil { 175 return errors.Annotate(err, "register test failure bisector").Err() 176 } 177 178 return nil 179 }) 180 } 181 182 type LUCIAnalysisProject struct { 183 DefaultProject string 184 } 185 186 // Project return LUCI Analysis project given a LUCI Project. 187 // In normal cases, it will just check the app.yaml for the LUCI Analysis project. 188 // However, for the case of Chrome, where we don't have dev data, we need to 189 // query from LUCI Analysis prod instead. 190 func (pg *LUCIAnalysisProject) Project(luciProject string) string { 191 // TODO (nqmtuan): Remove this when we finish testing Chrome. 192 if luciProject == "chrome" { 193 return "luci-analysis" 194 } 195 return pg.DefaultProject 196 }