diff --git a/pkg/mocks/helpers.go b/pkg/mocks/helpers.go index 418e7ad..d26ec33 100644 --- a/pkg/mocks/helpers.go +++ b/pkg/mocks/helpers.go @@ -1,6 +1,7 @@ package mocks import ( + "errors" "fmt" "path/filepath" "regexp" @@ -175,6 +176,8 @@ func (ts *TestSuite) InitTestSuite() { ts.MockApiClient.EXPECT().AddTags(gomock.Any(), gomock.Any(), "tfbuddylock", "101").AnyTimes() ts.MockStreamClient.EXPECT().AddRunMeta(gomock.Any()).AnyTimes() + ts.MockStreamClient.EXPECT().AddWorkspaceMeta(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() + ts.MockStreamClient.EXPECT().GetWorkspaceMeta(gomock.Any(), gomock.Any()).Return(nil, errors.New("not found")).AnyTimes() ts.MockProject.EXPECT().GetPathWithNamespace().Return(ts.MetaData.ProjectNameNS).AnyTimes() diff --git a/pkg/mocks/mock_runstream.go b/pkg/mocks/mock_runstream.go index f2442e1..41dfc8a 100644 --- a/pkg/mocks/mock_runstream.go +++ b/pkg/mocks/mock_runstream.go @@ -54,6 +54,20 @@ func (mr *MockStreamClientMockRecorder) AddRunMeta(rmd any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddRunMeta", reflect.TypeOf((*MockStreamClient)(nil).AddRunMeta), rmd) } +// AddWorkspaceMeta mocks base method. +func (m *MockStreamClient) AddWorkspaceMeta(rmd runstream.WorkspaceMetadata, mrID, workspace string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddWorkspaceMeta", rmd, mrID, workspace) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddWorkspaceMeta indicates an expected call of AddWorkspaceMeta. +func (mr *MockStreamClientMockRecorder) AddWorkspaceMeta(rmd, mrID, workspace any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddWorkspaceMeta", reflect.TypeOf((*MockStreamClient)(nil).AddWorkspaceMeta), rmd, mrID, workspace) +} + // GetRunMeta mocks base method. func (m *MockStreamClient) GetRunMeta(runID string) (runstream.RunMetadata, error) { m.ctrl.T.Helper() @@ -69,6 +83,21 @@ func (mr *MockStreamClientMockRecorder) GetRunMeta(runID any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRunMeta", reflect.TypeOf((*MockStreamClient)(nil).GetRunMeta), runID) } +// GetWorkspaceMeta mocks base method. +func (m *MockStreamClient) GetWorkspaceMeta(mrID, workspace string) (*runstream.TFCWorkspacesMetadata, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetWorkspaceMeta", mrID, workspace) + ret0, _ := ret[0].(*runstream.TFCWorkspacesMetadata) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetWorkspaceMeta indicates an expected call of GetWorkspaceMeta. +func (mr *MockStreamClientMockRecorder) GetWorkspaceMeta(mrID, workspace any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceMeta", reflect.TypeOf((*MockStreamClient)(nil).GetWorkspaceMeta), mrID, workspace) +} + // HealthCheck mocks base method. func (m *MockStreamClient) HealthCheck() error { m.ctrl.T.Helper() diff --git a/pkg/tfc_trigger/tfc_trigger.go b/pkg/tfc_trigger/tfc_trigger.go index 8df6655..8639ca0 100644 --- a/pkg/tfc_trigger/tfc_trigger.go +++ b/pkg/tfc_trigger/tfc_trigger.go @@ -441,7 +441,14 @@ func (t *TFCTrigger) TriggerTFCEvents(ctx context.Context) (*TriggeredTFCWorkspa log.Debug().Msg("No Terraform changes found in changeset.") return nil, nil } - t.runstream.AddWorkspaceMeta(&runstream.TFCWorkspacesMetadata{}, t.GetMergeRequestIID(), t.GetProjectNameWithNamespace()) + //only set workspace metadata for a MR when the first run is triggered. This prevents the count of executed workspaces from being reset + wsMeta, err := t.runstream.GetWorkspaceMeta(fmt.Sprintf("%d", t.GetMergeRequestIID()), t.GetProjectNameWithNamespace()) + if err != nil || wsMeta == nil { + t.runstream.AddWorkspaceMeta(&runstream.TFCWorkspacesMetadata{ + CountTotalWorkspaces: len(triggeredWorkspaces), + CountExecutedWorkspaces: 0, + }, fmt.Sprintf("%d", t.GetMergeRequestIID()), t.GetProjectNameWithNamespace()) + } return workspaceStatus, nil } diff --git a/pkg/tfc_trigger/tfc_trigger_test.go b/pkg/tfc_trigger/tfc_trigger_test.go index fdf429b..4ab13a9 100644 --- a/pkg/tfc_trigger/tfc_trigger_test.go +++ b/pkg/tfc_trigger/tfc_trigger_test.go @@ -2,6 +2,7 @@ package tfc_trigger_test import ( "context" + "errors" "fmt" "os" "testing" @@ -296,6 +297,10 @@ func TestTFCEvents_MultiWorkspaceApply(t *testing.T) { }).Times(2) testSuite.MockStreamClient.EXPECT().AddRunMeta(gomock.Any()).Times(2) + testSuite.MockStreamClient.EXPECT().AddWorkspaceMeta(&runstream.TFCWorkspacesMetadata{ + CountTotalWorkspaces: 2, + CountExecutedWorkspaces: 0, + }, fmt.Sprintf("%d", testSuite.MetaData.MRIID), testSuite.MetaData.ProjectNameNS) testSuite.InitTestSuite() testLogger := zltest.New(t) log.Logger = log.Logger.Output(testLogger) @@ -474,7 +479,11 @@ func TestTFCEvents_WorkspaceApplyModifiedBothSrcDstBranches(t *testing.T) { testSuite.MockGitClient.EXPECT().GetMergeRequestModifiedFiles(gomock.Any(), testSuite.MetaData.MRIID, testSuite.MetaData.ProjectNameNS).Return([]string{"main.tf"}, nil) mockStreamClient := mocks.NewMockStreamClient(mockCtrl) - + mockStreamClient.EXPECT().GetWorkspaceMeta(gomock.Any(), gomock.Any()).Return(nil, errors.New("no record")) + mockStreamClient.EXPECT().AddWorkspaceMeta(&runstream.TFCWorkspacesMetadata{ + CountTotalWorkspaces: 1, + CountExecutedWorkspaces: 0, + }, fmt.Sprintf("%d", testSuite.MetaData.MRIID), testSuite.MetaData.ProjectNameNS) testSuite.InitTestSuite() testLogger := zltest.New(t) diff --git a/pkg/vcs/gitlab/mr_status_updater.go b/pkg/vcs/gitlab/mr_status_updater.go index 35f53eb..68aaea1 100644 --- a/pkg/vcs/gitlab/mr_status_updater.go +++ b/pkg/vcs/gitlab/mr_status_updater.go @@ -208,8 +208,29 @@ func (p *RunStatusUpdater) mergeMRIfPossible(ctx context.Context, rmd runstream. if !rmd.GetAutoMerge() { return } + //check that all triggered workspaces have been executed or increment + wsMeta, err := p.rs.GetWorkspaceMeta(fmt.Sprintf("%d", rmd.GetMRInternalID()), rmd.GetMRProjectNameWithNamespace()) + if err != nil { + log.Debug().AnErr("err", err).Msg("get workspace metadata") + return + } + wsMeta.CountExecutedWorkspaces++ + + if wsMeta.CountTotalWorkspaces > wsMeta.CountExecutedWorkspaces { + err = p.rs.AddWorkspaceMeta(wsMeta, fmt.Sprintf("%d", rmd.GetMRInternalID()), rmd.GetMRProjectNameWithNamespace()) + if err != nil { + log.Debug().AnErr("err", err).Msg("add workspace metadata") + span.RecordError(err) + } + return + } + if wsMeta.CountExecutedWorkspaces > wsMeta.CountTotalWorkspaces { + log.Debug().Msg("count executed workspaces is greater than total workspaces") + span.RecordError(errors.New("count executed workspaces is greater than total workspaces")) + return + } - err := p.client.MergeMR(ctx, rmd.GetMRInternalID(), rmd.GetMRProjectNameWithNamespace()) + err = p.client.MergeMR(ctx, rmd.GetMRInternalID(), rmd.GetMRProjectNameWithNamespace()) if err != nil { span.RecordError(err) } diff --git a/pkg/vcs/gitlab/mr_status_updater_test.go b/pkg/vcs/gitlab/mr_status_updater_test.go index caab988..590f4de 100644 --- a/pkg/vcs/gitlab/mr_status_updater_test.go +++ b/pkg/vcs/gitlab/mr_status_updater_test.go @@ -22,6 +22,10 @@ func TestAutoMergeNoChangesApply(t *testing.T) { testSuite.MockGitClient.EXPECT().MergeMR(gomock.Any(), gomock.Any(), gomock.Any()) testSuite.MockGitClient.EXPECT().GetPipelinesForCommit(gomock.Any(), gomock.Any(), gomock.Any()).Return([]vcs.ProjectPipeline{&GitlabPipeline{&gogitlab.PipelineInfo{ID: 1}}}, nil).AnyTimes() testSuite.MockGitClient.EXPECT().SetCommitStatus(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, errors.New("could not commit status")).AnyTimes() + testSuite.MockStreamClient.EXPECT().GetWorkspaceMeta(gomock.Any(), gomock.Any()).Return(&runstream.TFCWorkspacesMetadata{ + CountExecutedWorkspaces: 0, + CountTotalWorkspaces: 1, + }, nil) testSuite.InitTestSuite() r := &RunStatusUpdater{ tfc: testSuite.MockApiClient, @@ -71,6 +75,12 @@ func TestAutoMergeApply(t *testing.T) { testSuite.MockGitClient.EXPECT().MergeMR(gomock.Any(), gomock.Any(), gomock.Any()) testSuite.MockGitClient.EXPECT().GetPipelinesForCommit(gomock.Any(), gomock.Any(), gomock.Any()).Return([]vcs.ProjectPipeline{&GitlabPipeline{&gogitlab.PipelineInfo{ID: 1}}}, nil).AnyTimes() testSuite.MockGitClient.EXPECT().SetCommitStatus(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, errors.New("could not commit status")).AnyTimes() + + testSuite.MockStreamClient.EXPECT().GetWorkspaceMeta(gomock.Any(), gomock.Any()).Return(&runstream.TFCWorkspacesMetadata{ + CountExecutedWorkspaces: 0, + CountTotalWorkspaces: 1, + }, nil) + testSuite.InitTestSuite() r := &RunStatusUpdater{ tfc: testSuite.MockApiClient, @@ -86,6 +96,54 @@ func TestAutoMergeApply(t *testing.T) { }) } +func TestAutoMergeApplyMultiWorkspace(t *testing.T) { + + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + testSuite := mocks.CreateTestSuite(mockCtrl, mocks.TestOverrides{}, t) + + testSuite.MockGitClient.EXPECT().MergeMR(gomock.Any(), gomock.Any(), gomock.Any()) + testSuite.MockGitClient.EXPECT().GetPipelinesForCommit(gomock.Any(), gomock.Any(), gomock.Any()).Return([]vcs.ProjectPipeline{&GitlabPipeline{&gogitlab.PipelineInfo{ID: 1}}}, nil).AnyTimes() + testSuite.MockGitClient.EXPECT().SetCommitStatus(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, errors.New("could not commit status")).AnyTimes() + + //workspace 1 in same mr + testSuite.MockStreamClient.EXPECT().GetWorkspaceMeta("101", "zapier/test").Return(&runstream.TFCWorkspacesMetadata{ + CountExecutedWorkspaces: 0, + CountTotalWorkspaces: 2, + }, nil) + //workspace 2 in same mr + testSuite.MockStreamClient.EXPECT().GetWorkspaceMeta("101", "zapier/test").Return(&runstream.TFCWorkspacesMetadata{ + CountExecutedWorkspaces: 1, + CountTotalWorkspaces: 2, + }, nil) + + testSuite.InitTestSuite() + r := &RunStatusUpdater{ + tfc: testSuite.MockApiClient, + client: testSuite.MockGitClient, + rs: testSuite.MockStreamClient, + } + r.updateCommitStatusForRun(context.Background(), &tfe.Run{ + Status: tfe.RunApplied, + HasChanges: true, + }, &runstream.TFRunMetadata{ + Action: "apply", + AutoMerge: true, + MergeRequestIID: 101, + MergeRequestProjectNameWithNamespace: "zapier/test", + }) + + r.updateCommitStatusForRun(context.Background(), &tfe.Run{ + Status: tfe.RunApplied, + HasChanges: true, + }, &runstream.TFRunMetadata{ + Action: "apply", + AutoMerge: true, + MergeRequestIID: 101, + MergeRequestProjectNameWithNamespace: "zapier/test", + }) +} func TestAutoMergeTargetedApply(t *testing.T) { mockCtrl := gomock.NewController(t)