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  }