go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/buildbucket/appengine/internal/redirect/redirect.go (about)

     1  // Copyright 2024 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 redirect handles the URLs which need a redirection.
    16  package redirect
    17  
    18  import (
    19  	"context"
    20  	"fmt"
    21  	"net/http"
    22  	"strconv"
    23  	"strings"
    24  
    25  	"google.golang.org/grpc/status"
    26  
    27  	"go.chromium.org/luci/auth/identity"
    28  	"go.chromium.org/luci/common/errors"
    29  	"go.chromium.org/luci/common/logging"
    30  	"go.chromium.org/luci/gae/service/datastore"
    31  	"go.chromium.org/luci/grpc/grpcutil"
    32  	"go.chromium.org/luci/server/auth"
    33  	"go.chromium.org/luci/server/router"
    34  
    35  	"go.chromium.org/luci/buildbucket/appengine/common"
    36  	"go.chromium.org/luci/buildbucket/appengine/internal/config"
    37  	"go.chromium.org/luci/buildbucket/appengine/internal/perm"
    38  	"go.chromium.org/luci/buildbucket/appengine/model"
    39  	"go.chromium.org/luci/buildbucket/bbperms"
    40  	pb "go.chromium.org/luci/buildbucket/proto"
    41  )
    42  
    43  // InstallHandlers adds routes handlers which need redirections.
    44  // TODO(b/326502532): may worth investing time on a possible future improvement
    45  // to make these handers more lightweight as said in b/326502532.
    46  func InstallHandlers(r *router.Router, mw router.MiddlewareChain) {
    47  	r.GET("/build/*BuildID", mw, handleViewBuild)
    48  	r.GET("/builds/*BuildID", mw, handleViewBuild)
    49  	r.GET("/log/:BuildID/*StepName", mw, handleViewBuild)
    50  }
    51  
    52  func handleViewLog(c *router.Context) {
    53  	ctx := c.Request.Context()
    54  	bID, err := strconv.Atoi(c.Params.ByName("BuildID"))
    55  	if err != nil {
    56  		replyError(c, err, "invalid build id", http.StatusBadRequest)
    57  		return
    58  	}
    59  
    60  	bld := getBuild(c, bID)
    61  	if bld == nil {
    62  		return
    63  	}
    64  	stepName := strings.Trim(c.Params.ByName("StepName"), "/")
    65  	buildSteps := &model.BuildSteps{Build: datastore.KeyForObj(ctx, bld)}
    66  	switch err := datastore.Get(ctx, buildSteps); {
    67  	case errors.Contains(err, datastore.ErrNoSuchEntity):
    68  		replyError(c, nil, "no steps found", http.StatusNotFound)
    69  		return
    70  	case err != nil:
    71  		replyError(c, err, "error in fetching steps", http.StatusInternalServerError)
    72  		return
    73  	}
    74  
    75  	steps, err := buildSteps.ToProto(ctx)
    76  	if err != nil {
    77  		replyError(c, err, "failed to parse steps", http.StatusInternalServerError)
    78  		return
    79  	}
    80  
    81  	logName := c.Request.URL.Query().Get("log")
    82  	if logName == "" {
    83  		logName = "stdout"
    84  	}
    85  	logURL := findLogURL(stepName, logName, steps)
    86  	if logURL == "" {
    87  		replyError(c, nil, fmt.Sprintf("view url for log %q in step %q in build %d not found", logName, stepName, bID), http.StatusNotFound)
    88  		return
    89  	}
    90  	http.Redirect(c.Writer, c.Request, logURL, http.StatusFound)
    91  }
    92  
    93  func findLogURL(stepName, logName string, steps []*pb.Step) string {
    94  	for _, step := range steps {
    95  		if step.GetName() == stepName {
    96  			for _, log := range step.Logs {
    97  				if log.GetName() == logName {
    98  					return log.ViewUrl
    99  				}
   100  			}
   101  			break
   102  		}
   103  	}
   104  	return ""
   105  }
   106  
   107  // handleViewBuild redirects to Milo build page.
   108  func handleViewBuild(c *router.Context) {
   109  	ctx := c.Request.Context()
   110  	bID, err := strconv.Atoi(strings.Trim(c.Params.ByName("BuildID"), "/"))
   111  	if err != nil {
   112  		replyError(c, err, "invalid build id", http.StatusBadRequest)
   113  		return
   114  	}
   115  
   116  	bld := getBuild(c, bID)
   117  	if bld == nil {
   118  		return
   119  	}
   120  
   121  	buildURL, err := getBuildURL(ctx, bld)
   122  	if err != nil {
   123  		replyError(c, err, "failed to generate the build url", http.StatusInternalServerError)
   124  		return
   125  	}
   126  	http.Redirect(c.Writer, c.Request, buildURL, http.StatusFound)
   127  }
   128  
   129  // getBuild will return a build.
   130  // For the unfounded build or a build that user has no access, it will directly reply http error.
   131  // For anonymous user, it will redirect to the login page.
   132  func getBuild(c *router.Context, bID int) *model.Build {
   133  	ctx := c.Request.Context()
   134  	bld, err := common.GetBuild(ctx, int64(bID))
   135  	if err != nil {
   136  		if s := status.Convert(err); s != nil {
   137  			replyError(c, err, s.Message(), grpcutil.CodeStatus(s.Code()))
   138  			return nil
   139  		}
   140  		replyError(c, err, "failed to get the build", http.StatusInternalServerError)
   141  		return nil
   142  	}
   143  
   144  	if _, err := perm.GetFirstAvailablePerm(ctx, bld.Proto.Builder, bbperms.BuildsGet, bbperms.BuildsGetLimited); err != nil {
   145  		// For anonymous users, redirect to the login page
   146  		if caller := auth.CurrentIdentity(ctx); caller == identity.AnonymousIdentity {
   147  			loginURL, err := auth.LoginURL(ctx, c.Request.URL.RequestURI())
   148  			if err != nil {
   149  				replyError(c, err, "failed to generate the login url", http.StatusInternalServerError)
   150  				return nil
   151  			}
   152  			http.Redirect(c.Writer, c.Request, loginURL, http.StatusFound)
   153  			return nil
   154  		}
   155  
   156  		if s := status.Convert(err); s != nil {
   157  			replyError(c, err, s.Message(), grpcutil.CodeStatus(s.Code()))
   158  			return nil
   159  		}
   160  		replyError(c, err, "failed to check perm", http.StatusInternalServerError)
   161  		return nil
   162  	}
   163  	return bld
   164  }
   165  
   166  func getBuildURL(ctx context.Context, bld *model.Build) (string, error) {
   167  	// For lagacy v1 case.
   168  	if bld.URL != "" {
   169  		return bld.URL, nil
   170  	}
   171  
   172  	// For V2 builds with view_url.
   173  	if bld.Proto.ViewUrl != "" {
   174  		return bld.Proto.ViewUrl, nil
   175  	}
   176  
   177  	globalCfg, err := config.GetSettingsCfg(ctx)
   178  	if err != nil {
   179  		return "", err
   180  	}
   181  
   182  	if bld.BackendTarget != "" {
   183  		for _, backendSetting := range globalCfg.Backends {
   184  			if backendSetting.Target == bld.BackendTarget {
   185  				if backendSetting.GetFullMode().GetRedirectToTaskPage() {
   186  					bInfra := &model.BuildInfra{Build: datastore.KeyForObj(ctx, bld)}
   187  					if err := datastore.Get(ctx, bInfra); err != nil {
   188  						return "", err
   189  					}
   190  					if bInfra.Proto.Backend.GetTask().GetLink() != "" {
   191  						return bInfra.Proto.Backend.Task.Link, nil
   192  					}
   193  					logging.Errorf(ctx, "build %d had GetRedirectToTaskPage set to true but no task link was found", bld.ID)
   194  				}
   195  			}
   196  		}
   197  	}
   198  	return fmt.Sprintf("https://%s/b/%d", globalCfg.Swarming.MiloHostname, bld.ID), nil
   199  }
   200  
   201  // replyError convert the provided error to http error and also logs the error.
   202  func replyError(c *router.Context, err error, message string, code int) {
   203  	if code < 500 {
   204  		// User side error. Log it to info level.
   205  		logging.Infof(c.Request.Context(), "%s: %s", message, err)
   206  	} else {
   207  		logging.Errorf(c.Request.Context(), "%s: %s", message, err)
   208  	}
   209  
   210  	http.Error(c.Writer, message, code)
   211  }