diff --git a/cli/cdsctl/workflow_transform_as_code.go b/cli/cdsctl/workflow_transform_as_code.go index c197fbcb91..4df1f757a7 100644 --- a/cli/cdsctl/workflow_transform_as_code.go +++ b/cli/cdsctl/workflow_transform_as_code.go @@ -81,7 +81,7 @@ func workflowTransformAsCodeRun(v cli.Values) (interface{}, error) { } switch ope.Status { case sdk.OperationStatusError: - return nil, fmt.Errorf("cannot perform operation: %s", ope.Error) + return nil, fmt.Errorf("cannot perform operation: %v", ope.Error) } return response, nil } diff --git a/engine/api/api.go b/engine/api/api.go index 7f03b08ca2..7b8bebbc8c 100644 --- a/engine/api/api.go +++ b/engine/api/api.go @@ -662,7 +662,7 @@ func (a *API) Serve(ctx context.Context) error { a.serviceAPIHeartbeat(ctx) }, a.PanicDump()) sdk.GoRoutine(ctx, "authentication.SessionCleaner", func(ctx context.Context) { - authentication.SessionCleaner(ctx, a.mustDB) + authentication.SessionCleaner(ctx, a.mustDB, 10*time.Second) }, a.PanicDump()) isFreshInstall, errF := version.IsFreshInstall(a.mustDB()) diff --git a/engine/api/ascode/pull_request.go b/engine/api/ascode/pull_request.go index 86770ba940..4c5decc841 100644 --- a/engine/api/ascode/pull_request.go +++ b/engine/api/ascode/pull_request.go @@ -54,12 +54,12 @@ forLoop: case <-tick.C: ope, err := operation.GetRepositoryOperation(ctx, db, ed.OperationUUID) if err != nil { - globalErr = sdk.NewErrorFrom(err, "unable to get repository operation %s", ed.OperationUUID) + globalErr = sdk.WrapError(err, "unable to get repository operation %s", ed.OperationUUID) break forLoop } - if ope.Status == sdk.OperationStatusError { - globalErr = sdk.NewErrorFrom(sdk.ErrUnknownError, "repository operation in error: %s", ope.Error) + globalOperation.Error = ope.Error + globalErr = ope.Error.ToError() break forLoop } if ope.Status == sdk.OperationStatusDone { @@ -76,7 +76,6 @@ forLoop: } } if globalErr != nil { - httpErr := sdk.ExtractHTTPError(globalErr, "") isErrWithStack := sdk.IsErrorWithStack(globalErr) fields := logrus.Fields{} if isErrWithStack { @@ -85,7 +84,9 @@ forLoop: log.ErrorWithFields(ctx, fields, "%s", globalErr) globalOperation.Status = sdk.OperationStatusError - globalOperation.Error = httpErr.Error() + if globalOperation.Error == nil { + globalOperation.Error = sdk.ToOperationError(globalErr) + } } _ = store.SetWithTTL(cache.Key(operation.CacheOperationKey, globalOperation.UUID), globalOperation, 300) diff --git a/engine/api/authentication/session.go b/engine/api/authentication/session.go index 57fe0ec2cb..bb48633833 100644 --- a/engine/api/authentication/session.go +++ b/engine/api/authentication/session.go @@ -70,10 +70,10 @@ func CheckSessionJWT(jwtToken string) (*jwt.Token, error) { } // SessionCleaner must be run as a goroutine -func SessionCleaner(ctx context.Context, dbFunc func() *gorp.DbMap) { +func SessionCleaner(ctx context.Context, dbFunc func() *gorp.DbMap, tickerDuration time.Duration) { log.Info(ctx, "Initializing session cleaner...") db := dbFunc() - tick := time.NewTicker(10 * time.Second) + tick := time.NewTicker(tickerDuration) tickCorruped := time.NewTicker(12 * time.Hour) defer tick.Stop() defer tickCorruped.Stop() diff --git a/engine/api/authentication/session_test.go b/engine/api/authentication/session_test.go index 15b8fcadb6..7f24c79ebb 100644 --- a/engine/api/authentication/session_test.go +++ b/engine/api/authentication/session_test.go @@ -43,9 +43,9 @@ func Test_CheckSessionJWT(t *testing.T) { } func Test_SessionCleaner(t *testing.T) { - ctx, cancel := context.WithTimeout(context.TODO(), 15*time.Second) + ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second) defer cancel() db, _ := test.SetupPG(t, bootstrap.InitiliazeDB) - authentication.SessionCleaner(ctx, func() *gorp.DbMap { return db }) + authentication.SessionCleaner(ctx, func() *gorp.DbMap { return db }, 1*time.Second) } diff --git a/engine/api/services/http.go b/engine/api/services/http.go index 86d6ccf86c..7a9901dce5 100644 --- a/engine/api/services/http.go +++ b/engine/api/services/http.go @@ -142,6 +142,9 @@ func doJSONRequest(ctx context.Context, db gorp.SqlExecutor, srvs []sdk.Service, } return headers, code, nil } + if lastCode < 409 { + break + } } log.Error(ctx, "unable to call service: maximum attempt exceed : %+v", lastErr) diff --git a/engine/api/workflow/dao_run.go b/engine/api/workflow/dao_run.go index 3c5160d73c..60dbb6149c 100644 --- a/engine/api/workflow/dao_run.go +++ b/engine/api/workflow/dao_run.go @@ -494,7 +494,7 @@ func CanBeRun(workflowRun *sdk.WorkflowRun, workflowNodeRun *sdk.WorkflowNodeRun } ancestorsID := node.Ancestors(workflowRun.Workflow.WorkflowData) - if ancestorsID == nil || len(ancestorsID) == 0 { + if len(ancestorsID) == 0 { return true } for _, ancestorID := range ancestorsID { diff --git a/engine/api/workflow/execute_node_run.go b/engine/api/workflow/execute_node_run.go index 468feb493e..b41119595e 100644 --- a/engine/api/workflow/execute_node_run.go +++ b/engine/api/workflow/execute_node_run.go @@ -956,14 +956,14 @@ func stopWorkflowNodeJobRun(ctx context.Context, dbFunc func() *gorp.DbMap, stor njr, errNRJ := LoadAndLockNodeJobRunWait(ctx, tx, store, njrID) if errNRJ != nil { chanErr <- sdk.WrapError(errNRJ, "StopWorkflowNodeRun> Cannot load node job run id") - tx.Rollback() + _ = tx.Rollback() wg.Done() return report } if err := AddSpawnInfosNodeJobRun(tx, njr.WorkflowNodeRunID, njr.ID, []sdk.SpawnInfo{stopInfos}); err != nil { chanErr <- sdk.WrapError(err, "Cannot save spawn info job %d", njr.ID) - tx.Rollback() + _ = tx.Rollback() wg.Done() return report } @@ -973,14 +973,14 @@ func stopWorkflowNodeJobRun(ctx context.Context, dbFunc func() *gorp.DbMap, stor report.Merge(ctx, r) if err != nil { chanErr <- sdk.WrapError(err, "cannot update node job run") - tx.Rollback() + _ = tx.Rollback() wg.Done() return report } if err := tx.Commit(); err != nil { chanErr <- sdk.WithStack(err) - tx.Rollback() + _ = tx.Rollback() wg.Done() return report } @@ -1077,7 +1077,7 @@ func getVCSInfos(ctx context.Context, db gorp.SqlExecutor, store cache.Store, pr // Check repository value if vcsInfos.Repository == "" { vcsInfos.Repository = applicationRepositoryFullname - } else if strings.ToLower(vcsInfos.Repository) != strings.ToLower(applicationRepositoryFullname) { + } else if !strings.EqualFold(vcsInfos.Repository, applicationRepositoryFullname) { //The input repository is not the same as the application, we have to check if it is a fork forks, err := client.ListForks(ctx, applicationRepositoryFullname) if err != nil { diff --git a/engine/api/workflow/hook.go b/engine/api/workflow/hook.go index 20be683fe5..70c1215bf5 100644 --- a/engine/api/workflow/hook.go +++ b/engine/api/workflow/hook.go @@ -131,15 +131,22 @@ func hookRegistration(ctx context.Context, db gorp.SqlExecutor, store cache.Stor previousHook, has := oldHooks[h.UUID] // If previous hook is the same, we do nothing if has && h.Equals(*previousHook) { + // If this a repowebhook with an empty eventFilter, let's keep the old one because vcs won't be called to get the default eventFilter + eventFilter, has := h.GetConfigValue(sdk.HookConfigEventFilter) + if previousHook.IsRepositoryWebHook() && h.IsRepositoryWebHook() && + (!has || eventFilter == "") { + h.Config[sdk.HookConfigEventFilter] = previousHook.Config[sdk.HookConfigEventFilter] + } continue } + } // initialize a UUID is there no uuid if h.UUID == "" { h.UUID = sdk.UUID() } - if h.HookModelName == sdk.RepositoryWebHookModelName || h.HookModelName == sdk.GitPollerModelName || h.HookModelName == sdk.GerritHookModelName { + if h.IsRepositoryWebHook() || h.HookModelName == sdk.GitPollerModelName || h.HookModelName == sdk.GerritHookModelName { if wf.WorkflowData.Node.Context.ApplicationID == 0 || wf.Applications[wf.WorkflowData.Node.Context.ApplicationID].RepositoryFullname == "" || wf.Applications[wf.WorkflowData.Node.Context.ApplicationID].VCSServer == "" { return sdk.NewErrorFrom(sdk.ErrForbidden, "cannot create a git poller or repository webhook on an application without a repository") } @@ -157,6 +164,7 @@ func hookRegistration(ctx context.Context, db gorp.SqlExecutor, store cache.Stor return err } hookToUpdate[h.UUID] = *h + log.Debug("workflow.hookrRegistration> following hook must be updated: %+v", h) } if len(hookToUpdate) > 0 { @@ -179,7 +187,10 @@ func hookRegistration(ctx context.Context, db gorp.SqlExecutor, store cache.Stor continue } v, ok := h.Config[sdk.HookConfigWebHookID] - if h.HookModelName == sdk.RepositoryWebHookModelName && h.Config["vcsServer"].Value != "" { + if h.IsRepositoryWebHook() { + log.Debug("workflow.hookRegistration> managing vcs configuration: %+v", h) + } + if h.IsRepositoryWebHook() && h.Config["vcsServer"].Value != "" { if !ok || v.Value == "" { if err := createVCSConfiguration(ctx, db, store, proj, h); err != nil { return sdk.WithStack(err) diff --git a/engine/api/workflow/process_start.go b/engine/api/workflow/process_start.go index bd8ce7d35b..a6e4bc6506 100644 --- a/engine/api/workflow/process_start.go +++ b/engine/api/workflow/process_start.go @@ -115,7 +115,6 @@ func processAllJoins(ctx context.Context, db gorp.SqlExecutor, store cache.Store //now checks if all sources have been completed var ok = true - nodeRunIDs := []int64{} sourcesParams := map[string]string{} for _, nodeRun := range sources { if nodeRun == nil { @@ -136,7 +135,6 @@ func processAllJoins(ctx context.Context, db gorp.SqlExecutor, store cache.Store } } - nodeRunIDs = append(nodeRunIDs, nodeRun.ID) //Merge build parameters from all sources sourcesParams = sdk.ParametersMapMerge(sourcesParams, sdk.ParametersToMap(nodeRun.BuildParameters)) } diff --git a/engine/api/workflow/repository.go b/engine/api/workflow/repository.go index acd7177f19..f739c9c6cc 100644 --- a/engine/api/workflow/repository.go +++ b/engine/api/workflow/repository.go @@ -189,7 +189,7 @@ func pollRepositoryOperation(c context.Context, db gorp.SqlExecutor, store cache opeTrusted := *ope opeTrusted.RepositoryStrategy.SSHKeyContent = sdk.PasswordPlaceholder opeTrusted.RepositoryStrategy.Password = sdk.PasswordPlaceholder - return nil, sdk.WrapError(fmt.Errorf("%s", ope.Error), "operation in error: %+v", opeTrusted) + return nil, sdk.WrapError(fmt.Errorf("%+v", ope.Error), "operation in error: %+v", opeTrusted) case sdk.OperationStatusDone: return ope, nil } diff --git a/engine/api/workflow/resync_workflow.go b/engine/api/workflow/resync_workflow.go index fcdbd35672..6b1b4b30d7 100644 --- a/engine/api/workflow/resync_workflow.go +++ b/engine/api/workflow/resync_workflow.go @@ -42,7 +42,7 @@ func Resync(ctx context.Context, db gorp.SqlExecutor, store cache.Store, proj sd wr.Workflow.HookModels = wf.HookModels wr.Workflow.OutGoingHookModels = wf.OutGoingHookModels - return UpdateWorkflowRun(nil, db, wr) + return UpdateWorkflowRun(ctx, db, wr) } //ResyncWorkflowRunStatus resync the status of workflow if you stop a node run when workflow run is building diff --git a/engine/api/workflow/workflow_parser.go b/engine/api/workflow/workflow_parser.go index 82f6c1d204..a063ad9739 100644 --- a/engine/api/workflow/workflow_parser.go +++ b/engine/api/workflow/workflow_parser.go @@ -99,7 +99,7 @@ func ParseAndImport(ctx context.Context, db gorp.SqlExecutor, store cache.Store, if oldW != nil { for i := range oldW.WorkflowData.Node.Hooks { h := &oldW.WorkflowData.Node.Hooks[i] - if h.HookModelName == sdk.RepositoryWebHookModel.Name { + if h.IsRepositoryWebHook() { oldRepoWebHook = h break } @@ -111,24 +111,36 @@ func ParseAndImport(ctx context.Context, db gorp.SqlExecutor, store cache.Store, // Get current webhook for i := range w.WorkflowData.Node.Hooks { h := &w.WorkflowData.Node.Hooks[i] - if h.HookModelName == sdk.RepositoryWebHookModel.Name { + if h.IsRepositoryWebHook() { h.UUID = oldRepoWebHook.UUID - h.Config = oldRepoWebHook.Config.Clone() - h.Config[sdk.HookConfigWorkflow] = sdk.WorkflowNodeHookConfigValue{Value: w.Name} + h.Config.MergeWith( + oldRepoWebHook.Config.Filter( + func(k string, v sdk.WorkflowNodeHookConfigValue) bool { + return !v.Configurable + }, + ), + ) + // get only non cofigurable stuff currentRepoWebHook = h + log.Debug("workflow.ParseAndImport> keeping the old repository web hook: %+v (%+v)", h, oldRepoWebHook) break } } - // If not found + // If not found, take the default config if currentRepoWebHook == nil { h := sdk.NodeHook{ UUID: oldRepoWebHook.UUID, HookModelName: oldRepoWebHook.HookModelName, - Config: oldRepoWebHook.Config.Clone(), - HookModelID: oldRepoWebHook.HookModelID, + Config: sdk.RepositoryWebHookModel.DefaultConfig.Clone(), + HookModelID: sdk.RepositoryWebHookModel.ID, + } + oldNonConfigurableConfig := oldRepoWebHook.Config.Filter(func(k string, v sdk.WorkflowNodeHookConfigValue) bool { + return !v.Configurable + }) + for k, v := range oldNonConfigurableConfig { + h.Config[k] = v } - h.Config[sdk.HookConfigWorkflow] = sdk.WorkflowNodeHookConfigValue{Value: w.Name} w.WorkflowData.Node.Hooks = append(w.WorkflowData.Node.Hooks, h) } } @@ -144,13 +156,18 @@ func ParseAndImport(ctx context.Context, db gorp.SqlExecutor, store cache.Store, Config: sdk.RepositoryWebHookModel.DefaultConfig.Clone(), } - // If the new workflow already contains a repowebhook (comparing refs), we dont have to add a new one + // If the new workflow already contains a repowebhook, we dont have to add a new one var hasARepoWebHook bool for _, h := range w.WorkflowData.Node.Hooks { if h.Ref() == newRepoWebHook.Ref() { hasARepoWebHook = true break } + if h.HookModelName == newRepoWebHook.HookModelName && + h.ConfigValueContainsEventsDefault() { + hasARepoWebHook = true + break + } } if !hasARepoWebHook { w.WorkflowData.Node.Hooks = append(w.WorkflowData.Node.Hooks, newRepoWebHook) diff --git a/engine/api/workflow_ascode_rename_test.go b/engine/api/workflow_ascode_rename_test.go index 7a70b26d8a..05a04d1ec8 100644 --- a/engine/api/workflow_ascode_rename_test.go +++ b/engine/api/workflow_ascode_rename_test.go @@ -264,6 +264,11 @@ version: v1.0`), func(ctx context.Context, method, path string, in interface{}, out interface{}, _ interface{}) (http.Header, int, error) { vcsHooks := in.(*sdk.VCSHook) vcsHooks.ID = sdk.UUID() + require.Len(t, vcsHooks.Events, 0, "events list should be empty, default value is set by vcs") + + vcsHooks.Events = []string{ + "push", + } *(out.(*sdk.VCSHook)) = *vcsHooks return nil, 200, nil }, @@ -296,6 +301,20 @@ version: v1.0`), assert.NoError(t, err) assert.NotNil(t, wk) + require.Len(t, wk.WorkflowData.GetHooks(), 1) + + for _, h := range wk.WorkflowData.GetHooks() { + log.Debug("--> %T %+v", h, h) + require.Equal(t, "RepositoryWebHook", h.HookModelName) + require.Equal(t, "push", h.Config["eventFilter"].Value) + require.Equal(t, "Github", h.Config["hookIcon"].Value) + require.Equal(t, "POST", h.Config["method"].Value) + require.Equal(t, proj.Key, h.Config["project"].Value) + require.Equal(t, "fsamin/go-repo", h.Config["repoFullName"].Value) + require.Equal(t, "github", h.Config["vcsServer"].Value) + require.Equal(t, wk.Name, h.Config["workflow"].Value) + } + // Then we will trigger a run of the workflow wich should trigger an as-code operation with a renamed workflow vars := map[string]string{ "key": proj.Key, @@ -343,13 +362,13 @@ version: v1.0`), for _, h := range wk.WorkflowData.GetHooks() { log.Debug("--> %T %+v", h, h) - assert.Equal(t, "RepositoryWebHook", h.HookModelName) - assert.Equal(t, "push", h.Config["eventFilter"].Value) - assert.Equal(t, "Github", h.Config["hookIcon"].Value) - assert.Equal(t, "POST", h.Config["method"].Value) - assert.Equal(t, proj.Key, h.Config["project"].Value) - assert.Equal(t, "fsamin/go-repo", h.Config["repoFullName"].Value) - assert.Equal(t, "github", h.Config["vcsServer"].Value) - assert.Equal(t, wk.Name, h.Config["workflow"].Value) + require.Equal(t, "RepositoryWebHook", h.HookModelName) + require.Equal(t, "push", h.Config["eventFilter"].Value) + require.Equal(t, "Github", h.Config["hookIcon"].Value) + require.Equal(t, "POST", h.Config["method"].Value) + require.Equal(t, proj.Key, h.Config["project"].Value) + require.Equal(t, "fsamin/go-repo", h.Config["repoFullName"].Value) + require.Equal(t, "github", h.Config["vcsServer"].Value) + require.Equal(t, wk.Name, h.Config["workflow"].Value) } } diff --git a/engine/hooks/tasks_test.go b/engine/hooks/tasks_test.go index 41373da7bb..afd5be77be 100644 --- a/engine/hooks/tasks_test.go +++ b/engine/hooks/tasks_test.go @@ -78,14 +78,14 @@ func Test_dequeueTaskExecutions_ScheduledTask(t *testing.T) { s, cancel := setupTestHookService(t) defer cancel() - ctx, cancel := context.WithTimeout(context.TODO(), 65*time.Second) + ctx, cancel := context.WithTimeout(context.TODO(), 60*time.Second) defer cancel() // Get the mock m := s.Client.(*mock_cdsclient.MockInterface) // Mock the sync of tasks - // It will remove all the tascks from the database + // It will remove all the tasks from the database m.EXPECT().WorkflowAllHooksList().Return([]sdk.NodeHook{}, nil) m.EXPECT().VCSConfiguration().Return(nil, nil).AnyTimes() require.NoError(t, s.synchronizeTasks(ctx)) diff --git a/engine/hooks/test.go b/engine/hooks/test.go index b9745ff462..6e523434f0 100644 --- a/engine/hooks/test.go +++ b/engine/hooks/test.go @@ -30,11 +30,11 @@ func setupTestHookService(t *testing.T) (Service, func()) { ctrl := gomock.NewController(t) s.Client = mock_cdsclient.NewMockInterface(ctrl) - cancel := func() { + t.Cleanup(func() { store.Client.Close() store.Client = nil ctrl.Finish() - } + }) - return s, cancel + return s, func() {} } diff --git a/engine/repositories/processor.go b/engine/repositories/processor.go index ef0a0f0c66..b84b38810d 100644 --- a/engine/repositories/processor.go +++ b/engine/repositories/processor.go @@ -20,10 +20,17 @@ func (s *Service) processor(ctx context.Context) error { } if uuid != "" { op := s.dao.loadOperation(ctx, uuid) + ctx = context.WithValue(ctx, log.ContextLoggingRequestIDKey, op.RequestID) if err := s.do(ctx, *op); err != nil { if err == errLockUnavailable { - log.Info(ctx, "repositories > processor > lock unavailable. Retry") - s.dao.pushOperation(op) + sdk.GoRoutine(ctx, "operation "+uuid+" retry", func(ctx context.Context) { + op.NbRetries++ + log.Info(ctx, "repositories > processor > lock unavailable. retry") + time.Sleep(time.Duration(2*op.NbRetries) * time.Second) + if err := s.dao.pushOperation(op); err != nil { + log.Error(ctx, "repositories > processor > %v", err) + } + }) } else { log.Error(ctx, "repositories > processor > %v", err) } @@ -55,10 +62,10 @@ func (s *Service) do(ctx context.Context, op sdk.Operation) error { } log.ErrorWithFields(ctx, fields, "%s", err) - op.Error = sdk.ExtractHTTPError(err, "").Error() + op.Error = sdk.ToOperationError(err) op.Status = sdk.OperationStatusError } else { - op.Error = "" + op.Error = nil op.Status = sdk.OperationStatusDone switch { case op.LoadFiles.Pattern != "": @@ -70,14 +77,14 @@ func (s *Service) do(ctx context.Context, op sdk.Operation) error { } log.ErrorWithFields(ctx, fields, "%s", err) - op.Error = sdk.ExtractHTTPError(err, "").Error() + op.Error = sdk.ToOperationError(err) op.Status = sdk.OperationStatusError } else { - op.Error = "" + op.Error = nil op.Status = sdk.OperationStatusDone } default: - op.Error = "unrecognized operation" + op.Error = sdk.ToOperationError(sdk.NewErrorFrom(sdk.ErrUnknownError, "unrecognized operation")) op.Status = sdk.OperationStatusError } } @@ -91,14 +98,14 @@ func (s *Service) do(ctx context.Context, op sdk.Operation) error { } log.ErrorWithFields(ctx, fields, "%s", err) - op.Error = sdk.ExtractHTTPError(err, "").Error() + op.Error = sdk.ToOperationError(err) op.Status = sdk.OperationStatusError } else { - op.Error = "" + op.Error = nil op.Status = sdk.OperationStatusDone } default: - op.Error = "unrecognized setup" + op.Error = sdk.ToOperationError(sdk.NewErrorFrom(sdk.ErrUnknownError, "unrecognized setup")) op.Status = sdk.OperationStatusError } diff --git a/engine/repositories/processor_push.go b/engine/repositories/processor_push.go index ecdd19e223..e14d236423 100644 --- a/engine/repositories/processor_push.go +++ b/engine/repositories/processor_push.go @@ -101,8 +101,7 @@ func (s *Service) processPush(ctx context.Context, op *sdk.Operation) error { // In case that there are no changes (ex: push changes on an existing branch that was not merged) if !gitRepo.ExistsDiff() { - log.Debug("processPush> no files changes") - return nil + return sdk.WrapError(sdk.ErrNothingToPush, "processPush> %s : no files changes", op.UUID) } // Commit files @@ -119,6 +118,6 @@ func (s *Service) processPush(ctx context.Context, op *sdk.Operation) error { return sdk.WrapError(err, "push %s", op.Setup.Push.FromBranch) } - log.Debug("processPush> files pushed") + log.Debug("processPush> %s : files pushed", op.UUID) return nil } diff --git a/engine/repositories/repositories_handlers.go b/engine/repositories/repositories_handlers.go index 609c7283fe..482c41fee8 100644 --- a/engine/repositories/repositories_handlers.go +++ b/engine/repositories/repositories_handlers.go @@ -14,6 +14,7 @@ import ( "github.com/ovh/cds/engine/service" "github.com/ovh/cds/sdk" + "github.com/ovh/cds/sdk/log" ) func muxVar(r *http.Request, s string) string { @@ -38,6 +39,10 @@ func (s *Service) postOperationHandler() service.Handler { } } + requestID := ctx.Value(log.ContextLoggingRequestIDKey) + log.Info(ctx, "setting request_id:%s on operation:%s", requestID, op.UUID) + op.RequestID, _ = requestID.(string) + uuid := sdk.UUID() op.UUID = uuid now := time.Now() @@ -111,6 +116,16 @@ func (s *Service) getOperationsHandler() service.Handler { op := s.dao.loadOperation(ctx, uuid) op.RepositoryStrategy.SSHKeyContent = sdk.PasswordPlaceholder op.RepositoryStrategy.Password = sdk.PasswordPlaceholder + + // Handle old representation of operation error + if op.DeprecatedError != "" && op.Error == nil { + op.Error = &sdk.OperationError{ + ID: sdk.ErrUnknownError.ID, + Message: op.DeprecatedError, + Status: sdk.ErrUnknownError.Status, + } + } + return service.WriteJSON(w, op, http.StatusOK) } } diff --git a/engine/vcs/bitbucketcloud/client_hook.go b/engine/vcs/bitbucketcloud/client_hook.go index 15c1192961..72f9f6ba03 100644 --- a/engine/vcs/bitbucketcloud/client_hook.go +++ b/engine/vcs/bitbucketcloud/client_hook.go @@ -24,7 +24,7 @@ func (client *bitbucketcloudClient) CreateHook(ctx context.Context, repo string, } if len(hook.Events) == 0 { - hook.Events = []string{"repo:push"} + hook.Events = sdk.BitbucketCloudEventsDefault } r := WebhookCreate{ Description: "CDS webhook - " + hook.Name, @@ -140,7 +140,7 @@ func (client *bitbucketcloudClient) UpdateHook(ctx context.Context, repo string, } if len(hook.Events) == 0 { - hook.Events = []string{"repo:push"} + hook.Events = sdk.BitbucketCloudEventsDefault } bitbucketHook.Events = hook.Events diff --git a/engine/vcs/bitbucketserver/client_hook.go b/engine/vcs/bitbucketserver/client_hook.go index 9f62db9d76..dff551b12e 100644 --- a/engine/vcs/bitbucketserver/client_hook.go +++ b/engine/vcs/bitbucketserver/client_hook.go @@ -83,7 +83,7 @@ func (b *bitbucketClient) CreateHook(ctx context.Context, repo string, hook *sdk } if len(hook.Events) == 0 { - hook.Events = []string{"repo:refs_changed"} + hook.Events = sdk.BitbucketEventsDefault } url := fmt.Sprintf("/projects/%s/repos/%s/webhooks", project, slug) @@ -119,7 +119,7 @@ func (b *bitbucketClient) UpdateHook(ctx context.Context, repo string, hook *sdk } if len(hook.Events) == 0 { - hook.Events = []string{"repo:refs_changed"} + hook.Events = sdk.BitbucketEventsDefault } bitbucketHook.Events = hook.Events diff --git a/engine/vcs/gerrit/client_hook.go b/engine/vcs/gerrit/client_hook.go index fcaf769f8d..6365403f67 100644 --- a/engine/vcs/gerrit/client_hook.go +++ b/engine/vcs/gerrit/client_hook.go @@ -14,7 +14,7 @@ func (c *gerritClient) GetHook(ctx context.Context, repo, id string) (sdk.VCSHoo //CreateHook enables the default HTTP POST Hook in Gerrit func (c *gerritClient) CreateHook(ctx context.Context, repo string, hook *sdk.VCSHook) error { if len(hook.Events) == 0 { - hook.Events = []string{"patchset-created"} + hook.Events = sdk.GerritEventsDefault } return nil } @@ -22,7 +22,7 @@ func (c *gerritClient) CreateHook(ctx context.Context, repo string, hook *sdk.VC //UpdateHook enables the default HTTP POST Hook in Gerrit func (c *gerritClient) UpdateHook(ctx context.Context, repo string, hook *sdk.VCSHook) error { if len(hook.Events) == 0 { - hook.Events = []string{"patchset-created"} + hook.Events = sdk.GerritEventsDefault } return nil } diff --git a/engine/vcs/github/client_hook.go b/engine/vcs/github/client_hook.go index 5f9cd59423..392f3243ef 100644 --- a/engine/vcs/github/client_hook.go +++ b/engine/vcs/github/client_hook.go @@ -25,7 +25,7 @@ func (g *githubClient) CreateHook(ctx context.Context, repo string, hook *sdk.VC hook.URL = g.proxyURL + hook.URL[lastIndexSlash:] } if len(hook.Events) == 0 { - hook.Events = []string{"push"} + hook.Events = sdk.GitHubEventsDefault } r := WebhookCreate{ @@ -80,7 +80,7 @@ func (g *githubClient) UpdateHook(ctx context.Context, repo string, hook *sdk.VC hook.URL = g.proxyURL + hook.URL[lastIndexSlash:] } if len(hook.Events) == 0 { - hook.Events = []string{"push"} + hook.Events = sdk.GitHubEventsDefault } githubWebHook.Events = hook.Events diff --git a/engine/vcs/github/client_pull_request.go b/engine/vcs/github/client_pull_request.go index 8f19c14eeb..2ac132ea2f 100644 --- a/engine/vcs/github/client_pull_request.go +++ b/engine/vcs/github/client_pull_request.go @@ -33,7 +33,12 @@ func (g *githubClient) PullRequest(ctx context.Context, fullname string, id int) if err := g.Cache.Delete(cachePullRequestKey); err != nil { log.Error(ctx, "githubclient.PullRequest > unable to delete cache key %v: %v", cachePullRequestKey, err) } - return sdk.VCSPullRequest{}, sdk.NewError(sdk.ErrUnknownError, errorAPI(body)) + if status > 500 { + return sdk.VCSPullRequest{}, sdk.NewError(sdk.ErrUnknownError, errorAPI(body)) + } else { + return sdk.VCSPullRequest{}, sdk.NewError(sdk.ErrNotFound, errorAPI(body)) + } + } //Github may return 304 status because we are using conditional request with ETag based headers @@ -198,11 +203,11 @@ func (g *githubClient) PullRequestComment(ctx context.Context, repo string, prRe func (g *githubClient) PullRequestCreate(ctx context.Context, repo string, pr sdk.VCSPullRequest) (sdk.VCSPullRequest, error) { canWrite, err := g.UserHasWritePermission(ctx, repo) if err != nil { - return sdk.VCSPullRequest{}, nil + return sdk.VCSPullRequest{}, err } if !canWrite { if err := g.GrantWritePermission(ctx, repo); err != nil { - return sdk.VCSPullRequest{}, nil + return sdk.VCSPullRequest{}, err } } diff --git a/engine/vcs/github/client_repos.go b/engine/vcs/github/client_repos.go index d5c709d371..ab9311e2c6 100644 --- a/engine/vcs/github/client_repos.go +++ b/engine/vcs/github/client_repos.go @@ -153,9 +153,14 @@ func (g *githubClient) repoByFullname(ctx context.Context, fullname string) (Rep func (g *githubClient) UserHasWritePermission(ctx context.Context, fullname string) (bool, error) { owner := strings.SplitN(fullname, "/", 2)[0] - if g.username == "" || owner == g.username { + if g.username == "" { return false, sdk.WrapError(sdk.ErrUserNotFound, "No user found in configuration") } + if g.username == owner { + log.Debug("githubClient.UserHasWritePermission> nothing to do ¯\\_(ツ)_/¯") + return true, nil + } + url := "/repos/" + fullname + "/collaborators/" + g.username + "/permission" k := cache.Key("vcs", "github", "user-write", g.OAuthToken, url) diff --git a/engine/vcs/gitlab/client_hook.go b/engine/vcs/gitlab/client_hook.go index 925b1a5d95..7aef4e4428 100644 --- a/engine/vcs/gitlab/client_hook.go +++ b/engine/vcs/gitlab/client_hook.go @@ -58,7 +58,7 @@ func (c *gitlabClient) CreateHook(ctx context.Context, repo string, hook *sdk.VC var pushEvent, mergeRequestEvent, TagPushEvent, issueEvent, noteEvent, wikiPageEvent, pipelineEvent, jobEvent bool if len(hook.Events) == 0 { - hook.Events = []string{string(gitlab.EventTypePush), string(gitlab.EventTypeTagPush)} + hook.Events = sdk.GitlabEventsDefault } for _, e := range hook.Events { @@ -117,7 +117,7 @@ func (c *gitlabClient) UpdateHook(ctx context.Context, repo string, hook *sdk.VC var pushEvent, mergeRequestEvent, TagPushEvent, issueEvent, noteEvent, wikiPageEvent, pipelineEvent, jobEvent bool if len(hook.Events) == 0 { - hook.Events = []string{string(gitlab.EventTypePush), string(gitlab.EventTypeTagPush)} + hook.Events = sdk.GitlabEventsDefault } for _, e := range hook.Events { diff --git a/engine/vcs/vcs_handlers.go b/engine/vcs/vcs_handlers.go index 51e0057e18..6fd95b4044 100644 --- a/engine/vcs/vcs_handlers.go +++ b/engine/vcs/vcs_handlers.go @@ -102,102 +102,19 @@ func (s *Service) getVCSServersHooksHandler() service.Handler { res.WebhooksDisabled = cfg.Bitbucket.DisableWebHooks res.WebhooksIcon = sdk.BitbucketIcon // https://confluence.atlassian.com/bitbucketserver/event-payload-938025882.html - res.Events = []string{ - "repo:refs_changed", - "repo:modified", - "repo:forked", - "repo:comment:added", - "repo:comment:edited", - "repo:comment:deleted", - "pr:opened", - "pr:modified", - "pr:reviewer:updated", - "pr:reviewer:approved", - "pr:reviewer:unapproved", - "pr:reviewer:needs_work", - "pr:merged", - "pr:declined", - "pr:deleted", - "pr:comment:added", - "pr:comment:edited", - "pr:comment:deleted", - } + res.Events = sdk.BitbucketEvents case cfg.BitbucketCloud != nil: res.WebhooksSupported = true res.WebhooksDisabled = cfg.BitbucketCloud.DisableWebHooks res.WebhooksIcon = sdk.BitbucketIcon // https://developer.atlassian.com/bitbucket/api/2/reference/resource/hook_events/%7Bsubject_type%7D - res.Events = []string{ - "repo:push", - "pullrequest:unapproved", - "issue:comment_created", - "pullrequest:approved", - "repo:created", - "repo:deleted", - "repo:imported", - "pullrequest:comment_updated", - "issue:updated", - "project:updated", - "pullrequest:comment_created", - "repo:commit_status_updated", - "pullrequest:updated", - "issue:created", - "repo:fork", - "pullrequest:comment_deleted", - "repo:commit_status_created", - "repo:updated", - "pullrequest:rejected", - "pullrequest:fulfilled", - "pullrequest:created", - "repo:transfer", - "repo:commit_comment_created", - } + res.Events = sdk.BitbucketCloudEvents case cfg.Github != nil: res.WebhooksSupported = true res.WebhooksDisabled = cfg.Github.DisableWebHooks res.WebhooksIcon = sdk.GitHubIcon // https://developer.github.com/v3/activity/events/types/ - res.Events = []string{ - "push", - "check_run", - "check_suite", - "commit_comment", - "create", - "delete", - "deployment", - "deployment_status", - "fork", - "github_app_authorization", - "gollum", - "installation", - "installation_repositories", - "issue_comment", - "issues", - "label", - "marketplace_purchase", - "member", - "membership", - "milestone", - "organization", - "org_block", - "page_build", - "project_card", - "project_column", - "project", - "public", - "pull-request_review_comment", - "pull-request_review", - "pull_request", - "repository", - "repository_import", - "repository_vulnerability_alert", - "release", - "security_advisory", - "status", - "team", - "team_add", - "watch", - } + res.Events = sdk.GitHubEvents case cfg.Gitlab != nil: res.WebhooksSupported = true res.WebhooksDisabled = cfg.Gitlab.DisableWebHooks @@ -218,26 +135,7 @@ func (s *Service) getVCSServersHooksHandler() service.Handler { res.GerritHookDisabled = cfg.Gerrit.DisableGerritEvent res.WebhooksIcon = sdk.GerritIcon // https://git.eclipse.org/r/Documentation/cmd-stream-events.html#events - res.Events = []string{ - "patchset-created", - "assignee-changed", - "change-abandoned", - "change-deleted", - "change-merged", - "change-restored", - "comment-added", - "draft-published", - "dropped-output", - "hashtags-changed", - "project-created", - "ref-updated", - "reviewer-added", - "reviewer-deleted", - "topic-changed", - "wip-state-changed", - "private-state-changed", - "vote-deleted", - } + res.Events = sdk.GerritEvents } return service.WriteJSON(w, res, http.StatusOK) diff --git a/sdk/error.go b/sdk/error.go index 5dd549e00b..4c5d4a3f40 100644 --- a/sdk/error.go +++ b/sdk/error.go @@ -196,7 +196,8 @@ var ( ErrWorkflowAsCodeResync = Error{ID: 186, Status: http.StatusForbidden} ErrWorkflowNodeNameDuplicate = Error{ID: 187, Status: http.StatusBadRequest} ErrUnsupportedMediaType = Error{ID: 188, Status: http.StatusUnsupportedMediaType} - ErrWorkerErrorCommand = Error{ID: 189, Status: http.StatusBadRequest} + ErrNothingToPush = Error{ID: 189, Status: http.StatusBadRequest} + ErrWorkerErrorCommand = Error{ID: 190, Status: http.StatusBadRequest} ) var errorsAmericanEnglish = map[int]string{ @@ -375,7 +376,8 @@ var errorsAmericanEnglish = map[int]string{ ErrWorkflowAsCodeResync.ID: "You cannot resynchronize an as-code workflow", ErrWorkflowNodeNameDuplicate.ID: "You cannot have same name for different pipelines in your workflow", ErrUnsupportedMediaType.ID: "Request format invalid", - ErrWorkerErrorCommand.ID: "Worker command in error", + ErrNothingToPush.ID: "No diff to push", + ErrWorkerErrorCommand.ID: "Worker command in error", } var errorsFrench = map[int]string{ @@ -554,7 +556,8 @@ var errorsFrench = map[int]string{ ErrWorkflowAsCodeResync.ID: "Impossible de resynchroniser un workflow en mode as-code", ErrWorkflowNodeNameDuplicate.ID: "Vous ne pouvez pas avoir plusieurs fois le même nom de pipeline dans votre workflow", ErrUnsupportedMediaType.ID: "Le format de la requête est invalide", - ErrWorkerErrorCommand.ID: "Commande du worker en erreur:", + ErrNothingToPush.ID: "Aucune modification à pousser", + ErrWorkerErrorCommand.ID: "Commande du worker en erreur", } var errorsLanguages = []map[int]string{ diff --git a/sdk/exportentities/v2/workflow.go b/sdk/exportentities/v2/workflow.go index f20b577a91..26f0e212eb 100644 --- a/sdk/exportentities/v2/workflow.go +++ b/sdk/exportentities/v2/workflow.go @@ -5,6 +5,7 @@ import ( "fmt" "math/rand" "sort" + "strings" "time" "github.com/fsamin/go-dump" @@ -66,6 +67,32 @@ type HookEntry struct { Conditions *sdk.WorkflowNodeConditions `json:"conditions,omitempty" yaml:"conditions,omitempty" jsonschema_description:"Conditions to run this hook.\nhttps://ovh.github.io/cds/docs/concepts/workflow/run-conditions."` } +func (h HookEntry) IsDefault(model sdk.WorkflowHookModel) bool { + if h.Conditions != nil { + if h.Conditions.LuaScript != "" || len(h.Conditions.PlainConditions) > 0 { + return false + } + } + + if h.Config != nil { + for k, v := range h.Config { + dfault, has := model.DefaultConfig[k] + if has { + if dfault.Configurable && dfault.Value != v && + v != strings.Join(sdk.BitbucketCloudEventsDefault, ";") && + v != strings.Join(sdk.BitbucketEventsDefault, ";") && + v != strings.Join(sdk.GitHubEventsDefault, ";") && + v != strings.Join(sdk.GitlabEventsDefault, ";") && + v != strings.Join(sdk.GerritEventsDefault, ";") { + return false + } + } + } + } + + return true +} + type ExportOptions func(w sdk.Workflow, exportedWorkflow *Workflow) error //NewWorkflow creates a new exportable workflow @@ -301,6 +328,9 @@ func WorkflowSkipIfOnlyOneRepoWebhook(w sdk.Workflow, exportedWorkflow *Workflow for nodeName, hs := range exportedWorkflow.Hooks { if nodeName == w.WorkflowData.Node.Name && len(hs) == 1 { if hs[0].Model == sdk.RepositoryWebHookModelName { + if !hs[0].IsDefault(sdk.RepositoryWebHookModel) { + return nil + } delete(exportedWorkflow.Hooks, nodeName) if exportedWorkflow.Workflow != nil { for nodeName := range exportedWorkflow.Workflow { diff --git a/sdk/hook.go b/sdk/hook.go index acbad69ec1..cd6b8b3aa4 100644 --- a/sdk/hook.go +++ b/sdk/hook.go @@ -145,6 +145,11 @@ var ( Configurable: false, Type: HookConfigTypeString, }, + HookConfigEventFilter: { + Value: "", + Configurable: true, + Type: HookConfigTypeMultiChoice, + }, }, } diff --git a/sdk/repositories_operation.go b/sdk/repositories_operation.go index 1fe070ba7f..578a878016 100644 --- a/sdk/repositories_operation.go +++ b/sdk/repositories_operation.go @@ -15,7 +15,8 @@ type Operation struct { Setup OperationSetup `json:"setup,omitempty"` LoadFiles OperationLoadFiles `json:"load_files,omitempty"` Status OperationStatus `json:"status"` - Error string `json:"error,omitempty"` + Error *OperationError `json:"error_details,omitempty"` + DeprecatedError string `json:"error,omitempty"` RepositoryInfo *OperationRepositoryInfo `json:"repository_info,omitempty"` Date *time.Time `json:"date,omitempty"` User struct { @@ -23,6 +24,42 @@ type Operation struct { Fullname string `json:"fullname,omitempty" db:"-" cli:"-"` Email string `json:"email,omitempty" db:"-" cli:"-"` } `json:"user,omitempty"` + RequestID string `json:"request_id,omitempty"` + NbRetries int `json:"nb_retries,omitempty"` +} + +type OperationError struct { + ID int `json:"id"` + Status int `json:"status,omitempty"` + Message string `json:"message"` + StackTrace string `json:"stack_trace,omitempty"` + From string `json:"from,omitempty"` +} + +func ToOperationError(err error) *OperationError { + if err == nil { + return nil + } + sdkError := ExtractHTTPError(err, "") + return &OperationError{ + ID: sdkError.ID, + Status: sdkError.Status, + From: sdkError.From, + Message: sdkError.Message, + StackTrace: sdkError.StackTrace, + } +} + +func (opError *OperationError) ToError() error { + if opError == nil { + return nil + } + return &Error{ + ID: opError.ID, + Status: opError.Status, + Message: opError.Message, + From: opError.From, + } } // OperationSetup is the setup for an operation basically its a checkout diff --git a/sdk/vcs.go b/sdk/vcs.go index 00d310d0be..b2aa89e95f 100644 --- a/sdk/vcs.go +++ b/sdk/vcs.go @@ -15,6 +15,158 @@ const ( HeaderXAccessTokenSecret = "X-CDS-ACCESS-TOKEN-SECRET" ) +var ( + BitbucketEvents = []string{ + "repo:refs_changed", + "repo:modified", + "repo:forked", + "repo:comment:added", + "repo:comment:edited", + "repo:comment:deleted", + "pr:opened", + "pr:modified", + "pr:reviewer:updated", + "pr:reviewer:approved", + "pr:reviewer:unapproved", + "pr:reviewer:needs_work", + "pr:merged", + "pr:declined", + "pr:deleted", + "pr:comment:added", + "pr:comment:edited", + "pr:comment:deleted", + } + + BitbucketEventsDefault = []string{ + "repo:refs_changed", + } + + BitbucketCloudEvents = []string{ + "repo:push", + "pullrequest:unapproved", + "issue:comment_created", + "pullrequest:approved", + "repo:created", + "repo:deleted", + "repo:imported", + "pullrequest:comment_updated", + "issue:updated", + "project:updated", + "pullrequest:comment_created", + "repo:commit_status_updated", + "pullrequest:updated", + "issue:created", + "repo:fork", + "pullrequest:comment_deleted", + "repo:commit_status_created", + "repo:updated", + "pullrequest:rejected", + "pullrequest:fulfilled", + "pullrequest:created", + "repo:transfer", + "repo:commit_comment_created", + } + + BitbucketCloudEventsDefault = []string{ + "repo:push", + } + + GitHubEvents = []string{ + "push", + "check_run", + "check_suite", + "commit_comment", + "create", + "delete", + "deployment", + "deployment_status", + "fork", + "github_app_authorization", + "gollum", + "installation", + "installation_repositories", + "issue_comment", + "issues", + "label", + "marketplace_purchase", + "member", + "membership", + "milestone", + "organization", + "org_block", + "page_build", + "project_card", + "project_column", + "project", + "public", + "pull-request_review_comment", + "pull-request_review", + "pull_request", + "repository", + "repository_import", + "repository_vulnerability_alert", + "release", + "security_advisory", + "status", + "team", + "team_add", + "watch", + } + + GitHubEventsDefault = []string{ + "push", + } + + GitlabEventsDefault = []string{ + "Push Hook", + "Tag Push Hook", + } + + GerritEvents = []string{ + GerritEventTypePatchsetCreated, + GerritEventTypeAssignedChanged, + GerritEventTypeChangeAbandoned, + GerritEventTypeChangeDeleted, + GerritEventTypeChangeMerged, + GerritEventTypeChangeRestored, + GerritEventTypeCommentAdded, + GerritEventTypeDrafPublished, + GerritEventTypeDroppedOutput, + GerritEventTypeHashTagsChanged, + GerritEventTypeProjectCreated, + GerritEventTypeRefUpdated, + GerritEventTypeReviewerAdded, + GerritEventTypeReviewerDelete, + GerritEventTypeTopicChanged, + GerritEventTypeWIPStateChanged, + GerritEventTypePrivateStateChanged, + GerritEventTypeVoteDeleted, + } + + GerritEventTypeAssignedChanged = "assignee-changed" + GerritEventTypeChangeAbandoned = "change-abandoned" + GerritEventTypeChangeDeleted = "change-deleted" + GerritEventTypeChangeMerged = "change-merged" + GerritEventTypeChangeRestored = "change-restored" + GerritEventTypeCommentAdded = "comment-added" + GerritEventTypeDrafPublished = "draft-published" + GerritEventTypeDroppedOutput = "dropped-output" + GerritEventTypeHashTagsChanged = "hashtags-changed" + GerritEventTypeProjectCreated = "project-created" + GerritEventTypePatchsetCreated = "patchset-created" + GerritEventTypeRefUpdated = "ref-updated" + GerritEventTypeReviewerAdded = "reviewer-added" + GerritEventTypeReviewerDelete = "reviewer-deleted" + GerritEventTypeTopicChanged = "topic-changed" + GerritEventTypeWIPStateChanged = "wip-state-changed" + GerritEventTypePrivateStateChanged = "private-state-changed" + GerritEventTypeVoteDeleted = "vote-deleted" + + GerritEventsDefault = []string{ + GerritEventTypePatchsetCreated, + } +) + // BuildNumberAndHash represents BuildNumber, Commit Hash and Branch for a Pipeline Build or Node Run type BuildNumberAndHash struct { BuildNumber int64 diff --git a/sdk/workflow_hook.go b/sdk/workflow_hook.go index c99374b21b..2eb56808df 100644 --- a/sdk/workflow_hook.go +++ b/sdk/workflow_hook.go @@ -7,6 +7,7 @@ import ( "fmt" "reflect" "sort" + "strings" ) // Those are icon for hooks @@ -28,6 +29,18 @@ type NodeHook struct { Conditions WorkflowNodeConditions `json:"conditions" db:"conditions"` } +func (h NodeHook) IsRepositoryWebHook() bool { + return h.HookModelName == RepositoryWebHookModel.Name || h.HookModelID == RepositoryWebHookModel.ID +} + +func (h NodeHook) GetConfigValue(k string) (string, bool) { + v, ok := h.Config[k] + if !ok { + return "", false + } + return v.Value, true +} + func (h NodeHook) Ref() string { s := "model:" + h.HookModelName + ";" @@ -45,8 +58,53 @@ func (h NodeHook) Ref() string { return base64.StdEncoding.EncodeToString([]byte(s)) } +func (h NodeHook) ConfigValueContainsEventsDefault() bool { + eventFilterValue, has := h.GetConfigValue(HookConfigEventFilter) + if !has { + return false + } + eventFilterValues := strings.Split(eventFilterValue, ";") + + allDefaultsValue := [][]string{ + BitbucketCloudEventsDefault, + BitbucketEventsDefault, + GitHubEventsDefault, + GitlabEventsDefault, + GerritEventsDefault, + } + + var atLeastOneFound bool + for _, defaultValues := range allDefaultsValue { + var allFound = true + for _, s := range defaultValues { + if !IsInArray(s, eventFilterValues) { + allFound = false + break + } + } + if allFound { + atLeastOneFound = true + break + } + } + + return atLeastOneFound +} + //Equals checks functional equality between two hooks func (h NodeHook) Equals(h1 NodeHook) bool { + var areRepoWebHook = (h1.HookModelID == h.HookModelID) && (h.HookModelID == RepositoryWebHookModel.ID) + var isEventFilter = func(s string) bool { return s == HookConfigEventFilter } + var isEmptyEventFilter = func(s string) bool { return s == "" } + var isDefaultEventFilter = func(v string) bool { + return v == "" || + v == strings.Join(BitbucketCloudEventsDefault, ";") || + v == strings.Join(BitbucketEventsDefault, ";") || + v == strings.Join(GitHubEventsDefault, ";") || + v == strings.Join(GitlabEventsDefault, ";") || + v == strings.Join(GerritEventsDefault, ";") + } + if h.UUID != h1.UUID { return false } @@ -58,7 +116,11 @@ func (h NodeHook) Equals(h1 NodeHook) bool { if !has { return false } - if cfg.Value != cfg1.Value { + if areRepoWebHook && isEventFilter(k) { + if isEmptyEventFilter(cfg.Value) && !isDefaultEventFilter(cfg1.Value) { + return false + } + } else if cfg.Value != cfg1.Value { return false } } @@ -67,7 +129,11 @@ func (h NodeHook) Equals(h1 NodeHook) bool { if !has { return false } - if cfg.Value != cfg1.Value { + if areRepoWebHook && isEventFilter(k) { + if isEmptyEventFilter(cfg1.Value) && !isDefaultEventFilter(cfg.Value) { + return false + } + } else if cfg.Value != cfg1.Value { return false } } @@ -106,6 +172,35 @@ func (w *WorkflowNodeHookConfig) Scan(src interface{}) error { return WrapError(json.Unmarshal(source, w), "cannot unmarshal WorkflowNodeHookConfig") } +func (w WorkflowNodeHookConfig) Equals(o WorkflowNodeHookConfig) bool { + for k, v := range w { + ov, has := o[k] + if !has { + return false + } + if v.Value != ov.Value { + return false + } + } + return true +} + +func (w WorkflowNodeHookConfig) MergeWith(cfg WorkflowNodeHookConfig) { + for k, v := range cfg { + w[k] = v + } +} + +func (w WorkflowNodeHookConfig) Filter(f func(k string, v WorkflowNodeHookConfigValue) bool) WorkflowNodeHookConfig { + var newCfg = WorkflowNodeHookConfig{} + for k, v := range w { + if f(k, v) { + newCfg[k] = v + } + } + return newCfg.Clone() +} + // GetBuiltinHookModelByName retrieve the hook model func GetBuiltinHookModelByName(name string) *WorkflowHookModel { for _, m := range BuiltinHookModels { diff --git a/sdk/workflow_hook_test.go b/sdk/workflow_hook_test.go new file mode 100644 index 0000000000..6643f8f77b --- /dev/null +++ b/sdk/workflow_hook_test.go @@ -0,0 +1,62 @@ +package sdk + +import ( + "fmt" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNodeHook_ConfigValueContainsEventsDefault(t *testing.T) { + var tests = []struct { + given []string + expected bool + }{ + { + given: BitbucketCloudEventsDefault, + expected: true, + }, + { + given: BitbucketCloudEventsDefault, + expected: true, + }, + { + given: GitHubEventsDefault, + expected: true, + }, + { + given: GitlabEventsDefault, + expected: true, + }, + { + given: GerritEventsDefault, + expected: true, + }, + { + given: GitHubEvents, + expected: true, + }, + { + given: []string{"foo", "bar"}, + expected: false, + }, + { + given: []string{"push", "bar"}, // push is the default events for github + expected: true, + }, + } + + for i, tt := range tests { + t.Run(fmt.Sprintf("test #%d with %+v", i, tt.given), func(t *testing.T) { + h := &NodeHook{ + Config: WorkflowNodeHookConfig{ + HookConfigEventFilter: WorkflowNodeHookConfigValue{ + Value: strings.Join(tt.given, ";"), + }, + }, + } + require.Equal(t, tt.expected, h.ConfigValueContainsEventsDefault()) + }) + } +} diff --git a/ui/src/app/shared/ascode/save-form/ascode.save-form.html b/ui/src/app/shared/ascode/save-form/ascode.save-form.html index e2531398d4..4b94a5959a 100644 --- a/ui/src/app/shared/ascode/save-form/ascode.save-form.html +++ b/ui/src/app/shared/ascode/save-form/ascode.save-form.html @@ -22,7 +22,8 @@
- {{operation.error}} + {{operation.error_details.message}} - + {{operation.error_details.from}}
{{ 'workflow_as_code_pr_success' | translate }} diff --git a/ui/src/app/shared/ascode/save-modal/ascode.save-modal.component.ts b/ui/src/app/shared/ascode/save-modal/ascode.save-modal.component.ts index 6cb69d4b94..90ff98d66a 100644 --- a/ui/src/app/shared/ascode/save-modal/ascode.save-modal.component.ts +++ b/ui/src/app/shared/ascode/save-modal/ascode.save-modal.component.ts @@ -78,7 +78,7 @@ export class AsCodeSaveModalComponent { this.loading = true; this._cd.markForCheck(); this._workflowService.updateAsCode(this.project.key, this.name, this.parameters.branch_name, - this.parameters.commit_message, this.dataToSave).subscribe(o => { + this.parameters.commit_message, this.dataToSave as Workflow).subscribe(o => { this.asCodeOperation = o; this.startPollingOperation(this.name); });