5 Commits

Author SHA1 Message Date
9d646297c7 fix: check for nil Command.Hooks in ExecuteHooks [#12] 2024-11-15 10:46:27 -06:00
bf8d261cf3 added timeout to golangci-lint command 2024-11-14 21:18:14 -06:00
686cd0019a Added Changie files 2024-11-14 21:13:40 -06:00
b7b002bd72 Added: Hooks for Commands.[name]: error, success, and final. Closes [#12]
Added Command.generateLogger() method.

Fixed: make command logger be used for errors, not just when running the command.
2024-11-14 21:10:49 -06:00
b8a63f39f5 add working command hooks
Some checks failed
ci/woodpecker/push/go-lint Pipeline failed
2024-11-11 22:44:28 -06:00
9 changed files with 232 additions and 73 deletions

View File

@ -0,0 +1,3 @@
kind: Added
body: Hooks for Commands.[name]. Error, success, and final. [#12]
time: 2024-11-14T21:01:48.823426401-06:00

View File

@ -0,0 +1,3 @@
kind: Fixed
body: make command logger be used for errors, not just when running the command
time: 2024-11-14T21:13:06.404134926-06:00

View File

@ -7,4 +7,4 @@ steps:
release:
image: golangci/golangci-lint:v1.53.3
commands:
- golangci-lint run -v
- golangci-lint run -v --timeout 5m

View File

@ -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()
// }

View File

@ -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()

View File

@ -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,42 +390,35 @@ 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
res := CmdListResults{}
for list := range jobs {
fieldsMap := make(map[string]interface{})
fieldsMap["list"] = list.Name
cmdLog := opts.Logger.Info()
var cmdLogger zerolog.Logger
var count int // count of how many commands have been executed
var cmdsRan []string // store the commands that have been executed
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()
cmdLogger := opts.Logger.With().
Str("backy-cmd", cmd).Str("Host", "local machine").
Logger()
if cmdToRun.Host != nil {
cmdLogger = opts.Logger.With().
Str("backy-cmd", cmd).Str("Host", *cmdToRun.Host).
Logger()
}
cmdLogger = cmdToRun.generateLogger(opts)
cmdLogger.Info().Fields(fieldsMap).Send()
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 +429,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{})
@ -451,23 +447,24 @@ func cmdListWorker(msgTemps *msgTemplates, jobs <-chan *CmdList, results chan<-
tmpErr := msgTemps.err.Execute(&errMsg, errStruct)
if tmpErr != nil {
opts.Logger.Err(tmpErr).Send()
cmdLogger.Err(tmpErr).Send()
}
notifySendErr := list.NotifyConfig.Send(context.Background(), fmt.Sprintf("List %s failed", list.Name), errMsg.String())
if notifySendErr != nil {
opts.Logger.Err(notifySendErr).Send()
cmdLogger.Err(notifySendErr).Send()
}
}
opts.Logger.Err(runOutErr).Send()
cmdLogger.Err(runOutErr).Send()
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,
@ -483,23 +480,21 @@ func cmdListWorker(msgTemps *msgTemplates, jobs <-chan *CmdList, results chan<-
tmpErr := msgTemps.success.Execute(&successMsg, successStruct)
if tmpErr != nil {
opts.Logger.Err(tmpErr).Send()
cmdLogger.Err(tmpErr).Send()
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()
cmdLogger.Err(err).Send()
}
}
} else {
cmdsRan = append(cmdsRan, cmd)
}
}
}
results <- "done"
results <- res.ErrCmd
}
}
@ -535,7 +530,23 @@ func (opts *ConfigOpts) RunListConfig(cron string) {
close(listChan)
for a := 1; a <= configListsLen; a++ {
<-results
l := <-results
opts.Logger.Debug().Msg(l)
if l != "" {
// execute error hooks
opts.Logger.Debug().Msg("hooks are working")
opts.Cmds[l].ExecuteHooks("error", opts)
} else {
// execute success hooks
opts.Cmds[l].ExecuteHooks("success", opts)
}
// execute final hooks
opts.Cmds[l].ExecuteHooks("final", opts)
}
opts.closeHostConnections()
@ -544,13 +555,18 @@ func (opts *ConfigOpts) RunListConfig(cron string) {
func (config *ConfigOpts) ExecuteCmds(opts *ConfigOpts) {
for _, cmd := range opts.executeCmds {
cmdToRun := opts.Cmds[cmd]
cmdLogger := opts.Logger.With().
Str("backy-cmd", cmd).
Logger()
cmdLogger := cmdToRun.generateLogger(opts)
_, runErr := cmdToRun.RunCmd(cmdLogger, opts)
if runErr != nil {
opts.Logger.Err(runErr).Send()
cmdToRun.ExecuteHooks("error", opts)
} else {
cmdToRun.ExecuteHooks("success", opts)
}
cmdToRun.ExecuteHooks("final", opts)
}
opts.closeHostConnections()
@ -593,3 +609,50 @@ func (c *ConfigOpts) closeHostConnections() {
}
}
}
func (cmd *Command) ExecuteHooks(hookType string, opts *ConfigOpts) {
if cmd.Hooks == nil {
return
}
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)
}
case "success":
for _, v := range cmd.Hooks.Success {
successCmd := opts.Cmds[v]
cmdLogger := opts.Logger.With().
Str("backy-cmd", v).
Logger()
successCmd.RunCmd(cmdLogger, opts)
}
case "final":
for _, v := range cmd.Hooks.Final {
finalCmd := opts.Cmds[v]
cmdLogger := opts.Logger.With().
Str("backy-cmd", v).
Logger()
finalCmd.RunCmd(cmdLogger, opts)
}
}
}
func (cmd *Command) generateLogger(opts *ConfigOpts) zerolog.Logger {
cmdLogger := opts.Logger.With().
Str("backy-cmd", cmd.Name).Str("Host", "local machine").
Logger()
if cmd.Host != nil {
cmdLogger = opts.Logger.With().
Str("backy-cmd", cmd.Name).Str("Host", *cmd.Host).
Logger()
}
return cmdLogger
}

