mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
* Alerting: Introduce a Mimir client as part of the Remote Alertmanager Mimir client that understands the new APIs developed for mimir. Very much a WIP still. * more wip * appease the linter * more linting * add more code * get state from kvstore, encode, send * send state to the remote Alertmanager, extract fullstate logic into its own function * pass kvstore to remote.NewAlertmanager() * refactor * add fake kvstore to tests * tests * use FileStore to get state * always log 'completed state upload' * refactor compareRemoteConfig * base64-encode the state in the file store * export silences and nflog filenames, refactor * log 'completed state/config upload...' regardless of outcome * add values to the state store in tests * address code review comments * log error from filestore --------- Co-authored-by: gotjosh <josue.abreu@gmail.com>
175 lines
4.7 KiB
Go
175 lines
4.7 KiB
Go
package client
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"path"
|
|
"strings"
|
|
|
|
"github.com/grafana/grafana/pkg/infra/log"
|
|
)
|
|
|
|
// 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, hash string, id, updatedAt int64, isDefault bool) error
|
|
DeleteGrafanaAlertmanagerConfig(ctx context.Context) error
|
|
}
|
|
|
|
type Mimir struct {
|
|
endpoint *url.URL
|
|
client http.Client
|
|
logger log.Logger
|
|
}
|
|
|
|
type Config struct {
|
|
URL *url.URL
|
|
TenantID string
|
|
Password string
|
|
|
|
Logger log.Logger
|
|
}
|
|
|
|
// 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) (*Mimir, error) {
|
|
rt := &MimirAuthRoundTripper{
|
|
TenantID: cfg.TenantID,
|
|
Password: cfg.Password,
|
|
Next: http.DefaultTransport,
|
|
}
|
|
|
|
c := http.Client{
|
|
Transport: rt,
|
|
}
|
|
|
|
return &Mimir{
|
|
endpoint: cfg.URL,
|
|
client: c,
|
|
logger: cfg.Logger,
|
|
}, 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)
|
|
}
|
|
}
|