github.com/argoproj/argo-events@v1.9.1/eventsources/sources/gcppubsub/start.go (about) 1 /* 2 Copyright 2018 BlackRock, Inc. 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 gcppubsub 18 19 import ( 20 "context" 21 "encoding/json" 22 "fmt" 23 "os" 24 "time" 25 26 "cloud.google.com/go/compute/metadata" 27 "cloud.google.com/go/pubsub" 28 29 "go.uber.org/zap" 30 "google.golang.org/api/option" 31 "google.golang.org/grpc/codes" 32 "google.golang.org/grpc/status" 33 34 "github.com/argoproj/argo-events/common" 35 "github.com/argoproj/argo-events/common/logging" 36 eventsourcecommon "github.com/argoproj/argo-events/eventsources/common" 37 "github.com/argoproj/argo-events/eventsources/sources" 38 metrics "github.com/argoproj/argo-events/metrics" 39 apicommon "github.com/argoproj/argo-events/pkg/apis/common" 40 "github.com/argoproj/argo-events/pkg/apis/events" 41 "github.com/argoproj/argo-events/pkg/apis/eventsource/v1alpha1" 42 ) 43 44 // EventListener implements Eventing for gcp pub-sub event source 45 type EventListener struct { 46 EventSourceName string 47 EventName string 48 PubSubEventSource v1alpha1.PubSubEventSource 49 Metrics *metrics.Metrics 50 } 51 52 // GetEventSourceName returns name of event source 53 func (el *EventListener) GetEventSourceName() string { 54 return el.EventSourceName 55 } 56 57 // GetEventName returns name of event 58 func (el *EventListener) GetEventName() string { 59 return el.EventName 60 } 61 62 // GetEventSourceType return type of event server 63 func (el *EventListener) GetEventSourceType() apicommon.EventSourceType { 64 return apicommon.PubSubEvent 65 } 66 67 // StartListening listens to GCP PubSub events 68 func (el *EventListener) StartListening(ctx context.Context, dispatch func([]byte, ...eventsourcecommon.Option) error) error { 69 // In order to listen events from GCP PubSub, 70 // 1. Parse the event source that contains configuration to connect to GCP PubSub 71 // 2. Create a new PubSub client 72 // 3. Create the topic if one doesn't exist already 73 // 4. Create a subscription if one doesn't exist already. 74 // 5. Start listening to messages on the queue 75 // 6. Once the event source is stopped perform cleaning up - 1. Delete the subscription if configured so 2. Close the PubSub client 76 77 logger := logging.FromContext(ctx). 78 With(logging.LabelEventSourceType, el.GetEventSourceType(), logging.LabelEventName, el.GetEventName()) 79 logger.Info("started processing the GCP Pub Sub event source...") 80 defer sources.Recover(el.GetEventName()) 81 82 err := el.fillDefault(logger) 83 if err != nil { 84 return fmt.Errorf("failed to fill default values for %s, %w", el.GetEventName(), err) 85 } 86 87 pubsubEventSource := &el.PubSubEventSource 88 log := logger.With( 89 "topic", pubsubEventSource.Topic, 90 "topicProjectID", pubsubEventSource.TopicProjectID, 91 "projectID", pubsubEventSource.ProjectID, 92 "subscriptionID", pubsubEventSource.SubscriptionID, 93 ) 94 95 if pubsubEventSource.JSONBody { 96 log.Info("assuming all events have a json body...") 97 } 98 99 log.Info("setting up a client to connect to PubSub...") 100 client, subscription, err := el.prepareSubscription(ctx, log) 101 if err != nil { 102 return fmt.Errorf("failed to prepare client or subscription for %s, %w", el.GetEventName(), err) 103 } 104 105 log.Info("listening for messages from PubSub...") 106 err = subscription.Receive(ctx, func(msgCtx context.Context, m *pubsub.Message) { 107 defer func(start time.Time) { 108 el.Metrics.EventProcessingDuration(el.GetEventSourceName(), el.GetEventName(), float64(time.Since(start)/time.Millisecond)) 109 }(time.Now()) 110 111 log.Info("received GCP PubSub Message from topic") 112 eventData := &events.PubSubEventData{ 113 ID: m.ID, 114 Body: m.Data, 115 Attributes: m.Attributes, 116 PublishTime: m.PublishTime.String(), 117 Metadata: pubsubEventSource.Metadata, 118 } 119 if pubsubEventSource.JSONBody { 120 eventData.Body = (*json.RawMessage)(&m.Data) 121 } 122 eventBytes, err := json.Marshal(eventData) 123 if err != nil { 124 log.Errorw("failed to marshal the event data", zap.Error(err)) 125 el.Metrics.EventProcessingFailed(el.GetEventSourceName(), el.GetEventName()) 126 m.Nack() 127 return 128 } 129 130 log.Info("dispatching event...") 131 if err = dispatch(eventBytes); err != nil { 132 log.Errorw("failed to dispatch GCP PubSub event", zap.Error(err)) 133 el.Metrics.EventProcessingFailed(el.GetEventSourceName(), el.GetEventName()) 134 m.Nack() 135 return 136 } 137 m.Ack() 138 }) 139 if err != nil { 140 return fmt.Errorf("failed to receive the messages for subscription %s for %s, %w", subscription, el.GetEventName(), err) 141 } 142 143 <-ctx.Done() 144 145 log.Info("event source has been stopped") 146 147 if pubsubEventSource.DeleteSubscriptionOnFinish { 148 log.Info("deleting PubSub subscription...") 149 if err = subscription.Delete(context.Background()); err != nil { 150 log.Errorw("failed to delete the PubSub subscription", zap.Error(err)) 151 } 152 } 153 154 log.Info("closing PubSub client...") 155 if err = client.Close(); err != nil { 156 log.Errorw("failed to close the PubSub client", zap.Error(err)) 157 } 158 159 return nil 160 } 161 162 func (el *EventListener) fillDefault(logger *zap.SugaredLogger) error { 163 // Default value for each field 164 // - ProjectID: determine from GCP metadata server (only valid in GCP) 165 // - TopicProjectID: same as ProjectID (filled only if topic is specified) 166 // - SubscriptionID: name + hash suffix 167 // - Topic: nothing (fine if subsc. exists, otherwise fail) 168 169 if el.PubSubEventSource.ProjectID == "" { 170 logger.Debug("determine project ID from GCP metadata server") 171 proj, err := metadata.ProjectID() 172 if err != nil { 173 return fmt.Errorf("project ID is not given and couldn't determine from GCP metadata server, %w", err) 174 } 175 el.PubSubEventSource.ProjectID = proj 176 } 177 178 if el.PubSubEventSource.TopicProjectID == "" && el.PubSubEventSource.Topic != "" { 179 el.PubSubEventSource.TopicProjectID = el.PubSubEventSource.ProjectID 180 } 181 182 if el.PubSubEventSource.SubscriptionID == "" { 183 logger.Debug("auto generate subscription ID") 184 hashcode, err := el.hash() 185 if err != nil { 186 return fmt.Errorf("failed get hashcode, %w", err) 187 } 188 el.PubSubEventSource.SubscriptionID = fmt.Sprintf("%s-%s", el.GetEventName(), hashcode) 189 } 190 191 return nil 192 } 193 194 func (el *EventListener) hash() (string, error) { 195 body, err := json.Marshal(&el.PubSubEventSource) 196 if err != nil { 197 return "", err 198 } 199 return common.Hasher(el.GetEventName() + string(body)), nil 200 } 201 202 func (el *EventListener) prepareSubscription(ctx context.Context, logger *zap.SugaredLogger) (*pubsub.Client, *pubsub.Subscription, error) { 203 pubsubEventSource := &el.PubSubEventSource 204 205 opts := make([]option.ClientOption, 0, 1) 206 if secret := el.PubSubEventSource.CredentialSecret; secret != nil { 207 logger.Debug("using credentials from secret") 208 jsonCred, err := common.GetSecretFromVolume(secret) 209 if err != nil { 210 return nil, nil, fmt.Errorf("could not find credentials, %w", err) 211 } 212 opts = append(opts, option.WithCredentialsJSON([]byte(jsonCred))) 213 } else { 214 logger.Debug("using default credentials") 215 } 216 client, err := pubsub.NewClient(ctx, pubsubEventSource.ProjectID, opts...) 217 if err != nil { 218 return nil, nil, fmt.Errorf("failed to set up client for %s, %w", el.GetEventName(), err) 219 } 220 logger.Debug("set up pubsub client") 221 222 subscription := client.Subscription(pubsubEventSource.SubscriptionID) 223 224 // Overall logics are as follows: 225 // 226 // subsc. exists | topic given | topic exists | action | required permissions 227 // :------------ | :---------- | :----------- | :-------------------- | :----------------------------------------------------------------------------- 228 // no | no | - | invalid | - 229 // yes | no | - | do nothing | nothing extra 230 // yes | yes | - | verify topic | pubsub.subscriptions.get (subsc.) 231 // no | yes | yes | create subsc. | pubsub.subscriptions.create (proj.) + pubsub.topics.attachSubscription (topic) 232 // no | yes | no | create topic & subsc. | above + pubsub.topics.create (proj. for topic) 233 234 subscExists := false 235 if addr := os.Getenv("PUBSUB_EMULATOR_HOST"); addr != "" { 236 logger.Debug("using pubsub emulator - skipping permissions check") 237 subscExists, err = subscription.Exists(ctx) 238 if err != nil { 239 client.Close() 240 return nil, nil, fmt.Errorf("failed to check if subscription %s exists", subscription) 241 } 242 } else { 243 // trick: you don't need to have get permission to check only whether it exists 244 perms, err := subscription.IAM().TestPermissions(ctx, []string{"pubsub.subscriptions.consume"}) 245 subscExists = len(perms) == 1 246 if !subscExists { 247 switch status.Code(err) { 248 case codes.OK: 249 client.Close() 250 return nil, nil, fmt.Errorf("you lack permission to pull from %s", subscription) 251 case codes.NotFound: 252 // OK, maybe the subscription doesn't exist yet, so create it later 253 // (it possibly means project itself doesn't exist, but it's ok because we'll see an error later in such case) 254 default: 255 client.Close() 256 return nil, nil, fmt.Errorf("failed to test permission for subscription %s, %w", subscription, err) 257 } 258 } 259 logger.Debug("checked if subscription exists and you have right permission") 260 } 261 262 // subsc. exists | topic given | topic exists | action | required permissions 263 // :------------ | :---------- | :----------- | :-------------------- | :----------------------------------------------------------------------------- 264 // no | no | - | invalid | - 265 // yes | no | - | do nothing | nothing extra 266 if pubsubEventSource.Topic == "" { 267 if !subscExists { 268 client.Close() 269 return nil, nil, fmt.Errorf("you need to specify topicID to create missing subscription %s", subscription) 270 } 271 logger.Debug("subscription exists and no topic given, fine") 272 return client, subscription, nil 273 } 274 275 // subsc. exists | topic given | topic exists | action | required permissions 276 // :------------ | :---------- | :----------- | :-------------------- | :----------------------------------------------------------------------------- 277 // yes | yes | - | verify topic | pubsub.subscriptions.get (subsc.) 278 topic := client.TopicInProject(pubsubEventSource.Topic, pubsubEventSource.TopicProjectID) 279 280 if subscExists { 281 subscConfig, err := subscription.Config(ctx) 282 if err != nil { 283 client.Close() 284 return nil, nil, fmt.Errorf("failed to get subscription's config for verifying topic, %w", err) 285 } 286 switch actualTopic := subscConfig.Topic.String(); actualTopic { 287 case "_deleted-topic_": 288 client.Close() 289 return nil, nil, fmt.Errorf("the topic for the subscription has been deleted") 290 case topic.String(): 291 logger.Debug("subscription exists and its topic matches given one, fine") 292 return client, subscription, nil 293 default: 294 client.Close() 295 return nil, nil, fmt.Errorf("this subscription belongs to wrong topic %s", actualTopic) 296 } 297 } 298 299 // subsc. exists | topic given | topic exists | action | required permissions 300 // :------------ | :---------- | :----------- | :-------------------- | :----------------------------------------------------------------------------- 301 // no | yes | ??? | create subsc. | pubsub.subscriptions.create (proj.) + pubsub.topics.attachSubscription (topic) 302 // ↑ We don't know yet, but just try to create subsc. 303 logger.Debug("subscription doesn't seem to exist") 304 _, err = client.CreateSubscription(ctx, subscription.ID(), pubsub.SubscriptionConfig{Topic: topic}) 305 switch status.Code(err) { 306 case codes.OK: 307 logger.Debug("subscription created") 308 return client, subscription, nil 309 case codes.NotFound: 310 // OK, maybe the topic doesn't exist yet, so create it later 311 // (it possibly means project itself doesn't exist, but it's ok because we'll see an error later in such case) 312 default: 313 client.Close() 314 return nil, nil, fmt.Errorf("failed to create %s for %s, %w", subscription, topic, err) 315 } 316 317 // subsc. exists | topic given | topic exists | action | required permissions 318 // :------------ | :---------- | :----------- | :-------------------- | :----------------------------------------------------------------------------- 319 // no | yes | no | create topic & subsc. | above + pubsub.topics.create (proj. for topic) 320 logger.Debug("topic doesn't seem to exist neither") 321 // NB: you need another client for topic because it might be in different project 322 topicClient, err := pubsub.NewClient(ctx, pubsubEventSource.TopicProjectID, opts...) 323 if err != nil { 324 client.Close() 325 return nil, nil, fmt.Errorf("failed to create client to create %s, %w", topic, err) 326 } 327 defer topicClient.Close() 328 329 _, err = topicClient.CreateTopic(ctx, topic.ID()) 330 if err != nil { 331 client.Close() 332 return nil, nil, fmt.Errorf("failed to create %s, %w", topic, err) 333 } 334 logger.Debug("topic created") 335 _, err = client.CreateSubscription(ctx, subscription.ID(), pubsub.SubscriptionConfig{Topic: topic}) 336 if err != nil { 337 client.Close() 338 return nil, nil, fmt.Errorf("failed to create %s for %s, %w", subscription, topic, err) 339 } 340 logger.Debug("subscription created") 341 return client, subscription, nil 342 }