From 9739d06bb418a29a0733f8baf58ad8d569a665f3 Mon Sep 17 00:00:00 2001 From: Gabe Cook Date: Mon, 4 Mar 2024 14:26:24 -0600 Subject: [PATCH] feat(nofify): Add support for Healthchecks notifications --- cmd/cmd.go | 20 +++++++++ cmd/finalizer.go | 13 ++++++ docs/kubedb.md | 19 ++++---- docs/kubedb_dump.md | 15 ++++--- docs/kubedb_exec.md | 15 ++++--- docs/kubedb_port-forward.md | 15 ++++--- docs/kubedb_restore.md | 15 ++++--- docs/kubedb_status.md | 15 ++++--- internal/config/flags/log.go | 10 +++++ internal/consts/flags.go | 9 ++-- internal/consts/viper.go | 23 +++++----- internal/notifier/context.go | 16 +++++++ internal/notifier/healthchecks.go | 73 +++++++++++++++++++++++++++++++ internal/notifier/notifier.go | 35 +++++++++++++++ internal/util/io.go | 8 ++++ main.go | 2 + 16 files changed, 244 insertions(+), 59 deletions(-) create mode 100644 cmd/finalizer.go create mode 100644 internal/notifier/context.go create mode 100644 internal/notifier/healthchecks.go create mode 100644 internal/notifier/notifier.go create mode 100644 internal/util/io.go diff --git a/cmd/cmd.go b/cmd/cmd.go index c831a079..2afa3d01 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -15,6 +15,7 @@ import ( "github.com/clevyr/kubedb/cmd/status" "github.com/clevyr/kubedb/internal/config/flags" "github.com/clevyr/kubedb/internal/consts" + "github.com/clevyr/kubedb/internal/notifier" "github.com/clevyr/kubedb/internal/util" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -38,6 +39,7 @@ func NewCommand() *cobra.Command { flags.Pod(cmd) flags.LogLevel(cmd) flags.LogFormat(cmd) + flags.Healthchecks(cmd) flags.Redact(cmd) cmd.InitDefaultVersionFlag() @@ -68,6 +70,7 @@ func preRun(cmd *cobra.Command, args []string) error { flags.BindLogLevel(cmd) flags.BindLogFormat(cmd) flags.BindRedact(cmd) + flags.BindHealthchecks(cmd) ctx, cancel := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill, syscall.SIGTERM) cmd.PersistentPostRun = func(cmd *cobra.Command, args []string) { cancel() } @@ -88,6 +91,23 @@ func preRun(cmd *cobra.Command, args []string) error { return err } initLog(cmd) + + if url := viper.GetString(consts.HealthchecksPingUrlKey); url != "" { + if handler, err := notifier.NewHealthchecks(url); err != nil { + log.WithError(err).Error("Notifications creation failed") + } else { + if err := handler.Started(); err != nil { + log.WithError(err).Error("Notifications ping start failed") + } + + OnFinalize(func(err error) { + if err := handler.Finished(err); err != nil { + log.WithError(err).Error("Notifications ping finished failed") + } + }) + } + } + return nil } diff --git a/cmd/finalizer.go b/cmd/finalizer.go new file mode 100644 index 00000000..0438a74e --- /dev/null +++ b/cmd/finalizer.go @@ -0,0 +1,13 @@ +package cmd + +var finalizers []func(err error) + +func OnFinalize(y ...func(err error)) { + finalizers = append(finalizers, y...) +} + +func PostRun(err error) { + for _, x := range finalizers { + x(err) + } +} diff --git a/docs/kubedb.md b/docs/kubedb.md index 1a425e03..725dcf5e 100644 --- a/docs/kubedb.md +++ b/docs/kubedb.md @@ -5,15 +5,16 @@ Painlessly work with databases in Kubernetes. ### Options ``` - --context string Kubernetes context name - --dialect string Database dialect. One of (postgres|mariadb|mongodb) (default discovered) - -h, --help help for kubedb - --kubeconfig string Paths to the kubeconfig file (default "$HOME/.kube/config") - --log-format string Log formatter. One of (text|json) (default "text") - --log-level string Log level. One of (trace|debug|info|warning|error|fatal|panic) (default "info") - -n, --namespace string Kubernetes namespace - --pod string Perform detection from a pod instead of searching the namespace - -v, --version version for kubedb + --context string Kubernetes context name + --dialect string Database dialect. One of (postgres|mariadb|mongodb) (default discovered) + --healthchecks-ping-url string Notification handler URL + -h, --help help for kubedb + --kubeconfig string Paths to the kubeconfig file (default "$HOME/.kube/config") + --log-format string Log formatter. One of (text|json) (default "text") + --log-level string Log level. One of (trace|debug|info|warning|error|fatal|panic) (default "info") + -n, --namespace string Kubernetes namespace + --pod string Perform detection from a pod instead of searching the namespace + -v, --version version for kubedb ``` ### SEE ALSO diff --git a/docs/kubedb_dump.md b/docs/kubedb_dump.md index 9e5b3e75..e79dc34e 100644 --- a/docs/kubedb_dump.md +++ b/docs/kubedb_dump.md @@ -52,13 +52,14 @@ kubedb dump [filename | S3 URI] [flags] ### Options inherited from parent commands ``` - --context string Kubernetes context name - --dialect string Database dialect. One of (postgres|mariadb|mongodb) (default discovered) - --kubeconfig string Paths to the kubeconfig file (default "$HOME/.kube/config") - --log-format string Log formatter. One of (text|json) (default "text") - --log-level string Log level. One of (trace|debug|info|warning|error|fatal|panic) (default "info") - -n, --namespace string Kubernetes namespace - --pod string Perform detection from a pod instead of searching the namespace + --context string Kubernetes context name + --dialect string Database dialect. One of (postgres|mariadb|mongodb) (default discovered) + --healthchecks-ping-url string Notification handler URL + --kubeconfig string Paths to the kubeconfig file (default "$HOME/.kube/config") + --log-format string Log formatter. One of (text|json) (default "text") + --log-level string Log level. One of (trace|debug|info|warning|error|fatal|panic) (default "info") + -n, --namespace string Kubernetes namespace + --pod string Perform detection from a pod instead of searching the namespace ``` ### SEE ALSO diff --git a/docs/kubedb_exec.md b/docs/kubedb_exec.md index f6c91c2a..e0f467c0 100644 --- a/docs/kubedb_exec.md +++ b/docs/kubedb_exec.md @@ -22,13 +22,14 @@ kubedb exec [flags] ### Options inherited from parent commands ``` - --context string Kubernetes context name - --dialect string Database dialect. One of (postgres|mariadb|mongodb) (default discovered) - --kubeconfig string Paths to the kubeconfig file (default "$HOME/.kube/config") - --log-format string Log formatter. One of (text|json) (default "text") - --log-level string Log level. One of (trace|debug|info|warning|error|fatal|panic) (default "info") - -n, --namespace string Kubernetes namespace - --pod string Perform detection from a pod instead of searching the namespace + --context string Kubernetes context name + --dialect string Database dialect. One of (postgres|mariadb|mongodb) (default discovered) + --healthchecks-ping-url string Notification handler URL + --kubeconfig string Paths to the kubeconfig file (default "$HOME/.kube/config") + --log-format string Log formatter. One of (text|json) (default "text") + --log-level string Log level. One of (trace|debug|info|warning|error|fatal|panic) (default "info") + -n, --namespace string Kubernetes namespace + --pod string Perform detection from a pod instead of searching the namespace ``` ### SEE ALSO diff --git a/docs/kubedb_port-forward.md b/docs/kubedb_port-forward.md index d23b62b0..2cf882bf 100644 --- a/docs/kubedb_port-forward.md +++ b/docs/kubedb_port-forward.md @@ -18,13 +18,14 @@ kubedb port-forward [local_port] [flags] ### Options inherited from parent commands ``` - --context string Kubernetes context name - --dialect string Database dialect. One of (postgres|mariadb|mongodb) (default discovered) - --kubeconfig string Paths to the kubeconfig file (default "$HOME/.kube/config") - --log-format string Log formatter. One of (text|json) (default "text") - --log-level string Log level. One of (trace|debug|info|warning|error|fatal|panic) (default "info") - -n, --namespace string Kubernetes namespace - --pod string Perform detection from a pod instead of searching the namespace + --context string Kubernetes context name + --dialect string Database dialect. One of (postgres|mariadb|mongodb) (default discovered) + --healthchecks-ping-url string Notification handler URL + --kubeconfig string Paths to the kubeconfig file (default "$HOME/.kube/config") + --log-format string Log formatter. One of (text|json) (default "text") + --log-level string Log level. One of (trace|debug|info|warning|error|fatal|panic) (default "info") + -n, --namespace string Kubernetes namespace + --pod string Perform detection from a pod instead of searching the namespace ``` ### SEE ALSO diff --git a/docs/kubedb_restore.md b/docs/kubedb_restore.md index 33e86556..b1265c38 100644 --- a/docs/kubedb_restore.md +++ b/docs/kubedb_restore.md @@ -39,13 +39,14 @@ kubedb restore filename [flags] ### Options inherited from parent commands ``` - --context string Kubernetes context name - --dialect string Database dialect. One of (postgres|mariadb|mongodb) (default discovered) - --kubeconfig string Paths to the kubeconfig file (default "$HOME/.kube/config") - --log-format string Log formatter. One of (text|json) (default "text") - --log-level string Log level. One of (trace|debug|info|warning|error|fatal|panic) (default "info") - -n, --namespace string Kubernetes namespace - --pod string Perform detection from a pod instead of searching the namespace + --context string Kubernetes context name + --dialect string Database dialect. One of (postgres|mariadb|mongodb) (default discovered) + --healthchecks-ping-url string Notification handler URL + --kubeconfig string Paths to the kubeconfig file (default "$HOME/.kube/config") + --log-format string Log formatter. One of (text|json) (default "text") + --log-level string Log level. One of (trace|debug|info|warning|error|fatal|panic) (default "info") + -n, --namespace string Kubernetes namespace + --pod string Perform detection from a pod instead of searching the namespace ``` ### SEE ALSO diff --git a/docs/kubedb_status.md b/docs/kubedb_status.md index 725ba036..7ebfd9d8 100644 --- a/docs/kubedb_status.md +++ b/docs/kubedb_status.md @@ -20,13 +20,14 @@ kubedb status [flags] ### Options inherited from parent commands ``` - --context string Kubernetes context name - --dialect string Database dialect. One of (postgres|mariadb|mongodb) (default discovered) - --kubeconfig string Paths to the kubeconfig file (default "$HOME/.kube/config") - --log-format string Log formatter. One of (text|json) (default "text") - --log-level string Log level. One of (trace|debug|info|warning|error|fatal|panic) (default "info") - -n, --namespace string Kubernetes namespace - --pod string Perform detection from a pod instead of searching the namespace + --context string Kubernetes context name + --dialect string Database dialect. One of (postgres|mariadb|mongodb) (default discovered) + --healthchecks-ping-url string Notification handler URL + --kubeconfig string Paths to the kubeconfig file (default "$HOME/.kube/config") + --log-format string Log formatter. One of (text|json) (default "text") + --log-level string Log level. One of (trace|debug|info|warning|error|fatal|panic) (default "info") + -n, --namespace string Kubernetes namespace + --pod string Perform detection from a pod instead of searching the namespace ``` ### SEE ALSO diff --git a/internal/config/flags/log.go b/internal/config/flags/log.go index ea7e6e5f..5715b666 100644 --- a/internal/config/flags/log.go +++ b/internal/config/flags/log.go @@ -67,3 +67,13 @@ func BindRedact(cmd *cobra.Command) { panic(err) } } + +func Healthchecks(cmd *cobra.Command) { + cmd.PersistentFlags().String(consts.HealthchecksPingUrlFlag, "", "Notification handler URL") +} + +func BindHealthchecks(cmd *cobra.Command) { + if err := viper.BindPFlag(consts.HealthchecksPingUrlKey, cmd.Flags().Lookup(consts.HealthchecksPingUrlFlag)); err != nil { + panic(err) + } +} diff --git a/internal/consts/flags.go b/internal/consts/flags.go index 0768d44a..f0143fee 100644 --- a/internal/consts/flags.go +++ b/internal/consts/flags.go @@ -28,10 +28,11 @@ const ( JobPodLabelsFlag = "job-pod-labels" NoJobFlag = "no-job" - QuietFlag = "quiet" - LogLevelFlag = "log-level" - LogFormatFlag = "log-format" - RedactFlag = "redact" + QuietFlag = "quiet" + LogLevelFlag = "log-level" + LogFormatFlag = "log-format" + RedactFlag = "redact" + HealthchecksPingUrlFlag = "healthchecks-ping-url" RemoteGzipFlag = "remote-gzip" diff --git a/internal/consts/viper.go b/internal/consts/viper.go index 05530c58..109c713b 100644 --- a/internal/consts/viper.go +++ b/internal/consts/viper.go @@ -1,15 +1,16 @@ package consts const ( - AnalyzeKey = "restore.analyze" - HaltOnErrorKey = "restore.halt-on-error" - SpinnerKey = "spinner.name" - KubeconfigKey = "kubernetes.kubeconfig" - JobPodLabelsKey = "kubernetes.job-pod-labels" - NoJobKey = "kubernetes.no-job" - LogLevelKey = "log.level" - LogFormatKey = "log.format" - LogRedactKey = "log.redact" - RemoteGzipKey = "remote-gzip" - PortForwardAddrKey = "port-forward.address" + AnalyzeKey = "restore.analyze" + HaltOnErrorKey = "restore.halt-on-error" + SpinnerKey = "spinner.name" + KubeconfigKey = "kubernetes.kubeconfig" + JobPodLabelsKey = "kubernetes.job-pod-labels" + NoJobKey = "kubernetes.no-job" + LogLevelKey = "log.level" + LogFormatKey = "log.format" + LogRedactKey = "log.redact" + RemoteGzipKey = "remote-gzip" + PortForwardAddrKey = "port-forward.address" + HealthchecksPingUrlKey = "healthchecks.ping-url" ) diff --git a/internal/notifier/context.go b/internal/notifier/context.go new file mode 100644 index 00000000..7ceaef32 --- /dev/null +++ b/internal/notifier/context.go @@ -0,0 +1,16 @@ +package notifier + +import "context" + +type contextKey uint8 + +const notifierContextKey contextKey = iota + +func NewContext(ctx context.Context, n Notifier) context.Context { + return context.WithValue(ctx, notifierContextKey, n) +} + +func FromContext(ctx context.Context) (Notifier, bool) { + notifier, ok := ctx.Value(notifierContextKey).(Notifier) + return notifier, ok +} diff --git a/internal/notifier/healthchecks.go b/internal/notifier/healthchecks.go new file mode 100644 index 00000000..91d30a44 --- /dev/null +++ b/internal/notifier/healthchecks.go @@ -0,0 +1,73 @@ +package notifier + +import ( + "fmt" + "net/http" + "net/url" + "path" + "strings" + + "github.com/clevyr/kubedb/internal/util" +) + +func NewHealthchecks(url string) (Notifier, error) { + if url == "" { + return nil, fmt.Errorf("healthchecks %w", ErrEmptyUrl) + } + + return &Healthchecks{ + url: url, + }, nil +} + +type Healthchecks struct { + url string +} + +func (h Healthchecks) SendStatus(status Status, log string) error { + var statusStr string + switch status { + case StatusStart: + statusStr = "start" + case StatusFailure: + statusStr = "fail" + } + + u, err := url.Parse(h.url) + if err != nil { + return err + } + + u.Path = path.Join(u.Path, statusStr) + + req, err := http.NewRequest(http.MethodPost, u.String(), strings.NewReader(log)) + if err != nil { + return err + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer util.ReadAndClose(resp.Body) + + switch resp.StatusCode { + case http.StatusOK: + case http.StatusCreated: + default: + return fmt.Errorf("%w: %s", ErrInvalidResponse, resp.Status) + } + return nil +} + +func (h Healthchecks) Started() error { + return h.SendStatus(StatusStart, "") +} + +func (h Healthchecks) Finished(err error) error { + if err == nil { + return h.SendStatus(StatusSuccess, "") + } else { + return h.SendStatus(StatusFailure, "Error: "+err.Error()) + } +} diff --git a/internal/notifier/notifier.go b/internal/notifier/notifier.go new file mode 100644 index 00000000..3330644f --- /dev/null +++ b/internal/notifier/notifier.go @@ -0,0 +1,35 @@ +package notifier + +import ( + "errors" + "fmt" + "strings" +) + +type Status uint8 + +const ( + StatusSuccess Status = iota + StatusFailure + StatusStart +) + +var ( + ErrInvalidResponse = errors.New("invalid http response") + ErrUnknownHandler = errors.New("unknown handler") + ErrEmptyUrl = errors.New("url must be set") +) + +type Notifier interface { + Started() error + Finished(err error) error +} + +func New(handler, url string) (Notifier, error) { + switch strings.ToLower(handler) { + case "healthchecks": + return NewHealthchecks(url) + default: + return nil, fmt.Errorf("%w: %s", ErrUnknownHandler, handler) + } +} diff --git a/internal/util/io.go b/internal/util/io.go new file mode 100644 index 00000000..70e32b72 --- /dev/null +++ b/internal/util/io.go @@ -0,0 +1,8 @@ +package util + +import "io" + +func ReadAndClose(r io.ReadCloser) { + _, _ = io.Copy(io.Discard, r) + _ = r.Close() +} diff --git a/main.go b/main.go index 9beaeeff..42bce8c8 100644 --- a/main.go +++ b/main.go @@ -9,6 +9,8 @@ import ( func main() { rootCmd := cmd.NewCommand() if err := rootCmd.Execute(); err != nil { + cmd.PostRun(err) os.Exit(1) } + cmd.PostRun(nil) }