Skip to content

Commit

Permalink
Expand on_error on_success and change last duration to readable output
Browse files Browse the repository at this point in the history
  • Loading branch information
marshyski committed Dec 2, 2024
1 parent 9d84230 commit 02bcfcd
Show file tree
Hide file tree
Showing 8 changed files with 312 additions and 12 deletions.
19 changes: 16 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
![goreport]
![ci]
![license]
[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/marshyski/pal/badge)](https://scorecard.dev/viewer/?uri=github.com/marshyski/pal)

**A simple API and UI for executing and scheduling system commands or scripts.** Great for webhooks and automating Linux server operations over HTTPS contained in a small binary.

Expand All @@ -28,7 +29,7 @@
- [File Management (Basic Auth)](#file-management-basic-auth)
- [Notifications](#notifications)
- [Crons](#crons)
- [Action](#action)
- [Actions](#actions)
- [Configurations](#configurations)
- [Built-In Variables](#built-in-variables)
- [Env Variables](#env-variables)
Expand Down Expand Up @@ -182,6 +183,17 @@ deploy:
retries: 1
# Pause in seconds before running the next retry
retry_interval: 10
# Run action on_error
run: group/action
# Input for run action on_error
input: $PAL_OUTPUT
on_success:
# Send notification when no errors occurs using built-in vars $PAL_GROUP $PAL_ACTION $PAL_INPUT $PAL_OUTPUT
notification: "deploy failed group=$PAL_GROUP action=$PAL_ACTION input=$PAL_INPUT output=$PAL_OUTPUT"
# Run action when no errors occurs
run: group/action
# Input for run action when no errors occurs
input: $PAL_OUTPUT
# Set list of string tags no format/convention required
tags:
- deploy
Expand Down Expand Up @@ -308,13 +320,14 @@ GET /v1/pal/crons?group={{ group }}&action={{ action }}&run={{ run }}
- `action` (**Optional**): action name
- `run` (**Optional**): keyword "now" is only supported at this time. Runs action now.

### Action
### Actions

Get action configuration including last_output and other run stats.
Get actions configuration including last_output and other run stats.

```js
GET /v1/pal/action?group={{ group }}&action={{ action }}
GET /v1/pal/action?group={{ group }}&action={{ action }}&disabled={{ boolean }}
GET /v1/pal/actions
```

- `group` (**Required**): group name
Expand Down
11 changes: 10 additions & 1 deletion data/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ type OnError struct {
Notification string `yaml:"notification" json:"notification"`
Retries int `yaml:"retries" json:"retries" validate:"number"`
RetryInterval int `yaml:"retry_interval" json:"retry_interval" validate:"number"`
Run string `yaml:"run" json:"run"`
Input string `yaml:"input" json:"input"`
}

type OnSuccess struct {
Notification string `yaml:"notification" json:"notification"`
Run string `yaml:"run" json:"run"`
Input string `yaml:"input" json:"input"`
}

type Container struct {
Expand All @@ -51,13 +59,14 @@ type ActionData struct {
ResponseHeaders []ResponseHeaders `yaml:"headers" json:"headers"`
Crons []string `yaml:"crons" json:"crons"`
OnError OnError `yaml:"on_error" json:"on_error"`
OnSuccess OnSuccess `yaml:"on_success" json:"on_success"`
Input string `yaml:"input" json:"input"`
InputValidate string `yaml:"input_validate" json:"input_validate"`
Tags []string `yaml:"tags" json:"tags"`
LastRan string `yaml:"-" json:"last_ran"`
LastSuccess string `yaml:"-" json:"last_success"`
LastFailure string `yaml:"-" json:"last_failure"`
LastDuration int `yaml:"-" json:"last_duration" validate:"number"`
LastDuration string `yaml:"-" json:"last_duration"`
LastSuccessOutput string `yaml:"-" json:"last_success_output"`
LastFailureOutput string `yaml:"-" json:"last_failure_output"`
Status string `yaml:"-" json:"status"`
Expand Down
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ Documentation: https://github.com/marshyski/pal
e.PUT("/v1/pal/notifications", routes.PutNotifications)
e.GET("/v1/pal/run/:group/:action", routes.RunGroup)
e.POST("/v1/pal/run/:group/:action", routes.RunGroup)
e.GET("/v1/pal/actions", routes.GetActions)
e.GET("/v1/pal/action", routes.GetAction)

// Setup UI Routes Only If Basic Auth Isn't Empty
Expand Down
2 changes: 1 addition & 1 deletion nfpm.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ contents:
dst: /usr/bin/pal
type: symlink

- src: ./pal.yml
- src: ./test/pal.yml
dst: /etc/pal/pal.yml
type: config|noreplace
file_info:
Expand Down
229 changes: 226 additions & 3 deletions routes/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ func RunGroup(c echo.Context) error {
actionData.LastRan = utils.TimeNow(config.GetConfigStr("global_timezone"))
actionData.LastFailure = actionData.LastRan
if actionData.Output {
actionData.LastFailureOutput = err.Error()
actionData.LastFailureOutput = err.Error() + " " + cmdOutput
}
mergeGroup(actionData)
logError("", "", err)
Expand All @@ -301,6 +301,18 @@ func RunGroup(c echo.Context) error {
logError("", "", err)
}
}
if actionData.OnError.Run != "" {
go func() {
errorGroup := strings.Split(actionData.OnError.Run, "/")[0]
errorAction := strings.Split(actionData.OnError.Run, "/")[1]
errorInput := actionData.OnError.Input
errorInput = strings.ReplaceAll(errorInput, "$PAL_GROUP", errorGroup)
errorInput = strings.ReplaceAll(errorInput, "$PAL_ACTION", errorAction)
errorInput = strings.ReplaceAll(errorInput, "$PAL_OUTPUT", actionData.LastFailureOutput)

runBackground(errorGroup, errorAction, errorInput)
}()
}
return
}
actionData.Status = "success"
Expand All @@ -311,6 +323,31 @@ func RunGroup(c echo.Context) error {
actionData.LastSuccessOutput = cmdOutput
}
mergeGroup(actionData)
if actionData.OnSuccess.Notification != "" {
notification := actionData.OnSuccess.Notification
notification = strings.ReplaceAll(notification, "$PAL_GROUP", actionData.Group)
notification = strings.ReplaceAll(notification, "$PAL_ACTION", actionData.Action)
notification = strings.ReplaceAll(notification, "$PAL_INPUT", input)
if actionData.Output {
notification = strings.ReplaceAll(notification, "$PAL_OUTPUT", cmdOutput)
}
err := putNotifications(data.Notification{Group: group, Notification: notification})
if err != nil {
logError("", "", err)
}
}
if actionData.OnSuccess.Run != "" {
go func() {
successGroup := strings.Split(actionData.OnSuccess.Run, "/")[0]
successAction := strings.Split(actionData.OnSuccess.Run, "/")[1]
successInput := actionData.OnSuccess.Input
successInput = strings.ReplaceAll(successInput, "$PAL_GROUP", errorGroup)
successInput = strings.ReplaceAll(successInput, "$PAL_ACTION", errorAction)
successInput = strings.ReplaceAll(successInput, "$PAL_OUTPUT", cmdOutput)

runBackground(successGroup, successAction, successInput)
}()
}
}()

if !actionData.Concurrent {
Expand All @@ -331,7 +368,7 @@ func RunGroup(c echo.Context) error {
actionData.LastRan = utils.TimeNow(config.GetConfigStr("global_timezone"))
actionData.LastFailure = actionData.LastRan
if actionData.Output {
actionData.LastFailureOutput = err.Error()
actionData.LastFailureOutput = err.Error() + " " + cmdOutput
}
mergeGroup(actionData)
logError(c.Response().Header().Get(echo.HeaderXRequestID), c.Request().RequestURI, errors.New(errorScript+" "+err.Error()))
Expand All @@ -348,6 +385,18 @@ func RunGroup(c echo.Context) error {
logError(c.Response().Header().Get(echo.HeaderXRequestID), c.Request().RequestURI, err)
}
}
if actionData.OnError.Run != "" {
go func() {
errorGroup := strings.Split(actionData.OnError.Run, "/")[0]
errorAction := strings.Split(actionData.OnError.Run, "/")[1]
errorInput := actionData.OnError.Input
errorInput = strings.ReplaceAll(errorInput, "$PAL_GROUP", errorGroup)
errorInput = strings.ReplaceAll(errorInput, "$PAL_ACTION", errorAction)
errorInput = strings.ReplaceAll(errorInput, "$PAL_OUTPUT", actionData.LastFailureOutput)

runBackground(errorGroup, errorAction, errorInput)
}()
}
return c.String(http.StatusInternalServerError, err.Error())
}

Expand All @@ -365,6 +414,31 @@ func RunGroup(c echo.Context) error {
actionData.LastSuccessOutput = cmdOutput
}
mergeGroup(actionData)
if actionData.OnSuccess.Notification != "" {
notification := actionData.OnSuccess.Notification
notification = strings.ReplaceAll(notification, "$PAL_GROUP", actionData.Group)
notification = strings.ReplaceAll(notification, "$PAL_ACTION", actionData.Action)
notification = strings.ReplaceAll(notification, "$PAL_INPUT", input)
if actionData.Output {
notification = strings.ReplaceAll(notification, "$PAL_OUTPUT", cmdOutput)
}
err := putNotifications(data.Notification{Group: group, Notification: notification})
if err != nil {
logError("", "", err)
}
}
if actionData.OnSuccess.Run != "" {
go func() {
successGroup := strings.Split(actionData.OnSuccess.Run, "/")[0]
successAction := strings.Split(actionData.OnSuccess.Run, "/")[1]
successInput := actionData.OnSuccess.Input
successInput = strings.ReplaceAll(successInput, "$PAL_GROUP", errorGroup)
successInput = strings.ReplaceAll(successInput, "$PAL_ACTION", errorAction)
successInput = strings.ReplaceAll(successInput, "$PAL_OUTPUT", cmdOutput)

runBackground(successGroup, successAction, successInput)
}()
}
return c.String(http.StatusOK, cmdOutput)
}

Expand Down Expand Up @@ -820,6 +894,19 @@ func GetSystemPage(c echo.Context) error {
return c.Render(http.StatusOK, "system.tmpl", uiData)
}

func GetActions(c echo.Context) error {
if !sessionValid(c) {
return c.Redirect(http.StatusSeeOther, "/v1/pal/ui/login")
}
res := db.DBC.GetGroups()
var actionsSlice []data.ActionData
for _, actions := range res {
actionsSlice = append(actionsSlice, actions...)
}

return c.JSON(http.StatusOK, actionsSlice)
}

func GetActionsPage(c echo.Context) error {
if !sessionValid(c) {
return c.Redirect(http.StatusSeeOther, "/v1/pal/ui/login")
Expand Down Expand Up @@ -1109,9 +1196,35 @@ func cronTask(res data.ActionData) string {
res.LastRan = timeNow
res.LastFailure = timeNow
if res.Output {
res.LastFailureOutput = cmdOutput
res.LastFailureOutput = err.Error() + " " + cmdOutput
}
mergeGroup(res)
logError("", "", err)
if res.OnError.Notification != "" {
notification := res.OnError.Notification
notification = strings.ReplaceAll(notification, "$PAL_GROUP", res.Group)
notification = strings.ReplaceAll(notification, "$PAL_ACTION", res.Action)
notification = strings.ReplaceAll(notification, "$PAL_INPUT", res.Input)
if res.Output {
notification = strings.ReplaceAll(notification, "$PAL_OUTPUT", res.LastFailureOutput)
}
err := putNotifications(data.Notification{Group: res.Group, Notification: notification})
if err != nil {
logError("", "", err)
}
}
if res.OnError.Run != "" {
go func() {
errorGroup := strings.Split(res.OnError.Run, "/")[0]
errorAction := strings.Split(res.OnError.Run, "/")[1]
errorInput := res.OnError.Input
errorInput = strings.ReplaceAll(errorInput, "$PAL_GROUP", errorGroup)
errorInput = strings.ReplaceAll(errorInput, "$PAL_ACTION", errorAction)
errorInput = strings.ReplaceAll(errorInput, "$PAL_OUTPUT", res.LastFailureOutput)

runBackground(errorGroup, errorAction, errorInput)
}()
}
return err.Error()
}

Expand All @@ -1126,6 +1239,31 @@ func cronTask(res data.ActionData) string {
res.LastSuccessOutput = cmdOutput
}
mergeGroup(res)
if res.OnSuccess.Notification != "" {
notification := res.OnSuccess.Notification
notification = strings.ReplaceAll(notification, "$PAL_GROUP", res.Group)
notification = strings.ReplaceAll(notification, "$PAL_ACTION", res.Action)
notification = strings.ReplaceAll(notification, "$PAL_INPUT", res.Input)
if res.Output {
notification = strings.ReplaceAll(notification, "$PAL_OUTPUT", cmdOutput)
}
err := putNotifications(data.Notification{Group: res.Group, Notification: notification})
if err != nil {
logError("", "", err)
}
}
if res.OnSuccess.Run != "" {
go func() {
successGroup := strings.Split(res.OnSuccess.Run, "/")[0]
successAction := strings.Split(res.OnSuccess.Run, "/")[1]
successInput := res.OnSuccess.Input
successInput = strings.ReplaceAll(successInput, "$PAL_GROUP", errorGroup)
successInput = strings.ReplaceAll(successInput, "$PAL_ACTION", errorAction)
successInput = strings.ReplaceAll(successInput, "$PAL_OUTPUT", cmdOutput)

runBackground(successGroup, successAction, successInput)
}()
}
return cmdOutput
}

Expand Down Expand Up @@ -1260,3 +1398,88 @@ func cmdString(actionData data.ActionData, input, req string) string {
// TODO: Add Debug cmd output
return cmd
}

func runBackground(group, action, input string) {
actionData := db.DBC.GetGroupAction(group, action)
origCmd := actionData.Cmd
actionData.Cmd = cmdString(actionData, input, "")
cmdOutput, duration, err := utils.CmdRun(actionData, config.GetConfigStr("global_cmd_prefix"), config.GetConfigStr("global_working_dir"))
actionData.Cmd = origCmd
if err != nil {
if !actionData.Concurrent {
lock(actionData.Group, actionData.Action, false)
}
actionData.Status = "error"
actionData.LastDuration = duration
actionData.LastRan = utils.TimeNow(config.GetConfigStr("global_timezone"))
actionData.LastFailure = actionData.LastRan
if actionData.Output {
actionData.LastFailureOutput = err.Error() + " " + cmdOutput
}
mergeGroup(actionData)
logError("", "", err)
if actionData.OnError.Notification != "" {
notification := actionData.OnError.Notification
notification = strings.ReplaceAll(notification, "$PAL_GROUP", actionData.Group)
notification = strings.ReplaceAll(notification, "$PAL_ACTION", actionData.Action)
notification = strings.ReplaceAll(notification, "$PAL_INPUT", input)
if actionData.Output {
notification = strings.ReplaceAll(notification, "$PAL_OUTPUT", actionData.LastFailureOutput)
}
err := putNotifications(data.Notification{Group: actionData.Group, Notification: notification})
if err != nil {
logError("", "", err)
}
}
if actionData.OnError.Run != "" {
go func() {
errorGroup := strings.Split(actionData.OnError.Run, "/")[0]
errorAction := strings.Split(actionData.OnError.Run, "/")[1]
errorInput := actionData.OnError.Input
errorInput = strings.ReplaceAll(errorInput, "$PAL_GROUP", errorGroup)
errorInput = strings.ReplaceAll(errorInput, "$PAL_ACTION", errorAction)
errorInput = strings.ReplaceAll(errorInput, "$PAL_OUTPUT", actionData.LastFailureOutput)

runBackground(errorGroup, errorAction, errorInput)
}()
}
return
}
actionData.Status = "success"
actionData.LastDuration = duration
actionData.LastRan = utils.TimeNow(config.GetConfigStr("global_timezone"))
actionData.LastSuccess = actionData.LastRan
if actionData.Output {
actionData.LastSuccessOutput = cmdOutput
}
mergeGroup(actionData)
if actionData.OnSuccess.Notification != "" {
notification := actionData.OnSuccess.Notification
notification = strings.ReplaceAll(notification, "$PAL_GROUP", actionData.Group)
notification = strings.ReplaceAll(notification, "$PAL_ACTION", actionData.Action)
notification = strings.ReplaceAll(notification, "$PAL_INPUT", input)
if actionData.Output {
notification = strings.ReplaceAll(notification, "$PAL_OUTPUT", cmdOutput)
}
err := putNotifications(data.Notification{Group: group, Notification: notification})
if err != nil {
logError("", "", err)
}
}
if actionData.OnSuccess.Run != "" {
go func() {
successGroup := strings.Split(actionData.OnSuccess.Run, "/")[0]
successAction := strings.Split(actionData.OnSuccess.Run, "/")[1]
successInput := actionData.OnSuccess.Input
successInput = strings.ReplaceAll(successInput, "$PAL_GROUP", errorGroup)
successInput = strings.ReplaceAll(successInput, "$PAL_ACTION", errorAction)
successInput = strings.ReplaceAll(successInput, "$PAL_OUTPUT", cmdOutput)

runBackground(successGroup, successAction, successInput)
}()
}

if !actionData.Concurrent {
lock(actionData.Group, actionData.Action, false)
}
}
Loading

0 comments on commit 02bcfcd

Please sign in to comment.