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  }