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: &timestamppb.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: &timestamppb.Timestamp{
   321  			Seconds: start,
   322  		},
   323  		Duration: &durationpb.Duration{
   324  			Seconds: duration,
   325  		},
   326  	}
   327  }