github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/pkg/crier/reporters/pubsub/reporter.go (about) 1 /* 2 Copyright 2018 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 reporter contains helpers for publishing statues to Pub 18 // statuses in GitHub. 19 package pubsub 20 21 import ( 22 "context" 23 "encoding/json" 24 "fmt" 25 "strings" 26 "time" 27 28 "cloud.google.com/go/pubsub" 29 "github.com/sirupsen/logrus" 30 "sigs.k8s.io/controller-runtime/pkg/reconcile" 31 32 prowapi "sigs.k8s.io/prow/pkg/apis/prowjobs/v1" 33 "sigs.k8s.io/prow/pkg/config" 34 "sigs.k8s.io/prow/pkg/crier/reporters/criercommonlib" 35 "sigs.k8s.io/prow/pkg/io/providers" 36 "sigs.k8s.io/prow/pkg/spyglass/api" 37 ) 38 39 const ( 40 // PubSubProjectLabel annotation 41 PubSubProjectLabel = "prow.k8s.io/pubsub.project" 42 // PubSubTopicLabel annotation 43 PubSubTopicLabel = "prow.k8s.io/pubsub.topic" 44 // PubSubRunIDLabel annotation 45 PubSubRunIDLabel = "prow.k8s.io/pubsub.runID" 46 ) 47 48 // ReportMessage is a message structure used to pass a prowjob status to Pub/Sub topic.s 49 type ReportMessage struct { 50 Project string `json:"project"` 51 Topic string `json:"topic"` 52 RunID string `json:"runid"` 53 Status prowapi.ProwJobState `json:"status"` 54 URL string `json:"url"` 55 GCSPath string `json:"gcs_path"` 56 Refs []prowapi.Refs `json:"refs,omitempty"` 57 JobType prowapi.ProwJobType `json:"job_type"` 58 JobName string `json:"job_name"` 59 Message string `json:"message,omitempty"` 60 } 61 62 // Client is a reporter client fed to crier controller 63 type Client struct { 64 config config.Getter 65 } 66 67 // NewReporter creates a new Pub/Sub reporter 68 func NewReporter(cfg config.Getter) *Client { 69 return &Client{ 70 config: cfg, 71 } 72 } 73 74 // GetName returns the name of the reporter 75 func (c *Client) GetName() string { 76 return "pubsub-reporter" 77 } 78 79 func findLabels(pj *prowapi.ProwJob, labels ...string) map[string]string { 80 // Support checking for both labels(deprecated) and annotations(new) for backward compatibility 81 pubSubMap := map[string]string{} 82 for _, label := range labels { 83 if pj.Annotations[label] != "" { 84 pubSubMap[label] = pj.Annotations[label] 85 } else { 86 pubSubMap[label] = pj.Labels[label] 87 } 88 } 89 return pubSubMap 90 } 91 92 // ShouldReport tells if a prowjob should be reported by this reporter 93 func (c *Client) ShouldReport(_ context.Context, _ *logrus.Entry, pj *prowapi.ProwJob) bool { 94 pubSubMap := findLabels(pj, PubSubProjectLabel, PubSubTopicLabel) 95 return pubSubMap[PubSubProjectLabel] != "" && pubSubMap[PubSubTopicLabel] != "" 96 } 97 98 // Report takes a prowjob, and generate a pubsub ReportMessage and publish to specific Pub/Sub topic 99 // based on Pub/Sub related labels if they exist in this prowjob 100 func (c *Client) Report(ctx context.Context, l *logrus.Entry, pj *prowapi.ProwJob) ([]*prowapi.ProwJob, *reconcile.Result, error) { 101 ctx, cancel := context.WithTimeout(ctx, 10*time.Second) 102 defer cancel() 103 104 message := c.generateMessageFromPJ(pj) 105 // TODO: Consider caching the pubsub client. 106 client, err := pubsub.NewClient(ctx, message.Project) 107 if err != nil { 108 return nil, nil, fmt.Errorf("could not create pubsub Client: %w", err) 109 } 110 defer func() { 111 logrus.WithError(client.Close()).Debug("Closed pubsub client.") 112 }() 113 114 l = l.WithFields(logrus.Fields{"project": message.Project, "topic": message.Topic, "run-id": message.RunID, "status": pj.Status.State}) 115 l.Debug("Reporting prowjob status to pubsub.") 116 topic := client.Topic(message.Topic) 117 defer topic.Stop() // Sends remaining messages then stops goroutines. 118 119 d, err := json.Marshal(message) 120 if err != nil { 121 l.WithError(err).Debug("Failed marshalling pubsub message.") 122 return nil, nil, fmt.Errorf("could not marshal pubsub report: %w", err) 123 } 124 125 res := topic.Publish(ctx, &pubsub.Message{ 126 Data: d, 127 }) 128 129 _, err = res.Get(ctx) 130 if err != nil { 131 wrappedError := fmt.Errorf( 132 "failed to publish pubsub message with run ID %q to topic: \"%s/%s\". %v", 133 message.RunID, message.Project, message.Topic, err) 134 135 // It would be a user error if the topic doesn't exist, return a user 136 // error in this case so that we can avoid logging on error level. 137 topicExist, existErr := topic.Exists(ctx) 138 if existErr == nil && !topicExist { 139 l.Debug("Pubsub topic doesn't exist.") 140 return nil, nil, criercommonlib.UserError(wrappedError) 141 } 142 143 l.WithError(err).Debug("Failed sending pubsub message.") 144 return nil, nil, wrappedError 145 } 146 147 return []*prowapi.ProwJob{pj}, nil, nil 148 } 149 150 func (c *Client) generateMessageFromPJ(pj *prowapi.ProwJob) *ReportMessage { 151 pubSubMap := findLabels(pj, PubSubProjectLabel, PubSubTopicLabel, PubSubRunIDLabel) 152 var refs []prowapi.Refs 153 if pj.Spec.Refs != nil { 154 refs = append(refs, *pj.Spec.Refs) 155 } 156 refs = append(refs, pj.Spec.ExtraRefs...) 157 158 var storagePath string 159 // calculate storagePath if pj.Status.URL is set 160 if pj.Status.URL != "" { 161 // example: 162 // * pj.Status.URL: https://prow.k8s.io/view/gs/kubernetes-jenkins/logs/ci-benchmark-microbenchmarks/1258197944759226371 163 // * prefix: https://prow.k8s.io/view/ 164 // * storageURLPath: gs/kubernetes-jenkins/logs/ci-benchmark-microbenchmarks/1258197944759226371 165 prefix := c.config().Plank.GetJobURLPrefix(pj) 166 167 storageURLPath := strings.TrimPrefix(pj.Status.URL, prefix) 168 if strings.HasPrefix(storageURLPath, api.GCSKeyType) { 169 storageURLPath = strings.Replace(storageURLPath, api.GCSKeyType, providers.GS, 1) 170 } 171 172 if providers.HasStorageProviderPrefix(storageURLPath) { 173 storagePathSegments := strings.SplitN(storageURLPath, "/", 2) 174 if len(storagePathSegments) == 1 { 175 storagePath = storagePathSegments[0] 176 } else { 177 storagePath = fmt.Sprintf("%s://%s", storagePathSegments[0], storagePathSegments[1]) 178 } 179 } else { 180 storagePath = fmt.Sprintf("%s://%s", providers.GS, storageURLPath) 181 } 182 183 } 184 185 return &ReportMessage{ 186 Project: pubSubMap[PubSubProjectLabel], 187 Topic: pubSubMap[PubSubTopicLabel], 188 RunID: pubSubMap[PubSubRunIDLabel], 189 Status: pj.Status.State, 190 URL: pj.Status.URL, 191 GCSPath: storagePath, 192 Refs: refs, 193 JobType: pj.Spec.Type, 194 JobName: pj.Spec.Job, 195 Message: pj.Status.Description, 196 } 197 }