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 }