github.com/argoproj/argo-events@v1.9.1/eventsources/sources/gitlab/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 gitlab 18 19 import ( 20 "context" 21 "crypto/rand" 22 "encoding/json" 23 "fmt" 24 "io" 25 "math/big" 26 "net/http" 27 "reflect" 28 "time" 29 30 "github.com/argoproj/argo-events/common" 31 "github.com/argoproj/argo-events/common/logging" 32 eventsourcecommon "github.com/argoproj/argo-events/eventsources/common" 33 "github.com/argoproj/argo-events/eventsources/common/webhook" 34 "github.com/argoproj/argo-events/eventsources/sources" 35 "github.com/argoproj/argo-events/pkg/apis/events" 36 "github.com/xanzy/go-gitlab" 37 "go.uber.org/zap" 38 ) 39 40 // controller controls the webhook operations 41 var ( 42 controller = webhook.NewController() 43 ) 44 45 // set up the activation and inactivation channels to control the state of routes. 46 func init() { 47 go webhook.ProcessRouteStatus(controller) 48 } 49 50 // Implement Router 51 // 1. GetRoute 52 // 2. HandleRoute 53 // 3. PostActivate 54 // 4. PostDeactivate 55 56 // GetRoute returns the route 57 func (router *Router) GetRoute() *webhook.Route { 58 return router.route 59 } 60 61 // HandleRoute handles incoming requests on the route 62 func (router *Router) HandleRoute(writer http.ResponseWriter, request *http.Request) { 63 route := router.GetRoute() 64 65 logger := route.Logger.With( 66 logging.LabelEndpoint, route.Context.Endpoint, 67 logging.LabelPort, route.Context.Port, 68 logging.LabelHTTPMethod, route.Context.Method, 69 ) 70 71 logger.Info("received a request, processing it...") 72 73 if !route.Active { 74 logger.Info("endpoint is not active, won't process the request") 75 common.SendErrorResponse(writer, "inactive endpoint") 76 return 77 } 78 79 defer func(start time.Time) { 80 route.Metrics.EventProcessingDuration(route.EventSourceName, route.EventName, float64(time.Since(start)/time.Millisecond)) 81 }(time.Now()) 82 83 if router.secretToken != "" { 84 if t := request.Header.Get("X-Gitlab-Token"); t != router.secretToken { 85 common.SendErrorResponse(writer, "token mismatch") 86 return 87 } 88 } 89 request.Body = http.MaxBytesReader(writer, request.Body, route.Context.GetMaxPayloadSize()) 90 body, err := io.ReadAll(request.Body) 91 if err != nil { 92 logger.Errorw("failed to parse request body", zap.Error(err)) 93 common.SendErrorResponse(writer, err.Error()) 94 route.Metrics.EventProcessingFailed(route.EventSourceName, route.EventName) 95 return 96 } 97 98 event := &events.GitLabEventData{ 99 Headers: request.Header, 100 Body: (*json.RawMessage)(&body), 101 Metadata: router.gitlabEventSource.Metadata, 102 } 103 104 eventBody, err := json.Marshal(event) 105 if err != nil { 106 logger.Info("failed to marshal event") 107 common.SendErrorResponse(writer, "invalid event") 108 route.Metrics.EventProcessingFailed(route.EventSourceName, route.EventName) 109 return 110 } 111 112 logger.Info("dispatching event on route's data channel") 113 route.DataCh <- eventBody 114 115 logger.Info("request successfully processed") 116 common.SendSuccessResponse(writer, "success") 117 } 118 119 // PostActivate performs operations once the route is activated and ready to consume requests 120 func (router *Router) PostActivate() error { 121 return nil 122 } 123 124 // PostInactivate performs operations after the route is inactivated 125 func (router *Router) PostInactivate() error { 126 gitlabEventSource := router.gitlabEventSource 127 if !gitlabEventSource.NeedToCreateHooks() || !gitlabEventSource.DeleteHookOnFinish { 128 return nil 129 } 130 131 logger := router.route.Logger 132 logger.Info("deleting Gitlab hooks...") 133 134 for _, g := range gitlabEventSource.GetGroups() { 135 id, ok := router.groupHookIDs[g] 136 if !ok { 137 return fmt.Errorf("can not find hook ID for group %s", g) 138 } 139 if _, err := router.gitlabClient.Groups.DeleteGroupHook(g, id); err != nil { 140 return fmt.Errorf("failed to delete hook for group %s. err: %w", g, err) 141 } 142 logger.Infof("Gitlab hook deleted for group %s", g) 143 } 144 145 for _, p := range gitlabEventSource.GetProjects() { 146 id, ok := router.projectHookIDs[p] 147 if !ok { 148 return fmt.Errorf("can not find hook ID for project %s", p) 149 } 150 if _, err := router.gitlabClient.Projects.DeleteProjectHook(p, id); err != nil { 151 return fmt.Errorf("failed to delete hook for project %s. err: %w", p, err) 152 } 153 logger.Infof("Gitlab hook deleted for project %s", p) 154 } 155 return nil 156 } 157 158 // StartListening starts an event source 159 func (el *EventListener) StartListening(ctx context.Context, dispatch func([]byte, ...eventsourcecommon.Option) error) error { 160 logger := logging.FromContext(ctx). 161 With(logging.LabelEventSourceType, el.GetEventSourceType(), logging.LabelEventName, el.GetEventName()) 162 logger.Info("started processing the Gitlab event source...") 163 164 defer sources.Recover(el.GetEventName()) 165 166 gitlabEventSource := &el.GitlabEventSource 167 168 route := webhook.NewRoute(gitlabEventSource.Webhook, logger, el.GetEventSourceName(), el.GetEventName(), el.Metrics) 169 router := &Router{ 170 route: route, 171 gitlabEventSource: gitlabEventSource, 172 projectHookIDs: make(map[string]int), 173 groupHookIDs: make(map[string]int), 174 } 175 176 if gitlabEventSource.NeedToCreateHooks() { 177 // In order to set up a hook for the GitLab project, 178 // 1. Get the API access token for client 179 // 2. Set up GitLab client 180 // 3. Configure Hook with given event type 181 // 4. Create project hook 182 183 logger.Info("retrieving the access token credentials...") 184 185 defaultEventValue := false 186 formattedURL := common.FormattedURL(gitlabEventSource.Webhook.URL, gitlabEventSource.Webhook.Endpoint) 187 opt := &gitlab.AddProjectHookOptions{ 188 URL: &formattedURL, 189 EnableSSLVerification: &router.gitlabEventSource.EnableSSLVerification, 190 ConfidentialNoteEvents: &defaultEventValue, 191 PushEvents: &defaultEventValue, 192 IssuesEvents: &defaultEventValue, 193 ConfidentialIssuesEvents: &defaultEventValue, 194 MergeRequestsEvents: &defaultEventValue, 195 TagPushEvents: &defaultEventValue, 196 NoteEvents: &defaultEventValue, 197 JobEvents: &defaultEventValue, 198 PipelineEvents: &defaultEventValue, 199 WikiPageEvents: &defaultEventValue, 200 } 201 202 for _, event := range gitlabEventSource.Events { 203 elem := reflect.ValueOf(opt).Elem().FieldByName(event) 204 if ok := elem.IsValid(); !ok { 205 return fmt.Errorf("unknown event %s", event) 206 } 207 iev := reflect.New(elem.Type().Elem()) 208 reflect.Indirect(iev).SetBool(true) 209 elem.Set(iev) 210 } 211 groupHookOpt := &gitlab.AddGroupHookOptions{ 212 URL: opt.URL, 213 EnableSSLVerification: opt.EnableSSLVerification, 214 ConfidentialNoteEvents: opt.ConfidentialNoteEvents, 215 PushEvents: opt.PushEvents, 216 IssuesEvents: opt.IssuesEvents, 217 ConfidentialIssuesEvents: opt.ConfidentialIssuesEvents, 218 MergeRequestsEvents: opt.MergeRequestsEvents, 219 TagPushEvents: opt.TagPushEvents, 220 NoteEvents: opt.NoteEvents, 221 JobEvents: opt.JobEvents, 222 PipelineEvents: opt.PipelineEvents, 223 WikiPageEvents: opt.WikiPageEvents, 224 } 225 226 if gitlabEventSource.SecretToken != nil { 227 token, err := common.GetSecretFromVolume(gitlabEventSource.SecretToken) 228 if err != nil { 229 return fmt.Errorf("failed to retrieve secret token. err: %w", err) 230 } 231 opt.Token = &token 232 groupHookOpt.Token = &token 233 router.secretToken = token 234 } 235 236 accessToken, err := common.GetSecretFromVolume(gitlabEventSource.AccessToken) 237 if err != nil { 238 return fmt.Errorf("failed to get gitlab credentials. err: %w", err) 239 } 240 241 logger.Info("setting up the client to connect to GitLab...") 242 router.gitlabClient, err = gitlab.NewClient(accessToken, gitlab.WithBaseURL(gitlabEventSource.GitlabBaseURL)) 243 if err != nil { 244 return fmt.Errorf("failed to initialize client, %w", err) 245 } 246 247 f := func() { 248 for _, g := range gitlabEventSource.GetGroups() { 249 hooks, _, err := router.gitlabClient.Groups.ListGroupHooks(g, &gitlab.ListGroupHooksOptions{}) 250 if err != nil { 251 logger.Errorf("failed to list existing webhooks of group %s. err: %+v", g, err) 252 continue 253 } 254 hook := getGroupHook(hooks, formattedURL) 255 if hook != nil { 256 router.groupHookIDs[g] = hook.ID 257 continue 258 } 259 logger.Infof("hook not found for group %s, creating ...", g) 260 hook, _, err = router.gitlabClient.Groups.AddGroupHook(g, groupHookOpt) 261 if err != nil { 262 logger.Errorf("failed to create gitlab webhook for group %s. err: %+v", g, err) 263 continue 264 } 265 router.groupHookIDs[g] = hook.ID 266 time.Sleep(500 * time.Millisecond) 267 } 268 269 for _, p := range gitlabEventSource.GetProjects() { 270 hooks, _, err := router.gitlabClient.Projects.ListProjectHooks(p, &gitlab.ListProjectHooksOptions{}) 271 if err != nil { 272 logger.Errorf("failed to list existing webhooks of project %s. err: %+v", p, err) 273 continue 274 } 275 hook := getProjectHook(hooks, formattedURL) 276 if hook != nil { 277 router.projectHookIDs[p] = hook.ID 278 continue 279 } 280 logger.Infof("hook not found for project %s, creating ...", p) 281 hook, _, err = router.gitlabClient.Projects.AddProjectHook(p, opt) 282 if err != nil { 283 logger.Errorf("failed to create gitlab webhook for project %s. err: %+v", p, err) 284 continue 285 } 286 router.projectHookIDs[p] = hook.ID 287 time.Sleep(500 * time.Millisecond) 288 } 289 } 290 291 // Mitigate race condtions - it might create multiple hooks with same config when replicas > 1 292 randomNum, _ := rand.Int(rand.Reader, big.NewInt(int64(2000))) 293 time.Sleep(time.Duration(randomNum.Int64()) * time.Millisecond) 294 f() 295 296 ctx, cancel := context.WithCancel(ctx) 297 defer cancel() 298 299 go func() { 300 // Another kind of race conditions might happen when pods do rolling upgrade - new pod starts 301 // and old pod terminates, if DeleteHookOnFinish is true, the hook will be deleted from gitlab. 302 // This is a workround to mitigate the race conditions. 303 logger.Info("starting gitlab hooks manager daemon") 304 ticker := time.NewTicker(60 * time.Second) 305 defer ticker.Stop() 306 for { 307 select { 308 case <-ctx.Done(): 309 logger.Info("exiting gitlab hooks manager daemon") 310 return 311 case <-ticker.C: 312 f() 313 } 314 } 315 }() 316 } else { 317 logger.Info("no need to create webhooks") 318 } 319 320 return webhook.ManageRoute(ctx, router, controller, dispatch) 321 }