Skip to content

Commit

Permalink
When generating version file, enable matching non-annotated tags (#28)
Browse files Browse the repository at this point in the history
severity and impact
  • Loading branch information
karl-johan-grahn authored Dec 23, 2021
1 parent 2b35036 commit 238d31d
Show file tree
Hide file tree
Showing 8 changed files with 145 additions and 33 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.13.0] - 2021-12-22
### Updates
- When generating version file, enable matching non-annotated tags
- Slack does not yet allow users to create reminders recurring more often than once a day, so just create one that runs daily 30 min after the incident has been declared
- Include year in Slack channel name to decrease chance of having name creation conflicts and to make the name more explicit
- Describe incidents according to severity and impact

## [0.12.0] - 2021-11-29
### Adds
- Add broadcast channel as input option, add dispatch action when characters are entered, update to Slack Go API v0.10.0
Expand Down
78 changes: 59 additions & 19 deletions bot/bot.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ type Opts struct {
IncidentEnvs string
// IncidentRegions - the regions that could possibly be affected
IncidentRegions string
// IncidentSeverityLevels - the possible severity levels of an incident
IncidentSeverityLevels string
// IncidentImpactLevels - the possible impact levels of an incident
IncidentImpactLevels string
// Localizer - the localizer to use for the set of language preferences
Localizer *i18n.Localizer
}
Expand Down Expand Up @@ -171,7 +175,6 @@ func (h *botHandler) cmdIncident(ctx context.Context, w http.ResponseWriter, cmd
DefaultMessage: &i18n.Message{
ID: "IncidentCreationDescription",
Other: "This will create a new incident Slack channel, and notify about the incident in a broadcast channel. This incident response system is based on the Incident Command System."},
TemplateData: map[string]string{"broadcastChannel": fmt.Sprintf("<#%s>", h.opts.BroadcastChannelID)},
}), false, false)
contextBlock := slack.NewContextBlock("context", contextText)

