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 (#4434)
* 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:
parent
1c3f205932
commit
e0208869d3
11 changed files with 227 additions and 131 deletions
Documentation
bundle.yamlexample/prometheus-operator-crd
jsonnet/prometheus-operator
pkg
alertmanager
apis/monitoring/v1alpha1
|
@ -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 alert’s 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)
|
||||
|
||||
|
|
22
bundle.yaml
22
bundle.yaml
|
@ -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 alert’s 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.
|
||||
|
|
|
@ -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 alert’s 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.
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 alert’s 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 user’s 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 application’s 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.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue