github.com/argoproj/argo-events@v1.9.1/eventsources/sources/bitbucket/start.go (about)

     1  /*
     2  
     3  Licensed under the Apache License, Version 2.0 (the "License");
     4  you may not use this file except in compliance with the License.
     5  You may obtain a copy of the License at
     6  
     7  	http://www.apache.org/licenses/LICENSE-2.0
     8  
     9  Unless required by applicable law or agreed to in writing, software
    10  distributed under the License is distributed on an "AS IS" BASIS,
    11  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  See the License for the specific language governing permissions and
    13  limitations under the License.
    14  */
    15  
    16  package bitbucket
    17  
    18  import (
    19  	"context"
    20  	"crypto/rand"
    21  	"encoding/json"
    22  	"errors"
    23  	"fmt"
    24  	"io"
    25  	"math/big"
    26  	"net/http"
    27  	"time"
    28  
    29  	bitbucketv2 "github.com/ktrysmt/go-bitbucket"
    30  	"github.com/mitchellh/mapstructure"
    31  	"go.uber.org/zap"
    32  
    33  	"github.com/argoproj/argo-events/common"
    34  	"github.com/argoproj/argo-events/common/logging"
    35  	eventsourcecommon "github.com/argoproj/argo-events/eventsources/common"
    36  	"github.com/argoproj/argo-events/eventsources/common/webhook"
    37  	"github.com/argoproj/argo-events/eventsources/sources"
    38  	"github.com/argoproj/argo-events/pkg/apis/events"
    39  	"github.com/argoproj/argo-events/pkg/apis/eventsource/v1alpha1"
    40  )
    41  
    42  // controller controls the webhook operations
    43  var (
    44  	controller = webhook.NewController()
    45  )
    46  
    47  // set up the activation and inactivation channels to control the state of routes.
    48  func init() {
    49  	go webhook.ProcessRouteStatus(controller)
    50  }
    51  
    52  // Implement Router
    53  // 1. GetRoute
    54  // 2. HandleRoute
    55  // 3. PostActivate
    56  // 4. PostInactivate
    57  
    58  // GetRoute returns the route
    59  func (router *Router) GetRoute() *webhook.Route {
    60  	return router.route
    61  }
    62  
    63  // HandleRoute handles incoming requests on the route
    64  func (router *Router) HandleRoute(writer http.ResponseWriter, request *http.Request) {
    65  	route := router.GetRoute()
    66  	logger := route.Logger.With(
    67  		logging.LabelEndpoint, route.Context.Endpoint,
    68  		logging.LabelPort, route.Context.Port,
    69  		logging.LabelHTTPMethod, route.Context.Method,
    70  	)
    71  
    72  	logger.Info("received a request, processing it...")
    73  
    74  	if !route.Active {
    75  		logger.Info("endpoint is not active, won't process the request")
    76  		common.SendErrorResponse(writer, "inactive endpoint")
    77  		return
    78  	}
    79  
    80  	request.Body = http.MaxBytesReader(writer, request.Body, route.Context.GetMaxPayloadSize())
    81  	body, err := io.ReadAll(request.Body)
    82  	if err != nil {
    83  		logger.Desugar().Error("failed to parse request body", zap.Error(err))
    84  		common.SendErrorResponse(writer, err.Error())
    85  		return
    86  	}
    87  
    88  	event := &events.BitbucketEventData{
    89  		Headers:  request.Header,
    90  		Body:     (*json.RawMessage)(&body),
    91  		Metadata: router.bitbucketEventSource.Metadata,
    92  	}
    93  
    94  	eventBody, err := json.Marshal(event)
    95  	if err != nil {
    96  		logger.Info("failed to marshal event")
    97  		common.SendErrorResponse(writer, "invalid event")
    98  		return
    99  	}
   100  
   101  	logger.Info("dispatching event on route's data channel")
   102  	route.DataCh <- eventBody
   103  
   104  	logger.Info("request successfully processed")
   105  	common.SendSuccessResponse(writer, "success")
   106  }
   107  
   108  // PostActivate performs operations once the route is activated and ready to consume requests
   109  func (router *Router) PostActivate() error {
   110  	return nil
   111  }
   112  
   113  // PostInactivate performs operations after the route is inactivated
   114  func (router *Router) PostInactivate() error {
   115  	bitbucketEventSource := router.bitbucketEventSource
   116  	logger := router.GetRoute().Logger
   117  
   118  	if bitbucketEventSource.DeleteHookOnFinish && len(router.hookIDs) > 0 {
   119  		logger.Info("deleting webhooks from bitbucket...")
   120  
   121  		for _, repo := range bitbucketEventSource.GetBitbucketRepositories() {
   122  			hookID, ok := router.hookIDs[repo.GetRepositoryID()]
   123  			if !ok {
   124  				return fmt.Errorf("can not find hook ID for repo key: %s", repo.GetRepositoryID())
   125  			}
   126  
   127  			if err := router.deleteWebhook(repo, hookID); err != nil {
   128  				logger.Errorw("failed to delete webhook",
   129  					zap.String("owner", repo.Owner), zap.String("repository-slug", repo.RepositorySlug), zap.Error(err))
   130  				return fmt.Errorf("failed to delete hook for repo %s/%s, %w", repo.Owner, repo.RepositorySlug, err)
   131  			}
   132  
   133  			logger.Info("successfully deleted hook for repo",
   134  				zap.String("owner", repo.Owner), zap.String("repository-slug", repo.RepositorySlug))
   135  		}
   136  	}
   137  
   138  	return nil
   139  }
   140  
   141  // StartListening starts an event source
   142  func (el *EventListener) StartListening(ctx context.Context, dispatch func([]byte, ...eventsourcecommon.Option) error) error {
   143  	defer sources.Recover(el.GetEventName())
   144  
   145  	bitbucketEventSource := &el.BitbucketEventSource
   146  	logger := logging.FromContext(ctx).With(
   147  		logging.LabelEventSourceType, el.GetEventSourceType(),
   148  		logging.LabelEventName, el.GetEventName(),
   149  	)
   150  
   151  	logger.Info("started processing the Bitbucket event source...")
   152  	route := webhook.NewRoute(bitbucketEventSource.Webhook, logger, el.GetEventSourceName(), el.GetEventName(), el.Metrics)
   153  	router := &Router{
   154  		route:                route,
   155  		bitbucketEventSource: bitbucketEventSource,
   156  		hookIDs:              make(map[string]string),
   157  	}
   158  
   159  	if !bitbucketEventSource.ShouldCreateWebhooks() {
   160  		logger.Info("access token or webhook configuration were not provided, skipping webhooks creation")
   161  		return webhook.ManageRoute(ctx, router, controller, dispatch)
   162  	}
   163  
   164  	logger.Info("choosing bitbucket auth strategy...")
   165  	authStrategy, err := router.chooseAuthStrategy()
   166  	if err != nil {
   167  		return fmt.Errorf("failed to get bitbucket auth strategy, %w", err)
   168  	}
   169  
   170  	router.client = authStrategy.BitbucketClient()
   171  
   172  	applyWebhooks := func() {
   173  		for _, repo := range bitbucketEventSource.GetBitbucketRepositories() {
   174  			if err = router.applyBitbucketWebhook(repo); err != nil {
   175  				logger.Errorw("failed to apply Bitbucket webhook",
   176  					zap.String("owner", repo.Owner), zap.String("repository-slug", repo.RepositorySlug), zap.Error(err))
   177  				continue
   178  			}
   179  
   180  			time.Sleep(500 * time.Millisecond)
   181  		}
   182  	}
   183  
   184  	// When running multiple replicas of the eventsource, they will all try to create the webhook.
   185  	// Randomly sleep some time to mitigate the issue.
   186  	randomNum, _ := rand.Int(rand.Reader, big.NewInt(int64(5000)))
   187  	time.Sleep(time.Duration(randomNum.Int64()) * time.Millisecond)
   188  	applyWebhooks()
   189  
   190  	return webhook.ManageRoute(ctx, router, controller, dispatch)
   191  }
   192  
   193  // chooseAuthStrategy returns an AuthStrategy based on the given credentials
   194  func (router *Router) chooseAuthStrategy() (AuthStrategy, error) {
   195  	es := router.bitbucketEventSource
   196  	switch {
   197  	case es.HasBitbucketBasicAuth():
   198  		return NewBasicAuthStrategy(es.Auth.Basic.Username, es.Auth.Basic.Password)
   199  	case es.HasBitbucketOAuthToken():
   200  		return NewOAuthTokenAuthStrategy(es.Auth.OAuthToken)
   201  	default:
   202  		return nil, fmt.Errorf("none of the supported auth options were provided")
   203  	}
   204  }
   205  
   206  // applyBitbucketWebhook creates or updates the configured webhook in Bitbucket
   207  func (router *Router) applyBitbucketWebhook(repo v1alpha1.BitbucketRepository) error {
   208  	bitbucketEventSource := router.bitbucketEventSource
   209  	route := router.route
   210  	logger := router.GetRoute().Logger.With(
   211  		logging.LabelEndpoint, route.Context.Endpoint,
   212  		logging.LabelPort, route.Context.Port,
   213  		logging.LabelHTTPMethod, route.Context.Method,
   214  		"owner", repo.Owner,
   215  		"repository-slug", repo.RepositorySlug,
   216  	)
   217  
   218  	formattedWebhookURL := common.FormattedURL(bitbucketEventSource.Webhook.URL, bitbucketEventSource.Webhook.Endpoint)
   219  
   220  	logger.Info("listing existing webhooks...")
   221  	hooks, err := router.listWebhooks(repo)
   222  	if err != nil {
   223  		logger.Errorw("failed to list webhooks", zap.Error(err))
   224  		return fmt.Errorf("failed to list webhooks, %w", err)
   225  	}
   226  
   227  	logger.Info("checking if webhook already exists...")
   228  	existingHookSubscription, isFound := router.findWebhook(hooks, formattedWebhookURL)
   229  	if isFound {
   230  		logger.Info("webhook already exists, removing old webhook...")
   231  		if err := router.deleteWebhook(repo, existingHookSubscription.Uuid); err != nil {
   232  			logger.Errorw("failed to delete old webhook",
   233  				zap.String("owner", repo.Owner), zap.String("repository-slug", repo.RepositorySlug), zap.Error(err))
   234  			return fmt.Errorf("failed to delete old webhook for repo %s/%s, %w", repo.Owner, repo.RepositorySlug, err)
   235  		}
   236  	}
   237  
   238  	logger.Info("creating a new webhook...")
   239  	newWebhook, err := router.createWebhook(repo, formattedWebhookURL)
   240  	if err != nil {
   241  		logger.Errorw("failed to create new webhook", zap.Error(err))
   242  		return fmt.Errorf("failed to create new webhook, %w", err)
   243  	}
   244  
   245  	router.hookIDs[repo.GetRepositoryID()] = newWebhook.Uuid
   246  
   247  	logger.Info("successfully created a new webhook")
   248  	return nil
   249  }
   250  
   251  // createWebhook creates a new webhook
   252  func (router *Router) createWebhook(repo v1alpha1.BitbucketRepository, formattedWebhookURL string) (*bitbucketv2.Webhook, error) {
   253  	opt := &bitbucketv2.WebhooksOptions{
   254  		Owner:       repo.Owner,
   255  		RepoSlug:    repo.RepositorySlug,
   256  		Url:         formattedWebhookURL,
   257  		Description: "webhook managed by Argo-Events",
   258  		Active:      true,
   259  		Events:      router.bitbucketEventSource.Events,
   260  	}
   261  
   262  	return router.client.Repositories.Webhooks.Create(opt)
   263  }
   264  
   265  // deleteWebhook deletes an existing webhook
   266  func (router *Router) deleteWebhook(repo v1alpha1.BitbucketRepository, hookID string) error {
   267  	_, err := router.client.Repositories.Webhooks.Delete(&bitbucketv2.WebhooksOptions{
   268  		Owner:    repo.Owner,
   269  		RepoSlug: repo.RepositorySlug,
   270  		Uuid:     hookID,
   271  	})
   272  	if err != nil {
   273  		// Skip not found errors in case the webhook was already deleted
   274  		var bitbucketErr *bitbucketv2.UnexpectedResponseStatusError
   275  		if errors.As(err, &bitbucketErr) && bitbucketErr.Status == "404 Not Found" {
   276  			return nil
   277  		}
   278  	}
   279  
   280  	return err
   281  }
   282  
   283  // listWebhooks gets a list of all existing webhooks in target repository
   284  func (router *Router) listWebhooks(repo v1alpha1.BitbucketRepository) ([]WebhookSubscription, error) {
   285  	hooksResponse, err := router.client.Repositories.Webhooks.Gets(&bitbucketv2.WebhooksOptions{
   286  		Owner:    repo.Owner,
   287  		RepoSlug: repo.RepositorySlug,
   288  	})
   289  	if err != nil {
   290  		return nil, err
   291  	}
   292  
   293  	return router.extractHooksFromListResponse(hooksResponse)
   294  }
   295  
   296  // extractHooksFromListResponse helper that extracts the list of webhooks from the response of listWebhooks
   297  func (router *Router) extractHooksFromListResponse(listHooksResponse interface{}) ([]WebhookSubscription, error) {
   298  	logger := router.GetRoute().Logger
   299  	res, ok := listHooksResponse.(map[string]interface{})
   300  	if !ok {
   301  		logger.Errorw("failed to parse the list webhooks response", zap.Any("response", listHooksResponse))
   302  		return nil, fmt.Errorf("failed to parse the list webhooks response")
   303  	}
   304  
   305  	var hooks []WebhookSubscription
   306  	err := mapstructure.Decode(res["values"], &hooks)
   307  	if err != nil || hooks == nil {
   308  		logger.Errorw("failed to parse the list webhooks response", zap.Any("response", listHooksResponse))
   309  		return nil, fmt.Errorf("failed to parse the list webhooks response")
   310  	}
   311  
   312  	return hooks, nil
   313  }
   314  
   315  // findWebhook searches for a webhook in a list by its URL and returns the webhook if its found
   316  func (router *Router) findWebhook(hooks []WebhookSubscription, targetWebhookURL string) (*WebhookSubscription, bool) {
   317  	var existingHookSubscription *WebhookSubscription
   318  	isFound := false
   319  	for _, hook := range hooks {
   320  		if hook.Url == targetWebhookURL {
   321  			isFound = true
   322  			existingHookSubscription = &hook
   323  			break
   324  		}
   325  	}
   326  
   327  	return existingHookSubscription, isFound
   328  }