View File

@ -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,14 +292,13 @@ 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 {
logging.ExitWithMSG(err.Error(), 1, &opts.Logger)
}
if len(opts.executeLists) > 0 {
@ -317,23 +317,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 +445,88 @@ 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 {
if cmd.Name == "" {
cmd.Name = cmdName
}
// println("Cmd.Name = " + cmd.Name)
hooks := cmd.Hooks
// resolve hooks
if hooks != nil {
processHookSuccess := processHooks(cmd, hooks.Error, opts, "error")
if processHookSuccess != nil {
return processHookSuccess
}
processHookSuccess = processHooks(cmd, hooks.Success, opts, "success")
if processHookSuccess != nil {
return processHookSuccess
}
processHookSuccess = processHooks(cmd, hooks.Final, opts, "final")
if processHookSuccess != nil {
return processHookSuccess
}
}
// 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:
//
// An error, if any, if the command is not found
func processHooks(cmd *Command, hooks []string, opts *ConfigOpts, hookType string) error {
// initialize hook type
var hookCmdFound bool
cmd.hookRefs = map[string]map[string]*Command{}
cmd.hookRefs[hookType] = map[string]*Command{}
for _, hook := range hooks {
var hookCmd *Command
// TODO: match by Command.Name
hookCmd, hookCmdFound = opts.Cmds[hook]
if !hookCmdFound {
return fmt.Errorf("error in command %s hook %s list: command %s not found", cmd.Name, hookType, hook)
}
cmd.hookRefs[hookType][hook] = hookCmd
// Recursive, decide if this is good
// if hookCmd.hookRefs == nil {
// }
// hookRef[hookType][h] = hookCmd
}
return nil
}

View File

@ -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"`
Success []string `yaml:"success,omitempty"`
Final []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
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
}
}