Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 24 additions & 19 deletions cli/alert.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,21 @@ import (
"github.com/prometheus/alertmanager/pkg/parse"
)

var (
alertCmd = app.Command("alert", "View and search through current alerts")
alertQueryCmd = alertCmd.Command("query", "View and search through current alerts").Default()
expired = alertQueryCmd.Flag("expired", "Show expired alerts as well as active").Bool()
silenced = alertQueryCmd.Flag("silenced", "Show silenced alerts").Short('s').Bool()
alertQuery = alertQueryCmd.Arg("matcher-groups", "Query filter").Strings()
)
type alertQueryCmd struct {
expired, silenced bool
matcherGroups []string
}

func init() {
alertQueryCmd.Action(queryAlerts)
func configureAlertCmd(app *kingpin.Application, longHelpText map[string]string) {
var (
a = &alertQueryCmd{}
alertCmd = app.Command("alert", "View and search through current alerts")
queryCmd = alertCmd.Command("query", "View and search through current alerts").Default()
)
queryCmd.Flag("expired", "Show expired alerts as well as active").BoolVar(&a.expired)
queryCmd.Flag("silenced", "Show silenced alerts").Short('s').BoolVar(&a.silenced)
queryCmd.Arg("matcher-groups", "Query filter").StringsVar(&a.matcherGroups)
queryCmd.Action(a.queryAlerts)
longHelpText["alert"] = `View and search through current alerts.

Amtool has a simplified prometheus query syntax, but contains robust support for
Expand All @@ -49,33 +54,33 @@ amtool alert query 'alertname=~foo.*'
longHelpText["alert query"] = longHelpText["alert"]
}

func queryAlerts(element *kingpin.ParseElement, ctx *kingpin.ParseContext) error {
func (a *alertQueryCmd) queryAlerts(element *kingpin.ParseElement, ctx *kingpin.ParseContext) error {
var filterString = ""
if len(*alertQuery) == 1 {
if len(a.matcherGroups) == 1 {
// If the parser fails then we likely don't have a (=|=~|!=|!~) so lets
// assume that the user wants alertname=<arg> and prepend `alertname=`
// to the front.
_, err := parse.Matcher((*alertQuery)[0])
_, err := parse.Matcher(a.matcherGroups[0])
if err != nil {
filterString = fmt.Sprintf("{alertname=%s}", (*alertQuery)[0])
filterString = fmt.Sprintf("{alertname=%s}", a.matcherGroups[0])
} else {
filterString = fmt.Sprintf("{%s}", strings.Join(*alertQuery, ","))
filterString = fmt.Sprintf("{%s}", strings.Join(a.matcherGroups, ","))
}
} else if len(*alertQuery) > 1 {
filterString = fmt.Sprintf("{%s}", strings.Join(*alertQuery, ","))
} else if len(a.matcherGroups) > 1 {
filterString = fmt.Sprintf("{%s}", strings.Join(a.matcherGroups, ","))
}

c, err := api.NewClient(api.Config{Address: (*alertmanagerUrl).String()})
c, err := api.NewClient(api.Config{Address: alertmanagerURL.String()})
if err != nil {
return err
}
alertAPI := client.NewAlertAPI(c)
fetchedAlerts, err := alertAPI.List(context.Background(), filterString, *expired, *silenced)
fetchedAlerts, err := alertAPI.List(context.Background(), filterString, a.expired, a.silenced)
if err != nil {
return err
}

formatter, found := format.Formatters[*output]
formatter, found := format.Formatters[output]
if !found {
return errors.New("unknown output formatter")
}
Expand Down
23 changes: 14 additions & 9 deletions cli/check_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,28 @@ import (
"github.com/prometheus/alertmanager/template"
)

// alertCmd represents the alert command
var (
checkConfigCmd = app.Command("check-config", "Validate alertmanager config files")
checkFiles = checkConfigCmd.Arg("check-files", "Files to be validated").ExistingFiles()
)
// TODO: This can just be a type that is []string, doesn't have to be a struct
type checkConfigCmd struct {
files []string
}

func configureCheckConfigCmd(app *kingpin.Application, longHelpText map[string]string) {
var (
c = &checkConfigCmd{}
checkCmd = app.Command("check-config", "Validate alertmanager config files")
)
checkCmd.Arg("check-files", "Files to be validated").ExistingFilesVar(&c.files)

func init() {
checkConfigCmd.Action(checkConfig)
checkCmd.Action(c.checkConfig)
longHelpText["check-config"] = `Validate alertmanager config files

Will validate the syntax and schema for alertmanager config file
and associated templates. Non existing templates will not trigger
errors`
}

func checkConfig(element *kingpin.ParseElement, ctx *kingpin.ParseContext) error {
return CheckConfig(*checkFiles)
func (c *checkConfigCmd) checkConfig(element *kingpin.ParseElement, ctx *kingpin.ParseContext) error {
return CheckConfig(c.files)
}

func CheckConfig(args []string) error {
Expand Down
8 changes: 4 additions & 4 deletions cli/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import (
)

// configCmd represents the config command
var configCmd = app.Command("config", "View the running config").Action(queryConfig)
func configureConfigCmd(app *kingpin.Application, longHelpText map[string]string) {
app.Command("config", "View the running config").Action(queryConfig)

func init() {
longHelpText["config"] = `View current config
The amount of output is controlled by the output selection flag:
- Simple: Print just the running config
Expand All @@ -23,7 +23,7 @@ The amount of output is controlled by the output selection flag:
}

func queryConfig(element *kingpin.ParseElement, ctx *kingpin.ParseContext) error {
c, err := api.NewClient(api.Config{Address: (*alertmanagerUrl).String()})
c, err := api.NewClient(api.Config{Address: alertmanagerURL.String()})
if err != nil {
return err
}
Expand All @@ -33,7 +33,7 @@ func queryConfig(element *kingpin.ParseElement, ctx *kingpin.ParseContext) error
return err
}

formatter, found := format.Formatters[*output]
formatter, found := format.Formatters[output]
if !found {
return errors.New("unknown output formatter")
}
Expand Down
57 changes: 57 additions & 0 deletions cli/config_resolver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package cli
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've already noticed that most of the files in the cli package are missing the Apache 2 license header. This shouldn't be addressed in this PR but rather tackled once prometheus/prometheus#3904 is fixed.


import (
"io/ioutil"
"os"

"github.com/alecthomas/kingpin"
"gopkg.in/yaml.v2"
)

type configResolver []map[string]string

func newConfigResolver() (configResolver, error) {
files := []string{
os.ExpandEnv("$HOME/.config/amtool/config.yml"),
"/etc/amtool/config.yml",
}

resolver := configResolver{}
for _, f := range files {
b, err := ioutil.ReadFile(f)
if err != nil {
if os.IsNotExist(err) {
continue
}
return nil, err
}

var m map[string]string
err = yaml.Unmarshal(b, &m)
if err != nil {
return nil, err
}
resolver = append(resolver, m)
}

return resolver, nil
}

func (r configResolver) Resolve(key string, context *kingpin.ParseContext) ([]string, error) {
for _, c := range r {
if v, ok := c[key]; ok {
return []string{v}, nil
}
}
return nil, nil
}

// This function maps things which have previously had different names in the
// config file to their new names, so old configurations keep working
func backwardsCompatibilityResolver(key string) string {
switch key {
case "require-comment":
return "comment_required"
}
return key
}
132 changes: 44 additions & 88 deletions cli/root.go
Original file line number Diff line number Diff line change
@@ -1,79 +1,67 @@
package cli

import (
"io/ioutil"
"net/url"
"os"

"github.com/alecthomas/kingpin"
"github.com/prometheus/alertmanager/cli/format"
"github.com/prometheus/common/version"
"gopkg.in/yaml.v2"
)

var (
app = kingpin.New("amtool", "Alertmanager CLI").DefaultEnvars()
verbose = app.Flag("verbose", "Verbose running information").Short('v').Bool()
alertmanagerUrl = app.Flag("alertmanager.url", "Alertmanager to talk to").Required().URL()
output = app.Flag("output", "Output formatter (simple, extended, json)").Short('o').Default("simple").Enum("simple", "extended", "json")

// This contains a mapping from command path to the long-format help string
// Separate subcommands with spaces, eg longHelpText["silence query"]
longHelpText = make(map[string]string)
verbose bool
alertmanagerURL *url.URL
output string
)

type amtoolConfigResolver struct {
configData []map[string]string
}
func Execute() {
var (
longHelpText = map[string]string{}
app = kingpin.New("amtool", "Alertmanager CLI").DefaultEnvars()
)

func newConfigResolver() (amtoolConfigResolver, error) {
files := []string{
os.ExpandEnv("$HOME/.config/amtool/config.yml"),
"/etc/amtool/config.yml",
}
longHelpText["root"] = longHelpTextRoot

resolver := amtoolConfigResolver{
configData: make([]map[string]string, 0),
}
for _, f := range files {
b, err := ioutil.ReadFile(f)
if err != nil {
if os.IsNotExist(err) {
continue
}
return resolver, err
}
var config map[string]string
err = yaml.Unmarshal(b, &config)
if err != nil {
return resolver, err
}
resolver.configData = append(resolver.configData, config)
}
format.InitFormatFlags(app)

return resolver, nil
}
app.Flag("verbose", "Verbose running information").Short('v').BoolVar(&verbose)
app.Flag("alertmanager.url", "Alertmanager to talk to").Required().URLVar(&alertmanagerURL)
app.Flag("output", "Output formatter (simple, extended, json)").Short('o').Default("simple").EnumVar(&output, "simple", "extended", "json")
app.Version(version.Print("amtool"))
app.GetFlag("help").Short('h')
app.UsageTemplate(kingpin.CompactUsageTemplate)
app.Flag("help-long", "Give more detailed help output").UsageAction(&kingpin.UsageContext{
Template: longHelpTemplate,
Vars: map[string]interface{}{"LongHelp": longHelpText},
}).Bool()

func (r amtoolConfigResolver) Resolve(key string, context *kingpin.ParseContext) ([]string, error) {
for _, c := range r.configData {
if v, ok := c[key]; ok {
return []string{v}, nil
}
configResolver, err := newConfigResolver()
if err != nil {
kingpin.Fatalf("could not load config file: %v\n", err)
}
return nil, nil
}
// Use the same resolver twice, first for checking backwards compatibility,
// then again for the new names. This order ensures that the newest wins, if
// both old and new are present
app.Resolver(
kingpin.RenamingResolver(configResolver, backwardsCompatibilityResolver),
configResolver,
)

// This function maps things which have previously had different names in the
// config file to their new names, so old configurations keep working
func backwardsCompatibilityResolver(key string) string {
switch key {
case "require-comment":
return "comment_required"
configureAlertCmd(app, longHelpText)
configureSilenceCmd(app, longHelpText)
configureCheckConfigCmd(app, longHelpText)
configureConfigCmd(app, longHelpText)

_, err = app.Parse(os.Args[1:])
if err != nil {
kingpin.Fatalf("%v\n", err)
}
return key

}

func init() {
longHelpText["root"] = `View and modify the current Alertmanager state.
const (
longHelpTextRoot = `View and modify the current Alertmanager state.

Config File:
The alertmanager tool will read a config file in YAML format from one of two
Expand All @@ -99,40 +87,7 @@ static configuration:
date.format
Sets the output format for dates. Defaults to "2006-01-02 15:04:05 MST"
`
}

// Execute parses the arguments and executes the corresponding command, it is
// called by cmd/amtool/main.main().
func Execute() {
format.InitFormatFlags(app)

app.Version(version.Print("amtool"))
app.GetFlag("help").Short('h')
app.UsageTemplate(kingpin.CompactUsageTemplate)
app.Flag("help-long", "Give more detailed help output").UsageAction(&kingpin.UsageContext{
Template: longHelpTemplate,
Vars: map[string]interface{}{"LongHelp": longHelpText},
}).Bool()

configResolver, err := newConfigResolver()
if err != nil {
kingpin.Fatalf("could not load config file: %v\n", err)
}
// Use the same resolver twice, first for checking backwards compatibility,
// then again for the new names. This order ensures that the newest wins, if
// both old and new are present
app.Resolver(
kingpin.RenamingResolver(configResolver, backwardsCompatibilityResolver),
configResolver,
)

_, err = app.Parse(os.Args[1:])
if err != nil {
kingpin.Fatalf("%v\n", err)
}
}

const longHelpTemplate = `{{define "FormatCommands" -}}
longHelpTemplate = `{{define "FormatCommands" -}}
{{range .FlattenedCommands -}}
{{if not .Hidden}}
{{.CmdSummary}}
Expand Down Expand Up @@ -178,3 +133,4 @@ const longHelpTemplate = `{{define "FormatCommands" -}}
{{template "FormatCommands" .App}}
{{end -}}
`
)
14 changes: 10 additions & 4 deletions cli/silence.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
package cli

import "github.com/alecthomas/kingpin"

// silenceCmd represents the silence command
var (
silenceCmd = app.Command("silence", "Add, expire or view silences. For more information and additional flags see query help")
silenceQuiet = silenceCmd.Flag("quiet", "Only show silence ids").Short('q').Bool()
)
func configureSilenceCmd(app *kingpin.Application, longHelpText map[string]string) {
silenceCmd := app.Command("silence", "Add, expire or view silences. For more information and additional flags see query help")
configureSilenceAddCmd(silenceCmd, longHelpText)
configureSilenceExpireCmd(silenceCmd, longHelpText)
configureSilenceImportCmd(silenceCmd, longHelpText)
configureSilenceQueryCmd(silenceCmd, longHelpText)
configureSilenceUpdateCmd(silenceCmd, longHelpText)
}
Loading