2023-02-12 03:04:17 +01:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"encoding/base64"
|
|
|
|
"encoding/json"
|
2023-08-13 14:48:27 +02:00
|
|
|
"fmt"
|
2023-02-12 03:04:17 +01:00
|
|
|
"io"
|
2023-08-13 14:48:27 +02:00
|
|
|
"log/slog"
|
2023-02-12 03:04:17 +01:00
|
|
|
"net/http"
|
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
|
|
|
const dateLayout = "2006-01-02 15:04:05"
|
|
|
|
|
|
|
|
type silence struct {
|
|
|
|
Matchers []matcher `json:"matchers"`
|
|
|
|
StartsAt string `json:"startsAt"`
|
|
|
|
EndsAt string `json:"endsAt"`
|
|
|
|
CreatedBy string `json:"createdBy"`
|
|
|
|
Comment string `json:"comment"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type matcher struct {
|
|
|
|
Name string `json:"name"`
|
|
|
|
Value string `json:"value"`
|
|
|
|
IsRegex bool `json:"isRegex"`
|
|
|
|
IsEqual bool `json:"isEqual"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type silenceBody struct {
|
2023-02-12 14:40:32 +01:00
|
|
|
AlertManagerURL string `json:"alertmanagerURL"`
|
|
|
|
Labels map[string]string `json:"labels"`
|
2023-02-12 03:04:17 +01:00
|
|
|
}
|
|
|
|
|
2023-02-12 16:07:49 +01:00
|
|
|
type silenceResponse struct {
|
|
|
|
ID string `json:"silenceID"`
|
|
|
|
}
|
|
|
|
|
2023-02-21 13:57:21 +01:00
|
|
|
func (br *bridge) handleSilences(w http.ResponseWriter, r *http.Request) {
|
2023-08-13 15:16:26 +02:00
|
|
|
logger := br.logger.With(slog.String("handler", "/silences"))
|
|
|
|
|
2023-02-12 03:04:17 +01:00
|
|
|
if r.Method != http.MethodPost {
|
|
|
|
http.Error(w, "Only POST allowed", http.StatusMethodNotAllowed)
|
2023-08-13 15:16:26 +02:00
|
|
|
logger.Debug(fmt.Sprintf("Illegal HTTP method: expected %q, got %q", "POST", r.Method))
|
2023-02-12 03:04:17 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
b, err := io.ReadAll(r.Body)
|
|
|
|
if err != nil {
|
2023-08-13 15:16:26 +02:00
|
|
|
logger.Error("Failed to read body",
|
2023-08-13 14:48:27 +02:00
|
|
|
slog.String("error", err.Error()))
|
2023-02-12 03:04:17 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
b, err = base64.StdEncoding.DecodeString(string(b))
|
|
|
|
if err != nil {
|
2023-08-13 15:16:26 +02:00
|
|
|
logger.Error("Failed to decode",
|
2023-08-13 14:48:27 +02:00
|
|
|
slog.String("error", err.Error()))
|
2023-02-12 03:04:17 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
var sb silenceBody
|
|
|
|
err = json.Unmarshal(b, &sb)
|
|
|
|
if err != nil {
|
2023-08-13 15:16:26 +02:00
|
|
|
logger.Error("Failed to unmarshal",
|
2023-08-13 14:48:27 +02:00
|
|
|
slog.String("error", err.Error()))
|
2023-02-12 03:04:17 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
var matchers []matcher
|
|
|
|
for key, value := range sb.Labels {
|
|
|
|
m := matcher{
|
|
|
|
Name: key,
|
2023-02-12 14:40:32 +01:00
|
|
|
Value: value,
|
2023-02-12 03:04:17 +01:00
|
|
|
IsRegex: false,
|
|
|
|
IsEqual: true,
|
|
|
|
}
|
|
|
|
|
|
|
|
matchers = append(matchers, m)
|
|
|
|
}
|
|
|
|
|
|
|
|
silence := &silence{
|
|
|
|
StartsAt: time.Now().UTC().Format(dateLayout),
|
2023-07-12 14:56:48 +02:00
|
|
|
EndsAt: time.Now().Add(br.cfg.Am.SilenceDuration).UTC().Format(dateLayout),
|
2023-02-12 03:04:17 +01:00
|
|
|
CreatedBy: "ntfy-alertmanager",
|
|
|
|
Comment: "",
|
|
|
|
Matchers: matchers,
|
|
|
|
}
|
|
|
|
|
|
|
|
b, err = json.Marshal(silence)
|
|
|
|
if err != nil {
|
2023-08-13 15:16:26 +02:00
|
|
|
logger.Error("Failed to marshal",
|
2023-08-13 14:48:27 +02:00
|
|
|
slog.String("error", err.Error()))
|
2023-02-12 03:04:17 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-02-12 14:58:37 +01:00
|
|
|
url := sb.AlertManagerURL
|
2023-07-12 14:56:48 +02:00
|
|
|
if br.cfg.Am.URL != "" {
|
|
|
|
url = br.cfg.Am.URL
|
2023-02-12 14:58:37 +01:00
|
|
|
}
|
|
|
|
url += "/api/v2/silences"
|
|
|
|
|
2023-02-12 03:04:17 +01:00
|
|
|
req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(b))
|
|
|
|
if err != nil {
|
2023-08-13 15:16:26 +02:00
|
|
|
logger.Error("Failed to create request",
|
2023-08-13 14:48:27 +02:00
|
|
|
slog.String("error", err.Error()))
|
2023-02-12 03:04:17 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-02-12 14:15:02 +01:00
|
|
|
// Basic auth
|
2023-07-12 14:56:48 +02:00
|
|
|
if br.cfg.Am.User != "" && br.cfg.Am.Password != "" {
|
|
|
|
req.SetBasicAuth(br.cfg.Am.User, br.cfg.Am.Password)
|
2023-02-12 14:15:02 +01:00
|
|
|
}
|
|
|
|
|
2023-02-12 03:04:17 +01:00
|
|
|
req.Header.Add("Content-Type", "application/json")
|
2023-02-21 13:57:21 +01:00
|
|
|
resp, err := br.client.Do(req)
|
2023-02-12 03:04:17 +01:00
|
|
|
if err != nil {
|
2023-08-13 15:16:26 +02:00
|
|
|
logger.Error("Failed to POST request",
|
2023-08-13 14:48:27 +02:00
|
|
|
slog.String("error", err.Error()))
|
2023-02-12 03:04:17 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
b, err = io.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
2023-08-13 15:16:26 +02:00
|
|
|
logger.Error("Failed to read response body",
|
2023-08-13 14:48:27 +02:00
|
|
|
slog.String("error", err.Error()))
|
2023-02-12 03:04:17 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
2023-08-13 15:16:26 +02:00
|
|
|
logger.Error("Received non-200 status code",
|
2023-08-13 14:48:27 +02:00
|
|
|
slog.Int("status", resp.StatusCode))
|
2023-02-12 03:04:17 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-02-12 16:07:49 +01:00
|
|
|
var id silenceResponse
|
|
|
|
if err := json.Unmarshal(b, &id); err != nil {
|
2023-08-13 15:16:26 +02:00
|
|
|
logger.Error("Failed to unmarshal response",
|
2023-08-13 14:48:27 +02:00
|
|
|
slog.String("error", err.Error()))
|
2023-02-12 16:07:49 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-08-13 15:16:26 +02:00
|
|
|
logger.Info("Created new silence",
|
2023-08-13 14:48:27 +02:00
|
|
|
slog.String("ID", id.ID))
|
2023-02-12 03:04:17 +01:00
|
|
|
}
|