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 }