github.com/web-platform-tests/wpt.fyi@v0.0.0-20240530210107-70cf978996f1/api/receiver/api.go (about) 1 // Copyright 2018 The WPT Dashboard Project. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 //go:generate mockgen -destination mock_receiver/api_mock.go github.com/web-platform-tests/wpt.fyi/api/receiver API 6 7 package receiver 8 9 import ( 10 "context" 11 "fmt" 12 "io" 13 "net/http" 14 "net/url" 15 "regexp" 16 "time" 17 18 "github.com/web-platform-tests/wpt.fyi/shared" 19 ) 20 21 // gcsPattern is the pattern for gs:// URI. 22 var gcsPattern = regexp.MustCompile(`^gs://([^/]+)/(.+)$`) 23 24 // AuthenticateUploader checks the HTTP basic auth against SecretManager, and returns the username if 25 // it's valid or "" otherwise. 26 // 27 // This function is not defined on API interface for easier reuse in other packages. 28 func AuthenticateUploader(aeAPI shared.AppEngineAPI, r *http.Request) string { 29 username, password, ok := r.BasicAuth() 30 if !ok { 31 return "" 32 } 33 user, err := aeAPI.GetUploader(username) 34 if err != nil || user.Password != password { 35 return "" 36 } 37 38 return user.Username 39 } 40 41 // API abstracts all AppEngine/GCP APIs used by the results receiver. 42 type API interface { 43 shared.AppEngineAPI 44 45 AddTestRun(testRun *shared.TestRun) (shared.Key, error) 46 IsAdmin(*http.Request) bool 47 ScheduleResultsTask(uploader string, results, screenshots []string, extraParams map[string]string) (string, error) 48 UpdatePendingTestRun(pendingRun shared.PendingTestRun) error 49 UploadToGCS(gcsPath string, f io.Reader, gzipped bool) error 50 } 51 52 type apiImpl struct { 53 shared.AppEngineAPI 54 55 gcs gcs 56 store shared.Datastore 57 queue string 58 59 githubACLFactory func(*http.Request) (shared.GitHubAccessControl, error) 60 } 61 62 // NewAPI creates a real API from a given context. 63 // nolint:ireturn // TODO: Fix ireturn lint error 64 func NewAPI(ctx context.Context) API { 65 api := shared.NewAppEngineAPI(ctx) 66 store := shared.NewAppEngineDatastore(ctx, false) 67 // nolint:exhaustruct // TODO: Fix exhaustruct lint error 68 return &apiImpl{ 69 AppEngineAPI: api, 70 store: store, 71 queue: ResultsQueue, 72 githubACLFactory: func(r *http.Request) (shared.GitHubAccessControl, error) { 73 return shared.NewGitHubAccessControlFromRequest(api, store, r) 74 }, 75 } 76 } 77 78 // nolint:ireturn // TODO: Fix ireturn lint error 79 func (a apiImpl) AddTestRun(testRun *shared.TestRun) (shared.Key, error) { 80 key := a.store.NewIDKey("TestRun", testRun.ID) 81 var err error 82 if testRun.ID != 0 { 83 err = a.store.Insert(key, testRun) 84 } else { 85 key, err = a.store.Put(key, testRun) 86 } 87 if err != nil { 88 return nil, err 89 } 90 91 return key, nil 92 } 93 94 func (a apiImpl) IsAdmin(r *http.Request) bool { 95 logger := shared.GetLogger(a.Context()) 96 acl, err := a.githubACLFactory(r) 97 if err != nil { 98 logger.Errorf("Error creating GitHubAccessControl: %s", err.Error()) 99 100 return false 101 } 102 if acl == nil { 103 return false 104 } 105 admin, err := acl.IsValidAdmin() 106 if err != nil { 107 logger.Errorf("Error checking admin: %s", err.Error()) 108 109 return false 110 } 111 112 return admin 113 } 114 115 func (a apiImpl) UpdatePendingTestRun(newRun shared.PendingTestRun) error { 116 var buffer shared.PendingTestRun 117 key := a.store.NewIDKey("PendingTestRun", newRun.ID) 118 119 return a.store.Update(key, &buffer, func(obj interface{}) error { 120 run := obj.(*shared.PendingTestRun) 121 if newRun.Stage != 0 { 122 if err := run.Transition(newRun.Stage); err != nil { 123 return err 124 } 125 } 126 if newRun.Error != "" { 127 run.Error = newRun.Error 128 } 129 if newRun.CheckRunID != 0 { 130 run.CheckRunID = newRun.CheckRunID 131 } 132 if newRun.Uploader != "" { 133 run.Uploader = newRun.Uploader 134 } 135 // ProductAtRevision 136 if newRun.BrowserName != "" { 137 run.BrowserName = newRun.BrowserName 138 } 139 if newRun.BrowserVersion != "" { 140 run.BrowserVersion = newRun.BrowserVersion 141 } 142 if newRun.OSName != "" { 143 run.OSName = newRun.OSName 144 } 145 if newRun.OSVersion != "" { 146 run.OSVersion = newRun.OSVersion 147 } 148 // nolint:staticcheck // TODO: Fix staticcheck lint error (SA1019). 149 if newRun.FullRevisionHash != "" { 150 run.Revision = newRun.FullRevisionHash[:10] 151 run.FullRevisionHash = newRun.FullRevisionHash 152 } 153 154 if run.Created.IsZero() { 155 run.Created = time.Now() 156 } 157 run.Updated = time.Now() 158 159 return nil 160 }) 161 } 162 163 func (a *apiImpl) UploadToGCS(gcsPath string, f io.Reader, gzipped bool) error { 164 matches := gcsPattern.FindStringSubmatch(gcsPath) 165 if len(matches) != 3 { 166 return fmt.Errorf("invalid GCS path: %s", gcsPath) 167 } 168 bucketName := matches[1] 169 fileName := matches[2] 170 171 if a.gcs == nil { 172 // nolint:exhaustruct // TODO: Fix exhaustruct lint error. 173 a.gcs = &gcsImpl{ctx: a.Context()} 174 } 175 176 encoding := "" 177 if gzipped { 178 encoding = "gzip" 179 } 180 // We don't defer wc.Close() here so that the file is only closed (and 181 // hence saved) if nothing fails. 182 w, err := a.gcs.NewWriter(bucketName, fileName, "", encoding) 183 if err != nil { 184 return err 185 } 186 _, err = io.Copy(w, f) 187 if err != nil { 188 return err 189 } 190 err = w.Close() 191 192 return err 193 } 194 195 func (a apiImpl) ScheduleResultsTask( 196 uploader string, results, screenshots []string, extraParams map[string]string) (string, error) { 197 key, err := a.store.ReserveID("TestRun") 198 if err != nil { 199 return "", err 200 } 201 202 // nolint:exhaustruct // TODO: Fix exhaustruct lint error 203 pendingRun := shared.PendingTestRun{ 204 ID: key.IntID(), 205 Stage: shared.StageWptFyiReceived, 206 Uploader: uploader, 207 ProductAtRevision: shared.ProductAtRevision{ 208 FullRevisionHash: extraParams["revision"], 209 }, 210 } 211 if err := a.UpdatePendingTestRun(pendingRun); err != nil { 212 return "", err 213 } 214 215 payload := url.Values{ 216 "results": results, 217 "screenshots": screenshots, 218 } 219 payload.Set("id", fmt.Sprint(key.IntID())) 220 payload.Set("uploader", uploader) 221 222 for k, v := range extraParams { 223 if v != "" { 224 payload.Set(k, v) 225 } 226 } 227 228 return a.ScheduleTask(ResultsQueue, fmt.Sprint(key.IntID()), ResultsTarget, payload) 229 }