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  }