github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/pkg/spyglass/lenses/lenses.go (about) 1 /* 2 Copyright 2018 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 lenses provides interfaces and methods necessary for implementing custom artifact viewers 18 package lenses 19 20 import ( 21 "bufio" 22 "bytes" 23 "encoding/json" 24 "errors" 25 "fmt" 26 "io" 27 "path/filepath" 28 29 "github.com/sirupsen/logrus" 30 31 "sigs.k8s.io/prow/pkg/config" 32 "sigs.k8s.io/prow/pkg/spyglass/api" 33 ) 34 35 var ( 36 lensReg = map[string]Lens{} 37 38 // ErrGzipOffsetRead will be thrown when an offset read is attempted on a gzip-compressed object 39 ErrGzipOffsetRead = errors.New("offset read on gzipped files unsupported") 40 // ErrInvalidLensName will be thrown when a viewer method is called on a view name that has not 41 // been registered. Ensure your viewer is registered using RegisterViewer and that you are 42 // providing the correct viewer name. 43 ErrInvalidLensName = errors.New("invalid lens name") 44 // ErrFileTooLarge will be thrown when a size-limited operation (ex. ReadAll) is called on an 45 // artifact whose size exceeds the configured limit. 46 ErrFileTooLarge = errors.New("file size over specified limit") 47 // ErrRequestSizeTooLarge will be thrown when any operation is called whose size exceeds the configured limit. 48 ErrRequestSizeTooLarge = errors.New("request size over specified limit") 49 // ErrContextUnsupported is thrown when attempting to use a context with an artifact that 50 // does not support context operations (cancel, withtimeout, etc.) 51 ErrContextUnsupported = errors.New("artifact does not support context operations") 52 ) 53 54 type LensConfig struct { 55 // Name is the name of the lens. It must match the package name. 56 Name string 57 // Title is a human-readable title for the lens. 58 Title string 59 // Priority is used to determine where to position the lens. Higher is better. 60 Priority uint 61 // HideTitle will hide the lens title after loading if set to true. 62 HideTitle bool 63 } 64 65 // Lens defines the interface that lenses are required to implement in order to be used by Spyglass. 66 type Lens interface { 67 // Config returns a LensConfig that describes the lens. 68 Config() LensConfig 69 // Header returns a a string that is injected into the rendered lens's <head> 70 Header(artifacts []api.Artifact, resourceDir string, config json.RawMessage, spyglassConfig config.Spyglass) string 71 // Body returns a string that is initially injected into the rendered lens's <body>. 72 // The lens's front-end code may call back to Body again, passing in some data string of its choosing. 73 Body(artifacts []api.Artifact, resourceDir string, data string, config json.RawMessage, spyglassConfig config.Spyglass) string 74 // Callback receives a string sent by the lens's front-end code and returns another string to be returned 75 // to that frontend code. 76 Callback(artifacts []api.Artifact, resourceDir string, data string, config json.RawMessage, spyglassConfig config.Spyglass) string 77 } 78 79 // ResourceDirForLens returns the path to a lens's public resource directory. 80 func ResourceDirForLens(baseDir, name string) string { 81 return filepath.Join(baseDir, name) 82 } 83 84 // RegisterLens registers new viewers 85 func RegisterLens(lens Lens) error { 86 config := lens.Config() 87 _, ok := lensReg[config.Name] 88 if ok { 89 return fmt.Errorf("viewer already registered with name %s", config.Name) 90 } 91 92 if config.Title == "" { 93 return errors.New("empty title field in view metadata") 94 } 95 lensReg[config.Name] = lens 96 logrus.Infof("Spyglass registered viewer %s with title %s.", config.Name, config.Title) 97 return nil 98 } 99 100 // GetLens returns a Lens or a remoteLens by name, if it exists; otherwise it returns an error. 101 func GetLens(name string) (Lens, error) { 102 lens, ok := lensReg[name] 103 if !ok { 104 return nil, ErrInvalidLensName 105 } 106 return lens, nil 107 } 108 109 // UnregisterLens unregisters lenses 110 func UnregisterLens(viewerName string) { 111 delete(lensReg, viewerName) 112 logrus.Infof("Spyglass unregistered viewer %s.", viewerName) 113 } 114 115 // LastNLines reads the last n lines from an artifact. 116 func LastNLines(a api.Artifact, n int64) ([]string, error) { 117 // 300B, a reasonable log line length, probably a bit more scalable than a hard-coded value 118 return LastNLinesChunked(a, n, 300*n+1) 119 } 120 121 // LastNLinesChunked reads the last n lines from an artifact by reading chunks of size chunkSize 122 // from the end of the artifact. Best performance is achieved by: 123 // argmin 0<chunkSize<INTMAX, f(chunkSize) = chunkSize - n * avgLineLength 124 func LastNLinesChunked(a api.Artifact, n, chunkSize int64) ([]string, error) { 125 toRead := chunkSize + 1 // Add 1 for exclusive upper bound read range 126 chunks := int64(1) 127 var contents []byte 128 var linesInContents int64 129 artifactSize, err := a.Size() 130 if err != nil { 131 return nil, fmt.Errorf("error getting artifact size: %w", err) 132 } 133 offset := artifactSize - chunks*chunkSize 134 lastOffset := offset 135 var lastRead int64 136 for linesInContents < n && offset != 0 { 137 offset = lastOffset - lastRead 138 if offset < 0 { 139 toRead = offset + chunkSize + 1 140 offset = 0 141 } 142 bytesRead := make([]byte, toRead) 143 numBytesRead, err := a.ReadAt(bytesRead, offset) 144 if err != nil && err != io.EOF { 145 return nil, fmt.Errorf("error reading artifact: %w", err) 146 } 147 lastRead = int64(numBytesRead) 148 lastOffset = offset 149 bytesRead = bytes.Trim(bytesRead, "\x00") 150 linesInContents += int64(bytes.Count(bytesRead, []byte("\n"))) 151 contents = append(bytesRead, contents...) 152 chunks++ 153 } 154 155 var lines []string 156 scanner := bufio.NewScanner(bytes.NewReader(contents)) 157 scanner.Split(bufio.ScanLines) 158 for scanner.Scan() { 159 line := scanner.Text() 160 lines = append(lines, line) 161 } 162 l := int64(len(lines)) 163 if l < n { 164 return lines, nil 165 } 166 return lines[l-n:], nil 167 }