sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/crier/reporters/resultstore/reporter.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  	"context"
    21  	"encoding/json"
    22  
    23  	"github.com/GoogleCloudPlatform/testgrid/metadata"
    24  	"github.com/sirupsen/logrus"
    25  	"sigs.k8s.io/controller-runtime/pkg/reconcile"
    26  	v1 "sigs.k8s.io/prow/pkg/apis/prowjobs/v1"
    27  	"sigs.k8s.io/prow/pkg/config"
    28  	"sigs.k8s.io/prow/pkg/crier/reporters/gcs/util"
    29  	"sigs.k8s.io/prow/pkg/io"
    30  	"sigs.k8s.io/prow/pkg/io/providers"
    31  	"sigs.k8s.io/prow/pkg/resultstore"
    32  )
    33  
    34  // Reporter reports Prow results to ResultStore and satisfies the
    35  // crier.reportClient interface.
    36  type Reporter struct {
    37  	cfg      config.Getter
    38  	opener   io.Opener
    39  	uploader *resultstore.Uploader
    40  	dirOnly  bool
    41  }
    42  
    43  // New returns a new Reporter.
    44  func New(cfg config.Getter, opener io.Opener, uploader *resultstore.Uploader, dirOnly bool) *Reporter {
    45  	return &Reporter{
    46  		cfg:      cfg,
    47  		opener:   opener,
    48  		uploader: uploader,
    49  		dirOnly:  dirOnly,
    50  	}
    51  }
    52  
    53  // GetName returns the name of this reporter.
    54  func (r *Reporter) GetName() string {
    55  	return "resultstorereporter"
    56  }
    57  
    58  // ShouldReport returns whether results should be reported for this
    59  // job at this time.
    60  func (r *Reporter) ShouldReport(ctx context.Context, log *logrus.Entry, pj *v1.ProwJob) bool {
    61  	if !pj.Spec.Report {
    62  		return false
    63  	}
    64  
    65  	// Require configured ResultStore ProjectID for now. It may be determined
    66  	// automatically from storage in the future.
    67  	if projectID(pj) == "" {
    68  		return false
    69  	}
    70  
    71  	// ResultStore requires files stored in GCS.
    72  	if !util.IsGCSDestination(r.cfg, pj) {
    73  		return false
    74  	}
    75  
    76  	if !pj.Complete() {
    77  		// TODO: Change to debug or remove after alpha testing.
    78  		log.Infof("job not finished")
    79  		return false
    80  	}
    81  
    82  	return true
    83  }
    84  
    85  func projectID(pj *v1.ProwJob) string {
    86  	if d := pj.Spec.ProwJobDefault; d != nil && d.ResultStoreConfig != nil {
    87  		return d.ResultStoreConfig.ProjectID
    88  	}
    89  	return ""
    90  }
    91  
    92  // Report reports results for this job to ResultStore.
    93  func (r *Reporter) Report(ctx context.Context, log *logrus.Entry, pj *v1.ProwJob) ([]*v1.ProwJob, *reconcile.Result, error) {
    94  	bucket, dir, err := util.GetJobDestination(r.cfg, pj)
    95  	if err != nil {
    96  		return nil, nil, err
    97  	}
    98  	path, err := providers.StoragePath(bucket, dir)
    99  	if err != nil {
   100  		return nil, nil, err
   101  	}
   102  	log = log.WithField("BuildID", pj.Status.BuildID)
   103  	started := readStartedFile(ctx, log, r.opener, path)
   104  	finished := readFinishedFile(ctx, log, r.opener, path)
   105  
   106  	files, err := resultstore.ArtifactFiles(ctx, r.opener, resultstore.ArtifactOpts{
   107  		Dir:              path,
   108  		ArtifactsDirOnly: r.dirOnly,
   109  		DefaultFiles:     defaultFiles(pj),
   110  	})
   111  	if err != nil {
   112  		// Log and continue in case of errors.
   113  		log.WithError(err).Errorf("error reading artifact files from %q", path)
   114  	}
   115  	err = r.uploader.Upload(ctx, log, &resultstore.Payload{
   116  		Job:       pj,
   117  		Started:   started,
   118  		Finished:  finished,
   119  		Files:     files,
   120  		ProjectID: projectID(pj),
   121  	})
   122  	return []*v1.ProwJob{pj}, nil, err
   123  }
   124  
   125  func readFinishedFile(ctx context.Context, log *logrus.Entry, opener io.Opener, dir string) *metadata.Finished {
   126  	n := dir + "/" + v1.FinishedStatusFile
   127  	bs, err := io.ReadContent(ctx, log, opener, n)
   128  	if err != nil {
   129  		log.WithError(err).Errorf("Failed to read %q", n)
   130  		return nil
   131  	}
   132  	var finished metadata.Finished
   133  	if err := json.Unmarshal(bs, &finished); err != nil {
   134  		log.WithError(err).Errorf("Error unmarshalling %v", n)
   135  		return nil
   136  	}
   137  	return &finished
   138  }
   139  
   140  func readStartedFile(ctx context.Context, log *logrus.Entry, opener io.Opener, dir string) *metadata.Started {
   141  	n := dir + "/" + v1.StartedStatusFile
   142  	bs, err := io.ReadContent(ctx, log, opener, n)
   143  	if err != nil {
   144  		log.WithError(err).Warnf("Failed to read %q", v1.StartedStatusFile)
   145  		return nil
   146  	}
   147  	var started metadata.Started
   148  	if err := json.Unmarshal(bs, &started); err != nil {
   149  		log.WithError(err).Warnf("Failed to unmarshal %q", n)
   150  		return nil
   151  	}
   152  	return &started
   153  }
   154  
   155  // defaultFiles returns the files to ensure are uploaded to
   156  // ResultStore, even if not (yet) present.
   157  func defaultFiles(pj *v1.ProwJob) []resultstore.DefaultFile {
   158  	var fs []resultstore.DefaultFile
   159  
   160  	// There is a race with the GCS reporter writing prowjob.json and
   161  	// finished.json, so provide these as defaults. In the unlikely
   162  	// case of error, skip it since the GCS reporter won't write it.
   163  	if bs, err := util.MarshalProwJob(pj); err == nil {
   164  		fs = append(fs, resultstore.DefaultFile{
   165  			Name: "prowjob.json",
   166  			Size: int64(len(bs)),
   167  		})
   168  	}
   169  	if bs, err := util.MarshalFinishedJSON(pj); err == nil {
   170  		fs = append(fs, resultstore.DefaultFile{
   171  			Name: "finished.json",
   172  			Size: int64(len(bs)),
   173  		})
   174  	}
   175  	return fs
   176  }