From b8a63f39f50ddf1a402acdb1eb698ae5c2e7b5f6 Mon Sep 17 00:00:00 2001 From: Andrew Woodlee Date: Mon, 11 Nov 2024 22:44:28 -0600 Subject: [PATCH] add working command hooks --- cmd/config.go | 2 +- cmd/cron.go | 2 +- pkg/backy/backy.go | 81 +++++++++++++++++++-------- pkg/backy/config.go | 130 ++++++++++++++++++++++++++++++++++---------- pkg/backy/types.go | 26 ++++++++- pkg/backy/utils.go | 6 +- 6 files changed, 189 insertions(+), 58 deletions(-) diff --git a/cmd/config.go b/cmd/config.go index 80c3da1..53f212f 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -20,7 +20,7 @@ package cmd // func config(cmd *cobra.Command, args []string) { -// opts := backy.NewOpts(cfgFile, backy.UseCron()) +// opts := backy.NewOpts(cfgFile, backy.cronEnabled()) // opts.InitConfig() // } diff --git a/cmd/cron.go b/cmd/cron.go index 27ac338..2f56699 100644 --- a/cmd/cron.go +++ b/cmd/cron.go @@ -17,7 +17,7 @@ var ( func cron(cmd *cobra.Command, args []string) { - opts := backy.NewOpts(cfgFile, backy.UseCron()) + opts := backy.NewOpts(cfgFile, backy.CronEnabled()) opts.InitConfig() backy.ReadConfig(opts) opts.Cron() diff --git a/pkg/backy/backy.go b/pkg/backy/backy.go index 6a149b4..a513ab5 100644 --- a/pkg/backy/backy.go +++ b/pkg/backy/backy.go @@ -30,6 +30,8 @@ var Sprintf = fmt.Sprintf // The environment of local commands will be the machine's environment plus any extra // variables specified in the Env file or Environment. // 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) { var ( @@ -53,9 +55,9 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([ command.Type = strings.TrimSpace(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 { - 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 { @@ -165,7 +167,7 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([ outScanner := bufio.NewScanner(&cmdOutBuf) for outScanner.Scan() { outMap := make(map[string]interface{}) - outMap["cmd"] = cmd + outMap["cmd"] = command.Name outMap["output"] = outScanner.Text() if str, ok := outMap["output"].(string); ok { outputArr = append(outputArr, str) @@ -178,7 +180,7 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([ outScanner := bufio.NewScanner(&cmdOutBuf) for outScanner.Scan() { outMap := make(map[string]interface{}) - outMap["cmd"] = cmd + outMap["cmd"] = command.Name outMap["output"] = outScanner.Text() if str, ok := outMap["output"].(string); ok { outputArr = append(outputArr, str) @@ -288,7 +290,7 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([ outScanner := bufio.NewScanner(&cmdOutBuf) for outScanner.Scan() { outMap := make(map[string]interface{}) - outMap["cmd"] = cmd + outMap["cmd"] = command.Name outMap["output"] = outScanner.Text() if str, ok := outMap["output"].(string); ok { outputArr = append(outputArr, str) @@ -297,14 +299,14 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([ } 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 } } else { var err error 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) @@ -330,7 +332,7 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([ for outScanner.Scan() { outMap := make(map[string]interface{}) - outMap["cmd"] = command.Cmd + outMap["cmd"] = command.Name outMap["output"] = outScanner.Text() if str, ok := outMap["output"].(string); ok { outputArr = append(outputArr, str) @@ -339,13 +341,13 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([ } 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, 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...) @@ -379,7 +381,7 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([ cmdCtxLogger.Info().Fields(outMap).Send() } 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 } } @@ -388,9 +390,9 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([ // cmdListWorker func cmdListWorker(msgTemps *msgTemplates, jobs <-chan *CmdList, results chan<- string, opts *ConfigOpts) { - // iterate over list to run for list := range jobs { + res := CmdListResults{} fieldsMap := make(map[string]interface{}) fieldsMap["list"] = list.Name @@ -401,9 +403,10 @@ func cmdListWorker(msgTemps *msgTemplates, jobs <-chan *CmdList, results chan<- var outStructArr []outStruct // stores output messages 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] cmdLog.Fields(fieldsMap).Send() @@ -418,12 +421,13 @@ func cmdListWorker(msgTemps *msgTemplates, jobs <-chan *CmdList, results chan<- } outputArr, runOutErr := cmdToRun.RunCmd(cmdLogger, opts) + if list.NotifyConfig != nil { // check if the command output should be included if cmdToRun.GetOutput || list.GetOutput { outputStruct := outStruct{ - CmdName: cmd, + CmdName: cmdToRun.Name, CmdExecuted: currentCmd, Output: outputArr, } @@ -434,6 +438,7 @@ func cmdListWorker(msgTemps *msgTemplates, jobs <-chan *CmdList, results chan<- } count++ if runOutErr != nil { + res.ErrCmd = cmd if list.NotifyConfig != nil { var errMsg bytes.Buffer errStruct := make(map[string]interface{}) @@ -466,8 +471,9 @@ func cmdListWorker(msgTemps *msgTemplates, jobs <-chan *CmdList, results chan<- break } else { + cmdsRan = append(cmdsRan, cmd) + if count == len(list.Order) { - cmdsRan = append(cmdsRan, cmd) var successMsg bytes.Buffer // 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 } - 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 { 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) - for a := 1; a <= configListsLen; a++ { - <-results + for a := 0; a <= configListsLen; a++ { + 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() @@ -550,7 +566,14 @@ func (config *ConfigOpts) ExecuteCmds(opts *ConfigOpts) { _, runErr := cmdToRun.RunCmd(cmdLogger, opts) if runErr != nil { opts.Logger.Err(runErr).Send() + + ExecuteHooks(*cmdToRun, "error", opts) + } else { + + ExecuteHooks(*cmdToRun, "success", opts) } + + ExecuteHooks(*cmdToRun, "final", opts) } 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) + } + + } +} diff --git a/pkg/backy/config.go b/pkg/backy/config.go index 3663bd7..439a76e 100644 --- a/pkg/backy/config.go +++ b/pkg/backy/config.go @@ -245,6 +245,7 @@ func ReadConfig(opts *ConfigOpts) *ConfigOpts { cmdListFilePath := path.Clean(opts.CmdListFile) + // if path is not absolute, check config directory if !strings.HasPrefix(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) } - 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 for cmdListName, cmdList := range opts.CmdConfigLists { - if opts.useCron { + if opts.cronEnabled { cron := strings.TrimSpace(cmdList.Cron) if cron == "" { delete(opts.CmdConfigLists, cmdListName) @@ -291,21 +292,18 @@ func ReadConfig(opts *ConfigOpts) *ConfigOpts { 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) } - for c := range opts.Cmds { - if opts.executeCmds != nil && !contains(opts.executeCmds, c) { - delete(opts.Cmds, c) - } + // process commands + if err := processCmds(opts); err != nil { + log.Panic().Err(err).Send() } - if len(opts.executeLists) > 0 { - for l := range opts.CmdConfigLists { - if !contains(opts.executeLists, l) { - delete(opts.CmdConfigLists, l) - } + for l := range opts.CmdConfigLists { + if !contains(opts.executeLists, l) { + delete(opts.CmdConfigLists, l) } } @@ -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() + if err := opts.setupVault(); err != nil { log.Err(err).Send() } @@ -460,3 +443,94 @@ func GetVaultKey(str string, opts *ConfigOpts, log zerolog.Logger) string { } 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 +} diff --git a/pkg/backy/types.go b/pkg/backy/types.go index 1677d63..7032e1b 100644 --- a/pkg/backy/types.go +++ b/pkg/backy/types.go @@ -43,17 +43,24 @@ type ( } Command struct { + Name string `yaml:"name,omitempty"` // command to run Cmd string `yaml:"cmd"` // Possible values: script, scriptFile - // If blank, it is regualar command. - Type string `yaml:"type"` + // If blank, it is regular command. + Type string `yaml:"type,omitempty"` // host on which to run cmd 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. Not applicable when host is defined. @@ -123,7 +130,7 @@ type ( CmdListFile string // use command lists using cron - useCron bool + cronEnabled bool // Holds commands to execute for the exec command executeCmds []string // Holds lists to execute for the backup command @@ -188,4 +195,17 @@ type ( Commands []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 + } ) diff --git a/pkg/backy/utils.go b/pkg/backy/utils.go index 44d8beb..e1a61cb 100644 --- a/pkg/backy/utils.go +++ b/pkg/backy/utils.go @@ -56,10 +56,10 @@ func SetCmdsToSearch(cmds []string) BackyOptionFunc { } } -// UseCron enables the execution of command lists at specified times -func UseCron() BackyOptionFunc { +// cronEnabled enables the execution of command lists at specified times +func CronEnabled() BackyOptionFunc { return func(bco *ConfigOpts) { - bco.useCron = true + bco.cronEnabled = true } }