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  }