Skip to content

Commit 6efe6cd

Browse files
committed
OTA-1813: Populate risks from alerts
1 parent 7413293 commit 6efe6cd

12 files changed

Lines changed: 907 additions & 48 deletions

File tree

pkg/alert/alerts.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package alert
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"time"
8+
9+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10+
"k8s.io/client-go/rest"
11+
12+
routev1 "github.com/openshift/api/route/v1"
13+
routev1client "github.com/openshift/client-go/route/clientset/versioned/typed/route/v1"
14+
)
15+
16+
type DataAndStatus struct {
17+
Status string `json:"status"`
18+
Data Data `json:"data"`
19+
}
20+
21+
type Data struct {
22+
Alerts []Alert `json:"alerts"`
23+
}
24+
25+
type Alert struct {
26+
Labels AlertLabels `json:"labels,omitempty"`
27+
Annotations AlertAnnotations `json:"annotations,omitempty"`
28+
State string `json:"state,omitempty"`
29+
Value string `json:"value,omitempty"`
30+
ActiveAt time.Time `json:"activeAt,omitempty"`
31+
PartialResponseStrategy string `json:"partialResponseStrategy,omitempty"`
32+
}
33+
34+
type AlertLabels struct {
35+
AlertName string `json:"alertname,omitempty"`
36+
Name string `json:"name,omitempty"`
37+
Namespace string `json:"namespace,omitempty"`
38+
PodDisruptionBudget string `json:"poddisruptionbudget,omitempty"`
39+
Reason string `json:"reason,omitempty"`
40+
Severity string `json:"severity,omitempty"`
41+
}
42+
43+
type AlertAnnotations struct {
44+
Description string `json:"description,omitempty"`
45+
Summary string `json:"summary,omitempty"`
46+
Runbook string `json:"runbook_url,omitempty"`
47+
Message string `json:"message,omitempty"`
48+
}
49+
50+
type Getter interface {
51+
Get(ctx context.Context) (*DataAndStatus, error)
52+
}
53+
54+
func NewAlertGetterOrDie(c *rest.Config) Getter {
55+
client := routev1client.NewForConfigOrDie(c)
56+
return &ocAlertGetter{config: c, routeClient: client}
57+
}
58+
59+
type ocAlertGetter struct {
60+
config *rest.Config
61+
routeClient *routev1client.RouteV1Client
62+
}
63+
64+
func (o *ocAlertGetter) Get(ctx context.Context) (*DataAndStatus, error) {
65+
roundTripper, err := rest.TransportFor(o.config)
66+
if err != nil {
67+
return nil, fmt.Errorf("failed to create roundtripper: %w", err)
68+
}
69+
70+
routeGetter := func(ctx context.Context, namespace string, name string, opts metav1.GetOptions) (*routev1.Route, error) {
71+
return o.routeClient.Routes(namespace).Get(ctx, name, opts)
72+
}
73+
alertsBytes, err := GetAlerts(ctx, roundTripper, routeGetter)
74+
if err != nil {
75+
return nil, fmt.Errorf("failed to get alerts: %w", err)
76+
}
77+
var ret DataAndStatus
78+
if err := json.Unmarshal(alertsBytes, &ret); err != nil {
79+
return nil, fmt.Errorf("parsing alerts: %w", err)
80+
}
81+
return &ret, nil
82+
}

