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 }