From 61103fc19ae5a03bdd38108ee4a9004e93002baf Mon Sep 17 00:00:00 2001 From: Somtochi Onyekwere Date: Wed, 18 Aug 2021 12:48:02 +0100 Subject: [PATCH] Add support for Matrix notification Signed-off-by: Somtochi Onyekwere --- api/v1beta1/provider_types.go | 3 +- ...ification.toolkit.fluxcd.io_providers.yaml | 1 + docs/spec/v1beta1/provider.md | 25 ++++++ internal/notifier/factory.go | 2 + internal/notifier/matrix.go | 82 +++++++++++++++++++ internal/notifier/matrix_test.go | 50 +++++++++++ 6 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 internal/notifier/matrix.go create mode 100644 internal/notifier/matrix_test.go diff --git a/api/v1beta1/provider_types.go b/api/v1beta1/provider_types.go index 9cd6a79e0..57a976baa 100644 --- a/api/v1beta1/provider_types.go +++ b/api/v1beta1/provider_types.go @@ -28,7 +28,7 @@ const ( // ProviderSpec defines the desired state of Provider type ProviderSpec struct { // Type of provider - // +kubebuilder:validation:Enum=slack;discord;msteams;rocket;generic;github;gitlab;bitbucket;azuredevops;googlechat;webex;sentry;azureeventhub;telegram;lark + // +kubebuilder:validation:Enum=slack;discord;msteams;rocket;generic;github;gitlab;bitbucket;azuredevops;googlechat;webex;sentry;azureeventhub;telegram;lark;matrix; // +required Type string `json:"type"` @@ -79,6 +79,7 @@ const ( AzureEventHubProvider string = "azureeventhub" TelegramProvider string = "telegram" LarkProvider string = "lark" + Matrix string = "matrix" ) // ProviderStatus defines the observed state of Provider diff --git a/config/crd/bases/notification.toolkit.fluxcd.io_providers.yaml b/config/crd/bases/notification.toolkit.fluxcd.io_providers.yaml index 0ecba6f53..0904d7cbf 100644 --- a/config/crd/bases/notification.toolkit.fluxcd.io_providers.yaml +++ b/config/crd/bases/notification.toolkit.fluxcd.io_providers.yaml @@ -89,6 +89,7 @@ spec: - azureeventhub - telegram - lark + - matrix type: string username: description: Bot username for this provider diff --git a/docs/spec/v1beta1/provider.md b/docs/spec/v1beta1/provider.md index 40887a316..72724c886 100644 --- a/docs/spec/v1beta1/provider.md +++ b/docs/spec/v1beta1/provider.md @@ -362,3 +362,28 @@ spec: secretRef: name: lark-token ``` + +## Matrix +For Matrix, the address is the homeserver URL and the token is the access token returned by a call +to /login or /register + +To create secret +``` +kubectl create secret generic matrix-token \ +--from-literal=token= \ +--from-literal=address=https://matrix.org # replace with if using a different server +``` + +`spec.channel` holds the room id. +```yaml +apiVersion: notification.toolkit.fluxcd.io/v1beta1 +kind: Provider +metadata: + name: matrix + namespace: flux-system +spec: + type: matrix + channel: "!jezptmDwEeLapMLjOc:matrix.org" + secretRef: + name: matrix-token +``` diff --git a/internal/notifier/factory.go b/internal/notifier/factory.go index bf945059a..95e3c94ae 100644 --- a/internal/notifier/factory.go +++ b/internal/notifier/factory.go @@ -81,6 +81,8 @@ func (f Factory) Notifier(provider string) (Interface, error) { n, err = NewTelegram(f.Channel, f.Token) case v1beta1.LarkProvider: n, err = NewLark(f.URL) + case v1beta1.Matrix: + n, err = NewMatrix(f.URL, f.Token, f.Channel) default: err = fmt.Errorf("provider %s not supported", provider) } diff --git a/internal/notifier/matrix.go b/internal/notifier/matrix.go new file mode 100644 index 000000000..8eb0da24c --- /dev/null +++ b/internal/notifier/matrix.go @@ -0,0 +1,82 @@ +package notifier + +import ( + "crypto/sha1" + "encoding/json" + "fmt" + "net/http" + "net/url" + "strings" + + "github.com/fluxcd/pkg/runtime/events" + "github.com/hashicorp/go-retryablehttp" +) + +type Matrix struct { + Token string + URL string + RoomId string +} + +type MatrixPayload struct { + Body string `json:"body"` + MsgType string `json:"msgtype"` +} + +func NewMatrix(serverURL, token, roomId string) (*Matrix, error) { + _, err := url.ParseRequestURI(serverURL) + if err != nil { + return nil, fmt.Errorf("invalid Matrix homeserver URL %s", serverURL) + } + + return &Matrix{ + URL: serverURL, + RoomId: roomId, + Token: token, + }, nil +} + +func (m *Matrix) Post(event events.Event) error { + txId, err := sha1sum(event) + if err != nil { + return fmt.Errorf("unable to generate unique tx id: %s", err) + } + fullURL := fmt.Sprintf("%s/_matrix/client/r0/rooms/%s/send/m.room.message/%s", + m.URL, m.RoomId, txId) + + emoji := "💫" + if event.Severity == events.EventSeverityError { + emoji = "🚨" + } + var metadata string + for k, v := range event.Metadata { + metadata = metadata + fmt.Sprintf("- %s: %s\n", k, v) + } + heading := fmt.Sprintf("%s %s/%s.%s", emoji, strings.ToLower(event.InvolvedObject.Kind), + event.InvolvedObject.Name, event.InvolvedObject.Namespace) + msg := fmt.Sprintf("%s\n%s\n%s", heading, event.Message, metadata) + + payload := MatrixPayload{ + Body: msg, + MsgType: "m.text", + } + + err = postMessage(fullURL, "", nil, payload, func(request *retryablehttp.Request) { + request.Method = http.MethodPut + request.Header.Add("Authorization", "Bearer "+m.Token) + }) + if err != nil { + return fmt.Errorf("postMessage failed: %w", err) + } + + return nil +} + +func sha1sum(event events.Event) (string, error) { + val, err := json.Marshal(event) + if err != nil { + return "", err + } + digest := sha1.Sum(val) + return fmt.Sprintf("%x", digest), nil +} diff --git a/internal/notifier/matrix_test.go b/internal/notifier/matrix_test.go new file mode 100644 index 000000000..ad8468521 --- /dev/null +++ b/internal/notifier/matrix_test.go @@ -0,0 +1,50 @@ +package notifier + +import ( + "testing" + "time" + + "github.com/fluxcd/pkg/runtime/events" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestSha1Sum(t *testing.T) { + timestamp, err := time.Parse("Jan 2, 2006 at 3:04pm (WAT)", "Aug 24, 2021 at 4:18pm (WAT)") + if err != nil { + t.Fatalf("unexpected error getting timestamp: %s", err) + } + + tests := []struct { + event events.Event + sha1 string + }{ + { + event: events.Event{ + InvolvedObject: corev1.ObjectReference{}, + Severity: events.EventSeverityInfo, + Timestamp: metav1.Time{ + Time: timestamp, + }, + Message: "update successful", + Reason: "update sucesful", + Metadata: nil, + ReportingController: "", + ReportingInstance: "", + }, + sha1: "37d91b4f6a1e44c6a38273b0a0fd408fade7b0f5", + }, + } + + for _, tt := range tests { + hash, err := sha1sum(tt.event) + if err != nil { + t.Fatalf("unexpected err: %s", err) + } + + if tt.sha1 != hash { + t.Errorf("wrong sha1 sum from event %v. expected %q got %q", + tt.event, tt.sha1, hash) + } + } +}