TP-46247 | API to add jira links to an incident (#257)

* TP-46247 | API to add jira links to an incident

* TP-464408 | Add Jira link modal

* TP-45730 | renaming log entity name back to log from logger
This commit is contained in:
Shashank Shekhar
2023-11-03 15:30:07 +05:30
committed by GitHub
parent 3f4a671eb6
commit 5ce7d38064
18 changed files with 594 additions and 106 deletions

View File

@@ -18,18 +18,20 @@ const (
)
type IncidentHandler struct {
gin *gin.Engine
db *gorm.DB
gin *gin.Engine
db *gorm.DB
service *incident.IncidentServiceV2
}
func NewIncidentHandler(gin *gin.Engine, db *gorm.DB) *IncidentHandler {
func NewIncidentHandler(gin *gin.Engine, db *gorm.DB, incidentService *incident.IncidentServiceV2) *IncidentHandler {
return &IncidentHandler{
gin: gin,
db: db,
gin: gin,
db: db,
service: incidentService,
}
}
func (h *IncidentHandler) HandleCreateIncident(c *gin.Context) {
func (handler *IncidentHandler) HandleCreateIncident(c *gin.Context) {
var createIncidentRequest request.CreateIncidentRequestV2
if err := c.ShouldBindJSON(&createIncidentRequest); err != nil {
c.JSON(http.StatusInternalServerError, err)
@@ -41,8 +43,7 @@ func (h *IncidentHandler) HandleCreateIncident(c *gin.Context) {
c.JSON(http.StatusBadRequest, common.ErrorResponse(err, http.StatusBadRequest, nil))
return
}
incidentServiceV2 := incident.NewIncidentServiceV2(h.db)
incidentResponse, err := incidentServiceV2.CreateIncident(createIncidentRequest, "API", "")
incidentResponse, err := handler.service.CreateIncident(createIncidentRequest, "API", "")
if err != nil {
logger.Error(fmt.Sprintf("%s Failed to create incident", logTag), zap.Error(err))
c.JSON(http.StatusInternalServerError, common.ErrorResponse(err, http.StatusInternalServerError, nil))
@@ -50,3 +51,52 @@ func (h *IncidentHandler) HandleCreateIncident(c *gin.Context) {
}
c.JSON(http.StatusOK, common.SuccessResponse(incidentResponse, http.StatusOK))
}
func (handler *IncidentHandler) HandleJiraLinking(c *gin.Context) {
var linkJiraRequest request.LinkJiraRequest
if err := c.ShouldBindJSON(&linkJiraRequest); err != nil {
c.JSON(http.StatusBadRequest, err)
return
}
if err := utils.ValidateLinkJiraRequest(linkJiraRequest); err != nil {
logger.Debug(fmt.Sprintf("%s invalid request to link Jira reveived. %s", logTag, err.Error()))
c.JSON(http.StatusBadRequest, common.ErrorResponse(err, http.StatusBadGateway, nil))
return
}
err := handler.service.LinkJiraToIncident(linkJiraRequest.IncidentID, linkJiraRequest.User, linkJiraRequest.JiraLink)
if err != nil {
logger.Error(
fmt.Sprintf(
"%s failed to link jira to the incident %d", logTag, linkJiraRequest.IncidentID,
), zap.Error(err),
)
c.JSON(http.StatusInternalServerError, common.ErrorResponse(err, http.StatusBadRequest, nil))
return
}
c.JSON(http.StatusOK, common.SuccessResponse("Jira link added successfully", http.StatusOK))
}
func (handler *IncidentHandler) HandleJiraUnLinking(c *gin.Context) {
var unlinkJiraRequest request.LinkJiraRequest
if err := c.ShouldBindJSON(&unlinkJiraRequest); err != nil {
c.JSON(http.StatusBadRequest, err)
return
}
if err := utils.ValidateLinkJiraRequest(unlinkJiraRequest); err != nil {
logger.Debug(fmt.Sprintf("%s invalid request to unLink Jira reveived. %s", logTag, err.Error()))
c.JSON(http.StatusBadRequest, common.ErrorResponse(err, http.StatusBadGateway, nil))
return
}
if err := handler.service.UnLinkJiraFromIncident(
unlinkJiraRequest.IncidentID, unlinkJiraRequest.User, unlinkJiraRequest.JiraLink,
); err != nil {
logger.Error(
fmt.Sprintf(
"%s failed to unlink jira from the incident %d", logTag, unlinkJiraRequest.IncidentID,
), zap.Error(err),
)
c.JSON(http.StatusInternalServerError, common.ErrorResponse(err, http.StatusNotFound, nil))
return
}
c.JSON(http.StatusOK, common.SuccessResponse("Jira link removed successfully", http.StatusOK))
}

View File

@@ -15,6 +15,8 @@ import (
"houston/model/team"
"houston/model/user"
"houston/pkg/slackbot"
incidentServiceV2 "houston/service/incident"
slack2 "houston/service/slack"
"github.com/slack-go/slack"
"github.com/slack-go/slack/slackevents"
@@ -48,6 +50,8 @@ func NewSlackHandler(gormClient *gorm.DB, socketModeClient *socketmode.Client) *
slashCommandProcessor := processor.NewSlashCommandProcessor(socketModeClient, incidentService, slackbotClient)
grafanaRepository := diagnostic.NewDiagnoseRepository(gormClient)
diagnosticCommandProcessor := processor.NewDiagnosticCommandProcessor(socketModeClient, grafanaRepository)
incidentServiceV2 := incidentServiceV2.NewIncidentServiceV2(gormClient)
slackService := slack2.NewSlackService()
cron.RunJob(socketModeClient, gormClient, incidentService, severityService, teamService, shedlockService, userService)
@@ -59,7 +63,7 @@ func NewSlackHandler(gormClient *gorm.DB, socketModeClient *socketmode.Client) *
socketModeClient, incidentService, teamService, severityService,
),
blockActionProcessor: processor.NewBlockActionProcessor(
socketModeClient, incidentService, teamService, severityService, tagService, slackbotClient,
socketModeClient, incidentService, teamService, severityService, tagService, slackbotClient, incidentServiceV2, slackService,
),
viewSubmissionProcessor: processor.NewViewSubmissionProcessor(
socketModeClient, incidentService, teamService, severityService, tagService, teamService, slackbotClient, gormClient,

View File

@@ -13,6 +13,7 @@ import (
"houston/model/ingester"
"houston/pkg/slackbot"
"houston/service"
"houston/service/incident"
"net/http"
"strings"
"time"
@@ -120,8 +121,11 @@ func (s *Server) incidentClientHandlerV2(houstonGroup *gin.RouterGroup) {
origin := c.Request.Header.Get("Origin")
c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
})
incidentHandler := handler.NewIncidentHandler(s.gin, s.db)
incidentServiceV2 := incident.NewIncidentServiceV2(s.db)
incidentHandler := handler.NewIncidentHandler(s.gin, s.db, incidentServiceV2)
houstonGroup.POST("/create-incident-v2", incidentHandler.HandleCreateIncident)
houstonGroup.POST("/link-jira-to-incident", incidentHandler.HandleJiraLinking)
houstonGroup.POST("/unlink-jira-from-incident", incidentHandler.HandleJiraUnLinking)
}
func (s *Server) incidentHandler(houstonGroup *gin.RouterGroup) {

View File

@@ -6,6 +6,7 @@ import (
"github.com/slack-go/slack"
"github.com/slack-go/slack/socketmode"
"go.uber.org/zap"
"golang.org/x/exp/slices"
"houston/logger"
"houston/model/incident"
"houston/model/severity"
@@ -27,6 +28,49 @@ func RemoveDuplicate[T string | int](sliceList []T) []T {
return list
}
// Difference : finds difference of two slices and returns a new slice
func Difference(s1, s2 []string) []string {
combinedSlice := append(s1, s2...)
m := make(map[string]int)
for _, v := range combinedSlice {
if _, ok := m[v]; ok {
// remove element later as it exist in both slice.
m[v] += 1
continue
}
// new entry, add in map!
m[v] = 1
}
var result []string
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 {
hash[e] = true
}
for _, e := range s2 {
// If elements present in the hashmap then append intersection list.
if hash[e] {
inter = append(inter, e)
}
}
//Remove dups from slice.
inter = RemoveDuplicate(inter)
return
}
// Contains checks if the given string exists on the slice of string and returns boolean
func Contains[S ~[]E, E comparable](s S, v E) bool {
return slices.Contains(s, v)
}
func GetColorBySeverity(severityId uint) string {
switch severityId {
case 1:

View File

@@ -15,6 +15,7 @@ const (
SetIncidentTitle = "set_incident_title"
SetIncidentDescription = "set_incident_description"
SetRCA = "set_rca"
SetJiraLinks = "set_jira_links"
AddTags = "add_tags"
ShowTags = "show_tags"
RemoveTag = "remove_tags"
@@ -33,6 +34,7 @@ const (
SetIncidentTypeSubmit = "set_incident_type_submit"
UpdateTagSubmit = "updateTagSubmit"
SetIncidentRcaSubmit = "set_rca_submit"
SetIncidentJiraLinksSubmit = "set_Jira_links_submit"
ShowIncidentSubmit = "show_incident_submit"
MarkIncidentDuplicateSubmit = "mark_incident_duplicate_submit"
)

View File

@@ -78,4 +78,5 @@ create-incident.title.max-length=100
create-incident-v2-enabled=CREATE_INCIDENT_V2_ENABLED
#slack details
slack.workspace.id=SLACK_WORKSPACE_ID
slack.workspace.id=SLACK_WORKSPACE_ID
navi.jira.base.url=https://navihq.atlassian.net/

30
go.mod
View File

@@ -10,7 +10,7 @@ require (
github.com/chromedp/chromedp v0.9.1
github.com/gin-contrib/zap v0.1.0
github.com/gin-gonic/gin v1.9.1
github.com/google/uuid v1.3.0
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
@@ -19,13 +19,14 @@ require (
github.com/spf13/viper v1.16.0
github.com/thoas/go-funk v0.9.3
go.uber.org/zap v1.24.0
golang.org/x/exp v0.0.0-20231006140011-7918f672742d
gorm.io/datatypes v1.2.0
gorm.io/driver/postgres v1.5.2
gorm.io/gorm v1.25.2
)
require (
cloud.google.com/go/compute v1.19.0 // indirect
cloud.google.com/go/compute v1.23.1 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
github.com/aws/aws-sdk-go-v2 v1.18.0 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect
@@ -53,9 +54,9 @@ require (
github.com/gobwas/ws v1.1.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/s2a-go v0.1.3 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
github.com/googleapis/gax-go/v2 v2.8.0 // indirect
github.com/google/s2a-go v0.1.7 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
@@ -63,10 +64,11 @@ require (
github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.9.0 // indirect
go.opencensus.io v0.24.0 // indirect
golang.org/x/oauth2 v0.7.0 // indirect
golang.org/x/oauth2 v0.13.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
google.golang.org/grpc v1.55.0 // indirect
google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b // indirect
google.golang.org/grpc v1.59.0 // indirect
gorm.io/driver/mysql v1.4.7 // indirect
)
@@ -110,12 +112,12 @@ require (
go.uber.org/goleak v1.1.12 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.9.0 // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/text v0.9.0 // indirect
google.golang.org/api v0.122.0
google.golang.org/protobuf v1.30.0 // indirect
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
google.golang.org/api v0.149.0
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@@ -0,0 +1,97 @@
package action
import (
"fmt"
"github.com/slack-go/slack"
"github.com/slack-go/slack/socketmode"
"github.com/spf13/viper"
"houston/internal/processor/action/view"
"houston/logger"
"houston/model/incident"
incidentService "houston/service/incident"
slack2 "houston/service/slack"
"strings"
)
type IncidentUpdateJiraLinksAction struct {
client *socketmode.Client
incidentRepository *incident.Repository
incidentService *incidentService.IncidentServiceV2
slackService *slack2.SlackService
}
const (
logTag = "[IncidentUpdateJiraLinksAction]"
)
func NewIncidentUpdateJiraLinksAction(
client *socketmode.Client,
incidentRepository *incident.Repository,
incidentServiceV2 *incidentService.IncidentServiceV2,
slackService *slack2.SlackService,
) *IncidentUpdateJiraLinksAction {
return &IncidentUpdateJiraLinksAction{
client: client,
incidentRepository: incidentRepository,
incidentService: incidentServiceV2,
slackService: slackService,
}
}
func (action *IncidentUpdateJiraLinksAction) IncidentUpdateJiraLinksRequestProcess(callback slack.InteractionCallback, request *socketmode.Request) {
result, err := action.incidentRepository.FindIncidentByChannelId(callback.Channel.ID)
if err != nil || result == nil {
logger.Error(fmt.Sprintf("%s failed to find incident entity for: %s", logTag, callback.Channel.Name))
return
}
modalRequest := view.BuildJiraLinksModal(callback.Channel, result.JiraLinks...)
_, err = action.client.OpenView(callback.TriggerID, modalRequest)
if err != nil {
logger.Error(fmt.Sprintf("%s failed to open view command for: %s", logTag, callback.Channel.Name))
return
}
var payload interface{}
action.client.Ack(*request, payload)
}
func (action *IncidentUpdateJiraLinksAction) IncidentUpdateJiraLinks(callback slack.InteractionCallback, request *socketmode.Request, channel slack.Channel, user slack.User) {
channelID := callback.View.PrivateMetadata
incidentEntity, err := action.incidentRepository.FindIncidentByChannelId(channelID)
if err != nil || incidentEntity == nil {
logger.Error(fmt.Sprintf("%s failed to find incident entity for: %s", logTag, channel.Name))
return
}
jiraLinks := strings.Split(
strings.ReplaceAll(strings.ReplaceAll(buildUpdateJiraLinksRequest(callback.View.State.Values), "\n", ""), " ", ""),
",",
)
for _, l := range jiraLinks {
if strings.HasPrefix(l, viper.GetString("navi.jira.base.url")) == false {
err := action.slackService.PostEphemeralByChannelID(fmt.Sprintf("%s is not a valid jira link", l), user.ID, false, channelID)
if err != nil {
logger.Debug(fmt.Sprintf("%s failed to post jira link validation failure ephemeral to slack channel: %s", logTag, incidentEntity.IncidentName))
}
return
}
}
err = action.incidentService.LinkJiraToIncident(incidentEntity.ID, user.ID, jiraLinks...)
var payload interface{}
action.client.Ack(*request, payload)
}
func buildUpdateJiraLinksRequest(blockActions map[string]map[string]slack.BlockAction) string {
var requestMap = make(map[string]string, 0)
for _, actions := range blockActions {
for actionID, a := range actions {
if a.Type == "plain_text_input" {
requestMap[actionID] = a.Value
}
}
}
return requestMap["jira"]
}

View File

@@ -0,0 +1,39 @@
package view
import (
"github.com/slack-go/slack"
"houston/common/util"
"strings"
)
func BuildJiraLinksModal(channel slack.Channel, jiraLinks ...string) slack.ModalViewRequest {
titleText := slack.NewTextBlockObject(slack.PlainTextType, "Jira link(s)", false, false)
closeText := slack.NewTextBlockObject(slack.PlainTextType, "Close", false, false)
submitText := slack.NewTextBlockObject(slack.PlainTextType, "Submit", false, false)
jiraLinksText := slack.NewTextBlockObject(slack.PlainTextType, " ", false, false)
jiraLinkPlaceholder := slack.NewTextBlockObject(slack.PlainTextType, "Add comma separated jira links here...", false, false)
jiraLinkElement := slack.NewPlainTextInputBlockElement(jiraLinkPlaceholder, "jira")
jiraLinkElement.Multiline = true
jiraLinkElement.InitialValue = strings.Join(jiraLinks, ", ")
jiraLinkElement.MaxLength = 3000
jiraLinksBlock := slack.NewInputBlock("JIRA_LINKS", jiraLinksText, nil, jiraLinkElement)
jiraLinksBlock.Optional = false
blocks := slack.Blocks{
BlockSet: []slack.Block{
jiraLinksBlock,
},
}
return slack.ModalViewRequest{
Type: slack.VTModal,
Title: titleText,
Close: closeText,
Submit: submitText,
Blocks: blocks,
PrivateMetadata: channel.ID,
CallbackID: util.SetIncidentJiraLinksSubmit,
}
}

View File

@@ -63,11 +63,9 @@ func ExistingIncidentOptionsBlock() map[string]interface{} {
return map[string]interface{}{
"blocks": []slack.Block{
incidentSectionBlock(),
taskSectionBlock(),
tagsSectionBlock(),
onCallSectionBlock(),
rcaSectionBlock(),
botHelpSectionBlock(),
jiraLinksSectionBlock(),
},
}
}
@@ -331,3 +329,30 @@ func rcaSectionBlock() *slack.SectionBlock {
}
return slack.NewSectionBlock(textBlock, nil, accessoryOption, slack.SectionBlockOptionBlockID("set_rca"))
}
func jiraLinksSectionBlock() *slack.SectionBlock {
textBlock := &slack.TextBlockObject{
Type: "mrkdwn",
Text: "Jira links",
}
optionBlockObjects := []*slack.OptionBlockObject{
{
Text: &slack.TextBlockObject{
Type: "plain_text",
Text: "Add Jira link(s)",
},
Value: util.SetJiraLinks,
},
}
accessoryOption := &slack.Accessory{
SelectElement: &slack.SelectBlockElement{
Type: "static_select",
ActionID: util.SetJiraLinks,
Options: optionBlockObjects,
Placeholder: slack.NewTextBlockObject("plain_text", "Select command", false, false),
},
}
return slack.NewSectionBlock(textBlock, nil, accessoryOption, slack.SectionBlockOptionBlockID(util.SetJiraLinks))
}

View File

@@ -11,6 +11,8 @@ import (
"houston/model/tag"
"houston/model/team"
"houston/pkg/slackbot"
incidentService "houston/service/incident"
slack2 "houston/service/slack"
"github.com/slack-go/slack"
"github.com/slack-go/slack/socketmode"
@@ -35,27 +37,49 @@ type BlockActionProcessor struct {
incidentUpdateTagsAction *action.IncidentUpdateTagsAction
incidentShowTagsAction *action.IncidentShowTagsAction
incidentUpdateRcaAction *action.IncidentUpdateRcaAction
incidentUpdateJiraLinksAction *action.IncidentUpdateJiraLinksAction
incidentDuplicateAction *action.DuplicateIncidentAction
incidentServiceV2 *incidentService.IncidentServiceV2
slackService *slack2.SlackService
}
func NewBlockActionProcessor(socketModeClient *socketmode.Client, incidentRepository *incident.Repository,
teamService *team.Repository, severityService *severity.Repository, tagService *tag.Repository,
slackbotClient *slackbot.Client) *BlockActionProcessor {
func NewBlockActionProcessor(
socketModeClient *socketmode.Client,
incidentRepository *incident.Repository,
teamService *team.Repository,
severityService *severity.Repository,
tagService *tag.Repository,
slackbotClient *slackbot.Client,
incidentServiceV2 *incidentService.IncidentServiceV2,
slackService *slack2.SlackService,
) *BlockActionProcessor {
return &BlockActionProcessor{
socketModeClient: socketModeClient,
startIncidentBlockAction: action.NewStartIncidentBlockAction(socketModeClient, teamService, severityService),
showIncidentsAction: action.ShowIncidentsBlockAction(socketModeClient, teamService),
assignIncidentAction: action.NewAssignIncidentAction(socketModeClient, incidentRepository),
incidentResolveAction: action.NewIncidentResolveProcessor(socketModeClient, incidentRepository, tagService, teamService, severityService),
incidentUpdateAction: action.NewIncidentUpdateAction(socketModeClient, incidentRepository, tagService, teamService, severityService),
incidentUpdateTypeAction: action.NewIncidentUpdateTypeAction(socketModeClient, incidentRepository, teamService, severityService, slackbotClient),
incidentUpdateSeverityAction: action.NewIncidentUpdateSeverityAction(socketModeClient, incidentRepository, severityService, teamService, slackbotClient),
incidentUpdateTitleAction: action.NewIncidentUpdateTitleAction(socketModeClient, incidentRepository, teamService, severityService, slackbotClient),
socketModeClient: socketModeClient,
startIncidentBlockAction: action.NewStartIncidentBlockAction(socketModeClient, teamService,
severityService),
showIncidentsAction: action.ShowIncidentsBlockAction(socketModeClient, teamService),
assignIncidentAction: action.NewAssignIncidentAction(socketModeClient, incidentRepository),
incidentResolveAction: action.NewIncidentResolveProcessor(socketModeClient, incidentRepository,
tagService, teamService, severityService),
incidentUpdateAction: action.NewIncidentUpdateAction(socketModeClient, incidentRepository,
tagService, teamService, severityService),
incidentUpdateTypeAction: action.NewIncidentUpdateTypeAction(socketModeClient, incidentRepository,
teamService, severityService, slackbotClient),
incidentUpdateSeverityAction: action.NewIncidentUpdateSeverityAction(socketModeClient, incidentRepository,
severityService, teamService, slackbotClient),
incidentUpdateTitleAction: action.NewIncidentUpdateTitleAction(socketModeClient, incidentRepository,
teamService, severityService, slackbotClient),
incidentUpdateDescriptionAction: action.NewIncidentUpdateDescriptionAction(socketModeClient, incidentRepository),
incidentUpdateTagsAction: action.NewIncidentUpdateTagsAction(socketModeClient, incidentRepository, teamService, tagService),
incidentShowTagsAction: action.NewIncidentShowTagsProcessor(socketModeClient, incidentRepository, tagService),
incidentUpdateRcaAction: action.NewIncidentUpdateRcaAction(socketModeClient, incidentRepository),
incidentDuplicateAction: action.NewDuplicateIncidentProcessor(socketModeClient, incidentRepository, tagService, teamService, severityService),
incidentUpdateTagsAction: action.NewIncidentUpdateTagsAction(socketModeClient, incidentRepository,
teamService, tagService),
incidentShowTagsAction: action.NewIncidentShowTagsProcessor(socketModeClient, incidentRepository,
tagService),
incidentUpdateRcaAction: action.NewIncidentUpdateRcaAction(socketModeClient, incidentRepository),
incidentUpdateJiraLinksAction: action.NewIncidentUpdateJiraLinksAction(socketModeClient, incidentRepository,
incidentServiceV2, slackService),
incidentDuplicateAction: action.NewDuplicateIncidentProcessor(socketModeClient, incidentRepository,
tagService, teamService, severityService),
}
}
@@ -95,6 +119,10 @@ func (bap *BlockActionProcessor) ProcessCommand(callback slack.InteractionCallba
{
bap.processRcaCommands(callback, request)
}
case util.SetJiraLinks:
{
bap.processJiraLinkCommands(callback, request)
}
default:
{
msgOption := slack.MsgOptionText(fmt.Sprintf("We are working on it"), false)
@@ -159,6 +187,16 @@ func (bap *BlockActionProcessor) processRcaCommands(callback slack.InteractionCa
}
}
func (bap *BlockActionProcessor) processJiraLinkCommands(callback slack.InteractionCallback, request *socketmode.Request) {
action1 := util.BlockActionType(callback.ActionCallback.BlockActions[0].SelectedOption.Value)
switch action1 {
case util.SetJiraLinks:
{
bap.incidentUpdateJiraLinksAction.IncidentUpdateJiraLinksRequestProcess(callback, request)
}
}
}
func (bap *BlockActionProcessor) processTagsCommands(callback slack.InteractionCallback, request *socketmode.Request) {
action1 := util.BlockActionType(callback.ActionCallback.BlockActions[0].SelectedOption.Value)
switch action1 {
@@ -189,6 +227,7 @@ type ViewSubmissionProcessor struct {
incidentUpdateTypeAction *action.IncidentUpdateTypeAction
incidentUpdateTagsAction *action.IncidentUpdateTagsAction
incidentUpdateRca *action.IncidentUpdateRcaAction
incidentUpdateJiraLinks *action.IncidentUpdateJiraLinksAction
showIncidentSubmitAction *action.ShowIncidentsSubmitAction
incidentDuplicateAction *action.DuplicateIncidentAction
db *gorm.DB
@@ -204,20 +243,33 @@ func NewViewSubmissionProcessor(
slackbotClient *slackbot.Client,
db *gorm.DB,
) *ViewSubmissionProcessor {
incidentServiceV2 := incidentService.NewIncidentServiceV2(db)
slackService := slack2.NewSlackService()
return &ViewSubmissionProcessor{
socketModeClient: socketModeClient,
incidentChannelMessageUpdateAction: action.NewIncidentChannelMessageUpdateAction(socketModeClient, incidentRepository, teamService, severityService),
createIncidentAction: action.NewCreateIncidentProcessor(socketModeClient, incidentRepository, teamService, severityService, slackbotClient, db),
assignIncidentAction: action.NewAssignIncidentAction(socketModeClient, incidentRepository),
updateIncidentAction: action.NewIncidentUpdateAction(socketModeClient, incidentRepository, tagService, teamService, severityService),
incidentUpdateTitleAction: action.NewIncidentUpdateTitleAction(socketModeClient, incidentRepository, teamService, severityService, slackbotClient),
incidentUpdateDescriptionAction: action.NewIncidentUpdateDescriptionAction(socketModeClient, incidentRepository),
incidentUpdateSeverityAction: action.NewIncidentUpdateSeverityAction(socketModeClient, incidentRepository, severityService, teamService, slackbotClient),
incidentUpdateTypeAction: action.NewIncidentUpdateTypeAction(socketModeClient, incidentRepository, teamService, severityService, slackbotClient),
incidentUpdateTagsAction: action.NewIncidentUpdateTagsAction(socketModeClient, incidentRepository, teamService, tagService),
incidentUpdateRca: action.NewIncidentUpdateRcaAction(socketModeClient, incidentRepository),
showIncidentSubmitAction: action.NewShowIncidentsSubmitAction(socketModeClient, incidentRepository, teamRepository),
incidentDuplicateAction: action.NewDuplicateIncidentProcessor(socketModeClient, incidentRepository, tagService, teamRepository, severityService),
socketModeClient: socketModeClient,
incidentChannelMessageUpdateAction: action.NewIncidentChannelMessageUpdateAction(socketModeClient,
incidentRepository, teamService, severityService),
createIncidentAction: action.NewCreateIncidentProcessor(socketModeClient, incidentRepository,
teamService, severityService, slackbotClient, db),
assignIncidentAction: action.NewAssignIncidentAction(socketModeClient, incidentRepository),
updateIncidentAction: action.NewIncidentUpdateAction(socketModeClient, incidentRepository,
tagService, teamService, severityService),
incidentUpdateTitleAction: action.NewIncidentUpdateTitleAction(socketModeClient, incidentRepository,
teamService, severityService, slackbotClient),
incidentUpdateDescriptionAction: action.NewIncidentUpdateDescriptionAction(socketModeClient, incidentRepository),
incidentUpdateSeverityAction: action.NewIncidentUpdateSeverityAction(socketModeClient, incidentRepository,
severityService, teamService, slackbotClient),
incidentUpdateTypeAction: action.NewIncidentUpdateTypeAction(socketModeClient, incidentRepository,
teamService, severityService, slackbotClient),
incidentUpdateTagsAction: action.NewIncidentUpdateTagsAction(socketModeClient, incidentRepository,
teamService, tagService),
incidentUpdateRca: action.NewIncidentUpdateRcaAction(socketModeClient, incidentRepository),
incidentUpdateJiraLinks: action.NewIncidentUpdateJiraLinksAction(socketModeClient, incidentRepository,
incidentServiceV2, slackService),
showIncidentSubmitAction: action.NewShowIncidentsSubmitAction(socketModeClient, incidentRepository,
teamRepository),
incidentDuplicateAction: action.NewDuplicateIncidentProcessor(socketModeClient, incidentRepository,
tagService, teamRepository, severityService),
}
}
@@ -271,6 +323,10 @@ func (vsp *ViewSubmissionProcessor) ProcessCommand(callback slack.InteractionCal
{
vsp.incidentUpdateRca.IncidentUpdateRca(callback, request, callback.Channel, callback.User)
}
case util.SetIncidentJiraLinksSubmit:
{
vsp.incidentUpdateJiraLinks.IncidentUpdateJiraLinks(callback, request, callback.Channel, callback.User)
}
case util.ShowIncidentSubmit:
{
vsp.showIncidentSubmitAction.ShowIncidentsCommandProcessing(callback, request)

View File

@@ -44,25 +44,25 @@ const (
// IncidentEntity all the incident created will go in this table
type IncidentEntity struct {
gorm.Model
Title string `gorm:"column:title"`
Description string `gorm:"column:description"`
Status uint `gorm:"column:status"`
SeverityId uint `gorm:"column:severity_id"`
IncidentName string `gorm:"column:incident_name"`
SlackChannel string `gorm:"column:slack_channel"`
DetectionTime *time.Time `gorm:"column:detection_time"`
StartTime time.Time `gorm:"column:start_time"`
EndTime *time.Time `gorm:"column:end_time"`
TeamId uint `gorm:"column:team_id"`
JiraId *string `gorm:"column:jira_id"`
ConfluenceId *string `gorm:"column:confluence_id"`
SeverityTat time.Time `gorm:"column:severity_tat"`
RemindMeAt *time.Time `gorm:"column:remind_me_at"`
EnableReminder bool `gorm:"column:enable_reminder"`
CreatedBy string `gorm:"column:created_by"`
UpdatedBy string `gorm:"column:updated_by"`
MetaData JSON `gorm:"column:meta_data"`
RCA string `gorm:"column:rca_text"`
Title string `gorm:"column:title"`
Description string `gorm:"column:description"`
Status uint `gorm:"column:status"`
SeverityId uint `gorm:"column:severity_id"`
IncidentName string `gorm:"column:incident_name"`
SlackChannel string `gorm:"column:slack_channel"`
DetectionTime *time.Time `gorm:"column:detection_time"`
StartTime time.Time `gorm:"column:start_time"`
EndTime *time.Time `gorm:"column:end_time"`
TeamId uint `gorm:"column:team_id"`
JiraLinks pq.StringArray `gorm:"column:jira_links;type:string[]"`
ConfluenceId *string `gorm:"column:confluence_id"`
SeverityTat time.Time `gorm:"column:severity_tat"`
RemindMeAt *time.Time `gorm:"column:remind_me_at"`
EnableReminder bool `gorm:"column:enable_reminder"`
CreatedBy string `gorm:"column:created_by"`
UpdatedBy string `gorm:"column:updated_by"`
MetaData JSON `gorm:"column:meta_data"`
RCA string `gorm:"column:rca_text"`
}
func (IncidentEntity) TableName() string {

View File

@@ -5,7 +5,7 @@ import (
"fmt"
"github.com/slack-go/slack/socketmode"
"github.com/spf13/viper"
logger "houston/logger"
"houston/logger"
"houston/model/log"
"houston/model/severity"
"houston/model/team"
@@ -297,7 +297,7 @@ func (r *Repository) FindIncidentById(Id uint) (*IncidentEntity, error) {
}
if result.RowsAffected == 0 {
return nil, nil
return nil, fmt.Errorf("could not find incident with ID: %d", Id)
}
return &incidentEntity, nil

View File

@@ -3,6 +3,7 @@ package incident
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/google/uuid"
slackClient "github.com/slack-go/slack"
@@ -22,6 +23,7 @@ import (
request "houston/service/request"
service "houston/service/response"
"houston/service/slack"
"regexp"
"strconv"
"strings"
"time"
@@ -150,6 +152,125 @@ func (i *IncidentServiceV2) CreateIncident(
return service.ConvertToIncidentResponse(*incidentEntity), nil
}
func (i *IncidentServiceV2) LinkJiraToIncident(incidentId uint, linkedBy string, jiraLinks ...string) error {
logger.Info(fmt.Sprintf("%s received request to link jira to %d", logTag, incidentId))
logTag := fmt.Sprintf("%s [LinkJiraToIncident]", logTag)
entity, err := i.incidentRepository.FindIncidentById(incidentId)
if err != nil {
errorMessage := fmt.Sprintf("%s failed to fetch indient by id %d", logTag, incidentId)
logger.Info(errorMessage)
return fmt.Errorf("failed to fetch incident by id %d", incidentId)
}
if len(jiraLinks) == 1 {
if util.Contains(entity.JiraLinks, jiraLinks[0]) {
return errors.New("The jira link already exists")
}
}
err = i.updateJiraIDs(entity, linkedBy, logTag, LinkJira, jiraLinks...)
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) UnLinkJiraFromIncident(incidentId uint, unLinkedBy, jiraLink string) error {
logger.Info(fmt.Sprintf("%s received request to unLink jira from %d", logTag, incidentId))
logTag := fmt.Sprintf("%s [UnLinkJiraFromIncident]", logTag)
entity, err := i.incidentRepository.FindIncidentById(incidentId)
if err != nil {
logger.Info(fmt.Sprintf("%s failed to fetch indient by id %d", logTag, incidentId))
return fmt.Errorf("failed to fetch indient by id %d", incidentId)
}
err = i.updateJiraIDs(entity, unLinkedBy, logTag, UnLinkJira, jiraLink)
if err != nil {
logger.Error(fmt.Sprintf("%s failed to unlink Jira ID(s): %s from incident %s", logTag, jiraLink, entity.IncidentName))
return err
}
return nil
}
const (
LinkJira string = "link"
UnLinkJira = "unLink"
)
func (i *IncidentServiceV2) updateJiraIDs(entity *incident.IncidentEntity, user, logTag, action string, jiraLinks ...string) error {
slackUser, err := i.slackService.GetUserByEmailOrID(user)
var updatedBy string
re := regexp.MustCompile(`\n+`)
if err != nil {
logger.Info(fmt.Sprintf("%s failed to find user in slack for given input %s", logTag, user))
updatedBy = user
} else {
updatedBy = slackUser.ID
}
var jiraToBeUpdated []string
var slackMessage string
if action == LinkJira {
jiraToBeUpdated = util.RemoveDuplicateStr(append(entity.JiraLinks, jiraLinks...))
var newJiraLinks []string
for _, i := range jiraLinks {
if !util.Contains(entity.JiraLinks, i) {
newJiraLinks = append(newJiraLinks, i)
}
}
if len(newJiraLinks) == 0 {
_ = i.slackService.PostEphemeralByChannelID("`No new jira link to add`", updatedBy, false, entity.SlackChannel)
} else if len(newJiraLinks) > 1 {
slackMessage = fmt.Sprintf(
"<@%s> `linked the following jiras to the incident: \"%s\"`",
updatedBy, re.ReplaceAllString(strings.Join(newJiraLinks, ", "), " "),
)
} else {
slackMessage = fmt.Sprintf(
"<@%s> `linked the following jira to the incident: \"%s\"`",
updatedBy, re.ReplaceAllString(strings.Join(newJiraLinks, ", "), " "),
)
}
} else {
var validatedJiraIDsToBeRemoved []string
for _, j := range jiraLinks {
if util.Contains(entity.JiraLinks, j) {
validatedJiraIDsToBeRemoved = append(validatedJiraIDsToBeRemoved, j)
}
}
jiraToBeUpdated = util.Difference(entity.JiraLinks, validatedJiraIDsToBeRemoved)
if len(validatedJiraIDsToBeRemoved) > 0 {
slackMessage = fmt.Sprintf(
"<@%s> `unlinked the following jira from the incident: \"%s\"`",
updatedBy, re.ReplaceAllString(strings.Join(validatedJiraIDsToBeRemoved, ", "), " "),
)
}
}
if jiraToBeUpdated == nil {
jiraToBeUpdated = make([]string, 0)
}
entity.JiraLinks = jiraToBeUpdated
entity.UpdatedBy = updatedBy
entity.UpdatedAt = time.Now()
err = i.incidentRepository.UpdateIncident(entity)
if err != nil {
errorMessage := fmt.Sprintf("%s failed to update Jira IDs for incident %s", logTag, entity.IncidentName)
logger.Error(errorMessage)
return fmt.Errorf(errorMessage, err)
}
if slackMessage != "" {
_, err = i.slackService.PostMessageByChannelID(slackMessage, false, entity.SlackChannel)
if err != nil {
logger.Error(fmt.Sprintf("%s failed to post message about update jira link to incident for %s", logTag, entity.IncidentName))
}
}
return nil
}
func createIncidentWorkflow(
i *IncidentServiceV2,
channel *slackClient.Channel,
@@ -540,7 +661,7 @@ func tagPseOrDevOncallToIncident(
//oncall handles should already be added to channel
ts, err := i.slackService.PostMessage(fmt.Sprintf("<@%s>", onCallToBeTagged), false, channel)
if err != nil {
logger.Debug(
logger.Info(
fmt.Sprintf("%s [%s] failed to tag oncall in channel: %s", logTag, incidentName, channel.Name),
zap.Error(err),
)

View File

@@ -0,0 +1,7 @@
package service
type LinkJiraRequest struct {
IncidentID uint `json:"incident_id"`
JiraLink string `json:"jira_link"`
User string `json:"user"`
}

View File

@@ -1,34 +1,35 @@
package service
import (
"github.com/lib/pq"
"houston/model/incident"
"time"
)
type IncidentResponse struct {
ID uint `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Status uint `json:"status"`
StatusName string `json:"statusName"`
SeverityId uint `json:"severityId"`
SeverityName string `json:"severityName"`
IncidentName string `json:"incidentName"`
SlackChannel string `json:"slackChannel"`
DetectionTime *time.Time `json:"detectionTime"`
StartTime time.Time `json:"startTime"`
EndTime *time.Time `json:"endTime"`
TeamId uint `json:"teamId"`
TeamName string `json:"teamName"`
JiraId *string `json:"jiraId"`
ConfluenceId *string `json:"confluenceId"`
SeverityTat time.Time `json:"severityTat"`
RemindMeAt *time.Time `json:"remindMeAt"`
EnableReminder bool `json:"enableReminder"`
CreatedBy string `json:"createdBy"`
UpdatedBy string `json:"updatedBy"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
ID uint `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Status uint `json:"status"`
StatusName string `json:"statusName"`
SeverityId uint `json:"severityId"`
SeverityName string `json:"severityName"`
IncidentName string `json:"incidentName"`
SlackChannel string `json:"slackChannel"`
DetectionTime *time.Time `json:"detectionTime"`
StartTime time.Time `json:"startTime"`
EndTime *time.Time `json:"endTime"`
TeamId uint `json:"teamId"`
TeamName string `json:"teamName"`
JiraLinks pq.StringArray `json:"jiraLinks"`
ConfluenceId *string `json:"confluenceId"`
SeverityTat time.Time `json:"severityTat"`
RemindMeAt *time.Time `json:"remindMeAt"`
EnableReminder bool `json:"enableReminder"`
CreatedBy string `json:"createdBy"`
UpdatedBy string `json:"updatedBy"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
func ConvertToIncidentResponse(incidentEntity incident.IncidentEntity) IncidentResponse {
@@ -44,7 +45,7 @@ func ConvertToIncidentResponse(incidentEntity incident.IncidentEntity) IncidentR
DetectionTime: incidentEntity.DetectionTime,
StartTime: incidentEntity.StartTime,
EndTime: incidentEntity.EndTime,
JiraId: incidentEntity.JiraId,
JiraLinks: incidentEntity.JiraLinks,
ConfluenceId: incidentEntity.ConfluenceId,
SeverityTat: incidentEntity.SeverityTat,
RemindMeAt: incidentEntity.RemindMeAt,

View File

@@ -58,6 +58,17 @@ func (s *SlackService) PostMessageByChannelID(message string, escape bool, chann
return timeStamp, nil
}
func (s *SlackService) PostEphemeralByChannelID(message string, userID string, escape bool, channelID string) error {
msgOption := slack.MsgOptionText(message, escape)
_, err := s.SocketModeClient.PostEphemeral(channelID, userID, msgOption)
if err != nil {
e := fmt.Sprintf("%s Failed to post message into channel: %s", logTag, channelID)
logger.Error(e, zap.Error(err))
return fmt.Errorf("%s : %+v", e, err)
}
return nil
}
func (s *SlackService) PostMessageBlocks(channelID string, blocks slack.Blocks, color string) (string, error) {
att := slack.Attachment{Blocks: blocks, Color: color}
messageOption := slack.MsgOptionAttachments(att)
@@ -105,10 +116,13 @@ func (s *SlackService) GetUserByEmailOrID(userEmailOrSlackID string) (*slack.Use
slackUser, err = s.GetUserBySlackID(userEmailOrSlackID)
if err != nil {
slackUser, err = s.GetUserByEmail(userEmailOrSlackID)
if err != nil {
return nil, fmt.Errorf(
"failed to find slack user by email or slack for %s. Error: %v", userEmailOrSlackID, err,
)
}
}
return slackUser, fmt.Errorf(
"failed to find slack user by email or slack for %s. Error: %v", userEmailOrSlackID, err,
)
return slackUser, nil
}
func (s *SlackService) GetUserByEmail(userEmail string) (*slack.User, error) {

View File

@@ -7,6 +7,7 @@ import (
"github.com/spf13/viper"
service "houston/service/request"
"strconv"
"strings"
)
func ValidatePage(pageSize, pageNumber string) (int64, int64, error) {
@@ -82,6 +83,26 @@ func ValidateCreateIncidentRequestV2(request service.CreateIncidentRequestV2) er
return nil
}
func ValidateLinkJiraRequest(request service.LinkJiraRequest) error {
var errorMessage []string
if request.IncidentID == 0 {
errorMessage = append(errorMessage, "Enter a valid incident I.D.")
}
if request.JiraLink == "" {
errorMessage = append(errorMessage, "Jira links can not be empty")
}
if strings.HasPrefix(request.JiraLink, viper.GetString("navi.jira.base.url")) == false {
errorMessage = append(errorMessage, fmt.Sprintf("%s is not a valid Jira link", request.JiraLink))
}
if request.User == "" {
errorMessage = append(errorMessage, "Enter a valid navi email or slack ID for the linked_by field")
}
if errorMessage != nil {
return fmt.Errorf(strings.Join(errorMessage, ". "))
}
return nil
}
func ValidateUpdateTeamRequest(request service.UpdateTeamRequest) error {
if request.Id == 0 {
return errors.New("id should be present in update request")