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:
2
Makefile
2
Makefile
@@ -60,3 +60,5 @@ generatemocks:
|
||||
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/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
|
||||
|
||||
@@ -24,7 +24,7 @@ type IncidentHandler struct {
|
||||
gin *gin.Engine
|
||||
db *gorm.DB
|
||||
authService *service.AuthService
|
||||
service *incident.IncidentServiceV2
|
||||
service *incident.IncidentServiceV2
|
||||
}
|
||||
|
||||
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,
|
||||
db: db,
|
||||
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))
|
||||
}
|
||||
|
||||
func (h *IncidentHandler) HandleUpdateIncident(c *gin.Context) {
|
||||
func (handler *IncidentHandler) HandleUpdateIncident(c *gin.Context) {
|
||||
userEmail := c.GetHeader(util.UserEmailHeader)
|
||||
sessionToken := c.GetHeader(util.SessionTokenHeader)
|
||||
isValidUser, err := h.authService.CheckValidUser(sessionToken, userEmail)
|
||||
isValidUser, err := handler.authService.CheckValidUser(sessionToken, userEmail)
|
||||
if err != nil || !isValidUser {
|
||||
c.JSON(http.StatusUnauthorized, common.ErrorResponse(errors.New("Unauthorized user"), http.StatusUnauthorized, nil))
|
||||
return
|
||||
@@ -77,7 +77,7 @@ func (h *IncidentHandler) HandleUpdateIncident(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
incidentServiceV2 := incident.NewIncidentServiceV2(h.db)
|
||||
incidentServiceV2 := incident.NewIncidentServiceV2(handler.db)
|
||||
|
||||
result, err := incidentServiceV2.UpdateIncident(updateIncidentRequest, userEmail)
|
||||
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))
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -160,6 +160,7 @@ func (s *Server) incidentClientHandlerV2(houstonGroup *gin.RouterGroup) {
|
||||
}
|
||||
houstonGroup.POST("/link-jira-to-incident", incidentHandler.HandleJiraLinking)
|
||||
houstonGroup.POST("/unlink-jira-from-incident", incidentHandler.HandleJiraUnLinking)
|
||||
houstonGroup.GET("/get-jira-statuses", incidentHandler.HandleGetJiraStatuses)
|
||||
}
|
||||
|
||||
func (s *Server) incidentHandler(houstonGroup *gin.RouterGroup) {
|
||||
|
||||
@@ -88,6 +88,28 @@ func Difference(s1, s2 []string) []string {
|
||||
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) {
|
||||
hash := make(map[string]bool)
|
||||
for _, e := range s1 {
|
||||
|
||||
@@ -86,7 +86,7 @@ create-incident-v2-enabled=CREATE_INCIDENT_V2_ENABLED
|
||||
get-teams.v2.enabled=GET_TEAMS_V2_ENABLED
|
||||
#slack details
|
||||
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
|
||||
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.username=JIRA_USERNAME
|
||||
jira.api.token=JIRA_API_TOKEN
|
||||
jira.link.max.length=JIRA_LINK_MAX_LENGTH
|
||||
18
db/migration/000010_incident_jira_table.up.sql
Normal file
18
db/migration/000010_incident_jira_table.up.sql
Normal 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
4
go.mod
@@ -15,7 +15,7 @@ require (
|
||||
github.com/google/uuid v1.4.0
|
||||
github.com/jackc/pgx/v5 v5.3.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/slack-go/slack v0.12.1
|
||||
github.com/spf13/cobra v1.6.1
|
||||
@@ -54,7 +54,7 @@ require (
|
||||
github.com/chromedp/sysutil v1.0.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // 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/pool v0.2.1 // indirect
|
||||
github.com/gobwas/ws v1.1.0 // indirect
|
||||
|
||||
@@ -5,18 +5,22 @@ import (
|
||||
"fmt"
|
||||
"github.com/slack-go/slack"
|
||||
"github.com/spf13/viper"
|
||||
"houston/appcontext"
|
||||
"houston/common/util"
|
||||
"houston/internal/processor/action/view"
|
||||
"houston/logger"
|
||||
"houston/model/incident"
|
||||
incidentJiraModel "houston/model/incident_jira"
|
||||
incidentService "houston/service/incident"
|
||||
"houston/service/incident_jira"
|
||||
slack2 "houston/service/slack"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type IncidentJiraLinksAction struct {
|
||||
incidentService incidentService.IIncidentService
|
||||
slackService slack2.ISlackService
|
||||
incidentService incidentService.IIncidentService
|
||||
incidentJiraService incident_jira.IncidentJiraService
|
||||
slackService slack2.ISlackService
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -28,8 +32,9 @@ func NewIncidentJiraLinksAction(
|
||||
slackService slack2.ISlackService,
|
||||
) *IncidentJiraLinksAction {
|
||||
return &IncidentJiraLinksAction{
|
||||
incidentService: incidentService,
|
||||
slackService: slackService,
|
||||
incidentService: incidentService,
|
||||
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)
|
||||
}
|
||||
|
||||
// Deprecated: updateJiraLinks is deprecated. Use updateIncidentJiraLinks instead
|
||||
func (action *IncidentJiraLinksAction) updateJiraLinks(jiraLinks string, callback slack.InteractionCallback, incidentEntity *incident.IncidentEntity) error {
|
||||
channelID := callback.View.PrivateMetadata
|
||||
formattedJiraLinks := strings.Split(
|
||||
@@ -72,3 +78,46 @@ func (action *IncidentJiraLinksAction) updateJiraLinks(jiraLinks string, callbac
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
if err != nil || incidentEntity == nil {
|
||||
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))
|
||||
}
|
||||
jiraLinksValue := actions[util.SetJiraLinks].Value
|
||||
err = action.jiraAction.updateJiraLinks(jiraLinksValue, callback, incidentEntity)
|
||||
err = action.jiraAction.updateIncidentJiraLinks(jiraLinksValue, incidentEntity)
|
||||
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)
|
||||
tagValuesMap, _ := action.tagsAction.getIncidentTagValuesAsMap(incidentEntity.ID)
|
||||
|
||||
16
model/incident_jira/entity.go
Normal file
16
model/incident_jira/entity.go
Normal 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"
|
||||
}
|
||||
18
model/incident_jira/incident_jira_repository.go
Normal file
18
model/incident_jira/incident_jira_repository.go
Normal 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}
|
||||
}
|
||||
108
model/incident_jira/incident_jira_repository_impl.go
Normal file
108
model/incident_jira/incident_jira_repository_impl.go
Normal 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
|
||||
}
|
||||
7
model/incident_jira/model.go
Normal file
7
model/incident_jira/model.go
Normal 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"`
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
logger.Info(fmt.Sprintf("response body: %s", responseBody))
|
||||
|
||||
searchJQLResponse := response.JiraSearchJQLResponse{}
|
||||
|
||||
// Parse api response body into JiraSearchJQLResponse
|
||||
|
||||
@@ -14,17 +14,25 @@ import (
|
||||
"houston/logger"
|
||||
"houston/model/incident"
|
||||
"houston/model/incident_channel"
|
||||
incident_jira2 "houston/model/incident_jira"
|
||||
"houston/model/log"
|
||||
"houston/model/severity"
|
||||
"houston/model/team"
|
||||
"houston/model/user"
|
||||
"houston/pkg/atlassian"
|
||||
"houston/pkg/atlassian/dto/response"
|
||||
conference2 "houston/pkg/conference"
|
||||
"houston/pkg/rest"
|
||||
service2 "houston/service/conference"
|
||||
incidentChannel "houston/service/incident_channel"
|
||||
"houston/service/incident_jira"
|
||||
"houston/service/krakatoa"
|
||||
request "houston/service/request"
|
||||
service "houston/service/response"
|
||||
common "houston/service/response/common"
|
||||
"houston/service/slack"
|
||||
"math"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -41,6 +49,7 @@ type IncidentServiceV2 struct {
|
||||
incidentRepository incident.IIncidentRepository
|
||||
userRepository user.IUserRepository
|
||||
krakatoaService krakatoa.IKrakatoaService
|
||||
incidentJiraService incident_jira.IncidentJiraService
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -66,6 +75,7 @@ func NewIncidentServiceV2(db *gorm.DB) *IncidentServiceV2 {
|
||||
userRepository: userRepository,
|
||||
incidentChannelService: incidentChannelService,
|
||||
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)
|
||||
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 util.Contains(entity.JiraLinks, jiraLinks[0]) {
|
||||
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))
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
func (i *IncidentServiceV2) GetAllOpenIncidents() ([]incident.IncidentEntity, int, error) {
|
||||
incidents, resultLength, err := i.incidentRepository.GetAllOpenIncidents()
|
||||
@@ -340,6 +499,22 @@ func (i *IncidentServiceV2) updateJiraIDs(entity *incident.IncidentEntity, user,
|
||||
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
|
||||
incident slack channel, tags pse/dev oncall, assigns responder, posts SLA message, creates gmeet and posts the link
|
||||
|
||||
@@ -9,10 +9,13 @@ import (
|
||||
type IIncidentService interface {
|
||||
CreateIncident(request request.CreateIncidentRequestV2, source string, blazeGroupChannelID string) (service.IncidentResponse, error)
|
||||
GetIncidentById(incidentId uint) (*incident.IncidentEntity, error)
|
||||
GetIncidentByChannelID(channelID string) (*incident.IncidentEntity, error)
|
||||
LinkJiraToIncident(incidentId uint, linkedBy string, jiraLinks ...string) error
|
||||
UnLinkJiraFromIncident(incidentId uint, unLinkedBy, jiraLink string) error
|
||||
GetAllOpenIncidents() ([]incident.IncidentEntity, int, error)
|
||||
GetIncidentRoleByIncidentIdAndRole(incidentId uint, role string) (*incident.IncidentRoleEntity, 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)
|
||||
}
|
||||
|
||||
15
service/incident_jira/incident_jira_service.go
Normal file
15
service/incident_jira/incident_jira_service.go
Normal 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}
|
||||
}
|
||||
41
service/incident_jira/incident_jira_service_impl.go
Normal file
41
service/incident_jira/incident_jira_service_impl.go
Normal 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)
|
||||
}
|
||||
68
service/incident_jira/incident_jira_service_test.go
Normal file
68
service/incident_jira/incident_jira_service_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
21
service/response/get_all_jira_status_response.go
Normal file
21
service/response/get_all_jira_status_response.go
Normal 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"`
|
||||
}
|
||||
Reference in New Issue
Block a user