From fd2a977e828b93b991f32896e0e51369a6f9f6f1 Mon Sep 17 00:00:00 2001 From: Vijay Joshi Date: Tue, 17 Sep 2024 10:01:07 +0530 Subject: [PATCH] INFRA-3705 : Creation of RCA ticket for sev-0 non-escalated non-CX incidents (#456) * INFRA-3705 : Creation of RCA ticket for sev-0 non-escalated non-CX incidents * INFRA-3705 : Self review * INFRA-3705 : Fix tests: * INFRA-3705 : UT's and minor constant changes * INFRA-3705 : Add migration script --- Makefile | 2 + appcontext/app.go | 27 ++- cmd/app/handler/slack_handler.go | 2 +- cmd/app/server.go | 5 +- common/jira/jira.go | 44 ++++ common/util/constant.go | 6 + ...0033_add_team_and_jira_type_columns.up.sql | 7 + .../action/incident_jira_links_action.go | 9 +- model/externalTeam/entity.go | 16 ++ model/externalTeam/model.go | 10 +- model/incident_jira/entity.go | 1 + .../incident_jira/incident_jira_repository.go | 3 +- .../incident_jira_repository_impl.go | 24 ++- model/log/log.go | 24 +++ model/team/entity.go | 2 + model/team/model.go | 1 + .../dto/request/create_jira_ticket.go | 41 ++++ .../response/create_jira_ticket_response.go | 7 + pkg/atlassian/jira_client.go | 5 + pkg/atlassian/jira_client_impl.go | 165 ++++++++++++++- pkg/atlassian/jira_client_test.go | 190 ++++++++++++++++++ pkg/rest/http_client.go | 27 +++ .../external_team_service_impl.go | 30 +++ .../external_team_service_interface.go | 14 ++ .../external_team_service_test.go | 65 ++++++ service/incident/impl/incident_service_v2.go | 29 ++- .../incident/impl/incident_update_status.go | 44 +++- .../incident_jira/incident_jira_service.go | 19 +- .../incident_jira_service_impl.go | 79 +++++++- .../incident_jira_service_test.go | 151 +++++++++++++- service/{ => log}/log_service.go | 20 +- service/rca/impl/rca_service.go | 48 +++-- service/rca/impl/rca_service_test.go | 94 ++++----- service/request/team/add_team.go | 1 + service/teamService/team_service_v2.go | 1 + 35 files changed, 1106 insertions(+), 107 deletions(-) create mode 100644 db/migration/000033_add_team_and_jira_type_columns.up.sql create mode 100644 pkg/atlassian/dto/request/create_jira_ticket.go create mode 100644 pkg/atlassian/dto/response/create_jira_ticket_response.go create mode 100644 service/externalTeam/external_team_service_impl.go create mode 100644 service/externalTeam/external_team_service_interface.go create mode 100644 service/externalTeam/external_team_service_test.go rename service/{ => log}/log_service.go (80%) diff --git a/Makefile b/Makefile index 4f3983d..9690b5d 100644 --- a/Makefile +++ b/Makefile @@ -91,3 +91,5 @@ generatemocks: cd $(CURDIR)/service/incidentTeamTagValue && minimock -i IncidentTeamTagValueService -s _mock.go -o $(CURDIR)/mocks cd $(CURDIR)/repository/incidentTeam && minimock -i IncidentTeamRepository -s _mock.go -o $(CURDIR)/mocks cd $(CURDIR)/repository/incidentTeamTagValue && minimock -i IncidentTeamTagValueRepository -s _mock.go -o $(CURDIR)/mocks + cd $(CURDIR)/service/externalTeam && minimock -i ExternalTeamService -s _mock.go -o $(CURDIR)/mocks + cd $(CURDIR)/repository/externalTeamRepo && minimock -i ExternalTeamRepository -s _mock.go -o $(CURDIR)/mocks diff --git a/appcontext/app.go b/appcontext/app.go index 733ec29..e0b5a3c 100644 --- a/appcontext/app.go +++ b/appcontext/app.go @@ -5,6 +5,7 @@ import ( "github.com/spf13/viper" "gorm.io/gorm" "houston/model/incident" + incidentJiraModel "houston/model/incident_jira" "houston/model/incident_products" "houston/model/log" productModel "houston/model/product" @@ -29,11 +30,13 @@ import ( teamUserSeverityRepo "houston/repository/teamUserSeverity" "houston/service/conference" "houston/service/documentService" + "houston/service/externalTeam" "houston/service/google" incidentService "houston/service/incident/impl" "houston/service/incidentProducts" "houston/service/incidentStatus" "houston/service/incidentUser" + "houston/service/incident_jira" "houston/service/products" "houston/service/productsTeams" rcaService "houston/service/rca/impl" @@ -88,6 +91,8 @@ type houstonServices struct { incidentUserService incidentUser.IncidentUserService requestStatusService requestStatus.RequestStatusService tagValueService tagValue.TagValueService + incidentJiraService incident_jira.IncidentJiraService + externalTeamService externalTeam.ExternalTeamService } var appContext *applicationContext @@ -108,6 +113,8 @@ func InitializeServices() { incidentStatusService := initIncidentStatusService() teamUserService := initTeamUserService() requestStatusService := initRequestStatusService() + externalTeamService := initExternalTeamService() + incidentJiraService := initIncidentJiraService(externalTeamService) services = &houstonServices{ logRepo: logRepo, teamRepo: teamRepo, @@ -133,6 +140,8 @@ func InitializeServices() { incidentStatusService: incidentStatusService, requestStatusService: requestStatusService, tagValueService: initTagValueService(), + externalTeamService: externalTeamService, + incidentJiraService: incidentJiraService, } services.userService = initUserService() services.teamService = initTeamService() @@ -278,7 +287,7 @@ func GetDocumentService() *documentService.ActionsImpl { func initRCAService() *rcaService.RcaService { rcaService := rcaService.NewRcaService( initIncidentService(), initSlackService(), initDocumentService(), - initRCARepo(), initRCAInputRepo(), initUserRepo(), initDriveService(), + initRCARepo(), initRCAInputRepo(), initUserRepo(), initDriveService(), initIncidentJiraService(initExternalTeamService()), ) return rcaService } @@ -422,3 +431,19 @@ func initTagValueService() tagValue.TagValueService { func GetTagValueService() tagValue.TagValueService { return services.tagValueService } + +func initIncidentJiraService(externalTeamService externalTeam.ExternalTeamService) incident_jira.IncidentJiraService { + return incident_jira.NewIncidentJiraService(incidentJiraModel.NewIncidentJiraRepo(GetDB()), externalTeamService) +} + +func GetIncidentJiraService() incident_jira.IncidentJiraService { + return services.incidentJiraService +} + +func initExternalTeamService() externalTeam.ExternalTeamService { + return externalTeam.NewExternalTeamService(externalTeamRepo.NewExternalTeamRepository(GetDB())) +} + +func GetExternalTeamService() externalTeam.ExternalTeamService { + return services.externalTeamService +} diff --git a/cmd/app/handler/slack_handler.go b/cmd/app/handler/slack_handler.go index 6f252e8..87daa42 100644 --- a/cmd/app/handler/slack_handler.go +++ b/cmd/app/handler/slack_handler.go @@ -68,7 +68,7 @@ func NewSlackHandler( documentService := documentService.NewActionsImpl(restClient) rcaService := rcaService.NewRcaService( incidentServiceV2, slackService, documentService, rcaRepository, - rcaInputRepository, userService, appcontext.GetDriveService(), + rcaInputRepository, userService, appcontext.GetDriveService(), appcontext.GetIncidentJiraService(), ) productTeamService := appcontext.GetProductTeamsService() slashCommandProcessor := processor.NewSlashCommandProcessor(socketModeClient, slackbotClient, rcaService) diff --git a/cmd/app/server.go b/cmd/app/server.go index 87b6a9c..1db71a6 100644 --- a/cmd/app/server.go +++ b/cmd/app/server.go @@ -24,6 +24,7 @@ import ( "houston/service/documentService" incidentService "houston/service/incident/impl" "houston/service/incident_channel" + logService "houston/service/log" "houston/service/orchestration" rcaService "houston/service/rca/impl" "houston/service/slack" @@ -266,7 +267,7 @@ func (s *Server) requestStatusHandler(houstonGroup *gin.RouterGroup) { } func (s *Server) logHandler(houstonGroup *gin.RouterGroup) { - logHandler := service.NewLogService(s.gin, s.db) + logHandler := logService.NewLogService(s.gin, s.db) houstonGroup.GET("/logs/:log_type/:id", s.authService.CanUserAccessIncidentLogs(logHandler.GetLogs)) } @@ -279,7 +280,7 @@ func (s *Server) rcaHandler(houstonGroup *gin.RouterGroup) { documentService := documentService.NewActionsImpl(restClient) userRepository := user.NewUserRepository(s.db) rcaService := rcaService.NewRcaService(incidentServiceV2, slackService, documentService, rcaRepository, - rcaInputRepository, userRepository, appcontext.GetDriveService()) + rcaInputRepository, userRepository, appcontext.GetDriveService(), appcontext.GetIncidentJiraService()) rcaHandler := handler.NewRcaHandler(s.gin, rcaService) houstonGroup.POST("/rca", rcaHandler.HandlePostRca) diff --git a/common/jira/jira.go b/common/jira/jira.go index 3dd3113..0c7ad14 100644 --- a/common/jira/jira.go +++ b/common/jira/jira.go @@ -1,10 +1,50 @@ package jira import ( + "gorm.io/datatypes" incidentJiraModel "houston/model/incident_jira" "strings" ) +type JiraTeamMetadata struct { + CreateIssueConfig CreateIssueConfig `json:"createIssueConfig"` +} + +type CreateIssueConfig struct { + Fields []Field `json:"fields"` +} + +type Field struct { + Name string `json:"name"` + Key string `json:"key"` + Value datatypes.JSON `json:"value"` +} + +type CreateJiraTicketInput struct { + IncidentID uint + Metadata JiraTeamMetadata +} + +type UpdateJiraTicketInput struct { + JiraKey string + Description []DescriptionText +} + +type DescriptionText struct { + Text string + Link string +} + +const DefaultJiraTimeout = "DEFAULT_JIRA_TIMEOUT" +const RCAJiraType = "RCA" + +const HoustonIDField = "houston_id" +const SummaryField = "summary" +const DescriptionField = "description" + +const IncidentLink = "Incident Link" +const RCAJiraLink = "RCA Link" + func ConvertIncidentJiraEntitiesToCommaSeparatedLinks(jiraLinks *[]incidentJiraModel.IncidentJiraEntity) string { var jiraLinksString string @@ -18,3 +58,7 @@ func ConvertIncidentJiraEntitiesToCommaSeparatedLinks(jiraLinks *[]incidentJiraM return jiraLinksString } + +func GetJiraKeyFromLink(jiraLink string) string { + return jiraLink[strings.LastIndex(jiraLink, "/")+1:] +} diff --git a/common/util/constant.go b/common/util/constant.go index 3650359..1d78dd0 100644 --- a/common/util/constant.go +++ b/common/util/constant.go @@ -147,3 +147,9 @@ const ( const ( DUPLICATE_KEY_VALUE_ERROR_CODE = "23505" ) + +type TeamType string + +const ( + CXTeam TeamType = "CX" +) diff --git a/db/migration/000033_add_team_and_jira_type_columns.up.sql b/db/migration/000033_add_team_and_jira_type_columns.up.sql new file mode 100644 index 0000000..d57fb98 --- /dev/null +++ b/db/migration/000033_add_team_and_jira_type_columns.up.sql @@ -0,0 +1,7 @@ +BEGIN; + +ALTER TABLE incident_jira ADD COLUMN IF NOT EXISTS jira_type varchar(100) DEFAULT ''; + +ALTER TABLE team ADD COLUMN IF NOT EXISTS team_type varchar(100); + +COMMIT; \ No newline at end of file diff --git a/internal/processor/action/incident_jira_links_action.go b/internal/processor/action/incident_jira_links_action.go index 1b3af6d..7b33d02 100644 --- a/internal/processor/action/incident_jira_links_action.go +++ b/internal/processor/action/incident_jira_links_action.go @@ -6,6 +6,8 @@ import ( "houston/common/util" "houston/internal/processor/action/view" incidentJiraModel "houston/model/incident_jira" + "houston/repository/externalTeamRepo" + "houston/service/externalTeam" "houston/service/incident_jira" slack2 "houston/service/slack" ) @@ -23,8 +25,11 @@ func NewIncidentJiraLinksAction( slackService slack2.ISlackService, ) *IncidentJiraLinksAction { return &IncidentJiraLinksAction{ - incidentJiraService: incident_jira.NewIncidentJiraService(incidentJiraModel.NewIncidentJiraRepo(appcontext.GetDB())), - slackService: slackService, + incidentJiraService: incident_jira.NewIncidentJiraService( + incidentJiraModel.NewIncidentJiraRepo(appcontext.GetDB()), + externalTeam.NewExternalTeamService(externalTeamRepo.NewExternalTeamRepository(appcontext.GetDB())), + ), + slackService: slackService, } } diff --git a/model/externalTeam/entity.go b/model/externalTeam/entity.go index e0fcaff..c5a1f40 100644 --- a/model/externalTeam/entity.go +++ b/model/externalTeam/entity.go @@ -22,3 +22,19 @@ type ExternalTeamEntity struct { func (ExternalTeamEntity) TableName() string { return "external_team" } + +func (entity *ExternalTeamEntity) ToDTO() *ExternalTeamDTO { + return &ExternalTeamDTO{ + ID: entity.ID, + ExternalTeamID: entity.ExternalTeamID, + ExternalTeamName: entity.ExternalTeamName, + Metadata: entity.Metadata, + ProviderName: entity.ProviderName, + TeamID: entity.TeamID, + IsActive: entity.IsActive, + CreatedAt: entity.CreatedAt, + UpdatedAt: entity.UpdatedAt, + CreatedBy: entity.CreatedBy, + UpdatedBy: entity.UpdatedBy, + } +} diff --git a/model/externalTeam/model.go b/model/externalTeam/model.go index 9c38027..7ab1266 100644 --- a/model/externalTeam/model.go +++ b/model/externalTeam/model.go @@ -1,14 +1,22 @@ package externalTeam -import "gorm.io/datatypes" +import ( + "gorm.io/datatypes" + "time" +) type ExternalTeamDTO struct { + ID uint `json:"id,omitempty"` ExternalTeamID string `json:"external_team_id,omitempty"` ExternalTeamName string `json:"external_team_name,omitempty"` Metadata datatypes.JSON `json:"metadata,omitempty"` ProviderName string `json:"provider_name,omitempty"` TeamID uint `json:"team_id,omitempty"` IsActive bool `json:"is_active,omitempty"` + CreatedAt time.Time `json:"created_at,omitempty"` + UpdatedAt time.Time `json:"updated_at,omitempty"` + CreatedBy string `json:"created_by,omitempty"` + UpdatedBy string `json:"updated_by,omitempty"` } type ExternalTeamMetadata struct { diff --git a/model/incident_jira/entity.go b/model/incident_jira/entity.go index e6946a2..7b401b5 100644 --- a/model/incident_jira/entity.go +++ b/model/incident_jira/entity.go @@ -9,6 +9,7 @@ type IncidentJiraEntity struct { CreatedAt time.Time `gorm:"column:created_at"` IncidentEntityID uint `gorm:"column:incident_id"` JiraLink string `gorm:"column:jira_link"` + JiraType string `gorm:"column:jira_type"` } func (IncidentJiraEntity) TableName() string { diff --git a/model/incident_jira/incident_jira_repository.go b/model/incident_jira/incident_jira_repository.go index a522db1..6b63471 100644 --- a/model/incident_jira/incident_jira_repository.go +++ b/model/incident_jira/incident_jira_repository.go @@ -5,9 +5,10 @@ import ( ) type IncidentJiraRepository interface { - InsertJiraLinks(incidentID uint, jiraLinks []string) ([]uint, error) + InsertJiraLinks(incidentID uint, jiraLinks []string, jiraType string) ([]uint, error) GetJiraLinksByIncidentID(incidentID uint) (*[]IncidentJiraEntity, error) GetIncidentJiraEntity(incidentID uint, jiraLink string) (*IncidentJiraEntity, error) + GetIncidentJiraLinksByIncidentIdAndJiraType(incidentID uint, jiraType string) ([]IncidentJiraEntity, error) DeleteJiraLink(incidentID uint, jiraLink string) error DeleteAllJiraLinksForIncident(incidentID uint) error GetAllJiraIdsByPage(incidentName string, pageNumber, pageSize int64) (*[]IncidentJiraLinksDTO, int64, error) diff --git a/model/incident_jira/incident_jira_repository_impl.go b/model/incident_jira/incident_jira_repository_impl.go index 706a1c2..83e4c61 100644 --- a/model/incident_jira/incident_jira_repository_impl.go +++ b/model/incident_jira/incident_jira_repository_impl.go @@ -8,12 +8,13 @@ type IncidentJiraRepositoryImpl struct { db *gorm.DB } -func (repo *IncidentJiraRepositoryImpl) InsertJiraLinks(incidentID uint, jiraLinks []string) ([]uint, error) { +func (repo *IncidentJiraRepositoryImpl) InsertJiraLinks(incidentID uint, jiraLinks []string, jiraType string) ([]uint, error) { var records []IncidentJiraEntity for _, jiraLink := range jiraLinks { records = append(records, IncidentJiraEntity{ IncidentEntityID: incidentID, JiraLink: jiraLink, + JiraType: jiraType, }) } @@ -34,7 +35,7 @@ func (repo *IncidentJiraRepositoryImpl) InsertJiraLinks(incidentID uint, jiraLin func (repo *IncidentJiraRepositoryImpl) GetJiraLinksByIncidentID(incidentID uint) (*[]IncidentJiraEntity, error) { var entities []IncidentJiraEntity - result := repo.db.Find(&entities, "incident_id = ?", incidentID) + result := repo.db.Find(&entities, "incident_id = ? AND jira_type != ?", incidentID, RCAJiraType) if result.Error != nil { return nil, result.Error } @@ -58,6 +59,21 @@ func (repo *IncidentJiraRepositoryImpl) GetIncidentJiraEntity(incidentID uint, j return &entity, nil } +func (repo *IncidentJiraRepositoryImpl) GetIncidentJiraLinksByIncidentIdAndJiraType(incidentID uint, jiraType string) ([]IncidentJiraEntity, error) { + var entities []IncidentJiraEntity + + result := repo.db.Find(&entities, "incident_id = ? AND jira_type = ?", incidentID, jiraType) + if result.Error != nil { + return nil, result.Error + } + + if result.RowsAffected == 0 { + return nil, nil + } + + return entities, nil +} + func (repo *IncidentJiraRepositoryImpl) DeleteJiraLink(incidentID uint, jiraLink string) error { result := repo.db.Delete(&IncidentJiraEntity{}, "incident_id = ? AND jira_link = ?", incidentID, jiraLink) if result.Error != nil { @@ -67,7 +83,7 @@ func (repo *IncidentJiraRepositoryImpl) DeleteJiraLink(incidentID uint, jiraLink } func (repo *IncidentJiraRepositoryImpl) DeleteAllJiraLinksForIncident(incidentID uint) error { - result := repo.db.Delete(&IncidentJiraEntity{}, "incident_id = ?", incidentID) + result := repo.db.Delete(&IncidentJiraEntity{}, "incident_id = ? AND jira_type != ?", incidentID, RCAJiraType) if result.Error != nil { return result.Error } @@ -106,3 +122,5 @@ func (repo *IncidentJiraRepositoryImpl) GetAllJiraIdsByPage(incidentName string, return &allJiraLinksFromDB, totalElements, result.Error } + +const RCAJiraType = "RCA" diff --git a/model/log/log.go b/model/log/log.go index d6d05c5..61f6ff0 100644 --- a/model/log/log.go +++ b/model/log/log.go @@ -1,6 +1,7 @@ package log import ( + "github.com/spf13/viper" "gorm.io/gorm" ) @@ -43,3 +44,26 @@ func (r *Repository) FetchLogsByRelationNameAndRecordId(relationName string, rec return logEntity, nil } + +func (r *Repository) IsIncidentAutoEscalated(incidentID uint) (bool, error) { + var logs []LogEntity + result := r.gormClient.Raw(autoEscalationCheckQueryTemplate, incidentID, viper.GetString("HOUSTON_BOT_SLACK_ID")).Scan(&logs) + if result.Error != nil { + return false, result.Error + } + + return len(logs) > 0, nil +} + +const autoEscalationCheckQueryTemplate = ` + SELECT + * + FROM log l + WHERE l.record_id = ? AND l.relation_name = 'incident' + AND EXISTS ( + SELECT 1 + FROM jsonb_array_elements(l.changes) AS change + WHERE change ->> 'attribute' = 'SeverityId' + AND (l.user_info ->> 'id' = ? ) + ) +` diff --git a/model/team/entity.go b/model/team/entity.go index bfd3cf8..f4681b3 100644 --- a/model/team/entity.go +++ b/model/team/entity.go @@ -19,6 +19,7 @@ type TeamEntity struct { CreatedBy string `gorm:"column:created_by"` UpdatedBy string `gorm:"column:updated_by"` TeamSeverityUpdateRule datatypes.JSON `gorm:"column:team_severity_update_strategy"` + TeamType string `gorm:"column:team_type"` } func (TeamEntity) TableName() string { @@ -39,6 +40,7 @@ func (entity *TeamEntity) ToDTO() *TeamDTO { CreatedBy: entity.CreatedBy, UpdatedBy: entity.UpdatedBy, TeamSeverityUpdateRule: entity.TeamSeverityUpdateRule, + TeamType: entity.TeamType, } } diff --git a/model/team/model.go b/model/team/model.go index 6b24fc5..45e1db4 100644 --- a/model/team/model.go +++ b/model/team/model.go @@ -15,6 +15,7 @@ type TeamDTO struct { CreatedBy string `json:"created_by"` UpdatedBy string `json:"updated_by"` TeamSeverityUpdateRule datatypes.JSON `json:"team_severity_update_strategy,omitempty"` + TeamType string `json:"team_type"` } type TeamSeverityUpdateRule struct { diff --git a/pkg/atlassian/dto/request/create_jira_ticket.go b/pkg/atlassian/dto/request/create_jira_ticket.go new file mode 100644 index 0000000..df7a64d --- /dev/null +++ b/pkg/atlassian/dto/request/create_jira_ticket.go @@ -0,0 +1,41 @@ +package request + +type CreateJiraTicketRequest struct { + Fields map[string]interface{} `json:"fields"` +} + +type UpdateJiraTicketRequest struct { + Fields map[string]interface{} `json:"fields"` +} + +type DescriptionField struct { + Type string `json:"type"` + Version int `json:"version"` + Content []Content `json:"content"` +} + +type Content struct { + Type string `json:"type"` + Content []TextContent `json:"content"` +} + +type TextContent struct { + Type string `json:"type"` + Text string `json:"text"` + Marks []Mark `json:"marks"` +} + +type Mark struct { + Type string `json:"type"` + Attrs Attrs `json:"attrs"` +} + +type Attrs struct { + Href string `json:"href"` +} + +const DocumentType = "doc" +const ParagraphType = "paragraph" +const TextType = "text" +const LinkType = "link" +const Version = 1 diff --git a/pkg/atlassian/dto/response/create_jira_ticket_response.go b/pkg/atlassian/dto/response/create_jira_ticket_response.go new file mode 100644 index 0000000..a0db923 --- /dev/null +++ b/pkg/atlassian/dto/response/create_jira_ticket_response.go @@ -0,0 +1,7 @@ +package response + +type CreateJiraTicketResponse struct { + Id string `json:"id"` + Key string `json:"key"` + Self string `json:"self"` +} diff --git a/pkg/atlassian/jira_client.go b/pkg/atlassian/jira_client.go index aea57b3..5e46c41 100644 --- a/pkg/atlassian/jira_client.go +++ b/pkg/atlassian/jira_client.go @@ -3,6 +3,7 @@ package atlassian import ( "github.com/spf13/viper" "houston/common/errors" + "houston/common/jira" "houston/pkg/atlassian/dto/response" "houston/pkg/rest" ) @@ -12,6 +13,8 @@ type JiraClient interface { GetAuthorizationHeaderValue() string // SearchByJQL executes a Jira search using JQL. SearchByJQL(jql string) (*response.JiraSearchJQLResponse, *errors.ApiError) + CreateJiraTicket(input jira.CreateJiraTicketInput) (string, error) + UpdateJiraTicket(input jira.UpdateJiraTicketInput) error } type JiraConfig struct { @@ -29,6 +32,7 @@ func NewJiraClient(httpRestClient rest.HttpRestClient) *JiraClientImpl { return &JiraClientImpl{ JiraConfig: config, httpRestClient: httpRestClient, + DefaultTimeout: viper.GetDuration(jira.DefaultJiraTimeout), } } @@ -36,4 +40,5 @@ type JiraAPI string const ( SearchAPI JiraAPI = "rest/api/3/search" + IssueAPI JiraAPI = "rest/api/3/issue" ) diff --git a/pkg/atlassian/jira_client_impl.go b/pkg/atlassian/jira_client_impl.go index e274ef9..7f03ae2 100644 --- a/pkg/atlassian/jira_client_impl.go +++ b/pkg/atlassian/jira_client_impl.go @@ -5,13 +5,17 @@ import ( "encoding/base64" "encoding/json" "fmt" + "github.com/spf13/viper" "houston/common/errors" + "houston/common/jira" + "houston/common/util" "houston/logger" "houston/pkg/atlassian/dto/request" "houston/pkg/atlassian/dto/response" "houston/pkg/rest" "io" "net/http" + "time" ) const logTag = "[jira_client_impl]" @@ -19,6 +23,7 @@ const logTag = "[jira_client_impl]" type JiraClientImpl struct { *JiraConfig httpRestClient rest.HttpRestClient + DefaultTimeout time.Duration } func (client *JiraClientImpl) GetAuthorizationHeaderValue() string { @@ -37,12 +42,8 @@ func (client *JiraClientImpl) SearchByJQL(jql string) (*response.JiraSearchJQLRe return nil, errors.NewApiError(err.Error(), "BadRequest", http.StatusBadRequest) } - headers := map[string]string{ - "Authorization": client.GetAuthorizationHeaderValue(), - } - // Make Jira API call - apiResponse, err := client.httpRestClient.Post(url, *bytes.NewBuffer(requestBody), headers, nil) + apiResponse, err := client.httpRestClient.Post(url, *bytes.NewBuffer(requestBody), client.getJiraApiHeaders(), nil) if err != nil { logger.Error(fmt.Sprintf("%s error in making api call to Jira: %+v", logTag, err)) return nil, errors.NewApiError(err.Error(), apiResponse.Status, http.StatusInternalServerError) @@ -72,3 +73,157 @@ func (client *JiraClientImpl) SearchByJQL(jql string) (*response.JiraSearchJQLRe return &searchJQLResponse, nil } + +func (client *JiraClientImpl) CreateJiraTicket(input jira.CreateJiraTicketInput) (string, error) { + logger.Info(fmt.Sprintf("%s creating Jira ticket for incident: %d", logTag, input.IncidentID)) + url := fmt.Sprintf("%s/%s", client.BaseURL, IssueAPI) + + createIssueRequestBody, err := getCreateIssueRequestFromInput(input) + if err != nil { + return "", err + } + + apiResponse, err := client.httpRestClient.PostWithTimeout( + url, *bytes.NewBuffer(createIssueRequestBody), client.getJiraApiHeaders(), client.DefaultTimeout, nil, + ) + if err != nil { + logger.Error(fmt.Sprintf("%s error in making api call to Jira: %+v", logTag, err)) + return "", err + } + + responseBody, err := io.ReadAll(apiResponse.Body) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to read api response body: %+v", logTag, err)) + return "", err + } + + if apiResponse.StatusCode != http.StatusCreated { + logger.Error(fmt.Sprintf("%s jira create api responded %d %s with body: %s", logTag, apiResponse.StatusCode, apiResponse.Status, responseBody)) + return "", errors.NewApiError(fmt.Sprintf("Jira api returned %d", apiResponse.StatusCode), apiResponse.Status, http.StatusInternalServerError) + } + + createJiraTicketResponse := response.CreateJiraTicketResponse{} + err = json.Unmarshal(responseBody, &createJiraTicketResponse) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to parse jira create api response body: [%+v] into CreateJiraTicketResponse: %+v", logTag, responseBody, err)) + return "", errors.NewApiError(err.Error(), apiResponse.Status, http.StatusInternalServerError) + } + + return createJiraTicketResponse.Key, nil +} + +func (client *JiraClientImpl) UpdateJiraTicket(input jira.UpdateJiraTicketInput) error { + logger.Info(fmt.Sprintf("%s updating Jira ticket: %s", logTag, input.JiraKey)) + url := fmt.Sprintf("%s/%s/%s", client.BaseURL, IssueAPI, input.JiraKey) + + updateJiraTicketRequestBody, err := getUpdateJiraTicketRequest(input) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to get update jira ticket request: %+v", logTag, err)) + return err + } + + apiResponse, err := client.httpRestClient.PutWithTimeout( + url, *bytes.NewBuffer(updateJiraTicketRequestBody), client.getJiraApiHeaders(), client.DefaultTimeout, nil, + ) + if err != nil { + logger.Error(fmt.Sprintf("%s error in making api call to Jira: %+v", logTag, err)) + return err + } + + if apiResponse.StatusCode != http.StatusNoContent { + logger.Error(fmt.Sprintf("%s jira update api responded %d %s", logTag, apiResponse.StatusCode, apiResponse.Status)) + return errors.NewApiError(fmt.Sprintf("Jira api returned %d", apiResponse.StatusCode), apiResponse.Status, http.StatusInternalServerError) + } + + return nil +} + +func (client *JiraClientImpl) getJiraApiHeaders() map[string]string { + return map[string]string{"Authorization": client.GetAuthorizationHeaderValue()} +} + +func getCreateIssueRequestFromInput(input jira.CreateJiraTicketInput) ([]byte, error) { + createJiraTicketRequest := request.CreateJiraTicketRequest{ + Fields: make(map[string]interface{}), + } + + for _, field := range input.Metadata.CreateIssueConfig.Fields { + createJiraTicketRequest.Fields[field.Key] = mapFieldValue(field, input.IncidentID) + } + + logger.Info(fmt.Sprintf("%s create issue request: %+v", logTag, createJiraTicketRequest)) + + requestBody, err := json.Marshal(createJiraTicketRequest) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to marshal CreateJiraTicketRequest: %+v: %+v", logTag, createJiraTicketRequest, err)) + return nil, err + } + + logger.Info(fmt.Sprintf("%s create issue request body: %s", logTag, requestBody)) + return requestBody, nil +} + +func mapFieldValue(field jira.Field, incidentID uint) interface{} { + switch field.Name { + case jira.HoustonIDField: + return fmt.Sprintf("%d", incidentID) + case jira.SummaryField: + return fmt.Sprintf("Houston-%d RCA", incidentID) + case jira.DescriptionField: + return createDescriptionField([]jira.DescriptionText{{Text: jira.IncidentLink, Link: GetIncidentPageLink(incidentID)}}) + default: + return field.Value + } +} + +func createDescriptionField(descriptionBlocks []jira.DescriptionText) request.DescriptionField { + descriptionField := request.DescriptionField{ + Type: request.DocumentType, + Version: request.Version, + Content: []request.Content{}, + } + + for _, descriptionBlock := range descriptionBlocks { + content := request.Content{ + Type: request.ParagraphType, + Content: []request.TextContent{ + { + Type: request.TextType, + Text: descriptionBlock.Text, + }, + }, + } + if !util.IsBlank(descriptionBlock.Link) { + content.Content[0].Marks = []request.Mark{ + { + Type: request.LinkType, + Attrs: request.Attrs{ + Href: descriptionBlock.Link, + }, + }, + } + } + descriptionField.Content = append(descriptionField.Content, content) + } + + return descriptionField +} + +func GetIncidentPageLink(incidentID uint) string { + return viper.GetString("HOUSTON_UI_INCIDENT_PAGE") + fmt.Sprintf("%d", incidentID) +} + +func getUpdateJiraTicketRequest(input jira.UpdateJiraTicketInput) ([]byte, error) { + updateJiraTicketRequest := request.UpdateJiraTicketRequest{Fields: make(map[string]interface{})} + + updateJiraTicketRequest.Fields[jira.DescriptionField] = createDescriptionField(input.Description) + + requestBody, err := json.Marshal(updateJiraTicketRequest) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to marshal UpdateJiraTicketRequest: %+v: %+v", logTag, updateJiraTicketRequest, err)) + return nil, err + } + + logger.Info(fmt.Sprintf("%s update issue request body: %s", logTag, requestBody)) + return requestBody, nil +} diff --git a/pkg/atlassian/jira_client_test.go b/pkg/atlassian/jira_client_test.go index 9f04557..9659b23 100644 --- a/pkg/atlassian/jira_client_test.go +++ b/pkg/atlassian/jira_client_test.go @@ -7,6 +7,8 @@ import ( "github.com/gojuno/minimock/v3" "github.com/spf13/viper" "github.com/stretchr/testify/suite" + "gorm.io/datatypes" + "houston/common/jira" "houston/logger" "houston/mocks" "houston/pkg/atlassian/dto/request" @@ -80,3 +82,191 @@ func (suite *JiraClientSuite) TestJiraClientImpl_SearchByJQL() { } suite.Equal(expectedJiraAPiResponse, *jiraAPIResponse) } + +func (suite *JiraClientSuite) TestJiraClientImpl_CreateJiraTicket_ApiErrorCase() { + controller := minimock.NewController(suite.T()) + suite.T().Cleanup(controller.Finish) + createJiraTicketInput := getMockCreateJiraTicketInput() + + jiraClient := NewJiraClient(suite.mockHttpRestClient) + suite.mockHttpRestClient.PostWithTimeoutMock.Return(nil, fmt.Errorf("error in making api call to Jira")) + + jiraKey, err := jiraClient.CreateJiraTicket(createJiraTicketInput) + suite.Emptyf(jiraKey, "Jira key should be empty") + suite.NotNil(err, "Error should not be nil") +} + +func (suite *JiraClientSuite) TestJiraClientImpl_CreateJiraTicket_StatusNotCreatedCase() { + controller := minimock.NewController(suite.T()) + suite.T().Cleanup(controller.Finish) + createJiraTicketInput := getMockCreateJiraTicketInput() + + jiraClient := NewJiraClient(suite.mockHttpRestClient) + body := &http.Response{StatusCode: http.StatusNotFound} + bodyJson, err := json.Marshal(body) + if err != nil { + suite.Fail(err.Error(), err) + } + suite.mockHttpRestClient.PostWithTimeoutMock.Return( + &http.Response{StatusCode: http.StatusNotFound, Body: io.NopCloser(bytes.NewBufferString(string(bodyJson)))}, + nil, + ) + + jiraKey, err := jiraClient.CreateJiraTicket(createJiraTicketInput) + suite.Emptyf(jiraKey, "Jira key should be empty") + suite.NotNil(err, "Error should not be nil") +} + +func (suite *JiraClientSuite) TestJiraClientImpl_CreateJiraTicket_SuccessCase() { + controller := minimock.NewController(suite.T()) + suite.T().Cleanup(controller.Finish) + createJiraTicketInput := getMockCreateJiraTicketInput() + + jiraClient := NewJiraClient(suite.mockHttpRestClient) + body := &http.Response{StatusCode: http.StatusCreated, Body: io.NopCloser(bytes.NewBufferString(`{"key": "TP-48566"}`))} + bodyJson, err := json.Marshal(body) + if err != nil { + suite.Fail(err.Error(), err) + } + suite.mockHttpRestClient.PostWithTimeoutMock.Return( + &http.Response{StatusCode: http.StatusCreated, Body: io.NopCloser(bytes.NewBufferString(string(bodyJson)))}, + nil, + ) + jiraKey, err := jiraClient.CreateJiraTicket(createJiraTicketInput) + suite.NotNil(jiraKey, "Jira key should not be empty") + suite.Nil(err, "Error should be nil") +} + +func (suite *JiraClientSuite) TestJiraClientImpl_UpdateJiraTicket_ApiErrorCase() { + controller := minimock.NewController(suite.T()) + suite.T().Cleanup(controller.Finish) + updateJiraTicketInput := getMockUpdateJiraTicketInput() + + jiraClient := NewJiraClient(suite.mockHttpRestClient) + suite.mockHttpRestClient.PutWithTimeoutMock.Return(nil, fmt.Errorf("error in making api call to Jira")) + + err := jiraClient.UpdateJiraTicket(updateJiraTicketInput) + suite.NotNil(err, "Error should not be nil") +} + +func (suite *JiraClientSuite) TestJiraClientImpl_UpdateJiraTicket_StatusNotUpdatedCase() { + controller := minimock.NewController(suite.T()) + suite.T().Cleanup(controller.Finish) + updateJiraTicketInput := getMockUpdateJiraTicketInput() + + jiraClient := NewJiraClient(suite.mockHttpRestClient) + body := &http.Response{StatusCode: http.StatusNotFound} + bodyJson, err := json.Marshal(body) + if err != nil { + suite.Fail(err.Error(), err) + } + suite.mockHttpRestClient.PutWithTimeoutMock.Return( + &http.Response{StatusCode: http.StatusNotFound, Body: io.NopCloser(bytes.NewBufferString(string(bodyJson)))}, + nil, + ) + + err = jiraClient.UpdateJiraTicket(updateJiraTicketInput) + suite.NotNil(err, "Error should not be nil") +} + +func (suite *JiraClientSuite) TestJiraClientImpl_UpdateJiraTicket_SuccessCase() { + controller := minimock.NewController(suite.T()) + suite.T().Cleanup(controller.Finish) + updateJiraTicketInput := getMockUpdateJiraTicketInput() + + jiraClient := NewJiraClient(suite.mockHttpRestClient) + body := &http.Response{StatusCode: http.StatusNoContent} + bodyJson, err := json.Marshal(body) + if err != nil { + suite.Fail(err.Error(), err) + } + suite.mockHttpRestClient.PutWithTimeoutMock.Return( + &http.Response{StatusCode: http.StatusNoContent, Body: io.NopCloser(bytes.NewBufferString(string(bodyJson)))}, + nil, + ) + + err = jiraClient.UpdateJiraTicket(updateJiraTicketInput) + suite.Nil(err, "Error should be nil") +} + +func getMockCreateJiraTicketInput() jira.CreateJiraTicketInput { + return jira.CreateJiraTicketInput{ + IncidentID: 12345, + Metadata: jira.JiraTeamMetadata{ + CreateIssueConfig: jira.CreateIssueConfig{ + Fields: []jira.Field{ + { + Name: "assignee", + Key: "assignee", + Value: datatypes.JSON(`{ + "id": "712020:086d543a-cfe1-4ffb-9da2-8fca2cae085f" + }`), + }, + { + Name: "issuetype", + Key: "issuetype", + Value: datatypes.JSON(`{ + "id": "10002" + }`), + }, + { + Name: "project", + Key: "project", + Value: datatypes.JSON(`{ + "id": "10029" + }`), + }, + { + Name: "feature_owning_team", + Key: "customfield_10327", + Value: datatypes.JSON(`{ + "id": "10561" + }`), + }, + { + Name: "houston_id", + Key: "customfield_10484", + Value: datatypes.JSON(`"ABCD"`), + }, + { + Name: "summary", + Key: "summary", + Value: datatypes.JSON(`"Houston 12345"`), + }, + { + Name: "description", + Key: "description", + Value: datatypes.JSON(`{ + "type": "doc", + "version": 1, + "content": [{ + "type": "paragraph", + "content": [{ + "type": "text", + "text": "Incident description goes here" + }] + }] + }`), + }, + }, + }, + }, + } + +} + +func getMockUpdateJiraTicketInput() jira.UpdateJiraTicketInput { + return jira.UpdateJiraTicketInput{ + JiraKey: "TP-48566", + Description: []jira.DescriptionText{ + { + Text: "Incident description goes here", + Link: "https://houston.com/incident/12345", + }, + { + Text: "Incident description goes here", + Link: "https://houston.com/incident/12345", + }, + }, + } +} diff --git a/pkg/rest/http_client.go b/pkg/rest/http_client.go index 3ccb022..a90b845 100644 --- a/pkg/rest/http_client.go +++ b/pkg/rest/http_client.go @@ -20,6 +20,8 @@ type HttpRestClient interface { formData map[string]interface{}) (*http.Response, error) PostWithTimeout(url string, body bytes.Buffer, headers map[string]string, timeout time.Duration, formData map[string]interface{}) (*http.Response, error) + PutWithTimeout(url string, body bytes.Buffer, headers map[string]string, + timeout time.Duration, formData map[string]interface{}) (*http.Response, error) Get(url string, urlParams map[string]string, headers map[string]string, encodedURL bool) (*http.Response, error) GetWithTimeout(url string, urlParams map[string]string, headers map[string]string, timeout time.Duration, encodedURL bool) (*http.Response, error) } @@ -63,6 +65,31 @@ func (client *HttpRestClientImpl) PostWithTimeout(url string, body bytes.Buffer, return client.makeHttpRequest(restClient, request) } +func (client *HttpRestClientImpl) PutWithTimeout(url string, body bytes.Buffer, headers map[string]string, + timeout time.Duration, formData map[string]interface{}) (*http.Response, error) { + requestContext, cancelFunc := context.WithTimeout(context.Background(), timeout) + defer cancelFunc() + contentType := util.ContentTypeJSON + restClient := &http.Client{ + Timeout: timeout, + } + if formData != nil { + var err error + body, contentType, err = constructFormFields(formData) + if err != nil { + return nil, err + } + } + var request, _ = http.NewRequest("PUT", url, &body) + request.WithContext(requestContext) + request.Header.Set("Content-Type", contentType) + for key, value := range headers { + request.Header.Set(key, value) + } + + return client.makeHttpRequest(restClient, request) +} + func (client *HttpRestClientImpl) Get(url string, urlParams map[string]string, headers map[string]string, encodedURL bool) (*http.Response, error) { return client.GetWithTimeout(url, urlParams, headers, client.DefaultTimeout, encodedURL) diff --git a/service/externalTeam/external_team_service_impl.go b/service/externalTeam/external_team_service_impl.go new file mode 100644 index 0000000..7070b4f --- /dev/null +++ b/service/externalTeam/external_team_service_impl.go @@ -0,0 +1,30 @@ +package externalTeam + +import ( + "fmt" + "houston/logger" + "houston/model/externalTeam" + "houston/repository/externalTeamRepo" +) + +type externalTeamServiceImpl struct { + externalTeamRepository externalTeamRepo.IExternalTeamRepository +} + +const logTag = "[external-team-service]" + +func (service *externalTeamServiceImpl) GetExternalTeamByTeamIdAndProvider(teamId uint, provider string) (*externalTeam.ExternalTeamDTO, error) { + externalTeamEntity, err := service.externalTeamRepository.GetExternalTeamEntityByTeamIdAndProvider(teamId, provider) + if err != nil { + logger.Error(fmt.Sprintf("%s Error getting external team entity by team id %d and provider %s: %s", logTag, teamId, provider, err.Error())) + return nil, err + } + + if externalTeamEntity == nil { + errMessage := fmt.Sprintf("External team entity not found by team id %d and provider %s", teamId, provider) + logger.Error(fmt.Sprintf("%s %s", logTag, errMessage)) + return nil, fmt.Errorf(errMessage) + } + + return externalTeamEntity.ToDTO(), nil +} diff --git a/service/externalTeam/external_team_service_interface.go b/service/externalTeam/external_team_service_interface.go new file mode 100644 index 0000000..60eb11c --- /dev/null +++ b/service/externalTeam/external_team_service_interface.go @@ -0,0 +1,14 @@ +package externalTeam + +import ( + "houston/model/externalTeam" + "houston/repository/externalTeamRepo" +) + +type ExternalTeamService interface { + GetExternalTeamByTeamIdAndProvider(teamId uint, provider string) (*externalTeam.ExternalTeamDTO, error) +} + +func NewExternalTeamService(repo externalTeamRepo.IExternalTeamRepository) ExternalTeamService { + return &externalTeamServiceImpl{externalTeamRepository: repo} +} diff --git a/service/externalTeam/external_team_service_test.go b/service/externalTeam/external_team_service_test.go new file mode 100644 index 0000000..f753bac --- /dev/null +++ b/service/externalTeam/external_team_service_test.go @@ -0,0 +1,65 @@ +package externalTeam + +import ( + "github.com/stretchr/testify/suite" + "houston/logger" + "houston/mocks" + "houston/model/externalTeam" + "testing" +) + +type externalTeamServiceSuite struct { + suite.Suite + externalTeamRepository *mocks.IExternalTeamRepositoryMock + externalTeamService ExternalTeamService +} + +func (suite *externalTeamServiceSuite) Test_GetExternalTeamByTeamIdAndProvider_FailureCase() { + teamId := uint(1) + provider := "dummy" + suite.externalTeamRepository.GetExternalTeamEntityByTeamIdAndProviderMock.Return(nil, nil) + + result, err := suite.externalTeamService.GetExternalTeamByTeamIdAndProvider(teamId, provider) + suite.Error(err, "should return error") + suite.Nil(result, "should return nil") +} + +func (suite *externalTeamServiceSuite) Test_GetExternalTeamByTeamIdAndProvider_NilDataCase() { + teamId := uint(1) + provider := "dummy" + suite.externalTeamRepository.GetExternalTeamEntityByTeamIdAndProviderMock.Return(nil, nil) + + result, err := suite.externalTeamService.GetExternalTeamByTeamIdAndProvider(teamId, provider) + suite.Nil(result, "should return nil") + suite.Error(err, "should return error") +} + +func (suite *externalTeamServiceSuite) Test_GetExternalTeamByTeamIdAndProvider_SuccessCase() { + teamId := uint(1) + provider := "dummy" + externalTeamEntity := getMockExternalTeamEntity() + suite.externalTeamRepository.GetExternalTeamEntityByTeamIdAndProviderMock.Return(&externalTeamEntity, nil) + + result, err := suite.externalTeamService.GetExternalTeamByTeamIdAndProvider(teamId, provider) + suite.NoError(err, "should not return error") + suite.Equal(externalTeamEntity.ToDTO(), result, "should return equal") +} + +func (suite *externalTeamServiceSuite) SetupTest() { + logger.InitLogger() + suite.externalTeamRepository = mocks.NewIExternalTeamRepositoryMock(suite.T()) + suite.externalTeamService = NewExternalTeamService(suite.externalTeamRepository) +} + +func TestIncidentChannelServiceSuite(t *testing.T) { + suite.Run(t, new(externalTeamServiceSuite)) +} + +func getMockExternalTeamEntity() externalTeam.ExternalTeamEntity { + return externalTeam.ExternalTeamEntity{ + ID: 1, + ProviderName: "dummy", + TeamID: 1, + ExternalTeamID: "dummy", + } +} diff --git a/service/incident/impl/incident_service_v2.go b/service/incident/impl/incident_service_v2.go index 7185d45..ee112ea 100644 --- a/service/incident/impl/incident_service_v2.go +++ b/service/incident/impl/incident_service_v2.go @@ -46,6 +46,7 @@ import ( "houston/service/alertService" conferenceService "houston/service/conference" "houston/service/documentService" + "houston/service/externalTeam" "houston/service/google" incidentService "houston/service/incident" incidentStatusRepo "houston/service/incidentStatus" @@ -54,6 +55,7 @@ import ( incidentChannel "houston/service/incident_channel" "houston/service/incident_jira" "houston/service/krakatoa" + logService "houston/service/log" rcaService "houston/service/rca" rcaServiceImpl "houston/service/rca/impl" request "houston/service/request" @@ -101,6 +103,7 @@ type IncidentServiceV2 struct { incidentStatusService incidentStatusRepo.IncidentStatusService incidentTeamService incidentTeam.IncidentTeamService incidentTeamTagValueService incidentTeamTagValue.IncidentTeamTagValueService + logService *logService.LogService } /* @@ -149,17 +152,21 @@ func NewIncidentServiceV2(db *gorm.DB) *IncidentServiceV2 { incidentChannelService: incidentChannelService, krakatoaService: krakatoaService, calendarService: calendarService, - incidentJiraService: incident_jira.NewIncidentJiraService(incidentJiraModel.NewIncidentJiraRepo(db)), - tagService: tagService, - teamServiceV2: teamServiceV2, - alertService: alertService.NewAlertService(), - teamUserService: teamUserService, - severityService: severityService, - incidentStatusService: incidentStatusService, - incidentTeamService: incidentTeam.NewIncidentTeamService(incidentTeamModel.NewIncidentTeamRepository(db)), + incidentJiraService: incident_jira.NewIncidentJiraService( + incidentJiraModel.NewIncidentJiraRepo(db), + externalTeam.NewExternalTeamService(externalTeamRepo.NewExternalTeamRepository(db)), + ), + tagService: tagService, + teamServiceV2: teamServiceV2, + alertService: alertService.NewAlertService(), + teamUserService: teamUserService, + severityService: severityService, + incidentStatusService: incidentStatusService, + incidentTeamService: incidentTeam.NewIncidentTeamService(incidentTeamModel.NewIncidentTeamRepository(db)), incidentTeamTagValueService: incidentTeamTagValue.NewIncidentTeamTagValueService( incidentTeamTagValueRepo.NewIncidentTeamTagValueRepository(db), ), + logService: logService.NewLogService(nil, db), } driveActions, _ := googleDrive.NewGoogleDriveActions() incidentService.rcaService = rcaServiceImpl.NewRcaService( @@ -170,6 +177,10 @@ func NewIncidentServiceV2(db *gorm.DB) *IncidentServiceV2 { rcaInput.NewRcaInputRepository(db), userRepository, google.NewDriveService(driveActions), + incident_jira.NewIncidentJiraService( + incidentJiraModel.NewIncidentJiraRepo(db), + externalTeam.NewExternalTeamService(externalTeamRepo.NewExternalTeamRepository(db)), + ), ) return incidentService } @@ -597,7 +608,7 @@ func (i *IncidentServiceV2) updateJiraIDs(entity *incident.IncidentEntity, user, } func (i *IncidentServiceV2) AddJiraLinks(entity *incident.IncidentEntity, jiraLinks ...string) error { - _, err := i.incidentJiraService.AddJiraLinksByIncidentID(entity.ID, jiraLinks) + _, err := i.incidentJiraService.AddJiraLinksByIncidentID(entity.ID, jiraLinks, "") if err != nil { return err } diff --git a/service/incident/impl/incident_update_status.go b/service/incident/impl/incident_update_status.go index fc72622..8d5d636 100644 --- a/service/incident/impl/incident_update_status.go +++ b/service/incident/impl/incident_update_status.go @@ -538,7 +538,7 @@ func (i *IncidentServiceV2) updateIncidentJiraLinks( if err != nil { return err } - _, err = i.incidentJiraService.AddJiraLinksByIncidentID(incidentEntity.ID, uniqueJiraStrings) + _, err = i.incidentJiraService.AddJiraLinksByIncidentID(incidentEntity.ID, uniqueJiraStrings, "") if err != nil { logger.Error(fmt.Sprintf("%s unable to add jira links for incident %d", logTag, incidentEntity.ID)) i.slackService.PostEphemeralByChannelID( @@ -928,6 +928,19 @@ func (i *IncidentServiceV2) processIncidentRCAFlow(incidentEntity *incident.Inci return } + if i.checkJiraTicketCreationCondition(incidentEntity) { + jiraTicketUrl, err := i.incidentJiraService.CreateJiraTicketForIncident(incidentEntity.ID, incidentEntity.TeamId) + if err != nil { + logger.Error(fmt.Sprintf("%s Error in creating Jira ticket for incident with id: %d", resolveLogTag, incidentEntity.ID), zap.Error(err)) + } else if !util.IsBlank(jiraTicketUrl) { + _, err = i.slackService.PostMessageByChannelID( + fmt.Sprintf("RCA Ticket for the incident can be viewed *<%s|here>*", jiraTicketUrl), + false, + incidentEntity.SlackChannel, + ) + } + } + err := i.rcaService.SendConversationDataForGeneratingRCA( incidentEntity.ID, incidentEntity.IncidentName, incidentEntity.SlackChannel, ) @@ -1015,3 +1028,32 @@ func (i *IncidentServiceV2) runMoveToInvestigationOnIncidents(incidents []incide return nil } + +func (i *IncidentServiceV2) checkJiraTicketCreationCondition(incidentEntity *incident.IncidentEntity) bool { + if incidentEntity.SeverityId != incident.Sev0Id { + logger.Info(fmt.Sprintf("%s Incident severity is not Sev0, skipping Jira ticket creation", updateLogTag)) + return false + } + + reportingTeam, err := i.teamServiceV2.GetTeamById(*incidentEntity.ReportingTeamId) + if err != nil { + logger.Error(fmt.Sprintf("%s Failed to get team with id %d", updateLogTag, incidentEntity.ReportingTeamId), zap.Error(err)) + return false + } + + if reportingTeam.TeamType == string(util.CXTeam) { + logger.Error(fmt.Sprintf("%s Incident reported by CX team, skipping Jira ticket creation", updateLogTag)) + return false + } + + isAutoEscalated, err := i.logService.IsIncidentAutoEscalated(incidentEntity.ID) + if err != nil { + logger.Error(fmt.Sprintf("%s Error while checking if incident is auto escalated", updateLogTag), zap.Error(err)) + return false + } else if isAutoEscalated { + logger.Error(fmt.Sprintf("%s Incident is auto escalated, skipping Jira ticket creation", updateLogTag)) + return false + } + + return true +} diff --git a/service/incident_jira/incident_jira_service.go b/service/incident_jira/incident_jira_service.go index 14f4c88..6a674d8 100644 --- a/service/incident_jira/incident_jira_service.go +++ b/service/incident_jira/incident_jira_service.go @@ -1,16 +1,27 @@ package incident_jira -import "houston/model/incident_jira" +import ( + "houston/model/incident_jira" + "houston/pkg/atlassian" + "houston/pkg/rest" + "houston/service/externalTeam" +) type IncidentJiraService interface { - AddJiraLinksByIncidentID(incidentID uint, jiraLinks []string) ([]uint, error) + AddJiraLinksByIncidentID(incidentID uint, jiraLinks []string, jiraType string) ([]uint, error) GetJiraLinks(incidentName string, pageNumber, pageSize int64) (*[]incident_jira.IncidentJiraLinksDTO, int64, error) GetJiraLinksByIncidentID(incidentID uint) (*[]incident_jira.IncidentJiraEntity, error) GetJiraLinksValuesByIncidentID(incidentID uint) ([]string, error) RemoveJiraLinkByIncidentID(incidentID uint, jiraLink string) (uint, error) RemoveAllJiraLinksByIncidentID(incidentID uint) error + CreateJiraTicketForIncident(incidentID, responderTeamID uint) (string, error) + UpdateJiraTicketForIncident(incidentID uint, rcaLink string) error } -func NewIncidentJiraService(repo incident_jira.IncidentJiraRepository) IncidentJiraService { - return &IncidentJiraServiceImpl{repo} +func NewIncidentJiraService(repo incident_jira.IncidentJiraRepository, externalTeamService externalTeam.ExternalTeamService) IncidentJiraService { + return &IncidentJiraServiceImpl{ + repo, + atlassian.NewJiraClient(rest.NewHttpRestClient()), + externalTeamService, + } } diff --git a/service/incident_jira/incident_jira_service_impl.go b/service/incident_jira/incident_jira_service_impl.go index 8109fe9..5eaa7f2 100644 --- a/service/incident_jira/incident_jira_service_impl.go +++ b/service/incident_jira/incident_jira_service_impl.go @@ -1,23 +1,33 @@ package incident_jira import ( + "encoding/json" "fmt" "github.com/spf13/viper" + "go.uber.org/zap" + "houston/common/jira" + "houston/logger" "houston/model/incident_jira" + "houston/pkg/atlassian" + "houston/service/externalTeam" ) type IncidentJiraServiceImpl struct { - repo incident_jira.IncidentJiraRepository + repo incident_jira.IncidentJiraRepository + jiraClient atlassian.JiraClient + externalTeamService externalTeam.ExternalTeamService } -func (service *IncidentJiraServiceImpl) AddJiraLinksByIncidentID(incidentID uint, jiraLinks []string) ([]uint, error) { +const logTag = "[incident-jira-service]" + +func (service *IncidentJiraServiceImpl) AddJiraLinksByIncidentID(incidentID uint, jiraLinks []string, jiraType string) ([]uint, error) { jiraLinkMaxLength := viper.GetInt("jira.link.max.length") for _, jiraLink := range jiraLinks { if len(jiraLink) > jiraLinkMaxLength { return nil, fmt.Errorf("Jira link %s is too long", jiraLink) } } - return service.repo.InsertJiraLinks(incidentID, jiraLinks) + return service.repo.InsertJiraLinks(incidentID, jiraLinks, jiraType) } func (service *IncidentJiraServiceImpl) GetJiraLinks(incidentName string, pageNumber, pageSize int64) (*[]incident_jira.IncidentJiraLinksDTO, int64, error) { @@ -55,3 +65,66 @@ func (service *IncidentJiraServiceImpl) RemoveJiraLinkByIncidentID(incidentID ui func (service *IncidentJiraServiceImpl) RemoveAllJiraLinksByIncidentID(incidentID uint) error { return service.repo.DeleteAllJiraLinksForIncident(incidentID) } + +func (service *IncidentJiraServiceImpl) CreateJiraTicketForIncident(incidentID, responderTeamID uint) (string, error) { + rcaJiraLinks, _ := service.repo.GetIncidentJiraLinksByIncidentIdAndJiraType(incidentID, jira.RCAJiraType) + if len(rcaJiraLinks) > 0 { + logger.Error(fmt.Sprintf("%s incident already has a RCA Jira ticket", logTag)) + return "", nil + } + + jiraTeam, err := service.externalTeamService.GetExternalTeamByTeamIdAndProvider(responderTeamID, "ATLASSIAN") + if err != nil { + return "", err + } + + teamMetadata := jira.JiraTeamMetadata{} + err = json.Unmarshal(jiraTeam.Metadata, &teamMetadata) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to unmarshal jira team metadata: %+v", logTag, jiraTeam.Metadata), zap.Error(err)) + return "", err + } + + jiraTicketKey, err := service.jiraClient.CreateJiraTicket(jira.CreateJiraTicketInput{ + IncidentID: incidentID, + Metadata: teamMetadata, + }) + if err != nil { + return "", err + } + + jiraTicketUrl := viper.GetString("navi.jira.base.url") + jiraTicketKey + _, err = service.AddJiraLinksByIncidentID(incidentID, []string{jiraTicketUrl}, jira.RCAJiraType) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to add jira link to incident: %+v", logTag, err)) + return "", err + } + + return jiraTicketUrl, nil +} + +func (service *IncidentJiraServiceImpl) UpdateJiraTicketForIncident(incidentID uint, rcaLink string) error { + rcaJiraLinks, err := service.repo.GetIncidentJiraLinksByIncidentIdAndJiraType(incidentID, jira.RCAJiraType) + if err != nil { + logger.Error(fmt.Sprintf("%s incident already has a RCA Jira ticket", logTag)) + return err + } else if len(rcaJiraLinks) == 0 { + logger.Error(fmt.Sprintf("%s incident does not have a RCA Jira ticket", logTag)) + return nil + } + + err = service.jiraClient.UpdateJiraTicket(jira.UpdateJiraTicketInput{ + JiraKey: jira.GetJiraKeyFromLink(rcaJiraLinks[0].JiraLink), + Description: []jira.DescriptionText{ + {Text: jira.IncidentLink, Link: atlassian.GetIncidentPageLink(incidentID)}, + {Text: jira.RCAJiraLink, Link: rcaLink}, + }, + }) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to update jira ticket: %+v", logTag, err)) + } + + return err +} + +const JiraTicketCreation = "JIRA_TICKET_CREATION" diff --git a/service/incident_jira/incident_jira_service_test.go b/service/incident_jira/incident_jira_service_test.go index 89daa91..5bda2f3 100644 --- a/service/incident_jira/incident_jira_service_test.go +++ b/service/incident_jira/incident_jira_service_test.go @@ -1,12 +1,16 @@ package incident_jira import ( + "encoding/json" "errors" "github.com/gojuno/minimock/v3" "github.com/spf13/viper" "github.com/stretchr/testify/suite" + "gorm.io/datatypes" + "houston/common/jira" "houston/logger" "houston/mocks" + "houston/model/externalTeam" "houston/model/incident_jira" "testing" "time" @@ -14,9 +18,11 @@ import ( type IncidentJiraServiceSuite struct { suite.Suite - controller *minimock.Controller - repoMock *mocks.IncidentJiraRepositoryMock - service IncidentJiraService + controller *minimock.Controller + repoMock *mocks.IncidentJiraRepositoryMock + externalTeamService *mocks.ExternalTeamServiceMock + jiraClient *mocks.JiraClientMock + service IncidentJiraService } func (suite *IncidentJiraServiceSuite) SetupTest() { @@ -25,7 +31,13 @@ func (suite *IncidentJiraServiceSuite) SetupTest() { suite.controller = minimock.NewController(suite.T()) suite.T().Cleanup(suite.controller.Finish) suite.repoMock = mocks.NewIncidentJiraRepositoryMock(suite.controller) - suite.service = NewIncidentJiraService(suite.repoMock) + suite.externalTeamService = mocks.NewExternalTeamServiceMock(suite.controller) + suite.jiraClient = mocks.NewJiraClientMock(suite.controller) + suite.service = &IncidentJiraServiceImpl{ + repo: suite.repoMock, + jiraClient: suite.jiraClient, + externalTeamService: suite.externalTeamService, + } } func TestJiraClient(t *testing.T) { @@ -34,9 +46,9 @@ func TestJiraClient(t *testing.T) { func (suite *IncidentJiraServiceSuite) TestIncidentJiraServiceImpl_AddJiraLinksByIncidentID() { - suite.repoMock.InsertJiraLinksMock.When(1, []string{"https://navihq.atlassian.net/browse/TP-48564"}).Then([]uint{1}, nil) + suite.repoMock.InsertJiraLinksMock.When(1, []string{"https://navihq.atlassian.net/browse/TP-48564"}, "").Then([]uint{1}, nil) - _, err := suite.service.AddJiraLinksByIncidentID(1, []string{"https://navihq.atlassian.net/browse/TP-48564"}) + _, err := suite.service.AddJiraLinksByIncidentID(1, []string{"https://navihq.atlassian.net/browse/TP-48564"}, "") if err != nil { suite.Fail("Add Jira Link Failed", err) } @@ -70,6 +82,7 @@ func (suite *IncidentJiraServiceSuite) TestIncidentJiraServiceImpl_RemoveJiraLin func (suite *IncidentJiraServiceSuite) Test_GetJiraLinksValuesByIncidentID_DBError() { suite.repoMock.GetJiraLinksByIncidentIDMock.When(1).Then(nil, errors.New("DB error")) + jiraLinkValues, err := suite.service.GetJiraLinksValuesByIncidentID(1) suite.Error(err, "service must throw error") suite.Nil(jiraLinkValues, "jiraLinkValues must be nil") @@ -77,6 +90,7 @@ func (suite *IncidentJiraServiceSuite) Test_GetJiraLinksValuesByIncidentID_DBErr func (suite *IncidentJiraServiceSuite) Test_GetJiraLinksValuesByIncidentID_NilJiraLinks() { suite.repoMock.GetJiraLinksByIncidentIDMock.When(1).Then(nil, nil) + jiraLinkValues, err := suite.service.GetJiraLinksValuesByIncidentID(1) suite.Nil(err, "err must be nil") suite.Nil(jiraLinkValues, "jiraLinkValues must be nil") @@ -84,6 +98,7 @@ func (suite *IncidentJiraServiceSuite) Test_GetJiraLinksValuesByIncidentID_NilJi func (suite *IncidentJiraServiceSuite) Test_GetJiraLinksValuesByIncidentID_EmptyJiraLinksArray() { suite.repoMock.GetJiraLinksByIncidentIDMock.When(1).Then(&[]incident_jira.IncidentJiraEntity{}, nil) + jiraLinkValues, err := suite.service.GetJiraLinksValuesByIncidentID(1) suite.Nil(err, "err must be nil") suite.Equal(0, len(jiraLinkValues), "jiraLinkValues must be empty") @@ -91,6 +106,7 @@ func (suite *IncidentJiraServiceSuite) Test_GetJiraLinksValuesByIncidentID_Empty func (suite *IncidentJiraServiceSuite) Test_GetJiraLinksValuesByIncidentID_SingleElementJiraLinkArray() { suite.repoMock.GetJiraLinksByIncidentIDMock.When(1).Then(&[]incident_jira.IncidentJiraEntity{GetIncidentJiraEntity(1)}, nil) + jiraLinkValues, err := suite.service.GetJiraLinksValuesByIncidentID(1) suite.Nil(err, "err must be nil") suite.NotNil(jiraLinkValues, "jiraLinkValues must not be nil") @@ -100,12 +116,104 @@ func (suite *IncidentJiraServiceSuite) Test_GetJiraLinksValuesByIncidentID_Singl func (suite *IncidentJiraServiceSuite) Test_GetJiraLinksValuesByIncidentID_MultiElementJiraLinkArray() { jiraEntities := GetIncidentJiraEntities() suite.repoMock.GetJiraLinksByIncidentIDMock.When(1).Then(jiraEntities, nil) + jiraLinkValues, err := suite.service.GetJiraLinksValuesByIncidentID(1) suite.Nil(err, "err must be nil") suite.NotNil(jiraLinkValues, "jiraLinkValues must not be nil") suite.Equal(len(*jiraEntities), len(jiraLinkValues), "jiraLinkValues must have desired element count") } +func (suite *IncidentJiraServiceSuite) Test_CreateJiraTicketForIncident_RCAJiraLinkExists() { + suite.repoMock.GetIncidentJiraLinksByIncidentIdAndJiraTypeMock.When(1, "RCA").Then([]incident_jira.IncidentJiraEntity{GetIncidentJiraEntity(1)}, nil) + + jiraKey, err := suite.service.CreateJiraTicketForIncident(1, 1) + suite.Empty(jiraKey, "jiraKey must be empty") + suite.Nil(err, "err must be nil") +} + +func (suite *IncidentJiraServiceSuite) Test_CreateJiraTicketForIncident_GetExternalTeamError() { + suite.repoMock.GetIncidentJiraLinksByIncidentIdAndJiraTypeMock.When(1, "RCA").Then(nil, nil) + suite.externalTeamService.GetExternalTeamByTeamIdAndProviderMock.Return(nil, errors.New("GetExternalTeamByTeamIdAndProvider error")) + + jiraKey, err := suite.service.CreateJiraTicketForIncident(1, 1) + suite.Empty(jiraKey, "jiraKey must be empty") + suite.NotNil(err, "err must not be nil") +} + +func (suite *IncidentJiraServiceSuite) Test_CreateJiraTicketForIncident_UnmarshalJiraTeamMetadataError() { + suite.repoMock.GetIncidentJiraLinksByIncidentIdAndJiraTypeMock.When(1, "RCA").Then(nil, nil) + externalTeamDTO := GetMockExternalTeam() + externalTeamDTO.Metadata = nil + suite.externalTeamService.GetExternalTeamByTeamIdAndProviderMock.Return(externalTeamDTO, nil) + + jiraKey, err := suite.service.CreateJiraTicketForIncident(1, 1) + suite.Empty(jiraKey, "jiraKey must be empty") + suite.NotNil(err, "err must not be nil") +} + +func (suite *IncidentJiraServiceSuite) Test_CreateJiraTicketForIncident_JiraClientFailure() { + suite.repoMock.GetIncidentJiraLinksByIncidentIdAndJiraTypeMock.When(1, "RCA").Then(nil, nil) + suite.externalTeamService.GetExternalTeamByTeamIdAndProviderMock.Return(GetMockExternalTeam(), nil) + suite.jiraClient.CreateJiraTicketMock.Return("", errors.New("JiraClient.CreateJiraTicket error")) + + jiraKey, err := suite.service.CreateJiraTicketForIncident(1, 1) + suite.Empty(jiraKey, "jiraKey must be empty") + suite.NotNil(err, "err must not be nil") +} + +func (suite *IncidentJiraServiceSuite) Test_CreateJiraTicketForIncident_AddJiraLinksError() { + suite.repoMock.GetIncidentJiraLinksByIncidentIdAndJiraTypeMock.When(1, "RCA").Then(nil, nil) + + suite.externalTeamService.GetExternalTeamByTeamIdAndProviderMock.Return(GetMockExternalTeam(), nil) + suite.jiraClient.CreateJiraTicketMock.Return("TP-12345", nil) + suite.repoMock.InsertJiraLinksMock.Return(nil, errors.New("InsertJiraLinks error")) + + jiraKey, err := suite.service.CreateJiraTicketForIncident(1, 1) + suite.Empty(jiraKey, "jiraKey must be empty") + suite.NotNil(err, "err must not be nil") +} + +func (suite *IncidentJiraServiceSuite) Test_CreateJiraTicketForIncident_Success() { + suite.repoMock.GetIncidentJiraLinksByIncidentIdAndJiraTypeMock.When(1, "RCA").Then(nil, nil) + suite.externalTeamService.GetExternalTeamByTeamIdAndProviderMock.Return(GetMockExternalTeam(), nil) + suite.jiraClient.CreateJiraTicketMock.Return("TP-12345", nil) + suite.repoMock.InsertJiraLinksMock.Return([]uint{1}, nil) + + jiraKey, err := suite.service.CreateJiraTicketForIncident(1, 1) + suite.Equal("TP-12345", jiraKey, "jiraKey must be TP-12345") + suite.Nil(err, "err must be nil") +} + +func (suite *IncidentJiraServiceSuite) Test_UpdateJiraTicketForIncident_GetJiraLinksError() { + suite.repoMock.GetIncidentJiraLinksByIncidentIdAndJiraTypeMock.When(1, "RCA").Then(nil, errors.New("GetIncidentJiraLinksByIncidentIdAndJiraType error")) + + err := suite.service.UpdateJiraTicketForIncident(1, "https://navihq.atlassian.net/browse/TP-48564") + suite.NotNil(err, "err must not be nil") +} + +func (suite *IncidentJiraServiceSuite) Test_UpdateJiraTicketForIncident_NoJiraLinks() { + suite.repoMock.GetIncidentJiraLinksByIncidentIdAndJiraTypeMock.When(1, "RCA").Then(nil, nil) + + err := suite.service.UpdateJiraTicketForIncident(1, "https://navihq.atlassian.net/browse/TP-48564") + suite.Nil(err, "err must be nil") +} + +func (suite *IncidentJiraServiceSuite) Test_UpdateJiraTicketForIncident_UpdateJiraTicketError() { + suite.repoMock.GetIncidentJiraLinksByIncidentIdAndJiraTypeMock.When(1, "RCA").Then([]incident_jira.IncidentJiraEntity{GetIncidentJiraEntity(1)}, nil) + suite.jiraClient.UpdateJiraTicketMock.Return(errors.New("UpdateJiraTicket error")) + + err := suite.service.UpdateJiraTicketForIncident(1, "https://navihq.atlassian.net/browse/TP-48564") + suite.NotNil(err, "err must not be nil") +} + +func (suite *IncidentJiraServiceSuite) Test_UpdateJiraTicketForIncident_Success() { + suite.repoMock.GetIncidentJiraLinksByIncidentIdAndJiraTypeMock.When(1, "RCA").Then([]incident_jira.IncidentJiraEntity{GetIncidentJiraEntity(1)}, nil) + suite.jiraClient.UpdateJiraTicketMock.Return(nil) + + err := suite.service.UpdateJiraTicketForIncident(1, "https://navihq.atlassian.net/browse/TP-48564") + suite.Nil(err, "err must be nil") +} + func GetIncidentJiraEntity(id uint) incident_jira.IncidentJiraEntity { return incident_jira.IncidentJiraEntity{ ID: id, @@ -122,3 +230,34 @@ func GetIncidentJiraEntities() *[]incident_jira.IncidentJiraEntity { GetIncidentJiraEntity(3), } } + +func GetMockExternalTeam() *externalTeam.ExternalTeamDTO { + teamMetadataJson, err := json.Marshal(jira.JiraTeamMetadata{ + CreateIssueConfig: jira.CreateIssueConfig{ + Fields: []jira.Field{ + { + Name: "a", + Key: "b", + Value: datatypes.JSON(`"Houston 12345"`), + }, + }, + }, + }) + if err != nil { + return nil + } + + return &externalTeam.ExternalTeamDTO{ + ID: 0, + ExternalTeamID: "", + ExternalTeamName: "", + Metadata: teamMetadataJson, + ProviderName: "", + TeamID: 0, + IsActive: false, + CreatedAt: time.Time{}, + UpdatedAt: time.Time{}, + CreatedBy: "", + UpdatedBy: "", + } +} diff --git a/service/log_service.go b/service/log/log_service.go similarity index 80% rename from service/log_service.go rename to service/log/log_service.go index 4c408ff..efb3f2b 100644 --- a/service/log_service.go +++ b/service/log/log_service.go @@ -1,4 +1,4 @@ -package service +package log import ( "encoding/json" @@ -15,22 +15,22 @@ import ( "strconv" ) -type logService struct { +type LogService struct { gin *gin.Engine db *gorm.DB logRepository *log.Repository } -func NewLogService(gin *gin.Engine, db *gorm.DB) *logService { +func NewLogService(gin *gin.Engine, db *gorm.DB) *LogService { logRepository := log.NewLogRepository(db) - return &logService{ + return &LogService{ gin: gin, db: db, logRepository: logRepository, } } -func (l *logService) GetLogs(c *gin.Context) { +func (l *LogService) GetLogs(c *gin.Context) { logType := c.Param("log_type") id := c.Param("id") @@ -77,3 +77,13 @@ func (l *logService) GetLogs(c *gin.Context) { c.JSON(http.StatusOK, common.SuccessResponse(logResponse, http.StatusOK)) } + +func (l *LogService) IsIncidentAutoEscalated(incidentID uint) (bool, error) { + isAutoEscalated, err := l.logRepository.IsIncidentAutoEscalated(incidentID) + if err != nil { + logger.Error("error in checking if incident is auto escalated", zap.Uint("incident_id", incidentID), zap.Error(err)) + return false, err + } + + return isAutoEscalated, nil +} diff --git a/service/rca/impl/rca_service.go b/service/rca/impl/rca_service.go index 7a1f6a7..eec68f8 100644 --- a/service/rca/impl/rca_service.go +++ b/service/rca/impl/rca_service.go @@ -23,6 +23,7 @@ import ( rcaInputRepository "houston/repository/rcaInput" "houston/service/google" "houston/service/incident" + "houston/service/incident_jira" service "houston/service/request" response "houston/service/response" common "houston/service/response/common" @@ -35,30 +36,35 @@ import ( ) type RcaService struct { - incidentService incident.IIncidentService - slackService slackService.ISlackService - documentService documentService.ServiceActions - maverickClient maverick.IMaverickClient - rcaRepository rcaRepository.IRcaRepository - rcaInputRepository rcaInputRepository.IRcaInputRepository - userRepository user.IUserRepository - driveService google.IDriveService + incidentService incident.IIncidentService + slackService slackService.ISlackService + documentService documentService.ServiceActions + maverickClient maverick.IMaverickClient + rcaRepository rcaRepository.IRcaRepository + rcaInputRepository rcaInputRepository.IRcaInputRepository + userRepository user.IUserRepository + driveService google.IDriveService + incidentJiraService incident_jira.IncidentJiraService } const logTag = "[post-rca]" const SlackIdMentionPatternString = `<@(\w+)>` -func NewRcaService(incidentService incident.IIncidentService, slackService slackService.ISlackService, documentService documentService.ServiceActions, rcaRepository rcaRepository.IRcaRepository, - rcaInputRepository rcaInputRepository.IRcaInputRepository, userRepository user.IUserRepository, driveService google.IDriveService) *RcaService { +func NewRcaService( + incidentService incident.IIncidentService, slackService slackService.ISlackService, + documentService documentService.ServiceActions, rcaRepository rcaRepository.IRcaRepository, + rcaInputRepository rcaInputRepository.IRcaInputRepository, userRepository user.IUserRepository, + driveService google.IDriveService, incidentJiraService incident_jira.IncidentJiraService) *RcaService { return &RcaService{ - incidentService: incidentService, - slackService: slackService, - documentService: documentService, - maverickClient: maverick.NewMaverickClient(), - rcaRepository: rcaRepository, - rcaInputRepository: rcaInputRepository, - userRepository: userRepository, - driveService: driveService, + incidentService: incidentService, + slackService: slackService, + documentService: documentService, + maverickClient: maverick.NewMaverickClient(), + rcaRepository: rcaRepository, + rcaInputRepository: rcaInputRepository, + userRepository: userRepository, + driveService: driveService, + incidentJiraService: incidentJiraService, } } @@ -129,6 +135,12 @@ func (r *RcaService) PostRcaToIncidentChannel(postRcaRequest service.PostRcaRequ return errors.New("Could not post RCA to the given incident channel") } + err = r.incidentJiraService.UpdateJiraTicketForIncident(incidentEntity.ID, successData.RcaLink) + if err != nil { + logger.Error(fmt.Sprintf("%s Error while updating jira ticket for incident id: %d", logTag, incidentEntity.ID), zap.Error(err)) + return err + } + case util.RcaStatusFailure: metrics.PublishHoustonFlowFailureMetrics(RCA_GENERATION, fmt.Sprintf("RCA generation failed for incident id: %d due to %s", postRcaRequest.IncidentID, string(postRcaRequest.Data))) var failureData service.FailureData diff --git a/service/rca/impl/rca_service_test.go b/service/rca/impl/rca_service_test.go index d387507..e1d20c0 100644 --- a/service/rca/impl/rca_service_test.go +++ b/service/rca/impl/rca_service_test.go @@ -27,7 +27,7 @@ import ( func setupTest(t *testing.T) (*mocks.IIncidentServiceMock, *mocks.ISlackServiceMock, *mocks.IRcaRepositoryMock, *RcaService, *mocks.IRcaInputRepositoryMock, *mocks.ServiceActionsMock, *mocks.IMaverickClientMock, - *mocks.IUserRepositoryMock, *mocks.IDriveServiceMock) { + *mocks.IUserRepositoryMock, *mocks.IDriveServiceMock, *mocks.IncidentJiraServiceMock) { logger.InitLogger() incidentService := mocks.NewIIncidentServiceMock(t) slackService := mocks.NewISlackServiceMock(t) @@ -37,23 +37,25 @@ func setupTest(t *testing.T) (*mocks.IIncidentServiceMock, *mocks.ISlackServiceM maverickClient := mocks.NewIMaverickClientMock(t) userRepository := mocks.NewIUserRepositoryMock(t) driveService := mocks.NewIDriveServiceMock(t) + incidentJiraService := mocks.NewIncidentJiraServiceMock(t) rcaService := &RcaService{ - incidentService: incidentService, - slackService: slackService, - documentService: documentService, - maverickClient: maverickClient, - rcaRepository: rcaRepository, - rcaInputRepository: rcaInputRepository, - userRepository: userRepository, - driveService: driveService, + incidentService: incidentService, + slackService: slackService, + documentService: documentService, + maverickClient: maverickClient, + rcaRepository: rcaRepository, + rcaInputRepository: rcaInputRepository, + userRepository: userRepository, + driveService: driveService, + incidentJiraService: incidentJiraService, } return incidentService, slackService, rcaRepository, rcaService, rcaInputRepository, documentService, - maverickClient, userRepository, driveService + maverickClient, userRepository, driveService, incidentJiraService } func TestRcaService_PostRcaToIncidentChannel_Success(t *testing.T) { - incidentService, slackService, rcaRepository, rcaService, _, _, _, _, _ := setupTest(t) + incidentService, slackService, rcaRepository, rcaService, _, _, _, _, _, incidentJiraService := setupTest(t) incidentService.GetIncidentByIdMock.When(1).Then(GetMockIncident(), nil) @@ -69,6 +71,8 @@ func TestRcaService_PostRcaToIncidentChannel_Success(t *testing.T) { "1234", ).Then("", nil) + incidentJiraService.UpdateJiraTicketForIncidentMock.Return(nil) + testData, _ := json.Marshal(service.SuccessData{RcaLink: "test_link"}) err := rcaService.PostRcaToIncidentChannel(service.PostRcaRequest{ @@ -80,7 +84,7 @@ func TestRcaService_PostRcaToIncidentChannel_Success(t *testing.T) { } func TestRcaService_PostRcaToIncidentChannel_Failure(t *testing.T) { - incidentService, slackService, rcaRepository, rcaService, _, _, _, _, _ := setupTest(t) + incidentService, slackService, rcaRepository, rcaService, _, _, _, _, _, _ := setupTest(t) incidentService.GetIncidentByIdMock.When(1).Then(GetMockIncident(), nil) @@ -109,7 +113,7 @@ func TestRcaService_PostRcaToIncidentChannel_Failure(t *testing.T) { } func TestRcaService_PostRcaToIncidentChannel_InvalidIncident(t *testing.T) { - incidentService, _, _, rcaService, _, _, _, _, _ := setupTest(t) + incidentService, _, _, rcaService, _, _, _, _, _, _ := setupTest(t) incidentService.GetIncidentByIdMock.When(1).Then(nil, errors.New("record not found")) @@ -124,7 +128,7 @@ func TestRcaService_PostRcaToIncidentChannel_InvalidIncident(t *testing.T) { } func TestRcaService_PostRcaToIncidentChannel_NonExistingRca(t *testing.T) { - incidentService, _, rcaRepository, rcaService, _, _, _, _, _ := setupTest(t) + incidentService, _, rcaRepository, rcaService, _, _, _, _, _, _ := setupTest(t) incidentService.GetIncidentByIdMock.When(1).Then(GetMockIncident(), nil) @@ -141,7 +145,7 @@ func TestRcaService_PostRcaToIncidentChannel_NonExistingRca(t *testing.T) { } func TestRcaService_PostRcaToIncidentChannel_UpdateFailure(t *testing.T) { - incidentService, _, rcaRepository, rcaService, _, _, _, _, _ := setupTest(t) + incidentService, _, rcaRepository, rcaService, _, _, _, _, _, _ := setupTest(t) incidentService.GetIncidentByIdMock.When(1).Then(GetMockIncident(), nil) @@ -163,7 +167,7 @@ func TestRcaService_PostRcaToIncidentChannel_UpdateFailure(t *testing.T) { } func TestRcaService_PostRcaToIncidentChannel_SlackFailure_RcaSuccessCase(t *testing.T) { - incidentService, slackService, rcaRepository, rcaService, _, _, _, _, _ := setupTest(t) + incidentService, slackService, rcaRepository, rcaService, _, _, _, _, _, _ := setupTest(t) incidentService.GetIncidentByIdMock.When(1).Then(GetMockIncident(), nil) @@ -191,7 +195,7 @@ func TestRcaService_PostRcaToIncidentChannel_SlackFailure_RcaSuccessCase(t *test } func TestRcaService_PostRcaToIncidentChannel_SlackFailure_RcaFailureCase(t *testing.T) { - incidentService, slackService, rcaRepository, rcaService, _, _, _, _, _ := setupTest(t) + incidentService, slackService, rcaRepository, rcaService, _, _, _, _, _, _ := setupTest(t) incidentService.GetIncidentByIdMock.When(1).Then(GetMockIncident(), nil) @@ -220,7 +224,7 @@ func TestRcaService_PostRcaToIncidentChannel_SlackFailure_RcaFailureCase(t *test } func TestUploadSlackConversationHistory(t *testing.T) { - incidentService, slackService, _, rcaService, _, documentService, _, userRepository, _ := setupTest(t) + incidentService, slackService, _, rcaService, _, documentService, _, userRepository, _, _ := setupTest(t) incidentService.GetIncidentByIdMock.When(1).Then(GetMockIncident(), nil) slackService.GetSlackConversationHistoryWithRepliesMock.When("channelID").Then(*getMockConversation(), nil) @@ -239,7 +243,7 @@ func TestUploadSlackConversationHistory(t *testing.T) { } func TestExtractUserIds(t *testing.T) { - _, _, _, rcaService, _, _, _, _, _ := setupTest(t) + _, _, _, rcaService, _, _, _, _, _, _ := setupTest(t) testConversation := getMockConversation() testUserIdMap := make(map[string]response.UserResponse) @@ -267,7 +271,7 @@ func TestExtractUserIds(t *testing.T) { } func TestReplaceUserIds(t *testing.T) { - _, _, _, rcaService, _, _, _, _, _ := setupTest(t) + _, _, _, rcaService, _, _, _, _, _, _ := setupTest(t) // replacing user ids successfully testConversation := getMockConversation() testUserIdMap := getMockUserIdMap() @@ -291,7 +295,7 @@ func TestReplaceUserIds(t *testing.T) { } func TestGetExtraConversationData(t *testing.T) { - incidentService, _, _, rcaService, _, _, _, _, _ := setupTest(t) + incidentService, _, _, rcaService, _, _, _, _, _, _ := setupTest(t) incidentService.GetIncidentByIdMock.When(1).Then(&incident.IncidentEntity{StartTime: time.Now()}, nil) actualResponse, err := rcaService.getExtraConversationData(1) @@ -306,7 +310,7 @@ func TestGetExtraConversationData(t *testing.T) { } func TestUploadTranscriptFilesAndGetDownloadUrlsSuccess(t *testing.T) { - _, _, _, rcaService, _, documentService, _, _, _ := setupTest(t) + _, _, _, rcaService, _, documentService, _, _, _, _ := setupTest(t) mockDirectory := t.TempDir() createMockDirectoryWithDummyFiles(mockDirectory) @@ -329,7 +333,7 @@ func TestUploadTranscriptFilesAndGetDownloadUrlsSuccess(t *testing.T) { util.DeleteDirectory(mockDirectory) } func TestUploadTranscriptFilesAndGetDownloadUrls_NoFilesUploadedFailure(t *testing.T) { - _, _, _, rcaService, _, _, _, _, _ := setupTest(t) + _, _, _, rcaService, _, _, _, _, _, _ := setupTest(t) mockDirectory := t.TempDir() fileType := "fileType" @@ -346,7 +350,7 @@ func TestUploadTranscriptFilesAndGetDownloadUrls_NoFilesUploadedFailure(t *testi util.DeleteDirectory(mockDirectory) } func TestUploadTranscriptFilesAndGetDownloadUrls_InvalidDirectoryFailure(t *testing.T) { - _, _, _, rcaService, _, _, _, _, _ := setupTest(t) + _, _, _, rcaService, _, _, _, _, _, _ := setupTest(t) mockDirectory := t.TempDir() createMockDirectoryWithDummyFiles(mockDirectory) @@ -364,7 +368,7 @@ func TestUploadTranscriptFilesAndGetDownloadUrls_InvalidDirectoryFailure(t *test } func TestCollectAndDownloadTranscripts_NilDriveServiceFailure(t *testing.T) { - _, _, _, rcaService, _, _, _, _, _ := setupTest(t) + _, _, _, rcaService, _, _, _, _, _, _ := setupTest(t) rcaService.driveService = nil directoryPathResponseCh := make(chan model.DirectoryPathResponse) go rcaService.collectAndDownloadTranscripts("incidentName", directoryPathResponseCh) @@ -375,7 +379,7 @@ func TestCollectAndDownloadTranscripts_NilDriveServiceFailure(t *testing.T) { } } func TestCollectAndDownloadTranscripts_Failure(t *testing.T) { - _, _, _, rcaService, _, _, _, _, driveService := setupTest(t) + _, _, _, rcaService, _, _, _, _, driveService, _ := setupTest(t) driveService.CollectTranscriptsMock.When("incidentName").Then(nil, errors.New("error")) driveService.CollectTranscriptsMock.When("incidentName2").Then(nil, nil) @@ -397,7 +401,7 @@ func TestCollectAndDownloadTranscripts_Failure(t *testing.T) { } } func TestCollectAndDownloadTranscripts_Success(t *testing.T) { - _, _, _, rcaService, _, _, _, _, driveService := setupTest(t) + _, _, _, rcaService, _, _, _, _, driveService, _ := setupTest(t) driveService.CollectTranscriptsMock.When("incidentName").Then(nil, nil) driveService.DownloadTranscriptsMock.When(nil, util.ContentTypeTextHTML).Then("mockDirectory", nil) @@ -413,7 +417,7 @@ func TestCollectAndDownloadTranscripts_Success(t *testing.T) { } func TestGetConversationDataForGeneratingRCA_DownloadTranscriptsFailure(t *testing.T) { - incidentService, slackService, _, rcaService, _, documentService, _, userRepository, driveService := setupTest(t) + incidentService, slackService, _, rcaService, _, documentService, _, userRepository, driveService, _ := setupTest(t) // uploading slack conversation successful channelID := "channelID" incidentService.GetIncidentByIdMock.When(1).Then(GetMockIncident(), nil) @@ -438,7 +442,7 @@ func TestGetConversationDataForGeneratingRCA_DownloadTranscriptsFailure(t *testi } func TestGetConversationDataForGeneratingRCA_UploadTranscriptsFailure(t *testing.T) { - incidentService, slackService, _, rcaService, _, documentService, _, userRepository, driveService := setupTest(t) + incidentService, slackService, _, rcaService, _, documentService, _, userRepository, driveService, _ := setupTest(t) // uploading slack conversation successful channelID := "channelID" incidentService.GetIncidentByIdMock.When(1).Then(GetMockIncident(), nil) @@ -464,7 +468,7 @@ func TestGetConversationDataForGeneratingRCA_UploadTranscriptsFailure(t *testing } func TestGetConversationDataForGeneratingRCA_uploadSlackDataFailure(t *testing.T) { - _, slackService, _, rcaService, _, _, _, _, driveService := setupTest(t) + _, slackService, _, rcaService, _, _, _, _, driveService, _ := setupTest(t) slackService.GetSlackConversationHistoryWithRepliesMock.When("channelID").Then(nil, errors.New("error")) directoryPathCh := make(chan model.DirectoryPathResponse) driveService.CollectTranscriptsMock.When("incidentName1").Then(nil, nil) @@ -475,7 +479,7 @@ func TestGetConversationDataForGeneratingRCA_uploadSlackDataFailure(t *testing.T } func TestSaveRcaInput_Failure(t *testing.T) { - _, _, _, rcaService, rcaInputRepository, _, _, _, _ := setupTest(t) + _, _, _, rcaService, rcaInputRepository, _, _, _, _, _ := setupTest(t) rcaInputRepository.CreateRcaInputMock.Inspect(func(rcaInput *rcaInput.RcaInputEntity) { assert.Equal(t, uint(1), rcaInput.IncidentID) @@ -490,7 +494,7 @@ func TestSaveRcaInput_Failure(t *testing.T) { } func TestSaveRcaInput_Success(t *testing.T) { - _, _, rcaRepository, rcaService, rcaInputRepository, _, _, _, _ := setupTest(t) + _, _, rcaRepository, rcaService, rcaInputRepository, _, _, _, _, _ := setupTest(t) rcaInputRepository.CreateRcaInputMock.Inspect(func(rcaInput *rcaInput.RcaInputEntity) { assert.Equal(t, uint(1), rcaInput.IncidentID) @@ -504,14 +508,14 @@ func TestSaveRcaInput_Success(t *testing.T) { } func TestGetRcaInputPreSignedUrl_DBFailure(t *testing.T) { - _, _, _, rcaService, rcaInputRepository, _, _, _, _ := setupTest(t) + _, _, _, rcaService, rcaInputRepository, _, _, _, _, _ := setupTest(t) rcaInputRepository.GetLatestRcaInputByIncidentIdAndDocumentTypeMock.Return(nil, errors.New("error")) preSignedUrl, err := rcaService.GetRcaInputPreSignedUrl(1) assert.Error(t, err) assert.Equal(t, "", preSignedUrl) } func TestGetRcaInputPreSignedUrl_DocServiceFailure(t *testing.T) { - _, _, _, rcaService, rcaInputRepository, documentService, _, _, _ := setupTest(t) + _, _, _, rcaService, rcaInputRepository, documentService, _, _, _, _ := setupTest(t) rcaSlackInput := &rcaInput.RcaInputEntity{ IncidentID: 1, IdentifierKeys: []string{"key"}, @@ -527,7 +531,7 @@ func TestGetRcaInputPreSignedUrl_DocServiceFailure(t *testing.T) { assert.Equal(t, "", preSignedUrl) } func TestGetRcaInputPreSignedUrl_Success(t *testing.T) { - _, _, _, rcaService, rcaInputRepository, documentService, _, _, _ := setupTest(t) + _, _, _, rcaService, rcaInputRepository, documentService, _, _, _, _ := setupTest(t) rcaSlackInput := &rcaInput.RcaInputEntity{ IncidentID: 1, IdentifierKeys: []string{"key"}, @@ -706,7 +710,7 @@ func getMockRCA() *rca.RcaEntity { } func TestGetIncidentResponseWithRCALinkSuccess(t *testing.T) { - incidentService, _, rcaRepository, rcaService, _, _, _, _, _ := setupTest(t) + incidentService, _, rcaRepository, rcaService, _, _, _, _, _, _ := setupTest(t) mockIncident := GetMockIncident() mockIncident.Status = incident.ResolvedId testRca := getMockRCA() @@ -724,7 +728,7 @@ func TestGetIncidentResponseWithRCALinkSuccess(t *testing.T) { } func TestGetIncidentResponseWithRCALinkFailureAtFetchRCA(t *testing.T) { - incidentService, _, rcaRepository, rcaService, _, _, _, _, _ := setupTest(t) + incidentService, _, rcaRepository, rcaService, _, _, _, _, _, _ := setupTest(t) mockIncident := GetMockIncident() mockIncident.Status = incident.ResolvedId testRca := getMockRCA() @@ -741,7 +745,7 @@ func TestGetIncidentResponseWithRCALinkFailureAtFetchRCA(t *testing.T) { } func TestGetIncidentResponseWithRCALinkSuccessForDuplicateStatus(t *testing.T) { - incidentService, _, _, rcaService, _, _, _, _, _ := setupTest(t) + incidentService, _, _, rcaService, _, _, _, _, _, _ := setupTest(t) mockIncident := GetMockIncident() mockIncident.Status = incident.DuplicateId incidentService.GetIncidentByIdMock.When(mockIncident.ID).Then(mockIncident, nil) @@ -751,7 +755,7 @@ func TestGetIncidentResponseWithRCALinkSuccessForDuplicateStatus(t *testing.T) { } func TestGetIncidentResponseWithRCALinkSuccessForInvestigatingStatus(t *testing.T) { - incidentService, _, _, rcaService, _, _, _, _, _ := setupTest(t) + incidentService, _, _, rcaService, _, _, _, _, _, _ := setupTest(t) mockIncident := GetMockIncident() mockIncident.Status = incident.InvestigatingId incidentService.GetIncidentByIdMock.When(mockIncident.ID).Then(mockIncident, nil) @@ -761,7 +765,7 @@ func TestGetIncidentResponseWithRCALinkSuccessForInvestigatingStatus(t *testing. } func TestGetIncidentResponseWithRCALinkSuccessForMonitoringStatus(t *testing.T) { - incidentService, _, _, rcaService, _, _, _, _, _ := setupTest(t) + incidentService, _, _, rcaService, _, _, _, _, _, _ := setupTest(t) mockIncident := GetMockIncident() mockIncident.Status = incident.MonitoringId incidentService.GetIncidentByIdMock.When(mockIncident.ID).Then(mockIncident, nil) @@ -771,7 +775,7 @@ func TestGetIncidentResponseWithRCALinkSuccessForMonitoringStatus(t *testing.T) } func TestGetIncidentResponseWithRCALinkSuccessForIdentifiedStatus(t *testing.T) { - incidentService, _, _, rcaService, _, _, _, _, _ := setupTest(t) + incidentService, _, _, rcaService, _, _, _, _, _, _ := setupTest(t) mockIncident := GetMockIncident() mockIncident.Status = incident.IdentifiedId incidentService.GetIncidentByIdMock.When(mockIncident.ID).Then(mockIncident, nil) @@ -782,7 +786,7 @@ func TestGetIncidentResponseWithRCALinkSuccessForIdentifiedStatus(t *testing.T) func TestRcaService_GenerateRCA_RCA_Disabled(t *testing.T) { viper.Set("RCA_GENERATION_ENABLED", false) - incidentService, slackServiceMock, _, rcaService, _, _, _, _, _ := setupTest(t) + incidentService, slackServiceMock, _, rcaService, _, _, _, _, _, _ := setupTest(t) mockIncident := GetMockIncident() incidentService.GetIncidentByIdMock.When(mockIncident.ID).Then(mockIncident, nil) slackServiceMock.PostEphemeralByChannelIDMock.Return(errors.New("error")) @@ -792,7 +796,7 @@ func TestRcaService_GenerateRCA_RCA_Disabled(t *testing.T) { func TestRcaService_GenerateRCA_Disabled_Success(t *testing.T) { viper.Set("RCA_GENERATION_ENABLED", false) - incidentService, slackServiceMock, _, rcaService, _, _, _, _, _ := setupTest(t) + incidentService, slackServiceMock, _, rcaService, _, _, _, _, _, _ := setupTest(t) mockIncident := GetMockIncident() incidentService.GetIncidentByIdMock.When(mockIncident.ID).Then(mockIncident, nil) slackServiceMock.PostEphemeralByChannelIDMock.Return(nil) @@ -802,7 +806,7 @@ func TestRcaService_GenerateRCA_Disabled_Success(t *testing.T) { func TestRcaService_GenerateRCA_Enabled_FailureAtGetConversations(t *testing.T) { viper.Set("RCA_GENERATION_ENABLED", true) - incidentService, slackServiceMock, _, rcaService, _, _, _, _, driveServiceMock := setupTest(t) + incidentService, slackServiceMock, _, rcaService, _, _, _, _, driveServiceMock, _ := setupTest(t) mockIncident := GetMockIncident() incidentService.GetIncidentByIdMock.When(mockIncident.ID).Then(mockIncident, nil) rcaAServiceMock := mocks.NewIRcaServiceMock(t) @@ -816,7 +820,7 @@ func TestRcaService_GenerateRCA_Enabled_FailureAtGetConversations(t *testing.T) func TestRcaService_GenerateRCA_Enabled_FailureAtGetConversationsPostRetryButtonMessage(t *testing.T) { viper.Set("RCA_GENERATION_ENABLED", true) - incidentService, slackServiceMock, _, rcaService, _, _, _, _, driveServiceMock := setupTest(t) + incidentService, slackServiceMock, _, rcaService, _, _, _, _, driveServiceMock, _ := setupTest(t) mockIncident := GetMockIncident() incidentService.GetIncidentByIdMock.When(mockIncident.ID).Then(mockIncident, nil) rcaAServiceMock := mocks.NewIRcaServiceMock(t) diff --git a/service/request/team/add_team.go b/service/request/team/add_team.go index 93ff4ca..0c63cdc 100644 --- a/service/request/team/add_team.go +++ b/service/request/team/add_team.go @@ -3,6 +3,7 @@ package team type AddTeamRequest struct { Name string `json:"name"` ManagerDetails *ManagerDetails `json:"manager_details"` + TeamType string `json:"team_type"` } type ManagerDetails struct { diff --git a/service/teamService/team_service_v2.go b/service/teamService/team_service_v2.go index b904700..28a406b 100644 --- a/service/teamService/team_service_v2.go +++ b/service/teamService/team_service_v2.go @@ -381,6 +381,7 @@ func (teamService *TeamServiceV2) AddTeam(request teamRequest.AddTeamRequest, us CreatedBy: userEmail, UpdatedBy: userEmail, Active: true, + TeamType: request.TeamType, } _, err = teamService.teamRepository.CreateTeam(teamEntity)