238 lines
7.0 KiB
Go
238 lines
7.0 KiB
Go
package client
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"path"
|
|
"strings"
|
|
|
|
alertingNotify "github.com/grafana/alerting/notify"
|
|
"github.com/grafana/grafana/pkg/infra/log"
|
|
"github.com/grafana/grafana/pkg/infra/tracing"
|
|
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
|
"github.com/grafana/grafana/pkg/services/ngalert/client"
|
|
"github.com/grafana/grafana/pkg/services/ngalert/metrics"
|
|
)
|
|
|
|
// MimirClient contains all the methods to query the migration critical endpoints of Mimir instance, it's an interface to allow multiple implementations.
|
|
type MimirClient interface {
|
|
GetGrafanaAlertmanagerState(ctx context.Context) (*UserGrafanaState, error)
|
|
CreateGrafanaAlertmanagerState(ctx context.Context, state string) error
|
|
DeleteGrafanaAlertmanagerState(ctx context.Context) error
|
|
|
|
GetGrafanaAlertmanagerConfig(ctx context.Context) (*UserGrafanaConfig, error)
|
|
CreateGrafanaAlertmanagerConfig(ctx context.Context, configuration *apimodels.PostableUserConfig, hash string, createdAt int64, isDefault bool) error
|
|
DeleteGrafanaAlertmanagerConfig(ctx context.Context) error
|
|
|
|
TestTemplate(ctx context.Context, c alertingNotify.TestTemplatesConfigBodyParams) (*alertingNotify.TestTemplatesResults, error)
|
|
TestReceivers(ctx context.Context, c alertingNotify.TestReceiversConfigBodyParams) (*alertingNotify.TestReceiversResult, int, error)
|
|
|
|
ShouldPromoteConfig() bool
|
|
|
|
// Mimir implements an extended version of the receivers API under a different path.
|
|
GetReceivers(ctx context.Context) ([]apimodels.Receiver, error)
|
|
}
|
|
|
|
type Mimir struct {
|
|
client client.Requester
|
|
endpoint *url.URL
|
|
logger log.Logger
|
|
metrics *metrics.RemoteAlertmanager
|
|
promoteConfig bool
|
|
externalURL string
|
|
staticHeaders map[string]string
|
|
}
|
|
|
|
type Config struct {
|
|
URL *url.URL
|
|
TenantID string
|
|
Password string
|
|
|
|
Logger log.Logger
|
|
PromoteConfig bool
|
|
ExternalURL string
|
|
StaticHeaders map[string]string
|
|
}
|
|
|
|
// successResponse represents a successful response from the Mimir API.
|
|
type successResponse struct {
|
|
Status string `json:"status"`
|
|
Data any `json:"data"`
|
|
}
|
|
|
|
// errorResponse represents an error from the Mimir API.
|
|
type errorResponse struct {
|
|
Status string `json:"status"`
|
|
Error1 string `json:"error"`
|
|
Error2 string `json:"Error"`
|
|
}
|
|
|
|
func (e *errorResponse) Error() string {
|
|
if e.Error1 != "" {
|
|
return e.Error1
|
|
}
|
|
|
|
return e.Error2
|
|
}
|
|
|
|
func New(cfg *Config, metrics *metrics.RemoteAlertmanager, tracer tracing.Tracer) (*Mimir, error) {
|
|
rt := &MimirAuthRoundTripper{
|
|
TenantID: cfg.TenantID,
|
|
Password: cfg.Password,
|
|
Next: http.DefaultTransport,
|
|
}
|
|
|
|
c := &http.Client{
|
|
Transport: rt,
|
|
}
|
|
tc := client.NewTimedClient(c, metrics.RequestLatency)
|
|
trc := client.NewTracedClient(tc, tracer, "remote.alertmanager.client")
|
|
|
|
return &Mimir{
|
|
endpoint: cfg.URL,
|
|
client: trc,
|
|
logger: cfg.Logger,
|
|
metrics: metrics,
|
|
promoteConfig: cfg.PromoteConfig,
|
|
externalURL: cfg.ExternalURL,
|
|
staticHeaders: cfg.StaticHeaders,
|
|
}, nil
|
|
}
|
|
|
|
// do execute an HTTP requests against the specified path and method using the specified payload.
|
|
// It returns the HTTP response.
|
|
func (mc *Mimir) do(ctx context.Context, p, method string, payload io.Reader, out any) (*http.Response, error) {
|
|
pathURL, err := url.Parse(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
endpoint := *mc.endpoint
|
|
endpoint.Path = path.Join(endpoint.Path, pathURL.Path)
|
|
|
|
r, err := http.NewRequestWithContext(ctx, method, endpoint.String(), payload)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
r.Header.Set("Accept", "application/json")
|
|
r.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := mc.client.Do(r)
|
|
if err != nil {
|
|
msg := "Unable to fulfill request to the Mimir API"
|
|
mc.logger.Error(msg, "err", err, "url", r.URL.String(), "method", r.Method)
|
|
return nil, fmt.Errorf("%s: %w", msg, err)
|
|
}
|
|
defer func() {
|
|
if err := resp.Body.Close(); err != nil {
|
|
mc.logger.Error("Error closing HTTP body", "err", err, "url", r.URL.String(), "method", r.Method)
|
|
}
|
|
}()
|
|
|
|
ct := resp.Header.Get("Content-Type")
|
|
if !strings.HasPrefix(ct, "application/json") {
|
|
msg := "Response content-type is not application/json"
|
|
mc.logger.Error(msg, "content-type", "url", r.URL.String(), "method", r.Method, ct, "status", resp.StatusCode)
|
|
return nil, fmt.Errorf("%s: %s", msg, ct)
|
|
}
|
|
|
|
if out == nil {
|
|
return resp, nil
|
|
}
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
msg := "Failed to read the request body"
|
|
mc.logger.Error(msg, "err", err, "url", r.URL.String(), "method", r.Method, "status", resp.StatusCode)
|
|
return nil, fmt.Errorf("%s: %w", msg, err)
|
|
}
|
|
|
|
if resp.StatusCode/100 != 2 {
|
|
errResponse := &errorResponse{}
|
|
err = json.Unmarshal(body, errResponse)
|
|
|
|
if err == nil && errResponse.Error() != "" {
|
|
msg := "Error response from the Mimir API"
|
|
mc.logger.Error(msg, "err", errResponse, "url", r.URL.String(), "method", r.Method, "status", resp.StatusCode)
|
|
return nil, fmt.Errorf("%s: %w", msg, errResponse)
|
|
}
|
|
|
|
msg := "Failed to decode non-2xx JSON response"
|
|
mc.logger.Error(msg, "err", err, "url", r.URL.String(), "method", r.Method, "status", resp.StatusCode)
|
|
return nil, fmt.Errorf("%s: %w", msg, err)
|
|
}
|
|
|
|
if err = json.Unmarshal(body, out); err != nil {
|
|
msg := "Failed to decode 2xx JSON response"
|
|
mc.logger.Error(msg, "err", err, "url", r.URL.String(), "method", r.Method, "status", resp.StatusCode)
|
|
return nil, fmt.Errorf("%s: %w", msg, err)
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
func (mc *Mimir) doOK(ctx context.Context, p, method string, payload io.Reader) error {
|
|
var sr successResponse
|
|
resp, err := mc.do(ctx, p, method, payload, &sr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
if err := resp.Body.Close(); err != nil {
|
|
mc.logger.Error("Error closing HTTP body", "err", err)
|
|
}
|
|
}()
|
|
|
|
switch sr.Status {
|
|
case "success":
|
|
return nil
|
|
case "error":
|
|
return errors.New("received an 2xx status code but the request body reflected an error")
|
|
default:
|
|
return fmt.Errorf("received an unknown status from the request body: %s", sr.Status)
|
|
}
|
|
}
|
|
|
|
func (mc *Mimir) TestReceivers(ctx context.Context, c alertingNotify.TestReceiversConfigBodyParams) (*alertingNotify.TestReceiversResult, int, error) {
|
|
payload, err := json.Marshal(c)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
trResult := &alertingNotify.TestReceiversResult{}
|
|
|
|
// nolint:bodyclose
|
|
// closed within `do`
|
|
_, err = mc.do(ctx, "api/v1/grafana/receivers/test", http.MethodPost, bytes.NewBuffer(payload), &trResult)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
return trResult, http.StatusOK, nil
|
|
}
|
|
|
|
func (mc *Mimir) TestTemplate(ctx context.Context, c alertingNotify.TestTemplatesConfigBodyParams) (*alertingNotify.TestTemplatesResults, error) {
|
|
payload, err := json.Marshal(c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ttResult := &alertingNotify.TestTemplatesResults{}
|
|
|
|
// nolint:bodyclose
|
|
// closed within `do`
|
|
_, err = mc.do(ctx, "api/v1/grafana/templates/test", http.MethodPost, bytes.NewBuffer(payload), &ttResult)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return ttResult, nil
|
|
}
|