TP-0000 | Add tag condition for not resolved (#19)

* TP-0000 | Add tag condition for not resolved

* TP-0000 | archive uncomment

* TP-0000 | ping api

* TP-0000 | mjolnir
This commit is contained in:
Abhijeet Gupta
2023-04-20 23:19:10 +05:30
committed by GitHub Enterprise
parent 9e8195d298
commit 810b314649
11 changed files with 282 additions and 51 deletions

View File

@@ -3,14 +3,17 @@ package app
import (
"fmt"
"houston/cmd/app/handler"
"houston/internal/metrics"
"houston/internal/metrics"
"houston/pkg/slackbot"
"houston/service"
"net/http"
"strconv"
"strings"
"time"
"houston/internal/clients"
"houston/internal/metrics"
"github.com/gin-gonic/gin"
"github.com/spf13/viper"
"go.uber.org/zap"
@@ -18,16 +21,18 @@ import (
)
type Server struct {
gin *gin.Engine
logger *zap.Logger
db *gorm.DB
gin *gin.Engine
logger *zap.Logger
db *gorm.DB
mjolnirClient *clients.MjolnirClient
}
func NewServer(gin *gin.Engine, logger *zap.Logger, db *gorm.DB) *Server {
func NewServer(gin *gin.Engine, logger *zap.Logger, db *gorm.DB, mjolnirClient *clients.MjolnirClient) *Server {
return &Server{
gin: gin,
logger: logger,
db: db,
gin: gin,
logger: logger,
db: db,
mjolnirClient: mjolnirClient,
}
}
@@ -50,6 +55,16 @@ func (s *Server) createMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
whitelistedDomains := getWhitelistedDomains()
//auth handling
isAuthEnabled := viper.GetBool("auth.enabled")
if isAuthEnabled {
sessionResponse, err := s.mjolnirClient.GetSessionResponse(c.Request.Header.Get("X-Session-Token"))
if err != nil || sessionResponse.StatusCode == 401 {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
}
//cors handling
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT")
c.Writer.Header().Set("Access-Control-Allow-Headers", viper.GetString("allowed.custom.headers"))
@@ -98,7 +113,7 @@ func (s *Server) teamHandler() {
houstonClient := NewHoustonClient(s.logger)
slackClient := slackbot.NewSlackClient(s.logger, houstonClient.socketModeClient)
teamHandler := service.NewTeamService(s.gin, s.logger, s.db, slackClient)
s.gin.GET("/teams", teamHandler.GetTeams)
s.gin.GET("/teams/:id", teamHandler.GetTeams)
s.gin.POST("/teams", teamHandler.UpdateTeam)
@@ -106,7 +121,7 @@ func (s *Server) teamHandler() {
func (s *Server) severityHandler() {
severityHandler := service.NewSeverityService(s.gin, s.logger, s.db)
s.gin.GET("/severities", severityHandler.GetSeverities)
s.gin.POST("/severities", severityHandler.UpdateSeverities)
}
@@ -114,7 +129,7 @@ func (s *Server) severityHandler() {
func (s *Server) incidentHandler() {
houstonClient := NewHoustonClient(s.logger)
incidentHandler := service.NewIncidentService(s.gin, s.logger, s.db, houstonClient.socketModeClient)
s.gin.GET("/incidents", incidentHandler.GetIncidents)
s.gin.GET("/incidents/:id", incidentHandler.GetIncidents)
s.gin.POST("/incidents", incidentHandler.UpdateIncident)

View File

@@ -3,6 +3,7 @@ package main
import (
"houston/cmd/app"
"houston/config"
"houston/internal/clients"
"houston/pkg/postgres"
"os"
"time"
@@ -34,7 +35,11 @@ func main() {
db := postgres.NewGormClient(viper.GetString("postgres.dsn"), viper.GetString("postgres.connection.max.idle.time"),
viper.GetString("postgres.connection.max.lifetime"), viper.GetInt("postgres.connections.max.idle"),
viper.GetInt("postgres.connections.max.open"), logger)
sv := app.NewServer(r, logger, db)
httpClient := clients.NewHttpClient()
mjolnirClient := clients.NewMjolnirClient(httpClient.HttpClient, logger,
viper.GetString("mjolnir.service.url"), viper.GetString("mjolnir.realm.id"),
)
sv := app.NewServer(r, logger, db, mjolnirClient)
sv.Handler()
sv.Start()

View File

@@ -1,6 +1,6 @@
env=ENV
port=PORT
metrics.port=METRIC_POST
metrics.port=METRICS_PORT
#houston token config
houston.slack.app.token=HOUSTON_SLACK_APP_TOKEN
@@ -23,10 +23,22 @@ cron.job.name=CRON_JOB_NAME
incidents.show.limit=INCIDENTS_SHOW_LIMIT
# Prometheus Config
prometheus.app.namePROMETHEUS_APP_NAME
prometheus.host:PROMETHEUS_HOST
prometheus.port:PROMETHEUS_PORT
prometheus.enabled:PROMETHEUS_ENABLED
prometheus.timeout:PROMETHEUS_TIMEOUT
prometheus.flush.interval.in.ms:PROMETHEUS_FLUSH_INTERVAL_IN_MS
prometheus.histogram.buckets:PROMETHEUS_HISTOGRAM_BUCKETS
prometheus.app.name=PROMETHEUS_APP_NAME
prometheus.host=PROMETHEUS_HOST
prometheus.port=PROMETHEUS_PORT
prometheus.enabled=PROMETHEUS_ENABLED
prometheus.timeout=PROMETHEUS_TIMEOUT
prometheus.flush.interval.in.ms=PROMETHEUS_FLUSH_INTERVAL_IN_MS
prometheus.histogram.buckets=PROMETHEUS_HISTOGRAM_BUCKETS
#http
http.max.idle.connection.pool=HTTP_MAX_IDLE_CONNECTION_POOL
http.max.connection=HTTP_MAX_CONNECTION
http.max.timeout.seconds=HTTP_MAX_TIMEOUT_SECONDS
whitelisted.domains=WHITELISTED_DOMAINS
allowed.custom.headers=ALLOWED_CUSTOM_HEADERS
auth.enabled=AUTH_ENABLED
#client
mjolnir.service.url=MJOLNIR_SERVICE_URL
mjolnir.realm.id=MJOLNIR_REALM_ID

View File

@@ -31,6 +31,7 @@ CREATE TABLE team
active boolean DEFAULT false,
confluence_link text,
version bigint default 0,
oncall_handle varchar(50),
created_at timestamp without time zone,
updated_at timestamp without time zone,
deleted_at timestamp without time zone

View File

@@ -0,0 +1,23 @@
package clients
import (
"github.com/spf13/viper"
"net/http"
"time"
)
type HttpClient struct {
HttpClient *http.Client
}
func NewHttpClient() *HttpClient {
return &HttpClient{
HttpClient: &http.Client{
Transport: &http.Transport{
MaxIdleConns: viper.GetInt("http.max.idle.connection.pool"),
MaxConnsPerHost: viper.GetInt("http.max.connection"),
},
Timeout: time.Duration(viper.GetInt("http.max.timeout.seconds")) * time.Second,
},
}
}

View File

@@ -0,0 +1,60 @@
package clients
import (
"encoding/json"
"errors"
"fmt"
"houston/model/clients"
"io"
"net/http"
"go.uber.org/zap"
)
type MjolnirClient struct {
HttpClient *http.Client
Logger *zap.Logger
baseUrl string
realmId string
}
func NewMjolnirClient(httpClient *http.Client, logger *zap.Logger, baseUrl, realmId string) *MjolnirClient {
return &MjolnirClient{
HttpClient: httpClient,
Logger: logger,
baseUrl: baseUrl,
realmId: realmId,
}
}
const (
url = "%s/session/%s"
)
func (m *MjolnirClient) GetSessionResponse(sessionToken string) (*clients.MjolnirSessionResponse, error) {
if sessionToken == "null" {
return nil, errors.New("unauthorized request")
}
client := m.HttpClient
req, _ := http.NewRequest("GET", fmt.Sprintf(url, m.baseUrl, m.realmId), nil)
req.Header.Add("X-Session-Token", sessionToken)
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var response clients.MjolnirSessionResponse
err = json.Unmarshal(responseBody, &response)
if err != nil {
return nil, err
}
m.Logger.Info(fmt.Sprintf("%v", response))
return &response, nil
}

View File

@@ -3,6 +3,7 @@ package action
import (
"fmt"
"houston/model/incident"
"houston/model/tag"
"time"
"github.com/slack-go/slack"
@@ -14,13 +15,15 @@ type ResolveIncidentAction struct {
client *socketmode.Client
logger *zap.Logger
incidentService *incident.Repository
tagService *tag.Repository
}
func NewIncidentResolveProcessor(client *socketmode.Client, logger *zap.Logger, incidentService *incident.Repository) *ResolveIncidentAction {
func NewIncidentResolveProcessor(client *socketmode.Client, logger *zap.Logger, incidentService *incident.Repository, tagService *tag.Repository) *ResolveIncidentAction {
return &ResolveIncidentAction{
client: client,
logger: logger,
incidentService: incidentService,
tagService: tagService,
}
}
@@ -35,32 +38,73 @@ func (irp *ResolveIncidentAction) IncidentResolveProcess(callback slack.Interact
incidentStatusEntity, _ := irp.incidentService.FindIncidentStatusByName(incident.Resolved)
now := time.Now()
incidentEntity.Status = incidentStatusEntity.ID
incidentEntity.EndTime = &now
tags, err := irp.tagService.FindTagsByTeamId(incidentEntity.TeamId)
if err != nil || tags == nil {
irp.logger.Error(fmt.Sprintf("failure while getting tags for incident id: %v", incidentEntity.ID))
return
}
var flag = true
for _, t := range *tags {
if t.Optional == false {
incidentTag, err := irp.incidentService.GetIncidentTagByTagId(incidentEntity.ID, t.Id)
if err != nil {
irp.logger.Error(fmt.Sprintf("failed to get the incident tag for incidentId: %v", incidentEntity.ID))
return
}
if nil == incidentTag {
flag = false
break
}
if t.Type == "free_text" {
if incidentTag.FreeTextValue == nil || len(*incidentTag.FreeTextValue) == 0 {
flag = false
break
}
err = irp.incidentService.UpdateIncident(incidentEntity)
if err != nil {
irp.logger.Error("failed to update incident to resolve state",
} else {
if incidentTag.TagValueIds == nil || len(incidentTag.TagValueIds) == 0 {
flag = false
break
}
}
}
}
if flag == true {
now := time.Now()
incidentEntity.Status = incidentStatusEntity.ID
incidentEntity.EndTime = &now
err = irp.incidentService.UpdateIncident(incidentEntity)
if err != nil {
irp.logger.Error("failed to update incident to resolve state",
zap.String("channel", channelId),
zap.String("user_id", callback.User.ID), zap.Error(err))
return
}
irp.logger.Info("successfully resolved the incident",
zap.String("channel", channelId),
zap.String("user_id", callback.User.ID), zap.Error(err))
return
zap.String("user_id", callback.User.ID))
msgOption := slack.MsgOptionText(fmt.Sprintf("<@%s> *>* `houston set status to %s`", callback.User.ID,
incident.Resolved), false)
_, _, errMessage := irp.client.PostMessage(callback.Channel.ID, msgOption)
if errMessage != nil {
irp.logger.Error("post response failed for ResolveIncident", zap.Error(errMessage))
return
}
irp.client.ArchiveConversation(channelId)
} else {
msgOption := slack.MsgOptionText(fmt.Sprintf("`Please set tag value`"), false)
_, errMessage := irp.client.PostEphemeral(callback.Channel.ID, callback.User.ID, msgOption)
if errMessage != nil {
irp.logger.Error("post response failed for ResolveIncident", zap.Error(errMessage))
return
}
}
irp.logger.Info("successfully resolved the incident",
zap.String("channel", channelId),
zap.String("user_id", callback.User.ID))
msgOption := slack.MsgOptionText(fmt.Sprintf("<@%s> *>* `houston set status to %s`", callback.User.ID,
incident.Resolved), false)
_, _, errMessage := irp.client.PostMessage(callback.Channel.ID, msgOption)
if errMessage != nil {
irp.logger.Error("post response failed for ResolveIncident", zap.Error(errMessage))
return
}
irp.client.ArchiveConversation(channelId)
var payload interface{}
irp.client.Ack(*request, payload)
}

View File

@@ -4,6 +4,7 @@ import (
"houston/common/util"
"houston/internal/processor/action/view"
"houston/model/incident"
"houston/model/tag"
"strconv"
"time"
@@ -16,13 +17,15 @@ type UpdateIncidentAction struct {
client *socketmode.Client
logger *zap.Logger
incidentService *incident.Repository
tagService *tag.Repository
}
func NewIncidentUpdateAction(client *socketmode.Client, logger *zap.Logger, incidentService *incident.Repository) *UpdateIncidentAction {
func NewIncidentUpdateAction(client *socketmode.Client, logger *zap.Logger, incidentService *incident.Repository, tagService *tag.Repository) *UpdateIncidentAction {
return &UpdateIncidentAction{
client: client,
logger: logger,
incidentService: incidentService,
tagService: tagService,
}
}
@@ -74,11 +77,58 @@ func (isp *UpdateIncidentAction) IncidentUpdateStatus(callback slack.Interaction
return
}
tags, err := isp.tagService.FindTagsByTeamId(incidentEntity.TeamId)
if err != nil || tags == nil {
isp.logger.Error(fmt.Sprintf("failure while getting tags for incident id: %v", incidentEntity.ID))
return
}
var flag = true
for _, t := range *tags {
if t.Optional == false {
incidentTag, err := isp.incidentService.GetIncidentTagByTagId(incidentEntity.ID, t.Id)
if err != nil {
isp.logger.Error(fmt.Sprintf("failed to get the incident tag for incidentId: %v", incidentEntity.ID))
return
}
if nil == incidentTag {
flag = false
break
}
if t.Type == "free_text" {
if incidentTag.FreeTextValue == nil || len(*incidentTag.FreeTextValue) == 0 {
flag = false
break
}
} else {
if incidentTag.TagValueIds == nil || len(incidentTag.TagValueIds) == 0 {
flag = false
break
}
}
}
}
incidentEntity.Status = result.ID
incidentEntity.UpdatedBy = user.ID
if result.IsTerminalStatus {
now := time.Now()
incidentEntity.EndTime = &now
if flag == false {
msgOption := slack.MsgOptionText(fmt.Sprintf("`Please set tag value`"), false)
_, errMessage := isp.client.PostEphemeral(callback.View.PrivateMetadata, callback.User.ID, msgOption)
if errMessage != nil {
isp.logger.Error("post response failed for ResolveIncident", zap.Error(errMessage))
return
}
var payload interface{}
isp.client.Ack(*request, payload)
return
} else {
now := time.Now()
incidentEntity.EndTime = &now
}
}
err = isp.incidentService.UpdateIncident(incidentEntity)
if err != nil {

View File

@@ -112,7 +112,7 @@ func (cip *CreateIncidentAction) InviteOnCallPersonToIncident(channelId, ts stri
}
func (cip *CreateIncidentAction) createSlackChannel(incidentEntity *incident.IncidentEntity) (*string, error) {
channelName := fmt.Sprintf("houston-%d", incidentEntity.ID)
channelName := fmt.Sprintf("_houston-%d", incidentEntity.ID)
channelID, err := cip.slackbotClient.CreateChannel(channelName)
if err != nil {
return nil, err

View File

@@ -44,8 +44,8 @@ func NewBlockActionProcessor(logger *zap.Logger, socketModeClient *socketmode.Cl
startIncidentBlockAction: action.NewStartIncidentBlockAction(socketModeClient, logger, teamService, severityService),
showIncidentsAction: action.ShowIncidentsProcessor(socketModeClient, logger, incidentService),
assignIncidentAction: action.NewAssignIncidentAction(socketModeClient, logger, incidentService),
incidentResolveAction: action.NewIncidentResolveProcessor(socketModeClient, logger, incidentService),
incidentUpdateAction: action.NewIncidentUpdateAction(socketModeClient, logger, incidentService),
incidentResolveAction: action.NewIncidentResolveProcessor(socketModeClient, logger, incidentService, tagService),
incidentUpdateAction: action.NewIncidentUpdateAction(socketModeClient, logger, incidentService, tagService),
incidentUpdateTypeAction: action.NewIncidentUpdateTypeAction(socketModeClient, logger, incidentService, teamService, severityService, slackbotClient),
incidentUpdateSeverityAction: action.NewIncidentUpdateSeverityAction(socketModeClient, logger, incidentService, severityService, teamService, slackbotClient),
incidentUpdateTitleAction: action.NewIncidentUpdateTitleAction(socketModeClient, logger, incidentService),
@@ -176,7 +176,7 @@ func NewViewSubmissionProcessor(logger *zap.Logger, socketModeClient *socketmode
incidentChannelMessageUpdateAction: action.NewIncidentChannelMessageUpdateAction(socketModeClient, logger, incidentService, teamService, severityService),
createIncidentAction: action.NewCreateIncidentProcessor(socketModeClient, logger, incidentService, teamService, severityService, slackbotClient),
assignIncidentAction: action.NewAssignIncidentAction(socketModeClient, logger, incidentService),
updateIncidentAction: action.NewIncidentUpdateAction(socketModeClient, logger, incidentService),
updateIncidentAction: action.NewIncidentUpdateAction(socketModeClient, logger, incidentService, tagService),
incidentUpdateTitleAction: action.NewIncidentUpdateTitleAction(socketModeClient, logger, incidentService),
incidentUpdateDescriptionAction: action.NewIncidentUpdateDescriptionAction(socketModeClient, logger, incidentService),
incidentUpdateSeverityAction: action.NewIncidentUpdateSeverityAction(socketModeClient, logger, incidentService, severityService, teamService, slackbotClient),

View File

@@ -0,0 +1,21 @@
package clients
type MjolnirError struct {
Code string `json:"code,omitempty"`
Message string `json:"message,omitempty"`
Params interface{} `json:"params,omitempty"`
}
type MjolnirSessionResponse struct {
SessionToken string `json:"sessionToken,omitempty"`
ClientId string `json:"clientId,omitempty"`
EmailId string `json:"emailId,omitempty"`
AccountId string `json:"accountId,omitempty"`
PhoneNumber string `json:"phoneNumber,omitempty"`
PreferredUsername string `json:"preferred_username,omitempty"`
Roles []string `json:"roles,omitempty"`
Groups []string `json:"groups,omitempty"`
Permissions []string `json:"permissions,omitempty"`
StatusCode int `json:"statusCode,omitempty"`
Errors []MjolnirError `json:"errors"`
}