diff --git a/internal/notifier/discord.go b/internal/notifier/discord.go index 932d07ec9..0651d5d56 100644 --- a/internal/notifier/discord.go +++ b/internal/notifier/discord.go @@ -19,8 +19,10 @@ package notifier import ( "context" "fmt" + "maps" "net/url" "path" + "slices" "strings" eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1" @@ -71,7 +73,8 @@ func (s *Discord) Post(ctx context.Context, event eventv1.Event) error { } sfields := make([]SlackField, 0, len(event.Metadata)) - for k, v := range event.Metadata { + for _, k := range slices.Sorted(maps.Keys(event.Metadata)) { + v := event.Metadata[k] sfields = append(sfields, SlackField{k, v, false}) } diff --git a/internal/notifier/google_chat.go b/internal/notifier/google_chat.go index 74b33f5b4..69c2783ed 100644 --- a/internal/notifier/google_chat.go +++ b/internal/notifier/google_chat.go @@ -19,7 +19,9 @@ package notifier import ( "context" "fmt" + "maps" "net/url" + "slices" "strings" eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1" @@ -114,7 +116,8 @@ func (s *GoogleChat) Post(ctx context.Context, event eventv1.Event) error { // Meta-Data if len(event.Metadata) > 0 { kvfields := make([]GoogleChatCardWidget, 0, len(event.Metadata)) - for k, v := range event.Metadata { + for _, k := range slices.Sorted(maps.Keys(event.Metadata)) { + v := event.Metadata[k] kvfields = append(kvfields, GoogleChatCardWidget{ KeyValue: &GoogleChatCardWidgetKeyValue{ TopLabel: k, diff --git a/internal/notifier/grafana.go b/internal/notifier/grafana.go index 279fa4876..d5080b20a 100644 --- a/internal/notifier/grafana.go +++ b/internal/notifier/grafana.go @@ -20,7 +20,9 @@ import ( "context" "crypto/tls" "fmt" + "maps" "net/url" + "slices" "strings" eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1" @@ -65,7 +67,8 @@ func (g *Grafana) Post(ctx context.Context, event eventv1.Event) error { sfields := make([]string, 0, len(event.Metadata)) // add tag to filter on grafana sfields = append(sfields, "flux", event.ReportingController) - for k, v := range event.Metadata { + for _, k := range slices.Sorted(maps.Keys(event.Metadata)) { + v := event.Metadata[k] key := strings.ReplaceAll(k, ":", "|") value := strings.ReplaceAll(v, ":", "|") sfields = append(sfields, fmt.Sprintf("%s: %s", key, value)) diff --git a/internal/notifier/lark.go b/internal/notifier/lark.go index 0ba723051..4a499929b 100644 --- a/internal/notifier/lark.go +++ b/internal/notifier/lark.go @@ -3,7 +3,9 @@ package notifier import ( "context" "fmt" + "maps" "net/url" + "slices" "strings" eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1" @@ -70,7 +72,8 @@ func (l *Lark) Post(ctx context.Context, event eventv1.Event) error { } message := fmt.Sprintf("**%s**\n\n", event.Message) - for k, v := range event.Metadata { + for _, k := range slices.Sorted(maps.Keys(event.Metadata)) { + v := event.Metadata[k] message = message + fmt.Sprintf("%s: %s\n", k, v) } diff --git a/internal/notifier/matrix.go b/internal/notifier/matrix.go index 07eabc098..b1ae5d298 100644 --- a/internal/notifier/matrix.go +++ b/internal/notifier/matrix.go @@ -6,8 +6,10 @@ import ( "crypto/tls" "encoding/json" "fmt" + "maps" "net/http" "net/url" + "slices" "strings" eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1" @@ -53,7 +55,8 @@ func (m *Matrix) Post(ctx context.Context, event eventv1.Event) error { emoji = "🚨" } var metadata string - for k, v := range event.Metadata { + for _, k := range slices.Sorted(maps.Keys(event.Metadata)) { + v := event.Metadata[k] metadata = metadata + fmt.Sprintf("- %s: %s\n", k, v) } heading := fmt.Sprintf("%s %s/%s.%s", emoji, strings.ToLower(event.InvolvedObject.Kind), diff --git a/internal/notifier/rocket.go b/internal/notifier/rocket.go index 08d46944e..b8609b55b 100644 --- a/internal/notifier/rocket.go +++ b/internal/notifier/rocket.go @@ -20,7 +20,9 @@ import ( "context" "crypto/tls" "fmt" + "maps" "net/url" + "slices" "strings" eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1" @@ -64,7 +66,8 @@ func (s *Rocket) Post(ctx context.Context, event eventv1.Event) error { } sfields := make([]SlackField, 0, len(event.Metadata)) - for k, v := range event.Metadata { + for _, k := range slices.Sorted(maps.Keys(event.Metadata)) { + v := event.Metadata[k] sfields = append(sfields, SlackField{k, v, false}) } diff --git a/internal/notifier/slack.go b/internal/notifier/slack.go index b235f16bb..fc3c42456 100644 --- a/internal/notifier/slack.go +++ b/internal/notifier/slack.go @@ -21,8 +21,10 @@ import ( "crypto/tls" "encoding/json" "fmt" + "maps" "net/http" "net/url" + "slices" "strings" eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1" @@ -102,7 +104,8 @@ func (s *Slack) Post(ctx context.Context, event eventv1.Event) error { } sfields := make([]SlackField, 0, len(event.Metadata)) - for k, v := range event.Metadata { + for _, k := range slices.Sorted(maps.Keys(event.Metadata)) { + v := event.Metadata[k] sfields = append(sfields, SlackField{k, v, false}) } diff --git a/internal/notifier/slack_test.go b/internal/notifier/slack_test.go index f73abe66d..64e9d655a 100644 --- a/internal/notifier/slack_test.go +++ b/internal/notifier/slack_test.go @@ -66,3 +66,46 @@ func TestSlack_ValidateResponse(t *testing.T) { err = validateSlackResponse(resp.Result()) g.Expect(err).To(MatchError(ContainSubstring("Slack responded with error: too_many_attachments"))) } + +// Go randomises map iteration, so the fields shuffled between messages and the +// same four keys arrived in a different order every time. Ten posts of one event +// is enough for an unsorted map to disagree with itself. +func TestSlack_PostFieldsAreSorted(t *testing.T) { + g := NewWithT(t) + + var got [][]string + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + b, err := io.ReadAll(r.Body) + g.Expect(err).ToNot(HaveOccurred()) + + var payload SlackPayload + g.Expect(json.Unmarshal(b, &payload)).To(Succeed()) + + keys := make([]string, 0, len(payload.Attachments[0].Fields)) + for _, f := range payload.Attachments[0].Fields { + keys = append(keys, f.Title) + } + got = append(got, keys) + })) + defer ts.Close() + + slack, err := NewSlack(ts.URL, "", "", nil, "", "test") + g.Expect(err).ToNot(HaveOccurred()) + + event := testEvent() + event.Metadata = map[string]string{ + "revision": "main/1234", + "cluster": "staging", + "image-tag": "v1.2.3", + "env": "staging", + } + + for i := 0; i < 10; i++ { + g.Expect(slack.Post(context.TODO(), event)).To(Succeed()) + } + + want := []string{"cluster", "env", "image-tag", "revision"} + for i, keys := range got { + g.Expect(keys).To(Equal(want), "post %d returned fields out of order", i) + } +} diff --git a/internal/notifier/teams.go b/internal/notifier/teams.go index 43ad99495..8af55db8f 100644 --- a/internal/notifier/teams.go +++ b/internal/notifier/teams.go @@ -20,6 +20,7 @@ import ( "context" "crypto/tls" "fmt" + "maps" "net/url" "slices" "strings" @@ -173,7 +174,8 @@ func (s *MSTeams) Post(ctx context.Context, event eventv1.Event) error { func buildMSTeamsDeprecatedConnectorPayload(event *eventv1.Event, objName string) *MSTeamsPayload { facts := make([]MSTeamsField, 0, len(event.Metadata)) - for k, v := range event.Metadata { + for _, k := range slices.Sorted(maps.Keys(event.Metadata)) { + v := event.Metadata[k] facts = append(facts, MSTeamsField{ Name: k, Value: v, diff --git a/internal/notifier/telegram.go b/internal/notifier/telegram.go index cad10e270..76be5e74c 100644 --- a/internal/notifier/telegram.go +++ b/internal/notifier/telegram.go @@ -4,7 +4,9 @@ import ( "context" "errors" "fmt" + "maps" "net/url" + "slices" "strings" eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1" @@ -57,7 +59,8 @@ func (t *Telegram) Post(ctx context.Context, event eventv1.Event) error { heading := fmt.Sprintf("%s %s/%s/%s", emoji, strings.ToLower(event.InvolvedObject.Kind), event.InvolvedObject.Name, event.InvolvedObject.Namespace) var metadata string - for k, v := range event.Metadata { + for _, k := range slices.Sorted(maps.Keys(event.Metadata)) { + v := event.Metadata[k] metadata = metadata + fmt.Sprintf("\\- *%s*: %s\n", escapeString(k), escapeString(v)) } message := fmt.Sprintf("*%s*\n%s\n%s", escapeString(heading), escapeString(event.Message), metadata) diff --git a/internal/notifier/webex.go b/internal/notifier/webex.go index 65dd57893..92bb23374 100644 --- a/internal/notifier/webex.go +++ b/internal/notifier/webex.go @@ -20,7 +20,9 @@ import ( "context" "crypto/tls" "fmt" + "maps" "net/url" + "slices" "strings" eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1" @@ -88,7 +90,8 @@ func (s *Webex) CreateMarkdown(event *eventv1.Event) string { fmt.Fprintf(&b, "%s\n", event.Message) if len(event.Metadata) > 0 { - for k, v := range event.Metadata { + for _, k := range slices.Sorted(maps.Keys(event.Metadata)) { + v := event.Metadata[k] fmt.Fprintf(&b, ">**%s**: %s\n", k, v) } } diff --git a/internal/notifier/zoom.go b/internal/notifier/zoom.go index 097614376..792bf9302 100644 --- a/internal/notifier/zoom.go +++ b/internal/notifier/zoom.go @@ -21,7 +21,9 @@ import ( "crypto/tls" "errors" "fmt" + "maps" "net/url" + "slices" "strings" eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1" @@ -111,7 +113,8 @@ func (s *Zoom) Post(ctx context.Context, event eventv1.Event) error { if len(event.Metadata) > 0 { fields := make([]ZoomField, 0, len(event.Metadata)) - for k, v := range event.Metadata { + for _, k := range slices.Sorted(maps.Keys(event.Metadata)) { + v := event.Metadata[k] fields = append(fields, ZoomField{ Key: k, Value: v,