Jira link table (#331)

* TP-51013 | incident_jira entity, repo and service

* TP-51013 | get jira status api

* TP-51013 | added db migration file

* TP-51013 | added migration query to migrate existing jira links into new table

* TP-51013 | removing linked_jira_issues column from incident table

* TP-51013 | removing empty jira fields if no response found for a jira key in jira api response

* TP-51013 | handled jira api failure cases, will return empty jira fields

* TP-51013 | removed linked_jira_issues field from incident entity

* TP-51013 | handled jira link addition and removal in slack action

* TP-51013 | resolving PR comments

* TP-51013 | adding jira link max length check
This commit is contained in:
Shashank Shekhar
2023-12-21 16:52:35 +05:30
committed by GitHub
parent f31c75a1fb
commit 5758e603e8
20 changed files with 614 additions and 19 deletions

View File

@@ -60,3 +60,5 @@ generatemocks:
cd $(CURDIR)/service/krakatoa && minimock -i IKrakatoaService -s _mock.go -o $(CURDIR)/mocks cd $(CURDIR)/service/krakatoa && minimock -i IKrakatoaService -s _mock.go -o $(CURDIR)/mocks
cd $(CURDIR)/pkg/socketModeClient && minimock -i ISocketModeClientWrapper -s _mock.go -o $(CURDIR)/mocks cd $(CURDIR)/pkg/socketModeClient && minimock -i ISocketModeClientWrapper -s _mock.go -o $(CURDIR)/mocks
cd $(CURDIR)/pkg/maverick && minimock -i IMaverickClient -s _mock.go -o $(CURDIR)/mocks cd $(CURDIR)/pkg/maverick && minimock -i IMaverickClient -s _mock.go -o $(CURDIR)/mocks
cd $(CURDIR)/service/incident_jira && minimock -i IncidentJiraService -s _mock.go -o $(CURDIR)/mocks
cd $(CURDIR)/model/incident_jira && minimock -i IncidentJiraRepository -s _mock.go -o $(CURDIR)/mocks

View File

@@ -24,7 +24,7 @@ type IncidentHandler struct {
gin *gin.Engine gin *gin.Engine
db *gorm.DB db *gorm.DB
authService *service.AuthService authService *service.AuthService
service *incident.IncidentServiceV2 service *incident.IncidentServiceV2
} }
func NewIncidentHandler(gin *gin.Engine, db *gorm.DB, authService *service.AuthService, incidentService *incident.IncidentServiceV2) *IncidentHandler { func NewIncidentHandler(gin *gin.Engine, db *gorm.DB, authService *service.AuthService, incidentService *incident.IncidentServiceV2) *IncidentHandler {
@@ -32,7 +32,7 @@ func NewIncidentHandler(gin *gin.Engine, db *gorm.DB, authService *service.AuthS
gin: gin, gin: gin,
db: db, db: db,
authService: authService, authService: authService,
service: incidentService, service: incidentService,
} }
} }
@@ -57,10 +57,10 @@ func (handler *IncidentHandler) HandleCreateIncident(c *gin.Context) {
c.JSON(http.StatusOK, common.SuccessResponse(incidentResponse, http.StatusOK)) c.JSON(http.StatusOK, common.SuccessResponse(incidentResponse, http.StatusOK))
} }
func (h *IncidentHandler) HandleUpdateIncident(c *gin.Context) { func (handler *IncidentHandler) HandleUpdateIncident(c *gin.Context) {
userEmail := c.GetHeader(util.UserEmailHeader) userEmail := c.GetHeader(util.UserEmailHeader)
sessionToken := c.GetHeader(util.SessionTokenHeader) sessionToken := c.GetHeader(util.SessionTokenHeader)
isValidUser, err := h.authService.CheckValidUser(sessionToken, userEmail) isValidUser, err := handler.authService.CheckValidUser(sessionToken, userEmail)
if err != nil || !isValidUser { if err != nil || !isValidUser {
c.JSON(http.StatusUnauthorized, common.ErrorResponse(errors.New("Unauthorized user"), http.StatusUnauthorized, nil)) c.JSON(http.StatusUnauthorized, common.ErrorResponse(errors.New("Unauthorized user"), http.StatusUnauthorized, nil))
return return
@@ -77,7 +77,7 @@ func (h *IncidentHandler) HandleUpdateIncident(c *gin.Context) {
return return
} }
incidentServiceV2 := incident.NewIncidentServiceV2(h.db) incidentServiceV2 := incident.NewIncidentServiceV2(handler.db)
result, err := incidentServiceV2.UpdateIncident(updateIncidentRequest, userEmail) result, err := incidentServiceV2.UpdateIncident(updateIncidentRequest, userEmail)
if err != nil { if err != nil {
@@ -136,3 +136,20 @@ func (handler *IncidentHandler) HandleJiraUnLinking(c *gin.Context) {
} }
c.JSON(http.StatusOK, common.SuccessResponse("JIRA link removed successfully", http.StatusOK)) c.JSON(http.StatusOK, common.SuccessResponse("JIRA link removed successfully", http.StatusOK))
} }
func (handler *IncidentHandler) HandleGetJiraStatuses(c *gin.Context) {
IncidentName := c.Query("incident_name")
pageSize, pageNumber, err :=
utils.ValidatePage(c.Query("page_size"), c.Query("page_number"))
if err != nil {
logger.Error("error in query parameters", zap.Int64("page_size", pageSize),
zap.Int64("page_number", pageNumber), zap.Error(err))
c.JSON(http.StatusBadRequest, common.ErrorResponse(err, http.StatusBadRequest, nil))
return
}
statuses, err := handler.service.GetJiraStatuses(IncidentName, pageNumber, pageSize)
if err != nil {
c.JSON(http.StatusInternalServerError, err)
}
c.JSON(http.StatusOK, statuses)
}

View File

@@ -160,6 +160,7 @@ func (s *Server) incidentClientHandlerV2(houstonGroup *gin.RouterGroup) {
} }
houstonGroup.POST("/link-jira-to-incident", incidentHandler.HandleJiraLinking) houstonGroup.POST("/link-jira-to-incident", incidentHandler.HandleJiraLinking)
houstonGroup.POST("/unlink-jira-from-incident", incidentHandler.HandleJiraUnLinking) houstonGroup.POST("/unlink-jira-from-incident", incidentHandler.HandleJiraUnLinking)
houstonGroup.GET("/get-jira-statuses", incidentHandler.HandleGetJiraStatuses)
} }
func (s *Server) incidentHandler(houstonGroup *gin.RouterGroup) { func (s *Server) incidentHandler(houstonGroup *gin.RouterGroup) {

View File

@@ -88,6 +88,28 @@ func Difference(s1, s2 []string) []string {
return result return result
} }
func GetDifference[T comparable](s1, s2 []T) []T {
combinedSlice := append(s1, s2...)
m := make(map[T]int)
for _, v := range combinedSlice {
if _, ok := m[v]; ok {
// remove element later as it exists in both slices.
m[v] += 1
continue
}
// new entry, add to the map!
m[v] = 1
}
var result []T
for k, v := range m {
if v == 1 {
result = append(result, k)
}
}
return result
}
func Intersection(s1, s2 []string) (inter []string) { func Intersection(s1, s2 []string) (inter []string) {
hash := make(map[string]bool) hash := make(map[string]bool)
for _, e := range s1 { for _, e := range s1 {

View File

@@ -86,7 +86,7 @@ create-incident-v2-enabled=CREATE_INCIDENT_V2_ENABLED
get-teams.v2.enabled=GET_TEAMS_V2_ENABLED get-teams.v2.enabled=GET_TEAMS_V2_ENABLED
#slack details #slack details
slack.workspace.id=SLACK_WORKSPACE_ID slack.workspace.id=SLACK_WORKSPACE_ID
navi.jira.base.url=https://navihq.atlassian.net/ navi.jira.base.url=https://navihq.atlassian.net/browse/
houston.channel.help.message=/houston: General command to open the other options| /houston severity: Opens the view to update severity of the incident| /houston set severity to <severities_label>: Sets the incident severity| /houston team: Opens the view to update team| /houston set team to <incident team>: Sets the incident team| /houston status: Opens the view to set status| /houston set status to <Investigating/Identified/Monitoring/Resolved>: Sets the incident status| /houston description: Opens the view to set incident description| /houston set description to <incident description>: Sets the incident description| /houston resolve: Opens the view to fill RCA and resolve| /houston rca: Opens the view to fill RCA houston.channel.help.message=/houston: General command to open the other options| /houston severity: Opens the view to update severity of the incident| /houston set severity to <severities_label>: Sets the incident severity| /houston team: Opens the view to update team| /houston set team to <incident team>: Sets the incident team| /houston status: Opens the view to set status| /houston set status to <Investigating/Identified/Monitoring/Resolved>: Sets the incident status| /houston description: Opens the view to set incident description| /houston set description to <incident description>: Sets the incident description| /houston resolve: Opens the view to fill RCA and resolve| /houston rca: Opens the view to fill RCA
non.houston.channel.help.message=/houston: General command to open the other options| /houston start: Opens the view to start a new incident| /houston start <severity> <team> title <incident title> description <incident description>: Starts an incident of a specific severity and team non.houston.channel.help.message=/houston: General command to open the other options| /houston start: Opens the view to start a new incident| /houston start <severity> <team> title <incident title> description <incident description>: Starts an incident of a specific severity and team
@@ -94,3 +94,4 @@ non.houston.channel.help.message=/houston: General command to open the other opt
jira.base.url=JIRA_BASE_URL jira.base.url=JIRA_BASE_URL
jira.username=JIRA_USERNAME jira.username=JIRA_USERNAME
jira.api.token=JIRA_API_TOKEN jira.api.token=JIRA_API_TOKEN
jira.link.max.length=JIRA_LINK_MAX_LENGTH

View File

@@ -0,0 +1,18 @@
CREATE TABLE if not exists incident_jira (
id BIGSERIAL PRIMARY KEY,
created_at timestamp with time zone,
incident_id bigint REFERENCES incident(id),
jira_link TEXT
);
-- Migrate existing jira links -------------------------------------------------------
INSERT INTO incident_jira (incident_id, jira_link, created_at)
SELECT incident_id, jira_link, now()
FROM (SELECT id AS incident_id, unnest(jira_links) AS jira_link
FROM incident
WHERE jira_links IS NOT NULL
AND array_length(jira_links, 1) > 0) AS subquery
WHERE jira_link <> '';

4
go.mod
View File

@@ -15,7 +15,7 @@ require (
github.com/google/uuid v1.4.0 github.com/google/uuid v1.4.0
github.com/jackc/pgx/v5 v5.3.1 github.com/jackc/pgx/v5 v5.3.1
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/lib/pq v1.10.7 github.com/lib/pq v1.10.9
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/slack-go/slack v0.12.1 github.com/slack-go/slack v0.12.1
github.com/spf13/cobra v1.6.1 github.com/spf13/cobra v1.6.1
@@ -54,7 +54,7 @@ require (
github.com/chromedp/sysutil v1.0.0 // indirect github.com/chromedp/sysutil v1.0.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect github.com/go-sql-driver/mysql v1.7.1 // indirect
github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.1.0 // indirect github.com/gobwas/ws v1.1.0 // indirect

View File

@@ -5,18 +5,22 @@ import (
"fmt" "fmt"
"github.com/slack-go/slack" "github.com/slack-go/slack"
"github.com/spf13/viper" "github.com/spf13/viper"
"houston/appcontext"
"houston/common/util" "houston/common/util"
"houston/internal/processor/action/view" "houston/internal/processor/action/view"
"houston/logger" "houston/logger"
"houston/model/incident" "houston/model/incident"
incidentJiraModel "houston/model/incident_jira"
incidentService "houston/service/incident" incidentService "houston/service/incident"
"houston/service/incident_jira"
slack2 "houston/service/slack" slack2 "houston/service/slack"
"strings" "strings"
) )
type IncidentJiraLinksAction struct { type IncidentJiraLinksAction struct {
incidentService incidentService.IIncidentService incidentService incidentService.IIncidentService
slackService slack2.ISlackService incidentJiraService incident_jira.IncidentJiraService
slackService slack2.ISlackService
} }
const ( const (
@@ -28,8 +32,9 @@ func NewIncidentJiraLinksAction(
slackService slack2.ISlackService, slackService slack2.ISlackService,
) *IncidentJiraLinksAction { ) *IncidentJiraLinksAction {
return &IncidentJiraLinksAction{ return &IncidentJiraLinksAction{
incidentService: incidentService, incidentService: incidentService,
slackService: slackService, incidentJiraService: incident_jira.NewIncidentJiraService(incidentJiraModel.NewIncidentJiraRepo(appcontext.GetDB())),
slackService: slackService,
} }
} }
@@ -39,6 +44,7 @@ func (action *IncidentJiraLinksAction) getJiraLinksBlock(initialValue string) *s
return view.CreatePlainTextInputBlock(jiraLinksBlockData) return view.CreatePlainTextInputBlock(jiraLinksBlockData)
} }
// Deprecated: updateJiraLinks is deprecated. Use updateIncidentJiraLinks instead
func (action *IncidentJiraLinksAction) updateJiraLinks(jiraLinks string, callback slack.InteractionCallback, incidentEntity *incident.IncidentEntity) error { func (action *IncidentJiraLinksAction) updateJiraLinks(jiraLinks string, callback slack.InteractionCallback, incidentEntity *incident.IncidentEntity) error {
channelID := callback.View.PrivateMetadata channelID := callback.View.PrivateMetadata
formattedJiraLinks := strings.Split( formattedJiraLinks := strings.Split(
@@ -72,3 +78,46 @@ func (action *IncidentJiraLinksAction) updateJiraLinks(jiraLinks string, callbac
} }
return nil return nil
} }
func (action *IncidentJiraLinksAction) updateIncidentJiraLinks(jiraLinks string, incidentEntity *incident.IncidentEntity) error {
err := action.incidentJiraService.RemoveAllJiraLinksByIncidentID(incidentEntity.ID)
if err != nil {
logger.Error(fmt.Sprintf("%s unable to remove jira links for incident %s", logTag, incidentEntity.IncidentName))
return err
}
validatedJiraLinksPtr, err := action.getValidatedJiraLinks(jiraLinks)
if err != nil {
return err
}
if validatedJiraLinksPtr != nil && len(*validatedJiraLinksPtr) > 0 {
_, err = action.incidentJiraService.AddJiraLinksByIncidentID(incidentEntity.ID, *validatedJiraLinksPtr)
if err != nil {
logger.Error(fmt.Sprintf("%s unable to add jira link(s) for incident %s", logTag, incidentEntity.IncidentName))
return err
}
}
return nil
}
func (action *IncidentJiraLinksAction) getValidatedJiraLinks(jiraLinks string) (*[]string, error) {
var formattedJiraLinks []string
if jiraLinks != "" {
formattedJiraLinks = strings.Split(
strings.ReplaceAll(strings.ReplaceAll(jiraLinks, "\n", ""), " ", ""),
",",
)
}
if len(formattedJiraLinks) > 0 && formattedJiraLinks != nil {
for _, link := range formattedJiraLinks {
//Validate jira link
if !strings.HasPrefix(link, viper.GetString("navi.jira.base.url")) {
return nil, errors.New(fmt.Sprintf("%s is not a valid Jira link", link))
}
}
}
return &formattedJiraLinks, nil
}

View File

@@ -75,7 +75,11 @@ func (action *IncidentRCASectionAction) ProcessIncidentRCAActionRequest(callback
} }
} }
func (action *IncidentRCASectionAction) PerformSetIncidentRCADetailsAction(callback slack.InteractionCallback, request *socketmode.Request, requesterType util.ViewSubmissionType) { func (action *IncidentRCASectionAction) PerformSetIncidentRCADetailsAction(
callback slack.InteractionCallback,
request *socketmode.Request,
requesterType util.ViewSubmissionType,
) {
incidentEntity, err := action.incidentRepository.FindIncidentByChannelId(callback.View.PrivateMetadata) incidentEntity, err := action.incidentRepository.FindIncidentByChannelId(callback.View.PrivateMetadata)
if err != nil || incidentEntity == nil { if err != nil || incidentEntity == nil {
logger.Error(fmt.Sprintf("failed to get the incicent for channel id: %v", callback.View.PrivateMetadata)) logger.Error(fmt.Sprintf("failed to get the incicent for channel id: %v", callback.View.PrivateMetadata))
@@ -101,9 +105,20 @@ func (action *IncidentRCASectionAction) PerformSetIncidentRCADetailsAction(callb
logger.Error(fmt.Sprintf("failed to update rca summary for incident id: %v", incidentEntity.ID)) logger.Error(fmt.Sprintf("failed to update rca summary for incident id: %v", incidentEntity.ID))
} }
jiraLinksValue := actions[util.SetJiraLinks].Value jiraLinksValue := actions[util.SetJiraLinks].Value
err = action.jiraAction.updateJiraLinks(jiraLinksValue, callback, incidentEntity) err = action.jiraAction.updateIncidentJiraLinks(jiraLinksValue, incidentEntity)
if err != nil { if err != nil {
logger.Error(fmt.Sprintf("failed to update jira link(s) for incident id: %v", incidentEntity.ID)) _, _ = action.client.PostEphemeral(
callback.View.PrivateMetadata,
callback.User.ID,
slack.MsgOptionText(fmt.Sprintf("Failed to update Jira link(s). %s", err.Error()), false),
)
logger.Error(fmt.Sprintf("failed to update Jira link(s) for incident id: %v. %+v", incidentEntity.ID, err))
} else {
//todo: this is to be removed after jira_links column is removed from incident table
err = action.jiraAction.updateJiraLinks(jiraLinksValue, callback, incidentEntity)
if err != nil {
logger.Error(fmt.Sprintf("failed to update Jira link(s) for incident id: %v", incidentEntity.ID))
}
} }
updatedIncidentEntity, _ := action.incidentRepository.FindIncidentById(incidentEntity.ID) updatedIncidentEntity, _ := action.incidentRepository.FindIncidentById(incidentEntity.ID)
tagValuesMap, _ := action.tagsAction.getIncidentTagValuesAsMap(incidentEntity.ID) tagValuesMap, _ := action.tagsAction.getIncidentTagValuesAsMap(incidentEntity.ID)

View File

@@ -0,0 +1,16 @@
package incident_jira
import (
"time"
)
type IncidentJiraEntity struct {
ID uint `gorm:"primarykey;column:id"`
CreatedAt time.Time `gorm:"column:created_at"`
IncidentEntityID uint `gorm:"column:incident_id"`
JiraLink string `gorm:"column:jira_link"`
}
func (IncidentJiraEntity) TableName() string {
return "incident_jira"
}

View File

@@ -0,0 +1,18 @@
package incident_jira
import (
"gorm.io/gorm"
)
type IncidentJiraRepository interface {
InsertJiraLinks(incidentID uint, jiraLinks []string) ([]uint, error)
GetJiraLinksByIncidentID(incidentID uint) (*[]IncidentJiraEntity, error)
GetIncidentJiraEntity(incidentID uint, jiraLink string) (*IncidentJiraEntity, error)
DeleteJiraLink(incidentID uint, jiraLink string) error
DeleteAllJiraLinksForIncident(incidentID uint) error
GetAllJiraIdsByPage(incidentName string, pageNumber, pageSize int64) (*[]IncidentJiraLinksDTO, int64, error)
}
func NewIncidentJiraRepo(db *gorm.DB) *IncidentJiraRepositoryImpl {
return &IncidentJiraRepositoryImpl{db: db}
}

View File

@@ -0,0 +1,108 @@
package incident_jira
import (
"gorm.io/gorm"
)
type IncidentJiraRepositoryImpl struct {
db *gorm.DB
}
func (repo *IncidentJiraRepositoryImpl) InsertJiraLinks(incidentID uint, jiraLinks []string) ([]uint, error) {
var records []IncidentJiraEntity
for _, jiraLink := range jiraLinks {
records = append(records, IncidentJiraEntity{
IncidentEntityID: incidentID,
JiraLink: jiraLink,
})
}
result := repo.db.Create(&records)
if result.Error != nil {
return nil, result.Error
}
var insertedRows []uint
for _, record := range records {
insertedRows = append(insertedRows, record.ID)
}
return insertedRows, nil
}
func (repo *IncidentJiraRepositoryImpl) GetJiraLinksByIncidentID(incidentID uint) (*[]IncidentJiraEntity, error) {
var entities []IncidentJiraEntity
result := repo.db.Find(&entities, "incident_id = ?", incidentID)
if result.Error != nil {
return nil, result.Error
}
if result.RowsAffected == 0 {
return nil, nil
}
return &entities, nil
}
func (repo *IncidentJiraRepositoryImpl) GetIncidentJiraEntity(incidentID uint, jiraLink string) (*IncidentJiraEntity, error) {
var entity IncidentJiraEntity
result := repo.db.First(&entity, "incident_id = ? AND jira_link = ?", incidentID, jiraLink)
if result.Error != nil {
return nil, result.Error
}
if result.RowsAffected == 0 {
return nil, nil
}
return &entity, 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 {
return result.Error
}
return nil
}
func (repo *IncidentJiraRepositoryImpl) DeleteAllJiraLinksForIncident(incidentID uint) error {
result := repo.db.Delete(&IncidentJiraEntity{}, "incident_id = ?", incidentID)
if result.Error != nil {
return result.Error
}
return nil
}
func (repo *IncidentJiraRepositoryImpl) GetAllJiraIdsByPage(incidentName string, pageNumber, pageSize int64) (*[]IncidentJiraLinksDTO, int64, error) {
var allJiraLinksFromDB []IncidentJiraLinksDTO
query := repo.db.Table("incident_jira").
Joins("JOIN incident ON incident_jira.incident_id = incident.id").
Select("incident.id as incident_id, incident.incident_name as incident_name, incident_jira.jira_link as jira_link").
Group("incident_jira.jira_link, incident.id")
if incidentName != "" {
query = query.Where("incident.incident_name LIKE ?", "%"+incidentName+"%")
}
var totalElements int64
result := query.Count(&totalElements)
if result.Error != nil {
return nil, 0, result.Error
}
result = query.Order("incident.id desc").
Offset(int(pageNumber * pageSize)).
Limit(int(pageSize)).
Find(&allJiraLinksFromDB)
if result.Error != nil {
return nil, 0, result.Error
}
if result.RowsAffected == 0 {
return nil, totalElements, nil
}
return &allJiraLinksFromDB, totalElements, result.Error
}

View File

@@ -0,0 +1,7 @@
package incident_jira
type IncidentJiraLinksDTO struct {
IncidentID uint `json:"incident_id"`
IncidentName string `json:"incident_name"`
JiraLink string `json:"jira_link"`
}

View File

@@ -61,8 +61,6 @@ func (client *JiraClientImpl) SearchByJQL(jql string) (*response.JiraSearchJQLRe
return nil, errors.NewApiError(fmt.Sprintf("Jira api returned %d", apiResponse.StatusCode), apiResponse.Status, http.StatusInternalServerError) return nil, errors.NewApiError(fmt.Sprintf("Jira api returned %d", apiResponse.StatusCode), apiResponse.Status, http.StatusInternalServerError)
} }
logger.Info(fmt.Sprintf("response body: %s", responseBody))
searchJQLResponse := response.JiraSearchJQLResponse{} searchJQLResponse := response.JiraSearchJQLResponse{}
// Parse api response body into JiraSearchJQLResponse // Parse api response body into JiraSearchJQLResponse

View File

@@ -14,17 +14,25 @@ import (
"houston/logger" "houston/logger"
"houston/model/incident" "houston/model/incident"
"houston/model/incident_channel" "houston/model/incident_channel"
incident_jira2 "houston/model/incident_jira"
"houston/model/log" "houston/model/log"
"houston/model/severity" "houston/model/severity"
"houston/model/team" "houston/model/team"
"houston/model/user" "houston/model/user"
"houston/pkg/atlassian"
"houston/pkg/atlassian/dto/response"
conference2 "houston/pkg/conference" conference2 "houston/pkg/conference"
"houston/pkg/rest"
service2 "houston/service/conference" service2 "houston/service/conference"
incidentChannel "houston/service/incident_channel" incidentChannel "houston/service/incident_channel"
"houston/service/incident_jira"
"houston/service/krakatoa" "houston/service/krakatoa"
request "houston/service/request" request "houston/service/request"
service "houston/service/response" service "houston/service/response"
common "houston/service/response/common"
"houston/service/slack" "houston/service/slack"
"math"
"net/http"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
@@ -41,6 +49,7 @@ type IncidentServiceV2 struct {
incidentRepository incident.IIncidentRepository incidentRepository incident.IIncidentRepository
userRepository user.IUserRepository userRepository user.IUserRepository
krakatoaService krakatoa.IKrakatoaService krakatoaService krakatoa.IKrakatoaService
incidentJiraService incident_jira.IncidentJiraService
} }
/* /*
@@ -66,6 +75,7 @@ func NewIncidentServiceV2(db *gorm.DB) *IncidentServiceV2 {
userRepository: userRepository, userRepository: userRepository,
incidentChannelService: incidentChannelService, incidentChannelService: incidentChannelService,
krakatoaService: krakatoaService, krakatoaService: krakatoaService,
incidentJiraService: incident_jira.NewIncidentJiraService(incident_jira2.NewIncidentJiraRepo(db)),
} }
} }
@@ -191,6 +201,11 @@ func (i *IncidentServiceV2) LinkJiraToIncident(incidentId uint, linkedBy string,
logger.Info(errorMessage) logger.Info(errorMessage)
return fmt.Errorf("failed to fetch incident by id %d", incidentId) return fmt.Errorf("failed to fetch incident by id %d", incidentId)
} }
err = i.AddJiraLinks(entity, jiraLinks...)
if err != nil {
logger.Error(fmt.Sprintf("%s failed to link JIRA to the incident: %s. %+v", logTag, entity.IncidentName, err))
return err
}
if len(jiraLinks) == 1 { if len(jiraLinks) == 1 {
if util.Contains(entity.JiraLinks, jiraLinks[0]) { if util.Contains(entity.JiraLinks, jiraLinks[0]) {
return errors.New("This JIRA link already exists") return errors.New("This JIRA link already exists")
@@ -220,9 +235,153 @@ func (i *IncidentServiceV2) UnLinkJiraFromIncident(incidentId uint, unLinkedBy,
logger.Error(fmt.Sprintf("%s failed to unlink JIRA ID(s): %s from incident %s", logTag, jiraLink, entity.IncidentName)) logger.Error(fmt.Sprintf("%s failed to unlink JIRA ID(s): %s from incident %s", logTag, jiraLink, entity.IncidentName))
return err return err
} }
err = i.RemoveJiraLink(entity, jiraLink)
if err != nil {
logger.Error(fmt.Sprintf("%s failed to link JIRA to the incident: %s", logTag, entity.IncidentName))
_, err := i.slackService.PostMessageByChannelID("failed to link JIRA", false, entity.SlackChannel)
if err != nil {
logger.Info(fmt.Sprintf("%s failed to post jira linking failure message to slack channel: %s", logTag, entity.IncidentName))
}
}
return nil return nil
} }
func (i *IncidentServiceV2) GetJiraStatuses(incidentName string, pageNumber, pageSize int64) (service.GetAllJiraStatusPaginatedResponse, error) {
emptyResponse := service.GetAllJiraStatusPaginatedResponse{}
incidentJiraLinksDTOs, totalJiraLinks, err := i.incidentJiraService.GetJiraLinks(incidentName, pageNumber, pageSize)
if err != nil {
return service.GetAllJiraStatusPaginatedResponse{}, err
}
if incidentJiraLinksDTOs == nil || len(*incidentJiraLinksDTOs) == 0 {
return service.GetAllJiraStatusPaginatedResponse{}, nil
}
var allJiraLinks []string
for _, dto := range *incidentJiraLinksDTOs {
allJiraLinks = append(allJiraLinks, dto.JiraLink)
}
if len(allJiraLinks) == 0 {
return service.GetAllJiraStatusPaginatedResponse{}, nil
}
jql, err := getJQLFromJiraLinks(allJiraLinks...)
if err != nil {
return emptyResponse, err
}
var jiraKeyToJiraFieldsMap = make(map[string]response.Fields)
jiraClient := atlassian.NewJiraClient(rest.NewHttpRestClient())
jiraResponse, apiErr := jiraClient.SearchByJQL(jql)
if apiErr != nil {
logger.Error(fmt.Sprintf("%s failed to search by jql: %s. %+v", logTag, jql, err))
} else {
for _, jiraIssue := range jiraResponse.Issues {
jiraKeyToJiraFieldsMap[jiraIssue.Key] = jiraIssue.Fields
}
}
data, err := getHoustonJiraStatuses(incidentJiraLinksDTOs, &jiraKeyToJiraFieldsMap, incidentName)
if err != nil {
return service.GetAllJiraStatusPaginatedResponse{}, err
}
page := common.Page{
PageSize: pageSize,
TotalPages: int64(math.Ceil(float64(totalJiraLinks) / float64(pageSize))),
PageNumber: pageNumber,
TotalElements: int(totalJiraLinks),
}
r := service.GetAllJiraStatusPaginatedResponse{
Data: *data,
Page: page,
Status: http.StatusOK,
}
return r, nil
}
func getHoustonJiraStatuses(
incidentJiraLinksDTOs *[]incident_jira2.IncidentJiraLinksDTO,
jiraKeyToJiraFieldsMap *map[string]response.Fields,
incidentName string,
) (*[]service.HoustonJiraStatus, error) {
var data []service.HoustonJiraStatus
for _, dto := range *incidentJiraLinksDTOs {
jiraKeyFromJiraLink, err := extractJiraKeyFromURL(dto.JiraLink)
if err != nil {
logger.Error(
fmt.Sprintf("%s failed to extract jira key from jira link: %s for incident: %s. %+v",
logTag, dto.JiraLink, incidentName, err,
),
)
return nil, err
}
jiraFields, ok := (*jiraKeyToJiraFieldsMap)[jiraKeyFromJiraLink]
if !ok {
logger.Error(fmt.Sprintf("%s details for jiraKey: %s could not be found in the jira api response", logTag, jiraKeyFromJiraLink))
jiraFields = response.Fields{}
}
var teamsInvolved []string
for _, teamInvolved := range jiraFields.TeamsInvolved {
teamsInvolved = append(teamsInvolved, teamInvolved.Value)
}
data = append(data, service.HoustonJiraStatus{
IncidentID: dto.IncidentID,
IncidentName: dto.IncidentName,
JiraKey: jiraKeyFromJiraLink,
JiraLink: dto.JiraLink,
JiraType: jiraFields.IssueType.Name,
JiraSummary: jiraFields.Summary,
JiraStatus: jiraFields.Status.Name,
TeamsInvolved: teamsInvolved,
JiraCreatedAt: jiraFields.CreatedAt,
})
}
return &data, nil
}
func extractJiraKeyFromURL(url string) (string, error) {
// Define a regular expression to match Jira issue URLs
naviJiraBaseUrl := viper.GetString("navi.jira.base.url")
regex := regexp.MustCompile(naviJiraBaseUrl + `([A-Z][A-Z0-9]+-\d+)`)
// Find the matches
matches := regex.FindStringSubmatch(url)
// Check if a match is found
if len(matches) < 2 {
return "", fmt.Errorf("no Jira issue key found in the URL")
}
// Extract and return the Jira issue key
return matches[1], nil
}
func getJQLFromJiraLinks(jiraLinks ...string) (string, error) {
var jiraKeys []string
for _, jiraLink := range jiraLinks {
jiraKey, err := extractJiraKeyFromURL(jiraLink)
if err == nil {
jiraKeys = append(jiraKeys, jiraKey)
}
}
if len(jiraKeys) == 0 {
return "", fmt.Errorf("no jira key could be extracted from the given links")
}
commaSeparatedJiraKeys := strings.Join(jiraKeys, ", ")
jql := fmt.Sprintf("issueKey in (%s)", commaSeparatedJiraKeys)
return jql, nil
}
// GetAllOpenIncidents - returns list of all the open incidents and length of the result when success otherwise error // GetAllOpenIncidents - returns list of all the open incidents and length of the result when success otherwise error
func (i *IncidentServiceV2) GetAllOpenIncidents() ([]incident.IncidentEntity, int, error) { func (i *IncidentServiceV2) GetAllOpenIncidents() ([]incident.IncidentEntity, int, error) {
incidents, resultLength, err := i.incidentRepository.GetAllOpenIncidents() incidents, resultLength, err := i.incidentRepository.GetAllOpenIncidents()
@@ -340,6 +499,22 @@ func (i *IncidentServiceV2) updateJiraIDs(entity *incident.IncidentEntity, user,
return nil return nil
} }
func (i *IncidentServiceV2) AddJiraLinks(entity *incident.IncidentEntity, jiraLinks ...string) error {
_, err := i.incidentJiraService.AddJiraLinksByIncidentID(entity.ID, jiraLinks)
if err != nil {
return err
}
return nil
}
func (i *IncidentServiceV2) RemoveJiraLink(entity *incident.IncidentEntity, jiraLink string) error {
_, err := i.incidentJiraService.RemoveJiraLinkByIncidentID(entity.ID, jiraLink)
if err != nil {
return err
}
return nil
}
/* /*
performs essential slack operations post incident creation including invitation of team member and incident creator to performs essential slack operations post incident creation including invitation of team member and incident creator to
incident slack channel, tags pse/dev oncall, assigns responder, posts SLA message, creates gmeet and posts the link incident slack channel, tags pse/dev oncall, assigns responder, posts SLA message, creates gmeet and posts the link

View File

@@ -9,10 +9,13 @@ import (
type IIncidentService interface { type IIncidentService interface {
CreateIncident(request request.CreateIncidentRequestV2, source string, blazeGroupChannelID string) (service.IncidentResponse, error) CreateIncident(request request.CreateIncidentRequestV2, source string, blazeGroupChannelID string) (service.IncidentResponse, error)
GetIncidentById(incidentId uint) (*incident.IncidentEntity, error) GetIncidentById(incidentId uint) (*incident.IncidentEntity, error)
GetIncidentByChannelID(channelID string) (*incident.IncidentEntity, error)
LinkJiraToIncident(incidentId uint, linkedBy string, jiraLinks ...string) error LinkJiraToIncident(incidentId uint, linkedBy string, jiraLinks ...string) error
UnLinkJiraFromIncident(incidentId uint, unLinkedBy, jiraLink string) error UnLinkJiraFromIncident(incidentId uint, unLinkedBy, jiraLink string) error
GetAllOpenIncidents() ([]incident.IncidentEntity, int, error) GetAllOpenIncidents() ([]incident.IncidentEntity, int, error)
GetIncidentRoleByIncidentIdAndRole(incidentId uint, role string) (*incident.IncidentRoleEntity, error) GetIncidentRoleByIncidentIdAndRole(incidentId uint, role string) (*incident.IncidentRoleEntity, error)
UpdateIncidentJiraLinksEntity(incidentEntity *incident.IncidentEntity, updatedBy string, jiraLinks []string) error UpdateIncidentJiraLinksEntity(incidentEntity *incident.IncidentEntity, updatedBy string, jiraLinks []string) error
AddJiraLinks(entity *incident.IncidentEntity, jiraLinks ...string) error
RemoveJiraLink(entity *incident.IncidentEntity, jiraLink string) error
IsHoustonChannel(channelID string) (bool, error) IsHoustonChannel(channelID string) (bool, error)
} }

View File

@@ -0,0 +1,15 @@
package incident_jira
import "houston/model/incident_jira"
type IncidentJiraService interface {
AddJiraLinksByIncidentID(incidentID uint, jiraLinks []string) ([]uint, error)
GetJiraLinks(incidentName string, pageNumber, pageSize int64) (*[]incident_jira.IncidentJiraLinksDTO, int64, error)
GetJiraLinksByIncidentID(incidentID uint) (*[]incident_jira.IncidentJiraEntity, error)
RemoveJiraLinkByIncidentID(incidentID uint, jiraLink string) (uint, error)
RemoveAllJiraLinksByIncidentID(incidentID uint) error
}
func NewIncidentJiraService(repo incident_jira.IncidentJiraRepository) IncidentJiraService {
return &IncidentJiraServiceImpl{repo}
}

View File

@@ -0,0 +1,41 @@
package incident_jira
import (
"fmt"
"github.com/spf13/viper"
"houston/model/incident_jira"
)
type IncidentJiraServiceImpl struct {
repo incident_jira.IncidentJiraRepository
}
func (service *IncidentJiraServiceImpl) AddJiraLinksByIncidentID(incidentID uint, jiraLinks []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)
}
func (service *IncidentJiraServiceImpl) GetJiraLinks(incidentName string, pageNumber, pageSize int64) (*[]incident_jira.IncidentJiraLinksDTO, int64, error) {
return service.repo.GetAllJiraIdsByPage(incidentName, pageNumber, pageSize)
}
func (service *IncidentJiraServiceImpl) GetJiraLinksByIncidentID(incidentID uint) (*[]incident_jira.IncidentJiraEntity, error) {
return service.repo.GetJiraLinksByIncidentID(incidentID)
}
func (service *IncidentJiraServiceImpl) RemoveJiraLinkByIncidentID(incidentID uint, jiraLink string) (uint, error) {
incidentJiraEntity, err := service.repo.GetIncidentJiraEntity(incidentID, jiraLink)
if err != nil {
return 0, err
}
return incidentJiraEntity.ID, service.repo.DeleteJiraLink(incidentID, jiraLink)
}
func (service *IncidentJiraServiceImpl) RemoveAllJiraLinksByIncidentID(incidentID uint) error {
return service.repo.DeleteAllJiraLinksForIncident(incidentID)
}

View File

@@ -0,0 +1,68 @@
package incident_jira
import (
"github.com/gojuno/minimock/v3"
"github.com/spf13/viper"
"github.com/stretchr/testify/suite"
"houston/logger"
"houston/mocks"
"houston/model/incident_jira"
"testing"
"time"
)
type IncidentJiraServiceSuite struct {
suite.Suite
controller *minimock.Controller
repoMock *mocks.IncidentJiraRepositoryMock
service IncidentJiraService
}
func (suite *IncidentJiraServiceSuite) SetupSuite() {
logger.InitLogger()
viper.Set("jira.link.max.length", 50)
suite.controller = minimock.NewController(suite.T())
suite.T().Cleanup(suite.controller.Finish)
suite.repoMock = mocks.NewIncidentJiraRepositoryMock(suite.controller)
suite.service = NewIncidentJiraService(suite.repoMock)
}
func TestJiraClient(t *testing.T) {
suite.Run(t, new(IncidentJiraServiceSuite))
}
func (suite *IncidentJiraServiceSuite) TestIncidentJiraServiceImpl_AddJiraLinksByIncidentID() {
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"})
if err != nil {
suite.Fail("Add Jira Link Failed", err)
}
}
func (suite *IncidentJiraServiceSuite) TestIncidentJiraServiceImpl_GetJiraLinksPaginated() {
suite.repoMock.GetAllJiraIdsByPageMock.When("", 1, 10).Then(&[]incident_jira.IncidentJiraLinksDTO{}, int64(0), nil)
_, _, err := suite.service.GetJiraLinks("", 1, 10)
if err != nil {
suite.Fail("Add Jira Link Failed", err)
}
}
func (suite *IncidentJiraServiceSuite) TestIncidentJiraServiceImpl_RemoveJiraLinkByIncidentID() {
e := incident_jira.IncidentJiraEntity{
ID: uint(1),
CreatedAt: time.Now(),
IncidentEntityID: 1,
JiraLink: "https://navihq.atlassian.net/browse/TP-48564",
}
suite.repoMock.GetIncidentJiraEntityMock.When(1, "https://navihq.atlassian.net/browse/TP-48564").Then(&e, nil)
suite.repoMock.DeleteJiraLinkMock.When(1, "https://navihq.atlassian.net/browse/TP-48564").Then(nil)
_, err := suite.service.RemoveJiraLinkByIncidentID(1, "https://navihq.atlassian.net/browse/TP-48564")
if err != nil {
suite.Fail("Remove Jira Link By incident ID Failed", err)
}
}

View File

@@ -0,0 +1,21 @@
package service
import service "houston/service/response/common"
type GetAllJiraStatusPaginatedResponse struct {
Data []HoustonJiraStatus `json:"data"`
Page service.Page `json:"page"`
Status int `json:"status"`
}
type HoustonJiraStatus struct {
IncidentID uint `json:"incidentID"`
IncidentName string `json:"incidentName"`
JiraKey string `json:"jiraKey"`
JiraLink string `json:"jiraLink"`
JiraType string `json:"jiraType"`
JiraSummary string `json:"jiraSummary"`
JiraStatus string `json:"jiraStatus"`
TeamsInvolved []string `json:"teamsInvolved"`
JiraCreatedAt string `json:"jiraCreateAt"`
}