sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/crier/reporters/slack/reporter.go (about)

     1  /*
     2  Copyright 2019 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package slack
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"errors"
    23  	"fmt"
    24  	"text/template"
    25  
    26  	"github.com/sirupsen/logrus"
    27  	"sigs.k8s.io/controller-runtime/pkg/reconcile"
    28  
    29  	prowapi "sigs.k8s.io/prow/pkg/apis/prowjobs/v1"
    30  	"sigs.k8s.io/prow/pkg/config"
    31  	slackclient "sigs.k8s.io/prow/pkg/slack"
    32  )
    33  
    34  const (
    35  	reporterName    = "slackreporter"
    36  	DefaultHostName = "*"
    37  )
    38  
    39  type slackClient interface {
    40  	WriteMessage(text, channel string) error
    41  }
    42  
    43  type slackReporter struct {
    44  	clients map[string]slackClient
    45  	config  func(*prowapi.Refs) config.SlackReporter
    46  	dryRun  bool
    47  }
    48  
    49  func hostAndChannel(cfg *prowapi.SlackReporterConfig) (string, string) {
    50  	host, channel := cfg.Host, cfg.Channel
    51  	if host == "" {
    52  		host = DefaultHostName
    53  	}
    54  	return host, channel
    55  }
    56  
    57  func (sr *slackReporter) getConfig(pj *prowapi.ProwJob) (*config.SlackReporter, *prowapi.SlackReporterConfig) {
    58  	refs := pj.Spec.Refs
    59  	if refs == nil && len(pj.Spec.ExtraRefs) > 0 {
    60  		refs = &pj.Spec.ExtraRefs[0]
    61  	}
    62  	globalConfig := sr.config(refs)
    63  	var jobSlackConfig *prowapi.SlackReporterConfig
    64  	if pj.Spec.ReporterConfig != nil && pj.Spec.ReporterConfig.Slack != nil {
    65  		jobSlackConfig = pj.Spec.ReporterConfig.Slack
    66  	}
    67  	return &globalConfig, jobSlackConfig
    68  }
    69  
    70  func (sr *slackReporter) Report(_ context.Context, log *logrus.Entry, pj *prowapi.ProwJob) ([]*prowapi.ProwJob, *reconcile.Result, error) {
    71  	return []*prowapi.ProwJob{pj}, nil, sr.report(log, pj)
    72  }
    73  
    74  func (sr *slackReporter) report(log *logrus.Entry, pj *prowapi.ProwJob) error {
    75  	globalSlackConfig, jobSlackConfig := sr.getConfig(pj)
    76  	if globalSlackConfig != nil {
    77  		jobSlackConfig = jobSlackConfig.ApplyDefault(&globalSlackConfig.SlackReporterConfig)
    78  	}
    79  	if jobSlackConfig == nil {
    80  		return errors.New("resolved slack config is empty") // Shouldn't happen at all, just in case
    81  	}
    82  	host, channel := hostAndChannel(jobSlackConfig)
    83  
    84  	client, ok := sr.clients[host]
    85  	if !ok {
    86  		return fmt.Errorf("host '%s' not supported", host)
    87  	}
    88  	b := &bytes.Buffer{}
    89  	tmpl, err := template.New("").Parse(jobSlackConfig.ReportTemplate)
    90  	if err != nil {
    91  		log.WithError(err).Error("failed to parse template")
    92  		return fmt.Errorf("failed to parse template: %w", err)
    93  	}
    94  	if err := tmpl.Execute(b, pj); err != nil {
    95  		log.WithError(err).Error("failed to execute report template")
    96  		return fmt.Errorf("failed to execute report template: %w", err)
    97  	}
    98  	if sr.dryRun {
    99  		log.WithField("messagetext", b.String()).Debug("Skipping reporting because dry-run is enabled")
   100  		return nil
   101  	}
   102  	if err := client.WriteMessage(b.String(), channel); err != nil {
   103  		log.WithError(err).Error("failed to write Slack message")
   104  		return fmt.Errorf("failed to write Slack message: %w", err)
   105  	}
   106  	return nil
   107  }
   108  
   109  func (sr *slackReporter) GetName() string {
   110  	return reporterName
   111  }
   112  
   113  func (sr *slackReporter) ShouldReport(_ context.Context, logger *logrus.Entry, pj *prowapi.ProwJob) bool {
   114  	globalSlackConfig, jobSlackConfig := sr.getConfig(pj)
   115  
   116  	var typeShouldReport bool
   117  	if globalSlackConfig.JobTypesToReport != nil {
   118  		for _, tp := range globalSlackConfig.JobTypesToReport {
   119  			if tp == pj.Spec.Type {
   120  				typeShouldReport = true
   121  				break
   122  			}
   123  		}
   124  	}
   125  
   126  	// If a user specifically put a channel on their job, they want
   127  	// it to be reported regardless of the job types setting.
   128  	var jobShouldReport bool
   129  	if jobSlackConfig != nil && jobSlackConfig.Channel != "" {
   130  		jobShouldReport = true
   131  	}
   132  
   133  	// The job should only be reported if its state has a match with the
   134  	// JobStatesToReport config.
   135  	// Note the JobStatesToReport configured in the Prow job can overwrite the
   136  	// Prow config.
   137  	var stateShouldReport bool
   138  	if merged := jobSlackConfig.ApplyDefault(&globalSlackConfig.SlackReporterConfig); merged != nil && merged.JobStatesToReport != nil {
   139  		if merged.Report != nil && !*merged.Report {
   140  			logger.WithField("job_states_to_report", merged.JobStatesToReport).Debug("Skip slack reporting as 'report: false', could result from 'job_states_to_report: []'.")
   141  			return false
   142  		}
   143  		for _, stateToReport := range merged.JobStatesToReport {
   144  			if pj.Status.State == stateToReport {
   145  				stateShouldReport = true
   146  				break
   147  			}
   148  		}
   149  	}
   150  
   151  	shouldReport := stateShouldReport && (typeShouldReport || jobShouldReport)
   152  	logger.WithField("reporting", shouldReport).Debug("Determined should report")
   153  	return shouldReport
   154  }
   155  
   156  func New(cfg func(refs *prowapi.Refs) config.SlackReporter, dryRun bool, tokensMap map[string]func() []byte) *slackReporter {
   157  	clients := map[string]slackClient{}
   158  	for key, val := range tokensMap {
   159  		clients[key] = slackclient.NewClient(val)
   160  	}
   161  	return &slackReporter{
   162  		clients: clients,
   163  		config:  cfg,
   164  		dryRun:  dryRun,
   165  	}
   166  }