blob: 6e7b75dfd8f344d73fb0f1c3f8e37d3b2a26fb56 [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package main
import (
"context"
"regexp"
"strconv"
"github.com/golang/protobuf/proto"
"go.chromium.org/luci/common/clock"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/common/logging"
"go.chromium.org/luci/common/sync/parallel"
ds "go.chromium.org/luci/gae/service/datastore"
tq "go.chromium.org/luci/gae/service/taskqueue"
"go.chromium.org/luci/grpc/grpcutil"
admin "infra/tricium/api/admin/v1"
tricium "infra/tricium/api/v1"
"infra/tricium/appengine/common"
"infra/tricium/appengine/common/config"
"infra/tricium/appengine/common/track"
)
// TriciumServer represents the Tricium pRPC server.
type TriciumServer struct{}
// Server instance to use within this module/package.
var server = &TriciumServer{}
// Regular expression describing a valid change ID (https://goo.gl/U49fRn).
var changeIDPattern = regexp.MustCompile(".+~.+~I[0-9a-fA-F]{40}.*")
// Analyze processes one Analyze request to Tricium.
//
// Launches a workflow customized to the project and listed paths. The run ID
// in the response can be used to track the progress and results of the request
// via the Tricium UI.
//
// Requests are not deduped, so multiple identical requests will get separate
// runs with separate run IDs, and possibly post duplicate comments.
func (r *TriciumServer) Analyze(c context.Context, req *tricium.AnalyzeRequest) (res *tricium.AnalyzeResponse, err error) {
defer func() {
err = grpcutil.GRPCifyAndLogErr(c, err)
}()
if err = validateAnalyzeRequest(c, req); err != nil {
return nil, err
}
runID, err := analyzeWithAuth(c, req, config.LuciConfigServer)
if err != nil {
return nil, errors.Annotate(err, "invalid request").
Tag(grpcutil.InvalidArgumentTag).Err()
}
logging.Fields{
"runID": runID,
}.Infof(c, "Analyze request processed.")
return &tricium.AnalyzeResponse{RunId: runID}, nil
}
// validateAnalyzeRequest returns an error if the request is invalid.
//
// The returned error should be tagged for gRPC by the caller.
func validateAnalyzeRequest(c context.Context, req *tricium.AnalyzeRequest) error {
if req.Project == "" {
return errors.Reason("missing project").Err()
}
if len(req.Files) == 0 {
return errors.Reason("missing paths").Err()
}
switch source := req.Source.(type) {
case *tricium.AnalyzeRequest_GitCommit:
gc := source.GitCommit
if gc.Url == "" {
return errors.Reason("missing git repo URL").Err()
}
if gc.Ref == "" {
return errors.Reason("missing git ref").Err()
}
case *tricium.AnalyzeRequest_GerritRevision:
gr := source.GerritRevision
if gr.Host == "" {
return errors.Reason("missing Gerrit host").Err()
}
if gr.Project == "" {
return errors.Reason("missing Gerrit project").Err()
}
if gr.Change == "" {
return errors.Reason("missing Gerrit change ID").Err()
}
if !changeIDPattern.MatchString(gr.Change) {
return errors.Reason("invalid Gerrit change ID: " + gr.Change).Err()
}
if gr.GitUrl == "" {
return errors.Reason("missing git repo URL for Gerrit change").Err()
}
if gr.GitRef == "" {
return errors.Reason("missing Gerrit revision git ref").Err()
}
case nil:
return errors.Reason("missing source").Err()
default:
return errors.Reason("unexpected source type").Err()
}
return nil
}
// analyzeWithAuth wraps 'analyze' in an auth check.
//
// This wrapper is used by the Analyze RPC call and the unwrapped method is
// used by requests coming in via the internal analyze queue.
func analyzeWithAuth(c context.Context, req *tricium.AnalyzeRequest, cp config.ProviderAPI) (string, error) {
pc, err := cp.GetProjectConfig(c, req.Project)
if err != nil {
return "", errors.Annotate(err, "failed to get project config %q", req.Project).
Tag(grpcutil.InternalTag).Err()
}
ok, err := tricium.CanRequest(c, pc)
if err != nil {
return "", errors.Annotate(err, "failed to authorize").
Tag(grpcutil.InternalTag).Err()
}
if !ok {
return "", errors.Reason("no request access for project %q", req.Project).
Tag(grpcutil.PermissionDeniedTag).Err()
}
runID, err := analyze(c, req, cp)
if err != nil {
return runID, errors.Annotate(err, "failed to analyze").
Tag(grpcutil.InternalTag).Err()
}
return runID, nil
}
func analyze(c context.Context, req *tricium.AnalyzeRequest, cp config.ProviderAPI) (string, error) {
// Construct the track.AnalyzeRequest entity.
pc, err := cp.GetProjectConfig(c, req.Project)
if err != nil {
return "", errors.Annotate(err, "failed to get project config").Err()
}
var paths []string
var files []tricium.Data_File
for _, file := range req.Files {
paths = append(paths, file.Path)
files = append(files, *file)
}
request := &track.AnalyzeRequest{
Received: clock.Now(c).UTC(),
Project: req.Project,
Files: files,
}
repo := tricium.LookupRepoDetails(pc, req)
if repo == nil {
return "", errors.Reason("failed to find matching repo in project config: %v", req.Project).Err()
}
if source := req.GetGerritRevision(); source != nil {
request.GerritHost = source.Host
request.GerritProject = source.Project
request.GerritChange = source.Change
request.GitURL = source.GitUrl
request.GitRef = source.GitRef
request.GerritReportingDisabled = repo.DisableReporting
request.CommitMessage = source.CommitMessage
} else if source := req.GetGitCommit(); source != nil {
request.GitURL = source.Url
request.GitRef = source.Ref
} else {
return "", errors.Reason("unsupported request source").Err()
}
requestRes := &track.AnalyzeRequestResult{
ID: 1,
State: tricium.State_PENDING,
}
lr := &admin.LaunchRequest{
Project: req.Project,
Files: req.Files,
GitUrl: request.GitURL,
GitRef: request.GitRef,
CommitMessage: request.CommitMessage,
}
// This is a cross-group transaction because first AnalyzeRequest is
// stored to get the ID, and then AnalyzeRequestResult is stored, with
// the previously added AnalyzeRequest entity as parent.
err = ds.RunInTransaction(c, func(c context.Context) (err error) {
// Add request entity to get ID.
if err := ds.Put(c, request); err != nil {
return errors.Annotate(err, "failed to store AnalyzeRequest entity").Err()
}
// We can do a few things in parallel when starting an analyze request.
return parallel.FanOutIn(func(taskC chan<- func() error) {
// Add AnalyzeRequestResult entity for request status tracking.
taskC <- func() error {
requestRes.Parent = ds.KeyForObj(c, request)
if err := ds.Put(c, requestRes); err != nil {
return errors.Annotate(err, "failed to store AnalyzeRequestResult entity").Err()
}
return nil
}
// Launch workflow, enqueue launch request.
taskC <- func() error {
lr.RunId = request.ID
t := tq.NewPOSTTask("/launcher/internal/launch", nil)
b, err := proto.Marshal(lr)
if err != nil {
return errors.Annotate(err, "failed to enqueue launch request").Err()
}
t.Payload = b
return tq.Add(c, common.LauncherQueue, t)
}
// Map Gerrit change ID to run ID.
taskC <- func() error {
// Nothing to do if there isn't a Gerrit consumer.
if req.GetGerritRevision() == nil {
return nil
}
g := &GerritChangeToRunID{
ID: gerritMappingID(request.GerritHost, request.GerritProject, request.GerritChange, request.GitRef),
RunID: request.ID,
}
if err := ds.Put(c, g); err != nil {
return errors.Annotate(err, "failed to store GerritChangeIDtoRunID entity").Err()
}
return nil
}
})
}, nil)
if err != nil {
return "", errors.Annotate(err, "failed to track and launch request").Err()
}
return strconv.FormatInt(request.ID, 10), nil
}