diff --git a/.gitignore b/.gitignore index 7a3c303..b0f877d 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ matrix-alertmanager-receiver +config.toml diff --git a/config.toml.sample b/config.toml.sample new file mode 100644 index 0000000..c7613ef --- /dev/null +++ b/config.toml.sample @@ -0,0 +1,5 @@ +Homeserver = "https://staging.matrix.ungleich.cloud" +TargetRoomID = "!jHFKHemgIAaDJekoxN:matrix-staging.ungleich.ch" +MXID = "@fnux:matrix-staging.ungleich.ch" +MXToken = "secret" +HTTPPort = 9088 diff --git a/main.go b/main.go index d6d0a14..618a1c0 100644 --- a/main.go +++ b/main.go @@ -5,98 +5,127 @@ import ( "flag" "fmt" "log" + "io/ioutil" "net/http" "encoding/json" "github.com/prometheus/alertmanager/template" + "github.com/BurntSushi/toml" "github.com/matrix-org/gomatrix" ) +var logger *log.Logger + +type Configuration struct { + Homeserver string + TargetRoomID string + + MXID string + MXToken string + + HTTPPort int + HTTPToken string +} + func generateMatrixMessageBody(alert template.Alert) string { return alert.Status + " // " + alert.Annotations["summary"] } -func main() { - // Initialize logger. - var logger *log.Logger = log.New(os.Stdout, "", log.Flags()) - - // Handle command-line arguments. - var homeserver = flag.String("homeserver", "https://matrix.org", "Address of Matrix homeserver") - var user = flag.String("user", "", "Full MXID (e.g. @example.domain.tld) of Matrix user") - var token = flag.String("token", "", "Access Token of Matrix user") - var target = flag.String("target-room", "", "Matrix room to be notified of alerts.") - var port = flag.Int("port", 9088, "HTTP port to listen on (incoming alertmanager webhooks)") - flag.Parse() - - if *user == "" { - logger.Fatal("Matrix user is required. See --help for usage.") - } - if *token == "" { - logger.Fatal("Matrix access token is required. See --help for usage.") - } - if *target== "" { - logger.Fatal("Matrix target room is required. See --help for usage.") - } - - // Initialize Matrix client. - matrixClient, err := gomatrix.NewClient(*homeserver, *user, *token) +func getMatrixClient(homeserver string, user string, token string, targetRoomID string) *gomatrix.Client { + logger.Printf("Connecting to Matrix Homserver %v as %v.", homeserver, user) + matrixClient, err := gomatrix.NewClient(homeserver, user, token) if err != nil { - logger.Fatalf("Could not log in to Matrix (%v): %v", *homeserver, err) + logger.Fatalf("Could not log in to Matrix Homeserver (%v): %v", homeserver, err) } joinedRooms, err := matrixClient.JoinedRooms() if err != nil { - logger.Fatalf("Could not fetch joined rooms: %v", err) + logger.Fatalf("Could not fetch Matrix rooms: %v", err) } alreadyJoinedTarget := false for _, roomID := range joinedRooms.JoinedRooms { - // FIXME: will only work if target is a roomID, not an alias. - if *target == roomID { + if targetRoomID == roomID { alreadyJoinedTarget = true } } - if !alreadyJoinedTarget { - logger.Printf("Trying to join %v...", *target) - _, err := matrixClient.JoinRoom(*target, "", nil) + if alreadyJoinedTarget { + logger.Printf("%v is already part of %v.", user, targetRoomID,) + } else { + logger.Printf("Joining %v.", targetRoomID) + _, err := matrixClient.JoinRoom(targetRoomID, "", nil) if err != nil { - logger.Fatalf("Failed to join %v: %v", *target, err) + logger.Fatalf("Failed to join %v: %v", targetRoomID, err) } } - // Initialize HTTP serve (= listen for incoming requests). - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintf(w, `Hi! I receive prometheus-alertmanager webhooks on /alert and forward them to Matrix. - -You will find more details on: http://git.sr.ht/~fnux/matrix-prometheus-alertmanager`) - }) - - http.HandleFunc("/alert", func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - w.WriteHeader(http.StatusMethodNotAllowed) - return - } - - payload := template.Data{} - if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { - w.WriteHeader(http.StatusBadRequest) - } - - logger.Printf("Received valid hook from %v", r.RemoteAddr) - - for _, alert := range payload.Alerts { - body := generateMatrixMessageBody(alert) - logger.Printf("> %v", body) - _, err := matrixClient.SendText(*target, body) - if err != nil { - logger.Fatalf("Could not forward to Matrix: %v", err) - } - } - - w.WriteHeader(http.StatusOK) - }) - - var listenAddr = fmt.Sprintf(":%v", *port) - logger.Printf("Listening for HTTP requests (webhooks) on %v", listenAddr) - log.Fatal(http.ListenAndServe(listenAddr, nil)) + return matrixClient +} + +func handleIncomingHooks( w http.ResponseWriter, r *http.Request, + matrixClient *gomatrix.Client, targetRoomID string) { + + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + payload := template.Data{} + if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { + w.WriteHeader(http.StatusBadRequest) + } + + logger.Printf("Received valid hook from %v", r.RemoteAddr) + + for _, alert := range payload.Alerts { + body := generateMatrixMessageBody(alert) + logger.Printf("> %v", body) + _, err := matrixClient.SendText(targetRoomID, body) + if err != nil { + logger.Printf(">> Could not forward to Matrix: %v", err) + } + } + + w.WriteHeader(http.StatusOK) +} + +func main() { + // Initialize logger. + logger = log.New(os.Stdout, "", log.Flags()) + + // We use a configuration file since we need to specify secrets, and read + // everything else from it to keep things simple. + var configPath = flag.String("config", "/etc/matrix-alertmanager-receiver.toml", "Path to configuration file") + flag.Parse() + + logger.Printf("Reading configuration from %v.", *configPath) + raw, err := ioutil.ReadFile(*configPath) + if err != nil { + logger.Fatalf("Could not read configuration file (%v): %v", *configPath, err) + } + + var config Configuration + md, err := toml.Decode(string(raw), &config) + if err != nil { + logger.Fatalf("Could not parse configuration file (%v): %v", *configPath, err) + } + + for _, field := range []string{"Homeserver", "MXID", "MXToken", "TargetRoomID", "HTTPPort"} { + if ! md.IsDefined(field) { + logger.Fatalf("Field %v is not set in config. Exiting.", field) + } + } + + // Initialize Matrix client. + matrixClient := getMatrixClient( + config.Homeserver, config.MXID, config.MXToken, config.TargetRoomID) + + // Initialize HTTP server. + http.HandleFunc("/alert", func(w http.ResponseWriter, r *http.Request) { + handleIncomingHooks(w, r, matrixClient, config.TargetRoomID) + }) + + var listenAddr = fmt.Sprintf(":%v", config.HTTPPort) + logger.Printf("Listening for HTTP requests (webhooks) on %v", listenAddr) + logger.Fatal(http.ListenAndServe(listenAddr, nil)) }