add working command hooks
Some checks failed
ci/woodpecker/push/go-lint Pipeline failed

This commit is contained in:
Andrew Woodlee 2024-11-11 22:44:28 -06:00
parent feacb83274
commit b8a63f39f5
6 changed files with 189 additions and 58 deletions

View File

@ -20,7 +20,7 @@ package cmd
// func config(cmd *cobra.Command, args []string) { // func config(cmd *cobra.Command, args []string) {
// opts := backy.NewOpts(cfgFile, backy.UseCron()) // opts := backy.NewOpts(cfgFile, backy.cronEnabled())
// opts.InitConfig() // opts.InitConfig()
// } // }

View File

@ -17,7 +17,7 @@ var (
func cron(cmd *cobra.Command, args []string) { func cron(cmd *cobra.Command, args []string) {
opts := backy.NewOpts(cfgFile, backy.UseCron()) opts := backy.NewOpts(cfgFile, backy.CronEnabled())
opts.InitConfig() opts.InitConfig()
backy.ReadConfig(opts) backy.ReadConfig(opts)
opts.Cron() opts.Cron()

View File

@ -30,6 +30,8 @@ var Sprintf = fmt.Sprintf
// The environment of local commands will be the machine's environment plus any extra // The environment of local commands will be the machine's environment plus any extra
// variables specified in the Env file or Environment. // variables specified in the Env file or Environment.
// Dir can also be specified for local commands. // Dir can also be specified for local commands.
//
// Returns the output as a slice and an error, if any
func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([]string, error) { func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([]string, error) {
var ( var (
@ -53,9 +55,9 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([
command.Type = strings.TrimSpace(command.Type) command.Type = strings.TrimSpace(command.Type)
if command.Type != "" { if command.Type != "" {
cmdCtxLogger.Info().Str("Command", fmt.Sprintf("Running script %s on host %s", command.Cmd, *command.Host)).Send() cmdCtxLogger.Info().Str("Command", fmt.Sprintf("Running script %s on host %s", command.Name, *command.Host)).Send()
} else { } else {
cmdCtxLogger.Info().Str("Command", fmt.Sprintf("Running command %s %s on host %s", command.Cmd, ArgsStr, *command.Host)).Send() cmdCtxLogger.Info().Str("Command", fmt.Sprintf("Running command %s on host %s", command.Name, *command.Host)).Send()
} }
if command.RemoteHost.SshClient == nil { if command.RemoteHost.SshClient == nil {
@ -165,7 +167,7 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([
outScanner := bufio.NewScanner(&cmdOutBuf) outScanner := bufio.NewScanner(&cmdOutBuf)
for outScanner.Scan() { for outScanner.Scan() {
outMap := make(map[string]interface{}) outMap := make(map[string]interface{})
outMap["cmd"] = cmd outMap["cmd"] = command.Name
outMap["output"] = outScanner.Text() outMap["output"] = outScanner.Text()
if str, ok := outMap["output"].(string); ok { if str, ok := outMap["output"].(string); ok {
outputArr = append(outputArr, str) outputArr = append(outputArr, str)
@ -178,7 +180,7 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([
outScanner := bufio.NewScanner(&cmdOutBuf) outScanner := bufio.NewScanner(&cmdOutBuf)
for outScanner.Scan() { for outScanner.Scan() {
outMap := make(map[string]interface{}) outMap := make(map[string]interface{})
outMap["cmd"] = cmd outMap["cmd"] = command.Name
outMap["output"] = outScanner.Text() outMap["output"] = outScanner.Text()
if str, ok := outMap["output"].(string); ok { if str, ok := outMap["output"].(string); ok {
outputArr = append(outputArr, str) outputArr = append(outputArr, str)
@ -288,7 +290,7 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([
outScanner := bufio.NewScanner(&cmdOutBuf) outScanner := bufio.NewScanner(&cmdOutBuf)
for outScanner.Scan() { for outScanner.Scan() {
outMap := make(map[string]interface{}) outMap := make(map[string]interface{})
outMap["cmd"] = cmd outMap["cmd"] = command.Name
outMap["output"] = outScanner.Text() outMap["output"] = outScanner.Text()
if str, ok := outMap["output"].(string); ok { if str, ok := outMap["output"].(string); ok {
outputArr = append(outputArr, str) outputArr = append(outputArr, str)
@ -297,14 +299,14 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([
} }
if err != nil { if err != nil {
cmdCtxLogger.Error().Err(fmt.Errorf("error when running cmd: %s: %w", command.Cmd, err)).Send() cmdCtxLogger.Error().Err(fmt.Errorf("error when running cmd: %s: %w", command.Name, err)).Send()
return outputArr, err return outputArr, err
} }
} else { } else {
var err error var err error
if command.Shell != "" { if command.Shell != "" {
cmdCtxLogger.Info().Str("Command", fmt.Sprintf("Running command %s %s on local machine in %s", command.Cmd, ArgsStr, command.Shell)).Send() cmdCtxLogger.Info().Str("Command", fmt.Sprintf("Running command %s on local machine in %s", command.Name, command.Shell)).Send()
ArgsStr = fmt.Sprintf("%s %s", command.Cmd, ArgsStr) ArgsStr = fmt.Sprintf("%s %s", command.Cmd, ArgsStr)
@ -330,7 +332,7 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([
for outScanner.Scan() { for outScanner.Scan() {
outMap := make(map[string]interface{}) outMap := make(map[string]interface{})
outMap["cmd"] = command.Cmd outMap["cmd"] = command.Name
outMap["output"] = outScanner.Text() outMap["output"] = outScanner.Text()
if str, ok := outMap["output"].(string); ok { if str, ok := outMap["output"].(string); ok {
outputArr = append(outputArr, str) outputArr = append(outputArr, str)
@ -339,13 +341,13 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([
} }
if err != nil { if err != nil {
cmdCtxLogger.Error().Err(fmt.Errorf("error when running cmd %s: %w", command.Cmd, err)).Send() cmdCtxLogger.Error().Err(fmt.Errorf("error when running cmd %s: %w", command.Name, err)).Send()
return outputArr, err return outputArr, err
} }
return outputArr, nil return outputArr, nil
} }
cmdCtxLogger.Info().Str("Command", fmt.Sprintf("Running command %s %s on local machine", command.Cmd, ArgsStr)).Send() cmdCtxLogger.Info().Str("Command", fmt.Sprintf("Running command %s on local machine", command.Name)).Send()
localCMD := exec.Command(command.Cmd, command.Args...) localCMD := exec.Command(command.Cmd, command.Args...)
@ -379,7 +381,7 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([
cmdCtxLogger.Info().Fields(outMap).Send() cmdCtxLogger.Info().Fields(outMap).Send()
} }
if err != nil { if err != nil {
cmdCtxLogger.Error().Err(fmt.Errorf("error when running cmd %s: %w", command.Cmd, err)).Send() cmdCtxLogger.Error().Err(fmt.Errorf("error when running cmd %s: %w", command.Name, err)).Send()
return outputArr, err return outputArr, err
} }
} }
@ -388,9 +390,9 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([
// cmdListWorker // cmdListWorker
func cmdListWorker(msgTemps *msgTemplates, jobs <-chan *CmdList, results chan<- string, opts *ConfigOpts) { func cmdListWorker(msgTemps *msgTemplates, jobs <-chan *CmdList, results chan<- string, opts *ConfigOpts) {
// iterate over list to run // iterate over list to run
for list := range jobs { for list := range jobs {
res := CmdListResults{}
fieldsMap := make(map[string]interface{}) fieldsMap := make(map[string]interface{})
fieldsMap["list"] = list.Name fieldsMap["list"] = list.Name
@ -401,9 +403,10 @@ func cmdListWorker(msgTemps *msgTemplates, jobs <-chan *CmdList, results chan<-
var outStructArr []outStruct // stores output messages var outStructArr []outStruct // stores output messages
for _, cmd := range list.Order { for _, cmd := range list.Order {
currentCmd := opts.Cmds[cmd].Cmd
fieldsMap["cmd"] = opts.Cmds[cmd].Cmd currentCmd := opts.Cmds[cmd].Name
fieldsMap["cmd"] = opts.Cmds[cmd].Name
cmdToRun := opts.Cmds[cmd] cmdToRun := opts.Cmds[cmd]
cmdLog.Fields(fieldsMap).Send() cmdLog.Fields(fieldsMap).Send()
@ -418,12 +421,13 @@ func cmdListWorker(msgTemps *msgTemplates, jobs <-chan *CmdList, results chan<-
} }
outputArr, runOutErr := cmdToRun.RunCmd(cmdLogger, opts) outputArr, runOutErr := cmdToRun.RunCmd(cmdLogger, opts)
if list.NotifyConfig != nil { if list.NotifyConfig != nil {
// check if the command output should be included // check if the command output should be included
if cmdToRun.GetOutput || list.GetOutput { if cmdToRun.GetOutput || list.GetOutput {
outputStruct := outStruct{ outputStruct := outStruct{
CmdName: cmd, CmdName: cmdToRun.Name,
CmdExecuted: currentCmd, CmdExecuted: currentCmd,
Output: outputArr, Output: outputArr,
} }
@ -434,6 +438,7 @@ func cmdListWorker(msgTemps *msgTemplates, jobs <-chan *CmdList, results chan<-
} }
count++ count++
if runOutErr != nil { if runOutErr != nil {
res.ErrCmd = cmd
if list.NotifyConfig != nil { if list.NotifyConfig != nil {
var errMsg bytes.Buffer var errMsg bytes.Buffer
errStruct := make(map[string]interface{}) errStruct := make(map[string]interface{})
@ -466,8 +471,9 @@ func cmdListWorker(msgTemps *msgTemplates, jobs <-chan *CmdList, results chan<-
break break
} else { } else {
if count == len(list.Order) {
cmdsRan = append(cmdsRan, cmd) cmdsRan = append(cmdsRan, cmd)
if count == len(list.Order) {
var successMsg bytes.Buffer var successMsg bytes.Buffer
// if notification config is not nil, and NotifyOnSuccess is true or GetOuput is true, // if notification config is not nil, and NotifyOnSuccess is true or GetOuput is true,
@ -487,19 +493,17 @@ func cmdListWorker(msgTemps *msgTemplates, jobs <-chan *CmdList, results chan<-
break break
} }
err := list.NotifyConfig.Send(context.Background(), fmt.Sprintf("List %s succeded", list.Name), successMsg.String()) err := list.NotifyConfig.Send(context.Background(), fmt.Sprintf("List %s succeeded", list.Name), successMsg.String())
if err != nil { if err != nil {
opts.Logger.Err(err).Send() opts.Logger.Err(err).Send()
} }
} }
} else {
cmdsRan = append(cmdsRan, cmd)
} }
} }
} }
results <- "done" results <- res.ErrCmd
} }
} }
@ -534,8 +538,20 @@ func (opts *ConfigOpts) RunListConfig(cron string) {
} }
close(listChan) close(listChan)
for a := 1; a <= configListsLen; a++ { for a := 0; a <= configListsLen; a++ {
<-results l := <-results
opts.Logger.Debug().Msg(l)
if l != "" {
// execute error hooks
opts.Logger.Debug().Msg("hooks are working")
} else {
// execute success hooks
}
// execute final hooks
} }
opts.closeHostConnections() opts.closeHostConnections()
@ -550,7 +566,14 @@ func (config *ConfigOpts) ExecuteCmds(opts *ConfigOpts) {
_, runErr := cmdToRun.RunCmd(cmdLogger, opts) _, runErr := cmdToRun.RunCmd(cmdLogger, opts)
if runErr != nil { if runErr != nil {
opts.Logger.Err(runErr).Send() opts.Logger.Err(runErr).Send()
ExecuteHooks(*cmdToRun, "error", opts)
} else {
ExecuteHooks(*cmdToRun, "success", opts)
} }
ExecuteHooks(*cmdToRun, "final", opts)
} }
opts.closeHostConnections() opts.closeHostConnections()
@ -593,3 +616,17 @@ func (c *ConfigOpts) closeHostConnections() {
} }
} }
} }
func ExecuteHooks(cmd Command, hookType string, opts *ConfigOpts) {
switch hookType {
case "error":
for _, v := range cmd.Hooks.Error {
errCmd := opts.Cmds[v]
cmdLogger := opts.Logger.With().
Str("backy-cmd", v).
Logger()
errCmd.RunCmd(cmdLogger, opts)
}
}
}

View File

@ -245,6 +245,7 @@ func ReadConfig(opts *ConfigOpts) *ConfigOpts {
cmdListFilePath := path.Clean(opts.CmdListFile) cmdListFilePath := path.Clean(opts.CmdListFile)
// if path is not absolute, check config directory
if !strings.HasPrefix(cmdListFilePath, "/") { if !strings.HasPrefix(cmdListFilePath, "/") {
opts.CmdListFile = path.Join(backyConfigFileDir, cmdListFilePath) opts.CmdListFile = path.Join(backyConfigFileDir, cmdListFilePath)
} }
@ -259,7 +260,7 @@ func ReadConfig(opts *ConfigOpts) *ConfigOpts {
logging.ExitWithMSG(fmt.Sprintf("error loading config: %v", err), 1, &opts.Logger) logging.ExitWithMSG(fmt.Sprintf("error loading config: %v", err), 1, &opts.Logger)
} }
log.Info().Str("lists config file", opts.CmdListFile).Send() log.Info().Str("using lists config file", opts.CmdListFile).Send()
} }
@ -269,7 +270,7 @@ func ReadConfig(opts *ConfigOpts) *ConfigOpts {
var cmdNotFoundSliceErr []error var cmdNotFoundSliceErr []error
for cmdListName, cmdList := range opts.CmdConfigLists { for cmdListName, cmdList := range opts.CmdConfigLists {
if opts.useCron { if opts.cronEnabled {
cron := strings.TrimSpace(cmdList.Cron) cron := strings.TrimSpace(cmdList.Cron)
if cron == "" { if cron == "" {
delete(opts.CmdConfigLists, cmdListName) delete(opts.CmdConfigLists, cmdListName)
@ -291,23 +292,20 @@ func ReadConfig(opts *ConfigOpts) *ConfigOpts {
cmdNotFoundErrorLog.Errs("commands not found", cmdNotFoundSliceErr).Send() cmdNotFoundErrorLog.Errs("commands not found", cmdNotFoundSliceErr).Send()
} }
if opts.useCron && (len(opts.CmdConfigLists) == 0) { if opts.cronEnabled && (len(opts.CmdConfigLists) == 0) {
logging.ExitWithMSG("No cron fields detected in any command lists", 1, nil) logging.ExitWithMSG("No cron fields detected in any command lists", 1, nil)
} }
for c := range opts.Cmds { // process commands
if opts.executeCmds != nil && !contains(opts.executeCmds, c) { if err := processCmds(opts); err != nil {
delete(opts.Cmds, c) log.Panic().Err(err).Send()
}
} }
if len(opts.executeLists) > 0 {
for l := range opts.CmdConfigLists { for l := range opts.CmdConfigLists {
if !contains(opts.executeLists, l) { if !contains(opts.executeLists, l) {
delete(opts.CmdConfigLists, l) delete(opts.CmdConfigLists, l)
} }
} }
}
if backyKoanf.Exists("notifications") { if backyKoanf.Exists("notifications") {
@ -317,23 +315,8 @@ func ReadConfig(opts *ConfigOpts) *ConfigOpts {
} }
} }
for _, cmd := range opts.Cmds {
if cmd.Host != nil {
host, hostFound := opts.Hosts[*cmd.Host]
if hostFound {
cmd.RemoteHost = host
cmd.RemoteHost.Host = host.Host
if host.HostName != "" {
cmd.RemoteHost.HostName = host.HostName
}
} else {
opts.Hosts[*cmd.Host] = &Host{Host: *cmd.Host}
cmd.RemoteHost = &Host{Host: *cmd.Host}
}
}
}
opts.SetupNotify() opts.SetupNotify()
if err := opts.setupVault(); err != nil { if err := opts.setupVault(); err != nil {
log.Err(err).Send() log.Err(err).Send()
} }
@ -460,3 +443,94 @@ func GetVaultKey(str string, opts *ConfigOpts, log zerolog.Logger) string {
} }
return value return value
} }
func processCmds(opts *ConfigOpts) error {
// process commands
for cmdName, cmd := range opts.Cmds {
cmd.hookRefs = map[string]map[string]*Command{}
cmd.hookRefs["error"] = map[string]*Command{}
cmd.hookRefs["success"] = map[string]*Command{}
if cmd.Name == "" {
cmd.Name = cmdName
}
// println("Cmd.Name = " + cmd.Name)
hooks := cmd.Hooks
// resolve hooks
if hooks != nil {
opts.Logger.Debug().Msg("Hooks found")
errHook, hookRefs, processHookErr := processHooks(hooks.Error, opts.Cmds, "error")
if !processHookErr {
return fmt.Errorf("error in command %s hook list: hook command %s not found", cmd.Name, errHook)
}
cmd.hookRefs["error"] = hookRefs
successHook, SuccessHookRefs, processHookSuccess := processHooks(hooks.Error, opts.Cmds, "error")
if !processHookSuccess {
return fmt.Errorf("error in command %s hook list: hook command %s not found", cmd.Name, successHook)
}
cmd.hookRefs["success"] = SuccessHookRefs
}
// resolve hosts
if cmd.Host != nil {
host, hostFound := opts.Hosts[*cmd.Host]
if hostFound {
cmd.RemoteHost = host
cmd.RemoteHost.Host = host.Host
if host.HostName != "" {
cmd.RemoteHost.HostName = host.HostName
}
} else {
opts.Hosts[*cmd.Host] = &Host{Host: *cmd.Host}
cmd.RemoteHost = &Host{Host: *cmd.Host}
}
}
}
return nil
}
// processHooks evaluates if hooks are valid Commands
//
// Takes the following arguments:
//
// 1. a []string of hooks
// 2. a map of Commands as arguments
// 3. a string hookType, must be the hook type
//
// The cmds.hookRef is modified in this function.
//
// Returns the following:
//
// 1. command string
// 2. each hook type's command map
// 2. a bool which determines if the command is valid
func processHooks(hooks []string, cmds map[string]*Command, hookType string) (hook string, hookRefs map[string]*Command, hookCmdFound bool) {
// fmt.Printf("%v\n", hooks)
// for _, v := range cmds {
// fmt.Printf("CmdName=%v\n", v.Name)
// fmt.Printf("Cmd=%v\n", v.Cmd)
// }
// initialize hook type
hookRefs = make(map[string]*Command)
// hookRefs[hookType] = map[string]*Command{}
for _, hook = range hooks {
var hookCmd *Command
// TODO: match by Command.Name
hookCmd, hookCmdFound = cmds[hook]
if !hookCmdFound {
return
}
hookRefs[hook] = hookCmd
// Recursive, decide if this is good
// if hookCmd.hookRefs == nil {
// }
// hookRef[hookType][h] = hookCmd
}
return
}

View File

@ -43,17 +43,24 @@ type (
} }
Command struct { Command struct {
Name string `yaml:"name,omitempty"`
// command to run // command to run
Cmd string `yaml:"cmd"` Cmd string `yaml:"cmd"`
// Possible values: script, scriptFile // Possible values: script, scriptFile
// If blank, it is regualar command. // If blank, it is regular command.
Type string `yaml:"type"` Type string `yaml:"type,omitempty"`
// host on which to run cmd // host on which to run cmd
Host *string `yaml:"host,omitempty"` Host *string `yaml:"host,omitempty"`
// Hooks are for running commands on certain events
Hooks *Hooks `yaml:"hooks,omitempty"`
// hook refs are internal references of commands for each hook type
hookRefs map[string]map[string]*Command
/* /*
Shell specifies which shell to run the command in, if any. Shell specifies which shell to run the command in, if any.
Not applicable when host is defined. Not applicable when host is defined.
@ -123,7 +130,7 @@ type (
CmdListFile string CmdListFile string
// use command lists using cron // use command lists using cron
useCron bool cronEnabled bool
// Holds commands to execute for the exec command // Holds commands to execute for the exec command
executeCmds []string executeCmds []string
// Holds lists to execute for the backup command // Holds lists to execute for the backup command
@ -188,4 +195,17 @@ type (
Commands []string Commands []string
Hosts []string Hosts []string
} }
Hooks struct {
Error []string `yaml:"error,omitempty"`
SuccessHooks []string `yaml:"success,omitempty"`
FinalHooks []string `yaml:"final,omitempty"`
}
CmdListResults struct {
// name of the list
ListName string
// command that caused the list to fail
ErrCmd string
}
) )

View File

@ -56,10 +56,10 @@ func SetCmdsToSearch(cmds []string) BackyOptionFunc {
} }
} }
// UseCron enables the execution of command lists at specified times // cronEnabled enables the execution of command lists at specified times
func UseCron() BackyOptionFunc { func CronEnabled() BackyOptionFunc {
return func(bco *ConfigOpts) { return func(bco *ConfigOpts) {
bco.useCron = true bco.cronEnabled = true
} }
} }