diff --git a/Makefile b/Makefile index bfc138f..5ffaf0e 100644 --- a/Makefile +++ b/Makefile @@ -22,6 +22,7 @@ build: test test: generatemocks go test -v -count=1 $(CURDIR)/service/... go test -v -count=1 $(CURDIR)/pkg/... + go test -v -count=1 $(CURDIR)/common/... @rm -rf $(CURDIR)/mocks # Keep all mock file generation below this line @@ -41,11 +42,17 @@ generatemocks: cd $(CURDIR)/repository/rca && minimock -i IRcaRepository -s _mock.go -o $(CURDIR)/mocks cd $(CURDIR)/model/incident && minimock -i IIncidentRepository -s _mock.go -o $(CURDIR)/mocks cd $(CURDIR)/pkg/monitoringService && minimock -i MonitoringServiceActions -s _mock.go -o $(CURDIR)/mocks + cd $(CURDIR)/model/incident && minimock -i IIncidentRepository -s _mock.go -o $(CURDIR)/mocks + cd $(CURDIR)/pkg/monitoringService && minimock -i ServiceActions -s _mock.go -o $(CURDIR)/mocks + cd $(CURDIR)/pkg/monitoringService && minimock -i MonitoringServiceActions -s _mock.go -o $(CURDIR)/mocks + cd $(CURDIR)/common/util/channel && minimock -i IChannelUtil -s _mock.go -o $(CURDIR)/mocks cd $(CURDIR)/service/incident_channel && minimock -i IIncidentChannelService -s _mock.go -o $(CURDIR)/mocks cd $(CURDIR)/model/team && minimock -i ITeamRepository -s _mock.go -o $(CURDIR)/mocks cd $(CURDIR)/model/severity && minimock -i ISeverityRepository -s _mock.go -o $(CURDIR)/mocks - cd $(CURDIR)/model/incident && minimock -i IIncidentRepository -s _mock.go -o $(CURDIR)/mocks cd $(CURDIR)/model/user && minimock -i IUserRepository -s _mock.go -o $(CURDIR)/mocks cd $(CURDIR)/pkg/documentService && minimock -i ServiceActions -s _mock.go -o $(CURDIR)/mocks cd $(CURDIR)/repository/rcaInput && minimock -i IRcaInputRepository -s _mock.go -o $(CURDIR)/mocks cd $(CURDIR)/service/teamService && minimock -i ITeamServiceV2 -s _mock.go -o $(CURDIR)/mocks + cd $(CURDIR)/model/user && minimock -i IUserRepositoryInterface -s _mock.go -o $(CURDIR)/mocks + cd $(CURDIR)/pkg/monitoringService && minimock -i MonitoringServiceActions -s _mock.go -o $(CURDIR)/mocks + cd $(CURDIR)/service/krakatoa && minimock -i IKrakatoaService -s _mock.go -o $(CURDIR)/mocks diff --git a/common/util/channel/channel_util.go b/common/util/channel/channel_util.go new file mode 100644 index 0000000..8d8321c --- /dev/null +++ b/common/util/channel/channel_util.go @@ -0,0 +1,16 @@ +package channel + +import ( + service "houston/service/response" +) + +type ChannelUtil struct { +} + +func (util *ChannelUtil) CreateGrafanaImageResponseChannel() chan service.GrafanaImageFetchResponse { + return make(chan service.GrafanaImageFetchResponse) +} + +func NewChannelUtil() *ChannelUtil { + return &ChannelUtil{} +} diff --git a/common/util/channel/channel_util_interface.go b/common/util/channel/channel_util_interface.go new file mode 100644 index 0000000..2039952 --- /dev/null +++ b/common/util/channel/channel_util_interface.go @@ -0,0 +1,9 @@ +package channel + +import ( + service "houston/service/response" +) + +type IChannelUtil interface { + CreateGrafanaImageResponseChannel() chan service.GrafanaImageFetchResponse +} diff --git a/common/util/common_util.go b/common/util/common_util.go index e46220e..c084e3c 100644 --- a/common/util/common_util.go +++ b/common/util/common_util.go @@ -204,9 +204,9 @@ func PostIncidentSeverityEscalationHeadsUpMessage(severities *[]severity.Severit } } -func ExecuteConcurrentAction(waitGroup *sync.WaitGroup, f func()) { +func ExecuteConcurrentAction(waitGroup *sync.WaitGroup, concurrentTask func()) { defer waitGroup.Done() - f() + concurrentTask() } func getSeverityById(severities *[]severity.SeverityEntity, severityId uint) string { diff --git a/common/util/constant.go b/common/util/constant.go index 6791661..142c963 100644 --- a/common/util/constant.go +++ b/common/util/constant.go @@ -95,3 +95,12 @@ const ( const ( IdParam = "id" ) + +const ( + SingleConcurrentProcessCount = 1 +) + +const ( + ExtensionPNG = ".png" + ExtensionCSV = ".csv" +) diff --git a/common/util/file/file_util.go b/common/util/file/file_util.go index d35d063..845d8ff 100644 --- a/common/util/file/file_util.go +++ b/common/util/file/file_util.go @@ -5,9 +5,11 @@ import ( "go.uber.org/zap" "houston/logger" "os" + "path/filepath" + "strings" ) -func FetchFilesInDirectory(directory string) ([]os.DirEntry, error) { +func FetchFilePathsInDirectoryWithGivenExtension(directory string, validExtensions ...string) ([]string, error) { files, err := os.ReadDir(directory) if err != nil { logger.Error( @@ -16,7 +18,15 @@ func FetchFilesInDirectory(directory string) ([]os.DirEntry, error) { ) return nil, err } - return files, nil + + validFilePaths := []string{} + for _, file := range files { + fileName := file.Name() + if hasValidExtension(fileName, validExtensions...) { + validFilePaths = append(validFilePaths, filepath.Join(directory, fileName)) + } + } + return validFilePaths, nil } func RemoveDirectory(directory string) error { @@ -29,3 +39,12 @@ func RemoveDirectory(directory string) error { } return err } + +func hasValidExtension(fileName string, validExtensions ...string) bool { + for _, ext := range validExtensions { + if strings.HasSuffix(fileName, ext) { + return true + } + } + return false +} diff --git a/common/util/file/file_util_test.go b/common/util/file/file_util_test.go index c662fe7..925618a 100644 --- a/common/util/file/file_util_test.go +++ b/common/util/file/file_util_test.go @@ -1,8 +1,10 @@ package file import ( + "houston/common/util" "houston/logger" "os" + "path/filepath" "testing" "github.com/stretchr/testify/suite" @@ -16,38 +18,94 @@ func (suite *FileUtilSuite) SetupSuite() { logger.InitLogger() } -func (suite *FileUtilSuite) TestFetchFilesInDirectory_Success() { +func (suite *FileUtilSuite) Test_FetchFilePathsInDirectoryWithGivenExtension_AllValidFiles() { // Create a temporary directory for testing - testDir := "test_fetch_files" + testDir := "test_get_valid_file_paths" err := os.Mkdir(testDir, os.ModePerm) suite.NoError(err, "Error creating test directory") - // Create some test files in the temporary directory - _, err = os.Create(testDir + "/file1.txt") + // Create some test files in the temporary directory with different extensions + _, err = os.Create(testDir + "/file1.png") suite.NoError(err, "Error creating test file 1") - _, err = os.Create(testDir + "/file2.txt") + _, err = os.Create(testDir + "/file2.png") suite.NoError(err, "Error creating test file 2") + _, err = os.Create(testDir + "/file3.csv") + suite.NoError(err, "Error creating test file 3") - // Call the function being tested - files, err := FetchFilesInDirectory(testDir) + validFilePaths, err := FetchFilePathsInDirectoryWithGivenExtension(testDir, util.ExtensionPNG, util.ExtensionCSV) // Assertions - suite.NoError(err, "Unexpected error fetching files") - suite.NotNil(files, "Files should not be nil") - suite.Equal(2, len(files), "Unexpected number of files") + suite.NoError(err, "Error fetching valid file paths") + suite.NotNil(validFilePaths, "Valid file paths should not be nil") + suite.Equal(3, len(validFilePaths), "Unexpected number of valid file paths") + suite.ElementsMatch( + validFilePaths, + []string{filepath.Join(testDir, "file1.png"), filepath.Join(testDir, "file2.png"), filepath.Join(testDir, "file3.csv")}, + "Valid file paths for file1.png and file3.csv not found", + ) // Cleanup: remove the temporary directory err = os.RemoveAll(testDir) suite.NoError(err, "Error removing test directory") } -func (suite *FileUtilSuite) TestFetchFilesInDirectory_Failure() { - // Call the function being tested with a non-existent directory - files, err := FetchFilesInDirectory("nonexistent_directory") +func (suite *FileUtilSuite) TestGetValidFilePathsInDirectoryByExtensions_ValidAndInvalidFiles() { + // Create a temporary directory for testing + testDir := "test_get_valid_file_paths" + err := os.Mkdir(testDir, os.ModePerm) + suite.NoError(err, "Error creating test directory") + + // Create some test files in the temporary directory with different extensions + _, err = os.Create(testDir + "/file1.png") + suite.NoError(err, "Error creating test file 1") + _, err = os.Create(testDir + "/file2.txt") + suite.NoError(err, "Error creating test file 2") + _, err = os.Create(testDir + "/file3.csv") + suite.NoError(err, "Error creating test file 3") + + validFilePaths, err := FetchFilePathsInDirectoryWithGivenExtension(testDir, util.ExtensionPNG, util.ExtensionCSV) // Assertions - suite.Error(err, "Expected an error fetching files") - suite.Nil(files, "Files should be nil") + suite.NotNil(validFilePaths, "Valid file paths should not be nil") + suite.Equal(2, len(validFilePaths), "Unexpected number of valid file paths") + suite.ElementsMatch( + validFilePaths, + []string{filepath.Join(testDir, "file1.png"), filepath.Join(testDir, "file3.csv")}, + "Valid file paths for file1.png and file3.csv not found", + ) + + // Cleanup: remove the temporary directory + err = os.RemoveAll(testDir) + suite.NoError(err, "Error removing test directory") +} + +func (suite *FileUtilSuite) TestGetValidFilePathsInDirectoryByExtensions_NoValidFiles() { + // Create a temporary directory for testing + testDir := "test_no_valid_file_paths" + err := os.Mkdir(testDir, os.ModePerm) + suite.NoError(err, "Error creating test directory") + + // Create some test files in the temporary directory with invalid extensions + _, err = os.Create(testDir + "/file1.txt") + suite.NoError(err, "Error creating test file 1") + _, err = os.Create(testDir + "/file2.doc") + suite.NoError(err, "Error creating test file 2") + + validFilePaths, err := FetchFilePathsInDirectoryWithGivenExtension(testDir, util.ExtensionPNG, util.ExtensionCSV) + + // Assertions + suite.NotNil(validFilePaths, "Valid file paths should not be nil") + suite.Empty(validFilePaths, "There should be no valid file paths") + + // Cleanup: remove the temporary directory + err = os.RemoveAll(testDir) + suite.NoError(err, "Error removing test directory") +} + +func (suite *FileUtilSuite) Test_FetchFilePathsInDirectoryWithGivenExtension_NonExistentDirectory() { + testDir := "test_get_valid_file_paths" + _, err := FetchFilePathsInDirectoryWithGivenExtension(testDir, util.ExtensionPNG, util.ExtensionCSV) + suite.Error(err, "Expected an error fetching valid file paths") } func (suite *FileUtilSuite) TestRemoveDirectory_Success() { diff --git a/common/util/incident_helper.go b/common/util/incident_helper.go index 3319a64..7f60318 100644 --- a/common/util/incident_helper.go +++ b/common/util/incident_helper.go @@ -100,3 +100,13 @@ func UpdateIncidentWithConferenceDetails(incidentEntity *incident.IncidentEntity logger.Error(fmt.Sprintf("Unable to update incident %s with conference details due to error: %s", incidentEntity.IncidentName, err.Error())) } } + +func GetSlackChannelNamesFromIncidentEntities(incidents *[]incident.IncidentEntity) []string { + var incidentNames []string + if incidents != nil { + for _, incident := range *incidents { + incidentNames = append(incidentNames, incident.IncidentName) + } + } + return incidentNames +} diff --git a/internal/processor/action/incident_update_type_action.go b/internal/processor/action/incident_update_type_action.go index 7426865..e39862c 100644 --- a/internal/processor/action/incident_update_type_action.go +++ b/internal/processor/action/incident_update_type_action.go @@ -13,6 +13,7 @@ import ( "houston/model/severity" "houston/model/team" "houston/pkg/slackbot" + incidentV2 "houston/service/incident" "strconv" ) @@ -22,15 +23,17 @@ type IncidentUpdateTypeAction struct { incidentRepository *incident.Repository severityRepository *severity.Repository slackbotClient *slackbot.Client + incidentServiceV2 *incidentV2.IncidentServiceV2 } -func NewIncidentUpdateTypeAction(client *socketmode.Client, incidentService *incident.Repository, teamService *team.Repository, severityService *severity.Repository, slackbotClient *slackbot.Client) *IncidentUpdateTypeAction { +func NewIncidentUpdateTypeAction(client *socketmode.Client, incidentService *incident.Repository, teamService *team.Repository, severityService *severity.Repository, slackbotClient *slackbot.Client, incidentServiceV2 *incidentV2.IncidentServiceV2) *IncidentUpdateTypeAction { return &IncidentUpdateTypeAction{ socketModeClient: client, teamRepository: teamService, incidentRepository: incidentService, severityRepository: severityService, slackbotClient: slackbotClient, + incidentServiceV2: incidentServiceV2, } } @@ -144,6 +147,8 @@ func (incidentUpdateTypeAction *IncidentUpdateTypeAction) IncidentUpdateType(cal } topic := fmt.Sprintf("%s-%s(%s) Incident-%d | %s", teamEntity.Name, severityEntity.Name, severityEntity.Description, incidentEntity.ID, incidentEntity.Title) incidentUpdateTypeAction.slackbotClient.SetChannelTopic(callback.View.PrivateMetadata, topic) + + incidentUpdateTypeAction.incidentServiceV2.ExecuteKrakatoaWorkflow(incidentEntity) }() var payload interface{} diff --git a/internal/processor/event_type_interactive_processor.go b/internal/processor/event_type_interactive_processor.go index 7199c8d..f5c234c 100644 --- a/internal/processor/event_type_interactive_processor.go +++ b/internal/processor/event_type_interactive_processor.go @@ -70,7 +70,7 @@ func NewBlockActionProcessor( incidentUpdateAction: action.NewIncidentUpdateAction(socketModeClient, incidentRepository, tagService, teamService, severityService), incidentUpdateTypeAction: action.NewIncidentUpdateTypeAction(socketModeClient, incidentRepository, - teamService, severityService, slackbotClient), + teamService, severityService, slackbotClient, incidentServiceV2), incidentUpdateSeverityAction: action.NewIncidentUpdateSeverityAction(socketModeClient, incidentRepository, severityService, teamService, slackbotClient, incidentServiceV2), incidentUpdateTitleAction: action.NewIncidentUpdateTitleAction(socketModeClient, incidentRepository, @@ -269,7 +269,7 @@ func NewViewSubmissionProcessor( incidentUpdateSeverityAction: action.NewIncidentUpdateSeverityAction(socketModeClient, incidentRepository, severityService, teamService, slackbotClient, incidentServiceV2), incidentUpdateTypeAction: action.NewIncidentUpdateTypeAction(socketModeClient, incidentRepository, - teamService, severityService, slackbotClient), + teamService, severityService, slackbotClient, incidentServiceV2), incidentUpdateTagsAction: action.NewIncidentUpdateTagsAction(socketModeClient, incidentRepository, teamService, tagService), incidentUpdateRca: action.NewIncidentUpdateRcaAction(socketModeClient, incidentRepository), diff --git a/model/incident/incident.go b/model/incident/incident.go index 5891e11..7c54cbb 100644 --- a/model/incident/incident.go +++ b/model/incident/incident.go @@ -2,6 +2,7 @@ package incident import ( "encoding/json" + "errors" "fmt" "github.com/slack-go/slack/socketmode" "github.com/spf13/viper" @@ -629,6 +630,38 @@ func (r *Repository) GetIncidentRoleByIncidentIdAndRole(incident_id uint, role s return &incidentRoleEntity, nil } +func (r *Repository) GetOpenIncidentsByCreatorIdForGivenTeam(created_by string, teamId uint) (*[]IncidentEntity, error) { + var incidentEntities []IncidentEntity + + incidentStatuses, err := r.FetchAllNonTerminalIncidentStatuses() + + if err != nil { + logger.Error("Error in fetching non terminal incident statuses", zap.Error(err)) + return nil, err + } + + if incidentStatuses == nil { + err := errors.New("Could not find any non terminal incident statuses") + logger.Error("Error in fetching non terminal incident statuses", zap.Error(err)) + return nil, err + } + + var statusIds []uint + + for _, status := range *incidentStatuses { + statusIds = append(statusIds, status.ID) + } + + result := r.gormClient.Find(&incidentEntities, "created_by = ? AND team_id = ? AND status IN ?", created_by, teamId, statusIds) + + if result.Error != nil { + logger.Error(fmt.Sprintf("Error in fetching open incidents created by %s and for team %d", created_by, teamId), zap.Error(err)) + return nil, result.Error + } + + return &incidentEntities, nil +} + func (r *Repository) FindOpenIncidentsByTeamOrderedByCreationTimeAndSeverity(team string) (*[]IncidentEntity, error) { var incidentEntity []IncidentEntity query := fmt.Sprintf("SELECT * FROM incident WHERE team_id = %v AND status NOT IN (4, 5) AND deleted_at IS NULL ORDER BY severity_id, id", team) diff --git a/model/incident/incident_repository_interface.go b/model/incident/incident_repository_interface.go index c391d38..97700d1 100644 --- a/model/incident/incident_repository_interface.go +++ b/model/incident/incident_repository_interface.go @@ -39,4 +39,5 @@ type IIncidentRepository interface { FetchAllOpenIncidentsWithSeverityTATGreaterThan(slaStart, slaEnd string, severityId ...int) (*[]IncidentEntity, error) GetUnArchivedAndTerminatedIncidentChannels() ([]IncidentChannelEntity, error) UpdateIncidentChannelEntity(incidentChannelEntity *IncidentChannelEntity) error + GetOpenIncidentsByCreatorIdForGivenTeam(created_by string, teamId uint) (*[]IncidentEntity, error) } diff --git a/pkg/monitoringService/monitoring_service_client_interface.go b/pkg/monitoringService/monitoring_service_client_interface.go index d778708..19734a0 100644 --- a/pkg/monitoringService/monitoring_service_client_interface.go +++ b/pkg/monitoringService/monitoring_service_client_interface.go @@ -1,5 +1,10 @@ package monitoringService -type ServiceActions interface { - GetGrafanaImages(incidents string) (string, error) +import ( + service "houston/service/response" +) + +type MonitoringServiceActions interface { + GetGrafanaImages(incidents []string, requestId string, + responseChannel chan service.GrafanaImageFetchResponse) } diff --git a/service/incident/incident_service_test.go b/service/incident/incident_service_test.go index 0db0220..dfe5ca1 100644 --- a/service/incident/incident_service_test.go +++ b/service/incident/incident_service_test.go @@ -5,7 +5,9 @@ import ( "github.com/google/uuid" "github.com/lib/pq" "github.com/slack-go/slack" + "github.com/spf13/viper" "github.com/stretchr/testify/suite" + "go.uber.org/zap" "gorm.io/gorm" "houston/logger" "houston/mocks" @@ -14,6 +16,8 @@ import ( "houston/model/severity" "houston/model/team" service "houston/service/request" + "os" + "path/filepath" "testing" "time" ) @@ -21,12 +25,16 @@ import ( type IncidentServiceSuite struct { suite.Suite slackService mocks.ISlackServiceMock - incidentChannelService mocks.IIncidentChannelServiceMock - teamRepository mocks.ITeamRepositoryMock - severityRepository mocks.ISeverityRepositoryMock - incidentRepository mocks.IIncidentRepositoryMock - userRepository mocks.IUserRepositoryMock incidentService *IncidentServiceV2 + incidentRepository mocks.IIncidentRepositoryMock + teamRepository mocks.ITeamRepositoryMock + userRepository mocks.IUserRepositoryMock + severityRepository mocks.ISeverityRepositoryMock + krakatoaService mocks.IKrakatoaServiceMock + incidentChannelService mocks.IIncidentChannelServiceMock + mockDirectory string + mockPngName string + mockCsvName string mockIncidentId uint mockUserEmail string previousSeverityId uint @@ -37,32 +45,78 @@ type IncidentServiceSuite struct { updatedTeamId uint } -func (suite *IncidentServiceSuite) SetupTest() { - logger.InitLogger() - suite.slackService = *mocks.NewISlackServiceMock(suite.T()) - suite.incidentChannelService = *mocks.NewIIncidentChannelServiceMock(suite.T()) - suite.teamRepository = *mocks.NewITeamRepositoryMock(suite.T()) - suite.severityRepository = *mocks.NewISeverityRepositoryMock(suite.T()) - suite.incidentRepository = *mocks.NewIIncidentRepositoryMock(suite.T()) - suite.userRepository = *mocks.NewIUserRepositoryMock(suite.T()) - suite.mockIncidentId = 1 - suite.mockUserEmail = "testemail@testemail.com" - suite.previousStatusId = 1 - suite.updatedStatusId = 2 - suite.previousSeverityId = 4 - suite.updatedSeverityId = 3 - suite.previousTeamId = 1 - suite.updatedTeamId = 2 +func (suite *IncidentServiceSuite) Test_KrakatoaFlow_SuccessCase() { + createDirectoryWithDummyFiles(suite.mockDirectory, suite.mockPngName, suite.mockCsvName) + channel := "testId" + incidents := []incident.IncidentEntity{*GetMockIncident(), *GetMockIncident()} - suite.incidentService = &IncidentServiceV2{ - db: nil, - slackService: &suite.slackService, - incidentChannelService: &suite.incidentChannelService, - teamRepository: &suite.teamRepository, - severityRepository: &suite.severityRepository, - incidentRepository: &suite.incidentRepository, - userRepository: &suite.userRepository, - } + suite.incidentRepository.GetOpenIncidentsByCreatorIdForGivenTeamMock.Return(&incidents, nil) + + suite.krakatoaService.GetGrafanaImagesMock.Return(suite.mockDirectory, nil) + + suite.slackService.UploadFileByPathMock.When(filepath.Join(suite.mockDirectory, suite.mockCsvName), channel). + Then(nil, nil) + + suite.slackService.UploadFilesToChannelMock.Return() + + err := suite.incidentService.ExecuteKrakatoaWorkflow(GetMockIncident()) + suite.Equal(err, nil) + //assert that directory cleanup is done after posting files to slack channel + _, err = os.ReadDir(suite.mockDirectory) + suite.Error(err) +} + +func (suite *IncidentServiceSuite) Test_KrakatoaFlow_IncidentRepoDBErrorCase() { + suite.incidentRepository.GetOpenIncidentsByCreatorIdForGivenTeamMock.Return(nil, errors.New("Some error occured")) + err := suite.incidentService.ExecuteKrakatoaWorkflow(GetMockIncident()) + suite.Error(err) +} + +func (suite *IncidentServiceSuite) Test_KrakatoaFlow_NoKrakatoaIncidentsCase() { + suite.incidentRepository.GetOpenIncidentsByCreatorIdForGivenTeamMock.Return(nil, nil) + err := suite.incidentService.ExecuteKrakatoaWorkflow(GetMockIncident()) + suite.Error(err) +} + +func (suite *IncidentServiceSuite) Test_KrakatoaFlow_MonitoringServiceFailureCase() { + incidents := []incident.IncidentEntity{*GetMockIncident(), *GetMockIncident()} + + suite.incidentRepository.GetOpenIncidentsByCreatorIdForGivenTeamMock.Return(&incidents, nil) + + suite.krakatoaService.GetGrafanaImagesMock.Return("", errors.New("S3 failure")) + suite.slackService.PostMessageByChannelIDMock.Return("", nil) + + err := suite.incidentService.ExecuteKrakatoaWorkflow(GetMockIncident()) + suite.Error(err) +} + +func (suite *IncidentServiceSuite) Test_KrakatoaFlow_MonitoringServiceTimeoutCase() { + incidents := []incident.IncidentEntity{*GetMockIncident(), *GetMockIncident()} + + suite.incidentRepository.GetOpenIncidentsByCreatorIdForGivenTeamMock.Return(&incidents, nil) + + suite.krakatoaService.GetGrafanaImagesMock.Return("", errors.New("Monitoring service timed out")) + suite.slackService.PostMessageByChannelIDMock.Return("", nil) + + err := suite.incidentService.ExecuteKrakatoaWorkflow(GetMockIncident()) + suite.Error(err) +} + +func (suite *IncidentServiceSuite) Test_FetchAndPostFilesToSlack_SuccessCase() { + createDirectoryWithDummyFiles(suite.mockDirectory, suite.mockPngName, suite.mockCsvName) + channel := "testId" + suite.slackService.UploadFilesToChannelMock.Return() + err := suite.incidentService.FetchAndPostFilesToSlack(suite.mockDirectory, channel) + suite.Equal(err, nil) + //assert that directory cleanup is done after posting files to slack channel + _, err = os.ReadDir(suite.mockDirectory) + suite.Error(err) +} + +func (suite *IncidentServiceSuite) Test_FetchAndPostFilesToSlack_NonExistingDirectory() { + channel := "testId" + err := suite.incidentService.FetchAndPostFilesToSlack(suite.mockDirectory, channel) + suite.Error(err) } func (suite *IncidentServiceSuite) Test_UpdateIncident_Success() { @@ -76,6 +130,7 @@ func (suite *IncidentServiceSuite) Test_UpdateIncident_Success() { mockUpdatedStatus := GetMockStatusWithId(suite.updatedStatusId) mockChannels := GetMockChannels() mockMetaData := getMockMetaData() + krakatoaIncidents := []incident.IncidentEntity{*GetMockIncident(), *GetMockIncident()} suite.incidentRepository.FindIncidentByIdMock.When(suite.mockIncidentId). Then(mockIncident, nil) @@ -123,6 +178,14 @@ func (suite *IncidentServiceSuite) Test_UpdateIncident_Success() { suite.slackService.PostMessageOptionMock.Return("", nil) + suite.incidentRepository.GetOpenIncidentsByCreatorIdForGivenTeamMock.Return(&krakatoaIncidents, nil) + + suite.krakatoaService.GetGrafanaImagesMock.Return(suite.mockDirectory, nil) + + suite.slackService.UploadFileByPathMock.Return(nil, nil) + + suite.slackService.UploadFilesToChannelMock.Return() + _, err := suite.incidentService.UpdateIncident( service.UpdateIncidentRequest{ Id: suite.mockIncidentId, @@ -438,6 +501,64 @@ func getMockMetaData() *service.CreateIncidentMetaData { } } +func createDirectoryWithDummyFiles(mockDirectory, mockPngName, mockCsvName string) { + err := os.MkdirAll(mockDirectory, os.ModePerm) + if err != nil { + logger.Error("Error while creating dummy directory", zap.Error(err)) + return + } + + // Create empty PNG file + pngFile, err := os.Create(filepath.Join(mockDirectory, mockPngName)) + if err != nil { + logger.Error("Error while creating PNG file", zap.Error(err)) + return + } + pngFile.Close() + + // Create empty CSV file + csvFile, err := os.Create(filepath.Join(mockDirectory, mockCsvName)) + if err != nil { + logger.Error("Error while creating CSV file", zap.Error(err)) + return + } + csvFile.Close() +} + +func (suite *IncidentServiceSuite) SetupTest() { + logger.InitLogger() + viper.Set("MONITORING_SERVICE_TIMEOUT_IN_SECONDS", 3) + suite.slackService = *mocks.NewISlackServiceMock(suite.T()) + suite.incidentRepository = *mocks.NewIIncidentRepositoryMock(suite.T()) + suite.krakatoaService = *mocks.NewIKrakatoaServiceMock(suite.T()) + suite.severityRepository = *mocks.NewISeverityRepositoryMock(suite.T()) + suite.teamRepository = *mocks.NewITeamRepositoryMock(suite.T()) + suite.userRepository = *mocks.NewIUserRepositoryMock(suite.T()) + suite.incidentChannelService = *mocks.NewIIncidentChannelServiceMock(suite.T()) + suite.mockIncidentId = 1 + suite.mockUserEmail = "testemail@testemail.com" + suite.previousStatusId = 1 + suite.updatedStatusId = 2 + suite.previousSeverityId = 4 + suite.updatedSeverityId = 3 + suite.previousTeamId = 1 + suite.updatedTeamId = 2 + suite.incidentService = &IncidentServiceV2{ + db: nil, + slackService: &suite.slackService, + teamRepository: &suite.teamRepository, + severityRepository: &suite.severityRepository, + incidentRepository: &suite.incidentRepository, + userRepository: &suite.userRepository, + incidentChannelService: &suite.incidentChannelService, + krakatoaService: &suite.krakatoaService, + } + + suite.mockDirectory = "test_file" + suite.mockPngName = "/dummy.png" + suite.mockCsvName = "/dummy.csv" +} + func TestIncidentService(t *testing.T) { suite.Run(t, new(IncidentServiceSuite)) } diff --git a/service/incident/incident_service_v2.go b/service/incident/incident_service_v2.go index 767a06a..7d25598 100644 --- a/service/incident/incident_service_v2.go +++ b/service/incident/incident_service_v2.go @@ -10,8 +10,9 @@ import ( "go.uber.org/zap" "gorm.io/gorm" "houston/common/util" + "houston/common/util/file" "houston/internal/processor/action/view" - logger "houston/logger" + "houston/logger" "houston/model/incident" "houston/model/incident_channel" "houston/model/log" @@ -21,6 +22,7 @@ import ( conference2 "houston/pkg/conference" service2 "houston/service/conference" incidentChannel "houston/service/incident_channel" + "houston/service/krakatoa" request "houston/service/request" service "houston/service/response" "houston/service/slack" @@ -39,6 +41,7 @@ type IncidentServiceV2 struct { severityRepository severity.ISeverityRepository incidentRepository incident.IIncidentRepository userRepository user.IUserRepository + krakatoaService krakatoa.IKrakatoaService } /* @@ -54,14 +57,16 @@ func NewIncidentServiceV2(db *gorm.DB) *IncidentServiceV2 { db, severityRepository, logRepository, teamRepository, slackService.SocketModeClient, ) incidentChannelService := incidentChannel.NewIncidentChannelService(db) + krakatoaService := krakatoa.NewKrakatoaService() return &IncidentServiceV2{ db: db, slackService: slackService, - incidentChannelService: incidentChannelService, teamRepository: teamRepository, severityRepository: severityRepository, incidentRepository: incidentRepository, userRepository: userRepository, + incidentChannelService: incidentChannelService, + krakatoaService: krakatoaService, } } @@ -149,6 +154,7 @@ func (i *IncidentServiceV2) CreateIncident( if err != nil { return } + i.ExecuteKrakatoaWorkflow(incidentEntity) }() go postInWebhookSlackChannel(i, teamEntity, incidentEntity, severityEntity) @@ -436,6 +442,53 @@ func createIncidentWorkflow( return nil } +func (i *IncidentServiceV2) ExecuteKrakatoaWorkflow(incidentEntity *incident.IncidentEntity) error { + if incidentEntity.CreatedBy == viper.GetString("KRAKATOA_BOT_ID") { + err := errors.New("Cannot execute krakatoa workflow on krakatoa incidents") + logger.Error("Invalid incident", zap.Error(err)) + return err + } + + krakatoaIncidents, err := i.incidentRepository.GetOpenIncidentsByCreatorIdForGivenTeam( + viper.GetString("KRAKATOA_BOT_ID"), + incidentEntity.TeamId, + ) + + if err != nil { + logger.Error( + fmt.Sprintf("Error while fetching open krakatoa incidents for team id: %d", incidentEntity.TeamId), + zap.Error(err), + ) + return err + } + + if krakatoaIncidents == nil || len(*krakatoaIncidents) == 0 { + err := errors.New(fmt.Sprintf("No open krakatoa incidents found for team id: %d", incidentEntity.TeamId)) + logger.Error( + fmt.Sprintf("Error while fetching open krakatoa incidents for team id: %d", incidentEntity.TeamId), + zap.Error(err), + ) + return err + } + + slackChannels := util.GetSlackChannelNamesFromIncidentEntities(krakatoaIncidents) + + directory, err := i.krakatoaService.GetGrafanaImages(slackChannels) + + if err != nil { + logger.Error("Error while fetching grafana images", zap.Error(err)) + i.slackService.PostMessageByChannelID("`Some issue occurred while getting Grafana images`", false, incidentEntity.SlackChannel) + return err + } + + err = i.FetchAndPostFilesToSlack(directory, incidentEntity.SlackChannel) + if err != nil { + logger.Error("Error while posting files to slack channel", zap.Error(err)) + return err + } + return nil +} + /* Builds incident summary message blocks and posts incident summary to Blaze Group channel and incident channel */ @@ -1335,6 +1388,7 @@ func (i *IncidentServiceV2) UpdateTeamId( logger.Error(fmt.Sprintf("%s error in update team id workflow", updateLogTag), zap.Error(err)) return err } + go i.ExecuteKrakatoaWorkflow(incidentEntity) } } return nil @@ -1565,3 +1619,30 @@ func (i *IncidentServiceV2) addDefaultUsersToIncident( return nil } + +func (i *IncidentServiceV2) FetchAndPostFilesToSlack( + directory string, + channelId string, +) error { + filePaths, err := file.FetchFilePathsInDirectoryWithGivenExtension(directory, util.ExtensionPNG, util.ExtensionCSV) + if err != nil { + logger.Error( + fmt.Sprintf("%s Error occurred while fetching files in directory: %s", logTag, directory), + zap.Error(err), + ) + return err + } + + i.slackService.UploadFilesToChannel(filePaths, channelId) + + err = file.RemoveDirectory(directory) + if err != nil { + logger.Error( + fmt.Sprintf("%s Error occurred while removing directory: %s", logTag, directory), + zap.Error(err), + ) + return err + } + + return nil +} diff --git a/service/incident_service.go b/service/incident_service.go index 1bd783e..c470404 100644 --- a/service/incident_service.go +++ b/service/incident_service.go @@ -14,6 +14,7 @@ import ( "houston/model/team" "houston/model/user" "houston/pkg/slackbot" + incidentV2 "houston/service/incident" request "houston/service/request" service "houston/service/response" common "houston/service/response/common" @@ -43,6 +44,7 @@ type incidentService struct { userRepository *user.Repository messageUpdateAction *action.IncidentChannelMessageUpdateAction slackbotClient *slackbot.Client + incidentServiceV2 *incidentV2.IncidentServiceV2 } func NewIncidentService(gin *gin.Engine, db *gorm.DB, socketModeClient *socketmode.Client) *incidentService { @@ -54,6 +56,7 @@ func NewIncidentService(gin *gin.Engine, db *gorm.DB, socketModeClient *socketmo messageUpdateAction := action.NewIncidentChannelMessageUpdateAction( socketModeClient, incidentRepository, teamRepository, severityRepository) slackBot := slackbot.NewSlackClient(socketModeClient) + incidentServiceV2 := incidentV2.NewIncidentServiceV2(db) return &incidentService{ gin: gin, db: db, @@ -64,6 +67,7 @@ func NewIncidentService(gin *gin.Engine, db *gorm.DB, socketModeClient *socketmo userRepository: userRepository, messageUpdateAction: messageUpdateAction, slackbotClient: slackBot, + incidentServiceV2: incidentServiceV2, } } @@ -397,6 +401,8 @@ func (i *incidentService) CreateIncident(c *gin.Context) { logger.Error("[Create incident] Error while assigning responder to the incident ", zap.Error(err)) } + go i.incidentServiceV2.ExecuteKrakatoaWorkflow(incidentEntity) + if incidentEntity.SeverityId >= 3 && incidentEntity.Status <= 4 { slaDate := time.Now().AddDate(0, 0, severityEntity.Sla).Format("02 Jan 2006") message := fmt.Sprintf("SLA for this incident is `%v day(s)` and will be closed by `%s`", severityEntity.Sla, @@ -620,6 +626,9 @@ func (i *incidentService) UpdateIncident(c *gin.Context) { topic := fmt.Sprintf("%s-%s(%s) Incident-%d | %s", teamEntity.Name, incidentSeverityEntity.Name, incidentSeverityEntity.Description, incidentEntity.ID, incidentEntity.Title) i.socketModeClient.SetTopicOfConversation(incidentEntity.SlackChannel, topic) + if !util.IsBlank(updateIncidentRequest.TeamId) { + go i.incidentServiceV2.ExecuteKrakatoaWorkflow(incidentEntity) + } } c.JSON(http.StatusOK, common.SuccessResponse(incidentEntity.SlackChannel, http.StatusOK)) diff --git a/service/krakatoa/krakatoa_service.go b/service/krakatoa/krakatoa_service.go new file mode 100644 index 0000000..639b746 --- /dev/null +++ b/service/krakatoa/krakatoa_service.go @@ -0,0 +1,47 @@ +package krakatoa + +import ( + "errors" + "github.com/google/uuid" + "github.com/spf13/viper" + "go.uber.org/zap" + "houston/common/util/channel" + "houston/logger" + "houston/pkg/monitoringService" + "houston/pkg/rest" + monitoringServiceImpl "houston/service/monitoringService" + "time" +) + +type KrakatoaService struct { + monitoringService monitoringService.MonitoringServiceActions + channelUtil channel.IChannelUtil +} + +func NewKrakatoaService() *KrakatoaService { + return &KrakatoaService{ + monitoringService: monitoringServiceImpl.NewMonitoringServiceActionsImpl(rest.NewHttpRestClient()), + channelUtil: channel.NewChannelUtil(), + } +} + +func (service *KrakatoaService) GetGrafanaImages(slackChannels []string) (path string, err error) { + requestId := uuid.New().String() + + responseChannel := service.channelUtil.CreateGrafanaImageResponseChannel() + + go service.monitoringService.GetGrafanaImages(slackChannels, requestId, responseChannel) + + select { + case response := <-responseChannel: + if response.Error != nil { + logger.Error("Error while fetching grafana images", zap.Error(response.Error)) + return "", response.Error + } + return response.DirectoryPath, nil + case <-time.After(viper.GetDuration("MONITORING_SERVICE_TIMEOUT_IN_SECONDS") * time.Second): + err := errors.New("Monitoring Serice response timed out") + logger.Error("Error while fetching grafana images", zap.Error(err)) + return "", err + } +} diff --git a/service/krakatoa/krakatoa_service_interface.go b/service/krakatoa/krakatoa_service_interface.go new file mode 100644 index 0000000..20cf83e --- /dev/null +++ b/service/krakatoa/krakatoa_service_interface.go @@ -0,0 +1,5 @@ +package krakatoa + +type IKrakatoaService interface { + GetGrafanaImages(slackChannels []string) (path string, err error) +} diff --git a/service/krakatoa/krakatoa_service_test.go b/service/krakatoa/krakatoa_service_test.go new file mode 100644 index 0000000..aae4879 --- /dev/null +++ b/service/krakatoa/krakatoa_service_test.go @@ -0,0 +1,86 @@ +package krakatoa + +import ( + "errors" + "github.com/spf13/viper" + "github.com/stretchr/testify/suite" + "houston/logger" + "houston/mocks" + serviceResponse "houston/service/response" + "testing" +) + +type KrakatoaServiceSuite struct { + suite.Suite + KrakatoaService *KrakatoaService + monitoringService mocks.MonitoringServiceActionsMock + channelUtil mocks.IChannelUtilMock + mockDirectory string +} + +func (suite *KrakatoaServiceSuite) Test_GetGrafanaImages_SuccessCase() { + grafanaImagesChannel := make(chan serviceResponse.GrafanaImageFetchResponse) + + suite.channelUtil.CreateGrafanaImageResponseChannelMock.Return(grafanaImagesChannel) + + go func() { + grafanaImagesChannel <- serviceResponse.GrafanaImageFetchResponse{ + DirectoryPath: suite.mockDirectory, + } + }() + + suite.monitoringService.GetGrafanaImagesMock.Return() + + path, err := suite.KrakatoaService.GetGrafanaImages([]string{"testChannel"}) + + suite.Nil(err) + suite.Equal(suite.mockDirectory, path) +} + +func (suite *KrakatoaServiceSuite) Test_GetGrafanaImages_MonitoringServiceErrorCase() { + grafanaImagesChannel := make(chan serviceResponse.GrafanaImageFetchResponse) + + suite.channelUtil.CreateGrafanaImageResponseChannelMock.Return(grafanaImagesChannel) + + go func() { + grafanaImagesChannel <- serviceResponse.GrafanaImageFetchResponse{ + Error: errors.New("Some error occurred"), + } + }() + + suite.monitoringService.GetGrafanaImagesMock.Return() + + path, err := suite.KrakatoaService.GetGrafanaImages([]string{"testChannel"}) + + suite.Empty(path) + suite.Error(err) +} + +func (suite *KrakatoaServiceSuite) Test_GetGrafanaImages_MonitoringServiceTimeoutCase() { + grafanaImagesChannel := make(chan serviceResponse.GrafanaImageFetchResponse) + + suite.channelUtil.CreateGrafanaImageResponseChannelMock.Return(grafanaImagesChannel) + + suite.monitoringService.GetGrafanaImagesMock.Return() + + path, err := suite.KrakatoaService.GetGrafanaImages([]string{"testChannel"}) + + suite.Empty(path) + suite.Error(err) +} + +func (suite *KrakatoaServiceSuite) SetupTest() { + logger.InitLogger() + viper.Set("MONITORING_SERVICE_TIMEOUT_IN_SECONDS", 3) + suite.monitoringService = *mocks.NewMonitoringServiceActionsMock(suite.T()) + suite.channelUtil = *mocks.NewIChannelUtilMock(suite.T()) + suite.mockDirectory = "testDir" + suite.KrakatoaService = &KrakatoaService{ + monitoringService: &suite.monitoringService, + channelUtil: &suite.channelUtil, + } +} + +func TestKrakatoaService(t *testing.T) { + suite.Run(t, new(KrakatoaServiceSuite)) +} diff --git a/service/slack/slack_service.go b/service/slack/slack_service.go index 7be81fb..0ff398e 100644 --- a/service/slack/slack_service.go +++ b/service/slack/slack_service.go @@ -7,6 +7,7 @@ import ( "github.com/slack-go/slack/socketmode" "github.com/spf13/viper" "go.uber.org/zap" + "houston/common/util" "houston/logger" "houston/model/incident" "houston/model/severity" @@ -15,6 +16,7 @@ import ( "os" "strconv" "strings" + "sync" ) type SlackService struct { @@ -50,6 +52,37 @@ func (s *SlackService) PostMessage(message string, escape bool, channel *slack.C return timeStamp, nil } +func (s *SlackService) UploadFilesToChannel(filePaths []string, channel string) { + var waitGroup sync.WaitGroup + for _, filePath := range filePaths { + waitGroup.Add(util.SingleConcurrentProcessCount) + go func(uploadFilePath string) { + util.ExecuteConcurrentAction(&waitGroup, func() { + _, err := s.UploadFileByPath(uploadFilePath, channel) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to upload filePath %s to channel %s", logTag, uploadFilePath, channel), zap.Error(err)) + } + }) + }(filePath) + } + waitGroup.Wait() +} + +func (s *SlackService) UploadFileByPath(filePath string, channel string) (file *slack.File, err error) { + file, err = s.SocketModeClient.UploadFile(slack.FileUploadParameters{ + File: filePath, + Channels: []string{channel}, + }) + + if err != nil { + e := fmt.Sprintf("%s Failed to upload file into channels:", logTag) + logger.Error(e, zap.Error(err)) + return nil, fmt.Errorf("%s - %+v", e, err) + } + + return file, nil +} + func (s *SlackService) AddBookmark(bookmark string, channel *slack.Channel, title string) { bookmarkParam := slack.AddBookmarkParameters{Link: bookmark, Title: title} _, err := s.SocketModeClient.AddBookmark(channel.ID, bookmarkParam) diff --git a/service/slack/slack_service_interface.go b/service/slack/slack_service_interface.go index 976adda..bf160fa 100644 --- a/service/slack/slack_service_interface.go +++ b/service/slack/slack_service_interface.go @@ -39,5 +39,7 @@ type ISlackService interface { severityEntity *severity.SeverityEntity, incidentEntity *incident.IncidentEntity, ) error + UploadFilesToChannel(files []string, channel string) + UploadFileByPath(filePath string, channels string) (file *slack.File, err error) GetConversationInfo(channelId string) (*slack.Channel, error) }