@@ -2,12 +2,26 @@ package notification
22
33import (
44 "bytes"
5+ "context"
6+ "encoding/json"
7+ "fmt"
58 slackapi "github.com/slack-go/slack"
69 "html/template"
10+ "io"
711 "k8s.io/klog"
12+ "net/http"
13+ "net/url"
814 "os"
15+ "strconv"
16+ "strings"
17+ "time"
918)
1019
20+ // httpDo is a package-level variable to allow tests to stub HTTP requests easily.
21+ var httpDo = func (req * http.Request ) (* http.Response , error ) {
22+ return http .DefaultClient .Do (req )
23+ }
24+
1125const (
1226 START = "start"
1327 SUCCESS = "success"
@@ -38,6 +52,7 @@ var slackColors = map[string]string{
3852
3953type slackClient interface {
4054 PostMessage (channelID string , options ... slackapi.MsgOption ) (string , string , error )
55+ // Deprecated in Slack API: kept for backward compatibility in tests, but no longer used
4156 UploadFile (params slackapi.FileUploadParameters ) (file * slackapi.File , err error )
4257}
4358
@@ -251,20 +266,126 @@ func (s slack) notify(attachment slackapi.Attachment) (err error) {
251266}
252267
253268func (s slack ) uploadLog (param MessageTemplateParam ) (file * slackapi.File , err error ) {
254- file , err = s .client .UploadFile (
255- slackapi.FileUploadParameters {
256- Title : param .Namespace + "_" + param .JobName ,
257- Content : param .Log ,
258- Filetype : "txt" ,
259- Channels : []string {s .channel },
260- })
269+ // External upload flow per Slack deprecation of files.upload
270+ // 1) Call files.getUploadURLExternal to obtain an upload URL and file ID
271+ token := os .Getenv ("SLACK_TOKEN" )
272+ if token == "" {
273+ return nil , fmt .Errorf ("SLACK_TOKEN is not set" )
274+ }
275+
276+ title := param .Namespace + "_" + param .JobName
277+ content := param .Log
278+ contentBytes := []byte (content )
279+ length := len (contentBytes )
280+
281+ ctx , cancel := context .WithTimeout (context .Background (), 30 * time .Second )
282+ defer cancel ()
283+
284+ // Step 1: get upload URL
285+ form := url.Values {}
286+ form .Set ("filename" , title + ".txt" )
287+ form .Set ("length" , strconv .Itoa (length ))
288+ getURLReq , err := http .NewRequestWithContext (ctx , http .MethodPost , "https://slack.com/api/files.getUploadURLExternal" , strings .NewReader (form .Encode ()))
261289 if err != nil {
262- klog .Errorf ("File uploadLog failed %s\n " , err )
263- return
290+ return nil , err
291+ }
292+ getURLReq .Header .Set ("Authorization" , "Bearer " + token )
293+ getURLReq .Header .Set ("Content-Type" , "application/x-www-form-urlencoded" )
294+
295+ resp , err := httpDo (getURLReq )
296+ if err != nil {
297+ return nil , err
298+ }
299+ defer resp .Body .Close ()
300+ if resp .StatusCode < 200 || resp .StatusCode >= 300 {
301+ b , _ := io .ReadAll (resp .Body )
302+ return nil , fmt .Errorf ("files.getUploadURLExternal failed: status=%d body=%s" , resp .StatusCode , string (b ))
303+ }
304+
305+ var getURLRes struct {
306+ OK bool `json:"ok"`
307+ UploadURL string `json:"upload_url"`
308+ FileID string `json:"file_id"`
309+ Error string `json:"error"`
310+ }
311+ if err := json .NewDecoder (resp .Body ).Decode (& getURLRes ); err != nil {
312+ return nil , err
313+ }
314+ if ! getURLRes .OK {
315+ return nil , fmt .Errorf ("files.getUploadURLExternal error: %s" , getURLRes .Error )
316+ }
317+
318+ // 2) Upload bytes to the pre-signed URL with PUT
319+ putReq , err := http .NewRequestWithContext (ctx , http .MethodPut , getURLRes .UploadURL , bytes .NewReader (contentBytes ))
320+ if err != nil {
321+ return nil , err
322+ }
323+ putReq .Header .Set ("Content-Type" , "text/plain" )
324+ putReq .Header .Set ("Content-Length" , strconv .Itoa (length ))
325+ putResp , err := httpDo (putReq )
326+ if err != nil {
327+ return nil , err
328+ }
329+ defer putResp .Body .Close ()
330+ if putResp .StatusCode < 200 || putResp .StatusCode >= 300 {
331+ b , _ := io .ReadAll (putResp .Body )
332+ return nil , fmt .Errorf ("upload to upload_url failed: status=%d body=%s" , putResp .StatusCode , string (b ))
333+ }
334+
335+ // 3) Complete upload to share into the channel
336+ completeBody := struct {
337+ ChannelID string `json:"channel_id"`
338+ Files []struct {
339+ ID string `json:"id"`
340+ Title string `json:"title"`
341+ } `json:"files"`
342+ }{
343+ ChannelID : s .channel ,
344+ Files : []struct {
345+ ID string `json:"id"`
346+ Title string `json:"title"`
347+ }{{ID : getURLRes .FileID , Title : title + ".txt" }},
348+ }
349+
350+ bodyBytes , err := json .Marshal (completeBody )
351+ if err != nil {
352+ return nil , err
353+ }
354+
355+ completeReq , err := http .NewRequestWithContext (ctx , http .MethodPost , "https://slack.com/api/files.completeUploadExternal" , bytes .NewReader (bodyBytes ))
356+ if err != nil {
357+ return nil , err
358+ }
359+ completeReq .Header .Set ("Authorization" , "Bearer " + token )
360+ completeReq .Header .Set ("Content-Type" , "application/json" )
361+
362+ completeResp , err := httpDo (completeReq )
363+ if err != nil {
364+ return nil , err
365+ }
366+ defer completeResp .Body .Close ()
367+ if completeResp .StatusCode < 200 || completeResp .StatusCode >= 300 {
368+ b , _ := io .ReadAll (completeResp .Body )
369+ return nil , fmt .Errorf ("files.completeUploadExternal failed: status=%d body=%s" , completeResp .StatusCode , string (b ))
370+ }
371+
372+ var completeRes struct {
373+ OK bool `json:"ok"`
374+ Files []slackapi.File `json:"files"`
375+ Error string `json:"error"`
376+ }
377+ if err := json .NewDecoder (completeResp .Body ).Decode (& completeRes ); err != nil {
378+ return nil , err
379+ }
380+ if ! completeRes .OK {
381+ return nil , fmt .Errorf ("files.completeUploadExternal error: %s" , completeRes .Error )
382+ }
383+ if len (completeRes .Files ) == 0 {
384+ return nil , fmt .Errorf ("files.completeUploadExternal returned no files" )
264385 }
265386
266- klog .Infof ("File uploadLog successfully %s" , file .Name )
267- return
387+ klog .Infof ("File uploadLog successfully %s" , completeRes . Files [ 0 ] .Name )
388+ return & completeRes . Files [ 0 ], nil
268389}
269390
270391func isNotifyFromEnv (key string ) bool {
0 commit comments