github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/pkg/spyglass/lenses/common/common.go (about) 1 /* 2 Copyright 2020 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package common 18 19 import ( 20 "context" 21 "encoding/json" 22 "errors" 23 "fmt" 24 "html/template" 25 "io" 26 "net/http" 27 "regexp" 28 "strings" 29 "time" 30 31 "github.com/sirupsen/logrus" 32 "k8s.io/apimachinery/pkg/util/sets" 33 34 prowv1 "sigs.k8s.io/prow/pkg/apis/prowjobs/v1" 35 "sigs.k8s.io/prow/pkg/config" 36 "sigs.k8s.io/prow/pkg/io/providers" 37 "sigs.k8s.io/prow/pkg/spyglass/api" 38 ) 39 40 var lensTemplate = template.Must(template.New("sg").Parse(string(MustAsset("static/spyglass-lens.html")))) 41 var buildLogRegex = regexp.MustCompile(`^(?:[^/]*-)?build-log\.txt$`) 42 43 type LensWithConfiguration struct { 44 Config LensOpt 45 Lens api.Lens 46 } 47 48 func NewLensServer( 49 listenAddress string, 50 pjFetcher ProwJobFetcher, 51 storageArtifactFetcher ArtifactFetcher, 52 podLogArtifactFetcher ArtifactFetcher, 53 cfg config.Getter, 54 lenses []LensWithConfiguration, 55 ) (*http.Server, error) { 56 57 mux := http.NewServeMux() 58 59 seenLens := sets.Set[string]{} 60 for _, lens := range lenses { 61 if seenLens.Has(lens.Config.LensName) { 62 return nil, fmt.Errorf("duplicate lens named %q", lens.Config.LensName) 63 } 64 seenLens.Insert(lens.Config.LensName) 65 66 logrus.WithField("Lens", lens.Config.LensName).Info("Adding handler for lens") 67 opt := lensHandlerOpts{ 68 PJFetcher: pjFetcher, 69 StorageArtifactFetcher: storageArtifactFetcher, 70 PodLogArtifactFetcher: podLogArtifactFetcher, 71 ConfigGetter: cfg, 72 LensOpt: lens.Config, 73 } 74 mux.Handle(DyanmicPathForLens(lens.Config.LensName), newLensHandler(lens.Lens, opt)) 75 } 76 mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 77 logrus.WithField("path", r.URL.Path).Error("LensServer got request on unhandled path") 78 http.NotFound(w, r) 79 })) 80 81 return &http.Server{Addr: listenAddress, Handler: mux}, nil 82 } 83 84 type LensOpt struct { 85 LensResourcesDir string 86 LensName string 87 LensTitle string 88 } 89 90 type lensHandlerOpts struct { 91 PJFetcher ProwJobFetcher 92 StorageArtifactFetcher ArtifactFetcher 93 PodLogArtifactFetcher ArtifactFetcher 94 ConfigGetter config.Getter 95 LensOpt 96 } 97 98 func newLensHandler(lens api.Lens, opts lensHandlerOpts) http.HandlerFunc { 99 return func(w http.ResponseWriter, r *http.Request) { 100 body, err := io.ReadAll(r.Body) 101 if err != nil { 102 writeHTTPError(w, fmt.Errorf("failed to read request body: %w", err), http.StatusInternalServerError) 103 return 104 } 105 106 request := &api.LensRequest{} 107 if err := json.Unmarshal(body, request); err != nil { 108 writeHTTPError(w, fmt.Errorf("failed to unmarshal request: %w", err), http.StatusBadRequest) 109 return 110 } 111 112 artifacts, err := FetchArtifacts(r.Context(), opts.PJFetcher, opts.ConfigGetter, opts.StorageArtifactFetcher, opts.PodLogArtifactFetcher, request.ArtifactSource, "", opts.ConfigGetter().Deck.Spyglass.SizeLimit, request.Artifacts) 113 if err != nil || len(artifacts) == 0 { 114 statusCode := http.StatusInternalServerError 115 if len(artifacts) == 0 { 116 statusCode = http.StatusNotFound 117 err = errors.New("no artifacts found") 118 } 119 120 writeHTTPError(w, fmt.Errorf("failed to retrieve expected artifacts: %w", err), statusCode) 121 return 122 } 123 124 switch request.Action { 125 case api.RequestActionInitial: 126 w.Header().Set("Content-Type", "text/html; encoding=utf-8") 127 lensTemplate.Execute(w, struct { 128 Title string 129 BaseURL string 130 Head template.HTML 131 Body template.HTML 132 }{ 133 opts.LensTitle, 134 request.ResourceRoot, 135 template.HTML(lens.Header(artifacts, opts.LensResourcesDir, opts.ConfigGetter().Deck.Spyglass.Lenses[request.LensIndex].Lens.Config, opts.ConfigGetter().Deck.Spyglass)), 136 template.HTML(lens.Body(artifacts, opts.LensResourcesDir, "", opts.ConfigGetter().Deck.Spyglass.Lenses[request.LensIndex].Lens.Config, opts.ConfigGetter().Deck.Spyglass)), 137 }) 138 139 case api.RequestActionRerender: 140 w.Header().Set("Content-Type", "text/html; encoding=utf-8") 141 w.Write([]byte(lens.Body(artifacts, opts.LensResourcesDir, request.Data, opts.ConfigGetter().Deck.Spyglass.Lenses[request.LensIndex].Lens.Config, opts.ConfigGetter().Deck.Spyglass))) 142 143 case api.RequestActionCallBack: 144 w.Write([]byte(lens.Callback(artifacts, opts.LensResourcesDir, request.Data, opts.ConfigGetter().Deck.Spyglass.Lenses[request.LensIndex].Lens.Config, opts.ConfigGetter().Deck.Spyglass))) 145 146 default: 147 w.WriteHeader(http.StatusBadRequest) 148 // This is a bit weird as we proxy this and the request we are complaining about was issued by Deck, not by the original client that sees this error 149 w.Write([]byte(fmt.Sprintf("Invalid action %q", request.Action))) 150 } 151 } 152 } 153 154 func writeHTTPError(w http.ResponseWriter, err error, statusCode int) { 155 if statusCode == 0 { 156 statusCode = http.StatusInternalServerError 157 } 158 logrus.WithError(err).WithField("statusCode", statusCode).Debug("Failed to process request") 159 w.WriteHeader(statusCode) 160 if _, err := w.Write([]byte(err.Error())); err != nil { 161 logrus.WithError(err).Error("Failed to write response") 162 } 163 } 164 165 // ArtifactFetcher knows how to fetch artifacts 166 type ArtifactFetcher interface { 167 Artifact(ctx context.Context, key string, artifactName string, sizeLimit int64) (api.Artifact, error) 168 } 169 170 // FetchArtifacts fetches artifacts. 171 // TODO: Unexport once we only have remote lenses 172 func FetchArtifacts( 173 ctx context.Context, 174 pjFetcher ProwJobFetcher, 175 cfg config.Getter, 176 storageArtifactFetcher ArtifactFetcher, 177 podLogArtifactFetcher ArtifactFetcher, 178 src string, 179 podName string, 180 sizeLimit int64, 181 artifactNames []string, 182 ) ([]api.Artifact, error) { 183 artStart := time.Now() 184 arts := []api.Artifact{} 185 keyType, key, err := splitSrc(src) 186 if err != nil { 187 return arts, fmt.Errorf("error parsing src: %w", err) 188 } 189 gcsKey := "" 190 switch keyType { 191 case api.ProwKeyType: 192 storageProvider, key, err := ProwToGCS(pjFetcher, cfg, key) 193 if err != nil { 194 logrus.Warningln(err) 195 } 196 gcsKey = fmt.Sprintf("%s://%s", storageProvider, strings.TrimSuffix(key, "/")) 197 default: 198 if keyType == api.GCSKeyType { 199 keyType = providers.GS 200 } 201 gcsKey = fmt.Sprintf("%s://%s", keyType, strings.TrimSuffix(key, "/")) 202 } 203 204 logsNeeded := []string{} 205 206 for _, name := range artifactNames { 207 art, err := storageArtifactFetcher.Artifact(ctx, gcsKey, name, sizeLimit) 208 if err == nil { 209 // Actually try making a request, because calling StorageArtifactFetcher.artifact does no I/O. 210 // (these files are being explicitly requested and so will presumably soon be accessed, so 211 // the extra network I/O should not be too problematic). 212 _, err = art.Size() 213 } 214 if err != nil { 215 if buildLogRegex.MatchString(name) { 216 logsNeeded = append(logsNeeded, name) 217 } 218 logrus.WithError(err).WithField("artifact", name).Debug("Failed to fetch artifact") 219 continue 220 } 221 arts = append(arts, art) 222 } 223 224 for _, logName := range logsNeeded { 225 art, err := podLogArtifactFetcher.Artifact(ctx, src, logName, sizeLimit) 226 if config.IsNotAllowedBucketError(err) { 227 logrus.Debugf("Failed to fetch pod log: %v", err) 228 } else if err != nil { 229 logrus.Errorf("Failed to fetch pod log: %v", err) 230 } else { 231 arts = append(arts, art) 232 } 233 } 234 235 logrus.WithField("duration", time.Since(artStart).String()).Infof("Retrieved artifacts for %v", src) 236 return arts, nil 237 } 238 239 // ProwJobFetcher knows how to get a ProwJob 240 type ProwJobFetcher interface { 241 GetProwJob(job string, id string) (prowv1.ProwJob, error) 242 } 243 244 // prowToGCS returns the GCS key corresponding to the given prow key 245 // TODO: Unexport once we only have remote lenses 246 func ProwToGCS(fetcher ProwJobFetcher, config config.Getter, prowKey string) (string, string, error) { 247 jobName, buildID, err := KeyToJob(prowKey) 248 if err != nil { 249 return "", "", fmt.Errorf("could not get GCS src: %w", err) 250 } 251 252 job, err := fetcher.GetProwJob(jobName, buildID) 253 if err != nil { 254 return "", "", fmt.Errorf("failed to get prow job from src %q: %w", prowKey, err) 255 } 256 257 url := job.Status.URL 258 prefix := config().Plank.GetJobURLPrefix(&job) 259 if !strings.HasPrefix(url, prefix) { 260 return "", "", fmt.Errorf("unexpected job URL %q when finding GCS path: expected something starting with %q", url, prefix) 261 } 262 263 // example: 264 // * url: https://prow.k8s.io/view/gs/kubernetes-jenkins/logs/ci-benchmark-microbenchmarks/1258197944759226371 265 // * prefix: https://prow.k8s.io/view/ 266 // * storagePath: gs/kubernetes-jenkins/logs/ci-benchmark-microbenchmarks/1258197944759226371 267 storagePath := strings.TrimPrefix(url, prefix) 268 if strings.HasPrefix(storagePath, api.GCSKeyType) { 269 storagePath = strings.Replace(storagePath, api.GCSKeyType, providers.GS, 1) 270 } 271 storagePathWithoutProvider := storagePath 272 storagePathSegments := strings.SplitN(storagePath, "/", 2) 273 if providers.HasStorageProviderPrefix(storagePath) { 274 storagePathWithoutProvider = storagePathSegments[1] 275 } 276 277 // try to parse storageProvider from DecorationConfig.GCSConfiguration.Bucket 278 // if it doesn't work fallback to URL parsing 279 if job.Spec.DecorationConfig != nil && job.Spec.DecorationConfig.GCSConfiguration != nil { 280 prowPath, err := prowv1.ParsePath(job.Spec.DecorationConfig.GCSConfiguration.Bucket) 281 if err == nil { 282 return prowPath.StorageProvider(), storagePathWithoutProvider, nil 283 } 284 logrus.Warnf("Could not parse storageProvider from DecorationConfig.GCSConfiguration.Bucket = %s: %v", job.Spec.DecorationConfig.GCSConfiguration.Bucket, err) 285 } 286 287 return storagePathSegments[0], storagePathWithoutProvider, nil 288 } 289 290 func splitSrc(src string) (keyType, key string, err error) { 291 split := strings.SplitN(src, "/", 2) 292 if len(split) < 2 { 293 err = fmt.Errorf("invalid src %s: expected <key-type>/<key>", src) 294 return 295 } 296 keyType = split[0] 297 key = split[1] 298 return 299 } 300 301 // keyToJob takes a spyglass URL and returns the jobName and buildID. 302 func KeyToJob(src string) (jobName string, buildID string, err error) { 303 src = strings.Trim(src, "/") 304 parsed := strings.Split(src, "/") 305 if len(parsed) < 2 { 306 return "", "", fmt.Errorf("expected at least two path components in %q", src) 307 } 308 jobName = parsed[len(parsed)-2] 309 buildID = parsed[len(parsed)-1] 310 return jobName, buildID, nil 311 } 312 313 const prefixSpyglassDynamicHandlers = "dynamic" 314 315 func DyanmicPathForLens(lensName string) string { 316 return fmt.Sprintf("/%s/%s", prefixSpyglassDynamicHandlers, lensName) 317 }