1
0
Fork 0
mirror of https://github.com/prometheus-operator/prometheus-operator.git synced 2025-04-21 03:38:43 +00:00

alertmanager: Sync validation with upstream alertmanager ()

* v1alpha1: Add duration pattern regex

* make: generate docs and assets

* alertmanager: Sync validation with upstream alertmanager

The changes here close the gap between the upstream
alertmanager validations (code, docs) and our internal
representation of those types.

Changes are made with the intention of reducing the chances
of accepting an invalid CR representation.

* alertmanager: add func to parse and validate a url from secret

* alertmanager: remove duration checks from pushoverconf

These are covered by validation in OpenAPI spec regex.

* alertmanager: re-name checking func for am conf

To avoid confusion, we rename the checking function
to clarify that it checks CR resources and not the global conf.

Improve error messaging.
This commit is contained in:
Philip Gough 2021-12-07 10:41:22 +00:00 committed by GitHub
parent 1c3f205932
commit e0208869d3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 227 additions and 131 deletions

View file

@ -1600,13 +1600,13 @@ Route defines a node in the routing tree.
| ----- | ----------- | ------ | -------- |
| receiver | Name of the receiver for this route. If not empty, it should be listed in the `receivers` field. | string | true |
| groupBy | List of labels to group by. Labels must not be repeated (unique list). Special label \"...\" (aggregate by all possible labels), if provided, must be the only element in the list. | []string | false |
| groupWait | How long to wait before sending the initial notification. Must match the regular expression `[0-9]+(ms\|s\|m\|h)` (milliseconds seconds minutes hours). | string | false |
| groupInterval | How long to wait before sending an updated notification. Must match the regular expression `[0-9]+(ms\|s\|m\|h)` (milliseconds seconds minutes hours). | string | false |
| repeatInterval | How long to wait before repeating the last notification. Must match the regular expression `[0-9]+(ms\|s\|m\|h)` (milliseconds seconds minutes hours). | string | false |
| groupWait | How long to wait before sending the initial notification. Must match the regular expression`^(([0-9]+)y)?(([0-9]+)w)?(([0-9]+)d)?(([0-9]+)h)?(([0-9]+)m)?(([0-9]+)s)?(([0-9]+)ms)?$` Example: \"30s\" | string | false |
| groupInterval | How long to wait before sending an updated notification. Must match the regular expression`^(([0-9]+)y)?(([0-9]+)w)?(([0-9]+)d)?(([0-9]+)h)?(([0-9]+)m)?(([0-9]+)s)?(([0-9]+)ms)?$` Example: \"5m\" | string | false |
| repeatInterval | How long to wait before repeating the last notification. Must match the regular expression`^(([0-9]+)y)?(([0-9]+)w)?(([0-9]+)d)?(([0-9]+)h)?(([0-9]+)m)?(([0-9]+)s)?(([0-9]+)ms)?$` Example: \"4h\" | string | false |
| matchers | List of matchers that the alerts labels should match. For the first level route, the operator removes any existing equality and regexp matcher on the `namespace` label and adds a `namespace: <object namespace>` matcher. | [][Matcher](#matcher) | false |
| continue | Boolean indicating whether an alert should continue matching subsequent sibling nodes. It will always be overridden to true for the first-level route by the Prometheus operator. | bool | false |
| routes | Child routes. | [][apiextensionsv1.JSON](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#json-v1-apiextensions-k8s-io) | false |
| muteTimeIntervals | Note: this comment applies to the field definition above but appears below otherwise it gets included in the generated manifest. CRD schema doesn't support self referential types for now (see https://github.com/kubernetes/kubernetes/issues/62872). We have to use an alternative type to circumvent the limitation. The downside is that the Kube API can't validate the data beyond the fact that it is a valid JSON representation. MuteTimeIntervals is a list of MuteTimeInterval names that will mute this route when matched, | []string | false |
| muteTimeIntervals | Note: this comment applies to the field definition above but appears below otherwise it gets included in the generated manifest. CRD schema doesn't support self-referential types for now (see https://github.com/kubernetes/kubernetes/issues/62872). We have to use an alternative type to circumvent the limitation. The downside is that the Kube API can't validate the data beyond the fact that it is a valid JSON representation. MuteTimeIntervals is a list of MuteTimeInterval names that will mute this route when matched, | []string | false |
[Back to TOC](#table-of-contents)

View file

@ -1160,6 +1160,7 @@ spec:
description: How long your notification will continue
to be retried for, unless the user acknowledges the
notification.
pattern: ^(([0-9]+)y)?(([0-9]+)w)?(([0-9]+)d)?(([0-9]+)h)?(([0-9]+)m)?(([0-9]+)s)?(([0-9]+)ms)?$
type: string
html:
description: Whether notification message is HTML or plain
@ -1412,6 +1413,7 @@ spec:
description: How often the Pushover servers will send
the same notification to the user. Must be at least
30 seconds.
pattern: ^(([0-9]+)y)?(([0-9]+)w)?(([0-9]+)d)?(([0-9]+)h)?(([0-9]+)m)?(([0-9]+)s)?(([0-9]+)ms)?$
type: string
sendResolved:
description: Whether or not to notify about resolved alerts.
@ -2761,14 +2763,14 @@ spec:
type: string
type: array
groupInterval:
description: How long to wait before sending an updated notification.
Must match the regular expression `[0-9]+(ms|s|m|h)` (milliseconds
seconds minutes hours).
description: 'How long to wait before sending an updated notification.
Must match the regular expression`^(([0-9]+)y)?(([0-9]+)w)?(([0-9]+)d)?(([0-9]+)h)?(([0-9]+)m)?(([0-9]+)s)?(([0-9]+)ms)?$`
Example: "5m"'
type: string
groupWait:
description: How long to wait before sending the initial notification.
Must match the regular expression `[0-9]+(ms|s|m|h)` (milliseconds
seconds minutes hours).
description: 'How long to wait before sending the initial notification.
Must match the regular expression`^(([0-9]+)y)?(([0-9]+)w)?(([0-9]+)d)?(([0-9]+)h)?(([0-9]+)m)?(([0-9]+)s)?(([0-9]+)ms)?$`
Example: "30s"'
type: string
matchers:
description: 'List of matchers that the alerts labels should
@ -2807,7 +2809,7 @@ spec:
muteTimeIntervals:
description: 'Note: this comment applies to the field definition
above but appears below otherwise it gets included in the generated
manifest. CRD schema doesn''t support self referential types
manifest. CRD schema doesn''t support self-referential types
for now (see https://github.com/kubernetes/kubernetes/issues/62872).
We have to use an alternative type to circumvent the limitation.
The downside is that the Kube API can''t validate the data beyond
@ -2822,9 +2824,9 @@ spec:
it should be listed in the `receivers` field.
type: string
repeatInterval:
description: How long to wait before repeating the last notification.
Must match the regular expression `[0-9]+(ms|s|m|h)` (milliseconds
seconds minutes hours).
description: 'How long to wait before repeating the last notification.
Must match the regular expression`^(([0-9]+)y)?(([0-9]+)w)?(([0-9]+)d)?(([0-9]+)h)?(([0-9]+)m)?(([0-9]+)s)?(([0-9]+)ms)?$`
Example: "4h"'
type: string
routes:
description: Child routes.

View file

@ -1160,6 +1160,7 @@ spec:
description: How long your notification will continue
to be retried for, unless the user acknowledges the
notification.
pattern: ^(([0-9]+)y)?(([0-9]+)w)?(([0-9]+)d)?(([0-9]+)h)?(([0-9]+)m)?(([0-9]+)s)?(([0-9]+)ms)?$
type: string
html:
description: Whether notification message is HTML or plain
@ -1412,6 +1413,7 @@ spec:
description: How often the Pushover servers will send
the same notification to the user. Must be at least
30 seconds.
pattern: ^(([0-9]+)y)?(([0-9]+)w)?(([0-9]+)d)?(([0-9]+)h)?(([0-9]+)m)?(([0-9]+)s)?(([0-9]+)ms)?$
type: string
sendResolved:
description: Whether or not to notify about resolved alerts.
@ -2761,14 +2763,14 @@ spec:
type: string
type: array
groupInterval:
description: How long to wait before sending an updated notification.
Must match the regular expression `[0-9]+(ms|s|m|h)` (milliseconds
seconds minutes hours).
description: 'How long to wait before sending an updated notification.
Must match the regular expression`^(([0-9]+)y)?(([0-9]+)w)?(([0-9]+)d)?(([0-9]+)h)?(([0-9]+)m)?(([0-9]+)s)?(([0-9]+)ms)?$`
Example: "5m"'
type: string
groupWait:
description: How long to wait before sending the initial notification.
Must match the regular expression `[0-9]+(ms|s|m|h)` (milliseconds
seconds minutes hours).
description: 'How long to wait before sending the initial notification.
Must match the regular expression`^(([0-9]+)y)?(([0-9]+)w)?(([0-9]+)d)?(([0-9]+)h)?(([0-9]+)m)?(([0-9]+)s)?(([0-9]+)ms)?$`
Example: "30s"'
type: string
matchers:
description: 'List of matchers that the alerts labels should
@ -2807,7 +2809,7 @@ spec:
muteTimeIntervals:
description: 'Note: this comment applies to the field definition
above but appears below otherwise it gets included in the generated
manifest. CRD schema doesn''t support self referential types
manifest. CRD schema doesn''t support self-referential types
for now (see https://github.com/kubernetes/kubernetes/issues/62872).
We have to use an alternative type to circumvent the limitation.
The downside is that the Kube API can''t validate the data beyond
@ -2822,9 +2824,9 @@ spec:
it should be listed in the `receivers` field.
type: string
repeatInterval:
description: How long to wait before repeating the last notification.
Must match the regular expression `[0-9]+(ms|s|m|h)` (milliseconds
seconds minutes hours).
description: 'How long to wait before repeating the last notification.
Must match the regular expression`^(([0-9]+)y)?(([0-9]+)w)?(([0-9]+)d)?(([0-9]+)h)?(([0-9]+)m)?(([0-9]+)s)?(([0-9]+)ms)?$`
Example: "4h"'
type: string
routes:
description: Child routes.

View file

@ -1235,6 +1235,7 @@
"properties": {
"expire": {
"description": "How long your notification will continue to be retried for, unless the user acknowledges the notification.",
"pattern": "^(([0-9]+)y)?(([0-9]+)w)?(([0-9]+)d)?(([0-9]+)h)?(([0-9]+)m)?(([0-9]+)s)?(([0-9]+)ms)?$",
"type": "string"
},
"html": {
@ -1492,6 +1493,7 @@
},
"retry": {
"description": "How often the Pushover servers will send the same notification to the user. Must be at least 30 seconds.",
"pattern": "^(([0-9]+)y)?(([0-9]+)w)?(([0-9]+)d)?(([0-9]+)h)?(([0-9]+)m)?(([0-9]+)s)?(([0-9]+)ms)?$",
"type": "string"
},
"sendResolved": {
@ -2908,11 +2910,11 @@
"type": "array"
},
"groupInterval": {
"description": "How long to wait before sending an updated notification. Must match the regular expression `[0-9]+(ms|s|m|h)` (milliseconds seconds minutes hours).",
"description": "How long to wait before sending an updated notification. Must match the regular expression`^(([0-9]+)y)?(([0-9]+)w)?(([0-9]+)d)?(([0-9]+)h)?(([0-9]+)m)?(([0-9]+)s)?(([0-9]+)ms)?$` Example: \"5m\"",
"type": "string"
},
"groupWait": {
"description": "How long to wait before sending the initial notification. Must match the regular expression `[0-9]+(ms|s|m|h)` (milliseconds seconds minutes hours).",
"description": "How long to wait before sending the initial notification. Must match the regular expression`^(([0-9]+)y)?(([0-9]+)w)?(([0-9]+)d)?(([0-9]+)h)?(([0-9]+)m)?(([0-9]+)s)?(([0-9]+)ms)?$` Example: \"30s\"",
"type": "string"
},
"matchers": {
@ -2952,7 +2954,7 @@
"type": "array"
},
"muteTimeIntervals": {
"description": "Note: this comment applies to the field definition above but appears below otherwise it gets included in the generated manifest. CRD schema doesn't support self referential types for now (see https://github.com/kubernetes/kubernetes/issues/62872). We have to use an alternative type to circumvent the limitation. The downside is that the Kube API can't validate the data beyond the fact that it is a valid JSON representation. MuteTimeIntervals is a list of MuteTimeInterval names that will mute this route when matched,",
"description": "Note: this comment applies to the field definition above but appears below otherwise it gets included in the generated manifest. CRD schema doesn't support self-referential types for now (see https://github.com/kubernetes/kubernetes/issues/62872). We have to use an alternative type to circumvent the limitation. The downside is that the Kube API can't validate the data beyond the fact that it is a valid JSON representation. MuteTimeIntervals is a list of MuteTimeInterval names that will mute this route when matched,",
"items": {
"type": "string"
},
@ -2963,7 +2965,7 @@
"type": "string"
},
"repeatInterval": {
"description": "How long to wait before repeating the last notification. Must match the regular expression `[0-9]+(ms|s|m|h)` (milliseconds seconds minutes hours).",
"description": "How long to wait before repeating the last notification. Must match the regular expression`^(([0-9]+)y)?(([0-9]+)w)?(([0-9]+)d)?(([0-9]+)h)?(([0-9]+)m)?(([0-9]+)s)?(([0-9]+)ms)?$` Example: \"4h\"",
"type": "string"
},
"routes": {

View file

@ -34,12 +34,19 @@ import (
"github.com/prometheus/alertmanager/config"
"github.com/prometheus/alertmanager/timeinterval"
"gopkg.in/yaml.v2"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
)
const inhibitRuleNamespaceKey = "namespace"
func loadCfg(s string) (*alertmanagerConfig, error) {
// alertmanagerConfigFrom returns a valid global alertmanagerConfig from s
// or returns an error if
// 1. s fails validation provided by upstream
// 2. s fails to unmarshal into internal type
// 3. the unmarshalled output is invalid
func alertmanagerConfigFrom(s string) (*alertmanagerConfig, error) {
// Run upstream Load function to get any validation checks that it runs.
_, err := config.Load(s)
if err != nil {
@ -52,6 +59,22 @@ func loadCfg(s string) (*alertmanagerConfig, error) {
return nil, err
}
rootRoute := cfg.Route
if rootRoute == nil {
return nil, errors.New("root route must exist")
}
if rootRoute.Receiver == "" {
return nil, errors.New("root route's receiver must exist")
}
if len(rootRoute.Matchers) > 0 || len(rootRoute.Match) > 0 || len(rootRoute.MatchRE) > 0 {
return nil, errors.New("'matchers' not permitted on root route")
}
if len(rootRoute.MuteTimeIntervals) > 0 {
return nil, errors.New("'mute_time_intervals' not permitted on root route")
}
return cfg, nil
}
@ -198,6 +221,19 @@ func (cg *configGenerator) enforceNamespaceForRoute(r *route, namespace string)
return r
}
func (cg *configGenerator) getValidURLFromSecret(ctx context.Context, namespace string, selector v1.SecretKeySelector) (string, error) {
url, err := cg.store.GetSecretKey(ctx, namespace, selector)
if err != nil {
return "", errors.Errorf("failed to get key %q from secret %q", selector.Key, selector.Name)
}
url = strings.TrimSpace(url)
if _, err := ValidateURL(url); err != nil {
return url, errors.Wrapf(err, "invalid url %s in secret %s config", url, selector.Name)
}
return url, nil
}
func (cg *configGenerator) convertRoute(in *monitoringv1alpha1.Route, crKey types.NamespacedName) *route {
var matchers []string
@ -376,13 +412,17 @@ func (cg *configGenerator) convertWebhookConfig(ctx context.Context, in monitori
}
if in.URLSecret != nil {
url, err := cg.store.GetSecretKey(ctx, crKey.Namespace, *in.URLSecret)
url, err := cg.getValidURLFromSecret(ctx, crKey.Namespace, *in.URLSecret)
if err != nil {
return nil, errors.Errorf("failed to get key %q from secret %q", in.URLSecret.Key, in.URLSecret.Name)
return nil, err
}
out.URL = strings.TrimSpace(url)
out.URL = url
} else if in.URL != nil {
out.URL = *in.URL
url, err := ValidateURL(*in.URL)
if err != nil {
return nil, err
}
out.URL = url.String()
}
if in.HTTPConfig != nil {
@ -423,11 +463,11 @@ func (cg *configGenerator) convertSlackConfig(ctx context.Context, in monitoring
}
if in.APIURL != nil {
url, err := cg.store.GetSecretKey(ctx, crKey.Namespace, *in.APIURL)
url, err := cg.getValidURLFromSecret(ctx, crKey.Namespace, *in.APIURL)
if err != nil {
return nil, errors.Errorf("failed to get key %q from secret %q", in.APIURL.Key, in.APIURL.Name)
return nil, err
}
out.APIURL = strings.TrimSpace(url)
out.APIURL = url
}
var actions []slackAction

View file

@ -1657,7 +1657,7 @@ templates: []
}
for _, tc := range testCase {
ac, err := loadCfg(tc.rawConf)
ac, err := alertmanagerConfigFrom(tc.rawConf)
if err != nil {
t.Error(err)
}

View file

@ -898,7 +898,7 @@ receivers:
return nil
}
baseConfig, err := loadCfg(string(rawBaseConfig))
baseConfig, err := alertmanagerConfigFrom(string(rawBaseConfig))
if err != nil {
return errors.Wrap(err, "base config from Secret could not be parsed")
}
@ -1011,7 +1011,7 @@ func (c *Operator) selectAlertmanagerConfigs(ctx context.Context, am *monitoring
res := make(map[string]*monitoringv1alpha1.AlertmanagerConfig, len(amConfigs))
for namespaceAndName, amc := range amConfigs {
if err := checkAlertmanagerConfig(ctx, amc, amVersion, store); err != nil {
if err := checkAlertmanagerConfigResource(ctx, amc, amVersion, store); err != nil {
rejected++
level.Warn(c.logger).Log(
"msg", "skipping alertmanagerconfig",
@ -1040,9 +1040,9 @@ func (c *Operator) selectAlertmanagerConfigs(ctx context.Context, am *monitoring
return res, nil
}
// checkAlertmanagerConfig verifies that an AlertmanagerConfig object is valid
// checkAlertmanagerConfigResource verifies that an AlertmanagerConfig object is valid
// and has no missing references to other objects.
func checkAlertmanagerConfig(ctx context.Context, amc *monitoringv1alpha1.AlertmanagerConfig, amVersion semver.Version, store *assets.Store) error {
func checkAlertmanagerConfigResource(ctx context.Context, amc *monitoringv1alpha1.AlertmanagerConfig, amVersion semver.Version, store *assets.Store) error {
receiverNames, err := checkReceivers(ctx, amc, store)
if err != nil {
return err
@ -1209,9 +1209,13 @@ func checkWebhookConfigs(ctx context.Context, configs []monitoringv1alpha1.Webho
webhookConfigKey := fmt.Sprintf("%s/webhook/%d", key, i)
if config.URLSecret != nil {
if _, err := store.GetSecretKey(ctx, namespace, *config.URLSecret); err != nil {
url, err := store.GetSecretKey(ctx, namespace, *config.URLSecret)
if err != nil {
return err
}
if _, err := ValidateURL(strings.TrimSpace(url)); err != nil {
return errors.Wrapf(err, "webhook 'url' %s invalid", url)
}
}
if err := configureHTTPConfigInStore(ctx, config.HTTPConfig, namespace, webhookConfigKey, store); err != nil {

View file

@ -51,7 +51,7 @@ func TestCheckAlertmanagerConfig(t *testing.T) {
Namespace: "ns1",
},
Data: map[string][]byte{
"key1": []byte("val1"),
"key1": []byte("https://val1.com"),
},
},
)
@ -807,7 +807,7 @@ func TestCheckAlertmanagerConfig(t *testing.T) {
} {
t.Run(tc.amConfig.Name, func(t *testing.T) {
store := assets.NewStore(c.CoreV1(), c.CoreV1())
err := checkAlertmanagerConfig(context.Background(), tc.amConfig, version, store)
err := checkAlertmanagerConfigResource(context.Background(), tc.amConfig, version, store)
if tc.ok {
if err != nil {
t.Fatalf("expecting no error but got %q", err)

View file

@ -15,16 +15,19 @@
package alertmanager
import (
"encoding/json"
"fmt"
"net"
"net/url"
"regexp"
"strings"
"time"
"github.com/pkg/errors"
monitoringv1alpha1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1alpha1"
"github.com/prometheus/alertmanager/config"
)
var durationRe = regexp.MustCompile(`^(([0-9]+)y)?(([0-9]+)w)?(([0-9]+)d)?(([0-9]+)h)?(([0-9]+)m)?(([0-9]+)s)?(([0-9]+)ms)?$`)
func ValidateConfig(amc *monitoringv1alpha1.AlertmanagerConfig) error {
receivers, err := validateReceivers(amc.Spec.Receivers)
if err != nil {
@ -39,6 +42,19 @@ func ValidateConfig(amc *monitoringv1alpha1.AlertmanagerConfig) error {
return validateAlertManagerRoutes(amc.Spec.Route, receivers, muteTimeIntervals, true)
}
// ValidateURL against the config.URL
// This could potentially become a regex and be validated via OpenAPI
// but right now, since we know we need to unmarshal into an upstream type
// after conversion, we validate we don't error when doing so
func ValidateURL(url string) (*config.URL, error) {
var u config.URL
err := json.Unmarshal([]byte(fmt.Sprintf(`"%s"`, url)), &u)
if err != nil {
return nil, fmt.Errorf("validate url from string failed for %s: %w", url, err)
}
return &u, nil
}
func validateReceivers(receivers []monitoringv1alpha1.Receiver) (map[string]struct{}, error) {
var err error
receiverNames := make(map[string]struct{})
@ -62,7 +78,7 @@ func validateReceivers(receivers []monitoringv1alpha1.Receiver) (map[string]stru
}
if err := validateWebhookConfigs(receiver.WebhookConfigs); err != nil {
return nil, errors.Wrapf(err, "failed to validate 'slackConfig' - receiver %s", receiver.Name)
return nil, errors.Wrapf(err, "failed to validate 'webhookConfig' - receiver %s", receiver.Name)
}
if err := validateWechatConfigs(receiver.WeChatConfigs); err != nil {
@ -86,8 +102,17 @@ func validateReceivers(receivers []monitoringv1alpha1.Receiver) (map[string]stru
return receiverNames, nil
}
// validatePagerDutyConfigs is a no-op
func validatePagerDutyConfigs(configs []monitoringv1alpha1.PagerDutyConfig) error {
for _, conf := range configs {
if conf.URL != "" {
if _, err := ValidateURL(conf.URL); err != nil {
return errors.Wrap(err, "pagerduty validation failed for 'url'")
}
}
if conf.RoutingKey == nil && conf.ServiceKey == nil {
return errors.New("one of 'routingKey' or 'serviceKey' is required")
}
}
return nil
}
@ -96,6 +121,11 @@ func validateOpsGenieConfigs(configs []monitoringv1alpha1.OpsGenieConfig) error
if err := config.Validate(); err != nil {
return err
}
if config.APIURL != "" {
if _, err := ValidateURL(config.APIURL); err != nil {
return errors.Wrap(err, "invalid 'apiURL'")
}
}
}
return nil
}
@ -114,6 +144,11 @@ func validateWebhookConfigs(configs []monitoringv1alpha1.WebhookConfig) error {
if config.URL == nil && config.URLSecret == nil {
return errors.New("one of 'url' or 'urlSecret' must be specified")
}
if config.URL != nil {
if _, err := ValidateURL(*config.URL); err != nil {
return errors.Wrapf(err, "invalid 'url'")
}
}
}
return nil
}
@ -121,8 +156,8 @@ func validateWebhookConfigs(configs []monitoringv1alpha1.WebhookConfig) error {
func validateWechatConfigs(configs []monitoringv1alpha1.WeChatConfig) error {
for _, config := range configs {
if config.APIURL != "" {
if _, err := url.Parse(config.APIURL); err != nil {
return errors.Wrap(err, "weChat 'apiURL' not valid")
if _, err := ValidateURL(config.APIURL); err != nil {
return errors.Wrap(err, "invalid 'apiURL'")
}
}
}
@ -132,13 +167,13 @@ func validateWechatConfigs(configs []monitoringv1alpha1.WeChatConfig) error {
func validateEmailConfig(configs []monitoringv1alpha1.EmailConfig) error {
for _, config := range configs {
if config.To == "" {
return errors.New("missing to address in email config")
return errors.New("missing 'to' address")
}
if config.Smarthost != "" {
_, _, err := net.SplitHostPort(config.Smarthost)
if err != nil {
return errors.New("invalid email field 'smarthost'")
return errors.Wrapf(err, "invalid field 'smarthost': %s", config.Smarthost)
}
}
@ -148,7 +183,7 @@ func validateEmailConfig(configs []monitoringv1alpha1.EmailConfig) error {
for _, v := range config.Headers {
normalized := strings.Title(v.Key)
if _, ok := normalizedHeaders[normalized]; ok {
return fmt.Errorf("duplicate header %q in email config", normalized)
return fmt.Errorf("duplicate header %q", normalized)
}
normalizedHeaders[normalized] = struct{}{}
}
@ -180,7 +215,13 @@ func validateVictorOpsConfigs(configs []monitoringv1alpha1.VictorOpsConfig) erro
}
if config.RoutingKey == "" {
return errors.New("missing 'routingKey' key in VictorOps config")
return errors.New("missing 'routingKey' key")
}
if config.APIURL != "" {
if _, err := ValidateURL(config.APIURL); err != nil {
return errors.Wrapf(err, "'apiURL' %s invalid", config.APIURL)
}
}
}
return nil
@ -195,25 +236,14 @@ func validatePushoverConfigs(configs []monitoringv1alpha1.PushoverConfig) error
if config.Token == nil {
return errors.Errorf("mandatory field %q is empty", "token")
}
if config.Retry != "" {
_, err := time.ParseDuration(config.Retry)
if err != nil {
return errors.New("invalid retry duration")
}
}
if config.Expire != "" {
_, err := time.ParseDuration(config.Expire)
if err != nil {
return errors.New("invalid expire duration")
}
}
}
return nil
}
// validateAlertManagerRoutes verifies that the given route and all its children are semantically valid.
// because of the self-referential issues mentioned in https://github.com/kubernetes/kubernetes/issues/62872
// it is not currently possible to apply OpenAPI validation to a v1alpha1.Route
func validateAlertManagerRoutes(r *monitoringv1alpha1.Route, receivers, muteTimeIntervals map[string]struct{}, topLevelRoute bool) error {
if r == nil {
return nil
@ -242,6 +272,19 @@ func validateAlertManagerRoutes(r *monitoringv1alpha1.Route, receivers, muteTime
}
}
// validate that if defaults are set, they match regex
if r.GroupInterval != "" && !durationRe.MatchString(r.GroupInterval) {
return errors.Errorf("groupInterval %s does not match required regex: %s", r.GroupInterval, durationRe.String())
}
if r.GroupWait != "" && !durationRe.MatchString(r.GroupWait) {
return errors.Errorf("groupWait %s does not match required regex: %s", r.GroupInterval, durationRe.String())
}
if r.RepeatInterval != "" && !durationRe.MatchString(r.RepeatInterval) {
return errors.Errorf("repeatInterval %s does not match required regex: %s", r.GroupInterval, durationRe.String())
}
children, err := r.ChildRoutes()
if err != nil {
return err

View file

@ -15,11 +15,14 @@
package alertmanager
import (
"net/url"
"reflect"
"testing"
v1 "k8s.io/api/core/v1"
monitoringv1alpha1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1alpha1"
"github.com/prometheus/alertmanager/config"
)
func TestValidateConfig(t *testing.T) {
@ -268,53 +271,6 @@ func TestValidateConfig(t *testing.T) {
},
expectErr: true,
},
{
name: "Test fail to validate PushoverConfigs - invalid retry duration",
in: &monitoringv1alpha1.AlertmanagerConfig{
Spec: monitoringv1alpha1.AlertmanagerConfigSpec{
Receivers: []monitoringv1alpha1.Receiver{
{
Name: "same",
},
{
Name: "different",
PushoverConfigs: []monitoringv1alpha1.PushoverConfig{
{
UserKey: &v1.SecretKeySelector{},
Token: &v1.SecretKeySelector{},
Retry: "n/a",
},
},
},
},
},
},
expectErr: true,
},
{
name: "Test fail to validate PushoverConfigs - invalid expiry duration",
in: &monitoringv1alpha1.AlertmanagerConfig{
Spec: monitoringv1alpha1.AlertmanagerConfigSpec{
Receivers: []monitoringv1alpha1.Receiver{
{
Name: "same",
},
{
Name: "different",
PushoverConfigs: []monitoringv1alpha1.PushoverConfig{
{
UserKey: &v1.SecretKeySelector{},
Token: &v1.SecretKeySelector{},
Retry: "10m",
Expire: "n/a",
},
},
},
},
},
},
expectErr: true,
},
{
name: "Test fail to validate routes - parent route has no receiver",
in: &monitoringv1alpha1.AlertmanagerConfig{
@ -409,7 +365,7 @@ func TestValidateConfig(t *testing.T) {
{
Type: "a",
Text: "b",
URL: "www.test.com",
URL: "https://www.test.com",
Name: "c",
ConfirmField: &monitoringv1alpha1.SlackConfirmationField{
Text: "d",
@ -426,7 +382,7 @@ func TestValidateConfig(t *testing.T) {
},
WebhookConfigs: []monitoringv1alpha1.WebhookConfig{
{
URL: strToPtr("www.test.com"),
URL: strToPtr("https://www.test.com"),
URLSecret: &v1.SecretKeySelector{},
},
},
@ -509,6 +465,54 @@ func TestValidateConfig(t *testing.T) {
}
}
func TestValidateUrl(t *testing.T) {
tests := []struct {
name string
in string
expectErr bool
expectResult func() *config.URL
}{
{
name: "Test invalid url returns error",
in: "https://!^invalid.com",
expectErr: true,
},
{
name: "Test missing scheme returns error",
in: "is.normally.valid",
expectErr: true,
},
{
name: "Test happy path",
in: "https://u:p@is.compliant.with.upstream.unmarshal",
expectResult: func() *config.URL {
u, _ := url.Parse("https://u:p@is.compliant.with.upstream.unmarshal")
return &config.URL{URL: u}
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
u, err := ValidateURL(tc.in)
if tc.expectErr {
if err == nil {
t.Fatal("expected error but got none")
}
return
}
if err != nil {
t.Fatal(err)
}
res := tc.expectResult()
if !reflect.DeepEqual(u, res) {
t.Fatalf("wanted %v but got %v", res, u)
}
})
}
}
func strToPtr(s string) *string {
return &s
}

View file

@ -18,7 +18,6 @@ import (
"encoding/json"
"errors"
"fmt"
"regexp"
"strings"
monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1"
@ -37,10 +36,6 @@ const (
AlertmanagerConfigKindKey = "alertmanagerconfig"
)
var (
opsGenieTypeRe = regexp.MustCompile("^(team|user|escalation|schedule)$")
)
// AlertmanagerConfig defines a namespaced AlertmanagerConfig to be aggregated
// across multiple namespaces configuring one Alertmanager cluster.
// +genclient
@ -96,19 +91,19 @@ type Route struct {
// Special label "..." (aggregate by all possible labels), if provided, must be the only element in the list.
// +optional
GroupBy []string `json:"groupBy,omitempty"`
// How long to wait before sending the initial notification. Must match the
// regular expression `[0-9]+(ms|s|m|h)` (milliseconds seconds minutes
// hours).
// How long to wait before sending the initial notification.
// Must match the regular expression`^(([0-9]+)y)?(([0-9]+)w)?(([0-9]+)d)?(([0-9]+)h)?(([0-9]+)m)?(([0-9]+)s)?(([0-9]+)ms)?$`
// Example: "30s"
// +optional
GroupWait string `json:"groupWait,omitempty"`
// How long to wait before sending an updated notification. Must match the
// regular expression `[0-9]+(ms|s|m|h)` (milliseconds seconds minutes
// hours).
// How long to wait before sending an updated notification.
// Must match the regular expression`^(([0-9]+)y)?(([0-9]+)w)?(([0-9]+)d)?(([0-9]+)h)?(([0-9]+)m)?(([0-9]+)s)?(([0-9]+)ms)?$`
// Example: "5m"
// +optional
GroupInterval string `json:"groupInterval,omitempty"`
// How long to wait before repeating the last notification. Must match the
// regular expression `[0-9]+(ms|s|m|h)` (milliseconds seconds minutes
// hours).
// How long to wait before repeating the last notification.
// Must match the regular expression`^(([0-9]+)y)?(([0-9]+)w)?(([0-9]+)d)?(([0-9]+)h)?(([0-9]+)m)?(([0-9]+)s)?(([0-9]+)ms)?$`
// Example: "4h"
// +optional
RepeatInterval string `json:"repeatInterval,omitempty"`
// List of matchers that the alerts labels should match. For the first
@ -126,7 +121,7 @@ type Route struct {
Routes []apiextensionsv1.JSON `json:"routes,omitempty"`
// Note: this comment applies to the field definition above but appears
// below otherwise it gets included in the generated manifest.
// CRD schema doesn't support self referential types for now (see
// CRD schema doesn't support self-referential types for now (see
// https://github.com/kubernetes/kubernetes/issues/62872). We have to use
// an alternative type to circumvent the limitation. The downside is that
// the Kube API can't validate the data beyond the fact that it is a valid
@ -679,10 +674,12 @@ type PushoverConfig struct {
// The secret's key that contains the recipient users user key.
// The secret needs to be in the same namespace as the AlertmanagerConfig
// object and accessible by the Prometheus Operator.
// +kubebuilder:validation:Required
UserKey *v1.SecretKeySelector `json:"userKey,omitempty"`
// The secret's key that contains the registered applications API token, see https://pushover.net/apps.
// The secret needs to be in the same namespace as the AlertmanagerConfig
// object and accessible by the Prometheus Operator.
// +kubebuilder:validation:Required
Token *v1.SecretKeySelector `json:"token,omitempty"`
// Notification title.
// +optional
@ -704,10 +701,12 @@ type PushoverConfig struct {
Priority string `json:"priority,omitempty"`
// How often the Pushover servers will send the same notification to the user.
// Must be at least 30 seconds.
// +kubebuilder:validation:Pattern=`^(([0-9]+)y)?(([0-9]+)w)?(([0-9]+)d)?(([0-9]+)h)?(([0-9]+)m)?(([0-9]+)s)?(([0-9]+)ms)?$`
// +optional
Retry string `json:"retry,omitempty"`
// How long your notification will continue to be retried for, unless the user
// acknowledges the notification.
// +kubebuilder:validation:Pattern=`^(([0-9]+)y)?(([0-9]+)w)?(([0-9]+)d)?(([0-9]+)h)?(([0-9]+)m)?(([0-9]+)s)?(([0-9]+)ms)?$`
// +optional
Expire string `json:"expire,omitempty"`
// Whether notification message is HTML or plain text.