Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ntfy middleware #314

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
10 changes: 10 additions & 0 deletions cli/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type Config struct {
middlewares.SlackConfig `mapstructure:",squash"`
middlewares.SaveConfig `mapstructure:",squash"`
middlewares.MailConfig `mapstructure:",squash"`
middlewares.NtfyConfig `mapstructure:",squash"`
}
ExecJobs map[string]*ExecJobConfig `gcfg:"job-exec" mapstructure:"job-exec,squash"`
RunJobs map[string]*RunJobConfig `gcfg:"job-run" mapstructure:"job-run,squash"`
Expand Down Expand Up @@ -146,6 +147,7 @@ func (c *Config) buildSchedulerMiddlewares(sh *core.Scheduler) {
sh.Use(middlewares.NewSlack(&c.Global.SlackConfig))
sh.Use(middlewares.NewSave(&c.Global.SaveConfig))
sh.Use(middlewares.NewMail(&c.Global.MailConfig))
sh.Use(middlewares.NewNtfy(&c.Global.NtfyConfig))
}

// ExecJobConfig contains all configuration params needed to build a ExecJob
Expand All @@ -155,13 +157,15 @@ type ExecJobConfig struct {
middlewares.SlackConfig `mapstructure:",squash"`
middlewares.SaveConfig `mapstructure:",squash"`
middlewares.MailConfig `mapstructure:",squash"`
middlewares.NtfyConfig `mapstructure:",squash"`
}

func (c *ExecJobConfig) buildMiddlewares() {
c.ExecJob.Use(middlewares.NewOverlap(&c.OverlapConfig))
c.ExecJob.Use(middlewares.NewSlack(&c.SlackConfig))
c.ExecJob.Use(middlewares.NewSave(&c.SaveConfig))
c.ExecJob.Use(middlewares.NewMail(&c.MailConfig))
c.ExecJob.Use(middlewares.NewNtfy(&c.NtfyConfig))
}

// RunServiceConfig contains all configuration params needed to build a RunJob
Expand All @@ -171,6 +175,7 @@ type RunServiceConfig struct {
middlewares.SlackConfig `mapstructure:",squash"`
middlewares.SaveConfig `mapstructure:",squash"`
middlewares.MailConfig `mapstructure:",squash"`
middlewares.NtfyConfig `mapstructure:",squash"`
}

