Skip to content

Commit ae4ca54

Browse files
authored
Change to using api deprecated slack api (#240)
1 parent de383fa commit ae4ca54

File tree

2 files changed

+180
-11
lines changed

2 files changed

+180
-11
lines changed

pkg/notification/slack.go

Lines changed: 132 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,26 @@ package notification
22

33
import (
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+
1125
const (
1226
START = "start"
1327
SUCCESS = "success"
@@ -38,6 +52,7 @@ var slackColors = map[string]string{
3852

3953
type 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

253268
func (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

270391
func isNotifyFromEnv(key string) bool {

pkg/notification/slack_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package notification
22

33
import (
4+
"bytes"
5+
"io"
6+
"net/http"
47
"os"
58
"testing"
69
"time"
@@ -493,3 +496,48 @@ func TestIsNotificationSuppressed(t *testing.T) {
493496
})
494497
}
495498
}
499+
500+
func TestUploadLog_Success(t *testing.T) {
501+
// stub HTTP
502+
original := httpDo
503+
defer func() { httpDo = original }()
504+
505+
httpDo = func(req *http.Request) (*http.Response, error) {
506+
if req.URL.Host == "slack.com" && req.URL.Path == "/api/files.getUploadURLExternal" && req.Method == http.MethodPost {
507+
body := []byte(`{"ok":true,"upload_url":"https://uploads.slack.com/abc123","file_id":"F123"}`)
508+
return &http.Response{StatusCode: 200, Body: io.NopCloser(bytes.NewReader(body))}, nil
509+
}
510+
if req.URL.Host == "uploads.slack.com" && req.Method == http.MethodPut {
511+
return &http.Response{StatusCode: 200, Body: io.NopCloser(bytes.NewReader(nil))}, nil
512+
}
513+
if req.URL.Host == "slack.com" && req.URL.Path == "/api/files.completeUploadExternal" && req.Method == http.MethodPost {
514+
body := []byte(`{"ok":true,"files":[{"id":"F123","name":"ns_job.txt","permalink":"https://slack-files.com/TX/F123"}]}`)
515+
return &http.Response{StatusCode: 200, Body: io.NopCloser(bytes.NewReader(body))}, nil
516+
}
517+
t.Fatalf("unexpected request: %s %s", req.Method, req.URL.String())
518+
return nil, nil
519+
}
520+
521+
os.Setenv("SLACK_TOKEN", "xoxb-test")
522+
defer os.Unsetenv("SLACK_TOKEN")
523+
524+
s := slack{client: &MockSlackClient{}, channel: "C123", username: "bot"}
525+
file, err := s.uploadLog(MessageTemplateParam{Namespace: "ns", JobName: "job", Log: "hello"})
526+
assert.NoError(t, err)
527+
assert.Equal(t, "ns_job.txt", file.Name)
528+
assert.Equal(t, "https://slack-files.com/TX/F123", file.Permalink)
529+
}
530+
531+
func TestUploadLog_TokenMissing(t *testing.T) {
532+
original := httpDo
533+
defer func() { httpDo = original }()
534+
httpDo = func(req *http.Request) (*http.Response, error) {
535+
t.Fatalf("httpDo should not be called when token is missing")
536+
return nil, nil
537+
}
538+
539+
os.Unsetenv("SLACK_TOKEN")
540+
s := slack{client: &MockSlackClient{}, channel: "C123", username: "bot"}
541+
_, err := s.uploadLog(MessageTemplateParam{Namespace: "ns", JobName: "job", Log: "hello"})
542+
assert.Error(t, err)
543+
}

0 commit comments

Comments
 (0)