github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/pkg/pubsub/subscriber/subscriber.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 subscriber 18 19 import ( 20 "context" 21 "encoding/json" 22 "fmt" 23 "strings" 24 25 "cloud.google.com/go/pubsub" 26 27 "github.com/prometheus/client_golang/prometheus" 28 "github.com/sirupsen/logrus" 29 "sigs.k8s.io/controller-runtime/pkg/reconcile" 30 prowcrd "sigs.k8s.io/prow/pkg/apis/prowjobs/v1" 31 "sigs.k8s.io/prow/pkg/config" 32 "sigs.k8s.io/prow/pkg/gangway" 33 "sigs.k8s.io/prow/pkg/kube" 34 ) 35 36 const ( 37 ProwEventType = "prow.k8s.io/pubsub.EventType" 38 PeriodicProwJobEvent = "prow.k8s.io/pubsub.PeriodicProwJobEvent" 39 PresubmitProwJobEvent = "prow.k8s.io/pubsub.PresubmitProwJobEvent" 40 PostsubmitProwJobEvent = "prow.k8s.io/pubsub.PostsubmitProwJobEvent" 41 ) 42 43 // ProwJobEvent contains the minimum information required to start a ProwJob. 44 type ProwJobEvent struct { 45 Name string `json:"name"` 46 // Refs are used by presubmit and postsubmit jobs supplying baseSHA and SHA 47 Refs *prowcrd.Refs `json:"refs,omitempty"` 48 Envs map[string]string `json:"envs,omitempty"` 49 Labels map[string]string `json:"labels,omitempty"` 50 Annotations map[string]string `json:"annotations,omitempty"` 51 } 52 53 // FromPayload set the ProwJobEvent from the PubSub message payload. 54 func (pe *ProwJobEvent) FromPayload(data []byte) error { 55 if err := json.Unmarshal(data, pe); err != nil { 56 return err 57 } 58 return nil 59 } 60 61 // ToMessage generates a PubSub Message from a ProwJobEvent. 62 func (pe *ProwJobEvent) ToMessage() (*pubsub.Message, error) { 63 return pe.ToMessageOfType(PeriodicProwJobEvent) 64 } 65 66 // ToMessage generates a PubSub Message from a ProwJobEvent. 67 func (pe *ProwJobEvent) ToMessageOfType(t string) (*pubsub.Message, error) { 68 data, err := json.Marshal(pe) 69 if err != nil { 70 return nil, err 71 } 72 message := pubsub.Message{ 73 Data: data, 74 Attributes: map[string]string{ 75 ProwEventType: t, 76 }, 77 } 78 return &message, nil 79 } 80 81 // Subscriber handles Pub/Sub subscriptions, update metrics, 82 // validates them using Prow Configuration and 83 // use a ProwJobClient to create Prow Jobs. 84 type Subscriber struct { 85 ConfigAgent *config.Agent 86 Metrics *Metrics 87 ProwJobClient gangway.ProwJobClient 88 Reporter reportClient 89 InRepoConfigGetter config.InRepoConfigGetter 90 } 91 92 type messageInterface interface { 93 getAttributes() map[string]string 94 getPayload() []byte 95 getID() string 96 ack() 97 nack() 98 } 99 100 type reportClient interface { 101 Report(ctx context.Context, log *logrus.Entry, pj *prowcrd.ProwJob) ([]*prowcrd.ProwJob, *reconcile.Result, error) 102 ShouldReport(ctx context.Context, log *logrus.Entry, pj *prowcrd.ProwJob) bool 103 } 104 105 type pubSubMessage struct { 106 pubsub.Message 107 } 108 109 func (m *pubSubMessage) getAttributes() map[string]string { 110 return m.Attributes 111 } 112 113 func (m *pubSubMessage) getPayload() []byte { 114 return m.Data 115 } 116 117 func (m *pubSubMessage) getID() string { 118 return m.ID 119 } 120 121 func (m *pubSubMessage) ack() { 122 m.Message.Ack() 123 } 124 func (m *pubSubMessage) nack() { 125 m.Message.Nack() 126 } 127 128 func extractFromAttribute(attrs map[string]string, key string) (string, error) { 129 value, ok := attrs[key] 130 if !ok { 131 return "", fmt.Errorf("unable to find %q from the attributes", key) 132 } 133 return value, nil 134 } 135 136 func (s *Subscriber) getReporterFunc(l *logrus.Entry) gangway.ReporterFunc { 137 return func(pj *prowcrd.ProwJob, state prowcrd.ProwJobState, err error) { 138 pj.Status.State = state 139 pj.Status.Description = "Successfully triggered prowjob." 140 if err != nil { 141 pj.Status.Description = fmt.Sprintf("Failed creating prowjob: %v", err) 142 } 143 if s.Reporter.ShouldReport(context.TODO(), l, pj) { 144 if _, _, err := s.Reporter.Report(context.TODO(), l, pj); err != nil { 145 l.WithError(err).Warning("Failed to report status.") 146 } 147 } 148 } 149 } 150 151 func (s *Subscriber) handleMessage(msg messageInterface, subscription string, allowedClusters []string) error { 152 153 msgID := msg.getID() 154 l := logrus.WithFields(logrus.Fields{ 155 "pubsub-subscription": subscription, 156 "pubsub-id": msgID}) 157 158 // First, convert the incoming message into a CreateJobExecutionRequest type. 159 cjer, err := s.msgToCjer(l, msg, subscription) 160 if err != nil { 161 return err 162 } 163 164 // Do not check for HTTP client authorization, because we're handling a 165 // PubSub message. 166 var allowedApiClient *config.AllowedApiClient = nil 167 var requireTenantID bool = false 168 169 cfgAdapter := gangway.ProwCfgAdapter{Config: s.ConfigAgent.Config()} 170 if _, err = gangway.HandleProwJob(l, s.getReporterFunc(l), cjer, s.ProwJobClient, &cfgAdapter, s.InRepoConfigGetter, allowedApiClient, requireTenantID, allowedClusters); err != nil { 171 l.WithError(err).Info("failed to create Prow Job") 172 s.Metrics.ErrorCounter.With(prometheus.Labels{ 173 subscriptionLabel: subscription, 174 // This should be the only case prow operator should pay more 175 // attention too, because errors here are more likely caused by 176 // prow. (There are exceptions, which we can iterate slightly later) 177 errorTypeLabel: "failed-handle-prowjob", 178 }).Inc() 179 } 180 181 // TODO(chaodaiG): debugging purpose, remove once done debugging. 182 l.WithField("payload", string(msg.getPayload())).WithField("post-id", msg.getID()).Debug("Finished handling message") 183 return err 184 } 185 186 // msgToCjer converts an incoming message (PubSub message) into a CJER. It 187 // actually does 2 conversions --- from the message to ProwJobEvent (in order to 188 // unmarshal the raw bytes) then again from ProwJobEvent to a CJER. 189 func (s *Subscriber) msgToCjer(l *logrus.Entry, msg messageInterface, subscription string) (*gangway.CreateJobExecutionRequest, error) { 190 msgAttributes := msg.getAttributes() 191 msgPayload := msg.getPayload() 192 193 l.WithField("payload", string(msgPayload)).Debug("Received message") 194 s.Metrics.MessageCounter.With(prometheus.Labels{subscriptionLabel: subscription}).Inc() 195 196 // Note that a CreateJobExecutionRequest is a superset of ProwJobEvent. 197 // However we still use ProwJobEvent here because we want to use the 198 // existing jobHandlers to fetch the prowJobSpec (and the jobHandlers expect 199 // a ProwJobEvent as an argument). 200 var pe ProwJobEvent 201 202 // We use ProwJobEvent here mainly to ensrue that the incoming payload 203 // (JSON) is well-formed. We convert it into a CreateJobExecutionRequest 204 // type here and never use it anywhere else. 205 l.WithField("raw-payload", string(msgPayload)).Debug("Raw payload passed in handleProwJob.") 206 if err := pe.FromPayload(msgPayload); err != nil { 207 return nil, err 208 } 209 210 eType, err := extractFromAttribute(msgAttributes, ProwEventType) 211 if err != nil { 212 l.WithError(err).Error("failed to read message") 213 s.Metrics.ErrorCounter.With(prometheus.Labels{ 214 subscriptionLabel: subscription, 215 errorTypeLabel: "malformed-message", 216 }).Inc() 217 return nil, err 218 } 219 220 return s.peToCjer(l, &pe, eType, subscription) 221 } 222 223 func (s *Subscriber) peToCjer(l *logrus.Entry, pe *ProwJobEvent, eType, subscription string) (*gangway.CreateJobExecutionRequest, error) { 224 225 cjer := gangway.CreateJobExecutionRequest{ 226 JobName: strings.TrimSpace(pe.Name), 227 } 228 229 // First encode the job type. 230 switch eType { 231 case PeriodicProwJobEvent: 232 cjer.JobExecutionType = gangway.JobExecutionType_PERIODIC 233 case PresubmitProwJobEvent: 234 cjer.JobExecutionType = gangway.JobExecutionType_PRESUBMIT 235 case PostsubmitProwJobEvent: 236 cjer.JobExecutionType = gangway.JobExecutionType_POSTSUBMIT 237 default: 238 l.WithField("type", eType).Info("Unsupported event type") 239 s.Metrics.ErrorCounter.With(prometheus.Labels{ 240 subscriptionLabel: subscription, 241 errorTypeLabel: "unsupported-event-type", 242 }).Inc() 243 return nil, fmt.Errorf("unsupported event type: %s", eType) 244 } 245 246 pso := gangway.PodSpecOptions{} 247 pso.Labels = make(map[string]string) 248 for k, v := range pe.Labels { 249 pso.Labels[k] = v 250 } 251 252 pso.Annotations = make(map[string]string) 253 for k, v := range pe.Annotations { 254 pso.Annotations[k] = v 255 } 256 257 pso.Envs = make(map[string]string) 258 for k, v := range pe.Envs { 259 pso.Envs[k] = v 260 } 261 262 cjer.PodSpecOptions = &pso 263 264 var err error 265 266 if pe.Refs != nil { 267 cjer.Refs, err = gangway.FromCrdRefs(pe.Refs) 268 if err != nil { 269 return nil, err 270 } 271 272 // Add "https://" prefix to orgRepo if this is a gerrit job. 273 // (Unfortunately gerrit jobs use the full repo URL as the identifier.) 274 prefix := "https://" 275 if pso.Labels[kube.GerritRevision] != "" && !strings.HasPrefix(cjer.Refs.Org, prefix) { 276 cjer.Refs.Org = prefix + cjer.Refs.Org 277 } 278 } 279 280 return &cjer, nil 281 }