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 }