github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/prow/pubsub/subscriber/server.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  	"net/http"
    24  	"reflect"
    25  
    26  	"github.com/sirupsen/logrus"
    27  
    28  	"cloud.google.com/go/pubsub"
    29  	"github.com/prometheus/client_golang/prometheus"
    30  	"golang.org/x/sync/errgroup"
    31  	"k8s.io/test-infra/prow/config"
    32  )
    33  
    34  const (
    35  	tokenLabel = "token"
    36  )
    37  
    38  type message struct {
    39  	Attributes map[string]string
    40  	Data       []byte
    41  	ID         string `json:"message_id"`
    42  }
    43  
    44  // pushRequest is the format of the push Pub/Sub subscription received form the WebHook.
    45  type pushRequest struct {
    46  	Message      message
    47  	Subscription string
    48  }
    49  
    50  // PushServer implements http.Handler. It validates incoming Pub/Sub subscriptions handle them.
    51  type PushServer struct {
    52  	Subscriber     *Subscriber
    53  	TokenGenerator func() []byte
    54  }
    55  
    56  // PullServer listen to Pull Pub/Sub subscriptions and handle them.
    57  type PullServer struct {
    58  	Subscriber *Subscriber
    59  	Client     pubsubClientInterface
    60  }
    61  
    62  // NewPullServer creates a new PullServer
    63  func NewPullServer(s *Subscriber) *PullServer {
    64  	return &PullServer{
    65  		Subscriber: s,
    66  		Client:     &pubSubClient{},
    67  	}
    68  }
    69  
    70  // ServeHTTP validates an incoming Push Pub/Sub subscription and handle them.
    71  func (s *PushServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    72  	HTTPCode := http.StatusOK
    73  	subscription := "unknown-subscription"
    74  	var finalError error
    75  
    76  	defer func() {
    77  		s.Subscriber.Metrics.ResponseCounter.With(prometheus.Labels{
    78  			subscriptionLabel: subscription,
    79  			responseCodeLabel: string(HTTPCode),
    80  		}).Inc()
    81  		if finalError != nil {
    82  			http.Error(w, finalError.Error(), HTTPCode)
    83  		}
    84  	}()
    85  
    86  	if s.TokenGenerator != nil {
    87  		token := r.URL.Query().Get(tokenLabel)
    88  		if token != string(s.TokenGenerator()) {
    89  			finalError = fmt.Errorf("wrong token")
    90  			HTTPCode = http.StatusForbidden
    91  			return
    92  		}
    93  	}
    94  	// Get the payload and act on it.
    95  	pr := &pushRequest{}
    96  	if err := json.NewDecoder(r.Body).Decode(pr); err != nil {
    97  		finalError = err
    98  		HTTPCode = http.StatusBadRequest
    99  		return
   100  	}
   101  
   102  	msg := pubsub.Message{
   103  		Data:       pr.Message.Data,
   104  		ID:         pr.Message.ID,
   105  		Attributes: pr.Message.Attributes,
   106  	}
   107  
   108  	if err := s.Subscriber.handleMessage(&pubSubMessage{Message: msg}, pr.Subscription); err != nil {
   109  		finalError = err
   110  		HTTPCode = http.StatusNotModified
   111  		return
   112  	}
   113  }
   114  
   115  // For testing
   116  type subscriptionInterface interface {
   117  	string() string
   118  	receive(ctx context.Context, f func(context.Context, messageInterface)) error
   119  }
   120  
   121  // pubsubClientInterface interfaces with Cloud Pub/Sub client for testing reason
   122  type pubsubClientInterface interface {
   123  	new(ctx context.Context, project string) (pubsubClientInterface, error)
   124  	subscription(id string) subscriptionInterface
   125  }
   126  
   127  // pubSubClient is used to interface with a new Cloud Pub/Sub Client
   128  type pubSubClient struct {
   129  	client *pubsub.Client
   130  }
   131  
   132  type pubSubSubscription struct {
   133  	sub *pubsub.Subscription
   134  }
   135  
   136  func (s *pubSubSubscription) string() string {
   137  	return s.sub.String()
   138  }
   139  
   140  func (s *pubSubSubscription) receive(ctx context.Context, f func(context.Context, messageInterface)) error {
   141  	g := func(ctx2 context.Context, msg2 *pubsub.Message) {
   142  		f(ctx2, &pubSubMessage{Message: *msg2})
   143  	}
   144  	return s.sub.Receive(ctx, g)
   145  }
   146  
   147  // New creates new Cloud Pub/Sub Client
   148  func (c *pubSubClient) new(ctx context.Context, project string) (pubsubClientInterface, error) {
   149  	client, err := pubsub.NewClient(ctx, project)
   150  	if err != nil {
   151  		return nil, err
   152  	}
   153  	c.client = client
   154  	return c, nil
   155  }
   156  
   157  // Subscription creates a subscription from the Cloud Pub/Sub Client
   158  func (c *pubSubClient) subscription(id string) subscriptionInterface {
   159  	return &pubSubSubscription{
   160  		sub: c.client.Subscription(id),
   161  	}
   162  }
   163  
   164  // handlePulls pull for Pub/Sub subscriptions and handle them.
   165  func (s *PullServer) handlePulls(ctx context.Context, projectSubscriptions config.PubsubSubscriptions) (*errgroup.Group, context.Context, error) {
   166  	// Since config might change we need be able to cancel the current run
   167  	errGroup, derivedCtx := errgroup.WithContext(ctx)
   168  	for project, subscriptions := range projectSubscriptions {
   169  		client, err := s.Client.new(ctx, project)
   170  		if err != nil {
   171  			return errGroup, derivedCtx, err
   172  		}
   173  		for _, subName := range subscriptions {
   174  			sub := client.subscription(subName)
   175  			errGroup.Go(func() error {
   176  				logrus.Infof("Listening for subscription %s on project %s", sub.string(), project)
   177  				defer logrus.Warnf("Stopped Listening for subscription %s on project %s", sub.string(), project)
   178  				err := sub.receive(derivedCtx, func(ctx context.Context, msg messageInterface) {
   179  					if err = s.Subscriber.handleMessage(msg, sub.string()); err != nil {
   180  						s.Subscriber.Metrics.ACKMessageCounter.With(prometheus.Labels{subscriptionLabel: sub.string()}).Inc()
   181  					} else {
   182  						s.Subscriber.Metrics.NACKMessageCounter.With(prometheus.Labels{subscriptionLabel: sub.string()}).Inc()
   183  					}
   184  					msg.ack()
   185  				})
   186  				if err != nil {
   187  					logrus.WithError(err).Errorf("failed to listen for subscription %s on project %s", sub.string(), project)
   188  					return err
   189  				}
   190  				return nil
   191  			})
   192  		}
   193  	}
   194  	return errGroup, derivedCtx, nil
   195  }
   196  
   197  // Run will block listening to all subscriptions and return once the context is cancelled
   198  // or one of the subscription has a unrecoverable error.
   199  func (s *PullServer) Run(ctx context.Context) error {
   200  	configEvent := make(chan config.Delta, 2)
   201  	s.Subscriber.ConfigAgent.Subscribe(configEvent)
   202  
   203  	var err error
   204  	defer func() {
   205  		if err != nil {
   206  			logrus.WithError(ctx.Err()).Error("Pull server shutting down")
   207  		}
   208  		logrus.Warn("Pull server shutting down")
   209  	}()
   210  	currentConfig := s.Subscriber.ConfigAgent.Config().PubSubSubscriptions
   211  	errGroup, derivedCtx, err := s.handlePulls(ctx, currentConfig)
   212  	if err != nil {
   213  		return err
   214  	}
   215  
   216  	for {
   217  		select {
   218  		// Parent context. Shutdown
   219  		case <-ctx.Done():
   220  			return ctx.Err()
   221  		// Current thread context, it may be failing already
   222  		case <-derivedCtx.Done():
   223  			err = errGroup.Wait()
   224  			return err
   225  		// Checking for update config
   226  		case event := <-configEvent:
   227  			newConfig := event.After.PubSubSubscriptions
   228  			logrus.Info("Received new config")
   229  			if !reflect.DeepEqual(currentConfig, newConfig) {
   230  				logrus.Warn("New config found, reloading pull Server")
   231  				// Making sure the current thread finishes before starting a new one.
   232  				errGroup.Wait()
   233  				// Starting a new thread with new config
   234  				errGroup, derivedCtx, err = s.handlePulls(ctx, newConfig)
   235  				if err != nil {
   236  					return err
   237  				}
   238  				currentConfig = newConfig
   239  			}
   240  		}
   241  	}
   242  }