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  }