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 }