github.com/kubeshop/testkube@v1.17.23/pkg/slack/slack.go (about) 1 package slack 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "os" 7 8 "github.com/slack-go/slack" 9 10 "github.com/kubeshop/testkube/pkg/api/v1/testkube" 11 "github.com/kubeshop/testkube/pkg/log" 12 "github.com/kubeshop/testkube/pkg/utils" 13 "github.com/kubeshop/testkube/pkg/utils/text" 14 ) 15 16 type MessageArgs struct { 17 ExecutionID string 18 ExecutionName string 19 EventType string 20 Namespace string 21 Labels string 22 TestName string 23 TestType string 24 Status string 25 FailedSteps int 26 TotalSteps int 27 StartTime string 28 EndTime string 29 Duration string 30 ClusterName string 31 DashboardURI string 32 Envs map[string]string 33 } 34 35 type Notifier struct { 36 client *slack.Client 37 timestamps map[string]string 38 Ready bool 39 messageTemplate string 40 clusterName string 41 dashboardURI string 42 config *Config 43 envs map[string]string 44 } 45 46 func NewNotifier(template, clusterName, dashboardURI string, config []NotificationsConfig, envs map[string]string) *Notifier { 47 notifier := Notifier{messageTemplate: template, clusterName: clusterName, dashboardURI: dashboardURI, 48 config: NewConfig(config), envs: envs} 49 notifier.timestamps = make(map[string]string) 50 if token, ok := os.LookupEnv("SLACK_TOKEN"); ok && token != "" { 51 log.DefaultLogger.Infow("initializing slack client", "SLACK_TOKEN", text.Obfuscate(token)) 52 notifier.client = slack.New(token, slack.OptionDebug(true)) 53 notifier.Ready = true 54 } else { 55 log.DefaultLogger.Warn("SLACK_TOKEN is not set") 56 } 57 return ¬ifier 58 } 59 60 // SendMessage posts a message to the slack configured channel 61 func (s *Notifier) SendMessage(channelID string, message string) error { 62 if s.client != nil { 63 _, _, err := s.client.PostMessage(channelID, slack.MsgOptionText(message, false)) 64 if err != nil { 65 log.DefaultLogger.Warnw("error while posting message to channel", "channelID", channelID, "error", err.Error()) 66 return err 67 } 68 } else { 69 log.DefaultLogger.Warnw("slack client is not initialised") 70 } 71 return nil 72 } 73 74 // SendEvent composes an event message and sends it to slack 75 func (s *Notifier) SendEvent(event *testkube.Event) error { 76 77 message, name, err := s.composeMessage(event) 78 if err != nil { 79 return err 80 } 81 82 if s.client != nil { 83 84 log.DefaultLogger.Debugw("sending event to slack", "event", event) 85 channels, err := s.getChannels(event) 86 if err != nil { 87 return err 88 } 89 log.DefaultLogger.Infow("channels to send event to", "channels", channels) 90 91 for _, channelID := range channels { 92 prevTimestamp, ok := s.timestamps[name] 93 var timestamp string 94 95 if ok { 96 _, timestamp, _, err = s.client.UpdateMessage(channelID, prevTimestamp, slack.MsgOptionBlocks(message.Blocks.BlockSet...)) 97 } 98 99 if !ok || err != nil { 100 _, timestamp, err = s.client.PostMessage(channelID, slack.MsgOptionBlocks(message.Blocks.BlockSet...)) 101 } 102 103 if err != nil { 104 log.DefaultLogger.Warnw("error while posting message to channel", 105 "channelID", channelID, 106 "error", err.Error(), 107 "slackMessageOptions", slack.MsgOptionBlocks(message.Blocks.BlockSet...)) 108 return err 109 } 110 111 if event.IsSuccess() { 112 delete(s.timestamps, name) 113 } else { 114 s.timestamps[name] = timestamp 115 } 116 } 117 } else { 118 log.DefaultLogger.Warnw("slack client is not initialised") 119 } 120 121 return nil 122 } 123 124 func (s *Notifier) getChannels(event *testkube.Event) ([]string, error) { 125 result := []string{} 126 if !s.config.HasChannelsDefined() { 127 channels, _, err := s.client.GetConversationsForUser(&slack.GetConversationsForUserParameters{}) 128 if err != nil { 129 log.DefaultLogger.Warnw("error while getting bot channels", "error", err.Error()) 130 return nil, err 131 } 132 _, needsSending := s.config.NeedsSending(event) 133 if len(channels) > 0 && needsSending { 134 result = append(result, channels[0].GroupConversation.ID) 135 return result, nil 136 } 137 } else { 138 channels, needsSending := s.config.NeedsSending(event) 139 if needsSending { 140 return channels, nil 141 } 142 } 143 return nil, nil 144 } 145 146 func (s *Notifier) composeMessage(event *testkube.Event) (view *slack.Message, name string, err error) { 147 var message []byte 148 if event.TestExecution != nil { 149 message, err = s.composeTestMessage(event.TestExecution, event.Type()) 150 name = event.TestExecution.Name 151 } else if event.TestSuiteExecution != nil { 152 message, err = s.composeTestsuiteMessage(event.TestSuiteExecution, event.Type()) 153 name = event.TestSuiteExecution.Name 154 } else { 155 log.DefaultLogger.Warnw("event type is not handled by Slack notifier", "event", event) 156 return nil, "", nil 157 } 158 159 if err != nil { 160 return nil, "", err 161 } 162 view = &slack.Message{} 163 err = json.Unmarshal(message, view) 164 if err != nil { 165 log.DefaultLogger.Warnw("error while creating slack specific message", "error", err.Error(), "message", string(message)) 166 return nil, "", err 167 } 168 169 return view, name, nil 170 } 171 172 func (s *Notifier) composeTestsuiteMessage(execution *testkube.TestSuiteExecution, eventType testkube.EventType) ([]byte, error) { 173 t, err := utils.NewTemplate("message").Parse(s.messageTemplate) 174 if err != nil { 175 log.DefaultLogger.Warnw("error while parsing slack template", "error", err.Error()) 176 return nil, err 177 } 178 179 args := MessageArgs{ 180 ExecutionID: execution.Id, 181 ExecutionName: execution.Name, 182 EventType: string(eventType), 183 Namespace: execution.TestSuite.Namespace, 184 Labels: testkube.MapToString(execution.Labels), 185 TestName: execution.TestSuite.Name, 186 TestType: "Test Suite", 187 Status: string(*execution.Status), 188 StartTime: execution.StartTime.String(), 189 EndTime: execution.EndTime.String(), 190 Duration: execution.Duration, 191 TotalSteps: len(execution.ExecuteStepResults), 192 FailedSteps: execution.FailedStepsCount(), 193 ClusterName: s.clusterName, 194 DashboardURI: s.dashboardURI, 195 Envs: s.envs, 196 } 197 198 log.DefaultLogger.Infow("Execution changed", "status", execution.Status) 199 200 var message bytes.Buffer 201 err = t.Execute(&message, args) 202 if err != nil { 203 log.DefaultLogger.Warnw("error while executing slack template", "error", err.Error(), "template", s.messageTemplate, "args", args) 204 return nil, err 205 } 206 return message.Bytes(), nil 207 } 208 209 func (s *Notifier) composeTestMessage(execution *testkube.Execution, eventType testkube.EventType) ([]byte, error) { 210 t, err := utils.NewTemplate("message").Parse(s.messageTemplate) 211 if err != nil { 212 log.DefaultLogger.Warnw("error while parsing slack template", "error", err.Error(), "template", s.messageTemplate) 213 return nil, err 214 } 215 216 args := MessageArgs{ 217 ExecutionID: execution.Id, 218 ExecutionName: execution.Name, 219 EventType: string(eventType), 220 Namespace: execution.TestNamespace, 221 Labels: testkube.MapToString(execution.Labels), 222 TestName: execution.TestName, 223 TestType: execution.TestType, 224 Status: string(testkube.QUEUED_ExecutionStatus), 225 StartTime: execution.StartTime.String(), 226 EndTime: execution.EndTime.String(), 227 Duration: execution.Duration, 228 TotalSteps: 0, 229 FailedSteps: 0, 230 ClusterName: s.clusterName, 231 DashboardURI: s.dashboardURI, 232 Envs: s.envs, 233 } 234 235 if execution.ExecutionResult != nil { 236 if execution.ExecutionResult.Status != nil { 237 args.Status = string(*execution.ExecutionResult.Status) 238 } 239 args.TotalSteps = len(execution.ExecutionResult.Steps) 240 args.FailedSteps = execution.ExecutionResult.FailedStepsCount() 241 } 242 243 log.DefaultLogger.Infow("Execution changed", "status", execution.ExecutionResult.Status) 244 245 var message bytes.Buffer 246 err = t.Execute(&message, args) 247 if err != nil { 248 log.DefaultLogger.Warnw("error while executing slack template", "error", err.Error(), "template", s.messageTemplate, "args", args) 249 return nil, err 250 } 251 return message.Bytes(), nil 252 }