diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index b36f71c3c1..5ceb590991 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -24,8 +24,8 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 - - name: install Go + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + - name: Install Go uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 with: go-version: 1.22.x @@ -33,6 +33,7 @@ jobs: run: sudo apt-get update && sudo apt-get -y install libsnmp-dev if: github.repository == 'prometheus/snmp_exporter' - name: Lint - uses: golangci/golangci-lint-action@9d1e0624a798bb64f6c3cea93db47765312263dc # v5.1.0 + uses: golangci/golangci-lint-action@a4f60bb28d35aeee14e6880718e0c85ff1882e64 # v6.0.1 with: - version: v1.56.2 + args: --verbose + version: v1.59.0 diff --git a/Makefile.common b/Makefile.common index 0e9ace29b4..1617292350 100644 --- a/Makefile.common +++ b/Makefile.common @@ -61,7 +61,7 @@ PROMU_URL := https://github.com/prometheus/promu/releases/download/v$(PROMU_ SKIP_GOLANGCI_LINT := GOLANGCI_LINT := GOLANGCI_LINT_OPTS ?= -GOLANGCI_LINT_VERSION ?= v1.56.2 +GOLANGCI_LINT_VERSION ?= v1.59.0 # golangci-lint only supports linux, darwin and windows platforms on i386/amd64/arm64. # windows isn't included here because of the path separator being different. ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux darwin)) diff --git a/api/v2/api.go b/api/v2/api.go index 6f034d2596..aa6c8a72ab 100644 --- a/api/v2/api.go +++ b/api/v2/api.go @@ -45,7 +45,7 @@ import ( "github.com/prometheus/alertmanager/cluster" "github.com/prometheus/alertmanager/config" "github.com/prometheus/alertmanager/dispatch" - "github.com/prometheus/alertmanager/matchers/compat" + "github.com/prometheus/alertmanager/matcher/compat" "github.com/prometheus/alertmanager/pkg/labels" "github.com/prometheus/alertmanager/provider" "github.com/prometheus/alertmanager/silence" @@ -660,8 +660,7 @@ func (api *API) postSilencesHandler(params silence_ops.PostSilencesParams) middl return silence_ops.NewPostSilencesBadRequest().WithPayload(msg) } - sid, err := api.silences.Set(sil) - if err != nil { + if err = api.silences.Set(sil); err != nil { level.Error(logger).Log("msg", "Failed to create silence", "err", err) if errors.Is(err, silence.ErrNotFound) { return silence_ops.NewPostSilencesNotFound().WithPayload(err.Error()) @@ -670,7 +669,7 @@ func (api *API) postSilencesHandler(params silence_ops.PostSilencesParams) middl } return silence_ops.NewPostSilencesOK().WithPayload(&silence_ops.PostSilencesOKBody{ - SilenceID: sid, + SilenceID: sil.Id, }) } diff --git a/api/v2/api_test.go b/api/v2/api_test.go index d520dbde13..438e6f44d4 100644 --- a/api/v2/api_test.go +++ b/api/v2/api_test.go @@ -15,6 +15,7 @@ package v2 import ( "bytes" + "encoding/json" "fmt" "io" "net/http" @@ -24,6 +25,7 @@ import ( "time" "github.com/go-openapi/runtime" + "github.com/go-openapi/runtime/middleware" "github.com/go-openapi/strfmt" "github.com/prometheus/common/model" "github.com/stretchr/testify/require" @@ -159,8 +161,7 @@ func TestDeleteSilenceHandler(t *testing.T) { EndsAt: now.Add(time.Hour), UpdatedAt: now, } - unexpiredSid, err := silences.Set(unexpiredSil) - require.NoError(t, err) + require.NoError(t, silences.Set(unexpiredSil)) expiredSil := &silencepb.Silence{ Matchers: []*silencepb.Matcher{m}, @@ -168,9 +169,8 @@ func TestDeleteSilenceHandler(t *testing.T) { EndsAt: now.Add(time.Hour), UpdatedAt: now, } - expiredSid, err := silences.Set(expiredSil) - require.NoError(t, err) - require.NoError(t, silences.Expire(expiredSid)) + require.NoError(t, silences.Set(expiredSil)) + require.NoError(t, silences.Expire(expiredSil.Id)) for i, tc := range []struct { sid string @@ -181,11 +181,11 @@ func TestDeleteSilenceHandler(t *testing.T) { 404, }, { - unexpiredSid, + unexpiredSil.Id, 200, }, { - expiredSid, + expiredSil.Id, 200, }, } { @@ -223,8 +223,7 @@ func TestPostSilencesHandler(t *testing.T) { EndsAt: now.Add(time.Hour), UpdatedAt: now, } - unexpiredSid, err := silences.Set(unexpiredSil) - require.NoError(t, err) + require.NoError(t, silences.Set(unexpiredSil)) expiredSil := &silencepb.Silence{ Matchers: []*silencepb.Matcher{m}, @@ -232,9 +231,8 @@ func TestPostSilencesHandler(t *testing.T) { EndsAt: now.Add(time.Hour), UpdatedAt: now, } - expiredSid, err := silences.Set(expiredSil) - require.NoError(t, err) - require.NoError(t, silences.Expire(expiredSid)) + require.NoError(t, silences.Set(expiredSil)) + require.NoError(t, silences.Expire(expiredSil.Id)) t.Run("Silences CRUD", func(t *testing.T) { for i, tc := range []struct { @@ -259,46 +257,122 @@ func TestPostSilencesHandler(t *testing.T) { }, { "with an active silence ID - it extends the silence", - unexpiredSid, + unexpiredSil.Id, now.Add(time.Hour), now.Add(time.Hour * 2), 200, }, { "with an expired silence ID - it re-creates the silence", - expiredSid, + expiredSil.Id, now.Add(time.Hour), now.Add(time.Hour * 2), 200, }, } { t.Run(tc.name, func(t *testing.T) { - silence, silenceBytes := createSilence(t, tc.sid, "silenceCreator", tc.start, tc.end) - api := API{ uptime: time.Now(), silences: silences, logger: log.NewNopLogger(), } - r, err := http.NewRequest("POST", "/api/v2/silence/${tc.sid}", bytes.NewReader(silenceBytes)) - require.NoError(t, err) - + sil := createSilence(t, tc.sid, "silenceCreator", tc.start, tc.end) w := httptest.NewRecorder() - p := runtime.TextProducer() - responder := api.postSilencesHandler(silence_ops.PostSilencesParams{ - HTTPRequest: r, - Silence: &silence, - }) - responder.WriteResponse(w, p) + postSilences(t, w, api.postSilencesHandler, sil) body, _ := io.ReadAll(w.Result().Body) - require.Equal(t, tc.expectedCode, w.Code, fmt.Sprintf("test case: %d, response: %s", i, string(body))) }) } }) } +func TestPostSilencesHandlerMissingIdCreatesSilence(t *testing.T) { + now := time.Now() + silences := newSilences(t) + api := API{ + uptime: time.Now(), + silences: silences, + logger: log.NewNopLogger(), + } + + // Create a new silence. It should be assigned a random UUID. + sil := createSilence(t, "", "silenceCreator", now.Add(time.Hour), now.Add(time.Hour*2)) + w := httptest.NewRecorder() + postSilences(t, w, api.postSilencesHandler, sil) + require.Equal(t, http.StatusOK, w.Code) + + // Get the silences from the API. + w = httptest.NewRecorder() + getSilences(t, w, api.getSilencesHandler) + require.Equal(t, http.StatusOK, w.Code) + var resp []open_api_models.GettableSilence + require.NoError(t, json.NewDecoder(w.Body).Decode(&resp)) + require.Len(t, resp, 1) + + // Change the ID. It should return 404 Not Found. + sil = open_api_models.PostableSilence{ + ID: "unknownID", + Silence: resp[0].Silence, + } + w = httptest.NewRecorder() + postSilences(t, w, api.postSilencesHandler, sil) + require.Equal(t, http.StatusNotFound, w.Code) + + // Remove the ID. It should duplicate the silence with a different UUID. + sil = open_api_models.PostableSilence{ + ID: "", + Silence: resp[0].Silence, + } + w = httptest.NewRecorder() + postSilences(t, w, api.postSilencesHandler, sil) + require.Equal(t, http.StatusOK, w.Code) + + // Get the silences from the API. There should now be 2 silences. + w = httptest.NewRecorder() + getSilences(t, w, api.getSilencesHandler) + require.Equal(t, http.StatusOK, w.Code) + require.NoError(t, json.NewDecoder(w.Body).Decode(&resp)) + require.Len(t, resp, 2) + require.NotEqual(t, resp[0].ID, resp[1].ID) +} + +func getSilences( + t *testing.T, + w *httptest.ResponseRecorder, + handlerFunc func(params silence_ops.GetSilencesParams) middleware.Responder, +) { + r, err := http.NewRequest("GET", "/api/v2/silences", nil) + require.NoError(t, err) + + p := runtime.TextProducer() + responder := handlerFunc(silence_ops.GetSilencesParams{ + HTTPRequest: r, + Filter: nil, + }) + responder.WriteResponse(w, p) +} + +func postSilences( + t *testing.T, + w *httptest.ResponseRecorder, + handlerFunc func(params silence_ops.PostSilencesParams) middleware.Responder, + sil open_api_models.PostableSilence, +) { + b, err := json.Marshal(sil) + require.NoError(t, err) + + r, err := http.NewRequest("POST", "/api/v2/silences", bytes.NewReader(b)) + require.NoError(t, err) + + p := runtime.TextProducer() + responder := handlerFunc(silence_ops.PostSilencesParams{ + HTTPRequest: r, + Silence: &sil, + }) + responder.WriteResponse(w, p) +} + func TestCheckSilenceMatchesFilterLabels(t *testing.T) { type test struct { silenceMatchers []*silencepb.Matcher diff --git a/api/v2/testing.go b/api/v2/testing.go index c813d350d3..7665a19f67 100644 --- a/api/v2/testing.go +++ b/api/v2/testing.go @@ -14,19 +14,17 @@ package v2 import ( - "encoding/json" "testing" "time" "github.com/go-openapi/strfmt" - "github.com/stretchr/testify/require" open_api_models "github.com/prometheus/alertmanager/api/v2/models" "github.com/prometheus/alertmanager/pkg/labels" "github.com/prometheus/alertmanager/silence/silencepb" ) -func createSilence(t *testing.T, ID, creator string, start, ends time.Time) (open_api_models.PostableSilence, []byte) { +func createSilence(t *testing.T, ID, creator string, start, ends time.Time) open_api_models.PostableSilence { t.Helper() comment := "test" @@ -46,10 +44,7 @@ func createSilence(t *testing.T, ID, creator string, start, ends time.Time) (ope Comment: &comment, }, } - b, err := json.Marshal(&sil) - require.NoError(t, err) - - return sil, b + return sil } func createSilenceMatcher(t *testing.T, name, pattern string, matcherType silencepb.Matcher_Type) *silencepb.Matcher { diff --git a/cli/alert_add.go b/cli/alert_add.go index 4e7eae7ed0..2890946c24 100644 --- a/cli/alert_add.go +++ b/cli/alert_add.go @@ -25,7 +25,7 @@ import ( "github.com/prometheus/alertmanager/api/v2/client/alert" "github.com/prometheus/alertmanager/api/v2/models" - "github.com/prometheus/alertmanager/matchers/compat" + "github.com/prometheus/alertmanager/matcher/compat" "github.com/prometheus/alertmanager/pkg/labels" ) diff --git a/cli/alert_query.go b/cli/alert_query.go index e4bddaa651..de42ed0ff7 100644 --- a/cli/alert_query.go +++ b/cli/alert_query.go @@ -23,7 +23,7 @@ import ( "github.com/prometheus/alertmanager/api/v2/client/alert" "github.com/prometheus/alertmanager/cli/format" - "github.com/prometheus/alertmanager/matchers/compat" + "github.com/prometheus/alertmanager/matcher/compat" ) type alertQueryCmd struct { diff --git a/cli/root.go b/cli/root.go index fe02fb82f3..1e63cfe88e 100644 --- a/cli/root.go +++ b/cli/root.go @@ -34,7 +34,7 @@ import ( "github.com/prometheus/alertmanager/cli/config" "github.com/prometheus/alertmanager/cli/format" "github.com/prometheus/alertmanager/featurecontrol" - "github.com/prometheus/alertmanager/matchers/compat" + "github.com/prometheus/alertmanager/matcher/compat" ) var ( diff --git a/cli/silence_add.go b/cli/silence_add.go index 4456ddec9a..e23d1fcc86 100644 --- a/cli/silence_add.go +++ b/cli/silence_add.go @@ -27,7 +27,7 @@ import ( "github.com/prometheus/alertmanager/api/v2/client/silence" "github.com/prometheus/alertmanager/api/v2/models" - "github.com/prometheus/alertmanager/matchers/compat" + "github.com/prometheus/alertmanager/matcher/compat" "github.com/prometheus/alertmanager/pkg/labels" ) diff --git a/cli/silence_query.go b/cli/silence_query.go index 5eb33a27d7..ba5f01e15d 100644 --- a/cli/silence_query.go +++ b/cli/silence_query.go @@ -25,7 +25,7 @@ import ( "github.com/prometheus/alertmanager/api/v2/client/silence" "github.com/prometheus/alertmanager/api/v2/models" "github.com/prometheus/alertmanager/cli/format" - "github.com/prometheus/alertmanager/matchers/compat" + "github.com/prometheus/alertmanager/matcher/compat" ) type silenceQueryCmd struct { diff --git a/cli/test_routing.go b/cli/test_routing.go index 589a2e8cf8..163dce391b 100644 --- a/cli/test_routing.go +++ b/cli/test_routing.go @@ -24,7 +24,7 @@ import ( "github.com/prometheus/alertmanager/api/v2/models" "github.com/prometheus/alertmanager/dispatch" - "github.com/prometheus/alertmanager/matchers/compat" + "github.com/prometheus/alertmanager/matcher/compat" "github.com/prometheus/alertmanager/pkg/labels" ) diff --git a/cluster/tls_transport_test.go b/cluster/tls_transport_test.go index ac8d1c95e2..5b9e3d5149 100644 --- a/cluster/tls_transport_test.go +++ b/cluster/tls_transport_test.go @@ -217,7 +217,7 @@ func TestDialTimeout(t *testing.T) { sent := []byte(("test stream")) m, err := from.Write(sent) require.NoError(t, err) - require.Greater(t, m, 0) + require.Positive(t, m) wg.Wait() diff --git a/cmd/alertmanager/main.go b/cmd/alertmanager/main.go index 7566fba8d5..06d697a055 100644 --- a/cmd/alertmanager/main.go +++ b/cmd/alertmanager/main.go @@ -42,6 +42,7 @@ import ( "github.com/prometheus/common/version" "github.com/prometheus/exporter-toolkit/web" webflag "github.com/prometheus/exporter-toolkit/web/kingpinflag" + "go.uber.org/automaxprocs/maxprocs" "github.com/prometheus/alertmanager/api" "github.com/prometheus/alertmanager/cluster" @@ -50,7 +51,7 @@ import ( "github.com/prometheus/alertmanager/dispatch" "github.com/prometheus/alertmanager/featurecontrol" "github.com/prometheus/alertmanager/inhibit" - "github.com/prometheus/alertmanager/matchers/compat" + "github.com/prometheus/alertmanager/matcher/compat" "github.com/prometheus/alertmanager/nflog" "github.com/prometheus/alertmanager/notify" "github.com/prometheus/alertmanager/provider/mem" @@ -147,7 +148,7 @@ func run() int { retention = kingpin.Flag("data.retention", "How long to keep data for.").Default("120h").Duration() maintenanceInterval = kingpin.Flag("data.maintenance-interval", "Interval between garbage collection and snapshotting to disk of the silences and the notification logs.").Default("15m").Duration() maxSilences = kingpin.Flag("silences.max-silences", "Maximum number of silences, including expired silences. If negative or zero, no limit is set.").Default("0").Int() - maxPerSilenceBytes = kingpin.Flag("silences.max-per-silence-bytes", "Maximum per silence size in bytes. If negative or zero, no limit is set.").Default("0").Int() + maxSilenceSizeBytes = kingpin.Flag("silences.max-silence-size-bytes", "Maximum silence size in bytes. If negative or zero, no limit is set.").Default("0").Int() alertGCInterval = kingpin.Flag("alerts.gc-interval", "Interval between alert GC.").Default("30m").Duration() webConfig = webflag.AddFlags(kingpin.CommandLine, ":9093") @@ -194,6 +195,15 @@ func run() int { } compat.InitFromFlags(logger, ff) + if ff.EnableAutoGOMAXPROCS() { + l := func(format string, a ...interface{}) { + level.Info(logger).Log("component", "automaxprocs", "msg", fmt.Sprintf(strings.TrimPrefix(format, "maxprocs: "), a...)) + } + if _, err := maxprocs.Set(maxprocs.Logger(l)); err != nil { + level.Warn(logger).Log("msg", "Failed to set GOMAXPROCS automatically", "err", err) + } + } + err = os.MkdirAll(*dataDir, 0o777) if err != nil { level.Error(logger).Log("msg", "Unable to create data directory", "err", err) @@ -262,8 +272,8 @@ func run() int { SnapshotFile: filepath.Join(*dataDir, "silences"), Retention: *retention, Limits: silence.Limits{ - MaxSilences: *maxSilences, - MaxPerSilenceBytes: *maxPerSilenceBytes, + MaxSilences: func() int { return *maxSilences }, + MaxSilenceSizeBytes: func() int { return *maxSilenceSizeBytes }, }, Logger: log.With(logger, "component", "silences"), Metrics: prometheus.DefaultRegisterer, diff --git a/config/config.go b/config/config.go index 493d4af4d4..4d694eefcc 100644 --- a/config/config.go +++ b/config/config.go @@ -30,7 +30,7 @@ import ( "github.com/prometheus/common/model" "gopkg.in/yaml.v2" - "github.com/prometheus/alertmanager/matchers/compat" + "github.com/prometheus/alertmanager/matcher/compat" "github.com/prometheus/alertmanager/pkg/labels" "github.com/prometheus/alertmanager/timeinterval" ) @@ -165,7 +165,12 @@ func (s *SecretURL) UnmarshalJSON(data []byte) error { s.URL = &url.URL{} return nil } - return json.Unmarshal(data, (*URL)(s)) + // Redact the secret URL in case of errors + if err := json.Unmarshal(data, (*URL)(s)); err != nil { + return errors.New(strings.ReplaceAll(err.Error(), string(data), "[REDACTED]")) + } + + return nil } // Load parses the YAML input s into a Config. @@ -368,6 +373,9 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { } } for _, ec := range rcv.EmailConfigs { + if ec.TLSConfig == nil { + ec.TLSConfig = c.Global.SMTPTLSConfig + } if ec.Smarthost.String() == "" { if c.Global.SMTPSmarthost.String() == "" { return fmt.Errorf("no global SMTP smarthost set") @@ -643,12 +651,14 @@ func checkTimeInterval(r *Route, timeIntervals map[string]struct{}) error { // DefaultGlobalConfig returns GlobalConfig with default values. func DefaultGlobalConfig() GlobalConfig { defaultHTTPConfig := commoncfg.DefaultHTTPClientConfig - return GlobalConfig{ - ResolveTimeout: model.Duration(5 * time.Minute), - HTTPConfig: &defaultHTTPConfig, + defaultSMTPTLSConfig := commoncfg.TLSConfig{} + return GlobalConfig{ + ResolveTimeout: model.Duration(5 * time.Minute), + HTTPConfig: &defaultHTTPConfig, SMTPHello: "localhost", SMTPRequireTLS: true, + SMTPTLSConfig: &defaultSMTPTLSConfig, PagerdutyURL: mustParseURL("https://events.pagerduty.com/v2/enqueue"), OpsGenieAPIURL: mustParseURL("https://api.opsgenie.com/"), WeChatAPIURL: mustParseURL("https://qyapi.weixin.qq.com/cgi-bin/"), @@ -756,30 +766,31 @@ type GlobalConfig struct { HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"` - JiraAPIURL *URL `yaml:"jira_api_url,omitempty" json:"jira_api_url,omitempty"` - SMTPFrom string `yaml:"smtp_from,omitempty" json:"smtp_from,omitempty"` - SMTPHello string `yaml:"smtp_hello,omitempty" json:"smtp_hello,omitempty"` - SMTPSmarthost HostPort `yaml:"smtp_smarthost,omitempty" json:"smtp_smarthost,omitempty"` - SMTPAuthUsername string `yaml:"smtp_auth_username,omitempty" json:"smtp_auth_username,omitempty"` - SMTPAuthPassword Secret `yaml:"smtp_auth_password,omitempty" json:"smtp_auth_password,omitempty"` - SMTPAuthPasswordFile string `yaml:"smtp_auth_password_file,omitempty" json:"smtp_auth_password_file,omitempty"` - SMTPAuthSecret Secret `yaml:"smtp_auth_secret,omitempty" json:"smtp_auth_secret,omitempty"` - SMTPAuthIdentity string `yaml:"smtp_auth_identity,omitempty" json:"smtp_auth_identity,omitempty"` - SMTPRequireTLS bool `yaml:"smtp_require_tls" json:"smtp_require_tls,omitempty"` - SlackAPIURL *SecretURL `yaml:"slack_api_url,omitempty" json:"slack_api_url,omitempty"` - SlackAPIURLFile string `yaml:"slack_api_url_file,omitempty" json:"slack_api_url_file,omitempty"` - PagerdutyURL *URL `yaml:"pagerduty_url,omitempty" json:"pagerduty_url,omitempty"` - OpsGenieAPIURL *URL `yaml:"opsgenie_api_url,omitempty" json:"opsgenie_api_url,omitempty"` - OpsGenieAPIKey Secret `yaml:"opsgenie_api_key,omitempty" json:"opsgenie_api_key,omitempty"` - OpsGenieAPIKeyFile string `yaml:"opsgenie_api_key_file,omitempty" json:"opsgenie_api_key_file,omitempty"` - WeChatAPIURL *URL `yaml:"wechat_api_url,omitempty" json:"wechat_api_url,omitempty"` - WeChatAPISecret Secret `yaml:"wechat_api_secret,omitempty" json:"wechat_api_secret,omitempty"` - WeChatAPICorpID string `yaml:"wechat_api_corp_id,omitempty" json:"wechat_api_corp_id,omitempty"` - VictorOpsAPIURL *URL `yaml:"victorops_api_url,omitempty" json:"victorops_api_url,omitempty"` - VictorOpsAPIKey Secret `yaml:"victorops_api_key,omitempty" json:"victorops_api_key,omitempty"` - VictorOpsAPIKeyFile string `yaml:"victorops_api_key_file,omitempty" json:"victorops_api_key_file,omitempty"` - TelegramAPIUrl *URL `yaml:"telegram_api_url,omitempty" json:"telegram_api_url,omitempty"` - WebexAPIURL *URL `yaml:"webex_api_url,omitempty" json:"webex_api_url,omitempty"` + JiraAPIURL *URL `yaml:"jira_api_url,omitempty" json:"jira_api_url,omitempty"` + SMTPFrom string `yaml:"smtp_from,omitempty" json:"smtp_from,omitempty"` + SMTPHello string `yaml:"smtp_hello,omitempty" json:"smtp_hello,omitempty"` + SMTPSmarthost HostPort `yaml:"smtp_smarthost,omitempty" json:"smtp_smarthost,omitempty"` + SMTPAuthUsername string `yaml:"smtp_auth_username,omitempty" json:"smtp_auth_username,omitempty"` + SMTPAuthPassword Secret `yaml:"smtp_auth_password,omitempty" json:"smtp_auth_password,omitempty"` + SMTPAuthPasswordFile string `yaml:"smtp_auth_password_file,omitempty" json:"smtp_auth_password_file,omitempty"` + SMTPAuthSecret Secret `yaml:"smtp_auth_secret,omitempty" json:"smtp_auth_secret,omitempty"` + SMTPAuthIdentity string `yaml:"smtp_auth_identity,omitempty" json:"smtp_auth_identity,omitempty"` + SMTPRequireTLS bool `yaml:"smtp_require_tls" json:"smtp_require_tls,omitempty"` + SMTPTLSConfig *commoncfg.TLSConfig `yaml:"smtp_tls_config,omitempty" json:"smtp_tls_config,omitempty"` + SlackAPIURL *SecretURL `yaml:"slack_api_url,omitempty" json:"slack_api_url,omitempty"` + SlackAPIURLFile string `yaml:"slack_api_url_file,omitempty" json:"slack_api_url_file,omitempty"` + PagerdutyURL *URL `yaml:"pagerduty_url,omitempty" json:"pagerduty_url,omitempty"` + OpsGenieAPIURL *URL `yaml:"opsgenie_api_url,omitempty" json:"opsgenie_api_url,omitempty"` + OpsGenieAPIKey Secret `yaml:"opsgenie_api_key,omitempty" json:"opsgenie_api_key,omitempty"` + OpsGenieAPIKeyFile string `yaml:"opsgenie_api_key_file,omitempty" json:"opsgenie_api_key_file,omitempty"` + WeChatAPIURL *URL `yaml:"wechat_api_url,omitempty" json:"wechat_api_url,omitempty"` + WeChatAPISecret Secret `yaml:"wechat_api_secret,omitempty" json:"wechat_api_secret,omitempty"` + WeChatAPICorpID string `yaml:"wechat_api_corp_id,omitempty" json:"wechat_api_corp_id,omitempty"` + VictorOpsAPIURL *URL `yaml:"victorops_api_url,omitempty" json:"victorops_api_url,omitempty"` + VictorOpsAPIKey Secret `yaml:"victorops_api_key,omitempty" json:"victorops_api_key,omitempty"` + VictorOpsAPIKeyFile string `yaml:"victorops_api_key_file,omitempty" json:"victorops_api_key_file,omitempty"` + TelegramAPIUrl *URL `yaml:"telegram_api_url,omitempty" json:"telegram_api_url,omitempty"` + WebexAPIURL *URL `yaml:"webex_api_url,omitempty" json:"webex_api_url,omitempty"` } // UnmarshalYAML implements the yaml.Unmarshaler interface for GlobalConfig. diff --git a/config/config_test.go b/config/config_test.go index 7aba475f72..fa21bb498a 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -602,6 +602,15 @@ func TestUnmarshalSecretURL(t *testing.T) { require.Equal(t, "http://example.com/se%20cret", u.String(), "SecretURL not properly unmarshaled in YAML.") } +func TestHideSecretURL(t *testing.T) { + b := []byte(`"://wrongurl/"`) + var u SecretURL + + err := json.Unmarshal(b, &u) + require.Error(t, err) + require.NotContains(t, err.Error(), "wrongurl") +} + func TestMarshalURL(t *testing.T) { for name, tc := range map[string]struct { input *URL @@ -857,9 +866,12 @@ func TestEmptyFieldsAndRegex(t *testing.T) { FollowRedirects: true, EnableHTTP2: true, }, - ResolveTimeout: model.Duration(5 * time.Minute), - SMTPSmarthost: HostPort{Host: "localhost", Port: "25"}, - SMTPFrom: "alertmanager@example.org", + ResolveTimeout: model.Duration(5 * time.Minute), + SMTPSmarthost: HostPort{Host: "localhost", Port: "25"}, + SMTPFrom: "alertmanager@example.org", + SMTPTLSConfig: &commoncfg.TLSConfig{ + InsecureSkipVerify: false, + }, SlackAPIURL: (*SecretURL)(mustParseURL("http://slack.example.com/")), SMTPRequireTLS: true, PagerdutyURL: mustParseURL("https://events.pagerduty.com/v2/enqueue"), @@ -905,6 +917,9 @@ func TestEmptyFieldsAndRegex(t *testing.T) { Smarthost: HostPort{Host: "localhost", Port: "25"}, HTML: "{{ template \"email.default.html\" . }}", RequireTLS: &boolFoo, + TLSConfig: &commoncfg.TLSConfig{ + InsecureSkipVerify: false, + }, }, }, }, diff --git a/config/notifiers.go b/config/notifiers.go index 1142c52b04..91f84adf8a 100644 --- a/config/notifiers.go +++ b/config/notifiers.go @@ -258,20 +258,20 @@ type EmailConfig struct { NotifierConfig `yaml:",inline" json:",inline"` // Email address to notify. - To string `yaml:"to,omitempty" json:"to,omitempty"` - From string `yaml:"from,omitempty" json:"from,omitempty"` - Hello string `yaml:"hello,omitempty" json:"hello,omitempty"` - Smarthost HostPort `yaml:"smarthost,omitempty" json:"smarthost,omitempty"` - AuthUsername string `yaml:"auth_username,omitempty" json:"auth_username,omitempty"` - AuthPassword Secret `yaml:"auth_password,omitempty" json:"auth_password,omitempty"` - AuthPasswordFile string `yaml:"auth_password_file,omitempty" json:"auth_password_file,omitempty"` - AuthSecret Secret `yaml:"auth_secret,omitempty" json:"auth_secret,omitempty"` - AuthIdentity string `yaml:"auth_identity,omitempty" json:"auth_identity,omitempty"` - Headers map[string]string `yaml:"headers,omitempty" json:"headers,omitempty"` - HTML string `yaml:"html,omitempty" json:"html,omitempty"` - Text string `yaml:"text,omitempty" json:"text,omitempty"` - RequireTLS *bool `yaml:"require_tls,omitempty" json:"require_tls,omitempty"` - TLSConfig commoncfg.TLSConfig `yaml:"tls_config,omitempty" json:"tls_config,omitempty"` + To string `yaml:"to,omitempty" json:"to,omitempty"` + From string `yaml:"from,omitempty" json:"from,omitempty"` + Hello string `yaml:"hello,omitempty" json:"hello,omitempty"` + Smarthost HostPort `yaml:"smarthost,omitempty" json:"smarthost,omitempty"` + AuthUsername string `yaml:"auth_username,omitempty" json:"auth_username,omitempty"` + AuthPassword Secret `yaml:"auth_password,omitempty" json:"auth_password,omitempty"` + AuthPasswordFile string `yaml:"auth_password_file,omitempty" json:"auth_password_file,omitempty"` + AuthSecret Secret `yaml:"auth_secret,omitempty" json:"auth_secret,omitempty"` + AuthIdentity string `yaml:"auth_identity,omitempty" json:"auth_identity,omitempty"` + Headers map[string]string `yaml:"headers,omitempty" json:"headers,omitempty"` + HTML string `yaml:"html,omitempty" json:"html,omitempty"` + Text string `yaml:"text,omitempty" json:"text,omitempty"` + RequireTLS *bool `yaml:"require_tls,omitempty" json:"require_tls,omitempty"` + TLSConfig *commoncfg.TLSConfig `yaml:"tls_config,omitempty" json:"tls_config,omitempty"` } // UnmarshalYAML implements the yaml.Unmarshaler interface. diff --git a/docs/configuration.md b/docs/configuration.md index 681a937e8a..dd78a736ae 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -94,6 +94,8 @@ global: # The default SMTP TLS requirement. # Note that Go does not support unencrypted connections to remote SMTP endpoints. [ smtp_require_tls: | default = true ] + # The default TLS configuration for SMTP receivers + [ smtp_tls_config: ] # Default settings for the JIRA integration. [ jira_api_url: ] @@ -909,7 +911,7 @@ to: # TLS configuration. tls_config: - [ ] + [ | default = global.smtp_tls_config ] # The HTML body of the email notification. [ html: | default = '{{ template "email.default.html" . }}' ] diff --git a/featurecontrol/featurecontrol.go b/featurecontrol/featurecontrol.go index 9ff7a2d8fd..4616a057fb 100644 --- a/featurecontrol/featurecontrol.go +++ b/featurecontrol/featurecontrol.go @@ -26,18 +26,21 @@ const ( FeatureReceiverNameInMetrics = "receiver-name-in-metrics" FeatureClassicMode = "classic-mode" FeatureUTF8StrictMode = "utf8-strict-mode" + FeatureAutoGOMAXPROCS = "auto-gomaxprocs" ) var AllowedFlags = []string{ FeatureReceiverNameInMetrics, FeatureClassicMode, FeatureUTF8StrictMode, + FeatureAutoGOMAXPROCS, } type Flagger interface { EnableReceiverNamesInMetrics() bool ClassicMode() bool UTF8StrictMode() bool + EnableAutoGOMAXPROCS() bool } type Flags struct { @@ -45,6 +48,7 @@ type Flags struct { enableReceiverNamesInMetrics bool classicMode bool utf8StrictMode bool + enableAutoGOMAXPROCS bool } func (f *Flags) EnableReceiverNamesInMetrics() bool { @@ -59,6 +63,10 @@ func (f *Flags) UTF8StrictMode() bool { return f.utf8StrictMode } +func (f *Flags) EnableAutoGOMAXPROCS() bool { + return f.enableAutoGOMAXPROCS +} + type flagOption func(flags *Flags) func enableReceiverNameInMetrics() flagOption { @@ -79,6 +87,12 @@ func enableUTF8StrictMode() flagOption { } } +func enableAutoGOMAXPROCS() flagOption { + return func(configs *Flags) { + configs.enableAutoGOMAXPROCS = true + } +} + func NewFlags(logger log.Logger, features string) (Flagger, error) { fc := &Flags{logger: logger} opts := []flagOption{} @@ -98,6 +112,9 @@ func NewFlags(logger log.Logger, features string) (Flagger, error) { case FeatureUTF8StrictMode: opts = append(opts, enableUTF8StrictMode()) level.Warn(logger).Log("msg", "UTF-8 strict mode enabled") + case FeatureAutoGOMAXPROCS: + opts = append(opts, enableAutoGOMAXPROCS()) + level.Warn(logger).Log("msg", "Automatically set GOMAXPROCS to match Linux container CPU quota") default: return nil, fmt.Errorf("Unknown option '%s' for --enable-feature", feature) } @@ -121,3 +138,5 @@ func (n NoopFlags) EnableReceiverNamesInMetrics() bool { return false } func (n NoopFlags) ClassicMode() bool { return false } func (n NoopFlags) UTF8StrictMode() bool { return false } + +func (n NoopFlags) EnableAutoGOMAXPROCS() bool { return false } diff --git a/go.mod b/go.mod index 97e8f42dde..1f1dbda557 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/aws/aws-sdk-go v1.53.14 github.com/benbjohnson/clock v1.3.5 github.com/cenkalti/backoff/v4 v4.3.0 - github.com/cespare/xxhash/v2 v2.2.0 + github.com/cespare/xxhash/v2 v2.3.0 github.com/go-kit/log v0.2.1 github.com/go-openapi/analysis v0.23.0 github.com/go-openapi/errors v0.22.0 @@ -40,6 +40,7 @@ require ( github.com/trivago/tgo v1.0.7 github.com/xlab/treeprint v1.2.0 go.uber.org/atomic v1.11.0 + go.uber.org/automaxprocs v1.5.3 golang.org/x/mod v0.18.0 golang.org/x/net v0.26.0 golang.org/x/text v0.16.0 diff --git a/go.sum b/go.sum index 42625c0602..cea064751e 100644 --- a/go.sum +++ b/go.sum @@ -91,8 +91,8 @@ github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyY github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -411,6 +411,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= +github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= @@ -521,6 +523,8 @@ go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqe go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8= +go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= diff --git a/matchers/compat/parse.go b/matcher/compat/parse.go similarity index 95% rename from matchers/compat/parse.go rename to matcher/compat/parse.go index 0c0dfffb1f..14aeb5a2ae 100644 --- a/matchers/compat/parse.go +++ b/matcher/compat/parse.go @@ -24,7 +24,7 @@ import ( "github.com/prometheus/common/model" "github.com/prometheus/alertmanager/featurecontrol" - "github.com/prometheus/alertmanager/matchers/parse" + "github.com/prometheus/alertmanager/matcher/parse" "github.com/prometheus/alertmanager/pkg/labels" ) @@ -90,7 +90,7 @@ func ClassicMatchersParser(l log.Logger) ParseMatchers { } } -// UTF8MatcherParser uses the new matchers/parse parser to parse the matcher +// UTF8MatcherParser uses the new matcher/parse parser to parse the matcher // in the input string. If this fails it does not revert to the pkg/labels parser. func UTF8MatcherParser(l log.Logger) ParseMatcher { return func(input, origin string) (matcher *labels.Matcher, err error) { @@ -102,7 +102,7 @@ func UTF8MatcherParser(l log.Logger) ParseMatcher { } } -// UTF8MatchersParser uses the new matchers/parse parser to parse zero or more +// UTF8MatchersParser uses the new matcher/parse parser to parse zero or more // matchers in the input string. If this fails it does not revert to the // pkg/labels parser. func UTF8MatchersParser(l log.Logger) ParseMatchers { @@ -112,7 +112,7 @@ func UTF8MatchersParser(l log.Logger) ParseMatchers { } } -// FallbackMatcherParser uses the new matchers/parse parser to parse zero or more +// FallbackMatcherParser uses the new matcher/parse parser to parse zero or more // matchers in the string. If this fails it reverts to the pkg/labels parser and // emits a warning log line. func FallbackMatcherParser(l log.Logger) ParseMatcher { @@ -130,7 +130,7 @@ func FallbackMatcherParser(l log.Logger) ParseMatcher { if cErr != nil { return nil, cErr } - // The input is valid in the pkg/labels parser, but not the matchers/parse + // The input is valid in the pkg/labels parser, but not the matcher/parse // parser. This means the input is not forwards compatible. suggestion := cMatcher.String() level.Warn(l).Log("msg", "Alertmanager is moving to a new parser for labels and matchers, and this input is incompatible. Alertmanager has instead parsed the input using the classic matchers parser as a fallback. To make this input compatible with the UTF-8 matchers parser please make sure all regular expressions and values are double-quoted. If you are still seeing this message please open an issue.", "input", input, "origin", origin, "err", nErr, "suggestion", suggestion) @@ -146,7 +146,7 @@ func FallbackMatcherParser(l log.Logger) ParseMatcher { } } -// FallbackMatchersParser uses the new matchers/parse parser to parse the +// FallbackMatchersParser uses the new matcher/parse parser to parse the // matcher in the input string. If this fails it falls back to the pkg/labels // parser and emits a warning log line. func FallbackMatchersParser(l log.Logger) ParseMatchers { @@ -161,7 +161,7 @@ func FallbackMatchersParser(l log.Logger) ParseMatchers { if cErr != nil { return nil, cErr } - // The input is valid in the pkg/labels parser, but not the matchers/parse + // The input is valid in the pkg/labels parser, but not the matcher/parse // parser. This means the input is not forwards compatible. var sb strings.Builder for i, n := range cMatchers { @@ -172,7 +172,7 @@ func FallbackMatchersParser(l log.Logger) ParseMatchers { } suggestion := sb.String() // The input is valid in the pkg/labels parser, but not the - // new matchers/parse parser. + // new matcher/parse parser. level.Warn(l).Log("msg", "Alertmanager is moving to a new parser for labels and matchers, and this input is incompatible. Alertmanager has instead parsed the input using the classic matchers parser as a fallback. To make this input compatible with the UTF-8 matchers parser please make sure all regular expressions and values are double-quoted. If you are still seeing this message please open an issue.", "input", input, "origin", origin, "err", nErr, "suggestion", suggestion) return cMatchers, nil } diff --git a/matchers/compat/parse_test.go b/matcher/compat/parse_test.go similarity index 100% rename from matchers/compat/parse_test.go rename to matcher/compat/parse_test.go diff --git a/matchers/compliance/compliance_test.go b/matcher/compliance/compliance_test.go similarity index 99% rename from matchers/compliance/compliance_test.go rename to matcher/compliance/compliance_test.go index 844f80a542..2fd39ec68d 100644 --- a/matchers/compliance/compliance_test.go +++ b/matcher/compliance/compliance_test.go @@ -17,7 +17,7 @@ import ( "reflect" "testing" - "github.com/prometheus/alertmanager/matchers/parse" + "github.com/prometheus/alertmanager/matcher/parse" "github.com/prometheus/alertmanager/pkg/labels" ) diff --git a/matchers/parse/bench_test.go b/matcher/parse/bench_test.go similarity index 100% rename from matchers/parse/bench_test.go rename to matcher/parse/bench_test.go diff --git a/matchers/parse/fuzz_test.go b/matcher/parse/fuzz_test.go similarity index 100% rename from matchers/parse/fuzz_test.go rename to matcher/parse/fuzz_test.go diff --git a/matchers/parse/lexer.go b/matcher/parse/lexer.go similarity index 100% rename from matchers/parse/lexer.go rename to matcher/parse/lexer.go diff --git a/matchers/parse/lexer_test.go b/matcher/parse/lexer_test.go similarity index 100% rename from matchers/parse/lexer_test.go rename to matcher/parse/lexer_test.go diff --git a/matchers/parse/parse.go b/matcher/parse/parse.go similarity index 100% rename from matchers/parse/parse.go rename to matcher/parse/parse.go diff --git a/matchers/parse/parse_test.go b/matcher/parse/parse_test.go similarity index 100% rename from matchers/parse/parse_test.go rename to matcher/parse/parse_test.go diff --git a/matchers/parse/token.go b/matcher/parse/token.go similarity index 100% rename from matchers/parse/token.go rename to matcher/parse/token.go diff --git a/notify/email/email.go b/notify/email/email.go index 98c98c17a8..04dfd501cd 100644 --- a/notify/email/email.go +++ b/notify/email/email.go @@ -131,7 +131,7 @@ func (n *Email) Notify(ctx context.Context, as ...*types.Alert) (bool, error) { success = false ) if n.conf.Smarthost.Port == "465" { - tlsConfig, err := commoncfg.NewTLSConfig(&n.conf.TLSConfig) + tlsConfig, err := commoncfg.NewTLSConfig(n.conf.TLSConfig) if err != nil { return false, fmt.Errorf("parse TLS configuration: %w", err) } @@ -178,7 +178,7 @@ func (n *Email) Notify(ctx context.Context, as ...*types.Alert) (bool, error) { return true, fmt.Errorf("'require_tls' is true (default) but %q does not advertise the STARTTLS extension", n.conf.Smarthost) } - tlsConf, err := commoncfg.NewTLSConfig(&n.conf.TLSConfig) + tlsConf, err := commoncfg.NewTLSConfig(n.conf.TLSConfig) if err != nil { return false, fmt.Errorf("parse TLS configuration: %w", err) } diff --git a/notify/email/email_test.go b/notify/email/email_test.go index 6303b85d72..970c117749 100644 --- a/notify/email/email_test.go +++ b/notify/email/email_test.go @@ -407,7 +407,7 @@ func TestEmailNotifyWithSTARTTLS(t *testing.T) { Text: "Text body", RequireTLS: &trueVar, // MailDev embeds a self-signed certificate which can't be retrieved. - TLSConfig: commoncfg.TLSConfig{InsecureSkipVerify: true}, + TLSConfig: &commoncfg.TLSConfig{InsecureSkipVerify: true}, }, c.Server, ) diff --git a/notify/notify_test.go b/notify/notify_test.go index 5c2297e993..eb14c58b76 100644 --- a/notify/notify_test.go +++ b/notify/notify_test.go @@ -718,11 +718,11 @@ func TestMuteStageWithSilences(t *testing.T) { if err != nil { t.Fatal(err) } - silID, err := silences.Set(&silencepb.Silence{ + sil := &silencepb.Silence{ EndsAt: utcNow().Add(time.Hour), Matchers: []*silencepb.Matcher{{Name: "mute", Pattern: "me"}}, - }) - if err != nil { + } + if err = silences.Set(sil); err != nil { t.Fatal(err) } @@ -799,7 +799,7 @@ func TestMuteStageWithSilences(t *testing.T) { } // Expire the silence and verify that no alerts are silenced now. - if err := silences.Expire(silID); err != nil { + if err := silences.Expire(sil.Id); err != nil { t.Fatal(err) } diff --git a/pkg/labels/matcher.go b/pkg/labels/matcher.go index f37fcb2173..eba6b4ca60 100644 --- a/pkg/labels/matcher.go +++ b/pkg/labels/matcher.go @@ -205,7 +205,7 @@ func (ms Matchers) String() string { return buf.String() } -// This is copied from matchers/parse/lexer.go. It will be removed when +// This is copied from matcher/parse/lexer.go. It will be removed when // the transition window from classic matchers to UTF-8 matchers is complete, // as then we can use double quotes when printing the label name for all // matchers. Until then, the classic parser does not understand double quotes diff --git a/silence/silence.go b/silence/silence.go index 6b67b2ca38..ee97551908 100644 --- a/silence/silence.go +++ b/silence/silence.go @@ -37,17 +37,17 @@ import ( "github.com/prometheus/common/model" "github.com/prometheus/alertmanager/cluster" - "github.com/prometheus/alertmanager/matchers/compat" + "github.com/prometheus/alertmanager/matcher/compat" "github.com/prometheus/alertmanager/pkg/labels" pb "github.com/prometheus/alertmanager/silence/silencepb" "github.com/prometheus/alertmanager/types" ) // ErrNotFound is returned if a silence was not found. -var ErrNotFound = fmt.Errorf("silence not found") +var ErrNotFound = errors.New("silence not found") // ErrInvalidState is returned if the state isn't valid. -var ErrInvalidState = fmt.Errorf("invalid state") +var ErrInvalidState = errors.New("invalid state") type matcherCache map[*pb.Silence]labels.Matchers @@ -206,10 +206,10 @@ type Silences struct { type Limits struct { // MaxSilences limits the maximum number of silences, including expired // silences. - MaxSilences int - // MaxPerSilenceBytes is the maximum size of an individual silence as + MaxSilences func() int + // MaxSilenceSizeBytes is the maximum size of an individual silence as // stored on disk. - MaxPerSilenceBytes int + MaxSilenceSizeBytes func() int } // MaintenanceFunc represents the function to run as part of the periodic maintenance for silences. @@ -338,7 +338,7 @@ type Options struct { func (o *Options) validate() error { if o.SnapshotFile != "" && o.SnapshotReader != nil { - return fmt.Errorf("only one of SnapshotFile and SnapshotReader must be set") + return errors.New("only one of SnapshotFile and SnapshotReader must be set") } return nil } @@ -518,9 +518,6 @@ func matchesEmpty(m *pb.Matcher) bool { } func validateSilence(s *pb.Silence) error { - if s.Id == "" { - return errors.New("ID missing") - } if len(s.Matchers) == 0 { return errors.New("at least one matcher required") } @@ -544,9 +541,6 @@ func validateSilence(s *pb.Silence) error { if s.EndsAt.Before(s.StartsAt) { return errors.New("end time must not be before start time") } - if s.UpdatedAt.IsZero() { - return errors.New("invalid zero update timestamp") - } return nil } @@ -556,6 +550,16 @@ func cloneSilence(sil *pb.Silence) *pb.Silence { return &s } +func (s *Silences) checkSizeLimits(msil *pb.MeshSilence) error { + if s.limits.MaxSilenceSizeBytes != nil { + n := msil.Size() + if m := s.limits.MaxSilenceSizeBytes(); m > 0 && n > m { + return fmt.Errorf("silence exceeded maximum size: %d bytes (limit: %d bytes)", n, m) + } + } + return nil +} + func (s *Silences) getSilence(id string) (*pb.Silence, bool) { msil, ok := s.st[id] if !ok { @@ -564,85 +568,88 @@ func (s *Silences) getSilence(id string) (*pb.Silence, bool) { return msil.Silence, true } -func (s *Silences) setSilence(sil *pb.Silence, now time.Time, skipValidate bool) error { - sil.UpdatedAt = now - - if !skipValidate { - if err := validateSilence(sil); err != nil { - return fmt.Errorf("silence invalid: %w", err) - } - } - - msil := &pb.MeshSilence{ +func (s *Silences) toMeshSilence(sil *pb.Silence) *pb.MeshSilence { + return &pb.MeshSilence{ Silence: sil, ExpiresAt: sil.EndsAt.Add(s.retention), } +} + +func (s *Silences) setSilence(msil *pb.MeshSilence, now time.Time) error { b, err := marshalMeshSilence(msil) if err != nil { return err } - - // Check the limit unless the silence has been expired. This is to avoid - // situations where silences cannot be expired after the limit has been - // reduced. - if n := msil.Size(); s.limits.MaxPerSilenceBytes > 0 && n > s.limits.MaxPerSilenceBytes && sil.EndsAt.After(now) { - return fmt.Errorf("silence exceeded maximum size: %d bytes (limit: %d bytes)", n, s.limits.MaxPerSilenceBytes) - } - if s.st.merge(msil, now) { s.version++ } s.broadcast(b) - return nil } // Set the specified silence. If a silence with the ID already exists and the modification // modifies history, the old silence gets expired and a new one is created. -func (s *Silences) Set(sil *pb.Silence) (string, error) { +func (s *Silences) Set(sil *pb.Silence) error { s.mtx.Lock() defer s.mtx.Unlock() now := s.nowUTC() + if sil.StartsAt.IsZero() { + sil.StartsAt = now + } + + if err := validateSilence(sil); err != nil { + return fmt.Errorf("invalid silence: %w", err) + } + prev, ok := s.getSilence(sil.Id) if sil.Id != "" && !ok { - return "", ErrNotFound + return ErrNotFound } - if ok { - if canUpdate(prev, sil, now) { - return sil.Id, s.setSilence(sil, now, false) - } - if getState(prev, s.nowUTC()) != types.SilenceStateExpired { - // We cannot update the silence, expire the old one. - if err := s.expire(prev.Id); err != nil { - return "", fmt.Errorf("expire previous silence: %w", err) - } + if ok && canUpdate(prev, sil, now) { + sil.UpdatedAt = now + msil := s.toMeshSilence(sil) + if err := s.checkSizeLimits(msil); err != nil { + return err } + return s.setSilence(msil, now) } - // If we got here it's either a new silence or a replacing one. - if s.limits.MaxSilences > 0 { - if len(s.st)+1 > s.limits.MaxSilences { - return "", fmt.Errorf("exceeded maximum number of silences: %d (limit: %d)", len(s.st), s.limits.MaxSilences) + // If we got here it's either a new silence or a replacing one (which would + // also create a new silence) so we need to make sure we have capacity for + // the new silence. + if s.limits.MaxSilences != nil { + if m := s.limits.MaxSilences(); m > 0 && len(s.st)+1 > m { + return fmt.Errorf("exceeded maximum number of silences: %d (limit: %d)", len(s.st), m) } } uid, err := uuid.NewV4() if err != nil { - return "", fmt.Errorf("generate uuid: %w", err) + return fmt.Errorf("generate uuid: %w", err) } sil.Id = uid.String() if sil.StartsAt.Before(now) { sil.StartsAt = now } + sil.UpdatedAt = now + + msil := s.toMeshSilence(sil) + if err := s.checkSizeLimits(msil); err != nil { + return err + } - if err = s.setSilence(sil, now, false); err != nil { - return "", err + if ok && getState(prev, s.nowUTC()) != types.SilenceStateExpired { + // We cannot update the silence, expire the old one to leave a history of + // the silence before modification. + if err := s.expire(prev.Id); err != nil { + return fmt.Errorf("expire previous silence: %w", err) + } } - return sil.Id, nil + return s.setSilence(msil, now) } // canUpdate returns true if silence a can be updated to b without @@ -700,10 +707,8 @@ func (s *Silences) expire(id string) error { sil.StartsAt = now sil.EndsAt = now } - - // Skip validation of the silence when expiring it. Without this, silences created - // with valid UTF-8 matchers cannot be expired when Alertmanager is run in classic mode. - return s.setSilence(sil, now, true) + sil.UpdatedAt = now + return s.setSilence(s.toMeshSilence(sil), now) } // QueryParam expresses parameters along which silences are queried. diff --git a/silence/silence_bench_test.go b/silence/silence_bench_test.go index 146316e3f3..e1d39a9418 100644 --- a/silence/silence_bench_test.go +++ b/silence/silence_bench_test.go @@ -58,8 +58,7 @@ func benchmarkMutes(b *testing.B, n int) { var silenceIDs []string for i := 0; i < n; i++ { - var silenceID string - silenceID, err = silences.Set(&silencepb.Silence{ + s := &silencepb.Silence{ Matchers: []*silencepb.Matcher{{ Type: silencepb.Matcher_EQUAL, Name: "foo", @@ -67,9 +66,10 @@ func benchmarkMutes(b *testing.B, n int) { }}, StartsAt: now, EndsAt: now.Add(time.Minute), - }) + } + require.NoError(b, silences.Set(s)) require.NoError(b, err) - silenceIDs = append(silenceIDs, silenceID) + silenceIDs = append(silenceIDs, s.Id) } require.Len(b, silenceIDs, n) diff --git a/silence/silence_test.go b/silence/silence_test.go index a956d756b3..6fe536e557 100644 --- a/silence/silence_test.go +++ b/silence/silence_test.go @@ -15,6 +15,7 @@ package silence import ( "bytes" + "fmt" "os" "runtime" "sort" @@ -33,7 +34,7 @@ import ( "go.uber.org/atomic" "github.com/prometheus/alertmanager/featurecontrol" - "github.com/prometheus/alertmanager/matchers/compat" + "github.com/prometheus/alertmanager/matcher/compat" pb "github.com/prometheus/alertmanager/silence/silencepb" "github.com/prometheus/alertmanager/types" ) @@ -294,7 +295,7 @@ func TestSilencesSetSilence(t *testing.T) { func() { s.mtx.Lock() defer s.mtx.Unlock() - require.NoError(t, s.setSilence(sil, nowpb, false)) + require.NoError(t, s.setSilence(s.toMeshSilence(sil), nowpb)) }() // Ensure broadcast was called. @@ -321,14 +322,13 @@ func TestSilenceSet(t *testing.T) { StartsAt: start1.Add(2 * time.Minute), EndsAt: start1.Add(5 * time.Minute), } - id1, err := s.Set(sil1) - require.NoError(t, err) - require.NotEqual(t, "", id1) + require.NoError(t, s.Set(sil1)) + require.NotEqual(t, "", sil1.Id) want := state{ - id1: &pb.MeshSilence{ + sil1.Id: &pb.MeshSilence{ Silence: &pb.Silence{ - Id: id1, + Id: sil1.Id, Matchers: []*pb.Matcher{{Name: "a", Pattern: "b"}}, StartsAt: start1.Add(2 * time.Minute), EndsAt: start1.Add(5 * time.Minute), @@ -347,15 +347,14 @@ func TestSilenceSet(t *testing.T) { Matchers: []*pb.Matcher{{Name: "a", Pattern: "b"}}, EndsAt: start2.Add(1 * time.Minute), } - id2, err := s.Set(sil2) - require.NoError(t, err) - require.NotEqual(t, "", id2) + require.NoError(t, s.Set(sil2)) + require.NotEqual(t, "", sil2.Id) want = state{ - id1: want[id1], - id2: &pb.MeshSilence{ + sil1.Id: want[sil1.Id], + sil2.Id: &pb.MeshSilence{ Silence: &pb.Silence{ - Id: id2, + Id: sil2.Id, Matchers: []*pb.Matcher{{Name: "a", Pattern: "b"}}, StartsAt: start2, EndsAt: start2.Add(1 * time.Minute), @@ -366,104 +365,129 @@ func TestSilenceSet(t *testing.T) { } require.Equal(t, want, s.st, "unexpected state after silence creation") - // Overwrite silence 2 with new end time. - clock.Add(time.Minute) - start3 := s.nowUTC() - + // Should be able to update silence without modifications. It is expected to + // keep the same ID. sil3 := cloneSilence(sil2) - sil3.EndsAt = start3.Add(100 * time.Minute) + require.NoError(t, s.Set(sil3)) + require.Equal(t, sil2.Id, sil3.Id) - id3, err := s.Set(sil3) - require.NoError(t, err) - require.Equal(t, id2, id3) + // Should be able to update silence with comment. It is also expected to + // keep the same ID. + sil4 := cloneSilence(sil3) + sil4.Comment = "c" + require.NoError(t, s.Set(sil4)) + require.Equal(t, sil3.Id, sil4.Id) + // Extend sil4 to expire at a later time. This should not expire the + // existing silence, and so should also keep the same ID. + clock.Add(time.Minute) + start5 := s.nowUTC() + sil5 := cloneSilence(sil4) + sil5.EndsAt = start5.Add(100 * time.Minute) + require.NoError(t, s.Set(sil5)) + require.Equal(t, sil4.Id, sil5.Id) want = state{ - id1: want[id1], - id2: &pb.MeshSilence{ + sil1.Id: want[sil1.Id], + sil2.Id: &pb.MeshSilence{ Silence: &pb.Silence{ - Id: id2, + Id: sil2.Id, Matchers: []*pb.Matcher{{Name: "a", Pattern: "b"}}, StartsAt: start2, - EndsAt: start3.Add(100 * time.Minute), - UpdatedAt: start3, + EndsAt: start5.Add(100 * time.Minute), + UpdatedAt: start5, + Comment: "c", }, - ExpiresAt: start3.Add(100*time.Minute + s.retention), + ExpiresAt: start5.Add(100*time.Minute + s.retention), }, } require.Equal(t, want, s.st, "unexpected state after silence creation") - // Update this silence again with new matcher. This expires it and creates a new one. + // Replace the silence sil5 with another silence with different matchers. + // Unlike previous updates, changing the matchers for an existing silence + // will expire the existing silence and create a new silence. The new + // silence is expected to have a different ID to preserve the history of + // the previous silence. clock.Add(time.Minute) - start4 := s.nowUTC() - - sil4 := cloneSilence(sil3) - sil4.Matchers = []*pb.Matcher{{Name: "a", Pattern: "c"}} - - id4, err := s.Set(sil4) - require.NoError(t, err) - // This new silence gets a new id. - require.NotEqual(t, id2, id4) + start6 := s.nowUTC() + sil6 := cloneSilence(sil5) + sil6.Matchers = []*pb.Matcher{{Name: "a", Pattern: "c"}} + require.NoError(t, s.Set(sil6)) + require.NotEqual(t, sil5.Id, sil6.Id) want = state{ - id1: want[id1], - id2: &pb.MeshSilence{ + sil1.Id: want[sil1.Id], + sil2.Id: &pb.MeshSilence{ Silence: &pb.Silence{ - Id: id2, + Id: sil2.Id, Matchers: []*pb.Matcher{{Name: "a", Pattern: "b"}}, StartsAt: start2, - EndsAt: start4, // Expired - UpdatedAt: start4, + EndsAt: start6, // Expired + UpdatedAt: start6, + Comment: "c", }, - ExpiresAt: start4.Add(s.retention), + ExpiresAt: start6.Add(s.retention), }, - id4: &pb.MeshSilence{ + sil6.Id: &pb.MeshSilence{ Silence: &pb.Silence{ - Id: id4, + Id: sil6.Id, Matchers: []*pb.Matcher{{Name: "a", Pattern: "c"}}, - StartsAt: start4, - EndsAt: start3.Add(100 * time.Minute), - UpdatedAt: start4, + StartsAt: start6, + EndsAt: start5.Add(100 * time.Minute), + UpdatedAt: start6, + Comment: "c", }, - ExpiresAt: start3.Add(100*time.Minute + s.retention), + ExpiresAt: start5.Add(100*time.Minute + s.retention), }, } require.Equal(t, want, s.st, "unexpected state after silence creation") - // Re-create the silence that just expired. + // Re-create the silence that we just replaced. Changing the start time, + // just like changing the matchers, creates a new silence with a different + // ID. This is again to preserve the history of the original silence. clock.Add(time.Minute) - start5 := s.nowUTC() - - sil5 := cloneSilence(sil3) - sil5.StartsAt = start1 - sil5.EndsAt = start1.Add(5 * time.Minute) - - id5, err := s.Set(sil5) - require.NoError(t, err) - require.NotEqual(t, id2, id4) - + start7 := s.nowUTC() + sil7 := cloneSilence(sil5) + sil7.StartsAt = start1 + sil7.EndsAt = start1.Add(5 * time.Minute) + require.NoError(t, s.Set(sil7)) + require.NotEqual(t, sil2.Id, sil7.Id) want = state{ - id1: want[id1], - id2: want[id2], - id4: want[id4], - id5: &pb.MeshSilence{ + sil1.Id: want[sil1.Id], + sil2.Id: want[sil2.Id], + sil6.Id: want[sil6.Id], + sil7.Id: &pb.MeshSilence{ Silence: &pb.Silence{ - Id: id5, + Id: sil7.Id, Matchers: []*pb.Matcher{{Name: "a", Pattern: "b"}}, - StartsAt: start5, // New silences have their start time set to "now" when created. + StartsAt: start7, // New silences have their start time set to "now" when created. EndsAt: start1.Add(5 * time.Minute), - UpdatedAt: start5, + UpdatedAt: start7, + Comment: "c", }, ExpiresAt: start1.Add(5*time.Minute + s.retention), }, } require.Equal(t, want, s.st, "unexpected state after silence creation") + + // Updating an existing silence with an invalid silence should not expire + // the original silence. + clock.Add(time.Millisecond) + sil8 := cloneSilence(sil7) + sil8.EndsAt = time.Time{} + require.EqualError(t, s.Set(sil8), "invalid silence: invalid zero end timestamp") + + // sil7 should not be expired because the update failed. + clock.Add(time.Millisecond) + sil7, err = s.QueryOne(QIDs(sil7.Id)) + require.NoError(t, err) + require.Equal(t, types.SilenceStateActive, getState(sil7, s.nowUTC())) } func TestSilenceLimits(t *testing.T) { s, err := New(Options{ Limits: Limits{ - MaxSilences: 1, - MaxPerSilenceBytes: 2 << 11, // 4KB + MaxSilences: func() int { return 1 }, + MaxSilenceSizeBytes: func() int { return 2 << 11 }, // 4KB }, }) require.NoError(t, err) @@ -474,38 +498,26 @@ func TestSilenceLimits(t *testing.T) { StartsAt: time.Now(), EndsAt: time.Now().Add(5 * time.Minute), } - id1, err := s.Set(sil1) - require.NoError(t, err) - require.NotEqual(t, "", id1) + require.NoError(t, s.Set(sil1)) - // Insert sil2 should fail because maximum number of silences - // has been exceeded. + // Insert sil2 should fail because maximum number of silences has been + // exceeded. sil2 := &pb.Silence{ - Matchers: []*pb.Matcher{{Name: "a", Pattern: "b"}}, + Matchers: []*pb.Matcher{{Name: "c", Pattern: "d"}}, StartsAt: time.Now(), EndsAt: time.Now().Add(5 * time.Minute), } - id2, err := s.Set(sil2) - require.EqualError(t, err, "exceeded maximum number of silences: 1 (limit: 1)") - require.Equal(t, "", id2) + require.EqualError(t, s.Set(sil2), "exceeded maximum number of silences: 1 (limit: 1)") - // Expire sil1 and run the GC. This should allow sil2 to be - // inserted. - require.NoError(t, s.Expire(id1)) + // Expire sil1 and run the GC. This should allow sil2 to be inserted. + require.NoError(t, s.Expire(sil1.Id)) n, err := s.GC() require.NoError(t, err) require.Equal(t, 1, n) + require.NoError(t, s.Set(sil2)) - id2, err = s.Set(sil2) - require.NoError(t, err) - require.NotEqual(t, "", id2) - - // Should be able to update sil2 without hitting the limit. - _, err = s.Set(sil2) - require.NoError(t, err) - - // Expire sil2. - require.NoError(t, s.Expire(id2)) + // Expire sil2 and run the GC. + require.NoError(t, s.Expire(sil2.Id)) n, err = s.GC() require.NoError(t, err) require.Equal(t, 1, n) @@ -514,25 +526,99 @@ func TestSilenceLimits(t *testing.T) { sil3 := &pb.Silence{ Matchers: []*pb.Matcher{ { - Name: strings.Repeat("a", 2<<9), - Pattern: strings.Repeat("b", 2<<9), + Name: strings.Repeat("e", 2<<9), + Pattern: strings.Repeat("f", 2<<9), }, { - Name: strings.Repeat("c", 2<<9), - Pattern: strings.Repeat("d", 2<<9), + Name: strings.Repeat("g", 2<<9), + Pattern: strings.Repeat("h", 2<<9), }, }, - CreatedBy: strings.Repeat("e", 2<<9), - Comment: strings.Repeat("f", 2<<9), + CreatedBy: strings.Repeat("i", 2<<9), + Comment: strings.Repeat("j", 2<<9), StartsAt: time.Now(), EndsAt: time.Now().Add(5 * time.Minute), } - id3, err := s.Set(sil3) - require.Error(t, err) - // Do not check the exact size as it can change between consecutive runs - // due to padding. - require.Contains(t, err.Error(), "silence exceeded maximum size") - require.Equal(t, "", id3) + require.EqualError(t, s.Set(sil3), fmt.Sprintf("silence exceeded maximum size: %d bytes (limit: 4096 bytes)", s.toMeshSilence(sil3).Size())) + + // Should be able to insert sil4. + sil4 := &pb.Silence{ + Matchers: []*pb.Matcher{{Name: "k", Pattern: "l"}}, + StartsAt: time.Now(), + EndsAt: time.Now().Add(5 * time.Minute), + } + require.NoError(t, s.Set(sil4)) + + // Should be able to update sil4 without modifications. It is expected to + // keep the same ID. + sil5 := cloneSilence(sil4) + require.NoError(t, s.Set(sil5)) + require.Equal(t, sil4.Id, sil5.Id) + + // Should be able to update the comment. It is also expected to keep the + // same ID. + sil6 := cloneSilence(sil5) + sil6.Comment = "m" + require.NoError(t, s.Set(sil6)) + require.Equal(t, sil5.Id, sil6.Id) + + // Should not be able to update the start and end time as this requires + // sil6 to be expired and a new silence to be created. However, this would + // exceed the maximum number of silences, which counts both active and + // expired silences. + sil7 := cloneSilence(sil6) + sil7.StartsAt = time.Now().Add(5 * time.Minute) + sil7.EndsAt = time.Now().Add(10 * time.Minute) + require.EqualError(t, s.Set(sil7), "exceeded maximum number of silences: 1 (limit: 1)") + + // sil6 should not be expired because the update failed. + sil6, err = s.QueryOne(QIDs(sil6.Id)) + require.NoError(t, err) + require.Equal(t, types.SilenceStateActive, getState(sil6, s.nowUTC())) + + // Should not be able to update with a comment that exceeds maximum size. + // Need to increase the maximum number of silences to test this. + s.limits.MaxSilences = func() int { return 2 } + sil8 := cloneSilence(sil6) + sil8.Comment = strings.Repeat("m", 2<<11) + require.EqualError(t, s.Set(sil8), fmt.Sprintf("silence exceeded maximum size: %d bytes (limit: 4096 bytes)", s.toMeshSilence(sil8).Size())) + + // sil6 should not be expired because the update failed. + sil6, err = s.QueryOne(QIDs(sil6.Id)) + require.NoError(t, err) + require.Equal(t, types.SilenceStateActive, getState(sil6, s.nowUTC())) + + // Should not be able to replace with a silence that exceeds maximum size. + // This is different from the previous assertion as unlike when adding or + // updating a comment, changing the matchers for a silence should expire + // the existing silence, unless the silence that is replacing it exceeds + // limits, in which case the operation should fail and the existing silence + // should still be active. + sil9 := cloneSilence(sil8) + sil9.Matchers = []*pb.Matcher{{Name: "n", Pattern: "o"}} + require.EqualError(t, s.Set(sil9), fmt.Sprintf("silence exceeded maximum size: %d bytes (limit: 4096 bytes)", s.toMeshSilence(sil9).Size())) + + // sil6 should not be expired because the update failed. + sil6, err = s.QueryOne(QIDs(sil6.Id)) + require.NoError(t, err) + require.Equal(t, types.SilenceStateActive, getState(sil6, s.nowUTC())) +} + +func TestSilenceNoLimits(t *testing.T) { + s, err := New(Options{ + Limits: Limits{}, + }) + require.NoError(t, err) + + // Insert sil should succeed without error. + sil := &pb.Silence{ + Matchers: []*pb.Matcher{{Name: "a", Pattern: "b"}}, + StartsAt: time.Now(), + EndsAt: time.Now().Add(5 * time.Minute), + Comment: strings.Repeat("c", 2<<9), + } + require.NoError(t, s.Set(sil)) + require.NotEqual(t, "", sil.Id) } func TestSetActiveSilence(t *testing.T) { @@ -553,7 +639,7 @@ func TestSetActiveSilence(t *testing.T) { StartsAt: startsAt, EndsAt: endsAt, } - id1, _ := s.Set(sil1) + require.NoError(t, s.Set(sil1)) // Update silence with 2 extra nanoseconds so the "seconds" part should not change @@ -561,20 +647,19 @@ func TestSetActiveSilence(t *testing.T) { newEndsAt := endsAt.Add(2 * time.Minute) sil2 := cloneSilence(sil1) - sil2.Id = id1 + sil2.Id = sil1.Id sil2.StartsAt = newStartsAt sil2.EndsAt = newEndsAt clock.Add(time.Minute) now = s.nowUTC() - id2, err := s.Set(sil2) - require.NoError(t, err) - require.Equal(t, id1, id2) + require.NoError(t, s.Set(sil2)) + require.Equal(t, sil1.Id, sil2.Id) want := state{ - id2: &pb.MeshSilence{ + sil2.Id: &pb.MeshSilence{ Silence: &pb.Silence{ - Id: id1, + Id: sil1.Id, Matchers: []*pb.Matcher{{Name: "a", Pattern: "b"}}, StartsAt: newStartsAt, EndsAt: newEndsAt, @@ -598,16 +683,19 @@ func TestSilencesSetFail(t *testing.T) { err string }{ { - s: &pb.Silence{Id: "some_id"}, + s: &pb.Silence{ + Id: "some_id", + Matchers: []*pb.Matcher{{Name: "a", Pattern: "b"}}, + EndsAt: clock.Now().Add(5 * time.Minute), + }, err: ErrNotFound.Error(), }, { s: &pb.Silence{}, // Silence without matcher. - err: "silence invalid", + err: "invalid silence", }, } for _, c := range cases { - _, err := s.Set(c.s) - checkErr(t, c.err, err) + checkErr(t, c.err, s.Set(c.s)) } } @@ -1172,22 +1260,22 @@ func TestSilencer(t *testing.T) { require.False(t, s.Mutes(model.LabelSet{"foo": "bar"}), "expected alert not silenced without any silences") - _, err = ss.Set(&pb.Silence{ + sil1 := &pb.Silence{ Matchers: []*pb.Matcher{{Name: "foo", Pattern: "baz"}}, StartsAt: now.Add(-time.Hour), EndsAt: now.Add(5 * time.Minute), - }) - require.NoError(t, err) + } + require.NoError(t, ss.Set(sil1)) require.False(t, s.Mutes(model.LabelSet{"foo": "bar"}), "expected alert not silenced by non-matching silence") - id, err := ss.Set(&pb.Silence{ + sil2 := &pb.Silence{ Matchers: []*pb.Matcher{{Name: "foo", Pattern: "bar"}}, StartsAt: now.Add(-time.Hour), EndsAt: now.Add(5 * time.Minute), - }) - require.NoError(t, err) - require.NotEmpty(t, id) + } + require.NoError(t, ss.Set(sil2)) + require.NotEmpty(t, sil2.Id) require.True(t, s.Mutes(model.LabelSet{"foo": "bar"}), "expected alert silenced by matching silence") @@ -1198,8 +1286,8 @@ func TestSilencer(t *testing.T) { require.False(t, s.Mutes(model.LabelSet{"foo": "bar"}), "expected alert not silenced by expired silence") // Update silence to start in the future. - _, err = ss.Set(&pb.Silence{ - Id: id, + err = ss.Set(&pb.Silence{ + Id: sil2.Id, Matchers: []*pb.Matcher{{Name: "foo", Pattern: "bar"}}, StartsAt: now.Add(time.Hour), EndsAt: now.Add(3 * time.Hour), @@ -1215,7 +1303,7 @@ func TestSilencer(t *testing.T) { // Exposes issue #2426. require.True(t, s.Mutes(model.LabelSet{"foo": "bar"}), "expected alert silenced by activated silence") - _, err = ss.Set(&pb.Silence{ + err = ss.Set(&pb.Silence{ Matchers: []*pb.Matcher{{Name: "foo", Pattern: "b..", Type: pb.Matcher_REGEXP}}, StartsAt: now.Add(time.Hour), EndsAt: now.Add(3 * time.Hour), @@ -1444,18 +1532,6 @@ func TestValidateSilence(t *testing.T) { }, err: "", }, - { - s: &pb.Silence{ - Id: "", - Matchers: []*pb.Matcher{ - {Name: "a", Pattern: "b"}, - }, - StartsAt: validTimestamp, - EndsAt: validTimestamp, - UpdatedAt: validTimestamp, - }, - err: "ID missing", - }, { s: &pb.Silence{ Id: "some_id", @@ -1528,18 +1604,6 @@ func TestValidateSilence(t *testing.T) { }, err: "invalid zero end timestamp", }, - { - s: &pb.Silence{ - Id: "some_id", - Matchers: []*pb.Matcher{ - {Name: "a", Pattern: "b"}, - }, - StartsAt: validTimestamp, - EndsAt: validTimestamp, - UpdatedAt: zeroTimestamp, - }, - err: "invalid zero update timestamp", - }, } for _, c := range cases { checkErr(t, c.err, validateSilence(c.s)) diff --git a/test/with_api_v2/acceptance/utf8_test.go b/test/with_api_v2/acceptance/utf8_test.go index 6c12b2f762..c0c1dce256 100644 --- a/test/with_api_v2/acceptance/utf8_test.go +++ b/test/with_api_v2/acceptance/utf8_test.go @@ -267,7 +267,7 @@ receivers: _, err := am.Client().Silence.PostSilences(silenceParams) require.Error(t, err) - require.True(t, strings.Contains(err.Error(), "silence invalid: invalid label matcher")) + require.True(t, strings.Contains(err.Error(), "invalid silence: invalid label matcher")) } func TestSendAlertsToUTF8Route(t *testing.T) { diff --git a/types/types.go b/types/types.go index 85391ce91f..727ac320e3 100644 --- a/types/types.go +++ b/types/types.go @@ -22,7 +22,7 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/common/model" - "github.com/prometheus/alertmanager/matchers/compat" + "github.com/prometheus/alertmanager/matcher/compat" "github.com/prometheus/alertmanager/pkg/labels" ) diff --git a/types/types_test.go b/types/types_test.go index 198804862b..c5ca4b58cf 100644 --- a/types/types_test.go +++ b/types/types_test.go @@ -26,7 +26,7 @@ import ( "github.com/stretchr/testify/require" "github.com/prometheus/alertmanager/featurecontrol" - "github.com/prometheus/alertmanager/matchers/compat" + "github.com/prometheus/alertmanager/matcher/compat" ) func TestMemMarker_Muted(t *testing.T) { diff --git a/ui/react-app/package-lock.json b/ui/react-app/package-lock.json index 1585dc53e5..2fbe2aad96 100644 --- a/ui/react-app/package-lock.json +++ b/ui/react-app/package-lock.json @@ -16,7 +16,7 @@ "react": "^18.0.0", "react-dom": "^18.0.0", "react-router-dom": "^6.3.0", - "use-query-params": "^2.1.1" + "use-query-params": "^2.2.1" }, "devDependencies": { "@types/react": "^18.2.51", @@ -34,13 +34,13 @@ "eslint-plugin-react": "^7.30.1", "eslint-plugin-react-hooks": "^4.6.0", "eslint-webpack-plugin": "^4.0.1", - "fork-ts-checker-webpack-plugin": "^7.3.0", + "fork-ts-checker-webpack-plugin": "^9.0.2", "html-webpack-plugin": "^5.5.0", "style-loader": "^3.3.4", "ts-loader": "^9.5.1", "ts-node": "^10.9.1", "typescript": "^4.7.4", - "webpack": "^5.75.0", + "webpack": "^5.76.0", "webpack-bundle-analyzer": "^4.7.0", "webpack-cli": "^4.10.0", "webpack-dev-server": "^4.11.1", @@ -4155,15 +4155,15 @@ } }, "node_modules/fork-ts-checker-webpack-plugin": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-7.3.0.tgz", - "integrity": "sha512-IN+XTzusCjR5VgntYFgxbxVx3WraPRnKehBFrf00cMSrtUuW9MsG9dhL6MWpY6MkjC3wVwoujfCDgZZCQwbswA==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.0.2.tgz", + "integrity": "sha512-Uochze2R8peoN1XqlSi/rGUkDQpRogtLFocP9+PGu68zk1BDAKXfdeCdyVZpgTk8V8WFVQXdEz426VKjXLO1Gg==", "dev": true, "dependencies": { "@babel/code-frame": "^7.16.7", "chalk": "^4.1.2", "chokidar": "^3.5.3", - "cosmiconfig": "^7.0.1", + "cosmiconfig": "^8.2.0", "deepmerge": "^4.2.2", "fs-extra": "^10.0.0", "memfs": "^3.4.1", @@ -4179,11 +4179,31 @@ }, "peerDependencies": { "typescript": ">3.6.0", - "vue-template-compiler": "*", "webpack": "^5.11.0" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" }, "peerDependenciesMeta": { - "vue-template-compiler": { + "typescript": { "optional": true } } @@ -7857,15 +7877,25 @@ } }, "node_modules/use-query-params": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/use-query-params/-/use-query-params-2.2.0.tgz", - "integrity": "sha512-MPBwXVZYzFeJEdjv0YgPNFsafUOM8WTpwBEZfNEMlyzbTsf2c+ZpOBkdM95/w4rxzk4eVO3E4DW7v33+VDbiQw==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/use-query-params/-/use-query-params-2.2.1.tgz", + "integrity": "sha512-i6alcyLB8w9i3ZK3caNftdb+UnbfBRNPDnc89CNQWkGRmDrm/gfydHvMBfVsQJRq3NoHOM2dt/ceBWG2397v1Q==", "dependencies": { "serialize-query-params": "^2.0.2" }, "peerDependencies": { + "@reach/router": "^1.2.1", "react": ">=16.8.0", - "react-dom": ">=16.8.0" + "react-dom": ">=16.8.0", + "react-router-dom": ">=5" + }, + "peerDependenciesMeta": { + "@reach/router": { + "optional": true + }, + "react-router-dom": { + "optional": true + } } }, "node_modules/use-sync-external-store": { @@ -7944,9 +7974,9 @@ } }, "node_modules/webpack": { - "version": "5.75.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.75.0.tgz", - "integrity": "sha512-piaIaoVJlqMsPtX/+3KTTO6jfvrSYgauFVdt8cr9LTHKmcq/AMd4mhzsiP7ZF/PGRNPGA8336jldh9l2Kt2ogQ==", + "version": "5.76.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.0.tgz", + "integrity": "sha512-l5sOdYBDunyf72HW8dF23rFtWq/7Zgvt/9ftMof71E/yUb1YLOBmTgA2K4vQthB3kotMrSj609txVE0dnr2fjA==", "dev": true, "dependencies": { "@types/eslint-scope": "^3.7.3", @@ -11412,15 +11442,15 @@ } }, "fork-ts-checker-webpack-plugin": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-7.3.0.tgz", - "integrity": "sha512-IN+XTzusCjR5VgntYFgxbxVx3WraPRnKehBFrf00cMSrtUuW9MsG9dhL6MWpY6MkjC3wVwoujfCDgZZCQwbswA==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.0.2.tgz", + "integrity": "sha512-Uochze2R8peoN1XqlSi/rGUkDQpRogtLFocP9+PGu68zk1BDAKXfdeCdyVZpgTk8V8WFVQXdEz426VKjXLO1Gg==", "dev": true, "requires": { "@babel/code-frame": "^7.16.7", "chalk": "^4.1.2", "chokidar": "^3.5.3", - "cosmiconfig": "^7.0.1", + "cosmiconfig": "^8.2.0", "deepmerge": "^4.2.2", "fs-extra": "^10.0.0", "memfs": "^3.4.1", @@ -11431,6 +11461,18 @@ "tapable": "^2.2.1" }, "dependencies": { + "cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "requires": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + } + }, "schema-utils": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", @@ -14096,9 +14138,9 @@ } }, "use-query-params": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/use-query-params/-/use-query-params-2.2.0.tgz", - "integrity": "sha512-MPBwXVZYzFeJEdjv0YgPNFsafUOM8WTpwBEZfNEMlyzbTsf2c+ZpOBkdM95/w4rxzk4eVO3E4DW7v33+VDbiQw==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/use-query-params/-/use-query-params-2.2.1.tgz", + "integrity": "sha512-i6alcyLB8w9i3ZK3caNftdb+UnbfBRNPDnc89CNQWkGRmDrm/gfydHvMBfVsQJRq3NoHOM2dt/ceBWG2397v1Q==", "requires": { "serialize-query-params": "^2.0.2" } @@ -14165,9 +14207,9 @@ } }, "webpack": { - "version": "5.75.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.75.0.tgz", - "integrity": "sha512-piaIaoVJlqMsPtX/+3KTTO6jfvrSYgauFVdt8cr9LTHKmcq/AMd4mhzsiP7ZF/PGRNPGA8336jldh9l2Kt2ogQ==", + "version": "5.76.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.0.tgz", + "integrity": "sha512-l5sOdYBDunyf72HW8dF23rFtWq/7Zgvt/9ftMof71E/yUb1YLOBmTgA2K4vQthB3kotMrSj609txVE0dnr2fjA==", "dev": true, "requires": { "@types/eslint-scope": "^3.7.3", diff --git a/ui/react-app/package.json b/ui/react-app/package.json index e1ff99ceab..b049cbb05b 100644 --- a/ui/react-app/package.json +++ b/ui/react-app/package.json @@ -18,7 +18,7 @@ "react": "^18.0.0", "react-dom": "^18.0.0", "react-router-dom": "^6.3.0", - "use-query-params": "^2.1.1" + "use-query-params": "^2.2.1" }, "devDependencies": { "@types/react": "^18.2.51", @@ -36,13 +36,13 @@ "eslint-plugin-react": "^7.30.1", "eslint-plugin-react-hooks": "^4.6.0", "eslint-webpack-plugin": "^4.0.1", - "fork-ts-checker-webpack-plugin": "^7.3.0", + "fork-ts-checker-webpack-plugin": "^9.0.2", "html-webpack-plugin": "^5.5.0", "style-loader": "^3.3.4", "ts-loader": "^9.5.1", "ts-node": "^10.9.1", "typescript": "^4.7.4", - "webpack": "^5.75.0", + "webpack": "^5.76.0", "webpack-bundle-analyzer": "^4.7.0", "webpack-cli": "^4.10.0", "webpack-dev-server": "^4.11.1",