Skip to content

Commit 0448740

Browse files
Slack notification improved. (#8)
Signed-off-by: viktor-kurchenko <[email protected]>
1 parent a4ea774 commit 0448740

File tree

6 files changed

+150
-54
lines changed

6 files changed

+150
-54
lines changed

cmd/slack.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package cmd
22

33
import (
44
"c7n-helper/pkg/slack"
5+
"context"
56
"github.com/spf13/cobra"
67
"log"
78
)
@@ -14,19 +15,24 @@ var slackCmd = &cobra.Command{
1415
Run: notify,
1516
}
1617

17-
var slackFile, slackURL, slackTitle *string
18+
var slackResourceFile, slackToken, slackChannel, slackMembersFile, slackTitle *string
1819

1920
func init() {
20-
slackFile = slackCmd.Flags().StringP("resource-file", "r", "resources.json", "Resource JSON file")
21+
slackResourceFile = slackCmd.Flags().StringP("resource-file", "r", "resources.json", "Resource JSON file")
2122
_ = slackCmd.MarkFlagFilename("resource-file")
22-
slackURL = slackCmd.Flags().StringP("url", "u", "", "Slack webhook URL")
23-
_ = slackCmd.MarkFlagRequired("url")
23+
slackToken = slackCmd.Flags().StringP("auth-token", "a", "", "Slack token")
24+
_ = slackCmd.MarkFlagRequired("auth-token")
25+
slackChannel = slackCmd.Flags().StringP("channel", "c", "", "Slack default channel ID")
26+
_ = slackCmd.MarkFlagRequired("channel")
27+
slackMembersFile = slackCmd.Flags().StringP("members", "m", "", "Slack members YAML file")
28+
_ = slackCmd.MarkFlagFilename("members")
2429
slackTitle = slackCmd.Flags().StringP("title", "t", "", "Slack notification title")
2530
rootCmd.AddCommand(slackCmd)
2631
}
2732

2833
func notify(_ *cobra.Command, _ []string) {
29-
if err := slack.Notify(*slackFile, *slackURL, *slackTitle); err != nil {
34+
ctx := context.Background()
35+
if err := slack.Notify(ctx, *slackResourceFile, *slackToken, *slackChannel, *slackMembersFile, *slackTitle); err != nil {
3036
log.Fatal(err.Error())
3137
}
3238
}

go.mod

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,14 @@ require (
1010
github.com/aws/aws-sdk-go-v2/service/ec2 v1.91.0
1111
github.com/aws/aws-sdk-go-v2/service/eks v1.27.8
1212
github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.15.6
13+
github.com/aws/smithy-go v1.13.5
1314
github.com/hashicorp/go-multierror v1.1.1
1415
github.com/lensesio/tableprinter v0.0.0-20201125135848-89e81fc956e7
16+
github.com/slack-go/slack v0.12.1
1517
github.com/spf13/cobra v1.6.1
18+
go.uber.org/multierr v1.6.0
1619
go.uber.org/zap v1.24.0
20+
gopkg.in/yaml.v3 v3.0.1
1721
)
1822

1923
require (
@@ -26,8 +30,8 @@ require (
2630
github.com/aws/aws-sdk-go-v2/service/sso v1.12.6 // indirect
2731
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.6 // indirect
2832
github.com/aws/aws-sdk-go-v2/service/sts v1.18.7 // indirect
29-
github.com/aws/smithy-go v1.13.5 // indirect
3033
github.com/dustin/go-humanize v1.0.1 // indirect
34+
github.com/gorilla/websocket v1.4.2 // indirect
3135
github.com/hashicorp/errwrap v1.0.0 // indirect
3236
github.com/inconshreveable/mousetrap v1.1.0 // indirect
3337
github.com/jmespath/go-jmespath v0.4.0 // indirect
@@ -36,5 +40,4 @@ require (
3640
github.com/rivo/uniseg v0.2.0 // indirect
3741
github.com/spf13/pflag v1.0.5 // indirect
3842
go.uber.org/atomic v1.7.0 // indirect
39-
go.uber.org/multierr v1.6.0 // indirect
4043
)

go.sum

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,13 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
3939
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4040
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
4141
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
42+
github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho=
43+
github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
44+
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
4245
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
4346
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
47+
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
48+
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
4449
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
4550
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
4651
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
@@ -64,11 +69,14 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
6469
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
6570
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
6671
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
72+
github.com/slack-go/slack v0.12.1 h1:X97b9g2hnITDtNsNe5GkGx6O2/Sz/uC20ejRZN6QxOw=
73+
github.com/slack-go/slack v0.12.1/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=
6774
github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
6875
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
6976
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
7077
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
7178
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
79+
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
7280
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
7381
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
7482
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
@@ -78,6 +86,8 @@ go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
7886
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
7987
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
8088
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
89+
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
90+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
8191
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
8292
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
8393
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

pkg/aws/cleaner.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"c7n-helper/pkg/dto"
55
"c7n-helper/pkg/log"
66
"context"
7+
"errors"
78
"github.com/hashicorp/go-multierror"
89
"go.uber.org/multierr"
910
"time"
@@ -21,6 +22,10 @@ func DeleteResources(ctx context.Context, accounts []dto.Account, tries int, ret
2122
logger.Info("finding cluster and vpc")
2223
cluster, err := listEKS(ctx, cls.EKS, clusterName)
2324
if err != nil {
25+
if errors.As(err, &eksNotFoundErr) {
26+
logger.Info("cluster not found, probably it was deleted previously")
27+
return nil
28+
}
2429
return err
2530
}
2631
vpcID := *cluster.ResourcesVpcConfig.VpcId

pkg/dto/dto.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ type Account struct {
2020
type Resource struct {
2121
Name string `json:"name"`
2222
Location string `json:"location"`
23+
Owner string `json:"owner"`
2324
Created time.Time `json:"created"`
2425
}
2526

pkg/slack/slack.go

Lines changed: 118 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -3,62 +3,132 @@ package slack
33
import (
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

1418
const (
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

Comments
 (0)