diff --git a/cmd/list.go b/cmd/list.go index ffb36f1..4f9bffd 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -14,20 +14,20 @@ import ( var ( listCmd = &cobra.Command{ Use: "list [command]", - Short: "Lists commands, lists, or hosts defined in config file.", - Long: "Backup lists commands or groups defined in config file.\nUse the --lists or -l flag to list the specified lists. If not flag is not given, all lists will be executed.", + Short: "List commands, lists, or hosts defined in config file.", + Long: "List commands, lists, or hosts defined in config file", } listCmds = &cobra.Command{ Use: "cmds [cmd1 cmd2 cmd3...]", - Short: "Lists commands, lists, or hosts defined in config file.", - Long: "Backup lists commands or groups defined in config file.\nUse the --lists or -l flag to list the specified lists. If not flag is not given, all lists will be executed.", + Short: "List commands defined in config file.", + Long: "List commands defined in config file", Run: ListCmds, } listCmdLists = &cobra.Command{ Use: "lists [list1 list2 ...]", - Short: "Lists commands, lists, or hosts defined in config file.", - Long: "Backup lists commands or groups defined in config file.\nUse the --lists or -l flag to list the specified lists. If not flag is not given, all lists will be executed.", + Short: "List lists defined in config file.", + Long: "List lists defined in config file", Run: ListCmdLists, } ) diff --git a/docs/content/cli/_index.md b/docs/content/cli/_index.md index ad62c95..df29c2e 100644 --- a/docs/content/cli/_index.md +++ b/docs/content/cli/_index.md @@ -19,10 +19,11 @@ Available Commands: cron Starts a scheduler that runs lists defined in config file. exec Runs commands defined in config file in order given. help Help about any command - list Lists commands, lists, or hosts defined in config file. + list List commands, lists, or hosts defined in config file. version Prints the version and exits Flags: + --cmdStdOut Pass to print command output to stdout -f, --config string config file to read from -h, --help help for backy --log-file string log file to write to @@ -48,6 +49,7 @@ Flags: -l, --lists stringArray Accepts comma-separated names of command lists to execute. Global Flags: + --cmdStdOut Pass to print command output to stdout -f, --config string config file to read from --log-file string log file to write to --s3-endpoint string Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable. @@ -66,6 +68,7 @@ Flags: -h, --help help for cron Global Flags: + --cmdStdOut Pass to print command output to stdout -f, --config string config file to read from --log-file string log file to write to --s3-endpoint string Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable. @@ -88,6 +91,7 @@ Flags: -h, --help help for exec Global Flags: + --cmdStdOut Pass to print command output to stdout -f, --config string config file to read from --log-file string log file to write to --s3-endpoint string Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable. @@ -111,6 +115,7 @@ Flags: -m, --hosts stringArray Accepts space-separated names of hosts. Specify multiple times for multiple hosts. Global Flags: + --cmdStdOut Pass to print command output to stdout -f, --config string config file to read from --log-file string log file to write to --s3-endpoint string Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable. @@ -131,6 +136,7 @@ Flags: -V, --vpre Output the version with v prefixed. Global Flags: + --cmdStdOut Pass to print command output to stdout -f, --config string config file to read from --log-file string log file to write to --s3-endpoint string Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable. @@ -140,20 +146,24 @@ Global Flags: ## list ``` -Backup lists commands or groups defined in config file. -Use the --lists or -l flag to list the specified lists. If not flag is not given, all lists will be executed. +List commands, lists, or hosts defined in config file Usage: - backy list [--list=list1,list2,... | -l list1, list2,...] [ -cmd cmd1 cmd2 cmd3...] [flags] + backy list [command] + +Available Commands: + cmds List commands defined in config file. + lists List lists defined in config file. Flags: - -c, --cmds strings Accepts comma-separated names of commands to list. - -h, --help help for list - -l, --lists strings Accepts comma-separated names of command lists to list. + -h, --help help for list Global Flags: + --cmdStdOut Pass to print command output to stdout -f, --config string config file to read from --log-file string log file to write to --s3-endpoint string Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable. -v, --verbose Sets verbose level + +Use "backy list [command] --help" for more information about a command. ``` diff --git a/docs/content/cli/exec.md b/docs/content/cli/exec.md index 7a6ccc6..7767380 100644 --- a/docs/content/cli/exec.md +++ b/docs/content/cli/exec.md @@ -7,13 +7,13 @@ The `exec` subcommand can do some things that the configuration file can't do ye `exec host` takes the following arguments: ```sh - -c, --commands strings Accepts comma-separated names of commands. + -c, --commands strings Accepts space-separated names of commands. -h, --help help for host - -m, --hosts strings Accepts comma-separated names of hosts. + -m, --hosts strings Accepts space-separated names of hosts. ``` The commands have to be defined in the config file. The hosts need to at least be in the ssh_config(5) file. ```sh -backy exec host [--commands=command1,command2, ... | -c command1,command2, ...] [--hosts=host1,hosts2, ... | -m host1,host2, ...] [flags] +backy exec host [--commands command1 -commands command2 ... | -c command1 -c command2 ...] [--hosts host1 --hosts hosts2 ... | -m host1 -c host2 ...] [flags] ``` diff --git a/go.mod b/go.mod index 90fea86..72bbb71 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ replace git.andrewnw.xyz/CyberShell/backy => /home/andrew/Projects/backy require ( github.com/aws/aws-sdk-go-v2/service/s3 v1.76.0 + github.com/dmarkham/enumer v1.5.11 github.com/go-co-op/gocron v1.37.0 github.com/hashicorp/vault/api v1.15.0 github.com/joho/godotenv v1.5.1 @@ -69,6 +70,7 @@ require ( github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/pascaldekloe/name v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect github.com/rs/xid v1.6.0 // indirect @@ -83,9 +85,11 @@ require ( go.mau.fi/util v0.8.4 // indirect go.uber.org/atomic v1.11.0 // indirect golang.org/x/exp v0.0.0-20250207012021-f9890c6ad9f3 // indirect - golang.org/x/net v0.34.0 // indirect + golang.org/x/mod v0.23.0 // indirect + golang.org/x/net v0.35.0 // indirect golang.org/x/sync v0.11.0 // indirect golang.org/x/sys v0.30.0 // indirect golang.org/x/text v0.22.0 // indirect golang.org/x/time v0.10.0 // indirect + golang.org/x/tools v0.30.0 // indirect ) diff --git a/go.sum b/go.sum index c72f226..6ce2098 100644 --- a/go.sum +++ b/go.sum @@ -30,6 +30,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dmarkham/enumer v1.5.11 h1:quorLCaEfzjJ23Pf7PB9lyyaHseh91YfTM/sAD/4Mbo= +github.com/dmarkham/enumer v1.5.11/go.mod h1:yixql+kDDQRYqcuBM2n9Vlt7NoT9ixgXhaXry8vmRg8= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= @@ -128,6 +130,8 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/nikoksr/notify v1.3.0 h1:UxzfxzAYGQD9a5JYLBTVx0lFMxeHCke3rPCkfWdPgLs= github.com/nikoksr/notify v1.3.0/go.mod h1:Xor2hMmkvrCfkCKvXGbcrESez4brac2zQjhd6U2BbeM= +github.com/pascaldekloe/name v1.0.0 h1:n7LKFgHixETzxpRv2R77YgPUFo85QHGZKrdaYm7eY5U= +github.com/pascaldekloe/name v1.0.0/go.mod h1:Z//MfYJnH4jVpQ9wkclwu2I2MkHmXTlT9wR5UZScttM= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -184,8 +188,10 @@ golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/exp v0.0.0-20250207012021-f9890c6ad9f3 h1:qNgPs5exUA+G0C96DrPwNrvLSj7GT/9D+3WMWUcUg34= golang.org/x/exp v0.0.0-20250207012021-f9890c6ad9f3/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= -golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= -golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= +golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -199,6 +205,8 @@ golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= +golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/pkg/backy/backy.go b/pkg/backy/backy.go index be3e11d..6ad21ed 100644 --- a/pkg/backy/backy.go +++ b/pkg/backy/backy.go @@ -47,22 +47,19 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([ outputArr []string // holds the output strings returned by processes ) - // Get the command type - // This must be done before concatenating the arguments - command = getCommandType(command) + // Getting the command type must be done before concatenating the arguments + command = getCommandTypeAndSetCommandInfo(command) for _, v := range command.Args { ArgsStr += fmt.Sprintf(" %s", v) } - // print the user's password if it is updated - if command.Type == "user" { + if command.Type == User { if command.UserOperation == "password" { cmdCtxLogger.Info().Str("password", command.UserPassword).Msg("user password to be updated") } } - // is host defined if command.Host != nil { outputArr, errSSH = command.RunCmdSSH(cmdCtxLogger, opts) if errSSH != nil { @@ -71,7 +68,7 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([ } else { // Handle package operations - if command.Type == "package" && command.PackageOperation == "checkVersion" { + if command.Type == Package && command.PackageOperation == "checkVersion" { cmdCtxLogger.Info().Str("package", command.PackageName).Msg("Checking package versions") // Execute the package version command @@ -88,6 +85,64 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([ } var localCMD *exec.Cmd + if command.Type == RemoteScript { + script, err := command.Fetcher.Fetch(command.Cmd) + if err != nil { + return nil, err + } + + if command.Shell == "" { + command.Shell = "sh" + } + localCMD = exec.Command(command.Shell, command.Args...) + injectEnvIntoLocalCMD(envVars, localCMD, cmdCtxLogger) + + cmdOutWriters = io.MultiWriter(&cmdOutBuf) + + if IsCmdStdOutEnabled() { + cmdOutWriters = io.MultiWriter(os.Stdout, &cmdOutBuf) + } + if command.OutputFile != "" { + file, err := os.Create(command.OutputFile) + if err != nil { + return nil, fmt.Errorf("error creating output file: %w", err) + } + defer file.Close() + cmdOutWriters = io.MultiWriter(file, &cmdOutBuf) + + if IsCmdStdOutEnabled() { + cmdOutWriters = io.MultiWriter(os.Stdout, file, &cmdOutBuf) + } + + } + + localCMD.Stdin = bytes.NewReader(script) + localCMD.Stdout = cmdOutWriters + localCMD.Stderr = cmdOutWriters + + cmdCtxLogger.Info().Str("Command", fmt.Sprintf("Running remoteScript %s on local machine in %s", command.Cmd, command.Shell)).Send() + err = localCMD.Run() + if err != nil { + return nil, fmt.Errorf("error running remote script: %w", err) + } + + outScanner := bufio.NewScanner(&cmdOutBuf) + + for outScanner.Scan() { + outMap := make(map[string]interface{}) + outMap["cmd"] = command.Cmd + outMap["output"] = outScanner.Text() + + if str, ok := outMap["output"].(string); ok { + outputArr = append(outputArr, str) + } + if command.OutputToLog { + cmdCtxLogger.Info().Fields(outMap).Send() + } + } + return outputArr, nil + } + var err error if command.Shell != "" { cmdCtxLogger.Info().Str("Command", fmt.Sprintf("Running command %s on local machine in %s", command.Name, command.Shell)).Send() @@ -101,7 +156,7 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([ cmdCtxLogger.Info().Str("Command", fmt.Sprintf("Running command %s on local machine", command.Name)).Send() // execute package commands in a shell - if command.Type == "package" { + if command.Type == Package { cmdCtxLogger.Info().Str("package", command.PackageName).Msg("Executing package command") ArgsStr = fmt.Sprintf("%s %s", command.Cmd, ArgsStr) localCMD = exec.Command("/bin/sh", "-c", ArgsStr) diff --git a/pkg/backy/config.go b/pkg/backy/config.go index 0ac825e..605b72b 100644 --- a/pkg/backy/config.go +++ b/pkg/backy/config.go @@ -578,7 +578,7 @@ func processCmds(opts *ConfigOpts) error { } // Parse package commands - if cmd.Type == "package" { + if cmd.Type == Package { if cmd.PackageManager == "" { return fmt.Errorf("package manager is required for package command %s", cmd.PackageName) } @@ -603,7 +603,7 @@ func processCmds(opts *ConfigOpts) error { } // Parse user commands - if cmd.Type == "user" { + if cmd.Type == User { if cmd.Username == "" { return fmt.Errorf("username is required for user command %s", cmd.Name) } @@ -630,12 +630,24 @@ func processCmds(opts *ConfigOpts) error { } - if cmd.Type == "remoteScript" { + if cmd.Type == RemoteScript { + var fetchErr error if !isRemoteURL(cmd.Cmd) { return fmt.Errorf("remoteScript command %s must be a remote resource", cmdName) } + cmd.Fetcher, fetchErr = remotefetcher.NewRemoteFetcher(cmd.Cmd, opts.Cache, remotefetcher.WithFileType("script")) + if fetchErr != nil { + return fmt.Errorf("error initializing remote fetcher for remoteScript: %v", fetchErr) + } } + if cmd.OutputFile != "" { + var err error + cmd.OutputFile, err = resolveDir(cmd.OutputFile) + if err != nil { + return err + } + } } return nil } diff --git a/pkg/backy/list.go b/pkg/backy/list.go index 82d3293..06226db 100644 --- a/pkg/backy/list.go +++ b/pkg/backy/list.go @@ -64,6 +64,11 @@ func (opts *ConfigOpts) ListCommand(cmd string) { println() } + if cmdInfo.Type.String() != "" { + print("Type: ", cmdInfo.Type.String()) + println() + } + } else { fmt.Printf("Command %s not found. Check spelling.\n", cmd) diff --git a/pkg/backy/ssh.go b/pkg/backy/ssh.go index f0a11bb..f711e26 100644 --- a/pkg/backy/ssh.go +++ b/pkg/backy/ssh.go @@ -252,7 +252,6 @@ func (remoteHost *Host) GetPort() { // port specifed? // port will be 0 if missing from backy config if port == "0" { - // get port from specified SSH config file port, _ = remoteHost.SSHConfigFile.SshConfigFile.Get(remoteHost.Host, "Port") if port == "" { @@ -315,7 +314,6 @@ func (remoteHost *Host) ConnectThroughBastion(log zerolog.Logger) (*ssh.Client, return nil, err } - // sClient is an ssh client connected to the service host, through the bastion host. sClient := ssh.NewClient(ncc, chans, reqs) return sClient, nil @@ -480,7 +478,6 @@ func (remoteConfig *Host) GetProxyJumpConfig(hosts map[string]*Host, opts *Confi return nil } -// RunCmdSSH runs commands over SSH. func (command *Command) RunCmdSSH(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([]string, error) { var ( ArgsStr string @@ -492,10 +489,8 @@ func (command *Command) RunCmdSSH(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) env: command.Environment, } ) - // Get the command type - // This must be done before concatenating the arguments - command.Type = strings.TrimSpace(command.Type) - command = getCommandType(command) + // Getting the command type must be done before concatenating the arguments + command = getCommandTypeAndSetCommandInfo(command) // Prepare command arguments for _, v := range command.Args { @@ -505,7 +500,7 @@ func (command *Command) RunCmdSSH(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) cmdCtxLogger.Info(). Str("Command", command.Name). Str("Host", *command.Host). - Msgf("Running %s on host %s", getCommandTypeLabel(command.Type), *command.Host) + Msgf("Running %s on host %s", getCommandTypeAndSetCommandInfoLabel(command.Type), *command.Host) // cmdCtxLogger.Debug().Str("cmd", command.Cmd).Strs("args", command.Args).Send() @@ -536,11 +531,13 @@ func (command *Command) RunCmdSSH(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) // Handle command execution based on type switch command.Type { - case "script": + case Script: return command.runScript(commandSession, cmdCtxLogger, &cmdOutBuf) - case "scriptFile": + case RemoteScript: + return command.runRemoteScript(commandSession, cmdCtxLogger, &cmdOutBuf) + case ScriptFile: return command.runScriptFile(commandSession, cmdCtxLogger, &cmdOutBuf) - case "package": + case Package: if command.PackageOperation == "checkVersion" { commandSession.Stderr = nil // Execute the package version command remotely @@ -558,7 +555,7 @@ func (command *Command) RunCmdSSH(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) cmdCtxLogger.Debug().Str("cmd + args", ArgsStr).Send() // Run simple command if err := commandSession.Run(ArgsStr); err != nil { - return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, true), fmt.Errorf("error running command: %w", err) + return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.OutputToLog), fmt.Errorf("error running command: %w", err) } } default: @@ -570,11 +567,11 @@ func (command *Command) RunCmdSSH(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) cmdCtxLogger.Debug().Str("cmd + args", ArgsStr).Send() // Run simple command if err := commandSession.Run(ArgsStr); err != nil { - return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.GetOutput), fmt.Errorf("error running command: %w", err) + return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.OutputToLog), fmt.Errorf("error running command: %w", err) } } - return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, true), nil + return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.OutputToLog), nil } func checkPackageVersion(cmdCtxLogger zerolog.Logger, command *Command, commandSession *ssh.Session, cmdOutBuf bytes.Buffer) ([]string, error) { @@ -593,17 +590,17 @@ func checkPackageVersion(cmdCtxLogger zerolog.Logger, command *Command, commandS _, parseErr := parsePackageVersion(string(cmdOut), cmdCtxLogger, command, cmdOutBuf) if parseErr != nil { - return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.GetOutput), fmt.Errorf("error: package %s not listed: %w", command.PackageName, err) + return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.OutputToLog), fmt.Errorf("error: package %s not listed: %w", command.PackageName, err) } - return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.GetOutput), fmt.Errorf("error running %s: %w", ArgsStr, err) + return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.OutputToLog), fmt.Errorf("error running %s: %w", ArgsStr, err) } return parsePackageVersion(string(cmdOut), cmdCtxLogger, command, cmdOutBuf) } -// getCommandTypeLabel returns a human-readable label for the command type. -func getCommandTypeLabel(commandType string) string { - if commandType == "" { +// getCommandTypeAndSetCommandInfoLabel returns a human-readable label for the command type. +func getCommandTypeAndSetCommandInfoLabel(commandType CommandType) string { + if !commandType.IsACommandType() { return "command" } return fmt.Sprintf("%s command", commandType) @@ -644,7 +641,7 @@ func (command *Command) runScriptFile(session *ssh.Session, cmdCtxLogger zerolog return collectOutput(outputBuf, command.Name, cmdCtxLogger, true), fmt.Errorf("error waiting for shell: %w", err) } - return collectOutput(outputBuf, command.Name, cmdCtxLogger, true), nil + return collectOutput(outputBuf, command.Name, cmdCtxLogger, command.OutputToLog), nil } // prepareScriptBuffer prepares a buffer for inline scripts. @@ -688,6 +685,25 @@ func (command *Command) prepareScriptFileBuffer() (*bytes.Buffer, error) { return &buffer, nil } +// runRemoteScript handles the execution of remote scripts +func (command *Command) runRemoteScript(session *ssh.Session, cmdCtxLogger zerolog.Logger, outputBuf *bytes.Buffer) ([]string, error) { + script, err := command.Fetcher.Fetch(command.Cmd) + if err != nil { + return nil, err + } + if command.Shell == "" { + command.Shell = "sh" + } + session.Stdin = bytes.NewReader(script) + err = session.Run(command.Shell) + + if err != nil { + return collectOutput(outputBuf, command.Name, cmdCtxLogger, command.OutputToLog), fmt.Errorf("error running remote script: %w", err) + } + + return collectOutput(outputBuf, command.Name, cmdCtxLogger, command.OutputToLog), nil +} + // readFileToBuffer reads a file into a buffer. func readFileToBuffer(filePath string) (*bytes.Buffer, error) { resolvedPath, err := resolveDir(filePath) diff --git a/pkg/backy/types.go b/pkg/backy/types.go index 46bf90f..b49e23c 100644 --- a/pkg/backy/types.go +++ b/pkg/backy/types.go @@ -54,9 +54,8 @@ type ( // command to run Cmd string `yaml:"cmd"` - // Possible values: script, scriptFile - // If blank, it is regular command. - Type string `yaml:"type,omitempty"` + // See CommandType enum further down the page for acceptable values + Type CommandType `yaml:"type,omitempty"` // host on which to run cmd Host *string `yaml:"host,omitempty"` @@ -93,6 +92,10 @@ type ( ScriptEnvFile string `yaml:"scriptEnvFile"` + OutputToLog bool `yaml:"outputToLog,omitempty"` + + OutputFile string `yaml:"outputFile,omitempty"` + // BEGIN PACKAGE COMMAND FIELDS PackageManager string `yaml:"packageManager,omitempty"` @@ -116,6 +119,8 @@ type ( // FetchBeforeExecution determines if the remoteSource should be fetched before running FetchBeforeExecution bool `yaml:"fetchBeforeExecution,omitempty"` + Fetcher remotefetcher.RemoteFetcher + // BEGIN USER COMMAND FIELDS // Username specifies the username for user creation or related operations @@ -289,4 +294,15 @@ type ( ListName string // Name of the command list Error error // Error encountered, if any } + CommandType int +) + +//go:generate go run github.com/dmarkham/enumer -linecomment -yaml -text -json -type=CommandType +const ( + Default CommandType = iota // + Script // script + ScriptFile // scriptFile + RemoteScript // remoteScript + Package // package + User // user ) diff --git a/pkg/backy/utils.go b/pkg/backy/utils.go index 0500456..3e76c22 100644 --- a/pkg/backy/utils.go +++ b/pkg/backy/utils.go @@ -251,12 +251,12 @@ func expandEnvVars(backyEnv map[string]string, envVars []string) { } } -// getCommandType checks for command type and if the command has already been set +// getCommandTypeAndSetCommandInfo checks for command type and if the command has already been set // Checks for types package and user // Returns the modified Command with the package- or userManager command as Cmd and the package- or userOperation as args, plus any additional Args -func getCommandType(command *Command) *Command { +func getCommandTypeAndSetCommandInfo(command *Command) *Command { - if command.Type == "package" && !command.packageCmdSet { + if command.Type == Package && !command.packageCmdSet { command.packageCmdSet = true switch command.PackageOperation { case "install": @@ -270,7 +270,7 @@ func getCommandType(command *Command) *Command { } } - if command.Type == "user" && !command.userCmdSet { + if command.Type == User && !command.userCmdSet { command.userCmdSet = true switch command.UserOperation { case "add": diff --git a/pkg/remotefetcher/cache.go b/pkg/remotefetcher/cache.go index 51d046c..ec5abde 100644 --- a/pkg/remotefetcher/cache.go +++ b/pkg/remotefetcher/cache.go @@ -16,6 +16,7 @@ type CacheData struct { Hash string `yaml:"hash"` Path string `yaml:"path"` Type string `yaml:"type"` + URL string `yaml:"url"` } type Cache struct { @@ -101,13 +102,17 @@ func (c *Cache) AddDataToStore(hash string, cacheData CacheData) error { return c.saveToFile() } +// Set stores data on disk and in the cache file and returns the cache data +// The filepath of the data is the file name + a SHA256 hash of the URL func (c *Cache) Set(source, hash string, data []byte, dataType string) (CacheData, error) { c.mu.Lock() defer c.mu.Unlock() + sourceHash := HashURL(source) + fileName := filepath.Base(source) - path := filepath.Join(c.dir, fmt.Sprintf("%s-%s", fileName, hash)) + path := filepath.Join(c.dir, fmt.Sprintf("%s-%s", fileName, sourceHash)) if _, err := os.Stat(path); os.IsNotExist(err) { os.MkdirAll(c.dir, 0700) @@ -122,9 +127,10 @@ func (c *Cache) Set(source, hash string, data []byte, dataType string) (CacheDat Hash: hash, Path: path, Type: dataType, + URL: sourceHash, } - c.store[hash] = cacheData + c.store[sourceHash] = cacheData // Unlock before calling saveToFile to avoid double-locking c.mu.Unlock() @@ -184,3 +190,8 @@ func LoadMetadataFromFile(filePath string) ([]*CacheData, error) { return cacheData, nil } + +func HashURL(url string) string { + hash := sha256.Sum256([]byte(url)) + return hex.EncodeToString(hash[:]) +} diff --git a/pkg/remotefetcher/configfetcher.go b/pkg/remotefetcher/configfetcher.go index 221a776..f453198 100644 --- a/pkg/remotefetcher/configfetcher.go +++ b/pkg/remotefetcher/configfetcher.go @@ -57,11 +57,12 @@ func NewRemoteFetcher(source string, cache *Cache, options ...FetcherOption) (Re return nil, err } - hash := fetcher.Hash(data) - if cachedData, cacheMeta, exists := cache.Get(hash); exists { + URLHash := HashURL(source) + if cachedData, cacheMeta, exists := cache.Get(URLHash); exists { return &CachedFetcher{data: cachedData, path: cacheMeta.Path, dataType: cacheMeta.Type}, nil } + hash := fetcher.Hash(data) cacheData, err := cache.Set(source, hash, data, config.FileType) if err != nil { return nil, err diff --git a/release b/release index c2ee88b..9165ca4 100755 --- a/release +++ b/release @@ -1,5 +1,6 @@ #!/bin/bash set -eou pipefail +go generate ./... export CURRENT_TAG="$(go run backy.go version -V)" goreleaser -f .goreleaser/github.yml check goreleaser -f .goreleaser/gitea.yml check