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  }