github.com/argoproj/argo-events@v1.9.1/eventsources/sources/bitbucketserver/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 bitbucketserver
    17  
    18  import (
    19  	"context"
    20  	"crypto/hmac"
    21  	"crypto/rand"
    22  	"crypto/sha256"
    23  	"encoding/hex"
    24  	"encoding/json"
    25  	"fmt"
    26  	"io"
    27  	"math/big"
    28  	"net/http"
    29  	"time"
    30  
    31  	bitbucketv1 "github.com/gfleury/go-bitbucket-v1"
    32  	"github.com/mitchellh/mapstructure"
    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/common/webhook"
    38  	"github.com/argoproj/argo-events/eventsources/sources"
    39  	"github.com/argoproj/argo-events/pkg/apis/events"
    40  	"github.com/argoproj/argo-events/pkg/apis/eventsource/v1alpha1"
    41  	"go.uber.org/zap"
    42  )
    43  
    44  // controller controls the webhook operations
    45  var (
    46  	controller = webhook.NewController()
    47  )
    48  
    49  // set up the activation and inactivation channels to control the state of routes.
    50  func init() {
    51  	go webhook.ProcessRouteStatus(controller)
    52  }
    53  
    54  // Implement Router
    55  // 1. GetRoute
    56  // 2. HandleRoute
    57  // 3. PostActivate
    58  // 4. PostDeactivate
    59  
    60  // GetRoute returns the route
    61  func (router *Router) GetRoute() *webhook.Route {
    62  	return router.route
    63  }
    64  
    65  // HandleRoute handles incoming requests on the route
    66  func (router *Router) HandleRoute(writer http.ResponseWriter, request *http.Request) {
    67  	route := router.GetRoute()
    68  
    69  	logger := route.Logger.With(
    70  		logging.LabelEndpoint, route.Context.Endpoint,
    71  		logging.LabelPort, route.Context.Port,
    72  		logging.LabelHTTPMethod, route.Context.Method,
    73  	)
    74  
    75  	logger.Info("received a request, processing it...")
    76  
    77  	if !route.Active {
    78  		logger.Info("endpoint is not active, won't process the request")
    79  		common.SendErrorResponse(writer, "inactive endpoint")
    80  		return
    81  	}
    82  
    83  	defer func(start time.Time) {
    84  		route.Metrics.EventProcessingDuration(route.EventSourceName, route.EventName, float64(time.Since(start)/time.Millisecond))
    85  	}(time.Now())
    86  
    87  	request.Body = http.MaxBytesReader(writer, request.Body, route.Context.GetMaxPayloadSize())
    88  	body, err := router.parseAndValidateBitbucketServerRequest(request)
    89  	if err != nil {
    90  		logger.Errorw("failed to parse/validate request", zap.Error(err))
    91  		common.SendErrorResponse(writer, err.Error())
    92  		route.Metrics.EventProcessingFailed(route.EventSourceName, route.EventName)
    93  		return
    94  	}
    95  
    96  	event := &events.BitbucketServerEventData{
    97  		Headers:  request.Header,
    98  		Body:     (*json.RawMessage)(&body),
    99  		Metadata: router.bitbucketserverEventSource.Metadata,
   100  	}
   101  
   102  	eventBody, err := json.Marshal(event)
   103  	if err != nil {
   104  		logger.Info("failed to marshal event")
   105  		common.SendErrorResponse(writer, "invalid event")
   106  		route.Metrics.EventProcessingFailed(route.EventSourceName, route.EventName)
   107  		return
   108  	}
   109  
   110  	logger.Info("dispatching event on route's data channel")
   111  	route.DataCh <- eventBody
   112  
   113  	logger.Info("request successfully processed")
   114  	common.SendSuccessResponse(writer, "success")
   115  }
   116  
   117  // PostActivate performs operations once the route is activated and ready to consume requests
   118  func (router *Router) PostActivate() error {
   119  	return nil
   120  }
   121  
   122  // PostInactivate performs operations after the route is inactivated
   123  func (router *Router) PostInactivate() error {
   124  	bitbucketserverEventSource := router.bitbucketserverEventSource
   125  	route := router.route
   126  	logger := route.Logger
   127  
   128  	if bitbucketserverEventSource.DeleteHookOnFinish && len(router.hookIDs) > 0 {
   129  		logger.Info("deleting webhooks from bitbucket")
   130  
   131  		bitbucketToken, err := common.GetSecretFromVolume(bitbucketserverEventSource.AccessToken)
   132  		if err != nil {
   133  			return fmt.Errorf("failed to get bitbucketserver token. err: %w", err)
   134  		}
   135  
   136  		bitbucketConfig := bitbucketv1.NewConfiguration(bitbucketserverEventSource.BitbucketServerBaseURL)
   137  		bitbucketConfig.AddDefaultHeader("x-atlassian-token", "no-check")
   138  		bitbucketConfig.AddDefaultHeader("x-requested-with", "XMLHttpRequest")
   139  
   140  		if bitbucketserverEventSource.TLS != nil {
   141  			tlsConfig, err := common.GetTLSConfig(router.bitbucketserverEventSource.TLS)
   142  			if err != nil {
   143  				return fmt.Errorf("failed to get the tls configuration, %w", err)
   144  			}
   145  
   146  			bitbucketConfig.HTTPClient = &http.Client{
   147  				Transport: &http.Transport{
   148  					TLSClientConfig: tlsConfig,
   149  				},
   150  			}
   151  		}
   152  
   153  		for _, repo := range bitbucketserverEventSource.GetBitbucketServerRepositories() {
   154  			id, ok := router.hookIDs[repo.ProjectKey+","+repo.RepositorySlug]
   155  			if !ok {
   156  				return fmt.Errorf("can not find hook ID for project-key: %s, repository-slug: %s", repo.ProjectKey, repo.RepositorySlug)
   157  			}
   158  
   159  			ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
   160  			defer cancel()
   161  
   162  			ctx = context.WithValue(ctx, bitbucketv1.ContextAccessToken, bitbucketToken)
   163  			bitbucketClient := bitbucketv1.NewAPIClient(ctx, bitbucketConfig)
   164  
   165  			_, err = bitbucketClient.DefaultApi.DeleteWebhook(repo.ProjectKey, repo.RepositorySlug, int32(id))
   166  			if err != nil {
   167  				return fmt.Errorf("failed to delete bitbucketserver webhook. err: %w", err)
   168  			}
   169  
   170  			logger.Infow("bitbucket server webhook deleted",
   171  				zap.String("project-key", repo.ProjectKey), zap.String("repository-slug", repo.RepositorySlug))
   172  		}
   173  	} else {
   174  		logger.Info("no need to delete webhooks, skipping.")
   175  	}
   176  
   177  	return nil
   178  }
   179  
   180  // StartListening starts an event source
   181  func (el *EventListener) StartListening(ctx context.Context, dispatch func([]byte, ...eventsourcecommon.Option) error) error {
   182  	defer sources.Recover(el.GetEventName())
   183  
   184  	bitbucketserverEventSource := &el.BitbucketServerEventSource
   185  
   186  	logger := logging.FromContext(ctx).With(
   187  		logging.LabelEventSourceType, el.GetEventSourceType(),
   188  		logging.LabelEventName, el.GetEventName(),
   189  		"base-url", bitbucketserverEventSource.BitbucketServerBaseURL,
   190  	)
   191  
   192  	logger.Info("started processing the Bitbucket Server event source...")
   193  
   194  	route := webhook.NewRoute(bitbucketserverEventSource.Webhook, logger, el.GetEventSourceName(), el.GetEventName(), el.Metrics)
   195  	router := &Router{
   196  		route:                      route,
   197  		bitbucketserverEventSource: bitbucketserverEventSource,
   198  		hookIDs:                    make(map[string]int),
   199  	}
   200  
   201  	if !bitbucketserverEventSource.ShouldCreateWebhooks() {
   202  		logger.Info("access token or webhook configuration were not provided, skipping webhooks creation")
   203  		return webhook.ManageRoute(ctx, router, controller, dispatch)
   204  	}
   205  
   206  	logger.Info("retrieving the access token credentials...")
   207  	bitbucketToken, err := common.GetSecretFromVolume(bitbucketserverEventSource.AccessToken)
   208  	if err != nil {
   209  		return fmt.Errorf("failed to get bitbucketserver token. err: %w", err)
   210  	}
   211  
   212  	if bitbucketserverEventSource.WebhookSecret != nil {
   213  		logger.Info("retrieving the webhook secret...")
   214  		webhookSecret, err := common.GetSecretFromVolume(bitbucketserverEventSource.WebhookSecret)
   215  		if err != nil {
   216  			return fmt.Errorf("failed to get bitbucketserver webhook secret. err: %w", err)
   217  		}
   218  
   219  		router.hookSecret = webhookSecret
   220  	}
   221  
   222  	logger.Info("setting up the client to connect to Bitbucket Server...")
   223  	bitbucketConfig := bitbucketv1.NewConfiguration(bitbucketserverEventSource.BitbucketServerBaseURL)
   224  	bitbucketConfig.AddDefaultHeader("x-atlassian-token", "no-check")
   225  	bitbucketConfig.AddDefaultHeader("x-requested-with", "XMLHttpRequest")
   226  
   227  	if bitbucketserverEventSource.TLS != nil {
   228  		tlsConfig, err := common.GetTLSConfig(router.bitbucketserverEventSource.TLS)
   229  		if err != nil {
   230  			return fmt.Errorf("failed to get the tls configuration, %w", err)
   231  		}
   232  
   233  		bitbucketConfig.HTTPClient = &http.Client{
   234  			Transport: &http.Transport{
   235  				TLSClientConfig: tlsConfig,
   236  			},
   237  		}
   238  	}
   239  
   240  	ctx, cancel := context.WithCancel(ctx)
   241  	defer cancel()
   242  
   243  	ctx = context.WithValue(ctx, bitbucketv1.ContextAccessToken, bitbucketToken)
   244  
   245  	applyWebhooks := func() {
   246  		for _, repo := range bitbucketserverEventSource.GetBitbucketServerRepositories() {
   247  			if err = router.applyBitbucketServerWebhook(ctx, bitbucketConfig, repo); err != nil {
   248  				logger.Errorw("failed to apply Bitbucket webhook",
   249  					zap.String("project-key", repo.ProjectKey), zap.String("repository-slug", repo.RepositorySlug), zap.Error(err))
   250  				continue
   251  			}
   252  
   253  			time.Sleep(500 * time.Millisecond)
   254  		}
   255  	}
   256  
   257  	// When running multiple replicas of the eventsource, they will all try to create the webhook.
   258  	// Randomly sleep some time to mitigate the issue.
   259  	randomNum, _ := rand.Int(rand.Reader, big.NewInt(int64(2000)))
   260  	time.Sleep(time.Duration(randomNum.Int64()) * time.Millisecond)
   261  	applyWebhooks()
   262  
   263  	go func() {
   264  		// Another kind of race conditions might happen when pods do rolling upgrade - new pod starts
   265  		// and old pod terminates, if DeleteHookOnFinish is true, the hook will be deleted from Bitbucket.
   266  		// This is a workaround to mitigate the race conditions.
   267  		logger.Info("starting bitbucket hooks manager daemon")
   268  		ticker := time.NewTicker(60 * time.Second)
   269  		defer ticker.Stop()
   270  		for {
   271  			select {
   272  			case <-ctx.Done():
   273  				logger.Info("exiting bitbucket hooks manager daemon")
   274  				return
   275  			case <-ticker.C:
   276  				applyWebhooks()
   277  			}
   278  		}
   279  	}()
   280  
   281  	return webhook.ManageRoute(ctx, router, controller, dispatch)
   282  }
   283  
   284  // applyBitbucketServerWebhook creates or updates the configured webhook in Bitbucket
   285  func (router *Router) applyBitbucketServerWebhook(ctx context.Context, bitbucketConfig *bitbucketv1.Configuration, repo v1alpha1.BitbucketServerRepository) error {
   286  	bitbucketserverEventSource := router.bitbucketserverEventSource
   287  	route := router.route
   288  
   289  	logger := route.Logger.With(
   290  		logging.LabelEndpoint, route.Context.Endpoint,
   291  		logging.LabelPort, route.Context.Port,
   292  		logging.LabelHTTPMethod, route.Context.Method,
   293  		"project-key", repo.ProjectKey,
   294  		"repository-slug", repo.RepositorySlug,
   295  		"base-url", bitbucketserverEventSource.BitbucketServerBaseURL,
   296  	)
   297  
   298  	bitbucketClient := bitbucketv1.NewAPIClient(ctx, bitbucketConfig)
   299  	formattedURL := common.FormattedURL(bitbucketserverEventSource.Webhook.URL, bitbucketserverEventSource.Webhook.Endpoint)
   300  
   301  	hooks, err := router.listWebhooks(bitbucketClient, repo)
   302  	if err != nil {
   303  		return fmt.Errorf("failed to list existing hooks to check for duplicates for repository %s/%s, %w", repo.ProjectKey, repo.RepositorySlug, err)
   304  	}
   305  
   306  	var existingHook bitbucketv1.Webhook
   307  	isAlreadyExists := false
   308  
   309  	for _, hook := range hooks {
   310  		if hook.Url == formattedURL {
   311  			isAlreadyExists = true
   312  			existingHook = hook
   313  			router.hookIDs[repo.ProjectKey+","+repo.RepositorySlug] = hook.ID
   314  			break
   315  		}
   316  	}
   317  
   318  	newHook := bitbucketv1.Webhook{
   319  		Name:          "Argo Events",
   320  		Url:           formattedURL,
   321  		Active:        true,
   322  		Events:        bitbucketserverEventSource.Events,
   323  		Configuration: bitbucketv1.WebhookConfiguration{Secret: router.hookSecret},
   324  	}
   325  
   326  	requestBody, err := router.createRequestBodyFromWebhook(newHook)
   327  	if err != nil {
   328  		return fmt.Errorf("failed to create request body from webhook, %w", err)
   329  	}
   330  
   331  	// Update the webhook when it does exist and the events/configuration have changed
   332  	if isAlreadyExists {
   333  		logger.Info("webhook already exists")
   334  		if router.shouldUpdateWebhook(existingHook, newHook) {
   335  			logger.Info("webhook requires an update")
   336  			err = router.updateWebhook(bitbucketClient, existingHook.ID, requestBody, repo)
   337  			if err != nil {
   338  				return fmt.Errorf("failed to update webhook. err: %w", err)
   339  			}
   340  
   341  			logger.With("hook-id", existingHook.ID).Info("hook successfully updated")
   342  		}
   343  
   344  		return nil
   345  	}
   346  
   347  	// Create the webhook when it doesn't exist yet
   348  	createdHook, err := router.createWebhook(bitbucketClient, requestBody, repo)
   349  	if err != nil {
   350  		return fmt.Errorf("failed to create webhook. err: %w", err)
   351  	}
   352  
   353  	router.hookIDs[repo.ProjectKey+","+repo.RepositorySlug] = createdHook.ID
   354  
   355  	logger.With("hook-id", createdHook.ID).Info("hook successfully registered")
   356  
   357  	return nil
   358  }
   359  
   360  func (router *Router) listWebhooks(bitbucketClient *bitbucketv1.APIClient, repo v1alpha1.BitbucketServerRepository) ([]bitbucketv1.Webhook, error) {
   361  	apiResponse, err := bitbucketClient.DefaultApi.FindWebhooks(repo.ProjectKey, repo.RepositorySlug, nil)
   362  	if err != nil {
   363  		return nil, fmt.Errorf("failed to list existing hooks to check for duplicates for repository %s/%s, %w", repo.ProjectKey, repo.RepositorySlug, err)
   364  	}
   365  
   366  	hooks, err := bitbucketv1.GetWebhooksResponse(apiResponse)
   367  	if err != nil {
   368  		return nil, fmt.Errorf("failed to convert the list of webhooks for repository %s/%s, %w", repo.ProjectKey, repo.RepositorySlug, err)
   369  	}
   370  
   371  	return hooks, nil
   372  }
   373  
   374  func (router *Router) createWebhook(bitbucketClient *bitbucketv1.APIClient, requestBody []byte, repo v1alpha1.BitbucketServerRepository) (*bitbucketv1.Webhook, error) {
   375  	apiResponse, err := bitbucketClient.DefaultApi.CreateWebhook(repo.ProjectKey, repo.RepositorySlug, requestBody, []string{"application/json"})
   376  	if err != nil {
   377  		return nil, fmt.Errorf("failed to add webhook. err: %w", err)
   378  	}
   379  
   380  	var createdHook *bitbucketv1.Webhook
   381  	err = mapstructure.Decode(apiResponse.Values, &createdHook)
   382  	if err != nil {
   383  		return nil, fmt.Errorf("failed to convert API response to Webhook struct. err: %w", err)
   384  	}
   385  
   386  	return createdHook, nil
   387  }
   388  
   389  func (router *Router) updateWebhook(bitbucketClient *bitbucketv1.APIClient, hookID int, requestBody []byte, repo v1alpha1.BitbucketServerRepository) error {
   390  	_, err := bitbucketClient.DefaultApi.UpdateWebhook(repo.ProjectKey, repo.RepositorySlug, int32(hookID), requestBody, []string{"application/json"})
   391  
   392  	return err
   393  }
   394  
   395  func (router *Router) shouldUpdateWebhook(existingHook bitbucketv1.Webhook, newHook bitbucketv1.Webhook) bool {
   396  	return !common.ElementsMatch(existingHook.Events, newHook.Events) ||
   397  		existingHook.Configuration.Secret != newHook.Configuration.Secret
   398  }
   399  
   400  func (router *Router) createRequestBodyFromWebhook(hook bitbucketv1.Webhook) ([]byte, error) {
   401  	var err error
   402  	var finalHook interface{} = hook
   403  
   404  	// if the hook doesn't have a secret, the configuration field must be removed in order for the request to succeed,
   405  	// otherwise Bitbucket Server sends 500 response because of empty string value in the hook.Configuration.Secret field
   406  	if hook.Configuration.Secret == "" {
   407  		hookMap := make(map[string]interface{})
   408  		err = common.StructToMap(hook, hookMap)
   409  		if err != nil {
   410  			return nil, fmt.Errorf("failed to convert webhook to map, %w", err)
   411  		}
   412  
   413  		delete(hookMap, "configuration")
   414  
   415  		finalHook = hookMap
   416  	}
   417  
   418  	requestBody, err := json.Marshal(finalHook)
   419  	if err != nil {
   420  		return nil, fmt.Errorf("failed to marshal new webhook to JSON, %w", err)
   421  	}
   422  
   423  	return requestBody, nil
   424  }
   425  
   426  func (router *Router) parseAndValidateBitbucketServerRequest(request *http.Request) ([]byte, error) {
   427  	body, err := io.ReadAll(request.Body)
   428  	if err != nil {
   429  		return nil, fmt.Errorf("failed to parse request body, %w", err)
   430  	}
   431  
   432  	if len(router.hookSecret) != 0 {
   433  		signature := request.Header.Get("X-Hub-Signature")
   434  		if len(signature) == 0 {
   435  			return nil, fmt.Errorf("missing signature header")
   436  		}
   437  
   438  		mac := hmac.New(sha256.New, []byte(router.hookSecret))
   439  		_, _ = mac.Write(body)
   440  		expectedMAC := hex.EncodeToString(mac.Sum(nil))
   441  
   442  		if !hmac.Equal([]byte(signature[7:]), []byte(expectedMAC)) {
   443  			return nil, fmt.Errorf("hmac verification failed")
   444  		}
   445  	}
   446  
   447  	return body, nil
   448  }