go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/provenance/reporter/reporter.go (about) 1 // Copyright 2022 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package reporter 16 17 import ( 18 "context" 19 "errors" 20 "fmt" 21 "os" 22 23 "google.golang.org/grpc/codes" 24 "google.golang.org/grpc/status" 25 "google.golang.org/protobuf/types/known/timestamppb" 26 27 "go.chromium.org/luci/common/clock" 28 "go.chromium.org/luci/common/logging" 29 snooperpb "go.chromium.org/luci/provenance/api/snooperpb/v1" 30 ) 31 32 var ErrServiceUnavailable = errors.New("local provenance service unavailable") 33 34 // Report implements all provenance interfaces. 35 // 36 // This can be used as a caching opportunity by users for storing client for 37 // longer use. 38 // TODO(crbug/1269830): Implement a custom retry logic for transient errors. 39 // Custom retry is needed because grpc status `Unavailable` is tagged as 40 // transient. In this application, `ErrServiceUnavailable` is a permanent but 41 // acceptable error code. 42 type Report struct { 43 RClient snooperpb.SelfReportClient 44 } 45 46 // ReportCipdAdmission reports a local cipd admission to provenance. 47 // 48 // It returns a success status and annotated error. Status is to indicate user 49 // whether to block further execution. 50 // If local provenance service is unavailable, it will return an ok status and 51 // annotated error. This is to indicate, the user should continue normal 52 // execution. 53 // All other errors are annotated to indicate permanent failures. 54 func (r *Report) ReportCipdAdmission(ctx context.Context, pkgName, iid string) (bool, error) { 55 req := &snooperpb.ReportCipdRequest{ 56 CipdReport: &snooperpb.CipdReport{ 57 PackageName: pkgName, 58 Iid: iid, 59 EventTs: timestamppb.New(clock.Now(ctx)), 60 }, 61 } 62 63 _, err := r.RClient.ReportCipd(ctx, req) 64 switch errS, _ := status.FromError(err); errS.Code() { 65 case codes.OK: 66 logging.Infof(ctx, "success to report cipd admission") 67 return true, nil 68 case codes.Unavailable: 69 logging.Errorf(ctx, "failed to report cipd admission: %v", ErrServiceUnavailable) 70 return true, ErrServiceUnavailable 71 default: 72 logging.Errorf(ctx, "failed to report cipd admission: %v", err) 73 return false, err 74 } 75 } 76 77 // ReportGitCheckout reports a local git checkout/fetch to provenance. 78 // 79 // It returns a success status and annotated error. Status is to indicate user 80 // whether to block further execution. 81 // If local provenance service is unavailable, it will return an ok status and 82 // annotated error. This is to indicate, the user should continue normal 83 // execution. 84 // All other errors are annotated to indicate permanent failures. 85 func (r *Report) ReportGitCheckout(ctx context.Context, repo, commit, ref string) (bool, error) { 86 req := &snooperpb.ReportGitRequest{ 87 GitReport: &snooperpb.GitReport{ 88 Repo: repo, 89 Commit: commit, 90 Refs: ref, 91 EventTs: timestamppb.New(clock.Now(ctx)), 92 }, 93 } 94 95 _, err := r.RClient.ReportGit(ctx, req) 96 switch errS, _ := status.FromError(err); errS.Code() { 97 case codes.OK: 98 logging.Infof(ctx, "success to report git checkout") 99 return true, nil 100 case codes.Unavailable: 101 logging.Errorf(ctx, "failed to report git checkout: %v", ErrServiceUnavailable) 102 return true, ErrServiceUnavailable 103 default: 104 logging.Errorf(ctx, "failed to report git checkout: %v", err) 105 return false, err 106 } 107 } 108 109 // ReportGcsDownload reports a local gcs download to provenance. 110 // 111 // It returns a success status and annotated error. Status is to indicate user 112 // whether to block further execution. 113 // If local provenance service is unavailable, it will return an ok status and 114 // annotated error. This is to indicate, the user should continue normal 115 // execution. 116 // All other errors are annotated to indicate permanent failures. 117 func (r *Report) ReportGcsDownload(ctx context.Context, uri, digest string) (bool, error) { 118 req := &snooperpb.ReportGcsRequest{ 119 GcsReport: &snooperpb.GcsReport{ 120 GcsUri: uri, 121 Digest: digest, 122 EventTs: timestamppb.New(clock.Now(ctx)), 123 }, 124 } 125 126 _, err := r.RClient.ReportGcs(ctx, req) 127 switch errS, _ := status.FromError(err); errS.Code() { 128 case codes.OK: 129 logging.Infof(ctx, "success to report gcs download") 130 return true, nil 131 case codes.Unavailable: 132 logging.Errorf(ctx, "failed to report gcs download: %v", ErrServiceUnavailable) 133 return true, ErrServiceUnavailable 134 default: 135 logging.Errorf(ctx, "failed to report gcs download: %v", err) 136 return false, err 137 } 138 } 139 140 // ReportStage reports task stage via provenance local server. 141 // 142 // It returns a success status and annotated error. Status is to indicate user 143 // whether to block further execution. 144 // If local provenance service is unavailable, it will return an ok status and 145 // annotated error. This is to indicate, the user should continue normal 146 // execution. 147 // All other errors are annotated to indicate permanent failures. 148 // TODO: Use go struct when a new parameter is added. 149 func (r *Report) ReportStage(ctx context.Context, stage snooperpb.TaskStage, recipe string, pid int64) (bool, error) { 150 // Must pass recipe name and pid when reporting task start. 151 if stage == snooperpb.TaskStage_STARTED && (recipe == "" || pid == 0) { 152 logging.Errorf(ctx, "failed to export task stage") 153 return false, fmt.Errorf("a recipe and pid must be provided when task starts") 154 } 155 156 req := &snooperpb.ReportTaskStageRequest{ 157 TaskStage: stage, 158 Timestamp: timestamppb.New(clock.Now(ctx)), 159 // required when task starts 160 Recipe: recipe, 161 Pid: pid, 162 } 163 164 _, err := r.RClient.ReportTaskStage(ctx, req) 165 switch errS, _ := status.FromError(err); errS.Code() { 166 case codes.OK: 167 logging.Infof(ctx, "success to report task stage") 168 return true, nil 169 case codes.Unavailable: 170 logging.Errorf(ctx, "failed to report task stage: %v", ErrServiceUnavailable) 171 return true, ErrServiceUnavailable 172 default: 173 logging.Errorf(ctx, "failed to report task stage: %v", err) 174 return false, err 175 } 176 } 177 178 // ReportPID reports process id to provenance local server for tracking. 179 // 180 // It returns a success status and annotated error. Status is to indicate user 181 // whether to block further execution. 182 // 183 // If local provenance service is unavailable, it will return an ok status and 184 // annotated error. This is to indicate that the user should continue normal 185 // execution. 186 // All other errors are annotated to indicate permanent failures. 187 func (r *Report) ReportPID(ctx context.Context, pid int64) (bool, error) { 188 // Must pass all arguments when reporting pid. 189 if pid == 0 { 190 logging.Errorf(ctx, "failed to export pid") 191 return false, fmt.Errorf("pid must be present") 192 } 193 194 // Find who called this report and include in the report. 195 // This will be an absolute path of the executable that invoked this 196 // process. 197 reporter, err := os.Executable() 198 if err != nil { 199 logging.Errorf(ctx, "failed to export pid") 200 return false, err 201 } 202 203 req := &snooperpb.ReportPIDRequest{ 204 Pid: pid, 205 Reporter: reporter, 206 } 207 208 _, err = r.RClient.ReportPID(ctx, req) 209 switch errS, _ := status.FromError(err); errS.Code() { 210 case codes.OK: 211 logging.Infof(ctx, "succeeded to report task pid") 212 return true, nil 213 case codes.Unavailable: 214 logging.Errorf(ctx, "failed to report task pid: %v", ErrServiceUnavailable) 215 return true, ErrServiceUnavailable 216 default: 217 logging.Errorf(ctx, "failed to report task pid: %v", err) 218 return false, err 219 } 220 } 221 222 // ReportCipdDigest reports digest of built cipd package to provenance. 223 // 224 // It returns a success status and annotated error. Status is to indicate user 225 // whether to block further execution. 226 // If local provenance service is unavailable, it will return an ok status and 227 // annotated error. This is to indicate, the user should continue normal 228 // execution. 229 // All other errors are annotated to indicate permanent failures. 230 func (r *Report) ReportCipdDigest(ctx context.Context, digest, pkgName, iid string) (bool, error) { 231 req := &snooperpb.ReportArtifactDigestRequest{ 232 Digest: digest, 233 Artifact: &snooperpb.Artifact{ 234 Kind: &snooperpb.Artifact_Cipd{ 235 Cipd: &snooperpb.Artifact_CIPD{ 236 PackageName: pkgName, 237 InstanceId: iid, 238 }, 239 }, 240 }, 241 } 242 243 _, err := r.RClient.ReportArtifactDigest(ctx, req) 244 switch errS, _ := status.FromError(err); errS.Code() { 245 case codes.OK: 246 logging.Infof(ctx, "success to report cipd digest") 247 return true, nil 248 case codes.Unavailable: 249 logging.Errorf(ctx, "failed to report cipd digest: %v", ErrServiceUnavailable) 250 return true, ErrServiceUnavailable 251 default: 252 logging.Errorf(ctx, "failed to report cipd digest: %v", err) 253 return false, err 254 } 255 } 256 257 // ReportGcsDigest reports digest of a built gcs app to provenance. 258 // 259 // It returns a success status and annotated error. Status is to indicate user 260 // whether to block further execution. 261 // If local provenance service is unavailable, it will return an ok status and 262 // annotated error. This is to indicate, the user should continue normal 263 // execution. 264 // All other errors are annotated to indicate permanent failures. 265 func (r *Report) ReportGcsDigest(ctx context.Context, digest, gcsURI string) (bool, error) { 266 req := &snooperpb.ReportArtifactDigestRequest{ 267 Digest: digest, 268 Artifact: &snooperpb.Artifact{ 269 Kind: &snooperpb.Artifact_Gcs{ 270 Gcs: gcsURI, 271 }, 272 }, 273 } 274 275 _, err := r.RClient.ReportArtifactDigest(ctx, req) 276 switch errS, _ := status.FromError(err); errS.Code() { 277 case codes.OK: 278 logging.Infof(ctx, "success to report gcs digest") 279 return true, nil 280 case codes.Unavailable: 281 logging.Errorf(ctx, "failed to report gcs digest: %v", ErrServiceUnavailable) 282 return true, ErrServiceUnavailable 283 default: 284 logging.Errorf(ctx, "failed to report gcs digest: %v", err) 285 return false, err 286 } 287 } 288 289 // ReportSbomDigest reports digest of a built gcs SBOM to provenance. 290 // 291 // It returns a success status and annotated error. Status is to indicate user 292 // whether to block further execution. 293 // If local provenance service is unavailable, it will return an ok status and 294 // annotated error. This is to indicate, the user should continue normal 295 // execution. 296 // All other errors are annotated to indicate permanent failures. 297 func (r *Report) ReportSbomDigest(ctx context.Context, digest, gcsURI string, subjectDigests []string) (bool, error) { 298 req := &snooperpb.ReportArtifactDigestRequest{ 299 Digest: digest, 300 Artifact: &snooperpb.Artifact{ 301 Kind: &snooperpb.Artifact_Gcs{ 302 Gcs: gcsURI, 303 }, 304 }, 305 SbomSubjects: subjectDigests, 306 } 307 308 _, err := r.RClient.ReportArtifactDigest(ctx, req) 309 switch errS, _ := status.FromError(err); errS.Code() { 310 case codes.OK: 311 logging.Infof(ctx, "success to report sbom digest") 312 return true, nil 313 case codes.Unavailable: 314 logging.Errorf(ctx, "failed to report sbom digest: %v", ErrServiceUnavailable) 315 return true, ErrServiceUnavailable 316 default: 317 logging.Errorf(ctx, "failed to report sbom digest: %v", err) 318 return false, err 319 } 320 }