Expand Down Expand Up @@ -201,14 +204,13 @@ func (h *botHandler) cmdIncident(ctx context.Context, w http.ResponseWriter, cmd
}
channelIDs := []string{}
for i := range channels {
channelIDs = append(channelIDs, fmt.Sprintf("<#%s>", channels[i].ID))
channelIDs = append(channelIDs, channels[i].ID)
}
botChannels := createOptionBlockObjects(channelIDs)
botChannels := createOptionBlockObjects(channelIDs, "channel")
broadcastChOption := slack.NewOptionsSelectBlockElement(slack.OptTypeStatic, nil, "broadcast_channel", botChannels...)
initialChannel := fmt.Sprintf("<#%s>", h.opts.BroadcastChannelID)
initialChannelLabel := slack.NewTextBlockObject(slack.PlainTextType, initialChannel, false, false)
initialChannelOptionBlockObject := slack.NewOptionBlockObject(initialChannel, initialChannelLabel, nil)
broadcastChOption.InitialOption = initialChannelOptionBlockObject
initialChannelLabel := slack.NewTextBlockObject(slack.PlainTextType,
fmt.Sprintf("<#%s>", h.opts.BroadcastChannelID), false, false)
broadcastChOption.InitialOption = slack.NewOptionBlockObject(h.opts.BroadcastChannelID, initialChannelLabel, nil)
broadcastChBlock := slack.NewInputBlock("broadcast_channel", broadcastChLabel, broadcastChOption)
broadcastChBlock.Hint = slack.NewTextBlockObject(slack.PlainTextType,
h.opts.Localizer.MustLocalize(&i18n.LocalizeConfig{
Expand Down Expand Up @@ -298,7 +300,7 @@ func (h *botHandler) cmdIncident(ctx context.Context, w http.ResponseWriter, cmd
w.WriteHeader(http.StatusInternalServerError)
return err
}
envOptions := createOptionBlockObjects(envs)
envOptions := createOptionBlockObjects(envs, "")
envOptionsBlock := slack.NewCheckboxGroupsBlockElement("incident_environment_affected", envOptions...)
environmentBlock := slack.NewInputBlock("incident_environment_affected", envTxt, envOptionsBlock)

Expand All @@ -314,10 +316,42 @@ func (h *botHandler) cmdIncident(ctx context.Context, w http.ResponseWriter, cmd
w.WriteHeader(http.StatusInternalServerError)
return err
}
regionOptions := createOptionBlockObjects(regions)
regionOptions := createOptionBlockObjects(regions, "")
regionOptionsBlock := slack.NewCheckboxGroupsBlockElement("incident_region_affected", regionOptions...)
regionBlock := slack.NewInputBlock("incident_region_affected", regionTxt, regionOptionsBlock)

severityTxt := slack.NewTextBlockObject(slack.PlainTextType,
h.opts.Localizer.MustLocalize(&i18n.LocalizeConfig{
DefaultMessage: &i18n.Message{
ID: "Severity",
Other: "Severity"},
}), false, false)
var severityLevels []string
if err := json.Unmarshal([]byte(h.opts.IncidentSeverityLevels), &severityLevels); err != nil {
log.Error().Err(err).Msg("Failed to unmarshal incident severity levels")
w.WriteHeader(http.StatusInternalServerError)
return err
}
severityOptions := createOptionBlockObjects(severityLevels, "")
severityOptionsBlock := slack.NewRadioButtonsBlockElement("incident_severity_level", severityOptions...)
severityBlock := slack.NewInputBlock("incident_severity_level", severityTxt, severityOptionsBlock)

impactTxt := slack.NewTextBlockObject(slack.PlainTextType,
h.opts.Localizer.MustLocalize(&i18n.LocalizeConfig{
DefaultMessage: &i18n.Message{
ID: "Impact",
Other: "Impact"},
}), false, false)
var impactLevels []string
if err := json.Unmarshal([]byte(h.opts.IncidentImpactLevels), &impactLevels); err != nil {
log.Error().Err(err).Msg("Failed to unmarshal incident impact levels")
w.WriteHeader(http.StatusInternalServerError)
return err
}
impactOptions := createOptionBlockObjects(impactLevels, "")
impactOptionsBlock := slack.NewRadioButtonsBlockElement("incident_impact_level", impactOptions...)
impactBlock := slack.NewInputBlock("incident_impact_level", impactTxt, impactOptionsBlock)

summaryText := slack.NewTextBlockObject(slack.PlainTextType,
h.opts.Localizer.MustLocalize(&i18n.LocalizeConfig{
DefaultMessage: &i18n.Message{
Expand Down Expand Up @@ -350,6 +384,8 @@ func (h *botHandler) cmdIncident(ctx context.Context, w http.ResponseWriter, cmd
commanderBlock,
environmentBlock,
regionBlock,
severityBlock,
impactBlock,
summaryBlock,
inviteeBlock,
},
Expand Down Expand Up @@ -403,7 +439,6 @@ func (h *botHandler) cmdResolveIncident(ctx context.Context, w http.ResponseWrit
DefaultMessage: &i18n.Message{
ID: "ResolveIncidentDescription",
Other: "This will resolve an incident and notify about the resolution in a broadcast channel"},
TemplateData: map[string]string{"broadcastChannel": fmt.Sprintf("<#%s>", h.opts.BroadcastChannelID)},
}), false, false)
contextBlock := slack.NewContextBlock("context", contextText)

Expand Down Expand Up @@ -433,14 +468,13 @@ func (h *botHandler) cmdResolveIncident(ctx context.Context, w http.ResponseWrit
}
channelIDs := []string{}
for i := range channels {
channelIDs = append(channelIDs, fmt.Sprintf("<#%s>", channels[i].ID))
channelIDs = append(channelIDs, channels[i].ID)
}
botChannels := createOptionBlockObjects(channelIDs)
botChannels := createOptionBlockObjects(channelIDs, "channel")
broadcastChOption := slack.NewOptionsSelectBlockElement(slack.OptTypeStatic, nil, "broadcast_channel", botChannels...)
initialChannel := fmt.Sprintf("<#%s>", h.opts.BroadcastChannelID)
initialChannelLabel := slack.NewTextBlockObject(slack.PlainTextType, initialChannel, false, false)
initialChannelOptionBlockObject := slack.NewOptionBlockObject(initialChannel, initialChannelLabel, nil)
broadcastChOption.InitialOption = initialChannelOptionBlockObject
initialChannelLabel := slack.NewTextBlockObject(slack.PlainTextType,
fmt.Sprintf("<#%s>", h.opts.BroadcastChannelID), false, false)
broadcastChOption.InitialOption = slack.NewOptionBlockObject(h.opts.BroadcastChannelID, initialChannelLabel, nil)
broadcastChBlock := slack.NewInputBlock("broadcast_channel", broadcastChLabel, broadcastChOption)
broadcastChBlock.Hint = slack.NewTextBlockObject(slack.PlainTextType,
h.opts.Localizer.MustLocalize(&i18n.LocalizeConfig{
Expand Down Expand Up @@ -487,7 +521,7 @@ func (h *botHandler) cmdResolveIncident(ctx context.Context, w http.ResponseWrit
DefaultMessage: &i18n.Message{
ID: "No",
Other: "No"},
})})
})}, "")
archiveOptionsBlock := slack.NewRadioButtonsBlockElement("archive_choice", archiveOptions...)
archiveBlock := slack.NewInputBlock("archive_choice", archiveTxt, archiveOptionsBlock)

Expand Down Expand Up @@ -533,10 +567,16 @@ func (h *botHandler) cmdResolveIncident(ctx context.Context, w http.ResponseWrit
}

// createOptionBlockObjects - utility function for generating option block objects
func createOptionBlockObjects(options []string) []*slack.OptionBlockObject {
func createOptionBlockObjects(options []string, optionType string) []*slack.OptionBlockObject {
optionBlockObjects := make([]*slack.OptionBlockObject, 0, len(options))
var text string
for _, o := range options {
optionText := slack.NewTextBlockObject(slack.PlainTextType, o, false, false)
if optionType == "channel" {
text = fmt.Sprintf("<#%s>", o)
} else {
text = o
}
optionText := slack.NewTextBlockObject(slack.PlainTextType, text, false, false)
optionBlockObjects = append(optionBlockObjects, slack.NewOptionBlockObject(o, optionText, nil))
}
return optionBlockObjects
Expand Down
17 changes: 15 additions & 2 deletions bot/bot_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,11 @@ func TestHandleCommand(t *testing.T) {

func TestCreateOptionBlockObjects(t *testing.T) {
options := []string{}
optionBlockObjects := createOptionBlockObjects(options)
optionBlockObjects := createOptionBlockObjects(options, "")
assert.Empty(t, optionBlockObjects)

options = []string{"a", "b"}
optionBlockObjects = createOptionBlockObjects(options)
optionBlockObjects = createOptionBlockObjects(options, "")
assert.Equal(t, []*slack.OptionBlockObject{
(&slack.OptionBlockObject{
Text: &slack.TextBlockObject{Type: "plain_text", Text: "a", Emoji: false, Verbatim: false},
Expand All @@ -141,6 +141,19 @@ func TestCreateOptionBlockObjects(t *testing.T) {
Value: "b",
URL: ""}),
}, optionBlockObjects)

options = []string{"ID1", "ID2"}
optionBlockObjects = createOptionBlockObjects(options, "channel")
assert.Equal(t, []*slack.OptionBlockObject{
(&slack.OptionBlockObject{
Text: &slack.TextBlockObject{Type: "plain_text", Text: "<#ID1>", Emoji: false, Verbatim: false},
Value: "ID1",
URL: ""}),
(&slack.OptionBlockObject{
Text: &slack.TextBlockObject{Type: "plain_text", Text: "<#ID2>", Emoji: false, Verbatim: false},
Value: "ID2",
URL: ""}),
}, optionBlockObjects)
}

type dummyClient struct {
Expand Down
42 changes: 32 additions & 10 deletions bot/interactive.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ type inputParams struct {
incidentInvitees []string
incidentEnvironmentsAffected string
incidentRegionsAffected string
IncidentSeverityLevel string
IncidentImpactLevel string
incidentSummary string
incidentDeclarer string
broadcastChannel string
Expand Down Expand Up @@ -194,15 +196,21 @@ func (h *botHandler) declareIncident(ctx context.Context, payload *slack.Interac
for i, r := range payload.View.State.Values["incident_region_affected"]["incident_region_affected"].SelectedOptions {
incidentRegionsAffected[i] = r.Value
}
incidentSecurityRelated := false
if len(payload.View.State.Values["security_incident"]["security_incident"].SelectedOptions) > 0 {
incidentSecurityRelated = payload.View.State.Values["security_incident"]["security_incident"].SelectedOptions[0].Value == "yes"
}
inputParams := &inputParams{
broadcastChannel: payload.View.State.Values["broadcast_channel"]["broadcast_channel"].SelectedOption.Value,
incidentChannelName: incidentChannelName,
incidentSecurityRelated: payload.View.State.Values["security_incident"]["security_incident"].SelectedOption.Value == "yes",
incidentSecurityRelated: incidentSecurityRelated,
incidentResponder: payload.View.State.Values["incident_responder"]["incident_responder"].SelectedUser,
incidentCommander: payload.View.State.Values["incident_commander"]["incident_commander"].SelectedUser,
incidentInvitees: payload.View.State.Values["incident_invitees"]["incident_invitees"].SelectedUsers,
incidentEnvironmentsAffected: strings.Join(incidentEnvironmentsAffected, ", "),
incidentRegionsAffected: strings.Join(incidentRegionsAffected, ", "),
IncidentSeverityLevel: payload.View.State.Values["incident_severity_level"]["incident_severity_level"].SelectedOption.Value,
IncidentImpactLevel: payload.View.State.Values["incident_impact_level"]["incident_impact_level"].SelectedOption.Value,
incidentSummary: payload.View.State.Values["incident_summary"]["incident_summary"].Value,
incidentDeclarer: payload.User.ID,
}
Expand Down Expand Up @@ -237,17 +245,18 @@ func (h *botHandler) doIncidentTasks(ctx context.Context, params *inputParams, i
} else {
securityMessage = ""
}
// Set channel purpose and topic
overview := fmt.Sprintf("*Incident channel*\n"+
"*Incident summary:* %s\n"+
"*Environment affected:* %s\n"+
// Set channel purpose and topic - they can be maximum 250 characters
overview := fmt.Sprintf("*Environment affected:* %s\n"+
"*Region affected:* %s\n"+
"*Severity:* %s\n"+
"*Impact:* %s\n"+
"*Responder:* <@%s>\n"+
"*Commander:* <@%s>\n"+
"*Broadcast channel:* <#%s>\n\n"+
"Declared by: <@%s>\n"+
securityMessage,
params.incidentSummary, params.incidentEnvironmentsAffected, params.incidentRegionsAffected,
params.incidentEnvironmentsAffected, params.incidentRegionsAffected,
params.IncidentSeverityLevel, params.IncidentImpactLevel,
params.incidentResponder, params.incidentCommander, params.broadcastChannel, params.incidentDeclarer)
if _, err := h.slackClient.SetPurposeOfConversationContext(ctx, incidentChannel.ID, overview); err != nil {
if sendErr := h.sendMessage(ctx, params.broadcastChannel, slack.MsgOptionPostEphemeral(params.incidentDeclarer),
Expand Down Expand Up @@ -280,12 +289,15 @@ func (h *botHandler) doIncidentTasks(ctx context.Context, params *inputParams, i
"*Incident summary:* %s\n"+
"*Environment affected:* %s\n"+
"*Region affected:* %s\n"+
"*Severity:* %s\n"+
"*Impact:* %s\n"+
"*Responder:* <@%s>\n"+
"*Commander:* <@%s>\n"+
"*Incident channel:* <#%s>\n"+
securityMessage,
params.incidentDeclarer, params.incidentSummary, params.incidentEnvironmentsAffected,
params.incidentRegionsAffected, params.incidentResponder, params.incidentCommander,
params.incidentRegionsAffected, params.IncidentSeverityLevel, params.IncidentImpactLevel,
params.incidentResponder, params.incidentCommander,
incidentChannel.ID), false)); err != nil {
log.Error().Err(err).Msg(sendError)
return
Expand All @@ -305,9 +317,19 @@ func (h *botHandler) doIncidentTasks(ctx context.Context, params *inputParams, i
// Add channel reminder about updating progress
// Need to use user access token since bot token is not allowed token type: https://api.slack.com/methods/reminders.add
userSlackClient := slack.New(h.opts.UserAccessToken)
user, err := h.slackClient.GetUserInfoContext(ctx, params.incidentDeclarer)
if err != nil {
if sendErr := h.sendMessage(ctx, incidentChannel.ID, slack.MsgOptionPostEphemeral(params.incidentDeclarer),
slack.MsgOptionText(fmt.Sprintf("Failed to get user info context: %s", err.Error()), false)); sendErr != nil {
log.Error().Err(sendErr).Msg(sendError)
return
}
}
loc := time.FixedZone("CUSTOM-TZ", user.TZOffset)
now := time.Now().In(loc)
if _, err := userSlackClient.AddChannelReminder(incidentChannel.ID,
fmt.Sprintf("Reminder for IC <@%s>: Update progress about the incident in <#%s>", params.incidentCommander, params.broadcastChannel),
"Every 30 min"); err != nil {
fmt.Sprintf("\"Reminder for IC <@%s>: Update progress about the incident every 30 min in <#%s>, or remove the reminder and archive the channel if the incident is resolved\"", params.incidentCommander, params.broadcastChannel),
fmt.Sprintf("every day at %s", now.Add(time.Minute*time.Duration(30)).Format("03:04:05PM"))); err != nil {
if sendErr := h.sendMessage(ctx, incidentChannel.ID, slack.MsgOptionPostEphemeral(params.incidentDeclarer),
slack.MsgOptionText(fmt.Sprintf("Failed to add channel reminder: %s", err.Error()), false)); sendErr != nil {
log.Error().Err(sendErr).Msg(sendError)
Expand Down Expand Up @@ -409,7 +431,7 @@ func postErrorResponse(ctx context.Context, verr map[string]string, w http.Respo

// createChannelName - create incident channel name based on input field
func createChannelName(s string) string {
return fmt.Sprintf("inc_%s_%s", s, strings.ToLower(time.Now().Format("Jan2")))
return fmt.Sprintf("inc_%s_%s", s, strings.ToLower(time.Now().Format("2Jan2006")))
}

// createUserFriendlyConversationError - Map https://api.slack.com/methods/conversations.create error codes to user friendly messages
Expand Down
4 changes: 4 additions & 0 deletions cmd/devopsbot/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ func initConfig(cmd *cobra.Command) func() {
_ = viper.BindEnv("incidentDocTemplateURL", "incidentDocTemplateURL")
_ = viper.BindEnv("incident.environments", "incident.environments")
_ = viper.BindEnv("incident.regions", "incident.regions")
_ = viper.BindEnv("incident.severityLevels", "incident.severityLevels")
_ = viper.BindEnv("incident.impactLevels", "incident.impactLevels")
_ = viper.BindEnv("addr", "addr")
_ = viper.BindEnv(tlsAddr, tlsAddr)
_ = viper.BindEnv(tlsCert, tlsCert)
Expand Down Expand Up @@ -165,6 +167,8 @@ func newCmd() *cobra.Command {
IncidentDocTemplateURL: cfg.IncidentDocTemplateURL,
IncidentEnvs: cfg.IncidentEnvs,
IncidentRegions: cfg.IncidentRegions,
IncidentSeverityLevels: cfg.IncidentSeverityLevels,
IncidentImpactLevels: cfg.IncidentImpactLevels,
}
log.Debug().Msgf("opts: %#v", opts)

Expand Down
4 changes: 4 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ type Config struct {

IncidentEnvs string
IncidentRegions string
IncidentSeverityLevels string
IncidentImpactLevels string
IncidentDocTemplateURL string
}

Expand All @@ -42,6 +44,8 @@ func FromViper(v *viper.Viper) (Config, error) {

c.IncidentEnvs = v.GetString("incident.environments")
c.IncidentRegions = v.GetString("incident.regions")
c.IncidentSeverityLevels = v.GetString("incident.severityLevels")
c.IncidentImpactLevels = v.GetString("incident.impactLevels")
c.IncidentDocTemplateURL = v.GetString("incidentDocTemplateURL")

return c, nil
Expand Down
22 changes: 22 additions & 0 deletions docs/src/GETTING_STARTED.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,18 @@ data:
"eu-west-1",
"us-east-1"
]
incident.severityLevels: |-
[
"high",
"medium",
"low"
]
incident.impactLevels: |-
[
"high",
"medium",
"low"
]
server.prometheusNamespace: devopsbot
tls.addr: :3443
tls.cert: /var/devopsbot/tls.crt
Expand Down Expand Up @@ -194,6 +206,16 @@ spec:
configMapKeyRef:
key: incident.regions
name: devopsbot-settings
- name: incident.severityLevels
valueFrom:
configMapKeyRef:
key: incident.severityLevels
name: devopsbot-settings
- name: incident.impactLevels
valueFrom:
configMapKeyRef:
key: incident.impactLevels
name: devopsbot-settings
- name: addr
valueFrom:
configMapKeyRef:
Expand Down
4 changes: 2 additions & 2 deletions gen_version.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/bin/bash
if (git describe --abbrev=0 --exact-match &>/dev/null); then
if (git describe --abbrev=0 --tags --exact-match &>/dev/null); then
# If we are on a tagged commit, use that tag as version
git describe --abbrev=0 --exact-match | sed 's/v\(.*\)/\1/'
git describe --abbrev=0 --tags --exact-match | sed 's/v\(.*\)/\1/'
else
# Otherwise get the latest tagged commit
tag=$(git rev-list --tags --max-count=1 2>/dev/null)
Expand Down

0 comments on commit 238d31d

Please sign in to comment.