pkg/alert/inspectalerts.go

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package alert
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/hex"
7+
"fmt"
8+
"io"
9+
"net/http"
10+
"net/url"
11+
12+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
13+
"k8s.io/apimachinery/pkg/util/errors"
14+
"k8s.io/klog/v2"
15+
16+
routev1 "github.com/openshift/api/route/v1"
17+
)
18+
19+
// RouteGetter is a function that gets a Route.
20+
type RouteGetter func(ctx context.Context, namespace string, name string, opts metav1.GetOptions) (*routev1.Route, error)
21+
22+
// GetAlerts gets alerts (both firing and pending) from openshift-monitoring Thanos.
23+
func GetAlerts(ctx context.Context, roundTripper http.RoundTripper, getRoute RouteGetter) ([]byte, error) {
24+
uri := &url.URL{ // configure everything except Host, which will come from the Route
25+
Scheme: "https",
26+
Path: "/api/v1/alerts",
27+
}
28+
29+
// if we end up going this way, probably port to github.com/prometheus/client_golang/api/prometheus/v1 NewAPI
30+
alertBytes, err := getWithRoundTripper(ctx, roundTripper, getRoute, "openshift-monitoring", "thanos-querier", uri)
31+
if err != nil {
32+
return alertBytes, fmt.Errorf("failed to get alerts from Thanos: %w", err)
33+
}
34+
35+
// if we end up going this way, probably check and error on 'result' being an empty set (it should at least contain Watchdog)
36+
37+
return alertBytes, nil
38+
}
39+
40+
// getWithRoundTripper gets a Route by namespace/name, constructs a URI using
41+
// status.ingress[].host and the path argument, and performs GETs on that
42+
// URI.
43+
func getWithRoundTripper(ctx context.Context, roundTripper http.RoundTripper, getRoute RouteGetter, namespace, name string, baseURI *url.URL) ([]byte, error) {
44+
route, err := getRoute(ctx, namespace, name, metav1.GetOptions{})
45+
if err != nil {
46+
return nil, err
47+
}
48+
49+
client := &http.Client{Transport: roundTripper}
50+
errs := make([]error, 0, len(route.Status.Ingress))
51+
for _, ingress := range route.Status.Ingress {
52+
uri := *baseURI
53+
uri.Host = ingress.Host
54+
content, err := checkedGet(uri, client)
55+
if err == nil {
56+
return content, nil
57+
} else {
58+
errs = append(errs, fmt.Errorf("%s->%w", ingress.Host, err))
59+
}
60+
}
61+
62+
if len(errs) == 1 {
63+
return nil, fmt.Errorf("unable to get %s from URI in the %s/%s Route: %s", baseURI.Path, namespace, name, errors.NewAggregate(errs))
64+
} else {
65+
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))
66+
}
67+
68+
}
69+
70+
func checkedGet(uri url.URL, client *http.Client) ([]byte, error) {
71+
req, err := http.NewRequest("GET", uri.String(), nil)
72+
if err != nil {
73+
return nil, err
74+
}
75+
76+
resp, err := client.Do(req)
77+
if err != nil {
78+
return nil, err
79+
}
80+
defer resp.Body.Close()
81+
82+
body, err := io.ReadAll(resp.Body)
83+
if err != nil {
84+
return nil, err
85+
}
86+
87+
glogBody("Response Body", body)
88+
89+
if resp.StatusCode != http.StatusOK {
90+
return body, fmt.Errorf("GET status code=%d", resp.StatusCode)
91+
}
92+
93+
return body, nil
94+
}
95+
96+
// glogBody and truncateBody taken from client-go Request
97+
// https://github.com/openshift/oc/blob/4be3c8609f101a8c5867abc47bda33caae629113/vendor/k8s.io/client-go/rest/request.go#L1183-L1215
98+
99+
// truncateBody decides if the body should be truncated, based on the glog Verbosity.
100+
func truncateBody(body string) string {
101+
max := 0
102+
switch {
103+
case bool(klog.V(10).Enabled()):
104+
return body
105+
case bool(klog.V(9).Enabled()):
106+
max = 10240
107+
case bool(klog.V(8).Enabled()):
108+
max = 1024
109+
}
110+
111+
if len(body) <= max {
112+
return body
113+
}
114+
115+
return body[:max] + fmt.Sprintf(" [truncated %d chars]", len(body)-max)
116+
}
117+
118+
// glogBody logs a body output that could be either JSON or protobuf. It explicitly guards against
119+
// allocating a new string for the body output unless necessary. Uses a simple heuristic to determine
120+
// whether the body is printable.
121+
func glogBody(prefix string, body []byte) {
122+
if klogV := klog.V(8); klogV.Enabled() {
123+
if bytes.IndexFunc(body, func(r rune) bool {
124+
return r < 0x0a
125+
}) != -1 {
126+
klogV.Infof("%s:\n%s", prefix, truncateBody(hex.Dump(body)))
127+
} else {
128+
klogV.Infof("%s: %s", prefix, truncateBody(string(body)))
129+
}
130+
}
131+
}

0 commit comments

Comments
 (0)