Alerting: Add support for retrieving imported Prometheus Alertmanager configurations (#106864)

This commit is contained in:
Alexander Akhmetov
2025-06-18 10:49:40 +02:00
committed by GitHub
parent 4bfd7b6d7c
commit 8c6df8b449
9 changed files with 550 additions and 151 deletions
@@ -3,12 +3,16 @@ package api
import (
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"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"
"gopkg.in/yaml.v3"
@@ -60,7 +64,7 @@ func TestRouteConvertPrometheusPostRuleGroup(t *testing.T) {
}
t.Run("without datasource UID header should return 400", func(t *testing.T) {
srv, _, _, _ := createConvertPrometheusSrv(t)
srv, _, _ := createConvertPrometheusSrv(t)
rc := createRequestCtx()
rc.Req.Header.Set(datasourceUIDHeader, "")
@@ -71,7 +75,7 @@ func TestRouteConvertPrometheusPostRuleGroup(t *testing.T) {
})
t.Run("with invalid datasource should return error", func(t *testing.T) {
srv, _, _, _ := createConvertPrometheusSrv(t)
srv, _, _ := createConvertPrometheusSrv(t)
rc := createRequestCtx()
rc.Req.Header.Set(datasourceUIDHeader, "non-existing-ds")
@@ -81,7 +85,7 @@ func TestRouteConvertPrometheusPostRuleGroup(t *testing.T) {
})
t.Run("with rule group without evaluation interval should return 202", func(t *testing.T) {
srv, _, _, _ := createConvertPrometheusSrv(t)
srv, _, _ := createConvertPrometheusSrv(t)
rc := createRequestCtx()
response := srv.RouteConvertPrometheusPostRuleGroup(rc, "test", simpleGroup)
@@ -90,7 +94,8 @@ func TestRouteConvertPrometheusPostRuleGroup(t *testing.T) {
t.Run("should replace an existing rule group", func(t *testing.T) {
provenanceStore := fakes.NewFakeProvisioningStore()
srv, _, ruleStore, folderService := createConvertPrometheusSrv(t, withProvenanceStore(provenanceStore))
folderService := foldertest.NewFakeService()
srv, _, ruleStore := createConvertPrometheusSrv(t, withProvenanceStore(provenanceStore), withFolderService(folderService))
// Create a folder in the root
fldr := randFolder()
@@ -152,7 +157,8 @@ func TestRouteConvertPrometheusPostRuleGroup(t *testing.T) {
t.Run("should fail to replace a provisioned rule group", func(t *testing.T) {
provenanceStore := fakes.NewFakeProvisioningStore()
srv, _, ruleStore, folderService := createConvertPrometheusSrv(t, withProvenanceStore(provenanceStore))
folderService := foldertest.NewFakeService()
srv, _, ruleStore := createConvertPrometheusSrv(t, withProvenanceStore(provenanceStore), withFolderService(folderService))
// Create a folder in the root
fldr := randFolder()
@@ -187,7 +193,7 @@ func TestRouteConvertPrometheusPostRuleGroup(t *testing.T) {
t.Run("with no access to the datasource should return 403", func(t *testing.T) {
acFake := &acfakes.FakeRuleService{}
srv, _, _, _ := createConvertPrometheusSrv(t, withFakeAccessControlRuleService(acFake))
srv, _, _ := createConvertPrometheusSrv(t, withFakeAccessControlRuleService(acFake))
acFake.AuthorizeRuleChangesFunc = func(context.Context, identity.Requester, *store.GroupDelta) error {
return datasources.ErrDataSourceAccessDenied
@@ -203,7 +209,7 @@ func TestRouteConvertPrometheusPostRuleGroup(t *testing.T) {
quotas := &provisioning.MockQuotaChecker{}
quotas.EXPECT().LimitExceeded()
srv, _, _, _ := createConvertPrometheusSrv(t, withQuotaChecker(quotas))
srv, _, _ := createConvertPrometheusSrv(t, withQuotaChecker(quotas))
rc := createRequestCtx()
response := srv.RouteConvertPrometheusPostRuleGroup(rc, "folder", simpleGroup)
@@ -241,7 +247,7 @@ func TestRouteConvertPrometheusPostRuleGroup(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
srv, _, _, _ := createConvertPrometheusSrv(t)
srv, _, _ := createConvertPrometheusSrv(t)
rc := createRequestCtx()
rc.Req.Header.Set(tc.headerName, tc.headerValue)
@@ -274,7 +280,7 @@ func TestRouteConvertPrometheusPostRuleGroup(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
srv, _, _, _ := createConvertPrometheusSrv(t)
srv, _, _ := createConvertPrometheusSrv(t)
rc := createRequestCtx()
rc.Req.Header.Set(tc.headerName, tc.headerValue)
@@ -286,7 +292,7 @@ func TestRouteConvertPrometheusPostRuleGroup(t *testing.T) {
})
t.Run("with valid request should return 202", func(t *testing.T) {
srv, _, _, _ := createConvertPrometheusSrv(t)
srv, _, _ := createConvertPrometheusSrv(t)
rc := createRequestCtx()
response := srv.RouteConvertPrometheusPostRuleGroup(rc, "test", simpleGroup)
@@ -315,7 +321,7 @@ func TestRouteConvertPrometheusPostRuleGroup(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
features := featuremgmt.WithFeatures()
srv, _, _, _ := createConvertPrometheusSrv(t, withFeatureToggles(features))
srv, _, _ := createConvertPrometheusSrv(t, withFeatureToggles(features))
srv.cfg.RecordingRules.Enabled = tc.recordingRules
rc := createRequestCtx()
@@ -327,7 +333,8 @@ func TestRouteConvertPrometheusPostRuleGroup(t *testing.T) {
t.Run("with disable provenance header should use ProvenanceNone", func(t *testing.T) {
provenanceStore := fakes.NewFakeProvisioningStore()
srv, _, ruleStore, folderService := createConvertPrometheusSrv(t, withProvenanceStore(provenanceStore))
folderService := foldertest.NewFakeService()
srv, _, ruleStore := createConvertPrometheusSrv(t, withProvenanceStore(provenanceStore), withFolderService(folderService))
// Create a folder in the root
fldr := randFolder()
@@ -361,7 +368,7 @@ func TestRouteConvertPrometheusPostRuleGroup(t *testing.T) {
})
t.Run("returns error when target datasource does not exist", func(t *testing.T) {
srv, _, _, _ := createConvertPrometheusSrv(t)
srv, _, _ := createConvertPrometheusSrv(t)
rc := createRequestCtx()
rc.Req.Header.Set(targetDatasourceUIDHeader, "some-data-source")
@@ -371,7 +378,7 @@ func TestRouteConvertPrometheusPostRuleGroup(t *testing.T) {
})
t.Run("uses target datasource for recording rules", func(t *testing.T) {
srv, dsCache, ruleStore, _ := createConvertPrometheusSrv(t)
srv, dsCache, ruleStore := createConvertPrometheusSrv(t)
rc := createRequestCtx()
targetDSUID := util.GenerateShortUID()
ds := &datasources.DataSource{
@@ -408,7 +415,7 @@ func TestRouteConvertPrometheusPostRuleGroup(t *testing.T) {
})
t.Run("sets notification settings for rules if specified", func(t *testing.T) {
srv, _, ruleStore, _ := createConvertPrometheusSrv(t)
srv, _, ruleStore := createConvertPrometheusSrv(t)
rc := createRequestCtx()
receiver := "test-receiver"
@@ -450,7 +457,7 @@ func TestRouteConvertPrometheusPostRuleGroup(t *testing.T) {
})
t.Run("returns error when notification settings header contains invalid JSON", func(t *testing.T) {
srv, _, _, _ := createConvertPrometheusSrv(t)
srv, _, _ := createConvertPrometheusSrv(t)
rc := createRequestCtx()
rc.Req.Header.Set(notificationSettingsHeader, "{invalid json")
@@ -472,7 +479,7 @@ func TestRouteConvertPrometheusPostRuleGroup(t *testing.T) {
})
t.Run("returns error when notification settings contain invalid values", func(t *testing.T) {
srv, _, _, _ := createConvertPrometheusSrv(t)
srv, _, _ := createConvertPrometheusSrv(t)
rc := createRequestCtx()
settings := apimodels.AlertRuleNotificationSettings{
@@ -515,7 +522,7 @@ func TestRouteConvertPrometheusGetRuleGroup(t *testing.T) {
require.NoError(t, err)
t.Run("with non-existent folder should return 404", func(t *testing.T) {
srv, _, _, _ := createConvertPrometheusSrv(t)
srv, _, _ := createConvertPrometheusSrv(t)
rc := createRequestCtx()
response := srv.RouteConvertPrometheusGetRuleGroup(rc, "non-existent", "test")
@@ -523,7 +530,7 @@ func TestRouteConvertPrometheusGetRuleGroup(t *testing.T) {
})
t.Run("with non-existent group should return 404", func(t *testing.T) {
srv, _, _, _ := createConvertPrometheusSrv(t)
srv, _, _ := createConvertPrometheusSrv(t)
rc := createRequestCtx()
response := srv.RouteConvertPrometheusGetRuleGroup(rc, "test", "non-existent")
@@ -531,7 +538,8 @@ func TestRouteConvertPrometheusGetRuleGroup(t *testing.T) {
})
t.Run("with valid request should return 200", func(t *testing.T) {
srv, _, ruleStore, folderService := createConvertPrometheusSrv(t)
folderService := foldertest.NewFakeService()
srv, _, ruleStore := createConvertPrometheusSrv(t, withFolderService(folderService))
rc := createRequestCtx()
// Create two folders in the root folder
@@ -619,7 +627,7 @@ func TestRouteConvertPrometheusGetNamespace(t *testing.T) {
}
t.Run("with non-existent folder should return 404", func(t *testing.T) {
srv, _, _, _ := createConvertPrometheusSrv(t)
srv, _, _ := createConvertPrometheusSrv(t)
rc := createRequestCtx()
response := srv.RouteConvertPrometheusGetNamespace(rc, "non-existent")
@@ -627,7 +635,8 @@ func TestRouteConvertPrometheusGetNamespace(t *testing.T) {
})
t.Run("with valid request should return 200", func(t *testing.T) {
srv, _, ruleStore, folderService := createConvertPrometheusSrv(t)
folderService := foldertest.NewFakeService()
srv, _, ruleStore := createConvertPrometheusSrv(t, withFolderService(folderService))
rc := createRequestCtx()
// Create two folders in the root folder
@@ -731,12 +740,13 @@ func TestRouteConvertPrometheusGetRules(t *testing.T) {
}
t.Run("for non-existent folder should return empty response", func(t *testing.T) {
srv, _, _, _ := createConvertPrometheusSrv(t)
srv, _, _ := createConvertPrometheusSrv(t)
assertEmptyResponse(t, srv, rc)
})
t.Run("for existing folder with no children should return empty response", func(t *testing.T) {
srv, _, ruleStore, folderService := createConvertPrometheusSrv(t)
folderService := foldertest.NewFakeService()
srv, _, ruleStore := createConvertPrometheusSrv(t, withFolderService(folderService))
fldr := randFolder()
fldr.UID = unknownFolderUID
@@ -757,7 +767,8 @@ func TestRouteConvertPrometheusGetRules(t *testing.T) {
})
t.Run("with rules should return 200 with rules", func(t *testing.T) {
srv, _, ruleStore, folderService := createConvertPrometheusSrv(t)
folderService := foldertest.NewFakeService()
srv, _, ruleStore := createConvertPrometheusSrv(t, withFolderService(folderService))
rc := createRequestCtx()
// Create a folder in the root
@@ -797,7 +808,7 @@ func TestRouteConvertPrometheusGetRules(t *testing.T) {
func TestRouteConvertPrometheusDeleteNamespace(t *testing.T) {
t.Run("for non-existent folder should return 404", func(t *testing.T) {
srv, _, _, _ := createConvertPrometheusSrv(t)
srv, _, _ := createConvertPrometheusSrv(t)
rc := createRequestCtx()
response := srv.RouteConvertPrometheusDeleteNamespace(rc, "non-existent")
@@ -805,7 +816,8 @@ func TestRouteConvertPrometheusDeleteNamespace(t *testing.T) {
})
t.Run("for existing folder with no groups should return 404", func(t *testing.T) {
srv, _, ruleStore, folderService := createConvertPrometheusSrv(t)
folderService := foldertest.NewFakeService()
srv, _, ruleStore := createConvertPrometheusSrv(t, withFolderService(folderService))
rc := createRequestCtx()
fldr := randFolder()
@@ -820,7 +832,8 @@ func TestRouteConvertPrometheusDeleteNamespace(t *testing.T) {
t.Run("valid request should delete rules", func(t *testing.T) {
initNamespace := func(promDefinition string, opts ...convertPrometheusSrvOptionsFunc) (*ConvertPrometheusSrv, *fakes.RuleStore, *folder.Folder, *models.AlertRule) {
srv, _, ruleStore, folderService := createConvertPrometheusSrv(t, opts...)
folderService := foldertest.NewFakeService()
srv, _, ruleStore := createConvertPrometheusSrv(t, append(opts, withFolderService(folderService))...)
// Create a folder in the root
fldr := randFolder()
@@ -927,7 +940,7 @@ func TestRouteConvertPrometheusDeleteNamespace(t *testing.T) {
func TestRouteConvertPrometheusDeleteRuleGroup(t *testing.T) {
t.Run("for non-existent folder should return 404", func(t *testing.T) {
srv, _, _, _ := createConvertPrometheusSrv(t)
srv, _, _ := createConvertPrometheusSrv(t)
rc := createRequestCtx()
response := srv.RouteConvertPrometheusDeleteRuleGroup(rc, "non-existent", "test-group")
@@ -935,7 +948,8 @@ func TestRouteConvertPrometheusDeleteRuleGroup(t *testing.T) {
})
t.Run("for existing folder with no group should return 404", func(t *testing.T) {
srv, _, ruleStore, folderService := createConvertPrometheusSrv(t)
folderService := foldertest.NewFakeService()
srv, _, ruleStore := createConvertPrometheusSrv(t, withFolderService(folderService))
rc := createRequestCtx()
fldr := randFolder()
@@ -952,7 +966,8 @@ func TestRouteConvertPrometheusDeleteRuleGroup(t *testing.T) {
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) {
srv, _, ruleStore, folderService := createConvertPrometheusSrv(t, opts...)
folderService := foldertest.NewFakeService()
srv, _, ruleStore := createConvertPrometheusSrv(t, append(opts, withFolderService(folderService))...)
// Create a folder in the root
fldr := randFolder()
@@ -1060,7 +1075,8 @@ func TestRouteConvertPrometheusDeleteRuleGroup(t *testing.T) {
}
func TestRouteConvertPrometheusPostRuleGroups(t *testing.T) {
srv, _, ruleStore, folderService := createConvertPrometheusSrv(t)
folderService := foldertest.NewFakeService()
srv, _, ruleStore := createConvertPrometheusSrv(t, withFolderService(folderService))
req := createRequestCtx()
req.Req.Header.Set(datasourceUIDHeader, existingDSUID)
@@ -1265,6 +1281,8 @@ type convertPrometheusSrvOptions struct {
fakeAccessControlRuleService *acfakes.FakeRuleService
quotaChecker *provisioning.MockQuotaChecker
featureToggles featuremgmt.FeatureToggles
alertmanager Alertmanager
folderService folder.Service
}
type convertPrometheusSrvOptionsFunc func(*convertPrometheusSrvOptions)
@@ -1293,7 +1311,19 @@ func withFeatureToggles(toggles featuremgmt.FeatureToggles) convertPrometheusSrv
}
}
func createConvertPrometheusSrv(t *testing.T, opts ...convertPrometheusSrvOptionsFunc) (*ConvertPrometheusSrv, *dsfakes.FakeCacheService, *fakes.RuleStore, *foldertest.FakeService) {
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
@@ -1304,6 +1334,7 @@ func createConvertPrometheusSrv(t *testing.T, opts ...convertPrometheusSrvOption
provenanceStore: fakes.NewFakeProvisioningStore(),
fakeAccessControlRuleService: &acfakes.FakeRuleService{},
quotaChecker: quotas,
folderService: foldertest.NewFakeService(),
}
for _, opt := range opts {
@@ -1321,12 +1352,10 @@ func createConvertPrometheusSrv(t *testing.T, opts ...convertPrometheusSrvOption
}
dsCache.DataSources = append(dsCache.DataSources, ds)
folderService := foldertest.NewFakeService()
alertRuleService := provisioning.NewAlertRuleService(
ruleStore,
options.provenanceStore,
folderService,
options.folderService,
options.quotaChecker,
&provisioning.NopTransactionManager{},
60,
@@ -1344,9 +1373,9 @@ func createConvertPrometheusSrv(t *testing.T, opts ...convertPrometheusSrvOption
},
}
srv := NewConvertPrometheusSrv(cfg, log.NewNopLogger(), ruleStore, dsCache, alertRuleService, options.featureToggles)
srv := NewConvertPrometheusSrv(cfg, log.NewNopLogger(), ruleStore, dsCache, alertRuleService, options.featureToggles, options.alertmanager)
return srv, dsCache, ruleStore, folderService
return srv, dsCache, ruleStore
}
func createRequestCtx() *contextmodel.ReqContext {
@@ -1466,3 +1495,307 @@ func TestGetProvenance(t *testing.T) {
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) (apimodels.GettableUserConfig, error) {
args := m.Called(ctx, org, withAutogen)
return args.Get(0).(apimodels.GettableUserConfig), args.Error(1)
}
func (m *mockAlertmanager) DeleteAndApplyExtraConfiguration(ctx context.Context, org int64, identifier string) error {
args := m.Called(ctx, org, identifier)
return args.Error(0)
}
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 return 400", func(t *testing.T) {
mockAM := &mockAlertmanager{}
ft := featuremgmt.WithFeatures(featuremgmt.FlagAlertingImportAlertmanagerAPI)
srv, _, _ := createConvertPrometheusSrv(t, withAlertmanager(mockAM), withFeatureToggles(ft))
rc := createRequestCtx()
response := srv.RouteConvertPrometheusGetAlertmanagerConfig(rc)
require.Equal(t, http.StatusBadRequest, response.Status())
})
t.Run("with empty config identifier header should return 400", func(t *testing.T) {
mockAM := &mockAlertmanager{}
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.StatusBadRequest, response.Status())
})
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))
expectedConfig := apimodels.GettableUserConfig{
ExtraConfigs: []apimodels.ExtraConfiguration{
{
Identifier: identifier,
TemplateFiles: map[string]string{
"test.tmpl": "{{ define \"test\" }}Hello{{ end }}",
},
AlertmanagerConfig: `route:
receiver: default
receivers:
- name: default`,
},
},
}
mockAM.On("GetAlertmanagerConfiguration", mock.Anything, int64(1), 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: |-
route:
receiver: default
receivers:
- name: default
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).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).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 error",
headerValue: "",
expectedError: true,
},
{
name: "whitespace only identifier should return error",
headerValue: " ",
expectedError: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
rc := createRequestCtx()
rc.Req.Header.Set(configIdentifierHeader, tc.headerValue)
identifier, err := parseConfigIdentifierHeader(rc)
if tc.expectedError {
require.Error(t, err)
} else {
require.NoError(t, err)
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)
})
}