@@ -3,62 +3,132 @@ package slack
33import (
44 "bytes"
55 "c7n-helper/pkg/dto"
6+ "c7n-helper/pkg/log"
7+ "context"
68 "fmt"
79 "github.com/lensesio/tableprinter"
8- "log"
9- "net/http"
10+ "github.com/slack-go/slack"
11+ "gopkg.in/yaml.v3"
12+ "os"
13+ "sort"
1014 "strings"
1115 "unicode/utf8"
1216)
1317
1418const (
15- MaxSlackMessageLength = 3_900
16- SplitMessageThreshold = MaxSlackMessageLength - MaxSlackMessageLength / 5
19+ maxSlackMessageLength = 3_000
20+ splitMessageThreshold = maxSlackMessageLength - maxSlackMessageLength / 5
1721)
1822
19- type Resource struct {
23+ type msgLine struct {
2024 Index int `header:"#"`
2125 Region string `header:"Region"`
2226 Name string `header:"Name"`
2327 Created string `header:"Created date"`
2428}
2529
26- func Notify (resourceFile , url , title string ) error {
27- log .Println ("Reading resource file..." )
30+ func Notify (ctx context.Context , resourceFile , slackToken , slackDefaultChannel , membersFile , title string ) error {
31+ logger := log .FromContext (ctx )
32+ logger .Info ("reading resource file..." )
2833 var report dto.PolicyReport
2934 if err := report .ReadFromFile (resourceFile ); err != nil {
3035 return err
3136 }
3237 if len (report .Accounts ) == 0 {
33- log . Println ( "Nothing to send... " )
38+ logger . Info ( "nothing to send" )
3439 return nil
3540 }
36- log .Println ("Preparing slack messages..." )
37- messages := reportToSlackMessages (title , report )
38- log .Println ("Sending slack notification..." )
39- return notifySlack (url , messages )
41+ slackClient := slack .New (slackToken )
42+ logger .Info ("reading slack members file..." )
43+ slackMembers , err := readSlackMembers (ctx , membersFile , slackClient )
44+ if err != nil {
45+ return err
46+ }
47+ logger .Info ("preparing slack messages..." )
48+ slackGroups := groupSlackMessage (report .Accounts , slackMembers , slackDefaultChannel )
49+ channelMessages := prepareSlackMessage (slackGroups )
50+ logger .Info ("sending slack notification..." )
51+ return notifySlack (ctx , slackClient , title , channelMessages )
4052}
4153
42- func reportToSlackMessages (title string , report dto.PolicyReport ) []string {
43- messages := make ([]string , 0 , len (report .Accounts )+ 1 )
44- if title != "" {
45- messages = append (messages , fmt .Sprintf ("{\" text\" :\" %s\" }" , title ))
54+ func readSlackMembers (ctx context.Context , file string , client * slack.Client ) (map [string ]string , error ) {
55+ if file == "" {
56+ return nil , nil
57+ }
58+ data , err := os .ReadFile (file )
59+ if err != nil {
60+ return nil , err
61+ }
62+ var rawData map [string ]interface {}
63+ if err := yaml .Unmarshal (data , & rawData ); err != nil {
64+ return nil , err
65+ }
66+ if len (rawData ) == 0 {
67+ return nil , nil
68+ }
69+ members := rawData ["members" ].(map [string ]interface {})
70+ result := make (map [string ]string )
71+ for name := range members {
72+ member := members [name ].(map [string ]interface {})
73+ if id , ok := member ["slackID" ]; ok {
74+ slackID := id .(string )
75+ // Filter out not existed users
76+ if _ , err := client .GetUserInfo (slackID ); err != nil {
77+ log .FromContext (ctx ).Errorf ("slack user [%s] not found: %s" , slackID , err .Error ())
78+ continue
79+ }
80+ result [strings .ToLower (name )] = id .(string )
81+ }
82+ }
83+ return result , nil
84+ }
85+
86+ // Groups Slack messages: SlackChannelID -> Account|Project|Subscription -> []Resources
87+ func groupSlackMessage (accounts []dto.Account , slackMembers map [string ]string , defaultChannel string ) map [string ]map [string ][]dto.Resource {
88+ groups := make (map [string ]map [string ][]dto.Resource )
89+ for _ , account := range accounts {
90+ for _ , resource := range account .Resources {
91+ channel , ok := slackMembers [strings .ToLower (resource .Owner )]
92+ if ! ok {
93+ channel = defaultChannel
94+ }
95+ accountResources , ok := groups [channel ]
96+ if ! ok {
97+ accountResources = make (map [string ][]dto.Resource )
98+ groups [channel ] = accountResources
99+ }
100+ if _ , ok := accountResources [account .Name ]; ! ok {
101+ accountResources [account .Name ] = make ([]dto.Resource , 0 )
102+ }
103+ accountResources [account .Name ] = append (accountResources [account .Name ], resource )
104+ }
46105 }
47- for _ , account := range report .Accounts {
48- buf := bytes .NewBufferString ("" )
49- tableprinter .Print (buf , resourcesFromDto (account .Resources ))
50- for _ , message := range normalizeMessage (buf .String ()) {
51- payload := fmt .Sprintf ("*%s*\n ```\n %s```\n " , account .Name , message )
52- messages = append (messages , fmt .Sprintf ("{\" text\" :\" %s\" }" , payload ))
106+ return groups
107+ }
108+
109+ func prepareSlackMessage (groups map [string ]map [string ][]dto.Resource ) map [string ][]string {
110+ channelMessages := make (map [string ][]string )
111+ for channel , accountResources := range groups {
112+ channelMessages [channel ] = make ([]string , 0 )
113+ for account , resources := range accountResources {
114+ sort .Slice (resources , func (i , j int ) bool {
115+ return resources [i ].Created .Before (resources [j ].Created )
116+ })
117+ buf := bytes .NewBufferString ("" )
118+ tableprinter .Print (buf , normalizeDTO (resources ))
119+ for _ , message := range splitMessage (buf .String ()) {
120+ payload := fmt .Sprintf ("*%s*\n ```\n %s```\n " , account , message )
121+ channelMessages [channel ] = append (channelMessages [channel ], payload )
122+ }
53123 }
54124 }
55- return messages
125+ return channelMessages
56126}
57127
58- func resourcesFromDto (resources []dto.Resource ) []Resource {
59- result := make ([]Resource , 0 , len (resources ))
128+ func normalizeDTO (resources []dto.Resource ) []msgLine {
129+ result := make ([]msgLine , 0 , len (resources ))
60130 for i , r := range resources {
61- result = append (result , Resource {
131+ result = append (result , msgLine {
62132 Index : i + 1 ,
63133 Region : r .Location ,
64134 Name : r .Name ,
@@ -68,14 +138,14 @@ func resourcesFromDto(resources []dto.Resource) []Resource {
68138 return result
69139}
70140
71- func normalizeMessage (message string ) []string {
72- if utf8 .RuneCountInString (message ) > MaxSlackMessageLength {
141+ func splitMessage (message string ) []string {
142+ if utf8 .RuneCountInString (message ) > maxSlackMessageLength {
73143 messages := make ([]string , 0 )
74144 builder := strings.Builder {}
75145 lines := strings .Split (message , "\n " )
76146 for _ , line := range lines {
77147 builder .WriteString (line + "\n " )
78- if utf8 .RuneCountInString (builder .String ()) > SplitMessageThreshold {
148+ if utf8 .RuneCountInString (builder .String ()) > splitMessageThreshold {
79149 messages = append (messages , builder .String ())
80150 builder .Reset ()
81151 }
@@ -88,26 +158,27 @@ func normalizeMessage(message string) []string {
88158 return []string {message }
89159}
90160
91- func notifySlack (url string , messages []string ) error {
92- client := & http.Client {}
93- for _ , payload := range messages {
94- req , err := http .NewRequest ("POST" , url , bytes .NewBuffer ([]byte (payload )))
95- if err != nil {
96- return err
97- }
98- req .Header .Set ("Content-Type" , "application/json" )
99- if err := sendMessage (client , req ); err != nil {
100- return err
161+ func notifySlack (ctx context.Context , client * slack.Client , title string , channelMessages map [string ][]string ) error {
162+ var header * slack.HeaderBlock
163+ if title != "" {
164+ header = & slack.HeaderBlock {
165+ Type : slack .MBTHeader ,
166+ Text : slack .NewTextBlockObject (slack .PlainTextType , title , false , false ),
101167 }
102168 }
103- return nil
104- }
105-
106- func sendMessage (client * http.Client , req * http.Request ) error {
107- resp , err := client .Do (req )
108- if err != nil {
109- return err
169+ for channel , messages := range channelMessages {
170+ if header != nil {
171+ _ , _ , _ , err := client .SendMessageContext (ctx , channel , slack .MsgOptionBlocks (header ))
172+ if err != nil {
173+ return err
174+ }
175+ }
176+ for _ , message := range messages {
177+ _ , _ , _ , err := client .SendMessageContext (ctx , channel , slack .MsgOptionText (message , false ))
178+ if err != nil {
179+ return err
180+ }
181+ }
110182 }
111- defer resp .Body .Close ()
112183 return nil
113184}
0 commit comments