Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions pkg/alert/alerts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package alert

import (
"context"
"encoding/json"
"fmt"
"time"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/rest"

routev1 "github.com/openshift/api/route/v1"
routev1client "github.com/openshift/client-go/route/clientset/versioned/typed/route/v1"
)

type DataAndStatus struct {
Status string `json:"status"`
Data Data `json:"data"`
}

type Data struct {
Alerts []Alert `json:"alerts"`
}

type Alert struct {
Labels AlertLabels `json:"labels,omitempty"`
Annotations AlertAnnotations `json:"annotations,omitempty"`
State string `json:"state,omitempty"`
Value string `json:"value,omitempty"`
ActiveAt time.Time `json:"activeAt,omitempty"`
PartialResponseStrategy string `json:"partialResponseStrategy,omitempty"`
}

type AlertLabels struct {
AlertName string `json:"alertname,omitempty"`
Name string `json:"name,omitempty"`
Namespace string `json:"namespace,omitempty"`
PodDisruptionBudget string `json:"poddisruptionbudget,omitempty"`
Reason string `json:"reason,omitempty"`
Severity string `json:"severity,omitempty"`
}

type AlertAnnotations struct {
Description string `json:"description,omitempty"`
Summary string `json:"summary,omitempty"`
Runbook string `json:"runbook_url,omitempty"`
Message string `json:"message,omitempty"`
}

type Getter interface {
Get(ctx context.Context) (*DataAndStatus, error)
}

func NewAlertGetterOrDie(c *rest.Config) Getter {
client := routev1client.NewForConfigOrDie(c)
return &ocAlertGetter{config: c, routeClient: client}
}

type ocAlertGetter struct {
config *rest.Config
routeClient *routev1client.RouteV1Client
}

func (o *ocAlertGetter) Get(ctx context.Context) (*DataAndStatus, error) {
roundTripper, err := rest.TransportFor(o.config)
if err != nil {
return nil, fmt.Errorf("failed to create roundtripper: %w", err)
}

routeGetter := func(ctx context.Context, namespace string, name string, opts metav1.GetOptions) (*routev1.Route, error) {
return o.routeClient.Routes(namespace).Get(ctx, name, opts)
}
alertsBytes, err := GetAlerts(ctx, roundTripper, routeGetter)
if err != nil {
return nil, fmt.Errorf("failed to get alerts: %w", err)
}
var ret DataAndStatus
if err := json.Unmarshal(alertsBytes, &ret); err != nil {
return nil, fmt.Errorf("parsing alerts: %w", err)
}
return &ret, nil
}
134 changes: 134 additions & 0 deletions pkg/alert/inspectalerts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package alert

import (
"bytes"
"context"
"encoding/hex"
"fmt"
"io"
"net/http"
"net/url"
"time"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/errors"
"k8s.io/klog/v2"

routev1 "github.com/openshift/api/route/v1"
)

// RouteGetter is a function that gets a Route.
type RouteGetter func(ctx context.Context, namespace string, name string, opts metav1.GetOptions) (*routev1.Route, error)

// GetAlerts gets alerts (both firing and pending) from openshift-monitoring Thanos.
func GetAlerts(ctx context.Context, roundTripper http.RoundTripper, getRoute RouteGetter) ([]byte, error) {
uri := &url.URL{ // configure everything except Host, which will come from the Route
Scheme: "https",
Path: "/api/v1/alerts",
}

// if we end up going this way, probably port to github.com/prometheus/client_golang/api/prometheus/v1 NewAPI
alertBytes, err := getWithRoundTripper(ctx, roundTripper, getRoute, "openshift-monitoring", "thanos-querier", uri)
if err != nil {
return alertBytes, fmt.Errorf("failed to get alerts from Thanos: %w", err)
}

// if we end up going this way, probably check and error on 'result' being an empty set (it should at least contain Watchdog)

return alertBytes, nil
}

// getWithRoundTripper gets a Route by namespace/name, constructs a URI using
// status.ingress[].host and the path argument, and performs GETs on that
// URI.
func getWithRoundTripper(ctx context.Context, roundTripper http.RoundTripper, getRoute RouteGetter, namespace, name string, baseURI *url.URL) ([]byte, error) {
route, err := getRoute(ctx, namespace, name, metav1.GetOptions{})
if err != nil {
return nil, err
}

client := &http.Client{Transport: roundTripper, Timeout: 15 * time.Second}
errs := make([]error, 0, len(route.Status.Ingress))
for _, ingress := range route.Status.Ingress {
uri := *baseURI
uri.Host = ingress.Host
content, err := checkedGet(ctx, uri, client)
if err == nil {
return content, nil
} else {
errs = append(errs, fmt.Errorf("%s->%w", ingress.Host, err))
}
}

if len(errs) == 1 {
return nil, fmt.Errorf("unable to get %s from URI in the %s/%s Route: %s", baseURI.Path, namespace, name, errors.NewAggregate(errs))
} else {
return nil, fmt.Errorf("unable to get %s from any of %d URIs in the %s/%s Route: %s", baseURI.Path, len(errs), namespace, name, errors.NewAggregate(errs))
}

}

func checkedGet(ctx context.Context, uri url.URL, client *http.Client) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, "GET", uri.String(), nil)
if err != nil {
return nil, err
}

resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer func() {
_ = resp.Body.Close()
}()

body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}

glogBody("Response Body", body)

if resp.StatusCode != http.StatusOK {
return body, fmt.Errorf("GET status code=%d", resp.StatusCode)
}

return body, nil
}

// glogBody and truncateBody taken from client-go Request
// https://github.com/openshift/oc/blob/4be3c8609f101a8c5867abc47bda33caae629113/vendor/k8s.io/client-go/rest/request.go#L1183-L1215

// truncateBody decides if the body should be truncated, based on the glog Verbosity.
func truncateBody(body string) string {
max := 0
switch {
case bool(klog.V(10).Enabled()):
return body
case bool(klog.V(9).Enabled()):
max = 10240
case bool(klog.V(8).Enabled()):
max = 1024
}

if len(body) <= max {
return body
}

return body[:max] + fmt.Sprintf(" [truncated %d chars]", len(body)-max)
}

// glogBody logs a body output that could be either JSON or protobuf. It explicitly guards against
// allocating a new string for the body output unless necessary. Uses a simple heuristic to determine
// whether the body is printable.
func glogBody(prefix string, body []byte) {
if klogV := klog.V(8); klogV.Enabled() {
if bytes.IndexFunc(body, func(r rune) bool {
return r < 0x0a
}) != -1 {
klogV.Infof("%s:\n%s", prefix, truncateBody(hex.Dump(body)))
} else {
klogV.Infof("%s: %s", prefix, truncateBody(string(body)))
}
}
}
Loading