feat: gotify notification
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
parent
660fc317ab
commit
01e283f96b
5 changed files with 173 additions and 28 deletions
42
README.md
42
README.md
|
|
@ -9,6 +9,7 @@ A lightweight, efficient cron-like scheduler written in Go that executes shell c
|
||||||
- **Comprehensive Logging**: Detailed execution logs using `zerolog` with configurable levels
|
- **Comprehensive Logging**: Detailed execution logs using `zerolog` with configurable levels
|
||||||
- **Error Handling**: Robust error handling with exit code tracking and output capture
|
- **Error Handling**: Robust error handling with exit code tracking and output capture
|
||||||
- **Shell Command Support**: Execute any shell command via bash
|
- **Shell Command Support**: Execute any shell command via bash
|
||||||
|
- **Notifications**: Send job status notifications via Gotify
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
|
@ -46,12 +47,40 @@ jobs:
|
||||||
|
|
||||||
### Configuration Fields
|
### Configuration Fields
|
||||||
|
|
||||||
| Field | Description | Format | Examples |
|
|| Field | Description | Format | Examples |
|
||||||
|-------|-------------|--------|----------|
|
||-------|-------------|--------|----------|
|
||||||
| `name` | Descriptive job name for logging | String | "Daily Backup", "Health Check" |
|
|| `name` | Descriptive job name for logging | String | "Daily Backup", "Health Check" |
|
||||||
| `minute` | Minute pattern (0-59) | String | `"0"`, `"*/5"`, `"15,30,45"`, `"*"` |
|
|| `minute` | Minute pattern (0-59) | String | `"0"`, `"*/5"`, `"15,30,45"`, `"*"` |
|
||||||
| `hour` | Hour pattern (0-23) | String | `"2"`, `"*/2"`, `"9-17"`, `"*"` |
|
|| `hour` | Hour pattern (0-23) | String | `"2"`, `"*/2"`, `"9-17"`, `"*"` |
|
||||||
| `command` | Shell command to execute | String | `"echo 'Hello'"`, `"backup.sh"` |
|
|| `command` | Shell command to execute | String | `"echo 'Hello'"`, `"backup.sh"` |
|
||||||
|
|| `notification` | Name of notification configuration to use | String | `"default"` |
|
||||||
|
|
||||||
|
### Notifications
|
||||||
|
|
||||||
|
You can configure notifications for job success or failure using Gotify:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
notification:
|
||||||
|
- name: default
|
||||||
|
success:
|
||||||
|
gotify:
|
||||||
|
url: https://gotify.example.com/message?token=YOUR_TOKEN
|
||||||
|
error:
|
||||||
|
gotify:
|
||||||
|
url: https://gotify.example.com/message?token=YOUR_TOKEN
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
- name: "Daily Backup"
|
||||||
|
minute: "0"
|
||||||
|
hour: "2"
|
||||||
|
command: "backup.sh /data /backup"
|
||||||
|
notification: default
|
||||||
|
```
|
||||||
|
|
||||||
|
Notifications support:
|
||||||
|
- Multiple named notification configurations
|
||||||
|
- Separate success and error notification URLs
|
||||||
|
- Optional configuration per job
|
||||||
|
|
||||||
### Pattern Syntax
|
### Pattern Syntax
|
||||||
- `*` - Every minute/hour
|
- `*` - Every minute/hour
|
||||||
|
|
@ -99,4 +128,3 @@ jobs:
|
||||||
hour: "3"
|
hour: "3"
|
||||||
command: "cleanup.sh"
|
command: "cleanup.sh"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
32
config.go
32
config.go
|
|
@ -12,14 +12,36 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type config struct {
|
type config struct {
|
||||||
Jobs []jobconfig `yaml:"jobs"`
|
Jobs []jobconfig `yaml:"jobs"`
|
||||||
|
Notification []notificationConfig `yaml:"notification"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type jobconfig struct {
|
type jobconfig struct {
|
||||||
Name string `yaml:"name"`
|
Name string `yaml:"name"`
|
||||||
Hour string `yaml:"hour"`
|
Hour string `yaml:"hour"`
|
||||||
Minute string `yaml:"minute"`
|
Minute string `yaml:"minute"`
|
||||||
Command string `yaml:"command"`
|
Command string `yaml:"command"`
|
||||||
|
Notification string `yaml:"notification,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type notificationConfig struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
Success notification `yaml:"success,omitempty"`
|
||||||
|
Error notification `yaml:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type notification struct {
|
||||||
|
Gotify gotifyConfig `yaml:"gotify,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type gotifyConfig struct {
|
||||||
|
URL string `yaml:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n notification) SendNotification(job jobconfig, execCode int, outString string, err error) error {
|
||||||
|
if n.Gotify.URL != "" {
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ReadFromFile(path string) (config, error) {
|
func ReadFromFile(path string) (config, error) {
|
||||||
|
|
|
||||||
40
gotify.go
Normal file
40
gotify.go
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Message struct {
|
||||||
|
Title string `json:"title,omitempty"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Priority int `json:"priority,omitempty"`
|
||||||
|
Extras map[string]interface{} `json:"extras,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func SendGotifyNotification(url string, msg Message) error {
|
||||||
|
bodyBytes, err := json.Marshal(msg)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to encode payload: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", url, bytes.NewBuffer(bodyBytes))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("HTTP request error: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("unexpected status: %s", resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
15
jobResult.go
Normal file
15
jobResult.go
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
type jobResult struct {
|
||||||
|
job jobconfig
|
||||||
|
outputString string
|
||||||
|
execCode int
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j jobResult) isSuccess() bool {
|
||||||
|
if j.execCode == 0 && j.err == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
70
main.go
70
main.go
|
|
@ -28,7 +28,7 @@ func main() {
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case t := <-cronTicker:
|
case t := <-cronTicker:
|
||||||
execucteJobs(t, currentConfig.Jobs)
|
execucteJobs(t, currentConfig)
|
||||||
case <-configTicker:
|
case <-configTicker:
|
||||||
log.Debug().Msg("Reload Config")
|
log.Debug().Msg("Reload Config")
|
||||||
currentConfig, err = ReadFromFile("config.yml")
|
currentConfig, err = ReadFromFile("config.yml")
|
||||||
|
|
@ -39,8 +39,8 @@ func main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func execucteJobs(t time.Time, jobs []jobconfig) {
|
func execucteJobs(t time.Time, c config) {
|
||||||
for _, job := range jobs {
|
for _, job := range c.Jobs {
|
||||||
execute, err := job.MatchCurrentTime(t)
|
execute, err := job.MatchCurrentTime(t)
|
||||||
log.Debug().Bool("execution", execute).Time("t", t).Msg("check cron execution")
|
log.Debug().Bool("execution", execute).Time("t", t).Msg("check cron execution")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -50,34 +50,74 @@ func execucteJobs(t time.Time, jobs []jobconfig) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
go executeCommand(job)
|
go executeJob(t, job, c)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func executeCommand(job jobconfig) error {
|
func executeJob(t time.Time, job jobconfig, c config) {
|
||||||
l := log.With().Str("name", job.Name).Str("command", job.Command).Logger()
|
result, err := executeCommand(job)
|
||||||
|
log.Info().Str("name", job.Name).Str("command", job.Command).Time("execute-for", t).Int("execCode", result.execCode).Str("output", result.outputString).Err(result.err).Msg("Done execute task")
|
||||||
|
if err != nil {
|
||||||
|
log.Err(err).Msg("Error while Execute command")
|
||||||
|
}
|
||||||
|
nc := notificationConfig{}
|
||||||
|
for _, n := range c.Notification {
|
||||||
|
if n.Name == job.Notification {
|
||||||
|
nc = n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sendNotification(result, nc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendNotification(result jobResult, notification notificationConfig) error {
|
||||||
|
title := ""
|
||||||
|
msg := Message{}
|
||||||
|
msg.Priority = 10
|
||||||
|
|
||||||
|
if result.isSuccess() {
|
||||||
|
title = "✅ " + result.job.Name
|
||||||
|
msg.Priority = -1
|
||||||
|
} else {
|
||||||
|
title = "❌ " + result.job.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.Message = result.outputString
|
||||||
|
msg.Title = title
|
||||||
|
|
||||||
|
if result.isSuccess() {
|
||||||
|
if notification.Success.Gotify.URL != "" {
|
||||||
|
SendGotifyNotification(notification.Success.Gotify.URL, msg)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if notification.Error.Gotify.URL != "" {
|
||||||
|
SendGotifyNotification(notification.Error.Gotify.URL, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func executeCommand(job jobconfig) (jobResult, error) {
|
||||||
|
j := jobResult{}
|
||||||
|
j.job = job
|
||||||
var output bytes.Buffer
|
var output bytes.Buffer
|
||||||
cmd := exec.Command("bash", "-c", job.Command)
|
cmd := exec.Command("bash", "-c", job.Command)
|
||||||
cmd.Stdout = &output
|
cmd.Stdout = &output
|
||||||
cmd.Stderr = &output
|
cmd.Stderr = &output
|
||||||
l.Debug().Msg("Start Execution")
|
|
||||||
err := cmd.Run()
|
err := cmd.Run()
|
||||||
|
|
||||||
outString := output.String()
|
j.outputString = output.String()
|
||||||
|
|
||||||
exitCode := 0
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||||
if status, ok := exitErr.Sys().(syscall.WaitStatus); ok {
|
if status, ok := exitErr.Sys().(syscall.WaitStatus); ok {
|
||||||
exitCode = status.ExitStatus()
|
j.execCode = status.ExitStatus()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
l.Error().Err(err).Str("output", outString).Msg("Faild Execution")
|
j.err = err
|
||||||
return err
|
return j, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
l.Debug().Err(err).Str("output", outString).Int("exitcode", exitCode).Msg("Success Execution")
|
return j, nil
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue