From 9dbd4a30667e903bc9892f2f50405fc977c6a18f Mon Sep 17 00:00:00 2001 From: Craig Peterson Date: Thu, 11 Jan 2018 11:15:19 -0500 Subject: [PATCH] Simple notification framework (#297) * bonfire notifications working * make interface to make more extensible * some docs * typo * rename typo --- commands/createDomains.go | 2 +- commands/previewPush.go | 29 ++++++++++++++---- docs/index.md | 3 ++ docs/notifications.md | 47 ++++++++++++++++++++++++++++++ pkg/notifications/bonfire.go | 33 +++++++++++++++++++++ pkg/notifications/notifications.go | 41 ++++++++++++++++++++++++++ 6 files changed, 148 insertions(+), 7 deletions(-) create mode 100644 docs/notifications.md create mode 100644 pkg/notifications/bonfire.go create mode 100644 pkg/notifications/notifications.go diff --git a/commands/createDomains.go b/commands/createDomains.go index 3a9e82fc1..586a813c7 100644 --- a/commands/createDomains.go +++ b/commands/createDomains.go @@ -38,7 +38,7 @@ func CreateDomains(args CreateDomainsArgs) error { if err != nil { return err } - registrars, dnsProviders, _, err := InitializeProviders(args.CredsFile, cfg) + registrars, dnsProviders, _, _, err := InitializeProviders(args.CredsFile, cfg, false) if err != nil { return err } diff --git a/commands/previewPush.go b/commands/previewPush.go index 5263b84f0..e203c7d5e 100644 --- a/commands/previewPush.go +++ b/commands/previewPush.go @@ -8,6 +8,7 @@ import ( "github.com/StackExchange/dnscontrol/models" "github.com/StackExchange/dnscontrol/pkg/nameservers" "github.com/StackExchange/dnscontrol/pkg/normalize" + "github.com/StackExchange/dnscontrol/pkg/notifications" "github.com/StackExchange/dnscontrol/pkg/printer" "github.com/StackExchange/dnscontrol/providers" "github.com/StackExchange/dnscontrol/providers/config" @@ -31,12 +32,18 @@ type PreviewArgs struct { GetDNSConfigArgs GetCredentialsArgs FilterArgs + Notify bool } func (args *PreviewArgs) flags() []cli.Flag { flags := args.GetDNSConfigArgs.flags() flags = append(flags, args.GetCredentialsArgs.flags()...) flags = append(flags, args.FilterArgs.flags()...) + flags = append(flags, cli.BoolFlag{ + Name: "notify", + Destination: &args.Notify, + Usage: `set to true to send notifications to configured destinations`, + }) return flags } @@ -89,7 +96,7 @@ func run(args PreviewArgs, push bool, interactive bool, out printer.CLI) error { if PrintValidationErrors(errs) { return fmt.Errorf("Exiting due to validation errors") } - registrars, dnsProviders, nonDefaultProviders, err := InitializeProviders(args.CredsFile, cfg) + registrars, dnsProviders, nonDefaultProviders, notifier, err := InitializeProviders(args.CredsFile, cfg, args.Notify) if err != nil { return err } @@ -130,7 +137,7 @@ DomainLoop: continue DomainLoop } totalCorrections += len(corrections) - anyErrors = printOrRunCorrections(corrections, out, push, interactive) || anyErrors + anyErrors = printOrRunCorrections(domain.Name, prov, corrections, out, push, interactive, notifier) || anyErrors } run := args.shouldRunProvider(domain.Registrar, domain, nonDefaultProviders) out.StartRegistrar(domain.Registrar, !run) @@ -156,11 +163,12 @@ DomainLoop: continue } totalCorrections += len(corrections) - anyErrors = printOrRunCorrections(corrections, out, push, interactive) || anyErrors + anyErrors = printOrRunCorrections(domain.Name, domain.Registrar, corrections, out, push, interactive, notifier) || anyErrors } if os.Getenv("TEAMCITY_VERSION") != "" { fmt.Fprintf(os.Stderr, "##teamcity[buildStatus status='SUCCESS' text='%d corrections']", totalCorrections) } + notifier.Done() out.Debugf("Done. %d corrections.\n", totalCorrections) if anyErrors { return fmt.Errorf("Completed with errors") @@ -170,12 +178,19 @@ DomainLoop: // InitializeProviders takes a creds file path and a DNSConfig object. Creates all providers with the proper types, and returns them. // nonDefaultProviders is a list of providers that should not be run unless explicitly asked for by flags. -func InitializeProviders(credsFile string, cfg *models.DNSConfig) (registrars map[string]providers.Registrar, dnsProviders map[string]providers.DNSServiceProvider, nonDefaultProviders []string, err error) { +func InitializeProviders(credsFile string, cfg *models.DNSConfig, notifyFlag bool) (registrars map[string]providers.Registrar, dnsProviders map[string]providers.DNSServiceProvider, nonDefaultProviders []string, notify notifications.Notifier, err error) { var providerConfigs map[string]map[string]string + var notificationCfg map[string]string + defer func() { + notify = notifications.Init(notificationCfg) + }() providerConfigs, err = config.LoadProviderConfigs(credsFile) if err != nil { return } + if notifyFlag { + notificationCfg = providerConfigs["notifications"] + } nonDefaultProviders = []string{} for name, vals := range providerConfigs { // add "_exclude_from_defaults":"true" to a provider to exclude it from being run unless @@ -195,23 +210,25 @@ func InitializeProviders(credsFile string, cfg *models.DNSConfig) (registrars ma return } -func printOrRunCorrections(corrections []*models.Correction, out printer.CLI, push bool, interactive bool) (anyErrors bool) { +func printOrRunCorrections(domain string, provider string, corrections []*models.Correction, out printer.CLI, push bool, interactive bool, notifier notifications.Notifier) (anyErrors bool) { anyErrors = false if len(corrections) == 0 { return false } for i, correction := range corrections { out.PrintCorrection(i, correction) + var err error if push { if interactive && !out.PromptToRun() { continue } - err := correction.F() + err = correction.F() out.EndCorrection(err) if err != nil { anyErrors = true } } + notifier.Notify(domain, provider, correction.Msg, err, !push) } return anyErrors } diff --git a/docs/index.md b/docs/index.md index da782e10c..6cc103756 100644 --- a/docs/index.md +++ b/docs/index.md @@ -129,6 +129,9 @@ title: DnsControl
  • Testing: Unit Testing for you DNS Data
  • +
  • + Notifications: Be alerted when your domains are changed +
  • diff --git a/docs/notifications.md b/docs/notifications.md new file mode 100644 index 000000000..33dd885b2 --- /dev/null +++ b/docs/notifications.md @@ -0,0 +1,47 @@ +--- +layout: default +title: Notifications +--- +# Notifications + +DNSControl has build in support for notifications when changes are made. This allows you to post messages in team chat, or send emails when dns changes are made. + +Notifications are written in the [notifications package](https://github.com/StackExchange/dnscontrol/tree/master/pkg/notifications), and is a really simple interface to implement if you want to add +new types or destinations. + +## Configuration + +Notifications are set up in your credentials json file. They will use the `notifications` key to look for keys or configuration needed for various notification types. + +``` + "r53": { + ... + }, + "gcloud": { + ... + } , + "notifications":{ + "bonfire_url": "https://chat.meta.stackexchange.com/feeds/rooms/123?key=xyz" + } +``` + +You also must run `dnscontrol preview` or `dnscontrol push` with the `-notify` flag to enable notification sending at all. + +## Notification types + +### Bonfire + +This is stack overflow's built in chat system. This is probably not useful for most people. + +Configure `bonfire_url` to be the full url including room and api key. + +## Future work + +Yes, this seems pretty limited right now in what it can do. We didn't want to add a bunch of notification types if nobody was going to use them. The good news is, it should +be really simple to add more. We gladly welcome any PRs with new notification destinations. Some easy possibilities: + +- Email +- Slack +- Generic Webhooks + +Please update this documentation if you add anything. \ No newline at end of file diff --git a/pkg/notifications/bonfire.go b/pkg/notifications/bonfire.go new file mode 100644 index 000000000..e96920782 --- /dev/null +++ b/pkg/notifications/bonfire.go @@ -0,0 +1,33 @@ +package notifications + +import ( + "fmt" + "net/http" + "strings" +) + +func init() { + initers = append(initers, func(cfg map[string]string) Notifier { + if url, ok := cfg["bonfire_url"]; ok { + return bonfireNotifier(url) + } + return nil + }) +} + +// bonfire notifier for stack exchange internal chat. String is just url with room and token in it +type bonfireNotifier string + +func (b bonfireNotifier) Notify(domain, provider, msg string, err error, preview bool) { + var payload string + if preview { + payload = fmt.Sprintf(`**Preview: %s[%s] -** %s`, domain, provider, msg) + } else if err != nil { + payload = fmt.Sprintf(`**ERROR running correction on %s[%s] -** (%s) Error: %s`, domain, provider, msg, err) + } else { + payload = fmt.Sprintf(`Successfully ran correction for %s[%s] - %s`, domain, provider, msg) + } + http.Post(string(b), "text/markdown", strings.NewReader(payload)) +} + +func (b bonfireNotifier) Done() {} diff --git a/pkg/notifications/notifications.go b/pkg/notifications/notifications.go new file mode 100644 index 000000000..2a29490b1 --- /dev/null +++ b/pkg/notifications/notifications.go @@ -0,0 +1,41 @@ +package notifications + +// Notifier is a type that can send a notification +type Notifier interface { + // Notify will be called after a correction is performed. + // It will be given the correction's message, the result of executing it, + // and a flag for whether this is a preview or if it actually ran. + // If preview is true, err will always be nil. + Notify(domain, provider string, message string, err error, preview bool) + // Done will be called exactly once after all notifications are done. This will allow "batched" notifiers to flush and send + Done() +} + +// new notification types should add themselves to this array +var initers = []func(map[string]string) Notifier{} + +// Init will take the given config map (from creds.json notifications key) and create a single Notifier with +// all notifications it has full config for. +func Init(config map[string]string) Notifier { + notifiers := multiNotifier{} + for _, i := range initers { + n := i(config) + if n != nil { + notifiers = append(notifiers, n) + } + } + return notifiers +} + +type multiNotifier []Notifier + +func (m multiNotifier) Notify(domain, provider string, message string, err error, preview bool) { + for _, n := range m { + n.Notify(domain, provider, message, err, preview) + } +} +func (m multiNotifier) Done() { + for _, n := range m { + n.Done() + } +}