Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions docs/services/slack.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,3 +206,42 @@ template.app-sync-failed: |
```

The message is sent according to the `deliveryPolicy` string field under the `slack` field. The available modes are `Post` (default), `PostAndUpdate`, and `Update`. The `PostAndUpdate` and `Update` settings require `groupingKey` to be set.

### Thread Replies and Message Updates

You can explicitly control thread replies and message updates using the `threadTs` and `updateTs` fields:

#### Reply to a Specific Thread

Use `threadTs` to post a message as a reply in a specific Slack thread. This is useful when you want to reply to a particular message thread based on a timestamp you already know (e.g., from a previous notification or external system).

```yaml
template.app-deploy-status: |
message: |
Deployment completed for {{.app.metadata.name}}.
slack:
threadTs: "1234567890.123456" # Reply to this specific thread
```

The `threadTs` value should be the timestamp of the parent message in the thread. When set, the message will be posted as a reply in that thread, regardless of the `groupingKey` or `deliveryPolicy` settings.

#### Update a Specific Message

Use `updateTs` to update a specific existing message. This is useful when you want to update a particular message based on its timestamp (e.g., to update a deployment status message).

```yaml
template.app-deploy-update: |
message: |
Deployment status: {{.app.status.operationState.phase}}
slack:
updateTs: "1234567890.123456" # Update this specific message
```

The `updateTs` value should be the timestamp of the message you want to update. When set, the existing message will be updated with the new content, regardless of the `groupingKey` or `deliveryPolicy` settings.

**Important Notes:**
- `threadTs` and `updateTs` are mutually exclusive - only use one at a time
- Both fields support template expressions, allowing dynamic values from your notification context
- When either field is set, it overrides the normal `groupingKey` and `deliveryPolicy` behavior
- Only messages posted by the authenticated bot user can be updated
- To update a message, you need the original message's timestamp (which can be obtained from the Slack API response or stored externally)
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ require (
github.com/opsgenie/opsgenie-go-sdk-v2 v1.2.23
github.com/prometheus/client_golang v1.21.0
github.com/sirupsen/logrus v1.9.3
github.com/slack-go/slack v0.16.0
github.com/slack-go/slack v0.17.3
github.com/spf13/cast v1.7.1
github.com/spf13/cobra v1.9.1
github.com/stretchr/testify v1.10.0
Expand Down Expand Up @@ -75,7 +75,7 @@ require (
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
Expand Down
14 changes: 6 additions & 8 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,8 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho=
github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
Expand All @@ -127,7 +127,6 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
Expand All @@ -154,9 +153,8 @@ github.com/gopackage/ddp v0.0.0-20170117053602-652027933df4 h1:4EZlYQIiyecYJlUbV
github.com/gopackage/ddp v0.0.0-20170117053602-652027933df4/go.mod h1:lEO7XoHJ/xNRBCxrn4h/CEB67h0kW1B0t4ooP2yrjUA=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gregdel/pushover v1.3.1 h1:4bMLITOZ15+Zpi6qqoGqOPuVHCwSUvMCgVnN5Xhilfo=
github.com/gregdel/pushover v1.3.1/go.mod h1:EcaO66Nn1StkpEm1iKtBTV3d2A16SoMsVER1PthX7to=
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
Expand Down Expand Up @@ -261,8 +259,8 @@ github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+D
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/slack-go/slack v0.16.0 h1:khp/WCFv+Hb/B/AJaAwvcxKun0hM6grN0bUZ8xG60P8=
github.com/slack-go/slack v0.16.0/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=
github.com/slack-go/slack v0.17.3 h1:zV5qO3Q+WJAQ/XwbGfNFrRMaJ5T/naqaonyPV/1TP4g=
github.com/slack-go/slack v0.17.3/go.mod h1:X+UqOufi3LYQHDnMG1vxf0J8asC6+WllXrVrhl8/Prk=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/sony/sonyflake v1.0.0 h1:MpU6Ro7tfXwgn2l5eluf9xQvQJDROTBImNCfRXn/YeM=
Expand Down
26 changes: 26 additions & 0 deletions pkg/services/slack.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ type SlackNotification struct {
GroupingKey string `json:"groupingKey"`
NotifyBroadcast bool `json:"notifyBroadcast"`
DeliveryPolicy slackutil.DeliveryPolicy `json:"deliveryPolicy"`
ThreadTS string `json:"threadTs,omitempty"`
UpdateTS string `json:"updateTs,omitempty"`
}

func (n *SlackNotification) GetTemplater(name string, f texttemplate.FuncMap) (Templater, error) {
Expand All @@ -54,6 +56,16 @@ func (n *SlackNotification) GetTemplater(name string, f texttemplate.FuncMap) (T
return nil, err
}

threadTS, err := texttemplate.New(name).Funcs(f).Parse(n.ThreadTS)
if err != nil {
return nil, err
}

updateTS, err := texttemplate.New(name).Funcs(f).Parse(n.UpdateTS)
if err != nil {
return nil, err
}

return func(notification *Notification, vars map[string]interface{}) error {
if notification.Slack == nil {
notification.Slack = &SlackNotification{}
Expand Down Expand Up @@ -88,6 +100,18 @@ func (n *SlackNotification) GetTemplater(name string, f texttemplate.FuncMap) (T
}
notification.Slack.GroupingKey = groupingKeyData.String()

var threadTSData bytes.Buffer
if err := threadTS.Execute(&threadTSData, vars); err != nil {
return err
}
notification.Slack.ThreadTS = threadTSData.String()

var updateTSData bytes.Buffer
if err := updateTS.Execute(&updateTSData, vars); err != nil {
return err
}
notification.Slack.UpdateTS = updateTSData.String()

notification.Slack.NotifyBroadcast = n.NotifyBroadcast
notification.Slack.DeliveryPolicy = n.DeliveryPolicy
return nil
Expand Down Expand Up @@ -187,6 +211,8 @@ func (s *slackService) Send(notification Notification, dest Destination) error {
slackNotification.GroupingKey,
slackNotification.NotifyBroadcast,
slackNotification.DeliveryPolicy,
slackNotification.ThreadTS,
slackNotification.UpdateTS,
msgOptions,
)
}
Expand Down
43 changes: 42 additions & 1 deletion pkg/util/slack/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,48 @@ func (c *threadedClient) setThreadTimestamp(recipient string, groupingKey string
thread[groupingKey] = ts
}

func (c *threadedClient) SendMessage(ctx context.Context, recipient string, groupingKey string, broadcast bool, policy DeliveryPolicy, options []sl.MsgOption) error {
func (c *threadedClient) SendMessage(ctx context.Context, recipient string, groupingKey string, broadcast bool, policy DeliveryPolicy, threadTS string, updateTS string, options []sl.MsgOption) error {
// Validate that threadTS and updateTS are mutually exclusive
if threadTS != "" && updateTS != "" {
return errors.New("threadTS and updateTS are mutually exclusive; only one may be set")
}
// If explicit updateTS is provided, update that specific message and return
if updateTS != "" {
_, _, err := SendMessageRateLimited(
c.Client,
ctx,
c.Limiter,
c.getChannelID(recipient),
sl.MsgOptionUpdate(updateTS),
sl.MsgOptionAsUser(true),
sl.MsgOptionCompose(options...),
)
return err
}

// If explicit threadTS is provided, post as a reply in that thread and return
if threadTS != "" {
options = append(options, sl.MsgOptionTS(threadTS))
_, channelID, err := SendMessageRateLimited(
c.Client,
ctx,
c.Limiter,
recipient,
sl.MsgOptionPost(),
buildPostOptions(broadcast, options),
)
if err != nil {
return err
}

c.lock.Lock()
c.ChannelIDs[recipient] = channelID
c.lock.Unlock()

return nil
}

// Otherwise, use the existing groupingKey + policy logic
ts := c.getThreadTimestamp(recipient, groupingKey)
if groupingKey != "" && ts != "" {
options = append(options, sl.MsgOptionTS(ts))
Expand Down
Loading