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 }