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)/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

View File

@@ -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)
}

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -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

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/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

View File

@@ -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
}

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)
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)

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)
}
logger.Info(fmt.Sprintf("response body: %s", responseBody))
searchJQLResponse := response.JiraSearchJQLResponse{}
// Parse api response body into JiraSearchJQLResponse

View File

@@ -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

View File

@@ -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)
}

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"`
}