sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/spyglass/podlogartifact.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 spyglass
    18  
    19  import (
    20  	"bytes"
    21  	"errors"
    22  	"fmt"
    23  	"io"
    24  	"net/url"
    25  
    26  	prowapi "sigs.k8s.io/prow/pkg/apis/prowjobs/v1"
    27  	"sigs.k8s.io/prow/pkg/spyglass/lenses"
    28  )
    29  
    30  type jobAgent interface {
    31  	GetProwJob(job string, id string) (prowapi.ProwJob, error)
    32  	GetJobLog(job string, id string, container string) ([]byte, error)
    33  }
    34  
    35  // PodLogArtifact holds data for reading from a specific pod log
    36  type PodLogArtifact struct {
    37  	name         string
    38  	buildID      string
    39  	artifactName string
    40  	container    string
    41  	sizeLimit    int64
    42  	jobAgent
    43  }
    44  
    45  var (
    46  	errInsufficientJobInfo = errors.New("insufficient job information provided")
    47  	errInvalidSizeLimit    = errors.New("sizeLimit must be a 64-bit integer greater than 0")
    48  )
    49  
    50  // NewPodLogArtifact creates a new PodLogArtifact
    51  func NewPodLogArtifact(jobName string, buildID string, artifactName string, container string, sizeLimit int64, ja jobAgent) (*PodLogArtifact, error) {
    52  	if jobName == "" {
    53  		return nil, errInsufficientJobInfo
    54  	}
    55  	if buildID == "" {
    56  		return nil, errInsufficientJobInfo
    57  	}
    58  	if artifactName == "" {
    59  		return nil, errInsufficientJobInfo
    60  	}
    61  	if sizeLimit < 0 {
    62  		return nil, errInvalidSizeLimit
    63  	}
    64  	return &PodLogArtifact{
    65  		name:         jobName,
    66  		buildID:      buildID,
    67  		artifactName: artifactName,
    68  		container:    container,
    69  		sizeLimit:    sizeLimit,
    70  		jobAgent:     ja,
    71  	}, nil
    72  }
    73  
    74  // CanonicalLink returns a link to where pod logs are streamed
    75  func (a *PodLogArtifact) CanonicalLink() string {
    76  	q := url.Values{
    77  		"job":       []string{a.name},
    78  		"id":        []string{a.buildID},
    79  		"container": []string{a.container},
    80  	}
    81  	u := url.URL{
    82  		Path:     "/log",
    83  		RawQuery: q.Encode(),
    84  	}
    85  	return u.String()
    86  }
    87  
    88  // JobPath gets the path within the job for the pod log. Always returns build-log.txt if we have only 1 test container
    89  // in the ProwJob. Returns <containerName>-build-log.txt if we have multiple containers in the ProwJob.
    90  // This is because the pod log becomes the build log after the job artifact uploads
    91  // are complete, which should be used instead of the pod log.
    92  func (a *PodLogArtifact) JobPath() string {
    93  	return a.artifactName
    94  }
    95  
    96  // ReadAt implements reading a range of bytes from the pod logs endpoint
    97  func (a *PodLogArtifact) ReadAt(p []byte, off int64) (n int, err error) {
    98  	if int64(len(p)) > a.sizeLimit {
    99  		return 0, lenses.ErrRequestSizeTooLarge
   100  	}
   101  	logs, err := a.jobAgent.GetJobLog(a.name, a.buildID, a.container)
   102  	if err != nil {
   103  		return 0, fmt.Errorf("error getting pod log: %w", err)
   104  	}
   105  	r := bytes.NewReader(logs)
   106  	readBytes, err := r.ReadAt(p, off)
   107  	if err == io.EOF {
   108  		return readBytes, io.EOF
   109  	}
   110  	if err != nil {
   111  		return 0, fmt.Errorf("error reading pod logs: %w", err)
   112  	}
   113  	return readBytes, nil
   114  }
   115  
   116  // ReadAll reads all available pod logs, failing if they are too large
   117  func (a *PodLogArtifact) ReadAll() ([]byte, error) {
   118  	size, err := a.Size()
   119  	if err != nil {
   120  		return nil, fmt.Errorf("error getting pod log size: %w", err)
   121  	}
   122  	if size > a.sizeLimit {
   123  		return nil, lenses.ErrFileTooLarge
   124  	}
   125  	logs, err := a.jobAgent.GetJobLog(a.name, a.buildID, a.container)
   126  	if err != nil {
   127  		return nil, fmt.Errorf("error getting pod log: %w", err)
   128  	}
   129  	return logs, nil
   130  }
   131  
   132  // ReadAtMost reads at most n bytes
   133  func (a *PodLogArtifact) ReadAtMost(n int64) ([]byte, error) {
   134  	if n > a.sizeLimit {
   135  		return nil, lenses.ErrRequestSizeTooLarge
   136  	}
   137  	logs, err := a.jobAgent.GetJobLog(a.name, a.buildID, a.container)
   138  	if err != nil {
   139  		return nil, fmt.Errorf("error getting pod log: %w", err)
   140  	}
   141  	reader := bytes.NewReader(logs)
   142  	var byteCount int64
   143  	var p []byte
   144  	for byteCount < n {
   145  		b, err := reader.ReadByte()
   146  		if err == io.EOF {
   147  			return p, io.EOF
   148  		}
   149  		if err != nil {
   150  			return nil, fmt.Errorf("error reading pod log: %w", err)
   151  		}
   152  		p = append(p, b)
   153  		byteCount++
   154  	}
   155  	return p, nil
   156  }
   157  
   158  // ReadTail reads the last n bytes of the pod log
   159  func (a *PodLogArtifact) ReadTail(n int64) ([]byte, error) {
   160  	if n > a.sizeLimit {
   161  		return nil, lenses.ErrRequestSizeTooLarge
   162  	}
   163  	logs, err := a.jobAgent.GetJobLog(a.name, a.buildID, a.container)
   164  	if err != nil {
   165  		return nil, fmt.Errorf("error getting pod log tail: %w", err)
   166  	}
   167  	size := int64(len(logs))
   168  	var off int64
   169  	if n > size {
   170  		off = 0
   171  	} else {
   172  		off = size - n
   173  	}
   174  	p := make([]byte, n)
   175  	readBytes, err := bytes.NewReader(logs).ReadAt(p, off)
   176  	if err != nil && err != io.EOF {
   177  		return nil, fmt.Errorf("error reading pod log tail: %w", err)
   178  	}
   179  	return p[:readBytes], nil
   180  }
   181  
   182  // Size gets the size of the pod log. Note: this function makes the same network call as reading the entire file.
   183  func (a *PodLogArtifact) Size() (int64, error) {
   184  	logs, err := a.jobAgent.GetJobLog(a.name, a.buildID, a.container)
   185  	if err != nil {
   186  		return 0, fmt.Errorf("error getting size of pod log: %w", err)
   187  	}
   188  	return int64(len(logs)), nil
   189  
   190  }
   191  
   192  func (a *PodLogArtifact) Metadata() (map[string]string, error) {
   193  	return nil, nil
   194  }
   195  
   196  func (a *PodLogArtifact) UpdateMetadata(meta map[string]string) error {
   197  	return errors.New("not implemented")
   198  }