From dbff31dbf110d8013c19829f2610d2b230bf30e5 Mon Sep 17 00:00:00 2001 From: Binh Le Date: Mon, 11 Dec 2017 21:46:59 +0800 Subject: [PATCH] [amtool] - Add new command to update silence (#1123) This adds a new command, update (and also its alias, extend), to update existing silence in Alertmanager. User can use this command to update the expiration or comment on existing silences. The API already support this so I only expose the same functionality to amtool. Don't allow update CreatedBy field as it is "Created" not "Updated", so we should keep the original author. --- cli/silence.go | 1 + cli/silence_update.go | 147 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 148 insertions(+) create mode 100644 cli/silence_update.go diff --git a/cli/silence.go b/cli/silence.go index 494126f3..c0b16e45 100644 --- a/cli/silence.go +++ b/cli/silence.go @@ -32,4 +32,5 @@ func init() { silenceCmd.AddCommand(expireCmd) silenceCmd.AddCommand(queryCmd) silenceCmd.AddCommand(importCmd) + silenceCmd.AddCommand(updateCmd) } diff --git a/cli/silence_update.go b/cli/silence_update.go new file mode 100644 index 00000000..46c765a6 --- /dev/null +++ b/cli/silence_update.go @@ -0,0 +1,147 @@ +package cli + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "path" + "time" + + "github.com/prometheus/alertmanager/cli/format" + "github.com/prometheus/alertmanager/types" + "github.com/spf13/cobra" + flag "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +type getResponse struct { + Status string `json:"status"` + Data types.Silence `json:"data,omitempty"` + ErrorType string `json:"errorType,omitempty"` + Error string `json:"error,omitempty"` +} + +var updateFlags *flag.FlagSet +var updateCmd = &cobra.Command{ + Use: "update ...", + Aliases: []string{"extend"}, + Args: cobra.MinimumNArgs(1), + Short: "Update silences", + Long: `Extend or update existing silence in Alertmanager.`, + Run: CommandWrapper(update), +} + +func init() { + updateCmd.Flags().StringP("expires", "e", "", "Duration of silence (100h)") + updateCmd.Flags().String("expire-on", "", "Expire at a certain time (Overwrites expires) RFC3339 format 2006-01-02T15:04:05Z07:00") + updateCmd.Flags().StringP("comment", "c", "", "A comment to help describe the silence") + updateFlags = updateCmd.Flags() +} + +func update(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return fmt.Errorf("no silence IDs specified") + } + + alertmanagerUrl, err := GetAlertmanagerURL() + if err != nil { + return err + } + + var updatedSilences []types.Silence + for _, silenceId := range args { + silence, err := getSilenceById(silenceId, *alertmanagerUrl) + if err != nil { + return err + } + silence, err = updateSilence(silence) + if err != nil { + return err + } + updatedSilences = append(updatedSilences, *silence) + } + + quiet := viper.GetBool("quiet") + if quiet { + for _, silence := range updatedSilences { + fmt.Println(silence.ID) + } + } else { + formatter, found := format.Formatters[viper.GetString("output")] + if !found { + return fmt.Errorf("unknown output formatter") + } + formatter.FormatSilences(updatedSilences) + } + return nil +} + +// This takes an url.URL and not a pointer as we will modify it for our API call. +func getSilenceById(silenceId string, baseUrl url.URL) (*types.Silence, error) { + baseUrl.Path = path.Join(baseUrl.Path, "/api/v1/silence", silenceId) + res, err := http.Get(baseUrl.String()) + if err != nil { + return nil, err + } + + defer res.Body.Close() + body, err := ioutil.ReadAll(res.Body) + if err != nil { + return nil, fmt.Errorf("couldn't read response body: %v", err) + } + + if res.StatusCode == 404 { + return nil, fmt.Errorf("no silence found with id: %v", silenceId) + } + if res.StatusCode != 200 { + return nil, fmt.Errorf("received %d response from Alertmanager: %v", res.StatusCode, body) + } + + var response getResponse + err = json.Unmarshal(body, &response) + return &response.Data, nil +} + +func updateSilence(silence *types.Silence) (*types.Silence, error) { + if updateFlags.Changed("expires") { + expires, err := updateFlags.GetString("expires") + if err != nil { + return nil, err + } + duration, err := time.ParseDuration(expires) + if err != nil { + return nil, err + } + silence.EndsAt = time.Now().UTC().Add(duration) + } + + // expire-on will override expires value if both are specified + if updateFlags.Changed("expire-on") { + expireOn, err := updateFlags.GetString("expire-on") + if err != nil { + return nil, err + } + endsAt, err := time.Parse(time.RFC3339, expireOn) + if err != nil { + return nil, err + } + silence.EndsAt = endsAt + } + + if updateFlags.Changed("comment") { + comment, err := updateFlags.GetString("comment") + if err != nil { + return nil, err + } + silence.Comment = comment + } + + // addSilence can also be used to update an existing silence + _, err := addSilence(silence) + if err != nil { + return nil, err + } + return silence, nil +}