diff --git a/pkg/services/ngalert/notifier/alertmanager.go b/pkg/services/ngalert/notifier/alertmanager.go index 03b9280b62a..85e1e3b4085 100644 --- a/pkg/services/ngalert/notifier/alertmanager.go +++ b/pkg/services/ngalert/notifier/alertmanager.go @@ -504,7 +504,7 @@ func (am *Alertmanager) PutAlerts(postableAlerts apimodels.PostableAlerts) error am.Metrics.Resolved().Inc() } - if err := alert.Validate(); err != nil { + if err := validateAlert(alert); err != nil { if validationErr == nil { validationErr = &AlertValidationError{} } @@ -528,6 +528,59 @@ func (am *Alertmanager) PutAlerts(postableAlerts apimodels.PostableAlerts) error return nil } +// validateAlert is a.Validate() while additionally allowing +// space for label and annotation names. +func validateAlert(a *types.Alert) error { + if a.StartsAt.IsZero() { + return fmt.Errorf("start time missing") + } + if !a.EndsAt.IsZero() && a.EndsAt.Before(a.StartsAt) { + return fmt.Errorf("start time must be before end time") + } + if err := validateLabelSet(a.Labels); err != nil { + return fmt.Errorf("invalid label set: %s", err) + } + if len(a.Labels) == 0 { + return fmt.Errorf("at least one label pair required") + } + if err := validateLabelSet(a.Annotations); err != nil { + return fmt.Errorf("invalid annotations: %s", err) + } + return nil +} + +// validateLabelSet is ls.Validate() while additionally allowing +// space for label names. +func validateLabelSet(ls model.LabelSet) error { + for ln, lv := range ls { + if !isValidLabelName(ln) { + return fmt.Errorf("invalid name %q", ln) + } + if !lv.IsValid() { + return fmt.Errorf("invalid value %q", lv) + } + } + return nil +} + +// isValidLabelName is ln.IsValid() while additionally allowing spaces. +// The regex for Prometheus data model is ^[a-zA-Z_][a-zA-Z0-9_]*$ +// while we will follow ^[a-zA-Z_][a-zA-Z0-9_ ]*$ +func isValidLabelName(ln model.LabelName) bool { + if len(ln) == 0 { + return false + } + for i, b := range ln { + if !((b >= 'a' && b <= 'z') || + (b >= 'A' && b <= 'Z') || + b == '_' || + (i > 0 && (b == ' ' || (b >= '0' && b <= '9')))) { + return false + } + } + return true +} + // AlertValidationError is the error capturing the validation errors // faced on the alerts. type AlertValidationError struct { @@ -538,7 +591,7 @@ type AlertValidationError struct { func (e AlertValidationError) Error() string { errMsg := "" if len(e.Errors) != 0 { - errMsg := e.Errors[0].Error() + errMsg = e.Errors[0].Error() for _, e := range e.Errors[1:] { errMsg += ";" + e.Error() } diff --git a/pkg/services/ngalert/notifier/alertmanager_test.go b/pkg/services/ngalert/notifier/alertmanager_test.go index 2814add0263..38349d7bb29 100644 --- a/pkg/services/ngalert/notifier/alertmanager_test.go +++ b/pkg/services/ngalert/notifier/alertmanager_test.go @@ -182,6 +182,36 @@ func TestPutAlert(t *testing.T) { }, } }, + }, { + title: "Allow spaces in label and annotation name", + postableAlerts: apimodels.PostableAlerts{ + PostableAlerts: []models.PostableAlert{ + { + Annotations: models.LabelSet{"Dashboard URL": "http://localhost:3000"}, + Alert: models.Alert{ + Labels: models.LabelSet{"alertname": "Alert4", "Spaced Label": "works"}, + GeneratorURL: "http://localhost/url1", + }, + StartsAt: strfmt.DateTime{}, + EndsAt: strfmt.DateTime{}, + }, + }, + }, + expAlerts: func(now time.Time) []*types.Alert { + return []*types.Alert{ + { + Alert: model.Alert{ + Annotations: model.LabelSet{"Dashboard URL": "http://localhost:3000"}, + Labels: model.LabelSet{"alertname": "Alert4", "Spaced Label": "works"}, + StartsAt: now, + EndsAt: now.Add(defaultResolveTimeout), + GeneratorURL: "http://localhost/url1", + }, + UpdatedAt: now, + Timeout: true, + }, + } + }, }, { title: "Invalid labels", postableAlerts: apimodels.PostableAlerts{