diff --git a/config/config.go b/config/config.go index 05cd59ce7f..dfb3ce832b 100644 --- a/config/config.go +++ b/config/config.go @@ -298,6 +298,9 @@ func resolveFilepaths(baseDir string, cfg *Config) { for _, cfg := range receiver.RocketchatConfigs { cfg.HTTPConfig.SetDirectory(baseDir) } + for _, cfg := range receiver.ExecConfigs { + cfg.SetDirectory(baseDir) + } } } @@ -412,6 +415,14 @@ func (c *Config) UnmarshalYAML(unmarshal func(any) error) error { wh.HTTPConfig = c.Global.HTTPConfig } } + for _, ec := range rcv.ExecConfigs { + if ec.ExecFile == "" { + if c.Global.ExecFile == "" { + return errors.New("no global executable file set") + } + ec.ExecFile = c.Global.ExecFile + } + } for _, ec := range rcv.EmailConfigs { if ec.TLSConfig == nil { ec.TLSConfig = c.Global.SMTPTLSConfig @@ -867,6 +878,7 @@ type GlobalConfig struct { RocketchatTokenFile string `yaml:"rocketchat_token_file,omitempty" json:"rocketchat_token_file,omitempty"` RocketchatTokenID *Secret `yaml:"rocketchat_token_id,omitempty" json:"rocketchat_token_id,omitempty"` RocketchatTokenIDFile string `yaml:"rocketchat_token_id_file,omitempty" json:"rocketchat_token_id_file,omitempty"` + ExecFile string `yaml:"exec_file,omitempty" json:"exec_file,omitempty"` } // UnmarshalYAML implements the yaml.Unmarshaler interface for GlobalConfig. @@ -1010,6 +1022,7 @@ type Receiver struct { PagerdutyConfigs []*PagerdutyConfig `yaml:"pagerduty_configs,omitempty" json:"pagerduty_configs,omitempty"` SlackConfigs []*SlackConfig `yaml:"slack_configs,omitempty" json:"slack_configs,omitempty"` WebhookConfigs []*WebhookConfig `yaml:"webhook_configs,omitempty" json:"webhook_configs,omitempty"` + ExecConfigs []*ExecConfig `yaml:"exec_configs,omitempty" json:"exec_configs,omitempty"` OpsGenieConfigs []*OpsGenieConfig `yaml:"opsgenie_configs,omitempty" json:"opsgenie_configs,omitempty"` WechatConfigs []*WechatConfig `yaml:"wechat_configs,omitempty" json:"wechat_configs,omitempty"` PushoverConfigs []*PushoverConfig `yaml:"pushover_configs,omitempty" json:"pushover_configs,omitempty"` diff --git a/config/notifiers.go b/config/notifiers.go index 8d9763b979..f2487697df 100644 --- a/config/notifiers.go +++ b/config/notifiers.go @@ -35,6 +35,13 @@ var ( }, } + // DefaultExecConfig defines default values for Exec configurations. + DefaultExecConfig = ExecConfig{ + NotifierConfig: NotifierConfig{ + VSendResolved: true, + }, + } + // DefaultWebexConfig defines default values for Webex configurations. DefaultWebexConfig = WebexConfig{ NotifierConfig: NotifierConfig{ @@ -557,6 +564,51 @@ func (c *WebhookConfig) UnmarshalYAML(unmarshal func(any) error) error { return nil } +// ExecConfig configures notifications via a generic exec. +type ExecConfig struct { + NotifierConfig `yaml:",inline" json:",inline"` + + // WorkingDir specifies the working directory of the command. + // If Dir is the empty string, the command is run in + // the current directory of Alertmanager. + WorkingDir string `yaml:"working_dir" json:"working_dir"` + // ExecFile is the path of the command to run. Relative values + // will be resolved against WorkingDir prior to execution. + ExecFile string `yaml:"exec_file" json:"exec_file"` + // Arguments is the list of additional command line arguments + // to pass to the executable. + Arguments []string `yaml:"arguments,omitempty" json:"arguments,omitempty"` + // Environment specifies the environment of the process. + Environment map[string]string `yaml:"environment,omitempty" json:"environment,omitempty"` + // Environment specifies environment variables with values read from files. + EnvironmentFiles map[string]string `yaml:"environment_files,omitempty" json:"environment_files,omitempty"` + + // Timeout is the maximum time allowed to invoke the command. Setting this to 0 + // does not impose a timeout. + Timeout time.Duration `yaml:"timeout" json:"timeout"` +} + +// SetDirectory uses [config.JoinDir] to resolver internal +// path properties to absolute values. +func (c *ExecConfig) SetDirectory(s string) { + c.WorkingDir = commoncfg.JoinDir(s, c.WorkingDir) + c.ExecFile = commoncfg.JoinDir(s, c.ExecFile) + + for k, f := range c.EnvironmentFiles { + c.EnvironmentFiles[k] = commoncfg.JoinDir(s, f) + } +} + +// UnmarshalYAML implements the yaml.Unmarshaler interface. +func (c *ExecConfig) UnmarshalYAML(unmarshal func(any) error) error { + *c = DefaultExecConfig + type plain ExecConfig + if err := unmarshal((*plain)(c)); err != nil { + return err + } + return nil +} + // WechatConfig configures notifications via Wechat. type WechatConfig struct { NotifierConfig `yaml:",inline" json:",inline"` diff --git a/config/receiver/receiver.go b/config/receiver/receiver.go index d92a19a4c5..395c23ca50 100644 --- a/config/receiver/receiver.go +++ b/config/receiver/receiver.go @@ -23,6 +23,7 @@ import ( "github.com/prometheus/alertmanager/notify" "github.com/prometheus/alertmanager/notify/discord" "github.com/prometheus/alertmanager/notify/email" + "github.com/prometheus/alertmanager/notify/exec" "github.com/prometheus/alertmanager/notify/jira" "github.com/prometheus/alertmanager/notify/msteams" "github.com/prometheus/alertmanager/notify/msteamsv2" @@ -64,6 +65,9 @@ func BuildReceiverIntegrations(nc config.Receiver, tmpl *template.Template, logg for i, c := range nc.WebhookConfigs { add("webhook", i, c, func(l *slog.Logger) (notify.Notifier, error) { return webhook.New(c, tmpl, l, httpOpts...) }) } + for i, c := range nc.ExecConfigs { + add("exec", i, c, func(l *slog.Logger) (notify.Notifier, error) { return exec.New(c, tmpl, l) }) + } for i, c := range nc.EmailConfigs { add("email", i, c, func(l *slog.Logger) (notify.Notifier, error) { return email.New(c, tmpl, l), nil }) } diff --git a/config/testdata/conf.good.yml b/config/testdata/conf.good.yml index 2429f7961c..11acf6ebdb 100644 --- a/config/testdata/conf.good.yml +++ b/config/testdata/conf.good.yml @@ -115,3 +115,11 @@ receivers: slack_configs: - channel: '#my-channel' image_url: 'http://some.img.com/img.png' + - name: exec-receiver + exec_configs: + - exec_file: /bin/cat + working_dir: /tmp + arguments: [ "-A" ] + environment: + LANG: C + timeout: 5s diff --git a/docs/configuration.md b/docs/configuration.md index 26c43954df..571858f281 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -736,6 +736,8 @@ discord_configs: [ - , ... ] email_configs: [ - , ... ] +exec_configs: + [ - , ... ] msteams_configs: [ - , ... ] msteamsv2_configs: @@ -986,6 +988,51 @@ tls_config: [ headers: { : , ... } ] ``` +### `` + +The exec receiver allows configuring a generic receiver +using executables for processing. + +```yaml +# Whether to notify about resolved alerts. +[ send_resolved: | default = true ] + +# Directory to change into before running the executable. +[ working_dir: ] + +# Script/Binary to invoke for data processing. +exec_file: + +# Commandline arguments to pass to the executable. +arguments: + [ - ... ] + +# Environment variables to expose to the process in addition to +# the variables the Alertmanager process already inherited. +environment: + [ : ... ] + +# Environment variables to expose to the process in addition to +# the variables the Alertmanager process already inherited. +# The actual values are read from the specified files. +environment_files: + [ : ... ] + +# The maximum time to wait for the call to complete, before interrupting the +# process and allowing it to be retried. The default value of 0s indicates that +# no timeout should be applied. +# NOTE: This will have no effect if set higher than the group_interval. +[ timeout: | default = 0s ] +``` + +The executable will receive data via STDIN formatted in a similar fashion as the +[webhook](#webhook_config`. The exit code is used to determine the result of the +processing: + +* Success (0) +* Error but retryable (3) +* Unrecoverable error (all other status codes) + ### `` Microsoft Teams notifications are sent via the [Incoming Webhooks](https://learn.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/what-are-webhooks-and-connectors) API endpoint. diff --git a/examples/exec/main.go b/examples/exec/main.go new file mode 100644 index 0000000000..330be5db51 --- /dev/null +++ b/examples/exec/main.go @@ -0,0 +1,34 @@ +// Copyright 2018 Prometheus Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "encoding/json" + "log" + "os" + + "github.com/prometheus/alertmanager/notify/exec" +) + +func main() { + var msg exec.Message + err := json.NewDecoder(os.Stdin).Decode(&msg) + if err != nil { + log.Fatal(err) + } + + log.Printf("version=%q, group_key=%s\n", msg.Version, msg.GroupKey) + + os.Exit(exec.ExitSuccess) +} diff --git a/notify/exec/exec.go b/notify/exec/exec.go new file mode 100644 index 0000000000..65d0cc936b --- /dev/null +++ b/notify/exec/exec.go @@ -0,0 +1,187 @@ +// Copyright 2019 Prometheus Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package exec + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "log/slog" + "os" + "os/exec" + "syscall" + + "github.com/prometheus/alertmanager/config" + "github.com/prometheus/alertmanager/notify" + "github.com/prometheus/alertmanager/template" + "github.com/prometheus/alertmanager/types" +) + +const ( + ExitSuccess = 0 + ExitFailure = 1 + ExitRetry = 3 +) + +const messageVersion = "1" + +// Message defines the JSON object send to executables. +type Message struct { + *template.Data + + // The protocol version. + Version string `json:"version"` + GroupKey string `json:"groupKey"` +} + +// Notifier implements a Notifier for Exec notifications. +type Notifier struct { + conf *config.ExecConfig + tmpl *template.Template + logger *slog.Logger + env []string +} + +// New returns a new Exec notifier. +func New(c *config.ExecConfig, t *template.Template, l *slog.Logger) (*Notifier, error) { + env := make([]string, len(c.Environment)) + for k, v := range c.Environment { + env = append(env, k+"="+v) + } + + for k, f := range c.EnvironmentFiles { + v, err := os.ReadFile(f) + if err != nil { + return nil, err + } + + v = bytes.TrimSpace(v) + env = append(env, k+"="+string(v)) + } + + return &Notifier{conf: c, tmpl: t, logger: l, env: env}, nil +} + +// Notify implements the Notifier interface. +func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) { + var ( + err error + cmd *exec.Cmd + cmdCtx context.Context = ctx + ) + + if n.conf.Timeout > 0 { + var cancel func() + cmdCtx, cancel = context.WithTimeout(ctx, n.conf.Timeout) + + defer cancel() + } + + cmd = exec.CommandContext(cmdCtx, n.conf.ExecFile, n.conf.Arguments...) + cmd.Dir = n.conf.WorkingDir + cmd.Env = append(os.Environ(), n.env...) + + var ( + stdin io.WriteCloser + stdout io.ReadCloser + stderr io.ReadCloser + ) + + stdin, err = cmd.StdinPipe() + if err != nil { + return false, fmt.Errorf("failed to open STDIN for executable: %w", err) + } + + stdout, err = cmd.StdoutPipe() + if err != nil { + return false, fmt.Errorf("failed to open STDOUT for executable: %w", err) + } + + stderr, err = cmd.StderrPipe() + if err != nil { + return false, fmt.Errorf("failed to open STDERR for executable: %w", err) + } + + n.logger.Debug("invoking executable notifier", "exec", n.conf.ExecFile) + err = cmd.Start() + if err != nil { + return false, fmt.Errorf("unable to start executable: %w", err) + } + + err = n.dispatchExecPayload(ctx, stdin, as...) + if err != nil { + n.logger.Error("failed to open STDERR for executable", "fd", "stdin", "exec", n.conf.ExecFile, "err", err) + } + + // explicitly close the input in case the executable + // is waiting indefinetly for data + err = stdin.Close() + if err != nil { + n.logger.Warn("unable to close STDIN for executable", "fd", "stdin", "exec", n.conf.ExecFile, "err", err) + } + + err = n.logOutput(ctx, slog.LevelDebug, "stdout", stdout) + if err != nil { + n.logger.Warn("unable to read STDOUT from executable", "fd", "stdout", "exec", n.conf.ExecFile, "err", err) + } + + err = n.logOutput(ctx, slog.LevelWarn, "stderr", stderr) + if err != nil { + n.logger.Warn("unable to read STDERR from executable", "fd", "stderr", "exec", n.conf.ExecFile, "err", err) + } + + var status int + err = cmd.Wait() + if err == nil { + status = ExitSuccess + } else if ee, ok := err.(*exec.ExitError); ok { + status = ee.Sys().(syscall.WaitStatus).ExitStatus() + } else { + status = ExitFailure + } + + return status == ExitRetry, err +} + +func (n *Notifier) logOutput(ctx context.Context, level slog.Level, fd string, r io.Reader) error { + if !n.logger.Enabled(ctx, level) { + return nil + } + + message, err := io.ReadAll(r) + n.logger.Log(ctx, level, string(message), "fd", fd, "exec", n.conf.ExecFile) + + return err +} + +// Write the given alerts to the executable input. +func (n *Notifier) dispatchExecPayload(ctx context.Context, w io.Writer, as ...*types.Alert) error { + groupKey, err := notify.ExtractGroupKey(ctx) + if err != nil { + return err + } + + n.logger.Debug("extracted group key", "key", groupKey) + + data := notify.GetTemplateData(ctx, n.tmpl, as, n.logger) + msg := &Message{ + Version: messageVersion, + Data: data, + GroupKey: groupKey.String(), + } + + return json.NewEncoder(w).Encode(msg) +} diff --git a/notify/exec/exec_benchmark_test.go b/notify/exec/exec_benchmark_test.go new file mode 100644 index 0000000000..33c5242c89 --- /dev/null +++ b/notify/exec/exec_benchmark_test.go @@ -0,0 +1,40 @@ +// Copyright 2019 Prometheus Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package exec + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/prometheus/alertmanager/config" + "github.com/prometheus/alertmanager/notify" +) + +func BenchmarkExecEmpty(b *testing.B) { + conf := &config.ExecConfig{ + ExecFile: "/bin/true", + } + notifier := newSubject(b, conf) + + ctx := context.Background() + ctx = notify.WithGroupKey(ctx, "1") + + for b.Loop() { + retry, err := notifier.Notify(ctx) + require.NoError(b, err) + require.Equal(b, false, retry) + } +} diff --git a/notify/exec/exec_test.go b/notify/exec/exec_test.go new file mode 100644 index 0000000000..d0a1cdcca5 --- /dev/null +++ b/notify/exec/exec_test.go @@ -0,0 +1,191 @@ +// Copyright 2019 Prometheus Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package exec + +import ( + "context" + "flag" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/prometheus/common/model" + "github.com/prometheus/common/promslog" + "github.com/stretchr/testify/require" + + "github.com/prometheus/alertmanager/config" + "github.com/prometheus/alertmanager/notify" + "github.com/prometheus/alertmanager/notify/test" + "github.com/prometheus/alertmanager/types" +) + +const updateFlag = "update" + +func init() { + if f := flag.Lookup(updateFlag); f != nil { + getter, ok := f.Value.(flag.Getter) + if !ok { + panic("existing -update flag is not a Getter") + } + + _, ok = getter.Get().(bool) + if !ok { + panic("existing -update flag does not provide boolean values") + } + } else { + flag.Bool(updateFlag, false, "update test fixtures") + } +} + +func updateFixtures() bool { + return flag.Lookup(updateFlag).Value.(flag.Getter).Get().(bool) +} + +func newConfig(t testing.TB, fixtureAssertion string, argv ...string) *config.ExecConfig { + var ( + arguments []string + filePath string + execFile string + err error + ) + + if fixtureAssertion != "" { + filePath = filepath.Join("testdata", fixtureAssertion) + filePath, err = filepath.Abs(filePath) + require.NoError(t, err) + + if updateFixtures() && !strings.HasPrefix(fixtureAssertion, "bad_") { + arguments = append(arguments, "-f", filePath) + } + } else { + filePath = filepath.Join("testdata", "404.json") + filePath, err = filepath.Abs(filePath) + require.NoError(t, err) + } + + arguments = append(arguments, argv...) + arguments = append(arguments, filePath) + execFile = filepath.Join("testdata", "test-exec.sh") + execFile, err = filepath.Abs(execFile) + require.NoError(t, err) + + return &config.ExecConfig{ + ExecFile: execFile, + Arguments: arguments, + } +} + +func newSubject(t testing.TB, conf *config.ExecConfig) *Notifier { + logger := promslog.NewNopLogger() + tmpl := test.CreateTmpl(t) + notifier, err := New(conf, tmpl, logger) + require.NoError(t, err) + + return notifier +} + +func TestExecSuccess(t *testing.T) { + conf := newConfig(t, "success.json") + notifier := newSubject(t, conf) + + ctx := context.Background() + ctx = notify.WithGroupKey(ctx, "1") + + alert := &types.Alert{ + Alert: model.Alert{ + Labels: model.LabelSet{ + "Message": "message", + }, + StartsAt: time.Unix(0, 0), + }, + } + + retry, err := notifier.Notify(ctx, alert) + require.NoError(t, err) + require.Equal(t, false, retry) +} + +func TestExecRetry(t *testing.T) { + conf := newConfig(t, "") + notifier := newSubject(t, conf) + + ctx := context.Background() + ctx = notify.WithGroupKey(ctx, "1") + + alert := &types.Alert{} + + retry, err := notifier.Notify(ctx, alert) + require.Error(t, err) + require.Equal(t, true, retry) +} + +func TestExecError(t *testing.T) { + conf := newConfig(t, "bad_error.json") + notifier := newSubject(t, conf) + + ctx := context.Background() + ctx = notify.WithGroupKey(ctx, "1") + + retry, err := notifier.Notify(ctx) + require.Error(t, err) + require.Equal(t, false, retry) +} + +func TestExecTimeout(t *testing.T) { + if testing.Short() { + t.Skip("not testing timeouts in short mode") + } + + conf := newConfig(t, "timeout.json", "-s", "5") + conf.Timeout = 2 * time.Second + notifier := newSubject(t, conf) + + ctx := context.Background() + ctx = notify.WithGroupKey(ctx, "1") + + retry, err := notifier.Notify(ctx) + require.Error(t, err) + require.Equal(t, false, retry) +} + +func TestExecScope(t *testing.T) { + conf := newConfig(t, "scope.json", + "-e", "FOO=BAR", + "-e", "GAFF=BAC", + "-e", "SECRET=DEADBEEFCAFE", + "-d", "./testdata", + ) + conf.Environment = map[string]string{ + "FOO": "BAR", + "GAFF": "BAC", + } + conf.EnvironmentFiles = map[string]string{ + "SECRET": "testdata/secret.env", + } + conf.WorkingDir = "./testdata" + conf.ExecFile = filepath.Join("testdata", "assert-scope.sh") + notifier := newSubject(t, conf) + + rootDir, err := filepath.Abs(".") + require.NoError(t, err) + conf.SetDirectory(rootDir) + + ctx := context.Background() + ctx = notify.WithGroupKey(ctx, "1") + + retry, err := notifier.Notify(ctx) + require.NoError(t, err) + require.Equal(t, false, retry) +} diff --git a/notify/exec/testdata/assert-scope.sh b/notify/exec/testdata/assert-scope.sh new file mode 100755 index 0000000000..2353edf093 --- /dev/null +++ b/notify/exec/testdata/assert-scope.sh @@ -0,0 +1,43 @@ +#!/bin/sh -eu + +TESTDATA_DIR=$(dirname "$0") +PROJECT_ROOT=$(dirname "$TESTDATA_DIR") +DEBUG_ENV=/dev/null +DEBUG_CWD=/dev/null + +resolve_dir() ( + cd "$PROJECT_ROOT" + cd "$1" + + pwd +) + +assert_env() { + env | tee "$DEBUG_ENV" | grep -q "^$1\$" || { + printf "environment mismatch: want='%s'\n" "$1" >&2 + exit 1 + } +} + +assert_cwd() { + WANT=$(pwd) + GOT=$(resolve_dir "$1") + + if test "$GOT" != "$WANT"; then + printf "current directory mismatch: got='%s'; want='%s'\n" "$GOT" "$WANT" | tee "$DEBUG_CWD" >&2 + exit 1 + fi +} + +while getopts e:d:E:D: opt; do + case "$opt" in + e) assert_env "$OPTARG" ;; + d) assert_cwd "$OPTARG" ;; + E) DEBUG_ENV="$OPTARG" ;; + D) DEBUG_CWD="$OPTARG" ;; + ?) echo "unsupported argument '$opt'" >&2; exit 2 ;; + esac +done +shift $(($OPTIND - 1)) + +exit 0 diff --git a/notify/exec/testdata/bad_error.json b/notify/exec/testdata/bad_error.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/notify/exec/testdata/bad_error.json @@ -0,0 +1 @@ +{} diff --git a/notify/exec/testdata/scope.json b/notify/exec/testdata/scope.json new file mode 100644 index 0000000000..e89278f63f --- /dev/null +++ b/notify/exec/testdata/scope.json @@ -0,0 +1 @@ +{"receiver":"","status":"resolved","alerts":[],"groupLabels":{},"commonLabels":{},"commonAnnotations":{},"externalURL":"http://am","version":"1","groupKey":"1"} diff --git a/notify/exec/testdata/secret.env b/notify/exec/testdata/secret.env new file mode 100644 index 0000000000..9bd5b265dc --- /dev/null +++ b/notify/exec/testdata/secret.env @@ -0,0 +1 @@ +DEADBEEFCAFE diff --git a/notify/exec/testdata/success.json b/notify/exec/testdata/success.json new file mode 100644 index 0000000000..7c646d0500 --- /dev/null +++ b/notify/exec/testdata/success.json @@ -0,0 +1 @@ +{"receiver":"","status":"firing","alerts":[{"status":"firing","labels":{"Message":"message"},"annotations":{},"startsAt":"1970-01-01T00:00:00Z","endsAt":"0001-01-01T00:00:00Z","generatorURL":"","fingerprint":"4ea129fd903f0e45"}],"groupLabels":{},"commonLabels":{"Message":"message"},"commonAnnotations":{},"externalURL":"http://am","version":"1","groupKey":"1"} diff --git a/notify/exec/testdata/test-exec.sh b/notify/exec/testdata/test-exec.sh new file mode 100755 index 0000000000..f39218cbfc --- /dev/null +++ b/notify/exec/testdata/test-exec.sh @@ -0,0 +1,51 @@ +#!/bin/sh -eu + +# file to write incoming data to for debugging +DEBUG_PATH="/dev/null" +# file to compare the input against +ASSERTION_PATH="/dev/null" +SLEEP_SECONDS=0 + +while getopts s:f: opt; do + case "$opt" in + f) DEBUG_PATH="$OPTARG" ;; + s) SLEEP_SECONDS="$OPTARG" ;; + ?) echo "unsupported argument '$opt'" >&2; exit 2 ;; + esac +done +shift $(($OPTIND - 1)) + +if test $# -gt 0; then + ASSERTION_PATH="$1" +fi + +if ! test -t 0; then + STDIN_CSUM=$(cat | tee "$DEBUG_PATH" | md5sum 2>/dev/null | cut -d' ' -f1) +else + echo "running notifier interactively is not supported" >&2 + exit 1 +fi + +FILE_CSUM=$(md5sum "$ASSERTION_PATH" 2>/dev/null | cut -d' ' -f1) + +if test -z "$STDIN_CSUM"; then + echo "unable to calculate input checksum" >&2 + exit 3 +fi + +if test -z "$FILE_CSUM"; then + echo "unable to calculate expected checksum" >&2 + exit 3 +fi + +if test "$SLEEP_SECONDS" -gt 0; then + sleep "$SLEEP_SECONDS" +fi + +if test "$STDIN_CSUM" != "$FILE_CSUM"; then + printf "fixture checksum mismatch: got='%s'; want='%s'\n" "$STDIN_CSUM" "$FILE_CSUM" >&2 + exit 1 +fi + +printf "checksum matches: got='%s'\n" "$FILE_CSUM" +exit 0 diff --git a/notify/exec/testdata/timeout.json b/notify/exec/testdata/timeout.json new file mode 100644 index 0000000000..e89278f63f --- /dev/null +++ b/notify/exec/testdata/timeout.json @@ -0,0 +1 @@ +{"receiver":"","status":"resolved","alerts":[],"groupLabels":{},"commonLabels":{},"commonAnnotations":{},"externalURL":"http://am","version":"1","groupKey":"1"} diff --git a/notify/test/test.go b/notify/test/test.go index 75729b2a4f..df4acc8fc7 100644 --- a/notify/test/test.go +++ b/notify/test/test.go @@ -127,7 +127,7 @@ func DefaultRetryCodes() []int { } // CreateTmpl returns a ready-to-use template. -func CreateTmpl(t *testing.T) *template.Template { +func CreateTmpl(t testing.TB) *template.Template { tmpl, err := template.FromGlobs([]string{}) require.NoError(t, err) tmpl.ExternalURL, _ = url.Parse("http://am")