github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/pkg/spyglass/lenses/links/links.go (about)

     1  /*
     2  Copyright 2021 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 links
    18  
    19  import (
    20  	"bytes"
    21  	"encoding/json"
    22  	"fmt"
    23  	"html/template"
    24  	"io"
    25  	"net/url"
    26  	"path/filepath"
    27  	"sort"
    28  	"strings"
    29  
    30  	"github.com/sirupsen/logrus"
    31  	"golang.org/x/text/cases"
    32  	"golang.org/x/text/language"
    33  	"sigs.k8s.io/prow/pkg/config"
    34  	"sigs.k8s.io/prow/pkg/spyglass/api"
    35  	"sigs.k8s.io/prow/pkg/spyglass/lenses"
    36  )
    37  
    38  const (
    39  	name     = "links"
    40  	title    = "Debugging links"
    41  	priority = 20
    42  
    43  	bytesLimit = 20 * 1024
    44  )
    45  
    46  func init() {
    47  	lenses.RegisterLens(Lens{})
    48  }
    49  
    50  // Lens prints link to master and node logs.
    51  type Lens struct{}
    52  
    53  // Config returns the lens's configuration.
    54  func (lens Lens) Config() lenses.LensConfig {
    55  	return lenses.LensConfig{
    56  		Name:     name,
    57  		Title:    title,
    58  		Priority: priority,
    59  	}
    60  }
    61  
    62  // Header renders the content of <head> from template.html.
    63  func (lens Lens) Header(artifacts []api.Artifact, resourceDir string, config json.RawMessage, spyglassConfig config.Spyglass) string {
    64  	output, err := renderTemplate(resourceDir, "header", nil)
    65  	if err != nil {
    66  		logrus.Warnf("Failed to render header: %v", err)
    67  		return "Error: " + err.Error()
    68  	}
    69  	return output
    70  }
    71  
    72  func renderTemplate(resourceDir, block string, params interface{}) (string, error) {
    73  	t, err := template.ParseFiles(filepath.Join(resourceDir, "template.html"))
    74  	if err != nil {
    75  		return "", fmt.Errorf("Failed to parse template: %w", err)
    76  	}
    77  
    78  	var buf bytes.Buffer
    79  	if err := t.ExecuteTemplate(&buf, block, params); err != nil {
    80  		return "", fmt.Errorf("Failed to execute template: %w", err)
    81  	}
    82  	return buf.String(), nil
    83  }
    84  
    85  // humanReadableName translates a fileName to human readable name, e.g.:
    86  // * master-and-node-logs.txt -> "Master and node logs"
    87  // * dashboard.link.txt -> "Dashboard"
    88  func humanReadableName(name string) string {
    89  	name = strings.TrimSuffix(name, ".link.txt")
    90  	name = strings.TrimSuffix(name, ".txt")
    91  	words := strings.Split(name, "-")
    92  	if len(words) > 0 {
    93  		words[0] = cases.Title(language.English).String(words[0])
    94  	}
    95  	return strings.Join(words, " ")
    96  }
    97  
    98  func clickableLink(link string, spyglassConfig config.Spyglass) string {
    99  	if strings.HasPrefix(link, "gs://") {
   100  		return spyglassConfig.GCSBrowserPrefix + strings.TrimPrefix(link, "gs://")
   101  	}
   102  	if strings.HasPrefix(link, "http://") || strings.HasPrefix(link, "https://") {
   103  		// Use https://google.com/url?q=[url] redirect to avoid leaking referer.
   104  		url := url.URL{
   105  			Scheme: "https",
   106  			Host:   "google.com",
   107  			Path:   "/url",
   108  		}
   109  		q := url.Query()
   110  		q.Add("q", link)
   111  		url.RawQuery = q.Encode()
   112  		return url.String()
   113  	}
   114  	return ""
   115  }
   116  
   117  type link struct {
   118  	Name string `json:"name"`
   119  	URL  string `json:"url"`
   120  	Link string `json:"-"`
   121  }
   122  
   123  type linkGroup struct {
   124  	Title string `json:"title"`
   125  	Links []link `json:"links"`
   126  }
   127  
   128  func toLink(jobPath string, content []byte, spyglassConfig config.Spyglass) link {
   129  	url := strings.TrimSpace(string(content))
   130  	return link{
   131  		Name: humanReadableName(filepath.Base(jobPath)),
   132  		URL:  url,
   133  		Link: clickableLink(url, spyglassConfig),
   134  	}
   135  }
   136  
   137  func parseLinkFile(jobPath string, content []byte, spyglassConfig config.Spyglass) (linkGroup, error) {
   138  	if extension := filepath.Ext(jobPath); extension == ".json" {
   139  		group := linkGroup{}
   140  		if err := json.Unmarshal(content, &group); err != nil {
   141  			return group, err
   142  		}
   143  		for i := range group.Links {
   144  			group.Links[i].Link = clickableLink(group.Links[i].URL, spyglassConfig)
   145  		}
   146  		return group, nil
   147  	}
   148  	// Wrap a single link inside a linkGroup.
   149  	wrappedLink := toLink(jobPath, content, spyglassConfig)
   150  	return linkGroup{
   151  		Title: wrappedLink.Name,
   152  		Links: []link{
   153  			wrappedLink,
   154  		},
   155  	}, nil
   156  }
   157  
   158  // Body renders link to logs.
   159  func (lens Lens) Body(artifacts []api.Artifact, resourceDir string, data string, config json.RawMessage, spyglassConfig config.Spyglass) string {
   160  	var linkGroups []linkGroup
   161  	for _, artifact := range artifacts {
   162  		jobPath := artifact.JobPath()
   163  		content, err := artifact.ReadAtMost(bytesLimit)
   164  		if err != nil && err != io.EOF {
   165  			logrus.WithError(err).Warnf("Failed to read artifact file: %q", jobPath)
   166  			continue
   167  		}
   168  		group, err := parseLinkFile(jobPath, content, spyglassConfig)
   169  		if err != nil {
   170  			logrus.WithError(err).Warnf("Failed to parse link file: %q", jobPath)
   171  			continue
   172  		}
   173  		linkGroups = append(linkGroups, group)
   174  	}
   175  
   176  	sort.Slice(linkGroups, func(i, j int) bool { return linkGroups[i].Title < linkGroups[j].Title })
   177  
   178  	params := struct {
   179  		LinkGroups []linkGroup
   180  	}{
   181  		LinkGroups: linkGroups,
   182  	}
   183  
   184  	output, err := renderTemplate(resourceDir, "body", params)
   185  	if err != nil {
   186  		logrus.Warnf("Failed to render body: %v", err)
   187  		return "Error: " + err.Error()
   188  	}
   189  	return output
   190  }
   191  
   192  // Callback does nothing.
   193  func (lens Lens) Callback(artifacts []api.Artifact, resourceDir string, data string, config json.RawMessage, spyglassConfig config.Spyglass) string {
   194  	return ""
   195  }