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 }