sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/resultstore/payload.go (about) 1 /* 2 Copyright 2023 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 resultstore 18 19 import ( 20 "errors" 21 "fmt" 22 "slices" 23 "strings" 24 "time" 25 26 "github.com/GoogleCloudPlatform/testgrid/metadata" 27 "google.golang.org/genproto/googleapis/devtools/resultstore/v2" 28 "google.golang.org/protobuf/types/known/durationpb" 29 "google.golang.org/protobuf/types/known/timestamppb" 30 corev1 "k8s.io/api/core/v1" 31 v1 "sigs.k8s.io/prow/pkg/apis/prowjobs/v1" 32 gerrit "sigs.k8s.io/prow/pkg/gerrit/source" 33 "sigs.k8s.io/prow/pkg/kube" 34 ) 35 36 type Payload struct { 37 Job *v1.ProwJob 38 Started *metadata.Started 39 Finished *metadata.Finished 40 Files []*resultstore.File 41 ProjectID string 42 } 43 44 // InvocationID returns the ResultStore InvocationId. 45 func (p *Payload) InvocationID() (string, error) { 46 if p.Job == nil { 47 return "", errors.New("internal error: pj is nil") 48 } 49 // Name is a v4 UUID set in pjutil.go. 50 return p.Job.Name, nil 51 } 52 53 // Invocation returns an Invocation suitable to upload to ResultStore. 54 func (p *Payload) Invocation() (*resultstore.Invocation, error) { 55 if p.Job == nil { 56 return nil, errors.New("internal error: pj is nil") 57 } 58 i := &resultstore.Invocation{ 59 StatusAttributes: invocationStatusAttributes(p.Job), 60 Timing: invocationTiming(p.Job), 61 InvocationAttributes: invocationAttributes(p.ProjectID, p.Job), 62 WorkspaceInfo: workspaceInfo(p.Job), 63 Properties: invocationProperties(p.Job, p.Started), 64 Files: p.Files, 65 } 66 return i, nil 67 } 68 69 func invocationStatusAttributes(job *v1.ProwJob) *resultstore.StatusAttributes { 70 status := resultstore.Status_TOOL_FAILED 71 if job != nil { 72 switch job.Status.State { 73 case v1.SuccessState: 74 status = resultstore.Status_PASSED 75 case v1.FailureState: 76 status = resultstore.Status_FAILED 77 case v1.AbortedState: 78 status = resultstore.Status_CANCELLED 79 case v1.ErrorState: 80 status = resultstore.Status_INCOMPLETE 81 } 82 } 83 return &resultstore.StatusAttributes{ 84 Status: status, 85 } 86 } 87 88 func invocationTiming(pj *v1.ProwJob) *resultstore.Timing { 89 if pj == nil { 90 return nil 91 } 92 start := pj.Status.StartTime.Time 93 var duration time.Duration 94 if pj.Status.CompletionTime != nil { 95 duration = pj.Status.CompletionTime.Time.Sub(start) 96 } 97 return &resultstore.Timing{ 98 StartTime: ×tamppb.Timestamp{ 99 Seconds: start.Unix(), 100 }, 101 Duration: &durationpb.Duration{ 102 Seconds: int64(duration.Seconds()), 103 }, 104 } 105 } 106 107 func invocationAttributes(projectID string, pj *v1.ProwJob) *resultstore.InvocationAttributes { 108 var labels map[string]string 109 if pj != nil { 110 labels = pj.Labels 111 } 112 return &resultstore.InvocationAttributes{ 113 // TODO: ProjectID might be assigned directly from the GCS 114 // BucketAttrs.ProjectNumber; requires a raw GCS client. 115 ProjectId: projectID, 116 Labels: []string{"prow"}, 117 Description: descriptionFromLabels(labels), 118 } 119 } 120 121 func descriptionFromLabels(labels map[string]string) string { 122 jt := labels[kube.ProwJobTypeLabel] 123 parts := []string{ 124 labels[kube.RepoLabel], 125 } 126 if pull := labels[kube.PullLabel]; pull != "" { 127 parts = append(parts, pull) 128 if ps := labels[kube.GerritPatchset]; ps != "" { 129 parts = append(parts, ps) 130 } 131 } 132 parts = append(parts, labels[kube.ProwBuildIDLabel], labels[kube.ProwJobAnnotation]) 133 return fmt.Sprintf("%s for %s", jt, strings.Join(parts, "/")) 134 } 135 136 func workspaceInfo(job *v1.ProwJob) *resultstore.WorkspaceInfo { 137 return &resultstore.WorkspaceInfo{ 138 CommandLines: commandLines(job), 139 } 140 } 141 142 // Per the ResultStore maintainers, the CommandLine Label must be 143 // populated, and should be either "original" or "canonical". (To be 144 // documented by them: if the original value contains placeholders, 145 // the final values should be added as "canonical".) 146 const commandLineLabel = "original" 147 148 func commandLines(pj *v1.ProwJob) []*resultstore.CommandLine { 149 var cl []*resultstore.CommandLine 150 if pj != nil && pj.Spec.PodSpec != nil { 151 for _, c := range pj.Spec.PodSpec.Containers { 152 cl = append(cl, &resultstore.CommandLine{ 153 Label: commandLineLabel, 154 Tool: strings.Join(c.Command, " "), 155 Args: c.Args, 156 }) 157 } 158 } 159 return cl 160 } 161 162 func invocationProperties(pj *v1.ProwJob, started *metadata.Started) []*resultstore.Property { 163 var ps []*resultstore.Property 164 ps = append(ps, jobProperties(pj)...) 165 ps = append(ps, startedProperties(started)...) 166 return ps 167 } 168 169 func jobProperties(pj *v1.ProwJob) []*resultstore.Property { 170 if pj == nil { 171 return nil 172 } 173 ps := []*resultstore.Property{ 174 { 175 Key: "Instance", 176 Value: pj.Status.BuildID, 177 }, 178 { 179 Key: "Job", 180 Value: pj.Spec.Job, 181 }, 182 { 183 Key: "Prow_Dashboard_URL", 184 Value: pj.Status.URL, 185 }, 186 } 187 ps = append(ps, podSpecProperties(pj.Spec.PodSpec)...) 188 return ps 189 } 190 191 func podSpecProperties(podSpec *corev1.PodSpec) []*resultstore.Property { 192 if podSpec == nil { 193 return nil 194 } 195 var ps []*resultstore.Property 196 seenEnv := map[string]bool{} 197 for _, c := range podSpec.Containers { 198 for _, e := range c.Env { 199 if e.Name == "" { 200 continue 201 } 202 v := e.Name + "=" + e.Value 203 if !seenEnv[v] { 204 seenEnv[v] = true 205 ps = append(ps, &resultstore.Property{ 206 Key: "Env", 207 Value: v, 208 }) 209 } 210 } 211 } 212 return ps 213 } 214 215 func startedProperties(started *metadata.Started) []*resultstore.Property { 216 if started == nil { 217 return nil 218 } 219 ps := []*resultstore.Property{{ 220 Key: "Commit", 221 Value: started.RepoCommit, 222 }} 223 224 var branches, repos []string 225 seenBranch := map[string]bool{} 226 for repo, branch := range started.Repos { 227 if !seenBranch[branch] { 228 seenBranch[branch] = true 229 branches = append(branches, branch) 230 } 231 repos = append(repos, repo) 232 } 233 slices.Sort(branches) 234 for _, b := range branches { 235 ps = append(ps, &resultstore.Property{ 236 Key: "Branch", 237 Value: b, 238 }) 239 } 240 slices.Sort(repos) 241 for _, r := range repos { 242 ps = append(ps, &resultstore.Property{ 243 Key: "Repo", 244 Value: gerrit.EnsureCodeURL(r), 245 }) 246 } 247 return ps 248 } 249 250 const defaultConfigurationId = "default" 251 252 func (p *Payload) DefaultConfiguration() *resultstore.Configuration { 253 return &resultstore.Configuration{ 254 Id: &resultstore.Configuration_Id{ 255 ConfigurationId: defaultConfigurationId, 256 }, 257 } 258 } 259 260 func targetID(pj *v1.ProwJob) string { 261 if pj == nil { 262 return "Unknown" 263 } 264 return pj.Spec.Job 265 266 } 267 268 func (p *Payload) OverallTarget() *resultstore.Target { 269 return &resultstore.Target{ 270 Id: &resultstore.Target_Id{ 271 TargetId: targetID(p.Job), 272 }, 273 TargetAttributes: &resultstore.TargetAttributes{ 274 Type: resultstore.TargetType_TEST, 275 }, 276 Visible: true, 277 } 278 } 279 280 func (p *Payload) ConfiguredTarget() *resultstore.ConfiguredTarget { 281 return &resultstore.ConfiguredTarget{ 282 Id: &resultstore.ConfiguredTarget_Id{ 283 TargetId: targetID(p.Job), 284 ConfigurationId: defaultConfigurationId, 285 }, 286 StatusAttributes: invocationStatusAttributes(p.Job), 287 Timing: metadataTiming(p.Job, p.Started, p.Finished), 288 } 289 } 290 291 func (p *Payload) OverallAction() *resultstore.Action { 292 return &resultstore.Action{ 293 Id: &resultstore.Action_Id{ 294 TargetId: targetID(p.Job), 295 ConfigurationId: defaultConfigurationId, 296 ActionId: "overall", 297 }, 298 StatusAttributes: invocationStatusAttributes(p.Job), 299 Timing: metadataTiming(p.Job, p.Started, p.Finished), 300 // TODO: What else if anything is required here? 301 ActionType: &resultstore.Action_TestAction{}, 302 } 303 } 304 305 func metadataTiming(job *v1.ProwJob, started *metadata.Started, finished *metadata.Finished) *resultstore.Timing { 306 if started == nil { 307 return nil 308 } 309 start := started.Timestamp 310 var duration int64 311 switch { 312 case finished != nil: 313 duration = *finished.Timestamp - start 314 case job != nil && job.Status.CompletionTime != nil: 315 duration = job.Status.CompletionTime.Unix() - start 316 default: 317 return nil 318 } 319 return &resultstore.Timing{ 320 StartTime: ×tamppb.Timestamp{ 321 Seconds: start, 322 }, 323 Duration: &durationpb.Duration{ 324 Seconds: duration, 325 }, 326 } 327 }