Part 2 of adding version messages to the `alert_rule_version` able. This allows setting the message via a header when using the Prometheus conversion API, which can be useful for e.g. linking changes back to source control.
2361 lines
78 KiB
Go
2361 lines
78 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
amconfig "github.com/prometheus/alertmanager/config"
|
|
"github.com/prometheus/alertmanager/pkg/labels"
|
|
prommodel "github.com/prometheus/common/model"
|
|
"github.com/stretchr/testify/mock"
|
|
"github.com/stretchr/testify/require"
|
|
"go.yaml.in/yaml/v3"
|
|
|
|
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
|
"github.com/grafana/grafana/pkg/infra/log"
|
|
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
|
"github.com/grafana/grafana/pkg/services/datasources"
|
|
dsfakes "github.com/grafana/grafana/pkg/services/datasources/fakes"
|
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
|
"github.com/grafana/grafana/pkg/services/folder"
|
|
"github.com/grafana/grafana/pkg/services/folder/foldertest"
|
|
acfakes "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol/fakes"
|
|
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
|
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
|
"github.com/grafana/grafana/pkg/services/ngalert/provisioning"
|
|
"github.com/grafana/grafana/pkg/services/ngalert/store"
|
|
"github.com/grafana/grafana/pkg/services/ngalert/tests/fakes"
|
|
"github.com/grafana/grafana/pkg/services/user"
|
|
"github.com/grafana/grafana/pkg/setting"
|
|
"github.com/grafana/grafana/pkg/util"
|
|
"github.com/grafana/grafana/pkg/web"
|
|
)
|
|
|
|
const (
|
|
existingDSUID = "test-ds"
|
|
)
|
|
|
|
func TestRouteConvertPrometheusPostRuleGroup(t *testing.T) {
|
|
simpleGroup := apimodels.PrometheusRuleGroup{
|
|
Name: "Test Group",
|
|
Interval: prommodel.Duration(1 * time.Minute),
|
|
Rules: []apimodels.PrometheusRule{
|
|
{
|
|
Alert: "TestAlert",
|
|
Expr: "up == 0",
|
|
For: util.Pointer(prommodel.Duration(5 * time.Minute)),
|
|
Labels: map[string]string{
|
|
"severity": "critical",
|
|
},
|
|
},
|
|
{
|
|
Record: "recorded-metric",
|
|
Expr: "vector(1)",
|
|
Labels: map[string]string{
|
|
"severity": "warning",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
t.Run("without datasource UID header should return 400", func(t *testing.T) {
|
|
srv, _, _ := createConvertPrometheusSrv(t)
|
|
rc := createRequestCtx()
|
|
rc.Req.Header.Set(datasourceUIDHeader, "")
|
|
|
|
response := srv.RouteConvertPrometheusPostRuleGroup(rc, "test", apimodels.PrometheusRuleGroup{})
|
|
|
|
require.Equal(t, http.StatusBadRequest, response.Status())
|
|
require.Contains(t, string(response.Body()), "Missing datasource UID header")
|
|
})
|
|
|
|
t.Run("with invalid datasource should return error", func(t *testing.T) {
|
|
srv, _, _ := createConvertPrometheusSrv(t)
|
|
rc := createRequestCtx()
|
|
rc.Req.Header.Set(datasourceUIDHeader, "non-existing-ds")
|
|
|
|
response := srv.RouteConvertPrometheusPostRuleGroup(rc, "test", apimodels.PrometheusRuleGroup{})
|
|
|
|
require.Equal(t, http.StatusNotFound, response.Status())
|
|
})
|
|
|
|
t.Run("with rule group without evaluation interval should return 202", func(t *testing.T) {
|
|
srv, _, _ := createConvertPrometheusSrv(t)
|
|
rc := createRequestCtx()
|
|
|
|
response := srv.RouteConvertPrometheusPostRuleGroup(rc, "test", simpleGroup)
|
|
require.Equal(t, http.StatusAccepted, response.Status())
|
|
})
|
|
|
|
t.Run("should replace an existing rule group", func(t *testing.T) {
|
|
provenanceStore := fakes.NewFakeProvisioningStore()
|
|
folderService := foldertest.NewFakeService()
|
|
srv, _, ruleStore := createConvertPrometheusSrv(t, withProvenanceStore(provenanceStore), withFolderService(folderService))
|
|
|
|
// Create a folder in the root
|
|
fldr := randFolder()
|
|
fldr.ParentUID = ""
|
|
folderService.ExpectedFolder = fldr
|
|
folderService.ExpectedFolders = []*folder.Folder{fldr}
|
|
ruleStore.Folders[1] = append(ruleStore.Folders[1], fldr)
|
|
|
|
// And a rule
|
|
rule := models.RuleGen.
|
|
With(models.RuleGen.WithNamespaceUID(fldr.UID)).
|
|
With(models.RuleGen.WithGroupName(simpleGroup.Name)).
|
|
With(models.RuleGen.WithOrgID(1)).
|
|
With(models.RuleGen.WithPrometheusOriginalRuleDefinition("123")).
|
|
GenerateRef()
|
|
ruleStore.PutRule(context.Background(), rule)
|
|
|
|
rc := createRequestCtx()
|
|
response := srv.RouteConvertPrometheusPostRuleGroup(rc, fldr.Title, simpleGroup)
|
|
require.Equal(t, http.StatusAccepted, response.Status())
|
|
|
|
// Get the rules
|
|
remaining, err := ruleStore.ListAlertRules(context.Background(), &models.ListAlertRulesQuery{
|
|
OrgID: 1,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, remaining, 2)
|
|
|
|
// Create a map of rule titles to their expected definitions
|
|
expectedRules := make(map[string]string)
|
|
for _, rule := range simpleGroup.Rules {
|
|
if rule.Alert != "" {
|
|
promRuleYAML, err := yaml.Marshal(rule)
|
|
require.NoError(t, err)
|
|
expectedRules[rule.Alert] = string(promRuleYAML)
|
|
} else if rule.Record != "" {
|
|
promRuleYAML, err := yaml.Marshal(rule)
|
|
require.NoError(t, err)
|
|
expectedRules[rule.Record] = string(promRuleYAML)
|
|
}
|
|
}
|
|
|
|
// Verify each rule matches its expected definition
|
|
for _, r := range remaining {
|
|
require.Equal(t, simpleGroup.Name, r.RuleGroup)
|
|
expectedDef, exists := expectedRules[r.Title]
|
|
require.True(t, exists, "unexpected rule title: %s", r.Title)
|
|
|
|
promDefinition, err := r.PrometheusRuleDefinition()
|
|
require.NoError(t, err)
|
|
require.Equal(t, expectedDef, promDefinition)
|
|
|
|
// Verify provenance was set to ProvenanceConvertedPrometheus
|
|
prov, err := provenanceStore.GetProvenance(context.Background(), r, 1)
|
|
require.NoError(t, err)
|
|
require.Equal(t, models.ProvenanceConvertedPrometheus, prov)
|
|
}
|
|
})
|
|
|
|
t.Run("should fail to replace a provisioned rule group", func(t *testing.T) {
|
|
provenanceStore := fakes.NewFakeProvisioningStore()
|
|
folderService := foldertest.NewFakeService()
|
|
srv, _, ruleStore := createConvertPrometheusSrv(t, withProvenanceStore(provenanceStore), withFolderService(folderService))
|
|
|
|
// Create a folder in the root
|
|
fldr := randFolder()
|
|
fldr.ParentUID = ""
|
|
folderService.ExpectedFolder = fldr
|
|
folderService.ExpectedFolders = []*folder.Folder{fldr}
|
|
ruleStore.Folders[1] = append(ruleStore.Folders[1], fldr)
|
|
|
|
rule := models.RuleGen.
|
|
With(models.RuleGen.WithNamespaceUID(fldr.UID)).
|
|
With(models.RuleGen.WithGroupName(simpleGroup.Name)).
|
|
With(models.RuleGen.WithOrgID(1)).
|
|
With(models.RuleGen.WithPrometheusOriginalRuleDefinition("123")).
|
|
GenerateRef()
|
|
ruleStore.PutRule(context.Background(), rule)
|
|
// mark the rule as provisioned
|
|
err := provenanceStore.SetProvenance(context.Background(), rule, 1, models.ProvenanceAPI)
|
|
require.NoError(t, err)
|
|
|
|
rc := createRequestCtx()
|
|
response := srv.RouteConvertPrometheusPostRuleGroup(rc, fldr.Title, simpleGroup)
|
|
require.Equal(t, http.StatusConflict, response.Status())
|
|
|
|
// Verify the rule is still present
|
|
remaining, err := ruleStore.GetAlertRuleByUID(context.Background(), &models.GetAlertRuleByUIDQuery{
|
|
UID: rule.UID,
|
|
OrgID: rule.OrgID,
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, remaining)
|
|
})
|
|
|
|
t.Run("with no access to the datasource should return 403", func(t *testing.T) {
|
|
acFake := &acfakes.FakeRuleService{}
|
|
srv, _, _ := createConvertPrometheusSrv(t, withFakeAccessControlRuleService(acFake))
|
|
|
|
acFake.AuthorizeRuleChangesFunc = func(context.Context, identity.Requester, *store.GroupDelta) error {
|
|
return datasources.ErrDataSourceAccessDenied
|
|
}
|
|
|
|
rc := createRequestCtx()
|
|
response := srv.RouteConvertPrometheusPostRuleGroup(rc, "folder", simpleGroup)
|
|
require.Equal(t, http.StatusForbidden, response.Status())
|
|
require.Contains(t, string(response.Body()), "data source access denied")
|
|
})
|
|
|
|
t.Run("when alert rule quota limit exceeded", func(t *testing.T) {
|
|
quotas := &provisioning.MockQuotaChecker{}
|
|
quotas.EXPECT().LimitExceeded()
|
|
|
|
srv, _, _ := createConvertPrometheusSrv(t, withQuotaChecker(quotas))
|
|
|
|
rc := createRequestCtx()
|
|
response := srv.RouteConvertPrometheusPostRuleGroup(rc, "folder", simpleGroup)
|
|
require.Equal(t, http.StatusForbidden, response.Status())
|
|
require.Contains(t, string(response.Body()), "quota has been exceeded")
|
|
})
|
|
|
|
t.Run("with valid pause header values should return 202", func(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
headerName string
|
|
headerValue string
|
|
}{
|
|
{
|
|
name: "true recording rules pause value",
|
|
headerName: recordingRulesPausedHeader,
|
|
headerValue: "true",
|
|
},
|
|
{
|
|
name: "false recording rules pause value",
|
|
headerName: recordingRulesPausedHeader,
|
|
headerValue: "false",
|
|
},
|
|
{
|
|
name: "true alert rules pause value",
|
|
headerName: alertRulesPausedHeader,
|
|
headerValue: "true",
|
|
},
|
|
{
|
|
name: "false alert rules pause value",
|
|
headerName: alertRulesPausedHeader,
|
|
headerValue: "false",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
srv, _, _ := createConvertPrometheusSrv(t)
|
|
rc := createRequestCtx()
|
|
rc.Req.Header.Set(tc.headerName, tc.headerValue)
|
|
|
|
response := srv.RouteConvertPrometheusPostRuleGroup(rc, "test", simpleGroup)
|
|
require.Equal(t, http.StatusAccepted, response.Status())
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("with invalid pause header values should return 400", func(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
headerName string
|
|
headerValue string
|
|
expectedError string
|
|
}{
|
|
{
|
|
name: "invalid recording rules pause value",
|
|
headerName: recordingRulesPausedHeader,
|
|
headerValue: "invalid",
|
|
expectedError: "Invalid value for header X-Grafana-Alerting-Recording-Rules-Paused: must be 'true' or 'false'",
|
|
},
|
|
{
|
|
name: "invalid alert rules pause value",
|
|
headerName: alertRulesPausedHeader,
|
|
headerValue: "invalid",
|
|
expectedError: "Invalid value for header X-Grafana-Alerting-Alert-Rules-Paused: must be 'true' or 'false'",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
srv, _, _ := createConvertPrometheusSrv(t)
|
|
rc := createRequestCtx()
|
|
rc.Req.Header.Set(tc.headerName, tc.headerValue)
|
|
|
|
response := srv.RouteConvertPrometheusPostRuleGroup(rc, "test", simpleGroup)
|
|
require.Equal(t, http.StatusBadRequest, response.Status())
|
|
require.Contains(t, string(response.Body()), tc.expectedError)
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("with extra labels header should apply labels only to alert rules", func(t *testing.T) {
|
|
srv, _, ruleStore := createConvertPrometheusSrv(t)
|
|
rc := createRequestCtx()
|
|
rc.Req.Header.Set(extraLabelsHeader, "environment=production,team=alerting")
|
|
|
|
response := srv.RouteConvertPrometheusPostRuleGroup(rc, "test", simpleGroup)
|
|
require.Equal(t, http.StatusAccepted, response.Status())
|
|
|
|
rules, err := ruleStore.ListAlertRules(context.Background(), &models.ListAlertRulesQuery{
|
|
OrgID: 1,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, rules, 2)
|
|
|
|
var alertRule, recordingRule *models.AlertRule
|
|
for i := range rules {
|
|
switch rules[i].Title {
|
|
case "TestAlert":
|
|
alertRule = rules[i]
|
|
case "recorded-metric":
|
|
recordingRule = rules[i]
|
|
}
|
|
}
|
|
|
|
require.NotNil(t, alertRule, "Alert rule should be present")
|
|
require.NotNil(t, recordingRule, "Recording rule should be present")
|
|
|
|
// Alert rule should have extra labels
|
|
require.Equal(t, "production", alertRule.Labels["environment"])
|
|
require.Equal(t, "alerting", alertRule.Labels["team"])
|
|
require.Equal(t, "critical", alertRule.Labels["severity"])
|
|
|
|
// Recording rule should not have extra labels
|
|
require.NotContains(t, recordingRule.Labels, "environment")
|
|
require.NotContains(t, recordingRule.Labels, "team")
|
|
require.Equal(t, "warning", recordingRule.Labels["severity"])
|
|
})
|
|
|
|
t.Run("with extra labels that conflict with rule labels", func(t *testing.T) {
|
|
srv, _, ruleStore := createConvertPrometheusSrv(t)
|
|
rc := createRequestCtx()
|
|
// rules in the simpleGroup already have a severity label, so
|
|
// it should not be overwritten by the label from the header
|
|
rc.Req.Header.Set(extraLabelsHeader, "environment=production,severity=low")
|
|
|
|
response := srv.RouteConvertPrometheusPostRuleGroup(rc, "test", simpleGroup)
|
|
require.Equal(t, http.StatusAccepted, response.Status())
|
|
|
|
rules, err := ruleStore.ListAlertRules(context.Background(), &models.ListAlertRulesQuery{
|
|
OrgID: 1,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, rules, 2)
|
|
|
|
var alertRule, recordingRule *models.AlertRule
|
|
for i := range rules {
|
|
switch rules[i].Title {
|
|
case "TestAlert":
|
|
alertRule = rules[i]
|
|
case "recorded-metric":
|
|
recordingRule = rules[i]
|
|
}
|
|
}
|
|
|
|
require.NotNil(t, alertRule, "Alert rule should be present")
|
|
require.NotNil(t, recordingRule, "Recording rule should be present")
|
|
|
|
// Alert rule should have extra labels where they don't conflict
|
|
require.Equal(t, "production", alertRule.Labels["environment"])
|
|
// Rule label should take precedence over extra label
|
|
require.Equal(t, "critical", alertRule.Labels["severity"])
|
|
require.NotEqual(t, "low", alertRule.Labels["severity"])
|
|
|
|
// Recording rule should not have extra labels
|
|
require.NotContains(t, recordingRule.Labels, "environment")
|
|
require.Equal(t, "warning", recordingRule.Labels["severity"])
|
|
})
|
|
|
|
t.Run("with invalid extra labels header should return 400", func(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
headerValue string
|
|
expectedError string
|
|
}{
|
|
{
|
|
name: "missing equals sign",
|
|
headerValue: "environment,team=platform",
|
|
expectedError: "Invalid value for header X-Grafana-Alerting-Extra-Labels: format should be 'key=value,key2=value2'",
|
|
},
|
|
{
|
|
name: "empty key",
|
|
headerValue: "=production,team=platform",
|
|
expectedError: "Invalid value for header X-Grafana-Alerting-Extra-Labels: keys and values cannot be empty",
|
|
},
|
|
{
|
|
name: "empty value",
|
|
headerValue: "environment=,team=platform",
|
|
expectedError: "Invalid value for header X-Grafana-Alerting-Extra-Labels: keys and values cannot be empty",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
srv, _, _ := createConvertPrometheusSrv(t)
|
|
rc := createRequestCtx()
|
|
rc.Req.Header.Set(extraLabelsHeader, tc.headerValue)
|
|
|
|
response := srv.RouteConvertPrometheusPostRuleGroup(rc, "test", simpleGroup)
|
|
require.Equal(t, http.StatusBadRequest, response.Status())
|
|
require.Contains(t, string(response.Body()), tc.expectedError)
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("with empty rule group name should return 400", func(t *testing.T) {
|
|
srv, _, _ := createConvertPrometheusSrv(t)
|
|
rc := createRequestCtx()
|
|
|
|
emptyNameGroup := apimodels.PrometheusRuleGroup{
|
|
Name: "",
|
|
Interval: prommodel.Duration(1 * time.Minute),
|
|
Rules: []apimodels.PrometheusRule{
|
|
{
|
|
Alert: "TestAlert",
|
|
Expr: "up == 0",
|
|
For: util.Pointer(prommodel.Duration(5 * time.Minute)),
|
|
Labels: map[string]string{
|
|
"severity": "critical",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
response := srv.RouteConvertPrometheusPostRuleGroup(rc, "test", emptyNameGroup)
|
|
require.Equal(t, http.StatusBadRequest, response.Status())
|
|
require.Contains(t, string(response.Body()), "rule group name must not be empty")
|
|
})
|
|
|
|
t.Run("with valid request should return 202", func(t *testing.T) {
|
|
srv, _, _ := createConvertPrometheusSrv(t)
|
|
rc := createRequestCtx()
|
|
|
|
response := srv.RouteConvertPrometheusPostRuleGroup(rc, "test", simpleGroup)
|
|
require.Equal(t, http.StatusAccepted, response.Status())
|
|
})
|
|
|
|
t.Run("with disabled recording rules", func(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
recordingRules bool
|
|
expectedStatus int
|
|
}{
|
|
{
|
|
name: "when recording rules are enabled",
|
|
recordingRules: true,
|
|
expectedStatus: http.StatusAccepted,
|
|
},
|
|
{
|
|
name: "when recording rules are disabled",
|
|
recordingRules: false,
|
|
expectedStatus: http.StatusBadRequest,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
features := featuremgmt.WithFeatures()
|
|
|
|
srv, _, _ := createConvertPrometheusSrv(t, withFeatureToggles(features))
|
|
srv.cfg.RecordingRules.Enabled = tc.recordingRules
|
|
rc := createRequestCtx()
|
|
|
|
response := srv.RouteConvertPrometheusPostRuleGroup(rc, "test", simpleGroup)
|
|
require.Equal(t, tc.expectedStatus, response.Status())
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("with disable provenance header should use ProvenanceNone", func(t *testing.T) {
|
|
provenanceStore := fakes.NewFakeProvisioningStore()
|
|
folderService := foldertest.NewFakeService()
|
|
srv, _, ruleStore := createConvertPrometheusSrv(t, withProvenanceStore(provenanceStore), withFolderService(folderService))
|
|
|
|
// Create a folder in the root
|
|
fldr := randFolder()
|
|
fldr.ParentUID = ""
|
|
folderService.ExpectedFolder = fldr
|
|
folderService.ExpectedFolders = []*folder.Folder{fldr}
|
|
ruleStore.Folders[1] = append(ruleStore.Folders[1], fldr)
|
|
|
|
// Create request with the X-Disable-Provenance header
|
|
rc := createRequestCtx()
|
|
rc.Req.Header.Set("X-Disable-Provenance", "true")
|
|
|
|
response := srv.RouteConvertPrometheusPostRuleGroup(rc, fldr.Title, simpleGroup)
|
|
require.Equal(t, http.StatusAccepted, response.Status())
|
|
|
|
// Get the created rules
|
|
rules, err := ruleStore.ListAlertRules(context.Background(), &models.ListAlertRulesQuery{
|
|
OrgID: 1,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, rules, 2)
|
|
|
|
// Verify provenance was set to ProvenanceNone
|
|
for _, r := range rules {
|
|
prov, err := provenanceStore.GetProvenance(context.Background(), r, 1)
|
|
require.NoError(t, err)
|
|
require.Equal(t, models.ProvenanceNone, prov, "Provenance should be ProvenanceNone when X-Disable-Provenance header is set")
|
|
// Prometheus rule definition should not be saved when provenance is disabled
|
|
require.Nil(t, r.Metadata.PrometheusStyleRule)
|
|
}
|
|
})
|
|
|
|
t.Run("with version message header should pass message to rule store", func(t *testing.T) {
|
|
provenanceStore := fakes.NewFakeProvisioningStore()
|
|
folderService := foldertest.NewFakeService()
|
|
srv, _, ruleStore := createConvertPrometheusSrv(t, withProvenanceStore(provenanceStore), withFolderService(folderService))
|
|
|
|
// Create a folder in the root
|
|
fldr := randFolder()
|
|
fldr.ParentUID = ""
|
|
folderService.ExpectedFolder = fldr
|
|
folderService.ExpectedFolders = []*folder.Folder{fldr}
|
|
ruleStore.Folders[1] = append(ruleStore.Folders[1], fldr)
|
|
|
|
makeGroup := func(alertname string) apimodels.PrometheusRuleGroup {
|
|
return apimodels.PrometheusRuleGroup{
|
|
Name: "Test Group",
|
|
Interval: prommodel.Duration(1 * time.Minute),
|
|
Rules: []apimodels.PrometheusRule{
|
|
{
|
|
Alert: alertname,
|
|
Expr: "up == 0",
|
|
For: util.Pointer(prommodel.Duration(5 * time.Minute)),
|
|
Labels: map[string]string{
|
|
"severity": "critical",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// Create a rule with the X-Grafana-Alerting-Version-Message to check it's passed to InsertRule.
|
|
|
|
rc1 := createRequestCtx()
|
|
rc1.Req.Header.Set("X-Grafana-Alerting-Version-Message", "version #1")
|
|
response1 := srv.RouteConvertPrometheusPostRuleGroup(rc1, fldr.Title, makeGroup("AlertV1"))
|
|
require.Equal(t, http.StatusAccepted, response1.Status())
|
|
|
|
inserts := ruleStore.GetRecordedCommands(func(cmd any) (any, bool) {
|
|
a, ok := cmd.([]models.InsertRule)
|
|
return a, ok
|
|
})
|
|
require.Len(t, inserts, 1)
|
|
cmd1 := inserts[0].([]models.InsertRule)
|
|
require.Len(t, cmd1, 1)
|
|
require.Equal(t, "version #1", cmd1[0].Message)
|
|
|
|
// Now update the rule to check it gets passed to UpdateRule.
|
|
|
|
rc2 := createRequestCtx()
|
|
rc2.Req.Header.Set("X-Grafana-Alerting-Version-Message", "version #2")
|
|
response2 := srv.RouteConvertPrometheusPostRuleGroup(rc2, fldr.Title, makeGroup("AlertV2"))
|
|
require.Equal(t, http.StatusAccepted, response2.Status())
|
|
|
|
updates := ruleStore.GetRecordedCommands(func(cmd any) (any, bool) {
|
|
a, ok := cmd.([]models.UpdateRule)
|
|
return a, ok
|
|
})
|
|
require.Len(t, updates, 1)
|
|
cmd2 := updates[0].([]models.UpdateRule)
|
|
require.Len(t, cmd2, 1)
|
|
require.Equal(t, "version #2", cmd2[0].Message)
|
|
})
|
|
|
|
t.Run("returns error when target datasource does not exist", func(t *testing.T) {
|
|
srv, _, _ := createConvertPrometheusSrv(t)
|
|
rc := createRequestCtx()
|
|
rc.Req.Header.Set(targetDatasourceUIDHeader, "some-data-source")
|
|
|
|
response := srv.RouteConvertPrometheusPostRuleGroup(rc, "test", simpleGroup)
|
|
require.Equal(t, http.StatusNotFound, response.Status())
|
|
require.Contains(t, string(response.Body()), "failed to get recording rules target datasource")
|
|
})
|
|
|
|
t.Run("uses target datasource for recording rules", func(t *testing.T) {
|
|
srv, dsCache, ruleStore := createConvertPrometheusSrv(t)
|
|
rc := createRequestCtx()
|
|
targetDSUID := util.GenerateShortUID()
|
|
ds := &datasources.DataSource{
|
|
UID: targetDSUID,
|
|
Type: datasources.DS_PROMETHEUS,
|
|
}
|
|
dsCache.DataSources = append(dsCache.DataSources, ds)
|
|
rc.Req.Header.Set(targetDatasourceUIDHeader, targetDSUID)
|
|
|
|
simpleGroup := apimodels.PrometheusRuleGroup{
|
|
Name: "Test Group",
|
|
Interval: prommodel.Duration(1 * time.Minute),
|
|
Rules: []apimodels.PrometheusRule{
|
|
{
|
|
Record: "recorded-metric",
|
|
Expr: "vector(1)",
|
|
Labels: map[string]string{
|
|
"severity": "warning",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
response := srv.RouteConvertPrometheusPostRuleGroup(rc, "test", simpleGroup)
|
|
require.Equal(t, http.StatusAccepted, response.Status())
|
|
|
|
remaining, err := ruleStore.ListAlertRules(context.Background(), &models.ListAlertRulesQuery{
|
|
OrgID: 1,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, remaining, 1)
|
|
require.NotNil(t, remaining[0].Record)
|
|
require.Equal(t, targetDSUID, remaining[0].Record.TargetDatasourceUID)
|
|
})
|
|
|
|
t.Run("sets notification settings for rules if specified", func(t *testing.T) {
|
|
srv, _, ruleStore := createConvertPrometheusSrv(t)
|
|
rc := createRequestCtx()
|
|
|
|
receiver := "test-receiver"
|
|
groupBy := []string{"cluster", "pod"}
|
|
settings := apimodels.AlertRuleNotificationSettings{
|
|
Receiver: receiver,
|
|
GroupBy: groupBy,
|
|
}
|
|
settingsJSON, err := json.Marshal(settings)
|
|
require.NoError(t, err)
|
|
rc.Req.Header.Set(notificationSettingsHeader, string(settingsJSON))
|
|
|
|
simpleGroup := apimodels.PrometheusRuleGroup{
|
|
Name: "Test Group",
|
|
Interval: prommodel.Duration(1 * time.Minute),
|
|
Rules: []apimodels.PrometheusRule{
|
|
{
|
|
Alert: "TestAlert",
|
|
Expr: "up == 0",
|
|
For: util.Pointer(prommodel.Duration(5 * time.Minute)),
|
|
Labels: map[string]string{
|
|
"severity": "critical",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
response := srv.RouteConvertPrometheusPostRuleGroup(rc, "test", simpleGroup)
|
|
require.Equal(t, http.StatusAccepted, response.Status())
|
|
|
|
createdRules, err := ruleStore.ListAlertRules(context.Background(), &models.ListAlertRulesQuery{
|
|
OrgID: 1,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, createdRules, 1)
|
|
require.Len(t, createdRules[0].NotificationSettings, 1)
|
|
require.Equal(t, receiver, createdRules[0].NotificationSettings[0].Receiver)
|
|
require.Equal(t, groupBy, createdRules[0].NotificationSettings[0].GroupBy)
|
|
})
|
|
|
|
t.Run("returns error when notification settings header contains invalid JSON", func(t *testing.T) {
|
|
srv, _, _ := createConvertPrometheusSrv(t)
|
|
rc := createRequestCtx()
|
|
|
|
rc.Req.Header.Set(notificationSettingsHeader, "{invalid json")
|
|
|
|
simpleGroup := apimodels.PrometheusRuleGroup{
|
|
Name: "Test Group",
|
|
Interval: prommodel.Duration(1 * time.Minute),
|
|
Rules: []apimodels.PrometheusRule{
|
|
{
|
|
Alert: "TestAlert",
|
|
Expr: "up == 0",
|
|
},
|
|
},
|
|
}
|
|
|
|
response := srv.RouteConvertPrometheusPostRuleGroup(rc, "test", simpleGroup)
|
|
require.Equal(t, http.StatusBadRequest, response.Status())
|
|
require.Contains(t, string(response.Body()), "Invalid value for header X-Grafana-Alerting-Notification-Settings")
|
|
})
|
|
|
|
t.Run("returns error when notification settings contain invalid values", func(t *testing.T) {
|
|
srv, _, _ := createConvertPrometheusSrv(t)
|
|
rc := createRequestCtx()
|
|
|
|
settings := apimodels.AlertRuleNotificationSettings{
|
|
Receiver: "", // empty receiver is invalid
|
|
}
|
|
settingsJSON, err := json.Marshal(settings)
|
|
require.NoError(t, err)
|
|
rc.Req.Header.Set(notificationSettingsHeader, string(settingsJSON))
|
|
|
|
simpleGroup := apimodels.PrometheusRuleGroup{
|
|
Name: "Test Group",
|
|
Interval: prommodel.Duration(1 * time.Minute),
|
|
Rules: []apimodels.PrometheusRule{
|
|
{
|
|
Alert: "TestAlert",
|
|
Expr: "up == 0",
|
|
},
|
|
},
|
|
}
|
|
|
|
response := srv.RouteConvertPrometheusPostRuleGroup(rc, "test", simpleGroup)
|
|
require.Equal(t, http.StatusBadRequest, response.Status())
|
|
require.Contains(t, string(response.Body()), "Invalid value for header X-Grafana-Alerting-Notification-Settings")
|
|
})
|
|
}
|
|
|
|
func TestRouteConvertPrometheusGetRuleGroup(t *testing.T) {
|
|
promRule := apimodels.PrometheusRule{
|
|
Alert: "test alert",
|
|
Expr: "vector(1) > 0",
|
|
For: util.Pointer(prommodel.Duration(5 * time.Minute)),
|
|
Labels: map[string]string{
|
|
"severity": "critical",
|
|
},
|
|
Annotations: map[string]string{
|
|
"summary": "test alert",
|
|
},
|
|
}
|
|
promRuleYAML, err := yaml.Marshal(promRule)
|
|
require.NoError(t, err)
|
|
|
|
t.Run("with non-existent folder should return 404", func(t *testing.T) {
|
|
srv, _, _ := createConvertPrometheusSrv(t)
|
|
rc := createRequestCtx()
|
|
|
|
response := srv.RouteConvertPrometheusGetRuleGroup(rc, "non-existent", "test")
|
|
require.Equal(t, http.StatusNotFound, response.Status(), string(response.Body()))
|
|
})
|
|
|
|
t.Run("with non-existent group should return 404", func(t *testing.T) {
|
|
srv, _, _ := createConvertPrometheusSrv(t)
|
|
rc := createRequestCtx()
|
|
|
|
response := srv.RouteConvertPrometheusGetRuleGroup(rc, "test", "non-existent")
|
|
require.Equal(t, http.StatusNotFound, response.Status(), string(response.Body()))
|
|
})
|
|
|
|
t.Run("with valid request should return 200", func(t *testing.T) {
|
|
folderService := foldertest.NewFakeService()
|
|
srv, _, ruleStore := createConvertPrometheusSrv(t, withFolderService(folderService))
|
|
rc := createRequestCtx()
|
|
|
|
// Create two folders in the root folder
|
|
fldr := randFolder()
|
|
fldr.ParentUID = ""
|
|
folderService.ExpectedFolder = fldr
|
|
folderService.ExpectedFolders = []*folder.Folder{fldr}
|
|
folderService.AddFolder(fldr)
|
|
ruleStore.Folders[1] = append(ruleStore.Folders[1], fldr)
|
|
|
|
// Create rules in both folders
|
|
groupKey := models.GenerateGroupKey(rc.OrgID)
|
|
groupKey.NamespaceUID = fldr.UID
|
|
groupKey.RuleGroup = "test-group"
|
|
rule := models.RuleGen.
|
|
With(models.RuleGen.WithGroupKey(groupKey)).
|
|
With(models.RuleGen.WithTitle("TestAlert")).
|
|
With(models.RuleGen.WithIntervalSeconds(60)).
|
|
With(models.RuleGen.WithPrometheusOriginalRuleDefinition(string(promRuleYAML))).
|
|
GenerateRef()
|
|
ruleStore.PutRule(context.Background(), rule)
|
|
|
|
// Create a rule in another group
|
|
groupKeyNotFromProm := models.GenerateGroupKey(rc.OrgID)
|
|
groupKeyNotFromProm.NamespaceUID = fldr.UID
|
|
groupKeyNotFromProm.RuleGroup = "test-group-2"
|
|
ruleInOtherFolder := models.RuleGen.
|
|
With(models.RuleGen.WithGroupKey(groupKeyNotFromProm)).
|
|
With(models.RuleGen.WithTitle("in another group")).
|
|
With(models.RuleGen.WithIntervalSeconds(60)).
|
|
GenerateRef()
|
|
ruleStore.PutRule(context.Background(), ruleInOtherFolder)
|
|
|
|
t.Run("YAML response", func(t *testing.T) {
|
|
getResp := srv.RouteConvertPrometheusGetRuleGroup(rc, fldr.Title, groupKey.RuleGroup)
|
|
require.Equal(t, http.StatusOK, getResp.Status())
|
|
|
|
var respGroup apimodels.PrometheusRuleGroup
|
|
err := yaml.Unmarshal(getResp.Body(), &respGroup)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, groupKey.RuleGroup, respGroup.Name)
|
|
require.Equal(t, prommodel.Duration(time.Duration(rule.IntervalSeconds)*time.Second), respGroup.Interval)
|
|
require.Len(t, respGroup.Rules, 1)
|
|
require.Equal(t, promRule.Alert, respGroup.Rules[0].Alert)
|
|
})
|
|
|
|
t.Run("JSON response", func(t *testing.T) {
|
|
rc.Req.Header.Set("Accept", "application/json")
|
|
getResp := srv.RouteConvertPrometheusGetRuleGroup(rc, fldr.Title, groupKey.RuleGroup)
|
|
require.Equal(t, http.StatusOK, getResp.Status())
|
|
|
|
var jsonGroup apimodels.PrometheusRuleGroup
|
|
err := json.Unmarshal(getResp.Body(), &jsonGroup)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, groupKey.RuleGroup, jsonGroup.Name)
|
|
require.Equal(t, prommodel.Duration(time.Duration(rule.IntervalSeconds)*time.Second), jsonGroup.Interval)
|
|
require.Len(t, jsonGroup.Rules, 1)
|
|
require.Equal(t, promRule.Alert, jsonGroup.Rules[0].Alert)
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestRouteConvertPrometheusGetNamespace(t *testing.T) {
|
|
promRule1 := apimodels.PrometheusRule{
|
|
Alert: "test alert",
|
|
Expr: "vector(1) > 0",
|
|
For: util.Pointer(prommodel.Duration(5 * time.Minute)),
|
|
Labels: map[string]string{
|
|
"severity": "critical",
|
|
},
|
|
Annotations: map[string]string{
|
|
"summary": "test alert",
|
|
},
|
|
}
|
|
|
|
promRule2 := apimodels.PrometheusRule{
|
|
Alert: "test alert 2",
|
|
Expr: "vector(1) > 0",
|
|
For: util.Pointer(prommodel.Duration(5 * time.Minute)),
|
|
Labels: map[string]string{
|
|
"severity": "also critical",
|
|
},
|
|
Annotations: map[string]string{
|
|
"summary": "test alert 2",
|
|
},
|
|
}
|
|
|
|
promGroup1 := apimodels.PrometheusRuleGroup{
|
|
Name: "Test Group",
|
|
Interval: prommodel.Duration(1 * time.Minute),
|
|
Rules: []apimodels.PrometheusRule{
|
|
promRule1,
|
|
},
|
|
}
|
|
promGroup2 := apimodels.PrometheusRuleGroup{
|
|
Name: "Test Group 2",
|
|
Interval: prommodel.Duration(1 * time.Minute),
|
|
Rules: []apimodels.PrometheusRule{
|
|
promRule2,
|
|
},
|
|
}
|
|
|
|
t.Run("with non-existent folder should return 404", func(t *testing.T) {
|
|
srv, _, _ := createConvertPrometheusSrv(t)
|
|
rc := createRequestCtx()
|
|
|
|
response := srv.RouteConvertPrometheusGetNamespace(rc, "non-existent")
|
|
require.Equal(t, http.StatusNotFound, response.Status())
|
|
})
|
|
|
|
t.Run("with valid request should return 200", func(t *testing.T) {
|
|
folderService := foldertest.NewFakeService()
|
|
srv, _, ruleStore := createConvertPrometheusSrv(t, withFolderService(folderService))
|
|
rc := createRequestCtx()
|
|
|
|
// Create two folders in the root folder
|
|
fldr := randFolder()
|
|
fldr.ParentUID = ""
|
|
fldr2 := randFolder()
|
|
fldr2.ParentUID = ""
|
|
folderService.ExpectedFolders = []*folder.Folder{fldr, fldr2}
|
|
folderService.AddFolder(fldr)
|
|
folderService.AddFolder(fldr2)
|
|
ruleStore.Folders[1] = append(ruleStore.Folders[1], fldr, fldr2)
|
|
|
|
// Create a Grafana rule for each Prometheus rule
|
|
for _, promGroup := range []apimodels.PrometheusRuleGroup{promGroup1, promGroup2} {
|
|
groupKey := models.GenerateGroupKey(rc.OrgID)
|
|
groupKey.NamespaceUID = fldr.UID
|
|
groupKey.RuleGroup = promGroup.Name
|
|
promRuleYAML, err := yaml.Marshal(promGroup.Rules[0])
|
|
require.NoError(t, err)
|
|
rule := models.RuleGen.
|
|
With(models.RuleGen.WithGroupKey(groupKey)).
|
|
With(models.RuleGen.WithTitle(promGroup.Rules[0].Alert)).
|
|
With(models.RuleGen.WithIntervalSeconds(60)).
|
|
With(models.RuleGen.WithPrometheusOriginalRuleDefinition(string(promRuleYAML))).
|
|
GenerateRef()
|
|
ruleStore.PutRule(context.Background(), rule)
|
|
}
|
|
|
|
t.Run("YAML response", func(t *testing.T) {
|
|
response := srv.RouteConvertPrometheusGetNamespace(rc, fldr.Title)
|
|
require.Equal(t, http.StatusOK, response.Status())
|
|
|
|
var respNamespaces map[string][]apimodels.PrometheusRuleGroup
|
|
err := yaml.Unmarshal(response.Body(), &respNamespaces)
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, respNamespaces, 1)
|
|
require.Contains(t, respNamespaces, fldr.Title)
|
|
require.ElementsMatch(t, respNamespaces[fldr.Title], []apimodels.PrometheusRuleGroup{promGroup1, promGroup2})
|
|
})
|
|
|
|
t.Run("JSON response", func(t *testing.T) {
|
|
rc.Req.Header.Set("Accept", "application/json")
|
|
response := srv.RouteConvertPrometheusGetNamespace(rc, fldr.Title)
|
|
require.Equal(t, http.StatusOK, response.Status())
|
|
|
|
var jsonNamespaces map[string][]apimodels.PrometheusRuleGroup
|
|
err := json.Unmarshal(response.Body(), &jsonNamespaces)
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, jsonNamespaces, 1)
|
|
require.Contains(t, jsonNamespaces, fldr.Title)
|
|
require.ElementsMatch(t, jsonNamespaces[fldr.Title], []apimodels.PrometheusRuleGroup{promGroup1, promGroup2})
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestRouteConvertPrometheusGetRules(t *testing.T) {
|
|
promRule1 := apimodels.PrometheusRule{
|
|
Alert: "test alert",
|
|
Expr: "vector(1) > 0",
|
|
For: util.Pointer(prommodel.Duration(5 * time.Minute)),
|
|
Labels: map[string]string{
|
|
"severity": "critical",
|
|
},
|
|
Annotations: map[string]string{
|
|
"summary": "test alert",
|
|
},
|
|
}
|
|
|
|
promRule2 := apimodels.PrometheusRule{
|
|
Alert: "test alert 2",
|
|
Expr: "vector(1) > 0",
|
|
For: util.Pointer(prommodel.Duration(5 * time.Minute)),
|
|
Labels: map[string]string{
|
|
"severity": "also critical",
|
|
},
|
|
Annotations: map[string]string{
|
|
"summary": "test alert 2",
|
|
},
|
|
}
|
|
|
|
promGroup1 := apimodels.PrometheusRuleGroup{
|
|
Name: "Test Group",
|
|
Interval: prommodel.Duration(1 * time.Minute),
|
|
Rules: []apimodels.PrometheusRule{
|
|
promRule1,
|
|
},
|
|
}
|
|
promGroup2 := apimodels.PrometheusRuleGroup{
|
|
Name: "Test Group 2",
|
|
Interval: prommodel.Duration(1 * time.Minute),
|
|
Rules: []apimodels.PrometheusRule{
|
|
promRule2,
|
|
},
|
|
}
|
|
|
|
assertEmptyResponse := func(t *testing.T, srv *ConvertPrometheusSrv, reqCtx *contextmodel.ReqContext) {
|
|
t.Helper()
|
|
|
|
response := srv.RouteConvertPrometheusGetRules(reqCtx)
|
|
require.Equal(t, http.StatusOK, response.Status())
|
|
|
|
var respNamespaces map[string][]apimodels.PrometheusRuleGroup
|
|
err := yaml.Unmarshal(response.Body(), &respNamespaces)
|
|
require.NoError(t, err)
|
|
require.Empty(t, respNamespaces)
|
|
}
|
|
|
|
// testForEmptyResponses tests that RouteConvertPrometheusGetRules returns an empty response
|
|
// when there are no rules in the folder or the folder does not exist.
|
|
testForEmptyResponses := func(t *testing.T, withCustomFolderHeader bool) {
|
|
rc := createRequestCtx()
|
|
unknownFolderUID := "some unknown folder"
|
|
rootFolderUID := ""
|
|
if withCustomFolderHeader {
|
|
rootFolderUID = unknownFolderUID
|
|
rc.Req.Header.Set(folderUIDHeader, unknownFolderUID)
|
|
}
|
|
|
|
t.Run("for non-existent folder should return empty response", func(t *testing.T) {
|
|
srv, _, _ := createConvertPrometheusSrv(t)
|
|
assertEmptyResponse(t, srv, rc)
|
|
})
|
|
|
|
t.Run("for existing folder with no children should return empty response", func(t *testing.T) {
|
|
folderService := foldertest.NewFakeService()
|
|
srv, _, ruleStore := createConvertPrometheusSrv(t, withFolderService(folderService))
|
|
|
|
fldr := randFolder()
|
|
fldr.UID = unknownFolderUID
|
|
fldr.ParentUID = rootFolderUID
|
|
folderService.ExpectedFolders = []*folder.Folder{fldr}
|
|
ruleStore.Folders[1] = append(ruleStore.Folders[1], fldr)
|
|
|
|
assertEmptyResponse(t, srv, rc)
|
|
})
|
|
}
|
|
|
|
t.Run("without custom root folder", func(t *testing.T) {
|
|
testForEmptyResponses(t, false)
|
|
})
|
|
|
|
t.Run("with custom root folder", func(t *testing.T) {
|
|
testForEmptyResponses(t, true)
|
|
})
|
|
|
|
t.Run("with rules should return 200 with rules", func(t *testing.T) {
|
|
folderService := foldertest.NewFakeService()
|
|
srv, _, ruleStore := createConvertPrometheusSrv(t, withFolderService(folderService))
|
|
rc := createRequestCtx()
|
|
|
|
// Create a folder in the root
|
|
fldr := randFolder()
|
|
fldr.ParentUID = ""
|
|
folderService.AddFolder(fldr)
|
|
folderService.ExpectedFolders = []*folder.Folder{fldr}
|
|
ruleStore.Folders[1] = append(ruleStore.Folders[1], fldr)
|
|
|
|
// Create a Grafana rule for each Prometheus rule
|
|
for _, promGroup := range []apimodels.PrometheusRuleGroup{promGroup1, promGroup2} {
|
|
groupKey := models.GenerateGroupKey(rc.OrgID)
|
|
groupKey.NamespaceUID = fldr.UID
|
|
groupKey.RuleGroup = promGroup.Name
|
|
promRuleYAML, err := yaml.Marshal(promGroup.Rules[0])
|
|
require.NoError(t, err)
|
|
rule := models.RuleGen.
|
|
With(models.RuleGen.WithGroupKey(groupKey)).
|
|
With(models.RuleGen.WithTitle(promGroup.Rules[0].Alert)).
|
|
With(models.RuleGen.WithIntervalSeconds(60)).
|
|
With(models.RuleGen.WithPrometheusOriginalRuleDefinition(string(promRuleYAML))).
|
|
GenerateRef()
|
|
ruleStore.PutRule(context.Background(), rule)
|
|
}
|
|
|
|
t.Run("YAML response", func(t *testing.T) {
|
|
response := srv.RouteConvertPrometheusGetRules(rc)
|
|
require.Equal(t, http.StatusOK, response.Status())
|
|
|
|
var respNamespaces map[string][]apimodels.PrometheusRuleGroup
|
|
err := yaml.Unmarshal(response.Body(), &respNamespaces)
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, respNamespaces, 1)
|
|
require.Contains(t, respNamespaces, fldr.Title)
|
|
require.ElementsMatch(t, respNamespaces[fldr.Title], []apimodels.PrometheusRuleGroup{promGroup1, promGroup2})
|
|
})
|
|
|
|
t.Run("JSON response", func(t *testing.T) {
|
|
rc.Req.Header.Set("Accept", "application/json")
|
|
response := srv.RouteConvertPrometheusGetRules(rc)
|
|
require.Equal(t, http.StatusOK, response.Status())
|
|
|
|
var jsonNamespaces map[string][]apimodels.PrometheusRuleGroup
|
|
err := json.Unmarshal(response.Body(), &jsonNamespaces)
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, jsonNamespaces, 1)
|
|
require.Contains(t, jsonNamespaces, fldr.Title)
|
|
require.ElementsMatch(t, jsonNamespaces[fldr.Title], []apimodels.PrometheusRuleGroup{promGroup1, promGroup2})
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestConvertPrometheusResponse(t *testing.T) {
|
|
testData := map[string][]apimodels.PrometheusRuleGroup{
|
|
"test": {
|
|
{
|
|
Name: "test-group",
|
|
Rules: []apimodels.PrometheusRule{
|
|
{
|
|
Alert: "TestAlert",
|
|
Expr: "up == 0",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
testCases := []struct {
|
|
name string
|
|
acceptHeader string
|
|
expectedType string
|
|
checkResponse func(t *testing.T, body []byte)
|
|
}{
|
|
{
|
|
name: "by default returns YAML",
|
|
expectedType: "text/yaml",
|
|
checkResponse: func(t *testing.T, body []byte) {
|
|
require.True(t, strings.Contains(string(body), "test-group"))
|
|
require.True(t, strings.Contains(string(body), "TestAlert"))
|
|
var result map[string][]apimodels.PrometheusRuleGroup
|
|
err := yaml.Unmarshal(body, &result)
|
|
require.NoError(t, err)
|
|
},
|
|
},
|
|
{
|
|
name: "with application/json Accept header returns JSON",
|
|
acceptHeader: "application/json",
|
|
expectedType: "application/json",
|
|
checkResponse: func(t *testing.T, body []byte) {
|
|
require.True(t, strings.Contains(string(body), "test-group"))
|
|
require.True(t, strings.Contains(string(body), "TestAlert"))
|
|
var result map[string][]apimodels.PrometheusRuleGroup
|
|
err := json.Unmarshal(body, &result)
|
|
require.NoError(t, err)
|
|
},
|
|
},
|
|
{
|
|
name: "with application/yaml accept header returns YAML",
|
|
acceptHeader: "application/yaml",
|
|
expectedType: "text/yaml",
|
|
checkResponse: func(t *testing.T, body []byte) {
|
|
require.True(t, strings.Contains(string(body), "test-group"))
|
|
require.True(t, strings.Contains(string(body), "TestAlert"))
|
|
var result map[string][]apimodels.PrometheusRuleGroup
|
|
err := yaml.Unmarshal(body, &result)
|
|
require.NoError(t, err)
|
|
},
|
|
},
|
|
{
|
|
name: "with a header with both json and yaml returns JSON",
|
|
acceptHeader: "application/yaml, application/json",
|
|
expectedType: "application/json",
|
|
checkResponse: func(t *testing.T, body []byte) {
|
|
require.True(t, strings.Contains(string(body), "test-group"))
|
|
require.True(t, strings.Contains(string(body), "TestAlert"))
|
|
var result map[string][]apimodels.PrometheusRuleGroup
|
|
err := json.Unmarshal(body, &result)
|
|
require.NoError(t, err)
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
rc := createRequestCtx()
|
|
if tc.acceptHeader != "" {
|
|
rc.Req.Header.Set("Accept", tc.acceptHeader)
|
|
}
|
|
|
|
response := convertPrometheusResponse(rc, http.StatusOK, testData)
|
|
|
|
require.Equal(t, http.StatusOK, response.Status())
|
|
tc.checkResponse(t, response.Body())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRouteConvertPrometheusDeleteNamespace(t *testing.T) {
|
|
t.Run("for non-existent folder should return 404", func(t *testing.T) {
|
|
srv, _, _ := createConvertPrometheusSrv(t)
|
|
rc := createRequestCtx()
|
|
|
|
response := srv.RouteConvertPrometheusDeleteNamespace(rc, "non-existent")
|
|
require.Equal(t, http.StatusNotFound, response.Status())
|
|
})
|
|
|
|
t.Run("for existing folder with no groups should return 404", func(t *testing.T) {
|
|
folderService := foldertest.NewFakeService()
|
|
srv, _, ruleStore := createConvertPrometheusSrv(t, withFolderService(folderService))
|
|
rc := createRequestCtx()
|
|
|
|
fldr := randFolder()
|
|
fldr.ParentUID = ""
|
|
folderService.ExpectedFolder = fldr
|
|
folderService.ExpectedFolders = []*folder.Folder{fldr}
|
|
ruleStore.Folders[1] = append(ruleStore.Folders[1], fldr)
|
|
|
|
response := srv.RouteConvertPrometheusDeleteNamespace(rc, "non-existent")
|
|
require.Equal(t, http.StatusNotFound, response.Status())
|
|
})
|
|
|
|
t.Run("valid request should delete rules", func(t *testing.T) {
|
|
initNamespace := func(promDefinition string, opts ...convertPrometheusSrvOptionsFunc) (*ConvertPrometheusSrv, *fakes.RuleStore, *folder.Folder, *models.AlertRule) {
|
|
folderService := foldertest.NewFakeService()
|
|
srv, _, ruleStore := createConvertPrometheusSrv(t, append(opts, withFolderService(folderService))...)
|
|
|
|
// Create a folder in the root
|
|
fldr := randFolder()
|
|
fldr.ParentUID = ""
|
|
folderService.ExpectedFolder = fldr
|
|
folderService.ExpectedFolders = []*folder.Folder{fldr}
|
|
ruleStore.Folders[1] = append(ruleStore.Folders[1], fldr)
|
|
|
|
rule := models.RuleGen.
|
|
With(models.RuleGen.WithNamespaceUID(fldr.UID)).
|
|
With(models.RuleGen.WithOrgID(1)).
|
|
With(models.RuleGen.WithPrometheusOriginalRuleDefinition(promDefinition)).
|
|
GenerateRef()
|
|
ruleStore.PutRule(context.Background(), rule)
|
|
|
|
return srv, ruleStore, fldr, rule
|
|
}
|
|
|
|
t.Run("valid request should delete rules", func(t *testing.T) {
|
|
srv, ruleStore, fldr, rule := initNamespace("prometheus definition")
|
|
|
|
// Create another rule group in a different namespace that should not be deleted
|
|
otherGroupName := "other-group"
|
|
otherRule := models.RuleGen.
|
|
With(models.RuleGen.WithOrgID(1)).
|
|
With(models.RuleGen.WithGroupName(otherGroupName)).
|
|
With(models.RuleGen.WithPrometheusOriginalRuleDefinition("other prometheus definition")).
|
|
GenerateRef()
|
|
ruleStore.PutRule(context.Background(), otherRule)
|
|
|
|
rc := createRequestCtx()
|
|
|
|
response := srv.RouteConvertPrometheusDeleteNamespace(rc, fldr.Title)
|
|
require.Equal(t, http.StatusAccepted, response.Status())
|
|
|
|
// Verify the rule in the specified group was deleted
|
|
remaining, err := ruleStore.GetAlertRuleByUID(context.Background(), &models.GetAlertRuleByUIDQuery{
|
|
UID: rule.UID,
|
|
OrgID: rule.OrgID,
|
|
})
|
|
require.Error(t, err)
|
|
require.Nil(t, remaining)
|
|
|
|
// Verify the rule in the other group still exists
|
|
remainingOther, err := ruleStore.GetAlertRuleByUID(context.Background(), &models.GetAlertRuleByUIDQuery{
|
|
UID: otherRule.UID,
|
|
OrgID: otherRule.OrgID,
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, remainingOther)
|
|
})
|
|
|
|
t.Run("fails to delete rules when they are provisioned", func(t *testing.T) {
|
|
provenanceStore := fakes.NewFakeProvisioningStore()
|
|
srv, ruleStore, fldr, rule := initNamespace("", withProvenanceStore(provenanceStore))
|
|
rc := createRequestCtx()
|
|
|
|
// Create a provisioned rule
|
|
rule2 := models.RuleGen.
|
|
With(models.RuleGen.WithNamespaceUID(fldr.UID)).
|
|
With(models.RuleGen.WithOrgID(1)).
|
|
With(models.RuleGen.WithPrometheusOriginalRuleDefinition("prometheus definition")).
|
|
GenerateRef()
|
|
ruleStore.PutRule(context.Background(), rule2)
|
|
err := provenanceStore.SetProvenance(context.Background(), rule2, 1, models.ProvenanceAPI)
|
|
require.NoError(t, err)
|
|
|
|
response := srv.RouteConvertPrometheusDeleteNamespace(rc, fldr.Title)
|
|
require.Equal(t, http.StatusConflict, response.Status())
|
|
|
|
// Verify the rule is still present
|
|
remaining, err := ruleStore.GetAlertRuleByUID(context.Background(), &models.GetAlertRuleByUIDQuery{
|
|
UID: rule.UID,
|
|
OrgID: rule.OrgID,
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, remaining)
|
|
})
|
|
|
|
t.Run("with disable provenance header should still be able to delete rules", func(t *testing.T) {
|
|
provenanceStore := fakes.NewFakeProvisioningStore()
|
|
srv, ruleStore, fldr, rule := initNamespace("prometheus definition", withProvenanceStore(provenanceStore))
|
|
|
|
// Mark the rule as provisioned with API provenance
|
|
err := provenanceStore.SetProvenance(context.Background(), rule, 1, models.ProvenanceConvertedPrometheus)
|
|
require.NoError(t, err)
|
|
|
|
rc := createRequestCtx()
|
|
rc.Req.Header.Set("X-Disable-Provenance", "true")
|
|
|
|
response := srv.RouteConvertPrometheusDeleteNamespace(rc, fldr.Title)
|
|
require.Equal(t, http.StatusAccepted, response.Status())
|
|
|
|
// Verify the rule was deleted
|
|
remaining, err := ruleStore.GetAlertRuleByUID(context.Background(), &models.GetAlertRuleByUIDQuery{
|
|
UID: rule.UID,
|
|
OrgID: rule.OrgID,
|
|
})
|
|
require.Error(t, err)
|
|
require.Nil(t, remaining)
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestRouteConvertPrometheusDeleteRuleGroup(t *testing.T) {
|
|
t.Run("for non-existent folder should return 404", func(t *testing.T) {
|
|
srv, _, _ := createConvertPrometheusSrv(t)
|
|
rc := createRequestCtx()
|
|
|
|
response := srv.RouteConvertPrometheusDeleteRuleGroup(rc, "non-existent", "test-group")
|
|
require.Equal(t, http.StatusNotFound, response.Status())
|
|
})
|
|
|
|
t.Run("for existing folder with no group should return 404", func(t *testing.T) {
|
|
folderService := foldertest.NewFakeService()
|
|
srv, _, ruleStore := createConvertPrometheusSrv(t, withFolderService(folderService))
|
|
rc := createRequestCtx()
|
|
|
|
fldr := randFolder()
|
|
fldr.ParentUID = ""
|
|
folderService.ExpectedFolder = fldr
|
|
folderService.ExpectedFolders = []*folder.Folder{fldr}
|
|
ruleStore.Folders[1] = append(ruleStore.Folders[1], fldr)
|
|
|
|
response := srv.RouteConvertPrometheusDeleteRuleGroup(rc, fldr.Title, "test-group")
|
|
require.Equal(t, http.StatusNotFound, response.Status())
|
|
})
|
|
|
|
const groupName = "test-group"
|
|
|
|
t.Run("valid request should delete rules", func(t *testing.T) {
|
|
initGroup := func(promDefinition string, groupName string, opts ...convertPrometheusSrvOptionsFunc) (*ConvertPrometheusSrv, *fakes.RuleStore, *folder.Folder, *models.AlertRule) {
|
|
folderService := foldertest.NewFakeService()
|
|
srv, _, ruleStore := createConvertPrometheusSrv(t, append(opts, withFolderService(folderService))...)
|
|
|
|
// Create a folder in the root
|
|
fldr := randFolder()
|
|
fldr.ParentUID = ""
|
|
folderService.ExpectedFolder = fldr
|
|
folderService.ExpectedFolders = []*folder.Folder{fldr}
|
|
ruleStore.Folders[1] = append(ruleStore.Folders[1], fldr)
|
|
|
|
rule := models.RuleGen.
|
|
With(models.RuleGen.WithNamespaceUID(fldr.UID)).
|
|
With(models.RuleGen.WithOrgID(1)).
|
|
With(models.RuleGen.WithGroupName(groupName)).
|
|
With(models.RuleGen.WithPrometheusOriginalRuleDefinition(promDefinition)).
|
|
GenerateRef()
|
|
ruleStore.PutRule(context.Background(), rule)
|
|
|
|
return srv, ruleStore, fldr, rule
|
|
}
|
|
|
|
t.Run("valid request should delete rules", func(t *testing.T) {
|
|
srv, ruleStore, fldr, rule := initGroup("prometheus definition", groupName)
|
|
rc := createRequestCtx()
|
|
|
|
// Create another rule in a different group that should not be deleted
|
|
otherGroupName := "other-group"
|
|
otherRule := models.RuleGen.
|
|
With(models.RuleGen.WithNamespaceUID(fldr.UID)).
|
|
With(models.RuleGen.WithOrgID(1)).
|
|
With(models.RuleGen.WithGroupName(otherGroupName)).
|
|
With(models.RuleGen.WithPrometheusOriginalRuleDefinition("other prometheus definition")).
|
|
GenerateRef()
|
|
ruleStore.PutRule(context.Background(), otherRule)
|
|
|
|
response := srv.RouteConvertPrometheusDeleteRuleGroup(rc, fldr.Title, groupName)
|
|
require.Equal(t, http.StatusAccepted, response.Status())
|
|
|
|
// Verify the rule was deleted
|
|
remaining, err := ruleStore.GetAlertRuleByUID(context.Background(), &models.GetAlertRuleByUIDQuery{
|
|
UID: rule.UID,
|
|
OrgID: rule.OrgID,
|
|
})
|
|
require.Error(t, err)
|
|
require.Nil(t, remaining)
|
|
|
|
// Verify the otherRule from the "other-group" is still present
|
|
otherRuleRefreshed, err := ruleStore.GetAlertRuleByUID(context.Background(), &models.GetAlertRuleByUIDQuery{
|
|
UID: otherRule.UID,
|
|
OrgID: otherRule.OrgID,
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, otherRuleRefreshed)
|
|
})
|
|
|
|
t.Run("fails to delete rules when they are provisioned", func(t *testing.T) {
|
|
provenanceStore := fakes.NewFakeProvisioningStore()
|
|
srv, ruleStore, fldr, rule := initGroup("", groupName, withProvenanceStore(provenanceStore))
|
|
rc := createRequestCtx()
|
|
|
|
// Create a provisioned rule
|
|
rule2 := models.RuleGen.
|
|
With(models.RuleGen.WithNamespaceUID(fldr.UID)).
|
|
With(models.RuleGen.WithOrgID(1)).
|
|
With(models.RuleGen.WithGroupName(groupName)).
|
|
With(models.RuleGen.WithPrometheusOriginalRuleDefinition("prometheus definition")).
|
|
GenerateRef()
|
|
ruleStore.PutRule(context.Background(), rule2)
|
|
err := provenanceStore.SetProvenance(context.Background(), rule2, 1, models.ProvenanceAPI)
|
|
require.NoError(t, err)
|
|
|
|
response := srv.RouteConvertPrometheusDeleteRuleGroup(rc, fldr.Title, groupName)
|
|
require.Equal(t, http.StatusConflict, response.Status())
|
|
|
|
// Verify the rule is still present
|
|
remaining, err := ruleStore.GetAlertRuleByUID(context.Background(), &models.GetAlertRuleByUIDQuery{
|
|
UID: rule.UID,
|
|
OrgID: rule.OrgID,
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, remaining)
|
|
})
|
|
|
|
t.Run("with disable provenance header should still be able to delete rules", func(t *testing.T) {
|
|
provenanceStore := fakes.NewFakeProvisioningStore()
|
|
srv, ruleStore, fldr, rule := initGroup("", groupName, withProvenanceStore(provenanceStore))
|
|
|
|
// Mark the rule as provisioned with API provenance
|
|
err := provenanceStore.SetProvenance(context.Background(), rule, 1, models.ProvenanceConvertedPrometheus)
|
|
require.NoError(t, err)
|
|
|
|
rc := createRequestCtx()
|
|
rc.Req.Header.Set("X-Disable-Provenance", "true")
|
|
|
|
response := srv.RouteConvertPrometheusDeleteRuleGroup(rc, fldr.Title, groupName)
|
|
require.Equal(t, http.StatusAccepted, response.Status())
|
|
|
|
// Verify the rule was deleted
|
|
remaining, err := ruleStore.GetAlertRuleByUID(context.Background(), &models.GetAlertRuleByUIDQuery{
|
|
UID: rule.UID,
|
|
OrgID: rule.OrgID,
|
|
})
|
|
require.Error(t, err)
|
|
require.Nil(t, remaining)
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestRouteConvertPrometheusPostRuleGroups(t *testing.T) {
|
|
folderService := foldertest.NewFakeService()
|
|
srv, _, ruleStore := createConvertPrometheusSrv(t, withFolderService(folderService))
|
|
|
|
req := createRequestCtx()
|
|
req.Req.Header.Set(datasourceUIDHeader, existingDSUID)
|
|
|
|
// Create test prometheus rules
|
|
promAlertRule := apimodels.PrometheusRule{
|
|
Alert: "TestAlert",
|
|
Expr: "up == 0",
|
|
For: util.Pointer(prommodel.Duration(5 * time.Minute)),
|
|
Labels: map[string]string{
|
|
"severity": "critical",
|
|
},
|
|
}
|
|
|
|
promRecordingRule := apimodels.PrometheusRule{
|
|
Record: "TestRecordingRule",
|
|
Expr: "up == 0",
|
|
}
|
|
|
|
promGroup1 := apimodels.PrometheusRuleGroup{
|
|
Name: "TestGroup1",
|
|
Interval: prommodel.Duration(1 * time.Minute),
|
|
Rules: []apimodels.PrometheusRule{promAlertRule},
|
|
Labels: map[string]string{
|
|
"group_label": "group_value",
|
|
},
|
|
}
|
|
|
|
queryOffset := prommodel.Duration(5 * time.Minute)
|
|
promGroup2 := apimodels.PrometheusRuleGroup{
|
|
Name: "TestGroup2",
|
|
Interval: prommodel.Duration(1 * time.Minute),
|
|
Rules: []apimodels.PrometheusRule{promAlertRule},
|
|
QueryOffset: &queryOffset,
|
|
}
|
|
|
|
promGroup3 := apimodels.PrometheusRuleGroup{
|
|
Name: "TestGroup3",
|
|
Interval: prommodel.Duration(1 * time.Minute),
|
|
Rules: []apimodels.PrometheusRule{promAlertRule, promRecordingRule},
|
|
}
|
|
|
|
promGroups := map[string][]apimodels.PrometheusRuleGroup{
|
|
"namespace1": {promGroup1, promGroup2},
|
|
"namespace2": {promGroup3},
|
|
}
|
|
|
|
t.Run("should convert prometheus rules to Grafana rules", func(t *testing.T) {
|
|
// Call the endpoint
|
|
response := srv.RouteConvertPrometheusPostRuleGroups(req, promGroups)
|
|
require.Equal(t, http.StatusAccepted, response.Status())
|
|
|
|
// Verify the rules were created
|
|
rules, err := ruleStore.ListAlertRules(req.Req.Context(), &models.ListAlertRulesQuery{
|
|
OrgID: req.GetOrgID(),
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, rules, 4)
|
|
|
|
// Verify rule content
|
|
for _, rule := range rules {
|
|
require.Equal(t, int64(60), rule.IntervalSeconds) // 1 minute interval
|
|
|
|
// Check that the rule matches one of our original prometheus rules
|
|
switch rule.RuleGroup {
|
|
case "TestGroup1":
|
|
require.Equal(t, "TestAlert", rule.Title)
|
|
require.Equal(t, "critical", rule.Labels["severity"])
|
|
require.Equal(t, 5*time.Minute, rule.For)
|
|
require.Equal(t, "group_value", rule.Labels["group_label"])
|
|
case "TestGroup2":
|
|
require.Equal(t, "TestAlert", rule.Title)
|
|
require.Equal(t, "critical", rule.Labels["severity"])
|
|
require.Equal(t, 5*time.Minute, rule.For)
|
|
require.Equal(t, models.Duration(queryOffset), rule.Data[0].RelativeTimeRange.To)
|
|
case "TestGroup3":
|
|
switch rule.Title {
|
|
case "TestAlert":
|
|
require.Equal(t, "critical", rule.Labels["severity"])
|
|
require.Equal(t, 5*time.Minute, rule.For)
|
|
case "TestRecordingRule":
|
|
require.Equal(t, "TestRecordingRule", rule.Record.Metric)
|
|
default:
|
|
t.Fatalf("unexpected rule title: %s", rule.Title)
|
|
}
|
|
default:
|
|
t.Fatalf("unexpected rule group: %s", rule.RuleGroup)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("should convert Prometheus rules to Grafana rules but pause recording rules", func(t *testing.T) {
|
|
clear(ruleStore.Rules)
|
|
|
|
req.Req.Header.Set(alertRulesPausedHeader, "false")
|
|
req.Req.Header.Set(recordingRulesPausedHeader, "true")
|
|
|
|
// Call the endpoint
|
|
response := srv.RouteConvertPrometheusPostRuleGroups(req, promGroups)
|
|
require.Equal(t, http.StatusAccepted, response.Status())
|
|
|
|
// Verify the rules were created
|
|
rules, err := ruleStore.ListAlertRules(req.Req.Context(), &models.ListAlertRulesQuery{
|
|
OrgID: req.GetOrgID(),
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, rules, 4)
|
|
|
|
// Verify the recording rule is paused
|
|
for _, rule := range rules {
|
|
if rule.Record != nil {
|
|
require.True(t, rule.IsPaused)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("should convert Prometheus rules to Grafana rules but pause alert rules", func(t *testing.T) {
|
|
clear(ruleStore.Rules)
|
|
|
|
req.Req.Header.Set(alertRulesPausedHeader, "true")
|
|
req.Req.Header.Set(recordingRulesPausedHeader, "false")
|
|
|
|
// Call the endpoint
|
|
response := srv.RouteConvertPrometheusPostRuleGroups(req, promGroups)
|
|
require.Equal(t, http.StatusAccepted, response.Status())
|
|
|
|
// Verify the rules were created
|
|
rules, err := ruleStore.ListAlertRules(req.Req.Context(), &models.ListAlertRulesQuery{
|
|
OrgID: req.GetOrgID(),
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, rules, 4)
|
|
|
|
// Verify the alert rule is paused
|
|
for _, rule := range rules {
|
|
if rule.Record == nil {
|
|
require.True(t, rule.IsPaused)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("should convert Prometheus rules to Grafana rules but pause both alert and recording rules", func(t *testing.T) {
|
|
clear(ruleStore.Rules)
|
|
|
|
req.Req.Header.Set(recordingRulesPausedHeader, "true")
|
|
req.Req.Header.Set(alertRulesPausedHeader, "true")
|
|
|
|
// Call the endpoint
|
|
response := srv.RouteConvertPrometheusPostRuleGroups(req, promGroups)
|
|
require.Equal(t, http.StatusAccepted, response.Status())
|
|
|
|
// Verify the rules were created
|
|
rules, err := ruleStore.ListAlertRules(req.Req.Context(), &models.ListAlertRulesQuery{
|
|
OrgID: req.GetOrgID(),
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, rules, 4)
|
|
|
|
// Verify the alert rule is paused
|
|
for _, rule := range rules {
|
|
require.True(t, rule.IsPaused)
|
|
}
|
|
})
|
|
|
|
t.Run("convert Prometheus rules to Grafana rules into a specified target folder", func(t *testing.T) {
|
|
clear(ruleStore.Rules)
|
|
|
|
// Create a target folder to move the rules into
|
|
fldr := randFolder()
|
|
fldr.ParentUID = ""
|
|
folderService.ExpectedFolder = fldr
|
|
folderService.ExpectedFolders = []*folder.Folder{fldr}
|
|
ruleStore.Folders[1] = append(ruleStore.Folders[1], fldr)
|
|
|
|
req.Req.Header.Del(recordingRulesPausedHeader)
|
|
req.Req.Header.Del(alertRulesPausedHeader)
|
|
req.Req.Header.Set(folderUIDHeader, fldr.UID)
|
|
|
|
// Call the endpoint
|
|
response := srv.RouteConvertPrometheusPostRuleGroups(req, promGroups)
|
|
require.Equal(t, http.StatusAccepted, response.Status())
|
|
|
|
// Verify the rules were created
|
|
rules, err := ruleStore.ListAlertRules(req.Req.Context(), &models.ListAlertRulesQuery{
|
|
OrgID: req.GetOrgID(),
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
require.Len(t, rules, 4)
|
|
|
|
for _, rule := range rules {
|
|
parentFolders, err := folderService.GetParents(context.Background(), folder.GetParentsQuery{UID: rule.NamespaceUID, OrgID: 1})
|
|
require.NoError(t, err)
|
|
require.Len(t, parentFolders, 1)
|
|
require.Equal(t, fldr.UID, parentFolders[0].UID)
|
|
}
|
|
})
|
|
}
|
|
|
|
type convertPrometheusSrvOptions struct {
|
|
provenanceStore provisioning.ProvisioningStore
|
|
fakeAccessControlRuleService *acfakes.FakeRuleService
|
|
quotaChecker *provisioning.MockQuotaChecker
|
|
featureToggles featuremgmt.FeatureToggles
|
|
alertmanager Alertmanager
|
|
folderService folder.Service
|
|
}
|
|
|
|
type convertPrometheusSrvOptionsFunc func(*convertPrometheusSrvOptions)
|
|
|
|
func withProvenanceStore(store provisioning.ProvisioningStore) convertPrometheusSrvOptionsFunc {
|
|
return func(opts *convertPrometheusSrvOptions) {
|
|
opts.provenanceStore = store
|
|
}
|
|
}
|
|
|
|
func withFakeAccessControlRuleService(service *acfakes.FakeRuleService) convertPrometheusSrvOptionsFunc {
|
|
return func(opts *convertPrometheusSrvOptions) {
|
|
opts.fakeAccessControlRuleService = service
|
|
}
|
|
}
|
|
|
|
func withQuotaChecker(checker *provisioning.MockQuotaChecker) convertPrometheusSrvOptionsFunc {
|
|
return func(opts *convertPrometheusSrvOptions) {
|
|
opts.quotaChecker = checker
|
|
}
|
|
}
|
|
|
|
func withFeatureToggles(toggles featuremgmt.FeatureToggles) convertPrometheusSrvOptionsFunc {
|
|
return func(opts *convertPrometheusSrvOptions) {
|
|
opts.featureToggles = toggles
|
|
}
|
|
}
|
|
|
|
func withAlertmanager(am Alertmanager) convertPrometheusSrvOptionsFunc {
|
|
return func(opts *convertPrometheusSrvOptions) {
|
|
opts.alertmanager = am
|
|
}
|
|
}
|
|
|
|
func withFolderService(f folder.Service) convertPrometheusSrvOptionsFunc {
|
|
return func(opts *convertPrometheusSrvOptions) {
|
|
opts.folderService = f
|
|
}
|
|
}
|
|
|
|
func createConvertPrometheusSrv(t *testing.T, opts ...convertPrometheusSrvOptionsFunc) (*ConvertPrometheusSrv, *dsfakes.FakeCacheService, *fakes.RuleStore) {
|
|
t.Helper()
|
|
|
|
// By default the quota checker will allow the operation
|
|
quotas := &provisioning.MockQuotaChecker{}
|
|
quotas.EXPECT().LimitOK()
|
|
|
|
options := convertPrometheusSrvOptions{
|
|
provenanceStore: fakes.NewFakeProvisioningStore(),
|
|
fakeAccessControlRuleService: &acfakes.FakeRuleService{},
|
|
quotaChecker: quotas,
|
|
folderService: foldertest.NewFakeService(),
|
|
}
|
|
|
|
for _, opt := range opts {
|
|
opt(&options)
|
|
}
|
|
|
|
ruleStore := fakes.NewRuleStore(t)
|
|
folder := randFolder()
|
|
ruleStore.Folders[1] = append(ruleStore.Folders[1], folder)
|
|
|
|
dsCache := &dsfakes.FakeCacheService{}
|
|
ds := &datasources.DataSource{
|
|
UID: existingDSUID,
|
|
Type: datasources.DS_PROMETHEUS,
|
|
}
|
|
dsCache.DataSources = append(dsCache.DataSources, ds)
|
|
|
|
alertRuleService := provisioning.NewAlertRuleService(
|
|
ruleStore,
|
|
options.provenanceStore,
|
|
options.folderService,
|
|
options.quotaChecker,
|
|
&provisioning.NopTransactionManager{},
|
|
60,
|
|
10,
|
|
100,
|
|
log.New("test"),
|
|
&provisioning.NotificationSettingsValidatorProviderFake{},
|
|
options.fakeAccessControlRuleService,
|
|
)
|
|
|
|
cfg := &setting.UnifiedAlertingSettings{
|
|
DefaultRuleEvaluationInterval: 1 * time.Minute,
|
|
RecordingRules: setting.RecordingRuleSettings{
|
|
Enabled: true,
|
|
},
|
|
}
|
|
|
|
srv := NewConvertPrometheusSrv(cfg, log.NewNopLogger(), ruleStore, dsCache, alertRuleService, options.featureToggles, options.alertmanager)
|
|
|
|
return srv, dsCache, ruleStore
|
|
}
|
|
|
|
func createRequestCtx() *contextmodel.ReqContext {
|
|
req := httptest.NewRequest("GET", "http://localhost", nil)
|
|
req.Header.Set(datasourceUIDHeader, existingDSUID)
|
|
|
|
return &contextmodel.ReqContext{
|
|
Context: &web.Context{
|
|
Req: req,
|
|
Resp: web.NewResponseWriter("GET", httptest.NewRecorder()),
|
|
},
|
|
SignedInUser: &user.SignedInUser{OrgID: 1},
|
|
}
|
|
}
|
|
|
|
// Test parseBooleanHeader function which handles boolean header values
|
|
func TestParseBooleanHeader(t *testing.T) {
|
|
headerName := "X-Test-Header"
|
|
|
|
t.Run("should return false when header is not present", func(t *testing.T) {
|
|
result, err := parseBooleanHeader("", headerName)
|
|
require.NoError(t, err)
|
|
require.False(t, result)
|
|
})
|
|
|
|
t.Run("should return true when header is 'true'", func(t *testing.T) {
|
|
result, err := parseBooleanHeader("true", headerName)
|
|
require.NoError(t, err)
|
|
require.True(t, result)
|
|
})
|
|
|
|
t.Run("should return false when header is 'false'", func(t *testing.T) {
|
|
result, err := parseBooleanHeader("false", headerName)
|
|
require.NoError(t, err)
|
|
require.False(t, result)
|
|
})
|
|
|
|
t.Run("should return true when header is 'TRUE' (case insensitive)", func(t *testing.T) {
|
|
result, err := parseBooleanHeader("TRUE", headerName)
|
|
require.NoError(t, err)
|
|
require.True(t, result)
|
|
})
|
|
|
|
t.Run("should return error when header has invalid value", func(t *testing.T) {
|
|
_, err := parseBooleanHeader("invalid", headerName)
|
|
require.Error(t, err)
|
|
require.ErrorContains(t, err, "Invalid value for header")
|
|
})
|
|
|
|
t.Run("should return error when header is numeric but not 0/1", func(t *testing.T) {
|
|
_, err := parseBooleanHeader("2", headerName)
|
|
require.Error(t, err)
|
|
})
|
|
}
|
|
|
|
func TestGetWorkingFolderUID(t *testing.T) {
|
|
t.Run("should return root folder UID when header is not present", func(t *testing.T) {
|
|
rc := createRequestCtx()
|
|
rc.Req.Header.Del(folderUIDHeader)
|
|
|
|
folderUID := getWorkingFolderUID(rc)
|
|
require.Equal(t, folder.RootFolderUID, folderUID)
|
|
})
|
|
|
|
t.Run("should return specified folder UID when header is present", func(t *testing.T) {
|
|
rc := createRequestCtx()
|
|
specifiedFolderUID := "specified-folder-uid"
|
|
rc.Req.Header.Set(folderUIDHeader, specifiedFolderUID)
|
|
|
|
folderUID := getWorkingFolderUID(rc)
|
|
require.Equal(t, specifiedFolderUID, folderUID)
|
|
})
|
|
|
|
t.Run("should return root folder UID when header is empty", func(t *testing.T) {
|
|
rc := createRequestCtx()
|
|
rc.Req.Header.Set(folderUIDHeader, "")
|
|
|
|
folderUID := getWorkingFolderUID(rc)
|
|
require.Equal(t, folder.RootFolderUID, folderUID)
|
|
})
|
|
|
|
t.Run("should trim whitespace from header value", func(t *testing.T) {
|
|
rc := createRequestCtx()
|
|
specifiedFolderUID := "specified-folder-uid"
|
|
rc.Req.Header.Set(folderUIDHeader, " "+specifiedFolderUID+" ")
|
|
|
|
folderUID := getWorkingFolderUID(rc)
|
|
require.Equal(t, specifiedFolderUID, folderUID)
|
|
})
|
|
}
|
|
|
|
func TestGetProvenance(t *testing.T) {
|
|
t.Run("should return ProvenanceConvertedPrometheus when header is not present", func(t *testing.T) {
|
|
rc := createRequestCtx()
|
|
// Ensure the header is not present
|
|
rc.Req.Header.Del(disableProvenanceHeaderName)
|
|
|
|
provenance := getProvenance(rc)
|
|
require.Equal(t, models.ProvenanceConvertedPrometheus, provenance)
|
|
})
|
|
|
|
t.Run("should return ProvenanceNone when header is present", func(t *testing.T) {
|
|
rc := createRequestCtx()
|
|
// Set the disable provenance header
|
|
rc.Req.Header.Set(disableProvenanceHeaderName, "true")
|
|
|
|
provenance := getProvenance(rc)
|
|
require.Equal(t, models.ProvenanceNone, provenance)
|
|
})
|
|
|
|
t.Run("should return ProvenanceNone when header is present with any value", func(t *testing.T) {
|
|
rc := createRequestCtx()
|
|
// Set the disable provenance header with an empty value
|
|
rc.Req.Header.Set(disableProvenanceHeaderName, "")
|
|
|
|
provenance := getProvenance(rc)
|
|
require.Equal(t, models.ProvenanceNone, provenance)
|
|
})
|
|
}
|
|
|
|
type mockAlertmanager struct {
|
|
mock.Mock
|
|
}
|
|
|
|
func (m *mockAlertmanager) SaveAndApplyExtraConfiguration(ctx context.Context, org int64, extraConfig apimodels.ExtraConfiguration) error {
|
|
args := m.Called(ctx, org, extraConfig)
|
|
return args.Error(0)
|
|
}
|
|
|
|
func (m *mockAlertmanager) GetAlertmanagerConfiguration(ctx context.Context, org int64, withAutogen bool, withMergedExtraConfig bool) (apimodels.GettableUserConfig, error) {
|
|
args := m.Called(ctx, org, withAutogen, withMergedExtraConfig)
|
|
return args.Get(0).(apimodels.GettableUserConfig), args.Error(1)
|
|
}
|
|
|
|
func (m *mockAlertmanager) DeleteExtraConfiguration(ctx context.Context, org int64, identifier string) error {
|
|
args := m.Called(ctx, org, identifier)
|
|
return args.Error(0)
|
|
}
|
|
|
|
func TestRouteConvertPrometheusPostAlertmanagerConfig(t *testing.T) {
|
|
const identifier = "test-config"
|
|
mockAM := &mockAlertmanager{}
|
|
|
|
ft := featuremgmt.WithFeatures(featuremgmt.FlagAlertingImportAlertmanagerAPI)
|
|
srv, _, _ := createConvertPrometheusSrv(t, withAlertmanager(mockAM), withFeatureToggles(ft))
|
|
|
|
t.Run("should parse headers and call SaveAndApplyExtraConfiguration", func(t *testing.T) {
|
|
mockAM.On("SaveAndApplyExtraConfiguration", mock.Anything, int64(1), mock.MatchedBy(func(extraConfig apimodels.ExtraConfiguration) bool {
|
|
return extraConfig.Identifier == identifier &&
|
|
len(extraConfig.MergeMatchers) == 2 &&
|
|
len(extraConfig.TemplateFiles) == 1 &&
|
|
extraConfig.TemplateFiles["test.tmpl"] == "{{ define \"test\" }}Hello{{ end }}"
|
|
})).Return(nil).Once()
|
|
|
|
rc := createRequestCtx()
|
|
rc.Req.Header.Set(configIdentifierHeader, identifier)
|
|
rc.Req.Header.Set(mergeMatchersHeader, "environment=production,team=backend")
|
|
|
|
amCfg := apimodels.AlertmanagerUserConfig{
|
|
AlertmanagerConfig: `{
|
|
"route": {
|
|
"receiver": "default"
|
|
},
|
|
"receivers": [
|
|
{
|
|
"name": "default"
|
|
}
|
|
]
|
|
}`,
|
|
TemplateFiles: map[string]string{
|
|
"test.tmpl": "{{ define \"test\" }}Hello{{ end }}",
|
|
},
|
|
}
|
|
|
|
response := srv.RouteConvertPrometheusPostAlertmanagerConfig(rc, amCfg)
|
|
|
|
require.Equal(t, http.StatusAccepted, response.Status())
|
|
mockAM.AssertExpectations(t)
|
|
})
|
|
|
|
t.Run("should use default identifier when header is missing", func(t *testing.T) {
|
|
rc := createRequestCtx()
|
|
rc.Req.Header.Set(mergeMatchersHeader, "test=value")
|
|
mockAM := &mockAlertmanager{}
|
|
mockAM.On("SaveAndApplyExtraConfiguration", mock.Anything, int64(1), mock.MatchedBy(func(extraConfig apimodels.ExtraConfiguration) bool {
|
|
return extraConfig.Identifier == defaultConfigIdentifier
|
|
})).Return(nil)
|
|
|
|
ft := featuremgmt.WithFeatures(featuremgmt.FlagAlertingImportAlertmanagerAPI)
|
|
srv, _, _ := createConvertPrometheusSrv(t, withAlertmanager(mockAM), withFeatureToggles(ft))
|
|
|
|
amCfg := apimodels.AlertmanagerUserConfig{
|
|
AlertmanagerConfig: `{
|
|
"route": {
|
|
"receiver": "default"
|
|
},
|
|
"receivers": [
|
|
{
|
|
"name": "default"
|
|
}
|
|
]
|
|
}`,
|
|
}
|
|
response := srv.RouteConvertPrometheusPostAlertmanagerConfig(rc, amCfg)
|
|
|
|
require.Equal(t, http.StatusAccepted, response.Status())
|
|
mockAM.AssertExpectations(t)
|
|
})
|
|
|
|
t.Run("should return error when merge matchers header has invalid format", func(t *testing.T) {
|
|
rc := createRequestCtx()
|
|
rc.Req.Header.Set(configIdentifierHeader, identifier)
|
|
rc.Req.Header.Set(mergeMatchersHeader, "invalid-format")
|
|
|
|
amCfg := apimodels.AlertmanagerUserConfig{}
|
|
response := srv.RouteConvertPrometheusPostAlertmanagerConfig(rc, amCfg)
|
|
|
|
require.Equal(t, http.StatusBadRequest, response.Status())
|
|
require.Contains(t, string(response.Body()), "format should be 'key=value,key2=value2'")
|
|
})
|
|
|
|
t.Run("should return error when alertmanager config has empty route", func(t *testing.T) {
|
|
rc := createRequestCtx()
|
|
rc.Req.Header.Set(configIdentifierHeader, identifier)
|
|
rc.Req.Header.Set(mergeMatchersHeader, "env=prod")
|
|
|
|
amCfg := apimodels.AlertmanagerUserConfig{
|
|
AlertmanagerConfig: `{
|
|
"receivers": [
|
|
{
|
|
"name": "default"
|
|
}
|
|
]
|
|
}`,
|
|
}
|
|
response := srv.RouteConvertPrometheusPostAlertmanagerConfig(rc, amCfg)
|
|
|
|
require.Equal(t, http.StatusBadRequest, response.Status())
|
|
require.Contains(t, string(response.Body()), "failed to parse alertmanager config")
|
|
})
|
|
}
|
|
|
|
func TestRouteConvertPrometheusGetAlertmanagerConfig(t *testing.T) {
|
|
const identifier = "test-config"
|
|
const orgID = int64(1)
|
|
|
|
t.Run("without feature flag should return 501", func(t *testing.T) {
|
|
ft := featuremgmt.WithFeatures()
|
|
srv, _, _ := createConvertPrometheusSrv(t, withFeatureToggles(ft))
|
|
|
|
rc := createRequestCtx()
|
|
rc.Req.Header.Set(configIdentifierHeader, identifier)
|
|
response := srv.RouteConvertPrometheusGetAlertmanagerConfig(rc)
|
|
|
|
require.Equal(t, http.StatusNotImplemented, response.Status())
|
|
})
|
|
|
|
t.Run("without config identifier header should use default identifier", func(t *testing.T) {
|
|
mockAM := &mockAlertmanager{}
|
|
mockAM.On("GetAlertmanagerConfiguration", mock.Anything, orgID, false, false).Return(apimodels.GettableUserConfig{
|
|
ExtraConfigs: []apimodels.ExtraConfiguration{
|
|
{
|
|
Identifier: defaultConfigIdentifier,
|
|
AlertmanagerConfig: `route:
|
|
receiver: default
|
|
receivers:
|
|
- name: default`,
|
|
},
|
|
},
|
|
}, nil)
|
|
ft := featuremgmt.WithFeatures(featuremgmt.FlagAlertingImportAlertmanagerAPI)
|
|
srv, _, _ := createConvertPrometheusSrv(t, withAlertmanager(mockAM), withFeatureToggles(ft))
|
|
|
|
rc := createRequestCtx()
|
|
response := srv.RouteConvertPrometheusGetAlertmanagerConfig(rc)
|
|
|
|
require.Equal(t, http.StatusOK, response.Status())
|
|
mockAM.AssertExpectations(t)
|
|
})
|
|
|
|
t.Run("with empty config identifier header should use default identifier", func(t *testing.T) {
|
|
mockAM := &mockAlertmanager{}
|
|
mockAM.On("GetAlertmanagerConfiguration", mock.Anything, orgID, false, false).Return(apimodels.GettableUserConfig{
|
|
ExtraConfigs: []apimodels.ExtraConfiguration{
|
|
{
|
|
Identifier: defaultConfigIdentifier,
|
|
AlertmanagerConfig: `route:
|
|
receiver: default
|
|
receivers:
|
|
- name: default`,
|
|
},
|
|
},
|
|
}, nil)
|
|
ft := featuremgmt.WithFeatures(featuremgmt.FlagAlertingImportAlertmanagerAPI)
|
|
srv, _, _ := createConvertPrometheusSrv(t, withAlertmanager(mockAM), withFeatureToggles(ft))
|
|
|
|
rc := createRequestCtx()
|
|
rc.Req.Header.Set(configIdentifierHeader, "")
|
|
response := srv.RouteConvertPrometheusGetAlertmanagerConfig(rc)
|
|
|
|
require.Equal(t, http.StatusOK, response.Status())
|
|
mockAM.AssertExpectations(t)
|
|
})
|
|
|
|
t.Run("should return config when it is found", func(t *testing.T) {
|
|
mockAM := &mockAlertmanager{}
|
|
ft := featuremgmt.WithFeatures(featuremgmt.FlagAlertingImportAlertmanagerAPI)
|
|
srv, _, _ := createConvertPrometheusSrv(t, withAlertmanager(mockAM), withFeatureToggles(ft))
|
|
|
|
// Create a config with secrets to check that they will be hided in the response.
|
|
expectedConfig := apimodels.GettableUserConfig{
|
|
ExtraConfigs: []apimodels.ExtraConfiguration{
|
|
{
|
|
Identifier: identifier,
|
|
TemplateFiles: map[string]string{
|
|
"test.tmpl": "{{ define \"test\" }}Hello{{ end }}",
|
|
},
|
|
AlertmanagerConfig: `route:
|
|
receiver: webhook
|
|
receivers:
|
|
- name: webhook
|
|
webhook_configs:
|
|
- url: "http://localhost/webhook"
|
|
http_config:
|
|
bearer_token: "some-token"
|
|
`,
|
|
},
|
|
},
|
|
}
|
|
|
|
mockAM.On("GetAlertmanagerConfiguration", mock.Anything, int64(1), false, false).Return(expectedConfig, nil).Once()
|
|
|
|
rc := createRequestCtx()
|
|
rc.Req.Header.Set(configIdentifierHeader, identifier)
|
|
response := srv.RouteConvertPrometheusGetAlertmanagerConfig(rc)
|
|
|
|
require.Equal(t, http.StatusOK, response.Status())
|
|
|
|
expectedResponse := `alertmanager_config: |
|
|
global:
|
|
resolve_timeout: 5m
|
|
http_config:
|
|
follow_redirects: true
|
|
enable_http2: true
|
|
smtp_hello: localhost
|
|
smtp_require_tls: true
|
|
pagerduty_url: https://events.pagerduty.com/v2/enqueue
|
|
opsgenie_api_url: https://api.opsgenie.com/
|
|
wechat_api_url: https://qyapi.weixin.qq.com/cgi-bin/
|
|
victorops_api_url: https://alert.victorops.com/integrations/generic/20131114/alert/
|
|
telegram_api_url: https://api.telegram.org
|
|
webex_api_url: https://webexapis.com/v1/messages
|
|
route:
|
|
receiver: webhook
|
|
continue: false
|
|
receivers:
|
|
- name: webhook
|
|
webhook_configs:
|
|
- send_resolved: true
|
|
http_config:
|
|
authorization:
|
|
type: Bearer
|
|
credentials: <secret>
|
|
follow_redirects: true
|
|
enable_http2: true
|
|
url: <secret>
|
|
url_file: ""
|
|
max_alerts: 0
|
|
timeout: 0s
|
|
templates: []
|
|
template_files:
|
|
test.tmpl: '{{ define "test" }}Hello{{ end }}'`
|
|
|
|
require.YAMLEq(t, expectedResponse, string(response.Body()))
|
|
mockAM.AssertExpectations(t)
|
|
})
|
|
|
|
t.Run("when config not found should return 404", func(t *testing.T) {
|
|
mockAM := &mockAlertmanager{}
|
|
ft := featuremgmt.WithFeatures(featuremgmt.FlagAlertingImportAlertmanagerAPI)
|
|
srv, _, _ := createConvertPrometheusSrv(t, withAlertmanager(mockAM), withFeatureToggles(ft))
|
|
|
|
expectedConfig := apimodels.GettableUserConfig{
|
|
ExtraConfigs: []apimodels.ExtraConfiguration{
|
|
{
|
|
Identifier: "other-config",
|
|
TemplateFiles: map[string]string{
|
|
"test.tmpl": "{{ define \"test\" }}Hello{{ end }}",
|
|
},
|
|
AlertmanagerConfig: `route:
|
|
receiver: default
|
|
receivers:
|
|
- name: default`,
|
|
},
|
|
},
|
|
}
|
|
|
|
mockAM.On("GetAlertmanagerConfiguration", mock.Anything, orgID, false, false).Return(expectedConfig, nil).Once()
|
|
|
|
rc := createRequestCtx()
|
|
rc.Req.Header.Set(configIdentifierHeader, identifier)
|
|
response := srv.RouteConvertPrometheusGetAlertmanagerConfig(rc)
|
|
|
|
require.Equal(t, http.StatusNotFound, response.Status())
|
|
mockAM.AssertExpectations(t)
|
|
})
|
|
|
|
t.Run("should return error when GetAlertmanagerConfiguration fails", func(t *testing.T) {
|
|
mockAM := &mockAlertmanager{}
|
|
ft := featuremgmt.WithFeatures(featuremgmt.FlagAlertingImportAlertmanagerAPI)
|
|
srv, _, _ := createConvertPrometheusSrv(t, withAlertmanager(mockAM), withFeatureToggles(ft))
|
|
|
|
mockAM.On("GetAlertmanagerConfiguration", mock.Anything, orgID, false, false).Return(apimodels.GettableUserConfig{}, errors.New("config error")).Once()
|
|
|
|
rc := createRequestCtx()
|
|
rc.Req.Header.Set(configIdentifierHeader, identifier)
|
|
response := srv.RouteConvertPrometheusGetAlertmanagerConfig(rc)
|
|
|
|
require.Equal(t, http.StatusInternalServerError, response.Status())
|
|
mockAM.AssertExpectations(t)
|
|
})
|
|
}
|
|
|
|
func TestParseMergeMatchersHeader(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
headerValue string
|
|
expectedError bool
|
|
expectedMatchers amconfig.Matchers
|
|
}{
|
|
{
|
|
name: "empty header should return error",
|
|
headerValue: "",
|
|
expectedError: true,
|
|
},
|
|
{
|
|
name: "single matcher should parse correctly",
|
|
headerValue: "env=prod",
|
|
expectedError: false,
|
|
expectedMatchers: amconfig.Matchers{
|
|
{Type: labels.MatchEqual, Name: "env", Value: "prod"},
|
|
},
|
|
},
|
|
{
|
|
name: "multiple matchers should be parsed correctly",
|
|
headerValue: "env=prod,team=alerting",
|
|
expectedError: false,
|
|
expectedMatchers: amconfig.Matchers{
|
|
{Type: labels.MatchEqual, Name: "env", Value: "prod"},
|
|
{Type: labels.MatchEqual, Name: "team", Value: "alerting"},
|
|
},
|
|
},
|
|
{
|
|
name: "matchers with spaces should be parsed correctly",
|
|
headerValue: " env = prod , team = alerting ",
|
|
expectedError: false,
|
|
expectedMatchers: amconfig.Matchers{
|
|
{Type: labels.MatchEqual, Name: "env", Value: "prod"},
|
|
{Type: labels.MatchEqual, Name: "team", Value: "alerting"},
|
|
},
|
|
},
|
|
{
|
|
name: "invalid format without equals should return error",
|
|
headerValue: "env:prod",
|
|
expectedError: true,
|
|
},
|
|
{
|
|
name: "empty key should return error",
|
|
headerValue: "=prod",
|
|
expectedError: true,
|
|
},
|
|
{
|
|
name: "empty value should return error",
|
|
headerValue: "env=",
|
|
expectedError: true,
|
|
},
|
|
{
|
|
name: "missing value should return error",
|
|
headerValue: "env",
|
|
expectedError: true,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
rc := createRequestCtx()
|
|
rc.Req.Header.Set(mergeMatchersHeader, tc.headerValue)
|
|
|
|
matchers, err := parseMergeMatchersHeader(rc)
|
|
|
|
if tc.expectedError {
|
|
require.Error(t, err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
require.ElementsMatch(t, tc.expectedMatchers, matchers)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseConfigIdentifierHeader(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
headerValue string
|
|
expectedValue string
|
|
expectedError bool
|
|
}{
|
|
{
|
|
name: "valid identifier should parse correctly",
|
|
headerValue: "test-config",
|
|
expectedValue: "test-config",
|
|
expectedError: false,
|
|
},
|
|
{
|
|
name: "identifier with spaces should be trimmed",
|
|
headerValue: " test-config ",
|
|
expectedValue: "test-config",
|
|
expectedError: false,
|
|
},
|
|
{
|
|
name: "empty identifier should return the default value",
|
|
headerValue: "",
|
|
expectedValue: defaultConfigIdentifier,
|
|
},
|
|
{
|
|
name: "whitespace only identifier should return the default value",
|
|
headerValue: " ",
|
|
expectedValue: defaultConfigIdentifier,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
rc := createRequestCtx()
|
|
rc.Req.Header.Set(configIdentifierHeader, tc.headerValue)
|
|
|
|
identifier := parseConfigIdentifierHeader(rc)
|
|
require.Equal(t, tc.expectedValue, identifier)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFormatMergeMatchers(t *testing.T) {
|
|
t.Run("empty matchers should return empty string", func(t *testing.T) {
|
|
result := formatMergeMatchers(nil)
|
|
require.Equal(t, "", result)
|
|
})
|
|
|
|
t.Run("single matcher should format correctly", func(t *testing.T) {
|
|
matchers := amconfig.Matchers{
|
|
&labels.Matcher{
|
|
Type: labels.MatchEqual,
|
|
Name: "env",
|
|
Value: "prod",
|
|
},
|
|
}
|
|
result := formatMergeMatchers(matchers)
|
|
require.Equal(t, "env=prod", result)
|
|
})
|
|
|
|
t.Run("multiple matchers should format correctly", func(t *testing.T) {
|
|
matchers := amconfig.Matchers{
|
|
&labels.Matcher{
|
|
Type: labels.MatchEqual,
|
|
Name: "env",
|
|
Value: "prod",
|
|
},
|
|
&labels.Matcher{
|
|
Type: labels.MatchEqual,
|
|
Name: "team",
|
|
Value: "backend",
|
|
},
|
|
}
|
|
result := formatMergeMatchers(matchers)
|
|
require.Equal(t, "env=prod,team=backend", result)
|
|
})
|
|
}
|
|
|
|
func TestRouteConvertPrometheusDeleteAlertmanagerConfig(t *testing.T) {
|
|
const identifier = "test-config"
|
|
const orgID = int64(1)
|
|
|
|
mockAM := &mockAlertmanager{}
|
|
ft := featuremgmt.WithFeatures(featuremgmt.FlagAlertingImportAlertmanagerAPI)
|
|
srv, _, _ := createConvertPrometheusSrv(t, withAlertmanager(mockAM), withFeatureToggles(ft))
|
|
|
|
t.Run("should parse identifier header and call DeleteExtraConfiguration", func(t *testing.T) {
|
|
mockAM.On("DeleteExtraConfiguration", mock.Anything, orgID, identifier).Return(nil).Once()
|
|
|
|
rc := createRequestCtx()
|
|
rc.Req.Header.Set(configIdentifierHeader, identifier)
|
|
|
|
response := srv.RouteConvertPrometheusDeleteAlertmanagerConfig(rc)
|
|
|
|
require.Equal(t, http.StatusAccepted, response.Status())
|
|
mockAM.AssertExpectations(t)
|
|
})
|
|
|
|
t.Run("should use default identifier when header is missing", func(t *testing.T) {
|
|
mockAM.On("DeleteExtraConfiguration", mock.Anything, orgID, defaultConfigIdentifier).Return(nil).Once()
|
|
rc := createRequestCtx()
|
|
|
|
response := srv.RouteConvertPrometheusDeleteAlertmanagerConfig(rc)
|
|
|
|
require.Equal(t, http.StatusAccepted, response.Status())
|
|
mockAM.AssertExpectations(t)
|
|
})
|
|
|
|
t.Run("should return error when DeleteExtraConfiguration fails", func(t *testing.T) {
|
|
mockAM.On("DeleteExtraConfiguration", mock.Anything, orgID, identifier).Return(errors.New("delete error")).Once()
|
|
|
|
rc := createRequestCtx()
|
|
rc.Req.Header.Set(configIdentifierHeader, identifier)
|
|
|
|
response := srv.RouteConvertPrometheusDeleteAlertmanagerConfig(rc)
|
|
|
|
require.Equal(t, http.StatusInternalServerError, response.Status())
|
|
mockAM.AssertExpectations(t)
|
|
})
|
|
|
|
t.Run("should return not implemented when feature toggle is disabled", func(t *testing.T) {
|
|
ft := featuremgmt.WithFeatures()
|
|
srv, _, _ := createConvertPrometheusSrv(t, withAlertmanager(mockAM), withFeatureToggles(ft))
|
|
|
|
rc := createRequestCtx()
|
|
rc.Req.Header.Set(configIdentifierHeader, identifier)
|
|
|
|
response := srv.RouteConvertPrometheusDeleteAlertmanagerConfig(rc)
|
|
|
|
require.Equal(t, http.StatusNotImplemented, response.Status())
|
|
})
|
|
|
|
t.Run("should use default identifier for empty identifier header", func(t *testing.T) {
|
|
mockAM.On("DeleteExtraConfiguration", mock.Anything, orgID, defaultConfigIdentifier).Return(nil).Once()
|
|
rc := createRequestCtx()
|
|
rc.Req.Header.Set(configIdentifierHeader, "")
|
|
|
|
response := srv.RouteConvertPrometheusDeleteAlertmanagerConfig(rc)
|
|
|
|
require.Equal(t, http.StatusAccepted, response.Status())
|
|
mockAM.AssertExpectations(t)
|
|
})
|
|
}
|