diff --git a/cmd/tfc_lock.go b/cmd/tfc_lock.go index bdfe6c1..526f896 100644 --- a/cmd/tfc_lock.go +++ b/cmd/tfc_lock.go @@ -1,6 +1,7 @@ package cmd import ( + "context" "fmt" "os" @@ -14,7 +15,8 @@ var tfcLockCmd = &cobra.Command{ Short: "Lock a Terraform workspace.", Long: ``, Run: func(cmd *cobra.Command, args []string) { - tfc_utils.LockUnlockWorkspace(tfcToken, tfcWorkspace, true, "Locked from MR") + ctx := context.Background() + tfc_utils.LockUnlockWorkspace(ctx, tfcToken, tfcWorkspace, true, "Locked from MR") }, PreRunE: func(cmd *cobra.Command, args []string) error { if tfcWorkspace == "" { diff --git a/cmd/tfc_unlock.go b/cmd/tfc_unlock.go index f99a318..b9d9e30 100644 --- a/cmd/tfc_unlock.go +++ b/cmd/tfc_unlock.go @@ -1,6 +1,7 @@ package cmd import ( + "context" "fmt" "os" @@ -14,7 +15,8 @@ var tfcUnlockCmd = &cobra.Command{ Short: "Unlock a Terraform workspace.", Long: ``, Run: func(cmd *cobra.Command, args []string) { - tfc_utils.LockUnlockWorkspace(tfcToken, tfcWorkspace, false, "") + ctx := context.Background() + tfc_utils.LockUnlockWorkspace(ctx, tfcToken, tfcWorkspace, false, "") }, PreRunE: func(cmd *cobra.Command, args []string) error { if tfcWorkspace == "" { diff --git a/pkg/gitlab_hooks/comment_actions.go b/pkg/gitlab_hooks/comment_actions.go index 0b07082..1748f56 100644 --- a/pkg/gitlab_hooks/comment_actions.go +++ b/pkg/gitlab_hooks/comment_actions.go @@ -11,6 +11,7 @@ import ( "github.com/zapier/tfbuddy/pkg/tfc_trigger" "github.com/zapier/tfbuddy/pkg/vcs" "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" ) // processNoteEvent processes GitLab Webhooks for Note events @@ -28,7 +29,7 @@ func (w *GitlabEventWorker) processNoteEvent(ctx context.Context, event vcs.MRCo opts, err := comment_actions.ParseCommentCommand(event.GetAttributes().GetNote()) if err != nil { if err == comment_actions.ErrOtherTFTool { - w.postMessageToMergeRequest(event, "Use tfc to interact with tfbuddy") + w.postMessageToMergeRequest(ctx, event, "Use tfc to interact with tfbuddy") } if err == comment_actions.ErrNotTFCCommand || err == comment_actions.ErrOtherTFTool { gitlabWebHookIgnored.WithLabelValues("comment", "not-tfc-command", proj).Inc() @@ -56,17 +57,27 @@ func (w *GitlabEventWorker) processNoteEvent(ctx context.Context, event vcs.MRCo trigger.SetMergeRequestDiscussionID(event.GetAttributes().GetDiscussionID()) } + span.SetAttributes( + attribute.String("agent", opts.Args.Agent), + attribute.String("command", opts.Args.Command), + attribute.String("branch", opts.TriggerOpts.Branch), + attribute.String("commit_sha", opts.TriggerOpts.CommitSHA), + attribute.String("project_name", opts.TriggerOpts.ProjectNameWithNamespace), + attribute.Int("merge_request_iid", opts.TriggerOpts.MergeRequestIID), + attribute.String("vcs_provider", opts.TriggerOpts.VcsProvider), + ) + // TODO: support additional commands and arguments (e.g. destroy, refresh, lock, unlock) // TODO: this should be refactored and be agnostic to the VCS type switch opts.Args.Command { case "apply": log.Info().Msg("Got TFC apply command") if !w.checkApproval(ctx, event) { - w.postMessageToMergeRequest(event, ":no_entry: Apply failed. Merge Request requires approval.") + w.postMessageToMergeRequest(ctx, event, ":no_entry: Apply failed. Merge Request requires approval.") return proj, nil } if !w.checkForMergeConflicts(ctx, event) { - w.postMessageToMergeRequest(event, ":no_entry: Apply failed. Merge Request has conflicts that need to be resolved.") + w.postMessageToMergeRequest(ctx, event, ":no_entry: Apply failed. Merge Request has conflicts that need to be resolved.") return proj, nil } case "lock": @@ -82,13 +93,13 @@ func (w *GitlabEventWorker) processNoteEvent(ctx context.Context, event vcs.MRCo if tfError == nil && executedWorkspaces != nil { if len(executedWorkspaces.Errored) > 0 { for _, failedWS := range executedWorkspaces.Errored { - w.postMessageToMergeRequest(event, fmt.Sprintf(":no_entry: %s could not be run because: %s", failedWS.Name, failedWS.Error)) + w.postMessageToMergeRequest(ctx, event, fmt.Sprintf(":no_entry: %s could not be run because: %s", failedWS.Name, failedWS.Error)) } return proj, nil } } if tfError != nil { - w.postMessageToMergeRequest(event, fmt.Sprintf(":no_entry: could not be run because: %s", tfError.Error())) + w.postMessageToMergeRequest(ctx, event, fmt.Sprintf(":no_entry: could not be run because: %s", tfError.Error())) } return proj, tfError @@ -100,9 +111,9 @@ func (w *GitlabEventWorker) checkApproval(ctx context.Context, event vcs.MRComme mrIID := event.GetMR().GetInternalID() proj := event.GetProject().GetPathWithNamespace() - approvals, err := w.gl.GetMergeRequestApprovals(mrIID, proj) + approvals, err := w.gl.GetMergeRequestApprovals(ctx, mrIID, proj) if err != nil { - w.postErrorToMergeRequest(event, fmt.Errorf("could not get MergeRequest from GitlabAPI: %v", err)) + w.postErrorToMergeRequest(ctx, event, fmt.Errorf("could not get MergeRequest from GitlabAPI: %v", err)) return false } @@ -117,15 +128,19 @@ func (w *GitlabEventWorker) checkForMergeConflicts(ctx context.Context, event vc proj := event.GetProject().GetPathWithNamespace() mr, err := w.gl.GetMergeRequest(ctx, mrIID, proj) if err != nil { - w.postErrorToMergeRequest(event, fmt.Errorf("could not get MergeRequest from GitlabAPI: %v", err)) + w.postErrorToMergeRequest(ctx, event, fmt.Errorf("could not get MergeRequest from GitlabAPI: %v", err)) return false } // fail if the MR has conflicts only. return !mr.HasConflicts() } -func (w *GitlabEventWorker) postMessageToMergeRequest(event vcs.MRCommentEvent, msg string) { +func (w *GitlabEventWorker) postMessageToMergeRequest(ctx context.Context, event vcs.MRCommentEvent, msg string) { + ctx, span := otel.Tracer("GitlabHooks").Start(context.Background(), "postMessageToMergeRequest") + defer span.End() + if err := w.gl.CreateMergeRequestComment( + ctx, event.GetMR().GetInternalID(), event.GetProject().GetPathWithNamespace(), msg, @@ -134,6 +149,10 @@ func (w *GitlabEventWorker) postMessageToMergeRequest(event vcs.MRCommentEvent, } } -func (w *GitlabEventWorker) postErrorToMergeRequest(event vcs.MRCommentEvent, err error) { - w.postMessageToMergeRequest(event, fmt.Sprintf(":fire:
Error: %v", err)) +func (w *GitlabEventWorker) postErrorToMergeRequest(ctx context.Context, event vcs.MRCommentEvent, err error) { + ctx, span := otel.Tracer("GitlabHooks").Start(context.Background(), "postErrorToMergeRequest") + defer span.End() + span.RecordError(err) + + w.postMessageToMergeRequest(ctx, event, fmt.Sprintf(":fire:
Error: %v", err)) } diff --git a/pkg/gitlab_hooks/comment_actions_test.go b/pkg/gitlab_hooks/comment_actions_test.go index 022d55b..9ae4cbc 100644 --- a/pkg/gitlab_hooks/comment_actions_test.go +++ b/pkg/gitlab_hooks/comment_actions_test.go @@ -119,7 +119,7 @@ func TestProcessNoteEventPlanError(t *testing.T) { defer mockCtrl.Finish() mockGitClient := mocks.NewMockGitClient(mockCtrl) - mockGitClient.EXPECT().CreateMergeRequestComment(101, "zapier/service-tf-buddy", ":no_entry: could not be run because: something went wrong") + mockGitClient.EXPECT().CreateMergeRequestComment(gomock.Any(), 101, "zapier/service-tf-buddy", ":no_entry: could not be run because: something went wrong") mockApiClient := mocks.NewMockApiClient(mockCtrl) mockStreamClient := mocks.NewMockStreamClient(mockCtrl) mockProject := mocks.NewMockProject(mockCtrl) @@ -257,7 +257,7 @@ func TestProcessNoteEventPlanFailedWorkspace(t *testing.T) { defer mockCtrl.Finish() testSuite := mocks.CreateTestSuite(mockCtrl, mocks.TestOverrides{}, t) - testSuite.MockGitClient.EXPECT().CreateMergeRequestComment(101, testSuite.MetaData.ProjectNameNS, ":no_entry: service-tf-buddy could not be run because: could not fetch upstream").Return(nil) + testSuite.MockGitClient.EXPECT().CreateMergeRequestComment(gomock.Any(), 101, testSuite.MetaData.ProjectNameNS, ":no_entry: service-tf-buddy could not be run because: could not fetch upstream").Return(nil) mockLastCommit := mocks.NewMockCommit(mockCtrl) @@ -314,8 +314,8 @@ func TestProcessNoteEventPlanFailedMultipleWorkspaces(t *testing.T) { testSuite := mocks.CreateTestSuite(mockCtrl, mocks.TestOverrides{}, t) gomock.InOrder( - testSuite.MockGitClient.EXPECT().CreateMergeRequestComment(101, testSuite.MetaData.ProjectNameNS, ":no_entry: service-tf-buddy could not be run because: could not fetch upstream").Return(nil), - testSuite.MockGitClient.EXPECT().CreateMergeRequestComment(101, testSuite.MetaData.ProjectNameNS, ":no_entry: service-tf-buddy-staging could not be run because: workspace has been modified on target branch").Return(nil), + testSuite.MockGitClient.EXPECT().CreateMergeRequestComment(gomock.Any(), 101, testSuite.MetaData.ProjectNameNS, ":no_entry: service-tf-buddy could not be run because: could not fetch upstream").Return(nil), + testSuite.MockGitClient.EXPECT().CreateMergeRequestComment(gomock.Any(), 101, testSuite.MetaData.ProjectNameNS, ":no_entry: service-tf-buddy-staging could not be run because: workspace has been modified on target branch").Return(nil), ) mockLastCommit := mocks.NewMockCommit(mockCtrl) mockLastCommit.EXPECT().GetSHA().Return("abvc12345") diff --git a/pkg/mocks/helpers.go b/pkg/mocks/helpers.go index 8e1ba9d..6757cf8 100644 --- a/pkg/mocks/helpers.go +++ b/pkg/mocks/helpers.go @@ -165,10 +165,10 @@ func (ts *TestSuite) InitTestSuite() { ts.MockGitDisc.EXPECT().GetMRNotes().Return([]vcs.MRNote{ts.MockMRNote}).AnyTimes() ts.MockGitClient.EXPECT().GetMergeRequest(gomock.Any(), ts.MetaData.MRIID, ts.MetaData.ProjectNameNS).Return(ts.MockGitMR, nil).AnyTimes() - ts.MockGitClient.EXPECT().GetMergeRequestModifiedFiles(ts.MetaData.MRIID, ts.MetaData.ProjectNameNS).Return([]string{"main.tf"}, nil).AnyTimes() - ts.MockGitClient.EXPECT().GetRepoFile(ts.MetaData.ProjectNameNS, ".tfbuddy.yaml", ts.MetaData.SourceBranch).Return(ts.MetaData.TFBuddyConfig, nil).AnyTimes() - ts.MockGitClient.EXPECT().CloneMergeRequest(ts.MetaData.ProjectNameNS, gomock.Any(), gomock.Any()).Return(ts.MockGitRepo, nil).AnyTimes() - ts.MockGitClient.EXPECT().CreateMergeRequestDiscussion(ts.MetaData.MRIID, ts.MetaData.ProjectNameNS, &RegexMatcher{regex: regexp.MustCompile("Starting TFC apply for Workspace: `([A-z\\-]){1,}/([A-z\\-]){1,}`.")}).Return(ts.MockGitDisc, nil).AnyTimes() + ts.MockGitClient.EXPECT().GetMergeRequestModifiedFiles(gomock.Any(), ts.MetaData.MRIID, ts.MetaData.ProjectNameNS).Return([]string{"main.tf"}, nil).AnyTimes() + ts.MockGitClient.EXPECT().GetRepoFile(gomock.Any(), ts.MetaData.ProjectNameNS, ".tfbuddy.yaml", ts.MetaData.SourceBranch).Return(ts.MetaData.TFBuddyConfig, nil).AnyTimes() + ts.MockGitClient.EXPECT().CloneMergeRequest(gomock.Any(), ts.MetaData.ProjectNameNS, gomock.Any(), gomock.Any()).Return(ts.MockGitRepo, nil).AnyTimes() + ts.MockGitClient.EXPECT().CreateMergeRequestDiscussion(gomock.Any(), ts.MetaData.MRIID, ts.MetaData.ProjectNameNS, &RegexMatcher{regex: regexp.MustCompile("Starting TFC apply for Workspace: `([A-z\\-]){1,}/([A-z\\-]){1,}`.")}).Return(ts.MockGitDisc, nil).AnyTimes() ts.MockApiClient.EXPECT().GetWorkspaceByName(gomock.Any(), gomock.Any(), gomock.Any()).Return(&tfe.Workspace{ID: "service-tfbuddy"}, nil).AnyTimes() ts.MockApiClient.EXPECT().GetTagsByQuery(gomock.Any(), gomock.Any(), "tfbuddylock").AnyTimes() diff --git a/pkg/mocks/mock_runstream.go b/pkg/mocks/mock_runstream.go index 8584ec8..bba93a9 100644 --- a/pkg/mocks/mock_runstream.go +++ b/pkg/mocks/mock_runstream.go @@ -160,6 +160,20 @@ func (m *MockRunEvent) EXPECT() *MockRunEventMockRecorder { return m.recorder } +// GetContext mocks base method. +func (m *MockRunEvent) GetContext() context.Context { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetContext") + ret0, _ := ret[0].(context.Context) + return ret0 +} + +// GetContext indicates an expected call of GetContext. +func (mr *MockRunEventMockRecorder) GetContext() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetContext", reflect.TypeOf((*MockRunEvent)(nil).GetContext)) +} + // GetMetadata mocks base method. func (m *MockRunEvent) GetMetadata() runstream.RunMetadata { m.ctrl.T.Helper() @@ -202,6 +216,30 @@ func (mr *MockRunEventMockRecorder) GetRunID() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRunID", reflect.TypeOf((*MockRunEvent)(nil).GetRunID)) } +// SetCarrier mocks base method. +func (m *MockRunEvent) SetCarrier(arg0 map[string]string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetCarrier", arg0) +} + +// SetCarrier indicates an expected call of SetCarrier. +func (mr *MockRunEventMockRecorder) SetCarrier(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetCarrier", reflect.TypeOf((*MockRunEvent)(nil).SetCarrier), arg0) +} + +// SetContext mocks base method. +func (m *MockRunEvent) SetContext(arg0 context.Context) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetContext", arg0) +} + +// SetContext indicates an expected call of SetContext. +func (mr *MockRunEventMockRecorder) SetContext(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetContext", reflect.TypeOf((*MockRunEvent)(nil).SetContext), arg0) +} + // SetMetadata mocks base method. func (m *MockRunEvent) SetMetadata(arg0 runstream.RunMetadata) { m.ctrl.T.Helper() @@ -471,31 +509,43 @@ func (mr *MockRunPollingTaskMockRecorder) GetRunMetaData() *gomock.Call { } // Reschedule mocks base method. -func (m *MockRunPollingTask) Reschedule() error { +func (m *MockRunPollingTask) Reschedule(ctx context.Context) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Reschedule") + ret := m.ctrl.Call(m, "Reschedule", ctx) ret0, _ := ret[0].(error) return ret0 } // Reschedule indicates an expected call of Reschedule. -func (mr *MockRunPollingTaskMockRecorder) Reschedule() *gomock.Call { +func (mr *MockRunPollingTaskMockRecorder) Reschedule(ctx interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Reschedule", reflect.TypeOf((*MockRunPollingTask)(nil).Reschedule)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Reschedule", reflect.TypeOf((*MockRunPollingTask)(nil).Reschedule), ctx) } // Schedule mocks base method. -func (m *MockRunPollingTask) Schedule() error { +func (m *MockRunPollingTask) Schedule(ctx context.Context) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Schedule") + ret := m.ctrl.Call(m, "Schedule", ctx) ret0, _ := ret[0].(error) return ret0 } // Schedule indicates an expected call of Schedule. -func (mr *MockRunPollingTaskMockRecorder) Schedule() *gomock.Call { +func (mr *MockRunPollingTaskMockRecorder) Schedule(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Schedule", reflect.TypeOf((*MockRunPollingTask)(nil).Schedule), ctx) +} + +// SetCarrier mocks base method. +func (m *MockRunPollingTask) SetCarrier(arg0 map[string]string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetCarrier", arg0) +} + +// SetCarrier indicates an expected call of SetCarrier. +func (mr *MockRunPollingTaskMockRecorder) SetCarrier(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Schedule", reflect.TypeOf((*MockRunPollingTask)(nil).Schedule)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetCarrier", reflect.TypeOf((*MockRunPollingTask)(nil).SetCarrier), arg0) } // SetLastStatus mocks base method. diff --git a/pkg/mocks/mock_tfc_api.go b/pkg/mocks/mock_tfc_api.go index 294d361..87f5791 100644 --- a/pkg/mocks/mock_tfc_api.go +++ b/pkg/mocks/mock_tfc_api.go @@ -51,18 +51,18 @@ func (mr *MockApiClientMockRecorder) AddTags(ctx, workspace, prefix, value inter } // CreateRunFromSource mocks base method. -func (m *MockApiClient) CreateRunFromSource(opts *tfc_api.ApiRunOptions) (*tfe.Run, error) { +func (m *MockApiClient) CreateRunFromSource(ctx context.Context, opts *tfc_api.ApiRunOptions) (*tfe.Run, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateRunFromSource", opts) + ret := m.ctrl.Call(m, "CreateRunFromSource", ctx, opts) ret0, _ := ret[0].(*tfe.Run) ret1, _ := ret[1].(error) return ret0, ret1 } // CreateRunFromSource indicates an expected call of CreateRunFromSource. -func (mr *MockApiClientMockRecorder) CreateRunFromSource(opts interface{}) *gomock.Call { +func (mr *MockApiClientMockRecorder) CreateRunFromSource(ctx, opts interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateRunFromSource", reflect.TypeOf((*MockApiClient)(nil).CreateRunFromSource), opts) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateRunFromSource", reflect.TypeOf((*MockApiClient)(nil).CreateRunFromSource), ctx, opts) } // GetPlanOutput mocks base method. @@ -81,18 +81,18 @@ func (mr *MockApiClientMockRecorder) GetPlanOutput(id interface{}) *gomock.Call } // GetRun mocks base method. -func (m *MockApiClient) GetRun(id string) (*tfe.Run, error) { +func (m *MockApiClient) GetRun(ctx context.Context, id string) (*tfe.Run, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetRun", id) + ret := m.ctrl.Call(m, "GetRun", ctx, id) ret0, _ := ret[0].(*tfe.Run) ret1, _ := ret[1].(error) return ret0, ret1 } // GetRun indicates an expected call of GetRun. -func (mr *MockApiClientMockRecorder) GetRun(id interface{}) *gomock.Call { +func (mr *MockApiClientMockRecorder) GetRun(ctx, id interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRun", reflect.TypeOf((*MockApiClient)(nil).GetRun), id) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRun", reflect.TypeOf((*MockApiClient)(nil).GetRun), ctx, id) } // GetTagsByQuery mocks base method. diff --git a/pkg/mocks/mock_vcs.go b/pkg/mocks/mock_vcs.go index b039718..6d87034 100644 --- a/pkg/mocks/mock_vcs.go +++ b/pkg/mocks/mock_vcs.go @@ -36,62 +36,62 @@ func (m *MockGitClient) EXPECT() *MockGitClientMockRecorder { } // AddMergeRequestDiscussionReply mocks base method. -func (m *MockGitClient) AddMergeRequestDiscussionReply(mrIID int, project, discussionID, comment string) (vcs.MRNote, error) { +func (m *MockGitClient) AddMergeRequestDiscussionReply(ctx context.Context, mrIID int, project, discussionID, comment string) (vcs.MRNote, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AddMergeRequestDiscussionReply", mrIID, project, discussionID, comment) + ret := m.ctrl.Call(m, "AddMergeRequestDiscussionReply", ctx, mrIID, project, discussionID, comment) ret0, _ := ret[0].(vcs.MRNote) ret1, _ := ret[1].(error) return ret0, ret1 } // AddMergeRequestDiscussionReply indicates an expected call of AddMergeRequestDiscussionReply. -func (mr *MockGitClientMockRecorder) AddMergeRequestDiscussionReply(mrIID, project, discussionID, comment interface{}) *gomock.Call { +func (mr *MockGitClientMockRecorder) AddMergeRequestDiscussionReply(ctx, mrIID, project, discussionID, comment interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddMergeRequestDiscussionReply", reflect.TypeOf((*MockGitClient)(nil).AddMergeRequestDiscussionReply), mrIID, project, discussionID, comment) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddMergeRequestDiscussionReply", reflect.TypeOf((*MockGitClient)(nil).AddMergeRequestDiscussionReply), ctx, mrIID, project, discussionID, comment) } // CloneMergeRequest mocks base method. -func (m *MockGitClient) CloneMergeRequest(arg0 string, arg1 vcs.MR, arg2 string) (vcs.GitRepo, error) { +func (m *MockGitClient) CloneMergeRequest(arg0 context.Context, arg1 string, arg2 vcs.MR, arg3 string) (vcs.GitRepo, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CloneMergeRequest", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "CloneMergeRequest", arg0, arg1, arg2, arg3) ret0, _ := ret[0].(vcs.GitRepo) ret1, _ := ret[1].(error) return ret0, ret1 } // CloneMergeRequest indicates an expected call of CloneMergeRequest. -func (mr *MockGitClientMockRecorder) CloneMergeRequest(arg0, arg1, arg2 interface{}) *gomock.Call { +func (mr *MockGitClientMockRecorder) CloneMergeRequest(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CloneMergeRequest", reflect.TypeOf((*MockGitClient)(nil).CloneMergeRequest), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CloneMergeRequest", reflect.TypeOf((*MockGitClient)(nil).CloneMergeRequest), arg0, arg1, arg2, arg3) } // CreateMergeRequestComment mocks base method. -func (m *MockGitClient) CreateMergeRequestComment(id int, fullPath, comment string) error { +func (m *MockGitClient) CreateMergeRequestComment(ctx context.Context, id int, fullPath, comment string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateMergeRequestComment", id, fullPath, comment) + ret := m.ctrl.Call(m, "CreateMergeRequestComment", ctx, id, fullPath, comment) ret0, _ := ret[0].(error) return ret0 } // CreateMergeRequestComment indicates an expected call of CreateMergeRequestComment. -func (mr *MockGitClientMockRecorder) CreateMergeRequestComment(id, fullPath, comment interface{}) *gomock.Call { +func (mr *MockGitClientMockRecorder) CreateMergeRequestComment(ctx, id, fullPath, comment interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateMergeRequestComment", reflect.TypeOf((*MockGitClient)(nil).CreateMergeRequestComment), id, fullPath, comment) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateMergeRequestComment", reflect.TypeOf((*MockGitClient)(nil).CreateMergeRequestComment), ctx, id, fullPath, comment) } // CreateMergeRequestDiscussion mocks base method. -func (m *MockGitClient) CreateMergeRequestDiscussion(mrID int, fullPath, comment string) (vcs.MRDiscussionNotes, error) { +func (m *MockGitClient) CreateMergeRequestDiscussion(ctx context.Context, mrID int, fullPath, comment string) (vcs.MRDiscussionNotes, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateMergeRequestDiscussion", mrID, fullPath, comment) + ret := m.ctrl.Call(m, "CreateMergeRequestDiscussion", ctx, mrID, fullPath, comment) ret0, _ := ret[0].(vcs.MRDiscussionNotes) ret1, _ := ret[1].(error) return ret0, ret1 } // CreateMergeRequestDiscussion indicates an expected call of CreateMergeRequestDiscussion. -func (mr *MockGitClientMockRecorder) CreateMergeRequestDiscussion(mrID, fullPath, comment interface{}) *gomock.Call { +func (mr *MockGitClientMockRecorder) CreateMergeRequestDiscussion(ctx, mrID, fullPath, comment interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateMergeRequestDiscussion", reflect.TypeOf((*MockGitClient)(nil).CreateMergeRequestDiscussion), mrID, fullPath, comment) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateMergeRequestDiscussion", reflect.TypeOf((*MockGitClient)(nil).CreateMergeRequestDiscussion), ctx, mrID, fullPath, comment) } // GetMergeRequest mocks base method. @@ -110,107 +110,107 @@ func (mr *MockGitClientMockRecorder) GetMergeRequest(arg0, arg1, arg2 interface{ } // GetMergeRequestApprovals mocks base method. -func (m *MockGitClient) GetMergeRequestApprovals(id int, project string) (vcs.MRApproved, error) { +func (m *MockGitClient) GetMergeRequestApprovals(ctx context.Context, id int, project string) (vcs.MRApproved, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetMergeRequestApprovals", id, project) + ret := m.ctrl.Call(m, "GetMergeRequestApprovals", ctx, id, project) ret0, _ := ret[0].(vcs.MRApproved) ret1, _ := ret[1].(error) return ret0, ret1 } // GetMergeRequestApprovals indicates an expected call of GetMergeRequestApprovals. -func (mr *MockGitClientMockRecorder) GetMergeRequestApprovals(id, project interface{}) *gomock.Call { +func (mr *MockGitClientMockRecorder) GetMergeRequestApprovals(ctx, id, project interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMergeRequestApprovals", reflect.TypeOf((*MockGitClient)(nil).GetMergeRequestApprovals), id, project) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMergeRequestApprovals", reflect.TypeOf((*MockGitClient)(nil).GetMergeRequestApprovals), ctx, id, project) } // GetMergeRequestModifiedFiles mocks base method. -func (m *MockGitClient) GetMergeRequestModifiedFiles(mrIID int, projectID string) ([]string, error) { +func (m *MockGitClient) GetMergeRequestModifiedFiles(ctx context.Context, mrIID int, projectID string) ([]string, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetMergeRequestModifiedFiles", mrIID, projectID) + ret := m.ctrl.Call(m, "GetMergeRequestModifiedFiles", ctx, mrIID, projectID) ret0, _ := ret[0].([]string) ret1, _ := ret[1].(error) return ret0, ret1 } // GetMergeRequestModifiedFiles indicates an expected call of GetMergeRequestModifiedFiles. -func (mr *MockGitClientMockRecorder) GetMergeRequestModifiedFiles(mrIID, projectID interface{}) *gomock.Call { +func (mr *MockGitClientMockRecorder) GetMergeRequestModifiedFiles(ctx, mrIID, projectID interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMergeRequestModifiedFiles", reflect.TypeOf((*MockGitClient)(nil).GetMergeRequestModifiedFiles), mrIID, projectID) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMergeRequestModifiedFiles", reflect.TypeOf((*MockGitClient)(nil).GetMergeRequestModifiedFiles), ctx, mrIID, projectID) } // GetPipelinesForCommit mocks base method. -func (m *MockGitClient) GetPipelinesForCommit(projectWithNS, commitSHA string) ([]vcs.ProjectPipeline, error) { +func (m *MockGitClient) GetPipelinesForCommit(ctx context.Context, projectWithNS, commitSHA string) ([]vcs.ProjectPipeline, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetPipelinesForCommit", projectWithNS, commitSHA) + ret := m.ctrl.Call(m, "GetPipelinesForCommit", ctx, projectWithNS, commitSHA) ret0, _ := ret[0].([]vcs.ProjectPipeline) ret1, _ := ret[1].(error) return ret0, ret1 } // GetPipelinesForCommit indicates an expected call of GetPipelinesForCommit. -func (mr *MockGitClientMockRecorder) GetPipelinesForCommit(projectWithNS, commitSHA interface{}) *gomock.Call { +func (mr *MockGitClientMockRecorder) GetPipelinesForCommit(ctx, projectWithNS, commitSHA interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPipelinesForCommit", reflect.TypeOf((*MockGitClient)(nil).GetPipelinesForCommit), projectWithNS, commitSHA) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPipelinesForCommit", reflect.TypeOf((*MockGitClient)(nil).GetPipelinesForCommit), ctx, projectWithNS, commitSHA) } // GetRepoFile mocks base method. -func (m *MockGitClient) GetRepoFile(arg0, arg1, arg2 string) ([]byte, error) { +func (m *MockGitClient) GetRepoFile(arg0 context.Context, arg1, arg2, arg3 string) ([]byte, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetRepoFile", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "GetRepoFile", arg0, arg1, arg2, arg3) ret0, _ := ret[0].([]byte) ret1, _ := ret[1].(error) return ret0, ret1 } // GetRepoFile indicates an expected call of GetRepoFile. -func (mr *MockGitClientMockRecorder) GetRepoFile(arg0, arg1, arg2 interface{}) *gomock.Call { +func (mr *MockGitClientMockRecorder) GetRepoFile(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRepoFile", reflect.TypeOf((*MockGitClient)(nil).GetRepoFile), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRepoFile", reflect.TypeOf((*MockGitClient)(nil).GetRepoFile), arg0, arg1, arg2, arg3) } // ResolveMergeRequestDiscussion mocks base method. -func (m *MockGitClient) ResolveMergeRequestDiscussion(arg0 string, arg1 int, arg2 string) error { +func (m *MockGitClient) ResolveMergeRequestDiscussion(arg0 context.Context, arg1 string, arg2 int, arg3 string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ResolveMergeRequestDiscussion", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "ResolveMergeRequestDiscussion", arg0, arg1, arg2, arg3) ret0, _ := ret[0].(error) return ret0 } // ResolveMergeRequestDiscussion indicates an expected call of ResolveMergeRequestDiscussion. -func (mr *MockGitClientMockRecorder) ResolveMergeRequestDiscussion(arg0, arg1, arg2 interface{}) *gomock.Call { +func (mr *MockGitClientMockRecorder) ResolveMergeRequestDiscussion(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResolveMergeRequestDiscussion", reflect.TypeOf((*MockGitClient)(nil).ResolveMergeRequestDiscussion), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResolveMergeRequestDiscussion", reflect.TypeOf((*MockGitClient)(nil).ResolveMergeRequestDiscussion), arg0, arg1, arg2, arg3) } // SetCommitStatus mocks base method. -func (m *MockGitClient) SetCommitStatus(projectWithNS, commitSHA string, status vcs.CommitStatusOptions) (vcs.CommitStatus, error) { +func (m *MockGitClient) SetCommitStatus(ctx context.Context, projectWithNS, commitSHA string, status vcs.CommitStatusOptions) (vcs.CommitStatus, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SetCommitStatus", projectWithNS, commitSHA, status) + ret := m.ctrl.Call(m, "SetCommitStatus", ctx, projectWithNS, commitSHA, status) ret0, _ := ret[0].(vcs.CommitStatus) ret1, _ := ret[1].(error) return ret0, ret1 } // SetCommitStatus indicates an expected call of SetCommitStatus. -func (mr *MockGitClientMockRecorder) SetCommitStatus(projectWithNS, commitSHA, status interface{}) *gomock.Call { +func (mr *MockGitClientMockRecorder) SetCommitStatus(ctx, projectWithNS, commitSHA, status interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetCommitStatus", reflect.TypeOf((*MockGitClient)(nil).SetCommitStatus), projectWithNS, commitSHA, status) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetCommitStatus", reflect.TypeOf((*MockGitClient)(nil).SetCommitStatus), ctx, projectWithNS, commitSHA, status) } // UpdateMergeRequestDiscussionNote mocks base method. -func (m *MockGitClient) UpdateMergeRequestDiscussionNote(mrIID, noteID int, project, discussionID, comment string) (vcs.MRNote, error) { +func (m *MockGitClient) UpdateMergeRequestDiscussionNote(ctx context.Context, mrIID, noteID int, project, discussionID, comment string) (vcs.MRNote, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateMergeRequestDiscussionNote", mrIID, noteID, project, discussionID, comment) + ret := m.ctrl.Call(m, "UpdateMergeRequestDiscussionNote", ctx, mrIID, noteID, project, discussionID, comment) ret0, _ := ret[0].(vcs.MRNote) ret1, _ := ret[1].(error) return ret0, ret1 } // UpdateMergeRequestDiscussionNote indicates an expected call of UpdateMergeRequestDiscussionNote. -func (mr *MockGitClientMockRecorder) UpdateMergeRequestDiscussionNote(mrIID, noteID, project, discussionID, comment interface{}) *gomock.Call { +func (mr *MockGitClientMockRecorder) UpdateMergeRequestDiscussionNote(ctx, mrIID, noteID, project, discussionID, comment interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateMergeRequestDiscussionNote", reflect.TypeOf((*MockGitClient)(nil).UpdateMergeRequestDiscussionNote), mrIID, noteID, project, discussionID, comment) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateMergeRequestDiscussionNote", reflect.TypeOf((*MockGitClient)(nil).UpdateMergeRequestDiscussionNote), ctx, mrIID, noteID, project, discussionID, comment) } // MockGitRepo is a mock of GitRepo interface. diff --git a/pkg/runstream/interfaces.go b/pkg/runstream/interfaces.go index 83d1b7e..1d57f24 100644 --- a/pkg/runstream/interfaces.go +++ b/pkg/runstream/interfaces.go @@ -19,6 +19,9 @@ type StreamClient interface { type RunEvent interface { GetRunID() string + GetContext() context.Context + SetContext(context.Context) + SetCarrier(map[string]string) GetNewStatus() string GetMetadata() RunMetadata SetMetadata(RunMetadata) @@ -38,11 +41,12 @@ type RunMetadata interface { } type RunPollingTask interface { - Schedule() error - Reschedule() error + Schedule(ctx context.Context) error + Reschedule(ctx context.Context) error Completed() error GetRunID() string GetContext() context.Context + SetCarrier(map[string]string) GetLastStatus() string SetLastStatus(string) GetRunMetaData() RunMetadata diff --git a/pkg/runstream/run_event.go b/pkg/runstream/run_event.go index 1b39322..25abef5 100644 --- a/pkg/runstream/run_event.go +++ b/pkg/runstream/run_event.go @@ -11,6 +11,7 @@ import ( "github.com/nats-io/nats.go" "github.com/rs/zerolog/log" "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/propagation" ) const RunEventsStreamName = "RUN_EVENTS" @@ -22,11 +23,22 @@ type TFRunEvent struct { Workspace string NewStatus string Metadata RunMetadata + Carrier propagation.MapCarrier `json:"Carrier"` + context context.Context } func (e *TFRunEvent) GetRunID() string { return e.RunID } +func (e *TFRunEvent) GetContext() context.Context { + return e.context +} +func (e *TFRunEvent) SetContext(ctx context.Context) { + e.context = ctx +} +func (e *TFRunEvent) SetCarrier(carrier map[string]string) { + e.Carrier = carrier +} func (e *TFRunEvent) GetNewStatus() string { return e.NewStatus } @@ -36,9 +48,11 @@ func (e *TFRunEvent) GetMetadata() RunMetadata { func (e *TFRunEvent) SetMetadata(meta RunMetadata) { e.Metadata = meta } + func (s *Stream) PublishTFRunEvent(ctx context.Context, re RunEvent) error { - _, span := otel.Tracer("TF").Start(ctx, "PublishTFRunEvent") + ctx, span := otel.Tracer("TF").Start(ctx, "PublishTFRunEvent") defer span.End() + re.SetContext(ctx) rmd, err := s.waitForTFRunMetadata(re) if err != nil { @@ -46,7 +60,7 @@ func (s *Stream) PublishTFRunEvent(ctx context.Context, re RunEvent) error { return err } - b, err := encodeTFRunEvent(re) + b, err := encodeTFRunEvent(ctx, re) if err != nil { return err } @@ -158,9 +172,13 @@ func decodeTFRunEvent(b []byte) (RunEvent, error) { run := &TFRunEvent{} run.Metadata = &TFRunMetadata{} err := json.Unmarshal(b, &run) + run.context = otel.GetTextMapPropagator().Extract(context.Background(), run.Carrier) return run, err } -func encodeTFRunEvent(run RunEvent) ([]byte, error) { +func encodeTFRunEvent(ctx context.Context, run RunEvent) ([]byte, error) { + carrier := propagation.MapCarrier(map[string]string{}) + otel.GetTextMapPropagator().Inject(ctx, carrier) + run.SetCarrier(carrier) return json.Marshal(run) } diff --git a/pkg/runstream/run_metadata.go b/pkg/runstream/run_metadata.go index 12f49b4..956cb4b 100644 --- a/pkg/runstream/run_metadata.go +++ b/pkg/runstream/run_metadata.go @@ -2,8 +2,9 @@ package runstream import ( "encoding/json" - "github.com/nats-io/nats.go" "time" + + "github.com/nats-io/nats.go" ) // ensure type complies with interface diff --git a/pkg/runstream/run_polling.go b/pkg/runstream/run_polling.go index af42225..548bc86 100644 --- a/pkg/runstream/run_polling.go +++ b/pkg/runstream/run_polling.go @@ -8,6 +8,9 @@ import ( "github.com/nats-io/nats.go" "github.com/rs/zerolog/log" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/propagation" ) const RunPollingStreamNameV0 = "RUN_POLLING" @@ -34,6 +37,7 @@ type TFRunPollingTask struct { // Revision is the NATS KV entry revision Revision uint64 ctx context.Context + Carrier propagation.MapCarrier `json:"Carrier"` } func (s *Stream) NewTFRunPollingTask(meta RunMetadata, delay time.Duration) RunPollingTask { @@ -53,14 +57,14 @@ func (s *Stream) NewTFRunPollingTask(meta RunMetadata, delay time.Duration) RunP } } -func (task *TFRunPollingTask) Schedule() error { - return task.stream.addTFRunPollingTask(task) +func (task *TFRunPollingTask) Schedule(ctx context.Context) error { + return task.stream.addTFRunPollingTask(ctx, task) } -func (task *TFRunPollingTask) Reschedule() error { +func (task *TFRunPollingTask) Reschedule(ctx context.Context) error { task.NextPoll = time.Now().Add(TaskPollingDelayDefault) task.Processing = false - return task.update() + return task.update(ctx) } func (task *TFRunPollingTask) Completed() error { @@ -72,6 +76,9 @@ func (task *TFRunPollingTask) GetRunID() string { func (task *TFRunPollingTask) GetContext() context.Context { return task.ctx } +func (task *TFRunPollingTask) SetCarrier(carrier map[string]string) { + task.Carrier = carrier +} func (task *TFRunPollingTask) GetLastStatus() string { return task.LastStatus } @@ -81,9 +88,16 @@ func (task *TFRunPollingTask) GetRunMetaData() RunMetadata { func (task *TFRunPollingTask) SetLastStatus(status string) { task.LastStatus = status } -func (task *TFRunPollingTask) update() error { +func (task *TFRunPollingTask) update(ctx context.Context) error { + ctx, span := otel.Tracer("terraform").Start(ctx, "update") + defer span.End() + task.LastUpdate = time.Now() - b, _ := encodeTFRunPollingTask(task) + span.SetAttributes( + attribute.String("runID", task.GetRunID()), + ) + + b, _ := encodeTFRunPollingTask(ctx, task) rev, err := task.stream.pollingKV.Update(pollingKVKey(task), b, task.Revision) if err != nil { // TODO: are there are errors we need to handle? @@ -94,8 +108,11 @@ func (task *TFRunPollingTask) update() error { return nil } -func (s *Stream) addTFRunPollingTask(task *TFRunPollingTask) error { - b, err := encodeTFRunPollingTask(task) +func (s *Stream) addTFRunPollingTask(ctx context.Context, task *TFRunPollingTask) error { + ctx, span := otel.Tracer("terraform").Start(ctx, "addTFRunPollingTask") + defer span.End() + + b, err := encodeTFRunPollingTask(ctx, task) if err != nil { return err } @@ -136,19 +153,21 @@ func (s *Stream) startPollingTaskDispatcher() { continue } if !task.Processing && time.Now().After(task.NextPoll) { + ctx, span := otel.Tracer("terraform").Start(task.GetContext(), "polling") // set & get processing status task.Processing = true - err := task.update() + err := task.update(ctx) if err == nil { // dispatch task and wait for response - b, _ := encodeTFRunPollingTask(task) + b, _ := encodeTFRunPollingTask(ctx, task) log.Debug().Str("runID", task.GetRunID()).Msg("enqueuing polling task") if _, err := s.js.PublishAsync(pollingStreamKey(task), b); err != nil { + span.RecordError(err) log.Error().Err(err).Msg("could not queue polling task") continue } - } + span.End() } } @@ -209,6 +228,8 @@ func (s *Stream) decodeTFRunPollingTaskKVEntry(entry nats.KeyValueEntry) (*TFRun log.Error().Err(err).Msg("unexpected error while decoding TF Run Polling Task KV entry") } + run.ctx = otel.GetTextMapPropagator().Extract(context.Background(), run.Carrier) + // backwards compat // TODO: remove once upgraded if run.RunMetadata.GetRunID() == "" { @@ -230,10 +251,15 @@ func (s *Stream) decodeTFRunPollingTask(b []byte) (*TFRunPollingTask, error) { err := json.Unmarshal(b, &run) run.stream = s + ctx := context.Background() + run.ctx = otel.GetTextMapPropagator().Extract(ctx, run.Carrier) + return run, err } -func encodeTFRunPollingTask(run *TFRunPollingTask) ([]byte, error) { +func encodeTFRunPollingTask(ctx context.Context, run *TFRunPollingTask) ([]byte, error) { + run.Carrier = make(map[string]string) + otel.GetTextMapPropagator().Inject(ctx, run.Carrier) return json.Marshal(run) } diff --git a/pkg/tfc_api/api_client.go b/pkg/tfc_api/api_client.go index 062a1ac..4e29e5e 100644 --- a/pkg/tfc_api/api_client.go +++ b/pkg/tfc_api/api_client.go @@ -7,15 +7,18 @@ import ( "github.com/hashicorp/go-tfe" "github.com/rs/zerolog/log" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" ) //go:generate mockgen -source api_client.go -destination=../mocks/mock_tfc_api.go -package=mocks github.com/zapier/tfbuddy/pkg/tfc_api type ApiClient interface { GetPlanOutput(id string) ([]byte, error) - GetRun(id string) (*tfe.Run, error) + GetRun(ctx context.Context, id string) (*tfe.Run, error) GetWorkspaceByName(ctx context.Context, org, name string) (*tfe.Workspace, error) GetWorkspaceById(ctx context.Context, id string) (*tfe.Workspace, error) - CreateRunFromSource(opts *ApiRunOptions) (*tfe.Run, error) + CreateRunFromSource(ctx context.Context, opts *ApiRunOptions) (*tfe.Run, error) LockUnlockWorkspace(ctx context.Context, workspace string, reason string, tag string, lock bool) error AddTags(ctx context.Context, workspace string, prefix string, value string) error RemoveTagsByQuery(ctx context.Context, workspace string, query string) error @@ -45,9 +48,12 @@ func NewTFCClient() ApiClient { return &TFCClient{Client: tfcClient} } -func (t *TFCClient) GetRun(id string) (*tfe.Run, error) { +func (t *TFCClient) GetRun(ctx context.Context, id string) (*tfe.Run, error) { + ctx, span := otel.Tracer("TFC").Start(ctx, "GetTFRun", trace.WithAttributes(attribute.String("run_id", id))) + defer span.End() + run, err := t.Client.Runs.ReadWithOptions( - context.Background(), + ctx, id, &tfe.RunReadOptions{ Include: []tfe.RunIncludeOpt{tfe.RunPlan, tfe.RunWorkspace, tfe.RunConfigVer, tfe.RunApply}, @@ -73,10 +79,14 @@ func (t *TFCClient) GetPlanOutput(id string) ([]byte, error) { } func (t *TFCClient) GetWorkspaceById(ctx context.Context, id string) (*tfe.Workspace, error) { + ctx, span := otel.Tracer("TFC").Start(ctx, "GetTFWorkspaceById", trace.WithAttributes(attribute.String("run_id", id))) + defer span.End() return t.Client.Workspaces.ReadByID(ctx, id) } func (t *TFCClient) GetWorkspaceByName(ctx context.Context, org, name string) (*tfe.Workspace, error) { + ctx, span := otel.Tracer("TFC").Start(ctx, "GetTFWorkspaceByName", trace.WithAttributes(attribute.String("name", name), attribute.String("org", org))) + defer span.End() return t.Client.Workspaces.ReadWithOptions( ctx, org, @@ -86,7 +96,11 @@ func (t *TFCClient) GetWorkspaceByName(ctx context.Context, org, name string) (* } func (t *TFCClient) LockUnlockWorkspace(ctx context.Context, workspaceID string, reason string, tag string, lock bool) error { - + ctx, span := otel.Tracer("TFC").Start(ctx, "LockUnlockWorkspace", trace.WithAttributes( + attribute.String("workspaceID", workspaceID), + attribute.Bool("lock", lock), + )) + defer span.End() LockOptions := tfe.WorkspaceLockOptions{Reason: &reason} TagPrefix := "gl-lock" @@ -129,9 +143,15 @@ func (t *TFCClient) LockUnlockWorkspace(ctx context.Context, workspaceID string, // The tags take the format of prefix dash value, which is just a convention and not required by terraform cloud for naming format. // The tag, however, will be lowercased by terraform cloud, and in any retrieval operations. func (t *TFCClient) AddTags(ctx context.Context, workspace string, prefix string, value string) error { + ctx, span := otel.Tracer("TFC").Start(ctx, "AddTags", trace.WithAttributes( + attribute.String("workspace", workspace), + )) + defer span.End() LockTag := &tfe.Tag{ Name: fmt.Sprintf("%s-%s", prefix, value), } + span.SetAttributes(attribute.String("tag", LockTag.Name)) + AddTagsOptions := tfe.WorkspaceAddTagsOptions{ Tags: []*tfe.Tag{LockTag}, } @@ -147,6 +167,11 @@ func (t *TFCClient) AddTags(ctx context.Context, workspace string, prefix string // RemoveTagsByQuery removes all tags matching a query from a terraform cloud workspace. It returns an error if one is returned fom searching or removing tags.// Note: the query will match anywhere in the tag, so common substrings should be avoided. func (t *TFCClient) RemoveTagsByQuery(ctx context.Context, workspace string, query string) error { + ctx, span := otel.Tracer("TFC").Start(ctx, "RemoveTagsByQuery", trace.WithAttributes( + attribute.String("workspace", workspace), + )) + defer span.End() + taglist, err := t.GetTagsByQuery(ctx, workspace, query) if err != nil { log.Error().Err(err) @@ -179,6 +204,11 @@ func (t *TFCClient) RemoveTagsByQuery(ctx context.Context, workspace string, que // GetTagsByQuery returns a list of values of tags on a terraform workspace matching the query string. // It operates on strings reporesenting the value of the tag and internally converts it to and from the upstreams tag struct as needed. Attempting to query tags based on their tag ID will not match the tag. func (t *TFCClient) GetTagsByQuery(ctx context.Context, workspace string, query string) ([]string, error) { + ctx, span := otel.Tracer("TFC").Start(ctx, "GetTagsByQuery", trace.WithAttributes( + attribute.String("workspace", workspace), + )) + defer span.End() + ListTagOptions := &tfe.WorkspaceTagListOptions{ Query: &query, } diff --git a/pkg/tfc_api/api_driven_runs.go b/pkg/tfc_api/api_driven_runs.go index e7e1334..0fe20e7 100644 --- a/pkg/tfc_api/api_driven_runs.go +++ b/pkg/tfc_api/api_driven_runs.go @@ -7,6 +7,7 @@ import ( "github.com/hashicorp/go-tfe" "github.com/rs/zerolog/log" + "go.opentelemetry.io/otel" ) type ApiRunOptions struct { @@ -25,8 +26,10 @@ type ApiRunOptions struct { } // CreateRunFromSource creates a new Terraform Cloud run from source files -func (c *TFCClient) CreateRunFromSource(opts *ApiRunOptions) (*tfe.Run, error) { - ctx := context.Background() +func (c *TFCClient) CreateRunFromSource(ctx context.Context, opts *ApiRunOptions) (*tfe.Run, error) { + ctx, span := otel.Tracer("TFC").Start(ctx, "CreateRunFromSource") + defer span.End() + log := log.With().Str("workspace", opts.Workspace).Logger() ws, err := c.GetWorkspaceByName(ctx, opts.Organization, opts.Workspace) diff --git a/pkg/tfc_hooks/notification.go b/pkg/tfc_hooks/notification.go index 0b05ef0..2e91865 100644 --- a/pkg/tfc_hooks/notification.go +++ b/pkg/tfc_hooks/notification.go @@ -121,7 +121,7 @@ func (h *NotificationHandler) processNotification(ctx context.Context, n *Notifi if n.RunId == "" { return } - run, err := h.api.GetRun(n.RunId) + run, err := h.api.GetRun(ctx, n.RunId) if err != nil { span.RecordError(err) log.Error().Err(err) diff --git a/pkg/tfc_hooks/polling.go b/pkg/tfc_hooks/polling.go index ebe44cd..ac0df45 100644 --- a/pkg/tfc_hooks/polling.go +++ b/pkg/tfc_hooks/polling.go @@ -10,12 +10,12 @@ import ( // pollingStreamCallback processes TFC run polling tasks for speculative plans. We do not receive webhook notifications // for speculative plans, so they need to be polled instead. func (p *NotificationHandler) pollingStreamCallback(task runstream.RunPollingTask) bool { - ctx, span := otel.Tracer("TFC").Start(task.GetContext(), "Gitlab - PollingStreamCallback") + ctx, span := otel.Tracer("TFC").Start(task.GetContext(), "PollingStreamCallback") defer span.End() log.Debug().Interface("task", task).Msg("TFC Run Polling Callback()") - run, err := p.api.GetRun(task.GetRunID()) + run, err := p.api.GetRun(ctx, task.GetRunID()) if err != nil { log.Error().Err(err).Str("runID", task.GetRunID()).Msg("could not get run") return false @@ -34,12 +34,18 @@ func (p *NotificationHandler) pollingStreamCallback(task runstream.RunPollingTas RunID: run.ID, NewStatus: string(run.Status), }) + if err != nil { + span.RecordError(err) + log.Error().Err(err).Str("runID", task.GetRunID()).Msg("could not publish run event") + return false + } + } if isRunning(run) { // queue another polling task task.SetLastStatus(string(run.Status)) - if err := task.Reschedule(); err != nil { + if err := task.Reschedule(ctx); err != nil { log.Error().Err(err).Msg("could not reschedule TFC run polling task") } diff --git a/pkg/tfc_trigger/project_config.go b/pkg/tfc_trigger/project_config.go index 56c693d..3610b7f 100644 --- a/pkg/tfc_trigger/project_config.go +++ b/pkg/tfc_trigger/project_config.go @@ -1,6 +1,7 @@ package tfc_trigger import ( + "context" "errors" "fmt" "os" @@ -12,6 +13,7 @@ import ( "github.com/rs/zerolog/log" "github.com/zapier/tfbuddy/pkg/utils" "github.com/zapier/tfbuddy/pkg/vcs" + "go.opentelemetry.io/otel" "gopkg.in/dealancer/validate.v2" "gopkg.in/yaml.v2" ) @@ -91,11 +93,14 @@ type TFCWorkspace struct { TriggerDirs []string `yaml:"triggerDirs"` } -func getProjectConfigFile(gl vcs.GitClient, trigger *TFCTrigger) (*ProjectConfig, error) { +func getProjectConfigFile(ctx context.Context, gl vcs.GitClient, trigger *TFCTrigger) (*ProjectConfig, error) { + ctx, span := otel.Tracer("GitlabHandler").Start(ctx, "getProjectConfigFile") + defer span.End() + branches := []string{trigger.GetBranch(), "master", "main"} for _, branch := range branches { log.Debug().Msg(fmt.Sprintf("considering branch %s", branch)) - b, err := gl.GetRepoFile(trigger.GetProjectNameWithNamespace(), ProjectConfigFilename, branch) + b, err := gl.GetRepoFile(ctx, trigger.GetProjectNameWithNamespace(), ProjectConfigFilename, branch) if err != nil { log.Info().Err(err).Msg(fmt.Sprintf("no file on branch %s", branch)) continue diff --git a/pkg/tfc_trigger/tfc_trigger.go b/pkg/tfc_trigger/tfc_trigger.go index 3e0d5d1..4f6a598 100644 --- a/pkg/tfc_trigger/tfc_trigger.go +++ b/pkg/tfc_trigger/tfc_trigger.go @@ -197,7 +197,10 @@ var ( ErrWorkspaceUnlocked = errors.New("workspace is already unlocked") ) -func FindLockingMR(tags []string, thisMR string) string { +func FindLockingMR(ctx context.Context, tags []string, thisMR string) string { + ctx, span := otel.Tracer("TFC").Start(ctx, "FindLockingMR") + defer span.End() + for _, tag := range tags { tag = strings.TrimSpace(tag) log.Debug().Msg(fmt.Sprintf("processing tag: '%s'", tag)) @@ -222,22 +225,29 @@ func FindLockingMR(tags []string, thisMR string) string { // handleError both logs an error and reports it back to the Merge Request via an MR comment. // the returned error is identical to the input parameter as a convenience -func (t *TFCTrigger) handleError(err error, msg string) error { +func (t *TFCTrigger) handleError(ctx context.Context, err error, msg string) error { + ctx, span := otel.Tracer("TFC").Start(ctx, "handleError") + defer span.End() + span.RecordError(err) + log.Error().Err(err).Msg(msg) - if err := t.gl.CreateMergeRequestComment(t.GetMergeRequestIID(), t.GetProjectNameWithNamespace(), fmt.Sprintf("Error: %s: %v", msg, err)); err != nil { + if err := t.gl.CreateMergeRequestComment(ctx, t.GetMergeRequestIID(), t.GetProjectNameWithNamespace(), fmt.Sprintf("Error: %s: %v", msg, err)); err != nil { log.Error().Err(err).Msg("could not post error to Gitlab MR") } return err } // / postUpdate puts a message on a relevant MR -func (t *TFCTrigger) postUpdate(msg string) error { - return t.gl.CreateMergeRequestComment(t.GetMergeRequestIID(), t.GetProjectNameWithNamespace(), msg) +func (t *TFCTrigger) postUpdate(ctx context.Context, msg string) error { + return t.gl.CreateMergeRequestComment(ctx, t.GetMergeRequestIID(), t.GetProjectNameWithNamespace(), msg) } -func (t *TFCTrigger) getLockingMR(workspace string) string { - tags, err := t.tfc.GetTagsByQuery(context.Background(), workspace, tfPrefix) +func (t *TFCTrigger) getLockingMR(ctx context.Context, workspace string) string { + ctx, span := otel.Tracer("TFC").Start(ctx, "getLockingMR") + defer span.End() + + tags, err := t.tfc.GetTagsByQuery(ctx, workspace, tfPrefix) if err != nil { log.Error().Err(err) } @@ -245,12 +255,15 @@ func (t *TFCTrigger) getLockingMR(workspace string) string { log.Info().Msg("no tags returned") return "" } - lockingMR := FindLockingMR(tags, fmt.Sprintf("%d", t.GetMergeRequestIID())) + lockingMR := FindLockingMR(ctx, tags, fmt.Sprintf("%d", t.GetMergeRequestIID())) return lockingMR } -func (t *TFCTrigger) getTriggeredWorkspaces(modifiedFiles []string) ([]*TFCWorkspace, error) { - cfg, err := getProjectConfigFile(t.gl, t) +func (t *TFCTrigger) getTriggeredWorkspaces(ctx context.Context, modifiedFiles []string) ([]*TFCWorkspace, error) { + ctx, span := otel.Tracer("GitlabHandler").Start(ctx, "getTriggeredWorkspaces") + defer span.End() + + cfg, err := getProjectConfigFile(ctx, t.gl, t) if err != nil { if t.GetTriggerSource() == CommentTrigger { return nil, fmt.Errorf("could not read .tfbuddy.yml file for this repo. %w", err) @@ -290,7 +303,10 @@ type TriggeredTFCWorkspaces struct { Executed []string } -func (t *TFCTrigger) getModifiedWorkspaceBetweenMergeBaseTargetBranch(mr vcs.MR, repo vcs.GitRepo) (map[string]struct{}, error) { +func (t *TFCTrigger) getModifiedWorkspaceBetweenMergeBaseTargetBranch(ctx context.Context, mr vcs.MR, repo vcs.GitRepo) (map[string]struct{}, error) { + ctx, span := otel.Tracer("GitlabHandler").Start(ctx, "getModifiedWorkspaceBetweenMergeBaseTargetBranch") + defer span.End() + modifiedWSMap := make(map[string]struct{}, 0) // update the cloned repo to have the latest commits from target branch (usually main) err := repo.FetchUpstreamBranch(mr.GetTargetBranch()) @@ -314,7 +330,7 @@ func (t *TFCTrigger) getModifiedWorkspaceBetweenMergeBaseTargetBranch(mr vcs.MR, if len(targetModifiedFiles) > 0 { // use the same logic to find triggeredWorkspaces based on files modified between when the source branch was // forked and the current HEAD of the target branch - targetBranchWorkspaces, err := t.getTriggeredWorkspaces(targetModifiedFiles) + targetBranchWorkspaces, err := t.getTriggeredWorkspaces(ctx, targetModifiedFiles) if err != nil { return modifiedWSMap, fmt.Errorf("could not find modified workspaces for target branch. %w", err) } @@ -324,25 +340,30 @@ func (t *TFCTrigger) getModifiedWorkspaceBetweenMergeBaseTargetBranch(mr vcs.MR, } return modifiedWSMap, err } -func (t *TFCTrigger) getTriggeredWorkspacesForRequest(mr vcs.MR) ([]*TFCWorkspace, error) { +func (t *TFCTrigger) getTriggeredWorkspacesForRequest(ctx context.Context, mr vcs.MR) ([]*TFCWorkspace, error) { + ctx, span := otel.Tracer("GitlabHandler").Start(ctx, "getTriggeredWorkspacesForRequest") + defer span.End() - mrModifiedFiles, err := t.gl.GetMergeRequestModifiedFiles(mr.GetInternalID(), t.GetProjectNameWithNamespace()) + mrModifiedFiles, err := t.gl.GetMergeRequestModifiedFiles(ctx, mr.GetInternalID(), t.GetProjectNameWithNamespace()) if err != nil { return nil, fmt.Errorf("failed to get a list of modified files. %w", err) } log.Debug().Strs("modifiedFiles", mrModifiedFiles).Msg("modified files") - return t.getTriggeredWorkspaces(mrModifiedFiles) + return t.getTriggeredWorkspaces(ctx, mrModifiedFiles) } // cloneGitRepo will clone the git repo for a specific MR and returns the temp path to be cleaned up later -func (t *TFCTrigger) cloneGitRepo(mr vcs.MR) (vcs.GitRepo, error) { +func (t *TFCTrigger) cloneGitRepo(ctx context.Context, mr vcs.MR) (vcs.GitRepo, error) { + ctx, span := otel.Tracer("GitlabHandler").Start(ctx, "cloneGitRepo") + defer span.End() + safeProj := strings.ReplaceAll(t.GetProjectNameWithNamespace(), "/", "-") cloneDir, err := ioutil.TempDir("", fmt.Sprintf("%s-%d-*", safeProj, t.GetMergeRequestIID())) if err != nil { return nil, fmt.Errorf("could not create tmp directory. %w", err) } - repo, err := t.gl.CloneMergeRequest(t.GetProjectNameWithNamespace(), mr, cloneDir) + repo, err := t.gl.CloneMergeRequest(ctx, t.GetProjectNameWithNamespace(), mr, cloneDir) if err != nil { return nil, utils.CreatePermanentError(err) } @@ -356,7 +377,7 @@ func (t *TFCTrigger) TriggerTFCEvents(ctx context.Context) (*TriggeredTFCWorkspa if err != nil { return nil, fmt.Errorf("could not read MergeRequest data from Gitlab API. %w", err) } - triggeredWorkspaces, err := t.getTriggeredWorkspacesForRequest(mr) + triggeredWorkspaces, err := t.getTriggeredWorkspacesForRequest(ctx, mr) if err != nil { return nil, fmt.Errorf("could not read triggered workspaces. %w", err) } @@ -366,15 +387,15 @@ func (t *TFCTrigger) TriggerTFCEvents(ctx context.Context) (*TriggeredTFCWorkspa } if len(triggeredWorkspaces) > 0 { - repo, err := t.cloneGitRepo(mr) + repo, err := t.cloneGitRepo(ctx, mr) if err != nil { return nil, err } defer os.Remove(repo.GetLocalDirectory()) - modifiedWSMap, err := t.getModifiedWorkspaceBetweenMergeBaseTargetBranch(mr, repo) + modifiedWSMap, err := t.getModifiedWorkspaceBetweenMergeBaseTargetBranch(ctx, mr, repo) if err != nil { - err = t.postUpdate(":warning: Could not identify modified workspaces on target branch. Please review the plan carefully for unrelated changes.") + err = t.postUpdate(ctx, ":warning: Could not identify modified workspaces on target branch. Please review the plan carefully for unrelated changes.") if err != nil { log.Error().Err(err).Msg("could not update MR with message") } @@ -398,7 +419,7 @@ func (t *TFCTrigger) TriggerTFCEvents(ctx context.Context) (*TriggeredTFCWorkspa }) continue } - if err := t.triggerRunForWorkspace(cfgWS, mr, repo.GetLocalDirectory()); err != nil { + if err := t.triggerRunForWorkspace(ctx, cfgWS, mr, repo.GetLocalDirectory()); err != nil { log.Error().Err(err).Msg("could not trigger Run for Workspace") workspaceStatus.Errored = append(workspaceStatus.Errored, &ErroredWorkspace{ Name: cfgWS.Name, @@ -411,7 +432,7 @@ func (t *TFCTrigger) TriggerTFCEvents(ctx context.Context) (*TriggeredTFCWorkspa } else if t.GetTriggerSource() == CommentTrigger { log.Error().Err(ErrNoChangesDetected) - t.postUpdate(ErrNoChangesDetected.Error()) + t.postUpdate(ctx, ErrNoChangesDetected.Error()) return nil, nil } else { @@ -431,35 +452,35 @@ func (t *TFCTrigger) TriggerCleanupEvent(ctx context.Context) error { return fmt.Errorf("could not read MergeRequest data from Gitlab API. %w", err) } var wsNames []string - cfg, err := getProjectConfigFile(t.gl, t) + cfg, err := getProjectConfigFile(ctx, t.gl, t) if err != nil { return fmt.Errorf("ignoring cleanup trigger for project, missing .tfbuddy.yaml. %w", err) } tag := fmt.Sprintf("%s-%d", tfPrefix, mr.GetInternalID()) for _, cfgWS := range cfg.Workspaces { - ws, err := t.tfc.GetWorkspaceByName(context.Background(), + ws, err := t.tfc.GetWorkspaceByName(ctx, cfgWS.Organization, cfgWS.Name) if err != nil { - t.handleError(err, "error getting workspace") + t.handleError(ctx, err, "error getting workspace") } - tags, err := t.tfc.GetTagsByQuery(context.Background(), + tags, err := t.tfc.GetTagsByQuery(ctx, ws.ID, tag, ) if err != nil { - t.handleError(err, "error getting tags") + t.handleError(ctx, err, "error getting tags") } if len(tags) != 0 { - err = t.tfc.RemoveTagsByQuery(context.Background(), ws.ID, tag) + err = t.tfc.RemoveTagsByQuery(ctx, ws.ID, tag) if err != nil { - t.handleError(err, "Error removing locking tag from workspace") + t.handleError(ctx, err, "Error removing locking tag from workspace") continue } wsNames = append(wsNames, cfgWS.Name) } } - _, err = t.gl.CreateMergeRequestDiscussion(mr.GetInternalID(), + _, err = t.gl.CreateMergeRequestDiscussion(ctx, mr.GetInternalID(), t.GetProjectNameWithNamespace(), fmt.Sprintf("Released locks for workspaces: %s", strings.Join(wsNames, ",")), ) @@ -493,12 +514,15 @@ func (t *TFCTrigger) LockUnlockWorkspace(ws *tfe.Workspace, mr vcs.DetailedMR, l return ErrWorkspaceUnlocked } -func (t *TFCTrigger) triggerRunForWorkspace(cfgWS *TFCWorkspace, mr vcs.DetailedMR, cloneDir string) error { +func (t *TFCTrigger) triggerRunForWorkspace(ctx context.Context, cfgWS *TFCWorkspace, mr vcs.DetailedMR, cloneDir string) error { + ctx, span := otel.Tracer("TFC").Start(ctx, "triggerRunForWorkspace") + defer span.End() + org := cfgWS.Organization wsName := cfgWS.Name // retrieve TFC workspace details, so we can sanity check this request. - ws, err := t.tfc.GetWorkspaceByName(context.Background(), org, wsName) + ws, err := t.tfc.GetWorkspaceByName(ctx, org, wsName) if err != nil { return fmt.Errorf("could not get Workspace from TFC API. %w", err) } @@ -515,7 +539,7 @@ func (t *TFCTrigger) triggerRunForWorkspace(cfgWS *TFCWorkspace, mr vcs.Detailed if err != nil { return fmt.Errorf("error modifying the TFC lock on the workspace. %w", err) } - _, err := t.gl.CreateMergeRequestDiscussion(mr.GetInternalID(), + _, err := t.gl.CreateMergeRequestDiscussion(ctx, mr.GetInternalID(), t.GetProjectNameWithNamespace(), fmt.Sprintf("Successfully %sed Workspace `%s/%s`", t.GetAction(), org, wsName), ) @@ -540,13 +564,13 @@ func (t *TFCTrigger) triggerRunForWorkspace(cfgWS *TFCWorkspace, mr vcs.Detailed // If the workspace is locked tell the user and don't queue a run // Otherwise, TFC wil queue an apply, which might put them out of order if isApply { - lockingMR := t.getLockingMR(ws.ID) + lockingMR := t.getLockingMR(ctx, ws.ID) if ws.Locked { return fmt.Errorf("refusing to Apply changes to a locked workspace. %w", err) } else if lockingMR != "" { return fmt.Errorf("workspace is locked by another MR! %s", lockingMR) } else { - err = t.tfc.AddTags(context.Background(), + err = t.tfc.AddTags(ctx, ws.ID, tfPrefix, fmt.Sprintf("%d", t.GetMergeRequestIID()), @@ -557,7 +581,7 @@ func (t *TFCTrigger) triggerRunForWorkspace(cfgWS *TFCWorkspace, mr vcs.Detailed } } // create a new Merge Request discussion thread where status updates will be nested - disc, err := t.gl.CreateMergeRequestDiscussion(mr.GetInternalID(), + disc, err := t.gl.CreateMergeRequestDiscussion(ctx, mr.GetInternalID(), t.GetProjectNameWithNamespace(), fmt.Sprintf("Starting TFC %v for Workspace: `%s/%s`.", t.GetAction(), org, wsName), ) @@ -572,7 +596,7 @@ func (t *TFCTrigger) triggerRunForWorkspace(cfgWS *TFCWorkspace, mr vcs.Detailed } // create new TFC run - run, err := t.tfc.CreateRunFromSource(&tfc_api.ApiRunOptions{ + run, err := t.tfc.CreateRunFromSource(ctx, &tfc_api.ApiRunOptions{ IsApply: isApply, Path: pkgDir, Message: fmt.Sprintf("MR [!%d]: %s", t.GetMergeRequestIID(), mr.GetTitle()), @@ -592,10 +616,13 @@ func (t *TFCTrigger) triggerRunForWorkspace(cfgWS *TFCWorkspace, mr vcs.Detailed Bool("speculative", run.ConfigurationVersion.Speculative). Msg("created TFC run") - return t.publishRunToStream(run) + return t.publishRunToStream(ctx, run) } -func (t *TFCTrigger) publishRunToStream(run *tfe.Run) error { +func (t *TFCTrigger) publishRunToStream(ctx context.Context, run *tfe.Run) error { + ctx, span := otel.Tracer("TFC").Start(ctx, "publishRunToStream") + defer span.End() + rmd := &runstream.TFRunMetadata{ RunID: run.ID, Organization: run.Workspace.Organization.Name, @@ -617,7 +644,7 @@ func (t *TFCTrigger) publishRunToStream(run *tfe.Run) error { if run.ConfigurationVersion.Speculative { // TFC doesn't send Notification webhooks for speculative plans, so we need to poll for updates. task := t.runstream.NewTFRunPollingTask(rmd, 1*time.Second) - err := task.Schedule() + err := task.Schedule(ctx) if err != nil { return fmt.Errorf("failed to create TFC plan polling task. Updates may not be posted to MR. %w", err) diff --git a/pkg/tfc_trigger/tfc_trigger_test.go b/pkg/tfc_trigger/tfc_trigger_test.go index ec7067b..3c5acac 100644 --- a/pkg/tfc_trigger/tfc_trigger_test.go +++ b/pkg/tfc_trigger/tfc_trigger_test.go @@ -85,7 +85,7 @@ func TestFindLockingMR(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := tfc_trigger.FindLockingMR(tt.tags, tt.MR) + got := tfc_trigger.FindLockingMR(context.Background(), tt.tags, tt.MR) if got != tt.want { t.Fatalf("didn't match got: %s, want: %s", got, tt.want) } @@ -105,8 +105,8 @@ func TestTFCEvents_SingleWorkspacePlan(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() testSuite := mocks.CreateTestSuite(mockCtrl, mocks.TestOverrides{ProjectConfig: ws}, t) - testSuite.MockGitClient.EXPECT().CreateMergeRequestDiscussion(testSuite.MetaData.MRIID, testSuite.MetaData.ProjectNameNS, "Starting TFC plan for Workspace: `zapier-test/service-tfbuddy`.").Return(testSuite.MockGitDisc, nil) - testSuite.MockApiClient.EXPECT().CreateRunFromSource(gomock.Any()).Return(&tfe.Run{ + testSuite.MockGitClient.EXPECT().CreateMergeRequestDiscussion(gomock.Any(), testSuite.MetaData.MRIID, testSuite.MetaData.ProjectNameNS, "Starting TFC plan for Workspace: `zapier-test/service-tfbuddy`.").Return(testSuite.MockGitDisc, nil) + testSuite.MockApiClient.EXPECT().CreateRunFromSource(gomock.Any(), gomock.Any()).Return(&tfe.Run{ ID: "101", Workspace: &tfe.Workspace{Name: "service-tfbuddy", Organization: &tfe.Organization{Name: "zapier-test"}, @@ -114,7 +114,7 @@ func TestTFCEvents_SingleWorkspacePlan(t *testing.T) { ConfigurationVersion: &tfe.ConfigurationVersion{Speculative: true}}, nil) mockRunPollingTask := mocks.NewMockRunPollingTask(mockCtrl) - mockRunPollingTask.EXPECT().Schedule() + mockRunPollingTask.EXPECT().Schedule(gomock.Any()) testSuite.MockStreamClient.EXPECT().NewTFRunPollingTask(gomock.Any(), time.Second*1).Return(mockRunPollingTask) @@ -129,8 +129,7 @@ func TestTFCEvents_SingleWorkspacePlan(t *testing.T) { TriggerSource: tfc_trigger.CommentTrigger, }) trigger := tfc_trigger.NewTFCTrigger(testSuite.MockGitClient, testSuite.MockApiClient, testSuite.MockStreamClient, tCfg) - ctx, _ := otel.Tracer("FAKE").Start(context.Background(), "TEST") - triggeredWS, err := trigger.TriggerTFCEvents(ctx) + triggeredWS, err := trigger.TriggerTFCEvents(context.Background()) if err != nil { t.Fatal(err) } @@ -158,9 +157,9 @@ func TestTFCEvents_SingleWorkspacePlanError(t *testing.T) { defer mockCtrl.Finish() testSuite := mocks.CreateTestSuite(mockCtrl, mocks.TestOverrides{ProjectConfig: ws}, t) - testSuite.MockGitClient.EXPECT().CreateMergeRequestDiscussion(testSuite.MetaData.MRIID, testSuite.MetaData.ProjectNameNS, "Starting TFC plan for Workspace: `zapier-test/service-tfbuddy`.").Return(testSuite.MockGitDisc, nil) + testSuite.MockGitClient.EXPECT().CreateMergeRequestDiscussion(gomock.Any(), testSuite.MetaData.MRIID, testSuite.MetaData.ProjectNameNS, "Starting TFC plan for Workspace: `zapier-test/service-tfbuddy`.").Return(testSuite.MockGitDisc, nil) - testSuite.MockApiClient.EXPECT().CreateRunFromSource(gomock.Any()).Return(nil, fmt.Errorf("could not create run from source")) + testSuite.MockApiClient.EXPECT().CreateRunFromSource(gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("could not create run from source")) testSuite.InitTestSuite() @@ -211,7 +210,7 @@ func TestTFCEvents_SingleWorkspaceApply(t *testing.T) { defer mockCtrl.Finish() testSuite := mocks.CreateTestSuite(mockCtrl, mocks.TestOverrides{ProjectConfig: ws}, t) testSuite.MockGitRepo.EXPECT().GetModifiedFileNamesBetweenCommits(testSuite.MetaData.CommonSHA, "main").Return([]string{}, nil) - testSuite.MockApiClient.EXPECT().CreateRunFromSource(gomock.Any()).Return(&tfe.Run{ + testSuite.MockApiClient.EXPECT().CreateRunFromSource(gomock.Any(), gomock.Any()).Return(&tfe.Run{ ID: "101", Workspace: &tfe.Workspace{Name: "service-tfbuddy", Organization: &tfe.Organization{Name: "zapier-test"}, @@ -275,16 +274,16 @@ func TestTFCEvents_MultiWorkspaceApply(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() testSuite := mocks.CreateTestSuite(mockCtrl, mocks.TestOverrides{ProjectConfig: ws}, t) - testSuite.MockGitClient.EXPECT().GetMergeRequestModifiedFiles(testSuite.MetaData.MRIID, testSuite.MetaData.ProjectNameNS).Return([]string{"main.tf", "staging/terraform.tf"}, nil) + testSuite.MockGitClient.EXPECT().GetMergeRequestModifiedFiles(gomock.Any(), testSuite.MetaData.MRIID, testSuite.MetaData.ProjectNameNS).Return([]string{"main.tf", "staging/terraform.tf"}, nil) - testSuite.MockGitClient.EXPECT().CreateMergeRequestDiscussion(testSuite.MetaData.MRIID, testSuite.MetaData.ProjectNameNS, "Starting TFC apply for Workspace: `zapier-test/service-tfbuddy`.").Return(testSuite.MockGitDisc, nil) - testSuite.MockGitClient.EXPECT().CreateMergeRequestDiscussion(testSuite.MetaData.MRIID, testSuite.MetaData.ProjectNameNS, "Starting TFC apply for Workspace: `zapier-test/service-tfbuddy-staging`.").Return(testSuite.MockGitDisc, nil) + testSuite.MockGitClient.EXPECT().CreateMergeRequestDiscussion(gomock.Any(), testSuite.MetaData.MRIID, testSuite.MetaData.ProjectNameNS, "Starting TFC apply for Workspace: `zapier-test/service-tfbuddy`.").Return(testSuite.MockGitDisc, nil) + testSuite.MockGitClient.EXPECT().CreateMergeRequestDiscussion(gomock.Any(), testSuite.MetaData.MRIID, testSuite.MetaData.ProjectNameNS, "Starting TFC apply for Workspace: `zapier-test/service-tfbuddy-staging`.").Return(testSuite.MockGitDisc, nil) - testSuite.MockApiClient.EXPECT().GetWorkspaceByName(gomock.Any(), "zapier-test", gomock.Any()).DoAndReturn(func(a interface{}, b, c string) (*tfe.Workspace, error) { + testSuite.MockApiClient.EXPECT().GetWorkspaceByName(gomock.Any(), "zapier-test", gomock.Any()).DoAndReturn(func(a interface{}, c, d string) (*tfe.Workspace, error) { return &tfe.Workspace{ID: c}, nil }).AnyTimes() - testSuite.MockApiClient.EXPECT().CreateRunFromSource(gomock.Any()).DoAndReturn(func(opts *tfc_api.ApiRunOptions) (*tfe.Run, error) { + testSuite.MockApiClient.EXPECT().CreateRunFromSource(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, opts *tfc_api.ApiRunOptions) (*tfe.Run, error) { return &tfe.Run{ ID: "101", Workspace: &tfe.Workspace{Name: opts.Workspace, @@ -350,9 +349,9 @@ func TestTFCEvents_SingleWorkspaceApplyError(t *testing.T) { defer mockCtrl.Finish() testSuite := mocks.CreateTestSuite(mockCtrl, mocks.TestOverrides{ProjectConfig: ws}, t) - testSuite.MockGitClient.EXPECT().GetMergeRequestModifiedFiles(testSuite.MetaData.MRIID, testSuite.MetaData.ProjectNameNS).Return([]string{"main.tf"}, nil) - testSuite.MockGitClient.EXPECT().CloneMergeRequest(testSuite.MetaData.ProjectNameNS, gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("could not clone repo")) - testSuite.MockGitClient.EXPECT().CreateMergeRequestComment(testSuite.MetaData.MRIID, testSuite.MetaData.ProjectNameNS, "Error: could not clone repo: could not clone repo").MaxTimes(2) + testSuite.MockGitClient.EXPECT().GetMergeRequestModifiedFiles(gomock.Any(), testSuite.MetaData.MRIID, testSuite.MetaData.ProjectNameNS).Return([]string{"main.tf"}, nil) + testSuite.MockGitClient.EXPECT().CloneMergeRequest(gomock.Any(), testSuite.MetaData.ProjectNameNS, gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("could not clone repo")) + testSuite.MockGitClient.EXPECT().CreateMergeRequestComment(gomock.Any(), testSuite.MetaData.MRIID, testSuite.MetaData.ProjectNameNS, "Error: could not clone repo: could not clone repo").MaxTimes(2) testSuite.InitTestSuite() @@ -368,8 +367,7 @@ func TestTFCEvents_SingleWorkspaceApplyError(t *testing.T) { TriggerSource: tfc_trigger.CommentTrigger, }) trigger := tfc_trigger.NewTFCTrigger(testSuite.MockGitClient, testSuite.MockApiClient, testSuite.MockStreamClient, tCfg) - ctx, _ := otel.Tracer("FAKE").Start(context.Background(), "TEST") - triggeredWS, err := trigger.TriggerTFCEvents(ctx) + triggeredWS, err := trigger.TriggerTFCEvents(context.Background()) if err == nil { t.Fatal("expected error to be returned") return @@ -406,16 +404,16 @@ func TestTFCEvents_MultiWorkspaceApplyError(t *testing.T) { defer mockCtrl.Finish() testSuite := mocks.CreateTestSuite(mockCtrl, mocks.TestOverrides{ProjectConfig: ws}, t) - testSuite.MockGitClient.EXPECT().GetMergeRequestModifiedFiles(testSuite.MetaData.MRIID, testSuite.MetaData.ProjectNameNS).Return([]string{"main.tf", "staging/terraform.tf"}, nil) + testSuite.MockGitClient.EXPECT().GetMergeRequestModifiedFiles(gomock.Any(), testSuite.MetaData.MRIID, testSuite.MetaData.ProjectNameNS).Return([]string{"main.tf", "staging/terraform.tf"}, nil) - testSuite.MockGitClient.EXPECT().CreateMergeRequestDiscussion(testSuite.MetaData.MRIID, testSuite.MetaData.ProjectNameNS, "Starting TFC apply for Workspace: `zapier-test/service-tfbuddy`.").Return(testSuite.MockGitDisc, nil) - testSuite.MockGitClient.EXPECT().CreateMergeRequestDiscussion(testSuite.MetaData.MRIID, testSuite.MetaData.ProjectNameNS, "Starting TFC apply for Workspace: `zapier-test/service-tfbuddy-staging`.").Return(testSuite.MockGitDisc, nil) + testSuite.MockGitClient.EXPECT().CreateMergeRequestDiscussion(gomock.Any(), testSuite.MetaData.MRIID, testSuite.MetaData.ProjectNameNS, "Starting TFC apply for Workspace: `zapier-test/service-tfbuddy`.").Return(testSuite.MockGitDisc, nil) + testSuite.MockGitClient.EXPECT().CreateMergeRequestDiscussion(gomock.Any(), testSuite.MetaData.MRIID, testSuite.MetaData.ProjectNameNS, "Starting TFC apply for Workspace: `zapier-test/service-tfbuddy-staging`.").Return(testSuite.MockGitDisc, nil) testSuite.MockApiClient.EXPECT().GetWorkspaceByName(gomock.Any(), "zapier-test", gomock.Any()).DoAndReturn(func(a interface{}, b, c string) (*tfe.Workspace, error) { return &tfe.Workspace{ID: c}, nil }).AnyTimes() - testSuite.MockApiClient.EXPECT().CreateRunFromSource(gomock.Any()).DoAndReturn(func(opts *tfc_api.ApiRunOptions) (*tfe.Run, error) { + testSuite.MockApiClient.EXPECT().CreateRunFromSource(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, opts *tfc_api.ApiRunOptions) (*tfe.Run, error) { if opts.Workspace == "service-tfbuddy" { return nil, fmt.Errorf("api error with terraform cloud") } @@ -470,7 +468,7 @@ func TestTFCEvents_WorkspaceApplyModifiedBothSrcDstBranches(t *testing.T) { testSuite := mocks.CreateTestSuite(mockCtrl, mocks.TestOverrides{}, t) testSuite.MockGitRepo.EXPECT().GetModifiedFileNamesBetweenCommits(testSuite.MetaData.CommonSHA, "main").Return([]string{"terraform.tf"}, nil) - testSuite.MockGitClient.EXPECT().GetMergeRequestModifiedFiles(testSuite.MetaData.MRIID, testSuite.MetaData.ProjectNameNS).Return([]string{"main.tf"}, nil) + testSuite.MockGitClient.EXPECT().GetMergeRequestModifiedFiles(gomock.Any(), testSuite.MetaData.MRIID, testSuite.MetaData.ProjectNameNS).Return([]string{"main.tf"}, nil) mockStreamClient := mocks.NewMockStreamClient(mockCtrl) @@ -529,10 +527,10 @@ func TestTFCEvents_MultiWorkspaceApplyModifiedBothSrcDstBranches(t *testing.T) { Dir: "production", }}}, }, t) - testSuite.MockGitClient.EXPECT().GetMergeRequestModifiedFiles(testSuite.MetaData.MRIID, testSuite.MetaData.ProjectNameNS).Return([]string{"/production/main.tf"}, nil).AnyTimes() + testSuite.MockGitClient.EXPECT().GetMergeRequestModifiedFiles(gomock.Any(), testSuite.MetaData.MRIID, testSuite.MetaData.ProjectNameNS).Return([]string{"/production/main.tf"}, nil).AnyTimes() testSuite.MockGitRepo.EXPECT().GetModifiedFileNamesBetweenCommits(testSuite.MetaData.CommonSHA, testSuite.MetaData.TargetBranch).Return([]string{"/staging/terraform.tf"}, nil).AnyTimes() - testSuite.MockApiClient.EXPECT().CreateRunFromSource(gomock.Any()).Return(&tfe.Run{ + testSuite.MockApiClient.EXPECT().CreateRunFromSource(gomock.Any(), gomock.Any()).Return(&tfe.Run{ ID: "101", Workspace: &tfe.Workspace{Name: "service-tfbuddy", Organization: &tfe.Organization{Name: "zapier-test"}, @@ -540,7 +538,7 @@ func TestTFCEvents_MultiWorkspaceApplyModifiedBothSrcDstBranches(t *testing.T) { ConfigurationVersion: &tfe.ConfigurationVersion{Speculative: true}}, nil) mockRunPollingTask := mocks.NewMockRunPollingTask(mockCtrl) - mockRunPollingTask.EXPECT().Schedule() + mockRunPollingTask.EXPECT().Schedule(gomock.Any()) testSuite.MockStreamClient.EXPECT().NewTFRunPollingTask(gomock.Any(), time.Second*1).Return(mockRunPollingTask) testSuite.InitTestSuite() diff --git a/pkg/tfc_utils/ci_job_lock.go b/pkg/tfc_utils/ci_job_lock.go index 00735a2..cf4a0dd 100644 --- a/pkg/tfc_utils/ci_job_lock.go +++ b/pkg/tfc_utils/ci_job_lock.go @@ -5,10 +5,12 @@ import ( tfe "github.com/hashicorp/go-tfe" "github.com/rs/zerolog/log" + "go.opentelemetry.io/otel" ) -func LockUnlockWorkspace(token, workspace string, lock bool, lockReason string) { - ctx := context.Background() +func LockUnlockWorkspace(ctx context.Context, token, workspace string, lock bool, lockReason string) { + ctx, span := otel.Tracer("TFC").Start(ctx, "LockUnlockWorkspace") + defer span.End() log.Info().Str("workspace", workspace).Msgf("LockUnlockWorkspace for workspace.") diff --git a/pkg/tfc_utils/ci_job_run_status.go b/pkg/tfc_utils/ci_job_run_status.go index b5266e5..d7a8d28 100644 --- a/pkg/tfc_utils/ci_job_run_status.go +++ b/pkg/tfc_utils/ci_job_run_status.go @@ -1,6 +1,7 @@ package tfc_utils import ( + "context" "fmt" "log" "os" @@ -11,6 +12,7 @@ import ( "github.com/zapier/tfbuddy/pkg/tfc_api" "github.com/zapier/tfbuddy/pkg/vcs/gitlab" + "go.opentelemetry.io/otel" tfe "github.com/hashicorp/go-tfe" gogitlab "github.com/xanzy/go-gitlab" @@ -43,10 +45,11 @@ const MR_RUN_DETAILS_FORMAT = ` func MonitorRunStatus() { glClient = gitlab.NewGitlabClient() tfcClient = tfc_api.NewTFCClient() + ctx := context.Background() projectID := os.Getenv("CI_PROJECT_ID") sha := os.Getenv("CI_COMMIT_SHA") - statuses := glClient.GetCommitStatuses(projectID, sha) + statuses := glClient.GetCommitStatuses(ctx, projectID, sha) commentBody := "" wg := sync.WaitGroup{} for _, s := range statuses { @@ -64,29 +67,29 @@ func MonitorRunStatus() { commentBody += fmt.Sprintf(MR_RUN_DETAILS_FORMAT, workspace, s.Status, s.TargetURL, s.TargetURL, description) st := s wg.Add(1) - go waitForRunCompletionOrFailure(&wg, st, workspace, runID) + go waitForRunCompletionOrFailure(ctx, &wg, st, workspace, runID) case "success": fallthrough case "failed": // get run summary - postRunSummary(s, workspace, runID) + postRunSummary(ctx, s, workspace, runID) } } } } - postCommentBody(commentBody) + postCommentBody(ctx, commentBody) wg.Wait() } -func postCommentBody(commentBody string) { +func postCommentBody(ctx context.Context, commentBody string) { if commentBody != "" { projectID := os.Getenv("CI_PROJECT_ID") mrIID, err := strconv.Atoi(os.Getenv("CI_MERGE_REQUEST_IID")) if err != nil { log.Printf("erroring posting comment: %v", err) } - glClient.CreateMergeRequestComment(mrIID, projectID, fmt.Sprintf(MR_COMMENT_FORMAT, commentBody)) + glClient.CreateMergeRequestComment(ctx, mrIID, projectID, fmt.Sprintf(MR_COMMENT_FORMAT, commentBody)) } } @@ -104,9 +107,9 @@ var failedPlanSummaryFormat = ` *Click Terraform Cloud URL to see detailed plan output* ` -func postRunSummary(commitStatus *gogitlab.CommitStatus, wsName, runID string) { +func postRunSummary(ctx context.Context, commitStatus *gogitlab.CommitStatus, wsName, runID string) { //run, _ := tfcClient.Client.Runs.ReadWithOptions(context.Background(), runID, &tfe.RunReadOptions{Include: "plan"}) - run, err := tfcClient.GetRun(runID) + run, err := tfcClient.GetRun(ctx, runID) if err != nil { log.Printf("err: %v\n", err) return @@ -122,10 +125,12 @@ func postRunSummary(commitStatus *gogitlab.CommitStatus, wsName, runID string) { } commentBody := fmt.Sprintf(MR_RUN_DETAILS_FORMAT, wsName, run.Status, commitStatus.TargetURL, commitStatus.TargetURL, description) - postCommentBody(commentBody) + postCommentBody(ctx, commentBody) } -func waitForRunCompletionOrFailure(wg *sync.WaitGroup, commitStatus *gogitlab.CommitStatus, wsName, runID string) { +func waitForRunCompletionOrFailure(ctx context.Context, wg *sync.WaitGroup, commitStatus *gogitlab.CommitStatus, wsName, runID string) { + ctx, span := otel.Tracer("TFC").Start(ctx, "waitForRunCompletionOrFailure") + defer span.End() defer wg.Done() attempts := 360 @@ -135,7 +140,7 @@ func waitForRunCompletionOrFailure(wg *sync.WaitGroup, commitStatus *gogitlab.Co time.Sleep(retryInterval) log.Println("Reading Run details.", runID) - run, err := tfcClient.GetRun(runID) + run, err := tfcClient.GetRun(ctx, runID) if err != nil { log.Printf("err: %v\n", err) } @@ -145,7 +150,7 @@ func waitForRunCompletionOrFailure(wg *sync.WaitGroup, commitStatus *gogitlab.Co continue } - postRunSummary(commitStatus, wsName, runID) + postRunSummary(ctx, commitStatus, wsName, runID) break } } diff --git a/pkg/vcs/github/client.go b/pkg/vcs/github/client.go index 3788ffe..ce79af8 100644 --- a/pkg/vcs/github/client.go +++ b/pkg/vcs/github/client.go @@ -54,36 +54,49 @@ func NewGithubClient() *Client { } } -func (c *Client) GetMergeRequestApprovals(id int, project string) (vcs.MRApproved, error) { - pr, err := c.GetPullRequest(project, id) +func (c *Client) GetMergeRequestApprovals(ctx context.Context, id int, project string) (vcs.MRApproved, error) { + ctx, span := otel.Tracer("TFC").Start(ctx, "GetMergeRequestApprovals") + defer span.End() + + pr, err := c.GetPullRequest(ctx, project, id) if err != nil { return nil, err } return pr, nil } -func (c *Client) CreateMergeRequestComment(prID int, fullName string, comment string) error { - _, err := c.PostIssueComment(prID, fullName, comment) +func (c *Client) CreateMergeRequestComment(ctx context.Context, prID int, fullName string, comment string) error { + ctx, span := otel.Tracer("TFC").Start(ctx, "CreateMergeRequestComment") + defer span.End() + + _, err := c.PostIssueComment(ctx, prID, fullName, comment) return err } -func (c *Client) CreateMergeRequestDiscussion(prID int, fullName string, comment string) (vcs.MRDiscussionNotes, error) { +func (c *Client) CreateMergeRequestDiscussion(ctx context.Context, prID int, fullName string, comment string) (vcs.MRDiscussionNotes, error) { + ctx, span := otel.Tracer("TFC").Start(ctx, "CreateMergeRequestDiscussion") + defer span.End() + // GitHub doesn't support discussion threads AFAICT. - iss, err := c.PostIssueComment(prID, fullName, comment) + iss, err := c.PostIssueComment(ctx, prID, fullName, comment) return &GithubPRIssueComment{iss}, err } func (c *Client) GetMergeRequest(ctx context.Context, prID int, fullName string) (vcs.DetailedMR, error) { - _, span := otel.Tracer("hooks").Start(ctx, "GetMergeRequest") + ctx, span := otel.Tracer("hooks").Start(ctx, "GetMergeRequest") defer span.End() - pr, err := c.GetPullRequest(fullName, prID) + + pr, err := c.GetPullRequest(ctx, fullName, prID) if err != nil { return nil, err } return pr, nil } -func (c *Client) GetRepoFile(fullName string, file string, ref string) ([]byte, error) { +func (c *Client) GetRepoFile(ctx context.Context, fullName string, file string, ref string) ([]byte, error) { + ctx, span := otel.Tracer("TFC").Start(ctx, "GetRepoFile") + defer span.End() + if ref == "" { ref = "HEAD" } @@ -106,8 +119,11 @@ func (c *Client) GetRepoFile(fullName string, file string, ref string) ([]byte, }, createBackOffWithRetries()) } -func (c *Client) GetMergeRequestModifiedFiles(prID int, fullName string) ([]string, error) { - pr, err := c.GetPullRequest(fullName, prID) +func (c *Client) GetMergeRequestModifiedFiles(ctx context.Context, prID int, fullName string) ([]string, error) { + ctx, span := otel.Tracer("TFC").Start(ctx, "GetMergeRequestModifiedFiles") + defer span.End() + + pr, err := c.GetPullRequest(ctx, fullName, prID) if err != nil { return nil, err } @@ -138,13 +154,16 @@ func (c *Client) GetMergeRequestModifiedFiles(prID int, fullName string) ([]stri const GITHUB_CLONE_DEPTH_ENV = "TFBUDDY_GITHUB_CLONE_DEPTH" -func (c *Client) CloneMergeRequest(project string, mr vcs.MR, dest string) (vcs.GitRepo, error) { +func (c *Client) CloneMergeRequest(ctx context.Context, project string, mr vcs.MR, dest string) (vcs.GitRepo, error) { + ctx, span := otel.Tracer("TFC").Start(ctx, "CloneMergeRequest") + defer span.End() + parts, err := splitFullName(project) if err != nil { return nil, err } - repo, _, err := c.client.Repositories.Get(context.Background(), parts[0], parts[1]) + repo, _, err := c.client.Repositories.Get(ctx, parts[0], parts[1]) if err != nil { return nil, utils.CreatePermanentError(err) } @@ -195,57 +214,69 @@ func (c *Client) CloneMergeRequest(project string, mr vcs.MR, dest string) (vcs. return zgit.NewRepository(gitRepo, auth, dest), nil } -func (c *Client) UpdateMergeRequestDiscussionNote(mrIID, noteID int, project, discussionID, comment string) (vcs.MRNote, error) { +func (c *Client) UpdateMergeRequestDiscussionNote(ctx context.Context, mrIID, noteID int, project, discussionID, comment string) (vcs.MRNote, error) { //TODO implement me //panic("implement me") return nil, nil } -func (c *Client) ResolveMergeRequestDiscussion(s string, i int, s2 string) error { +func (c *Client) ResolveMergeRequestDiscussion(ctx context.Context, s string, i int, s2 string) error { // This is a NoOp on GitHub return nil } -func (c *Client) AddMergeRequestDiscussionReply(prID int, fullName, discussionID, comment string) (vcs.MRNote, error) { +func (c *Client) AddMergeRequestDiscussionReply(ctx context.Context, prID int, fullName, discussionID, comment string) (vcs.MRNote, error) { + ctx, span := otel.Tracer("TFC").Start(ctx, "ApplyMergeRequestDiscussionReply") + defer span.End() + // GitHub doesn't support discussion threads AFAICT. - iss, err := c.PostIssueComment(prID, fullName, comment) + iss, err := c.PostIssueComment(ctx, prID, fullName, comment) return &IssueComment{iss}, err } -func (c *Client) SetCommitStatus(projectWithNS string, commitSHA string, status vcs.CommitStatusOptions) (vcs.CommitStatus, error) { +func (c *Client) SetCommitStatus(ctx context.Context, projectWithNS string, commitSHA string, status vcs.CommitStatusOptions) (vcs.CommitStatus, error) { //TODO implement me return nil, nil } -func (c *Client) GetPipelinesForCommit(projectWithNS string, commitSHA string) ([]vcs.ProjectPipeline, error) { +func (c *Client) GetPipelinesForCommit(ctx context.Context, projectWithNS string, commitSHA string) ([]vcs.ProjectPipeline, error) { //TODO implement me return nil, nil } -func (c *Client) GetIssue(owner *gogithub.User, repo string, issueId int) (*gogithub.Issue, error) { +func (c *Client) GetIssue(ctx context.Context, owner *gogithub.User, repo string, issueId int) (*gogithub.Issue, error) { + ctx, span := otel.Tracer("TFC").Start(ctx, "GetIssue") + defer span.End() + owName, err := ResolveOwnerName(owner) if err != nil { return nil, utils.CreatePermanentError(err) } return backoff.RetryWithData(func() (*gogithub.Issue, error) { - iss, _, err := c.client.Issues.Get(context.Background(), owName, repo, issueId) + iss, _, err := c.client.Issues.Get(ctx, owName, repo, issueId) return iss, utils.CreatePermanentError(err) }, createBackOffWithRetries()) } -func (c *Client) GetPullRequest(fullName string, prID int) (*GithubPR, error) { +func (c *Client) GetPullRequest(ctx context.Context, fullName string, prID int) (*GithubPR, error) { + ctx, span := otel.Tracer("TFC").Start(ctx, "GetPullRequest") + defer span.End() + parts, err := splitFullName(fullName) if err != nil { return nil, err } return backoff.RetryWithData(func() (*GithubPR, error) { - pr, _, err := c.client.PullRequests.Get(c.ctx, parts[0], parts[1], prID) + pr, _, err := c.client.PullRequests.Get(ctx, parts[0], parts[1], prID) return &GithubPR{pr}, utils.CreatePermanentError(err) }, createBackOffWithRetries()) } // PostIssueComment adds a comment to an existing Pull Request -func (c *Client) PostIssueComment(prId int, fullName string, body string) (*gogithub.IssueComment, error) { +func (c *Client) PostIssueComment(ctx context.Context, prId int, fullName string, body string) (*gogithub.IssueComment, error) { + ctx, span := otel.Tracer("TFC").Start(ctx, "PostIssueComment") + defer span.End() + projectParts, err := splitFullName(fullName) if err != nil { return nil, utils.CreatePermanentError(err) @@ -254,7 +285,7 @@ func (c *Client) PostIssueComment(prId int, fullName string, body string) (*gogi comment := &gogithub.IssueComment{ Body: String(body), } - iss, _, err := c.client.Issues.CreateComment(context.Background(), projectParts[0], projectParts[1], prId, comment) + iss, _, err := c.client.Issues.CreateComment(ctx, projectParts[0], projectParts[1], prId, comment) if err != nil { log.Error().Err(err).Msg("github client: could not post issue comment") } @@ -264,7 +295,10 @@ func (c *Client) PostIssueComment(prId int, fullName string, body string) (*gogi } // PostPullRequestComment adds a review comment to an existing PullRequest -func (c *Client) PostPullRequestComment(owner, repo string, prId int, body string) error { +func (c *Client) PostPullRequestComment(ctx context.Context, owner, repo string, prId int, body string) error { + ctx, span := otel.Tracer("TFC").Start(ctx, "PostPullRequestComment") + defer span.End() + // TODO: this is broken return backoff.Retry(func() error { comment := &gogithub.PullRequestComment{ diff --git a/pkg/vcs/github/hooks/github_hooks_handler.go b/pkg/vcs/github/hooks/github_hooks_handler.go index 45ec2c1..cede34f 100644 --- a/pkg/vcs/github/hooks/github_hooks_handler.go +++ b/pkg/vcs/github/hooks/github_hooks_handler.go @@ -83,6 +83,7 @@ func (h *GithubHooksHandler) Handler(c echo.Context) error { func onError(deliveryID string, eventName string, event interface{}, err error) error { _, span := otel.Tracer("GithubEvents").Start(context.Background(), "Github - ErrorHookHandler") defer span.End() + log.Warn().Str("deliveryID", deliveryID).Str("eventName", eventName).Err(err).Msg("GitHub hook handler error") lbl := prometheus.Labels{} lbl["event-type"] = eventName diff --git a/pkg/vcs/github/hooks/stream_worker.go b/pkg/vcs/github/hooks/stream_worker.go index 434e52b..cbfb907 100644 --- a/pkg/vcs/github/hooks/stream_worker.go +++ b/pkg/vcs/github/hooks/stream_worker.go @@ -35,6 +35,7 @@ func (h *GithubHooksHandler) processIssueCommentEvent(msg *GithubIssueCommentEve func (h *GithubHooksHandler) processIssueComment(ctx context.Context, msg *GithubIssueCommentEventMsg) error { ctx, span := otel.Tracer("hooks").Start(ctx, "processIssueComment") defer span.End() + if msg == nil || msg.Payload == nil { return errors.New("msg is nil") } @@ -51,7 +52,7 @@ func (h *GithubHooksHandler) processIssueComment(ctx context.Context, msg *Githu opts, err := comment_actions.ParseCommentCommand(*event.Comment.Body) if err != nil { if err == comment_actions.ErrOtherTFTool { - h.postPullRequestComment(event, "Use 'tfc' to interact with TFBuddy") + h.postPullRequestComment(ctx, event, "Use 'tfc' to interact with TFBuddy") } if err == comment_actions.ErrNotTFCCommand || err == comment_actions.ErrOtherTFTool { githubWebHookIgnored.WithLabelValues( @@ -92,12 +93,12 @@ func (h *GithubHooksHandler) processIssueComment(ctx context.Context, msg *Githu case "apply": log.Info().Msg("Got TFC apply command") if !pullReq.IsApproved() { - h.postPullRequestComment(event, ":no_entry: Apply failed. Pull Request requires approval.") + h.postPullRequestComment(ctx, event, ":no_entry: Apply failed. Pull Request requires approval.") return nil } if pullReq.HasConflicts() { - h.postPullRequestComment(event, ":no_entry: Apply failed. Pull Request has conflicts that need to be resolved.") + h.postPullRequestComment(ctx, event, ":no_entry: Apply failed. Pull Request has conflicts that need to be resolved.") return nil } case "lock": @@ -112,17 +113,20 @@ func (h *GithubHooksHandler) processIssueComment(ctx context.Context, msg *Githu executedWorkspaces, tfError := trigger.TriggerTFCEvents(ctx) if tfError == nil && len(executedWorkspaces.Errored) > 0 { for _, failedWS := range executedWorkspaces.Errored { - h.postPullRequestComment(event, fmt.Sprintf(":no_entry: %s could not be run because: %s", failedWS.Name, failedWS.Error)) + h.postPullRequestComment(ctx, event, fmt.Sprintf(":no_entry: %s could not be run because: %s", failedWS.Name, failedWS.Error)) } return nil } return tfError } -func (h *GithubHooksHandler) postPullRequestComment(event *gogithub.IssueCommentEvent, body string) error { +func (h *GithubHooksHandler) postPullRequestComment(ctx context.Context, event *gogithub.IssueCommentEvent, body string) error { + ctx, span := otel.Tracer("hooks").Start(ctx, "postPullRequestComment") + defer span.End() + log.Debug().Msg("postPullRequestComment") prID := event.GetIssue().GetNumber() log.Debug().Str("repo", event.GetRepo().GetFullName()).Int("PR", prID).Msg("postPullRequestComment") - return h.vcs.CreateMergeRequestComment(prID, event.GetRepo().GetFullName(), body) + return h.vcs.CreateMergeRequestComment(ctx, prID, event.GetRepo().GetFullName(), body) } diff --git a/pkg/vcs/github/run_events_worker.go b/pkg/vcs/github/run_events_worker.go index edb8869..d54ffc9 100644 --- a/pkg/vcs/github/run_events_worker.go +++ b/pkg/vcs/github/run_events_worker.go @@ -1,6 +1,7 @@ package github import ( + "context" "fmt" "github.com/hashicorp/go-tfe" @@ -9,6 +10,7 @@ import ( "github.com/zapier/tfbuddy/pkg/runstream" "github.com/zapier/tfbuddy/pkg/tfc_api" "github.com/zapier/tfbuddy/pkg/vcs" + "go.opentelemetry.io/otel" ) const runEventsConsumerDurableName = "github" @@ -43,26 +45,33 @@ func (w *RunEventsWorker) Close() { // eventStreamCallback processes TFC run notifications via the NATS stream func (w *RunEventsWorker) eventStreamCallback(re runstream.RunEvent) bool { + ctx, span := otel.Tracer("TFC").Start(re.GetContext(), "eventStreamCallback") + defer span.End() + log.Debug().Interface("TFRunEvent", re).Msg("Gitlab RunEventsWorker.eventStreamCallback()") - run, err := w.tfc.GetRun(re.GetRunID()) + run, err := w.tfc.GetRun(ctx, re.GetRunID()) if err != nil { + span.RecordError(err) log.Error().Err(err).Str("runID", re.GetRunID()).Msg("could not get run") return false } run.Status = tfe.RunStatus(re.GetNewStatus()) - w.postRunStatusComment(run, re.GetMetadata()) + w.postRunStatusComment(ctx, run, re.GetMetadata()) //w.updateCommitStatusForRun(run, re.GetMetadata()) return true } -func (w *RunEventsWorker) postRunStatusComment(run *tfe.Run, rmd runstream.RunMetadata) { +func (w *RunEventsWorker) postRunStatusComment(ctx context.Context, run *tfe.Run, rmd runstream.RunMetadata) { + ctx, span := otel.Tracer("TFC").Start(ctx, "postRunStatusComment") + defer span.End() commentBody, _, _ := comment_formatter.FormatRunStatusCommentBody(w.tfc, run, rmd) if commentBody != "" { w.client.CreateMergeRequestComment( + ctx, rmd.GetMRInternalID(), rmd.GetMRProjectNameWithNamespace(), fmt.Sprintf( diff --git a/pkg/vcs/gitlab/client.go b/pkg/vcs/gitlab/client.go index a7c4564..7858763 100644 --- a/pkg/vcs/gitlab/client.go +++ b/pkg/vcs/gitlab/client.go @@ -58,7 +58,10 @@ func NewGitlabClient() *GitlabClient { return &GitlabClient{glClient, token, tokenUser} } -func (c *GitlabClient) ResolveMergeRequestDiscussion(projectWithNamespace string, mrIID int, discussionID string) error { +func (c *GitlabClient) ResolveMergeRequestDiscussion(ctx context.Context, projectWithNamespace string, mrIID int, discussionID string) error { + _, span := otel.Tracer("TFC").Start(ctx, "ResolveMergeRequestDiscussion") + defer span.End() + return backoff.Retry(func() error { _, _, err := c.client.Discussions.ResolveMergeRequestDiscussion(projectWithNamespace, mrIID, discussionID, &gogitlab.ResolveMergeRequestDiscussionOptions{Resolved: gogitlab.Bool(true)}) return utils.CreatePermanentError(err) @@ -96,13 +99,19 @@ func (gS *GitlabCommitStatus) Info() string { return fmt.Sprintf("%s %s %s", gS.Author.Username, gS.Name, gS.SHA) } -func (c *GitlabClient) SetCommitStatus(projectWithNS string, commitSHA string, status vcs.CommitStatusOptions) (vcs.CommitStatus, error) { +func (c *GitlabClient) SetCommitStatus(ctx context.Context, projectWithNS string, commitSHA string, status vcs.CommitStatusOptions) (vcs.CommitStatus, error) { + _, span := otel.Tracer("TFC").Start(ctx, "SetCommitStatus") + defer span.End() + return backoff.RetryWithData(func() (vcs.CommitStatus, error) { commitStatus, _, err := c.client.Commits.SetCommitStatus(projectWithNS, commitSHA, status.(*GitlabCommitStatusOptions).SetCommitStatusOptions) return &GitlabCommitStatus{commitStatus}, utils.CreatePermanentError(err) }, createBackOffWithRetries()) } -func (c *GitlabClient) GetCommitStatuses(projectID, commitSHA string) []*gogitlab.CommitStatus { +func (c *GitlabClient) GetCommitStatuses(ctx context.Context, projectID, commitSHA string) []*gogitlab.CommitStatus { + _, span := otel.Tracer("TFC").Start(ctx, "GetCommitStatuses") + defer span.End() + statuses, _, err := c.client.Commits.GetCommitStatuses(projectID, commitSHA, &gogitlab.GetCommitStatusesOptions{Stage: &glExternalStageName}) if err != nil { log.Fatal().Msgf("could not get commit statuses: %v\n", err) @@ -112,7 +121,10 @@ func (c *GitlabClient) GetCommitStatuses(projectID, commitSHA string) []*gogitla } // CreateMergeRequestComment creates a comment on the merge request. -func (c *GitlabClient) CreateMergeRequestComment(mrIID int, projectID, comment string) error { +func (c *GitlabClient) CreateMergeRequestComment(ctx context.Context, mrIID int, projectID, comment string) error { + _, span := otel.Tracer("TFC").Start(ctx, "CreateMergeRequestComment") + defer span.End() + if comment != "" { return backoff.Retry(func() error { log.Debug().Str("projectID", projectID).Int("mrIID", mrIID).Msg("posting Gitlab comment") @@ -146,7 +158,10 @@ func (gn *GitlabMRNote) GetNoteID() int64 { return int64(gn.Note.ID) } -func (c *GitlabClient) CreateMergeRequestDiscussion(mrIID int, project, comment string) (vcs.MRDiscussionNotes, error) { +func (c *GitlabClient) CreateMergeRequestDiscussion(ctx context.Context, mrIID int, project, comment string) (vcs.MRDiscussionNotes, error) { + _, span := otel.Tracer("TFC").Start(ctx, "CreateMergeRequestDiscussion") + defer span.End() + if comment == "" { return nil, errors.New("comment is empty") } @@ -159,7 +174,10 @@ func (c *GitlabClient) CreateMergeRequestDiscussion(mrIID int, project, comment }, createBackOffWithRetries()) } -func (c *GitlabClient) UpdateMergeRequestDiscussionNote(mrIID, noteID int, project, discussionID, comment string) (vcs.MRNote, error) { +func (c *GitlabClient) UpdateMergeRequestDiscussionNote(ctx context.Context, mrIID, noteID int, project, discussionID, comment string) (vcs.MRNote, error) { + _, span := otel.Tracer("TFC").Start(ctx, "UpdateMergeRequestDiscussionNote") + defer span.End() + if comment == "" { return nil, utils.CreatePermanentError(errors.New("comment is empty")) } @@ -178,7 +196,10 @@ func (c *GitlabClient) UpdateMergeRequestDiscussionNote(mrIID, noteID int, proje } // AddMergeRequestDiscussionReply creates a comment on the merge request. -func (c *GitlabClient) AddMergeRequestDiscussionReply(mrIID int, project, discussionID, comment string) (vcs.MRNote, error) { +func (c *GitlabClient) AddMergeRequestDiscussionReply(ctx context.Context, mrIID int, project, discussionID, comment string) (vcs.MRNote, error) { + _, span := otel.Tracer("TFC").Start(ctx, "AddMergeRequestDiscussionReply") + defer span.End() + if comment != "" { return backoff.RetryWithData(func() (vcs.MRNote, error) { log.Debug().Str("project", project).Int("mrIID", mrIID).Msg("posting Gitlab discussion reply") @@ -191,7 +212,10 @@ func (c *GitlabClient) AddMergeRequestDiscussionReply(mrIID int, project, discus } // ResolveMergeRequestDiscussionReply marks a discussion thread as resolved / unresolved. -func (c *GitlabClient) ResolveMergeRequestDiscussionReply(mrIID int, project, discussionID string, resolved bool) error { +func (c *GitlabClient) ResolveMergeRequestDiscussionReply(ctx context.Context, mrIID int, project, discussionID string, resolved bool) error { + _, span := otel.Tracer("TFC").Start(ctx, "ResolveMergeRequestDiscussionReply") + defer span.End() + return backoff.Retry(func() error { log.Debug().Str("project", project).Int("mrIID", mrIID).Msg("posting Gitlab discussion reply") _, _, err := c.client.Discussions.ResolveMergeRequestDiscussion(project, mrIID, discussionID, &gogitlab.ResolveMergeRequestDiscussionOptions{Resolved: gogitlab.Bool(resolved)}) @@ -200,7 +224,10 @@ func (c *GitlabClient) ResolveMergeRequestDiscussionReply(mrIID int, project, di } // GetRepoFile retrieves a single file from a Gitlab repository using the RepositoryFiles API -func (g *GitlabClient) GetRepoFile(project, file, ref string) ([]byte, error) { +func (g *GitlabClient) GetRepoFile(ctx context.Context, project, file, ref string) ([]byte, error) { + _, span := otel.Tracer("TFC").Start(ctx, "GetRepoFile") + defer span.End() + if ref == "" { ref = "HEAD" } @@ -212,7 +239,10 @@ func (g *GitlabClient) GetRepoFile(project, file, ref string) ([]byte, error) { // GetMergeRequestModifiedFiles returns the names of files that were modified in the merge request // relative to the repo root, e.g. parent/child/file.txt. -func (g *GitlabClient) GetMergeRequestModifiedFiles(mrIID int, projectID string) ([]string, error) { +func (g *GitlabClient) GetMergeRequestModifiedFiles(ctx context.Context, mrIID int, projectID string) ([]string, error) { + _, span := otel.Tracer("TFC").Start(ctx, "GetMergeRequestModifiedFiles") + defer span.End() + const maxPerPage = 100 return backoff.RetryWithData(func() ([]string, error) { var files []string @@ -316,7 +346,10 @@ type GitlabMRApproval struct { func (gm *GitlabMRApproval) IsApproved() bool { return gm.Approved } -func (g *GitlabClient) GetMergeRequestApprovals(mrIID int, project string) (vcs.MRApproved, error) { +func (g *GitlabClient) GetMergeRequestApprovals(ctx context.Context, mrIID int, project string) (vcs.MRApproved, error) { + _, span := otel.Tracer("TFC").Start(ctx, "GetMergeRequestApprovals") + defer span.End() + return backoff.RetryWithData(func() (vcs.MRApproved, error) { approvals, _, err := g.client.MergeRequestApprovals.GetConfiguration( project, @@ -339,7 +372,10 @@ func (gP *GitlabPipeline) GetSource() string { func (gP *GitlabPipeline) GetID() int { return gP.ID } -func (g *GitlabClient) GetPipelinesForCommit(project, commitSHA string) ([]vcs.ProjectPipeline, error) { +func (g *GitlabClient) GetPipelinesForCommit(ctx context.Context, project, commitSHA string) ([]vcs.ProjectPipeline, error) { + _, span := otel.Tracer("TFC").Start(ctx, "GetPipelinesForCommit") + defer span.End() + return backoff.RetryWithData(func() ([]vcs.ProjectPipeline, error) { pipelines, _, err := g.client.Pipelines.ListProjectPipelines(project, &gogitlab.ListProjectPipelinesOptions{ SHA: gogitlab.String(commitSHA), diff --git a/pkg/vcs/gitlab/git_actions.go b/pkg/vcs/gitlab/git_actions.go index b8448c4..5a2a711 100644 --- a/pkg/vcs/gitlab/git_actions.go +++ b/pkg/vcs/gitlab/git_actions.go @@ -1,6 +1,7 @@ package gitlab import ( + "context" "os" "path/filepath" @@ -12,20 +13,26 @@ import ( "github.com/xanzy/go-gitlab" zgit "github.com/zapier/tfbuddy/pkg/git" "github.com/zapier/tfbuddy/pkg/vcs" + "go.opentelemetry.io/otel" "gopkg.in/errgo.v2/fmt/errors" ) const GITLAB_CLONE_DEPTH_ENV = "TFBUDDY_GITLAB_CLONE_DEPTH" // CloneMergeRequest performs a git clone of the target Gitlab project & merge request branch to the `dest` path. -func (c *GitlabClient) CloneMergeRequest(project string, mr vcs.MR, dest string) (vcs.GitRepo, error) { +func (c *GitlabClient) CloneMergeRequest(ctx context.Context, project string, mr vcs.MR, dest string) (vcs.GitRepo, error) { + _, span := otel.Tracer("TFC").Start(ctx, "CloneMergeRequest") + defer span.End() + proj, _, err := c.client.Projects.GetProject(project, &gitlab.GetProjectOptions{ License: gitlab.Bool(false), Statistics: gitlab.Bool(false), WithCustomAttributes: gitlab.Bool(false), }) if err != nil { - return nil, errors.Newf("could not clone MR - unable to read project details from Gitlab API: %v", err) + err = errors.Newf("could not clone MR - unable to read project details from Gitlab API: %v", err) + span.RecordError(err) + return nil, err } ref := plumbing.NewBranchReferenceName(mr.GetSourceBranch()) @@ -50,7 +57,9 @@ func (c *GitlabClient) CloneMergeRequest(project string, mr vcs.MR, dest string) }) if err != nil && err != git.ErrRepositoryAlreadyExists { - return nil, errors.Newf("could not clone MR: %v", err) + err = errors.Newf("could not clone MR: %v", err) + span.RecordError(err) + return nil, err } wt, _ := repo.Worktree() @@ -65,7 +74,9 @@ func (c *GitlabClient) CloneMergeRequest(project string, mr vcs.MR, dest string) Force: false, }) if err != nil && err != git.NoErrAlreadyUpToDate { - return nil, errors.Newf("could not pull MR: %v", err) + err = errors.Newf("could not pull MR: %v", err) + span.RecordError(err) + return nil, err } if log.Trace().Enabled() { diff --git a/pkg/vcs/gitlab/mr_comment_updater.go b/pkg/vcs/gitlab/mr_comment_updater.go index fc69404..3a05cce 100644 --- a/pkg/vcs/gitlab/mr_comment_updater.go +++ b/pkg/vcs/gitlab/mr_comment_updater.go @@ -1,9 +1,11 @@ package gitlab import ( + "context" "fmt" "github.com/zapier/tfbuddy/pkg/comment_formatter" + "go.opentelemetry.io/otel" "github.com/hashicorp/go-tfe" "github.com/rs/zerolog/log" @@ -11,11 +13,14 @@ import ( "github.com/zapier/tfbuddy/pkg/runstream" ) -func (p *RunStatusUpdater) postRunStatusComment(run *tfe.Run, rmd runstream.RunMetadata) { +func (p *RunStatusUpdater) postRunStatusComment(ctx context.Context, run *tfe.Run, rmd runstream.RunMetadata) { + ctx, span := otel.Tracer("TFC").Start(ctx, "postRunStatusComment") + defer span.End() commentBody, topLevelNoteBody, resolveDiscussion := comment_formatter.FormatRunStatusCommentBody(p.tfc, run, rmd) if _, err := p.client.UpdateMergeRequestDiscussionNote( + ctx, rmd.GetMRInternalID(), int(rmd.GetRootNoteID()), rmd.GetMRProjectNameWithNamespace(), @@ -26,7 +31,7 @@ func (p *RunStatusUpdater) postRunStatusComment(run *tfe.Run, rmd runstream.RunM } if commentBody != "" { - p.postComment(fmt.Sprintf( + p.postComment(ctx, fmt.Sprintf( "Status: `%s`
%s", run.Status, commentBody), @@ -39,6 +44,7 @@ func (p *RunStatusUpdater) postRunStatusComment(run *tfe.Run, rmd runstream.RunM if resolveDiscussion { err := p.client.ResolveMergeRequestDiscussion( + ctx, rmd.GetMRProjectNameWithNamespace(), rmd.GetMRInternalID(), rmd.GetDiscussionID(), @@ -49,18 +55,21 @@ func (p *RunStatusUpdater) postRunStatusComment(run *tfe.Run, rmd runstream.RunM } } -func (p *RunStatusUpdater) postComment(commentBody, projectID string, mrIID int, discussionID string) error { +func (p *RunStatusUpdater) postComment(ctx context.Context, commentBody, projectID string, mrIID int, discussionID string) error { + ctx, span := otel.Tracer("TFC").Start(ctx, "postComment") + defer span.End() + content := fmt.Sprintf(MR_COMMENT_FORMAT, commentBody) if discussionID != "" { - _, err := p.client.AddMergeRequestDiscussionReply(mrIID, projectID, discussionID, content) + _, err := p.client.AddMergeRequestDiscussionReply(ctx, mrIID, projectID, discussionID, content) if err != nil { log.Error().Err(err).Msg("error posting Gitlab discussion reply") return err } return nil } else { - err := p.client.CreateMergeRequestComment(mrIID, projectID, content) + err := p.client.CreateMergeRequestComment(ctx, mrIID, projectID, content) if err != nil { log.Error().Err(err).Msg("error posting Gitlab comment to MR") return err @@ -69,19 +78,6 @@ func (p *RunStatusUpdater) postComment(commentBody, projectID string, mrIID int, } } -func hasChanges(plan *tfe.Plan) bool { - if plan.ResourceAdditions > 0 { - return true - } - if plan.ResourceDestructions > 0 { - return true - } - if plan.ResourceChanges > 0 { - return true - } - return false -} - const MR_COMMENT_FORMAT = ` ### Terraform Cloud %s diff --git a/pkg/vcs/gitlab/mr_status_updater.go b/pkg/vcs/gitlab/mr_status_updater.go index 25758a4..5363424 100644 --- a/pkg/vcs/gitlab/mr_status_updater.go +++ b/pkg/vcs/gitlab/mr_status_updater.go @@ -1,55 +1,60 @@ package gitlab import ( + "context" "fmt" "github.com/hashicorp/go-tfe" "github.com/rs/zerolog/log" gogitlab "github.com/xanzy/go-gitlab" "github.com/zapier/tfbuddy/pkg/runstream" + "go.opentelemetry.io/otel" ) -func (p *RunStatusUpdater) updateCommitStatusForRun(run *tfe.Run, rmd runstream.RunMetadata) { +func (p *RunStatusUpdater) updateCommitStatusForRun(ctx context.Context, run *tfe.Run, rmd runstream.RunMetadata) { + ctx, span := otel.Tracer("TFC").Start(ctx, "updateCommitStatusForRun") + defer span.End() + switch run.Status { // https://www.terraform.io/cloud-docs/api-docs/run#run-states case tfe.RunPending: // The initial status of a run once it has been created. if rmd.GetAction() == "plan" { - p.updateStatus(gogitlab.Pending, "plan", rmd) - p.updateStatus(gogitlab.Failed, "apply", rmd) + p.updateStatus(ctx, gogitlab.Pending, "plan", rmd) + p.updateStatus(ctx, gogitlab.Failed, "apply", rmd) } else { - p.updateStatus(gogitlab.Pending, "apply", rmd) + p.updateStatus(ctx, gogitlab.Pending, "apply", rmd) } case tfe.RunApplyQueued: // Once the changes in the plan have been confirmed, the run run will transition to apply_queued. // This status indicates that the run should start as soon as the backend services have available capacity. - p.updateStatus(gogitlab.Pending, "apply", rmd) + p.updateStatus(ctx, gogitlab.Pending, "apply", rmd) case tfe.RunApplying: // The applying phase of a run is in progress. - p.updateStatus(gogitlab.Running, "apply", rmd) + p.updateStatus(ctx, gogitlab.Running, "apply", rmd) case tfe.RunApplied: // The applying phase of a run has completed. - p.updateStatus(gogitlab.Success, "apply", rmd) + p.updateStatus(ctx, gogitlab.Success, "apply", rmd) case tfe.RunCanceled: // The run has been discarded. This is a final state. - p.updateStatus(gogitlab.Failed, rmd.GetAction(), rmd) + p.updateStatus(ctx, gogitlab.Failed, rmd.GetAction(), rmd) case tfe.RunDiscarded: // The run has been discarded. This is a final state. - p.updateStatus(gogitlab.Failed, "plan", rmd) - p.updateStatus(gogitlab.Failed, "apply", rmd) + p.updateStatus(ctx, gogitlab.Failed, "plan", rmd) + p.updateStatus(ctx, gogitlab.Failed, "apply", rmd) case tfe.RunErrored: // The run has errored. This is a final state. - p.updateStatus(gogitlab.Failed, rmd.GetAction(), rmd) + p.updateStatus(ctx, gogitlab.Failed, rmd.GetAction(), rmd) case tfe.RunPlanning: // The planning phase of a run is in progress. - p.updateStatus(gogitlab.Running, rmd.GetAction(), rmd) + p.updateStatus(ctx, gogitlab.Running, rmd.GetAction(), rmd) case tfe.RunPlanned: // this status is for Apply runs (as opposed to `RunPlannedAndFinished` below, so don't update the status. @@ -58,16 +63,16 @@ func (p *RunStatusUpdater) updateCommitStatusForRun(run *tfe.Run, rmd runstream. case tfe.RunPlannedAndFinished: // The completion of a run containing a plan only, or a run the produces a plan with no changes to apply. // This is a final state. - p.updateStatus(gogitlab.Success, rmd.GetAction(), rmd) + p.updateStatus(ctx, gogitlab.Success, rmd.GetAction(), rmd) if run.HasChanges { // TODO: is pending enough to block merging before apply? - p.updateStatus(gogitlab.Pending, "apply", rmd) + p.updateStatus(ctx, gogitlab.Pending, "apply", rmd) } case tfe.RunPolicySoftFailed: // A sentinel policy has soft failed for a plan-only run. This is a final state. // During the apply, the policy failure will need to be overriden. - p.updateStatus(gogitlab.Success, rmd.GetAction(), rmd) + p.updateStatus(ctx, gogitlab.Success, rmd.GetAction(), rmd) case tfe.RunPolicyChecked: // The sentinel policy checking phase of a run has completed. @@ -81,18 +86,22 @@ func (p *RunStatusUpdater) updateCommitStatusForRun(run *tfe.Run, rmd runstream. } -func (p *RunStatusUpdater) updateStatus(state gogitlab.BuildStateValue, action string, rmd runstream.RunMetadata) { +func (p *RunStatusUpdater) updateStatus(ctx context.Context, state gogitlab.BuildStateValue, action string, rmd runstream.RunMetadata) { + ctx, span := otel.Tracer("TFC").Start(ctx, "updateStatus") + defer span.End() + status := &gogitlab.SetCommitStatusOptions{ Name: statusName(rmd.GetWorkspace(), action), Context: statusName(rmd.GetWorkspace(), action), TargetURL: runUrlForTFRunMetadata(rmd), Description: descriptionForState(state), State: state, - PipelineID: p.getLatestPipelineID(rmd), + PipelineID: p.getLatestPipelineID(ctx, rmd), } log.Debug().Interface("new_status", status).Msg("updating Gitlab commit status") cs, err := p.client.SetCommitStatus( + ctx, rmd.GetMRProjectNameWithNamespace(), rmd.GetCommitSHA(), &GitlabCommitStatusOptions{status}, @@ -131,8 +140,8 @@ func runUrlForTFRunMetadata(rmd runstream.RunMetadata) *string { )) } -func (p *RunStatusUpdater) getLatestPipelineID(rmd runstream.RunMetadata) *int { - pipelines, err := p.client.GetPipelinesForCommit(rmd.GetMRProjectNameWithNamespace(), rmd.GetCommitSHA()) +func (p *RunStatusUpdater) getLatestPipelineID(ctx context.Context, rmd runstream.RunMetadata) *int { + pipelines, err := p.client.GetPipelinesForCommit(ctx, rmd.GetMRProjectNameWithNamespace(), rmd.GetCommitSHA()) if err != nil { log.Error().Err(err).Msg("could not retrieve pipelines for commit") return nil diff --git a/pkg/vcs/gitlab/run_status_updater.go b/pkg/vcs/gitlab/run_status_updater.go index e15334a..f503346 100644 --- a/pkg/vcs/gitlab/run_status_updater.go +++ b/pkg/vcs/gitlab/run_status_updater.go @@ -6,6 +6,7 @@ import ( "github.com/zapier/tfbuddy/pkg/runstream" "github.com/zapier/tfbuddy/pkg/tfc_api" "github.com/zapier/tfbuddy/pkg/vcs" + "go.opentelemetry.io/otel" ) type RunStatusUpdater struct { @@ -38,16 +39,19 @@ func (p *RunStatusUpdater) Close() { // eventStreamCallback processes TFC run notifications via the NATS stream func (p *RunStatusUpdater) eventStreamCallback(re runstream.RunEvent) bool { + ctx, span := otel.Tracer("TFC").Start(re.GetContext(), "eventStreamCallback") + defer span.End() + log.Debug().Interface("TFRunEvent", re).Msg("Gitlab RunStatusUpdater.eventStreamCallback()") - run, err := p.tfc.GetRun(re.GetRunID()) + run, err := p.tfc.GetRun(ctx, re.GetRunID()) if err != nil { log.Error().Err(err).Str("runID", re.GetRunID()).Msg("could not get run") return false } run.Status = tfe.RunStatus(re.GetNewStatus()) - p.postRunStatusComment(run, re.GetMetadata()) - p.updateCommitStatusForRun(run, re.GetMetadata()) + p.postRunStatusComment(ctx, run, re.GetMetadata()) + p.updateCommitStatusForRun(ctx, run, re.GetMetadata()) return true } diff --git a/pkg/vcs/interfaces.go b/pkg/vcs/interfaces.go index d145cae..a1d351c 100644 --- a/pkg/vcs/interfaces.go +++ b/pkg/vcs/interfaces.go @@ -5,18 +5,18 @@ import "context" //go:generate mockgen -source interfaces.go -destination=../mocks/mock_vcs.go -package=mocks github.com/zapier/tfbuddy/pkg/vcs type GitClient interface { - GetMergeRequestApprovals(id int, project string) (MRApproved, error) - CreateMergeRequestComment(id int, fullPath string, comment string) error - CreateMergeRequestDiscussion(mrID int, fullPath string, comment string) (MRDiscussionNotes, error) + GetMergeRequestApprovals(ctx context.Context, id int, project string) (MRApproved, error) + CreateMergeRequestComment(ctx context.Context, id int, fullPath string, comment string) error + CreateMergeRequestDiscussion(ctx context.Context, mrID int, fullPath string, comment string) (MRDiscussionNotes, error) GetMergeRequest(context.Context, int, string) (DetailedMR, error) - GetRepoFile(string, string, string) ([]byte, error) - GetMergeRequestModifiedFiles(mrIID int, projectID string) ([]string, error) - CloneMergeRequest(string, MR, string) (GitRepo, error) - UpdateMergeRequestDiscussionNote(mrIID, noteID int, project, discussionID, comment string) (MRNote, error) - ResolveMergeRequestDiscussion(string, int, string) error - AddMergeRequestDiscussionReply(mrIID int, project, discussionID, comment string) (MRNote, error) - SetCommitStatus(projectWithNS string, commitSHA string, status CommitStatusOptions) (CommitStatus, error) - GetPipelinesForCommit(projectWithNS string, commitSHA string) ([]ProjectPipeline, error) + GetRepoFile(context.Context, string, string, string) ([]byte, error) + GetMergeRequestModifiedFiles(ctx context.Context, mrIID int, projectID string) ([]string, error) + CloneMergeRequest(context.Context, string, MR, string) (GitRepo, error) + UpdateMergeRequestDiscussionNote(ctx context.Context, mrIID, noteID int, project, discussionID, comment string) (MRNote, error) + ResolveMergeRequestDiscussion(context.Context, string, int, string) error + AddMergeRequestDiscussionReply(ctx context.Context, mrIID int, project, discussionID, comment string) (MRNote, error) + SetCommitStatus(ctx context.Context, projectWithNS string, commitSHA string, status CommitStatusOptions) (CommitStatus, error) + GetPipelinesForCommit(ctx context.Context, projectWithNS string, commitSHA string) ([]ProjectPipeline, error) } type GitRepo interface { FetchUpstreamBranch(string) error