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  }