501 lines
15 KiB
Go
501 lines
15 KiB
Go
package orchestration
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
slack2 "github.com/slack-go/slack"
|
|
"go.uber.org/zap"
|
|
"gorm.io/gorm"
|
|
"houston/appcontext"
|
|
"houston/common/util"
|
|
"houston/logger"
|
|
"houston/model/incident"
|
|
"houston/model/product"
|
|
"houston/model/team"
|
|
user2 "houston/model/user"
|
|
incidentService "houston/service/incident"
|
|
incidentServiceImpl "houston/service/incident/impl"
|
|
"houston/service/products"
|
|
"houston/service/productsTeams"
|
|
incidentRequest "houston/service/request/incident"
|
|
response "houston/service/response"
|
|
"houston/service/severity"
|
|
"houston/service/slack"
|
|
"houston/service/teamService"
|
|
"houston/service/teamUser"
|
|
"houston/service/user"
|
|
utils "houston/service/utils"
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type incidentOrchestratorImpl struct {
|
|
productTeamService productsTeams.ProductTeamsService
|
|
productsService products.ProductService
|
|
teamUserService teamUser.ITeamUserService
|
|
userService user.UserService
|
|
teamService teamService.ITeamServiceV2
|
|
severityService severity.ISeverityService
|
|
slackService slack.ISlackService
|
|
incidentService incidentService.IIncidentService
|
|
}
|
|
|
|
func newIncidentOrchestratorImpl(
|
|
productTeamService productsTeams.ProductTeamsService,
|
|
productsService products.ProductService,
|
|
teamUserService teamUser.ITeamUserService,
|
|
userService user.UserService,
|
|
teamService teamService.ITeamServiceV2,
|
|
severityService severity.ISeverityService,
|
|
slackService slack.ISlackService,
|
|
incidentService incidentService.IIncidentService,
|
|
) *incidentOrchestratorImpl {
|
|
return &incidentOrchestratorImpl{
|
|
productTeamService: productTeamService,
|
|
productsService: productsService,
|
|
teamUserService: teamUserService,
|
|
userService: userService,
|
|
teamService: teamService,
|
|
severityService: severityService,
|
|
slackService: slackService,
|
|
incidentService: incidentService,
|
|
}
|
|
}
|
|
|
|
const (
|
|
defaultTeam = "Others"
|
|
logTag = "[incident_orchestrator]"
|
|
)
|
|
|
|
func (i *incidentOrchestratorImpl) GetProductsOfUserByEmailID(emailID string) (*response.ProductsOfUser, error) {
|
|
return i.getProductsOfTeams(i.getTeamsOfUserByEmailID(emailID))
|
|
}
|
|
|
|
func (i *incidentOrchestratorImpl) GetProductsOfUserBySlackUserID(slackUserID string) (*response.ProductsOfUser, error) {
|
|
return i.getProductsOfTeams(i.getTeamsOfUserBySlackUserID(slackUserID))
|
|
}
|
|
|
|
func (i *incidentOrchestratorImpl) getProductsOfTeams(teams []team.TeamDTO) (*response.ProductsOfUser, error) {
|
|
var productsOfUser []product.ProductDTO
|
|
var err error
|
|
var teamIDs []uint
|
|
for _, t := range teams {
|
|
teamIDs = append(teamIDs, t.ID)
|
|
}
|
|
productsOfUser = i.productTeamService.GetProductsByTeamId(teamIDs...)
|
|
allProducts, err := i.productsService.GetAllProducts()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var defaultProduct *product.ProductDTO = nil
|
|
if len(productsOfUser) == 1 {
|
|
defaultProduct = &productsOfUser[0]
|
|
}
|
|
|
|
return &response.ProductsOfUser{
|
|
DefaultProduct: defaultProduct,
|
|
Products: allProducts,
|
|
}, nil
|
|
}
|
|
|
|
func (i *incidentOrchestratorImpl) GetReportingAndResponderTeams(
|
|
emailID string, productIDs []uint,
|
|
) (*response.ReportingAndResponderTeams, error) {
|
|
reportingTeams := i.getTeamsOfUserByEmailID(emailID)
|
|
return i.getReportingAndResponderTeams(reportingTeams, productIDs)
|
|
}
|
|
|
|
func (i *incidentOrchestratorImpl) GetReportingAndResponderTeamsBySlackUserId(
|
|
slackUserID string, productIDs []uint,
|
|
) (*response.ReportingAndResponderTeams, error) {
|
|
reportingTeams := i.getTeamsOfUserBySlackUserID(slackUserID)
|
|
return i.getReportingAndResponderTeams(reportingTeams, productIDs)
|
|
}
|
|
|
|
func (i *incidentOrchestratorImpl) GetResponderTeamsForUpdate(
|
|
slackChannelID string, productIDs []uint,
|
|
) *response.ProductAndUserTeams {
|
|
logger.Info("[GetResponderTeamsForUpdate] channel id is: " + slackChannelID)
|
|
incidentEntity, err := i.incidentService.GetIncidentByChannelID(slackChannelID)
|
|
if err != nil {
|
|
logger.Error(fmt.Sprintf("%s Error in fetching incident by channel id: %s", logTag, slackChannelID), zap.Error(err))
|
|
return nil
|
|
}
|
|
teamEntity, err := i.teamService.GetTeamById(incidentEntity.TeamId)
|
|
if err != nil {
|
|
logger.Error(fmt.Sprintf("%s Error in fetching default team", logTag), zap.Error(err))
|
|
}
|
|
if productIDs == nil {
|
|
currentProducts := incidentEntity.Products
|
|
for _, p := range currentProducts {
|
|
productIDs = append(productIDs, p.ID)
|
|
}
|
|
}
|
|
teamsOfProducts, err := i.getTeamsOfProducts(productIDs)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
if ifTeamExists(teamsOfProducts, teamEntity) {
|
|
teamsOfProducts.DefaultTeam = teamEntity.ToDTO()
|
|
}
|
|
|
|
return teamsOfProducts
|
|
}
|
|
|
|
func (i *incidentOrchestratorImpl) GetProductsForUpdate(slackChannelID string) *response.ProductsForUpdate {
|
|
incidentEntity, err := i.incidentService.GetIncidentByChannelID(slackChannelID)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
var currentProducts []product.ProductDTO
|
|
for _, p := range incidentEntity.Products {
|
|
currentProducts = append(currentProducts, *p.ToDTO())
|
|
}
|
|
allProducts, err := i.productsService.GetAllProducts()
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
productsForUpdate := &response.ProductsForUpdate{
|
|
DefaultProducts: currentProducts,
|
|
Products: allProducts,
|
|
}
|
|
return productsForUpdate
|
|
}
|
|
|
|
func ifTeamExists(teamsOfProducts *response.ProductAndUserTeams, teamEntity *team.TeamEntity) bool {
|
|
for _, t := range teamsOfProducts.Teams {
|
|
if t.ID == teamEntity.ID {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (i *incidentOrchestratorImpl) getReportingAndResponderTeams(
|
|
teamsOfUser []team.TeamDTO, productIDs []uint,
|
|
) (*response.ReportingAndResponderTeams, error) {
|
|
if len(teamsOfUser) == 0 {
|
|
defaultTeam, err := i.getDefaultTeam()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error in fetching default team")
|
|
}
|
|
teamsOfUser = append(teamsOfUser, *defaultTeam)
|
|
}
|
|
teamsOfProducts, err := i.getTeamsOfProducts(productIDs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var defaultSelectedReportingTeam *team.TeamDTO = nil
|
|
sort.Sort(sortListOfTeamDTOByName(teamsOfUser))
|
|
sort.Sort(sortListOfTeamDTOByName(teamsOfProducts.Teams))
|
|
defaultSelectedReportingTeam = &teamsOfUser[0]
|
|
var reportingTeams []team.TeamDTO
|
|
reportingTeams = append(reportingTeams, teamsOfUser...)
|
|
reportingTeams = append(reportingTeams, teamsOfProducts.Teams...)
|
|
reportingTeams = util.RemoveDuplicates(reportingTeams, "ID").([]team.TeamDTO)
|
|
return &response.ReportingAndResponderTeams{
|
|
ReportingTeam: &response.ProductAndUserTeams{DefaultTeam: defaultSelectedReportingTeam, Teams: reportingTeams},
|
|
ResponderTeam: teamsOfProducts,
|
|
}, nil
|
|
}
|
|
|
|
func (i *incidentOrchestratorImpl) CreateIncident(
|
|
request *incidentRequest.CreateIncidentRequestV3, blazeGroupChannelID string,
|
|
) (*response.IncidentResponse, error) {
|
|
tx := appcontext.GetDB().Begin()
|
|
defer util.RollbackTransaction(tx)
|
|
|
|
incidentEntity, channelTopic, err := i.createIncidentEntityTransactionally(request, tx)
|
|
if err != nil {
|
|
tx.Rollback()
|
|
return nil, err
|
|
}
|
|
slackChannel, err := i.createSlackChannelTransactionally(
|
|
incidentEntity.ID,
|
|
incidentEntity.IncidentName,
|
|
utils.ConstructIncidentChannelName(
|
|
incidentEntity.ID, request.SeverityID, string(incident.Investigating), incidentEntity.Title,
|
|
),
|
|
request.IsPrivate,
|
|
)
|
|
if err != nil {
|
|
tx.Rollback()
|
|
return nil, err
|
|
}
|
|
tx.Commit()
|
|
|
|
err = i.updateSlackDetailsIntoIncidentEntity(slackChannel, incidentEntity)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = i.setChannelTopic(slackChannel, channelTopic)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
go func() {
|
|
i.incidentService.PostIncidentCreationWorkflow(
|
|
slackChannel,
|
|
incidentEntity.ID,
|
|
blazeGroupChannelID,
|
|
)
|
|
}()
|
|
i.incidentService.EeMonitoringServiceWebhookCall(incidentEntity)
|
|
incidentResponse := response.ConvertToIncidentResponse(*incidentEntity)
|
|
return &incidentResponse, nil
|
|
}
|
|
|
|
func (i *incidentOrchestratorImpl) getTeamsOfUserByEmailID(emailID string) []team.TeamDTO {
|
|
userDTO, err := i.userService.GetHoustonUserByEmailId(emailID)
|
|
if err != nil {
|
|
logger.Error(fmt.Sprintf("%s Error in fetching user by email id", logTag), zap.Error(err))
|
|
}
|
|
return i.getTeamByUserDTO(userDTO)
|
|
}
|
|
|
|
func (i *incidentOrchestratorImpl) getTeamsOfUserBySlackUserID(slackUserId string) []team.TeamDTO {
|
|
userDTO, err := i.userService.GetHoustonUserBySlackUserId(slackUserId)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return i.getTeamByUserDTO(userDTO)
|
|
}
|
|
|
|
func (i *incidentOrchestratorImpl) getTeamByUserDTO(userDTO *user2.UserDTO) []team.TeamDTO {
|
|
if userDTO == nil {
|
|
return nil
|
|
}
|
|
teamUserDTO, err := i.teamUserService.GetTeamsByUserId(userDTO.ID)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
var teams []team.TeamDTO
|
|
if len(teamUserDTO) > 0 {
|
|
for _, t := range teamUserDTO {
|
|
teams = append(teams, t.Team)
|
|
}
|
|
}
|
|
return teams
|
|
}
|
|
|
|
func (i *incidentOrchestratorImpl) getTeamsOfProducts(productIDs []uint) (*response.ProductAndUserTeams, error) {
|
|
productTeamDTO, err := i.productTeamService.GetProductTeamsByProductIDs(productIDs)
|
|
if err != nil {
|
|
logger.Error(fmt.Sprintf("%s Error in fetching product teams", logTag), zap.Error(err))
|
|
}
|
|
if len(productTeamDTO) == 0 {
|
|
return i.defaultProductAndUserTeams(), nil
|
|
}
|
|
|
|
var allTeams []team.TeamDTO
|
|
for _, pt := range productTeamDTO {
|
|
allTeams = append(allTeams, pt.Teams...)
|
|
}
|
|
if len(allTeams) == 0 {
|
|
return i.defaultProductAndUserTeams(), nil
|
|
}
|
|
|
|
allUniqueTeams := util.RemoveDuplicates(allTeams, "ID").([]team.TeamDTO)
|
|
var defaultSelectedTeam *team.TeamDTO = nil
|
|
if len(allUniqueTeams) == 1 {
|
|
defaultSelectedTeam = &allUniqueTeams[0]
|
|
}
|
|
return &response.ProductAndUserTeams{
|
|
DefaultTeam: defaultSelectedTeam,
|
|
Teams: allUniqueTeams,
|
|
}, nil
|
|
}
|
|
|
|
func (i *incidentOrchestratorImpl) defaultProductAndUserTeams() *response.ProductAndUserTeams {
|
|
defaultTeam, _ := i.getDefaultTeam()
|
|
return &response.ProductAndUserTeams{
|
|
DefaultTeam: defaultTeam,
|
|
Teams: []team.TeamDTO{*defaultTeam},
|
|
}
|
|
}
|
|
|
|
func (i *incidentOrchestratorImpl) getDefaultTeam() (*team.TeamDTO, error) {
|
|
tr, err := i.teamService.GetTeamByName(defaultTeam)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error in fetching default team")
|
|
}
|
|
return tr.ToDTO(), nil
|
|
}
|
|
|
|
func (i *incidentOrchestratorImpl) buildCreateIncidentDTO(
|
|
createIncRequest *incidentRequest.CreateIncidentRequestV3,
|
|
) (*incident.CreateIncidentDTO, *incidentServiceImpl.ChannelTopic, error) {
|
|
var (
|
|
createIncidentDTO incident.CreateIncidentDTO
|
|
err error
|
|
wg sync.WaitGroup
|
|
mutex sync.Mutex
|
|
channelTopic = &incidentServiceImpl.ChannelTopic{}
|
|
)
|
|
|
|
wg.Add(5)
|
|
|
|
go func() {
|
|
defer wg.Done()
|
|
reportingTeam, err := i.teamService.GetTeamDetails(createIncRequest.ReportingTeamID)
|
|
if err != nil {
|
|
return
|
|
}
|
|
mutex.Lock()
|
|
createIncidentDTO.ReportingTeamID = &reportingTeam.ID
|
|
channelTopic.ReportingTeamName = reportingTeam.Name
|
|
mutex.Unlock()
|
|
}()
|
|
|
|
go func() {
|
|
defer wg.Done()
|
|
responderTeam, err := i.teamService.GetTeamDetails(createIncRequest.ResponderTeamID)
|
|
if err != nil {
|
|
return
|
|
}
|
|
mutex.Lock()
|
|
createIncidentDTO.TeamId = fmt.Sprintf("%d", responderTeam.ID)
|
|
channelTopic.ResponderTeamName = responderTeam.Name
|
|
mutex.Unlock()
|
|
}()
|
|
|
|
go func() {
|
|
defer wg.Done()
|
|
severityDTO, err := i.severityService.FindSeverityById(createIncRequest.SeverityID)
|
|
if err != nil {
|
|
return
|
|
}
|
|
mutex.Lock()
|
|
createIncidentDTO.Severity = fmt.Sprintf("%d", severityDTO.ID)
|
|
channelTopic.SeverityName = severityDTO.Name
|
|
mutex.Unlock()
|
|
}()
|
|
|
|
go func() {
|
|
defer wg.Done()
|
|
productDTOs, err := i.productsService.GetProductsByIDs(createIncRequest.ProductIds)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if len(productDTOs) != len(createIncRequest.ProductIds) {
|
|
err = fmt.Errorf("error in fetching product details")
|
|
return
|
|
}
|
|
var productNames []string
|
|
for _, product := range productDTOs {
|
|
productNames = append(productNames, product.ProductName)
|
|
}
|
|
mutex.Lock()
|
|
createIncidentDTO.ProductIds = createIncRequest.ProductIds
|
|
channelTopic.ProductNames = productNames
|
|
mutex.Unlock()
|
|
}()
|
|
|
|
go func() {
|
|
defer wg.Done()
|
|
mutex.Lock()
|
|
createIncidentDTO.CreatedBy = i.slackService.GetSlackUserIdOrEmail(createIncRequest.CreatedBy)
|
|
mutex.Unlock()
|
|
}()
|
|
|
|
wg.Wait()
|
|
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
createIncidentDTO.Title = createIncRequest.Title
|
|
channelTopic.Title = createIncRequest.Title
|
|
createIncidentDTO.Description = createIncRequest.Description
|
|
createIncidentDTO.Status = incident.Investigating
|
|
channelTopic.StatusName = string(incident.Investigating)
|
|
createIncidentDTO.UpdatedBy = createIncidentDTO.CreatedBy
|
|
createIncidentDTO.StartTime = time.Now()
|
|
createIncidentDTO.EnableReminder = false
|
|
createIncidentDTO.IsPrivate = createIncRequest.IsPrivate
|
|
|
|
if createIncRequest.Metadata != nil {
|
|
metadata, _ := json.Marshal([]incidentRequest.CreateIncidentMetadata{*createIncRequest.Metadata})
|
|
createIncidentDTO.MetaData = metadata
|
|
}
|
|
|
|
return &createIncidentDTO, channelTopic, nil
|
|
}
|
|
|
|
func (i *incidentOrchestratorImpl) createIncidentEntityTransactionally(
|
|
request *incidentRequest.CreateIncidentRequestV3, tx *gorm.DB,
|
|
) (*incident.IncidentEntity, *incidentServiceImpl.ChannelTopic, error) {
|
|
createIncidentDTO, channelTopic, err := i.buildCreateIncidentDTO(request)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
entity, err := i.incidentService.CreateIncidentEntity(createIncidentDTO, tx)
|
|
if err != nil {
|
|
logger.Error(fmt.Sprintf("%s Error while creating incident", logTag), zap.Error(err))
|
|
return nil, nil, err
|
|
}
|
|
logger.Info(fmt.Sprintf("%s Incident entity created. Incident is: %s", logTag, entity.IncidentName))
|
|
return entity, channelTopic, nil
|
|
}
|
|
|
|
func (i *incidentOrchestratorImpl) createSlackChannelTransactionally(incidentID uint, incidentName string, channelName string, isPrivate bool) (*slack2.Channel, error) {
|
|
channel, err := i.slackService.CreateSlackChannel(incidentID, channelName, isPrivate)
|
|
if err != nil {
|
|
logger.Error(
|
|
fmt.Sprintf("%s [%s] Error while crating slack channel", logTag, incidentName),
|
|
zap.Error(err),
|
|
)
|
|
return nil, err
|
|
}
|
|
logger.Info(fmt.Sprintf(
|
|
"%s [%s] Slack channel created. Channel name is %s", logTag, incidentName, channel.Name),
|
|
)
|
|
return channel, nil
|
|
}
|
|
|
|
func (i *incidentOrchestratorImpl) setChannelTopic(
|
|
channel *slack2.Channel, channelTopic *incidentServiceImpl.ChannelTopic,
|
|
) error {
|
|
err := i.slackService.SetTopicOfConversation(channel, channelTopic.String())
|
|
if err != nil {
|
|
logger.Error(
|
|
fmt.Sprintf("%s [%s] Failed to set channel topic", logTag, channel.Name),
|
|
zap.Error(err),
|
|
)
|
|
return err
|
|
}
|
|
logger.Info(fmt.Sprintf("%s [%s] Channel topic is set", logTag, channel.Name))
|
|
return nil
|
|
}
|
|
|
|
func (i *incidentOrchestratorImpl) updateSlackDetailsIntoIncidentEntity(channel *slack2.Channel, incident *incident.IncidentEntity) error {
|
|
incident.SlackChannel = channel.ID
|
|
incident.IncidentName = channel.Name
|
|
incident.UpdatedBy = incident.CreatedBy
|
|
err := i.incidentService.UpdateIncidentEntity(incident)
|
|
if err != nil {
|
|
logger.Error(fmt.Sprintf("%s [%s] Failed to update the slack channel details in DB", logTag, incident.IncidentName))
|
|
return err
|
|
}
|
|
logger.Info(fmt.Sprintf("%s [%s] Slack channel details updated to incident entity", logTag, incident.IncidentName))
|
|
return nil
|
|
}
|
|
|
|
type sortListOfTeamDTOByName []team.TeamDTO
|
|
|
|
func (t sortListOfTeamDTOByName) Len() int {
|
|
return len(t)
|
|
}
|
|
|
|
func (t sortListOfTeamDTOByName) Less(i, j int) bool {
|
|
return t[i].Name < t[j].Name
|
|
}
|
|
|
|
func (t sortListOfTeamDTOByName) Swap(i, j int) {
|
|
t[i], t[j] = t[j], t[i]
|
|
}
|