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 }