type RunJobConfig struct {
Expand All @@ -179,13 +184,15 @@ type RunJobConfig struct {
middlewares.SlackConfig `mapstructure:",squash"`
middlewares.SaveConfig `mapstructure:",squash"`
middlewares.MailConfig `mapstructure:",squash"`
middlewares.NtfyConfig `mapstructure:",squash"`
}

func (c *RunJobConfig) buildMiddlewares() {
c.RunJob.Use(middlewares.NewOverlap(&c.OverlapConfig))
c.RunJob.Use(middlewares.NewSlack(&c.SlackConfig))
c.RunJob.Use(middlewares.NewSave(&c.SaveConfig))
c.RunJob.Use(middlewares.NewMail(&c.MailConfig))
c.RunJob.Use(middlewares.NewNtfy(&c.NtfyConfig))
}

// LocalJobConfig contains all configuration params needed to build a RunJob
Expand All @@ -195,18 +202,21 @@ type LocalJobConfig struct {
middlewares.SlackConfig `mapstructure:",squash"`
middlewares.SaveConfig `mapstructure:",squash"`
middlewares.MailConfig `mapstructure:",squash"`
middlewares.NtfyConfig `mapstructure:",squash"`
}

func (c *LocalJobConfig) buildMiddlewares() {
c.LocalJob.Use(middlewares.NewOverlap(&c.OverlapConfig))
c.LocalJob.Use(middlewares.NewSlack(&c.SlackConfig))
c.LocalJob.Use(middlewares.NewSave(&c.SaveConfig))
c.LocalJob.Use(middlewares.NewMail(&c.MailConfig))
c.LocalJob.Use(middlewares.NewNtfy(&c.NtfyConfig))
}

func (c *RunServiceConfig) buildMiddlewares() {
c.RunServiceJob.Use(middlewares.NewOverlap(&c.OverlapConfig))
c.RunServiceJob.Use(middlewares.NewSlack(&c.SlackConfig))
c.RunServiceJob.Use(middlewares.NewSave(&c.SaveConfig))
c.RunServiceJob.Use(middlewares.NewMail(&c.MailConfig))
c.RunServiceJob.Use(middlewares.NewNtfy(&c.NtfyConfig))
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@ require (

require (
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
github.com/AnthonyHewins/gotfy v0.0.10 // indirect
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/klauspost/compress v1.17.9 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/AnthonyHewins/gotfy v0.0.10 h1:23ZjRVG7wuGuqn7CQq/bOrkXa3gg2XyYrrD3RYEmvE8=
github.com/AnthonyHewins/gotfy v0.0.10/go.mod h1:q2orErDDpl9/gZ5L4oJhejb7TaP/eBdtkzjWDruNRlg=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
Expand All @@ -26,6 +28,8 @@ github.com/fsouza/go-dockerclient v1.12.0 h1:S2f2crEUbBNCFiF06kR/GvioEB8EMsb3Td/
github.com/fsouza/go-dockerclient v1.12.0/go.mod h1:YWUtjg8japrqD/80L98nTtCoxQFp5B5wrSsnyeB5lFo=
github.com/gobs/args v0.0.0-20210311043657-b8c0b223be93 h1:70jFzur8/dg4E5NKFMOPLAxk4wSyGm3vQ+7PuBEoHzE=
github.com/gobs/args v0.0.0-20210311043657-b8c0b223be93/go.mod h1:ZpqkpUmnBz2Jz7hMGSPRbHtYC82FP/IZ1Y7A2riYH0s=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
Expand Down
92 changes: 92 additions & 0 deletions middlewares/ntfy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package middlewares

import (
"context"
"errors"
"fmt"
"net/http"
"net/url"

"github.com/AnthonyHewins/gotfy"
"github.com/mcuadros/ofelia/core"
)

type NtfyConfig struct {
NtfyBaseURL string `gcfg:"ntfy-base-url" mapstructure:"ntfy-base-url"`
NtfyTopic string `gcfg:"ntfy-topic" mapstructure:"ntfy-topic"`
NtfyApiKey string `gcfg:"ntfy-api-key" mapstructure:"ntfy-api-key" json:"-"`
NtfyOnlyOnError bool `gcfg:"ntfy-only-on-error" mapstructure:"ntfy-only-on-error"`
}

func NewNtfy(c *NtfyConfig) core.Middleware {
var m core.Middleware

if !IsEmpty(c) {
m = &Ntfy{*c}
}

return m
}

type Ntfy struct {
NtfyConfig
}

func (n *Ntfy) ContinueOnStop() bool {
return true
}

func (n *Ntfy) Run(ctx *core.Context) error {
err := ctx.Next()
ctx.Stop(err)

if ctx.Execution.Failed || !n.NtfyOnlyOnError {
err := n.sendNtfy(ctx)
if err != nil {
ctx.Logger.Errorf("ntfy error: %q", err)
}
}

return err
}

func (n *Ntfy) sendNtfy(ctx *core.Context) error {
server, _ := url.Parse(n.NtfyBaseURL)
httpClient := http.DefaultClient

if n.NtfyApiKey == "" {
return errors.New("missing credentials for ntfy service")
}

if n.NtfyTopic == "" {
return errors.New("missing topic for ntfy service")
}

tp, err := gotfy.NewPublisher(server, httpClient)
if err != nil {
return err
}
tp.Headers.Add("Authorization", "Bearer "+n.NtfyApiKey)
tp.Headers.Add("X-Markdown", "true")

msg := &gotfy.Message{
Topic: n.NtfyTopic,
}
msg.Message = fmt.Sprintf(
"Job *%q* finished in *%s*, command `%s`",
ctx.Job.GetName(), ctx.Execution.Duration, ctx.Job.GetCommand(),
)

if ctx.Execution.Failed {
msg.Title = "Execution failed"
msg.Message = ctx.Execution.Error.Error()
} else if ctx.Execution.Skipped {
msg.Title = "Execution skipped"
} else {
msg.Title = "Execution successful"
}

_, err = tp.SendMessage(context.Background(), msg)

return err
}
105 changes: 105 additions & 0 deletions middlewares/ntfy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package middlewares

import (
"encoding/json"
"errors"
"net/http"
"net/http/httptest"

"github.com/AnthonyHewins/gotfy"
. "gopkg.in/check.v1"
)

type SuiteNtfy struct {
BaseSuite
}

var _ = Suite(&SuiteNtfy{})

func (s *SuiteNtfy) TestNewNtfyEmpty(c *C) {
c.Assert(NewNtfy(&NtfyConfig{}), IsNil)
}

func (s *SuiteNtfy) TestRunSuccess(c *C) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var m gotfy.Message
// read json from request body
json.NewDecoder(r.Body).Decode(&m)
c.Assert(m.Title, Equals, "Execution successful")

w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(gotfy.PublishResp{
ID: "foo",
Time: 1,
Expires: 2147483647,
Topic: "bar",
Event: "message",
Message: "triggered",
})
}))

defer ts.Close()

s.ctx.Start()
s.ctx.Stop(nil)

m := NewNtfy(&NtfyConfig{
NtfyBaseURL: ts.URL,
NtfyApiKey: "foo",
NtfyTopic: "bar",
NtfyOnlyOnError: false,
})
c.Assert(m.Run(s.ctx), IsNil)
}

func (s *SuiteNtfy) TestRunSuccessFailed(c *C) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var m gotfy.Message

// read json from request body
json.NewDecoder(r.Body).Decode(&m)
c.Assert(m.Title, Equals, "Execution failed")

w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(gotfy.PublishResp{
ID: "foo",
Time: 1,
Expires: 2147483647,
Topic: "bar",
Event: "message",
Message: "triggered",
})
}))

defer ts.Close()

s.ctx.Start()
s.ctx.Stop(errors.New("foo"))

m := NewNtfy(&NtfyConfig{
NtfyBaseURL: ts.URL,
NtfyApiKey: "foo",
NtfyTopic: "bar",
NtfyOnlyOnError: false,
})
c.Assert(m.Run(s.ctx), IsNil)
}

func (s *SuiteNtfy) TestRunSuccessOnError(c *C) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c.Assert(true, Equals, false)
}))

defer ts.Close()

s.ctx.Start()
s.ctx.Stop(nil)

m := NewNtfy(&NtfyConfig{
NtfyBaseURL: ts.URL,
NtfyApiKey: "foo",
NtfyTopic: "bar",
NtfyOnlyOnError: true,
})
c.Assert(m.Run(s.ctx), IsNil)
}