Skip to content

Commit 2767365

Browse files
sradcoAI Assistant
andcommitted
management: add CRD support and create alert rule API
Add AlertingRule, AlertRelabelConfig, and RelabeledRules CRD interfaces with the management client, router, server wiring, and POST /api/v1/alerting/rules endpoint. Signed-off-by: Shirly Radco <sradco@redhat.com> Signed-off-by: João Vilaça <jvilaca@redhat.com> Signed-off-by: Aviv Litman <alitman@redhat.com> Co-authored-by: AI Assistant <noreply@cursor.com>
1 parent ba3697b commit 2767365

33 files changed

Lines changed: 2921 additions & 6 deletions

Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ RUN make install-backend
2525

2626
COPY cmd/ cmd/
2727
COPY pkg/ pkg/
28+
COPY internal/ internal/
2829

2930
ENV GOEXPERIMENT=strictfipsruntime
3031
ENV CGO_ENABLED=1

Dockerfile.dev

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ RUN go mod download
2828

2929
COPY cmd/ cmd/
3030
COPY pkg/ pkg/
31+
COPY internal/ internal/
3132

3233
RUN go build -mod=mod -o plugin-backend cmd/plugin-backend.go
3334

Dockerfile.dev-mcp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ RUN go mod download
3131

3232
COPY cmd/ cmd/
3333
COPY pkg/ pkg/
34+
COPY internal/ internal/
3435

3536
RUN go build -mod=mod -o plugin-backend cmd/plugin-backend.go
3637

Dockerfile.devspace

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ RUN make install-backend
2020
COPY config/ config/
2121
COPY cmd/ cmd/
2222
COPY pkg/ pkg/
23+
COPY internal/ internal/
2324

2425
RUN make build-backend
2526

Dockerfile.konflux

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ RUN make install-backend
2828

2929
COPY cmd/ cmd/
3030
COPY pkg/ pkg/
31+
COPY internal/ internal/
3132

3233
ENV GOEXPERIMENT=strictfipsruntime
3334
ENV CGO_ENABLED=1

Dockerfile.mcp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ RUN make install-backend
2828

2929
COPY cmd/ cmd/
3030
COPY pkg/ pkg/
31+
COPY internal/ internal/
3132

3233
ENV GOOS=${TARGETOS:-linux}
3334
ENV GOARCH=${TARGETARCH}

Makefile

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ lint-frontend:
4141
lint-backend:
4242
go mod tidy
4343
go fmt ./cmd/
44-
go fmt ./pkg/
44+
go fmt ./pkg/... ./internal/...
4545

4646
.PHONY: install-backend
4747
install-backend:
@@ -57,7 +57,11 @@ start-backend:
5757

5858
.PHONY: test-backend
5959
test-backend:
60-
go test ./pkg/... -v
60+
go test ./pkg/... ./internal/... -v
61+
62+
.PHONY: test-e2e
63+
test-e2e:
64+
PLUGIN_URL=http://localhost:9001 go test -v -timeout=150m -count=1 ./test/e2e
6165

