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 &notifier
    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  }