6266
.PHONY: build-image
6367
build-image:
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package managementrouter
2+
3+
import (
4+
"encoding/json"
5+
"net/http"
6+
7+
monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1"
8+
9+
"github.com/openshift/monitoring-plugin/pkg/management"
10+
)
11+
12+
type CreateAlertRuleRequest struct {
13+
AlertingRule *monitoringv1.Rule `json:"alertingRule,omitempty"`
14+
PrometheusRule *management.PrometheusRuleOptions `json:"prometheusRule,omitempty"`
15+
}
16+
17+
type CreateAlertRuleResponse struct {
18+
Id string `json:"id"`
19+
}
20+
21+
func (hr *httpRouter) CreateAlertRule(w http.ResponseWriter, req *http.Request) {
22+
var payload CreateAlertRuleRequest
23+
if err := json.NewDecoder(req.Body).Decode(&payload); err != nil {
24+
writeError(w, http.StatusBadRequest, "invalid request body")
25+
return
26+
}
27+
28+
if payload.AlertingRule == nil {
29+
writeError(w, http.StatusBadRequest, "alertingRule is required")
30+
return
31+
}
32+
33+
alertRule := *payload.AlertingRule
34+
35+
var (
36+
id string
37+
err error
38+
)
39+
40+
if payload.PrometheusRule != nil {
41+
id, err = hr.managementClient.CreateUserDefinedAlertRule(req.Context(), alertRule, *payload.PrometheusRule)
42+
} else {
43+
id, err = hr.managementClient.CreatePlatformAlertRule(req.Context(), alertRule)
44+
}
45+
46+
if err != nil {
47+
handleError(w, err)
48+
return
49+
}
50+
51+
w.Header().Set("Content-Type", "application/json")
52+
w.WriteHeader(http.StatusCreated)
53+
_ = json.NewEncoder(w).Encode(CreateAlertRuleResponse{Id: id})
54+
}
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
package managementrouter_test
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"net/http"
8+
"net/http/httptest"
9+
10+
. "github.com/onsi/ginkgo/v2"
11+
. "github.com/onsi/gomega"
12+
13+
monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1"
14+
15+
"github.com/openshift/monitoring-plugin/internal/managementrouter"
16+
"github.com/openshift/monitoring-plugin/pkg/k8s"
17+
"github.com/openshift/monitoring-plugin/pkg/management"
18+
"github.com/openshift/monitoring-plugin/pkg/management/testutils"
19+
)
20+
21+
var _ = Describe("CreateAlertRule", func() {
22+
var (
23+
router http.Handler
24+
mockK8sRules *testutils.MockPrometheusRuleInterface
25+
mockARules *testutils.MockAlertingRuleInterface
26+
mockK8s *testutils.MockClient
27+
)
28+
29+
BeforeEach(func() {
30+
mockK8sRules = &testutils.MockPrometheusRuleInterface{}
31+
mockARules = &testutils.MockAlertingRuleInterface{}
32+
mockK8s = &testutils.MockClient{
33+
PrometheusRulesFunc: func() k8s.PrometheusRuleInterface {
34+
return mockK8sRules
35+
},
36+
AlertingRulesFunc: func() k8s.AlertingRuleInterface {
37+
return mockARules
38+
},
39+
NamespaceFunc: func() k8s.NamespaceInterface {
40+
return &testutils.MockNamespaceInterface{
41+
IsClusterMonitoringNamespaceFunc: func(name string) bool {
42+
return false
43+
},
44+
}
45+
},
46+
}
47+
})
48+
49+
Context("create new user defined alert rule", func() {
50+
It("creates a new rule", func() {
51+
mgmt := management.New(context.Background(), mockK8s)
52+
router = managementrouter.New(mgmt)
53+
54+
body := map[string]interface{}{
55+
"alertingRule": map[string]interface{}{
56+
"alert": "cpuHigh",
57+
"expr": "vector(1)",
58+
"for": "5m",
59+
"labels": map[string]string{"severity": "warning"},
60+
"annotations": map[string]string{"summary": "cpu high"},
61+
},
62+
"prometheusRule": map[string]interface{}{
63+
"prometheusRuleName": "user-pr",
64+
"prometheusRuleNamespace": "default",
65+
},
66+
}
67+
buf, _ := json.Marshal(body)
68+
69+
req := httptest.NewRequest(http.MethodPost, "/api/v1/alerting/rules", bytes.NewReader(buf))
70+
req.Header.Set("Content-Type", "application/json")
71+
w := httptest.NewRecorder()
72+
router.ServeHTTP(w, req)
73+
74+
Expect(w.Code).To(Equal(http.StatusCreated))
75+
var resp struct {
76+
Id string `json:"id"`
77+
}
78+
Expect(json.NewDecoder(w.Body).Decode(&resp)).To(Succeed())
79+
Expect(resp.Id).NotTo(BeEmpty())
80+
81+
pr, found, err := mockK8sRules.Get(context.Background(), "default", "user-pr")
82+
Expect(err).NotTo(HaveOccurred())
83+
Expect(found).To(BeTrue())
84+
allAlerts := []string{}
85+
for _, g := range pr.Spec.Groups {
86+
for _, r := range g.Rules {
87+
allAlerts = append(allAlerts, r.Alert)
88+
}
89+
}
90+
Expect(allAlerts).To(ContainElement("cpuHigh"))
91+
})
92+
93+
It("creates a new rule into a non-default group when groupName is provided", func() {
94+
mgmt := management.New(context.Background(), mockK8s)
95+
router = managementrouter.New(mgmt)
96+
97+
body := map[string]interface{}{
98+
"alertingRule": map[string]interface{}{
99+
"alert": "cpuCustomGroup",
100+
"expr": "vector(1)",
101+
},
102+
"prometheusRule": map[string]interface{}{
103+
"prometheusRuleName": "user-pr",
104+
"prometheusRuleNamespace": "default",
105+
"groupName": "custom-group",
106+
},
107+
}
108+
buf, _ := json.Marshal(body)
109+
110+
req := httptest.NewRequest(http.MethodPost, "/api/v1/alerting/rules", bytes.NewReader(buf))
111+
req.Header.Set("Content-Type", "application/json")
112+
w := httptest.NewRecorder()
113+
router.ServeHTTP(w, req)
114+
115+
Expect(w.Code).To(Equal(http.StatusCreated))
116+
117+
pr, found, err := mockK8sRules.Get(context.Background(), "default", "user-pr")
118+
Expect(err).NotTo(HaveOccurred())
119+
Expect(found).To(BeTrue())
120+
121+
var grp *monitoringv1.RuleGroup
122+
for i := range pr.Spec.Groups {
123+
if pr.Spec.Groups[i].Name == "custom-group" {
124+
grp = &pr.Spec.Groups[i]
125+
break
126+
}
127+
}
128+
Expect(grp).NotTo(BeNil())
129+
alerts := []string{}
130+
for _, r := range grp.Rules {
131+
alerts = append(alerts, r.Alert)
132+
}
133+
Expect(alerts).To(ContainElement("cpuCustomGroup"))
134+
})
135+
})
136+
137+
Context("invalid JSON body", func() {
138+
It("fails for invalid JSON", func() {
139+
mgmt := management.New(context.Background(), mockK8s)
140+
router = managementrouter.New(mgmt)
141+
142+
req := httptest.NewRequest(http.MethodPost, "/api/v1/alerting/rules", bytes.NewBufferString("{"))
143+
req.Header.Set("Content-Type", "application/json")
144+
w := httptest.NewRecorder()
145+
router.ServeHTTP(w, req)
146+
147+
Expect(w.Code).To(Equal(http.StatusBadRequest))
148+
Expect(w.Body.String()).To(ContainSubstring("invalid request body"))
149+
})
150+
})
151+
152+
Context("missing target PrometheusRule (name/namespace)", func() {
153+
It("fails for missing target PR", func() {
154+
mgmt := management.New(context.Background(), mockK8s)
155+
router = managementrouter.New(mgmt)
156+
157+
body := map[string]interface{}{
158+
"alertingRule": map[string]interface{}{
159+
"alert": "x",
160+
"expr": "vector(1)",
161+
},
162+
"prometheusRule": map[string]interface{}{
163+
// missing PR name/namespace
164+
},
165+
}
166+
buf, _ := json.Marshal(body)
167+
168+
req := httptest.NewRequest(http.MethodPost, "/api/v1/alerting/rules", bytes.NewReader(buf))
169+
req.Header.Set("Content-Type", "application/json")
170+
w := httptest.NewRecorder()
171+
router.ServeHTTP(w, req)
172+
173+
Expect(w.Code).To(Equal(http.StatusBadRequest))
174+
Expect(w.Body.String()).To(ContainSubstring("PrometheusRule Name and Namespace must be specified"))
175+
})
176+
})
177+
178+
Context("target is platform-managed PR", func() {
179+
It("rejects with MethodNotAllowed", func() {
180+
mockNamespace := &testutils.MockNamespaceInterface{
181+
IsClusterMonitoringNamespaceFunc: func(name string) bool {
182+
return name == "openshift-monitoring"
183+
},
184+
}
185+
mockK8s.NamespaceFunc = func() k8s.NamespaceInterface {
186+
return mockNamespace
187+
}
188+
mgmt := management.New(context.Background(), mockK8s)
189+
router = managementrouter.New(mgmt)
190+
191+
body := map[string]interface{}{
192+
"alertingRule": map[string]interface{}{
193+
"alert": "x",
194+
"expr": "vector(1)",
195+
},
196+
"prometheusRule": map[string]interface{}{
197+
"prometheusRuleName": "platform-pr",
198+
"prometheusRuleNamespace": "openshift-monitoring",
199+
},
200+
}
201+
buf, _ := json.Marshal(body)
202+
203+
req := httptest.NewRequest(http.MethodPost, "/api/v1/alerting/rules", bytes.NewReader(buf))
204+
req.Header.Set("Content-Type", "application/json")
205+
w := httptest.NewRecorder()
206+
router.ServeHTTP(w, req)
207+
208+
Expect(w.Code).To(Equal(http.StatusMethodNotAllowed))
209+
Expect(w.Body.String()).To(ContainSubstring("cannot add user-defined alert rule to a platform-managed PrometheusRule"))
210+
})
211+
})
212+
})
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package managementrouter_test
2+
3+
import (
4+
"testing"
5+
6+
. "github.com/onsi/ginkgo/v2"
7+
. "github.com/onsi/gomega"
8+
)
9+
10+
func TestHTTPRouter(t *testing.T) {
11+
RegisterFailHandler(Fail)
12+
RunSpecs(t, "HTTPRouter Suite")
13+
}

0 commit comments

Comments
 (0)